[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.{py,pyx,pxd,pxi,h}]\nindent_size = 4\nindent_style = space\n\n[*.yml]\nindent_size = 2\nindent_style = space\n\n[edb_stat_statements/*.{c,h,l,y,pl,pm}]\nindent_style = tab\nindent_size = tab\ntab_width = 4\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\n---\n\n<!-- Please search existing issues to avoid creating duplicates. -->\n\n<!--\nFor the Gel Version: run `gel query 'select sys::get_version_as_str()'` from your project directory (or run `select sys::get_version_as_str();` in the Gel interactive shell).\nFor the Gel CLI Version: Run `gel --version` from anywhere\n-->\n\n- Gel Version:\n- Gel CLI Version:\n- OS Version:\n\nSteps to Reproduce:\n\n1.\n2.\n\n<!-- If the issue is about a query error, please also provide your schema -->\n\nSchema:\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Problem with Gel UI\n    url: https://github.com/geldata/gel-ui/issues\n    about: If you've found a bug or have a feature request for Gel UI, please open an issue in the gel-ui repo.\n  - name: Long question or idea\n    url: https://github.com/geldata/gel/discussions\n    about: Ask long-form questions and discuss ideas.\n  - name: Quick questions or chat\n    url: https://www.geldata.com/p/discord\n    about: Ask quick questions or simply chat on the Gel Discord server.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest a feature for Gel\n\n---\n\n<!-- Please search existing issues to avoid creating duplicates. -->\n\n<!-- Describe the feature you'd like to see implemented in Gel. -->\n"
  },
  {
    "path": ".github/Makefile",
    "content": ".PHONY: all\n\nROOT = $(dir $(realpath $(firstword $(MAKEFILE_LIST))))\n\nall: \\\n\tworkflows/build.nightly.yml \\\n\tworkflows/build.release.yml \\\n\tworkflows/build.testing.yml \\\n\tworkflows/build.dryrun.yml \\\n\tworkflows/build.ls-nightly.yml \\\n\tworkflows/tests.yml \\\n\tworkflows/tests.pool.yml \\\n\tworkflows/tests.managed-pg.yml \\\n\tworkflows/tests.ha.yml \\\n\tworkflows/tests.pg-versions.yml \\\n\tworkflows/tests.patches.yml \\\n\tworkflows/tests.inplace.yml \\\n\tworkflows/tests.inplace7x.yml \\\n\tworkflows/tests.reflection.yml \\\n\nworkflows/build.%.yml: workflows.src/build.%.tpl.yml workflows.src/build.%.targets.yml workflows.src/build.inc.yml\n\t$(ROOT)/workflows.src/render.py --workflow=build build.$* build.$*.targets.yml\n\nworkflows/tests.yml: workflows.src/tests.tpl.yml workflows.src/tests.targets.yml workflows.src/tests.inc.yml\n\t$(ROOT)/workflows.src/render.py --workflow=test tests tests.targets.yml\n\nworkflows/tests.%.yml: workflows.src/tests.%.tpl.yml workflows.src/tests.%.targets.yml workflows.src/tests.inc.yml\n\t$(ROOT)/workflows.src/render.py --workflow=test tests.$* tests.$*.targets.yml\n"
  },
  {
    "path": ".github/aws-aurora/.gitignore",
    "content": "/.terraform/\n/terraform.tfstate*\n/.terraform.lock.hcl\n/.terraform.tfstate.lock.info\n"
  },
  {
    "path": ".github/aws-aurora/main.tf",
    "content": "variable \"vpc_id\" {\n  description = \"VPC ID\"\n}\n\nvariable \"sg_id\" {\n  description = \"security group ID\"\n}\n\nvariable \"password\" {\n  description = \"password, provide through your ENV variables\"\n}\n\nmodule \"aurora\" {\n  source  = \"terraform-aws-modules/rds-aurora/aws\"\n  version = \"~> 5.0\"\n\n  name           = \"aws-aurora-instance\"\n  engine         = \"aurora-postgresql\"\n  engine_version = \"13.4\"\n  instance_type = \"db.r6g.large\"\n\n\n  vpc_id  = var.vpc_id\n  db_subnet_group_name\t= \"default\"\n\n  replica_count           = 1\n  create_security_group   = false\n  vpc_security_group_ids  = [var.sg_id]\n\n\n  storage_encrypted   = true\n  apply_immediately   = true\n\n  username                = \"edbtest\"\n  password                = var.password\n  create_random_password  = false\n\n  enabled_cloudwatch_logs_exports = [\"postgresql\"]\n  publicly_accessible = true\n  skip_final_snapshot = true\n\n  tags = {\n    Environment = \"dev\"\n    Terraform   = \"true\"\n  }\n}\n\noutput \"rds_cluster_endpoint\" {\n  description = \"The cluster endpoint\"\n  value       = module.aurora.rds_cluster_endpoint\n}\n"
  },
  {
    "path": ".github/aws-rds/.gitignore",
    "content": "/.terraform/\n/terraform.tfstate*\n"
  },
  {
    "path": ".github/aws-rds/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"terraform init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.terraform.io/hashicorp/aws\" {\n  version = \"3.31.0\"\n  hashes = [\n    \"h1:Wou3ZnO10ZvN+n1iwyuaxn3zyGMFj9KYL+9IFb0gGkw=\",\n    \"zh:07f5b2f4cfaa25e26a4062ac675e3e5aaf65bb21b94b8fd7f30d576398e7410f\",\n    \"zh:08a2154ad29ae130ea9e46948b7b332ec4b45321b4852b45ba60adcfd049f8d6\",\n    \"zh:35ed643c2b999021ad56b49f7d9d3a77c98d152477fe54b5c8a68f696bb1a0b7\",\n    \"zh:3a8dc51b4be1c04130fd76cda4280019020b276336d307e7074ad52f35d4fdda\",\n    \"zh:3c910c4f25e3ffd6d84f051c32161f03d1843753cd545e769757d7b42d654003\",\n    \"zh:5d23f316f89937cbda36207271bbe150f633298f96d4644fd02063fc6bf0c28f\",\n    \"zh:61fedb2915c5188c6550677a10acb955f32834bbe99ba0cafb2a118be282827b\",\n    \"zh:65076a6899c0781ce95064d47d587ad07f80becd1510e4c475e4554131caec09\",\n    \"zh:acca833c2d9985e46298323222285b370ea7cf5299b131dbdfc7c3e66fa32401\",\n    \"zh:c212cf8ba7fdf64e75accf7e745f76d2349b00553ebd928cc6cafbfda99d97b7\",\n    \"zh:cd3f5e89ac5f5cf3f8fed3aca4cc50261d537b60a3490feaddf9ba2f06e5e7aa\",\n  ]\n}\n"
  },
  {
    "path": ".github/aws-rds/main.tf",
    "content": "resource \"aws_db_instance\" \"default\" {\n  allocated_storage = 10\n  engine = \"postgres\"\n  engine_version = \"13.4\"\n  instance_class = \"db.m6g.large\"\n  name = \"edbtest\"\n  username = \"edbtest\"\n  password = var.password\n  parameter_group_name = \"default.postgres13\"\n  skip_final_snapshot = true\n  auto_minor_version_upgrade = false\n  publicly_accessible = true\n  vpc_security_group_ids = [var.sg_id]\n}\n"
  },
  {
    "path": ".github/aws-rds/outputs.tf",
    "content": "output \"db_instance_id\" {\n  value = aws_db_instance.default.id\n}\n\noutput \"db_instance_address\" {\n  value = aws_db_instance.default.address\n}\n"
  },
  {
    "path": ".github/aws-rds/variables.tf",
    "content": "variable \"sg_id\" {\n  description = \"security group ID\"\n}\n\nvariable \"password\" {\n  description = \"password, provide through your ENV variables\"\n}\n"
  },
  {
    "path": ".github/do-database/.gitignore",
    "content": "/.terraform/\n/terraform.tfstate*\n"
  },
  {
    "path": ".github/do-database/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"terraform init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.terraform.io/digitalocean/digitalocean\" {\n  version     = \"2.6.0\"\n  constraints = \"2.6.0\"\n  hashes = [\n    \"h1:P1C7e6RlhLpi6KuE/sMruDdM5zZisJwMuKGbnxg8tAw=\",\n    \"zh:088c2a4eb9579947d50d8bcd722e75f2f1839acae302c8d43133b1da9926dae3\",\n    \"zh:323ba833d011371ca6d953752b133c0acad6462176cd2f804077a5f9d892cd2e\",\n    \"zh:3fbc64f1fabe57b6df49511c0d8753f1bbf776d5824ba060a51961d2a4265097\",\n    \"zh:4c90a933e23288ee2db2228e4e30055882d91bed831c2191cbecd849b27e44cb\",\n    \"zh:62f1cf4c82e5fcaf1a17e39cb96638f006b303758813a6c5ecb08bc93cd93364\",\n    \"zh:68ad1354e9f925477dc41e658e84a4996ba662920bbc61a2680235b94811169b\",\n    \"zh:9119b573c59429c2dfacb7d95b39c4e021783b8281ecd68f1621ad4a17c112cd\",\n    \"zh:9c15e3660f2399c25ee3ad53bd54927a6529d1393a54f1e1c2a523e0369dea46\",\n    \"zh:bc88f68bf6a6b5e803734f06731e31d61a5977ed1a638bfe102a54094c4d4030\",\n    \"zh:c2b013a5d7e60b31211b0f8c0dd898840b8f1aa7225318da05def33b5edb9388\",\n    \"zh:e46e21f6ffa7aac11ade8ab4b87a28ac405ef40a35793cef1f1fd6db6d8e5a0a\",\n    \"zh:e879643369e03abc192fbcf7ab06611bb8f36d37ceb5641ba05d58869f10ab7c\",\n    \"zh:ee9b56400e545ce1805842b795179a004313b8a947bd8f3490f5c5a0cb7703e5\",\n    \"zh:fb44861ae0b58b594aa4e565e0ed06bce939753b14a20b4abd3e8276e839e7a7\",\n  ]\n}\n"
  },
  {
    "path": ".github/do-database/main.tf",
    "content": "terraform {\n  required_providers {\n    digitalocean = {\n      source = \"digitalocean/digitalocean\"\n      version = \"2.6.0\"\n    }\n  }\n}\n\nvariable \"do_token\" {}\n\nprovider \"digitalocean\" {\n\n  token = var.do_token\n}\n\nresource \"digitalocean_database_cluster\" \"default\" {\n  name = \"edbtest\"\n  engine     = \"pg\"\n  version    = \"13\"\n  size       = \"db-s-4vcpu-8gb\"\n  region     = \"nyc1\"\n  node_count = 1\n}\n"
  },
  {
    "path": ".github/do-database/outputs.tf",
    "content": "output \"db_instance_address\" {\n  value = digitalocean_database_cluster.default.host\n}\n\noutput \"db_instance_port\" {\n  value = digitalocean_database_cluster.default.port\n}\n\noutput \"db_instance_user\" {\n  value = digitalocean_database_cluster.default.user\n}\n\noutput \"db_instance_password\" {\n  value = digitalocean_database_cluster.default.password\n  sensitive = true\n}\n\noutput \"db_instance_database\" {\n  value = digitalocean_database_cluster.default.database\n}\n"
  },
  {
    "path": ".github/gcp-cloud-sql/.gitignore",
    "content": "/.terraform/\n/terraform.tfstate*\n"
  },
  {
    "path": ".github/gcp-cloud-sql/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"terraform init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.terraform.io/hashicorp/google\" {\n  version = \"3.62.0\"\n  hashes = [\n    \"h1:FgfQz6EhKglcoU7vu1srYqEQFXy1Dti9MoZCxW8HL/w=\",\n    \"zh:26e44482924c9d22624054dcebf23c89b102aee6b5c66675747cf2f7274cf703\",\n    \"zh:518ebd73eb8f286f60a0c74970cd4e06883962c4af57f2899bc790d89e04038f\",\n    \"zh:814036d49d5034cf26fd2239fc57075b42982e1f76ab703fa1cd7609802d979f\",\n    \"zh:822dce72d1a77e1418b0e9187b4fe6f3e47b38ea5e51b81e5912074a8be3a7b7\",\n    \"zh:981fc6780e1e9c756390727b94ebd822490f7504a05a26c818922da5635ff9b8\",\n    \"zh:9a1a7e76ac6c37922261bdb148052fcdcbaf1f521ade68e26b430c106f1974b1\",\n    \"zh:cb67b6abed58b6d1b789a72690154fcf35707f65c3fca1936bf72c0c819a03dd\",\n    \"zh:cb87e8425b0eb97d80627243a37a67f0f81640499416ad32f1b786cc9d78c6f4\",\n    \"zh:d3754c3f05dc9bbd4933b45676144c2dd456de775bff0252c058e0cff94b8f21\",\n    \"zh:e2d8b0a78d698e92035e339782b299108d6021768ea4d97d150106c524f84ca1\",\n  ]\n}\n"
  },
  {
    "path": ".github/gcp-cloud-sql/main.tf",
    "content": "variable \"password\" {}\n\nprovider \"google\" {\n  region  = \"us-east1\"\n}\n\nresource \"google_sql_database_instance\" \"default\" {\n  database_version    = \"POSTGRES_13\"\n  deletion_protection = false\n\n  settings {\n    tier = \"db-custom-1-3840\"\n\n    ip_configuration {\n      authorized_networks {\n        value = \"0.0.0.0/0\"\n      }\n    }\n  }\n}\n\nresource \"google_sql_user\" \"users\" {\n  instance        = google_sql_database_instance.default.name\n  name            = \"postgres\"\n  password        = var.password\n  deletion_policy = \"ABANDON\"\n}\n\noutput \"db_instance_address\" {\n  value = google_sql_database_instance.default.public_ip_address\n}\n"
  },
  {
    "path": ".github/heroku-postgres/.gitignore",
    "content": "/.terraform/\n/terraform.tfstate*\n/.terraform.lock.hcl\n/.terraform.tfstate.lock.info\n"
  },
  {
    "path": ".github/heroku-postgres/main.tf",
    "content": "terraform {\n  required_providers {\n    heroku = {\n      source  = \"heroku/heroku\"\n      version = \"~> 4.0\"\n    }\n  }\n}\n\nresource \"heroku_addon\" \"database\" {\n  app  = \"edgedb-heroku-ci\"\n  plan = \"heroku-postgresql:mini\"\n  config = {\n    version = \"14\"\n  }\n}\n\noutput \"heroku_postgres_dsn\" {\n  value     = heroku_addon.database.config_var_values.DATABASE_URL\n  sensitive = true\n}\n"
  },
  {
    "path": ".github/scripts/docs/preview-deploy.js",
    "content": "const DOCS_SITE_REPO = {\n  org: \"edgedb\",\n  repo: \"edgedb.com\",\n  ref: \"master\",\n};\n\nmodule.exports = async ({ github, context }) => {\n  const { VERCEL_TOKEN, VERCEL_TEAM_ID } = process.env;\n\n  if (!VERCEL_TOKEN || !VERCEL_TEAM_ID) {\n    throw new Error(\n      `cannot run docs preview deploy workflow, ` +\n        `VERCEL_TOKEN or VERCEL_TEAM_ID secrets are missing`\n    );\n  }\n\n  const prBranch = context.payload.pull_request.head.ref;\n  const commitSHA = context.payload.pull_request.head.sha;\n  const shortCommitSHA = commitSHA.slice(0, 8);\n\n  const existingComments = (\n    await github.rest.issues.listComments({\n      owner: context.repo.owner,\n      repo: context.repo.repo,\n      issue_number: context.issue.number,\n    })\n  ).data;\n\n  const commentHeader = `### Docs preview deploy\\n`;\n  let commentMessage = commentHeader;\n\n  let updateComment = existingComments.find(\n    (c) =>\n      c.performed_via_github_app?.slug === \"github-actions\" &&\n      c.body?.startsWith(commentHeader)\n  );\n\n  let deploymentError = null;\n  let deployment;\n  try {\n    deployment = await vercelFetch(\"https://api.vercel.com/v13/deployments\", {\n      name: \"edgedb-docs\",\n      gitSource: {\n        type: \"github\",\n        ...DOCS_SITE_REPO,\n      },\n      projectSettings: {\n        buildCommand: `EDGEDB_REPO_BRANCH=${prBranch} EDGEDB_REPO_SHA=${commitSHA} yarn vercel-build`,\n      },\n    });\n\n    commentMessage += `\\n🔄 Deploying docs preview for commit ${shortCommitSHA}:\\n\\n<https://${deployment.url}>`;\n  } catch (e) {\n    deploymentError = e;\n    commentMessage += `\\n❌ Failed to deploy docs preview for commit ${shortCommitSHA}:\\n\\n\\`\\`\\`\\n${e.message}\\n\\`\\`\\``;\n  }\n\n  commentMessage += `\\n\\n(Last updated: ${formatDatetime(new Date())})`;\n\n  if (updateComment) {\n    await github.rest.issues.updateComment({\n      owner: context.repo.owner,\n      repo: context.repo.repo,\n      comment_id: updateComment.id,\n      body: commentMessage,\n    });\n  } else {\n    updateComment = (\n      await github.rest.issues.createComment({\n        owner: context.repo.owner,\n        repo: context.repo.repo,\n        issue_number: context.issue.number,\n        body: commentMessage,\n      })\n    ).data;\n  }\n\n  if (deploymentError) {\n    throw new Error(`Docs preview deployment failed: ${e.message}`);\n  }\n\n  let i = 0;\n  while (i < 40) {\n    await sleep(15_000);\n    i++;\n\n    const status = (\n      await vercelFetch(\n        `https://api.vercel.com/v13/deployments/${deployment.id}`\n      )\n    ).status;\n\n    const latestComment = await github.rest.issues.getComment({\n      owner: context.repo.owner,\n      repo: context.repo.repo,\n      comment_id: updateComment.id,\n    });\n\n    if (!latestComment.data.body.includes(shortCommitSHA)) {\n      console.log(\"Skipping further updates, new deployment has started\");\n      return;\n    }\n\n    if (status === \"READY\" || status === \"ERROR\" || status === \"CANCELED\") {\n      await github.rest.issues.updateComment({\n        owner: context.repo.owner,\n        repo: context.repo.repo,\n        comment_id: updateComment.id,\n        body: `${commentHeader}${\n          status === \"READY\"\n            ? `\\n✅ Successfully deployed docs preview for commit ${shortCommitSHA}:`\n            : `\\n❌ Docs preview deployment ${\n                status === \"CANCELED\" ? \"was canceled\" : \"failed\"\n              } for commit ${shortCommitSHA}:`\n        }\\n\\n<https://${deployment.url}>\\n\\n(Last updated: ${formatDatetime(\n          new Date()\n        )})`,\n      });\n      if (status !== \"READY\") {\n        throw new Error(\n          `Docs preview deployment failed with status ${status}: https://${deployment.url}`\n        );\n      }\n      return;\n    }\n  }\n\n  await github.rest.issues.updateComment({\n    owner: context.repo.owner,\n    repo: context.repo.repo,\n    comment_id: updateComment.id,\n    body: `${commentHeader}\n❌ Timed out waiting for deployment status to succeed or fail for commit ${shortCommitSHA}:\\n\\n<https://${\n      deployment.url\n    }>\\n\\n(Last updated: ${formatDatetime(new Date())})`,\n  });\n  throw new Error(\"Timed out waiting for deployment status to succeed or fail\");\n};\n\nasync function vercelFetch(url, body) {\n  const { VERCEL_TOKEN, VERCEL_TEAM_ID } = process.env;\n  const _url = new URL(url);\n  url = `${_url.origin}${_url.pathname}?${new URLSearchParams({\n    teamId: VERCEL_TEAM_ID,\n  })}`;\n\n  let res;\n  try {\n    res = await fetch(url, {\n      body: body ? JSON.stringify(body) : undefined,\n      headers: {\n        Authorization: `Bearer ${VERCEL_TOKEN}`,\n        \"Content-Type\": body ? \"application/json\" : undefined,\n      },\n      method: body ? \"post\" : \"get\",\n    });\n  } catch (e) {\n    throw new Error(`vercel api request failed: ${e}`);\n  }\n\n  if (res.ok) {\n    return await res.json();\n  } else {\n    let body;\n    try {\n      body = await res.text();\n    } catch (e) {\n      // ignore\n    }\n    throw new Error(\n      `vercel api request failed: ${res.status} ${res.statusText}, ${body}`\n    );\n  }\n}\n\nfunction formatDatetime(date) {\n  return date.toLocaleString(\"en-US\", {\n    year: \"numeric\",\n    month: \"short\",\n    day: \"numeric\",\n    hour: \"numeric\",\n    minute: \"numeric\",\n    second: \"numeric\",\n    hourCycle: \"h24\",\n    timeZoneName: \"short\",\n  });\n}\n\nfunction sleep(milliseconds) {\n  return new Promise((resolve) => setTimeout(resolve, milliseconds));\n}\n"
  },
  {
    "path": ".github/scripts/patches/compute-ipu-versions.py",
    "content": "# Compute prior minor versions to test upgrading from\n\n\nimport json\nimport os\nimport pathlib\nimport re\nimport sys\nfrom urllib import request\n\nsys.path.append(str(pathlib.Path(__file__).parent.parent.parent.parent))\n\nimport edb.buildmeta\n\nbase = 'https://packages.geldata.com'\nu = f'{base}/archive/.jsonindexes/x86_64-unknown-linux-gnu.json'\ndata = json.loads(request.urlopen(u).read())\n\nu = f'{base}/archive/.jsonindexes/x86_64-unknown-linux-gnu.testing.json'\ndata_testing = json.loads(request.urlopen(u).read())\n\nversion = edb.buildmeta.EDGEDB_MAJOR_VERSION - 1\n\nversions = []\nprerelease_versions = []\nfor obj in data['packages'] + data_testing['packages']:\n    if (\n        obj['basename'] == 'gel-server'\n        and obj['version_details']['major'] == version\n        and (\n            not obj['version_details']['prerelease']\n            or obj['version_details']['prerelease'][0]['phase'] in ('beta', 'rc')\n        )\n    ):\n        l = (\n            versions if not obj['version_details']['prerelease']\n            else prerelease_versions\n        )\n        l.append((\n            obj['version'],\n            obj['basename'],\n            base + obj['installrefs'][0]['ref'],\n        ))\n\nprerelease_versions.sort(key=lambda x: x[0])\nif not versions:\n    # Some 7.x prerelease versions are busted due to having taken\n    # extension patches that we don't intend to bundle with 8.x.\n    # Only look at the last.\n    versions = prerelease_versions[-1:]\n\nversions.sort(key=lambda x: x[0])\nif len(versions) > 3:\n    # We want to try 6.0 and 6.2\n    versions = [versions[0], versions[2], versions[-1]]\nelif len(versions) > 1:\n    versions = [versions[0], versions[-1]]\n\nmatrix = {\n    \"include\": [\n        {\"edgedb-version\": v, \"edgedb-url\": url, \"edgedb-basename\": base}\n        for v, base, url in versions\n    ]\n}\n\nprint(\"matrix:\", matrix)\nif output := os.getenv('GITHUB_OUTPUT'):\n    with open(output, 'a') as f:\n        print(f'matrix={json.dumps(matrix)}', file=f)\n"
  },
  {
    "path": ".github/scripts/patches/compute-versions.py",
    "content": "# Compute prior minor versions to test upgrading from\n\nimport json\nimport os\nimport re\nfrom urllib import request\n\nbase = 'https://packages.edgedb.com'\nu = f'{base}/archive/.jsonindexes/x86_64-unknown-linux-gnu.json'\ndata = json.loads(request.urlopen(u).read())\n\nu = f'{base}/archive/.jsonindexes/x86_64-unknown-linux-gnu.testing.json'\ndata_testing = json.loads(request.urlopen(u).read())\n\n\nbranch = os.getenv('GITHUB_BASE_REF') or os.getenv('GITHUB_REF_NAME')\nprint(\"BRANCH\", branch)\nversion = int(re.findall(r'\\d+', branch)[0])\n\nversions = []\nfor obj in data['packages'] + data_testing['packages']:\n    if (\n        obj['basename'] in {'gel-server', 'edgedb-server'}\n        and obj['version_details']['major'] == version\n        and (\n            not obj['version_details']['prerelease']\n            or obj['version_details']['prerelease'][0]['phase'] in ('beta', 'rc')\n        )\n    ):\n        versions.append((\n            obj['version'],\n            obj['basename'],\n            base + obj['installrefs'][0]['ref'],\n        ))\n\nmatrix = {\n    \"include\": [\n        {\"edgedb-version\": v, \"edgedb-url\": url, \"edgedb-basename\": base, \"make-dbs\": mk}\n        for v, base, url in versions\n        for mk in [True, False]\n    ]\n}\n\nprint(\"matrix:\", matrix)\nif output := os.getenv('GITHUB_OUTPUT'):\n    with open(output, 'a') as f:\n        print(f'matrix={json.dumps(matrix)}', file=f)\n"
  },
  {
    "path": ".github/scripts/patches/create-databases.py",
    "content": "# Create databases on the older edgedb version\n\nimport edgedb\nimport subprocess\nimport sys\n\ncmd = [\n    sys.argv[1], '-D' 'test-dir',\n    '--testmode', '--security', 'insecure_dev_mode', '--port', '10000',\n]\nproc = subprocess.Popen(cmd)\n\ntry:\n    db = edgedb.create_client(\n        host='localhost', port=10000, tls_security='insecure'\n    )\n    for name in [\n        'json', 'functions', 'expressions', 'casts', 'policies', 'vector',\n        'scope', 'httpextauth',\n    ]:\n        db.execute(f'create database {name};')\n\n    # For the scope database, let's actually migrate to it.  This\n    # will test that the migrations can still work after the upgrade.\n    db2 = edgedb.create_client(\n        host='localhost', port=10000, tls_security='insecure', database='scope'\n    )\n    with open(\"tests/schemas/cards.esdl\") as f:\n        body = f.read()\n    db2.execute(f'''\n        START MIGRATION TO {{\n            module default {{\n                {body}\n            }}\n        }};\n        POPULATE MIGRATION;\n        COMMIT MIGRATION;\n    ''')\n\n    # Put something in the query cache\n    db2.query(r'''\n        SELECT User {\n            name,\n            id\n        }\n        ORDER BY User.name;\n    ''')\n\n    db2.close()\n\n    # Compile a query from the CLI.\n    # (At one point, having a cached query with proto version 1 caused\n    # trouble...)\n    cli_base = [\n        'gel',\n        'query',\n        '-H',\n        'localhost',\n        '-P',\n        '10000',\n        '-b',\n        'json',\n        '--tls-security',\n        'insecure',\n    ]\n    subprocess.run(\n        [*cli_base, 'select 1+1'],\n        check=True,\n    )\n\n    # For the httpextauth database, create the proper extensions, so\n    # that patching of the auth extension in place can get tested.\n    db2 = edgedb.create_client(\n        host='localhost', port=10000, tls_security='insecure',\n        database='httpextauth'\n    )\n    db2.execute(f'''\n        create extension pgcrypto;\n        create extension auth;\n    ''')\n    db2.close()\n\nfinally:\n    proc.terminate()\n    proc.wait()\n"
  },
  {
    "path": ".github/scripts/patches/test-downgrade.py",
    "content": "# Test downgrading a database after an upgrade\n\nimport edgedb\nimport os\nimport subprocess\nimport json\n\nversion = os.getenv('EDGEDB_VERSION')\ncmd = [\n    f'edgedb-server-{version}/bin/edgedb-server', '-D' 'test-dir',\n    '--testmode', '--security', 'insecure_dev_mode', '--port', '10000',\n]\nproc = subprocess.Popen(cmd)\n\ndb = edgedb.create_client(\n    host='localhost', port=10000, tls_security='insecure',\n    database='policies',\n)\n\ntry:\n    # Test that a basic query works\n    res = json.loads(db.query_json('''\n        select Issue { name, number, watchers: {name} }\n        filter .number = \"1\"\n    '''))\n    expected = [{\n        \"name\": \"Release EdgeDB\",\n        \"number\": \"1\",\n        \"watchers\": [{\"name\": \"Yury\"}],\n    }]\n\n    assert res == expected, res\nfinally:\n    proc.terminate()\n    proc.wait()\n"
  },
  {
    "path": ".github/workflows/.gitattributes",
    "content": "*.yml    linguist-generated=true\n"
  },
  {
    "path": ".github/workflows/build.dryrun.yml",
    "content": "name: Package Build Dry Run\n\non:\n  workflow_dispatch:\n    inputs:\n      gelpkg_ref:\n        description: \"gel-pkg git ref used to build the packages\"\n        default: \"master\"\n      metapkg_ref:\n        description: \"metapkg git ref used to build the packages\"\n        default: \"master\"\n\njobs:\n  prep:\n    runs-on: ubuntu-latest\n\n    outputs:\n\n      if_debian_buster_x86_64: ${{ steps.scm.outputs.if_debian_buster_x86_64 }}\n\n      if_debian_buster_aarch64: ${{ steps.scm.outputs.if_debian_buster_aarch64 }}\n\n      if_debian_bullseye_x86_64: ${{ steps.scm.outputs.if_debian_bullseye_x86_64 }}\n\n      if_debian_bullseye_aarch64: ${{ steps.scm.outputs.if_debian_bullseye_aarch64 }}\n\n      if_debian_bookworm_x86_64: ${{ steps.scm.outputs.if_debian_bookworm_x86_64 }}\n\n      if_debian_bookworm_aarch64: ${{ steps.scm.outputs.if_debian_bookworm_aarch64 }}\n\n      if_ubuntu_focal_x86_64: ${{ steps.scm.outputs.if_ubuntu_focal_x86_64 }}\n\n      if_ubuntu_focal_aarch64: ${{ steps.scm.outputs.if_ubuntu_focal_aarch64 }}\n\n      if_ubuntu_jammy_x86_64: ${{ steps.scm.outputs.if_ubuntu_jammy_x86_64 }}\n\n      if_ubuntu_jammy_aarch64: ${{ steps.scm.outputs.if_ubuntu_jammy_aarch64 }}\n\n      if_ubuntu_noble_x86_64: ${{ steps.scm.outputs.if_ubuntu_noble_x86_64 }}\n\n      if_ubuntu_noble_aarch64: ${{ steps.scm.outputs.if_ubuntu_noble_aarch64 }}\n\n      if_centos_8_x86_64: ${{ steps.scm.outputs.if_centos_8_x86_64 }}\n\n      if_centos_8_aarch64: ${{ steps.scm.outputs.if_centos_8_aarch64 }}\n\n      if_rockylinux_9_x86_64: ${{ steps.scm.outputs.if_rockylinux_9_x86_64 }}\n\n      if_rockylinux_9_aarch64: ${{ steps.scm.outputs.if_rockylinux_9_aarch64 }}\n\n      if_linux_x86_64: ${{ steps.scm.outputs.if_linux_x86_64 }}\n\n      if_linux_aarch64: ${{ steps.scm.outputs.if_linux_aarch64 }}\n\n      if_linuxmusl_x86_64: ${{ steps.scm.outputs.if_linuxmusl_x86_64 }}\n\n      if_linuxmusl_aarch64: ${{ steps.scm.outputs.if_linuxmusl_aarch64 }}\n\n      if_macos_x86_64: ${{ steps.scm.outputs.if_macos_x86_64 }}\n\n      if_macos_aarch64: ${{ steps.scm.outputs.if_macos_aarch64 }}\n\n\n    steps:\n    - uses: actions/checkout@v4\n\n\n    - name: Determine SCM revision\n      id: scm\n      shell: bash\n      run: |\n        rev=$(git rev-parse HEAD)\n        jq_filter='.packages[] | select(.basename == \"gel-server\") | select(.architecture == $ARCH) | .version_details.metadata.scm_revision | . as $rev | select(($rev != null) and ($REV | startswith($rev)))'\n\n        key=\"debian-buster-x86_64\"\n        val=true\n\n\n        idx_file=buster.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"debian-buster-aarch64\"\n        val=true\n\n\n        idx_file=buster.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"debian-bullseye-x86_64\"\n        val=true\n\n\n        idx_file=bullseye.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"debian-bullseye-aarch64\"\n        val=true\n\n\n        idx_file=bullseye.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"debian-bookworm-x86_64\"\n        val=true\n\n\n        idx_file=bookworm.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"debian-bookworm-aarch64\"\n        val=true\n\n\n        idx_file=bookworm.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"ubuntu-focal-x86_64\"\n        val=true\n\n\n        idx_file=focal.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"ubuntu-focal-aarch64\"\n        val=true\n\n\n        idx_file=focal.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"ubuntu-jammy-x86_64\"\n        val=true\n\n\n        idx_file=jammy.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"ubuntu-jammy-aarch64\"\n        val=true\n\n\n        idx_file=jammy.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"ubuntu-noble-x86_64\"\n        val=true\n\n\n        idx_file=noble.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"ubuntu-noble-aarch64\"\n        val=true\n\n\n        idx_file=noble.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"centos-8-x86_64\"\n        val=true\n\n\n        idx_file=el8.nightly.json\n        url=https://packages.edgedb.com/rpm/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"centos-8-aarch64\"\n        val=true\n\n\n        idx_file=el8.nightly.json\n        url=https://packages.edgedb.com/rpm/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"rockylinux-9-x86_64\"\n        val=true\n\n\n        idx_file=el9.nightly.json\n        url=https://packages.edgedb.com/rpm/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"rockylinux-9-aarch64\"\n        val=true\n\n\n        idx_file=el9.nightly.json\n        url=https://packages.edgedb.com/rpm/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"linux-x86_64\"\n        val=true\n\n\n        idx_file=x86_64-unknown-linux-gnu.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"linux-aarch64\"\n        val=true\n\n\n        idx_file=aarch64-unknown-linux-gnu.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"linuxmusl-x86_64\"\n        val=true\n\n\n        idx_file=x86_64-unknown-linux-musl.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"linuxmusl-aarch64\"\n        val=true\n\n\n        idx_file=aarch64-unknown-linux-musl.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"macos-x86_64\"\n        val=true\n\n\n        idx_file=x86_64-unknown-linux-gnu.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"macos-aarch64\"\n        val=true\n\n\n        idx_file=aarch64-unknown-linux-gnu.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n\n\n  build-debian-buster-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_debian_buster_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-buster:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-buster-x86_64\n        path: artifacts/debian-buster\n\n  build-debian-buster-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_debian_buster_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-buster:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-buster-aarch64\n        path: artifacts/debian-buster\n\n  build-debian-bullseye-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_debian_bullseye_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-bullseye:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-bullseye-x86_64\n        path: artifacts/debian-bullseye\n\n  build-debian-bullseye-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_debian_bullseye_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-bullseye:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-bullseye-aarch64\n        path: artifacts/debian-bullseye\n\n  build-debian-bookworm-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_debian_bookworm_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-bookworm:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-bookworm-x86_64\n        path: artifacts/debian-bookworm\n\n  build-debian-bookworm-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_debian_bookworm_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-bookworm:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-bookworm-aarch64\n        path: artifacts/debian-bookworm\n\n  build-ubuntu-focal-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_ubuntu_focal_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-focal:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-focal-x86_64\n        path: artifacts/ubuntu-focal\n\n  build-ubuntu-focal-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_ubuntu_focal_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-focal:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-focal-aarch64\n        path: artifacts/ubuntu-focal\n\n  build-ubuntu-jammy-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_ubuntu_jammy_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-jammy:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-jammy-x86_64\n        path: artifacts/ubuntu-jammy\n\n  build-ubuntu-jammy-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_ubuntu_jammy_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-jammy:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-jammy-aarch64\n        path: artifacts/ubuntu-jammy\n\n  build-ubuntu-noble-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_ubuntu_noble_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-noble:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-noble-x86_64\n        path: artifacts/ubuntu-noble\n\n  build-ubuntu-noble-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_ubuntu_noble_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-noble:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-noble-aarch64\n        path: artifacts/ubuntu-noble\n\n  build-centos-8-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_centos_8_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-centos-8:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-centos-8-x86_64\n        path: artifacts/centos-8\n\n  build-centos-8-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_centos_8_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-centos-8:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-centos-8-aarch64\n        path: artifacts/centos-8\n\n  build-rockylinux-9-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_rockylinux_9_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-rockylinux-9:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-rockylinux-9-x86_64\n        path: artifacts/rockylinux-9\n\n  build-rockylinux-9-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_rockylinux_9_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-rockylinux-9:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-rockylinux-9-aarch64\n        path: artifacts/rockylinux-9\n\n  build-linux-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_linux_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linux-x86_64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_GENERIC: true\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n  build-linux-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_linux_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linux-aarch64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_GENERIC: true\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n  build-linuxmusl-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_linuxmusl_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linuxmusl-x86_64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_GENERIC: true\n        PKG_PLATFORM_LIBC: \"musl\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n  build-linuxmusl-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_linuxmusl_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linuxmusl-aarch64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_GENERIC: true\n        PKG_PLATFORM_LIBC: \"musl\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n  build-macos-x86_64:\n    runs-on: ['macos-13']\n    needs: prep\n\n    if: needs.prep.outputs.if_macos_x86_64 == 'true'\n\n\n    steps:\n    - name: Update Homebrew before installing Rust toolchain\n      run: |\n        # Homebrew renamed `rustup-init` to `rustup`:\n        #   https://github.com/Homebrew/homebrew-core/pull/177840\n        # But the GitHub Action runner is not updated with this change yet.\n        # This caused the later `brew update` in step `Build` to relink Rust\n        # toolchain executables, overwriting the custom toolchain installed by\n        # `dsherret/rust-toolchain-file`. So let's just run `brew update` early.\n        brew update\n\n    - uses: actions/checkout@v4\n      if: true\n      with:\n        sparse-checkout: |\n          rust-toolchain.toml\n        sparse-checkout-cone-mode: false\n\n    - name: Install Rust toolchain\n      uses: dsherret/rust-toolchain-file@v1\n      if: true\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      if: true\n      with:\n        python-version: \"3.12\"\n\n    - name: Set up NodeJS\n      uses: actions/setup-node@v4\n      if: true\n      with:\n        node-version: '20'\n\n    - name: Install dependencies\n      if: true\n      run: |\n        env HOMEBREW_NO_AUTO_UPDATE=1 brew install libmagic\n\n    - name: Install an alias\n      # This is probably not strictly needed, but sentencepiece build script reports\n      # errors without it.\n      if: true\n      run: |\n        printf '#!/bin/sh\\n\\nexec sysctl -n hw.logicalcpu' > /usr/local/bin/nproc\n        chmod +x /usr/local/bin/nproc\n\n    - name: Build\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_ARCH: \"x86_64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        BUILD_GENERIC: true\n        CMAKE_POLICY_VERSION_MINIMUM: '3.5'\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n      run: |\n        edgedb-pkg/integration/macos/build.sh\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-macos-x86_64\n        path: artifacts/macos-x86_64\n\n  build-macos-aarch64:\n    runs-on: ['macos-14']\n    needs: prep\n\n    if: needs.prep.outputs.if_macos_aarch64 == 'true'\n\n\n    steps:\n    - name: Update Homebrew before installing Rust toolchain\n      run: |\n        # Homebrew renamed `rustup-init` to `rustup`:\n        #   https://github.com/Homebrew/homebrew-core/pull/177840\n        # But the GitHub Action runner is not updated with this change yet.\n        # This caused the later `brew update` in step `Build` to relink Rust\n        # toolchain executables, overwriting the custom toolchain installed by\n        # `dsherret/rust-toolchain-file`. So let's just run `brew update` early.\n        brew update\n\n    - uses: actions/checkout@v4\n      if: true\n      with:\n        sparse-checkout: |\n          rust-toolchain.toml\n        sparse-checkout-cone-mode: false\n\n    - name: Install Rust toolchain\n      uses: dsherret/rust-toolchain-file@v1\n      if: true\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      if: true\n      with:\n        python-version: \"3.12\"\n\n    - name: Set up NodeJS\n      uses: actions/setup-node@v4\n      if: true\n      with:\n        node-version: '20'\n\n    - name: Install dependencies\n      if: true\n      run: |\n        env HOMEBREW_NO_AUTO_UPDATE=1 brew install libmagic\n\n    - name: Install an alias\n      # This is probably not strictly needed, but sentencepiece build script reports\n      # errors without it.\n      if: true\n      run: |\n        printf '#!/bin/sh\\n\\nexec sysctl -n hw.logicalcpu' > /usr/local/bin/nproc\n        chmod +x /usr/local/bin/nproc\n\n    - name: Build\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_ARCH: \"aarch64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        BUILD_GENERIC: true\n        CMAKE_POLICY_VERSION_MINIMUM: '3.5'\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n      run: |\n        edgedb-pkg/integration/macos/build.sh\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-macos-aarch64\n        path: artifacts/macos-aarch64\n\n  test-debian-buster-x86_64:\n    needs: [build-debian-buster-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-x86_64\n        path: artifacts/debian-buster\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-buster:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-buster-aarch64:\n    needs: [build-debian-buster-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-aarch64\n        path: artifacts/debian-buster\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-buster:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-bullseye-x86_64:\n    needs: [build-debian-bullseye-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-x86_64\n        path: artifacts/debian-bullseye\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-bullseye:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-bullseye-aarch64:\n    needs: [build-debian-bullseye-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-aarch64\n        path: artifacts/debian-bullseye\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-bullseye:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-bookworm-x86_64:\n    needs: [build-debian-bookworm-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-x86_64\n        path: artifacts/debian-bookworm\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-bookworm:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-bookworm-aarch64:\n    needs: [build-debian-bookworm-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-aarch64\n        path: artifacts/debian-bookworm\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-bookworm:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-focal-x86_64:\n    needs: [build-ubuntu-focal-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-x86_64\n        path: artifacts/ubuntu-focal\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-focal:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-focal-aarch64:\n    needs: [build-ubuntu-focal-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-aarch64\n        path: artifacts/ubuntu-focal\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-focal:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-jammy-x86_64:\n    needs: [build-ubuntu-jammy-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-x86_64\n        path: artifacts/ubuntu-jammy\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-jammy:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-jammy-aarch64:\n    needs: [build-ubuntu-jammy-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-aarch64\n        path: artifacts/ubuntu-jammy\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-jammy:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-noble-x86_64:\n    needs: [build-ubuntu-noble-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-x86_64\n        path: artifacts/ubuntu-noble\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-noble:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-noble-aarch64:\n    needs: [build-ubuntu-noble-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-aarch64\n        path: artifacts/ubuntu-noble\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-noble:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-centos-8-x86_64:\n    needs: [build-centos-8-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-x86_64\n        path: artifacts/centos-8\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-centos-8:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-centos-8-aarch64:\n    needs: [build-centos-8-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-aarch64\n        path: artifacts/centos-8\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-centos-8:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-rockylinux-9-x86_64:\n    needs: [build-rockylinux-9-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-x86_64\n        path: artifacts/rockylinux-9\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-rockylinux-9:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-rockylinux-9-aarch64:\n    needs: [build-rockylinux-9-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-aarch64\n        path: artifacts/rockylinux-9\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-rockylinux-9:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-linux-x86_64:\n    needs: [build-linux-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-linux-aarch64:\n    needs: [build-linux-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-linux-aarch64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-linuxmusl-x86_64:\n    needs: [build-linuxmusl-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-linuxmusl-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_LIBC: \"musl\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-linuxmusl-aarch64:\n    needs: [build-linuxmusl-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-linuxmusl-aarch64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_LIBC: \"musl\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-macos-x86_64:\n    needs: [build-macos-x86_64]\n    runs-on: ['macos-13']\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-macos-x86_64\n        path: artifacts/macos-x86_64\n\n    - name: Test\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" test_dump*.py test_backend_*.py test_database.py test_server_*.py test_edgeql_ddl.py test_session.py\n\"\n      run: |\n        # Bump shmmax and shmall to avoid test failures.\n        sudo sysctl -w kern.sysv.shmmax=12582912\n        sudo sysctl -w kern.sysv.shmall=12582912\n        edgedb-pkg/integration/macos/test.sh\n\n  test-macos-aarch64:\n    needs: [build-macos-aarch64]\n    runs-on: ['macos-14']\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-macos-aarch64\n        path: artifacts/macos-aarch64\n\n    - name: Test\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n      run: |\n        edgedb-pkg/integration/macos/test.sh\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n\n    needs:\n      - prep\n      - build-debian-buster-x86_64\n      - test-debian-buster-x86_64\n      - build-debian-buster-aarch64\n      - test-debian-buster-aarch64\n      - build-debian-bullseye-x86_64\n      - test-debian-bullseye-x86_64\n      - build-debian-bullseye-aarch64\n      - test-debian-bullseye-aarch64\n      - build-debian-bookworm-x86_64\n      - test-debian-bookworm-x86_64\n      - build-debian-bookworm-aarch64\n      - test-debian-bookworm-aarch64\n      - build-ubuntu-focal-x86_64\n      - test-ubuntu-focal-x86_64\n      - build-ubuntu-focal-aarch64\n      - test-ubuntu-focal-aarch64\n      - build-ubuntu-jammy-x86_64\n      - test-ubuntu-jammy-x86_64\n      - build-ubuntu-jammy-aarch64\n      - test-ubuntu-jammy-aarch64\n      - build-ubuntu-noble-x86_64\n      - test-ubuntu-noble-x86_64\n      - build-ubuntu-noble-aarch64\n      - test-ubuntu-noble-aarch64\n      - build-centos-8-x86_64\n      - test-centos-8-x86_64\n      - build-centos-8-aarch64\n      - test-centos-8-aarch64\n      - build-rockylinux-9-x86_64\n      - test-rockylinux-9-x86_64\n      - build-rockylinux-9-aarch64\n      - test-rockylinux-9-aarch64\n      - build-linux-x86_64\n      - test-linux-x86_64\n      - build-linux-aarch64\n      - test-linux-aarch64\n      - build-linuxmusl-x86_64\n      - test-linuxmusl-x86_64\n      - build-linuxmusl-aarch64\n      - test-linuxmusl-aarch64\n      - build-macos-x86_64\n      - test-macos-x86_64\n      - build-macos-aarch64\n      - test-macos-aarch64\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows/build.ls-nightly.yml",
    "content": "name: 'ls: Build and Publish Nightly Packages'\n\non:\n  schedule:\n    - cron: \"0 1 * * *\"\n  workflow_dispatch:\n    inputs:\n      gelpkg_ref:\n        description: \"gel-pkg git ref used to build the packages\"\n        default: \"master\"\n      metapkg_ref:\n        description: \"metapkg git ref used to build the packages\"\n        default: \"master\"\n  push:\n    branches:\n      - nightly\n\njobs:\n  prep:\n    runs-on: ubuntu-latest\n\n    outputs:\n\n      if_linux_x86_64: ${{ steps.scm.outputs.if_linux_x86_64 }}\n\n      if_linux_aarch64: ${{ steps.scm.outputs.if_linux_aarch64 }}\n\n      if_linuxmusl_x86_64: ${{ steps.scm.outputs.if_linuxmusl_x86_64 }}\n\n      if_linuxmusl_aarch64: ${{ steps.scm.outputs.if_linuxmusl_aarch64 }}\n\n      if_macos_x86_64: ${{ steps.scm.outputs.if_macos_x86_64 }}\n\n      if_macos_aarch64: ${{ steps.scm.outputs.if_macos_aarch64 }}\n\n\n    steps:\n    - uses: actions/checkout@v4\n\n\n    - name: Determine SCM revision\n      id: scm\n      shell: bash\n      run: |\n        rev=$(git rev-parse HEAD)\n        jq_filter='.packages[] | select(.basename == \"gel-ls\") | select(.architecture == $ARCH) | .version_details.metadata.scm_revision | . as $rev | select(($rev != null) and ($REV | startswith($rev)))'\n\n        key=\"linux-x86_64\"\n        val=true\n\n\n        idx_file=x86_64-unknown-linux-gnu.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"linux-aarch64\"\n        val=true\n\n\n        idx_file=aarch64-unknown-linux-gnu.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"linuxmusl-x86_64\"\n        val=true\n\n\n        idx_file=x86_64-unknown-linux-musl.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"linuxmusl-aarch64\"\n        val=true\n\n\n        idx_file=aarch64-unknown-linux-musl.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"macos-x86_64\"\n        val=true\n\n\n        idx_file=x86_64-unknown-linux-gnu.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"macos-aarch64\"\n        val=true\n\n\n        idx_file=aarch64-unknown-linux-gnu.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n\n\n  build-linux-x86_64:\n    runs-on: ['self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_linux_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linux-x86_64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb_ls:EdgeDBLanguageServer\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_GENERIC: true\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n  build-linux-aarch64:\n    runs-on: ['self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_linux_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linux-aarch64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb_ls:EdgeDBLanguageServer\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_GENERIC: true\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n  build-linuxmusl-x86_64:\n    runs-on: ['self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_linuxmusl_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linuxmusl-x86_64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb_ls:EdgeDBLanguageServer\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_GENERIC: true\n        PKG_PLATFORM_LIBC: \"musl\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n  build-linuxmusl-aarch64:\n    runs-on: ['self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_linuxmusl_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linuxmusl-aarch64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb_ls:EdgeDBLanguageServer\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_GENERIC: true\n        PKG_PLATFORM_LIBC: \"musl\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n  build-macos-x86_64:\n    runs-on: ['macos-13']\n    needs: prep\n\n    if: needs.prep.outputs.if_macos_x86_64 == 'true'\n\n\n    steps:\n    - name: Update Homebrew before installing Rust toolchain\n      run: |\n        # Homebrew renamed `rustup-init` to `rustup`:\n        #   https://github.com/Homebrew/homebrew-core/pull/177840\n        # But the GitHub Action runner is not updated with this change yet.\n        # This caused the later `brew update` in step `Build` to relink Rust\n        # toolchain executables, overwriting the custom toolchain installed by\n        # `dsherret/rust-toolchain-file`. So let's just run `brew update` early.\n        brew update\n\n    - uses: actions/checkout@v4\n      if: true\n      with:\n        sparse-checkout: |\n          rust-toolchain.toml\n        sparse-checkout-cone-mode: false\n\n    - name: Install Rust toolchain\n      uses: dsherret/rust-toolchain-file@v1\n      if: true\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      if: true\n      with:\n        python-version: \"3.12\"\n\n    - name: Set up NodeJS\n      uses: actions/setup-node@v4\n      if: true\n      with:\n        node-version: '20'\n\n    - name: Install dependencies\n      if: true\n      run: |\n        env HOMEBREW_NO_AUTO_UPDATE=1 brew install libmagic\n\n    - name: Install an alias\n      # This is probably not strictly needed, but sentencepiece build script reports\n      # errors without it.\n      if: true\n      run: |\n        printf '#!/bin/sh\\n\\nexec sysctl -n hw.logicalcpu' > /usr/local/bin/nproc\n        chmod +x /usr/local/bin/nproc\n\n    - name: Build\n      env:\n        PACKAGE: \"edgedbpkg.edgedb_ls:EdgeDBLanguageServer\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_ARCH: \"x86_64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        BUILD_GENERIC: true\n        CMAKE_POLICY_VERSION_MINIMUM: '3.5'\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n      run: |\n        edgedb-pkg/integration/macos/build.sh\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-macos-x86_64\n        path: artifacts/macos-x86_64\n\n  build-macos-aarch64:\n    runs-on: ['macos-14']\n    needs: prep\n\n    if: needs.prep.outputs.if_macos_aarch64 == 'true'\n\n\n    steps:\n    - name: Update Homebrew before installing Rust toolchain\n      run: |\n        # Homebrew renamed `rustup-init` to `rustup`:\n        #   https://github.com/Homebrew/homebrew-core/pull/177840\n        # But the GitHub Action runner is not updated with this change yet.\n        # This caused the later `brew update` in step `Build` to relink Rust\n        # toolchain executables, overwriting the custom toolchain installed by\n        # `dsherret/rust-toolchain-file`. So let's just run `brew update` early.\n        brew update\n\n    - uses: actions/checkout@v4\n      if: true\n      with:\n        sparse-checkout: |\n          rust-toolchain.toml\n        sparse-checkout-cone-mode: false\n\n    - name: Install Rust toolchain\n      uses: dsherret/rust-toolchain-file@v1\n      if: true\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      if: true\n      with:\n        python-version: \"3.12\"\n\n    - name: Set up NodeJS\n      uses: actions/setup-node@v4\n      if: true\n      with:\n        node-version: '20'\n\n    - name: Install dependencies\n      if: true\n      run: |\n        env HOMEBREW_NO_AUTO_UPDATE=1 brew install libmagic\n\n    - name: Install an alias\n      # This is probably not strictly needed, but sentencepiece build script reports\n      # errors without it.\n      if: true\n      run: |\n        printf '#!/bin/sh\\n\\nexec sysctl -n hw.logicalcpu' > /usr/local/bin/nproc\n        chmod +x /usr/local/bin/nproc\n\n    - name: Build\n      env:\n        PACKAGE: \"edgedbpkg.edgedb_ls:EdgeDBLanguageServer\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_ARCH: \"aarch64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        BUILD_GENERIC: true\n        CMAKE_POLICY_VERSION_MINIMUM: '3.5'\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n      run: |\n        edgedb-pkg/integration/macos/build.sh\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-macos-aarch64\n        path: artifacts/macos-aarch64\n\n  publish-linux-x86_64:\n    needs: [build-linux-x86_64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-linux-x86_64:\n    needs: [publish-linux-x86_64]\n    runs-on: ['self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: linux-x86_64\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-linux-x86_64:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-linux-aarch64:\n    needs: [build-linux-aarch64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-linux-aarch64:\n    needs: [publish-linux-aarch64]\n    runs-on: ['self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: linux-aarch64\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-linux-aarch64:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-linuxmusl-x86_64:\n    needs: [build-linuxmusl-x86_64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_LIBC: \"musl\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-linuxmusl-x86_64:\n    needs: [publish-linuxmusl-x86_64]\n    runs-on: ['self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: linuxmusl-x86_64\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-linuxmusl-x86_64:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-linuxmusl-aarch64:\n    needs: [build-linuxmusl-aarch64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_LIBC: \"musl\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-linuxmusl-aarch64:\n    needs: [publish-linuxmusl-aarch64]\n    runs-on: ['self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: linuxmusl-aarch64\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-linuxmusl-aarch64:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-macos-x86_64:\n    needs: [build-macos-x86_64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-macos-x86_64\n        path: artifacts/macos-x86_64\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: macos-x86_64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  publish-macos-aarch64:\n    needs: [build-macos-aarch64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-macos-aarch64\n        path: artifacts/macos-aarch64\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: macos-aarch64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n\n    needs:\n      - prep\n      - build-linux-x86_64\n      - publish-linux-x86_64\n      - check-published-linux-x86_64\n      - build-linux-aarch64\n      - publish-linux-aarch64\n      - check-published-linux-aarch64\n      - build-linuxmusl-x86_64\n      - publish-linuxmusl-x86_64\n      - check-published-linuxmusl-x86_64\n      - build-linuxmusl-aarch64\n      - publish-linuxmusl-aarch64\n      - check-published-linuxmusl-aarch64\n      - build-macos-x86_64\n      - publish-macos-x86_64\n      - build-macos-aarch64\n      - publish-macos-aarch64\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows/build.nightly.yml",
    "content": "name: Build Test and Publish Nightly Packages\n\non:\n  schedule:\n    - cron: \"0 1 * * *\"\n  workflow_dispatch:\n    inputs:\n      gelpkg_ref:\n        description: \"gel-pkg git ref used to build the packages\"\n        default: \"master\"\n      metapkg_ref:\n        description: \"metapkg git ref used to build the packages\"\n        default: \"master\"\n  push:\n    branches:\n      - nightly\n\njobs:\n  prep:\n    runs-on: ubuntu-latest\n\n    outputs:\n\n      if_debian_buster_x86_64: ${{ steps.scm.outputs.if_debian_buster_x86_64 }}\n\n      if_debian_buster_aarch64: ${{ steps.scm.outputs.if_debian_buster_aarch64 }}\n\n      if_debian_bullseye_x86_64: ${{ steps.scm.outputs.if_debian_bullseye_x86_64 }}\n\n      if_debian_bullseye_aarch64: ${{ steps.scm.outputs.if_debian_bullseye_aarch64 }}\n\n      if_debian_bookworm_x86_64: ${{ steps.scm.outputs.if_debian_bookworm_x86_64 }}\n\n      if_debian_bookworm_aarch64: ${{ steps.scm.outputs.if_debian_bookworm_aarch64 }}\n\n      if_ubuntu_focal_x86_64: ${{ steps.scm.outputs.if_ubuntu_focal_x86_64 }}\n\n      if_ubuntu_focal_aarch64: ${{ steps.scm.outputs.if_ubuntu_focal_aarch64 }}\n\n      if_ubuntu_jammy_x86_64: ${{ steps.scm.outputs.if_ubuntu_jammy_x86_64 }}\n\n      if_ubuntu_jammy_aarch64: ${{ steps.scm.outputs.if_ubuntu_jammy_aarch64 }}\n\n      if_ubuntu_noble_x86_64: ${{ steps.scm.outputs.if_ubuntu_noble_x86_64 }}\n\n      if_ubuntu_noble_aarch64: ${{ steps.scm.outputs.if_ubuntu_noble_aarch64 }}\n\n      if_centos_8_x86_64: ${{ steps.scm.outputs.if_centos_8_x86_64 }}\n\n      if_centos_8_aarch64: ${{ steps.scm.outputs.if_centos_8_aarch64 }}\n\n      if_rockylinux_9_x86_64: ${{ steps.scm.outputs.if_rockylinux_9_x86_64 }}\n\n      if_rockylinux_9_aarch64: ${{ steps.scm.outputs.if_rockylinux_9_aarch64 }}\n\n      if_linux_x86_64: ${{ steps.scm.outputs.if_linux_x86_64 }}\n\n      if_linux_aarch64: ${{ steps.scm.outputs.if_linux_aarch64 }}\n\n      if_linuxmusl_x86_64: ${{ steps.scm.outputs.if_linuxmusl_x86_64 }}\n\n      if_linuxmusl_aarch64: ${{ steps.scm.outputs.if_linuxmusl_aarch64 }}\n\n      if_macos_x86_64: ${{ steps.scm.outputs.if_macos_x86_64 }}\n\n      if_macos_aarch64: ${{ steps.scm.outputs.if_macos_aarch64 }}\n\n\n    steps:\n    - uses: actions/checkout@v4\n\n\n    - name: Determine SCM revision\n      id: scm\n      shell: bash\n      run: |\n        rev=$(git rev-parse HEAD)\n        jq_filter='.packages[] | select(.basename == \"gel-server\") | select(.architecture == $ARCH) | .version_details.metadata.scm_revision | . as $rev | select(($rev != null) and ($REV | startswith($rev)))'\n\n        key=\"debian-buster-x86_64\"\n        val=true\n\n\n        idx_file=buster.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"debian-buster-aarch64\"\n        val=true\n\n\n        idx_file=buster.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"debian-bullseye-x86_64\"\n        val=true\n\n\n        idx_file=bullseye.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"debian-bullseye-aarch64\"\n        val=true\n\n\n        idx_file=bullseye.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"debian-bookworm-x86_64\"\n        val=true\n\n\n        idx_file=bookworm.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"debian-bookworm-aarch64\"\n        val=true\n\n\n        idx_file=bookworm.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"ubuntu-focal-x86_64\"\n        val=true\n\n\n        idx_file=focal.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"ubuntu-focal-aarch64\"\n        val=true\n\n\n        idx_file=focal.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"ubuntu-jammy-x86_64\"\n        val=true\n\n\n        idx_file=jammy.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"ubuntu-jammy-aarch64\"\n        val=true\n\n\n        idx_file=jammy.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"ubuntu-noble-x86_64\"\n        val=true\n\n\n        idx_file=noble.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"ubuntu-noble-aarch64\"\n        val=true\n\n\n        idx_file=noble.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"centos-8-x86_64\"\n        val=true\n\n\n        idx_file=el8.nightly.json\n        url=https://packages.edgedb.com/rpm/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"centos-8-aarch64\"\n        val=true\n\n\n        idx_file=el8.nightly.json\n        url=https://packages.edgedb.com/rpm/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"rockylinux-9-x86_64\"\n        val=true\n\n\n        idx_file=el9.nightly.json\n        url=https://packages.edgedb.com/rpm/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"rockylinux-9-aarch64\"\n        val=true\n\n\n        idx_file=el9.nightly.json\n        url=https://packages.edgedb.com/rpm/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"linux-x86_64\"\n        val=true\n\n\n        idx_file=x86_64-unknown-linux-gnu.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"linux-aarch64\"\n        val=true\n\n\n        idx_file=aarch64-unknown-linux-gnu.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"linuxmusl-x86_64\"\n        val=true\n\n\n        idx_file=x86_64-unknown-linux-musl.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"linuxmusl-aarch64\"\n        val=true\n\n\n        idx_file=aarch64-unknown-linux-musl.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"macos-x86_64\"\n        val=true\n\n\n        idx_file=x86_64-unknown-linux-gnu.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"x86_64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n        key=\"macos-aarch64\"\n        val=true\n\n\n        idx_file=aarch64-unknown-linux-gnu.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"aarch64\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n\n\n\n  build-debian-buster-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_debian_buster_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-buster:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-buster-x86_64\n        path: artifacts/debian-buster\n\n  build-debian-buster-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_debian_buster_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-buster:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-buster-aarch64\n        path: artifacts/debian-buster\n\n  build-debian-bullseye-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_debian_bullseye_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-bullseye:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-bullseye-x86_64\n        path: artifacts/debian-bullseye\n\n  build-debian-bullseye-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_debian_bullseye_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-bullseye:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-bullseye-aarch64\n        path: artifacts/debian-bullseye\n\n  build-debian-bookworm-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_debian_bookworm_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-bookworm:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-bookworm-x86_64\n        path: artifacts/debian-bookworm\n\n  build-debian-bookworm-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_debian_bookworm_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-bookworm:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-bookworm-aarch64\n        path: artifacts/debian-bookworm\n\n  build-ubuntu-focal-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_ubuntu_focal_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-focal:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-focal-x86_64\n        path: artifacts/ubuntu-focal\n\n  build-ubuntu-focal-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_ubuntu_focal_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-focal:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-focal-aarch64\n        path: artifacts/ubuntu-focal\n\n  build-ubuntu-jammy-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_ubuntu_jammy_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-jammy:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-jammy-x86_64\n        path: artifacts/ubuntu-jammy\n\n  build-ubuntu-jammy-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_ubuntu_jammy_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-jammy:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-jammy-aarch64\n        path: artifacts/ubuntu-jammy\n\n  build-ubuntu-noble-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_ubuntu_noble_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-noble:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-noble-x86_64\n        path: artifacts/ubuntu-noble\n\n  build-ubuntu-noble-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_ubuntu_noble_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-noble:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-noble-aarch64\n        path: artifacts/ubuntu-noble\n\n  build-centos-8-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_centos_8_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-centos-8:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-centos-8-x86_64\n        path: artifacts/centos-8\n\n  build-centos-8-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_centos_8_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-centos-8:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-centos-8-aarch64\n        path: artifacts/centos-8\n\n  build-rockylinux-9-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_rockylinux_9_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-rockylinux-9:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-rockylinux-9-x86_64\n        path: artifacts/rockylinux-9\n\n  build-rockylinux-9-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_rockylinux_9_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-rockylinux-9:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-rockylinux-9-aarch64\n        path: artifacts/rockylinux-9\n\n  build-linux-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_linux_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linux-x86_64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_GENERIC: true\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n  build-linux-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_linux_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linux-aarch64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_GENERIC: true\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n  build-linuxmusl-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n    if: needs.prep.outputs.if_linuxmusl_x86_64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linuxmusl-x86_64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_GENERIC: true\n        PKG_PLATFORM_LIBC: \"musl\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n  build-linuxmusl-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n    if: needs.prep.outputs.if_linuxmusl_aarch64 == 'true'\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linuxmusl-aarch64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_GENERIC: true\n        PKG_PLATFORM_LIBC: \"musl\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n  build-macos-x86_64:\n    runs-on: ['macos-13']\n    needs: prep\n\n    if: needs.prep.outputs.if_macos_x86_64 == 'true'\n\n\n    steps:\n    - name: Update Homebrew before installing Rust toolchain\n      run: |\n        # Homebrew renamed `rustup-init` to `rustup`:\n        #   https://github.com/Homebrew/homebrew-core/pull/177840\n        # But the GitHub Action runner is not updated with this change yet.\n        # This caused the later `brew update` in step `Build` to relink Rust\n        # toolchain executables, overwriting the custom toolchain installed by\n        # `dsherret/rust-toolchain-file`. So let's just run `brew update` early.\n        brew update\n\n    - uses: actions/checkout@v4\n      if: true\n      with:\n        sparse-checkout: |\n          rust-toolchain.toml\n        sparse-checkout-cone-mode: false\n\n    - name: Install Rust toolchain\n      uses: dsherret/rust-toolchain-file@v1\n      if: true\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      if: true\n      with:\n        python-version: \"3.12\"\n\n    - name: Set up NodeJS\n      uses: actions/setup-node@v4\n      if: true\n      with:\n        node-version: '20'\n\n    - name: Install dependencies\n      if: true\n      run: |\n        env HOMEBREW_NO_AUTO_UPDATE=1 brew install libmagic\n\n    - name: Install an alias\n      # This is probably not strictly needed, but sentencepiece build script reports\n      # errors without it.\n      if: true\n      run: |\n        printf '#!/bin/sh\\n\\nexec sysctl -n hw.logicalcpu' > /usr/local/bin/nproc\n        chmod +x /usr/local/bin/nproc\n\n    - name: Build\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_ARCH: \"x86_64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        BUILD_GENERIC: true\n        CMAKE_POLICY_VERSION_MINIMUM: '3.5'\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n      run: |\n        edgedb-pkg/integration/macos/build.sh\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-macos-x86_64\n        path: artifacts/macos-x86_64\n\n  build-macos-aarch64:\n    runs-on: ['macos-14']\n    needs: prep\n\n    if: needs.prep.outputs.if_macos_aarch64 == 'true'\n\n\n    steps:\n    - name: Update Homebrew before installing Rust toolchain\n      run: |\n        # Homebrew renamed `rustup-init` to `rustup`:\n        #   https://github.com/Homebrew/homebrew-core/pull/177840\n        # But the GitHub Action runner is not updated with this change yet.\n        # This caused the later `brew update` in step `Build` to relink Rust\n        # toolchain executables, overwriting the custom toolchain installed by\n        # `dsherret/rust-toolchain-file`. So let's just run `brew update` early.\n        brew update\n\n    - uses: actions/checkout@v4\n      if: true\n      with:\n        sparse-checkout: |\n          rust-toolchain.toml\n        sparse-checkout-cone-mode: false\n\n    - name: Install Rust toolchain\n      uses: dsherret/rust-toolchain-file@v1\n      if: true\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      if: true\n      with:\n        python-version: \"3.12\"\n\n    - name: Set up NodeJS\n      uses: actions/setup-node@v4\n      if: true\n      with:\n        node-version: '20'\n\n    - name: Install dependencies\n      if: true\n      run: |\n        env HOMEBREW_NO_AUTO_UPDATE=1 brew install libmagic\n\n    - name: Install an alias\n      # This is probably not strictly needed, but sentencepiece build script reports\n      # errors without it.\n      if: true\n      run: |\n        printf '#!/bin/sh\\n\\nexec sysctl -n hw.logicalcpu' > /usr/local/bin/nproc\n        chmod +x /usr/local/bin/nproc\n\n    - name: Build\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_ARCH: \"aarch64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        BUILD_GENERIC: true\n        CMAKE_POLICY_VERSION_MINIMUM: '3.5'\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n      run: |\n        edgedb-pkg/integration/macos/build.sh\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-macos-aarch64\n        path: artifacts/macos-aarch64\n\n  test-debian-buster-x86_64:\n    needs: [build-debian-buster-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-x86_64\n        path: artifacts/debian-buster\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-buster:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-buster-aarch64:\n    needs: [build-debian-buster-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-aarch64\n        path: artifacts/debian-buster\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-buster:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-bullseye-x86_64:\n    needs: [build-debian-bullseye-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-x86_64\n        path: artifacts/debian-bullseye\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-bullseye:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-bullseye-aarch64:\n    needs: [build-debian-bullseye-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-aarch64\n        path: artifacts/debian-bullseye\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-bullseye:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-bookworm-x86_64:\n    needs: [build-debian-bookworm-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-x86_64\n        path: artifacts/debian-bookworm\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-bookworm:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-bookworm-aarch64:\n    needs: [build-debian-bookworm-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-aarch64\n        path: artifacts/debian-bookworm\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-bookworm:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-focal-x86_64:\n    needs: [build-ubuntu-focal-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-x86_64\n        path: artifacts/ubuntu-focal\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-focal:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-focal-aarch64:\n    needs: [build-ubuntu-focal-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-aarch64\n        path: artifacts/ubuntu-focal\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-focal:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-jammy-x86_64:\n    needs: [build-ubuntu-jammy-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-x86_64\n        path: artifacts/ubuntu-jammy\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-jammy:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-jammy-aarch64:\n    needs: [build-ubuntu-jammy-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-aarch64\n        path: artifacts/ubuntu-jammy\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-jammy:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-noble-x86_64:\n    needs: [build-ubuntu-noble-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-x86_64\n        path: artifacts/ubuntu-noble\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-noble:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-noble-aarch64:\n    needs: [build-ubuntu-noble-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-aarch64\n        path: artifacts/ubuntu-noble\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-noble:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-centos-8-x86_64:\n    needs: [build-centos-8-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-x86_64\n        path: artifacts/centos-8\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-centos-8:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-centos-8-aarch64:\n    needs: [build-centos-8-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-aarch64\n        path: artifacts/centos-8\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-centos-8:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-rockylinux-9-x86_64:\n    needs: [build-rockylinux-9-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-x86_64\n        path: artifacts/rockylinux-9\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-rockylinux-9:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-rockylinux-9-aarch64:\n    needs: [build-rockylinux-9-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-aarch64\n        path: artifacts/rockylinux-9\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-rockylinux-9:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-linux-x86_64:\n    needs: [build-linux-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-linux-aarch64:\n    needs: [build-linux-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-linux-aarch64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-linuxmusl-x86_64:\n    needs: [build-linuxmusl-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-linuxmusl-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_LIBC: \"musl\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-linuxmusl-aarch64:\n    needs: [build-linuxmusl-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-linuxmusl-aarch64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_LIBC: \"musl\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-macos-x86_64:\n    needs: [build-macos-x86_64]\n    runs-on: ['macos-13']\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-macos-x86_64\n        path: artifacts/macos-x86_64\n\n    - name: Test\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" test_dump*.py test_backend_*.py test_database.py test_server_*.py test_edgeql_ddl.py test_session.py\n\"\n      run: |\n        # Bump shmmax and shmall to avoid test failures.\n        sudo sysctl -w kern.sysv.shmmax=12582912\n        sudo sysctl -w kern.sysv.shmall=12582912\n        edgedb-pkg/integration/macos/test.sh\n\n  test-macos-aarch64:\n    needs: [build-macos-aarch64]\n    runs-on: ['macos-14']\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-macos-aarch64\n        path: artifacts/macos-aarch64\n\n    - name: Test\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n      run: |\n        edgedb-pkg/integration/macos/test.sh\n\n  publish-debian-buster-x86_64:\n    needs: [test-debian-buster-x86_64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-x86_64\n        path: artifacts/debian-buster\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-buster-x86_64:\n    needs: [publish-debian-buster-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-x86_64\n        path: artifacts/debian-buster\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-buster\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-buster:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-debian-buster-aarch64:\n    needs: [test-debian-buster-aarch64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-aarch64\n        path: artifacts/debian-buster\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-buster-aarch64:\n    needs: [publish-debian-buster-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-aarch64\n        path: artifacts/debian-buster\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-buster\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-buster:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-debian-bullseye-x86_64:\n    needs: [test-debian-bullseye-x86_64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-x86_64\n        path: artifacts/debian-bullseye\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-bullseye-x86_64:\n    needs: [publish-debian-bullseye-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-x86_64\n        path: artifacts/debian-bullseye\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-bullseye\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-bullseye:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-debian-bullseye-aarch64:\n    needs: [test-debian-bullseye-aarch64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-aarch64\n        path: artifacts/debian-bullseye\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-bullseye-aarch64:\n    needs: [publish-debian-bullseye-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-aarch64\n        path: artifacts/debian-bullseye\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-bullseye\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-bullseye:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-debian-bookworm-x86_64:\n    needs: [test-debian-bookworm-x86_64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-x86_64\n        path: artifacts/debian-bookworm\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-bookworm-x86_64:\n    needs: [publish-debian-bookworm-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-x86_64\n        path: artifacts/debian-bookworm\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-bookworm\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-bookworm:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-debian-bookworm-aarch64:\n    needs: [test-debian-bookworm-aarch64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-aarch64\n        path: artifacts/debian-bookworm\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-bookworm-aarch64:\n    needs: [publish-debian-bookworm-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-aarch64\n        path: artifacts/debian-bookworm\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-bookworm\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-bookworm:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-focal-x86_64:\n    needs: [test-ubuntu-focal-x86_64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-x86_64\n        path: artifacts/ubuntu-focal\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-focal-x86_64:\n    needs: [publish-ubuntu-focal-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-x86_64\n        path: artifacts/ubuntu-focal\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-focal\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-focal:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-focal-aarch64:\n    needs: [test-ubuntu-focal-aarch64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-aarch64\n        path: artifacts/ubuntu-focal\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-focal-aarch64:\n    needs: [publish-ubuntu-focal-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-aarch64\n        path: artifacts/ubuntu-focal\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-focal\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-focal:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-jammy-x86_64:\n    needs: [test-ubuntu-jammy-x86_64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-x86_64\n        path: artifacts/ubuntu-jammy\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-jammy-x86_64:\n    needs: [publish-ubuntu-jammy-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-x86_64\n        path: artifacts/ubuntu-jammy\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-jammy\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-jammy:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-jammy-aarch64:\n    needs: [test-ubuntu-jammy-aarch64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-aarch64\n        path: artifacts/ubuntu-jammy\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-jammy-aarch64:\n    needs: [publish-ubuntu-jammy-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-aarch64\n        path: artifacts/ubuntu-jammy\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-jammy\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-jammy:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-noble-x86_64:\n    needs: [test-ubuntu-noble-x86_64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-x86_64\n        path: artifacts/ubuntu-noble\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-noble-x86_64:\n    needs: [publish-ubuntu-noble-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-x86_64\n        path: artifacts/ubuntu-noble\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-noble\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-noble:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-noble-aarch64:\n    needs: [test-ubuntu-noble-aarch64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-aarch64\n        path: artifacts/ubuntu-noble\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-noble-aarch64:\n    needs: [publish-ubuntu-noble-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-aarch64\n        path: artifacts/ubuntu-noble\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-noble\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-noble:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-centos-8-x86_64:\n    needs: [test-centos-8-x86_64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-x86_64\n        path: artifacts/centos-8\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-centos-8-x86_64:\n    needs: [publish-centos-8-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-x86_64\n        path: artifacts/centos-8\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: centos-8\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-centos-8:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-centos-8-aarch64:\n    needs: [test-centos-8-aarch64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-aarch64\n        path: artifacts/centos-8\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-centos-8-aarch64:\n    needs: [publish-centos-8-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-aarch64\n        path: artifacts/centos-8\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: centos-8\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-centos-8:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-rockylinux-9-x86_64:\n    needs: [test-rockylinux-9-x86_64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-x86_64\n        path: artifacts/rockylinux-9\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-rockylinux-9-x86_64:\n    needs: [publish-rockylinux-9-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-x86_64\n        path: artifacts/rockylinux-9\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: rockylinux-9\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-rockylinux-9:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-rockylinux-9-aarch64:\n    needs: [test-rockylinux-9-aarch64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-aarch64\n        path: artifacts/rockylinux-9\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-rockylinux-9-aarch64:\n    needs: [publish-rockylinux-9-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-aarch64\n        path: artifacts/rockylinux-9\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: rockylinux-9\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-rockylinux-9:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-linux-x86_64:\n    needs: [test-linux-x86_64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-linux-x86_64:\n    needs: [publish-linux-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: linux-x86_64\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-linux-x86_64:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-linux-aarch64:\n    needs: [test-linux-aarch64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-linux-aarch64:\n    needs: [publish-linux-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: linux-aarch64\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-linux-aarch64:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-linuxmusl-x86_64:\n    needs: [test-linuxmusl-x86_64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_LIBC: \"musl\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-linuxmusl-x86_64:\n    needs: [publish-linuxmusl-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: linuxmusl-x86_64\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-linuxmusl-x86_64:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-linuxmusl-aarch64:\n    needs: [test-linuxmusl-aarch64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_LIBC: \"musl\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-linuxmusl-aarch64:\n    needs: [publish-linuxmusl-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: linuxmusl-aarch64\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-linuxmusl-aarch64:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"nightly\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-macos-x86_64:\n    needs: [test-macos-x86_64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-macos-x86_64\n        path: artifacts/macos-x86_64\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: macos-x86_64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  publish-macos-aarch64:\n    needs: [test-macos-aarch64]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-macos-aarch64\n        path: artifacts/macos-aarch64\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: macos-aarch64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"nightly\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  publish-docker:\n    needs:\n      - check-published-debian-bookworm-x86_64\n      - check-published-debian-bookworm-aarch64\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        repository: geldata/gel-docker\n        ref: master\n        path: dockerfile\n\n    - name: Login to Docker Hub\n      uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0\n      with:\n        username: ${{ secrets.DOCKER_USERNAME }}\n        password: ${{ secrets.DOCKER_PASSWORD }}\n\n    - name: Login to GitHub Container Registry\n      uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0\n      with:\n        registry: ghcr.io\n        username: \"edgedb-ci\"\n        password: ${{ secrets.GITHUB_CI_BOT_TOKEN }}\n\n    - env:\n        VERSION_SLOT: \"${{ needs.check-published-debian-bookworm-x86_64.outputs.version-slot }}\"\n        VERSION_CORE: \"${{ needs.check-published-debian-bookworm-x86_64.outputs.version-core }}\"\n        CATALOG_VERSION: \"${{ needs.check-published-debian-bookworm-x86_64.outputs.catalog-version }}\"\n        PKG_SUBDIST: \"nightly\"\n      id: tags\n      run: |\n        set -e\n\n        url='https://registry.hub.docker.com/v2/repositories/geldata/gel/tags?page_size=100'\n        repo_tags=$(\n          while [ -n \"$url\" ]; do\n            resp=$(curl -L -s \"$url\")\n            url=$(echo \"$resp\" | jq -r \".next\")\n            if [ \"$url\" = \"null\" ] || [ -z \"$url\" ]; then\n              break\n            fi\n            echo \"$resp\" | jq -r '.\"results\"[][\"name\"]'\n          done | grep \"^[[:digit:]]\\+.*\" | grep -v \"alpha\\|beta\\|rc\" || :\n        )\n\n        tags=()\n\n        if [ \"$PKG_SUBDIST\" = \"nightly\" ]; then\n          tags+=(\n            \"nightly\"\n            \"nightly_${VERSION_SLOT}_cv${CATALOG_VERSION}\"\n          )\n        else\n          tags+=( \"$VERSION_CORE\" )\n\n          top=$(printf \"%s\\n%s\\n\" \"$VERSION_CORE\" \"$repo_tags\" \\\n                | grep \"^${VERSION_SLOT}[\\.-]\" \\\n                | sort --version-sort --reverse | head -n 1)\n          if [ \"$top\" == \"$VERSION_CORE\" ]; then\n            tags+=( \"$VERSION_SLOT\" )\n          fi\n\n          if [ -z \"$PKG_SUBDIST\" ]; then\n            top=$(printf \"%s\\n%s\\n\" \"$VERSION_CORE\" \"$repo_tags\" \\\n                  | sort --version-sort --reverse | head -n 1)\n            if [ \"$top\" == \"$VERSION_CORE\" ]; then\n              tags+=( \"latest\" )\n            fi\n          fi\n        fi\n\n        fq_tags=()\n        images=(\"geldata/gel\" \"ghcr.io/geldata/gel\")\n\n        for image in \"${images[@]}\"; do\n          fq_tags+=(\"${tags[@]/#/${image}:}\")\n        done\n\n        IFS=,\n        echo \"tags=${fq_tags[*]}\" >> $GITHUB_OUTPUT\n\n    - name: Set up QEMU\n      uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0\n\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0\n\n    - name: Build and Publish Docker Image\n      uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.10.0\n      with:\n        push: true\n        provenance: mode=max\n        tags: \"${{ steps.tags.outputs.tags }}\"\n        context: dockerfile\n        build-args: |\n          version=${{ needs.check-published-debian-bookworm-x86_64.outputs.version-slot }}\n          exact_version=${{ needs.check-published-debian-bookworm-x86_64.outputs.version-core }}\n          subdist=nightly\n        platforms: linux/amd64,linux/arm64\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n\n    needs:\n      - prep\n      - build-debian-buster-x86_64\n      - test-debian-buster-x86_64\n      - publish-debian-buster-x86_64\n      - check-published-debian-buster-x86_64\n      - build-debian-buster-aarch64\n      - test-debian-buster-aarch64\n      - publish-debian-buster-aarch64\n      - check-published-debian-buster-aarch64\n      - build-debian-bullseye-x86_64\n      - test-debian-bullseye-x86_64\n      - publish-debian-bullseye-x86_64\n      - check-published-debian-bullseye-x86_64\n      - build-debian-bullseye-aarch64\n      - test-debian-bullseye-aarch64\n      - publish-debian-bullseye-aarch64\n      - check-published-debian-bullseye-aarch64\n      - build-debian-bookworm-x86_64\n      - test-debian-bookworm-x86_64\n      - publish-debian-bookworm-x86_64\n      - check-published-debian-bookworm-x86_64\n      - build-debian-bookworm-aarch64\n      - test-debian-bookworm-aarch64\n      - publish-debian-bookworm-aarch64\n      - check-published-debian-bookworm-aarch64\n      - build-ubuntu-focal-x86_64\n      - test-ubuntu-focal-x86_64\n      - publish-ubuntu-focal-x86_64\n      - check-published-ubuntu-focal-x86_64\n      - build-ubuntu-focal-aarch64\n      - test-ubuntu-focal-aarch64\n      - publish-ubuntu-focal-aarch64\n      - check-published-ubuntu-focal-aarch64\n      - build-ubuntu-jammy-x86_64\n      - test-ubuntu-jammy-x86_64\n      - publish-ubuntu-jammy-x86_64\n      - check-published-ubuntu-jammy-x86_64\n      - build-ubuntu-jammy-aarch64\n      - test-ubuntu-jammy-aarch64\n      - publish-ubuntu-jammy-aarch64\n      - check-published-ubuntu-jammy-aarch64\n      - build-ubuntu-noble-x86_64\n      - test-ubuntu-noble-x86_64\n      - publish-ubuntu-noble-x86_64\n      - check-published-ubuntu-noble-x86_64\n      - build-ubuntu-noble-aarch64\n      - test-ubuntu-noble-aarch64\n      - publish-ubuntu-noble-aarch64\n      - check-published-ubuntu-noble-aarch64\n      - build-centos-8-x86_64\n      - test-centos-8-x86_64\n      - publish-centos-8-x86_64\n      - check-published-centos-8-x86_64\n      - build-centos-8-aarch64\n      - test-centos-8-aarch64\n      - publish-centos-8-aarch64\n      - check-published-centos-8-aarch64\n      - build-rockylinux-9-x86_64\n      - test-rockylinux-9-x86_64\n      - publish-rockylinux-9-x86_64\n      - check-published-rockylinux-9-x86_64\n      - build-rockylinux-9-aarch64\n      - test-rockylinux-9-aarch64\n      - publish-rockylinux-9-aarch64\n      - check-published-rockylinux-9-aarch64\n      - build-linux-x86_64\n      - test-linux-x86_64\n      - publish-linux-x86_64\n      - check-published-linux-x86_64\n      - build-linux-aarch64\n      - test-linux-aarch64\n      - publish-linux-aarch64\n      - check-published-linux-aarch64\n      - build-linuxmusl-x86_64\n      - test-linuxmusl-x86_64\n      - publish-linuxmusl-x86_64\n      - check-published-linuxmusl-x86_64\n      - build-linuxmusl-aarch64\n      - test-linuxmusl-aarch64\n      - publish-linuxmusl-aarch64\n      - check-published-linuxmusl-aarch64\n      - build-macos-x86_64\n      - test-macos-x86_64\n      - publish-macos-x86_64\n      - build-macos-aarch64\n      - test-macos-aarch64\n      - publish-macos-aarch64\n      - publish-docker\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows/build.release.yml",
    "content": "name: Build Test and Publish a Release\n\non:\n  workflow_dispatch:\n    inputs:\n      gelpkg_ref:\n        description: \"gel-pkg git ref used to build the packages\"\n        default: \"master\"\n      metapkg_ref:\n        description: \"metapkg git ref used to build the packages\"\n        default: \"master\"\n\njobs:\n  prep:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n\n\n\n  build-debian-buster-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-buster:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-buster-x86_64\n        path: artifacts/debian-buster\n\n  build-debian-buster-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-buster:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-buster-aarch64\n        path: artifacts/debian-buster\n\n  build-debian-bullseye-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-bullseye:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-bullseye-x86_64\n        path: artifacts/debian-bullseye\n\n  build-debian-bullseye-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-bullseye:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-bullseye-aarch64\n        path: artifacts/debian-bullseye\n\n  build-debian-bookworm-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-bookworm:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-bookworm-x86_64\n        path: artifacts/debian-bookworm\n\n  build-debian-bookworm-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-bookworm:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-bookworm-aarch64\n        path: artifacts/debian-bookworm\n\n  build-ubuntu-focal-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-focal:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-focal-x86_64\n        path: artifacts/ubuntu-focal\n\n  build-ubuntu-focal-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-focal:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-focal-aarch64\n        path: artifacts/ubuntu-focal\n\n  build-ubuntu-jammy-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-jammy:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-jammy-x86_64\n        path: artifacts/ubuntu-jammy\n\n  build-ubuntu-jammy-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-jammy:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-jammy-aarch64\n        path: artifacts/ubuntu-jammy\n\n  build-ubuntu-noble-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-noble:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-noble-x86_64\n        path: artifacts/ubuntu-noble\n\n  build-ubuntu-noble-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-noble:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-noble-aarch64\n        path: artifacts/ubuntu-noble\n\n  build-centos-8-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-centos-8:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-centos-8-x86_64\n        path: artifacts/centos-8\n\n  build-centos-8-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-centos-8:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-centos-8-aarch64\n        path: artifacts/centos-8\n\n  build-rockylinux-9-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-rockylinux-9:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-rockylinux-9-x86_64\n        path: artifacts/rockylinux-9\n\n  build-rockylinux-9-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-rockylinux-9:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-rockylinux-9-aarch64\n        path: artifacts/rockylinux-9\n\n  build-linux-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linux-x86_64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        BUILD_GENERIC: true\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n  build-linux-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linux-aarch64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        BUILD_GENERIC: true\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n  build-linuxmusl-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linuxmusl-x86_64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        BUILD_GENERIC: true\n        PKG_PLATFORM_LIBC: \"musl\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n  build-linuxmusl-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linuxmusl-aarch64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        BUILD_GENERIC: true\n        PKG_PLATFORM_LIBC: \"musl\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n  build-macos-x86_64:\n    runs-on: ['macos-13']\n    needs: prep\n\n\n    steps:\n    - name: Update Homebrew before installing Rust toolchain\n      run: |\n        # Homebrew renamed `rustup-init` to `rustup`:\n        #   https://github.com/Homebrew/homebrew-core/pull/177840\n        # But the GitHub Action runner is not updated with this change yet.\n        # This caused the later `brew update` in step `Build` to relink Rust\n        # toolchain executables, overwriting the custom toolchain installed by\n        # `dsherret/rust-toolchain-file`. So let's just run `brew update` early.\n        brew update\n\n    - uses: actions/checkout@v4\n      if: true\n      with:\n        sparse-checkout: |\n          rust-toolchain.toml\n        sparse-checkout-cone-mode: false\n\n    - name: Install Rust toolchain\n      uses: dsherret/rust-toolchain-file@v1\n      if: true\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      if: true\n      with:\n        python-version: \"3.12\"\n\n    - name: Set up NodeJS\n      uses: actions/setup-node@v4\n      if: true\n      with:\n        node-version: '20'\n\n    - name: Install dependencies\n      if: true\n      run: |\n        env HOMEBREW_NO_AUTO_UPDATE=1 brew install libmagic\n\n    - name: Install an alias\n      # This is probably not strictly needed, but sentencepiece build script reports\n      # errors without it.\n      if: true\n      run: |\n        printf '#!/bin/sh\\n\\nexec sysctl -n hw.logicalcpu' > /usr/local/bin/nproc\n        chmod +x /usr/local/bin/nproc\n\n    - name: Build\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        BUILD_IS_RELEASE: \"true\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_ARCH: \"x86_64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        BUILD_GENERIC: true\n        CMAKE_POLICY_VERSION_MINIMUM: '3.5'\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n      run: |\n        edgedb-pkg/integration/macos/build.sh\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-macos-x86_64\n        path: artifacts/macos-x86_64\n\n  build-macos-aarch64:\n    runs-on: ['macos-14']\n    needs: prep\n\n\n    steps:\n    - name: Update Homebrew before installing Rust toolchain\n      run: |\n        # Homebrew renamed `rustup-init` to `rustup`:\n        #   https://github.com/Homebrew/homebrew-core/pull/177840\n        # But the GitHub Action runner is not updated with this change yet.\n        # This caused the later `brew update` in step `Build` to relink Rust\n        # toolchain executables, overwriting the custom toolchain installed by\n        # `dsherret/rust-toolchain-file`. So let's just run `brew update` early.\n        brew update\n\n    - uses: actions/checkout@v4\n      if: true\n      with:\n        sparse-checkout: |\n          rust-toolchain.toml\n        sparse-checkout-cone-mode: false\n\n    - name: Install Rust toolchain\n      uses: dsherret/rust-toolchain-file@v1\n      if: true\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      if: true\n      with:\n        python-version: \"3.12\"\n\n    - name: Set up NodeJS\n      uses: actions/setup-node@v4\n      if: true\n      with:\n        node-version: '20'\n\n    - name: Install dependencies\n      if: true\n      run: |\n        env HOMEBREW_NO_AUTO_UPDATE=1 brew install libmagic\n\n    - name: Install an alias\n      # This is probably not strictly needed, but sentencepiece build script reports\n      # errors without it.\n      if: true\n      run: |\n        printf '#!/bin/sh\\n\\nexec sysctl -n hw.logicalcpu' > /usr/local/bin/nproc\n        chmod +x /usr/local/bin/nproc\n\n    - name: Build\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        BUILD_IS_RELEASE: \"true\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_ARCH: \"aarch64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        BUILD_GENERIC: true\n        CMAKE_POLICY_VERSION_MINIMUM: '3.5'\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n      run: |\n        edgedb-pkg/integration/macos/build.sh\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-macos-aarch64\n        path: artifacts/macos-aarch64\n\n  test-debian-buster-x86_64:\n    needs: [build-debian-buster-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-x86_64\n        path: artifacts/debian-buster\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-buster:latest\n      env:\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-buster-aarch64:\n    needs: [build-debian-buster-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-aarch64\n        path: artifacts/debian-buster\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-buster:latest\n      env:\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-bullseye-x86_64:\n    needs: [build-debian-bullseye-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-x86_64\n        path: artifacts/debian-bullseye\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-bullseye:latest\n      env:\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-bullseye-aarch64:\n    needs: [build-debian-bullseye-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-aarch64\n        path: artifacts/debian-bullseye\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-bullseye:latest\n      env:\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-bookworm-x86_64:\n    needs: [build-debian-bookworm-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-x86_64\n        path: artifacts/debian-bookworm\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-bookworm:latest\n      env:\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-bookworm-aarch64:\n    needs: [build-debian-bookworm-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-aarch64\n        path: artifacts/debian-bookworm\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-bookworm:latest\n      env:\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-focal-x86_64:\n    needs: [build-ubuntu-focal-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-x86_64\n        path: artifacts/ubuntu-focal\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-focal:latest\n      env:\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-focal-aarch64:\n    needs: [build-ubuntu-focal-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-aarch64\n        path: artifacts/ubuntu-focal\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-focal:latest\n      env:\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-jammy-x86_64:\n    needs: [build-ubuntu-jammy-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-x86_64\n        path: artifacts/ubuntu-jammy\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-jammy:latest\n      env:\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-jammy-aarch64:\n    needs: [build-ubuntu-jammy-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-aarch64\n        path: artifacts/ubuntu-jammy\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-jammy:latest\n      env:\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-noble-x86_64:\n    needs: [build-ubuntu-noble-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-x86_64\n        path: artifacts/ubuntu-noble\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-noble:latest\n      env:\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-noble-aarch64:\n    needs: [build-ubuntu-noble-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-aarch64\n        path: artifacts/ubuntu-noble\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-noble:latest\n      env:\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-centos-8-x86_64:\n    needs: [build-centos-8-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-x86_64\n        path: artifacts/centos-8\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-centos-8:latest\n      env:\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-centos-8-aarch64:\n    needs: [build-centos-8-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-aarch64\n        path: artifacts/centos-8\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-centos-8:latest\n      env:\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-rockylinux-9-x86_64:\n    needs: [build-rockylinux-9-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-x86_64\n        path: artifacts/rockylinux-9\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-rockylinux-9:latest\n      env:\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-rockylinux-9-aarch64:\n    needs: [build-rockylinux-9-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-aarch64\n        path: artifacts/rockylinux-9\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-rockylinux-9:latest\n      env:\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-linux-x86_64:\n    needs: [build-linux-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-linux-x86_64:latest\n      env:\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-linux-aarch64:\n    needs: [build-linux-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-linux-aarch64:latest\n      env:\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-linuxmusl-x86_64:\n    needs: [build-linuxmusl-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-linuxmusl-x86_64:latest\n      env:\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_LIBC: \"musl\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-linuxmusl-aarch64:\n    needs: [build-linuxmusl-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-linuxmusl-aarch64:latest\n      env:\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_LIBC: \"musl\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-macos-x86_64:\n    needs: [build-macos-x86_64]\n    runs-on: ['macos-13']\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-macos-x86_64\n        path: artifacts/macos-x86_64\n\n    - name: Test\n      env:\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" test_dump*.py test_backend_*.py test_database.py test_server_*.py test_edgeql_ddl.py test_session.py\n\"\n      run: |\n        # Bump shmmax and shmall to avoid test failures.\n        sudo sysctl -w kern.sysv.shmmax=12582912\n        sudo sysctl -w kern.sysv.shmall=12582912\n        edgedb-pkg/integration/macos/test.sh\n\n  test-macos-aarch64:\n    needs: [build-macos-aarch64]\n    runs-on: ['macos-14']\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-macos-aarch64\n        path: artifacts/macos-aarch64\n\n    - name: Test\n      env:\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n      run: |\n        edgedb-pkg/integration/macos/test.sh\n  collect:\n    needs:\n    - test-debian-buster-x86_64\n    - test-debian-buster-aarch64\n    - test-debian-bullseye-x86_64\n    - test-debian-bullseye-aarch64\n    - test-debian-bookworm-x86_64\n    - test-debian-bookworm-aarch64\n    - test-ubuntu-focal-x86_64\n    - test-ubuntu-focal-aarch64\n    - test-ubuntu-jammy-x86_64\n    - test-ubuntu-jammy-aarch64\n    - test-ubuntu-noble-x86_64\n    - test-ubuntu-noble-aarch64\n    - test-centos-8-x86_64\n    - test-centos-8-aarch64\n    - test-rockylinux-9-x86_64\n    - test-rockylinux-9-aarch64\n    - test-linux-x86_64\n    - test-linux-aarch64\n    - test-linuxmusl-x86_64\n    - test-linuxmusl-aarch64\n    - test-macos-x86_64\n    - test-macos-aarch64\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo 'All build+tests passed, ready to publish now!'\n\n  publish-debian-buster-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-x86_64\n        path: artifacts/debian-buster\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-buster-x86_64:\n    needs: [publish-debian-buster-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-x86_64\n        path: artifacts/debian-buster\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-buster\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-buster:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-debian-buster-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-aarch64\n        path: artifacts/debian-buster\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-buster-aarch64:\n    needs: [publish-debian-buster-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-aarch64\n        path: artifacts/debian-buster\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-buster\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-buster:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-debian-bullseye-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-x86_64\n        path: artifacts/debian-bullseye\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-bullseye-x86_64:\n    needs: [publish-debian-bullseye-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-x86_64\n        path: artifacts/debian-bullseye\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-bullseye\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-bullseye:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-debian-bullseye-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-aarch64\n        path: artifacts/debian-bullseye\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-bullseye-aarch64:\n    needs: [publish-debian-bullseye-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-aarch64\n        path: artifacts/debian-bullseye\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-bullseye\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-bullseye:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-debian-bookworm-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-x86_64\n        path: artifacts/debian-bookworm\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-bookworm-x86_64:\n    needs: [publish-debian-bookworm-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-x86_64\n        path: artifacts/debian-bookworm\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-bookworm\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-bookworm:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-debian-bookworm-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-aarch64\n        path: artifacts/debian-bookworm\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-bookworm-aarch64:\n    needs: [publish-debian-bookworm-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-aarch64\n        path: artifacts/debian-bookworm\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-bookworm\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-bookworm:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-focal-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-x86_64\n        path: artifacts/ubuntu-focal\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-focal-x86_64:\n    needs: [publish-ubuntu-focal-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-x86_64\n        path: artifacts/ubuntu-focal\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-focal\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-focal:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-focal-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-aarch64\n        path: artifacts/ubuntu-focal\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-focal-aarch64:\n    needs: [publish-ubuntu-focal-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-aarch64\n        path: artifacts/ubuntu-focal\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-focal\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-focal:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-jammy-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-x86_64\n        path: artifacts/ubuntu-jammy\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-jammy-x86_64:\n    needs: [publish-ubuntu-jammy-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-x86_64\n        path: artifacts/ubuntu-jammy\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-jammy\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-jammy:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-jammy-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-aarch64\n        path: artifacts/ubuntu-jammy\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-jammy-aarch64:\n    needs: [publish-ubuntu-jammy-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-aarch64\n        path: artifacts/ubuntu-jammy\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-jammy\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-jammy:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-noble-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-x86_64\n        path: artifacts/ubuntu-noble\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-noble-x86_64:\n    needs: [publish-ubuntu-noble-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-x86_64\n        path: artifacts/ubuntu-noble\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-noble\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-noble:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-noble-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-aarch64\n        path: artifacts/ubuntu-noble\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-noble-aarch64:\n    needs: [publish-ubuntu-noble-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-aarch64\n        path: artifacts/ubuntu-noble\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-noble\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-noble:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-centos-8-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-x86_64\n        path: artifacts/centos-8\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-centos-8-x86_64:\n    needs: [publish-centos-8-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-x86_64\n        path: artifacts/centos-8\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: centos-8\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-centos-8:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-centos-8-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-aarch64\n        path: artifacts/centos-8\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-centos-8-aarch64:\n    needs: [publish-centos-8-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-aarch64\n        path: artifacts/centos-8\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: centos-8\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-centos-8:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-rockylinux-9-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-x86_64\n        path: artifacts/rockylinux-9\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-rockylinux-9-x86_64:\n    needs: [publish-rockylinux-9-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-x86_64\n        path: artifacts/rockylinux-9\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: rockylinux-9\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-rockylinux-9:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-rockylinux-9-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-aarch64\n        path: artifacts/rockylinux-9\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-rockylinux-9-aarch64:\n    needs: [publish-rockylinux-9-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-aarch64\n        path: artifacts/rockylinux-9\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: rockylinux-9\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-rockylinux-9:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-linux-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-linux-x86_64:\n    needs: [publish-linux-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: linux-x86_64\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-linux-x86_64:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-linux-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-linux-aarch64:\n    needs: [publish-linux-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: linux-aarch64\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-linux-aarch64:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-linuxmusl-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_LIBC: \"musl\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-linuxmusl-x86_64:\n    needs: [publish-linuxmusl-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: linuxmusl-x86_64\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-linuxmusl-x86_64:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-linuxmusl-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_LIBC: \"musl\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-linuxmusl-aarch64:\n    needs: [publish-linuxmusl-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: linuxmusl-aarch64\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-linuxmusl-aarch64:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-macos-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-macos-x86_64\n        path: artifacts/macos-x86_64\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: macos-x86_64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  publish-macos-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-macos-aarch64\n        path: artifacts/macos-aarch64\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: macos-aarch64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  publish-docker:\n    needs:\n      - check-published-debian-bookworm-x86_64\n      - check-published-debian-bookworm-aarch64\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        repository: geldata/gel-docker\n        ref: master\n        path: dockerfile\n\n    - name: Login to Docker Hub\n      uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0\n      with:\n        username: ${{ secrets.DOCKER_USERNAME }}\n        password: ${{ secrets.DOCKER_PASSWORD }}\n\n    - name: Login to GitHub Container Registry\n      uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0\n      with:\n        registry: ghcr.io\n        username: \"edgedb-ci\"\n        password: ${{ secrets.GITHUB_CI_BOT_TOKEN }}\n\n    - env:\n        VERSION_SLOT: \"${{ needs.check-published-debian-bookworm-x86_64.outputs.version-slot }}\"\n        VERSION_CORE: \"${{ needs.check-published-debian-bookworm-x86_64.outputs.version-core }}\"\n        CATALOG_VERSION: \"${{ needs.check-published-debian-bookworm-x86_64.outputs.catalog-version }}\"\n        PKG_SUBDIST: \"\"\n      id: tags\n      run: |\n        set -e\n\n        url='https://registry.hub.docker.com/v2/repositories/geldata/gel/tags?page_size=100'\n        repo_tags=$(\n          while [ -n \"$url\" ]; do\n            resp=$(curl -L -s \"$url\")\n            url=$(echo \"$resp\" | jq -r \".next\")\n            if [ \"$url\" = \"null\" ] || [ -z \"$url\" ]; then\n              break\n            fi\n            echo \"$resp\" | jq -r '.\"results\"[][\"name\"]'\n          done | grep \"^[[:digit:]]\\+.*\" | grep -v \"alpha\\|beta\\|rc\" || :\n        )\n\n        tags=()\n\n        if [ \"$PKG_SUBDIST\" = \"nightly\" ]; then\n          tags+=(\n            \"nightly\"\n            \"nightly_${VERSION_SLOT}_cv${CATALOG_VERSION}\"\n          )\n        else\n          tags+=( \"$VERSION_CORE\" )\n\n          top=$(printf \"%s\\n%s\\n\" \"$VERSION_CORE\" \"$repo_tags\" \\\n                | grep \"^${VERSION_SLOT}[\\.-]\" \\\n                | sort --version-sort --reverse | head -n 1)\n          if [ \"$top\" == \"$VERSION_CORE\" ]; then\n            tags+=( \"$VERSION_SLOT\" )\n          fi\n\n          if [ -z \"$PKG_SUBDIST\" ]; then\n            top=$(printf \"%s\\n%s\\n\" \"$VERSION_CORE\" \"$repo_tags\" \\\n                  | sort --version-sort --reverse | head -n 1)\n            if [ \"$top\" == \"$VERSION_CORE\" ]; then\n              tags+=( \"latest\" )\n            fi\n          fi\n        fi\n\n        fq_tags=()\n        images=(\"geldata/gel\" \"ghcr.io/geldata/gel\")\n\n        for image in \"${images[@]}\"; do\n          fq_tags+=(\"${tags[@]/#/${image}:}\")\n        done\n\n        IFS=,\n        echo \"tags=${fq_tags[*]}\" >> $GITHUB_OUTPUT\n\n    - name: Set up QEMU\n      uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0\n\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0\n\n    - name: Build and Publish Docker Image\n      uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.10.0\n      with:\n        push: true\n        provenance: mode=max\n        tags: \"${{ steps.tags.outputs.tags }}\"\n        context: dockerfile\n        build-args: |\n          version=${{ needs.check-published-debian-bookworm-x86_64.outputs.version-slot }}\n          exact_version=${{ needs.check-published-debian-bookworm-x86_64.outputs.version-core }}\n        platforms: linux/amd64,linux/arm64\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n\n    needs:\n      - prep\n      - collect\n      - build-debian-buster-x86_64\n      - test-debian-buster-x86_64\n      - publish-debian-buster-x86_64\n      - check-published-debian-buster-x86_64\n      - build-debian-buster-aarch64\n      - test-debian-buster-aarch64\n      - publish-debian-buster-aarch64\n      - check-published-debian-buster-aarch64\n      - build-debian-bullseye-x86_64\n      - test-debian-bullseye-x86_64\n      - publish-debian-bullseye-x86_64\n      - check-published-debian-bullseye-x86_64\n      - build-debian-bullseye-aarch64\n      - test-debian-bullseye-aarch64\n      - publish-debian-bullseye-aarch64\n      - check-published-debian-bullseye-aarch64\n      - build-debian-bookworm-x86_64\n      - test-debian-bookworm-x86_64\n      - publish-debian-bookworm-x86_64\n      - check-published-debian-bookworm-x86_64\n      - build-debian-bookworm-aarch64\n      - test-debian-bookworm-aarch64\n      - publish-debian-bookworm-aarch64\n      - check-published-debian-bookworm-aarch64\n      - build-ubuntu-focal-x86_64\n      - test-ubuntu-focal-x86_64\n      - publish-ubuntu-focal-x86_64\n      - check-published-ubuntu-focal-x86_64\n      - build-ubuntu-focal-aarch64\n      - test-ubuntu-focal-aarch64\n      - publish-ubuntu-focal-aarch64\n      - check-published-ubuntu-focal-aarch64\n      - build-ubuntu-jammy-x86_64\n      - test-ubuntu-jammy-x86_64\n      - publish-ubuntu-jammy-x86_64\n      - check-published-ubuntu-jammy-x86_64\n      - build-ubuntu-jammy-aarch64\n      - test-ubuntu-jammy-aarch64\n      - publish-ubuntu-jammy-aarch64\n      - check-published-ubuntu-jammy-aarch64\n      - build-ubuntu-noble-x86_64\n      - test-ubuntu-noble-x86_64\n      - publish-ubuntu-noble-x86_64\n      - check-published-ubuntu-noble-x86_64\n      - build-ubuntu-noble-aarch64\n      - test-ubuntu-noble-aarch64\n      - publish-ubuntu-noble-aarch64\n      - check-published-ubuntu-noble-aarch64\n      - build-centos-8-x86_64\n      - test-centos-8-x86_64\n      - publish-centos-8-x86_64\n      - check-published-centos-8-x86_64\n      - build-centos-8-aarch64\n      - test-centos-8-aarch64\n      - publish-centos-8-aarch64\n      - check-published-centos-8-aarch64\n      - build-rockylinux-9-x86_64\n      - test-rockylinux-9-x86_64\n      - publish-rockylinux-9-x86_64\n      - check-published-rockylinux-9-x86_64\n      - build-rockylinux-9-aarch64\n      - test-rockylinux-9-aarch64\n      - publish-rockylinux-9-aarch64\n      - check-published-rockylinux-9-aarch64\n      - build-linux-x86_64\n      - test-linux-x86_64\n      - publish-linux-x86_64\n      - check-published-linux-x86_64\n      - build-linux-aarch64\n      - test-linux-aarch64\n      - publish-linux-aarch64\n      - check-published-linux-aarch64\n      - build-linuxmusl-x86_64\n      - test-linuxmusl-x86_64\n      - publish-linuxmusl-x86_64\n      - check-published-linuxmusl-x86_64\n      - build-linuxmusl-aarch64\n      - test-linuxmusl-aarch64\n      - publish-linuxmusl-aarch64\n      - check-published-linuxmusl-aarch64\n      - build-macos-x86_64\n      - test-macos-x86_64\n      - publish-macos-x86_64\n      - build-macos-aarch64\n      - test-macos-aarch64\n      - publish-macos-aarch64\n      - publish-docker\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows/build.testing.yml",
    "content": "name: Build Test and Publish a Testing Release\n\non:\n  workflow_dispatch:\n    inputs:\n      gelpkg_ref:\n        description: \"gel-pkg git ref used to build the packages\"\n        default: \"master\"\n      metapkg_ref:\n        description: \"metapkg git ref used to build the packages\"\n        default: \"master\"\n\njobs:\n  prep:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n\n\n\n  build-debian-buster-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-buster:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-buster-x86_64\n        path: artifacts/debian-buster\n\n  build-debian-buster-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-buster:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-buster-aarch64\n        path: artifacts/debian-buster\n\n  build-debian-bullseye-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-bullseye:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-bullseye-x86_64\n        path: artifacts/debian-bullseye\n\n  build-debian-bullseye-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-bullseye:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-bullseye-aarch64\n        path: artifacts/debian-bullseye\n\n  build-debian-bookworm-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-bookworm:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-bookworm-x86_64\n        path: artifacts/debian-bookworm\n\n  build-debian-bookworm-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-debian-bookworm:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-debian-bookworm-aarch64\n        path: artifacts/debian-bookworm\n\n  build-ubuntu-focal-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-focal:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-focal-x86_64\n        path: artifacts/ubuntu-focal\n\n  build-ubuntu-focal-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-focal:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-focal-aarch64\n        path: artifacts/ubuntu-focal\n\n  build-ubuntu-jammy-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-jammy:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-jammy-x86_64\n        path: artifacts/ubuntu-jammy\n\n  build-ubuntu-jammy-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-jammy:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-jammy-aarch64\n        path: artifacts/ubuntu-jammy\n\n  build-ubuntu-noble-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-noble:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-noble-x86_64\n        path: artifacts/ubuntu-noble\n\n  build-ubuntu-noble-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-ubuntu-noble:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-ubuntu-noble-aarch64\n        path: artifacts/ubuntu-noble\n\n  build-centos-8-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-centos-8:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-centos-8-x86_64\n        path: artifacts/centos-8\n\n  build-centos-8-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-centos-8:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-centos-8-aarch64\n        path: artifacts/centos-8\n\n  build-rockylinux-9-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-rockylinux-9:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-rockylinux-9-x86_64\n        path: artifacts/rockylinux-9\n\n  build-rockylinux-9-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-rockylinux-9:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-rockylinux-9-aarch64\n        path: artifacts/rockylinux-9\n\n  build-linux-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linux-x86_64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        BUILD_GENERIC: true\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n  build-linux-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linux-aarch64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        BUILD_GENERIC: true\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n  build-linuxmusl-x86_64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linuxmusl-x86_64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        BUILD_GENERIC: true\n        PKG_PLATFORM_LIBC: \"musl\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n  build-linuxmusl-aarch64:\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n    needs: prep\n\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-linuxmusl-aarch64:latest\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        BUILD_IS_RELEASE: \"true\"\n        BUILD_GENERIC: true\n        PKG_PLATFORM_LIBC: \"musl\"\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n  build-macos-x86_64:\n    runs-on: ['macos-13']\n    needs: prep\n\n\n    steps:\n    - name: Update Homebrew before installing Rust toolchain\n      run: |\n        # Homebrew renamed `rustup-init` to `rustup`:\n        #   https://github.com/Homebrew/homebrew-core/pull/177840\n        # But the GitHub Action runner is not updated with this change yet.\n        # This caused the later `brew update` in step `Build` to relink Rust\n        # toolchain executables, overwriting the custom toolchain installed by\n        # `dsherret/rust-toolchain-file`. So let's just run `brew update` early.\n        brew update\n\n    - uses: actions/checkout@v4\n      if: true\n      with:\n        sparse-checkout: |\n          rust-toolchain.toml\n        sparse-checkout-cone-mode: false\n\n    - name: Install Rust toolchain\n      uses: dsherret/rust-toolchain-file@v1\n      if: true\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      if: true\n      with:\n        python-version: \"3.12\"\n\n    - name: Set up NodeJS\n      uses: actions/setup-node@v4\n      if: true\n      with:\n        node-version: '20'\n\n    - name: Install dependencies\n      if: true\n      run: |\n        env HOMEBREW_NO_AUTO_UPDATE=1 brew install libmagic\n\n    - name: Install an alias\n      # This is probably not strictly needed, but sentencepiece build script reports\n      # errors without it.\n      if: true\n      run: |\n        printf '#!/bin/sh\\n\\nexec sysctl -n hw.logicalcpu' > /usr/local/bin/nproc\n        chmod +x /usr/local/bin/nproc\n\n    - name: Build\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        BUILD_IS_RELEASE: \"true\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_ARCH: \"x86_64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        BUILD_GENERIC: true\n        CMAKE_POLICY_VERSION_MINIMUM: '3.5'\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n      run: |\n        edgedb-pkg/integration/macos/build.sh\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-macos-x86_64\n        path: artifacts/macos-x86_64\n\n  build-macos-aarch64:\n    runs-on: ['macos-14']\n    needs: prep\n\n\n    steps:\n    - name: Update Homebrew before installing Rust toolchain\n      run: |\n        # Homebrew renamed `rustup-init` to `rustup`:\n        #   https://github.com/Homebrew/homebrew-core/pull/177840\n        # But the GitHub Action runner is not updated with this change yet.\n        # This caused the later `brew update` in step `Build` to relink Rust\n        # toolchain executables, overwriting the custom toolchain installed by\n        # `dsherret/rust-toolchain-file`. So let's just run `brew update` early.\n        brew update\n\n    - uses: actions/checkout@v4\n      if: true\n      with:\n        sparse-checkout: |\n          rust-toolchain.toml\n        sparse-checkout-cone-mode: false\n\n    - name: Install Rust toolchain\n      uses: dsherret/rust-toolchain-file@v1\n      if: true\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      if: true\n      with:\n        python-version: \"3.12\"\n\n    - name: Set up NodeJS\n      uses: actions/setup-node@v4\n      if: true\n      with:\n        node-version: '20'\n\n    - name: Install dependencies\n      if: true\n      run: |\n        env HOMEBREW_NO_AUTO_UPDATE=1 brew install libmagic\n\n    - name: Install an alias\n      # This is probably not strictly needed, but sentencepiece build script reports\n      # errors without it.\n      if: true\n      run: |\n        printf '#!/bin/sh\\n\\nexec sysctl -n hw.logicalcpu' > /usr/local/bin/nproc\n        chmod +x /usr/local/bin/nproc\n\n    - name: Build\n      env:\n        PACKAGE: \"edgedbpkg.edgedb:Gel\"\n        SRC_REF: \"${{ github.sha }}\"\n        BUILD_IS_RELEASE: \"true\"\n        PKG_REVISION: \"<current-date>\"\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_ARCH: \"aarch64\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        BUILD_GENERIC: true\n        CMAKE_POLICY_VERSION_MINIMUM: '3.5'\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n      run: |\n        edgedb-pkg/integration/macos/build.sh\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-macos-aarch64\n        path: artifacts/macos-aarch64\n\n  test-debian-buster-x86_64:\n    needs: [build-debian-buster-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-x86_64\n        path: artifacts/debian-buster\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-buster:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-buster-aarch64:\n    needs: [build-debian-buster-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-aarch64\n        path: artifacts/debian-buster\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-buster:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-bullseye-x86_64:\n    needs: [build-debian-bullseye-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-x86_64\n        path: artifacts/debian-bullseye\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-bullseye:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-bullseye-aarch64:\n    needs: [build-debian-bullseye-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-aarch64\n        path: artifacts/debian-bullseye\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-bullseye:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-bookworm-x86_64:\n    needs: [build-debian-bookworm-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-x86_64\n        path: artifacts/debian-bookworm\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-bookworm:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-debian-bookworm-aarch64:\n    needs: [build-debian-bookworm-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-aarch64\n        path: artifacts/debian-bookworm\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-debian-bookworm:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-focal-x86_64:\n    needs: [build-ubuntu-focal-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-x86_64\n        path: artifacts/ubuntu-focal\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-focal:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-focal-aarch64:\n    needs: [build-ubuntu-focal-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-aarch64\n        path: artifacts/ubuntu-focal\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-focal:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-jammy-x86_64:\n    needs: [build-ubuntu-jammy-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-x86_64\n        path: artifacts/ubuntu-jammy\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-jammy:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-jammy-aarch64:\n    needs: [build-ubuntu-jammy-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-aarch64\n        path: artifacts/ubuntu-jammy\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-jammy:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-noble-x86_64:\n    needs: [build-ubuntu-noble-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-x86_64\n        path: artifacts/ubuntu-noble\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-noble:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-ubuntu-noble-aarch64:\n    needs: [build-ubuntu-noble-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-aarch64\n        path: artifacts/ubuntu-noble\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-ubuntu-noble:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-centos-8-x86_64:\n    needs: [build-centos-8-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-x86_64\n        path: artifacts/centos-8\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-centos-8:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-centos-8-aarch64:\n    needs: [build-centos-8-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-aarch64\n        path: artifacts/centos-8\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-centos-8:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-rockylinux-9-x86_64:\n    needs: [build-rockylinux-9-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-x86_64\n        path: artifacts/rockylinux-9\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-rockylinux-9:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-rockylinux-9-aarch64:\n    needs: [build-rockylinux-9-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-aarch64\n        path: artifacts/rockylinux-9\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-rockylinux-9:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-linux-x86_64:\n    needs: [build-linux-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-linux-aarch64:\n    needs: [build-linux-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-linux-aarch64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_LIBC: \"\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-linuxmusl-x86_64:\n    needs: [build-linuxmusl-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-linuxmusl-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_LIBC: \"musl\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-linuxmusl-aarch64:\n    needs: [build-linuxmusl-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-linuxmusl-aarch64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_LIBC: \"musl\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: 0\n\n  test-macos-x86_64:\n    needs: [build-macos-x86_64]\n    runs-on: ['macos-13']\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-macos-x86_64\n        path: artifacts/macos-x86_64\n\n    - name: Test\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" test_dump*.py test_backend_*.py test_database.py test_server_*.py test_edgeql_ddl.py test_session.py\n\"\n      run: |\n        # Bump shmmax and shmall to avoid test failures.\n        sudo sysctl -w kern.sysv.shmmax=12582912\n        sudo sysctl -w kern.sysv.shmall=12582912\n        edgedb-pkg/integration/macos/test.sh\n\n  test-macos-aarch64:\n    needs: [build-macos-aarch64]\n    runs-on: ['macos-14']\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-macos-aarch64\n        path: artifacts/macos-aarch64\n\n    - name: Test\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_TEST_SELECT: \"\"\n        PKG_TEST_EXCLUDE: \"\"\n        PKG_TEST_FILES: \" \"\n      run: |\n        edgedb-pkg/integration/macos/test.sh\n  collect:\n    needs:\n    - test-debian-buster-x86_64\n    - test-debian-buster-aarch64\n    - test-debian-bullseye-x86_64\n    - test-debian-bullseye-aarch64\n    - test-debian-bookworm-x86_64\n    - test-debian-bookworm-aarch64\n    - test-ubuntu-focal-x86_64\n    - test-ubuntu-focal-aarch64\n    - test-ubuntu-jammy-x86_64\n    - test-ubuntu-jammy-aarch64\n    - test-ubuntu-noble-x86_64\n    - test-ubuntu-noble-aarch64\n    - test-centos-8-x86_64\n    - test-centos-8-aarch64\n    - test-rockylinux-9-x86_64\n    - test-rockylinux-9-aarch64\n    - test-linux-x86_64\n    - test-linux-aarch64\n    - test-linuxmusl-x86_64\n    - test-linuxmusl-aarch64\n    - test-macos-x86_64\n    - test-macos-aarch64\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo 'All build+tests passed, ready to publish now!'\n\n  publish-debian-buster-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-x86_64\n        path: artifacts/debian-buster\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-buster-x86_64:\n    needs: [publish-debian-buster-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-x86_64\n        path: artifacts/debian-buster\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-buster\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-buster:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-debian-buster-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-aarch64\n        path: artifacts/debian-buster\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-buster-aarch64:\n    needs: [publish-debian-buster-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-buster-aarch64\n        path: artifacts/debian-buster\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-buster\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-buster:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"buster\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-debian-bullseye-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-x86_64\n        path: artifacts/debian-bullseye\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-bullseye-x86_64:\n    needs: [publish-debian-bullseye-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-x86_64\n        path: artifacts/debian-bullseye\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-bullseye\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-bullseye:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-debian-bullseye-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-aarch64\n        path: artifacts/debian-bullseye\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-bullseye-aarch64:\n    needs: [publish-debian-bullseye-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bullseye-aarch64\n        path: artifacts/debian-bullseye\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-bullseye\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-bullseye:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bullseye\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-debian-bookworm-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-x86_64\n        path: artifacts/debian-bookworm\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-bookworm-x86_64:\n    needs: [publish-debian-bookworm-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-x86_64\n        path: artifacts/debian-bookworm\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-bookworm\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-bookworm:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-debian-bookworm-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-aarch64\n        path: artifacts/debian-bookworm\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-debian-bookworm-aarch64:\n    needs: [publish-debian-bookworm-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-debian-bookworm-aarch64\n        path: artifacts/debian-bookworm\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: debian-bookworm\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-debian-bookworm:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"debian\"\n        PKG_PLATFORM_VERSION: \"bookworm\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-focal-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-x86_64\n        path: artifacts/ubuntu-focal\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-focal-x86_64:\n    needs: [publish-ubuntu-focal-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-x86_64\n        path: artifacts/ubuntu-focal\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-focal\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-focal:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-focal-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-aarch64\n        path: artifacts/ubuntu-focal\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-focal-aarch64:\n    needs: [publish-ubuntu-focal-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-focal-aarch64\n        path: artifacts/ubuntu-focal\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-focal\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-focal:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"focal\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-jammy-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-x86_64\n        path: artifacts/ubuntu-jammy\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-jammy-x86_64:\n    needs: [publish-ubuntu-jammy-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-x86_64\n        path: artifacts/ubuntu-jammy\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-jammy\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-jammy:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-jammy-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-aarch64\n        path: artifacts/ubuntu-jammy\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-jammy-aarch64:\n    needs: [publish-ubuntu-jammy-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-jammy-aarch64\n        path: artifacts/ubuntu-jammy\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-jammy\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-jammy:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"jammy\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-noble-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-x86_64\n        path: artifacts/ubuntu-noble\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-noble-x86_64:\n    needs: [publish-ubuntu-noble-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-x86_64\n        path: artifacts/ubuntu-noble\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-noble\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-noble:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-ubuntu-noble-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-aarch64\n        path: artifacts/ubuntu-noble\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-ubuntu-noble-aarch64:\n    needs: [publish-ubuntu-noble-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-ubuntu-noble-aarch64\n        path: artifacts/ubuntu-noble\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: ubuntu-noble\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-ubuntu-noble:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"ubuntu\"\n        PKG_PLATFORM_VERSION: \"noble\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-centos-8-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-x86_64\n        path: artifacts/centos-8\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-centos-8-x86_64:\n    needs: [publish-centos-8-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-x86_64\n        path: artifacts/centos-8\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: centos-8\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-centos-8:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-centos-8-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-aarch64\n        path: artifacts/centos-8\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-centos-8-aarch64:\n    needs: [publish-centos-8-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-centos-8-aarch64\n        path: artifacts/centos-8\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: centos-8\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-centos-8:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"centos\"\n        PKG_PLATFORM_VERSION: \"8\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-rockylinux-9-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-x86_64\n        path: artifacts/rockylinux-9\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-rockylinux-9-x86_64:\n    needs: [publish-rockylinux-9-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-x86_64\n        path: artifacts/rockylinux-9\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: rockylinux-9\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-rockylinux-9:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-rockylinux-9-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-aarch64\n        path: artifacts/rockylinux-9\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-rockylinux-9-aarch64:\n    needs: [publish-rockylinux-9-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-rockylinux-9-aarch64\n        path: artifacts/rockylinux-9\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: rockylinux-9\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-rockylinux-9:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"rockylinux\"\n        PKG_PLATFORM_VERSION: \"9\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-linux-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-linux-x86_64:\n    needs: [publish-linux-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-x86_64\n        path: artifacts/linux-x86_64\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: linux-x86_64\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-linux-x86_64:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-linux-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_LIBC: \"\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-linux-aarch64:\n    needs: [publish-linux-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linux-aarch64\n        path: artifacts/linux-aarch64\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: linux-aarch64\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-linux-aarch64:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-linuxmusl-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_PLATFORM_LIBC: \"musl\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-linuxmusl-x86_64:\n    needs: [publish-linuxmusl-x86_64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'x64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-x86_64\n        path: artifacts/linuxmusl-x86_64\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: linuxmusl-x86_64\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-linuxmusl-x86_64:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-linuxmusl-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_PLATFORM_LIBC: \"musl\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published-linuxmusl-aarch64:\n    needs: [publish-linuxmusl-aarch64]\n    runs-on: ['package-builder', 'self-hosted', 'linux', 'arm64']\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-linuxmusl-aarch64\n        path: artifacts/linuxmusl-aarch64\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: linuxmusl-aarch64\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-linuxmusl-aarch64:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        PKG_SUBDIST: \"testing\"\n        PACKAGE_SERVER: sftp://uploader@package-upload.edgedb.net:22/\n        PKG_PLATFORM: \"linux\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n\n  publish-macos-x86_64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-macos-x86_64\n        path: artifacts/macos-x86_64\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: macos-x86_64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"x86_64\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  publish-macos-aarch64:\n    needs: [collect]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-macos-aarch64\n        path: artifacts/macos-aarch64\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: macos-aarch64\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        PKG_SUBDIST: \"testing\"\n        PKG_PLATFORM: \"macos\"\n        PKG_PLATFORM_VERSION: \"aarch64\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  publish-docker:\n    needs:\n      - check-published-debian-bookworm-x86_64\n      - check-published-debian-bookworm-aarch64\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        repository: geldata/gel-docker\n        ref: master\n        path: dockerfile\n\n    - name: Login to Docker Hub\n      uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0\n      with:\n        username: ${{ secrets.DOCKER_USERNAME }}\n        password: ${{ secrets.DOCKER_PASSWORD }}\n\n    - name: Login to GitHub Container Registry\n      uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0\n      with:\n        registry: ghcr.io\n        username: \"edgedb-ci\"\n        password: ${{ secrets.GITHUB_CI_BOT_TOKEN }}\n\n    - env:\n        VERSION_SLOT: \"${{ needs.check-published-debian-bookworm-x86_64.outputs.version-slot }}\"\n        VERSION_CORE: \"${{ needs.check-published-debian-bookworm-x86_64.outputs.version-core }}\"\n        CATALOG_VERSION: \"${{ needs.check-published-debian-bookworm-x86_64.outputs.catalog-version }}\"\n        PKG_SUBDIST: \"testing\"\n      id: tags\n      run: |\n        set -e\n\n        url='https://registry.hub.docker.com/v2/repositories/geldata/gel/tags?page_size=100'\n        repo_tags=$(\n          while [ -n \"$url\" ]; do\n            resp=$(curl -L -s \"$url\")\n            url=$(echo \"$resp\" | jq -r \".next\")\n            if [ \"$url\" = \"null\" ] || [ -z \"$url\" ]; then\n              break\n            fi\n            echo \"$resp\" | jq -r '.\"results\"[][\"name\"]'\n          done | grep \"^[[:digit:]]\\+.*\" | grep -v \"alpha\\|beta\\|rc\" || :\n        )\n\n        tags=()\n\n        if [ \"$PKG_SUBDIST\" = \"nightly\" ]; then\n          tags+=(\n            \"nightly\"\n            \"nightly_${VERSION_SLOT}_cv${CATALOG_VERSION}\"\n          )\n        else\n          tags+=( \"$VERSION_CORE\" )\n\n          top=$(printf \"%s\\n%s\\n\" \"$VERSION_CORE\" \"$repo_tags\" \\\n                | grep \"^${VERSION_SLOT}[\\.-]\" \\\n                | sort --version-sort --reverse | head -n 1)\n          if [ \"$top\" == \"$VERSION_CORE\" ]; then\n            tags+=( \"$VERSION_SLOT\" )\n          fi\n\n          if [ -z \"$PKG_SUBDIST\" ]; then\n            top=$(printf \"%s\\n%s\\n\" \"$VERSION_CORE\" \"$repo_tags\" \\\n                  | sort --version-sort --reverse | head -n 1)\n            if [ \"$top\" == \"$VERSION_CORE\" ]; then\n              tags+=( \"latest\" )\n            fi\n          fi\n        fi\n\n        fq_tags=()\n        images=(\"geldata/gel\" \"ghcr.io/geldata/gel\")\n\n        for image in \"${images[@]}\"; do\n          fq_tags+=(\"${tags[@]/#/${image}:}\")\n        done\n\n        IFS=,\n        echo \"tags=${fq_tags[*]}\" >> $GITHUB_OUTPUT\n\n    - name: Set up QEMU\n      uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0\n\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0\n\n    - name: Build and Publish Docker Image\n      uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.10.0\n      with:\n        push: true\n        provenance: mode=max\n        tags: \"${{ steps.tags.outputs.tags }}\"\n        context: dockerfile\n        build-args: |\n          version=${{ needs.check-published-debian-bookworm-x86_64.outputs.version-slot }}\n          exact_version=${{ needs.check-published-debian-bookworm-x86_64.outputs.version-core }}\n          subdist=testing\n        platforms: linux/amd64,linux/arm64\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n\n    needs:\n      - prep\n      - collect\n      - build-debian-buster-x86_64\n      - test-debian-buster-x86_64\n      - publish-debian-buster-x86_64\n      - check-published-debian-buster-x86_64\n      - build-debian-buster-aarch64\n      - test-debian-buster-aarch64\n      - publish-debian-buster-aarch64\n      - check-published-debian-buster-aarch64\n      - build-debian-bullseye-x86_64\n      - test-debian-bullseye-x86_64\n      - publish-debian-bullseye-x86_64\n      - check-published-debian-bullseye-x86_64\n      - build-debian-bullseye-aarch64\n      - test-debian-bullseye-aarch64\n      - publish-debian-bullseye-aarch64\n      - check-published-debian-bullseye-aarch64\n      - build-debian-bookworm-x86_64\n      - test-debian-bookworm-x86_64\n      - publish-debian-bookworm-x86_64\n      - check-published-debian-bookworm-x86_64\n      - build-debian-bookworm-aarch64\n      - test-debian-bookworm-aarch64\n      - publish-debian-bookworm-aarch64\n      - check-published-debian-bookworm-aarch64\n      - build-ubuntu-focal-x86_64\n      - test-ubuntu-focal-x86_64\n      - publish-ubuntu-focal-x86_64\n      - check-published-ubuntu-focal-x86_64\n      - build-ubuntu-focal-aarch64\n      - test-ubuntu-focal-aarch64\n      - publish-ubuntu-focal-aarch64\n      - check-published-ubuntu-focal-aarch64\n      - build-ubuntu-jammy-x86_64\n      - test-ubuntu-jammy-x86_64\n      - publish-ubuntu-jammy-x86_64\n      - check-published-ubuntu-jammy-x86_64\n      - build-ubuntu-jammy-aarch64\n      - test-ubuntu-jammy-aarch64\n      - publish-ubuntu-jammy-aarch64\n      - check-published-ubuntu-jammy-aarch64\n      - build-ubuntu-noble-x86_64\n      - test-ubuntu-noble-x86_64\n      - publish-ubuntu-noble-x86_64\n      - check-published-ubuntu-noble-x86_64\n      - build-ubuntu-noble-aarch64\n      - test-ubuntu-noble-aarch64\n      - publish-ubuntu-noble-aarch64\n      - check-published-ubuntu-noble-aarch64\n      - build-centos-8-x86_64\n      - test-centos-8-x86_64\n      - publish-centos-8-x86_64\n      - check-published-centos-8-x86_64\n      - build-centos-8-aarch64\n      - test-centos-8-aarch64\n      - publish-centos-8-aarch64\n      - check-published-centos-8-aarch64\n      - build-rockylinux-9-x86_64\n      - test-rockylinux-9-x86_64\n      - publish-rockylinux-9-x86_64\n      - check-published-rockylinux-9-x86_64\n      - build-rockylinux-9-aarch64\n      - test-rockylinux-9-aarch64\n      - publish-rockylinux-9-aarch64\n      - check-published-rockylinux-9-aarch64\n      - build-linux-x86_64\n      - test-linux-x86_64\n      - publish-linux-x86_64\n      - check-published-linux-x86_64\n      - build-linux-aarch64\n      - test-linux-aarch64\n      - publish-linux-aarch64\n      - check-published-linux-aarch64\n      - build-linuxmusl-x86_64\n      - test-linuxmusl-x86_64\n      - publish-linuxmusl-x86_64\n      - check-published-linuxmusl-x86_64\n      - build-linuxmusl-aarch64\n      - test-linuxmusl-aarch64\n      - publish-linuxmusl-aarch64\n      - check-published-linuxmusl-aarch64\n      - build-macos-x86_64\n      - test-macos-x86_64\n      - publish-macos-x86_64\n      - build-macos-aarch64\n      - test-macos-aarch64\n      - publish-macos-aarch64\n      - publish-docker\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows/docs-preview-deploy.yml",
    "content": "name: Docs Preview Deploy\n\non:\n  pull_request:\n    paths:\n      - \"docs/**\"\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    permissions: write-all\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/github-script@v7\n        env:\n          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}\n          VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }}\n        with:\n          script: |\n            const script = require('./.github/scripts/docs/preview-deploy.js');\n            await script({github, context});\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "name: Deploy Documentation Changes\n\non:\n  push:\n    branches:\n      - master\n      - release/**\n    paths:\n      - \"docs/**\"\n  workflow_dispatch:\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Trigger vercel deploy hook\n        run: curl \\\n          --fail-with-body \\\n          --request POST \\\n          ${{ secrets.VERCEL_DOC_DEPLOY_URL_HOOK }}\n"
  },
  {
    "path": ".github/workflows/pull-request-meta.yml",
    "content": "name: Pull Request Meta\n\non:\n  pull_request:\n    types: [opened, edited, synchronize]\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number }}\n  cancel-in-progress: true\n\njobs:\n\n  test-pr:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - name: Verify that postgres/ was not changed unintentionally\n      env:\n        PR_TITLE: ${{ github.event.pull_request.title }}\n      shell: bash\n      run: |\n        required_prefix=\"Update bundled PostgreSQL\"\n\n        if [[ \"$PR_TITLE\" == $required_prefix* ]]; then\n          exit 0\n        fi\n\n        if git diff --quiet \\\n          ${{ github.event.pull_request.base.sha }} \\\n          ${{ github.event.pull_request.head.sha }} -- postgres/\n        then\n          echo 'all ok'\n        else\n          echo \"postgres/ submodule has been changed,\"\\\n            \"but PR title does not indicate that\"\n          echo \"(it should start with '$required_prefix')\"\n          exit 1\n        fi\n"
  },
  {
    "path": ".github/workflows/tests.ha.yml",
    "content": "name: High Availability Tests\n\non:\n  workflow_dispatch:\n    inputs: {}\n  workflow_run:\n    workflows: [\"Tests\"]\n    types:\n      - completed\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    if: github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch'\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n\n    - name: Cached requirements.txt\n      uses: actions/cache@v4\n      id: requirements-cache\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Compute requirements.txt\n      if: steps.requirements-cache.outputs.cache-hit != 'true'\n      run: |\n        python -m pip install pip-tools\n        pip-compile --no-strip-extras --all-build-deps \\\n          --extra test,language-server \\\n          --output-file requirements.txt pyproject.toml\n\n    - name: Install Python dependencies\n      run: |\n        python -c \"import sys; print(sys.prefix)\"\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Our HA tests currently only work on Postgres 14 (see #6332),\n    # so check it out before we compute our build cache keys.\n    - name: Switch back to Postgres 14\n      shell: bash\n      run: |\n        set -e\n        cd postgres\n        # Fetch postgres 14, since the clone was shallow\n        git fetch origin REL_14_8 --depth=1\n        # For whatever reason the tag doesn't get fetched, so find it\n        # at FETCH_HEAD\n        git checkout FETCH_HEAD\n\n    - name: Compute cache keys\n      env:\n        GIST_TOKEN: ${{ secrets.CI_BOT_GIST_TOKEN }}\n      run: |\n        mkdir -p shared-artifacts\n        if [ \"$(uname)\" = \"Darwin\" ]; then\n          find /usr/lib -type f -name 'lib*' -exec stat -f '%N %z' {} + | sort | shasum -a 256 | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        else\n          find /usr/lib -type f -name 'lib*' -printf '%P %s\\n' | sort | sha256sum | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        fi\n        python setup.py -q ci_helper --type rust >shared-artifacts/rust_cache_key.txt\n        python setup.py -q ci_helper --type ext >shared-artifacts/ext_cache_key.txt\n        python setup.py -q ci_helper --type parsers >shared-artifacts/parsers_cache_key.txt\n        python setup.py -q ci_helper --type postgres >shared-artifacts/postgres_git_rev.txt\n        python setup.py -q ci_helper --type libpg_query >shared-artifacts/libpg_query_git_rev.txt\n        echo 'f8cd94309eaccbfba5dea7835b88c78377608a37' >shared-artifacts/stolon_git_rev.txt\n        python setup.py -q ci_helper --type bootstrap >shared-artifacts/bootstrap_cache_key.txt\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo LIBPG_QUERY_GIT_REV=$(cat shared-artifacts/libpg_query_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    - name: Upload shared artifacts\n      uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n        retention-days: 1\n\n    # Restore binary cache\n\n    - name: Handle cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-v4-\n\n    - name: Handle cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Handle cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Handle cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Handle cached libpg_query build\n      uses: actions/cache@v4\n      id: libpg-query-cache\n      with:\n        path: edb/pgsql/parser/libpg_query/libpg_query.a\n        key: edb-libpg_query-v1-${{ env.LIBPG_QUERY_GIT_REV }}\n\n    # Install system dependencies for building\n\n    - name: Install system deps\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true'\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y uuid-dev libreadline-dev bison flex libprotobuf-c-dev\n\n    - name: Install Rust toolchain\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      uses: dsherret/rust-toolchain-file@v1\n\n    # Build Rust extensions\n\n    - name: Handle Rust extensions build cache\n      uses: actions/cache@v4\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/rust/extensions\n        key: edb-rust-build-v1-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-build-v1-\n\n    - name: Build Rust extensions\n      env:\n        CARGO_HOME: ${{ env.BUILD_TEMP }}/rust/extensions/cargo_home\n        CACHE_HIT: ${{ steps.rust-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p build/rust_extensions\n          rsync -av ./build/rust_extensions/ ${BUILD_LIB}/\n          python setup.py -v build_rust\n          rsync -av ${BUILD_LIB}/ build/rust_extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n\n    # Build libpg_query\n\n    - name: Build libpg_query\n      if: |\n        steps.libpg-query-cache.outputs.cache-hit != 'true' &&\n        steps.ext-cache.outputs.cache-hit != 'true'\n      run: |\n        python setup.py build_libpg_query\n\n    # Build extensions\n\n    - name: Handle Cython extensions build cache\n      uses: actions/cache@v4\n      if: steps.ext-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/edb\n        key: edb-ext-build-v4-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Build Cython extensions\n      env:\n        CACHE_HIT: ${{ steps.ext-cache.outputs.cache-hit }}\n        BUILD_EXT_MODE: py-only\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/extensions\n          rsync -av ./build/extensions/ ${BUILD_LIB}/\n          BUILD_EXT_MODE=py-only python setup.py -v build_ext\n          rsync -av ${BUILD_LIB}/ ./build/extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/extensions/edb/ ./edb/\n\n    # Build parsers\n\n    - name: Handle compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n        restore-keys: |\n          edb-parsers-v3-\n\n    - name: Build parsers\n      env:\n        CACHE_HIT: ${{ steps.parsers-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/lib\n          rsync -av ./build/lib/ ${BUILD_LIB}/\n          python setup.py -v build_parsers\n          rsync -av ${BUILD_LIB}/ ./build/lib/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/lib/edb/ ./edb/\n\n    # Build PostgreSQL\n\n    - name: Build PostgreSQL\n      env:\n        CACHE_HIT: ${{ steps.postgres-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" == \"true\" ]]; then\n          cp build/postgres/install/stamp build/postgres/\n        else\n          python setup.py build_postgres\n          cp build/postgres/stamp build/postgres/install/\n        fi\n\n    # Build Stolon\n\n    - name: Set up Go\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      uses: actions/setup-go@v2\n      with:\n        go-version: 1.16\n\n    - uses: actions/checkout@v4\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      with:\n        repository: edgedb/stolon\n        path: build/stolon\n        ref: ${{ env.STOLON_GIT_REV }}\n        fetch-depth: 0\n        submodules: false\n\n    - name: Build Stolon\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      run: |\n        mkdir -p build/stolon/bin/\n        curl -fsSL https://releases.hashicorp.com/consul/1.10.1/consul_1.10.1_linux_amd64.zip | zcat > build/stolon/bin/consul\n        chmod +x build/stolon/bin/consul\n        cd build/stolon && make\n\n    # Install edgedb-server and populate egg-info\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Refresh the bootstrap cache\n\n    - name: Handle bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n        restore-keys: |\n          edb-bootstrap-v2-\n\n    - name: Bootstrap EdgeDB Server\n      if: steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        edb server --bootstrap-only\n\n  ha-test:\n    needs: build\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Run the test\n\n    - name: Test\n      env:\n        SHARD: ${{ matrix.shard }}\n        EDGEDB_TEST_HA: 1\n        EDGEDB_TEST_CONSUL_PATH: build/stolon/bin/consul\n        EDGEDB_TEST_STOLON_CTL: build/stolon/bin/stolonctl\n        EDGEDB_TEST_STOLON_SENTINEL: build/stolon/bin/stolon-sentinel\n        EDGEDB_TEST_STOLON_KEEPER: build/stolon/bin/stolon-keeper\n      run: |\n        edb test -j1 -v -k test_ha_\n\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n    needs:\n      - build\n      - ha-test\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows/tests.inplace.yml",
    "content": "name: Tests of in-place upgrades and patching\n\non:\n  schedule:\n    - cron: \"0 3 * * *\"\n  workflow_dispatch:\n    inputs: {}\n  push:\n    branches:\n      - \"A-inplace*\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n\n    - name: Cached requirements.txt\n      uses: actions/cache@v4\n      id: requirements-cache\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Compute requirements.txt\n      if: steps.requirements-cache.outputs.cache-hit != 'true'\n      run: |\n        python -m pip install pip-tools\n        pip-compile --no-strip-extras --all-build-deps \\\n          --extra test,language-server \\\n          --output-file requirements.txt pyproject.toml\n\n    - name: Install Python dependencies\n      run: |\n        python -c \"import sys; print(sys.prefix)\"\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    - name: Compute cache keys\n      run: |\n        mkdir -p shared-artifacts\n        if [ \"$(uname)\" = \"Darwin\" ]; then\n          find /usr/lib -type f -name 'lib*' -exec stat -f '%N %z' {} + | sort | shasum -a 256 | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        else\n          find /usr/lib -type f -name 'lib*' -printf '%P %s\\n' | sort | sha256sum | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        fi\n        python setup.py -q ci_helper --type rust >shared-artifacts/rust_cache_key.txt\n        python setup.py -q ci_helper --type ext >shared-artifacts/ext_cache_key.txt\n        python setup.py -q ci_helper --type parsers >shared-artifacts/parsers_cache_key.txt\n        python setup.py -q ci_helper --type postgres >shared-artifacts/postgres_git_rev.txt\n        python setup.py -q ci_helper --type libpg_query >shared-artifacts/libpg_query_git_rev.txt\n        echo 'f8cd94309eaccbfba5dea7835b88c78377608a37' >shared-artifacts/stolon_git_rev.txt\n        python setup.py -q ci_helper --type bootstrap >shared-artifacts/bootstrap_cache_key.txt\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo LIBPG_QUERY_GIT_REV=$(cat shared-artifacts/libpg_query_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    - name: Upload shared artifacts\n      uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n        retention-days: 1\n\n    # Restore binary cache\n\n    - name: Handle cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-v4-\n\n    - name: Handle cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Handle cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Handle cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Handle cached libpg_query build\n      uses: actions/cache@v4\n      id: libpg-query-cache\n      with:\n        path: edb/pgsql/parser/libpg_query/libpg_query.a\n        key: edb-libpg_query-v1-${{ env.LIBPG_QUERY_GIT_REV }}\n\n    # Install system dependencies for building\n\n    - name: Install system deps\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true'\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y uuid-dev libreadline-dev bison flex libprotobuf-c-dev\n\n    - name: Install Rust toolchain\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      uses: dsherret/rust-toolchain-file@v1\n\n    # Build Rust extensions\n\n    - name: Handle Rust extensions build cache\n      uses: actions/cache@v4\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/rust/extensions\n        key: edb-rust-build-v1-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-build-v1-\n\n    - name: Build Rust extensions\n      env:\n        CARGO_HOME: ${{ env.BUILD_TEMP }}/rust/extensions/cargo_home\n        CACHE_HIT: ${{ steps.rust-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p build/rust_extensions\n          rsync -av ./build/rust_extensions/ ${BUILD_LIB}/\n          python setup.py -v build_rust\n          rsync -av ${BUILD_LIB}/ build/rust_extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n\n    # Build libpg_query\n\n    - name: Build libpg_query\n      if: |\n        steps.libpg-query-cache.outputs.cache-hit != 'true' &&\n        steps.ext-cache.outputs.cache-hit != 'true'\n      run: |\n        python setup.py build_libpg_query\n\n    # Build extensions\n\n    - name: Handle Cython extensions build cache\n      uses: actions/cache@v4\n      if: steps.ext-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/edb\n        key: edb-ext-build-v4-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Build Cython extensions\n      env:\n        CACHE_HIT: ${{ steps.ext-cache.outputs.cache-hit }}\n        BUILD_EXT_MODE: py-only\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/extensions\n          rsync -av ./build/extensions/ ${BUILD_LIB}/\n          BUILD_EXT_MODE=py-only python setup.py -v build_ext\n          rsync -av ${BUILD_LIB}/ ./build/extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/extensions/edb/ ./edb/\n\n    # Build parsers\n\n    - name: Handle compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n        restore-keys: |\n          edb-parsers-v3-\n\n    - name: Build parsers\n      env:\n        CACHE_HIT: ${{ steps.parsers-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/lib\n          rsync -av ./build/lib/ ${BUILD_LIB}/\n          python setup.py -v build_parsers\n          rsync -av ${BUILD_LIB}/ ./build/lib/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/lib/edb/ ./edb/\n\n    # Build PostgreSQL\n\n    - name: Build PostgreSQL\n      env:\n        CACHE_HIT: ${{ steps.postgres-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" == \"true\" ]]; then\n          cp build/postgres/install/stamp build/postgres/\n        else\n          python setup.py build_postgres\n          cp build/postgres/stamp build/postgres/install/\n        fi\n\n    # Build Stolon\n\n    - name: Set up Go\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      uses: actions/setup-go@v2\n      with:\n        go-version: 1.16\n\n    - uses: actions/checkout@v4\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      with:\n        repository: edgedb/stolon\n        path: build/stolon\n        ref: ${{ env.STOLON_GIT_REV }}\n        fetch-depth: 0\n        submodules: false\n\n    - name: Build Stolon\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      run: |\n        mkdir -p build/stolon/bin/\n        curl -fsSL https://releases.hashicorp.com/consul/1.10.1/consul_1.10.1_linux_amd64.zip | zcat > build/stolon/bin/consul\n        chmod +x build/stolon/bin/consul\n        cd build/stolon && make\n\n    # Install edgedb-server and populate egg-info\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Refresh the bootstrap cache\n\n    - name: Handle bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n        restore-keys: |\n          edb-bootstrap-v2-\n\n    - name: Bootstrap EdgeDB Server\n      if: steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        edb server --bootstrap-only\n\n  test-inplace:\n    runs-on: ubuntu-latest\n    needs: build\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - flags:\n            tests:\n          - flags: --rollback-and-test\n            tests:\n          # Do the reapply test on a smaller selection of tests, since\n          # it is slower.\n          - flags: --rollback-and-reapply\n            tests: -k test_link_on_target_delete -k test_edgeql_select -k test_dump\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Run the test\n    # TODO: Would it be better to split this up into multiple jobs?\n    - name: Test performing in-place upgrades\n      run: |\n        ./tests/inplace-testing/test.sh ${{ matrix.flags }} vt ${{ matrix.tests }}\n\n  test-patches:\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    - name: Test performing in-place upgrades\n      run: |\n        ./tests/patch-testing/test.sh test-dir -k test_link_on_target_delete -k test_edgeql_select -k test_edgeql_scope -k test_dump\n\n  compute-versions:\n    runs-on: ubuntu-latest\n    outputs:\n      matrix: ${{ steps.set-matrix.outputs.matrix }}\n    steps:\n    - uses: actions/checkout@v4\n    - id: set-matrix\n      name: Compute versions to run on\n      run: python3 .github/scripts/patches/compute-ipu-versions.py\n\n\n  test:\n    runs-on: ubuntu-latest\n    needs: [build, compute-versions]\n    strategy:\n      fail-fast: false\n      matrix: ${{fromJSON(needs.compute-versions.outputs.matrix)}}\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Run the test\n\n    - name: Download an earlier database version\n      run: |\n        wget -q \"${{ matrix.edgedb-url }}\"\n        tar xzf ${{ matrix.edgedb-basename }}-${{ matrix.edgedb-version }}.tar.gz\n\n    - name: Make sure a CLI named \"edgedb\" exists (sigh)\n      run: |\n        ln -s gel $(dirname $(which gel))/edgedb\n\n    - name: Test inplace upgrades from previous major version\n      run: |\n        ./tests/inplace-testing/test-old.sh vt ${{ matrix.edgedb-basename }}-${{ matrix.edgedb-version }}\n\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n    needs:\n      - build\n      - test-inplace\n      - test-patches\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows/tests.inplace7x.yml",
    "content": "name: Tests of in-place upgrades to 7.x\n\non:\n  schedule:\n    - cron: \"0 3 * * *\"\n  workflow_dispatch:\n    inputs: {}\n  push:\n    branches:\n      - \"A-inplace*\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n        ref: release/7.x\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n        ref: release/7.x\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n\n    - name: Cached requirements.txt\n      uses: actions/cache@v4\n      id: requirements-cache\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Compute requirements.txt\n      if: steps.requirements-cache.outputs.cache-hit != 'true'\n      run: |\n        python -m pip install pip-tools\n        pip-compile --no-strip-extras --all-build-deps \\\n          --extra test,language-server \\\n          --output-file requirements.txt pyproject.toml\n\n    - name: Install Python dependencies\n      run: |\n        python -c \"import sys; print(sys.prefix)\"\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    - name: Compute cache keys\n      run: |\n        mkdir -p shared-artifacts\n        if [ \"$(uname)\" = \"Darwin\" ]; then\n          find /usr/lib -type f -name 'lib*' -exec stat -f '%N %z' {} + | sort | shasum -a 256 | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        else\n          find /usr/lib -type f -name 'lib*' -printf '%P %s\\n' | sort | sha256sum | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        fi\n        python setup.py -q ci_helper --type rust >shared-artifacts/rust_cache_key.txt\n        python setup.py -q ci_helper --type ext >shared-artifacts/ext_cache_key.txt\n        python setup.py -q ci_helper --type parsers >shared-artifacts/parsers_cache_key.txt\n        python setup.py -q ci_helper --type postgres >shared-artifacts/postgres_git_rev.txt\n        python setup.py -q ci_helper --type libpg_query >shared-artifacts/libpg_query_git_rev.txt\n        echo 'f8cd94309eaccbfba5dea7835b88c78377608a37' >shared-artifacts/stolon_git_rev.txt\n        python setup.py -q ci_helper --type bootstrap >shared-artifacts/bootstrap_cache_key.txt\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo LIBPG_QUERY_GIT_REV=$(cat shared-artifacts/libpg_query_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    - name: Upload shared artifacts\n      uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n        retention-days: 1\n\n    # Restore binary cache\n\n    - name: Handle cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-v4-\n\n    - name: Handle cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Handle cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Handle cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Handle cached libpg_query build\n      uses: actions/cache@v4\n      id: libpg-query-cache\n      with:\n        path: edb/pgsql/parser/libpg_query/libpg_query.a\n        key: edb-libpg_query-v1-${{ env.LIBPG_QUERY_GIT_REV }}\n\n    # Install system dependencies for building\n\n    - name: Install system deps\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true'\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y uuid-dev libreadline-dev bison flex libprotobuf-c-dev\n\n    - name: Install Rust toolchain\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      uses: dsherret/rust-toolchain-file@v1\n\n    # Build Rust extensions\n\n    - name: Handle Rust extensions build cache\n      uses: actions/cache@v4\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/rust/extensions\n        key: edb-rust-build-v1-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-build-v1-\n\n    - name: Build Rust extensions\n      env:\n        CARGO_HOME: ${{ env.BUILD_TEMP }}/rust/extensions/cargo_home\n        CACHE_HIT: ${{ steps.rust-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p build/rust_extensions\n          rsync -av ./build/rust_extensions/ ${BUILD_LIB}/\n          python setup.py -v build_rust\n          rsync -av ${BUILD_LIB}/ build/rust_extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n\n    # Build libpg_query\n\n    - name: Build libpg_query\n      if: |\n        steps.libpg-query-cache.outputs.cache-hit != 'true' &&\n        steps.ext-cache.outputs.cache-hit != 'true'\n      run: |\n        python setup.py build_libpg_query\n\n    # Build extensions\n\n    - name: Handle Cython extensions build cache\n      uses: actions/cache@v4\n      if: steps.ext-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/edb\n        key: edb-ext-build-v4-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Build Cython extensions\n      env:\n        CACHE_HIT: ${{ steps.ext-cache.outputs.cache-hit }}\n        BUILD_EXT_MODE: py-only\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/extensions\n          rsync -av ./build/extensions/ ${BUILD_LIB}/\n          BUILD_EXT_MODE=py-only python setup.py -v build_ext\n          rsync -av ${BUILD_LIB}/ ./build/extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/extensions/edb/ ./edb/\n\n    # Build parsers\n\n    - name: Handle compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n        restore-keys: |\n          edb-parsers-v3-\n\n    - name: Build parsers\n      env:\n        CACHE_HIT: ${{ steps.parsers-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/lib\n          rsync -av ./build/lib/ ${BUILD_LIB}/\n          python setup.py -v build_parsers\n          rsync -av ${BUILD_LIB}/ ./build/lib/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/lib/edb/ ./edb/\n\n    # Build PostgreSQL\n\n    - name: Build PostgreSQL\n      env:\n        CACHE_HIT: ${{ steps.postgres-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" == \"true\" ]]; then\n          cp build/postgres/install/stamp build/postgres/\n        else\n          python setup.py build_postgres\n          cp build/postgres/stamp build/postgres/install/\n        fi\n\n    # Build Stolon\n\n    - name: Set up Go\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      uses: actions/setup-go@v2\n      with:\n        go-version: 1.16\n\n    - uses: actions/checkout@v4\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      with:\n        repository: edgedb/stolon\n        path: build/stolon\n        ref: ${{ env.STOLON_GIT_REV }}\n        fetch-depth: 0\n        submodules: false\n\n    - name: Build Stolon\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      run: |\n        mkdir -p build/stolon/bin/\n        curl -fsSL https://releases.hashicorp.com/consul/1.10.1/consul_1.10.1_linux_amd64.zip | zcat > build/stolon/bin/consul\n        chmod +x build/stolon/bin/consul\n        cd build/stolon && make\n\n    # Install edgedb-server and populate egg-info\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Refresh the bootstrap cache\n\n    - name: Handle bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n        restore-keys: |\n          edb-bootstrap-v2-\n\n    - name: Bootstrap EdgeDB Server\n      if: steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        edb server --bootstrap-only\n\n  test-inplace:\n    runs-on: ubuntu-latest\n    needs: build\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - flags:\n            tests:\n          - flags: --rollback-and-test\n            tests:\n          # Do the reapply test on a smaller selection of tests, since\n          # it is slower.\n          - flags: --rollback-and-reapply\n            tests: -k test_link_on_target_delete -k test_edgeql_select -k test_dump\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n        ref: release/7.x\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n        ref: release/7.x\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Run the test\n    # TODO: Would it be better to split this up into multiple jobs?\n    - name: Test performing in-place upgrades\n      run: |\n        ./tests/inplace-testing/test.sh ${{ matrix.flags }} vt ${{ matrix.tests }}\n\n  compute-versions:\n    runs-on: ubuntu-latest\n    outputs:\n      matrix: ${{ steps.set-matrix.outputs.matrix }}\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n        ref: release/7.x\n    - id: set-matrix\n      name: Compute versions to run on\n      run: python3 .github/scripts/patches/compute-ipu-versions.py\n\n\n  test:\n    runs-on: ubuntu-latest\n    needs: [build, compute-versions]\n    strategy:\n      fail-fast: false\n      matrix: ${{fromJSON(needs.compute-versions.outputs.matrix)}}\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n        ref: release/7.x\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n        ref: release/7.x\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Run the test\n\n    - name: Download an earlier database version\n      run: |\n        wget -q \"${{ matrix.edgedb-url }}\"\n        tar xzf ${{ matrix.edgedb-basename }}-${{ matrix.edgedb-version }}.tar.gz\n\n    - name: Make sure a CLI named \"edgedb\" exists (sigh)\n      run: |\n        ln -s gel $(dirname $(which gel))/edgedb\n\n    - name: Test inplace upgrades from previous major version\n      run: |\n        ./tests/inplace-testing/test-old.sh vt ${{ matrix.edgedb-basename }}-${{ matrix.edgedb-version }}\n\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n    needs:\n      - build\n      - test-inplace\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows/tests.managed-pg.yml",
    "content": "name: Tests on Managed PostgreSQL\n\non:\n  schedule:\n    - cron: \"0 3 * * 6\"\n  workflow_dispatch:\n    inputs: {}\n  push:\n    branches:\n      - cloud-test\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n\n    - name: Cached requirements.txt\n      uses: actions/cache@v4\n      id: requirements-cache\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Compute requirements.txt\n      if: steps.requirements-cache.outputs.cache-hit != 'true'\n      run: |\n        python -m pip install pip-tools\n        pip-compile --no-strip-extras --all-build-deps \\\n          --extra test,language-server \\\n          --output-file requirements.txt pyproject.toml\n\n    - name: Install Python dependencies\n      run: |\n        python -c \"import sys; print(sys.prefix)\"\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    - name: Compute cache keys\n      run: |\n        mkdir -p shared-artifacts\n        if [ \"$(uname)\" = \"Darwin\" ]; then\n          find /usr/lib -type f -name 'lib*' -exec stat -f '%N %z' {} + | sort | shasum -a 256 | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        else\n          find /usr/lib -type f -name 'lib*' -printf '%P %s\\n' | sort | sha256sum | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        fi\n        python setup.py -q ci_helper --type rust >shared-artifacts/rust_cache_key.txt\n        python setup.py -q ci_helper --type ext >shared-artifacts/ext_cache_key.txt\n        python setup.py -q ci_helper --type parsers >shared-artifacts/parsers_cache_key.txt\n        python setup.py -q ci_helper --type postgres >shared-artifacts/postgres_git_rev.txt\n        python setup.py -q ci_helper --type libpg_query >shared-artifacts/libpg_query_git_rev.txt\n        echo 'f8cd94309eaccbfba5dea7835b88c78377608a37' >shared-artifacts/stolon_git_rev.txt\n        python setup.py -q ci_helper --type bootstrap >shared-artifacts/bootstrap_cache_key.txt\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo LIBPG_QUERY_GIT_REV=$(cat shared-artifacts/libpg_query_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    - name: Upload shared artifacts\n      uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n        retention-days: 1\n\n    # Restore binary cache\n\n    - name: Handle cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-v4-\n\n    - name: Handle cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Handle cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Handle cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Handle cached libpg_query build\n      uses: actions/cache@v4\n      id: libpg-query-cache\n      with:\n        path: edb/pgsql/parser/libpg_query/libpg_query.a\n        key: edb-libpg_query-v1-${{ env.LIBPG_QUERY_GIT_REV }}\n\n    # Install system dependencies for building\n\n    - name: Install system deps\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true'\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y uuid-dev libreadline-dev bison flex libprotobuf-c-dev\n\n    - name: Install Rust toolchain\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      uses: dsherret/rust-toolchain-file@v1\n\n    # Build Rust extensions\n\n    - name: Handle Rust extensions build cache\n      uses: actions/cache@v4\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/rust/extensions\n        key: edb-rust-build-v1-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-build-v1-\n\n    - name: Build Rust extensions\n      env:\n        CARGO_HOME: ${{ env.BUILD_TEMP }}/rust/extensions/cargo_home\n        CACHE_HIT: ${{ steps.rust-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p build/rust_extensions\n          rsync -av ./build/rust_extensions/ ${BUILD_LIB}/\n          python setup.py -v build_rust\n          rsync -av ${BUILD_LIB}/ build/rust_extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n\n    # Build libpg_query\n\n    - name: Build libpg_query\n      if: |\n        steps.libpg-query-cache.outputs.cache-hit != 'true' &&\n        steps.ext-cache.outputs.cache-hit != 'true'\n      run: |\n        python setup.py build_libpg_query\n\n    # Build extensions\n\n    - name: Handle Cython extensions build cache\n      uses: actions/cache@v4\n      if: steps.ext-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/edb\n        key: edb-ext-build-v4-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Build Cython extensions\n      env:\n        CACHE_HIT: ${{ steps.ext-cache.outputs.cache-hit }}\n        BUILD_EXT_MODE: py-only\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/extensions\n          rsync -av ./build/extensions/ ${BUILD_LIB}/\n          BUILD_EXT_MODE=py-only python setup.py -v build_ext\n          rsync -av ${BUILD_LIB}/ ./build/extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/extensions/edb/ ./edb/\n\n    # Build parsers\n\n    - name: Handle compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n        restore-keys: |\n          edb-parsers-v3-\n\n    - name: Build parsers\n      env:\n        CACHE_HIT: ${{ steps.parsers-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/lib\n          rsync -av ./build/lib/ ${BUILD_LIB}/\n          python setup.py -v build_parsers\n          rsync -av ${BUILD_LIB}/ ./build/lib/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/lib/edb/ ./edb/\n\n    # Build PostgreSQL\n\n    - name: Build PostgreSQL\n      env:\n        CACHE_HIT: ${{ steps.postgres-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" == \"true\" ]]; then\n          cp build/postgres/install/stamp build/postgres/\n        else\n          python setup.py build_postgres\n          cp build/postgres/stamp build/postgres/install/\n        fi\n\n    # Build Stolon\n\n    - name: Set up Go\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      uses: actions/setup-go@v2\n      with:\n        go-version: 1.16\n\n    - uses: actions/checkout@v4\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      with:\n        repository: edgedb/stolon\n        path: build/stolon\n        ref: ${{ env.STOLON_GIT_REV }}\n        fetch-depth: 0\n        submodules: false\n\n    - name: Build Stolon\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      run: |\n        mkdir -p build/stolon/bin/\n        curl -fsSL https://releases.hashicorp.com/consul/1.10.1/consul_1.10.1_linux_amd64.zip | zcat > build/stolon/bin/consul\n        chmod +x build/stolon/bin/consul\n        cd build/stolon && make\n\n    # Install edgedb-server and populate egg-info\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Refresh the bootstrap cache\n\n    - name: Handle bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n        restore-keys: |\n          edb-bootstrap-v2-\n\n    - name: Bootstrap EdgeDB Server\n      if: steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        edb server --bootstrap-only\n\n\n  setup-aws-rds:\n    runs-on: ubuntu-latest\n    outputs:\n      pghost: ${{ steps.pghost.outputs.stdout }}\n    defaults:\n      run:\n        working-directory: .github/aws-rds\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          submodules: false\n\n      - name: Setup Terraform\n        uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1  # v2.0.3\n\n      - name: Initialize Terraform\n        run: terraform init\n\n      - name: Configure AWS Credentials\n        uses: aws-actions/configure-aws-credentials@v1\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: us-east-2\n\n      - name: Setup AWS RDS\n        env:\n          TF_VAR_sg_id: ${{ secrets.AWS_SECURITY_GROUP }}\n          TF_VAR_password: ${{ secrets.AWS_RDS_PASSWORD }}\n        run: |\n          terraform apply -auto-approve\n\n      - name: Store Terraform state\n        if: ${{ always() }}\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: aws-rds-tfstate\n          path: .github/aws-rds/terraform.tfstate\n          retention-days: 1\n\n      - name: Get RDS host\n        id: pghost\n        run: |\n          terraform output -raw db_instance_address\n\n  test-aws-rds:\n    runs-on: ubuntu-latest\n    needs: [setup-aws-rds, build]\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Run the test\n\n    - name: Test\n      env:\n        EDGEDB_TEST_BACKEND_DSN: postgres://edbtest:${{ secrets.AWS_RDS_PASSWORD }}@${{ needs.setup-aws-rds.outputs.pghost }}/postgres\n      run: |\n        edb server --bootstrap-only --backend-dsn=$EDGEDB_TEST_BACKEND_DSN --testmode\n        edb test -j2 -v --backend-dsn=$EDGEDB_TEST_BACKEND_DSN\n\n  teardown-aws-rds:\n    runs-on: ubuntu-latest\n    needs: test-aws-rds\n    if: ${{ always() }}\n    defaults:\n      run:\n        working-directory: .github/aws-rds\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          submodules: false\n\n      - name: Setup Terraform\n        uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1  # v2.0.3\n\n      - name: Initialize Terraform\n        run: terraform init\n\n      - name: Configure AWS Credentials\n        uses: aws-actions/configure-aws-credentials@v1\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: us-east-2\n\n      - name: Restore Terraform state\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n        with:\n          name: aws-rds-tfstate\n          path: .github/aws-rds\n\n      - name: Destroy AWS RDS\n        run: terraform destroy -auto-approve\n        env:\n          TF_VAR_sg_id: ${{ secrets.AWS_SECURITY_GROUP }}\n          TF_VAR_password: ${{ secrets.AWS_RDS_PASSWORD }}\n\n      - name: Overwrite Terraform state\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: aws-rds-tfstate\n          path: .github/aws-rds/terraform.tfstate\n          retention-days: 1\n\n\n  setup-do-database:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: .github/do-database\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          submodules: false\n\n      - name: Setup Terraform\n        uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1  # v2.0.3\n\n      - name: Initialize Terraform\n        run: terraform init\n\n      - name: Setup DigitalOcean Database\n        env:\n          TF_VAR_do_token: ${{ secrets.DIGITALOCEAN_TOKEN }}\n        run: |\n          terraform apply -auto-approve\n\n      - name: Store Terraform state\n        if: ${{ always() }}\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: do-database-tfstate\n          path: .github/do-database/terraform.tfstate\n          retention-days: 1\n\n  test-do-database:\n    runs-on: ubuntu-latest\n    needs: [setup-do-database, build]\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    - name: Setup Terraform\n      uses: hashicorp/setup-terraform@v1\n\n    - name: Initialize Terraform\n      working-directory: .github/do-database\n      run: terraform init\n\n    - name: Restore Terraform state\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: do-database-tfstate\n        path: .github/do-database\n\n    - name: Get Database host\n      id: pghost\n      working-directory: .github/do-database\n      run: |\n        terraform output -raw db_instance_address\n\n    - name: Get Database port\n      id: pgport\n      working-directory: .github/do-database\n      run: |\n        terraform output -raw db_instance_port\n\n    - name: Get Database user\n      id: pguser\n      working-directory: .github/do-database\n      run: |\n        terraform output -raw db_instance_user\n\n    - name: Get Database password\n      id: pgpass\n      working-directory: .github/do-database\n      run: |\n        terraform output -raw db_instance_password\n\n    - name: Get Database dbname\n      id: pgdatabase\n      working-directory: .github/do-database\n      run: |\n        terraform output -raw db_instance_database\n\n    # Run the test\n\n    - name: Test\n      env:\n        EDGEDB_TEST_BACKEND_DSN: postgres://${{ steps.pguser.outputs.stdout }}:${{ steps.pgpass.outputs.stdout }}@${{ steps.pghost.outputs.stdout }}:${{ steps.pgport.outputs.stdout }}/${{ steps.pgdatabase.outputs.stdout }}\n      run: |\n        edb server --bootstrap-only --backend-dsn=$EDGEDB_TEST_BACKEND_DSN --testmode\n        edb test -j2 -v --backend-dsn=$EDGEDB_TEST_BACKEND_DSN\n\n  teardown-do-database:\n    runs-on: ubuntu-latest\n    needs: test-do-database\n    if: ${{ always() }}\n    defaults:\n      run:\n        working-directory: .github/do-database\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          submodules: false\n\n      - name: Setup Terraform\n        uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1  # v2.0.3\n\n      - name: Initialize Terraform\n        run: terraform init\n\n      - name: Restore Terraform state\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n        with:\n          name: do-database-tfstate\n          path: .github/do-database\n\n      - name: Destroy DigitalOcean Database\n        run: terraform destroy -auto-approve\n        env:\n          TF_VAR_do_token: ${{ secrets.DIGITALOCEAN_TOKEN }}\n\n      - name: Overwrite Terraform state\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: do-database-tfstate\n          path: .github/do-database/terraform.tfstate\n          retention-days: 1\n\n\n  setup-gcp-cloud-sql:\n    runs-on: ubuntu-latest\n    outputs:\n      pghost: ${{ steps.pghost.outputs.stdout }}\n    defaults:\n      run:\n        working-directory: .github/gcp-cloud-sql\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          submodules: false\n\n      - name: Setup Terraform\n        uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1  # v2.0.3\n\n      - name: Initialize Terraform\n        run: terraform init\n\n      - name: Configure GCP Credentials\n        uses: google-github-actions/setup-gcloud@main\n        with:\n          service_account_key: ${{ secrets.GCP_SA_KEY }}\n          export_default_credentials: true\n\n      - name: Setup GCP Cloud SQL\n        env:\n          TF_VAR_password: ${{ secrets.AWS_RDS_PASSWORD }}\n        run: |\n          terraform apply -auto-approve\n\n      - name: Store Terraform state\n        if: ${{ always() }}\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: gcp-cloud-sql-tfstate\n          path: .github/gcp-cloud-sql/terraform.tfstate\n          retention-days: 1\n\n      - name: Get Cloud SQL host\n        id: pghost\n        run: |\n          terraform output -raw db_instance_address\n\n  test-gcp-cloud-sql:\n    runs-on: ubuntu-latest\n    needs: [setup-gcp-cloud-sql, build]\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Run the test\n\n    - name: Test\n      env:\n        EDGEDB_TEST_BACKEND_DSN: postgres://postgres:${{ secrets.AWS_RDS_PASSWORD }}@${{ needs.setup-gcp-cloud-sql.outputs.pghost }}/postgres\n      run: |\n        edb server --bootstrap-only --backend-dsn=$EDGEDB_TEST_BACKEND_DSN --testmode\n        edb test -j2 -v --backend-dsn=$EDGEDB_TEST_BACKEND_DSN\n\n  teardown-gcp-cloud-sql:\n    runs-on: ubuntu-latest\n    needs: test-gcp-cloud-sql\n    if: ${{ always() }}\n    defaults:\n      run:\n        working-directory: .github/gcp-cloud-sql\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          submodules: false\n\n      - name: Setup Terraform\n        uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1  # v2.0.3\n\n      - name: Initialize Terraform\n        run: terraform init\n\n      - name: Configure GCP Credentials\n        uses: google-github-actions/setup-gcloud@main\n        with:\n          service_account_key: ${{ secrets.GCP_SA_KEY }}\n          export_default_credentials: true\n\n      - name: Restore Terraform state\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n        with:\n          name: gcp-cloud-sql-tfstate\n          path: .github/gcp-cloud-sql\n\n      - name: Destroy GCP Cloud SQL\n        run: terraform destroy -auto-approve\n        env:\n          TF_VAR_password: ${{ secrets.AWS_RDS_PASSWORD }}\n\n      - name: Overwrite Terraform state\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: gcp-cloud-sql-tfstate\n          path: .github/gcp-cloud-sql/terraform.tfstate\n          retention-days: 1\n\n\n  setup-aws-aurora:\n    runs-on: ubuntu-latest\n    outputs:\n      pghost: ${{ steps.pghost.outputs.stdout }}\n    defaults:\n      run:\n        working-directory: .github/aws-aurora\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          submodules: false\n\n      - name: Setup Terraform\n        uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1  # v2.0.3\n\n      - name: Initialize Terraform\n        run: terraform init\n\n      - name: Configure AWS Credentials\n        uses: aws-actions/configure-aws-credentials@v1\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: us-east-2\n\n      - name: Setup AWS RDS Aurora\n        env:\n          TF_VAR_sg_id: ${{ secrets.AWS_SECURITY_GROUP }}\n          TF_VAR_password: ${{ secrets.AWS_RDS_PASSWORD }}\n          TF_VAR_vpc_id: ${{ secrets.AWS_VPC_ID }}\n        run: |\n          terraform apply -auto-approve\n\n      - name: Store Terraform state\n        if: ${{ always() }}\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: aws-aurora-tfstate\n          path: .github/aws-aurora/terraform.tfstate\n          retention-days: 1\n\n      - name: Get RDS Aurora host\n        id: pghost\n        run: |\n          terraform output -raw rds_cluster_endpoint\n\n  test-aws-aurora:\n    runs-on: ubuntu-latest\n    needs: [setup-aws-aurora, build]\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Run the test\n\n    - name: Test\n      env:\n        EDGEDB_TEST_BACKEND_DSN: postgres://edbtest:${{ secrets.AWS_RDS_PASSWORD }}@${{ needs.setup-aws-aurora.outputs.pghost }}/postgres\n      run: |\n        edb server --bootstrap-only --backend-dsn=$EDGEDB_TEST_BACKEND_DSN --testmode\n        edb test -j1 -v --backend-dsn=$EDGEDB_TEST_BACKEND_DSN\n\n  teardown-aws-aurora:\n    runs-on: ubuntu-latest\n    needs: test-aws-aurora\n    if: ${{ always() }}\n    defaults:\n      run:\n        working-directory: .github/aws-aurora\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          submodules: false\n\n      - name: Setup Terraform\n        uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1  # v2.0.3\n\n      - name: Initialize Terraform\n        run: terraform init\n\n      - name: Configure AWS Credentials\n        uses: aws-actions/configure-aws-credentials@v1\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: us-east-2\n\n      - name: Restore Terraform state\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n        with:\n          name: aws-aurora-tfstate\n          path: .github/aws-aurora\n\n      - name: Destroy AWS RDS Aurora\n        run: terraform destroy -auto-approve\n        env:\n          TF_VAR_sg_id: ${{ secrets.AWS_SECURITY_GROUP }}\n          TF_VAR_password: ${{ secrets.AWS_RDS_PASSWORD }}\n          TF_VAR_vpc_id: ${{ secrets.AWS_VPC_ID }}\n\n      - name: Overwrite Terraform state\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: aws-aurora-tfstate\n          path: .github/aws-aurora/terraform.tfstate\n          retention-days: 1\n\n\n  setup-heroku-postgres:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: .github/heroku-postgres\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          submodules: false\n\n      - name: Setup Terraform\n        uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1  # v2.0.3\n\n      - name: Initialize Terraform\n        run: terraform init\n\n      - name: Setup Heroku Postgres\n        env:\n          HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}\n          HEROKU_EMAIL: ${{ secrets.HEROKU_EMAIL }}\n        run: |\n          terraform apply -auto-approve\n\n      - name: Store Terraform state\n        if: ${{ always() }}\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: heroku-postgres-tfstate\n          path: .github/heroku-postgres/terraform.tfstate\n          retention-days: 1\n\n  test-heroku-postgres:\n    runs-on: ubuntu-latest\n    needs: [setup-heroku-postgres, build]\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    - name: Setup Terraform\n      uses: hashicorp/setup-terraform@v1\n\n    - name: Initialize Terraform\n      working-directory: .github/heroku-postgres\n      run: terraform init\n\n    - name: Restore Terraform state\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: heroku-postgres-tfstate\n        path: .github/heroku-postgres\n\n    - name: Get Heroku Postgres DSN\n      id: pgdsn\n      working-directory: .github/heroku-postgres\n      run: |\n        terraform output -raw heroku_postgres_dsn\n\n    # Run the test\n\n    - name: Test\n      env:\n        EDGEDB_TEST_BACKEND_VENDOR: heroku-postgres\n        EDGEDB_TEST_BACKEND_DSN: ${{ steps.pgdsn.outputs.stdout }}\n      run: |\n        edb server --bootstrap-only --backend-dsn=$EDGEDB_TEST_BACKEND_DSN --testmode\n        edb test -j1 -v --backend-dsn=$EDGEDB_TEST_BACKEND_DSN\n\n  teardown-heroku-postgres:\n    runs-on: ubuntu-latest\n    needs: test-heroku-postgres\n    if: ${{ always() }}\n    defaults:\n      run:\n        working-directory: .github/heroku-postgres\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          submodules: false\n\n      - name: Setup Terraform\n        uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1  # v2.0.3\n\n      - name: Initialize Terraform\n        run: terraform init\n\n      - name: Configure AWS Credentials\n        uses: aws-actions/configure-aws-credentials@v1\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: us-east-2\n\n      - name: Restore Terraform state\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n        with:\n          name: heroku-postgres-tfstate\n          path: .github/heroku-postgres\n\n      - name: Destroy Heroku Postgres\n        run: terraform destroy -auto-approve\n        env:\n          HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}\n          HEROKU_EMAIL: ${{ secrets.HEROKU_EMAIL }}\n\n      - name: Overwrite Terraform state\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: heroku-postgres-tfstate\n          path: .github/heroku-postgres/terraform.tfstate\n          retention-days: 1\n\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n    needs:\n      - setup-aws-rds\n      - test-aws-rds\n      - teardown-aws-rds\n      - setup-do-database\n      - test-do-database\n      - teardown-do-database\n      - setup-gcp-cloud-sql\n      - test-gcp-cloud-sql\n      - teardown-gcp-cloud-sql\n      - setup-aws-aurora\n      - test-aws-aurora\n      - teardown-aws-aurora\n      - setup-heroku-postgres\n      - test-heroku-postgres\n      - teardown-heroku-postgres\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows/tests.patches.yml",
    "content": "name: Tests of patching old EdgeDB Versions\n\non:\n  workflow_dispatch:\n    inputs: {}\n  pull_request:\n    branches:\n      - release/*\n  push:\n    branches:\n      - patch-test*\n      - release/*\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n\n    - name: Cached requirements.txt\n      uses: actions/cache@v4\n      id: requirements-cache\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Compute requirements.txt\n      if: steps.requirements-cache.outputs.cache-hit != 'true'\n      run: |\n        python -m pip install pip-tools\n        pip-compile --no-strip-extras --all-build-deps \\\n          --extra test,language-server \\\n          --output-file requirements.txt pyproject.toml\n\n    - name: Install Python dependencies\n      run: |\n        python -c \"import sys; print(sys.prefix)\"\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    - name: Compute cache keys\n      run: |\n        mkdir -p shared-artifacts\n        if [ \"$(uname)\" = \"Darwin\" ]; then\n          find /usr/lib -type f -name 'lib*' -exec stat -f '%N %z' {} + | sort | shasum -a 256 | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        else\n          find /usr/lib -type f -name 'lib*' -printf '%P %s\\n' | sort | sha256sum | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        fi\n        python setup.py -q ci_helper --type rust >shared-artifacts/rust_cache_key.txt\n        python setup.py -q ci_helper --type ext >shared-artifacts/ext_cache_key.txt\n        python setup.py -q ci_helper --type parsers >shared-artifacts/parsers_cache_key.txt\n        python setup.py -q ci_helper --type postgres >shared-artifacts/postgres_git_rev.txt\n        python setup.py -q ci_helper --type libpg_query >shared-artifacts/libpg_query_git_rev.txt\n        echo 'f8cd94309eaccbfba5dea7835b88c78377608a37' >shared-artifacts/stolon_git_rev.txt\n        python setup.py -q ci_helper --type bootstrap >shared-artifacts/bootstrap_cache_key.txt\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo LIBPG_QUERY_GIT_REV=$(cat shared-artifacts/libpg_query_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    - name: Upload shared artifacts\n      uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n        retention-days: 1\n\n    # Restore binary cache\n\n    - name: Handle cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-v4-\n\n    - name: Handle cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Handle cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Handle cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Handle cached libpg_query build\n      uses: actions/cache@v4\n      id: libpg-query-cache\n      with:\n        path: edb/pgsql/parser/libpg_query/libpg_query.a\n        key: edb-libpg_query-v1-${{ env.LIBPG_QUERY_GIT_REV }}\n\n    # Install system dependencies for building\n\n    - name: Install system deps\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true'\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y uuid-dev libreadline-dev bison flex libprotobuf-c-dev\n\n    - name: Install Rust toolchain\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      uses: dsherret/rust-toolchain-file@v1\n\n    # Build Rust extensions\n\n    - name: Handle Rust extensions build cache\n      uses: actions/cache@v4\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/rust/extensions\n        key: edb-rust-build-v1-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-build-v1-\n\n    - name: Build Rust extensions\n      env:\n        CARGO_HOME: ${{ env.BUILD_TEMP }}/rust/extensions/cargo_home\n        CACHE_HIT: ${{ steps.rust-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p build/rust_extensions\n          rsync -av ./build/rust_extensions/ ${BUILD_LIB}/\n          python setup.py -v build_rust\n          rsync -av ${BUILD_LIB}/ build/rust_extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n\n    # Build libpg_query\n\n    - name: Build libpg_query\n      if: |\n        steps.libpg-query-cache.outputs.cache-hit != 'true' &&\n        steps.ext-cache.outputs.cache-hit != 'true'\n      run: |\n        python setup.py build_libpg_query\n\n    # Build extensions\n\n    - name: Handle Cython extensions build cache\n      uses: actions/cache@v4\n      if: steps.ext-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/edb\n        key: edb-ext-build-v4-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Build Cython extensions\n      env:\n        CACHE_HIT: ${{ steps.ext-cache.outputs.cache-hit }}\n        BUILD_EXT_MODE: py-only\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/extensions\n          rsync -av ./build/extensions/ ${BUILD_LIB}/\n          BUILD_EXT_MODE=py-only python setup.py -v build_ext\n          rsync -av ${BUILD_LIB}/ ./build/extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/extensions/edb/ ./edb/\n\n    # Build parsers\n\n    - name: Handle compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n        restore-keys: |\n          edb-parsers-v3-\n\n    - name: Build parsers\n      env:\n        CACHE_HIT: ${{ steps.parsers-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/lib\n          rsync -av ./build/lib/ ${BUILD_LIB}/\n          python setup.py -v build_parsers\n          rsync -av ${BUILD_LIB}/ ./build/lib/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/lib/edb/ ./edb/\n\n    # Build PostgreSQL\n\n    - name: Build PostgreSQL\n      env:\n        CACHE_HIT: ${{ steps.postgres-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" == \"true\" ]]; then\n          cp build/postgres/install/stamp build/postgres/\n        else\n          python setup.py build_postgres\n          cp build/postgres/stamp build/postgres/install/\n        fi\n\n    # Build Stolon\n\n    - name: Set up Go\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      uses: actions/setup-go@v2\n      with:\n        go-version: 1.16\n\n    - uses: actions/checkout@v4\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      with:\n        repository: edgedb/stolon\n        path: build/stolon\n        ref: ${{ env.STOLON_GIT_REV }}\n        fetch-depth: 0\n        submodules: false\n\n    - name: Build Stolon\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      run: |\n        mkdir -p build/stolon/bin/\n        curl -fsSL https://releases.hashicorp.com/consul/1.10.1/consul_1.10.1_linux_amd64.zip | zcat > build/stolon/bin/consul\n        chmod +x build/stolon/bin/consul\n        cd build/stolon && make\n\n    # Install edgedb-server and populate egg-info\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Refresh the bootstrap cache\n\n    - name: Handle bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n        restore-keys: |\n          edb-bootstrap-v2-\n\n    - name: Bootstrap EdgeDB Server\n      if: steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        edb server --bootstrap-only\n\n  compute-versions:\n    runs-on: ubuntu-latest\n    outputs:\n      matrix: ${{ steps.set-matrix.outputs.matrix }}\n    steps:\n    - uses: actions/checkout@v4\n    - id: set-matrix\n      name: Compute versions to run on\n      run: python3 .github/scripts/patches/compute-versions.py\n\n  test:\n    runs-on: ubuntu-latest\n    needs: [build, compute-versions]\n    strategy:\n      fail-fast: false\n      matrix: ${{fromJSON(needs.compute-versions.outputs.matrix)}}\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Run the test\n\n    - name: Download an earlier database version and set up a instance\n      run: |\n        wget -q \"${{ matrix.edgedb-url }}\"\n        tar xzf ${{ matrix.edgedb-basename }}-${{ matrix.edgedb-version }}.tar.gz\n        ${{ matrix.edgedb-basename }}-${{ matrix.edgedb-version }}/bin/edgedb-server -D test-dir --bootstrap-only --testmode\n\n    - name: Create databases on the older version\n      if: ${{ matrix.make-dbs }}\n      run: python3 .github/scripts/patches/create-databases.py ${{ matrix.edgedb-basename }}-${{ matrix.edgedb-version }}/bin/edgedb-server\n\n    - name: Run tests with instance created on an older version\n      run: |\n        # Run the server explicitly first to do the upgrade, since edb test\n        # has timeouts.\n        edb server --bootstrap-only --data-dir test-dir\n        # Should we run *all* the tests?\n        edb test -j2 -v --data-dir test-dir tests/test_edgeql_json.py tests/test_edgeql_casts.py tests/test_edgeql_functions.py tests/test_edgeql_expressions.py tests/test_edgeql_policies.py tests/test_edgeql_vector.py tests/test_edgeql_scope.py tests/test_http_ext_auth.py\n\n    - name: Test downgrading a database after an upgrade\n      if: ${{ !contains(matrix.edgedb-version, '-rc') && !contains(matrix.edgedb-version, '-beta') }}\n      env:\n        EDGEDB_VERSION: ${{ matrix.edgedb-version }}\n      run: python3 .github/scripts/patches/test-downgrade.py\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n    needs:\n      - build\n      - compute-versions\n      - test\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows/tests.pg-versions.yml",
    "content": "name: Tests on PostgreSQL Versions\n\non:\n  schedule:\n    - cron: \"0 3 * * *\"\n  workflow_dispatch:\n    inputs: {}\n  push:\n    branches:\n      - pg-test\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n\n    - name: Cached requirements.txt\n      uses: actions/cache@v4\n      id: requirements-cache\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Compute requirements.txt\n      if: steps.requirements-cache.outputs.cache-hit != 'true'\n      run: |\n        python -m pip install pip-tools\n        pip-compile --no-strip-extras --all-build-deps \\\n          --extra test,language-server \\\n          --output-file requirements.txt pyproject.toml\n\n    - name: Install Python dependencies\n      run: |\n        python -c \"import sys; print(sys.prefix)\"\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    - name: Compute cache keys\n      run: |\n        mkdir -p shared-artifacts\n        if [ \"$(uname)\" = \"Darwin\" ]; then\n          find /usr/lib -type f -name 'lib*' -exec stat -f '%N %z' {} + | sort | shasum -a 256 | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        else\n          find /usr/lib -type f -name 'lib*' -printf '%P %s\\n' | sort | sha256sum | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        fi\n        python setup.py -q ci_helper --type rust >shared-artifacts/rust_cache_key.txt\n        python setup.py -q ci_helper --type ext >shared-artifacts/ext_cache_key.txt\n        python setup.py -q ci_helper --type parsers >shared-artifacts/parsers_cache_key.txt\n        python setup.py -q ci_helper --type postgres >shared-artifacts/postgres_git_rev.txt\n        python setup.py -q ci_helper --type libpg_query >shared-artifacts/libpg_query_git_rev.txt\n        echo 'f8cd94309eaccbfba5dea7835b88c78377608a37' >shared-artifacts/stolon_git_rev.txt\n        python setup.py -q ci_helper --type bootstrap >shared-artifacts/bootstrap_cache_key.txt\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo LIBPG_QUERY_GIT_REV=$(cat shared-artifacts/libpg_query_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    - name: Upload shared artifacts\n      uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n        retention-days: 1\n\n    # Restore binary cache\n\n    - name: Handle cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-v4-\n\n    - name: Handle cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Handle cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Handle cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Handle cached libpg_query build\n      uses: actions/cache@v4\n      id: libpg-query-cache\n      with:\n        path: edb/pgsql/parser/libpg_query/libpg_query.a\n        key: edb-libpg_query-v1-${{ env.LIBPG_QUERY_GIT_REV }}\n\n    # Install system dependencies for building\n\n    - name: Install system deps\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true'\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y uuid-dev libreadline-dev bison flex libprotobuf-c-dev\n\n    - name: Install Rust toolchain\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      uses: dsherret/rust-toolchain-file@v1\n\n    # Build Rust extensions\n\n    - name: Handle Rust extensions build cache\n      uses: actions/cache@v4\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/rust/extensions\n        key: edb-rust-build-v1-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-build-v1-\n\n    - name: Build Rust extensions\n      env:\n        CARGO_HOME: ${{ env.BUILD_TEMP }}/rust/extensions/cargo_home\n        CACHE_HIT: ${{ steps.rust-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p build/rust_extensions\n          rsync -av ./build/rust_extensions/ ${BUILD_LIB}/\n          python setup.py -v build_rust\n          rsync -av ${BUILD_LIB}/ build/rust_extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n\n    # Build libpg_query\n\n    - name: Build libpg_query\n      if: |\n        steps.libpg-query-cache.outputs.cache-hit != 'true' &&\n        steps.ext-cache.outputs.cache-hit != 'true'\n      run: |\n        python setup.py build_libpg_query\n\n    # Build extensions\n\n    - name: Handle Cython extensions build cache\n      uses: actions/cache@v4\n      if: steps.ext-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/edb\n        key: edb-ext-build-v4-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Build Cython extensions\n      env:\n        CACHE_HIT: ${{ steps.ext-cache.outputs.cache-hit }}\n        BUILD_EXT_MODE: py-only\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/extensions\n          rsync -av ./build/extensions/ ${BUILD_LIB}/\n          BUILD_EXT_MODE=py-only python setup.py -v build_ext\n          rsync -av ${BUILD_LIB}/ ./build/extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/extensions/edb/ ./edb/\n\n    # Build parsers\n\n    - name: Handle compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n        restore-keys: |\n          edb-parsers-v3-\n\n    - name: Build parsers\n      env:\n        CACHE_HIT: ${{ steps.parsers-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/lib\n          rsync -av ./build/lib/ ${BUILD_LIB}/\n          python setup.py -v build_parsers\n          rsync -av ${BUILD_LIB}/ ./build/lib/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/lib/edb/ ./edb/\n\n    # Build PostgreSQL\n\n    - name: Build PostgreSQL\n      env:\n        CACHE_HIT: ${{ steps.postgres-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" == \"true\" ]]; then\n          cp build/postgres/install/stamp build/postgres/\n        else\n          python setup.py build_postgres\n          cp build/postgres/stamp build/postgres/install/\n        fi\n\n    # Build Stolon\n\n    - name: Set up Go\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      uses: actions/setup-go@v2\n      with:\n        go-version: 1.16\n\n    - uses: actions/checkout@v4\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      with:\n        repository: edgedb/stolon\n        path: build/stolon\n        ref: ${{ env.STOLON_GIT_REV }}\n        fetch-depth: 0\n        submodules: false\n\n    - name: Build Stolon\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      run: |\n        mkdir -p build/stolon/bin/\n        curl -fsSL https://releases.hashicorp.com/consul/1.10.1/consul_1.10.1_linux_amd64.zip | zcat > build/stolon/bin/consul\n        chmod +x build/stolon/bin/consul\n        cd build/stolon && make\n\n    # Install edgedb-server and populate egg-info\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Refresh the bootstrap cache\n\n    - name: Handle bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n        restore-keys: |\n          edb-bootstrap-v2-\n\n    - name: Bootstrap EdgeDB Server\n      if: steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        edb server --bootstrap-only\n\n  test:\n    runs-on: ubuntu-latest\n    needs: build\n    strategy:\n      fail-fast: false\n      matrix:\n        postgres-version: [ 17 ]\n        single-mode:\n         - ''\n         # These are very broken. Disabling them for now until we\n         # decide whether to fix them or give up.\n         # - 'NOCREATEDB NOCREATEROLE'\n         # - 'CREATEDB NOCREATEROLE'\n        multi-tenant-mode: [ '' ]\n        include:\n          - postgres-version: 14\n            single-mode: ''\n            multi-tenant-mode: ''\n          - postgres-version: 15\n            single-mode: ''\n            multi-tenant-mode: ''\n          - postgres-version: 16\n            single-mode: ''\n            multi-tenant-mode: ''\n          - postgres-version: 17\n            single-mode: ''\n            multi-tenant-mode: 'remote-compiler'\n          - postgres-version: 17\n            single-mode: ''\n            multi-tenant-mode: 'multi-tenant'\n    services:\n      postgres:\n        image: pgvector/pgvector:0.7.4-pg${{ matrix.postgres-version }}\n        env:\n          POSTGRES_PASSWORD: postgres\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n          --name postgres\n        ports:\n          - 5432:5432\n\n    steps:\n    - name: Trust pgvector extension\n      uses: docker://docker\n      with:\n        args: docker exec postgres sed -i $a\\trusted=true /usr/share/postgresql/${{ matrix.postgres-version }}/extension/vector.control\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Run the test\n\n    - name: Setup single mode role and database\n      if: ${{ matrix.single-mode }}\n      shell: python\n      run: |\n        import asyncio\n        import subprocess\n\n        from edb.server.pgcluster import get_pg_bin_dir\n\n        async def main():\n            psql = await get_pg_bin_dir() / \"psql\"\n            dsn = \"postgres://postgres:postgres@localhost/postgres\"\n\n            script = \"\"\"\\\n                CREATE ROLE singles;\n                ALTER ROLE singles WITH LOGIN PASSWORD 'test' NOSUPERUSER\n                  ${{ matrix.single-mode }};\n                CREATE DATABASE singles OWNER singles;\n                REVOKE ALL ON DATABASE singles FROM PUBLIC;\n                GRANT CONNECT ON DATABASE singles TO singles;\n                GRANT ALL ON DATABASE singles TO singles;\n            \"\"\"\n\n            subprocess.run(\n                [str(psql), dsn],\n                check=True,\n                text=True,\n                input=script,\n            )\n\n        asyncio.run(main())\n\n    - name: Test\n      env:\n        EDGEDB_TEST_POSTGRES_VERSION: ${{ matrix.postgres-version }}\n      run: |\n        if [[ \"${{ matrix.single-mode }}\" ]]; then\n          export EDGEDB_TEST_BACKEND_DSN=postgres://singles:test@localhost/singles\n        else\n          export EDGEDB_TEST_BACKEND_DSN=postgres://postgres:postgres@localhost/postgres\n        fi\n        if [[ \"${{ matrix.multi-tenant-mode }}\" == \"remote-compiler\" ]]; then\n          export EDGEDB_TEST_REMOTE_COMPILER=localhost:5660\n          export _EDGEDB_SERVER_COMPILER_POOL_SECRET=secret\n          __EDGEDB_DEVMODE=1 edgedb-server compiler --pool-size 2 &\n        fi\n        edb server --bootstrap-only --backend-dsn=$EDGEDB_TEST_BACKEND_DSN --testmode\n        if [[ \"${{ matrix.multi-tenant-mode }}\" == \"multi-tenant\" ]]; then\n          export EDGEDB_SERVER_MULTITENANT_CONFIG_FILE=/tmp/edb.mt.json\n          echo \"{\\\"localhost\\\":{\\\"instance-name\\\":\\\"localtest\\\",\\\"backend-dsn\\\":\\\"$EDGEDB_TEST_BACKEND_DSN\\\",\\\"admin\\\":true,\\\"max-backend-connections\\\":10}}\" > /tmp/edb.mt.json\n        fi\n        if [[ \"${{ matrix.single-mode }}\" == *\"NOCREATEDB\"* ]]; then\n          edb test -j1 -v --backend-dsn=$EDGEDB_TEST_BACKEND_DSN\n        else\n          edb test -j2 -v --backend-dsn=$EDGEDB_TEST_BACKEND_DSN\n        fi\n\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n    needs:\n      - build\n      - test\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows/tests.pool.yml",
    "content": "name: Pool Simulation Test\n\non:\n  push:\n    branches:\n      - master\n      - pool-test\n    paths:\n      - 'edb/server/connpool/**'\n      - 'edb/server/conn_pool/**'\n      - 'tests/test_server_pool.py'\n      - '.github/workflows/tests-pool.yml'\n  pull_request:\n    branches:\n      - master\n    paths:\n      - 'edb/server/connpool/**'\n      - 'edb/server/conn_pool/**'\n      - 'tests/test_server_pool.py'\n      - '.github/workflows/tests-pool.yml'\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    concurrency: pool-test\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n\n    - name: Cached requirements.txt\n      uses: actions/cache@v4\n      id: requirements-cache\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Compute requirements.txt\n      if: steps.requirements-cache.outputs.cache-hit != 'true'\n      run: |\n        python -m pip install pip-tools\n        pip-compile --no-strip-extras --all-build-deps \\\n          --extra test,language-server \\\n          --output-file requirements.txt pyproject.toml\n\n    - name: Install Python dependencies\n      run: |\n        python -c \"import sys; print(sys.prefix)\"\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    - name: Compute cache keys\n      run: |\n        mkdir -p shared-artifacts\n        if [ \"$(uname)\" = \"Darwin\" ]; then\n          find /usr/lib -type f -name 'lib*' -exec stat -f '%N %z' {} + | sort | shasum -a 256 | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        else\n          find /usr/lib -type f -name 'lib*' -printf '%P %s\\n' | sort | sha256sum | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        fi\n        python setup.py -q ci_helper --type rust >shared-artifacts/rust_cache_key.txt\n        python setup.py -q ci_helper --type ext >shared-artifacts/ext_cache_key.txt\n        python setup.py -q ci_helper --type parsers >shared-artifacts/parsers_cache_key.txt\n        python setup.py -q ci_helper --type postgres >shared-artifacts/postgres_git_rev.txt\n        python setup.py -q ci_helper --type libpg_query >shared-artifacts/libpg_query_git_rev.txt\n        echo 'f8cd94309eaccbfba5dea7835b88c78377608a37' >shared-artifacts/stolon_git_rev.txt\n        python setup.py -q ci_helper --type bootstrap >shared-artifacts/bootstrap_cache_key.txt\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo LIBPG_QUERY_GIT_REV=$(cat shared-artifacts/libpg_query_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    - name: Upload shared artifacts\n      uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n        retention-days: 1\n\n    # Restore binary cache\n\n    - name: Handle cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-v4-\n\n    - name: Handle cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Handle cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Handle cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Handle cached libpg_query build\n      uses: actions/cache@v4\n      id: libpg-query-cache\n      with:\n        path: edb/pgsql/parser/libpg_query/libpg_query.a\n        key: edb-libpg_query-v1-${{ env.LIBPG_QUERY_GIT_REV }}\n\n    # Install system dependencies for building\n\n    - name: Install system deps\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true'\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y uuid-dev libreadline-dev bison flex libprotobuf-c-dev\n\n    - name: Install Rust toolchain\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      uses: dsherret/rust-toolchain-file@v1\n\n    # Build Rust extensions\n\n    - name: Handle Rust extensions build cache\n      uses: actions/cache@v4\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/rust/extensions\n        key: edb-rust-build-v1-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-build-v1-\n\n    - name: Build Rust extensions\n      env:\n        CARGO_HOME: ${{ env.BUILD_TEMP }}/rust/extensions/cargo_home\n        CACHE_HIT: ${{ steps.rust-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p build/rust_extensions\n          rsync -av ./build/rust_extensions/ ${BUILD_LIB}/\n          python setup.py -v build_rust\n          rsync -av ${BUILD_LIB}/ build/rust_extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n\n    # Build libpg_query\n\n    - name: Build libpg_query\n      if: |\n        steps.libpg-query-cache.outputs.cache-hit != 'true' &&\n        steps.ext-cache.outputs.cache-hit != 'true'\n      run: |\n        python setup.py build_libpg_query\n\n    # Build extensions\n\n    - name: Handle Cython extensions build cache\n      uses: actions/cache@v4\n      if: steps.ext-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/edb\n        key: edb-ext-build-v4-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Build Cython extensions\n      env:\n        CACHE_HIT: ${{ steps.ext-cache.outputs.cache-hit }}\n        BUILD_EXT_MODE: py-only\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/extensions\n          rsync -av ./build/extensions/ ${BUILD_LIB}/\n          BUILD_EXT_MODE=py-only python setup.py -v build_ext\n          rsync -av ${BUILD_LIB}/ ./build/extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/extensions/edb/ ./edb/\n\n    # Build parsers\n\n    - name: Handle compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n        restore-keys: |\n          edb-parsers-v3-\n\n    - name: Build parsers\n      env:\n        CACHE_HIT: ${{ steps.parsers-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/lib\n          rsync -av ./build/lib/ ${BUILD_LIB}/\n          python setup.py -v build_parsers\n          rsync -av ${BUILD_LIB}/ ./build/lib/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/lib/edb/ ./edb/\n\n    # Build PostgreSQL\n\n    - name: Build PostgreSQL\n      env:\n        CACHE_HIT: ${{ steps.postgres-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" == \"true\" ]]; then\n          cp build/postgres/install/stamp build/postgres/\n        else\n          python setup.py build_postgres\n          cp build/postgres/stamp build/postgres/install/\n        fi\n\n    # Build Stolon\n\n    - name: Set up Go\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      uses: actions/setup-go@v2\n      with:\n        go-version: 1.16\n\n    - uses: actions/checkout@v4\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      with:\n        repository: edgedb/stolon\n        path: build/stolon\n        ref: ${{ env.STOLON_GIT_REV }}\n        fetch-depth: 0\n        submodules: false\n\n    - name: Build Stolon\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      run: |\n        mkdir -p build/stolon/bin/\n        curl -fsSL https://releases.hashicorp.com/consul/1.10.1/consul_1.10.1_linux_amd64.zip | zcat > build/stolon/bin/consul\n        chmod +x build/stolon/bin/consul\n        cd build/stolon && make\n\n    # Install edgedb-server and populate egg-info\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Refresh the bootstrap cache\n\n    - name: Handle bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n        restore-keys: |\n          edb-bootstrap-v2-\n\n    - name: Bootstrap EdgeDB Server\n      if: steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        edb server --bootstrap-only\n\n    - uses: actions/checkout@v4\n      if: startsWith(github.ref, 'refs/heads')\n      with:\n        repository: edgedb/edgedb-pool-simulation\n        path: pool-simulation\n        token: ${{ secrets.GITHUB_CI_BOT_TOKEN }}\n\n    - name: Run the pool simulation test\n      env:\n        PYTHONPATH: .\n        SIMULATION_CI: yes\n        TIME_SCALE: 10\n      run: |\n        mkdir -p pool-simulation/reports\n        python tests/test_server_pool.py\n\n    - uses: EndBug/add-and-commit@v7.0.0\n      if: ${{ always() }}\n      continue-on-error: true\n      with:\n        branch: main\n        cwd: pool-simulation\n        author_name: github-actions\n        author_email: 41898282+github-actions[bot]@users.noreply.github.com\n"
  },
  {
    "path": ".github/workflows/tests.reflection.yml",
    "content": "name: Tests with reflection validation\n\non:\n  schedule:\n    - cron: \"0 3 * * *\"\n  workflow_dispatch:\n    inputs: {}\n  push:\n    branches:\n      - \"REFL-*\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n\n    - name: Cached requirements.txt\n      uses: actions/cache@v4\n      id: requirements-cache\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Compute requirements.txt\n      if: steps.requirements-cache.outputs.cache-hit != 'true'\n      run: |\n        python -m pip install pip-tools\n        pip-compile --no-strip-extras --all-build-deps \\\n          --extra test,language-server \\\n          --output-file requirements.txt pyproject.toml\n\n    - name: Install Python dependencies\n      run: |\n        python -c \"import sys; print(sys.prefix)\"\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    - name: Compute cache keys\n      env:\n        GIST_TOKEN: ${{ secrets.CI_BOT_GIST_TOKEN }}\n      run: |\n        mkdir -p shared-artifacts\n        if [ \"$(uname)\" = \"Darwin\" ]; then\n          find /usr/lib -type f -name 'lib*' -exec stat -f '%N %z' {} + | sort | shasum -a 256 | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        else\n          find /usr/lib -type f -name 'lib*' -printf '%P %s\\n' | sort | sha256sum | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        fi\n        python setup.py -q ci_helper --type rust >shared-artifacts/rust_cache_key.txt\n        python setup.py -q ci_helper --type ext >shared-artifacts/ext_cache_key.txt\n        python setup.py -q ci_helper --type parsers >shared-artifacts/parsers_cache_key.txt\n        python setup.py -q ci_helper --type postgres >shared-artifacts/postgres_git_rev.txt\n        python setup.py -q ci_helper --type libpg_query >shared-artifacts/libpg_query_git_rev.txt\n        echo 'f8cd94309eaccbfba5dea7835b88c78377608a37' >shared-artifacts/stolon_git_rev.txt\n        python setup.py -q ci_helper --type bootstrap >shared-artifacts/bootstrap_cache_key.txt\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo LIBPG_QUERY_GIT_REV=$(cat shared-artifacts/libpg_query_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    - name: Upload shared artifacts\n      uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n        retention-days: 1\n\n    # Restore binary cache\n\n    - name: Handle cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-v4-\n\n    - name: Handle cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Handle cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Handle cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Handle cached libpg_query build\n      uses: actions/cache@v4\n      id: libpg-query-cache\n      with:\n        path: edb/pgsql/parser/libpg_query/libpg_query.a\n        key: edb-libpg_query-v1-${{ env.LIBPG_QUERY_GIT_REV }}\n\n    # Install system dependencies for building\n\n    - name: Install system deps\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true'\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y uuid-dev libreadline-dev bison flex libprotobuf-c-dev\n\n    - name: Install Rust toolchain\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      uses: dsherret/rust-toolchain-file@v1\n\n    # Build Rust extensions\n\n    - name: Handle Rust extensions build cache\n      uses: actions/cache@v4\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/rust/extensions\n        key: edb-rust-build-v1-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-build-v1-\n\n    - name: Build Rust extensions\n      env:\n        CARGO_HOME: ${{ env.BUILD_TEMP }}/rust/extensions/cargo_home\n        CACHE_HIT: ${{ steps.rust-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p build/rust_extensions\n          rsync -av ./build/rust_extensions/ ${BUILD_LIB}/\n          python setup.py -v build_rust\n          rsync -av ${BUILD_LIB}/ build/rust_extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n\n    # Build libpg_query\n\n    - name: Build libpg_query\n      if: |\n        steps.libpg-query-cache.outputs.cache-hit != 'true' &&\n        steps.ext-cache.outputs.cache-hit != 'true'\n      run: |\n        python setup.py build_libpg_query\n\n    # Build extensions\n\n    - name: Handle Cython extensions build cache\n      uses: actions/cache@v4\n      if: steps.ext-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/edb\n        key: edb-ext-build-v4-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Build Cython extensions\n      env:\n        CACHE_HIT: ${{ steps.ext-cache.outputs.cache-hit }}\n        BUILD_EXT_MODE: py-only\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/extensions\n          rsync -av ./build/extensions/ ${BUILD_LIB}/\n          BUILD_EXT_MODE=py-only python setup.py -v build_ext\n          rsync -av ${BUILD_LIB}/ ./build/extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/extensions/edb/ ./edb/\n\n    # Build parsers\n\n    - name: Handle compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n        restore-keys: |\n          edb-parsers-v3-\n\n    - name: Build parsers\n      env:\n        CACHE_HIT: ${{ steps.parsers-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/lib\n          rsync -av ./build/lib/ ${BUILD_LIB}/\n          python setup.py -v build_parsers\n          rsync -av ${BUILD_LIB}/ ./build/lib/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/lib/edb/ ./edb/\n\n    # Build PostgreSQL\n\n    - name: Build PostgreSQL\n      env:\n        CACHE_HIT: ${{ steps.postgres-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" == \"true\" ]]; then\n          cp build/postgres/install/stamp build/postgres/\n        else\n          python setup.py build_postgres\n          cp build/postgres/stamp build/postgres/install/\n        fi\n\n    # Build Stolon\n\n    - name: Set up Go\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      uses: actions/setup-go@v2\n      with:\n        go-version: 1.16\n\n    - uses: actions/checkout@v4\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      with:\n        repository: edgedb/stolon\n        path: build/stolon\n        ref: ${{ env.STOLON_GIT_REV }}\n        fetch-depth: 0\n        submodules: false\n\n    - name: Build Stolon\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      run: |\n        mkdir -p build/stolon/bin/\n        curl -fsSL https://releases.hashicorp.com/consul/1.10.1/consul_1.10.1_linux_amd64.zip | zcat > build/stolon/bin/consul\n        chmod +x build/stolon/bin/consul\n        cd build/stolon && make\n\n    # Install edgedb-server and populate egg-info\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Refresh the bootstrap cache\n\n    - name: Handle bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n        restore-keys: |\n          edb-bootstrap-v2-\n\n    - name: Bootstrap EdgeDB Server\n      if: steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        edb server --bootstrap-only\n\n  test:\n    needs: build\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Run the test\n\n    - name: Test\n      env:\n        EDGEDB_DEBUG_DELTA_VALIDATE_REFLECTION: 1\n      run: |\n        edb test -j2 -v\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n    needs:\n      - build\n      - test\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Tests\n\non:\n  push:\n    branches:\n      - master\n      - ci\n      - \"release/*\"\n  pull_request:\n    branches:\n      - '**'\n  schedule:\n    - cron: \"0 */3 * * *\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n\n    - name: Cached requirements.txt\n      uses: actions/cache@v4\n      id: requirements-cache\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Compute requirements.txt\n      if: steps.requirements-cache.outputs.cache-hit != 'true'\n      run: |\n        python -m pip install pip-tools\n        pip-compile --no-strip-extras --all-build-deps \\\n          --extra test,language-server \\\n          --output-file requirements.txt pyproject.toml\n\n    - name: Install Python dependencies\n      run: |\n        python -c \"import sys; print(sys.prefix)\"\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    - name: Compute cache keys and download the running times log\n      env:\n        GIST_TOKEN: ${{ secrets.CI_BOT_GIST_TOKEN }}\n      run: |\n        mkdir -p shared-artifacts\n        if [ \"$(uname)\" = \"Darwin\" ]; then\n          find /usr/lib -type f -name 'lib*' -exec stat -f '%N %z' {} + | sort | shasum -a 256 | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        else\n          find /usr/lib -type f -name 'lib*' -printf '%P %s\\n' | sort | sha256sum | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n        fi\n        python setup.py -q ci_helper --type rust >shared-artifacts/rust_cache_key.txt\n        python setup.py -q ci_helper --type ext >shared-artifacts/ext_cache_key.txt\n        python setup.py -q ci_helper --type parsers >shared-artifacts/parsers_cache_key.txt\n        python setup.py -q ci_helper --type postgres >shared-artifacts/postgres_git_rev.txt\n        python setup.py -q ci_helper --type libpg_query >shared-artifacts/libpg_query_git_rev.txt\n        echo 'f8cd94309eaccbfba5dea7835b88c78377608a37' >shared-artifacts/stolon_git_rev.txt\n        python setup.py -q ci_helper --type bootstrap >shared-artifacts/bootstrap_cache_key.txt\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo LIBPG_QUERY_GIT_REV=$(cat shared-artifacts/libpg_query_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n        curl \\\n          -H \"Accept: application/vnd.github.v3+json\" \\\n          -u edgedb-ci:$GIST_TOKEN \\\n          https://api.github.com/gists/8b722a65397f7c4c0df72f5394efa04c \\\n        | jq '.files.\"time_stats.csv\".raw_url' \\\n        | xargs curl > shared-artifacts/time_stats.csv\n\n    - name: Upload shared artifacts\n      uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n        retention-days: 1\n\n    # Restore binary cache\n\n    - name: Handle cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-v4-\n\n    - name: Handle cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Handle cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Handle cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Handle cached libpg_query build\n      uses: actions/cache@v4\n      id: libpg-query-cache\n      with:\n        path: edb/pgsql/parser/libpg_query/libpg_query.a\n        key: edb-libpg_query-v1-${{ env.LIBPG_QUERY_GIT_REV }}\n\n    # Install system dependencies for building\n\n    - name: Install system deps\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true'\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y uuid-dev libreadline-dev bison flex libprotobuf-c-dev\n\n    - name: Install Rust toolchain\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      uses: dsherret/rust-toolchain-file@v1\n\n    # Build Rust extensions\n\n    - name: Handle Rust extensions build cache\n      uses: actions/cache@v4\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/rust/extensions\n        key: edb-rust-build-v1-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-build-v1-\n\n    - name: Build Rust extensions\n      env:\n        CARGO_HOME: ${{ env.BUILD_TEMP }}/rust/extensions/cargo_home\n        CACHE_HIT: ${{ steps.rust-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p build/rust_extensions\n          rsync -av ./build/rust_extensions/ ${BUILD_LIB}/\n          python setup.py -v build_rust\n          rsync -av ${BUILD_LIB}/ build/rust_extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n\n    # Build libpg_query\n\n    - name: Build libpg_query\n      if: |\n        steps.libpg-query-cache.outputs.cache-hit != 'true' &&\n        steps.ext-cache.outputs.cache-hit != 'true'\n      run: |\n        python setup.py build_libpg_query\n\n    # Build extensions\n\n    - name: Handle Cython extensions build cache\n      uses: actions/cache@v4\n      if: steps.ext-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/edb\n        key: edb-ext-build-v4-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Build Cython extensions\n      env:\n        CACHE_HIT: ${{ steps.ext-cache.outputs.cache-hit }}\n        BUILD_EXT_MODE: py-only\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/extensions\n          rsync -av ./build/extensions/ ${BUILD_LIB}/\n          BUILD_EXT_MODE=py-only python setup.py -v build_ext\n          rsync -av ${BUILD_LIB}/ ./build/extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/extensions/edb/ ./edb/\n\n    # Build parsers\n\n    - name: Handle compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n        restore-keys: |\n          edb-parsers-v3-\n\n    - name: Build parsers\n      env:\n        CACHE_HIT: ${{ steps.parsers-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/lib\n          rsync -av ./build/lib/ ${BUILD_LIB}/\n          python setup.py -v build_parsers\n          rsync -av ${BUILD_LIB}/ ./build/lib/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/lib/edb/ ./edb/\n\n    # Build PostgreSQL\n\n    - name: Build PostgreSQL\n      env:\n        CACHE_HIT: ${{ steps.postgres-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" == \"true\" ]]; then\n          cp build/postgres/install/stamp build/postgres/\n        else\n          python setup.py build_postgres\n          cp build/postgres/stamp build/postgres/install/\n        fi\n\n    # Build Stolon\n\n    - name: Set up Go\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      uses: actions/setup-go@v2\n      with:\n        go-version: 1.16\n\n    - uses: actions/checkout@v4\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      with:\n        repository: edgedb/stolon\n        path: build/stolon\n        ref: ${{ env.STOLON_GIT_REV }}\n        fetch-depth: 0\n        submodules: false\n\n    - name: Build Stolon\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      run: |\n        mkdir -p build/stolon/bin/\n        curl -fsSL https://releases.hashicorp.com/consul/1.10.1/consul_1.10.1_linux_amd64.zip | zcat > build/stolon/bin/consul\n        chmod +x build/stolon/bin/consul\n        cd build/stolon && make\n\n    # Install edgedb-server and populate egg-info\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Refresh the bootstrap cache\n\n    - name: Handle bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n        restore-keys: |\n          edb-bootstrap-v2-\n\n    - name: Bootstrap EdgeDB Server\n      if: steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        edb server --bootstrap-only\n\n  cargo-test:\n    needs: build\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    - name: Download cache key\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Generate environment variables\n      run: |\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    - name: Handle Rust extensions build cache\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: ${{ env.BUILD_TEMP }}/rust/extensions\n        key: edb-rust-build-v1-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Install Rust toolchain\n      uses: dsherret/rust-toolchain-file@v1\n\n    - name: Cargo test\n      env:\n        CARGO_TARGET_DIR: ${{ env.BUILD_TEMP }}/rust/extensions\n        CARGO_HOME: ${{ env.BUILD_TEMP }}/rust/extensions/cargo_home\n      run:\n        cargo test --all-features\n\n  python-test:\n    needs: build\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        shard: [\n           1,  2,  3,  4,\n           5,  6,  7,  8,\n           9, 10, 11, 12,\n          13, 14, 15, 16,\n        ]\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Run the test\n\n    - name: Install Rust toolchain\n      uses: dsherret/rust-toolchain-file@v1\n\n    - name: Test\n      env:\n        SHARD: ${{ matrix.shard }}\n        EDGEDB_TEST_REPEATS: 1\n      run: |\n        mkdir -p results/\n        cp shared-artifacts/time_stats.csv results/running_times_${SHARD}.csv\n        edb test --jobs 2 --verbose --shard ${SHARD}/16 \\\n          --running-times-log=results/running_times_${SHARD}.csv \\\n          --result-log=results/result_${SHARD}.json\n\n    - name: Upload test results\n      uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      if: ${{ always() }}\n      with:\n        name: python-test-results-${{ matrix.shard }}\n        path: results\n        retention-days: 1\n\n  python-test-list:\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n    \n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # List tests and upload\n\n    - name: Generate complete list of tests for verification\n      env:\n        SHARD: ${{ matrix.shard }}\n        EDGEDB_TEST_REPEATS: 1\n      run: |\n        edb test --list > shared-artifacts/all_tests.txt\n\n    - name: Upload list of tests\n      uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: test-list\n        path: shared-artifacts\n        retention-days: 1\n\n  test-conclusion:\n    needs: [cargo-test, python-test, python-test-list]\n    runs-on: ubuntu-latest\n    if: ${{ always() }}\n    steps:\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.12.2'\n\n      - name: Install Python deps\n        run: |\n          python -m pip install requests click\n\n      - uses: actions/checkout@v4\n        with:\n          submodules: false\n\n      - name: Download python-test results\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n        with:\n          pattern: python-test-results-*\n          merge-multiple: true\n          path: results\n\n      # Render results and exit if they were unsuccessful\n      - name: Render results\n        run: |\n          python edb/tools/test/results.py 'results/result_*.json'\n\n      - name: Download shared artifacts\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n        with:\n          name: shared-artifacts\n          path: shared-artifacts\n\n      - name: Download test list\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n        with:\n          name: test-list\n          path: shared-artifacts\n\n      - name: Merge stats and verify tests completion\n        shell: python\n        env:\n          GIST_TOKEN: ${{ secrets.CI_BOT_GIST_TOKEN }}\n          GIT_REF: ${{ github.ref }}\n        run: |\n          import csv\n          import glob\n          import io\n          import os\n          import requests\n\n          orig = {}\n          new = {}\n          all_tests = set()\n          with open(\"shared-artifacts/time_stats.csv\") as f:\n              for name, t, c in csv.reader(f):\n                  assert name not in orig, \"duplicate test name in original stats!\"\n                  orig[name] = (t, int(c))\n\n          with open(\"shared-artifacts/all_tests.txt\") as f:\n              for line in f:\n                  assert line not in all_tests, \"duplicate test name in this run!\"\n                  all_tests.add(line.strip())\n\n          for new_file in glob.glob(\"results/running_times_*.csv\"):\n              with open(new_file) as f:\n                  for name, t, c in csv.reader(f):\n                      if int(c) > orig.get(name, (0, 0))[1]:\n                          if name.startswith(\"setup::\"):\n                              new[name] = (t, c)\n                          else:\n                              assert name not in new, f\"duplicate test! {name}\"\n                              new[name] = (t, c)\n                              all_tests.remove(name)\n\n          assert not all_tests, \"Tests not run! \\n\" + \"\\n\".join(all_tests)\n\n          if os.environ[\"GIT_REF\"] == \"refs/heads/master\":\n              buf = io.StringIO()\n              writer = csv.writer(buf)\n              orig.update(new)\n              for k, v in sorted(orig.items()):\n                  writer.writerow((k,) + v)\n\n              resp = requests.patch(\n                  \"https://api.github.com/gists/8b722a65397f7c4c0df72f5394efa04c\",\n                  headers={\"Accept\": \"application/vnd.github.v3+json\"},\n                  auth=(\"edgedb-ci\", os.environ[\"GIST_TOKEN\"]),\n                  json={\"files\": {\"time_stats.csv\": {\"content\": buf.getvalue()}}},\n              )\n              resp.raise_for_status()\n\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n    needs:\n      - test-conclusion\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows.src/build.dryrun.tpl.yml",
    "content": "<% from \"build.inc.yml\" import workflow, workflow_dispatch -%>\nname: Package Build Dry Run\n\non:\n  <<- workflow_dispatch() >>\n\njobs:\n  <<- workflow(package, targets, [], subdist=\"nightly\") ->>\n"
  },
  {
    "path": ".github/workflows.src/build.inc.yml",
    "content": "<% macro workflow_dispatch() %>\n  workflow_dispatch:\n    inputs:\n      gelpkg_ref:\n        description: \"gel-pkg git ref used to build the packages\"\n        default: \"master\"\n      metapkg_ref:\n        description: \"metapkg git ref used to build the packages\"\n        default: \"master\"\n<%- endmacro %>\n\n<% macro workflow(package, targets, publications, subdist=\"\", publish_all=False) %>\n  prep:\n    runs-on: ubuntu-latest\n<% if subdist == \"nightly\" %>\n    outputs:\n<% for tgt in targets.linux + targets.macos %>\n      if_<< tgt.name.replace('-', '_') >>: ${{ steps.scm.outputs.if_<< tgt.name.replace('-', '_') >> }}\n<% endfor %>\n<% endif %>\n    steps:\n    - uses: actions/checkout@v4\n\n<% if subdist == \"nightly\" %>\n    - name: Determine SCM revision\n      id: scm\n      shell: bash\n      run: |\n        rev=$(git rev-parse HEAD)\n        jq_filter='.packages[] | select(.basename == \"<< package.basename >>\") | select(.architecture == $ARCH) | .version_details.metadata.scm_revision | . as $rev | select(($rev != null) and ($REV | startswith($rev)))'\n<% for tgt in targets.linux + targets.macos %>\n        key=\"<< tgt.name >>\"\n        val=true\n\n<% if tgt.family == \"debian\" %>\n        idx_file=<< tgt.platform_version >>.nightly.json\n        url=https://packages.edgedb.com/apt/.jsonindexes/$idx_file\n\n<% elif tgt.family == \"redhat\" %>\n        idx_file=el<< tgt.platform_version >>.nightly.json\n        url=https://packages.edgedb.com/rpm/.jsonindexes/$idx_file\n\n<% elif tgt.family == \"generic\" %>\n        idx_file=<< tgt.platform_version >>-unknown-linux-<< \"{}\".format(tgt.platform_libc) if tgt.platform_libc else \"gnu\" >>.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n<% elif tgt.platform == \"macos\" %>\n        idx_file=<< tgt.platform_version >>-apple-darwin.nightly.json\n        url=https://packages.edgedb.com/archive/.jsonindexes/$idx_file\n\n<% endif %>\n        tmp_file=\"/tmp/$idx_file\"\n\n        if [ ! -e \"$tmp_file\" ]; then\n          curl --fail -o $tmp_file -s $url || true\n        fi\n        if [ -e \"$tmp_file\" ]; then\n          out=$(< \"$tmp_file\" jq -r --arg REV \"$rev\" --arg ARCH \"<< tgt.arch >>\" \"$jq_filter\")\n          if [ -n \"$out\" ]; then\n            echo \"Skip rebuilding existing ${key}\"\n            val=false\n          fi\n        fi\n\n        echo if_${key//-/_}=\"$val\" >> $GITHUB_OUTPUT\n\n<% endfor %>\n<% endif %>\n\n<%- for tgt in targets.linux %>\n<%- set plat_id = tgt.platform + (\"{}\".format(tgt.platform_libc) if tgt.platform_libc else \"\") + (\"-{}\".format(tgt.platform_version) if tgt.platform_version else \"\") %>\n\n  build-<< tgt.name >>:\n    runs-on: << tgt.runs_on if tgt.runs_on else \"ubuntu-latest\" >>\n    needs: prep\n<% if subdist == \"nightly\" %>\n    if: needs.prep.outputs.if_<< tgt.name.replace('-', '_') >> == 'true'\n<% endif %>\n\n    steps:\n    - name: Build\n      uses: docker://ghcr.io/geldata/gelpkg-build-<< plat_id >>:latest\n      env:\n        PACKAGE: \"<< package.name >>\"\n        SRC_REF: \"${{ github.sha }}\"\n        PKG_REVISION: \"<current-date>\"\n        <%- if subdist != \"\" %>\n        PKG_SUBDIST: \"<< subdist >>\"\n        <%- endif %>\n        PKG_PLATFORM: \"<< tgt.platform >>\"\n        PKG_PLATFORM_VERSION: \"<< tgt.platform_version >>\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        <%- if subdist != \"nightly\" %>\n        BUILD_IS_RELEASE: \"true\"\n        <%- endif %>\n        <%- if tgt.family == \"generic\" %>\n        BUILD_GENERIC: true\n        <%- endif %>\n        <%- if tgt.platform_libc %>\n        PKG_PLATFORM_LIBC: \"<< tgt.platform_libc >>\"\n        <%- endif %>\n        METAPKG_GIT_CACHE: disabled\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-<< tgt.name >>\n        path: artifacts/<< plat_id >>\n<%- endfor %>\n\n<%- for tgt in targets.macos %>\n<%- set plat_id = tgt.platform + (\"{}\".format(tgt.platform_libc) if tgt.platform_libc else \"\") + (\"-{}\".format(tgt.platform_version) if tgt.platform_version else \"\") %>\n\n  build-<< tgt.name >>:\n    runs-on: << tgt.runs_on if tgt.runs_on else \"macos-latest\" >>\n    needs: prep\n<% if subdist == \"nightly\" %>\n    if: needs.prep.outputs.if_<< tgt.name.replace('-', '_') >> == 'true'\n<% endif %>\n\n    steps:\n    - name: Update Homebrew before installing Rust toolchain\n      run: |\n        # Homebrew renamed `rustup-init` to `rustup`:\n        #   https://github.com/Homebrew/homebrew-core/pull/177840\n        # But the GitHub Action runner is not updated with this change yet.\n        # This caused the later `brew update` in step `Build` to relink Rust\n        # toolchain executables, overwriting the custom toolchain installed by\n        # `dsherret/rust-toolchain-file`. So let's just run `brew update` early.\n        brew update\n\n    - uses: actions/checkout@v4\n      if: << 'false' if tgt.runs_on and 'self-hosted' in tgt.runs_on else 'true' >>\n      with:\n        sparse-checkout: |\n          rust-toolchain.toml\n        sparse-checkout-cone-mode: false\n\n    - name: Install Rust toolchain\n      uses: dsherret/rust-toolchain-file@v1\n      if: << 'false' if tgt.runs_on and 'self-hosted' in tgt.runs_on else 'true' >>\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      if: << 'false' if tgt.runs_on and 'self-hosted' in tgt.runs_on else 'true' >>\n      with:\n        python-version: \"3.12\"\n\n    - name: Set up NodeJS\n      uses: actions/setup-node@v4\n      if: << 'false' if tgt.runs_on and 'self-hosted' in tgt.runs_on else 'true' >>\n      with:\n        node-version: '20'\n\n    - name: Install dependencies\n      if: << 'false' if tgt.runs_on and 'self-hosted' in tgt.runs_on else 'true' >>\n      run: |\n        env HOMEBREW_NO_AUTO_UPDATE=1 brew install libmagic\n\n    - name: Install an alias\n      # This is probably not strictly needed, but sentencepiece build script reports\n      # errors without it.\n      if: << 'false' if tgt.runs_on and 'self-hosted' in tgt.runs_on else 'true' >>\n      run: |\n        printf '#!/bin/sh\\n\\nexec sysctl -n hw.logicalcpu' > /usr/local/bin/nproc\n        chmod +x /usr/local/bin/nproc\n\n    - name: Build\n      env:\n        PACKAGE: \"<< package.name >>\"\n        SRC_REF: \"${{ github.sha }}\"\n        <%- if subdist != \"nightly\" %>\n        BUILD_IS_RELEASE: \"true\"\n        <%- endif %>\n        PKG_REVISION: \"<current-date>\"\n        <%- if subdist != \"\" %>\n        PKG_SUBDIST: \"<< subdist >>\"\n        <%- endif %>\n        PKG_PLATFORM: \"<< tgt.platform >>\"\n        PKG_PLATFORM_VERSION: \"<< tgt.platform_version >>\"\n        PKG_PLATFORM_ARCH: \"<< tgt.arch if tgt.arch else '' >>\"\n        EXTRA_OPTIMIZATIONS: \"true\"\n        METAPKG_GIT_CACHE: disabled\n        <%- if tgt.family == \"generic\" %>\n        BUILD_GENERIC: true\n        <%- endif %>\n        CMAKE_POLICY_VERSION_MINIMUM: '3.5'\n        GEL_PKG_REF: ${{ inputs.gelpkg_ref }}\n        METAPKG_REF: ${{ inputs.metapkg_ref }}\n      run: |\n        edgedb-pkg/integration/macos/build.sh\n\n    - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: builds-<< tgt.name >>\n        path: artifacts/<< plat_id >>\n<%- endfor %>\n\n<%- if package.name != \"edgedbpkg.edgedb_ls:EdgeDBLanguageServer\" %>\n<%- for tgt in targets.linux %>\n<%- set plat_id = tgt.platform + (\"{}\".format(tgt.platform_libc) if tgt.platform_libc else \"\") + (\"-{}\".format(tgt.platform_version) if tgt.platform_version else \"\") %>\n\n  test-<< tgt.name >>:\n    needs: [build-<< tgt.name >>]\n    runs-on: << tgt.runs_on if tgt.runs_on else \"ubuntu-latest\" >>\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-<< tgt.name >>\n        path: artifacts/<< plat_id >>\n\n    - name: Test\n      uses: docker://ghcr.io/geldata/gelpkg-test-<< plat_id >>:latest\n      env:\n        <%- if subdist != \"\" %>\n        PKG_SUBDIST: \"<< subdist >>\"\n        <%- endif %>\n        PKG_PLATFORM: \"<< tgt.platform >>\"\n        PKG_PLATFORM_VERSION: \"<< tgt.platform_version >>\"\n        PKG_PLATFORM_LIBC: \"<< tgt.platform_libc >>\"\n        PKG_TEST_SELECT: \"<< package.test.select >>\"\n        PKG_TEST_EXCLUDE: \"<< package.test.exclude >>\"\n        PKG_TEST_FILES: \"<< package.test.files >> << tgt.test.files >>\"\n        # edb test with -j higher than 1 seems to result in workflow\n        # jobs getting killed arbitrarily by Github.\n        PKG_TEST_JOBS: << 0 if tgt.runs_on and 'self-hosted' in tgt.runs_on else 1 >>\n<%- endfor %>\n\n<%- for tgt in targets.macos %>\n<%- set plat_id = tgt.platform + (\"{}\".format(tgt.platform_libc) if tgt.platform_libc else \"\") + (\"-{}\".format(tgt.platform_version) if tgt.platform_version else \"\") %>\n\n  test-<< tgt.name >>:\n    needs: [build-<< tgt.name >>]\n    runs-on: << tgt.runs_on if tgt.runs_on else \"macos-latest\" >>\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-<< tgt.name >>\n        path: artifacts/<< plat_id >>\n\n    - name: Test\n      env:\n        <%- if subdist != \"\" %>\n        PKG_SUBDIST: \"<< subdist >>\"\n        <%- endif %>\n        PKG_PLATFORM: \"<< tgt.platform >>\"\n        PKG_PLATFORM_VERSION: \"<< tgt.platform_version >>\"\n        PKG_TEST_SELECT: \"<< package.test.select >>\"\n        PKG_TEST_EXCLUDE: \"<< package.test.exclude >>\"\n        PKG_TEST_FILES: \"<< package.test.files >> << tgt.test.files >>\"\n      run: |\n        <%- if tgt.platform_version == \"x86_64\" %>\n        # Bump shmmax and shmall to avoid test failures.\n        sudo sysctl -w kern.sysv.shmmax=12582912\n        sudo sysctl -w kern.sysv.shmall=12582912\n        <%- endif %>\n        edgedb-pkg/integration/macos/test.sh\n<%- endfor %>\n<%- endif %>\n\n<%- if publish_all %>\n  collect:\n    needs:\n    <%- for tgt in targets.linux + targets.macos %>\n    - test-<< tgt.name >>\n    <%- endfor %>\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo 'All build+tests passed, ready to publish now!'\n<%- endif %>\n\n<%- for tgt in targets.linux %>\n<%- set plat_id = tgt.platform + (\"{}\".format(tgt.platform_libc) if tgt.platform_libc else \"\") + (\"-{}\".format(tgt.platform_version) if tgt.platform_version else \"\") %>\n<%- for publish in publications %>\n\n  publish<< publish.suffix>>-<< tgt.name >>:\n    needs: [<% if publish_all %>collect<% elif package.name != \"edgedbpkg.edgedb_ls:EdgeDBLanguageServer\" %>test-<< tgt.name >><% else %>build-<< tgt.name >><% endif %>]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-<< tgt.name >>\n        path: artifacts/<< plat_id >>\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        <%- if subdist != \"\" %>\n        PKG_SUBDIST: \"<< subdist >>\"\n        <%- endif %>\n        <%- if publish.server != \"\" %>\n        PACKAGE_SERVER: << publish.server >>\n        <%- endif %>\n        PKG_PLATFORM: \"<< tgt.platform >>\"\n        PKG_PLATFORM_VERSION: \"<< tgt.platform_version >>\"\n        PKG_PLATFORM_LIBC: \"<< tgt.platform_libc >>\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n\n  check-published<<publish.suffix>>-<< tgt.name >>:\n    needs: [publish<< publish.suffix >>-<< tgt.name >>]\n    runs-on: << tgt.runs_on if tgt.runs_on else \"ubuntu-latest\" >>\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-<< tgt.name >>\n        path: artifacts/<< plat_id >>\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: << plat_id >>\n\n    - name: Test Published\n      uses: docker://ghcr.io/geldata/gelpkg-testpublished-<< plat_id >>:latest\n      env:\n        PKG_NAME: \"${{ steps.describe.outputs.name }}\"\n        <%- if subdist != \"\" %>\n        PKG_SUBDIST: \"<< subdist >>\"\n        <%- endif %>\n        <%- if publish.server != \"\" %>\n        PACKAGE_SERVER: << publish.server >>\n        <%- endif %>\n        PKG_PLATFORM: \"<< tgt.platform >>\"\n        PKG_PLATFORM_VERSION: \"<< tgt.platform_version >>\"\n        PKG_INSTALL_REF: \"${{ steps.describe.outputs.install-ref }}\"\n        PKG_VERSION_SLOT: \"${{ steps.describe.outputs.version-slot }}\"\n\n    outputs:\n      version-slot: ${{ steps.describe.outputs.version-slot }}\n      version-core: ${{ steps.describe.outputs.version-core }}\n      catalog-version: ${{ steps.describe.outputs.catalog-version }}\n<%- endfor %>\n<%- endfor %>\n\n<%- if publications %>\n<%- for tgt in targets.macos %>\n<%- set plat_id = tgt.platform + (\"{}\".format(tgt.platform_libc) if tgt.platform_libc else \"\") + (\"-{}\".format(tgt.platform_version) if tgt.platform_version else \"\") %>\n\n  publish-<< tgt.name >>:\n    needs: [<% if publish_all %>collect<% elif package.name != \"edgedbpkg.edgedb_ls:EdgeDBLanguageServer\" %>test-<< tgt.name >><% else %>build-<< tgt.name >><% endif %>]\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: builds-<< tgt.name >>\n        path: artifacts/<< plat_id >>\n\n    - uses: actions/checkout@v4\n      with:\n        repository: edgedb/edgedb-pkg\n        ref: master\n        path: edgedb-pkg\n\n    - name: Describe\n      id: describe\n      uses: edgedb/edgedb-pkg/integration/actions/describe-artifact@master\n      with:\n        target: << plat_id >>\n\n    - name: Publish\n      uses: docker://ghcr.io/geldata/gelpkg-upload-linux-x86_64:latest\n      env:\n        <%- if subdist != \"\" %>\n        PKG_SUBDIST: \"<< subdist >>\"\n        <%- endif %>\n        PKG_PLATFORM: \"<< tgt.platform >>\"\n        PKG_PLATFORM_VERSION: \"<< tgt.platform_version >>\"\n        PACKAGE_UPLOAD_SSH_KEY: \"${{ secrets.PACKAGE_UPLOAD_SSH_KEY }}\"\n<%- endfor %>\n<%- endif %>\n\n<%- set docker_tgts = targets.linux | selectattr(\"docker_arch\") | list %>\n<%- if docker_tgts and publications %>\n<%- set pub_outputs = \"needs.check-published-\" + (docker_tgts|first)[\"name\"] + \".outputs\" %>\n\n  publish-docker:\n    needs:\n      <%- for tgt in docker_tgts %>\n      - check-published-<< tgt.name >>\n      <%- endfor %>\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        repository: geldata/gel-docker\n        ref: master\n        path: dockerfile\n\n    - name: Login to Docker Hub\n      uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0\n      with:\n        username: ${{ secrets.DOCKER_USERNAME }}\n        password: ${{ secrets.DOCKER_PASSWORD }}\n\n    - name: Login to GitHub Container Registry\n      uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0\n      with:\n        registry: ghcr.io\n        username: \"edgedb-ci\"\n        password: ${{ secrets.GITHUB_CI_BOT_TOKEN }}\n\n    - env:\n        VERSION_SLOT: \"${{ << pub_outputs >>.version-slot }}\"\n        VERSION_CORE: \"${{ << pub_outputs >>.version-core }}\"\n        CATALOG_VERSION: \"${{ << pub_outputs >>.catalog-version }}\"\n        PKG_SUBDIST: \"<< subdist >>\"\n      id: tags\n      run: |\n        set -e\n\n        url='https://registry.hub.docker.com/v2/repositories/geldata/gel/tags?page_size=100'\n        repo_tags=$(\n          while [ -n \"$url\" ]; do\n            resp=$(curl -L -s \"$url\")\n            url=$(echo \"$resp\" | jq -r \".next\")\n            if [ \"$url\" = \"null\" ] || [ -z \"$url\" ]; then\n              break\n            fi\n            echo \"$resp\" | jq -r '.\"results\"[][\"name\"]'\n          done | grep \"^[[:digit:]]\\+.*\" | grep -v \"alpha\\|beta\\|rc\" || :\n        )\n\n        tags=()\n\n        if [ \"$PKG_SUBDIST\" = \"nightly\" ]; then\n          tags+=(\n            \"nightly\"\n            \"nightly_${VERSION_SLOT}_cv${CATALOG_VERSION}\"\n          )\n        else\n          tags+=( \"$VERSION_CORE\" )\n\n          top=$(printf \"%s\\n%s\\n\" \"$VERSION_CORE\" \"$repo_tags\" \\\n                | grep \"^${VERSION_SLOT}[\\.-]\" \\\n                | sort --version-sort --reverse | head -n 1)\n          if [ \"$top\" == \"$VERSION_CORE\" ]; then\n            tags+=( \"$VERSION_SLOT\" )\n          fi\n\n          if [ -z \"$PKG_SUBDIST\" ]; then\n            top=$(printf \"%s\\n%s\\n\" \"$VERSION_CORE\" \"$repo_tags\" \\\n                  | sort --version-sort --reverse | head -n 1)\n            if [ \"$top\" == \"$VERSION_CORE\" ]; then\n              tags+=( \"latest\" )\n            fi\n          fi\n        fi\n\n        fq_tags=()\n        images=(\"geldata/gel\" \"ghcr.io/geldata/gel\")\n\n        for image in \"${images[@]}\"; do\n          fq_tags+=(\"${tags[@]/#/${image}:}\")\n        done\n\n        IFS=,\n        echo \"tags=${fq_tags[*]}\" >> $GITHUB_OUTPUT\n\n    - name: Set up QEMU\n      uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0\n\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0\n\n    - name: Build and Publish Docker Image\n      uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.10.0\n      with:\n        push: true\n        provenance: mode=max\n        tags: \"${{ steps.tags.outputs.tags }}\"\n        context: dockerfile\n        build-args: |\n          version=${{ << pub_outputs >>.version-slot }}\n          exact_version=${{ << pub_outputs >>.version-core }}\n          <%- if subdist != \"\" %>\n          subdist=<< subdist >>\n          <%- endif %>\n        platforms: << docker_tgts|map(attribute=\"docker_arch\")|join(\",\") >>\n<%- endif %>\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n\n    needs:\n      - prep\n    <%- if publish_all %>\n      - collect\n    <%- else %>\n    <%- endif %>\n    <%- for tgt in targets.linux %>\n      - build-<< tgt.name >>\n    <%- if package.name != \"edgedbpkg.edgedb_ls:EdgeDBLanguageServer\" %>\n      - test-<< tgt.name >>\n    <%- endif %>\n    <%- for publish in publications %>\n      - publish<< publish.suffix>>-<< tgt.name >>\n      - check-published<< publish.suffix>>-<< tgt.name >>\n    <%- endfor %>\n    <%- endfor %>\n    <%- for tgt in targets.macos %>\n      - build-<< tgt.name >>\n    <%- if package.name != \"edgedbpkg.edgedb_ls:EdgeDBLanguageServer\" %>\n      - test-<< tgt.name >>\n    <%- endif %>\n    <%- for publish in publications %>\n      - publish<< publish.suffix>>-<< tgt.name >>\n    <%- endfor %>\n    <%- endfor %>\n    <%- if docker_tgts and publications %>\n      - publish-docker\n    <%- endif %>\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n\n<%- endmacro %>\n"
  },
  {
    "path": ".github/workflows.src/build.ls-nightly.tpl.yml",
    "content": "<% from \"build.inc.yml\" import workflow, workflow_dispatch -%>\nname: 'ls: Build and Publish Nightly Packages'\n\non:\n  schedule:\n    - cron: \"0 1 * * *\"\n  <<- workflow_dispatch() >>\n  push:\n    branches:\n      - nightly\n\njobs:\n  <<- workflow(package, targets, publications, subdist=\"nightly\") ->>\n"
  },
  {
    "path": ".github/workflows.src/build.ls.targets.yml",
    "content": "publications:\n  - name: prod\n    suffix: \"\"\n    server: sftp://uploader@package-upload.edgedb.net:22/\n\npackage:\n  name: edgedbpkg.edgedb_ls:EdgeDBLanguageServer\n  basename: gel-ls\n  tests:\n    files: \"test_language_server.py\"\n\ntargets:\n  linux:\n    - name: linux-x86_64\n      arch: x86_64\n      platform: linux\n      platform_version: x86_64\n      family: generic\n      runs_on: [self-hosted, linux, x64]\n    - name: linux-aarch64\n      arch: aarch64\n      platform: linux\n      platform_version: aarch64\n      family: generic\n      runs_on: [self-hosted, linux, arm64]\n    - name: linuxmusl-x86_64\n      arch: x86_64\n      platform: linux\n      platform_version: x86_64\n      platform_libc: musl\n      family: generic\n      runs_on: [self-hosted, linux, x64]\n    - name: linuxmusl-aarch64\n      arch: aarch64\n      platform: linux\n      platform_version: aarch64\n      platform_libc: musl\n      family: generic\n      runs_on: [self-hosted, linux, arm64]\n\n  macos:\n    - name: macos-x86_64\n      arch: x86_64\n      platform: macos\n      platform_version: x86_64\n      family: generic\n      runs_on: [macos-13]\n    - name: macos-aarch64\n      arch: aarch64\n      platform: macos\n      platform_version: aarch64\n      family: generic\n      runs_on: [macos-14]\n"
  },
  {
    "path": ".github/workflows.src/build.nightly.tpl.yml",
    "content": "<% from \"build.inc.yml\" import workflow, workflow_dispatch -%>\nname: Build Test and Publish Nightly Packages\n\non:\n  schedule:\n    - cron: \"0 1 * * *\"\n  <<- workflow_dispatch() >>\n  push:\n    branches:\n      - nightly\n\njobs:\n  <<- workflow(package, targets, publications, subdist=\"nightly\") ->>\n"
  },
  {
    "path": ".github/workflows.src/build.release.tpl.yml",
    "content": "<% from \"build.inc.yml\" import workflow, workflow_dispatch -%>\nname: Build Test and Publish a Release\n\non:\n  <<- workflow_dispatch() >>\n\njobs:\n  <<- workflow(package, targets, publications, subdist=\"\", publish_all=True) ->>\n"
  },
  {
    "path": ".github/workflows.src/build.targets.yml",
    "content": "publications:\n    - name: prod\n      suffix: \"\"\n      server: sftp://uploader@package-upload.edgedb.net:22/\n\npackage:\n    name: \"edgedbpkg.edgedb:Gel\"\n    basename: gel-server\n\ntargets:\n    linux:\n        - name: debian-buster-x86_64\n          arch: x86_64\n          platform: debian\n          platform_version: buster\n          family: debian\n          runs_on: [package-builder, self-hosted, linux, x64]\n        - name: debian-buster-aarch64\n          arch: aarch64\n          platform: debian\n          platform_version: buster\n          family: debian\n          runs_on: [package-builder, self-hosted, linux, arm64]\n        - name: debian-bullseye-x86_64\n          arch: x86_64\n          platform: debian\n          platform_version: bullseye\n          family: debian\n          runs_on: [package-builder, self-hosted, linux, x64]\n        - name: debian-bullseye-aarch64\n          arch: aarch64\n          platform: debian\n          platform_version: bullseye\n          family: debian\n          runs_on: [package-builder, self-hosted, linux, arm64]\n        - name: debian-bookworm-x86_64\n          arch: x86_64\n          platform: debian\n          platform_version: bookworm\n          family: debian\n          runs_on: [package-builder, self-hosted, linux, x64]\n          docker_arch: linux/amd64\n        - name: debian-bookworm-aarch64\n          arch: aarch64\n          platform: debian\n          platform_version: bookworm\n          family: debian\n          runs_on: [package-builder, self-hosted, linux, arm64]\n          docker_arch: linux/arm64\n        - name: ubuntu-focal-x86_64\n          arch: x86_64\n          platform: ubuntu\n          platform_version: focal\n          family: debian\n          runs_on: [package-builder, self-hosted, linux, x64]\n        - name: ubuntu-focal-aarch64\n          arch: aarch64\n          platform: ubuntu\n          platform_version: focal\n          family: debian\n          runs_on: [package-builder, self-hosted, linux, arm64]\n        - name: ubuntu-jammy-x86_64\n          arch: x86_64\n          platform: ubuntu\n          platform_version: jammy\n          family: debian\n          runs_on: [package-builder, self-hosted, linux, x64]\n        - name: ubuntu-jammy-aarch64\n          arch: aarch64\n          platform: ubuntu\n          platform_version: jammy\n          family: debian\n          runs_on: [package-builder, self-hosted, linux, arm64]\n        - name: ubuntu-noble-x86_64\n          arch: x86_64\n          platform: ubuntu\n          platform_version: noble\n          family: debian\n          runs_on: [package-builder, self-hosted, linux, x64]\n        - name: ubuntu-noble-aarch64\n          arch: aarch64\n          platform: ubuntu\n          platform_version: noble\n          family: debian\n          runs_on: [package-builder, self-hosted, linux, arm64]\n        - name: centos-8-x86_64\n          arch: x86_64\n          platform: centos\n          platform_version: 8\n          family: redhat\n          runs_on: [package-builder, self-hosted, linux, x64]\n        - name: centos-8-aarch64\n          arch: aarch64\n          platform: centos\n          platform_version: 8\n          family: redhat\n          runs_on: [package-builder, self-hosted, linux, arm64]\n        - name: rockylinux-9-x86_64\n          arch: x86_64\n          platform: rockylinux\n          platform_version: 9\n          family: redhat\n          runs_on: [package-builder, self-hosted, linux, x64]\n        - name: rockylinux-9-aarch64\n          arch: aarch64\n          platform: rockylinux\n          platform_version: 9\n          family: redhat\n          runs_on: [package-builder, self-hosted, linux, arm64]\n        - name: linux-x86_64\n          arch: x86_64\n          platform: linux\n          platform_version: x86_64\n          family: generic\n          runs_on: [package-builder, self-hosted, linux, x64]\n        - name: linux-aarch64\n          arch: aarch64\n          platform: linux\n          platform_version: aarch64\n          family: generic\n          runs_on: [package-builder, self-hosted, linux, arm64]\n        - name: linuxmusl-x86_64\n          arch: x86_64\n          platform: linux\n          platform_version: x86_64\n          platform_libc: musl\n          family: generic\n          runs_on: [package-builder, self-hosted, linux, x64]\n        - name: linuxmusl-aarch64\n          arch: aarch64\n          platform: linux\n          platform_version: aarch64\n          platform_libc: musl\n          family: generic\n          runs_on: [package-builder, self-hosted, linux, arm64]\n\n    macos:\n        - name: macos-x86_64\n          arch: x86_64\n          platform: macos\n          platform_version: x86_64\n          family: generic\n          runs_on: [macos-13]\n          # Run fewer tests on x86_64, since the test runner is very slow.\n          test:\n            files: >\n              test_dump*.py test_backend_*.py test_database.py\n              test_server_*.py test_edgeql_ddl.py test_session.py\n        - name: macos-aarch64\n          arch: aarch64\n          platform: macos\n          platform_version: aarch64\n          family: generic\n          runs_on: [macos-14]\n"
  },
  {
    "path": ".github/workflows.src/build.testing.tpl.yml",
    "content": "<% from \"build.inc.yml\" import workflow, workflow_dispatch -%>\nname: Build Test and Publish a Testing Release\n\non:\n  <<- workflow_dispatch() >>\n\njobs:\n  <<- workflow(package, targets, publications, subdist=\"testing\", publish_all=True) ->>\n"
  },
  {
    "path": ".github/workflows.src/render.py",
    "content": "#!/usr/bin/env python3\n\nimport argparse\nimport pathlib\nimport sys\n\nimport jinja2\nimport yaml\n\n\nenv = jinja2.Environment(\n    variable_start_string='<<',\n    variable_end_string='>>',\n    block_start_string='<%',\n    block_end_string='%>',\n    loader=jinja2.FileSystemLoader(pathlib.Path(__file__).parent),\n)\n\n\ndef die(msg):\n    print(msg, file=sys.stderr)\n    sys.exit(1)\n\n\ndef _expand_test_spec(target):\n    if \"test\" not in target:\n        target[\"test\"] = {\n            \"include\": \"\",\n            \"exclude\": \"\",\n            \"files\": \"\"\n        }\n\n    for key in {\"include\", \"exclude\", \"files\"}:\n        if key not in target[\"test\"]:\n            target[\"test\"][key] = \"\"\n\n\ndef _render(tpl_path, data):\n    with open(tpl_path) as f:\n        tpl = env.from_string(f.read())\n\n    return tpl.render(**data)\n\n\ndef main():\n    parser = argparse.ArgumentParser()\n    parser.add_argument('--workflow', choices=[\"build\", \"test\"], required=True)\n    parser.add_argument('template')\n    parser.add_argument('datafile')\n\n    args = parser.parse_args()\n\n    tplfile = f'{args.template}.tpl.yml'\n    path = pathlib.Path(__file__).parent / tplfile\n\n    if not path.exists():\n        die(f'template does not exist: {tplfile}')\n\n    datapath = pathlib.Path(__file__).parent / args.datafile\n\n    if datapath.exists():\n        with open(datapath) as f:\n            data = yaml.load(f, Loader=yaml.SafeLoader)\n    else:\n        data = {}\n\n    if args.workflow == \"build\":\n        package = data.get(\"package\")\n        if not package or not isinstance(package, dict):\n            die(f\"invalid package: specification in {datapath}\")\n        if not package.get(\"name\"):\n            die(f\"missing package.name in {datapath}\")\n\n        _expand_test_spec(package)\n\n        targets = data.get(\"targets\")\n        if not targets or not isinstance(targets, dict):\n            die(f\"invalid targets: specification in {datapath}\")\n\n        for target_list in targets.values():\n            for target in target_list:\n                _expand_test_spec(target)\n\n    output = _render(path, data)\n\n    target = (\n        pathlib.Path(__file__).parent.parent\n        / 'workflows'\n        / f'{args.template}.yml'\n    )\n    with open(target, 'w') as f:\n        print(output, file=f)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": ".github/workflows.src/tests.ha.targets.yml",
    "content": "data:\n"
  },
  {
    "path": ".github/workflows.src/tests.ha.tpl.yml",
    "content": "<% from \"tests.inc.yml\" import build, calc_cache_key, restore_cache -%>\nname: High Availability Tests\n\non:\n  workflow_dispatch:\n    inputs: {}\n  workflow_run:\n    workflows: [\"Tests\"]\n    types:\n      - completed\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    if: github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch'\n\n    steps:\n    <%- call build() -%>\n    # Our HA tests currently only work on Postgres 14 (see #6332),\n    # so check it out before we compute our build cache keys.\n    - name: Switch back to Postgres 14\n      shell: bash\n      run: |\n        set -e\n        cd postgres\n        # Fetch postgres 14, since the clone was shallow\n        git fetch origin REL_14_8 --depth=1\n        # For whatever reason the tag doesn't get fetched, so find it\n        # at FETCH_HEAD\n        git checkout FETCH_HEAD\n\n    - name: Compute cache keys\n      env:\n        GIST_TOKEN: ${{ secrets.CI_BOT_GIST_TOKEN }}\n      run: |\n        << calc_cache_key()|indent >>\n    <%- endcall %>\n\n  ha-test:\n    needs: build\n    runs-on: ubuntu-latest\n\n    steps:\n    <<- restore_cache() >>\n\n    # Run the test\n\n    - name: Test\n      env:\n        SHARD: ${{ matrix.shard }}\n        EDGEDB_TEST_HA: 1\n        EDGEDB_TEST_CONSUL_PATH: build/stolon/bin/consul\n        EDGEDB_TEST_STOLON_CTL: build/stolon/bin/stolonctl\n        EDGEDB_TEST_STOLON_SENTINEL: build/stolon/bin/stolon-sentinel\n        EDGEDB_TEST_STOLON_KEEPER: build/stolon/bin/stolon-keeper\n      run: |\n        edb test -j1 -v -k test_ha_\n\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n    needs:\n      - build\n      - ha-test\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows.src/tests.inc.yml",
    "content": "<% macro init(ref='') -%>\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n<%- if ref != \"\" %>\n        ref: << ref >>\n<%- endif %>\n\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 50\n        submodules: true\n<%- if ref != \"\" %>\n        ref: << ref >>\n<%- endif %>\n\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      id: setup-python\n      with:\n        python-version: '3.12.2'\n        cache: 'pip'\n        cache-dependency-path: |\n          pyproject.toml\n\n    # The below is technically a lie as we are technically not\n    # inside a virtual env, but there is really no reason to bother\n    # actually creating and activating one as below works just fine.\n    - name: Export $VIRTUAL_ENV\n      run: |\n        venv=\"$(python -c 'import sys; sys.stdout.write(sys.prefix)')\"\n        echo \"VIRTUAL_ENV=${venv}\" >> $GITHUB_ENV\n\n    - name: Set up uv cache\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/uv\n        key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}\n\n<%- endmacro %>\n\n<% macro build(ref=\"\") %>\n    << init(ref) >>\n\n    - name: Cached requirements.txt\n      uses: actions/cache@v4\n      id: requirements-cache\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Compute requirements.txt\n      if: steps.requirements-cache.outputs.cache-hit != 'true'\n      run: |\n        python -m pip install pip-tools\n        pip-compile --no-strip-extras --all-build-deps \\\n          --extra test,language-server \\\n          --output-file requirements.txt pyproject.toml\n\n    - name: Install Python dependencies\n      run: |\n        python -c \"import sys; print(sys.prefix)\"\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n    << caller() >>\n\n    - name: Upload shared artifacts\n      uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n        retention-days: 1\n\n    # Restore binary cache\n\n    - name: Handle cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-v4-\n\n    - name: Handle cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Handle cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Handle cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Handle cached libpg_query build\n      uses: actions/cache@v4\n      id: libpg-query-cache\n      with:\n        path: edb/pgsql/parser/libpg_query/libpg_query.a\n        key: edb-libpg_query-v1-${{ env.LIBPG_QUERY_GIT_REV }}\n\n    # Install system dependencies for building\n\n    - name: Install system deps\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true'\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y uuid-dev libreadline-dev bison flex libprotobuf-c-dev\n\n    - name: Install Rust toolchain\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      uses: dsherret/rust-toolchain-file@v1\n\n    # Build Rust extensions\n\n    - name: Handle Rust extensions build cache\n      uses: actions/cache@v4\n      if: steps.rust-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/rust/extensions\n        key: edb-rust-build-v1-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n        restore-keys: |\n          edb-rust-build-v1-\n\n    - name: Build Rust extensions\n      env:\n        CARGO_HOME: ${{ env.BUILD_TEMP }}/rust/extensions/cargo_home\n        CACHE_HIT: ${{ steps.rust-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p build/rust_extensions\n          rsync -av ./build/rust_extensions/ ${BUILD_LIB}/\n          python setup.py -v build_rust\n          rsync -av ${BUILD_LIB}/ build/rust_extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n\n    # Build libpg_query\n\n    - name: Build libpg_query\n      if: |\n        steps.libpg-query-cache.outputs.cache-hit != 'true' &&\n        steps.ext-cache.outputs.cache-hit != 'true'\n      run: |\n        python setup.py build_libpg_query\n\n    # Build extensions\n\n    - name: Handle Cython extensions build cache\n      uses: actions/cache@v4\n      if: steps.ext-cache.outputs.cache-hit != 'true'\n      with:\n        path: ${{ env.BUILD_TEMP }}/edb\n        key: edb-ext-build-v4-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Build Cython extensions\n      env:\n        CACHE_HIT: ${{ steps.ext-cache.outputs.cache-hit }}\n        BUILD_EXT_MODE: py-only\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/extensions\n          rsync -av ./build/extensions/ ${BUILD_LIB}/\n          BUILD_EXT_MODE=py-only python setup.py -v build_ext\n          rsync -av ${BUILD_LIB}/ ./build/extensions/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/extensions/edb/ ./edb/\n\n    # Build parsers\n\n    - name: Handle compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n        restore-keys: |\n          edb-parsers-v3-\n\n    - name: Build parsers\n      env:\n        CACHE_HIT: ${{ steps.parsers-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" != \"true\" ]]; then\n          rm -rf ${BUILD_LIB}\n          mkdir -p ./build/lib\n          rsync -av ./build/lib/ ${BUILD_LIB}/\n          python setup.py -v build_parsers\n          rsync -av ${BUILD_LIB}/ ./build/lib/\n          rm -rf ${BUILD_LIB}\n        fi\n        rsync -av ./build/lib/edb/ ./edb/\n\n    # Build PostgreSQL\n\n    - name: Build PostgreSQL\n      env:\n        CACHE_HIT: ${{ steps.postgres-cache.outputs.cache-hit }}\n      run: |\n        if [[ \"$CACHE_HIT\" == \"true\" ]]; then\n          cp build/postgres/install/stamp build/postgres/\n        else\n          python setup.py build_postgres\n          cp build/postgres/stamp build/postgres/install/\n        fi\n\n    # Build Stolon\n\n    - name: Set up Go\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      uses: actions/setup-go@v2\n      with:\n        go-version: 1.16\n\n    - uses: actions/checkout@v4\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      with:\n        repository: edgedb/stolon\n        path: build/stolon\n        ref: ${{ env.STOLON_GIT_REV }}\n        fetch-depth: 0\n        submodules: false\n\n    - name: Build Stolon\n      if: steps.stolon-cache.outputs.cache-hit != 'true'\n      run: |\n        mkdir -p build/stolon/bin/\n        curl -fsSL https://releases.hashicorp.com/consul/1.10.1/consul_1.10.1_linux_amd64.zip | zcat > build/stolon/bin/consul\n        chmod +x build/stolon/bin/consul\n        cd build/stolon && make\n\n    # Install edgedb-server and populate egg-info\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n\n    # Refresh the bootstrap cache\n\n    - name: Handle bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n        restore-keys: |\n          edb-bootstrap-v2-\n\n    - name: Bootstrap EdgeDB Server\n      if: steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        edb server --bootstrap-only\n<%- endmacro %>\n\n<% macro calc_cache_key() -%>\n    mkdir -p shared-artifacts\n    if [ \"$(uname)\" = \"Darwin\" ]; then\n      find /usr/lib -type f -name 'lib*' -exec stat -f '%N %z' {} + | sort | shasum -a 256 | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n    else\n      find /usr/lib -type f -name 'lib*' -printf '%P %s\\n' | sort | sha256sum | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt\n    fi\n    python setup.py -q ci_helper --type rust >shared-artifacts/rust_cache_key.txt\n    python setup.py -q ci_helper --type ext >shared-artifacts/ext_cache_key.txt\n    python setup.py -q ci_helper --type parsers >shared-artifacts/parsers_cache_key.txt\n    python setup.py -q ci_helper --type postgres >shared-artifacts/postgres_git_rev.txt\n    python setup.py -q ci_helper --type libpg_query >shared-artifacts/libpg_query_git_rev.txt\n    echo 'f8cd94309eaccbfba5dea7835b88c78377608a37' >shared-artifacts/stolon_git_rev.txt\n    python setup.py -q ci_helper --type bootstrap >shared-artifacts/bootstrap_cache_key.txt\n    echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n    echo LIBPG_QUERY_GIT_REV=$(cat shared-artifacts/libpg_query_git_rev.txt) >> $GITHUB_ENV\n    echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n    echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n    echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n<%- endmacro %>\n\n<% macro install_python_requirements() %>\n    - name: Download requirements.txt\n      uses: actions/cache@v4\n      with:\n        path: requirements.txt\n        key: edb-requirements-${{ hashFiles('pyproject.toml') }}\n\n    - name: Install Python dependencies\n      run: |\n        python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt\n        # 80.9.0 breaks our sphinx, and it keeps sneaking in\n        uv pip install setuptools==80.8.0\n\n<%- endmacro %>\n\n<% macro restore_cache(ref=\"\") %>\n    << init(ref) >>\n    << install_python_requirements() >>\n\n    # Restore the artifacts and environment variables\n\n    - name: Download shared artifacts\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Set environment variables\n      run: |\n        echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV\n        echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV\n        echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    # Restore build cache\n\n    - name: Restore cached Rust extensions\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: build/rust_extensions\n        key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Restore cached Cython extensions\n      uses: actions/cache@v4\n      id: ext-cache\n      with:\n        path: build/extensions\n        key: edb-ext-v6-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }}\n\n    - name: Restore compiled parsers cache\n      uses: actions/cache@v4\n      id: parsers-cache\n      with:\n        path: build/lib\n        key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }}\n\n    - name: Restore cached PostgreSQL build\n      uses: actions/cache@v4\n      id: postgres-cache\n      with:\n        path: build/postgres/install\n        key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }}\n\n    - name: Restore cached Stolon build\n      uses: actions/cache@v4\n      id: stolon-cache\n      with:\n        path: build/stolon/bin\n        key: edb-stolon-v2-${{ env.STOLON_GIT_REV }}\n\n    - name: Restore bootstrap cache\n      uses: actions/cache@v4\n      id: bootstrap-cache\n      with:\n        path: build/cache\n        key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }}\n\n    - name: Stop if we cannot retrieve the cache\n      if: |\n        steps.rust-cache.outputs.cache-hit != 'true' ||\n        steps.ext-cache.outputs.cache-hit != 'true' ||\n        steps.parsers-cache.outputs.cache-hit != 'true' ||\n        steps.postgres-cache.outputs.cache-hit != 'true' ||\n        steps.stolon-cache.outputs.cache-hit != 'true' ||\n        steps.bootstrap-cache.outputs.cache-hit != 'true'\n      run: |\n        echo ::error::Cannot retrieve build cache.\n        exit 1\n\n    - name: Validate cached binaries\n      run: |\n        # Validate Stolon\n        ./build/stolon/bin/stolon-sentinel --version || exit 1\n        ./build/stolon/bin/stolon-keeper --version || exit 1\n        ./build/stolon/bin/stolon-proxy --version || exit 1\n\n        # Validate PostgreSQL\n        ./build/postgres/install/bin/postgres --version || exit 1\n        ./build/postgres/install/bin/pg_config --version || exit 1\n\n    - name: Restore cache into the source tree\n      run: |\n        rsync -av ./build/rust_extensions/edb/ ./edb/\n        rsync -av ./build/extensions/edb/ ./edb/\n        rsync -av ./build/lib/edb/ ./edb/\n        cp build/postgres/install/stamp build/postgres/\n\n    - name: Install edgedb-server\n      env:\n        BUILD_EXT_MODE: skip\n      run: |\n        # --no-build-isolation because we have explicitly installed all deps\n        # and don't want them to be reinstalled in an \"isolated env\".\n        pip install --no-build-isolation --no-deps -e .[test,docs]\n<%- endmacro %>\n\n<% macro setup_terraform() -%>\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n\n    - name: Setup Terraform\n      uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1  # v2.0.3\n\n    - name: Initialize Terraform\n      run: terraform init\n<%- endmacro %>\n"
  },
  {
    "path": ".github/workflows.src/tests.inplace.targets.yml",
    "content": "data:\n"
  },
  {
    "path": ".github/workflows.src/tests.inplace.tpl.yml",
    "content": "<% from \"tests.inc.yml\" import build, calc_cache_key, restore_cache -%>\n\nname: Tests of in-place upgrades and patching\n\non:\n  schedule:\n    - cron: \"0 3 * * *\"\n  workflow_dispatch:\n    inputs: {}\n  push:\n    branches:\n      - \"A-inplace*\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    <%- call build() -%>\n    - name: Compute cache keys\n      run: |\n        << calc_cache_key()|indent >>\n    <%- endcall %>\n\n  test-inplace:\n    runs-on: ubuntu-latest\n    needs: build\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - flags:\n            tests:\n          - flags: --rollback-and-test\n            tests:\n          # Do the reapply test on a smaller selection of tests, since\n          # it is slower.\n          - flags: --rollback-and-reapply\n            tests: -k test_link_on_target_delete -k test_edgeql_select -k test_dump\n\n    steps:\n    <<- restore_cache() >>\n\n    # Run the test\n    # TODO: Would it be better to split this up into multiple jobs?\n    - name: Test performing in-place upgrades\n      run: |\n        ./tests/inplace-testing/test.sh ${{ matrix.flags }} vt ${{ matrix.tests }}\n\n  test-patches:\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n    <<- restore_cache() >>\n\n    - name: Test performing in-place upgrades\n      run: |\n        ./tests/patch-testing/test.sh test-dir -k test_link_on_target_delete -k test_edgeql_select -k test_edgeql_scope -k test_dump\n\n  compute-versions:\n    runs-on: ubuntu-latest\n    outputs:\n      matrix: ${{ steps.set-matrix.outputs.matrix }}\n    steps:\n    - uses: actions/checkout@v4\n    - id: set-matrix\n      name: Compute versions to run on\n      run: python3 .github/scripts/patches/compute-ipu-versions.py\n\n\n  test:\n    runs-on: ubuntu-latest\n    needs: [build, compute-versions]\n    strategy:\n      fail-fast: false\n      matrix: ${{fromJSON(needs.compute-versions.outputs.matrix)}}\n\n    steps:\n    <<- restore_cache() >>\n\n    # Run the test\n\n    - name: Download an earlier database version\n      run: |\n        wget -q \"${{ matrix.edgedb-url }}\"\n        tar xzf ${{ matrix.edgedb-basename }}-${{ matrix.edgedb-version }}.tar.gz\n\n    - name: Make sure a CLI named \"edgedb\" exists (sigh)\n      run: |\n        ln -s gel $(dirname $(which gel))/edgedb\n\n    - name: Test inplace upgrades from previous major version\n      run: |\n        ./tests/inplace-testing/test-old.sh vt ${{ matrix.edgedb-basename }}-${{ matrix.edgedb-version }}\n\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n    needs:\n      - build\n      - test-inplace\n      - test-patches\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows.src/tests.inplace7x.targets.yml",
    "content": "data:\n"
  },
  {
    "path": ".github/workflows.src/tests.inplace7x.tpl.yml",
    "content": "<% from \"tests.inc.yml\" import build, calc_cache_key, restore_cache -%>\n\nname: Tests of in-place upgrades to 7.x\n\non:\n  schedule:\n    - cron: \"0 3 * * *\"\n  workflow_dispatch:\n    inputs: {}\n  push:\n    branches:\n      - \"A-inplace*\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    <%- call build(\"release/7.x\") -%>\n    - name: Compute cache keys\n      run: |\n        << calc_cache_key()|indent >>\n    <%- endcall %>\n\n  test-inplace:\n    runs-on: ubuntu-latest\n    needs: build\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - flags:\n            tests:\n          - flags: --rollback-and-test\n            tests:\n          # Do the reapply test on a smaller selection of tests, since\n          # it is slower.\n          - flags: --rollback-and-reapply\n            tests: -k test_link_on_target_delete -k test_edgeql_select -k test_dump\n\n    steps:\n    <<- restore_cache(\"release/7.x\") >>\n\n    # Run the test\n    # TODO: Would it be better to split this up into multiple jobs?\n    - name: Test performing in-place upgrades\n      run: |\n        ./tests/inplace-testing/test.sh ${{ matrix.flags }} vt ${{ matrix.tests }}\n\n  compute-versions:\n    runs-on: ubuntu-latest\n    outputs:\n      matrix: ${{ steps.set-matrix.outputs.matrix }}\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        submodules: false\n        ref: release/7.x\n    - id: set-matrix\n      name: Compute versions to run on\n      run: python3 .github/scripts/patches/compute-ipu-versions.py\n\n\n  test:\n    runs-on: ubuntu-latest\n    needs: [build, compute-versions]\n    strategy:\n      fail-fast: false\n      matrix: ${{fromJSON(needs.compute-versions.outputs.matrix)}}\n\n    steps:\n    <<- restore_cache(\"release/7.x\") >>\n\n    # Run the test\n\n    - name: Download an earlier database version\n      run: |\n        wget -q \"${{ matrix.edgedb-url }}\"\n        tar xzf ${{ matrix.edgedb-basename }}-${{ matrix.edgedb-version }}.tar.gz\n\n    - name: Make sure a CLI named \"edgedb\" exists (sigh)\n      run: |\n        ln -s gel $(dirname $(which gel))/edgedb\n\n    - name: Test inplace upgrades from previous major version\n      run: |\n        ./tests/inplace-testing/test-old.sh vt ${{ matrix.edgedb-basename }}-${{ matrix.edgedb-version }}\n\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n    needs:\n      - build\n      - test-inplace\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows.src/tests.managed-pg.targets.yml",
    "content": "data:\n"
  },
  {
    "path": ".github/workflows.src/tests.managed-pg.tpl.yml",
    "content": "<% from \"tests.inc.yml\" import build, calc_cache_key, restore_cache, setup_terraform -%>\n\n<% macro setup_aws_creds() -%>\n    - name: Configure AWS Credentials\n      uses: aws-actions/configure-aws-credentials@v1\n      with:\n        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n        aws-region: us-east-2\n<%- endmacro -%>\n\n<% macro setup_gcp_creds() -%>\n    - name: Configure GCP Credentials\n      uses: google-github-actions/setup-gcloud@main\n      with:\n        service_account_key: ${{ secrets.GCP_SA_KEY }}\n        export_default_credentials: true\n<%- endmacro -%>\n\nname: Tests on Managed PostgreSQL\n\non:\n  schedule:\n    - cron: \"0 3 * * 6\"\n  workflow_dispatch:\n    inputs: {}\n  push:\n    branches:\n      - cloud-test\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    <%- call build() -%>\n    - name: Compute cache keys\n      run: |\n        << calc_cache_key()|indent >>\n    <%- endcall %>\n\n\n  setup-aws-rds:\n    runs-on: ubuntu-latest\n    outputs:\n      pghost: ${{ steps.pghost.outputs.stdout }}\n    defaults:\n      run:\n        working-directory: .github/aws-rds\n    steps:\n      << setup_terraform()|indent(2) >>\n\n      << setup_aws_creds()|indent(2) >>\n\n      - name: Setup AWS RDS\n        env:\n          TF_VAR_sg_id: ${{ secrets.AWS_SECURITY_GROUP }}\n          TF_VAR_password: ${{ secrets.AWS_RDS_PASSWORD }}\n        run: |\n          terraform apply -auto-approve\n\n      - name: Store Terraform state\n        if: ${{ always() }}\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: aws-rds-tfstate\n          path: .github/aws-rds/terraform.tfstate\n          retention-days: 1\n\n      - name: Get RDS host\n        id: pghost\n        run: |\n          terraform output -raw db_instance_address\n\n  test-aws-rds:\n    runs-on: ubuntu-latest\n    needs: [setup-aws-rds, build]\n    steps:\n    <<- restore_cache() >>\n\n    # Run the test\n\n    - name: Test\n      env:\n        EDGEDB_TEST_BACKEND_DSN: postgres://edbtest:${{ secrets.AWS_RDS_PASSWORD }}@${{ needs.setup-aws-rds.outputs.pghost }}/postgres\n      run: |\n        edb server --bootstrap-only --backend-dsn=$EDGEDB_TEST_BACKEND_DSN --testmode\n        edb test -j2 -v --backend-dsn=$EDGEDB_TEST_BACKEND_DSN\n\n  teardown-aws-rds:\n    runs-on: ubuntu-latest\n    needs: test-aws-rds\n    if: ${{ always() }}\n    defaults:\n      run:\n        working-directory: .github/aws-rds\n    steps:\n      << setup_terraform()|indent(2) >>\n\n      << setup_aws_creds()|indent(2) >>\n\n      - name: Restore Terraform state\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n        with:\n          name: aws-rds-tfstate\n          path: .github/aws-rds\n\n      - name: Destroy AWS RDS\n        run: terraform destroy -auto-approve\n        env:\n          TF_VAR_sg_id: ${{ secrets.AWS_SECURITY_GROUP }}\n          TF_VAR_password: ${{ secrets.AWS_RDS_PASSWORD }}\n\n      - name: Overwrite Terraform state\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: aws-rds-tfstate\n          path: .github/aws-rds/terraform.tfstate\n          retention-days: 1\n\n\n  setup-do-database:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: .github/do-database\n    steps:\n      << setup_terraform()|indent(2) >>\n\n      - name: Setup DigitalOcean Database\n        env:\n          TF_VAR_do_token: ${{ secrets.DIGITALOCEAN_TOKEN }}\n        run: |\n          terraform apply -auto-approve\n\n      - name: Store Terraform state\n        if: ${{ always() }}\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: do-database-tfstate\n          path: .github/do-database/terraform.tfstate\n          retention-days: 1\n\n  test-do-database:\n    runs-on: ubuntu-latest\n    needs: [setup-do-database, build]\n    steps:\n    <<- restore_cache() >>\n\n    - name: Setup Terraform\n      uses: hashicorp/setup-terraform@v1\n\n    - name: Initialize Terraform\n      working-directory: .github/do-database\n      run: terraform init\n\n    - name: Restore Terraform state\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: do-database-tfstate\n        path: .github/do-database\n\n    - name: Get Database host\n      id: pghost\n      working-directory: .github/do-database\n      run: |\n        terraform output -raw db_instance_address\n\n    - name: Get Database port\n      id: pgport\n      working-directory: .github/do-database\n      run: |\n        terraform output -raw db_instance_port\n\n    - name: Get Database user\n      id: pguser\n      working-directory: .github/do-database\n      run: |\n        terraform output -raw db_instance_user\n\n    - name: Get Database password\n      id: pgpass\n      working-directory: .github/do-database\n      run: |\n        terraform output -raw db_instance_password\n\n    - name: Get Database dbname\n      id: pgdatabase\n      working-directory: .github/do-database\n      run: |\n        terraform output -raw db_instance_database\n\n    # Run the test\n\n    - name: Test\n      env:\n        EDGEDB_TEST_BACKEND_DSN: postgres://${{ steps.pguser.outputs.stdout }}:${{ steps.pgpass.outputs.stdout }}@${{ steps.pghost.outputs.stdout }}:${{ steps.pgport.outputs.stdout }}/${{ steps.pgdatabase.outputs.stdout }}\n      run: |\n        edb server --bootstrap-only --backend-dsn=$EDGEDB_TEST_BACKEND_DSN --testmode\n        edb test -j2 -v --backend-dsn=$EDGEDB_TEST_BACKEND_DSN\n\n  teardown-do-database:\n    runs-on: ubuntu-latest\n    needs: test-do-database\n    if: ${{ always() }}\n    defaults:\n      run:\n        working-directory: .github/do-database\n    steps:\n      << setup_terraform()|indent(2) >>\n\n      - name: Restore Terraform state\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n        with:\n          name: do-database-tfstate\n          path: .github/do-database\n\n      - name: Destroy DigitalOcean Database\n        run: terraform destroy -auto-approve\n        env:\n          TF_VAR_do_token: ${{ secrets.DIGITALOCEAN_TOKEN }}\n\n      - name: Overwrite Terraform state\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: do-database-tfstate\n          path: .github/do-database/terraform.tfstate\n          retention-days: 1\n\n\n  setup-gcp-cloud-sql:\n    runs-on: ubuntu-latest\n    outputs:\n      pghost: ${{ steps.pghost.outputs.stdout }}\n    defaults:\n      run:\n        working-directory: .github/gcp-cloud-sql\n    steps:\n      << setup_terraform()|indent(2) >>\n\n      << setup_gcp_creds()|indent(2) >>\n\n      - name: Setup GCP Cloud SQL\n        env:\n          TF_VAR_password: ${{ secrets.AWS_RDS_PASSWORD }}\n        run: |\n          terraform apply -auto-approve\n\n      - name: Store Terraform state\n        if: ${{ always() }}\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: gcp-cloud-sql-tfstate\n          path: .github/gcp-cloud-sql/terraform.tfstate\n          retention-days: 1\n\n      - name: Get Cloud SQL host\n        id: pghost\n        run: |\n          terraform output -raw db_instance_address\n\n  test-gcp-cloud-sql:\n    runs-on: ubuntu-latest\n    needs: [setup-gcp-cloud-sql, build]\n    steps:\n    <<- restore_cache() >>\n\n    # Run the test\n\n    - name: Test\n      env:\n        EDGEDB_TEST_BACKEND_DSN: postgres://postgres:${{ secrets.AWS_RDS_PASSWORD }}@${{ needs.setup-gcp-cloud-sql.outputs.pghost }}/postgres\n      run: |\n        edb server --bootstrap-only --backend-dsn=$EDGEDB_TEST_BACKEND_DSN --testmode\n        edb test -j2 -v --backend-dsn=$EDGEDB_TEST_BACKEND_DSN\n\n  teardown-gcp-cloud-sql:\n    runs-on: ubuntu-latest\n    needs: test-gcp-cloud-sql\n    if: ${{ always() }}\n    defaults:\n      run:\n        working-directory: .github/gcp-cloud-sql\n    steps:\n      << setup_terraform()|indent(2) >>\n\n      << setup_gcp_creds()|indent(2) >>\n\n      - name: Restore Terraform state\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n        with:\n          name: gcp-cloud-sql-tfstate\n          path: .github/gcp-cloud-sql\n\n      - name: Destroy GCP Cloud SQL\n        run: terraform destroy -auto-approve\n        env:\n          TF_VAR_password: ${{ secrets.AWS_RDS_PASSWORD }}\n\n      - name: Overwrite Terraform state\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: gcp-cloud-sql-tfstate\n          path: .github/gcp-cloud-sql/terraform.tfstate\n          retention-days: 1\n\n\n  setup-aws-aurora:\n    runs-on: ubuntu-latest\n    outputs:\n      pghost: ${{ steps.pghost.outputs.stdout }}\n    defaults:\n      run:\n        working-directory: .github/aws-aurora\n    steps:\n      << setup_terraform()|indent(2) >>\n\n      << setup_aws_creds()|indent(2) >>\n\n      - name: Setup AWS RDS Aurora\n        env:\n          TF_VAR_sg_id: ${{ secrets.AWS_SECURITY_GROUP }}\n          TF_VAR_password: ${{ secrets.AWS_RDS_PASSWORD }}\n          TF_VAR_vpc_id: ${{ secrets.AWS_VPC_ID }}\n        run: |\n          terraform apply -auto-approve\n\n      - name: Store Terraform state\n        if: ${{ always() }}\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: aws-aurora-tfstate\n          path: .github/aws-aurora/terraform.tfstate\n          retention-days: 1\n\n      - name: Get RDS Aurora host\n        id: pghost\n        run: |\n          terraform output -raw rds_cluster_endpoint\n\n  test-aws-aurora:\n    runs-on: ubuntu-latest\n    needs: [setup-aws-aurora, build]\n    steps:\n    <<- restore_cache() >>\n\n    # Run the test\n\n    - name: Test\n      env:\n        EDGEDB_TEST_BACKEND_DSN: postgres://edbtest:${{ secrets.AWS_RDS_PASSWORD }}@${{ needs.setup-aws-aurora.outputs.pghost }}/postgres\n      run: |\n        edb server --bootstrap-only --backend-dsn=$EDGEDB_TEST_BACKEND_DSN --testmode\n        edb test -j1 -v --backend-dsn=$EDGEDB_TEST_BACKEND_DSN\n\n  teardown-aws-aurora:\n    runs-on: ubuntu-latest\n    needs: test-aws-aurora\n    if: ${{ always() }}\n    defaults:\n      run:\n        working-directory: .github/aws-aurora\n    steps:\n      << setup_terraform()|indent(2) >>\n\n      << setup_aws_creds()|indent(2) >>\n\n      - name: Restore Terraform state\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n        with:\n          name: aws-aurora-tfstate\n          path: .github/aws-aurora\n\n      - name: Destroy AWS RDS Aurora\n        run: terraform destroy -auto-approve\n        env:\n          TF_VAR_sg_id: ${{ secrets.AWS_SECURITY_GROUP }}\n          TF_VAR_password: ${{ secrets.AWS_RDS_PASSWORD }}\n          TF_VAR_vpc_id: ${{ secrets.AWS_VPC_ID }}\n\n      - name: Overwrite Terraform state\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: aws-aurora-tfstate\n          path: .github/aws-aurora/terraform.tfstate\n          retention-days: 1\n\n\n  setup-heroku-postgres:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: .github/heroku-postgres\n    steps:\n      << setup_terraform()|indent(2) >>\n\n      - name: Setup Heroku Postgres\n        env:\n          HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}\n          HEROKU_EMAIL: ${{ secrets.HEROKU_EMAIL }}\n        run: |\n          terraform apply -auto-approve\n\n      - name: Store Terraform state\n        if: ${{ always() }}\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: heroku-postgres-tfstate\n          path: .github/heroku-postgres/terraform.tfstate\n          retention-days: 1\n\n  test-heroku-postgres:\n    runs-on: ubuntu-latest\n    needs: [setup-heroku-postgres, build]\n    steps:\n    <<- restore_cache() >>\n\n    - name: Setup Terraform\n      uses: hashicorp/setup-terraform@v1\n\n    - name: Initialize Terraform\n      working-directory: .github/heroku-postgres\n      run: terraform init\n\n    - name: Restore Terraform state\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: heroku-postgres-tfstate\n        path: .github/heroku-postgres\n\n    - name: Get Heroku Postgres DSN\n      id: pgdsn\n      working-directory: .github/heroku-postgres\n      run: |\n        terraform output -raw heroku_postgres_dsn\n\n    # Run the test\n\n    - name: Test\n      env:\n        EDGEDB_TEST_BACKEND_VENDOR: heroku-postgres\n        EDGEDB_TEST_BACKEND_DSN: ${{ steps.pgdsn.outputs.stdout }}\n      run: |\n        edb server --bootstrap-only --backend-dsn=$EDGEDB_TEST_BACKEND_DSN --testmode\n        edb test -j1 -v --backend-dsn=$EDGEDB_TEST_BACKEND_DSN\n\n  teardown-heroku-postgres:\n    runs-on: ubuntu-latest\n    needs: test-heroku-postgres\n    if: ${{ always() }}\n    defaults:\n      run:\n        working-directory: .github/heroku-postgres\n    steps:\n      << setup_terraform()|indent(2) >>\n\n      << setup_aws_creds()|indent(2) >>\n\n      - name: Restore Terraform state\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n        with:\n          name: heroku-postgres-tfstate\n          path: .github/heroku-postgres\n\n      - name: Destroy Heroku Postgres\n        run: terraform destroy -auto-approve\n        env:\n          HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}\n          HEROKU_EMAIL: ${{ secrets.HEROKU_EMAIL }}\n\n      - name: Overwrite Terraform state\n        uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n        with:\n          name: heroku-postgres-tfstate\n          path: .github/heroku-postgres/terraform.tfstate\n          retention-days: 1\n\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n    needs:\n      - setup-aws-rds\n      - test-aws-rds\n      - teardown-aws-rds\n      - setup-do-database\n      - test-do-database\n      - teardown-do-database\n      - setup-gcp-cloud-sql\n      - test-gcp-cloud-sql\n      - teardown-gcp-cloud-sql\n      - setup-aws-aurora\n      - test-aws-aurora\n      - teardown-aws-aurora\n      - setup-heroku-postgres\n      - test-heroku-postgres\n      - teardown-heroku-postgres\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows.src/tests.patches.targets.yml",
    "content": "data:\n"
  },
  {
    "path": ".github/workflows.src/tests.patches.tpl.yml",
    "content": "<% from \"tests.inc.yml\" import build, calc_cache_key, restore_cache -%>\n\nname: Tests of patching old EdgeDB Versions\n\non:\n  workflow_dispatch:\n    inputs: {}\n  pull_request:\n    branches:\n      - release/*\n  push:\n    branches:\n      - patch-test*\n      - release/*\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    <%- call build() -%>\n    - name: Compute cache keys\n      run: |\n        << calc_cache_key()|indent >>\n    <%- endcall %>\n\n  compute-versions:\n    runs-on: ubuntu-latest\n    outputs:\n      matrix: ${{ steps.set-matrix.outputs.matrix }}\n    steps:\n    - uses: actions/checkout@v4\n    - id: set-matrix\n      name: Compute versions to run on\n      run: python3 .github/scripts/patches/compute-versions.py\n\n  test:\n    runs-on: ubuntu-latest\n    needs: [build, compute-versions]\n    strategy:\n      fail-fast: false\n      matrix: ${{fromJSON(needs.compute-versions.outputs.matrix)}}\n\n    steps:\n    <<- restore_cache() >>\n\n    # Run the test\n\n    - name: Download an earlier database version and set up a instance\n      run: |\n        wget -q \"${{ matrix.edgedb-url }}\"\n        tar xzf ${{ matrix.edgedb-basename }}-${{ matrix.edgedb-version }}.tar.gz\n        ${{ matrix.edgedb-basename }}-${{ matrix.edgedb-version }}/bin/edgedb-server -D test-dir --bootstrap-only --testmode\n\n    - name: Create databases on the older version\n      if: ${{ matrix.make-dbs }}\n      run: python3 .github/scripts/patches/create-databases.py ${{ matrix.edgedb-basename }}-${{ matrix.edgedb-version }}/bin/edgedb-server\n\n    - name: Run tests with instance created on an older version\n      run: |\n        # Run the server explicitly first to do the upgrade, since edb test\n        # has timeouts.\n        edb server --bootstrap-only --data-dir test-dir\n        # Should we run *all* the tests?\n        edb test -j2 -v --data-dir test-dir tests/test_edgeql_json.py tests/test_edgeql_casts.py tests/test_edgeql_functions.py tests/test_edgeql_expressions.py tests/test_edgeql_policies.py tests/test_edgeql_vector.py tests/test_edgeql_scope.py tests/test_http_ext_auth.py\n\n    - name: Test downgrading a database after an upgrade\n      if: ${{ !contains(matrix.edgedb-version, '-rc') && !contains(matrix.edgedb-version, '-beta') }}\n      env:\n        EDGEDB_VERSION: ${{ matrix.edgedb-version }}\n      run: python3 .github/scripts/patches/test-downgrade.py\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n    needs:\n      - build\n      - compute-versions\n      - test\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows.src/tests.pg-versions.targets.yml",
    "content": "data:\n"
  },
  {
    "path": ".github/workflows.src/tests.pg-versions.tpl.yml",
    "content": "<% from \"tests.inc.yml\" import build, calc_cache_key, restore_cache, setup_terraform -%>\n\nname: Tests on PostgreSQL Versions\n\non:\n  schedule:\n    - cron: \"0 3 * * *\"\n  workflow_dispatch:\n    inputs: {}\n  push:\n    branches:\n      - pg-test\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    <%- call build() -%>\n    - name: Compute cache keys\n      run: |\n        << calc_cache_key()|indent >>\n    <%- endcall %>\n\n  test:\n    runs-on: ubuntu-latest\n    needs: build\n    strategy:\n      fail-fast: false\n      matrix:\n        postgres-version: [ 17 ]\n        single-mode:\n         - ''\n         # These are very broken. Disabling them for now until we\n         # decide whether to fix them or give up.\n         # - 'NOCREATEDB NOCREATEROLE'\n         # - 'CREATEDB NOCREATEROLE'\n        multi-tenant-mode: [ '' ]\n        include:\n          - postgres-version: 14\n            single-mode: ''\n            multi-tenant-mode: ''\n          - postgres-version: 15\n            single-mode: ''\n            multi-tenant-mode: ''\n          - postgres-version: 16\n            single-mode: ''\n            multi-tenant-mode: ''\n          - postgres-version: 17\n            single-mode: ''\n            multi-tenant-mode: 'remote-compiler'\n          - postgres-version: 17\n            single-mode: ''\n            multi-tenant-mode: 'multi-tenant'\n    services:\n      postgres:\n        image: pgvector/pgvector:0.7.4-pg${{ matrix.postgres-version }}\n        env:\n          POSTGRES_PASSWORD: postgres\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n          --name postgres\n        ports:\n          - 5432:5432\n\n    steps:\n    - name: Trust pgvector extension\n      uses: docker://docker\n      with:\n        args: docker exec postgres sed -i $a\\trusted=true /usr/share/postgresql/${{ matrix.postgres-version }}/extension/vector.control\n\n    <<- restore_cache() >>\n\n    # Run the test\n\n    - name: Setup single mode role and database\n      if: ${{ matrix.single-mode }}\n      shell: python\n      run: |\n        import asyncio\n        import subprocess\n\n        from edb.server.pgcluster import get_pg_bin_dir\n\n        async def main():\n            psql = await get_pg_bin_dir() / \"psql\"\n            dsn = \"postgres://postgres:postgres@localhost/postgres\"\n\n            script = \"\"\"\\\n                CREATE ROLE singles;\n                ALTER ROLE singles WITH LOGIN PASSWORD 'test' NOSUPERUSER\n                  ${{ matrix.single-mode }};\n                CREATE DATABASE singles OWNER singles;\n                REVOKE ALL ON DATABASE singles FROM PUBLIC;\n                GRANT CONNECT ON DATABASE singles TO singles;\n                GRANT ALL ON DATABASE singles TO singles;\n            \"\"\"\n\n            subprocess.run(\n                [str(psql), dsn],\n                check=True,\n                text=True,\n                input=script,\n            )\n\n        asyncio.run(main())\n\n    - name: Test\n      env:\n        EDGEDB_TEST_POSTGRES_VERSION: ${{ matrix.postgres-version }}\n      run: |\n        if [[ \"${{ matrix.single-mode }}\" ]]; then\n          export EDGEDB_TEST_BACKEND_DSN=postgres://singles:test@localhost/singles\n        else\n          export EDGEDB_TEST_BACKEND_DSN=postgres://postgres:postgres@localhost/postgres\n        fi\n        if [[ \"${{ matrix.multi-tenant-mode }}\" == \"remote-compiler\" ]]; then\n          export EDGEDB_TEST_REMOTE_COMPILER=localhost:5660\n          export _EDGEDB_SERVER_COMPILER_POOL_SECRET=secret\n          __EDGEDB_DEVMODE=1 edgedb-server compiler --pool-size 2 &\n        fi\n        edb server --bootstrap-only --backend-dsn=$EDGEDB_TEST_BACKEND_DSN --testmode\n        if [[ \"${{ matrix.multi-tenant-mode }}\" == \"multi-tenant\" ]]; then\n          export EDGEDB_SERVER_MULTITENANT_CONFIG_FILE=/tmp/edb.mt.json\n          echo \"{\\\"localhost\\\":{\\\"instance-name\\\":\\\"localtest\\\",\\\"backend-dsn\\\":\\\"$EDGEDB_TEST_BACKEND_DSN\\\",\\\"admin\\\":true,\\\"max-backend-connections\\\":10}}\" > /tmp/edb.mt.json\n        fi\n        if [[ \"${{ matrix.single-mode }}\" == *\"NOCREATEDB\"* ]]; then\n          edb test -j1 -v --backend-dsn=$EDGEDB_TEST_BACKEND_DSN\n        else\n          edb test -j2 -v --backend-dsn=$EDGEDB_TEST_BACKEND_DSN\n        fi\n\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n    needs:\n      - build\n      - test\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows.src/tests.pool.targets.yml",
    "content": "data:\n"
  },
  {
    "path": ".github/workflows.src/tests.pool.tpl.yml",
    "content": "<% from \"tests.inc.yml\" import build, calc_cache_key -%>\n\nname: Pool Simulation Test\n\non:\n  push:\n    branches:\n      - master\n      - pool-test\n    paths:\n      - 'edb/server/connpool/**'\n      - 'edb/server/conn_pool/**'\n      - 'tests/test_server_pool.py'\n      - '.github/workflows/tests-pool.yml'\n  pull_request:\n    branches:\n      - master\n    paths:\n      - 'edb/server/connpool/**'\n      - 'edb/server/conn_pool/**'\n      - 'tests/test_server_pool.py'\n      - '.github/workflows/tests-pool.yml'\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    concurrency: pool-test\n    steps:\n    <%- call build() -%>\n    - name: Compute cache keys\n      run: |\n        << calc_cache_key()|indent >>\n    <%- endcall %>\n\n    - uses: actions/checkout@v4\n      if: startsWith(github.ref, 'refs/heads')\n      with:\n        repository: edgedb/edgedb-pool-simulation\n        path: pool-simulation\n        token: ${{ secrets.GITHUB_CI_BOT_TOKEN }}\n\n    - name: Run the pool simulation test\n      env:\n        PYTHONPATH: .\n        SIMULATION_CI: yes\n        TIME_SCALE: 10\n      run: |\n        mkdir -p pool-simulation/reports\n        python tests/test_server_pool.py\n\n    - uses: EndBug/add-and-commit@v7.0.0\n      if: ${{ always() }}\n      continue-on-error: true\n      with:\n        branch: main\n        cwd: pool-simulation\n        author_name: github-actions\n        author_email: 41898282+github-actions[bot]@users.noreply.github.com\n"
  },
  {
    "path": ".github/workflows.src/tests.reflection.targets.yml",
    "content": "data:\n"
  },
  {
    "path": ".github/workflows.src/tests.reflection.tpl.yml",
    "content": "<% from \"tests.inc.yml\" import build, calc_cache_key, restore_cache -%>\nname: Tests with reflection validation\n\non:\n  schedule:\n    - cron: \"0 3 * * *\"\n  workflow_dispatch:\n    inputs: {}\n  push:\n    branches:\n      - \"REFL-*\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    <%- call build() -%>\n\n    - name: Compute cache keys\n      env:\n        GIST_TOKEN: ${{ secrets.CI_BOT_GIST_TOKEN }}\n      run: |\n        << calc_cache_key()|indent >>\n    <%- endcall %>\n\n  test:\n    needs: build\n    runs-on: ubuntu-latest\n\n    steps:\n    <<- restore_cache() >>\n\n    # Run the test\n\n    - name: Test\n      env:\n        EDGEDB_DEBUG_DELTA_VALIDATE_REFLECTION: 1\n      run: |\n        edb test -j2 -v\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n    needs:\n      - build\n      - test\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".github/workflows.src/tests.targets.yml",
    "content": "data:\n"
  },
  {
    "path": ".github/workflows.src/tests.tpl.yml",
    "content": "<% from \"tests.inc.yml\" import init, build, calc_cache_key, install_python_requirements, restore_cache -%>\nname: Tests\n\non:\n  push:\n    branches:\n      - master\n      - ci\n      - \"release/*\"\n  pull_request:\n    branches:\n      - '**'\n  schedule:\n    - cron: \"0 */3 * * *\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    <%- call build() -%>\n    - name: Compute cache keys and download the running times log\n      env:\n        GIST_TOKEN: ${{ secrets.CI_BOT_GIST_TOKEN }}\n      run: |\n        << calc_cache_key()|indent >>\n\n        curl \\\n          -H \"Accept: application/vnd.github.v3+json\" \\\n          -u edgedb-ci:$GIST_TOKEN \\\n          https://api.github.com/gists/8b722a65397f7c4c0df72f5394efa04c \\\n        | jq '.files.\"time_stats.csv\".raw_url' \\\n        | xargs curl > shared-artifacts/time_stats.csv\n    <%- endcall %>\n\n  cargo-test:\n    needs: build\n    runs-on: ubuntu-latest\n\n    steps:\n    << init() >>\n    << install_python_requirements() >>\n\n    - name: Download cache key\n      uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n      with:\n        name: shared-artifacts\n        path: shared-artifacts\n\n    - name: Generate environment variables\n      run: |\n        echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV\n\n    - name: Handle Rust extensions build cache\n      uses: actions/cache@v4\n      id: rust-cache\n      with:\n        path: ${{ env.BUILD_TEMP }}/rust/extensions\n        key: edb-rust-build-v1-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }}\n\n    - name: Install Rust toolchain\n      uses: dsherret/rust-toolchain-file@v1\n\n    - name: Cargo test\n      env:\n        CARGO_TARGET_DIR: ${{ env.BUILD_TEMP }}/rust/extensions\n        CARGO_HOME: ${{ env.BUILD_TEMP }}/rust/extensions/cargo_home\n      run:\n        cargo test --all-features\n\n  python-test:\n    needs: build\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        shard: [\n           1,  2,  3,  4,\n           5,  6,  7,  8,\n           9, 10, 11, 12,\n          13, 14, 15, 16,\n        ]\n\n    steps:\n    <<- restore_cache() >>\n\n    # Run the test\n\n    - name: Install Rust toolchain\n      uses: dsherret/rust-toolchain-file@v1\n\n    - name: Test\n      env:\n        SHARD: ${{ matrix.shard }}\n        EDGEDB_TEST_REPEATS: 1\n      run: |\n        mkdir -p results/\n        cp shared-artifacts/time_stats.csv results/running_times_${SHARD}.csv\n        edb test --jobs 2 --verbose --shard ${SHARD}/16 \\\n          --running-times-log=results/running_times_${SHARD}.csv \\\n          --result-log=results/result_${SHARD}.json\n\n    - name: Upload test results\n      uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      if: ${{ always() }}\n      with:\n        name: python-test-results-${{ matrix.shard }}\n        path: results\n        retention-days: 1\n\n  python-test-list:\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n    <<- restore_cache() >>\n\n    # List tests and upload\n\n    - name: Generate complete list of tests for verification\n      env:\n        SHARD: ${{ matrix.shard }}\n        EDGEDB_TEST_REPEATS: 1\n      run: |\n        edb test --list > shared-artifacts/all_tests.txt\n\n    - name: Upload list of tests\n      uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874  # v4.4.0\n      with:\n        name: test-list\n        path: shared-artifacts\n        retention-days: 1\n\n  test-conclusion:\n    needs: [cargo-test, python-test, python-test-list]\n    runs-on: ubuntu-latest\n    if: ${{ always() }}\n    steps:\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.12.2'\n\n      - name: Install Python deps\n        run: |\n          python -m pip install requests click\n\n      - uses: actions/checkout@v4\n        with:\n          submodules: false\n\n      - name: Download python-test results\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n        with:\n          pattern: python-test-results-*\n          merge-multiple: true\n          path: results\n\n      # Render results and exit if they were unsuccessful\n      - name: Render results\n        run: |\n          python edb/tools/test/results.py 'results/result_*.json'\n\n      - name: Download shared artifacts\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n        with:\n          name: shared-artifacts\n          path: shared-artifacts\n\n      - name: Download test list\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16  # v4.1.8\n        with:\n          name: test-list\n          path: shared-artifacts\n\n      - name: Merge stats and verify tests completion\n        shell: python\n        env:\n          GIST_TOKEN: ${{ secrets.CI_BOT_GIST_TOKEN }}\n          GIT_REF: ${{ github.ref }}\n        run: |\n          import csv\n          import glob\n          import io\n          import os\n          import requests\n\n          orig = {}\n          new = {}\n          all_tests = set()\n          with open(\"shared-artifacts/time_stats.csv\") as f:\n              for name, t, c in csv.reader(f):\n                  assert name not in orig, \"duplicate test name in original stats!\"\n                  orig[name] = (t, int(c))\n\n          with open(\"shared-artifacts/all_tests.txt\") as f:\n              for line in f:\n                  assert line not in all_tests, \"duplicate test name in this run!\"\n                  all_tests.add(line.strip())\n\n          for new_file in glob.glob(\"results/running_times_*.csv\"):\n              with open(new_file) as f:\n                  for name, t, c in csv.reader(f):\n                      if int(c) > orig.get(name, (0, 0))[1]:\n                          if name.startswith(\"setup::\"):\n                              new[name] = (t, c)\n                          else:\n                              assert name not in new, f\"duplicate test! {name}\"\n                              new[name] = (t, c)\n                              all_tests.remove(name)\n\n          assert not all_tests, \"Tests not run! \\n\" + \"\\n\".join(all_tests)\n\n          if os.environ[\"GIT_REF\"] == \"refs/heads/master\":\n              buf = io.StringIO()\n              writer = csv.writer(buf)\n              orig.update(new)\n              for k, v in sorted(orig.items()):\n                  writer.writerow((k,) + v)\n\n              resp = requests.patch(\n                  \"https://api.github.com/gists/8b722a65397f7c4c0df72f5394efa04c\",\n                  headers={\"Accept\": \"application/vnd.github.v3+json\"},\n                  auth=(\"edgedb-ci\", os.environ[\"GIST_TOKEN\"]),\n                  json={\"files\": {\"time_stats.csv\": {\"content\": buf.getvalue()}}},\n              )\n              resp.raise_for_status()\n\n\n  workflow-notifications:\n    if: failure() && github.event_name != 'pull_request'\n    name: Notify in Slack on failures\n    needs:\n      - test-conclusion\n    runs-on: ubuntu-latest\n    permissions:\n      actions: 'read'\n    steps:\n      - name: Slack Workflow Notification\n        uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309\n        with:\n          repo_token: ${{secrets.GITHUB_TOKEN}}\n          slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}}\n          name: 'Workflow notifications'\n          icon_emoji: ':hammer:'\n          include_jobs: 'on-failure'\n"
  },
  {
    "path": ".gitignore",
    "content": "*._*\n*.pyc\n*.pyo\n*.o\n*.so\n*.dylib\n.vscode/\n.zed/\n.helix/\n*~\n.#*\n.*.swp\n.DS_Store\n\\#*#\n/test*.py\n/.local\n/perf.data*\n/build\n/target\n/tmp\n__pycache__/\n.d8_history\n/.venv\n/.eggs\n/*.egg\n/*.egg-info\n/dist\n/.cache\ndocs/_build\n/AUTHORS\n/ChangeLog\n/tests/dumps/**/*_dev*.dump\n/edb/_buildmeta.py\n\n/.coverage*\n/htmlcov\n\n*.code-workspace\n/.pytest_cache\n/.mypy_cache\n/.vagga\n/.dmypy.json\n/compile_commands.json\n/pyrightconfig.json\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"postgres\"]\n\tpath = postgres\n\turl = https://github.com/geldata/postgres.git\n\tignore = untracked\n[submodule \"edb/server/pgproto\"]\n\tpath = edb/server/pgproto\n\turl = https://github.com/MagicStack/py-pgproto.git\n[submodule \"edb/pgsql/parser/libpg_query\"]\n\tpath = edb/pgsql/parser/libpg_query\n\turl = https://github.com/geldata/libpg_query.git\n"
  },
  {
    "path": ".mailmap",
    "content": "Elvis Pranskevichus <elvis@edgedb.com> <elvis@magic.io>\nYury Selivanov <yury@edgedb.com> <yury@magic.io>\nYury Selivanov <yury@edgedb.com> Yuri Selivanov <yselivanov@sprymix.com>\nVictor Petrovykh <victor@edgedb.com> <victor@magic.io>\nVictor Petrovykh <victor@edgedb.com> Vicor Petrovykh <victor.petrovykh@gmail.com>\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at coc@edgedb.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "CONTRIBUTING.rst",
    "content": "How to contribute to Gel\n========================\n\nThank you for contributing to Gel! We love our open source community and\nwant to foster a healthy contributor ecosystem.\n\nTo make sure the project can continue to improve quickly, we have a few\nguidelines designed to make it easier for your contributions to make it into\nthe project.\n\nThese are guidelines rather than hard rules. If you want to submit a pull\nrequest that strays from these, it might be a good idea to start a discussion\nabout it first. Otherwise, it's possible your pull request might not be merged.\n\nAll contributions\n-----------------\n\n- **Avoid making pull requests that do not have an associated Github Issue.**\n  This could be an already existing issue or one you create yourself when you\n  discover the problem. This will allow the team to help you scope your\n  solution, warn you of potential gotchas, or give you a heads-up on solutions\n  that are likely not feasible. It's a good idea to mention in the issue that\n  you'd like to contribute code to resolve the issue.  **If you're fixing\n  something trivial like a typo,** an associated issue isn't necessary.\n- **Write good commit messages.** The subject of your commit message — that's\n  the first line — should tell us *what* you did. The body of your message —\n  that's the rest of it — should tell us *why* you did it (unless that's\n  self-evident).\n\nContributing code\n--------------------------\n\n- **Pull requests without thorough testing are not likely to be merged.** If\n  you're not sure if yours is well-tested enough, go ahead and submit. We can\n  help guide you to the finish line.\n\nContributing documentation\n--------------------------\n\n- **Avoid changes that don't fix an obvious mistake or add clarity.** This is\n  subjective, but try to look at your changes with a critical eye. Do they fix\n  errors in the original like misspellings or typos? Do they make existing\n  prose more clear or accessible while maintaining accuracy? If you answered\n  \"yes\" to either of those questions, this might be a great addition to our\n  docs! If not, consider starting a discussion instead to see if your changes\n  might be the exception to this guideline before submitting.\n- **Keep commits and pull requests small.** We get it. It's more convenient to\n  throw all your changes into a single pull request or even into a single\n  commit. The problem is that, if some of the changes are good and others don't\n  quite work, having everything in one bucket makes it harder to filter out the\n  great changes from those that need more work.\n- **Make spelling and grammar fixes in a separate pull request from any content\n  changes.** These changes are quick to check and important to anyone reading\n  the docs. We want to make sure they hit the live documentation as quickly as\n  possible without being bogged down by other changes that require more\n  intensive review.\n\nPlease see Gel's guide for `building documentation\n<https://www.geldata.com/docs/guides/contributing#writing-documentation>`_ from\nsource.\n\nDocumentation style\n~~~~~~~~~~~~~~~~~~~\n\n- **Lines should be no longer than 79 characters.**\n- **Remove trailing whitespace or whitespace on empty lines.**\n- **Surround references to parameter named with asterisks.** You may be tempted\n  to surround parameter names with double backticks (````param````). We avoid\n  that in favor of ``*param*``, in order to distinguish between parameter\n  references and inline code (which *should* be surrounded by double\n  backticks).\n- **Gel is singular.** Choose \"Gel is\" over \"Gel are\" and \"Gel\n  does\" over \"Gel do.\"\n- **Use American English spellings.** Choose \"color\" over \"colour\" and\n  \"organize\" over \"organise.\"\n- **Use the Oxford comma.** When delineating a series, place a comma between\n  each item in the series, even the one with the conjunction. Use \"eggs, bacon,\n  and juice\" rather than \"eggs, bacon and juice.\"\n- **Write in the simplest prose that is still accurate and expresses everything\n  you need to convey.** You may be tempted to write documentation that sounds\n  like a computer science textbook. Sometimes that's necessary, but in most\n  cases, it isn't. Prioritize accuracy first and accessibility a close second.\n- **Be careful using words that have a special meaning in the context of\n  Gel.** In casual speech or writing, you might talk about a \"set\" of\n  something in a generic sense. Using the word this way in Gel documentation\n  might easily be interpreted as a reference to Gel's `sets <ref_eql_sets>`.\n  Avoid this kind of casual usage of key terms.\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nmembers = [\n    \"edb/edgeql-parser\",\n    \"edb/edgeql-parser/edgeql-parser-derive\",\n    \"edb/edgeql-parser/edgeql-parser-python\",\n    \"edb/graphql-rewrite\",\n    \"edb/server/_rust_native\",\n    \"rust/conn_pool\",\n    \"rust/gel-http\",\n    \"rust/pgrust\",\n    \"rust/pyo3_util\",\n]\nresolver = \"2\"\n\n[workspace.dependencies]\npyo3 = { version = \"0.26\", features = [\"extension-module\", \"serde\", \"macros\"] }\ntokio = { version = \"1\", features = [\"rt\", \"rt-multi-thread\", \"macros\", \"time\", \"sync\", \"net\", \"io-util\"] }\ntracing = \"0.1.40\"\ntracing-subscriber = { version = \"0.3.20\", features = [\"registry\", \"env-filter\"] }\n\ngel-auth = { version = \"=0.1.6\" }\ngel-stream = { version = \"=0.4.3\" }\ngel-protocol = { version = \"=0.8.5\" }\ngel-jwt = { version = \"=0.1.4\" }\ngel-db-protocol = { version = \"=0.1.2\" }\ngel-pg-protocol = { version = \"=0.1.1\" }\ngel-pg-captive = { version = \"=0.1.1\" }\ngel-dsn = { version = \"=0.2.14\" }\n\nconn_pool = { path = \"rust/conn_pool\" }\npgrust = { path = \"rust/pgrust\" }\ngel-http = { path = \"rust/gel-http\" }\npyo3_util = { path = \"rust/pyo3_util\" }\n\n[profile.release]\ndebug = true\nlto = true\n\n[workspace.lints.rust]\nunexpected_cfgs = { level = \"warn\", check-cfg = ['cfg(never)'] }\n\n[patch.crates-io]\nopenssl-probe = { git = \"https://github.com/edgedb/openssl-probe/\", rev = \"e5ed593600d1f8128629565d349682f54b3a8b57\" }\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "recursive-include edb *.edgeql *.esdl *.py *.txt\nrecursive-include tests *.edgeql *.esdl *.py\ninclude LICENSE README.md logo.svg\n\nrecursive-include edb/edgeql-parser *\nrecursive-include edb/edgeql-parser/edgeql-parser-python *\nrecursive-include edb/server/protocol/auth_ext/_static *\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: build docs cython postgres postgres-ext pygments build-reqs\n.DEFAULT_GOAL := build\n\nSPHINXOPTS:=\"-W -n\"\n\nBUILD_REQS_SCRIPT='print(\"\\x00\".join(__import__(\"build\").ProjectBuilder(\".\").build_system_requires))'\n\nbuild-reqs:\n\tpython -m pip install --no-build-isolation build\n\tpython -c $(BUILD_REQS_SCRIPT) | xargs -0 python -m pip install --no-build-isolation\n\n\ncython: build-reqs\n\tfind edb -name '*.pyx' | xargs touch\n\tBUILD_EXT_MODE=py-only python setup.py build_ext --inplace\n\n\n# Just rebuild actually changed cython. This *should* work, since\n# that is how build systems are supposed to be, but it sometimes\n# fails in annoying ways.\ncython-fast: build-reqs\n\tBUILD_EXT_MODE=py-only python setup.py build_ext --inplace\n\n\nrust: build-reqs\n\tBUILD_EXT_MODE=rust-only python setup.py build_ext --inplace\n\n\ncli: build-reqs\n\tpython setup.py build_cli\n\n\ndocs: build-reqs\n\tfind docs -name '*.rst' | xargs touch\n\t$(MAKE) -C docs html SPHINXOPTS=$(SPHINXOPTS) BUILDDIR=\"../build\"\n\n\npostgres: build-reqs\n\tpython setup.py build_postgres\n\n\nparsers:\n\tpython setup.py build_parsers --inplace\n\n\nlibpg-query:\n\tpython setup.py build_libpg_query\n\n\nui: build-reqs\n\tpython setup.py build_ui\n\n\npygments: build-reqs\n\tout=$$(edb gen-meta-grammars edgeql) && \\\n\t\techo \"$$out\" > edb/tools/pygments/edgeql/meta.py\n\n\ncasts: build-reqs\n\tout=$$(edb gen-cast-table) && \\\n\t\techo \"$$out\" > docs/reference/edgeql/casts.csv\n\n\nbuild: build-reqs\n\tfind edb -name '*.pyx' | xargs touch\n\tpip install --upgrade --editable .[docs,test,language-server]\n\n\nclean:\n\tgit clean -Xfd -e \"!/*.code-workspace\" -e \"!/*.vscode\"\n"
  },
  {
    "path": "NOTICE",
    "content": "EdgeDB\nCopyright 2008-present EdgeDB Inc.\n\nThis product includes software developed by\nEdgeDB Inc (https://www.edgedb.com/).\n"
  },
  {
    "path": "README.md",
    "content": "<!--<p align=\"center\">\n  <a href=\"https://www.geldata.com\">\n    <img src=\"https://www.geldata.com/github_banner.png\">\n  </a>\n</p>-->\n\n<div align=\"center\">\n  <h1>Gel</h1>\n  <a href=\"https://github.com/geldata/gel\" rel=\"nofollow\">\n    <img src=\"https://img.shields.io/github/stars/geldata/gel\" alt=\"Stars\">\n  </a>\n  <a href=\"https://github.com/geldata/gel/actions\">\n    <img src=\"https://github.com/geldata/gel/workflows/Tests/badge.svg?event=push&branch=master\" />\n  </a>\n  <a href=\"https://github.com/geldata/gel/blob/master/LICENSE\">\n    <img alt=\"license\" src=\"https://img.shields.io/badge/license-Apache%202.0-blue\" />\n  </a>\n  <a href=\"https://discord.gg/umUueND6ag\">\n    <img alt=\"discord\" src=\"https://img.shields.io/discord/841451783728529451?color=5865F2&label=discord&logo=discord&logoColor=8a9095\">\n  </a>\n  <br />\n  <br />\n  <a href=\"https://docs.geldata.com/learn/quickstart/overview/nextjs\">Learn: build an app with Gel</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://www.geldata.com\">Website</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://docs.geldata.com\">Docs</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://www.geldata.com/blog\">Blog</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://discord.gg/gel\">Discord</a>\n  <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>\n  <a href=\"https://twitter.com/geldata\">Twitter</a>\n  <br />\n\n</div>\n\n<br />\n<br />\n\n<br/>\n<div align=\"center\">\n  <h2>What is Gel?</h2>\n  <p style=\"max-width: 450px;\">\n    Gel is a new kind of database\n    <br/>\n    that takes the best parts of\n    <br/>\n    relational databases, graph\n    <br/>\n    databases, and ORMs. We call it\n    <br/>a <b>graph-relational database</b>.\n  </p>\n</div>\n\n<br/>\n\n<br/>\n<div align=\"center\">\n  <h3>🧩 Types, not tables 🧩</h3>\n</div>\n<br/>\n\nSchema is the foundation of your application. It should be something you can\nread, write, and understand.\n\nForget foreign keys; tabular data modeling is a relic of an older age, and it\n[isn't compatible](https://en.wikipedia.org/wiki/Object%E2%80%93relational_impedance_mismatch)\nwith modern languages. Instead, Gel thinks about schema the same way you do:\nas **object types** containing **properties** connected by **links**.\n\n```esdl\ntype Person {\n  required name: str;\n}\n\ntype Movie {\n  required title: str;\n  multi actors: Person;\n}\n```\n\nThis example is intentionally simple, but Gel supports everything you'd\nexpect from your database: a strict type system, indexes, constraints, computed\nproperties, stored procedures...the list goes on. Plus it gives you some shiny\nnew features too: link properties, schema mixins, and best-in-class JSON\nsupport. Read the [schema docs](https://docs.geldata.com/reference/datamodel)\nfor details.\n\n<!-- ### Objects, not rows. ❄️ -->\n\n<br/>\n<div align=\"center\">\n  <h3>🌳 Objects, not rows 🌳</h3>\n</div>\n<br/>\n\nGel's super-powered query language EdgeQL is designed as a ground-up\nredesign of SQL. EdgeQL queries produce rich, structured objects, not flat\nlists of rows. Deeply fetching related objects is painless...bye, bye, JOINs.\n\n```esdl\nselect Movie {\n  title,\n  actors: {\n    name\n  }\n}\nfilter .title = \"The Matrix\"\n```\n\nEdgeQL queries are also _composable_; you can use one EdgeQL query as an\nexpression inside another. This property makes things like _subqueries_ and\n_nested mutations_ a breeze.\n\n```esdl\ninsert Movie {\n  title := \"The Matrix Resurrections\",\n  actors := (\n    select Person\n    filter .name in {\n      'Keanu Reeves',\n      'Carrie-Anne Moss',\n      'Laurence Fishburne'\n    }\n  )\n}\n```\n\nThere's a lot more to EdgeQL: a comprehensive standard library, computed\nproperties, polymorphic queries, `with` blocks, transactions, and much more.\nRead the [EdgeQL docs](https://docs.geldata.com/reference/edgeql) for the full\npicture.\n\n<br/>\n<div align=\"center\">\n  <h3>🦋 More than a mapper 🦋</h3>\n</div>\n<br/>\n\nWhile Gel solves the same problems as ORM libraries, it's so much more. It's\na full-fledged database with a\n[powerful and elegant query language](https://docs.geldata.com/reference/edgeql), a\n[migrations system](https://docs.geldata.com/learn/migrations), a\n[suite of client libraries](https://docs.geldata.com/reference/clients) in\ndifferent languages, a\n[command line tool](https://docs.geldata.com/learn/cli), and a managed\n[cloud service](https://geldata.com/cloud).\nThe goal is to rethink every aspect of how developers model, migrate,\nmanage, and query their database.\n\nHere's a taste-test of Gel's next-level developer experience: you can\ninstall our CLI, spin up an instance, and open an interactive EdgeQL shell with\njust three commands.\n\n```\n$ curl --proto '=https' --tlsv1.2 -sSf https://geldata.com/sh | sh\n$ edgedb project init\n$ edgedb\nedgedb> select \"Hello world!\"\n```\n\nWindows users: use this Powershell command to install the CLI.\n\n```\nPS> iwr https://geldata.com/ps1 -useb | iex\n```\n\n<br />\n\n## Get started\n\nTo start learning about Gel, check out the following resources:\n\n- **[The quickstart](https://docs.geldata.com/learn/quickstart/overview/nextjs)**. If\n  you're just starting out, the 10-minute quickstart guide is the fastest way\n  to get up and running.\n- **[Gel Cloud 🌤️](https://www.geldata.com/cloud)**. The best\n  most effortless way to host your Gel database in the cloud.\n<!--- **[The interactive tutorial](https://www.geldata.com/tutorial)**. For a\n  structured deep-dive into the EdgeQL query language, try the web-based\n  tutorial— no need to install anything.\n The e-book needs to be converted to Gel\n\n- **[The e-book](https://www.edgedb.com/easy-edgedb)**. For the most\n  comprehensive walkthrough of EdgeDB concepts, check out our illustrated\n  e-book [Easy EdgeDB](https://www.edgedb.com/easy-edgedb). It's designed to\n  walk a total beginner through EdgeDB in its entirety, from the basics through\n  advanced concepts.\n-->\n- **The docs.** Jump straight into the docs for\n  [schema modeling](https://docs.geldata.com/reference/datamodel) or\n  [EdgeQL](https://docs.geldata.com/reference/edgeql)!\n\n<br />\n\n## Contributing\n\nPRs are always welcome! To get started, follow\n[this guide](https://docs.geldata.com/resources/guides/contributing) to build Gel from\nsource on your local machine.\n\n[File an issue 👉](https://github.com/geldata/gel/issues/new/choose)\n<br />\n[Start a Discussion 👉](https://github.com/geldata/gel/discussions/new)\n<br />\n[Join the discord 👉](https://discord.gg/gel)\n\n<br />\n\n## License\n\nThe code in this repository is developed and distributed under the\nApache 2.0 license. See [LICENSE](LICENSE) for details.\n"
  },
  {
    "path": "build_backend.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# This is a straight proxy to setuptools.build_meta backend that exists\n# solely because someone thought that in-tree build dependencies should\n# require this.\n\nfrom setuptools.build_meta import *  # noqa\n"
  },
  {
    "path": "dev-notes/concurrent-indexes.py",
    "content": "#!/usr/bin/env python3\n\nimport gel\n\n\ndef create_concurrent_indexes(db, msg_callback=print):\n    '''Actually create all \"create concurrently\" indexes\n\n    The protocol here is to find all the indexes that need created,\n    and create them with `administer concurrent_index_build()`.\n    It's possible that the database will shut down after an index\n    creation but before the metadata is updated, in which case\n    we might rerun the command later, which is harmless.\n\n    If we stick with this ADMINISTER-based schemed, I figure this code\n    would live in the CLI.\n    '''\n    indexes = db.query('''\n        select schema::Index {\n            id, expr, subject_name := .<indexes[is schema::ObjectType].name\n        }\n        filter .build_concurrently and not .active\n    ''')\n    for index in indexes:\n        msg_callback(\n            f\"Creating concurrent index on '{index.subject_name}' \"\n            f\"with expr ({index.expr})\"\n        )\n        db.execute(f'''\n            administer concurrent_index_build(\"<uuid>{index.id}\")\n        ''')\n\n\ndef main():\n    with gel.create_client() as db:\n        create_concurrent_indexes(db)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "dev-notes/inplace-upgrades.md",
    "content": "The inplace upgrade system adds three new flags to edgedb-server. They may (though probably usually won't) be specified together. If any of them is specified, the server will exit after performing the in-place upgrade operations instead of starting up.\n\n * ``-inplace-upgrade-prepare <file>`` -- \"prepare\" an inplace upgrade, using schema information provided in ``<file>``. (More about this later.) This will create the new standard library (in a namespace), populate the schema tables with user schemas, and prepare (but not execute) any irreversible scripts for updating the standard library trampolines and fixing up user-defined functions.\n\n   This operation should not do anything that cannot be backed out.\n   It may be run while an older version of the server is still live.\n\n   If this is interrupted, crashes, or fails, it *will leave a partially prepared database*. To deal with this, see the next command.\n\n   The file should be in the format produced by ``tests/inplace-testing/prep-upgrades.py``: a JSON object where the keys are branch names and the values are the results of executing ``administer prepare_upgrade()``.\n\n * ``-inplace-upgrade-rollback`` -- Rolls back a prepared upgrade.\n   This works by deleting everything in the newly created schemas. It can rollback partially prepared upgrades.\n\n   It may be run while an older version of the server is still live.\n\n * ``-inplace-upgrade-finalize`` -- Finalizes a prepared upgrade by fully flipping the database to the new version. This flips standard library trampolines, patches user-defined functions, and deletes the old standard library.\n\n   The old version must not be running. (Though there is not a clear way to enforce this.)\n\n   Finalize does a dry run of each branch's upgrade inside a transaction before making any changes. If this fails, the upgrade may be broken (due to a bug or an incompatibility), and it may still be rolled back.\n\n   If finalize fails *after* the dry run, once it has started actually finalizing branches, then it *may not* be rolled back. Because all of the upgrades were tested in (reverted) transactions, this *should* only happen in the case of interruption or postgres crash, and it should be safe to retry the finalize.\n\n   If finalize emitted a message of the form \"Finished pivoting branch '<something>'\", then the upgrade may not be rolled back; the only way out is through. Rollback will refuse to operate in this case.\n\n-----\n\nSuggested procedure:\n\n0.5. Make a backup\n\n1. ``edgedb query 'configure instance set force_database_error := $${\"type\": \"AvailabilityError\", \"message\": \"DDL is disabled due to in-place upgrade.\", \"_scopes\": [\"ddl\"]}$$;'``.\n   This will disable all DDL commands to the database, while leaving it running for both read and write queries.\n\n2. ``tests/inplace-testing/prep-upgrades.py > \"upgrade.json\"``.\n   This will dump the information needed for upgrade.\n\n3. ``edgedb-server --backend-dsn=\"$DSN\" --inplace-upgrade-prepare upgrade.json``.\n   This will prepare the upgrade.\n\n4. Stop the old edgedb server.\n\n4.5. Make a backup\n\n5. ``edgedb-server --backend-dsn=\"$DSN\" --inplace-upgrade-finalize``.\n   This will finalize the upgrade.\n\n6. Start the new server.\n\n7. ``edgedb query 'configure instance reset force_database_error'``\n\nIf there is a failure in step 3 or step 5 *before* a branch has finished pivoting, then it can be rolled back with ``edgedb-server --backend-dsn=\"$DSN\" --inplace-upgrade-rollback``.\n\nIf there is a failure after a branch has been pivoted, then there is nothing to do but retry it.\n(And restore from a backup if that doesn't work. That would be a bug, and one that has slipped past at least one line of defence.)\n\n\n----\n\nTesting notes:\n\nCurrently, we can only inplace upgrade beween full major versions, since we use the major version number to distinguish between the namespaced stdlibs.\n\nFor testing inplace upgrades, we have a test that applies a patch that bumps the major version number and catalog.\n\nTODO: Maybe we should use the catalog number instead, which will make it easier to test between different nightlies.\n"
  },
  {
    "path": "dev-notes/newtype-checklist.md",
    "content": "This is a checklist of steps needed to add a new type to EdgeDB, along with links to examples of PRs doing the tasks.\n\nCore database range PRs:\n * https://github.com/edgedb/edgedb/pull/3983\n * https://github.com/edgedb/edgedb/pull/4020\n\nCore database cal::duration PRs:\n * https://github.com/edgedb/edgedb/pull/3948\n\n- [ ] JSON handling\n  - [ ] Implement JSON casts if the default Postgres behavior won't work\n  - [ ] Update output.serialize_expr_to_json(), if the default won't work\n    * range: https://github.com/edgedb/edgedb/pull/4008\n\n- [ ] If any new functions or constructors have an implementation that is not\n      just purely a call to a strict function, make sure to test with inputs\n      that are NULL at runtime!\n      Probably the easiest way to generate NULL-at-runtime values is\n      `<optional TYPE>$0` and then passing in `{}`.\n  * range test example and bugfix: https://github.com/edgedb/edgedb/pull/4207/\n\n- [ ] For compound types, add a schema class in edb/schema/types.py and\n\n- [ ] Add mapping to pgsql types in edb/pgsql/types.py\n\n- [ ] Add implementations of any relevant functions/operations to `edb/lib`.\n\n- [ ] For compound types, add a type descriptor and code for encoding it in\nedb/server/compiler/sertypes.py.\n  * range: https://github.com/edgedb/edgedb/pull/4016\n\n- [ ] For new scalar types, add it to edb/api/types.txt and edb/graphql/types.py. Run `edb gen-types`.\n\n- [ ] Update all of the first-party language drivers (or get their owners to)\n  - [ ] Python (Fantix/Elvis/Sully)\n    * cal::date_duration: https://github.com/edgedb/edgedb-python/pull/335\n    * range: https://github.com/edgedb/edgedb-python/pull/332/\n  - [ ] Go (Frederick)\n    * cal::date_duration: https://github.com/edgedb/edgedb-go/pull/232\n  - [ ] Javascript (James/Colin)\n    * cal::date_duration: https://github.com/edgedb/edgedb-js/pull/373/\n    * range: https://github.com/edgedb/edgedb-js/pull/377\n  - [ ] Rust/CLI (Paul)\n    * This requires updating both the Rust bindings to support the\n      new type and the CLI to properly print it\n    * cal::date_duration: https://github.com/edgedb/edgedb-rust/pull/146, https://github.com/edgedb/edgedb-cli/pull/759\n    * range: https://github.com/edgedb/edgedb-rust/pull/145, https://github.com/edgedb/edgedb-cli/pull/755\n\n- [ ] Add a field of the new type to the `dump` test for the new version\n\n- [ ] Write tests.\n"
  },
  {
    "path": "dev-notes/release-process.md",
    "content": "# Instructions for releasing a new version\n\nDeprecates release instruction from\n[RFC 2](https://github.com/edgedb/rfcs/blob/master/text/0002-edgedb-release-process.rst).\n\nEdgeDB packages are published on https://packages.edgedb.com.\nThey are build in GitHub Actions pipelines, using\nhttps://github.com/edgedb/edgedb-pkg.\n\nReleases are built from a release branch associated with a major version\n(i.e. \"release/4.x\"). At feature freeze, we create this branch. From that moment\non, all additional commits will have to be cherry-picked to this branch.\n\nBefore the major version, we publish \"testing releases\":\n\n- \"alpha\" (i.e. `v4.0a1`, `v4.0a2`),\n- \"beta\" (i.e. `v4.0b1`, `v4.0b2`),\n- \"release candidates\" (`v4.0rc1`) that we might promote into the final release.\n\n## Internal Communication\n\nAnnounce on team slack when you are beginning to prepare a release,\nwhen a release build has been kicked off, and when the release has\nsucceeded. Update the thread with any problems and attempted\nresolutions.\n\nCommunicate in the other direction as well: make sure the release\nmanager knows of any pending work that you want in a release.\n\n\"b1\", \"rc1\", and \".0\" releases are big deals. Make sure to get signoff\nbefore releasing.\n\n## edgedb-ui\n\nOn release branches, `edgedb-ui` should be pinned to the associated branch.\nThis can be done in `setup.py` with the variable `EDGEDBGUI_COMMIT`.\nFor example, on branch `release/4.x`, it is pinned to `edgedb-ui`'s branch `4.x`.\nThis means any release off `release/4.x` will contain latest commits from\n`edgedb-ui`'s branch `4.x`.\n\n\n## Preparing commits for a release\n\nFor each major release `N`, we have two GitHub labels: `to-backport-N.x` and\n`backported-N.x`. PRs that need to be backported should be labelled with\n`to-backport-N.x` for each of the target versions.\n\nOnce a PR is backported, `to-backport-N.x` should be removed and\n`backported-N.x` added.\n\nTracking both states makes it easy to tell what needs to be backported\nand what has been backported.\n\n(Historical note: previously we had simply a `backport-N.x` label.\nThis made it easy to ensure that everything that got labelled with\n`backport` actually got backported, but there was not an at-a-glance\nway to see if something *had* been backported. Even looking at the\nissue didn't always tell you, since sometimes we labelled things as\n`backport` and then thought better of it.)\n\n### Technical helpers\n\nThe `gh` command line makes a bunch of these operations simple.\n\nTo enumerate all pending backports for a branch:\n```bash\ngh pr list --state all -l to-backport-5.x\n```\n\nTo adjust labels to mark a PR as backported:\n```bash\ngh pr edit --remove-label to-backport-N.x --add-label backported-N.x <PR NUMBER>\n```\n\nA helper shell script to cherry-pick a commit using its PR number:\n\n```bash\n# this won't work if a PR is not squashed into a single commit\nfunction cp-pr {\n    git cherry-pick $(gh pr view $1 --json mergeCommit --jq .mergeCommit.oid)\n}\n```\n\n\n### What to backport?\nSometimes, people will forget to label the PR to be back-ported, so a good\npractice is to list all commits since the last release:\n\n```\ngit show releases/4.x # to see the last commit that has been cherry-picked\ngit log master # find the hash of that commit on master\ngit log hash_of_that_commit..master > ../to-backport.txt\n```\n\nNow, one can go through the list and see if the commits are worth back-porting.\nA few pointers:\n\n- Don't backport new features, unless it is high-priority for some reason.\n- Don't backport docs, since the website is built from master.\n- Don't backport refactors, since they might introduce bugs and there is no\n  point in improving the codebase of a branch we are not developing on anymore.\n  Disregard this rule early on after the fork of the release branch, since\n  porting refactors will decrease chances merge conflicts of other commits later\n  on.\n- Don't backport \"build\" commits (updating of build deps, refactoring of the\n  release pipeline), since that might trigger problems in the release process.\n- If a PR changes:\n\n  - any of the schema objects (i.e. adding a field to `s_types.Type`) or\n  - a std library object (i.e. changing implementation of `std::round`),\n  - metaschema (i.e. changing a pg function `edgedb.range_to_jsonb`),\n    ... a \"patch\" needs to be added into `pgsql/patches.py`.\n    This is needed, because minor releases don't require a \"dump and restore\",\n    so we must apply these changes to existing user databases.\n\n  Patches must be tested using this GHA workflow:\n  https://github.com/edgedb/edgedb/actions/workflows/tests-patches.yml\n\n## Release pipeline\n\nWhen you have your commits ready, tag the commit and push:\n\n```\n# git tag --sign v4.5\n# git push origin releases/4.x --follow-tags\n```\n\nThen open GitHub Actions page and run one of these pipelines:\n\n- https://github.com/edgedb/edgedb/actions/workflows/testing.yml\n- https://github.com/edgedb/edgedb/actions/workflows/release.yml\n\nThis will kick-off an GHA workflow that should take ~3 hours.\nIt will build, test and publish for each of the supported platforms.\nIt will not publish any packages if any of the tests fail.\n\nSometimes, tests will be flakey and just need to be re-run.\nYou can do that with a button top-right.\n\n## Changelog\n\nEach major release has a changelog page in the docs (i.e.\n`docs/changelog/4_x.rst`). It should contain explanations of the new features,\nwhich are usually composed by our dev-rel team.\n\nEach minor release is just a subsection in the page, as a list of back-ported\nPRs. Any PRs that fix internal stuff (like our test framework) or are not user\nfacing should not be included in the changelog.\n\nDon't forget to include commits released from `edgedb-ui`.\n\nThese changes need to land on master branch and are not needed on the release\nbranch, so best course of action if to open a PR to master after kicking off\nthe release pipeline. After that PR is merged, the website needs to be\ndeployed, for changelog to land on the website (ping dev rel team).\n\nA helper function to generate changelog is:\n\n```python\n# I keep this in ../compose-changelog.py\n\nimport json\nimport requests\nimport re\nimport sys\n\nBASE_URL = 'https://api.github.com/repos/edgedb/edgedb/compare'\n\ndef main():\n    if len(sys.argv) < 2:\n        print('pass a sha1 hash as a first argument')\n        sys.exit(1)\n\n    from_hash = sys.argv[1]\n    if len(sys.argv) > 2:\n        to_hash = sys.argv[2]\n\n    r = requests.get(f'{BASE_URL}/{from_hash}...{to_hash}')\n    data = json.loads(r.text)\n\n    for commit in data['commits']:\n        message = commit['commit']['message']\n        first_line = message.partition('\\n\\n')[0]\n        if commit.get('author'):\n            username = '@{}'.format(commit['author']['login'])\n        else:\n            username = commit['commit']['author']['name']\n        sha = commit[\"sha\"][:8]\n\n        m = re.search(r'\\#(?P<num>\\d+)\\b', message)\n        if m:\n            issue_num = m.group('num')\n        else:\n            issue_num = None\n\n        first_line = re.sub(r'\\(\\#(?P<num>\\d+)\\)', '', first_line)\n        print(f'* {first_line}')\n        # print(f'  (by {username} in {sha}', end='')\n        if issue_num:\n            print(f'  (:eql:gh:`#{issue_num}`)')\n        print()\n\nif __name__ == '__main__':\n    main()\n```\n\n```bash\npython ../compose-changelog.py v4.5 v4.6 >> docs/changelog/4_x.rst\n```\n\n## After the release\n\nThe release pipelines will make the new version available at\nhttps://packages.edgedb.com. This is enough for it to be installable using the\nCLI, but other methods of installation need to be kicked of manually:\n\n- our cloud team needs to deploy separate _cloud wizardly groups_,\n- docker image needs to be published to https://hub.docker.com,\n- Digital Ocean image needs to be published by Frederick,\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "/_build\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nPAPER         =\nBUILDDIR      = _build\n\n# User-friendly check for sphinx-build\nifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)\n$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)\nendif\n\n# Internal variables.\nPAPEROPT_a4     = -D latex_paper_size=a4\nPAPEROPT_letter = -D latex_paper_size=letter\nALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n# the i18n builder cannot share the environment and doctrees with the others\nI18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n\n.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext\n\nhelp:\n\t@echo \"Please use \\`make <target>' where <target> is one of\"\n\t@echo \"  html       to make standalone HTML files\"\n\t@echo \"  dirhtml    to make HTML files named index.html in directories\"\n\t@echo \"  singlehtml to make a single large HTML file\"\n\t@echo \"  pickle     to make pickle files\"\n\t@echo \"  json       to make JSON files\"\n\t@echo \"  htmlhelp   to make HTML files and a HTML help project\"\n\t@echo \"  qthelp     to make HTML files and a qthelp project\"\n\t@echo \"  applehelp  to make an Apple Help Book\"\n\t@echo \"  devhelp    to make HTML files and a Devhelp project\"\n\t@echo \"  epub       to make an epub\"\n\t@echo \"  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\"\n\t@echo \"  latexpdf   to make LaTeX files and run them through pdflatex\"\n\t@echo \"  latexpdfja to make LaTeX files and run them through platex/dvipdfmx\"\n\t@echo \"  text       to make text files\"\n\t@echo \"  man        to make manual pages\"\n\t@echo \"  texinfo    to make Texinfo files\"\n\t@echo \"  info       to make Texinfo files and run them through makeinfo\"\n\t@echo \"  gettext    to make PO message catalogs\"\n\t@echo \"  changes    to make an overview of all changed/added/deprecated items\"\n\t@echo \"  xml        to make Docutils-native XML files\"\n\t@echo \"  pseudoxml  to make pseudoxml-XML files for display purposes\"\n\t@echo \"  linkcheck  to check all external links for integrity\"\n\t@echo \"  doctest    to run all doctests embedded in the documentation (if enabled)\"\n\t@echo \"  coverage   to run coverage check of the documentation (if enabled)\"\n\nclean:\n\trm -rf $(BUILDDIR)/*\n\nhtml:\n\t$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/html.\"\n\ndirhtml:\n\t$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/dirhtml.\"\n\nsinglehtml:\n\t$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml\n\t@echo\n\t@echo \"Build finished. The HTML page is in $(BUILDDIR)/singlehtml.\"\n\npickle:\n\t$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle\n\t@echo\n\t@echo \"Build finished; now you can process the pickle files.\"\n\njson:\n\t$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json\n\t@echo\n\t@echo \"Build finished; now you can process the JSON files.\"\n\nhtmlhelp:\n\t$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp\n\t@echo\n\t@echo \"Build finished; now you can run HTML Help Workshop with the\" \\\n\t      \".hhp project file in $(BUILDDIR)/htmlhelp.\"\n\nqthelp:\n\t$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp\n\t@echo\n\t@echo \"Build finished; now you can run \"qcollectiongenerator\" with the\" \\\n\t      \".qhcp project file in $(BUILDDIR)/qthelp, like this:\"\n\t@echo \"# qcollectiongenerator $(BUILDDIR)/qthelp/EdgeDB.qhcp\"\n\t@echo \"To view the help file:\"\n\t@echo \"# assistant -collectionFile $(BUILDDIR)/qthelp/EdgeDB.qhc\"\n\napplehelp:\n\t$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp\n\t@echo\n\t@echo \"Build finished. The help book is in $(BUILDDIR)/applehelp.\"\n\t@echo \"N.B. You won't be able to view it unless you put it in\" \\\n\t      \"~/Library/Documentation/Help or install it in your application\" \\\n\t      \"bundle.\"\n\ndevhelp:\n\t$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp\n\t@echo\n\t@echo \"Build finished.\"\n\t@echo \"To view the help file:\"\n\t@echo \"# mkdir -p $$HOME/.local/share/devhelp/EdgeDB\"\n\t@echo \"# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/EdgeDB\"\n\t@echo \"# devhelp\"\n\nepub:\n\t$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub\n\t@echo\n\t@echo \"Build finished. The epub file is in $(BUILDDIR)/epub.\"\n\nlatex:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo\n\t@echo \"Build finished; the LaTeX files are in $(BUILDDIR)/latex.\"\n\t@echo \"Run \\`make' in that directory to run these through (pdf)latex\" \\\n\t      \"(use \\`make latexpdf' here to do that automatically).\"\n\nlatexpdf:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through pdflatex...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\nlatexpdfja:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through platex and dvipdfmx...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\ntext:\n\t$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text\n\t@echo\n\t@echo \"Build finished. The text files are in $(BUILDDIR)/text.\"\n\nman:\n\t$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man\n\t@echo\n\t@echo \"Build finished. The manual pages are in $(BUILDDIR)/man.\"\n\ntexinfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo\n\t@echo \"Build finished. The Texinfo files are in $(BUILDDIR)/texinfo.\"\n\t@echo \"Run \\`make' in that directory to run these through makeinfo\" \\\n\t      \"(use \\`make info' here to do that automatically).\"\n\ninfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo \"Running Texinfo files through makeinfo...\"\n\tmake -C $(BUILDDIR)/texinfo info\n\t@echo \"makeinfo finished; the Info files are in $(BUILDDIR)/texinfo.\"\n\ngettext:\n\t$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale\n\t@echo\n\t@echo \"Build finished. The message catalogs are in $(BUILDDIR)/locale.\"\n\nchanges:\n\t$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes\n\t@echo\n\t@echo \"The overview file is in $(BUILDDIR)/changes.\"\n\nlinkcheck:\n\t$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck\n\t@echo\n\t@echo \"Link check complete; look for any errors in the above output \" \\\n\t      \"or in $(BUILDDIR)/linkcheck/output.txt.\"\n\ndoctest:\n\t$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest\n\t@echo \"Testing of doctests in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/doctest/output.txt.\"\n\ncoverage:\n\t$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage\n\t@echo \"Testing of coverage in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/coverage/python.txt.\"\n\nxml:\n\t$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml\n\t@echo\n\t@echo \"Build finished. The XML files are in $(BUILDDIR)/xml.\"\n\npseudoxml:\n\t$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml\n\t@echo\n\t@echo \"Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml.\"\n"
  },
  {
    "path": "docs/cloud/cli.rst",
    "content": ".. _ref_guide_cloud_cli:\n\n===\nCLI\n===\n\n:edb-alt-title: Using Gel Cloud via the CLI\n\nTo use |Gel| Cloud via the CLI, first log in using\n:ref:`ref_cli_gel_cloud_login`.\n\n.. note::\n\n    This is the way you'll log in interactively on your development machine,\n    but when interacting with Gel Cloud via a script or in CI, you'll\n    instead set the :gelenv:`SECRET_KEY` environment variable to your secret\n    key. Generate a secret key in the Gel Cloud UI or by running\n    :ref:`ref_cli_gel_cloud_secretkey_create`. The :gelcmd:`cloud\n    login` and :gelcmd:`cloud logout` commands are not intended for use\n    in this context.\n\nOnce your login is successful, you will be able to create an instance using\neither :ref:`ref_cli_gel_instance_create` or\n:ref:`ref_cli_gel_project_init`, depending on whether you also want to\ncreate a local project linked to your instance.\n\n* :ref:`ref_cli_gel_instance_create` with an instance name of\n  ``<org-name>/<instance-name>``.\n\n  .. code-block:: bash\n\n      $ gel instance create <org-name>/<instance-name>\n\n* :ref:`ref_cli_gel_project_init` with the ``--server-instance`` option. Set\n  the server instance name to ``<org-name>/<instance-name>``.\n\n  .. code-block:: bash\n\n      $ gel project init \\\n        --server-instance <org-name>/<instance-name>\n\n  Alternatively, you can run :gelcmd:`project init` *without* the\n  ``--server-instance`` option and enter an instance name in the\n  ``<org-name>/<instance-name>`` format when prompted interactively.\n\n.. note::\n\n    Please be aware of the following restrictions on |Gel| Cloud instance\n    names:\n\n    * can contain only Latin alpha-numeric characters or ``-``\n    * cannot start with a dash (``-``) or contain double dashes (``--``)\n    * maximum instance name length is 61 characters minus the length of your\n      organization name (i.e., length of organization name + length of instance\n      name must be fewer than 62 characters)\n\nTo use :gelcmd:`instance create`:\n\n.. code-block:: bash\n\n    $ gel instance create <org-name>/<instance-name>\n\nTo use :gelcmd:`project init`:\n\n.. code-block:: bash\n\n    $ gel project init \\\n      --server-instance <org-name>/<instance-name>\n\nAlternatively, you can run :gelcmd:`project init` *without* the\n``--server-instance`` option and enter an instance name in the\n``<org-name>/<instance-name>`` format when prompted interactively.\n"
  },
  {
    "path": "docs/cloud/deploy/fly.rst",
    "content": ".. _ref_guide_cloud_deploy_fly:\n\n======\nFly.io\n======\n\n:edb-alt-title: Deploying applications built on Gel Cloud to Fly.io\n\n1. Install the `Fly.io CLI <https://fly.io/docs/hands-on/install-flyctl/>`_\n2. Log in to Fly.io with ``flyctl auth login``\n3. Run ``flyctl launch`` to create a new app on Fly.io and configure it.\n   It will ask you to select a region and a name for your app. When done it will\n   create a ``fly.toml`` file and a ``Dockerfile`` in your project directory.\n4. Set :gelenv:`INSTANCE` and :gelenv:`SECRET_KEY` as secrets in your Fly.io\n   app.\n\n   For **runtime secrets**, you can do this by running the following commands:\n\n   .. code-block:: bash\n\n    $ flyctl secrets set GEL_INSTANCE <GEL_INSTANCE>\n    $ flyctl secrets set GEL_SECRET_KEY <GEL_SECRET_KEY>\n\n   `Read more about Fly.io runtime secrets\n   <https://fly.io/docs/reference/secrets/>`_.\n\n   For **build secrets**, you can do this by modifying the ``Dockerfile`` to\n   mount the secrets as environment variables.\n\n   .. code-block:: dockerfile-diff\n    :caption: Dockerfile\n\n      # Build application\n    -  RUN pnpm run build\n    +  RUN --mount=type=secret,id=GEL_INSTANCE \\\n    +      --mount=type=secret,id=GEL_SECRET_KEY \\\n    +      GEL_INSTANCE=\"$(cat /run/secrets/GEL_INSTANCE)\" \\\n    +      GEL_SECRET_KEY=\"$(cat /run/secrets/GEL_SECRET_KEY)\" \\\n    +      pnpm run build\n\n   `Read more about Fly.io build secrets\n   <https://fly.io/docs/reference/build-secrets/>`_.\n\n5. Deploy your app to Fly.io\n\n   .. code-block:: bash\n\n    $ flyctl deploy\n\n   If your app requires build secrets, you can pass them as arguments\n   to the ``deploy`` command:\n\n   .. code-block:: bash\n\n    $ flyctl deploy --build-secret GEL_INSTANCE=\"<GEL_INSTANCE>\" \\\n        --build-secret GEL_SECRET_KEY=\"<GEL_SECRET_KEY>\"\n"
  },
  {
    "path": "docs/cloud/deploy/index.rst",
    "content": ".. _ref_guide_cloud_deploy:\n\n=============\nDeploy an app\n=============\n\n:edb-alt-title: Deploying applications built on Gel Cloud\n\nFor your production deployment, generate a dedicated secret key for your\ninstance with :ref:`ref_cli_gel_cloud_secretkey_create` or via the web UI's\n\"Secret Keys\" pane in your instance dashboard. Create two environment variables\naccessible to your production application:\n\n* :gelenv:`SECRET_KEY`- contains the secret key you generated\n* :gelenv:`INSTANCE`- the name of your Gel Cloud instance\n  (``<org-name>/<instance-name>``)\n\nIf you use one of these platforms, try the platform's guide for\nplatform-specific instructions:\n\n.. toctree::\n    :maxdepth: 1\n\n    vercel\n    netlify\n    fly\n    railway\n    render\n"
  },
  {
    "path": "docs/cloud/deploy/netlify.rst",
    "content": ".. _ref_guide_cloud_deploy_netlify:\n\n=======\nNetlify\n=======\n\n:edb-alt-title: Deploying applications built on Gel Cloud to Netlify\n\n.. note::\n\n    This guide assumes the Git deployment method on Netlify, but you may also\n    deploy your site using other methods. Just make sure the Gel Cloud\n    environment variables are set, and your app should have connectivity to\n    your instance.\n\n1. Push project to GitHub or some other Git remote repository\n2. Create and make note of a secret key for your Gel Cloud instance\n3. On your Netlify Team Overview view under Sites, click Import from Git\n4. Import your project's repository\n5. Configure the build settings appropriately for your app\n6. Click the Add environment variable button\n7. Use the New variable button to add two variables:\n\n   - :gelenv:`INSTANCE` containing your Gel Cloud instance name (in\n     ``<org>/<instance-name>`` format)\n   - :gelenv:`SECRET_KEY` containing the secret key you created and noted\n     previously.\n\n8. Click Deploy\n\n.. image:: images/cloud-netlify-config.png\n    :width: 100%\n    :alt: A screenshot of the Netlify deployment configuration view\n          highlighting the environment variables section where a user will\n          need to set the necessary variables for Gel Cloud instance\n          connection.\n"
  },
  {
    "path": "docs/cloud/deploy/railway.rst",
    "content": ".. _ref_guide_cloud_deploy_railway:\n\n=======\nRailway\n=======\n\n:edb-alt-title: Deploying applications built on Gel Cloud to Railway\n\n1. Push project to GitHub or some other Git remote repository\n2. Create and make note of a secret key for your Gel Cloud instance\n3. From Railway's dashboard, click the \"New Project\" button\n4. Select the repository you want to deploy\n5. Click the \"Add variables\" button to add the following environment variables:\n\n   - :gelenv:`INSTANCE` containing your Gel Cloud instance name (in\n     ``<org>/<instance-name>`` format)\n   - :gelenv:`SECRET_KEY` containing the secret key you created and noted\n     previously.\n\n6. Click \"Deploy\"\n\n.. image:: images/cloud-railway-config.png\n    :width: 100%\n    :alt: A screenshot of the Railway deployment configuration view\n          highlighting the environment variables section where a user will\n          need to set the necessary variables for Gel Cloud instance\n          connection.\n"
  },
  {
    "path": "docs/cloud/deploy/render.rst",
    "content": ".. _ref_guide_cloud_deploy_render:\n\n======\nRender\n======\n\n:edb-alt-title: Deploying applications built on Gel Cloud to Render\n\n1. Push project to GitHub or some other Git remote repository\n2. Create and make note of a secret key for your Gel Cloud instance\n3. From Render's dashboard, click \"New > Web Service\"\n4. Import your project's repository\n5. In the setup page, scroll down to the \"Environment Variables\" section and\n   add the following environment variables:\n\n   - :gelenv:`INSTANCE` containing your Gel Cloud instance name (in\n     ``<org>/<instance-name>`` format)\n   - :gelenv:`SECRET_KEY` containing the secret key you created and noted\n     previously.\n\n6. Click Deploy\n\n.. image:: images/cloud-render-config.png\n    :width: 100%\n    :alt: A screenshot of the Render deployment configuration view\n          highlighting the environment variables section where a user\n          will need to set the necessary variables for Gel Cloud instance\n          connection.\n"
  },
  {
    "path": "docs/cloud/deploy/vercel.rst",
    "content": ".. _ref_guide_cloud_deploy_vercel:\n\n======\nVercel\n======\n\n:edb-alt-title: Deploying applications built on Gel Cloud to Vercel\n\n1. Push project to GitHub or some other Git remote repository\n2. Create and make note of a secret key for your Gel Cloud instance\n3. From Vercel's Overview tab, click Add New > Project\n4. Import your project's repository\n5. In \"Configure Project,\" expand \"Environment Variables\" to add two variables:\n\n   - :gelenv:`INSTANCE` containing your Gel Cloud instance name (in\n     ``<org>/<instance-name>`` format)\n   - :gelenv:`SECRET_KEY` containing the secret key you created and noted\n     previously.\n\n6. Click Deploy\n\n.. image:: images/cloud-vercel-config.png\n    :width: 100%\n    :alt: A screenshot of the Vercel deployment configuration view highlighting\n          the environment variables section where a user will need to set the\n          necessary variables for |Gel| Cloud instance connection.\n"
  },
  {
    "path": "docs/cloud/http_gql.rst",
    "content": ".. _ref_guide_cloud_http_gql:\n\n===================\nHTTP & GraphQL APIs\n===================\n\n:edb-alt-title: Querying Gel Cloud over HTTP and GraphQL\n\nUsing |Gel| Cloud via HTTP and GraphQL works the same as :ref:`using any other\n|Gel| instance <ref_edgeql_http>`. The two differences are in **how to\ndiscover your instance's URL** and **authentication**.\n\n\nEnabling\n========\n\n|Gel| Cloud can expose an HTTP endpoint for EdgeQL queries. Since HTTP is a\nstateless protocol, no :ref:`DDL <ref_eql_ddl>` or :ref:`transaction commands\n<ref_eql_statements_start_tx>`, can be executed using this endpoint.  Only one\nquery per request can be executed.\n\nIn order to set up HTTP access to the database add the following to\nthe schema:\n\n.. code-block:: sdl\n\n    using extension edgeql_http;\n\nThen create a new migration and apply it using\n:ref:`ref_cli_gel_migration_create` and\n:ref:`ref_cli_gel_migrate`, respectively.\n\nYour instance can now receive EdgeQL queries over HTTP at\n``https://<host>:<port>/branch/<branch-name>/edgeql``.\n\n\nInstance URL\n============\n\nTo determine the URL of a |Gel| Cloud instance, find the host by running\n:gelcmd:`instance credentials -I <org-name>/<instance-name>`. Use the\n``host`` and ``port`` from that table in the URL format above this note.\nChange the protocol to ``https`` since Gel Cloud instances are secured\nwith TLS.\n\nYour instance can now receive EdgeQL queries over HTTP at\n``https://<hostname>:<port>/branch/<branch-name>/edgeql``.\n\n\nAuthentication\n==============\n\n\nTo authenticate to your |Gel| Cloud instance, first create a secret key using\nthe Gel Cloud UI or :ref:`ref_cli_gel_cloud_secretkey_create`. Use the\nsecret key as your token with the bearer authentication method. Here is an\nexample showing how you might send the query ``select Person {*};`` using cURL:\n\n.. lint-off\n\n.. code-block:: bash\n\n    $ curl -G https://<cloud-instance-host>:<cloud-instance-port>/branch/main/edgeql \\\n       -H \"Authorization: Bearer <secret-key> \\\n       --data-urlencode \"query=select Person {*};\"\n\n.. lint-on\n\n\nUsage\n=====\n\nUsage of the HTTP and GraphQL APIs is identical on a |Gel| Cloud instance.\nReference the HTTP and GraphQL documentation for more information.\n\n\nHTTP\n----\n\n- :ref:`Overview <ref_edgeql_http>`\n- :ref:`ref_edgeql_protocol`\n- :ref:`ref_edgeql_http_health_checks`\n\n\nGraphQL\n-------\n\n- :ref:`Overview <ref_graphql_index>`\n- :ref:`ref_graphql_overview`\n- :ref:`ref_graphql_mutations`\n- :ref:`ref_graphql_introspection`\n- :ref:`ref_cheatsheet_graphql`\n"
  },
  {
    "path": "docs/cloud/index.rst",
    "content": ".. _ref_guide_cloud:\n\n=====\nCloud\n=====\n\n:edb-alt-title: Using Gel Cloud\n\n.. toctree::\n    :maxdepth: 2\n    :hidden:\n\n    cli\n    web\n    http_gql\n    deploy/index\n    deploy/vercel\n    deploy/netlify\n    deploy/fly\n    deploy/render\n    deploy/railway\n    migrate_from\n\n\n|Gel| Cloud is a fully managed, effortless cloud database service,\nengineered to let you deploy your database instantly and connect from\nanywhere with near-zero configuration.\n\nConnecting your app\n===================\n\nTry a guide for connecting your app running on your platform of choice:\n\n.. TODO: render these with icons\n\n* :ref:`Vercel <ref_guide_cloud_deploy_vercel>`\n* :ref:`Netlify <ref_guide_cloud_deploy_netlify>`\n* :ref:`Fly.io <ref_guide_cloud_deploy_fly>`\n* :ref:`Render <ref_guide_cloud_deploy_render>`\n* :ref:`Railway <ref_guide_cloud_deploy_railway>`\n\nTo connect your apps running on other platforms, generate a dedicated\nsecret key for your instance with :gelcmd:`cloud secretkey create` or via the\nweb UI's “Secret Keys” pane in your instance dashboard. Create two environment\nvariables accessible to your production application:\n\n* :gelenv:`SECRET_KEY` - contains the secret key you generated\n* :gelenv:`INSTANCE` - the name of your |Gel| Cloud instance (``<org-name>/<instance-name>``)\n\n\nTwo ways to use Gel Cloud\n=========================\n\n1. CLI\n^^^^^^\n\nLog in to |Gel| Cloud via the CLI:\n\n.. code-block:: bash\n\n  $ gel cloud login\n\n\nThis will open a browser window and allow you to log in via GitHub.\nNow, create your |Gel| Cloud instance the same way you would create a\nlocal instance:\n\n.. code-block:: bash\n\n  $ gel instance create <org-name>/<instance-name>\n\nor\n\n.. code-block:: bash\n\n  $ gel project init \\\n  --server-instance <org-name>/<instance-name>\n\n\n2. GUI\n^^^^^^\n\nCreate your instance at `cloud.geldata.com <https://cloud.geldata.com>`_ by\nclicking on “Create new instance” in the “Instances” tab.\n\n.. <div className={styles.cloudGuiImg} />\n\nComplete the following form to configure your instance. You can access\nyour instance via the CLI using the name ``<org-name>/<instance-name>`` or via the GUI.\n\n\nUseful Gel Cloud commands\n=========================\n\nGet REPL\n^^^^^^^^\n\n.. code-block:: bash\n\n  $ gel \\\n    -I <org-name>/<instance-name>\n\nRun migrations\n^^^^^^^^^^^^^^\n\n.. code-block:: bash\n\n  $ gel migrate \\\n    -I <org-name>/<instance-name>\n\nUpdate your instance\n^^^^^^^^^^^^^^^^^^^^\n\n.. code-block:: bash\n\n  $ gel instance upgrade \\\n    --to-version <target-version> \\\n    -I <org-name>/<instance-name>\n\nManual full backup\n^^^^^^^^^^^^^^^^^^\n\n.. code-block:: bash\n\n  $ gel dump \\\n    --all --format dir \\\n    -I <org-name>/<instance-name> \\\n    <local-dump-path>\n\nFull restore\n^^^^^^^^^^^^\n\n.. code-block:: bash\n\n  $ gel restore \\\n    --all \\\n    -I <org-name>/<instance-name> \\\n    <local-dump-path>\n\n.. note::\n\n  Restoring works only to an empty database.\n\n\nQuestions? Problems? Bugs?\n==========================\n\nThank you for helping us make the best way to host your |Gel| instances even\nbetter!\n\n* Please join us on `our Discord <https://discord.gg/umUueND6ag>`_  to ask\n  questions.\n* If you're experiencing a service interruption, check `our status page\n  <https://www.gelstatus.com/>`_ for information on what may be\n  causing it.\n* Report any bugs you find by `submitting a support ticket\n  <https://www.geldata.com/p/cloud-support>`_. Note: when using |Gel| Cloud\n  through the CLI, setting the ``RUST_LOG`` environment variable to ``info``,\n  ``debug``, or ``trace`` may provide additional debugging information\n  which will be useful to include with your ticket.\n"
  },
  {
    "path": "docs/cloud/migrate_from.rst",
    "content": ".. _ref_migrate_from:\n\n=======================================\nMigrating from Gel Cloud to Self-Hosted\n=======================================\n\n:edb-alt-title: Migrating from Gel Cloud to Self-Hosted Gel\n\n|Gel| Cloud is sunsetting at the end of January 2026. To ensure your\napplications continue to run smoothly, you should migrate your data to a\nself-hosted |Gel| instance.\n\nThis guide outlines the process of migrating your production data. We strongly\nrecommend performing a \"dry run\" with your staging or development environment\nfirst to familiarize yourself with the workflow.\n\n\nPhase 1: Preparation\n====================\n\n1. Spin up your self-hosted deployment\n--------------------------------------\n\nWhile you can host |Gel| on any infrastructure that supports Docker or binary\ninstallations, we recommend using a managed cloud provider (such as Fly.io,\nAWS, or GCP with managed Postgres) for production reliability.\n\nSee our :ref:`self-hosted deployment guides <ref_guide_deployment>` for\nstep-by-step instructions on deploying to various platforms.\n\nEnsure your new instance is:\n\n* Running the same version of |Gel| as your Cloud instance (or newer).\n* Configured with a persistent volume for data or connected to a managed\n  Postgres instance.\n\n2. Retrieve connection parameters\n---------------------------------\n\nOnce your new instance is live, you need its DSN (Data Source Name) or\nindividual connection parameters. Each :ref:`deployment guide\n<ref_guide_deployment>` outlines the best way to retrieve the various\nconnection parameters from your specific setup. You will typically need:\n\n* **Host**: The domain or IP of your new instance.\n* **Port**: Default is ``5656``.\n* **User**: Default is |admin|.\n* **Password**: The password you set during initialization.\n* **TLS CA**: The certificate used to secure the connection (unless using a\n  public CA or ``--trust-tls-cert``).\n\nThe DSN format is:\n\n.. code-block:: text\n\n    gel://<user>:<password>@<host>:<port>/<branch>\n\nAll components except the scheme are optional. See :ref:`ref_reference_connection_dsn`\nfor more details.\n\n\nPhase 2: The Migration (Cutover)\n================================\n\nTo ensure data consistency, you must prevent new writes to your database\nduring the transfer.\n\n1. Enable maintenance mode\n--------------------------\n\nBefore touching the data, put your application into maintenance mode. This\nensures that no new records are created in |Gel| Cloud after you've started\nthe dump.\n\n* **Web Apps**: Point your load balancer to a static \"Maintenance\" page.\n* **Background Jobs**: Stop all workers, cron jobs, or queues that interact\n  with the database.\n\n2. Perform the migration\n------------------------\n\nYou can use the |Gel| CLI to move data directly from your Cloud instance to\nyour new self-hosted instance.\n\n.. code-block:: bash\n\n    # 1. Dump from Gel Cloud to directory\n    $ gel dump --instance <org-name>/<instance-name> \\\n      --all --format=dir \\\n      production_dump\n\n    # 2. Restore to self-hosted from dump\n    $ gel restore --dsn <new_self_hosted_dsn> --all production_dump\n\n.. note::\n\n    If your self-hosted instance uses a self-signed TLS certificate, you may\n    need to add ``--tls-security insecure`` to the restore command, or first\n    retrieve the TLS certificate and set it via :gelenv:`TLS_CA`.\n\n\nPhase 3: Verification and Go-Live\n=================================\n\n1. Update application environment variables\n-------------------------------------------\n\nUpdate your application's configuration to point to the new instance. Replace\nthe Gel Cloud specific connection environment variables :gelenv:`INSTANCE` and\n:gelenv:`SECRET_KEY` variables with the new connection details:\n\n* :gelenv:`DSN`: :geluri:`user:password@host:port/branch`\n* :gelenv:`TLS_CA`: The TLS certificate content (if your instance uses a\n  self-signed certificate)\n* :gelenv:`CLIENT_TLS_SECURITY`: Set to ``insecure`` if you need to skip TLS\n  verification (not recommended for production)\n\n2. Sanity check\n---------------\n\nBefore turning off maintenance mode:\n\n* Run a few :gelcmd:`query` commands against the new instance to verify data\n  integrity.\n* Check that your schema migrated correctly: :gelcmd:`migrate --status`.\n* Launch a local instance of your app connected to the new production DB to\n  ensure connection logic is sound.\n\n3. Disable maintenance mode\n---------------------------\n\nOnce verified, restart your application servers and background workers.\nMonitor your logs closely for any connection or permission errors.\n\n\nPost-Migration Note\n===================\n\nOnce you are 100% certain your data is safe and your app is stable on the new\nhost, you can de-provision your |Gel| Cloud instance. Remember that all |Gel|\nCloud data will be deleted after the January 2026 deadline.\n"
  },
  {
    "path": "docs/cloud/web.rst",
    "content": ".. _ref_guide_cloud_web:\n\n=======\nWeb GUI\n=======\n\n:edb-alt-title: Using Gel Cloud via the web GUI\n\nIf you'd prefer, you can also manage your account via `the Gel Cloud\nweb-based GUI <https://cloud.geldata.com/>`_.\n\nThe first time you access the web UI, you will be prompted to log in. Once you\nlog in with your account, you'll be on the \"Instances\" tab of the front page\nwhich shows your instance list. The other two tabs allow you to manage your\norganization settings and billing.\n\nInstances\n---------\n\nIf this is your first time accessing Gel Cloud, this list will be empty. To\ncreate an instance, click \"Create new instance.\" This will pop up a modal\nallowing you to name your instance and specify the version of Gel and the\nregion for the instance.\n\nOnce the instance has been created, you'll see the instance dashboard which\nallows you to monitor your instance, navigate to the management page for its\nbranches, and create secret keys.\n\nYou'll also see instructions in the bottom-right for linking your |Gel| CLI to\nyour Gel Cloud account. You do this by running the CLI command\n:gelcmd:`cloud login`. This will make all of your Gel Cloud instances accessible via\nthe CLI. You can manage them just as you would other remote Gel instances.\n\nIf you want to manage a branch of your database, click through on the\ninstance's name from the top right of the instance dashboard. If you just\ncreated a database, the branch management view will be mostly empty except\nfor a button offering to create a sample branch. Once you have a schema\ncreated and some data in a database, this view will offer you similar tools to\nthose in our local UI.\n\nYou'll be able to access a REPL, edit complex queries or build them\ngraphically, inspect your schema, and browse your data.\n\nOrg Settings\n------------\n\nThis tab allows you to add GitHub organizations for which you are an admin.\nIf you don't see your organization's name here, you may need to update your\n`org settings`_ in GitHub to allow |Gel| Cloud to read your list of\norganizations, and then refresh the org list.\n\n.. lint-off\n\n.. _org setings:\n  https://docs.github.com/en/organizations/managing-oauth-access-to-your-organizations-data/approving-oauth-apps-for-your-organization\n\n.. lint-on\n\nBilling\n-------\n\nOn this page you can manage your account type and payment methods, and set your\nemail for receiving billing info. Optionally, you can also save your payment\ninfo using `Link <https://link.com/>`_, `Stripe's <https://stripe.com/>`_\nfast-checkout solution.\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# EdgeDB documentation build configuration file, created by\n# sphinx-quickstart on Wed Aug  3 17:58:14 2016.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n# sys.path.insert(0, os.path.abspath('.'))\n\n# -- General configuration ------------------------------------------------\n\n\n# If your documentation needs a minimal Sphinx version, state it here.\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.todo',\n    'sphinx.ext.viewcode',\n    'edb.tools.docs',\n    'sphinxcontrib.asyncio',\n    'sphinx.ext.intersphinx',\n    'sphinx_code_tabs',\n]\n\nintersphinx_mapping = {'python': ('https://docs.python.org/3', None)}\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = []\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The encoding of source files.\n# source_encoding = 'utf-8-sig'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'EdgeDB'\ncopyright = u'2016, magicstack'\nauthor = u'magicstack'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = '0.5.0'\n# The full version, including alpha/beta/rc tags.\nrelease = '0.5.0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = None\n\n# There are two options for replacing |today|: either, you set today to some\n# non-false value, then it is used:\n# today = ''\n# Else, today_fmt is used as the format for a strftime call.\n# today_fmt = '%B %d, %Y'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\nexclude_patterns = []\n\n# The reST default role (used for this markup: `text`) to use for all\n# documents.\n# default_role = None\n\n# If true, '()' will be appended to :func: etc. cross-reference text.\n# add_function_parentheses = True\n\n# If true, the current module name will be prepended to all description\n# unit titles (such as .. function::).\n# add_module_names = True\n\n# If true, sectionauthor and moduleauthor directives will be shown in the\n# output. They are ignored by default.\n# show_authors = False\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# A list of ignored prefixes for module index sorting.\n# modindex_common_prefix = []\n\n# If true, keep warnings as \"system message\" paragraphs in the built documents.\n# keep_warnings = False\n\nsuppress_warnings = ['image.not_readable']\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\nprimary_domain = None\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\nhtml_theme = 'alabaster'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n# html_theme_options = {}\n\n# Add any paths that contain custom themes here, relative to this directory.\n# html_theme_path = []\n\n# The name for this set of Sphinx documents.  If None, it defaults to\n# \"<project> v<release> documentation\".\n# html_title = None\n\n# A shorter title for the navigation bar.  Default is the same as html_title.\n# html_short_title = None\n\n# The name of an image file (relative to this directory) to place at the top\n# of the sidebar.\n# html_logo = None\n\n# The name of an image file (within the static path) to use as favicon of the\n# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32\n# pixels large.\n# html_favicon = None\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = []\n\n# Add any extra paths that contain custom files (such as robots.txt or\n# .htaccess) here, relative to this directory. These files are copied\n# directly to the root of the documentation.\n# html_extra_path = []\n\n# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,\n# using the given strftime format.\n# html_last_updated_fmt = '%b %d, %Y'\n\n# If true, SmartyPants will be used to convert quotes and dashes to\n# typographically correct entities.\n# html_use_smartypants = True\n\n# Custom sidebar templates, maps document names to template names.\nhtml_sidebars = {\n    '**': [\n        'globaltoc.html',\n        'searchbox.html',\n    ]\n}\n\n# Additional templates that should be rendered to pages, maps page names to\n# template names.\n# html_additional_pages = {}\n\n# If false, no module index is generated.\n# html_domain_indices = True\n\n# If false, no index is generated.\n# html_use_index = True\n\n# If true, the index is split into individual pages for each letter.\n# html_split_index = False\n\n# If true, links to the reST sources are added to the pages.\n# html_show_sourcelink = True\n\n# If true, \"Created using Sphinx\" is shown in the HTML footer. Default is True.\n# html_show_sphinx = True\n\n# If true, \"(C) Copyright ...\" is shown in the HTML footer. Default is True.\n# html_show_copyright = True\n\n# If true, an OpenSearch description file will be output, and all pages will\n# contain a <link> tag referring to it.  The value of this option must be the\n# base URL from which the finished HTML is served.\n# html_use_opensearch = ''\n\n# This is the file name suffix for HTML files (e.g. \".xhtml\").\n# html_file_suffix = None\n\n# Language to be used for generating the HTML full-text search index.\n# Sphinx supports the following languages:\n#   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'\n#   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'\n# html_search_language = 'en'\n\n# A dictionary with options for the search language support, empty by default.\n# Now only 'ja' uses this config value\n# html_search_options = {'type': 'default'}\n\n# The name of a javascript file (relative to the configuration directory) that\n# implements a search results scorer. If empty, the default will be used.\n# html_search_scorer = 'scorer.js'\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'EdgeDBdoc'\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'EdgeDB.tex', u'EdgeDB Documentation',\n     u'magicstack', 'manual'),\n]\n\n# The name of an image file (relative to this directory) to place at the top of\n# the title page.\n# latex_logo = None\n\n# For \"manual\" documents, if this is true, then toplevel headings are parts,\n# not chapters.\n# latex_use_parts = False\n\n# If true, show page references after internal links.\n# latex_show_pagerefs = False\n\n# If true, show URL addresses after external links.\n# latex_show_urls = False\n\n# Documents to append as an appendix to all manuals.\n# latex_appendices = []\n\n# If false, no module index is generated.\n# latex_domain_indices = True\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'edgedb', u'EdgeDB Documentation',\n     [author], 1)\n]\n\n# If true, show URL addresses after external links.\n# man_show_urls = False\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'EdgeDB', u'EdgeDB Documentation',\n     author, 'EdgeDB', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n# Documents to append as an appendix to all manuals.\n# texinfo_appendices = []\n\n# If false, no module index is generated.\n# texinfo_domain_indices = True\n\n# How to display URL addresses: 'footnote', 'no', or 'inline'.\n# texinfo_show_urls = 'footnote'\n\n# If true, do not generate a @detailmenu in the \"Top\" node's menu.\n# texinfo_no_detailmenu = False\n\n\n# config for srclink\n\nsrclink_project = 'https://github.com/edgedb/edgedb'\nsrclink_src_path = 'doc/'\nsrclink_branch = 'doc'\n"
  },
  {
    "path": "docs/index.rst",
    "content": ".. _index_toplevel:\n\n\n=================\nGel documentation\n=================\n\nWelcome to the |Gel| |version| documentation.\n\n\n.. !!! DO NOT CHANGE :maxdepth: egdedb.com/docs depends on it !!!\n.. toctree::\n    :maxdepth: 5\n    :includehidden:\n\n    intro/index\n    reference/index\n    resources/index\n    cloud/index\n"
  },
  {
    "path": "docs/intro/branches.rst",
    "content": ".. _ref_intro_branches:\n\n========\nBranches\n========\n\n|Gel's| branches make it easy to prototype app features that impact your\ndatabase schema, even in cases where those features are never released. You can\ncreate a branch in your Gel database that corresponds to a feature branch in\nyour VCS. When you're done, either :ref:`merge <ref_cli_gel_branch_merge>`\nthat branch into your main branch or :ref:`drop <ref_cli_gel_branch_drop>`\nit leaving your original schema intact.\n\n.. note::\n\n    The procedure we will describe should be adaptable to any VCS offering\n    branching and rebasing, but in order to make the examples concrete and\n    easy-to-follow, we'll be demonstrating how Gel branches interact with\n    Git branches. You may adapt these examples to your VCS of choice.\n\n\n1. Create a new feature branch\n------------------------------\n\nCreate a feature branch in your VCS and switch to it. Then, create and switch\nto a corresponding branch in Gel using the CLI.\n\n.. code-block:: bash\n\n    $ gel branch create feature\n    Creating branch 'feature'...\n    OK: CREATE BRANCH\n    $ gel branch switch feature\n    Switching from 'main' to 'feature'\n\n.. note::\n\n    You can alternatively create and switch in one shot using :gelcmd:`branch\n    switch -c feature`.\n\n\n2. Build your feature\n---------------------\n\nWrite your code and make any schema changes your feature requires.\n\n\n3. Pull any changes on \"main\"\n-----------------------------\n\n.. note::\n\n    This step is optional. If you know your |main| code branch is current and\n    all migrations in that code branch have already been applied to your\n    |main| database branch, feel free to skip it.\n\nWe need to make sure that merging our feature branch onto |main| is a simple\nfast-forward. The next two steps take care of that.\n\nSwitch back to your |main| code branch. Run ``git pull`` to pull down any new\nchanges. If any of these are schema changes, use :gelcmd:`branch switch main`\nto switch back to your |main| database branch and apply the new schema with\n:gelcmd:`migrate`.\n\nOnce this is done, you can switch back to your feature branches in your VCS and\n|Gel|.\n\n\n4. Rebase your feature branch on \"main\"\n---------------------------------------\n\n.. note::\n\n    If you skipped the previous step, you can skip this one too. This is only\n    necessary if you had to pull down new changes on |main|.\n\nFor your code branch, first make sure you're on ``feature`` and then run the\nrebase:\n\n.. code-block:: bash\n\n    $ git rebase main\n\nNow, do the same for your database, also from ``feature``:\n\n.. code-block:: bash\n\n    $ gel branch rebase main\n\n\n5. Merge ``feature`` onto \"main\"\n--------------------------------\n\nSwitch back to both |main| branches and merge ``feature``.\n\n.. code-block:: bash\n\n    $ git switch main\n    <changes>\n    Switched to branch 'main'\n    $ git merge feature\n\n.. code-block:: bash\n\n    $ gel branch switch main\n    Switching from 'feature' to 'main'\n    $ gel branch merge feature\n\nNow, your feature and its schema have been successfully merged! 🎉\n\n\nFurther reading\n^^^^^^^^^^^^^^^\n\n- :ref:`Branches CLI <ref_cli_gel_branch>`\n\nFurther information can be found in the `branches RFC\n<https://github.com/geldata/rfcs/blob/master/text/1025-branches.rst#rebasing-branches>`_,\nwhich describes the design of the migration system.\n"
  },
  {
    "path": "docs/intro/cli.rst",
    "content": ".. _ref_intro_cli:\n\n.. _ref_admin_install:\n\n=======\nThe CLI\n=======\n\nThe |gelcmd| command line tool is an integral part of the developer workflow\nof building with Gel. Below are instructions for installing it.\n\nInstallation\n------------\n\nTo get started with Gel, the first step is install the |gelcmd| CLI.\n\n**Linux or macOS**\n\n.. code-block:: bash\n\n    $ curl --proto '=https' --tlsv1.2 -sSf https://www.geldata.com/sh | sh\n\n**Windows Powershell**\n\n.. note::\n\n    Gel on Windows requires WSL 2 because the Gel server runs on Linux.\n\n.. code-block:: powershell\n\n    PS> iwr https://www.geldata.com/ps1 -useb | iex\n\nFollow the prompts on screen to complete the installation. The script will\ndownload the |gelcmd| command built for your OS and add a path to it to your\nshell environment. Then test the installation:\n\n.. code-block:: bash\n\n    $ gel --version\n    Gel CLI x.x+abcdefg\n\n.. note::\n\n  If you encounter a ``command not found`` error, you may need to open a fresh\n  shell window.\n\n\nSee ``help`` commands\n---------------------\n\nThe entire CLI is self-documenting. Once it's installed, run :gelcmd:`--help`\nto see a breakdown of all the commands and options.\n\n.. code-block:: bash\n\n  $ gel --help\n  Usage: gel [OPTIONS] [COMMAND]\n\n  Commands:\n    <list of commands>\n\n  Options:\n    <list of options>\n\n  Connection Options (gel --help-connect to see full list):\n    <list of connection options>\n\n  Cloud Connection Options:\n    <list of cloud connection options>\n\nThe majority of CLI commands perform some action against a *particular* Gel\ninstance. As such, there are a standard set of flags that are used to specify\n*which instance* should be the target of the command, plus additional\ninformation like TLS certificates. The following command documents these flags.\n\n.. code-block:: bash\n\n  $ gel --help-connect\n  Connection Options (full list):\n\n    -I, --instance <INSTANCE>\n            Instance name (use `gel instance list` to list local, remote and\n            Cloud instances available to you)\n\n        --dsn <DSN>\n            DSN for Gel to connect to (overrides all other options except\n            password)\n\n        --credentials-file <CREDENTIALS_FILE>\n            Path to JSON file to read credentials from\n\n    -H, --host <HOST>\n            Gel instance host\n\n    -P, --port <PORT>\n            Port to connect to Gel\n\n        --unix-path <UNIX_PATH>\n            A path to a Unix socket for Gel connection\n\n            When the supplied path is a directory, the actual path will be\n            computed using the `--port` and `--admin` parameters.\n    ...\n\nIf you ever want to see documentation for a particular command (\n:gelcmd:`migration create`) or group of commands (:gelcmd:`instance`),\njust append the ``--help`` flag.\n\n.. code-block:: bash\n\n  $ gel instance --help\n  Manage local Gel instances\n\n  Usage: gel instance <COMMAND>\n\n  Commands:\n    create          Initialize a new Gel instance\n    list            Show all instances\n    status          Show status of an instance\n    start           Start an instance\n    stop            Stop an instance\n    ...\n\nUpgrade the CLI\n---------------\n\nTo upgrade to the latest version:\n\n.. code-block:: bash\n\n  $ gel cli upgrade\n"
  },
  {
    "path": "docs/intro/clients.rst",
    "content": ".. _ref_intro_clients:\n\n================\nClient Libraries\n================\n\n|Gel| implements libraries for popular languages that make it easier to work\nwith Gel. These libraries provide a common set of functionality.\n\n- *Instantiating clients.* Most libraries implement a ``Client`` class that\n  internally manages a pool of physical connections to your Gel instance.\n- *Resolving connections.* All client libraries implement a standard protocol\n  for determining how to connect to your database. In most cases, this will\n  involve checking for special environment variables like :gelenv:`DSN` or, in\n  the case of Gel Cloud instances, :gelenv:`INSTANCE` and\n  :gelenv:`SECRET_KEY`.\n  (More on this in :ref:`the Connection section below\n  <ref_intro_clients_connection>`.)\n- *Executing queries.* A ``Client`` will provide some methods for executing\n  queries against your database. Under the hood, this query is executed using\n  Gel's efficient binary protocol.\n\n.. note::\n\n  For some use cases, you may not need a client library. Gel allows you to\n  execute :ref:`queries over HTTP <ref_edgeql_http>`. This is slower than the\n  binary protocol and lacks support for transactions and rich data types, but\n  may be suitable if a client library isn't available for your language of\n  choice.\n\nAvailable libraries\n===================\n\nTo execute queries from your application code, use one of :ref:`Gel's client\nlibraries <ref_clients_index>`.\n\nUsage\n=====\n\nTo follow along with the guide below, first create a new directory and\ninitialize a project.\n\n.. code-block:: bash\n\n  $ mydir myproject\n  $ cd myproject\n  $ gel project init\n\nConfigure the environment as needed for your preferred language.\n\n.. tabs::\n\n  .. code-tab:: bash\n    :caption: Node.js\n\n    $ npm init -y\n    $ tsc --init # (TypeScript only)\n    $ touch index.ts\n\n  .. code-tab:: bash\n    :caption: Deno\n\n    $ touch index.ts\n\n  .. code-tab:: bash\n    :caption: Python\n\n    $ python -m venv venv\n    $ source venv/bin/activate\n    $ touch main.py\n\n  .. code-tab:: bash\n    :caption: Rust\n\n    $ cargo init\n\n  .. code-tab:: bash\n    :caption: Go\n\n    $ go mod init example/quickstart\n    $ touch hello.go\n\n  .. code-tab:: bash\n    :caption: .NET\n\n    $ dotnet new console -o . -f net6.0\n\n\nInstall the Gel client library.\n\n.. tabs::\n\n  .. code-tab:: bash\n    :caption: Node.js\n\n    $ npm install gel    # npm\n    $ yarn add gel       # yarn\n\n  .. code-tab:: txt\n    :caption: Deno\n\n    n/a\n\n  .. code-tab:: bash\n    :caption: Python\n\n    $ pip install gel\n\n  .. code-tab:: toml\n    :caption: Rust\n\n    # Cargo.toml\n\n    [dependencies]\n    gel-tokio = \"0.5.0\"\n    # Additional dependency\n    tokio = { version = \"1.28.1\", features = [\"macros\", \"rt-multi-thread\"] }\n\n  .. code-tab:: bash\n    :caption: Go\n\n    $ go get github.com/geldata/gel-go\n\n  .. code-tab:: bash\n    :caption: .NET\n\n    $ dotnet add package Gel.Net.Driver\n\n\nCopy and paste the following simple script. This script initializes a\n``Client`` instance. Clients manage an internal pool of connections to your\ndatabase and provide a set of methods for executing queries.\n\n.. note::\n\n  Note that we aren't passing connection information (say, a connection\n  URL) when creating a client. The client libraries can detect that\n  they are inside a project directory and connect to the project-linked\n  instance automatically. For details on configuring connections, refer\n  to the :ref:`Connection <ref_intro_clients_connection>` section below.\n\n.. lint-off\n\n.. tabs::\n\n  .. code-tab:: typescript\n    :caption: Node.js\n\n    import {createClient} from 'gel';\n\n    const client = createClient();\n\n    client.querySingle(`select random()`).then((result) => {\n      console.log(result);\n    });\n\n\n  .. code-tab:: python\n\n    from gel import create_client\n\n    client = create_client()\n\n    result = client.query_single(\"select random()\")\n    print(result)\n\n  .. code-tab:: rust\n\n    // src/main.rs\n    #[tokio::main]\n    async fn main() {\n        let conn = gel_tokio::create_client()\n            .await\n            .expect(\"Client initiation\");\n        let val = conn\n            .query_required_single::<f64, _>(\"select random()\", &())\n            .await\n            .expect(\"Returning value\");\n        println!(\"Result: {}\", val);\n    }\n\n  .. code-tab:: go\n\n    // hello.go\n    package main\n\n    import (\n      \"context\"\n      \"fmt\"\n      \"log\"\n\n      \"github.com/geldata/gel-go\"\n    )\n\n    func main() {\n      ctx := context.Background()\n      client, err := gel.CreateClient(ctx, gel.Options{})\n      if err != nil {\n        log.Fatal(err)\n      }\n      defer client.Close()\n\n      var result float64\n      err = client.\n        QuerySingle(ctx, \"select random();\", &result)\n      if err != nil {\n        log.Fatal(err)\n      }\n\n      fmt.Println(result)\n    }\n\n  .. code-tab:: csharp\n    :caption: .NET\n\n    using Gel;\n\n    var client = new GelClient();\n    var result = await client.QuerySingleAsync<double>(\"select random();\");\n    Console.WriteLine(result);\n\n  .. code-tab:: elixir\n    :caption: Elixir\n\n    # lib/gel_quickstart.ex\n    defmodule GelQuickstart do\n      def run do\n        {:ok, client} = Gel.start_link()\n        result = Gel.query_single!(client, \"select random()\")\n        IO.inspect(result)\n      end\n    end\n\n.. lint-on\n\n\nFinally, execute the file.\n\n.. tabs::\n\n  .. code-tab:: bash\n    :caption: Node.js\n\n    $ npx tsx index.ts\n\n  .. code-tab:: bash\n    :caption: Deno\n\n    $ deno run --allow-all --unstable index.deno.ts\n\n  .. code-tab:: bash\n    :caption: Python\n\n    $ python index.py\n\n  .. code-tab:: bash\n    :caption: Rust\n\n    $ cargo run\n\n  .. code-tab:: bash\n    :caption: Go\n\n    $ go run .\n\n  .. code-tab:: bash\n    :caption: .NET\n\n    $ dotnet run\n\n  .. code-tab:: bash\n    :caption: Elixir\n\n    $ mix run -e GelQuickstart.run\n\nYou should see a random number get printed to the console. This number was\ngenerated inside your Gel instance using EdgeQL's built-in\n:eql:func:`random` function.\n\n.. _ref_intro_clients_connection:\n\nConnection\n==========\n\nAll client libraries implement a standard protocol for determining how to\nconnect to your database.\n\nUsing projects\n--------------\n\nIn development, we recommend :ref:`initializing a\nproject <ref_intro_projects>` in the root of your codebase.\n\n.. code-block:: bash\n\n  $ gel project init\n\nOnce the project is initialized, any code that uses an official client library\nwill automatically connect to the project-linked instance—no need for\nenvironment variables or hard-coded credentials. Follow the :ref:`Using\nprojects <ref_guide_using_projects>` guide to get started.\n\nUsing environment variables\n---------------------------\n\n.. _ref_intro_clients_connection_cloud:\n\nFor Gel Cloud\n^^^^^^^^^^^^^\n\nIn production, connection information can be securely passed to the client\nlibrary via environment variables. For Gel Cloud instances, the recommended\nvariables to set are :gelenv:`INSTANCE` and :gelenv:`SECRET_KEY`.\n\nSet :gelenv:`INSTANCE` to ``<org-name>/<instance-name>`` where\n``<instance-name>`` is the name you set when you created the Gel Cloud\ninstance.\n\nIf you have not yet created a secret key, you can do so in the Gel Cloud UI\nor by running :ref:`ref_cli_gel_cloud_secretkey_create` via the CLI.\n\nFor self-hosted instances\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\nMost commonly for self-hosted remote instances, you set a value for the\n:gelenv:`DSN` environment variable.\n\n.. note::\n\n  If environment variables like :gelenv:`DSN` are defined inside a project\n  directory, the environment variables will take precedence.\n\nA DSN is also known as a \"connection string\" and takes the\nfollowing form: :geluri:`<username>:<password>@<hostname>:<port>`.\n\n\nEach element of the DSN is optional; in fact |geluri| is a technically a\nvalid DSN. Any unspecified element will default to the following values.\n\n.. list-table::\n\n  * - ``<host>``\n    - ``localhost``\n  * - ``<port>``\n    - ``5656``\n  * - ``<user>``\n    - |admin|\n  * - ``<password>``\n    -  ``null``\n\nA typical DSN may look like this:\n:geluri:`admin:PASSWORD@db.domain.com:8080`.\n\nDSNs can also contain the following query parameters.\n\n.. list-table::\n\n  * - ``branch``\n    - The database branch to connect to within the given instance. Defaults to\n      |main|.\n\n  * - ``tls_security``\n    - The TLS security mode. Accepts the following values.\n\n      - ``\"strict\"`` (**default**) — verify certificates and hostnames\n      - ``\"no_host_verification\"`` — verify certificates only\n      - ``\"insecure\"`` — trust self-signed certificates\n\n  * - ``tls_ca_file``\n    - A filesystem path pointing to a CA root certificate. This is usually only\n      necessary when attempting to connect via TLS to a remote instance with a\n      self-signed certificate.\n\nThese parameters can be added to any DSN using web-standard query string\nnotation: :geluri:`user:pass@example.com:8080?branch=my_branch&tls_security=insecure`.\n\n\nFor a more comprehensive guide to DSNs, see the :ref:`DSN Specification\n<ref_dsn>`.\n\nUsing multiple environment variables\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nIf needed for your deployment pipeline, each element of the DSN can be\nspecified independently.\n\n- :gelenv:`HOST`\n- :gelenv:`PORT`\n- :gelenv:`USER`\n- :gelenv:`PASSWORD`\n- :gelenv:`BRANCH`\n- :gelenv:`TLS_CA_FILE`\n- :gelenv:`CLIENT_TLS_SECURITY`\n\n.. note::\n\n  If a value for :gelenv:`DSN` is defined, it will override these variables!\n\nOther mechanisms\n----------------\n\n:gelenv:`CREDENTIALS_FILE`\n  A path to a ``.json`` file containing connection information. In some\n  scenarios (including local Docker development) its useful to represent\n  connection information with files.\n\n  .. code-block:: json\n\n    {\n      \"host\": \"localhost\",\n      \"port\": 10700,\n      \"user\": \"testuser\",\n      \"password\": \"testpassword\",\n      \"branch\": \"main\",\n      \"tls_cert_data\": \"-----BEGIN CERTIFICATE-----\\nabcdef...\"\n    }\n\n:gelenv:`INSTANCE` (local/Gel Cloud only)\n  The name of an instance. Useful only for local or Gel Cloud instances.\n\n  .. note::\n\n      For more on Gel Cloud instances, see the :ref:`Gel Cloud instance\n      connection section <ref_intro_clients_connection_cloud>` above.\n\nReference\n---------\n\nThese are the most common ways to connect to an instance, however Gel\nsupports several other options for advanced use cases. For a complete reference\non connection configuration, see :ref:`Reference > Connection Parameters\n<ref_reference_connection>`.\n"
  },
  {
    "path": "docs/intro/edgeql.rst",
    "content": ".. _ref_intro_edgeql:\n\nEdgeQL\n======\n\nEdgeQL is the query language of Gel. It's intended as a spiritual successor\nto SQL that solves some of its biggest design limitations. This page is\nintended as a rapid-fire overview so you can hit the ground running with\n|Gel|. Refer to the linked pages for more in-depth documentation.\n\n\n.. note::\n\n  The examples below also demonstrate how to express the query with the\n  :ref:`TypeScript client's <gel-js-intro>` query builder, which lets you\n  express arbitrary EdgeQL queries in a code-first, typesafe way.\n\n\nScalar literals\n^^^^^^^^^^^^^^^\n\n|Gel| has a rich primitive type system consisting of the following data types.\n\n.. list-table::\n\n  * - Strings\n    - ``str``\n  * - Booleans\n    - ``bool``\n  * - Numbers\n    - ``int16`` ``int32`` ``int64`` ``float32`` ``float64``\n      ``bigint`` ``decimal``\n  * - UUID\n    - ``uuid``\n  * - JSON\n    - ``json``\n  * - Dates and times\n    - ``datetime`` ``cal::local_datetime`` ``cal::local_date``\n      ``cal::local_time``\n  * - Durations\n    - ``duration`` ``cal::relative_duration`` ``cal::date_duration``\n  * - Binary data\n    - ``bytes``\n  * - Auto-incrementing counters\n    - ``sequence``\n  * - Enums\n    - ``enum<x, y, z>``\n\nBasic literals can be declared using familiar syntax.\n\n.. tabs::\n\n  .. code-tab:: edgeql-repl\n\n    db> select \"I ❤️ EdgeQL\"; # str\n    {'U ❤️ EdgeQL'}\n    db> select false; # bool\n    {false}\n    db> select 42; # int64\n    {42}\n    db> select 3.14; # float64\n    {3.14}\n    db> select 12345678n; # bigint\n    {12345678n}\n    db> select 15.0e+100n;  # decimal\n    {15.0e+100n}\n    db> select b'bina\\\\x01ry'; # bytes\n    {b'bina\\\\x01ry'}\n\n\n  .. code-tab:: typescript\n\n    e.str(\"I ❤️ EdgeQL\")\n    // string\n    e.bool(false)\n    // boolean\n    e.int64(42)\n    // number\n    e.float64(3.14)\n    // number\n    e.bigint(BigInt(12345678))\n    // bigint\n    e.decimal(\"1234.4567\")\n    // n/a (not supported by JS clients)\n    e.bytes(Buffer.from(\"bina\\\\x01ry\"))\n    // Buffer\n\nOther type literals are declared by *casting* an appropriately\nstructured string.\n\n.. tabs::\n\n  .. code-tab:: edgeql-repl\n\n    db> select <uuid>'a5ea6360-75bd-4c20-b69c-8f317b0d2857';\n    {a5ea6360-75bd-4c20-b69c-8f317b0d2857}\n    db> select <datetime>'1999-03-31T15:17:00Z';\n    {<datetime>'1999-03-31T15:17:00Z'}\n    db> select <duration>'5 hours 4 minutes 3 seconds';\n    {<duration>'5:04:03'}\n    db> select <cal::relative_duration>'2 years 18 days';\n    {<cal::relative_duration>'P2Y18D'}\n\n\n  .. code-tab:: typescript\n\n    e.uuid(\"a5ea6360-75bd-4c20-b69c-8f317b0d2857\")\n    // string\n    e.datetime(\"1999-03-31T15:17:00Z\")\n    // Date\n    e.duration(\"5 hours 4 minutes 3 seconds\")\n    // gel.Duration (custom class)\n    e.cal.relative_duration(\"2 years 18 days\")\n    // gel.RelativeDuration (custom class)\n\nPrimitive data can be composed into arrays and tuples, which can themselves be\nnested.\n\n.. tabs::\n\n  .. code-tab:: edgeql-repl\n\n    db> select ['hello', 'world'];\n    {['hello', 'world']}\n    db> select ('Apple', 7, true);\n    {('Apple', 7, true)} # unnamed tuple\n    db> select (fruit := 'Apple', quantity := 3.14, fresh := true);\n    {(fruit := 'Apple', quantity := 3.14, fresh := true)} # named tuple\n    db> select <json>[\"this\", \"is\", \"an\", \"array\"];\n    {\"[\\\"this\\\", \\\"is\\\", \\\"an\\\", \\\"array\\\"]\"}\n\n  .. code-tab:: typescript\n\n    e.array([\"hello\", \"world\"]);\n    // string[]\n    e.tuple([\"Apple\", 7, true]);\n    // [string, number, boolean]\n    e.tuple({fruit: \"Apple\", quantity: 3.14, fresh: true});\n    // {fruit: string; quantity: number; fresh: boolean}\n    e.json([\"this\", \"is\", \"an\", \"array\"]);\n    // unknown\n\n\n|Gel| also supports a special ``json`` type for representing unstructured\ndata. Primitive data structures can be converted to JSON using a type cast\n(``<json>``). Alternatively, a properly JSON-encoded string can be converted\nto ``json`` with the built-in ``to_json`` function. Indexing a ``json`` value\nreturns another ``json`` value.\n\n.. code-tabs::\n\n  .. code-tab:: edgeql-repl\n\n    gel> select <json>5;\n    {\"5\"}\n    gel> select <json>[1,2,3];\n    {\"[1, 2, 3]\"}\n    gel> select to_json('[{ \"name\": \"Peter Parker\" }]');\n    {\"[{\\\"name\\\": \\\"Peter Parker\\\"}]\"}\n    gel> select to_json('[{ \"name\": \"Peter Parker\" }]')[0]['name'];\n    {\"\\\"Peter Parker\\\"\"}\n\n  .. code-tab:: typescript\n\n    /*\n      The result of an query returning `json` is represented\n      with `unknown` in TypeScript.\n    */\n    e.json(5);  // => unknown\n    e.json([1, 2, 3]);  // => unknown\n    e.to_json('[{ \"name\": \"Peter Parker\" }]');  // => unknown\n    e.to_json('[{ \"name\": \"Peter Parker\" }]')[0][\"name\"];  // => unknown\n\n\nRefer to :ref:`Docs > EdgeQL > Literals <ref_eql_literals>` for complete docs.\n\nFunctions and operators\n^^^^^^^^^^^^^^^^^^^^^^^\n\n|Gel| provides a rich standard library of functions to operate and manipulate\nvarious data types.\n\n.. tabs::\n\n  .. code-tab:: edgeql-repl\n\n    db> select str_upper('oh hi mark');\n    {'OH HI MARK'}\n    db> select len('oh hi mark');\n    {10}\n    db> select uuid_generate_v1mc();\n    {c68e3836-0d59-11ed-9379-fb98e50038bb}\n    db> select contains(['a', 'b', 'c'], 'd');\n    {false}\n\n  .. code-tab:: typescript\n\n    e.str_upper(\"oh hi mark\");\n    // string\n    e.len(\"oh hi mark\");\n    // number\n    e.uuid_generate_v1mc();\n    // string\n    e.contains([\"a\", \"b\", \"c\"], \"d\");\n    // boolean\n\nSimilarly, it provides a comprehensive set of built-in operators.\n\n.. tabs::\n\n  .. code-tab:: edgeql-repl\n\n    db> select not true;\n    {false}\n    db> select exists 'hi';\n    {true}\n    db> select 2 + 2;\n    {4}\n    db> select 'Hello' ++ ' world!';\n    {'Hello world!'}\n    db> select '😄' if true else '😢';\n    {'😄'}\n    db> select <duration>'5 minutes' + <duration>'2 hours';\n    {<duration>'2:05:00'}\n\n  .. code-tab:: typescript\n\n    e.op(\"not\", e.bool(true));\n    // booolean\n    e.op(\"exists\", e.set(\"hi\"));\n    // boolean\n    e.op(\"exists\", e.cast(e.str, e.set()));\n    // boolean\n    e.op(e.int64(2), \"+\", e.int64(2));\n    // number\n    e.op(e.str(\"Hello \"), \"++\", e.str(\"World!\"));\n    // string\n    e.op(e.str(\"😄\"), \"if\", e.bool(true), \"else\", e.str(\"😢\"));\n    // string\n    e.op(e.duration(\"5 minutes\"), \"+\", e.duration(\"2 hours\"))\n\nSee :ref:`Docs > Standard Library <ref_std>` for reference documentation on\nall built-in types, including the functions and operators that apply to them.\n\nInsert an object\n^^^^^^^^^^^^^^^^\n\nObjects are created using ``insert``. The ``insert`` statement relies on\ndeveloper-friendly syntax like curly braces and the ``:=`` operator.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    insert Movie {\n      title := 'Doctor Strange 2',\n      release_year := 2022\n    };\n\n  .. code-tab:: typescript\n\n    const query = e.insert(e.Movie, {\n      title: 'Doctor Strange 2',\n      release_year: 2022\n    });\n\n    const result = await query.run(client);\n    // {id: string}\n    // by default INSERT only returns\n    // the id of the new object\n\nSee :ref:`Docs > EdgeQL > Insert <ref_eql_insert>`.\n\nNested inserts\n^^^^^^^^^^^^^^\n\nOne of EdgeQL's greatest features is that it's easy to compose. Nested inserts\nare easily achieved with subqueries.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    insert Movie {\n      title := 'Doctor Strange 2',\n      release_year := 2022,\n      director := (insert Person {\n        name := 'Sam Raimi'\n      })\n    };\n\n  .. code-tab:: typescript\n\n    const query = e.insert(e.Movie, {\n      title: 'Doctor Strange 2',\n      release_year: 2022,\n      director: e.insert(e.Person, {\n        name: 'Sam Raimi'\n      })\n    });\n\n    const result = await query.run(client);\n    // {id: string}\n    // by default INSERT only returns\n    // the id of the new object\n\nSelect objects\n^^^^^^^^^^^^^^\n\nUse a *shape* to define which properties to ``select`` from the given object\ntype.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    select Movie {\n      id,\n      title\n    };\n\n  .. code-tab:: typescript\n\n    const query = e.select(e.Movie, () => ({\n      id: true,\n      title: true\n    }));\n    const result = await query.run(client);\n    // {id: string; title: string; }[]\n\n    // To select all properties of an object, use the\n    // spread operator with the special \"*\"\" property:\n    const query = e.select(e.Movie, () => ({\n      ...e.Movie['*']\n    }));\n\nFetch linked objects with a nested shape.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    select Movie {\n      id,\n      title,\n      actors: {\n        name\n      }\n    };\n\n  .. code-tab:: typescript\n\n    const query = e.select(e.Movie, () => ({\n      id: true,\n      title: true,\n      actors: {\n        name: true,\n      }\n    }));\n\n    const result = await query.run(client);\n    // {id: string; title: string, actors: {name: string}[]}[]\n\nSee :ref:`Docs > EdgeQL > Select > Shapes <ref_eql_shapes>`.\n\nFiltering, ordering, and pagination\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nThe ``select`` statement can be augmented with ``filter``, ``order by``,\n``offset``, and ``limit`` clauses (in that order).\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    select Movie {\n      id,\n      title\n    }\n    filter .release_year > 2017\n    order by .title\n    offset 10\n    limit 10;\n\n  .. code-tab:: typescript\n\n    const query = e.select(e.Movie, (movie) => ({\n      id: true,\n      title: true,\n      filter: e.op(movie.release_year, \">\", 1999),\n      order_by: movie.title,\n      offset: 10,\n      limit: 10,\n    }));\n\n    const result = await query.run(client);\n    // {id: string; title: number}[]\n\nNote that you reference properties of the object to include in your ``select``\nby prepending the property name with a period: ``.release_year``. This is known\nas *leading dot notation*.\n\nEvery new set of curly braces introduces a new scope. You can add ``filter``,\n``limit``, and ``offset`` clauses to nested shapes.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    select Movie {\n      title,\n      actors: {\n        name\n      } filter .name ilike 'chris%'\n    }\n    filter .title ilike '%avengers%';\n\n  .. code-tab:: typescript\n\n    e.select(e.Movie, movie => ({\n      title: true,\n      characters: c => ({\n        name: true,\n        filter: e.op(c.name, \"ilike\", \"chris%\"),\n      }),\n      filter: e.op(movie.title, \"ilike\", \"%avengers%\"),\n    }));\n    // => { characters: { name: string; }[]; title: string; }[]\n\n    const result = await query.run(client);\n    // {id: string; title: number}[]\n\n\n\nSee :ref:`Filtering <ref_eql_select_filter>`, :ref:`Ordering\n<ref_eql_select_order>`, and :ref:`Pagination <ref_eql_select_pagination>`.\n\nQuery composition\n^^^^^^^^^^^^^^^^^\n\nWe've seen how to ``insert`` and ``select``. How do we do both in one query?\nAnswer: query composition. EdgeQL's syntax is designed to be *composable*,\nlike any good programming language.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    select (\n      insert Movie { title := 'The Marvels' }\n    ) {\n      id,\n      title\n    };\n\n  .. code-tab:: typescript\n\n    const newMovie = e.insert(e.Movie, {\n      title: \"The Marvels\"\n    });\n    const query = e.select(newMovie, () => ({\n      id: true,\n      title: true\n    }));\n\n    const result = await query.run(client);\n    // {id: string; title: string}\n\nWe can clean up this query by pulling out the ``insert`` statement into a\n``with`` block. A ``with`` block is useful for composing complex multi-step\nqueries, like a script.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    with new_movie := (insert Movie { title := 'The Marvels' })\n    select new_movie {\n      id,\n      title\n    };\n\n  .. code-tab:: typescript\n\n    /*\n      Same as above.\n\n      In the query builder, explicit ``with`` blocks aren't necessary!\n      Just assign your EdgeQL subqueries to variables and compose them as you\n      like. The query builder automatically convert your top-level query to an\n      EdgeQL expression with proper ``with`` blocks.\n    */\n\nComputed properties\n^^^^^^^^^^^^^^^^^^^\n\nSelection shapes can contain computed properties.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    select Movie {\n      title,\n      title_upper := str_upper(.title),\n      cast_size := count(.actors)\n    };\n\n  .. code-tab:: typescript\n\n    e.select(e.Movie, movie => ({\n      title: true,\n      title_upper: e.str_upper(movie.title),\n      cast_size: e.count(movie.actors)\n    }))\n    // {title: string; title_upper: string; cast_size: number}[]\n\nA common use for computed properties is to query a link in reverse; this is\nknown as a *backlink* and it has special syntax.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    select Person {\n      name,\n      acted_in := .<actors[is Content] {\n        title\n      }\n    };\n\n  .. code-tab:: typescript\n\n    e.select(e.Person, person => ({\n      name: true,\n      acted_in: e.select(person[\"<actors[is Content]\"], () => ({\n        title: true,\n      })),\n    }));\n    // {name: string; acted_in: {title: string}[];}[]\n\nSee :ref:`Docs > EdgeQL > Select > Computed fields <ref_eql_select_computeds>` and\n:ref:`Docs > EdgeQL > Select > Backlinks <ref_eql_select_backlinks>`.\n\nUpdate objects\n^^^^^^^^^^^^^^\n\nThe ``update`` statement accepts a ``filter`` clause up-front, followed by a\n``set`` shape indicating how the matching objects should be updated.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    update Movie\n    filter .title = \"Doctor Strange 2\"\n    set {\n      title := \"Doctor Strange in the Multiverse of Madness\"\n    };\n\n  .. code-tab:: typescript\n\n    const query = e.update(e.Movie, (movie) => ({\n      filter: e.op(movie.title, '=', 'Doctor Strange 2'),\n      set: {\n        title: 'Doctor Strange in the Multiverse of Madness',\n      },\n    }));\n\n    const result = await query.run(client);\n    // {id: string}\n\nWhen updating links, the set of linked objects can be added to with ``+=``,\nsubtracted from with ``-=``, or overwritten with ``:=``.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    update Movie\n    filter .title = \"Doctor Strange 2\"\n    set {\n      actors += (select Person filter .name = \"Rachel McAdams\")\n    };\n\n  .. code-tab:: typescript\n\n    e.update(e.Movie, (movie) => ({\n      filter: e.op(movie.title, '=', 'Doctor Strange 2'),\n      set: {\n        actors: {\n          \"+=\": e.select(e.Person, person => ({\n            filter: e.op(person.name, \"=\", \"Rachel McAdams\")\n          }))\n        }\n      },\n    }));\n\nSee :ref:`Docs > EdgeQL > Update <ref_eql_update>`.\n\nDelete objects\n^^^^^^^^^^^^^^\n\nThe ``delete`` statement can contain ``filter``, ``order by``, ``offset``, and\n``limit`` clauses.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    delete Movie\n    filter .title ilike \"the avengers%\"\n    limit 3;\n\n  .. code-tab:: typescript\n\n    const query = e.delete(e.Movie, (movie) => ({\n      filter: e.op(movie.title, 'ilike', \"the avengers%\"),\n    }));\n\n    const result = await query.run(client);\n    // {id: string}[]\n\nSee :ref:`Docs > EdgeQL > Delete <ref_eql_delete>`.\n\nQuery parameters\n^^^^^^^^^^^^^^^^\n\nYou can reference query parameters in your queries with ``$<name>`` notation.\nSince EdgeQL is a strongly typed language, all query parameters must be\nprepending with a *type cast* to indicate the expected type.\n\n.. note::\n\n  Scalars like ``str``, ``int64``, and ``json`` are\n  supported. Tuples, arrays, and object types are not.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    insert Movie {\n      title := <str>$title,\n      release_year := <int64>$release_year\n    };\n\n  .. code-tab:: typescript\n\n    const query = e.params({ title: e.str, release_year: e.int64 }, ($) => {\n      return e.insert(e.Movie, {\n        title: $.title,\n        release_year: $.release_year,\n      }))\n    };\n\n    const result = await query.run(client, {\n      title: 'Thor: Love and Thunder',\n      release_year: 2022,\n    });\n    // {id: string}\n\nAll client libraries provide a dedicated API for specifying parameters when\nexecuting a query.\n\n.. tabs::\n\n  .. code-tab:: javascript\n\n    import {createClient} from \"gel\";\n\n    const client = createClient();\n    const result = await client.query(`select <str>$param`, {\n      param: \"Play it, Sam.\"\n    });\n    // => \"Play it, Sam.\"\n\n  .. code-tab:: python\n\n    import gel\n\n    client = gel.create_async_client()\n\n    async def main():\n        result = await client.query(\"select <str>$param\", param=\"Play it, Sam\")\n        # => \"Play it, Sam\"\n\n  .. code-tab:: go\n\n    package main\n\n    import (\n        \"context\"\n        \"log\"\n\n        \"github.com/geldata/gel-go\"\n    )\n\n    func main() {\n        ctx := context.Background()\n        client, err := gel.CreateClient(ctx, gel.Options{})\n        if err != nil {\n            log.Fatal(err)\n        }\n        defer client.Close()\n\n        var (\n            param     string = \"Play it, Sam.\"\n            result  string\n        )\n\n        query := \"select <str>$0\"\n        err = client.Query(ctx, query, &result, param)\n        // ...\n    }\n\n  .. code-tab:: rust\n\n    // [dependencies]\n    // gel-tokio = \"0.5.0\"\n    // tokio = { version = \"1.28.1\", features = [\"macros\", \"rt-multi-thread\"] }\n\n    #[tokio::main]\n    async fn main() {\n        let conn = gel_tokio::create_client()\n            .await\n            .expect(\"Client initiation\");\n        let param = \"Play it, Sam.\";\n        let val = conn\n            .query_required_single::<String, _>(\"select <str>$0\", &(param,))\n            .await\n            .expect(\"Returning value\");\n        println!(\"{val}\");\n    }\n\nSee :ref:`Docs > EdgeQL > Parameters <ref_eql_params>`.\n\nSubqueries\n^^^^^^^^^^\n\nUnlike SQL, EdgeQL is *composable*; queries can be naturally nested. This is\nuseful, for instance, when performing nested mutations.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    with\n      dr_strange := (select Movie filter .title = \"Doctor Strange\"),\n      benedicts := (select Person filter .name in {\n        'Benedict Cumberbatch',\n        'Benedict Wong'\n      })\n    update dr_strange\n    set {\n      actors += benedicts\n    };\n\n  .. code-tab:: typescript\n\n    // select Doctor Strange\n    const drStrange = e.select(e.Movie, movie => ({\n      filter: e.op(movie.title, '=', \"Doctor Strange\")\n    }));\n\n    // select actors\n    const actors = e.select(e.Person, person => ({\n      filter: e.op(person.name, 'in', e.set(\n        'Benedict Cumberbatch',\n        'Benedict Wong'\n      ))\n    }));\n\n    // add actors to cast of drStrange\n    const query = e.update(drStrange, ()=>({\n      actors: { \"+=\": actors }\n    }));\n\nWe can also use subqueries to fetch properties of an object we just inserted.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n     with new_movie := (insert Movie {\n       title := \"Avengers: The Kang Dynasty\",\n       release_year := 2025\n     })\n     select new_movie {\n      title, release_year\n    };\n\n  .. code-tab:: typescript\n\n    // \"with\" blocks are added automatically\n    // in the generated query!\n\n    const newMovie = e.insert(e.Movie, {\n      title: \"Avengers: The Kang Dynasty\",\n      release_year: 2025\n    });\n\n    const query = e.select(newMovie, ()=>({\n      title: true,\n      release_year: true,\n    }));\n\n    const result = await query.run(client);\n    // {title: string; release_year: number;}\n\nSee :ref:`Docs > EdgeQL > Select > Subqueries <ref_eql_select_subqueries>`.\n\nPolymorphic queries\n^^^^^^^^^^^^^^^^^^^\n\nConsider the following schema.\n\n.. code-block:: sdl\n\n    abstract type Content {\n      required title: str;\n    }\n\n    type Movie extending Content {\n      release_year: int64;\n    }\n\n    type TVShow extending Content {\n      num_seasons: int64;\n    }\n\nWe can ``select`` the abstract type ``Content`` to simultaneously fetch all\nobjects that extend it, and use the ``[is <type>]`` syntax to select\nproperties from known subtypes.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    select Content {\n      title,\n      [is TVShow].num_seasons,\n      [is Movie].release_year\n    };\n\n  .. code-tab:: typescript\n\n    const query = e.select(e.Content, (content) => ({\n      title: true,\n      ...e.is(e.Movie, {release_year: true}),\n      ...e.is(e.TVShow, {num_seasons: true}),\n    }));\n    /* {\n      title: string;\n      release_year: number | null;\n      num_seasons: number | null;\n    }[] */\n\nSee :ref:`Docs > EdgeQL > Select > Polymorphic queries\n<ref_eql_select_polymorphic>`.\n\nGrouping objects\n^^^^^^^^^^^^^^^^\n\nUnlike SQL, EdgeQL provides a top-level ``group`` statement to compute\ngroupings of objects.\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    group Movie { title, actors: { name }}\n    by .release_year;\n\n  .. code-tab:: typescript\n\n    e.group(e.Movie, (movie) => {\n      const release_year = movie.release_year;\n      return {\n        title: true,\n        by: {release_year},\n      };\n    });\n    /* {\n      grouping: string[];\n      key: { release_year: number | null };\n      elements: { title: string; }[];\n    }[] */\n\nSee :ref:`Docs > EdgeQL > Group <ref_eql_group>`.\n"
  },
  {
    "path": "docs/intro/guides/ai/edgeql.rst",
    "content": ".. _ref_ai_guide_edgeql:\n\n================\nGel AI in EdgeQL\n================\n\n:edb-alt-title: How to set up Gel AI in EdgeQL\n\n\n|Gel| AI brings vector search capabilities and retrieval-augmented generation\ndirectly into the database.\n\n\nEnable and configure the extension\n==================================\n\n.. edb:split-section::\n\n    AI is a |Gel| extension. To enable it, we will need to add the extension\n    to the app’s schema:\n\n    .. code-block:: sdl\n\n        using extension ai;\n\n\n.. edb:split-section::\n\n    |Gel| AI uses external APIs in order to get vectors and LLM completions. For it\n    to work, we need to configure an API provider and specify their API key. Let's\n    open EdgeQL REPL and run the following query:\n\n    .. code-block:: edgeql\n\n        configure current database\n        insert ext::ai::OpenAIProviderConfig {\n          secret := 'sk-....',\n        };\n\n\nNow our |Gel| application can take advantage of OpenAI's API to implement AI\ncapabilities.\n\n\n.. note::\n\n   |Gel| AI comes with its own :ref:`UI <ref_ai_extai_reference_ui>` that can\n   be used to configure providers, set up prompts and test them in a sandbox.\n\n\n.. note::\n\n   Most API providers require you to set up and account and charge money for\n   model use.\n\n\nAdd vectors and perform similarity search\n=========================================\n\n.. edb:split-section::\n\n    Before we start introducing AI capabilities, let's set up our database with a\n    schema and populate it with some data (we're going to be helping Komi-san keep\n    track of her friends).\n\n    .. code-block:: sdl\n\n        module default {\n            type Friend {\n                required name: str {\n                    constraint exclusive;\n                };\n\n                summary: str;               # A brief description of personality and role\n                relationship_to_komi: str;  # Relationship with Komi\n                defining_trait: str;        # Primary character trait or quirk\n            }\n        }\n\n.. edb:split-section::\n\n    Here's a shell command you can paste and run that will populate the\n    database with some sample data.\n\n    .. code-block:: bash\n        :class: collapsible\n\n        $ cat << 'EOF' > populate_db.edgeql\n        insert Friend {\n            name := 'Tadano Hitohito',\n            summary := 'An extremely average high school boy with a remarkable ability to read the atmosphere and understand others\\' feelings, especially Komi\\'s.',\n            relationship_to_komi := 'First friend and love interest',\n            defining_trait := 'Perceptiveness',\n        };\n\n        insert Friend {\n            name := 'Osana Najimi',\n            summary := 'An extremely outgoing person who claims to have been everyone\\'s childhood friend. Gender: Najimi.',\n            relationship_to_komi := 'Second friend and social catalyst',\n            defining_trait := 'Universal childhood friend',\n        };\n\n        insert Friend {\n            name := 'Yamai Ren',\n            summary := 'An intense and sometimes obsessive classmate who is completely infatuated with Komi.',\n            relationship_to_komi := 'Self-proclaimed guardian and admirer',\n            defining_trait := 'Obsessive devotion',\n        };\n\n        insert Friend {\n            name := 'Katai Makoto',\n            summary := 'A intimidating-looking but shy student who shares many communication problems with Komi.',\n            relationship_to_komi := 'Fellow communication-challenged friend',\n            defining_trait := 'Scary appearance but gentle nature',\n        };\n\n        insert Friend {\n            name := 'Nakanaka Omoharu',\n            summary := 'A self-proclaimed wielder of dark powers who acts like an anime character and is actually just a regular gaming enthusiast.',\n            relationship_to_komi := 'Gaming buddy and chuunibyou friend',\n            defining_trait := 'Chuunibyou tendencies',\n        };\n        EOF\n        $ gel query -f populate_db.edgeql\n\n\n.. edb:split-section::\n\n    In order to get |Gel| to produce embedding vectors, we need to create a special\n    ``deferred index`` on the type we would like to perform similarity search on.\n    More specifically, we need to specify an EdgeQL expression that produces a\n    string that we're going to create an embedding vector for. This is how we would\n    set up an index if we wanted to perform similarity search on\n    ``Friend.summary``:\n\n    .. code-block:: sdl-diff\n\n          module default {\n              type Friend {\n                  required name: str {\n                      constraint exclusive;\n                  };\n\n                  summary: str;               # A brief description of personality and role\n                  relationship_to_komi: str;  # Relationship with Komi\n                  defining_trait: str;        # Primary character trait or quirk\n\n        +         deferred index ext::ai::index(embedding_model := 'text-embedding-3-small')\n        +             on (.summary);\n              }\n          }\n\n\n.. edb:split-section::\n\n    But actually, in our case it would be better if we could similarity search\n    across all properties at the same time. We can define the index on a more\n    complex expression - like a concatenation of string properties - like this:\n\n\n    .. code-block:: sdl-diff\n\n          module default {\n              type Friend {\n                  required name: str {\n                      constraint exclusive;\n                  };\n\n                  summary: str;               # A brief description of personality and role\n                  relationship_to_komi: str;  # Relationship with Komi\n                  defining_trait: str;        # Primary character trait or quirk\n\n                  deferred index ext::ai::index(embedding_model := 'text-embedding-3-small')\n        -             on (.summary);\n        +             on (\n        +                 .name ++ ' ' ++ .summary ++ ' '\n        +                 ++ .relationship_to_komi ++ ' '\n        +                 ++ .defining_trait\n        +             );\n              }\n          }\n\n\n.. edb:split-section::\n\n    Once we're done with schema modification, we need to apply them by going\n    through a migration:\n\n    .. code-block:: bash\n\n        $ gel migration create\n        $ gel migrate\n\n\n.. edb:split-section::\n\n    That's it! |Gel| will make necessary API requests in the background and create an\n    index that will enable us to perform efficient similarity search like this:\n\n    .. code-block:: edgeql\n\n        select ext::ai::search(Friend, query_vector);\n\n\n.. edb:split-section::\n\n    Note that this function accepts an embedding vector as the second argument, not\n    a text string. This means that in order to similarity search for a string, we\n    need to create a vector embedding for it using the same model as we used to\n    create the index. |Gel| offers an HTTP endpoint ``/ai/embeddings`` that can\n    handle it for us. All we need to do is to pass the vector it produces into the\n    search query:\n\n    .. note::\n\n        Note that we're passing our login and password in order to autheticate the\n        request. We can find those using the CLI: :gelcmd:`instance credentials\n        --json`. Learn about all the other ways you can authenticate a request\n        :ref:`here <ref_http_auth>`.\n\n    .. code-block:: bash\n\n        $ curl --user user:password \\\n          --json '{\"input\": \"Who helps Komi make friends?\", \"model\": \"text-embedding-3-small\"}' \\\n          http://localhost:<port>/branch/main/ai/embeddings \\\n          | jq -r '.data[0].embedding' \\                                                    # extract the embedding out of the JSON\n          | tr -d '\\n' \\                                                                    # remove newlines\n          | sed 's/^\\[//;s/\\]$//' \\                                                         # remove square brackets\n          | awk '{print \"select ext::ai::search(Friend, <array<float32>>[\" $0 \"]);\"}' \\     # assemble the query\n          | gel query --file -  # pass the query into Gel CLI\n\n\n\nUse the built-in RAG\n====================\n\nOne more feature |Gel| AI offers is built-in retrieval-augmented generation, also\nknown as RAG.\n\n.. edb:split-section::\n\n    |Gel| comes preconfigured to be able to process our text query, perform\n    similarity search across the index we just created, pass the results to an LLM\n    and return a response. We can access the built-in RAG using the ``/ai/rag``\n    HTTP endpoint:\n\n\n    .. code-block:: bash\n\n        $ curl --user user:password --json '{\n            \"query\": \"Who helps Komi make friends?\",\n            \"model\": \"gpt-4-turbo-preview\",\n            \"context\": {\"query\":\"select Friend\"}\n          }' http://localhost:<port>/branch/main/ai/rag\n\n\n.. edb:split-section::\n\n    We can also stream the response like this:\n\n\n    .. code-block:: bash-diff\n\n          $ curl --user user:password --json '{\n              \"query\": \"Who helps Komi make friends?\",\n              \"model\": \"gpt-4-turbo-preview\",\n              \"context\": {\"query\":\"select Friend\"},\n        +     \"stream\": true,\n            }' http://localhost:<port>/branch/main/ai/rag\n\n\nKeep going!\n===========\n\nYou are now sufficiently equipped to use |Gel| AI in your applications.\n\nIf you'd like to build something on your own, make sure to check out the\n:ref:`Reference manual <ref_ai_extai_reference>` in order to learn the details\nabout using different APIs and models, configuring prompts or using the UI.\nMake sure to also check out the |Gel| AI bindings in :ref:`Python\n<ref_ai_python_reference>` and :ref:`JavaScript <ref_ai_javascript_reference>`\nif those languages are relevant to you.\n\nAnd if you would like more guidance for how |Gel| AI can be fit into an\napplication, take a look at the :ref:`FastAPI Gel AI Tutorial\n<ref_guide_fastapi_gelai_searchbot>`, where we're building a search bot using\nfeatures you learned about above.\n\n"
  },
  {
    "path": "docs/intro/guides/ai/index.rst",
    "content": ".. edb:env-switcher::\n\n=========\nAdding AI\n=========\n\n.. toctree::\n  :maxdepth: 1\n  :hidden:\n\n  edgeql\n  python"
  },
  {
    "path": "docs/intro/guides/ai/python.rst",
    "content": ".. _ref_ai_guide_python:\n\n================\nGel AI in Python\n================\n\n:edb-alt-title: How to set up Gel AI in Python\n\n.. edb:split-section::\n\n    |Gel| AI brings vector search capabilities and retrieval-augmented\n    generation directly into the database. It's integrated into the |Gel|\n    Python binding via the ``gel.ai`` module.\n\n    .. code-block:: bash\n\n      $ pip install 'gel[ai]'\n\n\nEnable and configure the extension\n==================================\n\n.. edb:split-section::\n\n    AI is an |Gel| extension. To enable it, we will need to add the extension\n    to the app’s schema:\n\n    .. code-block:: sdl\n\n        using extension ai;\n\n\n.. edb:split-section::\n\n    |Gel| AI uses external APIs in order to get vectors and LLM completions.\n    For it to work, we need to configure an API provider and specify their API\n    key. Let's open EdgeQL REPL and run the following query:\n\n    .. code-block:: edgeql\n\n        configure current database\n        insert ext::ai::OpenAIProviderConfig {\n          secret := 'sk-....',\n        };\n\n\nNow our |Gel| application can take advantage of OpenAI's API to implement AI\ncapabilities.\n\n\n.. note::\n\n   |Gel| AI comes with its own :ref:`UI <ref_ai_extai_reference_ui>` that can\n   be used to configure providers, set up prompts and test them in a sandbox.\n\n\n.. note::\n\n   Most API providers require you to set up and account and charge money for\n   model use.\n\n\nAdd vectors\n===========\n\n.. edb:split-section::\n\n    Before we start introducing AI capabilities, let's set up our database with a\n    schema and populate it with some data (we're going to be helping Komi-san keep\n    track of her friends).\n\n    .. code-block:: sdl\n\n        module default {\n            type Friend {\n                required name: str {\n                    constraint exclusive;\n                };\n\n                summary: str;               # A brief description of personality and role\n                relationship_to_komi: str;  # Relationship with Komi\n                defining_trait: str;        # Primary character trait or quirk\n            }\n        }\n\n.. edb:split-section::\n\n    Here's a shell command you can paste and run that will populate the\n    database with some sample data.\n\n    .. code-block:: bash\n        :class: collapsible\n\n        $ cat << 'EOF' > populate_db.edgeql\n        insert Friend {\n            name := 'Tadano Hitohito',\n            summary := 'An extremely average high school boy with a remarkable ability to read the atmosphere and understand others\\' feelings, especially Komi\\'s.',\n            relationship_to_komi := 'First friend and love interest',\n            defining_trait := 'Perceptiveness',\n        };\n\n        insert Friend {\n            name := 'Osana Najimi',\n            summary := 'An extremely outgoing person who claims to have been everyone\\'s childhood friend. Gender: Najimi.',\n            relationship_to_komi := 'Second friend and social catalyst',\n            defining_trait := 'Universal childhood friend',\n        };\n\n        insert Friend {\n            name := 'Yamai Ren',\n            summary := 'An intense and sometimes obsessive classmate who is completely infatuated with Komi.',\n            relationship_to_komi := 'Self-proclaimed guardian and admirer',\n            defining_trait := 'Obsessive devotion',\n        };\n\n        insert Friend {\n            name := 'Katai Makoto',\n            summary := 'A intimidating-looking but shy student who shares many communication problems with Komi.',\n            relationship_to_komi := 'Fellow communication-challenged friend',\n            defining_trait := 'Scary appearance but gentle nature',\n        };\n\n        insert Friend {\n            name := 'Nakanaka Omoharu',\n            summary := 'A self-proclaimed wielder of dark powers who acts like an anime character and is actually just a regular gaming enthusiast.',\n            relationship_to_komi := 'Gaming buddy and chuunibyou friend',\n            defining_trait := 'Chuunibyou tendencies',\n        };\n        EOF\n        $ gel query -f populate_db.edgeql\n\n\n.. edb:split-section::\n\n    In order to get |Gel| to produce embedding vectors, we need to create a\n    special ``deferred index`` on the type we would like to perform similarity\n    search on. More specifically, we need to specify an EdgeQL expression that\n    produces a string that we're going to create an embedding vector for. This\n    is how we would set up an index if we wanted to perform similarity search\n    on ``Friend.summary``:\n\n    .. code-block:: sdl-diff\n\n          module default {\n              type Friend {\n                  required name: str {\n                      constraint exclusive;\n                  };\n\n                  summary: str;               # A brief description of personality and role\n                  relationship_to_komi: str;  # Relationship with Komi\n                  defining_trait: str;        # Primary character trait or quirk\n\n        +         deferred index ext::ai::index(embedding_model := 'text-embedding-3-small')\n        +             on (.summary);\n              }\n          }\n\n\n.. edb:split-section::\n\n    But actually, in our case it would be better if we could similarity search\n    across all properties at the same time. We can define the index on a more\n    complex expression - like a concatenation of string properties - like this:\n\n\n    .. code-block:: sdl-diff\n\n          module default {\n              type Friend {\n                  required name: str {\n                      constraint exclusive;\n                  };\n\n                  summary: str;               # A brief description of personality and role\n                  relationship_to_komi: str;  # Relationship with Komi\n                  defining_trait: str;        # Primary character trait or quirk\n\n                  deferred index ext::ai::index(embedding_model := 'text-embedding-3-small')\n        -             on (.summary);\n        +             on (\n        +                 .name ++ ' ' ++ .summary ++ ' '\n        +                 ++ .relationship_to_komi ++ ' '\n        +                 ++ .defining_trait\n        +             );\n              }\n          }\n\n\n.. edb:split-section::\n\n    Once we're done with schema modification, we need to apply them by going\n    through a migration:\n\n    .. code-block:: bash\n\n        $ gel migration create\n        $ gel migrate\n\n\nThat's it! |Gel| will make necessary API requests in the background and create an\nindex that will enable us to perform efficient similarity search.\n\n\nPerform similarity search in Python\n===================================\n\n.. edb:split-section::\n\n    In order to run queries against the index we just created, we need to create a\n    |Gel| client and pass it to a |Gel| AI instance.\n\n    .. code-block:: python\n\n        import gel\n        import gel.ai\n\n        gel_client = gel.create_client()\n        gel_ai = gel.ai.create_rag_client(client)\n\n        text = \"Who helps Komi make friends?\"\n        vector = gel_ai.generate_embeddings(\n            text,\n            \"text-embedding-3-small\",\n        )\n\n        gel_client.query(\n            \"select ext::ai::search(Friend, <array<float32>>$embedding_vector\",\n            embedding_vector=vector,\n        )\n\n\n.. edb:split-section::\n\n    We are going to execute a query that calls a single function:\n    ``ext::ai::search(<type>, <search_vector>)``. That function accepts an\n    embedding vector as the second argument, not a text string. This means that in\n    order to similarity search for a string, we need to create a vector embedding\n    for it using the same model as we used to create the index. The |Gel| AI binding\n    in Python comes with a ``generate_embeddings`` function that does exactly that:\n\n\n    .. code-block:: python-diff\n\n          import gel\n          import gel.ai\n\n          gel_client = gel.create_client()\n          gel_ai = gel.ai.create_rag_client(client)\n\n        + text = \"Who helps Komi make friends?\"\n        + vector = gel_ai.generate_embeddings(\n        +     text,\n        +     \"text-embedding-3-small\",\n        + )\n\n\n.. edb:split-section::\n\n    Now we can plug that vector directly into our query to get similarity search\n    results:\n\n\n    .. code-block:: python-diff\n\n          import gel\n          import gel.ai\n\n          gel_client = gel.create_client()\n          gel_ai = gel.ai.create_rag_client(client)\n\n          text = \"Who helps Komi make friends?\"\n          vector = gel_ai.generate_embeddings(\n              text,\n              \"text-embedding-3-small\",\n          )\n\n        + gel_client.query(\n        +     \"select ext::ai::search(Friend, <array<float32>>$embedding_vector\",\n        +     embedding_vector=vector,\n        + )\n\n\nUse the built-in RAG\n====================\n\nOne more feature |Gel| AI offers is built-in retrieval-augmented generation,\nalso known as RAG.\n\n.. edb:split-section::\n\n    |Gel| comes preconfigured to be able to process our text query, perform\n    similarity search across the index we just created, pass the results to an\n    LLM and return a response. In order to access the built-in RAG, we need to\n    start by selecting an LLM and passing its name to the |Gel| AI instance\n    constructor:\n\n\n    .. code-block:: python-diff\n\n          import gel\n          import gel.ai\n\n          gel_client = gel.create_client()\n          gel_ai = gel.ai.create_rag_client(\n              client,\n        +     model=\"gpt-4-turbo-preview\"\n          )\n\n\n.. edb:split-section::\n\n    Now we can access the RAG using the ``query_rag`` function like this:\n\n\n    .. code-block:: python-diff\n\n          import gel\n          import gel.ai\n\n          gel_client = gel.create_client()\n          gel_ai = gel.ai.create_rag_client(\n              client,\n              model=\"gpt-4-turbo-preview\"\n          )\n\n        + gel_ai.query_rag(\n        +     \"Who helps Komi make friends?\",\n        +     context=\"Friend\",\n        + )\n\n\n.. edb:split-section::\n\n    We can also stream the response like this:\n\n\n    .. code-block:: python-diff\n\n          import gel\n          import gel.ai\n\n          gel_client = gel.create_client()\n          gel_ai = gel.ai.create_rag_client(\n              client,\n              model=\"gpt-4-turbo-preview\"\n          )\n\n        - gel_ai.query_rag(\n        + gel_ai.stream_rag(\n              \"Who helps Komi make friends?\",\n              context=\"Friend\",\n          )\n\n\nKeep going!\n===========\n\nYou are now sufficiently equipped to use |Gel| AI in your applications.\n\nIf you'd like to build something on your own, make sure to check out the\n:ref:`Reference manual <ref_ai_extai_reference>` for the AI extension in order\nto learn the details about using different APIs and models, configuring prompts\nor using the UI. Make sure to take a look at the :ref:`Python binding reference\n<ref_ai_python_reference>`, too.\n\nAnd if you would like more guidance for how |Gel| AI can be fit into an\napplication, take a look at the :ref:`FastAPI Gel AI Tutorial\n<ref_guide_fastapi_gelai_searchbot>`, where we're building a search bot using\nfeatures you learned about above.\n\n\n"
  },
  {
    "path": "docs/intro/guides/drizzle/index.rst",
    "content": ".. edb:env-switcher::\n\n==================\nAdding Drizzle ORM\n==================\n\n.. toctree::\n  :maxdepth: 1\n  :hidden:\n\n  nextjs\n"
  },
  {
    "path": "docs/intro/guides/drizzle/nextjs.rst",
    "content": ".. _ref_guide_gel_drizzle:\n\n======================\nDrizzle ORM in Next.js\n======================\n\n|Gel| integrates seamlessly with Drizzle ORM, providing a type-safe and intuitive way to interact with your database in TypeScript applications.\n\nEnable Drizzle in your Gel project\n==================================\n\n.. edb:split-section::\n\n    To integrate Drizzle with your Gel project, you'll need to install the necessary dependencies:\n\n    .. code-block:: bash\n\n        $ npm install drizzle-orm\n        $ npm install -D drizzle-kit\n\n\n.. edb:split-section::\n\n    Next, create a Drizzle configuration file in your project root to tell Drizzle how to work with your Gel database:\n\n    .. code-block:: typescript\n        :caption: drizzle.config.ts\n\n        import { defineConfig } from 'drizzle-kit';\n\n        export default defineConfig({\n          dialect: 'gel',\n        });\n\n\nSync your Gel schema with Drizzle\n=================================\n\n.. edb:split-section::\n\n    Before using Drizzle with your Gel database, you'll need to let Drizzle introspect your schema. This step generates TypeScript files that Drizzle can use to interact with your database.\n\n    .. code-block:: bash\n\n        $ npx drizzle-kit pull\n\n\n.. edb:split-section::\n\n    This command will create a schema file based on your Gel database. The file will typically look something like this:\n\n    .. code-block:: typescript\n        :caption: drizzle/schema.ts\n        :class: collapsible\n\n        import { gelTable, uniqueIndex, uuid, smallint, text, timestamp, relations } from \"drizzle-orm/gel-core\"\n        import { sql } from \"drizzle-orm\"\n\n        export const books = gelTable(\"Book\", {\n          id: uuid().default(sql`uuid_generate_v4()`).primaryKey().notNull(),\n          title: text().notNull(),\n          author: text(),\n          year: smallint(),\n          genre: text(),\n          read_date: timestamp(),\n        }, (table) => [\n          uniqueIndex(\"books_pkey\").using(\"btree\", table.id.asc().nullsLast().op(\"uuid_ops\")),\n        ]);\n\n        export const notes = gelTable(\"Note\", {\n          id: uuid().default(sql`uuid_generate_v4()`).primaryKey().notNull(),\n          text: text().notNull(),\n          created_at: timestamp().default(sql`datetime_current()`),\n          book_id: uuid().notNull(),\n        }, (table) => [\n          uniqueIndex(\"notes_pkey\").using(\"btree\", table.id.asc().nullsLast().op(\"uuid_ops\")),\n        ]);\n\n\nKeep Drizzle in sync with Gel\n=============================\n\n.. edb:split-section::\n\n    To keep your Drizzle schema in sync with your Gel schema, add a hook to your ``gel.toml`` file. This hook will automatically run ``drizzle-kit pull`` after each migration:\n\n    .. code-block:: toml\n        :caption: gel.toml\n\n        [hooks]\n        after_migration_apply = [\n          \"npx drizzle-kit pull\"\n        ]\n\n\n\nWith this hook in place, your Drizzle schema will automatically update whenever you apply Gel migrations.\n\n\nCreate a database client\n========================\n\n.. edb:split-section::\n\n    Now, let's create a database client that you can use throughout your application:\n\n    .. code-block:: typescript\n        :caption: src/db/index.ts\n\n        import { drizzle } from 'drizzle-orm/gel';\n        import { createClient } from 'gel-js';\n        import * as schema from '@/drizzle/schema';\n        import * as relations from '@/drizzle/relations';\n\n        // Import our schema\n        import * as schema from './schema';\n\n        // Initialize Gel client\n        const gelClient = createClient();\n\n        // Create Drizzle instance\n        export const db = drizzle({\n          client: gelClient,\n          schema: {\n            ...schema,\n            ...relations\n          },\n        });\n\n        // Helper types for use in our application\n        export type Book = typeof schema.book.$inferSelect;\n        export type NewBook = typeof schema.book.$inferInsert;\n\n        export type Note = typeof schema.note.$inferSelect;\n        export type NewNote = typeof schema.note.$inferInsert;\n\n\nPerform database operations with Drizzle\n========================================\n\nFor more detailed information on querying and other operations, refer to the `Drizzle documentation <https://orm.drizzle.team/docs/rqb>`_. Below are some examples of common database operations you can perform with Drizzle.\n\n.. edb:split-section::\n\n    Drizzle provides a clean, type-safe API for database operations. Here are some examples of common operations:\n\n    **Selecting data:**\n\n    .. code-block:: typescript\n\n        // Get all books with their notes\n        const allBooks = await db.query.book.findMany({\n          with: {\n            notes: true,\n          },\n        });\n\n        // Get a specific book\n        const book = await db.query.book.findFirst({\n          where: eq(books.id, id),\n          with: { notes: true },\n        });\n\n\n.. edb:split-section::\n\n    **Inserting data:**\n\n    .. code-block:: typescript\n\n      // Insert a new book\n      const newBook = await db.insert(book).values({\n        title: 'The Great Gatsby',\n        author: 'F. Scott Fitzgerald',\n        year: 1925,\n        genre: 'Novel',\n      }).returning();\n\n      // Insert a note for a book\n      const newNote = await db.insert(note).values({\n        text: 'A classic novel about the American Dream',\n        book_id: newBook.bookId,\n      }).returning();\n\n    **Bulk inserting data:**\n\n    .. code-block:: typescript\n\n      // Insert multiple books at once\n      const newBooks = await db.insert(book).values([\n        {\n          title: '1984',\n          author: 'George Orwell',\n          year: 1949,\n          genre: 'Dystopian',\n        },\n        {\n          title: 'To Kill a Mockingbird',\n          author: 'Harper Lee',\n          year: 1960,\n          genre: 'Fiction',\n        },\n        {\n          title: 'Pride and Prejudice',\n          author: 'Jane Austen',\n          year: 1813,\n          genre: 'Romance',\n        },\n      ]).returning();\n\n.. edb:split-section::\n\n    **Updating data:**\n\n    .. code-block:: typescript\n\n        // Update a book\n        const updatedBook = await db.update(book)\n          .set({\n            title: 'Updated Title',\n            author: 'Updated Author',\n          })\n          .where(eq(books.id, bookId))\n          .returning();\n\n\n.. edb:split-section::\n\n    **Deleting data:**\n\n    .. code-block:: typescript\n\n        // Delete a note\n        await db.delete(notes).where(eq(notes.id, noteId));\n\n\nUsing Drizzle with Next.js\n==========================\n\n.. edb:split-section::\n\n    In a Next.js application, you can use your Drizzle client in API routes and server components. Here's an example of an API route that gets all books:\n\n    .. code-block:: typescript\n        :caption: src/app/api/books/route.ts\n\n        import { NextResponse } from 'next/server';\n        import { db } from '@/db';\n\n        export async function GET() {\n          try {\n            const allBooks = await db.query.book.findMany({\n              with: { notes: true },\n            });\n\n            return NextResponse.json(allBooks);\n          } catch (error) {\n            console.error('Error fetching books:', error);\n            return NextResponse.json(\n              { error: 'Failed to fetch books' },\n              { status: 500 }\n            );\n          }\n        }\n\n\n.. edb:split-section::\n\n    And here's an example of using Drizzle in a server component:\n\n    .. code-block:: typescript\n        :caption: src/app/books/page.tsx\n\n        import { db } from '@/db';\n        import BookCard from '@/components/BookCard';\n\n        export default async function BooksPage() {\n          const books = await db.query.book.findMany({\n            with: { notes: true },\n          });\n\n          return (\n            <div>\n              {books.map((book) => (\n                <BookCard key={book.id} book={book} />\n              ))}\n            </div>\n          );\n        }\n\n\nKeep going!\n===========\n\nYou are now ready to use Gel with Drizzle in your applications. This integration gives you the best of both worlds: Gel's powerful features and Drizzle's type-safe, intuitive API.\n\nFor a complete example of using Gel with Drizzle in a Next.js application, check out our `Book Notes app example <https://github.com/geldata/gel-examples/tree/main/drizzle-book-notes-app>`_.\n\nYou can also find a detailed tutorial on building a Book Notes app with Gel, Drizzle, and Next.js in our :ref:`documentation <ref_guide_gel_drizzle_booknotes>`.\n"
  },
  {
    "path": "docs/intro/guides/index.rst",
    "content": "======\nGuides\n======\n\n.. toctree::\n  :maxdepth: 1\n\n  ai/index\n  drizzle/index\n"
  },
  {
    "path": "docs/intro/index.rst",
    "content": ".. _ref_intro:\n\n==============\nWelcome to Gel\n==============\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    installation\n    quickstart/index\n    tutorials/index\n    guides/index\n    localdev\n    cli\n    instances\n    projects\n    schema\n    migrations\n    branches\n    edgeql\n    clients\n\n|Gel| is a next-generation `graph-relational database\n<https://www.geldata.com/blog/the-graph-relational-database-defined>`_ designed\nas a spiritual successor to the relational database.\n\nIt inherits the strengths of SQL databases: type safety, performance,\nreliability, and transactionality. But instead of modeling data in a\nrelational (tabular) way, Gel represents data with *object types*\ncontaining *properties* and *links* to other objects. It leverages\nthis object-oriented model to provide a superpowered query language that\nsolves some of SQL's biggest usability problems.\n\nHow to read the docs\n^^^^^^^^^^^^^^^^^^^^\n\n|Gel| is a complex system, but we've structured the documentation so you can\nlearn it in \"phases\". You only need to learn as much as you need to start\nbuilding your application.\n\n- **Get Started** —\n  Start with the :ref:`quickstart <ref_quickstart>`. It walks\n  through Gel's core workflows: how to install Gel, create an instance,\n  write a simple schema, execute a migration, write some simple queries, and\n  use the client libraries. The rest of the section goes deeper on each of\n  these subjects.\n\n- **Schema** —\n  A set of pages that break down the concepts of syntax of Gel's schema\n  definition language (SDL). This starts with a rundown of Gel's primitive\n  type system (:ref:`Primitives <ref_datamodel_primitives>`), followed by a\n  description of (:ref:`Object Types <ref_datamodel_object_types>`) and the\n  things they can contain: links, properties, indexes, access policies, and\n  more.\n\n- **EdgeQL** —\n  A set of pages that break down Gel's query language, EdgeQL. It starts\n  with a rundown of how to declare :ref:`literal values <ref_eql_literals>`,\n  then introduces some key EdgeQL concepts like sets, paths, and type casts.\n  With the basics established, it proceeds to break down all of EdgeQL's\n  top-level statements: ``select``, ``insert``, and so on.\n\n- **Guides** —\n  Contains collections of guides on topics that are peripheral to Gel\n  itself: how to deploy to various cloud providers, how to integrate with\n  various frameworks, and how to introspect the schema to build\n  code-generation tools on top of Gel.\n\n- **Standard Library** —\n  This section contains an encyclopedic breakdown of Gel's built-in types\n  and the functions/operators that can be used with them. We didn't want to \\\n  clutter the **EdgeQL** section with all the nitty-gritty on each of these.\n  If you're looking for a particular function (say, a ``replace``), go to the\n  Standard Library page for the relevant type (in this case, :ref:`String\n  <ref_std_string>`), and peruse the table for what you're looking for\n  (:eql:func:`str_replace`).\n\n- **Client Libraries**\n  The documentation for Gel's set of official client libraries for\n  JavaScript/TypeScript, Python, Go, and Rust. All client libraries implement\n  Gel's binary protocol and provide a standard interface for executing\n  queries. If you're using another language, you can execute queries\n  :ref:`over HTTP <ref_edgeql_http>`.  This section also includes\n  documentation for Gel's :ref:`GraphQL <ref_graphql_overview>` endpoint.\n\n- **CLI**\n  Complete reference for the |gelcmd| command-line tool. The CLI is\n  self-documenting—add the ``--help`` flag after any command to print the\n  relevant documentation—so you shouldn't need to reference this section often.\n\n- **Reference**\n  The *Reference* section contains a complete breakdown of Gel's *syntax*\n  (for both EdgeQL and SDL), *internals* (like the binary protocol and dump\n  file format), and *configuration settings*. Usually you'll only need to\n  reference these once you're an advanced user.\n\n- **Changelog**\n  Detailed changelogs for each successive version of Gel, including any\n  breaking changes, new features, bigfixes, and links to\n\n\nTooling\n^^^^^^^\n\nTo actually build apps with Gel, you'll need to know more than SDL and\nEdgeQL.\n\n- **CLI** —\n  The most commonly used CLI functionality is covered in the :ref:`Quickstart\n  <ref_quickstart>`. For additional details, we have dedicated guides for\n  :ref:`Migrations <ref_intro_migrations>` and :ref:`Projects\n  <ref_guide_using_projects>`. A full CLI reference is available under\n  :ref:`CLI <ref_cli_overview>`.\n\n- **Client Libraries** —\n  To actually execute queries, you'll use one of our client libraries for\n  JavaScript, Go, or Python; find your preferred library under :ref:`Client\n  Libraries <ref_clients_index>`. If you're using another language, you can\n  still use Gel! You can execute :ref:`queries via HTTP <ref_edgeql_http>`.\n- **Deployment** —\n  To publish a Gel-backed application, you'll need to deploy Gel. Refer\n  to :ref:`Guides > Deployment <ref_guide_deployment>` for step-by-step\n  deployment guides for all major cloud hosting platforms, as well as\n  instructions for self-hosting with Docker.\n\n.. .. eql:react-element:: DocsNavTable\n\n|Gel| features:\n\n.. class:: ticklist\n\n- strict, strongly typed schema;\n- powerful and clean query language;\n- ability to easily work with complex hierarchical data;\n- built-in support for schema migrations.\n\n|Gel| is not a graph database: the data is stored and queried using\nrelational database techniques.  Unlike most graph databases, Gel\nmaintains a strict schema.\n\n|Gel| is not a document database, but inserting and querying hierarchical\ndocument-like data is trivial.\n\n|Gel| is not a traditional object database, despite the classification,\nit is not an implementation of OOP persistence.\n\n"
  },
  {
    "path": "docs/intro/install_table.rst",
    "content": ".. tabs::\n\n  .. code-tab:: bash\n    :caption: bash\n\n    $ curl https://www.geldata.com/sh --proto \"=https\" -sSf1 | sh\n\n  .. code-tab:: powershell\n    :caption: Powershell\n\n    PS> irm https://www.geldata.com/ps1 | iex\n\n  .. code-tab:: bash\n    :caption: Homebrew\n\n    $ brew install geldata/tap/gel-cli\n\n  .. code-tab:: bash\n    :caption: Nixpkgs\n\n    $ nix-shell -p gel\n\n  .. code-tab:: bash\n    :caption: JavaScript\n\n    $ npx gel --version\n\n  .. code-tab:: bash\n    :caption: Python\n\n    $ uvx gel --version\n\n"
  },
  {
    "path": "docs/intro/installation.rst",
    "content": ".. _ref_cli_gel_install:\n\n============\nInstallation\n============\n\nWe provide a :ref:`CLI for managing and interacting with local and remote databases <ref_cli_overview>`. If you're using JavaScript or Python, our client libraries will handle downloading and running the CLI for you using tools like ``npx`` and ``uvx``.\n\nFor everyone else, or if you wish to install the CLI globally, you can install using our bash installer or your operating system's package manager.\n\n.. include:: ./install_table.rst"
  },
  {
    "path": "docs/intro/instances.rst",
    "content": ".. _ref_intro_instances:\n\n=========\nInstances\n=========\n\nLet's get to the good stuff. You can spin up a Gel instance with a single\ncommand.\n\n.. code-block:: bash\n\n  $ gel instance create my_instance\n\nThis creates a new instance named ``my_instance`` that runs the latest stable\nversion of Gel. (Gel itself will be automatically installed if it isn't\nalready.) Alternatively you can specify a specific version with\n``--version``.\n\n.. code-block:: bash\n\n  $ gel instance create my_instance --version 6.1\n  $ gel instance create my_instance --version nightly\n\nWe can execute a query against our new instance with :gelcmd:`query`. Specify\nwhich instance to connect to by passing an instance name into the ``-I`` flag.\n\n.. code-block:: bash\n\n  $ gel query \"select 3.14\" -I my_instance\n  3.14\n\nManaging instances\n^^^^^^^^^^^^^^^^^^\nInstances can be stopped, started, restarted, and destroyed.\n\n.. code-block:: bash\n\n  $ gel instance stop -I my_instance\n  $ gel instance start -I my_instance\n  $ gel instance restart -I my_instance\n  $ gel instance destroy -I my_instance\n\n\nListing instances\n^^^^^^^^^^^^^^^^^\n\nTo list all instances on your machine:\n\n.. code-block:: bash\n\n  $ gel instance list\n  ┌────────┬──────────────────┬──────────┬────────────────┬──────────┐\n  │ Kind   │ Name             │ Port     │ Version        │ Status   │\n  ├────────┼──────────────────┼──────────┼────────────────┼──────────┤\n  │ local  │ my_instance      │ 10700    │ x.x+cc4f3b5    │ active   │\n  │ local  │ my_instance_2    │ 10701    │ x.x+cc4f3b5    │ active   │\n  │ local  │ my_instance_3    │ 10702    │ x.x+cc4f3b5    │ active   │\n  └────────┴──────────────────┴──────────┴────────────────┴──────────┘\n\nFurther reference\n^^^^^^^^^^^^^^^^^\n\nFor complete documentation on managing instances with the CLI (upgrading,\nviewing logs, etc.), refer to the :ref:`gel instance\n<ref_cli_gel_instance>` reference or view the help text in your shell:\n\n.. code-block:: bash\n\n  $ gel instance --help\n"
  },
  {
    "path": "docs/intro/localdev.rst",
    "content": "=================\nLocal Development\n=================\n\nOne of Gel's most powerful features is its seamless support for local development. The Gel CLI makes it incredibly easy to spin up a local instance, manage it, access GUI, and iterate on your schema quickly and safely. This guide outlines the flexible options available for your local development workflow.\n\nIf you're using JavaScript or Python, our client libraries will automatically handle the installation for you using tools like ``npx`` and ``uvx``. For other environments or to install the CLI globally, you can use one of the following methods:\n\n.. include:: ./install_table.rst\n\n\nInitialize your local instance\n==============================\n\nIt's easy to get started with a local Gel instance. Navigate to the root of your project repository and run:\n\n.. code-block:: bash\n\n  $ gel init\n\nCreates a database tied to the current directory and to the :ref:`gel.toml <ref_reference_gel_toml>` file in it. This simplifies connection configuration and installation for you. Alias for :ref:`gel project init <ref_cli_gel_project_init>`.\n\nTo conserve resources, Gel automatically puts inactive local development instances to sleep. This means you can have multiple instances running without them draining your system's resources when not in use.\n\nIterate on your schema\n======================\n\nGel simplifies the process of evolving your data model. You can apply changes from your Gel schema files directly to your running local instance without needing to create a separate migration file for every minor adjustment.\n\nThere are two primary ways to apply schema changes during development:\n\n1. **Automatic updates with** :gelcmd:`watch --migrate`\n    For a hands-off approach, you can use the watch command. This starts a process that monitors your Gel schema files for changes and automatically migrates your local instance as soon as you save them:\n\n    .. code-block:: bash\n\n      $ gel watch --migrate\n\n    This is ideal for rapid iteration when you want to see your schema changes reflected immediately.\n\n2. **Manual updates with** :gelcmd:`migrate --dev-mode`\n    If you prefer more explicit control, or don't want a background process running, you can apply schema changes manually:\n\n    .. code-block:: bash\n\n      $ gel migrate --dev-mode\n\n    This command performs the same action as the watch --migrate mode—applying the current state of your schema files to the local instance—but only when you explicitly run it.\n\nFinalizing changes\n==================\n\nOnce you're satisfied with the schema changes you've made iteratively, you'll want to create a migration file which will be committed to version control and shared with others. This new migration file will encapsulate all the modifications made since your last migration.\n\n1. **Create the migration file**\n    .. code-block:: bash\n\n      $ gel migration create\n\n    This command inspects the differences between your last migration file and the current state of your database schema, then generates a new migration file reflecting these changes.\n\n1. **Align your local instance**\n    After creating the migration, run the following command to ensure your local instance's migration history is aligned with this new migration. You can do this by running:\n\n    .. code-block:: bash\n\n      $ gel migrate --dev-mode\n\n    This command effectively \"fast-forwards\" your local instance. From its perspective, it will appear as though all the iterative changes were applied as part of this single, new migration. This keeps your local development environment consistent with the migration history you'll use in other environments (like staging or production).\n\nUndoing destructive changes\n===========================\n\nMistakes happen! You might accidentally make a destructive schema change. Fortunately, Gel has your back. Every time you migrate your schema (either via :gelcmd:`watch --migrate` or :gelcmd:`migrate --dev-mode`), a backup of your local instance is automatically taken.\nThis behavior can be disabled by setting env variable ``GEL_AUTO_BACKUP_MODE`` to ``disabled``.\n\nIf you need to roll back to a previous state:\n\n1. **Stop any active migration processes**: Ensure :gelcmd:`watch --migrate` is not running.\n\n2. **Find the backup ID**: Look through your shell's scrollback history. You'll find messages indicating backups were made, along with their IDs. Identify the ID of the backup created before the destructive change. You can also use the :gelcmd:`instance listbackups` command to list all backups for this instance.\n\n3. **Restore the instance**\n    .. code-block:: bash\n\n      $ gel instance restore <backup-id> -I <your-local-instance-name>\n\n    Replace <backup-id> with the actual ID and <your-local-instance-name> with the name of your instance (e.g., my_project). This will restore both your data and schema to the state at that backup point.\n\nOnce restored, you can make the intended schema changes and then restart :gelcmd:`watch --migrate` or use :gelcmd:`migrate --dev-mode` as preferred.\n\nKeeping code in sync\n====================\n\nMany Gel language bindings offer code generation capabilities (e.g., query builders, typed query functions). This generated code needs to stay synchronized with your schema. Gel provides a system of hooks and watchers that you can configure in your |gel.toml| file to automate this.\n\nThese hooks can trigger codegen scripts when:\n\n* The schema changes (using the \"schema.change.after\" hook).\n* Specific files are edited (using watch scripts).\n\nHere's an example |gel.toml| configuration for a TypeScript project. It runs a query builder generator and a queries generator at the appropriate times:\n\n.. code-block:: toml\n\n  [instance]\n  server-version = \"6.7\"\n\n  [hooks]\n  \"schema.change.after\" = \"npx @gel/generate edgeql-js && npx @gel/generate queries\"\n\n  [watch]\n  \"src/queries/**/*.edgeql\" = \"npx @gel/generate queries\"\n\nExplanation:\n\n* ``[hooks] / \"schema.change.after\"``: When any schema change is successfully applied, we run the query builder generator (to reflect schema structure changes) and the queries generator (to update based on new or modified types).\n* ``[watch] / \"src/queries/**/*.edgeql\"``: If any ``.edgeql`` files within the ``src/queries/`` directory (or its subdirectories) are modified, the command ``npx @gel/generate queries`` is executed. This ensures that your typed query functions are always up-to-date with your EdgeQL query definitions.\n\nBy configuring these hooks and watchers, you can maintain a smooth workflow where your generated code automatically adapts to changes in your schema and query files.\n"
  },
  {
    "path": "docs/intro/migrations.rst",
    "content": ".. _ref_intro_migrations:\n\n==========\nMigrations\n==========\n\n.. index:: fill_expr, cast_expr\n\n|Gel's| baked-in migration system lets you painlessly evolve your schema\nthroughout the development process. If you want to work along with this guide,\nstart a new project with :ref:`ref_cli_gel_project_init`. This will create a\nnew instance and some empty schema files to get you started.\n\n\n1. Start the ``watch`` command\n------------------------------\n\nThe easiest way to work with your schema in development is by running\n:gelcmd:`watch --migrate`. This long-running task will monitor your schema files and\nautomatically apply schema changes in your database as you work.\n\n.. code-block:: bash\n\n    $ gel watch --migrate\n    Hint: --migrate will apply any changes from your schema files to the database.\n    When ready to commit your changes, use:\n    1) `gel migration create` to write those changes to a migration file,\n    2) `gel migrate --dev-mode` to replace all synced changes with the migration.\n\n    Monitoring /home/instancename for changes in:\n\n      --migrate: gel migration apply --dev-mode\n\n\nIf you get output similar to the output above, you're ready to get started!\n\n\n2. Write an initial schema\n--------------------------\n\nBy convention, your Gel schema is defined inside one or more |.gel|\nfiles that live in a directory called ``dbschema`` in the root directory of\nyour codebase.\n\n.. code-block::\n\n  .\n  ├── dbschema\n  │   └── default.gel          # schema file (written by you)\n  └── gel.toml\n\nThe schema itself is written using Gel's schema definition language. Edit\nyour :dotgel:`dbschema/default` and add the following schema inside your\n``module default`` block:\n\n.. code-block:: sdl\n\n  type User {\n    required name: str;\n  }\n\n  type Post {\n    required title: str;\n    required author: User;\n  }\n\n\nIt's common to keep your entire schema in a single file, and many users use\nthis :dotgel:`default` that is created when you start a project. However it's\nalso possible to split their schemas across a number of |.gel| files.\n\nOnce you save your initial schema, assuming it is valid, the ``watch`` command\nwill pick it up and apply it to your database.\n\n\n3. Edit your schema files\n-------------------------\n\nAs your application evolves, directly edit your schema files to reflect your\ndesired data model. Try updating your :dotgel:`dbschema/default` to add a\n``Comment`` type:\n\n.. code-block:: sdl-diff\n\n    type User {\n      required name: str;\n    }\n\n    type Post {\n      required title: str;\n      required author: User;\n    }\n\n  + type Comment {\n  +   required content: str;\n  + }\n\nWhen you save your changes, ``watch`` will immediately begin applying your\nnew schema to the database.\n\n.. note::\n\n    If your schema cannot be applied, the ``watch`` command will generate an\n    error. If you're using one of our client bindings as you update your schema\n    with ``watch``, you will see the error there the next time you execute a\n    query using that client binding.\n\n    If things aren't working the way you expect after making a schema change,\n    take a look at the ``watch`` console to find out why.\n\nOnce you have the schema the way you want it, and you're ready to lock it in\nand commit it to version control, it's time to generate a migration.\n\n\n4. Generate a migration\n-----------------------\n\nTo generate a migration that reflects all your changes, run :gelcmd:`migration\ncreate`.\n\n.. code-block:: bash\n\n  $ gel migration create\n\n\nThe CLI reads your schema file and sends it to the active Gel instance. The\ninstance compares the file's contents to its current schema state and\ndetermines a migration plan.  **The migration plan is generated by the\ndatabase itself.**\n\nThis plan is then presented to you interactively; each detected schema change\nwill be individually presented to you for approval. For each prompt, you have\na variety of commands at your disposal. Type ``y`` to approve, ``n`` to\nreject, ``q`` to cancel the migration, or ``?`` for a breakdown of some more\nadvanced options.\n\n.. code-block:: bash\n\n  $ gel migration create\n  did you create object type 'default::Comment'? [y,n,l,c,b,s,q,?]\n  > y\n  did you create object type 'default::User'? [y,n,l,c,b,s,q,?]\n  > y\n  did you create object type 'default::Post'? [y,n,l,c,b,s,q,?]\n  > y\n  Created dbschema/migrations/00001.edgeql, id: <hash>\n\n\n.. _ref_intro_migrations_wo_iteration:\n\nMigration without iteration\n---------------------------\n\nIf you want to change the schema, but you already know exactly what you want to\nchange and don't need to iterate on your schema — you want to lock in the\nmigration right away — :gelcmd:`watch` might not be the tool you reach for.\n\nInstead, you might use this method:\n\n1. Edit your schema files\n2. Create your migration with :gelcmd:`migration create`\n3. Apply your migration with :gelcmd:`migrate`\n\nSince you're not using ``watch``, the schema changes are not applied when you\nsave your schema files. As a result, we need to tack an extra step on the end\nof the process of applying the migration. That's handled by :gelcmd:`migrate`.\n\n.. code-block:: bash\n\n  $ gel migrate\n  Applied m1virjowa... (00002.edgeql)\n\nOnce your migration is applied, you'll see the schema changes reflected in your\ndatabase.\n\n\nData migrations\n---------------\n\nDepending on how the schema was changed, data in your database may prevent\n|Gel| from applying your schema changes. Imagine we added a required ``body``\nproperty to our ``Post`` type:\n\n.. code-block:: sdl-diff\n\n    type User {\n      required name: str;\n    }\n\n    type Post {\n      required title: str;\n  +   required body: str;\n      required author: User;\n    }\n\n    type Comment {\n      required content: str;\n    }\n\nIf we hadn't added any ``Post`` objects to our database before this, everything\nwould have worked fine, but it's likely that, in testing out our schema, we\n*did* add a ``Post`` object. It does not have a ``body`` property, but now\nwe've told the database this property is required on all ``Post`` objects. The\ndatabase can't apply this change because existing data would break it.\n\nWe have a couple of options here. We could delete all the offending objects.\n\n.. code-block:: edgeql-repl\n\n    db> delete Post;\n    {\n      default::Post {id: a4a0a40c-d9f5-11ed-8912-1397f7af9fdf},\n      default::Post {id: cc051bea-d9f5-11ed-a26d-2b64b6b273a4}\n    }\n\nNow, if we save the schema again, :gelcmd:`watch` will be able to apply it. If\nwe have data in here we don't want to lose though, that's not a good option. In\nthat case, we might drop back to creating and applying the migration outside of\n:gelcmd:`watch`.\n\nTo start, run :gelcmd:`migration create`. The interactive plan generator will\nask you for an EdgeQL expression to map the contents of your database to the\nnew schema.\n\n.. code-block:: bash\n\n  $ gel migration create\n  did you create property 'body' of object type\n  'default::Post'? [y,n,l,c,b,s,q,?]\n  > y\n  Please specify an expression to populate existing objects in order to make\n  property 'body' of object type 'default::Post' required:\n  fill_expr>\n\nBecause the ``body`` property does not currently exist, the database contains\n``Post`` objects without it. The expression you provide will be used to *assign\na body* to any ``Post`` object that doesn't have one. We'll just provide a\nsimple default: ``'No content'``.\n\n.. code-block::\n\n  fill_expr> 'No content'\n  Created dbschema/migrations/00002.edgeql, id:\n  m1pjiibv4sa4cao7txpgsbuw2erctmacyrj4qmn45ggapsaztmvxfa\n\nNice! It accepted our answer and created a new migration file\n``00002.edgeql``. Let's see what the newly created ``00002.edgeql`` file\ncontains.\n\n.. code-block:: edgeql\n\n    CREATE MIGRATION m1pjiibv4sa4cao7txpgsbuw2erctmacyrj4qmn45ggapsaztmvxfa\n        ONTO m1nlvzbm7buwktkp4vu4shylq6zp2shruokbbssyeidqmmmfqz77yq\n    {\n      ALTER TYPE default::Post {\n          CREATE REQUIRED PROPERTY body: std::str {\n              SET REQUIRED USING ('No content');\n          };\n      };\n    };\n\nWe have a ``CREATE MIGRATION`` block containing an ``ALTER TYPE`` statement to\ncreate ``Post.body`` as a ``required`` property. We can see that our fill\nexpression (``'No content'``) is included directly in the migration file.\n\nNote that we could have provide an *arbitrary EdgeQL expression*! The\nfollowing EdgeQL features are often useful:\n\n.. list-table::\n\n  * - ``assert_exists``\n    - This is an \"escape hatch\" function that tells Gel to assume the input\n      has *at least* one element.\n\n      .. code-block::\n\n        fill_expr> assert_exists(.body)\n\n      If you provide a ``fill_expr`` like the one above, you must separately\n      ensure that all posts have a ``body`` before executing the migration;\n      otherwise it will fail.\n\n  * - ``assert_single``\n    - This tells Gel to assume the input has *at most* one element. This\n      will throw an error if the argument is a set containing more than one\n      element. This is useful is you are changing a property from ``multi`` to\n      ``single``.\n\n      .. code-block::\n\n        fill_expr> assert_single(.sheep)\n\n  * - type casts\n    - Useful when converting a property to a different type.\n\n      .. code-block::\n\n        cast_expr> <bigint>.xp\n\n\nFurther reading\n^^^^^^^^^^^^^^^\n\n- :ref:`Guide to schema migrations <ref_migration_guide>`\n- :ref:`Migration tips <ref_migration_tips>`\n\nFurther information can be found in the :ref:`CLI\nreference <ref_cli_gel_migration>` or the `Beta 1 blog post\n<https://www.geldata.com/blog/geldata-1-0-beta-1-sirius#built-in-database-migrations-in-use>`_,\nwhich describes the design of the migration system.\n"
  },
  {
    "path": "docs/intro/projects.rst",
    "content": ".. _ref_intro_projects:\n\n========\nProjects\n========\n\nIt can be inconvenient to pass the ``-I`` flag every time you wish to run a\nCLI command.\n\n.. code-block:: bash\n\n  $ gel migration create -I my_instance\n\nThat's one of the reasons we introduced the concept of an *Gel\nproject*. A project is a directory on your file system that is associated\n(\"linked\") with a Gel instance.\n\n.. note::\n\n  Projects are intended to make *local development* easier! They only exist on\n  your local machine and are managed with the CLI. When deploying Gel for\n  production, you will typically pass connection information to the client\n  library using environment variables.\n\nWhen you're inside a project, all CLI commands will be applied against the\n*linked instance* by default (no CLI flags required).\n\n.. code-block:: bash\n\n  $ gel migration create\n\nThe same is true for all Gel client libraries (discussed in more depth in\nthe :ref:`Clients <ref_intro_clients>` section). If the following file lives\ninside a Gel project directory, ``createClient`` will discover the project\nand connect to its linked instance with no additional configuration.\n\n.. code-block:: typescript\n\n    // clientTest.js\n    import {createClient} from 'gel';\n\n    const client = createClient();\n    await client.query(\"select 5\");\n\nInitializing\n^^^^^^^^^^^^\n\nTo initialize a project, create a new directory and run :gelcmd:`project init`\ninside it. You'll see something like this:\n\n.. code-block:: bash\n\n  $ gel project init\n  No `gel.toml` found in this repo or above.\n  Do you want to initialize a new project? [Y/n]\n  > Y\n  Specify the name of Gel instance to use with this project\n  [default: my_instance]:\n  > my_instance\n  Checking Gel versions...\n  Specify the version of Gel to use with this project [default: x.x]:\n  > # (left blank for default)\n  ...\n  Successfully installed x.x+cc4f3b5\n  Initializing Gel instance...\n  Applying migrations...\n  Everything is up to date. Revision initial\n  Project initialized.\n  To connect to my_instance, run `gel`\n\nThis command does a couple important things.\n\n1. It spins up a new Gel instance called ``my_instance``.\n2. If no |gel.toml| file exists, it will create one. This is a\n   configuration file that marks a given directory as a Gel project. Learn\n   more about it in the :ref:`gel.toml reference <ref_reference_gel_toml>`.\n\n   .. code-block:: toml\n\n     [instance]\n     server-version = \"6.0\"\n\n3. If no ``dbschema`` directory exists, it will be created, along with an\n   empty :dotgel:`default` file which will contain your schema. If a\n   ``dbschema`` directory exists and contains a subdirectory called\n   ``migrations``, those migrations will be applied against the new instance.\n\nEvery project maps one-to-one to a particular Gel instance. From\ninside a project directory, you can run :gelcmd:`project info` to see\ninformation about the current project.\n\n.. code-block:: bash\n\n  $ gel project info\n  ┌───────────────┬──────────────────────────────────────────┐\n  │ Instance name │ my_instance                              │\n  │ Project root  │ /path/to/project                         │\n  └───────────────┴──────────────────────────────────────────┘\n\n\nConnection\n^^^^^^^^^^\n\nAs long as you are inside the project directory, all CLI commands will be\nexecuted against the project-linked instance. For instance, you can simply run\n|gelcmd| to open a REPL.\n\n.. code-block:: bash\n\n  $ gel\n  Gel x.x+cc4f3b5 (repl x.x+da2788e)\n  Type \\help for help, \\quit to quit.\n  my_instance:main> select \"Hello world!\";\n\nBy contrast, if you leave the project directory, the CLI will no longer know\nwhich instance to connect to. You can solve this by specifing an instance name\nwith the ``-I`` flag.\n\n.. code-block:: bash\n\n  $ cd ~\n  $ gel\n  gel error: no `gel.toml` found and no connection options are specified\n    Hint: Run `gel project init` or use any of `-H`, `-P`, `-I` arguments to\n    specify connection parameters. See `--help` for details\n  $ gel -I my_instance\n  Gel x.x+cc4f3b5 (repl x.x+da2788e)\n  Type \\help for help, \\quit to quit.\n  my_instance:main>\n\nSimilarly, client libraries will auto-connect to the project's\nlinked instance without additional configuration.\n\nUsing remote instances\n^^^^^^^^^^^^^^^^^^^^^^\n\nYou may want to initialize a project that points to a remote Gel instance.\nThis is totally a valid case and Gel fully supports it! Before running\n:gelcmd:`project init`, you just need to create an alias for the remote\ninstance using :gelcmd:`instance link`, like so:\n\n.. lint-off\n\n.. code-block:: bash\n\n  $ gel instance link\n  Specify server host [default: localhost]:\n  > 192.168.4.2\n  Specify server port [default: 5656]:\n  > 10818\n  Specify database user [default: admin]:\n  > admin\n  Specify branch [default: main]:\n  > main\n  Unknown server certificate: SHA1:c38a7a90429b033dfaf7a81e08112a9d58d97286.\n  Trust? [y/N]\n  > y\n  Password for 'admin':\n  Specify a new instance name for the remote server [default: abcd]:\n  > staging_db\n  Successfully linked to remote instance. To connect run:\n    gel -I staging_db\n\n.. lint-on\n\nAfter receiving the necessary connection information, this command links the\nremote instance to a local alias ``\"staging_db\"``. You can use this as\ninstance name in CLI commands.\n\n.. code-block::\n\n  $ gel -I staging_db\n  gel>\n\nTo initialize a project that uses the remote instance, provide this alias when\nprompted for an instance name during the :gelcmd:`project init` workflow.\n\n\nUnlinking\n^^^^^^^^^\n\nAn instance can be unlinked from a project. This leaves the instance running\nbut effectively \"uninitializes\" the project. The |gel.toml| and\n``dbschema`` are left untouched.\n\n.. code-block:: bash\n\n    $ gel project unlink\n\nIf you wish to delete the instance as well, use the ``-D`` flag.\n\n.. code-block:: bash\n\n    $ gel project unlink -D\n\nUpgrading\n^^^^^^^^^\n\nA standalone instance (not linked to a project) can be upgraded with the\n:gelcmd:`instance upgrade` command.\n\n.. code-block:: bash\n\n  $ gel project upgrade --to-latest\n  $ gel project upgrade --to-nightly\n  $ gel project upgrade --to-version x.x\n\n\nSee info\n^^^^^^^^\n\nYou can see the location of a project and the name of its linked instance.\n\n.. code-block:: bash\n\n  $ gel project info\n  ┌───────────────┬──────────────────────────────────────────┐\n  │ Instance name │ my_app                                   │\n  │ Project root  │ /path/to/my_app                          │\n  └───────────────┴──────────────────────────────────────────┘\n"
  },
  {
    "path": "docs/intro/quickstart/ai/fastapi.rst",
    "content": ".. _ref_quickstart_ai:\n\n======================\nUsing the built-in RAG\n======================\n\n.. edb:split-section::\n\n    In this section we'll learn about |Gel's| built-in vector search and\n    retrieval-augmented generation capabilities. We'll be continuing from where\n    we left off in the :ref:`main quickstart <ref_quickstart>`. Feel free to browse the\n    complete flascards app code in this `repo\n    <https://github.com/edgedb/quickstart-fastapi>`_.\n\n    In this tutorial we'll focus on creating a ``/fetch_similar`` endpoint for\n    looking up flashcards similar to a text search query, as well as a\n    ``/fetch_rag`` endpoint that's going to enable us to talk to an LLM about\n    the content of our flashcard deck.\n\n    We're going to start with the same schema we left off with in the primary\n    quickstart.\n\n\n    .. code-block:: sdl\n        :caption: dbschema/default.gel\n\n        module default {\n            abstract type Timestamped {\n                required created_at: datetime {\n                    default := datetime_of_statement();\n                };\n                required updated_at: datetime {\n                    default := datetime_of_statement();\n                };\n            }\n\n            type Deck extending Timestamped {\n                required name: str;\n                description: str;\n\n                multi cards: Card {\n                    constraint exclusive;\n                    on target delete allow;\n                };\n            };\n\n            type Card extending Timestamped {\n                required order: int64;\n                required front: str;\n                required back: str;\n            }\n        }\n\n\n.. edb:split-section::\n\n    AI-related features in |Gel| come packaged in the extension called ``ai``.\n    Let's enable it by adding the following line on top of the\n    :dotgel:`dbschema/default` and running a migration.\n\n    This does a few things. First, it enables us to use features from the extension by prefixing them with ``ext::ai::``.\n\n\n    .. code-block:: sdl-diff\n        :caption: dbschema/default.gel\n\n        + using extension ai;\n\n          module default {\n              abstract type Timestamped {\n                  required created_at: datetime {\n                      default := datetime_of_statement();\n                  };\n                  required updated_at: datetime {\n                      default := datetime_of_statement();\n                  };\n              }\n\n              type Deck extending Timestamped {\n                  required name: str;\n                  description: str;\n\n                  multi cards: Card {\n                      constraint exclusive;\n                      on target delete allow;\n                  };\n              };\n\n              type Card extending Timestamped {\n                  required order: int64;\n                  required front: str;\n                  required back: str;\n              }\n          }\n\n.. edb:split-section::\n\n    This enabled us to use features in the ``ext::ai::`` namespace. Here's a\n    notable one: ``ProviderConfig``, which we can use to configure our API\n    keys. |Gel| supports a variety of external APIs for creating embedding\n    vectors for text and fetching LLM completions.\n\n    Let's configure an API key for OpenAI by running the following query in the\n    REPL:\n\n    .. note::\n\n        Once the extension is active, we can also access the dedicated AI tab\n        in the UI. There we can manage provider configurations and try out\n        different RAG configuraton in the Playground.\n\n\n    .. code-block:: edgeql-repl\n\n        db> configure current database\n            insert ext::ai::OpenAIProviderConfig {\n                secret := 'sk-....',\n            };\n\n\n.. edb:split-section::\n\n    Once last thing before we move on. Let's add some sample data to give the\n    embedding model something to work with. You can copy and run this command\n    in the terminal, or come up with your own sample data.\n\n\n    .. code-block:: edgeql\n        :class: collapsible\n\n        $ cat << 'EOF' | gel query --file -\n        with deck := (\n            insert Deck {\n                name := 'Smelly Cheeses',\n                description := 'To impress everyone with stinky cheese trivia.'\n            }\n        )\n        for card_data in {(\n            1,\n            'Époisses de Bourgogne',\n            'Known as the \"king of cheeses\", this French cheese is so pungent it\\'s banned on public transport in France. Washed in brandy, it becomes increasingly funky as it ages. Orange-red rind, creamy interior.'\n        ), (\n            2,\n            'Vieux-Boulogne',\n            'Officially the smelliest cheese in the world according to scientific studies. This northern French cheese has a reddish-orange rind from being washed in beer. Smooth, creamy texture with a powerful aroma.'\n        ), (\n            3,\n            'Durian Cheese',\n            'This Malaysian creation combines durian fruit with cheese, creating what some consider the ultimate \"challenging\" dairy product. Combines the pungency of blue cheese with durian\\'s notorious aroma.'\n        ), (\n            4,\n            'Limburger',\n            'German cheese famous for its intense smell, often compared to foot odor due to the same bacteria. Despite its reputation, has a surprisingly mild taste with notes of mushroom and grass.'\n        ), (\n            5,\n            'Roquefort',\n            'The \"king of blue cheeses\", aged in limestone caves in southern France. Contains Penicillium roqueforti mold. Strong, tangy, and salty with a crumbly texture. Legend says it was discovered when a shepherd left his lunch in a cave.'\n        ), (\n            6,\n            'What makes washed-rind cheeses so smelly?',\n            'The process of washing cheese rinds in brine, alcohol, or other solutions promotes the growth of Brevibacterium linens, the same bacteria responsible for human body odor. This bacteria contributes to both the orange color and distinctive aroma.'\n        ), (\n            7,\n            'Stinking Bishop',\n            'Named after the Stinking Bishop pear (not a religious figure). This English cheese is washed in perry made from these pears. Known for its powerful aroma and sticky, pink-orange rind. Gained fame after being featured in Wallace & Gromit.'\n        )}\n        union (\n            insert Card {\n                deck := deck,\n                order := card_data.0,\n                front := card_data.1,\n                back := card_data.2\n            }\n        );\n        EOF\n\n\n.. edb:split-section::\n\n    Now we can finally start producing embedding vectors. Since |Gel| is fully\n    aware of when your data gets inserted, updated and deleted, it's perfectly\n    equipped to handle all the tedious work of keeping those vectors up to\n    date. All that's left for us is to create a special ``deferred index`` on\n    the data we would like to perform similarity search on.\n\n\n    .. code-block:: sdl-diff\n        :caption: dbschema/default.gel\n\n          using extension ai;\n\n          module default {\n              abstract type Timestamped {\n                  required created_at: datetime {\n                      default := datetime_of_statement();\n                  };\n                  required updated_at: datetime {\n                      default := datetime_of_statement();\n                  };\n              }\n\n              type Deck extending Timestamped {\n                  required name: str;\n                  description: str;\n\n                  multi cards: Card {\n                      constraint exclusive;\n                      on target delete allow;\n                  };\n              };\n\n              type Card extending Timestamped {\n                  required order: int64;\n                  required front: str;\n                  required back: str;\n\n        +         deferred index ext::ai::index(embedding_model := 'text-embedding-3-small')\n        +             on (.front ++ ' ' ++ .back);\n              }\n          }\n\n\n.. edb:split-section::\n\n    It's time to start running queries.\n\n    Let's begin by creating the ``/fetch_similar`` endpoint we mentioned\n    earlier. It's job is going to be to find 3 flashcards that are the most\n    similar to the provided text query. We can use this endpoint to implement a\n    \"recommended flashcards\" on the frontend.\n\n    The AI extension contains a function called ``ext::ai::search(Type,\n    embedding_vector)`` that we can use to do our fetch. Note that the second\n    argument is an embedding vector, not a text query. To transform our text\n    query into a vector, we will use the ``generate_embeddings`` function from\n    the ``ai`` module of |Gel|'s Python binding.\n\n    Gathered together, here are the modifications we need to do to the\n    ``main.py`` function:\n\n\n    .. code-block:: python-diff\n        :caption: main.py\n\n          import gel\n        + import gel.ai\n\n          from fastapi import FastAPI\n\n\n          client = gel.create_async_client()\n\n          app = FastAPI()\n\n\n        + @app.get(\"/fetch_similar\")\n        + async def fetch_similar_cards(query: str):\n        +     rag = await gel.ai.create_async_rag_client(client, model=\"gpt-4-turbo-preview\")\n        +     embedding_vector = await rag.generate_embeddings(\n        +         query, model=\"text-embedding-3-small\"\n        +     )\n\n        +     similar_cards = await client.query(\n        +         \"select ext::ai::search(Card, <array<float32>>$embedding_vector)\",\n        +         embedding_vector=embedding_vector,\n        +     )\n\n        +     return similar_cards\n\n\n.. edb:split-section::\n\n    Let's test the endpoint to see that everything works the way we expect.\n\n\n    .. code-block:: bash\n\n        $ curl -X 'GET' \\\n          'http://localhost:8000/fetch_similar?query=the%20stinkiest%20cheese' \\\n          -H 'accept: application/json'\n\n\n.. edb:split-section::\n\n    Finally, let's create the second endpoint we mentioned, called\n    ``/fetch_rag``. We'll be able to use this one to, for example, ask an LLM\n    to quiz us on the contents of our deck.\n\n    The RAG feature is represented in the Python binding with the ``query_rag``\n    method of the ``GelRAG`` class. To use it, we're going to instantiate the\n    class and call the method... And that's it!\n\n\n    .. code-block:: python-diff\n        :caption: main.py\n\n          import gel\n          import gel.ai\n\n          from fastapi import FastAPI\n\n\n          client = gel.create_async_client()\n\n          app = FastAPI()\n\n\n          @app.get(\"/fetch_similar\")\n          async def fetch_similar_cards(query: str):\n              rag = await gel.ai.create_async_rag_client(client, model=\"gpt-4-turbo-preview\")\n              embedding_vector = await rag.generate_embeddings(\n                  query, model=\"text-embedding-3-small\"\n              )\n\n              similar_cards = await client.query(\n                  \"select ext::ai::search(Card, <array<float32>>$embedding_vector)\",\n                  embedding_vector=embedding_vector,\n              )\n\n              return similar_cards\n\n\n        + @app.get(\"/fetch_rag\")\n        + async def fetch_rag_response(query: str):\n        +     rag = await gel.ai.create_async_rag_client(client, model=\"gpt-4-turbo-preview\")\n        +     response = await rag.query_rag(\n        +         message=query,\n        +         context=gel.ai.QueryContext(query=\"select Card\"),\n        +     )\n        +     return response\n\n\n.. edb:split-section::\n\n    Let's test the endpoint to see if it works:\n\n\n    .. code-block:: bash\n\n        $ curl -X 'GET' \\\n          'http://localhost:8000/fetch_rag?query=what%20cheese%20smells%20like%20feet' \\\n          -H 'accept: application/json'\n\n\n\nCongratulations! We've now implemented AI features in our flashcards app.\nOf course, there's more to learn when it comes to using the AI extension.\nMake sure to check out the :ref:`Reference manual\n<ref_ai_python_reference>`, or build an LLM-powered search bot from the\nground up with the :ref:`FastAPI Gel AI tutorial\n<ref_guide_fastapi_gelai_searchbot>`.\n"
  },
  {
    "path": "docs/intro/quickstart/ai/index.rst",
    "content": ".. edb:env-switcher::\n\n=========\nAdding AI\n=========\n\n.. toctree::\n  :maxdepth: 1\n\n  fastapi\n"
  },
  {
    "path": "docs/intro/quickstart/connecting/fastapi.rst",
    "content": ".. _ref_quickstart_fastapi_connecting:\n\n==========================\nConnecting to the database\n==========================\n\n.. edb:split-section::\n\n  Before diving into the application, let's take a quick look at how to connect to the database from your code. We will intialize a client and use it to make a simple, static query to the database, and log the result to the console.\n\n  .. note::\n\n    Notice that the ``create_async_client`` function isn't being passed any connection details. With |Gel|, you do not need to come up with your own scheme for how to build the correct database connection credentials and worry about leaking them into your code. You simply use |Gel| \"projects\" for local development, and set the appropriate environment variables in your deployment environments, and the ``create_async_client`` function knows what to do!\n\n  .. edb:split-point::\n\n  .. code-block:: python\n    :caption: ./test.py\n\n    import gel\n    import asyncio\n\n    async def main():\n        client = gel.create_async_client()\n        result = await client.query_single(\"select 'Hello from Gel!';\")\n        print(result)\n\n    asyncio.run(main())\n\n  .. code-block:: sh\n\n    $ python test.py\n    Hello from Gel!\n\n.. edb:split-section::\n\n  In Python, we write EdgeQL queries directly as strings. This gives us the full power and expressiveness of EdgeQL while maintaining type safety through Gel's strict schema. Let's try inserting a few ``Deck`` objects into the database and then selecting them back.\n\n  .. edb:split-point::\n\n  .. code-block:: python-diff\n    :caption: ./test.py\n\n      import gel\n      import asyncio\n\n      async def main():\n          client = gel.create_async_client()\n    -     result = await client.query_single(\"select 'Hello from Gel!';\")\n    -     print(result)\n    +     await client.query(\"\"\"\n    +         insert Deck { name := \"I am one\" }\n    +     \"\"\")\n    +\n    +     await client.query(\"\"\"\n    +         insert Deck { name := \"I am two\" }\n    +     \"\"\")\n    +\n    +     decks = await client.query(\"\"\"\n    +         select Deck {\n    +             id,\n    +             name\n    +         }\n    +     \"\"\")\n    +\n    +     for deck in decks:\n    +         print(f\"ID: {deck.id}, Name: {deck.name}\")\n    +\n    +     await client.query(\"delete Deck\")\n\n      asyncio.run(main())\n\n  .. code-block:: sh\n\n    $ python test.py\n    Hello from Gel!\n    ID: f4cd3e6c-ea75-11ef-83ec-037350ea8a6e, Name: I am one\n    ID: f4cf27ae-ea75-11ef-83ec-3f7b2fceab24, Name: I am two\n"
  },
  {
    "path": "docs/intro/quickstart/connecting/index.rst",
    "content": ".. edb:env-switcher::\n\n==========================\nConnecting to the database\n==========================\n\n.. toctree::\n  :maxdepth: 3\n  :hidden:\n\n  nextjs\n  fastapi\n"
  },
  {
    "path": "docs/intro/quickstart/connecting/nextjs.rst",
    "content": ".. _ref_quickstart_connecting:\n\n==========================\nConnecting to the database\n==========================\n\n.. edb:split-section::\n\n  Before diving into the application, let's take a quick look at how to connect to the database from your code. We will intialize a client and use it to make a simple, static query to the database, and log the result to the console.\n\n  .. note::\n\n    Notice that the ``createClient`` function isn't being passed any connection details. With |Gel|, you do not need to come up with your own scheme for how to build the correct database connection credentials and worry about leaking them into your code. You simply use |Gel| \"projects\" for local development, and set the appropriate environment variables in your deployment environments, and the ``createClient`` function knows what to do!\n\n  .. edb:split-point::\n\n  .. code-block:: typescript\n    :caption: ./test.ts\n\n    import { createClient } from \"gel\";\n\n    const client = createClient();\n\n    async function main() {\n      console.log(await client.query(\"select 'Hello from Gel!';\"));\n    }\n\n    main().then(\n      () => process.exit(0),\n      (err) => {\n        console.error(err);\n        process.exit(1);\n      }\n    );\n\n\n  .. code-block:: sh\n\n    $ npx tsx test.ts\n    [ 'Hello from Gel!' ]\n\n.. edb:split-section::\n\n\n  With TypeScript, there are three ways to run a query: use a string EdgeQL query, use the ``queries`` generator to turn a string of EdgeQL into a TypeScript function, or use the query builder API to build queries dynamically in a type-safe manner. In this tutorial, you will use the TypeScript query builder API.\n\n  This query builder must be regenerated any time the schema changes, so a hook has been added to the ``gel.toml`` file to generate the query builder any time the schema is updated. Moving beyond this simple query, use the query builder API to insert a few ``Deck`` objects into the database, and then select them back.\n\n  .. edb:split-point::\n\n  .. code-block:: typescript-diff\n    :caption: ./test.ts\n\n      import { createClient } from \"gel\";\n    + import e from \"@/dbschema/edgeql-js\";\n\n      const client = createClient();\n\n      async function main() {\n        console.log(await client.query(\"select 'Hello from Gel!';\"));\n\n    +   await e.insert(e.Deck, { name: \"I am one\" }).run(client);\n    +\n    +   await e.insert(e.Deck, { name: \"I am two\" }).run(client);\n    +\n    +   const decks = await e\n    +     .select(e.Deck, () => ({\n    +       id: true,\n    +       name: true,\n    +     }))\n    +     .run(client);\n    +\n    +   console.table(decks);\n    +\n    +   await e.delete(e.Deck).run(client);\n      }\n\n      main().then(\n        () => process.exit(0),\n        (err) => {\n          console.error(err);\n          process.exit(1);\n        }\n      );\n\n  .. code-block:: sh\n\n    $ npx tsx test.ts\n    [ 'Hello from Gel!' ]\n    ┌─────────┬────────────────────────────────────────┬────────────┐\n    │ (index) │ id                                     │ name       │\n    ├─────────┼────────────────────────────────────────┼────────────┤\n    │ 0       │ 'f4cd3e6c-ea75-11ef-83ec-037350ea8a6e' │ 'I am one' │\n    │ 1       │ 'f4cf27ae-ea75-11ef-83ec-3f7b2fceab24' │ 'I am two' │\n    └─────────┴────────────────────────────────────────┴────────────┘\n\nNow that you know how to connect to the database, you will see that we have provided an initialized ``Client`` object in the ``/lib/gel.ts`` module. Throughout the rest of the tutorial, you will import this ``Client`` object and use it to make queries.\n"
  },
  {
    "path": "docs/intro/quickstart/index.rst",
    "content": "==========\nQuickstart\n==========\n\n.. toctree::\n  :maxdepth: 1\n  :hidden:\n\n  overview/index\n  setup/index\n  modeling/index\n  connecting/index\n  working/index\n  inheritance/index\n  ai/index\n"
  },
  {
    "path": "docs/intro/quickstart/inheritance/fastapi.rst",
    "content": ".. _ref_quickstart_fastapi_inheritance:\n\n========================\nAdding shared properties\n========================\n\n.. edb:split-section::\n\n  One common pattern in applications is to add shared properties to the schema that are used by multiple objects. For example, you might want to add a ``created_at`` and ``updated_at`` property to every object in your schema. You can do this by adding an abstract type and using it as a mixin for your other object types.\n\n  .. code-block:: sdl-diff\n    :caption: dbschema/default.gel\n\n      module default {\n    +   abstract type Timestamped {\n    +     required created_at: datetime {\n    +       default := datetime_of_statement();\n    +     };\n    +     required updated_at: datetime {\n    +       default := datetime_of_statement();\n    +     };\n    +   }\n    +\n    -   type Deck {\n    +   type Deck extending Timestamped {\n          required name: str;\n          description: str;\n\n          cards := (\n            select .<deck[is Card]\n            order by .order\n          );\n        };\n\n    -   type Card {\n    +   type Card extending Timestamped {\n          required order: int64;\n          required front: str;\n          required back: str;\n\n          required deck: Deck;\n        }\n      }\n\n.. edb:split-section::\n\n  Since you don't have historical data for when these objects were actually created or modified, the migration will fall back to the default values set in the ``Timestamped`` type.\n\n  .. code-block:: sh\n\n    $ gel migration create\n    did you create object type 'default::Timestamped'? [y,n,l,c,b,s,q,?]\n    > y\n    did you alter object type 'default::Card'? [y,n,l,c,b,s,q,?]\n    > y\n    did you alter object type 'default::Deck'? [y,n,l,c,b,s,q,?]\n    > y\n    Created /home/strinh/projects/flashcards/dbschema/migrations/00004-m1d2m5n.edgeql, id: m1d2m5n5ajkalyijrxdliioyginonqbtfzihvwdfdmfwodunszstya\n\n    $ gel migrate\n    Applying m1d2m5n5ajkalyijrxdliioyginonqbtfzihvwdfdmfwodunszstya (00004-m1d2m5n.edgeql)\n    ... parsed\n    ... applied\n\n.. edb:split-section::\n\n  Update the ``get_decks`` query to sort the decks by ``updated_at`` in descending order.\n\n  .. code-block:: python-diff\n    :caption: main.py\n\n      @app.get(\"/decks\", response_model=List[Deck])\n      async def get_decks():\n          decks = await client.query(\"\"\"\n              select Deck {\n                  id,\n                  name,\n                  description,\n                  cards := (\n                      select .cards {\n                          id,\n                          front,\n                          back\n                      }\n                      order by .order\n                  )\n              }\n    +         order by .updated_at desc\n          \"\"\")\n          return decks\n"
  },
  {
    "path": "docs/intro/quickstart/inheritance/index.rst",
    "content": ".. edb:env-switcher::\n\n========================\nAdding shared properties\n========================\n\n.. toctree::\n  :maxdepth: 3\n  :hidden:\n\n  nextjs\n  fastapi\n\n"
  },
  {
    "path": "docs/intro/quickstart/inheritance/nextjs.rst",
    "content": ".. _ref_quickstart_inheritance:\n\n========================\nAdding shared properties\n========================\n\n.. edb:split-section::\n\n  One common pattern in applications is to add shared properties to the schema that are used by multiple objects. For example, you might want to add a ``created_at`` and ``updated_at`` property to every object in your schema. You can do this by adding an abstract type and using it as a mixin for your other object types.\n\n  .. code-block:: sdl-diff\n    :caption: dbschema/default.gel\n\n      module default {\n    +   abstract type Timestamped {\n    +     required created_at: datetime {\n    +       default := datetime_of_statement();\n    +     };\n    +     required updated_at: datetime {\n    +       default := datetime_of_statement();\n    +     };\n    +   }\n    +\n    -   type Deck {\n    +   type Deck extending Timestamped {\n          required name: str;\n          description: str;\n\n          multi cards: Card {\n            constraint exclusive;\n            on target delete allow;\n          };\n        };\n\n    -   type Card {\n    +   type Card extending Timestamped {\n          required order: int64;\n          required front: str;\n          required back: str;\n        }\n      }\n\n.. edb:split-section::\n\n  Since you don't have historical data for when these objects were actually created or modified, the migration will fall back to the default values set in the ``Timestamped`` type.\n\n  .. code-block:: sh\n\n    $ npx gel migration create\n    did you create object type 'default::Timestamped'? [y,n,l,c,b,s,q,?]\n    > y\n    did you alter object type 'default::Card'? [y,n,l,c,b,s,q,?]\n    > y\n    did you alter object type 'default::Deck'? [y,n,l,c,b,s,q,?]\n    > y\n    Created /home/strinh/projects/flashcards/dbschema/migrations/00004-m1d2m5n.edgeql, id: m1d2m5n5ajkalyijrxdliioyginonqbtfzihvwdfdmfwodunszstya\n\n    $ npx gel migrate\n    Applying m1d2m5n5ajkalyijrxdliioyginonqbtfzihvwdfdmfwodunszstya (00004-m1d2m5n.edgeql)\n    ... parsed\n    ... applied\n    Generating query builder...\n    Detected tsconfig.json, generating TypeScript files.\n      To override this, use the --target flag.\n      Run `npx @gel/generate --help` for full options.\n    Introspecting database schema...\n    Generating runtime spec...\n    Generating cast maps...\n    Generating scalars...\n    Generating object types...\n    Generating function types...\n    Generating operators...\n    Generating set impl...\n    Generating globals...\n    Generating index...\n    Writing files to ./dbschema/edgeql-js\n    Generation complete! 🤘\n\n.. edb:split-section::\n\n  Update the ``getDecks`` query to sort the decks by ``updated_at`` in descending order.\n\n  .. code-block:: typescript-diff\n    :caption: app/queries.ts\n\n      import { client } from \"@/lib/gel\";\n      import e from \"@/dbschema/edgeql-js\";\n\n      export async function getDecks() {\n        const decks = await e.select(e.Deck, (deck) => ({\n          id: true,\n          name: true,\n          description: true,\n          cards: e.select(deck.cards, (card) => ({\n            id: true,\n            front: true,\n            back: true,\n            order_by: card.order,\n          })),\n    +     order_by: {\n    +       expression: deck.updated_at,\n    +       direction: e.DESC,\n    +     },\n        })).run(client);\n\n        return decks;\n      }\n\n.. edb:split-section::\n\n  Now when you look at the data in the UI, you will see the new properties on each of your object types.\n\n  .. image:: images/timestamped.png\n"
  },
  {
    "path": "docs/intro/quickstart/modeling/fastapi.rst",
    "content": ".. _ref_quickstart_fastapi_modeling:\n\n=================\nModeling the data\n=================\n\n.. edb:split-section::\n\n  The flashcards application has a simple data model, but it's interesting enough to utilize many unique features of the |Gel| schema language.\n\n  Looking at the mock data in the example JSON file ``./deck-edgeql.json``, you can see this structure in the JSON. There is a ``Card`` class that describes a single flashcard, which contains two required string properties: ``front`` and ``back``. Each ``Deck`` object has zero or more ``Card`` objects in a list.\n\n  .. code-block:: python\n\n    from pydantic import BaseModel\n\n    class CardBase(BaseModel):\n      front: str\n      back: str\n\n    class Card(CardBase):\n      id: str\n\n    class DeckBase(BaseModel):\n      name: str\n      description: Optional[str] = None\n\n    class Deck(DeckBase):\n      id: str\n      cards: List[Card]\n\n.. edb:split-section::\n\n  Starting with this simple model, add these types to the :dotgel:`dbschema/default` schema file. As you can see, the types closely mirror the JSON mock data.\n\n  Also of note, the link between ``Card`` and ``Deck`` objects creates a \"1-to-n\" relationship, where each ``Deck`` object has a link to zero or more ``Card`` objects. When you query the ``Deck.cards`` link, the cards will be unordered, so the ``Card`` type needs an explicit ``order`` property to allow sorting them at query time.\n\n  By default, when you try to delete an object that is linked to another object, the database will prevent you from doing so. We want to support removing a ``Card``, so we define a deletion policy on the ``cards`` link that allows deleting the target of this link.\n\n  .. code-block:: sdl-diff\n    :caption: dbschema/default.gel\n\n      module default {\n    +   type Card {\n    +     required order: int64;\n    +     required front: str;\n    +     required back: str;\n    +   };\n    +\n    +   type Deck {\n    +     required name: str;\n    +     description: str;\n    +     multi cards: Card {\n    +       constraint exclusive;\n    +       on target delete allow;\n    +     };\n    +   };\n      };\n\n.. edb:split-section::\n\n  Congratulations! This first version of the data model's schema is *stored in a file on disk*. Now you need to signal the database to actually create types for ``Deck`` and ``Card`` in the database.\n\n  To make |Gel| do that, you need to do two quick steps:\n\n  1. **Create a migration**: a \"migration\" is a file containing a set of low level instructions that define how the database schema should change. It records any additions, modifications, or deletions to your schema in a way that the database can understand.\n\n     .. note::\n\n       When you are changing existing schema, the CLI migration tool might ask questions to ensure that it understands your changes exactly. Since the existing schema was empty, the CLI will skip asking any questions and simply create the migration file.\n\n  2. **Apply the migration**: This executes the migration file on the database, instructing |Gel| to implement the recorded changes in the database. Essentially, this step updates the database structure to match your defined schema, ensuring that the ``Deck`` and ``Card`` types are created and ready for use.\n\n  .. code-block:: sh\n\n    $ uvx gel migration create\n    Created ./dbschema/migrations/00001-m125ajr.edgeql, id: m125ajrbqp7ov36s7aniefxc376ofxdlketzspy4yddd3hrh4lxmla\n    $ uvx gel migrate\n    Applying m125ajrbqp7ov36s7aniefxc376ofxdlketzspy4yddd3hrh4lxmla (00001-m125ajr.edgeql)\n    ... parsed\n    ... applied\n\n\n.. edb:split-section::\n\n  Take a look at the schema you've generated in the built-in database UI. Use this tool to visualize your data model and see the object types and links you've defined.\n\n  .. edb:split-point::\n\n  .. code-block:: sh\n\n    $ uvx gel ui\n\n  .. image:: images/schema-ui.png\n"
  },
  {
    "path": "docs/intro/quickstart/modeling/index.rst",
    "content": ".. edb:env-switcher::\n\n=================\nModeling the data\n=================\n\n.. toctree::\n  :maxdepth: 3\n  :hidden:\n\n  nextjs\n  fastapi\n"
  },
  {
    "path": "docs/intro/quickstart/modeling/nextjs.rst",
    "content": ".. _ref_quickstart_modeling:\n\n=================\nModeling the data\n=================\n\n.. edb:split-section::\n\n  The flashcards application has a simple data model, but it's interesting enough to utilize many unique features of the |Gel| schema language.\n\n  Looking at the mock data in the example JSON file ``./deck-edgeql.json``, you can see this structure in the JSON. There is a ``Card`` type that describes a single flashcard, which contains two required string properties: ``front`` and ``back``. Each ``Deck`` object has zero or more ``Card`` objects in an array.\n\n  .. code-block:: typescript\n\n    interface Card {\n      front: string;\n      back: string;\n    }\n\n    interface Deck {\n      name: string;\n      description: string | null;\n      cards: Card[];\n    }\n\n.. edb:split-section::\n\n  Starting with this simple model, add these types to the :dotgel:`dbschema/default` schema file. As you can see, the types closely mirror the JSON mock data.\n\n  Also of note, the link between ``Card`` and ``Deck`` objects creates a \"1-to-n\" relationship, where each ``Deck`` object has a link to zero or more ``Card`` objects. When you query the ``Deck.cards`` link, the cards will be unordered, so the ``Card`` type needs an explicit ``order`` property to allow sorting them at query time.\n\n  By default, when you try to delete an object that is linked to another object, the database will prevent you from doing so. We want to support removing a ``Card``, so we define a deletion policy on the ``cards`` link that allows deleting the target of this link.\n\n  .. code-block:: sdl-diff\n    :caption: dbschema/default.gel\n\n      module default {\n    +   type Card {\n    +     required order: int64;\n    +     required front: str;\n    +     required back: str;\n    +   };\n    +\n    +   type Deck {\n    +     required name: str;\n    +     description: str;\n    +     multi cards: Card {\n    +       constraint exclusive;\n    +       on target delete allow;\n    +     };\n    +   };\n      };\n\n.. edb:split-section::\n\n  Congratulations! This first version of the data model's schema is *stored in a file on disk*. Now you need to signal the database to actually create types for ``Deck`` and ``Card`` in the database.\n\n  To make |Gel| do that, you need to do two quick steps:\n\n  1. **Create a migration**: a \"migration\" is a file containing a set of low level instructions that define how the database schema should change. It records any additions, modifications, or deletions to your schema in a way that the database can understand.\n\n     .. note::\n\n       When you are changing existing schema, the CLI migration tool might ask questions to ensure that it understands your changes exactly. Since the existing schema was empty, the CLI will skip asking any questions and simply create the migration file.\n\n  2. **Apply the migration**: This executes the migration file on the database, instructing |Gel| to implement the recorded changes in the database. Essentially, this step updates the database structure to match your defined schema, ensuring that the ``Deck`` and ``Card`` types are created and ready for use.\n\n     .. note::\n\n       Notice that after the migration is applied, the CLI will automatically run the script to generate the query builder. This is a convenience feature that is enabled by the ``schema.update.after`` hook in the ``gel.toml`` file.\n\n  .. code-block:: sh\n\n    $ npx gel migration create\n    Created ./dbschema/migrations/00001-m125ajr.edgeql, id: m125ajrbqp7ov36s7aniefxc376ofxdlketzspy4yddd3hrh4lxmla\n    $ npx gel migrate\n    Applying m125ajrbqp7ov36s7aniefxc376ofxdlketzspy4yddd3hrh4lxmla (00001-m125ajr.edgeql)\n    ... parsed\n    ... applied\n    Generating query builder...\n    Detected tsconfig.json, generating TypeScript files.\n      To override this, use the --target flag.\n      Run `npx @gel/generate --help` for full options.\n    Introspecting database schema...\n    Generating runtime spec...\n    Generating cast maps...\n    Generating scalars...\n    Generating object types...\n    Generating function types...\n    Generating operators...\n    Generating set impl...\n    Generating globals...\n    Generating index...\n    Writing files to ./dbschema/edgeql-js\n    Generation complete! 🤘\n\n\n.. edb:split-section::\n\n  Take a look at the schema you've generated in the built-in database UI. Use this tool to visualize your data model and see the object types and links you've defined.\n\n  .. edb:split-point::\n\n  .. code-block:: sh\n\n    $ npx gel ui\n\n  .. image:: images/schema-ui.png\n"
  },
  {
    "path": "docs/intro/quickstart/overview/fastapi.rst",
    "content": ".. _ref_quickstart_fastapi:\n\n==========\nQuickstart\n==========\n\nWelcome to the quickstart tutorial! In this tutorial, you will update a FastAPI\nbackend for a Flashcards application to use |Gel| as your data layer. The\napplication will let users build and manage their own study decks, with each\nflashcard featuring customizable text on both sides - making it perfect for\nstudying, memorization practice, or creating educational games.\n\nDon't worry if you're new to |Gel| - you will be up and running with a working\nFastAPI backend and a local |Gel| database in just about **5 minutes**. From\nthere, you will replace the static mock data with a |Gel| powered data layer in\nroughly 30-45 minutes.\n\nBy the end of this tutorial, you will be comfortable with:\n\n*  Creating and updating a database schema\n*  Running migrations to evolve your data\n*  Writing EdgeQL queries\n*  Building an app backed by |Gel|\n\n\nFeatures of the flashcards app\n------------------------------\n\n*  Create, edit, and delete decks\n*  Add/remove cards with front/back content\n*  Clean, type-safe schema with |Gel|\n\nRequirements\n------------\n\nBefore you start, you need:\n\n*  Basic familiarity with Python and FastAPI\n*  Python 3.8+ on a Unix-like OS (Linux, macOS, or WSL)\n*  A code editor you love\n\nWhy |Gel| for FastAPI?\n----------------------\n\n*  **Type Safety**: Catch data errors before runtime\n*  **Rich Modeling**: Use object types and links to model relations\n*  **Modern Tooling**: Python-friendly schemas and migrations\n*  **Performance**: Efficient queries for complex data\n*  **Developer Experience**: An intuitive query language (EdgeQL)\n\nNeed Help?\n----------\n\nIf you run into issues while following this tutorial:\n\n-  Check the `Gel documentation <https://docs.geldata.com>`_\n-  Visit our `community Discord <https://discord.gg/gel>`_\n-  File an issue on `GitHub <https://github.com/geldata/gel>`_\n"
  },
  {
    "path": "docs/intro/quickstart/overview/index.rst",
    "content": ".. edb:env-switcher::\n\n========\nOverview\n========\n\n.. toctree::\n  :maxdepth: 3\n  :hidden:\n\n  nextjs\n  fastapi\n"
  },
  {
    "path": "docs/intro/quickstart/overview/nextjs.rst",
    "content": ".. _gel-js-quickstart:\n.. _ref_quickstart:\n\n==========\nQuickstart\n==========\n\nWelcome to the quickstart tutorial! In this tutorial, you will update a simple Next.js application to use |Gel| as your data layer. The application will let users build and manage their own study decks, with each flashcard featuring customizable text on both sides - making it perfect for studying, memorization practice, or creating educational games.\n\nDon't worry if you're new to |Gel| - you will be up and running with a working Next.js application and a local |Gel| database in just about **5 minutes**. From there, you will replace the static mock data with a |Gel| powered data layer in roughly 30-45 minutes.\n\nBy the end of this tutorial, you will be comfortable with:\n\n* Creating and updating a database schema\n* Running migrations to evolve your data\n* Writing EdgeQL queries in text and via a TypeScript query builder\n* Building an app backed by |Gel|\n\nFeatures of the flashcards app\n------------------------------\n\n* Create, edit, and delete decks\n* Add/remove cards with front/back content\n* Simple Next.js + Tailwind UI\n* Clean, type-safe schema with |Gel|\n\nRequirements\n------------\n\nBefore you start, you need:\n\n* Basic familiarity with TypeScript, Next.js, and React\n* Node.js 20+ on a Unix-like OS (Linux, macOS, or WSL)\n* A code editor you love\n\nWhy |Gel| for Next.js?\n----------------------\n\n* **Type Safety**: Catch data errors before runtime\n* **Rich Modeling**: Use object types and links to model relations\n* **Modern Tooling**: TypeScript-friendly schemas and migrations\n* **Performance**: Efficient queries for complex data\n* **Developer Experience**: An intuitive query language (EdgeQL)\n\nNeed Help?\n----------\n\nIf you run into issues while following this tutorial:\n\n* Check the `Gel documentation <https://docs.geldata.com>`_\n* Visit our `community Discord <https://discord.gg/gel>`_\n* File an issue on `GitHub <https://github.com/geldata/gel>`_\n"
  },
  {
    "path": "docs/intro/quickstart/setup/fastapi.rst",
    "content": ".. _ref_quickstart_fastapi_setup:\n\n===========================\nSetting up your environment\n===========================\n\n.. edb:split-section::\n\n  Use git to clone the `FastAPI starter template <https://github.com/geldata/quickstart-fastapi>`_ into a new directory called ``flashcards``. This will create a fully configured FastAPI project and a local |Gel| instance with an empty schema. You will see the database instance being created and the project being initialized. You are now ready to start building the application.\n\n  .. code-block:: sh\n\n    $ git clone \\\n        git@github.com:geldata/quickstart-fastapi.git \\\n        flashcards\n    $ cd flashcards\n    $ python -m venv venv\n    $ source venv/bin/activate # or venv\\Scripts\\activate on Windows\n    $ pip install -r requirements.txt\n    $ uvx gel project init\n\n.. edb:split-section::\n\n  Explore the empty database by starting our REPL from the project root.\n\n  .. code-block:: sh\n\n    $ uvx gel\n\n.. edb:split-section::\n\n  Try the following queries which will work without any schema defined.\n\n  .. code-block:: edgeql-repl\n\n    db> select 42;\n    {42}\n    db> select sum({1, 2, 3});\n    {6}\n    db> with cards := {\n    ...   (\n    ...     front := \"What is the highest mountain in the world?\",\n    ...     back := \"Mount Everest\",\n    ...   ),\n    ...   (\n    ...     front := \"Which ocean contains the deepest trench on Earth?\",\n    ...     back := \"The Pacific Ocean\",\n    ...   ),\n    ... }\n    ... select cards order by random() limit 1;\n    {\n      (\n        front := \"What is the highest mountain in the world?\",\n        back := \"Mount Everest\",\n      )\n    }\n\n.. edb:split-section::\n\n  Fun! You will create a proper data model for the application in the next step, but for now, take a look around the project we have. Here are the files that integrate |Gel|:\n\n  - ``gel.toml``: The configuration file for the |Gel| project instance. Notice that we have a ``hooks.migration.apply.after`` hook that will run ``uvx gel-py`` after migrations are applied. This will run the code generator that you will use later to get fully type-safe queries you can run from your FastAPI backend. More details on that to come!\n  - ``dbschema/``: This directory contains the schema for the database, and later supporting files like migrations, and generated code.\n  - :dotgel:`dbschema/default`: The default schema file that you'll use to define your data model. It is empty for now, but you'll add your data model to this file in the next step.\n\n  .. tabs::\n\n    .. code-tab:: toml\n      :caption: gel.toml\n\n      [instance]\n      server-version = \"6.11\"\n\n      [hooks]\n      schema.update.after = \"uvx gel-py\"\n\n    .. code-tab:: sdl\n      :caption: dbschema/default.gel\n\n      module default {\n\n      }\n"
  },
  {
    "path": "docs/intro/quickstart/setup/index.rst",
    "content": ".. edb:env-switcher::\n\n===========================\nSetting up your environment\n===========================\n\n.. toctree::\n  :maxdepth: 3\n  :hidden:\n\n  nextjs\n  fastapi\n"
  },
  {
    "path": "docs/intro/quickstart/setup/nextjs.rst",
    "content": ".. _ref_quickstart_setup:\n\n===========================\nSetting up your environment\n===========================\n\n.. edb:split-section::\n\n  Use git to clone `the Next.js starter template <https://github.com/geldata/quickstart-nextjs>`_ into a new directory called ``flashcards``. This will create a fully configured Next.js project and a local |Gel| instance with an empty schema. You will see the database instance being created and the project being initialized. You are now ready to start building the application.\n\n  .. code-block:: sh\n\n    $ git clone \\\n        git@github.com:geldata/quickstart-nextjs.git \\\n        flashcards\n    $ cd flashcards\n    $ npm install\n    $ npx gel project init\n\n\n.. edb:split-section::\n\n  Explore the empty database by starting our REPL from the project root.\n\n  .. code-block:: sh\n\n    $ npx gel\n\n.. edb:split-section::\n\n  Try the following queries which will work without any schema defined.\n\n  .. code-block:: edgeql-repl\n\n    db> select 42;\n    {42}\n    db> select sum({1, 2, 3});\n    {6}\n    db> with cards := {\n    ...   (\n    ...     front := \"What is the highest mountain in the world?\",\n    ...     back := \"Mount Everest\",\n    ...   ),\n    ...   (\n    ...     front := \"Which ocean contains the deepest trench on Earth?\",\n    ...     back := \"The Pacific Ocean\",\n    ...   ),\n    ... }\n    ... select cards order by random() limit 1;\n    {\n      (\n        front := \"What is the highest mountain in the world?\",\n        back := \"Mount Everest\",\n      )\n    }\n\n.. edb:split-section::\n\n  Fun! You will create a proper data model for the application in the next step, but for now, take a look around the project you've just created. Most of the project files will be familiar if you've worked with Next.js before. Here are the files that integrate |Gel|:\n\n  - ``gel.toml``: The configuration file for the |Gel| project instance. Notice that we have a ``hooks.migration.apply.after`` hook that will run ``npx @gel/generate edgeql-js`` after migrations are applied. This will generate the query builder code that you'll use to interact with the database. More details on that to come!\n  - ``dbschema/``: This directory contains the schema for the database, and later supporting files like migrations, and generated code.\n  - :dotgel:`dbschema/default`: The default schema file that you'll use to define your data model. It is empty for now, but you'll add your data model to this file in the next step.\n  - ``lib/gel.ts``: A utility module that exports the |Gel| client, which you'll use to interact with the database.\n\n  .. tabs::\n\n    .. code-tab:: toml\n      :caption: gel.toml\n\n      [instance]\n      server-version = \"6.11\"\n\n      [hooks]\n      schema.update.after = \"npx @gel/generate edgeql-js\"\n\n    .. code-tab:: sdl\n      :caption: dbschema/default.gel\n\n      module default {\n\n      }\n\n    .. code-tab:: typescript\n      :caption: lib/gel.ts\n\n      import { createClient } from \"gel\";\n\n      export const client = createClient();\n"
  },
  {
    "path": "docs/intro/quickstart/working/fastapi.rst",
    "content": ".. _ref_quickstart_fastapi_working:\n\n=====================\nWorking with the data\n=====================\n\nIn this section, you will update the existing FastAPI application to use |Gel| to store and query data, instead of a JSON file. Having a working application with mock data allows you to focus on learning how |Gel| works, without getting bogged down by the details of the application.\n\nBulk importing of data\n======================\n\n.. edb:split-section::\n\n  First, update the imports and Pydantic models to use UUID instead of string for ID fields, since this is what |Gel| returns. You also need to initialize the |Gel| client and import the asyncio module to work with async functions.\n\n  .. code-block:: python-diff\n    :caption: main.py\n\n      from fastapi import FastAPI, HTTPException\n      from pydantic import BaseModel\n      from typing import List, Optional\n    - import json\n    - from pathlib import Path\n    + from uuid import UUID\n    + from gel import create_async_client\n    + import asyncio\n\n      app = FastAPI(title=\"Flashcards API\")\n\n      # Pydantic models\n      class CardBase(BaseModel):\n          front: str\n          back: str\n\n      class Card(CardBase):\n    -     id: str\n    +     id: UUID\n\n      class DeckBase(BaseModel):\n          name: str\n          description: Optional[str] = None\n\n      class DeckCreate(DeckBase):\n          cards: List[CardBase]\n\n      class Deck(DeckBase):\n    -     id: str\n    +     id: UUID\n          cards: List[Card]\n\n    - DATA_DIR = Path(__file__).parent / \"data\"\n    - DECKS_FILE = DATA_DIR / \"decks.json\"\n    + client = create_async_client()\n\n\n.. edb:split-section::\n\n   Next, update the deck import operation to use |Gel| to create the deck and cards. The operation creates cards first, then creates a deck with links to the cards. Finally, it fetches the newly created deck with all required fields.\n\n   .. note::\n\n      Notice the ``{ ** }`` in the query. This is a shorthand for selecting all fields of the object. It's useful when you want to return the entire object without specifying each field. In our case, we want to return the entire deck object with all the nested fields.\n\n   .. code-block:: python-diff\n    :caption: main.py\n\n      from fastapi import FastAPI, HTTPException\n      from pydantic import BaseModel\n      from typing import List, Optional\n      from uuid import UUID\n      from gel import create_async_client\n      import asyncio\n\n      app = FastAPI(title=\"Flashcards API\")\n\n      # Pydantic models\n      class CardBase(BaseModel):\n          front: str\n          back: str\n\n      class Card(CardBase):\n          id: UUID\n\n      class DeckBase(BaseModel):\n          name: str\n          description: Optional[str] = None\n\n      class DeckCreate(DeckBase):\n          cards: List[CardBase]\n\n      class Deck(DeckBase):\n          id: UUID\n          cards: List[Card]\n\n      client = create_client()\n\n    - DATA_DIR.mkdir(exist_ok=True)\n    - if not DECKS_FILE.exists():\n    -     DECKS_FILE.write_text(\"[]\")\n\n    - def read_decks() -> List[Deck]:\n    -     content = DECKS_FILE.read_text()\n    -     data = json.loads(content)\n    -     return [Deck(**deck) for deck in data]\n    -\n    - def write_decks(decks: List[Deck]) -> None:\n    -     data = [deck.model_dump() for deck in decks]\n    -     DECKS_FILE.write_text(json.dumps(data, indent=2))\n\n      @app.post(\"/decks/import\", response_model=Deck)\n      async def import_deck(deck: DeckCreate):\n    -     decks = read_decks()\n    -     new_deck = Deck(\n    -         id=str(uuid.uuid4()),\n    -         name=deck.name,\n    -         description=deck.description,\n    -         cards=[Card(id=str(uuid.uuid4()), **card.model_dump())\n    -                for card in deck.cards]\n    -     )\n    -     decks.append(new_deck)\n    -     write_decks(decks)\n    -     return new_deck\n    +     card_ids = []\n    +     for i, card in enumerate(deck.cards):\n    +         created_card = await client.query_single(\"\"\"\n    +             insert Card {\n    +                 front := <str>$front,\n    +                 back := <str>$back,\n    +                 order := <int64>$order\n    +             }\n    +         \"\"\", front=card.front, back=card.back, order=i)\n    +         card_ids.append(created_card.id)\n    +\n    +     new_deck = await client.query_single(\"\"\"\n    +         select(\n    +             insert Deck {\n    +                 name := <str>$name,\n    +                 description := <optional str>$description,\n    +                 cards := (\n    +                     select Card\n    +                     filter contains(<array<uuid>>$card_ids, .id)\n    +                 )\n    +             }\n    +         ) { ** }\n    +     \"\"\", name=deck.name, description=deck.description,\n    +          card_ids=card_ids)\n    +\n    +     return new_deck\n\n.. edb:split-section::\n\n  The above works but isn't atomic - if any single query fails, you could end up with partial data. Let's wrap it in a transaction:\n\n  .. code-block:: python-diff\n    :caption: main.py\n\n      @app.post(\"/decks/import\", response_model=Deck)\n      async def import_deck(deck: DeckCreate):\n    +     async for tx in client.transaction():\n    +         async with tx:\n              card_ids = []\n              for i, card in enumerate(deck.cards):\n    -              created_card = await client.query_single(\n    +              created_card = await tx.query_single(\n                       \"\"\"\n                       insert Card {\n                           front := <str>$front,\n                           back := <str>$back,\n                           order := <int64>$order\n                       }\n                       \"\"\",\n                       front=card.front,\n                       back=card.back,\n                       order=i,\n                   )\n                   card_ids.append(created_card.id)\n\n    -         new_deck = await client.query_single(\"\"\"\n    +         new_deck = await tx.query_single(\"\"\"\n                  select(\n                      insert Deck {\n                          name := <str>$name,\n                          description := <optional str>$description,\n                          cards := (\n                              select Card\n                              filter .id IN array_unpack(<array<uuid>>$card_ids)\n                          )\n                      }\n                  ) { ** }\n                  \"\"\",\n                  name=deck.name,\n                  description=deck.description,\n                  card_ids=card_ids,\n              )\n\n          return new_deck\n\n.. edb:split-section::\n\n  One of the most powerful features of EdgeQL is the ability to compose complex queries in a way that is both readable and efficient. Use this super-power to create a single query that inserts the deck and cards, along with their links, in one efficient query.\n\n  This new query uses a ``for`` expression to iterate over the set of cards, and sets the ``Deck.cards`` link to the result of inserting each card. This is logically equivalent to the previous approach, but is more efficient since it inserts the deck and cards in a single query.\n\n  .. code-block:: python-diff\n    :caption: main.py\n\n      @app.post(\"/decks/import\", response_model=Deck)\n      async def import_deck(deck: DeckCreate):\n    -     async for tx in client.transaction():\n    -         async with tx:\n    -         card_ids = []\n    -         for i, card in enumerate(deck.cards):\n    -              created_card = await tx.query_single(\n    -                  \"\"\"\n    -                  insert Card {\n    -                      front := <str>$front,\n    -                      back := <str>$back,\n    -                      order := <int64>$order\n    -                  }\n    -                  \"\"\",\n    -                  front=card.front,\n    -                  back=card.back,\n    -                  order=i,\n    -              )\n    -              card_ids.append(created_card.id)\n    -\n    -         new_deck = await client.query_single(\"\"\"\n    -             select(\n    -                 insert Deck {\n    -                     name := <str>$name,\n    -                     description := <optional str>$description,\n    -                     cards := (\n    -                         select Card\n    -                         filter .id IN array_unpack(<array<uuid>>$card_ids)\n    -                     )\n    -                 }\n    -             ) { ** }\n    -             \"\"\",\n    -             name=deck.name,\n    -             description=deck.description,\n    -             card_ids=card_ids,\n    -         )\n    +     cards_data = [(c.front, c.back, i) for i, c in enumerate(deck.cards)]\n    +\n    +     new_deck = await client.query_single(\"\"\"\n    +         select(\n    +             with cards := <array<tuple<str, str, int64>>>$cards_data\n    +             insert Deck {\n    +                 name := <str>$name,\n    +                 description := <optional str>$description,\n    +                 cards := (\n    +                     for card in array_unpack(cards)\n    +                     insert Card {\n    +                         front := card.0,\n    +                         back := card.1,\n    +                         order := card.2\n    +                     }\n    +                 )\n    +             }\n    +         ) { ** }\n    +     \"\"\", name=deck.name, description=deck.description,\n    +          cards_data=cards_data)\n\n          return new_deck\n\nUpdating data\n=============\n\n.. edb:split-section::\n\n  Next, update the deck operations. The update operation needs to handle partial updates of name and description:\n\n  .. code-block:: python-diff\n    :caption: main.py\n\n      @app.put(\"/decks/{deck_id}\", response_model=Deck)\n      async def update_deck(deck_id: UUID, deck_update: DeckBase):\n    -     decks = read_decks()\n    -     deck = next((deck for deck in decks if deck.id == deck_id), None)\n    -     if not deck:\n    -         raise HTTPException(status_code=404, detail=\"Deck not found\")\n    -\n    -     deck.name = deck_update.name\n    -     deck.description = deck_update.description\n    -     write_decks(decks)\n    -     return deck\n    +     # Build update sets based on provided fields\n    +     sets = []\n    +     params = {\"id\": deck_id}\n    +\n    +     if deck_update.name is not None:\n    +         sets.append(\"name := <str>$name\")\n    +         params[\"name\"] = deck_update.name\n    +\n    +     if deck_update.description is not None:\n    +         sets.append(\"description := <optional str>$description\")\n    +         params[\"description\"] = deck_update.description\n    +\n    +     if not sets:\n    +         return await get_deck(deck_id)\n    +\n    +     updated_deck = await client.query(f\"\"\"\n    +         with updated := (\n    +             update Deck\n    +             filter .id = <uuid>$id\n    +             set {{ {', '.join(sets)} }}\n    +         )\n    +         select updated {{ ** }}\n    +     \"\"\", **params)\n    +\n    +     if not updated_deck:\n    +         raise HTTPException(status_code=404, detail=\"Deck not found\")\n    +\n    +     return updated_deck\n\n\nAdding linked data\n==================\n\n.. edb:split-section::\n\n  Now, update the add card operation to use |Gel|. This operation will insert a new ``Card`` object and update the ``Deck.cards`` set to include the new ``Card`` object. Notice that the ``order`` property is set by selecting the maximum ``order`` property of this ``Deck.cards`` set and incrementing it by 1.\n\n  The syntax for adding an object to a set of links is ``{ \"+=\": object }``. You can think of this as a shortcut for setting the link set to the current set plus the new object.\n\n  .. code-block:: python-diff\n      :caption: main.py\n\n        @app.post(\"/decks/{deck_id}/cards\", response_model=Card)\n        async def add_card(deck_id: UUID, card: CardBase):\n      -     decks = read_decks()\n      -     deck = next((deck for deck in decks if deck.id == deck_id), None)\n      -     if not deck:\n      -         raise HTTPException(status_code=404, detail=\"Deck not found\")\n      -\n      -     new_card = Card(id=str(uuid.uuid4()), **card.model_dump())\n      -     deck.cards.append(new_card)\n      -     write_decks(decks)\n      -     return new_card\n      +     new_card = await client.query_single(\n      +         \"\"\"\n      +         with\n      +             deck := (select Deck filter .id = <uuid>$id),\n      +             order := (max(deck.cards.order) + 1),\n      +             new_card := (\n      +                 insert Card {\n      +                     front := <str>$front,\n      +                     back := <str>$back,\n      +                     order := order,\n      +                 }\n      +             ),\n      +             updated := (\n      +                 update deck\n      +                 set {\n      +                     cards += new_card\n      +                 }\n      +             ),\n      +         select new_card { ** }\n      +         \"\"\",\n      +         id=deck_id,\n      +         front=card.front,\n      +         back=card.back,\n      +     )\n      +\n      +     if not new_card:\n      +         raise HTTPException(status_code=404, detail=\"Deck not found\")\n      +\n      +     return new_card\n\n\nDeleting linked data\n====================\n\n.. edb:split-section::\n\n  As the next step, update the card deletion operation to use |Gel| to remove a card from a deck:\n\n  .. code-block:: python-diff\n    :caption: main.py\n\n      @app.delete(\"/cards/{card_id}\")\n      async def delete_card(card_id: str):\n    -     decks = read_decks()\n    -     deck = next((deck for deck in decks if deck.id == deck_id), None)\n    -     if not deck:\n    -         raise HTTPException(status_code=404, detail=\"Deck not found\")\n    -\n    -     deck.cards = [card for card in deck.cards if card.id != card_id]\n    -     write_decks(decks)\n    +     deleted = await client.query_single(\"\"\"\n    +         delete Card filter .id = <uuid>$card_id\n    +     \"\"\", card_id=card_id)\n    +\n    +     if not deleted:\n    +         raise HTTPException(status_code=404, detail=\"Card not found\")\n    +\n          return {\"message\": \"Card deleted\"}\n\nQuerying data\n=============\n\n.. edb:split-section::\n\n  Finally, update the query endpoints to fetch data from |Gel|:\n\n  .. code-block:: python-diff\n    :caption: main.py\n\n      @app.get(\"/decks\", response_model=List[Deck])\n      async def get_decks():\n    -     return read_decks()\n    +     decks = await client.query(\"\"\"\n    +         select Deck {\n    +             id,\n    +             name,\n    +             description,\n    +             cards := (\n    +                 select .cards {\n    +                     id,\n    +                     front,\n    +                     back\n    +                 }\n    +                 order by .order\n    +             )\n    +         }\n    +     \"\"\")\n    +     return decks\n\n      @app.get(\"/decks/{deck_id}\", response_model=Deck)\n      async def get_deck(deck_id: UUID):\n    -     decks = read_decks()\n    -     deck = next((deck for deck in decks if deck.id == deck_id), None)\n    -     if not deck:\n    -         raise HTTPException(status_code=404, detail=f\"Deck with id {deck_id} not found\")\n    -     return deck\n    +     deck = await client.query_single(\"\"\"\n    +         select Deck {\n    +             id,\n    +             name,\n    +             description,\n    +             cards := (\n    +                 select .cards {\n    +                     id,\n    +                     front,\n    +                     back\n    +                 }\n    +                 order by .order\n    +             )\n    +         }\n    +         filter .id = <uuid>$id\n    +     \"\"\", id=deck_id)\n    +\n    +     if not deck:\n    +         raise HTTPException(\n    +             status_code=404,\n    +             detail=f\"Deck with id {deck_id} not found\"\n    +         )\n    +\n    +     return deck\n\n.. edb:split-section::\n\n  You can now run your FastAPI application with:\n\n  .. code-block:: sh\n\n    $ uvicorn main:app --reload\n\n.. edb:split-section::\n\n  The API documentation will be available at http://localhost:8000/docs. You can use this interface to test your endpoints and import the sample flashcard deck.\n\n  .. image:: images/flashcards-api.png\n"
  },
  {
    "path": "docs/intro/quickstart/working/index.rst",
    "content": ".. edb:env-switcher::\n\n=====================\nWorking with the data\n=====================\n\n.. toctree::\n  :maxdepth: 3\n  :hidden:\n\n  nextjs\n  fastapi\n"
  },
  {
    "path": "docs/intro/quickstart/working/nextjs.rst",
    "content": ".. _ref_quickstart_working:\n\n=====================\nWorking with the data\n=====================\n\nIn this section, you will update the existing application to use |Gel| to store and query data, instead of a static JSON file. Having a working application with mock data allows you to focus on learning how |Gel| works, without getting bogged down by the details of the application.\n\nBulk importing of data\n======================\n\n.. edb:split-section::\n\n  Begin by updating the server action to import a deck with cards. Loop through each card in the deck and insert it, building an array of IDs as you go. This array of IDs will be used to set the ``cards`` link on the ``Deck`` object after all cards have been inserted.\n\n  The array of card IDs is initially an array of strings. To satisfy the |Gel| type system, which expects the ``id`` property of ``Card`` objects to be a ``uuid`` rather than a ``str``, you need to cast the array of strings to an array of UUIDs. Use the ``e.literal(e.array(e.uuid), cardIds)`` function to perform this casting.\n\n  The function ``e.contains(cardIdsLiteral, c.id)`` from our standard library checks if a value is present in an array and returns a boolean. When inserting the ``Deck`` object, set the ``cards`` to the result of selecting only the ``Card`` objects whose ``id`` is included in the ``cardIds`` array.\n\n  .. code-block:: typescript-diff\n    :caption: app/actions.ts\n\n      \"use server\";\n\n    - import { readFile, writeFile } from \"node:fs/promises\";\n    + import { client } from \"@/lib/gel\";\n    + import e from \"@/dbschema/edgeql-js\";\n      import { revalidatePath } from \"next/cache\";\n    - import { RawJSONDeck, Deck } from \"@/lib/models\";\n    + import { RawJSONDeck } from \"@/lib/models\";\n\n      export async function importDeck(formData: FormData) {\n        const file = formData.get(\"file\") as File;\n        const rawDeck = JSON.parse(await file.text()) as RawJSONDeck;\n        const deck = {\n          ...rawDeck,\n    -     id: crypto.randomUUID(),\n    -     cards: rawDeck.cards.map((card) => ({\n    +     cards: rawDeck.cards.map((card, index) => ({\n            ...card,\n    -       id: crypto.randomUUID(),\n    +       order: index,\n          })),\n        };\n    -\n    -   const existingDecks = JSON.parse(\n    -     await readFile(\"./decks.json\", \"utf-8\")\n    -   ) as Deck[];\n    -\n    -   await writeFile(\n    -     \"./decks.json\",\n    -     JSON.stringify([...existingDecks, deck], null, 2)\n    -   );\n    +   const cardIds: string[] = [];\n    +   for (const card of deck.cards) {\n    +     const createdCard = await e\n    +       .insert(e.Card, {\n    +         front: card.front,\n    +         back: card.back,\n    +         order: card.order,\n    +       })\n    +       .run(client);\n    +\n    +     cardIds.push(createdCard.id);\n    +   }\n    +\n    +   const cardIdsLiteral = e.literal(e.array(e.uuid), cardIds);\n    +\n    +   await e.insert(e.Deck, {\n    +     name: deck.name,\n    +     description: deck.description,\n    +     cards: e.select(e.Card, (c) => ({\n    +       filter: e.contains(cardIdsLiteral, c.id),\n    +     })),\n    +   }).run(client);\n\n        revalidatePath(\"/\");\n      }\n\n.. edb:split-section::\n\n  This works, but you might notice that it is not atomic. For instance, if one of the ``Card`` objects fails to insert, the entire operation will fail and the ``Deck`` will not be inserted, but some data will still linger. To make this operation atomic, update the ``importDeck`` action to use a transaction.\n\n  .. code-block:: typescript-diff\n    :caption: app/actions.ts\n\n      \"use server\";\n\n      import { client } from \"@/lib/gel\";\n      import e from \"@/dbschema/edgeql-js\";\n      import { revalidatePath } from \"next/cache\";\n      import { RawJSONDeck } from \"@/lib/models\";\n\n      export async function importDeck(formData: FormData) {\n        const file = formData.get(\"file\") as File;\n        const rawDeck = JSON.parse(await file.text()) as RawJSONDeck;\n        const deck = {\n          ...rawDeck,\n          cards: rawDeck.cards.map((card, index) => ({\n            ...card,\n            order: index,\n          })),\n        };\n    +   await client.transaction(async (tx) => {\n          const cardIds: string[] = [];\n          for (const card of deck.cards) {\n            const createdCard = await e\n              .insert(e.Card, {\n                front: card.front,\n                back: card.back,\n                order: card.order,\n              })\n    -         .run(client);\n    +         .run(tx);\n\n            cardIds.push(createdCard.id);\n          }\n\n          const cardIdsLiteral = e.literal(e.array(e.uuid), cardIds);\n\n          await e.insert(e.Deck, {\n            name: deck.name,\n            description: deck.description,\n            cards: e.select(e.Card, (c) => ({\n              filter: e.contains(cardIdsLiteral, c.id),\n            })),\n    -     }).run(client);\n    +     }).run(tx);\n    +   });\n\n        revalidatePath(\"/\");\n      }\n\n.. edb:split-section::\n\n  You might think this is as good as it gets, and many ORMs will create a similar set of queries. However, with the query builder, you can improve this by crafting a single query that inserts the ``Deck`` and ``Card`` objects, along with their links, in one efficient query.\n\n  The first thing to notice is that the ``e.params`` function is used to define parameters for your query instead of embedding literal values directly. This approach eliminates the need for casting, as was necessary with the ``cardIds`` array. By defining the ``cards`` parameter as an array of tuples, you ensure full type safety with both TypeScript and the database.\n\n  Another key feature of this query builder expression is the ``e.for(e.array_unpack(params.cards), (card) => {...})`` construct. This expression converts the array of tuples into a set of tuples and generates a set containing an expression for each element. Essentially, you assign the ``Deck.cards`` set of ``Card`` objects to the result of inserting each element from the ``cards`` array. This is similar to what you were doing before by selecting all ``Card`` objects by their ``id``, but is more efficient since you are inserting the ``Deck`` and all ``Card`` objects in one query.\n\n  .. code-block:: typescript-diff\n    :caption: app/actions.ts\n\n      \"use server\";\n\n      import { client } from \"@/lib/gel\";\n      import e from \"@/dbschema/edgeql-js\";\n      import { revalidatePath } from \"next/cache\";\n      import { RawJSONDeck } from \"@/lib/models\";\n\n      export async function importDeck(formData: FormData) {\n        const file = formData.get(\"file\") as File;\n        const rawDeck = JSON.parse(await file.text()) as RawJSONDeck;\n        const deck = {\n          ...rawDeck,\n          cards: rawDeck.cards.map((card, index) => ({\n            ...card,\n            order: index,\n          })),\n        };\n    -   await client.transaction(async (tx) => {\n    -     const cardIds: string[] = [];\n    -     for (const card of deck.cards) {\n    -       const createdCard = await e\n    -         .insert(e.Card, {\n    -           front: card.front,\n    -           back: card.back,\n    -           order: card.order,\n    -         })\n    -         .run(tx);\n    -\n    -       cardIds.push(createdCard.id);\n    -     }\n    -\n    -     const cardIdsLiteral = e.literal(e.array(e.uuid), cardIds);\n    -\n    -     await e.insert(e.Deck, {\n    -       name: deck.name,\n    -       description: deck.description,\n    -       cards: e.select(e.Card, (c) => ({\n    -         filter: e.contains(cardIdsLiteral, c.id),\n    -       })),\n    -     }).run(tx);\n    -   });\n    +   await e\n    +     .params(\n    +       {\n    +         name: e.str,\n    +         description: e.optional(e.str),\n    +         cards: e.array(e.tuple({ front: e.str, back: e.str, order: e.int64 })),\n    +       },\n    +       (params) =>\n    +         e.insert(e.Deck, {\n    +           name: params.name,\n    +           description: params.description,\n    +           cards: e.for(e.array_unpack(params.cards), (card) =>\n    +             e.insert(e.Card, {\n    +               front: card.front,\n    +               back: card.back,\n    +               order: card.order,\n    +             })\n    +           ),\n    +         })\n    +     )\n    +     .run(client, deck);\n\n        revalidatePath(\"/\");\n      }\n\nUpdating data\n=============\n\n.. edb:split-section::\n\n  Next, you will update the Server Actions for each ``Deck`` object: ``updateDeck``, ``addCard``, and ``deleteCard``. Start with ``updateDeck``, which is the most complex because it is dynamic. You can set either the ``title`` or ``description`` fields in an update. Use the dynamic nature of the query builder to generate separate queries based on which fields are present in the form data.\n\n  This may seem a bit intimidating at first, but the key to making this query dynamic is the ``nameSet`` and ``descriptionSet`` variables. These variables conditionally add the ``name`` or ``description`` fields to the ``set`` parameter of the ``update`` call.\n\n  .. code-block:: typescript-diff\n    :caption: app/deck/[id]/actions.ts\n\n      \"use server\";\n\n      import { revalidatePath } from \"next/cache\";\n      import { readFile, writeFile } from \"node:fs/promises\";\n    + import { client } from \"@/lib/gel\";\n    + import e from \"@/dbschema/edgeql-js\";\n      import { Deck } from \"@/lib/models\";\n\n      export async function updateDeck(formData: FormData) {\n        const id = formData.get(\"id\");\n        const name = formData.get(\"name\");\n        const description = formData.get(\"description\");\n\n        if (\n          typeof id !== \"string\" ||\n          (typeof name !== \"string\" &&\n          typeof description !== \"string\")\n        ) {\n          return;\n        }\n\n    -   const decks = JSON.parse(\n    -     await readFile(\"./decks.json\", \"utf-8\")\n    -   ) as Deck[];\n    -   decks[index].name = name ?? decks[index].name;\n    +   const nameSet = typeof name === \"string\" ? { name } : {};\n    -   decks[index].description = description ?? decks[index].description;\n    +   const descriptionSet =\n    +     typeof description === \"string\" ? { description: description || null } : {};\n\n    +   await e\n    +     .update(e.Deck, (d) => ({\n    +       filter_single: e.op(d.id, \"=\", e.uuid(id)),\n    +       set: {\n    +         ...nameSet,\n    +         ...descriptionSet,\n    +       },\n    +     })).run(client);\n    -   await writeFile(\"./decks.json\", JSON.stringify(decks, null, 2));\n        revalidatePath(`/deck/${id}`);\n      }\n\n      export async function addCard(formData: FormData) {\n        const deckId = formData.get(\"deckId\");\n        const front = formData.get(\"front\");\n        const back = formData.get(\"back\");\n\n        if (\n          typeof deckId !== \"string\" ||\n          typeof front !== \"string\" ||\n          typeof back !== \"string\"\n        ) {\n          return;\n        }\n\n        const decks = JSON.parse(await readFile(\"./decks.json\", \"utf-8\")) as Deck[];\n\n        const deck = decks.find((deck) => deck.id === deckId);\n        if (!deck) {\n          return;\n        }\n\n        deck.cards.push({ front, back, id: crypto.randomUUID() });\n        await writeFile(\"./decks.json\", JSON.stringify(decks, null, 2));\n\n        revalidatePath(`/deck/${deckId}`);\n      }\n\n      export async function deleteCard(formData: FormData) {\n        const cardId = formData.get(\"cardId\");\n\n        if (typeof cardId !== \"string\") {\n          return;\n        }\n\n        const decks = JSON.parse(await readFile(\"./decks.json\", \"utf-8\")) as Deck[];\n        const deck = decks.find((deck) => deck.cards.some((card) => card.id === cardId));\n        if (!deck) {\n          return;\n        }\n\n        deck.cards = deck.cards.filter((card) => card.id !== cardId);\n        await writeFile(\"./decks.json\", JSON.stringify(decks, null, 2));\n\n        revalidatePath(`/`);\n      }\n\nAdding linked data\n==================\n\n.. edb:split-section::\n\n  For the ``addCard`` action, you need to insert a new ``Card`` object and update the ``Deck.cards`` set to include the new ``Card`` object. Notice that the ``order`` property is set by selecting the maximum ``order`` property of this ``Deck.cards`` set and incrementing it by 1.\n\n  The syntax for adding an object to a set of links is ``{ \"+=\": object }``. You can think of this as a shortcut for setting the link set to the current set plus the new object.\n\n  .. code-block:: typescript-diff\n    :caption: app/deck/[id]/actions.ts\n\n      \"use server\";\n\n      import { revalidatePath } from \"next/cache\";\n      import { readFile, writeFile } from \"node:fs/promises\";\n      import { client } from \"@/lib/gel\";\n      import e from \"@/dbschema/edgeql-js\";\n      import { Deck } from \"@/lib/models\";\n\n      export async function updateDeck(formData: FormData) {\n        const id = formData.get(\"id\");\n        const name = formData.get(\"name\");\n        const description = formData.get(\"description\");\n\n        if (\n          typeof id !== \"string\" ||\n          (typeof name !== \"string\" &&\n          typeof description !== \"string\")\n        ) {\n          return;\n        }\n\n        const nameSet = typeof name === \"string\" ? { name } : {};\n        const descriptionSet =\n          typeof description === \"string\" ? { description: description || null } : {};\n\n        await e\n          .update(e.Deck, (d) => ({\n            filter_single: e.op(d.id, \"=\", e.uuid(id)),\n            set: {\n              ...nameSet,\n              ...descriptionSet,\n            },\n          })).run(client);\n        revalidatePath(`/deck/${id}`);\n      }\n\n      export async function addCard(formData: FormData) {\n        const deckId = formData.get(\"deckId\");\n        const front = formData.get(\"front\");\n        const back = formData.get(\"back\");\n\n        if (\n          typeof deckId !== \"string\" ||\n          typeof front !== \"string\" ||\n          typeof back !== \"string\"\n        ) {\n          return;\n        }\n\n    -   const decks = JSON.parse(await readFile(\"./decks.json\", \"utf-8\")) as Deck[];\n    -\n    -   const deck = decks.find((deck) => deck.id === deckId);\n    -   if (!deck) {\n    -     return;\n    -   }\n    -\n    -   deck.cards.push({ front, back, id: crypto.randomUUID() });\n    -   await writeFile(\"./decks.json\", JSON.stringify(decks, null, 2));\n    +   await e\n    +     .params(\n    +       {\n    +         front: e.str,\n    +         back: e.str,\n    +         deckId: e.uuid,\n    +       },\n    +       (params) => {\n    +         const deck = e.assert_exists(\n    +           e.select(e.Deck, (d) => ({\n    +             filter_single: e.op(d.id, \"=\", params.deckId),\n    +           }))\n    +         );\n    +\n    +         const order = e.cast(e.int64, e.max(deck.cards.order));\n    +         const card = e.insert(e.Card, {\n    +           front: params.front,\n    +           back: params.back,\n    +           order: e.op(order, \"+\", 1),\n    +         });\n    +         return e.update(deck, (d) => ({\n    +           set: {\n    +             cards: {\n    +               \"+=\": card\n    +             },\n    +           },\n    +         }))\n    +       }\n    +     )\n    +     .run(client, {\n    +       front,\n    +       back,\n    +       deckId,\n    +     });\n\n        revalidatePath(`/deck/${deckId}`);\n      }\n\n      export async function deleteCard(formData: FormData) {\n        const cardId = formData.get(\"cardId\");\n\n        if (typeof cardId !== \"string\") {\n          return;\n        }\n\n        const decks = JSON.parse(await readFile(\"./decks.json\", \"utf-8\")) as Deck[];\n        const deck = decks.find((deck) => deck.cards.some((card) => card.id === cardId));\n        if (!deck) {\n          return;\n        }\n\n        deck.cards = deck.cards.filter((card) => card.id !== cardId);\n        await writeFile(\"./decks.json\", JSON.stringify(decks, null, 2));\n\n        revalidatePath(`/`);\n      }\n\nDeleting linked data\n====================\n\n.. edb:split-section::\n\n  For the ``deleteCard`` action, delete the ``Card`` object and based on the deletion policy we set up earlier in the schema, the object will be deleted from the database and removed from the ``Deck.cards`` set.\n\n  .. code-block:: typescript-diff\n    :caption: app/deck/[id]/actions.ts\n\n      \"use server\";\n\n      import { revalidatePath } from \"next/cache\";\n    - import { readFile, writeFile } from \"node:fs/promises\";\n      import { client } from \"@/lib/gel\";\n      import e from \"@/dbschema/edgeql-js\";\n      import { Deck } from \"@/lib/models\";\n\n      export async function updateDeck(formData: FormData) {\n        const id = formData.get(\"id\");\n        const name = formData.get(\"name\");\n        const description = formData.get(\"description\");\n\n        if (\n          typeof id !== \"string\" ||\n          (typeof name !== \"string\" &&\n          typeof description !== \"string\")\n        ) {\n          return;\n        }\n\n        const nameSet = typeof name === \"string\" ? { name } : {};\n        const descriptionSet =\n          typeof description === \"string\" ? { description: description || null } : {};\n\n        await e\n          .update(e.Deck, (d) => ({\n            filter_single: e.op(d.id, \"=\", e.uuid(id)),\n            set: {\n              ...nameSet,\n              ...descriptionSet,\n            },\n          })).run(client);\n        revalidatePath(`/deck/${id}`);\n      }\n\n      export async function addCard(formData: FormData) {\n        const deckId = formData.get(\"deckId\");\n        const front = formData.get(\"front\");\n        const back = formData.get(\"back\");\n\n        if (\n          typeof deckId !== \"string\" ||\n          typeof front !== \"string\" ||\n          typeof back !== \"string\"\n        ) {\n          return;\n        }\n\n        await e\n          .params(\n            {\n              front: e.str,\n              back: e.str,\n              deckId: e.uuid,\n            },\n            (params) => {\n              const deck = e.assert_exists(\n                e.select(e.Deck, (d) => ({\n                  filter_single: e.op(d.id, \"=\", params.deckId),\n                }))\n              );\n\n              const order = e.cast(e.int64, e.max(deck.cards.order));\n              const card = e.insert(e.Card, {\n                front: params.front,\n                back: params.back,\n                order: e.op(order, \"+\", 1),\n              });\n              return e.update(deck, (d) => ({\n                set: {\n                  cards: {\n                    \"+=\": card\n                  },\n                },\n              }))\n            }\n          )\n          .run(client, {\n            front,\n            back,\n            deckId,\n          });\n\n        revalidatePath(`/deck/${deckId}`);\n      }\n\n      export async function deleteCard(formData: FormData) {\n        const cardId = formData.get(\"cardId\");\n\n        if (typeof cardId !== \"string\") {\n          return;\n        }\n\n    -   const decks = JSON.parse(await readFile(\"./decks.json\", \"utf-8\")) as Deck[];\n    -   const deck = decks.find((deck) => deck.cards.some((card) => card.id === cardId));\n    -   if (!deck) {\n    -     return;\n    -   }\n    -\n    -   deck.cards = deck.cards.filter((card) => card.id !== cardId);\n    -   await writeFile(\"./decks.json\", JSON.stringify(decks, null, 2));\n    +   await e\n    +     .params({ id: e.uuid }, (params) =>\n    +       e.delete(e.Card, (c) => ({\n    +         filter_single: e.op(c.id, \"=\", params.id),\n    +       }))\n    +     )\n    +     .run(client, { id: cardId });\n    +\n\n        revalidatePath(`/`);\n      }\n\nQuerying data\n=============\n\n.. edb:split-section::\n\n  Next, update the two ``queries.ts`` methods: ``getDecks`` and ``getDeck``.\n\n  .. tabs::\n\n    .. code-tab:: typescript-diff\n      :caption: app/queries.ts\n\n      - import { readFile } from \"node:fs/promises\";\n      + import { client } from \"@/lib/gel\";\n      + import e from \"@/dbschema/edgeql-js\";\n      -\n      - import { Deck } from \"@/lib/models\";\n\n        export async function getDecks() {\n      -   const decks = JSON.parse(await readFile(\"./decks.json\", \"utf-8\")) as Deck[];\n      +   const decks = await e.select(e.Deck, (deck) => ({\n      +     id: true,\n      +     name: true,\n      +     description: true,\n      +     cards: e.select(deck.cards, (card) => ({\n      +       id: true,\n      +       front: true,\n      +       back: true,\n      +       order_by: card.order,\n      +     })),\n      +   })).run(client);\n\n          return decks;\n        }\n\n    .. code-tab:: typescript-diff\n      :caption: app/deck/[id]/queries.ts\n\n      - import { readFile } from \"node:fs/promises\";\n      - import { Deck } from \"@/lib/models\";\n      + import { client } from \"@/lib/gel\";\n      + import e from \"@/dbschema/edgeql-js\";\n\n        export async function getDeck({ id }: { id: string }) {\n      -   const decks = JSON.parse(await readFile(\"./decks.json\", \"utf-8\")) as Deck[];\n      -   return decks.find((deck) => deck.id === id) ?? null;\n      +   return await e\n      +     .select(e.Deck, (deck) => ({\n      +       filter_single: e.op(deck.id, \"=\", e.uuid(id)),\n      +       id: true,\n      +       name: true,\n      +       description: true,\n      +       cards: e.select(deck.cards, (card) => ({\n      +         id: true,\n      +         front: true,\n      +         back: true,\n      +         order_by: card.order,\n      +       })),\n      +     }))\n      +     .run(client);\n        }\n\n.. edb:split-section::\n\n  In a terminal, run the Next.js development server.\n\n  .. code-block:: sh\n\n    $ npm run dev\n\n.. edb:split-section::\n\n  A static JSON file to seed your database with a deck of trivia cards is included in the project. Open your browser and navigate to the app at `<http://localhost:3000>`_. Use the \"Import JSON\" button to import this JSON file into your database.\n\n  .. image:: images/flashcards-import.png\n"
  },
  {
    "path": "docs/intro/schema.rst",
    "content": ".. _ref_intro_schema:\n\n======\nSchema\n======\n\n\nThis page is intended as a rapid-fire overview of Gel's schema definition\nlanguage (SDL) so you can hit the ground running with Gel. Refer to the\nlinked pages for more in-depth documentation!\n\nScalar types\n------------\n\n|Gel| implements a rigorous type system containing the following primitive\ntypes.\n\n.. list-table::\n\n  * - Strings\n    - ``str``\n  * - Booleans\n    - ``bool``\n  * - Numbers\n    - ``int16`` ``int32`` ``int64`` ``float32`` ``float64``\n      ``bigint`` ``decimal``\n  * - UUID\n    - ``uuid``\n  * - JSON\n    - ``json``\n  * - Dates and times\n    - ``datetime`` ``cal::local_datetime`` ``cal::local_date``\n      ``cal::local_time``\n  * - Durations\n    - ``duration`` ``cal::relative_duration`` ``cal::date_duration``\n  * - Binary data\n    - ``bytes``\n  * - Auto-incrementing counters\n    - ``sequence``\n  * - Enums\n    - ``enum<x, y, z>``\n\nThese primitives can be combined into arrays, tuples, and ranges.\n\n.. list-table::\n\n  * - Arrays\n    - ``array<str>``\n  * - Tuples (unnamed)\n    - ``tuple<str, int64, bool>``\n  * - Tuples (named)\n    - ``tuple<name: str, age: int64, is_awesome: bool>``\n  * - Ranges\n    - ``range<float64>``\n\nCollectively, *primitive* and *collection* types comprise Gel's *scalar\ntype system*.\n\nObject types\n------------\n\nObject types are analogous to tables in SQL. They can contain **properties**,\nwhich can correspond to any scalar types, and **links**, which can correspond\nto any object types.\n\nProperties\n----------\n\nDeclare a property by naming it and setting its type.\n\n.. code-block:: sdl\n\n    type Movie {\n      title: str;\n    }\n\nThe ``property`` keyword can be omitted for non-computed properties.\n\nSee :ref:`Schema > Object types <ref_std_object_types>`.\n\nRequired vs optional\n^^^^^^^^^^^^^^^^^^^^\n\nProperties are optional by default. Use the ``required`` keyword to make them\nrequired.\n\n.. code-block:: sdl\n\n    type Movie {\n      required title: str;       # required\n      release_year: int64;       # optional\n    }\n\nSee :ref:`Schema > Properties <ref_datamodel_props>`.\n\nConstraints\n^^^^^^^^^^^\n\nAdd a pair of curly braces after the property to define additional\ninformation, including constraints.\n\n.. code-block:: sdl\n\n    type Movie {\n      required title: str {\n        constraint exclusive;\n        constraint min_len_value(8);\n        constraint regexp(r'^[A-Za-z0-9 ]+$');\n      }\n    }\n\nSee :ref:`Schema > Constraints <ref_datamodel_constraints>`.\n\n\nComputed properties\n^^^^^^^^^^^^^^^^^^^\n\nObject types can contain *computed properties* that correspond to EdgeQL\nexpressions. This expression is dynamically computed whenever the property is\nqueried.\n\n.. code-block:: sdl\n\n    type Movie {\n      required title: str;\n      uppercase_title := str_upper(.title);\n    }\n\nSee :ref:`Schema > Computeds <ref_datamodel_computed>`.\n\nLinks\n-----\n\nObject types can have links to other object types.\n\n.. code-block:: sdl\n\n    type Movie {\n      required title: str;\n      director: Person;\n    }\n\n    type Person {\n      required name: str;\n    }\n\nThe ``link`` keyword can be omitted for non-computed links since Gel v3.\n\nUse the ``required`` and ``multi`` keywords to specify the cardinality of the\nrelation.\n\n.. code-block:: sdl\n\n    type Movie {\n      required title: str;\n\n      cinematographer: Person;             # zero or one\n      required director: Person;           # exactly one\n      multi writers: Person;               # zero or more\n      required multi actors: Person;       # one or more\n    }\n\n    type Person {\n      required name: str;\n    }\n\nTo define a one-to-one relation, use an ``exclusive`` constraint.\n\n.. code-block:: sdl\n\n    type Movie {\n      required title: str;\n      required stats: MovieStats {\n        constraint exclusive;\n      };\n    }\n\n    type MovieStats {\n      required budget: int64;\n      required box_office: int64;\n    }\n\nSee :ref:`Schema > Links <ref_datamodel_links>`.\n\nComputed links\n^^^^^^^^^^^^^^\n\nObjects can contain \"computed links\": stored expressions that return a set of\nobjects. Computed links are dynamically computed when they are referenced in\nqueries. The example below defines a backlink.\n\n.. code-block:: sdl\n\n    type Movie {\n      required title: str;\n      multi actors: Person;\n\n      # returns all movies with same title\n      multi same_title := (\n        with t := .title\n        select detached Movie filter .title = t\n      )\n    }\n\nBacklinks\n^^^^^^^^^\n\nA common use case for computed links is *backlinks*.\n\n.. code-block:: sdl\n\n    type Movie {\n      required title: str;\n      multi actors: Person;\n    }\n\n    type Person {\n      required name: str;\n      multi acted_in := .<actors[is Movie];\n    }\n\nThe computed link ``acted_in`` returns all ``Movie`` objects with a link\ncalled ``actors`` that points to the current ``Person``. The easiest way to\nunderstand backlink syntax is to split it into two parts:\n\n``.<actors``\n  This uses a special syntax ``.<`` to return all objects in the database with\n  a link called ``actors`` that points to the current object. This set could\n  conceivably contain other objects besides ``Movie``; for instance, we could\n  define a ``TVShow`` type that also included ``link actors -> Person``.\n\n``[is Movie]``\n  This is a *type filter* that filters out all objects that aren't ``Movie``\n  objects. A backlink still works without this filter, but could contain any\n  other number of objects besides ``Movie`` objects.\n\nSee :ref:`Schema > Computeds > Backlinks <ref_datamodel_links_backlinks>`.\n\nConstraints\n-----------\n\nConstraints can also be defined at the *object level*.\n\n.. code-block:: sdl\n\n    type BlogPost {\n      title: str;\n      author: User;\n\n      constraint exclusive on ((.title, .author));\n    }\n\nConstraints can contain exceptions; these are called *partial constraints*.\n\n.. code-block:: sdl\n\n    type BlogPost {\n      title: str;\n      published: bool;\n\n      constraint exclusive on (.title) except (not .published);\n    }\n\nIndexes\n-------\n\nUse ``index on`` to define indexes on an object type.\n\n.. code-block:: sdl\n\n    type Movie {\n      required title: str;\n      required release_year: int64;\n\n      index on (.title);                        # simple index\n      index on ((.title, .release_year));       # composite index\n      index on (str_trim(str_lower(.title)));   # computed index\n    }\n\nThe ``id`` property, all links, and all properties with ``exclusive``\nconstraints are automatically indexed.\n\nSee :ref:`Schema > Indexes <ref_datamodel_indexes>`.\n\nSchema mixins\n-------------\n\nObject types can be declared as ``abstract``. Non-abstract types can *extend*\nabstract types.\n\n.. code-block:: sdl\n\n    abstract type Content {\n      required title: str;\n    }\n\n    type Movie extending Content {\n      required release_year: int64;\n    }\n\n    type TVShow extending Content {\n      required num_seasons: int64;\n    }\n\nMultiple inheritance is supported.\n\n.. code-block:: sdl\n\n    abstract type HasTitle {\n      required title: str;\n    }\n\n    abstract type HasReleaseYear {\n      required release_year: int64;\n    }\n\n    type Movie extending HasTitle, HasReleaseYear {\n      sequel_to: Movie;\n    }\n\nSee :ref:`Schema > Object types > Inheritance\n<ref_datamodel_objects_inheritance>`.\n\nPolymorphism\n------------\n\nLinks can correspond to abstract types. These are known as *polymorphic links*.\n\n.. code-block:: sdl\n\n    abstract type Content {\n      required title: str;\n    }\n\n    type Movie extending Content {\n      required release_year: int64;\n    }\n\n    type TVShow extending Content {\n      required num_seasons: int64;\n    }\n\n    type Franchise {\n      required name: str;\n      multi entries: Content;\n    }\n\nSee :ref:`Schema > Links > Polymorphism\n<ref_datamodel_link_polymorphic>` and :ref:`EdgeQL > Select > Polymorphic\nqueries <ref_eql_select_polymorphic>`.\n\n"
  },
  {
    "path": "docs/intro/tutorials/ai_fastapi_searchbot.rst",
    "content": ".. _ref_guide_fastapi_gelai_searchbot:\n\n===============================\nBuild a Search Bot with FastAPI\n===============================\n\n:edb-alt-title: Building a search bot with memory using FastAPI and Gel AI\n\nIn this tutorial we're going to walk you through building a chat bot with search\ncapabilities using Gel and `FastAPI <https://fastapi.tiangolo.com/>`_.\n\nFastAPI is a framework designed to help you build web apps *fast*. Gel is a\ndata layer designed to help you figure out storage in your application - also\n*fast*. By the end of this tutorial, you will have tried out different aspects\nof using those two together.\n\nWe will start by creating an app with FastAPI, adding web search capabilities,\nand then putting search results through a language model to get a\nhuman-friendly answer. After that, we'll use Gel to implement chat history so\nthat the bot remembers previous interactions with the user. We'll finish it off\nwith semantic search-based cross-chat memory.\n\n\n1. Initialize the project\n=========================\n\n.. edb:split-section::\n\n  We're going to start by installing `uv <https://docs.astral.sh/uv/>`_ - a Python\n  package manager that's going to simplify environment management for us. You can\n  follow their `installation instructions\n  <https://docs.astral.sh/uv/getting-started/installation/>`_ or simply run:\n\n  .. code-block:: bash\n\n      $ curl -LsSf https://astral.sh/uv/install.sh | sh\n\n.. edb:split-section::\n\n  Once that is done, we can use uv to create scaffolding for our project following\n  the `documentation <https://docs.astral.sh/uv/guides/projects/>`_:\n\n  .. code-block:: bash\n\n      $ uv init searchbot \\\n        && cd searchbot\n\n.. edb:split-section::\n\n  For now, we know we're going to need Gel and FastAPI, so let's add those\n  following uv's instructions on `managing dependencies\n  <https://docs.astral.sh/uv/concepts/projects/dependencies/#optional-dependencies>`_,\n  as well as FastAPI's `installation docs\n  <https://fastapi.tiangolo.com/#installation>`_. Running ``uv sync`` after\n  that will create our virtual environment in a ``.venv`` directory and ensure\n  it's ready. As the last step, we'll activate the environment and get started.\n\n  .. note::\n\n      Every time you open a new terminal session, you should source the\n      environment before running ``python``, ``gel`` or ``fastapi`` commands.\n\n  .. code-block:: bash\n\n      $ uv add \"fastapi[standard]\" \\\n        && uv add gel \\\n        && uv sync \\\n        && source .venv/bin/activate\n\n\n2. Get started with FastAPI\n===========================\n\n.. edb:split-section::\n\n  At this stage we need to follow FastAPI's `tutorial\n  <https://fastapi.tiangolo.com/tutorial/>`_ to create the foundation of our app.\n\n  We're going to make a minimal web API with one endpoint that takes in a user\n  query as an input and echoes it as an output. First, let's make a directory\n  called ``app`` in our project root, and put an empty ``__init__.py`` there.\n\n  .. code-block:: bash\n\n     $ mkdir app && touch app/__init__.py\n\n.. edb:split-section::\n\n  Now let's create a file called ``main.py`` inside the ``app`` directory and put\n  the \"Hello World\" example in it:\n\n  .. code-block:: python\n      :caption: app/main.py\n\n      from fastapi import FastAPI\n\n      app = FastAPI()\n\n\n      @app.get(\"/\")\n      async def root():\n          return {\"message\": \"Hello World\"}\n\n\n.. edb:split-section::\n\n  To start the server, we'll run:\n\n  .. code-block:: bash\n\n      $ fastapi dev app/main.py\n\n\n.. edb:split-section::\n\n  Once the server gets up and running, we can make sure it works using FastAPI's\n  built-in UI at <http://127.0.0.1:8000/docs>_, or manually with ``curl``:\n\n  .. code-block:: bash\n\n      $ curl -X 'GET' \\\n        'http://127.0.0.1:8000/' \\\n        -H 'accept: application/json'\n\n      {\"message\":\"Hello World\"}\n\n\n.. edb:split-section::\n\n  Now, to create the search endpoint we mentioned earlier, we need to pass our\n  query as a parameter to it. We'd prefer to have it in the request's body\n  since user messages can be long.\n\n  In FastAPI land, this is done by creating a Pydantic schema and making it the\n  type of the input parameter. `Pydantic <https://docs.pydantic.dev/latest/>`_ is\n  a data validation library for Python. It has many features, but we don't\n  actually need to know about them for now. All we need to know is that FastAPI\n  uses Pydantic types to automatically figure out schemas for `input\n  <https://fastapi.tiangolo.com/tutorial/body/>`_, as well as `output\n  <https://fastapi.tiangolo.com/tutorial/response-model/>`_.\n\n  Let's add the following to our ``main.py``:\n\n  .. code-block:: python\n      :caption: app/main.py\n\n      from pydantic import BaseModel\n\n\n      class SearchTerms(BaseModel):\n          query: str\n\n      class SearchResult(BaseModel):\n          response: str | None = None\n\n\n.. edb:split-section::\n\n  Now, we can define our endpoint. We'll set the two classes we just created as\n  the new endpoint's argument and return type.\n\n  .. code-block:: python\n      :caption: app/main.py\n\n      @app.post(\"/search\")\n      async def search(search_terms: SearchTerms) -> SearchResult:\n          return SearchResult(response=search_terms.query)\n\n\n.. edb:split-section::\n\n  Same as before, we can test the endpoint using the UI, or by sending a request\n  with ``curl``:\n\n  .. code-block:: bash\n\n     $ curl -X 'POST' \\\n        'http://127.0.0.1:8000/search' \\\n        -H 'accept: application/json' \\\n        -H 'Content-Type: application/json' \\\n        -d '{ \"query\": \"string\" }'\n\n      {\n        \"response\": \"string\",\n      }\n\n3. Implement web search\n=======================\n\nNow that we have our web app infrastructure in place, let's add some substance\nto it by implementing web search capabilities.\n\n.. edb:split-section::\n\n  There're many powerful feature-rich products for LLM-driven web search. But\n  in this tutorial we're going to use a much more reliable source of real-world\n  information that is comment threads on `Hacker News\n  <https://news.ycombinator.com/>`_. Their `web API\n  <https://hn.algolia.com/api>`_ is free of charge and doesn't require an\n  account. Below is a simple function that requests a full-text search for a\n  string query and extracts a nice sampling of comment threads from each of the\n  stories that came up in the result.\n\n  We are not going to cover this code sample in too much depth. Feel free to grab\n  it save it to ``app/web.py``, or make your own.\n\n  Notice that we've created another Pydantic type called ``WebSource`` to store\n  our web search results. There's no framework-related reason for that, it's just\n  nicer than passing dictionaries around.\n\n  .. code-block:: python\n      :caption: app/web.py\n      :class: collapsible\n\n      import requests\n      from pydantic import BaseModel\n      from datetime import datetime\n      import html\n\n\n      class WebSource(BaseModel):\n          \"\"\"Type that stores search results.\"\"\"\n\n          url: str | None = None\n          title: str | None = None\n          text: str | None = None\n\n\n      def extract_comment_thread(\n          comment: dict,\n          max_depth: int = 3,\n          current_depth: int = 0,\n          max_children=3,\n      ) -> list[str]:\n          \"\"\"\n          Recursively extract comments from a thread up to max_depth.\n          Returns a list of formatted comment strings.\n          \"\"\"\n          if not comment or current_depth > max_depth:\n              return []\n\n          results = []\n\n          # Get timestamp, author and the body of the comment,\n          # then pad it with spaces so that it's offset appropriately for its depth\n\n          if comment[\"text\"]:\n              timestamp = datetime.fromisoformat(comment[\"created_at\"].replace(\"Z\", \"+00:00\"))\n              author = comment[\"author\"]\n              text = html.unescape(comment[\"text\"])\n              formatted_comment = f\"[{timestamp.strftime('%Y-%m-%d %H:%M')}] {author}: {text}\"\n              results.append((\"  \" * current_depth) + formatted_comment)\n\n          # If there're children comments, we are going to extract them too,\n          # and add them to the list.\n\n          if comment.get(\"children\"):\n              for child in comment[\"children\"][:max_children]:\n                  child_comments = extract_comment_thread(child, max_depth, current_depth + 1)\n                  results.extend(child_comments)\n\n          return results\n\n\n      def fetch_web_sources(query: str, limit: int = 5) -> list[WebSource]:\n          \"\"\"\n          For a given query perform a full-text search for stories on Hacker News.\n          From each of the matched stories extract the comment thread and format it into a single string.\n          For each story return its title, url and comment thread.\n          \"\"\"\n          search_url = \"http://hn.algolia.com/api/v1/search_by_date?numericFilters=num_comments>0\"\n\n          # Search for stories\n          response = requests.get(\n              search_url,\n              params={\n                  \"query\": query,\n                  \"tags\": \"story\",\n                  \"hitsPerPage\": limit,\n                  \"page\": 0,\n              },\n          )\n\n          response.raise_for_status()\n          search_result = response.json()\n\n          # For each search hit fetch and process the story\n          web_sources = []\n          for hit in search_result.get(\"hits\", []):\n              item_url = f\"https://hn.algolia.com/api/v1/items/{hit['story_id']}\"\n              response = requests.get(item_url)\n              response.raise_for_status()\n              item_result = response.json()\n\n              site_url = f\"https://news.ycombinator.com/item?id={hit['story_id']}\"\n              title = hit[\"title\"]\n              comments = extract_comment_thread(item_result)\n              text = \"\\n\".join(comments) if len(comments) > 0 else None\n              web_sources.append(\n                  WebSource(url=site_url, title=title, text=text)\n              )\n\n          return web_sources\n\n\n      if __name__ == \"__main__\":\n          web_sources = fetch_web_sources(\"edgedb\", limit=5)\n\n          for source in web_sources:\n              print(source.url)\n              print(source.title)\n              print(source.text)\n\n\n.. edb:split-section::\n\n  One more note: this snippet comes with an extra dependency called ``requests``,\n  which is a library for making HTTP requests. Let's add it by running:\n\n  .. code-block:: bash\n\n      $ uv add requests\n\n\n.. edb:split-section::\n\n  Now, we can test our web search on its own by running it like this:\n\n  .. code-block:: bash\n\n      $ python3 app/web.py\n\n\n.. edb:split-section::\n\n  It's time to reflect the new capabilities in our web app.\n\n  .. code-block:: python\n       :caption: app/main.py\n\n       from .web import fetch_web_sources, WebSource\n\n       async def search_web(query: str) -> list[WebSource]:\n           raw_sources = fetch_web_sources(query, limit=5)\n           return [s for s in raw_sources if s.text is not None]\n\n\n.. edb:split-section::\n\n  Now we can update the ``/search`` endpoint as follows:\n\n  .. code-block:: python-diff\n      :caption: app/main.py\n\n        class SearchResult(BaseModel):\n            response: str | None = None\n      +     sources: list[WebSource] | None = None\n\n\n        @app.post(\"/search\")\n        async def search(search_terms: SearchTerms) -> SearchResult:\n      +     web_sources = await search_web(search_terms.query)\n      -     return SearchResult(response=search_terms.query)\n      +     return SearchResult(\n      +         response=search_terms.query, sources=web_sources\n      +     )\n\n\n4. Connect to the LLM\n=====================\n\nNow that we're capable of scraping text from search results, we can forward\nthose results to the LLM to get a nice-looking summary.\n\n.. edb:split-section::\n\n  There's a million different LLMs accessible via a web API (`one\n  <https://docs.anthropic.com/en/api/getting-started>`_, `two\n  <https://ai.google.dev/gemini-api/docs>`_, `three\n  <https://ollama.com/search>`_, `four <https://docs.mistral.ai/api/>`_ to name\n  a few), feel free to choose whichever you prefer. In this tutorial we will\n  roll with OpenAI, primarily for how ubiquitous it is. To keep things somewhat\n  provider-agnostic, we're going to get completions via raw HTTP requests.\n  Let's grab API descriptions from OpenAI's `API documentation\n  <https://platform.openai.com/docs/api-reference/chat/create>`_, and set up\n  LLM generation like this:\n\n  .. code-block:: python\n      :caption: app/main.py\n\n      import requests\n      from dotenv import load_dotenv\n\n      _ = load_dotenv()\n\n\n      def get_llm_completion(system_prompt: str, messages: list[dict[str, str]]) -> str:\n          api_key = os.getenv(\"OPENAI_API_KEY\")\n          url = \"https://api.openai.com/v1/chat/completions\"\n          headers = {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {api_key}\"}\n\n          response = requests.post(\n              url,\n              headers=headers,\n              json={\n                  \"model\": \"gpt-4o-mini\",\n                  \"messages\": [\n                      {\"role\": \"developer\", \"content\": system_prompt},\n                      *messages,\n                  ],\n              },\n          )\n          response.raise_for_status()\n          result = response.json()\n          return result[\"choices\"][0][\"message\"][\"content\"]\n\n\n.. edb:split-section::\n\n  Note that this cloud LLM API (and many others) requires a secret key to be\n  set as an environment variable. A common way to manage those is to use the\n  ``python-dotenv`` library in combinations with a ``.env`` file. Feel free to\n  browse `the readme\n  <https://github.com/theskumar/python-dotenv?tab=readme-ov-file#getting-started>`_,\n  to learn more. Create a file called ``.env`` in the root directory and put\n  your api key in there:\n\n  .. code-block:: .env\n      :caption: .env\n\n      OPENAI_API_KEY=\"sk-...\"\n\n\n.. edb:split-section::\n\n  Don't forget to add the new dependency to the environment:\n\n  .. code-block:: bash\n\n      uv add python-dotenv\n\n\n.. edb:split-section::\n\n  And now we can integrate this LLM-related code with the rest of the app. First,\n  let's set up a function that prepares LLM inputs:\n\n\n  .. code-block:: python\n      :caption: app/main.py\n\n      async def generate_answer(\n          query: str,\n          web_sources: list[WebSource],\n      ) -> SearchResult:\n          system_prompt = (\n              \"You are a helpful assistant that answers user's questions\"\n              + \" by finding relevant information in Hacker News threads.\"\n              + \" When answering the question, describe conversations that people have around the subject,\"\n              + \" provided to you as a context, or say i don't know if they are completely irrelevant.\"\n          )\n\n          prompt = f\"User search query: {query}\\n\\nWeb search results:\\n\"\n\n          for i, source in enumerate(web_sources):\n              prompt += f\"Result {i} (URL: {source.url}):\\n\"\n              prompt += f\"{source.text}\\n\\n\"\n\n          messages = [{\"role\": \"user\", \"content\": prompt}]\n\n          llm_response = get_llm_completion(\n              system_prompt=system_prompt,\n              messages=messages,\n          )\n\n          search_result = SearchResult(\n              response=llm_response,\n              sources=web_sources,\n          )\n\n          return search_result\n\n\n.. edb:split-section::\n\n  Then we can plug that function into the ``/search`` endpoint:\n\n  .. code-block:: python-diff\n      :caption: app/main.py\n\n        @app.post(\"/search\")\n        async def search(search_terms: SearchTerms) -> SearchResult:\n            web_sources = await search_web(search_terms.query)\n      +     search_result = await generate_answer(search_terms.query, web_sources)\n      +     return search_result\n      -     return SearchResult(\n      -         response=search_terms.query, sources=web_sources\n      -     )\n\n\n.. edb:split-section::\n\n  And now we can test the result as usual.\n\n  .. code-block:: bash\n\n      $ curl -X 'POST' \\\n          'http://127.0.0.1:8000/search' \\\n          -H 'accept: application/json' \\\n          -H 'Content-Type: application/json' \\\n          -d '{ \"query\": \"gel\" }'\n\n\n5. Use Gel to implement chat history\n====================================\n\nSo far we've built an application that can take in a query, fetch some Hacker\nNews threads for it, sift through them using an LLM, and generate a nice\nsummary.\n\nHowever, right now it's hardly user-friendly since you have to speak in\nkeywords and basically start over every time you want to refine the query. To\nenable a more organic multi-turn interaction, we need to add chat history and\ninfer the query from the context of the entire conversation.\n\nNow's a good time to introduce Gel.\n\n.. edb:split-section::\n\n  In case you need installation instructions, take a look at the :ref:`Quickstart\n  <ref_quickstart>`. Once Gel CLI is present in your system, initialize the\n  project like this:\n\n  .. code-block:: bash\n\n      $ gel project init --non-interactive\n\n\nThis command is going to put some project scaffolding inside our app, spin up a\nlocal instace of Gel, and then link the two together. From now on, all\nGel-related things that happen inside our project directory are going to be\nautomatically run on the correct database instance, no need to worry about\nconnection incantations.\n\n\nDefining the schema\n-------------------\n\nThe database :ref:`schema <ref_datamodel_index>` in Gel is defined\ndeclaratively. The :gelcmd:`project init` command has created a file called\n:dotgel:`dbschema/default`, which we're going to use to define our types.\n\n.. edb:split-section::\n\n  We obviously want to keep track of the messages, so we need to represent\n  those in the schema. By convention established in the LLM space, each message\n  is going to have a role in addition to the message content itself. We can\n  also get Gel to automatically keep track of message's creation time by adding\n  a property callled ``timestamp`` and setting its :ref:`default value\n  <ref_datamodel_props>` to the output of the :ref:`datetime_current()\n  <ref_std_datetime>` function. Finally, LLM messages in our search bot have\n  source URLs associated with them. Let's keep track of those too, by adding a\n  :ref:`multi-property <ref_datamodel_props>`.\n\n  .. code-block:: sdl\n      :caption: dbschema/default.esdl\n\n      type Message {\n          role: str;\n          body: str;\n          timestamp: datetime {\n              default := datetime_current();\n          }\n          multi sources: str;\n      }\n\n\n.. edb:split-section::\n\n  Messages are grouped together into a chat, so let's add that entity to our\n  schema too.\n\n  .. code-block:: sdl\n      :caption: dbschema/default.esdl\n\n      type Chat {\n          multi messages: Message;\n      }\n\n\n.. edb:split-section::\n\n  And chats all belong to a certain user, making up their chat history. One other\n  thing we'd like to keep track of about our users is their username, and it would\n  make sense for us to make sure that it's unique by using an ``excusive``\n  :ref:`constraint <ref_datamodel_constraints>`.\n\n  .. code-block:: sdl\n      :caption: dbschema/default.esdl\n\n      type User {\n          name: str {\n              constraint exclusive;\n          }\n          multi chats: Chat;\n      }\n\n\n.. edb:split-section::\n\n  We're going to keep our schema super simple. One cool thing about Gel is that\n  it will enable us to easily implement advanced features such as authentication\n  or AI down the road, but we're gonna come back to that later.\n\n  For now, this is the entire schema we came up with:\n\n  .. code-block:: sdl\n      :caption: dbschema/default.esdl\n\n      module default {\n          type Message {\n              role: str;\n              body: str;\n              timestamp: datetime {\n                  default := datetime_current();\n              }\n              multi sources: str;\n          }\n\n          type Chat {\n              multi messages: Message;\n          }\n\n          type User {\n              name: str {\n                  constraint exclusive;\n              }\n              multi chats: Chat;\n          }\n      }\n\n\n.. edb:split-section::\n\n  Let's use the :gelcmd:`migration create` CLI command, followed by :gelcmd:`migrate` in\n  order to migrate to our new schema and proceed to writing some queries.\n\n  .. code-block:: bash\n\n      $ gel migration create\n      $ gel migrate\n\n\n.. edb:split-section::\n\n  Now that our schema is applied, let's quickly populate the database with some\n  fake data in order to be able to test the queries. We're going to explore\n  writing queries in a bit, but for now you can just run the following command in\n  the shell:\n\n  .. code-block:: bash\n      :class: collapsible\n\n      $ mkdir app/sample_data && cat << 'EOF' > app/sample_data/inserts.edgeql\n      # Create users first\n      insert User {\n          name := 'alice',\n      };\n      insert User {\n          name := 'bob',\n      };\n      # Insert chat histories for Alice\n      update User\n      filter .name = 'alice'\n      set {\n          chats := {\n              (insert Chat {\n                  messages := {\n                      (insert Message {\n                          role := 'user',\n                          body := 'What are the main differences between GPT-3 and GPT-4?',\n                          timestamp := <datetime>'2024-01-07T10:00:00Z',\n                          sources := {'arxiv:2303.08774', 'openai.com/research/gpt-4'}\n                      }),\n                      (insert Message {\n                          role := 'assistant',\n                          body := 'The key differences include improved reasoning capabilities, better context understanding, and enhanced safety features...',\n                          timestamp := <datetime>'2024-01-07T10:00:05Z',\n                          sources := {'openai.com/blog/gpt-4-details', 'arxiv:2303.08774'}\n                      })\n                  }\n              }),\n              (insert Chat {\n                  messages := {\n                      (insert Message {\n                          role := 'user',\n                          body := 'Can you explain what policy gradient methods are in RL?',\n                          timestamp := <datetime>'2024-01-08T14:30:00Z',\n                          sources := {'Sutton-Barto-RL-Book-Ch13', 'arxiv:1904.12901'}\n                      }),\n                      (insert Message {\n                          role := 'assistant',\n                          body := 'Policy gradient methods are a class of reinforcement learning algorithms that directly optimize the policy...',\n                          timestamp := <datetime>'2024-01-08T14:30:10Z',\n                          sources := {'Sutton-Barto-RL-Book-Ch13', 'spinning-up.openai.com'}\n                      })\n                  }\n              })\n          }\n      };\n      # Insert chat histories for Bob\n      update User\n      filter .name = 'bob'\n      set {\n          chats := {\n              (insert Chat {\n                  messages := {\n                      (insert Message {\n                          role := 'user',\n                          body := 'What are the pros and cons of different sharding strategies?',\n                          timestamp := <datetime>'2024-01-05T16:15:00Z',\n                          sources := {'martin-kleppmann-ddia-ch6', 'aws.amazon.com/sharding-patterns'}\n                      }),\n                      (insert Message {\n                          role := 'assistant',\n                          body := 'The main sharding strategies include range-based, hash-based, and directory-based sharding...',\n                          timestamp := <datetime>'2024-01-05T16:15:08Z',\n                          sources := {'martin-kleppmann-ddia-ch6', 'mongodb.com/docs/sharding'}\n                      }),\n                      (insert Message {\n                          role := 'user',\n                          body := 'Could you elaborate on hash-based sharding?',\n                          timestamp := <datetime>'2024-01-05T16:16:00Z',\n                          sources := {'mongodb.com/docs/sharding'}\n                      })\n                  }\n              })\n          }\n      };\n      EOF\n\n\n.. edb:split-section::\n\n  This created the ``app/sample_data/inserts.edgeql`` file, which we can now execute\n  using the CLI like this:\n\n  .. code-block:: bash\n\n      $ gel query -f app/sample_data/inserts.edgeql\n\n      {\"id\": \"862de904-de39-11ef-9713-4fab09220c4a\"}\n      {\"id\": \"862e400c-de39-11ef-9713-2f81f2b67013\"}\n      {\"id\": \"862de904-de39-11ef-9713-4fab09220c4a\"}\n      {\"id\": \"862e400c-de39-11ef-9713-2f81f2b67013\"}\n\n\n.. edb:split-section::\n\n  The :gelcmd:`query` command is one of many ways we can execute a query in Gel. Now\n  that we've done it, there's stuff in the database.\n\n  Let's verify it by running:\n\n  .. code-block:: bash\n\n      $ gel query \"select User { name };\"\n\n      {\"name\": \"alice\"}\n      {\"name\": \"bob\"}\n\n\nWriting queries\n---------------\n\nWith schema in place, it's time to focus on getting the data in and out of the\ndatabase.\n\nIn this tutorial we're going to write queries using :ref:`EdgeQL\n<ref_intro_edgeql>` and then use :ref:`codegen <gel-python-codegen>` to\ngenerate typesafe function that we can plug directly into out Python code. If\nyou are completely unfamiliar with EdgeQL, now is a good time to check out the\nbasics before proceeding.\n\n\n.. edb:split-section::\n\n  Let's move on. First, we'll create a directory inside ``app`` called\n  ``queries``. This is where we're going to put all of the EdgeQL-related stuff.\n\n  We're going to start by writing a query that fetches all of the users. In\n  ``queries`` create a file named ``get_users.edgeql`` and put the following query\n  in there:\n\n  .. code-block:: edgeql\n      :caption: app/queries/get_users.edgeql\n\n      select User { name };\n\n\n.. edb:split-section::\n\n  Now run the code generator from the shell:\n\n  .. code-block:: bash\n\n      $ gel-py\n\n\n.. edb:split-section::\n\n  It's going to automatically locate the ``.edgeql`` file and generate types for\n  it. We can inspect generated code in ``app.queries/get_users_async_edgeql.py``.\n  Once that is done, let's use those types to create the endpoint in ``main.py``:\n\n  .. code-block:: python\n      :caption: app/main.py\n\n      from edgedb import create_async_client\n      from .queries.get_users_async_edgeql import get_users as get_users_query, GetUsersResult\n\n\n      gel_client = create_async_client()\n\n      @app.get(\"/users\")\n      async def get_users() -> list[GetUsersResult]:\n          return await get_users_query(gel_client)\n\n\n.. edb:split-section::\n\n  Let's verify it that works as expected:\n\n  .. code-block:: bash\n\n      $ curl -X 'GET' \\\n      'http://127.0.0.1:8000/users' \\\n      -H 'accept: application/json'\n\n      [\n        {\n          \"id\": \"862de904-de39-11ef-9713-4fab09220c4a\",\n          \"name\": \"alice\"\n        },\n        {\n          \"id\": \"862e400c-de39-11ef-9713-2f81f2b67013\",\n          \"name\": \"bob\"\n        }\n      ]\n\n\n.. edb:split-section::\n\n  While we're at it, let's also implement the option to fetch a user by their\n  username. In order to do that, we need to write a new query in a separate file\n  ``app/queries/get_user_by_name.edgeql``:\n\n  .. code-block:: edgeql\n      :caption: app/queries/get_user_by_name.edgeql\n\n      select User { name }\n      filter .name = <str>$name;\n\n\n.. edb:split-section::\n\n  After that, we will run the code generator again by calling ``gel-py``. In the\n  app, we are going to reuse the same endpoint that fetches the list of all users.\n  From now on, if the user calls it without any arguments (e.g.\n  ``http://127.0.0.1/users``), they are going to receive the list of all users,\n  same as before. But if they pass a username as a query argument like this:\n  ``http://127.0.0.1/users?username=bob``, the system will attempt to fetch a user\n  named ``bob``.\n\n  In order to achieve this, we're going to need to add a ``Query``-type argument\n  to our endpoint function. You can learn more about how to configure this type of\n  arguments in `FastAPI's docs\n  <https://fastapi.tiangolo.com/tutorial/query-params/>`_. It's default value is\n  going to be ``None``, which will enable us to implement our conditional logic:\n\n  .. code-block:: python\n      :caption: app/main.py\n\n      from fastapi import Query, HTTPException\n      from http import HTTPStatus\n      from .queries.get_user_by_name_async_edgeql import (\n          get_user_by_name as get_user_by_name_query,\n          GetUserByNameResult,\n      )\n\n\n      @app.get(\"/users\")\n      async def get_users(\n          username: str = Query(None),\n      ) -> list[GetUsersResult] | GetUserByNameResult:\n          \"\"\"List all users or get a user by their username\"\"\"\n          if username:\n              user = await get_user_by_name_query(gel_client, name=username)\n              if not user:\n                  raise HTTPException(\n                      HTTPStatus.NOT_FOUND,\n                      detail={\"error\": f\"Error: user {username} does not exist.\"},\n                  )\n              return user\n          else:\n              return await get_users_query(gel_client)\n\n\n.. edb:split-section::\n\n  And once again, let's verify that everything works:\n\n  .. code-block:: bash\n\n      $ curl -X 'GET' \\\n        'http://127.0.0.1:8000/users?username=alice' \\\n        -H 'accept: application/json'\n\n      {\n        \"id\": \"862de904-de39-11ef-9713-4fab09220c4a\",\n        \"name\": \"alice\"\n      }\n\n\n.. edb:split-section::\n\n  Finally, let's also implement the option to add a new user. For this, just as\n  before, we'll create a new file ``app/queries/create_user.edgeql``, add a query\n  to it and run code generation.\n\n  Note that in this query we've wrapped the ``insert`` in a ``select`` statement.\n  This is a common pattern in EdgeQL, that can be used whenever you would like to\n  get something other than object ID when you just inserted it.\n\n  .. code-block:: edgeql\n      :caption: app/queries/create_user.edgeql\n\n      select(\n          insert User {\n              name := <str>$username\n          }\n      ) {\n          name\n      }\n\n\n\n.. edb:split-section::\n\n  In order to integrate this query into our app, we're going to add a new\n  endpoint. Note that this one has the same name ``/users``, but is for the POST\n  HTTP method.\n\n  .. code-block:: python\n      :caption: app/main.py\n\n      from gel import ConstraintViolationError\n      from .queries.create_user_async_edgeql import (\n          create_user as create_user_query,\n          CreateUserResult,\n      )\n\n      @app.post(\"/users\", status_code=HTTPStatus.CREATED)\n      async def post_user(username: str = Query()) -> CreateUserResult:\n          try:\n              return await create_user_query(gel_client, username=username)\n          except ConstraintViolationError:\n              raise HTTPException(\n                  status_code=HTTPStatus.BAD_REQUEST,\n                  detail={\"error\": f\"Username '{username}' already exists.\"},\n              )\n\n\n.. edb:split-section::\n\n  Once more, let's verify that the new endpoint works as expected:\n\n  .. code-block:: bash\n\n      $ curl -X 'POST' \\\n        'http://127.0.0.1:8000/users?username=charlie' \\\n        -H 'accept: application/json' \\\n        -d ''\n\n      {\n        \"id\": \"20372a1a-ded5-11ef-9a08-b329b578c45c\",\n        \"name\": \"charlie\"\n      }\n\n\n.. edb:split-section::\n\n  This wraps things up for our user-related functionality. Of course, we now need\n  to deal with Chats and Messages, too. We're not going to go in depth for those,\n  since the process would be quite similar to what we've just done. Instead, feel\n  free to implement those endpoints yourself as an exercise, or copy the code\n  below if you are in rush.\n\n  .. code-block:: bash\n      :class: collapsible\n\n      $ echo 'select Chat {\n          messages: { role, body, sources },\n          user := .<chats[is User],\n      } filter .user.name = <str>$username;' > app/queries/get_chats.edgeql && echo 'select Chat {\n          messages: { role, body, sources },\n          user := .<chats[is User],\n      } filter .user.name = <str>$username and .id = <uuid>$chat_id;' > app/queries/get_chat_by_id.edgeql && echo 'with new_chat := (insert Chat)\n      select (\n          update User filter .name = <str>$username\n          set {\n              chats := assert_distinct(.chats union new_chat)\n          }\n      ) {\n          new_chat_id := new_chat.id\n      }' > app/queries/create_chat.edgeql && echo 'with\n          user := (select User filter .name = <str>$username),\n          chat := (\n              select Chat filter .<chats[is User] = user and .id = <uuid>$chat_id\n          )\n      select Message {\n          role,\n          body,\n          sources,\n          chat := .<messages[is Chat]\n      } filter .chat = chat;' > app/queries/get_messages.edgeql && echo 'with\n          user := (select User filter .name = <str>$username),\n      update Chat\n      filter .id = <uuid>$chat_id and .<chats[is User] = user\n      set {\n          messages := assert_distinct(.messages union (\n              insert Message {\n                  role := <str>$message_role,\n                  body := <str>$message_body,\n                  sources := array_unpack(<array<str>>$sources)\n              }\n          ))\n      }' > app/queries/add_message.edgeql\n\n\n.. edb:split-section::\n\n  And these are the endpoint definitions, provided in bulk.\n\n  .. code-block:: python\n      :caption: app/main.py\n      :class: collapsible\n\n      from .queries.get_chats_async_edgeql import get_chats as get_chats_query, GetChatsResult\n      from .queries.get_chat_by_id_async_edgeql import (\n          get_chat_by_id as get_chat_by_id_query,\n          GetChatByIdResult,\n      )\n      from .queries.get_messages_async_edgeql import (\n          get_messages as get_messages_query,\n          GetMessagesResult,\n      )\n      from .queries.create_chat_async_edgeql import (\n          create_chat as create_chat_query,\n          CreateChatResult,\n      )\n      from .queries.add_message_async_edgeql import (\n          add_message as add_message_query,\n      )\n\n\n      @app.get(\"/chats\")\n      async def get_chats(\n          username: str = Query(), chat_id: str = Query(None)\n      ) -> list[GetChatsResult] | GetChatByIdResult:\n          \"\"\"List user's chats or get a chat by username and id\"\"\"\n          if chat_id:\n              chat = await get_chat_by_id_query(\n                  gel_client, username=username, chat_id=chat_id\n              )\n              if not chat:\n                  raise HTTPException(\n                      HTTPStatus.NOT_FOUND,\n                      detail={\"error\": f\"Chat {chat_id} for user {username} does not exist.\"},\n                  )\n              return chat\n          else:\n              return await get_chats_query(gel_client, username=username)\n\n\n      @app.post(\"/chats\", status_code=HTTPStatus.CREATED)\n      async def post_chat(username: str) -> CreateChatResult:\n          return await create_chat_query(gel_client, username=username)\n\n\n      @app.get(\"/messages\")\n      async def get_messages(\n          username: str = Query(), chat_id: str = Query()\n      ) -> list[GetMessagesResult]:\n          \"\"\"Fetch all messages from a chat\"\"\"\n          return await get_messages_query(gel_client, username=username, chat_id=chat_id)\n\n\n.. edb:split-section::\n\n  For the ``post_messages`` function we're going to do something a little bit\n  different though. Since this is now the primary way for the user to add their\n  queries to the system, it functionally superceeds the ``/search`` endpoint we\n  made before. To this end, this function is where we're going to handle saving\n  messages, retrieving chat history, invoking web search and generating the\n  answer.\n\n  .. code-block:: python-diff\n      :caption: app/main.py\n\n      - @app.post(\"/search\")\n      - async def search(search_terms: SearchTerms) -> SearchResult:\n      -     web_sources = await search_web(search_terms.query)\n      -     search_result = await generate_answer(search_terms.query, web_sources)\n      -     return search_result\n\n      + @app.post(\"/messages\", status_code=HTTPStatus.CREATED)\n      + async def post_messages(\n      +     search_terms: SearchTerms,\n      +     username: str = Query(),\n      +     chat_id: str = Query(),\n      + ) -> SearchResult:\n      +     chat_history = await get_messages_query(\n      +         gel_client, username=username, chat_id=chat_id\n      +     )\n\n      +     _ = await add_message_query(\n      +         gel_client,\n      +         username=username,\n      +         message_role=\"user\",\n      +         message_body=search_terms.query,\n      +         sources=[],\n      +         chat_id=chat_id,\n      +     )\n\n      +     search_query = search_terms.query\n      +     web_sources = await search_web(search_query)\n\n      +     search_result = await generate_answer(\n      +         search_terms.query, chat_history, web_sources\n      +     )\n\n      +     _ = await add_message_query(\n      +         gel_client,\n      +         username=username,\n      +         message_role=\"assistant\",\n      +         message_body=search_result.response,\n      +         sources=search_result.sources,\n      +         chat_id=chat_id,\n      +     )\n\n      +     return search_result\n\n\n.. edb:split-section::\n\n  Let's not forget to modify the ``generate_answer`` function, so it can also be\n  history-aware.\n\n  .. code-block:: python-diff\n      :caption: app/main.py\n\n        async def generate_answer(\n            query: str,\n      +     chat_history: list[GetMessagesResult],\n            web_sources: list[WebSource],\n        ) -> SearchResult:\n            system_prompt = (\n                \"You are a helpful assistant that answers user's questions\"\n                + \" by finding relevant information in HackerNews threads.\"\n                + \" When answering the question, describe conversations that people have around the subject,\"\n                + \" provided to you as a context, or say i don't know if they are completely irrelevant.\"\n            )\n\n            prompt = f\"User search query: {query}\\n\\nWeb search results:\\n\"\n\n            for i, source in enumerate(web_sources):\n                prompt += f\"Result {i} (URL: {source.url}):\\n\"\n                prompt += f\"{source.text}\\n\\n\"\n\n      -     messages = [{\"role\": \"user\", \"content\": prompt}]\n      +     messages = [\n      +         {\"role\": message.role, \"content\": message.body} for message in chat_history\n      +     ]\n      +     messages.append({\"role\": \"user\", \"content\": prompt})\n\n            llm_response = get_llm_completion(\n                system_prompt=system_prompt,\n                messages=messages,\n            )\n\n            search_result = SearchResult(\n                response=llm_response,\n                sources=web_sources,\n            )\n\n            return search_result\n\n\n.. edb:split-section::\n\n  Ok, this should be it for setting up the chat history. Let's test it. First, we\n  are going to start a new chat for our user:\n\n  .. code-block:: bash\n\n      $ curl -X 'POST' \\\n        'http://127.0.0.1:8000/chats?username=charlie' \\\n        -H 'accept: application/json' \\\n        -d ''\n\n      {\n        \"id\": \"20372a1a-ded5-11ef-9a08-b329b578c45c\",\n        \"new_chat_id\": \"544ef3f2-ded8-11ef-ba16-f7f254b95e36\"\n      }\n\n\n.. edb:split-section::\n\n  Next, let's add a couple messages and wait for the bot to respond:\n\n  .. code-block:: bash\n\n      $ curl -X 'POST' \\\n        'http://127.0.0.1:8000/messages?username=charlie&chat_id=544ef3f2-ded8-11ef-ba16-f7f254b95e36' \\\n        -H 'accept: application/json' \\\n        -H 'Content-Type: application/json' \\\n        -d '{\n        \"query\": \"best database in existence\"\n      }'\n\n      $ curl -X 'POST' \\\n        'http://127.0.0.1:8000/messages?username=charlie&chat_id=544ef3f2-ded8-11ef-ba16-f7f254b95e36' \\\n        -H 'accept: application/json' \\\n        -H 'Content-Type: application/json' \\\n        -d '{\n        \"query\": \"gel\"\n      }'\n\n\n.. edb:split-section::\n\n  Finally, let's check that the messages we saw are in fact stored in the chat\n  history:\n\n  .. code-block:: bash\n\n      $ curl -X 'GET' \\\n        'http://127.0.0.1:8000/messages?username=charlie&chat_id=544ef3f2-ded8-11ef-ba16-f7f254b95e36' \\\n        -H 'accept: application/json'\n\n\nIn reality this workflow would've been handled by the frontend, providing the\nuser with a nice inteface to interact with. But even without one our chatbot is\nalmost functional by now.\n\nGenerating a Google search query\n--------------------------------\n\nCongratulations! We just got done implementing multi-turn conversations for our\nsearch bot.\n\nHowever, there's still one crucial piece missing. Right now we're simply\nforwarding the users message straight to the full-text search. But what happens\nif their message is a followup that cannot be used as a standalone search\nquery?\n\nIdeally what we should do is we should infer the search query from the entire\nconversation, and use that to perform the search.\n\nLet's implement an extra step in which the LLM is going to produce a query for\nus based on the entire chat history. That way we can be sure we're progressively\nworking on our query rather than rewriting it from scratch every time.\n\n\n.. edb:split-section::\n\n  This is what we need to do: every time the user submits a message, we need to\n  fetch the chat history, extract a search query from it using the LLM, and the\n  other steps are going to the the same as before. Let's make the follwing\n  modifications to the ``main.py``: first we need to create a function that\n  prepares LLM inputs for the search query inference.\n\n\n  .. code-block:: python\n      :caption: app/main.py\n\n      async def generate_search_query(\n          query: str, message_history: list[GetMessagesResult]\n      ) -> str:\n          system_prompt = (\n              \"You are a helpful assistant.\"\n              + \" Your job is to extract a keyword search query\"\n              + \" from a chat between an AI and a human.\"\n              + \" Make sure it's a single most relevant keyword to maximize matching.\"\n              + \" Only provide the query itself as your response.\"\n          )\n\n          formatted_history = \"\\n---\\n\".join(\n              [\n                  f\"{message.role}: {message.body} (sources: {message.sources})\"\n                  for message in message_history\n              ]\n          )\n          prompt = f\"Chat history: {formatted_history}\\n\\nUser message: {query} \\n\\n\"\n\n          llm_response = get_llm_completion(\n              system_prompt=system_prompt, messages=[{\"role\": \"user\", \"content\": prompt}]\n          )\n\n          return llm_response\n\n\n.. edb:split-section::\n\n  And now we can use this function in ``post_messages`` in order to get our\n  search query:\n\n\n  .. code-block:: python-diff\n      :caption: app/main.py\n\n        class SearchResult(BaseModel):\n            response: str | None = None\n      +     search_query: str | None = None\n            sources: list[WebSource] | None = None\n\n\n        @app.post(\"/messages\", status_code=HTTPStatus.CREATED)\n        async def post_messages(\n            search_terms: SearchTerms,\n            username: str = Query(),\n            chat_id: str = Query(),\n        ) -> SearchResult:\n            # 1. Fetch chat history\n            chat_history = await get_messages_query(\n                gel_client, username=username, chat_id=chat_id\n            )\n\n            # 2. Add incoming message to Gel\n            _ = await add_message_query(\n                gel_client,\n                username=username,\n                message_role=\"user\",\n                message_body=search_terms.query,\n                sources=[],\n                chat_id=chat_id,\n            )\n\n            # 3. Generate a query and perform googling\n      -     search_query = search_terms.query\n      +     search_query = await generate_search_query(search_terms.query, chat_history)\n      +     web_sources = await search_web(search_query)\n\n\n            # 5. Generate answer\n            search_result = await generate_answer(\n                search_terms.query,\n                chat_history,\n                web_sources,\n            )\n      +     search_result.search_query = search_query  # add search query to the output\n      +                                                # to see what the bot is searching for\n            # 6. Add LLM response to Gel\n            _ = await add_message_query(\n                gel_client,\n                username=username,\n                message_role=\"assistant\",\n                message_body=search_result.response,\n                sources=[s.url for s in search_result.sources],\n                chat_id=chat_id,\n            )\n\n            # 7. Send result back to the client\n            return search_result\n\n\n.. edb:split-section::\n\n  Done! We've now fully integrated the chat history into out app and enabled\n  natural language conversations. As before, let's quickly test out the\n  improvements before moving on:\n\n\n  .. code-block:: bash\n\n      $ curl -X 'POST' \\\n          'http://localhost:8000/messages?username=alice&chat_id=d4eed420-e903-11ef-b8a7-8718abdafbe1' \\\n          -H 'accept: application/json' \\\n          -H 'Content-Type: application/json' \\\n          -d '{\n          \"query\": \"what are people saying about gel\"\n        }'\n\n      $ curl -X 'POST' \\\n          'http://localhost:8000/messages?username=alice&chat_id=d4eed420-e903-11ef-b8a7-8718abdafbe1' \\\n          -H 'accept: application/json' \\\n          -H 'Content-Type: application/json' \\\n          -d '{\n          \"query\": \"do they like it or not\"\n        }'\n\n\n6. Use Gel's advanced features to create a RAG\n==============================================\n\nAt this point we have a decent search bot that can refine a search query over\nmultiple turns of a conversation.\n\nIt's time to add the final touch: we can make the bot remember previous similar\ninteractions with the user using retrieval-augmented generation (RAG).\n\nTo achieve this we need to implement similarity search across message history:\nwe're going to create a vector embedding for every message in the database using\na neural network. Every time we generate a Google search query, we're also going\nto use it to search for similar messages in user's message history, and inject\nthe corresponding chat into the prompt. That way the search bot will be able to\nquickly \"remember\" similar interactions with the user and use them to understand\nwhat they are looking for.\n\nGel enables us to implement such a system with only minor modifications to the\nschema.\n\n\n.. edb:split-section::\n\n  We begin by enabling the ``ai`` extension by adding the following like on top of\n  the :dotgel:`dbschema/default`:\n\n  .. code-block:: sdl-diff\n      :caption: dbschema/default.esdl\n\n      + using extension ai;\n\n\n.. edb:split-section::\n\n  ... and do the migration:\n\n\n  .. code-block:: bash\n\n      $ gel migration create\n      $ gel migrate\n\n\n.. edb:split-section::\n\n  Next, we need to configure the API key in Gel for whatever embedding provider\n  we're going to be using. As per documentation, let's open up the CLI by typing\n  ``gel`` and run the following command (assuming we're using OpenAI):\n\n  .. code-block:: edgeql-repl\n\n      searchbot:main> configure current database\n      insert ext::ai::OpenAIProviderConfig {\n        secret := 'sk-....',\n      };\n\n      OK: CONFIGURE DATABASE\n\n\n.. edb:split-section::\n\n  In order to get Gel to automatically keep track of creating and updating\n  message embeddings, all we need to do is create a deferred index like this.\n  Don't forget to run a migration one more time!\n\n  .. code-block:: sdl-diff\n\n        type Message {\n            role: str;\n            body: str;\n            timestamp: datetime {\n                default := datetime_current();\n            }\n            multi sources: str;\n\n      +     deferred index ext::ai::index(embedding_model := 'text-embedding-3-small')\n      +         on (.body);\n        }\n\n\n.. edb:split-section::\n\n  And we're done! Gel is going to cook in the background for a while and generate\n  embedding vectors for our queries. To make sure nothing broke we can follow\n  Gel's AI documentation and take a look at instance logs:\n\n  .. code-block:: bash\n\n      $ gel instance logs -I searchbot | grep api.openai.com\n\n      INFO 50121 searchbot 2025-01-30T14:39:53.364 httpx: HTTP Request: POST https://api.openai.com/v1/embeddings \"HTTP/1.1 200 OK\"\n\n\n.. edb:split-section::\n\n  It's time to create the second half of the similarity search - the search query.\n  The query needs to fetch ``k`` chats in which there're messages that are most\n  similar to our current message. This can be a little difficult to visualize in\n  your head, so here's the query itself:\n\n  .. code-block:: edgeql\n      :caption: app/queries/search_chats.edgeql\n\n      with\n          user := (select User filter .name = <str>$username),\n              chats := (\n                  select Chat\n                  filter .<chats[is User] = user\n                         and .id != <uuid>$current_chat_id\n              )\n\n      select chats {\n          distance := min(\n              ext::ai::search(\n                  .messages,\n                  <array<float32>>$embedding,\n              ).distance,\n          ),\n          messages: {\n              role, body, sources\n          }\n      }\n\n      order by .distance\n      limit <int64>$limit;\n\n\n.. edb:split-section::\n\n  .. note::\n\n     Before we can integrate this query into our Python app, we also need to add a\n     new dependency for the Python binding: ``httpx-sse``. It's enables streaming\n     outputs, which we're not going to use right now, but we won't be able to\n     create the AI client without it.\n\n  Let's place in in ``app/queries/search_chats.edgeql``, run the codegen and modify\n  our ``post_messages`` endpoint to keep track of those similar chats.\n\n  .. code-block:: python-diff\n      :caption: app.main.py\n\n      + from edgedb.ai import create_async_ai, AsyncEdgeDBAI\n      + from .queries.search_chats_async_edgeql import (\n      +     search_chats as search_chats_query,\n      + )\n\n        class SearchResult(BaseModel):\n            response: str | None = None\n            search_query: str | None = None\n            sources: list[WebSource] | None = None\n      +     similar_chats: list[str] | None = None\n\n\n        @app.post(\"/messages\", status_code=HTTPStatus.CREATED)\n        async def post_messages(\n            search_terms: SearchTerms,\n            username: str = Query(),\n            chat_id: str = Query(),\n        ) -> SearchResult:\n            # 1. Fetch chat history\n            chat_history = await get_messages_query(\n                gel_client, username=username, chat_id=chat_id\n            )\n\n            # 2. Add incoming message to Gel\n            _ = await add_message_query(\n                gel_client,\n                username=username,\n                message_role=\"user\",\n                message_body=search_terms.query,\n                sources=[],\n                chat_id=chat_id,\n            )\n\n            # 3. Generate a query and perform googling\n            search_query = await generate_search_query(search_terms.query, chat_history)\n            web_sources = await search_web(search_query)\n\n      +     # 4. Fetch similar chats\n      +     db_ai: AsyncEdgeDBAI = await create_async_ai(gel_client, model=\"gpt-4o-mini\")\n      +     embedding = await db_ai.generate_embeddings(\n      +         search_query, model=\"text-embedding-3-small\"\n      +     )\n      +     similar_chats = await search_chats_query(\n      +         gel_client,\n      +         username=username,\n      +         current_chat_id=chat_id,\n      +         embedding=embedding,\n      +         limit=1,\n      +     )\n\n            # 5. Generate answer\n            search_result = await generate_answer(\n                search_terms.query,\n                chat_history,\n                web_sources,\n      +         similar_chats,\n            )\n            search_result.search_query = search_query  # add search query to the output\n                                                       # to see what the bot is searching for\n            # 6. Add LLM response to Gel\n            _ = await add_message_query(\n                gel_client,\n                username=username,\n                message_role=\"assistant\",\n                message_body=search_result.response,\n                sources=[s.url for s in search_result.sources],\n                chat_id=chat_id,\n            )\n\n            # 7. Send result back to the client\n            return search_result\n\n\n.. edb:split-section::\n\n  Finally, the answer generator needs to get updated one more time, since we need\n  to inject the additional messages into the prompt.\n\n  .. code-block:: python-diff\n      :caption: app/main.py\n\n        async def generate_answer(\n            query: str,\n            chat_history: list[GetMessagesResult],\n            web_sources: list[WebSource],\n      +     similar_chats: list[list[GetMessagesResult]],\n        ) -> SearchResult:\n            system_prompt = (\n                \"You are a helpful assistant that answers user's questions\"\n                + \" by finding relevant information in HackerNews threads.\"\n                + \" When answering the question, describe conversations that people have around the subject, provided to you as a context, or say i don't know if they are completely irrelevant.\"\n      +         + \" You can reference previous conversation with the user that\"\n      +         + \" are provided to you, if they are relevant, by explicitly referring\"\n      +         + \" to them by saying as we discussed in the past.\"\n            )\n\n            prompt = f\"User search query: {query}\\n\\nWeb search results:\\n\"\n\n            for i, source in enumerate(web_sources):\n                prompt += f\"Result {i} (URL: {source.url}):\\n\"\n                prompt += f\"{source.text}\\n\\n\"\n\n      +     prompt += \"Similar chats with the same user:\\n\"\n\n      +     formatted_chats = []\n      +     for i, chat in enumerate(similar_chats):\n      +         formatted_chat = f\"Chat {i}: \\n\"\n      +         for message in chat.messages:\n      +             formatted_chat += f\"{message.role}: {message.body}\\n\"\n      +         formatted_chats.append(formatted_chat)\n\n      +     prompt += \"\\n\".join(formatted_chats)\n\n            messages = [\n                {\"role\": message.role, \"content\": message.body} for message in chat_history\n            ]\n            messages.append({\"role\": \"user\", \"content\": prompt})\n\n            llm_response = get_llm_completion(\n                system_prompt=system_prompt,\n                messages=messages,\n            )\n\n            search_result = SearchResult(\n                response=llm_response,\n                sources=web_sources,\n      +         similar_chats=formatted_chats,\n            )\n\n            return search_result\n\n\n.. edb:split-section::\n\n  And one last time, let's check to make sure everything works:\n\n  .. code-block:: bash\n\n      $ curl -X 'POST' \\\n          'http://localhost:8000/messages?username=alice&chat_id=d4eed420-e903-11ef-b8a7-8718abdafbe1' \\\n          -H 'accept: application/json' \\\n          -H 'Content-Type: application/json' \\\n          -d '{\n                \"query\": \"remember that cool db i was talking to you about?\"\n              }'\n\n\nKeep going!\n===========\n\nThis tutorial is over, but this app surely could use way more features!\n\nBasic functionality like deleting messages, a user interface or real web\nsearch, sure. But also authentication or access policies -- Gel will let you\nset those up in minutes.\n\nThanks!\n\n\n\n\n\n\n\n"
  },
  {
    "path": "docs/intro/tutorials/gel_drizzle_booknotes.rst",
    "content": ".. _ref_guide_gel_drizzle_booknotes:\n\n====================================================\nBuild a Book Notes App with Drizzle\n====================================================\n\n:edb-alt-title: Building a book notes app using Gel, Drizzle ORM, and Next.js\n\nIn this tutorial we're going to walk you through building a Book Notes application\nthat lets you keep track of books you've read along with your personal notes.\nWe'll be using Gel as the database, Drizzle as the ORM layer, and Next.js as our\nfull-stack framework.\n\nGel is a data layer designed to supercharge PostgreSQL with a graph-like object model, access control, Auth, and many other features. It provides a unified schema and tooling experience across multiple languages, making it ideal for projects with diverse tech stacks. With Gel, you get access to EdgeQL, which eliminates n+1 query problems, supports automatic embeddings, and offers a seamless developer experience.\n\nDrizzle, on the other hand, is a TypeScript ORM that offers type safety and a great developer experience. By combining Gel with Drizzle, you can leverage Gel's powerful features while using Drizzle as a familiar ORM layer to interact with your database. This approach is perfect for developers who want to start learning Gel or prefer using Drizzle for their projects.\nexperience. Next.js is a React framework for building production-ready web applications with features like server components, built-in routing, and API routes. By the end of this tutorial, you will see how these technologies work together to create a modern, full-stack web application with a great developer experience.\n\n.. note::\n\n   The complete source code for this tutorial is available in our `Gel Examples repository\n   <https://github.com/geldata/gel-examples/tree/main/drizzle-book-notes-app>`_.\n\n\nWe will start by creating a Gel schema, setting up Drizzle, and then building\na Next.js application with API routes and a simple UI to manage your book collection and notes.\n\n\n1. Initialize the project\n=========================\n\n.. edb:split-section::\n\n  Let's start with setting up our project. We'll create a new Next.js application,\n  install the necessary dependencies, and initialize a Gel project.\n\n  Here's a summary of the setup steps we'll follow:\n\n  1. Create a Next.js application\n  2. Install Gel and related packages\n  3. Initialize a Gel project\n  4. Update schema and apply migrations\n  5. Install and set up Drizzle\n  6. Pull schema into Drizzle\n  7. Configure hooks in gel.toml\n\n.. edb:split-section::\n\n  First, let's create a new Next.js application. You can use the\n  ``create-next-app`` command to set up a new project. When prompted,\n  choose TypeScript, ESLint, Tailwind CSS, and the App Router. You can\n  skip the default import alias configuration.\n  This will create a new Next.js application with the necessary\n  configuration files and dependencies.\n\n  .. note::\n\n    Make sure you have Node.js and npm installed on your machine.\n\n  .. code-block:: bash\n\n      # Step 1: Create a Next.js application\n      $ npx create-next-app@latest book-notes-app\n\n      # When prompted, choose:\n      # ✔ Would you like to use TypeScript? Yes\n      # ✔ Would you like to use ESLint? Yes\n      # ✔ Would you like to use Tailwind CSS? Yes\n      # ✔ Would you like to use `src/` directory? Yes\n      # ✔ Would you like to use App Router? Yes\n      # ✔ Would you like to use Turbopack for `next dev`? No\n      # ✔ Would you like to customize the default import alias (@/*)? No\n\n      $ cd book-notes-app\n\n.. edb:split-section::\n\n  Next, let's install the Gel library. We'll need ``gel`` for database access.\n\n  .. code-block:: bash\n\n      $ npm i gel\n\n.. edb:split-section::\n\n  Now, we'll initialize a Gel project. This will create the necessary configuration\n  files and set up a local Gel instance.\n\n  .. code-block:: bash\n\n      $ npx gel project init\n\n2. Define the Gel schema\n========================\n\n\nNow that we have our project environment set up, let's define our database schema.\nFor our Book Notes app, we'll create two main types:\n\n1. ``Book`` - to store information about books\n2. ``Note`` - to store notes associated with each book\n\nLet's edit the :dotgel:`dbschema/default` file that was created during initialization.\n\n.. edb:split-section::\n\n  Our schema defines two types:\n\n  - ``Book`` with properties like title, author, publication year, genre, and read date.\n  - ``Note`` with text content and a timestamp, linked to a specific book.\n\n  The relationship is defined such that a book can have multiple notes, and\n  each note belongs to exactly one book. We're using a computed link ``notes`` to\n  allow easy access to a book's notes.\n\n  .. code-block:: sdl\n      :caption: :dotgel:`dbschema/default`\n\n      module default {\n        type Book {\n          required title: str;\n          author: str;\n          year: int16;\n          genre: str;\n          read_date: datetime;\n\n          # Relationship to notes\n          multi notes := .<book[is Note];\n        }\n\n        type Note {\n          required text: str;\n          created_at: datetime {\n            default := datetime_current();\n          }\n\n          # Link to the book\n          required book: Book;\n        }\n      }\n\n.. edb:split-section::\n\n  Now let's apply this schema to our database by creating and applying a migration:\n\n  .. code-block:: bash\n\n      $ gel migration create\n      $ gel migrate\n\n\n3. Install and set up Drizzle\n=============================\n\nNow that we have our Gel schema in place, we can integrate Drizzle ORM with our\nNext.js application. Drizzle will provide a type-safe way to interact with our\nGel database.\n\n.. edb:split-section::\n\n  First, let's install Drizzle and its dependencies. We'll need ``drizzle-orm`` and\n  ``drizzle-kit`` for this.\n  Drizzle ORM is the core library, while Drizzle Kit is a CLI tool that helps\n  with schema generation and migrations.\n\n  .. code-block:: bash\n\n      $ npm i drizzle-orm drizzle-kit\n\n.. edb:split-section::\n\n  Let's create a Drizzle configuration file in the root of our project. We'll set the ``dialect`` to ``gel``\n  to tell Drizzle that we're using Gel as our database.\n\n  .. code-block:: typescript\n      :caption: drizzle.config.ts\n\n      import { defineConfig } from 'drizzle-kit';\n\n      export default defineConfig({\n        dialect: 'gel',\n      });\n\n.. edb:split-section::\n\n  Now, let's pull the database schema into Drizzle. This step will introspect\n  our Gel database and generate TypeScript files that we can use with Drizzle.\n\n  .. code-block:: bash\n\n      $ npx drizzle-kit pull\n\n.. edb:split-section::\n\n  Drizzle Kit generated the schema based on the Gel schema we defined earlier. You can find this file in the ``drizzle`` directory along with the ``relations.ts`` file. The ``relations.ts`` file contains the relationships between the tables in our schema. The schema file that Drizzle generated will look like this:\n\n  .. note::\n\n    You should not modify the reflected schema directly. The correct flow is always: [change :dotgel:`dbschema/default` file] -> [run ``drizzle-kit pull``].\n\n\n  .. code-block:: typescript\n      :caption: drizzle/schema.ts\n      :class: collapsible\n\n      import { gelTable, uniqueIndex, uuid, text, timestamptz, smallint, foreignKey } from \"drizzle-orm/gel-core\"\n      import { sql } from \"drizzle-orm\"\n\n\n      export const book = gelTable(\"Book\", {\n      \tid: uuid().default(sql`uuid_generate_v4()`).primaryKey().notNull(),\n      \tauthor: text(),\n      \tgenre: text(),\n      \treadDate: timestamptz(\"read_date\"),\n      \ttitle: text().notNull(),\n      \tyear: smallint(),\n      }, (table) => [\n      \tuniqueIndex(\"5f1d3546-1943-11f0-be08-df1707d45eaa;schemaconstr\").using(\"btree\", table.id.asc().nullsLast().op(\"uuid_ops\")),\n      ]);\n\n      export const note = gelTable(\"Note\", {\n      \tid: uuid().default(sql`uuid_generate_v4()`).primaryKey().notNull(),\n      \tbookId: uuid(\"book_id\").notNull(),\n      \tcreatedAt: timestamptz(\"created_at\").default(sql`(clock_timestamp())`),\n      \ttext: text().notNull(),\n      }, (table) => [\n      \tuniqueIndex(\"5f1e4652-1943-11f0-a4a0-f1f912666606;schemaconstr\").using(\"btree\", table.id.asc().nullsLast().op(\"uuid_ops\")),\n      \tforeignKey({\n      \t\tcolumns: [table.bookId],\n      \t\tforeignColumns: [book.id],\n      \t\tname: \"Note_fk_book\"\n      \t}),\n      ]);\n\n\n.. edb:split-section::\n\n  Finally, we need to update the hooks in our ``gel.toml`` file to ensure that our\n  Drizzle schema stays in sync with our Gel schema. Every time we apply a migration,\n  we want to run the Drizzle pull command to update the TypeScript files.\n\n  .. code-block:: toml-diff\n      :caption: gel.toml\n\n      + [hooks]\n      + after_migration_apply = [\n      +   \"npx drizzle-kit pull\"\n      + ]\n\n\n4. Creating the database client\n================================\n\n.. edb:split-section::\n\n  Now that we have our schema set up, let's create a database client that we can use\n  throughout our application. This client will connect to our Gel database using\n  Drizzle.\n\n  .. code-block:: typescript\n      :caption: src/db/index.ts\n\n      import { drizzle } from 'drizzle-orm/gel';\n      import { createClient } from 'gel';\n\n      import * as schema from '../../drizzle/schema';\n      import * as relations from '../../drizzle/relations';\n\n      // Initialize Gel client\n      const gelClient = createClient();\n\n      // Create Drizzle instance\n      export const db = drizzle({ client: gelClient, schema: {\n        ...schema,\n        ...relations,\n      } });\n\n      // Helper types for use in our application\n      export type Book = typeof schema.book.$inferSelect;\n      export type NewBook = typeof schema.book.$inferInsert;\n      export interface BookWithNotes extends Book {\n        notes: Note[];\n      };\n\n      export type Note = typeof schema.note.$inferSelect;\n      export type NewNote = typeof schema.note.$inferInsert;\n\n5. Implementing API Routes\n===========================\n\nNext, let's implement the API routes for our book notes application. With Next.js, we can create API endpoints in the ``app/api`` directory to handle HTTP requests.\n\n.. edb:split-section::\n\n  We'll start by creating a route for managing all books. This will handle\n  fetching all books and adding new books. The ``GET`` method will return a list\n  of all books, while the ``POST`` method will allow us to add a new book.\n  We'll also include error handling for both methods. In both, we'll use\n  Drizzle ORM to interact with the database.\n\n  .. code-block:: typescript\n      :caption: app/api/books/route.ts\n\n      import { NextResponse } from 'next/server';\n      import { db } from '@/src/db';\n      import { books } from '@/drizzle/schema';\n\n      export async function GET() {\n        try {\n          const allBooks = await db.query.book.findMany({\n            with: {\n              notes: true,\n            },\n          });\n\n          return NextResponse.json(allBooks);\n        } catch (error) {\n          console.error('Error fetching books:', error);\n          return NextResponse.json(\n            { error: 'Failed to fetch books' },\n            { status: 500 }\n          );\n        }\n      }\n\n      export async function POST(request: Request) {\n        try {\n          const body = await request.json();\n\n          const result = await db.insert(book).values({\n            title: body.title,\n            author: body.author,\n            year: body.year,\n            genre: body.genre,\n            readDate: new Date(body.read_date),\n          }).returning();\n\n          return NextResponse.json(result[0], { status: 201 });\n        } catch (error) {\n          console.error('Error adding book:', error);\n          return NextResponse.json(\n            { error: 'Failed to add book' },\n            { status: 500 }\n          );\n        }\n      }\n\n.. edb:split-section::\n\n  Next, let's create a route for managing a specific book by its ID. This will handle\n  getting book details, updating books, and deleting books.\n\n  - ``GET`` method will fetch a specific book by its ID.\n  - ``PUT`` method will update the book details based on the request body.\n  - ``DELETE`` method will delete the book and all its associated notes.\n\n  We'll also include error handling for each method.\n\n  .. code-block:: typescript\n      :caption: src/app/api/books/[id]/route.ts\n\n      import { NextResponse } from 'next/server';\n      import { db } from '@/src/db';\n      import { book, note } from '@/drizzle/schema';\n      import { eq } from 'drizzle-orm';\n\n      export async function GET(\n        request: Request,\n        { params }: { params: Promise<{ id: string }> }\n      ) {\n        const { id } = await params;\n        try {\n          const requestedBook = await db.query.book.findFirst({\n            where: eq(books.id, id),\n            with: {\n              note: true,\n            },\n          });\n\n          if (!requestedBook) {\n            return NextResponse.json(\n              { error: 'Book not found' },\n              { status: 404 }\n            );\n          }\n\n          return NextResponse.json(requestedBook);\n        } catch (error) {\n          console.error('Error fetching book:', error);\n          return NextResponse.json(\n            { error: 'Failed to fetch book' },\n            { status: 500 }\n          );\n        }\n      }\n\n      export async function PUT(\n        request: Request,\n        { params }: { params: Promise<{ id: string }> }\n      ) {\n        const { id } = await params;\n        try {\n          const body = await request.json();\n\n          const result = await db.update(book)\n            .set({\n              title: body.title,\n              author: body.author,\n              year: body.year,\n              genre: body.genre,\n              readDate: new Date(body.read_date),\n            })\n            .where(eq(books.id, id))\n            .returning();\n\n          if (result.length === 0) {\n            return NextResponse.json(\n              { error: 'Book not found' },\n              { status: 404 }\n            );\n          }\n\n          return NextResponse.json(result[0]);\n        } catch (error) {\n          console.error('Error updating book:', error);\n          return NextResponse.json(\n            { error: 'Failed to update book' },\n            { status: 500 }\n          );\n        }\n      }\n\n      export async function DELETE(\n        request: Request,\n        { params }: { params: Promise<{ id: string }> }\n      ) {\n        const { id } = await params;\n        try {\n          // First delete associated notes\n          await db.delete(note).where(eq(note.bookId, id));\n\n          // Then delete the book\n          const result = await db.delete(book)\n            .where(eq(book.id, id))\n            .returning();\n\n          if (result.length === 0) {\n            return NextResponse.json(\n              { error: 'Book not found' },\n              { status: 404 }\n            );\n          }\n\n          return NextResponse.json({ success: true });\n        } catch (error) {\n          console.error('Error deleting book:', error);\n          return NextResponse.json(\n            { error: 'Failed to delete book' },\n            { status: 500 }\n          );\n        }\n      }\n\n.. edb:split-section::\n\n  Now, let's create a route for adding notes to a book. This endpoint will handle the\n  creation of new notes for a specific book. The ``POST`` method will accept\n  a request body with the note text and the book ID.\n\n  .. code-block:: typescript\n      :caption: src/app/api/books/[id]/notes/route.ts\n\n      import { NextResponse } from 'next/server';\n      import { db } from '@/src/db';\n      import { note } from '@/drizzle/schema';\n\n      export async function POST(\n        request: Request,\n        { params }: { params: Promise<{ id: string }> }\n      ) {\n        const { id } = await params;\n        try {\n          const body = await request.json();\n\n          const result = await db.insert(note).values({\n            text: body.text,\n            bookId: id,\n          }).returning();\n\n          return NextResponse.json(result[0], { status: 201 });\n        } catch (error) {\n          console.error('Error adding note:', error);\n          return NextResponse.json(\n            { error: 'Failed to add note' },\n            { status: 500 }\n          );\n        }\n      }\n\n.. edb:split-section::\n\n  Finally, let's create a route for updating and deleting individual notes.\n  This will handle the ``PUT`` and ``DELETE`` methods for a specific note.\n  The ``PUT`` method will update the note text, while the ``DELETE`` method\n  will delete the note.\n\n  .. code-block:: typescript\n      :caption: src/app/api/notes/[id]/route.ts\n\n      import { NextResponse } from 'next/server';\n      import { db } from '@/src/db';\n      import { note } from '@/drizzle/schema';\n      import { eq } from 'drizzle-orm';\n\n      export async function PUT(\n        request: Request,\n        { params }: { params: Promise<{ id: string }> }\n      ) {\n        const { id } = await params;\n        try {\n          const body = await request.json();\n\n          const result = await db.update(note)\n            .set({\n              text: body.text,\n            })\n            .where(eq(note.id, id))\n            .returning();\n\n          if (result.length === 0) {\n            return NextResponse.json(\n              { error: 'Note not found' },\n              { status: 404 }\n            );\n          }\n\n          return NextResponse.json(result[0]);\n        } catch (error) {\n          console.error('Error updating note:', error);\n          return NextResponse.json(\n            { error: 'Failed to update note' },\n            { status: 500 }\n          );\n        }\n      }\n\n      export async function DELETE(\n        request: Request,\n        { params }: { params: Promise<{ id: string }> }\n      ) {\n        const { id } = await params;\n        try {\n          const result = await db.delete(note)\n            .where(eq(notes.id, id))\n            .returning();\n\n          if (result.length === 0) {\n            return NextResponse.json(\n              { error: 'Note not found' },\n              { status: 404 }\n            );\n          }\n\n          return NextResponse.json({ success: true });\n        } catch (error) {\n          console.error('Error deleting note:', error);\n          return NextResponse.json(\n            { error: 'Failed to delete note' },\n            { status: 500 }\n          );\n        }\n      }\n\n.. edb:split-section::\n\n  We can test our API routes using a tool like Postman or cURL. Let's start the\n  development server and test the routes.\n\n  .. code-block:: bash\n\n      $ npm run dev\n\n.. edb:split-section::\n\n  You can now access the API routes at ``http://localhost:3000/api`` (or the port specified in your environment). For example, to access the books route, you can go to ``http://localhost:3000/api/books``. You can use Postman or cURL to test the endpoints. For example, to fetch all books, you can use the following cURL command:\n\n  .. code-block:: bash\n\n      $ curl -X GET http://localhost:3000/api/books\n\n.. edb:split-section::\n\n  To add a new book, you can use the following cURL command:\n\n  .. code-block:: bash\n\n      $ curl -X POST http://localhost:3000/api/books \\\n        -H \"Content-Type: application/json\" \\\n        -d '{\"title\": \"The Great Gatsby\", \"author\": \"F. Scott Fitzgerald\", \"year\": 1925, \"genre\": \"Fiction\", \"read_date\": \"2023-10-01\"}'\n\n.. edb:split-section::\n\n  Or to create a new note for a book, you can use the following cURL command\n  (replace ``<BOOK_ID>`` with the actual book ID):\n\n  .. code-block:: bash\n\n      $ curl -X POST http://localhost:3000/api/books/<BOOK_ID>/notes \\\n        -H \"Content-Type: application/json\" \\\n        -d '{\"text\": \"This is a great book!\"}'\n\n6. Building the UI\n==================\n\nNow that we have our API routes in place, we can build a user interface for our book\nnotes application. We'll use Tailwind CSS for styling, which was included when we\ncreated our Next.js application.\n\nWe won't go into extensive UI details, but here's a basic implementation for the\nhome page that lists all books.\n\n.. edb:split-section::\n\n  We'll start by creating a home page that fetches and displays all books from our API.\n  This page will also include a link to add a new book.\n  We'll use the ``useEffect`` hook to fetch the books when the component mounts.\n  We'll also handle loading states and error handling.\n  The home page will display a list of books with their titles, authors, publication years,\n  genres, and the number of notes associated with each book.\n\n  .. code-block:: typescript\n      :caption: app/page.tsx\n      :class: collapsible\n\n      'use client';\n\n      import { useState, useEffect } from 'react';\n      import Link from 'next/link';\n      import { BookWithNotes } from '@/db';\n\n      export default function Home() {\n        const [books, setBooks] = useState<Book[]>([]);\n        const [loading, setLoading] = useState(true);\n\n        useEffect(() => {\n          async function fetchBooks() {\n            try {\n              const response = await fetch('/api/books');\n              if (!response.ok) throw new Error('Failed to fetch books');\n              const data = await response.json();\n              setBooks(data);\n            } catch (error) {\n              console.error('Error:', error);\n            } finally {\n              setLoading(false);\n            }\n          }\n\n          fetchBooks();\n        }, []);\n\n        if (loading) {\n          return (\n            <div className=\"flex justify-center items-center min-h-screen\">\n              <p className=\"text-xl\">Loading...</p>\n            </div>\n          );\n        }\n\n        return (\n          <main className=\"container max-w-7xl mx-auto px-4 py-10\">\n            <h1 className=\"text-4xl md:text-5xl font-bold tracking-tight mb-8 text-center\">\n              My Book Notes\n            </h1>\n\n            <Link\n              href=\"/books/add\"\n              className=\"bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-400 text-white font-medium px-4 py-2 rounded transition duration-150 ease-in-out mb-8 inline-block\"\n            >\n              Add New Book\n            </Link>\n\n            <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-6\">\n              {books.length === 0 ? (\n                <p className=\"text-lg text-center\">No books found. Add your first book!</p>\n              ) : (\n                books.map((book) => (\n                  <Link\n                    key={book.id}\n                    href={`/books/${book.id}`}\n                    className=\"mt-3 inline-block font-medium hover:shadow-lg transform hover:scale-105 transition duration-200\"\n                  >\n                    <div\n                      className=\"bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg p-6 shadow-md \"\n                    >\n                      <h2 className=\"text-xl font-semibold mb-2\">{book.title}</h2>\n                      {book.author && (\n                        <p className=\"text-gray-600 dark:text-gray-400 mb-1\">\n                          by {book.author}\n                        </p>\n                      )}\n                      {book.year && (\n                        <p className=\"text-sm text-gray-500 dark:text-gray-400\">\n                          Published: {book.year}\n                        </p>\n                      )}\n                      {book.genre && (\n                        <p className=\"text-sm text-gray-500 dark:text-gray-400\">\n                          Genre: {book.genre}\n                        </p>\n                      )}\n                      <p className=\"mt-4 text-sm font-medium text-gray-700 dark:text-gray-300\">\n                        {book.notes?.length || 0} notes\n                      </p>\n\n                    </div>\n                  </Link>\n                ))\n              )}\n            </div>\n          </main>\n        );\n      }\n\n.. edb:split-section::\n\n  Next, let's create a form component for adding and editing books. This will be used in\n  both the \"Add Book\" page and the \"Edit Book\" page.\n\n  .. code-block:: typescript\n      :caption: src/components/BookForm.tsx\n      :class: collapsible\n\n      'use client';\n\n      import { useState, FormEvent } from 'react';\n      import { useRouter } from 'next/navigation';\n      import { Book } from '../db';\n\n      interface BookFormProps {\n        book?: Book;\n        isEditing?: boolean;\n      }\n\n      export default function BookForm({ book, isEditing = false }: BookFormProps) {\n        const router = useRouter();\n        const [title, setTitle] = useState(book?.title || '');\n        const [author, setAuthor] = useState(book?.author || '');\n        const [year, setYear] = useState(book?.year?.toString() || '');\n        const [genre, setGenre] = useState(book?.genre || '');\n        const [readDate, setReadDate] = useState(\n          book?.readDate\n            ? new Date(book.readDate).toISOString().split('T')[0]\n            : ''\n        );\n\n        const handleSubmit = async (e: FormEvent) => {\n          e.preventDefault();\n\n          const bookData = {\n            title,\n            author,\n            year: year ? parseInt(year) : undefined,\n            genre,\n            read_date: readDate || undefined,\n          };\n\n          try {\n            if (isEditing && book) {\n              // Update existing book\n              await fetch(`/api/books/${book.id}`, {\n                method: 'PUT',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify(bookData),\n              });\n            } else {\n              // Create new book\n              await fetch('/api/books', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application/json' },\n                body: JSON.stringify(bookData),\n              });\n            }\n\n            router.push('/');\n            router.refresh();\n          } catch (error) {\n            console.error('Error saving book:', error);\n          }\n        };\n\n        return (\n          <form onSubmit={handleSubmit} className=\"max-w-md mx-auto bg-gray-900 p-6 rounded-lg shadow\">\n            <div className=\"mb-4\">\n              <label className=\"block text-white font-semibold mb-1\" htmlFor=\"title\">\n                Title*\n              </label>\n              <input\n                id=\"title\"\n                type=\"text\"\n                value={title}\n                onChange={(e) => setTitle(e.target.value)}\n                required\n                className=\"w-full px-4 py-2 bg-gray-800 text-white border border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-150\"\n              />\n            </div>\n\n            <div className=\"mb-4\">\n              <label className=\"block text-white font-semibold mb-1\" htmlFor=\"author\">\n                Author\n              </label>\n              <input\n                id=\"author\"\n                type=\"text\"\n                value={author}\n                onChange={(e) => setAuthor(e.target.value)}\n                className=\"w-full px-4 py-2 bg-gray-800 text-white border border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-150\"\n              />\n            </div>\n\n            <div className=\"mb-4\">\n              <label className=\"block text-white font-semibold mb-1\" htmlFor=\"year\">\n                Publication Year\n              </label>\n              <input\n                id=\"year\"\n                type=\"number\"\n                value={year}\n                onChange={(e) => setYear(e.target.value)}\n                className=\"w-full px-4 py-2 bg-gray-800 text-white border border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-150\"\n              />\n            </div>\n\n            <div className=\"mb-4\">\n              <label className=\"block text-white font-semibold mb-1\" htmlFor=\"genre\">\n                Genre\n              </label>\n              <input\n                id=\"genre\"\n                type=\"text\"\n                value={genre}\n                onChange={(e) => setGenre(e.target.value)}\n                className=\"w-full px-4 py-2 bg-gray-800 text-white border border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-150\"\n              />\n            </div>\n\n            <div className=\"mb-6\">\n              <label className=\"block text-white font-semibold mb-1\" htmlFor=\"readDate\">\n                Date Read\n              </label>\n              <input\n                id=\"readDate\"\n                type=\"date\"\n                value={readDate}\n                onChange={(e) => setReadDate(e.target.value)}\n                className=\"w-full px-4 py-2 bg-gray-800 text-white border border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-150\"\n              />\n            </div>\n\n            <div className=\"flex justify-between\">\n              <button\n                type=\"button\"\n                onClick={() => router.back()}\n                className=\"px-4 py-2 border border-gray-600 text-white rounded hover:bg-gray-700 transition duration-150\"\n              >\n                Cancel\n              </button>\n              <button\n                type=\"submit\"\n                className=\"px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition duration-150\"\n              >\n                {isEditing ? 'Update Book' : 'Add Book'}\n              </button>\n            </div>\n          </form>\n        );\n      }\n\n.. edb:split-section::\n\n  Now, let's create the \"Add Book\" page that uses our form component.\n\n  .. code-block:: typescript\n      :caption: app/books/add/page.tsx\n\n      'use client';\n\n      import BookForm from \"@/src/components/BookForm\";\n\n      export default function AddBookPage() {\n        return (\n          <div className=\"container mx-auto p-4\">\n            <h1 className=\"text-2xl font-bold mb-6\">Add New Book</h1>\n            <BookForm />\n          </div>\n        );\n      }\n\n.. edb:split-section::\n\n  Let's also create a page to view book details and manage notes.\n\n  .. code-block:: typescript\n      :caption: src/app/books/[id]/page.tsx\n      :class: collapsible\n\n      'use client';\n\n      import { useState, useEffect, FormEvent, use } from 'react';\n      import { useRouter } from 'next/navigation';\n      import Link from 'next/link';\n      import { BookWithNotes } from '@/src/db';\n\n      export default function BookDetailPage({ params }: { params: Promise<{ id: string }> }) {\n        const { id } = use(params);\n        const router = useRouter();\n        const [book, setBook] = useState<BookWithNotes | null>(null);\n        const [loading, setLoading] = useState(true);\n        const [noteText, setNoteText] = useState('');\n\n        useEffect(() => {\n          async function fetchBook() {\n            try {\n              const response = await fetch(`/api/books/${id}`);\n              if (!response.ok) {\n                if (response.status === 404) {\n                  router.push('/');\n                  return;\n                }\n                throw new Error('Failed to fetch book');\n              }\n              const data = await response.json();\n              setBook(data);\n            } catch (error) {\n              console.error('Error:', error);\n            } finally {\n              setLoading(false);\n            }\n          }\n\n          fetchBook();\n        }, [id, router]);\n\n        const handleAddNote = async (e: FormEvent) => {\n          e.preventDefault();\n\n          if (!noteText.trim()) return;\n\n          try {\n            const response = await fetch(`/api/books/${id}/notes`, {\n              method: 'POST',\n              headers: { 'Content-Type': 'application/json' },\n              body: JSON.stringify({ text: noteText }),\n            });\n\n            if (!response.ok) throw new Error('Failed to add note');\n\n            const newNote = await response.json();\n            setBook(prev => prev ? {\n              ...prev,\n              notes: [...prev.notes, newNote]\n            } : null);\n            setNoteText('');\n          } catch (error) {\n            console.error('Error adding note:', error);\n          }\n        };\n\n        const handleDeleteNote = async (noteId: string) => {\n          try {\n            const response = await fetch(`/api/notes/${noteId}`, {\n              method: 'DELETE',\n            });\n\n            if (!response.ok) throw new Error('Failed to delete note');\n\n            setBook(prev => prev ? {\n              ...prev,\n              notes: prev.notes.filter(note => note.id !== noteId)\n            } : null);\n          } catch (error) {\n            console.error('Error deleting note:', error);\n          }\n        };\n\n        const handleDeleteBook = async () => {\n          if (!confirm('Are you sure you want to delete this book and all its notes?')) {\n            return;\n          }\n\n          try {\n            const response = await fetch(`/api/books/${id}`, {\n              method: 'DELETE',\n            });\n\n            if (!response.ok) throw new Error('Failed to delete book');\n\n            router.push('/');\n          } catch (error) {\n            console.error('Error deleting book:', error);\n          }\n        };\n\n        if (loading) {\n          return (\n            <div className=\"flex justify-center items-center min-h-screen\">\n              <p className=\"text-xl\">Loading...</p>\n            </div>\n          );\n        }\n\n        if (!book) {\n          return (\n            <div className=\"container mx-auto p-4\">\n              <p>Book not found.</p>\n              <Link href=\"/\" className=\"text-blue-500 hover:text-blue-600\">\n                Back to All Books\n              </Link>\n            </div>\n          );\n        }\n\n        return (\n          <div className=\"max-w-4xl mx-auto px-4 py-6\">\n            <div className=\"mb-6\">\n              <Link href=\"/\" className=\"text-blue-400 hover:underline\">\n                ← Back\n              </Link>\n            </div>\n\n            <div className=\"flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8\">\n              <h1 className=\"text-4xl font-extrabold leading-tight text-white\">\n                {book.title}\n              </h1>\n              <div className=\"mt-4 sm:mt-0 space-x-2\">\n                <Link\n                  href={`/books/${id}/edit`}\n                  className=\"px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 transition duration-150 inline-block\"\n                >\n                  Edit\n                </Link>\n                <button\n                  onClick={handleDeleteBook}\n                  className=\"px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-400 transition duration-150\"\n                >\n                  Delete\n                </button>\n              </div>\n            </div>\n\n            <div className=\"bg-gray-800 p-6 rounded-lg mb-8\">\n              {book.author && (\n                <p className=\"text-lg font-medium mb-2 text-white\">\n                  by {book.author}\n                </p>\n              )}\n              <div className=\"space-y-1 text-sm text-gray-300\">\n                {book.year && <p>Published: {book.year}</p>}\n                {book.genre && <p>Genre: {book.genre}</p>}\n                {book.readDate && (\n                  <p>Read on: {new Date(book.readDate).toLocaleDateString()}</p>\n                )}\n              </div>\n            </div>\n\n            <div className=\"mb-8\">\n              <h2 className=\"text-2xl font-semibold text-white mb-4\">Notes</h2>\n              <form onSubmit={handleAddNote} className=\"mb-6\">\n                <div className=\"flex\">\n                  <input\n                    type=\"text\"\n                    value={noteText}\n                    onChange={(e) => setNoteText(e.target.value)}\n                    placeholder=\"Add a new note...\"\n                    className=\"flex-grow px-4 py-2 bg-gray-700 text-white placeholder-gray-400 border border-gray-600 rounded-l focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-150\"\n                  />\n                  <button\n                    type=\"submit\"\n                    className=\"px-5 py-2 bg-blue-600 text-white rounded-r hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 transition duration-150\"\n                  >\n                    Add\n                  </button>\n                </div>\n              </form>\n\n              {book.notes.length === 0 ? (\n                <p className=\"text-gray-400 italic\">\n                  No notes yet. Add your first note above.\n                </p>\n              ) : (\n                <ul className=\"space-y-4\">\n                  {book.notes.map((note) => (\n                    <li\n                      key={note.id}\n                      className=\"flex justify-between items-start bg-gray-800 text-white p-4 rounded shadow-sm\"\n                    >\n                      <div>\n                        <p>{note.text}</p>\n                        {note.createdAt && (\n                          <p className=\"text-xs text-gray-400 mt-1\">\n                            {new Date(note.createdAt).toLocaleString()}\n                          </p>\n                        )}\n                      </div>\n                      <button\n                        onClick={() => handleDeleteNote(note.id)}\n                        className=\"text-red-400 hover:text-red-600 focus:outline-none transition duration-150\"\n                      >\n                        Delete\n                      </button>\n                    </li>\n                  ))}\n                </ul>\n              )}\n            </div>\n          </div>\n        );\n      }\n\n.. edb:split-section::\n\n  For a complete application, you would also need to implement an edit page for books.\n  Here's a simplified example:\n\n  .. code-block:: typescript\n      :caption: src/app/books/[id]/edit/page.tsx\n      :class: collapsible\n\n      'use client';\n\n      import { useState, useEffect, use } from 'react';\n      import { useRouter } from 'next/navigation';\n      import { Book } from '@/src/db';\n      import BookForm from '@/src/components/BookForm';\n\n      export default function EditBookPage({ params }: { params: Promise<{ id: string }> }) {\n        const router = useRouter();\n        const { id } = use(params);\n        const [book, setBook] = useState<Book | null>(null);\n        const [loading, setLoading] = useState(true);\n\n        useEffect(() => {\n          async function fetchBook() {\n            try {\n              const response = await fetch(`/api/books/${id}`);\n              if (!response.ok) {\n                if (response.status === 404) {\n                  router.push('/');\n                  return;\n                }\n                throw new Error('Failed to fetch book');\n              }\n              const data = await response.json();\n              setBook(data);\n            } catch (error) {\n              console.error('Error:', error);\n            } finally {\n              setLoading(false);\n            }\n          }\n\n          fetchBook();\n        }, [id, router]);\n\n        if (loading) {\n          return (\n            <div className=\"flex justify-center items-center min-h-screen\">\n              <p className=\"text-xl\">Loading...</p>\n            </div>\n          );\n        }\n\n        if (!book) {\n          return (\n            <div className=\"container mx-auto p-4\">\n              <p>Book not found.</p>\n              <button onClick={() => router.push('/')} className=\"text-blue-500 hover:text-blue-600\">\n                Back to All Books\n              </button>\n            </div>\n          );\n        }\n\n        return (\n          <div className=\"container mx-auto p-4\">\n            <h1 className=\"text-2xl font-bold mb-6\">Edit Book</h1>\n            <BookForm book={book} isEditing={true} />\n          </div>\n        );\n      }\n\n.. edb:split-section::\n\n  These UI components provide a basic but functional user interface for our Book Notes\n  application. Tailwind CSS helps us create a clean and responsive design with minimal\n  effort.\n\n  Since we're focusing on the Gel and Drizzle integration, we won't detail every UI\n  component, but the pattern is consistent throughout the application:\n\n  - We use React hooks for state management (useState, useEffect)\n  - We call our API endpoints to fetch and modify data\n  - We use Tailwind CSS classes for styling the components\n  - We implement client-side navigation with Next.js's useRouter\n\n7. Testing the application\n===========================\n\n.. edb:split-section::\n\n  Now that we have built our API routes and basic UI, let's test our application.\n  Start the development server:\n\n  .. code-block:: bash\n\n      $ npm run dev\n\n.. edb:split-section::\n\n  Navigate to http://localhost:3000 in your browser, and you should see your Book Notes\n  application. Try performing these operations to ensure everything is working correctly:\n\n  1. Adding a new book\n  2. Viewing book details\n  3. Adding notes to a book\n  4. Editing book information\n  5. Deleting notes\n  6. Deleting a book (which should also delete its notes)\n\n  If you encounter any issues, check your browser's developer console and the terminal\n  running your Next.js server for error messages.\n\n8. Next steps\n==============\n\nCongratulations! You've built a Book Notes application using Gel, Drizzle, and Next.js.\nThis tutorial demonstrated how these technologies can work together to create a\nfull-stack application.\n\nHere are some ideas for extending the application:\n\n1. **Add authentication**: Implement user authentication to allow multiple users\n   to have their own book collections.\n\n2. **Advanced filtering**: Add the ability to filter books by genre, author, or\n   reading status.\n\n3. **Book statistics**: Create a dashboard with statistics about your reading\n   habits.\n\n4. **Reading goals**: Implement a feature to set and track reading goals.\n\n5. **Book recommendations**: Add a feature to recommend books based on what\n   you've already read.\n\n6. **Import/Export**: Allow users to import or export their book data.\n\n7. **Search functionality**: Implement full-text search across books and notes.\n\nTo further explore the capabilities of Gel and Drizzle, you can check out these resources:\n\n- `Gel Documentation <https://docs.geldata.com/>`_\n- `Drizzle ORM Documentation <https://orm.drizzle.team/docs/overview>`_\n- `Next.js Documentation <https://nextjs.org/docs>`_\n\nRemember, you can find the complete source code for this tutorial in our\n`Gel Examples repository <https://github.com/geldata/gel-examples/tree/main/drizzle-book-notes-app>`_.\n\nHappy coding!\n"
  },
  {
    "path": "docs/intro/tutorials/index.rst",
    "content": "=========\nTutorials\n=========\n\n.. toctree::\n  :maxdepth: 2\n\n  ai_fastapi_searchbot\n  gel_drizzle_booknotes\n"
  },
  {
    "path": "docs/redirects",
    "content": "/guides/cloud -> /cloud\n\n/cli/edgedb -> docs/cli/gel\n/cli/edgedb_analyze -> docs/cli/gel_analyze\n/cli/edgedb_branch/edgedb_branch_create -> docs/cli/gel_branch/gel_branch_create\n/cli/edgedb_branch/edgedb_branch_drop -> docs/cli/gel_branch/gel_branch_drop\n/cli/edgedb_branch/edgedb_branch_list -> docs/cli/gel_branch/gel_branch_list\n/cli/edgedb_branch/edgedb_branch_merge -> docs/cli/gel_branch/gel_branch_merge\n/cli/edgedb_branch/edgedb_branch_rebase -> docs/cli/gel_branch/gel_branch_rebase\n/cli/edgedb_branch/edgedb_branch_rename -> docs/cli/gel_branch/gel_branch_rename\n/cli/edgedb_branch/edgedb_branch_switch -> docs/cli/gel_branch/gel_branch_switch\n/cli/edgedb_branch/edgedb_branch_wipe -> docs/cli/gel_branch/gel_branch_wipe\n/cli/edgedb_branch/index -> docs/cli/gel_branch/index\n/cli/edgedb_cli_upgrade -> docs/cli/gel_cli_upgrade\n/cli/edgedb_cloud/edgedb_cloud_login -> docs/cli/gel_cloud/gel_cloud_login\n/cli/edgedb_cloud/edgedb_cloud_logout -> docs/cli/gel_cloud/gel_cloud_logout\n/cli/edgedb_cloud/edgedb_cloud_secretkey/edgedb_cloud_secretkey_create -> docs/cli/gel_cloud/gel_cloud_secretkey/edgedb_cloud_secretkey_create\n/cli/edgedb_cloud/edgedb_cloud_secretkey/edgedb_cloud_secretkey_list -> docs/cli/gel_cloud/gel_cloud_secretkey/edgedb_cloud_secretkey_list\n/cli/edgedb_cloud/edgedb_cloud_secretkey/edgedb_cloud_secretkey_revoke -> docs/cli/gel_cloud/gel_cloud_secretkey/edgedb_cloud_secretkey_revoke\n/cli/edgedb_cloud/edgedb_cloud_secretkey/index -> docs/cli/gel_cloud/gel_cloud_secretkey/index\n/cli/edgedb_cloud/index -> docs/cli/gel_cloud/index\n/cli/edgedb_configure -> docs/cli/gel_configure\n/cli/edgedb_connopts -> docs/cli/gel_connopts\n/cli/edgedb_database/edgedb_database_create -> docs/cli/gel_database/gel_database_create\n/cli/edgedb_database/edgedb_database_drop -> docs/cli/gel_database/gel_database_drop\n/cli/edgedb_database/edgedb_database_wipe -> docs/cli/gel_database/gel_database_wipe\n/cli/edgedb_database/index -> docs/cli/gel_database/index\n/cli/edgedb_describe/edgedb_describe_object -> docs/cli/gel_describe/gel_describe_object\n/cli/edgedb_describe/edgedb_describe_schema -> docs/cli/gel_describe/gel_describe_schema\n/cli/edgedb_describe/index -> docs/cli/gel_describe/index\n/cli/edgedb_dump -> docs/cli/gel_dump\n/cli/edgedb_info -> docs/cli/gel_info\n/cli/edgedb_instance/edgedb_instance_create -> docs/cli/gel_instance/gel_instance_create\n/cli/edgedb_instance/edgedb_instance_credentials -> docs/cli/gel_instance/gel_instance_credentials\n/cli/edgedb_instance/edgedb_instance_destroy -> docs/cli/gel_instance/gel_instance_destroy\n/cli/edgedb_instance/edgedb_instance_link -> docs/cli/gel_instance/gel_instance_link\n/cli/edgedb_instance/edgedb_instance_list -> docs/cli/gel_instance/gel_instance_list\n/cli/edgedb_instance/edgedb_instance_logs -> docs/cli/gel_instance/gel_instance_logs\n/cli/edgedb_instance/edgedb_instance_reset_password -> docs/cli/gel_instance/gel_instance_reset_password\n/cli/edgedb_instance/edgedb_instance_restart -> docs/cli/gel_instance/gel_instance_restart\n/cli/edgedb_instance/edgedb_instance_revert -> docs/cli/gel_instance/gel_instance_revert\n/cli/edgedb_instance/edgedb_instance_start -> docs/cli/gel_instance/gel_instance_start\n/cli/edgedb_instance/edgedb_instance_status -> docs/cli/gel_instance/gel_instance_status\n/cli/edgedb_instance/edgedb_instance_stop -> docs/cli/gel_instance/gel_instance_stop\n/cli/edgedb_instance/edgedb_instance_unlink -> docs/cli/gel_instance/gel_instance_unlink\n/cli/edgedb_instance/edgedb_instance_upgrade -> docs/cli/gel_instance/gel_instance_upgrade\n/cli/edgedb_instance/index -> docs/cli/gel_instance/index\n/cli/edgedb_list -> docs/cli/gel_list\n/cli/edgedb_migrate -> docs/cli/gel_migrate\n/cli/edgedb_migration/edgedb_migration_apply -> docs/cli/gel_migration/gel_migration_apply\n/cli/edgedb_migration/edgedb_migration_create -> docs/cli/gel_migration/gel_migration_create\n/cli/edgedb_migration/edgedb_migration_edit -> docs/cli/gel_migration/gel_migration_edit\n/cli/edgedb_migration/edgedb_migration_extract -> docs/cli/gel_migration/gel_migration_extract\n/cli/edgedb_migration/edgedb_migration_log -> docs/cli/gel_migration/gel_migration_log\n/cli/edgedb_migration/edgedb_migration_status -> docs/cli/gel_migration/gel_migration_status\n/cli/edgedb_migration/edgedb_migration_upgrade_check -> docs/cli/gel_migration/gel_migration_upgrade_check\n/cli/edgedb_migration/index -> docs/cli/gel_migration/index\n/cli/edgedb_project/edgedb_project_info -> docs/cli/gel_project/gel_project_info\n/cli/edgedb_project/edgedb_project_init -> docs/cli/gel_project/gel_project_init\n/cli/edgedb_project/edgedb_project_unlink -> docs/cli/gel_project/gel_project_unlink\n/cli/edgedb_project/edgedb_project_upgrade -> docs/cli/gel_project/gel_project_upgrade\n/cli/edgedb_project/index -> docs/cli/gel_project/index\n/cli/edgedb_query -> docs/cli/gel_query\n/cli/edgedb_restore -> docs/cli/gel_restore\n/cli/edgedb_server/edgedb_server_info -> docs/cli/gel_server/gel_server_info\n/cli/edgedb_server/edgedb_server_install -> docs/cli/gel_server/gel_server_install\n/cli/edgedb_server/edgedb_server_list_versions -> docs/cli/gel_server/gel_server_list_versions\n/cli/edgedb_server/edgedb_server_uninstall -> docs/cli/gel_server/gel_server_uninstall\n/cli/edgedb_server/index -> docs/cli/gel_server/index\n/cli/edgedb_ui -> docs/cli/gel_ui\n/cli/edgedb_watch -> docs/cli/gel_watch\n"
  },
  {
    "path": "docs/redirects.js",
    "content": "// See https://nextjs.org/docs/app/api-reference/config/next-config-js/redirects\nmodule.exports = [\n  {\n    source: \"/changelog/:path*\",\n    destination: \"/resources/changelog/:path*\",\n    permanent: false,\n  },\n  {\n    source: \"/guides/cheatsheet/:path*\",\n    destination: \"/resources/cheatsheets/:path*\",\n    permanent: true,\n  },\n  {\n    source: \"/guides/ai/:path*\",\n    destination: \"/ai/:path*\",\n    permanent: true,\n  },\n  {\n    source: \"/changelog/6_x\",\n    destination: \"/resources/changelog/6_x\",\n    permanent: true,\n  },\n  {\n    source: \"/database/:path*\",\n    destination: \"/reference/:path*\",\n    permanent: false,\n  },\n  {\n    source: \"/guides/:path*\",\n    destination: \"/resources/guides/:path*\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/reference/bindings/datetime\",\n    destination: \"/reference/using/datetime\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/reference/bindings\",\n    destination: \"/reference/using\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/go/:path*\",\n    destination: \"https://pkg.go.dev/github.com/geldata/gel-go\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/rust/:path*\",\n    destination: \"https://docs.rs/gel-tokio\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/libraries/dotnet/:path*\",\n    destination: \"https://github.com/geldata/gel-net\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/libraries/elixir/:path*\",\n    destination: \"https://hexdocs.pm/gel\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/libraries/java/:path*\",\n    destination: \"https://github.com/geldata/gel-java\",\n    permanent: false,\n  },\n  // Use the further redirects to get to the correct point in the consolidated docs\n  {\n    source: \"/reference/libraries/js/:path*\",\n    destination: \"/reference/clients/js/:path*\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/libraries/python/:path*\",\n    destination: \"/reference/clients/python/:path*\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/libraries/:path*\",\n    destination: \"/reference/using\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/js/delete#delete\",\n    destination: \"/reference/using/js/querybuilder\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/js/driver\",\n    destination: \"/reference/using/js\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/js/for\",\n    destination: \"/reference/using/js/querybuilder#for\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/js/funcops\",\n    destination: \"/reference/using/js/querybuilder#functions-and-operators\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/js/group\",\n    destination: \"/reference/using/js/querybuilder#group\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/js/insert\",\n    destination: \"/reference/using/js/querybuilder#insert\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/js/literals\",\n    destination: \"/reference/using/js/querybuilder#types-and-literals\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/js/objects\",\n    destination: \"/reference/using/js/querybuilder#objects-and-paths\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/js/parameters\",\n    destination: \"/reference/using/js/querybuilder#parameters\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/js/select\",\n    destination: \"/reference/using/js/querybuilder#select\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/js/types\",\n    destination: \"/reference/using/js/querybuilder#types-and-literals\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/js/update\",\n    destination: \"/reference/using/js/querybuilder#update\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/js/with\",\n    destination: \"/reference/using/js/querybuilder#with-blocks\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/js/reference\",\n    destination: \"/reference/using/js/client#client-reference\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/reference/connection\",\n    destination: \"/reference/using/connection\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/reference/dsn\",\n    destination: \"/reference/using/connection#dsn\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/python/api/asyncio_client\",\n    destination: \"/reference/using/python/client#asyncio-client\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/python/api/blocking_client\",\n    destination: \"/reference/using/python/client#blocking-client\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/python/installation\",\n    destination: \"/reference/using/python#installation\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/python/usage\",\n    destination: \"/reference/using/python#basic-usage\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/http/health-checks\",\n    destination: \"/reference/running/http#health-checks\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/http/protocol\",\n    destination: \"/reference/using/http\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/clients/:path*\",\n    destination: \"/reference/using/:path*\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/reference/configuration\",\n    destination: \"/reference/running/configuration\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/reference/environment\",\n    destination: \"/reference/running/configuration#environment-variables\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/reference/gel_toml\",\n    destination: \"/reference/using/projects#gel-toml\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/reference/http\",\n    destination: \"/reference/running/http\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/reference/projects\",\n    destination: \"/reference/using/projects\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/reference/protocol/:path*\",\n    destination: \"/resources/protocol/:path*\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/reference/admin/databases\",\n    destination: \"/reference/datamodel/branches\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/reference/admin/:path*\",\n    destination: \"/reference/running/admin/:path*\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/reference/backend-ha\",\n    destination: \"/reference/running/backend-ha\",\n    permanent: false,\n  },\n  {\n    source: \"/resources/guides/deployment/:path*\",\n    destination: \"/reference/running/deployment/:path*\",\n    permanent: false,\n  },\n  {\n    source: \"/reference/reference/postgis\",\n    destination: \"/reference/stdlib/postgis\",\n    permanent: true,\n  },\n  {\n    source: \"/reference/cli/:path*\",\n    destination: \"/reference/using/cli/:path*\",\n    permanent: false,\n  },\n  {\n    source: \"/cli/:path*\",\n    destination: \"/reference/using/cli/:path*\",\n    permanent: false,\n  },\n];\n"
  },
  {
    "path": "docs/reference/ai/extai.rst",
    "content": ".. _ref_ai_extai_reference:\n\n=======\next::ai\n=======\n\nThis reference documents the |Gel| ``ext::ai`` extension components, configuration options, and database APIs.\n\n\nEnabling the Extension\n======================\n\nThe AI extension can be enabled using the :ref:`extension <ref_datamodel_extensions>` mechanism:\n\n.. code-block:: sdl\n\n    using extension ai;\n\nConfiguration\n=============\n\nThe AI extension can be configured using ``configure session`` or ``configure current branch``:\n\n.. code-block:: edgeql\n\n    configure current branch\n    set ext::ai::Config::indexer_naptime := <duration>'PT30S';\n\nConfiguration Properties\n------------------------\n\n* ``indexer_naptime``: Duration\n    Specifies minimum delay between deferred ``ext::ai::index`` indexer runs.\n\nView current configuration:\n\n.. code-block:: edgeql\n\n    select cfg::Config.extensions[is ext::ai::Config]{*};\n\nReset configuration:\n\n.. code-block:: edgeql\n\n    configure current branch\n    reset ext::ai::Config::indexer_naptime;\n\n\n.. _ref_ai_extai_reference_ui:\n\nUI\n==\n\nThe AI section of the UI can be accessed via the sidebar after the extension\nhas been enabled in the schema. It provides ways to manage provider\nconfigurations and RAG prompts, as well as try out different settings in the\nplayground.\n\nPlayground tab\n--------------\n\nProvides an interactive environment for testing and configuring the built-in\nRAG.\n\n.. image:: images/ui_playground.png\n    :alt: Screenshot of the Playground tab of the UI depicting an empty message window and three input fields set with default values.\n    :width: 100%\n\nComponents:\n\n* Message window: Displays conversation history between the user and the LLM.\n* Model: Dropdown menu for selecting the text generation model.\n* Prompt: Dropdown menu for selecting the RAG prompt template.\n* Context Query: Input field for entering an EdgeQL expression returning a set of objects with AI indexes.\n\n\nPrompts tab\n-----------\n\nProvides ways to manage system prompts used in the built-in RAG.\n\n.. image:: images/ui_prompts.png\n    :alt: Screenshot of the Prompts tab of the UI depicting an expanded prompt configuration menu.\n    :width: 100%\n\nProviders tab\n-------------\n\nEnables management of API configurations for AI API providers.\n\n.. image:: images/ui_providers.png\n    :alt: Screenshot of the Providers tab of the UI depicting an expanded provider configuration menu.\n    :width: 100%\n\n\n\n.. _ref_ai_extai_reference_index:\n\nIndex\n=====\n\nThe ``ext::ai::index`` creates a deferred semantic similarity index of an\nexpression on a type.\n\n.. code-block:: sdl-diff\n\n      module default {\n        type Astronomy {\n          content: str;\n    +     deferred index ext::ai::index(embedding_model := 'text-embedding-3-small')\n    +       on (.content);\n        }\n      };\n\n\nParameters:\n\n* ``embedding_model``- The name of the model to use for embedding generation as\n  a string.\n* ``distance_function``- The function to use for determining semantic\n  similarity. Default: ``ext::ai::DistanceFunction.Cosine``\n* ``index_type``- The type of index to create. Currently the only option is the\n  default: ``ext::ai::IndexType.HNSW``.\n* ``index_parameters``- A named tuple of additional index parameters:\n\n  * ``m``- The maximum number of edges of each node in the graph. Increasing\n    can increase the accuracy of searches at the cost of index size. Default:\n    ``32``\n  * ``ef_construction``- Dictates the depth and width of the search when\n    building the index. Higher values can lead to better connections and more\n    accurate results at the cost of time and resource usage when building the\n    index. Default: ``100``\n\n* ``dimensions``: int64 (Optional) - Embedding dimensions\n* ``truncate_to_max``: bool (Default: False)\n\n\nFunctions\n=========\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::ai::to_context`\n      - :eql:func-desc:`ext::ai::to_context`\n\n    * - :eql:func:`ext::ai::search`\n      - :eql:func-desc:`ext::ai::search`\n\n\n------------\n\n\n.. eql:function:: ext::ai::to_context(object: anyobject) -> str\n\n    Returns the indexed expression value for an object with an ``ext::ai::index``.\n\n    **Example**:\n\n    Schema:\n\n    .. code-block:: sdl\n\n        module default {\n          type Astronomy {\n            topic: str;\n            content: str;\n            deferred index ext::ai::index(embedding_model := 'text-embedding-3-small')\n              on (.topic ++ ' ' ++ .content);\n          }\n        };\n\n    Data:\n\n    .. code-block:: edgeql-repl\n\n        db> insert Astronomy {\n        ...   topic := 'Mars',\n        ...   content := 'Skies on Mars are red.'\n        ... }\n        db> insert Astronomy {\n        ...   topic := 'Earth',\n        ...   content := 'Skies on Earth are blue.'\n        ... }\n\n    Results of calling ``to_context``:\n\n    .. code-block:: edgeql-repl\n\n        db> select ext::ai::to_context(Astronomy);\n\n        {'Mars Skies on Mars are red.', 'Earth Skies on Earth are blue.'}\n\n\n------------\n\n\n.. eql:function:: ext::ai::search( \\\n                    object: anyobject, \\\n                    query: array<float32> \\\n                  ) -> optional tuple<object: anyobject, distance: float64>\n                  ext::ai::search( \\\n                    object: anyobject, \\\n                    query: str \\\n                  ) -> optional tuple<object: anyobject, distance: float64>\n\n    Searches objects using their :ref:`ai::index\n    <ref_ai_extai_reference_index>`.\n\n    Returns tuples of (object, distance).\n\n    .. versionadded:: 7.0\n\n        If the ``query``  is a ``str``, the ai extension will make an embedding\n        request to the provider and use the result to compute distances.\n\n        To prevent unwanted provider calls, this functionality may only be\n        used by roles with the :eql:permission:`ext::ai::perm::provider_call`\n        permission.\n\n    .. code-block:: edgeql-repl\n\n        db> with query := <array<float32>><json>$query\n        ... select ext::ai::search(Knowledge, query);\n\n        {\n          (\n            object := default::Knowledge {id: 9af0d0e8-0880-11ef-9b6b-4335855251c4},\n            distance := 0.20410746335983276\n          ),\n          (\n            object := default::Knowledge {id: eeacf638-07f6-11ef-b9e9-57078acfce39},\n            distance := 0.7843298847773637\n          ),\n          (\n            object := default::Knowledge {id: f70863c6-07f6-11ef-b9e9-3708318e69ee},\n            distance := 0.8560434728860855\n          ),\n        }\n\n\nScalar and Object Types\n=======================\n\nProvider Configuration Types\n----------------------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:type:`ext::ai::ProviderAPIStyle`\n      - Enum defining supported API styles\n\n    * - :eql:type:`ext::ai::ProviderConfig`\n      - Abstract base configuration for AI providers.\n\n\nProvider configurations are required for AI indexes and RAG functionality.\n\nExample provider configuration:\n\n.. code-block:: edgeql\n\n    configure current database\n    insert ext::ai::OpenAIProviderConfig {\n      secret := 'sk-....',\n    };\n\n.. note::\n\n    All provider types require the ``secret`` property be set with a string\n    containing the secret provided by the AI vendor.\n\n\n.. note::\n\n    ``ext::ai::CustomProviderConfig requires an ``api_style`` property be set.\n\n\n---------\n\n\n.. eql:type:: ext::ai::ProviderAPIStyle\n\n    Enum defining supported API styles:\n\n    * ``OpenAI``\n    * ``Anthropic``\n\n\n---------\n\n\n.. eql:type:: ext::ai::ProviderConfig\n\n    Abstract base configuration for AI providers.\n\n    Properties:\n\n    * ``name``: str (Required) - Unique provider identifier\n    * ``display_name``: str (Required) - Human-readable name\n    * ``api_url``: str (Required) - Provider API endpoint\n    * ``client_id``: str (Optional) - Provider-supplied client ID\n    * ``secret``: str (Required) - Provider API secret\n    * ``api_style``: ProviderAPIStyle (Required) - Provider's API style\n\n    Provider-specific types:\n\n    * ``ext::ai::OpenAIProviderConfig``\n    * ``ext::ai::MistralProviderConfig``\n    * ``ext::ai::AnthropicProviderConfig``\n    * ``ext::ai::CustomProviderConfig``\n\n    Each inherits from :eql:type:`ext::ai::ProviderConfig` with provider-specific defaults.\n\n\nModel Types\n-----------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:type:`ext::ai::Model`\n      - Abstract base type for AI models.\n\n    * - :eql:type:`ext::ai::EmbeddingModel`\n      - Abstract type for embedding models.\n\n    * - :eql:type:`ext::ai::TextGenerationModel`\n      - Abstract type for text generation models.\n\n\n.. _ref_ai_extai_reference_embedding_models:\n\nEmbedding models\n^^^^^^^^^^^^^^^^\n\nOpenAI (`documentation <https://platform.openai.com/docs/guides/embeddings/embedding-models>`__)\n\n* ``text-embedding-3-small``\n* ``text-embedding-3-large``\n* ``text-embedding-ada-002``\n\nMistral (`documentation <https://docs.mistral.ai/capabilities/embeddings/#mistral-embeddings-api>`__)\n\n* ``mistral-embed``\n\nOllama (`documentation <https://github.com/ollama/ollama/blob/main/docs/api.md#generate-embeddings>`__)\n\n* ``nomic-embed-text``\n* ``bge-m3``\n\n\n.. _ref_ai_extai_reference_text_generation_models:\n\nText generation models\n^^^^^^^^^^^^^^^^^^^^^^\n\nOpenAI (`documentation <https://platform.openai.com/docs/guides/text-generation>`__)\n\n* ``gpt-3.5-turbo``\n* ``gpt-4-turbo-preview``\n\nMistral (`documentation <https://docs.mistral.ai/getting-started/models/>`__)\n\n* ``mistral-small-latest``\n* ``mistral-medium-latest``\n* ``mistral-large-latest``\n\nAnthropic (`documentation <https://docs.anthropic.com/claude/docs/models-overview>`__)\n\n* ``claude-3-haiku-20240307``\n* ``claude-3-sonnet-20240229``\n* ``claude-3-opus-20240229``\n\nOllama (`documentation <https://github.com/ollama/ollama/blob/main/docs/api.md#generate-a-chat-completion>`__)\n\n* ``llama3.2``\n* ``llama3.3``\n\nWhen using RAG, It is possible to specify a text generation model using a URI,\ncombining the provider name (in lower case), and the model name.\n\n- eg. ``\"openai:gpt-5\"``\n- eg. ``\"anthropic:claude-opus-4-20250514\"``\n\nUsing this form allows text generation from models which are not explicitly\ninstantiated as a :eql:type:`ext::ai::TextGenerationModel`\n\n\n---------\n\n.. eql:type:: ext::ai::Model\n\n    Abstract base type for AI models.\n\n    Annotations:\n\n    * ``model_name`` - Model identifier\n    * ``model_provider`` - Provider identifier\n\n---------\n\n.. eql:type:: ext::ai::EmbeddingModel\n\n    Abstract type for embedding models.\n\n    Annotations:\n\n    * ``embedding_model_max_input_tokens`` - Maximum tokens per input\n    * ``embedding_model_max_batch_tokens`` - Maximum tokens per batch. Default: ``'8191'``.\n    * ``embedding_model_max_batch_size`` - Maximum inputs per batch. Optional.\n    * ``embedding_model_max_output_dimensions`` - Maximum embedding dimensions\n    * ``embedding_model_supports_shortening`` - Input shortening support flag\n\n---------\n\n.. eql:type:: ext::ai::TextGenerationModel\n\n    Abstract type for text generation models.\n\n    Annotations:\n\n    * ``text_gen_model_context_window`` - Model's context window size\n\n\nIndexing Types\n--------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:type:`ext::ai::DistanceFunction`\n      - Enum for similarity metrics.\n\n    * - :eql:type:`ext::ai::IndexType`\n      - Enum for index implementations.\n\n---------\n\n.. eql:type:: ext::ai::DistanceFunction\n\n    Enum for similarity metrics.\n\n    * ``Cosine``\n    * ``InnerProduct``\n    * ``L2``\n\n---------\n\n.. eql:type:: ext::ai::IndexType\n\n    Enum for index implementations.\n\n    * ``HNSW``\n\n\n\nPrompt Types\n------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:type:`ext::ai::ChatParticipantRole`\n      - Enum for chat roles.\n\n    * - :eql:type:`ext::ai::ChatPromptMessage`\n      - Type for chat prompt messages.\n\n    * - :eql:type:`ext::ai::ChatPrompt`\n      - Type for chat prompt configuration.\n\nExample custom prompt configuration:\n\n.. code-block:: edgeql\n\n    insert ext::ai::ChatPrompt {\n      name := 'test-prompt',\n      messages := (\n        insert ext::ai::ChatPromptMessage {\n          participant_role := ext::ai::ChatParticipantRole.System,\n          content := \"Your message content\"\n        }\n      )\n    };\n\n\n---------\n\n.. eql:type:: ext::ai::ChatParticipantRole\n\n    Enum for chat roles.\n\n    * ``System``\n    * ``User``\n    * ``Assistant``\n    * ``Tool``\n\n---------\n\n.. eql:type:: ext::ai::ChatPromptMessage\n\n    Type for chat prompt messages.\n\n    Properties:\n\n    * ``participant_role``: ChatParticipantRole (Required)\n    * ``participant_name``: str (Optional)\n    * ``content``: str (Required)\n\n---------\n\n.. eql:type:: ext::ai::ChatPrompt\n\n    Type for chat prompt configuration.\n\n    Properties:\n\n    * ``name``: str (Required)\n    * ``messages``: set of ChatPromptMessage (Required)\n\n\nPermissions\n===========\n\n.. _ref_ai_extai_reference_permissions:\n\n.. versionadded:: 7.0\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:permission:`ext::ai::perm::provider_call`\n    * - :eql:permission:`ext::ai::perm::chat_prompt_read`\n    * - :eql:permission:`ext::ai::perm::chat_prompt_write`\n\n---------\n\n.. eql:permission:: ext::ai::perm::provider_call\n\n    Gives permission to make ai provider calls.\n\n    Required to call :eql:func:`ext::ai::search` using text directly\n    instead of an already generated embedding.\n\n---------\n\n.. eql:permission:: ext::ai::perm::chat_prompt_read\n\n    Gives permission to read chat prompt configuration.\n\n---------\n\n.. eql:permission:: ext::ai::perm::chat_prompt_write\n\n    Gives permission to modify chat prompt configuration.\n"
  },
  {
    "path": "docs/reference/ai/extvectorstore.rst",
    "content": ":orphan:\n\n.. _ref_extvectorstore_reference:\n\n================\next::vectorstore\n================\n\n\nThe ``ext::vectorstore`` extension package provides simplified vectorstore\nworkflows for |Gel|, built on top of the pgvector integration. It includes\npredefined vector dimensions and a base schema for vector storage records.\n\n\nEnabling the extension\n======================\n\nThe extension package can be installed using the :gelcmd:`extension` CLI\ncommand:\n\n.. code-block:: bash\n\n    $ gel extension install vectorstore\n\n\nIt can be enabled using the :ref:`extension <ref_datamodel_extensions>`\nmechanism:\n\n.. code-block:: sdl\n\n    using extension vectorstore;\n\n\nThe Vectorstore extension is designed to be used in combination with the\n:ref:`Vectostore Python binding <ref_ai_vectorstore_python>` or other\nintegrations, rather than on its own.\n\n\nTypes\n=====\n\nVector Types\n------------\n\nThe extension provides two pre-defined vector types with different dimensions:\n\n- ``ext::vectorstore::vector_1024``: 1024-dimensional vector\n- ``ext::vectorstore::vector_1536``: 1536-dimensional vector\n\nAll vector types extend ``ext::pgvector::vector`` with their respective dimensions.\n\n\nRecord Types\n------------\n\n.. eql:type:: ext::vectorstore::BaseRecord\n\n    Abstract type that defines the basic structure for vector storage records.\n\n    Properties:\n\n    * ``collection: str`` (required): Identifies the collection the record belongs to\n    * ``text: str``: Associated text content\n    * ``embedding: ext::pgvector::vector``: The vector embedding\n    * ``external_id: str``: External identifier with unique constraint\n    * ``metadata: json``: Additional metadata in JSON format\n\n\n.. eql:type:: ext::vectorstore::DefaultRecord\n\n    Extends :eql:type:`ext::vectorstore::BaseRecord` with specific\n    configurations.\n\n    Properties:\n\n    * Inherits all properties from :eql:type:`ext::vectorstore::BaseRecord`\n    * Specializes ``embedding`` to use ``vector_1536`` type\n    * Includes an HNSW cosine similarity index on the embedding with:\n\n      * ``m = 16``\n      * ``ef_construction = 128``\n\n\n"
  },
  {
    "path": "docs/reference/ai/http.rst",
    "content": ".. _ref_ai_http_reference:\n\n========\nHTTP API\n========\n\n.. note::\n\n    All |Gel| server HTTP endpoints require :ref:`authentication\n    <ref_http_auth>`, such as `HTTP Basic Authentication\n    <https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme>`_\n    with Gel username and password.\n\n\nEmbeddings\n==========\n\n``POST``: ``https://<gel-host>:<port>/branch/<branch-name>/ai/embeddings``\n\nGenerates text embeddings using the specified embeddings model.\n\n\nRequest headers\n---------------\n\n* ``Content-Type: application/json`` (required)\n\n\nRequest body\n------------\n\n* ``inputs`` (array of strings, or single string, required): The text items to use as the basis\n  for embeddings generation.\n* ``model`` (string, required): The name of the embedding model to use. You may\n  use any of the supported :ref:`embedding models\n  <ref_ai_extai_reference_embedding_models>`.\n* ``dimensions`` (number, optional): The number of dimensions to truncate to.\n\n* ``user`` (string, optional): A user identifier for the request.\n\n\nExample request\n---------------\n\n.. code-block:: bash\n\n  $ curl --user <username>:<password> --json '{\\\n      \"inputs\": [\"What color is the sky on Mars?\"],\\\n      \"model\": \"text-embedding-3-small\"\\\n    }' http://localhost:10931/branch/main/ai/embeddings\n\n\nResponse\n--------\n\n* **HTTP status**: 200 OK\n* **Content-Type**: application/json\n* **Body**:\n\n\n.. code-block:: json\n\n    {\n      \"object\": \"list\",\n      \"data\": [\n        {\n          \"object\": \"embedding\",\n          \"index\": 0,\n          \"embedding\": [-0.009434271, 0.009137661]\n        }\n      ],\n      \"model\": \"text-embedding-3-small\",\n      \"usage\": {\n        \"prompt_tokens\": 8,\n        \"total_tokens\": 8\n      }\n    }\n\n.. note::\n\n    The ``embedding`` property is shown here with only two values for brevity,\n    but an actual response would contain many more values.\n\n\nError response\n--------------\n\n* **HTTP status**: 400 Bad Request\n* **Content-Type**: application/json\n* **Body**:\n\n  .. code-block:: json\n\n      {\n        \"message\": \"missing or empty required \\\"model\\\" value  in request\",\n        \"type\": \"BadRequestError\"\n      }\n\nRAG\n===\n\n``POST``: ``https://<gel-host>:<port>/branch/<branch-name>/ai/rag``\n\nPerforms retrieval-augmented text generation using the specified model based on\nthe provided text query and the database content selected using similarity\nsearch.\n\n\nRequest headers\n---------------\n\n* ``Content-Type: application/json`` (required)\n\n\nRequest body\n------------\n\n* ``context`` (object, required): Settings that define the context of the query.\n   * ``query`` (string, required): Specifies an expression to determine the relevant objects and index to serve as context for text generation. You may set this to any expression that produces a set of objects, even if it is not a standalone query.\n   * ``variables`` (object, optional): A dictionary of variables for use in the context query.\n   * ``globals`` (object, optional): A dictionary of globals for use in the context query.\n   * ``max_object_count`` (number, optional): Maximum number of objects to retrieve; default is 5.\n\n* ``model`` (string, required): The name of the text generation model to use. It is possible to specify the\n  model name as a URI, eg. ``openai:gpt-5``. See: :ref:`text generation models\n  <ref_ai_extai_reference_text_generation_models>`.\n\n* ``query`` (string, required): The query string used as the basis for text generation.\n\n* ``stream`` (boolean, optional): Specifies whether the response should be streamed. Defaults to false.\n\n* ``prompt`` (object, optional): Settings that define a prompt. Omit to use the default prompt.\n   * ``name`` (string, optional): Name of predefined prompt.\n   * ``id`` (string, optional): ID of predefined prompt.\n   * ``custom`` (array of objects, optional): Custom prompt messages, each containing a ``role`` and ``content``. If no ``name`` or ``id`` was provided, the custom messages provided here become the prompt. If one of those was provided, these messages will be added to that existing prompt.\n      * ``role`` (string): \"system\", \"user\", \"assistant\", or \"tool\".\n      * ``content`` (string | array): Content of the message.\n         * For ``role: \"system\"``: Must be a string.\n         * For ``role: \"user\"``: Must be an array of content blocks, e.g., ``[{\"type\": \"text\", \"text\": \"...\"}]``.\n         * For ``role: \"assistant\"``: Must be a string (the assistant's text response). May optionally include ``tool_calls``.\n         * For ``role: \"tool\"``: Must be a string (the result of the tool call). Requires ``tool_call_id``.\n      * ``tool_call_id`` (string, optional): Identifier for the tool call whose result this message represents (required if ``role: \"tool\"``).\n      * ``tool_calls`` (array, optional): Array of tool calls requested by the assistant (used if ``role: \"assistant\"``). Each object should follow the format: ``{\"id\": \"...\", \"type\": \"function\", \"function\": {\"name\": \"...\", \"arguments\": \"...\"}}``. Arguments should be a JSON string.\n\n* ``temperature`` (number, optional): Sampling temperature.\n\n* ``top_p`` (number, optional): Nucleus sampling parameter.\n\n* ``max_tokens`` (number, optional): Maximum tokens to generate.\n\n* ``seed`` (number, optional): Random seed.\n\n* ``safe_prompt`` (boolean, optional): Enable safety features.\n\n* ``top_k`` (number, optional): Top-k sampling parameter.\n\n* ``logit_bias`` (object, optional): Token biasing.\n\n* ``logprobs`` (number, optional): Return token log probabilities.\n\n* ``user`` (string, optional): User identifier.\n\n* ``tools`` (array, optional): A list of tools the model may call. Each tool\n  has a ``type`` (\"function\") and a ``function`` object with ``name``,\n  ``description`` (optional), and ``parameters`` (JSON schema). Example:\n  ``[{\"type\": \"function\", \"function\": {\"name\": \"get_weather\", \"description\": \"Get the current weather\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\"}}, \"required\": [\"location\"]}}}]``\n\n\nExample request\n---------------\n\n.. code-block:: bash\n\n  $ curl --user <username>:<password> --json '{\\\n      \"query\": \"What color is the sky on Mars?\",\\\n      \"model\": \"gpt-4-turbo-preview\",\\\n      \"context\": {\"query\":\"Knowledge\"}\\\n    }' http://<gel-host>:<port>/branch/main/ai/rag\n\n\nResponse\n--------\n\n* **HTTP status**: 200 OK\n* **Content-Type**: application/json\n* **Body**: A JSON object containing the RAG response details.\n\n  .. code-block:: json\n\n      {\n        \"id\": \"chatcmpl-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx\",\n        \"model\": \"gpt-4-turbo-preview\",\n        \"text\": \"The sky on Mars typically appears butterscotch or reddish due to the fine dust particles suspended in the atmosphere.\",\n        \"finish_reason\": \"stop\",\n        \"usage\": {\n          \"prompt_tokens\": 50,\n          \"completion_tokens\": 30,\n          \"total_tokens\": 80\n        },\n        \"logprobs\": null,\n        \"tool_calls\": null\n      }\n\n  * ``id`` (string): Unique identifier for the chat completion.\n  * ``model`` (string): The model used for the chat completion.\n  * ``text`` (string | null): The main text content of the response message.\n  * ``finish_reason`` (string | null): The reason the model stopped generating tokens (e.g., \"stop\", \"length\", \"tool_calls\").\n  * ``usage`` (object | null): Token usage statistics for the request.\n  * ``logprobs`` (object | null): Log probability information for the generated tokens (if requested).\n  * ``tool_calls`` (array | null): Any tool calls requested by the model. Each element contains ``id``, ``type`` (\"function\"), ``name``, and ``args`` (parsed JSON object).\n\n\nError response\n--------------\n\n* **HTTP status**: 400 Bad Request\n* **Content-Type**: application/json\n* **Body**:\n\n  .. code-block:: json\n\n      {\n        \"message\": \"missing required 'query' in request 'context' object\",\n        \"type\": \"BadRequestError\"\n      }\n\n\nStreaming response (SSE)\n------------------------\n\nWhen the ``stream`` parameter is set to ``true``, the server uses `Server-Sent\nEvents\n<https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events>`__\n(SSE) to stream responses. Here is a detailed breakdown of the typical\nsequence and structure of events in a streaming response:\n\n* **HTTP Status**: 200 OK\n* **Content-Type**: text/event-stream\n* **Cache-Control**: no-cache\n\nThe stream consists of a sequence of five events, each encapsulating part of\nthe response in a structured format:\n\n1. **Message start**\n\n   * Event type: ``message_start``\n\n   * Data: Starts a message, specifying identifiers, roles, and initial usage.\n\n   .. code-block:: json\n\n      {\n        \"type\": \"message_start\",\n        \"message\": {\n          \"id\": \"<message_id>\",\n          \"role\": \"assistant\",\n          \"model\": \"<model_name>\",\n          \"usage\": { \"prompt_tokens\": 10 }\n        }\n      }\n\n2. **Content block start**\n\n   * Event type: ``content_block_start``\n\n   * Data: Marks the beginning of a new content block (either text or a tool call).\n\n   .. code-block:: json\n\n      {\n        \"type\": \"content_block_start\",\n        \"index\": 0,\n        \"content_block\": {\n          \"type\": \"text\",\n          \"text\": \"\"\n        }\n      }\n\n   Or for a tool call:\n\n   .. code-block:: json\n\n      {\n        \"type\": \"content_block_start\",\n        \"index\": 0,\n        \"content_block\": {\n          \"id\": \"<tool_call_id>\",\n          \"type\": \"tool_use\",\n          \"name\": \"<function_name>\",\n          \"args\": \"{...\"\n        }\n      }\n\n\n3. **Content block delta**\n\n   * Event type: ``content_block_delta``\n\n   * Data: Incrementally updates the content, appending more text or tool arguments. Includes logprobs if requested.\n\n   .. code-block:: json\n\n      {\n        \"type\": \"content_block_delta\",\n        \"index\": 0,\n        \"delta\": {\n          \"type\": \"text_delta\",\n          \"text\": \"The\"\n        },\n        \"logprobs\": null\n      }\n\n   Or for tool arguments:\n\n   .. code-block:: json\n\n     {\n       \"type\": \"content_block_delta\",\n       \"index\": 0,\n       \"delta\": {\n         \"type\": \"tool_call_delta\",\n         \"args\": \"{\\\"location\"\n       }\n     }\n\n   Subsequent ``content_block_delta`` events add more text/arguments to the message.\n\n4. **Content block stop**\n\n   * Event type: ``content_block_stop``\n\n   * Data: Marks the end of a content block.\n\n   .. code-block:: json\n\n      {\n        \"type\": \"content_block_stop\",\n        \"index\": 0\n      }\n\n5. **Message delta**\n\n   * Event type: ``message_delta``\n\n   * Data: Provides final message-level updates like the stop reason and final usage statistics.\n\n   .. code-block:: json\n\n      {\n        \"type\": \"message_delta\",\n        \"delta\": {\n          \"stop_reason\": \"stop\"\n        },\n        \"usage\": { \"prompt_tokens\": 10 }\n      }\n\n\n6. **Message stop**\n\n   * Event type: ``message_stop``\n\n   * Data: Marks the end of the message.\n\n   .. code-block:: json\n\n      {\"type\": \"message_stop\"}\n\nEach event is sent as a separate SSE message, formatted as shown above. The\nconnection is closed after all events are sent, signaling the end of the\nstream.\n\n**Example SSE response**\n\n.. code-block:: text\n    :class: collapsible\n\n    event: message_start\n    data: {\"type\": \"message_start\", \"message\": {\"id\": \"chatcmpl-9MzuQiF0SxUjFLRjIdT3mTVaMWwiv\", \"role\": \"assistant\", \"model\": \"gpt-4-0125-preview\", \"usage\": {\"prompt_tokens\": 10}}}\n\n    event: content_block_start\n    data: {\"type\": \"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n    event: content_block_delta\n    data: {\"type\": \"content_block_delta\",\"index\":0,\"delta\":{\"type\": \"text_delta\", \"text\": \"The\"}, \"logprobs\": null}\n\n    event: content_block_delta\n    data: {\"type\": \"content_block_delta\",\"index\":0,\"delta\":{\"type\": \"text_delta\", \"text\": \" skies\"}, \"logprobs\": null}\n\n    event: content_block_delta\n    data: {\"type\": \"content_block_delta\",\"index\":0,\"delta\":{\"type\": \"text_delta\", \"text\": \" on\"}, \"logprobs\": null}\n\n    event: content_block_delta\n    data: {\"type\": \"content_block_delta\",\"index\":0,\"delta\":{\"type\": \"text_delta\", \"text\": \" Mars\"}, \"logprobs\": null}\n\n    event: content_block_delta\n    data: {\"type\": \"content_block_delta\",\"index\":0,\"delta\":{\"type\": \"text_delta\", \"text\": \" are\"}, \"logprobs\": null}\n\n    event: content_block_delta\n    data: {\"type\": \"content_block_delta\",\"index\":0,\"delta\":{\"type\": \"text_delta\", \"text\": \" red\"}, \"logprobs\": null}\n\n    event: content_block_delta\n    data: {\"type\": \"content_block_delta\",\"index\":0,\"delta\":{\"type\": \"text_delta\", \"text\": \".\"}, \"logprobs\": null}\n\n    event: content_block_stop\n    data: {\"type\": \"content_block_stop\",\"index\":0}\n\n    event: message_delta\n    data: {\"type\": \"message_delta\", \"delta\": {\"stop_reason\": \"stop\"}, \"usage\": {\"completion_tokens\": 7, \"total_tokens\": 17}}\n\n    event: message_stop\n    data: {\"type\": \"message_stop\"}\n\n\n"
  },
  {
    "path": "docs/reference/ai/index.rst",
    "content": ".. _ref_ai_overview:\n\n==\nAI\n==\n\n:edb-alt-title: Using Gel AI\n\n.. toctree::\n    :hidden:\n    :maxdepth: 3\n\n    extai\n    http\n    python\n    javascript\n\n|Gel| AI is a set of tools designed to enable you to ship AI-enabled apps with\npractically no effort.\n\n1. ``ext::ai``: this Gel extension automatically generates embeddings for your\n   data. Works with OpenAI, Mistral AI, Anthropic, and any other provider with a\n   compatible API.\n\n2. Python library: ``gel.ai``. Access all Gel AI features straight from your\n   Python application.\n\n3. JavaScript library: ``@gel/ai``. Access all Gel AI features right from your\n   JavaScript backend application.\n\n..\n   2. ``ext::vectorstore``: this extension is designed to replicate workflows that\n   might be familiar to you from vectorstore-style databases. Powered by\n   ``pgvector``, it allows you to store and search for embedding vectors, and\n   integrates with popular AI frameworks.\n\n"
  },
  {
    "path": "docs/reference/ai/javascript.rst",
    "content": ".. _ref_ai_javascript_reference:\n\n==============\nJavaScript API\n==============\n\n``@gel/ai`` is a wrapper around the :ref:`AI extension\n<ref_ai_extai_reference>` in |Gel|.\n\n.. tabs::\n\n    .. code-tab:: bash\n        :caption: npm\n\n        $ npm install @gel/ai\n\n    .. code-tab:: bash\n        :caption: yarn\n\n        $ yarn add @gel/ai\n\n    .. code-tab:: bash\n        :caption: pnpm\n\n        $ pnpm add @gel/ai\n\n    .. code-tab:: bash\n        :caption: bun\n\n        $ bun add @gel/ai\n\n\nOverview\n========\n\nThe AI package is built on top of the regular |Gel| client objects.\n\n**Example**:\n\n.. code-block:: typescript\n\n    import { createClient } from \"gel\";\n    import { createRAGClient } from \"@gel/ai\";\n\n\n    const client = createClient();\n\n    const gpt4Ai = createRAGClient(client, {\n      model: \"gpt-4-turbo-preview\",\n    });\n\n    const astronomyAi = gpt4Ai.withContext({\n      query: \"Astronomy\"\n    });\n\n    console.log(\n      await astronomyAi.queryRag(\"What color is the sky on Mars?\")\n    );\n\n\nFactory functions\n=================\n\n.. js:function:: createRAGClient( \\\n                   client: Client, \\\n                   options: Partial<RAGOptions> = {} \\\n                 ): RAGClient\n\n    Creates an instance of ``RAGClient`` with the specified client and options.\n\n    :param client:\n        A |Gel| client instance.\n\n    :param string options.model:\n        Required. Specifies the AI model to use. This could be a version of GPT\n        or any other model supported by |Gel| AI. It is possible to specify the\n        model name as a URI, eg. ``openai:gpt-5``.  See::ref:`text generation\n        models<ref_ai_extai_reference_text_generation_models>`.\n\n    :param options.prompt:\n        Optional. Defines the input prompt for the AI model. The prompt can be\n        a simple string, an ID referencing a stored prompt, or a custom prompt\n        structure that includes roles and content for more complex\n        interactions. The default is the built-in system prompt.\n\n\nCore classes\n============\n\n\n.. js:class:: RAGClient\n\n    Instances of ``RAGClient`` offer methods for client configuration and utilizing RAG.\n\n    :ivar client:\n        An instance of |Gel| client.\n\n    .. js:method:: withConfig(options: Partial<RAGOptions>): RAGClient\n\n        Returns a new ``RAGClient`` instance with updated configuration options.\n\n        :param string options.model:\n            Required. Specifies the AI model to use. This could be a version of GPT\n            or any other model supported by |Gel| AI.\n\n        :param options.prompt:\n            Optional. Defines the input prompt for the AI model. The prompt can be\n            a simple string, an ID referencing a stored prompt, or a custom prompt\n            structure that includes roles and content for more complex\n            interactions. The default is the built-in system prompt.\n\n    .. js:method:: withContext(context: Partial<QueryContext>): RAGClient\n\n        Returns a new ``RAGClient`` instance with an updated query context.\n\n        :param string context.query:\n            Required. Specifies an expression to determine the relevant objects and\n            index to serve as context for text generation. You may set this to any\n            expression that produces a set of objects, even if it is not a\n            standalone query.\n        :param string context.variables:\n            Optional. Variable settings required for the context query.\n        :param string context.globals:\n            Optional. Variable settings required for the context query.\n        :param number context.max_object_count:\n            Optional. A maximum number of objects to return from the context query.\n\n    .. js:method:: async queryRag( \\\n                    message: string, \\\n                    context: QueryContext = this.context \\\n                    ): Promise<string>\n\n        Sends a query with context to the configured AI model and returns the\n        response as a string.\n\n        :param string message:\n            Required. The message to be sent to the text generation provider's API.\n        :param string context.query:\n            Required. Specifies an expression to determine the relevant objects and\n            index to serve as context for text generation. You may set this to any\n            expression that produces a set of objects, even if it is not a\n            standalone query.\n        :param string context.variables:\n            Optional. Variable settings required for the context query.\n        :param string context.globals:\n            Optional. Variable settings required for the context query.\n        :param number context.max_object_count:\n            Optional. A maximum number of objects to return from the context query.\n\n    .. js:method:: async streamRag( \\\n                    message: string, \\\n                    context: QueryContext = this.context \\\n                    ): AsyncIterable<StreamingMessage> & PromiseLike<Response>\n\n        Can be used in two ways:\n\n        - as **an async iterator** - if you want to process streaming data in\n            real-time as it arrives, ideal for handling long-running streams.\n\n        - as **a Promise that resolves to a full Response object** - you have\n            complete control over how you want to handle the stream, this might be\n            useful when you want to manipulate the raw stream or parse it in a custom way.\n\n        :param string message:\n            Required. The message to be sent to the text generation provider's API.\n        :param string context.query:\n            Required. Specifies an expression to determine the relevant objects and\n            index to serve as context for text generation. You may set this to any\n            expression that produces a set of objects, even if it is not a\n            standalone query.\n        :param string context.variables:\n            Optional. Variable settings required for the context query.\n        :param string context.globals:\n            Optional. Variable settings required for the context query.\n        :param number context.max_object_count:\n            Optional. A maximum number of objects to return from the context query.\n\n    .. js:method:: async generateEmbeddings( \\\n                    inputs: string[], \\\n                    model: string \\\n                    ): Promise<number[]>\n\n        Generates embeddings for the array of strings.\n\n        :param string[] inputs:\n            Required. Strings array to generate embeddings for.\n        :param string model:\n            Required. Specifies the AI model to use.\n"
  },
  {
    "path": "docs/reference/ai/python.rst",
    "content": ".. _ref_ai_python_reference:\n\n==========\nPython API\n==========\n\nThe ``gel.ai`` package is an optional binding of the :ref:`AI extension\n<ref_ai_extai_reference>` in |Gel|.\n\n.. code-block:: bash\n\n  $ pip install 'gel[ai]'\n\n\nBlocking and async API\n======================\n\nThe AI binding is built on top of the regular |Gel| client objects, providing\nboth blocking and asynchronous versions of its API.\n\n**Blocking client example**:\n\n.. code-block:: python\n\n    import gel\n    import gel.ai\n\n    client = gel.create_client()\n    gpt4ai = gel.ai.create_rag_client(\n        client,\n        model=\"gpt-4-turbo-preview\"\n    )\n\n    astronomy_ai = gpt4ai.with_context(\n        query=\"Astronomy\"\n    )\n\n    print(\n        astronomy_ai.query_rag(\"What color is the sky on Mars?\")\n    );\n\n    for data in astronomy_ai.stream_rag(\"What color is the sky on Mars?\"):\n        print(data)\n\n\n**Async client example**:\n\n.. code-block:: python\n\n    import gel\n    import gel.ai\n    import asyncio\n\n    client = gel.create_async_client()\n\n    async def main():\n        gpt4ai = await gel.ai.create_async_rag_client(\n            client,\n            model=\"gpt-4-turbo-preview\"\n        )\n        astronomy_ai = gpt4ai.with_context(\n            query=\"Astronomy\"\n        )\n        query = \"What color is the sky on Mars?\"\n        print(\n            await astronomy_ai.query_rag(query)\n        );\n\n        #or streamed\n        async for data in blog_ai.stream_rag(query):\n            print(data)\n\n    asyncio.run(main())\n\n\nFactory functions\n=================\n\n.. py:function:: create_rag_client(client, **kwargs) -> RAGClient\n\n   Creates an instance of ``RAGClient`` with the specified client and options.\n\n   This function ensures that the client is connected before initializing the\n   AI with the specified options.\n\n   :param client:\n       A |Gel| client instance.\n\n   :param kwargs:\n       Keyword arguments that are passed to the ``RAGOptions`` data class to\n       configure AI-specific options. These options are:\n\n       * ``model``: The name of the model to be used. (required)\n       * ``prompt``: An optional prompt to guide the model's behavior.\n         ``None`` will result in the client using the default prompt.\n         (default: ``None``)\n\n.. py:function:: create_async_rag_client(client, **kwargs) -> AsyncRAGClient\n\n   Creates an instance of ``AsyncRAGClient`` w/ the specified client & options.\n\n   This function ensures that the client is connected asynchronously before\n   initializing the AI with the specified options.\n\n   :param client:\n       An asynchronous |Gel| client instance.\n\n   :param kwargs:\n       Keyword arguments that are passed to the ``RAGOptions`` data class to\n       configure AI-specific options. These options are:\n\n       * ``model``: The name of the model to be used. It is possible to specify\n         the model name as a URI, eg. ``openai:gpt-5``.  See: :ref:`text\n         generation models <ref_ai_extai_reference_text_generation_models>`.\n         (required)\n       * ``prompt``: An optional prompt to guide the model's behavior. (default: None)\n\n\nCore classes\n============\n\n.. py:class:: BaseRAGClient\n\n   The base class for |Gel| AI clients.\n\n   This class handles the initialization and configuration of AI clients and\n   provides methods to modify their configuration and context dynamically.\n\n   Both the blocking and async AI client classes inherit from this one, so\n   these methods are available on an AI client of either type.\n\n   :ivar options:\n       An instance of :py:class:`RAGOptions`, storing the RAG options.\n\n   :ivar context:\n       An instance of :py:class:`QueryContext`, storing the context for AI\n       queries.\n\n   :ivar client_cls:\n       A placeholder for the client class, should be implemented by subclasses.\n\n   :param client:\n       An instance of |Gel| client, which could be either a synchronous or\n       asynchronous client.\n\n   :param options:\n       AI options to be used with the client.\n\n   :param kwargs:\n       Keyword arguments to initialize the query context.\n\n.. py:method:: with_config(**kwargs)\n\n   Creates a new instance of the same class with modified configuration\n   options. This method uses the current instance's configuration as a base and\n   applies the changes specified in ``kwargs``.\n\n   :param kwargs:\n       Keyword arguments that specify the changes to the AI configuration.\n       These changes are passed to the ``derive`` method of the current\n       configuration options object. Possible keywords include:\n\n       * ``model``: Specifies the AI model to be used. This must be a string.\n       * ``prompt``: An optional prompt to guide the model's behavior. This is\n         optional and defaults to None.\n\n.. py:method:: with_context(**kwargs)\n\n   Creates a new instance of the same class with a modified context. This\n   method preserves the current AI options and client settings, but uses the\n   modified context specified by ``kwargs``.\n\n   :param kwargs:\n       Keyword arguments that specify the changes to the context. These changes\n       are passed to the ``derive`` method of the current context object.\n       Possible keywords include:\n\n       * ``query``: The database query string.\n       * ``variables``: A dictionary of variables used in the query.\n       * ``globals``: A dictionary of global settings affecting the query.\n       * ``max_object_count``: An optional integer to limit the number of\n         objects returned by the query.\n\n\n.. py:class:: RAGClient\n\n   A synchronous class for creating |Gel| AI clients.\n\n   This class provides methods to send queries and receive responses using both\n   blocking and streaming communication modes synchronously.\n\n   :ivar client:\n       An instance of ``httpx.AsyncClient`` used for making HTTP requests\n       asynchronously.\n\n.. py:method:: query_rag(message, context=None) -> str\n\n   Sends a request to the AI provider and returns the response as a string.\n\n   This method uses a blocking HTTP POST request. It raises an HTTP exception\n   if the request fails.\n\n   :param message:\n       The query string to be sent to the AI model.\n   :param context:\n       An optional ``QueryContext`` object to provide additional context for\n       the query. If not provided, uses the default context of this AI client\n       instance.\n\n.. py:method:: stream_rag(message, context=None)\n\n   Opens a connection to the AI provider to stream query responses.\n\n   This method yields data as it is received, utilizing Server-Sent Events\n   (SSE) to handle streaming data. It raises an HTTP exception if the request\n   fails.\n\n   :param message:\n       The query string to be sent to the AI model.\n   :param context:\n       An optional ``QueryContext`` object to provide additional context for\n       the query. If not provided, uses the default context of this AI client\n       instance.\n\n.. py:method:: generate_embeddings(*inputs: str, model: str) -> list[float]\n\n    Generates embeddings for input texts.\n\n    :param inputs:\n        Input texts.\n    :param model:\n        The embedding model to use\n\n\n.. py:class:: AsyncRAGClient\n\n   An asynchronous class for creating |Gel| AI clients.\n\n   This class provides methods to send queries and receive responses using both\n   blocking and streaming communication modes asynchronously.\n\n   :ivar client:\n       An instance of ``httpx.AsyncClient`` used for making HTTP requests\n       asynchronously.\n\n.. py:method:: query_rag(message, context=None) -> str\n   :noindex:\n\n   Sends an async request to the AI provider, returns the response as a string.\n\n   This method is asynchronous and should be awaited. It raises an HTTP\n   exception if the request fails.\n\n   :param message:\n       The query string to be sent to the AI model.\n\n   :param context:\n       An optional ``QueryContext`` object to provide additional context for\n       the query. If not provided, uses the default context of this AI client\n       instance.\n\n.. py:method:: stream_rag(message, context=None)\n   :noindex:\n\n   Opens an async connection to the AI provider to stream query responses.\n\n   This method yields data as it is received, using asynchronous Server-Sent\n   Events (SSE) to handle streaming data. This is an asynchronous generator\n   method and should be used in an async for loop. It raises an HTTP exception\n   if the connection fails.\n\n   :param message:\n       The query string to be sent to the AI model.\n   :param context:\n       An optional ``QueryContext`` object to provide additional context for\n       the query. If not provided, uses the default context of this AI client\n       instance.\n\n.. py:method:: generate_embeddings(*inputs: str, model: str) -> list[float]\n    :noindex:\n\n    Generates embeddings for input texts.\n\n    :param inputs:\n        Input texts.\n    :param model:\n        The embedding model to use\n\n\nConfiguration classes\n=====================\n\n.. py:class:: ChatParticipantRole\n\n   An enumeration of roles used when defining a custom text generation prompt.\n\n   :cvar SYSTEM:\n       Represents a system-level entity or process.\n   :cvar USER:\n       Represents a human user participating in the chat.\n   :cvar ASSISTANT:\n       Represents an AI assistant.\n   :cvar TOOL:\n       Represents a tool or utility used within the chat context.\n\n\n.. py:class:: Custom\n\n   A single message in a custom text generation prompt.\n\n   :ivar role:\n       The role of the chat participant. Must be an instance of\n       :py:class:`ChatParticipantRole`.\n   :ivar content:\n       The content associated with the role, expressed as a string.\n\n\n.. py:class:: Prompt\n\n   The metadata and content of a text generation prompt.\n\n   :ivar name:\n       An optional name identifying the prompt.\n   :ivar id:\n       An optional unique identifier for the prompt.\n   :ivar custom:\n       An optional list of :py:class:`Custom` objects, each providing\n       role-specific content within the prompt.\n\n\n.. py:class:: RAGOptions\n\n   A data class for RAG options, specifying model and prompt settings.\n\n   :ivar model:\n       The name of the AI model.\n   :ivar prompt:\n       An optional :py:class:`Prompt` providing additional guiding information for\n       the model.\n\n   :method derive(kwargs):\n       Creates a new instance of :py:class:`RAGOptions` by merging existing options\n       with provided keyword arguments. Returns a new :py:class:`RAGOptions`\n       instance with updated attributes.\n\n       :param kwargs:\n           Keyword arguments to update the current AI options. Possible\n           keywords include:\n\n           * ``model`` (str): Update the model name.\n           * ``prompt`` (:py:class:`Prompt`): Update or set a new prompt object.\n\n\n.. py:class:: QueryContext\n\n   A data class defining the context for a query to an AI model.\n\n   :ivar query:\n       The base query string.\n   :ivar variables:\n       An optional dictionary of variables used in the query.\n   :ivar globals:\n       An optional dictionary of global settings affecting the query.\n   :ivar max_object_count:\n       An optional integer specifying the maximum number of objects the query\n       should return.\n\n   :method derive(kwargs):\n       Creates a new instance of :py:class:`QueryContext` by merging existing\n       context with provided keyword arguments. Returns a new\n       :py:class:`QueryContext` instance with updated attributes.\n\n       :param kwargs:\n           Keyword arguments to update the current query context. Possible\n           keywords include:\n\n           * ``query`` (str): Update the query string.\n           * ``variables`` (dict): Update or set new variables for the query.\n           * ``globals`` (dict): Update or set new global settings for the query.\n           * ``max_object_count`` (int): Update the limit on the number of objects returned by the query.\n\n\n.. py:class:: RAGRequest\n\n   A data class defining a request to a text generation model.\n\n   :ivar model:\n       The name of the AI model to query.\n   :ivar prompt:\n       An optional :py:class:`Prompt` associated with the request.\n   :ivar context:\n       The :py:class:`QueryContext` defining the query context.\n   :ivar query:\n       The specific query string to be sent to the model.\n   :ivar stream:\n       A boolean indicating whether the response should be streamed (True) or\n       returned in a single response (False).\n\n   :method to_httpx_request():\n       Converts the RAGRequest into a dictionary suitable for making an HTTP\n       request using the httpx library.\n\n"
  },
  {
    "path": "docs/reference/ai/vectorstore_python.rst",
    "content": ":orphan:\n\n.. _ref_ai_vectorstore_python:\n\n======================\nVectorstore Python API\n======================\n\n\nCore Classes\n============\n\n.. py:class:: GelVectorstore\n\n    A framework-agnostic interface for interacting with |Gel's| ext::vectorstore.\n\n    This class provides methods for storing, retrieving, and searching\n    vector embeddings. It follows vector database conventions and supports\n    different embedding models.\n\n    Args:\n\n    * ``embedding_model`` (:py:class:`BaseEmbeddingModel`): The embedding model used to generate vectors.\n    * ``collection_name`` (str): The name of the collection.\n    * ``record_type`` (str): The schema type (table name) for storing records.\n    * ``client_config`` (dict | None): The config for the |Gel| client.\n\n\n.. py:method:: add_items(self, items: list[InsertItem])\n\n    Add multiple items to the vector store in a single transaction. Embeddings\n    will be generated and stored for all items.\n\n    Args:\n\n    * ``items`` (list[:py:class:`InsertItem`]): List of items to add. Each contains:\n\n      * ``text`` (str): The text content to be embedded\n      * ``metadata`` (dict[str, Any]): Additional data to store\n\n    Returns:\n\n    * List of database record IDs for the inserted items.\n\n\n.. py:method:: add_vectors(self, records: list[InsertRecord])\n\n    Add pre-computed vector embeddings to the store. Use this method when you have\n    already generated embeddings and want to store them directly without re-computing them.\n\n    Args:\n\n    * ``records`` (list[:py:class:`InsertRecord`]): List of records. Each contains:\n\n      * ``embedding`` (list[float]): Pre-computed embeddings\n      * ``text`` (Optional[str]): Original text content\n      * ``metadata`` (dict[str, Any]): Additional data to store\n\n    Returns:\n\n    * List of database record IDs for the inserted items.\n\n.. py:method:: delete(self, ids: list[uuid.UUID])\n\n    Delete records from the vector store by their IDs.\n\n    Args:\n\n    * ``ids`` (list[uuid.UUID]): List of record IDs to delete.\n\n    Returns:\n\n    * List of deleted record IDs.\n\n.. py:method:: get_by_ids(self, ids: list[uuid.UUID]) -> list[Record]\n\n    Retrieve specific records by their IDs.\n\n    Args:\n\n    * ``ids`` (list[uuid.UUID]): List of record IDs to retrieve.\n\n    Returns:\n\n    * List of retrieved records. Each result contains:\n\n      * ``id`` (uuid.UUID): The record's unique identifier\n      * ``text`` (Optional[str]): The original text content\n      * ``embedding`` (Optional[list[float]]): The stored vector embedding\n      * ``metadata`` (Optional[dict[str, Any]]): Any associated metadata\n\n.. py:method:: search_by_item(self, item: Any, filters: Optional[CompositeFilter] = None, limit: Optional[int] = 4) -> list[SearchResult]\n\n    Search for similar items in the vector store.\n\n    This method:\n\n    1. Generates an embedding for the input item\n    2. Finds records with similar embeddings\n    3. Optionally filters results based on metadata\n    4. Returns the most similar items up to the specified limit\n\n    Args:\n\n    * ``item`` (Any): The query item to find similar matches for. Must be compatible with the embedding model's target_type.\n    * ``filters`` (Optional[:py:class:`CompositeFilter`]): Metadata-based filters to use.\n    * ``limit`` (Optional[int]): Max number of results to return. Defaults to 4.\n\n    Returns:\n\n    * List of similar items, ordered by similarity. Each result contains:\n\n      * ``id`` (uuid.UUID): The record's unique identifier\n      * ``text`` (Optional[str]): The original text content\n      * ``embedding`` (list[float]): The stored vector embedding\n      * ``metadata`` (Optional[dict[str, Any]]): Any associated metadata\n      * ``cosine_similarity`` (float): Similarity score (higher is more similar)\n\n.. py:method:: search_by_vector(self, vector: list[float], filter_expression: str = \"\", limit: Optional[int] = 4) -> list[SearchResult]\n\n    Search using a pre-computed vector embedding. Useful when you have already computed\n    the embedding or want to search with a modified/combined embedding vector.\n\n    Args:\n\n    * ``vector`` (list[float]): The query embedding to search with. Must match the dimensionality of stored embeddings.\n    * ``filter_expression`` (str): Filter expression for metadata filtering.\n    * ``limit`` (Optional[int]): Max number of results to return. Defaults to 4.\n\n    Returns:\n\n    * List of similar items, ordered by similarity. Each result contains:\n\n      * ``id`` (uuid.UUID): The record's unique identifier\n      * ``text`` (Optional[str]): The original text content\n      * ``embedding`` (list[float]): The stored vector embedding\n      * ``metadata`` (Optional[dict[str, Any]]): Any associated metadata\n      * ``cosine_similarity`` (float): Similarity score (higher is more similar)\n\n.. py:method:: update_record(self, record: Record) -> Optional[uuid.UUID]\n\n    Update an existing record in the vector store. Only specified fields will be updated.\n    If text is provided but not embedding, a new embedding will be automatically generated.\n\n    Args:\n\n    * ``record`` (:py:class:`Record`):\n\n      * ``id`` (uuid.UUID): The ID of the record to update\n      * ``text`` (Optional[str]): New text content. If provided without embedding, a new embedding will be generated.\n      * ``embedding`` (Optional[list[float]]): New vector embedding.\n      * ``metadata`` (Optional[dict[str, Any]]): New metadata to store with the record. Completely replaces existing metadata.\n\n    Returns:\n\n    * The updated record's ID if found and updated, None if no record was found with the given ID.\n\n    Raises:\n\n    * ValueError: If no fields are specified for update.\n\n\n.. py:class:: BaseEmbeddingModel\n\n    Abstract base class for embedding models.\n    Any embedding model used with :py:class:`GelVectorstore` must implement this\n    interface. The model is expected to convert input data (text, images, etc.)\n    into a numerical vector representation.\n\n    .. py:method:: __call__(self, item) -> list[float]\n\n        Convert an input item into a list of floating-point values (vector\n        embedding). Must be implemented in subclasses.\n\n        Args:\n\n        * ``item``: Input item to be converted to an embedding\n\n        Returns:\n\n        * list[float]: Vector embedding of the input item\n\n    .. py:method:: dimensions(self) -> int\n\n        Return the number of dimensions in the embedding vector.\n        Must be implemented in subclasses.\n\n        Returns:\n\n        * int: Number of dimensions in the embedding vector\n\n    .. py:method:: target_type(self) -> TypeVar\n\n        Return the expected data type of the input (e.g., str for text, image\n        for vision models). Must be implemented in subclasses.\n\n        Returns:\n\n        * TypeVar: Expected input data type\n\n\nData Classes\n============\n\n.. py:class:: InsertItem\n\n    An item whose embedding will be created and stored alongside the item in the vector store.\n\n    Args:\n\n    * ``text`` (str): The text content to be embedded\n    * ``metadata`` (dict[str, Any]): Additional data to store. Defaults to empty dict.\n\n.. py:class:: InsertRecord\n\n    A record to be added to the vector store with embedding pre-computed.\n\n    Args:\n\n    * ``embedding`` (list[float]): Pre-computed embeddings\n    * ``text`` (str | None): Original text content. Defaults to None.\n    * ``metadata`` (dict[str, Any]): Additional data to store. Defaults to empty dict.\n\n.. py:class:: Record\n\n    A record retrieved from the vector store, or an update record.\n    Custom ``__init__`` so we can detect which fields the user passed\n    (even if they pass None or {}).\n\n    Args:\n\n    * ``id`` (uuid.UUID): The record's unique identifier\n    * ``text`` (str | None): The text content. Defaults to None.\n    * ``embedding`` (list[float] | None): The vector embedding. Defaults to None.\n    * ``metadata`` (dict[str, Any]): Additional data stored with the record. Defaults to empty dict.\n\n.. py:class:: SearchResult\n\n    A search result from the vector store.\n\n    Inherits from :py:class:`Record`\n\n    Args:\n\n    * ``cosine_similarity`` (float): Similarity score for the search result. Defaults to 0.0.\n\n\nMetadata Filtering\n==================\n\n.. py:class:: FilterOperator\n\n    Enumeration of supported filter operators for metadata filtering.\n\n    Values:\n\n    * ``EQ``: Equal to (=)\n    * ``NE``: Not equal to (!=)\n    * ``GT``: Greater than (>)\n    * ``LT``: Less than (<)\n    * ``GTE``: Greater than or equal to (>=)\n    * ``LTE``: Less than or equal to (<=)\n    * ``IN``: Value in array\n    * ``NOT_IN``: Value not in array\n    * ``LIKE``: Pattern matching\n    * ``ILIKE``: Case-insensitive pattern matching\n    * ``ANY``: Any array element matches\n    * ``ALL``: All array elements match\n    * ``CONTAINS``: String contains value\n    * ``EXISTS``: Field exists\n\n.. py:class:: FilterCondition\n\n    Enumeration of conditions for combining multiple filters.\n\n    Values:\n\n    * ``AND``: All conditions must be true\n    * ``OR``: Any condition must be true\n\n.. py:class:: MetadataFilter\n\n    Represents a single metadata filter condition.\n\n    Args:\n\n    * ``key`` (str): The metadata field key to filter on\n    * ``value`` (int | float | str): The value to compare against\n    * ``operator`` (:py:class:`FilterOperator`): The comparison operator. Defaults to FilterOperator.EQ.\n\n.. py:class:: CompositeFilter\n\n    Allows grouping multiple MetadataFilter instances using AND/OR conditions.\n\n    Args:\n\n    * ``filters`` (list[:py:class:`CompositeFilter` | :py:class:`MetadataFilter`]): List of filters to combine\n    * ``condition`` (:py:class:`FilterCondition`): How to combine the filters. Defaults to FilterCondition.AND.\n\n.. py:function:: get_filter_clause(filters: CompositeFilter) -> str\n\n    Get the filter clause for a given CompositeFilter.\n\n    Args:\n\n    * ``filters`` (:py:class:`CompositeFilter`): The composite filter to convert to a clause\n\n    Returns:\n\n    * str: The filter clause string for use in queries\n\n    Raises:\n\n    * ValueError: If an unknown operator or condition is encountered\n\n"
  },
  {
    "path": "docs/reference/auth/built_in_ui.rst",
    "content": ".. _ref_guide_auth_built_in_ui:\n\n===========\nBuilt-in UI\n===========\n\n:edb-alt-title: Integrating Gel Auth's built-in UI\n\nTo use the built-in UI for Gel Auth, enable the built-in Auth UI by clicking\nthe \"Enable UI\" button under \"Login UI\" in the configuration section of the\n|Gel| UI. Set these configuration values:\n\n-  ``redirect_to``: Once the authentication flow is complete, Gel will\n   redirect the user's browser back to this URL in your application's\n   backend.\n-  ``redirect_to_on_signup``: If this is a new user, Gel will redirect\n   the user's browser back to this URL in your application's backend.\n-  ``app_name``: Used in the built-in UI to show the user the\n   application's name in a few important places.\n-  ``logo_url``: If provided, will show in the built-in UI as part of the\n   page design.\n-  ``dark_logo_url``: If provided and the user's system has indicated\n   that they prefer a dark UI, this will show instead of ``logo_url`` in\n   the built-in UI as part of the page design.\n-  ``brand_color``: If provided, used in the built-in UI as part of the\n   page design.\n\n\nExample Implementation\n======================\n\nWe will demonstrate the various steps below by building a NodeJS HTTP server in\na single file that we will use to simulate a typical web application.\n\n.. note::\n\n    We are in the process of publishing helper libraries that you can use with\n    popular languages and web frameworks. The details below show the inner\n    workings of how data is exchanged with the Auth extension from a web app\n    using HTTP. You can use this as a guide to integrate with your application\n    written in any language that can send and receive HTTP requests.\n\nWe secure authentication tokens and other sensitive data by using PKCE\n(Proof Key of Code Exchange).\n\nStart the PKCE flow\n-------------------\n\nYour application server creates a 32-byte Base64 URL-encoded string (which will\nbe 43 bytes after encoding), called the ``verifier``. You need to store this\nvalue for the duration of the flow. One way to accomplish this bit of state is\nto use an HttpOnly cookie when the browser makes a request to the server for\nthis value, which you can then use to retrieve it from the cookie store at the\nend of the flow. Take this ``verifier`` string, hash it with SHA256, and then\nbase64url encode the resulting string. This new string is called the\n``challenge``.\n\n.. note::\n\n   Since ``=`` is not a URL-safe character, if your Base64-URL encoding\n   function adds padding, you should remove the padding before hashing the\n   ``verifier`` to derive the ``challenge`` or when providing the ``verifier``\n   or ``challenge`` in your requests.\n\n.. note::\n\n   If you are familiar with PKCE, you will notice some differences from how RFC\n   7636 defines PKCE. Our authentication flow is not an OAuth flow, but rather a\n   strict server-to-server flow with Proof Key of Code Exchange added for\n   additional security to avoid leaking the authentication token. Here are some\n   differences between PKCE as defined in RFC 7636 and our implementation:\n\n   - We do not support the ``plain`` value for ``code_challenge_method``, and\n     therefore do not read that value if provided in requests.\n   - Our parameters omit the ``code_`` prefix, however we do support\n     ``code_challenge`` and ``code_verifier`` as aliases, preferring\n     ``challenge`` and ``verifier`` if present.\n\n.. code-block:: javascript\n\n   import http from \"node:http\";\n   import { URL } from \"node:url\";\n   import crypto from \"node:crypto\";\n\n   /**\n    * You can get this value by running `gel instance credentials`.\n    * Value should be:\n    * `${protocol}://${host}:${port}/branch/${branch}/ext/auth/\n    */\n   const GEL_AUTH_BASE_URL = process.env.GEL_AUTH_BASE_URL;\n   const SERVER_PORT = 3000;\n\n   /**\n    * Generate a random Base64 url-encoded string, and derive a \"challenge\"\n    * string from that string to use as proof that the request for a token\n    * later is made from the same user agent that made the original request\n    *\n    * @returns {Object} The verifier and challenge strings\n    */\n   const generatePKCE = () => {\n      const verifier = crypto.randomBytes(32).toString(\"base64url\");\n\n      const challenge = crypto\n         .createHash(\"sha256\")\n         .update(verifier)\n         .digest(\"base64url\");\n\n      return { verifier, challenge };\n   };\n\n\n.. note::\n\n    For |EdgeDB| versions before 5.0, the value for :gelenv:`AUTH_BASE_URL`\n    in the above snippet should have the form:\n\n    ``${protocol}://${host}:${port}/db/${database}/ext/auth/``\n\n\nLink to built-in UI\n-------------------\n\nNext, provide a link to your web application to either the ``/auth/ui/signin``\nor ``auth/ui/signup``. Those routes will generate the ``verifier`` and\n``challenge`` strings, save the ``verifier`` in a cookie and redirect the user\nto the built-in UI with the ``challenge`` in the search parameters.\n\n.. lint-off\n\n.. code-block:: javascript\n\n   /**\n    * In Node, the `req.url` is only the `pathname` portion of a URL. In\n    * order to generate a full URL, we need to build the protocol and host\n    * from other parts of the request.\n    *\n    * One reason we like to use `URL` objects here is to easily parse the\n    * `URLSearchParams` from the request, and rather than do more error\n    * prone string manipulation, we build a `URL`.\n    *\n    * @param {Request} req\n    * @returns {URL}\n    */\n   const getRequestUrl = (req) => {\n      const protocol = req.connection.encrypted ? \"https\" : \"http\";\n      return new URL(req.url, `${protocol}://${req.headers.host}`);\n   };\n\n   const server = http.createServer(async (req, res) => {\n      const requestUrl = getRequestUrl(req);\n\n      switch (requestUrl.pathname) {\n         case \"/auth/ui/signin\": {\n            await handleUiSignIn(req, res);\n            break;\n         }\n\n         case \"/auth/ui/signup\": {\n            await handleUiSignUp(req, res);\n            break;\n         }\n\n         case \"/auth/callback\": {\n            await handleCallback(req, res);\n            break;\n         }\n\n         default: {\n            res.writeHead(404);\n            res.end(\"Not found\");\n            break;\n         }\n      }\n   });\n\n   /**\n    * Redirects browser requests to Gel Auth UI sign in page with the\n    * PKCE challenge, and saves PKCE verifier in an HttpOnly cookie.\n    *\n    * @param {Request} req\n    * @param {Response} res\n    */\n   const handleUiSignIn = async (req, res) => {\n      const { verifier, challenge } = generatePKCE();\n\n      const redirectUrl = new URL(\"ui/signin\", GEL_AUTH_BASE_URL);\n      redirectUrl.searchParams.set(\"challenge\", challenge);\n\n      res.writeHead(301, {\n         \"Set-Cookie\": `gel-pkce-verifier=${verifier}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n         Location: redirectUrl.href,\n      });\n      res.end();\n   };\n\n   /**\n    * Redirects browser requests to Gel Auth UI sign up page with the\n    * PKCE challenge, and saves PKCE verifier in an HttpOnly cookie.\n    *\n    * @param {Request} req\n    * @param {Response} res\n    */\n   const handleUiSignUp = async (req, res) => {\n      const { verifier, challenge } = generatePKCE();\n\n      const redirectUrl = new URL(\"ui/signup\", GEL_AUTH_BASE_URL);\n      redirectUrl.searchParams.set(\"challenge\", challenge);\n\n      res.writeHead(301, {\n         \"Set-Cookie\": `gel-pkce-verifier=${verifier}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n         Location: redirectUrl.href,\n      });\n      res.end();\n   };\n\n   server.listen(SERVER_PORT, () => {\n      console.log(`HTTP server listening on port ${SERVER_PORT}...`);\n   });\n\n\n.. lint-on\n\n\nRetrieve ``auth_token``\n-----------------------\n\nAt the very end of the flow, the Gel server will redirect the user's browser\nto the ``redirect_to`` address with a single query parameter: ``code``. This\nroute should be a server route that has access to the ``verifier``. You then\ntake that ``code`` and look up the ``verifier`` in the ``gel-pkce-verifier``\ncookie (``gel-pkce-verifier`` with |EdgeDB| <= 5), and make a request\nto the Gel Auth extension to exchange these two pieces of data for an\n``auth_token``.\n\n.. lint-off\n\n.. code-block:: javascript\n\n   /**\n    * Handles the PKCE callback and exchanges the `code` and `verifier\n    * for an auth_token, setting the auth_token as an HttpOnly cookie.\n    *\n    * @param {Request} req\n    * @param {Response} res\n    */\n   const handleCallback = async (req, res) => {\n      const requestUrl = getRequestUrl(req);\n\n      const code = requestUrl.searchParams.get(\"code\");\n      if (!code) {\n         const error = requestUrl.searchParams.get(\"error\");\n         res.status = 400;\n         res.end(\n            `OAuth callback is missing 'code'. \\\n   OAuth provider responded with error: ${error}`,\n         );\n         return;\n      }\n\n      const cookies = req.headers.cookie?.split(\"; \");\n      const verifier = cookies\n         ?.find((cookie) => cookie.startsWith(\"gel-pkce-verifier=\"))\n         ?.split(\"=\")[1];\n      if (!verifier) {\n         res.status = 400;\n         res.end(\n            `Could not find 'verifier' in the cookie store. Is this the \\\n   same user agent/browser that started the authorization flow?`,\n         );\n         return;\n      }\n\n      const codeExchangeUrl = new URL(\"token\", GEL_AUTH_BASE_URL);\n      codeExchangeUrl.searchParams.set(\"code\", code);\n      codeExchangeUrl.searchParams.set(\"verifier\", verifier);\n      const codeExchangeResponse = await fetch(codeExchangeUrl.href, {\n         method: \"GET\",\n      });\n\n      if (!codeExchangeResponse.ok) {\n         const text = await codeExchangeResponse.text();\n         res.status = 400;\n         res.end(`Error from the auth server: ${text}`);\n         return;\n      }\n\n      const { auth_token } = await codeExchangeResponse.json();\n      res.writeHead(204, {\n         \"Set-Cookie\": `gel-auth-token=${auth_token}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n      });\n      res.end();\n   };\n\n\n.. lint-on\n\n:ref:`Back to the Gel Auth guide <ref_guide_auth>`\n"
  },
  {
    "path": "docs/reference/auth/email_password.rst",
    "content": ".. _ref_guide_auth_email_password:\n\n==================\nEmail and password\n==================\n\n:edb-alt-title: Integrating Gel Auth's email and password provider\n\nAlong with using the :ref:`built-in UI <ref_guide_auth_built_in_ui>`, you can also\ncreate your own UI that calls to your own web application backend.\n\nUI considerations\n=================\n\nSimilar to how the built-in UI works, you can query the database configuration\nto discover which providers are configured and dynamically build the UI.\n\n.. code-block:: edgeql\n\n  select cfg::Config.extensions[is ext::auth::AuthConfig].providers {\n      name,\n      [is ext::auth::OAuthProviderConfig].display_name,\n  };\n\nThe ``name`` is a unique string that identifies the Identity Provider. OAuth\nproviders also have a ``display_name`` that you can use as a label for links or\nbuttons. In later steps, you'll be providing this ``name`` as the ``provider``\nin various endpoints.\n\n\nExample implementation\n======================\n\nWe will demonstrate the various steps below by building a NodeJS HTTP server in\na single file that we will use to simulate a typical web application. For this example,\nwe will require email verification to demonstrate the full flow, but you can\nconfigure your provider to not require verification by setting the\n``require_verification`` setting to ``false``.\n\n.. note::\n\n    The details below show the inner workings of how data is exchanged with the\n    Auth extension from a web app using HTTP. You can use this as a guide to\n    integrate with your application written in any language that can send and\n    receive HTTP requests.\n\n\nStart the PKCE flow\n-------------------\n\nWe secure authentication tokens and other sensitive data by using PKCE\n(Proof Key of Code Exchange).\n\nYour application server creates a 32-byte Base64 URL-encoded string (which will\nbe 43 bytes after encoding), called the ``verifier``. You need to store this\nvalue for the duration of the flow. One way to accomplish this bit of state is\nto use an HttpOnly cookie when the browser makes a request to the server for\nthis value, which you can then use to retrieve it from the cookie store at the\nend of the flow. Take this ``verifier`` string, hash it with SHA256, and then\nbase64url encode the resulting string. This new string is called the\n``challenge``.\n\n.. note::\n\n   Since ``=`` is not a URL-safe character, if your Base64-URL encoding\n   function adds padding, you should remove the padding before hashing the\n   ``verifier`` to derive the ``challenge`` or when providing the ``verifier``\n   or ``challenge`` in your requests.\n\n.. note::\n\n   If you are familiar with PKCE, you will notice some differences from how RFC\n   7636 defines PKCE. Our authentication flow is not an OAuth flow, but rather a\n   strict server-to-server flow with Proof Key of Code Exchange added for\n   additional security to avoid leaking the authentication token. Here are some\n   differences between PKCE as defined in RFC 7636 and our implementation:\n\n   - We do not support the ``plain`` value for ``code_challenge_method``, and\n     therefore do not read that value if provided in requests.\n   - Our parameters omit the ``code_`` prefix, however we do support\n     ``code_challenge`` and ``code_verifier`` as aliases, preferring\n     ``challenge`` and ``verifier`` if present.\n\n.. lint-off\n\n.. code-block:: javascript\n\n   import http from \"node:http\";\n   import { URL } from \"node:url\";\n   import crypto from \"node:crypto\";\n\n   /**\n    * You can get this value by running `gel instance credentials`.\n    * Value should be:\n    * `${protocol}://${host}:${port}/branch/${branch}/ext/auth/\n    */\n   const GEL_AUTH_BASE_URL = process.env.GEL_AUTH_BASE_URL;\n   const SERVER_PORT = 3000;\n\n   /**\n    * Generate a random Base64 url-encoded string, and derive a \"challenge\"\n    * string from that string to use as proof that the request for a token\n    * later is made from the same user agent that made the original request\n    *\n    * @returns {Object} The verifier and challenge strings\n    */\n   const generatePKCE = () => {\n      const verifier = crypto.randomBytes(32).toString(\"base64url\");\n\n      const challenge = crypto\n         .createHash(\"sha256\")\n         .update(verifier)\n         .digest(\"base64url\");\n\n      return { verifier, challenge };\n   };\n\n.. lint-on\n\n.. note::\n\n    For |EdgeDB| versions before 5.0, the value for :gelenv:`AUTH_BASE_URL`\n    in the above snippet should have the form:\n\n    ``${protocol}://${host}:${port}/db/${database}/ext/auth/``\n\n\nSign-in and sign-up\n-------------------\n\nNext, we implement routes that handle registering a new user and authenticating\nan existing user.\n\n.. lint-off\n\n.. code-block:: javascript\n\n   const server = http.createServer(async (req, res) => {\n     const requestUrl = getRequestUrl(req);\n\n     switch (requestUrl.pathname) {\n       case \"/auth/signup\": {\n         await handleSignUp(req, res);\n         break;\n       }\n\n       case \"/auth/signin\": {\n         await handleSignIn(req, res);\n         break;\n       }\n\n       case \"/auth/verify\": {\n         await handleVerify(req, res);\n         break;\n       }\n\n       case \"/auth/send-password-reset-email\": {\n         await handleSendPasswordResetEmail(req, res);\n         break;\n       }\n\n       case \"/auth/ui/reset-password\": {\n         await handleUiResetPassword(req, res);\n         break;\n       }\n\n       case \"/auth/reset-password\": {\n         await handleResetPassword(req, res);\n         break;\n       }\n\n       default: {\n         res.writeHead(404);\n         res.end(\"Not found\");\n         break;\n       }\n     }\n   });\n\n   /**\n    * Handles sign up with email and password.\n    *\n    * @param {Request} req\n    * @param {Response} res\n    */\n   const handleSignUp = async (req, res) => {\n     let body = \"\";\n     req.on(\"data\", (chunk) => {\n       body += chunk.toString();\n     });\n     req.on(\"end\", async () => {\n       const pkce = generatePKCE();\n       const { email, password, provider } = JSON.parse(body);\n       if (!email || !password || !provider) {\n         res.status = 400;\n         res.end(\n           `Request body malformed. Expected JSON body with 'email', 'password', and 'provider' keys, but got: ${body}`,\n         );\n         return;\n       }\n\n       const registerUrl = new URL(\"register\", GEL_AUTH_BASE_URL);\n       const registerResponse = await fetch(registerUrl.href, {\n         method: \"post\",\n         headers: {\n           \"Content-Type\": \"application/json\",\n         },\n         body: JSON.stringify({\n           challenge: pkce.challenge,\n           email,\n           password,\n           provider,\n           verify_url: `http://localhost:${SERVER_PORT}/auth/verify`,\n         }),\n       });\n\n       if (!registerResponse.ok) {\n         const text = await registerResponse.text();\n         res.status = 400;\n         res.end(`Error from the auth server: ${text}`);\n         return;\n       }\n\n       const registerJson = await registerResponse.json();\n\n       if (\"code\" in registerJson) {\n         // No verification required, we can immediately get an auth token\n         const tokenUrl = new URL(\"token\", GEL_AUTH_BASE_URL);\n         tokenUrl.searchParams.set(\"code\", registerJson.code);\n         tokenUrl.searchParams.set(\"verifier\", pkce.verifier);\n         const tokenResponse = await fetch(tokenUrl.href, {\n           method: \"get\",\n         });\n\n         if (!tokenResponse.ok) {\n           const text = await tokenResponse.text();\n           res.status = 400;\n           res.end(`Error from the auth server: ${text}`);\n           return;\n         }\n\n         const { auth_token } = await tokenResponse.json();\n         res.writeHead(204, {\n           \"Set-Cookie\": `gel-auth-token=${auth_token}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n         });\n\n         res.end();\n       } else {\n         // Verification required, we need to render a notice to the user\n         // to check their email for a verification link\n         res.writeHead(200, { \"Content-Type\": \"text/html\" });\n         res.end(`\n           <html>\n             <body>\n               <p>Please check your email for a verification link.</p>\n             </body>\n           </html>\n         `);\n       }\n     });\n   };\n\n   /**\n    * Handles sign in with email and password.\n    *\n    * @param {Request} req\n    * @param {Response} res\n    */\n   const handleSignIn = async (req, res) => {\n     let body = \"\";\n     req.on(\"data\", (chunk) => {\n       body += chunk.toString();\n     });\n     req.on(\"end\", async () => {\n       const pkce = generatePKCE();\n       const { email, password, provider } = JSON.parse(body);\n       if (!email || !password || !provider) {\n         res.status = 400;\n         res.end(\n           `Request body malformed. Expected JSON body with 'email', 'password', and 'provider' keys, but got: ${body}`,\n         );\n         return;\n       }\n\n       const authenticateUrl = new URL(\"authenticate\", GEL_AUTH_BASE_URL);\n       const authenticateResponse = await fetch(authenticateUrl.href, {\n         method: \"post\",\n         headers: {\n           \"Content-Type\": \"application/json\",\n         },\n         body: JSON.stringify({\n           challenge: pkce.challenge,\n           email,\n           password,\n           provider,\n         }),\n       });\n\n       if (!authenticateResponse.ok) {\n         const text = await authenticateResponse.text();\n         res.status = 400;\n         res.end(`Error from the auth server: ${text}`);\n         return;\n       }\n\n       const authenticateJson = await authenticateResponse.json();\n\n       if (\"code\" in authenticateJson) {\n         // User is verified, we can get an auth token\n         const tokenUrl = new URL(\"token\", GEL_AUTH_BASE_URL);\n         tokenUrl.searchParams.set(\"code\", authenticateJson.code);\n         tokenUrl.searchParams.set(\"verifier\", pkce.verifier);\n         const tokenResponse = await fetch(tokenUrl.href, {\n           method: \"get\",\n         });\n\n         if (!tokenResponse.ok) {\n           const text = await tokenResponse.text();\n           res.status = 400;\n           res.end(`Error from the auth server: ${text}`);\n           return;\n         }\n\n         const { auth_token } = await tokenResponse.json();\n         res.writeHead(204, {\n           \"Set-Cookie\": `gel-auth-token=${auth_token}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n         });\n         res.end();\n       } else {\n         // Verification required, we need to render a notice to the user\n         // to check their email for a verification link\n         res.writeHead(200, { \"Content-Type\": \"text/html\" });\n         res.end(`\n           <html>\n             <body>\n               <p>Please check your email for a verification link.</p>\n             </body>\n           </html>\n         `);\n       }\n     });\n   };\n\n.. lint-on\n\n\nEmail verification\n------------------\n\nWhen a new user signs up, by default we require them to verify their email\naddress before allowing the application to get an authentication token. To\nhandle the verification flow, we implement an endpoint:\n\n.. note::\n\n   If your Email/Password provider uses the **Code** verification method, the\n   verification email contains a one-time code rather than a link. In that\n   case, prompt the user for the code and call ``POST /verify`` with:\n\n   - **provider**: ``builtin::local_emailpassword``\n   - **email** and **code**\n   - optionally a **challenge** and **redirect_to** to receive a PKCE code or a redirect upon success\n\n   The Link-based example below continues to work when the provider uses the\n   Link method.\n\n.. note::\n\n   💡 If you would like to allow users to still log in, but offer limited access\n   to your application, you can check the associated\n   ``ext::auth::EmailPasswordFactor`` for the ``ext::auth::Identity`` to see if\n   the ``verified_at`` property is some time in the past. You'll need to set\n   the ``require_verification`` setting in the provider configuration to\n   ``false``.\n\n.. lint-off\n\n.. code-block:: javascript\n\n   /**\n    * Handles the link in the email verification flow.\n    *\n    * @param {Request} req\n    * @param {Response} res\n    */\n   const handleVerify = async (req, res) => {\n     const requestUrl = getRequestUrl(req);\n     const verification_token = requestUrl.searchParams.get(\"verification_token\");\n     if (!verification_token) {\n       res.status = 400;\n       res.end(\n         `Verify request is missing 'verification_token' search param. The verification email is malformed.`,\n       );\n       return;\n     }\n\n     const verifyUrl = new URL(\"verify\", GEL_AUTH_BASE_URL);\n     const verifyResponse = await fetch(verifyUrl.href, {\n       method: \"post\",\n       headers: {\n         \"Content-Type\": \"application/json\",\n       },\n       body: JSON.stringify({\n         verification_token,\n         provider: \"builtin::local_emailpassword\",\n       }),\n     });\n\n     if (!verifyResponse.ok) {\n       const text = await verifyResponse.text();\n       res.status = 400;\n       res.end(`Error from the auth server: ${text}`);\n       return;\n     }\n\n     const { code } = await verifyResponse.json();\n\n     const cookies = req.headers.cookie?.split(\"; \");\n     const verifier = cookies\n       ?.find((cookie) => cookie.startsWith(\"gel-pkce-verifier=\"))\n       ?.split(\"=\")[1];\n     if (verifier) {\n       // Email verification flow is continuing from the original\n       // user agent/browser, so we can immediately get an auth token\n       const tokenUrl = new URL(\"token\", GEL_AUTH_BASE_URL);\n       tokenUrl.searchParams.set(\"code\", code);\n       tokenUrl.searchParams.set(\"verifier\", verifier);\n       const tokenResponse = await fetch(tokenUrl.href, {\n         method: \"get\",\n       });\n\n       if (!tokenResponse.ok) {\n         const text = await tokenResponse.text();\n         res.status = 400;\n         res.end(`Error from the auth server: ${text}`);\n         return;\n       }\n\n       const { auth_token } = await tokenResponse.json();\n       res.writeHead(204, {\n         \"Set-Cookie\": `gel-auth-token=${auth_token}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n       });\n       res.end();\n       return;\n     }\n\n     // Email verification flow is continuing from a different user agent/browser,\n     // so we need to render a notice to the user to sign in, which will either\n     // complete the PKCE flow or start a new one\n     res.status = 200;\n     res.end(\n       `\n       <html>\n         <body>\n           <p>Email verified! Please sign in to continue.</p>\n         </body>\n       </html>`,\n     );\n   };\n\n.. lint-on\n\n\nCreate a User object\n--------------------\n\nFor some applications, you may want to create a custom ``User`` type in the\ndefault module to attach application-specific information. You can tie this to\nan ``ext::auth::Identity`` by using the ``identity_id`` returned during the\nsign-up flow.\n\n.. note::\n\n    For this example, we'll assume you have a one-to-one relationship between\n    ``User`` objects and ``ext::auth::Identity`` objects. In your own\n    application, you may instead decide to have a one-to-many relationship.\n\nGiven this ``User`` type:\n\n.. code-block:: sdl\n\n   type User {\n       email: str;\n       name: str;\n\n       required identity: ext::auth::Identity {\n           constraint exclusive;\n       };\n   }\n\nYou can update the ``handleRegister`` function like this to create a new ``User``\nobject:\n\n.. lint-off\n\n.. code-block:: javascript-diff\n\n     const registerJson = await registerResponse.json();\n\n   + if (\"identity_id\" in registerJson) {\n   +   await client.query(`\n   +     with\n   +       identity := <ext::auth::Identity><uuid>$identity_id,\n   +       emailFactor := (\n   +         select ext::auth::EmailFactor filter .identity = identity\n   +       ),\n   +     insert User {\n   +       email := emailFactor.email,\n   +       identity := identity\n   +     };\n   +   `, { identity_id: registerJson.identity_id });\n   + }\n   +\n     if (\"code\" in registerJson) {\n\n.. lint-on\n\n\nPassword reset\n--------------\n\nTo allow users to reset their password, we implement three endpoints. The first\none sends the reset email. The second is the HTML form that is rendered when\nthe user follows the link in their email. And, the final one is the endpoint\nthat updates the password and logs in the user.\n\n.. note::\n\n   If your provider is configured for the **Code** method for password reset,\n   the email will contain a one-time code instead of a reset link/token. In\n   that case:\n\n   - Call ``POST /reset-password`` with **email**, **code**, **password** and\n     optionally **challenge**.\n   - If you include a **challenge**, the response will include a PKCE ``code``\n     that you can exchange at ``POST /token`` to log the user in immediately.\n   - If you omit **challenge**, the response will indicate success without a\n     PKCE code and you should ask the user to sign in.\n\n.. lint-off\n\n.. code-block:: javascript\n\n   /**\n    * Request a password reset for an email.\n    *\n    * @param {Request} req\n    * @param {Response} res\n    */\n   const handleSendPasswordResetEmail = async (req, res) => {\n     let body = \"\";\n     req.on(\"data\", (chunk) => {\n       body += chunk.toString();\n     });\n     req.on(\"end\", async () => {\n       const { email } = JSON.parse(body);\n       const reset_url = `http://localhost:${SERVER_PORT}/auth/ui/reset-password`;\n       const provider = \"builtin::local_emailpassword\";\n       const pkce = generatePKCE();\n\n       const sendResetUrl = new URL(\"send-reset-email\", GEL_AUTH_BASE_URL);\n       const sendResetResponse = await fetch(sendResetUrl.href, {\n         method: \"post\",\n         headers: {\n           \"Content-Type\": \"application/json\",\n         },\n         body: JSON.stringify({\n           email,\n           provider,\n           reset_url,\n           challenge: pkce.challenge,\n         }),\n       });\n\n       if (!sendResetResponse.ok) {\n         const text = await sendResetResponse.text();\n         res.status = 400;\n         res.end(`Error from auth server: ${text}`);\n         return;\n       }\n\n       const { email_sent } = await sendResetResponse.json();\n\n       res.writeHead(200, {\n         \"Set-Cookie\": `gel-pkce-verifier=${pkce.verifier}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n       });\n       res.end(`Reset email sent to '${email_sent}'`);\n     });\n   };\n\n   /**\n    * Render a simple reset password UI\n    *\n    * @param {Request} req\n    * @param {Response} res\n    */\n   const handleUiResetPassword = async (req, res) => {\n     const url = new URL(req.url);\n     const reset_token = url.searchParams.get(\"reset_token\");\n     res.writeHead(200, { \"Content-Type\": \"text/html\" });\n     res.end(`\n       <html>\n         <body>\n           <form method=\"POST\" action=\"http://localhost:${SERVER_PORT}/auth/reset-password\">\n             <input type=\"hidden\" name=\"reset_token\" value=\"${reset_token}\">\n             <label>\n               New password:\n               <input type=\"password\" name=\"password\" required>\n             </label>\n             <button type=\"submit\">Reset Password</button>\n           </form>\n         </body>\n       </html>\n     `);\n   };\n\n   /**\n    * Send new password with reset token to Gel Auth.\n    *\n    * @param {Request} req\n    * @param {Response} res\n    */\n   const handleResetPassword = async (req, res) => {\n     let body = \"\";\n     req.on(\"data\", (chunk) => {\n       body += chunk.toString();\n     });\n     req.on(\"end\", async () => {\n       const { reset_token, password } = JSON.parse(body);\n       if (!reset_token || !password) {\n         res.status = 400;\n         res.end(\n           `Request body malformed. Expected JSON body with 'reset_token' and 'password' keys, but got: ${body}`\n         );\n         return;\n       }\n       const provider = \"builtin::local_emailpassword\";\n       const cookies = req.headers.cookie.split(\"; \");\n       const verifier = cookies\n         .find((cookie) => cookie.startsWith(\"gel-pkce-verifier=\"))\n         .split(\"=\")[1];\n       if (!verifier) {\n         res.status = 400;\n         res.end(\n           `Could not find 'verifier' in the cookie store. Is this the same user agent/browser that started the authorization flow?`\n         );\n         return;\n       }\n       const resetUrl = new URL(\"reset-password\", GEL_AUTH_BASE_URL);\n       const resetResponse = await fetch(resetUrl.href, {\n         method: \"post\",\n         headers: {\n           \"Content-Type\": \"application/json\",\n         },\n         body: JSON.stringify({\n           reset_token,\n           provider,\n           password,\n         }),\n       });\n       if (!resetResponse.ok) {\n         const text = await resetResponse.text();\n         res.status = 400;\n         res.end(`Error from the auth server: ${text}`);\n         return;\n       }\n       const { code } = await resetResponse.json();\n       const tokenUrl = new URL(\"token\", GEL_AUTH_BASE_URL);\n       tokenUrl.searchParams.set(\"code\", code);\n       tokenUrl.searchParams.set(\"verifier\", verifier);\n       const tokenResponse = await fetch(tokenUrl.href, {\n         method: \"get\",\n       });\n       if (!tokenResponse.ok) {\n         const text = await tokenResponse.text();\n         res.status = 400;\n         res.end(`Error from the auth server: ${text}`);\n         return;\n       }\n       const { auth_token } = await tokenResponse.json();\n       res.writeHead(204, {\n         \"Set-Cookie\": `gel-auth-token=${auth_token}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n       });\n       res.end();\n     });\n   };\n\n.. lint-on\n\n:ref:`Back to the Gel Auth guide <ref_guide_auth>`\n"
  },
  {
    "path": "docs/reference/auth/http.rst",
    "content": ".. _ref_auth_http:\n\n========\nHTTP API\n========\n\nYour application server will interact with the Gel extension primarily by sending HTTP requests to the Gel server. This page describes the HTTP API exposed by the Gel server. For more in-depth guidance about integrating Gel Auth into your application, see :ref:`ref_guide_auth` for a reference example.\n\nThe following sections are organized by authentication type.\n\nResponses\n=========\n\nResponses typically include a JSON object that include a ``code`` property that can be exchanged for an access token by providing the matching PKCE verifier associated with the ``code``. Some endpoints can be configured to return responses as redirects and include response data in the redirect location's query string.\n\nGeneral\n=======\n\nPOST /token\n-----------\n\nExchanges a PKCE authorization code (obtained from a successful registration, authentication, or email verification flow that included a PKCE challenge) for a session token.\n\n**Request Parameters (Query String):**\n\n*   ``code`` (string, required): The PKCE authorization code that was previously issued.\n*   ``verifier`` (string, required, also accepts ``code_verifier``): The PKCE code verifier string (plaintext, typically 43-128 characters) that was originally used to generate the ``code_challenge``.\n\n**Response:**\n\n1.  **Successful Token Exchange:**\n\n    *   This occurs if the ``code`` is valid, and the provided ``verifier`` correctly matches the ``challenge`` associated with the ``code``.\n    *   The PKCE ``code`` is consumed and cannot be reused.\n    *   A 200 OK response is returned with a JSON body containing the session token and identity information:\n\n        .. code-block:: json\n\n            {\n              \"auth_token\": \"your_new_session_jwt\",\n              \"identity_id\": \"the_users_identity_id\",\n              \"provider_token\": \"optional_oauth_provider_access_token\",\n              \"provider_refresh_token\": \"optional_oauth_provider_refresh_token\",\n              \"provider_id_token\": \"optional_oauth_provider_id_token\"\n            }\n\n        .. note::\n\n          ``provider_token``, ``provider_refresh_token``, and ``provider_id_token`` are only populated if the PKCE flow originated from an interaction with an external OAuth provider that returned these tokens.\n\n2.  **PKCE Verification Failed:**\n\n    *   The ``code`` was found, but the ``verifier`` did not match the stored challenge.\n    *   An HTTP error response 403 Forbidden with a JSON body indicating ``PKCEVerificationFailed``.\n\n3.  **Unknown Code:**\n\n    *   The provided ``code`` was not found.\n    *   An HTTP error response 403 Forbidden with a JSON body indicating \"NoIdentityFound\".\n\n4.  **Code found, but not associated with an Identity:**\n\n    *   The ``code`` was found, but it is not associated with a user identity.\n    *   An HTTP error response 400 Bad Request with a JSON body indicating \"InvalidData\".\n\n5.  **Invalid Verifier Length:**\n\n    *   The ``verifier`` string is shorter than 43 characters or longer than 128 characters.\n    *   An HTTP 400 Bad Request response with a JSON body detailing the length requirement.\n\n6.  **Missing Parameters:**\n\n    *   Either ``code`` or ``verifier`` (or ``code_verifier``) is missing from the query string.\n    *   An HTTP 400 Bad Request response with a JSON body indicating the missing parameter.\n\nEmail and password\n==================\n\nPOST /register\n--------------\n\nRegister a new user with email and password.\n\n**Request Body (JSON):**\n\n*   ``email`` (string, required): The user's email address.\n*   ``password`` (string, required): The user's desired password.\n*   ``provider`` (string, required): The name of the provider to use: ``builtin::local_emailpassword``\n*   ``challenge`` (string, optional): A PKCE code challenge. This is required if the provider is configured with ``require_verification: false`` since registering will also authenticate and authentication is protected by a PKCE code exchange.\n*   ``redirect_to`` (string, optional): A URL to redirect to upon successful registration.\n*   ``verify_url`` (string, optional): The base URL for the email verification link. If not provided, it defaults to ``<auth_server_base_url>/ui/verify``, the built-in UI endpoint for verifying email addresses. The verification token will be appended as a query parameter to this URL.\n*   ``redirect_on_failure`` (string, optional): A URL to redirect to if registration fails.\n\n.. note::\n\n  The verification email sent after registration depends on your provider's verification method:\n\n  - **Code**: users receive a one-time code and must call ``POST /verify`` with ``provider``, ``email`` and the ``code``.\n  - **Link**: users receive a verification link that carries a ``verification_token`` and must call ``POST /verify`` with ``provider`` and the ``verification_token`` (often done by following the link).\n\n**Response:**\n\nThe behavior of the response depends on the request parameters and server-side provider configuration (specifically, ``require_verification``).\n\n1.  **Successful Registration with Email Verification Required:**\n\n    *   This occurs if the provider has ``require_verification: true``.\n    *   If ``redirect_to`` is provided in the request:\n\n        *   A 302 redirect to the ``redirect_to`` URL occurs.\n        *   The redirect URL will include ``identity_id`` and ``verification_email_sent_at`` as query parameters.\n\n    *   If ``redirect_to`` is NOT provided:\n\n        *   A 201 Created response is returned with a JSON body:\n\n            .. code-block:: json\n\n              {\n                \"identity_id\": \"...\",\n                \"verification_email_sent_at\": \"YYYY-MM-DDTHH:MM:SS.ffffffZ\"\n              }\n\n2.  **Successful Registration with Email Verification NOT Required (PKCE Flow):**\n\n    *   This occurs if the provider has ``require_verification: false``. The ``challenge`` parameter is mandatory in the request.\n    *   If ``redirect_to`` is provided in the request:\n\n        *   A 302 redirect to the ``redirect_to`` URL occurs.\n        *   The redirect URL will include ``code`` (the PKCE authorization code) and ``provider`` as query parameters.\n\n    *   If ``redirect_to`` is NOT provided:\n\n        *   A 201 Created response is returned with a JSON body:\n\n            .. code-block:: json\n\n              {\n                \"code\": \"...\",\n                \"provider\": \"...\"\n              }\n\n3.  **Registration Failure:**\n\n    *   If ``redirect_on_failure`` is provided in the request and is an allowed URL:\n\n        *   A 302 redirect to the ``redirect_on_failure`` URL occurs.\n        *   The redirect URL will include ``error`` (a description of the error) and ``email`` (the submitted email) as query parameters.\n\n    *   Otherwise (no ``redirect_on_failure`` or it's not allowed):\n\n        *   An HTTP error response (e.g., 400 Bad Request, 500 Internal Server Error) is returned with a JSON body describing the error. For example:\n\n            .. code-block:: json\n\n              {\n                \"message\": \"Error description\",\n                \"type\": \"ErrorType\",\n                \"code\": \"ERROR_CODE\"\n              }\n\n**Common Error Scenarios:**\n\n*   Missing ``provider`` in the request.\n*   Missing ``challenge`` in the request when the provider has ``require_verification: false``.\n*   Email already exists.\n*   Invalid password (e.g., too short, if policies are enforced).\n\nPOST /authenticate\n------------------\n\nAuthenticate a user using email and password.\n\n**Request Body (JSON):**\n\n*   ``email`` (string, required): The user's email address.\n*   ``password`` (string, required): The user's password.\n*   ``provider`` (string, required): The name of the provider to use: ``builtin::local_emailpassword``\n*   ``challenge`` (string, required): A PKCE code challenge.\n*   ``redirect_to`` (string, optional): A URL to redirect to upon successful authentication.\n*   ``redirect_on_failure`` (string, optional): A URL to redirect to if authentication fails. If not provided, but ``redirect_to`` is, ``redirect_to`` will be used as the fallback for failure redirection.\n\n**Response:**\n\nThe behavior of the response depends on the request parameters and the outcome of the authentication attempt.\n\n1.  **Successful Authentication:**\n\n    *   A PKCE authorization code is generated and associated with the user's session.\n    *   If ``redirect_to`` is provided in the request:\n\n        *   A 302 redirect to the ``redirect_to`` URL occurs.\n        *   The redirect URL will include a ``code`` (the PKCE authorization code) as a query parameter.\n\n    *   If ``redirect_to`` is NOT provided:\n\n        *   A 200 OK response is returned with a JSON body:\n\n            .. code-block:: json\n\n                {\n                  \"code\": \"...\"\n                }\n\n2.  **Authentication Failure (e.g., invalid credentials, user not found):**\n\n    *   If ``redirect_on_failure`` (or ``redirect_to`` as a fallback) is provided in the request and is an allowed URL:\n\n        *   A 302 redirect to this URL occurs.\n        *   The redirect URL will include ``error`` (a description of the error) and ``email`` (the submitted email) as query parameters.\n\n    *   Otherwise (no applicable redirect URL or it's not allowed):\n\n        *   An HTTP error response (e.g., 400, 401) is returned with a JSON body describing the error. For example:\n\n            .. code-block:: json\n\n                {\n                  \"message\": \"Invalid credentials\",\n                  \"type\": \"InvalidCredentialsError\",\n                  \"code\": \"INVALID_CREDENTIALS\"\n                }\n\n3.  **Email Verification Required:**\n\n    *   This occurs if the provider is configured with ``require_verification: true`` and the user has not yet verified their email address.\n    *   The response follows the same logic as **Authentication Failure**:\n\n        *   If ``redirect_on_failure`` (or ``redirect_to``) is provided, a redirect occurs with an error like \"VerificationRequired\".\n        *   Otherwise, an HTTP error (often 403 Forbidden) is returned with a JSON body indicating that email verification is required.\n\n**Common Error Scenarios:**\n\n*   Missing required fields in the request: ``email``, ``password``, ``provider``, or ``challenge``.\n*   Invalid email or password.\n*   User account does not exist.\n*   User account exists but email is not verified (if ``require_verification: true`` for the provider).\n\nPOST /send-reset-email\n----------------------\n\nSend a password reset email to a user.\n\n**Request Body (JSON):**\n\n*   ``provider`` (string, required): The name of the provider: ``builtin::local_emailpassword``.\n*   ``email`` (string, required): The email address of the user requesting the password reset.\n*   ``reset_url`` (string, required): The base URL for the password reset page (used for the Link method). The ``reset_token`` will be appended as a query parameter. This URL must be an allowed redirect URI in the server configuration.\n*   ``challenge`` (string, required): A PKCE code challenge. For the Link method it is embedded in the ``reset_token``; for the Code method it can be re-used later when completing the reset to obtain a PKCE code.\n*   ``redirect_to`` (string, optional): A URL to redirect to after the reset email has been successfully queued for sending.\n*   ``redirect_on_failure`` (string, optional): A URL to redirect to if there's an error during the process. If not provided, but ``redirect_to`` is, ``redirect_to`` will be used as the fallback for failure redirection.\n\n.. note::\n\n  The email sent depends on your provider's configuration:\n\n  - **Link**: a reset link is sent containing a ``reset_token``; the user should then call ``POST /reset-password`` with this token.\n  - **Code**: a one-time code is sent to the email address; the user should then call ``POST /reset-password`` with ``email`` and ``code`` (and optionally ``challenge`` to receive a PKCE code).\n\n**Response:**\n\nThe endpoint always attempts to respond in a way that does not reveal whether an email address is registered or not.\n\n1.  **Reset Email Queued (or User Not Found):**\n\n    *   If the user exists, a password reset email is generated and sent.\n    *   If the user does not exist, the server simulates a successful send to prevent email enumeration attacks.\n    *   If ``redirect_to`` is provided in the request:\n\n        *   A 302 redirect to the ``redirect_to`` URL occurs.\n        *   The redirect URL will include ``email_sent`` (the email address provided in the request) as a query parameter.\n\n    *   If ``redirect_to`` is NOT provided:\n\n        *   A 200 OK response is returned with a JSON body:\n\n            .. code-block:: json\n\n                {\n                  \"email_sent\": \"user@example.com\"\n                }\n\n2.  **Failure (e.g., ``reset_url`` not allowed, SMTP server error):**\n\n    *   This occurs for errors not related to whether the user exists, such as configuration issues or mail server problems.\n    *   If ``redirect_on_failure`` (or ``redirect_to`` as a fallback) is provided in the request and is an allowed URL:\n\n        *   A 302 redirect to this URL occurs.\n        *   The redirect URL will include ``error`` (a description of the error) and ``email`` (the submitted email) as query parameters.\n\n    *   Otherwise (no applicable redirect URL or it's not allowed):\n\n        *   An HTTP error response (e.g., 400 Bad Request, 500 Internal Server Error) is returned with a JSON body describing the error.\n\n**Common Error Scenarios (leading to the Failure response):**\n\n*   Missing required fields in the request: ``provider``, ``email``, ``reset_url``, or ``challenge``.\n*   The provided ``reset_url`` is not in the server's list of allowed redirect URIs.\n*   Internal server error during email dispatch (e.g., SMTP configuration issues).\n\nPOST /reset-password\n--------------------\n\nResets a user's password using a reset token and a new password. This endpoint completes the password reset flow initiated by ``POST /send-reset-email``.\n\n**Request Body (JSON):**\n\n*   ``provider`` (string, required): The name of the provider: ``builtin::local_emailpassword``.\n*   ``password`` (string, required): The new password for the user's account.\n\nChoose one of the following modes:\n\n-  **Token mode (Link method)**\n\n   *   ``reset_token`` (string, required): The token that was emailed to the user.\n\n-  **Code mode**\n\n   *   ``email`` (string, required): The user's email address.\n   *   ``code`` (string, required): The one-time code sent by email.\n   *   ``challenge`` (string, optional): If provided, a PKCE authorization code will be generated upon success.\n\nOptional for both modes:\n\n*   ``redirect_to`` (string, optional): A URL to redirect to after the password has been successfully reset. If provided and a PKCE code is generated, it will be appended as a query parameter.\n*   ``redirect_on_failure`` (string, optional): A URL to redirect to if the password reset process fails. If not provided, but ``redirect_to`` is, ``redirect_to`` will be used as the fallback.\n\n**Response:**\n\n-  **Token mode (Link method)**\n\n   *   The ``reset_token`` is validated, and the user's password is updated.\n   *   A PKCE authorization ``code`` is generated using the challenge embedded in the token.\n   *   If ``redirect_to`` is provided, a 302 redirect occurs with ``code`` appended; otherwise, a 200 OK JSON response is returned with ``{\"code\": \"...\"}``.\n\n-  **Code mode**\n\n   *   The ``email``/``code`` are validated, and the user's password is updated.\n   *   If a ``challenge`` is provided, a PKCE authorization ``code`` is generated.\n   *   If ``redirect_to`` is provided and a PKCE code was generated, a 302 redirect occurs with ``code`` appended; if ``challenge`` was not provided, a 200 OK JSON response is returned with ``{\"status\": \"password_reset\"}``.\n\n-  **Failure (invalid inputs or server error)**\n\n   *   If ``redirect_on_failure`` (or ``redirect_to`` as a fallback) is provided and is an allowed URL, a 302 redirect occurs with an ``error`` parameter (and submitted ``reset_token``/``email`` where applicable).\n   *   Otherwise, an HTTP error response is returned with a JSON error body (e.g., 400, 403, 500).\n\n**Common Error Scenarios:**\n\n*   Missing required fields in the request: ``provider``, ``reset_token``, or ``password``.\n*   The ``reset_token`` is malformed, has an invalid signature, or is expired.\n*   Internal server error during the password update process.\n\nEmail verification\n==================\n\nThese endpoints apply to the Email and password provider, as well as the WebAuthn provider. Verification emails are sent even if you do not *require* verification. The difference between requiring verification and not is that if you require verification, the user must verify their email before they can authenticate. If you do not require verification, the user can authenticate without verifying their email.\n\nPOST /verify\n------------\n\nVerify a user's email address. Supports both Link and Code methods.\n\n**Request Body (JSON):**\n\n*   ``provider`` (string, required): The provider name, e.g., ``builtin::local_emailpassword`` or ``builtin::local_webauthn``.\n\nChoose exactly one verification mode:\n\n-  **Link mode**\n\n   *   ``verification_token`` (string, required): The JWT sent to the user (typically via an email link) to verify their email.\n\n-  **Code mode**\n\n   *   ``email`` (string, required): The user's email address to verify.\n   *   ``code`` (string, required): The one-time code sent via email.\n   *   ``challenge`` (string, optional, also accepts ``code_challenge``): If provided, a PKCE authorization code will be generated upon success.\n   *   ``redirect_to`` (string, optional): If provided, a redirect response will be sent upon success. This URL must be in the server's list of allowed redirect URIs.\n\n**Response:**\n\n-  **Link mode**\n\n   The primary action is to validate the ``verification_token`` and mark the associated email as verified. The exact response depends on the contents of the ``verification_token`` (it may include a PKCE challenge and/or a redirect URL specified during its creation):\n\n   1.  With challenge and redirect URL in token\n\n       *   A PKCE authorization code is generated using the challenge from the token.\n       *   A 302 redirect to the URL specified in the token (``maybe_redirect_to``) occurs, with ``code`` appended as a query parameter.\n\n   2.  With challenge only in token\n\n       *   A PKCE authorization code is generated using the challenge from the token.\n       *   A 200 OK response is returned with a JSON body:\n\n           .. code-block:: json\n\n               {\n                 \"code\": \"generated_pkce_code\"\n               }\n\n   3.  With redirect URL only in token\n\n       *   A 302 redirect to the URL specified in the token (``maybe_redirect_to``) occurs (no ``code`` is added).\n\n   4.  No challenge or redirect URL in token\n\n       *   A 204 No Content response is returned.\n\n   5.  Invalid or expired token\n\n       *   A 403 Forbidden response is returned with a JSON body (e.g., token expired).\n\n-  **Code mode**\n\n   After validating ``email`` and ``code`` and marking the email as verified, behavior depends on optional ``challenge`` and ``redirect_to``:\n\n   1.  ``challenge`` and ``redirect_to`` provided\n\n       *   A PKCE authorization code is generated and a 302 redirect to ``redirect_to`` occurs with ``code`` appended as a query parameter.\n\n   2.  Only ``challenge`` provided\n\n       *   A PKCE authorization code is generated and a 200 OK response is returned with a JSON body:\n\n           .. code-block:: json\n\n               {\n                 \"code\": \"generated_pkce_code\"\n               }\n\n   3.  Only ``redirect_to`` provided\n\n       *   A 302 redirect to ``redirect_to`` occurs (no PKCE code is generated).\n\n   4.  Neither provided\n\n       *   A 204 No Content response is returned.\n\n**Common Error Scenarios:**\n\n*   Missing ``provider`` or ``verification_token`` in the request (results in HTTP 400).\n*   The ``verification_token`` is malformed, has an invalid signature, or is expired (results in HTTP 403).\n*   An internal error occurs while trying to update the email verification status (results in HTTP 500).\n\nPOST /resend-verification-email\n-------------------------------\n\nResend a verification email to a user. This can be useful if the original email was lost or the token expired.\n\n**Request Body (JSON):**\n\nThe request must include ``provider`` and a way to identify the user's email factor.\n\n*   ``provider`` (string, required): The provider name, e.g., ``builtin::local_emailpassword`` or ``builtin::local_webauthn``.\n\nThen, choose **one** of the following methods to specify the user:\n\n*   **Method 1: Using an existing Verification Token**\n\n    *   ``verification_token`` (string): An old (even expired) verification token. The system will extract necessary details (like ``identity_id``, original ``verify_url``, ``challenge``, and ``redirect_to``) from this token to generate a new one.\n\n*   **Method 2: Using Email Address (for Email/Password provider)**\n\n    *   ``email`` (string, required if ``provider`` is ``builtin::local_emailpassword`` and ``verification_token`` is not used): The user's email address.\n    *   ``verify_url`` (string, optional): The base URL for the new verification link. Defaults to the server's configured UI verify path (e.g., ``<base_path>/ui/verify``).\n    *   ``challenge`` (string, optional, also accepts ``code_challenge``): A PKCE code challenge to be embedded in the new verification token.\n    *   ``redirect_to`` (string, optional): A URL to redirect to after successful verification using the new token. This URL must be in the server's list of allowed redirect URIs.\n\n*   **Method 3: Using WebAuthn Credential ID (for WebAuthn provider)**\n\n    *   ``credential_id`` (string, required if ``provider`` is ``builtin::local_webauthn`` and ``verification_token`` is not used): The Base64 encoded WebAuthn credential ID.\n    *   ``verify_url`` (string, optional): As above.\n    *   ``challenge`` (string, optional, also accepts ``code_challenge``): As above.\n    *   ``redirect_to`` (string, optional): As above. This URL must be in the server's list of allowed redirect URIs.\n\n**Response:**\n\nThe endpoint aims to prevent email enumeration by always returning a successful status code if the request format is valid, regardless of whether the user or email factor was found.\n\n1.  **Verification Email Queued (or User/Email Factor Not Found):**\n\n    *   If the user/email factor is found, a new verification email with a fresh token is generated and sent.\n    *   If the user/email factor is not found (based on the provided identifier), the server simulates a successful send.\n    *   A 200 OK response is returned. The response body is typically empty.\n\n2.  **Failure (Invalid Request or Server Error):**\n\n    *   If the request is malformed (e.g., unsupported ``provider``, ``redirect_to`` URL not allowed, missing required fields for the chosen identification method), an HTTP 400 Bad Request with a JSON error body is returned.\n    *   If an internal server error occurs (e.g., SMTP issues), an HTTP 500 Internal Server Error with a JSON error body is returned.\n\n**Common Error Scenarios:**\n\n*   Unsupported ``provider`` name.\n*   Missing ``verification_token`` when it's the chosen method, or missing ``email`` / ``credential_id`` for other methods.\n*   Providing a ``redirect_to`` URL that is not in the allowed list.\n*   Internal SMTP errors preventing email dispatch.\n\n.. note::\n\n  If the provider uses the **Code** verification method, the resend email will contain a one-time code instead of a link. In this case, ``verify_url``, ``challenge``, and ``redirect_to`` are not included in the email and are only relevant for the Link method.\n\nOAuth\n=====\n\nPOST /authorize\n---------------\n\nInitiate an OAuth authorization flow.\n\n**Request Parameters (Query String):**\n\n*   ``provider`` (string, required): The name of the OAuth provider to use (e.g., ``builtin::oauth::google``).\n*   ``redirect_to`` (string, required): The URL to redirect to after a successful OAuth flow completes and a PKCE code is obtained. This URL must be in the server's list of allowed redirect URIs.\n*   ``challenge`` (string, required, also accepts ``code_challenge``): A PKCE code challenge generated by your application.\n*   ``redirect_to_on_signup`` (string, optional): An alternative URL to redirect to after a *new* user successfully completes the OAuth flow. If not provided, ``redirect_to`` will be used for both new and existing users. This URL must also be in the server's list of allowed redirect URIs.\n*   ``callback_url`` (string, optional): The URL the OAuth provider should redirect back to after the user authorizes the application. If not provided, it defaults to ``<auth_server_base_url>/callback``. This URL must be in the server's list of allowed redirect URIs.\n\n**Response:**\n\n1.  **Successful Authorization Initiation:**\n\n    *   The server generates a PKCE challenge record and prepares for the OAuth flow.\n    *   A 302 Found redirect response is returned.\n    *   The ``Location`` header will contain the authorization URL provided by the external OAuth identity provider. The user's browser will be directed to this URL to begin the OAuth provider's authentication/authorization process.\n\n**Common Error Scenarios:**\n\n*   Missing required fields in the query string: ``provider``, ``redirect_to``, or ``challenge``.\n*   The provided ``redirect_to``, ``redirect_to_on_signup``, or ``callback_url`` is not in the server's list of allowed redirect URIs.\n*   Configuration error on the server (e.g., the specified provider is not configured).\n\nPOST /callback\n--------------\n\nHandle the redirect from the OAuth provider. This endpoint is typically called by the OAuth provider after the user has completed the authentication and authorization process on the provider's site. It processes the response from the provider, exchanges the authorization code for Gel session information (and potentially provider tokens), and redirects the user back to the application.\n\nThis endpoint accepts parameters either in the query string (for GET requests) or in the request body as ``application/x-www-form-urlencoded`` (for POST requests).\n\n**Request Parameters (Query String or Form Data):**\n\n*   ``state`` (string, required): The state parameter originally sent in the ``POST /authorize`` request. This is a signed JWT containing information needed to complete the flow (like provider name, redirect URLs, and the PKCE challenge).\n*   ``code`` (string, optional): The authorization code provided by the OAuth identity provider. This is present on successful authorization.\n*   ``error`` (string, optional): An error code provided by the OAuth identity provider, if authorization failed.\n*   ``error_description`` (string, optional): A human-readable description of the error provided by the OAuth identity provider.\n\n**Response:**\n\n1.  **Successful Callback and Token Exchange:**\n\n    *   This occurs when the OAuth provider returns a ``code``, and the ``state`` is valid.\n    *   The server exchanges the OAuth code for identity information and potentially provider access/refresh tokens.\n    *   The identity is linked to the PKCE challenge provided in the original ``state``.\n    *   A 302 Found redirect response is returned.\n    *   The ``Location`` header will contain the ``redirect_to`` (or ``redirect_to_on_signup`` if applicable) URL specified in the original ``state`` parameter.\n    *   The redirect URL will include the Gel PKCE authorization ``code`` and the ``provider`` name as query parameters (e.g., ``https://app.example.com/success?code=gel_pkce_code&provider=oauth_provider_name``). This PKCE code can then be exchanged for a session token via ``POST /token``.\n\n2.  **OAuth Provider Returned an Error:**\n\n    *   This occurs when the OAuth provider redirects back with an ``error`` parameter.\n    *   A 302 Found redirect response is returned.\n    *   The ``Location`` header will contain the ``redirect_to`` URL specified in the original ``state`` parameter.\n    *   The redirect URL will include the ``error`` and optionally ``error_description`` and the user's ``email`` (if available and relevant) as query parameters.\n\n**Common Error Scenarios (before redirect):**\n\n*   Missing ``state`` parameter in the request.\n*   Invalid or malformed ``state`` token.\n*   The OAuth provider did not return either a ``code`` or an ``error``.\n*   Errors during the server's exchange of the OAuth code with the provider (these typically result in an HTTP error response from this endpoint rather than a redirect with an error).\n\nWebAuthn\n========\n\nPOST /webauthn/register\n-----------------------\n\nRegister a new WebAuthn credential for a user. This typically follows a call to ``GET /webauthn/register/options`` where the registration options were obtained.\n\n**Request Body (JSON):**\n\n*   ``provider`` (string, required): The name of the WebAuthn provider to use: ``builtin::local_webauthn``.\n*   ``challenge`` (string, required): A PKCE code challenge. This challenge will be linked to the identity upon successful registration if email verification is not required.\n*   ``email`` (string, required): The user's email address associated with the WebAuthn credential.\n*   ``credentials`` (string, required): The credential data obtained from the client-side WebAuthn API (``navigator.credentials.create()``). This should be a JSON string.\n*   ``verify_url`` (string, required): The base URL for the email verification link that will be emailed to the user if email verification is required.\n*   ``user_handle`` (string, optional): The Base64 URL encoded user handle generated during the options request. This can also be passed via a cookie named ``edgedb-webauthn-registration-user-handle``.\n\n**Request Cookies:**\n\n*   ``edgedb-webauthn-registration-user-handle`` (string, optional): The Base64 URL encoded user handle generated during the options request. If present, this overrides the ``user_handle`` in the request body.\n\n**Response:**\n\nThe response depends on whether the WebAuthn provider is configured to require email verification or not.\n\n1.  **Successful Registration with Email Verification Required:**\n\n    *   A 201 Created response is returned with a JSON body:\n\n        .. code-block:: json\n\n          {\n            \"identity_id\": \"...\",\n            \"verification_email_sent_at\": \"YYYY-MM-DDTHH:MM:SS.ffffffZ\"\n          }\n\n    *   The ``edgedb-webauthn-registration-user-handle`` cookie is cleared.\n\n2.  **Successful Registration with Email Verification NOT Required (PKCE Flow):**\n\n    *   A 201 Created response is returned with a JSON body:\n\n        .. code-block:: json\n\n          {\n            \"code\": \"...\",\n            \"provider\": \"builtin::local_webauthn\"\n          }\n\n    *   The ``edgedb-webauthn-registration-user-handle`` cookie is cleared. The returned ``code`` can be exchanged for a session token at the ``POST /token`` endpoint.\n\n**Common Error Scenarios:**\n\n*   Missing required fields in the request body or user handle (either in body or cookie).\n*   Invalid or malformed ``credentials`` or ``user_handle`` data.\n*   The specified ``verify_url`` is not in the server's list of allowed redirect URIs.\n*   Errors during the WebAuthn registration process on the server (e.g., credential already registered).\n*   Configuration error on the server (e.g., WebAuthn provider not configured).\n\nPOST /webauthn/authenticate\n---------------------------\n\nAuthenticate a user using an existing WebAuthn credential. This typically follows a call to ``GET /webauthn/authenticate/options`` where the authentication options were obtained.\n\n**Request Body (JSON):**\n\n*   ``provider`` (string, required): The name of the WebAuthn provider to use: ``builtin::local_webauthn``.\n*   ``challenge`` (string, required): A PKCE code challenge. This challenge will be linked to the authenticated identity upon successful authentication.\n*   ``email`` (string, required): The user's email address associated with the WebAuthn credential they are attempting to use.\n*   ``assertion`` (string, required): The assertion data obtained from the client-side WebAuthn API (``navigator.credentials.get()``). This should be a JSON string.\n\n**Response:**\n\n1.  **Successful Authentication:**\n\n    *   This occurs when the provided ``assertion`` successfully verifies the user's identity based on the provided ``email``.\n    *   If email verification is required for the provider, the user's email must also be verified.\n    *   A PKCE authorization ``code`` is generated and linked to the authenticated identity using the provided ``challenge``.\n    *   A 200 OK response is returned with a JSON body:\n\n        .. code-block:: json\n\n          {\n            \"code\": \"...\"\n          }\n\n    *   The returned ``code`` can be exchanged for a session token at the ``POST /token`` endpoint.\n\n2.  **Authentication Failure:**\n\n    *   This occurs if the provided ``assertion`` does not match the registered credential for the given email, the email is not found, or if email verification is required but the email is not verified.\n    *   An HTTP error response (e.g., 401 Unauthorized or 403 Forbidden) is returned with a JSON body describing the error (e.g., \"Failed to authenticate WebAuthn\", \"VerificationRequired\").\n\n**Common Error Scenarios:**\n\n*   Missing required fields in the request body: ``challenge``, ``email``, or ``assertion``.\n*   Invalid or malformed ``assertion`` data.\n*   No WebAuthn credential found for the provided email.\n*   WebAuthn authentication failed (e.g., invalid signature).\n*   Email verification is required for the provider, but the user's email is not verified.\n*   Configuration error on the server (e.g., WebAuthn provider not configured).\n\nGET /webauthn/register/options\n------------------------------\n\nGet the necessary options from the server to initiate a WebAuthn registration ceremony on the client side (using ``navigator.credentials.create()``).\n\n**Request Parameters (Query String):**\n\n*   ``email`` (string, required): The user's email address for whom registration options are being requested.\n\n**Response:**\n\n1.  **Successful Options Retrieval:**\n\n    *   A 200 OK response is returned.\n    *   The ``Content-Type`` header is ``application/json``.\n    *   The response body contains a JSON object with the WebAuthn registration options, compatible with the Web Authentication API (``PublicKeyCredentialCreationOptions``).\n    *   A cookie named ``edgedb-webauthn-registration-user-handle`` is set containing the Base64 URL encoded user handle generated by the server. This cookie is needed for the subsequent ``POST /webauthn/register`` request.\n\n**Common Error Scenarios:**\n\n*   Missing required ``email`` query parameter.\n*   Configuration error on the server (e.g., WebAuthn provider not configured).\n*   Errors during the generation of registration options on the server.\n\nGET /webauthn/authenticate/options\n----------------------------------\n\nGet the necessary options from the server to initiate a WebAuthn authentication ceremony on the client side (using ``navigator.credentials.get()``).\n\n**Request Parameters (Query String):**\n\n*   ``email`` (string, required): The user's email address for whom authentication options are being requested. The server will look up associated WebAuthn credentials based on this email.\n\n**Response:**\n\n1.  **Successful Options Retrieval:**\n\n    *   A 200 OK response is returned.\n    *   The ``Content-Type`` header is ``application/json``.\n    *   The response body contains a JSON object with the WebAuthn authentication options, compatible with the Web Authentication API (``PublicKeyCredentialRequestOptions``). These options will include information about the user's registered credentials to challenge the client.\n\n**Common Error Scenarios:**\n\n*   Missing required ``email`` query parameter.\n*   Configuration error on the server (e.g., WebAuthn provider not configured).\n*   Errors during the generation of authentication options on the server (e.g., no credentials found for the email).\n\nMagic link\n==========\n\nPOST /magic-link/register\n-------------------------\n\nRegisters a new user with a magic link credential and sends a magic link email to their email address.\n\n**Request Body (JSON or application/x-www-form-urlencoded):**\n\nThe required fields depend on the provider's verification method.\n\n-  **Code method**\n\n   *   ``email`` (string, required): The user's email address.\n   *   ``redirect_to`` (string, optional): A URL to redirect to after the email has been queued. If omitted, the request must accept ``application/json``.\n\n-  **Link method**\n\n   *   ``email`` (string, required): The user's email address.\n   *   ``challenge`` (string, required): A PKCE code challenge that will be embedded in the magic link token.\n   *   ``callback_url`` (string, required): The URL that the user will be redirected to after clicking the magic link in the email. A PKCE authorization ``code`` will be appended to this URL. This URL must be in the server's list of allowed redirect URIs.\n   *   ``redirect_on_failure`` (string, required): A URL to redirect to if there's an error during the registration or email sending process. Error details will be appended as query parameters. This URL must be in the server's list of allowed redirect URIs.\n   *   ``redirect_to`` (string, optional): A URL to redirect to *after* the server has successfully queued the email for sending (before the user clicks the link). If provided, a JSON response will not be returned, and parameters like ``email_sent`` (or ``code=true`` in Code method) will be appended as query parameters. This URL must be in the server's list of allowed redirect URIs.\n   *   ``link_url`` (string, optional): The base URL for the magic link itself (the endpoint the link in the email will point to). If not provided, it defaults to ``<auth_server_base_url>/magic-link/authenticate``. This URL must be in the server's list of allowed redirect URIs.\n\n**Response:**\n\nThe endpoint attempts to prevent email enumeration by always returning a success status if the request format is valid.\n\n-  **Code method**\n\n   *   If the request accepts ``application/json`` and ``redirect_to`` is not provided, a 200 OK JSON response is returned:\n\n       .. code-block:: json\n\n           {\n             \"code\": \"true\",\n             \"signup\": \"true\",\n             \"email\": \"user@example.com\"\n           }\n\n   *   If ``redirect_to`` is provided, a 302 Found redirect occurs to ``redirect_to`` with ``code=true``, ``signup=true`` and ``email`` as query parameters.\n\n-  **Link method**\n\n   *   If the request accepts ``application/json`` and ``redirect_to`` is not provided, a 200 OK JSON response is returned:\n\n       .. code-block:: json\n\n           {\n             \"email_sent\": \"user@example.com\"\n           }\n\n   *   If ``redirect_to`` is provided, a 302 Found redirect occurs to ``redirect_to`` with ``email_sent`` as a query parameter.\n\n-  **Failure**\n\n   *   If an error occurs before a redirect would occur and the request accepts JSON, an HTTP error response (e.g., 400 Bad Request) is returned with a JSON body.\n   *   Otherwise, if ``redirect_on_failure`` was provided (Link method), a 302 Found redirect occurs to that URL with ``error`` and ``email`` query parameters.\n\n**Common Error Scenarios (leading to failure responses):**\n\n*   Missing required fields in the request body: ``provider``, ``email``, ``challenge``, ``callback_url``, or ``redirect_on_failure``.\n*   The provided ``callback_url``, ``redirect_on_failure``, ``redirect_to``, or ``link_url`` is not in the server's list of allowed redirect URIs.\n*   Unsupported ``provider`` name.\n*   Internal server error during email dispatch (e.g., SMTP issues).\n\nPOST /magic-link/email\n----------------------\n\nSends a magic link email to a user with an *existing* magic link credential. This is similar to ``POST /magic-link/register`` but does not attempt to create a new identity if the email is not found (though it still simulates a successful send to prevent enumeration).\n\n**Request Body (JSON or application/x-www-form-urlencoded):**\n\nThe required fields depend on the provider's verification method.\n\n-  **Code method**\n\n   *   ``email`` (string, required): The user's email address.\n   *   ``redirect_to`` (string, optional): A URL to redirect to after the email has been queued. If omitted, the response will be JSON.\n\n-  **Link method**\n\n   *   ``email`` (string, required): The user's email address.\n   *   ``challenge`` (string, required): A PKCE code challenge that will be embedded in the magic link token.\n   *   ``callback_url`` (string, required): The URL that the user will be redirected to after clicking the magic link in the email. A PKCE authorization ``code`` will be appended to this URL. This URL must be in the server's list of allowed redirect URIs.\n   *   ``redirect_on_failure`` (string, required): A URL to redirect to if there's an error during the email sending process. Error details will be appended as query parameters. This URL must be in the server's list of allowed redirect URIs.\n   *   ``redirect_to`` (string, optional): A URL to redirect to *after* the server has successfully queued the email for sending (before the user clicks the link). If provided, a JSON response will not be returned.\n   *   ``link_url`` (string, optional): The base URL for the magic link itself. If not provided, it defaults to ``<auth_server_base_url>/magic-link/authenticate``. This URL must be in the server's list of allowed redirect URIs.\n\n**Response:**\n\nThe endpoint attempts to prevent email enumeration by always returning a success status if the request format is valid, even if the email address is not found.\n\n-  **Code method**\n\n   *   If ``redirect_to`` is NOT provided, a 200 OK JSON response is returned:\n\n       .. code-block:: json\n\n           {\n             \"code\": \"true\",\n             \"email\": \"user@example.com\"\n           }\n\n   *   If ``redirect_to`` is provided, a 302 Found redirect occurs to the ``redirect_to`` URL with ``code=true`` and ``email`` as query parameters.\n\n-  **Link method**\n\n   *   If ``redirect_to`` is NOT provided, a 200 OK JSON response is returned:\n\n       .. code-block:: json\n\n           {\n             \"email_sent\": \"user@example.com\"\n           }\n\n   *   If ``redirect_to`` is provided, a 302 Found redirect occurs to the ``redirect_to`` URL with ``email_sent`` as a query parameter.\n\n-  **Failure**\n\n   *   If an error happens and a ``redirect_on_failure`` URL was provided (Link method), a 302 Found redirect is returned to that URL with ``error`` and the submitted ``email`` as query parameters. Otherwise, an HTTP error response is returned with a JSON body.\n\n**Common Error Scenarios (leading to failure responses):**\n\n*   Missing required fields in the request body: ``provider``, ``email``, ``challenge``, ``callback_url``, or ``redirect_on_failure``.\n*   The provided ``callback_url``, ``redirect_on_failure``, ``redirect_to``, or ``link_url`` is not in the server's list of allowed redirect URIs.\n*   Unsupported ``provider`` name.\n*   Internal server error during email dispatch (e.g., SMTP issues).\n\nPOST /magic-link/authenticate\n-----------------------------\n\nAuthenticates a user by validating a magic link token received from an email. This endpoint is typically the target of the magic link URL sent to the user.\n\nThis endpoint supports both Link and Code methods.\n\n**Link method (Query String):**\n\n*   ``token`` (string, required): The magic link token (a signed JWT) extracted from the magic link URL. This token contains the identity ID, the original PKCE challenge, and the callback URL.\n*   ``redirect_on_failure`` (string, optional): A URL to redirect to if the authentication process fails (e.g., invalid or expired token). Error details will be appended as query parameters. If not provided, an HTTP error response will be returned on failure.\n\n**Code method (JSON body):**\n\n*   ``email`` (string, required): The user's email address.\n*   ``code`` (string, required): The one-time code sent via email.\n*   ``callback_url`` (string, required): The URL to redirect to after successful authentication. Must be an allowed redirect URI.\n*   ``challenge`` (string, required): A PKCE code challenge. A PKCE authorization ``code`` will be generated upon success.\n\n**Response:**\n\n-  **Link method**\n\n   *   If the provided ``token`` is valid, the user's email factor is marked as verified and a PKCE authorization ``code`` is generated using the challenge embedded in the token. A 302 Found redirect is returned to the token's ``callback_url`` with ``code`` appended.\n   *   On failure, if ``redirect_on_failure`` is provided, a 302 redirect occurs to that URL with an ``error`` parameter; otherwise, an HTTP error response is returned with a JSON body.\n\n-  **Code method**\n\n   *   On success, the one-time code is validated, the email factor is marked as verified, and a PKCE authorization ``code`` is generated using the provided ``challenge``. A 302 Found redirect occurs to ``callback_url`` with ``code`` appended.\n   *   On failure, if a ``redirect_on_failure`` query parameter is present, a 302 redirect occurs to that URL with an ``error`` parameter; otherwise, a 400 Bad Request JSON response is returned with an error body.\n\n**Common Error Scenarios (leading to failure responses):**\n\n*   Missing required ``token`` query parameter.\n*   The provided ``token`` is malformed, has an invalid signature, or is expired.\n*   Internal server error during the authentication or email verification process.\n*   The ``callback_url`` extracted from the token is not in the server's list of allowed redirect URIs (this should ideally be caught earlier, but could potentially manifest here).\n"
  },
  {
    "path": "docs/reference/auth/index.rst",
    "content": ".. _ref_guide_auth:\n\n====\nAuth\n====\n\n.. toctree::\n    :hidden:\n    :maxdepth: 3\n\n    http\n    built_in_ui\n    email_password\n    oauth\n    magic_link\n    webauthn\n    webhooks\n\n:edb-alt-title: Using Gel Auth\n\n|Gel| Auth is a batteries-included authentication solution for your app built\ninto the Gel server. Here's how you can integrate it with your app.\n\n\nEnable extension in your schema\n===============================\n\nAuth is a Gel extension. To enable it, you will need to add the extension\nto your app's schema:\n\n.. code-block:: sdl\n\n    using extension auth;\n\n\nExtension configuration\n=======================\n\nThe best and easiest way to configure the extension for your database is\nto use the built-in UI. To access it, run  :gelcmd:`ui`. If you have the\nextension enabled in your schema as shown above and have migrated that\nschema change, you will see the \"Auth Admin\" icon in the left-hand toolbar.\n\n.. image:: images/ui-auth.png\n    :alt: The Gel local development server UI highlighting the auth admin\n          icon in the left-hand toolbar. The icon is two nested shield\n          outlines, the inner being a light pink color and the outer being\n          a light blue when selected.\n    :width: 100%\n\nThe auth admin UI exposes these values:\n\n\napp_name\n--------\n\nThe name of your application to be shown on the login screen when using the\nbuilt-in UI.\n\n\nlogo_url\n--------\n\nA URL to an image of your logo. This is also used to customize the built-in UI.\n\n\nlogo_url\n--------\n\nA URL to an image of your logo for use with a dark theme. This is also used to\ncustomize the built-in UI.\n\n\nbrand_color\n-----------\n\nYour brand color as a hex string. This will be used as the accent color in the\nbuilt-in UI.\n\n\nauth_signing_key\n----------------\n\nThe extension uses JSON Web Tokens (JWTs) internally for many operations.\n``auth_signing_key`` is the value that is used as a symmetric key for signing\nthe JWTs. At the moment, the JWTs are not considered \"public\" API, so there is\nno need to save this value for your own application use. It is exposed mainly\nto allow rotation.\n\nTo configure via query or script:\n\n.. lint-off\n\n.. code-block:: edgeql\n\n    CONFIGURE CURRENT BRANCH SET\n    ext::auth::AuthConfig::auth_signing_key := 'F2KHaJfHi9Dzd8+6DI7FB9IFIoJXnhz2rzG/UzCRE7jTtYxqgTHHydc8xnN6emDB3tlR99FvPsyJfcVLVcQ5odSQpceDXplBOP+N14+EBy2mV6rA/7W7azIEKebtr9TVKrpBTMTOLAXo08ZnA6lvjn0VMs95za6Pta7VW62hjcb8jy6yxulvvU5SWnwa0x2z401K0pLK7byDD5eNqgTl40YaeOGoQ0iCkSmGxvLxyQgCIz2IU0zUbBwC9bQsTDORvflunruJznHuMxwbfYo/czQIIGuawU0H+G3GJZ3hecZLQlvwYCyLF37PFQVrcNMtUuGyDy2OyYtYHru2GW5B7Q';\n\n.. lint-on\n\ntoken_time_to_live\n------------------\n\nThis value controls the expiration time on the authentication token's\nJSON Web Token. This is effectively the \"session\" time.\n\nTo configure via query or script:\n\n.. code-block:: edgeql\n\n    CONFIGURE CURRENT BRANCH SET\n    ext::auth::AuthConfig::token_time_to_live := <duration>\"336 hours\";\n\nallowed_redirect_urls\n---------------------\n\nThis value is a set of strings that we use to ensure we only redirect to\ndomains that are under the control of the application using the Auth extension.\nWe compare any ``redirect_to`` URLs against this list. A URL is considered a\n\"match\" if the URL is exactly the same as one on the list, or is a sub-path of\na URL on the list.\n\nFor example, if the set includes ``https://example.com/myapp``:\n\n.. list-table::\n   :header-rows: 1\n\n   * - URL\n     - Match\n   * - ``https://example.com/myapp``\n     - ✅\n   * - ``https://example.com/myapp/auth``\n     - ✅\n   * - ``https://example.com/myapp/auth/verify``\n     - ✅\n   * - ``https://example.com/myapp/somewhere/else``\n     - ✅\n   * - ``http://example.com/myapp``\n     - Does not match the protocol\n   * - ``https://example.com:443/myapp``\n     - Does not match the port\n   * - ``https://auth.example.com/myapp``\n     - Does not match the subdomain\n   * - ``https://example.com/different/subpath``\n     - Does not match the pathname or extend it\n\n.. note::\n\n    💡 We always allow redirects to the auth extension itself, so you do not\n    need to add it explicitly if, for instance, you are always using the\n    built-in UI.\n\n\nTo configure via query or script:\n\n.. code-block:: edgeql\n\n    CONFIGURE CURRENT BRANCH SET\n    ext::auth::AuthConfig::allowed_redirect_urls := {\n        'https://example.com',\n        'https://example.com/auth',\n        'https://localhost:3000',\n        'https://localhost:3000/auth'\n    };\n\n\nWebhooks\n========\n\nThe auth extension supports sending webhooks for a variety of auth events. You can use these webhooks to, for instance, send a fully customized email for email verification, or password reset instead of our built-in email verification and password reset emails. You could also use them to trigger analytics events, start an email drip campaign, create an audit log, or trigger other side effects in your application.\n\nSee the :ref:`webhooks documentation <ref_auth_webhooks>` for more details on how to configure and use webhooks.\n\nConfiguring SMTP\n================\n\n\nFor email-based factors, you can configure SMTP to allow the extension to send emails on your behalf. You should either configure SMTP, or webhooks for the relevant events.\n\nThe easiest way to configure SMTP is to use the built-in UI. Here is an example of configuring SMTP for local development using EdgeQL directly, using something like `Mailpit <https://mailpit.axllent.org/docs/>`__.\n\n.. note:: Gel Cloud users, rejoice!\n\n  If you are using Gel Cloud, you can use the built-in development SMTP provider without any configuration. This special provider is already configured for development usage and is ready to send emails while you are developing your application. This provider is tuned specifically for development: it is  rate limited and the sender is hardcoded. Do not use it in production, it will not work for that purpose.\n\n.. code-block:: edgeql\n\n    # Create a new SMTP provider:\n    #\n    configure current branch\n      insert cfg::SMTPProviderConfig {\n        # This name must be unique and is used to reference the provider\n        name := 'local_mailpit',\n        sender := '\"Display Name\" <hello@example.com>',\n        host := 'localhost',\n        port := <int32>1025,\n        username := 'smtpuser',\n        password := 'smtppassword',\n        security := 'STARTTLSOrPlainText',\n        validate_certs := false,\n        timeout_per_email := <duration>'60 seconds',\n        timeout_per_attempt := <duration>'15 seconds',\n      };\n\n    # Set this provider as the current email provider by name:\n    #\n    configure current branch\n      set current_email_provider_name := 'local_mailpit';\n\n.. note::\n\n  The ``sender`` property follows the `RFC 5322 <https://datatracker.ietf.org/doc/html/rfc5322#section-3.4>`_ specification, so you can include a display name in the email address or use a bare email address. Including a display name is recommended as it provides a more user-friendly experience. Email clients will show the display name (e.g., \"Display Name\" from the example above) instead of just the raw email address in the sender field, making your emails appear more professional and trustworthy to recipients.\n\nEnabling authentication providers\n=================================\n\nIn order to use the auth extension, you'll need to enable at least one of these\nauthentication providers. Providers can be added from the \"Providers\" section\nof the admin auth UI by clicking \"Add Provider.\" This will add a form to the UI\nallowing for selection of the provider and configuration of the values\ndescribed below.\n\nYou can also enable providers via query. We'll demonstrate how in each section\nbelow.\n\n\n.. _ref_guide_auth_overview_email_password:\n\nEmail and password\n------------------\n\n-  ``require_verification``: (Default: ``true``) If ``true``, your application\n   will not be able to retrieve an authentication token until the user\n   has verified their email. If ``false``, your application can retrieve an\n   authentication token, but a verification email will still be sent.\n   Regardless of this setting, you can always decide to limit access or\n   specific features in your application by testing if\n   ``ext::auth::EmailPasswordFactor.verified_at`` is set to a date in\n   the past on the ``ext::auth::LocalIdentity``.\n\nTo enable via query or script:\n\n.. code-block:: edgeql\n\n    CONFIGURE CURRENT BRANCH\n    INSERT ext::auth::EmailPasswordProviderConfig {\n        require_verification := false,\n    };\n\n.. note::\n\n    ``require_verification`` defaults to ``true``.\n\nIf you use the Email and Password provider, in addition to the\n``require_verification`` configuration, you'll need to configure SMTP to allow\n|Gel| to send email verification and password reset emails on your behalf or\nset up webhooks for the relevant events:\n\n- ``ext::auth::WebhookEvent.EmailVerificationRequested``\n- ``ext::auth::WebhookEvent.PasswordResetRequested``\n\nHere is an example of setting a local SMTP server, in this case using a\nproduct called `Mailpit <https://mailpit.axllent.org/docs/>`__ which is\ngreat for testing in development:\n\n.. code-block:: edgeql\n\n    CONFIGURE CURRENT BRANCH INSERT cfg::SMTPProviderConfig {\n        sender := 'hello@example.com',\n        host := 'localhost',\n        port := <int32>1025,\n        security := 'STARTTLSOrPlainText',\n        validate_certs := false,\n    };\n\nHere is an example of setting up webhooks for the email verification and\npassword reset events:\n\n.. code-block:: edgeql\n\n    CONFIGURE CURRENT BRANCH INSERT ext::auth::WebhookConfig {\n        url := 'https://example.com/auth/webhook',\n        events := {\n            ext::auth::WebhookEvent.EmailVerificationRequested,\n            ext::auth::WebhookEvent.PasswordResetRequested,\n        }\n    };\n\n\nOAuth\n-----\n\nWe currently support six different OAuth providers:\n\n.. lint-off\n\n-  `Apple <https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple>`__\n-  `Azure\n   (Microsoft) <https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc>`__\n-  `GitHub <https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app>`__\n-  `Google <https://developers.google.com/identity/protocols/oauth2>`__\n-  `Discord <https://discord.com/developers/docs/topics/oauth2>`__\n-  `Slack <https://api.slack.com/authentication/sign-in-with-slack>`__\n\n.. lint-on\n\nThe instructions for creating an app for each provider can be found on\neach provider's developer documentation website, which is linked above.\nThe important things you'll need to find and make note of for your\nconfiguration are the **client ID** and **secret**.\n\nOnce you select the OAuth provider in the configuration UI, you will need to\nprovide those values and the ``additional_scope``:\n\n-  ``client_id`` This is assigned to you by the Identity Provider when\n   you create an app with them.\n-  ``secret`` This is created by the Identity Provider when you create\n   an app with them.\n-  ``additional_scope`` We request certain scope from the Identity\n   Provider to fulfill our minimal data needs. You can pass additional\n   scope here in a space-separated string and we will request that\n   additional scope when getting the authentication token from the\n   Identity Provider.\n\n   .. note::\n\n        We return this authentication token with this scope from the Identity\n        Provider when we return our own authentication token.\n\nYou'll also need to set a callback URL in each provider's interface. To build\nthis callback URL, you will need the hostname, port, and branch name of your\ndatabase. The branch name is |main| by default. The hostname and port can\nbe found running this CLI command:\n\n.. code-block:: bash\n\n   $ gel instance credentials\n\nThis will output a table that includes the hostnames and ports of all your\ninstances. Grab those from the row corresponding to the correct instance for\nuse in your callback URL, which takes on this format:\n\n.. code-block::\n\n    http[s]://{gel_host}[:port]/db/{db_name}/ext/auth/callback\n\nTo enable the Azure OAuth provider via query or script:\n\n.. code-block:: edgeql\n\n    CONFIGURE CURRENT BRANCH\n    INSERT ext::auth::AzureOAuthProvider {\n        secret := 'cccccccccccccccccccccccccccccccc',\n        client_id := '1597b3fc-b67d-4d2b-b38f-acc256341dbc',\n        additional_scope := 'offline_access',\n    };\n\nTo enable any of the others, change ``AzureOAuthProvider`` in the example above\nto one of the other providers:\n\n- ``AppleOAuthProvider``\n- ``DiscordOAuthProvider``\n- ``GitHubOAuthProvider``\n- ``GoogleOAuthProvider``\n- ``SlackOAuthProvider``\n\n\nGeneric OpenID Connect providers\n--------------------------------\n\n.. versionadded:: 6.0\n\nGeneric OpenID Connect providers are now supported. In order to use them,\nyou will need to insert an ``ext::auth::OpenIDConnectProvider`` configuration\nobject with a few additional properties:\n\n- ``name``: A unique string identifying the provider.\n- ``display_name``: A human-readable name for the provider.\n- ``issuer_url``: The issuer URL of the provider. This must be the domain of\n  the provider's authorization server and will be used to drive the OpenID\n  Connect flow.\n- ``logo_url``: (optional) A URL to an image of the provider's logo. This is\n  used in the built-in UI to display the correct logo.\n\nInherited from ``ext::auth::OAuthProviderConfig``:\n\n- ``client_id``: The client ID of the provider.\n- ``secret``: The client secret of the provider.\n- ``additional_scope``: (optional) A space-separated string of additional\n  scopes to request from the provider.\n\nHere is an example of enabling the Google OpenID Connect provider (note, for\nGoogle, you can simply use the existing Google provider, but this is for\nillustration purposes):\n\n.. lint-off\n\n.. code-block:: edgeql\n\n    CONFIGURE CURRENT BRANCH\n    INSERT ext::auth::OpenIDConnectProvider {\n        name := 'google',\n        display_name := 'Google',\n        issuer_url := 'https://accounts.google.com',\n        logo_url := 'https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png',\n        client_id := '1234567890',\n        secret := '1234567890',\n    };\n\n.. lint-on\n\nMagic link\n----------\n\nMagic link offers the following settings:\n\n- ``verification_method``: ``Link`` (default) or ``Code``.\n  - ``Link``: users receive a link and are redirected back with a PKCE ``code``.\n  - ``Code``: users receive a one-time code. Collect the code and call ``POST /magic-link/authenticate`` with ``email``, ``code``, ``callback_url``, and the PKCE ``challenge`` to receive a PKCE ``code``.\n\n- ``token_time_to_live``: determines how long a magic link (or one-time code) remains valid after sending.\n\nSince magic links rely on email, you must also configure SMTP or webhooks. For\nlocal testing, you can use the same method used for SMTP previously for\n:ref:`the email and password provider <ref_guide_auth_overview_email_password>`.\n\nHere is an example of setting a local SMTP server, in this case using a\nproduct called `Mailpit <https://mailpit.axllent.org/docs/>`__ which is\ngreat for testing in development:\n\n.. code-block:: edgeql\n\n    CONFIGURE CURRENT BRANCH INSERT cfg::SMTPProviderConfig {\n        sender := 'hello@example.com',\n        host := 'localhost',\n        port := <int32>1025,\n        security := 'STARTTLSOrPlainText',\n        validate_certs := false,\n    };\n\nHere is an example of setting up webhooks for the magic link events:\n\n.. code-block:: edgeql\n\n    CONFIGURE CURRENT BRANCH INSERT ext::auth::WebhookConfig {\n        url := 'https://example.com/auth/webhook',\n        events := {\n            ext::auth::WebhookEvent.MagicLinkRequested,\n        }\n    };\n\n\nWebAuthn\n--------\n\n-  ``relying_party_origin``: This is the URL of the web application handling\n   the WebAuthn request. If you're using the built-in UI, it's the origin of\n   the Gel web server.\n\n-  ``require_verification``: (Default: ``true``) If ``true``, your application\n   will not be able to retrieve an authentication token until the user has\n   verified their email. If ``false``, your application can retrieve an\n   authentication token, but a verification email will still be sent.\n   Regardless of this setting, you can always decide to limit access or\n   specific features in your application by testing if\n   ``ext::auth::WebAuthnFactor.verified_at`` is set to a date in the past on\n   the ``ext::auth::LocalIdentity``.\n\n.. note::\n\n    You will need to configure SMTP or webhooks. For local testing, you can use\n    Mailpit as described in :ref:`the email/password section\n    <ref_guide_auth_overview_email_password>`.\n\n.. note::\n\n    You will need to configure CORS to allow the client-side script to call the\n    Gel Auth extension's endpoints from the web browser. You can do this by\n    updating the ``cors_allow_origins`` configuration in the Gel server\n    configuration.\n\nHere is an example of setting a local SMTP server, in this case using a\nproduct called `Mailpit <https://mailpit.axllent.org/docs/>`__ which is\ngreat for testing in development:\n\n.. code-block:: edgeql\n\n    CONFIGURE CURRENT BRANCH INSERT cfg::SMTPProviderConfig {\n        sender := 'hello@example.com',\n        host := 'localhost',\n        port := <int32>1025,\n        security := 'STARTTLSOrPlainText',\n        validate_certs := false,\n    };\n\nHere is an example of setting up webhooks for the WebAuthn events:\n\n.. code-block:: edgeql\n\n    CONFIGURE CURRENT BRANCH INSERT ext::auth::WebhookConfig {\n        url := 'https://example.com/auth/webhook',\n        events := {\n            ext::auth::WebhookEvent.EmailVerificationRequested,\n        }\n    };\n\n\nIntegrating your application\n============================\n\nIn the end, what we want to end up with is an authentication token\ncreated by Gel that we can set as a global in any authenticated\nqueries executed from our application, which will set a computed global linked\nto an ``ext::auth::Identity``.\n\n.. note::\n\n    💡 If you want your own ``User`` type that contains application specific\n    information like name, preferences, etc, you can link to this\n    ``ext::auth::Identity`` to do so.\n\nYou can then use the ``ext::auth::Identity`` (or custom ``User`` type)\nto define access policies and make authenticated queries.\n\nSelect your method for detailed configuration:\n\n.. toctree::\n    :maxdepth: 3\n\n    built_in_ui\n    email_password\n    oauth\n    magic_link\n    webauthn\n\n\nExample usage\n=============\n\nHere's an example schema that we can use to show how you would use the\n``auth_token`` you get back from Gel to make queries against a\nprotected resource, in this case being able to insert a ``Post``.\n\n.. code-block:: sdl\n\n   using extension auth;\n\n   module default {\n     global current_user := (\n       assert_single((\n         select User\n         filter .identity = global ext::auth::ClientTokenIdentity\n       ))\n     );\n\n     type User {\n       required name: str;\n       required identity: ext::auth::Identity;\n     }\n\n     type Post {\n       required text: str;\n       required author: User;\n\n       access policy author_has_full_access\n         allow all\n         using (.author ?= global current_user);\n\n       access policy others_read_only\n         allow select;\n     }\n   }\n\nLet's now insert a ``Post``.\n\n.. lint-off\n\n.. code-block:: tsx\n\n   const client = createClient().withGlobals({\n     \"ext::auth::client_token\": auth_token,\n   });\n\n   const inserted = await client.querySingle(\n     `\n     insert Post {\n       text := <str>$text,\n       author := global current_user,\n     }`,\n     {\n       text: 'if your grave doesnt say \"rest in peace\" on it you are automatically drafted into the skeleton war'\n     }\n   );\n\n.. lint-on\n\nI can even delete it, since I have access through the global:\n\n.. code-block:: tsx\n\n    await client.query(`delete Post filter .id = <str>$id`, {\n      id: inserted.id\n    });\n"
  },
  {
    "path": "docs/reference/auth/magic_link.rst",
    "content": ".. _ref_guide_auth_magic_link:\n\n================\nMagic Link Auth\n================\n\n:edb-alt-title: Integrating Gel Auth's Magic Link provider\n\nMagic Link is a passwordless authentication method that allows users to log in via a unique, time-sensitive link sent to their email. This guide will walk you through integrating Magic Link authentication with your application using Gel Auth.\n\nEnable Magic Link provider\n==========================\n\nBefore you can use Magic Link authentication, you need to enable the Magic Link provider in your Gel Auth configuration. This can be done through the Gel UI under the \"Providers\" section.\n\nMagic Link flow\n===============\n\nThe Magic Link authentication flow involves three main steps:\n\n1. **Sending a Magic Link Email**: Your application requests Gel Auth to send a magic link to the user's email.\n\n2. **User Clicks Magic Link**: The user receives the email and clicks on the magic link.\n\n3. **Authentication and Token Retrieval**: The magic link directs the user to your application, which then authenticates the user and retrieves an authentication token from Gel Auth.\n\n.. note::\n\n   Your Magic Link provider can be configured to send either a **Link** or a one-time **Code**:\n\n   - **Code**: Instead of clicking a link, users receive a one-time code. Prompt for the code and call ``POST /magic-link/authenticate`` with ``email``, ``code``, ``callback_url`` and the PKCE ``challenge``. On success, you will be redirected to ``callback_url`` with a PKCE ``code`` you can exchange at ``POST /token``.\n   - **Link**: Users click a link that includes a token. Your app handles the token-based callback as shown below.\n\nUI considerations\n=================\n\nSimilar to how the built-in UI works, you can query the database configuration\nto discover which providers are configured and dynamically build the UI.\n\n.. code-block:: edgeql\n\n  select cfg::Config.extensions[is ext::auth::AuthConfig].providers {\n      name,\n      [is ext::auth::OAuthProviderConfig].display_name,\n  };\n\nThe ``name`` is a unique string that identifies the Identity Provider. OAuth\nproviders also have a ``display_name`` that you can use as a label for links or\nbuttons.\n\n\nExample implementation\n======================\n\nWe will demonstrate the various steps below by building a NodeJS HTTP server in\na single file that we will use to simulate a typical web application.\n\n.. note::\n\n    The details below show the inner workings of how data is exchanged with the\n    Auth extension from a web app using HTTP. You can use this as a guide to\n    integrate with your application written in any language that can send and\n    receive HTTP requests.\n\n\nStart the PKCE flow\n-------------------\n\nWe secure authentication tokens and other sensitive data by using PKCE\n(Proof Key of Code Exchange).\n\nYour application server creates a 32-byte Base64 URL-encoded string (which will\nbe 43 bytes after encoding), called the ``verifier``. You need to store this\nvalue for the duration of the flow. One way to accomplish this bit of state is\nto use an HttpOnly cookie when the browser makes a request to the server for\nthis value, which you can then use to retrieve it from the cookie store at the\nend of the flow. Take this ``verifier`` string, hash it with SHA256, and then\nbase64url encode the resulting string. This new string is called the\n``challenge``.\n\n.. lint-off\n\n.. code-block:: javascript\n\n   import http from \"node:http\";\n   import { URL } from \"node:url\";\n   import crypto from \"node:crypto\";\n\n   /**\n    * You can get this value by running `gel instance credentials`.\n    * Value should be:\n    * `${protocol}://${host}:${port}/branch/${branch}/ext/auth/\n    */\n   const GEL_AUTH_BASE_URL = process.env.GEL_AUTH_BASE_URL;\n   const SERVER_PORT = 3000;\n\n   /**\n    * Generate a random Base64 url-encoded string, and derive a \"challenge\"\n    * string from that string to use as proof that the request for a token\n    * later is made from the same user agent that made the original request\n    *\n    * @returns {Object} The verifier and challenge strings\n    */\n   const generatePKCE = () => {\n      const verifier = crypto.randomBytes(32).toString(\"base64url\");\n\n      const challenge = crypto\n         .createHash(\"sha256\")\n         .update(verifier)\n         .digest(\"base64url\");\n\n      return { verifier, challenge };\n   };\n\n.. lint-on\n\nRouting\n-------\n\nLet's set up the routes we will use to handle the magic link authentication\nflow. We will then detail each route handler in the following sections.\n\n.. lint-off\n\n.. code-block:: javascript\n\n   const server = http.createServer(async (req, res) => {\n     const requestUrl = getRequestUrl(req);\n\n     switch (requestUrl.pathname) {\n       case \"/auth/magic-link/callback\": {\n         await handleCallback(req, res);\n         break;\n       }\n\n       case \"/auth/magic-link/signup\": {\n         await handleSignUp(req, res);\n         break;\n       }\n\n       case \"/auth/magic-link/send\": {\n         await handleSendMagicLink(req, res);\n         break;\n       }\n\n       default: {\n         res.writeHead(404);\n         res.end(\"Not found\");\n         break;\n       }\n     }\n   });\n\n.. lint-on\n\nSign up\n-------\n\n.. lint-off\n\n.. code-block:: javascript\n\n   /**\n    * Send magic link to new user's email for sign up.\n    *\n    * @param {Request} req\n    * @param {Response} res\n    */\n   const handleSignUp = async (req, res) => {\n     let body = \"\";\n     req.on(\"data\", (chunk) => {\n       body += chunk.toString();\n     });\n     req.on(\"end\", async () => {\n       const pkce = generatePKCE();\n       const { email, provider } = JSON.parse(body);\n       if (!email || !provider) {\n         res.status = 400;\n         res.end(\n           `Request body malformed. Expected JSON body with 'email' and 'provider' keys, but got: ${body}`,\n         );\n         return;\n       }\n\n       const registerUrl = new URL(\"magic-link/register\", GEL_AUTH_BASE_URL);\n       const callbackUrl = new URL(\"auth/magic-link/callback\", \"http://localhost:${SERVER_PORT}\");\n       const registerResponse = await fetch(registerUrl.href, {\n         method: \"post\",\n         headers: {\n           \"Content-Type\": \"application/json\",\n         },\n         body: JSON.stringify({\n           challenge: pkce.challenge,\n           email,\n           provider,\n           callback_url: callbackUrl.href,\n           // The following endpoint will be called if there is an error\n           // processing the magic link, such as expiration or malformed token,\n           // etc.\n           redirect_on_failure: `http://localhost:${SERVER_PORT}/auth_error.html`,\n         }),\n       });\n\n       if (!registerResponse.ok) {\n         const text = await registerResponse.text();\n         res.status = 400;\n         res.end(`Error from the auth server: ${text}`);\n         return;\n       }\n\n       res.writeHead(204, {\n         \"Set-Cookie\": `gel-pkce-verifier=${pkce.verifier}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n       });\n       res.end();\n     });\n   };\n\n.. lint-on\n\nSign in\n-------\n\nSigning in with a magic link simply involves telling the Gel Auth server to\nsend a magic link to the user's email. The user will then click on the link to\nauthenticate.\n\n.. lint-off\n\n.. code-block:: javascript\n\n   /**\n    * Send magic link to existing user's email for sign in.\n    *\n    * @param {Request} req\n    * @param {Response} res\n    */\n   const handleSendMagicLink = async (req, res) => {\n     let body = \"\";\n     req.on(\"data\", (chunk) => {\n       body += chunk.toString();\n     });\n     req.on(\"end\", async () => {\n       const pkce = generatePKCE();\n       const { email, provider } = JSON.parse(body);\n       if (!email || !provider) {\n         res.status = 400;\n         res.end(\n           `Request body malformed. Expected JSON body with 'email' and 'provider' keys, but got: ${body}`,\n         );\n         return;\n       }\n\n       const emailUrl = new URL(\"magic-link/email\", GEL_AUTH_BASE_URL);\n       const callbackUrl = new URL(\"auth/magic-link/callback\", \"http://localhost:${SERVER_PORT}\");\n       const authenticateResponse = await fetch(emailUrl.href, {\n         method: \"post\",\n         headers: {\n           \"Content-Type\": \"application/json\",\n         },\n         body: JSON.stringify({\n           challenge: pkce.challenge,\n           email,\n           provider,\n           callback_url: callbackUrl.href,\n         }),\n       });\n\n       if (!authenticateResponse.ok) {\n         const text = await authenticateResponse.text();\n         res.status = 400;\n         res.end(`Error from the auth server: ${text}`);\n         return;\n       }\n\n       res.writeHead(204, {\n         \"Set-Cookie\": `gel-pkce-verifier=${pkce.verifier}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n       });\n       res.end();\n     });\n   };\n\n.. lint-on\n\nCallback\n--------\n\nOnce the user clicks on the magic link (Link method), they will be redirected\nback to your application with a ``code`` query parameter. Your application will\nthen exchange this code for an authentication token.\n\nIf the provider uses the Code method, you will instead collect the one-time\ncode from the user and call ``POST /magic-link/authenticate`` with\n``email``, ``code``, ``callback_url`` and the PKCE ``challenge``. On success\nyou will be redirected to ``callback_url`` with a PKCE ``code`` to exchange at\n``POST /token``.\n\n.. lint-off\n\n.. code-block:: javascript\n\n   /**\n    * Handles the PKCE callback and exchanges the `code` and `verifier`\n    * for an auth_token, setting the auth_token as an HttpOnly cookie.\n    *\n    * @param {Request} req\n    * @param {Response} res\n    */\n   const handleCallback = async (req, res) => {\n      const requestUrl = getRequestUrl(req);\n\n      const code = requestUrl.searchParams.get(\"code\");\n      if (!code) {\n         const error = requestUrl.searchParams.get(\"error\");\n         res.status = 400;\n         res.end(\n            `Magic link callback is missing 'code'. Provider responded with error: ${error}`,\n         );\n         return;\n      }\n\n      const cookies = req.headers.cookie?.split(\"; \");\n      const verifier = cookies\n         ?.find((cookie) => cookie.startsWith(\"gel-pkce-verifier=\"))\n         ?.split(\"=\")[1];\n      if (!verifier) {\n         res.status = 400;\n         res.end(\n            `Could not find 'verifier' in the cookie store. Is this the same user agent/browser that started the authorization flow?`,\n         );\n         return;\n      }\n\n      const codeExchangeUrl = new URL(\"token\", GEL_AUTH_BASE_URL);\n      codeExchangeUrl.searchParams.set(\"code\", code);\n      codeExchangeUrl.searchParams.set(\"verifier\", verifier);\n      const codeExchangeResponse = await fetch(codeExchangeUrl.href, {\n         method: \"GET\",\n      });\n\n      if (!codeExchangeResponse.ok) {\n         const text = await codeExchangeResponse.text();\n         res.status = 400;\n         res.end(`Error from the auth server: ${text}`);\n         return;\n      }\n\n      const { auth_token } = await codeExchangeResponse.json();\n      res.writeHead(204, {\n         \"Set-Cookie\": `gel-auth-token=${auth_token}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n      });\n      res.end();\n   };\n\n.. lint-on\n\n\nCreate a User object\n--------------------\n\nFor some applications, you may want to create a custom ``User`` type in the\ndefault module to attach application-specific information. You can tie this to\nan ``ext::auth::Identity`` by using the ``identity_id`` returned during the\nsign-up flow.\n\n.. note::\n\n    For this example, we'll assume you have a one-to-one relationship between\n    ``User`` objects and ``ext::auth::Identity`` objects. In your own\n    application, you may instead decide to have a one-to-many relationship.\n\nGiven this ``User`` type:\n\n.. code-block:: sdl\n\n   type User {\n       email: str;\n       name: str;\n\n       required identity: ext::auth::Identity {\n           constraint exclusive;\n       };\n   }\n\nWe need to update two parts of the sign-up flow. First, we need to signal to the callback that this particular callback is for a sign-up, which we do by setting the ``isSignUp`` query parameter to ``true``. Second, we need to create a new ``User`` object and attach it to the ``ext::auth::Identity`` object.\n\n.. tabs::\n\n  .. code-tab:: javascript-diff\n    :caption: handleSignUp\n\n      const handleSignUp = async (req, res) => {\n        let body = \"\";\n        req.on(\"data\", (chunk) => {\n          body += chunk.toString();\n        });\n        req.on(\"end\", async () => {\n          const pkce = generatePKCE();\n          const { email, provider } = JSON.parse(body);\n          if (!email || !provider) {\n            res.status = 400;\n            res.end(\n              `Request body malformed. Expected JSON body with 'email' and 'provider' keys, but got: ${body}`,\n            );\n            return;\n          }\n\n          const registerUrl = new URL(\"magic-link/register\", GEL_AUTH_BASE_URL);\n          const callbackUrl = new URL(\"auth/magic-link/callback\", \"http://localhost:${SERVER_PORT}\");\n    +     callbackUrl.searchParams.set(\"isSignUp\", \"true\");\n          const registerResponse = await fetch(registerUrl.href, {\n            method: \"post\",\n            headers: {\n              \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n              challenge: pkce.challenge,\n              email,\n              provider,\n              callback_url: callbackUrl.href,\n              // The following endpoint will be called if there is an error\n              // processing the magic link, such as expiration or malformed token,\n              // etc.\n              redirect_on_failure: `http://localhost:${SERVER_PORT}/auth_error.html`,\n            }),\n          });\n\n          if (!registerResponse.ok) {\n            const text = await registerResponse.text();\n            res.status = 400;\n            res.end(`Error from the auth server: ${text}`);\n            return;\n          }\n\n          res.writeHead(204, {\n            \"Set-Cookie\": `gel-pkce-verifier=${pkce.verifier}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n          });\n          res.end();\n        });\n      };\n\n  .. code-tab:: javascript-diff\n    :caption: handleCallback\n\n      const handleCallback = async (req, res) => {\n        const requestUrl = getRequestUrl(req);\n\n        const code = requestUrl.searchParams.get(\"code\");\n        if (!code) {\n          const error = requestUrl.searchParams.get(\"error\");\n          res.status = 400;\n          res.end(\n              `Magic link callback is missing 'code'. Provider responded with error: ${error}`,\n          );\n          return;\n        }\n\n        const cookies = req.headers.cookie?.split(\"; \");\n        const verifier = cookies\n          ?.find((cookie) => cookie.startsWith(\"gel-pkce-verifier=\"))\n          ?.split(\"=\")[1];\n        if (!verifier) {\n          res.status = 400;\n          res.end(\n              `Could not find 'verifier' in the cookie store. Is this the same user agent/browser that started the authorization flow?`,\n          );\n          return;\n        }\n\n        const codeExchangeUrl = new URL(\"token\", GEL_AUTH_BASE_URL);\n        codeExchangeUrl.searchParams.set(\"code\", code);\n        codeExchangeUrl.searchParams.set(\"verifier\", verifier);\n        const codeExchangeResponse = await fetch(codeExchangeUrl.href, {\n          method: \"GET\",\n        });\n\n        if (!codeExchangeResponse.ok) {\n          const text = await codeExchangeResponse.text();\n          res.status = 400;\n          res.end(`Error from the auth server: ${text}`);\n          return;\n        }\n\n    -   const { auth_token } = await codeExchangeResponse.json();\n    +   const {\n    +     auth_token,\n    +     identity_id\n    +   } = await codeExchangeResponse.json();\n\n    +   if (requestUrl.searchParams.get(\"isSignUp\") === \"true\") {\n    +     await client.query(`\n    +       with\n    +         identity := <ext::auth::Identity><uuid>$identity_id,\n    +         emailFactor := (\n    +           select ext::auth::EmailFactor filter .identity = identity\n    +         ),\n    +       insert User {\n    +         email := emailFactor.email,\n    +         identity := identity\n    +       };\n    +     `, { identity_id });\n    +   }\n    +\n        res.writeHead(204, {\n          \"Set-Cookie\": `gel-auth-token=${auth_token}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n        });\n        res.end();\n      };\n\n:ref:`Back to the Gel Auth guide <ref_guide_auth>`\n"
  },
  {
    "path": "docs/reference/auth/oauth.rst",
    "content": ".. _ref_guide_auth_oauth:\n\n=====\nOAuth\n=====\n\n:edb-alt-title: Integrating Gel Auth's OAuth provider\n\nAlong with using the :ref:`built-in UI <ref_guide_auth_built_in_ui>`, you can also\ncreate your own UI that calls to your own web application backend.\n\nUI considerations\n=================\n\nSimilar to how the built-in UI works, you can query the database configuration\nto discover which providers are configured and dynamically build the UI.\n\n.. code-block:: edgeql\n\n  select cfg::Config.extensions[is ext::auth::AuthConfig].providers {\n      name,\n      [is ext::auth::OAuthProviderConfig].display_name,\n  };\n\nThe ``name`` is a unique string that identifies the Identity Provider. OAuth\nproviders also have a ``display_name`` that you can use as a label for links or\nbuttons. In later steps, you'll be providing this ``name`` as the ``provider``\nin various endpoints.\n\n\nExample implementation\n======================\n\nWe will demonstrate the various steps below by building a NodeJS HTTP server in\na single file that we will use to simulate a typical web application.\n\n.. note::\n\n    We are in the process of publishing helper libraries that you can use with\n    popular languages and web frameworks. The details below show the inner\n    workings of how data is exchanged with the Auth extension from a web app\n    using HTTP. You can use this as a guide to integrate with your application\n    written in any language that can send and receive HTTP requests.\n\nWe secure authentication tokens and other sensitive data by using PKCE\n(Proof Key of Code Exchange).\n\n\nStart the PKCE flow\n-------------------\n\nYour application server creates a 32-byte Base64 URL-encoded string (which will\nbe 43 bytes after encoding), called the ``verifier``. You need to store this\nvalue for the duration of the flow. One way to accomplish this bit of state is\nto use an HttpOnly cookie when the browser makes a request to the server for\nthis value, which you can then use to retrieve it from the cookie store at the\nend of the flow. Take this ``verifier`` string, hash it with SHA256, and then\nbase64url encode the resulting string. This new string is called the\n``challenge``.\n\n.. note::\n\n   Since ``=`` is not a URL-safe character, if your Base64-URL encoding\n   function adds padding, you should remove the padding before hashing the\n   ``verifier`` to derive the ``challenge`` or when providing the ``verifier``\n   or ``challenge`` in your requests.\n\n.. note::\n\n   If you are familiar with PKCE, you will notice some differences from how RFC\n   7636 defines PKCE. Our authentication flow is not an OAuth flow, but rather a\n   strict server-to-server flow with Proof Key of Code Exchange added for\n   additional security to avoid leaking the authentication token. Here are some\n   differences between PKCE as defined in RFC 7636 and our implementation:\n\n   - We do not support the ``plain`` value for ``code_challenge_method``, and\n     therefore do not read that value if provided in requests.\n   - Our parameters omit the ``code_`` prefix, however we do support\n     ``code_challenge`` and ``code_verifier`` as aliases, preferring\n     ``challenge`` and ``verifier`` if present.\n\n.. code-block:: javascript\n\n   import http from \"node:http\";\n   import { URL } from \"node:url\";\n   import crypto from \"node:crypto\";\n\n   /**\n    * You can get this value by running `gel instance credentials`.\n    * Value should be:\n    * `${protocol}://${host}:${port}/branch/${branch}/ext/auth/\n    */\n   const GEL_AUTH_BASE_URL = process.env.GEL_AUTH_BASE_URL;\n   const SERVER_PORT = 3000;\n\n   /**\n    * Generate a random Base64 url-encoded string, and derive a \"challenge\"\n    * string from that string to use as proof that the request for a token\n    * later is made from the same user agent that made the original request\n    *\n    * @returns {Object} The verifier and challenge strings\n    */\n   const generatePKCE = () => {\n     const verifier = crypto.randomBytes(32).toString(\"base64url\");\n\n     const challenge = crypto\n       .createHash(\"sha256\")\n       .update(verifier)\n       .digest(\"base64url\");\n\n     return { verifier, challenge };\n   };\n\n\n.. note::\n\n    For |EdgeDB| versions before 5.0, the value for :gelenv:`AUTH_BASE_URL`\n    in the above snippet should have the form:\n\n    ``${protocol}://${host}:${port}/db/${database}/ext/auth/``\n\n\nRedirect users to Identity Provider\n-----------------------------------\n\nNext, we implement a route at ``/auth/authorize`` that the application should\nlink to when signing in with a particular Identity Provider. We will redirect\nthe end user's browser to the Identity Provider with the proper setup.\n\n.. lint-off\n\n.. code-block:: javascript\n\n   const server = http.createServer(async (req, res) => {\n     const requestUrl = getRequestUrl(req);\n\n     switch (requestUrl.pathname) {\n       case \"/auth/authorize\": {\n         await handleAuthorize(req, res);\n         break;\n       }\n\n       case \"/auth/callback\": {\n         await handleCallback(req, res);\n         break;\n       }\n\n       default: {\n         res.writeHead(404);\n         res.end(\"Not found\");\n         break;\n       }\n     }\n   });\n\n   /**\n    * Redirects OAuth requests to Gel Auth OAuth authorize redirect\n    * with the PKCE challenge, and saves PKCE verifier in an HttpOnly\n    * cookie for later retrieval.\n    *\n    * @param {Request} req\n    * @param {Response} res\n    */\n   const handleAuthorize = async (req, res) => {\n     const requestUrl = getRequestUrl(req);\n     const provider = requestUrl.searchParams.get(\"provider\");\n\n     if (!provider) {\n       res.status = 400;\n       res.end(\"Must provider a 'provider' value in search parameters\");\n       return;\n     }\n\n     const pkce = generatePKCE();\n     const redirectUrl = new URL(\"authorize\", GEL_AUTH_BASE_URL);\n     redirectUrl.searchParams.set(\"provider\", provider);\n     redirectUrl.searchParams.set(\"challenge\", pkce.challenge);\n     redirectUrl.searchParams.set(\n       \"redirect_to\",\n       `http://localhost:${SERVER_PORT}/auth/callback`\n     );\n     redirectUrl.searchParams.set(\n       \"redirect_to_on_signup\",\n       `http://localhost:${SERVER_PORT}/auth/callback?isSignUp=true`\n     );\n\n     res.writeHead(302, {\n       \"Set-Cookie\": `gel-pkce-verifier=${pkce.verifier}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n       Location: redirectUrl.href,\n     });\n     res.end();\n   };\n\n.. lint-on\n\n\nRetrieve ``auth_token``\n-----------------------\n\nAt the very end of the flow, the Gel server will redirect the user's browser\nto the ``redirect_to`` address with a single query parameter: ``code``. This\nroute should be a server route that has access to the ``verifier``. You then\ntake that ``code`` and look up the ``verifier`` in the ``gel-pkce-verifier``\ncookie (``gel-pkce-verifier`` with |EdgeDB| <= 5), and make a request to\nthe Gel Auth extension to exchange these two pieces of data for an\n``auth_token``.\n\n.. lint-off\n\n.. code-block:: javascript\n\n   /**\n    * Handles the PKCE callback and exchanges the `code` and `verifier\n    * for an auth_token, setting the auth_token as an HttpOnly cookie.\n    *\n    * @param {Request} req\n    * @param {Response} res\n    */\n   const handleCallback = async (req, res) => {\n     const requestUrl = getRequestUrl(req);\n\n     const code = requestUrl.searchParams.get(\"code\");\n     if (!code) {\n       const error = requestUrl.searchParams.get(\"error\");\n       res.status = 400;\n       res.end(\n         `OAuth callback is missing 'code'. OAuth provider responded with error: ${error}`\n       );\n       return;\n     }\n\n     const cookies = req.headers.cookie?.split(\"; \");\n     const verifier = cookies\n       ?.find((cookie) => cookie.startsWith(\"gel-pkce-verifier=\"))\n       ?.split(\"=\")[1];\n     if (!verifier) {\n       res.status = 400;\n       res.end(\n         `Could not find 'verifier' in the cookie store. Is this the same user agent/browser that started the authorization flow?`\n       );\n       return;\n     }\n\n     const codeExchangeUrl = new URL(\"token\", GEL_AUTH_BASE_URL);\n     codeExchangeUrl.searchParams.set(\"code\", code);\n     codeExchangeUrl.searchParams.set(\"verifier\", verifier);\n     const codeExchangeResponse = await fetch(codeExchangeUrl.href, {\n       method: \"GET\",\n     });\n\n     if (!codeExchangeResponse.ok) {\n       const text = await codeExchangeResponse.text();\n       res.status = 400;\n       res.end(`Error from the auth server: ${text}`);\n       return;\n     }\n\n     const { auth_token } = await codeExchangeResponse.json();\n     res.writeHead(204, {\n       \"Set-Cookie\": `gel-auth-token=${auth_token}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n     });\n     res.end();\n   };\n\n.. lint-on\n\nCreating a User object\n----------------------\n\nFor some applications, you may want to create a custom ``User`` type in the\ndefault module to attach application-specific information. You can tie this to\nan ``ext::auth::Identity`` by using the ``auth_token`` in our\n``ext::auth::client_token`` global and inserting your ``User`` object with a\nlink to the ``Identity``.\n\n.. note::\n\n    For this example, we'll assume you have a one-to-one relationship between\n    ``User`` objects and ``ext::auth::Identity`` objects. In your own\n    application, you may instead decide to have a one-to-many relationship.\n\nGiven this ``User`` type:\n\n.. code-block:: sdl\n\n   type User {\n       email: str;\n       name: str;\n\n       required identity: ext::auth::Identity {\n           constraint exclusive;\n       };\n   }\n\nYou can update the callback function like this to create a new ``User`` object\nwhen the callback succeeds. Recall that in our ``handleAuthorize`` route\nhandler, we added a separate callback route for when the extension adds a new\nIdentity which sets a search parameter on the URL to ``isSignUp=true``:\n\n.. code-block:: javascript-diff\n\n     const { auth_token } = await codeExchangeResponse.json();\n   +\n   + const isSignUp = requestUrl.searchParams.get(\"isSignUp\");\n   + if (isSignUp === \"true\") {\n   +   const authedClient = client.withGlobals({\n   +     \"ext::auth::client_token\": auth_token,\n   +   });\n   +   await authedClient.query(`\n   +     insert User {\n   +       identity := (global ext::auth::ClientTokenIdentity)\n   +     };\n   +   `);\n   + }\n   +\n     res.writeHead(204, {\n       \"Set-Cookie\": `gel-auth-token=${auth_token}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n     });\n\n\nUsing an OpenID Connect id_token\n--------------------------------\n\nFor some providers that implement OpenID Connect, we also return an ``id_token``\nin the response. This token will have been validated by the extension to\nensure that it has been signed by the provider and that the token has not\nexpired. You can use this token to get additional information about the user\nfrom the provider to enrich your ``User`` object.\n\n.. code-block:: javascript-diff\n\n   - const { auth_token } = await codeExchangeResponse.json();\n   + const { auth_token, id_token } = await codeExchangeResponse.json();\n\n     const isSignUp = requestUrl.searchParams.get(\"isSignUp\");\n     if (isSignUp === \"true\") {\n   +   const { email, name, locale } = id_token\n         ? await decodeJwt(id_token)\n         : { email: null, name: null, locale: null };\n       const authedClient = client.withGlobals({\n         \"ext::auth::client_token\": auth_token,\n       });\n       await authedClient.query(`\n         insert User {\n           identity := (global ext::auth::ClientTokenIdentity)\n   +       email := <optional str>email,\n   +       name := <optional str>name,\n   +       locale := <optional str>locale\n         };\n   -   `);\n   +   `, { email, name, locale });\n     }\n\n\nMaking authenticated requests to the OAuth resource server\n----------------------------------------------------------\n\nAlong with the ``auth_token`` which represents the authenticated user's\nidentity within your system, for OAuth providers, we also return a\n``provider_token`` (and optionally a ``provider_refresh_token``) that you can\nuse to make requests to the OAuth provider's resource server on behalf of the\nuser.\n\nHere is an example of getting the user's profile information from Google\nutilizing OpenID Connect and the ``provider_token``:\n\n.. code-block:: javascript\n\n   /**\n    * Get the user's profile information from Google\n    */\n   async function getUserProfile(providerToken) {\n     const response = await fetch(\n       \"https://accounts.google.com/.well-known/openid-configuration\"\n     );\n     const discoveryDocument = await response.json();\n     const response = await fetch(discoveryDocument.userinfo_endpoint, {\n       headers: {\n         Authorization: `Bearer ${providerToken}`,\n         Accept: \"application/json\",\n       },\n     });\n     return await response.json();\n   }\n\nThen in our callback handler, we can use the ``provider_token`` to get the\nuser's profile information and save it into our ``User`` object when we create\nit:\n\n.. code-block:: javascript-diff\n\n   - const { auth_token } = await codeExchangeResponse.json();\n   + const { auth_token, provider_token } = await codeExchangeResponse.json();\n\n     const isSignUp = requestUrl.searchParams.get(\"isSignUp\");\n     if (isSignUp === \"true\") {\n   +   const profile = await getUserProfile(provider_token);\n       const authedClient = client.withGlobals({\n         \"ext::auth::client_token\": auth_token,\n       });\n       await authedClient.query(\n         `\n   +     with\n   +       email := <optional str>$email,\n   +       name := <optional str>$name,\n         insert User {\n   +       email := email,\n   +       name := name,\n           identity := (global ext::auth::ClientTokenIdentity)\n         };\n   -   `);\n   +     `,\n   +     { email: profile.email, name: profile.name }\n   +   );\n     }\n\n     res.writeHead(204, {\n       \"Set-Cookie\": `gel-auth-token=${auth_token}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n     });\n\n:ref:`Back to the Gel Auth guide <ref_guide_auth>`\n"
  },
  {
    "path": "docs/reference/auth/webauthn.rst",
    "content": ".. _ref_guide_auth_webauthn:\n\n========\nWebAuthn\n========\n\n:edb-alt-title: Integrating Gel Auth's WebAuthn provider\n\nWebAuthn, short for Web Authentication, is a web standard published by the\nWorld Wide Web Consortium (W3C) for secure and passwordless authentication on\nthe web. It allows users to log in using biometrics, mobile devices, or FIDO2\nsecurity keys instead of traditional passwords. This guide will walk you\nthrough integrating WebAuthn authentication with your application using Gel\nAuth.\n\nWhy choose WebAuthn?\n====================\n\nWebAuthn provides a more secure and user-friendly alternative to passwords and\nSMS-based OTPs. By leveraging public key cryptography, it significantly reduces\nthe risk of phishing, man-in-the-middle, and replay attacks. For application\ndevelopers, integrating WebAuthn can enhance security while improving the user\nexperience with seamless, passwordless logins.\n\nWhat is a Passkey?\n==================\n\nWhile WebAuthn focuses on authenticating users through cryptographic\ncredentials, Passkeys extend this concept by enabling users to easily access\ntheir credentials across devices, including those they haven't used before,\nwithout the need for a password. Passkeys are built on the WebAuthn framework\nand aim to simplify the user experience further by leveraging cloud\nsynchronization of credentials.\n\nMany operating systems and password managers have added support for Passkeys,\nmaking it easier for users to manage their credentials across devices. Gel\nAuth's WebAuthn provider supports Passkeys, allowing users to log in to your\napplication using their Passkeys.\n\nSecurity considerations\n=======================\n\nFor maximum flexibility, Gel Auth's WebAuthn provider allows multiple\nWebAuthn credentials per email. This means that it's very important to verify\nthe email before trusting a WebAuthn credential. This can be done by setting\nthe ``require_verification`` option to ``true`` (which is the default) in your\nWebAuthn provider configuration. Or you can check the verification status of\nthe factor directly.\n\nWebAuthn flow\n=============\n\nThe WebAuthn authentication flow is a sophisticated process that involves a\ncoordinated effort between the server and the client-side script. Unlike the\nother authentication methods outlined elsewhere in this guide, WebAuthn is a\ncoordinated flow that involves a client-side script access web browser APIs, the\nWeb Authentication API specifically, to interact with the user's authenticator\ndevice or passkey.\n\nAt a high level, the sign-up ceremony involves the following steps:\n\n1. The user initiates the sign-up process by providing their email address.\n2. The server generates a JSON object that is used to configure the WebAuthn\n   registration ceremony.\n3. The client takes that JSON object, and using the Web Authentication API,\n   interacts with the user's authenticator device to create a new credential.\n4. The client sends the credential back to the server.\n5. The server verifies the credential and associates it with the user's email\n   address.\n\nThe sign-in ceremony is similar, but instead of creating a new credential, the\nclient uses the Web Authentication API to authenticate the user with an existing\ncredential.\n\nExample implementation\n======================\n\nWe will demonstrate the various steps below by building a NodeJS HTTP server in\na single file that we will use to simulate a typical web application.\n\n.. note::\n\n    The details below show the inner workings of how data is exchanged with the\n    Auth extension from a web app using HTTP. You can use this as a guide to\n    integrate with your application written in any language that can send and\n    receive HTTP requests.\n\nStart the PKCE flow\n-------------------\n\nWe secure authentication tokens and other sensitive data by using PKCE\n(Proof Key of Code Exchange).\n\nYour application server creates a 32-byte Base64 URL-encoded string (which will\nbe 43 bytes after encoding), called the ``verifier``. You need to store this\nvalue for the duration of the flow. One way to accomplish this bit of state is\nto use an HttpOnly cookie when the browser makes a request to the server for\nthis value, which you can then use to retrieve it from the cookie store at the\nend of the flow. Take this ``verifier`` string, hash it with SHA256, and then\nbase64url encode the resulting string. This new string is called the\n``challenge``.\n\n.. lint-off\n\n.. code-block:: javascript\n\n   import http from \"node:http\";\n   import { URL } from \"node:url\";\n   import crypto from \"node:crypto\";\n\n   /**\n    * You can get this value by running `gel instance credentials`.\n    * Value should be:\n    * `${protocol}://${host}:${port}/branch/${branch}/ext/auth/\n    */\n   const GEL_AUTH_BASE_URL = process.env.GEL_AUTH_BASE_URL;\n   const SERVER_PORT = 3000;\n\n   /**\n    * Generate a random Base64 url-encoded string, and derive a \"challenge\"\n    * string from that string to use as proof that the request for a token\n    * later is made from the same user agent that made the original request\n    *\n    * @returns {Object} The verifier and challenge strings\n    */\n   const generatePKCE = () => {\n      const verifier = crypto.randomBytes(32).toString(\"base64url\");\n\n      const challenge = crypto\n         .createHash(\"sha256\")\n         .update(verifier)\n         .digest(\"base64url\");\n\n      return { verifier, challenge };\n   };\n\n.. lint-on\n\nRouting\n-------\n\nLet's set up the routes we will use to handle the WebAuthn flow. We will then\ndetail each route handler in the following sections.\n\n.. lint-off\n\n.. code-block:: javascript\n\n   const server = http.createServer(async (req, res) => {\n     const requestUrl = getRequestUrl(req);\n\n     switch (requestUrl.pathname) {\n       case \"/auth/webauthn/register/options\": {\n         await handleRegisterOptions(req, res);\n         break;\n       }\n\n       case \"/auth/webauthn/register\": {\n         await handleRegister(req, res);\n         break;\n       }\n\n       case \"/auth/webauthn/authenticate/options\": {\n         await handleAuthenticateOptions(req, res);\n         break;\n       }\n\n       case \"/auth/webauthn/authenticate\": {\n         await handleAuthenticate(req, res);\n         break;\n       }\n\n       case \"/auth/webauthn/verify\": {\n         await handleVerify(req, res);\n         break;\n       }\n\n       default: {\n         res.writeHead(404);\n         res.end(\"Not found\");\n         break;\n       }\n     }\n   });\n\n.. lint-on\n\nHandle register and authenticate options\n----------------------------------------\n\nThe first step in the WebAuthn flow is to get the options for registering a new\ncredential or authenticating an existing credential. The server generates a\nJSON object that is used to configure the WebAuthn registration or\nauthentication ceremony. The Gel Auth extension provides these endpoints\ndirectly, so you can either proxy the request to the Auth extension or redirect\nthe user to the Auth extension's URL. We'll show the proxy option here.\n\n.. lint-off\n\n.. code-block:: javascript\n\n   const handleRegisterOptions = async (req, res) => {\n     let body = \"\";\n     req.on(\"data\", (chunk) => {\n       body += chunk.toString();\n     });\n     req.on(\"end\", async () => {\n       const { email } = JSON.parse(body);\n       if (!email) {\n         res.status = 400;\n         res.end(\n           `Request body malformed. Expected JSON body with 'email' key, but got: ${body}`,\n         );\n         return;\n       }\n\n       const registerUrl = new URL(\n         \"webauthn/register/options\",\n         GEL_AUTH_BASE_URL\n       );\n       registerUrl.searchParams.set(\"email\", email);\n\n       const registerResponse = await fetch(registerUrl.href);\n\n       if (!registerResponse.ok) {\n         const text = await registerResponse.text();\n         res.status = 400;\n         res.end(`Error from the auth server: ${text}`);\n         return;\n       }\n\n       const registerData = await registerResponse.json();\n\n       res.writeHead(200, { \"Content-Type\": \"application/json\" });\n       res.end(JSON.stringify(registerData));\n     });\n   };\n\n   const handleAuthenticateOptions = async (req, res) => {\n     let body = \"\";\n     req.on(\"data\", (chunk) => {\n       body += chunk.toString();\n     });\n     req.on(\"end\", async () => {\n       const { email } = JSON.parse(body);\n       if (!email) {\n         res.status = 400;\n         res.end(\n           `Request body malformed. Expected JSON body with 'email' key, but got: ${body}`,\n         );\n         return;\n       }\n\n       const authenticateUrl = new URL(\n         \"webauthn/authenticate/options\",\n         GEL_AUTH_BASE_URL\n       );\n       authenticateUrl.searchParams.set(\"email\", email);\n\n       const authenticateResponse = await fetch(authenticateUrl.href);\n\n       if (!authenticateResponse.ok) {\n         const text = await authenticateResponse.text();\n         res.status = 400;\n         res.end(`Error from the auth server: ${text}`);\n         return;\n       }\n\n       const authenticateData = await authenticateResponse.json();\n\n       res.writeHead(200, { \"Content-Type\": \"application/json\" });\n       res.end(JSON.stringify(authenticateData));\n     });\n   };\n\n.. lint-on\n\nRegister a new credential\n-------------------------\n\nThe client script will call the Web Authentication API to create a new\ncredential payload and send it to this endpoint. This endpoints job will be to\nforward the serialized credential payload to the Gel Auth extension for\nverification, and then associate the credential with the user's email address.\n\n.. lint-off\n\n.. code-block:: javascript\n\n  const handleRegister = async (req, res) => {\n    let body = \"\";\n    req.on(\"data\", (chunk) => {\n      body += chunk.toString();\n    });\n    req.on(\"end\", async () => {\n      const { challenge, verifier } = generatePKCE();\n      const { email, provider, credentials, verify_url, user_handle } = JSON.parse(body);\n      if (!email || !provider || !credentials || !verify_url || !user_handle) {\n        res.status = 400;\n        res.end(\n          `Request body malformed. Expected JSON body with 'email', 'provider', 'credentials', 'verify_url', and 'user_handle' keys, but got: ${body}`,\n        );\n        return;\n      }\n\n      const registerUrl = new URL(\"webauthn/register\", GEL_AUTH_BASE_URL);\n\n      const registerResponse = await fetch(registerUrl.href, {\n        method: \"post\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n        },\n        body: JSON.stringify({\n          provider,\n          email,\n          credentials,\n          verify_url,\n          user_handle,\n          challenge,\n        }),\n      });\n\n      if (!registerResponse.ok) {\n        const text = await registerResponse.text();\n        res.status = 400;\n        res.end(`Error from the auth server: ${text}`);\n        return;\n      }\n\n      const registerData = await registerResponse.json();\n      if (\"code\" in registerData) {\n        const tokenUrl = new URL(\"token\", GEL_AUTH_BASE_URL);\n        tokenUrl.searchParams.set(\"code\", registerData.code);\n        tokenUrl.searchParams.set(\"verifier\", verifier);\n        const tokenResponse = await fetch(tokenUrl.href, {\n          method: \"get\",\n        });\n\n        if (!tokenResponse.ok) {\n          const text = await authenticateResponse.text();\n          res.status = 400;\n          res.end(`Error from the auth server: ${text}`);\n          return;\n        }\n\n        const { auth_token } = await tokenResponse.json();\n        res.writeHead(204, {\n          \"Set-Cookie\": `gel-auth-token=${auth_token}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n        });\n        res.end();\n      } else {\n        res.writeHead(204, {\n          \"Set-Cookie\": `gel-pkce-verifier=${pkce.verifier}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n        });\n        res.end();\n      }\n    });\n  };\n\n.. lint-on\n\nAuthenticate with an existing credential\n----------------------------------------\n\nThe client script will call the Web Authentication API to authenticate with an\nexisting credential and send the assertion to this endpoint. This endpoint's\njob will be to forward the serialized assertion to the Gel Auth extension\nfor verification.\n\n.. lint-off\n\n.. code-block:: javascript\n\n  const handleAuthenticate = async (req, res) => {\n    let body = \"\";\n    req.on(\"data\", (chunk) => {\n      body += chunk.toString();\n    });\n    req.on(\"end\", async () => {\n      const { challenge, verifier } = generatePKCE();\n      const { email, provider, assertion } = JSON.parse(body);\n      if (!email || !provider || !assertion) {\n        res.status = 400;\n        res.end(\n          `Request body malformed. Expected JSON body with 'email', 'provider', and 'assertion' keys, but got: ${body}`,\n        );\n        return;\n      }\n\n      const authenticateUrl = new URL(\"webauthn/authenticate\", GEL_AUTH_BASE_URL);\n\n      const authenticateResponse = await fetch(authenticateUrl.href, {\n        method: \"post\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n        },\n        body: JSON.stringify({\n          provider,\n          email,\n          assertion,\n          challenge,\n        }),\n      });\n\n      if (!authenticateResponse.ok) {\n        const text = await authenticateResponse.text();\n        res.status = 400;\n        res.end(`Error from the auth server: ${text}`);\n        return;\n      }\n\n      const authenticateData = await authenticateResponse.json();\n      if (\"code\" in authenticateData) {\n        const tokenUrl = new URL(\"token\", GEL_AUTH_BASE_URL);\n        tokenUrl.searchParams.set(\"code\", authenticateData.code);\n        const tokenResponse = await fetch(tokenUrl.href, {\n          method: \"get\",\n        });\n\n        if (!tokenResponse.ok) {\n          const text = await authenticateResponse.text();\n          res.status = 400;\n          res.end(`Error from the auth server: ${text}`);\n          return;\n        }\n\n        const { auth_token } = await tokenResponse.json();\n        res.writeHead(204, {\n          \"Set-Cookie\": `gel-auth-token=${auth_token}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n        });\n        res.end();\n      } else {\n        res.writeHead(400, { \"Content-Type\": \"application/json\" });\n        res.end(JSON.stringify({ error: \"Email must be verified before being able to authenticate.\" }));\n      }\n    });\n  };\n\n.. lint-on\n\nHandle email verification\n-------------------------\n\nWhen a new user signs up, by default we require them to verify their email\naddress before allowing the application to get an authentication token. To\nhandle the verification flow, we implement an endpoint:\n\n.. note::\n\n   If your WebAuthn provider uses the **Code** verification method, the\n   verification email contains a one-time code rather than a link. In that\n   case, prompt the user for the code and call ``POST /verify`` with:\n\n   - **provider**: ``builtin::local_webauthn``\n   - **email** and **code**\n   - optionally a **challenge** and **redirect_to** to receive a PKCE code or a redirect upon success\n\n   The Link-based example below continues to work when the provider uses the\n   Link method.\n\n.. note::\n\n   💡 If you would like to allow users to still log in, but offer limited access\n   to your application, you can check the associated\n   ``ext::auth::WebAuthnFactor`` for the ``ext::auth::Identity`` to see if the\n   ``verified_at`` property is some time in the past. You'll need to set the\n   ``require_verification`` setting in the provider configuration to ``false``.\n\n.. lint-off\n\n.. code-block:: javascript\n\n   /**\n    * Handles the link in the email verification flow.\n    *\n    * @param {Request} req\n    * @param {Response} res\n    */\n   const handleVerify = async (req, res) => {\n     const requestUrl = getRequestUrl(req);\n     const verification_token = requestUrl.searchParams.get(\"verification_token\");\n     if (!verification_token) {\n       res.status = 400;\n       res.end(\n         `Verify request is missing 'verification_token' search param. The verification email is malformed.`,\n       );\n       return;\n     }\n\n     const cookies = req.headers.cookie?.split(\"; \");\n     const verifier = cookies\n       ?.find((cookie) => cookie.startsWith(\"gel-pkce-verifier=\"))\n       ?.split(\"=\")[1];\n     if (!verifier) {\n       res.status = 400;\n       res.end(\n         `Could not find 'verifier' in the cookie store. Is this the same user agent/browser that started the authorization flow?`,\n       );\n       return;\n     }\n\n     const verifyUrl = new URL(\"verify\", GEL_AUTH_BASE_URL);\n     const verifyResponse = await fetch(verifyUrl.href, {\n       method: \"post\",\n       headers: {\n         \"Content-Type\": \"application/json\",\n       },\n       body: JSON.stringify({\n         verification_token,\n         verifier,\n         provider: \"builtin::local_webauthn\",\n       }),\n     });\n\n     if (!verifyResponse.ok) {\n       const text = await verifyResponse.text();\n       res.status = 400;\n       res.end(`Error from the auth server: ${text}`);\n       return;\n     }\n\n     const { code } = await verifyResponse.json();\n\n     const tokenUrl = new URL(\"token\", GEL_AUTH_BASE_URL);\n     tokenUrl.searchParams.set(\"code\", code);\n     tokenUrl.searchParams.set(\"verifier\", verifier);\n     const tokenResponse = await fetch(tokenUrl.href, {\n       method: \"get\",\n     });\n\n     if (!tokenResponse.ok) {\n       const text = await tokenResponse.text();\n       res.status = 400;\n       res.end(`Error from the auth server: ${text}`);\n       return;\n     }\n\n     const { auth_token } = await tokenResponse.json();\n     res.writeHead(204, {\n       \"Set-Cookie\": `gel-auth-token=${auth_token}; HttpOnly; Path=/; Secure; SameSite=Strict`,\n     });\n     res.end();\n   };\n\n.. lint-on\n\nClient-side script\n------------------\n\nOn the client-side, you will need to write a script that retrieves the options\nfrom the Gel Auth extension, calls the Web Authentication API, and sends the\nresulting credential or assertion to the server. Writing out the low-level\nhandling of serialization and deserialization of the WebAuthn data is beyond the\nscope of this guide, but we publish a WebAuthn client library that you can use\nto simlify this process. The library is available on npm as part of our\n``@gel/auth-core`` library. Here is an example of how you might set up a form\nwith appropriate click handlers to perform the WebAuthn sign in and sign up\nceremonies.\n\n.. lint-off\n\n.. code-block:: javascript\n\n  import { WebAuthnClient } from \"@gel/auth-core/webauthn\";\n\n  const webAuthnClient = new WebAuthnClient({\n    signupOptionsUrl: \"http://localhost:3000/auth/webauthn/register/options\",\n    signupUrl: \"http://localhost:3000/auth/webauthn/register\",\n    signinOptionsUrl: \"http://localhost:3000/auth/webauthn/authenticate/options\",\n    signinUrl: \"http://localhost:3000/auth/webauthn/authenticate\",\n    verifyUrl: \"http://localhost:3000/auth/webauthn/verify\",\n  });\n\n  document.addEventListener(\"DOMContentReady\", () => {\n    const signUpButton = document.querySelector(\"button#sign-up\");\n    const signInButton = document.querySelector(\"button#sign-in\");\n    const emailInput = document.querySelector(\"input#email\");\n\n    if (signUpButton) {\n      signUpButton.addEventListener(\"click\", async (event) => {\n        event.preventDefault();\n        const email = emailInput.value.trim();\n        if (!email) {\n          throw new Error(\"No email provided\");\n        }\n        try {\n          await webAuthnClient.signUp(email);\n          window.location = \"http://localhost:3000/signup-success\";\n        } catch (err) {\n          console.error(err);\n          window.location = \"http://localhost:3000/signup-error\";\n        }\n      });\n    }\n\n    if (signInButton) {\n      signInButton.addEventListener(\"click\", async (event) => {\n        event.preventDefault();\n        const email = emailInput.value.trim();\n        if (!email) {\n          throw new Error(\"No email provided\");\n        }\n        try {\n          await webAuthnClient.signIn(email);\n          window.location = \"http://localhost:3000\";\n        } catch (err) {\n          console.error(err);\n          window.location = \"http://localhost:3000/signup-error\";\n        }\n      })\n    }\n  });\n\n.. lint-on\n"
  },
  {
    "path": "docs/reference/auth/webhooks.rst",
    "content": ".. _ref_auth_webhooks:\n\n========\nWebhooks\n========\n\nThe auth extension supports sending webhooks for a variety of auth events. You can use these webhooks to, for instance, send a fully customized email for email verification, or password reset instead of our built-in email verification and password reset emails. You could also use them to trigger analytics events, start an email drip campaign, create an audit log, or trigger other side effects in your application.\n\nIf you are using Webhooks to send emails, be sure to not also configure an SMTP provider otherwise we will send the email via SMTP and also send the webhook which will trigger your custom email sending behavior.\n\n.. warning::\n\n  We send webhooks with no durability or reliability guarantees, so you should always provide a mechanism for retrying delivery of any critical events, such as email verification and password reset. We detail how to resend these events in the relevant sections on the various authentication flows.\n\nConfiguration\n=============\n\nYou can configure webhooks with the UI or via query. The URLs you register as webhooks must be unique across all webhooks configured for each branch. If you want to send multiple events to the same URL, you can do so by adding multiple ``ext::auth::WebhookEvent`` values to the ``events`` set, like in this example.\n\n.. code-block:: edgeql\n\n    configure current branch insert\n      ext::auth::WebhookConfig {\n        url := 'https://example.com/auth/webhook',\n        events := {\n          ext::auth::WebhookEvent.EmailVerificationRequested,\n          ext::auth::WebhookEvent.PasswordResetRequested,\n        },\n        # Optional, only needed if you want to verify the webhook request\n        signing_secret_key := '1234567890',\n      };\n\nWhen you receive a webhook, you'll look at the ``event_type`` field to determine which event corresponds to this webhook request and handle it accordingly.\n\nChecking webhook signatures\n===========================\n\nYou can provide a signing key, which you will need to generate and save in a place that your application will have access to. The extension will then add a ``x-ext-auth-signature-sha256`` header to the request, which you can use to verify the request by comparing the signature to the SHA256 hash of the request body.\n\nHere is an example of how you might verify the signature in a Node.js application:\n\n.. code-block:: typescript\n\n  /**\n   * Assert that if the request contains a signature header, that the signature\n   * is valid for the request body. Will return false if there is no signature\n   * header.\n   *\n   * @param {Request} request - The request to verify.\n   * @param {string} signingKey - The key to use to verify the signature.\n   * @returns {boolean} - True if the signature is present and valid, false if\n   *                    the signature is not present at all.\n   * @throws {AssertionError} - If the signature is present but invalid.\n   */\n  async function assertSignature(\n    request: Request,\n    signingKey: string,\n  ): Promise<boolean> {\n      const signatureHeader = request.headers.get('x-ext-auth-signature-sha256');\n      if (!signatureHeader) {\n        return false;\n      }\n\n      const requestBody = await request.text();\n      const encoder = new TextEncoder();\n      const data = encoder.encode(requestBody);\n      const key = await crypto.subtle.importKey(\n        'raw',\n        encoder.encode(signingKey),\n        { name: 'HMAC', hash: 'SHA-256' },\n        false,\n        ['sign']\n      );\n      const signature = await crypto.subtle.sign('HMAC', key, data);\n      const signatureHex = Buffer.from(signature).toString('hex');\n\n      assert.strictEqual(\n        signatureHeader,\n        signatureHex,\n        \"Signature header is set, but the signature is invalid\"\n      );\n\n      return true;\n  };\n\n\nTroubleshooting webhooks\n========================\n\nIf you are having trouble receiving webhooks, you might need to look for any responses from the requests that are being scheduled by the :ref:`std::net::http <ref_std_net>` module. You can list all of the :eql:type:`net::http::ScheduledRequest` objects, and any returned responses with the following query:\n\n.. code-block:: edgeql\n\n    select net::http::ScheduledRequest {\n        **,\n        response: { ** }\n    }\n\nEvents reference\n================\n\nCommon fields for all events:\n\n* ``event_type``: (string) This will be a literal string containing the name of the event. You can use this to determine which event occurred.\n* ``event_id``: (string) A unique identifier to help disambiguate events of the same type.\n* ``timestamp``: (string) The ISO 8601 timestamp of when the event was triggered.\n\n\nIdentity created\n^^^^^^^^^^^^^^^^\n\nWhen a new ``ext::auth::Identity`` object is created, like when a new user signs up, or an existing user adds a new factor, this event is triggered.\n\n**Example payload:**\n\n.. code-block:: text\n\n  POST http://localhost:8000/auth/webhook\n  Content-type: application/json\n  x-ext-auth-signature-sha256: 1234567890\n\n  {\n    \"event_type\": \"IdentityCreated\",\n    \"event_id\": \"1234567890\",\n    \"timestamp\": \"2021-01-01T00:00:00Z\",\n    \"identity_id\": \"identity123\"\n  }\n\nIdentity authenticated\n^^^^^^^^^^^^^^^^^^^^^^\n\nWhen an ``ext::auth::Identity`` object is authenticated, like when a user logs in, this event is triggered.\n\n**Example payload:**\n\n.. code-block:: text\n\n  POST http://localhost:8000/auth/webhook\n  Content-type: application/json\n  x-ext-auth-signature-sha256: 1234567890\n\n  {\n    \"event_type\": \"IdentityAuthenticated\",\n    \"event_id\": \"1234567890\",\n    \"timestamp\": \"2021-01-01T00:00:00Z\",\n    \"identity_id\": \"identity123\"\n  }\n\nEmail factor created\n^^^^^^^^^^^^^^^^^^^^\n\nWhen a new ``ext::auth::EmailFactor`` object is created, like when a user adds a new email factor, this event is triggered.\n\n**Example payload:**\n\n.. code-block:: text\n\n  POST http://localhost:8000/auth/webhook\n  Content-type: application/json\n  x-ext-auth-signature-sha256: 1234567890\n\n  {\n    \"event_type\": \"EmailFactorCreated\",\n    \"event_id\": \"1234567890\",\n    \"timestamp\": \"2021-01-01T00:00:00Z\",\n    \"identity_id\": \"identity123\",\n    \"email_factor_id\": \"emailfactor123\"\n  }\n\nEmail verified\n^^^^^^^^^^^^^^\n\nWhen a user verifies their email address, this event is triggered.\n\n**Example payload:**\n\n.. code-block:: text\n\n  POST http://localhost:8000/auth/webhook\n  Content-type: application/json\n  x-ext-auth-signature-sha256: 1234567890\n\n  {\n    \"event_type\": \"EmailVerified\",\n    \"event_id\": \"1234567890\",\n    \"timestamp\": \"2021-01-01T00:00:00Z\",\n    \"identity_id\": \"identity123\",\n    \"email_factor_id\": \"emailfactor123\"\n  }\n\nEmail verification requested\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nWhen a user requests to verify their email address, like when they first sign up, or requests to resend the verification email, this event is triggered.\n\n**Example payload:**\n\n.. code-block:: text\n\n  POST http://localhost:8000/auth/webhook\n  Content-type: application/json\n  x-ext-auth-signature-sha256: 1234567890\n\n  {\n    \"event_type\": \"EmailVerificationRequested\",\n    \"event_id\": \"1234567890\",\n    \"timestamp\": \"2021-01-01T00:00:00Z\",\n    \"identity_id\": \"identity123\",\n    \"verification_token\": \"verificationtoken123\"\n  }\n\nPassword reset requested\n^^^^^^^^^^^^^^^^^^^^^^^^\n\nWhen a user requests to reset their password, this event is triggered.\n\n**Example payload:**\n\n.. code-block:: text\n\n  POST http://localhost:8000/auth/webhook\n  Content-type: application/json\n  x-ext-auth-signature-sha256: 1234567890\n\n  {\n    \"event_type\": \"PasswordResetRequested\",\n    \"event_id\": \"1234567890\",\n    \"timestamp\": \"2021-01-01T00:00:00Z\",\n    \"identity_id\": \"identity123\",\n    \"reset_token\": \"resettoken123\"\n  }\n\nMagic link requested\n^^^^^^^^^^^^^^^^^^^^\n\nWhen a user requests to send a magic link email, like for signing in, or signing up for the first time, this event is triggered.\n\n**Example payload:**\n\n.. code-block:: text\n\n  POST http://localhost:8000/auth/webhook\n  Content-type: application/json\n  x-ext-auth-signature-sha256: 1234567890\n\n  {\n    \"event_type\": \"MagicLinkRequested\",\n    \"event_id\": \"1234567890\",\n    \"timestamp\": \"2021-01-01T00:00:00Z\",\n    \"identity_id\": \"identity123\",\n    \"email_factor_id\": \"emailfactor123\",\n    \"magic_link_token\": \"magiclinktoken123\",\n    \"magic_link_url\": \"http://localhost:8000/auth/magic-link?token=magiclinktoken123\"\n  }\n"
  },
  {
    "path": "docs/reference/datamodel/access_policies.rst",
    "content": ".. _ref_datamodel_access_policies:\n\n===============\nAccess Policies\n===============\n\n.. index:: object-level security, row-level security, RLS\n\nObject types in |Gel| can contain security policies that restrict the set of\nobjects that can be selected, inserted, updated, or deleted by a particular\nquery. This is known as *object-level security* and is similar in function\nto SQL's row-level security.\n\nWhen no access policies are defined, object-level security is not activated:\nany properly authenticated client can carry out any operation on any object\nin the database. Access policies allow you to ensure that the database itself\nhandles access control logic rather than having to implement it in every\napplication or service that connects to your database.\n\nAccess policies can greatly simplify your backend code, centralizing access\ncontrol logic in a single place. They can also be extremely useful for\nimplementing AI agentic flows, where you want to have guardrails around\nyour data that agents can't break.\n\nWe'll illustrate access policies in this document with this simple schema:\n\n.. code-block:: sdl\n\n    type User {\n      required email: str { constraint exclusive; }\n    }\n\n    type BlogPost {\n      required title: str;\n      required author: User;\n    }\n\n\n.. warning::\n\n  Once a policy is added to a particular object type, **all operations**\n  (``select``, ``insert``, ``delete``, ``update``, etc.) on any object of\n  that type are now *disallowed by default* unless specifically allowed by\n  an access policy! See :ref:`resolution order <ref_datamodel_access_policies>`\n  below for details.\n\nGlobal variables\n================\n\nGlobal variables are a convenient way to set up the context for your access\npolicies.  Gel's global variables are tightly integrated with the Gel's\ndata model, client APIs, EdgeQL and SQL, and the tooling around them.\n\nGlobal variables in Gel are not pre-defined. Users are free to define\nas many globals in their schema as they want to represent the business\nlogic of their application.\n\nA common scenario is storing a ``current_user`` global representing\nthe user executing queries. We'd like to have a slightly more complex example\nshowing that you can use more than one global variable. Let's do that:\n\n* We'll use one *global* ``uuid`` to represent the identity of the user\n  executing the query.\n* We'll have the ``Country`` *enum* to represent the type of country\n  that the user  is currently in. The enum represents three types of\n  countries: those where the service has not been rolled out, those with\n  read-only access, and those with full access.\n* We'll use the ``current_country`` *global* to represent the user's\n  current country. In our *example schema*, we want *country* to be\n  context-specific: the same user who can access certain content in one\n  country might not be able to in another country (let's imagine that's\n  due to different country-specific legal frameworks).\n\nHere is an illustration:\n\n.. code-block:: sdl-diff\n\n    +   scalar type Country extending enum<Full, ReadOnly, None>;\n    +   global current_user: uuid;\n    +   required global current_country: Country {\n    +     default := Country.None\n    +   }\n\n        type User {\n          required email: str { constraint exclusive; }\n        }\n\n        type BlogPost {\n          required title: str;\n          required author: User;\n        }\n\nYou can set and reset these globals in Gel client libraries, for example:\n\n.. tabs::\n\n  .. code-tab:: typescript\n\n    import createClient from 'gel';\n\n    const client = createClient();\n\n    // 'authedClient' will share the network connection with 'client',\n    // but will have the 'current_user' global set.\n    const authedClient = client.withGlobals({\n      current_user: '2141a5b4-5634-4ccc-b835-437863534c51',\n    });\n\n    const result = await authedClient.query(\n      `select global current_user;`);\n    console.log(result);\n\n  .. code-tab:: python\n\n    from gel import create_client\n\n    client = create_client().with_globals({\n        'current_user': '580cc652-8ab8-4a20-8db9-4c79a4b1fd81'\n    })\n\n    result = client.query(\"\"\"\n        select global current_user;\n    \"\"\")\n    print(result)\n\n  .. code-tab:: go\n\n    package main\n\n    import (\n      \"context\"\n      \"fmt\"\n      \"log\"\n\n      \"github.com/geldata/gel-go\"\n    )\n\n    func main() {\n      ctx := context.Background()\n      client, err := gel.CreateClient(ctx, gel.Options{})\n      if err != nil {\n        log.Fatal(err)\n      }\n      defer client.Close()\n\n      id, err := gel.ParseUUID(\"2141a5b4-5634-4ccc-b835-437863534c51\")\n      if err != nil {\n        log.Fatal(err)\n      }\n\n      var result gel.UUID\n      err = client.\n        WithGlobals(map[string]interface{}{\"current_user\": id}).\n        QuerySingle(ctx, \"SELECT global current_user;\", &result)\n      if err != nil {\n        log.Fatal(err)\n      }\n\n      fmt.Println(result)\n    }\n\n  .. code-tab:: rust\n\n    use gel_protocol::{\n      model::Uuid,\n      value::EnumValue\n    };\n\n    let client = gel_tokio::create_client()\n        .await\n        .expect(\"Client should init\")\n        .with_globals_fn(|c| {\n            c.set(\n                \"current_user\",\n                Value::Uuid(\n                    Uuid::parse_str(\"2141a5b4-5634-4ccc-b835-437863534c51\")\n                        .expect(\"Uuid should have parsed\"),\n                ),\n            );\n            c.set(\n                \"current_country\",\n                Value::Enum(EnumValue::from(\"Full\"))\n            );\n        });\n    client\n        .query_required_single::<Uuid, _>(\"select global current_user;\", &())\n        .await\n        .expect(\"Returning value\");\n\n\nDefining policies\n=================\n\nA policy example for our simple blog schema might look like:\n\n.. code-block:: sdl-diff\n\n      global current_user: uuid;\n      required global current_country: Country {\n        default := Country.None\n      }\n      scalar type Country extending enum<Full, ReadOnly, None>;\n\n      type User {\n        required email: str { constraint exclusive; }\n      }\n\n      type BlogPost {\n        required title: str;\n        required author: User;\n\n    +   access policy author_has_full_access\n    +     allow all\n    +     using (global current_user    ?= .author.id\n    +       and  global current_country ?= Country.Full) {\n    +       errmessage := \"User does not have full access\";\n    +     }\n\n    +   access policy author_has_read_access\n    +     allow select\n    +     using (global current_user    ?= .author.id\n    +       and  global current_country ?= Country.ReadOnly);\n      }\n\nExplanation:\n\n- ``access policy <name>`` introduces a new policy in an object type.\n- ``allow all`` grants ``select``, ``insert``, ``update``, and ``delete``\n  access if the condition passes. We also used a separate policy to allow\n  only ``select`` in some cases.\n- ``using (<expr>)`` is a boolean filter restricting the set of objects to\n  which the policy applies. (We used the coalescing operator ``?=`` to\n  handle empty sets gracefully.)\n- ``errmessage`` is an optional custom message to display in case of a write\n  violation.\n\nLet's run some experiments in the REPL:\n\n.. code-block:: edgeql-repl\n\n  db> insert User { email := \"test@example.com\" };\n  {default::User {id: be44b326-03db-11ed-b346-7f1594474966}}\n  db> set global current_user :=\n  ...   <uuid>\"be44b326-03db-11ed-b346-7f1594474966\";\n  OK: SET GLOBAL\n  db> set global current_country := Country.Full;\n  OK: SET GLOBAL\n  db> insert BlogPost {\n  ...    title := \"My post\",\n  ...    author := (select User filter .id = global current_user)\n  ...  };\n  {default::BlogPost {id: e76afeae-03db-11ed-b346-fbb81f537ca6}}\n\nBecause the user is in a \"full access\" country and the current user ID\nmatches the author, the new blog post is permitted. When the same user sets\n``global current_country := Country.ReadOnly;``:\n\n.. code-block:: edgeql-repl\n\n  db> set global current_country := Country.ReadOnly;\n  OK: SET GLOBAL\n  db> select BlogPost;\n  {default::BlogPost {id: e76afeae-03db-11ed-b346-fbb81f537ca6}}\n  db> insert BlogPost {\n  ...    title := \"My second post\",\n  ...    author := (select User filter .id = global current_user)\n  ...  };\n  gel error: AccessPolicyError: access policy violation on\n  insert of default::BlogPost (User does not have full access)\n\nFinally, let's unset ``current_user`` and see how many blog posts are returned\nwhen we count them.\n\n.. code-block:: edgeql-repl\n\n  db> set global current_user := {};\n  OK: SET GLOBAL\n  db> select BlogPost;\n  {}\n  db> select count(BlogPost);\n  {0}\n\n``select BlogPost`` returns zero results in this case as well. We can only\n``select`` the *posts* written by the *user* specified by ``current_user``.\nWhen ``current_user`` has no value or has a different value from the\n``.author.id`` of any existing ``BlogPost`` objects, we can't read any posts.\nBut thanks to ``Country`` being set to ``Country.Full``, this user will be\nable to write a new blog post.\n\n**The bottom line:** access policies use global variables to define a\n\"subgraph\" of data that is visible to your queries.\n\n\nPolicy types\n============\n\n.. api-index:: select, insert, delete, update read, update write, all\n\nThe types of policy rules map to the statement type in EdgeQL:\n\n- ``select``: Controls which objects are visible to any query.\n- ``insert``: Post-insert check. If the inserted object violates the policy,\n  the operation fails.\n- ``delete``: Controls which objects can be deleted.\n- ``update read``: Pre-update check on which objects can be updated at all.\n- ``update write``: Post-update check for how objects can be updated.\n- ``all``: Shorthand for granting or denying ``select, insert, update,\n  delete``.\n\nResolution order\n================\n\nIf multiple policies apply (some are ``allow`` and some are ``deny``), the\nlogic is:\n\n1. If there are no policies, access is allowed.\n2. All ``allow`` policies collectively form a *union* / *or* of allowed sets.\n3. All ``deny`` policies *subtract* from that union, overriding allows!\n4. The final set of objects is the intersection of the above logic for each\n   operation: ``select, insert, update read, update write, delete``.\n\nBy default, once you define any policy on an object type, you must explicitly\nallow the operations you need. This is a common **pitfall** when you are\nstarting out with access policies (but you will develop an intuition for this\nquickly). Let's look at an example:\n\n.. code-block:: sdl\n\n    global current_user_id: uuid;\n    global current_user := (\n      select User filter .id = global current_user_id\n    );\n\n    type User {\n      required email: str { constraint exclusive; }\n      required is_admin: bool { default := false };\n\n      access policy admin_only\n        allow all\n        using (global current_user.is_admin ?? false);\n    }\n\n    type BlogPost {\n      required title: str;\n      author: User;\n\n      access policy author_has_full_access\n        allow all\n        using (global current_user ?= .author.id);\n    }\n\nIn the above schema only admins will see a non-empty ``author`` link when\nrunning ``select BlogPost { author }``. Why? Because only admins can see\n``User`` objects at all: ``admin_only`` policy is the only one defined on\nthe ``User`` type!\n\nThis means that instead of making ``BlogPost`` visible to its author, all\nnon-admin authors won't be able to see their own posts. The above issue can be\nremedied by making the current user able to see their own ``User`` record.\n\n\nInteraction between policies\n============================\n\nPolicy expressions themselves do not take other policies into account\n(since |EdgeDB| 3). This makes it easier to reason about policies.\n\nCustom error messages\n=====================\n\nWhen an ``insert`` or ``update write`` violates an access policy, Gel will\nraise a generic ``AccessPolicyError``:\n\n.. code-block::\n\n    gel error: AccessPolicyError: access policy violation\n    on insert of <type>\n\n.. note::\n\n    Restricted access is represented either as an error message or an empty\n    set, depending on the filtering order of the operation. The operations\n    ``select``, ``delete``, or ``update read`` filter up front, and thus you\n    simply won't get the data that is being restricted. Other operations\n    (``insert`` and ``update write``) will return an error message.\n\nIf multiple policies are in effect, it can be helpful to define a distinct\n``errmessage`` in your policy:\n\n.. code-block:: sdl-diff\n\n      global current_user_id: uuid;\n      global current_user := (\n        select User filter .id = global current_user_id\n      );\n\n      type User {\n        required email: str { constraint exclusive; };\n        required is_admin: bool { default := false };\n\n        access policy admin_only\n          allow all\n    +     using (global current_user.is_admin ?? false) {\n    +       errmessage := 'Only admins may query Users'\n    +     };\n      }\n\n      type BlogPost {\n        required title: str;\n        author: User;\n\n        access policy author_has_full_access\n          allow all\n    +     using (global current_user ?= .author) {\n    +       errmessage := 'BlogPosts may only be queried by their authors'\n    +     };\n      }\n\nNow if you attempt, for example, a ``User`` insert as a non-admin user, you\nwill receive this error:\n\n.. code-block::\n\n    gel error: AccessPolicyError: access policy violation on insert of\n    default::User (Only admins may query Users)\n\n\nDisabling policies\n==================\n\n.. api-index:: apply_access_policies\n\nYou may disable all access policies by setting the ``apply_access_policies``\n:ref:`configuration parameter <ref_std_cfg>` to ``false``.\n\nYou may also temporarily disable access policies using the Gel UI configuration\ncheckbox (or via :gelcmd:`ui`), which only applies to your UI session.\n\nMore examples\n=============\n\nHere are some additional patterns:\n\n1. Publicly visible blog posts, only writable by the author:\n\n   .. code-block:: sdl-diff\n\n         global current_user: uuid;\n\n         type User {\n           required email: str { constraint exclusive; }\n         }\n\n         type BlogPost {\n           required title: str;\n           required author: User;\n       +   required published: bool { default := false };\n\n           access policy author_has_full_access\n             allow all\n             using (global current_user ?= .author.id);\n       +   access policy visible_if_published\n       +     allow select\n       +     using (.published);\n         }\n\n2. Visible to friends, only modifiable by the author:\n\n   .. code-block:: sdl-diff\n\n         global current_user: uuid;\n\n         type User {\n           required email: str { constraint exclusive; }\n       +   multi friends: User;\n         }\n\n         type BlogPost {\n           required title: str;\n           required author: User;\n\n           access policy author_has_full_access\n             allow all\n             using (global current_user ?= .author.id);\n       +   access policy friends_can_read\n       +     allow select\n       +     using ((global current_user in .author.friends.id) ?? false);\n         }\n\n3. Publicly visible except to those blocked by the author:\n\n   .. code-block:: sdl-diff\n\n         type User {\n           required email: str { constraint exclusive; }\n       +   multi blocked: User;\n         }\n\n         type BlogPost {\n           required title: str;\n           required author: User;\n\n           access policy author_has_full_access\n             allow all\n             using (global current_user ?= .author.id);\n       +   access policy anyone_can_read\n       +     allow select;\n       +   access policy exclude_blocked\n       +     deny select\n       +     using ((global current_user in .author.blocked.id) ?? false);\n         }\n\n4. \"Disappearing\" posts that become invisible after 24 hours:\n\n   .. code-block:: sdl-diff\n\n         type User {\n           required email: str { constraint exclusive; }\n         }\n\n         type BlogPost {\n           required title: str;\n           required author: User;\n       +   required created_at: datetime {\n       +     default := datetime_of_statement() # non-volatile\n       +   }\n\n           access policy author_has_full_access\n             allow all\n             using (global current_user ?= .author.id);\n       +   access policy hide_after_24hrs\n       +     allow select\n       +     using (\n       +       datetime_of_statement() - .created_at < <duration>'24 hours'\n       +     );\n           }\n\nSuper constraints\n=================\n\nAccess policies can act like \"super constraints.\" For instance, a policy on\n``insert`` or ``update write`` can do a post-write validity check, rejecting\nthe operation if a certain condition is not met.\n\nE.g. here's a policy that limits the number of blog posts a\n``User`` can post:\n\n.. code-block:: sdl-diff\n\n      type User {\n        required email: str { constraint exclusive; }\n    +   multi posts := .<author[is BlogPost]\n      }\n\n      type BlogPost {\n        required title: str;\n        required author: User;\n\n        access policy author_has_full_access\n          allow all\n          using (global current_user ?= .author.id);\n    +   access policy max_posts_limit\n    +     deny insert\n    +     using (count(.author.posts) > 500);\n      }\n\n.. _ref_eql_sdl_access_policies:\n.. _ref_eql_sdl_access_policies_syntax:\n\nDeclaring access policies\n=========================\n\n.. api-index:: access policy, when, allow, deny, all, select, insert, delete,\n               update, update read, update write, using, errmessage\n\nThis section describes the syntax to declare access policies in your schema.\n\nSyntax\n------\n\n.. sdl:synopsis::\n\n    access policy <name>\n      [ when (<condition>) ]\n      { allow | deny } <action> [, <action> ... ]\n      [ using (<expr>) ]\n      [ \"{\"\n         [ errmessage := value ; ]\n         [ <annotation-declarations> ]\n        \"}\" ] ;\n\n    # where <action> is one of\n    all\n    select\n    insert\n    delete\n    update [{ read | write }]\n\nWhere:\n\n:eql:synopsis:`<name>`\n    The name of the access policy.\n\n:eql:synopsis:`when (<condition>)`\n    Specifies which objects this policy applies to. The\n    :eql:synopsis:`<condition>` has to be a :eql:type:`bool` expression.\n\n    When omitted, it is assumed that this policy applies to all objects of a\n    given type.\n\n:eql:synopsis:`allow`\n    Indicates that qualifying objects should allow access under this policy.\n\n:eql:synopsis:`deny`\n    Indicates that qualifying objects should *not* allow access under this\n    policy. This flavor supersedes any :eql:synopsis:`allow` policy and can\n    be used to selectively deny access to a subset of objects that otherwise\n    explicitly allows accessing them.\n\n:eql:synopsis:`all`\n    Apply the policy to all actions. It is exactly equivalent to listing\n    :eql:synopsis:`select`, :eql:synopsis:`insert`, :eql:synopsis:`delete`,\n    :eql:synopsis:`update` actions explicitly.\n\n:eql:synopsis:`select`\n    Apply the policy to all selection queries. Note that any object that\n    cannot be selected, cannot be modified either. This makes\n    :eql:synopsis:`select` the most basic \"visibility\" policy.\n\n:eql:synopsis:`insert`\n    Apply the policy to all inserted objects. If a newly inserted object would\n    violate this policy, an error is produced instead.\n\n:eql:synopsis:`delete`\n    Apply the policy to all objects about to be deleted. If an object does not\n    allow access under this kind of policy, it is not going to be considered\n    by any :eql:stmt:`delete` command.\n\n    Note that any object that cannot be selected, cannot be modified either.\n\n:eql:synopsis:`update read`\n    Apply the policy to all objects selected for an update. If an object does\n    not allow access under this kind of policy, it is not visible cannot be\n    updated.\n\n    Note that any object that cannot be selected, cannot be modified either.\n\n:eql:synopsis:`update write`\n    Apply the policy to all objects at the end of an update. If an updated\n    object violates this policy, an error is produced instead.\n\n    Note that any object that cannot be selected, cannot be modified either.\n\n:eql:synopsis:`update`\n    This is just a shorthand for :eql:synopsis:`update read` and\n    :eql:synopsis:`update write`.\n\n    Note that any object that cannot be selected, cannot be modified either.\n\n:eql:synopsis:`using <expr>`\n    Specifies what the policy is with respect to a given eligible (based on\n    :eql:synopsis:`when` clause) object. The :eql:synopsis:`<expr>` has to be\n    a :eql:type:`bool` expression. The specific meaning of this value also\n    depends on whether this policy flavor is :eql:synopsis:`allow` or\n    :eql:synopsis:`deny`.\n\n    The expression must be :ref:`Stable <ref_reference_volatility>`.\n\n    When omitted, it is assumed that this policy applies to all eligible\n    objects of a given type.\n\n:eql:synopsis:`set errmessage := <value>`\n    Set a custom error message of :eql:synopsis:`<value>` that is displayed\n    when this access policy prevents a write action.\n\n:sdl:synopsis:`<annotation-declarations>`\n    Set access policy :ref:`annotation <ref_eql_sdl_annotations>`\n    to a given *value*.\n\nAny sub-type extending a type inherits all of its access policies.\nYou can define additional access policies on sub-types.\n\n\n.. _ref_eql_ddl_access_policies:\n\nDDL commands\n============\n\nThis section describes the low-level DDL commands for creating, altering, and\ndropping access policies. You typically don't need to use these commands\ndirectly, but knowing about them is useful for reviewing migrations.\n\nCreate access policy\n--------------------\n\n:eql-statement:\n\nDefine a new object access policy on a type:\n\n.. eql:synopsis::\n\n    [ with <with-item> [, ...] ]\n    { create | alter } type <TypeName> \"{\"\n      [ ... ]\n      create access policy <name>\n        [ when (<condition>) ; ]\n        { allow | deny } action [, action ... ; ]\n        [ using (<expr>) ; ]\n        [ \"{\"\n           [ set errmessage := value ; ]\n           [ create annotation <annotation-name> := value ; ]\n          \"}\" ]\n    \"}\"\n\n    # where <action> is one of\n    all\n    select\n    insert\n    delete\n    update [{ read | write }]\n\nSee the meaning of each parameter in the `Declaring access policies`_ section.\n\nThe following subcommands are allowed in the ``create access policy`` block:\n\n:eql:synopsis:`set errmessage := <value>`\n    Set a custom error message of :eql:synopsis:`<value>` that is displayed\n    when this access policy prevents a write action.\n\n:eql:synopsis:`create annotation <annotation-name> := <value>`\n    Set access policy annotation :eql:synopsis:`<annotation-name>` to\n    :eql:synopsis:`<value>`.\n\n    See :eql:stmt:`create annotation` for details.\n\n\nAlter access policy\n-------------------\n\n:eql-statement:\n\nModify an existing access policy:\n\n.. eql:synopsis::\n\n    [ with <with-item> [, ...] ]\n    alter type <TypeName> \"{\"\n      [ ... ]\n      alter access policy <name> \"{\"\n        [ when (<condition>) ; ]\n        [ reset when ; ]\n        { allow | deny } <action> [, <action> ... ; ]\n        [ using (<expr>) ; ]\n        [ set errmessage := value ; ]\n        [ reset expression ; ]\n        [ create annotation <annotation-name> := <value> ; ]\n        [ alter annotation <annotation-name> := <value> ; ]\n        [ drop annotation <annotation-name>; ]\n      \"}\"\n    \"}\"\n\nYou can change the policy's condition, actions, or error message, or add/drop\nannotations.\n\nThe parameters describing the action policy are identical to the parameters\nused by ``create action policy``. There are a handful of additional\nsubcommands that are allowed in the ``alter access policy`` block:\n\n:eql:synopsis:`reset when`\n    Clear the :eql:synopsis:`when (<condition>)` so that the policy applies to\n    all objects of a given type. This is equivalent to ``when (true)``.\n\n:eql:synopsis:`reset expression`\n    Clear the :eql:synopsis:`using (<condition>)` so that the policy always\n    passes. This is equivalent to ``using (true)``.\n\n:eql:synopsis:`alter annotation <annotation-name>;`\n    Alter access policy annotation :eql:synopsis:`<annotation-name>`.\n    See :eql:stmt:`alter annotation` for details.\n\n:eql:synopsis:`drop annotation <annotation-name>;`\n    Remove access policy annotation :eql:synopsis:`<annotation-name>`.\n    See :eql:stmt:`drop annotation` for details.\n\n\nAll the subcommands allowed in the ``create access policy`` block are also\nvalid subcommands for ``alter access policy`` block.\n\nDrop access policy\n------------------\n\n:eql-statement:\n\nRemove an existing policy:\n\n.. eql:synopsis::\n\n    [ with <with-item> [, ...] ]\n    alter type <TypeName> \"{\"\n      [ ... ]\n      drop access policy <name> ;\n    \"}\"\n"
  },
  {
    "path": "docs/reference/datamodel/aliases.rst",
    "content": ".. _ref_datamodel_aliases:\n\n=======\nAliases\n=======\n\n.. index:: alias, virtual type\n\nYou can think of *aliases* as a way to give schema names to arbitrary EdgeQL\nexpressions. You can later refer to aliases in queries and in other aliases.\n\nAliases are functionally equivalent to expression aliases defined in EdgeQL\nstatements in :ref:`with block <ref_eql_statements_with>`, but are available\nto all queries using the schema and can be introspected.\n\nLike computed properties, the aliased expression is evaluated on the fly\nwhenever the alias is referenced.\n\n\nScalar alias\n============\n\n.. code-block:: sdl\n\n  # in your schema:\n  alias digits := {0,1,2,3,4,5,6,7,8,9};\n\nLater, in some query:\n\n.. code-block:: edgeql\n\n  select count(digits);\n\n\nObject type alias\n=================\n\nThe name of a given object type (e.g. ``User``) is itself a pointer to the *set\nof all User objects*. After declaring the alias below, you can use ``User`` and\n``UserAlias`` interchangeably:\n\n.. code-block:: sdl\n\n  alias UserAlias := User;\n\nObject type alias with computeds\n================================\n\nObject type aliases can include a *shape* that declares additional computed\nproperties or links:\n\n.. code-block:: sdl\n\n  type Post {\n    required title: str;\n  }\n\n  alias PostWithTrimmedTitle := Post {\n    trimmed_title := str_trim(.title)\n  }\n\nLater, in some query:\n\n.. code-block:: edgeql\n\n  select PostWithTrimmedTitle {\n    trimmed_title\n  };\n\nArbitrary expressions\n=====================\n\nAliases can correspond to any arbitrary EdgeQL expression, including entire\nqueries.\n\n.. code-block:: sdl\n\n  # Tuple alias\n  alias Color := (\"Purple\", 128, 0, 128);\n\n  # Named tuple alias\n  alias GameInfo := (\n    name := \"Li Europan Lingues\",\n    country := \"Iceland\",\n    date_published := 2023,\n    creators := (\n      (name := \"Bob Bobson\", age := 20),\n      (name := \"Trina Trinadóttir\", age := 25),\n    ),\n  );\n\n  type BlogPost {\n    required title: str;\n    required is_published: bool;\n  }\n\n  # Query alias\n  alias PublishedPosts := (\n    select BlogPost\n    filter .is_published = true\n  );\n\n.. note::\n\n  All aliases are reflected in the database's built-in :ref:`GraphQL schema\n  <ref_graphql_index>`.\n\n\n.. _ref_eql_sdl_aliases:\n.. _ref_eql_sdl_aliases_syntax:\n\nDefining aliases\n================\n\n.. api-index:: alias\n\nSyntax\n------\n\nDefine a new alias corresponding to the :ref:`more explicit DDL\ncommands <ref_eql_ddl_aliases>`.\n\n.. sdl:synopsis::\n\n  alias <alias-name> := <alias-expr> ;\n\n  alias <alias-name> \"{\"\n      using <alias-expr>;\n      [ <annotation-declarations> ]\n  \"}\" ;\n\nWhere:\n\n:eql:synopsis:`<alias-name>`\n  The name (optionally module-qualified) of an alias to be created.\n\n:eql:synopsis:`<alias-expr>`\n  The aliased expression.  Must be a :ref:`Stable <ref_reference_volatility>`\n  EdgeQL expression.\n\nThe valid SDL sub-declarations are listed below:\n\n:sdl:synopsis:`<annotation-declarations>`\n  Set alias :ref:`annotation <ref_eql_sdl_annotations>`\n  to a given *value*.\n\n\n.. _ref_eql_ddl_aliases:\n\nDDL commands\n============\n\nThis section describes the low-level DDL commands for creating and\ndropping aliases. You typically don't need to use these commands\ndirectly, but knowing about them is useful for reviewing migrations.\n\nCreate alias\n------------\n\n:eql-statement:\n:eql-haswith:\n\nDefine a new alias in the schema.\n\n.. eql:synopsis::\n\n  [ with <with-item> [, ...] ]\n  create alias <alias-name> := <alias-expr> ;\n\n  [ with <with-item> [, ...] ]\n  create alias <alias-name> \"{\"\n      using <alias-expr>;\n      [ create annotation <attr-name> := <attr-value>; ... ]\n  \"}\" ;\n\n  # where <with-item> is:\n\n  [ <module-alias> := ] module <module-name>\n\nParameters\n^^^^^^^^^^\n\nMost sub-commands and options of this command are identical to the\n:ref:`SDL alias declaration <ref_eql_sdl_aliases_syntax>`, with some\nadditional features listed below:\n\n:eql:synopsis:`[ <module-alias> := ] module <module-name>`\n  An optional list of module alias declarations to be used in the\n  alias definition.\n\n:eql:synopsis:`create annotation <annotation-name> := <value>;`\n  An optional list of annotation values for the alias.\n  See :eql:stmt:`create annotation` for details.\n\nExample\n^^^^^^^\n\nCreate a new alias:\n\n.. code-block:: edgeql\n\n  create alias Superusers := (\n      select User filter User.groups.name = 'Superusers'\n  );\n\n\nDrop alias\n----------\n\n:eql-statement:\n:eql-haswith:\n\nRemove an alias from the schema.\n\n.. eql:synopsis::\n\n  [ with <with-item> [, ...] ]\n  drop alias <alias-name> ;\n\nParameters\n^^^^^^^^^^\n\n*alias-name*\n  The name (optionally qualified with a module name) of an existing\n  expression alias.\n\nExample\n^^^^^^^\n\nRemove an alias:\n\n.. code-block:: edgeql\n\n  drop alias SuperUsers;\n"
  },
  {
    "path": "docs/reference/datamodel/annotations.rst",
    "content": ".. _ref_datamodel_annotations:\n.. _ref_eql_sdl_annotations:\n\n===========\nAnnotations\n===========\n\n*Annotations* are named values associated with schema items and are\ndesigned to hold arbitrary schema-level metadata represented as a\n:eql:type:`str` (unstructured text).\n\nUsers can store JSON-encoded data in annotations if they need to store\nmore complex metadata.\n\n\nStandard annotations\n====================\n\n.. api-index:: title, description, deprecated\n\nThere are a number of annotations defined in the standard library. The\nfollowing are the annotations which can be set on any schema item:\n\n- ``std::title``\n- ``std::description``\n- ``std::deprecated``\n\nFor example, consider the following declaration:\n\n.. code-block:: sdl\n\n  type Status {\n    annotation title := 'Activity status';\n    annotation description := 'All possible user activities';\n\n    required name: str {\n      constraint exclusive\n    }\n  }\n\nAnd the ``std::deprecated`` annotation can be used to mark deprecated items\n(e.g., :eql:func:`str_rpad`) and to provide some information such as what\nshould be used instead.\n\nUser-defined annotations\n========================\n\nTo declare a custom annotation type beyond the three built-ins, add an abstract\nannotation type to your schema. A custom annotation could be used to attach\narbitrary JSON-encoded data to your schema—potentially useful for introspection\nand code generation.\n\n.. code-block:: sdl\n\n  abstract annotation admin_note;\n\n  type Status {\n    annotation admin_note := 'system-critical';\n  }\n\n\n.. _ref_eql_sdl_annotations_syntax:\n\nDeclaring annotations\n=====================\n\n.. api-index:: abstract, inheritable, annotation\n\nThis section describes the syntax to use annotations in your schema.\n\nSyntax\n------\n\n.. sdl:synopsis::\n\n  # Abstract annotation form:\n  abstract [ inheritable ] annotation <name>\n  [ \"{\" <annotation-declarations>; [...] \"}\" ] ;\n\n  # Concrete annotation (same as <annotation-declarations>) form:\n  annotation <name> := <value> ;\n\nDescription\n^^^^^^^^^^^\n\nThere are two forms of annotation declarations: abstract and concrete.\nThe *abstract annotation* form is used for declaring new kinds of\nannotation in a module. The *concrete annotation* declarations are\nused as sub-declarations for all other declarations in order to\nactually annotate them.\n\nThe annotation declaration options are as follows:\n\n:eql:synopsis:`abstract`\n  If specified, the annotation will be *abstract*.\n\n:eql:synopsis:`inheritable`\n  If specified, the annotation will be *inheritable*. The\n  annotations are non-inheritable by default. That is, if a schema\n  item has an annotation defined on it, the descendants of that\n  schema item will not automatically inherit the annotation. Normal\n  inheritance behavior can be turned on by declaring the annotation\n  with the ``inheritable`` qualifier. This is only valid for *abstract\n  annotation*.\n\n:eql:synopsis:`<name>`\n  The name (optionally module-qualified) of the annotation.\n\n:eql:synopsis:`<value>`\n  Any string value that the specified annotation is intended to have\n  for the given context.\n\nThe only valid SDL sub-declarations are *concrete annotations*:\n\n:sdl:synopsis:`<annotation-declarations>`\n  Annotations can also have annotations. Set the *annotation* of the\n  enclosing annotation to a specific value.\n\n\n.. _ref_eql_ddl_annotations:\n\nDDL commands\n============\n\nThis section describes the low-level DDL commands for creating, altering,\nand dropping annotations and abstract annotations. You typically don't need to\nuse these commands directly, but knowing about them is useful for reviewing\nmigrations.\n\n\nCreate abstract annotation\n--------------------------\n\n:eql-statement:\n\nDefine a new annotation.\n\n.. eql:synopsis::\n\n  [ with <with-item> [, ...] ]\n  create abstract [ inheritable ] annotation <name>\n  [\n    \"{\"\n      create annotation <annotation-name> := <value> ;\n      [...]\n    \"}\"\n  ] ;\n\nDescription\n^^^^^^^^^^^\n\nThe command ``create abstract annotation`` defines a new annotation\nfor use in the current Gel database.\n\nIf *name* is qualified with a module name, then the annotation is created\nin that module, otherwise it is created in the current module.\nThe annotation name must be distinct from that of any existing schema item\nin the module.\n\nThe annotations are non-inheritable by default. That is, if a schema item\nhas an annotation defined on it, the descendants of that schema item will\nnot automatically inherit the annotation. Normal inheritance behavior can\nbe turned on by declaring the annotation with the ``inheritable`` qualifier.\n\nMost sub-commands and options of this command are identical to the\n:ref:`SDL annotation declaration <ref_eql_sdl_annotations_syntax>`.\nThere's only one subcommand that is allowed in the ``create\nannotation`` block:\n\n:eql:synopsis:`create annotation <annotation-name> := <value>`\n  Annotations can also have annotations. Set the\n  :eql:synopsis:`<annotation-name>` of the\n  enclosing annotation to a specific :eql:synopsis:`<value>`.\n  See :eql:stmt:`create annotation` for details.\n\nExample\n^^^^^^^\n\nDeclare an annotation ``extrainfo``:\n\n.. code-block:: edgeql\n\n  create abstract annotation extrainfo;\n\n\nAlter abstract annotation\n-------------------------\n\n:eql-statement:\n\nChange the definition of an annotation.\n\n.. eql:synopsis::\n\n  alter abstract annotation <name>\n  [ \"{\" ] <subcommand>; [...] [ \"}\" ];\n\n  # where <subcommand> is one of\n\n    rename to <newname>\n    create annotation <annotation-name> := <value>\n    alter annotation <annotation-name> := <value>\n    drop annotation <annotation-name>\n\nDescription\n^^^^^^^^^^^\n\n:eql:synopsis:`alter abstract annotation` changes the definition of an\nabstract annotation.\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`<name>`\n  The name (optionally module-qualified) of the annotation to alter.\n\nThe following subcommands are allowed in the ``alter abstract annotation``\nblock:\n\n:eql:synopsis:`rename to <newname>`\n  Change the name of the annotation to :eql:synopsis:`<newname>`.\n\n:eql:synopsis:`alter annotation <annotation-name> := <value>`\n  Annotations can also have annotations. Change\n  :eql:synopsis:`<annotation-name>` to a specific\n  :eql:synopsis:`<value>`. See :eql:stmt:`alter annotation` for\n  details.\n\n:eql:synopsis:`drop annotation <annotation-name>`\n  Annotations can also have annotations. Remove annotation\n  :eql:synopsis:`<annotation-name>`.\n  See :eql:stmt:`drop annotation` for details.\n\nAll the subcommands allowed in the ``create abstract annotation``\nblock are also valid subcommands for ``alter annotation`` block.\n\nExample\n^^^^^^^\n\nRename an annotation:\n\n.. code-block:: edgeql\n\n  alter abstract annotation extrainfo\n    rename to extra_info;\n\n\nDrop abstract annotation\n------------------------\n\n:eql-statement:\n\nDrop a schema annotation.\n\n.. eql:synopsis::\n\n  [ with <with-item> [, ...] ]\n  drop abstract annotation <name> ;\n\nDescription\n^^^^^^^^^^^\n\nThe command ``drop abstract annotation`` removes an existing schema\nannotation from the database schema. Note that the ``inheritable``\nqualifier is not necessary in this statement.\n\nExample\n^^^^^^^\n\nDrop the annotation ``extra_info``:\n\n.. code-block:: edgeql\n\n  drop abstract annotation extra_info;\n\n\nCreate annotation\n-----------------\n\n:eql-statement:\n\nDefine an annotation value for a given schema item.\n\n.. eql:synopsis::\n\n  create annotation <annotation-name> := <value>\n\nDescription\n^^^^^^^^^^^\n\nThe command ``create annotation`` defines an annotation for a schema item.\n\n:eql:synopsis:`<annotation-name>` refers to the name of a defined annotation,\nand :eql:synopsis:`<value>` must be a constant EdgeQL expression\nevaluating into a string.\n\nThis statement can only be used as a subcommand in another\nDDL statement.\n\nExample\n^^^^^^^\n\nCreate an object type ``User`` and set its ``title`` annotation to\n``\"User type\"``.\n\n.. code-block:: edgeql\n\n  create type User {\n    create annotation title := \"User type\";\n  };\n\n\nAlter annotation\n----------------\n\n:eql-statement:\n\nAlter an annotation value for a given schema item.\n\n.. eql:synopsis::\n\n  alter annotation <annotation-name> := <value>\n\nDescription\n^^^^^^^^^^^\n\nThe command ``alter annotation`` alters an annotation value on a schema item.\n\n:eql:synopsis:`<annotation-name>` refers to the name of a defined annotation,\nand :eql:synopsis:`<value>` must be a constant EdgeQL expression\nevaluating into a string.\n\nThis statement can only be used as a subcommand in another\nDDL statement.\n\nExample\n^^^^^^^\n\nAlter an object type ``User`` and alter the value of its previously set\n``title`` annotation to ``\"User type\"``.\n\n.. code-block:: edgeql\n\n  alter type User {\n    alter annotation title := \"User type\";\n  };\n\n\nDrop annotation\n---------------\n\n:eql-statement:\n\nRemove an annotation from a given schema item.\n\n.. eql:synopsis::\n\n  drop annotation <annotation-name> ;\n\nDescription\n^^^^^^^^^^^\n\nThe command ``drop annotation`` removes an annotation value from a schema item.\n\n:eql:synopsis:`<annotaion_name>` refers to the name of a defined annotation.\nThe annotation value does not have to exist on a schema item.\n\nThis statement can only be used as a subcommand in another\nDDL statement.\n\nExample\n^^^^^^^\n\nDrop the ``title`` annotation from the ``User`` object type:\n\n.. code-block:: edgeql\n\n  alter type User {\n    drop annotation title;\n  };\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Cheatsheets > Annotations <ref_cheatsheet_annotations>`\n  * - :ref:`Introspection > Object types <ref_datamodel_introspection_object_types>`\n"
  },
  {
    "path": "docs/reference/datamodel/branches.rst",
    "content": ".. _ref_datamodel_branches:\n.. _ref_datamodel_databases:\n\n.. versionadded:: 5.0\n\n========\nBranches\n========\n\nGel's |branches| are equivalent to PostgreSQL's *databases* and map to\nthem directly. Gel comes with tooling to help manage branches and build\na development workflow around them. E.g. when developing locally you can\nmap your Gel branches to your Git branches, and when using Gel Cloud and\nGitHub you can have a branch per PR.\n\n\nCLI commands\n============\n\nRefer to the :ref:`gel branch <ref_cli_gel_branch>` command group for\ndetails on the CLI commands for managing branches.\n\n\n.. _ref_admin_branches:\n\nDDL commands\n============\n\nThese are low-level commands that are used to create, alter, and drop branches.\nYou can use them when experimenting in REPL, of if you want to create your own\ntools to manage Gel branches.\n\n\nCreate empty branch\n-------------------\n\n:eql-statement:\n\nCreate a new branch without schema or data.\n\n.. eql:synopsis::\n\n    create empty branch <name> ;\n\nDescription\n^^^^^^^^^^^\n\nThe command ``create empty branch`` creates a new Gel branch without schema\nor data, aside from standard schemas.\n\nExample\n^^^^^^^\n\nCreate a new empty branch:\n\n.. code-block:: edgeql\n\n    create empty branch newbranch;\n\n\nCreate schema branch\n--------------------\n\n:eql-statement:\n\nCreate a new branch copying the schema (without data)of an existing branch.\n\n.. eql:synopsis::\n\n    create schema branch <newbranch> from <oldbranch> ;\n\nDescription\n^^^^^^^^^^^\n\nThe command ``create schema branch`` creates a new Gel branch with schema\ncopied from an already existing branch.\n\nExample\n^^^^^^^\n\nCreate a new schema branch:\n\n.. code-block:: edgeql\n\n    create schema branch feature from main;\n\n\nCreate data branch\n------------------\n\n:eql-statement:\n\nCreate a new branch copying the schema and data of an existing branch.\n\n.. eql:synopsis::\n\n    create data branch <newbranch> from <oldbranch> ;\n\nDescription\n^^^^^^^^^^^\n\nThe command ``create data branch`` creates a new Gel branch with schema and\ndata copied from an already existing branch.\n\nExample\n^^^^^^^\n\nCreate a new data branch:\n\n.. code-block:: edgeql\n\n    create data branch feature from main;\n\n\nDrop branch\n-----------\n\n:eql-statement:\n\nRemove a branch.\n\n.. eql:synopsis::\n\n    drop branch <name> ;\n\nDescription\n^^^^^^^^^^^\n\nThe command ``drop branch`` removes an existing branch. It cannot be executed\nwhile there are existing connections to the target branch.\n\n.. warning::\n\n    Executing ``drop branch`` removes data permanently and cannot be undone.\n\nExample\n^^^^^^^\n\nRemove a branch:\n\n.. code-block:: edgeql\n\n    drop branch appdb;\n\n\nAlter branch\n------------\n\n:eql-statement:\n\nRename a branch.\n\n.. eql:synopsis::\n\n    alter branch <oldname> rename to <newname> ;\n\nDescription\n^^^^^^^^^^^\n\nThe command ``alter branch … rename`` changes the name of an existing branch.\nIt cannot be executed while there are existing connections to the target\nbranch.\n\nExample\n^^^^^^^\n\nRename a branch:\n\n.. code-block:: edgeql\n\n    alter branch featuer rename to feature;\n\nDatabase (deprecated)\n=====================\n\nVersions of Gel prior to 5.0 used the term *database* to refer to branches.\n\nCreate database\n---------------\n\n:eql-statement:\n\nCreate a new database.\n\n.. eql:synopsis::\n\n    create database <name> ;\n\nDescription\n^^^^^^^^^^^\n\nThe command ``create database`` creates a new Gel database.\n\nThe new database will be created with all standard schemas prepopulated.\n\nExamples\n^^^^^^^^\n\nCreate a new database:\n\n.. code-block:: edgeql\n\n    create database appdb;\n\n\nDrop database\n-------------\n\n:eql-statement:\n\nRemove a database.\n\n.. eql:synopsis::\n\n    drop database <name> ;\n\nDescription\n^^^^^^^^^^^\n\nThe command ``drop database`` removes an existing database.  It cannot\nbe executed while there are existing connections to the target\ndatabase.\n\n.. warning::\n\n    Executing ``drop database`` removes data permanently and cannot be undone.\n\nExamples\n^^^^^^^^\n\nRemove a database:\n\n.. code-block:: edgeql\n\n    drop database appdb;"
  },
  {
    "path": "docs/reference/datamodel/comparison.rst",
    "content": ".. _ref_datamodel_comparison:\n\n===============\nvs SQL and ORMs\n===============\n\n|Gel's| approach to schema modeling builds upon the foundation of SQL while\ntaking cues from modern tools like ORM libraries. Let's see how it stacks up.\n\n.. _ref_datamodel_sql_comparison:\n\nComparison to SQL\n-----------------\n\nWhen using SQL databases, there's no convenient representation of the schema.\nInstead, the schema only exists as a series of ``{CREATE|ALTER|DELETE} {TABLE|\nCOLUMN}`` commands, usually spread across several SQL migration scripts.\nThere's no simple way to see the current state of your schema at a glance.\n\nMoreover, SQL stores data in a *relational* way. Connections between tables are\nrepresented with foreign key constraints and ``JOIN`` operations are required\nto query across tables.\n\n.. code-block::\n\n  CREATE TABLE people (\n    id            uuid  PRIMARY KEY,\n    name          text,\n  );\n  CREATE TABLE movies (\n    id            uuid  PRIMARY KEY,\n    title         text,\n    director_id   uuid  REFERENCES people(id)\n  );\n\nIn |Gel|, connections between tables are represented with :ref:`Links\n<ref_datamodel_links>`.\n\n.. code-block:: sdl\n\n    type Movie {\n      required title: str;\n      required director: Person;\n    }\n\n    type Person {\n      required name: str;\n    }\n\nThis approach makes it simple to write queries that traverse this link, no\nJOINs required.\n\n.. code-block:: edgeql\n\n  select Movie {\n    title,\n    director: {\n      name\n    }\n  }\n\n.. _ref_datamodel_orm_comparison:\n\nComparison to ORMs\n------------------\n\nObject-relational mapping libraries are popular for a reason. They provide a\nway to model your schema and write queries in a way that feels natural in the\ncontext of modern, object-oriented programming languages. But ORMs have\ndownsides too.\n\n- **Lock-in**. Your schema is strongly coupled to the ORM library you are\n  using. More generally, this also locks you into using a particular\n  programming language.\n- Most ORMs have more **limited querying capabilities** than the query\n  languages they abstract.\n- Many ORMs produce **suboptimal queries** that can have serious performance\n  implications.\n- **Migrations** can be difficult. Since most ORMs aim to be the single source\n  of truth for your schema, they necessarily must provide some sort of\n  migration tool. These migration tools are maintained by the contributors to\n  the ORM library, not the maintainers of the database itself. Quality control\n  and long-term maintenance is not always guaranteed.\n\nFrom the beginning, Gel was designed to incorporate the best aspects of ORMs\n— declarative modeling, object-oriented APIs, and intuitive querying —\nwithout the drawbacks.\n"
  },
  {
    "path": "docs/reference/datamodel/computeds.rst",
    "content": ".. _ref_datamodel_computed:\n\n=========\nComputeds\n=========\n\n:edb-alt-title: Computed properties and links\n\n.. api-index:: :=\n\n.. important::\n\n  This section assumes a basic understanding of EdgeQL. If you aren't familiar\n  with it, feel free to skip this page for now.\n\nObject types can contain *computed* properties and links. Computed properties\nand links are not persisted in the database. Instead, they are evaluated *on\nthe fly* whenever that field is queried. Computed properties must be declared\nwith the ``property`` keyword and computed links must be declared with the\n``link`` keyword in |EdgeDB| versions prior to 4.0.\n\n\n.. code-block:: sdl\n\n    type Person {\n      name: str;\n      all_caps_name := str_upper(__source__.name);\n    }\n\nComputed fields are associated with an EdgeQL expression. This expression\ncan be an *arbitrary* EdgeQL query. This expression is evaluated whenever the\nfield is referenced in a query.\n\n.. note::\n\n  Computed fields don't need to be pre-defined in your schema; you can drop\n  them into individual queries as well. They behave in exactly the same way.\n  For more information, see the :ref:`EdgeQL > Select > Computeds\n  <ref_eql_select_computeds>`.\n\n.. warning::\n\n  :ref:`Volatile and modifying <ref_reference_volatility>` expressions are not\n  allowed in computed properties defined in schema. This means that, for\n  example, your schema-defined computed property cannot call\n  :eql:func:`datetime_current`, but it *can* call\n  :eql:func:`datetime_of_transaction` or :eql:func:`datetime_of_statement`.\n  This does *not* apply to computed properties outside of schema.\n\n.. _ref_dot_notation:\n\nLeading dot notation\n--------------------\n\n.. api-index:: __source__\n\nThe example above used the special keyword ``__source__`` to refer to the\ncurrent object; it's analogous to ``this`` or ``self``  in many object-oriented\nlanguages.\n\nHowever, explicitly using ``__source__`` is optional here; inside the scope of\nan object type declaration, you can omit it entirely and use the ``.<name>``\nshorthand.\n\n.. code-block:: sdl\n\n    type Person {\n      first_name: str;\n      last_name: str;\n      full_name := .first_name ++ ' ' ++ .last_name;\n    }\n\nType and cardinality inference\n------------------------------\n\nThe type and cardinality of a computed field is *inferred* from the expression.\nThere's no need for the modifier keywords you use for non-computed fields (like\n``multi`` and ``required``). However, it's common to specify them anyway; it\nmakes the schema more readable and acts as a sanity check: if the provided\nEdgeQL expression disagrees with the modifiers, an error will be thrown the\nnext time you try to :ref:`create a migration <ref_intro_migrations>`.\n\n.. code-block:: sdl\n\n    type Person {\n      first_name: str;\n\n      # this is invalid, because first_name is not a required property\n      required first_name_upper := str_upper(.first_name);\n    }\n\nCommon use cases\n----------------\n\nFiltering\n^^^^^^^^^\n\nIf you find yourself writing the same ``filter`` expression repeatedly in\nqueries, consider defining a computed field that encapsulates the filter.\n\n.. code-block:: sdl\n\n    type Club {\n      multi members: Person;\n      multi active_members := (\n        select .members filter .is_active = true\n      )\n    }\n\n    type Person {\n      name: str;\n      is_active: bool;\n    }\n\n.. _ref_datamodel_links_backlinks:\n\nBacklinks\n^^^^^^^^^\n\nBacklinks are one of the most common use cases for computed links. In |Gel|\nlinks are *directional*; they have a source and a target. Often it's convenient\nto traverse a link in the *reverse* direction.\n\n.. code-block:: sdl\n\n    type BlogPost {\n      title: str;\n      author: User;\n    }\n\n    type User {\n      name: str;\n      multi blog_posts := .<author[is BlogPost]\n    }\n\nThe ``User.blog_posts`` expression above uses the *backlink operator* ``.<`` in\nconjunction with a *type filter* ``[is BlogPost]`` to fetch all the\n``BlogPosts`` associated with a given ``User``. For details on this syntax, see\nthe EdgeQL docs for :ref:`Backlinks <ref_eql_paths_backlinks>`.\n\n\n.. list-table::\n  :class: seealso\n\n  * - :ref:`SDL > Links <ref_eql_sdl_links>`\n  * - :ref:`DDL > Links <ref_eql_ddl_links>`\n  * - :ref:`SDL > Properties <ref_eql_sdl_links>`\n  * - :ref:`DDL > Properties <ref_eql_ddl_links>`\n"
  },
  {
    "path": "docs/reference/datamodel/constraints.rst",
    "content": ".. _ref_datamodel_constraints:\n.. _ref_eql_sdl_constraints:\n\n===========\nConstraints\n===========\n\n.. index:: validation\n\nConstraints give users fine-grained control to ensure data consistency.\nThey can be defined on :ref:`properties <ref_datamodel_props>`,\n:ref:`links<ref_datamodel_links>`,\n:ref:`object types <ref_datamodel_object_types>`,\nand :ref:`custom scalars <ref_datamodel_links>`.\n\n\n.. _ref_datamodel_constraints_builtin:\n\nStandard constraints\n====================\n\n.. api-index:: exclusive, expression on, one_of, max_value, max_ex_value,\n               min_value, min_ex_value, max_len_value, min_len_value, regexp\n\n|Gel| includes a number of standard ready-to-use constraints:\n\n.. include:: ../stdlib/constraint_table.rst\n\n\nConstraints on properties\n=========================\n\nExample: enforce all ``User`` objects to have a unique ``username``\nno longer than 25 characters:\n\n.. code-block:: sdl\n\n  type User {\n    required username: str {\n      # usernames must be unique\n      constraint exclusive;\n\n      # max length (built-in)\n      constraint max_len_value(25);\n    };\n  }\n\n.. _ref_datamodel_constraints_objects:\n\nConstraints on object types\n===========================\n\n.. api-index:: __subject__\n\nConstraints can be defined on object types. This is useful when the\nconstraint logic must reference multiple links or properties.\n\nExample: enforce that the magnitude of ``ConstrainedVector`` objects\nis no more than 5\n\n.. code-block:: sdl\n\n  type ConstrainedVector {\n    required x: float64;\n    required y: float64;\n\n    constraint expression on (\n      (.x ^ 2 + .y ^ 2) ^ 0.5 <= 5\n      # or, long form: `(__subject__.x + __subject__.y) ^ 0.5 <= 5`\n    );\n  }\n\nThe ``expression`` constraint is used here to define custom constraint logic.\nInside constraints, the keyword ``__subject__`` can be used to reference the\n*value* being constrained.\n\n.. note::\n   Note that inside an object type declaration, you can omit ``__subject__``\n   and simply refer to properties with the\n   :ref:`leading dot notation <ref_dot_notation>` (e.g. ``.property``).\n\n.. note::\n\n   Also note that the constraint expression are fairly restricted. Due\n   to how constraints are implemented, you can only reference ``single``\n   (non-multi) properties and links defined on the object type:\n\n   .. code-block:: sdl\n\n     # Not valid!\n     type User {\n       required username: str;\n       multi friends: User;\n\n       # ❌ constraints cannot contain paths with more than one hop\n       constraint expression on ('bob' in .friends.username);\n     }\n\nAbstract constraints\n====================\n\n.. api-index:: abstract constraint\n\nYou can re-use constraints across multiple object types by declaring them as\nabstract constraints. Example:\n\n.. code-block:: sdl\n\n    abstract constraint min_value(min: anytype) {\n        errmessage :=\n          'Minimum allowed value for {__subject__} is {min}.';\n\n        using (__subject__ >= min);\n    }\n\n    # use it like this:\n\n    scalar type posint64 extending int64 {\n        constraint min_value(0);\n    }\n\n    # or like this:\n\n    type User {\n      required age: int16 {\n        constraint min_value(12);\n      };\n    }\n\n\nComputed constraints\n====================\n\nConstraints can be defined on computed properties:\n\n.. code-block:: sdl\n\n  type User {\n    required username: str;\n    required clean_username := str_trim(str_lower(.username));\n\n    constraint exclusive on (.clean_username);\n  }\n\n\nComposite constraints\n=====================\n\n.. api-index:: constraint exclusive on\n\nTo define a composite constraint, create an ``exclusive`` constraint on a\ntuple of properties or links.\n\n.. code-block:: sdl\n\n  type User {\n    username: str;\n  }\n\n  type BlogPost {\n    title: str;\n\n    author: User;\n\n    constraint exclusive on ((.title, .author));\n  }\n\n.. _ref_datamodel_constraints_partial:\n\nPartial constraints\n===================\n\n.. api-index:: constraint exclusive on, except\n\nConstraints on object types can be made partial, so that they are not enforced\nwhen the specified ``except`` condition is met.\n\n.. code-block:: sdl\n\n  type User {\n    required username: str;\n    deleted: bool;\n\n    # Usernames must be unique unless marked deleted\n    constraint exclusive on (.username) except (.deleted);\n  }\n\n\nConstraints on links\n====================\n\nYou can constrain links such that a given object can only be linked once by\nusing :eql:constraint:`exclusive`:\n\n.. code-block:: sdl\n\n  type User {\n    required name: str;\n\n    # Make sure none of the \"owned\" items belong\n    # to any other user.\n    multi owns: Item {\n      constraint exclusive;\n    }\n  }\n\n\nLink property constraints\n=========================\n\nYou can also add constraints for :ref:`link properties\n<ref_datamodel_link_properties>`:\n\n.. code-block:: sdl\n\n  type User {\n    name: str;\n\n    multi friends: User {\n      strength: float64;\n\n      constraint expression on (\n        @strength >= 0\n      );\n    }\n  }\n\n\nLink's \"@source\" and \"@target\"\n==============================\n\n.. api-index:: @source, @target\n\nYou can create a composite exclusive constraint on the object linking/linked\n*and* a link property by using ``@source`` or ``@target`` respectively. Here's\na schema for a library book management app that tracks books and who has\nchecked them out:\n\n.. code-block:: sdl\n\n  type Book {\n    required title: str;\n  }\n\n  type User {\n    name: str;\n    multi checked_out: Book {\n      date: cal::local_date;\n\n      # Ensures a given Book can be checked out\n      # only once on a given day.\n      constraint exclusive on ((@target, @date));\n    }\n  }\n\nHere, the constraint ensures that no book can be checked out to two ``User``\\s\non the same ``@date``.\n\nIn this example demonstrating ``@source``, we've created a schema to track\nplayer picks in a color-based memory game:\n\n.. code-block:: sdl\n\n  type Player {\n    required name: str;\n\n    multi picks: Color {\n      order: int16;\n\n      constraint exclusive on ((@source, @order));\n    }\n  }\n\n  type Color {\n    required name: str;\n  }\n\nThis constraint ensures that a single ``Player`` cannot pick two ``Color``\\s at\nthe same ``@order``.\n\n\nConstraints on custom scalars\n=============================\n\nCustom scalar types can be constrained.\n\n.. code-block:: sdl\n\n  scalar type username extending str {\n    constraint regexp(r'^[A-Za-z0-9_]{4,20}$');\n  }\n\nNote: you can't use :eql:constraint:`exclusive` constraints on custom scalar\ntypes, as the concept of exclusivity is only defined in the context of a given\nobject type.\n\nUse :eql:constraint:`expression` constraints to declare custom constraints\nusing arbitrary EdgeQL expressions. The example below uses the built-in\n:eql:func:`str_trim` function.\n\n.. code-block:: sdl\n\n  scalar type title extending str {\n    constraint expression on (\n      __subject__ = str_trim(__subject__)\n    );\n  }\n\n\nConstraints and inheritance\n===========================\n\n.. api-index:: delegated constraint\n\nIf you define a constraint on a type and then extend that type, the constraint\nwill *not* be applied individually to each extending type. Instead, it will\napply globally across all the types that inherited the constraint.\n\n.. code-block:: sdl\n\n  type User {\n    required name: str {\n      constraint exclusive;\n    }\n  }\n  type Administrator extending User;\n  type Moderator extending User;\n\n.. code-block:: edgeql-repl\n\n  gel> insert Administrator {\n  ....  name := 'Jan'\n  .... };\n  {default::Administrator {id: 7aeaa146-f5a5-11ed-a598-53ddff476532}}\n\n  gel> insert Moderator {\n  ....  name := 'Jan'\n  .... };\n  gel error: ConstraintViolationError: name violates exclusivity constraint\n    Detail: value of property 'name' of object type 'default::Moderator'\n    violates exclusivity constraint\n\n  gel> insert User {\n  ....  name := 'Jan'\n  .... };\n  gel error: ConstraintViolationError: name violates exclusivity constraint\n    Detail: value of property 'name' of object type 'default::User'\n    violates exclusivity constraint\n\nAs this example demonstrates, if an object of one extending type has a value\nfor a property that is exclusive, an object of a *different* extending type\ncannot have the same value.\n\nIf that's not what you want, you can instead delegate the constraint to the\ninheriting types by prepending the ``delegated`` keyword to the constraint.\nThe constraint would then be applied just as if it were declared individually\non each of the inheriting types.\n\n.. code-block:: sdl\n\n  type User {\n    required name: str {\n      delegated constraint exclusive;\n    }\n  }\n  type Administrator extending User;\n  type Moderator extending User;\n\n.. code-block:: edgeql-repl\n\n  gel> insert Administrator {\n  ....  name := 'Jan'\n  .... };\n  {default::Administrator {id: 7aeaa146-f5a5-11ed-a598-53ddff476532}}\n\n  gel> insert User {\n  ....  name := 'Jan'\n  .... };\n  {default::User {id: a6e3fdaf-c44b-4080-b39f-6a07496de66b}}\n\n  gel> insert Moderator {\n  ....  name := 'Jan'\n  .... };\n  {default::Moderator {id: d3012a3f-0f16-40a8-8884-7203f393b63d}}\n\n  gel> insert Moderator {\n  ....  name := 'Jan'\n  .... };\n  gel error: ConstraintViolationError: name violates exclusivity constraint\n    Detail: value of property 'name' of object type 'default::Moderator'\n    violates exclusivity constraint\n\nWith the addition of ``delegated`` to the constraints, the inserts were\nsuccessful for each of the types. We did not hit a constraint violation\nuntil we tried to insert a second ``Moderator`` object with the same\nname as the existing one.\n\n\n.. _ref_eql_sdl_constraints_syntax:\n\nDeclaring constraints\n=====================\n\nThis section describes the syntax to declare constraints in your schema.\n\nSyntax\n------\n\n.. sdl:synopsis::\n\n  [{abstract | delegated}] constraint <name> [ ( [<argspec>] [, ...] ) ]\n      [ on ( <subject-expr> ) ]\n      [ except ( <except-expr> ) ]\n      [ extending <base> [, ...] ]\n  \"{\"\n      [ using <constr-expression> ; ]\n      [ errmessage := <error-message> ; ]\n      [ <annotation-declarations> ]\n      [ ... ]\n  \"}\" ;\n\n  # where <argspec> is:\n\n  [ <argname>: ] {<argtype> | <argvalue>}\n\nDescription\n^^^^^^^^^^^\n\nThis declaration defines a new constraint with the following options:\n\n:eql:synopsis:`abstract`\n  If specified, the constraint will be *abstract*.\n\n:eql:synopsis:`delegated`\n  If specified, the constraint is defined as *delegated*, which means\n  that it will not be enforced on the type it's declared on, and the\n  enforcement will be delegated to the subtypes of this type.\n  This is particularly useful for :eql:constraint:`exclusive`\n  constraints in abstract types. This is only valid for *concrete\n  constraints*.\n\n:eql:synopsis:`<name>`\n  The name (optionally module-qualified) of the new constraint.\n\n:eql:synopsis:`<argspec>`\n  An optional list of constraint arguments.\n\n  For an *abstract constraint* :eql:synopsis:`<argname>` optionally\n  specifies the argument name and :eql:synopsis:`<argtype>` specifies\n  the argument type.\n\n  For a *concrete constraint* :eql:synopsis:`<argname>` optionally\n  specifies the argument name and :eql:synopsis:`<argvalue>` specifies\n  the argument value. The argument value specification must match the\n  parameter declaration of the abstract constraint.\n\n:eql:synopsis:`on ( <subject-expr> )`\n  An optional expression defining the *subject* of the constraint.\n  If not specified, the subject is the value of the schema item on which\n  the concrete constraint is defined.\n\n  The expression must refer to the original subject of the constraint as\n  ``__subject__``. The expression must be\n  :ref:`Immutable <ref_reference_volatility>`, but may refer to\n  ``__subject__`` and its properties and links.\n\n  Note also that ``<subject-expr>`` itself has to\n  be parenthesized.\n\n:eql:synopsis:`except ( <exception-expr> )`\n  An optional expression defining a condition to create exceptions to\n  the constraint. If ``<exception-expr>`` evaluates to ``true``,\n  the constraint is ignored for the current subject. If it evaluates\n  to ``false`` or ``{}``, the constraint applies normally.\n\n  ``except`` may only be declared on object constraints, and otherwise\n  follows the same rules as ``on``.\n\n:eql:synopsis:`extending <base> [, ...]`\n  If specified, declares the *parent* constraints for this abstract\n  constraint.\n\nThe valid SDL sub-declarations are listed below:\n\n:eql:synopsis:`using <constr_expression>`\n  A boolean expression that returns ``true`` for valid data and\n  ``false`` for invalid data. The expression may refer to the\n  subject of the constraint as ``__subject__``. This declaration is\n  only valid for *abstract constraints*.\n\n:eql:synopsis:`errmessage := <error_message>`\n  An optional string literal defining the error message template\n  that is raised when the constraint is violated. The template is a\n  formatted string that may refer to constraint context variables in\n  curly braces. The template may refer to the following:\n\n  - ``$argname`` -- the value of the specified constraint argument\n  - ``__subject__`` -- the value of the ``title`` annotation of the\n    scalar type, property or link on which the constraint is defined.\n\n  If the content of curly braces does not match any variables,\n  the curly braces are emitted as-is. They can also be escaped by\n  using double curly braces.\n\n:sdl:synopsis:`<annotation-declarations>`\n  Set constraint :ref:`annotation <ref_eql_sdl_annotations>`\n  to a given *value*.\n\n\n.. _ref_eql_ddl_constraints:\n\nDDL commands\n============\n\nThis section describes the low-level DDL commands for creating and dropping\nconstraints and abstract constraints. You typically don't need to use these\ncommands directly, but knowing about them is useful for reviewing migrations.\n\n\nCreate abstract constraint\n--------------------------\n\n:eql-statement:\n:eql-haswith:\n\nDefine a new abstract constraint.\n\n.. eql:synopsis::\n\n  [ with [ <module-alias> := ] module <module-name> ]\n  create abstract constraint <name> [ ( [<argspec>] [, ...] ) ]\n    [ on ( <subject-expr> ) ]\n    [ extending <base> [, ...] ]\n  \"{\" <subcommand>; [...] \"}\" ;\n\n  # where <argspec> is:\n\n    [ <argname>: ] <argtype>\n\n  # where <subcommand> is one of\n\n    using <constr-expression>\n    set errmessage := <error-message>\n    create annotation <annotation-name> := <value>\n\n\nDescription\n^^^^^^^^^^^\nThe command ``create abstract constraint`` defines a new abstract constraint.\n\nIf *name* is qualified with a module name, then the constraint is created in\nthat module, otherwise it is created in the current module. The constraint\nname must be distinct from that of any existing schema item in the module.\n\n\nParameters\n^^^^^^^^^^\nMost sub-commands and options of this command are identical to the\n:ref:`SDL constraint declaration <ref_eql_sdl_constraints_syntax>`,\nwith some additional features listed below:\n\n:eql:synopsis:`[ <module-alias> := ] module <module-name>`\n  An optional list of module alias declarations to be used in the\n  migration definition. When *module-alias* is not specified,\n  *module-name* becomes the effective current module and is used\n  to resolve all unqualified names.\n\n:eql:synopsis:`set errmessage := <error_message>`\n  An optional string literal defining the error message template\n  that is raised when the constraint is violated. Other than a\n  slight syntactical difference this is the same as the\n  corresponding SDL declaration.\n\n:eql:synopsis:`create annotation <annotation-name> := <value>;`\n  Set constraint annotation ``<annotation-name>`` to ``<value>``.\n  See :eql:stmt:`create annotation` for details.\n\n\nExample\n^^^^^^^\nCreate an abstract constraint \"uppercase\" which checks if the subject\nis a string in upper case:\n\n.. code-block:: edgeql\n\n  create abstract constraint uppercase {\n    create annotation title := \"Upper case constraint\";\n\n    using (str_upper(__subject__) = __subject__);\n\n    set errmessage := \"{__subject__} is not in upper case\";\n  };\n\n\nAlter abstract constraint\n-------------------------\n\n:eql-statement:\n:eql-haswith:\n\nAlter the definition of an abstract constraint.\n\n.. eql:synopsis::\n\n  [ with [ <module-alias> := ] module <module-name> ]\n  alter abstract constraint <name>\n  \"{\" <subcommand>; [...] \"}\" ;\n\n  # where <subcommand> is one of\n\n    rename to <newname>\n    using <constr-expression>\n    set errmessage := <error-message>\n    reset errmessage\n    create annotation <annotation-name> := <value>\n    alter annotation <annotation-name> := <value>\n    drop annotation <annotation-name>\n\n\nDescription\n^^^^^^^^^^^\n\nThe command ``alter abstract constraint`` changes the definition of an\nabstract constraint item. *name* must be a name of an existing\nabstract constraint, optionally qualified with a module name.\n\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`[ <module-alias> := ] module <module-name>`\n  An optional list of module alias declarations to be used in the\n  migration definition. When *module-alias* is not specified,\n  *module-name* becomes the effective current module and is used\n  to resolve all unqualified names.\n\n:eql:synopsis:`<name>`\n  The name (optionally module-qualified) of the constraint to alter.\n\nSubcommands allowed in the ``alter abstract constraint`` block:\n\n:eql:synopsis:`rename to <newname>`\n  Change the name of the constraint to *newname*. All concrete\n  constraints inheriting from this constraint are also renamed.\n\n:eql:synopsis:`alter annotation <annotation-name> := <value>`\n  Alter constraint annotation ``<annotation-name>``.\n  See :eql:stmt:`alter annotation` for details.\n\n:eql:synopsis:`drop annotation <annotation-name>`\n  Remove annotation ``<annotation-name>``.\n  See :eql:stmt:`drop annotation` for details.\n\n:eql:synopsis:`reset errmessage`\n  Remove the error message from this abstract constraint. The error message\n  specified in the base abstract constraint will be used instead.\n\nAll subcommands allowed in a ``create abstract constraint`` block are also\nvalid here.\n\n\nExample\n^^^^^^^\n\nRename the abstract constraint \"uppercase\" to \"upper_case\":\n\n.. code-block:: edgeql\n\n  alter abstract constraint uppercase rename to upper_case;\n\n\nDrop abstract constraint\n------------------------\n\n:eql-statement:\n:eql-haswith:\n\nRemove an abstract constraint from the schema.\n\n.. eql:synopsis::\n\n  [ with [ <module-alias> := ] module <module-name> ]\n  drop abstract constraint <name> ;\n\n\nDescription\n^^^^^^^^^^^\n\nThe command ``drop abstract constraint`` removes an existing abstract\nconstraint item from the database schema. If any schema items depending\non this constraint exist, the operation is refused.\n\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`[ <module-alias> := ] module <module-name>`\n  An optional list of module alias declarations to be used in the\n  migration definition.\n\n:eql:synopsis:`<name>`\n  The name (optionally module-qualified) of the constraint to remove.\n\n\nExample\n^^^^^^^\n\nDrop abstract constraint ``upper_case``:\n\n.. code-block:: edgeql\n\n  drop abstract constraint upper_case;\n\n\nCreate constraint\n-----------------\n\n:eql-statement:\n\nDefine a concrete constraint on the specified schema item.\n\n.. eql:synopsis::\n\n  [ with [ <module-alias> := ] module <module-name> ]\n  create [ delegated ] constraint <name>\n    [ ( [<argspec>] [, ...] ) ]\n    [ on ( <subject-expr> ) ]\n    [ except ( <except-expr> ) ]\n  \"{\" <subcommand>; [...] \"}\" ;\n\n  # where <argspec> is:\n\n    [ <argname>: ] <argvalue>\n\n  # where <subcommand> is one of\n\n    set errmessage := <error-message>\n    create annotation <annotation-name> := <value>\n\n\nDescription\n^^^^^^^^^^^\n\nThe command ``create constraint`` defines a new concrete constraint. It can\nonly be used in the context of :eql:stmt:`create scalar`,\n:eql:stmt:`alter scalar`, :eql:stmt:`create property`,\n:eql:stmt:`alter property`, :eql:stmt:`create link`, or :eql:stmt:`alter link`.\n\n*name* must be a name (optionally module-qualified) of a previously defined\nabstract constraint.\n\n\nParameters\n^^^^^^^^^^\n\nMost sub-commands and options of this command are identical to the\n:ref:`SDL constraint declaration <ref_eql_sdl_constraints_syntax>`,\nwith some additional features listed below:\n\n:eql:synopsis:`[ <module-alias> := ] module <module-name>`\n  An optional list of module alias declarations to be used in the\n  migration definition.\n\n:eql:synopsis:`set errmessage := <error_message>`\n  An optional string literal defining the error message template\n  that is raised when the constraint is violated. Other than a\n  slight syntactical difference, this is the same as the corresponding\n  SDL declaration.\n\n:eql:synopsis:`create annotation <annotation-name> := <value>;`\n  An optional list of annotations for the constraint. See\n  :eql:stmt:`create annotation` for details.\n\n\nExample\n^^^^^^^\n\nCreate a \"score\" property on the \"User\" type with a minimum value\nconstraint:\n\n.. code-block:: edgeql\n\n  alter type User create property score: int64 {\n    create constraint min_value(0)\n  };\n\nCreate a Vector with a maximum magnitude:\n\n.. code-block:: edgeql\n\n  create type Vector {\n    create required property x: float64;\n    create required property y: float64;\n    create constraint expression ON (\n      __subject__.x^2 + __subject__.y^2 < 25\n    );\n  }\n\n\nAlter constraint\n----------------\n\n:eql-statement:\n\nAlter the definition of a concrete constraint on the specified schema item.\n\n.. eql:synopsis::\n\n  [ with [ <module-alias> := ] module <module-name> [, ...] ]\n  alter constraint <name>\n    [ ( [<argspec>] [, ...] ) ]\n    [ on ( <subject-expr> ) ]\n    [ except ( <except-expr> ) ]\n  \"{\" <subcommand>; [ ... ] \"}\" ;\n\n  # -- or --\n\n  [ with [ <module-alias> := ] module <module-name> [, ...] ]\n  alter constraint <name>\n    [ ( [<argspec>] [, ...] ) ]\n    [ on ( <subject-expr> ) ]\n    <subcommand> ;\n\n  # where <subcommand> is one of:\n\n    set delegated\n    set not delegated\n    set errmessage := <error-message>\n    reset errmessage\n    create annotation <annotation-name> := <value>\n    alter annotation <annotation-name>\n    drop annotation <annotation-name>\n\n\nDescription\n^^^^^^^^^^^\n\nThe command ``alter constraint`` changes the definition of a concrete\nconstraint. Both single- and multi-command forms are supported.\n\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`[ <module-alias> := ] module <module-name>`\n  An optional list of module alias declarations for the migration.\n\n:eql:synopsis:`<name>`\n  The name (optionally module-qualified) of the concrete constraint\n  that is being altered.\n\n:eql:synopsis:`<argspec>`\n  A list of constraint arguments as specified at the time of\n  ``create constraint``.\n\n:eql:synopsis:`on ( <subject-expr> )`\n  An expression defining the *subject* of the constraint as specified\n  at the time of ``create constraint``.\n\nThe following subcommands are allowed in the ``alter constraint`` block:\n\n:eql:synopsis:`set delegated`\n  Mark the constraint as *delegated*, which means it will\n  not be enforced on the type it's declared on, and enforcement is\n  delegated to subtypes. Useful for :eql:constraint:`exclusive` constraints.\n\n:eql:synopsis:`set not delegated`\n  Mark the constraint as *not delegated*, so it is enforced globally across\n  the type and any extending types.\n\n:eql:synopsis:`rename to <newname>`\n  Change the name of the constraint to ``<newname>``.\n\n:eql:synopsis:`alter annotation <annotation-name>`\n  Alter a constraint annotation.\n\n:eql:synopsis:`drop annotation <annotation-name>`\n  Remove a constraint annotation.\n\n:eql:synopsis:`reset errmessage`\n  Remove the error message from this constraint, reverting to that of the\n  abstract constraint, if any.\n\nAll subcommands allowed in ``create constraint`` are also valid in\n``alter constraint``.\n\nExample\n^^^^^^^\n\nChange the error message on the minimum value constraint on the property\n\"score\" of the \"User\" type:\n\n.. code-block:: edgeql\n\n  alter type User alter property score\n    alter constraint min_value(0)\n      set errmessage := 'Score cannot be negative';\n\n\nDrop constraint\n---------------\n\n:eql-statement:\n:eql-haswith:\n\nRemove a concrete constraint from the specified schema item.\n\n.. eql:synopsis::\n\n  [ with [ <module-alias> := ] module <module-name> [, ...] ]\n  drop constraint <name>\n    [ ( [<argspec>] [, ...] ) ]\n    [ on ( <subject-expr> ) ]\n    [ except ( <except-expr> ) ] ;\n\n\nDescription\n^^^^^^^^^^^\n\nThe command ``drop constraint`` removes the specified constraint from\nits containing schema item.\n\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`[ <module-alias> := ] module <module-name>`\n  Optional module alias declarations for the migration definition.\n\n:eql:synopsis:`<name>`\n  The name (optionally module-qualified) of the concrete constraint\n  to remove.\n\n:eql:synopsis:`<argspec>`\n  A list of constraint arguments as specified at the time of\n  ``create constraint``.\n\n:eql:synopsis:`on ( <subject-expr> )`\n  Expression defining the *subject* of the constraint as specified\n  at the time of ``create constraint``.\n\nExample\n^^^^^^^\n\nRemove constraint \"min_value\" from the property \"score\" of the \"User\" type:\n\n.. code-block:: edgeql\n\n  alter type User alter property score\n    drop constraint min_value(0);\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Introspection > Constraints <ref_datamodel_introspection_constraints>`\n  * - :ref:`Standard Library > Constraints <ref_std_constraints>`\n"
  },
  {
    "path": "docs/reference/datamodel/extensions.rst",
    "content": ".. _ref_datamodel_extensions:\n\n==========\nExtensions\n==========\n\n.. api-index:: using extension\n\nExtensions are the way |Gel| can be extended with more functionality.\nThey can add new types, scalars, functions, etc., but, more\nimportantly, they can add new ways of interacting with the database.\n\n\nBuilt-in extensions\n===================\n\n.. api-index:: edgeql_http, graphql, auth, ai, pg_trgm, pg_unaccent, pgcrypto,\n               pgvector\n\nThere are a few built-in extensions available:\n\n- ``edgeql_http``: enables :ref:`EdgeQL over HTTP <ref_edgeql_http>`,\n- ``graphql``: enables :ref:`GraphQL <ref_graphql_index>`,\n- ``auth``: enables :ref:`Gel Auth <ref_guide_auth>`,\n- ``ai``: enables :ref:`ext::ai module <ref_ai_extai_reference>`,\n\n- ``pg_trgm``: enables ``ext::pg_trgm``, which re-exports\n  `pgtrgm <https://www.postgresql.org/docs/current/pgtrgm.html>`__,\n\n- ``pg_unaccent``: enables ``ext::pg_unaccent``, which re-exports\n  `unaccent <https://www.postgresql.org/docs/current/unaccent.html>`__,\n\n- ``pgcrypto``: enables ``ext::pgcrypto``, which re-exports\n  `pgcrypto <https://www.postgresql.org/docs/current/pgcrypto.html>`__,\n\n- ``pgvector``: enables ``ext::pgvector``, which re-exports\n  `pgvector <https://github.com/pgvector/pgvector/>`__,\n\n.. _ref_datamodel_using_extension:\n\nTo enable these extensions, add a ``using`` statement at the top level of\nyour schema:\n\n.. code-block:: sdl\n\n   using extension auth;\n   # or / and\n   using extension ai;\n\n\nStandalone extensions\n=====================\n\n.. api-index:: postgis\n\nAdditionally, standalone extension packages can be installed on local project-managed instances via the CLI, with ``postgis`` being a notable example.\n\nList installed extensions:\n\n.. code-block:: bash\n\n  $ gel extension list\n  ┌─────────┬─────────┐\n  │ Name    │ Version │\n  └─────────┴─────────┘\n\nList available extensions:\n\n.. code-block:: bash\n\n  $ gel extension list-available\n  ┌─────────┬───────────────┐\n  │ Name    │ Version       │\n  │ postgis │ 3.4.3+6b82d77 │\n  └─────────┴───────────────┘\n\nInstall the ``postgis`` extension:\n\n.. code-block:: bash\n\n  $ gel extension install postgis\n  Found extension package: postgis version 3.4.3+6b82d77\n  00:00:03 [====================] 22.49 MiB/22.49 MiB\n  Extension 'postgis' installed successfully.\n\nCheck that extension is installed:\n\n.. code-block:: bash\n\n  $ gel extension list\n  ┌─────────┬───────────────┐\n  │ Name    │ Version       │\n  │ postgis │ 3.4.3+6b82d77 │\n  └─────────┴───────────────┘\n\nAfter installing extensions, make sure to restart your instance:\n\n.. code-block:: bash\n\n  $ gel instance restart\n\nStandalone extensions can now be declared in the schema, same as\nbuilt-in extensions:\n\n.. code-block:: sdl\n\n  using extension postgis;\n\n.. note::\n   To restore a dump that uses a standalone extension, that extension must\n   be installed before the restore process.\n\n.. _ref_eql_sdl_extensions:\n\nUsing extensions\n================\n\nSyntax\n------\n\n.. sdl:synopsis::\n\n  using extension <ExtensionName> \";\"\n\n\nExtension declaration must be outside any :ref:`module block\n<ref_eql_sdl_modules>` since extensions affect the entire database and\nnot a specific module.\n\n\n\n.. _ref_eql_ddl_extensions:\n\nDDL commands\n============\n\nThis section describes the low-level DDL commands for creating and\ndropping extensions. You typically don't need to use these commands directly,\nbut knowing about them is useful for reviewing migrations.\n\n\ncreate extension\n----------------\n\n:eql-statement:\n\nEnable a particular extension for the current schema.\n\n.. eql:synopsis::\n\n  create extension <ExtensionName> \";\"\n\n\nDescription\n^^^^^^^^^^^\n\nThe command ``create extension`` enables the specified extension for\nthe current :versionreplace:`database;5.0:branch`.\n\nExamples\n^^^^^^^^\n\nEnable :ref:`GraphQL <ref_graphql_index>` extension for the current\nschema:\n\n.. code-block:: edgeql\n\n  create extension graphql;\n\nEnable :ref:`EdgeQL over HTTP <ref_edgeql_http>` extension for the\ncurrent :versionreplace:`database;5.0:branch`:\n\n.. code-block:: edgeql\n\n  create extension edgeql_http;\n\n\ndrop extension\n--------------\n\n:eql-statement:\n\nDisable an extension.\n\n.. eql:synopsis::\n\n  drop extension <ExtensionName> \";\"\n\n\nThe command ``drop extension`` disables a currently active extension for\nthe current |branch|.\n\nExamples\n^^^^^^^^\n\nDisable :ref:`GraphQL <ref_graphql_index>` extension for the current\nschema:\n\n.. code-block:: edgeql\n\n  drop extension graphql;\n\nDisable :ref:`EdgeQL over HTTP <ref_edgeql_http>` extension for the\ncurrent :versionreplace:`database;5.0:branch`:\n\n.. code-block:: edgeql\n\n  drop extension edgeql_http;\n"
  },
  {
    "path": "docs/reference/datamodel/functions.rst",
    "content": ".. _ref_datamodel_functions:\n.. _ref_eql_sdl_functions:\n\n=========\nFunctions\n=========\n\n.. note::\n\n  This page documents how to define custom functions, however |Gel| provides a\n  large library of built-in functions and operators. These are documented in\n  :ref:`Standard Library <ref_std>`.\n\n\nUser-defined Functions\n======================\n\nGel allows you to define custom functions. For example, consider\na function that adds an exclamation mark ``'!'`` at the end of the\nstring:\n\n.. code-block:: sdl\n\n  function exclamation(word: str) -> str\n    using (word ++ '!');\n\nThis function accepts a :eql:type:`str` as an argument and produces a\n:eql:type:`str` as output as well.\n\n.. code-block:: edgeql-repl\n\n  test> select exclamation({'Hello', 'World'});\n  {'Hello!', 'World!'}\n\n\n.. _ref_datamodel_functions_modifying:\n\nSets as arguments\n=================\n\nCalling a user-defined function on a set will always apply it as\n:ref:`*element-wise* <ref_reference_cardinality_functions_operators>`.\n\n.. code-block:: sdl\n\n  function magnitude(x: float64) -> float64\n    using (\n      math::sqrt(sum(x * x))\n    );\n\n.. code-block:: edgeql-repl\n\n    db> select magnitude({3, 4});\n    {3, 4}\n\nIn order to pass in multiple arguments at once, arguments should be packed into\narrays:\n\n.. code-block:: sdl\n\n  function magnitude(xs: array<float64>) -> float64\n    using (\n      with x := array_unpack(xs)\n      select math::sqrt(sum(x * x))\n    );\n\n.. code-block:: edgeql-repl\n\n    db> select magnitude([3, 4]);\n    {5}\n\nMultiple packed arrays can be passed into such a function, which will then be\napplied element-wise.\n\n.. code-block:: edgeql-repl\n\n    db> select magnitude({[3, 4], [5, 12]});\n    {5, 13}\n\n\nModifying Functions\n===================\n\n.. versionadded:: 6.0\n\nUser-defined functions can contain DML (i.e.,\n:ref:`insert <ref_eql_insert>`, :ref:`update <ref_eql_update>`,\n:ref:`delete <ref_eql_delete>`) to make changes to existing data. These\nfunctions have a :ref:`modifying <ref_reference_volatility>` volatility.\n\n.. code-block:: sdl\n\n  function add_user(name: str) -> User\n    using (\n      insert User {\n        name := name,\n        joined_at := std::datetime_current(),\n      }\n    );\n\n.. code-block:: edgeql-repl\n\n    db> select add_user('Jan') {name, joined_at};\n    {default::User {name: 'Jan', joined_at: <datetime>'2024-12-11T11:49:47Z'}}\n\nUnlike other functions, the arguments of modifying functions **must** have a\n:ref:`cardinality <ref_reference_cardinality>` of ``One``.\n\n.. code-block:: edgeql-repl\n\n    db> select add_user({'Feb','Mar'});\n    gel error: QueryError: possibly more than one element passed into\n    modifying function\n    db> select add_user(<str>{});\n    gel error: QueryError: possibly an empty set passed as non-optional\n    argument into modifying function\n\nOptional arguments can still accept empty sets. For example, if ``add_user``\nwas defined as:\n\n.. code-block:: sdl\n\n  function add_user(name: str, joined_at: optional datetime) -> User\n    using (\n      insert User {\n        name := name,\n        joined_at := joined_at ?? std::datetime_current(),\n      }\n    );\n\nthen the following queries are valid:\n\n.. code-block:: edgeql-repl\n\n    db> select add_user('Apr', <datetime>{}) {name, joined_at};\n    {default::User {name: 'Apr', joined_at: <datetime>'2024-12-11T11:50:51Z'}}\n    db> select add_user('May', <datetime>'2024-12-11T12:00:00-07:00') {name, joined_at};\n    {default::User {name: 'May', joined_at: <datetime>'2024-12-11T12:00:00Z'}}\n\nIn order to insert or update a multi parameter, the desired arguments should be\naggregated into an array as described above:\n\n.. code-block:: sdl\n\n  function add_user(name: str, nicknames: array<str>) -> User\n    using (\n      insert User {\n        name := name,\n        nicknames := array_unpack(nicknames),\n      }\n    );\n\n\n.. _ref_eql_sdl_functions_syntax:\n\nDeclaring functions\n===================\n\n.. api-index:: function, using, ->, variadic, named only, set of, optional,\n               volatility, Immutable, Stable, Volatile, Modifying\n\nThis section describes the syntax to declare a function in your schema.\n\nSyntax\n------\n\n.. sdl:synopsis::\n\n    function <name> ([ <argspec> ] [, ... ]) -> <returnspec>\n    using ( <edgeql> );\n\n    function <name> ([ <argspec> ] [, ... ]) -> <returnspec>\n    using <language> <functionbody> ;\n\n    function <name> ([ <argspec> ] [, ... ]) -> <returnspec>\n    \"{\"\n        [ <annotation-declarations> ]\n        [ volatility := {'Immutable' | 'Stable' | 'Volatile' | 'Modifying'} ]\n        [ using ( <expr> ) ; ]\n        [ using <language> <functionbody> ; ]\n        [ ... ]\n    \"}\" ;\n\n    # where <argspec> is:\n\n    [ <argkind> ] <argname>: [ <typequal> ] <argtype> [ = <default> ]\n\n    # <argkind> is:\n\n    [ { variadic | named only } ]\n\n    # <typequal> is:\n\n    [ { set of | optional } ]\n\n    # and <returnspec> is:\n\n    [ <typequal> ] <rettype>\n\n\nDescription\n^^^^^^^^^^^\n\nThis declaration defines a new **function** with the following options:\n\n:eql:synopsis:`<name>`\n    The name (optionally module-qualified) of the function to create.\n\n:eql:synopsis:`<argkind>`\n    The kind of an argument: ``variadic`` or ``named only``.\n\n    If not specified, the argument is called *positional*.\n\n    The ``variadic`` modifier indicates that the function takes an\n    arbitrary number of arguments of the specified type.  The passed\n    arguments will be passed as an array of the argument type.\n    Positional arguments cannot follow a ``variadic`` argument.\n    ``variadic`` parameters cannot have a default value.\n\n    The ``named only`` modifier indicates that the argument can only\n    be passed using that specific name.  Positional arguments cannot\n    follow a ``named only`` argument.\n\n:eql:synopsis:`<argname>`\n    The name of an argument.  If ``named only`` modifier is used this\n    argument *must* be passed using this name only.\n\n.. _ref_sdl_function_typequal:\n\n:eql:synopsis:`<typequal>`\n    The type qualifier: ``set of`` or ``optional``.\n\n    The ``set of`` qualifier indicates that the function is taking the\n    argument as a *whole set*, as opposed to being called on the input\n    product element-by-element.\n\n    User defined functions can not use ``set of`` arguments.\n\n    The ``optional`` qualifier indicates that the function will be called\n    if the argument is an empty set.  The default behavior is to return\n    an empty set if the argument is not marked as ``optional``.\n\n:eql:synopsis:`<argtype>`\n    The data type of the function's arguments\n    (optionally module-qualified).\n\n:eql:synopsis:`<default>`\n    An expression to be used as default value if the parameter is not\n    specified.  The expression has to be of a type compatible with the\n    type of the argument.\n\n.. _ref_sdl_function_rettype:\n\n:eql:synopsis:`<rettype>`\n    The return data type (optionally module-qualified).\n\n    The ``set of`` modifier indicates that the function will return\n    a non-singleton set.\n\n    The ``optional`` qualifier indicates that the function may return\n    an empty set.\n\nThe valid SDL sub-declarations are listed below:\n\n:eql:synopsis:`volatility := {'Immutable' | 'Stable' | 'Volatile' | 'Modifying'}`\n    Function volatility determines how aggressively the compiler can\n    optimize its invocations.\n\n    If not explicitly specified the function volatility is\n    :ref:`inferred <ref_reference_volatility>` from the function body.\n\n    * An ``Immutable`` function cannot modify the database and is\n      guaranteed to return the same results given the same arguments\n      *in all statements*.\n\n    * A ``Stable`` function cannot modify the database and is\n      guaranteed to return the same results given the same\n      arguments *within a single statement*.\n\n    * A ``Volatile`` function cannot modify the database and can return\n      different results on successive calls with the same arguments.\n\n    * A ``Modifying`` function can modify the database and can return\n      different results on successive calls with the same arguments.\n\n:eql:synopsis:`using ( <expr> )`\n    Specifies the body of the function.  :eql:synopsis:`<expr>` is an\n    arbitrary EdgeQL expression.\n\n:eql:synopsis:`using <language> <functionbody>`\n    A verbose version of the :eql:synopsis:`using` clause that allows\n    specifying the language of the function body.\n\n    * :eql:synopsis:`<language>` is the name of the language that\n      the function is implemented in.  Currently can only be ``edgeql``.\n\n    * :eql:synopsis:`<functionbody>` is a string constant defining\n      the function.  It is often helpful to use\n      :ref:`dollar quoting <ref_eql_lexical_dollar_quoting>`\n      to write the function definition string.\n\n:sdl:synopsis:`<annotation-declarations>`\n    Set function :ref:`annotation <ref_eql_sdl_annotations>`\n    to a given *value*.\n\nThe function name must be distinct from that of any existing function\nwith the same argument types in the same module.  Functions of\ndifferent argument types can share a name, in which case the functions\nare called *overloaded functions*.\n\n\n.. _ref_eql_ddl_functions:\n\nDDL commands\n============\n\nThis section describes the low-level DDL commands for creating, altering, and\ndropping functions. You typically don't need to use these commands directly, but\nknowing about them is useful for reviewing migrations.\n\nCreate function\n---------------\n\n:eql-statement:\n:eql-haswith:\n\nDefine a new function.\n\n.. eql:synopsis::\n\n    [ with <with-item> [, ...] ]\n    create function <name> ([ <argspec> ] [, ... ]) -> <returnspec>\n    using ( <expr> );\n\n    [ with <with-item> [, ...] ]\n    create function <name> ([ <argspec> ] [, ... ]) -> <returnspec>\n    using <language> <functionbody> ;\n\n    [ with <with-item> [, ...] ]\n    create function <name> ([ <argspec> ] [, ... ]) -> <returnspec>\n    \"{\" <subcommand> [, ...] \"}\" ;\n\n    # where <argspec> is:\n\n      [ <argkind> ] <argname>: [ <typequal> ] <argtype> [ = <default> ]\n\n    # <argkind> is:\n\n      [ { variadic | named only } ]\n\n    # <typequal> is:\n\n      [ { set of | optional } ]\n\n    # and <returnspec> is:\n\n      [ <typequal> ] <rettype>\n\n    # and <subcommand> is one of\n\n      set volatility := {'Immutable' | 'Stable' | 'Volatile' | 'Modifying'} ;\n      create annotation <annotation-name> := <value> ;\n      using ( <expr> ) ;\n      using <language> <functionbody> ;\n\n\nDescription\n^^^^^^^^^^^\n\nThe command ``create function`` defines a new function.  If *name* is\nqualified with a module name, then the function is created in that\nmodule, otherwise it is created in the current module.\n\nThe function name must be distinct from that of any existing function\nwith the same argument types in the same module.  Functions of\ndifferent argument types can share a name, in which case the functions\nare called *overloaded functions*.\n\n\nParameters\n^^^^^^^^^^\n\nMost sub-commands and options of this command are identical to the\n:ref:`SDL function declaration <ref_eql_sdl_functions_syntax>`, with\nsome additional features listed below:\n\n:eql:synopsis:`set volatility := {'Immutable' | 'Stable' | 'Volatile' | 'Modifying'}`\n    Function volatility determines how aggressively the compiler can\n    optimize its invocations. Other than a slight syntactical\n    difference this is the same as the corresponding SDL declaration.\n\n:eql:synopsis:`create annotation <annotation-name> := <value>`\n    Set the function's :eql:synopsis:`<annotation-name>` to\n    :eql:synopsis:`<value>`.\n\n    See :eql:stmt:`create annotation` for details.\n\n\nExamples\n^^^^^^^^\n\nDefine a function returning the sum of its arguments:\n\n.. code-block:: edgeql\n\n    create function mysum(a: int64, b: int64) -> int64\n    using (\n        select a + b\n    );\n\nThe same, but using a variadic argument and an explicit language:\n\n.. code-block:: edgeql\n\n    create function mysum(variadic argv: int64) -> int64\n    using edgeql $$\n        select sum(array_unpack(argv))\n    $$;\n\nDefine a function using the block syntax:\n\n.. code-block:: edgeql\n\n    create function mysum(a: int64, b: int64) -> int64 {\n        using (\n            select a + b\n        );\n        create annotation title := \"My sum function.\";\n    };\n\n\nAlter function\n--------------\n\n:eql-statement:\n:eql-haswith:\n\nChange the definition of a function.\n\n.. eql:synopsis::\n\n    [ with <with-item> [, ...] ]\n    alter function <name> ([ <argspec> ] [, ... ]) \"{\"\n        <subcommand> [, ...]\n    \"}\"\n\n    # where <argspec> is:\n\n    [ <argkind> ] <argname>: [ <typequal> ] <argtype> [ = <default> ]\n\n    # and <subcommand> is one of\n\n      set volatility := {'Immutable' | 'Stable' | 'Volatile' | 'Modifying'} ;\n      reset volatility ;\n      rename to <newname> ;\n      create annotation <annotation-name> := <value> ;\n      alter annotation <annotation-name> := <value> ;\n      drop annotation <annotation-name> ;\n      using ( <expr> ) ;\n      using <language> <functionbody> ;\n\n\nDescription\n^^^^^^^^^^^\n\nThe command ``alter function`` changes the definition of a function.\nThe command allows changing annotations, the volatility level, and\nother attributes.\n\n\nSubcommands\n^^^^^^^^^^^\n\nThe following subcommands are allowed in the ``alter function`` block\nin addition to the commands common to the ``create function``:\n\n:eql:synopsis:`reset volatility`\n    Remove explicitly specified volatility in favor of the volatility\n    inferred from the function body.\n\n:eql:synopsis:`rename to <newname>`\n    Change the name of the function to *newname*.\n\n:eql:synopsis:`alter annotation <annotation-name>;`\n    Alter function :eql:synopsis:`<annotation-name>`.\n    See :eql:stmt:`alter annotation` for details.\n\n:eql:synopsis:`drop annotation <annotation-name>;`\n    Remove function :eql:synopsis:`<annotation-name>`.\n    See :eql:stmt:`drop annotation` for details.\n\n:eql:synopsis:`reset errmessage;`\n    Remove the error message from this abstract constraint.\n    The error message specified in the base abstract constraint\n    will be used instead.\n\n\nExample\n^^^^^^^\n\n.. code-block:: edgeql\n\n    create function mysum(a: int64, b: int64) -> int64 {\n        using (\n            select a + b\n        );\n        create annotation title := \"My sum function.\";\n    };\n\n    alter function mysum(a: int64, b: int64) {\n        set volatility := 'Immutable';\n        drop annotation title;\n    };\n\n    alter function mysum(a: int64, b: int64) {\n        using (\n            select (a + b) * 100\n        )\n    };\n\n\nDrop function\n-------------\n\n:eql-statement:\n:eql-haswith:\n\n\nRemove a function.\n\n.. eql:synopsis::\n\n    [ with <with-item> [, ...] ]\n    drop function <name> ([ <argspec> ] [, ... ]);\n\n    # where <argspec> is:\n\n    [ <argkind> ] <argname>: [ <typequal> ] <argtype> [ = <default> ]\n\n\nDescription\n^^^^^^^^^^^\n\nThe command ``drop function`` removes the definition of an existing function.\nThe argument types to the function must be specified, since there\ncan be different functions with the same name.\n\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`<name>`\n    The name (optionally module-qualified) of an existing function.\n\n:eql:synopsis:`<argname>`\n    The name of an argument used in the function definition.\n\n:eql:synopsis:`<argmode>`\n    The mode of an argument: ``set of`` or ``optional`` or ``variadic``.\n\n:eql:synopsis:`<argtype>`\n    The data type(s) of the function's arguments\n    (optionally module-qualified), if any.\n\n\nExample\n^^^^^^^\n\nRemove the ``mysum`` function:\n\n.. code-block:: edgeql\n\n    drop function mysum(a: int64, b: int64);\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Reference > Function calls <ref_reference_function_call>`\n  * - :ref:`Introspection > Functions <ref_datamodel_introspection_functions>`\n  * - :ref:`Cheatsheets > Functions <ref_cheatsheet_functions>`\n\n"
  },
  {
    "path": "docs/reference/datamodel/future.rst",
    "content": ".. _ref_datamodel_future:\n\n===============\nFuture behavior\n===============\n\n.. api-index:: using future\n\nThis article explains what the ``using future ...;`` statement means in your\nschema.\n\nOur goal is to make |Gel| the best database system in the world, which requires\nus to keep evolving. Usually, we can add new functionality while preserving\nbackward compatibility, but on rare occasions we must implement changes that\nrequire elaborate transitions.\n\nTo handle these cases, we introduce *future* behavior, which lets you try out\nupcoming features before a major release. Sometimes enabling a future is\nnecessary to fix current issues; other times it offers a safe and easy way to\nensure your codebase remains compatible. This approach provides more time to\nadopt a new feature and identify any resulting bugs.\n\nAny time a behavior is available as a ``future,`` all new :ref:`projects\n<ref_intro_projects>` enable it by default for empty databases. You can remove\na ``future`` from your schema if absolutely necessary, but doing so is\ndiscouraged. Existing projects are unaffected by default, so you must manually\nadd the ``future`` specification to gain early access.\n\nFlags\n=====\n\n.. api-index:: simple_scoping, warn_old_scoping, nonrecursive_access_policies\n\nAt the moment there are three ``future`` flags available:\n\n- ``simple_scoping``\n\n  Introduced in |Gel| 6.0, this flag simplifies the scoping rules for\n  path expressions. Read more about it and in great detail in\n  :ref:`ref_eql_path_resolution`.\n\n- ``warn_old_scoping``\n\n  Introduced in |Gel| 6.0, this flag will emit a warning when a query\n  is detected to depend on the old scoping rules. This is an intermediate\n  step towards enabling the ``simple_scoping`` flag in existing large\n  codebases.\n\n  Read more about this flag in :ref:`ref_warn_old_scoping`.\n\n.. _ref_datamodel_access_policies_nonrecursive:\n.. _nonrecursive:\n\n- ``nonrecursive_access_policies``: makes access policies non-recursive.\n\n  This flag is no longer used becauae the behavior is enabled\n  by default since |EdgeDB| 4. The flag was helpful to ease transition\n  from EdgeDB 3.x to 4.x.\n\n  Since |EdgeDB| 3.0, access policy restrictions do **not** apply\n  to any access policy expression. This means that when reasoning about access\n  policies it is no longer necessary to take other policies into account.\n  Instead, all data is visible for the purpose of *defining* an access\n  policy.\n\n  This change was made to simplify reasoning about access policies and\n  to allow certain patterns to be expressed efficiently. Since those who have\n  access to modifying the schema can remove unwanted access policies, no\n  additional security is provided by applying access policies to each\n  other's expressions.\n\n\n.. _ref_eql_sdl_future:\n\nDeclaring future flags\n======================\n\nSyntax\n------\n\nDeclare that the current schema enables a particular future behavior.\n\n.. sdl:synopsis::\n\n  using future <FutureBehavior> \";\"\n\nDescription\n^^^^^^^^^^^\n\nFuture behavior declaration must be outside any :ref:`module block\n<ref_eql_sdl_modules>` since this behavior affects the entire database and not\na specific module.\n\nExample\n^^^^^^^\n\n.. code-block:: sdl-invalid\n\n  using future simple_scoping;\n\n\n.. _ref_eql_ddl_future:\n\nDDL commands\n============\n\nThis section describes the low-level DDL commands for creating and\ndropping future flags. You typically don't need to use these commands directly,\nbut knowing about them is useful for reviewing migrations.\n\nCreate future\n-------------\n\n:eql-statement:\n\nEnable a particular future behavior for the current schema.\n\n.. eql:synopsis::\n\n  create future <FutureBehavior> \";\"\n\n\nThe command ``create future`` enables the specified future behavior for\nthe current branch.\n\nExample\n^^^^^^^\n\n.. code-block:: edgeql\n\n  create future simple_scoping;\n\n\nDrop future\n-----------\n\n:eql-statement:\n\nDisable a particular future behavior for the current schema.\n\n.. eql:synopsis::\n\n  drop future <FutureBehavior> \";\"\n\nDescription\n^^^^^^^^^^^\n\nThe command ``drop future`` disables a currently active future behavior for the\ncurrent branch. However, this is only possible for versions of |Gel| when the\nbehavior in question is not officially introduced. Once a particular behavior is\nintroduced as the standard behavior in a |Gel| release, it cannot be disabled.\n\nExample\n^^^^^^^\n\n.. code-block:: edgeql\n\n  drop future warn_old_scoping;\n"
  },
  {
    "path": "docs/reference/datamodel/globals.rst",
    "content": ".. _ref_datamodel_globals:\n\n=======\nGlobals\n=======\n\nSchemas in Gel can contain typed *global variables*. These create a mechanism\nfor specifying session-level context that can be referenced in queries,\naccess policies, triggers, and elsewhere with the ``global`` keyword.\n\nHere's a very common example of a global variable representing the current\nuser ID:\n\n.. code-block:: sdl\n\n  global current_user_id: uuid;\n\n.. tabs::\n\n  .. code-tab:: edgeql\n\n    select User {\n      id,\n      posts: { title, content }\n    }\n    filter .id = global current_user_id;\n\n  .. code-tab:: python\n\n    # In a non-trivial example, `global current_user_id` would\n    # be used indirectly in an access policy or some other context.\n    await client.with_globals({'user_id': user_id}).qeury('''\n      select User {\n        id,\n        posts: { title, content }\n      }\n      filter .id = global current_user_id;\n    ''')\n\n  .. code-tab:: typescript\n\n    // In a non-trivial example, `global current_user_id` would\n    // be used indirectly in an access policy or some other context.\n    await client.withGlobals({user_id}).qeury('''\n      select User {\n        id,\n        posts: { title, content }\n      }\n      filter .id = global current_user_id;\n    ''')\n\n\nSetting global variables\n========================\n\nGlobal variables are set at session level or when initializing a client.\nThe exact API depends on which client library you're using, but the general\nbehavior and principles are the same across all libraries.\n\n.. tabs::\n\n  .. code-tab:: typescript\n\n    import createClient from 'gel';\n\n    const baseClient = createClient();\n\n    // returns a new Client instance, that shares the underlying\n    // network connection with `baseClient` , but sends the configured\n    // globals along with all queries run through it:\n    const clientWithGlobals = baseClient.withGlobals({\n      current_user_id: '2141a5b4-5634-4ccc-b835-437863534c51',\n    });\n\n    const result = await clientWithGlobals.query(\n      `select global current_user_id;`\n    );\n\n  .. code-tab:: python\n\n    from gel import create_client\n\n    base_client = create_client()\n\n    # returns a new Client instance, that shares the underlying\n    # network connection with `base_client` , but sends the configured\n    # globals along with all queries run through it:\n    client = base_client.with_globals({\n        'current_user_id': '580cc652-8ab8-4a20-8db9-4c79a4b1fd81'\n    })\n\n    result = client.query(\"\"\"\n        select global current_user_id;\n    \"\"\")\n\n  .. code-tab:: go\n\n    package main\n\n    import (\n      \"context\"\n      \"fmt\"\n      \"log\"\n\n      \"github.com/geldata/gel-go\"\n    )\n\n    func main() {\n      ctx := context.Background()\n      client, err := gel.CreateClient(ctx, gel.Options{})\n      if err != nil {\n        log.Fatal(err)\n      }\n      defer client.Close()\n\n      id, err := gel.ParseUUID(\"2141a5b4-5634-4ccc-b835-437863534c51\")\n      if err != nil {\n        log.Fatal(err)\n      }\n\n      var result gel.UUID\n      err = client.\n        WithGlobals(map[string]interface{}{\"current_user\": id}).\n        QuerySingle(ctx, \"SELECT global current_user;\", &result)\n      if err != nil {\n        log.Fatal(err)\n      }\n\n      fmt.Println(result)\n    }\n\n  .. code-tab:: rust\n\n    use uuid::Uuid;\n\n    let client = gel_tokio::create_client().await.expect(\"Client init\");\n\n    let client_with_globals = client.with_globals_fn(|c| {\n        c.set(\n            \"current_user_id\",\n            Value::Uuid(\n                Uuid::parse_str(\"2141a5b4-5634-4ccc-b835-437863534c51\")\n                    .expect(\"Uuid should have parsed\"),\n            ),\n        )\n    });\n    let val: Uuid = client_with_globals\n        .query_required_single(\"select global current_user_id;\", &())\n        .await\n        .expect(\"Returning value\");\n    println!(\"Result: {val}\");\n\n  .. code-tab:: edgeql\n\n    set global current_user_id :=\n      <uuid>'2141a5b4-5634-4ccc-b835-437863534c51';\n\n\nCardinality\n===========\n\nA global variable can be declared with one of two cardinalities:\n\n- ``single`` (the default): At most one value.\n- ``multi``: A set of values. Only valid for computed global variables.\n\nIn addition, a global can be marked ``required`` or ``optional`` (the default).\nIf marked ``required``, a default value must be provided.\n\n\nComputed globals\n================\n\n.. api-index:: global, :=\n\nGlobal variables can also be computed. The value of computed globals is\ndynamically computed when they are referenced in queries.\n\n.. code-block:: sdl\n\n  required global now := datetime_of_transaction();\n\nThe provided expression will be computed at the start of each query in which\nthe global is referenced. There's no need to provide an explicit type; the type\nis inferred from the computed expression.\n\nComputed globals can also be object-typed and have ``multi`` cardinality.\nFor example:\n\n.. code-block:: sdl\n\n  global current_user_id: uuid;\n\n  # object-typed global\n  global current_user := (\n    select User filter .id = global current_user_id\n  );\n\n  # multi global\n  global current_user_friends := (global current_user).friends;\n\n\nReferencing globals\n===================\n\n.. api-index:: global\n\nUnlike query parameters, globals can be referenced *inside your schema\ndeclarations*:\n\n.. code-block:: sdl\n\n  type User {\n    name: str;\n    is_self := (.id = global current_user_id)\n  };\n\nThis is particularly useful when declaring :ref:`access policies\n<ref_datamodel_access_policies>`:\n\n.. code-block:: sdl\n\n  type Person {\n    required name: str;\n\n    access policy my_policy allow all\n      using (.id = global current_user_id);\n  }\n\nRefer to :ref:`Access Policies <ref_datamodel_access_policies>` for complete\ndocumentation.\n\n.. _ref_eql_sdl_globals:\n.. _ref_eql_sdl_globals_syntax:\n\nDeclaring globals\n=================\n\n.. api-index:: required, optional, single, multi, global, :=, :, default\n\nThis section describes the syntax to declare a global variable in your schema.\n\nSyntax\n------\n\nDefine a new global variable in SDL, corresponding to the more explicit DDL\ncommands described later:\n\n.. sdl:synopsis::\n\n  # Global variable declaration:\n  [{required | optional}] [single]\n    global <name>: <type>\n    [ \"{\"\n        [ default := <expression> ; ]\n        [ <annotation-declarations> ]\n        ...\n      \"}\" ]\n\n  # Computed global variable declaration:\n  [{required | optional}] [{single | multi}]\n    global <name> := <expression>;\n\n\nDescription\n^^^^^^^^^^^\n\nThere are two different forms of ``global`` declarations, as shown in the\nsyntax synopsis above:\n\n1. A *settable* global (defined with ``: <type>``) which can be changed using\n   a session-level :ref:`set <ref_eql_statements_session_set_alias>` command.\n\n2. A *computed* global (defined with ``:= <expression>``), which cannot be\n   directly set but instead derives its value from the provided expression.\n\nThe following options are available:\n\n:eql:synopsis:`required`\n  If specified, the global variable is considered *required*. It is an\n  error for this variable to have an empty value. If a global variable is\n  declared *required*, it must also declare a *default* value.\n\n:eql:synopsis:`optional`\n  The global variable is considered *optional*, i.e. it is possible for the\n  variable to have an empty value. (This is the default.)\n\n:eql:synopsis:`multi`\n  Specifies that the global variable may have a set of values. Only\n  *computed* global variables can have this qualifier.\n\n:eql:synopsis:`single`\n  Specifies that the global variable must have at most a *single* value. It\n  is assumed that a global variable is ``single`` if neither ``multi`` nor\n  ``single`` is specified. All non-computed global variables must be *single*.\n\n:eql:synopsis:`<name>`\n  The name of the global variable. It can be fully-qualified with the module\n  name, or it is assumed to belong to the module in which it appears.\n\n:eql:synopsis:`<type>`\n  The type must be a valid :ref:`type expression <ref_eql_types>` denoting a\n  non-abstract scalar or a container type.\n\n:eql:synopsis:`<name> := <expression>`\n  Defines a *computed* global variable. The provided expression must be a\n  :ref:`Stable <ref_reference_volatility>` EdgeQL expression. It can refer\n  to other global variables. The type of a *computed* global variable is\n  not limited to scalar and container types; it can also be an object type.\n\nThe valid SDL sub-declarations are:\n\n:eql:synopsis:`default := <expression>`\n  Specifies the default value for the global variable as an EdgeQL\n  expression. The default value is used in a session if the value was not\n  explicitly specified by the client, or was reset with the :ref:`reset\n  <ref_eql_statements_session_reset_alias>` command.\n\n:sdl:synopsis:`<annotation-declarations>`\n  Set global variable :ref:`annotation <ref_eql_sdl_annotations>`\n  to a given *value*.\n\n\nExamples\n--------\n\nDeclare a new global variable:\n\n.. code-block:: sdl\n\n  global current_user_id: uuid;\n  global current_user := (\n      select User filter .id = global current_user_id\n  );\n\nSet the global variable to a specific value using :ref:`session-level commands\n<ref_eql_statements_session_set_alias>`:\n\n.. code-block:: edgeql\n\n  set global current_user_id :=\n      <uuid>'00ea8eaa-02f9-11ed-a676-6bd11cc6c557';\n\nUse the computed global variable that is based on the value that was just set:\n\n.. code-block:: edgeql\n\n  select global current_user { name };\n\n:ref:`Reset <ref_eql_statements_session_reset_alias>` the global variable to\nits default value:\n\n.. code-block:: edgeql\n\n  reset global user_id;\n\n\n.. _ref_eql_ddl_globals:\n\n\nDDL commands\n============\n\nThis section describes the low-level DDL commands for creating, altering, and\ndropping globals. You typically don't need to use these commands directly, but\nknowing about them is useful for reviewing migrations.\n\n\nCreate global\n-------------\n\n:eql-statement:\n:eql-haswith:\n\nDeclare a new global variable using DDL.\n\n.. eql:synopsis::\n\n  [ with <with-item> [, ...] ]\n  create [{required | optional}] [single]\n    global <name>: <type>\n      [ \"{\" <subcommand>; [...] \"}\" ] ;\n\n  # Computed global variable form:\n\n  [ with <with-item> [, ...] ]\n  create [{required | optional}] [{single | multi}]\n    global <name> := <expression>;\n\n  # where <subcommand> is one of\n\n    set default := <expression>\n    create annotation <annotation-name> := <value>\n\nDescription\n^^^^^^^^^^^\n\nAs with SDL, there are two different forms of ``global`` declaration:\n\n- A global variable that can be :ref:`set <ref_eql_statements_session_set_alias>`\n  in a session.\n- A *computed* global that is derived from an expression (and so cannot be\n  directly set in a session).\n\nThe subcommands mirror those in SDL:\n\n:eql:synopsis:`set default := <expression>`\n  Specifies the default value for the global variable as an EdgeQL\n  expression. The default value is used by the session if the value was not\n  explicitly specified or was reset with the :ref:`reset\n  <ref_eql_statements_session_reset_alias>` command.\n\n:eql:synopsis:`create annotation <annotation-name> := <value>`\n  Assign an annotation to the global variable. See :eql:stmt:`create annotation`\n  for details.\n\n\nExamples\n^^^^^^^^\n\nDefine a new global property ``current_user_id``:\n\n.. code-block:: edgeql\n\n  create global current_user_id: uuid;\n\nDefine a new *computed* global property ``current_user`` based on the\npreviously defined ``current_user_id``:\n\n.. code-block:: edgeql\n\n  create global current_user := (\n      select User filter .id = global current_user_id\n  );\n\n\nAlter global\n------------\n\n:eql-statement:\n:eql-haswith:\n\nChange the definition of a global variable.\n\n.. eql:synopsis::\n\n  [ with <with-item> [, ...] ]\n  alter global <name>\n    [ \"{\" <subcommand>; [...] \"}\" ] ;\n\n  # where <subcommand> is one of\n\n    set default := <expression>\n    reset default\n    rename to <newname>\n    set required\n    set optional\n    reset optionalily\n    set single\n    set multi\n    reset cardinality\n    set type <typename> reset to default\n    using (<computed-expr>)\n    create annotation <annotation-name> := <value>\n    alter annotation <annotation-name> := <value>\n    drop annotation <annotation-name>\n\nDescription\n^^^^^^^^^^^\n\nThe command :eql:synopsis:`alter global` changes the definition of a global\nvariable. It can modify default values, rename the global, or change other\nattributes like optionality, cardinality, computed expressions, etc.\n\nExamples\n^^^^^^^^\n\nSet the ``description`` annotation of global variable ``current_user``:\n\n.. code-block:: edgeql\n\n  alter global current_user\n      create annotation description :=\n          'Current User as specified by the global ID';\n\nMake the ``current_user_id`` global variable ``required``:\n\n.. code-block:: edgeql\n\n  alter global current_user_id {\n      set required;\n      # A required global variable MUST have a default value.\n      set default := <uuid>'00ea8eaa-02f9-11ed-a676-6bd11cc6c557';\n  }\n\n\nDrop global\n-----------\n\n:eql-statement:\n:eql-haswith:\n\nRemove a global variable from the schema.\n\n.. eql:synopsis::\n\n  [ with <with-item> [, ...] ]\n  drop global <name> ;\n\nDescription\n^^^^^^^^^^^\n\nThe command :eql:synopsis:`drop global` removes the specified global variable\nfrom the schema.\n\nExample\n^^^^^^^\n\nRemove the ``current_user`` global variable:\n\n.. code-block:: edgeql\n\n  drop global current_user;\n"
  },
  {
    "path": "docs/reference/datamodel/index.rst",
    "content": ".. versioned-section::\n\n.. _ref_datamodel_index:\n\n======\nSchema\n======\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    objects\n    properties\n    links\n    computeds\n    primitives\n    indexes\n    constraints\n    inheritance\n    aliases\n    globals\n    access_policies\n    permissions\n    functions\n    triggers\n    mutation_rewrites\n    linkprops\n    modules\n    migrations\n    branches\n    extensions\n    annotations\n    future\n    comparison\n    introspection/index\n\n\n|Gel| schema is a high-level description of your application's data model.\nIn the schema, you define your types, links, access policies, functions,\ntriggers, constraints, indexes, and more.\n\nGel schema is strictly typed and is high-level enough to be mapped directly\nto mainstream programming languages and back.\n\n\n.. _ref_eql_sdl:\n\nSchema Definition Language\n==========================\n\nMigrations are sequences of *data definition language* (DDL) commands.\nDDL is a low-level language that tells the database exactly how to change\nthe schema. You typically won't need to write any DDL by hand; the Gel server\nwill generate it for you.\n\nFor a full guide on migrations, refer to the :ref:`Creating and applying\nmigrations <ref_intro_migrations>` guide or the\n:ref:`migrations reference <ref_datamodel_migrations>` section.\n\n\nExample:\n\n.. code-block:: sdl\n\n    # dbschema/default.gel\n\n    type Movie {\n      required title: str;\n      required director: Person;\n    }\n\n    type Person {\n      required name: str;\n    }\n\n.. important::\n\n  Syntax highlighter packages/extensions for |.gel| files are available for\n  `Visual Studio Code <https://marketplace.visualstudio.com/\n  itemdetails?itemName=magicstack.edgedb>`_,\n  `Sublime Text <https://packagecontrol.io/packages/EdgeDB>`_,\n  `Atom <https://atom.io/packages/edgedb>`_, and `Vim <https://github.com/\n  geldata/edgedb-vim>`_.\n\nMigrations and DDL\n==================\n\nGel's baked-in migration system lets you painlessly evolve your schema over\ntime. Just update the contents of your |.gel| file(s) and use the |Gel| CLI\nto *create* and *apply* migrations.\n\n.. code-block:: bash\n\n  $ gel migration create\n  Created dbschema/migrations/00001.edgeql\n  $ gel migrate\n  Applied dbschema/migrations/00001.edgeql\n\nMigrations are sequences of *data definition language* (DDL) commands.\nDDL is a low level language that tells the database how exactly to change\nthe schema. Don't worry, you won't need to write any DDL directly, the Gel\nserver will generate it for you.\n\nFor a full guide on migrations, refer to the :ref:`Creating and applying\nmigrations <ref_intro_migrations>` guide or the\n:ref:`migrations reference <ref_datamodel_migrations>` section.\n\n\n.. _ref_datamodel_terminology:\n.. _ref_datamodel_instances:\n\nInstances, branches, and modules\n================================\n\nGel is like a stack of containers:\n\n* The *instance* is the running Gel process. Every instance has one or\n  more |branches|. Instances can be created, started, stopped, and\n  destroyed locally with :ref:`gel project <ref_cli_gel_project>`\n  or low-level :ref:`gel instance <ref_cli_gel_instance>` commands.\n\n* A *branch* is where your schema and data live. Branches map to PostgreSQL\n  databases. Like instances, branches can be conveniently created, removed,\n  and switched with the :ref:`gel branch <ref_cli_gel_branch>` commands.\n  Read more about branches in the\n  :ref:`branches reference <ref_datamodel_branches>`.\n\n* A *module* is a collection of types, functions, and other definitions.\n  The default module is called ``default``. Modules are used to organize\n  your schema logically. Read more about modules in the\n  :ref:`modules reference <ref_datamodel_modules>`.\n"
  },
  {
    "path": "docs/reference/datamodel/indexes.rst",
    "content": ".. _ref_datamodel_indexes:\n\n=======\nIndexes\n=======\n\n.. index:: performance, postgres query planner\n\nAn index is a data structure used internally to speed up filtering, ordering,\nand grouping operations in |Gel|. Indexes help accomplish this in two key ways:\n\n- They are pre-sorted, which saves time on costly sort operations on rows.\n- They can be used by the query planner to filter out irrelevant rows.\n\n.. note::\n\n   The Postgres query planner decides when to use indexes for a query. In some\n   cases—e.g. when tables are small—it may be faster to scan the whole table\n   rather than use an index. In such scenarios, the index might be ignored.\n\n   For more information on how the planner decides this, see\n   `the Postgres query planner documentation\n   <https://www.postgresql.org/docs/current/planner-optimizer.html>`_.\n\n\nTradeoffs\n=========\n\nWhile improving query performance, indexes also increase disk and memory usage\nand can slow down insertions and updates. Creating too many indexes may be\ndetrimental; only index properties you often filter, order, or group by.\n\n.. important::\n   **Foreign and primary keys**\n\n   In SQL databases, indexes are commonly used to index *primary keys* and\n   *foreign keys*. Gel's analog to a SQL primary key is the ``id`` field\n   automatically created for each object, while a link in Gel is the analog\n   to a SQL foreign key. Both of these are automatically indexed.\n\n   Moreover, any property with an :eql:constraint:`exclusive` constraint\n   is also automatically indexed.\n\n\nIndex on a property\n===================\n\nMost commonly, indexes are declared within object type declarations and\nreference a particular property. The index can be used to speed up queries\nthat reference that property in a filter, order by, or group by clause:\n\n.. code-block:: sdl\n\n   type User {\n     required name: str;\n     index on (.name);\n   }\n\nBy indexing on ``User.name``, the query planner will have access to that index\nwhen planning queries using the ``name`` property. This may result in better\nperformance as the database can look up a name in the index instead of scanning\nthrough all ``User`` objects sequentially—though ultimately it's up to the\nPostgres query planner whether to use the index.\n\nTo see if an index helps, compare query plans by adding\n:ref:`analyze <ref_cli_gel_analyze>` to your queries.\n\n.. note::\n\n   Even if your database is small now, you may benefit from an index as it grows.\n\n\nIndex on an expression\n======================\n\nIndexes may be defined using an arbitrary *singleton* expression that\nreferences multiple properties of the enclosing object type.\n\n.. important::\n   A singleton expression is an expression that's guaranteed to return\n   *at most one* element. As such, you can't index on a ``multi`` property.\n\nExample:\n\n.. code-block:: sdl\n\n   type User {\n     required first_name: str;\n     required last_name: str;\n     index on (str_lower(.first_name + ' ' + .last_name));\n   }\n\n\nIndex on multiple properties\n============================\n\nA *composite index* references multiple properties. This can speed up queries\nthat filter, order, or group on multiple properties at once.\n\n.. note::\n\n   An index on multiple properties may also be used in queries where only a\n   single property in the index is referenced. In many traditional database\n   systems, placing the most frequently used columns first in the composite\n   index can improve the likelihood of its use.\n\n   Read `the Postgres documentation on multicolumn indexes\n   <https://www.postgresql.org/docs/current/indexes-multicolumn.html>`_ to learn\n   more about how the query planner uses these indexes.\n\nIn |Gel|, a composite index is created by indexing on a ``tuple`` of properties:\n\n.. code-block:: sdl\n\n   type User {\n     required name: str;\n     required email: str;\n     index on ((.name, .email));\n   }\n\n\nIndex on a link property\n========================\n\nLink properties can also be indexed. The special placeholder\n``__subject__`` refers to the source object in a link property expression:\n\n.. code-block:: sdl\n\n   abstract link friendship {\n     strength: float64;\n     index on (__subject__@strength);\n   }\n\n   type User {\n     multi friends: User {\n       extending friendship;\n     };\n   }\n\nExclude objects from an index\n=============================\n\nWhen specifying an index, you can provide an optional ``except`` clause to exclude objects from the index. This is known as creating a *partial index*. Partial indexes are particularly useful in scenarios where you frequently query a subset of data that meets certain criteria, while consistently excluding other data. For example, if you often filter on a property but always exclude objects with a specific value for another property, a partial index can optimize these queries by indexing only the relevant subset of data, thus improving query performance and reducing index size.\n\n.. code-block:: sdl\n\n  type User {\n    required name: str;\n    required email: str;\n    archived_at: datetime;\n\n    index on (.name) except (exists .archived_at);\n  }\n\n\nSpecify a Postgres index type\n=============================\n\n.. api-index:: pg::hash, pg::btree, pg::gin, pg::gist, pg::spgist, pg::brin\n\n.. versionadded:: 3.0\n\nGel exposes Postgres index types that can be used directly in schemas via\nthe ``pg`` module:\n\n- ``pg::hash`` : Index based on a 32-bit hash of the value\n- ``pg::btree`` : B-tree index (can help with sorted data retrieval)\n- ``pg::gin`` : Inverted index for multi-element data (arrays, JSON)\n- ``pg::gist`` : Generalized Search Tree for range and geometric searches\n- ``pg::spgist`` : Space-partitioned GiST\n- ``pg::brin`` : Block Range INdex\n\nExample:\n\n.. code-block:: sdl\n\n   type User {\n     required name: str;\n     index pg::spgist on (.name);\n   }\n\n\n.. _ref_datamodel_indexes_concurrent:\n\nConcurrent index building\n=========================\n\nWhen creating an index, the object type will be locked for writes. This means\nthat until the index is created, all ``insert``, ``update`` and ``delete``\nqueries will be put on hold.\nOn types containing many objects, this can span minutes or even hours.\n\nInstead, index building can be deferred from migration application to a later\ntime. To do this, set ``build_concurrently`` index property to ``true``:\n\n.. code-block:: sdl\n\n   type User {\n     name: str;\n     index on (.name) {\n       build_concurrently := true;\n     };\n   }\n\nWhen this schema in applied to an instance, the index will be created, but it\nwill not yet be active. The migration will not attempt to read any objects to\nbuild the index.\n\nAs the last step of :gelcmd:`migration apply` (and :gelcmd:`migrate`), index\nwill actually be built. During this time, the object type will not be locked\nfor reads or writes.\n\nThis means that migration will lock for significantly less time and allow index\nthe be created while new writes are applied to the database.\n\nTo apply migrations, but not build indexes at all, use\n:gelcmd:`migration apply --no-index-build` flag.\nThis allows index building to be triggered at a later time,\nby using :gelcmd:`migration apply` again.\n\nUntil the index is created, it will not be used to speed up queries.\n\nFor tradeoffs of concurrent index building, refer to\n`PostgreSQL documentation <https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY>`_.\n\n\nAnnotate an index\n=================\n\nIndexes can include annotations:\n\n.. code-block:: sdl\n\n   type User {\n     name: str;\n     index on (.name) {\n       annotation description := 'Indexing all users by name.';\n     };\n   }\n\n\n.. _ref_eql_sdl_indexes:\n\nDeclaring indexes\n=================\n\n.. api-index:: index on, except\n\nThis section describes the syntax to use indexes in your schema.\n\nSyntax\n------\n\n.. sdl:synopsis::\n\n   index on ( <index-expr> )\n   [ except ( <except-expr> ) ]\n   [ \"{\" <annotation-declarations> \"}\" ] ;\n\n.. rubric:: Description\n\n- :sdl:synopsis:`on ( <index-expr> )`\n\n  The expression to index. It must be :ref:`Immutable <ref_reference_volatility>`\n  but may refer to the indexed object's properties/links. The expression itself\n  must be parenthesized.\n\n- :eql:synopsis:`except ( <except-expr> )`\n\n  An optional condition. If ``<except-expr>`` evaluates to ``true``, the object\n  is omitted from the index; if ``false`` or empty, it is included.\n\n- :sdl:synopsis:`<annotation-declarations>`\n\n  Allows setting index :ref:`annotation <ref_eql_sdl_annotations>` to a given\n  value.\n\n- :sdl:synopsis:`build_concurrently := <bool>`\n\n  Allows index to be built\n  :ref:`after migration is applied <ref_datamodel_indexes_concurrent>`\n  to the instance.\n\n.. _ref_eql_ddl_indexes:\n\nDDL commands\n============\n\nThis section describes the low-level DDL commands for creating, altering, and\ndropping indexes. You typically don't need to use these commands directly, but\nknowing about them is useful for reviewing migrations.\n\n\nCreate index\n------------\n\n:eql-statement:\n\n.. eql:synopsis::\n\n   create index on ( <index-expr> )\n   [ except ( <except-expr> ) ]\n   [ \"{\" <subcommand>; [...] \"}\" ] ;\n\n   # where <subcommand> is one of\n\n     create annotation <annotation-name> := <value>\n\nCreates a new index for a given object type or link using *index-expr*.\n\n- Most parameters/options match those in\n  :ref:`Declaring indexes <ref_eql_sdl_indexes>`.\n\n- Allowed subcommand:\n\n  :eql:synopsis:`create annotation <annotation-name> := <value>`\n     Assign an annotation to this index.\n     See :eql:stmt:`create annotation` for details.\n\n- :eql:synopsis:`set build_concurrently := <bool>`\n\n  Allows index to be built\n  :ref:`after migration is applied <ref_datamodel_indexes_concurrent>`\n  to the instance.\n\nExample:\n\n.. code-block:: edgeql\n\n   create type User {\n     create property name: str {\n       set default := '';\n     };\n\n     create index on (.name);\n   };\n\n\nAlter index\n-----------\n\n:eql-statement:\n\nAlter the definition of an index.\n\n.. eql:synopsis::\n\n   alter index on ( <index-expr> ) [ except ( <except-expr> ) ]\n   [ \"{\" <subcommand>; [...] \"}\" ] ;\n\n   # where <subcommand> is one of\n\n     create annotation <annotation-name> := <value>\n     alter annotation <annotation-name> := <value>\n     drop annotation <annotation-name>\n\nThe command ``alter index`` is used to change the :ref:`annotations\n<ref_datamodel_annotations>` of an index. The *index-expr* is used to\nidentify the index to be altered.\n\n:sdl:synopsis:`on ( <index-expr> )`\n    The specific expression for which the index is made.  Note also\n    that ``<index-expr>`` itself has to be parenthesized.\n\nThe following subcommands are allowed in the ``alter index`` block:\n\n:eql:synopsis:`create annotation <annotation-name> := <value>`\n    Set index :eql:synopsis:`<annotation-name>` to\n    :eql:synopsis:`<value>`.\n    See :eql:stmt:`create annotation` for details.\n\n:eql:synopsis:`alter annotation <annotation-name>;`\n    Alter index :eql:synopsis:`<annotation-name>`.\n    See :eql:stmt:`alter annotation` for details.\n\n:eql:synopsis:`drop annotation <annotation-name>;`\n    Remove constraint :eql:synopsis:`<annotation-name>`.\n    See :eql:stmt:`drop annotation` for details.\n\n\nExample:\n\n.. code-block:: edgeql\n\n   alter type User {\n     alter index on (.name) {\n       create annotation title := 'User name index';\n     };\n   };\n\n\nDrop index\n----------\n\n:eql-statement:\n\nRemove an index from a given schema item.\n\n.. eql:synopsis::\n\n   drop index on ( <index-expr> ) [ except ( <except-expr> ) ] ;\n\nRemoves an index from a schema item.\n\n- :sdl:synopsis:`on ( <index-expr> )` identifies the indexed expression.\n\nThis statement can only be used as a subdefinition in another DDL statement.\n\nExample:\n\n.. code-block:: edgeql\n\n   alter type User {\n     drop index on (.name);\n   };\n\n\n.. list-table::\n   :class: seealso\n\n   * - **See also**\n     - :ref:`Introspection > Indexes <ref_datamodel_introspection_indexes>`\n"
  },
  {
    "path": "docs/reference/datamodel/inheritance.rst",
    "content": ".. _ref_datamodel_inheritance:\n\n===========\nInheritance\n===========\n\n.. index:: extending, extends, subtype, supertype, parent type, child type\n\nInheritance is a crucial aspect of schema modeling in Gel. Schema items can\n*extend* one or more parent types. When extending, the child (subclass)\ninherits the definition of its parents (superclass).\n\nYou can declare ``abstract`` object types, properties, links, constraints, and\nannotations.\n\n- :ref:`Objects <ref_datamodel_inheritance_objects>`\n- :ref:`Properties <ref_datamodel_inheritance_props>`\n- :ref:`Links <ref_datamodel_inheritance_links>`\n- :ref:`Constraints <ref_datamodel_inheritance_constraints>`\n- :ref:`Annotations <ref_datamodel_inheritance_annotations>`\n\n.. _ref_datamodel_inheritance_objects:\n\nObject types\n------------\n\n.. api-index:: abstract type, extending\n\nObject types can *extend* other object types. The extending type (AKA the\n*subtype*) inherits all links, properties, indexes, constraints, etc. from its\n*supertypes*.\n\n.. code-block:: sdl\n\n    abstract type Animal {\n      species: str;\n    }\n\n    type Dog extending Animal {\n      breed: str;\n    }\n\nBoth abstract and concrete object types can be extended. Whether to make a\ntype abstract or concrete is a fairly simple decision: if you need to be\nable to insert objects of the type, make it a concrete type. If objects of\nthe type should never be inserted and it exists only to be extended, make it\nan abstract one. In the schema below the ``Animal`` type is now concrete\nand can be inserted, which was not the case in the example above. The new\n``CanBark`` type however is abstract and thus the database will not have\nany individual ``CanBark`` objects.\n\n.. code-block:: sdl\n\n    abstract type CanBark {\n      required bark_sound: str;\n    }\n\n    type Animal {\n      species: str;\n    }\n\n    type Dog extending Animal, CanBark {\n      breed: str;\n    }\n\n\nFor details on querying polymorphic data, see :ref:`EdgeQL > Select >\nPolymorphic queries <ref_eql_select_polymorphic>`. When using the SQL adapter,\nsee :ref:`SQL adapter <ref_sql_adapter>` for information about using ``ONLY``\nto query parent tables without including child objects.\n\n.. _ref_datamodel_inheritance_multiple:\n\nMultiple Inheritance\n^^^^^^^^^^^^^^^^^^^^\n\nObject types can :ref:`extend more\nthan one type <ref_eql_sdl_object_types_inheritance>` — that's called\n*multiple inheritance*. This mechanism allows building complex object\ntypes out of combinations of more basic types.\n\n.. code-block:: sdl\n\n    abstract type HasName {\n      first_name: str;\n      last_name: str;\n    }\n\n    abstract type HasEmail {\n      email: str;\n    }\n\n    type Person extending HasName, HasEmail {\n      profession: str;\n    }\n\n\n.. _ref_datamodel_overloading:\n\nOverloading\n^^^^^^^^^^^\n\n.. api-index:: overloaded\n\nAn object type can overload an inherited property or link. All overloaded\ndeclarations must be prefixed with the ``overloaded`` prefix to avoid\nunintentional overloads.\n\n.. code-block:: sdl\n\n    abstract type Person {\n      name: str;\n      multi friends: Person;\n    }\n\n    type Student extending Person {\n      overloaded name: str {\n        constraint exclusive;\n      }\n      overloaded multi friends: Student;\n    }\n\n\nOverloaded fields cannot *generalize* the associated type; it can only make it\n*more specific* by setting the type to a subtype of the original or adding\nadditional constraints.\n\n.. _ref_datamodel_inheritance_props:\n\nProperties\n----------\n\n.. api-index:: abstract property, readonly\n\nProperties can be *concrete* (the default) or *abstract*. Abstract properties\nare declared independent of a source or target, can contain :ref:`annotations\n<ref_datamodel_annotations>`, and can be marked as ``readonly``.\n\n.. code-block:: sdl\n\n  abstract property title_prop {\n    annotation title := 'A title.';\n    readonly := false;\n  }\n\n.. _ref_datamodel_inheritance_links:\n\nLinks\n-----\n\n.. api-index:: abstract link\n\nIt's possible to define ``abstract`` links that aren't tied to a particular\n*source* or *target*. Abstract links can be marked as readonly and contain\nannotations, property declarations, constraints, and indexes.\n\n.. code-block:: sdl\n\n    abstract link link_with_strength {\n      strength: float64;\n      index on (__subject__@strength);\n    }\n\n    type Person {\n      multi friends: Person {\n        extending link_with_strength;\n      };\n    }\n\n.. _ref_datamodel_inheritance_constraints:\n\nConstraints\n-----------\n\n.. api-index:: abstract constraint, using, errmessage\n\nUse ``abstract`` to declare reusable, user-defined constraint types.\n\n.. code-block:: sdl\n\n    abstract constraint in_range(min: anyreal, max: anyreal) {\n      errmessage :=\n        'Value must be in range [{min}, {max}].';\n      using (min <= __subject__ and __subject__ < max);\n    }\n\n    type Player {\n      points: int64 {\n        constraint in_range(0, 100);\n      }\n    }\n\n.. _ref_datamodel_inheritance_annotations:\n\nAnnotations\n-----------\n\n.. api-index:: abstract annotation, inheritable\n\nEdgeQL supports three annotation types by default: ``title``, ``description``,\nand ``deprecated``. Use ``abstract annotation`` to declare custom user-defined\nannotation types.\n\n.. code-block:: sdl\n\n  abstract annotation admin_note;\n\n  type Status {\n    annotation admin_note := 'system-critical';\n    # more properties\n  }\n\nBy default, annotations defined on abstract types, properties, and links will\nnot be inherited by their subtypes. To override this behavior, use the\n``inheritable`` modifier.\n\n.. code-block:: sdl\n\n  abstract inheritable annotation admin_note;\n\n"
  },
  {
    "path": "docs/reference/datamodel/introspection/casts.rst",
    "content": ".. _ref_datamodel_introspection_casts:\n\n=====\nCasts\n=====\n\nThis section describes introspection of Gel :eql:op:`type casts\n<cast>`. Features like whether the casts are implicit can be\ndiscovered by introspecting ``schema::Cast``.\n\nIntrospection of the ``schema::Cast``:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ObjectType {\n    ...     name,\n    ...     links: {\n    ...         name,\n    ...     },\n    ...     properties: {\n    ...         name,\n    ...     }\n    ... }\n    ... filter .name = 'schema::Cast';\n    {\n        Object {\n            name: 'schema::Cast',\n            links: {\n                Object { name: '__type__' },\n                Object { name: 'from_type' },\n                Object { name: 'to_type' }\n            },\n            properties: {\n                Object { name: 'allow_assignment' },\n                Object { name: 'allow_implicit' },\n                Object { name: 'id' },\n                Object { name: 'name' }\n            }\n        }\n    }\n\nIntrospection of the possible casts from ``std::int64`` to other\ntypes:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select Cast {\n    ...     allow_assignment,\n    ...     allow_implicit,\n    ...     to_type: { name },\n    ... }\n    ... filter .from_type.name = 'std::int64'\n    ... order by .to_type.name;\n    {\n        Object {\n            allow_assignment: false,\n            allow_implicit: true,\n            to_type: Object { name: 'std::bigint' }\n        },\n        Object {\n            allow_assignment: false,\n            allow_implicit: true,\n            to_type: Object { name: 'std::decimal' }\n        },\n        Object {\n            allow_assignment: true,\n            allow_implicit: false,\n            to_type: Object { name: 'std::float32' }\n        },\n        Object {\n            allow_assignment: false,\n            allow_implicit: true,\n            to_type: Object { name: 'std::float64' }\n        },\n        Object {\n            allow_assignment: true,\n            allow_implicit: false,\n            to_type: Object { name: 'std::int16' }\n        },\n        Object {\n            allow_assignment: true,\n            allow_implicit: false,\n            to_type: Object { name: 'std::int32' }\n        },\n        Object {\n            allow_assignment: false,\n            allow_implicit: false,\n            to_type: Object { name: 'std::json' }\n        },\n        Object {\n            allow_assignment: false,\n            allow_implicit: false,\n            to_type: Object { name: 'std::str' }\n        }\n    }\n\nThe ``allow_implicit`` property tells whether this is an *implicit cast*\nin all contexts (such as when determining the type of a set of mixed\nliterals or resolving the argument types of functions or operators if\nthere's no exact match). For example, a literal ``1`` is an\n:eql:type:`int64` and it is implicitly cast into a :eql:type:`bigint`\nor :eql:type:`float64` if it is added to a set containing either one\nof those types:\n\n.. code-block:: edgeql-repl\n\n    db> select {1, 2n};\n    {1n, 2n}\n    db> select {1, 2.0};\n    {1.0, 2.0}\n\nWhat happens if there's no implicit cast between a couple of scalars\nin this type of example? Gel checks whether there's a scalar type\nsuch that all of the set elements can be implicitly cast into that:\n\n.. code-block:: edgeql-repl\n\n    db> select introspect (typeof {<int64>1, <float32>2}).name;\n    {'std::float64'}\n\nThe scalar types :eql:type:`int64` and :eql:type:`float32` cannot be\nimplicitly cast into each other, but they both can be implicitly cast\ninto :eql:type:`float64`.\n\nThe ``allow_assignment`` property tells whether this is an implicit\ncast during assignment if a more general *implicit cast* is not\nallowed. For example, consider the following type:\n\n.. code-block:: sdl\n\n    type Example {\n        property p_int16: int16;\n        property p_float32: float32;\n        property p_json: json;\n    }\n\n.. code-block:: edgeql-repl\n\n    db> insert Example {\n    ...     p_int16 := 1,\n    ...     p_float32 := 2\n    ... };\n    {Object { id: <uuid>'...' }}\n    db> insert Example {\n    ...     p_json := 3  # assignment cast to json not allowed\n    ... };\n    InvalidPropertyTargetError: invalid target for property\n    'p_json' of object type 'default::Example': 'std::int64'\n    (expecting 'std::json')\n"
  },
  {
    "path": "docs/reference/datamodel/introspection/colltypes.rst",
    "content": ".. _ref_datamodel_introspection_collection_types:\n\n================\nCollection types\n================\n\nThis section describes introspection of :ref:`collection types\n<ref_datamodel_collection_types>`.\n\n\nArray\n-----\n\nIntrospection of the ``schema::Array``:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ObjectType {\n    ...     name,\n    ...     links: {\n    ...         name,\n    ...     },\n    ...     properties: {\n    ...         name,\n    ...     }\n    ... }\n    ... filter .name = 'schema::Array';\n    {\n        Object {\n            name: 'schema::Array',\n            links: {\n                Object { name: '__type__' },\n                Object { name: 'element_type' }\n            },\n            properties: {\n                Object { name: 'id' },\n                Object { name: 'name' }\n            }\n        }\n    }\n\nFor a type with an :eql:type:`array` property, consider the following:\n\n.. code-block:: sdl\n\n    type User {\n        required property name: str;\n        property favorites: array<str>;\n    }\n\nIntrospection of the ``User`` with emphasis on properties:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ObjectType {\n    ...     name,\n    ...     properties: {\n    ...         name,\n    ...         target: {\n    ...             name,\n    ...             [is Array].element_type: { name },\n    ...         },\n    ...     },\n    ... }\n    ... filter .name = 'default::User';\n    {\n        Object {\n            name: 'default::User',\n            properties: {\n                Object {\n                    name: 'favorites',\n                    target: Object {\n                        name: 'array',\n                        element_type: Object { name: 'std::str' }\n                    }\n                },\n                ...\n            }\n        }\n    }\n\n\nTuple\n-----\n\nIntrospection of the ``schema::Tuple``:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ObjectType {\n    ...     name,\n    ...     links: {\n    ...         name,\n    ...     },\n    ...     properties: {\n    ...         name,\n    ...     }\n    ... }\n    ... filter .name = 'schema::Tuple';\n    {\n        Object {\n            name: 'schema::Tuple',\n            links: {\n                Object { name: '__type__' },\n                Object { name: 'element_types' }\n            },\n            properties: {\n                Object { name: 'id' },\n                Object { name: 'name' }\n            }\n        }\n    }\n\nFor example, below is an introspection of the return type of\nthe :eql:func:`sys::get_version` function:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select `Function` {\n    ...     return_type[is Tuple]: {\n    ...         element_types: {\n    ...             name,\n    ...             type: { name }\n    ...         } order by .num\n    ...     }\n    ... }\n    ... filter .name = 'sys::get_version';\n    {\n        Object {\n            return_type: Object {\n                element_types: {\n                    Object {\n                        name: 'major',\n                        type: Object {\n                            name: 'std::int64'\n                        }\n                    },\n                    Object {\n                        name: 'minor',\n                        type: Object {\n                            name: 'std::int64'\n                        }\n                    },\n                    Object {\n                        name: 'stage',\n                        type: Object {\n                            name: 'sys::VersionStage'\n                        }\n                    },\n                    Object {\n                        name: 'stage_no',\n                        type: Object {\n                            name: 'std::int64'\n                        }\n                    },\n                    Object {\n                        name: 'local',\n                        type: Object { name: 'array' }\n                    }\n                }\n            }\n        }\n    }\n"
  },
  {
    "path": "docs/reference/datamodel/introspection/constraints.rst",
    "content": ".. _ref_datamodel_introspection_constraints:\n\n===========\nConstraints\n===========\n\nThis section describes introspection of :ref:`constraints\n<ref_datamodel_constraints>`.\n\nIntrospection of the ``schema::Constraint``:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ObjectType {\n    ...     name,\n    ...     links: {\n    ...         name,\n    ...     },\n    ...     properties: {\n    ...         name,\n    ...     }\n    ... }\n    ... filter .name = 'schema::Constraint';\n    {\n        Object {\n            name: 'schema::Constraint',\n            links: {\n                Object { name: '__type__' },\n                Object { name: 'args' },\n                Object { name: 'annotations' },\n                Object { name: 'bases' },\n                Object { name: 'ancestors' },\n                Object { name: 'params' },\n                Object { name: 'return_type' },\n                Object { name: 'subject' }\n            },\n            properties: {\n                Object { name: 'errmessage' },\n                Object { name: 'expr' },\n                Object { name: 'finalexpr' },\n                Object { name: 'id' },\n                Object { name: 'abstract' },\n                Object { name: 'name' },\n                Object { name: 'return_typemod' },\n                Object { name: 'subjectexpr' }\n            }\n        }\n    }\n\nConsider the following schema:\n\n.. code-block:: sdl\n\n    scalar type maxex_100 extending int64 {\n        constraint max_ex_value(100);\n    }\n\nIntrospection of the scalar ``maxex_100`` with focus on the constraint:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ScalarType {\n    ...     name,\n    ...     constraints: {\n    ...         name,\n    ...         expr,\n    ...         annotations: { name, @value },\n    ...         subject: { name },\n    ...         params: { name, @value, type: { name } },\n    ...         return_typemod,\n    ...         return_type: { name },\n    ...         errmessage,\n    ...     },\n    ... }\n    ... filter .name = 'default::maxex_100';\n    {\n        Object {\n            name: 'default::maxex_100',\n            constraints: {\n                Object {\n                    name: 'std::max_ex_value',\n                    expr: '(__subject__ <= max)',\n                    annotations: {},\n                    subject: Object { name: 'default::maxex_100' },\n                    params: {\n                        Object {\n                            name: 'max',\n                            type: Object { name: 'anytype' },\n                            @value: '100'\n                        }\n                    },\n                    return_typemod: 'SingletonType',\n                    return_type: Object { name: 'std::bool' }\n                    errmessage: '{__subject__} must be less ...',\n                }\n            }\n        }\n    }\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Schema > Constraints <ref_datamodel_constraints>`\n  * - :ref:`SDL > Constraints <ref_eql_sdl_constraints>`\n  * - :ref:`DDL > Constraints <ref_eql_ddl_constraints>`\n  * - :ref:`Standard Library > Constraints <ref_std_constraints>`\n"
  },
  {
    "path": "docs/reference/datamodel/introspection/functions.rst",
    "content": ".. _ref_datamodel_introspection_functions:\n\n=========\nFunctions\n=========\n\nThis section describes introspection of :ref:`functions\n<ref_datamodel_functions>`.\n\nIntrospection of the ``schema::Function``:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ObjectType {\n    ...     name,\n    ...     links: {\n    ...         name,\n    ...     },\n    ...     properties: {\n    ...         name,\n    ...     }\n    ... }\n    ... filter .name = 'schema::Function';\n    {\n        Object {\n            name: 'schema::Function',\n            links: {\n                Object { name: '__type__' },\n                Object { name: 'annotations' },\n                Object { name: 'params' },\n                Object { name: 'return_type' }\n            },\n            properties: {\n                Object { name: 'id' },\n                Object { name: 'name' },\n                Object { name: 'return_typemod' }\n            }\n        }\n    }\n\nSince ``params`` are quite important to functions, here's their structure:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ObjectType {\n    ...     name,\n    ...     links: {\n    ...         name,\n    ...     },\n    ...     properties: {\n    ...         name,\n    ...     }\n    ... }\n    ... filter .name = 'schema::Parameter';\n    {\n        Object {\n            name: 'schema::Parameter',\n            links: {\n                Object { name: '__type__' },\n                Object { name: 'type' }\n            },\n            properties: {\n                Object { name: 'default' },\n                Object { name: 'id' },\n                Object { name: 'kind' },\n                Object { name: 'name' },\n                Object { name: 'num' },\n                Object { name: 'typemod' }\n            }\n        }\n    }\n\nIntrospection of the built-in :eql:func:`count`:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select `Function` {\n    ...     name,\n    ...     annotations: { name, @value },\n    ...     params: {\n    ...         kind,\n    ...         name,\n    ...         num,\n    ...         typemod,\n    ...         type: { name },\n    ...         default,\n    ...     },\n    ...     return_typemod,\n    ...     return_type: { name },\n    ... }\n    ... filter .name = 'std::count';\n    {\n        Object {\n            name: 'std::count',\n            annotations: {},\n            params: {\n                Object {\n                    kind: 'PositionalParam',\n                    name: 's',\n                    num: 0,\n                    typemod: 'SetOfType',\n                    type: Object { name: 'anytype' },\n                    default: {}\n                }\n            },\n            return_typemod: 'SingletonType',\n            return_type: Object { name: 'std::int64' }\n        }\n    }\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Schema > Functions <ref_datamodel_functions>`\n  * - :ref:`SDL > Functions <ref_eql_sdl_functions>`\n  * - :ref:`DDL > Functions <ref_eql_ddl_functions>`\n  * - :ref:`Reference > Function calls <ref_reference_function_call>`\n  * - :ref:`Cheatsheets > Functions <ref_cheatsheet_functions>`\n\n"
  },
  {
    "path": "docs/reference/datamodel/introspection/index.rst",
    "content": ".. _ref_datamodel_introspection:\n\nIntrospection\n=============\n\n.. index:: schema module\n.. api-index:: describe, introspect, typeof\n\nAll of the schema information in Gel is stored in the ``schema``\n:ref:`module <ref_datamodel_modules>` and is accessible via\n*introspection queries*.\n\nAll the introspection types are themselves extending\n:eql:type:`BaseObject`, so they are also subject to introspection :ref:`as\nobject types <ref_datamodel_introspection_object_types>`. The following\nquery will give a list of all the types used in introspection:\n\n.. code-block:: edgeql\n\n    select name := schema::ObjectType.name\n    filter name like 'schema::%';\n\nThere's also a couple of ways of getting the introspection type of a\nparticular expression. Any :eql:type:`Object` has a ``__type__`` link\nto the ``schema::ObjectType``. For scalars there's the\n:eql:op:`introspect` and :eql:op:`typeof` operators that can be used\nto get the type of an expression.\n\nFinally, the command :eql:stmt:`describe` can be used to get\ninformation about Gel types in a variety of human-readable formats.\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    objects\n    scalars\n    colltypes\n    functions\n    triggers\n    mutation_rewrites\n    indexes\n    constraints\n    operators\n    casts\n"
  },
  {
    "path": "docs/reference/datamodel/introspection/indexes.rst",
    "content": ".. _ref_datamodel_introspection_indexes:\n\n=======\nIndexes\n=======\n\nThis section describes introspection of :ref:`indexes\n<ref_datamodel_indexes>`.\n\nIntrospection of the ``schema::Index``:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ObjectType {\n    ...     name,\n    ...     links: {\n    ...         name,\n    ...     },\n    ...     properties: {\n    ...         name,\n    ...     }\n    ... }\n    ... filter .name = 'schema::Index';\n    {\n        Object {\n            name: 'schema::Index',\n            links: {Object { name: '__type__' }},\n            properties: {\n                Object { name: 'expr' },\n                Object { name: 'id' },\n                Object { name: 'name' }\n            }\n        }\n    }\n\nConsider the following schema:\n\n.. code-block:: sdl\n\n    abstract type Addressable {\n        property address: str;\n    }\n\n    type User extending Addressable {\n        # define some properties and a link\n        required property name: str;\n\n        multi link friends: User;\n\n        # define an index for User based on name\n        index on (.name);\n    }\n\nIntrospection of ``User.name`` index:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select Index {\n    ...     expr,\n    ... }\n    ... filter .expr like '%.name';\n    {\n        Object {\n            expr: '.name'\n        }\n    }\n\nFor introspection of the index within the context of its host type see\n:ref:`object type introspection <ref_datamodel_introspection_object_types>`.\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Schema > Indexes <ref_datamodel_indexes>`\n  * - :ref:`SDL > Indexes <ref_eql_sdl_indexes>`\n  * - :ref:`DDL > Indexes <ref_eql_ddl_indexes>`\n"
  },
  {
    "path": "docs/reference/datamodel/introspection/mutation_rewrites.rst",
    "content": ".. _ref_datamodel_introspection_mutation_rewrites:\n\n=================\nMutation rewrites\n=================\n\nThis section describes introspection of :ref:`mutation rewrites\n<ref_datamodel_mutation_rewrites>`.\n\nIntrospection of the ``schema::Rewrite``:\n\n.. code-block:: edgeql-repl\n\n    db> select schema::ObjectType {\n    ...   name,\n    ...   links: {\n    ...     name\n    ...   },\n    ...   properties: {\n    ...     name\n    ...   }\n    ... } filter .name = 'schema::Rewrite';\n    {\n      schema::ObjectType {\n        name: 'schema::Rewrite',\n        links: {\n          schema::Link {name: 'subject'},\n          schema::Link {name: '__type__'},\n          schema::Link {name: 'ancestors'},\n          schema::Link {name: 'bases'},\n          schema::Link {name: 'annotations'}\n        },\n        properties: {\n          schema::Property {name: 'inherited_fields'},\n          schema::Property {name: 'computed_fields'},\n          schema::Property {name: 'builtin'},\n          schema::Property {name: 'internal'},\n          schema::Property {name: 'name'},\n          schema::Property {name: 'id'},\n          schema::Property {name: 'abstract'},\n          schema::Property {name: 'is_abstract'},\n          schema::Property {name: 'final'},\n          schema::Property {name: 'is_final'},\n          schema::Property {name: 'kind'},\n          schema::Property {name: 'expr'},\n        },\n      },\n    }\n\nIntrospection of all properties in the ``default`` schema with a mutation\nrewrite:\n\n.. code-block:: edgeql-repl\n\n    db> select schema::ObjectType {\n    ...   name,\n    ...   properties := (\n    ...     select .properties {\n    ...        name,\n    ...        rewrites: {\n    ...          kind\n    ...        }\n    ...     } filter exists .rewrites\n    ...   )\n    ... } filter .name ilike 'default::%'\n    ... and exists .properties.rewrites;\n    {\n      schema::ObjectType {\n        name: 'default::Post',\n        properties: {\n          schema::Property {\n            name: 'created',\n            rewrites: {\n              schema::Rewrite {\n                kind: Insert\n              }\n            }\n          },\n          schema::Property {\n            name: 'modified',\n            rewrites: {\n              schema::Rewrite {\n              kind: Insert\n              },\n              schema::Rewrite {\n                kind: Update\n              }\n            }\n          },\n        },\n      },\n    }\n\nIntrospection of all rewrites, including the type of query (``kind``),\nrewrite expression, and the object and property they are on:\n\n.. code-block:: edgeql-repl\n\n    db> select schema::Rewrite {\n    ...   subject := (\n    ...     select .subject {\n    ...       name,\n    ...       source: {\n    ...         name\n    ...       }\n    ...     }\n    ...   ),\n    ...   kind,\n    ...   expr\n    ... };\n    {\n      schema::Rewrite {\n        subject: schema::Property {\n          name: 'created',\n          source: schema::ObjectType {\n            name: 'default::Post'\n          }\n        },\n        kind: Insert,\n        expr: 'std::datetime_of_statement()'\n      },\n      schema::Rewrite {\n        subject: schema::Property {\n          name: 'modified',\n          source: schema::ObjectType {\n            name: 'default::Post'\n          }\n        },\n        kind: Insert,\n        expr: 'std::datetime_of_statement()'\n      },\n      schema::Rewrite {\n        subject: schema::Property {\n          name: 'modified',\n          source: schema::ObjectType {\n            name: 'default::Post'\n          }\n        },\n        kind: Update,\n        expr: 'std::datetime_of_statement()'\n      },\n    }\n\nIntrospection of all rewrites on a ``default::Post`` property named\n``modified``:\n\n.. code-block:: edgeql-repl\n\n    db> select schema::Rewrite {kind, expr}\n    ... filter .subject.source.name = 'default::Post'\n    ... and .subject.name = 'modified';\n    {\n      schema::Rewrite {\n        kind: Insert,\n        expr: 'std::datetime_of_statement()'\n      },\n      schema::Rewrite {\n        kind: Update,\n        expr: 'std::datetime_of_statement()'\n      }\n    }\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Schema > Mutation rewrites <ref_datamodel_mutation_rewrites>`\n  * - :ref:`SDL > Mutation rewrites <ref_eql_sdl_mutation_rewrites>`\n  * - :ref:`DDL > Mutation rewrites <ref_eql_ddl_mutation_rewrites>`\n\n"
  },
  {
    "path": "docs/reference/datamodel/introspection/objects.rst",
    "content": ".. _ref_datamodel_introspection_object_types:\n\n============\nObject types\n============\n\nThis section describes introspection of :ref:`object types\n<ref_datamodel_object_types>`.\n\nIntrospection of the ``schema::ObjectType``:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ObjectType {\n    ...     name,\n    ...     links: {\n    ...         name,\n    ...     },\n    ...     properties: {\n    ...         name,\n    ...     }\n    ... }\n    ... filter .name = 'schema::ObjectType';\n    {\n        Object {\n            name: 'schema::ObjectType',\n            links: {\n                Object { name: '__type__' },\n                Object { name: 'annotations' },\n                Object { name: 'bases' },\n                Object { name: 'constraints' },\n                Object { name: 'indexes' },\n                Object { name: 'links' },\n                Object { name: 'ancestors' },\n                Object { name: 'pointers' },\n                Object { name: 'properties' }\n            },\n            properties: {\n                Object { name: 'id' },\n                Object { name: 'abstract' },\n                Object { name: 'name' }\n            }\n        }\n    }\n\nConsider the following schema:\n\n.. code-block:: sdl\n\n    abstract type Addressable {\n        address: str;\n    }\n\n    type User extending Addressable {\n        # define some properties and a link\n        required name: str;\n\n        multi friends: User;\n\n        # define an index for User based on name\n        index on (.name);\n    }\n\nIntrospection of ``User``:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ObjectType {\n    ...     name,\n    ...     abstract,\n    ...     bases: { name },\n    ...     ancestors: { name },\n    ...     annotations: { name, @value },\n    ...     links: {\n    ...         name,\n    ...         cardinality,\n    ...         required,\n    ...         target: { name },\n    ...     },\n    ...     properties: {\n    ...         name,\n    ...         cardinality,\n    ...         required,\n    ...         target: { name },\n    ...     },\n    ...     constraints: { name },\n    ...     indexes: { expr },\n    ... }\n    ... filter .name = 'default::User';\n    {\n        Object {\n            name: 'default::User',\n            abstract: false,\n            bases: {Object { name: 'default::Addressable' }},\n            ancestors: {\n                Object { name: 'std::BaseObject' },\n                Object { name: 'std::Object' },\n                Object { name: 'default::Addressable' }\n            },\n            annotations: {},\n            links: {\n                Object {\n                    name: '__type__',\n                    cardinality: 'One',\n                    required: {},\n                    target: Object { name: 'schema::Type' }\n                },\n                Object {\n                    name: 'friends',\n                    cardinality: 'Many',\n                    required: false,\n                    target: Object { name: 'default::User' }\n                }\n            },\n            properties: {\n                Object {\n                    name: 'address',\n                    cardinality: 'One',\n                    required: false,\n                    target: Object { name: 'std::str' }\n                },\n                Object {\n                    name: 'id',\n                    cardinality: 'One',\n                    required: true,\n                    target: Object { name: 'std::uuid' }\n                },\n                Object {\n                    name: 'name',\n                    cardinality: 'One',\n                    required: true,\n                    target: Object { name: 'std::str' }\n                }\n            },\n            constraints: {},\n            indexes: {\n                Object {\n                    expr: '.name'\n                }\n            }\n        }\n    }\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Schema > Object types <ref_datamodel_object_types>`\n  * - :ref:`SDL > Object types <ref_eql_sdl_object_types>`\n  * - :ref:`DDL > Object types <ref_eql_ddl_object_types>`\n  * - :ref:`Cheatsheets > Object types <ref_cheatsheet_object_types>`\n\n"
  },
  {
    "path": "docs/reference/datamodel/introspection/operators.rst",
    "content": ".. _ref_datamodel_introspection_operators:\n\n=========\nOperators\n=========\n\nThis section describes introspection of Gel operators. Much like\nfunctions, operators have parameters and return types as well as a few\nother features.\n\nIntrospection of the ``schema::Operator``:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ObjectType {\n    ...     name,\n    ...     links: {\n    ...         name,\n    ...     },\n    ...     properties: {\n    ...         name,\n    ...     }\n    ... }\n    ... filter .name = 'schema::Operator';\n    {\n        Object {\n            name: 'schema::Operator',\n            links: {\n                Object { name: '__type__' },\n                Object { name: 'annotations' },\n                Object { name: 'params' },\n                Object { name: 'return_type' }\n            },\n            properties: {\n                Object { name: 'id' },\n                Object { name: 'name' },\n                Object { name: 'operator_kind' },\n                Object { name: 'return_typemod' }\n            }\n        }\n    }\n\nSince ``params`` are quite important to operators, here's their structure:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ObjectType {\n    ...     name,\n    ...     links: {\n    ...         name,\n    ...     },\n    ...     properties: {\n    ...         name,\n    ...     }\n    ... }\n    ... filter .name = 'schema::Parameter';\n    {\n        Object {\n            name: 'schema::Parameter',\n            links: {\n                Object { name: '__type__' },\n                Object { name: 'type' }\n            },\n            properties: {\n                Object { name: 'default' },\n                Object { name: 'id' },\n                Object { name: 'kind' },\n                Object { name: 'name' },\n                Object { name: 'num' },\n                Object { name: 'typemod' }\n            }\n        }\n    }\n\nIntrospection of the :eql:op:`and` operator:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select Operator {\n    ...     name,\n    ...     operator_kind,\n    ...     annotations: { name, @value },\n    ...     params: {\n    ...         kind,\n    ...         name,\n    ...         num,\n    ...         typemod,\n    ...         type: { name },\n    ...         default,\n    ...     },\n    ...     return_typemod,\n    ...     return_type: { name },\n    ... }\n    ... filter .name = 'std::AND';\n    {\n        Object {\n            name: 'std::AND',\n            operator_kind: 'Infix',\n            annotations: {},\n            params: {\n                Object {\n                    kind: 'PositionalParam',\n                    name: 'a',\n                    num: 0,\n                    typemod: 'SingletonType',\n                    type: Object { name: 'std::bool' },\n                    default: {}\n                },\n                Object {\n                    kind: 'PositionalParam',\n                    name: 'b',\n                    num: 1,\n                    typemod: 'SingletonType',\n                    type: Object { name: 'std::bool' },\n                    default: {}\n                }\n            },\n            return_typemod: 'SingletonType',\n            return_type: Object { name: 'std::bool' }\n        }\n    }\n"
  },
  {
    "path": "docs/reference/datamodel/introspection/scalars.rst",
    "content": ".. _ref_datamodel_introspection_scalar_types:\n\n============\nScalar types\n============\n\nThis section describes introspection of :ref:`scalar types\n<ref_datamodel_scalar_types>`.\n\nIntrospection of the ``schema::ScalarType``:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ObjectType {\n    ...     name,\n    ...     links: {\n    ...         name,\n    ...     },\n    ...     properties: {\n    ...         name,\n    ...     }\n    ... }\n    ... filter .name = 'schema::ScalarType';\n    {\n        Object {\n            name: 'schema::ScalarType',\n            links: {\n                Object { name: '__type__' },\n                Object { name: 'annotations' },\n                Object { name: 'bases' },\n                Object { name: 'constraints' },\n                Object { name: 'ancestors' }\n            },\n            properties: {\n                Object { name: 'default' },\n                Object { name: 'enum_values' },\n                Object { name: 'id' },\n                Object { name: 'abstract' },\n                Object { name: 'name' }\n            }\n        }\n    }\n\nIntrospection of the built-in scalar :eql:type:`str`:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ScalarType {\n    ...     name,\n    ...     default,\n    ...     enum_values,\n    ...     abstract,\n    ...     bases: { name },\n    ...     ancestors: { name },\n    ...     annotations: { name, @value },\n    ...     constraints: { name },\n    ... }\n    ... filter .name = 'std::str';\n    {\n        Object {\n            name: 'std::str',\n            default: {},\n            enum_values: {},\n            abstract: {},\n            bases: {Object { name: 'std::anyscalar' }},\n            ancestors: {Object { name: 'std::anyscalar' }},\n            annotations: {},\n            constraints: {}\n        }\n    }\n\nFor an :ref:`enumerated scalar type <ref_std_enum>`,\nconsider the following:\n\n.. code-block:: sdl\n\n    scalar type Color extending enum<Red, Green, Blue>;\n\nIntrospection of the enum scalar ``Color``:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ScalarType {\n    ...     name,\n    ...     default,\n    ...     enum_values,\n    ...     abstract,\n    ...     bases: { name },\n    ...     ancestors: { name },\n    ...     annotations: { name, @value },\n    ...     constraints: { name },\n    ... }\n    ... filter .name = 'default::Color';\n    {\n        Object {\n            name: 'default::Color',\n            default: {},\n            enum_values: ['Red', 'Green', 'Blue'],\n            abstract: {},\n            bases: {Object { name: 'std::anyenum' }},\n            ancestors: {\n                Object { name: 'std::anyscalar' },\n                Object { name: 'std::anyenum' }\n            },\n            annotations: {},\n            constraints: {}\n        }\n    }\n"
  },
  {
    "path": "docs/reference/datamodel/introspection/triggers.rst",
    "content": ".. _ref_datamodel_introspection_triggers:\n\n=========\nTriggers\n=========\n\nThis section describes introspection of :ref:`triggers\n<ref_datamodel_triggers>`.\n\nIntrospection of ``schema::Trigger``:\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select ObjectType {\n    ...     name,\n    ...     links: {\n    ...         name,\n    ...     },\n    ...     properties: {\n    ...         name,\n    ...     }\n    ... } filter .name = 'schema::Trigger';\n    {\n      schema::ObjectType {\n        name: 'schema::Trigger',\n        links: {\n          schema::Link {name: 'subject'},\n          schema::Link {name: '__type__'},\n          schema::Link {name: 'ancestors'},\n          schema::Link {name: 'bases'},\n          schema::Link {name: 'annotations'}\n        },\n        properties: {\n          schema::Property {name: 'inherited_fields'},\n          schema::Property {name: 'computed_fields'},\n          schema::Property {name: 'builtin'},\n          schema::Property {name: 'internal'},\n          schema::Property {name: 'name'},\n          schema::Property {name: 'id'},\n          schema::Property {name: 'abstract'},\n          schema::Property {name: 'is_abstract'},\n          schema::Property {name: 'final'},\n          schema::Property {name: 'is_final'},\n          schema::Property {name: 'timing'},\n          schema::Property {name: 'kinds'},\n          schema::Property {name: 'scope'},\n          schema::Property {name: 'expr'},\n        },\n      },\n    }\n\nIntrospection of a trigger named ``log_insert`` on the ``User`` type:\n\n.. lint-off\n\n.. code-block:: edgeql-repl\n\n    db> with module schema\n    ... select Trigger {\n    ...   name,\n    ...   kinds,\n    ...   timing,\n    ...   scope,\n    ...   expr,\n    ...   subject: {\n    ...     name\n    ...   }\n    ... } filter .name = 'log_insert';\n    {\n      schema::Trigger {\n        name: 'log_insert',\n        kinds: {Insert},\n        timing: After,\n        scope: Each,\n        expr: 'insert default::Log { action := \\'insert\\', target_name := __new__.name }',\n        subject: schema::ObjectType {name: 'default::User'},\n      },\n    }\n\n.. lint-on\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Schema > Triggers <ref_datamodel_triggers>`\n  * - :ref:`SDL > Triggers <ref_eql_sdl_triggers>`\n  * - :ref:`DDL > Triggers <ref_eql_ddl_triggers>`\n"
  },
  {
    "path": "docs/reference/datamodel/linkprops.rst",
    "content": ".. _ref_datamodel_linkprops:\n\n===============\nLink properties\n===============\n\n.. index:: link property, linkprops, link table, relations\n.. api-index:: @\n\nLinks, like objects, can also contain **properties**. These are used to store metadata about the link. Due to how they're persisted under the hood,\nlink properties have the additional constraint of always being ``single``.\n\nLink properties require non-trivial syntax to use them, so they are considered\nto be an advanced feature. In many cases, regular properties should be used\ninstead. To paraphrase a famous quote: \"Link properties are like a parachute,\nyou don't need them very often, but when you do, they can be clutch.\"\n\n.. note::\n\n  In practice, link properties are best used with many-to-many relationships\n  (``multi`` links without any exclusive constraints). For one-to-one,\n  one-to-many, and many-to-one relationships the same data should be stored in\n  object properties instead.\n\n.. versionchanged:: 7.0\n\n  Link properties can now be made required.\n\nDeclaration\n===========\n\nLet's a create a ``Person.friends`` link with a ``strength`` property\ncorresponding to the strength of the friendship.\n\n.. code-block:: sdl\n\n    type Person {\n      required name: str { constraint exclusive };\n\n      multi friends: Person {\n        strength: float64;\n      }\n    }\n\nConstraints\n===========\n\nNow let's ensure that the ``@strength`` property is always non-negative:\n\n.. code-block:: sdl\n\n    type Person {\n      required name: str { constraint exclusive };\n\n      multi friends: Person {\n        strength: float64;\n\n        constraint expression on (\n          __subject__@strength >= 0\n        );\n      }\n    }\n\nIndexes\n=======\n\nTo add an index on a link property, we have to refactor our code and define\nan abstract link ``friendship`` that will contain the ``strength`` property\nwith an index on it:\n\n\n.. code-block:: sdl\n\n    abstract link friendship {\n      required strength: float64;\n      index on (__subject__@strength);\n    }\n\n    type Person {\n      required name: str { constraint exclusive };\n\n      multi friends: Person {\n        extending friendship;\n      };\n    }\n\nConceptualizing link properties\n===============================\n\nA way to conceptualize the difference between a regular property and\na link property is that regular properties are used to construct an object,\nwhile link properties are used to construct the link between objects.\n\nFor example, here the ``name`` and ``email`` properties are used to construct a\n``Person`` object:\n\n.. code-block:: edgeql\n\n  insert Person {\n    name := \"Jane\",\n    email := \"jane@jane.com\"\n  }\n\nNow let's insert a ``Person`` object linking it to another ``Person`` object\nsetting the ``@strength`` property to the link between them:\n\n.. code-block:: edgeql\n\n  insert Person {\n    name := \"Bob\",\n    email := \"bob@bob.com\",\n    friends := (\n      insert Person {\n        name := \"Jane\",\n        email := \"jane@jane.com\",\n        @strength := 3.14\n      }\n    )\n  }\n\nSo we're not using ``@strength`` to construct a particular ``Person`` object,\nbut to quantify a link between two ``Person`` objects.\n\nInserting\n=========\n\nWhat if we want to insert a ``Person`` object while linking it to another\n``Person`` that's already in the database?\n\nThe ``@strength`` property then will be specified in the *shape* of a\n``select`` subquery:\n\n.. code-block:: edgeql\n\n  insert Person {\n    name := \"Bob\",\n    friends := (\n      select detached Person {\n        @strength := 3.14\n      }\n      filter .name = \"Alice\"\n    )\n  }\n\n.. note::\n\n  We are using the :eql:op:`detached` operator to unbind the\n  ``Person`` reference from the scope of the ``insert`` query.\n\nWhen doing a nested insert, link properties can be directly included in the\ninner ``insert`` subquery:\n\n.. code-block:: edgeql\n\n  insert Person {\n    name := \"Bob\",\n    friends := (\n      insert Person {\n        name := \"Jane\",\n        @strength := 3.14\n      }\n    )\n  }\n\nSimilarly, ``with`` can be used to capture an expression returning an\nobject type, after which a link property can be added when linking it to\nanother object type:\n\n.. code-block:: edgeql\n\n  with\n    alice := (\n\n      insert Person {\n        name := \"Alice\"\n      }\n      unless conflict on .name\n      else (\n        select Person\n        filter .name = \"Alice\" limit 1\n      )\n    )\n\n  insert Person {\n    name := \"Bob\",\n    friends := alice {\n      @strength := 3.14\n    }\n  };\n\nUpdating\n========\n\n.. code-block:: edgeql\n\n  update Person\n  filter .name = \"Bob\"\n  set {\n    friends += (\n      select .friends {\n        @strength := 3.7\n      }\n      filter .name = \"Alice\"\n    )\n  };\n\nThe example updates the ``@strength`` property of Bob's friends link to\nAlice to 3.7.\n\nIn the context of multi links the ``+=`` operator works like an\nan insert/update operator.\n\nTo update one or more links in a multi link, you can select from the current\nlinked objects, as the example does. Use a ``detached`` selection if you\nwant to insert/update a wider selection of linked objects instead.\n\n\nSelecting\n=========\n\nTo select a link property, you can use the ``@<>name`` syntax inside the\nselect *shape*. Keep in mind, that you're not selecting a property on\nan object with this syntax, but rather on the link, in this case ``friends``:\n\n.. code-block:: edgeql-repl\n\n  gel> select Person {\n  ....   name,\n  ....   friends: {\n  ....     name,\n  ....     @strength\n  ....   }\n  .... };\n  {\n    default::Person {name: 'Alice', friends: {}},\n    default::Person {\n      name: 'Bob',\n      friends: {\n        default::Person {name: 'Alice', @strength: 3.7}\n      }\n    },\n  }\n\nUnions\n======\n\nA link property cannot be referenced in a set union *except* in the case of\na :ref:`for loop <ref_eql_for>`. That means this will *not* work:\n\n.. code-block:: edgeql\n\n    # 🚫 Does not work\n    insert Movie {\n      title := 'The Incredible Hulk',\n      actors := {(\n        select Person {\n          @character_name := 'The Hulk'\n        } filter .name = 'Mark Ruffalo'\n      ),\n      (\n        select Person {\n          @character_name := 'Iron Man'\n        } filter .name = 'Robert Downey Jr.'\n      )}\n    };\n\nThat query will produce an error: ``QueryError: invalid reference to link\nproperty in top level shape``\n\nYou can use this workaround instead:\n\n.. code-block:: edgeql\n\n    # ✅ Works!\n    insert Movie {\n      title := 'The Incredible Hulk',\n\n      actors := assert_distinct((\n        with characters := {\n          ('The Hulk', 'Mark Ruffalo'),\n          ('Iron Man', 'Robert Downey Jr.')\n        }\n        for character in characters union (\n            select Person {\n                @character_name := character.0\n            } filter .name = character.1\n        )\n      ))\n    };\n\nNote that we are also required to wrap the ``actors`` query with\n:eql:func:`assert_distinct` here to assure the compiler that the result set\nis distinct.\n\nWith computed backlinks\n=======================\n\nSpecifying link properties of a computed backlink in your shape is also\nsupported. If you have this schema:\n\n.. code-block:: sdl\n\n    type Person {\n      required name: str;\n\n      multi follows: Person {\n        followed: datetime {\n          default := datetime_of_statement();\n        };\n      };\n\n      multi link followers := .<follows[is Person];\n    }\n\nthis query will work as expected:\n\n.. code-block:: edgeql\n\n    select Person {\n      name,\n\n      followers: {\n        name,\n        @followed\n      }\n    };\n\neven though ``@followed`` is a link property of ``follows`` and we are\naccessing is through the computed backlink ``followers`` instead.\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Links and link properties <ref_datamodel_link_properties>`\n  * - :ref:`Properties in schema <ref_eql_sdl_props>`\n  * - :ref:`Properties with DDL <ref_eql_ddl_props>`\n"
  },
  {
    "path": "docs/reference/datamodel/links.rst",
    "content": ".. _ref_datamodel_links:\n\n=====\nLinks\n=====\n\nLinks define a relationship between two\n:ref:`object types <ref_datamodel_object_types>` in Gel.\n\nLinks in |Gel| are incredibly powerful and flexible. They can be used to model\nrelationships of any cardinality, can be traversed in both directions,\ncan be polymorphic, can have constraints, and many other things.\n\n\nLinks are directional\n=====================\n\nLinks are *directional*: they have a **source** (the type on which they are\ndeclared) and a **target** (the type they point to).\n\nE.g. the following schema defines a link from ``Person`` to ``Person`` and\na link from ``Company`` to ``Person``:\n\n.. code-block:: sdl\n\n  type Person {\n    link best_friend: Person;\n  }\n\n  type Company {\n    multi link employees: Person;\n  }\n\nThe ``employees`` link's source is ``Company`` and its target is ``Person``.\n\nThe ``link`` keyword is optional, and can be omitted.\n\n\nLink cardinality\n================\n\n.. api-index:: single, multi\n\nAll links have a cardinality: either ``single`` or ``multi``. The default is\n``single`` (a \"to-one\" link). Use the ``multi`` keyword to declare a \"to-many\"\nlink:\n\n.. code-block:: sdl\n\n  type Person {\n    multi friends: Person;\n  }\n\n\nRequired links\n==============\n\n.. index:: not null\n.. api-index:: required, optional\n\nAll links are either ``optional`` or ``required``; the default is ``optional``.\nUse the ``required`` keyword to declare a required link. A required link must\npoint to *at least one* target instance, and if the cardinality of the required\nlink is ``single``, it must point to *exactly one* target instance. In this\nscenario, every ``Person`` must have *exactly one* ``best_friend``:\n\n.. code-block:: sdl\n\n  type Person {\n    required best_friend: Person;\n  }\n\nLinks with cardinality ``multi`` can also be ``required``;\n``required multi`` links must point to *at least one* target object:\n\n.. code-block:: sdl\n\n  type Person {\n    name: str;\n  }\n\n  type GroupChat {\n    required multi members: Person;\n  }\n\nAttempting to create a ``GroupChat`` with no members would fail.\n\nExclusive constraints\n=====================\n\n.. api-index:: constraint exclusive\n\nYou can add an ``exclusive`` constraint to a link to guarantee that no other\ninstances can link to the same target(s):\n\n.. code-block:: sdl\n\n  type Person {\n    name: str;\n  }\n\n  type GroupChat {\n    required multi members: Person {\n      constraint exclusive;\n    }\n  }\n\nWith ``exclusive`` on ``GroupChat.members``, two ``GroupChat`` objects cannot\nlink to the same ``Person``; put differently, no ``Person`` can be a\n``member`` of multiple ``GroupChat`` objects.\n\nBacklinks\n=========\n\n.. api-index:: .<\n\nIn Gel you can traverse links in reverse to find objects that link to\nthe object. You can do that directly in your query. E.g. for this example\nschema:\n\n.. code-block:: sdl\n\n  type Author {\n    name: str;\n  }\n\n  type Article {\n    title: str;\n    multi authors: Author;\n  }\n\nYou can find all articles by \"John Doe\" by traversing the ``authors``\nlink in reverse:\n\n.. code-block:: edgeql\n\n  select Author {\n    articles := .<authors[is Article]\n  }\n  filter .name = \"John Doe\";\n\nWhile the ``.<authors[is Article]`` exppression looks complicated,\nthe syntax is easy to read once you understand the structure of it:\n\n* ``.<`` is used to traverse a link in reverse, it's the reverse of\n  the familiar ``.`` operator.\n\n* ``authors`` is the name of the link that the type on the other side\n  has to point to ``Author``.  In this case we know that ``Article``\n  has a link ``authors`` to ``Author``, so we use it!\n\n* ``[is Article]`` is a filter that ensures we only traverse links\n  that point to ``Article`` objects.\n\nIf there's a backlink that you will be traversing often, you can declare it\nas a computed link:\n\n.. code-block:: sdl-diff\n\n    type Author {\n      name: str;\n  +   articles := .<authors[is Article];\n    }\n\nLast point to note: **backlinks** work in reverse to find objects that link\nto the object, and therefore assume ``multi`` as a default.\nUse the ``single`` keyword to declare a \"to-one\" backlink computed link:\n\n.. code-block:: sdl\n\n  type CompanyEmployee {\n    single company := .<employees[is Company];\n  }\n\n\nDefault values\n==============\n\n.. api-index:: default\n\nLinks can declare a default value in the form of an EdgeQL expression, which\nwill be executed upon insertion. In this example, new people are automatically\nassigned three random friends:\n\n.. code-block:: sdl\n\n  type Person {\n    required name: str;\n    multi friends: Person {\n      default := (select Person order by random() limit 3);\n    }\n  }\n\n\nModeling relations\n==================\n\n.. index:: cardinality, one-to-one, one-to-many, many-to-one, many-to-many,\n           link table, association table\n\nBy combining *link cardinality* and *exclusivity constraints*, we can model\nevery kind of relationship: one-to-one, one-to-many, many-to-one, and\nmany-to-many.\n\n.. list-table::\n\n  * - **Relation type**\n    - **Cardinality**\n    - **Exclusive**\n  * - One-to-one\n    - ``single``\n    - Yes\n  * - One-to-many\n    - ``multi``\n    - Yes\n  * - Many-to-one\n    - ``single``\n    - No\n  * - Many-to-many\n    - ``multi``\n    - No\n\n.. _ref_guide_many_to_one:\n\nMany-to-one\n-----------\n\nMany-to-one relationships typically represent concepts like ownership,\nmembership, or hierarchies. For example, ``Person`` and ``Shirt``. One person\nmay own many shirts, and a shirt is (usually) owned by just one person.\n\n.. code-block:: sdl\n\n  type Person {\n    required name: str\n  }\n\n  type Shirt {\n    required color: str;\n    owner: Person;\n  }\n\nSince links are ``single`` by default, each ``Shirt`` only corresponds to\none ``Person``. In the absence of any exclusivity constraints, multiple shirts\ncan link to the same ``Person``. Thus, we have a one-to-many relationship\nbetween ``Person`` and ``Shirt``.\n\nWhen fetching a ``Person``, it's possible to deeply fetch their collection of\n``Shirts`` by traversing the ``Shirt.owner`` link *in reverse*, known as a\n**backlink**. See the :ref:`select docs <ref_eql_statements_select>` to\nlearn more.\n\n\n.. _ref_guide_one_to_many:\n\nOne-to-many\n-----------\n\nConceptually, one-to-many and many-to-one relationships are identical; the\n\"directionality\" is a matter of perspective. Here, the same \"shirt owner\"\nrelationship is represented with a ``multi`` link:\n\n.. code-block:: sdl\n\n  type Person {\n    required name: str;\n    multi shirts: Shirt {\n      # ensures a one-to-many relationship\n      constraint exclusive;\n    }\n  }\n\n  type Shirt {\n    required color: str;\n  }\n\n.. note::\n\n  Don't forget the ``exclusive`` constraint! Without it, the relationship\n  becomes many-to-many.\n\nUnder the hood, a ``multi`` link is stored in an intermediate `association\ntable <https://en.wikipedia.org/wiki/Associative_entity>`_, whereas a\n``single`` link is stored as a column in the object type where it is declared.\n\n.. note::\n\n  Choosing a link direction can be tricky. Should you model this\n  relationship as one-to-many (with a ``multi`` link) or as many-to-one\n  (with a ``single`` link and a backlink)? A general rule of thumb:\n\n  - Use a ``multi`` link if the relationship is relatively stable and\n    not updated frequently, and the set of related objects is typically\n    small. For example, a list of postal addresses in a user profile.\n  - Otherwise, prefer a single link from one object type and a computed\n    backlink on the other. This can be more efficient and is generally\n    recommended for 1:N relations:\n\n  .. code-block:: sdl\n\n    type Post {\n      required author: User;\n    }\n\n    type User {\n      multi posts := (.<author[is Post])\n    }\n\n\n.. _ref_guide_one_to_one:\n\nOne-to-one\n----------\n\nUnder a *one-to-one* relationship, the source object links to a single instance\nof the target type, and vice versa. As an example, consider a schema to\nrepresent assigned parking spaces:\n\n.. code-block:: sdl\n\n  type Employee {\n    required name: str;\n    assigned_space: ParkingSpace {\n      constraint exclusive;\n    }\n  }\n\n  type ParkingSpace {\n    required number: int64;\n  }\n\nAll links are ``single`` unless otherwise specified, so no ``Employee`` can\nhave more than one ``assigned_space``. The :eql:constraint:`exclusive`\nconstraint guarantees that a given ``ParkingSpace`` can't be assigned to\nmultiple employees. Together, these form a one-to-one relationship.\n\n\n.. _ref_guide_many_to_many:\n\nMany-to-many\n------------\n\nA *many-to-many* relation is the least constrained kind of relationship. There\nis no exclusivity or cardinality constraint in either direction. As an example,\nconsider a simple app where a ``User`` can \"like\" their favorite ``Movie``:\n\n.. code-block:: sdl\n\n  type User {\n    required name: str;\n    multi likes: Movie;\n  }\n\n  type Movie {\n    required title: str;\n  }\n\nA user can like multiple movies. And in the absence of an ``exclusive``\nconstraint, each movie can be liked by multiple users, creating a many-to-many\nrelationship.\n\n.. note::\n\n  Links are always distinct. It's not possible to link the **same** objects\n  twice. For example:\n\n  .. code-block:: sdl\n\n    type User {\n      required name: str;\n      multi watch_history: Movie {\n        seen_at: datetime;\n      };\n    }\n\n    type Movie {\n      required title: str;\n    }\n\n  In this model, a user can't watch the same movie more than once (the link\n  from a specific user to a specific movie can exist only once). One approach\n  is to store multiple timestamps in an array on the link property:\n\n  .. code-block:: sdl\n\n    type User {\n      required name: str;\n      multi watch_history: Movie {\n        seen_at: array<datetime>;\n      };\n    }\n    type Movie {\n      required title: str;\n    }\n\n  Alternatively, you might introduce a dedicated type:\n\n  .. code-block:: sdl\n\n    type User {\n      required name: str;\n      multi watch_history := .<user[is WatchHistory];\n    }\n    type Movie {\n      required title: str;\n    }\n    type WatchHistory {\n      required user: User;\n      required movie: Movie;\n      seen_at: datetime;\n    }\n\n  Remember to use **single** links in the join table so you don't end up\n  with extra tables.\n\n\n.. _ref_datamodel_link_properties:\n\nLink properties\n===============\n\n.. index:: linkprops, metadata, link table\n\nLike object types, links in Gel can contain **properties**. Link properties\ncan store metadata about the link, such as the *date* a link was created\nor the *strength* of the relationship:\n\n.. code-block:: sdl\n\n  type Person {\n    name: str;\n    multi family_members: Person {\n      relationship: str;\n    }\n  }\n\n.. note::\n\n  Link properties can only be **primitive** data (scalars, enums,\n  arrays, or tuples) — *not* links to other objects.\n\nLink properties are especially useful with many-to-many relationships, where\nthe link itself is a distinct concept with its own data. For relations\nlike one-to-one or one-to-many, it's often clearer to store data in the\nobject type itself instead of in a link property.\n\nRead more about link properties in the :ref:`dedicated link properties article\n<ref_datamodel_linkprops>`.\n\nInserting and updating link properties\n--------------------------------------\n\nTo add a link with a link property, include the property name (prefixed by\n``@``) in the shape:\n\n.. code-block:: edgeql\n\n  insert Person {\n    name := \"Bob\",\n    family_members := (\n      select detached Person {\n        @relationship := \"sister\"\n      }\n      filter .name = \"Alice\"\n    )\n  };\n\nUpdating a link's property on an **existing** link is similar. You can select\nthe link from within the object being updated:\n\n.. code-block:: edgeql\n\n  update Person\n  filter .name = \"Bob\"\n  set {\n    family_members := (\n      select .family_members {\n        @relationship := \"step-sister\"\n      }\n      filter .name = \"Alice\"\n    )\n  };\n\n.. warning::\n\n  A link property cannot be referenced in a set union *except* in the case of\n  a :ref:`for loop <ref_eql_for>`. For instance:\n\n  .. code-block:: edgeql\n\n      # 🚫 Does not work\n      insert Movie {\n        title := 'The Incredible Hulk',\n        characters := {\n          (\n            select Person {\n              @character_name := 'The Hulk'\n            }\n            filter .name = 'Mark Ruffalo'\n          ),\n          (\n            select Person {\n              @character_name := 'Abomination'\n            }\n            filter .name = 'Tim Roth'\n          )\n        }\n      };\n\n  will produce an error ``QueryError: invalid reference to link property in\n  top level shape``.\n\n  One workaround is to insert them via a ``for`` loop, combined with\n  :eql:func:`assert_distinct`:\n\n  .. code-block:: edgeql\n\n      # ✅ Works!\n      insert Movie {\n        title := 'The Incredible Hulk',\n        characters := assert_distinct((\n          with actors := {\n            ('The Hulk', 'Mark Ruffalo'),\n            ('Abomination', 'Tim Roth')\n          },\n          for actor in actors union (\n            select Person {\n              @character_name := actor.0\n            }\n            filter .name = actor.1\n          )\n        ))\n      };\n\nQuerying link properties\n------------------------\n\nTo query a link property, add the link property's name (prefixed with ``@``)\nin the shape:\n\n.. code-block:: edgeql-repl\n\n  db> select Person {\n  ...   name,\n  ...   family_members: {\n  ...     name,\n  ...     @relationship\n  ...   }\n  ... };\n\n.. note::\n\n  In the results above, Bob has a *step-sister* property on the link to\n  Alice, but Alice does not automatically have a property describing Bob.\n  Changes to link properties are not mirrored on the \"backlink\" side unless\n  explicitly updated, because link properties cannot be required.\n\n.. note::\n\n  For a full guide on modeling, inserting, updating, and querying link\n  properties, see the :ref:`Using Link Properties <ref_datamodel_linkprops>`\n  guide.\n\n\n.. _ref_datamodel_link_deletion:\n\nDeletion policies\n=================\n\n.. api-index:: on target delete, on source delete, restrict, delete source,\n               allow, deferred restrict, delete target, if orphan\n\nLinks can declare their own **deletion policy** for when the **target** or\n**source** is deleted.\n\nTarget deletion\n---------------\n\nThe clause ``on target delete`` determines the action when the target object is\ndeleted:\n\n- ``restrict`` (default) — raises an exception if the target is deleted.\n- ``delete source`` — deletes the source when the target is deleted (a cascade).\n- ``allow`` — removes the target from the link if the target is deleted.\n- ``deferred restrict`` — like ``restrict`` but defers the error until the\n  end of the transaction if the object remains linked.\n\n.. code-block:: sdl\n\n  type MessageThread {\n    title: str;\n  }\n\n  type Message {\n    content: str;\n    chat: MessageThread {\n      on target delete delete source;\n    }\n  }\n\n\n.. _ref_datamodel_links_source_deletion:\n\nSource deletion\n---------------\n\nThe clause ``on source delete`` determines the action when the **source** is\ndeleted:\n\n- ``allow`` — deletes the source, removing the link to the target.\n- ``delete target`` — unconditionally deletes the target as well.\n- ``delete target if orphan`` — deletes the target if and only if it's no\n  longer linked by any other object *via the same link*.\n\n.. code-block:: sdl\n\n  type MessageThread {\n    title: str;\n    multi messages: Message {\n      on source delete delete target;\n    }\n  }\n\n  type Message {\n    content: str;\n  }\n\nYou can add ``if orphan`` if you'd like to avoid deleting a target that remains\nlinked elsewhere via the **same** link name.\n\n.. code-block:: sdl-diff\n\n    type MessageThread {\n      title: str;\n      multi messages: Message {\n  -     on source delete delete target;\n  +     on source delete delete target if orphan;\n      }\n    }\n\n.. note::\n\n  The ``if orphan`` qualifier **does not** apply globally across\n  all links in the database or even all links from the same type. If another\n  link *by a different name* or *with a different on-target-delete* policy\n  points at the same object, it *doesn't* prevent the object from being\n  considered \"orphaned\" for the link that includes ``if orphan``.\n\n\n.. _ref_datamodel_link_polymorphic:\n\nPolymorphic links\n=================\n\nLinks can be **polymorphic**, i.e., have an ``abstract`` target. In the\nexample below, we have an abstract type ``Person`` with concrete subtypes\n``Hero`` and ``Villain``:\n\n.. code-block:: sdl\n\n  abstract type Person {\n    name: str;\n  }\n\n  type Hero extending Person {\n    # additional fields\n  }\n\n  type Villain extending Person {\n    # additional fields\n  }\n\nA polymorphic link can target any non-abstract subtype:\n\n.. code-block:: sdl\n\n  type Movie {\n    title: str;\n    multi characters: Person;\n  }\n\nWhen querying a polymorphic link, you can filter by a specific subtype, cast\nthe link to a subtype, etc. See :ref:`Polymorphic Queries <ref_eql_select_polymorphic>`\nfor details.\n\nAbstract links\n==============\n\n.. api-index:: abstract link\n\nIt's possible to define ``abstract`` links that aren't tied to a particular\nsource or target, and then extend them in concrete object types. This can help\neliminate repetitive declarations:\n\n.. code-block:: sdl\n\n  abstract link link_with_strength {\n    strength: float64;\n    index on (__subject__@strength);\n  }\n\n  type Person {\n    multi friends: Person {\n      extending link_with_strength;\n    };\n  }\n\n\n.. _ref_eql_sdl_links_overloading:\n\nOverloading\n===========\n\n.. api-index:: overloaded\n\nWhen an inherited link is modified (by adding more constraints or changing its\ntarget type, etc.), the ``overloaded`` keyword is required. This prevents\nunintentional overloading due to name clashes:\n\n.. code-block:: sdl\n\n  abstract type Friendly {\n    # this type can have \"friends\"\n    multi friends: Friendly;\n  }\n\n  type User extending Friendly {\n    # overload the link target to to be specifically User\n    overloaded multi friends: User;\n\n    # ... other links and properties\n  }\n\n\n.. _ref_eql_sdl_links:\n.. _ref_eql_sdl_links_syntax:\n\nDeclaring links\n===============\n\nThis section describes the syntax to use links in your schema.\n\nSyntax\n------\n\n.. sdl:synopsis::\n\n  # Concrete link form used inside type declaration:\n  [ overloaded ] [{required | optional}] [{single | multi}]\n    [ link ] <name> : <type>\n    [ \"{\"\n        [ extending <base> [, ...] ; ]\n        [ default := <expression> ; ]\n        [ readonly := {true | false} ; ]\n        [ on target delete <action> ; ]\n        [ on source delete <action> ; ]\n        [ <annotation-declarations> ]\n        [ <property-declarations> ]\n        [ <constraint-declarations> ]\n        ...\n      \"}\" ]\n\n  # Computed link form used inside type declaration:\n  [{required | optional}] [{single | multi}]\n    [ link ] <name> := <expression>;\n\n  # Computed link form used inside type declaration (extended):\n  [ overloaded ] [{required | optional}] [{single | multi}]\n    link <name> [: <type>]\n    [ \"{\"\n        using (<expression>) ;\n        [ extending <base> [, ...] ; ]\n        [ <annotation-declarations> ]\n        [ <constraint-declarations> ]\n        ...\n      \"}\" ]\n\n  # Abstract link form:\n  abstract link <name>\n  [ \"{\"\n      [ extending <base> [, ...] ; ]\n      [ readonly := {true | false} ; ]\n      [ <annotation-declarations> ]\n      [ <property-declarations> ]\n      [ <constraint-declarations> ]\n      [ <index-declarations> ]\n      ...\n    \"}\" ]\n\nThere are several forms of link declaration, as shown in the syntax synopsis\nabove:\n\n- the first form is the canonical definition form;\n- the second form is used for defining a\n  :ref:`computed link <ref_datamodel_computed>`;\n- and the last form is used to define an abstract link.\n\nThe following options are available:\n\n:eql:synopsis:`overloaded`\n    If specified, indicates that the link is inherited and that some\n    feature of it may be altered in the current object type.  It is an\n    error to declare a link as *overloaded* if it is not inherited.\n\n:eql:synopsis:`required`\n    If specified, the link is considered *required* for the parent\n    object type.  It is an error for an object to have a required\n    link resolve to an empty value.  Child links **always** inherit\n    the *required* attribute, i.e it is not possible to make a\n    required link non-required by extending it.\n\n:eql:synopsis:`optional`\n    This is the default qualifier assumed when no qualifier is\n    specified, but it can also be specified explicitly. The link is\n    considered *optional* for the parent object type, i.e. it is\n    possible for the link to resolve to an empty value.\n\n:eql:synopsis:`multi`\n    Specifies that there may be more than one instance of this link\n    in an object, in other words, ``Object.link`` may resolve to a set\n    of a size greater than one.\n\n:eql:synopsis:`single`\n    Specifies that there may be at most *one* instance of this link\n    in an object, in other words, ``Object.link`` may resolve to a set\n    of a size not greater than one.  ``single`` is assumed if nether\n    ``multi`` nor ``single`` qualifier is specified.\n\n:eql:synopsis:`extending <base> [, ...]`\n    Optional clause specifying the *parents* of the new link item.\n\n    Use of ``extending`` creates a persistent schema relationship\n    between the new link and its parents.  Schema modifications\n    to the parent(s) propagate to the child.\n\n    If the same *property* name exists in more than one parent, or\n    is explicitly defined in the new link and at least one parent,\n    then the data types of the property targets must be *compatible*.\n    If there is no conflict, the link properties are merged to form a\n    single property in the new link item.\n\n:eql:synopsis:`<type>`\n    The type must be a valid :ref:`type expression <ref_eql_types>`\n    denoting an object type.\n\nThe valid SDL sub-declarations are listed below:\n\n:eql:synopsis:`default := <expression>`\n    Specifies the default value for the link as an EdgeQL expression.\n    The default value is used in an ``insert`` statement if an explicit\n    value for this link is not specified.\n\n    The expression must be :ref:`Stable <ref_reference_volatility>`.\n\n:eql:synopsis:`readonly := {true | false}`\n    If ``true``, the link is considered *read-only*.  Modifications of this link using ``update`` are prohibited once an object is created. Any :ref:`overloaded links <ref_eql_sdl_links_overloading>` **must** preserve the original *read-only* value. Changes to this link **will** occur if a link is deleted and the appropriate :ref:`deletion policy <ref_datamodel_link_deletion>` allows it.\n\n:sdl:synopsis:`<annotation-declarations>`\n    Set link :ref:`annotation <ref_eql_sdl_annotations>`\n    to a given *value*.\n\n:sdl:synopsis:`<property-declarations>`\n    Define a concrete :ref:`property <ref_eql_sdl_props>` on the link.\n\n:sdl:synopsis:`<constraint-declarations>`\n    Define a concrete :ref:`constraint <ref_eql_sdl_constraints>` on the link.\n\n:sdl:synopsis:`<index-declarations>`\n    Define an :ref:`index <ref_eql_sdl_indexes>` for this abstract\n    link. Note that this index can only refer to link properties.\n\n\n.. _ref_eql_ddl_links:\n\nDDL commands\n============\n\nThis section describes the low-level DDL commands for creating, altering, and\ndropping links. You typically don't need to use these commands directly, but\nknowing about them is useful for reviewing migrations.\n\nCreate link\n-----------\n\n:eql-statement:\n:eql-haswith:\n\nDefine a new link.\n\n.. eql:synopsis::\n\n  [ with <with-item> [, ...] ]\n  {create|alter} type <TypeName> \"{\"\n    [ ... ]\n    create [{required | optional}] [{single | multi}]\n      link <name>\n      [ extending <base> [, ...] ]: <type>\n      [ \"{\" <subcommand>; [...] \"}\" ] ;\n    [ ... ]\n  \"}\"\n\n  # Computed link form:\n\n  [ with <with-item> [, ...] ]\n  {create|alter} type <TypeName> \"{\"\n    [ ... ]\n    create [{required | optional}] [{single | multi}]\n      link <name> := <expression>;\n    [ ... ]\n  \"}\"\n\n  # Abstract link form:\n\n  [ with <with-item> [, ...] ]\n  create abstract link [<module>::]<name> [extending <base> [, ...]]\n  [ \"{\" <subcommand>; [...] \"}\" ]\n\n  # where <subcommand> is one of\n\n    set default := <expression>\n    set readonly := {true | false}\n    create annotation <annotation-name> := <value>\n    create property <property-name> ...\n    create constraint <constraint-name> ...\n    on target delete <action>\n    on source delete <action>\n    reset on target delete\n    create index on <index-expr>\n\nDescription\n^^^^^^^^^^^\n\nThe combinations of ``create type ... create link`` and ``alter type ...\ncreate link`` define a new concrete link for a given object type, in DDL form.\n\nThere are three forms of ``create link``:\n\n1. The canonical definition form (specifying a target type).\n2. The computed link form (declaring a link via an expression).\n3. The abstract link form (declaring a module-level link).\n\nParameters\n^^^^^^^^^^^\n\nMost sub-commands and options mirror those found in the\n:ref:`SDL link declaration <ref_eql_sdl_links_syntax>`. In DDL form:\n\n- ``set default := <expression>`` specifies a default value.\n- ``set readonly := {true | false}`` makes the link read-only or not.\n- ``create annotation <annotation-name> := <value>`` adds an annotation.\n- ``create property <property-name> ...`` defines a property on the link.\n- ``create constraint <constraint-name> ...`` defines a constraint on the link.\n- ``on target delete <action>`` and ``on source delete <action>`` specify\n  deletion policies.\n- ``reset on target delete`` resets the target deletion policy to default\n  or inherited.\n- ``create index on <index-expr>`` creates an index on the link.\n\nExamples\n^^^^^^^^\n\n.. code-block:: edgeql\n\n  alter type User {\n    create multi link friends: User\n  };\n\n.. code-block:: edgeql\n\n  alter type User {\n    create link special_group := (\n      select __source__.friends\n      filter .town = __source__.town\n    )\n  };\n\n.. code-block:: edgeql\n\n  create abstract link orderable {\n    create property weight: std::int64\n  };\n\n  alter type User {\n    create multi link interests extending orderable: Interest\n  };\n\n\nAlter link\n----------\n\n:eql-statement:\n:eql-haswith:\n\nChanges the definition of a link.\n\n.. eql:synopsis::\n\n  [ with <with-item> [, ...] ]\n  {create|alter} type <TypeName> \"{\"\n    [ ... ]\n    alter link <name>\n    [ \"{\" ] <subcommand>; [...] [ \"}\" ];\n    [ ... ]\n  \"}\"\n\n  [ with <with-item> [, ...] ]\n  alter abstract link [<module>::]<name>\n  [ \"{\" ] <subcommand>; [...] [ \"}\" ];\n\n  # where <subcommand> is one of\n\n    set default := <expression>\n    reset default\n    set readonly := {true | false}\n    reset readonly\n    rename to <newname>\n    extending ...\n    set required\n    set optional\n    reset optionality\n    set single\n    set multi\n    reset cardinality\n    set type <typename> [using (<conversion-expr>)]\n    reset type\n    using (<computed-expr>)\n    create annotation <annotation-name> := <value>\n    alter annotation <annotation-name> := <value>\n    drop annotation <annotation-name>\n    create property <property-name> ...\n    alter property <property-name> ...\n    drop property <property-name> ...\n    create constraint <constraint-name> ...\n    alter constraint <constraint-name> ...\n    drop constraint <constraint-name> ...\n    on target delete <action>\n    on source delete <action>\n    create index on <index-expr>\n    drop index on <index-expr>\n\nDescription\n^^^^^^^^^^^\n\nThis command modifies an existing link on a type. It can also be used on\nan abstract link at the module level.\n\nParameters\n^^^^^^^^^^\n\n- ``rename to <newname>`` changes the link's name.\n- ``extending ...`` changes or adds link parents.\n- ``set required`` / ``set optional`` changes the link optionality.\n- ``reset optionality`` reverts optionality to default or inherited value.\n- ``set single`` / ``set multi`` changes cardinality.\n- ``reset cardinality`` reverts cardinality to default or inherited value.\n- ``set type <typename> [using (<expr>)]`` changes the link's target type.\n- ``reset type`` reverts the link's type to inherited.\n- ``using (<expr>)`` changes the expression of a computed link.\n- ``create annotation``, ``alter annotation``, ``drop annotation`` manage\n  annotations.\n- ``create property``, ``alter property``, ``drop property`` manage link\n  properties.\n- ``create constraint``, ``alter constraint``, ``drop constraint`` manage\n  link constraints.\n- ``on target delete <action>`` and ``on source delete <action>`` manage\n  deletion policies.\n- ``reset on target delete`` reverts the target deletion policy.\n- ``create index on <index-expr>`` / ``drop index on <index-expr>`` manage\n  indexes on link properties.\n\nExamples\n^^^^^^^^\n\n.. code-block:: edgeql\n\n  alter type User {\n    alter link friends create annotation title := \"Friends\";\n  };\n\n.. code-block:: edgeql\n\n  alter abstract link orderable rename to sorted;\n\n.. code-block:: edgeql\n\n  alter type User {\n    alter link special_group using (\n      # at least one of the friend's interests\n      # must match the user's\n      select __source__.friends\n      filter .interests IN __source__.interests\n    );\n  };\n\nDrop link\n---------\n\n:eql-statement:\n:eql-haswith:\n\nRemoves the specified link from the schema.\n\n.. eql:synopsis::\n\n  [ with <with-item> [, ...] ]\n  alter type <TypeName> \"{\"\n    [ ... ]\n    drop link <name>\n    [ ... ]\n  \"}\"\n\n  [ with <with-item> [, ...] ]\n  drop abstract link [<module>]::<name>\n\nDescription\n^^^^^^^^^^^\n\n- ``alter type ... drop link <name>`` removes the link from an object type.\n- ``drop abstract link <name>`` removes an abstract link from the schema.\n\nExamples\n^^^^^^^^\n\n.. code-block:: edgeql\n\n  alter type User drop link friends;\n\n.. code-block:: edgeql\n\n  drop abstract link orderable;\n\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n    - :ref:`Introspection > Object types <ref_datamodel_introspection_object_types>`\n"
  },
  {
    "path": "docs/reference/datamodel/migrations.rst",
    "content": ".. _ref_datamodel_migrations:\n\n==========\nMigrations\n==========\n\n|Gel's| baked-in migration system lets you painlessly evolve your schema over\ntime. Just update the contents of your |.gel| file(s) and use the |Gel| CLI\nto *create* and *apply* migrations.\n\n.. code-block:: bash\n\n  $ gel migration create\n  Created dbschema/migrations/00001.edgeql\n\n  $ gel migrate\n  Applied dbschema/migrations/00001.edgeql\n\nRefer to the :ref:`creating and applying migrations <ref_intro_migrations>`\nguide for more information on how to use the migration system.\n\nThis document describes how migrations are implemented.\n\n\nThe migrations flow\n===================\n\nThe migration flow is as follows:\n\n1. The user edits the |.gel| files in the ``dbschema`` directory.\n\n   This makes the schema described in the |.gel| files **different** from the\n   actual schema in the database.\n\n2. The user runs the :gelcmd:`migration create` command to create a new\n   migration (a sequence of low-level DDL commands).\n\n   * The CLI reads the |.gel| files and sends them to the |Gel| server, to\n     analyze the changes.\n\n   * The |Gel| server generates a migration plan and sends it back to the CLI.\n\n   * The migration plan might require clarification from the user.\n\n     If so, the CLI and the |Gel| server will go back and forth presenting\n     the user with a sequence of questions, until the migration plan is\n     clear and approved by the user.\n\n3. The CLI writes the migration plan to a new file in the ``dbschema/migrations``\n   directory.\n\n4. The user runs the :gelcmd:`migrate` command to apply the migration to the\n   database.\n\n5. The user checks in the updated |.gel| files and the new\n   ``dbschema/migrations`` migration file (created by :gelcmd:`migration create`)\n   into version control.\n\n\nCommand line tools\n==================\n\nThe two most important commands are:\n\n* :gelcmd:`migration create`\n* :gelcmd:`migrate`\n\n\nAutomatic migrations\n====================\n\nSometimes when you're prototyping something new you don't want to spend\ntime worrying about migrations. There's no data to lose and not much code\nthat depends on the schema just yet.\n\nFor this use case you can use the :gelcmd:`watch --migrate` command, which\nwill monitor your |.gel| files and automatically create and apply migrations\nfor you in the background.\n\n.. _ref_eql_ddl:\n\nData definition language (DDL)\n==============================\n\nThe migration plan is a sequence of DDL commands. DDL commands are low-level\ninstructions that describe the changes to the schema.\n\nSDL and your |.gel| files are like a 3D printer: you design the final shape,\nand the system puts a database together for you. Using DDL is like building a\nhouse the traditional way: to add a window, you first need a frame; to have a\nframe, you need a wall; and so on.\n\nIf your schema looks like this:\n\n.. code-block:: sdl\n\n  type User {\n    required name: str;\n  }\n\nthen the corresponding DDL might look like this:\n\n.. code-block:: edgeql\n\n  create type User {\n    create required property name: str;\n  }\n\nThere are some circumstances where users might want to use DDL directly.\nBut in most cases you just need to learn how to read them to understand\nthe migration plan. Luckily, the DDL and SDL syntaxes were designed in tandem\nand are very similar.\n\nMost documentation pages on Gel's schema have a section about DDL commands,\ne.g. :ref:`object types DDL <ref_eql_ddl_object_types>`.\n\n\n.. _ref_eql_ddl_migrations:\n\nMigration DDL commands\n======================\n\nMigrations themselves are a sequence of special DDL commands.\n\nLike all DDL commands, ``start migration`` and other migration commands are\nconsidered low-level. Users are encouraged to use the built-in\n:ref:`migration tools <ref_cli_gel_migration>` instead.\n\nHowever, if you want to implement your own migration tools, this section\nwill give you a good understanding of how Gel migrations work under the hood.\n\n\nStart migration\n---------------\n\n:eql-statement:\n\nStart a migration block.\n\n.. eql:synopsis::\n\n    start migration to \"{\"\n        <sdl-declaration> ;\n        [ ... ]\n    \"}\" ;\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`<sdl-declaration>`\n    Complete schema text (content of all |.gel| files) defined with\n    the declarative :ref:`Gel schema definition language <ref_eql_sdl>`.\n\nDescription\n^^^^^^^^^^^\n\nThe command ``start migration`` defines a migration of the schema to a\nnew state. The target schema state is described using :ref:`SDL\n<ref_eql_sdl>` and describes the entire schema. This is important to\nremember when creating a migration to add a few more things to an\nexisting schema as all the existing schema objects and the new ones\nmust be included in the ``start migration`` command. Objects that\naren't included in the command will be removed from the new schema\n(which may result in data loss).\n\nThis command also starts a transaction block if not inside a\ntransaction already.\n\nWhile inside a migration block, all issued EdgeQL statements are not executed\nimmediately and are instead recorded to be part of the migration script. Aside\nfrom normal EdgeQL commands the following special migration commands are\navailable:\n\n* :eql:stmt:`describe current migration` -- return a list of statements\n  currently recorded as part of the migration;\n\n* :eql:stmt:`populate migration` -- auto-populate the migration with\n  system-generated DDL statements to achieve the target schema state;\n\n* :eql:stmt:`abort migration` -- abort the migration block and discard the\n  migration;\n\n* :eql:stmt:`commit migration` -- commit the migration by executing the\n  migration script statements and recording the migration into the system\n  migration log.\n\nExample\n^^^^^^^\n\nCreate a new migration to a target schema specified by the Gel Schema\nsyntax:\n\n.. code-block:: edgeql\n\n    start migration to {\n        module default {\n            type User {\n                property username: str;\n            };\n        };\n    };\n\n\n.. _ref_eql_ddl_migrations_create:\n\ncreate migration\n----------------\n\n:eql-statement:\n\nCreate a new migration using an explicit EdgeQL script.\n\n.. eql:synopsis::\n\n    create migration \"{\"\n        <edgeql-statement> ;\n        [ ... ]\n    \"}\" ;\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`<edgeql-statement>`\n    Any valid EdgeQL statement, except ``database``, ``branch``, ``role``,\n    ``configure``, ``migration``, or ``transaction`` statements.\n\nDescription\n^^^^^^^^^^^\n\nThe command ``create migration`` executes all the nested EdgeQL commands\nand records the migration into the system migration log.\n\nExample\n^^^^^^^\n\nCreate a new migration to a target schema specified by the Gel Schema\nsyntax:\n\n.. code-block:: edgeql\n\n    create migration {\n        create type default::User {\n            create property username: str;\n        }\n    };\n\n\nAbort migration\n---------------\n\n:eql-statement:\n\nAbort the current migration block and discard the migration.\n\n.. eql:synopsis::\n\n    abort migration ;\n\nDescription\n^^^^^^^^^^^\n\nThe command ``abort migration`` is used to abort a migration block started by\n:eql:stmt:`start migration`. Issuing ``abort migration`` outside of a\nmigration block is an error.\n\nExample\n^^^^^^^\n\nStart a migration block and then abort it:\n\n.. code-block:: edgeql\n\n    start migration to {\n        module default {\n            type User;\n        };\n    };\n\n    abort migration;\n\n\nPopulate migration\n------------------\n\n:eql-statement:\n\nPopulate the current migration with system-generated statements.\n\n.. eql:synopsis::\n\n    populate migration ;\n\nDescription\n^^^^^^^^^^^\n\nThe command ``populate migration`` is used within a migration block started by\n:eql:stmt:`start migration` to automatically fill the migration with\nsystem-generated statements to achieve the desired target schema state. If\nthe system is unable to automatically find a satisfactory sequence of\nstatements to perform the migration, an error is returned. Issuing ``populate\nmigration`` outside of a migration block is also an error.\n\n.. warning::\n\n    The statements generated by ``populate migration`` may drop schema objects,\n    which may result in data loss.  Make sure to inspect the generated\n    migration using :eql:stmt:`describe current migration` before running\n    :eql:stmt:`commit migration`!\n\nExample\n^^^^^^^\n\nStart a migration block and populate it with auto-generated statements.\n\n.. code-block:: edgeql\n\n    start migration to {\n        module default {\n            type User;\n        };\n    };\n\n    populate migration;\n\n\nDescribe current migration\n--------------------------\n\n:eql-statement:\n\nDescribe the migration in the current migration block.\n\n.. eql:synopsis::\n\n    describe current migration [ as {ddl | json} ];\n\n\nDescription\n^^^^^^^^^^^\n\nThe command ``describe current migration`` generates a description of\nthe migration in the current migration block in the specified output\nformat:\n\n:eql:synopsis:`as ddl`\n    Show a sequence of statements currently recorded as part of the migration\n    using valid :ref:`DDL <ref_eql_ddl>` syntax. The output will indicate\n    if the current migration is fully defined, i.e. the recorded statements\n    bring the schema to the state specified by :eql:stmt:`start migration`.\n\n:eql:synopsis:`as json`\n    Provide a machine-readable description of the migration using the following\n    JSON format:\n\n    .. code-block::\n\n        {\n          // Name of the parent migration\n          \"parent\": \"<parent-migration-name>\",\n\n          // Whether the confirmed DDL makes the migration complete,\n          // i.e. there are no more statements to issue.\n          \"complete\": {true|false},\n\n          // List of confirmed migration statements\n          \"confirmed\": [\n            \"<stmt text>\",\n            ...\n          ],\n\n          // The variants of the next statement\n          // suggested by the system to advance\n          // the migration script.\n          \"proposed\": {\n            \"statements\": [{\n              \"text\": \"<stmt text template>\"\n            }],\n            \"required-user-input\": [\n              {\n                \"placeholder\": \"<placeholder variable>\",\n                \"prompt\": \"<statement prompt>\"\n              },\n              ...\n            ],\n            \"confidence\": (0..1), // confidence coefficient\n            \"prompt\": \"<operation prompt>\",\n            \"prompt_id\": \"<prompt id>\",\n            // Whether the operation is considered to be non-destructive.\n            \"data_safe\": {true|false}\n          }\n        }\n\n    Where:\n\n    :eql:synopsis:`<stmt text>`\n        Regular statement text.\n\n    :eql:synopsis:`<stmt text template>`\n        Statement text template with interpolation points using the ``\\(name)``\n        syntax.\n\n    :eql:synopsis:`<placeholder variable>`\n        The name of an interpolation variable in the statement text template\n        for which the user prompt is given.\n\n    :eql:synopsis:`<statement prompt>`\n        The text of a user prompt for an interpolation variable.\n\n    :eql:synopsis:`<operation prompt>`\n        Prompt for the proposed migration step.\n\n    :eql:synopsis:`<prompt id>`\n        An opaque string identifier for a particular operation prompt.\n        The client should not repeat prompts with the same prompt id.\n\n\nCommit migration\n----------------\n\n:eql-statement:\n\nCommit the current migration to the database.\n\n.. eql:synopsis::\n\n    commit migration ;\n\nDescription\n^^^^^^^^^^^\n\nThe command ``commit migration`` executes all the commands defined by\nthe current migration and records the migration as the most recent\nmigration in the database.\n\nIssuing ``commit migration`` outside of a migration block initiated\nby :eql:stmt:`start migration` is an error.\n\nExample\n^^^^^^^\n\nCreate and execute the current migration:\n\n.. code-block:: edgeql\n\n    commit migration;\n\n\nReset schema to initial\n-----------------------\n\n:eql-statement:\n\nReset the database schema to its initial state.\n\n.. eql:synopsis::\n\n    reset schema to initial ;\n\n.. warning::\n\n    This command will drop all entities and, as a consequence, all data. You\n    won't want to use this statement on a production instance unless you want\n    to lose all that instance's data.\n\n\nMigration rewrites DDL commands\n===============================\n\nMigration rewrites allow you to change the migration history as long as your\nfinal schema matches the current database schema.\n\nStart migration rewrite\n-----------------------\n\nStart a migration rewrite.\n\n.. eql:synopsis::\n\n    start migration rewrite ;\n\nOnce the migration rewrite is started, you can run any arbitrary DDL until you\nare ready to :ref:`commit <ref_eql_ddl_migrations_rewrites_commit>` your new\nmigration history. The most useful DDL in this context will be :ref:`create\nmigration <ref_eql_ddl_migrations_create>` statements, which will allow you to\ncreate a sequence of migrations that will become your new migration history.\n\nDeclare savepoint\n-----------------\n\nEstablish a new savepoint within the current migration rewrite.\n\n.. eql:synopsis::\n\n    declare savepoint <savepoint-name> ;\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`<savepoint-name>`\n    The name which will be used to identify the new savepoint if you need to\n    later release it or roll back to it.\n\nRelease savepoint\n-----------------\n\nDestroys a savepoint previously defined in the current migration rewrite.\n\n.. eql:synopsis::\n\n    release savepoint <savepoint-name> ;\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`<savepoint-name>`\n    The name of the savepoint to be released.\n\nRollback to savepoint\n---------------------\n\nRollback to the named savepoint.\n\n.. eql:synopsis::\n\n    rollback to savepoint <savepoint-name> ;\n\nAll changes made after the savepoint are discarded. The savepoint remains valid\nand can be rolled back to again later, if needed.\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`<savepoint-name>`\n    The name of the savepoint to roll back to.\n\nRollback\n--------\n\nRollback the entire migration rewrite.\n\n.. eql:synopsis::\n\n    rollback ;\n\nAll updates made within the transaction are discarded.\n\n.. _ref_eql_ddl_migrations_rewrites_commit:\n\nCommit migration rewrite\n------------------------\n\nCommit a migration rewrite.\n\n.. eql:synopsis::\n\n    commit migration rewrite ;\n"
  },
  {
    "path": "docs/reference/datamodel/modules.rst",
    "content": ".. _ref_datamodel_modules:\n.. _ref_eql_sdl_modules:\n\n=======\nModules\n=======\n\nEach |branch| has a schema consisting of several **modules**, each with\na unique name. Modules can be used to organize large schemas into\nlogical units. In practice, though, most users put their entire\nschema inside a single module called ``default``.\n\n.. code-block:: sdl\n\n  module default {\n    # declare types here\n  }\n\n.. _ref_name_resolution:\n\nName resolution\n===============\n\nWhen you define a module that references schema objects from another module,\nyou must use a *fully-qualified* name in the form\n``other_module_name::object_name``:\n\n.. code-block:: sdl\n\n  module A {\n    type User extending B::AbstractUser;\n  }\n\n  module B {\n    abstract type AbstractUser {\n      required name: str;\n    }\n  }\n\nReserved module names\n=====================\n\nThe following module names are reserved by |Gel| and contain pre-defined\ntypes, utility functions, and operators:\n\n* ``std``: standard types, functions, and operators in the :ref:`standard\n  library <ref_std>`\n* ``math``: algebraic and statistical :ref:`functions <ref_std_math>`\n* ``cal``: local (non-timezone-aware) and relative date/time :ref:`types and\n  functions <ref_std_datetime>`\n* ``schema``: types describing the :ref:`introspection\n  <ref_datamodel_introspection>` schema\n* ``sys``: system-wide entities, such as user roles and\n  :ref:`databases <ref_datamodel_databases>`\n* ``cfg``: configuration and settings\n\n\nModules are containers\n======================\n\nThey can contain types, functions, and other modules. Here's an example of an\nempty module:\n\n.. code-block:: sdl\n\n    module my_module {}\n\nAnd here's an example of a module with a type:\n\n.. code-block:: sdl\n\n    module my_module {\n      type User {\n        required name: str;\n      }\n    }\n\n\nNested modules\n==============\n\n.. code-block:: sdl\n\n    module dracula {\n      type Person {\n        required name: str;\n        multi places_visited: City;\n        strength: int16;\n      }\n\n      module combat {\n        function fight(\n          one: dracula::Person,\n          two: dracula::Person\n        ) -> str\n          using (\n            (one.name ?? 'Fighter 1') ++ ' wins!'\n            IF (one.strength ?? 0) > (two.strength ?? 0)\n            ELSE (two.name ?? 'Fighter 2') ++ ' wins!'\n          );\n      }\n    }\n\nYou can chain together module names in a fully-qualified name to traverse a\ntree of nested modules. For example, to call the ``fight`` function in the\nnested module example above, you would use\n``dracula::combat::fight(<arguments>)``.\n\n\nDeclaring modules\n=================\n\nThis section describes the syntax to declare a module in your schema.\n\n\nSyntax\n------\n\n.. sdl:synopsis::\n\n    module <ModuleName> \"{\"\n      [ <schema-declarations> ]\n      ...\n    \"}\"\n\nDefine a nested module:\n\n.. sdl:synopsis::\n\n    module <ParentModuleName> \"{\"\n      [ <schema-declarations> ]\n      module <ModuleName> \"{\"\n        [ <schema-declarations> ]\n      \"}\"\n      ...\n    \"}\"\n\n\nDescription\n^^^^^^^^^^^\n\nThe module block declaration defines a new module similar to the\n:eql:stmt:`create module` command, but it also allows putting the\nmodule content as nested declarations:\n\n:sdl:synopsis:`<schema-declarations>`\n    Define various schema items that belong to this module.\n\nUnlike :eql:stmt:`create module`, a module block with the\nsame name can appear multiple times in an SDL document. In that case\nall blocks with the same name are merged into a single module under\nthat name. For example:\n\n.. code-block:: sdl\n\n    module my_module {\n      abstract type Named {\n        required name: str;\n      }\n    }\n\n    module my_module {\n      type User extending Named;\n    }\n\nThe above is equivalent to:\n\n.. code-block:: sdl\n\n    module my_module {\n      abstract type Named {\n        required name: str;\n      }\n\n      type User extending Named;\n    }\n\nTypically, in the documentation examples of SDL the *module block* is\nomitted and instead its contents are described without assuming which\nspecific module they belong to.\n\nIt's also possible to declare modules implicitly. In this style, SDL\ndeclaration uses a :ref:`fully-qualified name <ref_name_resolution>` for the\nitem that is being declared. The *module* part of the *fully-qualified* name\nimplies that a module by that name will be automatically created in the\nschema. The following declaration is equivalent to the previous examples,\nbut it declares module ``my_module`` implicitly:\n\n.. code-block:: sdl\n\n    abstract type my_module::Named {\n        required name: str;\n    }\n\n    type my_module::User extending my_module::Named;\n\nA module block can be nested inside another module block to create a nested\nmodule. If you want to reference an entity in a nested module by its\nfully-qualified name, you will need to include all of the containing\nmodules' names: ``<ParentModuleName>::<ModuleName>::<EntityName>``\n\n.. _ref_eql_ddl_modules:\n\nDDL commands\n============\n\nThis section describes the low-level DDL commands for creating and dropping\nmodules. You typically don't need to use these commands directly, but\nknowing about them is useful for reviewing migrations.\n\n\nCreate module\n-------------\n\n:eql-statement:\n\nCreate a new module.\n\n.. eql:synopsis::\n\n    create module [ <parent-name>:: ] <name>\n      [ if not exists ];\n\nThere's a :ref:`corresponding SDL declaration <ref_eql_sdl_modules>`\nfor a module, although in SDL a module declaration is likely to also\ninclude that module's content.\n\n\nDescription\n^^^^^^^^^^^\n\nThe command ``create module`` defines a new module for the current\n:versionreplace:`database;5.0:branch`. The name of the new module must be\ndistinct from any existing module in the current\n:versionreplace:`database;5.0:branch`. Unlike :ref:`SDL module declaration\n<ref_eql_sdl_modules>` the ``create module`` command does not have sub-commands;\nmodule contents are created separately.\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`if not exists`\n    Normally, creating a module that already exists is an error, but\n    with this flag the command will succeed. It is useful for scripts\n    that add something to a module or, if the module is missing, the\n    module is created as well.\n\nExamples\n^^^^^^^^\n\nCreate a new module:\n\n.. code-block:: edgeql\n\n    create module payments;\n\nCreate a new nested module:\n\n.. code-block:: edgeql\n\n    create module payments::currencies;\n\n\nDrop module\n-----------\n\n:eql-statement:\n\nRemove a module.\n\n.. eql:synopsis::\n\n    drop module <name> ;\n\nDescription\n^^^^^^^^^^^\n\nThe command ``drop module`` removes an existing empty module from the\ncurrent :versionreplace:`database;5.0:branch`. If the module contains any\nschema items, this command will fail.\n\nExamples\n^^^^^^^^\n\nRemove a module:\n\n.. code-block:: edgeql\n\n    drop module payments;\n"
  },
  {
    "path": "docs/reference/datamodel/mutation_rewrites.rst",
    "content": ".. _ref_datamodel_mutation_rewrites:\n\n=================\nMutation rewrites\n=================\n\n.. index:: modify, modification\n\nMutation rewrites allow you to intercept database mutations (i.e.,\n:ref:`inserts <ref_eql_insert>` and/or :ref:`updates <ref_eql_update>`) and set\nthe value of a property or link to the result of an expression you define. They\ncan be defined in your schema.\n\nMutation rewrites are complementary to :ref:`triggers\n<ref_datamodel_triggers>`. While triggers are unable to modify the triggering\nobject, mutation rewrites are built for that purpose.\n\nExample: last modified\n======================\n\nHere's an example of a mutation rewrite that updates a property of a ``Post``\ntype to reflect the time of the most recent modification:\n\n.. code-block:: sdl\n\n   type Post {\n     required title: str;\n     required body: str;\n     modified: datetime {\n       rewrite insert, update using (datetime_of_statement())\n     }\n   }\n\nEvery time a ``Post`` is updated, the mutation rewrite will be triggered,\nupdating the ``modified`` property:\n\n.. code-block:: edgeql-repl\n\n   db> insert Post {\n   ...   title := 'One wierd trick to fix all your spelling errors'\n   ... };\n   {default::Post {id: 19e024dc-d3b5-11ed-968c-37f5d0159e5f}}\n   db> select Post {title, modified};\n   {\n     default::Post {\n       title: 'One wierd trick to fix all your spelling errors',\n       modified: <datetime>'2023-04-05T13:23:49.488335Z',\n     },\n   }\n   db> update Post\n   ... filter .id = <uuid>'19e024dc-d3b5-11ed-968c-37f5d0159e5f'\n   ... set {title := 'One weird trick to fix all your spelling errors'};\n   {default::Post {id: 19e024dc-d3b5-11ed-968c-37f5d0159e5f}}\n   db> select Post {title, modified};\n   {\n     default::Post {\n       title: 'One weird trick to fix all your spelling errors',\n       modified: <datetime>'2023-04-05T13:25:04.119641Z',\n     },\n   }\n\nIn some cases, you will want different rewrites depending on the type of query.\nHere, we will add an ``insert`` rewrite and an ``update`` rewrite:\n\n.. code-block:: sdl\n\n   type Post {\n     required title: str;\n     required body: str;\n     created: datetime {\n       rewrite insert using (datetime_of_statement())\n     }\n     modified: datetime {\n       rewrite update using (datetime_of_statement())\n     }\n   }\n\nWith this schema, inserts will set the ``Post`` object's ``created`` property\nwhile updates will set the ``modified`` property:\n\n.. code-block:: edgeql-repl\n\n   db> insert Post {\n   ...   title := 'One wierd trick to fix all your spelling errors'\n   ... };\n   {default::Post {id: 19e024dc-d3b5-11ed-968c-37f5d0159e5f}}\n   db> select Post {title, created, modified};\n   {\n     default::Post {\n       title: 'One wierd trick to fix all your spelling errors',\n       created: <datetime>'2023-04-05T13:23:49.488335Z',\n       modified: {},\n     },\n   }\n   db> update Post\n   ... filter .id = <uuid>'19e024dc-d3b5-11ed-968c-37f5d0159e5f'\n   ... set {title := 'One weird trick to fix all your spelling errors'};\n   {default::Post {id: 19e024dc-d3b5-11ed-968c-37f5d0159e5f}}\n   db> select Post {title, created, modified};\n   {\n     default::Post {\n       title: 'One weird trick to fix all your spelling errors',\n       created: <datetime>'2023-04-05T13:23:49.488335Z',\n       modified: <datetime>'2023-04-05T13:25:04.119641Z',\n     },\n   }\n\n.. note::\n\n   Each property may have a single ``insert`` and a single ``update`` mutation\n   rewrite rule, or they may have a single rule that covers both.\n\n\nMutation context\n================\n\n.. api-index:: rewrite, __subject__, __specified__, __old__\n\nInside the rewrite rule's expression, you have access to a few special values:\n\n* ``__subject__`` refers to the object type with the new property and link\n  values.\n* ``__specified__`` is a named tuple with a key for each property or link in\n  the type and a boolean value indicating whether this value was explicitly set\n  in the mutation.\n* ``__old__`` refers to the object type with the previous property and link\n  values (available for update-only mutation rewrites).\n\nHere are some examples of the special values in use. Maybe your blog hosts\narticles about particularly controversial topics. You could use ``__subject__``\nto enforce a \"cooling off\" period before publishing a blog post:\n\n.. code-block:: sdl\n\n   type Post {\n     required title: str;\n     required body: str;\n     publish_time: datetime {\n       rewrite insert, update using (\n         __subject__.publish_time ?? datetime_of_statement() +\n         cal::to_relative_duration(days := 10)\n       )\n     }\n   }\n\nHere we take the post's ``publish_time`` if set or the time the statement is\nexecuted and add 10 days to it. That should give our authors time to consider\nif they want to make any changes before a post goes live.\n\nYou can omit ``__subject__`` in many cases and achieve the same thing:\n\n.. code-block:: sdl-diff\n\n     type Post {\n       required title: str;\n       required body: str;\n       publish_time: datetime {\n         rewrite insert, update using (\n   -       __subject__.publish_time ?? datetime_of_statement() +\n   +       .publish_time ?? datetime_of_statement() +\n           cal::to_relative_duration(days := 10)\n         )\n       }\n     }\n\nbut only if the path prefix has not changed. In the following schema, for\nexample, the ``__subject__`` in the rewrite rule is required, because in the\ncontext of the nested ``select`` query, the leading dot resolves from the\n``User`` path:\n\n.. code-block:: sdl\n\n   type Post {\n     required title: str;\n     required body: str;\n     author_email: str;\n     author_name: str {\n       rewrite insert, update using (\n         (select User {name} filter .email = __subject__.author_email).name\n       )\n     }\n   }\n   type User {\n     name: str;\n     email: str;\n   }\n\n.. note::\n\n   Learn more about how this works in our documentation on :ref:`path\n   resolution <ref_eql_path_resolution>`.\n\nUsing ``__specified__``, we can determine which fields were specified in the\nmutation. This would allow us to track when a single property was last modified\nas in the ``title_modified`` property in this schema:\n\n.. code-block:: sdl\n\n   type Post {\n     required title: str;\n     required body: str;\n     title_modified: datetime {\n       rewrite update using (\n         datetime_of_statement()\n         if __specified__.title\n         else __old__.title_modified\n       )\n     }\n   }\n\n``__specified__.title`` will be ``true`` if that value was set as part of the\nupdate, and this rewrite mutation rule will update ``title_modified`` to\n``datetime_of_statement()`` in that case.\n\nAnother way you might use this is to set a default value but allow overriding:\n\n.. code-block:: sdl\n\n   type Post {\n     required title: str;\n     required body: str;\n     modified: datetime {\n       rewrite update using (\n         datetime_of_statement()\n         if not __specified__.modified\n         else .modified\n       )\n     }\n   }\n\nHere, we rewrite ``modified`` on updates to ``datetime_of_statement()`` unless\n``modified`` was set in the update. In that case, we allow the specified value\nto be set. This is different from a :ref:`default\n<ref_datamodel_props_default_values>` value because the rewrite happens on each\nupdate whereas a default value is applied only on insert of a new object.\n\nOne shortcoming in using ``__specified__`` to decide whether to update the\n``modified`` property is that we still don't know whether the value changed —\nonly that it was specified in the query. It's possible the value specified was\nthe same as the existing value. You'd need to check the value itself to decide\nif it has changed.\n\nThis is easy enough for a single value, but what if you want a global\n``modified`` property that is updated only if any of the properties or links\nwere changed? That could get cumbersome quickly for an object of any\ncomplexity.\n\nInstead, you might try casting ``__subject__`` and ``__old__`` to ``json`` and\ncomparing them:\n\n.. code-block:: sdl\n\n   type Post {\n     required title: str;\n     required body: str;\n     modified: datetime {\n       rewrite update using (\n         datetime_of_statement()\n         if <json>__subject__ {**} != <json>__old__ {**}\n         else __old__.modified\n       )\n     }\n   }\n\nLastly, if we want to add an ``author`` property that can be set for each write\nand keep a history of all the authors, we can do this with the help of\n``__old__``:\n\n.. code-block:: sdl\n\n   type Post {\n     required title: str;\n     required body: str;\n     author: str;\n     all_authors: array<str> {\n       default := <array<str>>[];\n       rewrite update using (\n         __old__.all_authors\n         ++ [__subject__.author]\n       );\n     }\n   }\n\nOn insert, our ``all_authors`` property will get initialized to an empty array\nof strings. We will rewrite updates to concatenate that array with an array\ncontaining the new author value.\n\n\nCached computed\n===============\n\nMutation rewrites can be used to effectively create a cached computed value as\ndemonstrated with the ``byline`` property in this schema:\n\n.. code-block:: sdl\n\n   type Post {\n     required title: str;\n     required body: str;\n     author: str;\n     created: datetime {\n       rewrite insert using (datetime_of_statement())\n     }\n     byline: str {\n       rewrite insert, update using (\n         'by ' ++\n         __subject__.author ++\n         ' on ' ++\n         to_str(__subject__.created, 'Mon DD, YYYY')\n       )\n     }\n   }\n\nThe ``byline`` property will be updated on each insert or update, but the value\nwill not need to be calculated at read time like a proper :ref:`computed\nproperty <ref_datamodel_computed>`.\n\n\n.. _ref_eql_sdl_mutation_rewrites:\n.. _ref_eql_sdl_mutation_rewrites_syntax:\n\nDeclaring mutation rewrites\n===========================\n\n.. api-index:: rewrite insert, rewrite update, using\n\nThis section describes the syntax to declare mutation rewrites in your schema.\n\nSyntax\n------\n\nDefine a new mutation rewrite corresponding to the :ref:`more explicit DDL\ncommands <ref_eql_ddl_mutation_rewrites>`.\n\n.. sdl:synopsis::\n\n   rewrite {insert | update} [, ...]\n     using <expr>\n\nMutation rewrites must be defined inside a property or link block.\n\nDescription\n^^^^^^^^^^^\n\nThis declaration defines a new trigger with the following options:\n\n:eql:synopsis:`insert | update [, ...]`\n    The query type (or types) the rewrite runs on. Separate multiple values\n    with commas to invoke the same rewrite for multiple types of queries.\n\n:eql:synopsis:`<expr>`\n    The expression to be evaluated to produce the new value of the property.\n\n\n.. _ref_eql_ddl_mutation_rewrites:\n\nDDL commands\n============\n\nThis section describes the low-level DDL commands for creatin and\ndropping mutation rewrites. You typically don't need to use these commands\ndirectly, but knowing about them is useful for reviewing migrations.\n\n\nCreate rewrite\n--------------\n\n:eql-statement:\n\nDefine a new mutation rewrite.\n\nWhen creating a new property or link:\n\n.. eql:synopsis::\n\n   {create | alter} type <type-name> \"{\"\n     create { property | link } <prop-or-link-name>: <type> \"{\"\n       create rewrite {insert | update} [, ...]\n         using <expr>\n     \"}\" ;\n   \"}\" ;\n\nWhen altering an existing property or link:\n\n.. eql:synopsis::\n\n   {create | alter} type <type-name> \"{\"\n     alter { property | link } <prop-or-link-name> \"{\"\n       create rewrite {insert | update} [, ...]\n         using <expr>\n     \"}\" ;\n   \"}\" ;\n\n\nDescription\n^^^^^^^^^^^\n\nThe command ``create rewrite`` nested under ``create type`` or ``alter type``\nand then under ``create property/link`` or ``alter property/link`` defines a\nnew mutation rewrite for the given property or link on the given object.\n\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`<type-name>`\n    The name (optionally module-qualified) of the type containing the rewrite.\n\n:eql:synopsis:`<prop-or-link-name>`\n    The name (optionally module-qualified) of the property or link being\n    rewritten.\n\n:eql:synopsis:`insert | update [, ...]`\n    The query type (or types) that are rewritten. Separate multiple values with\n    commas to invoke the same rewrite for multiple types of queries.\n\n\nExamples\n^^^^^^^^\n\nDeclare two mutation rewrites on new properties: one that sets a ``created``\nproperty when a new object is inserted and one that sets a ``modified``\nproperty on each update:\n\n.. code-block:: edgeql\n\n   alter type User {\n     create property created: datetime {\n       create rewrite insert using (datetime_of_statement());\n     };\n     create property modified: datetime {\n       create rewrite update using (datetime_of_statement());\n     };\n   };\n\n\nDrop rewrite\n------------\n\n:eql-statement:\n\nDrop a mutation rewrite.\n\n.. eql:synopsis::\n\n   alter type <type-name> \"{\"\n     alter property <prop-or-link-name> \"{\"\n       drop rewrite {insert | update} ;\n     \"}\" ;\n   \"}\" ;\n\n\nDescription\n^^^^^^^^^^^\n\nThe command ``drop rewrite`` inside an ``alter type`` block and further inside\nan ``alter property`` block removes the definition of an existing mutation\nrewrite on the specified property or link of the specified type.\n\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`<type-name>`\n    The name (optionally module-qualified) of the type containing the rewrite.\n\n:eql:synopsis:`<prop-or-link-name>`\n    The name (optionally module-qualified) of the property or link being\n    rewritten.\n\n:eql:synopsis:`insert | update [, ...]`\n    The query type (or types) that are rewritten. Separate multiple values with\n    commas to invoke the same rewrite for multiple types of queries.\n\n\nExample\n^^^^^^^\n\nRemove the ``insert`` rewrite of the ``created`` property on the ``User`` type:\n\n.. code-block:: edgeql\n\n   alter type User {\n     alter property created {\n       drop rewrite insert;\n     };\n   };\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Introspection > Mutation rewrites\n      <ref_datamodel_introspection_mutation_rewrites>`\n"
  },
  {
    "path": "docs/reference/datamodel/objects.rst",
    "content": ".. _ref_datamodel_object_types:\n\n============\nObject Types\n============\n\n.. index:: tables, models\n\n*Object types* are the primary components of a Gel schema. They are\nanalogous to SQL *tables* or ORM *models*, and consist of :ref:`properties\n<ref_datamodel_props>` and :ref:`links <ref_datamodel_links>`.\n\nProperties\n==========\n\nProperties are used to attach primitive/scalar data to an object type.\nFor the full documentation on properties, see :ref:`ref_datamodel_props`.\n\n.. code-block:: sdl\n\n   type Person {\n     email: str;\n   }\n\nUsing in a query:\n\n.. code-block:: edgeql\n\n   select Person {\n     email\n   };\n\n\nLinks\n=====\n\nLinks are used to define relationships between object types. For the full\ndocumentation on links, see :ref:`ref_datamodel_links`.\n\n.. code-block:: sdl\n\n   type Person {\n     email: str;\n     best_friend: Person;\n   }\n\nUsing in a query:\n\n.. code-block:: edgeql\n\n   select Person {\n     email,\n     best_friend: {\n       email\n     }\n   };\n\nID\n==\n\n.. index:: uuid, primary key\n\nThere's no need to manually declare a primary key on your object types. All\nobject types automatically contain a property ``id`` of type ``UUID`` that's\n*required*, *globally unique*, *readonly*, and has an index on it.\nThe ``id`` is assigned upon creation and cannot be changed.\n\nUsing in a query:\n\n.. code-block:: edgeql\n\n   select Person { id };\n   select Person { email } filter .id = <uuid>'123e4567-e89b-...';\n\n\nAbstract types\n==============\n\n.. index:: abstract, inheritance\n\nObject types can either be *abstract* or *non-abstract*. By default all object\ntypes are non-abstract. You can't create or store instances of abstract types\n(a.k.a. mixins), but they're a useful way to share functionality and\nstructure among other object types.\n\n.. code-block:: sdl\n\n   abstract type HasName {\n     first_name: str;\n     last_name: str;\n   }\n\n.. _ref_datamodel_objects_inheritance:\n.. _ref_eql_sdl_object_types_inheritance:\n\nInheritance\n===========\n\n.. index:: extending, extends, subtypes, supertypes\n\nObject types can *extend* other object types. The extending type (AKA the\n*subtype*) inherits all links, properties, indexes, constraints, etc. from its\n*supertypes*.\n\n.. code-block:: sdl\n\n   abstract type HasName {\n     first_name: str;\n     last_name: str;\n   }\n\n   type Person extending HasName {\n     email: str;\n     best_friend: Person;\n   }\n\nUsing in a query:\n\n.. code-block:: edgeql\n\n   select Person {\n     first_name,\n     email,\n     best_friend: {\n       last_name\n     }\n   };\n\n\n.. _ref_datamodel_objects_multiple_inheritance:\n\nMultiple Inheritance\n====================\n\nObject types can extend more than one type — that's called\n*multiple inheritance*. This mechanism allows building complex object\ntypes out of combinations of more basic types.\n\n.. note::\n\n   Gel's multiple inheritance should not be confused with the multiple\n   inheritance of C++ or Python, where the complexity usually arises\n   from fine-grained mixing of logic. Gel's multiple inheritance is\n   structural and allows for natural composition.\n\n.. code-block:: sdl-diff\n\n      abstract type HasName {\n        first_name: str;\n        last_name: str;\n      }\n\n   +  abstract type HasEmail {\n   +    email: str;\n   +  }\n\n   -  type Person extending HasName {\n   +  type Person extending HasName, HasEmail {\n   -    email: str;\n        best_friend: Person;\n     }\n\nIf multiple supertypes share links or properties, those properties must be\nof the same type and cardinality.\n\n\n.. _ref_eql_sdl_object_types:\n.. _ref_eql_sdl_object_types_syntax:\n\n\nDefining object types\n=====================\n\n.. api-index:: abstract, type, extending\n\nThis section describes the syntax to declare object types in your schema.\n\nSyntax\n------\n\n.. sdl:synopsis::\n\n   [abstract] type <TypeName> [extending <supertype> [, ...] ]\n   [ \"{\"\n       [ <annotation-declarations> ]\n       [ <property-declarations> ]\n       [ <link-declarations> ]\n       [ <constraint-declarations> ]\n       [ <index-declarations> ]\n       ...\n     \"}\" ]\n\nDescription\n^^^^^^^^^^^\n\nThis declaration defines a new object type with the following options:\n\n:eql:synopsis:`abstract`\n    If specified, the created type will be *abstract*.\n\n:eql:synopsis:`<TypeName>`\n    The name (optionally module-qualified) of the new type.\n\n:eql:synopsis:`extending <supertype> [, ...]`\n    Optional clause specifying the *supertypes* of the new type.\n\n    Use of ``extending`` creates a persistent type relationship\n    between the new subtype and its supertype(s).  Schema modifications\n    to the supertype(s) propagate to the subtype.\n\n    References to supertypes in queries will also include objects of\n    the subtype.\n\n    If the same *link* name exists in more than one supertype, or\n    is explicitly defined in the subtype and at least one supertype,\n    then the data types of the link targets must be *compatible*.\n    If there is no conflict, the links are merged to form a single\n    link in the new type.\n\nThese sub-declarations are allowed in the ``Type`` block:\n\n:sdl:synopsis:`<annotation-declarations>`\n    Set object type :ref:`annotation <ref_eql_sdl_annotations>`\n    to a given *value*.\n\n:sdl:synopsis:`<property-declarations>`\n    Define a concrete :ref:`property <ref_eql_sdl_props>` for this object type.\n\n:sdl:synopsis:`<link-declarations>`\n    Define a concrete :ref:`link <ref_eql_sdl_links>` for this object type.\n\n:sdl:synopsis:`<constraint-declarations>`\n    Define a concrete :ref:`constraint <ref_eql_sdl_constraints>` for this\n    object type.\n\n:sdl:synopsis:`<index-declarations>`\n    Define an :ref:`index <ref_eql_sdl_indexes>` for this object type.\n\n\n.. _ref_eql_ddl_object_types:\n\nDDL commands\n============\n\nThis section describes the low-level DDL commands for creating, altering, and\ndropping object types. You typically don't need to use these commands directly,\nbut knowing about them is useful for reviewing migrations.\n\nCreate type\n-----------\n\n:eql-statement:\n:eql-haswith:\n\nDefine a new object type.\n\n.. eql:synopsis::\n\n   [ with <with-item> [, ...] ]\n   create [abstract] type <name> [ extending <supertype> [, ...] ]\n   [ \"{\" <subcommand>; [...] \"}\" ] ;\n\n   # where <subcommand> is one of\n\n     create annotation <annotation-name> := <value>\n     create link <link-name> ...\n     create property <property-name> ...\n     create constraint <constraint-name> ...\n     create index on <index-expr>\n\nDescription\n^^^^^^^^^^^\n\nThe command ``create type`` defines a new object type for use in the\ncurrent |branch|.\n\nIf *name* is qualified with a module name, then the type is created\nin that module, otherwise it is created in the current module.\nThe type name must be distinct from that of any existing schema item\nin the module.\n\nParameters\n^^^^^^^^^^\n\nMost sub-commands and options of this command are identical to the\n:ref:`SDL object type declaration <ref_eql_sdl_object_types_syntax>`,\nwith some additional features listed below:\n\n:eql:synopsis:`with <with-item> [, ...]`\n    Alias declarations.\n\n    The ``with`` clause allows specifying module aliases\n    that can be referenced by the command.  See :ref:`ref_eql_statements_with`\n    for more information.\n\nThe following subcommands are allowed in the ``create type`` block:\n\n:eql:synopsis:`create annotation <annotation-name> := <value>`\n    Set object type :eql:synopsis:`<annotation-name>` to\n    :eql:synopsis:`<value>`.\n\n    See :eql:stmt:`create annotation` for details.\n\n:eql:synopsis:`create link <link-name> ...`\n    Define a new link for this object type.  See\n    :eql:stmt:`create link` for details.\n\n:eql:synopsis:`create property <property-name> ...`\n    Define a new property for this object type.  See\n    :eql:stmt:`create property` for details.\n\n:eql:synopsis:`create constraint <constraint-name> ...`\n    Define a concrete constraint for this object type.  See\n    :eql:stmt:`create constraint` for details.\n\n:eql:synopsis:`create index on <index-expr>`\n    Define a new :ref:`index <ref_datamodel_indexes>`\n    using *index-expr* for this object type.  See\n    :eql:stmt:`create index` for details.\n\nExample\n^^^^^^^\n\nCreate an object type ``User``:\n\n.. code-block:: edgeql\n\n   create type User {\n       create property name: str;\n   };\n\n\nAlter type\n----------\n\n:eql-statement:\n:eql-haswith:\n\nChange the definition of an object type.\n\n.. eql:synopsis::\n\n   [ with <with-item> [, ...] ]\n   alter type <name>\n   [ \"{\" <subcommand>; [...] \"}\" ] ;\n\n   [ with <with-item> [, ...] ]\n   alter type <name> <subcommand> ;\n\n   # where <subcommand> is one of\n\n     rename to <newname>\n     extending <parent> [, ...]\n     create annotation <annotation-name> := <value>\n     alter annotation <annotation-name> := <value>\n     drop annotation <annotation-name>\n     create link <link-name> ...\n     alter link <link-name> ...\n     drop link <link-name> ...\n     create property <property-name> ...\n     alter property <property-name> ...\n     drop property <property-name> ...\n     create constraint <constraint-name> ...\n     alter constraint <constraint-name> ...\n     drop constraint <constraint-name> ...\n     create index on <index-expr>\n     drop index on <index-expr>\n\nDescription\n^^^^^^^^^^^\n\nThe command ``alter type`` changes the definition of an object type.\n*name* must be a name of an existing object type, optionally qualified\nwith a module name.\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`with <with-item> [, ...]`\n    Alias declarations.\n\n    The ``with`` clause allows specifying module aliases\n    that can be referenced by the command.  See :ref:`ref_eql_statements_with`\n    for more information.\n\n:eql:synopsis:`<name>`\n    The name (optionally module-qualified) of the type being altered.\n\n:eql:synopsis:`extending <parent> [, ...]`\n    Alter the supertype list.  The full syntax of this subcommand is:\n\n    .. eql:synopsis::\n\n       extending <parent> [, ...]\n         [ first | last | before <exparent> | after <exparent> ]\n\n    This subcommand makes the type a subtype of the specified list\n    of supertypes.  The requirements for the parent-child relationship\n    are the same as when creating an object type.\n\n    It is possible to specify the position in the parent list\n    using the following optional keywords:\n\n    * ``first`` -- insert parent(s) at the beginning of the\n      parent list,\n    * ``last`` -- insert parent(s) at the end of the parent list,\n    * ``before <parent>`` -- insert parent(s) before an\n      existing *parent*,\n    * ``after <parent>`` -- insert parent(s) after an existing\n      *parent*.\n\n:eql:synopsis:`alter annotation <annotation-name>;`\n    Alter object type annotation :eql:synopsis:`<annotation-name>`.\n    See :eql:stmt:`alter annotation` for details.\n\n:eql:synopsis:`drop annotation <annotation-name>`\n    Remove object type :eql:synopsis:`<annotation-name>`.\n    See :eql:stmt:`drop annotation` for details.\n\n:eql:synopsis:`alter link <link-name> ...`\n    Alter the definition of a link for this object type.  See\n    :eql:stmt:`alter link` for details.\n\n:eql:synopsis:`drop link <link-name>`\n    Remove a link item from this object type.  See\n    :eql:stmt:`drop link` for details.\n\n:eql:synopsis:`alter property <property-name> ...`\n    Alter the definition of a property item for this object type.\n    See :eql:stmt:`alter property` for details.\n\n:eql:synopsis:`drop property <property-name>`\n    Remove a property item from this object type.  See\n    :eql:stmt:`drop property` for details.\n\n:eql:synopsis:`alter constraint <constraint-name> ...`\n    Alter the definition of a constraint for this object type.  See\n    :eql:stmt:`alter constraint` for details.\n\n:eql:synopsis:`drop constraint <constraint-name>;`\n    Remove a constraint from this object type.  See\n    :eql:stmt:`drop constraint` for details.\n\n:eql:synopsis:`drop index on <index-expr>`\n    Remove an :ref:`index <ref_datamodel_indexes>` defined as *index-expr*\n    from this object type.  See :eql:stmt:`drop index` for details.\n\nAll the subcommands allowed in the ``create type`` block are also\nvalid subcommands for the ``alter type`` block.\n\nExample\n^^^^^^^\n\nAlter the ``User`` object type to make ``name`` required:\n\n.. code-block:: edgeql\n\n   alter type User {\n       alter property name {\n           set required;\n       }\n   };\n\n\nDrop type\n---------\n\n:eql-statement:\n:eql-haswith:\n\nRemove the specified object type from the schema.\n\n.. eql:synopsis::\n\n   drop type <name> ;\n\nDescription\n^^^^^^^^^^^\n\nThe command ``drop type`` removes the specified object type from the\nschema.  All subordinate schema items defined on this type,\nsuch as links and indexes, are removed as well.\n\nExample\n^^^^^^^\n\nRemove the ``User`` object type:\n\n.. code-block:: edgeql\n\n   drop type User;\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Introspection > Object types\n      <ref_datamodel_introspection_object_types>`\n  * - :ref:`Cheatsheets > Object types <ref_cheatsheet_object_types>`\n"
  },
  {
    "path": "docs/reference/datamodel/permissions.rst",
    "content": ".. _ref_datamodel_permissions:\n\n.. versionadded:: 7.0\n\n===========\nPermissions\n===========\n\n.. index:: RBAC, role based access control, capability\n\n*Permissions* are the mechanism for limiting access to the database based on\nprovided connection credentials.\n\nEach :ref:`role <ref_admin_roles>` has as set of granted permissions.\n\n.. code-block:: edgeql\n\n    create role alice {\n        set password := 'wonderland';\n        set permissions := {\n            sys::perm::data_modifiction,\n            default::can_see_secrets\n        };\n    };\n\nPermissions are either :ref:`built-in <ref_datamodel_permissions_built_in>` or\n:ref:`defined in schema <ref_datamodel_permissions_custom>`.\n\nSome language features or functions require current role to have certain\npermissions. For example, to use ``insert``, ``update`` or ``delete``, current\nrole is required to have ``sys::perm::data_modification``.\n\nAdditionally, permissions of current role can be accessed via\n:ref:`global variables<ref_datamodel_globals>` of the same name:\n\n.. code-block:: edgeql\n\n    select global sys::perm::data_modification;\n\n\nNote that roles are instance-wide object, which means that they exist\nindependent of branches and their schemas. This means that role's permissions\napply to all branches.\n\nRoles that are qualified as *superuser* are implicitly granted\n:ref:`all permissions<ref_datamodel_permissions_superuser>`.\n\nBuilt-in permissions\n====================\n\n.. _ref_datamodel_permissions_built_in:\n\n:eql:synopsis:`sys::perm::data_modification`\n    Required for using ``insert``, ``update`` or ``delete`` statements.\n\n:eql:synopsis:`sys::perm::ddl`\n    Required for modification of schema. This includes applying migrations,\n    and issuing bare DDL commands (e.g. ``create type Post;``).\n\n    It does not include global instance commands, such as ``create branch``\n    or ``create role``. These are only allowed to *superuser* roles.\n\n:eql:synopsis:`sys::perm::branch_config`\n    Required for issuing ``configure current branch``.\n\n:eql:synopsis:`sys::perm::sql_session_config`\n    Required for issuing ``SET`` and ``RESET`` SQL commands.\n\n:eql:synopsis:`sys::perm::analyze`\n    Required for issuing ``analyze ...`` queries.\n\n:eql:synopsis:`sys::perm::query_stats_read`\n    Required for reading ``sys::QueryStats``.\n\n:eql:synopsis:`sys::perm::approximate_count`\n    Required for accessing ``sys::approximate_count()``.\n\n\n:eql:synopsis:`cfg::perm::configure_timeout`\n    Required for setting various timeouts, for example\n    ``session_idle_transaction_timeout`` and ``query_execution_timeout``.\n\n:eql:synopsis:`cfg::perm::configure_apply_access_policies`\n    Required for disabling access policies.\n\n:eql:synopsis:`cfg::perm::configure_allow_user_specified_id`\n    Required for setting ``allow_user_specified_id``.\n\n\n:eql:synopsis:`std::net::perm::http_write`\n    Required for issuing HTTP requests.\n\n:eql:synopsis:`std::net::perm::http_read`\n    Required for reading status of issued HTTP requests and responses.\n\n\nPermissions for :ref:`auth <ref_guide_auth>` extension:\n\n:eql:synopsis:`ext::auth::perm::auth_read`\n\n:eql:synopsis:`ext::auth::perm::auth_write`\n\n:eql:synopsis:`ext::auth::perm::auth_read_user`\n\n\nPermissions for ``ai`` extension are described\nin :ref:`AI extension reference <ref_ai_extai_reference_permissions>`.\n\n\nCustom permissions\n==================\n\n.. _ref_datamodel_permissions_custom:\n\nCustom permissions can be defined in schema, to fit the security model of each\napplication.\n\n.. code-block:: sql\n\n    module default {\n        permission data_export;\n    }\n\nThese permissions can be assigned to roles, similar to built-in permissions:\n\n.. code-block:: edgeql\n\n    alter role warehouse {\n      set permissions := {default::data_export};\n    };\n\n.. note::\n\n    Role permissions are instance-wide.\n\n    If an unrelated branch defines ``default::data_export``, the ``warehouse``\n    role will receive it as well. This happens even if the unrelated branch\n    adds the permission after ``alter role``.\n\n    Additionally, a role may be given permissions which do not yet exist in\n    any schema. This is useful for creating roles before any schemas are\n    applied.\n\n\nTo check if the current database connection's role has a permission, use\n:ref:`global variable<ref_datamodel_globals>` with the same name\nas the permission. This global is a boolean and cannot be manually set.\n\n.. code-block:: edgeql\n\n    select global default::data_export;\n\n\nIn combination with access policies, permissions can be used to limit read or\nwrite access of any type:\n\n.. code-block:: sdl\n\n    type AuditLog {\n        property event: str;\n\n        access policy only_export_can_read\n            allow select\n            using (global data_export);\n\n        access policy anyone_can_insert\n            allow insert;\n    }\n\nIn this example, we have type ``AuditLog`` into which all roles are allowed to\ninsert new log entries. But reading is allowed only to roles that posses\n``data_export`` permission (or are qualified as a *superuser*).\n\n\nCommon patterns\n===============\n\n\nPublic readonly database\n------------------------\n\nGel server can be exposed to public internet, with clients connecting directy\nfrom browsers. Let's assume that only want to grant read access to the public\nbrowser client.\n\nIn such scenarios, it is recommended to create a separate role\nthat will be used by the JavaScript client (e.g. ``webapp``) and not grant it\nany permissions.\n\nThis way, it will not be able to issue ``DROP TYPE`` or ``DELETE`` commands,\nbut will be able to read all data in the database. More importantly, it will\nnot be able to configure ``apply_access_policies`` to ``false`` to bypass\nour restrictions.\n\nIf we want to limit that access further, for example limit read access to type\n``Secrets``, we can use such schema:\n\n.. code-block:: sdl\n\n    permission server_access;\n\n    type Secret {\n        access policy all_access\n            allow select, insert, update, delete\n            using (global server_access);\n    };\n\n\nBecause ``webapp`` role will not possess permission ``server_access`` it will\nnot be able to read (or modify) ``Secret``. For other, trusted clients, which\nshould be able to access ``Secrets``, we have use *superuser* role, or some\nother role with ``server_access`` permission:\n\n.. code-block:: edgeql\n\n    create role api_server {\n        set password := 'strong_password';\n        set permissions := {sys::perm::dml, default::server_access};\n    };\n\n\nPublic partially writable database\n----------------------------------\n\nA similar example to the previous one is a public database, with a JavaScript\nclient that needs write access to some, but not all, object types.\n\nIn such scenarios, it is recommended to create a separate role for it\n(e.g. ``webapp``) and assign it ``sys::perm::ddl`` permission.\n\nSuch role will be able to connect to the database, read all data and modify\nall types. For obvious reasons, this is undesirable, since client credentials\ncould be extracted and used to delete all data in the database.\n\nTo further limit access, the access policies must be used on\nevery object:\n\n.. code-block:: sdl\n\n    permission server_access;\n\n    type Posts {  # read-only\n        access policy everyone_can_read allow select using (true);\n        access policy server_can_do_everything\n            allow select, insert, update, delete\n            using (global server_access);\n    }\n\n    type Events {  # insert-only\n        access policy everyone_can_insert allow insert using (true);\n        access policy server_can_do_everything\n            allow select, insert, update, delete\n            using (global server_access);\n    }\n\n    type Secrets {  # no access\n        access policy server_can_do_everything\n            allow select, insert, update, delete\n            using (global server_access);\n    };\n\n\nAgain, we can then use superuser role for server to fully access the database,\nor setup a separate role with ``server_access`` permission.\n\n\nRestricting branches\n--------------------\n\nTo control access by branches instead of by object type, we can use\n``Role.branches`` setting.\n\nFor example, let's assume we have an instance with ``staging`` and ``prod``\nbranches. We want the role ``dev`` to have full access to ``staging``, but not\n``prod``.\n\n.. code-block:: edgeql\n\n    create role dev {\n        set password := 'strong_password';\n        set branches := {'staging'};\n    };\n\nFor more about this, see :ref:`Roles <ref_admin_roles>`. \n\n\nSuperuser permissions\n=====================\n\n.. _ref_datamodel_permissions_superuser:\n\nRoles with *superuser* status are exempt from permission checks and have full\naccess over the instance.\n\nThis includes some commands that are not covered by any permission and are thus\nallowed *only* to *superuser* roles.\n\nThese commands include:\n\n* :eql:synopsis:`ROLE` commands\n\n* :eql:synopsis:`BRANCH` commands\n\n* :eql:synopsis:`EXTENSION PACKAGE` commands\n\n* :eql:synopsis:`CONFIGURE INSTANCE` command\n\n* :eql:synopsis:`DESCRIBE` command\n\n* :eql:synopsis:`ADMINISTER` command\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Schema > Access policies\n      <ref_datamodel_access_policies>`\n  * - :ref:`Running Gel > Administration > Roles <ref_admin_roles>`\n\n\n"
  },
  {
    "path": "docs/reference/datamodel/primitives.rst",
    "content": ".. _ref_datamodel_primitives:\n\n==========\nPrimitives\n==========\n\n|Gel| has a robust type system consisting of primitive and object types.\ntypes. Primitive types are used to declare *properties* on object types,\nas query and function arguments, as as well as in other contexts.\n\n.. _ref_datamodel_scalars:\n\nBuilt-in scalar types\n=====================\n\nGel comes with a range of built-in scalar types, such as:\n\n* String: :eql:type:`str`\n* Boolean: :eql:type:`bool`\n* Various numeric types: :eql:type:`int16`, :eql:type:`int32`,\n  :eql:type:`int64`, :eql:type:`float32`, :eql:type:`float64`, :eql:type:`bigint`, :eql:type:`decimal`\n* JSON: :eql:type:`json`,\n* UUID: :eql:type:`uuid`,\n* Date/time: :eql:type:`datetime`, :eql:type:`duration`\n  :eql:type:`cal::local_datetime`, :eql:type:`cal::local_date`,\n  :eql:type:`cal::local_time`, :eql:type:`cal::relative_duration`,\n  :eql:type:`cal::date_duration`\n* Miscellaneous: :eql:type:`sequence`, :eql:type:`bytes`, etc.\n\nCustom scalars\n==============\n\nYou can extend built-in scalars with additional constraints or annotations.\nHere's an example of a non-negative custom ``int64`` variant:\n\n.. code-block:: sdl\n\n    scalar type posint64 extending int64 {\n        constraint min_value(0);\n    }\n\n.. _ref_datamodel_enums:\n\nEnums\n=====\n\nEnum types are created by extending the abstract :eql:type:`enum` type, e.g.:\n\n.. code-block:: sdl\n\n  scalar type Color extending enum<Red, Green, Blue>;\n\n  type Shirt {\n    color: Color;\n  }\n\nwhich can be queries with:\n\n.. code-block:: edgeql\n\n  select Shirt filter .color = Color.Red;\n\nFor a full reference on enum types, see the :ref:`Enum docs <ref_std_enum>`.\n\n.. _ref_datamodel_arrays:\n\nArrays\n======\n\nArrays store zero or more primitive values of the same type in an ordered list.\nArrays cannot contain object types or other arrays, but can contain virtually\nany other type.\n\n.. code-block:: sdl\n\n  type Person {\n    str_array: array<str>;\n    json_array: array<json>;\n    tuple_array: array<tuple<float32, float32>>;\n\n    # INVALID: arrays of object types not allowed:\n    # friends: array<Person>\n\n    # INVALID: arrays cannot be nested:\n    # nested_array: array<array<str>>\n\n    # VALID: arrays can contain tuples with arrays in them\n    nested_array_via_tuple: array<tuple<array<str>>>\n  }\n\nArray syntax in EdgeQL is very intuitive (indexing starts at ``0``):\n\n.. code-block:: edgeql\n\n  select [1, 2, 3];\n  select [1, 2, 3][1] = 2;  # true\n\nFor a full reference on array types, see the :ref:`Array docs <ref_std_array>`.\n\n.. _ref_datamodel_tuples:\n\nTuples\n======\n\nLike arrays, tuples are ordered sequences of primitive data. Unlike arrays,\neach element of a tuple can have a distinct type. Tuple elements can be *any\ntype*, including primitives, objects, arrays, and other tuples.\n\n.. code-block:: sdl\n\n  type Person {\n    unnamed_tuple: tuple<str, bool, int64>;\n    nested_tuple: tuple<tuple<str, tuple<bool, int64>>>;\n    tuple_of_arrays: tuple<array<str>, array<int64>>;\n  }\n\nOptionally, you can assign a *key* to each element of the tuple. Tuples\ncontaining explicit keys are known as *named tuples*. You must assign keys to\nall elements (or none of them).\n\n.. code-block:: sdl\n\n  type BlogPost {\n    metadata: tuple<title: str, published: bool, upvotes: int64>;\n  }\n\nNamed and unnamed tuples are the same data structure under the hood. You can\nadd, remove, and change keys in a tuple type after it's been declared. For\ndetails, see :ref:`Tuples <ref_eql_literal_tuple>`.\n\n.. note::\n\n  When you query an *unnamed* tuple using one of EdgeQL's\n  :ref:`client libraries <ref_clients_index>`, its value is converted to a\n  list/array. When you fetch a named tuple, it is converted into an\n  object/dictionary/hashmap depending on the language.\n\n.. _ref_datamodel_ranges:\n\nRanges\n======\n\nRanges represent some interval of values. The intervals can be bound or\nunbound on either end. They can also be empty, containing no values. Only\nsome scalar types have corresponding range types:\n\n- Numeric ranges: ``range<int32>``, ``range<int64>``, ``range<float32>``,\n  ``range<float64>``, ``range<decimal>``\n- Date/time ranges: ``range<datetime>``, ``range<cal::local_datetime>``,\n  ``range<cal::local_date>``\n\nExample:\n\n.. code-block:: sdl\n\n  type DieRoll {\n    values: range<int64>;\n  }\n\nFor a full reference on ranges, functions and operators see the\n:ref:`Range docs <ref_std_range>`.\n\nSequences\n=========\n\nTo represent an auto-incrementing integer property, declare a custom scalar\nthat extends the abstract ``sequence`` type. Creating a sequence type\ninitializes a global ``int64`` counter that auto-increments whenever a new\nobject is created. All properties that point to the same sequence type will\nshare the counter.\n\n.. code-block:: sdl\n\n  scalar type ticket_number extending sequence;\n  type Ticket {\n    number: ticket_number;\n    rendered_number := 'TICKET-\\(.number)';\n  }\n\nFor a full reference on sequences, see the :ref:`Sequence docs <ref_std_sequence>`.\n\n.. _ref_eql_sdl_scalars:\n.. _ref_eql_sdl_scalars_syntax:\n\nDeclaring scalars\n=================\n\nThis section describes the syntax to declare a custom scalar type in your\nschema.\n\n\nSyntax\n------\n\n.. sdl:synopsis::\n\n  [abstract] scalar type <TypeName> [extending <supertype> [, ...] ]\n  [ \"{\"\n      [ <annotation-declarations> ]\n      [ <constraint-declarations> ]\n      ...\n    \"}\" ]\n\nDescription\n^^^^^^^^^^^\n\nThis declaration defines a new object type with the following options:\n\n:eql:synopsis:`abstract`\n    If specified, the created scalar type will be *abstract*.\n\n:eql:synopsis:`<TypeName>`\n    The name (optionally module-qualified) of the new scalar type.\n\n:eql:synopsis:`extending <supertype>`\n    Optional clause specifying the *supertype* of the new type.\n\n    If :eql:synopsis:`<supertype>` is an\n    :eql:type:`enumerated type <std::enum>` declaration then\n    an enumerated scalar type is defined.\n\n    Use of ``extending`` creates a persistent type relationship\n    between the new subtype and its supertype(s).  Schema modifications\n    to the supertype(s) propagate to the subtype.\n\nThe valid SDL sub-declarations are listed below:\n\n:sdl:synopsis:`<annotation-declarations>`\n    Set scalar type :ref:`annotation <ref_eql_sdl_annotations>`\n    to a given *value*.\n\n:sdl:synopsis:`<constraint-declarations>`\n    Define a concrete :ref:`constraint <ref_eql_sdl_constraints>` for\n    this scalar type.\n\n\n.. _ref_eql_ddl_scalars:\n\nDDL commands\n============\n\nThis section describes the low-level DDL commands for creating, altering, and\ndropping scalar types. You typically don't need to use these commands directly,\nbut knowing about them is useful for reviewing migrations.\n\nCreate scalar\n-------------\n\n:eql-statement:\n:eql-haswith:\n\nDefine a new scalar type.\n\n.. eql:synopsis::\n\n  [ with <with-item> [, ...] ]\n  create [abstract] scalar type <name> [ extending <supertype> ]\n  [ \"{\" <subcommand>; [...] \"}\" ] ;\n\n  # where <subcommand> is one of\n\n    create annotation <annotation-name> := <value>\n    create constraint <constraint-name> ...\n\nDescription\n^^^^^^^^^^^\n\nThe command ``create scalar type`` defines a new scalar type for use in the\ncurrent |branch|.\n\nIf *name* is qualified with a module name, then the type is created\nin that module, otherwise it is created in the current module.\nThe type name must be distinct from that of any existing schema item\nin the module.\n\nIf the ``abstract`` keyword is specified, the created type will be\n*abstract*.\n\nAll non-abstract scalar types must have an underlying core\nimplementation. For user-defined scalar types this means that\n``create scalar type`` must have another non-abstract scalar type\nas its *supertype*.\n\nThe most common use of ``create scalar type`` is to define a scalar\nsubtype with constraints.\n\nMost sub-commands and options of this command are identical to the\n:ref:`SDL scalar type declaration <ref_eql_sdl_scalars_syntax>`. The\nfollowing subcommands are allowed in the ``create scalar type`` block:\n\n:eql:synopsis:`create annotation <annotation-name> := <value>;`\n    Set scalar type's :eql:synopsis:`<annotation-name>` to\n    :eql:synopsis:`<value>`.\n\n    See :eql:stmt:`create annotation` for details.\n\n:eql:synopsis:`create constraint <constraint-name> ...`\n    Define a new constraint for this scalar type.  See\n    :eql:stmt:`create constraint` for details.\n\n\nExamples\n^^^^^^^^\n\nCreate a new non-negative integer type:\n\n.. code-block:: edgeql\n\n  create scalar type posint64 extending int64 {\n      create constraint min_value(0);\n  };\n\nCreate a new enumerated type:\n\n.. code-block:: edgeql\n\n  create scalar type Color\n      extending enum<Black, White, Red>;\n\n\nAlter scalar\n------------\n\n:eql-statement:\n:eql-haswith:\n\nAlter the definition of a scalar type.\n\n.. eql:synopsis::\n\n  [ with <with-item> [, ...] ]\n  alter scalar type <name>\n  \"{\" <subcommand>; [...] \"}\" ;\n\n  # where <subcommand> is one of\n\n    rename to <newname>\n    extending ...\n    create annotation <annotation-name> := <value>\n    alter annotation <annotation-name> := <value>\n    drop annotation <annotation-name>\n    create constraint <constraint-name> ...\n    alter constraint <constraint-name> ...\n    drop constraint <constraint-name> ...\n\nDescription\n^^^^^^^^^^^\n\nThe command ``alter scalar type`` changes the definition of a scalar type.\n*name* must be a name of an existing scalar type, optionally qualified\nwith a module name.\n\nThe following subcommands are allowed in the ``alter scalar type`` block:\n\n:eql:synopsis:`rename to <newname>;`\n    Change the name of the scalar type to *newname*.\n\n:eql:synopsis:`extending ...`\n    Alter the supertype list. It works the same way as in\n    :eql:stmt:`alter type`.\n\n:eql:synopsis:`alter annotation <annotation-name>;`\n    Alter scalar type :eql:synopsis:`<annotation-name>`.\n    See :eql:stmt:`alter annotation` for details.\n\n:eql:synopsis:`drop annotation <annotation-name>`\n    Remove scalar type's :eql:synopsis:`<annotation-name>` from\n    :eql:synopsis:`<value>`.\n    See :eql:stmt:`drop annotation` for details.\n\n:eql:synopsis:`alter constraint <constraint-name> ...`\n    Alter the definition of a constraint for this scalar type. See\n    :eql:stmt:`alter constraint` for details.\n\n:eql:synopsis:`drop constraint <constraint-name>`\n    Remove a constraint from this scalar type. See\n    :eql:stmt:`drop constraint` for details.\n\nAll the subcommands allowed in the ``create scalar type`` block are also\nvalid subcommands for ``alter scalar type`` block.\n\n\nExamples\n^^^^^^^^\n\nDefine a new constraint on a scalar type:\n\n.. code-block:: edgeql\n\n  alter scalar type posint64 {\n      create constraint max_value(100);\n  };\n\nAdd one more label to an enumerated type:\n\n.. code-block:: edgeql\n\n  alter scalar type Color\n      extending enum<Black, White, Red, Green>;\n\n\nDrop scalar\n-----------\n\n:eql-statement:\n:eql-haswith:\n\nRemove a scalar type.\n\n.. eql:synopsis::\n\n  [ with <with-item> [, ...] ]\n  drop scalar type <name> ;\n\nDescription\n^^^^^^^^^^^\n\nThe command ``drop scalar type`` removes a scalar type.\n\nParameters\n^^^^^^^^^^\n\n*name*\n    The name (optionally qualified with a module name) of an existing\n    scalar type.\n\nExample\n^^^^^^^\n\nRemove a scalar type:\n\n.. code-block:: edgeql\n\n  drop scalar type posint64;\n"
  },
  {
    "path": "docs/reference/datamodel/properties.rst",
    "content": ".. _ref_datamodel_props:\n\n==========\nProperties\n==========\n\n.. index:: property, primitive types, fields, columns\n\nProperties are used to associate primitive data with an :ref:`object type <ref_datamodel_object_types>` or :ref:`link <ref_datamodel_link_properties>`.\n\n.. code-block:: sdl\n\n    type Player {\n      property email: str;\n      points: int64;\n      is_online: bool;\n    }\n\nProperties are associated with a *name* (e.g. ``email``) and a primitive\ntype (e.g. ``str``).\n\nThe term *primitive type* is an umbrella term that\nencompasses :ref:`scalar types <ref_datamodel_scalars>` like ``str``,\n:ref:`arrays <ref_datamodel_arrays>` and :ref:`tuples <ref_datamodel_tuples>`,\n:ref:`and more <ref_datamodel_primitives>`.\n\nProperties can be declared using the ``property`` keyword if that improves\nreadability, or it can be ommitted.\n\n\nRequired properties\n===================\n\n.. index:: not null\n.. api-index:: required, optional\n\nProperties can be either ``optional`` (the default) or ``required``.\n\nE.g. here we have a ``User`` type that's guaranteed to have an ``email``,\nbut ``name`` is optional and can be empty:\n\n.. code-block:: sdl\n\n    type User {\n      required email: str;\n      optional name: str;\n    }\n\nSince ``optional`` keyword is the default, we can omit it:\n\n.. code-block:: sdl\n\n    type User {\n      required email: str;\n      name: str;\n    }\n\n.. _ref_datamodel_props_cardinality:\n\nCardinality\n===========\n\n.. api-index:: single, multi\n\nProperties have a **cardinality**:\n\n* ``prop: type``, short for ``single prop: type``, can either hold zero or\n  one value (that's the default).\n\n* ``multi prop: type`` can hold an *unordered set* of values, which can\n  be zero, one, or more values of type ``type``.\n\nFor example:\n\n.. code-block:: sdl\n\n    type User {\n\n      # \"single\" keyword isn't necessary here:\n      # properties are single by default\n      single name: str;\n\n      # an unordered set of strings\n      multi nicknames: str;\n\n      # an unordered set of string arrays\n      multi set_of_arrays: array<str>;\n    }\n\nmulti vs. arrays\n================\n\n``multi`` properties are stored differently than arrays under the hood.\nEssentially they are stored in a separate table ``(owner_id, value)``.\n\n.. rubric:: Pros of multi properties vs. arrays\n\n* ``multi`` properties allow efficient search and mutation of large sets.\n  Arrays are much slower for those operations.\n\n* ``multi`` properties can have indexes and constraints appied to\n  individual elements; arrays, in general, cannot.\n\n* It's easier to aggregate sets and operate on them than on arrays.\n  In many cases arrays would require :ref:`unpacking them into a set\n  <ref_eql_set_array_conversion>` first.\n\n.. rubric:: Cons of multi properties vs. arrays\n\n* On small sets, arrays are faster to retrieve.\n\n* It's easier to retain the original order in arrays. Arrays are ordered,\n  but sets are not.\n\n\n.. _ref_datamodel_props_default_values:\n\nDefault values\n==============\n\n.. api-index:: default\n\nProperties can have a default value. This default can be a static value or an\narbitrary EdgeQL expression, which will be evaluated upon insertion.\n\n.. code-block:: sdl\n\n    type Player {\n      required points: int64 {\n        default := 0;\n      }\n\n      required latitude: float64 {\n        default := (360 * random() - 180);\n      }\n    }\n\n\nReadonly properties\n===================\n\n.. index:: immutable\n.. api-index:: readonly\n\nProperties can be marked as ``readonly``. In the example below, the\n``User.external_id`` property can be set at the time of creation but not\nmodified thereafter.\n\n.. code-block:: sdl\n\n    type User {\n      required external_id: uuid {\n        readonly := true;\n      }\n    }\n\n\nConstraints\n===========\n\n.. api-index:: constraint\n\nProperties can be augmented wth constraints. The example below showcases a\nsubset of Gel's built-in constraints.\n\n.. code-block:: sdl\n\n    type BlogPost {\n      title: str {\n        constraint exclusive; # all post titles must be unique\n        constraint min_len_value(8);\n        constraint max_len_value(30);\n        constraint regexp(r'^[A-Za-z0-9 ]+$');\n      }\n\n      status: str {\n        constraint one_of('Draft', 'InReview', 'Published');\n      }\n\n      upvotes: int64 {\n        constraint min_value(0);\n        constraint max_value(9999);\n      }\n    }\n\nYou can constrain properties with arbitrary :ref:`EdgeQL <ref_edgeql>` expressions\nreturning ``bool``. To reference the value of the property, use the special scope\nkeyword ``__subject__``.\n\n.. code-block:: sdl\n\n    type BlogPost {\n      title: str {\n        constraint expression on (\n          __subject__ = str_trim(__subject__)\n        );\n      }\n    }\n\nThe constraint above guarantees that ``BlogPost.title`` doesn't contain any\nleading or trailing whitespace by checking that the raw string is equal to the\ntrimmed version. It uses the built-in :eql:func:`str_trim` function.\n\nFor a full reference of built-in constraints, see the :ref:`Constraints\nreference <ref_std_constraints>`.\n\n\nAnnotations\n===========\n\n.. index:: metadata\n\nProperties can contain annotations, small human-readable notes. The built-in\nannotations are ``title``, ``description``, and ``deprecated``. You may also\ndeclare :ref:`custom annotation types <ref_datamodel_inheritance_annotations>`.\n\n.. code-block:: sdl\n\n    type User {\n      email: str {\n        annotation title := 'Email address';\n      }\n    }\n\n\nAbstract properties\n===================\n\n.. api-index:: abstract property\n\nProperties can be *concrete* (the default) or *abstract*. Abstract properties\nare declared independent of a source or target, can contain :ref:`annotations\n<ref_datamodel_annotations>`, constraints, indexes, and can be marked as\n``readonly``.\n\n.. code-block:: sdl\n\n    abstract property email_prop {\n      annotation title := 'An email address';\n      readonly := true;\n    }\n\n    type Student {\n      # inherits annotations and \"readonly := true\"\n      email: str {\n        extending email_prop;\n      };\n    }\n\nOverloading properties\n======================\n\nAny time we want to amend an inherited property (e.g. to add a constraint),\nthe ``overloaded`` keyword must be used. This is to prevent unintentional\noverloading due to a name clash:\n\n.. code-block:: sdl\n\n    abstract type Named {\n        optional name: str;\n    }\n\n    type User extending Named {\n        # make \"name\" required\n        overloaded required name: str;\n    }\n\n\n.. _ref_eql_sdl_props:\n.. _ref_eql_sdl_props_syntax:\n\nDeclaring properties\n====================\n\nSyntax\n------\n\nThis section describes the syntax to declare properties in your schema.\n\n.. sdl:synopsis::\n\n    # Concrete property form used inside type declaration:\n    [ overloaded ] [{required | optional}] [{single | multi}]\n      [ property ] <name> : <type>\n      [ \"{\"\n          [ extending <base> [, ...] ; ]\n          [ default := <expression> ; ]\n          [ readonly := {true | false} ; ]\n          [ <annotation-declarations> ]\n          [ <constraint-declarations> ]\n          ...\n        \"}\" ]\n\n    # Computed property form used inside type declaration:\n    [{required | optional}] [{single | multi}]\n      [ property ] <name> := <expression>;\n\n    # Computed property form used inside type declaration (extended):\n    [ overloaded ] [{required | optional}] [{single | multi}]\n      property <name> [: <type>]\n      [ \"{\"\n          using (<expression>) ;\n          [ extending <base> [, ...] ; ]\n          [ <annotation-declarations> ]\n          [ <constraint-declarations> ]\n          ...\n        \"}\" ]\n\n    # Abstract property form:\n    abstract property [<module>::]<name>\n    [ \"{\"\n        [extending <base> [, ...] ; ]\n        [ readonly := {true | false} ; ]\n        [ <annotation-declarations> ]\n        ...\n      \"}\" ]\n\n\nDescription\n^^^^^^^^^^^\n\nThere are several forms of ``property`` declaration, as shown in the\nsyntax synopsis above. The first form is the canonical definition\nform, the second and third forms are used for defining a\n:ref:`computed property <ref_datamodel_computed>`, and the last\none is a form to define an ``abstract property``.\n\nThe abstract form allows declaring the property directly inside\na :ref:`module <ref_eql_sdl_modules>`.\n\nConcrete property forms are always used as sub-declarations\nfor an :ref:`object type <ref_eql_sdl_object_types>` or\na :ref:`link <ref_eql_sdl_links>`.\n\nThe following options are available:\n\n:eql:synopsis:`overloaded`\n    If specified, indicates that the property is inherited and that some\n    feature of it may be altered in the current object type.  It is an\n    error to declare a property as *overloaded* if it is not inherited.\n\n:eql:synopsis:`required`\n    If specified, the property is considered *required* for the parent\n    object type.  It is an error for an object to have a required\n    property resolve to an empty value.  Child properties **always**\n    inherit the *required* attribute, i.e it is not possible to make a\n    required property non-required by extending it.\n\n:eql:synopsis:`optional`\n    This is the default qualifier assumed when no qualifier is\n    specified, but it can also be specified explicitly. The property\n    is considered *optional* for the parent object type, i.e. it is\n    possible for the property to resolve to an empty value.\n\n:eql:synopsis:`multi`\n    Specifies that there may be more than one instance of this\n    property in an object, in other words, ``Object.property`` may\n    resolve to a set of a size greater than one.\n\n:eql:synopsis:`single`\n    Specifies that there may be at most *one* instance of this\n    property in an object, in other words, ``Object.property`` may\n    resolve to a set of a size not greater than one.  ``single`` is\n    assumed if nether ``multi`` nor ``single`` qualifier is specified.\n\n:eql:synopsis:`extending <base> [, ...]`\n    Optional clause specifying the *parents* of the new property item.\n\n    Use of ``extending`` creates a persistent schema relationship\n    between the new property and its parents.  Schema modifications\n    to the parent(s) propagate to the child.\n\n:eql:synopsis:`<type>`\n    The type must be a valid :ref:`type expression <ref_eql_types>`\n    denoting a non-abstract scalar or a container type.\n\nThe valid SDL sub-declarations are listed below:\n\n:eql:synopsis:`default := <expression>`\n    Specifies the default value for the property as an EdgeQL expression.\n    The default value is used in an ``insert`` statement if an explicit\n    value for this property is not specified.\n\n    The expression must be :ref:`Stable <ref_reference_volatility>`.\n\n:eql:synopsis:`readonly := {true | false}`\n    If ``true``, the property is considered *read-only*.\n    Modifications of this property are prohibited once an object is\n    created.  All of the derived properties **must** preserve the\n    original *read-only* value.\n\n:sdl:synopsis:`<annotation-declarations>`\n    Set property :ref:`annotation <ref_eql_sdl_annotations>`\n    to a given *value*.\n\n:sdl:synopsis:`<constraint-declarations>`\n    Define a concrete :ref:`constraint <ref_eql_sdl_constraints>` on\n    the property.\n\n\n.. _ref_eql_ddl_props:\n\nDDL commands\n============\n\nThis section describes the low-level DDL commands for creating, altering, and\ndropping properties. You typically don't need to use these commands directly,\nbut knowing about them is useful for reviewing migrations.\n\n\n.. _ref_eql_ddl_props_syntax:\n\nCreate property\n---------------\n\n:eql-statement:\n:eql-haswith:\n\nDefine a new property.\n\n.. eql:synopsis::\n\n    [ with <with-item> [, ...] ]\n    {create|alter} {type|link} <SourceName> \"{\"\n      [ ... ]\n      create [{required | optional}] [{single | multi}]\n        property <name>\n        [ extending <base> [, ...] ] : <type>\n        [ \"{\" <subcommand>; [...] \"}\" ] ;\n      [ ... ]\n    \"}\"\n\n    # Computed property form:\n\n    [ with <with-item> [, ...] ]\n    {create|alter} {type|link} <SourceName> \"{\"\n      [ ... ]\n      create [{required | optional}] [{single | multi}]\n        property <name> := <expression>;\n      [ ... ]\n    \"}\"\n\n    # Abstract property form:\n\n    [ with <with-item> [, ...] ]\n    create abstract property [<module>::]<name> [extending <base> [, ...]]\n    [ \"{\" <subcommand>; [...] \"}\" ]\n\n    # where <subcommand> is one of\n\n      set default := <expression>\n      set readonly := {true | false}\n      create annotation <annotation-name> := <value>\n      create constraint <constraint-name> ...\n\nParameters\n^^^^^^^^^^\n\nMost sub-commands and options of this command are identical to the\n:ref:`SDL property declaration <ref_eql_sdl_props_syntax>`. The\nfollowing subcommands are allowed in the ``create property`` block:\n\n:eql:synopsis:`set default := <expression>`\n    Specifies the default value for the property as an EdgeQL expression.\n    Other than a slight syntactical difference this is the same as the\n    corresponding SDL declaration.\n\n:eql:synopsis:`set readonly := {true | false}`\n    Specifies whether the property is considered *read-only*. Other\n    than a slight syntactical difference this is the same as the\n    corresponding SDL declaration.\n\n:eql:synopsis:`create annotation <annotation-name> := <value>`\n    Set property :eql:synopsis:`<annotation-name>` to\n    :eql:synopsis:`<value>`.\n\n    See :eql:stmt:`create annotation` for details.\n\n:eql:synopsis:`create constraint`\n    Define a concrete constraint on the property.\n    See :eql:stmt:`create constraint` for details.\n\n\nExamples\n^^^^^^^^\n\nDefine a new link ``address`` on the ``User`` object type:\n\n.. code-block:: edgeql\n\n    alter type User {\n      create property address: str\n    };\n\nDefine a new :ref:`computed property <ref_datamodel_computed>`\n``number_of_connections`` on the ``User`` object type counting the\nnumber of interests:\n\n.. code-block:: edgeql\n\n    alter type User {\n      create property number_of_connections :=\n        count(.interests)\n    };\n\nDefine a new abstract link ``orderable`` with ``weight`` property:\n\n.. code-block:: edgeql\n\n    create abstract link orderable {\n      create property weight: std::int64\n    };\n\n\nAlter property\n--------------\n\n:eql-statement:\n:eql-haswith:\n\nChange the definition of a property.\n\n.. eql:synopsis::\n\n    [ with <with-item> [, ...] ]\n    {create | alter} {type | link} <source> \"{\"\n      [ ... ]\n      alter property <name>\n      [ \"{\" ] <subcommand>; [...] [ \"}\" ];\n      [ ... ]\n    \"}\"\n\n\n    [ with <with-item> [, ...] ]\n    alter abstract property [<module>::]<name>\n    [ \"{\" ] <subcommand>; [...] [ \"}\" ];\n\n    # where <subcommand> is one of\n\n      set default := <expression>\n      reset default\n      set readonly := {true | false}\n      reset readonly\n      rename to <newname>\n      extending ...\n      set required [using (<conversion-expr)]\n      set optional\n      reset optionality\n      set single [using (<conversion-expr)]\n      set multi\n      reset cardinality [using (<conversion-expr)]\n      set type <typename> [using (<conversion-expr)]\n      reset type\n      using (<computed-expr>)\n      create annotation <annotation-name> := <value>\n      alter annotation <annotation-name> := <value>\n      drop annotation <annotation-name>\n      create constraint <constraint-name> ...\n      alter constraint <constraint-name> ...\n      drop constraint <constraint-name> ...\n\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`<source>`\n    The name of an object type or link on which the property is defined.\n    May be optionally qualified with module.\n\n:eql:synopsis:`<name>`\n    The unqualified name of the property to modify.\n\n:eql:synopsis:`<module>`\n    Optional name of the module to create or alter the abstract property in.\n    If not specified, the current module is used.\n\nThe following subcommands are allowed in the ``alter link`` block:\n\n:eql:synopsis:`rename to <newname>`\n    Change the name of the property to :eql:synopsis:`<newname>`.\n    All concrete properties inheriting from this property are\n    also renamed.\n\n:eql:synopsis:`extending ...`\n    Alter the property parent list.  The full syntax of this subcommand is:\n\n    .. eql:synopsis::\n\n         extending <name> [, ...]\n           [ first | last | before <parent> | after <parent> ]\n\n    This subcommand makes the property a child of the specified list\n    of parent property items.  The requirements for the parent-child\n    relationship are the same as when creating a property.\n\n    It is possible to specify the position in the parent list\n    using the following optional keywords:\n\n    * ``first`` -- insert parent(s) at the beginning of the\n      parent list,\n    * ``last`` -- insert parent(s) at the end of the parent list,\n    * ``before <parent>`` -- insert parent(s) before an\n      existing *parent*,\n    * ``after <parent>`` -- insert parent(s) after an existing\n      *parent*.\n\n:eql:synopsis:`set required [using (<conversion-expr)]`\n    Make the property *required*.\n\n:eql:synopsis:`set optional`\n    Make the property no longer *required* (i.e. make it *optional*).\n\n:eql:synopsis:`reset optionality`\n    Reset the optionality of the property to the default value (``optional``),\n    or, if the property is inherited, to the value inherited from properties in\n    supertypes.\n\n:eql:synopsis:`set single [using (<conversion-expr)]`\n    Change the maximum cardinality of the property set to *one*.  Only\n    valid for concrete properties.\n\n:eql:synopsis:`set multi`\n    Change the maximum cardinality of the property set to\n    *greater than one*.  Only valid for concrete properties.\n\n:eql:synopsis:`reset cardinality [using (<conversion-expr)]`\n    Reset the maximum cardinality of the property to the default value\n    (``single``), or, if the property is inherited, to the value inherited\n    from properties in supertypes.\n\n:eql:synopsis:`set type <typename> [using (<conversion-expr)]`\n    Change the type of the property to the specified\n    :eql:synopsis:`<typename>`.  The optional ``using`` clause specifies\n    a conversion expression that computes the new property value from the old.\n    The conversion expression must return a singleton set and is evaluated\n    on each element of ``multi`` properties.  A ``using`` clause must be\n    provided if there is no implicit or assignment cast from old to new type.\n\n:eql:synopsis:`reset type`\n    Reset the type of the property to the type inherited from properties\n    of the same name in supertypes.  It is an error to ``reset type`` on\n    a property that is not inherited.\n\n:eql:synopsis:`using (<computed-expr>)`\n    Change the expression of a :ref:`computed property\n    <ref_datamodel_computed>`.  Only valid for concrete properties.\n\n:eql:synopsis:`alter annotation <annotation-name>;`\n    Alter property annotation :eql:synopsis:`<annotation-name>`.\n    See :eql:stmt:`alter annotation` for details.\n\n:eql:synopsis:`drop annotation <annotation-name>;`\n    Remove property annotation :eql:synopsis:`<annotation-name>`.\n    See :eql:stmt:`drop annotation` for details.\n\n:eql:synopsis:`alter constraint <constraint-name> ...`\n    Alter the definition of a constraint for this property.  See\n    :eql:stmt:`alter constraint` for details.\n\n:eql:synopsis:`drop constraint <constraint-name>;`\n    Remove a constraint from this property.  See\n    :eql:stmt:`drop constraint` for details.\n\n:eql:synopsis:`reset default`\n    Remove the default value from this property, or reset it to the value\n    inherited from a supertype, if the property is inherited.\n\n:eql:synopsis:`reset readonly`\n    Set property writability to the default value (writable), or, if the\n    property is inherited, to the value inherited from properties in\n    supertypes.\n\nAll the subcommands allowed in the ``create property`` block are also\nvalid subcommands for ``alter property`` block.\n\nExamples\n^^^^^^^^\n\nSet the ``title`` annotation of property ``address`` of object type\n``User`` to ``\"Home address\"``:\n\n.. code-block:: edgeql\n\n    alter type User {\n      alter property address\n        create annotation title := \"Home address\";\n    };\n\nAdd a maximum-length constraint to property ``address`` of object type\n``User``:\n\n.. code-block:: edgeql\n\n    alter type User {\n      alter property address {\n        create constraint max_len_value(500);\n      };\n    };\n\nRename the property ``weight`` of link ``orderable`` to ``sort_by``:\n\n.. code-block:: edgeql\n\n    alter abstract link orderable {\n      alter property weight rename to sort_by;\n    };\n\nRedefine the :ref:`computed property <ref_datamodel_computed>`\n``number_of_connections`` to be the number of friends:\n\n.. code-block:: edgeql\n\n    alter type User {\n      alter property number_of_connections using (\n        count(.friends)\n      )\n    };\n\n\nDrop property\n-------------\n\n:eql-statement:\n:eql-haswith:\n\nRemove a property from the schema.\n\n.. eql:synopsis::\n\n    [ with <with-item> [, ...] ]\n    {create|alter} type <TypeName> \"{\"\n      [ ... ]\n      drop link <name>\n      [ ... ]\n    \"}\"\n\n\n    [ with <with-item> [, ...] ]\n    drop abstract property <name> ;\n\n\nExample\n^^^^^^^\n\nRemove property ``address`` from type ``User``:\n\n.. code-block:: edgeql\n\n    alter type User {\n      drop property address;\n    };\n"
  },
  {
    "path": "docs/reference/datamodel/triggers.rst",
    "content": ".. _ref_datamodel_triggers:\n.. _ref_eql_sdl_triggers:\n\n========\nTriggers\n========\n\nTriggers allow you to define an expression to be executed whenever a given\nquery type is run on an object type. The original query will *trigger* your\npre-defined expression to run in a transaction along with the original query.\nThese can be defined in your schema.\n\n\nImportant notes\n===============\n\nTriggers are an advanced feature and have some caveats that\nyou should be aware of.\n\nConsider using mutation rewrites\n--------------------------------\n\nTriggers cannot be used to *modify* the object that set off the trigger,\nalthough they can be used with :eql:func:`assert` to do *validation* on\nthat object. If you need to modify the object, you can use :ref:`mutation\nrewrites <ref_datamodel_mutation_rewrites>`.\n\nUnified trigger query execution\n-------------------------------\n\nAll queries within triggers, along with the initial triggering query, are\ncompiled into a single combined SQL query under the hood. Keep this in mind\nwhen designing triggers that modify existing records. If multiple ``update``\nqueries within your triggers target the same object, only one of these\nqueries will ultimately be executed. To ensure all desired updates on an\nobject are applied, consolidate them into a single ``update`` query within\none trigger, instead of distributing them across multiple updates.\n\nMulti-stage trigger execution\n-----------------------------\n\nIn some cases, a trigger can cause another trigger to fire. When this\nhappens, Gel completes all the triggers fired by the initial query\nbefore kicking off a new \"stage\" of triggers. In the second stage, any\ntriggers fired by the initial stage of triggers will fire. Gel will\ncontinue adding trigger stages until all triggers are complete.\n\nThe exception to this is when triggers would cause a loop or would cause\nthe same trigger to be run in two different stages. These triggers will\ngenerate an error.\n\nData visibility\n---------------\n\nAny query in your trigger will return the state of the database *after* the\ntriggering query. If this query's results include the object that flipped\nthe trigger, the results will contain that object in the same state as\n``__new__``.\n\n\nExample: audit log\n==================\n\nHere's an example that creates a simple **audit log** type so that we can keep\ntrack of what's happening to our users in a database. First, we will create a\n``Log`` type:\n\n.. code-block:: sdl\n\n   type Log {\n     action: str;\n     timestamp: datetime {\n       default := datetime_current();\n     }\n     target_name: str;\n     change: str;\n   }\n\nWith the ``Log`` type in place, we can write some triggers that will\nautomatically create ``Log`` objects for any insert, update, or delete queries\non the ``Person`` type:\n\n.. code-block:: sdl\n\n   type Person {\n     required name: str;\n\n     trigger log_insert after insert for each do (\n       insert Log {\n         action := 'insert',\n         target_name := __new__.name\n       }\n     );\n\n     trigger log_update after update for each do (\n       insert Log {\n         action := 'update',\n         target_name := __new__.name,\n         change := __old__.name ++ '->' ++ __new__.name\n       }\n     );\n\n     trigger log_delete after delete for each do (\n       insert Log {\n         action := 'delete',\n         target_name := __old__.name\n       }\n     );\n   }\n\nIn a trigger's expression, we have access to the ``__old__`` and/or ``__new__``\nvariables which capture the object before and after the query. Triggers on\n``update`` can use both variables. Triggers on ``delete`` can use ``__old__``.\nTriggers on ``insert`` can use ``__new__``.\n\nNow, whenever we run a query, we get a log entry as well:\n\n.. code-block:: edgeql-repl\n\n   db> insert Person {name := 'Jonathan Harker'};\n   {default::Person {id: b4d4e7e6-bd19-11ed-8363-1737d8d4c3c3}}\n   db> select Log {action, timestamp, target_name, change};\n   {\n     default::Log {\n       action: 'insert',\n       timestamp: <datetime>'2023-03-07T18:56:02.403817Z',\n       target_name: 'Jonathan Harker',\n       change: {}\n     }\n   }\n   db> update Person filter .name = 'Jonathan Harker'\n   ... set {name := 'Mina Murray'};\n   {default::Person {id: b4d4e7e6-bd19-11ed-8363-1737d8d4c3c3}}\n   db> select Log {action, timestamp, target_name, change};\n   {\n     default::Log {\n       action: 'insert',\n       timestamp: <datetime>'2023-03-07T18:56:02.403817Z',\n       target_name: 'Jonathan Harker',\n       change: {}\n     },\n     default::Log {\n       action: 'update',\n       timestamp: <datetime>'2023-03-07T18:56:39.520889Z',\n       target_name: 'Mina Murray',\n       change: 'Jonathan Harker->Mina Murray'\n     },\n   }\n   db> delete Person filter .name = 'Mina Murray';\n   {default::Person {id: b4d4e7e6-bd19-11ed-8363-1737d8d4c3c3}}\n   db> select Log {action, timestamp, target_name, change};\n   {\n     default::Log {\n       action: 'insert',\n       timestamp: <datetime>'2023-03-07T18:56:02.403817Z',\n       target_name: 'Jonathan Harker',\n       change: {}\n     },\n     default::Log {\n       action: 'update',\n       timestamp: <datetime>'2023-03-07T18:56:39.520889Z',\n       target_name: 'Mina Murray',\n       change: 'Jonathan Harker->Mina Murray'\n     },\n     default::Log {\n       action: 'delete',\n       timestamp: <datetime>'2023-03-07T19:00:52.636084Z',\n       target_name: 'Mina Murray',\n       change: {}\n     },\n   }\n\nOur audit logging works, but the update logs have a major shortcoming: they\nlog an update even when nothing changes. Any time an ``update`` query runs,\nwe get a log, even if the values are the same. We can prevent that by\nusing the trigger's ``when`` to run the trigger conditionally. Here's a\nrework of our ``update`` logging query:\n\n.. code-block:: sdl-invalid\n\n  trigger log_update after update for each\n  when (__old__.name != __new__.name)\n  do (\n    insert Log {\n      action := 'update',\n      target_name := __new__.name,\n      change := __old__.name ++ '->' ++ __new__.name\n    }\n  );\n\nIf this object were more complicated and we had many properties to compare,\nwe could use a ``json`` cast to compare them all in one shot:\n\n.. code-block:: sdl-invalid\n\n  trigger log_update after update for each\n  when (<json>__old__ {**} != <json>__new__ {**})\n  do (\n    insert Log {\n      action := 'update',\n      target_name := __new__.name,\n      change := __old__.name ++ '->' ++ __new__.name\n    }\n  );\n\nYou might find that one log entry per row is too granular or too noisy for your\nuse case. In that case, a ``for all`` trigger may be a better fit. Here's a\nschema that changes the ``Log`` type so that each object can log multiple\nwrites by making ``target_name`` and ``change`` :ref:`multi properties\n<ref_datamodel_props_cardinality>` and switches to ``for all`` triggers:\n\n.. code-block:: sdl-diff\n\n      type Log {\n        action: str;\n        timestamp: datetime {\n          default := datetime_current();\n        }\n    -    target_name: str;\n    -    change: str;\n    +    multi target_name: str;\n    +    multi change: str;\n      }\n\n      type Person {\n        required name: str;\n\n    -    trigger log_insert after insert for each do (\n    +    trigger log_insert after insert for all do (\n          insert Log {\n            action := 'insert',\n            target_name := __new__.name\n          }\n        );\n\n    -    trigger log_update after update for each do (\n    +    trigger log_update after update for all do (\n          insert Log {\n            action := 'update',\n            target_name := __new__.name,\n            change := __old__.name ++ '->' ++ __new__.name\n          }\n        );\n\n    -    trigger log_delete after delete for each do (\n    +    trigger log_delete after delete for all do (\n          insert Log {\n            action := 'delete',\n            target_name := __old__.name\n          }\n        );\n      }\n\nUnder this new schema, each query matching the trigger gets a single ``Log``\nobject instead of one ``Log`` object per row:\n\n.. code-block:: edgeql-repl\n\n   db> for name in {'Jonathan Harker', 'Mina Murray', 'Dracula'}\n   ... union (\n   ...   insert Person {name := name}\n   ... );\n   {\n     default::Person {id: 3836f9c8-d393-11ed-9638-3793d3a39133},\n     default::Person {id: 38370a8a-d393-11ed-9638-d3e9b92ca408},\n     default::Person {id: 38370abc-d393-11ed-9638-5390f3cbd375},\n   }\n   db> select Log {action, timestamp, target_name, change};\n   {\n     default::Log {\n       action: 'insert',\n       timestamp: <datetime>'2023-03-07T19:12:21.113521Z',\n       target_name: {'Jonathan Harker', 'Mina Murray', 'Dracula'},\n       change: {},\n     },\n   }\n   db> for change in {\n   ...   (old_name := 'Jonathan Harker', new_name := 'Jonathan'),\n   ...   (old_name := 'Mina Murray', new_name := 'Mina')\n   ... }\n   ... union (\n   ...   update Person filter .name = change.old_name set {\n   ...     name := change.new_name\n   ...   }\n   ... );\n   {\n     default::Person {id: 3836f9c8-d393-11ed-9638-3793d3a39133},\n     default::Person {id: 38370a8a-d393-11ed-9638-d3e9b92ca408},\n   }\n   db> select Log {action, timestamp, target_name, change};\n   {\n     default::Log {\n       action: 'insert',\n       timestamp: <datetime>'2023-04-05T09:21:17.514089Z',\n       target_name: {'Jonathan Harker', 'Mina Murray', 'Dracula'},\n       change: {},\n     },\n     default::Log {\n       action: 'update',\n       timestamp: <datetime>'2023-04-05T09:35:30.389571Z',\n       target_name: {'Jonathan', 'Mina'},\n       change: {'Jonathan Harker->Jonathan', 'Mina Murray->Mina'},\n     },\n   }\n\nExample: validation\n===================\n\n.. index:: trigger, validate, assert\n\nTriggers may also be used for validation by calling :eql:func:`assert` inside\nthe trigger. In this example, the ``Person`` type has two multi links to other\n``Person`` objects named ``friends`` and ``enemies``. These two links should be\nmutually exclusive, so we have written a trigger to make sure there are no\ncommon objects linked in both.\n\n.. code-block:: sdl\n\n   type Person {\n     required name: str;\n     multi friends: Person;\n     multi enemies: Person;\n\n     trigger prohibit_frenemies after insert, update for each do (\n       assert(\n         not exists (__new__.friends intersect __new__.enemies),\n         message := \"Invalid frenemies\",\n       )\n     )\n   }\n\nWith this trigger in place, it is impossible to link the same ``Person`` as\nboth a friend and an enemy of any other person.\n\n.. code-block:: edgeql-repl\n\n   db> insert Person {name := 'Quincey Morris'};\n   {default::Person {id: e4a55480-d2de-11ed-93bd-9f4224fc73af}}\n   db> insert Person {name := 'Dracula'};\n   {default::Person {id: e7f2cff0-d2de-11ed-93bd-279780478afb}}\n   db> update Person\n   ... filter .name = 'Quincey Morris'\n   ... set {\n   ...   enemies := (\n   ...     select detached Person filter .name = 'Dracula'\n   ...   )\n   ... };\n   {default::Person {id: e4a55480-d2de-11ed-93bd-9f4224fc73af}}\n   db> update Person\n   ... filter .name = 'Quincey Morris'\n   ... set {\n   ...   friends := (\n   ...     select detached Person filter .name = 'Dracula'\n   ...   )\n   ... };\n   gel error: GelError: Invalid frenemies\n\n\nExample: logging\n================\n\nDeclare a trigger that inserts a ``Log`` object for each new ``User`` object:\n\n.. code-block:: sdl\n\n   type User {\n     required name: str;\n\n     trigger log_insert after insert for each do (\n       insert Log {\n         action := 'insert',\n         target_name := __new__.name\n       }\n     );\n   }\n\nDeclare a trigger that inserts a ``Log`` object conditionally when an update\nquery makes a change to a ``User`` object:\n\n.. code-block:: sdl\n\n   type User {\n     required name: str;\n\n     trigger log_update after update for each\n     when (<json>__old__ {**} != <json>__new__ {**})\n     do (\n       insert Log {\n         action := 'update',\n         target_name := __new__.name,\n         change := __old__.name ++ '->' ++ __new__.name\n       }\n     );\n   }\n\n\n.. _ref_eql_sdl_triggers_syntax:\n\n\nDeclaring triggers\n==================\n\n.. api-index:: trigger, after insert, after update, after delete, for each,\n               for all, when, do, __new__, __old__\n\nThis section describes the syntax to declare a trigger in your schema.\n\nSyntax\n------\n\n.. sdl:synopsis::\n\n   type <type-name> \"{\"\n     trigger <name>\n     after\n       {insert | update | delete} [, ...]\n       for {each | all}\n       [ when (<condition>) ]\n       do <expr>\n   \"}\"\n\nDescription\n-----------\n\nThis declaration defines a new trigger with the following options:\n\n:eql:synopsis:`<type-name>`\n   The name (optionally module-qualified) of the type to be triggered on.\n\n:eql:synopsis:`<name>`\n   The name of the trigger.\n\n:eql:synopsis:`insert | update | delete [, ...]`\n   The query type (or types) to trigger on. Separate multiple values with\n   commas to invoke the same trigger for multiple types of queries.\n\n:eql:synopsis:`each`\n   The expression will be evaluated once per modified object. ``__new__`` and\n   ``__old__`` in this context within the expression will refer to a single\n   object.\n\n:eql:synopsis:`all`\n   The expression will be evaluted once for the entire query, even if multiple\n   objects were modified. ``__new__`` and ``__old__`` in this context within\n   the expression refer to sets of the modified objects.\n\n.. versionadded:: 4.0\n\n   :eql:synopsis:`when (<condition>)`\n      Optionally provide a condition for the trigger. If the condition is\n      met, the trigger will run. If not, the trigger is skipped.\n\n:eql:synopsis:`<expr>`\n   The expression to be evaluated when the trigger is invoked.\n\nThe trigger name must be distinct from that of any existing trigger\non the same type.\n\n\n.. _ref_eql_ddl_triggers:\n\nDDL commands\n============\n\nThis section describes the low-level DDL commands for creating and dropping\ntriggers. You typically don't need to use these commands directly, but\nknowing about them is useful for reviewing migrations.\n\n\nCreate trigger\n--------------\n\n:eql-statement:\n\n:ref:`Define <ref_eql_sdl_triggers>` a new trigger.\n\n.. eql:synopsis::\n\n   {create | alter} type <type-name> \"{\"\n     create trigger <name>\n       after\n       {insert | update | delete} [, ...]\n       for {each | all}\n       [ when (<condition>) ]\n       do <expr>\n   \"}\"\n\nDescription\n^^^^^^^^^^^\n\nThe command ``create trigger`` nested under ``create type`` or ``alter type``\ndefines a new trigger for a given object type.\n\nThe trigger name must be distinct from that of any existing trigger\non the same type.\n\nParameters\n^^^^^^^^^^\n\nThe options of this command are identical to the\n:ref:`SDL trigger declaration <ref_eql_sdl_triggers_syntax>`.\n\nExample\n^^^^^^^\n\nDeclare a trigger that inserts a ``Log`` object for each new ``User`` object:\n\n.. code-block:: edgeql\n\n   alter type User {\n     create trigger log_insert after insert for each do (\n       insert Log {\n         action := 'insert',\n         target_name := __new__.name\n       }\n     );\n   };\n\n.. versionadded:: 4.0\n\n   Declare a trigger that inserts a ``Log`` object conditionally when an update\n   query makes a change to a ``User`` object:\n\n   .. code-block:: edgeql\n\n      alter type User {\n        create trigger log_update after update for each\n        when (<json>__old__ {**} != <json>__new__ {**})\n        do (\n          insert Log {\n            action := 'update',\n            target_name := __new__.name,\n            change := __old__.name ++ '->' ++ __new__.name\n          }\n        );\n      }\n\nDrop trigger\n------------\n\n:eql-statement:\n\nRemove a trigger.\n\n.. eql:synopsis::\n\n   alter type <type-name> \"{\"\n     drop trigger <name>;\n   \"}\"\n\nDescription\n^^^^^^^^^^^\n\nThe command ``drop trigger`` inside an ``alter type`` block removes the\ndefinition of an existing trigger on the specified type.\n\nParameters\n^^^^^^^^^^\n\n:eql:synopsis:`<type-name>`\n   The name (optionally module-qualified) of the type being triggered on.\n\n:eql:synopsis:`<name>`\n   The name of the trigger.\n\nExample\n^^^^^^^\n\nRemove the ``log_insert`` trigger on the ``User`` type:\n\n.. code-block:: edgeql\n\n   alter type User {\n     drop trigger log_insert;\n   };\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Introspection > Triggers <ref_datamodel_introspection_triggers>`\n"
  },
  {
    "path": "docs/reference/edgeql/analyze.rst",
    "content": ".. _ref_eql_analyze:\n\nAnalyze\n=======\n\n.. index:: explain, performance, postgres query planner\n.. api-index:: analyze\n\nPrefix an EdgeQL query with ``analyze`` to run a performance analysis of that\nquery.\n\n.. code-block:: edgeql-repl\n\n  db> analyze select Hero {\n  ...   name,\n  ...   secret_identity,\n  ...   villains: {\n  ...     name,\n  ...     nemesis: {\n  ...       name\n  ...     }\n  ...   }\n  ... };\n  ──────────────────────────────────────── Query ────────────────────────────────────────\n  analyze select ➊  Hero {name, secret_identity, ➋  villains: {name, ➌  nemesis: {name}}};\n\n  ──────────────────────── Coarse-grained Query Plan ────────────────────────\n                    │ Time     Cost Loops Rows Width │ Relations\n  ➊ root            │  0.0 69709.48   1.0  0.0    32 │ Hero\n  ╰──➋ .villains    │  0.0     92.9   0.0  0.0    32 │ Villain, Hero.villains\n  ╰──➌ .nemesis     │  0.0     8.18   0.0  0.0    32 │ Hero\n\n\n.. note::\n\n    In addition to using the ``analyze`` statement in the CLI or UI's REPL, you\n    may also run performance analysis via our CLI's :ref:`analyze command\n    <ref_cli_gel_analyze>` and the UI's query builder (accessible by running\n    :ref:`ref_cli_gel_ui` to invoke your instance's UI) by prepending your\n    query with ``analyze``. This method offers helpful visualizations to to\n    make it easy to understand your query's performance.\n\nAfter analyzing a query, you may run the ``\\expand`` command in the REPL to see\nmore fine-grained performance metrics on the previously analyzed query.\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`CLI > gel analyze <ref_cli_gel_analyze>`\n  * - :ref:`Reference > EdgeQL > analyze <ref_eql_statements_analyze>`\n"
  },
  {
    "path": "docs/reference/edgeql/delete.rst",
    "content": ".. _ref_eql_delete:\n\nDelete\n======\n\n.. api-index:: delete\n\nThe ``delete`` command is used to delete objects from the database.\n\n.. code-block:: edgeql\n\n  delete Hero\n  filter .name = 'Iron Man';\n\nClauses\n-------\n\nDeletion statements support ``filter``, ``order by``, ``offset``, and ``limit``\nclauses. See :ref:`EdgeQL > Select <ref_eql_select>` for full documentation\non these clauses.\n\n.. code-block:: edgeql\n\n  delete Hero\n  filter .name ilike 'the %'\n  order by .name\n  offset 10\n  limit 5;\n\nLink deletion\n-------------\n\n.. api-index:: ConstraintViolationError\n\nEvery link is associated with a *link deletion policy*. By default, it isn't\npossible to delete an object linked to by another.\n\n.. code-block:: edgeql-repl\n\n  db> delete Hero filter .name = \"Yelena Belova\";\n  ConstraintViolationError: deletion of default::Hero\n  (af7076e0-3e98-11ec-abb3-b3435bbe7c7e) is prohibited by link target policy\n  {}\n\nThis deletion failed because Yelena is still in the ``characters`` list of\nthe Black Widow movie. We must destroy this link before Yelena can be\ndeleted.\n\n.. code-block:: edgeql-repl\n\n  db> update Movie\n  ... filter .title = \"Black Widow\"\n  ... set {\n  ...   characters -= (select Hero filter .name = \"Yelena Belova\")\n  ... };\n  {default::Movie {id: af706c7c-3e98-11ec-abb3-4bbf3f18a61a}}\n  db> delete Hero filter .name = \"Yelena Belova\";\n  {default::Hero {id: af7076e0-3e98-11ec-abb3-b3435bbe7c7e}}\n\nTo avoid this behavior, we could update the ``Movie.characters`` link to use\nthe ``allow`` deletion policy.\n\n.. code-block:: sdl-diff\n\n      type Movie {\n        required title: str { constraint exclusive };\n        required release_year: int64;\n    -   multi characters: Person;\n    +   multi characters: Person {\n    +     on target delete allow;\n    +   };\n      }\n\n\nCascading deletes\n^^^^^^^^^^^^^^^^^\n\n.. index:: deletion policy\n.. api-index:: delete source, delete target\n\nIf a link uses the ``delete source`` policy, then deleting a *target* of the\nlink will also delete the object that links to it (the *source*). This behavior\ncan be used to implement cascading deletes; be careful with this power!\n\nThe full list of deletion policies is documented at :ref:`Schema > Links\n<ref_datamodel_link_deletion>`.\n\nReturn value\n------------\n\n.. index:: returning\n\nA ``delete`` statement returns the set of deleted objects. You can pass this\nset into ``select`` to fetch properties and links of the (now-deleted)\nobjects. This is the last moment this data will be available before being\npermanently deleted.\n\n.. code-block:: edgeql-repl\n\n  db> with movie := (delete Movie filter .title = \"Untitled\")\n  ... select movie {id, title};\n  {default::Movie {\n    id: b11303c6-40ac-11ec-a77d-d393cdedde83,\n    title: 'Untitled',\n  }}\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Reference > Commands > Delete <ref_eql_statements_delete>`\n  * - :ref:`Cheatsheets > Deleting data <ref_cheatsheet_delete>`\n"
  },
  {
    "path": "docs/reference/edgeql/for.rst",
    "content": ".. _ref_eql_for:\n\nFor\n===\n\n.. api-index:: for in, union\n\nEdgeQL supports a top-level ``for`` statement. These \"for loops\" iterate over\neach element of some input set, execute some expression with it, and merge the\nresults into a single output set.\n\n.. code-block:: edgeql-repl\n\n  db> for number in {0, 1, 2, 3}\n  ... union (\n  ...   select { number, number + 0.5 }\n  ... );\n  {0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5}\n\nThis statement iterates through each number in the set. Inside the loop, the\n``number`` variable is bound to a singleton set. The inner expression is\nexecuted for every element of the input set, and the results of each execution\nare merged into a single output set.\n\n.. note::\n\n  The ``union`` keyword is required prior to |EdgeDB| 5.0 and is intended to\n  indicate explicitly that the results of each loop execution are ultimately\n  merged.\n\n.. versionadded: 5.0\n\n    If the body of ``for`` is a statement — ``select``, ``insert``, ``update``,\n    ``delete``, ``group``, or ``with`` — ``union`` and the parentheses\n    surrounding the statement are no longer required:\n\n    .. code-block:: edgeql-repl\n\n      db> for number in {0, 1, 2, 3}\n      ... select { number, number + 0.5 }\n      {0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5}\n\n\nBulk inserts\n------------\n\nThe ``for`` statement is commonly used for bulk inserts.\n\n.. code-block:: edgeql-repl\n\n  db> for hero_name in {'Cersi', 'Ikaris', 'Thena'}\n  ... union (\n  ...   insert Hero { name := hero_name }\n  ... );\n  {\n    default::Hero {id: d7d7e0f6-40ae-11ec-87b1-3f06bed494b9},\n    default::Hero {id: d7d7f870-40ae-11ec-87b1-f712a4efc3a5},\n    default::Hero {id: d7d7f8c0-40ae-11ec-87b1-6b8685d56610}\n  }\n\nThis statement iterates through each name in the list of names. Inside the\nloop, ``hero_name`` is bound to a ``str`` singleton, so it can be assigned to\n``Hero.name``.\n\nInstead of literal sets, it's common to use a :ref:`json <ref_std_json>`\nparameter for bulk inserts. This value is then \"unpacked\" into a set of\n``json`` elements and used inside the ``for`` loop:\n\n.. code-block:: edgeql-repl\n\n  db> with\n  ...   raw_data := <json>$data,\n  ... for item in json_array_unpack(raw_data) union (\n  ...   insert Hero { name := <str>item['name'] }\n  ... );\n  Parameter <json>$data: [{\"name\":\"Sersi\"},{\"name\":\"Ikaris\"},{\"name\":\"Thena\"}]\n  {\n    default::Hero {id: d7d7e0f6-40ae-11ec-87b1-3f06bed494b9},\n    default::Hero {id: d7d7f870-40ae-11ec-87b1-f712a4efc3a5},\n    default::Hero {id: d7d7f8c0-40ae-11ec-87b1-6b8685d56610}\n  }\n\n\nA similar approach can be used for bulk updates.\n\n\n.. _ref_eql_for_conditional_dml:\n\nConditional DML\n---------------\n\n.. api-index:: for, if else, unless conflict\n\n.. versionadded:: 4.0\n\n    DML is now supported in ``if..else``.\n\nDML (i.e., :ref:`insert <ref_eql_insert>`, :ref:`update <ref_eql_update>`,\n:ref:`delete <ref_eql_delete>`) is not supported in :eql:op:`if..else`. If you\nneed to do one of these conditionally, you can use a ``for`` loop as a\nworkaround. For example, you might want to write this conditional:\n\n.. code-block::\n\n    # 🚫 Does not work\n    with admin := (select User filter .role = 'admin')\n    select admin if exists admin\n      else (insert User {role := 'admin'});\n\nBecause of the lack of support for DML in a conditional, this query will fail.\nHere's how you can accomplish the same thing using the workaround:\n\n.. code-block:: edgeql\n\n    # ✅ Works!\n    with\n      admin := (select User filter .role = 'admin'),\n      new := (for _ in (select () filter not exists admin) union (\n        insert User {role := 'admin'}\n      )),\n    select {admin, new};\n\nThe ``admin`` alias represents the condition we want to test for. In this case,\n\"do we have a ``User`` object with a value of ``admin`` for the ``role``\nproperty?\" In the ``new`` alias, we write a ``for`` loop with a ``select``\nquery that will produce a set with a single value if that object we queried for\ndoes *not* exist. (You can use ``exists`` instead of ``not exists`` in the\nnested ``select`` inside the ``for`` loop if you don't want to invert the\ncondition.)\n\nA set with a single value results in a single iteration of the ``for`` loop.\nInside that loop, we run our conditional DML — in this case to insert an admin\nuser. Then we ``select`` both aliases to execute both of their queries. The\nquery will return the ``User`` object. This in effect gives us a query that\nwill insert a ``User`` object with a ``role`` of ``admin`` if none exists or\nreturn that object if it *does* exist.\n\n.. note::\n\n    If you're trying to conditionally run DML in response to a violation of an\n    exclusivity constraint, you don't need this workaround. You should use\n    :ref:`unless conflict <ref_eql_insert_conflicts>` instead.\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Reference > Commands > For <ref_eql_statements_for>`\n"
  },
  {
    "path": "docs/reference/edgeql/group.rst",
    "content": ".. _ref_eql_group:\n\nGroup\n=====\n\n.. index:: analytics, aggregate\n.. api-index:: group by, group using by, key, grouping, elements, rollup, cube\n\nEdgeQL supports a top-level ``group`` statement. This is used to partition\nsets into subsets based on some parameters. These subsets then can be\nadditionally aggregated to provide some analytics.\n\nThe most basic format is just using the bare :eql:stmt:`group` to group a set\nof objects by some property:\n\n.. code-block:: edgeql-repl\n\n    db> group Movie by .release_year;\n    {\n      {\n        key: {release_year: 2016},\n        grouping: {'release_year'},\n        elements: {\n          default::Movie {title: 'Captain America: Civil War'},\n          default::Movie {title: 'Doctor Strange'},\n        },\n      },\n      {\n        key: {release_year: 2017},\n        grouping: {'release_year'},\n        elements: {\n          default::Movie {title: 'Spider-Man: Homecoming'},\n          default::Movie {title: 'Thor: Ragnarok'},\n        },\n      },\n      {\n        key: {release_year: 2018},\n        grouping: {'release_year'},\n        elements: {default::Movie {title: 'Ant-Man and the Wasp'}},\n      },\n      {\n        key: {release_year: 2019},\n        grouping: {'release_year'},\n        elements: {default::Movie {title: 'Spider-Man: No Way Home'}},\n      },\n      {\n        key: {release_year: 2021},\n        grouping: {'release_year'},\n        elements: {default::Movie {title: 'Black Widow'}},\n      },\n      ...\n    }\n\nNotice that the result of ``group`` is a set of :ref:`free objects\n<ref_eql_select_free_objects>` with three fields:\n\n* ``key``: another free object containing the specific value of the\n  grouping parameter for a given subset.\n* ``grouping``: set of names of grouping parameters, i.e. the specific\n  names that also appear in the ``key`` free object.\n* ``elements``: the actual subset of values that match the ``key``.\n\nIn the ``group`` statement, referring to the property in the ``by`` clause\n**must** be done by using the leading dot shothand ``.release_year``. The\nproperty name then shows up in ``grouping`` and ``key`` to indicate the\ndefining characteristics of the particular result. Alternatively, we can give\nit an alias in an optional ``using`` clause and then that alias can be used in\nthe ``by`` clause and will appear in the results:\n\n.. code-block:: edgeql-repl\n\n    db> group Movie {title}\n    ... using year := .release_year by year;\n    {\n      {\n        key: {year: 2016},\n        grouping: {'year'},\n        elements: {\n          default::Movie {title: 'Captain America: Civil War'},\n          default::Movie {title: 'Doctor Strange'},\n        },\n      },\n      {\n        key: {year: 2017},\n        grouping: {'year'},\n        elements: {\n          default::Movie {title: 'Spider-Man: Homecoming'},\n          default::Movie {title: 'Thor: Ragnarok'},\n        },\n      },\n      {\n        key: {year: 2018},\n        grouping: {'year'},\n        elements: {default::Movie {title: 'Ant-Man and the Wasp'}},\n      },\n      {\n        key: {year: 2019},\n        grouping: {'year'},\n        elements: {default::Movie {title: 'Spider-Man: No Way Home'}},\n      },\n      {\n        key: {year: 2021},\n        grouping: {'year'},\n        elements: {default::Movie {title: 'Black Widow'}},\n      },\n      ...\n    }\n\nThe ``using`` clause is perfect for defining a more complex expression to\ngroup things by. For example, instead of grouping by the ``release_year`` we\ncan group by the release decade:\n\n.. code-block:: edgeql-repl\n\n    db> group Movie {title}\n    ... using decade := .release_year // 10\n    ... by decade;\n    {\n    {\n      {\n        key: {decade: 200},\n        grouping: {'decade'},\n        elements: {\n          default::Movie {title: 'Spider-Man'},\n          default::Movie {title: 'Spider-Man 2'},\n          default::Movie {title: 'Spider-Man 3'},\n          default::Movie {title: 'Iron Man'},\n          default::Movie {title: 'The Incredible Hulk'},\n        },\n      },\n      {\n        key: {decade: 201},\n        grouping: {'decade'},\n        elements: {\n          default::Movie {title: 'Iron Man 2'},\n          default::Movie {title: 'Thor'},\n          default::Movie {title: 'Captain America: The First Avenger'},\n          default::Movie {title: 'The Avengers'},\n          default::Movie {title: 'Iron Man 3'},\n          default::Movie {title: 'Thor: The Dark World'},\n          default::Movie {title: 'Captain America: The Winter Soldier'},\n          default::Movie {title: 'Ant-Man'},\n          default::Movie {title: 'Captain America: Civil War'},\n          default::Movie {title: 'Doctor Strange'},\n          default::Movie {title: 'Spider-Man: Homecoming'},\n          default::Movie {title: 'Thor: Ragnarok'},\n          default::Movie {title: 'Ant-Man and the Wasp'},\n          default::Movie {title: 'Spider-Man: No Way Home'},\n        },\n      },\n      {\n        key: {decade: 202},\n        grouping: {'decade'},\n        elements: {default::Movie {title: 'Black Widow'}},\n      },\n    }\n\nIt's also possible to group by more than one parameter, so we can group by\nwhether the movie ``title`` contains a colon *and* the decade it was released.\nAdditionally, let's only consider more recent movies, say, released after\n2015, so that we're not overwhelmed by all the combination of results:\n\n.. code-block:: edgeql-repl\n\n    db> with\n    ...   # Apply the group query only to more recent movies\n    ...   M := (select Movie filter .release_year > 2015)\n    ... group M {title}\n    ... using\n    ...   decade := .release_year // 10,\n    ...   has_colon := .title like '%:%'\n    ... by decade, has_colon;\n    {\n      {\n        key: {decade: 201, has_colon: false},\n        grouping: {'decade', 'has_colon'},\n        elements: {\n          default::Movie {title: 'Ant-Man and the Wasp'},\n          default::Movie {title: 'Doctor Strange'},\n        },\n      },\n      {\n        key: {decade: 201, has_colon: true},\n        grouping: {'decade', 'has_colon'},\n        elements: {\n          default::Movie {title: 'Captain America: Civil War'},\n          default::Movie {title: 'Spider-Man: No Way Home'},\n          default::Movie {title: 'Thor: Ragnarok'},\n          default::Movie {title: 'Spider-Man: Homecoming'},\n        },\n      },\n      {\n        key: {decade: 202, has_colon: false},\n        grouping: {'decade', 'has_colon'},\n        elements: {default::Movie {title: 'Black Widow'}},\n      },\n    }\n\nOnce we break a set into partitions, we can also use :ref:`aggregate\n<ref_eql_set_aggregate>` functions to provide some analytics about the data.\nFor example, for the above partitioning (by decade and presence of ``:`` in\nthe ``title``) we can calculate how many movies are in each subset as well as\nthe average number of words in the movie titles:\n\n.. code-block:: edgeql-repl\n\n    db> with\n    ...   # Apply the group query only to more recent movies\n    ...   M := (select Movie filter .release_year > 2015),\n    ...   groups := (\n    ...     group M {title}\n    ...     using\n    ...       decade := .release_year // 10 - 200,\n    ...       has_colon := .title like '%:%'\n    ...     by decade, has_colon\n    ...   )\n    ... select groups {\n    ...   key := .key {decade, has_colon},\n    ...   count := count(.elements),\n    ...   avg_words := math::mean(\n    ...     len(str_split(.elements.title, ' ')))\n    ... };\n    {\n      {key: {decade: 1, has_colon: false}, count: 2, avg_words: 3},\n      {key: {decade: 1, has_colon: true}, count: 4, avg_words: 3},\n      {key: {decade: 2, has_colon: false}, count: 1, avg_words: 2},\n    }\n\n.. note::\n\n    It is possible to produce results that are grouped in multiple different\n    ways using :ref:`grouping sets <ref_eql_statements_group>`. This may be\n    useful in more sophisticated analytics.\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Reference > Commands > Group <ref_eql_statements_group>`\n"
  },
  {
    "path": "docs/reference/edgeql/index.rst",
    "content": ".. versioned-section::\n\n.. _ref_edgeql:\n\n======\nEdgeQL\n======\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    literals\n    sets\n    paths\n    types\n    parameters\n    select\n    insert\n    update\n    delete\n    for\n    group\n    with\n    analyze\n    path_resolution\n    transactions\n\nEdgeQL is a next-generation query language designed to match SQL in power and\nsurpass it in terms of clarity, brevity, and intuitiveness. It's used to query\nthe database, insert/update/delete data, modify/introspect the schema, manage\ntransactions, and more.\n\nDesign goals\n------------\n\nEdgeQL is a spiritual successor to SQL designed with a few core principles in\nmind.\n\n**Compatible with modern languages**. A jaw-dropping amount of effort has been\nspent attempting to `bridge the gap <https://en.wikipedia.org/wiki/\nObject%E2%80%93relational_impedance_mismatch>`_ between the *relational*\nparadigm of SQL and the *object-oriented* nature of modern programming\nlanguages. Gel sidesteps this problem by modeling data in an\n*object-relational* way.\n\n**Strongly typed**. EdgeQL is *inextricably tied* to Gel's rigorous\nobject-oriented type system. The type of all expressions is statically\ninferred by Gel.\n\n**Designed for programmers**. EdgeQL prioritizes syntax over keywords; It uses\n``{ curly braces }`` to define scopes/structures and the *assignment\noperator* ``:=`` to set values. The result is a query language that looks more\nlike code and less like word soup.\n\n.. All told, EdgeQL syntax contains roughly 180\n.. reserved keywords; by comparison Postgres-flavored SQL contains `469\n.. <https://www.postgresql.org/docs/current/sql-keywords-appendix.html>`_.\n\n.. **Compiles to SQL**. All EdgeQL queries, no matter how complex, compile to a\n.. single PostgreSQL query under the hood. With the exception of ``group by``,\n.. EdgeQL is equivalent to SQL in terms of power and expressivity.\n\n**Easy deep querying**. Gel's object-relational nature makes it painless\nto write deep, performant queries that traverse links, no ``JOINs`` required.\n\n**Composable**. `Unlike SQL\n<https://www.geldata.com/blog/we-can-do-better-than-sql#lack-of-orthogonality>`_,\nEdgeQL's syntax is readily composable; queries can be cleanly nested without\nworrying about Cartesian explosion.\n"
  },
  {
    "path": "docs/reference/edgeql/insert.rst",
    "content": ".. _ref_eql_insert:\n\nInsert\n======\n\n.. api-index:: insert, :=\n\nThe ``insert`` command is used to create instances of object types. The code\nsamples on this page assume the following schema:\n\n.. code-block:: sdl\n\n    module default {\n      abstract type Person {\n        required name: str { constraint exclusive };\n      }\n\n      type Hero extending Person {\n        secret_identity: str;\n        multi villains := .<nemesis[is Villain];\n      }\n\n      type Villain extending Person {\n        nemesis: Hero;\n      }\n\n      type Movie {\n        required title: str { constraint exclusive };\n        required release_year: int64;\n        multi characters: Person;\n      }\n    }\n\n\n.. _ref_eql_insert_basic:\n\nBasic usage\n-----------\n\nYou can ``insert`` instances of any *non-abstract* object type.\n\n.. code-block:: edgeql-repl\n\n  db> insert Hero {\n  ...   name := \"Spider-Man\",\n  ...   secret_identity := \"Peter Parker\"\n  ... };\n  {default::Hero {id: b0fbe9de-3e90-11ec-8c12-ffa2d5f0176a}}\n\nSimilar to :ref:`selecting fields <ref_eql_shapes>` in ``select``, ``insert``\nstatements include a *shape* specified with ``curly braces``; the values of\nproperties/links are assigned with the ``:=`` operator.\n\nOptional links or properties can be omitted entirely, as well as those with a\n``default`` value (like ``id``).\n\n.. code-block:: edgeql-repl\n\n  db> insert Hero {\n  ...   name := \"Spider-Man\"\n  ...   # secret_identity is omitted\n  ... };\n  {default::Hero {id: b0fbe9de-3e90-11ec-8c12-ffa2d5f0176a}}\n\nYou can only ``insert`` instances of concrete (non-abstract) object types.\n\n.. code-block:: edgeql-repl\n\n  db> insert Person {\n  ...   name := \"The Man With No Name\"\n  ... };\n  error: QueryError: cannot insert into abstract object type 'default::Person'\n\nBy default, ``insert`` returns only the inserted object's ``id`` as seen in the\nexamples above. If you want to get additional data back, you may wrap your\n``insert`` with a ``select`` and apply a shape specifying any properties and\nlinks you want returned:\n\n.. code-block:: edgeql-repl\n\n  db> select (insert Hero {\n  ...   name := \"Spider-Man\"\n  ...   # secret_identity is omitted\n  ... }) {id, name};\n  {\n    default::Hero {\n      id: b0fbe9de-3e90-11ec-8c12-ffa2d5f0176a,\n      name: \"Spider-Man\"\n    }\n  }\n\nYou can use :ref:`ref_eql_with` to tidy this up if you prefer:\n\n.. code-block:: edgeql-repl\n\n  db> with NewHero := (insert Hero {\n  ...   name := \"Spider-Man\"\n  ...   # secret_identity is omitted\n  ... })\n  ... select NewHero {\n  ...   id,\n  ...   name,\n  ... }\n  {\n    default::Hero {\n      id: b0fbe9de-3e90-11ec-8c12-ffa2d5f0176a,\n      name: \"Spider-Man\"\n    }\n  }\n\n\n.. _ref_eql_insert_links:\n\nInserting links\n---------------\n\nEdgeQL's composable syntax makes link insertion painless. Below, we insert\n\"Spider-Man: No Way Home\" and include all known heroes and villains as\n``characters`` (which is basically true).\n\n.. code-block:: edgeql-repl\n\n  db> insert Movie {\n  ...   title := \"Spider-Man: No Way Home\",\n  ...   release_year := 2021,\n  ...   characters := (\n  ...     select Person\n  ...     filter .name in {\n  ...       'Spider-Man',\n  ...       'Doctor Strange',\n  ...       'Doc Ock',\n  ...       'Green Goblin'\n  ...     }\n  ...   )\n  ... };\n  {default::Movie {id: 9b1cf9e6-3e95-11ec-95a2-138eeb32759c}}\n\nTo assign to the ``Movie.characters`` link, we're using a *subquery*. This\nsubquery is executed and resolves to a set of type ``Person``, which is\nassignable to ``characters``.  Note that the inner ``select Person`` statement\nis wrapped in parentheses; this is required for all subqueries in EdgeQL.\n\nNow let's assign to a *single link*.\n\n.. code-block:: edgeql-repl\n\n  db> insert Villain {\n  ...   name := \"Doc Ock\",\n  ...   nemesis := (select Hero filter .name = \"Spider-Man\")\n  ... };\n\n\nThis query is valid because the inner subquery is guaranteed to return at most\none ``Hero`` object, due to the uniqueness constraint on ``Hero.name``. If you\nare filtering on a non-exclusive property, use ``assert_single`` to guarantee\nthat the subquery will return zero or one results. If more than one result is\nreturned, this query will fail at runtime.\n\n.. code-block:: edgeql-repl\n\n  db> insert Villain {\n  ...   name := \"Doc Ock\",\n  ...   nemesis := assert_single((\n  ...     select Hero\n  ...     filter .secret_identity = \"Peter B. Parker\"\n  ...   ))\n  ... };\n\n\n.. _ref_eql_insert_nested:\n\nNested inserts\n--------------\n\nJust as we used subqueries to populate links with existing objects, we can also\nexecute *nested inserts*.\n\n.. code-block:: edgeql-repl\n\n  db> insert Villain {\n  ...   name := \"The Mandarin\",\n  ...   nemesis := (insert Hero {\n  ...     name := \"Shang-Chi\",\n  ...     secret_identity := \"Shaun\"\n  ...   })\n  ... };\n  {default::Villain {id: d47888a0-3e7b-11ec-af13-fb68c8777851}}\n\n\nNow let's write a nested insert for a ``multi`` link.\n\n.. code-block:: edgeql-repl\n\n  db> insert Movie {\n  ...   title := \"Black Widow\",\n  ...   release_year := 2021,\n  ...   characters := {\n  ...     (select Hero filter .name = \"Black Widow\"),\n  ...     (insert Hero { name := \"Yelena Belova\"}),\n  ...     (insert Villain {\n  ...       name := \"Dreykov\",\n  ...       nemesis := (select Hero filter .name = \"Black Widow\")\n  ...     })\n  ...   }\n  ... };\n  {default::Movie {id: af706c7c-3e98-11ec-abb3-4bbf3f18a61a}}\n\nWe are using :ref:`set literal syntax <ref_eql_set_constructor>` to construct a\nset literal containing several ``select`` and ``insert`` subqueries. This set\ncontains a mix of ``Hero`` and ``Villain`` objects; since these are both\nsubtypes of ``Person`` (the expected type of ``Movie.characters``), this is\nvalid.\n\nYou also can't *assign* to a computed property or link; these fields don't\nactually exist in the database.\n\n.. code-block:: edgeql-repl\n\n  db> insert Hero {\n  ...   name := \"Ant-Man\",\n  ...   villains := (select Villain)\n  ... };\n  error: QueryError: modification of computed link 'villains' of object type\n  'default::Hero' is prohibited\n\n.. _ref_eql_insert_with:\n\nWith block\n----------\n\n.. api-index:: with\n\nIn the previous query, we selected Black Widow twice: once in the\n``characters`` set and again as the ``nemesis`` of Dreykov. In circumstances\nlike this, pulling a subquery into a ``with`` block lets you avoid\nduplication.\n\n.. code-block:: edgeql-repl\n\n  db> with black_widow := (select Hero filter .name = \"Black Widow\")\n  ... insert Movie {\n  ...   title := \"Black Widow\",\n  ...   release_year := 2021,\n  ...   characters := {\n  ...     black_widow,\n  ...     (insert Hero { name := \"Yelena Belova\"}),\n  ...     (insert Villain {\n  ...       name := \"Dreykov\",\n  ...       nemesis := black_widow\n  ...     })\n  ...   }\n  ... };\n  {default::Movie {id: af706c7c-3e98-11ec-abb3-4bbf3f18a61a}}\n\n\nThe ``with`` block can contain an arbitrary number of clauses; later clauses\ncan reference earlier ones.\n\n.. code-block:: edgeql-repl\n\n  db> with\n  ...  black_widow := (select Hero filter .name = \"Black Widow\"),\n  ...  yelena := (insert Hero { name := \"Yelena Belova\"}),\n  ...  dreykov := (insert Villain {name := \"Dreykov\", nemesis := black_widow})\n  ... insert Movie {\n  ...   title := \"Black Widow\",\n  ...   release_year := 2021,\n  ...   characters := { black_widow, yelena, dreykov }\n  ... };\n  {default::Movie {id: af706c7c-3e98-11ec-abb3-4bbf3f18a61a}}\n\n\n.. _ref_eql_insert_conflicts:\n\nConflicts\n---------\n\n.. api-index:: unless conflict on, else\n\n|Gel| provides a general-purpose mechanism for gracefully handling possible\nexclusivity constraint violations. Consider a scenario where we are trying to\n``insert`` Eternals (the ``Movie``), but we can't remember if it already exists\nin the database.\n\n.. code-block:: edgeql-repl\n\n  db> insert Movie {\n  ...   title := \"Eternals\",\n  ...   release_year := 2021\n  ... }\n  ... unless conflict on .title\n  ... else (select Movie);\n  {default::Movie {id: af706c7c-3e98-11ec-abb3-4bbf3f18a61a}}\n\nThis query attempts to ``insert`` Eternals. If it already exists in the\ndatabase, it will violate the uniqueness constraint on ``Movie.title``, causing\na *conflict* on the ``title`` field. The ``else`` clause is then executed and\nreturned instead. In essence, ``unless conflict`` lets us \"catch\" exclusivity\nconflicts and provide a fallback expression.\n\n.. note::\n\n  Note that the ``else`` clause is simply ``select Movie``. There's no need to\n  apply additional filters on ``Movie``; in the context of the ``else`` clause,\n  ``Movie`` is bound to the conflicting object.\n\n.. note::\n\n    Using ``unless conflict`` on :ref:`multi properties\n    <ref_datamodel_props_cardinality>` is only supported in 2.10 and later.\n\n.. _ref_eql_upsert:\n\nUpserts\n^^^^^^^\n\nThere are no limitations on what the ``else`` clause can contain; it can be any\nEdgeQL expression, including an :ref:`update <ref_eql_update>` statement. This\nlets you express *upsert* logic in a single EdgeQL query.\n\n.. code-block:: edgeql-repl\n\n  db> with\n  ...   title := \"Eternals\",\n  ...   release_year := 2021\n  ... insert Movie {\n  ...   title := title,\n  ...   release_year := release_year\n  ... }\n  ... unless conflict on .title\n  ... else (\n  ...   update Movie set { release_year := release_year }\n  ... );\n  {default::Movie {id: f1bf5ac0-3e9d-11ec-b78d-c7dfb363362c}}\n\nWhen a conflict occurs during the initial ``insert``, the statement falls back\nto the ``update`` statement in the ``else`` clause. This updates the\n``release_year`` of the conflicting object.\n\n.. note::\n\n    It can be useful to know the outcome of an upsert. Here's an example\n    showing how you can return that:\n\n    .. code-block:: edgeql-repl\n\n      db> with\n      ...   title := \"Eternals\",\n      ...   release_year := 2021,\n      ...   movie := (\n      ...     insert Movie {\n      ...       title := title,\n      ...       release_year := release_year\n      ...     }\n      ...     unless conflict on .title\n      ...     else (\n      ...       update Movie set { release_year := release_year }\n      ...     )\n      ...   )\n      ... select movie {\n      ...   is_new := (movie not in Movie)\n      ... };\n      {default::Movie {is_new: true}}\n\n    This technique exploits the fact that a ``select`` will not return an\n    object inserted in the same query. We know that, if the record exists, we\n    updated it. If it does not, we inserted it.\n\n    By wrapping your upsert in a ``select`` and putting a shape on it that\n    queries for the object and returns whether or not it exists (as ``is_new``,\n    in this example), you can easily see whether the object was inserted or\n    updated.\n\n    If you want to also return some of the ``Movie`` object's data, drop\n    additional property names into the shape alongside ``is_new``. If you're on\n    3.0+, you can add ``Movie.*`` to the shape alongside ``is_new`` to get back\n    all of the ``Movie`` object's properties. You could even silo the data off,\n    keeping it separate from the ``is_new`` computed value like this:\n\n    .. code-block:: edgeql-repl\n\n      db> with\n      ...   title := \"Eternals\",\n      ...   release_year := 2021,\n      ...   movie := (\n      ...     insert Movie {\n      ...       title := title,\n      ...       release_year := release_year\n      ...     }\n      ...     unless conflict on .title\n      ...     else (\n      ...       update Movie set { release_year := release_year }\n      ...     )\n      ...   )\n      ... select {\n      ...   data := (select movie {*}),\n      ...   is_new := (movie not in Movie)\n      ... };\n      {\n        {\n          data: {\n            default::Movie {\n              id: 6880d0ba-62ca-11ee-9608-635818746433,\n              release_year: 2021,\n              title: 'Eternals'\n            }\n          },\n          is_new: false\n        }\n      }\n\n\nSuppressing failures\n^^^^^^^^^^^^^^^^^^^^\n\n.. api-index:: unless conflict\n\nThe ``else`` clause is optional; when omitted, the ``insert`` statement will\nreturn an *empty set* if a conflict occurs. This is a common way to prevent\n``insert`` queries from failing on constraint violations.\n\n.. code-block:: edgeql-repl\n\n  db> insert Hero { name := \"The Wasp\" } # initial insert\n  ... unless conflict;\n  {default::Hero {id: 35b97a92-3e9b-11ec-8e39-6b9695d671ba}}\n  db> insert Hero { name := \"The Wasp\" } # The Wasp now exists\n  ... unless conflict;\n  {}\n\n.. _ref_eql_insert_bulk:\n\nBulk inserts\n------------\n\nBulk inserts are performed by passing in a JSON array as a :ref:`query\nparameter <ref_eql_params>`, :eql:func:`unpacking <json_array_unpack>` it, and\nusing a :ref:`for loop <ref_eql_for>` to insert the objects.\n\n.. code-block:: edgeql-repl\n\n  db> with\n  ...   raw_data := <json>$data,\n  ... for item in json_array_unpack(raw_data) union (\n  ...   insert Hero { name := <str>item['name'] }\n  ... );\n  Parameter <json>$data: [{\"name\":\"Sersi\"},{\"name\":\"Ikaris\"},{\"name\":\"Thena\"}]\n  {\n    default::Hero {id: 35b97a92-3e9b-11ec-8e39-6b9695d671ba},\n    default::Hero {id: 35b97a92-3e9b-11ec-8e39-6b9695d671ba},\n    default::Hero {id: 35b97a92-3e9b-11ec-8e39-6b9695d671ba},\n    ...\n  }\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Reference > Commands > Insert <ref_eql_statements_insert>`\n  * - :ref:`Cheatsheets > Inserting data <ref_cheatsheet_insert>`\n"
  },
  {
    "path": "docs/reference/edgeql/literals.rst",
    "content": ".. _ref_eql_literals:\n\nLiterals\n========\n\n.. index:: primitive types\n\nEdgeQL is *inextricably tied* to Gel's rigorous type system. Below is an\noverview of how to declare a literal value of each *primitive type*. Click a\nlink in the left column to jump to the associated section.\n\n.. list-table::\n\n  * - :ref:`String <ref_eql_literal_strings>`\n    - ``str``\n\n  * - :ref:`Boolean <ref_eql_literal_boolean>`\n    - ``bool``\n\n  * - :ref:`Numbers <ref_eql_literal_numbers>`\n    - ``int16`` ``int32`` ``int64``\n      ``float32`` ``float64`` ``bigint``\n      ``decimal``\n\n  * - :ref:`UUID <ref_eql_literal_uuid>`\n    - ``uuid``\n\n  * - :ref:`Enums <ref_eql_literal_enum>`\n    - ``enum<X, Y, Z>``\n\n  * - :ref:`Dates and times <ref_eql_literal_dates>`\n    - ``datetime`` ``duration``\n      ``cal::local_datetime`` ``cal::local_date``\n      ``cal::local_time`` ``cal::relative_duration``\n\n  * - :ref:`Durations <ref_eql_literal_durations>`\n    - ``duration`` ``cal::relative_duration`` ``cal::date_duration``\n\n  * - :ref:`Ranges <ref_eql_ranges>`\n    - ``range<x>``\n\n  * - :ref:`Bytes <ref_eql_literal_bytes>`\n    - ``bytes``\n\n  * - :ref:`Arrays <ref_eql_literal_array>`\n    - ``array<x>``\n\n  * - :ref:`Tuples <ref_eql_literal_tuple>`\n    - ``tuple<x, y, ...>`` or\n      ``tuple<foo: x, bar: y, ...>``\n\n  * - :ref:`JSON <ref_eql_literal_json>`\n    - ``json``\n\n.. _ref_eql_literal_strings:\n\nStrings\n-------\n\n.. index:: unicode, quotes, raw strings, escape character\n.. api-index:: str, r'', r\"\", $$, $§label§$, \\\\§char§\n\nThe :eql:type:`str` type is a variable-length string of Unicode characters. A\nstring can be declared with either single or double quotes.\n\n.. code-block:: edgeql-repl\n\n  db> select 'I ❤️ EdgeQL';\n  {'I ❤️ EdgeQL'}\n  db> select \"hello there!\";\n  {'hello there!'}\n  db> select 'hello\\nthere!';\n  {'hello\n  there!'}\n  db> select 'hello\n  ... there!';\n  {'hello\n  there!'}\n  db> select r'hello\n  ... there!'; # multiline\n  {'hello\n  there!'}\n\nThere is a special syntax for declaring \"raw strings\". Raw strings treat the\nbackslash ``\\`` as a literal character instead of an escape character.\n\n.. code-block:: edgeql-repl\n\n  db> select r'hello\\nthere'; # raw string\n  {r'hello\\\\nthere'}\n  db> select $$one\n  ... two\n  ... three$$; # multiline raw string\n  {'one\n  two\n  three'}\n  db> select $label$You can add an interstitial label\n  ... if you need to use \"$$\" in your string.$label$;\n  {\n    'You can add an interstital label\n    if you need to use \"$$\" in your string.',\n  }\n\nEdgeQL contains a set of built-in functions and operators for searching,\ncomparing, and manipulating strings.\n\n.. code-block:: edgeql-repl\n\n  db> select 'hellothere'[5:10];\n  {'there'}\n  db> select 'hello' ++ 'there';\n  {'hellothere'}\n  db> select len('hellothere');\n  {10}\n  db> select str_trim('  hello there  ');\n  {'hello there'}\n  db> select str_split('hello there', ' ');\n  {['hello', 'there']}\n\n\nFor a complete reference on strings, see :ref:`Standard Library > String\n<ref_std_string>` or click an item below.\n\n.. list-table::\n\n  * - Indexing and slicing\n    - :eql:op:`str[i] <stridx>` :eql:op:`str[from:to] <strslice>`\n  * - Concatenation\n    - :eql:op:`str ++ str <strplus>`\n  * - Utilities\n    - :eql:func:`len`\n  * - Transformation functions\n    - :eql:func:`str_split` :eql:func:`str_lower` :eql:func:`str_upper`\n      :eql:func:`str_title` :eql:func:`str_pad_start` :eql:func:`str_pad_end`\n      :eql:func:`str_trim` :eql:func:`str_trim_start` :eql:func:`str_trim_end`\n      :eql:func:`str_repeat`\n  * - Comparison operators\n    - :eql:op:`= <eq>` :eql:op:`\\!= <neq>` :eql:op:`?= <coaleq>`\n      :eql:op:`?!= <coalneq>` :eql:op:`\\< <lt>` :eql:op:`\\> <gt>`\n      :eql:op:`\\<= <lteq>` :eql:op:`\\>= <gteq>`\n  * - Search\n    - :eql:func:`contains` :eql:func:`find`\n  * - Pattern matching and regexes\n    - :eql:op:`str like pattern <like>` :eql:op:`str ilike pattern <ilike>`\n      :eql:func:`re_match` :eql:func:`re_match_all` :eql:func:`re_replace`\n      :eql:func:`re_test`\n\n\n.. _ref_eql_literal_boolean:\n\nBooleans\n--------\n\n.. api-index:: bool\n\nThe :eql:type:`bool` type represents a true/false value.\n\n.. code-block:: edgeql-repl\n\n  db> select true;\n  {true}\n  db> select false;\n  {false}\n\n|Gel| provides a set of operators that operate on boolean values.\n\n.. list-table::\n\n  * - Comparison operators\n    - :eql:op:`= <eq>` :eql:op:`\\!= <neq>` :eql:op:`?= <coaleq>`\n      :eql:op:`?!= <coalneq>` :eql:op:`\\< <lt>` :eql:op:`\\> <gt>`\n      :eql:op:`\\<= <lteq>` :eql:op:`\\>= <gteq>`\n  * - Logical operators\n    - :eql:op:`or` :eql:op:`and` :eql:op:`not`\n  * - Aggregation\n    - :eql:func:`all` :eql:func:`any`\n\n\n.. _ref_eql_literal_numbers:\n\nNumbers\n-------\n\nThere are several numerical types in Gel's type system.\n\n.. list-table::\n\n  * - :eql:type:`int16`\n    - 16-bit integer\n\n  * - :eql:type:`int32`\n    - 32-bit integer\n\n  * - :eql:type:`int64`\n    - 64-bit integer\n\n  * - :eql:type:`float32`\n    - 32-bit floating point number\n\n  * - :eql:type:`float64`\n    - 64-bit floating point number\n\n  * - :eql:type:`bigint`\n    - Arbitrary precision integer.\n\n  * - :eql:type:`decimal`\n    - Arbitrary precision number.\n\nNumber literals that *do not* contain a decimal are interpreted as ``int64``.\nNumbers containing decimals are interpreted as ``float64``. The ``n`` suffix\ndesignates a number with *arbitrary precision*: either ``bigint`` or\n``decimal``.\n\n====================================== =============================\n Syntax                                 Inferred type\n====================================== =============================\n :eql:code:`select 3;`                  :eql:type:`int64`\n :eql:code:`select 3.14;`               :eql:type:`float64`\n :eql:code:`select 314e-2;`             :eql:type:`float64`\n :eql:code:`select 42n;`                :eql:type:`bigint`\n :eql:code:`select 42.0n;`              :eql:type:`decimal`\n :eql:code:`select 42e+100n;`           :eql:type:`decimal`\n\n====================================== =============================\n\nTo declare an ``int16``, ``int32``, or ``float32``, you must provide an\nexplicit type cast. For details on type casting, see :ref:`Casting\n<ref_eql_types>`.\n\n====================================== =============================\n Syntax                                 Type\n====================================== =============================\n :eql:code:`select <int16>1234;`        :eql:type:`int16`\n :eql:code:`select <int32>123456;`      :eql:type:`int32`\n :eql:code:`select <float32>123.456;`   :eql:type:`float32`\n====================================== =============================\n\nEdgeQL includes a full set of arithmetic and comparison operators. Parentheses\ncan be used to indicate the order-of-operations or visually group\nsubexpressions; this is true across all EdgeQL queries.\n\n.. code-block:: edgeql-repl\n\n  db> select 5 > 2;\n  {true}\n  db> select 2 + 2;\n  {4}\n  db> select 2 ^ 10;\n  {1024}\n  db> select (1 + 1) * 2 / (3 + 8);\n  {0.36363636363636365}\n\n\nEdgeQL provides a comprehensive set of built-in functions and operators on\nnumerical data.\n\n.. list-table::\n\n  * - Comparison operators\n    - :eql:op:`= <eq>` :eql:op:`\\!= <neq>` :eql:op:`?= <coaleq>`\n      :eql:op:`?!= <coalneq>` :eql:op:`\\< <lt>` :eql:op:`\\> <gt>`\n      :eql:op:`\\<= <lteq>` :eql:op:`\\>= <gteq>`\n  * - Arithmetic\n    - :eql:op:`+ <plus>` :eql:op:`- <minus>` :eql:op:`- <uminus>`\n      :eql:op:`* <mult>` :eql:op:`/ <div>` :eql:op:`//  <floordiv>`\n      :eql:op:`% <mod>` :eql:op:`^ <pow>`\n  * - Statistics\n    - :eql:func:`sum` :eql:func:`min` :eql:func:`max` :eql:func:`math::mean`\n      :eql:func:`math::stddev` :eql:func:`math::stddev_pop`\n      :eql:func:`math::var` :eql:func:`math::var_pop`\n  * - Math\n    - :eql:func:`round` :eql:func:`math::abs` :eql:func:`math::ceil`\n      :eql:func:`math::floor` :eql:func:`math::ln` :eql:func:`math::lg`\n      :eql:func:`math::log`\n  * - Random number\n    - :eql:func:`random`\n\n\n.. _ref_eql_literal_uuid:\n\nUUID\n----\n\nThe :eql:type:`uuid` type is commonly used to represent object identifiers.\nUUID literal must be explicitly cast from a string value matching the UUID\nspecification.\n\n.. code-block:: edgeql-repl\n\n  db> select <uuid>'a5ea6360-75bd-4c20-b69c-8f317b0d2857';\n  {a5ea6360-75bd-4c20-b69c-8f317b0d2857}\n\nGenerate a random UUID.\n\n.. code-blocK:: edgeql-repl\n\n  db> select uuid_generate_v1mc();\n  {b4d94e6c-3845-11ec-b0f4-93e867a589e7}\n\n\n.. _ref_eql_literal_enum:\n\nEnums\n-----\n\n.. api-index:: scalar type, extending enum\n\nEnum types must be :ref:`declared in your schema <ref_datamodel_enums>`.\n\n.. code-block:: sdl\n\n  scalar type Color extending enum<Red, Green, Blue>;\n\nOnce declared, an enum literal can be declared with dot notation, or by\ncasting an appropriate string literal:\n\n.. code-block:: edgeql-repl\n\n  db> select Color.Red;\n  {Red}\n  db> select <Color>\"Red\";\n  {Red}\n\n\n.. _ref_eql_literal_dates:\n\nDates and times\n---------------\n\n|Gel's| typesystem contains several temporal types.\n\n.. list-table::\n\n  * - :eql:type:`datetime`\n    - Timezone-aware point in time\n\n  * - :eql:type:`cal::local_datetime`\n    - Date and time w/o timezone\n\n  * - :eql:type:`cal::local_date`\n    - Date type\n\n  * - :eql:type:`cal::local_time`\n    - Time type\n\nAll temporal literals are declared by casting an appropriately formatted\nstring.\n\n.. code-block:: edgeql-repl\n\n  db> select <datetime>'1999-03-31T15:17:00Z';\n  {<datetime>'1999-03-31T15:17:00Z'}\n  db> select <datetime>'1999-03-31T17:17:00+02';\n  {<datetime>'1999-03-31T15:17:00Z'}\n  db> select <cal::local_datetime>'1999-03-31T15:17:00';\n  {<cal::local_datetime>'1999-03-31T15:17:00'}\n  db> select <cal::local_date>'1999-03-31';\n  {<cal::local_date>'1999-03-31'}\n  db> select <cal::local_time>'15:17:00';\n  {<cal::local_time>'15:17:00'}\n\nEdgeQL supports a set of functions and operators on datetime types.\n\n.. list-table::\n\n  * - Comparison operators\n    - :eql:op:`= <eq>` :eql:op:`\\!= <neq>` :eql:op:`?= <coaleq>`\n      :eql:op:`?!= <coalneq>` :eql:op:`\\< <lt>` :eql:op:`\\> <gt>`\n      :eql:op:`\\<= <lteq>` :eql:op:`\\>= <gteq>`\n  * - Arithmetic\n    - :eql:op:`dt + dt <dtplus>` :eql:op:`dt - dt <dtminus>`\n  * - String parsing\n    - :eql:func:`to_datetime` :eql:func:`cal::to_local_datetime`\n      :eql:func:`cal::to_local_date` :eql:func:`cal::to_local_time`\n  * - Component extraction\n    - :eql:func:`datetime_get` :eql:func:`cal::time_get`\n      :eql:func:`cal::date_get`\n  * - Truncation\n    - :eql:func:`datetime_truncate`\n  * - System timestamps\n    - :eql:func:`datetime_current` :eql:func:`datetime_of_transaction`\n      :eql:func:`datetime_of_statement`\n\n\n.. _ref_eql_literal_durations:\n\nDurations\n---------\n\n|Gel's| type system contains three duration types.\n\n\n.. list-table::\n\n  * - :eql:type:`duration`\n    - Exact duration\n  * - :eql:type:`cal::relative_duration`\n    - Duration in relative units\n  * - :eql:type:`cal::date_duration`\n    - Duration in months and days only\n\nExact durations\n^^^^^^^^^^^^^^^\n\nThe :eql:type:`duration` type represents *exact* durations that can be\nrepresented by some fixed number of microseconds. It can be negative and it\nsupports units of ``microseconds``, ``milliseconds``, ``seconds``, ``minutes``,\nand ``hours``.\n\n.. code-block:: edgeql-repl\n\n  db> select <duration>'45.6 seconds';\n  {<duration>'0:00:45.6'}\n  db> select <duration>'-15 microseconds';\n  {<duration>'-0:00:00.000015'}\n  db> select <duration>'5 hours 4 minutes 3 seconds';\n  {<duration>'5:04:03'}\n  db> select <duration>'8760 hours'; # about a year\n  {<duration>'8760:00:00'}\n\nAll temporal units beyond ``hour`` no longer correspond to a fixed duration of\ntime; the length of a day/month/year/etc changes based on daylight savings\ntime, the month in question, leap years, etc.\n\nRelative durations\n^^^^^^^^^^^^^^^^^^\n\nBy contrast, the :eql:type:`cal::relative_duration` type represents a\n\"calendar\" duration, like ``1 month``. Because months have different number of\ndays, ``1 month`` doesn't correspond to a fixed number of milliseconds, but\nit's often a useful quantity to represent recurring events, postponements, etc.\n\n.. note::\n\n  The ``cal::relative_duration`` type supports the same units as ``duration``,\n  plus ``days``, ``weeks``, ``months``, ``years``, ``decades``, ``centuries``,\n  and ``millennia``.\n\nTo declare relative duration literals:\n\n.. code-block:: edgeql-repl\n\n  db> select <cal::relative_duration>'15 milliseconds';\n  {<cal::relative_duration>'PT.015S'}\n  db> select <cal::relative_duration>'2 months 3 weeks 45 minutes';\n  {<cal::relative_duration>'P2M21DT45M'}\n  db> select <cal::relative_duration>'-7 millennia';\n  {<cal::relative_duration>'P-7000Y'}\n\nDate durations\n^^^^^^^^^^^^^^\n\nThe :eql:type:`cal::date_duration` represents spans consisting of some number\nof *months* and *days*. This type is primarily intended to simplify logic\ninvolving :eql:type:`cal::local_date` values.\n\n.. code-block:: edgeql-repl\n\n  db> select <cal::date_duration>'5 days';\n  {<cal::date_duration>'P5D'}\n  db> select <cal::local_date>'2022-06-25' + <cal::date_duration>'5 days';\n  {<cal::local_date>'2022-06-30'}\n  db> select <cal::local_date>'2022-06-30' - <cal::local_date>'2022-06-25';\n  {<cal::date_duration>'P5D'}\n\nEdgeQL supports a set of functions and operators on duration types.\n\n.. list-table::\n\n  * - Comparison operators\n    - :eql:op:`= <eq>` :eql:op:`\\!= <neq>` :eql:op:`?= <coaleq>`\n      :eql:op:`?!= <coalneq>` :eql:op:`\\< <lt>` :eql:op:`\\> <gt>`\n      :eql:op:`\\<= <lteq>` :eql:op:`\\>= <gteq>`\n  * - Arithmetic\n    - :eql:op:`dt + dt <dtplus>` :eql:op:`dt - dt <dtminus>`\n  * - Duration string parsing\n    - :eql:func:`to_duration` :eql:func:`cal::to_relative_duration`\n      :eql:func:`cal::to_date_duration`\n  * - Component extraction\n    - :eql:func:`duration_get`\n  * - Conversion\n    - :eql:func:`duration_truncate` :eql:func:`cal::duration_normalize_hours`\n      :eql:func:`cal::duration_normalize_days`\n\n\n.. _ref_eql_ranges:\n\nRanges\n------\n\n.. api-index:: range, inc_lower, inc_upper, empty\n\nRanges represent a range of orderable scalar values. A range comprises a lower\nbound, upper bound, and two boolean flags indicating whether each bound is\ninclusive.\n\nCreate a range literal with the ``range`` constructor function.\n\n.. code-block:: edgeql-repl\n\n    db> select range(1, 10);\n    {range(1, 10, inc_lower := true, inc_upper := false)}\n    db> select range(2.2, 3.3);\n    {range(2.2, 3.3, inc_lower := true, inc_upper := false)}\n\nRanges can be *empty*, when the upper and lower bounds are equal.\n\n.. code-block:: edgeql-repl\n\n    db> select range(1, 1);\n    {range({}, empty := true)}\n\nRanges can be *unbounded*. An empty set is used to indicate the\nlack of a particular upper or lower bound.\n\n.. code-block:: edgeql-repl\n\n    db> select range(4, <int64>{});\n    {range(4, {})}\n    db> select range(<int64>{}, 4);\n    {range({}, 4)}\n    db> select range(<int64>{}, <int64>{});\n    {range({}, {})}\n\nTo compute the set of concrete values defined by a range literal, use\n``range_unpack``. An empty range will unpack to the empty set. Unbounded\nranges cannot be unpacked.\n\n.. code-block:: edgeql-repl\n\n    db> select range_unpack(range(0, 10));\n    {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}\n    db> select range_unpack(range(1, 1));\n    {}\n    db> select range_unpack(range(0, <int64>{}));\n    gel error: InvalidValueError: cannot unpack an unbounded range\n\n.. _ref_eql_literal_bytes:\n\nBytes\n-----\n\n.. index:: binary, raw byte strings\n.. api-index:: b'', b\"\", rb'', br'', rb\"\", br\"\"\n\nThe ``bytes`` type represents raw binary data.\n\n.. code-block:: edgeql-repl\n\n  db> select b'bina\\\\x01ry';\n  {b'bina\\\\x01ry'}\n\nThere is a special syntax for declaring \"raw byte strings\". Raw byte strings\ntreat the backslash ``\\`` as a literal character instead of an escape\ncharacter.\n\n.. code-block:: edgeql-repl\n\n  db> select rb'hello\\nthere';\n  {b'hello\\\\nthere'}\n  db> select br'\\';\n  {b'\\\\'}\n\n\n.. _ref_eql_literal_array:\n\nArrays\n------\n\n.. index:: collection, lists\n\nAn array is an *ordered* collection of values of the *same type*. For example:\n\n.. code-block:: edgeql-repl\n\n    db> select [1, 2, 3];\n    {[1, 2, 3]}\n    db> select ['hello', 'world'];\n    {['hello', 'world']}\n    db> select [(1, 2), (100, 200)];\n    {[(1, 2), (100, 200)]}\n\nEdgeQL provides a set of functions and operators on arrays.\n\n.. list-table::\n\n  * - Indexing and slicing\n    - :eql:op:`array[i] <arrayidx>` :eql:op:`array[from:to] <arrayslice>`\n      :eql:func:`array_get`\n  * - Concatenation\n    - :eql:op:`array ++ array <arrayplus>`\n  * - Comparison operators\n    - :eql:op:`= <eq>` :eql:op:`\\!= <neq>` :eql:op:`?= <coaleq>`\n      :eql:op:`?!= <coalneq>` :eql:op:`\\< <lt>` :eql:op:`\\> <gt>`\n      :eql:op:`\\<= <lteq>` :eql:op:`\\>= <gteq>`\n  * - Utilities\n    - :eql:func:`len` :eql:func:`array_join`\n  * - Search\n    - :eql:func:`contains` :eql:func:`find`\n  * - Conversion to/from sets\n    - :eql:func:`array_agg` :eql:func:`array_unpack`\n\nSee :ref:`Standard Library > Array <ref_std_array>` for a complete\nreference on array data types.\n\n\n.. _ref_eql_literal_tuple:\n\nTuples\n------\n\nA tuple is *fixed-length*, *ordered* collection of values, each of which may\nhave a *different type*. The elements of a tuple can be of any type, including\nscalars, arrays, other tuples, and object types.\n\n.. code-block:: edgeql-repl\n\n  db> select ('Apple', 7, true);\n  {('Apple', 7, true)}\n\nOptionally, you can assign a key to each element of a tuple. These are known\nas *named tuples*. You must assign keys to all or none of the elements; you\ncan't mix-and-match.\n\n.. code-block:: edgeql-repl\n\n  db> select (fruit := 'Apple', quantity := 3.14, fresh := true);\n  {(fruit := 'Apple', quantity := 3.14, fresh := true)}\n\nIndexing tuples\n^^^^^^^^^^^^^^^\n\nTuple elements can be accessed with dot notation. Under the hood, there's no\ndifference between named and unnamed tuples. Named tuples support key-based\nand numerical indexing.\n\n.. code-block:: edgeql-repl\n\n    db> select (1, 3.14, 'red').0;\n    {1}\n    db> select (1, 3.14, 'red').2;\n    {'red'}\n    db> select (name := 'george', age := 12).name;\n    {('george')}\n    db> select (name := 'george', age := 12).0;\n    {('george')}\n\n.. important::\n\n  When you query an *unnamed* tuple using one of EdgeQL's :ref:`client\n  libraries <ref_clients_index>`, its value is converted to a list/array. When\n  you fetch a *named tuple*, it is converted to an object/dictionary/hashmap.\n\nFor a full reference on tuples, see :ref:`Standard Library > Tuple\n<ref_std_tuple>`.\n\n.. _ref_eql_literal_json:\n\nJSON\n----\n\nThe :eql:type:`json` scalar type is a stringified representation of structured\ndata. JSON literals are declared by explicitly casting other values or passing\na properly formatted JSON string into :eql:func:`to_json`. Any type can be\nconverted into JSON except :eql:type:`bytes`.\n\n.. code-block:: edgeql-repl\n\n  db> select <json>5;\n  {'5'}\n  db> select <json>\"a string\";\n  {'\"a string\"'}\n  db> select <json>[\"this\", \"is\", \"an\", \"array\"];\n  {'[\"this\", \"is\", \"an\", \"array\"]'}\n  db> select <json>(\"unnamed tuple\", 2);\n  {'[\"unnamed tuple\", 2]'}\n  db> select <json>(name := \"named tuple\", count := 2);\n  {'{\n    \"name\": \"named tuple\",\n    \"count\": 2\n  }'}\n  db> select to_json('{\"a\": 2, \"b\": 5}');\n  {'{\"a\": 2, \"b\": 5}'}\n\nJSON values support indexing operators. The resulting value is also of type\n``json``.\n\n.. code-block:: edgeql-repl\n\n  db> select to_json('{\"a\": 2, \"b\": 5}')['a'];\n  {2}\n  db> select to_json('[\"a\", \"b\", \"c\"]')[2];\n  {'\"c\"'}\n\n\nEdgeQL supports a set of functions and operators on ``json`` values. Refer to\nthe :ref:`Standard Library > JSON <ref_std_json>` or click an item below for\ndetailed documentation.\n\n.. list-table::\n\n    * - Indexing\n      - :eql:op:`json[i] <jsonidx>` :eql:op:`json[from:to] <jsonslice>`\n        :eql:op:`json[name] <jsonobjdest>` :eql:func:`json_get`\n    * - Merging\n      - :eql:op:`json ++ json <jsonplus>`\n    * - Comparison operators\n      - :eql:op:`= <eq>` :eql:op:`\\!= <neq>` :eql:op:`?= <coaleq>`\n        :eql:op:`?!= <coalneq>` :eql:op:`\\< <lt>` :eql:op:`\\> <gt>`\n        :eql:op:`\\<= <lteq>` :eql:op:`\\>= <gteq>`\n    * - Conversion to/from strings\n      - :eql:func:`to_json` :eql:func:`to_str`\n    * - Conversion to/from sets\n      - :eql:func:`json_array_unpack` :eql:func:`json_object_unpack`\n    * - Introspection\n      - :eql:func:`json_typeof`\n"
  },
  {
    "path": "docs/reference/edgeql/parameters.rst",
    "content": ".. _ref_eql_params:\n\nParameters\n==========\n\n.. index:: query params, query arguments, query args, input\n.. api-index:: $, <§type§>$\n\n:edb-alt-title: Query Parameters\n\nEdgeQL queries can reference parameters with ``$`` notation. The value of these\nparameters are supplied externally.\n\n.. code-block:: edgeql\n\n  select <str>$var;\n  select <int64>$a + <int64>$b;\n  select BlogPost filter .id = <uuid>$blog_id;\n\nNote that we provided an explicit type cast before the parameter. This is\nrequired, as it enables Gel to enforce the provided types at runtime.\n\nParameters can be named or unnamed tuples.\n\n.. code-block:: edgeql\n\n    select <tuple<str, bool>>$var;\n    select <optional tuple<str, bool>>$var;\n    select <tuple<name: str, flag: bool>>$var;\n    select <optional tuple<name: str, flag: bool>>$var;\n\nUsage with clients\n------------------\n\nREPL\n^^^^\n\nWhen you include a parameter reference in a Gel REPL, you'll be prompted\ninteractively to provide a value or values.\n\n.. code-block:: edgeql-repl\n\n  db> select 'I ❤️ ' ++ <str>$var ++ '!';\n  Parameter <str>$var: Gel\n  {'I ❤️ Gel!'}\n\n\nPython\n^^^^^^\n\n.. code-block:: python\n\n  await client.query(\n      \"select 'I ❤️ ' ++ <str>$var ++ '!';\",\n      var=\"lamp\")\n\n  await client.query(\n      \"select <datetime>$date;\",\n      date=datetime.today())\n\nJavaScript\n^^^^^^^^^^\n\n.. code-block:: javascript\n\n  await client.query(\"select 'I ❤️ ' ++ <str>$name ++ '!';\", {\n    name: \"rock and roll\"\n  });\n\n  await client.query(\"select <datetime>$date;\", {\n    date: new Date()\n  });\n\nGo\n^^\n\n.. code-block:: go\n\n  var result string\n  err = db.QuerySingle(ctx,\n    `select 'I ❤️ ' ++ <str>$var ++ '!';\"`,\n    &result, \"Golang\")\n\n  var date time.Time\n  err = db.QuerySingle(ctx,\n    `select <datetime>$date;`,\n    &date, time.Now())\n\n\nRefer to the Datatypes page of your preferred :ref:`client library\n<ref_clients_index>` to learn more about mapping between Gel types and\nlanguage-native types.\n\n\n.. _ref_eql_params_types:\n\nParameter types and JSON\n------------------------\n\nIn Gel, parameters can also be tuples. If you need to pass complex structures\nas parameters, use Gel's built-in :ref:`JSON <ref_std_json>` functionality.\n\n.. code-block:: edgeql-repl\n\n  db> with data := <json>$data\n  ... insert Movie {\n  ...   title := <str>data['title'],\n  ...   release_year := <int64>data['release_year'],\n  ... };\n  Parameter <json>$data: {\"title\": \"The Marvels\", \"release_year\": 2023}\n  {default::Movie {id: 8d286cfe-3c0a-11ec-aa68-3f3076ebd97f}}\n\nArrays can be \"unpacked\" into sets and assigned to ``multi`` links or\nproperties.\n\n.. code-block:: edgeql\n\n   with friends := (\n     select User filter .id in array_unpack(<array<uuid>>$friend_ids)\n   )\n   insert User {\n     name := <str>$name,\n     friends := friends,\n   };\n\n\n.. _ref_eql_params_optional:\n\nOptional parameters\n-------------------\n\n.. api-index:: <optional §type§>$\n\nBy default, query parameters are ``required``; the query will fail if the\nparameter value is an empty set. You can use an ``optional`` modifier inside\nthe type cast if the parameter is optional.\n\n.. code-block:: edgeql-repl\n\n  db> select <optional str>$name;\n  Parameter <str>$name (Ctrl+D for empty set `{}`):\n  {}\n\n.. note::\n\n  The ``<required foo>`` type cast is also valid (though redundant) syntax.\n\n  .. code-block:: edgeql\n\n    select <required str>$name;\n\n\nDefault parameter values\n------------------------\n\n.. api-index:: ??\n\nWhen using optional parameters, you may want to provide a default value to use\nin case the parameter is not passed. You can do this by using the\n:eql:op:`?? (coalesce) <coalesce>` operator.\n\n.. code-block:: edgeql-repl\n\n  db> select 'Hello ' ++ <optional str>$name ?? 'there';\n  Parameter <str>$name (Ctrl+D for empty set `{}`): Gel\n  {'Hello Gel'}\n  db> select 'Hello ' ++ <optional str>$name ?? 'there';\n  Parameter <str>$name (Ctrl+D for empty set `{}`):\n  {'Hello there'}\n\n\nWhat can be parameterized?\n--------------------------\n\nAny data manipulation language (DML) statement can be\nparameterized: ``select``, ``insert``, ``update``, and ``delete``. Since\nparameters can only be scalars, arrays of scalars, and\ntuples of scalars, only parts of the query that would be one of those types can\nbe parameterized. This excludes parts of the query like the type being queried\nand the property to order by.\n\n.. note::\n\n    You can parameterize ``order by`` for a limited number of options by using\n    :eql:op:`if..else`:\n\n    .. code-block:: edgeql\n\n        select Movie {*}\n          order by\n            (.title if <str>$order_by = 'title'\n              else <str>{})\n          then\n            (.release_year if <str>$order_by = 'release_year'\n              else <int64>{});\n\n    If a user running this query enters ``title`` as the parameter value,\n    ``Movie`` objects will be sorted by their ``title`` property. If they enter\n    ``release_year``, they will be sorted by the ``release_year`` property.\n\n    Since the ``if`` and ``else`` result clauses need to be of compatible\n    types, your ``else`` expressions should be an empty set of the same type as\n    the property.\n\nSchema definition language (SDL) and :ref:`configure\n<ref_eql_statements_configure>` statements **cannot** be parameterized. Data\ndefinition language (DDL) has limited support for parameters, but it's not a\nrecommended pattern. Some of the limitations might be lifted in future\nversions.\n\n"
  },
  {
    "path": "docs/reference/edgeql/path_resolution.rst",
    "content": ".. _ref_eql_path_resolution:\n\n============\nPath scoping\n============\n\n.. index:: using future simple_scoping, using future warn_old_scoping\n\nBeginning with Gel 6.0, we are phasing out our historical (and\nsomewhat notorious)\n:ref:`\"path scoping\" algorithm <ref_eql_old_path_resolution>`\nin favor of a much simpler algorithm that nevertheless behaves\nidentically on *most* idiomatic EdgeQL queries.\n\nGel 6.0 will contain features to support migration to and testing\nof the new semantics.  We expect the migration to be relatively\npainless for most users.\n\nDiscussion of rationale for this change is available in\n`the RFC <rfc_>`_.\n\n\nNew path scoping\n----------------\n\n.. versionadded:: 6.0\n\nWhen applying a shape to a path (or to a path that has shapes applied\nto it already), the path will be be bound inside computed\npointers in that shape:\n\n.. code-block:: edgeql-repl\n\n    db> select User {\n    ...   name := User.first_name ++ ' ' ++ User.last_name\n    ... }\n    {User {name: 'Peter Parker'}, User {name: 'Tony Stark'}}\n\n\nWhen doing ``SELECT``, ``UPDATE``, or ``DELETE``, if the subject is a\npath, optionally with shapes applied to it, the path will be\nbound in ``FILTER`` and ``ORDER BY`` clauses:\n\n.. code-block:: edgeql-repl\n\n    db> select User {\n    ...   name := User.first_name ++ ' ' ++ User.last_name\n    ... }\n    ... filter User.first_name = 'Peter'\n    {User {name: 'Peter Parker'}}\n\n\nHowever, when a path is used multiple times in \"sibling\" contexts,\na cross-product will be computed:\n\n.. code-block:: edgeql-repl\n\n    db> select User.first_name ++ ' ' ++ User.last_name;\n    {'Peter Parker', 'Peter Stark', 'Tony Parker', 'Tony Stark'}\n\n\nIf you want to produce one value per ``User``, you can rewrite the query\nwith a ``FOR`` to make the intention explicit:\n\n.. code-block:: edgeql-repl\n\n    db> for u in User\n    ... select u.first_name ++ ' ' ++ u.last_name;\n    {'Peter Parker', 'Tony Stark'}\n\nThe most idiomatic way to fetch such data in EdgeQL, however,\nremains:\n\n.. code-block:: edgeql-repl\n\n    db> select User { name := .first_name ++ ' ' ++ .last_name }\n    {User {name: 'Peter Parker'}, User {name: 'Tony Stark'}}\n\n(And, of course, you probably `shouldn't have first_name and last_name\nproperties anyway\n<https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/>`_)\n\n\nPath scoping configuration\n--------------------------\n\n.. versionadded:: 6.0\n\nGel 6.0 introduces a new\n:ref:`future feature <ref_datamodel_future>`\nnamed ``simple_scoping`` alongside a\nconfiguration setting also named ``simple_scoping``.  The future\nfeature presence will determine which behavior is used inside\nexpressions within the schema, as well as serve as the default value\nif the configuration value is not set. The configuration setting will\nallow overriding the presence or absence of the feature.\n\nFor concreteness, here are all of the posible combinations of whether\n``using future simple_scoping`` is set and the value of the\nconfiguration value ``simple_scoping``:\n\n.. list-table::\n   :widths: 25 25 25 25\n   :header-rows: 1\n\n   * - Future exists?\n     - Config value\n     - Query is simply scoped\n     - Schema is simply scoped\n   * - No\n     - ``{}``\n     - No\n     - No\n   * - No\n     - ``true``\n     - Yes\n     - No\n   * - No\n     - ``false``\n     - No\n     - No\n   * - Yes\n     - ``{}``\n     - Yes\n     - Yes\n   * - Yes\n     - ``true``\n     - Yes\n     - Yes\n   * - Yes\n     - ``false``\n     - No\n     - Yes\n\n.. _ref_warn_old_scoping:\n\nWarning on old scoping\n----------------------\n\n.. versionadded:: 6.0\n\nTo make the migration process safer, we have also introduced a\n``warn_old_scoping`` :ref:`future feature <ref_datamodel_future>` and\nconfig setting.\n\nWhen active, the server will emit a warning to the client when a query\nis detected to depend on the old scoping behavior.  The behavior of\nwarnings can be configured in client bindings, but by default they are\nlogged.\n\nThe check is known to sometimes produce false positives, on queries\nthat will not actually have changed behavior, but is intended to not\nhave false negatives.\n\nRecommended upgrade plan\n------------------------\n\n.. versionadded:: 6.0\n\nThe safest approach is to first get your entire schema and application\nworking with ``warn_old_scoping`` without producing any warnings. Once\nthat is done, it should be safe to switch to ``simple_scoping``\nwithout changes in behavior.\n\nIf you are very confident in your test coverage, though, you can try\nskipping dealing with ``warn_old_scoping`` and go straight to\n``simple_scoping``.\n\nThere are many different potential migration strategies. One that\nshould work well:\n\n1. Run ``CONFIGURE CURRENT DATABASE SET warn_old_scoping := true``\n2. Try running all of your queries against the database.\n3. Fix any that produce warnings.\n4. Adjust your schema until setting ``using future warn_old_scoping`` works\n   without producing warnings.\n\nIf you wish to proceed incrementally with steps 2 and 3, you can\nconfigure ``warn_old_scoping`` in your clients, having it enabled for\nqueries that you have verified work with it and disabled for queries\nthat have not yet been verified or updated.\n\n\n.. _ref_eql_old_path_resolution:\n\nLegacy path scoping\n-------------------\n\nThis section describes the path scoping algorithm used exclusively\nuntil |EdgeDB| 5.0 and by default in |Gel| 6.0.\nIt will be removed in Gel 7.0.\n\nElement-wise operations with multiple arguments in Gel are generally applied\nto the :ref:`cartesian product <ref_reference_cardinality_cartesian>` of all\nthe input sets.\n\n.. code-block:: edgeql-repl\n\n    db> select {'aaa', 'bbb'} ++ {'ccc', 'ddd'};\n    {'aaaccc', 'aaaddd', 'bbbccc', 'bbbddd'}\n\nHowever, in cases where multiple element-wise arguments share a common path\n(``User.`` in this example), Gel factors out the common path rather than\nusing cartesian multiplication.\n\n.. code-block:: edgeql-repl\n\n    db> select User.first_name ++ ' ' ++ User.last_name;\n    {'Mina Murray', 'Jonathan Harker', 'Lucy Westenra', 'John Seward'}\n\nWe assume this is what you want, but if your goal is to get the cartesian\nproduct, you can accomplish it one of three ways. You could use\n:eql:op:`detached`.\n\n.. code-block:: edgeql-repl\n\n    gel> select User.first_name ++ ' ' ++ detached User.last_name;\n    {\n      'Mina Murray',\n      'Mina Harker',\n      'Mina Westenra',\n      'Mina Seward',\n      'Jonathan Murray',\n      'Jonathan Harker',\n      'Jonathan Westenra',\n      'Jonathan Seward',\n      'Lucy Murray',\n      'Lucy Harker',\n      'Lucy Westenra',\n      'Lucy Seward',\n      'John Murray',\n      'John Harker',\n      'John Westenra',\n      'John Seward',\n    }\n\nYou could use :ref:`with <ref_eql_with>` to attach a different symbol to\nyour set of ``User`` objects.\n\n.. code-block:: edgeql-repl\n\n    gel> with U := User\n    .... select U.first_name ++ ' ' ++ User.last_name;\n    {\n      'Mina Murray',\n      'Mina Harker',\n      'Mina Westenra',\n      'Mina Seward',\n      'Jonathan Murray',\n      'Jonathan Harker',\n      'Jonathan Westenra',\n      'Jonathan Seward',\n      'Lucy Murray',\n      'Lucy Harker',\n      'Lucy Westenra',\n      'Lucy Seward',\n      'John Murray',\n      'John Harker',\n      'John Westenra',\n      'John Seward',\n    }\n\nOr you could leverage the effect scopes have on path resolution. More on that\n:ref:`in the Scopes section <ref_eql_path_resolution_scopes>`.\n\nThe reason ``with`` works here even though the alias ``U`` refers to the exact\nsame set is that we only assume you want the path factored in this way when you\nuse the same *symbol* to refer to a set. This means operations with\n``User.first_name`` and ``User.last_name`` *do* get the common path factored\nwhile ``U.first_name`` and ``User.last_name`` *do not* and are resolved with\ncartesian multiplication.\n\nThat may leave you still wondering why ``U`` and ``User`` did not get a common\npath factored. ``U`` is just an alias of ``select User`` and ``User`` is the\nsame symbol that we use in our name query. That's true, but |Gel| doesn't\nfactor in this case because of the queries' scopes.\n\n.. _ref_eql_path_resolution_scopes:\n\nScopes\n------\n\nScopes change the way path resolution works. Two sibling select queries — that\nis, queries at the same level — do not have their paths factored even when they\nuse a common symbol.\n\n.. code-block:: edgeql-repl\n\n    gel> select ((select User.first_name), (select User.last_name));\n    {\n      ('Mina', 'Murray'),\n      ('Mina', 'Harker'),\n      ('Mina', 'Westenra'),\n      ('Mina', 'Seward'),\n      ('Jonathan', 'Murray'),\n      ('Jonathan', 'Harker'),\n      ('Jonathan', 'Westenra'),\n      ('Jonathan', 'Seward'),\n      ('Lucy', 'Murray'),\n      ('Lucy', 'Harker'),\n      ('Lucy', 'Westenra'),\n      ('Lucy', 'Seward'),\n      ('John', 'Murray'),\n      ('John', 'Harker'),\n      ('John', 'Westenra'),\n      ('John', 'Seward'),\n    }\n\nCommon symbols in nested scopes *are* factored when they use the same symbol.\nIn this example, the nested queries both use the same ``User`` symbol as the\ntop-level query. As a result, the ``User`` in those queries refers to a single\nobject because it has been factored.\n\n.. code-block:: edgeql-repl\n\n    gel> select User {\n    ....   name:= (select User.first_name) ++ ' ' ++ (select User.last_name)\n    .... };\n    {\n      default::User {name: 'Mina Murray'},\n      default::User {name: 'Jonathan Harker'},\n      default::User {name: 'Lucy Westenra'},\n      default::User {name: 'John Seward'},\n    }\n\nIf you have two common scopes and only *one* of them is in a nested scope, the\npaths are still factored.\n\n.. code-block:: edgeql-repl\n\n    gel> select (Person.name, count(Person.friends));\n    {('Fran', 3), ('Bam', 2), ('Emma', 3), ('Geoff', 1), ('Tyra', 1)}\n\nIn this example, ``count``, like all aggregate function, creates a nested\nscope, but this doesn't prevent the paths from being factored as you can see\nfrom the results. If the paths were *not* factored, the friend count would be\nthe same for all the result tuples and it would reflect the total number of\n``Person`` objects that are in *all* ``friends`` links rather than the number\nof ``Person`` objects that are in the named ``Person`` object's ``friends``\nlink.\n\nIf you have two aggregate functions creating *sibling* nested scopes, the paths\nare *not* factored.\n\n.. code-block:: edgeql-repl\n\n    gel> select (array_agg(distinct Person.name), count(Person.friends));\n    {(['Fran', 'Bam', 'Emma', 'Geoff'], 3)}\n\nThis query selects a tuple containing two nested scopes. Here, |Gel| assumes\nyou want an array of all unique names and a count of the total number of people\nwho are anyone's friend.\n\nClauses & Nesting\n^^^^^^^^^^^^^^^^^\n\nMost clauses are nested and are subjected to the same rules described above:\ncommon symbols are factored and assumed to refer to the same object as the\nouter query. This is because clauses like :ref:`filter\n<ref_eql_select_filter>` and :ref:`order by <ref_eql_select_order>` need to\nbe applied to each value in the result.\n\nThe :ref:`offset <ref_eql_select_pagination>` and\n:ref:`limit <ref_eql_select_pagination>` clauses are not nested in the scope\nbecause they need to be applied globally to the entire result set of your\nquery.\n\n.. _rfc: https://github.com/geldata/rfcs/blob/master/text/1027-no-factoring.rst\n"
  },
  {
    "path": "docs/reference/edgeql/paths.rst",
    "content": ".. _ref_eql_paths:\n\n=====\nPaths\n=====\n\n.. index:: links, relations\n\nA *path expression* (or simply a *path*) represents a set of values that are\nreachable by traversing a given sequence of links or properties from some\nsource set of objects.\n\nConsider the following schema:\n\n.. code-block:: sdl\n\n    type User {\n      required email: str;\n      multi friends: User;\n    }\n\n    type BlogPost {\n      required title: str;\n      required author: User;\n    }\n\n    type Comment {\n      required text: str;\n      required author: User;\n    }\n\nA few simple inserts will allow some experimentation with paths.\n\nStart with a first user:\n\n.. code-block:: edgeql-repl\n\n    db> insert User {\n    ... email := \"user1@me.com\",\n    ... };\n\nAlong comes another user who adds the first user as a friend:\n\n.. code-block:: edgeql-repl\n\n    db> insert User {\n    ... email := \"user2@me.com\",\n    ... friends := (select detached User filter .email = \"user1@me.com\")\n    ... };\n\nThe first user reciprocates, adding the new user as a friend:\n\n.. code-block:: edgeql-repl\n\n    db> update User filter .email = \"user1@me.com\"\n    ... set {\n    ... friends += (select detached User filter .email = \"user2@me.com\")\n    ... };\n\nThe second user writes a blog post about how nice Gel is:\n\n.. code-block:: edgeql-repl\n\n    db> insert BlogPost {\n    ... title := \"Gel is awesome\",\n    ... author := assert_single((select User filter .email = \"user2@me.com\"))\n    ... };\n\nAnd the first user follows it up with a comment below the post:\n\n.. code-block:: edgeql-repl\n\n    db> insert Comment {\n    ... text := \"Nice post, user2!\",\n    ... author := assert_single((select User filter .email = \"user1@me.com\"))\n    ... };\n\nThe simplest path is simply ``User``. This is a :ref:`set reference\n<ref_eql_set_references>` that refers to all ``User`` objects in the database.\n\n.. code-block:: edgeql\n\n    select User;\n\nPaths can traverse links. The path below refers to *all Users who are the\nfriend of another User*.\n\n.. code-block:: edgeql\n\n    select User.friends;\n\nPaths can traverse to an arbitrary depth in a series of nested links.\nBoth ``select`` queries below end up showing the author of the ``BlogPost``.\nThe second query returns the friends of the friends of the author of the\n``BlogPost``, which in this case is just the author.\n\n.. code-block:: edgeql\n\n    select BlogPost.author; # The author\n    select BlogPost.author.friends.friends; # The author again\n\nPaths can terminate with a property reference.\n\n.. code-block:: edgeql\n\n    select BlogPost.title; # all blog post titles\n    select BlogPost.author.email; # all author emails\n    select User.friends.email; # all friends' emails\n\n.. _ref_eql_paths_backlinks:\n\nBacklinks\n---------\n\n.. api-index:: .<\n\nAll examples thus far have traversed links in the *forward direction*, however\nit's also possible to traverse links *backwards* with ``.<`` notation. These\nare called **backlinks**.\n\nStarting from each user, the path below traverses all *incoming* links labeled\n``author`` and returns the union of their sources.\n\n.. code-block:: edgeql\n\n    select User.<author;\n\nThis query works, showing both the ``BlogPost`` and the ``Comment`` in the\ndatabase. However, we can't impose a shape on it:\n\n.. code-block:: edgeql\n\n    select User.<author { text };\n\nAs written, Gel infers the *type* of this expression to be\n:eql:type:`BaseObject`. Why? Because in theory, there may be\nseveral links named ``author`` from different object types\nthat point to ``User``. And there is no guarantee that each\nof these types will have a property called ``text``.\n\n.. note::\n  ``BaseObject`` is the root ancestor of all object types and it only contains\n  a single property, ``id``.\n\nAs such, commonly you'll want to narrow the results to a particular type.\nTo do so, use the :eql:op:`type intersection <isintersect>` operator:\n``[is Foo]``:\n\n.. code-block:: edgeql\n\n    # BlogPost objects that link to the user via a link named author\n    select User.<author[is BlogPost];\n\n    # Comment objects that link to the user via a link named author\n    select User.<author[is Comment];\n\n    # All objects that link to the user via a link named author\n    select User.<author;\n\nOr parsed one step at a time, the above queries can be read as follows:\n\n================================ ===================================\nSyntax                           Meaning\n================================ ===================================\n``User.<``                       Objects that link to the user\n``author``                       via a link named author\n================================ ===================================\n\n================================ ===================================\nSyntax                           Meaning\n================================ ===================================\n``User.<``                       Objects that link to the user\n``author``                       via a link named author\n``[is BlogPost]``                that are ``BlogPost`` objects\n================================ ===================================\n\n================================ ===================================\nSyntax                           Meaning\n================================ ===================================\n``User.<``                       Objects that link to the user\n``author``                       via a link named author\n``[is Comment]``                 that are ``Comment`` objects\n================================ ===================================\n\nBacklinks can be inserted into a schema with the same format, except\nthat the type name (in this case ``User``) doesn't need to be specified.\n\n.. code-block:: sdl-diff\n\n      type User {\n        required email: str;\n        multi friends: User;\n    +   all_links := .<author;\n    +   blog_links := .<author[is BlogPost];\n    +   comment_links := .<author[is Comment];\n      }\n\n      type BlogPost {\n        required title: str;\n        required author: User;\n      }\n      type Comment {\n        required text: str;\n        required author: User;\n      }\n\n.. _ref_eql_paths_link_props:\n\nLink properties\n---------------\n\n.. index:: linkprops\n.. api-index:: @\n\nPaths can also reference :ref:`link properties <ref_datamodel_link_properties>`\nwith ``@`` notation. To demonstrate this, let's add a property to the ``User.\nfriends`` link:\n\n.. code-block:: sdl-diff\n\n      type User {\n        required email: str;\n    -   multi friends: User;\n    +   multi friends: User {\n    +     since: cal::local_date;\n    +   }\n      }\n\nThe following represents a set of all dates on which friendships were formed.\n\n.. code-block:: edgeql\n\n    select User.friends@since;\n\nPath roots\n----------\n\nFor simplicity, all examples above use set references like ``User`` as the root\nof the path; however, the root can be *any expression* returning object types.\nBelow, the root of the path is a *subquery*.\n\n.. code-block:: edgeql-repl\n\n    db> with gel_lovers := (\n    ...   select BlogPost filter .title ilike \"Gel is awesome\"\n    ... )\n    ... select gel_lovers.author;\n\nThis expression returns a set of all ``Users`` who have written a blog post\ntitled \"Gel is awesome\".\n\nFor a full syntax definition, see the :ref:`Reference > Paths\n<ref_reference_paths>`.\n"
  },
  {
    "path": "docs/reference/edgeql/select.rst",
    "content": ".. _ref_eql_select:\n\nSelect\n======\n\n.. api-index:: select\n\nThe ``select`` command retrieves or computes a set of values. We've already\nseen simple queries that select primitive values.\n\n.. code-block:: edgeql-repl\n\n  db> select 'hello world';\n  {'hello world'}\n  db> select [1, 2, 3];\n  {[1, 2, 3]}\n  db> select {1, 2, 3};\n  {1, 2, 3}\n\n\nWith the help of a ``with`` block, we can add filters, ordering, and\npagination clauses.\n\n.. code-block:: edgeql-repl\n\n  db> with x := {1, 2, 3, 4, 5}\n  ... select x\n  ... filter x >= 3;\n  {3, 4, 5}\n  db> with x := {1, 2, 3, 4, 5}\n  ... select x\n  ... order by x desc;\n  {5, 4, 3, 2, 1}\n  db> with x := {1, 2, 3, 4, 5}\n  ... select x\n  ... offset 1 limit 3;\n  {2, 3, 4}\n\nThese queries can also be rewritten to use inline aliases, like so:\n\n.. code-block:: edgeql-repl\n\n  db> select x := {1, 2, 3, 4, 5}\n  ... filter x >= 3;\n\n\n.. _ref_eql_select_objects:\n\nSelecting objects\n-----------------\n\nHowever most queries are selecting *objects* that live in the database. For\ndemonstration purposes, the queries below assume the following schema:\n\n.. code-block:: sdl\n\n    module default {\n      abstract type Person {\n        required name: str { constraint exclusive };\n      }\n\n      type Hero extending Person {\n        secret_identity: str;\n        multi villains := .<nemesis[is Villain];\n      }\n\n      type Villain extending Person {\n        nemesis: Hero;\n      }\n\n      type Movie {\n        required title: str { constraint exclusive };\n        required release_year: int64;\n        multi characters: Person;\n      }\n    }\n\nAnd the following inserts:\n\n.. code-block:: edgeql-repl\n\n  db> insert Hero {\n  ...   name := \"Spider-Man\",\n  ...   secret_identity := \"Peter Parker\"\n  ... };\n  {default::Hero {id: 6be1c9c6...}}\n\n  db> insert Hero {\n  ...   name := \"Iron Man\",\n  ...   secret_identity := \"Tony Stark\"\n  ... };\n  {default::Hero {id: 6bf7115a... }}\n\n  db> for n in { \"Sandman\", \"Electro\", \"Green Goblin\", \"Doc Ock\" }\n  ...   union (\n  ...     insert Villain {\n  ...     name := n,\n  ...     nemesis := (select Hero filter .name = \"Spider-Man\")\n  ...  });\n  {\n    default::Villain {id: 6c22bdf0...},\n    default::Villain {id: 6c22c3d6...},\n    default::Villain {id: 6c22c46c...},\n    default::Villain {id: 6c22c502...},\n  }\n\n  db> insert Villain {\n  ...   name := \"Obadiah Stane\",\n  ...   nemesis := (select Hero filter .name = \"Iron Man\")\n  ... };\n  {default::Villain {id: 6c42c4ec...}}\n\n  db> insert Movie {\n  ...  title := \"Spider-Man: No Way Home\",\n  ...  release_year := 2021,\n  ...  characters := (select Person filter .name in\n  ...    { \"Spider-Man\", \"Sandman\", \"Electro\", \"Green Goblin\", \"Doc Ock\" })\n  ...  };\n  {default::Movie {id: 6c60c28a...}}\n\n  db> insert Movie {\n  ...  title := \"Iron Man\",\n  ...  release_year := 2008,\n  ...  characters := (select Person filter .name in\n  ...   { \"Iron Man\", \"Obadiah Stane\" })\n  ...  };\n  {default::Movie {id: 6d1f430e...}}\n\nLet's start by selecting all ``Villain`` objects in the database. In this\nexample, there are only five. Remember, ``Villain`` is a :ref:`reference\n<ref_eql_set_references>` to the set of all Villain objects.\n\n.. code-block:: edgeql-repl\n\n  db> select Villain;\n  {\n    default::Villain {id: 6c22bdf0...},\n    default::Villain {id: 6c22c3d6...},\n    default::Villain {id: 6c22c46c...},\n    default::Villain {id: 6c22c502...},\n    default::Villain {id: 6c42c4ec...},\n  }\n\n.. note::\n\n  For the sake of readability, the ``id`` values have been truncated.\n\nBy default, this only returns the ``id`` of each object. If serialized to JSON,\nthis result would look like this:\n\n.. code-block::\n\n  [\n    {\"id\": \"6c22bdf0-5c03-11ee-99ff-dfaea4d947ce\"},\n    {\"id\": \"6c22c3d6-5c03-11ee-99ff-734255881e5d\"},\n    {\"id\": \"6c22c46c-5c03-11ee-99ff-c79f24cf638b\"},\n    {\"id\": \"6c22c502-5c03-11ee-99ff-cbacc3918129\"},\n    {\"id\": \"6c42c4ec-5c03-11ee-99ff-872c9906a467\"}\n  ]\n\n\n.. _ref_eql_shapes:\n\nShapes\n------\n\n.. api-index:: select, { }\n\nTo specify which properties to select, we attach a **shape** to ``Villain``. A\nshape can be attached to any object type expression in EdgeQL.\n\n.. code-block:: edgeql-repl\n\n  db> select Villain { id, name };\n  {\n    default::Villain {id: 6c22bdf0..., name: 'Sandman'},\n    default::Villain {id: 6c22c3d6..., name: 'Electro'},\n    default::Villain {id: 6c22c46c..., name: 'Green Goblin'},\n    default::Villain {id: 6c22c502..., name: 'Doc Ock'},\n    default::Villain {id: 6c42c4ec..., name: 'Obadiah Stane'},\n  }\n\nNested shapes\n^^^^^^^^^^^^^\n\nNested shapes can be used to fetch linked objects and their properties. Here we\nfetch all ``Villain`` objects and their nemeses.\n\n.. code-block:: edgeql-repl\n\n  db> select Villain {\n  ...   name,\n  ...   nemesis: { name }\n  ... };\n  {\n    default::Villain {\n      name: 'Sandman',\n      nemesis: default::Hero {name: 'Spider-Man'},\n    },\n    ...\n  }\n\nIn the context of EdgeQL, computed links like ``Hero.villains`` are treated\nidentically to concrete/non-computed links like ``Villain.nemesis``.\n\n.. code-block:: edgeql-repl\n\n  db> select Hero {\n  ...   name,\n  ...   villains: { name }\n  ... };\n  {\n    default::Hero {\n      name: 'Spider-Man',\n      villains: {\n        default::Villain {name: 'Sandman'},\n        default::Villain {name: 'Electro'},\n        default::Villain {name: 'Green Goblin'},\n        default::Villain {name: 'Doc Ock'},\n      },\n    },\n    ...\n  }\n\n\n.. _ref_eql_select_splats:\n\nSplats\n^^^^^^\n\n.. index:: select *, select all\n.. api-index:: *, **, §type§.*, §type§.**, [is §type§].*, [is §type§].**\n\nSplats allow you to select all properties of a type using the asterisk (``*``)\nor all properties of the type and a single level of linked types with a double\nasterisk (``**``).\n\n.. edb:youtube-embed:: 9-I1qjIp3KI\n\nSplats will help you more easily select all properties when using the REPL.\nYou can select all of an object's properties using the single splat:\n\n.. code-block:: edgeql-repl\n\n    db> select Movie {*};\n    {\n      default::Movie {\n        id: 6c60c28a-5c03-11ee-99ff-dfa425012a05,\n        release_year: 2021,\n        title: 'Spider-Man: No Way Home',\n      },\n      default::Movie {\n        id: 6d1f430e-5c03-11ee-99ff-e731e8da06d9,\n        release_year: 2008,\n        title: 'Iron Man'\n      },\n    }\n\nor you can select all of an object's properties and the properties of a single\nlevel of nested objects with the double splat:\n\n.. code-block:: edgeql-repl\n\n    db> select Movie {**};\n    {\n      default::Movie {\n        id: 6c60c28a-5c03-11ee-99ff-dfa425012a05,\n        release_year: 2021,\n        title: 'Spider-Man: No Way Home',\n        characters: {\n          default::Hero {\n            id: 6be1c9c6-5c03-11ee-99ff-63b1127d75f2,\n            name: 'Spider-Man'\n          },\n          default::Villain {\n            id: 6c22bdf0-5c03-11ee-99ff-dfaea4d947ce,\n            name: 'Sandman'\n          },\n          default::Villain {\n            id: 6c22c3d6-5c03-11ee-99ff-734255881e5d,\n            name: 'Electro'\n          },\n          default::Villain {\n            id: 6c22c46c-5c03-11ee-99ff-c79f24cf638b,,\n            name: 'Green Goblin'\n          },\n          default::Villain {\n            id: 6c22c502-5c03-11ee-99ff-cbacc3918129,\n            name: 'Doc Ock'\n          },\n        },\n      },\n      default::Movie {\n        id: 6d1f430e-5c03-11ee-99ff-e731e8da06d9,\n        release_year: 2008,\n        title: 'Iron Man',\n        characters: {\n          default::Hero {\n            id: 6bf7115a-5c03-11ee-99ff-c79c07f0e2db,\n            name: 'Iron Man'\n          },\n          default::Villain {\n            id: 6c42c4ec-5c03-11ee-99ff-872c9906a467,\n            name: 'Obadiah Stane'\n          },\n        },\n      },\n    }\n\n.. note::\n\n    Splats are not yet supported in function bodies.\n\nThe splat expands all properties defined on the type as well as inherited\nproperties:\n\n.. code-block:: edgeql-repl\n\n    db> select Hero {*};\n    {\n      default::Hero {\n        id: 6be1c9c6-5c03-11ee-99ff-63b1127d75f2,\n        name: 'Spider-Man',\n        secret_identity: 'Peter Parker'\n      },\n      default::Hero {\n        id: 6bf7115a-5c03-11ee-99ff-c79c07f0e2db,\n        name: 'Iron Man',\n        secret_identity: 'Tony Stark'\n      },\n    }\n\nThe splat here expands the heroes' names even though the ``name`` property is\nnot defined on the ``Hero`` type but on the ``Person`` type it extends. If we\nwant to select heroes but get only properties defined on the ``Person`` type,\nwe can do this instead:\n\n.. code-block:: edgeql-repl\n\n    db> select Hero {Person.*};\n    {\n      default::Hero {\n        id: 6be1c9c6-5c03-11ee-99ff-63b1127d75f2,\n        name: 'Spider-Man'\n      },\n      default::Hero {\n        id: 6bf7115a-5c03-11ee-99ff-c79c07f0e2db,\n        name: 'Iron Man'\n      },\n    }\n\nIf there are links on our ``Person`` type, we can use ``Person.**`` in a\nsimilar fashion to get all properties and one level of linked object\nproperties, but only for links and properties that are defined on the\n``Person`` type.\n\nYou can use the splat to expand properties using a :ref:`type intersection\n<ref_eql_types_intersection>`. Maybe we want to select all ``Person`` objects\nwith their names but also get any properties defined on the ``Hero`` for those\n``Person`` objects which are also ``Hero`` objects:\n\n.. code-block:: edgeql-repl\n\n    db> select Person {\n    ...   name,\n    ...   [is Hero].*\n    ... };\n    {\n      default::Hero {\n        name: 'Spider-Man',\n        id: 6be1c9c6-5c03-11ee-99ff-63b1127d75f2,\n        secret_identity: 'Peter Parker'\n      },\n      default::Hero {\n        name: 'Iron Man'\n        id: 6bf7115a-5c03-11ee-99ff-c79c07f0e2db,\n        secret_identity: 'Tony Stark'\n      },\n      default::Villain {\n        name: 'Sandman',\n        id: 6c22bdf0-5c03-11ee-99ff-dfaea4d947ce,\n        secret_identity: {}\n      },\n      default::Villain {\n        name: 'Electro',\n        id: 6c22c3d6-5c03-11ee-99ff-734255881e5d,\n        secret_identity: {}\n      },\n      default::Villain {\n        name: 'Green Goblin',\n        id: 6c22c46c-5c03-11ee-99ff-c79f24cf638b,\n        secret_identity: {}\n      },\n      default::Villain {\n        name: 'Doc Ock',\n        id: 6c22c502-5c03-11ee-99ff-cbacc3918129,\n        secret_identity: {}\n      },\n      default::Villain {\n        name: 'Obadiah Stane',\n        id: 6c42c4ec-5c03-11ee-99ff-872c9906a467,\n        secret_identity: {}\n      },\n    }\n\nThe double splat also works with type intersection expansion to expand both\nproperties and links on the specified type.\n\n.. code-block:: edgeql-repl\n\n    db> select Person {\n    ...   name,\n    ...   [is Hero].**\n    ... };\n    {\n      default::Villain {\n        name: 'Sandman',\n        id: 6c22bdf0-5c03-11ee-99ff-dfaea4d947ce,\n        secret_identity: {},\n        villains: {}\n      },\n      default::Villain {\n        name: 'Electro',\n        id: 6c22c3d6-5c03-11ee-99ff-734255881e5d,\n        secret_identity: {},\n        villains: {}\n      },\n      default::Villain {\n        name: 'Green Goblin',\n        id: 6c22c46c-5c03-11ee-99ff-c79f24cf638b,\n        secret_identity: {},\n        villains: {}\n      },\n      default::Villain {\n        name: 'Doc Ock',\n        id: 6c22c502-5c03-11ee-99ff-cbacc3918129,\n        secret_identity: {},\n        villains: {}\n      },\n      default::Villain {\n        name: 'Obadiah Stane',\n        id: 6c42c4ec-5c03-11ee-99ff-872c9906a467,\n        secret_identity: {},\n        villains: {}\n      },\n      default::Hero {\n        name: 'Spider-Man',\n        id: 6be1c9c6-5c03-11ee-99ff-63b1127d75f2,\n        secret_identity: 'Peter Parker',\n        villains: {\n          default::Villain {\n            name: 'Electro',\n            id: 6c22c3d6-5c03-11ee-99ff-734255881e5d\n          },\n          default::Villain {\n            name: 'Sandman',\n            id: 6c22bdf0-5c03-11ee-99ff-dfaea4d947ce\n          },\n          default::Villain {\n            name: 'Doc Ock',\n            id: 6c22c502-5c03-11ee-99ff-cbacc3918129\n          },\n          default::Villain {\n            name: 'Green Goblin',\n            id: 6c22c46c-5c03-11ee-99ff-c79f24cf638b\n          },\n        },\n      },\n    }\n\nWith this query, we get ``name`` for each ``Person`` and all the properties and\none level of links on the ``Hero`` objects. We don't get ``Villain`` objects'\nnemeses because that link is not covered by our double splat which only\nexpands ``Hero`` links. If the ``Villain`` type had properties defined on it,\nwe wouldn't get those with this query either.\n\n\n.. _ref_eql_select_filter:\n\nFiltering\n---------\n\n.. index:: where\n.. api-index:: filter\n\nTo filter the set of selected objects, use a ``filter <expr>`` clause. The\n``<expr>`` that follows the ``filter`` keyword can be *any boolean expression*.\n\nTo reference the ``name`` property of the ``Villain`` objects being selected,\nwe use ``Villain.name``.\n\n.. code-block:: edgeql-repl\n\n  db> select Villain {id, name}\n  ... filter Villain.name = \"Doc Ock\";\n  {default::Villain {id: 6c22c502..., name: 'Doc Ock'}}\n\n\n.. note::\n\n  This query contains two occurrences of ``Villain``. The first\n  (outer) is passed as the argument to ``select`` and refers to the set of all\n  ``Villain`` objects. However the *inner* occurrence is inside the *scope* of\n  the ``select`` statement and refers to the *object being\n  selected*.\n\nHowever, this looks a little clunky, so EdgeQL provides a shorthand: just drop\n``Villain`` entirely and simply use ``.name``. Since we are selecting a set of\nVillains, it's clear from context that ``.name`` must refer to a link/property\nof the ``Villain`` type. In other words, we are in the **scope** of the\n``Villain`` type.\n\n.. code-block:: edgeql-repl\n\n  db> select Villain {name}\n  ... filter .name = \"Doc Ock\";\n  {default::Villain {name: 'Doc Ock'}}\n\n.. warning::\n\n    When using comparison operators like ``=`` or ``!=``, or boolean operators\n    ``and``, ``or``, and ``not``, keep in mind that these operators will\n    produce an empty set if an operand is an empty set. Check out :ref:`our\n    boolean cheatsheet <ref_cheatsheet_boolean>` for more info and help on how\n    to mitigate this if you know your operands may be an empty set.\n\nFiltering by ID\n^^^^^^^^^^^^^^^\n\nTo filter by ``id``, remember to cast the desired ID to :ref:`uuid\n<ref_std_uuid>`:\n\n.. code-block:: edgeql-repl\n\n  db> select Villain {id, name}\n  ... filter .id = <uuid>\"6c22c502-5c03-11ee-99ff-cbacc3918129\";\n  {\n    default::Villain {\n      id: '6c22c502-5c03-11ee-99ff-cbacc3918129',\n      name: 'Doc Ock'\n    }\n  }\n\nNested filters\n^^^^^^^^^^^^^^\n\nFilters can be added at every level of shape nesting. The query below applies a\nfilter to both the selected ``Hero`` objects and their linked ``villains``.\n\n.. code-block:: edgeql-repl\n\n  db> select Hero {\n  ...   name,\n  ...   villains: {\n  ...     name\n  ...   } filter .name like \"%O%\"\n  ... } filter .name ilike \"%man\";\n  {\n    default::Hero {\n      name: 'Spider-Man',\n      villains: {\n        default::Villain {\n          name: 'Doc Ock'\n        }\n      }\n    },\n    default::Hero {\n      name: 'Iron Man',\n      villains: {\n        default::Villain {\n          name: 'Obadiah Stane'\n        }\n      }\n    },\n  }\n\nNote that the *scope* changes inside nested shapes. When we use ``.name`` in\nthe outer ``filter``, it refers to the name of the hero. But when we use\n``.name`` in the nested ``villains`` shape, the scope has changed to\n``Villain``.\n\nFiltering on a known backlink\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nAnother handy use for backlinks is using them to filter and find items\nwhen doing a ``select`` (or an ``update`` or other operation, of course).\nThis can work as a nice shortcut when you have the ID of one object that\nlinks to a second object without a link back to the first.\n\nSpider-Man's villains always have a grudging respect for him, and their names\ncan be displayed to reflect that if we know the ID of a movie that they\nstarred in. Note the ability to :ref:`cast from a uuid <ref_uuid_casting>`\nto an object type.\n\n.. code-block:: edgeql-repl\n\n    db> select Villain filter .<characters =\n    ...   <Movie><uuid>'6c60c28a-5c03-11ee-99ff-dfa425012a05' {\n    ...     name := .name ++ ', who got to see Spider-Man!'\n    ...   };\n    {\n      'Obadiah Stane',\n      'Sandman, who got to see Spider-Man!',\n      'Electro, who got to see Spider-Man!',\n      'Green Goblin, who got to see Spider-Man!',\n      'Doc Ock, who got to see Spider-Man!',\n    }\n\nIn other words, \"select every ``Villain`` object that the ``Movie`` object\nof this ID links to via a link called ``characters``\".\n\nA backlink is naturally not required, however. The same operation without\ntraversing a backlink would look like this:\n\n.. code-block:: edgeql-repl\n\n    db> with movie :=\n    ...   <Movie><uuid>'6c60c28a-5c03-11ee-99ff-dfa425012a05',\n    ...     select movie.characters[is Villain] {\n    ...       name := .name ++ ', who got to see Spider-Man!'\n    ...   };\n\n\n.. _ref_eql_select_order:\n\nFiltering, ordering, and limiting of links\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nClauses like ``filter``, ``order by``, and ``limit`` can be used on links.\nIf no properties of a link are selected, you can place the clauses directly\ninside the shape:\n\n.. code-block:: edgeql\n\n  select User {\n    likes order by .title desc limit 10\n  };\n\nIf properties are selected, place the clauses after the link's shape:\n\n.. code-block:: edgeql\n\n  select User {\n    likes: {\n      id,\n      title\n    } order by .title desc limit 10\n  };\n\n\nOrdering\n--------\n\n.. index:: sorting\n.. api-index:: order by, asc, desc, then, empty first, empty last\n\nOrder the result of a query with an ``order by`` clause.\n\n.. code-block:: edgeql-repl\n\n  db> select Villain { name }\n  ... order by .name;\n  {\n    default::Villain {name: 'Doc Ock'},\n    default::Villain {name: 'Electro'},\n    default::Villain {name: 'Green Goblin'},\n    default::Villain {name: 'Obadiah Stane'},\n    default::Villain {name: 'Sandman'},\n  }\n\nThe expression provided to ``order by`` may be *any* singleton\nexpression, primitive or otherwise.\n\n.. note::\n\n  In Gel all values are orderable. Objects are compared using their ``id``;\n  tuples and arrays are compared element-by-element from left to right. By\n  extension, the generic comparison operators :eql:op:`= <eq>`,\n  :eql:op:`\\< <lt>`, :eql:op:`\\> <gt>`, etc. can be used with any two\n  expressions of the same type.\n\nYou can also order by multiple\nexpressions and specify the *direction* with an ``asc`` (default) or ``desc``\nmodifier.\n\n.. note::\n\n  When ordering by multiple expressions, arrays, or tuples, the leftmost\n  expression/element is compared. If these elements are the same, the next\n  element is used to \"break the tie\", and so on. If all elements are the same,\n  the order is not well defined.\n\n.. code-block:: edgeql-repl\n\n  db> select Movie { title, release_year }\n  ... order by\n  ...   .release_year desc then\n  ...   str_trim(.title) desc;\n  {\n    default::Movie {title: 'Spider-Man: No Way Home', release_year: 2021},\n    ...\n    default::Movie {title: 'Iron Man', release_year: 2008},\n  }\n\nWhen ordering by multiple expressions, each expression is separated with the\n``then`` keyword. For a full reference on ordering, including how empty values\nare handled, see :ref:`Reference > Commands > Select\n<ref_reference_select_order>`.\n\n\n.. _ref_eql_select_pagination:\n\nPagination\n----------\n\n.. api-index:: limit, offset\n\n|Gel| supports ``limit`` and ``offset`` clauses. These are\ntypically used in conjunction with ``order by`` to maintain a consistent\nordering across pagination queries.\n\n.. code-block:: edgeql-repl\n\n  db> select Villain { name }\n  ... order by .name\n  ... offset 2\n  ... limit 2;\n  {\n    default::Villain {name: 'Obadiah Stane'},\n    default::Villain {name: 'Sandman'},\n  }\n\nThe expressions passed to ``limit`` and ``offset`` can be any singleton\n``int64`` expression. This query fetches all Villains except the last (sorted\nby name).\n\n.. code-block:: edgeql-repl\n\n  db> select Villain {name}\n  ... order by .name\n  ... limit count(Villain) - 1;\n  {\n    default::Villain {name: 'Doc Ock'},\n    default::Villain {name: 'Electro'},\n    default::Villain {name: 'Green Goblin'},\n    default::Villain {name: 'Obadiah Stane'}, # no Sandman\n  }\n\nYou may pass the empty set to ``limit`` or ``offset``. Passing the empty set is\neffectively the same as excluding ``limit`` or ``offset`` from your query\n(i.e., no limit or no offset). This is useful if you need to parameterize\n``limit`` and/or ``offset`` but may still need to execute your query without\nproviding one or the other.\n\n.. code-block:: edgeql-repl\n\n  db> select Villain {name}\n  ... order by .name\n  ... offset <optional int64>$offset\n  ... limit <optional int64>$limit;\n  Parameter <int64>$offset (Ctrl+D for empty set `{}`):\n  Parameter <int64>$limit (Ctrl+D for empty set `{}`):\n  {\n    default::Villain {name: 'Doc Ock'},\n    default::Villain {name: 'Electro'},\n    ...\n  }\n\n.. note::\n\n    If you parameterize ``limit`` and ``offset`` and want to reserve the option\n    to pass the empty set, make sure those parameters are ``optional`` as shown\n    in the example above.\n\n\n.. _ref_eql_select_computeds:\n\nComputed fields\n---------------\n\n.. api-index:: :=\n\nShapes can contain *computed fields*. These are EdgeQL expressions that are\ncomputed on the fly during the execution of the query. As with other clauses,\nwe can use :ref:`leading dot notation <ref_dot_notation>` (e.g. ``.name``) to\nrefer to the properties and links of the object type currently *in scope*.\n\n\n.. code-block:: edgeql-repl\n\n  db> select Villain {\n  ...   name,\n  ...   name_upper := str_upper(.name)\n  ... };\n  {\n    default::Villain {\n      id: 6c22bdf0...,\n      name: 'Sandman',\n      name_upper: 'SANDMAN',\n    },\n    ...\n  }\n\nAs with nested filters, the *current scope* changes inside nested shapes.\n\n.. code-block:: edgeql-repl\n\n  db> select Villain {\n  ...   id,\n  ...   name,\n  ...   name_upper := str_upper(.name),\n  ...   nemesis: {\n  ...     secret_identity,\n  ...     real_name_upper := str_upper(.secret_identity)\n  ...   }\n  ... };\n  {\n    default::Villain {\n      id: 6c22bdf0...,\n      name: 'Sandman',\n      name_upper: 'SANDMAN',\n      nemesis: default::Hero {\n        secret_identity: 'Peter Parker',\n        real_name_upper: 'PETER PARKER',\n      },\n    },\n    ...\n  }\n\n\n.. _ref_eql_select_backlinks:\n\nBacklinks\n---------\n\n.. api-index:: .<\n\nFetching backlinks is a common use case for computed fields. To demonstrate\nthis, let's fetch a list of all movies starring a particular Hero.\n\n.. code-block:: edgeql-repl\n\n  db> select Hero {\n  ...   name,\n  ...   movies := .<characters[is Movie] { title }\n  ... } filter .name = \"Iron Man\";\n  {\n    default::Hero {\n      name: 'Iron Man',\n      movies: {\n        default::Movie {title: 'Iron Man'}\n      },\n    },\n  }\n\n.. note::\n\n  The computed backlink ``movies`` is a combination of the *backlink\n  operator* ``.<`` and a type intersection ``[is Movie]``. For a full\n  reference on backlink syntax, see :ref:`EdgeQL > Paths\n  <ref_eql_paths_backlinks>`.\n\nInstead of re-declaring backlinks inside every query where they're needed, it's\ncommon to add them directly into your schema as computed links.\n\n.. code-block:: sdl-diff\n\n      abstract type Person {\n        required name: str {\n          constraint exclusive;\n        };\n    +   multi movies := .<characters[is Movie]\n      }\n\n.. note::\n\n  In the example above, the ``Person.movies`` is a ``multi`` link. Including\n  these keywords is optional, since Gel can infer this from the assigned\n  expression ``.<characters[is Movie]``. However, it's a good practice to\n  include the explicit keywords to make the schema more readable and \"sanity\n  check\" the cardinality.\n\nThis simplifies future queries; ``Person.movies`` can now be traversed in\nshapes just like a non-computed link.\n\n.. code-block:: edgeql\n\n  select Hero {\n    name,\n    movies: { title }\n  } filter .name = \"Iron Man\";\n\n.. _ref_eql_select_subqueries:\n\nSubqueries\n----------\n\n.. index:: nested queries, composition, composing queries, composable,\n           embedded queries, embedding queries\n\nThere's no limit to the complexity of computed expressions. EdgeQL is designed\nto be fully composable; entire queries can be embedded inside each other.\nBelow, we use a subquery to select all movies containing a villain's nemesis.\n\n.. code-block:: edgeql-repl\n\n  db> select Villain {\n  ...   name,\n  ...   nemesis_name := .nemesis.name,\n  ...   movies_with_nemesis := (\n  ...     select Movie { title }\n  ...     filter Villain.nemesis in .characters\n  ...   )\n  ... };\n  {\n    default::Villain {\n      name: 'Sandman',\n      nemesis_name: 'Spider-Man',\n      movies_with_nemesis: {\n        default::Movie {title: 'Spider-Man: No Way Home'}\n      }\n    },\n    ...\n  }\n\n.. _ref_eql_select_polymorphic:\n\nPolymorphic queries\n-------------------\n\n.. index:: polymorphism\n\nAll queries thus far have referenced concrete object types: ``Hero`` and\n``Villain``. However, both of these types extend the abstract type ``Person``,\nfrom which they inherit the ``name`` property.\n\nPolymorphic sets\n^^^^^^^^^^^^^^^^\n\nIt's possible to directly query all ``Person`` objects; the resulting set will\nbe a mix of ``Hero`` and ``Villain`` objects (and possibly other subtypes of\n``Person``, should they be declared).\n\n.. code-block:: edgeql-repl\n\n  db> select Person { name };\n  {\n    default::Hero {name: 'Spider-Man'},\n    default::Hero {name: 'Iron Man'},\n    default::Villain {name: 'Doc Ock'},\n    default::Villain {name: 'Obadiah Stane'},\n    ...\n  }\n\nYou may also encounter such \"mixed sets\" when querying a link that points to an\nabstract type (such as ``Movie.characters``) or a :eql:op:`union type\n<typeor>`.\n\n.. code-block:: edgeql-repl\n\n  db> select Movie {\n  ...   title,\n  ...   characters: {\n  ...     name\n  ...   }\n  ... }\n  ... filter .title = \"Iron Man 2\";\n  {\n    default::Movie {\n      title: 'Iron Man',\n      characters: {\n        default::Villain {name: 'Obadiah Stane'},\n        default::Hero {name: 'Iron Man'}\n      }\n    }\n  }\n\n\nPolymorphic fields\n^^^^^^^^^^^^^^^^^^\n\n.. api-index:: [is §type§].\n\nWe can fetch different properties *conditional* on the subtype of each object\nby prefixing property/link references with ``[is <type>]``. This is known as a\n**polymorphic query**.\n\n.. code-block:: edgeql-repl\n\n  db> select Person {\n  ...   name,\n  ...   secret_identity := [is Hero].secret_identity,\n  ...   number_of_villains := count([is Hero].villains),\n  ...   nemesis := [is Villain].nemesis {\n  ...     name\n  ...   }\n  ... };\n  {\n    ...\n    default::Villain {\n      name: 'Obadiah Stane',\n      secret_identity: {},\n      number_of_villains: 0,\n      nemesis: default::Hero {\n        name: 'Iron Man'\n      }\n    },\n    default::Hero {\n      name: 'Spider-Man',\n      secret_identity: 'Peter Parker',\n      number_of_villains: 4,\n      nemesis: {}\n    },\n    ...\n  }\n\nThis syntax might look familiar; it's the :ref:`type intersection\n<ref_eql_types_intersection>` again. In effect, this operator conditionally\nreturns the value of the referenced field only if the object matches a\nparticular type. If the match fails, an empty set is returned.\n\nThe line ``secret_identity := [is Hero].secret_identity`` is a bit redundant,\nsince the computed property has the same name as the polymorphic field. In\nthese cases, EdgeQL supports a shorthand.\n\n.. code-block:: edgeql-repl\n\n  db> select Person {\n  ...   name,\n  ...   [is Hero].secret_identity,\n  ...   [is Villain].nemesis: {\n  ...     name\n  ...   }\n  ... };\n  {\n    ...\n    default::Villain {\n      name: 'Obadiah Stane',\n      secret_identity: {},\n      nemesis: default::Hero {name: 'Iron Man'}\n    },\n    default::Hero {\n      name: 'Spider-Man',\n      secret_identity: 'Peter Parker',\n      nemesis: {}\n    },\n    ...\n  }\n\nFiltering polymorphic links\n^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nRelatedly, it's possible to filter polymorphic links by subtype. Below, we\nexclusively fetch the ``Movie.characters`` of type ``Hero``.\n\n.. code-block:: edgeql-repl\n\n  db> select Movie {\n  ...   title,\n  ...   characters[is Hero]: {\n  ...     secret_identity\n  ...   },\n  ... };\n  {\n    default::Movie {\n      title: 'Spider-Man: No Way Home',\n      characters: {default::Hero {secret_identity: 'Peter Parker'}},\n    },\n    default::Movie {\n      title: 'Iron Man',\n      characters: {default::Hero {secret_identity: 'Tony Stark'}},\n    },\n    ...\n  }\n\nAccessing types in polymorphic queries\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nWhile the type of an object is displayed alongside the results of polymorphic\nqueries run in the REPL, this is simply a convenience of the REPL and not a\nproperty that can be accessed. This is particularly noticeable if you cast an\nobject to ``json``, making it impossible to determine the type if the query is\npolymorphic. First, the result of a query as the REPL presents it with type\nannotations displayed:\n\n.. code-block:: edgeql-repl\n\n    db> select Person limit 1;\n    {default::Villain {id: 6c22bdf0-5c03-11ee-99ff-dfaea4d947ce}}\n\nNote the type ``default::Villain``, which is displayed for the user's\nconvenience but is not actually part of the data returned. This is the same\nquery with the result cast as ``json`` to show only the data returned:\n\n.. code-block:: edgeql-repl\n\n    db> select <json>Person limit 1;\n    {Json(\"{\\\"id\\\": \\\"6c22bdf0-5c03-11ee-99ff-dfaea4d947ce\\\"}\")}\n\n.. note::\n\n    We will continue to cast subesequent examples in this section to ``json``,\n    not because this is required for any of the functionality being\n    demonstrated, but to remove the convenience type annotations provided by\n    the REPL and make it easier to see what data is actually being returned by\n    the query.\n\nThe type of an object is found inside ``__type__`` which is a link that\ncarries various information about the object's type, including its ``name``.\n\n.. code-block:: edgeql-repl\n\n    db> select <json>Person {\n    ...  __type__: {\n    ...    name\n    ...    }\n    ...  } limit 1;\n    {Json(\"{\\\"__type__\\\": {\\\"name\\\": \\\"default::Villain\\\"}}\")}\n\nThis information can be pulled into the top level by assigning a name to\nthe ``name`` property inside ``__type__``:\n\n.. code-block:: edgeql-repl\n\n    db> select <json>Person { type := .__type__.name } limit 1;\n    {Json(\"{\\\"type\\\": \\\"default::Villain\\\"}\")}\n\nThere is nothing magical about ``__type__``; it is a simple link to an object\nof the type ``ObjectType`` which contains all of the possible information to\nknow about the type of the current object. The splat operator can be used to\nsee this object's makeup, while the double splat operator produces too much\noutput to show on this page. Playing around with the splat and double splat\noperator inside ``__type__`` is a quick way to get some insight into the\ninternals of Gel.\n\n.. code-block:: edgeql-repl\n\n    db> select Person.__type__ {*} limit 1;\n    {\n      schema::ObjectType {\n        id: 48be3a94-5bf3-11ee-bd60-0b44b607e31d,\n        name: 'default::Hero',\n        internal: false,\n        builtin: false,\n        computed_fields: [],\n        final: false,\n        is_final: false,\n        abstract: false,\n        is_abstract: false,\n        inherited_fields: [],\n        from_alias: false,\n        is_from_alias: false,\n        expr: {},\n        compound_type: false,\n        is_compound_type: false,\n      },\n    }\n\n.. _ref_eql_select_free_objects:\n\nFree objects\n------------\n\n.. index:: ad hoc type\n\nTo select several values simultaneously, you can \"bundle\" them into a \"free\nobject\". Free objects are a set of key-value pairs that can contain any\nexpression. Here, the term \"free\" is used to indicate that the object in\nquestion is not an instance of a particular *object type*; instead, it's\nconstructed ad hoc inside the query.\n\n.. code-block:: edgeql-repl\n\n  db> select {\n  ...   my_string := \"This is a string\",\n  ...   my_number := 42,\n  ...   several_numbers := {1, 2, 3},\n  ...   all_heroes := Hero { name }\n  ... };\n  {\n    {\n      my_string: 'This is a string',\n      my_number: 42,\n      several_numbers: {1, 2, 3},\n      all_heroes: {\n        default::Hero {name: 'Spider-Man'},\n        default::Hero {name: 'Iron Man'},\n      },\n    },\n  }\n\n\nNote that the result is a *singleton* but each key corresponds to a set of\nvalues, which may have any cardinality.\n\n.. _ref_eql_select_with:\n\nWith block\n----------\n\nAll top-level EdgeQL statements (``select``, ``insert``, ``update``, and\n``delete``) can be prefixed with a ``with`` block. These blocks let you declare\nstandalone expressions that can be used in your query.\n\n.. code-block:: edgeql-repl\n\n  db> with hero_name := \"Iron Man\"\n  ... select Hero { secret_identity }\n  ... filter .name = hero_name;\n  {default::Hero {secret_identity: 'Tony Stark'}}\n\n\nFor full documentation on ``with``, see :ref:`EdgeQL > With <ref_eql_with>`.\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Reference > Commands > Select <ref_eql_statements_select>`\n  * - :ref:`Cheatsheets > Selecting data <ref_cheatsheet_select>`\n"
  },
  {
    "path": "docs/reference/edgeql/sets.rst",
    "content": ".. _ref_eql_sets:\n\nSets\n====\n\n.. _ref_eql_everything_is_a_set:\n\nEverything is a set\n-------------------\n\n.. index:: multiset, cardinality, empty set, singleton\n\nAll values in EdgeQL are actually **sets**: a collection of values of a given\n**type**. All elements of a set must have the same type. The number of items in\na set is known as its **cardinality**. A set with a cardinality of zero is\nreferred to as an **empty set**. A set with a cardinality of one is known as a\n**singleton**.\n\n.. _ref_eql_set_constructor:\n\nConstructing sets\n-----------------\n\n.. api-index:: {§expr [\\, ...]§}, union\n\nSet literals are declared with *set constructor* syntax: a comma-separated\nlist of values inside a set of ``{curly braces}``.\n\n.. code-block:: edgeql-repl\n\n  db> select {\"set\", \"of\", \"strings\"};\n  {\"set\", \"of\", \"strings\"}\n  db> select {1, 2, 3};\n  {1, 2, 3}\n\nIn actuality, curly braces are a syntactic sugar for the\n:eql:op:`union` operator. The  previous examples are perfectly\nequivalent to the following:\n\n.. code-block:: edgeql-repl\n\n  db> select \"set\" union \"of\" union \"strings\";\n  {\"set\", \"of\", \"strings\"}\n  db> select 1 union 2 union 3;\n  {1, 2, 3}\n\nA consequence of this is that nested sets are *flattened*.\n\n.. code-block:: edgeql-repl\n\n  db> select {1, {2, {3, 4}}};\n  {1, 2, 3, 4}\n  db> select 1 union (2 union (3 union 4));\n  {1, 2, 3, 4}\n\nAll values in a set must have the same type. For convenience, Gel will\n*implicitly cast* values to other types, as long as there is no loss of\ninformation (e.g. converting a ``int16`` to an ``int64``). For a full\nreference, see the casting table in :ref:`Standard Library > Casts\n<ref_eql_casts_table>`.\n\n.. code-block:: edgeql-repl\n\n  db> select {1, 1.5};\n  {1.0, 1.5}\n  db> select {1, 1234.5678n};\n  {1.0n, 1234.5678n}\n\nAttempting to declare a set containing elements of *incompatible* types is not\npermitted.\n\n.. code-block:: edgeql-repl\n\n  db> select {\"apple\", 3.14};\n  error: QueryError: set constructor has arguments of incompatible types\n  'std::str' and 'std::float64'\n\n.. note::\n\n  Types are considered *compatible* if one can be implicitly cast into the\n  other. For reference on implicit castability, see :ref:`Standard Library >\n  Casts <ref_eql_casts_table>`.\n\n.. _ref_eql_set_literals_are_singletons:\n\nLiterals are singletons\n-----------------------\n\nLiteral syntax like ``6`` or ``\"hello world\"`` is just a shorthand for\ndeclaring a *singleton* of a given type. This is why the literals we created in\nthe previous section were printed inside braces: to indicate that these values\nare *actually sets*.\n\n.. code-block:: edgeql-repl\n\n  db> select 6;\n  {6}\n  db> select \"hello world\";\n  {\"hello world\"}\n\nWrapping a literal in curly braces does not change the meaning of the\nexpression. For instance, ``\"hello world\"`` is *exactly equivalent* to\n``{\"hello world\"}``.\n\n.. code-block:: edgeql-repl\n\n  db> select {\"hello world\"};\n  {\"hello world\"}\n  db> select \"hello world\" = {\"hello world\"};\n  {true}\n\n\nYou can retrieve the cardinality of a set with the :eql:func:`count` function.\n\n.. code-block:: edgeql-repl\n\n  db> select count('aaa');\n  {1}\n  db> select count({'aaa', 'bbb'});\n  {2}\n\n\n.. _ref_eql_empty_sets:\n\nEmpty sets\n----------\n\n.. index:: null, exists\n\nThe reason EdgeQL introduced the concept of *sets* is to eliminate the concept\nof ``null``. In SQL databases ``null`` is a special value denoting the absence\nof data; in Gel the absence of data is just an empty set.\n\n.. note::\n\n  Why is the existence of NULL a problem? Put simply, it's an edge case that\n  permeates all of SQL and is often handled inconsistently in different\n  circumstances. A number of specific inconsistencies are documented in detail\n  in the `We Can Do Better Than SQL\n  <https://www.geldata.com/blog/we-can-do-better-than-sql#null-a-bag-of-surprises>`_\n  post on the Gel blog. For broader context, see Tony Hoare's talk\n  `\"The Billion Dollar Mistake\" <https://bit.ly/3H238oG>`_.\n\n\nDeclaring empty sets isn't as simple as ``{}``; in EdgeQL, all expressions are\n*strongly typed*, including empty sets. With nonempty sets (like ``{1, 2, 3}``)\n, the type is inferred from the set's contents (``int64``). But with empty sets\nthis isn't possible, so an *explicit cast* is required.\n\n.. code-block:: edgeql-repl\n\n  db> select {};\n  error: QueryError: expression returns value of indeterminate type\n    ┌─ query:1:8\n    │\n  1 │ select {};\n    │        ^^ Consider using an explicit type cast.\n\n  db> select <int64>{};\n  {}\n  db> select <str>{};\n  {}\n  db> select count(<str>{});\n  {0}\n\nYou can check whether or not a set is *empty* with the :eql:op:`exists`\noperator.\n\n.. code-block:: edgeql-repl\n\n  db> select exists <str>{};\n  {false}\n  db> select exists {'not', 'empty'};\n  {true}\n\n\n.. _ref_eql_set_references:\n\nSet references\n--------------\n\n.. index:: pointer, alias, with\n\nA set reference is a *pointer* to a set of values. Most commonly, this is the\nname of an :ref:`object type <ref_datamodel_object_types>` you've declared in\nyour schema.\n\n.. code-block:: edgeql-repl\n\n  db> select User;\n  {\n    default::User {id: 9d2ce01c-35e8-11ec-acc3-83b1377efea0},\n    default::User {id: b0e0dd0c-35e8-11ec-acc3-abf1752973be},\n  }\n  db> select count(User);\n  {2}\n\nIt may also be an *alias*, which can be defined in a :ref:`with block\n<ref_eql_with>` or as an :ref:`alias declaration <ref_eql_sdl_aliases>` in your\nschema.\n\n.. note::\n\n  In the example above, the ``User`` object type was declared inside the\n  ``default`` module. If it was in a non-``default`` module (say,\n  ``my_module``, we would need to use its *fully-qualified* name.\n\n  .. code-block:: edgeql-repl\n\n    db> select my_module::User;\n\n\n.. _ref_eql_set_distinct:\n\nMultisets\n---------\n\n.. api-index:: distinct\n\nTechnically sets in Gel are actually *multisets*, because they can contain\nduplicates of the same element. To eliminate duplicates, use the\n:eql:op:`distinct` set operator.\n\n.. code-block:: edgeql-repl\n\n  db> select {'aaa', 'aaa', 'aaa'};\n  {'aaa', 'aaa', 'aaa'}\n  db> select distinct {'aaa', 'aaa', 'aaa'};\n  {'aaa'}\n\n.. _ref_eql_set_in:\n\nChecking membership\n-------------------\n\n.. api-index:: §element§ in §set§\n\nUse the :eql:op:`in` operator to check whether a set contains a particular\nelement.\n\n.. code-block:: edgeql-repl\n\n  db> select 'aaa' in {'aaa', 'bbb', 'ccc'};\n  {true}\n  db> select 'ddd' in {'aaa', 'bbb', 'ccc'};\n  {false}\n\n\n.. _ref_eql_set_union:\n\nMerging sets\n------------\n\n.. api-index:: union\n\nUse the :eql:op:`union` operator to merge two sets.\n\n.. code-block:: edgeql-repl\n\n  db> select 'aaa' union 'bbb' union 'ccc';\n  {'aaa', 'bbb', 'ccc'}\n  db> select {1, 2} union {3.1, 4.4};\n  {1.0, 2.0, 3.1, 4.4}\n\nFinding common members\n----------------------\n\n.. api-index:: intersect\n\nUse the :eql:op:`intersect` operator to find common members between two sets.\n\n.. code-block:: edgeql-repl\n\n    db> select {1, 2, 3, 4, 5} intersect {3, 4, 5, 6, 7};\n    {3, 5, 4}\n    db> select {'a', 'b', 'c', 'd', 'e'} intersect {'c', 'd', 'e', 'f', 'g'};\n    {'e', 'd', 'c'}\n\nIf set members are repeated in both sets, they will be repeated in the set\nproduced by :eql:op:`intersect` the same number of times they are repeated in\nboth of the operand sets.\n\n.. code-block:: edgeql-repl\n\n    db> select {0, 1, 1, 1, 2, 3, 3} intersect {1, 3, 3, 3, 3, 3};\n    {1, 3, 3}\n\nIn this example, ``1`` appears three times in the first set but only once in\nthe second, so it appears only once in the result. ``3`` appears twice in the\nfirst set and five times in the second. Both ``3`` appearances in the first set\nare overlapped by ``3`` appearances in the second, so they both end up in the\nresulting set.\n\n\nRemoving common members\n-----------------------\n\n.. api-index:: except\n\nUse the :eql:op:`except` operator to leave only the members in the first set\nthat do not appear in the second set.\n\n.. code-block:: edgeql-repl\n\n    db> select {1, 2, 3, 4, 5} except {3, 4, 5, 6, 7};\n    {1, 2}\n    db> select {'a', 'b', 'c', 'd', 'e'} except {'c', 'd', 'e', 'f', 'g'};\n    {'b', 'a'}\n\nWhen :eql:op:`except` eliminates a common member that is repeated, it never\neliminates more than the number of instances of that member appearing in the\nsecond set.\n\n.. code-block:: edgeql-repl\n\n    db> select {0, 1, 1, 1, 2, 3, 3} except {1, 3, 3, 3, 3, 3};\n    {0, 1, 1, 2}\n\nIn this example, both sets share the member ``1``. The first set contains three\nof them while the second contains only one. The result retains two ``1``\nmembers from the first set since the sets shared only a single ``1`` in common.\nThe second set has five ``3`` members to the first set's two, so both of the\nfirst set's ``3`` members are eliminated from the resulting set.\n\n\n.. _ref_eql_set_coalesce:\n\nCoalescing\n----------\n\n.. index:: empty set, default values, optional\n.. api-index:: ??\n\nOccasionally in queries, you need to handle the case where a set is empty. This\ncan be achieved with a coalescing operator :eql:op:`?? <coalesce>`. This is\ncommonly used to provide default values for optional :ref:`query parameters\n<ref_eql_params>`.\n\n.. code-block:: edgeql-repl\n\n  db> select 'value' ?? 'default';\n  {'value'}\n  db> select <str>{} ?? 'default';\n  {'default'}\n\n.. note::\n\n  Coalescing is an example of a function/operator with :ref:`optional inputs\n  <ref_sdl_function_typequal>`. By default, passing an empty set into a\n  function/operator will \"short circuit\" the operation and return an empty set.\n  However it's possible to mark inputs as *optional*, in which case the\n  operation will be defined over empty sets. Another example is\n  :eql:func:`count`, which returns ``{0}`` when an empty set is passed as\n  input.\n\n.. _ref_eql_set_type_filter:\n\nInheritance\n-----------\n\n.. index:: type intersection, backlinks\n.. api-index:: §expr§[is §type§]\n\n|Gel| schemas support :ref:`inheritance <ref_datamodel_objects_inheritance>`;\ntypes (usually object types) can extend one or more other types. For instance\nyou may declare an abstract object type ``Media`` that is extended by ``Movie``\nand ``TVShow``.\n\n.. code-block:: sdl\n\n    abstract type Media {\n      required title: str;\n    }\n\n    type Movie extending Media {\n      release_year: int64;\n    }\n\n    type TVShow extending Media {\n      num_seasons: int64;\n    }\n\nA set of type ``Media`` may contain both ``Movie`` and ``TVShow``\nobjects.\n\n.. code-block:: edgeql-repl\n\n  db> select Media;\n  {\n    default::Movie {id: 9d2ce01c-35e8-11ec-acc3-83b1377efea0},\n    default::Movie {id: 3bfe4900-3743-11ec-90ee-cb73d2740820},\n    default::TVShow {id: b0e0dd0c-35e8-11ec-acc3-abf1752973be},\n  }\n\nWe can use the *type intersection* operator ``[is <type>]`` to restrict the\nelements of a set by subtype.\n\n.. code-block:: edgeql-repl\n\n  db> select Media[is Movie];\n  {\n    default::Movie {id: 9d2ce01c-35e8-11ec-acc3-83b1377efea0},\n    default::Movie {id: 3bfe4900-3743-11ec-90ee-cb73d2740820},\n  }\n  db> select Media[is TVShow];\n  {\n    default::TVShow {id: b0e0dd0c-35e8-11ec-acc3-abf1752973be}\n  }\n\nType filters are commonly used in conjunction with :ref:`backlinks\n<ref_eql_select_backlinks>`.\n\n.. _ref_eql_set_aggregate:\n\nAggregate vs element-wise operations\n------------------------------------\n\n.. index:: cartesian product\n\nEdgeQL provides a large library of built-in functions and operators for\nhandling data structures. It's useful to consider functions/operators as either\n*aggregate* or *element-wise*.\n\n.. note::\n\n  This is an over-simplification, but it's a useful mental model when just\n  starting out with Gel. For a more complete guide, see :ref:`Reference >\n  Cardinality <ref_reference_cardinality>`.\n\n*Aggregate* operations are applied to the set *as a whole*; they\naccept a set with arbitrary cardinality and return a *singleton* (or perhaps an\nempty set if the input was also empty).\n\n.. code-block:: edgeql-repl\n\n  db> select count({'aaa', 'bbb'});\n  {2}\n  db> select sum({1, 2, 3});\n  {6}\n  db> select min({1, 2, 3});\n  {1}\n\nElement-wise operations are applied on *each element* of a set.\n\n.. code-block:: edgeql-repl\n\n  db> select str_upper({'aaa', 'bbb'});\n  {'AAA', 'BBB'}\n  db> select {1, 2, 3} ^ 2;\n  {1, 4, 9}\n  db> select str_split({\"hello world\", \"hi again\"}, \" \");\n  {[\"hello\", \"world\"], [\"hi\", \"again\"]}\n\nWhen an *element-wise* operation accepts two or more inputs, the operation is\napplied to all possible combinations of inputs; in other words, the operation\nis applied to the *Cartesian product* of the inputs.\n\n.. code-block:: edgeql-repl\n\n  db> select {'aaa', 'bbb'} ++ {'ccc', 'ddd'};\n  {'aaaccc', 'aaaddd', 'bbbccc', 'bbbddd'}\n\nAccordingly, operations involving an empty set typically return an empty set.\nIn constrast, aggregate operations like :eql:func:`count` are able to operate\non empty sets.\n\n.. code-block:: edgeql-repl\n\n  db> select <str>{} ++ 'ccc';\n  {}\n  db> select count(<str>{});\n  {0}\n\nFor a more complete discussion of cardinality, see :ref:`Reference >\nCardinality <ref_reference_cardinality>`.\n\n.. _ref_eql_set_array_conversion:\n\nConversion to/from arrays\n-------------------------\n\n.. api-index:: array_unpack, array_agg\n\nBoth arrays and sets are collections of values that share a type. EdgeQL\nprovides ways to convert one into the other.\n\n.. note::\n\n  Remember that *all values* in EdgeQL are sets; an array literal is just a\n  singleton set of arrays. So here, \"converting\" a set into an array means\n  converting a set of type ``x`` into another set with cardinality\n  ``1`` (a singleton) and type ``array<x>``.\n\n.. code-block:: edgeql-repl\n\n  db> select array_unpack([1,2,3]);\n  {1, 2, 3}\n  db> select array_agg({1,2,3});\n  {[1, 2, 3]}\n\nArrays are an *ordered collection*, whereas sets are generally unordered\n(unless explicitly sorted with an ``order by`` clause in a :ref:`select\n<ref_eql_select_order>` statement).\n\nElement-wise scalar operations in the standard library cannot be applied to\narrays, so sets of scalars are typically easier to manipulate, search, and\ntransform than arrays.\n\n.. code-block:: edgeql-repl\n\n  db> select str_trim({'  hello', 'world  '});\n  {'hello', 'world'}\n  db> select str_trim(['  hello', 'world  ']);\n  error: QueryError: function \"str_trim(arg0: array<std::str>)\" does not exist\n\nSome :ref:`aggregate <ref_reference_cardinality_aggregate>` operations have\nanalogs that operate on arrays. For instance, the set function\n:eql:func:`count` is analogous to the array function :eql:func:`len`.\n\n\nReference\n---------\n\n.. list-table::\n\n  * - Set operators\n    - :eql:op:`distinct` :eql:op:`in` :eql:op:`union`\n      :eql:op:`exists` :eql:op:`if..else`\n      :eql:op:`?? <coalesce>` :eql:op:`detached`\n      :eql:op:`[is type] <isintersect>`\n  * - Utility functions\n    - :eql:func:`count` :eql:func:`enumerate`\n  * - Cardinality assertion\n    - :eql:func:`assert_distinct` :eql:func:`assert_single`\n      :eql:func:`assert_exists`\n"
  },
  {
    "path": "docs/reference/edgeql/transactions.rst",
    "content": ".. _ref_eql_transactions:\n\nTransactions\n============\n\n.. api-index:: start transaction, declare savepoint, release savepoint,\n               rollback to savepoint, rollback, commit\n\nEdgeQL supports atomic transactions. The transaction API consists\nof several commands:\n\n:eql:stmt:`start transaction`\n  Start a transaction, specifying the isolation level, access mode (``read\n  only`` vs ``read write``), and deferrability.\n\n:eql:stmt:`declare savepoint`\n  Establish a new savepoint within the current transaction. A savepoint is a\n  intermediate point in a transaction flow that provides the ability to\n  partially rollback a transaction.\n\n:eql:stmt:`release savepoint`\n  Destroys a savepoint previously defined in the current transaction.\n\n:eql:stmt:`rollback to savepoint`\n  Rollback to the named savepoint. All changes made after the savepoint\n  are discarded. The savepoint remains valid and can be rolled back\n  to again later, if needed.\n\n:eql:stmt:`rollback`\n  Rollback the entire transaction. All updates made within the transaction are\n  discarded.\n\n:eql:stmt:`commit`\n  Commit the transaction. All changes made by the transaction become visible\n  to others and will persist if a crash occurs.\n\n\nClient libraries\n----------------\n\nThere is rarely a reason to use these commands directly. All Gel client\nlibraries provide dedicated transaction APIs that handle transaction creation\nunder the hood.\n\nExamples below show a transaction that sends 10 cents from the account\nof a ``BankCustomer`` called ``'Customer1'`` to ``BankCustomer`` called\n``'Customer2'``. The equivalent Gel schema and queries are:\n\n.. code-block::\n\n  module default {\n    type BankCustomer {\n      required name: str;\n      required balance: int64;\n    }\n  }\n  update BankCustomer\n      filter .name = 'Customer1'\n      set { bank_balance := .bank_balance -10 };\n  update BankCustomer\n      filter .name = 'Customer2'\n      set { bank_balance := .bank_balance +10 }\n\nTypeScript/JS\n^^^^^^^^^^^^^\n\nUsing an EdgeQL query string:\n\n.. code-block:: typescript\n\n  client.transaction(async tx => {\n    await tx.execute(`update BankCustomer\n      filter .name = 'Customer1'\n      set { bank_balance := .bank_balance -10 }`);\n    await tx.execute(`update BankCustomer\n      filter .name = 'Customer2'\n      set { bank_balance := .bank_balance +10 }`);\n  });\n\nUsing the querybuilder:\n\n.. code-block:: typescript\n\n  const query1 = e.update(e.BankCustomer, () => ({\n    filter_single: { name: \"Customer1\" },\n    set: {\n      bank_balance: { \"-=\":  10 }\n    },\n  }));\n  const query2 = e.update(e.BankCustomer, () => ({\n    filter_single: { name: \"Customer2\" },\n    set: {\n      bank_balance: { \"+=\":  10 }\n    },\n  }));\n\n  client.transaction(async (tx) => {\n    await query1.run(tx);\n    await query2.run(tx);\n  });\n\nFull documentation at :ref:`Client Libraries > TypeScript/JS <gel-js-intro>`;\n\nPython\n^^^^^^\n\n.. code-block:: python\n\n  async for tx in client.transaction():\n      async with tx:\n          await tx.execute(\"\"\"update BankCustomer\n              filter .name = 'Customer1'\n              set { bank_balance := .bank_balance -10 };\"\"\")\n          await tx.execute(\"\"\"update BankCustomer\n              filter .name = 'Customer2'\n              set { bank_balance := .bank_balance +10 };\"\"\")\n\nFull documentation at :ref:`Client Libraries > Python <gel-python-intro>`;\n\nGolang\n^^^^^^\n\n.. code-block:: go\n\n\terr = client.Tx(ctx, func(ctx context.Context, tx *gel.Tx) error {\n\t\tquery1 := `update BankCustomer\n\t\t\tfilter .name = 'Customer1'\n\t\t\tset { bank_balance := .bank_balance -10 };`\n\t\tif e := tx.Execute(ctx, query1); e != nil {\n\t\t\treturn e\n\t\t}\n\t\tquery2 := `update BankCustomer\n\t\t\tfilter .name = 'Customer2'\n\t\t\tset { bank_balance := .bank_balance +10 };`\n\t\tif e := tx.Execute(ctx, query2); e != nil {\n\t\t\treturn e\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\nFull documentation at `Client Libraries > Go <gel-go>`_.\n\nRust\n^^^^\n\n.. code-block:: rust\n\n  let balance_change_query = \"update BankCustomer\n    filter .name = <str>$0\n    set { bank_balance := .bank_balance + <int32>$1 }\";\n\n  client\n      .transaction(|mut conn| async move {\n          conn.execute(balance_change_query, &(\"Customer1\", -10))\n              .await\n              .expect(\"Execute should have worked\");\n          conn.execute(balance_change_query, &(\"Customer2\", 10))\n              .await\n              .expect(\"Execute should have worked\");\n          Ok(())\n      })\n      .await\n      .expect(\"Transaction should have worked\");\n\n.. XXX: Add Rust docs\n.. Full documentation at :ref:`Client Libraries > Rust <ref_rust_index>`.\n\n.. _gel-go: https://pkg.go.dev/github.com/geldata/gel-go"
  },
  {
    "path": "docs/reference/edgeql/types.rst",
    "content": ".. _ref_eql_types:\n\n\n=====\nTypes\n=====\n\nThe foundation of EdgeQL is Gel's rigorous type system. There is a set of\nEdgeQL operators and functions for changing, introspecting, and filtering by\ntypes.\n\n.. _ref_eql_types_names:\n\nType expressions\n----------------\n\n.. api-index:: array<§type§>, tuple<§type [\\, ...]§>\n\nType expressions are exactly what they sound like: EdgeQL expressions that\nrefer to a type. Most commonly, these are simply the *names* of established\ntypes: ``str``, ``int64``, ``BlogPost``, etc. Arrays and tuples have a\ndedicated type syntax.\n\n.. list-table::\n\n  * - **Type**\n    - **Syntax**\n  * - Array\n    - ``array<x>``\n  * - Tuple (unnamed)\n    - ``tuple<x, y, z>``\n  * - Tuple (named)\n    - ``tuple<foo: x, bar: y>``\n\nFor additional details on type syntax, see :ref:`Schema > Primitive Types\n<ref_datamodel_primitives>`.\n\n.. _ref_eql_types_typecast:\n\nType casting\n------------\n\n.. index:: casts, find object by id\n.. api-index:: <§type§>§expr§\n\nType casting is used to convert primitive values into another type. Casts are\nindicated with angle brackets containing a type expression.\n\n.. code-block:: edgeql-repl\n\n    db> select <str>10;\n    {\"10\"}\n    db> select <bigint>10;\n    {10n}\n    db> select <array<str>>[1, 2, 3];\n    {['1', '2', '3']}\n    db> select <tuple<str, float64, bigint>>(1, 2, 3);\n    {('1', 2, 3n)}\n\n\n\nType casts are useful for declaring literals for types like ``datetime``,\n``uuid``, and  ``int16`` that don't have a dedicated syntax.\n\n.. code-block:: edgeql-repl\n\n    db> select <datetime>'1999-03-31T15:17:00Z';\n    {<datetime>'1999-03-31T15:17:00Z'}\n    db> select <int16>42;\n    {42}\n    db> select <uuid>'89381587-705d-458f-b837-860822e1b219';\n    {89381587-705d-458f-b837-860822e1b219}\n\n\nThere are limits to what values can be cast to a certain type. In some cases\ntwo types are entirely incompatible, like ``bool`` and ``int64``; in other\ncases, the source data must be in a particular format, like casting ``str`` to\n``datetime``. For a comprehensive table of castability, see :ref:`Standard\nLibrary > Casts <ref_eql_casts_table>`.\n\nType casts can only be used on primitive expressions, not object type\nexpressions. Every object stored in the database is strongly and immutably\ntyped; you can't simply convert an object to an object of a different type.\n\n.. code-block:: edgeql-repl\n\n  db> select <BlogPost>10;\n  QueryError: cannot cast 'std::int64' to 'default::BlogPost'\n  db> select <int64>'asdf';\n  InvalidValueError: invalid input syntax for type std::int64: \"asdf\"\n  db> select <int16>100000000000000n;\n  NumericOutOfRangeError: std::int16 out of range\n\n\n.. lint-off\n\nYou can cast a UUID into an object:\n\n.. code-block:: edgeql-repl\n\n    db> select <Hero><uuid>'01d9cc22-b776-11ed-8bef-73f84c7e91e7';\n    {default::Hero {id: 01d9cc22-b776-11ed-8bef-73f84c7e91e7}}\n\nIf you try to cast a UUID that no object of the type has as its ``id``\nproperty, you'll get an error:\n\n.. code-block:: edgeql-repl\n\n    db> select <Hero><uuid>'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';\n    gel error: CardinalityViolationError: 'default::Hero' with id 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' does not exist\n\n.. lint-on\n\n\n.. _ref_eql_types_intersection:\n\nType intersections\n------------------\n\n.. api-index:: [is §type§]\n\nAll elements of a given set have the same type; however, in the context of\n*sets of objects*, this type might be ``abstract`` and contain elements of\nmultiple concrete subtypes. For instance, a set of ``Media`` objects may\ncontain both ``Movie`` and ``TVShow`` objects.\n\n.. code-block:: edgeql-repl\n\n  db> select Media;\n  {\n    default::Movie {id: 9d2ce01c-35e8-11ec-acc3-83b1377efea0},\n    default::Movie {id: 3bfe4900-3743-11ec-90ee-cb73d2740820},\n    default::TVShow {id: b0e0dd0c-35e8-11ec-acc3-abf1752973be},\n  }\n\nWe can use the *type intersection* operator to restrict the elements of a set\nby subtype.\n\n.. code-block:: edgeql-repl\n\n  db> select Media[is Movie];\n  {\n    default::Movie {id: 9d2ce01c-35e8-11ec-acc3-83b1377efea0},\n    default::Movie {id: 3bfe4900-3743-11ec-90ee-cb73d2740820},\n  }\n\nLogically, this computes the intersection of the ``Media`` and ``Movie`` sets;\nsince only ``Movie`` objects occur in both sets, this can be conceptualized as\na \"filter\" that removes all elements that aren't of type ``Movie``.\n\n.. Type unions\n.. -----------\n\n.. You can create a type union with the pipe operator: :eql:op:`type | type\n.. <typeor>`. This is mostly commonly used for object types.\n\n.. .. code-block:: edgeql-repl\n\n..   db> select 5 is int32 | int64;\n..   {true}\n..   db> select Media is Movie | TVShow;\n..   {true, true, true}\n\n\nType checking\n-------------\n\n.. api-index:: §expr§ is §type§, §expr§ is not §type§\n\nThe ``[is foo]`` \"type intersection\" syntax should not be confused with the\n*type checking* operator :eql:op:`is`.\n\n.. code-block:: edgeql-repl\n\n  db> select 5 is int64;\n  {true}\n  db> select {3.14, 2.718} is not int64;\n  {true, true}\n  db> select Media is Movie;\n  {true, true, false}\n\n\nThe ``typeof`` operator\n-----------------------\n\n.. api-index:: typeof §expr§\n\nThe type of any expression can be extracted with the :eql:op:`typeof`\noperator. This can be used in any expression that expects a type.\n\n.. code-block:: edgeql-repl\n\n  db> select <typeof 5>'100';\n  {100}\n  db> select \"tuna\" is typeof \"trout\";\n  {true}\n\nIntrospection\n-------------\n\nThe entire type system of Gel is *stored inside Gel*. All types are\nintrospectable as instances of the ``schema::Type`` type. For a set of\nintrospection examples, see :ref:`Guides > Introspection\n<ref_datamodel_introspection>`.\n"
  },
  {
    "path": "docs/reference/edgeql/update.rst",
    "content": ".. _ref_eql_update:\n\nUpdate\n======\n\n.. api-index:: update, filter, set\n\nThe ``update`` command is used to update existing objects.\n\n.. code-block:: edgeql-repl\n\n  db> update Hero\n  ... filter .name = \"Hawkeye\"\n  ... set { name := \"Ronin\" };\n  {default::Hero {id: d476b12e-3e7b-11ec-af13-2717f3dc1d8a}}\n\nIf you omit the ``filter`` clause, all objects will be updated. This is useful\nfor updating values across all objects of a given type. The example below\ncleans up all ``Hero.name`` values by trimming whitespace and converting them\nto title case.\n\n.. code-block:: edgeql-repl\n\n  db> update Hero\n  ... set { name := str_trim(str_title(.name)) };\n  {default::Hero {id: d476b12e-3e7b-11ec-af13-2717f3dc1d8a}}\n\nSyntax\n^^^^^^\n\nThe structure of the ``update`` statement (``update...filter...set``) is an\nintentional inversion of SQL's ``UPDATE...SET...WHERE`` syntax. Curiously, in\nSQL, the ``where`` clauses typically occur *last* despite being applied before\nthe ``set`` statement. EdgeQL is structured to reflect this; first, a target\nset is specified, then filters are applied, then the data is updated.\n\nUpdating properties\n-------------------\n\nTo explicitly unset a property that is not required, set it to an empty set.\n\n.. code-block:: edgeql\n\n   update Person filter .id = <uuid>$id set { middle_name := {} };\n\nUpdating links\n--------------\n\n.. api-index:: :=, +=, -=\n\nWhen updating links, the ``:=`` operator will *replace* the set of linked\nvalues.\n\n.. code-block:: edgeql-repl\n\n  db> update movie\n  ... filter .title = \"Black Widow\"\n  ... set {\n  ...  characters := (\n  ...   select Person\n  ...   filter .name in { \"Black Widow\", \"Yelena\", \"Dreykov\" }\n  ...  )\n  ... };\n  {default::Title {id: af706c7c-3e98-11ec-abb3-4bbf3f18a61a}}\n  db> select Movie { num_characters := count(.characters) }\n  ... filter .title = \"Black Widow\";\n  {default::Movie {num_characters: 3}}\n\nTo add additional linked items, use the ``+=`` operator.\n\n.. code-block:: edgeql-repl\n\n  db> update Movie\n  ... filter .title = \"Black Widow\"\n  ... set {\n  ...  characters += (insert Villain {name := \"Taskmaster\"})\n  ... };\n  {default::Title {id: af706c7c-3e98-11ec-abb3-4bbf3f18a61a}}\n  db> select Movie { num_characters := count(.characters) }\n  ... filter .title = \"Black Widow\";\n  {default::Movie {num_characters: 4}}\n\nTo remove items, use ``-=``.\n\n.. code-block:: edgeql-repl\n\n  db> update Movie\n  ... filter .title = \"Black Widow\"\n  ... set {\n  ...  characters -= Villain # remove all villains\n  ... };\n  {default::Title {id: af706c7c-3e98-11ec-abb3-4bbf3f18a61a}}\n  db> select Movie { num_characters := count(.characters) }\n  ... filter .title = \"Black Widow\";\n  {default::Movie {num_characters: 2}}\n\nReturning data on update\n------------------------\n\nBy default, ``update`` returns only the inserted object's ``id`` as seen in the\nexamples above. If you want to get additional data back, you may wrap your\n``update`` with a ``select`` and apply a shape specifying any properties and\nlinks you want returned:\n\n.. code-block:: edgeql-repl\n\n  db> select (update Hero\n  ...   filter .name = \"Hawkeye\"\n  ...   set { name := \"Ronin\" }\n  ... ) {id, name};\n  {\n    default::Hero {\n      id: d476b12e-3e7b-11ec-af13-2717f3dc1d8a,\n      name: \"Ronin\"\n    }\n  }\n\nWith blocks\n-----------\n\nAll top-level EdgeQL statements (``select``, ``insert``, ``update``, and\n``delete``) can be prefixed with a ``with`` block. This is useful for updating\nthe results of a complex query.\n\n.. code-block:: edgeql-repl\n\n  db> with people := (\n  ...     select Person\n  ...     order by .name\n  ...     offset 3\n  ...     limit 3\n  ...   )\n  ... update people\n  ... set { name := str_trim(.name) };\n  {\n    default::Hero {id: d4764c66-3e7b-11ec-af13-df1ba5b91187},\n    default::Hero {id: d7d7e0f6-40ae-11ec-87b1-3f06bed494b9},\n    default::Villain {id: d477a836-3e7b-11ec-af13-4fea611d1c31},\n  }\n\n.. note::\n\n  You can pass any object-type expression into ``update``, including\n  polymorphic ones (as above).\n\nYou can also use ``with`` to make returning additional data from an update more\nreadable:\n\n.. code-block:: edgeql-repl\n\n  db> with UpdatedHero := (update Hero\n  ...   filter .name = \"Hawkeye\"\n  ...   set { name := \"Ronin\" }\n  ... )\n  ... select UpdatedHero {\n  ...   id,\n  ...   name\n  ... };\n  {\n    default::Hero {\n      id: d476b12e-3e7b-11ec-af13-2717f3dc1d8a,\n      name: \"Ronin\"\n    }\n  }\n\n\nSee also\n--------\n\nFor documentation on performing *upsert* operations, see :ref:`EdgeQL > Insert\n> Upserts <ref_eql_upsert>`.\n\n.. list-table::\n\n  * - :ref:`Reference > Commands > Update <ref_eql_statements_update>`\n  * - :ref:`Cheatsheets > Updating data <ref_cheatsheet_update>`\n"
  },
  {
    "path": "docs/reference/edgeql/with.rst",
    "content": ".. _ref_eql_with:\n\nWith\n====\n\n.. index:: composition, composing queries, composable, CTE,\n           common table expressions, subquery, subqueries\n.. api-index:: with\n\nAll top-level EdgeQL statements (``select``, ``insert``, ``update``, and\n``delete``) can be prefixed by a ``with`` block. These blocks contain\ndeclarations of standalone expressions that can be used in your query.\n\n.. code-block:: edgeql-repl\n\n  db> with my_str := \"hello world\"\n  ... select str_title(my_str);\n  {'Hello World'}\n\n\nThe ``with`` clause can contain more than one variable. Earlier variables can\nbe referenced by later ones. Taken together, it becomes possible to write\n\"script-like\" queries that execute several statements in sequence.\n\n.. code-block:: edgeql-repl\n\n  db> with a := 5,\n  ...   b := 2,\n  ...   c := a ^ b\n  ... select c;\n  {25}\n\n\nSubqueries\n^^^^^^^^^^\n\nThere's no limit to the complexity of computed expressions. EdgeQL is fully\ncomposable; queries can simply be embedded inside each other. The following\nquery fetches a list of all movies featuring at least one of the original six\nAvengers.\n\n.. code-block:: edgeql-repl\n\n  db> with avengers := (select Hero filter .name in {\n  ...     'Iron Man',\n  ...     'Black Widow',\n  ...     'Captain America',\n  ...     'Thor',\n  ...     'Hawkeye',\n  ...     'The Hulk'\n  ...   })\n  ... select Movie {title}\n  ... filter avengers in .characters;\n  {\n\n    default::Movie {title: 'Iron Man'},\n    default::Movie {title: 'The Incredible Hulk'},\n    default::Movie {title: 'Iron Man 2'},\n    default::Movie {title: 'Thor'},\n    default::Movie {title: 'Captain America: The First Avenger'},\n    ...\n  }\n\n.. _ref_eql_with_params:\n\nQuery parameters\n^^^^^^^^^^^^^^^^\n\nA common use case for ``with`` clauses is the initialization of :ref:`query\nparameters <ref_eql_params>`.\n\n.. code-block:: edgeql\n\n  with user_id := <uuid>$user_id\n  select User { name }\n  filter .id = user_id;\n\nFor a full reference on using query parameters, see :ref:`EdgeQL > Parameters\n<ref_eql_params>`.\n\n\nModule alias\n^^^^^^^^^^^^\n\n.. api-index:: with, as module\n\nAnother use of ``with`` is to provide aliases for modules. This can be useful\nfor long queries which reuse many objects or functions from the same module.\n\n.. code-block:: edgeql\n\n  with http as module std::net::http\n  select http::ScheduledRequest\n  filter .method = http::Method.POST;\n\nIf the aliased module does not exist at the top level, but does exists as a\npart of the ``std`` module, that will be used automatically.\n\n.. code-block:: edgeql\n\n  with http as module net::http # <- omitting std\n  select http::ScheduledRequest\n  filter .method = http::Method.POST;\n\n\nModule selection\n^^^^^^^^^^^^^^^^\n\n.. index:: fully-qualified names\n.. api-index:: with module\n\nBy default, the *active module* is ``default``, so all schema objects inside\nthis module can be referenced by their *short name*, e.g. ``User``,\n``BlogPost``, etc. To reference objects in other modules, we must use\nfully-qualified names (``default::Hero``).\n\nHowever, ``with`` clauses also provide a mechanism for changing the *active\nmodule* on a per-query basis.\n\n.. code-block:: edgeql-repl\n\n  db> with module schema\n  ... select ObjectType;\n\nThis ``with module`` clause changes the default module to schema, so we can\nrefer to ``schema::ObjectType`` (a built-in Gel type) as simply\n``ObjectType``.\n\nAs with module aliases, if the active module does not exist at the top level,\nbut does exist as part of the ``std`` module, that will be used automatically.\n\n.. code-block:: edgeql-repl\n\n  db> with module math select abs(-1);\n  {1}\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Reference > Commands > With <ref_eql_statements_with>`\n"
  },
  {
    "path": "docs/reference/index.rst",
    "content": "=========\nReference\n=========\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    using/index\n    running/index\n    datamodel/index\n    edgeql/index\n    stdlib/index\n    ai/index\n    auth/index\n    reference/index\n\nLearn three components, and you know |Gel|: how to work with\n:ref:`schema <ref_datamodel_index>`, how to write queries with\n:ref:`EdgeQL <ref_edgeql>`, and what's available to you in our\n:ref:`standard library <ref_std>`. Start in those sections if you're new to |Gel|.\nMove over to our :ref:`reference <ref_reference_index>` when you're ready to\ndive deep into the internals, syntax, and other advanced topics.\n\n\nSchema\n------\n\n|Gel| schemas are declared using our schema definition language (SDL).\n\n.. code-block:: sdl\n\n  module default {\n    type Book {\n      required title: str;\n      release_year: int16;\n      author: Person;\n    }\n    type Person {\n      required name: str;\n    }\n  }\n\nThe example schema above defines two types: Book and Person, each with\na property or two. Book also contains a link to the author, which is a\nlink to objects of the Person type. Learn more about how to define\nyour schema using SDL in the :ref:`schema <ref_datamodel_index>` section.\n\nEdgeQL\n------\n\nEdgeQL is a next-generation query language designed to match SQL in power and\nsurpass it in terms of clarity, brevity, and intuitiveness.\n\n.. code-block:: edgeql-repl\n\n  db> select Book {\n  ...   title,\n  ...   release_year,\n  ...   author: {\n  ...     name\n  ...   }\n  ... } order by .title;\n  {\n    default::Book {\n      title: '1984',\n      release_year: 1949,\n      author: default::Person {\n        name: 'George Orwell'\n      }\n    },\n    default::Book {\n      title: 'Americanah',\n      release_year: 2013,\n      author: default::Person {\n        name: 'Chimamanda Ngozi Adichie'\n      }\n    },\n    ...\n  }\n\nYou can use EdgeQL to easily return nested data structures just by putting a\nshape with a link on an object as shown above.\n\n\nStandard library\n----------------\n\n|Gel| comes with a rigorously defined type system consisting of scalar\ntypes, collection types (like arrays and tuples), and object types. It\nalso includes a library of built-in functions and operators for working\nwith each datatype, alongside some additional utilities and extensions.\n\n.. code-block:: edgeql-repl\n\n  db> select count(Book);\n  {16}\n  db> select Book {\n  ...   title,\n  ...   title_length := len(.title)\n  ... } order by .title_length;\n  {\n    default::Book {\n      title: 'Sula',\n      title_length: 4\n    },\n    default::Book {\n      title: '1984',\n      title_length: 4\n    },\n    default::Book {\n      title: 'Beloved',\n      title_length: 7\n    },\n    default::Book {\n      title: 'The Fellowship of the Ring',\n      title_length: 26\n    },\n    default::Book {\n      title: 'One Hundred Years of Solitude',\n      title_length: 29\n    },\n  }\n  db> select math::stddev(len(Book.title));\n  {7.298401651503339}\n\nGel comes with a rigorously defined type system consisting of scalar\ntypes, collection types (like arrays and tuples), and object types. It\nalso includes a library of built-in functions and operators for working\nwith each datatype, alongside some additional utilities and extensions.\n\n\nCheatsheets\n-----------\n\nLearn to do various common tasks using the many tools included with |Gel|.\n\nQuerying\n^^^^^^^^\n\n* :ref:`Select <ref_cheatsheet_select>`\n* :ref:`Insert <ref_cheatsheet_insert>`\n* :ref:`Update <ref_cheatsheet_update>`\n* :ref:`Delete <ref_cheatsheet_delete>`\n* :ref:`via GraphQL <ref_cheatsheet_graphql>`\n\nSchema\n^^^^^^\n\n* :ref:`Booleans <ref_cheatsheet_boolean>`\n* :ref:`Object Types <ref_cheatsheet_object_types>`\n* :ref:`Functions <ref_cheatsheet_functions>`\n* :ref:`Aliases <ref_cheatsheet_aliases>`\n* :ref:`Annotations <ref_cheatsheet_annotations>`\n* :ref:`Link Properties <ref_datamodel_linkprops>`\n\nAdmin\n^^^^^\n* :ref:`CLI <ref_cheatsheet_cli>`\n* :ref:`REPL <ref_cheatsheet_select>`\n* :ref:`Admin <ref_cheatsheet_admin>`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/analyze.rst",
    "content": ".. _ref_eql_statements_analyze:\n\nAnalyze\n=======\n\n:eql-statement:\n\n``analyze`` -- trigger performance analysis of the appended query\n\n.. eql:synopsis::\n\n    analyze <query>;\n\n    # where <query> is any EdgeQL query\n\n\nDescription\n-----------\n\n``analyze`` returns a table with performance metrics broken down by node.\n\nYou may prepend the ``analyze`` keyword in either of our REPLs (CLI or :ref:`UI\n<ref_cli_gel_ui>`) or you may prepend in the UI's query builder for a\nhelpful visualization of your query's performance.\n\nAfter any ``analyze`` in a REPL, run the ``\\expand`` command to see\nfine-grained performance analysis of the previously analyzed query.\n\n\nExample\n-------\n\n.. code-block:: edgeql-repl\n\n  db> analyze select Hero {\n  ...   name,\n  ...   secret_identity,\n  ...   villains: {\n  ...     name,\n  ...     nemesis: {\n  ...       name\n  ...     }\n  ...   }\n  ... };\n  ──────────────────────────────────────── Query ────────────────────────────────────────\n  analyze select ➊  Hero {name, secret_identity, ➋  villains: {name, ➌  nemesis: {name}}};\n\n  ──────────────────────── Coarse-grained Query Plan ────────────────────────\n                    │ Time     Cost Loops Rows Width │ Relations\n  ➊ root            │  0.0 69709.48   1.0  0.0    32 │ Hero\n  ╰──➋ .villains    │  0.0     92.9   0.0  0.0    32 │ Villain, Hero.villains\n  ╰──➌ .nemesis     │  0.0     8.18   0.0  0.0    32 │ Hero\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`CLI > gel analyze <ref_cli_gel_analyze>`\n  * - :ref:`EdgeQL > Analyze <ref_eql_analyze>`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/cardinality.rst",
    "content": ".. _ref_reference_cardinality:\n\nCardinality\n===========\n\n\nThe number of items in a set is known as its **cardinality**. A set with a\ncardinality of zero is referred to as an **empty set**. A set with a\ncardinality of one is known as a **singleton**.\n\nTerminology\n-----------\n\nThe term **cardinality** is used to refer to both the *exact* number of\nelements in a given set or a *range* of possible values. Internally, Gel\ntracks 5 different cardinality ranges: ``Empty`` (zero elements), ``One`` (a\nsingleton set), ``AtMostOne`` (zero or one elements), ``AtLeastOne`` (one or\nmore elements), and ``Many`` (any number of elements).\n\n|Gel| uses this information to statically check queries for validity. For\ninstance, when assigning to a ``required multi`` link, the value being\nassigned in question *must* have a cardinality of ``One`` or ``AtLeastOne``\n(as empty sets are not permitted).\n\n.. _ref_reference_cardinality_functions_operators:\n\nFunctions and operators\n-----------------------\n\nIt's often useful to think of Gel functions/operators as either\n*element-wise* or *aggregate*. Element-wise operations are applied to *each\nitem* in a set. Aggregate operations operate on sets *as a whole*.\n\n.. note::\n\n  This is a simplification, but it's a useful mental model when getting\n  started with Gel.\n\n.. _ref_reference_cardinality_aggregate:\n\nAggregate operations\n^^^^^^^^^^^^^^^^^^^^\n\nAn example of an aggregate function is :eql:func:`count`. It returns the number\nof elements in a given set. Regardless of the size of the input set, the result\nis a singleton integer.\n\n.. code-block:: edgeql-repl\n\n  db> select count('hello');\n  {1}\n  db> select count({'this', 'is', 'a', 'set'});\n  {4}\n  db> select count(<str>{});\n  {0}\n\nAnother example is :eql:func:`array_agg`, which converts a *set* of elements\ninto a singleton array.\n\n.. code-block:: edgeql-repl\n\n  db> select array_agg({1,2,3});\n  {[1, 2, 3]}\n\n\n.. _ref_reference_cardinality_elementwise:\n\nElement-wise operations\n^^^^^^^^^^^^^^^^^^^^^^^\n\nBy contrast, the :eql:func:`len` function is element-wise; it computes the\nlength of each string inside a set of strings; as such, it converts a set\nof :eql:type:`str` into an equally-sized set of :eql:type:`int64`.\n\n.. code-block:: edgeql-repl\n\n  db> select len('hello');\n  {5}\n  db> select len({'hello', 'world'});\n  {5, 5}\n\n.. _ref_reference_cardinality_cartesian:\n\nCartesian products\n^^^^^^^^^^^^^^^^^^\n\nIn case of element-wise operations that accept multiple arguments, the\noperation is applied to a cartesian product of all the input sets.\n\n.. code-block:: edgeql-repl\n\n  db> select {'aaa', 'bbb'} ++ {'ccc', 'ddd'};\n  {'aaaccc', 'aaaddd', 'bbbccc', 'bbbddd'}\n  db> select {true, false} or {true, false};\n  {true, true, true, false}\n\nBy extension, if any of the input sets are empty, the result of applying an\nelement-wise function is also empty. In effect, when Gel detects an empty\nset, it \"short-circuits\" and returns an empty set without applying the\noperation.\n\n.. code-block:: edgeql-repl\n\n  db> select {} ++ {'ccc', 'ddd'};\n  {}\n  db> select {} or {true, false};\n  {}\n\n.. note::\n\n  Certain functions and operators avoid this \"short-circuit\" behavior by\n  marking their inputs as :ref:`optional <ref_eql_sdl_functions_syntax>`. A\n  notable example of an operator with optional inputs is the :eql:op:`??\n  <coalesce>` operator.\n\n  .. code-block:: edgeql-repl\n\n    db> select <str>{} ?? 'default';\n    {'default'}\n\n\nPer-input cardinality\n^^^^^^^^^^^^^^^^^^^^^\n\nUltimately, the distinction between \"aggregate vs element-wise\" operations is\na false one. Consider the :eql:op:`in` operation.\n\n.. code-block:: edgeql-repl\n\n  db> select {1, 4} in {1, 2, 3};\n  {true, false}\n\nThis operator takes two inputs. If it was \"element-wise\" we would expect the\ncardinality of the above operation to the cartesian product of the input\ncardinalities: ``2 x 3 = 6``. It it was aggregate, we'd expect a singleton\noutput.\n\nInstead, the cardinality is ``2``. This operator is element-wise with respect\nto the first input and aggregate with respect to the second. The \"element-wise\nvs aggregate\" concept isn't determined on a per-function/per-operator basis;\nit determined on a per-input basis.\n\n\nType qualifiers\n^^^^^^^^^^^^^^^\n\nWhen defining functions, all inputs are element-wise by default. The\n``set of`` :ref:`type qualifier  <ref_sdl_function_typequal>` is used to\ndesignate an input as *aggregate*. Currently this modifier is not supported\nfor user-defined functions, but it is used by certain standard library\nfunctions.\n\nSimilarly the ``optional`` qualifier marks the input as optional; an operation\nwill be executed is an optional input is empty, whereas passing an\nempty set for a \"standard\" (non-optional) element-wise input will always\nresult in an empty set.\n\nSimilarly, the *output* of a function :ref:`can be annotated\n<ref_sdl_function_rettype>` with ``set of`` and ``optional`` qualifiers.\n\n\nCardinality computation\n^^^^^^^^^^^^^^^^^^^^^^^\n\nTo compute the number of times a function/operator will be invoked, take the\ncardinality of each input and apply the following transformations, based on\nthe type qualifier (or lack thereof) for each:\n\n.. code-block::\n\n  element-wise:  N -> N\n  optional:      N -> max(1, N)\n  aggregate:     N -> 1\n\nThe ultimate cardinality of the result is the union of the results of each\ninvokation; as such, it depends on the *values returned* by each invokation.\n"
  },
  {
    "path": "docs/reference/reference/edgeql/casts.csv",
    "content": "from \\ to,:eql:type:`json <std::json>`,:eql:type:`str <std::str>`,:eql:type:`float32 <std::float32>`,:eql:type:`float64 <std::float64>`,:eql:type:`int16 <std::int16>`,:eql:type:`int32 <std::int32>`,:eql:type:`int64 <std::int64>`,:eql:type:`bigint <std::bigint>`,:eql:type:`decimal <std::decimal>`,:eql:type:`bool <std::bool>`,:eql:type:`bytes <std::bytes>`,:eql:type:`uuid <std::uuid>`,:eql:type:`datetime <std::datetime>`,:eql:type:`duration <std::duration>`,:eql:type:`local_date <cal::local_date>`,:eql:type:`local_datetime <cal::local_datetime>`,:eql:type:`local_time <cal::local_time>`,:eql:type:`relative_duration <cal::relative_duration>`,:eql:type:`date_duration <cal::date_duration>`,:eql:type:`enum`,object\n:eql:type:`json <std::json>`,,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,\n:eql:type:`str <std::str>`,``<>``,,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``:=``,\n:eql:type:`float32 <std::float32>`,``<>``,``<>``,,impl,``<>*``,``<>*``,``<>*``,``<>*``,``<>``,,,,,,,,,,,,\n:eql:type:`float64 <std::float64>`,``<>``,``<>``,``:=``,,``<>*``,``<>*``,``<>*``,``<>*``,``<>``,,,,,,,,,,,,\n:eql:type:`int16 <std::int16>`,``<>``,``<>``,impl,impl,,impl,impl,impl,impl,,,,,,,,,,,,\n:eql:type:`int32 <std::int32>`,``<>``,``<>``,,impl,``<>``,,impl,impl,impl,,,,,,,,,,,,\n:eql:type:`int64 <std::int64>`,``<>``,``<>``,``:=``,impl,``:=``,``:=``,,impl,impl,,,,,,,,,,,,\n:eql:type:`bigint <std::bigint>`,,,,,,,,,impl,,,,,,,,,,,,\n:eql:type:`decimal <std::decimal>`,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,``<>``,,,,,,,,,,,,,\n:eql:type:`bool <std::bool>`,``<>``,``<>``,,,,,,,,,,,,,,,,,,,\n:eql:type:`bytes <std::bytes>`,``<>``,,,,,,,,,,,,,,,,,,,,\n:eql:type:`uuid <std::uuid>`,``<>``,``<>``,,,,,,,,,,,,,,,,,,,``<>``\n:eql:type:`datetime <std::datetime>`,``<>``,``<>``,,,,,,,,,,,,,,,,,,,\n:eql:type:`duration <std::duration>`,``<>``,``<>``,,,,,,,,,,,,,,,,``<>``,,,\n:eql:type:`local_date <cal::local_date>`,``<>``,``<>``,,,,,,,,,,,,,,impl,,,,,\n:eql:type:`local_datetime <cal::local_datetime>`,``<>``,``<>``,,,,,,,,,,,,,``<>``,,``<>``,,,,\n:eql:type:`local_time <cal::local_time>`,``<>``,``<>``,,,,,,,,,,,,,,,,,,,\n:eql:type:`relative_duration <cal::relative_duration>`,``<>``,``<>``,,,,,,,,,,,,``<>``,,,,,``<>``,,\n:eql:type:`date_duration <cal::date_duration>`,``<>``,``<>``,,,,,,,,,,,,,,,,impl,,,\n:eql:type:`enum`,``<>``,``<>``,,,,,,,,,,,,,,,,,,,\nobject,``<>``,,,,,,,,,,,,,,,,,,,,\n"
  },
  {
    "path": "docs/reference/reference/edgeql/casts.rst",
    "content": ".. _ref_eql_casts:\n\n=====\nCasts\n=====\n\nThere are different ways that casts appear in EdgeQL.\n\n\nExplicit Casts\n--------------\n\nA type cast expression converts the specified value to another value of\nthe specified type:\n\n.. eql:synopsis::\n\n    \"<\" <type> \">\" <expression>\n\nThe :eql:synopsis:`<type>` must be a valid :ref:`type expression\n<ref_eql_types>` denoting a non-abstract scalar or a container type.\n\nFor example, the following expression casts an integer value into a string:\n\n.. code-block:: edgeql-repl\n\n    db> select <str>10;\n    {\"10\"}\n\nSee the :eql:op:`type cast operator <cast>` section for more\ninformation on type casting rules.\n\n.. _ref_uuid_casting:\n\n.. lint-off\n\n\nYou can cast a UUID into an object:\n\n.. code-block:: edgeql-repl\n\n    db> select <Hero><uuid>'01d9cc22-b776-11ed-8bef-73f84c7e91e7';\n    {default::Hero {id: 01d9cc22-b776-11ed-8bef-73f84c7e91e7}}\n\nIf you try to cast a UUID that no object of the type has as its ``id``\nproperty, you'll get an error:\n\n.. code-block:: edgeql-repl\n\n    db> select <Hero><uuid>'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';\n    gel error: CardinalityViolationError: 'default::Hero' with id 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' does not exist\n\n.. lint-on\n\n\nAssignment Casts\n----------------\n\n*Assignment casts* happen when inserting new objects. Numeric types\nwill often be automatically cast into the specific type corresponding\nto the property they are assigned to. This is to avoid extra typing\nwhen dealing with numeric value using fewer bits:\n\n.. code-block:: edgeql\n\n    # Automatically cast a literal 42 (which is int64\n    # by default) into an int16 value.\n    insert MyObject {\n        int16_val := 42\n    };\n\nIf *assignment* casting is supported for a given pair of types,\n*explicit* casting of those types is also supported.\n\n\nImplicit Casts\n--------------\n\n*Implicit casts* happen automatically whenever the value type doesn't\nmatch the expected type in an expression. This is mostly supported for\nnumeric casts that don't incur any potential information loss (in form\nof truncation), so typically from a less precise type, to a more\nprecise one. The :eql:type:`int64` to :eql:type:`float64` is a notable\nexception, which can suffer from truncation of significant digits for\nvery large integer values. There are a few scenarios when *implicit\ncasts* can occur:\n\n1) Passing arguments that don't match exactly the types in the\n   function signature:\n\n   .. code-block:: edgeql-repl\n\n        db> with x := <float32>12.34\n        ... select math::ceil(x);\n        {13}\n\n   The function :eql:func:`math::ceil` only takes :eql:type:`int64`,\n   :eql:type:`float64`, :eql:type:`bigint`, or :eql:type:`decimal` as\n   its argument. So the :eql:type:`float32` value will be *implicitly\n   cast* into a :eql:type:`float64` in order to match a valid\n   signature.\n\n2) Using operands that don't match exactly the types in the\n   operator signature (this works the same way as for functions):\n\n   .. code-block:: edgeql-repl\n\n        db> select 1 + 2.3;\n        {3.3}\n\n   The operator :eql:op:`+ <plus>` is defined only for operands of\n   the same type, so in the expression above the :eql:type:`int64`\n   value ``1`` is *implicitly cast* into a :eql:type:`float64` in\n   order to match the other operand and produce a valid signature.\n\n3) Mixing different numeric types in a set:\n\n   .. code-block:: edgeql-repl\n\n        db> select {1, 2.3, <float32>4.5} is float64;\n        {true, true, true}\n\n   All elements in a set have to be of the same type, so the values\n   are cast into :eql:type:`float64` as that happens to be the common\n   type to which all the set elements can be *implicitly cast*. This\n   would work out the same way if :eql:op:`union` was used instead:\n\n   .. code-block:: edgeql-repl\n\n        db> select (1 union 2.3 union <float32>4.5) is float64;\n        {true, true, true}\n\nIf *implicit* casting is supported for a given pair of types,\n*assignment* and *explicit* casting of those types is also supported.\n\n\n.. _ref_eql_casts_table:\n\nCasting Table\n-------------\n\n.. note::\n\n    The UUID-to-object cast is only available since |EdgeDB| 3.0+.\n\n.. This file is automatically generated by `make casts`:\n.. csv-table::\n    :file: casts.csv\n    :class: vertheadertable\n\n- ``<>`` - can be cast explicitly\n- ``:=`` - assignment cast is supported\n- ``impl`` - implicit cast is supported\n- ``*``- When casting a float type to an integer type, the fractional value\n  naturally cannot be preserved after the cast. When executing this cast, we\n  round to the nearest integer, rounding ties to the nearest even (e.g., 1.5 is\n  rounded up to 2; 2.5 is also rounded to 2).\n"
  },
  {
    "path": "docs/reference/reference/edgeql/delete.rst",
    "content": ".. _ref_eql_statements_delete:\n\nDelete\n======\n\n:eql-statement:\n:eql-haswith:\n\n``delete`` -- remove objects from a database.\n\n.. eql:synopsis::\n\n    [ with <with-item> [, ...] ]\n\n    delete <expr>\n\n    [ filter <filter-expr> ]\n\n    [ order by <order-expr> [direction] [then ...] ]\n\n    [ offset <offset-expr> ]\n\n    [ limit  <limit-expr> ] ;\n\n:eql:synopsis:`with`\n    Alias declarations.\n\n    The ``with`` clause allows specifying module aliases as well\n    as expression aliases that can be referenced by the ``delete``\n    statement.  See :ref:`ref_eql_statements_with` for more information.\n\n:eql:synopsis:`delete ...`\n    The entire :eql:synopsis:`delete ...` statement is syntactic\n    sugar for ``delete (select ...)``. Therefore, the base\n    :eql:synopsis:`<expr>` and the following :eql:synopsis:`filter`,\n    :eql:synopsis:`order by`, :eql:synopsis:`offset`, and\n    :eql:synopsis:`limit` clauses shape the set to\n    be deleted the same way an explicit :eql:stmt:`select` would.\n\n\nOutput\n~~~~~~\n\nOn successful completion, a ``delete`` statement returns the set\nof deleted objects.\n\n\nExamples\n~~~~~~~~\n\nHere's a simple example of deleting a specific user:\n\n.. code-block:: edgeql\n\n    with module example\n    delete User\n    filter User.name = 'Alice Smith';\n\nAnd here's the equivalent ``delete (select ...)`` statement:\n\n.. code-block:: edgeql\n\n    with module example\n    delete (select User\n            filter User.name = 'Alice Smith');\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`EdgeQL > Delete <ref_eql_delete>`\n  * - :ref:`Cheatsheets > Deleting data <ref_cheatsheet_delete>`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/describe.rst",
    "content": ".. _ref_eql_statements_describe:\n\nDescribe\n========\n\n:eql-statement:\n\n``describe`` -- provide human-readable description of a schema or a\nschema object\n\n.. eql:synopsis::\n\n    describe schema [ as {ddl | sdl | test [ verbose ]} ];\n\n    describe <schema-type> <name> [ as {ddl | sdl | text [ verbose ]} ];\n\n    # where <schema-type> is one of\n\n      object\n      annotation\n      constraint\n      function\n      link\n      module\n      property\n      scalar type\n      type\n\nDescription\n-----------\n\n``describe`` generates a human-readable description of a schema object.\n\nThe output of a ``describe`` command is a :eql:type:`str` , although\nit cannot be used as an expression in queries.\n\nThere are three output formats to choose from:\n\n:eql:synopsis:`as ddl`\n    Provide a valid :ref:`DDL <ref_eql_ddl>` definition.\n\n    The :ref:`DDL <ref_eql_ddl>` generated is a complete valid\n    definition of the particular schema object assuming all the other\n    referenced schema objects already exist.\n\n    This is the default format.\n\n:eql:synopsis:`as sdl`\n    Provide an :ref:`SDL <ref_eql_sdl>` definition.\n\n    The :ref:`SDL <ref_eql_sdl>` generated is a complete valid\n    definition of the particular schema object assuming all the other\n    referenced schema objects already exist.\n\n:eql:synopsis:`as text [verbose]`\n    Provide a human-oriented definition.\n\n    The human-oriented definition generated is similar to :ref:`SDL\n    <ref_eql_sdl>`, but it includes all the details that are inherited\n    (if any).\n\n    The :eql:synopsis:`verbose` mode enables displaying additional\n    details, such as :ref:`annotations <ref_datamodel_annotations>`\n    and :ref:`constraints <ref_datamodel_constraints>`, which are\n    otherwise omitted.\n\nWhen the ``describe`` command is used with the :eql:synopsis:`schema`\nthe result is a definition of the entire database schema. Only the\n:eql:synopsis:`as ddl` option is available for schema description.\n\nThe ``describe`` command can specify the type of schema object that it\nshould generate the description of:\n\n:eql:synopsis:`object <name>`\n    Match any module level schema object with the specified *name*.\n\n    This is the most general use of the ``describe`` command. It does\n    not match :ref:`modules <ref_datamodel_modules>` (and other\n    globals that cannot be uniquely identified just by the name).\n\n:eql:synopsis:`annotation <name>`\n    Match only :ref:`annotations <ref_datamodel_annotations>` with the\n    specified *name*.\n\n:eql:synopsis:`constraint <name>`\n    Match only :ref:`constraints <ref_datamodel_constraints>` with the\n    specified *name*.\n\n:eql:synopsis:`function <name>`\n    Match only :ref:`functions <ref_datamodel_functions>` with the\n    specified *name*.\n\n:eql:synopsis:`link <name>`\n    Match only :ref:`links <ref_datamodel_links>` with the specified *name*.\n\n:eql:synopsis:`module <name>`\n    Match only :ref:`modules <ref_datamodel_modules>` with the\n    specified *name*.\n\n:eql:synopsis:`property <name>`\n    Match only :ref:`properties <ref_datamodel_props>` with the\n    specified *name*.\n\n:eql:synopsis:`scalar type <name>`\n    Match only :ref:`scalar types <ref_datamodel_scalar_types>` with the\n    specified *name*.\n\n:eql:synopsis:`type <name>`\n    Match only :ref:`object types <ref_datamodel_object_types>` with the\n    specified *name*.\n\n\nExamples\n--------\n\nConsider the following schema:\n\n.. code-block:: sdl\n\n    abstract type Named {\n        required name: str {\n            delegated constraint exclusive;\n        }\n    }\n\n    type User extending Named {\n        required email: str {\n            annotation title := 'Contact email';\n        }\n    }\n\nHere are some examples of a ``describe`` command:\n\n.. code-block:: edgeql-repl\n\n    db> describe object User;\n    {\n        \"create type default::User extending default::Named {\n        create required single property email -> std::str {\n            create annotation std::title := 'Contact email';\n        };\n    };\"\n    }\n    db> describe object User as sdl;\n    {\n        \"type default::User extending default::Named {\n        required single property email -> std::str {\n            annotation std::title := 'Contact email';\n        };\n    };\"\n    }\n    db> describe object User as text;\n    {\n        'type default::User extending default::Named {\n        required single link __type__ -> schema::Type {\n            readonly := true;\n        };\n        required single property email -> std::str;\n        required single property id -> std::uuid {\n            readonly := true;\n        };\n        required single property name -> std::str;\n    };'\n    }\n    db> describe object User as text verbose;\n    {\n        \"type default::User extending default::Named {\n        required single link __type__ -> schema::Type {\n            readonly := true;\n        };\n        required single property email -> std::str {\n            annotation std::title := 'Contact email';\n        };\n        required single property id -> std::uuid {\n            readonly := true;\n            constraint std::exclusive;\n        };\n        required single property name -> std::str {\n            constraint std::exclusive;\n        };\n    };\"\n    }\n    db> describe schema;\n    {\n        \"create module default if not exists;\n    create abstract type default::Named {\n        create required single property name -> std::str {\n            create delegated constraint std::exclusive;\n        };\n    };\n    create type default::User extending default::Named {\n        create required single property email -> std::str {\n            create annotation std::title := 'Contact email';\n        };\n    };\"\n    }\n\nThe ``describe`` command also warns you if there are standard library\nmatches that are masked by some user-defined object. Consider the\nfollowing schema:\n\n.. code-block:: sdl\n\n    module default {\n        function len(v: tuple<float64, float64>) -> float64 using (\n            select (v.0 ^ 2 + v.1 ^ 2) ^ 0.5\n        );\n    }\n\nSo within the ``default`` module the user-defined function ``len``\n(computing the length of a vector) masks the built-ins:\n\n.. code-block:: edgeql-repl\n\n    db> describe function len as text;\n    {\n      'function default::len(v: tuple<std::float64, std::float64>) ->\n    std::float64 using (select\n        (((v.0 ^ 2) + (v.1 ^ 2)) ^ 0.5)\n    );\n\n    # The following builtins are masked by the above:\n\n    # function std::len(array: array<anytype>) ->  std::int64 {\n    #     volatility := \\'Immutable\\';\n    #     annotation std::description := \\'A polymorphic function to calculate\n    a \"length\" of its first argument.\\';\n    #     using sql $$\n    #     SELECT cardinality(\"array\")::bigint\n    #     $$\n    # ;};\n    # function std::len(bytes: std::bytes) ->  std::int64 {\n    #     volatility := \\'Immutable\\';\n    #     annotation std::description := \\'A polymorphic function to calculate\n    a \"length\" of its first argument.\\';\n    #     using sql $$\n    #     SELECT length(\"bytes\")::bigint\n    #     $$\n    # ;};\n    # function std::len(str: std::str) ->  std::int64 {\n    #     volatility := \\'Immutable\\';\n    #     annotation std::description := \\'A polymorphic function to calculate\n    a \"length\" of its first argument.\\';\n    #     using sql $$\n    #     SELECT char_length(\"str\")::bigint\n    #     $$\n    # ;};',\n    }\n"
  },
  {
    "path": "docs/reference/reference/edgeql/eval.rst",
    "content": ".. _ref_eql_fundamentals_queries:\n\n====================\nEvaluation algorithm\n====================\n\nEdgeQL is a functional language in the sense that every expression is\na composition of one or more queries.\n\nQueries can be *explicit*, such as a :eql:stmt:`select` statement,\nor *implicit*, as dictated by the semantics of a function, operator or\na statement clause.\n\nAn implicit ``select`` subquery is assumed in the following situations:\n\n- expressions passed as an argument for an aggregate function parameter\n  or operand;\n\n- the right side of the assignment operator (``:=``) in expression\n  aliases and :ref:`shape element declarations <ref_reference_shapes>`;\n\n- the majority of statement clauses.\n\nA nested query is called a *subquery*.  Here, the phrase\n\"*apearing directly in the query*\" means\n\"appearing directly in the query rather than in the subqueries\".\n\n.. _ref_eql_fundamentals_eval_algo:\n\nA query is evaluated recursively using the following procedure:\n\n1. Make a list of simple paths (i.e., paths that begin with a set reference)\n   appearing directly the query.  For every path in the list, find all paths\n   which begin with the same set reference and treat their longest common\n   prefix as an equivalent set reference.\n\n   Example:\n\n   .. code-block:: edgeql\n\n      select (\n        User.firstname,\n        User.friends.firstname,\n        User.friends.lastname,\n        Issue.priority.name,\n        Issue.number,\n        Status.name\n      );\n\n   In the above query, the longest common prefixes are: ``User``,\n   ``User.friends``, ``Issue``, and ``Status.name``.\n\n2. Make a *query input list* of all unique set references which appear\n   directly in the query (including the common path prefixes identified above).\n   The set references and path prefixes in this list are called *input\n   set references*,  and the sets they represent are called *input\n   sets*. Order this list such that any input references come before\n   any other input set reference for which it is a prefix (sorting\n   lexicographically works).\n\n3. Compute a set of *input tuples*.\n\n   - Begin with a set containing a single empty tuple.\n   - For each input set reference, we compute a *dependent* Cartesian\n     product of the input tuple set (``X``) so far and the input set\n     ``Y`` being considered. In this dependent product, we pair each\n     tuple ``x`` in the input tuple set ``X`` with each element of the\n     subset of the input set ``Y`` corresponding to the tuple ``x``. (For\n     example, in the above example, computing the dependent product\n     of User and User.friends would pair each user with all of their\n     friends.)\n\n     (Mathematically, ``X' = {(x, y) | x ∈ X, y ∈ f(x)}``, if ``f(x)``\n     selects the appropriate subset.)\n\n     The set produced becomes the new input tuple set and we continue\n     down the list.\n   - As a caveat to the above, if an input set appears exclusively as\n     an :ref:`optional <ref_sdl_function_typequal>` argument, it produces\n     pairs with a placeholder value ``Missing`` instead of an empty\n     Cartesian product in the above\n     set. (Mathematically, this corresponds to having ``f(x) =\n     {Missing}`` whenever it would otherwise produce an empty set.)\n\n4. Iterate over the set of input tuples, and on every iteration:\n\n   - in the query and its subqueries, replace each input set reference with the\n     corresponding value from the input tuple or an empty set if the value\n     is ``Missing``;\n\n   - evaluate the query expression in the order of precedence using\n     the following rules:\n\n     * subqueries are evaluated recursively from step 1;\n\n     * a function or an operator is evaluated in a loop over a Cartesian\n       product of its non-aggregate arguments\n       (empty ``optional`` arguments are excluded from the product);\n       aggregate arguments are passed as a whole set;\n       the results of the invocations are collected to form a single set.\n\n5. Collect the results of all iterations to obtain the final result set.\n"
  },
  {
    "path": "docs/reference/reference/edgeql/for.rst",
    "content": ".. _ref_eql_statements_for:\n\nFor\n===\n\n:eql-statement:\n:eql-haswith:\n\n\n``for``--compute a union of subsets based on values of another set\n\n.. eql:synopsis::\n\n    [ with <with-item> [, ...] ]\n\n    for <variable> in <iterator-expr>\n\n    union <output-expr> ;\n\n:eql:synopsis:`for <variable> in <iterator-expr>`\n    The ``for`` clause has this general form:\n\n    .. TODO: rewrite this\n\n    .. eql:synopsis::\n\n        for <variable> in <iterator-expr>\n\n    where :eql:synopsis:`<iterator-expr>` is a\n\t:ref:`literal <ref_eql_literals>`,\n\ta :ref:`function call <ref_reference_function_call>`,\n\ta :ref:`set constructor <ref_eql_set_constructor>`,\n\ta :ref:`path <ref_reference_paths>`,\n\tor any parenthesized expression or statement.\n\n:eql:synopsis:`union <output-expr>`\n    The ``union`` clause of the ``for`` statement has this general form:\n\n    .. TODO: rewrite this\n\n    .. eql:synopsis::\n\n        union <output-expr>\n\n    Here, :eql:synopsis:`<output-expr>`\n    is an arbitrary expression that is evaluated for\n    every element in a set produced by evaluating the ``for`` clause.\n    The results of the evaluation are appended to the result set.\n\n\n.. _ref_eql_forstatement:\n\nUsage of ``for`` statement\n++++++++++++++++++++++++++\n\n``for`` statement has some powerful features that deserve to be\nconsidered in detail separately. However, the common core is that\n``for`` iterates over elements of some arbitrary expression. Then for\neach element of the iterator some set is computed and combined via a\n:eql:op:`union` with the other such computed sets.\n\nThe simplest use case is when the iterator is given by a set\nexpression and it follows the general form of ``for x in A ...``:\n\n.. code-block:: edgeql\n\n    with module example\n    # the iterator is an explicit set of tuples, so x is an\n    # element of this set, i.e. a single tuple\n    for x in {\n        (name := 'Alice', theme := 'fire'),\n        (name := 'Bob', theme := 'rain'),\n        (name := 'Carol', theme := 'clouds'),\n        (name := 'Dave', theme := 'forest')\n    }\n    # typically this is used with an INSERT, DELETE or UPDATE\n    union (\n        insert\n            User {\n                name := x.name,\n                theme := x.theme,\n            }\n    );\n\nSince ``x`` is an element of a set it is guaranteed to be a non-empty\nsingleton in all of the expressions used by the ``union`` and later\nclauses of ``for``.\n\nAnother variation this usage of ``for`` is a bulk ``update``. There\nare cases when a bulk update involves a lot of external data that\ncannot be derived from the objects being updated. That is a good\nuse-case when a ``for`` statement is appropriate.\n\n.. code-block:: edgeql\n\n    # Here's an example of an update that is awkward to\n    # express without the use of FOR statement\n    with module example\n    update User\n    filter .name in {'Alice', 'Bob', 'Carol', 'Dave'}\n    set {\n        theme := 'red'  if .name = 'Alice' else\n                 'star' if .name = 'Bob' else\n                 'dark' if .name = 'Carol' else\n                 'strawberry'\n    };\n\n    # Using a FOR statement, the above update becomes simpler to\n    # express or review for a human.\n    with module example\n    for x in {\n        (name := 'Alice', theme := 'red'),\n        (name := 'Bob', theme := 'star'),\n        (name := 'Carol', theme := 'dark'),\n        (name := 'Dave', theme := 'strawberry')\n    }\n    union (\n        update User\n        filter .name = x.name\n        set {\n            theme := x.theme\n        }\n    );\n\nWhen updating data that mostly or completely depends on the objects\nbeing updated there's no need to use the ``for`` statement and it is not\nadvised to use it for performance reasons.\n\n.. code-block:: edgeql\n\n    with module example\n    update User\n    filter .name in {'Alice', 'Bob', 'Carol', 'Dave'}\n    set {\n        theme := 'halloween'\n    };\n\n    # The above can be accomplished with a for statement,\n    # but it is not recommended.\n    with module example\n    for x in {'Alice', 'Bob', 'Carol', 'Dave'}\n    union (\n        update User\n        filter .name = x\n        set {\n            theme := 'halloween'\n        }\n    );\n\nAnother example of using a ``for`` statement is working with link\nproperties. Specifying the link properties either at creation time or\nin a later step with an update is often simpler with a ``for``\nstatement helping to associate the link target to the link property in\nan intuitive manner.\n\n.. code-block:: edgeql\n\n    # Expressing this without for statement is fairly tedious.\n    with\n        module example,\n        U2 := User\n    for x in {\n        (\n            name := 'Alice',\n            friends := [('Bob', 'coffee buff'),\n                        ('Carol', 'dog person')]\n        ),\n        (\n            name := 'Bob',\n            friends := [('Alice', 'movie buff'),\n                        ('Dave', 'cat person')]\n        )\n    }\n    union (\n        update User\n        filter .name = x.name\n        set {\n            friends := assert_distinct(\n                (\n                    for f in array_unpack(x.friends)\n                    union (\n                        select U2 {@nickname := f.1}\n                        filter U2.name = f.0\n                    )\n                )\n            )\n        }\n    );\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`EdgeQL > For <ref_eql_for>`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/functions.rst",
    "content": ".. _ref_reference_function_call:\n\n\nFunction calls\n==============\n\n\n|Gel| provides a number of functions in the :ref:`standard library\n<ref_std>`. It is also possible for users to :ref:`define their own\n<ref_eql_sdl_functions>` functions.\n\n\nThe syntax for a function call is as follows:\n\n.. eql:synopsis::\n\n    <function_name> \"(\" [<argument> [, <argument>, ...]] \")\"\n\n    # where <argument> is:\n\n    <expr> | <identifier> := <expr>\n\n\n\nHere :eql:synopsis:`<function_name>` is a possibly qualified name of a\nfunction, and :eql:synopsis:`<argument>` is an *expression* optionally\nprefixed with an argument name and the assignment operator (``:=``)\nfor :ref:`named only <ref_eql_sdl_functions_syntax>` arguments.\n\nFor example, the following computes the length of a string ``'foo'``:\n\n.. code-block:: edgeql-repl\n\n    db> select len('foo');\n    {3}\n\nAnd here's an example of using a *named only* argument to provide a\ndefault value:\n\n.. code-block:: edgeql-repl\n\n    db> select array_get(['hello', 'world'], 10, default := 'n/a');\n    {'n/a'}\n\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Schema > Functions <ref_datamodel_functions>`\n  * - :ref:`SDL > Functions <ref_eql_sdl_functions>`\n  * - :ref:`DDL > Functions <ref_eql_ddl_functions>`\n  * - :ref:`Introspection > Functions <ref_datamodel_introspection_functions>`\n  * - :ref:`Cheatsheets > Functions <ref_cheatsheet_functions>`\n\n"
  },
  {
    "path": "docs/reference/reference/edgeql/group.rst",
    "content": ".. _ref_eql_statements_group:\n\nGroup\n=====\n\n:eql-statement:\n:eql-haswith:\n\n``group``--partition a set into subsets based on one or more keys\n\n.. eql:synopsis::\n\n    [ with <with-item> [, ...] ]\n\n    group [<alias> := ] <expr>\n\n    [ using <using-alias> := <expr>, [, ...] ]\n\n    by <grouping-element>, ... ;\n\n    # where a <grouping-element> is one of\n\n      <ref-or-list>\n      { <grouping-element>, ... }\n      ROLLUP( <ref-or-list>, ... )\n      CUBE( <ref-or-list>, ... )\n\n    # where a <ref-or-list> is one of\n\n      ()\n      <grouping-ref>\n      ( <grouping-ref>, ... )\n\n    # where a <grouping-ref> is one of\n\n      <using-alias>\n      .<field-name>\n\n:eql:synopsis:`group <expr>`\n    The ``group`` clause sets up the input set that will be operated on.\n\n    Much like in :eql:stmt:`select` it is possible to define an ad-hoc alias\n    at this stage to make referring to the starting set concisely.\n\n:eql:synopsis:`using <using-alias> := <expr>`\n    The ``using`` clause defines one or more aliases which can then be used as\n    part of the grouping key.\n\n    If the :eql:synopsis:`by` clause only refers to\n    :eql:synopsis:`.<field-name>` the ``using`` clause is optional.\n\n:eql:synopsis:`by <grouping-element>`\n    The ``by`` clause sepecifies which parameters will be used to partition\n    the starting set.\n\n    There are only two basic components for defining\n    :eql:synopsis:`<grouping-element>`: references to\n    :eql:synopsis:`<using-alias>` defined in the :eql:synopsis:`using` clause\n    or by references to the short-path format of\n    :eql:synopsis:`.<field-name>`. The :eql:synopsis:`.<field-name>` has to\n    refer to properties or links immediately present on the type of starting\n    set.\n\n    The basic building blocks can also be combined by using parentheses ``(\n    )`` to indicate that partitioning will happen based on several parameters\n    at once.\n\n    It is also possible to specify *grouping sets*, which are denoted using\n    curly braces ``{ }``. The results will contain different partitioning\n    based on each of the grouping set elements. When there are multiple\n    top-level grouping-elements then the cartesian product of them is taken to\n    determine the grouping set. Thus ``a, {b, c}`` is equivalent to ``{(a, b),\n    (a, c)}`` grouping sets.\n\n    :eql:synopsis:`ROLLUP` and :eql:synopsis:`CUBE` are a shorthand to specify\n    particular grouping sets. :eql:synopsis:`ROLLUP` groups by all prefixes\n    of a list of elements, so ``ROLLUP (a, b, c)`` is equivalent to ``{(),\n    (a), (a, b), (a, b, c)}``. :eql:synopsis:`CUBE` groups by all elements of\n    the power set, so ``CUBE (a, b)`` is equivalent to ``{(), (a), (b), (a,\n    b)}``.\n\n\nOutput\n------\n\nThe ``group`` statement partitions a starting set into subsets based on some\nspecified parameters. The output is organized into a set of :ref:`free objects\n<ref_eql_select_free_objects>` of the following structure:\n\n.. eql:synopsis::\n\n    {\n      \"key\": { <using-alias> := <value> [, ...] },\n      \"grouping\": <set of keys used in grouping>,\n      \"elements\": <the subset matching to the key>,\n    }\n\n:eql:synopsis:`\"key\"`\n    The :eql:synopsis:`\"key\"` contains another :ref:`free object\n    <ref_eql_select_free_objects>`, which contains all the aliases or field\n    names used as the key together with the specific values these parameters\n    take for this particular subset.\n\n:eql:synopsis:`\"grouping\"`\n    The :eql:synopsis:`\"grouping\"` contains a :eql:type:`str` set of all the\n    names of the parameters used as the key for this particular subset. This\n    is especially useful when using grouping sets and the parameters used in\n    the key are not the same for all partitionings.\n\n:eql:synopsis:`\"elements\"`\n    The :eql:synopsis:`\"elements\"` contains the actual subset of values that\n    match the :eql:synopsis:`\"key\"`.\n\n\nExamples\n--------\n\nHere's a simple example without using any aggregation or any further\nprocessing:\n\n.. code-block:: edgeql-repl\n\n    db> group Movie {title} by .release_year;\n    {\n      {\n        key: {release_year: 2016},\n        grouping: {'release_year'},\n        elements: {\n          default::Movie {title: 'Captain America: Civil War'},\n          default::Movie {title: 'Doctor Strange'},\n        },\n      },\n      {\n        key: {release_year: 2017},\n        grouping: {'release_year'},\n        elements: {\n          default::Movie {title: 'Spider-Man: Homecoming'},\n          default::Movie {title: 'Thor: Ragnarok'},\n        },\n      },\n      {\n        key: {release_year: 2018},\n        grouping: {'release_year'},\n        elements: {default::Movie {title: 'Ant-Man and the Wasp'}},\n      },\n      {\n        key: {release_year: 2019},\n        grouping: {'release_year'},\n        elements: {default::Movie {title: 'Spider-Man: No Way Home'}},\n      },\n      {\n        key: {release_year: 2021},\n        grouping: {'release_year'},\n        elements: {default::Movie {title: 'Black Widow'}},\n      },\n      ...\n    }\n\nOr we can group by an expression instead, such as whether the title starts\nwith a vowel or not:\n\n.. code-block:: edgeql-repl\n\n    db> with\n    ...   # Apply the group query only to more recent movies\n    ...   M := (select Movie filter .release_year > 2015)\n    ... group M {title}\n    ... using vowel := re_test('(?i)^[aeiou]', .title)\n    ... by vowel;\n    {\n      {\n        key: {vowel: false},\n        grouping: {'vowel'},\n        elements: {\n          default::Movie {title: 'Thor: Ragnarok'},\n          default::Movie {title: 'Doctor Strange'},\n          default::Movie {title: 'Spider-Man: Homecoming'},\n          default::Movie {title: 'Captain America: Civil War'},\n          default::Movie {title: 'Black Widow'},\n          default::Movie {title: 'Spider-Man: No Way Home'},\n        },\n      },\n      {\n        key: {vowel: true},\n        grouping: {'vowel'},\n        elements: {default::Movie {title: 'Ant-Man and the Wasp'}},\n      },\n    }\n\nIt is also possible to group scalars instead of objects, in which case you\nneed to define an ad-hoc alias to refer to the scalar set in order to specify\nhow it will be grouped:\n\n.. code-block:: edgeql-repl\n\n    db> with\n    ...   # Apply the group query only to more recent movies\n    ...   M := (select Movie filter .release_year > 2015)\n    ... group T := M.title\n    ... using vowel := re_test('(?i)^[aeiou]', T)\n    ... by vowel;\n    {\n      {\n        key: {vowel: false},\n        grouping: {'vowel'},\n        elements: {\n          'Captain America: Civil War',\n          'Doctor Strange',\n          'Spider-Man: Homecoming',\n          'Thor: Ragnarok',\n          'Spider-Man: No Way Home',\n          'Black Widow',\n        },\n      },\n      {\n        key: {vowel: true},\n        grouping: {'vowel'},\n        elements: {'Ant-Man and the Wasp'}\n      },\n    }\n\nOften the results of ``group`` are immediately used in a :eql:stmt:`select`\nstatement to provide some kind of analytical results:\n\n.. code-block:: edgeql-repl\n\n    db> with\n    ...   # Apply the group query only to more recent movies\n    ...   M := (select Movie filter .release_year > 2015),\n    ...   groups := (\n    ...     group M {title}\n    ...     using vowel := re_test('(?i)^[aeiou]', .title)\n    ...     by vowel\n    ...   )\n    ... select groups {\n    ...   starts_with_vowel := .key.vowel,\n    ...   count := count(.elements),\n    ...   mean_title_length :=\n    ...     round(math::mean(len(.elements.title)))\n    ... };\n    {\n      {starts_with_vowel: false, count: 6, mean_title_length: 18},\n      {starts_with_vowel: true, count: 1, mean_title_length: 20},\n    }\n\nIt's possible to group by more than one parameter. For example, we can add the\nrelease decade to whether the ``title`` starts with a vowel:\n\n.. code-block:: edgeql-repl\n\n    db> with\n    ...   # Apply the group query only to more recent movies\n    ...   M := (select Movie filter .release_year > 2015),\n    ...   groups := (\n    ...     group M {title}\n    ...     using\n    ...       vowel := re_test('(?i)^[aeiou]', .title),\n    ...       decade := .release_year // 10\n    ...     by vowel, decade\n    ...   )\n    ... select groups {\n    ...   key := .key {vowel, decade},\n    ...   count := count(.elements),\n    ...   mean_title_length :=\n    ...     math::mean(len(.elements.title))\n    ... };\n    {\n      {\n        key: {vowel: false, decade: 201},\n        count: 5,\n        mean_title_length: 19.8,\n      },\n      {\n        key: {vowel: false, decade: 202},\n        count: 1,\n        mean_title_length: 11,\n      },\n      {\n        key: {vowel: true, decade: 201},\n        count: 1,\n        mean_title_length: 20\n      },\n    }\n\nHaving more than one grouping parameter opens up the possibility to using\n*grouping sets* to see the way grouping parameters interact with the analytics\nwe're gathering:\n\n.. code-block:: edgeql-repl\n\n    db> with\n    ...   # Apply the group query only to more recent movies\n    ...   M := (select Movie filter .release_year > 2015),\n    ...   groups := (\n    ...     group M {title}\n    ...     using\n    ...       vowel := re_test('(?i)^[aeiou]', .title),\n    ...       decade := .release_year // 10\n    ...     by CUBE(vowel, decade)\n    ...   )\n    ... select groups {\n    ...   key := .key {vowel, decade},\n    ...   grouping,\n    ...   count := count(.elements),\n    ...   mean_title_length :=\n    ...     (math::mean(len(.elements.title)))\n    ... } order by array_agg(.grouping);\n    {\n      {\n        key: {vowel: {}, decade: {}},\n        grouping: {},\n        count: 7,\n        mean_title_length: 18.571428571428573,\n      },\n      {\n        key: {vowel: {}, decade: 202},\n        grouping: {'decade'},\n        count: 1,\n        mean_title_length: 11,\n      },\n      {\n        key: {vowel: {}, decade: 201},\n        grouping: {'decade'},\n        count: 6,\n        mean_title_length: 19.833333333333332,\n      },\n      {\n        key: {vowel: true, decade: {}},\n        grouping: {'vowel'},\n        count: 1,\n        mean_title_length: 20,\n      },\n      {\n        key: {vowel: false, decade: {}},\n        grouping: {'vowel'},\n        count: 6,\n        mean_title_length: 18.333333333333332,\n      },\n      {\n        key: {vowel: false, decade: 201},\n        grouping: {'vowel', 'decade'},\n        count: 5,\n        mean_title_length: 19.8,\n      },\n      {\n        key: {vowel: true, decade: 201},\n        grouping: {'vowel', 'decade'},\n        count: 1,\n        mean_title_length: 20,\n      },\n      {\n        key: {vowel: false, decade: 202},\n        grouping: {'vowel', 'decade'},\n        count: 1,\n        mean_title_length: 11,\n      },\n    }\n\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`EdgeQL > Group <ref_eql_group>`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/index.rst",
    "content": ".. _ref_eql_statements:\n\nEdgeQL\n========\n\nStatements in EdgeQL are a kind of an *expression* that has one or\nmore ``clauses`` and is used to retrieve or modify data in a database.\n\nQuery statements:\n\n* :eql:stmt:`select`\n\n  Retrieve data from a database and compute arbitrary expressions.\n\n* :eql:stmt:`for`\n\n  Compute an expression for every element of an input set and\n  concatenate the results.\n\n* :eql:stmt:`group`\n\n  Group data into subsets by keys.\n\nData modification statements:\n\n* :eql:stmt:`insert`\n\n  Create new object in a database.\n\n* :eql:stmt:`update`\n\n  Update objects in a database.\n\n* :eql:stmt:`delete`\n\n  Remove objects from a database.\n\nTransaction control statements:\n\n* :eql:stmt:`start transaction`\n\n  Start a transaction.\n\n* :eql:stmt:`commit`\n\n  Commit the current transaction.\n\n* :eql:stmt:`rollback`\n\n  Abort the current transaction.\n\n* :eql:stmt:`declare savepoint`\n\n  Declare a savepoint within the current transaction.\n\n* :eql:stmt:`rollback to savepoint`\n\n  Rollback to a savepoint within the current transaction.\n\n* :eql:stmt:`release savepoint`\n\n  Release a previously declared savepoint.\n\nSession state control statements:\n\n* :eql:stmt:`set` and :eql:stmt:`reset`.\n\nIntrospection command:\n\n* :eql:stmt:`describe`.\n\nPerformance analysis statement:\n\n* :eql:stmt:`analyze`.\n\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n\n    lexical\n    eval\n    shapes\n    paths\n    casts\n    functions\n    cardinality\n    volatility\n\n    select\n    insert\n    update\n    delete\n    for\n    group\n    with\n    analyze\n\n    tx_start\n    tx_commit\n    tx_rollback\n    tx_sp_declare\n    tx_sp_release\n    tx_sp_rollback\n\n    sess_set_alias\n    sess_reset_alias\n\n    describe\n"
  },
  {
    "path": "docs/reference/reference/edgeql/insert.rst",
    "content": ".. _ref_eql_statements_insert:\n\nInsert\n======\n\n:eql-statement:\n:eql-haswith:\n\n``insert`` -- create a new object in a database\n\n.. eql:synopsis::\n\n    [ with <with-spec> [ ,  ... ] ]\n    insert <expression> [ <insert-shape> ]\n    [ unless conflict\n        [ on <property-expr> [ else <alternative> ] ]\n    ] ;\n\n\nDescription\n-----------\n\n``insert`` inserts a new object into a database.\n\nWhen evaluating an ``insert`` statement, *expression* is used solely to\ndetermine the *type* of the inserted object and is not evaluated in any\nother way.\n\nIf a value for a *required* link is evaluated to an empty set, an error is\nraised.\n\nIt is possible to insert multiple objects by putting the ``insert``\ninto a :eql:stmt:`for` statement.\n\nSee :ref:`ref_eql_forstatement` for more details.\n\n:eql:synopsis:`with`\n    Alias declarations.\n\n    The ``with`` clause allows specifying module aliases as well\n    as expression aliases that can be referenced by the :eql:stmt:`update`\n    statement.  See :ref:`ref_eql_statements_with` for more information.\n\n:eql:synopsis:`<expression>`\n    An arbitrary expression returning a set of objects to be updated.\n\n    .. eql:synopsis::\n\n        insert <expression>\n        [ \"{\" <link> := <insert-value-expr> [, ...]  \"}\" ]\n\n.. _ref_eql_statements_conflict:\n\n:eql:synopsis:`unless conflict [ on <property-expr> ]`\n    :index: unless conflict\n\n    Handler of conflicts.\n\n    This clause allows to handle specific conflicts arising during\n    execution of ``insert`` without producing an error.  If the\n    conflict arises due to exclusive constraints on the properties\n    specified by *property-expr*, then instead of failing with an\n    error the ``insert`` statement produces an empty set (or an\n    alternative result).\n\n    The exclusive constraint on ``<property-expr>`` cannot be defined on a\n    parent type.\n\n    The specified *property-expr* may be either a reference to a property (or\n    link) or a tuple of references to properties (or links).\n\n    A caveat, however, is that ``unless conflict`` will not prevent\n    conflicts caused between multiple DML operations in the same\n    query; inserting two conflicting objects (through use of ``for``\n    or simply with two ``insert`` statements) will cause a constraint\n    error.\n\n    Example:\n\n    .. code-block:: edgeql\n\n        insert User { email := 'user@example.org' }\n        unless conflict on .email\n\n    .. code-block:: edgeql\n\n        insert User { first := 'Jason', last := 'Momoa' }\n        unless conflict on (.first, .last)\n\n:eql:synopsis:`else <alternative>`\n    Alternative result in case of conflict.\n\n    This clause can only appear after ``unless conflict`` clause. Any\n    valid expression can be specified as the *alternative*. When a\n    conflict arises, the result of the ``insert`` becomes the\n    *alternative* expression (instead of the default ``{}``).\n\n    In order to refer to the conflicting object in the *alternative*\n    expression, the name used in the ``insert`` must be used (see\n    :ref:`example below <ref_eql_statements_insert_unless>`).\n\nOutputs\n-------\n\nThe result of an ``insert`` statement used as an *expression* is a\nsingleton set containing the inserted object.\n\n\nExamples\n--------\n\nHere's a simple example of an ``insert`` statement creating a new user:\n\n.. code-block:: edgeql\n\n    with module example\n    insert User {\n        name := 'Bob Johnson'\n    };\n\n``insert`` is not only a statement, but also an expression and as such\nis has a value of the set of objects that has been created.\n\n.. code-block:: edgeql\n\n    with module example\n    insert Issue {\n        number := '100',\n        body := 'Fix errors in insert',\n        owner := (\n            select User filter User.name = 'Bob Johnson'\n        )\n    };\n\nIt is possible to create nested objects in a single ``insert``\nstatement as an atomic operation.\n\n.. code-block:: edgeql\n\n    with module example\n    insert Issue {\n        number := '101',\n        body := 'Nested insert',\n        owner := (\n            insert User {\n                name := 'Nested User'\n            }\n        )\n    };\n\nThe above statement will create a new ``Issue`` as well as a new\n``User`` as the owner of the ``Issue``. It will also return the new\n``Issue`` linked to the new ``User`` if the statement is used as an\nexpression.\n\nIt is also possible to create new objects based on some existing data\neither provided as an explicit list (possibly automatically generated\nby some tool) or a query. A ``for`` statement is the basis for this\nuse-case and ``insert`` is simply the expression in the ``union``\nclause.\n\n.. code-block:: edgeql\n\n    # example of a bulk insert of users based on explicitly provided\n    # data\n    with module example\n    for x in {'Alice', 'Bob', 'Carol', 'Dave'}\n    union (insert User {\n        name := x\n    });\n\n\n    # example of a bulk insert of issues based on a query\n    with\n        module example,\n        Elvis := (select User filter .name = 'Elvis'),\n        Open := (select Status filter .name = 'Open')\n\n    for Q in (select User filter .name ilike 'A%')\n\n    union (insert Issue {\n        name := Q.name + ' access problem',\n        body := 'This user was affected by recent system glitch',\n        owner := Elvis,\n        status := Open\n    });\n\n.. _ref_eql_statements_insert_unless:\n\nThere's an important use-case where it is necessary to either insert a\nnew object or update an existing one identified with some key. This is\nwhat the ``unless conflict`` clause allows:\n\n.. code-block:: edgeql\n\n    with module people\n    select (\n        insert Person {\n            name := \"Łukasz Langa\", is_admin := true\n        }\n        unless conflict on .name\n        else (\n            update Person\n            set { is_admin := true }\n        )\n    ) {\n        name,\n        is_admin\n    };\n\n\n.. note::\n\n    Statements in EdgeQL represent an atomic interaction with the\n    database. From the point of view of a statement all side-effects\n    (such as database updates) happen after the statement is executed.\n    So as far as each statement is concerned, it is some purely\n    functional expression evaluated on some specific input (database\n    state).\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`EdgeQL > Insert <ref_eql_insert>`\n  * - :ref:`Cheatsheets > Inserting data <ref_cheatsheet_insert>`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/lexical.rst",
    "content": ".. _ref_eql_lexical:\n\n\nLexical structure\n=================\n\nEvery EdgeQL command is composed of a sequence of *tokens*, terminated by\na semicolon (``;``).  The types of valid tokens as well as their order\nis determined by the syntax of the particular command.\n\nEdgeQL is case sensistive except for *keywords* (in the examples the\nkeywords are written in upper case as a matter of convention).\n\nThere are several kinds of tokens: *keywords*, *identifiers*,\n*literals* (constants) and *symbols* (operators and punctuation).\n\nTokens are normally separated by whitespace (space, tab, newline) or\ncomments.\n\n\nIdentifiers\n-----------\n\nThere are two ways of writing identifiers in EdgeQL: plain and quoted.\nThe plain identifiers are similar to many other languages, they are\nalphanumeric with underscores and cannot start with a digit. The\nquoted identifiers start and end with a *backtick*\n```quoted.identifier``` and can contain any characters inside with a\nfew exceptions. They must not start with an ampersand (``@``) or\ncontain a double colon (``::``). If there's a need to include a backtick\ncharacter as part of the identifier name a double-backtick sequence\n(``````) should be used: ```quoted``identifier``` will result in the\nactual identifier being ``quoted`identifier``.\n\n.. productionlist:: edgeql\n    identifier: `plain_ident` | `quoted_ident`\n    plain_ident: `ident_first` `ident_rest`*\n    ident_first: <any letter, underscore>\n    ident_rest: <any letter, digits, underscore>\n    quoted_ident: \"`\" `qident_first` `qident_rest`* \"`\"\n    qident_first: <any character except \"@\">\n    qident_rest: <any character>\n\nQuoted identifiers are usually needed to represent module names that\ncontain a dot (``.``) or to distinguish *names* from *reserved keywords*\n(for instance to allow referring to a link named \"order\" as ```order```).\n\n\n.. _ref_eql_lexical_names:\n\nNames and keywords\n------------------\n\n.. TODO::\n\n    This section needs a significant update.\n\nThere are a number of *reserved* and *unreserved* keywords in EdgeQL.\nEvery identifier that is not a *reserved* keyword is a valid *name*.\n*Names* are used to refer to concepts, links, link properties, etc.\n\n.. TODO: update this for \"branch\"\n\n.. productionlist:: edgeql\n    short_name: `not_keyword_ident` | `quoted_ident`\n    not_keyword_ident: <any `plain_ident` except for `keyword`>\n    keyword: `reserved_keyword` | `unreserved_keyword`\n    reserved_keyword: case insensitive sequence matching any\n                    : of the following\n                    : \"AGGREGATE\" | \"ALTER\" | \"AND\" |\n                    : \"ANY\" | \"COMMIT\" | \"CREATE\" |\n                    : \"DELETE\" | \"DETACHED\" | \"DISTINCT\" |\n                    : \"DROP\" | \"ELSE\" | \"EMPTY\" | \"EXISTS\" |\n                    : \"FALSE\" | \"FILTER\" | \"FUNCTION\" |\n                    : \"GET\" | \"GROUP\" | \"IF\" | \"ILIKE\" |\n                    : \"IN\" | \"INSERT\" | \"IS\" | \"LIKE\" |\n                    : \"LIMIT\" | \"MODULE\" | \"NOT\" | \"OFFSET\" |\n                    : \"OR\" | \"ORDER\" | \"OVER\" |\n                    : \"PARTITION\" | \"ROLLBACK\" | \"SELECT\" |\n                    : \"SET\" | \"SINGLETON\" | \"START\" | \"TRUE\" |\n                    : \"UPDATE\" | \"UNION\" | \"WITH\"\n    unreserved_keyword: case insensitive sequence matching any\n                      : of the following\n                      : \"ABSTRACT\" | \"ACTION\" | \"AFTER\" |\n                      : \"ARRAY\" | \"AS\" | \"ASC\" | \"ATOM\" |\n                      : \"ANNOTATION\" | \"BEFORE\" | \"BY\" |\n                      : \"CONCEPT\" | \"CONSTRAINT\" |\n                      : \"DATABASE\" | \"DESC\" | \"EVENT\" |\n                      : \"EXTENDING\" | \"FINAL\" | \"FIRST\" |\n                      : \"FOR\" | \"FROM\" | \"INDEX\" |\n                      : \"INITIAL\" | \"LAST\" | \"LINK\" |\n                      : \"MAP\" | \"MIGRATION\" | \"OF\" | \"ON\" |\n                      : \"POLICY\" | \"PROPERTY\" |\n                      : \"REQUIRED\" | \"RENAME\" | \"TARGET\" |\n                      : \"THEN\" | \"TO\" | \"TRANSACTION\" |\n                      : \"TUPLE\" | \"VALUE\" | \"VIEW\"\n\nFully-qualified names consist of a module, ``::``, and a short name.\nThey can be used in most places where a short name can appear (such as\npaths and shapes).\n\n.. productionlist:: edgeql\n    name: `short_name` | `fq_name`\n    fq_name: `short_name` \"::\" `short_name` |\n           : `short_name` \"::\" `unreserved_keyword`\n\n\n.. _ref_eql_lexical_const:\n\nConstants\n---------\n\nA number of scalar types have literal constant expressions.\n\n\n.. _ref_eql_lexical_str:\n\nStrings\n^^^^^^^\n\nProduction rules for :eql:type:`str` literals:\n\n.. productionlist:: edgeql\n    string: `str` | `raw_str`\n    str: \"'\" `str_content`* \"'\" | '\"' `str_content`* '\"'\n    raw_str: \"r'\" `raw_content`* \"'\" |\n           : 'r\"' `raw_content`* '\"' |\n           : `dollar_quote` `raw_content`* `dollar_quote`\n    raw_content: <any character different from delimiting quote>\n    dollar_quote: \"$\" `q_char0`? `q_char`* \"$\"\n    q_char0: \"A\"...\"Z\" | \"a\"...\"z\" | \"_\"\n    q_char: \"A\"...\"Z\" | \"a\"...\"z\" | \"_\" | \"0\"...\"9\"\n    str_content: <newline> | `unicode` | `str_escapes`\n    unicode: <any printable unicode character not preceded by \"\\\">\n    str_escapes: <see below for details>\n\nThe inclusion of \"high ASCII\" character in :token:`edgeql:q_char` in\npractice reflects the ability to use some of the letters with\ndiacritics like ``ò`` or ``ü`` in the dollar-quote delimiter.\n\nHere's a list of valid :token:`edgeql:str_escapes`:\n\n.. _ref_eql_lexical_str_escapes:\n\n+--------------------+---------------------------------------------+\n| Escape Sequence    | Meaning                                     |\n+====================+=============================================+\n| ``\\[newline]``     | Backslash and all whitespace up to next     |\n|                    | non-whitespace character is ignored         |\n+--------------------+---------------------------------------------+\n| ``\\\\``             | Backslash (\\\\)                              |\n+--------------------+---------------------------------------------+\n| ``\\'``             | Single quote (')                            |\n+--------------------+---------------------------------------------+\n| ``\\\"``             | Double quote (\")                            |\n+--------------------+---------------------------------------------+\n| ``\\b``             | ASCII backspace (``\\x08``)                  |\n+--------------------+---------------------------------------------+\n| ``\\f``             | ASCII form feed (``\\x0C``)                  |\n+--------------------+---------------------------------------------+\n| ``\\n``             | ASCII newline (``\\x0A``)                    |\n+--------------------+---------------------------------------------+\n| ``\\r``             | ASCII carriage return (``\\x0D``)            |\n+--------------------+---------------------------------------------+\n| ``\\t``             | ASCII tabulation (``\\x09``)                 |\n+--------------------+---------------------------------------------+\n| ``\\xhh``           | Character with hex value hh                 |\n+--------------------+---------------------------------------------+\n| ``\\uhhhh``         | Character with 16-bit hex value hhhh        |\n+--------------------+---------------------------------------------+\n| ``\\Uhhhhhhhh``     | Character with 32-bit hex value hhhhhhhh    |\n+--------------------+---------------------------------------------+\n\nHere's some examples of regular strings using escape sequences\n\n.. code-block:: edgeql-repl\n\n    db> select 'hello\n    ... world';\n    {'hello\n    world'}\n\n    db> select \"hello\\nworld\";\n    {'hello\n    world'}\n\n    db> select 'hello \\\n    ...         world';\n    {'hello world'}\n\n    db> select 'https://geldata.com/\\\n    ...         docs/edgeql/lexical\\\n    ...         #constants';\n    {'https://geldata.com/docs/edgeql/lexical#constants'}\n\n    db> select 'hello \\\\ world';\n    {'hello \\ world'}\n\n    db> select 'hello \\'world\\'';\n    {\"hello 'world'\"}\n\n    db> select 'hello \\x77orld';\n    {'hello world'}\n\n    db> select 'hello \\u0077orld';\n    {'hello world'}\n\n.. _ref_eql_lexical_raw:\n\nRaw strings don't have any specially interpreted symbols; they contain\nall the symbols between the quotes exactly as typed.\n\n.. code-block:: edgeql-repl\n\n    db> select r'hello \\\\ world';\n    {'hello \\\\ world'}\n\n    db> select r'hello \\\n    ... world';\n    {'hello \\\n     world'}\n\n    db> select r'hello\n    ... world';\n    {'hello\n     world'}\n\n.. _ref_eql_lexical_dollar_quoting:\n\nDollar-quoted String Constants\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nA special case of raw strings are *dollar-quoted* strings. They allow\nusing either kind of quote symbols ``'`` or ``\"`` as part of the\nstring content without the quotes terminating the string. In fact,\nbecause the *dollar-quote* delimiter sequences can have arbitrary\nalphanumeric additional fillers, it is always possible to surround any\ncontent with *dollar-quotes* in an unambiguous manner:\n\n.. code-block:: edgeql-repl\n\n    db> select $$hello\n    ... world$$;\n    {'hello\n    world'}\n\n    db> select $$hello\\nworld$$;\n    {'hello\\nworld'}\n\n    db> select $$\"hello\" 'world'$$;\n    {\"\\\"hello\\\" 'world'\"}\n\n    db> select $a$hello$$world$$$a$;\n    {'hello$$world$$'}\n\nMore specifically, a delimiter:\n\n* Must start with an ASCII letter or underscore\n* Has following characters that can be digits 0-9, underscores or\n  ASCII letters\n\n.. _ref_eql_lexical_bytes:\n\nBytes\n^^^^^\n\nProduction rules for :eql:type:`bytes` literals:\n\n.. productionlist:: edgeql\n    bytes: \"b'\" `bytes_content`* \"'\" | 'b\"' `bytes_content`* '\"'\n    bytes_content: <newline> | `ascii` | `bytes_escapes`\n    ascii: <any printable ascii character not preceded by \"\\\">\n    bytes_escapes: <see below for details>\n\nHere's a list of valid :token:`edgeql:bytes_escapes`:\n\n.. _ref_eql_lexical_bytes_escapes:\n\n+--------------------+---------------------------------------------+\n| Escape Sequence    | Meaning                                     |\n+====================+=============================================+\n| ``\\\\``             | Backslash (\\\\)                              |\n+--------------------+---------------------------------------------+\n| ``\\'``             | Single quote (')                            |\n+--------------------+---------------------------------------------+\n| ``\\\"``             | Double quote (\")                            |\n+--------------------+---------------------------------------------+\n| ``\\b``             | ASCII backspace (``\\x08``)                  |\n+--------------------+---------------------------------------------+\n| ``\\f``             | ASCII form feed (``\\x0C``)                  |\n+--------------------+---------------------------------------------+\n| ``\\n``             | ASCII newline (``\\x0A``)                    |\n+--------------------+---------------------------------------------+\n| ``\\r``             | ASCII carriage return (``\\x0D``)            |\n+--------------------+---------------------------------------------+\n| ``\\t``             | ASCII tabulation (``\\x09``)                 |\n+--------------------+---------------------------------------------+\n| ``\\xhh``           | Character with hex value hh                 |\n+--------------------+---------------------------------------------+\n\n\nIntegers\n^^^^^^^^\n\nThere are two kinds of integer constants: limited size\n(:eql:type:`int64`) and unlimited size (:eql:type:`bigint`). Unlimited\nsize integer :eql:type:`bigint` literals are similar to a regular\ninteger literals with an ``n`` suffix. The production rules are as\nfollows:\n\n.. productionlist:: edgeql\n    bigint: `integer` \"n\"\n    integer: \"0\" | `non_zero` `digit`*\n    non_zero: \"1\"...\"9\"\n    digit: \"0\"...\"9\"\n\nBy default all integer literals are interpreted as :eql:type:`int64`,\nwhile an explicit cast can be used to convert them to :eql:type:`int16`\nor :eql:type:`int32`:\n\n.. code-block:: edgeql-repl\n\n    db> select 0;\n    {0}\n\n    db> select 123;\n    {123}\n\n    db> select <int16>456;\n    {456}\n\n    db> select <int32>789;\n    {789}\n\nExamples of :eql:type:`bigint` literals:\n\n.. code-block:: edgeql-repl\n\n    db> select 123n;\n    {123n}\n\n    db> select 12345678901234567890n;\n    {12345678901234567890n}\n\n\nReal Numbers\n^^^^^^^^^^^^\n\nJust as for integers, there are two kinds of real number constants:\nlimited precision (:eql:type:`float64`) and unlimited precision\n(:eql:type:`decimal`). The :eql:type:`decimal` constants have the same\nlexical structure as :eql:type:`float64`, but with an ``n`` suffix:\n\n.. productionlist:: edgeql\n    decimal: `float` \"n\"\n    float: `float_wo_dec` | `float_w_dec`\n    float_wo_dec: `integer_part` `exp`\n    float_w_dec: `integer_part` \".\" `decimal_part`? `exp`?\n    integer_part: \"0\" | `non_zero` `digit`*\n    decimal_part: `digit`+\n    exp: \"e\" (\"+\" | \"-\")? `digit`+\n\nBy default all float literals are interpreted as :eql:type:`float64`,\nwhile an explicit cast can be used to convert them to :eql:type:`float32`:\n\n.. code-block:: edgeql-repl\n\n    db> select 0.1;\n    {0.1}\n\n    db> select 12.3;\n    {12.3}\n\n    db> select 1e3;\n    {1000.0}\n\n    db> select 1.2e-3;\n    {0.0012}\n\n    db> select <float32>12.3;\n    {12.3}\n\nExamples of :eql:type:`decimal` literals:\n\n.. code-block:: edgeql-repl\n\n    db> select 12.3n;\n    {12.3n}\n\n    db> select 12345678901234567890.12345678901234567890n;\n    {12345678901234567890.12345678901234567890n}\n\n    db> select 12345678901234567890.12345678901234567890e-3n;\n    {12345678901234567.89012345678901234567890n}\n\n\nPunctuation\n-----------\n\nEdgeQL uses ``;`` as a statement separator. It is idempotent, so\nmultiple repetitions of ``;`` don't have any additional effect.\n\n\nComments\n--------\n\nComments start with a ``#`` character that is not otherwise part of a\nstring literal and end at the end of line. Semantically, a comment is\nequivalent to whitespace.\n\n.. productionlist:: edgeql\n    comment: \"#\" <any other characters until the end of line>\n\n\nOperators\n---------\n\nEdgeQL operators listed in order of precedence from lowest to highest:\n\n.. list-table::\n    :widths: auto\n    :header-rows: 1\n\n    * - operator\n    * - :eql:op:`union`\n    * - :eql:op:`if..else`\n    * - :eql:op:`or`\n    * - :eql:op:`and`\n    * - :eql:op:`not`\n    * - :eql:op:`=<eq>`, :eql:op:`\\!=<neq>`, :eql:op:`?=<coaleq>`,\n        :eql:op:`?\\!=<coalneq>`\n    * - :eql:op:`\\<<lt>`, :eql:op:`><gt>`, :eql:op:`\\<=<lteq>`,\n        :eql:op:`>=<gteq>`\n    * - :eql:op:`like`, :eql:op:`ilike`\n    * - :eql:op:`in`, :eql:op:`not in <in>`\n    * - :eql:op:`is`, :eql:op:`is not <is>`\n    * - :eql:op:`+<plus>`, :eql:op:`-<minus>`, :eql:op:`++<strplus>`\n    * - :eql:op:`*<mult>`, :eql:op:`/<div>`,\n        :eql:op:`//<floordiv>`, :eql:op:`%<mod>`\n    * - :eql:op:`?? <coalesce>`\n    * - :eql:op:`distinct`, unary :eql:op:`-<uminus>`\n    * - :eql:op:`^<pow>`\n    * - :eql:op:`type cast <cast>`\n    * - :eql:op:`array[] <arrayidx>`,\n        :eql:op:`str[] <stridx>`,\n        :eql:op:`json[] <jsonidx>`,\n        :eql:op:`bytes[] <bytesidx>`\n    * - :eql:kw:`detached`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/paths.rst",
    "content": ".. _ref_reference_paths:\n\n=====\nPaths\n=====\n\n\nA *path expression* (or simply a *path*) represents a set of values that are\nreachable when traversing a given sequence of links or properties from some\nsource set.\n\nThe result of a path expression depends on whether it terminates with a link or\nproperty reference.\n\na) if a path *does not* end with a property reference, then it represents a\n   unique set of objects reachable from the set at the root of the path;\n\nb) if a path *does* end with a property reference, then it represents a\n   list of property values for every element in the unique set of\n   objects reachable from the set at the root of the path.\n\nThe syntactic form of a path is:\n\n.. eql:synopsis::\n\n    <expression> <path-step> [ <path-step> ... ]\n\n    # where <path-step> is:\n      <step-direction> <pointer-name>\n\nThe individual path components are:\n\n:eql:synopsis:`<expression>`\n    Any valid expression.\n\n:eql:synopsis:`<step-direction>`\n    It can be one of the following:\n\n    - ``.`` for an outgoing link reference\n    - ``.<`` for an incoming or :ref:`backlink <ref_datamodel_links>`\n      reference\n    - ``@`` for a link property reference\n\n:eql:synopsis:`<pointer-name>`\n    This must be a valid link or link property name.\n\n"
  },
  {
    "path": "docs/reference/reference/edgeql/select.rst",
    "content": ".. _ref_eql_statements_select:\n\nSelect\n======\n\n:eql-statement:\n:eql-haswith:\n\n``select``--retrieve or compute a set of values.\n\n.. eql:synopsis::\n\n    [ with <with-item> [, ...] ]\n\n    select <expr>\n\n    [ filter <filter-expr> ]\n\n    [ order by <order-expr> [direction] [then ...] ]\n\n    [ offset <offset-expr> ]\n\n    [ limit  <limit-expr> ] ;\n\n:eql:synopsis:`filter <filter-expr>`\n    The optional ``filter`` clause, where :eql:synopsis:`<filter-expr>`\n    is any expression that has a result of type :eql:type:`bool`.\n    The condition is evaluated for every element in the set produced by\n    the ``select`` clause.  The result of the evaluation of the\n    ``filter`` clause is a set of boolean values.  If at least one value\n    in this set is ``true``, the input element is included, otherwise\n    it is eliminated from the output.\n\n.. _ref_reference_select_order:\n\n:eql:synopsis:`order by <order-expr> [direction] [then ...]`\n    The optional ``order by`` clause has this general form:\n\n    .. eql:synopsis::\n\n        order by\n            <order-expr> [ asc | desc ] [ empty { first | last } ]\n            [ then ... ]\n\n    The ``order by`` clause produces a result set sorted according\n    to the specified expression or expressions, which are evaluated\n    for every element of the input set.\n\n    If two elements are equal according to the leftmost *expression*, they\n    are compared according to the next expression and so on.  If two\n    elements are equal according to all expressions, the resulting order\n    is undefined.\n\n    Each *expression* can be an arbitrary expression that results in a\n    value of an *orderable type*.  Primitive types are orderable,\n    object types are not.  Additionally, the result of each expression\n    must be an empty set or a singleton.  Using an expression that may\n    produce more elements is a compile-time error.\n\n    An optional ``asc`` or ``desc`` keyword can be added after any\n    *expression*.  If not specified ``asc`` is assumed by default.\n\n    If ``empty last`` is specified, then input values that produce\n    an empty set when evaluating an *expression* are sorted *after*\n    all other values; if ``empty first`` is specified, then they\n    are sorted *before* all other values.  If neither is specified,\n    ``empty first`` is assumed when ``asc`` is specified or implied,\n    and ``empty last`` when ``desc`` is specified.\n\n:eql:synopsis:`offset <offset-expr>`\n    The optional ``offset`` clause, where\n    :eql:synopsis:`<offset-expr>`\n    is a *singleton expression* of an integer type.\n    This expression is evaluated once and its result is used\n    to skip the first *element-count* elements of the input set\n    while producing the output.  If *element-count* evaluates to\n    an empty set, it is equivalent to ``offset 0``, which is equivalent\n    to omitting the ``offset`` clause.  If *element-count* evaluates\n    to a value that is larger then the cardinality of the input set,\n    an empty set is produced as the result.\n\n:eql:synopsis:`limit <limit-expr>`\n    The optional ``limit`` clause, where :eql:synopsis:`<limit-expr>`\n    is a *singleton expression* of an integer\n    type.  This expression is evaluated once and its result is used\n    to include only the first *element-count* elements of the input set\n    while producing the output.  If *element-count* evaluates to\n    an empty set, it is equivalent to specifying no ``limit`` clause.\n\n\nDescription\n-----------\n\n``select`` retrieves or computes a set of values.  The data\nflow of a ``select`` block can be conceptualized like this:\n\n.. eql:synopsis::\n\n    with module example\n\n    # select clause\n    select\n        <expr>  # compute a set of things\n\n    # optional clause\n    filter\n        <expr>  # filter the computed set\n\n    # optional clause\n    order by\n        <expr>  # define ordering of the filtered set\n\n    # optional clause\n    offset\n        <expr>  # slice the filtered/ordered set\n\n    # optional clause\n    limit\n        <expr>  # slice the filtered/ordered set\n\nPlease note that the ``order by`` clause defines ordering that can\nonly be relied upon if the resulting set is not used in any other\noperation. ``select``, ``offset`` and ``limit`` clauses are the only\nexception to that rule as they preserve the inherent ordering of the\nunderlying set.\n\nThe first clause is ``select``. It indicates that ``filter``, ``order\nby``, ``offset``, or ``limit`` clauses may follow an expression, i.e.\nit makes an expression into a ``select`` statement. Without any of the\noptional clauses a ``(select Expr)`` is completely equivalent to\n``Expr`` for any expression ``Expr``.\n\nConsider an example using the ``filter`` optional clause:\n\n.. code-block:: edgeql\n\n    with module example\n    select User {\n        name,\n        owned := (select\n            User.<owner[is Issue] {\n                number,\n                body\n            }\n        )\n    }\n    filter User.name like 'Alice%';\n\n\n\nThe above example retrieves a single user with a specific name. The\nfact that there is only one such user is a detail that can be well-\nknown and important to the creator of the database, but otherwise non-\nobvious. However, forcing the cardinality to be at most 1 by using the\n``limit`` clause ensures that a set with a single object or\n``{}`` is returned. This way any further code that relies on the\nresult of this query can safely assume there's only one result\navailable.\n\n.. code-block:: edgeql\n\n    with module example\n    select User {\n        name,\n        owned := (select\n            User.<owner[is Issue] {\n                number,\n                body\n            }\n        )\n    }\n    filter User.name like 'Alice%'\n    limit 1;\n\nNext example makes use of ``order by`` and ``limit`` clauses:\n\n.. code-block:: edgeql\n\n    with module example\n    select Issue {\n        number,\n        body,\n        due_date\n    }\n    filter\n        exists Issue.due_date\n        and\n        Issue.status.name = 'Open'\n    order by\n        Issue.due_date\n    limit 3;\n\nThe above query retrieves the top 3 open Issues with the closest due\ndate.\n\n\n.. _ref_eql_statements_select_filter:\n\nFilter\n------\n\nThe ``filter`` clause cannot affect anything aggregate-like in the\npreceding ``select`` clause. This is due to how ``filter`` clause\nworks. It can be conceptualized as a function like ``filter($input,\nset of $cond)``, where the ``$input`` represents the value of the\npreceding clause, while the ``$cond`` represents the filtering\ncondition expression. Consider the following:\n\n.. code-block:: edgeql\n\n    with module example\n    select count(User)\n    filter User.name like 'Alice%';\n\nThe above can be conceptualized as:\n\n.. code-block:: edgeql\n\n    with module example\n    select _filter(\n        count(User),\n        User.name like 'Alice%'\n    );\n\nIn this form it is more apparent that ``User`` is a ``set of``\nargument (of :eql:func:`count`), while ``User.name like 'Alice%'`` is\nalso a ``set of`` argument (of ``filter``). So the symbol ``User`` in\nthese two expressions exists in 2 parallel scopes. Contrast it with:\n\n.. code-block:: edgeql\n\n    # This will actually only count users whose name starts with\n    # 'Alice'.\n\n    with module example\n    select count(\n        (select User\n         filter User.name like 'Alice%')\n    );\n\n    # which can be represented as:\n    with module example\n    select count(\n        _filter(User,\n               User.name like 'Alice%')\n    );\n\nClause signatures\n-----------------\n\nHere is a summary of clauses that can be used with ``select``:\n\n- *A* filter ``set of`` *B*\n- *A* order by ``set of`` *B*\n- ``set of`` *A* offset ``set of`` *B*\n- ``set of`` *A* limit ``set of`` *B*\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`EdgeQL > Select <ref_eql_select>`\n  * - :ref:`Cheatsheets > Selecting data <ref_cheatsheet_select>`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/sess_reset_alias.rst",
    "content": ".. _ref_eql_statements_session_reset_alias:\n\nReset\n=====\n\n:eql-statement:\n\n\n``reset`` -- reset one or multiple session-level parameters\n\n.. eql:synopsis::\n\n    reset module ;\n    reset alias <alias> ;\n    reset alias * ;\n    reset global <name> ;\n\n\nDescription\n-----------\n\nThis command allows resetting one or many configuration parameters of\nthe current session.\n\n\nVariations\n----------\n\n:eql:synopsis:`reset module`\n    Reset the default module name back to \"default\" for the current\n    session.\n\n    For example, if a module ``foo`` contains type ``FooType``,\n    the following is how the ``set`` and ``reset`` commands can be used\n    to alias it:\n\n    .. code-block:: edgeql\n\n        # Set the default module to \"foo\" for the current session.\n        set module foo;\n\n        # This query is now equivalent to \"select foo::FooType\".\n        select FooType;\n\n        # Reset the default module for the current session.\n        reset module;\n\n        # This query will now produce an error.\n        select FooType;\n\n\n:eql:synopsis:`reset alias <alias>`\n    Reset :eql:synopsis:`<alias>` for the current session.\n\n    For example:\n\n    .. code-block:: edgeql\n\n        # Alias the \"std\" module as \"foo\".\n        set alias foo as module std;\n\n        # Now \"std::min()\" can be called as \"foo::min()\" in\n        # the current session.\n        select foo::min({1});\n\n        # Reset the alias.\n        reset alias foo;\n\n        # Now this query will error out, as there is no\n        # module \"foo\".\n        select foo::min({1});\n\n:eql:synopsis:`reset alias *`\n    Reset all aliases defined in the current session.  This command\n    affects aliases set with :eql:stmt:`set alias <set>` and\n    :eql:stmt:`set module <set>`. The default module will be set to \"default\".\n\n    Example:\n\n    .. code-block:: edgeql\n\n        # Reset all custom aliases for the current session.\n        reset alias *;\n\n\n:eql:synopsis:`reset global <name>`\n    Reset the global variable *name* to its default value or ``{}`` if the\n    variable has no default value and is ``optional``.\n\n\nExamples\n--------\n\n.. code-block:: edgeql\n\n    reset module;\n\n    reset alias foo;\n\n    reset alias *;\n\n    reset global current_user_id;\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Reference > EdgeQL > Set <ref_eql_statements_session_set_alias>`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/sess_set_alias.rst",
    "content": ".. _ref_eql_statements_session_set_alias:\n\nSet\n===\n\n:eql-statement:\n\n\n``set`` -- set one or multiple session-level parameters\n\n.. eql:synopsis::\n\n    set module <module> ;\n    set alias <alias> as module <module> ;\n    set global <name> := <expr> ;\n\n\nDescription\n-----------\n\nThis command allows altering the configuration of the current session.\n\n\nVariations\n----------\n\n:eql:synopsis:`set module <module>`\n    Set the default module for the current section to *module*.\n\n    For example, if a module ``foo`` contains type ``FooType``,\n    the following is how the type can be referred to:\n\n    .. code-block:: edgeql\n\n        # Use the fully-qualified name.\n        select foo::FooType;\n\n        # Use the WITH clause to define the default module\n        # for the query.\n        with module foo select foo::FooType;\n\n        # Set the default module for the current session ...\n        set module foo;\n        # ... and use an unqualified name.\n        select FooType;\n\n\n:eql:synopsis:`set alias <alias> as module <module>`\n    Define :eql:synopsis:`<alias>` for the\n    :eql:synopsis:`<module>`.\n\n    For example:\n\n    .. code-block:: edgeql\n\n        # Use the fully-qualified name.\n        select foo::FooType;\n\n        # Use the WITH clause to define a custom alias\n        # for the \"foo\" module.\n        with bar as module foo\n        select bar::FooType;\n\n        # Define \"bar\" as an alias for the \"foo\" module for\n        # the current session ...\n        set alias bar as module foo;\n        # ... and use \"bar\" instead of \"foo\".\n        select bar::FooType;\n\n\n:eql:synopsis:`set global <name> := <expr>`\n    Set the global variable *name* to the specified value.\n\n    For example:\n\n    .. code-block:: edgeql\n\n        # Set the global variable \"current_user_id\".\n        set global current_user_id :=\n            <uuid>'00ea8eaa-02f9-11ed-a676-6bd11cc6c557';\n\n        # We can now use that value in a query.\n        select User { name }\n        filter .id = global current_user_id;\n\n\nExamples\n--------\n\n.. code-block:: edgeql\n\n    set module foo;\n\n    set alias foo AS module std;\n\n    set global current_user_id :=\n        <uuid>'00ea8eaa-02f9-11ed-a676-6bd11cc6c557';\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Reference > EdgeQL > Reset\n      <ref_eql_statements_session_reset_alias>`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/shapes.rst",
    "content": ".. _ref_reference_shapes:\n\n======\nShapes\n======\n\nA *shape* is a powerful syntactic construct that can be used to describe\ntype variants in queries, data in ``insert`` and ``update`` statements,\nand to specify the format of statement output.\n\nShapes always follow an expression, and are a list of *shape elements*\nenclosed in curly braces:\n\n.. eql:synopsis::\n\n    <expr> \"{\"\n        <shape_element> [, ...]\n    \"}\"\n\n\nShape element has the following syntax:\n\n.. eql:synopsis::\n\n    [ \"[\" is <object-type> \"]\" ] <pointer-spec>\n\nIf an optional :eql:synopsis:`<object-type>` filter is used,\n:eql:synopsis:`<pointer-spec>` will only apply to those objects in\nthe :eql:synopsis:`<expr>` set that are instances of\n:eql:synopsis:`<object-type>`.\n\n:eql:synopsis:`<pointer-spec>` is one of the following:\n\n- a name of an existing link or property of a type produced\n  by :eql:synopsis:`<expr>`;\n\n- a declaration of a computed link or property in the form\n\n  .. eql:synopsis ::\n\n    [@]<name> := <ptrexpr>\n\n\n- a *subshape* in the form\n\n  .. eql:synopsis ::\n\n    <pointer-name>: [ \"[\" is <target-type> \"]\" ] \"{\" ... \"}\"`\n\n  The :eql:synopsis:`<pointer-name>` is the name of an existing link\n  or property, and :eql:synopsis:`<target-type>` is an optional object\n  type that specifies the type of target objects selected or inserted,\n  depending on the context.\n\nShaping Query Results\n=====================\n\nAt the end of the day, EdgeQL has two jobs that are similar, yet distinct:\n\n1) Express the values that we want computed.\n2) Arrange the values into a particular shape that we want.\n\nConsider the task of getting \"names of users and all of the friends'\nnames associated with the given user\" in a database defined by the\nfollowing schema:\n\n.. code-block:: sdl\n\n    type User {\n        required name: str;\n        multi friends: User;\n    }\n\nIf we only concern ourselves with getting the values, then a\nreasonable solution to this might be:\n\n.. code-block:: edgeql-repl\n\n    db> select (User.name, User.friends.name ?? '');\n    {\n      ('Alice', 'Cameron'),\n      ('Alice', 'Dana'),\n      ('Billie', 'Dana'),\n      ('Cameron', ''),\n      ('Dana', 'Alice'),\n      ('Dana', 'Billie'),\n      ('Dana', 'Cameron'),\n    }\n\nThis particular solution is very similar to what one might get using\nSQL. It's equivalent to a table with \"user name\" and \"friend name\"\ncolumns. It gets the job done, albeit with some redundant repeating of\n\"user names\".\n\nWe can improve things a little and reduce the repetition by\naggregating all the friend names into an array:\n\n.. code-block:: edgeql-repl\n\n    db> select (User.name, array_agg(User.friends.name));\n    {\n      ('Alice', ['Cameron', 'Dana']),\n      ('Billie', ['Dana']),\n      ('Cameron', []),\n      ('Dana', ['Alice', 'Billie', 'Cameron']),\n    }\n\n\nThis achieves a couple of things: it's easier to see which friends\nbelong to which user and we no longer need the placeholder ``''`` for\nthose users who don't have friends.\n\nThe recommended way to get this information in Gel, however, is to\nuse *shapes*, because they mimic the structure of the data and the output:\n\n.. code-block:: edgeql-repl\n\n    db> select User {\n    ...     name,\n    ...     friends: {\n    ...         name\n    ...     }\n    ... };\n    {\n      default::User {\n        name: 'Alice',\n        friends: {\n          default::User {name: 'Cameron'},\n          default::User {name: 'Dana'},\n        },\n      },\n      default::User {name: 'Billie', friends: {default::User {name: 'Dana'}}},\n      default::User {name: 'Cameron', friends: {}},\n      default::User {\n        name: 'Dana',\n        friends: {\n          default::User {name: 'Alice'},\n          default::User {name: 'Billie'},\n          default::User {name: 'Cameron'},\n        },\n      },\n    }\n\nSo far the expression for the data that we wanted was also acceptable\nfor structuring the output, but what if that's not the case? Let's add\na condition and only show those users who have friends with either the\nletter \"i\" or \"o\" in their names:\n\n.. code-block:: edgeql-repl\n\n    db> select User {\n    ...     name,\n    ...     friends: {\n    ...         name\n    ...     }\n    ... } filter .friends.name ilike '%i%' or .friends.name ilike '%o%';\n    {\n      default::User {\n        name: 'Alice',\n        friends: {\n          default::User {name: 'Cameron'},\n          default::User {name: 'Dana'},\n        },\n      },\n      default::User {\n        name: 'Dana',\n        friends: {\n          default::User {name: 'Alice'},\n          default::User {name: 'Billie'},\n          default::User {name: 'Cameron'},\n        },\n      },\n    }\n\nThat ``filter`` is getting a bit bulky, so perhaps we can just factor\nthese flags out as part of the shape's computed properties:\n\n.. code-block:: edgeql-repl\n\n    db> select User {\n    ...     name,\n    ...     friends: {\n    ...         name\n    ...     },\n    ...     has_i := .friends.name ilike '%i%',\n    ...     has_o := .friends.name ilike '%o%',\n    ... } filter .has_i or .has_o;\n    {\n      default::User {\n        name: 'Alice',\n        friends: {\n          default::User {name: 'Cameron'},\n          default::User {name: 'Dana'},\n        },\n        has_i: {false, false},\n        has_o: {true, false},\n      },\n      default::User {\n        name: 'Dana',\n        friends: {\n          default::User {name: 'Alice'},\n          default::User {name: 'Billie'},\n          default::User {name: 'Cameron'},\n        },\n        has_i: {true, true, false},\n        has_o: {false, false, true},\n      },\n    }\n\nIt looks like this refactoring came at the cost of putting extra\nthings into the output. In this case we don't want our intermediate\ncalculations to actually show up in the output, so what can we do? In\n|Gel| the output structure is determined *only* by the expression\nappearing in the top-level :eql:stmt:`select`. This means\nthat we can move our intermediate calculations into the :eql:kw:`with` block:\n\n.. code-block:: edgeql-repl\n\n    db> with U := (\n    ...     select User {\n    ...         has_i := .friends.name ilike '%i%',\n    ...         has_o := .friends.name ilike '%o%',\n    ...     }\n    ... )\n    ... select U {\n    ...     name,\n    ...     friends: {\n    ...         name\n    ...     },\n    ... } filter .has_i or .has_o;\n    {\n      default::User {\n        name: 'Alice',\n        friends: {\n          default::User {name: 'Cameron'},\n          default::User {name: 'Dana'},\n        },\n      },\n      default::User {\n        name: 'Dana',\n        friends: {\n          default::User {name: 'Alice'},\n          default::User {name: 'Billie'},\n          default::User {name: 'Cameron'},\n        },\n      },\n    }\n\nThis way we can use ``has_i`` and ``has_o`` in our query without\nleaking them into the output.\n\nGeneral Shaping Rules\n=====================\n\nIn Gel typically all shapes appearing in the top-level\n:eql:stmt:`select` should be reflected in the output. This\nalso applies to shapes no matter where and how they are nested.\nAside from other shapes, this includes nesting in arrays:\n\n.. code-block:: edgeql-repl\n\n    db> select array_agg(User {name});\n    {\n      [\n        default::User {name: 'Alice'},\n        default::User {name: 'Billie'},\n        default::User {name: 'Cameron'},\n        default::User {name: 'Dana'},\n      ],\n    }\n\n... or tuples:\n\n.. code-block:: edgeql-repl\n\n    db> select enumerate(User {name});\n    {\n      (0, default::User {name: 'Alice'}),\n      (1, default::User {name: 'Billie'}),\n      (2, default::User {name: 'Cameron'}),\n      (3, default::User {name: 'Dana'}),\n    }\n\nYou can safely access a tuple element and expect the output shape to\nbe intact:\n\n.. code-block:: edgeql-repl\n\n    db> select enumerate(User{name}).1;\n    {\n      default::User {name: 'Alice'},\n      default::User {name: 'Billie'},\n      default::User {name: 'Cameron'},\n      default::User {name: 'Dana'},\n    }\n\nAccessing array elements or working with slices also preserves output\nshape and is analogous to using ``offset`` and ``limit`` when working\nwith sets:\n\n.. code-block:: edgeql-repl\n\n    db> select array_agg(User {name})[2];\n    {default::User {name: 'Cameron'}}\n\n\nLosing Shapes\n=============\n\nThere are some situations where shape information gets completely or\npartially discarded. Any such operation also prevents the altered\nshape from appearing in the output altogether.\n\nIn order for the shape to be preserved, the original expression type\nmust be preserved. This means that :eql:op:`union` can alter the shape,\nbecause the result of a :eql:op:`union` is a :eql:op:`union type\n<typeor>`. So you can still refer to the common properties, but not to\nthe properties that appeared in the shape.\n\nAs mentioned above, since :eql:op:`union` potentially alters the\nexpression shape it never preserves output shape, even when the\nunderlying type wasn't altered:\n\n.. code-block:: edgeql-repl\n\n    db> select User{name} union User{name};\n    {\n      default::User {id: 7769045a-27bf-11ec-94ea-3f6c0ae59eb3},\n      default::User {id: 7b42ed20-27bf-11ec-94ea-7700ec77834e},\n      default::User {id: 7fcedbc4-27bf-11ec-94ea-73dcb6f297a4},\n      default::User {id: 82f52646-27bf-11ec-94ea-3718ffb8dd15},\n      default::User {id: 7769045a-27bf-11ec-94ea-3f6c0ae59eb3},\n      default::User {id: 7b42ed20-27bf-11ec-94ea-7700ec77834e},\n      default::User {id: 7fcedbc4-27bf-11ec-94ea-73dcb6f297a4},\n      default::User {id: 82f52646-27bf-11ec-94ea-3718ffb8dd15},\n    }\n\nListing several items inside a set ``{ ... }`` functions identically\nto a :eql:op:`union` and so will also produce a union type and remove\nshape from output.\n\nAnother subtle way for a type union to remove the shape from the output\nis by the :eql:op:`?? <coalesce>` and the :eql:op:`if..else` operators. Both\nof them determine the result type as the union of the left and right\noperands:\n\n.. code-block:: edgeql-repl\n\n    db> select <User>{} ?? User {name};\n    {\n      default::User {id: 7769045a-27bf-11ec-94ea-3f6c0ae59eb3},\n      default::User {id: 7b42ed20-27bf-11ec-94ea-7700ec77834e},\n      default::User {id: 7fcedbc4-27bf-11ec-94ea-73dcb6f297a4},\n      default::User {id: 82f52646-27bf-11ec-94ea-3718ffb8dd15},\n    }\n\nShapes survive array creation (either via :eql:func:`array_agg` or by\nusing ``[ ... ]``), but they follow the same rules as for :eql:op:`union`\nfor array :eql:op:`concatenation <arrayplus>`. Basically the element type\nof the resulting array must be a union type and thus all shape\ninformation is lost:\n\n.. code-block:: edgeql-repl\n\n    db> select array_agg(User{name}) ++ array_agg(User{name});\n    {\n      [\n        default::User {id: 7769045a-27bf-11ec-94ea-3f6c0ae59eb3},\n        default::User {id: 7b42ed20-27bf-11ec-94ea-7700ec77834e},\n        default::User {id: 7fcedbc4-27bf-11ec-94ea-73dcb6f297a4},\n        default::User {id: 82f52646-27bf-11ec-94ea-3718ffb8dd15},\n        default::User {id: 7769045a-27bf-11ec-94ea-3f6c0ae59eb3},\n        default::User {id: 7b42ed20-27bf-11ec-94ea-7700ec77834e},\n        default::User {id: 7fcedbc4-27bf-11ec-94ea-73dcb6f297a4},\n        default::User {id: 82f52646-27bf-11ec-94ea-3718ffb8dd15},\n      ],\n    }\n\n.. note::\n\n    The :eql:stmt:`for` statement preserves the shape given inside the\n    ``union`` clause, effectively applying the shape to its entire\n    result.\n"
  },
  {
    "path": "docs/reference/reference/edgeql/tx_commit.rst",
    "content": "..\n    Portions Copyright (c) 2019 MagicStack Inc. and the Gel authors.\n\n    Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group\n    Portions Copyright (c) 1994, The Regents of the University of California\n\n    Permission to use, copy, modify, and distribute this software and its\n    documentation for any purpose, without fee, and without a written agreement\n    is hereby granted, provided that the above copyright notice and this\n    paragraph and the following two paragraphs appear in all copies.\n\n    IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR\n    DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING\n    LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS\n    DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE\n    POSSIBILITY OF SUCH DAMAGE.\n\n    THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,\n    INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY\n    AND FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS\n    ON AN \"AS IS\" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO\n    PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.\n\n\n.. _ref_eql_statements_commit_tx:\n\nCommit\n======\n\n:eql-statement:\n\n\n``commit`` -- commit the current transaction\n\n.. eql:synopsis::\n\n    commit ;\n\n\nExample\n-------\n\nCommit the current transaction:\n\n.. code-block:: edgeql\n\n    commit;\n\n\nDescription\n-----------\n\nThe ``commit`` command  commits the current transaction. All changes\nmade by the transaction become visible to others and are guaranteed to\nbe durable if a crash occurs.\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Reference > EdgeQL > Start transaction\n      <ref_eql_statements_start_tx>`\n  * - :ref:`Reference > EdgeQL > Rollabck\n      <ref_eql_statements_rollback_tx>`\n  * - :ref:`Reference > EdgeQL > Declare savepoint\n      <ref_eql_statements_declare_savepoint>`\n  * - :ref:`Reference > EdgeQL > Rollback to savepoint\n      <ref_eql_statements_rollback_savepoint>`\n  * - :ref:`Reference > EdgeQL > Release savepoint\n      <ref_eql_statements_release_savepoint>`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/tx_rollback.rst",
    "content": "..\n    Portions Copyright (c) 2019 MagicStack Inc. and the Gel authors.\n\n    Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group\n    Portions Copyright (c) 1994, The Regents of the University of California\n\n    Permission to use, copy, modify, and distribute this software and its\n    documentation for any purpose, without fee, and without a written agreement\n    is hereby granted, provided that the above copyright notice and this\n    paragraph and the following two paragraphs appear in all copies.\n\n    IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR\n    DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING\n    LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS\n    DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE\n    POSSIBILITY OF SUCH DAMAGE.\n\n    THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,\n    INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY\n    AND FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS\n    ON AN \"AS IS\" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO\n    PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.\n\n\n.. _ref_eql_statements_rollback_tx:\n\nRollback\n========\n\n:eql-statement:\n\n\n``rollback`` -- abort the current transaction\n\n.. eql:synopsis::\n\n    rollback ;\n\n\nExample\n-------\n\nAbort the current transaction:\n\n.. code-block:: edgeql\n\n    rollback;\n\n\nDescription\n-----------\n\nThe ``rollback`` command rolls back the current transaction and causes all\nupdates made by the transaction to be discarded.\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Reference > EdgeQL > Start transaction\n      <ref_eql_statements_start_tx>`\n  * - :ref:`Reference > EdgeQL > Commit\n      <ref_eql_statements_commit_tx>`\n  * - :ref:`Reference > EdgeQL > Declare savepoint\n      <ref_eql_statements_declare_savepoint>`\n  * - :ref:`Reference > EdgeQL > Rollback to savepoint\n      <ref_eql_statements_rollback_savepoint>`\n  * - :ref:`Reference > EdgeQL > Release savepoint\n      <ref_eql_statements_release_savepoint>`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/tx_sp_declare.rst",
    "content": "..\n    Portions Copyright (c) 2019 MagicStack Inc. and the Gel authors.\n\n    Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group\n    Portions Copyright (c) 1994, The Regents of the University of California\n\n    Permission to use, copy, modify, and distribute this software and its\n    documentation for any purpose, without fee, and without a written agreement\n    is hereby granted, provided that the above copyright notice and this\n    paragraph and the following two paragraphs appear in all copies.\n\n    IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR\n    DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING\n    LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS\n    DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE\n    POSSIBILITY OF SUCH DAMAGE.\n\n    THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,\n    INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY\n    AND FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS\n    ON AN \"AS IS\" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO\n    PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.\n\n\n.. _ref_eql_statements_declare_savepoint:\n\nDeclare savepoint\n=================\n\n:eql-statement:\n\n\n``declare savepoint`` -- declare a savepoint within the current transaction\n\n.. eql:synopsis::\n\n    declare savepoint <savepoint-name> ;\n\n\nDescription\n-----------\n\n``savepoint`` establishes a new savepoint within the current\ntransaction.\n\nA savepoint is a special mark inside a transaction that allows all\ncommands that are executed after it was established to be rolled back,\nrestoring the transaction state to what it was at the time of the\nsavepoint.\n\nIt is an error to declare a savepoint outside of a transaction.\n\n\nExample\n-------\n\n.. code-block:: edgeql\n\n    # Will select no objects:\n    select test::TestObject { name };\n\n    start transaction;\n\n        insert test::TestObject { name := 'q1' };\n        insert test::TestObject { name := 'q2' };\n\n        # Will select two TestObjects with names 'q1' and 'q2'\n        select test::TestObject { name };\n\n        declare savepoint f1;\n            insert test::TestObject { name:='w1' };\n\n            # Will select three TestObjects with names\n            # 'q1' 'q2', and 'w1'\n            select test::TestObject { name };\n        rollback to savepoint f1;\n\n        # Will select two TestObjects with names 'q1' and 'q2'\n        select test::TestObject { name };\n\n    rollback;\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Reference > EdgeQL > Start transaction\n      <ref_eql_statements_start_tx>`\n  * - :ref:`Reference > EdgeQL > Commit\n      <ref_eql_statements_commit_tx>`\n  * - :ref:`Reference > EdgeQL > Rollabck\n      <ref_eql_statements_rollback_tx>`\n  * - :ref:`Reference > EdgeQL > Rollback to savepoint\n      <ref_eql_statements_rollback_savepoint>`\n  * - :ref:`Reference > EdgeQL > Release savepoint\n      <ref_eql_statements_release_savepoint>`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/tx_sp_release.rst",
    "content": "..\n    Portions Copyright (c) 2019 MagicStack Inc. and the Gel authors.\n\n    Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group\n    Portions Copyright (c) 1994, The Regents of the University of California\n\n    Permission to use, copy, modify, and distribute this software and its\n    documentation for any purpose, without fee, and without a written agreement\n    is hereby granted, provided that the above copyright notice and this\n    paragraph and the following two paragraphs appear in all copies.\n\n    IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR\n    DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING\n    LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS\n    DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE\n    POSSIBILITY OF SUCH DAMAGE.\n\n    THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,\n    INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY\n    AND FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS\n    ON AN \"AS IS\" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO\n    PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.\n\n\n.. _ref_eql_statements_release_savepoint:\n\nRelease savepoint\n=================\n\n:eql-statement:\n\n\n``release savepoint`` -- release a previously declared savepoint\n\n.. eql:synopsis::\n\n    release savepoint <savepoint-name> ;\n\n\nDescription\n-----------\n\n``release savepoint`` destroys a savepoint previously defined in the\ncurrent transaction.\n\nDestroying a savepoint makes it unavailable as a rollback point,\nbut it has no other user visible behavior. It does not undo the effects\nof commands executed after the savepoint was established.\n(To do that, see :eql:stmt:`rollback to savepoint`.)\n\n``release savepoint`` also destroys all savepoints that were\nestablished after the named savepoint was established.\n\n\nExample\n-------\n\n.. code-block:: edgeql\n\n    start transaction;\n    # ...\n    declare savepoint f1;\n    # ...\n    release savepoint f1;\n    # ...\n    rollback;\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Reference > EdgeQL > Start transaction\n      <ref_eql_statements_start_tx>`\n  * - :ref:`Reference > EdgeQL > Commit\n      <ref_eql_statements_commit_tx>`\n  * - :ref:`Reference > EdgeQL > Rollabck\n      <ref_eql_statements_rollback_tx>`\n  * - :ref:`Reference > EdgeQL > Declare savepoint\n      <ref_eql_statements_declare_savepoint>`\n  * - :ref:`Reference > EdgeQL > Rollback to savepoint\n      <ref_eql_statements_rollback_savepoint>`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/tx_sp_rollback.rst",
    "content": "..\n    Portions Copyright (c) 2019 MagicStack Inc. and the Gel authors.\n\n    Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group\n    Portions Copyright (c) 1994, The Regents of the University of California\n\n    Permission to use, copy, modify, and distribute this software and its\n    documentation for any purpose, without fee, and without a written agreement\n    is hereby granted, provided that the above copyright notice and this\n    paragraph and the following two paragraphs appear in all copies.\n\n    IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR\n    DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING\n    LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS\n    DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE\n    POSSIBILITY OF SUCH DAMAGE.\n\n    THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,\n    INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY\n    AND FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS\n    ON AN \"AS IS\" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO\n    PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.\n\n\n.. _ref_eql_statements_rollback_savepoint:\n\nRollback to savepoint\n=====================\n\n:eql-statement:\n\n\n``rollback to savepoint`` -- rollback to a savepoint within the current\ntransaction\n\n\n.. eql:synopsis::\n\n    rollback to savepoint <savepoint-name> ;\n\n\nDescription\n-----------\n\nRollback all commands that were executed after the savepoint\nwas established. The savepoint remains valid and can be rolled back\nto again later, if needed.\n\n``rollback to savepoint`` implicitly destroys all savepoints that\nwere established after the named savepoint.\n\n\nExample\n-------\n\n.. code-block:: edgeql\n\n    start transaction;\n    # ...\n    declare savepoint f1;\n    # ...\n    rollback to savepoint f1;\n    # ...\n    rollback;\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Reference > EdgeQL > Start transaction\n      <ref_eql_statements_start_tx>`\n  * - :ref:`Reference > EdgeQL > Commit\n      <ref_eql_statements_commit_tx>`\n  * - :ref:`Reference > EdgeQL > Rollabck\n      <ref_eql_statements_rollback_tx>`\n  * - :ref:`Reference > EdgeQL > Declare savepoint\n      <ref_eql_statements_declare_savepoint>`\n  * - :ref:`Reference > EdgeQL > Release savepoint\n      <ref_eql_statements_release_savepoint>`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/tx_start.rst",
    "content": "..\n    Portions Copyright (c) 2019 MagicStack Inc. and the Gel authors.\n\n    Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group\n    Portions Copyright (c) 1994, The Regents of the University of California\n\n    Permission to use, copy, modify, and distribute this software and its\n    documentation for any purpose, without fee, and without a written agreement\n    is hereby granted, provided that the above copyright notice and this\n    paragraph and the following two paragraphs appear in all copies.\n\n    IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR\n    DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING\n    LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS\n    DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE\n    POSSIBILITY OF SUCH DAMAGE.\n\n    THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,\n    INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY\n    AND FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS\n    ON AN \"AS IS\" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO\n    PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.\n\n\n.. _ref_eql_statements_start_tx:\n\nStart transaction\n=================\n\n:eql-statement:\n\n\n``start transaction`` -- start a transaction\n\n.. eql:synopsis::\n\n    start transaction <transaction-mode> [ , ... ] ;\n\n    # where <transaction-mode> is one of:\n\n    isolation repeatable read\n    isolation serializable\n    read write | read only\n    deferrable | not deferrable\n\n\nDescription\n-----------\n\nThis command starts a new transaction block.\n\nAny Gel command outside of an explicit transaction block starts\nan implicit transaction block; the transaction is then automatically\ncommitted if the command was executed successfully, or automatically\nrollbacked if there was an error.  This behavior is often called\n\"autocommit\".\n\nWhen isolation is not specified, it defaults to ``serializable``.\n\nParameters\n----------\n\nThe :eql:synopsis:`<transaction-mode>` can be one of the following:\n\n:eql:synopsis:`isolation serializable`\n    All statements in the current transaction can only see data\n    changes that were committed before the first query or data\n    modification statement was executed within this transaction.\n    If a pattern of reads and writes among concurrent serializable\n    transactions creates a situation that could not have occurred\n    in any serial (one-at-a-time) execution of those transactions,\n    one of them will be rolled back with a serialization failure.\n\n    This level is the default isolation level.\n\n    Note that, compared to ``repeatable read``, serializable level has a\n    significantly higher probability of resulting in serialization failures,\n    requires the whole transaction to be retried. If acceptable, consider using\n    ``repeatable read`` or\n    :ref:`prefer repeatable read <prefer_repeatable_read>`.\n\n:eql:synopsis:`isolation repeatable read`\n    All statements in the current transaction can only see data\n    changes that were committed before the first query or data\n    modification statement was executed within this transaction.\n\n    Compared to ``serializable``, this level is less likely to result in\n    serialization failures.\n\n    It is however possible for this level to allow serialization anomalies.\n    This constitutes a series of transactions that would not be allowed if they\n    were executed serially instead of concurrently.\n\n    For example, assume ``type X { is_selected: bool }`` and following query:\n\n    .. code-block:: edgeql\n\n        # transaction A: unselect all selected\n        update X filter .is_selected set { is_selected := false };\n\n        # transaction B: select all unselected\n        update X filter not .is_selected set { is_selected := true };\n\n    Running these two transactions serially would result in either all ``X``\n    being select or none being selected. But if executed concurrently, even with\n    the ``repeatable read`` isolation level, we can end up with some ``X`` being\n    selected and some not.\n\n    To avoid this, we can use the ``serializable`` isolation level.\n\n:eql:synopsis:`read write`\n    Sets the transaction access mode to read/write.\n\n    This is the default.\n\n:eql:synopsis:`read only`\n    Sets the transaction access mode to read-only.  Any data\n    modifications with :eql:stmt:`insert`, :eql:stmt:`update`, or\n    :eql:stmt:`delete` are disallowed. Schema mutations via :ref:`DDL\n    <ref_eql_ddl>` are also disallowed.\n\n:eql:synopsis:`deferrable`\n    The transaction can be set to deferrable mode only when it is\n    ``serializable`` and ``read only``.  When all three of these\n    properties are selected for a transaction, the transaction\n    may block when first acquiring its snapshot, after which it is\n    able to run without the normal overhead of a ``serializable``\n    transaction and without any risk of contributing to or being\n    canceled by a serialization failure. This mode is well suited\n    for long-running reports or backups.\n\n\nExamples\n--------\n\nStart a new transaction and rollback it:\n\n.. code-block:: edgeql\n\n    start transaction;\n    select 'Hello World!';\n    rollback;\n\nStart a serializable deferrable transaction:\n\n.. code-block:: edgeql\n\n    start transaction isolation serializable, read only, deferrable;\n\n\n.. _prefer_repeatable_read:\n\nPrefer repeatable read\n----------------------\n\nIn addition to the isolation levels above, some client libraries also support\n``PreferRepeatableRead`` as a transaction isolation level.\nIn this mode, the server will analyze the query and use ``repeatable read``\nisolation level if it can. When it cannot, it will use ``serializable``\nisolation level.\n\nClient libraries that currently support this mode:\n\n* TypeScript/JS\n* Python\n* Go\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Reference > EdgeQL > Commit\n      <ref_eql_statements_commit_tx>`\n  * - :ref:`Reference > EdgeQL > Rollback\n      <ref_eql_statements_rollback_tx>`\n  * - :ref:`Reference > EdgeQL > Declare savepoint\n      <ref_eql_statements_declare_savepoint>`\n  * - :ref:`Reference > EdgeQL > Rollback to savepoint\n      <ref_eql_statements_rollback_savepoint>`\n  * - :ref:`Reference > EdgeQL > Release savepoint\n      <ref_eql_statements_release_savepoint>`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/update.rst",
    "content": ".. _ref_eql_statements_update:\n\nUpdate\n======\n\n:eql-statement:\n:eql-haswith:\n\n``update`` -- update objects in a database\n\n.. eql:synopsis::\n\n    [ with <with-item> [, ...] ]\n\n    update <selector-expr>\n\n    [ filter <filter-expr> ]\n\n    set <shape> ;\n\n``update`` changes the values of the specified links in all objects\nselected by *update-selector-expr* and, optionally, filtered by\n*filter-expr*.\n\n:eql:synopsis:`with`\n    Alias declarations.\n\n    The ``with`` clause allows specifying module aliases as well\n    as expression aliases that can be referenced by the ``update``\n    statement.  See :ref:`ref_eql_statements_with` for more information.\n\n:eql:synopsis:`update <selector-expr>`\n    An arbitrary expression returning a set of objects to be updated.\n\n:eql:synopsis:`filter <filter-expr>`\n    An expression of type :eql:type:`bool` used to filter the\n    set of updated objects.\n\n    :eql:synopsis:`<filter-expr>` is an expression that has a result\n    of type :eql:type:`bool`.  Only objects that satisfy the filter\n    expression will be updated.  See the description of the\n    ``filter`` clause of the :eql:stmt:`select` statement for more\n    information.\n\n:eql:synopsis:`set <shape>`\n    A shape expression with the\n    new values for the links of the updated object. There are three\n    possible assignment operations permitted within the ``set`` shape:\n\n    .. eql:synopsis::\n\n        set { <field> := <update-expr> [, ...] }\n\n        set { <field> += <update-expr> [, ...] }\n\n        set { <field> -= <update-expr> [, ...] }\n\n    The most basic assignment is the ``:=``, which just sets the\n    :eql:synopsis:`<field>` to the specified\n    :eql:synopsis:`<update-expr>`. The ``+=`` and ``-=`` either add or\n    remove the set of values specified by the\n    :eql:synopsis:`<update-expr>` from the *current* value of the\n    :eql:synopsis:`<field>`.\n\nOutput\n~~~~~~\n\nOn successful completion, an ``update`` statement returns the\nset of updated objects.\n\n\nExamples\n~~~~~~~~\n\nHere are a couple of examples of the ``update`` statement with simple\nassignments using ``:=``:\n\n.. code-block:: edgeql\n\n    # update the user with the name 'Alice Smith'\n    with module example\n    update User\n    filter .name = 'Alice Smith'\n    set {\n        name := 'Alice J. Smith'\n    };\n\n    # update all users whose name is 'Bob'\n    with module example\n    update User\n    filter .name like 'Bob%'\n    set {\n        name := User.name ++ '*'\n    };\n\nFor usage of ``+=`` and ``-=`` consider the following ``Post`` type:\n\n.. code-block:: sdl\n\n    # ... Assume some User type is already defined\n    type Post {\n        required title: str;\n        required body: str;\n        # A \"tags\" property containing a set of strings\n        multi tags: str;\n        author: User;\n    }\n\nThe following queries add or remove tags from some user's posts:\n\n.. code-block:: edgeql\n\n    with module example\n    update Post\n    filter .author.name = 'Alice Smith'\n    set {\n        # add tags\n        tags += {'example', 'edgeql'}\n    };\n\n    with module example\n    update Post\n    filter .author.name = 'Alice Smith'\n    set {\n        # remove a tag, if it exist\n        tags -= 'todo'\n    };\n\n\nThe statement ``for <x> in <expr>`` allows to express certain bulk\nupdates more clearly. See\n:ref:`ref_eql_forstatement` for more details.\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`EdgeQL > Update <ref_eql_update>`\n  * - :ref:`Cheatsheets > Updating data <ref_cheatsheet_update>`\n"
  },
  {
    "path": "docs/reference/reference/edgeql/volatility.rst",
    "content": ".. _ref_reference_volatility:\n\n\nVolatility\n==========\n\nThe **volatility** of an expression refers to how its value may change across\nsuccessive evaluations. \n\nExpressions may have one of the following volatilities, in order of increasing\nvolatility:\n\n* ``Immutable``: The expression cannot modify the database and is\n    guaranteed to have the same value *in all statements*.\n\n* ``Stable``: The expression cannot modify the database and is\n    guaranteed to have the same value *within a single statement*.\n\n* ``Volatile``: The expression cannot modify the database and can have\n    different values on successive evaluations.\n\n* ``Modifying``: The expression can modify the database and can have\n    different values on successive evaluations.\n\n\nExpressions\n-----------\n\nAll :ref:`primitives <ref_datamodel_primitives>`,\n:ref:`ranges <ref_std_range>`, and\n:ref:`multiranges <ref_std_multirange>` are ``Immutable``.\n\n:ref:`Arrays <ref_std_array>`, :ref:`tuples <ref_std_tuple>`, and\n:ref:`sets <ref_eql_sets>` have the volatility of their most volatile\ncomponent.\n\n:ref:`Globals <ref_datamodel_globals>` are always ``Stable``, even computed\nglobals with an immutable expression.\n\n\nObjects and shapes\n^^^^^^^^^^^^^^^^^^\n\n:ref:`Objects <ref_datamodel_object_types>` are generally ``Stable`` except:\n\n* Objects with a :ref:`shape <ref_eql_shapes>` containing a more volatile\n  computed pointer will have the volatility of its most volatile component.\n\n* :ref:`Free objects <ref_eql_select_free_objects>` have the volatility of\n  their most volatile component. They may be ``Immutable``.\n\nAn object's non-computed pointers are ``Stable``. Its computed pointers have\nthe volatility of their expressions.\n\nAny DML (i.e., :ref:`insert <ref_eql_insert>`, :ref:`update <ref_eql_update>`,\n:ref:`delete <ref_eql_delete>`) is ``Modifying``.\n\n\nFunctions and operators\n^^^^^^^^^^^^^^^^^^^^^^^\n\nUnless explicitly specified, a :ref:`function's <ref_eql_sdl_functions>`\nvolatility will be inferred from its body expression.\n\nA function call's volatility is highest of its body expression and its call\narguments.\n\nGiven:\n\n.. code-block:: sdl\n\n    # Immutable\n    function plus_primitive(x: float64) -> float64\n        using (x + 1);\n\n    # Stable\n    global one := 1;\n    function plus_global(x: float64) -> float64\n        using (x + one);\n\n    # Volatile\n    function plus_random(x: float64) -> float64\n        using (x + random());\n\n    # Modifying\n    type One {\n        val := 1;\n    };\n    function plus_insert(x: float64) -> float64\n        using (x + (insert One).val);\n\nSome example operator and function calls:\n\n.. code-block::\n\n    1 + 1:                    Immutable\n    1 + global one:           Stable\n    global one + random():    Volatile\n    (insert One).val:         Modifying\n    plus_primitive(1):        Immutable\n    plus_stable(1):           Stable\n    plus_random(global one):  Volatile\n    plus_insert(random()):    Immutable\n\n\nRestrictions\n------------\n\nSome features restrict the volatility of expressions. A lower volatility\ncan be used.\n\n:ref:`Indexes <ref_datamodel_indexes>` expressions must be ``Immutable``.\nWithin the index, pointers to the indexed object are treated as immutable\n\n:ref:`constraints <ref_datamodel_constraints>` expressions must be\n``Immutable``. Within the constraint, the ``__subject__`` and its pointers are\ntreated as immutable.\n\n:ref:`Access policies <ref_datamodel_access_policies>` must be ``Stable``.\n\n:ref:`Aliases <ref_eql_ddl_aliases>`, :ref:`globals <ref_datamodel_globals>`,\nand :ref:`computed pointers <ref_datamodel_computed>` in the schema must be\n``Stable``.\n\nThe :ref:`cartesian product <ref_reference_cardinality_cartesian>` of a\n``Volatile`` or ``Modifying`` expression is not allowed.\n\n.. code-block:: edgeql-repl\n\n    db> SELECT {1, 2} + random()\n    QueryError: can not take cross product of volatile operation\n\n``Modifying`` expressions are not allowed in a non-scalar argument to a\nfunction, except for :ref:`standard set functions <ref_std_set>`.\n\nThe non-optional parameters of ``Modifying``\n:ref:`functions <ref_datamodel_functions_modifying>` must have a\n:ref:`cardinality <ref_reference_cardinality>` of ``One``. Optional\nparameters must have a cardinality of ``AtMostOne``.\n"
  },
  {
    "path": "docs/reference/reference/edgeql/with.rst",
    "content": ".. _ref_eql_statements_with:\n\nWith block\n==========\n\n.. eql:keyword:: with\n\n    The ``with`` block in EdgeQL is used to define aliases.\n\n    The expression aliases are evaluated in the lexical scope they appear in,\n    not the scope where their alias is used. This means that refactoring\n    queries using aliases must be done with care so as not to alter the query\n    semantics.\n\nSpecifying a module\n+++++++++++++++++++\n\n.. eql:keyword:: module\n\n    Used inside a ``with`` block to specify module names.\n\nOne of the more basic and common uses of the ``with`` block is to\nspecify the default module that is used in a query. ``with module\n<name>`` construct indicates that whenever an identifier is used\nwithout any module specified explicitly, the module will default to\n``<name>`` and then fall back to built-ins from ``std`` module.\n\nThe following queries are exactly equivalent:\n\n.. code-block:: edgeql\n\n    with module example\n    select User {\n        name,\n        owned := (select\n            User.<owner[is Issue] {\n                number,\n                body\n            }\n        )\n    }\n    filter User.name like 'Alice%';\n\n    select example::User {\n        name,\n        owned := (select\n            example::User.<owner[is example::Issue] {\n                number,\n                body\n            }\n        )\n    }\n    filter example::User.name like 'Alice%';\n\n\nIt is also possible to define aliased modules in the ``with`` block.\nConsider the following query that needs to compare objects\ncorresponding to concepts defined in two different modules.\n\n.. code-block:: edgeql\n\n    with\n        module example,\n        f as module foo\n    select User {\n        name\n    }\n    filter .name = f::Foo.name;\n\nAnother use case is for giving short aliases to long module names\n(especially if module names contain ``.``).\n\n.. code-block:: edgeql\n\n    with\n        module example,\n        fbz as module foo.bar.baz\n    select User {\n        name\n    }\n    filter .name = fbz::Baz.name;\n\n\nLocal Expression Aliases\n++++++++++++++++++++++++\n\nIt is possible to define an alias for an arbitrary expression. The result\nset of an alias expression behaves as a completely independent set of a\ngiven name. The contents of the set are determined by the expression\nat the point where the alias is defined. In terms of scope, the alias\nexpression in the ``with`` block is in a sibling scope to the rest\nof the query.\n\nIt may be useful to factor out a common sub-expression from a larger\ncomplex query. This can be done by assigning the sub-expression a new\nsymbol in the ``with`` block. However, care must be taken to ensure\nthat this refactoring doesn't alter the meaning of the expression due\nto scope change.\n\nAll expression aliases defined in a ``with`` block must be referenced in\nthe body of the query.\n\n.. code-block:: edgeql\n\n    # Consider a query to get all users that own Issues and the\n    # comments those users made.\n    with module example\n    select Issue.owner {\n        name,\n        comments := Issue.owner.<owner[is Comment]\n    };\n\n    # The above query can be refactored like this:\n    with\n        module example,\n        U := Issue.owner\n    select U {\n        name,\n        comments := U.<owner[is Comment]\n    };\n\nAn example of incorrect refactoring would be:\n\n.. code-block:: edgeql\n\n    # This query gets a set of tuples of\n    # issues and their owners.\n    with\n        module example\n    select (Issue, Issue.owner);\n\n    # This query gets a set of tuples that\n    # result from a cartesian product of all issues\n    # with all owners. This is because ``Issue`` and ``U``\n    # are considered independent sets.\n    with\n        module example,\n        U := Issue.owner\n    select (Issue, U);\n\n\n.. _ref_edgeql_with_detached:\n\nDetached\n++++++++\n\n.. eql:keyword:: detached\n\n    The ``detached`` keyword marks an expression as not belonging to\n    any scope.\n\nA ``detached`` expression allows referring to some set as if it were\ndefined in the top-level ``with`` block. Basically, ``detached``\nexpressions ignore all current scopes they are nested in and only take\ninto account module aliases. The net effect is that it is possible to\nrefer to an otherwise related set as if it were unrelated:\n\n.. code-block:: edgeql\n\n    with module example\n    update User\n    filter .name = 'Dave'\n    set {\n        friends := (select detached User filter .name = 'Alice'),\n        coworkers := (select detached User filter .name = 'Bob')\n    };\n\nHere you can use the ``detached User`` expression, rather than having to\ndefine ``U := User`` in the ``with`` block just to allow it to be used\nin the body of the ``update``. The goal is to indicate that the\n``User`` in the ``update`` body is not in any way related to the\n``User`` that's being updated.\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`EdgeQL > With <ref_eql_with>`\n"
  },
  {
    "path": "docs/reference/reference/index.rst",
    "content": ".. versioned-section::\n\n.. _ref_reference_index:\n\n=========\nReference\n=========\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    edgeql/index\n\n\nThis section contains comprehensive reference documentation on the internals of\n|Gel|, the binary protocol, the formal syntax of EdgeQL, and more.\n"
  },
  {
    "path": "docs/reference/running/admin/configure.rst",
    "content": ".. _ref_eql_statements_configure:\n\nConfigure\n=========\n\n:eql-statement:\n\n\n``configure`` -- change a server configuration parameter\n\n.. eql:synopsis::\n\n    configure {session | current branch | instance}\n        set <parameter> := <value> ;\n    configure instance insert <parameter-class> <insert-shape> ;\n    configure {session | current branch | instance} reset <parameter> ;\n    configure {current branch | instance}\n        reset <parameter-class> [ filter <filter-expr> ] ;\n\n.. note::\n    Prior to |Gel| and |EdgeDB| 5.0 *branches* were called *databases*.\n    ``configure current branch`` is used to be called\n    ``configure current database``, which is still supported for backwards\n    compatibility.\n\n\nDescription\n-----------\n\nThis command allows altering the server configuration.\n\nThe effects of :eql:synopsis:`configure session` last until the end of the\ncurrent session. Some configuration parameters cannot be modified by\n:eql:synopsis:`configure session` and can only be set by\n:eql:synopsis:`configure instance`.\n\n:eql:synopsis:`configure current branch` is used to configure an\nindividual Gel branch within a server instance with the\nchanges persisted across server restarts.\n\n:eql:synopsis:`configure instance` is used to configure the entire Gel\ninstance with the changes persisted across server restarts.  This variant\nacts directly on the file system and cannot be rolled back, so it cannot\nbe used in a transaction block.\n\nThe :eql:synopsis:`configure instance insert` variant is used for composite\nconfiguration parameters, such as ``Auth``.\n\n\nParameters\n----------\n\n:eql:synopsis:`<parameter>`\n    The name of a primitive configuration parameter.  Available\n    configuration parameters are described in the :ref:`ref_std_cfg`\n    section.\n\n:eql:synopsis:`<parameter-class>`\n    The name of a composite configuration value class.  Available\n    configuration classes are described in the :ref:`ref_std_cfg`\n    section.\n\n:eql:synopsis:`<filter-expr>`\n    An expression that returns a value of type :eql:type:`std::bool`.\n    Only configuration objects matching this condition will be affected.\n\n\nExamples\n--------\n\nSet the ``listen_addresses`` parameter:\n\n.. code-block:: edgeql\n\n    configure instance set listen_addresses := {'127.0.0.1', '::1'};\n\nSet the ``query_work_mem`` parameter:\n\n.. code-block:: edgeql\n\n    configure instance set query_work_mem := <cfg::memory>'4MiB';\n\nAdd a Trust authentication method for \"my_user\":\n\n.. code-block:: edgeql\n\n    configure instance insert Auth {\n        priority := 1,\n        method := (insert Trust),\n        user := 'my_user'\n    };\n\nRemove all Trust authentication methods:\n\n.. code-block:: edgeql\n\n    configure instance reset Auth filter Auth.method is Trust;\n"
  },
  {
    "path": "docs/reference/running/admin/index.rst",
    "content": ".. _ref_admin:\n\nAdministration\n==============\n\nAdministrative commands for managing Gel:\n\n\n* :ref:`configure <ref_eql_statements_configure>`\n\n  Configure server behavior.\n\n* :ref:`role <ref_admin_roles>`\n\n  Create, remove, or alter a role.\n\n.. versionadded:: 5.0\n\n    New administrative commands were added in |EdgeDB| 5 release:\n\n    * :ref:`branch <ref_admin_branches>`\n\n      Create, remove, or alter a branch.\n\n    * :ref:`administer statistics_update() <ref_admin_statistics_update>`\n\n      Update internal statistics about data.\n\n    * :ref:`administer vacuum() <ref_admin_vacuum>`\n\n      Reclaim storage space.\n\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    configure\n    roles\n    statistics_update\n    vacuum\n"
  },
  {
    "path": "docs/reference/running/admin/roles.rst",
    "content": ".. _ref_admin_roles:\n\n=====\nRoles\n=====\n\n:edb-alt-title: Roles\n\n\nThis section describes the administrative commands pertaining to *roles*.\n\n\nCreate role\n===========\n\n:eql-statement:\n\nCreate a role.\n\n.. eql:synopsis::\n\n    create superuser role <name> [ extending <base> [, ...] ]\n    \"{\" <subcommand>; [...] \"}\" ;\n\n    # where <subcommand> is one of\n\n      set password := <password>\n\n\nDescription\n-----------\n\nThe command ``create role`` defines a new database role.\n\n:eql:synopsis:`superuser`\n    If specified, the created role will have the *superuser* status, and\n    will be exempt from\n    :ref:`all permission checks<ref_datamodel_permissions_superuser>`.\n\n    Prior to version 7.0, ``superuser`` qualifier was mandatory, i.e. it was not\n    possible to create non-superuser roles.\n\n:eql:synopsis:`<name>`\n    The name of the role to create.\n\n:eql:synopsis:`extending <base> [, ...]`\n    If specified, declares the parent roles for this role. The role\n    inherits all the privileges of the parents.\n\nThe following subcommands are allowed in the ``create role`` block:\n\n:eql:synopsis:`set password := <password>`\n    Set the password for the role.\n\n.. versionadded:: 7.0\n\n    :eql:synopsis:`set permissions := <permissions>`\n        Set :ref:`permissions <ref_datamodel_permissions>` for the role.\n        Value is a set of identifiers of either built-in permissions or\n        permissions defined in schema.\n\n        Roles also gain the permissions of their base Roles. \n\n        Roles that are *superusers* are implicitly granted all permissions, so\n        setting this does not have any effect.\n\n        Note that permission names are not validated and it is possible to\n        reference a permission that does not yet exist in any schema.\n\n    :eql:synopsis:`set branches := <branches>`\n        Configure a set of branches that this role is allowed to access.\n        When connecting to instance branch that is not in this set, connection\n        will be refused.\n\n        If set to ``'*'``, this branch can connect to all branches of the\n        instance. Defaults to ``'*'``.\n\n\nExamples\n--------\n\nCreate a new role:\n\n.. code-block:: edgeql\n\n    create role alice {\n        set password := 'wonderland';\n        set permissions := {\n          sys::perm::data_modifiction,\n          sys::perm::query_stats,\n          cfg::perm::configure_timeouts,\n          cfg::perm::configure_apply_access_policies,\n          ext::auth::perm::auth_read,\n          ext::auth::perm::auth_write,\n      };\n      set branches := {'main', 'staging'};\n    };\n\n\nAlter role\n==========\n\n:eql-statement:\n\nAlter an existing role.\n\n.. eql:synopsis::\n\n    alter role <name> \"{\" <subcommand>; [...] \"}\" ;\n\n    # where <subcommand> is one of\n\n      rename to <newname>\n      set password := <password>\n      extending ...\n\n\nDescription\n-----------\n\nThe command ``alter role`` changes the settings of an existing role.\n\n\n:eql:synopsis:`<name>`\n    The name of the role to alter.\n\nThe following subcommands are allowed in the ``alter role`` block:\n\n:eql:synopsis:`rename to <newname>`\n    Change the name of the role to *newname*.\n\n:eql:synopsis:`extending ...`\n    Alter the role parent list.  The full syntax of this subcommand is:\n\n    .. eql:synopsis::\n\n         extending <name> [, ...]\n            [ first | last | before <parent> | after <parent> ]\n\n    This subcommand makes the role a child of the specified list of\n    parent roles. The role inherits all the privileges of the parents.\n\n    It is possible to specify the position in the parent list\n    using the following optional keywords:\n\n    * ``first`` -- insert parent(s) at the beginning of the\n      parent list,\n    * ``last`` -- insert parent(s) at the end of the parent list,\n    * ``before <parent>`` -- insert parent(s) before an\n      existing *parent*,\n    * ``after <parent>`` -- insert parent(s) after an existing\n      *parent*.\n\n.. versionadded:: 7.0\n\n    :eql:synopsis:`set permissions := <permissions>`\n        Set :ref:`permissions <ref_datamodel_permissions>` for the role.\n        Value is a set of identifiers of either built-in permissions or\n        permissions defined in schema.\n\n        Roles that are *superusers* are implicitly granted all permissions, so\n        setting this does not have any effect.\n\n        Note that permission names are not validated and it is possible to\n        reference a permission that does not yet exist in the schema.\n\n    :eql:synopsis:`set branches := <branches>`\n        Configure a set of branches that this role is allowed to access.\n        When connecting to instance branch that is not in this set, connection\n        will be refused.\n\n        If set to ``'*'``, this branch can connect to all branches of the\n        instance. Defaults to ``'*'``.\n\n\nExamples\n--------\n\nAlter a role:\n\n.. code-block:: edgeql\n\n    alter role alice {\n        set password := 'new password';\n        set branches := {'*'};\n    };\n\n\nDrop role\n=========\n\n:eql-statement:\n\nRemove a role.\n\n.. eql:synopsis::\n\n    drop role <name> ;\n\nDescription\n-----------\n\nThe command ``drop role`` removes an existing role.\n\nExamples\n--------\n\nRemove a role:\n\n.. code-block:: edgeql\n\n    drop role alice;\n"
  },
  {
    "path": "docs/reference/running/admin/statistics_update.rst",
    "content": ".. versionadded:: 6.0\n\n.. _ref_admin_statistics_update:\n\n==============================\nadminister statistics_update()\n==============================\n\n:eql-statement:\n\nUpdate internal statistics about data.\n\n.. eql:synopsis::\n\n    administer statistics_update \"(\"\n      [<type_link_or_property> [, ...]]\n    \")\"\n\n\nDescription\n-----------\n\nUpdates statistics about the contents of data in the current branch.\nSubsequently, the query planner uses these statistics to help determine the\nmost efficient execution plans for queries.\n\n:eql:synopsis:`<type_link_or_property>`\n    If a type name or a path to a link or property are specified, that data\n    will be targeted for statistics update. If omitted, all user-accessible\n    data will be analyzed.\n\n\nExamples\n--------\n\nUpdate the statistics on type ``SomeType``:\n\n.. code-block:: edgeql\n\n    administer statistics_update(SomeType);\n\nUpdate statistics of type ``SomeType`` and the link ``OtherType.ptr``.\n\n.. code-block:: edgeql\n\n    administer statistics_update(SomeType, OtherType.ptr);\n\nUpdate statistics on everything that is user-accessible in the database:\n\n.. code-block:: edgeql\n\n    administer statistics_update();\n"
  },
  {
    "path": "docs/reference/running/admin/vacuum.rst",
    "content": ".. versionadded:: 5.0\n\n.. _ref_admin_vacuum:\n\n======\nVacuum\n======\n\n:eql-statement:\n\nReclaim storage space.\n\n.. eql:synopsis::\n\n    administer vacuum \"(\"\n      [<type_link_or_property> [, ...]]\n      [, full := {true | false}]\n      [, statistics_update := {true | false}]\n    \")\"\n\n\nDescription\n-----------\n\nCleans and reclaims storage by removing obsolete data.\n\n:eql:synopsis:`<type_link_or_property>`\n    If a type name or a path to a link or property are specified, that data\n    will be targeted for the vacuum operation. If omitted, all user-accessible\n    data will be targeted.\n\n:eql:synopsis:`full := {true | false}`\n    If set to ``true``, an exclusive lock is obtained and reclaimed space is\n    returned to the operating system. If set to ``false`` or if not set, the\n    command can operate alongside normal reading and writing of the database\n    and reclaimed space is kept available for reuse in the database, reducing\n    the rate of growth of the database.\n\n:eql:synopsis:`statistics_update := {true | false}`\n    If set to ``true``, updates statistics used by the planner to determine\n    the most efficient way to execute queries on specified data.  See also\n    :ref:`administer statistics_update() <ref_admin_statistics_update>`.\n\nExamples\n--------\n\nVacuum the type ``SomeType``:\n\n.. code-block:: edgeql\n\n    administer vacuum(SomeType);\n\nVacuum the type ``SomeType`` and the link ``OtherType.ptr`` and return\nreclaimed space to the operating system:\n\n.. code-block:: edgeql\n\n    administer vacuum(SomeType, OtherType.ptr, full := true);\n\nVacuum everything that is user-accessible in the database:\n\n.. code-block:: edgeql\n\n    administer vacuum();\n"
  },
  {
    "path": "docs/reference/running/backend_ha.rst",
    "content": ".. _ref_backend_ha:\n\nBackend high-availability\n=========================\n\nHigh availability is a sophisticated and systematic challenge, especially for\ndatabases. To address the problem, Gel server now supports selected\nhighly-available backend Postgres clusters, namely in 2 categories:\n\n* API-based HA\n* Adaptive HA without API\n\nWhen the backend HA feature is enabled in Gel, Gel server will try its\nbest to detect and react to backend failovers, whether a proper API is\navailable or not.\n\nDuring backend failover, no frontend connections will be closed; instead, all\nincoming queries will fail with a retryable error until failover has completed\nsuccessfully. If the query originates from a client that supports retrying\ntransactions, these queries may be retried by the client until the backend\nconnection is restored and the query can be properly resolved.\n\nAPI-based HA\n------------\n\n|Gel| server accepts different types of backends by looking into the protocol\nof the ``--backend-dsn`` command-line parameter. Gel supports the following\nDSN protocols currently:\n\n* ``stolon+consul+http://``\n* ``stolon+consul+https://``\n\nWhen using these protocols, Gel builds the actual DSN of the cluster's\nleader node by calling the corresponding API using credentials in the\n``--backend-dsn`` and subscribes to that API for failover events. Once failover\nis detected, Gel drops all backend connections and routes all new backend\nconnections to the new leader node.\n\n`Stolon <https://github.com/sorintlab/stolon/>`_ is an open-source cloud native\nPostgreSQL manager for PostgreSQL high availability. Currently, Gel supports\nusing a Stolon cluster as the backend in a Consul-based setup, where Gel\nacts as a Stolon proxy. This way, you only need to manage Stolon sentinels and\nkeepers, plus a Consul deployment. To use a Stolon cluster, run Gel server\nwith a DSN, like so:\n\n.. code-block:: bash\n\n    $ gel-server \\\n        --backend-dsn stolon+consul+http://localhost:8500/my-cluster\n\n|Gel| will connect to the Consul HTTP service at ``localhost:8500``, and\nsubscribe to the updates of the cluster named ``my-cluster``.\n\nUsing a regular ``postgres://`` DSN disables API-based HA.\n\n\nAdaptive HA\n-----------\n\n|Gel| also supports DNS-based generic HA backends. This may be a cloud\ndatabase with multi-AZ failover or some custom HA Postgres cluster that keeps\na DNS name always resolved to the leader node. Adaptive HA can be enabled with\na switch in addition to a regular backend DSN:\n\n.. code-block:: bash\n\n    $ gel-server \\\n        --backend-dsn postgres://xxx.rds.amazonaws.com \\\n        --enable-backend-adaptive-ha\n\nOnce enabled, Gel server will keep track of unusual backend events like\nunexpected disconnects or Postgres shutdown notifications. When a threshold is\nreached, Gel considers the backend to be in the \"failover\" state. It then\ndrops all current backend connections and try to re-establish new connections\nwith the same backend DSN. Because Gel doesn't cache resolved DNS values,\nthe new connections will be established with the new leader node.\n\nUnder the hood of adaptive HA, Gel maintains a state machine to avoid\nendless switch-overs in an unstable network. State changes only happen when\ncertain conditions are met.\n\n**Set of possible states:**\n\n* ``Healthy`` - all is good\n* ``Unhealthy`` - a staging state before failover\n* ``Failover`` - backend failover is in process\n\n**Rules of state switches:**\n\n``Unhealthy`` -> ``Healthy``\n\n* Successfully connected to a non-hot-standby backend.\n\n``Unhealthy`` -> ``Failover``\n\n* More than 60% (configurable with environment variable\n  :gelenv:`SERVER_BACKEND_ADAPTIVE_HA_DISCONNECT_PERCENT`) of existing pgcons\n  are \"unexpectedly disconnected\" (number of existing pgcons is captured at the\n  moment we change to ``Unhealthy`` state, and maintained on \"expected\n  disconnects\" too).\n* (and) In ``Unhealthy`` state for more than 30 seconds\n  (:gelenv:`SERVER_BACKEND_ADAPTIVE_HA_UNHEALTHY_MIN_TIME`).\n* (and) sys_pgcon is down.\n* (or) Postgres shutdown/hot-standby notification received.\n\n``Healthy`` -> ``Unhealthy``\n\n* Any unexpected disconnect.\n\n``Healthy`` -> ``Failover``\n\n* Postgres shutdown/hot-standby notification received.\n\n``Failover`` -> ``Healthy``\n\n* Successfully connected to a non-hot-standby backend.\n* (and) sys_pgcon is healthy.\n\n(\"pgcon\" is a code name for backend connections, and \"sys_pgcon\" is a special\nbackend connection which Gel uses to talk to the \"Gel system database\".)\n"
  },
  {
    "path": "docs/reference/running/configuration.rst",
    "content": ".. _ref_admin_config:\n\n=============\nConfiguration\n=============\n\nThe behavior of the Gel server is configurable with sensible defaults. Some configuration can be set on the running instance using configuration parameters, while other configuration is set at startup using environment variables or command line arguments to the |gel-server| binary.\n\nConfiguration parameters\n========================\n\n|Gel| exposes a number of configuration parameters that affect its behavior.  In this section we review the ways to change the server configuration, as well as detail each available configuration parameter.\n\nEdgeQL\n------\n\nThe :eql:stmt:`configure` command can be used to set the configuration parameters using EdgeQL. For example, you can use the CLI REPL to set the ``listen_addresses`` parameter:\n\n.. code-block:: edgeql-repl\n\n  gel> configure instance set listen_addresses := {'127.0.0.1', '::1'};\n  CONFIGURE: OK\n\nCLI\n---\n\nThe :ref:`ref_cli_gel_configure` command allows modifying the system configuration from a terminal or a script:\n\n.. code-block:: bash\n\n  $ gel configure set listen_addresses 127.0.0.1 ::1\n\n\nConfiguration parameters\n========================\n\n:edb-alt-title: Available Configuration Parameters\n\n.. _ref_admin_config_connection:\n\nConnection settings\n-------------------\n\n.. api-index:: listen_addresses, listen_port, cors_allow_origins\n\n:eql:synopsis:`listen_addresses: multi str`\n  Specifies the TCP/IP address(es) on which the server is to listen for connections from client applications. If the list is empty, the server does not listen on any IP interface at all.\n\n:eql:synopsis:`listen_port: int16`\n  The TCP port the server listens on; ``5656`` by default. Note that the same port number is used for all IP addresses the server listens on.\n\n:eql:synopsis:`cors_allow_origins: multi str`\n  Origins that will be calling the server that need Cross-Origin Resource Sharing (CORS) support. Can use ``*`` to allow any origin. When HTTP clients make a preflight request to the server, the origins allowed here will be added to the ``Access-Control-Allow-Origin`` header in the response.\n\nResource usage\n--------------\n\n.. api-index:: effective_io_concurrency, query_work_mem, shared_buffers\n\n:eql:synopsis:`effective_io_concurrency: int64`\n  Sets the number of concurrent disk I/O operations that can be executed simultaneously. Corresponds to the PostgreSQL configuration parameter of the same name.\n\n:eql:synopsis:`query_work_mem: cfg::memory`\n  The amount of memory used by internal query operations such as sorting. Corresponds to the PostgreSQL ``work_mem`` configuration parameter.\n\n:eql:synopsis:`shared_buffers: cfg::memory`\n  The amount of memory the database uses for shared memory buffers. Corresponds to the PostgreSQL configuration parameter of the same name. Changing this value requires server restart.\n\n\nQuery planning\n--------------\n\n.. api-index:: default_statistics_target, effective_cache_size\n\n:eql:synopsis:`default_statistics_target: int64`\n  Sets the default data statistics target for the planner.  Corresponds to the PostgreSQL configuration parameter of the same name.\n\n:eql:synopsis:`effective_cache_size: cfg::memory`\n  Sets the planner's assumption about the effective size of the disk cache that is available to a single query. Corresponds to the PostgreSQL configuration parameter of the same name.\n\n\nQuery cache\n-----------\n\n.. versionadded:: 5.0\n\n.. api-index:: auto_rebuild_query_cache, query_cache_mode, cfg::QueryCacheMode\n\n:eql:synopsis:`auto_rebuild_query_cache: bool`\n  Determines whether to recompile the existing query cache to SQL any time DDL is executed.\n\n:eql:synopsis:`query_cache_mode: cfg::QueryCacheMode`\n  Allows the developer to set where the query cache is stored. Possible values:\n\n  * ``cfg::QueryCacheMode.InMemory``- All query cache is lost on server restart. This mirrors pre-5.0 |EdgeDB| behavior.\n  * ``cfg::QueryCacheMode.RegInline``- The in-memory query cache is also stored in the database as-is so it can be restored on restart.\n  * ``cfg::QueryCacheMode.Default``- Allow the server to select the best caching option. Currently, it will select ``InMemory`` for arm64 Linux and ``RegInline`` for everything else.\n  * ``cfg::QueryCacheMode.PgFunc``- Wraps queries into stored functions in Postgres and reduces backend request size and preparation time.\n\nQuery behavior\n--------------\n\n.. api-index:: allow_bare_ddl, cfg::AllowBareDDL, apply_access_policies,\n           apply_access_policies_pg, force_database_error\n\n:eql:synopsis:`allow_bare_ddl: cfg::AllowBareDDL`\n  Allows for running bare DDL outside a migration. Possible values are ``cfg::AllowBareDDL.AlwaysAllow`` and ``cfg::AllowBareDDL.NeverAllow``.\n\n  When you create an instance, this is set to ``cfg::AllowBareDDL.AlwaysAllow`` until you run a migration. At that point it is set to ``cfg::AllowBareDDL.NeverAllow`` because it's generally a bad idea to mix migrations with bare DDL.\n\n.. _ref_std_cfg_apply_access_policies:\n\n:eql:synopsis:`apply_access_policies: bool`\n  Determines whether access policies should be applied when running queries.  Setting this to ``false`` effectively puts you into super-user mode, ignoring any access policies that might otherwise limit you on the instance.\n\n  .. note::\n\n    This setting can also be conveniently accessed via the \"Config\" dropdown menu at the top of the Gel UI (accessible by running the CLI command :gelcmd:`ui` from within a project). The setting will apply only to your UI session, so you won't have to remember to re-enable it when you're done.\n\n:eql:synopsis:`apply_access_policies_pg -> bool`\n  Determines whether access policies should be applied when running queries over SQL adapter. Defaults to ``false``.\n\n:eql:synopsis:`force_database_error -> str`\n  A hook to force all queries to produce an error. Defaults to 'false'.\n\n  .. note::\n\n    This parameter takes a ``str`` instead of a ``bool`` to allow more verbose messages when all queries are forced to fail. The database will attempt to deserialize this ``str`` into a JSON object that must include a ``type`` (which must be a Gel :ref:`error type <ref_protocol_errors>` name), and may also include ``message``, ``hint``, and ``details`` which can be set ad-hoc by the user.\n\n    For example, the following is valid input:\n\n    ``'{ \"type\": \"QueryError\",\n    \"message\": \"Did not work\",\n    \"hint\": \"Try doing something else\",\n    \"details\": \"Indeed, something went really wrong\" }'``\n\n    As is this:\n\n    ``'{ \"type\": \"UnknownParameterError\" }'``\n\nTransaction behavior\n--------------------\n\n.. api-index:: default_transaction_isolation, default_transaction_access_mode, default_transaction_deferrable\n\n.. versionadded:: 6.0\n\nThese settings will affect both explicit transactions as well as the implicit transactions that each query runs in.\n\n:eql:synopsis:`default_transaction_isolation -> sys::TransactionIsolation`\n  Controls the default isolation level of each new transaction, including implicit transactions. Defaults to ``sys::TransactionIsolation.Serializable``.\n\n  * ``sys::TransactionIsolation.RepeatableRead``\n  * ``sys::TransactionIsolation.Serializable`` (default)\n\n:eql:synopsis:`default_transaction_access_mode -> sys::TransactionAccessMode`\n  Controls the default read-only status of each new transaction, including implicit transactions. Defaults to ``sys::TransactionAccessMode.ReadWrite``.\n\n  * ``sys::TransactionAccessMode.ReadOnly``\n  * ``sys::TransactionAccessMode.ReadWrite`` (default)\n\n:eql:synopsis:`default_transaction_deferrable -> sys::TransactionDeferrability`\n  Controls the default deferrable status of each new transaction. It currently has no effect on read-write transactions. Defaults to ``sys::TransactionDeferrability.NotDeferrable``.\n\n  * ``sys::TransactionDeferrability.Deferrable``\n  * ``sys::TransactionDeferrability.NotDeferrable`` (default)\n\n.. _ref_std_cfg_client_connections:\n\nClient connections\n------------------\n\n.. api-index:: allow_user_specified_id, session_idle_timeout,\n           session_idle_transaction_timeout, query_execution_timeout\n\n:eql:synopsis:`allow_user_specified_id: bool`\n  Makes it possible to set the ``.id`` property when inserting new objects.\n\n  .. warning::\n\n    Enabling this feature introduces some security vulnerabilities:\n\n    1. An unprivileged user can discover ids that already exist in the database by trying to insert new values and noting when there is a constraint violation on ``.id`` even if the user doesn't have access to the relevant table.\n\n    2. It allows re-using object ids for a different object type, which the application might not expect.\n\n    Additionally, enabling can have serious performance implications as, on an ``insert``, every object type must be checked for collisions.\n\n    As a result, we don't recommend enabling this. If you need to preserve UUIDs from an external source on your objects, it's best to create a new property to store these UUIDs. If you will need to filter on this external UUID property, you may add an :ref:`index <ref_datamodel_indexes>` or exclusive constraint on it.\n\n:eql:synopsis:`session_idle_timeout -> std::duration`\n  Sets the timeout for how long client connections can stay inactive before being forcefully closed by the server.\n\n  Time spent on waiting for query results doesn't count as idling.  E.g. if the session idle timeout is set to 1 minute it would be OK to run a query that takes 2 minutes to compute; to limit the query execution time use the ``query_execution_timeout`` setting.\n\n  The default is 60 seconds. Setting it to ``<duration>'0'`` disables the mechanism. Setting the timeout to less than ``2`` seconds is not recommended.\n\n  Note that the actual time an idle connection can live can be up to two times longer than the specified timeout.\n\n  This is a system-level config setting.\n\n:eql:synopsis:`session_idle_transaction_timeout -> std::duration`\n  Sets the timeout for how long client connections can stay inactive while in a transaction.\n\n  The default is 10 seconds. Setting it to ``<duration>'0'`` disables the mechanism.\n\n  .. note::\n\n    For ``session_idle_transaction_timeout`` and ``query_execution_timeout``, values under 1ms are rounded down to zero, which will disable the timeout.  In order to set a timeout, please set a duration of 1ms or greater.\n\n    ``session_idle_timeout`` can take values below 1ms.\n\n:eql:synopsis:`query_execution_timeout -> std::duration`\n  Sets a time limit on how long a query can be run.\n\n  Setting it to ``<duration>'0'`` disables the mechanism.  The timeout isn't enabled by default.\n\n  .. note::\n\n    For ``session_idle_transaction_timeout`` and ``query_execution_timeout``, values under 1ms are rounded down to zero, which will disable the timeout.  In order to set a timeout, please set a duration of 1ms or greater.\n\n    ``session_idle_timeout`` can take values below 1ms.\n\n.. _ref_reference_environment:\n.. _ref_reference_envvar_variants:\n\nEnvironment variables\n=====================\n\nCertain behaviors of the Gel server are configured at startup. This configuration can be set with environment variables. The variables documented on this page are supported when using the |gel-server| binary or the official :ref:`Docker image <ref_guide_deployment_docker>`.\n\nSome environment variables (noted below) support ``_FILE`` and ``_ENV`` variants.\n\n- The ``_FILE`` variant expects its value to be a file name.  The file's contents will be read and used as the value.\n- The ``_ENV`` variant expects its value to be the name of another environment variable. The value of the other environment variable is then used as the final value. This is convenient in deployment scenarios where relevant values are auto populated into fixed environment variables.\n\n.. note::\n\n   For |Gel| versions before 6.0 the prefix for all environment variables is ``EDGEDB_`` instead of ``GEL_``.\n\nGEL_DEBUG_HTTP_INJECT_CORS\n--------------------------\n\nSet to ``1`` to have Gel send appropriate CORS headers with HTTP responses.\n\n.. note::\n\n    This is set to ``1`` by default for Gel Cloud instances.\n\n\n.. _ref_reference_envvar_admin_ui:\n\nGEL_SERVER_ADMIN_UI\n-------------------\n\nSet to ``enabled`` to enable the web-based admininstrative UI for the instance.\n\nMaps directly to the |gel-server| flag ``--admin-ui``.\n\n\nGEL_SERVER_ALLOW_INSECURE_BINARY_CLIENTS\n----------------------------------------\n\n.. warning:: Deprecated\n\n    Use :gelenv:`SERVER_BINARY_ENDPOINT_SECURITY` instead.\n\nSpecifies the security mode of the server's binary endpoint. When set to ``1``,\nnon-TLS connections are allowed. Not set by default.\n\n.. warning::\n\n    Disabling TLS is not recommended in production.\n\n\nGEL_SERVER_ALLOW_INSECURE_HTTP_CLIENTS\n--------------------------------------\n\n.. warning:: Deprecated\n\n    Use :gelenv:`SERVER_HTTP_ENDPOINT_SECURITY` instead.\n\nSpecifies the security mode of the server's HTTP endpoint. When set to ``1``,\nnon-TLS connections are allowed. Not set by default.\n\n.. warning::\n\n    Disabling TLS is not recommended in production.\n\n\n.. _ref_reference_docker_gel_server_backend_dsn:\n\nGEL_SERVER_BACKEND_DSN / _FILE / _ENV\n-------------------------------------\n\nSpecifies a PostgreSQL connection string in the `URI format`_.  If set, the\nPostgreSQL cluster specified by the URI is used instead of the builtin\nPostgreSQL server.  Cannot be specified alongside :gelenv:`SERVER_DATADIR`. Maps directly to the |gel-server| flag ``--backend-dsn``.\n\nThe ``_FILE`` and ``_ENV`` variants are also supported.\n\n.. _URI format:\n   https://www.postgresql.org/docs/13/libpq-connect.html#id-1.7.3.8.3.6\n\nGEL_SERVER_MAX_BACKEND_CONNECTIONS\n----------------------------------\n\nThe maximum NUM of connections this Gel instance could make to the backend\nPostgreSQL cluster. If not set, Gel will detect and calculate the NUM:\nRAM/100MiB for local Postgres, or pg_settings.max_connections for remote\nPostgres minus the NUM of ``--reserved-pg-connections``.\n\nGEL_SERVER_BINARY_ENDPOINT_SECURITY\n-----------------------------------\n\nSpecifies the security mode of the server's binary endpoint. When set to\n``optional``, non-TLS connections are allowed. Default is ``tls``.\n\n.. warning::\n\n    Disabling TLS is not recommended in production.\n\n\nGEL_SERVER_BIND_ADDRESS / _FILE / _ENV\n--------------------------------------\n\nSpecifies the network interface on which Gel will listen. Maps directly to the |gel-server| flag ``--bind-address``.\n\nThe ``_FILE`` and ``_ENV`` variants are also supported.\n\n\nGEL_SERVER_BOOTSTRAP_COMMAND\n----------------------------\n\nUseful to fine-tune initial user creation and other initial setup. Maps directly to the |gel-server| flag ``--bootstrap-command``.\n\nThe ``_FILE`` and ``_ENV`` variants are also supported.\n\n.. note::\n\n    A create branch statement (i.e., :eql:stmt:`create empty branch`, :eql:stmt:`create schema branch`, or :eql:stmt:`create data branch`) cannot be combined in a block with any other statements. Since all statements in :gelenv:`SERVER_BOOTSTRAP_COMMAND` run in a single block, it cannot be used to create a branch and, for example, create a user on that branch.\n\n    For Docker deployments, you can instead write :ref:`custom scripts to run before migrations <ref_guide_deployment_docker_custom_bootstrap_scripts>`.  These are placed in ``/gel-bootstrap.d/``. By writing your ``create branch`` statements in one ``.edgeql`` file each placed in ``/gel-bootstrap.d/`` and other statements in their own file, you can create branches and still run other EdgeQL statements to bootstrap your instance.\n\n    Note that for |EdgeDB| versions prior to 5.0, paths contain \"edgedb\" instead of \"gel\", so ``/gel-bootstrap.d/`` becomes ``/edgedb-bootstrap.d/``.\n\nGEL_SERVER_BOOTSTRAP_ONLY\n-------------------------\n\nWhen set, bootstrap the database cluster and exit. Not set by default.\n\n\n.. _ref_reference_docker_gel_server_datadir:\n\nGEL_SERVER_DATADIR\n------------------\n\nSpecifies a path where the database files are located.  Default is\n``/var/lib/gel/data``.  Cannot be specified alongside\n:gelenv:`SERVER_BACKEND_DSN`.\n\nMaps directly to the |gel-server| flag ``--data-dir``.\n\n\nGEL_SERVER_DEFAULT_AUTH_METHOD / _FILE / _ENV\n---------------------------------------------\n\nOptionally specifies the authentication method used by the server instance.  Supported values are ``SCRAM`` (the default) and ``Trust``. When set to ``Trust``, the database will allow complete unauthenticated access for all who have access to the database port.\n\nThis is often useful when setting an admin password on an instance that lacks one.\n\nUse at your own risk and only for development and testing.\n\nThe ``_FILE`` and ``_ENV`` variants are also supported.\n\n\nGEL_SERVER_HTTP_ENDPOINT_SECURITY\n---------------------------------\n\nSpecifies the security mode of the server's HTTP endpoint. When set to ``optional``, non-TLS connections are allowed. Default is ``tls``.\n\n.. warning::\n\n    Disabling TLS is not recommended in production.\n\n\nGEL_SERVER_INSTANCE_NAME\n------------------------\n\nSpecify the server instance name.\n\n\nGEL_SERVER_JWS_KEY_FILE\n-----------------------\n\nSpecifies a path to a file containing a public key in PEM format used to verify JWT signatures. The file could also contain a private key to sign JWT for local testing.\n\n\nGEL_SERVER_LOG_LEVEL\n--------------------\n\nSet the logging level. Default is ``info``. Other possible values are ``debug``, ``warn``, ``error``, and ``silent``.\n\n\nGEL_SERVER_PORT / _FILE / _ENV\n------------------------------\n\nSpecifies the network port on which Gel will listen. Default is ``5656``. Maps directly to the |gel-server| flag ``--port``.\n\nThe ``_FILE`` and ``_ENV`` variants are also supported.\n\n\nGEL_SERVER_RUNSTATE_DIR\n-----------------------\n\nSpecifies a path where Gel will place its Unix socket and other transient files. Maps directly to the |gel-server| flag ``--runstate-dir``.\n\n\nGEL_SERVER_SECURITY\n-------------------\n\nWhen set to ``insecure_dev_mode``, sets :gelenv:`SERVER_DEFAULT_AUTH_METHOD` to ``Trust``, and :gelenv:`SERVER_TLS_CERT_MODE` to ``generate_self_signed`` (unless an explicit TLS certificate is specified). Finally, if this option is set, the server will accept plaintext HTTP connections.  Maps directly to the |gel-server| flag ``--security``.\n\n.. warning::\n\n    Disabling TLS is not recommended in production.\n\n\n\nGEL_SERVER_TLS_CERT_FILE\n------------------------\n\nThe TLS certificate file, exclusive with :gelenv:`SERVER_TLS_CERT_MODE=generate_self_signed`. Maps directly to the |gel-server| flag ``--tls-cert-file``.\n\nGEL_SERVER_TLS_KEY_FILE\n-----------------------\n\nThe TLS private key file, exclusive with :gelenv:`SERVER_TLS_CERT_MODE=generate_self_signed`. Maps directly to the |gel-server| flag ``--tls-key-file``.\n\n\nGEL_SERVER_TLS_CERT_MODE / _FILE / _ENV\n---------------------------------------\n\nSpecifies what to do when the TLS certificate and key are either not specified or are missing.\n\n- When set to ``require_file``, the TLS certificate and key must be specified in the :gelenv:`SERVER_TLS_CERT` and :gelenv:`SERVER_TLS_KEY` variables and both must exist.\n- When set to ``generate_self_signed`` a new self-signed certificate and private key will be generated and placed in the path specified by :gelenv:`SERVER_TLS_CERT` and :gelenv:`SERVER_TLS_KEY`, if those are set.  Otherwise, the generated certificate and key are stored as ``edbtlscert.pem`` and ``edbprivkey.pem`` in :gelenv:`SERVER_DATADIR`, or, if :gelenv:`SERVER_DATADIR` is not set, they will be placed in ``/etc/ssl/gel``.\n\nDefault is ``generate_self_signed`` when :gelenv:`SERVER_SECURITY=insecure_dev_mode`. Otherwise, the default is ``require_file``.\n\nMaps directly to the |gel-server| flag ``--tls-cert-mode``.\n\nThe ``_FILE`` and ``_ENV`` variants are also supported.\n\nDocker image specific variables\n===============================\n\nThese variables are only used by the Docker image. Setting these variables outside that context will have no effect.\n\n\nGEL_DOCKER_ABORT_CODE\n---------------------\n\nIf the process fails, the arguments are logged to stderr and the script is terminated with this exit code. Default is ``1``.\n\n\nGEL_DOCKER_APPLY_MIGRATIONS\n---------------------------\n\nThe container will attempt to apply migrations in ``dbschema/migrations`` unless this variable is set to ``never``.\n\n**Values**: ``always`` (default), ``never``\n\n\nGEL_DOCKER_BOOTSTRAP_TIMEOUT_SEC\n--------------------------------\n\nSets the number of seconds to wait for instance bootstrapping to complete before timing out. Default is ``300``.\n\n\nGEL_DOCKER_LOG_LEVEL\n--------------------\n\nChange the logging level for the docker container.\n\n**Values**: ``trace``, ``debug``, ``info`` (default), ``warn``, ``error``\n\n\nGEL_DOCKER_SHOW_GENERATED_CERT\n------------------------------\n\nShows the generated TLS certificate in console output.\n\n**Values**: ``always`` (default), ``never``\n\n\nGEL_SERVER_BINARY\n-----------------\n\nSets the Gel server binary to run. Default is |gel-server|.\n\n\nGEL_SERVER_BOOTSTRAP_COMMAND_FILE\n---------------------------------\n\nRun the script when initializing the database. The script is run by the default user within the default |branch|. May be used with or without :gelenv:`SERVER_BOOTSTRAP_ONLY`.\n\n\nGEL_SERVER_COMPILER_POOL_MODE\n-----------------------------\n\nChoose a mode for the compiler pool to scale. ``fixed`` means the pool will not scale and sticks to :gelenv:`SERVER_COMPILER_POOL_SIZE`, while ``on_demand`` means the pool will maintain at least 1 worker and automatically scale up (to :gelenv:`SERVER_COMPILER_POOL_SIZE` workers ) and down to the demand.\n\n**Values**: ``fixed``, ``on_demand``\n\nDefault is ``fixed`` in production mode and ``on_demand`` in development mode.\n\n\nGEL_SERVER_COMPILER_POOL_SIZE\n-----------------------------\n\nWhen :gelenv:`SERVER_COMPILER_POOL_MODE` is ``fixed``, this setting is the exact size of the compiler pool. When :gelenv:`SERVER_COMPILER_POOL_MODE` is ``on_demand``, this will serve as the maximum size of the compiler pool.\n\n\nGEL_SERVER_EMIT_SERVER_STATUS\n-----------------------------\n\nInstruct the server to emit changes in status to *DEST*, where *DEST* is a URI specifying a file (``file://<path>``), or a file descriptor (``fd://<fileno>``).  If the URI scheme is not specified, ``file://`` is assumed.\n\n\nGEL_SERVER_EXTRA_ARGS\n---------------------\n\nAdditional arguments to pass when starting the Gel server.\n\n\nGEL_SERVER_PASSWORD / _FILE / _ENV\n----------------------------------\n\nThe password for the default superuser account (or the user specified in :gelenv:`SERVER_USER`) will be set to this value. If no value is provided, a password will not be set, unless set via :gelenv:`SERVER_BOOTSTRAP_COMMAND`. (If a value for :gelenv:`SERVER_BOOTSTRAP_COMMAND` is provided, this variable will be ignored.)\n\nThe ``_FILE`` and ``_ENV`` variants are also supported.\n\n\nGEL_SERVER_PASSWORD_HASH / _FILE / _ENV\n---------------------------------------\n\nA variant of :gelenv:`SERVER_PASSWORD`, where the specified value is a hashed password verifier instead of plain text.\n\nIf :gelenv:`SERVER_BOOTSTRAP_COMMAND` is set, this variable will be ignored.\n\nThe ``_FILE`` and ``_ENV`` variants are also supported.\n\n\nGEL_SERVER_TENANT_ID\n--------------------\n\nSpecifies the tenant ID of this server. When using multiple Gel instances with one Postgres cluster each Gel instance must have a unique tenant ID. Must be an alphanumeric ASCII string, maximum 10 characters long. Defaults to \"E\" if not set.\n\n\nGEL_SERVER_UID\n--------------\n\nSpecifies the ID of the user which should run the server binary. Default is ``1``.\n\n\nGEL_SERVER_USER\n---------------\n\nIf set to anything other than the default username |admin|, the username specified will be created. The user defined here will be the one assigned the password set in :gelenv:`SERVER_PASSWORD` or the hash set in :gelenv:`SERVER_PASSWORD_HASH`.\n"
  },
  {
    "path": "docs/reference/running/deployment/aws_aurora_ecs.rst",
    "content": ".. _ref_guide_deployment_aws_aurora_ecs:\n\n===\nAWS\n===\n\n:edb-alt-title:  Deploying Gel to AWS\n\n.. note::\n\n   We recomend using our `helm chart <helm-chart_>`_ to deploy gel on AWS EKS.  The\n   CloudFormation guide below does not configure TLS certificates correctly.\n\n.. _helm-chart:\n   https://github.com/geldata/helm-charts/blob/main\n   /charts/gel-server/README.md\n\nIn this guide we show how to deploy Gel on AWS using Amazon Aurora and\nElastic Container Service.\n\n.. include:: ./note_cloud_reset_password.rst\n\nPrerequisites\n=============\n\n* AWS account with billing enabled (or a `free trial <aws-trial_>`_)\n* (optional) ``aws`` CLI (`install <awscli-install_>`_)\n\n.. _aws-trial: https://aws.amazon.com/free\n.. _awscli-install:\n   https://docs.aws.amazon.com\n   /cli/latest/userguide/getting-started-install.html\n\nQuick Install with CloudFormation\n=================================\n\nWe maintain a `CloudFormation template <cf-template_>`_ for easy automated\ndeployment of Gel in your AWS account.  The template deploys Gel\nto a new ECS service and connects it to a newly provisioned Aurora PostgreSQL\ncluster. The created instance has a public IP address with TLS configured and\nis protected by a password you provide.\n\nCloudFormation Web Portal\n-------------------------\n\nClick `here <cf-deploy_>`_ to start the deployment process using CloudFormation\nportal and follow the prompts. You'll be prompted to provide a value for the\nfollowing parameters:\n\n- ``DockerImage``: defaults to the latest version (``geldata/gel``), or you\n  can specify a particular tag from the ones published to `Docker Hub\n  <https://hub.docker.com/r/geldata/gel/tags>`_.\n- ``InstanceName``: ⚠️ Due to limitations with AWS, this must be 22 characters\n  or less!\n- ``SuperUserPassword``: this will be used as the password for the new Gel\n  instance. Keep track of the value you provide.\n\nOnce the deployment is complete, follow these steps to find the host name that\nhas been assigned to your Gel instance:\n\n.. lint-off\n\n1. Open the AWS Console and navigate to CloudFormation > Stacks. Click on the\n   newly created Stack.\n2. Wait for the status to read ``CREATE_COMPLETE``—it can take 15 minutes or\n   more.\n3. Once deployment is complete, click the ``Outputs`` tab. The value of\n   ``PublicHostname`` is the hostname at which your Gel instance is\n   publicly available.\n4. Copy the hostname and run the following command to open a REPL to your\n   instance.\n\n   .. code-block:: bash\n\n     $ gel --dsn gel://admin:<password>@<hostname> --tls-security insecure\n     Gel x.x\n     Type \\help for help, \\quit to quit.\n     gel>\n\n.. lint-on\n\nTo make changes to your Gel deployment like upgrading the Gel version or\nenabling the UI you can follow the CloudFormation\n`Updating a stack <stack-update_>`_ instructions. Search for\n``ContainerDefinitions`` in the template and you will find where Gel's\n:ref:`environment variables <ref_guides_deployment_docker_customization>` are\ndefined. To upgrade the Gel version specify a\n`docker image tag <docker-tags_>`_ with the image name ``geldata/gel`` in the\nsecond step of the update workflow.\n\nCloudFormation CLI\n------------------\n\nAlternatively, if you prefer to use AWS CLI, run the following command in\nyour terminal:\n\n.. code-block:: bash\n\n    $ aws cloudformation create-stack \\\n        --stack-name Gel \\\n        --template-url \\\n          https://gel-deployment.s3.us-east-2.amazonaws.com/gel-aurora.yml \\\n        --capabilities CAPABILITY_NAMED_IAM \\\n        --parameters ParameterKey=SuperUserPassword,ParameterValue=<password>\n\n\n.. _cf-template: https://github.com/geldata/gel-deploy/tree/dev/aws-cf\n.. _cf-deploy:\n   https://console.aws.amazon.com\n   /cloudformation/home#/stacks/new?stackName=Gel&templateURL=\n   https%3A%2F%2Fgel-deployment.s3.us-east-2.amazonaws.com%2Fgel-aurora.yml\n.. _aws_console:\n   https://console.aws.amazon.com\n   /ec2/v2/home#NIC:search=ec2-security-group\n.. _stack-update:\n   https://docs.aws.amazon.com\n   /AWSCloudFormation/latest/UserGuide/cfn-whatis-howdoesitwork.html\n.. _docker-tags: https://hub.docker.com/r/geldata/gel/tags\n\n\nConnecting your application\n===========================\n\nTo connect your application to the Gel instance, you'll need to provide\nconnection parameters. Gel client libraries can be configured using either\na DSN (connection string) or individual environment variables.\n\nObtaining connection parameters\n-------------------------------\n\nYour connection requires the following components:\n\n- **Host**: The ``PublicHostname`` value from the CloudFormation Stack's\n  ``Outputs`` tab.\n- **Port**: ``5656`` (the default Gel port)\n- **Username**: |admin| (the default superuser)\n- **Password**: The ``SuperUserPassword`` you specified during deployment\n- **Branch**: |main| (the default branch)\n\nConstruct the DSN using these values:\n\n.. code-block:: bash\n\n    $ GEL_DSN=\"gel://admin:<password>@<hostname>:5656\"\n\nObtaining the TLS certificate\n-----------------------------\n\n.. warning::\n\n    The CloudFormation template does not configure TLS certificates correctly.\n    We recommend using ``--tls-security insecure`` for testing, but for\n    production you should use our `helm chart <helm-chart_>`_ or configure\n    TLS manually.\n\nTo connect securely, your application needs the server's TLS certificate.\nFor self-signed certificates, you can retrieve the certificate by connecting\nto the instance and extracting it:\n\n.. code-block:: bash\n\n    $ gel --dsn $GEL_DSN --tls-security insecure \\\n        query \"SELECT sys::get_tls_certificate()\"\n\nStore this certificate and provide it to your application via the\n:gelenv:`TLS_CA` or :gelenv:`TLS_CA_FILE` environment variable.\n\nUsing in your application\n-------------------------\n\nSet these environment variables where you deploy your application:\n\n.. code-block:: bash\n\n    GEL_DSN=\"gel://admin:<password>@<hostname>:5656\"\n    # For self-signed certificates:\n    GEL_CLIENT_TLS_SECURITY=insecure\n    # Or with a proper TLS certificate:\n    GEL_TLS_CA=\"<certificate content>\"\n\nGel's client libraries will automatically read these environment variables.\n\nLocal development with the CLI\n------------------------------\n\nTo make your remote instance easier to work with during local development,\ncreate an alias using :gelcmd:`instance link`.\n\n.. note::\n\n   The command groups :gelcmd:`instance` and :gelcmd:`project` are not\n   intended to manage production instances.\n\n.. code-block:: bash\n\n    $ gel instance link \\\n        --dsn $GEL_DSN \\\n        --non-interactive \\\n        --trust-tls-cert \\\n        my_aws_instance\n\nYou can now refer to the remote instance using the alias ``my_aws_instance``.\nUse this alias wherever an instance name is expected:\n\n.. code-block:: bash\n\n    $ gel -I my_aws_instance\n    Gel x.x\n    Type \\help for help, \\quit to quit.\n    gel>\n\nOr apply migrations:\n\n.. code-block:: bash\n\n    $ gel -I my_aws_instance migrate\n\n\nHealth Checks\n=============\n\nUsing an HTTP client, you can perform health checks to monitor the status of\nyour Gel instance. Learn how to use them with our :ref:`health checks guide\n<ref_guide_deployment_health_checks>`.\n"
  },
  {
    "path": "docs/reference/running/deployment/azure_flexibleserver.rst",
    "content": ".. _ref_guide_deployment_azure_flexibleserver:\n\n=====\nAzure\n=====\n\n:edb-alt-title: Deploying Gel to Azure\n\nIn this guide we show how to deploy Gel using Azure's `Postgres\nFlexible Server\n<https://docs.microsoft.com/en-us/azure/postgresql/flexible-server>`_ as the\nbackend.\n\n.. include:: ./note_cloud_reset_password.rst\n\nPrerequisites\n=============\n\n* Valid Azure Subscription with billing enabled or credits (`free trial\n  <azure-trial_>`_).\n* Azure CLI (`install <azure-install_>`_).\n\n.. _azure-trial: https://azure.microsoft.com/en-us/free/\n.. _azure-install: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli\n\n\nProvision a Gel instance\n=========================\n\nLogin to your Microsoft Azure account.\n\n.. code-block:: bash\n\n   $ az login\n\nCreate a new resource group.\n\n.. code-block:: bash\n\n   $ GROUP=my-group-name\n   $ az group create --name $GROUP --location westus\n\nProvision a PostgreSQL server.\n\n.. note::\n\n   If you already have a database provisioned you can skip this step.\n\nFor convenience, assign a value to the ``PG_SERVER_NAME`` environment\nvariable; we'll use this variable in multiple later commands.\n\n.. code-block:: bash\n\n   $ PG_SERVER_NAME=postgres-for-gel\n\nUse the ``read`` command to securely assign a value to the ``PASSWORD``\nenvironment variable.\n\n.. code-block:: bash\n\n   $ echo -n \"> \" && read -s PASSWORD\n\nThen create a Postgres Flexible server.\n\n.. code-block:: bash\n\n   $ az postgres flexible-server create \\\n       --resource-group $GROUP \\\n       --name $PG_SERVER_NAME \\\n       --location westus \\\n       --admin-user gel_admin \\\n       --admin-password $PASSWORD \\\n       --sku-name Standard_D2s_v3 \\\n       --version 14 \\\n       --yes\n\n.. note::\n\n   If you get an error saying ``\"Specified server name is already used.\"``\n   change the value of ``PG_SERVER_NAME`` and rerun the command.\n\nAllow other Azure services access to the Postgres instance.\n\n.. code-block:: bash\n\n   $ az postgres flexible-server firewall-rule create \\\n       --resource-group $GROUP \\\n       --name $PG_SERVER_NAME \\\n       --rule-name allow-azure-internal \\\n       --start-ip-address 0.0.0.0 \\\n       --end-ip-address 0.0.0.0\n\n|Gel| requires Postgres' ``uuid-ossp`` extension which needs to be enabled.\n\n.. code-block:: bash\n\n   $ az postgres flexible-server parameter set \\\n       --resource-group $GROUP \\\n       --server-name $PG_SERVER_NAME \\\n       --name azure.extensions \\\n       --value uuid-ossp\n\nAzure is not able to reliably pull docker images `because of rate limits\n<azure-cli-issue_>`_, so you will need to provide docker hub login credentials\nto create a container. If you don't already have a docker hub account you can\ncreate one `here <https://app.docker.com/signup>`_.\n\n.. _azure-cli-issue: https://github.com/Azure/azure-cli/issues/29300\n\n.. code-block:: bash\n\n   $ echo -n \"docker user> \" && read -s DOCKER_USER\n   $ echo -n \"docker password> \" && read -s DOCKER_PASSWORD\n\nStart a Gel container.\n\n.. code-block:: bash\n\n   $ PG_HOST=$(\n       az postgres flexible-server list \\\n         --resource-group $GROUP \\\n         --query \"[?name=='$PG_SERVER_NAME'].fullyQualifiedDomainName | [0]\" \\\n         --output tsv\n     )\n   $ DSN=\"postgresql://gel_admin:$PASSWORD@$PG_HOST/postgres?sslmode=require\"\n   $ az container create \\\n       --registry-username $DOCKER_USER \\\n       --registry-password $DOCKER_PASSWORD \\\n       --registry-login-server index.docker.io \\\n       --os-type Linux \\\n       --cpu 1 \\\n       --memory 1 \\\n       --resource-group $GROUP \\\n       --name gel-container-group \\\n       --image geldata/gel \\\n       --dns-name-label geldb \\\n       --ports 5656 \\\n       --secure-environment-variables \\\n         \"GEL_SERVER_PASSWORD=$PASSWORD\" \\\n         \"GEL_SERVER_BACKEND_DSN=$DSN\" \\\n       --environment-variables \\\n         GEL_SERVER_TLS_CERT_MODE=generate_self_signed\n\nPersist the SSL certificate. We have configured Gel to generate a self\nsigned SSL certificate when it starts. However, if the container is restarted a\nnew certificate would be generated. To preserve the certificate across failures\nor reboots copy the certificate files and use their contents in the\n:gelenv:`SERVER_TLS_KEY` and :gelenv:`SERVER_TLS_CERT` environment variables.\n\n.. code-block:: bash\n\n   $ key=\"$( az container exec \\\n               --resource-group $GROUP \\\n               --name gel-container-group \\\n               --exec-command \"cat /tmp/gel/edbprivkey.pem\" \\\n             | tr -d \"\\r\" )\"\n   $ cert=\"$( az container exec \\\n                --resource-group $GROUP \\\n                --name gel-container-group \\\n                --exec-command \"cat /tmp/gel/edbtlscert.pem\" \\\n             | tr -d \"\\r\" )\"\n   $ az container delete \\\n       --resource-group $GROUP \\\n       --name gel-container-group \\\n       --yes\n   $ az container create \\\n       --registry-username $DOCKER_USER \\\n       --registry-password $DOCKER_PASSWORD \\\n       --registry-login-server index.docker.io \\\n       --os-type Linux \\\n       --cpu 1 \\\n       --memory 1 \\\n       --resource-group $GROUP \\\n       --name gel-container-group \\\n       --image geldata/gel \\\n       --dns-name-label geldb \\\n       --ports 5656 \\\n       --secure-environment-variables \\\n         \"GEL_SERVER_PASSWORD=$PASSWORD\" \\\n         \"GEL_SERVER_BACKEND_DSN=$DSN\" \\\n         \"GEL_SERVER_TLS_KEY=$key\" \\\n       --environment-variables \\\n         \"GEL_SERVER_TLS_CERT=$cert\"\n\n\nConnecting your application\n===========================\n\nTo connect your application to the Gel instance, you'll need to provide\nconnection parameters. Gel client libraries can be configured using either\na DSN (connection string) or individual environment variables.\n\nObtaining connection parameters\n-------------------------------\n\nYour connection requires the following components:\n\n- **Host**: The FQDN of your Azure container instance. Retrieve it with:\n\n  .. code-block:: bash\n\n      $ az container list \\\n          --resource-group $GROUP \\\n          --query \"[?name=='gel-container-group'].ipAddress.fqdn | [0]\" \\\n          --output tsv\n\n- **Port**: ``5656`` (the default Gel port)\n- **Username**: |admin| (the default superuser)\n- **Password**: The password you set in the ``$PASSWORD`` variable\n- **Branch**: |main| (the default branch)\n\nConstruct the DSN using these values:\n\n.. code-block:: bash\n\n    $ GEL_HOST=$(az container list \\\n        --resource-group $GROUP \\\n        --query \"[?name=='gel-container-group'].ipAddress.fqdn | [0]\" \\\n        --output tsv)\n    $ GEL_DSN=\"gel://admin:$PASSWORD@$GEL_HOST:5656\"\n\nObtaining the TLS certificate\n-----------------------------\n\nSince we configured Gel with a self-signed TLS certificate, your application\nneeds the certificate to connect securely. Retrieve it from the container:\n\n.. code-block:: bash\n\n    $ az container exec \\\n        --resource-group $GROUP \\\n        --name gel-container-group \\\n        --exec-command \"cat /tmp/gel/edbtlscert.pem\" \\\n      | tr -d \"\\r\" > gel-tls-cert.pem\n\nAlternatively, you can retrieve it using the Gel CLI:\n\n.. code-block:: bash\n\n    $ gel --dsn $GEL_DSN --tls-security insecure \\\n        query \"SELECT sys::get_tls_certificate()\" > gel-tls-cert.pem\n\nUsing in your application\n-------------------------\n\nSet these environment variables where you deploy your application:\n\n.. code-block:: bash\n\n    GEL_DSN=\"gel://admin:<password>@<hostname>:5656\"\n    # For self-signed certificates, either trust the cert:\n    GEL_TLS_CA_FILE=\"/path/to/gel-tls-cert.pem\"\n    # Or (for development only) disable TLS verification:\n    GEL_CLIENT_TLS_SECURITY=insecure\n\nGel's client libraries will automatically read these environment variables.\n\nLocal development with the CLI\n------------------------------\n\nTo make your remote instance easier to work with during local development,\ncreate an alias using :gelcmd:`instance link`.\n\n.. note::\n\n   The command groups :gelcmd:`instance` and :gelcmd:`project` are not\n   intended to manage production instances.\n\n.. code-block:: bash\n\n    $ printf $PASSWORD | gel instance link \\\n        --dsn $GEL_DSN \\\n        --password-from-stdin \\\n        --non-interactive \\\n        --trust-tls-cert \\\n        my_azure_instance\n\nYou can now refer to the remote instance using the alias ``my_azure_instance``.\nUse this alias wherever an instance name is expected:\n\n.. code-block:: bash\n\n    $ gel -I my_azure_instance\n    Gel x.x\n    Type \\help for help, \\quit to quit.\n    gel>\n\nOr apply migrations:\n\n.. code-block:: bash\n\n    $ gel -I my_azure_instance migrate\n\n\nHealth Checks\n=============\n\nUsing an HTTP client, you can perform health checks to monitor the status of\nyour Gel instance. Learn how to use them with our :ref:`health checks guide\n<ref_guide_deployment_health_checks>`.\n"
  },
  {
    "path": "docs/reference/running/deployment/bare_metal.rst",
    "content": ".. _ref_guide_deployment_bare_metal:\n\n==========\nBare Metal\n==========\n\n:edb-alt-title: Deploying Gel to a Bare Metal Server\n\nIn this guide we show how to deploy Gel to bare metal using your system's\npackage manager and systemd.\n\n.. include:: ./note_cloud_reset_password.rst\n\nInstall the Gel Package\n=======================\n\nThe steps for installing the Gel package will be slightly different\ndepending on your Linux distribution. Once you have the package installed you\ncan jump to :ref:`ref_guide_deployment_bare_metal_enable_unit`.\n\n\nDebian/Ubuntu LTS\n-----------------\nImport the Gel packaging key.\n\n.. code-block:: bash\n\n   $ sudo mkdir -p /usr/local/share/keyrings && \\\n       sudo curl --proto '=https' --tlsv1.2 -sSf \\\n       -o /usr/local/share/keyrings/gel-keyring.gpg \\\n       https://packages.geldata.com/keys/gel-keyring.gpg\n\nAdd the Gel package repository.\n\n.. code-block:: bash\n\n   $ echo deb '[signed-by=/usr/local/share/keyrings/gel-keyring.gpg]' \\\n       https://packages.geldata.com/apt \\\n       $(grep \"VERSION_CODENAME=\" /etc/os-release | cut -d= -f2) main \\\n       | sudo tee /etc/apt/sources.list.d/gel.list\n\n.. note::\n\n   For non-LTS releases of Debian/Ubuntu (e.g. Ubuntu Oracular), one can install\n   package for latest LTS release, because they are usually forward compatible.\n   To do this, replace the ``$(grep ...)`` with the name of latest LTS release\n   (e.g. ``noble``).\n\nInstall the Gel package.\n\n.. code-block:: bash\n\n   $ sudo apt-get update && sudo apt-get install gel-6\n\n\nCentOS/RHEL 7/8\n---------------\nAdd the Gel package repository.\n\n.. code-block:: bash\n\n   $ sudo curl --proto '=https' --tlsv1.2 -sSfL \\\n      https://packages.geldata.com/rpm/gel-rhel.repo \\\n      > /etc/yum.repos.d/gel.repo\n\nInstall the Gel package.\n\n.. code-block:: bash\n\n   $ sudo yum install gel-6\n\nDisable SELinux.\n\n.. code-block:: bash\n\n   $ sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config\n   $ reboot\n\n\n.. _ref_guide_deployment_bare_metal_enable_unit:\n\nEnable a systemd unit\n=====================\n\nThe Gel package comes bundled with a systemd unit that is disabled by\ndefault. You can start the server by enabling the unit.\n\n.. code-block:: bash\n\n   $ sudo systemctl enable --now gel-server-6\n\nThis will start the server on port 5656, and the data directory will be\n``/var/lib/gel/6/data``.\n\n.. warning::\n\n    |gel-server| cannot be run as root.\n\nSet environment variables\n=========================\n\nTo set environment variables when running Gel with ``systemctl``,\n\n.. code-block:: bash\n\n   $ systemctl edit --full gel-server-6\n\nThis opens a ``systemd`` unit file. Set the desired environment variables\nunder the ``[Service]`` section. View the supported environment variables at\n:ref:`Reference > Environment Variables <ref_reference_environment>`.\n\n.. code-block:: toml\n\n   [Service]\n   Environment=\"GEL_SERVER_TLS_CERT_MODE=generate_self_signed\"\n   Environment=\"GEL_SERVER_ADMIN_UI=enabled\"\n\nSave the file and exit, then restart the service.\n\n.. code-block:: bash\n\n   $ systemctl restart gel-server-6\n\n\nSet a password\n==============\nThere is no default password. To set one, you will first need to get the Unix\nsocket directory. You can find this by looking at your system.d unit file.\n\n.. code-block:: bash\n\n    $ sudo systemctl cat gel-server-6\n\nSet a password by connecting from localhost.\n\n.. code-block:: bash\n\n   $ echo -n \"> \" && read -s PASSWORD\n   $ RUNSTATE_DIR=$(systemctl show gel-server-6 -P ExecStart | \\\n      grep -o -m 1 -- \"--runstate-dir=[^ ]\\+\" | \\\n      awk -F \"=\" '{print $2}')\n   $ sudo gel --port 5656 --tls-security insecure --admin \\\n      --unix-path $RUNSTATE_DIR \\\n      query \"ALTER ROLE admin SET password := '$PASSWORD'\"\n\nThe server listens on localhost by default. Changing this looks like this.\n\n.. code-block:: bash\n\n   $ gel --port 5656 --tls-security insecure --password query \\\n      \"CONFIGURE INSTANCE SET listen_addresses := {'0.0.0.0'};\"\n\nThe listen port can be changed from the default ``5656`` if your deployment\nscenario requires a different value.\n\n.. code-block:: bash\n\n   $ gel --port 5656 --tls-security insecure --password query \\\n      \"CONFIGURE INSTANCE SET listen_port := 1234;\"\n\nYou may need to restart the server after changing the listen port or addresses.\n\n.. code-block:: bash\n\n   $ sudo systemctl restart gel-server-6\n\n\nConnecting your application\n===========================\n\nTo connect your application to the Gel instance, you'll need to provide\nconnection parameters. Gel client libraries can be configured using either\na DSN (connection string) or individual environment variables.\n\nObtaining connection parameters\n-------------------------------\n\nYour connection requires the following components:\n\n- **Host**: The IP address or hostname of your server (e.g., ``localhost``,\n  ``192.168.1.100``, or ``gel.example.com``)\n- **Port**: ``5656`` by default, or the custom port if you changed it with\n  ``CONFIGURE INSTANCE SET listen_port``\n- **Username**: |admin| (the default superuser)\n- **Password**: The password you set with ``ALTER ROLE admin SET password``\n- **Branch**: |main| (the default branch)\n\nConstruct the DSN using these values:\n\n.. code-block:: bash\n\n    $ GEL_DSN=\"gel://admin:<password>@<hostname>:5656\"\n\nObtaining the TLS certificate\n-----------------------------\n\nIf you configured Gel with ``GEL_SERVER_TLS_CERT_MODE=generate_self_signed``,\nyour application needs the certificate to connect securely.\n\nThe generated certificate is stored in the data directory. You can find it at:\n\n.. code-block:: bash\n\n    $ cat /var/lib/gel/6/data/edbtlscert.pem\n\nAlternatively, retrieve it using the Gel CLI:\n\n.. code-block:: bash\n\n    $ gel --dsn $GEL_DSN --tls-security insecure \\\n        query \"SELECT sys::get_tls_certificate()\"\n\nUsing in your application\n-------------------------\n\nSet these environment variables where you deploy your application:\n\n.. code-block:: bash\n\n    GEL_DSN=\"gel://admin:<password>@<hostname>:5656\"\n    # For self-signed certificates, provide the CA cert:\n    GEL_TLS_CA_FILE=\"/path/to/edbtlscert.pem\"\n    # Or embed the certificate content directly:\n    GEL_TLS_CA=\"<certificate content>\"\n\nGel's client libraries will automatically read these environment variables.\n\nLocal development with the CLI\n------------------------------\n\nTo make your instance easier to work with during local development,\ncreate an alias using :gelcmd:`instance link`.\n\n.. note::\n\n   The command groups :gelcmd:`instance` and :gelcmd:`project` are not\n   intended to manage production instances.\n\n.. code-block:: bash\n\n    $ gel instance link \\\n        --dsn $GEL_DSN \\\n        --non-interactive \\\n        --trust-tls-cert \\\n        my_bare_metal_instance\n\nYou can now refer to the instance using the alias ``my_bare_metal_instance``.\nUse this alias wherever an instance name is expected:\n\n.. code-block:: bash\n\n    $ gel -I my_bare_metal_instance\n    Gel x.x\n    Type \\help for help, \\quit to quit.\n    gel>\n\nOr apply migrations:\n\n.. code-block:: bash\n\n    $ gel -I my_bare_metal_instance migrate\n\n\nUpgrading Gel\n=============\n\nWhen you want to upgrade to the newest point release upgrade the package and\nrestart the ``gel-server-6`` unit.\n\n\nDebian/Ubuntu LTS\n-----------------\n\n.. code-block:: bash\n\n   $ sudo apt-get update && sudo apt-get install --only-upgrade gel-6\n   $ sudo systemctl restart gel-server-6\n\n\nCentOS/RHEL 7/8\n---------------\n\n.. code-block:: bash\n\n   $ sudo yum update gel-6\n   $ sudo systemctl restart gel-server-6\n\nHealth Checks\n=============\n\nUsing an HTTP client, you can perform health checks to monitor the status of\nyour Gel instance. Learn how to use them with our :ref:`health checks guide\n<ref_guide_deployment_health_checks>`.\n"
  },
  {
    "path": "docs/reference/running/deployment/digitalocean.rst",
    "content": ".. _ref_guide_deployment_digitalocean:\n\n============\nDigitalOcean\n============\n\n:edb-alt-title: Deploying Gel to DigitalOcean\n\nCreate a droplet and use the :ref:`ref_guide_deployment_bare_metal` guide to\ninstall gel server.\n"
  },
  {
    "path": "docs/reference/running/deployment/docker.rst",
    "content": ".. _ref_guide_deployment_docker:\n\n======\nDocker\n======\n\n:edb-alt-title: Deploying Gel with Docker\n\n.. include:: ./note_cloud_reset_password.rst\n\nWhen to use the \"geldata/gel\" Docker image\n==========================================\n\n.. _geldata/gel: https://hub.docker.com/r/geldata/gel\n\nThis image is primarily intended to be used directly when there is a\nrequirement to use Docker containers, such as in production, or in a\ndevelopment setup that involves multiple containers orchestrated by Docker\nCompose or a similar tool. Otherwise, using the :ref:`ref_cli_gel_server`\nCLI on the host system is the recommended way to install and run Gel\nservers.\n\n\nHow to use this image\n=====================\n\nThe simplest way to run the image (without data persistence) is this:\n\n.. code-block:: bash\n\n   $ docker run --name gel -d \\\n       -e GEL_SERVER_SECURITY=insecure_dev_mode \\\n       geldata/gel\n\nSee the :ref:`ref_guides_deployment_docker_customization` section below for the\nmeaning of the :gelenv:`SERVER_SECURITY` variable and other options.\n\nThen, to authenticate to the Gel instance and store the credentials in a\nDocker volume, run:\n\n.. code-block:: bash\n\n   $ docker run -it --rm --link=gel \\\n       -e GEL_SERVER_PASSWORD=secret \\\n       -v gel-cli-config:/.config/edgedb geldata/gel-cli \\\n       -H gel instance link my_instance \\\n           --tls-security insecure \\\n           --non-interactive\n\nNow, to open an interactive shell to the database instance run this:\n\n.. code-block:: bash\n\n   $ docker run -it --rm --link=gel \\\n       -v gel-cli-config:/.config/edgedb geldata/gel-cli \\\n       -I my_instance\n\n\nData Persistence\n================\n\nIf you want the contents of the database to survive container restarts, you\nmust mount a persistent volume at the path specified by\n:gelenv:`SERVER_DATADIR` (``/var/lib/gel/data`` by default).  For example:\n\n.. code-block:: bash\n\n   $ docker run \\\n       --name gel \\\n       -e GEL_SERVER_PASSWORD=secret \\\n       -e GEL_SERVER_TLS_CERT_MODE=generate_self_signed \\\n       -v /my/data/directory:/var/lib/gel/data \\\n       -d geldata/gel\n\nNote that on Windows you must use a Docker volume instead:\n\n.. code-block:: bash\n\n   $ docker volume create --name=gel-data\n   $ docker run \\\n       --name gel \\\n       -e GEL_SERVER_PASSWORD=secret \\\n       -e GEL_SERVER_TLS_CERT_MODE=generate_self_signed \\\n       -v gel-data:/var/lib/gel/data \\\n       -d geldata/gel\n\nIt is also possible to run a ``gel`` container on a remote PostgreSQL\ncluster specified by :gelenv:`SERVER_BACKEND_DSN`. See below for details.\n\n\nSchema Migrations\n=================\n\nA derived image may include application schema and migrations in ``/dbschema``,\nin which case the container will attempt to apply the schema migrations found\nin ``/dbschema/migrations``, unless the :gelenv:`DOCKER_APPLY_MIGRATIONS`\nenvironment variable is set to ``never``.\n\n\nDocker Compose\n==============\n\nA simple ``docker-compose`` configuration might look like this.\nWith a ``docker-compose.yaml`` containing:\n\n.. code-block:: yaml\n\n   services:\n     gel:\n       image: geldata/gel\n       environment:\n         GEL_SERVER_SECURITY: insecure_dev_mode\n       volumes:\n         - \"./dbschema:/dbschema\"\n       ports:\n         - \"5656:5656\"\n\nOnce there is a :ref:`schema <ref_datamodel_index>` in ``dbschema/`` a\nmigration can be created with:\n\n.. code-block:: bash\n\n   $ gel --tls-security=insecure -P 5656 migration create\n\nAlternatively, if you don't have the Gel CLI installed on your host\nmachine, you can use the CLI bundled with the server container:\n\n.. code-block:: bash\n\n   $ docker compose exec gel \\\n       gel --tls-security=insecure -P 5656 migration create\n\n\n.. _ref_guides_deployment_docker_customization:\n\nConfiguration\n=============\n\nThe Docker image supports the same set of enviroment variables as the Gel\nserver process, which are documented under :ref:`Reference > Environment\nVariables <ref_reference_environment>`.\n\n|Gel| containers can be additionally configured using initialization scripts\nand some Docker-specific environment variables, documented below.\n\n.. note::\n\n   Some variables support ``_ENV`` and ``_FILE`` :ref:`variants\n   <ref_reference_envvar_variants>` to support more advanced configurations.\n\n.. _ref_guides_deployment_docker_initial_setup:\n\nInitial configuration\n---------------------\n\nWhen a Gel container starts on the specified data directory or remote\nPostgres cluster for the first time, initial instance setup is performed. This\nis called the *bootstrap phase*.\n\nThe following environment variables affect the bootstrap only and have no\neffect on subsequent container runs.\n\n.. note::\n\n   For |EdgeDB| versions before 6.0 (Gel) the prefix for all environment\n   variables is ``EDGEDB_`` instead of ``GEL_``.\n\n\nGEL_SERVER_BOOTSTRAP_COMMAND\n............................\n\nUseful to fine-tune initial user and branch creation, and other initial\nsetup. If neither the :gelenv:`SERVER_BOOTSTRAP_COMMAND` variable or the\n:gelenv:`SERVER_BOOTSTRAP_SCRIPT_FILE` are explicitly specified, the container\nwill look for the presence of ``/gel-bootstrap.edgeql`` in the container\n(which can be placed in a derived image).\n\nMaps directly to the |gel-server| flag ``--bootstrap-command``. The\n``*_FILE`` and ``*_ENV`` variants are also supported.\n\n\nGEL_SERVER_BOOTSTRAP_SCRIPT_FILE\n................................\nDeprecated in image version 2.8: use :gelenv:`SERVER_BOOTSTRAP_COMMAND_FILE`\ninstead.\n\nRun the script when initializing the database. The script is run by default\nuser within default branch.\n\n\nGEL_SERVER_PASSWORD\n...................\n\nThe password for the default superuser account will be set to this value. If\nno value is provided a password will not be set, unless set via\n:gelenv:`SERVER_BOOTSTRAP_COMMAND`. (If a value for\n:gelenv:`SERVER_BOOTSTRAP_COMMAND` is provided, this variable will be\nignored.)\n\nThe ``*_FILE`` and ``*_ENV`` variants are also supported.\n\n\nGEL_SERVER_PASSWORD_HASH\n........................\n\nA variant of :gelenv:`SERVER_PASSWORD`, where the specified value is a hashed\npassword verifier instead of plain text.\n\nIf :gelenv:`SERVER_BOOTSTRAP_COMMAND` is set, this variable will be ignored.\n\nThe ``*_FILE`` and ``*_ENV`` variants are also supported.\n\n\nGEL_SERVER_GENERATE_SELF_SIGNED_CERT\n....................................\n\n.. warning::\n\n   Deprecated: use :gelenv:`SERVER_TLS_CERT_MODE=generate_self_signed`\n   instead.\n\nSet this option to ``1`` to tell the server to automatically generate a\nself-signed certificate with key file in the :gelenv:`SERVER_DATADIR` (if\npresent, see below), and echo the certificate content in the logs. If the\ncertificate file exists, the server will use it instead of generating a new\none.\n\nSelf-signed certificates are usually used in development and testing, you\nshould likely provide your own certificate and key file with the variables\nbelow.\n\n\nGEL_SERVER_TLS_CERT/GEL_SERVER_TLS_KEY\n......................................\n\nThe TLS certificate and private key data, exclusive with\n:gelenv:`SERVER_TLS_CERT_MODE=generate_self_signed`.\n\nThe ``*_FILE`` and ``*_ENV`` variants are also supported.\n\n\nCustom scripts in \"/docker-entrypoint.d/\"\n.........................................\n\nTo perform additional initialization, a derived image may include one or more\nexecutable files in ``/docker-entrypoint.d/``, which will get executed by the\ncontainer entrypoint *before* any other processing takes place.\n\n\nRuntime configuration\n---------------------\n\nGEL_DOCKER_LOG_LEVEL\n....................\n\nDetermines the log verbosity level in the entrypoint script. Valid levels are\n``trace``, ``debug``, ``info``, ``warning``, and ``error``.  The default is\n``info``.\n\n.. _ref_guide_deployment_docker_custom_bootstrap_scripts:\n\nCustom scripts in \"/gel-bootstrap.d/\" and \"/gel-bootstrap-late.d\"\n.................................................................\n\nTo perform additional initialization, a derived image may include one or more\n``*.edgeql`` or ``*.sh`` scripts, which are executed in addition to and\n*after* the initialization specified by the environment variables above or the\n``/gel-bootstrap.edgeql`` script.  Parts in ``/gel-bootstrap.d`` are\nexecuted *before* any schema migrations are applied, and parts in\n``/gel-bootstrap-late.d`` are executed *after* the schema migration have\nbeen applied.\n\n.. note::\n\n    Best practice for naming your script files when you will have multiple\n    script files to run on bootstrap is to prepend the filenames with ``01-``,\n    ``02-``, and so on to indicate your desired order of execution.\n\n\nConnecting your application\n===========================\n\nTo connect your application to the Gel instance, you'll need to provide\nconnection parameters. Gel client libraries can be configured using either\na DSN (connection string) or individual environment variables.\n\nObtaining connection parameters\n-------------------------------\n\nYour connection requires the following components:\n\n- **Host**: The container hostname or IP address. In Docker Compose, this is\n  the service name (e.g., ``gel``). For standalone containers, use\n  ``localhost`` if on the same host, or the container's IP/hostname.\n- **Port**: ``5656`` (the default Gel port, unless remapped with ``-p``)\n- **Username**: |admin| (the default superuser)\n- **Password**: The value of :gelenv:`SERVER_PASSWORD` you set when starting\n  the container\n- **Branch**: |main| (the default branch)\n\nConstruct the DSN using these values:\n\n.. code-block:: bash\n\n    $ GEL_DSN=\"gel://admin:<password>@<hostname>:5656\"\n\nFor a Docker Compose setup with the service named ``gel``:\n\n.. code-block:: bash\n\n    $ GEL_DSN=\"gel://admin:secret@gel:5656\"\n\nObtaining the TLS certificate\n-----------------------------\n\nIf you configured Gel with ``GEL_SERVER_TLS_CERT_MODE=generate_self_signed``,\nyour application needs the certificate to connect securely.\n\nRetrieve the certificate from the running container:\n\n.. code-block:: bash\n\n    $ docker exec <container-name> cat /var/lib/gel/data/edbtlscert.pem\n\nOr using the Gel utility script:\n\n.. code-block:: bash\n\n    $ docker exec <container-name> \\\n        gel-show-secrets.sh --format=raw GEL_SERVER_TLS_CERT\n\nAlternatively, retrieve it using the Gel CLI:\n\n.. code-block:: bash\n\n    $ gel --dsn $GEL_DSN --tls-security insecure \\\n        query \"SELECT sys::get_tls_certificate()\"\n\nIf you mounted a persistent volume at :gelenv:`SERVER_DATADIR`, the\ncertificate is also available at ``<volume-path>/edbtlscert.pem``.\n\nUsing in your application\n-------------------------\n\nSet these environment variables in your application container:\n\n.. code-block:: yaml\n\n    # docker-compose.yaml example\n    services:\n      app:\n        image: your-app\n        environment:\n          GEL_DSN: \"gel://admin:secret@gel:5656\"\n          # For self-signed certificates:\n          GEL_CLIENT_TLS_SECURITY: \"insecure\"\n          # Or provide the CA certificate:\n          # GEL_TLS_CA: \"<certificate content>\"\n\nFor production, we recommend providing the TLS certificate rather than\ndisabling TLS verification:\n\n.. code-block:: yaml\n\n    services:\n      app:\n        image: your-app\n        environment:\n          GEL_DSN: \"gel://admin:${GEL_PASSWORD}@gel:5656\"\n          GEL_TLS_CA_FILE: \"/certs/gel-ca.pem\"\n        volumes:\n          - ./certs:/certs:ro\n\nGel's client libraries will automatically read these environment variables.\n\nLocal development with the CLI\n------------------------------\n\nTo make your Gel container easier to work with during local development,\ncreate an alias using :gelcmd:`instance link`.\n\n.. note::\n\n   The command groups :gelcmd:`instance` and :gelcmd:`project` are not\n   intended to manage production instances.\n\nFrom your host machine, link to the container:\n\n.. code-block:: bash\n\n    $ gel instance link \\\n        --dsn gel://admin:secret@localhost:5656 \\\n        --non-interactive \\\n        --trust-tls-cert \\\n        my_docker_instance\n\nYou can now refer to the instance using the alias ``my_docker_instance``.\nUse this alias wherever an instance name is expected:\n\n.. code-block:: bash\n\n    $ gel -I my_docker_instance\n    Gel x.x\n    Type \\help for help, \\quit to quit.\n    gel>\n\nOr apply migrations:\n\n.. code-block:: bash\n\n    $ gel -I my_docker_instance migrate\n\n\nHealth Checks\n=============\n\nUsing an HTTP client, you can perform health checks to monitor the status of\nyour Gel instance. Learn how to use them with our :ref:`health checks guide\n<ref_guide_deployment_health_checks>`.\n"
  },
  {
    "path": "docs/reference/running/deployment/fly_io.rst",
    "content": ".. _ref_guide_deployment_fly_io:\n\n======\nFly.io\n======\n\n:edb-alt-title: Deploying Gel to Fly.io\n\nIn this guide we show how to deploy Gel using a `Fly.io <https://fly.io>`_\nPostgreSQL cluster as the backend. The deployment consists of two apps: one\nrunning Postgres and the other running Gel.\n\n.. include:: ./note_cloud_reset_password.rst\n\nPrerequisites\n=============\n\n* Fly.io account\n* ``flyctl`` CLI (`install <flyctl-install_>`_)\n\n.. _flyctl-install: https://fly.io/docs/getting-started/installing-flyctl/\n\n\nProvision a Fly.io app for Gel\n==============================\n\nEvery Fly.io app must have a globally unique name, including service VMs like\nPostgres and Gel. Pick a name and assign it to a local environment variable\ncalled ``EDB_APP``. In the command below, replace ``myorg-gel`` with a name\nof your choosing.\n\n.. code-block:: bash\n\n    $ EDB_APP=myorg-gel\n    $ flyctl apps create --name $EDB_APP\n    New app created: myorg-gel\n\n\nNow let's use the ``read`` command to securely assign a value to the\n``PASSWORD`` environment variable.\n\n.. code-block:: bash\n\n   $ echo -n \"> \" && read -s PASSWORD\n\n\nNow let's assign this password to a Fly `secret\n<https://fly.io/docs/reference/secrets/>`_, plus a few other secrets that\nwe'll need. There are a couple more environment variables we need to set:\n\n.. code-block:: bash\n\n    $ flyctl secrets set \\\n        GEL_SERVER_PASSWORD=\"$PASSWORD\" \\\n        GEL_SERVER_BACKEND_DSN_ENV=DATABASE_URL \\\n        GEL_SERVER_TLS_CERT_MODE=generate_self_signed \\\n        GEL_SERVER_PORT=8080 \\\n        --app $EDB_APP\n    Secrets are staged for the first deployment\n\nLet's discuss what's going on with all these secrets.\n\n- The :gelenv:`SERVER_BACKEND_DSN_ENV` tells the Gel container where to\n  look for the PostgreSQL connection string (more on that below)\n- The :gelenv:`SERVER_TLS_CERT_MODE` tells Gel to auto-generate a\n  self-signed TLS certificate.\n\n  You may instead choose to provision a custom TLS certificate. In this\n  case, you should instead create two other secrets: assign your certificate\n  to :gelenv:`SERVER_TLS_CERT` and your private key to\n  :gelenv:`SERVER_TLS_KEY`.\n- Lastly, :gelenv:`SERVER_PORT` tells Gel to listen on port 8080 instead\n  of the default 5656, because Fly.io prefers ``8080`` for its default health\n  checks.\n\nFinally, let's configure the VM size as Gel requires a little bit more than\nthe default Fly.io VM side provides. Put this in a file called ``fly.toml`` in\nyour current directory.:\n\n.. code-block:: yaml\n\n    [build]\n      image = \"geldata/gel\"\n\n    [[vm]]\n      memory = \"512mb\"\n      cpus = 1\n      cpu-kind = \"shared\"\n\n\nCreate a PostgreSQL cluster\n===========================\n\nNow we need to provision a PostgreSQL cluster and attach it to the Gel app.\n\n.. note::\n\n  If you have an existing PostgreSQL cluster in your Fly.io organization,\n  you can skip to the attachment step.\n\nThen create a new PostgreSQL cluster. This may take a few minutes to complete.\n\n.. code-block:: bash\n\n    $ PG_APP=myorg-postgres\n    $ flyctl pg create --name $PG_APP --vm-size shared-cpu-1x\n    ? Select region: sea (Seattle, Washington (US))\n    ? Specify the initial cluster size: 1\n    ? Volume size (GB): 10\n    Creating postgres cluster myorg-postgres in organization personal\n    Postgres cluster myorg-postgres created\n        Username:    postgres\n        Password:    <random password>\n        Hostname:    myorg-postgres.internal\n        Proxy Port:  5432\n        PG Port: 5433\n    Save your credentials in a secure place, you won't be able to see them\n    again!\n    Monitoring Deployment\n    ...\n    --> v0 deployed successfully\n\nIn the output, you'll notice a line that says ``Machine <machine-id> is\ncreated``. The ID in that line is the ID of the virtual machine created for\nyour Postgres cluster. We now need to use that ID to scale the cluster since\nthe ``shared-cpu-1x`` VM doesn't have enough memory by default. Scale it with\nthis command:\n\n.. code-block:: bash\n\n    $ flyctl machine update <machine-id> --memory 1024 --app $PG_APP -y\n    Searching for image 'flyio/postgres:14.6' remotely...\n    image found: img_0lq747j0ym646x35\n    Image: registry-1.docker.io/flyio/postgres:14.6\n    Image size: 361 MB\n\n    Updating machine <machine-id>\n      Waiting for <machine-id> to become healthy (started, 3/3)\n    Machine <machine-id> updated successfully!\n    ==> Monitoring health checks\n      Waiting for <machine-id> to become healthy (started, 3/3)\n    ...\n\nWith the VM scaled sufficiently, we can now attach the PostgreSQL cluster to\nthe Gel app:\n\n.. code-block:: bash\n\n    $ PG_ROLE=myorg_gel\n    $ flyctl pg attach \"$PG_APP\" \\\n        --database-user \"$PG_ROLE\" \\\n        --app $EDB_APP\n    Postgres cluster myorg-postgres is now attached to myorg-gel\n    The following secret was added to myorg-gel:\n      DATABASE_URL=postgres://...\n\nLastly, Gel needs the ability to create Postgres databases and roles,\nso let's adjust the permissions on the role that Gel will use to connect\nto Postgres:\n\n.. code-block:: bash\n\n    $ echo \"alter role \\\"$PG_ROLE\\\" createrole createdb; \\quit\" \\\n        | flyctl pg connect --app $PG_APP\n    ...\n    ALTER ROLE\n\n.. _ref_guide_deployment_fly_io_start_gel:\n\nStart Gel\n=========\n\nEverything is set! Time to start Gel.\n\n.. code-block:: bash\n\n    $ flyctl deploy --remote-only --app $EDB_APP\n    ...\n    Finished launching new machines\n    -------\n     ✔ Machine e286630dce9638 [app] was created\n    -------\n\nThat's it!  You can now start using the Gel instance located at\n:geluri:`myorg-gel.internal` in your Fly.io apps.\n\n\nIf deploy did not succeed:\n\n1. make sure you've created the ``fly.toml`` file.\n2. re-run the ``deploy`` command\n3. check the logs for more information: ``flyctl logs --app $EDB_APP``\n\nPersist the generated TLS certificate\n=====================================\n\nNow we need to persist the auto-generated TLS certificate to make sure it\nsurvives Gel app restarts. (If you've provided your own certificate,\nskip this step).\n\n.. code-block:: bash\n\n    $ EDB_SECRETS=\"GEL_SERVER_TLS_KEY GEL_SERVER_TLS_CERT\"\n    $ flyctl ssh console --app $EDB_APP -C \\\n        \"gel-show-secrets.sh --format=toml $EDB_SECRETS\" \\\n      | tr -d '\\r' | flyctl secrets import --app $EDB_APP\n\n\nConnecting your application\n===========================\n\nTo connect your application to the Gel instance, you'll need to provide\nconnection parameters. Gel client libraries can be configured using either\na DSN (connection string) or individual environment variables.\n\nObtaining connection parameters\n-------------------------------\n\nYour connection requires the following components:\n\n- **Host (internal)**: ``$EDB_APP.internal`` — Fly uses this synthetic TLD\n  for inter-app communication (e.g., ``myorg-gel.internal``)\n- **Host (external)**: ``$EDB_APP.fly.dev`` — for connections from outside\n  Fly.io (requires exposing the port, see below)\n- **Port**: ``8080``, which we configured earlier with :gelenv:`SERVER_PORT`\n- **Username**: |admin| (the default superuser)\n- **Password**: The value you assigned to ``$PASSWORD``\n- **Branch**: |main| (the default branch)\n\nConstruct the DSN for internal Fly.io connections:\n\n.. code-block:: bash\n\n    $ GEL_DSN=gel://admin:$PASSWORD@$EDB_APP.internal:8080\n\nConsider writing it to a file to ensure the DSN looks correct. Remember to\ndelete the file after you're done. (Printing this value to the terminal with\n``echo`` is insecure and can leak your password into shell logs.)\n\n.. code-block:: bash\n\n    $ echo $GEL_DSN > dsn.txt\n    $ open dsn.txt\n    $ rm dsn.txt\n\nObtaining the TLS certificate\n-----------------------------\n\nIf you need secure TLS connections (required for external access), retrieve\nthe server's TLS certificate:\n\n.. code-block:: bash\n\n    $ flyctl ssh console -a $EDB_APP \\\n        -C \"gel-show-secrets.sh --format=raw GEL_SERVER_TLS_CERT\"\n\nSave this to a file or set it as a secret in your application.\n\nFrom a Fly.io app\n-----------------\n\nTo connect to this instance from another Fly app (say, an app that runs your\nAPI server) set the value of the :gelenv:`DSN` secret inside that app.\n\n.. code-block:: bash\n\n    $ flyctl secrets set \\\n        GEL_DSN=$DSN \\\n        --app my-other-fly-app\n\nWe'll also set another variable that will disable Gel's TLS checks.\nInter-application communication is secured by Fly so TLS isn't vital in\nthis case; configuring TLS certificates is also beyond the scope of this guide.\n\n.. code-block:: bash\n\n    $ flyctl secrets set GEL_CLIENT_TLS_SECURITY=insecure \\\n        --app my-other-fly-app\n\n\nYou can also set these values as environment variables inside your\n``fly.toml`` file, but using Fly's built-in `secrets\n<https://fly.io/docs/reference/secrets/>`_ functionality is recommended.\n\nFrom external application\n-------------------------\n\nIf you need to access Gel from outside the Fly.io network, you'll need to\nconfigure the Fly.io proxy to let external connections in.\n\nLet's make sure the ``[[services]]`` section in our ``fly.toml`` looks\nsomething like this:\n\n.. code-block:: toml\n\n    [[services]]\n        http_checks = []\n        internal_port = 8080\n        processes = [\"app\"]\n        protocol = \"tcp\"\n        script_checks = []\n        [services.concurrency]\n            hard_limit = 25\n            soft_limit = 20\n            type = \"connections\"\n\n        [[services.ports]]\n            port = 5656\n\n        [[services.tcp_checks]]\n            grace_period = \"1s\"\n            interval = \"15s\"\n            restart_limit = 0\n            timeout = \"2s\"\n\nIn the same directory, :ref:`redeploy the Gel app\n<ref_guide_deployment_fly_io_start_gel>`. This makes the Gel port\navailable to the outside world. You can now access the instance from any host\nvia the following public DSN: :geluri:`admin:$PASSWORD@$EDB_APP.fly.dev`.\n\nTo secure communication between the server and the client, you will also\nneed to set the :gelenv:`TLS_CA` environment secret in your application.\nYou can securely obtain the certificate content by running:\n\n.. code-block:: bash\n\n    $ flyctl ssh console -a $EDB_APP \\\n        -C \"gel-show-secrets.sh --format=raw GEL_SERVER_TLS_CERT\"\n\nLocal development with the CLI\n------------------------------\n\nTo access the Gel instance from your local development machine, install\nthe Wireguard `VPN <vpn_>`_ and create a tunnel, as described on Fly's\n`Private Networking\n<https://fly.io/docs/reference/private-networking/#private-network-vpn>`_\ndocs.\n\nOnce it's up and running, use :gelcmd:`instance link` to create a local\nalias to the remote instance.\n\n.. note::\n\n   The command groups :gelcmd:`instance` and :gelcmd:`project` are not\n   intended to manage production instances.\n\n.. code-block:: bash\n\n    $ gel instance link \\\n        --dsn $GEL_DSN \\\n        --non-interactive \\\n        --trust-tls-cert \\\n        my_fly_instance\n    Authenticating to gel://admin@myorg-gel.internal:8080/main\n    Successfully linked to remote instance. To connect run:\n      gel -I my_fly_instance\n\nYou can now refer to the remote instance using the alias ``my_fly_instance``.\nUse this alias wherever an instance name is expected:\n\n.. code-block:: bash\n\n    $ gel -I my_fly_instance\n    Gel x.x\n    Type \\help for help, \\quit to quit.\n    gel>\n\nOr apply migrations:\n\n.. code-block:: bash\n\n    $ gel -I my_fly_instance migrate\n\n.. _vpn: https://fly.io/docs/reference/private-networking/#private-network-vpn\n\n\nHealth Checks\n=============\n\nUsing an HTTP client, you can perform health checks to monitor the status of\nyour Gel instance. Learn how to use them with our :ref:`health checks guide\n<ref_guide_deployment_health_checks>`.\n"
  },
  {
    "path": "docs/reference/running/deployment/gcp.rst",
    "content": ".. _ref_guide_deployment_gcp:\n\n============\nGoogle Cloud\n============\n\n:edb-alt-title: Deploying Gel to Google Cloud\n\nIn this guide we show how to deploy Gel on GCP using Cloud SQL and\nKubernetes.\n\n.. include:: ./note_cloud_reset_password.rst\n\nPrerequisites\n=============\n\n* Google Cloud account with billing enabled (or a `free trial <gcp-trial_>`_)\n* ``gcloud`` CLI (`install <gcloud-intsll_>`_)\n* ``kubectl`` CLI (`install <kubectl-install_>`_)\n\n.. _gcp-trial: https://cloud.google.com/free/\n.. _gcloud-intsll: https://cloud.google.com/sdk/\n.. _kubectl-install: https://kubernetes.io/docs/tasks/tools/install-kubectl/\n\nMake sure you are logged into Google Cloud.\n\n.. code-block:: bash\n\n   $ gcloud init\n\nCreate a project\n================\n\nSet the ``PROJECT`` environment variable to the project name you'd like to\nuse. Google Cloud only allow letters, numbers, and hyphens.\n\n.. code-block:: bash\n\n   $ PROJECT=gel\n\nThen create a project with this name. Skip this step if your project already\nexists.\n\n.. code-block:: bash\n\n   $ gcloud projects create $PROJECT\n\nThen enable the requisite APIs.\n\n.. code-block:: bash\n\n   $ gcloud services enable \\\n       container.googleapis.com \\\n       sqladmin.googleapis.com \\\n       iam.googleapis.com \\\n       --project=$PROJECT\n\nProvision a Postgres instance\n=============================\n\nUse the ``read`` command to securely assign a value to the ``PASSWORD``\nenvironment variable.\n\n.. code-block:: bash\n\n   $ echo -n \"> \" && read -s PASSWORD\n\nThen create a Cloud SQL instance and set the password.\n\n.. code-block:: bash\n\n   $ gcloud sql instances create ${PROJECT}-postgres \\\n       --database-version=POSTGRES_17 \\\n       --edition=enterprise \\\n       --cpu=1 \\\n       --memory=3840MiB \\\n       --region=us-west2 \\\n       --project=$PROJECT\n   $ gcloud sql users set-password postgres \\\n       --instance=${PROJECT}-postgres \\\n       --password=$PASSWORD \\\n       --project=$PROJECT\n\nCreate a Kubernetes cluster\n===========================\n\nCreate an empty Kubernetes cluster inside your project.\n\n.. code-block:: bash\n\n   $ gcloud container clusters create ${PROJECT}-k8s \\\n       --zone=us-west2-a \\\n       --num-nodes=1 \\\n       --project=$PROJECT\n\nConfigure service account\n=========================\n\nCreate a new service account, configure its permissions, and generate a\n``credentials.json`` file.\n\n.. code-block:: bash\n\n   $ gcloud iam service-accounts create ${PROJECT}-account \\\n       --project=$PROJECT\n\n   $ MEMBER=\"${PROJECT}-account@${PROJECT}.iam.gserviceaccount.com\"\n   $ gcloud projects add-iam-policy-binding $PROJECT \\\n       --member=serviceAccount:${MEMBER} \\\n       --role=roles/cloudsql.admin \\\n       --project=$PROJECT\n\n   $ gcloud iam service-accounts keys create credentials.json \\\n       --iam-account=${MEMBER}\n\nThen use this ``credentials.json`` to authenticate the Kubernetes CLI tool\n``kubectl``.\n\n.. code-block:: bash\n\n   $ gcloud components install gke-gcloud-auth-plugin\n   $ kubectl create secret generic cloudsql-instance-credentials \\\n       --from-file=credentials.json=credentials.json\n\n   $ INSTANCE_CONNECTION_NAME=$(\n       gcloud sql instances describe ${PROJECT}-postgres \\\n           --format=\"value(connectionName)\" \\\n           --project=$PROJECT\n     )\n\n   $ DSN=\"postgresql://postgres:${PASSWORD}@127.0.0.1:5432\"\n   $ kubectl create secret generic cloudsql-db-credentials \\\n       --from-literal=dsn=$DSN \\\n       --from-literal=password=$PASSWORD \\\n       --from-literal=instance=${INSTANCE_CONNECTION_NAME}=tcp:5432\n\nDeploy Gel\n==========\n\nDownload the starter Gel Kubernetes configuration file. This file specifies\na persistent volume, a container running a `Cloud SQL authorization proxy\n<https://github.com/GoogleCloudPlatform/cloudsql-proxy>`_, and a container to\nrun `Gel itself <https://github.com/geldata/gel-docker>`_. It relies on\nthe secrets we declared in the previous step.\n\n.. code-block:: bash\n\n   $ wget \"https://raw.githubusercontent.com\\\n   /geldata/gel-deploy/dev/gcp/deployment.yaml\"\n\n   $ kubectl apply -f deployment.yaml\n\nEnsure the pods are running.\n\n.. code-block:: bash\n\n   $ kubectl get pods\n   NAME                     READY   STATUS              RESTARTS   AGE\n   gel-977b8fdf6-jswlw      0/2     ContainerCreating   0          16s\n\nThe ``READY  0/2`` tells us neither of the two pods have finished booting.\nRe-run the command until ``2/2`` pods are ``READY``.\n\nIf there were errors you can check Gel's logs with:\n\n.. code-block:: bash\n\n   $ kubectl logs deployment/gel --container gel\n\nPersist TLS Certificate\n=======================\n\nNow that our Gel instance is up and running, we need to download a local\ncopy of its self-signed TLS certificate (which it generated on startup) and\npass it as a secret into Kubernetes. Then we'll redeploy the pods.\n\n.. code-block:: bash\n\n   $ kubectl create secret generic cloudsql-tls-credentials \\\n       --from-literal=tlskey=\"$(\n           kubectl exec deploy/gel -c=gel -- \\\n               gel-show-secrets.sh --format=raw GEL_SERVER_TLS_KEY\n       )\" \\\n       --from-literal=tlscert=\"$(\n           kubectl exec deploy/gel -c=gel -- \\\n               gel-show-secrets.sh --format=raw GEL_SERVER_TLS_CERT\n       )\"\n\n   $ kubectl delete -f deployment.yaml\n\n   $ kubectl apply -f deployment.yaml\n\nExpose Gel\n==========\n\n.. code-block:: bash\n\n   $ kubectl expose deploy/gel --type LoadBalancer\n\n\nConnecting your application\n===========================\n\nTo connect your application to the Gel instance, you'll need to provide\nconnection parameters. Gel client libraries can be configured using either\na DSN (connection string) or individual environment variables.\n\nObtaining connection parameters\n-------------------------------\n\nYour connection requires the following components:\n\n- **Host**: The ``EXTERNAL-IP`` of the LoadBalancer service\n- **Port**: ``5656`` (the default Gel port)\n- **Username**: |admin| (the default superuser)\n- **Password**: The value you assigned to ``$PASSWORD``\n- **Branch**: |main| (the default branch)\n\nGet the public-facing IP address of your database:\n\n.. code-block:: bash\n\n    $ kubectl get service\n    NAME         TYPE           CLUSTER-IP  EXTERNAL-IP   PORT(S)\n    gel          LoadBalancer   <ip>        <ip>          5656:30841/TCP\n\nCopy the ``EXTERNAL-IP`` associated with the ``gel`` service and construct\nyour instance's :ref:`DSN <ref_dsn>`:\n\n.. code-block:: bash\n\n    $ GEL_IP=<copy IP address here>\n    $ GEL_DSN=\"gel://admin:${PASSWORD}@${GEL_IP}\"\n\nTo print the final DSN, you can ``echo`` it. Note that you should only run\nthis command on a computer you trust, like a personal laptop or sandboxed\nenvironment.\n\n.. code-block:: bash\n\n    $ echo $GEL_DSN\n\nObtaining the TLS certificate\n-----------------------------\n\nSince we configured Gel with a self-signed TLS certificate, your application\nneeds the certificate to connect securely. Retrieve it from the running pod:\n\n.. code-block:: bash\n\n    $ kubectl exec deploy/gel -c=gel -- \\\n        gel-show-secrets.sh --format=raw GEL_SERVER_TLS_CERT \\\n        > gel-tls-cert.pem\n\nAlternatively, retrieve it using the Gel CLI:\n\n.. code-block:: bash\n\n    $ gel --dsn $GEL_DSN --tls-security insecure \\\n        query \"SELECT sys::get_tls_certificate()\" > gel-tls-cert.pem\n\nTest your connection by opening a REPL:\n\n.. code-block:: bash\n\n    $ gel --dsn $GEL_DSN --tls-security insecure\n    Gel x.x (repl x.x)\n    Type \\help for help, \\quit to quit.\n    gel> select \"hello world!\";\n\nLocal development with the CLI\n------------------------------\n\nTo make your remote instance easier to work with during local development,\ncreate an alias using :gelcmd:`instance link`.\n\n.. note::\n\n   The command groups :gelcmd:`instance` and :gelcmd:`project` are not\n   intended to manage production instances.\n\n.. code-block:: bash\n\n    $ echo $PASSWORD | gel instance link \\\n        --dsn $GEL_DSN \\\n        --password-from-stdin \\\n        --non-interactive \\\n        --trust-tls-cert \\\n        my_gcp_instance\n\nYou can now refer to the remote instance using the alias ``my_gcp_instance``.\nUse this alias wherever an instance name is expected:\n\n.. code-block:: bash\n\n    $ gel -I my_gcp_instance\n    Gel x.x\n    Type \\help for help, \\quit to quit.\n    gel>\n\nOr apply migrations:\n\n.. code-block:: bash\n\n    $ gel -I my_gcp_instance migrate\n\nUsing in your application\n-------------------------\n\nSet these environment variables where you deploy your application:\n\n.. code-block:: bash\n\n    GEL_DSN=\"gel://admin:<password>@<external-ip>:5656\"\n    # For self-signed certificates, provide the CA cert:\n    GEL_TLS_CA_FILE=\"/path/to/gel-tls-cert.pem\"\n    # Or embed the certificate content directly:\n    GEL_TLS_CA=\"<certificate content>\"\n    # Or (for development only) disable TLS verification:\n    # GEL_CLIENT_TLS_SECURITY=insecure\n\nGel's client libraries will automatically read these environment variables.\n\n\nHealth Checks\n=============\n\nUsing an HTTP client, you can perform health checks to monitor the status of\nyour Gel instance. Learn how to use them with our :ref:`health checks guide\n<ref_guide_deployment_health_checks>`.\n"
  },
  {
    "path": "docs/reference/running/deployment/index.rst",
    "content": ".. _ref_guide_deployment:\n\n\n==========\nDeployment\n==========\n\n|Gel| can be hosted on all major cloud hosting platforms. The guides below\ndemonstrate how to spin up both a managed PostgreSQL instance and a container\nrunning Gel `in Docker <https://github.com/geldata/gel-docker>`_.\n\n.. note:: Minimum requirements\n\n    As a rule of thumb, the Gel Docker container requires 1GB RAM! Images\n    with insufficient RAM may experience unexpected issues during startup.\n\n    When using an external PostgreSQL instance Gel must connect with the\n    PostgreSQL superuser.\n\n.. toctree::\n    :maxdepth: 1\n\n    aws_aurora_ecs\n    azure_flexibleserver\n    digitalocean\n    fly_io\n    gcp\n    docker\n    bare_metal\n"
  },
  {
    "path": "docs/reference/running/deployment/note_cloud_reset_password.rst",
    "content": ".. note:: Gel Cloud: Reset the default password for the admin role\n\n    If you want to dump an existing Gel Cloud instance and restore it to a new self-managed instance, you need to change the automatically generated password for the default admin role - ``edgedb`` or ``admin``.\n    The administrator role name and its password used in the dump/restore process must be the same in both the instance dumped from and the instance restored to for the Gel tooling to continue functioning properly.\n    To change the default password in the Cloud instance, execute the following query in the instance:\n\n    .. code-block:: edgeql\n\n        ALTER ROLE admin { set password := 'new_password' };\n"
  },
  {
    "path": "docs/reference/running/http.rst",
    "content": ".. _ref_reference_http_api:\n\n========\nHTTP API\n========\n\nGel provides HTTP endpoints that allow you to monitor the health and performance of your instance. You can use these endpoints to check if your instance is alive and ready to receive queries, as well as to collect metrics about its operation.\n\nYour branch's URL takes the form of ``http://<hostname>:<port>``.\n\nHere's how to determine your local Gel instance's HTTP server URL:\n\n- The ``hostname`` will be ``localhost``\n\n- Find the ``port`` by running :gelcmd:`instance list`. This will print a table of all Gel instances on your machine, including their associated port number.\n\nTo determine the URL of a remote instance you have linked with the CLI, you can get both the hostname and port of the instance from the \"Port\" column of the :gelcmd:`instance list` table (formatted as ``<hostname>:<port>``).\n\n.. _ref_edgeql_http_health_checks:\n.. _ref_reference_health_checks:\n.. _ref_guide_deployment_health_checks:\n\nHealth Checks\n=============\n\n|Gel| exposes endpoints to check for aliveness and readiness of your database\ninstance.\n\nAliveness\n---------\n\nCheck if your instance is alive.\n\n.. code-block::\n\n    http://<hostname>:<port>/server/status/alive\n\nIf your instance is alive, it will respond with a ``200`` status code and ``\"OK\"`` as the payload. Otherwise, it will respond with a ``50x`` or a network error.\n\nReadiness\n---------\n\nCheck if your instance is ready to receive queries.\n\n.. code-block::\n\n    http://<hostname>:<port>/server/status/ready\n\nIf your instance is ready, it will respond with a ``200`` status code and ``\"OK\"`` as the payload. Otherwise, it will respond with a ``50x`` or a network error.\n\n\n.. _ref_observability:\n\nObservability\n=============\n\nRetrieve instance metrics.\n\n.. code-block::\n\n    http://<hostname>:<port>/metrics\n\nAll Gel instances expose a Prometheus-compatible endpoint available via GET request. The following metrics are made available.\n\nSystem\n------\n\n``compiler_process_spawns_total``\n  **Counter.** Total number of compiler processes spawned.\n\n``compiler_processes_current``\n  **Gauge.** Current number of active compiler processes.\n\n``branches_current``\n  **Gauge.** Current number of branches.\n\nBackend connections and performance\n-----------------------------------\n\n``backend_connections_total``\n  **Counter.** Total number of backend connections established.\n\n``backend_connections_current``\n  **Gauge.** Current number of active backend connections.\n\n``backend_connection_establishment_errors_total``\n  **Counter.** Number of times the server could not establish a backend connection.\n\n``backend_connection_establishment_latency``\n  **Histogram.** Time it takes to establish a backend connection, in seconds.\n\n``backend_query_duration``\n  **Histogram.** Time it takes to run a query on a backend connection, in seconds.\n\nClient connections\n------------------\n\n``client_connections_total``\n  **Counter.** Total number of clients.\n\n``client_connections_current``\n  **Gauge.** Current number of active clients.\n\n``client_connections_idle_total``\n  **Counter.** Total number of forcefully closed idle client connections.\n\n``client_connection_duration``\n  **Histogram.** Time a client connection is open.\n\nQueries and compilation\n-----------------------\n\n``edgeql_query_compilations_total``\n  **Counter.** Number of compiled/cached queries or scripts since instance startup. A query is compiled and then cached on first use, increasing the ``path=\"compiler\"`` parameter. Subsequent uses of the same query only use the cache, thus only increasing the ``path=\"cache\"`` parameter.\n\n``edgeql_query_compilation_duration``\n  Deprecated in favor of ``query_compilation_duration[interface=\"edgeql\"]``.\n\n  **Histogram.** Time it takes to compile an EdgeQL query or script, in seconds.\n\n``graphql_query_compilations_total``\n  **Counter.** Number of compiled/cached GraphQL queries since instance startup. A query is compiled and then cached on first use, increasing the ``path=\"compiler\"`` parameter. Subsequent uses of the same query only use the cache, thus only increasing the ``path=\"cache\"`` parameter.\n\n``sql_queries_total``\n  **Counter.** Number of SQL queries since instance startup.\n\n``sql_compilations_total``\n  **Counter.** Number of SQL compilations since instance startup.\n\n``query_compilation_duration``\n  **Histogram.** Time it takes to compile a query or script, in seconds.\n\n``queries_per_connection``\n  **Histogram.** Number of queries per connection.\n\n``query_size``\n  **Histogram.** Number of bytes in a query, where the label ``interface=edgeql`` means the size of an EdgeQL query, ``=graphql`` for a GraphQL query, ``=sql`` for a readonly SQL query from the user, and ``=compiled`` for a backend SQL query compiled and issued by the server.\n\nAuth Extension\n--------------\n\n``auth_api_calls_total``\n  **Counter.** Number of API calls to the Auth extension.\n\n``auth_ui_renders_total``\n  **Counter.** Number of UI pages rendered by the Auth extension.\n\n``auth_providers``\n  **Histogram.** Number of Auth providers configured.\n\n``auth_successful_logins_total``\n  **Counter.** Number of successful logins in the Auth extension.\n\nErrors\n------\n\n``background_errors_total``\n  **Counter.** Number of unhandled errors in background server routines.\n\n``transaction_serialization_errors_total``\n  **Counter.** Number of transaction serialization errors.\n\n``connection_errors_total``\n  **Counter.** Number of network connection errors."
  },
  {
    "path": "docs/reference/running/index.rst",
    "content": ".. _ref_running_index:\n\n===========\nRunning Gel\n===========\n\n.. toctree::\n    :maxdepth: 2\n    :hidden:\n\n    local\n    deployment/index\n    configuration\n    http\n    backend_ha\n    admin/index\n\nThis section provides comprehensive guidance for deploying and managing Gel database instances.\n\nGet your instance running\n=========================\n\nWhile running local project instances with the CLI is low maintenance and easy to get started, Gel also makes it easy to configure your production deployment through:\n\n* Environment variables\n* Configuration files\n* Server CLI arguments\n\nThese configuration mechanisms allow you to tailor your Gel deployment to your specific needs. Operations teams can control everything from memory usage, connection limits, to TLS and network settings, ensuring your Gel deployment aligns with organizational policies and infrastructure constraints while maintaining optimal performance.\n\nAnd keep it humming\n===================\n\nRunning Gel in production requires effective management and monitoring capabilities. Gel provides a comprehensive set of tools for these purposes:\n\n- **HTTP endpoints** for health checks and metrics collection that integrate with your monitoring infrastructure\n- **Administrative commands** for:\n  - Role management (creating, altering, and dropping roles)\n  - Branch operations (creating, dropping, and managing branches)\n  - Database maintenance operations like vacuum for reclaiming space\n  - Updating internal statistics used by the query planner\n\nThese tools enable you to maintain optimal performance, manage access control, and troubleshoot issues in your Gel deployments. The reference documentation for these features is organized in the sections that follow.\n"
  },
  {
    "path": "docs/reference/running/local.rst",
    "content": ".. _ref_running_local:\n\n=================\nLocal development\n=================\n\nSee the :ref:`ref_guide_using_projects` page for information on how to use projects to manage local development."
  },
  {
    "path": "docs/reference/stdlib/abstract.rst",
    "content": ".. _ref_std_abstract_types:\n\n==============\nAbstract Types\n==============\n\nAbstract types are used to describe polymorphic functions, otherwise known as\n\"generic functions,\" which can be called on a broad range of types.\n\n\n----------\n\n\n.. eql:type:: anytype\n\n    :index: any anytype\n\n    A generic type.\n\n    It is a placeholder used in cases where no specific type\n    requirements are needed, such as defining polymorphic parameters\n    in functions and operators.\n\n\n----------\n\n\n.. eql:type:: std::anyobject\n\n    :index: any anytype object\n\n    A generic object.\n\n    Similarly to :eql:type:`anytype`, this type is used to denote a generic\n    object. This is useful when defining polymorphic parameters in functions\n    and operators as it conforms to whatever type is actually passed. This is\n    different friom :eql:type:`BaseObject` which although is the parent type\n    of any object also only has an ``id`` property, making access to other\n    properties and links harder.\n\n\n----------\n\n\n.. eql:type:: std::anyscalar\n\n    :index: any anytype scalar\n\n    An abstract base scalar type.\n\n    All scalar types are derived from this type.\n\n\n----------\n\n\n.. eql:type:: std::anyenum\n\n    :index: any anytype enum\n\n    An abstract base enumerated type.\n\n    All :eql:type:`enum` types are derived from this type.\n\n\n----------\n\n\n.. eql:type:: anytuple\n\n    :index: any anytype anytuple\n\n    A generic tuple.\n\n    Similarly to :eql:type:`anytype`, this type is used to denote a generic\n    tuple without detailing its component types. This is useful when defining\n    polymorphic parameters in functions and operators.\n\n\nAbstract Numeric Types\n======================\n\nThese abstract numeric types extend :eql:type:`anyscalar`.\n\n.. eql:type:: std::anyint\n\n    :index: any anytype int\n\n    An abstract base scalar type for\n    :eql:type:`int16`, :eql:type:`int32`, and :eql:type:`int64`.\n\n\n----------\n\n\n.. eql:type:: std::anyfloat\n\n    :index: any anytype float\n\n    An abstract base scalar type for\n    :eql:type:`float32` and :eql:type:`float64`.\n\n\n----------\n\n\n.. eql:type:: std::anyreal\n\n    :index: any anytype\n\n    An abstract base scalar type for\n    :eql:type:`anyint`, :eql:type:`anyfloat`, and :eql:type:`decimal`.\n\n\nAbstract Range Types\n====================\n\nThere are some types that can be used to construct :ref:`ranges\n<ref_std_range>`. These scalar types are distinguished by the following\nabstract types:\n\n.. eql:type:: std::anypoint\n\n    :index: any anypoint anyrange\n\n    Abstract base type for all valid ranges.\n\n    Abstract base scalar type for :eql:type:`int32`, :eql:type:`int64`,\n    :eql:type:`float32`, :eql:type:`float64`, :eql:type:`decimal`,\n    :eql:type:`datetime`, :eql:type:`cal::local_datetime`, and\n    :eql:type:`cal::local_date`.\n\n\n----------\n\n\n.. eql:type:: std::anydiscrete\n\n    :index: any anydiscrete anyrange discrete\n\n    An abstract base type for all valid *discrete* ranges.\n\n    This is an abstract base scalar type for :eql:type:`int32`,\n    :eql:type:`int64`, and :eql:type:`cal::local_date`.\n\n\n----------\n\n\n.. eql:type:: std::anycontiguous\n\n    :index: any anycontiguous anyrange\n\n    An abstract base type for all valid *contiguous* ranges.\n\n    This is an abstract base scalar type for :eql:type:`float32`,\n    :eql:type:`float64`, :eql:type:`decimal`, :eql:type:`datetime`, and\n    :eql:type:`cal::local_datetime`.\n"
  },
  {
    "path": "docs/reference/stdlib/array.rst",
    "content": ".. _ref_std_array:\n\n======\nArrays\n======\n\n:edb-alt-title: Array Functions and Operators\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:op:`array[i] <arrayidx>`\n      - :eql:op-desc:`arrayidx`\n\n    * - :eql:op:`array[from:to] <arrayslice>`\n      - :eql:op-desc:`arrayslice`\n\n    * - :eql:op:`array ++ array <arrayplus>`\n      - :eql:op-desc:`arrayplus`\n\n    * - :eql:op:`= <eq>` :eql:op:`\\!= <neq>` :eql:op:`?= <coaleq>`\n        :eql:op:`?!= <coalneq>` :eql:op:`\\< <lt>` :eql:op:`\\> <gt>`\n        :eql:op:`\\<= <lteq>` :eql:op:`\\>= <gteq>`\n      - Comparison operators\n\n    * - :eql:func:`len`\n      - Returns the number of elements in the array.\n\n    * - :eql:func:`contains`\n      - Checks if an element is in the array.\n\n    * - :eql:func:`find`\n      - Finds the index of an element in the array.\n\n    * - :eql:func:`array_join`\n      - Renders an array to a string or byte-string.\n\n    * - :eql:func:`array_fill`\n      - :eql:func-desc:`array_fill`\n\n    * - :eql:func:`array_replace`\n      - :eql:func-desc:`array_replace`\n\n    * - :eql:func:`array_set`\n      - :eql:func-desc:`array_set`\n\n    * - :eql:func:`array_insert`\n      - :eql:func-desc:`array_insert`\n\n    * - :eql:func:`array_agg`\n      - :eql:func-desc:`array_agg`\n\n    * - :eql:func:`array_get`\n      - :eql:func-desc:`array_get`\n\n    * - :eql:func:`array_unpack`\n      - :eql:func-desc:`array_unpack`\n\nArrays store expressions of the *same type* in an ordered list.\n\n.. _ref_std_array_constructor:\n\nConstructing arrays\n^^^^^^^^^^^^^^^^^^^\n\nAn array constructor is an expression that consists of a sequence of\ncomma-separated expressions *of the same type* enclosed in square brackets.\nIt produces an array value:\n\n.. eql:synopsis::\n\n    \"[\" <expr> [, ...] \"]\"\n\nFor example:\n\n.. code-block:: edgeql-repl\n\n    db> select [1, 2, 3];\n    {[1, 2, 3]}\n    db> select [('a', 1), ('b', 2), ('c', 3)];\n    {[('a', 1), ('b', 2), ('c', 3)]}\n\nEmpty arrays\n^^^^^^^^^^^^\n\nYou can also create an empty array, but it must be done by providing the type\ninformation using type casting. Gel cannot infer the type of an empty array\ncreated otherwise. For example:\n\n.. code-block:: edgeql-repl\n\n    db> select [];\n    QueryError: expression returns value of indeterminate type\n    Hint: Consider using an explicit type cast.\n    ### select [];\n    ###        ^\n\n    db> select <array<int64>>[];\n    {[]}\n\n\n\nReference\n^^^^^^^^^\n\n.. eql:type:: std::array\n\n    An ordered list of values of the same type.\n\n    Array indexing starts at zero.\n\n    An array can contain any type except another array. In Gel, arrays are\n    always one-dimensional.\n\n    An array type is created implicitly when an :ref:`array\n    constructor <ref_std_array_constructor>` is used:\n\n    .. code-block:: edgeql-repl\n\n        db> select [1, 2];\n        {[1, 2]}\n\n    The array types themselves are denoted by ``array`` followed by their\n    sub-type in angle brackets. These may appear in cast operations:\n\n    .. code-block:: edgeql-repl\n\n        db> select <array<str>>[1, 4, 7];\n        {['1', '4', '7']}\n        db> select <array<bigint>>[1, 4, 7];\n        {[1n, 4n, 7n]}\n\n    Array types may also appear in schema declarations:\n\n    .. code-block:: sdl\n\n        type Person {\n            str_array: array<str>;\n            json_array: array<json>;\n        }\n\n    See also the list of standard :ref:`array functions <ref_std_array>`, as\n    well as :ref:`generic functions <ref_std_generic>` such as\n    :eql:func:`len`.\n\n\n----------\n\n\n.. eql:operator:: arrayidx: array<anytype> [ int64 ] -> anytype\n\n    .. api-index:: §array§[§int§]\n\n    Accesses the array element at a given index.\n\n    Example:\n\n    .. code-block:: edgeql-repl\n\n        db> select [1, 2, 3][0];\n        {1}\n        db> select [(x := 1, y := 1), (x := 2, y := 3.3)][1];\n        {(x := 2, y := 3.3)}\n\n    This operator also allows accessing elements from the end of the array\n    using a negative index:\n\n    .. code-block:: edgeql-repl\n\n        db> select [1, 2, 3][-1];\n        {3}\n\n    Referencing a non-existent array element will result in an error:\n\n    .. code-block:: edgeql-repl\n\n        db> select [1, 2, 3][4];\n        InvalidValueError: array index 4 is out of bounds\n\n\n----------\n\n\n.. eql:operator:: arrayslice: array<anytype> [ int64 : int64 ] -> anytype\n\n    .. api-index:: §array§[§int§:§int§]\n\n    Produces a sub-array from an existing array.\n\n    Omitting the lower bound of an array slice will default to a lower bound\n    of zero.\n\n    Omitting the upper bound will default the upper bound to the length of the\n    array.\n\n    The lower bound of an array slice is inclusive while the upper bound is\n    not.\n\n    Examples:\n\n    .. code-block:: edgeql-repl\n\n        db> select [1, 2, 3][0:2];\n        {[1, 2]}\n        db> select [1, 2, 3][2:];\n        {[3]}\n        db> select [1, 2, 3][:1];\n        {[1]}\n        db> select [1, 2, 3][:-2];\n        {[1]}\n\n    Referencing an array slice beyond the array boundaries will result in an\n    empty array (unlike a direct reference to a specific index). Slicing with\n    a lower bound less than the minimum index or a upper bound greater than\n    the maximum index are functionally equivalent to not specifying those\n    bounds for your slice:\n\n    .. code-block:: edgeql-repl\n\n        db> select [1, 2, 3][1:20];\n        {[2, 3]}\n        db> select [1, 2, 3][10:20];\n        {[]}\n\n\n---------\n\n\n.. eql:operator:: arrayplus: array<anytype> ++ array<anytype> -> array<anytype>\n\n    .. index:: concatenate, join, add\n    .. api-index:: §array §++§ array§\n\n    Concatenates two arrays of the same type into one.\n\n    .. code-block:: edgeql-repl\n\n        db> select [1, 2, 3] ++ [99, 98];\n        {[1, 2, 3, 99, 98]}\n\n\n----------\n\n\n.. eql:function:: std::array_agg(s: set of anytype) -> array<anytype>\n\n    .. index:: aggregate\n\n    Returns an array made from all of the input set elements.\n\n    The ordering of the input set will be preserved if specified:\n\n    .. code-block:: edgeql-repl\n\n        db> select array_agg({2, 3, 5});\n        {[2, 3, 5]}\n\n        db> select array_agg(User.name order by User.name);\n        {['Alice', 'Bob', 'Joe', 'Sam']}\n\n\n----------\n\n\n.. eql:function:: std::array_get(array: array<anytype>, \\\n                                 index: int64, \\\n                                 named only default: anytype = {} \\\n                              ) -> optional anytype\n\n    Returns the element of a given *array* at the specified *index*.\n\n    If the index is out of the array's bounds, the *default* argument or\n    ``{}`` (empty set) will be returned.\n\n    This works the same as the :eql:op:`array indexing operator <arrayidx>`,\n    except that if the index is out of bounds, an empty set\n    of the array element's type is returned instead of raising an exception:\n\n    .. code-block:: edgeql-repl\n\n        db> select array_get([2, 3, 5], 1);\n        {3}\n        db> select array_get([2, 3, 5], 100);\n        {}\n        db> select array_get([2, 3, 5], 100, default := 42);\n        {42}\n\n\n----------\n\n\n.. eql:function:: std::array_unpack(array: array<anytype>) -> set of anytype\n\n    Returns the elements of an array as a set.\n\n    .. note::\n\n        The ordering of the returned set is not guaranteed.\n        However, if it is wrapped in a call to :eql:func:`enumerate`,\n        the assigned indexes are guaranteed to match the array.\n\n    .. code-block:: edgeql-repl\n\n        db> select array_unpack([2, 3, 5]);\n        {3, 2, 5}\n\n        db> select enumerate(array_unpack([2, 3, 5]));\n        {(1, 3), (0, 2), (2, 5)}\n\n\n----------\n\n\n.. eql:function:: std::array_join(array: array<str>, delimiter: str) -> str\n                  std::array_join(array: array<bytes>, \\\n                                  delimiter: bytes) -> bytes\n\n    .. index:: array_to_string, implode\n\n    Renders an array to a string or byte-string.\n\n    Join a string array into a single string using a specified *delimiter*:\n\n    .. code-block:: edgeql-repl\n\n        db> select array_join(['one', 'two', 'three'], ', ');\n        {'one, two, three'}\n\n    Similarly, an array of :eql:type:`bytes` can be joined as a single value\n    using a specified *delimiter*:\n\n    .. code-block:: edgeql-repl\n\n        db> select array_join([b'\\x01', b'\\x02', b'\\x03'], b'\\xff');\n        {b'\\x01\\xff\\x02\\xff\\x03'}\n\n\n----------\n\n\n.. eql:function:: std::array_fill(val: anytype, n: int64) -> array<anytype>\n\n    Returns an array of the specified size, filled with the provided value.\n\n    Create an array of size *n* where every element has the value *val*.\n\n    .. code-block:: edgeql-repl\n\n        db> select array_fill(0, 5);\n        {[0, 0, 0, 0, 0]}\n        db> select array_fill('n/a', 3);\n        {['n/a', 'n/a', 'n/a']}\n\n\n----------\n\n\n.. eql:function:: std::array_replace(array: array<anytype>, \\\n                                     old: anytype, \\\n                                     new: anytype) \\\n                  -> array<anytype>\n\n    Returns an array with all occurrences of one value replaced by another.\n\n    Return an array where every *old* value is replaced with *new*.\n\n    .. code-block:: edgeql-repl\n\n        db> select array_replace([1, 1, 2, 3, 5], 1, 99);\n        {[99, 99, 2, 3, 5]}\n        db> select array_replace(['h', 'e', 'l', 'l', 'o'], 'l', 'L');\n        {['h', 'e', 'L', 'L', 'o']}\n\n\n----------\n\n\n.. eql:function:: std::array_set(array: array<anytype>, \\\n                                 idx: int64, \\\n                                 val: anytype) \\\n                  -> array<anytype>\n\n    .. versionadded:: 6.0\n\n    Returns an array with an value at a specific index replaced by another.\n\n    Return an array where the value at the index indicated by *idx* is\n    replaced with *val*.\n\n    .. code-block:: edgeql-repl\n\n        db> select array_set(['hello', 'world'], 0, 'goodbye');\n        {['goodbye', 'world']}\n        db> select array_set([1, 1, 2, 3], 1, 99);\n        {[1, 99, 2, 3]}\n\n\n----------\n\n\n.. eql:function:: std::array_insert(array: array<anytype>, \\\n                                    idx: int64, \\\n                                    val: anytype) \\\n                  -> array<anytype>\n\n    .. versionadded:: 6.0\n\n    Returns an array with an value inserted at a specific.\n\n    Return an array where the value *val* is inserted at the index indicated by *idx*.\n\n    .. code-block:: edgeql-repl\n\n        db> select array_insert(['the', 'brown', 'fox'], 1, 'quick');\n        {['the', 'quick', 'brown', 'fox']}\n        db> select array_insert([1, 1, 2, 3], 1, 99);\n        {[1, 99, 1, 2, 3]}\n"
  },
  {
    "path": "docs/reference/stdlib/bool.rst",
    "content": ".. _ref_std_logical:\n\n\n========\nBooleans\n========\n\n:edb-alt-title: Boolean Functions and Operators\n\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:type:`bool`\n      - Boolean type\n\n    * - :eql:op:`bool or bool <or>`\n      - :eql:op-desc:`or`\n\n    * - :eql:op:`bool and bool <and>`\n      - :eql:op-desc:`and`\n\n    * - :eql:op:`not bool <not>`\n      - :eql:op-desc:`not`\n\n    * - :eql:op:`= <eq>` :eql:op:`\\!= <neq>` :eql:op:`?= <coaleq>`\n        :eql:op:`?!= <coalneq>` :eql:op:`\\< <lt>` :eql:op:`\\> <gt>`\n        :eql:op:`\\<= <lteq>` :eql:op:`\\>= <gteq>`\n      - Comparison operators\n\n    * - :eql:func:`all`\n      - :eql:func-desc:`all`\n\n    * - :eql:func:`any`\n      - :eql:func-desc:`any`\n\n    * - :eql:func:`assert`\n      - :eql:func-desc:`assert`\n\n----------\n\n\n.. eql:type:: std::bool\n\n    A boolean type of either ``true`` or ``false``.\n\n    EdgeQL has case-insensitive keywords and that includes the boolean\n    literals:\n\n    .. code-block:: edgeql-repl\n\n        db> select (True, true, TRUE);\n        {(true, true, true)}\n        db> select (False, false, FALSE);\n        {(false, false, false)}\n\n    These basic operators will always result in a boolean type value (although,\n    for some of them, that value may be the empty set if an operand is the\n    empty set):\n\n    - :eql:op:`= <eq>`\n    - :eql:op:`\\!= <neq>`\n    - :eql:op:`?= <coaleq>`\n    - :eql:op:`?!= <coalneq>`\n    - :eql:op:`in`\n    - :eql:op:`not in <in>`\n    - :eql:op:`\\< <lt>`\n    - :eql:op:`\\> <gt>`\n    - :eql:op:`\\<= <lteq>`\n    - :eql:op:`\\>= <gteq>`\n    - :eql:op:`like`\n    - :eql:op:`ilike`\n\n    These operators will result in a boolean type value even if the right\n    operand is the empty set:\n\n    - :eql:op:`in`\n    - :eql:op:`not in <in>`\n\n    These operators will always result in a boolean ``true`` or ``false``\n    value, even if either operand is the empty set:\n\n    - :eql:op:`?= <coaleq>`\n    - :eql:op:`?!= <coalneq>`\n\n    These operators will produce the empty set if either operand is the empty\n    set:\n\n    - :eql:op:`= <eq>`\n    - :eql:op:`\\!= <neq>`\n    - :eql:op:`\\< <lt>`\n    - :eql:op:`\\> <gt>`\n    - :eql:op:`\\<= <lteq>`\n    - :eql:op:`\\>= <gteq>`\n    - :eql:op:`like`\n    - :eql:op:`ilike`\n\n    If you need to use these operators and it's possible one or both operands\n    will be the empty set, you can ensure a ``bool`` product by\n    :eql:op:`coalescing <coalesce>`. With ``=`` and ``!=``, you can use their\n    respective dedicated coalescing operators, ``?=`` and ``?!=``. See each\n    individual operator for an example.\n\n    Some boolean operator examples:\n\n    .. code-block:: edgeql-repl\n\n        db> select true and 2 < 3;\n        {true}\n        db> select '!' IN {'hello', 'world'};\n        {false}\n\n    It's possible to get a boolean by casting a :eql:type:`str` or\n    :eql:type:`json` value into it:\n\n    .. code-block:: edgeql-repl\n\n        db> select <bool>('true');\n        {true}\n        db> select <bool>to_json('false');\n        {false}\n\n    :ref:`Filter clauses <ref_eql_statements_select_filter>` must\n    always evaluate to a boolean:\n\n    .. code-block:: edgeql\n\n        select User\n        filter .name ilike 'alice';\n\n\n----------\n\n\n.. eql:operator:: or: bool or bool -> bool\n\n    .. api-index:: or\n\n    Evaluates ``true`` if either boolean is ``true``.\n\n    .. code-block:: edgeql-repl\n\n        db> select false or true;\n        {true}\n\n    .. warning::\n\n        When either operand in an ``or`` is an empty set, the result will not\n        be a ``bool`` but instead an empty set.\n\n        .. code-block:: edgeql-repl\n\n            db> select true or <bool>{};\n            {}\n\n        If one of the operands in an ``or`` operation could be an empty set,\n        you may want to use the :eql:op:`coalesce` operator (``??``) on that\n        side to ensure you will still get a ``bool`` result.\n\n        .. code-block:: edgeql-repl\n\n            db> select true or (<bool>{} ?? false);\n            {true}\n\n\n----------\n\n\n.. eql:operator:: and: bool and bool -> bool\n\n    .. api-index:: and\n\n    Evaluates ``true`` if both booleans are ``true``.\n\n    .. code-block:: edgeql-repl\n\n        db> select false and true;\n        {false}\n\n    .. warning::\n\n        When either operand in an ``and`` is an empty set, the result will not\n        be a ``bool`` but instead an empty set.\n\n        .. code-block:: edgeql-repl\n\n            db> select true and <bool>{};\n            {}\n\n        If one of the operands in an ``and`` operation could be an empty set,\n        you may want to use the :eql:op:`coalesce` operator (``??``) on that\n        side to ensure you will still get a ``bool`` result.\n\n        .. code-block:: edgeql-repl\n\n            db> select true and (<bool>{} ?? false);\n            {false}\n\n\n----------\n\n\n.. eql:operator:: not: not bool -> bool\n\n    .. api-index:: not\n\n    Logically negates a given boolean value.\n\n    .. code-block:: edgeql-repl\n\n        db> select not false;\n        {true}\n\n    .. warning::\n\n        When the operand in a ``not`` is an empty set, the result will not be a\n        ``bool`` but instead an empty set.\n\n        .. code-block:: edgeql-repl\n\n            db> select not <bool>{};\n            {}\n\n        If the operand in a ``not`` operation could be an empty set, you may\n        want to use the :eql:op:`coalesce` operator (``??``) on that side to\n        ensure you will still get a ``bool`` result.\n\n        .. code-block:: edgeql-repl\n\n            db> select not (<bool>{} ?? false);\n            {true}\n\n\n----------\n\n\nThe ``and`` and ``or`` operators are commutative.\n\nThe truth tables are as follows:\n\n+-------+-------+---------------+--------------+--------------+\n|   a   |   b   |  a ``and`` b  |  a ``or`` b  |  ``not`` a   |\n+=======+=======+===============+==============+==============+\n| true  | true  |   true        |   true       |   false      |\n+-------+-------+---------------+--------------+--------------+\n| true  | false |   false       |   true       |   false      |\n+-------+-------+---------------+--------------+--------------+\n| false | true  |   false       |   true       |   true       |\n+-------+-------+---------------+--------------+--------------+\n| false | false |   false       |   false      |   true       |\n+-------+-------+---------------+--------------+--------------+\n\n\n----------\n\nThe operators ``and``/``or`` and the functions :eql:func:`all`/:eql:func:`any`\ndiffer in the way they handle an empty set (``{}``). Both ``and`` and ``or``\noperators apply to the cross-product of their operands. If either operand is\nan empty set, the result will also be an empty set. For example:\n\n.. code-block:: edgeql-repl\n\n    db> select {true, false} and <bool>{};\n    {}\n    db> select true and <bool>{};\n    {}\n\nOperating on an empty set with :eql:func:`all`/:eql:func:`any` does *not*\nreturn an empty set:\n\n.. code-block:: edgeql-repl\n\n    db> select all(<bool>{});\n    {true}\n    db> select any(<bool>{});\n    {false}\n\n:eql:func:`all` returns ``true`` because the empty set contains no false\nvalues.\n\n:eql:func:`any` returns ``false`` because the empty set contains no\ntrue values.\n\nThe :eql:func:`all` and :eql:func:`any` functions are generalized to apply to\nsets of values, including ``{}``. Thus they have the following truth\ntable:\n\n+-------+-------+-----------------+-----------------+\n|   a   |   b   | ``all({a, b})`` | ``any({a, b})`` |\n+=======+=======+=================+=================+\n| true  | true  |   true          |   true          |\n+-------+-------+-----------------+-----------------+\n| true  | false |   false         |   true          |\n+-------+-------+-----------------+-----------------+\n| {}    | true  |   true          |   true          |\n+-------+-------+-----------------+-----------------+\n| {}    | false |   false         |   false         |\n+-------+-------+-----------------+-----------------+\n| false | true  |   false         |   true          |\n+-------+-------+-----------------+-----------------+\n| false | false |   false         |   false         |\n+-------+-------+-----------------+-----------------+\n| true  | {}    |   true          |   true          |\n+-------+-------+-----------------+-----------------+\n| false | {}    |   false         |   false         |\n+-------+-------+-----------------+-----------------+\n| {}    | {}    |   true          |   false         |\n+-------+-------+-----------------+-----------------+\n\nSince :eql:func:`all` and :eql:func:`any` apply to sets as a whole,\nmissing values (represented by ``{}``) are just that - missing. They\ndon't affect the overall result.\n\nTo understand the last line in the above truth table it's useful to\nremember that ``all({a, b}) = all(a) and all(b)`` and ``any({a, b}) =\nany(a) or any(b)``.\n\nFor more customized handling of ``{}``, use the :eql:op:`?? <coalesce>`\noperator.\n\n\n----------\n\n\n.. eql:function:: std::assert( \\\n                    input: bool, \\\n                    named only message: optional str = <str>{} \\\n                  ) -> bool\n\n    Checks that the input bool is ``true``.\n\n    If the input bool is ``false``, ``assert`` raises a\n    ``QueryAssertionError``. Otherwise, this function returns ``true``.\n\n    .. code-block:: edgeql-repl\n\n        db> select assert(true);\n        {true}\n\n        db> select assert(false);\n        gel error: QueryAssertionError: assertion failed\n\n        db> select assert(false, message := 'value is not true');\n        gel error: QueryAssertionError: value is not true\n\n    ``assert`` can be used in triggers to create more powerful constraints. In\n    this schema, the ``Person`` type has both ``friends`` and ``enemies``\n    links. You may not want a ``Person`` to be both a friend and an enemy of\n    the same ``Person``. ``assert`` can be used inside a trigger to easily\n    prohibit this.\n\n    .. code-block:: sdl\n\n          type Person {\n            required name: str;\n            multi friends: Person;\n            multi enemies: Person;\n\n            trigger prohibit_frenemies after insert, update for each do (\n              assert(\n                not exists (__new__.friends intersect __new__.enemies),\n                message := \"Invalid frenemies\",\n              )\n            )\n          }\n\n    With this trigger in place, it is impossible to link the same ``Person`` as\n    both a friend and an enemy of any other person.\n\n    .. code-block:: edgeql-repl\n\n        db> insert Person {name := 'Quincey Morris'};\n        {default::Person {id: e4a55480-d2de-11ed-93bd-9f4224fc73af}}\n        db> insert Person {name := 'Dracula'};\n        {default::Person {id: e7f2cff0-d2de-11ed-93bd-279780478afb}}\n        db> update Person\n        ... filter .name = 'Quincey Morris'\n        ... set {\n        ...   enemies := (\n        ...     select detached Person filter .name = 'Dracula'\n        ...   )\n        ... };\n        {default::Person {id: e4a55480-d2de-11ed-93bd-9f4224fc73af}}\n        db> update Person\n        ... filter .name = 'Quincey Morris'\n        ... set {\n        ...   friends := (\n        ...     select detached Person filter .name = 'Dracula'\n        ...   )\n        ... };\n        gel error: GelError: Invalid frenemies\n\n    In the following examples, the ``size`` properties of the ``File`` objects\n    are ``1024``, ``1024``, and ``131,072``.\n\n    .. code-block:: edgeql-repl\n\n        db> for obj in (select File)\n        ... union (assert(obj.size <= 128*1024, message := 'file too big'));\n        {true, true, true}\n\n        db> for obj in (select File)\n        ... union (assert(obj.size <= 64*1024, message := 'file too big'));\n        gel error: QueryAssertionError: file too big\n\n    You may call ``assert`` in the ``order by`` clause of your ``select``\n    statement. This will ensure it is called only on objects that pass your\n    filter.\n\n    .. code-block:: edgeql-repl\n\n        db> select File { name, size }\n        ... order by assert(.size <= 128*1024, message := \"file too big\");\n        {\n          default::File {name: 'File 2', size: 1024},\n          default::File {name: 'Asdf 3', size: 1024},\n          default::File {name: 'File 1', size: 131072},\n        }\n\n        db> select File { name, size }\n        ... order by assert(.size <= 64*1024, message := \"file too big\");\n        gel error: QueryAssertionError: file too big\n\n        db> select File { name, size }\n        ... filter .size <= 64*1024\n        ... order by assert(.size <= 64*1024, message := \"file too big\");\n        {\n          default::File {name: 'File 2', size: 1024},\n          default::File {name: 'Asdf 3', size: 1024}\n        }\n"
  },
  {
    "path": "docs/reference/stdlib/bytes.rst",
    "content": ".. _ref_std_bytes:\n\n=====\nBytes\n=====\n\n:edb-alt-title: Bytes Functions and Operators\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:type:`bytes`\n      - Byte sequence\n\n    * - :eql:type:`Endian`\n      - An enum for indicating integer value encoding.\n\n    * - :eql:op:`bytes[i] <bytesidx>`\n      - :eql:op-desc:`bytesidx`\n\n    * - :eql:op:`bytes[from:to] <bytesslice>`\n      - :eql:op-desc:`bytesslice`\n\n    * - :eql:op:`bytes ++ bytes <bytesplus>`\n      - :eql:op-desc:`bytesplus`\n\n    * - :eql:op:`= <eq>` :eql:op:`\\!= <neq>` :eql:op:`?= <coaleq>`\n        :eql:op:`?!= <coalneq>` :eql:op:`\\< <lt>` :eql:op:`\\> <gt>`\n        :eql:op:`\\<= <lteq>` :eql:op:`\\>= <gteq>`\n      - Comparison operators\n\n    * - :eql:func:`len`\n      - Returns the number of bytes.\n\n    * - :eql:func:`contains`\n      - Checks if the byte sequence contains a given subsequence.\n\n    * - :eql:func:`find`\n      - Finds the index of the first occurrence of a subsequence.\n\n    * - :eql:func:`to_bytes`\n      - :eql:func-desc:`to_bytes`\n\n    * - :eql:func:`to_str`\n      - :eql:func-desc:`to_str`\n\n    * - :eql:func:`to_int16`\n      - :eql:func-desc:`to_int16`\n\n    * - :eql:func:`to_int32`\n      - :eql:func-desc:`to_int32`\n\n    * - :eql:func:`to_int64`\n      - :eql:func-desc:`to_int64`\n\n    * - :eql:func:`to_uuid`\n      - :eql:func-desc:`to_uuid`\n\n    * - :eql:func:`bytes_get_bit`\n      - :eql:func-desc:`bytes_get_bit`\n\n    * - :eql:func:`bit_count`\n      - :eql:func-desc:`bit_count`\n\n    * - :eql:func:`enc::base64_encode`\n      - :eql:func-desc:`enc::base64_encode`\n\n    * - :eql:func:`enc::base64_decode`\n      - :eql:func-desc:`enc::base64_decode`\n\n----------\n\n\n.. eql:type:: std::bytes\n\n    A sequence of bytes representing raw data.\n\n    Bytes can be represented as a literal using this syntax: ``b''``.\n\n    .. code-block:: edgeql-repl\n\n        db> select b'Hello, world';\n        {b'Hello, world'}\n        db> select b'Hello,\\x20world\\x01';\n        {b'Hello, world\\x01'}\n\n    There are also some :ref:`generic <ref_std_generic>`\n    functions that can operate on bytes:\n\n    .. code-block:: edgeql-repl\n\n        db> select contains(b'qwerty', b'42');\n        {false}\n\n    Bytes are rendered as base64-encoded strings in JSON. When you cast a\n    ``bytes`` value into JSON, that's what you'll get. In order to\n    :eql:op:`cast <cast>` a :eql:type:`json` value into bytes, it must be a\n    base64-encoded string.\n\n    .. code-block:: edgeql-repl\n\n        db> select <json>b'Hello Gel!';\n        {\"\\\"SGVsbG8gRWRnZURCIQ==\\\"\"}\n        db> select <bytes>to_json(\"\\\"SGVsbG8gRWRnZURCIQ==\\\"\");\n        {b'Hello Gel!'}\n\n\n----------\n\n\n.. eql:type:: std::Endian\n\n    .. versionadded:: 5.0\n\n    An enum for indicating integer value encoding.\n\n    This enum is used by the :eql:func:`to_int16`, :eql:func:`to_int32`,\n    :eql:func:`to_int64` and the :eql:func:`to_bytes` converters working with\n    :eql:type:`bytes` and integers.\n\n    ``Endian.Big`` stands for big-endian encoding going from most significant\n    byte to least. ``Endian.Little`` stands for little-endian encoding going\n    from least to most significant byte.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_bytes(<int32>16908295, Endian.Big);\n        {b'\\x01\\x02\\x00\\x07'}\n        db> select to_int32(b'\\x01\\x02\\x00\\x07', Endian.Big);\n        {16908295}\n        db> select to_bytes(<int32>16908295, Endian.Little);\n        {b'\\x07\\x00\\x02\\x01'}\n        db> select to_int32(b'\\x07\\x00\\x02\\x01', Endian.Little);\n        {16908295}\n\n\n----------\n\n\n.. eql:operator:: bytesidx: bytes [ int64 ] -> bytes\n\n    .. api-index:: §bytes§[§int§]\n\n    Accesses a byte at a given index.\n\n    Examples:\n\n    .. code-block:: edgeql-repl\n\n        db> select b'binary \\x01\\x02\\x03\\x04 ftw!'[2];\n        {b'n'}\n        db> select b'binary \\x01\\x02\\x03\\x04 ftw!'[8];\n        {b'\\x02'}\n\n\n----------\n\n\n.. eql:operator:: bytesslice: bytes [ int64 : int64 ] -> bytes\n\n    .. api-index:: §bytes§[§int§:§int§]\n\n    Produces a bytes sub-sequence from an existing bytes value.\n\n    Examples:\n\n    .. code-block:: edgeql-repl\n\n        db> select b'\\x01\\x02\\x03\\x04 ftw!'[2:-1];\n        {b'\\x03\\x04 ftw'}\n        db> select b'some bytes'[2:-3];\n        {b'me by'}\n\n\n---------\n\n\n.. eql:operator:: bytesplus: bytes ++ bytes ->\n\n    .. index:: join, add\n    .. api-index:: §bytes §++§ bytes§\n\n    Concatenates two bytes values into one.\n\n    .. code-block:: edgeql-repl\n\n        db> select b'\\x01\\x02' ++ b'\\x03\\x04';\n        {b'\\x01\\x02\\x03\\x04'}\n\n\n---------\n\n.. TODO: Function signatures except the first need to be revealed only for v5+\n\n.. eql:function:: std::to_bytes(s: str) -> bytes\n                  std::to_bytes(j: json) -> bytes\n                  std::to_bytes(val: int16, endian: Endian) -> bytes\n                  std::to_bytes(val: int32, endian: Endian) -> bytes\n                  std::to_bytes(val: int64, endian: Endian) -> bytes\n                  std::to_bytes(val: uuid) -> bytes\n\n    .. versionadded:: 4.0\n\n    .. index:: encode, stringencoder\n\n    Converts a given value into binary representation as :eql:type:`bytes`.\n\n    The strings get converted using UTF-8 encoding:\n\n    .. code-block:: edgeql-repl\n\n        db> select to_bytes('テキスト');\n        {b'\\xe3\\x83\\x86\\xe3\\x82\\xad\\xe3\\x82\\xb9\\xe3\\x83\\x88'}\n\n    The json values get converted as strings using UTF-8 encoding:\n\n    .. code-block:: edgeql-repl\n\n        db> select to_bytes(to_json('{\"a\": 1}'));\n        {b'{\"a\": 1}'}\n\n    The integer values can be encoded as big-endian (most significant bit\n    comes first) byte strings:\n\n    .. code-block:: edgeql-repl\n\n        db> select to_bytes(<int16>31, Endian.Big);\n        {b'\\x00\\x1f'}\n        db> select to_bytes(<int32>31, Endian.Big);\n        {b'\\x00\\x00\\x00\\x1f'}\n        db> select to_bytes(123456789123456789, Endian.Big);\n        {b'\\x01\\xb6\\x9bK\\xac\\xd0_\\x15'}\n\n    .. note::\n\n        Due to underlying implementation details using big-endian encoding\n        results in slightly faster performance of ``to_bytes`` when converting\n        integers.\n\n    The UUID values are converted to the underlying string of 16 bytes:\n\n    .. code-block:: edgeql-repl\n\n        db> select to_bytes(<uuid>'1d70c86e-cc92-11ee-b4c7-a7aa0a34e2ae');\n        {b'\\x1dp\\xc8n\\xcc\\x92\\x11\\xee\\xb4\\xc7\\xa7\\xaa\\n4\\xe2\\xae'}\n\n    To perform the reverse conversion there are corresponding functions:\n    :eql:func:`to_str`, :eql:func:`to_int16`, :eql:func:`to_int32`,\n    :eql:func:`to_int64`, :eql:func:`to_uuid`.\n\n\n---------\n\n.. eql:function:: std::bytes_get_bit(bytes: bytes, nth: int64) -> int64\n\n    Returns the specified bit of the :eql:type:`bytes` value.\n\n    When looking for the *nth* bit, this function will enumerate bits from\n    least to most significant in each byte.\n\n    .. code-block:: edgeql-repl\n\n        db> for n in {0, 1, 2, 3, 4, 5, 6, 7,\n        ...           8, 9, 10, 11, 12, 13 ,14, 15}\n        ... union bytes_get_bit(b'ab', n);\n        {1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0}\n\n\n---------\n\n\n.. eql:function:: enc::base64_encode(b: bytes) -> str\n\n    .. versionadded:: 4.0\n\n    Returns a Base64-encoded :eql:type:`str` of the :eql:type:`bytes` value.\n\n    .. code-block:: edgeql-repl\n\n        db> select enc::base64_encode(b'hello');\n        {'aGVsbG8='}\n\n---------\n\n.. eql:function:: enc::base64_decode(s: str) -> bytes\n\n    .. versionadded:: 4.0\n\n    Returns the :eql:type:`bytes` of a Base64-encoded :eql:type:`str`.\n\n    Returns an InvalidValueError if input is not valid Base64.\n\n    .. code-block:: edgeql-repl\n\n        db> select enc::base64_decode('aGVsbG8=');\n        {b'hello'}\n        db> select enc::base64_decode('aGVsbG8');\n        gel error: InvalidValueError: invalid base64 end sequence\n"
  },
  {
    "path": "docs/reference/stdlib/cfg.rst",
    "content": ".. _ref_std_cfg:\n\n======\nConfig\n======\n\nThe ``cfg`` module contains a set of types and scalars used for configuring\n|Gel|.\n\n\n.. list-table::\n  :class: funcoptable\n\n  * - **Type**\n    - **Description**\n  * - :eql:type:`cfg::AbstractConfig`\n    - The abstract base type for all configuration objects. The properties\n      of this type define the set of configuruation settings supported by\n      Gel.\n  * - :eql:type:`cfg::Config`\n    - The main configuration object. The properties of this object reflect\n      the overall configuration setting from instance level all the way to\n      session level.\n  * - :eql:type:`cfg::DatabaseConfig`\n    - The database configuration object. It reflects all the applicable\n      configuration at the Gel database level.\n  * - :eql:type:`cfg::BranchConfig`\n    - The database branch configuration object. It reflects all the applicable\n      configuration at the Gel branch level.\n  * - :eql:type:`cfg::InstanceConfig`\n    - The instance configuration object.\n  * - :eql:type:`cfg::ExtensionConfig`\n    - The abstract base type for all extension configuration objects. Each\n      extension can define the necessary configuration settings by extending\n      this type and adding the extension-specific properties.\n  * - :eql:type:`cfg::Auth`\n    - An object type representing an authentication profile.\n  * - :eql:type:`cfg::ConnectionTransport`\n    - An enum type representing the different protocols that Gel speaks.\n  * - :eql:type:`cfg::AuthMethod`\n    - An abstract object type representing a method of authentication\n  * - :eql:type:`cfg::Trust`\n    - A subclass of ``AuthMethod`` indicating an \"always trust\" policy (no\n      authentication).\n  * - :eql:type:`cfg::SCRAM`\n    - A subclass of ``AuthMethod`` indicating password-based authentication.\n  * - :eql:type:`cfg::Password`\n    - A subclass of ``AuthMethod`` indicating basic password-based\n      authentication.\n  * - :eql:type:`cfg::JWT`\n    - A subclass of ``AuthMethod`` indicating token-based authentication.\n  * - :eql:type:`cfg::memory`\n    - A scalar type for storing a quantity of memory storage.\n\n\n.. eql:type:: cfg::AbstractConfig\n\n  An abstract type representing the configuration of an instance or database.\n\n  The properties of this object type represent the set of configuration\n  options supported by Gel (listed above).\n\n\n----------\n\n\n.. eql:type:: cfg::Config\n\n  The main configuration object type.\n\n  This type will have only one object instance. The ``cfg::Config`` object\n  represents the sum total of the current Gel configuration. It reflects\n  the result of applying instance, branch, and session level configuration.\n  Examining this object is the recommended way of determining the current\n  configuration.\n\n  Here's an example of checking and disabling :ref:`access policies\n  <ref_std_cfg_apply_access_policies>`:\n\n  .. code-block:: edgeql-repl\n\n      db> select cfg::Config.apply_access_policies;\n      {true}\n      db> configure session set apply_access_policies := false;\n      OK: CONFIGURE SESSION\n      db> select cfg::Config.apply_access_policies;\n      {false}\n\n\n----------\n\n\n.. eql:type:: cfg::BranchConfig\n\n  .. versionadded:: 5.0\n\n  The branch-level configuration object type.\n\n  This type will have only one object instance. The ``cfg::BranchConfig``\n  object represents the state of the branch and instance-level Gel\n  configuration.\n\n  For overall configuration state please refer to the :eql:type:`cfg::Config`\n  instead.\n\n\n----------\n\n\n.. eql:type:: cfg::InstanceConfig\n\n  The instance-level configuration object type.\n\n  This type will have only one object instance. The ``cfg::InstanceConfig``\n  object represents the state of only instance-level Gel configuration.\n\n  For overall configuraiton state please refer to the :eql:type:`cfg::Config`\n  instead.\n\n\n----------\n\n\n.. eql:type:: cfg::ExtensionConfig\n\n  .. versionadded:: 5.0\n\n  An abstract type representing extension configuration.\n\n  Every extension is expected to define its own extension-specific config\n  object type extending ``cfg::ExtensionConfig``. Any necessary extension\n  configuration setting should be represented as properties of this concrete\n  config type.\n\n  Up to three instances of the extension-specific config type will be created,\n  each of them with a ``required single link cfg`` to the\n  :eql:type:`cfg::Config`, :eql:type:`cfg::DatabaseConfig`, or\n  :eql:type:`cfg::InstanceConfig` object depending on the configuration level.\n  The :eql:type:`cfg::AbstractConfig` exposes a corresponding computed\n  multi-backlink called ``extensions``.\n\n  For example, :ref:`ext::pgvector <ref_ext_pgvector>` extension exposes\n  ``probes`` as a configurable parameter via ``ext::pgvector::Config`` object:\n\n  .. code-block:: edgeql-repl\n\n    db> configure session\n    ... set ext::pgvector::Config::probes := 5;\n    OK: CONFIGURE SESSION\n    db> select cfg::Config.extensions[is ext::pgvector::Config]{*};\n    {\n      ext::pgvector::Config {\n        id: 12b5c70f-0bb8-508a-845f-ca3d41103b6f,\n        probes: 5,\n        ef_search: 40,\n      },\n    }\n\n\n----------\n\n\n.. eql:type:: cfg::Auth\n\n  An object type designed to specify a client authentication profile.\n\n  .. code-block:: edgeql-repl\n\n    db> configure instance insert\n    ...   Auth {priority := 0, method := (insert Trust)};\n    OK: CONFIGURE INSTANCE\n\n  Below are the properties of the ``Auth`` class.\n\n  :eql:synopsis:`priority -> int64`\n    The priority of the authentication rule.  The lower this number,\n    the higher the priority.\n\n  :eql:synopsis:`user -> multi str`\n    The name(s) of the database role(s) this rule applies to.  If set to\n    ``'*'``, then it applies to all roles.\n\n  :eql:synopsis:`method -> cfg::AuthMethod`\n    The name of the authentication method type. Expects an instance of\n    :eql:type:`cfg::AuthMethod`;  Valid values are:\n    ``Trust`` for no authentication and ``SCRAM`` for SCRAM-SHA-256\n    password authentication.\n\n  :eql:synopsis:`comment -> optional str`\n    An optional comment for the authentication rule.\n\n---------\n\n.. eql:type:: cfg::ConnectionTransport\n\n  An enum listing the various protocols that Gel can speak.\n\n  Possible values are:\n\n.. list-table::\n  :class: funcoptable\n\n  * - **Value**\n    - **Description**\n  * - ``cfg::ConnectionTransport.TCP``\n    - Gel binary protocol\n  * - ``cfg::ConnectionTransport.TCP_PG``\n    - Postgres protocol for the\n      :ref:`SQL query mode <ref_sql_adapter>`\n  * - ``cfg::ConnectionTransport.HTTP``\n    - Gel binary protocol\n      :ref:`tunneled over HTTP <ref_http_tunnelling>`\n  * - ``cfg::ConnectionTransport.SIMPLE_HTTP``\n    - :ref:`EdgeQL over HTTP <ref_edgeql_http>`\n      and :ref:`GraphQL <ref_graphql_index>` endpoints\n\n---------\n\n.. eql:type:: cfg::AuthMethod\n\n  An abstract object class that represents an authentication method.\n\n  It currently has four concrete subclasses, each of which represent an\n  available authentication method: :eql:type:`cfg::SCRAM`,\n  :eql:type:`cfg::JWT`, :eql:type:`cfg::Password`, and\n  :eql:type:`cfg::Trust`.\n\n  :eql:synopsis:`transports -> multi cfg::ConnectionTransport`\n    Which connection transports this method applies to.\n    The subclasses have their own defaults for this.\n\n-------\n\n.. eql:type:: cfg::Trust\n\n  The ``cfg::Trust`` indicates an \"always-trust\" policy.\n\n  When active, it disables password-based authentication.\n\n  .. code-block:: edgeql-repl\n\n    db> configure instance insert\n    ...   Auth {priority := 0, method := (insert Trust)};\n    OK: CONFIGURE INSTANCE\n\n-------\n\n.. eql:type:: cfg::SCRAM\n\n  ``cfg::SCRAM`` indicates password-based authentication.\n\n  It uses a challenge-response scheme to avoid transmitting the\n  password directly.  This policy is implemented via ``SCRAM-SHA-256``\n\n  It is available for the ``TCP``, ``TCP_PG``, and ``HTTP`` transports\n  and is the default for ``TCP`` and ``TCP_PG``.\n\n  .. code-block:: edgeql-repl\n\n    db> configure instance insert\n    ...   Auth {priority := 0, method := (insert SCRAM)};\n    OK: CONFIGURE INSTANCE\n\n-------\n\n.. eql:type:: cfg::JWT\n\n  ``cfg::JWT`` uses a JWT signed by the server to authenticate.\n\n  It is available for the ``TCP``, ``HTTP``, and ``HTTP_SIMPLE`` transports\n  and is the default for ``HTTP``.\n\n\n-------\n\n.. eql:type:: cfg::Password\n\n  ``cfg::Password`` indicates simple password-based authentication.\n\n  Unlike :eql:type:`cfg::SCRAM`, this policy transmits the password\n  over the (encrypted) channel.  It is implemened using HTTP Basic\n  Authentication over TLS.\n\n  This policy is available only for the ``SIMPLE_HTTP`` transport, where it is\n  the default.\n\n\n-------\n\n.. eql:type:: cfg::memory\n\n  A scalar type representing a quantity of memory storage.\n\n  As with ``uuid``, ``datetime``, and several other types, ``cfg::memory``\n  values are declared by casting from an appropriately formatted string.\n\n  .. code-block:: edgeql-repl\n\n    db> select <cfg::memory>'1B'; # 1 byte\n    {<cfg::memory>'1B'}\n    db> select <cfg::memory>'5KiB'; # 5 kibibytes\n    {<cfg::memory>'5KiB'}\n    db> select <cfg::memory>'128MiB'; # 128 mebibytes\n    {<cfg::memory>'128MiB'}\n\n  The numerical component of the value must be a non-negative integer; the\n  units must be one of ``B|KiB|MiB|GiB|TiB|PiB``. We're using the explicit\n  ``KiB`` unit notation (1024 bytes) instead of ``kB`` (which is ambiguous,\n  and may mean 1000 or 1024 bytes).\n"
  },
  {
    "path": "docs/reference/stdlib/constraint_table.rst",
    "content": ".. list-table::\n\n    * - :eql:constraint:`exclusive`\n      - Enforce uniqueness (disallow duplicate values)\n\n    * - :eql:constraint:`expression`\n      - Custom constraint expression (followed by keyword ``on``)\n\n    * - :eql:constraint:`one_of`\n      - A list of allowable values\n\n    * - :eql:constraint:`max_value`\n      - Maximum value numerically/lexicographically\n\n    * - :eql:constraint:`max_ex_value`\n      - Maximum value numerically/lexicographically (exclusive range)\n\n    * - :eql:constraint:`min_value`\n      - Minimum value numerically/lexicographically\n\n    * - :eql:constraint:`min_ex_value`\n      - Minimum value numerically/lexicographically (exclusive range)\n\n    * - :eql:constraint:`max_len_value`\n      - Maximum length (``str`` only)\n\n    * - :eql:constraint:`min_len_value`\n      - Minimum length (``str`` only)\n\n    * - :eql:constraint:`regexp`\n      - Regex constraint (``str`` only)\n\n"
  },
  {
    "path": "docs/reference/stdlib/constraints.rst",
    "content": ".. _ref_std_constraints:\n\n===========\nConstraints\n===========\n\n.. include:: constraint_table.rst\n\n\n.. eql:constraint:: std::expression on (expr)\n\n    A constraint based on an arbitrary expression returning a boolean.\n\n    The ``expression`` constraint may be used as in this example to create a\n    custom scalar type:\n\n    .. code-block:: sdl\n\n        scalar type StartsWithA extending str {\n            constraint expression on (__subject__[0] = 'A');\n        }\n\n    Example of using an ``expression`` constraint based on two\n    object properties to restrict maximum magnitude for a vector:\n\n    .. code-block:: sdl\n\n        type Vector {\n            required x: float64;\n            required y: float64;\n            constraint expression on (\n                __subject__.x^2 + __subject__.y^2 < 25\n            );\n        }\n\n.. eql:constraint:: std::one_of(variadic members: anytype)\n\n    Specifies a list of allowed values.\n\n    Example:\n\n    .. code-block:: sdl\n\n        scalar type Status extending str {\n            constraint one_of ('Open', 'Closed', 'Merged');\n        }\n\n.. eql:constraint:: std::max_value(max: anytype)\n\n    Specifies the maximum allowed value.\n\n    Example:\n\n    .. code-block:: sdl\n\n        scalar type Max100 extending int64 {\n            constraint max_value(100);\n        }\n\n.. eql:constraint:: std::max_ex_value(max: anytype)\n\n    Specifies a non-inclusive upper bound for the value.\n\n    Example:\n\n    .. code-block:: sdl\n\n        scalar type Under100 extending int64 {\n            constraint max_ex_value(100);\n        }\n\n    In this example, in contrast to the ``max_value`` constraint, a value\n    of the ``Under100`` type cannot be ``100`` since the valid range of\n    ``max_ex_value`` does not include the value specified in the constraint.\n\n.. eql:constraint:: std::max_len_value(max: int64)\n\n    Specifies the maximum allowed length of a value.\n\n    Example:\n\n    .. code-block:: sdl\n\n        scalar type Username extending str {\n            constraint max_len_value(30);\n        }\n\n.. eql:constraint:: std::min_value(min: anytype)\n\n    Specifies the minimum allowed value.\n\n    Example:\n\n    .. code-block:: sdl\n\n        scalar type NonNegative extending int64 {\n            constraint min_value(0);\n        }\n\n.. eql:constraint:: std::min_ex_value(min: anytype)\n\n    Specifies a non-inclusive lower bound for the value.\n\n    Example:\n\n    .. code-block:: sdl\n\n        scalar type PositiveFloat extending float64 {\n            constraint min_ex_value(0);\n        }\n\n    In this example, in contrast to the ``min_value`` constraint, a value\n    of the ``PositiveFloat`` type cannot be ``0`` since the valid range of\n    ``mix_ex_value`` does not include the value specified in the constraint.\n\n.. eql:constraint:: std::min_len_value(min: int64)\n\n    Specifies the minimum allowed length of a value.\n\n    Example:\n\n    .. code-block:: sdl\n\n        scalar type EmailAddress extending str {\n            constraint min_len_value(3);\n        }\n\n.. eql:constraint:: std::regexp(pattern: str)\n\n    Limits to string values matching a regular expression.\n\n    Example:\n\n    .. code-block:: sdl\n\n        scalar type LettersOnly extending str {\n            constraint regexp(r'[A-Za-z]*');\n        }\n\n    See our documentation on :ref:`regular expression patterns\n    <string_regexp>` for more information on those.\n\n.. eql:constraint:: std::exclusive\n\n    Specifies that the link or property value must be exclusive (unique).\n\n    When applied to a ``multi`` link or property, the exclusivity constraint\n    guarantees that for every object, the set of values held by a link or\n    property does not intersect with any other such set in any other object\n    of this type.\n\n    This constraint is only valid for concrete links and properties.\n    Scalar type definitions cannot include this constraint.\n\n    This constraint has an additional effect of creating an\n    implicit :ref:`index <ref_datamodel_indexes>` on a property.\n    This means that there's no need to add explicit indexes\n    for properties with this constraint.\n\n    Example:\n\n    .. code-block:: sdl\n\n        type User {\n            # Make sure user names are unique.\n            required name: str {\n                constraint exclusive;\n            }\n            # Already indexed, don't need to do this:\n            # index on (.name)\n\n            # Make sure none of the \"owned\" items belong\n            # to any other user.\n            multi owns: Item {\n                constraint exclusive;\n            }\n        }\n\n    Sometimes it may be necessary to create a type where each *combination*\n    of properties is unique. This can be achieved by defining an\n    ``exclusive`` constraint for the combination, rather than on each\n    property:\n\n    .. code-block:: sdl\n\n        type UniqueCoordinates {\n            required x: int64;\n            required y: int64;\n\n            # Each combination of x and y must be unique.\n            constraint exclusive on ( (.x, .y) );\n        }\n\n    Any possible expression can appear in the ``on (<expr>)`` clause of\n    the ``exclusive`` constraint as long as it adheres to the following:\n\n    * The expression can only contain references to the immediate\n      properties or links of the type.\n    * No :ref:`backlinks <ref_datamodel_links>` or long paths are\n      allowed.\n    * Only ``Immutable`` functions are allowed in the constraint\n      expression.\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Schema > Constraints <ref_datamodel_constraints>`\n  * - :ref:`SDL > Constraints <ref_eql_sdl_constraints>`\n  * - :ref:`DDL > Constraints <ref_eql_ddl_constraints>`\n  * - :ref:`Introspection > Constraints\n      <ref_datamodel_introspection_constraints>`\n"
  },
  {
    "path": "docs/reference/stdlib/datetime.rst",
    "content": ".. _ref_std_datetime:\n\n\n===============\nDates and Times\n===============\n\n:edb-alt-title: Types, Functions, and Operators for Dates and Times\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:type:`datetime`\n      - Timezone-aware point in time\n\n    * - :eql:type:`duration`\n      - Absolute time span\n\n    * - :eql:type:`cal::local_datetime`\n      - Date and time w/o timezone\n\n    * - :eql:type:`cal::local_date`\n      - Date type\n\n    * - :eql:type:`cal::local_time`\n      - Time type\n\n    * - :eql:type:`cal::relative_duration`\n      - Relative time span\n\n    * - :eql:type:`cal::date_duration`\n      - Relative time span in days\n\n    * - :eql:op:`dt + dt <dtplus>`\n      - :eql:op-desc:`dtplus`\n\n    * - :eql:op:`dt - dt <dtminus>`\n      - :eql:op-desc:`dtminus`\n\n    * - :eql:op:`= <eq>` :eql:op:`\\!= <neq>` :eql:op:`?= <coaleq>`\n        :eql:op:`?!= <coalneq>` :eql:op:`\\< <lt>` :eql:op:`\\> <gt>`\n        :eql:op:`\\<= <lteq>` :eql:op:`\\>= <gteq>`\n      - Comparison operators\n\n    * - :eql:func:`to_str`\n      - Render a date/time value to a string.\n\n    * - :eql:func:`to_datetime`\n      - :eql:func-desc:`to_datetime`\n\n    * - :eql:func:`cal::to_local_datetime`\n      - :eql:func-desc:`cal::to_local_datetime`\n\n    * - :eql:func:`cal::to_local_date`\n      - :eql:func-desc:`cal::to_local_date`\n\n    * - :eql:func:`cal::to_local_time`\n      - :eql:func-desc:`cal::to_local_time`\n\n    * - :eql:func:`to_duration`\n      - :eql:func-desc:`to_duration`\n\n    * - :eql:func:`cal::to_relative_duration`\n      - :eql:func-desc:`cal::to_relative_duration`\n\n    * - :eql:func:`cal::to_date_duration`\n      - :eql:func-desc:`cal::to_date_duration`\n\n    * - :eql:func:`datetime_get`\n      - :eql:func-desc:`datetime_get`\n\n    * - :eql:func:`cal::time_get`\n      - :eql:func-desc:`cal::time_get`\n\n    * - :eql:func:`cal::date_get`\n      - :eql:func-desc:`cal::date_get`\n\n    * - :eql:func:`duration_get`\n      - :eql:func-desc:`duration_get`\n\n    * - :eql:func:`datetime_truncate`\n      - :eql:func-desc:`datetime_truncate`\n\n    * - :eql:func:`duration_truncate`\n      - :eql:func-desc:`duration_truncate`\n\n    * - :eql:func:`datetime_current`\n      - :eql:func-desc:`datetime_current`\n\n    * - :eql:func:`datetime_of_transaction`\n      - :eql:func-desc:`datetime_of_transaction`\n\n    * - :eql:func:`datetime_of_statement`\n      - :eql:func-desc:`datetime_of_statement`\n\n    * - :eql:func:`cal::duration_normalize_hours`\n      - :eql:func-desc:`cal::duration_normalize_hours`\n\n    * - :eql:func:`cal::duration_normalize_days`\n      - :eql:func-desc:`cal::duration_normalize_days`\n\n.. _ref_std_datetime_intro:\n\n|Gel| offers two ways of representing date/time values:\n\n* a timezone-aware :eql:type:`std::datetime` type;\n\n* a set of \"local\" date/time types, not attached to any particular\n  timezone: :eql:type:`cal::local_datetime`, :eql:type:`cal::local_date`,\n  and :eql:type:`cal::local_time`.\n\nThere are also two different ways of measuring duration:\n\n* :eql:type:`duration` for using absolute and unambiguous units;\n\n* :eql:type:`cal::relative_duration` for using fuzzy units like years,\n  months and days in addition to the absolute units.\n\nAll related operators, functions, and type casts are designed to maintain a\nstrict separation between timezone-aware and \"local\" date/time values.\n\n|Gel| stores and outputs timezone-aware values in UTC format.\n\n.. note::\n\n    All date/time types are restricted to years between 1 and 9999, including\n    the years 1 and 9999.\n\n    Although many systems support ISO 8601 date/time formatting in theory,\n    in practice the formatting before year 1 and after 9999 tends to\n    be inconsistent. As such, dates outside this range are not reliably\n    portable.\n\n.. _ref_std_datetime_timezones:\n\nTimezones\n---------\n\nFor timezone string literals, you may specify timezones in one of two ways:\n\n* IANA (Olson) timezone database name (e.g. ``America/New_York``)\n\n* A time zone abbreviation (e.g. ``EDT`` for Eastern Daylight Time)\n\nSee the `relevant section from the PostgreSQL documentation\n<https://www.postgresql.org/docs/current/datetime-timezones.html#TIMEZONE-TABLES>`_\nfor more detail about how time zones affect the behavior of date/time\nfunctionality.\n\n.. note::\n\n  The IANA timezone database is maintained by Paul Eggert for the IANA. You can\n  find a `GitHub repository with the latest timezone data here\n  <https://github.com/eggert/tz>`_, and the `list of timezone names here\n  <https://github.com/eggert/tz/blob/master/zone1970.tab>`_.\n\n\n----------\n\n\n.. eql:type:: std::datetime\n\n\n    Represents a timezone-aware moment in time.\n\n    All dates must correspond to dates that exist in the proleptic Gregorian\n    calendar.\n\n    :eql:op:`Casting <cast>` is a simple way to obtain a\n    :eql:type:`datetime` value in an expression:\n\n    .. code-block:: edgeql\n\n        select <datetime>'2018-05-07T15:01:22.306916+00';\n        select <datetime>'2018-05-07T15:01:22+00';\n\n    When casting ``datetime`` from strings, the string must follow\n    the ISO 8601 format with a timezone included.\n\n    .. code-block:: edgeql-repl\n\n        db> select <datetime>'January 01 2019 UTC';\n        InvalidValueError: invalid input syntax for type\n        std::datetime: 'January 01 2019 UTC'\n        Hint: Please use ISO8601 format. Alternatively \"to_datetime\"\n        function provides custom formatting options.\n\n        db> select <datetime>'2019-01-01T15:01:22';\n        InvalidValueError: invalid input syntax for type\n        std::datetime: '2019-01-01T15:01:22'\n        Hint: Please use ISO8601 format. Alternatively \"to_datetime\"\n        function provides custom formatting options.\n\n    All ``datetime`` values are restricted to the range from year 1 to 9999.\n\n    For more information regarding interacting with this type, see\n    :eql:func:`datetime_get`, :eql:func:`to_datetime`, and :eql:func:`to_str`.\n\n\n----------\n\n\n.. eql:type:: cal::local_datetime\n\n    A type for representing a date and time without a timezone.\n\n    :eql:op:`Casting <cast>` is a simple way to obtain a\n    :eql:type:`cal::local_datetime` value in an expression:\n\n    .. code-block:: edgeql\n\n        select <cal::local_datetime>'2018-05-07T15:01:22.306916';\n        select <cal::local_datetime>'2018-05-07T15:01:22';\n\n    When casting ``cal::local_datetime`` from strings, the string must follow\n    the ISO 8601 format without timezone:\n\n    .. code-block:: edgeql-repl\n\n        db> select <cal::local_datetime>'2019-01-01T15:01:22+00';\n        InvalidValueError: invalid input syntax for type\n        cal::local_datetime: '2019-01-01T15:01:22+00'\n        Hint: Please use ISO8601 format. Alternatively\n        \"cal::to_local_datetime\" function provides custom formatting\n        options.\n\n        db> select <cal::local_datetime>'January 01 2019';\n        InvalidValueError: invalid input syntax for type\n        cal::local_datetime: 'January 01 2019'\n        Hint: Please use ISO8601 format. Alternatively\n        \"cal::to_local_datetime\" function provides custom formatting\n        options.\n\n    All ``datetime`` values are restricted to the range from year 1 to 9999.\n\n    For more information regarding interacting with this type, see\n    :eql:func:`datetime_get`, :eql:func:`cal::to_local_datetime`, and\n    :eql:func:`to_str`.\n\n\n----------\n\n\n.. eql:type:: cal::local_date\n\n    A type for representing a date without a timezone.\n\n    :eql:op:`Casting <cast>` is a simple way to obtain a\n    :eql:type:`cal::local_date` value in an expression:\n\n    .. code-block:: edgeql\n\n        select <cal::local_date>'2018-05-07';\n\n    When casting ``cal::local_date`` from strings, the string must follow the\n    ISO 8601 date format.\n\n    For more information regarding interacting with this type, see\n    :eql:func:`cal::date_get`, :eql:func:`cal::to_local_date`, and\n    :eql:func:`to_str`.\n\n\n----------\n\n\n.. eql:type:: cal::local_time\n\n    A type for representing a time without a timezone.\n\n    :eql:op:`Casting <cast>` is a simple way to obtain a\n    :eql:type:`cal::local_time` value in an expression:\n\n    .. code-block:: edgeql\n\n        select <cal::local_time>'15:01:22.306916';\n        select <cal::local_time>'15:01:22';\n\n    When casting ``cal::local_time`` from strings, the string must follow the\n    ISO 8601 time format.\n\n    For more information regarding interacting with this type, see\n    :eql:func:`cal::time_get`, :eql:func:`cal::to_local_time`, and\n    :eql:func:`to_str`.\n\n\n----------\n\n\n\n.. _ref_datetime_duration:\n\n.. eql:type:: std::duration\n\n    A type for representing a span of time.\n\n    A :eql:type:`duration` is a fixed number of seconds and microseconds and\n    isn't adjusted by timezone, length of month, or anything else in datetime\n    calculations.\n\n    When converting from a string, only units of ``'microseconds'``,\n    ``'milliseconds'``, ``'seconds'``, ``'minutes'``, and ``'hours'`` are\n    valid:\n\n    .. code-block:: edgeql-repl\n\n        db> select <duration>'45.6 seconds';\n        {<duration>'0:00:45.6'}\n        db> select <duration>'15 milliseconds';\n        {<duration>'0:00:00.015'}\n        db> select <duration>'48 hours 45 minutes';\n        {<duration>'48:45:00'}\n        db> select <duration>'11 months';\n        gel error: InvalidValueError: invalid input syntax for type\n        std::duration: '11 months'\n          Hint: Units bigger than hours cannot be used for std::duration.\n\n    All date/time types support the ``+`` and ``-`` arithmetic operations\n    with durations:\n\n    .. code-block:: edgeql-repl\n\n        db> select <datetime>'2019-01-01T00:00:00Z' - <duration>'24 hours';\n        {<datetime>'2018-12-31T00:00:00+00:00'}\n        db> select <cal::local_time>'22:00' + <duration>'1 hour';\n        {<cal::local_time>'23:00:00'}\n\n    For more information regarding interacting with this type, see\n    :eql:func:`to_duration`, and :eql:func:`to_str` and date/time\n    :eql:op:`operators <dtplus>`.\n\n\n----------\n\n\n.. eql:type:: cal::relative_duration\n\n    A type for representing a relative span of time.\n\n    Unlike :eql:type:`std::duration`, ``cal::relative_duration`` is an\n    imprecise form of measurement. When months and days are used, the same\n    relative duration could have a different absolute duration depending on\n    the date you're measuring from.\n\n    For example 2020 was a leap year and had 366 days. Notice how the number\n    of hours in each year below is different:\n\n    .. code-block:: edgeql-repl\n\n        db> with\n        ...     first_day_of_2020 := <datetime>'2020-01-01T00:00:00Z',\n        ...     one_year := <cal::relative_duration>'1 year',\n        ...     first_day_of_next_year := first_day_of_2020 + one_year\n        ... select first_day_of_next_year - first_day_of_2020;\n        {<duration>'8784:00:00'}\n        db> with\n        ...     first_day_of_2019 := <datetime>'2019-01-01T00:00:00Z',\n        ...     one_year := <cal::relative_duration>'1 year',\n        ...     first_day_of_next_year := first_day_of_2019 + one_year\n        ... select first_day_of_next_year - first_day_of_2019;\n        {<duration>'8760:00:00'}\n\n    When converting from a string, only the following units are valid:\n\n    - ``'microseconds'``\n    - ``'milliseconds'``\n    - ``'seconds'``\n    - ``'minutes'``\n    - ``'hours'``\n    - ``'days'``\n    - ``'weeks'``\n    - ``'months'``\n    - ``'years'``\n    - ``'decades'``\n    - ``'centuries'``\n    - ``'millennia'``\n\n    Examples of units usage:\n\n    .. code-block:: edgeql\n\n        select <cal::relative_duration>'45.6 seconds';\n        select <cal::relative_duration>'15 milliseconds';\n        select <cal::relative_duration>'3 weeks 45 minutes';\n        select <cal::relative_duration>'-7 millennia';\n\n    All date/time types support the ``+`` and ``-`` arithmetic operations\n    with ``relative_duration``:\n\n    .. code-block:: edgeql-repl\n\n        db> select <datetime>'2019-01-01T00:00:00Z' -\n        ...        <cal::relative_duration>'3 years';\n        {<datetime>'2016-01-01T00:00:00+00:00'}\n        db> select <cal::local_time>'22:00' +\n        ...        <cal::relative_duration>'1 hour';\n        {<cal::local_time>'23:00:00'}\n\n    If an arithmetic operation results in a day that doesn't exist in the\n    given month, the last day of the month will be used instead:\n\n    .. code-block:: edgeql-repl\n\n      db> select <cal::local_datetime>\"2021-01-31T15:00:00\" +\n      ...        <cal::relative_duration>\"1 month\";\n      {<cal::local_datetime>'2021-02-28T15:00:00'}\n\n    For arithmetic operations involving a ``cal::relative_duration``\n    consisting of multiple components (units), higher-order components are\n    applied first followed by lower-order components.\n\n    .. code-block:: edgeql-repl\n\n      db> select <cal::local_datetime>\"2021-04-30T15:00:00\" +\n      ...        <cal::relative_duration>\"1 month 1 day\";\n      {<cal::local_datetime>'2021-05-31T15:00:00'}\n\n    If you add the same components split into separate durations, adding the\n    higher-order units first followed by the lower-order units, the\n    calculation produces the same result as in the previous example:\n\n    .. code-block:: edgeql-repl\n\n      db> select <cal::local_datetime>\"2021-04-30T15:00:00\" +\n      ...        <cal::relative_duration>\"1 month\" +\n      ...        <cal::relative_duration>\"1 day\";\n      {<cal::local_datetime>'2021-05-31T15:00:00'}\n\n    When the order of operations is reversed, the result may be different for\n    some corner cases:\n\n    .. code-block:: edgeql-repl\n\n      db> select <cal::local_datetime>\"2021-04-30T15:00:00\" +\n      ...        <cal::relative_duration>\"1 day\" +\n      ...        <cal::relative_duration>\"1 month\";\n      {<cal::local_datetime>'2021-06-01T15:00:00'}\n\n    .. rubric:: Gotchas\n\n    Due to the implementation of ``relative_duration`` logic, arithmetic\n    operations may behave counterintuitively.\n\n    **Non-associative**\n\n    .. code-block:: edgeql-repl\n\n      db> select <cal::local_datetime>'2021-01-31T00:00:00' +\n      ...        <cal::relative_duration>'1 month' +\n      ...        <cal::relative_duration>'1 month';\n      {<cal::local_datetime>'2021-03-28T00:00:00'}\n      db> select <cal::local_datetime>'2021-01-31T00:00:00' +\n      ...       (<cal::relative_duration>'1 month' +\n      ...        <cal::relative_duration>'1 month');\n      {<cal::local_datetime>'2021-03-31T00:00:00'}\n\n    **Lossy**\n\n    .. code-block:: edgeql-repl\n\n      db> with m := <cal::relative_duration>'1 month'\n      ... select <cal::local_date>'2021-01-31' + m\n      ...        =\n      ...        <cal::local_date>'2021-01-30' + m;\n      {true}\n\n    **Asymmetric**\n\n    .. code-block:: edgeql-repl\n\n      db> with m := <cal::relative_duration>'1 month'\n      ... select <cal::local_date>'2021-01-31' + m - m;\n      {<cal::local_date>'2021-01-28'}\n\n    **Non-monotonic**\n\n    .. code-block:: edgeql-repl\n\n      db> with m := <cal::relative_duration>'1 month'\n      ... select <cal::local_datetime>'2021-01-31T01:00:00' + m\n      ...        <\n      ...        <cal::local_datetime>'2021-01-30T23:00:00' + m;\n      {true}\n      db> with m := <cal::relative_duration>'2 month'\n      ... select <cal::local_datetime>'2021-01-31T01:00:00' + m\n      ...        <\n      ...        <cal::local_datetime>'2021-01-30T23:00:00' + m;\n      {false}\n\n    For more information regarding interacting with this type, see\n    :eql:func:`cal::to_relative_duration`, and :eql:func:`to_str` and\n    date/time :eql:op:`operators <dtplus>`.\n\n\n----------\n\n\n.. eql:type:: cal::date_duration\n\n    A type for representing a span of time in days.\n\n    This type is similar to :eql:type:`cal::relative_duration`, except it only\n    uses 2 units: months and days. It is the result of subtracting one\n    :eql:type:`cal::local_date` from another. The purpose of this type is to\n    allow performing ``+`` and ``-`` operations on a\n    :eql:type:`cal::local_date` and to produce a :eql:type:`cal::local_date`\n    as the result:\n\n    .. code-block:: edgeql-repl\n\n      db> select <cal::local_date>'2022-06-30' -\n      ...   <cal::local_date>'2022-06-25';\n      {<cal::date_duration>'P5D'}\n      db> select <cal::local_date>'2022-06-25' +\n      ...   <cal::date_duration>'5 days';\n      {<cal::local_date>'2022-06-30'}\n      db> select <cal::local_date>'2022-06-25' -\n      ...   <cal::date_duration>'5 days';\n      {<cal::local_date>'2022-06-20'}\n\n\n    When converting from a string, only the following units are valid:\n\n    - ``'days'``,\n    - ``'weeks'``,\n    - ``'months'``,\n    - ``'years'``,\n    - ``'decades'``,\n    - ``'centuries'``,\n    - ``'millennia'``.\n\n    .. code-block:: edgeql\n\n        select <cal::date_duration>'45 days';\n        select <cal::date_duration>'3 weeks 5 days';\n        select <cal::date_duration>'-7 millennia';\n\n    In most cases, ``date_duration`` is fully compatible with\n    :eql:type:`cal::relative_duration` and shares the same general behavior\n    and caveats. Gel will apply type coercion in the event it expects a\n    :eql:type:`cal::relative_duration` and finds a ``cal::date_duration``\n    instead.\n\n    For more information regarding interacting with this type, see\n    :eql:func:`cal::to_date_duration` and date/time :eql:op:`operators\n    <dtplus>`.\n\n\n----------\n\n\n.. eql:operator:: dtplus: datetime + duration -> datetime\n                          datetime + cal::relative_duration \\\n                              -> cal::relative_duration\n                          duration + duration -> duration\n                          duration + cal::relative_duration \\\n                              -> cal::relative_duration\n                          cal::relative_duration + cal::relative_duration \\\n                              -> cal::relative_duration\n                          cal::local_datetime + cal::relative_duration \\\n                              -> cal::relative_duration\n                          cal::local_datetime + duration \\\n                              -> cal::local_datetime\n                          cal::local_time + cal::relative_duration \\\n                              -> cal::relative_duration\n                          cal::local_time + duration -> cal::local_time\n                          cal::local_date + cal::date_duration \\\n                              -> cal::local_date\n                          cal::date_duration + cal::date_duration \\\n                              -> cal::date_duration\n                          cal::local_date + cal::relative_duration \\\n                              -> cal::local_datetime\n                          cal::local_date + duration -> cal::local_datetime\n\n    .. api-index:: §datetime | duration §+§ datetime | duration§\n\n    Adds a duration and any other datetime value.\n\n    This operator is commutative.\n\n    .. code-block:: edgeql-repl\n\n        db> select <cal::local_time>'22:00' + <duration>'1 hour';\n        {<cal::local_time>'23:00:00'}\n        db> select <duration>'1 hour' + <cal::local_time>'22:00';\n        {<cal::local_time>'23:00:00'}\n        db> select <duration>'1 hour' + <duration>'2 hours';\n        {10800s}\n\n\n----------\n\n\n.. eql:operator:: dtminus: duration - duration -> duration\n                           datetime - datetime -> duration\n                           datetime - duration -> datetime\n                           datetime - cal::relative_duration -> datetime\n                           cal::relative_duration - cal::relative_duration \\\n                                -> cal::relative_duration\n                           cal::local_datetime - cal::local_datetime \\\n                                -> cal::relative_duration\n                           cal::local_datetime - cal::relative_duration \\\n                                -> cal::local_datetime\n                           cal::local_datetime - duration \\\n                                -> cal::local_datetime\n                           cal::local_time - cal::local_time \\\n                                -> cal::relative_duration\n                           cal::local_time - cal::relative_duration \\\n                                -> cal::local_time\n                           cal::local_time - duration -> cal::local_time\n                           cal::date_duration - cal::date_duration \\\n                                -> cal::date_duration\n                           cal::local_date - cal::local_date \\\n                                -> cal::date_duration\n                           cal::local_date - cal::date_duration \\\n                                -> cal::local_date\n                           cal::local_date - cal::relative_duration \\\n                                -> cal::local_datetime\n                           cal::local_date - duration -> cal::local_datetime\n                           duration - cal::relative_duration \\\n                                -> cal::relative_duration\n                           cal::relative_duration - duration\\\n                                -> cal::relative_duration\n\n    .. api-index:: §datetime | duration §-§ datetime | duration§\n\n    Subtracts two compatible datetime or duration values.\n\n    .. code-block:: edgeql-repl\n\n        db> select <datetime>'2019-01-01T01:02:03+00' -\n        ...   <duration>'24 hours';\n        {<datetime>'2018-12-31T01:02:03Z'}\n        db> select <datetime>'2019-01-01T01:02:03+00' -\n        ...   <datetime>'2019-02-01T01:02:03+00';\n        {-2678400s}\n        db> select <duration>'1 hour' -\n        ...   <duration>'2 hours';\n        {-3600s}\n\n    When subtracting a :eql:type:`cal::local_date` type from another, the\n    result is given as a whole number of days using the\n    :eql:type:`cal::date_duration` type:\n\n    .. code-block:: edgeql-repl\n\n        db> select <cal::local_date>'2022-06-25' -\n        ...   <cal::local_date>'2019-02-01';\n        {<cal::date_duration>'P1240D'}\n\n    .. note::\n\n        Subtraction doesn't make sense for some type combinations. You\n        couldn't subtract a point in time from a duration, so neither can\n        Gel (although the inverse — subtracting a duration from a point in\n        time — is perfectly fine). You also couldn't subtract a timezone-aware\n        datetime from a local one or vice versa. If you attempt any of these,\n        Gel will raise an exception as shown in these examples.\n\n    When subtracting a date/time object from a time interval, an exception\n    will be raised:\n\n    .. code-block:: edgeql-repl\n\n        db> select <duration>'1 day' -\n        ...   <datetime>'2019-01-01T01:02:03+00';\n        QueryError: operator '-' cannot be applied to operands ...\n\n    An exception will also be raised when trying to subtract a timezone-aware\n    :eql:type:`std::datetime` type from :eql:type:`cal::local_datetime` or\n    vice versa:\n\n    .. code-block:: edgeql-repl\n\n        db> select <datetime>'2019-01-01T01:02:03+00' -\n        ...   <cal::local_datetime>'2019-02-01T01:02:03';\n        QueryError: operator '-' cannot be applied to operands...\n        db> select <cal::local_datetime>'2019-02-01T01:02:03' -\n        ...   <datetime>'2019-01-01T01:02:03+00';\n        QueryError: operator '-' cannot be applied to operands...\n\n\n----------\n\n.. eql:function:: std::datetime_current() -> datetime\n\n    .. index:: now\n\n    Returns the server's current date and time.\n\n    .. code-block:: edgeql-repl\n\n        db> select datetime_current();\n        {<datetime>'2018-05-14T20:07:11.755827Z'}\n\n    This function is volatile since it always returns the current time when it\n    is called. As a result, it cannot be used in :ref:`computed properties\n    defined in schema <ref_datamodel_computed>`. This does *not* apply to\n    computed properties outside of schema.\n\n----------\n\n\n.. eql:function:: std::datetime_of_transaction() -> datetime\n\n    .. index:: now\n\n    Returns the date and time of the start of the current transaction.\n\n    This function is non-volatile since it returns the current time when the\n    transaction is started, not when the function is called. As a result, it\n    can be used in :ref:`computed properties <ref_datamodel_computed>` defined\n    in schema.\n\n----------\n\n\n.. eql:function:: std::datetime_of_statement() -> datetime\n\n    .. index:: now\n\n    Returns the date and time of the start of the current statement.\n\n    This function is non-volatile since it returns the current time when the\n    statement is started, not when the function is called. As a result, it\n    can be used in :ref:`computed properties <ref_datamodel_computed>` defined\n    in schema.\n\n----------\n\n\n.. eql:function:: std::datetime_get(dt: datetime, el: str) -> float64\n                  std::datetime_get(dt: cal::local_datetime, \\\n                                    el: str) -> float64\n\n    Returns the element of a date/time given a unit name.\n\n    You may pass any of these unit names for *el*:\n\n    - ``'epochseconds'`` - the number of seconds since 1970-01-01 00:00:00\n      UTC (Unix epoch) for :eql:type:`datetime` or local time for\n      :eql:type:`cal::local_datetime`. It can be negative.\n    - ``'century'`` - the century according to the Gregorian calendar\n    - ``'day'`` - the day of the month (1-31)\n    - ``'decade'`` - the decade (year divided by 10 and rounded down)\n    - ``'dow'`` - the day of the week from Sunday (0) to Saturday (6)\n    - ``'doy'`` - the day of the year (1-366)\n    - ``'hour'`` - the hour (0-23)\n    - ``'isodow'`` - the ISO day of the week from Monday (1) to Sunday (7)\n    - ``'isoyear'`` - the ISO 8601 week-numbering year that the date falls in.\n      See the ``'week'`` element for more details.\n    - ``'microseconds'`` - the seconds including fractional value expressed\n      as microseconds\n    - ``'millennium'`` - the millennium. The third millennium started\n      on Jan 1, 2001.\n    - ``'milliseconds'`` - the seconds including fractional value expressed\n      as milliseconds\n    - ``'minutes'`` - the minutes (0-59)\n    - ``'month'`` - the month of the year (1-12)\n    - ``'quarter'`` - the quarter of the year (1-4)\n    - ``'seconds'`` - the seconds, including fractional value from 0 up to and\n      not including 60\n    - ``'week'`` - the number of the ISO 8601 week-numbering week of\n      the year. ISO weeks are defined to start on Mondays and the\n      first week of a year must contain Jan 4 of that year.\n    - ``'year'`` - the year\n\n    .. code-block:: edgeql-repl\n\n        db> select datetime_get(\n        ...     <datetime>'2018-05-07T15:01:22.306916+00',\n        ...     'epochseconds');\n        {1525705282.306916}\n\n        db> select datetime_get(\n        ...     <datetime>'2018-05-07T15:01:22.306916+00',\n        ...     'year');\n        {2018}\n\n        db> select datetime_get(\n        ...     <datetime>'2018-05-07T15:01:22.306916+00',\n        ...     'quarter');\n        {2}\n\n        db> select datetime_get(\n        ...     <datetime>'2018-05-07T15:01:22.306916+00',\n        ...     'doy');\n        {127}\n\n        db> select datetime_get(\n        ...     <datetime>'2018-05-07T15:01:22.306916+00',\n        ...     'hour');\n        {15}\n\n\n----------\n\n\n.. eql:function:: cal::time_get(dt: cal::local_time, el: str) -> float64\n\n    Returns the element of a time value given a unit name.\n\n    You may pass any of these unit names for *el*:\n\n    - ``'midnightseconds'``\n    - ``'hour'``\n    - ``'microseconds'``\n    - ``'milliseconds'``\n    - ``'minutes'``\n    - ``'seconds'``\n\n    For full description of what these elements extract see\n    :eql:func:`datetime_get`.\n\n    .. code-block:: edgeql-repl\n\n        db> select cal::time_get(\n        ...     <cal::local_time>'15:01:22.306916', 'minutes');\n        {1}\n\n        db> select cal::time_get(\n        ...     <cal::local_time>'15:01:22.306916', 'milliseconds');\n        {22306.916}\n\n\n----------\n\n\n.. eql:function:: cal::date_get(dt: local_date, el: str) -> float64\n\n    Returns the element of a date given a unit name.\n\n    The :eql:type:`cal::local_date` scalar has the following elements\n    available for extraction:\n\n    - ``'century'`` - the century according to the Gregorian calendar\n    - ``'day'`` - the day of the month (1-31)\n    - ``'decade'`` - the decade (year divided by 10 and rounded down)\n    - ``'dow'`` - the day of the week from Sunday (0) to Saturday (6)\n    - ``'doy'`` - the day of the year (1-366)\n    - ``'isodow'`` - the ISO day of the week from Monday (1) to Sunday (7)\n    - ``'isoyear'`` - the ISO 8601 week-numbering year that the date falls in.\n      See the ``'week'`` element for more details.\n    - ``'millennium'`` - the millennium. The third millennium started\n      on Jan 1, 2001.\n    - ``'month'`` - the month of the year (1-12)\n    - ``'quarter'`` - the quarter of the year (1-4)\n      not including 60\n    - ``'week'`` - the number of the ISO 8601 week-numbering week of\n      the year. ISO weeks are defined to start on Mondays and the\n      first week of a year must contain Jan 4 of that year.\n    - ``'year'`` - the year\n\n    .. code-block:: edgeql-repl\n\n        db> select cal::date_get(\n        ...     <cal::local_date>'2018-05-07', 'century');\n        {21}\n\n        db> select cal::date_get(\n        ...     <cal::local_date>'2018-05-07', 'year');\n        {2018}\n\n        db> select cal::date_get(\n        ...     <cal::local_date>'2018-05-07', 'month');\n        {5}\n\n        db> select cal::date_get(\n        ...     <cal::local_date>'2018-05-07', 'doy');\n        {127}\n\n\n----------\n\n\n.. eql:function:: std::duration_get(dt: duration, el: str) -> float64\n                  std::duration_get(dt: cal::relative_duration, \\\n                                    el: str) -> float64\n                  std::duration_get(dt: cal::date_duration, \\\n                                    el: str) -> float64\n\n    Returns the element of a duration given a unit name.\n\n    You may pass any of these unit names as ``el``:\n\n    - ``'millennium'`` - number of 1000-year chunks rounded down\n    - ``'century'`` - number of centuries rounded down\n    - ``'decade'`` - number of decades rounded down\n    - ``'year'`` - number of years rounded down\n    - ``'quarter'``- remaining quarters after whole years are accounted for\n    - ``'month'`` - number of months left over after whole years are\n      accounted for\n    - ``'day'`` - number of days recorded in the duration\n    - ``'hour'`` - number of hours\n    - ``'minutes'`` - remaining minutes after whole hours are accounted for\n    - ``'seconds'`` - remaining seconds, including fractional value after whole\n      minutes are accounted for\n    - ``'milliseconds'`` - remaining seconds including fractional value\n      expressed as milliseconds\n    - ``'microseconds'`` - remaining seconds including fractional value\n      expressed as microseconds\n\n    .. note ::\n\n      Only for units ``'month'`` or larger or for units ``'hour'`` or smaller\n      will you receive a total across multiple units expressed in the original\n      duration. See *Gotchas* below for details.\n\n    Additionally, it's possible to convert a given duration into seconds:\n\n    - ``'totalseconds'`` - the number of seconds represented by the duration.\n      It will be approximate for :eql:type:`cal::relative_duration` and\n      :eql:type:`cal::date_duration` for units ``'month'`` or larger because a\n      month is assumed to be 30 days exactly.\n\n    The :eql:type:`duration` scalar has only ``'hour'`` and smaller units\n    available for extraction.\n\n    The :eql:type:`cal::relative_duration` scalar has all of the units\n    available for extraction.\n\n    The :eql:type:`cal::date_duration` scalar only has ``'date'`` and larger\n    units available for extraction.\n\n    .. code-block:: edgeql-repl\n\n        db> select duration_get(\n        ...   <cal::relative_duration>'400 months', 'year');\n        {33}\n        db> select duration_get(\n        ...   <cal::date_duration>'400 months', 'month');\n        {4}\n        db> select duration_get(\n        ...   <cal::relative_duration>'1 month 20 days 30 hours',\n        ...   'day');\n        {20}\n        db> select duration_get(\n        ...   <cal::relative_duration>'30 hours', 'hour');\n        {30}\n        db> select duration_get(\n        ...   <cal::relative_duration>'1 month 20 days 30 hours',\n        ...   'hour');\n        {30}\n        db> select duration_get(<duration>'30 hours', 'hour');\n        {30}\n        db> select duration_get(\n        ...   <cal::relative_duration>'1 month 20 days 30 hours',\n        ...   'totalseconds');\n        {4428000}\n        db> select duration_get(\n        ...   <duration>'30 hours', 'totalseconds');\n        {108000}\n\n    .. rubric:: Gotchas\n\n    This function will provide you with a calculated total for the unit passed\n    as ``el``, but only within the given \"size class\" of the unit. These size\n    classes exist because they are logical breakpoints that we can't reliably\n    convert values across. A month might be 30 days long, or it might be 28 or\n    29 or 31. A day is generally 24 hours, but with daylight savings, it might\n    be longer or shorter.\n\n    As a result, it's impossible to convert across these lines in a way that\n    works in every situation. For some use cases, assuming a 30 day month works\n    fine. For others, it might not. The size classes are as follows:\n\n    - ``'month'`` and larger\n    - ``'day'``\n    - ``'hour'`` and smaller\n\n    For example, if you specify ``'day'`` as your ``el`` argument, the function\n    will return only the number of days expressed as ``N days`` in your\n    duration. It will not add another day to the returned count for every 24\n    hours (defined as ``24 hours``) in the duration, nor will it consider the\n    months' constituent day counts in the returned value. Specifying\n    ``'decade'`` for ``el`` will total up all decades represented in units\n    ``'month'`` and larger, but it will not add a decade's worth of days to the\n    returned value as an additional decade.\n\n    In this example, the duration represents more than a day's time, but since\n    ``'day'`` and ``'hour'`` are in different size classes, the extra day\n    stemming from the duration's hours is not added.\n\n    .. code-block:: edgeql-repl\n\n        db> select duration_get(\n        ...   <cal::relative_duration>'1 day 36 hours', 'day');\n        {1}\n\n    In this counter example, both the decades and months are pooled together\n    since they are in the same size class. The return value is 5: the 2\n    ``'decades'`` and the 3 decades in ``'400 months'``.\n\n    .. code-block:: edgeql-repl\n\n        db> select duration_get(\n        ...   <cal::relative_duration>'2 decades 400 months', 'decade');\n        {5}\n\n    If a unit from a smaller size class would contribute to your desired unit's\n    total, it is not added.\n\n    .. code-block:: edgeql-repl\n\n        db> select duration_get(\n        ...   <cal::relative_duration>'1 year 400 days', 'year');\n        {1}\n\n    When you request a unit in the smallest size class, it will be pooled with\n    other durations in the same size class.\n\n    .. code-block:: edgeql-repl\n\n        db> select duration_get(\n        ...   <cal::relative_duration>'20 hours 3600 seconds', 'hour');\n        {21}\n\n    Seconds and smaller units always return remaining time in that unit after\n    accounting for the next larger unit.\n\n    .. code-block:: edgeql-repl\n\n        db> select duration_get(\n        ...   <cal::relative_duration>'20 hours 3600 seconds', 'seconds');\n        {0}\n        db> select duration_get(\n        ...   <cal::relative_duration>'20 hours 3630 seconds', 'seconds');\n        {30}\n\n    Normalization and truncation may help you deal with this. If your use case\n    allows for making assumptions about the duration of a month or a day, you\n    can make those conversions for yourself using the\n    :eql:func:`cal::duration_normalize_hours` or\n    :eql:func:`cal::duration_normalize_days` functions. If you got back a\n    duration as a result of a datetime calculation and don't need the level of\n    granularity you have, you can truncate the value with\n    :eql:func:`duration_truncate`.\n\n----------\n\n\n.. eql:function:: std::datetime_truncate(dt: datetime, unit: str) -> datetime\n\n    Truncates the input datetime to a particular precision.\n\n    The valid units in order or decreasing precision are:\n\n    - ``'microseconds'``\n    - ``'milliseconds'``\n    - ``'seconds'``\n    - ``'minutes'``\n    - ``'hours'``\n    - ``'days'``\n    - ``'weeks'``\n    - ``'months'``\n    - ``'quarters'``\n    - ``'years'``\n    - ``'decades'``\n    - ``'centuries'``\n\n    .. code-block:: edgeql-repl\n\n        db> select datetime_truncate(\n        ...   <datetime>'2018-05-07T15:01:22.306916+00', 'years');\n        {<datetime>'2018-01-01T00:00:00Z'}\n\n        db> select datetime_truncate(\n        ...   <datetime>'2018-05-07T15:01:22.306916+00', 'quarters');\n        {<datetime>'2018-04-01T00:00:00Z'}\n\n        db> select datetime_truncate(\n        ...   <datetime>'2018-05-07T15:01:22.306916+00', 'days');\n        {<datetime>'2018-05-07T00:00:00Z'}\n\n        db> select datetime_truncate(\n        ...   <datetime>'2018-05-07T15:01:22.306916+00', 'hours');\n        {<datetime>'2018-05-07T15:00:00Z'}\n\n\n----------\n\n\n.. eql:function:: std::duration_truncate(dt: duration, unit: str) -> duration\n                  std::duration_truncate(dt: cal::relative_duration, \\\n                    unit: str) -> cal::relative_duration\n\n    Truncates the input duration to a particular precision.\n\n    The valid units for :eql:type:`duration` are:\n\n    - ``'microseconds'``\n    - ``'milliseconds'``\n    - ``'seconds'``\n    - ``'minutes'``\n    - ``'hours'``\n\n    In addition to the above the following are also valid for\n    :eql:type:`cal::relative_duration`:\n\n    - ``'days'``\n    - ``'weeks'``\n    - ``'months'``\n    - ``'years'``\n    - ``'decades'``\n    - ``'centuries'``\n\n    .. code-block:: edgeql-repl\n\n        db> select duration_truncate(\n        ...   <duration>'15:01:22', 'hours');\n        {<duration>'15:00:00'}\n        db> select duration_truncate(\n        ...   <duration>'15:01:22.306916', 'minutes');\n        {<duration>'15:01:00'}\n        db> select duration_truncate(\n        ...   <cal::relative_duration>'400 months', 'years');\n        {<cal::relative_duration>'P33Y'}\n        db> select duration_truncate(\n        ...   <cal::relative_duration>'400 months', 'decades');\n        {<cal::relative_duration>'P30Y'}\n\n\n----------\n\n\n.. eql:function:: std::to_datetime(s: str, fmt: optional str={}) -> datetime\n                  std::to_datetime(local: cal::local_datetime, zone: str) \\\n                    -> datetime\n                  std::to_datetime(year: int64, month: int64, day: int64, \\\n                    hour: int64, min: int64, sec: float64, zone: str) \\\n                    -> datetime\n                  std::to_datetime(epochseconds: decimal) -> datetime\n                  std::to_datetime(epochseconds: float64) -> datetime\n                  std::to_datetime(epochseconds: int64) -> datetime\n\n    .. index:: parse datetime\n\n    Create a :eql:type:`datetime` value.\n\n    The :eql:type:`datetime` value can be parsed from the input\n    :eql:type:`str` *s*. By default, the input is expected to conform\n    to ISO 8601 format. However, the optional argument *fmt* can\n    be used to override the :ref:`input format\n    <ref_std_converters_datetime_fmt>` to other forms.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_datetime('2018-05-07T15:01:22.306916+00');\n        {<datetime>'2018-05-07T15:01:22.306916Z'}\n        db> select to_datetime('2018-05-07T15:01:22+00');\n        {<datetime>'2018-05-07T15:01:22Z'}\n        db> select to_datetime('May 7th, 2018 15:01:22 +00',\n        ...                    'Mon DDth, YYYY HH24:MI:SS TZH');\n        {<datetime>'2018-05-07T15:01:22Z'}\n\n    Alternatively, the :eql:type:`datetime` value can be constructed\n    from a :eql:type:`cal::local_datetime` value:\n\n    .. code-block:: edgeql-repl\n\n        db> select to_datetime(\n        ...   <cal::local_datetime>'2019-01-01T01:02:03', 'HKT');\n        {<datetime>'2018-12-31T17:02:03Z'}\n\n    Another way to construct a the :eql:type:`datetime` value is to specify it\n    in terms of its component parts: year, month, day, hour, min, sec, and\n    :ref:`zone <ref_std_datetime_timezones>`.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_datetime(\n        ...     2018, 5, 7, 15, 1, 22.306916, 'UTC');\n        {<datetime>'2018-05-07T15:01:22.306916000Z'}\n\n    Finally, it is also possible to convert a Unix timestamp to a\n    :eql:type:`datetime`\n\n    .. code-block:: edgeql-repl\n\n        db> select to_datetime(1590595184.584);\n        {<datetime>'2020-05-27T15:59:44.584000000Z'}\n\n------------\n\n\n.. eql:function:: cal::to_local_datetime(s: str, fmt: optional str={}) \\\n                    -> local_datetime\n                  cal::to_local_datetime(dt: datetime, zone: str) \\\n                    -> local_datetime\n                  cal::to_local_datetime(year: int64, month: int64, \\\n                    day: int64, hour: int64, min: int64, sec: float64) \\\n                    -> local_datetime\n\n    .. index:: parse local_datetime\n\n    Create a :eql:type:`cal::local_datetime` value.\n\n    Similar to :eql:func:`to_datetime`, the :eql:type:`cal::local_datetime`\n    value can be parsed from the input :eql:type:`str` *s* with an\n    optional *fmt* argument or it can be given in terms of its\n    component parts: *year*, *month*, *day*, *hour*, *min*, *sec*.\n\n    For more details on formatting see :ref:`here\n    <ref_std_converters_datetime_fmt>`.\n\n    .. code-block:: edgeql-repl\n\n        db> select cal::to_local_datetime('2018-05-07T15:01:22.306916');\n        {<cal::local_datetime>'2018-05-07T15:01:22.306916'}\n        db> select cal::to_local_datetime('May 7th, 2018 15:01:22',\n        ...                          'Mon DDth, YYYY HH24:MI:SS');\n        {<cal::local_datetime>'2018-05-07T15:01:22'}\n        db> select cal::to_local_datetime(\n        ...     2018, 5, 7, 15, 1, 22.306916);\n        {<cal::local_datetime>'2018-05-07T15:01:22.306916'}\n\n    A timezone-aware :eql:type:`datetime` type can be converted to local\n    datetime in the specified :ref:`timezone <ref_std_datetime_timezones>`:\n\n    .. code-block:: edgeql-repl\n\n        db> select cal::to_local_datetime(\n        ...   <datetime>'2018-12-31T22:00:00+08',\n        ...   'America/Chicago');\n        {<cal::local_datetime>'2018-12-31T08:00:00'}\n        db> select cal::to_local_datetime(\n        ...   <datetime>'2018-12-31T22:00:00+08',\n        ...   'CST');\n        {<cal::local_datetime>'2018-12-31T08:00:00'}\n\n\n------------\n\n\n.. eql:function:: cal::to_local_date(s: str, fmt: optional str={}) \\\n                    -> cal::local_date\n                  cal::to_local_date(dt: datetime, zone: str) \\\n                    -> cal::local_date\n                  cal::to_local_date(year: int64, month: int64, \\\n                    day: int64) -> cal::local_date\n\n    .. index:: parse local_date\n\n    Create a :eql:type:`cal::local_date` value.\n\n    Similar to :eql:func:`to_datetime`, the :eql:type:`cal::local_date`\n    value can be parsed from the input :eql:type:`str` *s* with an\n    optional *fmt* argument or it can be given in terms of its\n    component parts: *year*, *month*, *day*.\n\n    For more details on formatting see :ref:`here\n    <ref_std_converters_datetime_fmt>`.\n\n    .. code-block:: edgeql-repl\n\n        db> select cal::to_local_date('2018-05-07');\n        {<cal::local_date>'2018-05-07'}\n        db> select cal::to_local_date('May 7th, 2018', 'Mon DDth, YYYY');\n        {<cal::local_date>'2018-05-07'}\n        db> select cal::to_local_date(2018, 5, 7);\n        {<cal::local_date>'2018-05-07'}\n\n    A timezone-aware :eql:type:`datetime` type can be converted to local date\n    in the specified :ref:`timezone <ref_std_datetime_timezones>`:\n\n    .. code-block:: edgeql-repl\n\n        db> select cal::to_local_date(\n        ...   <datetime>'2018-12-31T22:00:00+08',\n        ...   'America/Chicago');\n        {<cal::local_date>'2019-01-01'}\n\n\n------------\n\n\n.. eql:function:: cal::to_local_time(s: str, fmt: optional str={}) \\\n                    -> local_time\n                  cal::to_local_time(dt: datetime, zone: str) \\\n                    -> local_time\n                  cal::to_local_time(hour: int64, min: int64, sec: float64) \\\n                    -> local_time\n\n    .. index:: parse local_time\n\n    Create a :eql:type:`cal::local_time` value.\n\n    Similar to :eql:func:`to_datetime`, the :eql:type:`cal::local_time`\n    value can be parsed from the input :eql:type:`str` *s* with an\n    optional *fmt* argument or it can be given in terms of its\n    component parts: *hour*, *min*, *sec*.\n\n    For more details on formatting see :ref:`here\n    <ref_std_converters_datetime_fmt>`.\n\n    .. code-block:: edgeql-repl\n\n        db> select cal::to_local_time('15:01:22.306916');\n        {<cal::local_time>'15:01:22.306916'}\n        db> select cal::to_local_time('03:01:22pm', 'HH:MI:SSam');\n        {<cal::local_time>'15:01:22'}\n        db> select cal::to_local_time(15, 1, 22.306916);\n        {<cal::local_time>'15:01:22.306916'}\n\n    A timezone-aware :eql:type:`datetime` type can be converted to local date\n    in the specified :ref:`timezone <ref_std_datetime_timezones>`:\n\n    .. code-block:: edgeql-repl\n\n        db> select cal::to_local_time(\n        ...   <datetime>'2018-12-31T22:00:00+08',\n        ...   'America/Los_Angeles');\n        {<cal::local_time>'06:00:00'}\n\n\n------------\n\n\n.. eql:function:: std::to_duration( \\\n                    named only hours: int64=0, \\\n                    named only minutes: int64=0, \\\n                    named only seconds: float64=0, \\\n                    named only microseconds: int64=0 \\\n                  ) -> duration\n\n    .. index:: parse duration\n\n    Create a :eql:type:`duration` value.\n\n    This function uses ``named only`` arguments to create a\n    :eql:type:`duration` value. The available duration fields are:\n    *hours*, *minutes*, *seconds*, *microseconds*.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_duration(hours := 1,\n        ...                    minutes := 20,\n        ...                    seconds := 45);\n        {4845s}\n        db> select to_duration(seconds := 4845);\n        {4845s}\n\n\n.. eql:function:: std::duration_to_seconds(cur: duration) -> decimal\n\n    Return duration as total number of seconds in interval.\n\n    .. code-block:: edgeql-repl\n\n        db> select duration_to_seconds(<duration>'1 hour');\n        {3600.000000n}\n        db> select duration_to_seconds(<duration>'10 second 123 ms');\n        {10.123000n}\n\n\n------------\n\n\n.. eql:function:: cal::to_relative_duration( \\\n                    named only years: int64=0, \\\n                    named only months: int64=0, \\\n                    named only days: int64=0, \\\n                    named only hours: int64=0, \\\n                    named only minutes: int64=0, \\\n                    named only seconds: float64=0, \\\n                    named only microseconds: int64=0 \\\n                  ) -> cal::relative_duration\n\n    .. index:: parse relative_duration\n\n    Create a :eql:type:`cal::relative_duration` value.\n\n    This function uses ``named only`` arguments to create a\n    :eql:type:`cal::relative_duration` value. The available duration fields\n    are: *years*, *months*, *days*, *hours*, *minutes*, *seconds*,\n    *microseconds*.\n\n    .. code-block:: edgeql-repl\n\n        db> select cal::to_relative_duration(years := 5, minutes := 1);\n        {<cal::relative_duration>'P5YT1S'}\n        db> select cal::to_relative_duration(months := 3, days := 27);\n        {<cal::relative_duration>'P3M27D'}\n\n\n------------\n\n\n.. eql:function:: cal::to_date_duration( \\\n                    named only years: int64=0, \\\n                    named only months: int64=0, \\\n                    named only days: int64=0 \\\n                  ) -> cal::date_duration\n\n    .. index:: parse date_duration\n\n    Create a :eql:type:`cal::date_duration` value.\n\n    This function uses ``named only`` arguments to create a\n    :eql:type:`cal::date_duration` value. The available duration fields\n    are: *years*, *months*, *days*.\n\n    .. code-block:: edgeql-repl\n\n        db> select cal::to_date_duration(years := 1, days := 3);\n        {<cal::date_duration>'P1Y3D'}\n        db> select cal::to_date_duration(days := 12);\n        {<cal::date_duration>'P12D'}\n\n\n------------\n\n\n.. eql:function:: cal::duration_normalize_hours( \\\n                    dur: cal::relative_duration \\\n                  ) -> cal::relative_duration\n\n    .. index:: justify_hours\n\n    Convert 24-hour chunks into days.\n\n    This function converts all 24-hour chunks into day units. The resulting\n    :eql:type:`cal::relative_duration` is guaranteed to have less than 24\n    hours in total in the units smaler than days.\n\n    .. code-block:: edgeql-repl\n\n        db> select cal::duration_normalize_hours(\n        ...   <cal::relative_duration>'1312 hours');\n        {<cal::relative_duration>'P54DT16H'}\n\n    This is a lossless operation because 24 hours are always equal to 1 day\n    in :eql:type:`cal::relative_duration` units.\n\n    This is sometimes used together with\n    :eql:func:`cal::duration_normalize_days`.\n\n------------\n\n\n.. eql:function:: cal::duration_normalize_days( \\\n                    dur: cal::relative_duration \\\n                  ) -> cal::relative_duration\n                  cal::duration_normalize_days( \\\n                    dur: cal::date_duration \\\n                  ) -> cal::date_duration\n\n    .. index:: justify_days\n\n    Convert 30-day chunks into months.\n\n    This function converts all 30-day chunks into month units. The resulting\n    :eql:type:`cal::relative_duration` or :eql:type:`cal::date_duration` is\n    guaranteed to have less than 30 day units.\n\n    .. code-block:: edgeql-repl\n\n        db> select cal::duration_normalize_days(\n        ...   <cal::relative_duration>'1312 days');\n        {<cal::relative_duration>'P3Y7M22D'}\n\n        db> select cal::duration_normalize_days(\n        ...   <cal::date_duration>'1312 days');\n        {<cal::date_duration>'P3Y7M22D'}\n\n    This function is a form of approximation and does not preserve the exact\n    duration.\n\n    This is often used together with\n    :eql:func:`cal::duration_normalize_hours`.\n"
  },
  {
    "path": "docs/reference/stdlib/deprecated.rst",
    "content": ".. _ref_std_deprecated:\n\n==========\nDeprecated\n==========\n\n:edb-alt-title: Deprecated Functions\n\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`str_lpad`\n      - :eql:func-desc:`str_lpad`\n\n    * - :eql:func:`str_rpad`\n      - :eql:func-desc:`str_rpad`\n\n    * - :eql:func:`str_ltrim`\n      - :eql:func-desc:`str_ltrim`\n\n    * - :eql:func:`str_rtrim`\n      - :eql:func-desc:`str_rtrim`\n\n\n----------\n\n\n.. eql:function:: std::str_lpad(string: str, n: int64, fill: str = ' ') -> str\n\n    Return the input *string* left-padded to the length *n*.\n\n    .. warning::\n\n        This function is deprecated. Use\n        :eql:func:`std::str_pad_start` instead.\n\n    If the *string* is longer than *n*, then it is truncated to the\n    first *n* characters. Otherwise, the *string* is padded on the\n    left up to the total length *n* using *fill* characters (space by\n    default).\n\n    .. code-block:: edgeql-repl\n\n        db> select str_lpad('short', 10);\n        {'     short'}\n        db> select str_lpad('much too long', 10);\n        {'much too l'}\n        db> select str_lpad('short', 10, '.:');\n        {'.:.:.short'}\n\n\n----------\n\n\n.. eql:function:: std::str_rpad(string: str, n: int64, fill: str = ' ') -> str\n\n    Return the input *string* right-padded to the length *n*.\n\n    .. warning::\n\n        This function is deprecated. Use\n        :eql:func:`std::str_pad_end` instead.\n\n    If the *string* is longer than *n*, then it is truncated to the\n    first *n* characters. Otherwise, the *string* is padded on the\n    right up to the total length *n* using *fill* characters (space by\n    default).\n\n    .. code-block:: edgeql-repl\n\n        db> select str_rpad('short', 10);\n        {'short     '}\n        db> select str_rpad('much too long', 10);\n        {'much too l'}\n        db> select str_rpad('short', 10, '.:');\n        {'short.:.:.'}\n\n\n----------\n\n\n.. eql:function:: std::str_ltrim(string: str, trim: str = ' ') -> str\n\n    Return the input string with all leftmost *trim* characters removed.\n\n    .. warning::\n\n        This function is deprecated. Use\n        :eql:func:`std::str_trim_start` instead.\n\n    If the *trim* specifies more than one character they will be\n    removed from the beginning of the *string* regardless of the order\n    in which they appear.\n\n    .. code-block:: edgeql-repl\n\n        db> select str_ltrim('     data');\n        {'data'}\n        db> select str_ltrim('.....data', '.:');\n        {'data'}\n        db> select str_ltrim(':::::data', '.:');\n        {'data'}\n        db> select str_ltrim(':...:data', '.:');\n        {'data'}\n        db> select str_ltrim('.:.:.data', '.:');\n        {'data'}\n\n\n----------\n\n\n.. eql:function:: std::str_rtrim(string: str, trim: str = ' ') -> str\n\n    Return the input string with all rightmost *trim* characters removed.\n\n    .. warning::\n\n        This function is deprecated. Use\n        :eql:func:`std::str_trim_end` instead.\n\n    If the *trim* specifies more than one character they will be\n    removed from the end of the *string* regardless of the order\n    in which they appear.\n\n    .. code-block:: edgeql-repl\n\n        db> select str_rtrim('data     ');\n        {'data'}\n        db> select str_rtrim('data.....', '.:');\n        {'data'}\n        db> select str_rtrim('data:::::', '.:');\n        {'data'}\n        db> select str_rtrim('data:...:', '.:');\n        {'data'}\n        db> select str_rtrim('data.:.:.', '.:');\n        {'data'}\n\n----------\n\n.. eql:type:: cfg::DatabaseConfig\n\n  The branch-level configuration object type.\n\n  As of |EdgeDB| 5.0, this config object represents database *branch*\n  and instance-level configuration.\n\n  **Use the identical** :eql:type:`cfg::BranchConfig` instead.\n"
  },
  {
    "path": "docs/reference/stdlib/enum.rst",
    "content": ".. _ref_std_enum:\n\n=====\nEnums\n=====\n\n:edb-alt-title: Enum Type\n\n\n.. eql:type:: std::enum\n\n    An enumerated type is a data type consisting of an ordered list of values.\n\n    An enum type can be declared in a schema by using the following\n    syntax:\n\n    .. code-block:: sdl\n\n        scalar type Color extending enum<Red, Green, Blue>;\n\n    Enum values can then be accessed directly:\n\n    .. code-block:: edgeql-repl\n\n        db> select Color.Red is Color;\n        {true}\n\n    :eql:op:`Casting <cast>` can be used to obtain an\n    enum value in an expression:\n\n    .. code-block:: edgeql-repl\n\n        db> select 'Red' is Color;\n        {false}\n        db> select <Color>'Red' is Color;\n        {true}\n        db> select <Color>'Red' = Color.Red;\n        {true}\n\n    .. note::\n\n        The enum values in EdgeQL are string-like in the fact that\n        they can contain any characters that the strings can. This is\n        different from some other languages where enum values are\n        identifier-like and thus cannot contain some characters. For\n        example, when working with GraphQL enum values that contain\n        characters that aren't allowed in identifiers cannot be\n        properly reflected. To address this, consider using only\n        identifier-like enum values in cases where such compatibility\n        is needed.\n\n        Currently, enum values cannot be longer than 63 characters.\n"
  },
  {
    "path": "docs/reference/stdlib/fts.rst",
    "content": ".. _ref_std_fts:\n\n.. versionadded:: 4.0\n\n================\nFull-text Search\n================\n\nThe ``fts`` built-in module contains various tools that enable full-text\nsearch functionality in Gel.\n\n.. note::\n\n    Since full-text search is a natural language search, it may not be ideal\n    for your use case, particularly if you want to find partial matches. In\n    that case, you may want to look instead at :ref:`ref_ext_pgtrgm`.\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:type:`fts::Language`\n      - Common languages :eql:type:`enum`\n\n    * - :eql:type:`fts::PGLanguage`\n      - Postgres languages :eql:type:`enum`\n\n    * - :eql:type:`fts::Weight`\n      - Weight category :eql:type:`enum`\n\n    * - :eql:type:`fts::document`\n      - Opaque document type\n\n    * - :eql:func:`fts::search`\n      - :eql:func-desc:`fts::search`\n\n    * - :eql:func:`fts::with_options`\n      - :eql:func-desc:`fts::with_options`\n\nWhen considering FTS functionality our goal was to come up with an interface\nthat could support different backend FTS providers. To achieve that we've\nidentified the following components to the FTS functionality:\n\n1) Valid FTS targets must be indexed.\n2) The expected language should be specified at the time of creating an index.\n3) It should be possible to mark document parts as having different relevance.\n4) It should be possible to assign custom weights at runtime so as to make\n   searching more flexible.\n5) The search query should be close to what people are already used to.\n\nTo address (1) we introduce a special ``fts::index``. The presence of this\nindex in a type declaration indicates that the type in question can be subject\nto full-text search. This is an unusual index as it actually affects the\nresults of :eql:func:`fts::search` function. This is unlike most indexes which\nonly affect the performance and not the actual results. Another special\nfeature of ``fts::index`` is that at most one such index can be declared per\ntype. If a type inherits this index from a parent and also declares its own,\nonly the new index applies and fully overrides the ``fts::index`` inherited\nfrom the parent type. This means that when dealing with a hierarchy of\nfull-text-searchable types, each type can customize what gets searched as\nneeded.\n\nThe language (2) is defined as part of the ``fts::index on`` expression. A\nspecial function :eql:func:`fts::with_options` is used for that purpose:\n\n.. code-block:: sdl\n\n    type Item {\n      required available: bool {\n        default := false;\n      };\n      required name: str;\n      description: str;\n\n      index fts::index on (\n        fts::with_options(\n          .name,\n          language := fts::Language.eng\n        )\n      );\n    }\n\nThe above declaration specifies that ``Item`` is full-text-searchable,\nspecifically by examining the ``name`` property (and ignoring ``description``)\nand assuming that the contents of that property are in English.\n\nMarking different parts of the document as having different relevance (3) can\nalso be done by the :eql:func:`fts::with_options` function:\n\n.. code-block:: sdl\n\n    type Item {\n      required available: bool {\n        default := false;\n      };\n      required name: str;\n      description: str;\n\n      index fts::index on ((\n        fts::with_options(\n          .name,\n          language := fts::Language.eng,\n          weight_category := fts::Weight.A,\n        ),\n        fts::with_options(\n          .description,\n          language := fts::Language.eng,\n          weight_category := fts::Weight.B,\n        )\n      ));\n    }\n\nThe schema now indicates that both ``name`` and ``description`` properties of\n``Item`` are full-text-searchable. Additionally, the ``name`` and\n``description`` have potentially different relevance.\n\nBy default :eql:func:`fts::search` assumes that the weight categories ``A``,\n``B``, ``C``, and ``D`` have the following weights: ``[1, 0.5, 0.25, 0.125]``.\nThis makes each successive category relevance score halved.\n\nConsider the following:\n\n.. code-block:: edgeql-repl\n\n    gel> select Item{name, description};\n    {\n      default::Item {name: 'Canned corn', description: {}},\n      default::Item {\n        name: 'Candy corn',\n        description: 'A great Halloween treat',\n      },\n      default::Item {\n        name: 'Sweet',\n        description: 'Treat made with corn sugar',\n      },\n    }\n\n    gel> with res := (\n    ....   select fts::search(Item, 'corn treat', language := 'eng')\n    .... )\n    .... select res.object {name, description, score := res.score}\n    .... order by res.score desc;\n    {\n      default::Item {\n        name: 'Candy corn',\n        description: 'A great Halloween treat',\n        score: 0.4559453,\n      },\n      default::Item {\n        name: 'Canned corn',\n        description: {},\n        score: 0.30396354,\n      },\n      default::Item {\n        name: 'Sweet',\n        description: 'Treat made with corn sugar',\n        score: 0.30396354,\n      },\n    }\n\nAs you can see, the highest scoring match came from an ``Item`` that had the\nsearch terms appear in both ``name`` and ``description``. It is also apparent\nthat matching a single term from the search query in the ``name`` property\nscores the same as matching two terms in ``description`` as we would expect\nbased on their weight categories. We can, however, customize the weights (4)\nto change this trend:\n\n.. code-block:: edgeql-repl\n\n    gel> with res := (\n    ....   select fts::search(\n    ....     Item, 'corn treat',\n    ....     language := 'eng',\n    ....     weights := [0.2, 1],\n    ....   )\n    .... )\n    .... select res.object {name, description, score := res.score}\n    .... order by res.score desc;\n    {\n      default::Item {\n        name: 'Sweet',\n        description: 'Treat made with corn sugar',\n        score: 0.6079271,\n      },\n      default::Item {\n        name: 'Candy corn',\n        description: 'A great Halloween treat',\n        score: 0.36475626,\n      },\n      default::Item {\n        name: 'Canned corn',\n        description: {},\n        score: 0.06079271,\n      },\n    }\n\nWe can even use custom weights to completely ignore one of the properties\n(e.g. ``name``) in our search, although we also need to add a filter based on\nthe score to make this work properly:\n\n.. code-block:: edgeql-repl\n\n    gel> with res := (\n    ....   select fts::search(\n    ....     Item, 'corn treat',\n    ....     language := 'eng',\n    ....     weights := [0, 1],\n    ....   )\n    .... )\n    .... select res.object {name, description, score := res.score}\n    .... filter res.score > 0\n    .... order by res.score desc;\n    {\n      default::Item {\n        name: 'Sweet',\n        description: 'Treat made with corn sugar',\n        score: 0.6079271,\n      },\n      default::Item {\n        name: 'Candy corn',\n        description: 'A great Halloween treat',\n        score: 0.30396354,\n      },\n    }\n\nFinally, the search query supports features for fine-tuning (5). By default,\nall search terms are desirable, but ultimately optional. You can enclose a\nterm or even a phrase in ``\"...\"`` to indicate that this specific term is of\nincreased importance and should appear in all matches:\n\n.. code-block:: edgeql-repl\n\n    gel> with res := (\n    ....   select fts::search(\n    ....     Item, '\"corn sugar\"',\n    ....     language := 'eng',\n    ....   )\n    .... )\n    .... select res.object {name, description, score := res.score}\n    .... order by res.score desc;\n    {\n      default::Item {\n        name: 'Sweet',\n        description: 'Treat made with corn sugar',\n        score: 0.4955161,\n      },\n    }\n\nOnly one ``Item`` contains the phrase \"corn sugar\" and incomplete matches are\nomitted.\n\nThe search query can also use ``AND`` (using upper-case to indicate that it is\na query modifier and not part of the query) to indicate whether terms are\nrequired or optional:\n\n.. code-block:: edgeql-repl\n\n    gel> with res := (\n    ....   select fts::search(\n    ....     Item, 'sweet AND treat',\n    ....     language := 'eng',\n    ....   )\n    .... )\n    .... select res.object {name, description, score := res.score}\n    .... order by res.score desc;\n    {\n      default::Item {\n        name: 'Sweet',\n        description: 'Treat made with corn sugar',\n        score: 0.70076555,\n      },\n    }\n\nAdding a ``!`` in front of a search term marks it as something that\nthe matching object *must not* contain:\n\n.. code-block:: edgeql-repl\n\n    gel> with res := (\n    ....   select fts::search(\n    ....     Item, '!treat',\n    ....     language := 'eng',\n    ....   )\n    .... )\n    .... select res.object {name, description, score := res.score}\n    .... order by res.score desc;\n    {\n      default::Item {\n        name: 'Canned corn',\n        description: {},\n        score: 0,\n      },\n    }\n\n----------\n\n\n.. eql:type:: fts::Language\n\n    An :eql:type:`enum` for representing commonly supported languages.\n\n    When indexing an object for full-text search it is important to specify\n    the expected language by :eql:func:`fts::with_options` function. This\n    particular :eql:type:`enum` represents languages that are common across\n    several possible [future] backend implementations and thus are \"safe\" even\n    if the backend implementation switches from one of the options to another.\n    This generic enum is the recommended way of specifying the language.\n\n    The following `ISO 639-3 <iso639_>`_ language identifiers are available:\n    ``ara``, ``hye``, ``eus``, ``cat``, ``dan``, ``nld``, ``eng``, ``fin``,\n    ``fra``, ``deu``, ``ell``, ``hin``, ``hun``, ``ind``, ``gle``, ``ita``,\n    ``nor``, ``por``, ``ron``, ``rus``, ``spa``, ``swe``, ``tur``.\n\n----------\n\n\n.. eql:type:: fts::PGLanguage\n\n    An :eql:type:`enum` for representing languages supported by PostgreSQL.\n\n    When indexing an object for full-text search it is important to specify\n    the expected language by :eql:func:`fts::with_options` function. This\n    particular :eql:type:`enum` represents languages that are available in\n    PostgreSQL implementation of full-text search.\n\n    The following `ISO 639-3 <iso639_>`_ language identifiers are available:\n    ``ara``, ``hye``, ``eus``, ``cat``, ``dan``, ``nld``, ``eng``, ``fin``,\n    ``fra``, ``deu``, ``ell``, ``hin``, ``hun``, ``ind``, ``gle``, ``ita``,\n    ``lit``, ``npi``, ``nor``, ``por``, ``ron``, ``rus``, ``srp``, ``spa``,\n    ``swe``, ``tam``, ``tur``, ``yid``.\n\n    Additionally, the ``xxx_simple`` identifier is also available to represent\n    the ``pg_catalog.simple`` language setting.\n\n    Unless you need some particular language setting that is not available in\n    the :eql:type:`fts::Language`, it is recommended that you use the more\n    general lanuguage enum instead.\n\n\n----------\n\n\n.. eql:type:: fts::Weight\n\n    An :eql:type:`enum` for representing weight categories.\n\n    When indexing an object for full-text search different properties of this\n    object may have different significance. To account for that, they can be\n    assigned different weight categories by using\n    :eql:func:`fts::with_options` function. There are four available weight\n    categories: ``A``, ``B``, ``C``, or ``D``.\n\n\n----------\n\n\n.. eql:type:: fts::document\n\n    An opaque transient type used in ``fts::index``.\n\n    This type is technically what the ``fts::index`` expects as a valid ``on``\n    expression. It cannot be directly instantiated and can only be produced as\n    the result of applying the special :eql:func:`fts::with_options` function.\n    Thus this type only appears in full-text search index definitions and\n    cannot appear as either a property type or anywhere in regular queries.\n\n\n------------\n\n\n.. eql:function:: fts::search( \\\n                    object: anyobject, \\\n                    query: str, \\\n                    named only language: str = <str>fts::Language.eng, \\\n                    named only weights: optional array<float64> = {}, \\\n                  ) -> optional tuple<object: anyobject, score: float32>\n\n    Perform full-text search on a target object.\n\n    This function applies the search ``query`` to the specified object. If a\n    match is found, the result will consist of a tuple with the matched\n    ``object`` and the corresponding ``score``. A higher ``score`` indicates a\n    better match. In case no match is found, the function will return an empty\n    set ``{}``. Likewise, ``{}`` is returned if the ``object`` has no\n    ``fts::index`` defined for it.\n\n    The ``language`` parameter can be specified in order to match the expected\n    indexed language. In case of mismatch there is a big chance that the query\n    will not produce the expected results.\n\n    The optional ``weights`` parameter can be passed in order to provide\n    custom weights to the different weight groups. By default, the weights are\n    ``[1, 0.5, 0.25, 0.125]`` representing groups of diminishing significance.\n\n\n------------\n\n\n.. eql:function:: fts::with_options( \\\n                    text: str, \\\n                    NAMED ONLY language: anyenum, \\\n                    NAMED ONLY weight_category: optional fts::Weight = \\\n                    fts::Weight.A, \\\n                  ) -> fts::document\n\n    Assign language and weight category to a document portion.\n\n    This is a special function that can only appear inside ``fts::index``\n    expressions.\n\n    The ``text`` expression specifies the portion of the document that will be\n    indexed and available for full-text search.\n\n    The ``language`` parameter specifies the expected language of the ``text``\n    expression. This affects how the index accounts for grammatical variants\n    of a given word (e.g. how plural and singular forms are determined, etc.).\n\n    The ``weight_category`` parameter assigns one of four available weight\n    categories to the ``text`` expression: ``A``, ``B``, ``C``, or ``D``. By\n    themselves, the categories simply group together portions of the document\n    so that these groups can be ascribed different significance by the\n    :eql:func:`fts::search` function. By default it is assumed that each\n    successive category is half as significant as the previous, starting with\n    ``A`` as the most significant. However, these default weights can be\n    overridden when making a call to :eql:func:`fts::search`.\n\n\n.. _iso639: https://iso639-3.sil.org/code_tables/639/data\n"
  },
  {
    "path": "docs/reference/stdlib/generic.rst",
    "content": ".. _ref_std_generic:\n\n=======\nGeneric\n=======\n\n:edb-alt-title: Generic Functions and Operators\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:op:`anytype = anytype <eq>`\n      - :eql:op-desc:`eq`\n\n    * - :eql:op:`anytype != anytype <neq>`\n      - :eql:op-desc:`neq`\n\n    * - :eql:op:`anytype ?= anytype <coaleq>`\n      - :eql:op-desc:`coaleq`\n\n    * - :eql:op:`anytype ?!= anytype <coalneq>`\n      - :eql:op-desc:`coalneq`\n\n    * - :eql:op:`anytype \\< anytype <lt>`\n      - :eql:op-desc:`lt`\n\n    * - :eql:op:`anytype \\> anytype <gt>`\n      - :eql:op-desc:`gt`\n\n    * - :eql:op:`anytype \\<= anytype <lteq>`\n      - :eql:op-desc:`lteq`\n\n    * - :eql:op:`anytype \\>= anytype <gteq>`\n      - :eql:op-desc:`gteq`\n\n    * - :eql:func:`len`\n      - :eql:func-desc:`len`\n\n    * - :eql:func:`contains`\n      - :eql:func-desc:`contains`\n\n    * - :eql:func:`find`\n      - :eql:func-desc:`find`\n\n.. note::\n\n    In EdgeQL, any value can be compared to another as long as their types\n    are compatible.\n\n\n-----------\n\n\n.. eql:operator:: eq: anytype = anytype -> bool\n\n    .. index:: comparison\n    .. api-index:: =\n\n    Compares two values for equality.\n\n    .. code-block:: edgeql-repl\n\n        db> select 3 = 3.0;\n        {true}\n        db> select 3 = 3.14;\n        {false}\n        db> select [1, 2] = [1, 2];\n        {true}\n        db> select (1, 2) = (x := 1, y := 2);\n        {true}\n        db> select (x := 1, y := 2) = (a := 1, b := 2);\n        {true}\n        db> select 'hello' = 'world';\n        {false}\n\n    .. warning::\n\n        When either operand in an equality comparison is an empty set, the\n        result will not be a ``bool`` but instead an empty set.\n\n        .. code-block:: edgeql-repl\n\n            db> select true = <bool>{};\n            {}\n\n        If one of the operands in an equality comparison could be an empty set,\n        you may want to use the :eql:op:`coalescing equality <coaleq>` operator\n        (``?=``) instead.\n\n----------\n\n\n.. eql:operator:: neq: anytype != anytype -> bool\n\n    .. index:: not equal, comparison\n    .. api-index:: !=\n\n    Compares two values for inequality.\n\n    .. code-block:: edgeql-repl\n\n\n        db> select 3 != 3.0;\n        {false}\n        db> select 3 != 3.14;\n        {true}\n        db> select [1, 2] != [2, 1];\n        {false}\n        db> select (1, 2) != (x := 1, y := 2);\n        {false}\n        db> select (x := 1, y := 2) != (a := 1, b := 2);\n        {false}\n        db> select 'hello' != 'world';\n        {true}\n\n    .. warning::\n\n        When either operand in an inequality comparison is an empty set, the\n        result will not be a ``bool`` but instead an empty set.\n\n        .. code-block:: edgeql-repl\n\n            db> select true != <bool>{};\n            {}\n\n        If one of the operands in an inequality comparison could be an empty\n        set, you may want to use the :eql:op:`coalescing inequality <coaleq>`\n        operator (``?!=``) instead.\n\n\n----------\n\n\n.. eql:operator:: coaleq: optional anytype ?= optional anytype -> bool\n\n    .. index:: coalesce equal, comparison, empty set\n    .. api-index:: ?=\n\n    Compares two (potentially empty) values for equality.\n\n    This works the same as a regular :eql:op:`=<eq>` operator, but also allows\n    comparing an empty ``{}`` set.  Two empty sets are considered equal.\n\n    .. code-block:: edgeql-repl\n\n        db> select {1} ?= {1.0};\n        {true}\n        db> select {1} ?= <int64>{};\n        {false}\n        db> select <int64>{} ?= <int64>{};\n        {true}\n\n\n----------\n\n\n.. eql:operator:: coalneq: optional anytype ?!= optional anytype -> bool\n\n    .. index:: coalesce not equal, comparison\n    .. api-index:: ?!=\n\n    Compares two (potentially empty) values for inequality.\n\n    This works the same as a regular :eql:op:`=<eq>` operator, but also allows\n    comparing an empty ``{}`` set.  Two empty sets are considered equal.\n\n    .. code-block:: edgeql-repl\n\n        db> select {2} ?!= {2};\n        {false}\n        db> select {1} ?!= <int64>{};\n        {true}\n        db> select <bool>{} ?!= <bool>{};\n        {false}\n\n\n----------\n\n\n.. eql:operator:: lt: anytype < anytype -> bool\n\n    .. index:: comparison\n    .. api-index:: <\n\n    Less than operator.\n\n    The operator returns ``true`` if the value of the left expression is less\n    than the value of the right expression:\n\n    .. code-block:: edgeql-repl\n\n        db> select 1 < 2;\n        {true}\n        db> select 2 < 2;\n        {false}\n        db> select 'hello' < 'world';\n        {true}\n        db> select (1, 'hello') < (1, 'world');\n        {true}\n\n    .. warning::\n\n        When either operand in a comparison is an empty set, the result will\n        not be a ``bool`` but instead an empty set.\n\n        .. code-block:: edgeql-repl\n\n            db> select 1 < <int16>{};\n            {}\n\n        If one of the operands in a comparison could be an empty set, you may\n        want to coalesce the result of the comparison with ``false`` to ensure\n        your result is boolean.\n\n        .. code-block:: edgeql-repl\n\n            db> select (1 < <int16>{}) ?? false;\n            {false}\n\n\n----------\n\n\n.. eql:operator:: gt: anytype > anytype -> bool\n\n    .. index:: comparison\n    .. api-index:: >\n\n    Greater than operator.\n\n    The operator returns ``true`` if the value of the left expression is\n    greater than the value of the right expression:\n\n    .. code-block:: edgeql-repl\n\n        db> select 1 > 2;\n        {false}\n        db> select 3 > 2;\n        {true}\n        db> select 'hello' > 'world';\n        {false}\n        db> select (1, 'hello') > (1, 'world');\n        {false}\n\n    .. warning::\n\n        When either operand in a comparison is an empty set, the result will\n        not be a ``bool`` but instead an empty set.\n\n        .. code-block:: edgeql-repl\n\n            db> select 1 > <int16>{};\n            {}\n\n        If one of the operands in a comparison could be an empty set, you may\n        want to coalesce the result of the comparison with ``false`` to ensure\n        your result is boolean.\n\n        .. code-block:: edgeql-repl\n\n            db> select (1 > <int16>{}) ?? false;\n            {false}\n\n\n----------\n\n\n.. eql:operator:: lteq: anytype <= anytype -> bool\n\n    .. index:: comparison\n    .. api-index:: <=\n\n    Less or equal operator.\n\n    The operator returns ``true`` if the value of the left expression is less\n    than or equal to the value of the right expression:\n\n    .. code-block:: edgeql-repl\n\n        db> select 1 <= 2;\n        {true}\n        db> select 2 <= 2;\n        {true}\n        db> select 3 <= 2;\n        {false}\n        db> select 'hello' <= 'world';\n        {true}\n        db> select (1, 'hello') <= (1, 'world');\n        {true}\n\n    .. warning::\n\n        When either operand in a comparison is an empty set, the result will\n        not be a ``bool`` but instead an empty set.\n\n        .. code-block:: edgeql-repl\n\n            db> select 1 <= <int16>{};\n            {}\n\n        If one of the operands in a comparison could be an empty set, you may\n        want to coalesce the result of the comparison with ``false`` to ensure\n        your result is boolean.\n\n        .. code-block:: edgeql-repl\n\n            db> select (1 <= <int16>{}) ?? false;\n            {false}\n\n\n----------\n\n\n.. eql:operator:: gteq: anytype >= anytype -> bool\n\n    .. index:: comparison\n    .. api-index:: >=\n\n    Greater or equal operator.\n\n    The operator returns ``true`` if the value of the left expression is\n    greater than or equal to the value of the right expression:\n\n    .. code-block:: edgeql-repl\n\n        db> select 1 >= 2;\n        {false}\n        db> select 2 >= 2;\n        {true}\n        db> select 3 >= 2;\n        {true}\n        db> select 'hello' >= 'world';\n        {false}\n        db> select (1, 'hello') >= (1, 'world');\n        {false}\n\n    .. warning::\n\n        When either operand in a comparison is an empty set, the result will\n        not be a ``bool`` but instead an empty set.\n\n        .. code-block:: edgeql-repl\n\n            db> select 1 >= <int16>{};\n            {}\n\n        If one of the operands in a comparison could be an empty set, you may\n        want to coalesce the result of the comparison with ``false`` to ensure\n        your result is boolean.\n\n        .. code-block:: edgeql-repl\n\n            db> select (1 >= <int16>{}) ?? false;\n            {false}\n\n\n----------\n\n\n.. eql:function:: std::len(value: str) -> int64\n                  std::len(value: bytes) -> int64\n                  std::len(value: array<anytype>) -> int64\n\n    .. index:: length, count\n\n    Returns the number of elements of a given value.\n\n    This function works with the :eql:type:`str`, :eql:type:`bytes` and\n    :eql:type:`array` types:\n\n    .. code-block:: edgeql-repl\n\n        db> select len('foo');\n        {3}\n\n        db> select len(b'bar');\n        {3}\n\n        db> select len([2, 5, 7]);\n        {3}\n\n\n----------\n\n\n.. eql:function:: std::contains(haystack: str, needle: str) -> bool\n                  std::contains(haystack: bytes, needle: bytes) -> bool\n                  std::contains(haystack: array<anytype>, needle: anytype) \\\n                  -> bool\n                  std::contains(haystack: range<anypoint>, \\\n                                needle: range<anypoint>) \\\n                  -> std::bool\n                  std::contains(haystack: range<anypoint>, \\\n                                needle: anypoint) \\\n                  -> std::bool\n                  std::contains(haystack: multirange<anypoint>, \\\n                                needle: multirange<anypoint>) \\\n                  -> std::bool\n                  std::contains(haystack: multirange<anypoint>, \\\n                                needle: range<anypoint>) \\\n                  -> std::bool\n                  std::contains(haystack: multirange<anypoint>, \\\n                                needle: anypoint) \\\n                  -> std::bool\n\n    .. index:: find, strpos, includes\n\n    Returns true if the given sub-value exists within the given value.\n\n    When *haystack* is a :eql:type:`str` or a :eql:type:`bytes` value,\n    this function will return ``true`` if it contains *needle* as a\n    subsequence within it or ``false`` otherwise:\n\n    .. code-block:: edgeql-repl\n\n        db> select contains('qwerty', 'we');\n        {true}\n\n        db> select contains(b'qwerty', b'42');\n        {false}\n\n    When *haystack* is an :eql:type:`array`, the function will return\n    ``true`` if the array contains the element specified as *needle* or\n    ``false`` otherwise:\n\n    .. code-block:: edgeql-repl\n\n        db> select contains([2, 5, 7, 2, 100], 2);\n        {true}\n\n    When *haystack* is a :ref:`range <ref_std_range>`, the function will\n    return ``true`` if it contains either the specified sub-range or element.\n    The function will return ``false`` otherwise.\n\n    .. code-block:: edgeql-repl\n\n        db> select contains(range(1, 10), range(2, 5));\n        {true}\n\n        db> select contains(range(1, 10), range(2, 15));\n        {false}\n\n        db> select contains(range(1, 10), 2);\n        {true}\n\n        db> select contains(range(1, 10), 10);\n        {false}\n\n    When *haystack* is a :ref:`multirange <ref_std_multirange>`, the function\n    will return ``true`` if it contains either the specified multirange,\n    sub-range or element. The function will return ``false`` otherwise.\n\n    .. code-block:: edgeql-repl\n\n        db> select contains(\n        ...   multirange([\n        ...     range(1, 4), range(7),\n        ...   ]),\n        ...   multirange([\n        ...     range(1, 2), range(8, 10),\n        ...   ]),\n        ... );\n        {true}\n\n        db> select contains(\n        ...   multirange([\n        ...     range(1, 4), range(8, 10),\n        ...   ]),\n        ...   range(8),\n        ... );\n        {false}\n\n        db> select contains(\n        ...   multirange([\n        ...     range(1, 4), range(8, 10),\n        ...   ]),\n        ...   3,\n        ... );\n        {true}\n\n    When *haystack* is :ref:`JSON <ref_std_json>`, the function will return\n    ``true`` if the json data contains the element specified as *needle* or\n    ``false`` otherwise:\n\n    .. code-block:: edgeql-repl\n\n        db> with haystack := to_json('{\n        ...   \"city\": \"Baerlon\",\n        ...   \"city\": \"Caemlyn\"\n        ... }'),\n        ... needle := to_json('{\n        ...   \"city\": \"Caemlyn\"\n        ... }'),\n        ... select contains(haystack, needle);\n        {true}\n\n\n----------\n\n\n.. eql:function:: std::find(haystack: str, needle: str) -> int64\n                  std::find(haystack: bytes, needle: bytes) -> int64\n                  std::find(haystack: array<anytype>, needle: anytype, \\\n                            from_pos: int64=0) -> int64\n\n    .. index:: find, strpos\n\n    Returns the index of a given sub-value in a given value.\n\n    When *haystack* is a :eql:type:`str` or a :eql:type:`bytes` value, the\n    function will return the index of the first occurrence of *needle* in it.\n\n    When *haystack* is an :eql:type:`array`, this will return the index of the\n    the first occurrence of the element passed as *needle*. For\n    :eql:type:`array` inputs it is also possible to provide an optional\n    *from_pos* argument to specify the position from which to start the\n    search.\n\n    If the *needle* is not found, return ``-1``.\n\n    .. code-block:: edgeql-repl\n\n        db> select find('qwerty', 'we');\n        {1}\n\n        db> select find(b'qwerty', b'42');\n        {-1}\n\n        db> select find([2, 5, 7, 2, 100], 2);\n        {0}\n\n        db> select find([2, 5, 7, 2, 100], 2, 1);\n        {3}\n"
  },
  {
    "path": "docs/reference/stdlib/index.rst",
    "content": ".. versioned-section::\n\n.. _ref_std:\n\n================\nStandard Library\n================\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    generic\n    set\n    type\n    math\n    string\n    bool\n    numbers\n    json\n    uuid\n    enum\n    datetime\n    array\n    tuple\n    range\n    bytes\n    sequence\n    objects\n    abstract\n    constraints\n    net\n    fts\n    sys\n    cfg\n    pgcrypto\n    pg_trgm\n    pg_unaccent\n    pgvector\n    postgis\n    deprecated\n\n|Gel| comes with a rigorously defined type system consisting of **scalar\ntypes**, **collection types** (like arrays and tuples), and **object types**.\nThere is also a library of built-in functions and operators for working with\neach datatype.\n\n\n.. _ref_datamodel_typesystem:\n\nScalar Types\n------------\n\n.. _ref_datamodel_scalar_types:\n\n*Scalar types* store primitive data.\n\n- :ref:`Strings <ref_std_string>`\n- :ref:`Numbers <ref_std_numeric>`\n- :ref:`Booleans <ref_std_logical>`\n- :ref:`Dates and times <ref_std_datetime>`\n- :ref:`Enums <ref_std_enum>`\n- :ref:`JSON <ref_std_json>`\n- :ref:`UUID <ref_std_uuid>`\n- :ref:`Bytes <ref_std_bytes>`\n- :ref:`Sequences <ref_std_sequence>`\n- :ref:`Abstract types <ref_std_abstract_types>`: these are the types that\n  undergird the scalar hierarchy.\n\n.. _ref_datamodel_collection_types:\n\nCollection Types\n----------------\n\n*Collection types* are special generic types used to group homogeneous or\nheterogeneous data.\n\n- :ref:`Arrays <ref_std_array>`\n- :ref:`Tuples <ref_std_tuple>`\n\nRange Types\n-----------\n\n- :ref:`Range <ref_std_range>`\n- :ref:`Multirange <ref_std_multirange>`\n\nObject Types\n------------\n\n- :ref:`Object Types <ref_std_object_types>`\n\nTypes and Sets\n--------------\n\n- :ref:`Sets <ref_std_set>`\n- :ref:`Types <ref_std_type>`\n- :ref:`Casting <ref_eql_casts>`\n\nUtilities\n---------\n\n- :ref:`Math <ref_std_math>`\n- :ref:`Comparison <ref_std_generic>`\n- :ref:`Constraints <ref_std_constraints>`\n- :ref:`Full-text Search <ref_std_fts>`\n- :ref:`System <ref_std_sys>`\n\nExtensions\n----------\n\n- :ref:`ext::pgvector <ref_ext_pgvector>`\n"
  },
  {
    "path": "docs/reference/stdlib/json.rst",
    "content": ".. _ref_std_json:\n\n====\nJSON\n====\n\n:edb-alt-title: JSON Functions and Operators\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:type:`json`\n      - JSON scalar type\n\n    * - :eql:op:`json[i] <jsonidx>`\n      - :eql:op-desc:`jsonidx`\n\n    * - :eql:op:`json[from:to] <jsonslice>`\n      - :eql:op-desc:`jsonslice`\n\n    * - :eql:op:`json ++ json <jsonplus>`\n      - :eql:op-desc:`jsonplus`\n\n    * - :eql:op:`json[name] <jsonobjdest>`\n      - :eql:op-desc:`jsonobjdest`\n\n    * - :eql:op:`= <eq>` :eql:op:`\\!= <neq>` :eql:op:`?= <coaleq>`\n        :eql:op:`?!= <coalneq>` :eql:op:`\\< <lt>` :eql:op:`\\> <gt>`\n        :eql:op:`\\<= <lteq>` :eql:op:`\\>= <gteq>`\n      - Comparison operators\n\n    * - :eql:func:`to_json`\n      - :eql:func-desc:`to_json`\n\n    * - :eql:func:`to_str`\n      - Render JSON value to a string.\n\n    * - :eql:func:`json_get`\n      - :eql:func-desc:`json_get`\n\n    * - :eql:func:`json_set`\n      - :eql:func-desc:`json_set`\n\n    * - :eql:func:`json_array_unpack`\n      - :eql:func-desc:`json_array_unpack`\n\n    * - :eql:func:`json_object_pack`\n      - :eql:func-desc:`json_object_pack`\n\n    * - :eql:func:`json_object_unpack`\n      - :eql:func-desc:`json_object_unpack`\n\n    * - :eql:func:`json_typeof`\n      - :eql:func-desc:`json_typeof`\n\n.. _ref_std_json_construction:\n\nConstructing JSON Values\n------------------------\n\nJSON in Gel is a :ref:`scalar type <ref_datamodel_scalar_types>`. This type\ndoesn't have its own literal, and instead can be obtained by either casting a\nvalue to the :eql:type:`json` type, or by using the :eql:func:`to_json`\nfunction:\n\n.. code-block:: edgeql-repl\n\n    db> select to_json('{\"hello\": \"world\"}');\n    {Json(\"{\\\"hello\\\": \\\"world\\\"}\")}\n    db> select <json>'hello world';\n    {Json(\"\\\"hello world\\\"\")}\n\nAny value in Gel can be cast to a :eql:type:`json` type as well:\n\n.. code-block:: edgeql-repl\n\n    db> select <json>2019;\n    {Json(\"2019\")}\n    db> select <json>cal::to_local_date(datetime_current(), 'UTC');\n    {Json(\"\\\"2022-11-21\\\"\")}\n\nThe :eql:func:`json_object_pack` function provides one more way to\nconstruct JSON. It constructs a JSON object from an array of key/value\ntuples:\n\n.. code-block:: edgeql-repl\n\n    db> select json_object_pack({(\"hello\", <json>\"world\")});\n    {Json(\"{\\\"hello\\\": \\\"world\\\"}\")}\n\nAdditionally, any :eql:type:`Object` in Gel can be cast as a\n:eql:type:`json` type. This produces the same JSON value as the\nJSON-serialized result of that said object. Furthermore, this result will\nbe the same as the output of a :eql:stmt:`select expression <select>` in\n*JSON mode*, including the shape of that type:\n\n.. code-block:: edgeql-repl\n\n    db> select <json>(\n    ...     select schema::Object {\n    ...         name,\n    ...         timestamp := cal::to_local_date(\n    ...             datetime_current(), 'UTC')\n    ...     }\n    ...     filter .name = 'std::bool');\n    {Json(\"{\\\"name\\\": \\\"std::bool\\\", \\\"timestamp\\\": \\\"2022-11-21\\\"}\")}\n\nJSON values can also be cast back into scalars. Casting JSON is symmetrical\nmeaning that, if a scalar value can be cast into JSON, a compatible JSON value\ncan be cast into a scalar of that type. Some scalar types will have specific\nconditions for casting:\n\n- JSON strings can be cast to a :eql:type:`str` type. Casting :eql:type:`uuid`\n  and :ref:`date/time <ref_std_datetime>` types to JSON results in a JSON\n  string representing its original value. This means it is also possible to\n  cast a JSON string back to those types. The value of the UUID or datetime\n  string must be properly formatted to successfully cast from JSON, otherwise\n  Gel will raise an exception.\n- JSON numbers can be cast to any :ref:`numeric type <ref_std_numeric>`.\n- JSON booleans can be cast to a :eql:type:`bool` type.\n- JSON ``null`` is unique because it can be cast to an empty set (``{}``) of\n  any type.\n- JSON arrays can be cast to any valid array type, as long as the JSON array\n  is homogeneous, does not contain ``null`` as an element of the array, and\n  does not contain another array.\n\nA named :eql:type:`tuple` is converted into a JSON object when cast as a\n:eql:type:`json` while a standard :eql:type:`tuple` is converted into a\nJSON array.\n\n----------\n\n\n.. eql:type:: std::json\n\n    Arbitrary JSON data.\n\n    Any other type can be :eql:op:`cast <cast>` to and from JSON:\n\n    .. code-block:: edgeql-repl\n\n        db> select <json>42;\n        {Json(\"42\")}\n        db> select <bool>to_json('true');\n        {true}\n\n    A :eql:type:`json` value can also be cast as a :eql:type:`str` type, but\n    only when recognized as a JSON string:\n\n    .. code-block:: edgeql-repl\n\n        db> select <str>to_json('\"something\"');\n        {'something'}\n\n    Casting a JSON array of strings (``[\"a\", \"b\", \"c\"]``) to a :eql:type:`str`\n    will result in an error:\n\n    .. code-block:: edgeql-repl\n\n        db> select <str>to_json('[\"a\", \"b\", \"c\"]');\n        InvalidValueError: expected json string or null; got JSON array\n\n    Instead, use the :eql:func:`to_str` function to dump a JSON value to a\n    :eql:type:`str` value. Use the :eql:func:`to_json` function to parse a\n    JSON string to a :eql:type:`json` value:\n\n    .. code-block:: edgeql-repl\n\n        db> select to_json('[1, \"a\"]');\n        {Json(\"[1, \\\"a\\\"]\")}\n        db> select to_str(<json>[1, 2]);\n        {'[1, 2]'}\n\n    .. note::\n\n        This type is backed by the Postgres ``jsonb`` type which has a size\n        limit of 256MiB minus one byte. The Gel ``json`` type is also\n        subject to this limitation.\n\n\n----------\n\n\n.. eql:operator:: jsonidx: json [ int64 ] -> json\n\n    .. api-index:: §json§[§int§]\n\n    Accesses the element of the JSON string or array at a given index.\n\n    The contents of JSON *arrays* and *strings* can also be\n    accessed via ``[]``:\n\n    .. code-block:: edgeql-repl\n\n        db> select <json>'hello'[1];\n        {Json(\"\\\"e\\\"\")}\n        db> select <json>'hello'[-1];\n        {Json(\"\\\"o\\\"\")}\n        db> select to_json('[1, \"a\", null]')[1];\n        {Json(\"\\\"a\\\"\")}\n        db> select to_json('[1, \"a\", null]')[-1];\n        {Json(\"null\")}\n\n    This will raise an exception if the specified index is not valid for the\n    base JSON value. To access an index that is potentially out of bounds, use\n    :eql:func:`json_get`.\n\n\n----------\n\n\n.. eql:operator:: jsonslice: json [ int64 : int64 ] -> json\n\n    .. api-index:: §json§[§int§:§int§]\n\n    Produces a JSON value comprising a portion of the existing JSON value.\n\n    JSON *arrays* and *strings* can be sliced in the same way as\n    regular arrays, producing a new JSON array or string:\n\n    .. code-block:: edgeql-repl\n\n        db> select <json>'hello'[0:2];\n        {Json(\"\\\"he\\\"\")}\n        db> select <json>'hello'[2:];\n        {Json(\"\\\"llo\\\"\")}\n        db> select to_json('[1, 2, 3]')[0:2];\n        {Json(\"[1, 2]\")}\n        db> select to_json('[1, 2, 3]')[2:];\n        {Json(\"[3]\")}\n        db> select to_json('[1, 2, 3]')[:1];\n        {Json(\"[1]\")}\n        db> select to_json('[1, 2, 3]')[:-2];\n        {Json(\"[1]\")}\n\n----------\n\n\n.. eql:operator:: jsonplus: json ++ json -> json\n\n    .. index:: join, add\n    .. api-index:: §json §++§ json§\n\n    Concatenates two JSON arrays, objects, or strings into one.\n\n    JSON arrays, objects and strings can be concatenated with JSON values of\n    the same type into a new JSON value.\n\n    If you concatenate two JSON objects, you get a new object whose keys will\n    be a union of the keys of the input objects. If a key is present in both\n    objects, the value from the second object is taken.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_json('[1, 2]') ++ to_json('[3]');\n        {Json(\"[1, 2, 3]\")}\n        db> select to_json('{\"a\": 1}') ++ to_json('{\"b\": 2}');\n        {Json(\"{\\\"a\\\": 1, \\\"b\\\": 2}\")}\n        db> select to_json('{\"a\": 1, \"b\": 2}') ++ to_json('{\"b\": 3}');\n        {Json(\"{\\\"a\\\": 1, \\\"b\\\": 3}\")}\n        db> select to_json('\"123\"') ++ to_json('\"456\"');\n        {Json(\"\\\"123456\\\"\")}\n\n----------\n\n\n.. eql:operator:: jsonobjdest: json [ str ] -> json\n\n    .. index:: json get key\n    .. api-index:: §json§[§str§]\n\n    Accesses an element of a JSON object given its key.\n\n    The fields of JSON *objects* can also be accessed via ``[]``:\n\n    .. code-block:: edgeql-repl\n\n        db> select to_json('{\"a\": 2, \"b\": 5}')['b'];\n        {Json(\"5\")}\n        db> select j := <json>(schema::Type {\n        ...     name,\n        ...     timestamp := cal::to_local_date(datetime_current(), 'UTC')\n        ... })\n        ... filter j['name'] = <json>'std::bool';\n        {Json(\"{\\\"name\\\": \\\"std::bool\\\", \\\"timestamp\\\": \\\"2022-11-21\\\"}\")}\n\n\n    This will raise an exception if the specified field does not exist for the\n    base JSON value. To access an index that is potentially out of bounds, use\n    :eql:func:`json_get`.\n\n\n----------\n\n\n.. eql:function:: std::to_json(string: str) -> json\n\n    .. index:: json parse, loads\n\n    Returns a JSON value parsed from the given string.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_json('[1, \"hello\", null]');\n        {Json(\"[1, \\\"hello\\\", null]\")}\n        db> select to_json('{\"hello\": \"world\"}');\n        {Json(\"{\\\"hello\\\": \\\"world\\\"}\")}\n\n\n----------\n\n\n.. eql:function:: std::json_array_unpack(json: json) -> set of json\n\n    Returns the elements of a JSON array as a set of :eql:type:`json`.\n\n    Calling this function on anything other than a JSON array will\n    result in a runtime error.\n\n    This function should be used only if the ordering of elements is not\n    important, or when the ordering of the set is preserved (such as an\n    immediate input to an aggregate function).\n\n    .. code-block:: edgeql-repl\n\n        db> select json_array_unpack(to_json('[1, \"a\"]'));\n        {Json(\"1\"), Json(\"\\\"a\\\"\")}\n\n\n----------\n\n\n.. eql:function:: std::json_get(json: json, \\\n                                variadic path: str) -> optional json\n\n    .. index:: safe navigation\n\n    Returns a value from a JSON object or array given its path.\n\n    This function provides \"safe\" navigation of a JSON value. If the\n    input path is a valid path for the input JSON object/array, the\n    JSON value at the end of that path is returned:\n\n    .. code-block:: edgeql-repl\n\n        db> select json_get(to_json('{\n        ...     \"q\": 1,\n        ...     \"w\": [2, \"foo\"],\n        ...     \"e\": true\n        ... }'), 'w', '1');\n        {Json(\"\\\"foo\\\"\")}\n\n    This is useful when certain structure of JSON data is assumed, but cannot\n    be reliably guaranteed. If the path cannot be followed for any reason, the\n    empty set is returned:\n\n    .. code-block:: edgeql-repl\n\n        db> select json_get(to_json('{\n        ...     \"q\": 1,\n        ...     \"w\": [2, \"foo\"],\n        ...     \"e\": true\n        ... }'), 'w', '2');\n        {}\n\n    If you want to supply your own default for the case where the path cannot\n    be followed, you can do so using the :eql:op:`coalesce` operator:\n\n    .. code-block:: edgeql-repl\n\n        db> select json_get(to_json('{\n        ...     \"q\": 1,\n        ...     \"w\": [2, \"foo\"],\n        ...     \"e\": true\n        ... }'), 'w', '2') ?? <json>'mydefault';\n        {Json(\"\\\"mydefault\\\"\")}\n\n\n----------\n\n\n.. eql:function:: std::json_set( \\\n                    target: json, \\\n                    variadic path: str, \\\n                    named only value: optional json, \\\n                    named only create_if_missing: bool = true, \\\n                    named only empty_treatment: JsonEmpty = \\\n                      JsonEmpty.ReturnEmpty) \\\n                  -> optional json\n\n    Returns an updated JSON target with a new value.\n\n    .. code-block:: edgeql-repl\n\n        db> select json_set(\n        ...   to_json('{\"a\": 10, \"b\": 20}'),\n        ...   'a',\n        ...   value := <json>true,\n        ... );\n        {Json(\"{\\\"a\\\": true, \\\"b\\\": 20}\")}\n        db> select json_set(\n        ...   to_json('{\"a\": {\"b\": {}}}'),\n        ...   'a', 'b', 'c',\n        ...   value := <json>42,\n        ... );\n        {Json(\"{\\\"a\\\": {\\\"b\\\": {\\\"c\\\": 42}}}\")}\n\n    If *create_if_missing* is set to ``false``, a new path for the value\n    won't be created:\n\n    .. code-block:: edgeql-repl\n\n        db> select json_set(\n        ...   to_json('{\"a\": 10, \"b\": 20}'),\n        ...   'с',\n        ...   value := <json>42,\n        ... );\n        {Json(\"{\\\"a\\\": 10, \\\"b\\\": 20, \\\"с\\\": 42}\")}\n        db> select json_set(\n        ...   to_json('{\"a\": 10, \"b\": 20}'),\n        ...   'с',\n        ...   value := <json>42,\n        ...   create_if_missing := false,\n        ... );\n        {Json(\"{\\\"a\\\": 10, \\\"b\\\": 20}\")}\n\n    The *empty_treatment* parameter defines the behavior of the function if an\n    empty set is passed as *new_value*. This parameter can take these values:\n\n    - ``ReturnEmpty``: return empty set, default\n    - ``ReturnTarget``: return ``target`` unmodified\n    - ``Error``: raise an ``InvalidValueError``\n    - ``UseNull``: use a ``null`` JSON value\n    - ``DeleteKey``: delete the object key\n\n    .. code-block:: edgeql-repl\n\n        db> select json_set(\n        ...   to_json('{\"a\": 10, \"b\": 20}'),\n        ...   'a',\n        ...   value := <json>{}\n        ... );\n        {}\n        db> select json_set(\n        ...   to_json('{\"a\": 10, \"b\": 20}'),\n        ...   'a',\n        ...   value := <json>{},\n        ...   empty_treatment := JsonEmpty.ReturnTarget,\n        ... );\n        {Json(\"{\\\"a\\\": 10, \\\"b\\\": 20}\")}\n        db> select json_set(\n        ...   to_json('{\"a\": 10, \"b\": 20}'),\n        ...   'a',\n        ...   value := <json>{},\n        ...   empty_treatment := JsonEmpty.Error,\n        ... );\n        InvalidValueError: invalid empty JSON value\n        db> select json_set(\n        ...   to_json('{\"a\": 10, \"b\": 20}'),\n        ...   'a',\n        ...   value := <json>{},\n        ...   empty_treatment := JsonEmpty.UseNull,\n        ... );\n        {Json(\"{\\\"a\\\": null, \\\"b\\\": 20}\")}\n        db> select json_set(\n        ...   to_json('{\"a\": 10, \"b\": 20}'),\n        ...   'a',\n        ...   value := <json>{},\n        ...   empty_treatment := JsonEmpty.DeleteKey,\n        ... );\n        {Json(\"{\\\"b\\\": 20}\")}\n\n----------\n\n\n.. eql:function:: std::json_object_pack(pairs: SET OF tuple<str, json>) -> \\\n                  json\n\n    Returns the given set of key/value tuples as a JSON object.\n\n    .. code-block:: edgeql-repl\n\n        db> select json_object_pack({\n        ...     (\"foo\", to_json(\"1\")),\n        ...     (\"bar\", to_json(\"null\")),\n        ...     (\"baz\", to_json(\"[]\"))\n        ... });\n        {Json(\"{\\\"bar\\\": null, \\\"baz\\\": [], \\\"foo\\\": 1}\")}\n\n    If the key/value tuples being packed have common keys, the last value for\n    each key will make the final object.\n\n    .. code-block:: edgeql-repl\n\n        db> select json_object_pack({\n        ...     (\"hello\", <json>\"world\"),\n        ...     (\"hello\", <json>true)\n        ... });\n        {Json(\"{\\\"hello\\\": true}\")}\n\n\n----------\n\n\n.. eql:function:: std::json_object_unpack(json: json) -> \\\n                  set of tuple<str, json>\n\n    Returns the data in a JSON object as a set of key/value tuples.\n\n    Calling this function on anything other than a JSON object will\n    result in a runtime error.\n\n    .. code-block:: edgeql-repl\n\n        db> select json_object_unpack(to_json('{\n        ...     \"q\": 1,\n        ...     \"w\": [2, \"foo\"],\n        ...     \"e\": true\n        ... }'));\n        {('e', Json(\"true\")), ('q', Json(\"1\")), ('w', Json(\"[2, \\\"foo\\\"]\"))}\n\n\n----------\n\n\n.. eql:function:: std::json_typeof(json: json) -> str\n\n    Returns the type of the outermost JSON value as a string.\n\n    Possible return values are: ``'object'``, ``'array'``,\n    ``'string'``, ``'number'``, ``'boolean'``, or ``'null'``:\n\n    .. code-block:: edgeql-repl\n\n        db> select json_typeof(<json>2);\n        {'number'}\n        db> select json_typeof(to_json('null'));\n        {'null'}\n        db> select json_typeof(to_json('{\"a\": 2}'));\n        {'object'}\n"
  },
  {
    "path": "docs/reference/stdlib/math.rst",
    "content": ".. _ref_std_math:\n\n\n====\nMath\n====\n\n:edb-alt-title: Mathematical Functions\n\n.. include:: math_funcops_table.rst\n\n-----------\n\n\n.. eql:function:: math::abs(x: anyreal) -> anyreal\n\n    Returns the absolute value of the input.\n\n\n    .. code-block:: edgeql-repl\n\n        db> select math::abs(1);\n        {1}\n        db> select math::abs(-1);\n        {1}\n\n\n----------\n\n\n.. eql:function:: math::ceil(x: int64) -> float64\n                  math::ceil(x: float64) -> float64\n                  math::ceil(x: bigint) -> bigint\n                  math::ceil(x: decimal) -> decimal\n\n    Rounds up a given value to the nearest integer.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::ceil(1.1);\n        {2}\n        db> select math::ceil(-1.1);\n        {-1}\n\n\n----------\n\n\n.. eql:function:: math::floor(x: int64) -> float64\n                  math::floor(x: float64) -> float64\n                  math::floor(x: bigint) -> bigint\n                  math::floor(x: decimal) -> decimal\n\n    Rounds down a given value to the nearest integer.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::floor(1.1);\n        {1}\n        db> select math::floor(-1.1);\n        {-2}\n\n\n----------\n\n\n.. eql:function:: math::exp(x: int64) -> float64\n                  math::exp(x: float64) -> float64\n                  math::exp(x: decimal) -> decimal\n\n    Returns the natural logarithm of a given value.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::exp(1);\n        {2.718281829}\n\n----------\n\n\n.. eql:function:: math::ln(x: int64) -> float64\n                  math::ln(x: float64) -> float64\n                  math::ln(x: decimal) -> decimal\n\n    Returns the natural logarithm of a given value.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::exp(math::ln(100));\n        {100.00000009164575}\n\n\n----------\n\n\n.. eql:function:: math::lg(x: int64) -> float64\n                  math::lg(x: float64) -> float64\n                  math::lg(x: decimal) -> decimal\n\n    Returns the base 10 logarithm of a given value.\n\n    .. code-block:: edgeql-repl\n\n        db> select 10 ^ math::lg(42);\n        {42.00000000000001}\n\n----------\n\n\n.. eql:function:: math::log(x: decimal, named only base: decimal) -> decimal\n\n    Returns the logarithm of a given value in the specified base.\n\n    .. code-block:: edgeql-repl\n\n        db> select 3 ^ math::log(15n, base := 3n);\n        {15.0000000000000005n}\n\n\n----------\n\n\n.. eql:function:: math::mean(vals: set of int64) -> float64\n                  math::mean(vals: set of float64) -> float64\n                  math::mean(vals: set of decimal) -> decimal\n\n    .. index:: average, avg\n\n    Returns the arithmetic mean of the input set.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::mean({1, 3, 5});\n        {3}\n\n\n----------\n\n\n.. eql:function:: math::stddev(vals: set of int64) -> float64\n                  math::stddev(vals: set of float64) -> float64\n                  math::stddev(vals: set of decimal) -> decimal\n\n    .. index:: average, avg\n\n    Returns the sample standard deviation of the input set.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::stddev({1, 3, 5});\n        {2}\n\n.. eql:function:: math::stddev_pop(vals: set of int64) -> float64\n                  math::stddev_pop(vals: set of float64) -> float64\n                  math::stddev_pop(vals: set of decimal) -> decimal\n\n    .. index:: average, avg\n\n    Returns the population standard deviation of the input set.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::stddev_pop({1, 3, 5});\n        {1.63299316185545}\n\n\n----------\n\n\n.. eql:function:: math::var(vals: set of int64) -> float64\n                  math::var(vals: set of float64) -> float64\n                  math::var(vals: set of decimal) -> decimal\n\n    .. index:: average, avg\n\n    Returns the sample variance of the input set.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::var({1, 3, 5});\n        {4}\n\n\n----------\n\n\n.. eql:function:: math::var_pop(vals: set of int64) -> float64\n                  math::var_pop(vals: set of float64) -> float64\n                  math::var_pop(vals: set of decimal) -> decimal\n\n    .. index:: average, avg\n\n    Returns the population variance of the input set.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::var_pop({1, 3, 5});\n        {2.66666666666667}\n\n\n-----------\n\n\n.. eql:function:: math::pi() -> float64\n\n    .. index:: trigonometry\n\n    Returns the value of pi.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::pi();\n        {3.141592653589793}\n\n-----------\n\n\n.. eql:function:: math::e() -> float64\n\n    Returns the value of e (euler's number).\n\n    .. code-block:: edgeql-repl\n\n        db> select math::e();\n        {2.718281828459045}\n\n\n-----------\n\n\n.. eql:function:: math::acos(x: float64) -> float64\n\n    .. index:: trigonometry\n\n    Returns the arc cosine of the input.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::acos(-1);\n        {3.141592653589793}\n        db> select math::acos(0);\n        {1.5707963267948966}\n        db> select math::acos(1);\n        {0}\n\n\n-----------\n\n\n.. eql:function:: math::asin(x: float64) -> float64\n\n    .. index:: trigonometry\n\n    Returns the arc sine of the input.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::asin(-1);\n        {-1.5707963267948966}\n        db> select math::asin(0);\n        {0}\n        db> select math::asin(1);\n        {1.5707963267948966}\n\n\n-----------\n\n\n.. eql:function:: math::atan(x: float64) -> float64\n\n    .. index:: trigonometry\n\n    Returns the arc tangent of the input.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::atan(-1);\n        {-0.7853981633974483}\n        db> select math::atan(0);\n        {0}\n        db> select math::atan(1);\n        {0.7853981633974483}\n\n\n-----------\n\n\n.. eql:function:: math::atan2(y: float64, x: float64) -> float64\n\n    .. index:: trigonometry\n\n    Returns the arc tangent of ``y / x``.\n\n    Uses the signs of the arguments determine the correct quadrant.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::atan2(1, 1);\n        {0.7853981633974483}\n        db> select math::atan2(1, -1);\n        {2.356194490192345}\n        db> select math::atan2(-1, -1);\n        {-2.356194490192345}\n        db> select math::atan2(-1, 1);\n        {-0.7853981633974483}\n\n\n-----------\n\n\n.. eql:function:: math::cos(x: float64) -> float64\n\n    .. index:: trigonometry\n\n    Returns the cosine of the input.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::cos(0);\n        {1}\n        db> select math::cos(math::pi() / 2);\n        {0.000000000}\n        db> select math::cos(math::pi());\n        {-1}\n        db> select math::cos(math::pi() * 3 / 2);\n        {-0.000000000}\n\n\n-----------\n\n\n.. eql:function:: math::cot(x: float64) -> float64\n\n    .. index:: trigonometry\n\n    Returns the cotangent of the input.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::cot(math::pi() / 4);\n        {1.000000000}\n        db> select math::cot(math::pi() / 2);\n        {0.000000000}\n        db> select math::cot(math::pi() * 3 / 4);\n        {-0.999999999}\n\n\n-----------\n\n\n.. eql:function:: math::sin(x: float64) -> float64\n\n    .. index:: trigonometry\n\n    Returns the sinine of the input.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::sin(0);\n        {0}\n        db> select math::sin(math::pi() / 2);\n        {1}\n        db> select math::sin(math::pi());\n        {0.000000000}\n        db> select math::sin(math::pi() * 3 / 2);\n        {-1}\n\n\n-----------\n\n\n.. eql:function:: math::tan(x: float64) -> float64\n\n    .. index:: trigonometry\n\n    Returns the tanangent of the input.\n\n    .. code-block:: edgeql-repl\n\n        db> select math::tan(-math::pi() / 4);\n        {-0.999999999}\n        db> select math::tan(0);\n        {0}\n        db> select math::tan(math::pi() / 4);\n        {0.999999999}\n"
  },
  {
    "path": "docs/reference/stdlib/math_funcops_table.rst",
    "content": "\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`math::abs`\n      - :eql:func-desc:`math::abs`\n\n    * - :eql:func:`math::ceil`\n      - :eql:func-desc:`math::ceil`\n\n    * - :eql:func:`math::floor`\n      - :eql:func-desc:`math::floor`\n\n    * - :eql:func:`math::exp`\n      - :eql:func-desc:`math::exp`\n\n    * - :eql:func:`math::ln`\n      - :eql:func-desc:`math::ln`\n\n    * - :eql:func:`math::lg`\n      - :eql:func-desc:`math::lg`\n\n    * - :eql:func:`math::log`\n      - :eql:func-desc:`math::log`\n\n    * - :eql:func:`math::mean`\n      - :eql:func-desc:`math::mean`\n\n    * - :eql:func:`math::stddev`\n      - :eql:func-desc:`math::stddev`\n\n    * - :eql:func:`math::stddev_pop`\n      - :eql:func-desc:`math::stddev_pop`\n\n    * - :eql:func:`math::var`\n      - :eql:func-desc:`math::var`\n\n    * - :eql:func:`math::var_pop`\n      - :eql:func-desc:`math::var_pop`\n\n    * - :eql:func:`math::pi`\n      - :eql:func-desc:`math::pi`\n\n    * - :eql:func:`math::e`\n      - :eql:func-desc:`math::e`\n\n    * - :eql:func:`math::acos`\n      - :eql:func-desc:`math::acos`\n\n    * - :eql:func:`math::asin`\n      - :eql:func-desc:`math::asin`\n\n    * - :eql:func:`math::atan`\n      - :eql:func-desc:`math::atan`\n\n    * - :eql:func:`math::atan2`\n      - :eql:func-desc:`math::atan2`\n\n    * - :eql:func:`math::cos`\n      - :eql:func-desc:`math::cos`\n\n    * - :eql:func:`math::cot`\n      - :eql:func-desc:`math::cot`\n\n    * - :eql:func:`math::sin`\n      - :eql:func-desc:`math::sin`\n\n    * - :eql:func:`math::tan`\n      - :eql:func-desc:`math::tan`\n"
  },
  {
    "path": "docs/reference/stdlib/net.rst",
    "content": ".. _ref_std_net:\n\n.. versionadded:: 6.0\n\n===\nNet\n===\n\nThe ``net`` module provides an interface for performing network-related operations directly from Gel. It is useful for integrating with external services, fetching data from APIs, or triggering webhooks as part of your database logic.\n\n.. list-table::\n  :class: funcoptable\n\n  * - :eql:type:`net::RequestState`\n    - An enum representing the state of a network request.\n  * - :eql:type:`net::RequestFailureKind`\n    - An enum representing the kind of failure that occurred for a network request.\n  * - :eql:type:`net::http::Method`\n    - An enum representing HTTP methods.\n  * - :eql:type:`net::http::Response`\n    - A type representing an HTTP response.\n  * - :eql:type:`net::http::ScheduledRequest`\n    - A type representing a scheduled HTTP request.\n\n----------\n\n.. eql:type:: net::RequestState\n\n  An enumeration of possible states for a network request.\n\n  Possible values are:\n\n  * ``Pending``\n  * ``InProgress``\n  * ``Completed``\n  * ``Failed``\n\n----------\n\n.. eql:type:: net::RequestFailureKind\n\n  An enumeration of possible failure kinds for a network request.\n\n  Possible values are:\n\n  * ``NetworkError``\n  * ``Timeout``\n\n----------\n\nHTTP Submodule\n==============\n\nThe ``net::http`` submodule provides types and functions for making HTTP requests.\n\nOverview\n--------\n\nThe primary function for scheduling HTTP requests is :eql:func:`net::http::schedule_request`. This function lets you specify the URL, HTTP method, headers, and body of the request. Once scheduled, you can monitor the request's status and process its response when available.\n\nExample Usage\n-------------\n\nHere's a simple example of how to use the ``net::http`` module to make a GET request:\n\n.. code-block:: edgeql\n\n  with request := (\n      net::http::schedule_request(\n          'https://example.com',\n          method := net::http::Method.POST,\n          headers := [('Content-Type', 'application/json')],\n          body := <bytes>$${\"key\": \"value\"}$$\n      )\n  )\n  select request.id;\n\nThis ID will be helpful if you need to observe a request's response. You can poll the ``ScheduledRequest`` object in order to get any response data or failure information:\n\n1. **Check the State**: Use the ``state`` field to determine the current status of the request.\n\n2. **Handle Failures**: If the request has failed, inspect the ``failure`` field to understand the kind of failure (e.g., ``NetworkError`` or ``Timeout``) and any associated message.\n\n3. **Process the Response**: If the request is completed successfully, access the ``response.body`` to retrieve the data returned by the request. The body is in ``bytes`` format and may need conversion or parsing.\n\nIn the following example, we'll query the ``ScheduledRequest`` object we created above using the ID we selected. Once the request is completed or it has failed, this query will return the response data or the failure information:\n\n.. code-block:: edgeql\n\n  with\n      request := <std::net::http::ScheduledRequest><uuid>$request_id,\n  select request {\n      state,\n      failure,\n      response: {\n          status,\n          headers,\n          body,\n      },\n  } filter .state in {net::RequestState.Failed, net::RequestState.Completed};\n\nReference\n---------\n\n.. eql:type:: net::http::Method\n\n  An enumeration of supported HTTP methods.\n\n  Possible values are:\n\n  * ``GET``\n  * ``POST``\n  * ``PUT``\n  * ``DELETE``\n  * ``HEAD``\n  * ``OPTIONS``\n  * ``PATCH``\n\n----------\n\n.. eql:type:: net::http::Response\n\n  A type representing an HTTP response.\n\n  :eql:synopsis:`created_at -> datetime`\n    The timestamp when the response was created.\n\n  :eql:synopsis:`status -> int16`\n    The HTTP status code of the response.\n\n  :eql:synopsis:`headers -> array<tuple<name: str, value: str>>`\n    The headers of the response.\n\n  :eql:synopsis:`body -> bytes`\n    The body of the response.\n\n----------\n\n.. eql:type:: net::http::ScheduledRequest\n\n  A type representing a scheduled HTTP request.\n\n  :eql:synopsis:`state -> net::RequestState`\n    The current state of the request.\n\n  :eql:synopsis:`created_at -> datetime`\n    The timestamp when the request was created.\n\n  :eql:synopsis:`failure -> tuple<kind: net::RequestFailureKind, message: str>`\n    Information about the failure, if the request failed.\n\n  :eql:synopsis:`url -> str`\n    The URL of the request.\n\n  :eql:synopsis:`method -> net::http::Method`\n    The HTTP method of the request.\n\n  :eql:synopsis:`headers -> array<tuple<name: str, value: str>>`\n    The headers of the request.\n\n  :eql:synopsis:`body -> bytes`\n    The body of the request.\n\n  :eql:synopsis:`response -> net::http::Response`\n    The response to the request, if completed.\n\n----------\n\n.. eql:function:: net::http::schedule_request( \\\n                    url: str, \\\n                    body: optional bytes = {}, \\\n                    method: optional net::http::Method = net::http::Method.`GET`, \\\n                    headers: optional array<tuple<name: str, value: str>> = {} \\\n                  ) -> net::http::ScheduledRequest\n\n  Schedules an HTTP request.\n\n  Parameters:\n\n  * ``url``: The URL to send the request to.\n  * ``body``: The body of the request (optional).\n  * ``method``: The HTTP method to use (optional, defaults to GET).\n  * ``headers``: The headers to include in the request (optional).\n\n  Returns ``net::http::ScheduledRequest`` object representing\n  the scheduled request.\n\n  Example:\n\n  .. code-block:: edgeql\n\n    SELECT net::http::schedule_request(\n      'https://example.com',\n      method := net::http::Method.POST,\n      headers := [('Content-Type', 'application/json')],\n      body := <bytes>$${\"key\": \"value\"}$$\n    );\n"
  },
  {
    "path": "docs/reference/stdlib/numbers.rst",
    "content": ".. _ref_std_numeric:\n\n=======\nNumbers\n=======\n\n:edb-alt-title: Numerical Types, Functions, and Operators\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:type:`int16`\n      - 16-bit integer\n\n    * - :eql:type:`int32`\n      - 32-bit integer\n\n    * - :eql:type:`int64`\n      - 64-bit integer\n\n    * - :eql:type:`float32`\n      - 32-bit floating point number\n\n    * - :eql:type:`float64`\n      - 64-bit floating point number\n\n    * - :eql:type:`bigint`\n      - Arbitrary precision integer.\n\n    * - :eql:type:`decimal`\n      - Arbitrary precision number.\n\n    * - :eql:op:`anyreal + anyreal <plus>`\n      - :eql:op-desc:`plus`\n\n    * - :eql:op:`anyreal - anyreal <minus>`\n      - :eql:op-desc:`minus`\n\n    * - :eql:op:`-anyreal <uminus>`\n      - :eql:op-desc:`uminus`\n\n    * - :eql:op:`anyreal * anyreal <mult>`\n      - :eql:op-desc:`mult`\n\n    * - :eql:op:`anyreal / anyreal <div>`\n      - :eql:op-desc:`div`\n\n    * - :eql:op:`anyreal // anyreal <floordiv>`\n      - :eql:op-desc:`floordiv`\n\n    * - :eql:op:`anyreal % anyreal <mod>`\n      - :eql:op-desc:`mod`\n\n    * - :eql:op:`anyreal ^ anyreal <pow>`\n      - :eql:op-desc:`pow`\n\n    * - :eql:op:`= <eq>` :eql:op:`\\!= <neq>` :eql:op:`?= <coaleq>`\n        :eql:op:`?!= <coalneq>` :eql:op:`\\< <lt>` :eql:op:`\\> <gt>`\n        :eql:op:`\\<= <lteq>` :eql:op:`\\>= <gteq>`\n      - Comparison operators\n\n    * - :eql:func:`sum`\n      - :eql:func-desc:`sum`\n\n    * - :eql:func:`min`\n      - :eql:func-desc:`min`\n\n    * - :eql:func:`max`\n      - :eql:func-desc:`max`\n\n    * - :eql:func:`round`\n      - :eql:func-desc:`round`\n\n    * - :eql:func:`random`\n      - :eql:func-desc:`random`\n\nMathematical functions\n----------------------\n\n.. include:: math_funcops_table.rst\n\nBitwise functions\n-----------------\n\n.. list-table::\n  :class: funcoptable\n\n  * - :eql:func:`bit_and`\n    - :eql:func-desc:`bit_and`\n\n  * - :eql:func:`bit_or`\n    - :eql:func-desc:`bit_or`\n\n  * - :eql:func:`bit_xor`\n    - :eql:func-desc:`bit_xor`\n\n  * - :eql:func:`bit_not`\n    - :eql:func-desc:`bit_not`\n\n  * - :eql:func:`bit_lshift`\n    - :eql:func-desc:`bit_lshift`\n\n  * - :eql:func:`bit_rshift`\n    - :eql:func-desc:`bit_rshift`\n\n  * - :eql:func:`bit_count`\n    - :eql:func-desc:`bit_count`\n\nString parsing\n--------------\n\n.. list-table::\n  :class: funcoptable\n\n  * - :eql:func:`to_bigint`\n    - :eql:func-desc:`to_bigint`\n\n  * - :eql:func:`to_decimal`\n    - :eql:func-desc:`to_decimal`\n\n  * - :eql:func:`to_int16`\n    - :eql:func-desc:`to_int16`\n\n  * - :eql:func:`to_int32`\n    - :eql:func-desc:`to_int32`\n\n  * - :eql:func:`to_int64`\n    - :eql:func-desc:`to_int64`\n\n  * - :eql:func:`to_float32`\n    - :eql:func-desc:`to_float32`\n\n  * - :eql:func:`to_float64`\n    - :eql:func-desc:`to_float64`\n\nIt's possible to explicitly :eql:op:`cast <cast>`\nbetween all numeric types. All numeric types can also be cast to and\nfrom :eql:type:`str` and :eql:type:`json`.\n\n\nDefinitions\n-----------\n\n\n.. eql:type:: std::int16\n\n    A 16-bit signed integer.\n\n    ``int16`` is capable of representing values from ``-32768`` to\n    ``+32767`` (inclusive).\n\n\n----------\n\n\n.. eql:type:: std::int32\n\n    A 32-bit signed integer.\n\n    ``int32`` is capable of representing values from ``-2147483648`` to\n    ``+2147483647`` (inclusive).\n\n\n----------\n\n\n.. eql:type:: std::int64\n\n    A 64-bit signed integer.\n\n    ``int64`` is capable of representing values from ``-9223372036854775808``\n    to ``+9223372036854775807`` (inclusive).\n\n\n----------\n\n\n.. eql:type:: std::float32\n\n    .. index:: float\n\n    A variable precision, inexact number.\n\n    The minimal guaranteed precision is at least 6 decimal digits. The\n    approximate range of a ``float32`` spans from ``-3.4e+38`` to\n    ``+3.4e+38``.\n\n\n----------\n\n\n.. eql:type:: std::float64\n\n    .. index:: float, double\n\n    A variable precision, inexact number.\n\n    The minimal guaranteed precision is at least 15 decimal digits. The\n    approximate range of a ``float64`` spans from ``-1.7e+308`` to\n    ``+1.7e+308``.\n\n\n----------\n\n\n.. eql:type:: std::bigint\n\n    .. index:: numeric\n\n    An arbitrary precision integer.\n\n    Our philosophy is that use of ``bigint`` should always be an explicit\n    opt-in and should never be implicit. Once used, these values should not be\n    accidentally cast to a different numerical type that could lead to a loss\n    of precision.\n\n    In keeping with this philosophy, :ref:`our mathematical functions\n    <ref_std_math>` are designed to maintain separation between big integer\n    values and the rest of our numeric types.\n\n    All of the following types can be explicitly cast into a ``bigint`` type:\n\n    - :eql:type:`str`\n    - :eql:type:`json`\n    - :eql:type:`int16`\n    - :eql:type:`int32`\n    - :eql:type:`int64`\n    - :eql:type:`float32`\n    - :eql:type:`float64`\n    - :eql:type:`decimal`\n\n    A bigint literal is an integer literal, followed by 'n':\n\n    .. code-block:: edgeql-repl\n\n        db> select 42n is bigint;\n        {true}\n\n    To represent really big integers, it is possible to use the\n    exponent notation (e.g. ``1e20n`` instead of ``100000000000000000000n``)\n    as long as the exponent is positive and there is no dot anywhere:\n\n    .. code-block:: edgeql-repl\n\n        db> select 1e+100n is bigint;\n        {true}\n\n    When a float literal is followed by ``n`` it will produce a\n    :eql:type:`decimal` value instead:\n\n    .. code-block:: edgeql-repl\n\n        db> select 1.23n is decimal;\n        {true}\n\n        db> select 1.0e+100n is decimal;\n        {true}\n\n    .. note::\n\n        Use caution when casting ``bigint`` values into\n        :eql:type:`json`. The JSON specification does not have a limit on\n        significant digits, so a ``bigint`` number can be losslessly\n        represented in JSON. However, JSON decoders in many languages\n        will read all such numbers as some kind of 32-bit or 64-bit\n        number type, which may result in errors or precision loss. If\n        such loss is unacceptable, then consider casting the value\n        into :eql:type:`str` and decoding it on the client side into a more\n        appropriate type.\n\n\n----------\n\n\n.. eql:type:: std::decimal\n\n    .. index:: numeric, float\n\n    Any number of arbitrary precision.\n\n    Our philosophy is that use of ``decimal`` should always be an explicit\n    opt-in and should never be implicit. Once used, these values should not be\n    accidentally cast to a different numerical type that could lead to a loss\n    of precision.\n\n    In keeping with this philosophy, :ref:`our mathematical functions\n    <ref_std_math>` are designed to maintain separation between ``decimal``\n    values and the rest of our numeric types.\n\n    All of the following types can be explicitly cast into decimal:\n\n    - :eql:type:`str`\n    - :eql:type:`json`\n    - :eql:type:`int16`\n    - :eql:type:`int32`\n    - :eql:type:`int64`\n    - :eql:type:`float32`\n    - :eql:type:`float64`\n    - :eql:type:`bigint`\n\n    A decimal literal is a float literal, followed by ``n``:\n\n    The Gel philosophy is that using a decimal type should be an\n    explicit opt-in, but once used, the values should not be\n    accidentally cast into a numeric type with less precision.\n\n    In accordance with this :ref:`the mathematical functions\n    <ref_std_math>` are designed to keep the separation\n    between decimal values and the rest of the numeric types.\n\n    All of the following types can be explicitly cast into decimal:\n    :eql:type:`str`, :eql:type:`json`, :eql:type:`int16`,\n    :eql:type:`int32`, :eql:type:`int64`, :eql:type:`float32`,\n    :eql:type:`float64`, and :eql:type:`bigint`.\n\n    A decimal literal is a float literal followed by 'n':\n\n    .. code-block:: edgeql-repl\n\n        db> select 1.23n is decimal;\n        {true}\n\n        db> select 1.0e+100n is decimal;\n        {true}\n\n    Note that an integer literal (without a dot or exponent) followed\n    by ``n`` produces a :eql:type:`bigint` value. A literal without a dot\n    and with a positive exponent makes a :eql:type:`bigint`, too:\n\n    .. code-block:: edgeql-repl\n\n        db> select 42n is bigint;\n        {true}\n\n        db> select 12e+34n is bigint;\n        {true}\n\n    .. note::\n\n        Use caution when casting ``decimal`` values into :eql:type:`json`. The\n        JSON specification does not have a limit on significant digits, so a\n        ``decimal`` number can be losslessly represented in JSON. However,\n        JSON decoders in many languages will read all such numbers as some\n        kind of floating point values, which may result in precision loss. If\n        such loss is unacceptable, then consider casting the value into a\n        :eql:type:`str` and decoding it on the client side into a more\n        appropriate type.\n\n\n----------\n\n\n.. eql:operator:: plus: anyreal + anyreal -> anyreal\n\n    .. index:: plus\n    .. api-index:: §number §+§ number§\n\n    Arithmetic addition.\n\n    .. code-block:: edgeql-repl\n\n        db> select 2 + 2;\n        {4}\n\n\n----------\n\n\n.. eql:operator:: minus: anyreal - anyreal -> anyreal\n\n    .. index:: minus\n    .. api-index:: §number §-§ number§\n\n    Arithmetic subtraction.\n\n    .. code-block:: edgeql-repl\n\n        db> select 3 - 2;\n        {1}\n\n\n----------\n\n\n.. eql:operator:: uminus: - anyreal -> anyreal\n\n    .. index:: unary minus, subtraction\n    .. api-index:: -§ number§\n\n    Arithmetic negation.\n\n    .. code-block:: edgeql-repl\n\n        db> select -5;\n        {-5}\n\n\n----------\n\n\n.. eql:operator:: mult: anyreal * anyreal -> anyreal\n\n    .. index:: times, multiply\n    .. api-index:: §number §*§ number§\n\n    Arithmetic multiplication.\n\n    .. code-block:: edgeql-repl\n\n        db> select 2 * 10;\n        {20}\n\n\n----------\n\n\n.. eql:operator:: div: anyreal / anyreal -> anyreal\n\n    .. index:: divide\n    .. api-index:: §number §/§ number§\n\n    Arithmetic division.\n\n    .. code-block:: edgeql-repl\n\n        db> select 10 / 4;\n        {2.5}\n\n    Division by zero will result in an error:\n\n    .. code-block:: edgeql-repl\n\n        db> select 10 / 0;\n        DivisionByZeroError: division by zero\n\n\n----------\n\n\n.. eql:operator:: floordiv: anyreal // anyreal -> anyreal\n\n    .. index:: floor divide\n    .. api-index:: §number §//§ number§\n\n    Floor division.\n\n    In floor-based division, the result of a standard division operation is\n    rounded down to its nearest integer. It is the equivalent to using regular\n    division and then applying :eql:func:`math::floor` to the result.\n\n    .. code-block:: edgeql-repl\n\n        db> select 10 // 4;\n        {2}\n        db> select math::floor(10 / 4);\n        {2}\n        db> select -10 // 4;\n        {-3}\n\n    It also works on :eql:type:`float <anyfloat>`, :eql:type:`bigint`, and\n    :eql:type:`decimal` types. The type of the result corresponds to\n    the type of the operands:\n\n    .. code-block:: edgeql-repl\n\n        db> select 3.7 // 1.1;\n        {3.0}\n        db> select 3.7n // 1.1n;\n        {3.0n}\n        db> select 37 // 11;\n        {3}\n\n    Regular division, floor division, and :eql:op:`%<mod>` operations are\n    related in the following way: ``A // B  =  (A - (A % B)) / B``.\n\n\n----------\n\n\n.. eql:operator:: mod: anyreal % anyreal -> anyreal\n\n    .. api-index:: §number §%§ number§\n\n    Remainder from division (modulo).\n\n    This is commonly referred to as a \"modulo\" operation.\n\n    This is the remainder from floor division. Just as is\n    the case with :eql:op:`//<floordiv>` the result type of the\n    remainder operator corresponds to the operand type:\n\n    .. code-block:: edgeql-repl\n\n        db> select 10 % 4;\n        {2}\n        db> select 10n % 4;\n        {2n}\n        db> select -10 % 4;\n        {2}\n        db> # floating arithmetic is inexact, so\n        ... # we get 0.3999999999999999 instead of 0.4\n        ... select 3.7 % 1.1;\n        {0.3999999999999999}\n        db> select 3.7n % 1.1n;\n        {0.4n}\n        db> select 37 % 11;\n        {4}\n\n    Regular division, :eql:op:`//<floordiv>` and :eql:op:`%<mod>` operations\n    are related in the following way: ``A // B  =  (A - (A % B)) / B``.\n\n    Modulo division by zero will result in an error:\n\n    .. code-block:: edgeql-repl\n\n        db> select 10 % 0;\n        DivisionByZeroError: division by zero\n\n\n-----------\n\n\n.. eql:operator:: pow: anyreal ^ anyreal -> anyreal\n\n    .. index:: exponentiation\n    .. api-index:: §number §^§ number§\n\n    Power operation.\n\n    .. code-block:: edgeql-repl\n\n        db> select 2 ^ 4;\n        {16}\n\n\n----------\n\n\n.. eql:function:: std::round(value: int64) -> float64\n                  std::round(value: float64) -> float64\n                  std::round(value: bigint) -> bigint\n                  std::round(value: decimal) -> decimal\n                  std::round(value: decimal, d: int64) -> decimal\n\n    Rounds a given number to the nearest value.\n\n    The function will round a ``.5`` value differently depending on the type\n    of the parameter passed.\n\n    The :eql:type:`float64` tie is rounded to the nearest even number:\n\n    .. code-block:: edgeql-repl\n\n        db> select round(1.2);\n        {1}\n\n        db> select round(1.5);\n        {2}\n\n        db> select round(2.5);\n        {2}\n\n    But the :eql:type:`decimal` tie is rounded away from zero:\n\n    .. code-block:: edgeql-repl\n\n        db> select round(1.2n);\n        {1n}\n\n        db> select round(1.5n);\n        {2n}\n\n        db> select round(2.5n);\n        {3n}\n\n    Additionally, when rounding a :eql:type:`decimal` value, you may pass the\n    optional argument *d* to specify the precision of the rounded result:\n\n    .. code-block:: edgeql-repl\n\n        db> select round(163.278n, 2);\n        {163.28n}\n\n        db> select round(163.278n, 1);\n        {163.3n}\n\n        db> select round(163.278n, 0);\n        {163n}\n\n        db> select round(163.278n, -1);\n        {160n}\n\n        db> select round(163.278n, -2);\n        {200n}\n\n\n----------\n\n\n.. eql:function:: std::random() -> float64\n\n    Returns a pseudo-random number in the range of ``0.0 <= x < 1.0``.\n\n    .. code-block:: edgeql-repl\n\n        db> select random();\n        {0.62649393780157}\n\n\n----------\n\n\n.. eql:function:: std::bit_and(l: int16, r: int16) -> int16\n                  std::bit_and(l: int32, r: int32) -> int32\n                  std::bit_and(l: int64, r: int64) -> int64\n\n    Bitwise AND operator for 2 integers.\n\n    .. code-block:: edgeql-repl\n\n        db> select bit_and(17, 3);\n        {1}\n\n\n----------\n\n\n.. eql:function:: std::bit_or(l: int16, r: int16) -> int16\n                  std::bit_or(l: int32, r: int32) -> int32\n                  std::bit_or(l: int64, r: int64) -> int64\n\n    Bitwise OR operator for 2 integers.\n\n    .. code-block:: edgeql-repl\n\n        db> select bit_or(17, 3);\n        {19}\n\n\n----------\n\n\n.. eql:function:: std::bit_xor(l: int16, r: int16) -> int16\n                  std::bit_xor(l: int32, r: int32) -> int32\n                  std::bit_xor(l: int64, r: int64) -> int64\n\n    Bitwise exclusive OR operator for 2 integers.\n\n    .. code-block:: edgeql-repl\n\n        db> select bit_xor(17, 3);\n        {18}\n\n\n----------\n\n\n.. eql:function:: std::bit_not(r: int16) -> int16\n                  std::bit_not(r: int32) -> int32\n                  std::bit_not(r: int64) -> int64\n\n    Bitwise negation operator for 2 integers.\n\n    Bitwise negation for integers ends up similar to mathematical negation\n    because typically the signed integers use \"two's complement\"\n    representation. In this represenation mathematical negation is achieved by\n    aplying bitwise negation and adding ``1``.\n\n    .. code-block:: edgeql-repl\n\n        db> select bit_not(17);\n        {-18}\n        db> select -17 = bit_not(17) + 1;\n        {true}\n\n\n----------\n\n\n.. eql:function:: std::bit_lshift(val: int16, n: int64) -> int16\n                  std::bit_lshift(val: int32, n: int64) -> int32\n                  std::bit_lshift(val: int64, n: int64) -> int64\n\n    Bitwise left-shift operator for integers.\n\n    The integer *val* is shifted by *n* bits to the left. The rightmost added\n    bits are all ``0``. Shifting an integer by a number of bits greater than\n    the bit size of the integer results in ``0``.\n\n\n    .. code-block:: edgeql-repl\n\n        db> select bit_lshift(123, 2);\n        {492}\n        db> select bit_lshift(123, 65);\n        {0}\n\n    Left-shifting an integer can change the sign bit:\n\n    .. code-block:: edgeql-repl\n\n        db> select bit_lshift(123, 60);\n        {-5764607523034234880}\n\n    In general, left-shifting an integer in small increments produces the same\n    result as shifting it in one step:\n\n    .. code-block:: edgeql-repl\n\n        db> select bit_lshift(bit_lshift(123, 1), 3);\n        {1968}\n        db> select bit_lshift(123, 4);\n        {1968}\n\n    It is an error to attempt to shift by a negative number of bits:\n\n    .. code-block:: edgeql-repl\n\n        db> select bit_lshift(123, -2);\n        gel error: InvalidValueError: bit_lshift(): cannot shift by\n        negative amount\n\n\n----------\n\n\n.. eql:function:: std::bit_rshift(val: int16, n: int64) -> int16\n                  std::bit_rshift(val: int32, n: int64) -> int32\n                  std::bit_rshift(val: int64, n: int64) -> int64\n\n    Bitwise arithemtic right-shift operator for integers.\n\n    The integer *val* is shifted by *n* bits to the right. In the arithmetic\n    right-shift, the sign is preserved. This means that the leftmost added bits\n    are ``1`` or ``0`` depending on the sign bit. Shifting an integer by a\n    number of bits greater than the bit size of the integer results in ``0``\n    for positive numbers or ``-1`` for negative numbers.\n\n    .. code-block:: edgeql-repl\n\n        db> select bit_rshift(123, 2);\n        {30}\n        db> select bit_rshift(123, 65);\n        {0}\n        db> select bit_rshift(-123, 2);\n        {-31}\n        db> select bit_rshift(-123, 65);\n        {-1}\n\n    In general, right-shifting an integer in small increments produces the same\n    result as shifting it in one step:\n\n    .. code-block:: edgeql-repl\n\n        db> select bit_rshift(bit_rshift(123, 1), 3);\n        {7}\n        db> select bit_rshift(123, 4);\n        {7}\n        db> select bit_rshift(bit_rshift(-123, 1), 3);\n        {-8}\n        db> select bit_rshift(-123, 4);\n        {-8}\n\n    It is an error to attempt to shift by a negative number of bits:\n\n    .. code-block:: edgeql-repl\n\n        db> select bit_rshift(123, -2);\n        gel error: InvalidValueError: bit_rshift(): cannot shift by\n        negative amount\n\n\n------------\n\n\n.. eql:function:: std::bit_count(val: int16) -> int64\n                  std::bit_count(val: int32) -> int64\n                  std::bit_count(val: int64) -> int64\n                  std::bit_count(bytes: bytes) -> int64\n\n    Return the number of bits set in the :eql:type:`bytes` value.\n\n    This is also known as the population count.\n\n    .. code-block:: edgeql-repl\n\n        db> select bit_count(255);\n        {8}\n        db> select bit_count(b'\\xff\\xff');\n        {16}\n\n\n------------\n\n\n.. eql:function:: std::to_bigint(s: str, fmt: optional str={}) -> bigint\n\n    .. index:: parse bigint\n\n    Returns a :eql:type:`bigint` value parsed from the given string.\n\n    The function will use an optional format string passed as *fmt*. See the\n    :ref:`number formatting options <ref_std_converters_number_fmt>` for help\n    writing a format string.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_bigint('-000,012,345', 'S099,999,999,999');\n        {-12345n}\n        db> select to_bigint('31st', '999th');\n        {31n}\n\n\n------------\n\n\n\n.. eql:function:: std::to_decimal(s: str, fmt: optional str={}) -> decimal\n\n    .. index:: parse decimal\n\n    Returns a :eql:type:`decimal` value parsed from the given string.\n\n    The function will use an optional format string passed as *fmt*. See the\n    :ref:`number formatting options <ref_std_converters_number_fmt>` for help\n    writing a format string.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_decimal('-000,012,345', 'S099,999,999,999');\n        {-12345.0n}\n        db> select to_decimal('-012.345');\n        {-12.345n}\n        db> select to_decimal('31st', '999th');\n        {31.0n}\n\n\n------------\n\n\n.. eql:function:: std::to_int16(s: str, fmt: optional str={}) -> int16\n                  std::to_int16(val: bytes, endian: Endian) -> int16\n\n    .. index:: parse int16\n\n    Returns an :eql:type:`int16` value parsed from the given input.\n\n    The string parsing function will use an optional format string passed as\n    *fmt*. See the :ref:`number formatting options\n    <ref_std_converters_number_fmt>` for help writing a format string.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_int16('23');\n        {23}\n        db> select to_int16('23%', '99%');\n        {23}\n\n    The bytes conversion function expects exactly 2 bytes with specified\n    endianness.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_int16(b'\\x00\\x07', Endian.Big);\n        {7}\n        db> select to_int16(b'\\x07\\x00', Endian.Little);\n        {7}\n\n    .. note::\n\n        Due to underlying implementation details using big-endian encoding\n        results in slightly faster performance of ``to_int16``.\n\n\n------------\n\n\n.. eql:function:: std::to_int32(s: str, fmt: optional str={}) -> int32\n                  std::to_int32(val: bytes, endian: Endian) -> int32\n\n    .. index:: parse int32\n\n    Returns an :eql:type:`int32` value parsed from the given input.\n\n    The string parsin function will use an optional format string passed as\n    *fmt*. See the :ref:`number formatting options\n    <ref_std_converters_number_fmt>` for help writing a format string.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_int32('1000023');\n        {1000023}\n        db> select to_int32('1000023%', '9999999%');\n        {1000023}\n\n    The bytes conversion function expects exactly 4 bytes with specified\n    endianness.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_int32(b'\\x01\\x02\\x00\\x07', Endian.Big);\n        {16908295}\n        db> select to_int32(b'\\x07\\x00\\x02\\x01', Endian.Little);\n        {16908295}\n\n    .. note::\n\n        Due to underlying implementation details using big-endian encoding\n        results in slightly faster performance of ``to_int32``.\n\n\n------------\n\n\n.. eql:function:: std::to_int64(s: str, fmt: optional str={}) -> int64\n                  std::to_int64(val: bytes, endian: Endian) -> int64\n\n    .. index:: parse int64\n\n    Returns an :eql:type:`int64` value parsed from the given input.\n\n    The string parsing function will use an optional format string passed as\n    *fmt*. See the :ref:`number formatting options\n    <ref_std_converters_number_fmt>` for help writing a format string.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_int64('10000234567');\n        {10000234567}\n        db> select to_int64('10000234567%', '99999999999%');\n        {10000234567}\n\n    The bytes conversion function expects exactly 8 bytes with specified\n    endianness.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_int64(b'\\x01\\x02\\x00\\x07\\x11\\x22\\x33\\x44',\n        ...                 Endian.Big);\n        {72620574343574340}\n        db> select to_int64(b'\\x44\\x33\\x22\\x11\\x07\\x00\\x02\\x01',\n        ...                 Endian.Little);\n        {72620574343574340}\n\n    .. note::\n\n        Due to underlying implementation details using big-endian encoding\n        results in slightly faster performance of ``to_int64``.\n\n\n------------\n\n\n.. eql:function:: std::to_float32(s: str, fmt: optional str={}) -> float32\n\n    .. index:: parse float32\n\n    Returns a :eql:type:`float32` value parsed from the given string.\n\n    The function will use an optional format string passed as *fmt*. See the\n    :ref:`number formatting options <ref_std_converters_number_fmt>` for help\n    writing a format string.\n\n\n------------\n\n\n.. eql:function:: std::to_float64(s: str, fmt: optional str={}) -> float64\n\n    .. index:: parse float64\n\n    Returns a :eql:type:`float64` value parsed from the given string.\n\n    The function will use an optional format string passed as *fmt*. See the\n    :ref:`number formatting options <ref_std_converters_number_fmt>` for help\n    writing a format string.\n"
  },
  {
    "path": "docs/reference/stdlib/objects.rst",
    "content": ".. _ref_std_object_types:\n\n============\nBase Objects\n============\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:type:`BaseObject`\n      - Root object type\n\n    * - :eql:type:`Object`\n      - Root for user-defined object types\n\n\n``std::BaseObject`` is the root of the object type hierarchy and all object\ntypes in Gel, including system types, extend it either directly or\nindirectly.  User-defined object types extend from :eql:type:`std::Object`\ntype, which is a subtype of ``std::BaseObject``.\n\n\n---------\n\n\n.. eql:type:: std::BaseObject\n\n    The root object type.\n\n    Definition:\n\n    .. code-block:: sdl\n\n        abstract type std::BaseObject {\n            # Universally unique object identifier\n            required id: uuid {\n                default := (select std::uuid_generate_v1mc());\n                readonly := true;\n                constraint exclusive;\n            }\n\n            # Object type in the information schema.\n            required readonly __type__: schema::ObjectType;\n        }\n\n    Subtypes may override the ``id`` property, but only with a valid UUID\n    generation function. Currently, these are :eql:func:`uuid_generate_v1mc`\n    and :eql:func:`uuid_generate_v4`.\n\n\n---------\n\n\n.. eql:type:: std::Object\n\n    The root object type for user-defined types.\n\n    Definition:\n\n    .. code-block:: sdl\n\n        abstract type std::Object extending std::BaseObject;\n"
  },
  {
    "path": "docs/reference/stdlib/pg_trgm.rst",
    "content": ".. versionadded:: 4.0\n\n.. _ref_ext_pgtrgm:\n\n============\next::pg_trgm\n============\n\nThis extension provides tools for determining similarity of text based on\ntrigram matching.\n\nWord similarity tools can often supplement :ref:`full-text search\n<ref_std_fts>`. Full-text search concentrates on matching words and phrases\ntypically trying to account for some grammatical variations, while trigram\nmatching analyzes similarity between words. Thus trigram matching can account\nfor misspelling or words that aren't dictionary words:\n\n.. code-block:: edgeql-repl\n\n  db> select fts::search(Doc, 'thaco').object{text};\n  {}\n\n  db> select Doc{text} filter ext::pg_trgm::word_similar('thaco', Doc.text);\n  {\n    default::Doc {\n      text: 'THAC0 is used in AD&D 2 to determine likelihood of hitting',\n    },\n  }\n\nThe first search attempt fails to produce results because \"THAC0\" is an\nobscure acronym that is misspelled in the query. However, using similarity\nsearch produces a hit because the acronym is not too badly misspelled and is\nclose enough.\n\nThe Postgres that comes packaged with |Gel| (since |EdgeDB| 4.0+) server\nincludes ``pg_trgm``, as does Gel Cloud. It you are using a separate\nPostgres backend, you will need to arrange for it to be installed.\n\nTo activate this functionality you can use the :ref:`extension\n<ref_datamodel_extensions>` mechanism:\n\n.. code-block:: sdl\n\n    using extension pg_trgm;\n\nThat will give you access to the ``ext::pg_trgm`` module where you may find\nthe following functions:\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::pg_trgm::similarity`\n      - :eql:func-desc:`ext::pg_trgm::similarity`\n\n    * - :eql:func:`ext::pg_trgm::similarity_dist`\n      - :eql:func-desc:`ext::pg_trgm::similarity_dist`\n\n    * - :eql:func:`ext::pg_trgm::similar`\n      - :eql:func-desc:`ext::pg_trgm::similar`\n\n    * - :eql:func:`ext::pg_trgm::word_similarity`\n      - :eql:func-desc:`ext::pg_trgm::word_similarity`\n\n    * - :eql:func:`ext::pg_trgm::word_similarity_dist`\n      - :eql:func-desc:`ext::pg_trgm::word_similarity_dist`\n\n    * - :eql:func:`ext::pg_trgm::word_similar`\n      - :eql:func-desc:`ext::pg_trgm::word_similar`\n\n    * - :eql:func:`ext::pg_trgm::strict_word_similarity`\n      - :eql:func-desc:`ext::pg_trgm::strict_word_similarity`\n\n    * - :eql:func:`ext::pg_trgm::strict_word_similarity_dist`\n      - :eql:func-desc:`ext::pg_trgm::strict_word_similarity_dist`\n\n    * - :eql:func:`ext::pg_trgm::strict_word_similar`\n      - :eql:func-desc:`ext::pg_trgm::strict_word_similar`\n\n\nIn addition to the functions this extension has two indexes that speed up\nqueries that involve similarity searches: ``ext::pg_trgm::gin`` and\n``ext::pg_trgm::gist``.\n\n\n.. _ref_ext_pgtrgm_config:\n\nConfiguration\n^^^^^^^^^^^^^\n\nThis extension also adds a few configuration options to control some of the\nsimilarity search behavior:\n\n.. code-block:: sdl\n\n    type Config extending cfg::ConfigObject {\n      required similarity_threshold: float32;\n      required word_similarity_threshold: float32;\n      required strict_word_similarity_threshold: float32;\n    }\n\nAll of the configuration parameters have to take values between 0 and 1.\n\nThe ``similarity_threshold`` sets the current similarity threshold that is\nused by :eql:func:`ext::pg_trgm::similar` (default is 0.3).\n\nThe ``word_similarity_threshold`` sets the current word similarity threshold\nthat is used by :eql:func:`ext::pg_trgm::word_similar` (default is 0.6).\n\nThe ``strict_word_similarity_threshold`` sets the current strict word\nsimilarity threshold that is used by\n:eql:func:`ext::pg_trgm::strict_word_similar` (default is 0.5).\n\n\n------------\n\n\n.. eql:function:: ext::pg_trgm::similarity(a: str, b: str) -> float32\n\n    Computes how similar two strings are.\n\n    The result is always a value between 0 and 1, where 0 indicates no\n    similarity and 1 indicates the strings are identical.\n\n    .. code-block:: edgeql-repl\n\n      db> select ext::pg_trgm::similarity('cat', 'dog');\n      {0}\n      db> select ext::pg_trgm::similarity('cat', 'cart');\n      {0.28571427}\n      db> select ext::pg_trgm::similarity('cat', 'car');\n      {0.33333337}\n      db> select ext::pg_trgm::similarity('cat', 'cat');\n      {1}\n\n\n------------\n\n\n.. eql:function:: ext::pg_trgm::similarity_dist(a: str, b: str) -> float32\n\n    Computes how distant two strings are.\n\n    The distance between *a* and *b* is simply defined as ``1 -\n    ext::pg_trgm::similarity(a, b)``.\n\n    .. code-block:: edgeql-repl\n\n      db> select ext::pg_trgm::similarity_dist('cat', 'dog');\n      {1}\n      db> select ext::pg_trgm::similarity_dist('cat', 'cart');\n      {0.71428573}\n      db> select ext::pg_trgm::similarity_dist('cat', 'car');\n      {0.6666666}\n      db> select ext::pg_trgm::similarity_dist('cat', 'cat');\n      {0}\n\n\n------------\n\n\n.. eql:function:: ext::pg_trgm::similar(a: str, b: str) -> bool\n\n    Returns whether two strings are similar.\n\n    The result is ``true`` if the :eql:func:`ext::pg_trgm::similarity` between\n    the two strings is greater than the currently configured\n    :ref:`similarity_threshold <ref_ext_pgtrgm_config>`.\n\n    .. code-block:: edgeql-repl\n\n      db> select ext::pg_trgm::similar('cat', 'dog');\n      {false}\n      db> select ext::pg_trgm::similar('cat', 'cart');\n      {false}\n      db> select ext::pg_trgm::similar('cat', 'car');\n      {true}\n      db> select ext::pg_trgm::similar('cat', 'cat');\n      {true}\n\n\n------------\n\n\n.. eql:function:: ext::pg_trgm::word_similarity(a: str, b: str) -> float32\n\n    Returns similarity between the first and any part of the second string.\n\n    The result is the greatest similarity between the set of\n    trigrams in *a* and any continuous extent of an ordered set\n    of trigrams in *b*.\n\n    .. code-block:: edgeql-repl\n\n      db> select ext::pg_trgm::word_similarity('cat', 'Lazy dog');\n      {0}\n      db> select ext::pg_trgm::word_similarity('cat', 'Dog in a car');\n      {0.5}\n      db> select ext::pg_trgm::word_similarity('cat', 'Dog catastrophe');\n      {0.75}\n      db> select ext::pg_trgm::word_similarity('cat', 'Lazy dog and cat');\n      {1}\n\n\n------------\n\n\n.. eql:function:: ext::pg_trgm::word_similarity_dist(a: str, b: str) \\\n                    -> float32\n\n    Returns distance between the first and any part of the second string.\n\n    The distance between *a* and *b* is simply defined as ``1 -\n    ext::pg_trgm::word_similarity(a, b)``.\n\n    .. code-block:: edgeql-repl\n\n      db> select ext::pg_trgm::word_similarity_dist('cat', 'Lazy dog');\n      {1}\n      db> select ext::pg_trgm::word_similarity_dist('cat', 'Dog in a car');\n      {0.5}\n      db> select ext::pg_trgm::word_similarity_dist('cat', 'Dog catastrophe');\n      {0.25}\n      db> select ext::pg_trgm::word_similarity_dist('cat', 'Lazy dog and cat');\n      {0}\n\n\n------------\n\n\n.. eql:function:: ext::pg_trgm::word_similar(a: str, b: str) -> bool\n\n    Returns whether the first string is similar to any part of the second.\n\n    The result is ``true`` if the :eql:func:`ext::pg_trgm::word_similarity`\n    between the two strings is greater than the currently configured\n    :ref:`word_similarity_threshold <ref_ext_pgtrgm_config>`.\n\n    .. code-block:: edgeql-repl\n\n      db> select ext::pg_trgm::word_similar('cat', 'Lazy dog');\n      {false}\n      db> select ext::pg_trgm::word_similar('cat', 'Dog in a car');\n      {false}\n      db> select ext::pg_trgm::word_similar('cat', 'Dog catastrophe');\n      {true}\n      db> select ext::pg_trgm::word_similar('cat', 'Lazy dog and cat');\n      {true}\n\n\n------------\n\n\n.. eql:function:: ext::pg_trgm::strict_word_similarity(a: str, b: str) \\\n                    -> float32\n\n    Same as ``word_similarity``, but with stricter boundaries.\n\n    This works much like :eql:func:`ext::pg_trgm::word_similarity`, but also\n    forces the match within *b* to happen at word boundaries.\n\n    .. code-block:: edgeql-repl\n\n      db> select ext::pg_trgm::strict_word_similarity('cat', 'Lazy dog');\n      {0}\n      db> select ext::pg_trgm::strict_word_similarity('cat', 'Dog in a car');\n      {0.5}\n      db> select ext::pg_trgm::strict_word_similarity(\n      ...   'cat', 'Dog catastrophy');\n      {0.23076922}\n      db> select ext::pg_trgm::strict_word_similarity(\n      ...   'cat', 'Lazy dog and cat');\n      {1}\n\n\n------------\n\n\n.. eql:function:: ext::pg_trgm::strict_word_similarity_dist(a: str, b: str) \\\n                    -> float32\n\n    Same as ``word_similarity_dist``, but with stricter boundaries.\n\n    This works much like :eql:func:`ext::pg_trgm::word_similarity_dist`, but\n    also forces the match within *b* to happen at word boundaries.\n\n    .. code-block:: edgeql-repl\n\n      db> select ext::pg_trgm::strict_word_similarity_dist(\n      ...   'cat', 'Lazy dog');\n      {1}\n      db> select ext::pg_trgm::strict_word_similarity_dist(\n      ...   'cat', 'Dog in a car');\n      {0.5}\n      db> select ext::pg_trgm::strict_word_similarity_dist(\n      ...   'cat', 'Dog catastrophy');\n      {0.7692308}\n      db> select ext::pg_trgm::strict_word_similarity_dist(\n      ...   'cat', 'Lazy dog and cat');\n      {0}\n\n\n------------\n\n\n.. eql:function:: ext::pg_trgm::strict_word_similar(a: str, b: str) -> bool\n\n    Same as ``word_similar``, but with stricter boundaries.\n\n    This works much like :eql:func:`ext::pg_trgm::word_similar`, but\n    also forces the match within *b* to happen at word boundaries.\n\n    .. code-block:: edgeql-repl\n\n      db> select ext::pg_trgm::strict_word_similar(\n      ...   'cat', 'Lazy dog');\n      {false}\n      db> select ext::pg_trgm::strict_word_similar(\n      ...   'cat', 'Lazy dog');\n      {false}\n      db> select ext::pg_trgm::strict_word_similar(\n      ...   'cat', 'Dog catastrophy');\n      {false}\n      db> select ext::pg_trgm::strict_word_similar(\n      ...   'cat', 'Lazy dog and cat');\n      {true}\n"
  },
  {
    "path": "docs/reference/stdlib/pg_unaccent.rst",
    "content": ".. versionadded:: 4.0\n\n.. _ref_ext_pgunaccent:\n\n================\next::pg_unaccent\n================\n\nThis extension provides a dictionary for removing accents (diacritic signs) from\ntext.\n\n.. code-block:: edgeql-repl\n\n  db> select ext::pg_unaccent::unaccent('Hôtel de la Mer');\n  {'Hotel de la Mer'}\n\nTo activate this functionality you can use the :ref:`extension\n<ref_datamodel_extensions>` mechanism:\n\n.. code-block:: sdl\n\n    using extension pg_unaccent;\n\nThat will give you access to the ``ext::pg_unaccent`` module where you may find\nthe function ``unaccent``.\n\nPostgreSQL extension ``unaccent`` also supports creating dictionaries that are\nused by other PostgreSQL text search, such as ``to_tsvector``. Gel extension\ncurrently does not support creating such dictionaries, but one can use the\n``unaccent`` function to achieve the same effect:\n\n.. code-block:: sdl\n\n    type Post {\n        title: str;\n\n        index fts::index on ((\n            fts::with_options(\n                ext::pg_unaccent::unaccent(.title),\n                language := fts::Language.fra\n            ),\n        ));\n    };\n\n.. code-block:: edgeql-repl\n\n  db> select fts::search(\n  ...   Post,\n  ...   ext::pg_unaccent::unaccent('Hôtel'),\n  ...   language := 'eng',\n  ... ).object.title;\n  {'Hôtel de la Mer'}\n"
  },
  {
    "path": "docs/reference/stdlib/pgcrypto.rst",
    "content": ".. versionadded:: 4.0\n\n.. _ref_ext_pgcrypto:\n\n=============\next::pgcrypto\n=============\n\nThis extension provides tools for your hashing and encrypting needs.\n\nThe Postgres that comes packaged with the |Gel| (since |EdgeDB| 4.0) server\nincludes ``pgcrypto``, as does Gel Cloud. It you are using a separate\nPostgres backend, you will need to arrange for it to be installed.\n\nTo activate this functionality you can use the :ref:`extension\n<ref_datamodel_extensions>` mechanism:\n\n.. code-block:: sdl\n\n    using extension pgcrypto;\n\nThat will give you access to the ``ext::pgcrypto`` module where you may find\nthe following functions:\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::pgcrypto::digest`\n      - :eql:func-desc:`ext::pgcrypto::digest`\n\n    * - :eql:func:`ext::pgcrypto::hmac`\n      - :eql:func-desc:`ext::pgcrypto::hmac`\n\n    * - :eql:func:`ext::pgcrypto::gen_salt`\n      - :eql:func-desc:`ext::pgcrypto::gen_salt`\n\n    * - :eql:func:`ext::pgcrypto::crypt`\n      - :eql:func-desc:`ext::pgcrypto::crypt`\n\n\n------------\n\n\n.. eql:function:: ext::pgcrypto::digest(data: str, type: str) -> bytes\n                  ext::pgcrypto::digest(data: bytes, type: str) -> bytes\n\n    Computes a hash of the *data* using the specified algorithm.\n\n    The *data* may come as a :eql:type:`str` or :eql:type:`bytes`. The value\n    of *type* argument determines the hashing algorithm that will be used.\n    Valid algorithms are: ``md5``, ``sha1``, ``sha224``, ``sha256``,\n    ``sha384`` and ``sha512``. Also, any digest algorithm OpenSSL supports is\n    automatically picked up as well.\n\n    The result is always a binary hash.\n\n    .. code-block:: edgeql-repl\n\n      db> select ext::pgcrypto::digest('encrypt this', 'sha1');\n      {b'\\x05\\x82\\xd8YLF\\xe7\\xd4\\x12\\x91\\n\\xdb$\\xf1!v\\xf9\\xd4\\x89\\xc4'}\n      db> select ext::pgcrypto::digest(b'encrypt this', 'md5');\n      {b'\\x15\\xd6\\x14y\\xcb\\xf2\"\\xa1+Z]8\\xf8\\xcf\\x0c['}\n\n\n------------\n\n\n.. eql:function:: ext::pgcrypto::hmac(data: str, key: str, type: str) \\\n                    -> bytes\n                  ext::pgcrypto::hmac(data: bytes, key: bytes, type: str) \\\n                    -> bytes\n\n    Computes a hashed MAC for *data* using *key* and the specified algorithm.\n\n    The *data* may come as a :eql:type:`str` or :eql:type:`bytes`. The *key*\n    type must match the *data* type. The value of *type* argument determines\n    the hashing algorithm that will be used. Valid algorithms are: ``md5``,\n    ``sha1``, ``sha224``, ``sha256``, ``sha384`` and ``sha512``. Also, any\n    digest algorithm OpenSSL supports is automatically picked up as well.\n\n    The result is always a binary hash.\n\n    The main difference between :eql:func:`ext::pgcrypto::digest` and this\n    function is that it's impossible to recalculate the hash without the key.\n\n    .. code-block:: edgeql-repl\n\n      db> select ext::pgcrypto::hmac('encrypt this', 'my key', 'sha1');\n      {b'\\x01G\\x12\\xb7\\xe76H\\x8b\\xa4T1\\x0fj\\x87\\xdf\\x86n\\x8f\\xed\\x15'}\n      db> select ext::pgcrypto::hmac(b'encrypt this', b'my key', 'md5');\n      {b'\\xa9{\\xc7\\x9e\\xc9\"7e\\xab\\x83\\xeb\\x0c\\xde\\x02Nn'}\n\n\n------------\n\n\n.. eql:function:: ext::pgcrypto::gen_salt() -> str\n                  ext::pgcrypto::gen_salt(type: str) -> str\n                  ext::pgcrypto::gen_salt(type: str, iter_count: int64) -> str\n\n    Generates a new random salt string.\n\n    When generating the salt string *type* may be specified. Valid salt types\n    are: ``des``, ``xdes``, ``md5``, and ``bf`` (default).\n\n    .. code-block:: edgeql-repl\n\n      db> select ext::pgcrypto::gen_salt();\n      {'$2a$06$5D2rBj3UY5/UYvPIUNILvu'}\n      db> select ext::pgcrypto::gen_salt('des');\n      {'o9'}\n      db> select ext::pgcrypto::gen_salt('xdes');\n      {'_J9..efC8'}\n\n    The *iter_count* specifies the number of iterations for algorithms that\n    allow iterations (``xdes`` and ``bf``). The ``xdes`` algorithm has an\n    additional requirement that *iter_count* must be odd. The higher the\n    iteration count the longer it takes to compute the hash and therefore it\n    also takes longer to break the encryption. However, if the count is too\n    high, it can take impractically long.\n\n    .. code-block:: edgeql-repl\n\n      db> select ext::pgcrypto::gen_salt('bf', 10);\n      {'$2a$10$fAQS9/UKS42OI.ftjHkj2O'}\n      db> select ext::pgcrypto::gen_salt('xdes', 5);\n      {'_3...oN2c'}\n\n\n------------\n\n\n.. eql:function:: ext::pgcrypto::crypt(password: str, salt: str) -> str\n\n    Calculates a crypt(3)-style hash of password.\n\n    Typically you would use :eql:func:`ext::pgcrypto::gen_salt` to generate a\n    salt value for a new password:\n\n    .. code-block:: edgeql-repl\n\n      db> with module ext::pgcrypto\n      ... select crypt('new password', gen_salt('des'));\n      {'0ddkJUiOnUFq6'}\n\n    To check the password against a stored encrypted value use the hash value\n    itself as salt and see if the result matches:\n\n    .. code-block:: edgeql-repl\n\n      db> with hash := '0ddkJUiOnUFq6'\n      ... select hash = ext::pgcrypto::crypt(\n      ...   'new password',\n      ...   hash,\n      ... );\n      {true}\n"
  },
  {
    "path": "docs/reference/stdlib/pgvector.rst",
    "content": ".. _ref_ext_pgvector:\n\n=============\next::pgvector\n=============\n\nThis can be used to store and efficiently retrieve text embeddings,\nsuch as those produced by OpenAI.\n\nThe Postgres that comes packaged with the Gel server includes\n``pgvector``, as does Gel Cloud. It you are using a separate\nPostgres backend, you will need to arrange for it to be installed.\n\nTo activate this new functionality you can use the :ref:`extension\n<ref_datamodel_extensions>` mechanism:\n\n.. code-block:: sdl\n\n    using extension pgvector;\n\nThat will give you access to the ``ext::pgvector`` module where you may find\nthe ``ext::pgvector::vector`` type as well as the following functions:\n\n* ``euclidean_distance(a: vector, b: vector) -> std::float64``\n* ``neg_inner_product(a: vector, b: vector) -> std::float64``\n* ``cosine_distance(a: vector, b: vector) -> std::float64``\n* ``euclidean_norm(a: vector, b: vector) -> std::float64``\n\nYou also get access to the following three indexes, each corresponding to one\nof the vector distance functions:\n\n* ``index ivfflat_euclidean(named only lists: int64)``\n* ``index ivfflat_ip(named only lists: int64)``\n* ``index ivfflat_cosine(named only lists: int64)``\n\n.. versionadded:: 5.0\n\n    ``ext::pgvector`` now also includes Hierarchical Navigable Small Worlds\n    (HNSW) indexes:\n\n    * ``index hnsw_euclidean``\n    * ``index hnsw_ip``\n    * ``index hnsw_cosine``\n\nWhen defining a new type, you can now add vector properties. However, in order\nto be able to use indexes, the vectors in question need to be of fixed\nlength. This can be achieved by creating a custom scalar ``extending`` the\nvector and specifying the desired length in angle brackets:\n\n.. code-block:: sdl\n\n    scalar type v3 extending ext::pgvector::vector<3>;\n\n    type Item {\n        embedding: v3\n    }\n\nTo populate your data, you can cast an array of any of the numeric types into\n``ext::pgvector::vector`` or simply assign that array directly:\n\n.. code-block:: edgeql-repl\n\n    gel> insert Item {embedding := <v3>[1.2, 3, 4.5]};\n    {default::Item {id: f119d64e-0995-11ee-8804-ff8cd739d8b7}}\n    gel> insert Item {embedding := [-0.1, 7, 0]};\n    {default::Item {id: f410c844-0995-11ee-8804-176f28167dd1}}\n\nYou can also cast the vectors into an ``array<float32>>``:\n\n.. code-block:: edgeql-repl\n\n    gel> select <array<float32>>Item.embedding;\n    {[1.2, 3, 4.5], [-0.1, 7, 0]}\n\nYou can query the nearest neighbour by ordering based on\n``euclidean_distance``:\n\n.. code-block:: edgeql-repl\n\n  gel> select Item {*}\n  .... order by ext::pgvector::euclidean_distance(\n  ....   .embedding, <v3>[3, 1, 2])\n  .... empty last\n  .... limit 1;\n  {\n    default::Item {\n      id: f119d64e-0995-11ee-8804-ff8cd739d8b7,\n      embedding: [1.2, 3, 4.5],\n    },\n  }\n\nYou can also just retrieve all results within a certain distance:\n\n.. code-block:: edgeql-repl\n\n  gel> select Item {*}\n  .... filter ext::pgvector::euclidean_distance(\n  ....   .embedding, <v3>[3, 1, 2]) < 5;\n  {\n    default::Item {\n      id: f119d64e-0995-11ee-8804-ff8cd739d8b7,\n      embedding: [1.2, 3, 4.5],\n    },\n  }\n\nThe functions mentioned earlier can be used to calculate various useful vector\ndistances:\n\n.. code-block:: edgeql-repl\n\n  gel> select Item {\n  ....   id,\n  ....   distance := ext::pgvector::euclidean_distance(\n  ....     .embedding, <v3>[3, 1, 2]),\n  ....   inner_product := -ext::pgvector::neg_inner_product(\n  ....     .embedding, <v3>[3, 1, 2]),\n  ....   cosine_similarity := 1 - ext::pgvector::cosine_distance(\n  ....     .embedding, <v3>[3, 1, 2]),\n  .... };\n  {\n    default::Item {\n      id: f119d64e-0995-11ee-8804-ff8cd739d8b7,\n      distance: 3.6728735110725803,\n      inner_product: 15.600000143051147,\n      cosine_similarity: 0.7525964057358976,\n    },\n    default::Item {\n      id: f410c844-0995-11ee-8804-176f28167dd1,\n      distance: 7.043436619202443,\n      inner_product: 6.699999988079071,\n      cosine_similarity: 0.2557810894509498,\n    },\n  }\n\nTo speed up queries three slightly different IVFFlat indexes can be added to\nthe type, each of them optimizing one of the distance calculating functions:\n\n.. code-block:: sdl\n\n    type Item {\n        embedding: v3;\n\n      index ext::pgvector::ivfflat_euclidean(lists := 10) on (.embedding);\n      index ext::pgvector::ivfflat_ip(lists := 10) on (.embedding);\n      index ext::pgvector::ivfflat_cosine(lists := 10) on (.embedding);\n    }\n\nIn order to take advantage of an index, your query must:\n\n1) Use ``order by`` using the function that corresponds to the index\n2) Specify ``empty last`` as part of the ``order by`` clause\n3) Provide a ``limit`` clause specifying how many results to return\n\nNote that unlike normal indexes, hitting an IVFFlat index changes the\nquery behavior: it does a (hopefully fast) approximate search instead\nof (usually slow) exact one.\n\nAs per the `pgvector <pgvector_>`_ recommendations, the keys to achieving good\nrecall are:\n\n1) Create the index after the table has some data\n2) Choose an appropriate number of lists - a good place to start is objects /\n   1000 for up to 1M objects and sqrt(objects) for over 1M objects\n3) When querying, specify an appropriate number of probes (higher is better\n   for recall, lower is better for speed) - a good place to start is sqrt(\n   lists). The number of probes can be set by ``ext::pgvector::set_probes()``\n   function.\n\nUse our newly introduced ``analyze`` feature to debug query performance and\nmake sure that the indexes are being used.\n\nThe ``ext::pgvector::set_probes()`` function configures the number of\nprobes to use in approximate index searches. It is scoped to the\ncurrent transaction, so if you call it from within a transaction, it\npersists until the transaction is finished. The recommended way to use\nit, however, is to take advantage of the implicit transactions provided\nby multi-statement queries:\n\n\n.. code-block:: python\n\n  result = client.query(\"\"\"\n      select set_probes(10);\n      select Item { id, name }\n      order by ext::pgvector::euclidean_distance(\n\t.embedding, <v3>$vector)\n      empty last\n      limit 1;\n  \"\"\", vector=vector)\n\n\n.. versionadded:: 5.0\n\n    We have updated the mechanism for tuning all of the indexes provided in\n    this extension. The ``probes`` (for IVFFlat) and ``ef_search`` (for HNSW)\n    parameters can now be accessed via the ``ext::pgvector::Config`` object.\n\n    Examine the ``extensions`` link of the ``cfg::Config`` object to check the\n    current config values:\n\n    .. code-block:: edgeql-repl\n\n        db> select cfg::Config.extensions[is ext::pgvector::Config]{*};\n        {\n          ext::pgvector::Config {\n            id: 12b5c70f-0bb8-508a-845f-ca3d41103b6f,\n            probes: 1,\n            ef_search: 40,\n          },\n        }\n\n    .. note::\n\n        In order to see the specific extension config properties you need to\n        use the type filter :eql:op:`[is ext::pgvector::Config] <isintersect>`\n\n    Update the value using the ``configure session`` or the ``configure current\n    branch`` command depending on the scope you prefer:\n\n    .. code-block:: edgeql-repl\n\n        db> configure session\n        ... set ext::pgvector::Config::probes := 5;\n        OK: CONFIGURE SESSION\n\n    You may also restore the default config value using ``configure session\n    reset`` if you set it on the session or ``configure current branch reset``\n    if you set it on the branch:\n\n    .. code-block:: edgeql-repl\n\n        db> configure session reset ext::pgvector::Config::probes;\n        OK: CONFIGURE SESSION\n\n\n\n.. _pgvector:\n    https://github.com/pgvector/pgvector\n"
  },
  {
    "path": "docs/reference/stdlib/postgis.rst",
    "content": ".. versionadded:: 6.0\n\n.. _ref_ext_postgis:\n\n============\next::postgis\n============\n\nThis extension exposes the functionality of the `PostGIS <postgis_>`_ library. It is a vast library dedicated to handling geographic and various geometric data. The scope of the EdgeDB extension is to mainly adapt the types and functions used in this library with minimal changes.\n\nAs a rule, many of the functions in PostGIS library have a ``ST_``` prefix, however, we omitted it since in EdgeDB all these functions would already be in the ``ext::postgis`` namespace and additional disambiguation is unnecessary.\n\n\nTypes\n=====\n\nThere are four basic scalar types introduced by this extension:\n\n----------\n\n\n.. eql:type:: postgis::geometry\n\n    The type representing 2- or 3-dimensional spatial features.\n\n    By default most of the ``geometry`` values are assumed to be representing planar geometry in a Cartesian coordinate system.\n\n    Every other ``ext::postgis`` scalar type is castable into ``geometry``. Many of the PostGIS functions only accept ``geometry`` as input.\n\n\n----------\n\n\n.. eql:type:: postgis::geography\n\n    The type representing spatial features with geodetic coordinate systems.\n\n    The PostGIS ``geography`` data type provides native support for spatial features represented on \"geographic\" coordinates (sometimes called \"geodetic\" coordinates, or \"lat/lon\", or \"lon/lat\"). Geographic coordinates are spherical coordinates expressed in angular units (degrees).\n\n\n----------\n\n\n.. eql:type:: postgis::box2d\n\n    The type representing a 2-dimensional bounding box.\n\n\n----------\n\n\n.. eql:type:: postgis::box3d\n\n    The type representing a 3-dimensional bounding box.\n\n\nOperators\n=========\n\nThere are many functions available for processing all this geometric and geographic data. Of note are the functions that represent *operations* affected by the indexes (``pg::gist``, ``pg::brin``, and ``pg::spgist``). These functions all have a ``op_`` prefix to help identify them.\n\n----------\n\n\n.. eql:function:: ext::postgis::op_above( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``|>>`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_below( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``<<|`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_contained_3d( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``<<@`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_contains( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``~`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_contains_2d( \\\n                    a: ext::postgis::box2d, \\\n                    b: ext::postgis::box2d, \\\n                  ) ->  std::bool\n                  ext::postgis::op_contains_2d( \\\n                    a: ext::postgis::box2d, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n                  ext::postgis::op_contains_2d( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::box2d, \\\n                  ) ->  std::bool\n\n    This is exposing the ``~`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_contains_3d( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``@>>`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_contains_nd( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``~~`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_distance_box( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::float64\n\n    This is exposing the ``<#>`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_distance_centroid( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::float64\n\n    This is exposing the ``<->`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_distance_centroid_nd( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::float64\n\n    This is exposing the ``<<->>`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_distance_cpa( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::float64\n\n    This is exposing the ``|=|`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_distance_knn( \\\n                    a: ext::postgis::geography, \\\n                    b: ext::postgis::geography, \\\n                  ) ->  std::float64\n\n    This is exposing the ``<->`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_is_contained_2d( \\\n                    a: ext::postgis::box2d, \\\n                    b: ext::postgis::box2d, \\\n                  ) ->  std::bool\n                  ext::postgis::op_is_contained_2d( \\\n                    a: ext::postgis::box2d, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n                  ext::postgis::op_is_contained_2d( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::box2d, \\\n                  ) ->  std::bool\n\n    This is exposing the ``@`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_left( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``<<`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_neq( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``<>`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_overabove( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``|&>`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_overbelow( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``&<|`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_overlaps( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n                  ext::postgis::op_overlaps( \\\n                    a: ext::postgis::geography, \\\n                    b: ext::postgis::geography, \\\n                  ) ->  std::bool\n\n    This is exposing the ``&&`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_overlaps_2d( \\\n                    a: ext::postgis::box2d, \\\n                    b: ext::postgis::box2d, \\\n                  ) ->  std::bool\n                  ext::postgis::op_overlaps_2d( \\\n                    a: ext::postgis::box2d, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n                  ext::postgis::op_overlaps_2d( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::box2d, \\\n                  ) ->  std::bool\n\n    This is exposing the ``&&`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_overlaps_3d( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``&/&`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_overlaps_nd( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``&&&`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_overleft( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``&<`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_overright( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``&>`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_right( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``>>`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_same( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``~=`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_same_3d( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``~==`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_same_nd( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``~~=`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_within( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``@`` operator.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::op_within_nd( \\\n                    a: ext::postgis::geometry, \\\n                    b: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing the ``@@`` operator.\n\n\nFunctions\n=========\n\nThe core functions can be roughly grouped into the following categories.\n\nGeometry Constructors\n---------------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::collect`\n      - :eql:func-desc:`ext::postgis::collect`\n\n    * - :eql:func:`ext::postgis::hexagon`\n      - :eql:func-desc:`ext::postgis::hexagon`\n\n    * - :eql:func:`ext::postgis::linefrommultipoint`\n      - :eql:func-desc:`ext::postgis::linefrommultipoint`\n\n    * - :eql:func:`ext::postgis::makeenvelope`\n      - :eql:func-desc:`ext::postgis::makeenvelope`\n\n    * - :eql:func:`ext::postgis::makeline`\n      - :eql:func-desc:`ext::postgis::makeline`\n\n    * - :eql:func:`ext::postgis::makepoint`\n      - :eql:func-desc:`ext::postgis::makepoint`\n\n    * - :eql:func:`ext::postgis::makepointm`\n      - :eql:func-desc:`ext::postgis::makepointm`\n\n    * - :eql:func:`ext::postgis::makepolygon`\n      - :eql:func-desc:`ext::postgis::makepolygon`\n\n    * - :eql:func:`ext::postgis::point`\n      - :eql:func-desc:`ext::postgis::point`\n\n    * - :eql:func:`ext::postgis::pointm`\n      - :eql:func-desc:`ext::postgis::pointm`\n\n    * - :eql:func:`ext::postgis::pointz`\n      - :eql:func-desc:`ext::postgis::pointz`\n\n    * - :eql:func:`ext::postgis::pointzm`\n      - :eql:func-desc:`ext::postgis::pointzm`\n\n    * - :eql:func:`ext::postgis::polygon`\n      - :eql:func-desc:`ext::postgis::polygon`\n\n    * - :eql:func:`ext::postgis::square`\n      - :eql:func-desc:`ext::postgis::square`\n\n    * - :eql:func:`ext::postgis::tileenvelope`\n      - :eql:func-desc:`ext::postgis::tileenvelope`\n\nGeometry Accessors\n------------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::boundary`\n      - :eql:func-desc:`ext::postgis::boundary`\n\n    * - :eql:func:`ext::postgis::boundingdiagonal`\n      - :eql:func-desc:`ext::postgis::boundingdiagonal`\n\n    * - :eql:func:`ext::postgis::coorddim`\n      - :eql:func-desc:`ext::postgis::coorddim`\n\n    * - :eql:func:`ext::postgis::dimension`\n      - :eql:func-desc:`ext::postgis::dimension`\n\n    * - :eql:func:`ext::postgis::endpoint`\n      - :eql:func-desc:`ext::postgis::endpoint`\n\n    * - :eql:func:`ext::postgis::envelope`\n      - :eql:func-desc:`ext::postgis::envelope`\n\n    * - :eql:func:`ext::postgis::exteriorring`\n      - :eql:func-desc:`ext::postgis::exteriorring`\n\n    * - :eql:func:`ext::postgis::geometryn`\n      - :eql:func-desc:`ext::postgis::geometryn`\n\n    * - :eql:func:`ext::postgis::geometrytype`\n      - :eql:func-desc:`ext::postgis::geometrytype`\n\n    * - :eql:func:`ext::postgis::hasarc`\n      - :eql:func-desc:`ext::postgis::hasarc`\n\n    * - :eql:func:`ext::postgis::interiorringn`\n      - :eql:func-desc:`ext::postgis::interiorringn`\n\n    * - :eql:func:`ext::postgis::isclosed`\n      - :eql:func-desc:`ext::postgis::isclosed`\n\n    * - :eql:func:`ext::postgis::iscollection`\n      - :eql:func-desc:`ext::postgis::iscollection`\n\n    * - :eql:func:`ext::postgis::isempty`\n      - :eql:func-desc:`ext::postgis::isempty`\n\n    * - :eql:func:`ext::postgis::ispolygonccw`\n      - :eql:func-desc:`ext::postgis::ispolygonccw`\n\n    * - :eql:func:`ext::postgis::ispolygoncw`\n      - :eql:func-desc:`ext::postgis::ispolygoncw`\n\n    * - :eql:func:`ext::postgis::isring`\n      - :eql:func-desc:`ext::postgis::isring`\n\n    * - :eql:func:`ext::postgis::issimple`\n      - :eql:func-desc:`ext::postgis::issimple`\n\n    * - :eql:func:`ext::postgis::m`\n      - :eql:func-desc:`ext::postgis::m`\n\n    * - :eql:func:`ext::postgis::memsize`\n      - :eql:func-desc:`ext::postgis::memsize`\n\n    * - :eql:func:`ext::postgis::ndims`\n      - :eql:func-desc:`ext::postgis::ndims`\n\n    * - :eql:func:`ext::postgis::npoints`\n      - :eql:func-desc:`ext::postgis::npoints`\n\n    * - :eql:func:`ext::postgis::nrings`\n      - :eql:func-desc:`ext::postgis::nrings`\n\n    * - :eql:func:`ext::postgis::numgeometries`\n      - :eql:func-desc:`ext::postgis::numgeometries`\n\n    * - :eql:func:`ext::postgis::numinteriorring`\n      - :eql:func-desc:`ext::postgis::numinteriorring`\n\n    * - :eql:func:`ext::postgis::numinteriorrings`\n      - :eql:func-desc:`ext::postgis::numinteriorrings`\n\n    * - :eql:func:`ext::postgis::numpatches`\n      - :eql:func-desc:`ext::postgis::numpatches`\n\n    * - :eql:func:`ext::postgis::numpoints`\n      - :eql:func-desc:`ext::postgis::numpoints`\n\n    * - :eql:func:`ext::postgis::patchn`\n      - :eql:func-desc:`ext::postgis::patchn`\n\n    * - :eql:func:`ext::postgis::pointn`\n      - :eql:func-desc:`ext::postgis::pointn`\n\n    * - :eql:func:`ext::postgis::points`\n      - :eql:func-desc:`ext::postgis::points`\n\n    * - :eql:func:`ext::postgis::startpoint`\n      - :eql:func-desc:`ext::postgis::startpoint`\n\n    * - :eql:func:`ext::postgis::summary`\n      - :eql:func-desc:`ext::postgis::summary`\n\n    * - :eql:func:`ext::postgis::x`\n      - :eql:func-desc:`ext::postgis::x`\n\n    * - :eql:func:`ext::postgis::y`\n      - :eql:func-desc:`ext::postgis::y`\n\n    * - :eql:func:`ext::postgis::z`\n      - :eql:func-desc:`ext::postgis::z`\n\n    * - :eql:func:`ext::postgis::zmflag`\n      - :eql:func-desc:`ext::postgis::zmflag`\n\nGeometry Editors\n----------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::addpoint`\n      - :eql:func-desc:`ext::postgis::addpoint`\n\n    * - :eql:func:`ext::postgis::collectionextract`\n      - :eql:func-desc:`ext::postgis::collectionextract`\n\n    * - :eql:func:`ext::postgis::collectionhomogenize`\n      - :eql:func-desc:`ext::postgis::collectionhomogenize`\n\n    * - :eql:func:`ext::postgis::curvetoline`\n      - :eql:func-desc:`ext::postgis::curvetoline`\n\n    * - :eql:func:`ext::postgis::flipcoordinates`\n      - :eql:func-desc:`ext::postgis::flipcoordinates`\n\n    * - :eql:func:`ext::postgis::force2d`\n      - :eql:func-desc:`ext::postgis::force2d`\n\n    * - :eql:func:`ext::postgis::force3d`\n      - :eql:func-desc:`ext::postgis::force3d`\n\n    * - :eql:func:`ext::postgis::force3dm`\n      - :eql:func-desc:`ext::postgis::force3dm`\n\n    * - :eql:func:`ext::postgis::force3dz`\n      - :eql:func-desc:`ext::postgis::force3dz`\n\n    * - :eql:func:`ext::postgis::force4d`\n      - :eql:func-desc:`ext::postgis::force4d`\n\n    * - :eql:func:`ext::postgis::forcecollection`\n      - :eql:func-desc:`ext::postgis::forcecollection`\n\n    * - :eql:func:`ext::postgis::forcecurve`\n      - :eql:func-desc:`ext::postgis::forcecurve`\n\n    * - :eql:func:`ext::postgis::forcepolygonccw`\n      - :eql:func-desc:`ext::postgis::forcepolygonccw`\n\n    * - :eql:func:`ext::postgis::forcepolygoncw`\n      - :eql:func-desc:`ext::postgis::forcepolygoncw`\n\n    * - :eql:func:`ext::postgis::forcerhr`\n      - :eql:func-desc:`ext::postgis::forcerhr`\n\n    * - :eql:func:`ext::postgis::forcesfs`\n      - :eql:func-desc:`ext::postgis::forcesfs`\n\n    * - :eql:func:`ext::postgis::lineextend`\n      - :eql:func-desc:`ext::postgis::lineextend`\n\n    * - :eql:func:`ext::postgis::linetocurve`\n      - :eql:func-desc:`ext::postgis::linetocurve`\n\n    * - :eql:func:`ext::postgis::multi`\n      - :eql:func-desc:`ext::postgis::multi`\n\n    * - :eql:func:`ext::postgis::normalize`\n      - :eql:func-desc:`ext::postgis::normalize`\n\n    * - :eql:func:`ext::postgis::project`\n      - :eql:func-desc:`ext::postgis::project`\n\n    * - :eql:func:`ext::postgis::quantizecoordinates`\n      - :eql:func-desc:`ext::postgis::quantizecoordinates`\n\n    * - :eql:func:`ext::postgis::removepoint`\n      - :eql:func-desc:`ext::postgis::removepoint`\n\n    * - :eql:func:`ext::postgis::removerepeatedpoints`\n      - :eql:func-desc:`ext::postgis::removerepeatedpoints`\n\n    * - :eql:func:`ext::postgis::reverse`\n      - :eql:func-desc:`ext::postgis::reverse`\n\n    * - :eql:func:`ext::postgis::scroll`\n      - :eql:func-desc:`ext::postgis::scroll`\n\n    * - :eql:func:`ext::postgis::segmentize`\n      - :eql:func-desc:`ext::postgis::segmentize`\n\n    * - :eql:func:`ext::postgis::setpoint`\n      - :eql:func-desc:`ext::postgis::setpoint`\n\n    * - :eql:func:`ext::postgis::shiftlongitude`\n      - :eql:func-desc:`ext::postgis::shiftlongitude`\n\n    * - :eql:func:`ext::postgis::snap`\n      - :eql:func-desc:`ext::postgis::snap`\n\n    * - :eql:func:`ext::postgis::snaptogrid`\n      - :eql:func-desc:`ext::postgis::snaptogrid`\n\n    * - :eql:func:`ext::postgis::wrapx`\n      - :eql:func-desc:`ext::postgis::wrapx`\n\nGeometry Validation\n-------------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::isvalid`\n      - :eql:func-desc:`ext::postgis::isvalid`\n\n    * - :eql:func:`ext::postgis::isvalidreason`\n      - :eql:func-desc:`ext::postgis::isvalidreason`\n\n    * - :eql:func:`ext::postgis::makevalid`\n      - :eql:func-desc:`ext::postgis::makevalid`\n\nSpatial Reference System Functions\n----------------------------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::inversetransformpipeline`\n      - :eql:func-desc:`ext::postgis::inversetransformpipeline`\n\n    * - :eql:func:`ext::postgis::postgis_srs_codes`\n      - :eql:func-desc:`ext::postgis::postgis_srs_codes`\n\n    * - :eql:func:`ext::postgis::setsrid`\n      - :eql:func-desc:`ext::postgis::setsrid`\n\n    * - :eql:func:`ext::postgis::srid`\n      - :eql:func-desc:`ext::postgis::srid`\n\n    * - :eql:func:`ext::postgis::transform`\n      - :eql:func-desc:`ext::postgis::transform`\n\n    * - :eql:func:`ext::postgis::transformpipeline`\n      - :eql:func-desc:`ext::postgis::transformpipeline`\n\nWell-Known Text (WKT)\n---------------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::asewkt`\n      - :eql:func-desc:`ext::postgis::asewkt`\n\n    * - :eql:func:`ext::postgis::astext`\n      - :eql:func-desc:`ext::postgis::astext`\n\n    * - :eql:func:`ext::postgis::bdmpolyfromtext`\n      - :eql:func-desc:`ext::postgis::bdmpolyfromtext`\n\n    * - :eql:func:`ext::postgis::bdpolyfromtext`\n      - :eql:func-desc:`ext::postgis::bdpolyfromtext`\n\n    * - :eql:func:`ext::postgis::geogfromtext`\n      - :eql:func-desc:`ext::postgis::geogfromtext`\n\n    * - :eql:func:`ext::postgis::geomcollfromtext`\n      - :eql:func-desc:`ext::postgis::geomcollfromtext`\n\n    * - :eql:func:`ext::postgis::geomfromewkt`\n      - :eql:func-desc:`ext::postgis::geomfromewkt`\n\n    * - :eql:func:`ext::postgis::geomfrommarc21`\n      - :eql:func-desc:`ext::postgis::geomfrommarc21`\n\n    * - :eql:func:`ext::postgis::geomfromtext`\n      - :eql:func-desc:`ext::postgis::geomfromtext`\n\n    * - :eql:func:`ext::postgis::linefromtext`\n      - :eql:func-desc:`ext::postgis::linefromtext`\n\n    * - :eql:func:`ext::postgis::mlinefromtext`\n      - :eql:func-desc:`ext::postgis::mlinefromtext`\n\n    * - :eql:func:`ext::postgis::mpointfromtext`\n      - :eql:func-desc:`ext::postgis::mpointfromtext`\n\n    * - :eql:func:`ext::postgis::mpolyfromtext`\n      - :eql:func-desc:`ext::postgis::mpolyfromtext`\n\n    * - :eql:func:`ext::postgis::pointfromtext`\n      - :eql:func-desc:`ext::postgis::pointfromtext`\n\n    * - :eql:func:`ext::postgis::polygonfromtext`\n      - :eql:func-desc:`ext::postgis::polygonfromtext`\n\nWell-Known Binary (WKB)\n-----------------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::asbinary`\n      - :eql:func-desc:`ext::postgis::asbinary`\n\n    * - :eql:func:`ext::postgis::asewkb`\n      - :eql:func-desc:`ext::postgis::asewkb`\n\n    * - :eql:func:`ext::postgis::ashexewkb`\n      - :eql:func-desc:`ext::postgis::ashexewkb`\n\n    * - :eql:func:`ext::postgis::geogfromwkb`\n      - :eql:func-desc:`ext::postgis::geogfromwkb`\n\n    * - :eql:func:`ext::postgis::geomfromewkb`\n      - :eql:func-desc:`ext::postgis::geomfromewkb`\n\n    * - :eql:func:`ext::postgis::geomfromwkb`\n      - :eql:func-desc:`ext::postgis::geomfromwkb`\n\n    * - :eql:func:`ext::postgis::linefromwkb`\n      - :eql:func-desc:`ext::postgis::linefromwkb`\n\n    * - :eql:func:`ext::postgis::linestringfromwkb`\n      - :eql:func-desc:`ext::postgis::linestringfromwkb`\n\n    * - :eql:func:`ext::postgis::pointfromwkb`\n      - :eql:func-desc:`ext::postgis::pointfromwkb`\n\nOther Formats\n-------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::asencodedpolyline`\n      - :eql:func-desc:`ext::postgis::asencodedpolyline`\n\n    * - :eql:func:`ext::postgis::asgeojson`\n      - :eql:func-desc:`ext::postgis::asgeojson`\n\n    * - :eql:func:`ext::postgis::asgml`\n      - :eql:func-desc:`ext::postgis::asgml`\n\n    * - :eql:func:`ext::postgis::askml`\n      - :eql:func-desc:`ext::postgis::askml`\n\n    * - :eql:func:`ext::postgis::aslatlontext`\n      - :eql:func-desc:`ext::postgis::aslatlontext`\n\n    * - :eql:func:`ext::postgis::asmarc21`\n      - :eql:func-desc:`ext::postgis::asmarc21`\n\n    * - :eql:func:`ext::postgis::asmvtgeom`\n      - :eql:func-desc:`ext::postgis::asmvtgeom`\n\n    * - :eql:func:`ext::postgis::assvg`\n      - :eql:func-desc:`ext::postgis::assvg`\n\n    * - :eql:func:`ext::postgis::astwkb`\n      - :eql:func-desc:`ext::postgis::astwkb`\n\n    * - :eql:func:`ext::postgis::asx3d`\n      - :eql:func-desc:`ext::postgis::asx3d`\n\n    * - :eql:func:`ext::postgis::box2dfromgeohash`\n      - :eql:func-desc:`ext::postgis::box2dfromgeohash`\n\n    * - :eql:func:`ext::postgis::geohash`\n      - :eql:func-desc:`ext::postgis::geohash`\n\n    * - :eql:func:`ext::postgis::geomfromgeohash`\n      - :eql:func-desc:`ext::postgis::geomfromgeohash`\n\n    * - :eql:func:`ext::postgis::geomfromgeojson`\n      - :eql:func-desc:`ext::postgis::geomfromgeojson`\n\n    * - :eql:func:`ext::postgis::geomfromgml`\n      - :eql:func-desc:`ext::postgis::geomfromgml`\n\n    * - :eql:func:`ext::postgis::geomfromkml`\n      - :eql:func-desc:`ext::postgis::geomfromkml`\n\n    * - :eql:func:`ext::postgis::geomfromtwkb`\n      - :eql:func-desc:`ext::postgis::geomfromtwkb`\n\n    * - :eql:func:`ext::postgis::linefromencodedpolyline`\n      - :eql:func-desc:`ext::postgis::linefromencodedpolyline`\n\n    * - :eql:func:`ext::postgis::pointfromgeohash`\n      - :eql:func-desc:`ext::postgis::pointfromgeohash`\n\nTopological Relationships\n-------------------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::contains`\n      - :eql:func-desc:`ext::postgis::contains`\n\n    * - :eql:func:`ext::postgis::containsproperly`\n      - :eql:func-desc:`ext::postgis::containsproperly`\n\n    * - :eql:func:`ext::postgis::coveredby`\n      - :eql:func-desc:`ext::postgis::coveredby`\n\n    * - :eql:func:`ext::postgis::covers`\n      - :eql:func-desc:`ext::postgis::covers`\n\n    * - :eql:func:`ext::postgis::crosses`\n      - :eql:func-desc:`ext::postgis::crosses`\n\n    * - :eql:func:`ext::postgis::disjoint`\n      - :eql:func-desc:`ext::postgis::disjoint`\n\n    * - :eql:func:`ext::postgis::equals`\n      - :eql:func-desc:`ext::postgis::equals`\n\n    * - :eql:func:`ext::postgis::intersects`\n      - :eql:func-desc:`ext::postgis::intersects`\n\n    * - :eql:func:`ext::postgis::intersects3d`\n      - :eql:func-desc:`ext::postgis::intersects3d`\n\n    * - :eql:func:`ext::postgis::linecrossingdirection`\n      - :eql:func-desc:`ext::postgis::linecrossingdirection`\n\n    * - :eql:func:`ext::postgis::orderingequals`\n      - :eql:func-desc:`ext::postgis::orderingequals`\n\n    * - :eql:func:`ext::postgis::overlaps`\n      - :eql:func-desc:`ext::postgis::overlaps`\n\n    * - :eql:func:`ext::postgis::relate`\n      - :eql:func-desc:`ext::postgis::relate`\n\n    * - :eql:func:`ext::postgis::relatematch`\n      - :eql:func-desc:`ext::postgis::relatematch`\n\n    * - :eql:func:`ext::postgis::touches`\n      - :eql:func-desc:`ext::postgis::touches`\n\n    * - :eql:func:`ext::postgis::within`\n      - :eql:func-desc:`ext::postgis::within`\n\nDistance Relationships\n----------------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::dfullywithin`\n      - :eql:func-desc:`ext::postgis::dfullywithin`\n\n    * - :eql:func:`ext::postgis::dfullywithin3d`\n      - :eql:func-desc:`ext::postgis::dfullywithin3d`\n\n    * - :eql:func:`ext::postgis::dwithin`\n      - :eql:func-desc:`ext::postgis::dwithin`\n\n    * - :eql:func:`ext::postgis::dwithin3d`\n      - :eql:func-desc:`ext::postgis::dwithin3d`\n\n    * - :eql:func:`ext::postgis::pointinsidecircle`\n      - :eql:func-desc:`ext::postgis::pointinsidecircle`\n\nMeasurement Functions\n---------------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::angle`\n      - :eql:func-desc:`ext::postgis::angle`\n\n    * - :eql:func:`ext::postgis::area`\n      - :eql:func-desc:`ext::postgis::area`\n\n    * - :eql:func:`ext::postgis::azimuth`\n      - :eql:func-desc:`ext::postgis::azimuth`\n\n    * - :eql:func:`ext::postgis::closestpoint`\n      - :eql:func-desc:`ext::postgis::closestpoint`\n\n    * - :eql:func:`ext::postgis::closestpoint3d`\n      - :eql:func-desc:`ext::postgis::closestpoint3d`\n\n    * - :eql:func:`ext::postgis::distance`\n      - :eql:func-desc:`ext::postgis::distance`\n\n    * - :eql:func:`ext::postgis::distance3d`\n      - :eql:func-desc:`ext::postgis::distance3d`\n\n    * - :eql:func:`ext::postgis::distancesphere`\n      - :eql:func-desc:`ext::postgis::distancesphere`\n\n    * - :eql:func:`ext::postgis::distancespheroid`\n      - :eql:func-desc:`ext::postgis::distancespheroid`\n\n    * - :eql:func:`ext::postgis::frechetdistance`\n      - :eql:func-desc:`ext::postgis::frechetdistance`\n\n    * - :eql:func:`ext::postgis::hausdorffdistance`\n      - :eql:func-desc:`ext::postgis::hausdorffdistance`\n\n    * - :eql:func:`ext::postgis::length`\n      - :eql:func-desc:`ext::postgis::length`\n\n    * - :eql:func:`ext::postgis::length2d`\n      - :eql:func-desc:`ext::postgis::length2d`\n\n    * - :eql:func:`ext::postgis::length3d`\n      - :eql:func-desc:`ext::postgis::length3d`\n\n    * - :eql:func:`ext::postgis::longestline`\n      - :eql:func-desc:`ext::postgis::longestline`\n\n    * - :eql:func:`ext::postgis::longestline3d`\n      - :eql:func-desc:`ext::postgis::longestline3d`\n\n    * - :eql:func:`ext::postgis::maxdistance`\n      - :eql:func-desc:`ext::postgis::maxdistance`\n\n    * - :eql:func:`ext::postgis::maxdistance3d`\n      - :eql:func-desc:`ext::postgis::maxdistance3d`\n\n    * - :eql:func:`ext::postgis::minimumclearance`\n      - :eql:func-desc:`ext::postgis::minimumclearance`\n\n    * - :eql:func:`ext::postgis::minimumclearanceline`\n      - :eql:func-desc:`ext::postgis::minimumclearanceline`\n\n    * - :eql:func:`ext::postgis::perimeter`\n      - :eql:func-desc:`ext::postgis::perimeter`\n\n    * - :eql:func:`ext::postgis::perimeter2d`\n      - :eql:func-desc:`ext::postgis::perimeter2d`\n\n    * - :eql:func:`ext::postgis::perimeter3d`\n      - :eql:func-desc:`ext::postgis::perimeter3d`\n\n    * - :eql:func:`ext::postgis::shortestline`\n      - :eql:func-desc:`ext::postgis::shortestline`\n\n    * - :eql:func:`ext::postgis::shortestline3d`\n      - :eql:func-desc:`ext::postgis::shortestline3d`\n\nOverlay Functions\n-----------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::clipbybox2d`\n      - :eql:func-desc:`ext::postgis::clipbybox2d`\n\n    * - :eql:func:`ext::postgis::difference`\n      - :eql:func-desc:`ext::postgis::difference`\n\n    * - :eql:func:`ext::postgis::intersection`\n      - :eql:func-desc:`ext::postgis::intersection`\n\n    * - :eql:func:`ext::postgis::node`\n      - :eql:func-desc:`ext::postgis::node`\n\n    * - :eql:func:`ext::postgis::split`\n      - :eql:func-desc:`ext::postgis::split`\n\n    * - :eql:func:`ext::postgis::subdivide`\n      - :eql:func-desc:`ext::postgis::subdivide`\n\n    * - :eql:func:`ext::postgis::symdifference`\n      - :eql:func-desc:`ext::postgis::symdifference`\n\n    * - :eql:func:`ext::postgis::unaryunion`\n      - :eql:func-desc:`ext::postgis::unaryunion`\n\n    * - :eql:func:`ext::postgis::union`\n      - :eql:func-desc:`ext::postgis::union`\n\nGeometry Processing\n-------------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::buffer`\n      - :eql:func-desc:`ext::postgis::buffer`\n\n    * - :eql:func:`ext::postgis::buildarea`\n      - :eql:func-desc:`ext::postgis::buildarea`\n\n    * - :eql:func:`ext::postgis::centroid`\n      - :eql:func-desc:`ext::postgis::centroid`\n\n    * - :eql:func:`ext::postgis::chaikinsmoothing`\n      - :eql:func-desc:`ext::postgis::chaikinsmoothing`\n\n    * - :eql:func:`ext::postgis::concavehull`\n      - :eql:func-desc:`ext::postgis::concavehull`\n\n    * - :eql:func:`ext::postgis::convexhull`\n      - :eql:func-desc:`ext::postgis::convexhull`\n\n    * - :eql:func:`ext::postgis::delaunaytriangles`\n      - :eql:func-desc:`ext::postgis::delaunaytriangles`\n\n    * - :eql:func:`ext::postgis::filterbym`\n      - :eql:func-desc:`ext::postgis::filterbym`\n\n    * - :eql:func:`ext::postgis::generatepoints`\n      - :eql:func-desc:`ext::postgis::generatepoints`\n\n    * - :eql:func:`ext::postgis::geometricmedian`\n      - :eql:func-desc:`ext::postgis::geometricmedian`\n\n    * - :eql:func:`ext::postgis::linemerge`\n      - :eql:func-desc:`ext::postgis::linemerge`\n\n    * - :eql:func:`ext::postgis::minimumboundingcircle`\n      - :eql:func-desc:`ext::postgis::minimumboundingcircle`\n\n    * - :eql:func:`ext::postgis::offsetcurve`\n      - :eql:func-desc:`ext::postgis::offsetcurve`\n\n    * - :eql:func:`ext::postgis::orientedenvelope`\n      - :eql:func-desc:`ext::postgis::orientedenvelope`\n\n    * - :eql:func:`ext::postgis::pointonsurface`\n      - :eql:func-desc:`ext::postgis::pointonsurface`\n\n    * - :eql:func:`ext::postgis::polygonize`\n      - :eql:func-desc:`ext::postgis::polygonize`\n\n    * - :eql:func:`ext::postgis::reduceprecision`\n      - :eql:func-desc:`ext::postgis::reduceprecision`\n\n    * - :eql:func:`ext::postgis::seteffectivearea`\n      - :eql:func-desc:`ext::postgis::seteffectivearea`\n\n    * - :eql:func:`ext::postgis::sharedpaths`\n      - :eql:func-desc:`ext::postgis::sharedpaths`\n\n    * - :eql:func:`ext::postgis::simplify`\n      - :eql:func-desc:`ext::postgis::simplify`\n\n    * - :eql:func:`ext::postgis::simplifypolygonhull`\n      - :eql:func-desc:`ext::postgis::simplifypolygonhull`\n\n    * - :eql:func:`ext::postgis::simplifypreservetopology`\n      - :eql:func-desc:`ext::postgis::simplifypreservetopology`\n\n    * - :eql:func:`ext::postgis::simplifyvw`\n      - :eql:func-desc:`ext::postgis::simplifyvw`\n\n    * - :eql:func:`ext::postgis::triangulatepolygon`\n      - :eql:func-desc:`ext::postgis::triangulatepolygon`\n\n    * - :eql:func:`ext::postgis::voronoilines`\n      - :eql:func-desc:`ext::postgis::voronoilines`\n\n    * - :eql:func:`ext::postgis::voronoipolygons`\n      - :eql:func-desc:`ext::postgis::voronoipolygons`\n\nCoverages\n---------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::coverageunion`\n      - :eql:func-desc:`ext::postgis::coverageunion`\n\nAffine Transformations\n----------------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::affine`\n      - :eql:func-desc:`ext::postgis::affine`\n\n    * - :eql:func:`ext::postgis::rotate`\n      - :eql:func-desc:`ext::postgis::rotate`\n\n    * - :eql:func:`ext::postgis::rotatex`\n      - :eql:func-desc:`ext::postgis::rotatex`\n\n    * - :eql:func:`ext::postgis::rotatey`\n      - :eql:func-desc:`ext::postgis::rotatey`\n\n    * - :eql:func:`ext::postgis::rotatez`\n      - :eql:func-desc:`ext::postgis::rotatez`\n\n    * - :eql:func:`ext::postgis::scale`\n      - :eql:func-desc:`ext::postgis::scale`\n\n    * - :eql:func:`ext::postgis::translate`\n      - :eql:func-desc:`ext::postgis::translate`\n\n    * - :eql:func:`ext::postgis::transscale`\n      - :eql:func-desc:`ext::postgis::transscale`\n\nClustering Functions\n--------------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::clusterintersecting`\n      - :eql:func-desc:`ext::postgis::clusterintersecting`\n\n    * - :eql:func:`ext::postgis::clusterwithin`\n      - :eql:func-desc:`ext::postgis::clusterwithin`\n\nBounding Box Functions\n----------------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::expand`\n      - :eql:func-desc:`ext::postgis::expand`\n\n    * - :eql:func:`ext::postgis::makebox2d`\n      - :eql:func-desc:`ext::postgis::makebox2d`\n\n    * - :eql:func:`ext::postgis::makebox3d`\n      - :eql:func-desc:`ext::postgis::makebox3d`\n\n    * - :eql:func:`ext::postgis::to_box2d`\n      - :eql:func-desc:`ext::postgis::to_box2d`\n\n    * - :eql:func:`ext::postgis::to_box3d`\n      - :eql:func-desc:`ext::postgis::to_box3d`\n\n    * - :eql:func:`ext::postgis::xmax`\n      - :eql:func-desc:`ext::postgis::xmax`\n\n    * - :eql:func:`ext::postgis::xmin`\n      - :eql:func-desc:`ext::postgis::xmin`\n\n    * - :eql:func:`ext::postgis::ymax`\n      - :eql:func-desc:`ext::postgis::ymax`\n\n    * - :eql:func:`ext::postgis::ymin`\n      - :eql:func-desc:`ext::postgis::ymin`\n\n    * - :eql:func:`ext::postgis::zmax`\n      - :eql:func-desc:`ext::postgis::zmax`\n\n    * - :eql:func:`ext::postgis::zmin`\n      - :eql:func-desc:`ext::postgis::zmin`\n\nLinear Referencing\n------------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::addmeasure`\n      - :eql:func-desc:`ext::postgis::addmeasure`\n\n    * - :eql:func:`ext::postgis::interpolatepoint`\n      - :eql:func-desc:`ext::postgis::interpolatepoint`\n\n    * - :eql:func:`ext::postgis::lineinterpolatepoint`\n      - :eql:func-desc:`ext::postgis::lineinterpolatepoint`\n\n    * - :eql:func:`ext::postgis::lineinterpolatepoint3d`\n      - :eql:func-desc:`ext::postgis::lineinterpolatepoint3d`\n\n    * - :eql:func:`ext::postgis::lineinterpolatepoints`\n      - :eql:func-desc:`ext::postgis::lineinterpolatepoints`\n\n    * - :eql:func:`ext::postgis::linelocatepoint`\n      - :eql:func-desc:`ext::postgis::linelocatepoint`\n\n    * - :eql:func:`ext::postgis::linesubstring`\n      - :eql:func-desc:`ext::postgis::linesubstring`\n\n    * - :eql:func:`ext::postgis::locatealong`\n      - :eql:func-desc:`ext::postgis::locatealong`\n\n    * - :eql:func:`ext::postgis::locatebetween`\n      - :eql:func-desc:`ext::postgis::locatebetween`\n\n    * - :eql:func:`ext::postgis::locatebetweenelevations`\n      - :eql:func-desc:`ext::postgis::locatebetweenelevations`\n\nTrajectory Functions\n--------------------\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::closestpointofapproach`\n      - :eql:func-desc:`ext::postgis::closestpointofapproach`\n\n    * - :eql:func:`ext::postgis::cpawithin`\n      - :eql:func-desc:`ext::postgis::cpawithin`\n\n    * - :eql:func:`ext::postgis::distancecpa`\n      - :eql:func-desc:`ext::postgis::distancecpa`\n\n    * - :eql:func:`ext::postgis::isvalidtrajectory`\n      - :eql:func-desc:`ext::postgis::isvalidtrajectory`\n\nOther\n-----\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`ext::postgis::area2d`\n      - :eql:func-desc:`ext::postgis::area2d`\n\n    * - :eql:func:`ext::postgis::cleangeometry`\n      - :eql:func-desc:`ext::postgis::cleangeometry`\n\n    * - :eql:func:`ext::postgis::combinebbox`\n      - :eql:func-desc:`ext::postgis::combinebbox`\n\n    * - :eql:func:`ext::postgis::curven`\n      - :eql:func-desc:`ext::postgis::curven`\n\n    * - :eql:func:`ext::postgis::geography_cmp`\n      - :eql:func-desc:`ext::postgis::geography_cmp`\n\n    * - :eql:func:`ext::postgis::geomcollfromwkb`\n      - :eql:func-desc:`ext::postgis::geomcollfromwkb`\n\n    * - :eql:func:`ext::postgis::geometry_cmp`\n      - :eql:func-desc:`ext::postgis::geometry_cmp`\n\n    * - :eql:func:`ext::postgis::geometry_hash`\n      - :eql:func-desc:`ext::postgis::geometry_hash`\n\n    * - :eql:func:`ext::postgis::get_proj4_from_srid`\n      - :eql:func-desc:`ext::postgis::get_proj4_from_srid`\n\n    * - :eql:func:`ext::postgis::hasm`\n      - :eql:func-desc:`ext::postgis::hasm`\n\n    * - :eql:func:`ext::postgis::hasz`\n      - :eql:func-desc:`ext::postgis::hasz`\n\n    * - :eql:func:`ext::postgis::mlinefromwkb`\n      - :eql:func-desc:`ext::postgis::mlinefromwkb`\n\n    * - :eql:func:`ext::postgis::mpointfromwkb`\n      - :eql:func-desc:`ext::postgis::mpointfromwkb`\n\n    * - :eql:func:`ext::postgis::mpolyfromwkb`\n      - :eql:func-desc:`ext::postgis::mpolyfromwkb`\n\n    * - :eql:func:`ext::postgis::multilinefromwkb`\n      - :eql:func-desc:`ext::postgis::multilinefromwkb`\n\n    * - :eql:func:`ext::postgis::multilinestringfromtext`\n      - :eql:func-desc:`ext::postgis::multilinestringfromtext`\n\n    * - :eql:func:`ext::postgis::multipointfromtext`\n      - :eql:func-desc:`ext::postgis::multipointfromtext`\n\n    * - :eql:func:`ext::postgis::multipointfromwkb`\n      - :eql:func-desc:`ext::postgis::multipointfromwkb`\n\n    * - :eql:func:`ext::postgis::multipolyfromwkb`\n      - :eql:func-desc:`ext::postgis::multipolyfromwkb`\n\n    * - :eql:func:`ext::postgis::multipolygonfromtext`\n      - :eql:func-desc:`ext::postgis::multipolygonfromtext`\n\n    * - :eql:func:`ext::postgis::numcurves`\n      - :eql:func-desc:`ext::postgis::numcurves`\n\n    * - :eql:func:`ext::postgis::polyfromtext`\n      - :eql:func-desc:`ext::postgis::polyfromtext`\n\n    * - :eql:func:`ext::postgis::polyfromwkb`\n      - :eql:func-desc:`ext::postgis::polyfromwkb`\n\n    * - :eql:func:`ext::postgis::polygonfromwkb`\n      - :eql:func-desc:`ext::postgis::polygonfromwkb`\n\n    * - :eql:func:`ext::postgis::postgis_addbbox`\n      - :eql:func-desc:`ext::postgis::postgis_addbbox`\n\n    * - :eql:func:`ext::postgis::postgis_constraint_dims`\n      - :eql:func-desc:`ext::postgis::postgis_constraint_dims`\n\n    * - :eql:func:`ext::postgis::postgis_constraint_srid`\n      - :eql:func-desc:`ext::postgis::postgis_constraint_srid`\n\n    * - :eql:func:`ext::postgis::postgis_dropbbox`\n      - :eql:func-desc:`ext::postgis::postgis_dropbbox`\n\n    * - :eql:func:`ext::postgis::postgis_full_version`\n      - :eql:func-desc:`ext::postgis::postgis_full_version`\n\n    * - :eql:func:`ext::postgis::postgis_geos_compiled_version`\n      - :eql:func-desc:`ext::postgis::postgis_geos_compiled_version`\n\n    * - :eql:func:`ext::postgis::postgis_geos_noop`\n      - :eql:func-desc:`ext::postgis::postgis_geos_noop`\n\n    * - :eql:func:`ext::postgis::postgis_geos_version`\n      - :eql:func-desc:`ext::postgis::postgis_geos_version`\n\n    * - :eql:func:`ext::postgis::postgis_getbbox`\n      - :eql:func-desc:`ext::postgis::postgis_getbbox`\n\n    * - :eql:func:`ext::postgis::postgis_hasbbox`\n      - :eql:func-desc:`ext::postgis::postgis_hasbbox`\n\n    * - :eql:func:`ext::postgis::postgis_lib_build_date`\n      - :eql:func-desc:`ext::postgis::postgis_lib_build_date`\n\n    * - :eql:func:`ext::postgis::postgis_lib_revision`\n      - :eql:func-desc:`ext::postgis::postgis_lib_revision`\n\n    * - :eql:func:`ext::postgis::postgis_lib_version`\n      - :eql:func-desc:`ext::postgis::postgis_lib_version`\n\n    * - :eql:func:`ext::postgis::postgis_libjson_version`\n      - :eql:func-desc:`ext::postgis::postgis_libjson_version`\n\n    * - :eql:func:`ext::postgis::postgis_liblwgeom_version`\n      - :eql:func-desc:`ext::postgis::postgis_liblwgeom_version`\n\n    * - :eql:func:`ext::postgis::postgis_libprotobuf_version`\n      - :eql:func-desc:`ext::postgis::postgis_libprotobuf_version`\n\n    * - :eql:func:`ext::postgis::postgis_libxml_version`\n      - :eql:func-desc:`ext::postgis::postgis_libxml_version`\n\n    * - :eql:func:`ext::postgis::postgis_noop`\n      - :eql:func-desc:`ext::postgis::postgis_noop`\n\n    * - :eql:func:`ext::postgis::postgis_proj_compiled_version`\n      - :eql:func-desc:`ext::postgis::postgis_proj_compiled_version`\n\n    * - :eql:func:`ext::postgis::postgis_proj_version`\n      - :eql:func-desc:`ext::postgis::postgis_proj_version`\n\n    * - :eql:func:`ext::postgis::postgis_scripts_build_date`\n      - :eql:func-desc:`ext::postgis::postgis_scripts_build_date`\n\n    * - :eql:func:`ext::postgis::postgis_scripts_installed`\n      - :eql:func-desc:`ext::postgis::postgis_scripts_installed`\n\n    * - :eql:func:`ext::postgis::postgis_scripts_released`\n      - :eql:func-desc:`ext::postgis::postgis_scripts_released`\n\n    * - :eql:func:`ext::postgis::postgis_svn_version`\n      - :eql:func-desc:`ext::postgis::postgis_svn_version`\n\n    * - :eql:func:`ext::postgis::postgis_transform_geometry`\n      - :eql:func-desc:`ext::postgis::postgis_transform_geometry`\n\n    * - :eql:func:`ext::postgis::postgis_transform_pipeline_geometry`\n      - :eql:func-desc:`ext::postgis::postgis_transform_pipeline_geometry`\n\n    * - :eql:func:`ext::postgis::postgis_typmod_dims`\n      - :eql:func-desc:`ext::postgis::postgis_typmod_dims`\n\n    * - :eql:func:`ext::postgis::postgis_typmod_srid`\n      - :eql:func-desc:`ext::postgis::postgis_typmod_srid`\n\n    * - :eql:func:`ext::postgis::postgis_typmod_type`\n      - :eql:func-desc:`ext::postgis::postgis_typmod_type`\n\n    * - :eql:func:`ext::postgis::postgis_version`\n      - :eql:func-desc:`ext::postgis::postgis_version`\n\n    * - :eql:func:`ext::postgis::postgis_wagyu_version`\n      - :eql:func-desc:`ext::postgis::postgis_wagyu_version`\n\n    * - :eql:func:`ext::postgis::removeirrelevantpointsforview`\n      - :eql:func-desc:`ext::postgis::removeirrelevantpointsforview`\n\n    * - :eql:func:`ext::postgis::removesmallparts`\n      - :eql:func-desc:`ext::postgis::removesmallparts`\n\n    * - :eql:func:`ext::postgis::symmetricdifference`\n      - :eql:func-desc:`ext::postgis::symmetricdifference`\n\n    * - :eql:func:`ext::postgis::to_geography`\n      - :eql:func-desc:`ext::postgis::to_geography`\n\n    * - :eql:func:`ext::postgis::to_geometry`\n      - :eql:func-desc:`ext::postgis::to_geometry`\n\n----------\n\n\n.. eql:function:: ext::postgis::addmeasure( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_addmeasure``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::addpoint( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::addpoint( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    a2: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_addpoint``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::affine( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                    a3: std::float64, \\\n                    a4: std::float64, \\\n                    a5: std::float64, \\\n                    a6: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::affine( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                    a3: std::float64, \\\n                    a4: std::float64, \\\n                    a5: std::float64, \\\n                    a6: std::float64, \\\n                    a7: std::float64, \\\n                    a8: std::float64, \\\n                    a9: std::float64, \\\n                    a10: std::float64, \\\n                    a11: std::float64, \\\n                    a12: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_affine``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::angle( \\\n                    line1: ext::postgis::geometry, \\\n                    line2: ext::postgis::geometry, \\\n                  ) ->  std::float64\n                  ext::postgis::angle( \\\n                    pt1: ext::postgis::geometry, \\\n                    pt2: ext::postgis::geometry, \\\n                    pt3: ext::postgis::geometry, \\\n                    pt4: ext::postgis::geometry = <ext::postgis::geometry>'POINT EMPTY', \\\n                  ) ->  std::float64\n\n    This is exposing ``st_angle``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::area( \\\n                    a0: std::str \\\n                  ) ->  std::float64\n                  ext::postgis::area( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::float64\n                  ext::postgis::area( \\\n                    geog: ext::postgis::geography, \\\n                    use_spheroid: std::bool = true, \\\n                  ) ->  std::float64\n\n    This is exposing ``st_area``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::area2d( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::float64\n\n    This is exposing ``st_area2d``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::asbinary( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::bytes\n                  ext::postgis::asbinary( \\\n                    a0: ext::postgis::geography \\\n                  ) ->  std::bytes\n                  ext::postgis::asbinary( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::str, \\\n                  ) ->  std::bytes\n                  ext::postgis::asbinary( \\\n                    a0: optional ext::postgis::geography, \\\n                    a1: optional std::str, \\\n                  ) -> optional std::bytes\n\n    Returns a geometry/geography in WKB format without SRID meta data.\n\n    Returns the OGC/ISO Well-Known Binary (WKB) representation of the\n    geometry/geography without SRID meta data.\n\n\n    This is exposing ``st_asbinary``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::asencodedpolyline( \\\n                    geom: ext::postgis::geometry, \\\n                    nprecision: std::int64 = 5, \\\n                  ) ->  std::str\n\n    This is exposing ``st_asencodedpolyline``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::asewkb( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::bytes\n                  ext::postgis::asewkb( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::str, \\\n                  ) ->  std::bytes\n\n    Returns a geometry in EWKB format with SRID meta data.\n\n    Returns the Extended Well-Known Binary (EWKB) representation of the\n    geometry with SRID meta data.\n\n\n    This is exposing ``st_asewkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::asewkt( \\\n                    a0: std::str \\\n                  ) ->  std::str\n                  ext::postgis::asewkt( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::str\n                  ext::postgis::asewkt( \\\n                    a0: ext::postgis::geography \\\n                  ) ->  std::str\n                  ext::postgis::asewkt( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::int64, \\\n                  ) ->  std::str\n                  ext::postgis::asewkt( \\\n                    a0: ext::postgis::geography, \\\n                    a1: std::int64, \\\n                  ) ->  std::str\n\n    Returns a geometry in WKT format with SRID meta data.\n\n    Returns the Well-Known Text (WKT) representation of the geometry with SRID\n    meta data.\n\n\n    This is exposing ``st_asewkt``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::asgeojson( \\\n                    a0: std::str \\\n                  ) ->  std::str\n                  ext::postgis::asgeojson( \\\n                    geom: ext::postgis::geometry, \\\n                    maxdecimaldigits: std::int64 = 9, \\\n                    options: std::int64 = 8, \\\n                  ) ->  std::str\n                  ext::postgis::asgeojson( \\\n                    geog: ext::postgis::geography, \\\n                    maxdecimaldigits: std::int64 = 9, \\\n                    options: std::int64 = 0, \\\n                  ) ->  std::str\n\n    This is exposing ``st_asgeojson``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::asgml( \\\n                    a0: std::str \\\n                  ) ->  std::str\n                  ext::postgis::asgml( \\\n                    geom: optional ext::postgis::geometry, \\\n                    maxdecimaldigits: optional std::int64 = 15, \\\n                    options: optional std::int64 = 0, \\\n                  ) -> optional std::str\n                  ext::postgis::asgml( \\\n                    geog: ext::postgis::geography, \\\n                    maxdecimaldigits: std::int64 = 15, \\\n                    options: std::int64 = 0, \\\n                    nprefix: std::str = 'gml', \\\n                    id: std::str = '', \\\n                  ) ->  std::str\n                  ext::postgis::asgml( \\\n                    version: std::int64, \\\n                    geog: ext::postgis::geography, \\\n                    maxdecimaldigits: std::int64 = 15, \\\n                    options: std::int64 = 0, \\\n                    nprefix: std::str = 'gml', \\\n                    id: std::str = '', \\\n                  ) ->  std::str\n                  ext::postgis::asgml( \\\n                    version: optional std::int64, \\\n                    geom: optional ext::postgis::geometry, \\\n                    maxdecimaldigits: optional std::int64 = 15, \\\n                    options: optional std::int64 = 0, \\\n                    nprefix: optional std::str = {}, \\\n                    id: optional std::str = {}, \\\n                  ) -> optional std::str\n\n    This is exposing ``st_asgml``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::ashexewkb( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::str\n                  ext::postgis::ashexewkb( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::str, \\\n                  ) ->  std::str\n\n    Returns a geometry in HEXEWKB format (as text).\n\n    Returnss a geometry in HEXEWKB format (as text) using either little-endian\n    (NDR) or big-endian (XDR) encoding.\n\n\n    This is exposing ``st_ashexewkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::askml( \\\n                    a0: std::str \\\n                  ) ->  std::str\n                  ext::postgis::askml( \\\n                    geom: ext::postgis::geometry, \\\n                    maxdecimaldigits: std::int64 = 15, \\\n                    nprefix: std::str = '', \\\n                  ) ->  std::str\n                  ext::postgis::askml( \\\n                    geog: ext::postgis::geography, \\\n                    maxdecimaldigits: std::int64 = 15, \\\n                    nprefix: std::str = '', \\\n                  ) ->  std::str\n\n    This is exposing ``st_askml``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::aslatlontext( \\\n                    geom: ext::postgis::geometry, \\\n                    tmpl: std::str = '', \\\n                  ) ->  std::str\n\n    This is exposing ``st_aslatlontext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::asmarc21( \\\n                    geom: ext::postgis::geometry, \\\n                    format: std::str = 'hdddmmss', \\\n                  ) ->  std::str\n\n    This is exposing ``st_asmarc21``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::asmvtgeom( \\\n                    geom: optional ext::postgis::geometry, \\\n                    bounds: optional ext::postgis::box2d, \\\n                    extent: optional std::int64 = 4096, \\\n                    buffer: optional std::int64 = 256, \\\n                    clip_geom: optional std::bool = true, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_asmvtgeom``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::assvg( \\\n                    a0: std::str \\\n                  ) ->  std::str\n                  ext::postgis::assvg( \\\n                    geom: ext::postgis::geometry, \\\n                    rel: std::int64 = 0, \\\n                    maxdecimaldigits: std::int64 = 15, \\\n                  ) ->  std::str\n                  ext::postgis::assvg( \\\n                    geog: ext::postgis::geography, \\\n                    rel: std::int64 = 0, \\\n                    maxdecimaldigits: std::int64 = 15, \\\n                  ) ->  std::str\n\n    This is exposing ``st_assvg``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::astext( \\\n                    a0: std::str \\\n                  ) ->  std::str\n                  ext::postgis::astext( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::str\n                  ext::postgis::astext( \\\n                    a0: ext::postgis::geography \\\n                  ) ->  std::str\n                  ext::postgis::astext( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::int64, \\\n                  ) ->  std::str\n                  ext::postgis::astext( \\\n                    a0: ext::postgis::geography, \\\n                    a1: std::int64, \\\n                  ) ->  std::str\n\n    Returns a geometry/geography in WKT format without SRID metadata.\n\n    Returns the Well-Known Text (WKT) representation of the geometry/geography\n    without SRID metadata.\n\n\n    This is exposing ``st_astext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::astwkb( \\\n                    geom: optional ext::postgis::geometry, \\\n                    prec: optional std::int64 = {}, \\\n                    prec_z: optional std::int64 = {}, \\\n                    prec_m: optional std::int64 = {}, \\\n                    with_sizes: optional std::bool = {}, \\\n                    with_boxes: optional std::bool = {}, \\\n                  ) -> optional std::bytes\n                  ext::postgis::astwkb( \\\n                    geom: optional array<ext::postgis::geometry>, \\\n                    ids: optional array<std::int64>, \\\n                    prec: optional std::int64 = {}, \\\n                    prec_z: optional std::int64 = {}, \\\n                    prec_m: optional std::int64 = {}, \\\n                    with_sizes: optional std::bool = {}, \\\n                    with_boxes: optional std::bool = {}, \\\n                  ) -> optional std::bytes\n\n    This is exposing ``st_astwkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::asx3d( \\\n                    geom: optional ext::postgis::geometry, \\\n                    maxdecimaldigits: optional std::int64 = 15, \\\n                    options: optional std::int64 = 0, \\\n                  ) -> optional std::str\n\n    Returns a geometry in X3D format.\n\n    Returns a geometry in X3D xml node element format:\n    ISO-IEC-19776-1.2-X3DEncodings-XML.\n\n\n    This is exposing ``st_asx3d``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::azimuth( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::float64\n                  ext::postgis::azimuth( \\\n                    geog1: ext::postgis::geography, \\\n                    geog2: ext::postgis::geography, \\\n                  ) ->  std::float64\n\n    This is exposing ``st_azimuth``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::bdmpolyfromtext( \\\n                    a0: std::str, \\\n                    a1: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_bdmpolyfromtext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::bdpolyfromtext( \\\n                    a0: std::str, \\\n                    a1: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_bdpolyfromtext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::boundary( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_boundary``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::boundingdiagonal( \\\n                    geom: ext::postgis::geometry, \\\n                    fits: std::bool = false, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_boundingdiagonal``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::box2dfromgeohash( \\\n                    a0: optional std::str, \\\n                    a1: optional std::int64 = {}, \\\n                  ) -> optional ext::postgis::box2d\n\n    This is exposing ``st_box2dfromgeohash``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::buffer( \\\n                    a0: std::str, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::buffer( \\\n                    a0: std::str, \\\n                    a1: std::float64, \\\n                    a2: std::str, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::buffer( \\\n                    a0: ext::postgis::geography, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::geography\n                  ext::postgis::buffer( \\\n                    a0: std::str, \\\n                    a1: std::float64, \\\n                    a2: std::int64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::buffer( \\\n                    a0: ext::postgis::geography, \\\n                    a1: std::float64, \\\n                    a2: std::str, \\\n                  ) ->  ext::postgis::geography\n                  ext::postgis::buffer( \\\n                    a0: ext::postgis::geography, \\\n                    a1: std::float64, \\\n                    a2: std::int64, \\\n                  ) ->  ext::postgis::geography\n                  ext::postgis::buffer( \\\n                    geom: ext::postgis::geometry, \\\n                    radius: std::float64, \\\n                    quadsegs: std::int64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::buffer( \\\n                    geom: ext::postgis::geometry, \\\n                    radius: std::float64, \\\n                    options: std::str = '', \\\n                  ) ->  ext::postgis::geometry\n\n    Returns a geometry covering all points within a given distance from a\n    geometry.\n\n    This is exposing ``st_buffer``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::buildarea( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_buildarea``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::centroid( \\\n                    a0: std::str \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::centroid( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::centroid( \\\n                    a0: ext::postgis::geography, \\\n                    use_spheroid: std::bool = true, \\\n                  ) ->  ext::postgis::geography\n\n    This is exposing ``st_centroid``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::chaikinsmoothing( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::int64 = 1, \\\n                    a2: std::bool = false, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_chaikinsmoothing``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::cleangeometry( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_cleangeometry``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::clipbybox2d( \\\n                    geom: ext::postgis::geometry, \\\n                    box: ext::postgis::box2d, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_clipbybox2d``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::closestpoint( \\\n                    a0: optional std::str, \\\n                    a1: optional std::str, \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::closestpoint( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::closestpoint( \\\n                    a0: ext::postgis::geography, \\\n                    a1: ext::postgis::geography, \\\n                    use_spheroid: std::bool = true, \\\n                  ) ->  ext::postgis::geography\n\n    Returns the 2D point of the first geometry closest to the second.\n\n    Returns the 2D point of the first geometry/geography that is closest to\n    the second geometry/geography. This is the first point of the shortest\n    line from one geometry to the other.\n\n\n    This is exposing ``st_closestpoint``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::closestpoint3d( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::geometry\n\n    Returns the 3D point of the first geometry closest to the second.\n\n    Returns the 3D point of the first geometry/geography that is closest to\n    the second geometry/geography. This is the first point of the 3D shortest\n    line.\n\n\n    This is exposing ``st_3dclosestpoint``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::closestpointofapproach( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: ext::postgis::geometry, \\\n                  ) ->  std::float64\n\n    This is exposing ``st_closestpointofapproach``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::clusterintersecting( \\\n                    a0: array<ext::postgis::geometry> \\\n                  ) ->  array<ext::postgis::geometry>\n\n    This is exposing ``st_clusterintersecting``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::clusterwithin( \\\n                    a0: array<ext::postgis::geometry>, \\\n                    a1: std::float64, \\\n                  ) ->  array<ext::postgis::geometry>\n\n    This is exposing ``st_clusterwithin``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::collect( \\\n                    a0: array<ext::postgis::geometry> \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::collect( \\\n                    geom1: optional ext::postgis::geometry, \\\n                    geom2: optional ext::postgis::geometry, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_collect``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::collectionextract( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::collectionextract( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_collectionextract``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::collectionhomogenize( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_collectionhomogenize``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::combinebbox( \\\n                    a0: optional ext::postgis::box3d, \\\n                    a1: optional ext::postgis::box3d, \\\n                  ) -> optional ext::postgis::box3d\n                  ext::postgis::combinebbox( \\\n                    a0: optional ext::postgis::box2d, \\\n                    a1: optional ext::postgis::geometry, \\\n                  ) -> optional ext::postgis::box2d\n                  ext::postgis::combinebbox( \\\n                    a0: optional ext::postgis::box3d, \\\n                    a1: optional ext::postgis::geometry, \\\n                  ) -> optional ext::postgis::box3d\n\n    This is exposing ``st_combinebbox``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::concavehull( \\\n                    param_geom: ext::postgis::geometry, \\\n                    param_pctconvex: std::float64, \\\n                    param_allow_holes: std::bool = false, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_concavehull``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::contains( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing ``st_contains``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::containsproperly( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    Tests if every point of *geom2* lies in the interior of *geom1*.\n\n    This is exposing ``st_containsproperly``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::convexhull( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_convexhull``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::coorddim( \\\n                    geometry: ext::postgis::geometry \\\n                  ) ->  std::int16\n\n    This is exposing ``st_coorddim``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::coverageunion( \\\n                    a0: array<ext::postgis::geometry> \\\n                  ) ->  ext::postgis::geometry\n\n    Computes polygonal coverage from a set of polygons.\n\n    Computes the union of a set of polygons forming a coverage by removing\n    shared edges.\n\n\n\n    This is exposing ``st_coverageunion``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::coveredby( \\\n                    a0: optional std::str, \\\n                    a1: optional std::str, \\\n                  ) -> optional std::bool\n                  ext::postgis::coveredby( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::bool\n                  ext::postgis::coveredby( \\\n                    geog1: ext::postgis::geography, \\\n                    geog2: ext::postgis::geography, \\\n                  ) ->  std::bool\n\n    This is exposing ``st_coveredby``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::covers( \\\n                    a0: optional std::str, \\\n                    a1: optional std::str, \\\n                  ) -> optional std::bool\n                  ext::postgis::covers( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::bool\n                  ext::postgis::covers( \\\n                    geog1: ext::postgis::geography, \\\n                    geog2: ext::postgis::geography, \\\n                  ) ->  std::bool\n\n    This is exposing ``st_covers``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::cpawithin( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: ext::postgis::geometry, \\\n                    a2: std::float64, \\\n                  ) ->  std::bool\n\n    Tests if two trajectoriesis approach within the specified distance.\n\n    Tests if the closest point of approach of two trajectoriesis within the\n    specified distance.\n\n\n    This is exposing ``st_cpawithin``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::crosses( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing ``st_crosses``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::curven( \\\n                    geometry: ext::postgis::geometry, \\\n                    i: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_curven``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::curvetoline( \\\n                    geom: ext::postgis::geometry, \\\n                    tol: std::float64 = 32, \\\n                    toltype: std::int64 = 0, \\\n                    flags: std::int64 = 0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_curvetoline``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::delaunaytriangles( \\\n                    g1: ext::postgis::geometry, \\\n                    tolerance: std::float64 = 0.0, \\\n                    flags: std::int64 = 0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_delaunaytriangles``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::dfullywithin( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    a2: std::float64, \\\n                  ) ->  std::bool\n\n    Tests if two geometries are entirely within a given distance.\n\n    This is exposing ``st_dfullywithin``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::dfullywithin3d( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    a2: std::float64, \\\n                  ) ->  std::bool\n\n    This is exposing ``st_3ddfullywithin``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::difference( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    gridsize: std::float64 = -1.0, \\\n                  ) ->  ext::postgis::geometry\n\n    Computes a geometry resulting from removing all points in *geom2* from\n    *geom1*.\n\n    Computes a geometry representing the part of geometry *geom1* that does\n    not intersect geometry *geom2*.\n\n\n    This is exposing ``st_difference``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::dimension( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::int64\n\n    This is exposing ``st_dimension``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::disjoint( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing ``st_disjoint``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::distance( \\\n                    a0: std::str, \\\n                    a1: std::str, \\\n                  ) ->  std::float64\n                  ext::postgis::distance( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::float64\n                  ext::postgis::distance( \\\n                    geog1: ext::postgis::geography, \\\n                    geog2: ext::postgis::geography, \\\n                    use_spheroid: std::bool = true, \\\n                  ) ->  std::float64\n\n    This is exposing ``st_distance``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::distance3d( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::float64\n\n    Returns the 3D cartesian minimum distance between two geometries.\n\n    Returns the 3D cartesian minimum distance (based on spatial ref) between\n    two geometries in projected units.\n\n\n    This is exposing ``st_3ddistance``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::distancecpa( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: ext::postgis::geometry, \\\n                  ) ->  std::float64\n\n    This is exposing ``st_distancecpa``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::distancesphere( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::float64\n                  ext::postgis::distancesphere( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    radius: std::float64, \\\n                  ) ->  std::float64\n\n    This is exposing ``st_distancesphere``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::distancespheroid( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::float64\n\n    This is exposing ``st_distancespheroid``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::dwithin( \\\n                    a0: optional std::str, \\\n                    a1: optional std::str, \\\n                    a2: optional std::float64, \\\n                  ) -> optional std::bool\n                  ext::postgis::dwithin( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    a2: std::float64, \\\n                  ) ->  std::bool\n                  ext::postgis::dwithin( \\\n                    geog1: ext::postgis::geography, \\\n                    geog2: ext::postgis::geography, \\\n                    tolerance: std::float64, \\\n                    use_spheroid: std::bool = true, \\\n                  ) ->  std::bool\n\n    This is exposing ``st_dwithin``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::dwithin3d( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    a2: std::float64, \\\n                  ) ->  std::bool\n\n    This is exposing ``st_3ddwithin``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::endpoint( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_endpoint``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::envelope( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_envelope``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::equals( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing ``st_equals``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::expand( \\\n                    a0: ext::postgis::box2d, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::box2d\n                  ext::postgis::expand( \\\n                    a0: ext::postgis::box3d, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::box3d\n                  ext::postgis::expand( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::expand( \\\n                    box: ext::postgis::box2d, \\\n                    dx: std::float64, \\\n                    dy: std::float64, \\\n                  ) ->  ext::postgis::box2d\n                  ext::postgis::expand( \\\n                    box: ext::postgis::box3d, \\\n                    dx: std::float64, \\\n                    dy: std::float64, \\\n                    dz: std::float64 = 0, \\\n                  ) ->  ext::postgis::box3d\n                  ext::postgis::expand( \\\n                    geom: ext::postgis::geometry, \\\n                    dx: std::float64, \\\n                    dy: std::float64, \\\n                    dz: std::float64 = 0, \\\n                    dm: std::float64 = 0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_expand``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::exteriorring( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_exteriorring``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::filterbym( \\\n                    a0: optional ext::postgis::geometry, \\\n                    a1: optional std::float64, \\\n                    a2: optional std::float64 = {}, \\\n                    a3: optional std::bool = false, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_filterbym``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::flipcoordinates( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_flipcoordinates``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::force2d( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_force2d``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::force3d( \\\n                    geom: ext::postgis::geometry, \\\n                    zvalue: std::float64 = 0.0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_force3d``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::force3dm( \\\n                    geom: ext::postgis::geometry, \\\n                    mvalue: std::float64 = 0.0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_force3dm``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::force3dz( \\\n                    geom: ext::postgis::geometry, \\\n                    zvalue: std::float64 = 0.0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_force3dz``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::force4d( \\\n                    geom: ext::postgis::geometry, \\\n                    zvalue: std::float64 = 0.0, \\\n                    mvalue: std::float64 = 0.0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_force4d``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::forcecollection( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_forcecollection``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::forcecurve( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_forcecurve``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::forcepolygonccw( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_forcepolygonccw``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::forcepolygoncw( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_forcepolygoncw``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::forcerhr( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    Forces the orientation of the vertices in a polygon to follow the RHR.\n\n    Forces the orientation of the vertices in a polygon to follow a\n    Right-Hand-Rule, in which the area that is bounded by the polygon is to\n    the right of the boundary. In particular, the exterior ring is orientated\n    in a clockwise direction and the interior rings in a counter-clockwise\n    direction.\n\n\n    This is exposing ``st_forcerhr``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::forcesfs( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::forcesfs( \\\n                    a0: ext::postgis::geometry, \\\n                    version: std::str, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_forcesfs``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::frechetdistance( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    a2: std::float64 = -1, \\\n                  ) ->  std::float64\n\n    This is exposing ``st_frechetdistance``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::generatepoints( \\\n                    area: ext::postgis::geometry, \\\n                    npoints: std::int64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::generatepoints( \\\n                    area: ext::postgis::geometry, \\\n                    npoints: std::int64, \\\n                    seed: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_generatepoints``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geogfromtext( \\\n                    a0: std::str \\\n                  ) ->  ext::postgis::geography\n\n    Creates a geography value from WKT or EWTK.\n\n    Creates a geography value from Well-Known Text or Extended\n    Well-Known Text representation.\n\n\n    This is exposing ``st_geogfromtext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geogfromwkb( \\\n                    a0: std::bytes \\\n                  ) ->  ext::postgis::geography\n\n    Creates a geography value from WKB or EWKB.\n\n    Creates a geography value from a Well-Known Binary geometry representation\n    (WKB) or extended Well Known Binary (EWKB).\n\n\n    This is exposing ``st_geogfromwkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geography_cmp( \\\n                    a0: ext::postgis::geography, \\\n                    a1: ext::postgis::geography, \\\n                  ) ->  std::int64\n\n    This is exposing ``geography_cmp``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geohash( \\\n                    geom: ext::postgis::geometry, \\\n                    maxchars: std::int64 = 0, \\\n                  ) ->  std::str\n                  ext::postgis::geohash( \\\n                    geog: ext::postgis::geography, \\\n                    maxchars: std::int64 = 0, \\\n                  ) ->  std::str\n\n    This is exposing ``st_geohash``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geomcollfromtext( \\\n                    a0: std::str \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::geomcollfromtext( \\\n                    a0: std::str, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    Makes a collection Geometry from collection WKT.\n\n    Makes a collection Geometry from collection WKT with the given SRID. If\n    SRID is not given, it defaults to 0.\n\n\n    This is exposing ``st_geomcollfromtext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geomcollfromwkb( \\\n                    a0: std::bytes \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::geomcollfromwkb( \\\n                    a0: std::bytes, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_geomcollfromwkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geometricmedian( \\\n                    g: optional ext::postgis::geometry, \\\n                    tolerance: optional std::float64 = {}, \\\n                    max_iter: optional std::int64 = 10000, \\\n                    fail_if_not_converged: optional std::bool = false, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_geometricmedian``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geometry_cmp( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::int64\n\n    This is exposing ``geometry_cmp``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geometry_hash( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::int64\n\n    This is exposing ``geometry_hash``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geometryn( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_geometryn``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geometrytype( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::str\n                  ext::postgis::geometrytype( \\\n                    a0: ext::postgis::geography \\\n                  ) ->  std::str\n\n    This is exposing ``geometrytype``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geomfromewkb( \\\n                    a0: std::bytes \\\n                  ) ->  ext::postgis::geometry\n\n    Creates a geometry value from EWKB.\n\n    Creates a geometry value from Extended Well-Known Binary representation\n    (EWKB).\n\n\n    This is exposing ``st_geomfromewkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geomfromewkt( \\\n                    a0: std::str \\\n                  ) ->  ext::postgis::geometry\n\n    Creates a geometry value from EWKT representation.\n\n    Creates a geometry value from Extended Well-Known Text representation (EWKT).\n\n\n    This is exposing ``st_geomfromewkt``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geomfromgeohash( \\\n                    a0: optional std::str, \\\n                    a1: optional std::int64 = {}, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_geomfromgeohash``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geomfromgeojson( \\\n                    a0: std::str \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::geomfromgeojson( \\\n                    a0: std::json \\\n                  ) ->  ext::postgis::geometry\n\n    Creates a geometry value from a geojson representation of a geometry.\n\n    Takes as input a geojson representation of a geometry and outputs a\n    ``geometry`` value.\n\n\n    This is exposing ``st_geomfromgeojson``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geomfromgml( \\\n                    a0: std::str \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::geomfromgml( \\\n                    a0: std::str, \\\n                    a1: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    Creates a geometry value from GML representation of a geometry.\n\n    Takes as input GML representation of geometry and outputs a  ``geometry``\n    value.\n\n\n    This is exposing ``st_geomfromgml``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geomfromkml( \\\n                    a0: std::str \\\n                  ) ->  ext::postgis::geometry\n\n    Creates a geometry value from KML representation of a geometry.\n\n    Takes as input KML representation of geometry and outputs a ``geometry``\n    value.\n\n\n    This is exposing ``st_geomfromkml``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geomfrommarc21( \\\n                    marc21xml: std::str \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_geomfrommarc21``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geomfromtext( \\\n                    a0: std::str \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::geomfromtext( \\\n                    a0: std::str, \\\n                    a1: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    Creates a geometry value from WKT representation.\n\n    Creates a geometry value from Well-Known Text representation (WKT).\n\n\n    This is exposing ``st_geomfromtext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geomfromtwkb( \\\n                    a0: std::bytes \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_geomfromtwkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::geomfromwkb( \\\n                    a0: std::bytes \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::geomfromwkb( \\\n                    a0: std::bytes, \\\n                    a1: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    Creates a geometry value from WKB representation.\n\n    Creates a geometry value from a Well-Known Binary geometry representation\n    (WKB) and optional SRID.\n\n\n    This is exposing ``st_geomfromwkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::get_proj4_from_srid( \\\n                    a0: std::int64 \\\n                  ) ->  std::str\n\n    This is exposing ``get_proj4_from_srid``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::hasarc( \\\n                    geometry: ext::postgis::geometry \\\n                  ) ->  std::bool\n\n    This is exposing ``st_hasarc``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::hasm( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::bool\n\n    This is exposing ``st_hasm``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::hasz( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::bool\n\n    This is exposing ``st_hasz``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::hausdorffdistance( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::float64\n                  ext::postgis::hausdorffdistance( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    a2: std::float64, \\\n                  ) ->  std::float64\n\n    This is exposing ``st_hausdorffdistance``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::hexagon( \\\n                    size: std::float64, \\\n                    cell_i: std::int64, \\\n                    cell_j: std::int64, \\\n                    origin: ext::postgis::geometry = 'POINT(0 0)', \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_hexagon``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::interiorringn( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_interiorringn``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::interpolatepoint( \\\n                    line: ext::postgis::geometry, \\\n                    point: ext::postgis::geometry, \\\n                  ) ->  std::float64\n\n    This is exposing ``st_interpolatepoint``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::intersection( \\\n                    a0: std::str, \\\n                    a1: std::str, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::intersection( \\\n                    a0: ext::postgis::geography, \\\n                    a1: ext::postgis::geography, \\\n                  ) ->  ext::postgis::geography\n                  ext::postgis::intersection( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    gridsize: std::float64 = -1, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_intersection``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::intersects( \\\n                    a0: optional std::str, \\\n                    a1: optional std::str, \\\n                  ) -> optional std::bool\n                  ext::postgis::intersects( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::bool\n                  ext::postgis::intersects( \\\n                    geog1: ext::postgis::geography, \\\n                    geog2: ext::postgis::geography, \\\n                  ) ->  std::bool\n\n    This is exposing ``st_intersects``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::intersects3d( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    Tests if two geometries spatially intersect in 3D.\n\n    Tests if two geometries spatially intersect in 3D - only for points,\n    linestrings, polygons, polyhedral surface (area).\n\n\n    This is exposing ``st_3dintersects``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::inversetransformpipeline( \\\n                    geom: ext::postgis::geometry, \\\n                    pipeline: std::str, \\\n                    to_srid: std::int64 = 0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_inversetransformpipeline``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::isclosed( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::bool\n\n    Tests if a geometry in 2D or 3D is closed.\n\n    Tests if a LineStrings's start and end points are coincident. For a\n    PolyhedralSurface tests if it is closed (volumetric).\n\n\n    This is exposing ``st_isclosed``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::iscollection( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::bool\n\n    This is exposing ``st_iscollection``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::isempty( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::bool\n\n    This is exposing ``st_isempty``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::ispolygonccw( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::bool\n\n    Tests counter-clockwise poligonal orientation of a geometry.\n\n    Tests if Polygons have exterior rings oriented counter-clockwise and\n    interior rings oriented clockwise.\n\n\n    This is exposing ``st_ispolygonccw``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::ispolygoncw( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::bool\n\n    Tests clockwise poligonal orientation of a geometry.\n\n    Tests if Polygons have exterior rings oriented clockwise and interior\n    rings oriented counter-clockwise.\n\n\n    This is exposing ``st_ispolygoncw``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::isring( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::bool\n\n    This is exposing ``st_isring``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::issimple( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::bool\n\n    This is exposing ``st_issimple``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::isvalid( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::bool\n                  ext::postgis::isvalid( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::int64, \\\n                  ) ->  std::bool\n\n    This is exposing ``st_isvalid``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::isvalidreason( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::str\n                  ext::postgis::isvalidreason( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::int64, \\\n                  ) ->  std::str\n\n    This is exposing ``st_isvalidreason``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::isvalidtrajectory( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::bool\n\n    This is exposing ``st_isvalidtrajectory``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::length( \\\n                    a0: std::str \\\n                  ) ->  std::float64\n                  ext::postgis::length( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::float64\n                  ext::postgis::length( \\\n                    geog: ext::postgis::geography, \\\n                    use_spheroid: std::bool = true, \\\n                  ) ->  std::float64\n\n    This is exposing ``st_length``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::length2d( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::float64\n\n    This is exposing ``st_length2d``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::length3d( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::float64\n\n    This is exposing ``st_3dlength``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::linecrossingdirection( \\\n                    line1: ext::postgis::geometry, \\\n                    line2: ext::postgis::geometry, \\\n                  ) ->  std::int64\n\n    This is exposing ``st_linecrossingdirection``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::lineextend( \\\n                    geom: ext::postgis::geometry, \\\n                    distance_forward: std::float64, \\\n                    distance_backward: std::float64 = 0.0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_lineextend``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::linefromencodedpolyline( \\\n                    txtin: std::str, \\\n                    nprecision: std::int64 = 5, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_linefromencodedpolyline``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::linefrommultipoint( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_linefrommultipoint``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::linefromtext( \\\n                    a0: std::str \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::linefromtext( \\\n                    a0: std::str, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    Creates a geometry from WKT LINESTRING.\n\n    Makes a Geometry from WKT representation with the given SRID. If SRID is\n    not given, it defaults to 0.\n\n\n    This is exposing ``st_linefromtext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::linefromwkb( \\\n                    a0: std::bytes \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::linefromwkb( \\\n                    a0: std::bytes, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_linefromwkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::lineinterpolatepoint( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::lineinterpolatepoint( \\\n                    a0: optional std::str, \\\n                    a1: optional std::float64, \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::lineinterpolatepoint( \\\n                    a0: ext::postgis::geography, \\\n                    a1: std::float64, \\\n                    use_spheroid: std::bool = true, \\\n                  ) ->  ext::postgis::geography\n\n    This is exposing ``st_lineinterpolatepoint``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::lineinterpolatepoint3d( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_3dlineinterpolatepoint``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::lineinterpolatepoints( \\\n                    a0: optional std::str, \\\n                    a1: optional std::float64, \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::lineinterpolatepoints( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    repeat: std::bool = true, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::lineinterpolatepoints( \\\n                    a0: ext::postgis::geography, \\\n                    a1: std::float64, \\\n                    use_spheroid: std::bool = true, \\\n                    repeat: std::bool = true, \\\n                  ) ->  ext::postgis::geography\n\n    This is exposing ``st_lineinterpolatepoints``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::linelocatepoint( \\\n                    a0: optional std::str, \\\n                    a1: optional std::str, \\\n                  ) -> optional std::float64\n                  ext::postgis::linelocatepoint( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::float64\n                  ext::postgis::linelocatepoint( \\\n                    a0: ext::postgis::geography, \\\n                    a1: ext::postgis::geography, \\\n                    use_spheroid: std::bool = true, \\\n                  ) ->  std::float64\n\n    This is exposing ``st_linelocatepoint``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::linemerge( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::linemerge( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::bool, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_linemerge``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::linestringfromwkb( \\\n                    a0: std::bytes \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::linestringfromwkb( \\\n                    a0: std::bytes, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_linestringfromwkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::linesubstring( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::linesubstring( \\\n                    a0: ext::postgis::geography, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                  ) ->  ext::postgis::geography\n                  ext::postgis::linesubstring( \\\n                    a0: optional std::str, \\\n                    a1: optional std::float64, \\\n                    a2: optional std::float64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_linesubstring``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::linetocurve( \\\n                    geometry: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_linetocurve``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::locatealong( \\\n                    geometry: ext::postgis::geometry, \\\n                    measure: std::float64, \\\n                    leftrightoffset: std::float64 = 0.0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_locatealong``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::locatebetween( \\\n                    geometry: ext::postgis::geometry, \\\n                    frommeasure: std::float64, \\\n                    tomeasure: std::float64, \\\n                    leftrightoffset: std::float64 = 0.0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_locatebetween``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::locatebetweenelevations( \\\n                    geometry: ext::postgis::geometry, \\\n                    fromelevation: std::float64, \\\n                    toelevation: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_locatebetweenelevations``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::longestline( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_longestline``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::longestline3d( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_3dlongestline``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::m( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::float64\n\n    This is exposing ``st_m``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::makebox2d( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::box2d\n\n    This is exposing ``st_makebox2d``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::makebox3d( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::box3d\n\n    This is exposing ``st_3dmakebox``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::makeenvelope( \\\n                    a0: std::float64, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                    a3: std::float64, \\\n                    a4: std::int64 = 0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_makeenvelope``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::makeline( \\\n                    a0: array<ext::postgis::geometry> \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::makeline( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_makeline``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::makepoint( \\\n                    a0: std::float64, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::makepoint( \\\n                    a0: std::float64, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::makepoint( \\\n                    a0: std::float64, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                    a3: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_makepoint``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::makepointm( \\\n                    a0: std::float64, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_makepointm``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::makepolygon( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::makepolygon( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: array<ext::postgis::geometry>, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_makepolygon``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::makevalid( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::makevalid( \\\n                    geom: ext::postgis::geometry, \\\n                    params: std::str, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_makevalid``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::maxdistance( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::float64\n\n    This is exposing ``st_maxdistance``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::maxdistance3d( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::float64\n\n    Returns the 3D cartesian maximum distance between two geometries.\n\n    Returns the 3D cartesian maximum distance (based on spatial ref) between\n    two geometries in projected units.\n\n\n    This is exposing ``st_3dmaxdistance``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::memsize( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::int64\n\n    This is exposing ``st_memsize``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::minimumboundingcircle( \\\n                    inputgeom: ext::postgis::geometry, \\\n                    segs_per_quarter: std::int64 = 48, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_minimumboundingcircle``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::minimumclearance( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::float64\n\n    This is exposing ``st_minimumclearance``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::minimumclearanceline( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_minimumclearanceline``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::mlinefromtext( \\\n                    a0: std::str \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::mlinefromtext( \\\n                    a0: std::str, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_mlinefromtext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::mlinefromwkb( \\\n                    a0: std::bytes \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::mlinefromwkb( \\\n                    a0: std::bytes, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_mlinefromwkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::mpointfromtext( \\\n                    a0: std::str \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::mpointfromtext( \\\n                    a0: std::str, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    Creates a geometry from WKT MULTIPOINT.\n\n    Makes a Geometry from WKT with the given SRID. If SRID is not given, it\n    defaults to 0.\n\n\n    This is exposing ``st_mpointfromtext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::mpointfromwkb( \\\n                    a0: std::bytes \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::mpointfromwkb( \\\n                    a0: std::bytes, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_mpointfromwkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::mpolyfromtext( \\\n                    a0: std::str \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::mpolyfromtext( \\\n                    a0: std::str, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    Creates a geometry from WKT MULTIPOLYGON.\n\n    Makes a MultiPolygon Geometry from WKT with the given SRID. If SRID is not\n    given, it defaults to 0.\n\n\n    This is exposing ``st_mpolyfromtext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::mpolyfromwkb( \\\n                    a0: std::bytes \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::mpolyfromwkb( \\\n                    a0: std::bytes, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_mpolyfromwkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::multi( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_multi``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::multilinefromwkb( \\\n                    a0: std::bytes \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_multilinefromwkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::multilinestringfromtext( \\\n                    a0: std::str \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::multilinestringfromtext( \\\n                    a0: std::str, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_multilinestringfromtext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::multipointfromtext( \\\n                    a0: std::str \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_multipointfromtext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::multipointfromwkb( \\\n                    a0: std::bytes \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::multipointfromwkb( \\\n                    a0: std::bytes, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_multipointfromwkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::multipolyfromwkb( \\\n                    a0: std::bytes \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::multipolyfromwkb( \\\n                    a0: std::bytes, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_multipolyfromwkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::multipolygonfromtext( \\\n                    a0: std::str \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::multipolygonfromtext( \\\n                    a0: std::str, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_multipolygonfromtext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::ndims( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::int16\n\n    This is exposing ``st_ndims``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::node( \\\n                    g: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_node``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::normalize( \\\n                    geom: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_normalize``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::npoints( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::int64\n\n    This is exposing ``st_npoints``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::nrings( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::int64\n\n    This is exposing ``st_nrings``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::numcurves( \\\n                    geometry: ext::postgis::geometry \\\n                  ) ->  std::int64\n\n    This is exposing ``st_numcurves``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::numgeometries( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::int64\n\n    This is exposing ``st_numgeometries``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::numinteriorring( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::int64\n\n    This is exposing ``st_numinteriorring``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::numinteriorrings( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::int64\n\n    This is exposing ``st_numinteriorrings``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::numpatches( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::int64\n\n    Return the number of faces on a Polyhedral Surface.\n\n    This is exposing ``st_numpatches``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::numpoints( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::int64\n\n    This is exposing ``st_numpoints``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::offsetcurve( \\\n                    line: ext::postgis::geometry, \\\n                    distance: std::float64, \\\n                    params: std::str = '', \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_offsetcurve``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::orderingequals( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    Tests if two geometries are the same geometry including points order.\n\n    Tests if two geometries represent the same geometry and have points in the same directional order.\n\n\n    This is exposing ``st_orderingequals``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::orientedenvelope( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_orientedenvelope``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::overlaps( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    Tests if two geometries overlap.\n\n    Tests if two geometries have the same dimension and intersect, but each\n    has at least one point not in the other.\n\n\n    This is exposing ``st_overlaps``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::patchn( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_patchn``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::perimeter( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::float64\n                  ext::postgis::perimeter( \\\n                    geog: ext::postgis::geography, \\\n                    use_spheroid: std::bool = true, \\\n                  ) ->  std::float64\n\n    This is exposing ``st_perimeter``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::perimeter2d( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::float64\n\n    This is exposing ``st_perimeter2d``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::perimeter3d( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::float64\n\n    This is exposing ``st_3dperimeter``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::point( \\\n                    a0: std::float64, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::point( \\\n                    a0: std::float64, \\\n                    a1: std::float64, \\\n                    srid: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_point``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::pointfromgeohash( \\\n                    a0: optional std::str, \\\n                    a1: optional std::int64 = {}, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_pointfromgeohash``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::pointfromtext( \\\n                    a0: std::str \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::pointfromtext( \\\n                    a0: std::str, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    Makes a POINT geometry from WKT.\n\n    Makes a POINT geometry from WKT with the given SRID. If SRID is not given,\n    it defaults to unknown.\n\n\n    This is exposing ``st_pointfromtext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::pointfromwkb( \\\n                    a0: std::bytes \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::pointfromwkb( \\\n                    a0: std::bytes, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_pointfromwkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::pointinsidecircle( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                    a3: std::float64, \\\n                  ) ->  std::bool\n\n    This is exposing ``st_pointinsidecircle``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::pointm( \\\n                    xcoordinate: std::float64, \\\n                    ycoordinate: std::float64, \\\n                    mcoordinate: std::float64, \\\n                    srid: std::int64 = 0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_pointm``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::pointn( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    Returns the Nth point in the first LineString in a geometry.\n\n    Returns the Nth point in the first LineString or circular LineString in a\n    geometry.\n\n\n    This is exposing ``st_pointn``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::pointonsurface( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_pointonsurface``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::points( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_points``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::pointz( \\\n                    xcoordinate: std::float64, \\\n                    ycoordinate: std::float64, \\\n                    zcoordinate: std::float64, \\\n                    srid: std::int64 = 0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_pointz``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::pointzm( \\\n                    xcoordinate: std::float64, \\\n                    ycoordinate: std::float64, \\\n                    zcoordinate: std::float64, \\\n                    mcoordinate: std::float64, \\\n                    srid: std::int64 = 0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_pointzm``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::polyfromtext( \\\n                    a0: std::str \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::polyfromtext( \\\n                    a0: std::str, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_polyfromtext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::polyfromwkb( \\\n                    a0: std::bytes \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::polyfromwkb( \\\n                    a0: std::bytes, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_polyfromwkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::polygon( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_polygon``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::polygonfromtext( \\\n                    a0: std::str \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::polygonfromtext( \\\n                    a0: std::str, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    Creates a geometry from WKT POLYGON.\n\n    Makes a Geometry from WKT with the given SRID. If SRID is not given, it defaults to 0.\n\n\n    This is exposing ``st_polygonfromtext``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::polygonfromwkb( \\\n                    a0: std::bytes \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::polygonfromwkb( \\\n                    a0: std::bytes, \\\n                    a1: std::int64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_polygonfromwkb``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::polygonize( \\\n                    a0: array<ext::postgis::geometry> \\\n                  ) ->  ext::postgis::geometry\n\n    Computes a collection of polygons formed from a set of linework.\n\n    Computes a collection of polygons formed from the linework of a set of\n    geometries.\n\n\n    This is exposing ``st_polygonize``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_addbbox( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``postgis_addbbox``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_constraint_dims( \\\n                    geomschema: std::str, \\\n                    geomtable: std::str, \\\n                    geomcolumn: std::str, \\\n                  ) ->  std::int64\n\n    This is exposing ``postgis_constraint_dims``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_constraint_srid( \\\n                    geomschema: std::str, \\\n                    geomtable: std::str, \\\n                    geomcolumn: std::str, \\\n                  ) ->  std::int64\n\n    This is exposing ``postgis_constraint_srid``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_dropbbox( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``postgis_dropbbox``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_full_version( \\\n                     \\\n                  ) -> optional std::str\n\n    This is exposing ``postgis_full_version``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_geos_compiled_version( \\\n                     \\\n                  ) -> optional std::str\n\n    This is exposing ``postgis_geos_compiled_version``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_geos_noop( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``postgis_geos_noop``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_geos_version( \\\n                     \\\n                  ) -> optional std::str\n\n    This is exposing ``postgis_geos_version``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_getbbox( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::box2d\n\n    This is exposing ``postgis_getbbox``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_hasbbox( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::bool\n\n    This is exposing ``postgis_hasbbox``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_lib_build_date( \\\n                     \\\n                  ) -> optional std::str\n\n    This is exposing ``postgis_lib_build_date``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_lib_revision( \\\n                     \\\n                  ) -> optional std::str\n\n    This is exposing ``postgis_lib_revision``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_lib_version( \\\n                     \\\n                  ) -> optional std::str\n\n    This is exposing ``postgis_lib_version``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_libjson_version( \\\n                     \\\n                  ) ->  std::str\n\n    This is exposing ``postgis_libjson_version``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_liblwgeom_version( \\\n                     \\\n                  ) -> optional std::str\n\n    This is exposing ``postgis_liblwgeom_version``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_libprotobuf_version( \\\n                     \\\n                  ) ->  std::str\n\n    This is exposing ``postgis_libprotobuf_version``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_libxml_version( \\\n                     \\\n                  ) -> optional std::str\n\n    This is exposing ``postgis_libxml_version``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_noop( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``postgis_noop``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_proj_compiled_version( \\\n                     \\\n                  ) -> optional std::str\n\n    This is exposing ``postgis_proj_compiled_version``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_proj_version( \\\n                     \\\n                  ) -> optional std::str\n\n    This is exposing ``postgis_proj_version``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_scripts_build_date( \\\n                     \\\n                  ) -> optional std::str\n\n    This is exposing ``postgis_scripts_build_date``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_scripts_installed( \\\n                     \\\n                  ) -> optional std::str\n\n    This is exposing ``postgis_scripts_installed``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_scripts_released( \\\n                     \\\n                  ) -> optional std::str\n\n    This is exposing ``postgis_scripts_released``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_srs_codes( \\\n                    auth_name: std::str \\\n                  ) ->  std::str\n\n    This is exposing ``postgis_srs_codes``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_svn_version( \\\n                     \\\n                  ) -> optional std::str\n\n    This is exposing ``postgis_svn_version``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_transform_geometry( \\\n                    geom: ext::postgis::geometry, \\\n                    a1: std::str, \\\n                    a2: std::str, \\\n                    a3: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``postgis_transform_geometry``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_transform_pipeline_geometry( \\\n                    geom: ext::postgis::geometry, \\\n                    pipeline: std::str, \\\n                    forward: std::bool, \\\n                    to_srid: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``postgis_transform_pipeline_geometry``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_typmod_dims( \\\n                    a0: std::int64 \\\n                  ) ->  std::int64\n\n    This is exposing ``postgis_typmod_dims``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_typmod_srid( \\\n                    a0: std::int64 \\\n                  ) ->  std::int64\n\n    This is exposing ``postgis_typmod_srid``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_typmod_type( \\\n                    a0: std::int64 \\\n                  ) ->  std::str\n\n    This is exposing ``postgis_typmod_type``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_version( \\\n                     \\\n                  ) -> optional std::str\n\n    This is exposing ``postgis_version``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::postgis_wagyu_version( \\\n                     \\\n                  ) -> optional std::str\n\n    This is exposing ``postgis_wagyu_version``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::project( \\\n                    geom1: ext::postgis::geometry, \\\n                    distance: std::float64, \\\n                    azimuth: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::project( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    distance: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::project( \\\n                    geog_from: ext::postgis::geography, \\\n                    geog_to: ext::postgis::geography, \\\n                    distance: std::float64, \\\n                  ) ->  ext::postgis::geography\n                  ext::postgis::project( \\\n                    geog: optional ext::postgis::geography, \\\n                    distance: optional std::float64, \\\n                    azimuth: optional std::float64, \\\n                  ) -> optional ext::postgis::geography\n\n    Returns a point projected from a start point by a distance and bearing.\n\n    This is exposing ``st_project``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::quantizecoordinates( \\\n                    g: optional ext::postgis::geometry, \\\n                    prec_x: optional std::int64, \\\n                    prec_y: optional std::int64 = {}, \\\n                    prec_z: optional std::int64 = {}, \\\n                    prec_m: optional std::int64 = {}, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_quantizecoordinates``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::reduceprecision( \\\n                    geom: ext::postgis::geometry, \\\n                    gridsize: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_reduceprecision``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::relate( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::str\n                  ext::postgis::relate( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    a2: std::str, \\\n                  ) ->  std::bool\n                  ext::postgis::relate( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    a2: std::int64, \\\n                  ) ->  std::str\n\n    Tests if two geometries have a topological relationship.\n\n    Tests if two geometries have a topological relationship matching an\n    Intersection Matrix pattern, or computes their Intersection Matrix.\n\n\n    This is exposing ``st_relate``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::relatematch( \\\n                    a0: std::str, \\\n                    a1: std::str, \\\n                  ) ->  std::bool\n\n    This is exposing ``st_relatematch``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::removeirrelevantpointsforview( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: ext::postgis::box2d, \\\n                    a2: std::bool = false, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_removeirrelevantpointsforview``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::removepoint( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_removepoint``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::removerepeatedpoints( \\\n                    geom: ext::postgis::geometry, \\\n                    tolerance: std::float64 = 0.0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_removerepeatedpoints``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::removesmallparts( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_removesmallparts``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::reverse( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_reverse``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::rotate( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::rotate( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    a2: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::rotate( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                    a3: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_rotate``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::rotatex( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_rotatex``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::rotatey( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_rotatey``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::rotatez( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_rotatez``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::scale( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::scale( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::scale( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                    a3: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::scale( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: ext::postgis::geometry, \\\n                    origin: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_scale``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::scroll( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_scroll``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::segmentize( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::segmentize( \\\n                    geog: ext::postgis::geography, \\\n                    max_segment_length: std::float64, \\\n                  ) ->  ext::postgis::geography\n\n    Makes a new geometry/geography with no segment longer than a given\n    distance.\n\n\n    This is exposing ``st_segmentize``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::seteffectivearea( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64 = -1, \\\n                    a2: std::int64 = 1, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_seteffectivearea``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::setpoint( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::int64, \\\n                    a2: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_setpoint``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::setsrid( \\\n                    geom: ext::postgis::geometry, \\\n                    srid: std::int64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::setsrid( \\\n                    geog: ext::postgis::geography, \\\n                    srid: std::int64, \\\n                  ) ->  ext::postgis::geography\n\n    This is exposing ``st_setsrid``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::sharedpaths( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_sharedpaths``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::shiftlongitude( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_shiftlongitude``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::shortestline( \\\n                    a0: optional std::str, \\\n                    a1: optional std::str, \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::shortestline( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::shortestline( \\\n                    a0: ext::postgis::geography, \\\n                    a1: ext::postgis::geography, \\\n                    use_spheroid: std::bool = true, \\\n                  ) ->  ext::postgis::geography\n\n    This is exposing ``st_shortestline``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::shortestline3d( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_3dshortestline``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::simplify( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::simplify( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    a2: std::bool, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_simplify``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::simplifypolygonhull( \\\n                    geom: ext::postgis::geometry, \\\n                    vertex_fraction: std::float64, \\\n                    is_outer: std::bool = true, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_simplifypolygonhull``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::simplifypreservetopology( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_simplifypreservetopology``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::simplifyvw( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_simplifyvw``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::snap( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    a2: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_snap``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::snaptogrid( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::snaptogrid( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::snaptogrid( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                    a3: std::float64, \\\n                    a4: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::snaptogrid( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    a2: std::float64, \\\n                    a3: std::float64, \\\n                    a4: std::float64, \\\n                    a5: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_snaptogrid``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::split( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_split``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::square( \\\n                    size: std::float64, \\\n                    cell_i: std::int64, \\\n                    cell_j: std::int64, \\\n                    origin: ext::postgis::geometry = 'POINT(0 0)', \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_square``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::srid( \\\n                    geom: ext::postgis::geometry \\\n                  ) ->  std::int64\n                  ext::postgis::srid( \\\n                    geog: ext::postgis::geography \\\n                  ) ->  std::int64\n\n    This is exposing ``st_srid``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::startpoint( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_startpoint``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::subdivide( \\\n                    geom: ext::postgis::geometry, \\\n                    maxvertices: std::int64 = 256, \\\n                    gridsize: std::float64 = -1.0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_subdivide``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::summary( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::str\n                  ext::postgis::summary( \\\n                    a0: ext::postgis::geography \\\n                  ) ->  std::str\n\n    This is exposing ``st_summary``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::symdifference( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    gridsize: std::float64 = -1.0, \\\n                  ) ->  ext::postgis::geometry\n\n    Merges two geometries excluding where they intersect.\n\n    This is exposing ``st_symdifference``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::symmetricdifference( \\\n                    geom1: optional ext::postgis::geometry, \\\n                    geom2: optional ext::postgis::geometry, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_symmetricdifference``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::tileenvelope( \\\n                    zoom: std::int64, \\\n                    x: std::int64, \\\n                    y: std::int64, \\\n                    bounds: ext::postgis::geometry = <ext::postgis::geometry>'SRID=3857;LINESTRING(-20037508.342789244 -20037508.342789244, \\\n                    20037508.342789244 20037508.342789244)', \\\n                    margin: std::float64 = 0.0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_tileenvelope``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::to_box2d( \\\n                    a0: ext::postgis::box3d \\\n                  ) ->  ext::postgis::box2d\n                  ext::postgis::to_box2d( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::box2d\n\n    This is exposing ``box2d``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::to_box3d( \\\n                    a0: ext::postgis::box2d \\\n                  ) ->  ext::postgis::box3d\n                  ext::postgis::to_box3d( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::box3d\n\n    This is exposing ``box3d``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::to_geography( \\\n                    a0: std::bytes \\\n                  ) ->  ext::postgis::geography\n                  ext::postgis::to_geography( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geography\n\n    This is exposing ``geography``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::to_geometry( \\\n                    a0: std::str \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::to_geometry( \\\n                    a0: std::bytes \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::to_geometry( \\\n                    a0: ext::postgis::box2d \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::to_geometry( \\\n                    a0: ext::postgis::box3d \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::to_geometry( \\\n                    a0: ext::postgis::geography \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``geometry``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::touches( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    Tests if two geometries touch without intersecting interiors.\n\n    Tests if two geometries have at least one point in common, but their interiors do not intersect.\n\n\n    This is exposing ``st_touches``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::transform( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::int64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::transform( \\\n                    geom: ext::postgis::geometry, \\\n                    to_proj: std::str, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::transform( \\\n                    geom: ext::postgis::geometry, \\\n                    from_proj: std::str, \\\n                    to_proj: std::str, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::transform( \\\n                    geom: ext::postgis::geometry, \\\n                    from_proj: std::str, \\\n                    to_srid: std::int64, \\\n                  ) ->  ext::postgis::geometry\n\n    Transforms a geometry to a different spatial reference system.\n\n    Returns a new geometry with coordinates transformed to a different spatial reference system.\n\n\n    This is exposing ``st_transform``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::transformpipeline( \\\n                    geom: ext::postgis::geometry, \\\n                    pipeline: std::str, \\\n                    to_srid: std::int64 = 0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_transformpipeline``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::translate( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::translate( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                    a3: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_translate``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::transscale( \\\n                    a0: ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                    a2: std::float64, \\\n                    a3: std::float64, \\\n                    a4: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_transscale``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::triangulatepolygon( \\\n                    g1: ext::postgis::geometry \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_triangulatepolygon``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::unaryunion( \\\n                    a0: ext::postgis::geometry, \\\n                    gridsize: std::float64 = -1.0, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_unaryunion``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::union( \\\n                    a0: array<ext::postgis::geometry> \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::union( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  ext::postgis::geometry\n                  ext::postgis::union( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                    gridsize: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_union``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::voronoilines( \\\n                    g1: optional ext::postgis::geometry, \\\n                    tolerance: optional std::float64 = 0.0, \\\n                    extend_to: optional ext::postgis::geometry = {}, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_voronoilines``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::voronoipolygons( \\\n                    g1: optional ext::postgis::geometry, \\\n                    tolerance: optional std::float64 = 0.0, \\\n                    extend_to: optional ext::postgis::geometry = {}, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_voronoipolygons``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::within( \\\n                    geom1: ext::postgis::geometry, \\\n                    geom2: ext::postgis::geometry, \\\n                  ) ->  std::bool\n\n    This is exposing ``st_within``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::wrapx( \\\n                    geom: ext::postgis::geometry, \\\n                    wrap: std::float64, \\\n                    `move`: std::float64, \\\n                  ) ->  ext::postgis::geometry\n\n    This is exposing ``st_wrapx``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::x( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::float64\n\n    This is exposing ``st_x``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::xmax( \\\n                    a0: ext::postgis::box3d \\\n                  ) ->  std::float64\n\n    This is exposing ``st_xmax``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::xmin( \\\n                    a0: ext::postgis::box3d \\\n                  ) ->  std::float64\n\n    This is exposing ``st_xmin``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::y( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::float64\n\n    This is exposing ``st_y``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::ymax( \\\n                    a0: ext::postgis::box3d \\\n                  ) ->  std::float64\n\n    This is exposing ``st_ymax``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::ymin( \\\n                    a0: ext::postgis::box3d \\\n                  ) ->  std::float64\n\n    This is exposing ``st_ymin``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::z( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::float64\n\n    This is exposing ``st_z``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::zmax( \\\n                    a0: ext::postgis::box3d \\\n                  ) ->  std::float64\n\n    This is exposing ``st_zmax``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::zmflag( \\\n                    a0: ext::postgis::geometry \\\n                  ) ->  std::int16\n\n    This is exposing ``st_zmflag``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::zmin( \\\n                    a0: ext::postgis::box3d \\\n                  ) ->  std::float64\n\n    This is exposing ``st_zmin``.\n\n\nAggregates\n==========\n\nThese functions operate of sets of geometric data.\n\n----------\n\n\n.. eql:function:: ext::postgis::clusterintersecting_agg( \\\n                    a0: set of ext::postgis::geometry \\\n                  ) -> optional array<ext::postgis::geometry>\n\n    This is exposing ``st_clusterintersecting``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::clusterwithin_agg( \\\n                    a0: set of ext::postgis::geometry, \\\n                    a1: std::float64, \\\n                  ) -> optional array<ext::postgis::geometry>\n\n    This is exposing ``st_clusterwithin``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::collect_agg( \\\n                    a0: set of ext::postgis::geometry \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_collect``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::coverageunion_agg( \\\n                    a0: set of ext::postgis::geometry \\\n                  ) -> optional ext::postgis::geometry\n\n    Computes polygonal coverage from a set of polygons.\n\n    Computes the union of a set of polygons forming a coverage by removing\n    shared edges.\n\n\n\n    This is exposing ``st_coverageunion``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::extent3d_agg( \\\n                    a0: set of ext::postgis::geometry \\\n                  ) -> optional ext::postgis::box2d\n\n    This is exposing ``st_3dextent``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::extent_agg( \\\n                    a0: set of ext::postgis::geometry \\\n                  ) -> optional ext::postgis::box2d\n\n    This is exposing ``st_extent``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::makeline_agg( \\\n                    a0: set of ext::postgis::geometry \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_makeline``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::memunion_agg( \\\n                    a0: set of ext::postgis::geometry \\\n                  ) -> optional ext::postgis::box2d\n\n    This is exposing ``st_memunion``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::polygonize_agg( \\\n                    a0: set of ext::postgis::geometry \\\n                  ) -> optional ext::postgis::geometry\n\n    Computes a collection of polygons formed from a set of linework.\n\n    Computes a collection of polygons formed from the linework of a set of\n    geometries.\n\n\n    This is exposing ``st_polygonize``.\n\n\n----------\n\n\n.. eql:function:: ext::postgis::union_agg( \\\n                    a0: set of ext::postgis::geometry \\\n                  ) -> optional ext::postgis::geometry\n                  ext::postgis::union_agg( \\\n                    a0: set of ext::postgis::geometry, \\\n                    gridsize: std::float64, \\\n                  ) -> optional ext::postgis::geometry\n\n    This is exposing ``st_union``.\n\n\n.. _postgis:\n    https://postgis.net/docs/manual-3.5/\n"
  },
  {
    "path": "docs/reference/stdlib/range.rst",
    "content": ".. _ref_std_range:\n\n======\nRanges\n======\n\n:edb-alt-title: Range Functions and Operators\n\nRanges represent some interval of values. The intervals can include or exclude\ntheir boundaries or can even omit one or both boundaries. Only some scalar\ntypes have corresponding range types:\n\n- ``range<int32>``\n- ``range<int64>``\n- ``range<float32>``\n- ``range<float64>``\n- ``range<decimal>``\n- ``range<datetime>``\n- ``range<cal::local_datetime>``\n- ``range<cal::local_date>``\n\nConstructing ranges\n^^^^^^^^^^^^^^^^^^^\n\nThere's a special :eql:func:`range` constructor function for making range\nvalues. This is a little different from how scalars, arrays and tuples are\ncreated typically in Gel.\n\nFor example:\n\n.. code-block:: edgeql-repl\n\n    db> select range(1, 10);\n    {range(1, 10)}\n    db> select range(2.2, 3.3);\n    {range(2.2, 3.3)}\n\nBroadly there are two kinds of ranges: :eql:type:`discrete <anydiscrete>` and\n:eql:type:`contiguous <anycontiguous>`. The discrete ranges are\n``range<int32>``, ``range<int64>``, and ``range<cal::local_date>``. All ranges\nover discrete types get normalized such that the lower bound is included\n(if present) and the upper bound is excluded:\n\n.. code-block:: edgeql-repl\n\n    db> select range(1, 10) = range(1, 9, inc_upper := true);\n    {true}\n    db> select range(1, 10) = range(0, 10, inc_lower := false);\n    {true}\n\nRanges over contiguous types don't have the same normalization mechanism\nbecause the underlying types don't have granularity which could be used to\neasily include or exclude a boundary value.\n\nSometimes a range cannot contain any values, this is called an *empty* range.\nThese kinds of ranges can arise from performing various operations on them,\nbut they can also be constructed. There are basically two equivalent ways of\nconstructing an *empty* range. It can be explicitly constructed by providing\nthe same upper and lower bounds and specifying that at least one of them is\nnot *inclusive* (which is the default for all range constructors):\n\n.. code-block:: edgeql-repl\n\n    db> select range(1, 1);\n    {range({}, inc_lower := false, empty := true)}\n\nAlternatively, it's possible to specify ``{}`` as a boundary and also provide\nthe ``empty := true`` named-only argument. If the empty set is provided as a\nliteral, it also needs to have a type cast, to specify which type of the range\nis being constructed:\n\n.. code-block:: edgeql-repl\n\n    db> select range(<int64>{}, empty := true);\n    {range({}, inc_lower := false, empty := true)}\n\nSince empty ranges contain no values, they are all considered to be equal to\neach other (as long as the types are compatible):\n\n.. code-block:: edgeql-repl\n\n    db> select range(1, 1) = range(<int64>{}, empty := true);\n    {true}\n    db> select range(1, 1) = range(42.0, 42.0);\n    {true}\n\n    db> select range(1, 1) = range(<cal::local_date>{}, empty := true);\n    error: InvalidTypeError: operator '=' cannot be applied to operands of\n    type 'range<std::int64>' and 'range<cal::local_date>'\n      ┌─ query:1:8\n      │\n    1 │ select range(1, 1) = range(<cal::local_date>{}, empty := true);\n      │        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n      Consider using an explicit type cast or a conversion function.\n\n\nJSON representation\n^^^^^^^^^^^^^^^^^^^\n\nMuch like :ref:`arrays<ref_std_array>` and :ref:`tuples<ref_std_tuple>`, the\nrange types cannot be directly cast to a :eql:type:`str`, but instead can be\ncast into a :eql:type:`json` structure:\n\n.. code-block:: edgeql-repl\n\n    db> select <json>range(1, 10);\n    {\"inc_lower\": true, \"inc_upper\": false, \"lower\": 1, \"upper\": 10}\n\nIt's also possible to cast in the other direction - from :eql:type:`json` to a\nspecific range type:\n\n.. code-block:: edgeql-repl\n\n    db> select <range<int64>>to_json('{\n    ...   \"lower\": 1,\n    ...   \"inc_lower\": true,\n    ...   \"upper\": 10,\n    ...   \"inc_upper\": false\n    ... }');\n    {range(1, 10)}\n\nEmpty ranges have a shorthand :eql:type:`json` representation:\n\n.. code-block:: edgeql-repl\n\n    db> select <json>range(<int64>{}, empty := true);\n    {\"empty\": true}\n\nWhen casting from :eql:type:`json` to an empty range, all other fields may be\nomitted, but if they are present, they must be consistent with an empty range:\n\n.. code-block:: edgeql-repl\n\n    db> select <range<int64>>to_json('{\"empty\": true}');\n    {range({}, inc_lower := false, empty := true)}\n\n    db> select <range<int64>>to_json('{\n    ...   \"lower\": 1,\n    ...   \"inc_lower\": true,\n    ...   \"upper\": 1,\n    ...   \"inc_upper\": false\n    ... }');\n    {range({}, inc_lower := false, empty := true)}\n\n    db> select <range<int64>>to_json('{\n    ...   \"lower\": 1,\n    ...   \"inc_lower\": true,\n    ...   \"upper\": 1,\n    ...   \"inc_upper\": false,\n    ...   \"empty\": true\n    ... }');\n    {range({}, inc_lower := false, empty := true)}\n\n    db> select <range<int64>>to_json('{\n    ...   \"lower\": 1,\n    ...   \"inc_lower\": true,\n    ...   \"upper\": 2,\n    ...   \"inc_upper\": false,\n    ...   \"empty\": true\n    ... }');\n    gel error: InvalidValueError: conflicting arguments in range\n    constructor: \"empty\" is ``true`` while the specified bounds suggest\n    otherwise\n\n.. note::\n\n  When casting from :eql:type:`json` to a range the ``lower`` and ``upper``\n  fields are optional, but the *inclusivity* fields ``inc_lower`` and\n  ``inc_upper`` are *mandatory*. This is to address the fact that whether the\n  range boundaries are included by default can vary based on system or context\n  and being explicit avoids subtle errors. The only exception to this are\n  empty ranges that can have just the ``\"empty\": true`` field.\n\n\n.. _ref_std_multirange:\n\nMultiranges\n^^^^^^^^^^^\n\n.. versionadded:: 4.0\n\nIntermittent availability or ranges with gaps can be naturally represented by\na set of ranges. However, using a :eql:func:`multirange` for this purpose is\neven better. At its core a multirange is a set of ranges packaged together\nso that it's easy to perform range operations on the whole set:\n\n.. code-block:: edgeql-repl\n\n    db> select multirange([range(1, 5), range(8,10)]);\n    {[range(1, 5), range(8, 10)]}\n    db> select contains(multirange([range(1, 5), range(8,10)]), 9);\n    true\n\nAnother advantage of a multirange is that its components are always\nautomatically ordered and normalized to be non-overlapping, even if it's\nconstructed from an array of ranges that don't satisfy either of these\nconditions:\n\n.. code-block:: edgeql-repl\n\n    db> select multirange([range(8, 10), range(1, 4), range(2, 5)]);\n    {[range(1, 5), range(8, 10)]}\n\nMultiranges are compatible with ranges for the purpose of most operations,\nmaking it more conveninet to manipulate them whenever you have more than one\nrange to work with:\n\n.. code-block:: edgeql-repl\n\n    db> select multirange([range(8, 10)]) + range(1, 5) - range(3, 4);\n    {[range(1, 3), range(4, 5), range(8, 10)]}\n\n\nFunctions and operators\n^^^^^^^^^^^^^^^^^^^^^^^\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:op:`range \\< range <rangelt>`\n      - :eql:op-desc:`rangelt`\n    * - :eql:op:`range \\> range <rangegt>`\n      - :eql:op-desc:`rangegt`\n    * - :eql:op:`range \\<= range <rangelteq>`\n      - :eql:op-desc:`rangelteq`\n    * - :eql:op:`range \\>= range <rangegteq>`\n      - :eql:op-desc:`rangegteq`\n    * - :eql:op:`range + range <rangeplus>`\n      - :eql:op-desc:`rangeplus`\n    * - :eql:op:`range - range <rangeminus>`\n      - :eql:op-desc:`rangeminus`\n    * - :eql:op:`range * range <rangemult>`\n      - :eql:op-desc:`rangemult`\n    * - :eql:func:`range`\n      - :eql:func-desc:`range`\n    * - :eql:func:`range_get_lower`\n      - :eql:func-desc:`range_get_lower`\n    * - :eql:func:`range_get_upper`\n      - :eql:func-desc:`range_get_upper`\n    * - :eql:func:`range_is_inclusive_lower`\n      - :eql:func-desc:`range_is_inclusive_lower`\n    * - :eql:func:`range_is_inclusive_upper`\n      - :eql:func-desc:`range_is_inclusive_upper`\n    * - :eql:func:`range_is_empty`\n      - :eql:func-desc:`range_is_empty`\n    * - :eql:func:`range_unpack`\n      - :eql:func-desc:`range_unpack`\n    * - :eql:func:`contains`\n      - Check if an element or a range is within another range.\n    * - :eql:func:`overlaps`\n      - :eql:func-desc:`overlaps`\n    * - :eql:func:`adjacent`\n      - :eql:func-desc:`adjacent`\n    * - :eql:func:`strictly_above`\n      - :eql:func-desc:`strictly_above`\n    * - :eql:func:`strictly_below`\n      - :eql:func-desc:`strictly_below`\n    * - :eql:func:`bounded_above`\n      - :eql:func-desc:`bounded_above`\n    * - :eql:func:`bounded_below`\n      - :eql:func-desc:`bounded_below`\n    * - :eql:func:`multirange`\n      - :eql:func-desc:`multirange`\n    * - :eql:func:`multirange_unpack`\n      - :eql:func-desc:`multirange_unpack`\n\n\n\nReference\n^^^^^^^^^\n\n.. eql:operator:: rangelt: range<anypoint> < range<anypoint> -> bool\n                  multirange<anypoint> < multirange<anypoint> -> bool\n\n    .. index:: less than, comparison\n    .. api-index:: §range §<§ range§\n\n    One range or multirange is before the other.\n\n    Returns ``true`` if the lower bound of the first range or multirange is\n    smaller than the lower bound of the second range or multirange. The\n    unspecified lower bound is considered to be smaller than any specified\n    lower bound. If the lower bounds are equal then the upper bounds are\n    compared. Unspecified upper bound is considered to be greater than any\n    specified upper bound.\n\n    .. code-block:: edgeql-repl\n\n        db> select range(1, 10) < range(2, 5);\n        {true}\n        db> select range(1, 10) < range(1, 15);\n        {true}\n        db> select range(1, 10) < range(1);\n        {true}\n        db> select range(1, 10) < range(<int64>{}, 10);\n        {false}\n\n        db> select multirange([range(2, 4), range(5, 7)]) <\n        ...   multirange([range(7, 10), range(20)]);\n        {true}\n\n    An empty range is considered to come before any non-empty range.\n\n    .. code-block:: edgeql-repl\n\n        db> select range(1, 10) < range(10, 10);\n        {false}\n        db> select range(1, 10) < range(<int64>{}, empty := true);\n        {false}\n\n        db> select multirange(<array<range<int64>>>[]) <\n        ...   multirange([range(7, 10), range(20)]);\n        {true}\n\n    This is also how the ``order by`` clauses compares ranges.\n\n\n----------\n\n\n.. eql:operator:: rangegt: range<anypoint> > range<anypoint> -> bool\n                  multirange<anypoint> > multirange<anypoint> -> bool\n\n    .. index:: greater than, comparison\n    .. api-index:: §range §>§ range§\n\n    One range or multirange is after the other.\n\n    Returns ``true`` if the lower bound of the first range  or multirange is\n    greater than the lower bound of the second range or multirange. The\n    unspecified lower bound is considered to be smaller than any specified\n    lower bound. If the lower bounds are equal then the upper bounds are\n    compared. Unspecified upper bound is considered to be greater than any\n    specified upper bound.\n\n    .. code-block:: edgeql-repl\n\n        db> select range(1, 10) > range(2, 5);\n        {false}\n        db> select range(1, 10) > range(1, 5);\n        {true}\n        db> select range(1, 10) > range(1);\n        {false}\n        db> select range(1, 10) > range(<int64>{}, 10);\n        {true}\n\n        db> select multirange([range(2, 4), range(5, 7)]) >\n        ...   multirange([range(7, 10), range(20)]);\n        {false}\n\n    An empty range is considered to come before any non-empty range.\n\n    .. code-block:: edgeql-repl\n\n        db> select range(1, 10) > range(10, 10);\n        {true}\n        db> select range(1, 10) > range(<int64>{}, empty := true);\n        {true}\n\n        db> select multirange(<array<range<int64>>>[]) >\n        ...   multirange([range(7, 10), range(20)]);\n        {false}\n\n    This is also how the ``order by`` clauses compares ranges.\n\n\n----------\n\n\n.. eql:operator:: rangelteq: range<anypoint> <= range<anypoint> -> bool\n                  multirange<anypoint> <= multirange<anypoint> -> bool\n\n    .. index:: less than or equal, comparison\n    .. api-index:: §range §<=§ range§\n\n    One range or multirange is before or same as the other.\n\n    Returns ``true`` if the ranges or multiranges are identical or if the\n    lower bound of the first one is smaller than the lower bound of the second\n    one. The unspecified lower bound is considered to be smaller than any\n    specified lower bound. If the lower bounds are equal then the upper bounds\n    are compared. Unspecified upper bound is considered to be greater than any\n    specified upper bound.\n\n    .. code-block:: edgeql-repl\n\n        db> select range(1, 10) <= range(1, 10);\n        {true}\n        db> select range(1, 10) <= range(2, 5);\n        {true}\n        db> select range(1, 10) <= range(1, 15);\n        {true}\n        db> select range(1, 10) <= range(1);\n        {true}\n        db> select range(1, 10) <= range(<int64>{}, 10);\n        {false}\n\n        db> select multirange([range(2, 4), range(5, 7)]) <=\n        ...   multirange([range(7, 10), range(20)]);\n        {true}\n        db> select multirange([range(2, 4), range(5, 7)]) <=\n        ...   multirange([range(5, 7), range(2, 4)]);\n        {true}\n\n    An empty range is considered to come before any non-empty range.\n\n    .. code-block:: edgeql-repl\n\n        db> select range(1, 10) <= range(10, 10);\n        {false}\n        db> select range(1, 1) <= range(10, 10);\n        {true}\n        db> select range(1, 10) <= range(<int64>{}, empty := true);\n        {false}\n\n        db> select multirange(<array<range<int64>>>[]) <=\n        ...   multirange([range(7, 10), range(20)]);\n        {true}\n\n    This is also how the ``order by`` clauses compares ranges.\n\n\n----------\n\n\n.. eql:operator:: rangegteq: range<anypoint> >= range<anypoint> -> bool\n                  multirange<anypoint> >= multirange<anypoint> -> bool\n\n    .. index:: greater than or equal, comparison\n    .. api-index:: §range §>=§ range§\n\n    One range or multirange is after or same as the other.\n\n    Returns ``true`` if the ranges or multiranges are identical or if the\n    lower bound of the first one is greater than the lower bound of the second\n    one. The unspecified lower bound is considered to be smaller than any\n    specified lower bound. If the lower bounds are equal then the upper bounds\n    are compared. Unspecified upper bound is considered to be greater than any\n    specified upper bound.\n\n    .. code-block:: edgeql-repl\n\n        db> select range(1, 10) >= range(2, 5);\n        {false}\n        db> select range(1, 10) >= range(1, 10);\n        {true}\n        db> select range(1, 10) >= range(1, 5);\n        {true}\n        db> select range(1, 10) >= range(1);\n        {false}\n        db> select range(1, 10) >= range(<int64>{}, 10);\n        {true}\n\n        db> select multirange([range(2, 4), range(5, 7)]) >=\n        ...   multirange([range(7, 10), range(20)]);\n        {false}\n        db> select multirange([range(2, 4), range(5, 7)]) >=\n        ...   multirange([range(5, 7), range(2, 4)]);\n        {true}\n\n    An empty range is considered to come before any non-empty range.\n\n    .. code-block:: edgeql-repl\n\n        db> select range(1, 10) >= range(10, 10);\n        {true}\n        db> select range(1, 1) >= range(10, 10);\n        {true}\n        db> select range(1, 10) >= range(<int64>{}, empty := true);\n        {true}\n\n        db> select multirange(<array<range<int64>>>[]) >=\n        ...   multirange([range(7, 10), range(20)]);\n        {false}\n\n    This is also how the ``order by`` clauses compares ranges.\n\n\n.. eql:operator:: rangeplus: range<anypoint> + range<anypoint> \\\n                    -> range<anypoint>\n                  multirange<anypoint> + multirange<anypoint> \\\n                    -> multirange<anypoint>\n\n    .. index:: plus, addition\n    .. api-index:: §range §+§ range§\n\n    Range or multirange union.\n\n    Find the union of two ranges as long as the result is a single range\n    without any discontinuities inside.\n\n    .. code-block:: edgeql-repl\n\n        db> select range(1, 10) + range(5, 15);\n        {range(1, 15)}\n        db> select range(1, 10) + range(5);\n        {range(1, {})}\n\n    If one of the arguments is a multirange, find the union and normalize the\n    result as a multirange.\n\n    .. code-block:: edgeql-repl\n\n        db> select range(1, 3) + multirange([\n        ...   range(7, 10), range(20),\n        ... ]);\n        {[range(1, 3), range(7, 10), range(20, {})]}\n        db> select multirange([range(2, 4), range(5, 8)]) +\n        ...   multirange([range(6, 10), range(20)]);\n        {[range(2, 4), range(5, 10), range(20, {})]}\n\n\n----------\n\n\n.. eql:operator:: rangeminus: range<anypoint> - range<anypoint> \\\n                    -> range<anypoint>\n                  multirange<anypoint> - multirange<anypoint> \\\n                    -> multirange<anypoint>\n\n    .. index:: minus\n    .. api-index:: §range §-§ range§\n\n    Range or multirange subtraction.\n\n    Subtract one range from another. This is only valid if the resulting range\n    does not have any discontinuities inside.\n\n    .. code-block:: edgeql-repl\n\n        db> select range(1, 10) - range(5, 15);\n        {range(1, 5)}\n        db> select range(1, 10) - range(<int64>{}, 5);\n        {range(5, 10)}\n        db> select range(1, 10) - range(0, 15);\n        {range({}, inc_lower := false, empty := true)}\n\n    If one of the arguments is a multirange, treat both arguments as\n    multiranges and perform the multirange subtraction.\n\n    .. code-block:: edgeql-repl\n\n        db> select multirange([range(1, 10)]) -\n        ...   range(4, 6);\n        {[range(1, 4), range(6, 10)]}\n        db> select multirange([range(1, 10)]) -\n        ...   multirange([range(2, 3), range(5, 6), range(9)]);\n        {[range(1, 2), range(3, 5), range(6, 9)]}\n        db> select multirange([range(2, 3), range(5, 6), range(9, 10)]) -\n        ...   multirange([range(-10, 0), range(4, 8)]);\n        {[range(2, 3), range(9, 10)]}\n\n\n----------\n\n\n.. eql:operator:: rangemult: range<anypoint> * range<anypoint> \\\n                    -> range<anypoint>\n                  multirange<anypoint> * multirange<anypoint> \\\n                    -> multirange<anypoint>\n\n    .. api-index:: §range §*§ range§\n\n    Range or multirange intersection.\n\n    Find the intersection of two ranges or multiranges.\n\n    .. code-block:: edgeql-repl\n\n        db> select range(1, 10) * range(5, 15);\n        {range(5, 10)}\n        db> select range(1, 10) * range(-15, 15);\n        {range(1, 10)}\n        db> select range(1) * range(-15, 15);\n        {range(1, 15)}\n        db> select range(10) * range(<int64>{}, 1);\n        {range({}, inc_lower := false, empty := true)}\n\n        db> select multirange([range(1, 10)]) *\n        ...   multirange([range(0, 3), range(5, 6), range(9)]);\n        {[range(1, 3), range(5, 6), range(9, 10)]}\n        db> select multirange([range(2, 3), range(5, 6), range(9, 10)]) *\n        ...   multirange([range(-10, 0), range(4, 8)]);\n        {[range(5, 6)]}\n\n\n----------\n\n\n.. eql:function:: std::range(lower: optional anypoint = {}, \\\n                             upper: optional anypoint = {}, \\\n                             named only inc_lower: bool = true, \\\n                             named only inc_upper: bool = false, \\\n                             named only empty: bool = false) \\\n                    -> range<anypoint>\n\n    Construct a range.\n\n    Either one of *lower* or *upper* bounds can be set to ``{}`` to indicate\n    an unbounded interval.\n\n    By default the *lower* bound is included and the *upper* bound is excluded\n    from the range, but this can be controlled explicitly via the *inc_lower*\n    and *inc_upper* named-only arguments.\n\n    .. code-block:: edgeql-repl\n\n        db> select range(1, 10);\n        {range(1, 10)}\n        db> select range(1.5, 7.5, inc_lower := false);\n        {range(1.5, 7.5, inc_lower := false)}\n\n    Finally, an empty range can be created by using the *empty* named-only\n    flag. The first argument still needs to be passed as an ``{}`` so that the\n    type of the range can be inferred from it.\n\n    .. code-block:: edgeql-repl\n\n        db> select range(<int64>{}, empty := true);\n        {range({}, inc_lower := false, empty := true)}\n\n\n----------\n\n\n.. eql:function:: std::range_get_lower(r: range<anypoint>) \\\n                    -> optional anypoint\n                  std::range_get_lower(r: multirange<anypoint>) \\\n                    -> optional anypoint\n\n    Return lower bound value.\n\n    Return the lower bound of the specified range or multirange.\n\n    .. code-block:: edgeql-repl\n\n        db> select range_get_lower(range(1, 10));\n        {1}\n        db> select range_get_lower(range(1.5, 7.5));\n        {1.5}\n        db> select range_get_lower(\n        ...   multirange([range(5, 10), range(2, 3)]));\n        {2}\n\n\n----------\n\n\n.. eql:function:: std::range_is_inclusive_lower(r: range<anypoint>) \\\n                    -> bool\n                  std::range_is_inclusive_lower(r: multirange<anypoint>) \\\n                    -> bool\n\n    Check whether lower bound is inclusive.\n\n    Return ``true`` if the lower bound is inclusive and ``false`` otherwise.\n    If there is no lower bound, then it is never considered inclusive.\n\n    .. code-block:: edgeql-repl\n\n        db> select range_is_inclusive_lower(range(1, 10));\n        {true}\n        db> select range_is_inclusive_lower(\n        ...     range(1.5, 7.5, inc_lower := false));\n        {false}\n        db> select range_is_inclusive_lower(range(<int64>{}, 10));\n        {false}\n        db> select range_is_inclusive_lower(\n        ...   multirange([\n        ...     range(2, 3),\n        ...     range(5, 10),\n        ...   ])\n        ... );\n        {true}\n\n\n----------\n\n\n.. eql:function:: std::range_get_upper(r: range<anypoint>) \\\n                    -> optional anypoint\n                  std::range_get_upper(r: multirange<anypoint>) \\\n                    -> optional anypoint\n\n    Return upper bound value.\n\n    Return the upper bound of the specified range or multirange.\n\n    .. code-block:: edgeql-repl\n\n        db> select range_get_upper(range(1, 10));\n        {10}\n        db> select range_get_upper(range(1.5, 7.5));\n        {7.5}\n        db> select range_get_upper(\n        ...   multirange([range(5, 10), range(2, 3)]));\n        {10}\n\n\n----------\n\n\n.. eql:function:: std::range_is_inclusive_upper(r: range<anypoint>) \\\n                    -> bool\n                  std::range_is_inclusive_upper(r: multirange<anypoint>) \\\n                    -> bool\n\n    Check whether upper bound is inclusive.\n\n    Return ``true`` if the upper bound is inclusive and ``false`` otherwise.\n    If there is no upper bound, then it is never considered inclusive.\n\n    .. code-block:: edgeql-repl\n\n        db> select range_is_inclusive_upper(range(1, 10));\n        {false}\n        db> select range_is_inclusive_upper(\n        ...     range(1.5, 7.5, inc_upper := true));\n        {true}\n        db> select range_is_inclusive_upper(range(1));\n        {false}\n        db> select range_is_inclusive_upper(\n        ...   multirange([\n        ...     range(2.0, 3.0),\n        ...     range(5.0, 10.0, inc_upper := true),\n        ...   ])\n        ... );\n        {true}\n\n\n----------\n\n\n.. eql:function:: std::range_is_empty(val: range<anypoint>) \\\n                    -> bool\n                  std::range_is_empty(val: multirange<anypoint>) \\\n                    -> bool\n\n    Check whether a range is empty.\n\n    Return ``true`` if the range or multirange contains no values and\n    ``false`` otherwise.\n\n    .. code-block:: edgeql-repl\n\n        db> select range_is_empty(range(1, 10));\n        {false}\n        db> select range_is_empty(range(1, 1));\n        {true}\n        db> select range_is_empty(range(<int64>{}, empty := true));\n        {true}\n        db> select range_is_empty(multirange(<array<range<int64>>>[]));\n        {true}\n        db> select range_is_empty(multirange([range(1, 10)]));\n        {false}\n\n\n----------\n\n\n.. eql:function:: std::range_unpack(val: range<anydiscrete>) \\\n                    -> set of anydiscrete\n                  std::range_unpack(val: range<anypoint>, step: anypoint) \\\n                    -> set of anypoint\n\n    Return values from a range.\n\n    For a range of discrete values this function when called without\n    indicating a *step* value simply produces a set of all the values within\n    the range, in order.\n\n    .. code-block:: edgeql-repl\n\n        db> select range_unpack(range(1, 10));\n        {1, 2, 3, 4, 5, 6, 7, 8, 9}\n        db> select range_unpack(range(\n        ...   <cal::local_date>'2022-07-01',\n        ...   <cal::local_date>'2022-07-10'));\n        {\n          <cal::local_date>'2022-07-01',\n          <cal::local_date>'2022-07-02',\n          <cal::local_date>'2022-07-03',\n          <cal::local_date>'2022-07-04',\n          <cal::local_date>'2022-07-05',\n          <cal::local_date>'2022-07-06',\n          <cal::local_date>'2022-07-07',\n          <cal::local_date>'2022-07-08',\n          <cal::local_date>'2022-07-09',\n        }\n\n    For any range type a *step* value can be specified. Then the values will\n    be picked from the range, starting at the lower boundary (skipping the\n    boundary value itself if it's not included in the range) and then\n    producing the next value by adding the *step* to the previous one.\n\n    .. code-block:: edgeql-repl\n\n        db> select range_unpack(range(1.5, 7.5), 0.7);\n        {1.5, 2.2, 2.9, 3.6, 4.3, 5, 5.7, 6.4}\n        db> select range_unpack(\n        ...   range(\n        ...     <cal::local_datetime>'2022-07-01T00:00:00',\n        ...     <cal::local_datetime>'2022-12-01T00:00:00'\n        ...   ),\n        ...   <cal::relative_duration>'25 days 5 hours');\n        {\n          <cal::local_datetime>'2022-07-01T00:00:00',\n          <cal::local_datetime>'2022-07-26T05:00:00',\n          <cal::local_datetime>'2022-08-20T10:00:00',\n          <cal::local_datetime>'2022-09-14T15:00:00',\n          <cal::local_datetime>'2022-10-09T20:00:00',\n          <cal::local_datetime>'2022-11-04T01:00:00',\n        }\n\n\n----------\n\n\n.. eql:function:: std::overlaps(l: range<anypoint>, r: range<anypoint>) \\\n                    -> bool\n                  std::overlaps(l: multirange<anypoint>, \\\n                                r: multirange<anypoint>, \\\n                  ) -> bool\n\n    Check whether ranges or multiranges overlap.\n\n    Return ``true`` if the ranges or multiranges have any elements in common\n    and ``false`` otherwise.\n\n    .. code-block:: edgeql-repl\n\n        db> select overlaps(range(1, 10), range(5));\n        {true}\n        db> select overlaps(range(1, 10), range(10));\n        {false}\n\n        db> select overlaps(\n        ...   multirange([\n        ...     range(1, 4), range(7),\n        ...   ]),\n        ...   multirange([\n        ...     range(-1, 2), range(8, 10),\n        ...   ]),\n        ... );\n        {true}\n        db> select overlaps(\n        ...   multirange([\n        ...     range(1, 4), range(7),\n        ...   ]),\n        ...   multirange([\n        ...     range(-1, 1), range(5, 6),\n        ...   ]),\n        ... );\n        {false}\n\n\n----------\n\n\n.. eql:function:: std::adjacent( \\\n                    l: range<anypoint>, \\\n                    r: range<anypoint>, \\\n                  ) -> bool\n                  std::adjacent( \\\n                    l: multirange<anypoint>, \\\n                    r: multirange<anypoint>, \\\n                  ) -> bool\n\n    .. versionadded:: 4.0\n\n    Check whether ranges or multiranges share a boundary without overlapping.\n\n    .. code-block:: edgeql-repl\n\n        db> select adjacent(range(1, 3), range(3, 4));\n        {true}\n        db> select adjacent(range(1.0, 3.0), range(3.0, 4.0));\n        {true}\n        db> select adjacent(\n        ...   range(1.0, 3.0, inc_upper := true), range(3.0, 4.0));\n        {false}\n\n        db> select adjacent(\n        ...   multirange([\n        ...     range(2, 4), range(5, 7),\n        ...   ]),\n        ...   multirange([\n        ...     range(7, 10), range(20),\n        ...   ]),\n        ... );\n        {true}\n\n    Since range values can be implicitly cast into multiranges, you can mix\n    the two types:\n\n    .. code-block:: edgeql-repl\n\n        db> select adjacent(\n        ...   range(7),\n        ...   multirange([\n        ...     range(1, 2), range(3, 7),\n        ...   ]),\n        ... );\n        {true}\n\n\n----------\n\n\n.. eql:function:: std::strictly_above( \\\n                    l: range<anypoint>, \\\n                    r: range<anypoint>, \\\n                  ) -> bool\n                  std::strictly_above( \\\n                    l: multirange<anypoint>, \\\n                    r: multirange<anypoint>, \\\n                  ) -> bool\n\n    .. versionadded:: 4.0\n\n    All values of the first range or multirange appear after the second.\n\n    .. code-block:: edgeql-repl\n\n        db> select strictly_above(\n        ...   range(7), range(1, 5)\n        ... );\n        {true}\n        db> select strictly_above(\n        ...   range(3, 7), range(1, 5)\n        ... );\n        {false}\n\n        db> select strictly_above(\n        ...   multirange([\n        ...     range(2, 4), range(5, 7),\n        ...   ]),\n        ...   multirange([\n        ...     range(-5, -2), range(-1, 1),\n        ...   ]),\n        ... );\n        {true}\n\n    Since range values can be implicitly cast into multiranges, you can mix\n    the two types:\n\n    .. code-block:: edgeql-repl\n\n        db> select strictly_above(\n        ...   range(8),\n        ...   multirange([\n        ...     range(1, 2), range(3, 7),\n        ...   ]),\n        ... );\n        {true}\n\n\n----------\n\n\n.. eql:function:: std::strictly_below( \\\n                    l: range<anypoint>, \\\n                    r: range<anypoint>, \\\n                  ) -> bool\n                  std::strictly_below( \\\n                    l: multirange<anypoint>, \\\n                    r: multirange<anypoint>, \\\n                  ) -> bool\n\n    .. versionadded:: 4.0\n\n    All values of the first range or multirange appear before the second.\n\n    .. code-block:: edgeql-repl\n\n        db> select strictly_below(\n        ...   range(1, 3), range(7)\n        ... );\n        {true}\n        db> select strictly_below(\n        ...   range(1, 7), range(3)\n        ... );\n        {false}\n\n        db> select strictly_below(\n        ...   multirange([\n        ...     range(-1, 0), range(-5, -3),\n        ...   ]),\n        ...   multirange([\n        ...     range(1, 4), range(7),\n        ...   ]),\n        ... );\n        {true}\n\n    Since range values can be implicitly cast into multiranges, you can mix\n    the two types:\n\n    .. code-block:: edgeql-repl\n\n        db> select strictly_below(\n        ...   range(-1, 0),\n        ...   multirange([\n        ...     range(1, 4), range(7),\n        ...   ]),\n        ... );\n        {true}\n\n\n----------\n\n\n.. eql:function:: std::bounded_above( \\\n                    l: range<anypoint>, \\\n                    r: range<anypoint>, \\\n                  ) -> bool\n                  std::bounded_above( \\\n                    l: multirange<anypoint>, \\\n                    r: multirange<anypoint>, \\\n                  ) -> bool\n\n    .. versionadded:: 4.0\n\n    The first argument is bounded above by the upper bound of the second.\n\n    .. code-block:: edgeql-repl\n\n        db> select bounded_above(\n        ...   range(1, 7), range(3, 7)\n        ... );\n        {true}\n        db> select bounded_above(\n        ...   range(1, 7), range(3, 6)\n        ... );\n        {false}\n        db> select bounded_above(\n        ...   range(1, 7), range(3)\n        ... );\n        {true}\n\n        db> select bounded_above(\n        ...   multirange([\n        ...     range(-1, 0), range(5, 7),\n        ...   ]),\n        ...   multirange([\n        ...     range(1, 2), range(3, 7),\n        ...   ]),\n        ... );\n        {true}\n\n    Since range values can be implicitly cast into multiranges, you can mix\n    the two types:\n\n    .. code-block:: edgeql-repl\n\n        db> select bounded_above(\n        ...   range(-1, 10),\n        ...   multirange([\n        ...     range(1, 4), range(7),\n        ...   ]),\n        ... );\n        {true}\n\n\n----------\n\n\n.. eql:function:: std::bounded_below( \\\n                    l: range<anypoint>, \\\n                    r: range<anypoint>, \\\n                  ) -> bool\n                  std::bounded_below( \\\n                    l: multirange<anypoint>, \\\n                    r: multirange<anypoint>, \\\n                  ) -> bool\n\n    .. versionadded:: 4.0\n\n    The first argument is bounded below by the lower bound of the second.\n\n    .. code-block:: edgeql-repl\n\n        db> select bounded_below(\n        ...   range(1, 7), range(3, 6)\n        ... );\n        {false}\n        db> select bounded_below(\n        ...   range(1, 7), range(0, 6)\n        ... );\n        {true}\n\n        db> select bounded_below(\n        ...   multirange([\n        ...     range(-1, 0), range(5, 7),\n        ...   ]),\n        ...   multirange([\n        ...     range(1, 2), range(3, 7),\n        ...   ]),\n        ... );\n        {false}\n\n    Since range values can be implicitly cast into multiranges, you can mix\n    the two types:\n\n    .. code-block:: edgeql-repl\n\n        db> select bounded_below(\n        ...   range(5, 7),\n        ...   multirange([\n        ...     range(1, 2), range(3, 7),\n        ...   ]),\n        ... );\n        {true}\n\n\n----------\n\n\n.. eql:function:: std::multirange(ranges: array<range<anypoint>>) \\\n                    -> multirange<anypoint>\n\n    .. versionadded:: 4.0\n\n    Construct a multirange.\n\n    Construct a multirange from the *ranges* array. Normalize the sub-ranges\n    so that they become ordered and non-overlapping.\n\n    .. code-block:: edgeql-repl\n\n        db> select multirange([range(8, 10), range(1, 4), range(2, 5)]);\n        {[range(1, 5), range(8, 10)]}\n\n    If either an empty array or an empty range is used to construct a\n    multirange, the resulting multirange will be empty. An empty multirange is\n    semantically similar to an empty range.\n\n    .. code-block:: edgeql-repl\n\n        db> with\n        ...   a := multirange(<array<range<int64>>>[]),\n        ...   b := multirange([range(<int64>{}, empty := true)]),\n        ...   c := range(<int64>{}, empty := true),\n        ... select (a = b, b = c);\n        {(true, true)}\n\n\n----------\n\n\n.. eql:function:: std::multirange_unpack(val: multirange<anypoint>) \\\n                    -> set of range<anypoint>\n\n    .. versionadded:: 4.0\n\n    Returns the sub-ranges of a multirange as a set or ranges.\n\n    .. code-block:: edgeql-repl\n\n        db> select multirange_unpack(\n        ...   multirange([\n        ...     range(1, 4), range(7), range(3, 5)\n        ...   ]),\n        ... );\n        {range(1, 5), range(7, {})}\n        db> select multirange_unpack(\n        ...   multirange(<array<range<int64>>>[]));\n        {}\n"
  },
  {
    "path": "docs/reference/stdlib/sequence.rst",
    "content": ".. _ref_std_sequence:\n\n=========\nSequences\n=========\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:type:`sequence`\n      - Auto-incrementing sequence of :eql:type:`int64`.\n\n    * - :eql:func:`sequence_next`\n      - :eql:func-desc:`sequence_next`\n\n    * - :eql:func:`sequence_reset`\n      - :eql:func-desc:`sequence_reset`\n\n\n----------\n\n\n.. eql:type:: std::sequence\n\n    An auto-incrementing sequence of :eql:type:`int64`.\n\n    This type can be used to create auto-incrementing :ref:`properties\n    <ref_datamodel_props>`:\n\n    .. code-block:: sdl\n\n        scalar type TicketNo extending sequence;\n\n        type Ticket {\n            number: TicketNo {\n                constraint exclusive;\n            }\n        }\n\n    A sequence is bound to the scalar type, not to the property, so\n    if multiple properties use the same sequence, they will\n    share the same counter. For each distinct counter, a separate\n    scalar type that is extending ``sequence`` should be used.\n\n\n---------\n\n\n.. eql:function:: std::sequence_next(seq: schema::ScalarType) -> int64\n\n    Increments the given sequence to its next value and returns that value.\n\n    See the note on :ref:`specifying your sequence\n    <ref_std_specifying_sequence>` for best practices on supplying the *seq*\n    parameter.\n\n    Sequence advancement is done atomically; each concurrent session and\n    transaction will receive a distinct sequence value.\n\n    .. code-block:: edgeql-repl\n\n       db> select sequence_next(introspect MySequence);\n       {11}\n\n\n---------\n\n\n.. eql:function:: std::sequence_reset(seq: schema::ScalarType) -> int64\n                  std::sequence_reset( \\\n                    seq: schema::ScalarType, val: int64) -> int64\n\n    Resets a sequence to initial state or a given value, returning the value.\n\n    See the note on :ref:`specifying your sequence\n    <ref_std_specifying_sequence>` for best practices on supplying the *seq*\n    parameter.\n\n    The single-parameter form resets the sequence to its initial state, where\n    the next :eql:func:`sequence_next` call will return the first value in\n    sequence. The two-parameter form allows you to set the current value of the\n    sequence. The next :eql:func:`sequence_next` call will return the value\n    after the one you passed to :eql:func:`sequence_reset`.\n\n    .. code-block:: edgeql-repl\n\n       db> select sequence_reset(introspect MySequence);\n       {1}\n       db> select sequence_next(introspect MySequence);\n       {1}\n       db> select sequence_reset(introspect MySequence, 22);\n       {22}\n       db> select sequence_next(introspect MySequence);\n       {23}\n\n\n---------\n\n.. _ref_std_specifying_sequence:\n\n.. note::\n\n    To specify the sequence to be operated on by either\n    :eql:func:`sequence_next` or :eql:func:`sequence_reset`, you must pass a\n    ``schema::ScalarType`` object. If the sequence argument is known ahead of\n    time and does not change, we recommend passing it by using the\n    :eql:op:`introspect` operator:\n\n    .. code-block:: edgeql\n\n        select sequence_next(introspect MySequenceType);\n        # or\n        select sequence_next(introspect typeof MyObj.seq_prop);\n\n    This style of execution will ensure that the reference to a sequential\n    type from a given expression is tracked properly to guarantee schema\n    referential integrity.\n\n    It doesn't work in every use case, though. If in your use case, the\n    sequence type must be determined at run time via a query argument,\n    you will need to query it from the ``schema::ScalarType`` set directly:\n\n    .. code-block:: edgeql\n\n        with\n          SeqType := (\n            select schema::ScalarType\n            filter .name = <str>$seq_type_name\n          )\n        select\n          sequence_next(SeqType);\n\n\n.. warning::\n\n    **Caution**\n\n    To work efficiently in high concurrency without lock contention, a\n    :eql:func:`sequence_next` execution is never rolled back, even if the\n    containing transaction is aborted. This may result in gaps in the\n    generated sequence. Likewise, the result of a :eql:func:`sequence_reset`\n    call is not undone if the transaction is rolled back.\n"
  },
  {
    "path": "docs/reference/stdlib/set.rst",
    "content": ".. _ref_std_set:\n\n====\nSets\n====\n\n:edb-alt-title: Set Functions and Operators\n\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:op:`distinct set <distinct>`\n      - :eql:op-desc:`distinct`\n\n    * - :eql:op:`anytype in set <in>`\n      - :eql:op-desc:`in`\n\n    * - :eql:op:`set union set <union>`\n      - :eql:op-desc:`union`\n\n    * - :eql:op:`set intersect set <intersect>`\n      - :eql:op-desc:`intersect`\n\n    * - :eql:op:`set except set <except>`\n      - :eql:op-desc:`except`\n\n    * - :eql:op:`exists set <exists>`\n      - :eql:op-desc:`exists`\n\n    * - :eql:op:`set if bool else set <if..else>`\n      - :eql:op-desc:`if..else`\n\n    * - :eql:op:`optional anytype ?? set <coalesce>`\n      - :eql:op-desc:`coalesce`\n\n    * - :eql:op:`detached`\n      - :eql:op-desc:`detached`\n\n    * - :eql:op:`anytype [is type] <isintersect>`\n      - :eql:op-desc:`isintersect`\n\n    * - :eql:func:`assert_distinct`\n      - :eql:func-desc:`assert_distinct`\n\n    * - :eql:func:`assert_single`\n      - :eql:func-desc:`assert_single`\n\n    * - :eql:func:`assert_exists`\n      - :eql:func-desc:`assert_exists`\n\n    * - :eql:func:`count`\n      - :eql:func-desc:`count`\n\n    * - :eql:func:`array_agg`\n      - :eql:func-desc:`array_agg`\n\n    * - :eql:func:`sum`\n      - :eql:func-desc:`sum`\n\n    * - :eql:func:`all`\n      - :eql:func-desc:`all`\n\n    * - :eql:func:`any`\n      - :eql:func-desc:`any`\n\n    * - :eql:func:`enumerate`\n      - :eql:func-desc:`enumerate`\n\n    * - :eql:func:`min`\n      - :eql:func-desc:`min`\n\n    * - :eql:func:`max`\n      - :eql:func-desc:`max`\n\n    * - :eql:func:`math::mean`\n      - :eql:func-desc:`math::mean`\n\n    * - :eql:func:`math::stddev`\n      - :eql:func-desc:`math::stddev`\n\n    * - :eql:func:`math::stddev_pop`\n      - :eql:func-desc:`math::stddev_pop`\n\n    * - :eql:func:`math::var`\n      - :eql:func-desc:`math::var`\n\n    * - :eql:func:`math::var_pop`\n      - :eql:func-desc:`math::var_pop`\n\n\n----------\n\n\n.. eql:operator:: distinct: distinct set of anytype -> set of anytype\n\n    .. api-index:: distinct§ set of type§\n\n    Produces a set of all unique elements in the given set.\n\n    ``distinct`` is a set operator that returns a new set where\n    no member is equal to any other member.\n\n    .. code-block:: edgeql-repl\n\n        db> select distinct {1, 2, 2, 3};\n        {1, 2, 3}\n\n\n----------\n\n\n.. eql:operator:: in: anytype in set of anytype -> bool\n                      anytype not in set of anytype -> bool\n\n    .. index:: intersection, contains\n    .. api-index:: §element §in§ set§, §element §not in§ set§\n\n    Checks if a given element is a member of a given set.\n\n    Set membership operators ``in`` and ``not in`` test whether each element\n    of the left operand is present in the right operand. This means supplying\n    a set as the left operand will produce a set of boolean results, one for\n    each element in the left operand.\n\n    .. code-block:: edgeql-repl\n\n        db> select 1 in {1, 3, 5};\n        {true}\n\n        db> select 'Alice' in User.name;\n        {true}\n\n        db> select {1, 2} in {1, 3, 5};\n        {true, false}\n\n    This operator can also be used to implement set intersection:\n\n    .. code-block:: edgeql-repl\n\n        db> with\n        ...     A := {1, 2, 3, 4},\n        ...     B := {2, 4, 6}\n        ... select A filter A in B;\n        {2, 4}\n\n    If your left operand is an empty set, the result will be an empty set, which you will need to handle in your conditional logic, typically with the :eql:op:`coalesce` operator:\n\n    .. code-block:: edgeql-repl\n\n      db> select (<bool>{} in {true, true, false});\n      {}\n\n      db> select (<bool>{} in {true, true, false}) ?? false;\n      {false}\n\n\n----------\n\n\n.. eql:operator:: union: set of anytype union set of anytype -> set of anytype\n\n    .. index:: join\n    .. api-index:: union\n\n    Merges two sets.\n\n    Since Gel sets are formally multisets, ``union`` is a *multiset sum*,\n    so effectively it merges two multisets keeping all of their members.\n\n    For example, applying ``union`` to ``{1, 2, 2}`` and\n    ``{2}``, results in ``{1, 2, 2, 2}``.\n\n    If you need a distinct union, wrap it with the :eql:op:`distinct`\n    operator.\n\n\n----------\n\n\n.. eql:operator:: intersect: set of anytype intersect set of anytype \\\n                                -> set of anytype\n\n    .. api-index:: intersect\n\n    Produces a set containing the common items between the given sets.\n\n    .. note::\n\n        The ordering of the returned set may not match that of the operands.\n\n    If you need a distinct intersection, wrap it with the :eql:op:`distinct`\n    operator.\n\n\n----------\n\n\n.. eql:operator:: except: set of anytype except set of anytype \\\n                                -> set of anytype\n\n    .. api-index:: except\n\n    Produces a set of all items in the first set which are not in the second.\n\n    .. note::\n\n        The ordering of the returned set may not match that of the operands.\n\n    If you need a distinct set of exceptions, wrap it with the\n    :eql:op:`distinct` operator.\n\n\n----------\n\n\n.. eql:operator:: if..else: set of anytype if bool else set of anytype \\\n                                -> set of anytype\n\n    .. index:: ternary, conditional\n    .. api-index:: §expr §if§ bool §else§ expr§\n\n    Produces one of two possible results based on a given condition.\n\n    .. eql:synopsis::\n\n        <left_expr> if <condition> else <right_expr>\n\n    If the :eql:synopsis:`<condition>` is ``true``, the ``if...else``\n    expression produces the value of the :eql:synopsis:`<left_expr>`. If the\n    :eql:synopsis:`<condition>` is ``false``, however, the ``if...else``\n    expression produces the value of the :eql:synopsis:`<right_expr>`.\n\n    .. code-block:: edgeql-repl\n\n        db> select 'real life' if 2 * 2 = 4 else 'dream';\n        {'real life'}\n\n    ``if..else`` expressions can be chained when checking multiple conditions\n    is necessary:\n\n    .. code-block:: edgeql-repl\n\n        db> with color := 'yellow'\n        ... select 'Apple' if color = 'red' else\n        ...        'Banana' if color = 'yellow' else\n        ...        'Orange' if color = 'orange' else\n        ...        'Other';\n        {'Banana'}\n\n    It can be used to create, update, or delete different objects based on\n    some condition:\n\n    .. code-block:: edgeql\n\n        with\n          name := <str>$0,\n          admin := <bool>$1\n        select (insert AdminUser { name := name }) if admin\n          else (insert User { name := name });\n\n\n-----------\n\n\n.. eql:operator:: if..then..else: if bool then set of anytype else set of \\\n                                anytype -> set of anytype\n\n    .. versionadded:: 4.0\n\n    .. index:: ternary, conditional\n    .. api-index:: if§ bool §then§ expr §else§ expr§\n\n    Produces one of two possible results based on a given condition.\n\n    Uses ``then`` for an alternative syntax order to ``if..else`` above.\n\n    .. eql:synopsis::\n\n        if <condition> then <left_expr> else <right_expr>\n\n    If the :eql:synopsis:`<condition>` is ``true``, the ``if...else``\n    expression produces the value of the :eql:synopsis:`<left_expr>`. If the\n    :eql:synopsis:`<condition>` is ``false``, however, the ``if...else``\n    expression produces the value of the :eql:synopsis:`<right_expr>`.\n\n    .. code-block:: edgeql-repl\n\n        db> select if 2 * 2 = 4 then 'real life' else 'dream';\n        {'real life'}\n\n    ``if..else`` expressions can be chained when checking multiple conditions\n    is necessary:\n\n    .. code-block:: edgeql-repl\n\n        db> with color := 'yellow', select\n        ... if color = 'red' then\n        ...   'Apple'\n        ... else if color = 'yellow' then\n        ...   'Banana'\n        ... else if color = 'orange' then\n        ...   'Orange'\n        ... else\n        ...   'Other';\n        {'Banana'}\n\n    It can be used to create, update, or delete different objects based on\n    some condition:\n\n    .. code-block:: edgeql\n\n        with\n          name := <str>$0,\n          admin := <bool>$1\n        select if admin then (\n            insert AdminUser { name := name }\n        ) else (\n            insert User { name := name }\n        )\n\n\n-----------\n\n.. eql:operator:: coalesce: optional anytype ?? set of anytype \\\n                              -> set of anytype\n\n    .. api-index:: ??\n\n    Produces the first of its operands that is not an empty set.\n\n    This evaluates to ``A`` for an non-empty ``A``, otherwise evaluates to\n    ``B``.\n\n    A typical use case of the coalescing operator is to provide default\n    values for optional properties:\n\n    .. code-block:: edgeql\n\n        # Get a set of tuples (<issue name>, <priority>)\n        # for all issues.\n        for issue in Issue\n        select (issue.name, issue.priority.name ?? 'n/a');\n\n    Without the coalescing operator, the above query will skip any\n    ``Issue`` without priority.\n\n    The coalescing operator can be used to express things like\n    \"select or insert if missing\":\n\n    .. code-block:: edgeql\n\n        select\n          (select User filter .name = 'Alice') ??\n          (insert User { name := 'Alice' });\n\n----------\n\n.. _ref_stdlib_set_detached:\n\n.. eql:operator:: detached: detached set of anytype -> set of anytype\n\n    .. api-index:: detached\n\n    Detaches the input set reference from the current scope.\n\n    A ``detached`` expression allows referring to some set as if it were\n    defined in the top-level ``with`` block. ``detached``\n    expressions ignore all current scopes in which they are nested.\n    This makes it possible to write queries that reference the same set\n    reference in multiple places.\n\n    .. code-block:: edgeql\n\n        update User\n        filter .name = 'Dave'\n        set {\n            friends := (select detached User filter .name = 'Alice'),\n            coworkers := (select detached User filter .name = 'Bob')\n        };\n\n    Without ``detached``, the occurrences of ``User`` inside the ``set`` shape\n    would be *bound* to the set of users named ``\"Dave\"``. However, in this\n    context we want to run an unrelated query on the \"unbound\" ``User`` set.\n\n    .. code-block:: edgeql\n\n        # does not work!\n        update User\n        filter .name = 'Dave'\n        set {\n            friends := (select User filter .name = 'Alice'),\n            coworkers := (select User filter .name = 'Bob')\n        };\n\n    Instead of explicitly detaching a set, you can create a reference to it in\n    a ``with`` block. All declarations inside a ``with`` block are implicitly\n    detached.\n\n    .. code-block:: edgeql\n\n        with U1 := User,\n             U2 := User\n        update User\n        filter .name = 'Dave'\n        set {\n            friends := (select U1 filter .name = 'Alice'),\n            coworkers := (select U2 filter .name = 'Bob')\n        };\n\n\n\n----------\n\n\n.. eql:operator:: exists: exists set of anytype -> bool\n\n    .. api-index:: exists\n\n    Determines whether a set is empty or not.\n\n    ``exists`` is an aggregate operator that returns a singleton set\n    ``{true}`` if the input set is not empty, and returns ``{false}``\n    otherwise:\n\n    .. code-block:: edgeql-repl\n\n        db> select exists {1, 2};\n        {true}\n\n\n----------\n\n\n.. eql:operator:: isintersect: anytype [is type] -> anytype\n\n    .. index:: type intersection, filter\n    .. api-index:: §expr§[is §type§]\n\n    Filters a set based on its type. Will return back the specified type.\n\n    The type intersection operator removes all elements from the input set\n    that aren't of the specified type. Additionally, since it\n    guarantees the type of the result set, all the links and properties\n    associated with the specified type can now be used on the\n    resulting expression. This is especially useful in combination\n    with :ref:`backlinks <ref_datamodel_links>`.\n\n    Consider the following types:\n\n    .. code-block:: sdl\n\n        type User {\n            required name: str;\n        }\n\n        abstract type Owned {\n            required owner: User;\n        }\n\n        type Issue extending Owned {\n            required title: str;\n        }\n\n        type Comment extending Owned {\n            required body: str;\n        }\n\n    The following expression will get all :eql:type:`Objects <Object>`\n    owned by all users (if there are any):\n\n    .. code-block:: edgeql\n\n        select User.<owner;\n\n    By default, :ref:`backlinks <ref_datamodel_links>` don't infer any\n    type information beyond the fact that it's an :eql:type:`Object`.\n    To ensure that this path specifically reaches ``Issue``, the type\n    intersection operator must then be used:\n\n    .. code-block:: edgeql\n\n        select User.<owner[is Issue];\n\n        # With the use of type intersection it's possible to refer\n        # to a specific property of Issue now:\n        select User.<owner[is Issue].title;\n\n\n----------\n\n\n.. eql:function:: std::assert_distinct( \\\n                    s: set of anytype, \\\n                    named only message: optional str = <str>{} \\\n                  ) -> set of anytype\n\n    .. index:: multiplicity, uniqueness\n\n    Checks that the input set contains only unique elements.\n\n    If the input set contains duplicate elements (i.e. it is not a *proper\n    set*), ``assert_distinct`` raises a ``ConstraintViolationError``.\n    Otherwise, this function returns the input set.\n\n    This function is useful as a runtime distinctness assertion in queries and\n    computed expressions that should always return proper sets, but where\n    static multiplicity inference is not capable enough or outright\n    impossible. An optional *message* named argument can be used to customize\n    the error message:\n\n    .. code-block:: edgeql-repl\n\n        db> select assert_distinct(\n        ...   (select User filter .groups.name = \"Administrators\")\n        ...   union\n        ...   (select User filter .groups.name = \"Guests\")\n        ... )\n        {default::User {id: ...}}\n\n        db> select assert_distinct(\n        ...   (select User filter .groups.name = \"Users\")\n        ...   union\n        ...   (select User filter .groups.name = \"Guests\")\n        ... )\n        ERROR: ConstraintViolationError: assert_distinct violation: expression\n               returned a set with duplicate elements.\n\n        db> select assert_distinct(\n        ...   (select User filter .groups.name = \"Users\")\n        ...   union\n        ...   (select User filter .groups.name = \"Guests\"),\n        ...   message := \"duplicate users!\"\n        ... )\n        ERROR: ConstraintViolationError: duplicate users!\n\n----------\n\n\n.. eql:function:: std::assert_single( \\\n                    s: set of anytype, \\\n                    named only message: optional str = <str>{} \\\n                  ) -> set of anytype\n\n    .. index:: cardinality, singleton\n\n    Checks that the input set has exactly zero or one elements.\n\n    If the input set contains more than one element, ``assert_single`` raises\n    a ``CardinalityViolationError``. Otherwise, this function returns the\n    input set.\n\n    This function is useful as a runtime cardinality assertion in queries and\n    computed expressions that should always return sets with at most a single\n    element, but where static cardinality inference is not capable enough or\n    outright impossible. An optional *message* named argument can be used to\n    customize the error message.\n\n    .. code-block:: edgeql-repl\n\n        db> select assert_single((select User filter .name = \"Unique\"))\n        {default::User {id: ...}}\n\n        db> select assert_single((select User))\n        ERROR: CardinalityViolationError: assert_single violation: more than\n               one element returned by an expression\n\n        db> select assert_single((select User), message := \"too many users!\")\n        ERROR: CardinalityViolationError: too many users!\n\n    .. note::\n\n        ``assert_single`` can be useful in many of the same contexts as ``limit\n        1`` with the key difference being that ``limit 1`` doesn't produce an\n        error if more than a single element exists in the set.\n\n----------\n\n\n.. eql:function:: std::assert_exists( \\\n                    s: set of anytype, \\\n                    named only message: optional str = <str>{} \\\n                  ) -> set of anytype\n\n    .. index:: cardinality, existence\n\n    Checks that the input set contains at least one element.\n\n    If the input set is empty, ``assert_exists`` raises a\n    ``CardinalityViolationError``.  Otherwise, this function returns the input\n    set.\n\n    This function is useful as a runtime existence assertion in queries and\n    computed expressions that should always return sets with at least a single\n    element, but where static cardinality inference is not capable enough or\n    outright impossible. An optional *message* named argument can be used to\n    customize the error message.\n\n    .. code-block:: edgeql-repl\n\n        db> select assert_exists((select User filter .name = \"Administrator\"))\n        {default::User {id: ...}}\n\n        db> select assert_exists((select User filter .name = \"Nonexistent\"))\n        ERROR: CardinalityViolationError: assert_exists violation: expression\n               returned an empty set.\n\n        db> select assert_exists(\n        ...   (select User filter .name = \"Nonexistent\"),\n        ...   message := \"no users!\"\n        ... )\n        ERROR: CardinalityViolationError: no users!\n\n----------\n\n\n.. eql:function:: std::count(s: set of anytype) -> int64\n\n    .. index:: aggregate\n\n    Returns the number of elements in a set.\n\n    .. code-block:: edgeql-repl\n\n        db> select count({2, 3, 5});\n        {3}\n\n        db> select count(User);  # number of User objects in db\n        {4}\n\n\n----------\n\n\n.. eql:function:: std::sum(s: set of int32) -> int64\n                  std::sum(s: set of int64) -> int64\n                  std::sum(s: set of float32) -> float32\n                  std::sum(s: set of float64) -> float64\n                  std::sum(s: set of bigint) -> bigint\n                  std::sum(s: set of decimal) -> decimal\n                  std::sum(s: set of duration) -> duration\n                  std::sum(s: set of cal::relative_duration) -> cal::relative_duration\n                  std::sum(s: set of cal::date_duration) -> cal::date_duration\n\n    .. index:: aggregate\n\n    Return the arithmetic sum of values in a set.\n\n    The result type depends on the input set type. The general rule of thumb\n    is that the type of the input set is preserved (as if a simple\n    :eql:op:`+<plus>` was used) while trying to reduce the chance of an\n    overflow (so all integers produce :eql:type:`int64` sum).\n\n    .. code-block:: edgeql-repl\n\n        db> select sum({2, 3, 5});\n        {10}\n\n        db> select sum({0.2, 0.3, 0.5});\n        {1.0}\n\n\n----------\n\n\n.. eql:function:: std::all(values: set of bool) -> bool\n\n    .. index:: aggregate\n\n    Returns ``true`` if none of the values in the given set are ``false``.\n\n    The result is ``true`` if all of the *values* are ``true`` or the set of\n    *values* is ``{}``, with ``false`` returned otherwise.\n\n    .. code-block:: edgeql-repl\n\n        db> select all(<bool>{});\n        {true}\n\n        db> select all({1, 2, 3, 4} < 4);\n        {false}\n\n\n----------\n\n\n.. eql:function:: std::any(values: set of bool) -> bool\n\n    .. index:: aggregate\n\n    Returns ``true`` if any of the values in the given set is ``true``.\n\n    The result is ``true`` if any of the *values* are ``true``, with ``false``\n    returned otherwise.\n\n    .. code-block:: edgeql-repl\n\n        db> select any(<bool>{});\n        {false}\n\n        db> select any({1, 2, 3, 4} < 4);\n        {true}\n\n\n----------\n\n\n.. eql:function:: std::enumerate(values: set of anytype) -> \\\n                  set of tuple<int64, anytype>\n\n    .. index:: enumerate\n\n    Returns a set of tuples in the form of ``(index, element)``.\n\n    The ``enumerate()`` function takes any set and produces a set of\n    tuples containing the zero-based index number and the value for each\n    element.\n\n    .. note::\n\n        The ordering of the returned set is not guaranteed, however,\n        the assigned indexes are guaranteed to be in order of the\n        original set.\n\n    .. code-block:: edgeql-repl\n\n        db> select enumerate({2, 3, 5});\n        {(1, 3), (0, 2), (2, 5)}\n\n    .. code-block:: edgeql-repl\n\n        db> select enumerate(User.name);\n        {(0, 'Alice'), (1, 'Bob'), (2, 'Dave')}\n\n\n----------\n\n\n.. eql:function:: std::min(values: set of anytype) -> optional anytype\n\n    .. index:: aggregate\n\n    Returns the smallest value in the given set.\n\n    .. code-block:: edgeql-repl\n\n        db> select min({-1, 100});\n        {-1}\n\n\n----------\n\n\n.. eql:function:: std::max(values: set of anytype) -> optional anytype\n\n    .. index:: aggregate\n\n    Returns the largest value in the given set.\n\n    .. code-block:: edgeql-repl\n\n        db> select max({-1, 100});\n        {100}\n"
  },
  {
    "path": "docs/reference/stdlib/string.rst",
    "content": ".. _ref_std_string:\n\n=======\nStrings\n=======\n\n:edb-alt-title: String Functions and Operators\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:type:`str`\n      - String\n\n    * - :eql:op:`str[i] <stridx>`\n      - :eql:op-desc:`stridx`\n\n    * - :eql:op:`str[from:to] <strslice>`\n      - :eql:op-desc:`strslice`\n\n    * - :eql:op:`str ++ str <strplus>`\n      - :eql:op-desc:`strplus`\n\n    * - :eql:op:`str like pattern <like>`\n      - :eql:op-desc:`like`\n\n    * - :eql:op:`str ilike pattern <ilike>`\n      - :eql:op-desc:`ilike`\n\n    * - :eql:op:`= <eq>` :eql:op:`\\!= <neq>` :eql:op:`?= <coaleq>`\n        :eql:op:`?!= <coalneq>` :eql:op:`\\< <lt>` :eql:op:`\\> <gt>`\n        :eql:op:`\\<= <lteq>` :eql:op:`\\>= <gteq>`\n      - Comparison operators\n\n    * - :eql:func:`to_str`\n      - :eql:func-desc:`to_str`\n\n    * - :eql:func:`len`\n      - Returns a string's length.\n\n    * - :eql:func:`contains`\n      - Tests if a string contains a substring.\n\n    * - :eql:func:`find`\n      - Finds the index of a substring.\n\n    * - :eql:func:`str_lower`\n      - :eql:func-desc:`str_lower`\n\n    * - :eql:func:`str_upper`\n      - :eql:func-desc:`str_upper`\n\n    * - :eql:func:`str_title`\n      - :eql:func-desc:`str_title`\n\n    * - :eql:func:`str_pad_start`\n      - :eql:func-desc:`str_pad_start`\n\n    * - :eql:func:`str_pad_end`\n      - :eql:func-desc:`str_pad_end`\n\n    * - :eql:func:`str_trim`\n      - :eql:func-desc:`str_trim`\n\n    * - :eql:func:`str_trim_start`\n      - :eql:func-desc:`str_trim_start`\n\n    * - :eql:func:`str_trim_end`\n      - :eql:func-desc:`str_trim_end`\n\n    * - :eql:func:`str_repeat`\n      - :eql:func-desc:`str_repeat`\n\n    * - :eql:func:`str_replace`\n      - :eql:func-desc:`str_replace`\n\n    * - :eql:func:`str_reverse`\n      - :eql:func-desc:`str_reverse`\n\n    * - :eql:func:`str_split`\n      - Splits a string into an array using a delimiter.\n\n    * - :eql:func:`re_match`\n      - :eql:func-desc:`re_match`\n\n    * - :eql:func:`re_match_all`\n      - :eql:func-desc:`re_match_all`\n\n    * - :eql:func:`re_replace`\n      - :eql:func-desc:`re_replace`\n\n    * - :eql:func:`re_test`\n      - :eql:func-desc:`re_test`\n\n\n----------\n\n\n.. eql:type:: std::str\n\n    .. index:: continuation\n\n    A unicode string of text.\n\n    Most primitive types (except :eql:type:`bytes`) can be\n    :eql:op:`cast <cast>` to and from a string:\n\n    .. code-block:: edgeql-repl\n\n        db> select <str>42;\n        {'42'}\n        db> select <bool>'true';\n        {true}\n        db> select \"I ❤️ Gel\";\n        {'I ❤️ Gel'}\n\n    Note that when a :eql:type:`str` is cast into a :eql:type:`json`,\n    the result is a JSON string value. The same applies for casting back\n    from :eql:type:`json` - only a JSON string value can be cast into\n    a :eql:type:`str`:\n\n    .. code-block:: edgeql-repl\n\n        db> select <json>'Hello, world';\n        {'\"Hello, world\"'}\n\n    There are two kinds of string literals in EdgeQL: regular and *raw*.\n    Raw string literals do not evaluate ``\\``, so ``\\n`` in in a raw string\n    is two characters ``\\`` and ``n``.\n\n    The regular string literal syntax is ``'a string'`` or a ``\"a string\"``.\n    Two *raw* string syntaxes are illustrated below:\n\n    .. code-block:: edgeql-repl\n\n        db> select r'A raw \\n string';\n        {'A raw \\\\n string'}\n        db> select 'Not a raw \\n string';\n        {\n          'Not a raw\n         string',\n        }\n        db> select $$something$$;\n        {'something'}\n        db> select $marker$something $$\n        ... nested \\!$$$marker$;\n        {'something $$\n        nested \\!$$'}\n\n    Regular strings use ``\\`` to indicate line continuation. When a\n    line continuation symbol is encountered, the symbol itself as well\n    as all the whitespace characters up to the next non-whitespace\n    character are omitted from the string:\n\n    .. code-block:: edgeql-repl\n\n        db> select 'Hello, \\\n        ...         world';\n        {'\"Hello, world\"'}\n\n    .. note::\n\n        This type is subject to `the Postgres maximum field size`_\n        of 1GB.\n\n    .. versionadded:: 6.0\n\n      Regular strings may use ``\\(expr)`` to interpolate the value of\n      ``expr`` into the string. The value will be cast to ``str`` if it\n      is not already. For example:\n\n      .. code-block:: edgeql-repl\n\n        db> select '1 + 1 = \\(1+1)';\n        {'1 + 1 = 2'}\n        db> select User { name := '\\(.first_name) \\(.last_name)' };\n        {\n          default::User {\n            name := 'Keanu Reeves',\n          },\n          ...\n        }\n\n\n.. lint-off\n.. _the Postgres maximum field size: https://wiki.postgresql.org/wiki/FAQ#What_is_the_maximum_size_for_a_row.2C_a_table.2C_and_a_database.3F>\n.. lint-on\n\n----------\n\n\n.. eql:operator:: stridx: str [ int64 ] -> str\n\n    .. api-index:: §str§[§int§]\n\n    String indexing.\n\n    Indexing starts at 0. Negative indexes are also valid and count from\n    the *end* of the string.\n\n    .. code-block:: edgeql-repl\n\n        db> select 'some text'[1];\n        {'o'}\n        db> select 'some text'[-1];\n        {'t'}\n\n    It is an error to attempt to extract a character at an index\n    outside the bounds of the string:\n\n    .. code-block:: edgeql-repl\n\n        db> select 'some text'[8];\n        {'t'}\n        db> select 'some text'[9];\n        InvalidValueError: string index 9 is out of bounds\n\n    A slice up to the next index can be used if an empty string is\n    preferred to an error when outside the bounds of the string:\n\n    .. code-block:: edgeql-repl\n\n        db> select 'some text'[8:9];\n        {'t'}\n        db> select 'some text'[9:10];\n        {''}\n\n\n----------\n\n\n.. eql:operator:: strslice: str [ int64 : int64 ] -> str\n\n    .. api-index:: §str§[§int§:§int§]\n\n    String slicing.\n\n    Indexing starts at 0. Negative indexes are also valid and count from\n    the *end* of the string.\n\n    .. code-block:: edgeql-repl\n\n        db> select 'some text'[1:3];\n        {'om'}\n        db> select 'some text'[-4:];\n        {'text'}\n        db> select 'some text'[:-5];\n        {'some'}\n        db> select 'some text'[5:-2];\n        {'te'}\n\n    It is perfectly acceptable to use indexes outside the bounds of a\n    string in a *slice*:\n\n    .. code-block:: edgeql-repl\n\n        db> select 'some text'[-4:100];\n        {'text'}\n        db> select 'some text'[-100:-5];\n        {'some'}\n\n\n----------\n\n\n.. eql:operator:: strplus: str ++ str -> str\n\n    .. index:: join, add\n    .. api-index:: §str §++§ str§\n\n    String concatenation.\n\n    .. code-block:: edgeql-repl\n\n        db> select 'some' ++ ' text';\n        {'some text'}\n\n\n----------\n\n\n.. eql:operator:: like: str like str -> bool\n                        str not like str -> bool\n\n    .. index:: comparison, compare\n    .. api-index:: §str §like§ str§, §str §not like§ str§\n\n    Case-sensitive simple string matching.\n\n    Returns ``true`` if the *value* (the ``str`` on the left) matches the\n    *pattern* (the ``str`` on the right) and ``false`` otherwise. The operator\n    ``not like`` is the negation of ``like``.\n\n    The pattern matching rules are as follows:\n\n    .. list-table::\n        :widths: auto\n        :header-rows: 1\n\n        * - pattern\n          - interpretation\n        * - ``%``\n          - matches zero or more characters\n        * - ``_``\n          - matches exactly one character\n        * - ``\\%``\n          - matches a literal \"%\"\n        * - ``\\_``\n          - matches a literal \"_\"\n        * - any other character\n          - matches itself\n\n    In particular, this means that if there are no special symbols in\n    the *pattern*, the operators ``like`` and ``not\n    like`` work identical to :eql:op:`= <eq>` and :eql:op:`\\!= <neq>`,\n    respectively.\n\n    .. code-block:: edgeql-repl\n\n        db> select 'abc' like 'abc';\n        {true}\n        db> select 'abc' like 'a%';\n        {true}\n        db> select 'abc' like '_b_';\n        {true}\n        db> select 'abc' like 'c';\n        {false}\n        db> select 'a%%c' not like r'a\\%c';\n        {true}\n\n\n----------\n\n\n.. eql:operator:: ilike: str ilike str -> bool\n                         str not ilike str -> bool\n\n    .. index:: comparison, compare\n    .. api-index:: §str §ilike§ str§, §str §not ilike§ str§\n\n    Case-insensitive simple string matching.\n\n    The operators ``ilike`` and ``not ilike`` work\n    the same way as :eql:op:`like` and :eql:op:`not like<like>`,\n    except that the *pattern* is matched in a case-insensitive manner.\n\n    .. code-block:: edgeql-repl\n\n        db> select 'Abc' ilike 'a%';\n        {true}\n\n\n----------\n\n\n.. eql:function:: std::str_lower(string: str) -> str\n\n    Returns a lowercase copy of the input string.\n\n    .. code-block:: edgeql-repl\n\n        db> select str_lower('Some Fancy Title');\n        {'some fancy title'}\n\n\n----------\n\n\n.. eql:function:: std::str_upper(string: str) -> str\n\n    Returns an uppercase copy of the input string.\n\n    .. code-block:: edgeql-repl\n\n        db> select str_upper('Some Fancy Title');\n        {'SOME FANCY TITLE'}\n\n\n----------\n\n\n.. eql:function:: std::str_title(string: str) -> str\n\n    Returns a titlecase copy of the input string.\n\n    Every word in the string will have the first letter capitalized\n    and the rest converted to lowercase.\n\n    .. code-block:: edgeql-repl\n\n        db> select str_title('sOmE fAnCy TiTlE');\n        {'Some Fancy Title'}\n\n\n----------\n\n\n.. eql:function:: std::str_pad_start(string: str, n: int64, fill: str = ' ') \\\n                    -> str\n\n    Returns the input string padded at the start to the length *n*.\n\n    If the string is longer than *n*, then it is truncated to the\n    first *n* characters. Otherwise, the string is padded on the\n    left up to the total length *n* using *fill* characters (space by\n    default).\n\n    .. code-block:: edgeql-repl\n\n        db> select str_pad_start('short', 10);\n        {'     short'}\n        db> select str_pad_start('much too long', 10);\n        {'much too l'}\n        db> select str_pad_start('short', 10, '.:');\n        {'.:.:.short'}\n\n\n----------\n\n\n.. eql:function:: std::str_pad_end(string: str, n: int64, fill: str = ' ') \\\n                    -> str\n\n    Returns the input string padded at the end to the length *n*.\n\n    If the string is longer than *n*, then it is truncated to the\n    first *n* characters. Otherwise, the string is padded on the\n    right up to the total length *n* using *fill* characters (space by\n    default).\n\n    .. code-block:: edgeql-repl\n\n        db> select str_pad_end('short', 10);\n        {'short     '}\n        db> select str_pad_end('much too long', 10);\n        {'much too l'}\n        db> select str_pad_end('short', 10, '.:');\n        {'short.:.:.'}\n\n\n----------\n\n\n.. eql:function:: std::str_trim_start(string: str, trim: str = ' ') -> str\n\n    Returns the input string with all *trim* characters removed\n    from the start.\n\n    If *trim* specifies more than one character they will be removed from\n    the beginning of the string regardless of the order in which they appear.\n\n    .. code-block:: edgeql-repl\n\n        db> select str_trim_start('     data');\n        {'data'}\n        db> select str_trim_start('.....data', '.:');\n        {'data'}\n        db> select str_trim_start(':::::data', '.:');\n        {'data'}\n        db> select str_trim_start(':...:data', '.:');\n        {'data'}\n        db> select str_trim_start('.:.:.data', '.:');\n        {'data'}\n\n\n----------\n\n\n.. eql:function:: std::str_trim_end(string: str, trim: str = ' ') -> str\n\n    Returns the input string with all *trim* characters removed from the end.\n\n    If *trim* specifies more than one character they will be removed from\n    the end of the string regardless of the order in which they appear.\n\n    .. code-block:: edgeql-repl\n\n        db> select str_trim_end('data     ');\n        {'data'}\n        db> select str_trim_end('data.....', '.:');\n        {'data'}\n        db> select str_trim_end('data:::::', '.:');\n        {'data'}\n        db> select str_trim_end('data:...:', '.:');\n        {'data'}\n        db> select str_trim_end('data.:.:.', '.:');\n        {'data'}\n\n\n----------\n\n\n.. eql:function:: std::str_trim(string: str, trim: str = ' ') -> str\n\n    Returns the input string with *trim* characters removed from both ends.\n\n    If *trim* specifies more than one character they will be removed from\n    both ends of the string regardless of the order in which they appear.\n    This is the same as applying\n    :eql:func:`str_trim_start` and :eql:func:`str_trim_end`.\n\n    .. code-block:: edgeql-repl\n\n        db> select str_trim('  data     ');\n        {'data'}\n        db> select str_trim('::data.....', '.:');\n        {'data'}\n        db> select str_trim('..data:::::', '.:');\n        {'data'}\n        db> select str_trim('.:data:...:', '.:');\n        {'data'}\n        db> select str_trim(':.:.data.:.', '.:');\n        {'data'}\n\n\n----------\n\n\n.. eql:function:: std::str_repeat(string: str, n: int64) -> str\n\n    Repeats the input string *n* times.\n\n    An empty string is returned if *n* is zero or negative.\n\n    .. code-block:: edgeql-repl\n\n        db> select str_repeat('.', 3);\n        {'...'}\n        db> select str_repeat('foo', -1);\n        {''}\n\n\n----------\n\n\n.. eql:function:: std::str_replace(s: str, old: str, new: str) -> str\n\n    Replaces all occurrences of a substring with a new one.\n\n    Given a string *s*, finds all non-overlapping occurrences of the\n    substring *old* and replaces them with the substring *new*.\n\n    .. code-block:: edgeql-repl\n\n        db> select str_replace('hello world', 'h', 'H');\n        {'Hello world'}\n        db> select str_replace('hello world', 'l', '[L]');\n        {'he[L][L]o wor[L]d'}\n        db> select str_replace('hello world', 'o', '😄');\n        {'hell😄 w😄rld'}\n\n\n----------\n\n\n.. eql:function:: std::str_reverse(string: str) -> str\n\n    Reverses the order of the characters in the string.\n\n    .. code-block:: edgeql-repl\n\n        db> select str_reverse('Hello world');\n        {'dlrow olleH'}\n        db> select str_reverse('Hello 👋 world 😄');\n        {'😄 dlrow 👋 olleH'}\n\n\n----------\n\n\n.. eql:function:: std::str_split(s: str, delimiter: str) -> array<str>\n\n    .. index:: explode\n\n    Splits a string into array elements using the supplied delimiter.\n\n    .. code-block:: edgeql-repl\n\n        db> select str_split('1, 2, 3', ', ');\n        {['1', '2', '3']}\n\n    .. code-block:: edgeql-repl\n\n        db> select str_split('123', '');\n        {['1', '2', '3']}\n\n\n----------\n\n\n.. eql:function:: std::re_match(pattern: str, \\\n                                string: str) -> array<str>\n\n    .. index:: regexp\n\n    Finds the first regular expression match in a string.\n\n    Given an input string and a regular expression :ref:`pattern\n    <string_regexp>`, finds the first match for the regular expression\n    within the string. Each match returned is represented by an\n    :eql:type:`array\\<str\\>` of matched groups.\n\n    .. code-block:: edgeql-repl\n\n        db> select re_match(r'\\w{4}ql', 'I ❤️ edgeql');\n        {['edgeql']}\n\n\n----------\n\n\n.. eql:function:: std::re_match_all(pattern: str, \\\n                                    string: str) -> set of array<str>\n\n    .. index:: regexp\n\n    Finds all regular expression matches in a string.\n\n    Given an input string and a regular expression :ref:`pattern\n    <string_regexp>`, repeatedly matches the regular expression within\n    the string. Returns the set of all matches, with each match\n    represented by an :eql:type:`array\\<str\\>` of matched groups.\n\n    .. code-block:: edgeql-repl\n\n        db> select re_match_all(r'a\\w+', 'an abstract concept');\n        {['an'], ['abstract']}\n\n\n----------\n\n\n.. eql:function:: std::re_replace(pattern: str, sub: str, \\\n                                  string: str, \\\n                                  named only flags: str='') \\\n                  -> str\n\n    .. index:: regexp\n\n    Replaces matching substrings in a given string.\n\n    Takes an input string and a regular expression :ref:`pattern\n    <string_regexp>`, replacing matching substrings with the replacement\n    string *sub*. Optional :ref:`flag arguments<string_regexp_flags>`\n    can be used to specify additional regular expression flags.\n\n    .. code-block:: edgeql-repl\n\n        db> select re_replace('a', 'A', 'Alabama');\n        {'AlAbama'}\n        db> select re_replace('a', 'A', 'Alabama', flags := 'g');\n        {'AlAbAmA'}\n        db> select re_replace('A', 'A', 'Alabama', flags := 'ig');\n        {'AlAbAmA'}\n\n\n----------\n\n\n.. eql:function:: std::re_test(pattern: str, string: str) -> bool\n\n    .. index:: regexp\n\n    Tests if a regular expression has a match in a string.\n\n    Given an input string and a regular expression :ref:`pattern\n    <string_regexp>`, tests whether there is a match for the regular\n    expression within the string. Returns ``true`` if there is a\n    match, ``false`` otherwise.\n\n    .. code-block:: edgeql-repl\n\n        db> select re_test(r'a', 'abc');\n        {true}\n\n\n------------\n\n\n.. eql:function:: std::to_str(val: datetime, fmt: optional str={}) -> str\n                  std::to_str(val: duration, fmt: optional str={}) -> str\n                  std::to_str(val: int64, fmt: optional str={}) -> str\n                  std::to_str(val: float64, fmt: optional str={}) -> str\n                  std::to_str(val: bigint, fmt: optional str={}) -> str\n                  std::to_str(val: decimal, fmt: optional str={}) -> str\n                  std::to_str(val: json, fmt: optional str={}) -> str\n                  std::to_str(val: bytes) -> str\n                  std::to_str(val: cal::local_datetime, \\\n                              fmt: optional str={}) -> str\n                  std::to_str(val: cal::local_date, \\\n                              fmt: optional str={}) -> str\n                  std::to_str(val: cal::local_time, \\\n                              fmt: optional str={}) -> str\n\n    .. index:: stringify, dumps, join, array_to_string, decode, TextDecoder\n\n    Returns the string representation of the input value.\n\n    A versatile polymorphic function defined for different input types,\n    ``to_str`` uses corresponding converter functions from :eql:type:`str`\n    to specific types via the format argument *fmt*.\n\n    When converting :eql:type:`bytes`, :eql:type:`datetime`,\n    :eql:type:`cal::local_datetime`, :eql:type:`cal::local_date`,\n    :eql:type:`cal::local_time`, :eql:type:`duration` this function\n    is the inverse of :eql:func:`to_bytes`, :eql:func:`to_datetime`,\n    :eql:func:`cal::to_local_datetime`, :eql:func:`cal::to_local_date`,\n    :eql:func:`cal::to_local_time`, :eql:func:`to_duration`, correspondingly.\n\n    For valid date and time formatting patterns see\n    :ref:`here <ref_std_converters_datetime_fmt>`.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_str(<datetime>'2018-05-07 15:01:22.306916-05',\n        ...               'FMDDth \"of\" FMMonth, YYYY');\n        {'7th of May, 2018'}\n        db> select to_str(<cal::local_date>'2018-05-07', 'CCth \"century\"');\n        {'21st century'}\n\n    .. note::\n\n        If you want to use literal text in your format string, it's best to\n        enclose it in double quotes as shown above with ``of`` and\n        ``century``.\n\n    When converting one of the numeric types, this function is the\n    reverse of: :eql:func:`to_bigint`, :eql:func:`to_decimal`,\n    :eql:func:`to_int16`, :eql:func:`to_int32`, :eql:func:`to_int64`,\n    :eql:func:`to_float32`, :eql:func:`to_float64`.\n\n    For valid number formatting patterns see\n    :ref:`here <ref_std_converters_number_fmt>`.\n\n    See also :eql:func:`to_json`.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_str(123, '999999');\n        {'    123'}\n        db> select to_str(123, '099999');\n        {' 000123'}\n        db> select to_str(123.45, 'S999.999');\n        {'+123.450'}\n        db> select to_str(123.45e-20, '9.99EEEE');\n        {' 1.23e-18'}\n        db> select to_str(-123.45n, 'S999.99');\n        {'-123.45'}\n\n    When converting :eql:type:`json`, this function can take\n    ``'pretty'`` as an optional *fmt* argument to produce a\n    pretty-formatted JSON string.\n\n    See also :eql:func:`to_json`.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_str(<json>2);\n        {'2'}\n\n        db> select to_str(<json>['hello', 'world']);\n        {'[\"hello\", \"world\"]'}\n\n        db> select to_str(<json>(a := 2, b := 'hello'), 'pretty');\n        {'{\n            \"a\": 2,\n            \"b\": \"hello\"\n        }'}\n\n    When converting :eql:type:`arrays <array>`, a *delimiter* argument\n    is required:\n\n    .. code-block:: edgeql-repl\n\n        db> select to_str(['one', 'two', 'three'], ', ');\n        {'one, two, three'}\n\n    .. warning::\n\n        A version of ``std::to_str`` exists which operates on arrays but has\n        been deprecated; :eql:func:`array_join` should be used instead.\n\n    A :eql:type:`bytes` value can be converted to a :eql:type:`str` using\n    UTF-8 encoding. Returns an InvalidValueError if input UTF-8 is invalid.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_str(b'\\xe3\\x83\\x86');\n        {'テ'}\n        db> select to_str(b'\\xe3\\x83');\n        gel error: InvalidValueError: invalid byte sequence for\n        encoding \"UTF8\": 0xe3 0x83\n\n\n----------\n\n\n.. _string_regexp:\n\nRegular Expressions\n-------------------\n\n|Gel| supports Regular expressions (REs), as defined in POSIX 1003.2.\nThey come in two forms: BRE (basic RE) and ERE (extended RE). In\naddition, Gel supports certain common extensions to the POSIX\nstandard commonly known as ARE (advanced RE). More details about\nBRE, ERE, and ARE support can be found in `PostgreSQL documentation`_.\n\n\n.. _`PostgreSQL documentation`:\n                https://www.postgresql.org/docs/10/static/\n                functions-matching.html#POSIX-SYNTAX-DETAILS\n\nThe table below outlines the different options accepted as the ``flags``\nargument to various regular expression functions, or as\n`embedded options`_ in the pattern itself, e.g. ``'(?i)fooBAR'``:\n\n.. _`embedded options`:\n  https://www.postgresql.org/docs/10/functions-matching.html#POSIX-METASYNTAX\n\n.. _string_regexp_flags:\n\nOption Flags\n^^^^^^^^^^^^\n\n======  ==================================================================\nOption  Description\n======  ==================================================================\n``b``   rest of RE is a BRE\n``c``   case-sensitive matching (overrides operator type)\n``e``   rest of RE is an ERE\n``i``   case-insensitive matching (overrides operator type)\n``m``   historical synonym for n\n``n``   newline-sensitive matching\n``p``   partial newline-sensitive matching\n``q``   rest of RE is a literal (\"quoted\") string, all ordinary characters\n``s``   non-newline-sensitive matching (default)\n``t``   tight syntax (default)\n``w``   inverse partial newline-sensitive (\"weird\") matching\n``x``   expanded syntax ignoring whitespace characters\n======  ==================================================================\n\n\n----------\n\n\nFormatting\n----------\n\n..\n    Portions Copyright (c) 2019 MagicStack Inc. and the Gel authors.\n\n    Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group\n    Portions Copyright (c) 1994, The Regents of the University of California\n\n    Permission to use, copy, modify, and distribute this software and its\n    documentation for any purpose, without fee, and without a written agreement\n    is hereby granted, provided that the above copyright notice and this\n    paragraph and the following two paragraphs appear in all copies.\n\n    IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR\n    DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING\n    LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS\n    DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE\n    POSSIBILITY OF SUCH DAMAGE.\n\n    THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,\n    INCLUDING, BUT not LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY\n    AND FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS\n    ON AN \"AS IS\" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO\n    PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.\n\n\nSome of the type converter functions take an extra argument specifying\nformatting (either for converting to a :eql:type:`str` or parsing from one).\nThe different formatting options are collected in this section.\n\n\n.. _ref_std_converters_datetime_fmt:\n\nDate and time formatting options\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n+-------------------------+----------------------------------------+\n| Pattern                 | Description                            |\n+=========================+========================================+\n| HH                      | hour of day (01-12)                    |\n+-------------------------+----------------------------------------+\n| HH12                    | hour of day (01-12)                    |\n+-------------------------+----------------------------------------+\n| HH24                    | hour of day (00-23)                    |\n+-------------------------+----------------------------------------+\n| MI                      | minute (00-59)                         |\n+-------------------------+----------------------------------------+\n| SS                      | second (00-59)                         |\n+-------------------------+----------------------------------------+\n| MS                      | millisecond (000-999)                  |\n+-------------------------+----------------------------------------+\n| US                      | microsecond (000000-999999)            |\n+-------------------------+----------------------------------------+\n| SSSS                    | seconds past midnight (0-86399)        |\n+-------------------------+----------------------------------------+\n| AM, am, PM or pm        | meridiem indicator (without periods)   |\n+-------------------------+----------------------------------------+\n| A.M., a.m., P.M. or     | meridiem indicator (with periods)      |\n| p.m.                    |                                        |\n+-------------------------+----------------------------------------+\n| Y,YYY                   | year (4 or more digits) with comma     |\n+-------------------------+----------------------------------------+\n| YYYY                    | year (4 or more digits)                |\n+-------------------------+----------------------------------------+\n| YYY                     | last 3 digits of year                  |\n+-------------------------+----------------------------------------+\n| YY                      | last 2 digits of year                  |\n+-------------------------+----------------------------------------+\n| Y                       | last digit of year                     |\n+-------------------------+----------------------------------------+\n| IYYY                    | ISO 8601 week-numbering year (4 or     |\n|                         | more digits)                           |\n+-------------------------+----------------------------------------+\n| IYY                     | last 3 digits of ISO 8601 week-        |\n|                         | numbering year                         |\n+-------------------------+----------------------------------------+\n| IY                      | last 2 digits of ISO 8601 week-        |\n|                         | numbering year                         |\n+-------------------------+----------------------------------------+\n| I                       | last digit of ISO 8601 week-numbering  |\n|                         | year                                   |\n+-------------------------+----------------------------------------+\n| BC, bc, AD or ad        | era indicator (without periods)        |\n+-------------------------+----------------------------------------+\n| B.C., b.c., A.D. or     | era indicator (with periods)           |\n| a.d.                    |                                        |\n+-------------------------+----------------------------------------+\n| MONTH                   | full upper case month name (blank-     |\n|                         | padded to 9 chars)                     |\n+-------------------------+----------------------------------------+\n| Month                   | full capitalized month name (blank-    |\n|                         | padded to 9 chars)                     |\n+-------------------------+----------------------------------------+\n| month                   | full lower case month name (blank-     |\n|                         | padded to 9 chars)                     |\n+-------------------------+----------------------------------------+\n| MON                     | abbreviated upper case month name (3   |\n|                         | chars in English, localized lengths    |\n|                         | vary)                                  |\n+-------------------------+----------------------------------------+\n| Mon                     | abbreviated capitalized month name (3  |\n|                         | chars in English, localized lengths    |\n|                         | vary)                                  |\n+-------------------------+----------------------------------------+\n| mon                     | abbreviated lower case month name (3   |\n|                         | chars in English, localized lengths    |\n|                         | vary)                                  |\n+-------------------------+----------------------------------------+\n| MM                      | month number (01-12)                   |\n+-------------------------+----------------------------------------+\n| DAY                     | full upper case day name (blank-padded |\n|                         | to 9 chars)                            |\n+-------------------------+----------------------------------------+\n| Day                     | full capitalized day name (blank-      |\n|                         | padded to 9 chars)                     |\n+-------------------------+----------------------------------------+\n| day                     | full lower case day name (blank-padded |\n|                         | to 9 chars)                            |\n+-------------------------+----------------------------------------+\n| DY                      | abbreviated upper case day name (3     |\n|                         | chars in English, localized lengths    |\n|                         | vary)                                  |\n+-------------------------+----------------------------------------+\n| Dy                      | abbreviated capitalized day name (3    |\n|                         | chars in English, localized lengths    |\n|                         | vary)                                  |\n+-------------------------+----------------------------------------+\n| dy                      | abbreviated lower case day name (3     |\n|                         | chars in English, localized lengths    |\n|                         | vary)                                  |\n+-------------------------+----------------------------------------+\n| DDD                     | day of year (001-366)                  |\n+-------------------------+----------------------------------------+\n| IDDD                    | day of ISO 8601 week-numbering year    |\n|                         | (001-371; day 1 of the year is Monday  |\n|                         | of the first ISO week)                 |\n+-------------------------+----------------------------------------+\n| DD                      | day of month (01-31)                   |\n+-------------------------+----------------------------------------+\n| D                       | day of the week, Sunday (1) to         |\n|                         | Saturday (7)                           |\n+-------------------------+----------------------------------------+\n| ID                      | ISO 8601 day of the week, Monday (1)   |\n|                         | to Sunday (7)                          |\n+-------------------------+----------------------------------------+\n| W                       | week of month (1-5) (the first week    |\n|                         | starts on the first day of the month)  |\n+-------------------------+----------------------------------------+\n| WW                      | week number of year (1-53) (the first  |\n|                         | week starts on the first day of the    |\n|                         | year)                                  |\n+-------------------------+----------------------------------------+\n| IW                      | week number of ISO 8601 week-numbering |\n|                         | year (01-53; the first Thursday of the |\n|                         | year is in week 1)                     |\n+-------------------------+----------------------------------------+\n| CC                      | century (2 digits) (the twenty-first   |\n|                         | century starts on 2001-01-01)          |\n+-------------------------+----------------------------------------+\n| J                       | Julian Day (integer days since         |\n|                         | November 24, 4714 BC at midnight UTC)  |\n+-------------------------+----------------------------------------+\n| Q                       | quarter                                |\n+-------------------------+----------------------------------------+\n| RM                      | month in upper case Roman numerals     |\n|                         | (I-XII; I=January)                     |\n+-------------------------+----------------------------------------+\n| rm                      | month in lower case Roman numerals     |\n|                         | (i-xii; i=January)                     |\n+-------------------------+----------------------------------------+\n| TZ                      | upper case time-zone abbreviation      |\n|                         | (only supported in :eql:func:`to_str`) |\n+-------------------------+----------------------------------------+\n| tz                      | lower case time-zone abbreviation      |\n|                         | (only supported in :eql:func:`to_str`) |\n+-------------------------+----------------------------------------+\n| TZH                     | time-zone hours                        |\n+-------------------------+----------------------------------------+\n| TZM                     | time-zone minutes                      |\n+-------------------------+----------------------------------------+\n| OF                      | time-zone offset from UTC (only        |\n|                         | supported in :eql:func:`to_str`)       |\n+-------------------------+----------------------------------------+\n\nSome additional formatting modifiers:\n\n+---------------+-----------------------------------+---------------+\n| Modifier      | Description                       | Example       |\n+===============+===================================+===============+\n| FM prefix     | fill mode (suppress leading       | FMMonth       |\n|               | zeroes and padding blanks)        |               |\n+---------------+-----------------------------------+---------------+\n| TH suffix     | upper case ordinal number suffix  | DDTH, e.g.,   |\n|               |                                   | 12TH          |\n+---------------+-----------------------------------+---------------+\n| th suffix     | lower case ordinal number suffix  | DDth, e.g.,   |\n|               |                                   | 12th          |\n+---------------+-----------------------------------+---------------+\n| FX prefix     | fixed format global option (see   | FX Month DD   |\n|               | usage notes)                      | Day           |\n+---------------+-----------------------------------+---------------+\n\nWhitespace is normally ignored when parsing string input, unless\nthe *FX* prefix modifier is used. For example:\n\n.. code-block:: edgeql-repl\n\n    db> select cal::to_local_date(\n    ...     '2000    JUN', 'YYYY MON');\n    {<cal::local_date>'2000-06-01'}\n    db> select cal::to_local_date(\n    ...     '2000    JUN', 'FXYYYY MON');\n    InternalServerError: invalid value \"   \" for \"MON\"\n\nOrdinary text is allowed in :eql:func:`to_str` format strings and will be\noutput literally.  For better compatibility you should put a plain substring\nin double quotes to force it to be interpreted as literal text even if it\ncontains template patterns now or could in the future.\n\n.. _ref_std_converters_number_fmt:\n\nNumber formatting options\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\n+------------+-----------------------------------------------------+\n| Pattern    | Description                                         |\n+============+=====================================================+\n| 9          | digit position (can be dropped if insignificant)    |\n+------------+-----------------------------------------------------+\n| 0          | digit position (will not be dropped even if         |\n|            | insignificant)                                      |\n+------------+-----------------------------------------------------+\n| .          | (period)  decimal point                             |\n+------------+-----------------------------------------------------+\n| ,          | (comma)   group (thousands) separator               |\n+------------+-----------------------------------------------------+\n| PR         | negative value in angle brackets                    |\n+------------+-----------------------------------------------------+\n| S          | sign anchored to number (uses locale)               |\n+------------+-----------------------------------------------------+\n| L          | currency symbol (uses locale)                       |\n+------------+-----------------------------------------------------+\n| D          | decimal point (uses locale)                         |\n+------------+-----------------------------------------------------+\n| G          | group separator (uses locale)                       |\n+------------+-----------------------------------------------------+\n| MI         | minus sign in specified position (if number < 0)    |\n+------------+-----------------------------------------------------+\n| PL         | plus sign in specified position (if number > 0)     |\n+------------+-----------------------------------------------------+\n| SG         | plus/minus sign in specified position               |\n+------------+-----------------------------------------------------+\n| RN         | Roman numeral (input between 1 and 3999)            |\n+------------+-----------------------------------------------------+\n| TH or th   | ordinal number suffix                               |\n+------------+-----------------------------------------------------+\n| V          | shift specified number of digits (see notes)        |\n+------------+-----------------------------------------------------+\n| EEEE       | exponent for scientific notation                    |\n+------------+-----------------------------------------------------+\n\nSome additional formatting modifiers:\n\n+---------------+-----------------------------------+---------------+\n| Modifier      | Description                       | Example       |\n+===============+===================================+===============+\n| FM prefix     | fill mode (suppress leading       | FM99.99       |\n|               | zeroes and padding blanks)        |               |\n+---------------+-----------------------------------+---------------+\n| TH suffix     | upper case ordinal number suffix  | 999TH         |\n+---------------+-----------------------------------+---------------+\n| th suffix     | lower case ordinal number suffix  | 999th         |\n+---------------+-----------------------------------+---------------+\n"
  },
  {
    "path": "docs/reference/stdlib/sys.rst",
    "content": ".. _ref_std_sys:\n\n\n======\nSystem\n======\n\n:edb-alt-title: System Functions and Types\n\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:func:`sys::approximate_count`\n      - :eql:func-desc:`sys::approximate_count`\n\n    * - :eql:func:`sys::get_version`\n      - :eql:func-desc:`sys::get_version`\n\n    * - :eql:func:`sys::get_version_as_str`\n      - :eql:func-desc:`sys::get_version_as_str`\n\n    * - :eql:func:`sys::get_current_database`\n      - :eql:func-desc:`sys::get_current_database`\n\n    * - :eql:func:`sys::get_current_branch`\n      - :eql:func-desc:`sys::get_current_branch`\n\n    * - :eql:type:`sys::Branch`\n      - A read-only type representing all database branches.\n\n    * - :eql:type:`sys::QueryStats`\n      - A read-only type representing query performance statistics.\n\n    * - :eql:func:`sys::reset_query_stats`\n      - :eql:func-desc:`sys::reset_query_stats`\n\n\n----------\n\n\n.. eql:function:: sys::approximate_count( \\\n                      type: schema::ObjectType, \\\n                      NAMED ONLY ignore_subtypes: std::bool=false, \\\n                  ) -> int64\n\n    Return an approximate count of the number of objects belonging to\n    a given type.\n\n    The ``type`` argument is a ``schema::ObjectType`` representing the\n    type to query.  It can be most easily obtained with the\n    :eql:op:`introspect` operator.\n\n    By default, the count includes all subtypes of the provided type as well.\n    If ``ignore_subtypes`` is true, then it includes only the type itself.\n\n    The value is based on postgres statistics, and may not be accurate.\n\n    .. code-block:: edgeql-repl\n\n        db> select sys::approximate_count(introspect schema::Type);\n        {278}\n        db> select sys::approximate_count(introspect schema::Type, ignore_subtypes:=True);\n        {0}\n        db> select schema::ObjectType {\n        ... name,\n        ... cnt := sys::approximate_count(schema::ObjectType, ignore_subtypes:=True),\n        ... };\n        {\n           schema::ObjectType {name: 'default::Issue', cnt: 4},\n           schema::ObjectType {name: 'default::User', cnt: 2},\n           ...\n        }\n\n\n----------\n\n\n.. eql:function:: sys::get_version() -> tuple<major: int64, \\\n                                              minor: int64, \\\n                                              stage: sys::VersionStage, \\\n                                              stage_no: int64, \\\n                                              local: array<str>>\n\n    Return the server version as a tuple.\n\n    The ``major`` and ``minor`` elements contain the major and the minor\n    components of the version; ``stage`` is an enumeration value containing\n    one of ``'dev'``, ``'alpha'``, ``'beta'``, ``'rc'`` or ``'final'``;\n    ``stage_no`` is the stage sequence number (e.g. ``2`` in an alpha 2\n    release); and ``local`` contains an arbitrary array of local version\n    identifiers.\n\n    .. code-block:: edgeql-repl\n\n        db> select sys::get_version();\n        {(major := 1, minor := 0, stage := <sys::VersionStage>'alpha',\n          stage_no := 1, local := [])}\n\n\n----------\n\n\n.. eql:function:: sys::get_version_as_str() -> str\n\n    Return the server version as a string.\n\n    .. code-block:: edgeql-repl\n\n        db> select sys::get_version_as_str();\n        {'1.0-alpha.1'}\n\n\n----------\n\n\n.. eql:function:: sys::get_transaction_isolation() -> \\\n                        sys::TransactionIsolation\n\n    Return the isolation level of the current transaction.\n\n    Possible return values are given by\n    :eql:type:`sys::TransactionIsolation`.\n\n    .. code-block:: edgeql-repl\n\n        db> select sys::get_transaction_isolation();\n        {sys::TransactionIsolation.Serializable}\n\n\n----------\n\n\n.. eql:function:: sys::get_current_database() -> str\n\n    Return the name of the current database as a string.\n\n    .. note::\n\n        This function is deprecated in |Gel|.\n        Use :eql:func:`sys::get_current_branch` instead\n        (it works in the same way).\n\n    .. code-block:: edgeql-repl\n\n        db> select sys::get_current_database();\n        {'my_database'}\n\n\n----------\n\n\n.. eql:function:: sys::get_current_branch() -> str\n\n    .. versionadded:: 5.0\n\n    Return the name of the current database branch as a string.\n\n    .. code-block:: edgeql-repl\n\n        db> select sys::get_current_branch();\n        {'my_branch'}\n\n\n-----------\n\n\n.. eql:type:: sys::TransactionIsolation\n\n    :eql:type:`Enum <enum>` indicating the possible transaction\n    isolation modes.\n\n    This enum only accepts a value of ``Serializable``.\n\n\n-----------\n\n\n.. eql:type:: sys::Branch\n\n    .. versionadded:: 6.0\n\n    A read-only type representing all database branches.\n\n    :eql:synopsis:`name -> str`\n        The name of the branch.\n\n    :eql:synopsis:`last_migration -> str`\n        The name of the last migration applied to the branch.\n\n\n-----------\n\n\n.. eql:type:: sys::QueryStats\n\n    .. versionadded:: 6.0\n\n    A read-only type representing query performance statistics.\n\n    :eql:synopsis:`branch -> sys::Branch`\n        The :eql:type:`branch <sys::Branch>` this statistics entry was\n        collected in.\n\n    :eql:synopsis:`query -> str`\n        Text string of a representative query.\n\n    :eql:synopsis:`query_type -> sys::QueryType`\n        The :eql:type:`type <sys::QueryType>` of the query.\n\n    :eql:synopsis:`tag -> str`\n        Query tag, commonly specifies the origin of the query, e.g 'gel/cli'\n        for queries originating from the CLI.  Clients can specify a tag for\n        easier query identification.\n\n    :eql:synopsis:`stats_since -> datetime`\n        Time at which statistics gathering started for this query.\n\n    :eql:synopsis:`minmax_stats_since -> datetime`\n        Time at which min/max statistics gathering started for this query\n        (fields ``min_plan_time``, ``max_plan_time``, ``min_exec_time`` and\n        ``max_exec_time``).\n\n    All queries have to be planned by the backend before execution. The planned\n    statements are cached (managed by the Gel server) and reused if the same\n    query is executed multiple times.\n\n    :eql:synopsis:`plans -> int64`\n        Number of times the query was planned in the backend.\n\n    :eql:synopsis:`total_plan_time -> duration`\n        Total time spent planning the query in the backend.\n\n    :eql:synopsis:`min_plan_time -> duration`\n        Minimum time spent planning the query in the backend. This field will\n        be zero if the counter has been reset using the\n        :eql:func:`sys::reset_query_stats` function with the ``minmax_only``\n        parameter set to ``true`` and never been planned since.\n\n    :eql:synopsis:`max_plan_time -> duration`\n        Maximum time spent planning the query in the backend. This field will\n        be zero if the counter has been reset using the\n        :eql:func:`sys::reset_query_stats` function with the ``minmax_only``\n        parameter set to ``true`` and never been planned since.\n\n    :eql:synopsis:`mean_plan_time -> duration`\n        Mean time spent planning the query in the backend.\n\n    :eql:synopsis:`stddev_plan_time -> duration`\n        Population standard deviation of time spent planning the query in the\n        backend.\n\n    After planning, the query is usually executed in the backend, with the\n    result being forwarded to the client.\n\n    :eql:synopsis:`calls -> int64`\n        Number of times the query was executed.\n\n    :eql:synopsis:`total_exec_time -> duration`\n        Total time spent executing the query in the backend.\n\n    :eql:synopsis:`min_exec_time -> duration`\n        Minimum time spent executing the query in the backend. This field will\n        be zero until this query is executed first time after reset performed\n        by the :eql:func:`sys::reset_query_stats` function with the\n        ``minmax_only`` parameter set to ``true``.\n\n    :eql:synopsis:`max_exec_time -> duration`\n        Maximum time spent executing the query in the backend. This field will\n        be zero until this query is executed first time after reset performed\n        by the :eql:func:`sys::reset_query_stats` function with the\n        ``minmax_only`` parameter set to ``true``.\n\n    :eql:synopsis:`mean_exec_time -> duration`\n        Mean time spent executing the query in the backend.\n\n    :eql:synopsis:`stddev_exec_time -> duration`\n        Population standard deviation of time spent executing the query in the\n        backend.\n\n    :eql:synopsis:`rows -> int64`\n        Total number of rows retrieved or affected by the query.\n\n    The following properties are used to identify a unique statistics entry\n    (together with the query text above):\n\n    :eql:synopsis:`compilation_config -> std::json`\n        The config used to compile the query.\n\n    :eql:synopsis:`protocol_version -> tuple<major: int16, minor: int16>`\n        The version of the binary protocol receiving the query.\n\n    :eql:synopsis:`default_namespace -> str`\n        The default module/schema used to compile the query.\n\n    :eql:synopsis:`namespace_aliases -> json`\n        The module aliases used to compile the query.\n\n    :eql:synopsis:`output_format -> sys::OutputFormat`\n        The :eql:type:`OutputFormat <sys::OutputFormat>` indicated in the\n        binary protocol receiving the query.\n\n    :eql:synopsis:`expect_one -> bool`\n        Whether the query is expected to return exactly one row.\n\n    :eql:synopsis:`implicit_limit -> int64`\n        The implicit limit set for the query.\n\n    :eql:synopsis:`inline_typeids -> bool`\n        Whether type IDs are inlined in the query result.\n\n    :eql:synopsis:`inline_typenames -> bool`\n        Whether type names are inlined in the query result.\n\n    :eql:synopsis:`inline_objectids -> bool`\n        Whether object IDs are inlined in the query result.\n\n\n-----------\n\n\n.. eql:type:: sys::QueryType\n\n    .. versionadded:: 6.0\n\n    :eql:type:`Enum <enum>` indicating the possible query types.\n\n    Possible values are:\n\n    * ``EdgeQL``\n    * ``SQL``\n\n\n-----------\n\n\n.. eql:type:: sys::OutputFormat\n\n    .. versionadded:: 6.0\n\n    :eql:type:`Enum <enum>` indicating the possible output formats in a binary\n    protocol.\n\n    Possible values are:\n\n    * ``BINARY``\n    * ``JSON``\n    * ``JSON_ELEMENTS``\n    * ``NONE``\n\n\n----------\n\n\n.. eql:function:: sys::reset_query_stats( \\\n        named only branch_name: OPTIONAL str = {}, \\\n        named only id: OPTIONAL uuid = {}, \\\n        named only minmax_only: OPTIONAL bool = false, \\\n    ) -> OPTIONAL datetime\n\n    .. versionadded:: 6.0\n\n    Discard selected query statistics gathered so far.\n\n    Discard query statistics gathered so far corresponding to the specified\n    ``branch_name`` and ``id``. If either of the parameters is not specified,\n    the statistics that match with the other parameter will be reset. If no\n    parameter is specified, it will discard all statistics. When ``minmax_only``\n    is ``true``, only the values of minimum and maximum planning and execution\n    time will be reset (i.e. ``min_plan_time``, ``max_plan_time``,\n    ``min_exec_time`` and ``max_exec_time`` fields). The default value for\n    ``minmax_only`` parameter is ``false``. This function returns the time of a\n    reset. This time is saved to ``stats_reset`` or ``minmax_stats_since`` field\n    of :eql:type:`sys::QueryStats` if the corresponding reset was actually\n    performed.\n\n    .. code-block:: edgeql-repl\n\n        db> select sys::reset_query_stats();\n        {'2021-01-01T00:00:00Z'}\n\n        db> select sys::reset_query_stats(branch_name := 'my_branch');\n        {'2021-01-01T00:00:00Z'}\n\n        db> select sys::reset_query_stats(id := <uuid>'00000000-0000-0000-0000-000000000000');\n        {'2021-01-01T00:00:00Z'}\n\n        db> select sys::reset_query_stats(minmax_only := true);\n        {'2021-01-01T00:00:00Z'}\n\n        db> select sys::reset_query_stats(branch_name := 'no_such_branch');\n        {}\n"
  },
  {
    "path": "docs/reference/stdlib/tuple.rst",
    "content": ".. _ref_std_tuple:\n\n======\nTuples\n======\n\nA tuple type is a heterogeneous sequence of other types. Tuples can be either\n*named* or *unnamed* (the default).\n\nConstructing tuples\n-------------------\n\nA tuple constructor is an expression that consists of a sequence of\ncomma-separated expressions enclosed in parentheses.  It produces a\ntuple value:\n\n.. eql:synopsis::\n\n    \"(\" <expr> [, ... ] \")\"\n\nDeclare a *named tuple*:\n\n.. eql:synopsis::\n\n    \"(\" <identifier> := <expr> [, ... ] \")\"\n\n*All* elements in a named tuple must have a name.\n\nA tuple constructor automatically creates a corresponding\n:ref:`tuple type <ref_eql_types_tuple>`.\n\n\n.. _ref_std_tuple_accessor:\n\nAccessing elements\n------------------\n\nAn element of a tuple can be referenced in the form:\n\n.. eql:synopsis::\n\n    <expr>.<element-index>\n\nHere, :eql:synopsis:`<expr>` is any expression that has a tuple type,\nand :eql:synopsis:`<element-index>` is either the *zero-based index*\nof the element or the name of an element in a named tuple.\n\nExamples:\n\n.. code-block:: edgeql-repl\n\n    db> select (1, 'Gel').0;\n    {1}\n\n    db> select (number := 1, name := 'Gel').name;\n    {\"Gel\"}\n\n    db> select (number := 1, name := 'Gel').1;\n    {\"Gel\"}\n\nNesting tuples\n--------------\n\nTuples can be nested:\n\n.. code-block:: edgeql-repl\n\n    db> select (nested_tuple := (1, 2)).nested_tuple.0;\n    {1}\n\nReferencing a non-existent tuple element will result in an error:\n\n.. code-block:: edgeql-repl\n\n    db> select (1, 2).5;\n    EdgeQLError: 5 is not a member of a tuple\n\n    ---- query context ----\n\n        line 1\n            > select (1, 2).3;\n\n\n.. _ref_eql_types_tuple:\n\nType syntax\n-----------\n\nA tuple type can be explicitly declared in an expression or schema\ndeclaration using the following syntax:\n\n.. eql:synopsis::\n\n    tuple \"<\" <element-type>, [<element-type>, ...] \">\"\n\nA named tuple:\n\n.. eql:synopsis::\n\n    tuple \"<\" <element-name> : <element-type> [, ... ] \">\"\n\nAny type can be used as a tuple element type.\n\nHere's an example of using this syntax in a schema definition:\n\n.. code-block:: sdl\n\n    type GameElement {\n        required name: str;\n        required position: tuple<x: int64, y: int64>;\n    }\n\nHere's a few examples of using tuple types in EdgeQL queries:\n\n.. code-block:: edgeql-repl\n\n    db> select <tuple<int64, str>>('1', 3);\n    {(1, '3')}\n    db> select <tuple<x: int64, y: int64>>(1, 2);\n    {(x := 1, y := 2)}\n    db> select (1, '3') is (tuple<int64, str>);\n    {true}\n    db> select ([1, 2], 'a') is (tuple<array<int64>, str>);\n    {true}\n\n\n.. eql:type:: std::tuple\n\n    A tuple type is a heterogeneous sequence of other types.\n\n    Tuple elements can optionally have names,\n    in which case the tuple is called a *named tuple*.\n\n    Any type can be used as a tuple element type.\n\n    A tuple type is created implicitly when a :ref:`tuple constructor\n    <ref_std_tuple>` is used:\n\n    .. code-block:: edgeql-repl\n\n        db> select ('foo', 42);\n        {('foo', 42)}\n\n    Two tuples are equal if all of their elements are equal and in the same\n    order.  Note that element names in named tuples are not significant for\n    comparison:\n\n    .. code-block:: edgeql-repl\n\n        db> select (1, 2, 3) = (a := 1, b := 2, c := 3);\n        {true}\n\n    The syntax of a tuple type declaration can be found in :ref:`this\n    section <ref_eql_types_tuple>`.\n\n\n\n"
  },
  {
    "path": "docs/reference/stdlib/type.rst",
    "content": ".. _ref_std_type:\n\n\n=====\nTypes\n=====\n\n:edb-alt-title: Type Operators\n\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:op:`is type <is>`\n      - :eql:op-desc:`is`\n\n    * - :eql:op:`type | type <typeor>`\n      - :eql:op-desc:`typeor`\n\n    * - :eql:op:`<type> val <cast>`\n      - :eql:op-desc:`cast`\n\n    * - :eql:op:`typeof anytype <typeof>`\n      - :eql:op-desc:`typeof`\n\n    * - :eql:op:`introspect type <introspect>`\n      - :eql:op-desc:`introspect`\n\n\nFinding an object's type\n------------------------\n\nYou can find the type of an object via that object's ``__type__`` link, which\ncarries various information about the object's type, including the type's\n``name``.\n\n.. code-block:: edgeql-repl\n\n    db> select <json>Person {\n    ...  __type__: {\n    ...    name\n    ...    }\n    ...  } limit 1;\n    {Json(\"{\\\"__type__\\\": {\\\"name\\\": \\\"default::Villain\\\"}}\")}\n\nThis information can be pulled into the top level by assigning a name to\nthe ``name`` property inside ``__type__``:\n\n.. code-block:: edgeql-repl\n\n    db> select <json>Person { type := .__type__.name } limit 1;\n    {Json(\"{\\\"type\\\": \\\"default::Villain\\\"}\")}\n\n.. note::\n\n    There's nothing magical about the ``__type__`` link: it's a standard link\n    that exists on every object due to their inheritance from\n    :eql:type:`BaseObject`, linking to the current object's type.\n\n----------\n\n\n.. eql:operator:: is: anytype is type -> bool\n                      anytype is not type -> bool\n\n    .. index:: comparison\n    .. api-index:: §type §is§ type§\n\n    Type checking operator.\n\n    Check if ``A`` is an instance of ``B`` or any of ``B``'s subtypes.\n\n    Type-checking operators ``is`` and ``is not`` that\n    test whether the left operand is of any of the types given by the\n    comma-separated list of types provided as the right operand.\n\n    Note that ``B`` is special and is not any kind of expression, so\n    it does not in any way participate in the interactions of sets and\n    longest common prefix rules.\n\n    .. code-block:: edgeql-repl\n\n        db> select 1 is int64;\n        {true}\n\n        db> select User is not SystemUser\n        ... filter User.name = 'Alice';\n        {true}\n\n        db> select User is (Text | Named);\n        {true, ..., true}  # one for every user instance\n\n\n----------\n\n\n.. eql:operator:: typeor: type | type -> type\n\n    .. index:: polymorphism, polymorphic queries, nested shapes\n    .. api-index:: §type §|§ type§\n\n    Type union operator.\n\n    This operator is only valid in contexts where type checking is\n    done. The most obvious use case is with the :eql:op:`is` and\n    :eql:op:`is not <is>`. The operator allows to refer to a union of\n    types in order to check whether a value is of any of these\n    types.\n\n    .. code-block:: edgeql-repl\n\n        db> select User is (Text | Named);\n        {true, ..., true}  # one for every user instance\n\n    It can similarly be used when specifying a link target type. The\n    same logic then applies: in order to be a valid link target the\n    object must satisfy ``object is (A | B | C)``.\n\n    .. code-block:: sdl\n\n        abstract type Named {\n            required name: str;\n        }\n\n        abstract type Text {\n            required body: str;\n        }\n\n        type Item extending Named;\n\n        type Note extending Text;\n\n        type User extending Named {\n            multi stuff: Named | Text;\n        }\n\n    With the above schema, the following would be valid:\n\n    .. code-block:: edgeql-repl\n\n        db> insert Item {name := 'cube'};\n        {Object { id: <uuid>'...' }}\n        db> insert Note {body := 'some reminder'};\n        {Object { id: <uuid>'...' }}\n        db> insert User {\n        ...     name := 'Alice',\n        ...     stuff := Note,  # all the notes\n        ... };\n        {Object { id: <uuid>'...' }}\n        db> insert User {\n        ...     name := 'Bob',\n        ...     stuff := Item,  # all the items\n        ... };\n        {Object { id: <uuid>'...' }}\n        db> select User {\n        ...     name,\n        ...     stuff: {\n        ...         [is Named].name,\n        ...         [is Text].body\n        ...     }\n        ... };\n        {\n            Object {\n                name: 'Alice',\n                stuff: {Object { name: {}, body: 'some reminder' }}\n            },\n            Object {\n                name: 'Bob',\n                stuff: {Object { name: 'cube', body: {} }}\n            }\n        }\n\n\n-----------\n\n\n.. eql:operator:: cast: < type > anytype -> anytype\n\n    .. index:: type conversion, convert type\n    .. api-index:: <§type§>§expr§\n\n    Type cast operator.\n\n    A type cast operator converts the specified value to another value of\n    the specified type:\n\n    .. eql:synopsis::\n\n        \"<\" <type> \">\" <expression>\n\n    The :eql:synopsis:`<type>` must be a valid :ref:`type expression\n    <ref_eql_types>` denoting a non-abstract scalar or a container type.\n\n    Type cast is a run-time operation.  The cast will succeed only if a\n    type conversion was defined for the type pair, and if the source value\n    satisfies the requirements of a target type. Gel allows casting any\n    scalar.\n\n    It is illegal to cast one :eql:type:`Object` into another. The\n    only way to construct a new :eql:type:`Object` is by using\n    :eql:stmt:`insert`. However, the :eql:op:`type intersection\n    <isintersect>` can be used to achieve an effect similar to\n    casting for Objects.\n\n    When a cast is applied to an expression of a known type, it represents a\n    run-time type conversion. The cast will succeed only if a suitable type\n    conversion operation has been defined.\n\n    Examples:\n\n    .. code-block:: edgeql-repl\n\n        db> # cast a string literal into an integer\n        ... select <int64>\"42\";\n        {42}\n\n        db> # cast an array of integers into an array of str\n        ... select <array<str>>[1, 2, 3];\n        {['1', '2', '3']}\n\n        db> # cast an issue number into a string\n        ... select <str>example::Issue.number;\n        {'142'}\n\n    Casts also work for converting tuples or declaring different tuple\n    element names for convenience.\n\n    .. code-block:: edgeql-repl\n\n        db> select <tuple<int64, str>>(1, 3);\n        {[1, '3']}\n\n        db> with\n        ...     # a test tuple set, that could be a result of\n        ...     # some other computation\n        ...     stuff := (1, 'foo', 42)\n        ... select (\n        ...     # cast the tuple into something more convenient\n        ...     <tuple<a: int64, name: str, b: int64>>stuff\n        ... ).name;  # access the 'name' element\n        {'foo'}\n\n\n    An important use of *casting* is in defining the type of an empty\n    set ``{}``, which can be required for purposes of type disambiguation.\n\n    .. code-block:: edgeql\n\n        with module example\n        select Text {\n            name :=\n                Text[is Issue].name if Text is Issue else\n                <str>{},\n                # the cast to str is necessary here, because\n                # the type of the computed expression must be\n                # defined\n            body,\n        };\n\n    Casting empty sets is also the only situation where casting into an\n    :eql:type:`Object` is valid:\n\n    .. code-block:: edgeql\n\n        with module example\n        select User {\n            name,\n            friends := <User>{}\n            # the cast is the only way to indicate that the\n            # computed link 'friends' is supposed to refer to\n            # a set of Users\n        };\n\n    For more information about casting between different types consult\n    the :ref:`casting table <ref_eql_casts_table>`.\n\n\n-----------\n\n\n.. eql:operator:: typeof: typeof anytype -> type\n\n    .. index:: introspection\n    .. api-index:: typeof§ type§\n\n    Static type inference operator.\n\n    This operator converts an expression into a type, which can be\n    used with :eql:op:`is`, :eql:op:`is not<is>`, and\n    :eql:op:`introspect`.\n\n    Currently, ``typeof`` operator only supports :ref:`scalars\n    <ref_datamodel_scalar_types>` and :ref:`objects\n    <ref_datamodel_object_types>`, but **not** the :ref:`collections\n    <ref_datamodel_collection_types>` as a valid operand.\n\n    Consider the following types using links and properties with names\n    that don't indicate their respective target types:\n\n    .. code-block:: sdl\n\n        type Foo {\n            bar: int16;\n            baz: Bar;\n        }\n\n        type Bar extending Foo;\n\n    We can use ``typeof`` to determine if certain expression has the\n    same type as the property ``bar``:\n\n    .. code-block:: edgeql-repl\n\n        db> insert Foo { bar := 1 };\n        {Object { id: <uuid>'...' }}\n        db> select (Foo.bar / 2) is typeof Foo.bar;\n        {false}\n\n    To determine the actual resulting type of an expression we can\n    use :eql:op:`introspect`:\n\n    .. code-block:: edgeql-repl\n\n        db> select introspect (typeof Foo.bar).name;\n        {'std::int16'}\n        db> select introspect (typeof (Foo.bar / 2)).name;\n        {'std::float64'}\n\n    Similarly, we can use ``typeof`` to discriminate between the\n    different ``Foo`` objects that can and cannot be targets of link\n    ``baz``:\n\n    .. code-block:: edgeql-repl\n\n        db> insert Bar { bar := 2 };\n        {Object { id: <uuid>'...' }}\n        db> select Foo {\n        ...     bar,\n        ...     can_be_baz := Foo is typeof Foo.baz\n        ... };\n        {\n            Object { bar: 1, can_be_baz: false },\n            Object { bar: 2, can_be_baz: true }\n        }\n\n\n-----------\n\n\n.. eql:operator:: introspect: introspect type -> schema::Type\n\n    .. api-index:: introspect§ type§\n\n    Static type introspection operator.\n\n    This operator returns the :ref:`introspection type\n    <ref_datamodel_introspection>` corresponding to type provided as\n    operand. It works well in combination with :eql:op:`typeof`.\n\n    Currently, the ``introspect`` operator only supports :ref:`scalar\n    types <ref_datamodel_scalar_types>` and :ref:`object types\n    <ref_datamodel_object_types>`, but **not** the :ref:`collection\n    types <ref_datamodel_collection_types>` as a valid operand.\n\n    Consider the following types using links and properties with names\n    that don't indicate their respective target types:\n\n    .. code-block:: sdl\n\n        type Foo {\n            bar: int16;\n            baz: Bar;\n        }\n\n        type Bar extending Foo;\n\n    .. code-block:: edgeql-repl\n\n        db> select (introspect int16).name;\n        {'std::int16'}\n        db> select (introspect Foo).name;\n        {'default::Foo'}\n        db> select (introspect typeof Foo.bar).name;\n        {'std::int16'}\n\n    .. note::\n\n        For any :ref:`object type <ref_datamodel_object_types>`\n        ``SomeType`` the expressions ``introspect SomeType`` and\n        ``introspect typeof SomeType`` are equivalent as the object\n        type name is syntactically identical to the *expression*\n        denoting the set of those objects.\n\n    There's an important difference between the combination of\n    ``introspect typeof SomeType`` and ``SomeType.__type__``\n    expressions when used with objects. ``introspect typeof SomeType``\n    is statically evaluated and does not take in consideration the\n    actual objects contained in the ``SomeType`` set. Conversely,\n    ``SomeType.__type__`` is the actual set of all the types reachable\n    from all the ``SomeType`` objects. Due to inheritance statically\n    inferred types and actual types may not be the same (although the\n    actual types will always be a subtype of the statically inferred\n    types):\n\n    .. code-block:: edgeql-repl\n\n        db> # first let's make sure we don't have any Foo objects\n        ... delete Foo;\n        { there may be some deleted objects here }\n        db> select (introspect typeof Foo).name;\n        {'default::Foo'}\n        db> select Foo.__type__.name;\n        {}\n        db> # let's add an object of type Foo\n        ... insert Foo;\n        {Object { id: <uuid>'...' }}\n        db> # Bar is also of type Foo\n        ... insert Bar;\n        {Object { id: <uuid>'...' }}\n        db> select (introspect typeof Foo).name;\n        {'default::Foo'}\n        db> select Foo.__type__.name;\n        {'default::Bar', 'default::Foo'}\n"
  },
  {
    "path": "docs/reference/stdlib/uuid.rst",
    "content": ".. _ref_std_uuid:\n\n=====\nUUIDs\n=====\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:type:`uuid`\n      - UUID type\n\n    * - :eql:op:`= <eq>` :eql:op:`\\!= <neq>` :eql:op:`?= <coaleq>`\n        :eql:op:`?!= <coalneq>` :eql:op:`\\< <lt>` :eql:op:`\\> <gt>`\n        :eql:op:`\\<= <lteq>` :eql:op:`\\>= <gteq>`\n      - Comparison operators\n\n    * - :eql:func:`uuid_generate_v1mc`\n      - :eql:func-desc:`uuid_generate_v1mc`\n\n    * - :eql:func:`uuid_generate_v4`\n      - :eql:func-desc:`uuid_generate_v4`\n\n    * - :eql:func:`to_uuid`\n      - :eql:func-desc:`to_uuid`\n\n\n---------\n\n\n.. eql:type:: std::uuid\n\n    Universally Unique Identifiers (UUID).\n\n    For formal definition see RFC 4122 and ISO/IEC 9834-8:2005.\n\n    Every :eql:type:`Object` has a globally unique property ``id``\n    represented by a UUID value.\n\n    A UUID can be cast to an object type if an object of that type with a\n    matching ID exists.\n\n    .. code-block:: edgeql-repl\n\n        db> select <Hero><uuid>'01d9cc22-b776-11ed-8bef-73f84c7e91e7';\n        {default::Hero {id: 01d9cc22-b776-11ed-8bef-73f84c7e91e7}}\n\n\n---------\n\n\n.. eql:function:: std::uuid_generate_v1mc() -> uuid\n\n    Return a version 1 UUID.\n\n    The algorithm uses a random multicast MAC address instead of the\n    real MAC address of the computer.\n\n    The UUID will contain 47 random bits, 60 bits representing the\n    current time, and 14 bits of clock sequence that may be used to\n    ensure uniqueness. The rest of the bits indicate the version of\n    the UUID.\n\n    This is the default function used to populate the ``id`` column.\n\n    .. code-block:: edgeql-repl\n\n        db> select uuid_generate_v1mc();\n        {1893e2b6-57ce-11e8-8005-13d4be166783}\n\n\n---------\n\n\n.. eql:function:: std::uuid_generate_v4() -> uuid\n\n    Return a version 4 UUID.\n\n    The UUID is derived entirely from random numbers: it will contain\n    122 random bits and 6 version bits.\n\n    It is permitted to override the ``default`` of the ``id`` column\n    with a call to this function, but this should be done with\n    caution: fully random ids will be less clustered than time-based id,\n    which may lead to worse index performance.\n\n    .. code-block:: edgeql-repl\n\n        db> select uuid_generate_v4();\n        {92673afc-9c4f-42b3-8273-afe0053f0f48}\n\n\n---------\n\n\n.. eql:function:: std::to_uuid(val: bytes) -> uuid\n\n    .. index:: parse uuid\n\n    Returns a :eql:type:`uuid` value parsed from 128-bit input.\n\n    The :eql:type:`bytes` string has to be a valid 128-bit UUID\n    representation.\n\n    .. code-block:: edgeql-repl\n\n        db> select to_uuid(\n        ...   b'\\x92\\x67\\x3a\\xfc\\\n        ...     \\x9c\\x4f\\\n        ...     \\x42\\xb3\\\n        ...     \\x82\\x73\\\n        ...     \\xaf\\xe0\\x05\\x3f\\x0f\\x48');\n        {92673afc-9c4f-42b3-8273-afe0053f0f48}\n"
  },
  {
    "path": "docs/reference/using/cli/gel.rst",
    "content": ".. _ref_cli_gel:\n\n===\ngel\n===\n\n:edb-alt-title: gel — Interactive Shell\n\nGel interactive shell:\n\n.. cli:synopsis::\n\n    gel [<connection-option>...]\n\nIt's also possible to run an EdgeQL script by piping it into the\n|Gel| shell. The shell will then run in non-interactive mode and\nprint all the responses into the standard output:\n\n.. cli:synopsis::\n\n    cat myscript.edgeql | gel [<connection-option>...]\n\nThe above command also works on PowerShell in Windows, while the classic\nWindows Command Prompt uses a different command as shown below:\n\n.. cli:synopsis::\n\n    type myscript.edgeql | gel [<connection-option>...]\n\nDescription\n===========\n\n|gelcmd| is a terminal-based front-end to |Gel|.  It allows running\nqueries and seeing results interactively.\n\n\nOptions\n=======\n\n:cli:synopsis:`-h, --help`\n    Show help about the command and exit.\n\n:cli:synopsis:`--help-connect`\n    Show all available :ref:`connection options <ref_cli_gel_connopts>`\n\n:cli:synopsis:`-V, --version`\n    Print version.\n\n:cli:synopsis:`--no-cli-update-check`\n    Disable version check.\n\n:cli:synopsis:`-I <name>, --instance=<name>`\n    Specifies the named instance to connect to. The actual connection\n    parameters are stored in ``<gel_config_dir>/credentials`` and are\n    usually created by :ref:`ref_cli_gel_instance_create` or similar\n    commands. Run :gelcmd:`info` to see the location of\n    ``<gel_config_dir>`` on your machine.\n\n    This option overrides host and port.\n\n:cli:synopsis:`--dsn=<dsn>`\n    Specifies the DSN for |Gel| to connect to.\n\n    This option overrides all other options except password.\n\n:cli:synopsis:`--credentials-file /path/to/file`\n    Path to JSON file containing credentials.\n\n:cli:synopsis:`-H <hostname>, --host=<hostname>`\n    Specifies the host name of the machine on which the server is running.\n    Defaults to the value of the :gelenv:`HOST` environment variable.\n\n:cli:synopsis:`-P <port>, --port=<port>`\n    Specifies the TCP port on which the server is listening for connections.\n    Defaults to the value of the :gelenv:`PORT` environment variable or,\n    if not set, to ``5656``.\n\n:cli:synopsis:`-u <username>, --user=<username>`\n    Connect to the database as the user :cli:synopsis:`<username>`.\n    Defaults to the value of the :gelenv:`USER` environment variable, or,\n    if not set, to the login name of the current OS user.\n\n:cli:synopsis:`-b <branch-name>, --branch=<branch-name>`\n    Specifies the name of the branch to connect to. Default to the value\n    of the :gelenv:`BRANCH` environment variable, or, if not set, to\n    the calculated value of :cli:synopsis:`<username>`.\n\n    .. note::\n\n        |EdgeDB| 5.0 introduced :ref:`branches <ref_datamodel_branches>` to\n        replace databases. If you are running an earlier version of\n        Gel, you will instead use the ``-d <dbname>, --database=<dbname>``\n        option above.\n\n:cli:synopsis:`--password | --no-password`\n    If :cli:synopsis:`--password` is specified, force |gelcmd| to prompt\n    for a password before connecting to the database.  This is usually not\n    necessary, since ``gel`` will prompt for a password automatically\n    if the server requires it.\n\n    Specifying :cli:synopsis:`--no-password` disables all password prompts.\n\n:cli:synopsis:`--password-from-stdin`\n    Use the first line of standard input as the password.\n\n:cli:synopsis:`--tls-ca-file /path/to/cert`\n    Certificate to match server against.\n\n    This might either be full self-signed server certificate or\n    certificate authority (CA) certificate that server certificate is\n    signed with.\n\n:cli:synopsis:`--tls-security mode`\n    Set the TLS security mode.\n\n    ``default``\n        Resolves to ``strict`` if no custom certificate is supplied via\n        :cli:synopsis:`--tls-ca-file`, environment variable, etc. Otherwise,\n        resolves to ``no_host_verification``.\n\n    ``strict``\n        Verify TLS certificate and hostname.\n\n    ``no_host_verification``\n        This allows using any certificate for any hostname. However,\n        certificate must be present and match the root certificate specified\n        with  :cli:synopsis:`--tls-ca-file`, credentials file, or system root\n        certificates.\n\n    ``insecure``\n        Disable all TLS security measures.\n\n:cli:synopsis:`--wait-until-available=<wait_time>`\n    In case |Gel| connection can't be established, keep retrying up\n    to :cli:synopsis:`<wait_time>` (e.g. ``30s``).\n\n:cli:synopsis:`--connect-timeout=<timeout>`\n    Specifies a :cli:synopsis:`<timeout>` period. In case |Gel|\n    doesn't respond for this period the command will fail (or retry if\n    :cli:synopsis:`--wait-until-available` is also specified). The\n    :cli:synopsis:`<timeout>` value must be given using time units\n    (e.g. ``hr``, ``min``, ``sec``, ``ms``, etc.). The default\n    value is ``10s``.\n\n\nBackslash Commands\n==================\n\nIntrospection\n-------------\n\nThe introspection commands share a few common options that are available to\nmany of the commands:\n\n- ``-v``- Verbose\n- ``-s``- Show system objects\n- ``-c``- Case-sensitive pattern matching\n\n:cli:synopsis:`\\\\d [-v] OBJECT-NAME, \\\\describe [-v] OBJECT-NAME`\n  Describe schema object specified by *OBJECT-NAME*.\n\n:cli:synopsis:`\\\\ds, \\\\d schema, \\\\describe schema`\n  Describe the entire schema.\n\n:cli:synopsis:`\\\\l`\n  List branches on |Gel| server 5+ or databases on prior versions.\n\n:cli:synopsis:`\\\\list branches`\n  List branches.\n\n  .. note::\n\n      |EdgeDB| 5.0 introduced :ref:`branches <ref_datamodel_branches>` to replace\n      databases. If you are running an earlier version of Gel,\n      you will instead use the ``\\list databases`` command above.\n\n:cli:synopsis:`\\\\ls [-sc] [PATTERN], \\\\list scalars [-sc] [PATTERN]`\n  List scalar types.\n\n:cli:synopsis:`\\\\lt [-sc] [PATTERN], \\\\list types [-sc] [PATTERN]`\n  List object types.\n\n:cli:synopsis:`\\\\lr [-c] [PATTERN], \\\\list roles [-c] [PATTERN]`\n  List roles.\n\n:cli:synopsis:`\\\\lm [-c] [PATTERN], \\\\list modules [-c] [PATTERN]`\n  List modules.\n\n:cli:synopsis:`\\\\la [-vsc] [PATTERN], \\\\list aliases [-vsc] [PATTERN]`\n  List expression aliases.\n\n:cli:synopsis:`\\\\lc [-c] [PATTERN], \\\\list casts [-c] [PATTERN]`\n  List available conversions between types.\n\n:cli:synopsis:`\\\\li [-vsc] [PATTERN], \\\\list indexes [-vsc] [PATTERN]`\n  List indexes.\n\nBranch\n------\n\n.. versionadded:: 5.0\n\n|EdgeDB| 5.0 introduced :ref:`branches <ref_datamodel_branches>` to replace\ndatabases. If you are running an earlier version of Gel,\nyou will instead use the database commands above.\n\n:cli:synopsis:`\\\\branch create NAME`\n  Create a new branch. The backslash command mirrors the options of the CLI's\n  :ref:`ref_cli_gel_branch_create`.\n\n:cli:synopsis:`\\\\branch switch NAME`\n  Switch to a different branch. The backslash command mirrors the options of\n  the CLI's :ref:`ref_cli_gel_branch_switch`.\n\nQuery Analysis\n--------------\n\n:cli:synopsis:`\\\\analyze QUERY`\n  Run a query performance analysis on the given query. Most conveniently used\n  without a backslash by just adding ``analyze`` before any query.\n\n:cli:synopsis:`\\\\expand`\n  Print expanded output of last ``analyze`` operation.\n\nData Operations\n---------------\n\n:cli:synopsis:`\\\\dump FILENAME`\n  Dump current database branch to a file at *FILENAME*.\n\n:cli:synopsis:`\\\\restore FILENAME`\n  Restore the database dump at *FILENAME* into the current |branch|.\n\nEditing\n-------\n\n:cli:synopsis:`\\\\s, \\\\history`\n  Show a history of commands executed in the shell.\n\n:cli:synopsis:`\\\\e, \\\\edit [N]`\n  Spawn ``$EDITOR`` to edit the most recent history entry or history entry *N*.\n  History entries are negative indexed with ``-1`` being the most recent\n  command. Use the ``\\history`` command (above) to see previous command\n  indexes.\n\n  The output of this will then be used as input into the shell.\n\nSettings\n--------\n\n:cli:synopsis:`\\\\set [OPTION [VALUE]]`\n  If *VALUE* is omitted, the command will show the current value of *OPTION*.\n  With *VALUE*, the option named by *OPTION* will be set to the provided value.\n  Use ``\\set`` with no arguments for a listing of all available options.\n\nConnection\n----------\n\n:cli:synopsis:`\\\\c, \\\\connect [NAME]`\n  Connect to branch *NAME*.\n\nMigrations\n----------\n\nThese migration commands are also accessible directly from the command line\nwithout first entering the |Gel| shell. Their counterpart commands are noted\nand linked in their descriptions if you want more detail.\n\n:cli:synopsis:`\\\\migration create`\n  Create a migration script based on differences between the current |branch|\n   and the schema file, just like running :ref:`ref_cli_gel_migration_create`.\n\n:cli:synopsis:`\\\\migrate, \\\\migration apply`\n  Apply your migration, just like running the\n  :ref:`ref_cli_gel_migrate`.\n\n:cli:synopsis:`\\\\migration edit`\n  Spawn ``$EDITOR`` on the last migration file and fixes the migration ID after\n  the editor exits, just like :ref:`ref_cli_gel_migration_edit`. This is\n  typically used only on migrations that have not yet been applied.\n\n:cli:synopsis:`\\\\migration log`\n  Show the migration history, just like :ref:`ref_cli_gel_migration_log`.\n\n:cli:synopsis:`\\\\migration status`\n  Show how the state of the schema in the |Gel| instance compares to the\n  migration stored in the schema directory, just like\n  :ref:`ref_cli_gel_migration_status`.\n\nHelp\n----\n\n:cli:synopsis:`\\\\?, \\\\h, \\\\help`\n  Show help on backslash commands.\n\n:cli:synopsis:`\\\\q, \\\\quit, \\\\exit`\n  Quit the REPL. You can also do this by pressing Ctrl+D.\n\n\nDatabase\n--------\n\n.. note::\n\n    |EdgeDB| 5.0 introduced :ref:`branches <ref_datamodel_branches>` to replace\n    databases. If you are running 5.0 or later, you will instead use the\n    commands in the \"Branch\" section above.\n\n:cli:synopsis:`\\\\database create NAME`\n  Create a new database.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_analyze.rst",
    "content": ".. _ref_cli_gel_analyze:\n\n\n===========\ngel analyze\n===========\n\n.. note::\n\n    Performance analysis is also available in our :ref:`CLI REPL\n    <ref_cli_gel>` and the UI's REPL and query builder (both accessible by\n    running :ref:`ref_cli_gel_ui` to invoke your instance's UI). Use it by\n    prepending your query with ``analyze``.\n\nRun a query performance analysis on the given query.\n\n.. cli:synopsis::\n\n\tgel analyze [<options>] <query>\n\nAn example of ``analyze`` output from a simple query:\n\n.. lint-off\n\n.. code-block::\n\n    ──────────────────────────────────────── Query ────────────────────────────────────────\n    analyze select ➊  Hero {name, secret_identity, ➋  villains: {name, ➌  nemesis: {name}}};\n\n    ──────────────────────── Coarse-grained Query Plan ────────────────────────\n                       │ Time     Cost Loops Rows Width │ Relations\n    ➊ root            │  0.0 69709.48   1.0  0.0    32 │ Hero\n    ╰──➋ .villains    │  0.0     92.9   0.0  0.0    32 │ Villain, Hero.villains\n    ╰──➌ .nemesis     │  0.0     8.18   0.0  0.0    32 │ Hero\n\n.. lint-on\n\n\nOptions\n=======\n\nThe ``analyze`` command runs on the database it is connected to. For specifying\nthe connection target see :ref:`connection options <ref_cli_gel_connopts>`.\n\n:cli:synopsis:`<query>`\n    The query to analyze. Be sure to wrap the query in quotes.\n\n:cli:synopsis:`--expand`\n    Print expanded output of the query analysis\n\n:cli:synopsis:`--debug-output-file <debug_output_file>`\n    Write analysis into the JSON file specified instead of formatting\n\n:cli:synopsis:`--read-json <read_json>`\n    Read JSON file instead of executing a query\n"
  },
  {
    "path": "docs/reference/using/cli/gel_branch/gel_branch_create.rst",
    "content": ".. _ref_cli_gel_branch_create:\n\n\n=================\ngel branch create\n=================\n\nCreate a new :ref:`branch <ref_datamodel_branches>`.\n\n.. cli:synopsis::\n\n    gel branch create [<options>] <name>\n\nDescription\n===========\n\n:gelcmd:`branch create` creates a new branch with the same schema as the\ncurrent branch specified in ``$CONFIG/credentials``. Without any options, it is\nequivalent to :eql:stmt:`create schema branch`.\n\n\nOptions\n=======\n\nThe ``branch create`` command runs in the |Gel| instance it is\nconnected to. For specifying the connection target see\n:ref:`connection options <ref_cli_gel_connopts>`.\n\n:cli:synopsis:`<name>`\n    The name of the new branch.\n\n:cli:synopsis:`--from <oldbranch>`\n    The optional base branch to create the new branch from. Defaults to the\n    current branch specified in ``$CONFIG/credentials``.\n\n:cli:synopsis:`-e, --empty`\n    Create a branch with no schema or data.\n\n:cli:synopsis:`--copy-data`\n    Copy data from the base branch to the new branch.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_branch/gel_branch_drop.rst",
    "content": ".. _ref_cli_gel_branch_drop:\n\n\n===============\ngel branch drop\n===============\n\nRemove an existing :ref:`branch <ref_datamodel_branches>`.\n\n.. cli:synopsis::\n\n    gel branch drop [<options>] <name>\n\nOptions\n=======\n\nThe ``branch drop`` command runs in the |Gel| instance it is\nconnected to. For specifying the connection target see\n:ref:`connection options <ref_cli_gel_connopts>`.\n\n:cli:synopsis:`<name>`\n    The name of the branch to drop.\n\n:cli:synopsis:`--non-interactive`\n    Drop the branch without asking for confirmation.\n\n:cli:synopsis:`--force`\n    Close any existing connections to the branch before dropping it.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_branch/gel_branch_list.rst",
    "content": ".. _ref_cli_gel_branch_list:\n\n\n===============\ngel branch list\n===============\n\nList all :ref:`branches <ref_datamodel_branches>`\n\n.. cli:synopsis::\n\n    gel branch list\n"
  },
  {
    "path": "docs/reference/using/cli/gel_branch/gel_branch_merge.rst",
    "content": ".. _ref_cli_gel_branch_merge:\n\n\n================\ngel branch merge\n================\n\nMerge a :ref:`branch <ref_datamodel_branches>` into the current branch.\n\n.. cli:synopsis::\n\n    gel branch merge [<options>] <name>\n\n\nDescription\n===========\n\nMerges the target branch with the current branch using a fast-forward strategy,\napplying any new migrations from the target branch on the current branch.\n\n.. note::\n\n    This is a fast-forward merge, so no conflict resolution will be applied to\n    the new migrations. If you want to merge but may have conflicts, you should\n    first use :ref:`ref_cli_gel_branch_rebase` from the target branch before\n    merging.\n\n.. note::\n\n    When merging, the data of the current branch is preserved. This means that\n    if you switch to a branch |main| and run :gelcmd:`branch merge feature`,\n    you will end up with a branch with the schema from |main| and any\n    new migrations from ``feature`` and the data from |main|.\n\n\nOptions\n=======\n\nThe ``branch merge`` command runs in the |Gel| instance it is\nconnected to. For specifying the connection target see\n:ref:`connection options <ref_cli_gel_connopts>`.\n\n:cli:synopsis:`<name>`\n    The name of the branch to merge into the current branch.\n:cli:synopsis:`--no-apply`\n    Skip applying migrations generated from the merge\n"
  },
  {
    "path": "docs/reference/using/cli/gel_branch/gel_branch_rebase.rst",
    "content": ".. _ref_cli_gel_branch_rebase:\n\n\n=================\ngel branch rebase\n=================\n\nCreate a :ref:`branch <ref_datamodel_branches>` based on the target branch but\nincluding new migrations on the current branch.\n\n.. cli:synopsis::\n\n    gel branch rebase [<options>] <name>\n\n\nDescription\n===========\n\nCreates a new branch that is based on the target branch, but also contains any new migrations on the\ncurrent branch.\n\n.. note::\n\n    When rebasing, the data of the target branch is preserved. This means that\n    if you switch to a branch ``feature`` and run :gelcmd:`branch rebase\n    main`, you will end up with a branch with the schema from |main| and any\n    new migrations from ``feature`` and the data from |main|.\n\nFor more about how rebasing works, check out the breakdown :ref:`in our schema\nmigrations guide <ref_migration_guide_branches_rebasing>`.\n\n\nOptions\n=======\n\nThe ``branch rebase`` command runs in the |Gel| instance it is\nconnected to. For specifying the connection target see\n:ref:`connection options <ref_cli_gel_connopts>`.\n\n:cli:synopsis:`<name>`\n    The name of the target branch.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_branch/gel_branch_rename.rst",
    "content": ".. _ref_cli_gel_branch_rename:\n\n\n=================\ngel branch rename\n=================\n\nRename a :ref:`branch <ref_datamodel_branches>`\n\n.. cli:synopsis::\n\n    gel branch rename [<options>] <old-name> <new-name>\n\n\nOptions\n=======\n\nThe ``branch rename`` command runs in the |Gel| instance it is\nconnected to. For specifying the connection target see\n:ref:`connection options <ref_cli_gel_connopts>`.\n\n:cli:synopsis:`<old-name>`\n    The current name of the branch to rename.\n\n:cli:synopsis:`<new-name>`\n    The new name of the branch.\n\n:cli:synopsis:`--force`\n    Close any existing connections to the branch before renaming it.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_branch/gel_branch_switch.rst",
    "content": ".. _ref_cli_gel_branch_switch:\n\n\n=================\ngel branch switch\n=================\n\nChange the currently active :ref:`branch <ref_datamodel_branches>`\n\n.. cli:synopsis::\n\n    gel branch switch [<options>] <name>\n\n.. note::\n\n    This CLI command requires |Gel| (or |EdgeDB| version 5.0 or later.)\n    Earlier versions did not feature branches and instead featured\n    **databases**.\n\n    Databases offered no direct analog to switching.\n\n    - To run a single command on a different |branch|, use the ``-d <dbname>``\n      or ``--database=<dbname>`` options described in\n      :ref:`ref_cli_gel_connopts`\n    - To change the database for *all* commands, set the :gelenv:`DATABASE`\n      environment variable described in :ref:`ref_cli_gel_connopts`\n    - To change the database for all commands in a project, you may update the\n      ``credentials.json`` file's ``database`` value. To find that file for\n      your project, run :ref:`ref_cli_gel_info` to get the config path and\n      navigate to ``/<config-path>/credentials``.\n    - You may use ``\\connect <dbname>`` or ``\\c <dbname>`` to change the\n      connected database while in a REPL session.\n\n    See the :ref:`ref_cli_gel_database` command suite for other database\n    management commands.\n\n\nOptions\n=======\n\nThe ``branch switch`` command runs in the |Gel| instance it is\nconnected to. For specifying the connection target see\n:ref:`connection options <ref_cli_gel_connopts>`.\n\n:cli:synopsis:`<name>`\n    The name of the new |branch|.\n\n:cli:synopsis:`-c, --create`\n    Create the branch if it doesn't exist.\n\n:cli:synopsis:`-e, --empty`\n    If creating a new branch: create the branch with no schema or data.\n\n:cli:synopsis:`--from <FROM>`\n    If creating a new branch: the optional base branch to create the new branch\n    from.\n\n:cli:synopsis:`--copy-data`\n    If creating a new branch: copy data from the base branch to the new branch.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_branch/gel_branch_wipe.rst",
    "content": ".. _ref_cli_gel_branch_wipe:\n\n\n===============\ngel branch wipe\n===============\n\nDestroy the contents of a :ref:`branch <ref_datamodel_branches>`\n\n.. cli:synopsis::\n\n    gel branch wipe [<options>] <name>\n\nDescription\n===========\n\nThe contents of the branch will be destroyed and the schema reset to its\nstate before any migrations, but the branch itself will be preserved.\n\n:gelcmd:`branch wipe` is a terminal command equivalent to\n:eql:stmt:`reset schema to initial`.\n\n\nOptions\n=======\n\nThe ``branch wipe`` command runs in the |Gel| instance it is\nconnected to. For specifying the connection target see\n:ref:`connection options <ref_cli_gel_connopts>`.\n\n:cli:synopsis:`<name>`\n    The name of the branch to wipe.\n\n:cli:synopsis:`--non-interactive`\n    Destroy the data without asking for confirmation.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_branch/index.rst",
    "content": ".. _ref_cli_gel_branch:\n\n\n==========\ngel branch\n==========\n\nThe :gelcmd:`branch` group of commands contains various branch management\ntools.\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    gel_branch_create\n    gel_branch_drop\n    gel_branch_list\n    gel_branch_merge\n    gel_branch_rebase\n    gel_branch_rename\n    gel_branch_switch\n    gel_branch_wipe\n\n.. list-table::\n    :class: funcoptable\n\n    * - :ref:`ref_cli_gel_branch_create`\n      - Create a new branch\n    * - :ref:`ref_cli_gel_branch_drop`\n      - Drop a branch\n    * - :ref:`ref_cli_gel_branch_list`\n      - List all branches\n    * - :ref:`ref_cli_gel_branch_merge`\n      - Merge a branch into the current branch\n    * - :ref:`ref_cli_gel_branch_rebase`\n      - Create a branch based on a target branch\n    * - :ref:`ref_cli_gel_branch_rename`\n      - Rename a branch\n    * - :ref:`ref_cli_gel_branch_switch`\n      - Change the currently active branch\n    * - :ref:`ref_cli_gel_branch_wipe`\n      - Destroy the contents of a branch\n"
  },
  {
    "path": "docs/reference/using/cli/gel_cli_upgrade.rst",
    "content": ".. _ref_cli_gel_cli_upgrade:\n\n\n===============\ngel cli upgrade\n===============\n\nUpgrade the CLI binary.\n\n.. cli:synopsis::\n\n    gel cli upgrade [<options>]\n\n\nDescription\n===========\n\n:gelcmd:`cli upgrade` is a terminal command used to upgrade the CLI\ntools to keep them up-to-date.\n\n\nOptions\n=======\n\n:cli:synopsis:`--force`\n    Reinstall the CLI even if there is no newer version.\n\n:cli:synopsis:`--quiet`\n    Don't show the progress bar.\n\n:cli:synopsis:`--verbose`\n    Enable verbose output.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_cloud/gel_cloud_login.rst",
    "content": ".. _ref_cli_gel_cloud_login:\n\n\n===============\ngel cloud login\n===============\n\nAuthenticate to the |Gel| Cloud and remember the secret key locally\n\n.. cli:synopsis::\n\n    gel cloud login\n\nThis command will launch your browser and start the |Gel| Cloud authentication\nflow. Once authentication is successful, the CLI will log a success message:\n\n.. code-block::\n\n    Successfully logged in to |Gel| Cloud as <your-email>\n\nIf you are unable to complete authentication in the browser, you can interrupt\nthe command by pressing Ctrl-C.\n\n.. warning:: CI users and scripters\n\n    This command is not intended for use in scripting and CI. Instead, you\n    should generate a secret key in the |Gel| Cloud UI or by running\n    :ref:`ref_cli_gel_cloud_secretkey_create` and set the\n    :gelenv:`SECRET_KEY` environment variable to your secret key. Once this\n    variable is set to your secret key, logging in is no longer required.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_cloud/gel_cloud_logout.rst",
    "content": ".. _ref_cli_gel_cloud_logout:\n\n\n================\ngel cloud logout\n================\n\nForget the stored access token\n\n.. cli:synopsis::\n\n    gel cloud logout [<options>]\n\n.. warning:: CI users and scripters\n\n    This command is not intended for use in scripting and CI. Instead, to\n    authenticate to your |Gel| Cloud account, you should generate a secret key\n    in the Gel Cloud UI or by running\n    :ref:`ref_cli_gel_cloud_secretkey_create` and set the\n    :gelenv:`SECRET_KEY` environment variable to your secret key. Logging out\n    is not necessary.\n\nOptions\n=======\n\n:cli:synopsis:`--all-profiles`\n    Logout from all Cloud profiles\n:cli:synopsis:`--force`\n    Force log out from all profiles, even if linked to a project\n:cli:synopsis:`--non-interactive`\n    Do not ask questions, assume user wants to log out of all profiles not\n    linked to a project\n"
  },
  {
    "path": "docs/reference/using/cli/gel_cloud/gel_cloud_secretkey/edgedb_cloud_secretkey_create.rst",
    "content": ".. _ref_cli_gel_cloud_secretkey_create:\n\n\n==========================\ngel cloud secretkey create\n==========================\n\nCreate a new secret key\n\n.. cli:synopsis::\n\n    gel cloud secretkey create [<options>]\n\n.. note::\n\n    This command works only if you have already authenticated using\n    :ref:`ref_cli_gel_cloud_login`.\n\nOptions\n=======\n\n:cli:synopsis:`--json`\n    Output results as JSON\n:cli:synopsis:`-n, --name <name>`\n    Friendly key name\n:cli:synopsis:`--description <description>`\n    Long key description\n:cli:synopsis:`--expires <<duration> | \"never\">`\n    Key expiration, in duration units, for example \"1 hour 30 minutes\". If set\n    to \"never\", the key would not expire.\n:cli:synopsis:`--scopes <scopes>`\n    Comma-separated list of key scopes. Mutually exclusive with\n    ``--inherit-scopes``.\n:cli:synopsis:`--inherit-scopes`\n    Inherit key scopes from the currently used key.  Mutually exclusive with\n    ``--scopes``.\n:cli:synopsis:`-y, --non-interactive`\n    Do not ask questions, assume default answers to all inputs that have a\n    default.  Requires key TTL and scopes to be explicitly specified via\n    ``--ttl`` or ``--no-expiration``, and ``--scopes`` or ``--inherit-scopes``.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_cloud/gel_cloud_secretkey/edgedb_cloud_secretkey_list.rst",
    "content": ".. _ref_cli_gel_cloud_secretkey_list:\n\n\n========================\ngel cloud secretkey list\n========================\n\n\nList existing secret keys\n\n.. cli:synopsis::\n\n    gel cloud secretkey list [<options>]\n\n.. note::\n\n    This command works only if you have already authenticated using\n    :ref:`ref_cli_gel_cloud_login`.\n\nOptions\n=======\n\n:cli:synopsis:`--json`\n    Output results as JSON\n"
  },
  {
    "path": "docs/reference/using/cli/gel_cloud/gel_cloud_secretkey/edgedb_cloud_secretkey_revoke.rst",
    "content": ".. _ref_cli_gel_cloud_secretkey_revoke:\n\n\n==========================\ngel cloud secretkey revoke\n==========================\n\nRevoke a secret key\n\n.. cli:synopsis::\n\n    gel cloud secretkey revoke [<options>] --secret-key-id <secret-key-id>\n\n.. note::\n\n    This command works only if you have already authenticated using\n    :ref:`ref_cli_gel_cloud_login`.\n\nOptions\n=======\n\n:cli:synopsis:`--json`\n    Output results as JSON\n:cli:synopsis:`--secret-key-id <secret_key_id>`\n    Id of secret key to revoke\n:cli:synopsis:`-y, --non-interactive`\n    Revoke the key without asking for confirmation.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_cloud/gel_cloud_secretkey/index.rst",
    "content": ".. _ref_cli_gel_cloud_secretkey:\n\n\n===================\ngel cloud secretkey\n===================\n\nManage your secret keys\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    edgedb_cloud_secretkey_create\n    edgedb_cloud_secretkey_list\n    edgedb_cloud_secretkey_revoke\n\n.. list-table::\n    :class: funcoptable\n\n    * - :ref:`ref_cli_gel_cloud_secretkey_create`\n      - Create a new secret key\n    * - :ref:`ref_cli_gel_cloud_secretkey_list`\n      - List existing secret keys\n    * - :ref:`ref_cli_gel_cloud_secretkey_revoke`\n      - Revoke a secret key\n\n.. note::\n\n    These commands work only if you have already authenticated using\n    :ref:`ref_cli_gel_cloud_login`.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_cloud/index.rst",
    "content": ".. _ref_cli_gel_cloud:\n\n\n=========\ngel cloud\n=========\n\n\nIn addition to managing your own local and remote instances, the |Gel| CLI\noffers tools to manage your instances running on our Gel Cloud.\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    gel_cloud_login\n    gel_cloud_logout\n    gel_cloud_secretkey/index\n\n.. list-table::\n    :class: funcoptable\n\n    * - :ref:`ref_cli_gel_cloud_login`\n      - Authenticate to the |Gel| Cloud and remember the access token locally\n    * - :ref:`ref_cli_gel_cloud_logout`\n      - Forget the stored access token\n    * - :ref:`ref_cli_gel_cloud_secretkey`\n      - Manage your secret keys\n\n.. warning:: CI users and scripters\n\n    The :gelcmd:`cloud login` and :gelcmd:`cloud logout` commands are not\n    intended for use in scripting and CI. Instead, you should generate a secret\n    key in the |Gel| Cloud UI or by running\n    :ref:`ref_cli_gel_cloud_secretkey_create` and set the\n    :gelenv:`SECRET_KEY` environment variable to your secret key. Once this\n    variable is set to your secret key, logging in and out are no longer\n    required.\n\nFollow :ref:`our Gel Cloud guide <ref_guide_cloud>` for information on how\nto use Gel Cloud.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_configure.rst",
    "content": ".. _ref_cli_gel_configure:\n\n=============\ngel configure\n=============\n\nConfigure the |Gel| server.\n\n.. cli:synopsis::\n\n    gel configure [<connection-options>] <action> \\\n        [<parameter> <value>] \\\n        [<parameter-class> --<property>=<value> ...]\n\nDescription\n===========\n\n:gelcmd:`configure` is a terminal command used to alter the\nconfiguration of a |Gel| instance. There are three types of\nconfiguration actions that can be performed.\n\nActions\n=======\n\n:cli:synopsis:`gel configure insert`\n    Insert a new configuration entry for a setting that supports\n    multiple configuration objects (e.g. Auth or Port).\n\n:cli:synopsis:`gel configure set`\n    Set a scalar configuration value.\n\n:cli:synopsis:`gel configure reset`\n    Reset an existing configuration entry or remove all values for an\n    entry that supports multiple configuration objects.\n\n\nOptions\n=======\n\nMost of the options are the same across all of the different\nconfiguration actions.\n\n:cli:synopsis:`<connection-options>`\n    See :ref:`connection options <ref_cli_gel_connopts>`.\n\n:cli:synopsis:`<parameter>`\n    The name of a primitive configuration parameter.  Available\n    configuration parameters are described in the :ref:`ref_std_cfg`\n    section.\n\n:cli:synopsis:`<value>`\n    A value literal for a given configuration parameter or configuration\n    object property.\n\n:cli:synopsis:`<parameter-class>`\n    The name of a composite configuration value class.  Available\n    configuration classes are described in the :ref:`ref_std_cfg`\n    section.\n\n:cli:synopsis:`--<property>=<value>`\n    Set the :cli:synopsis:`<property>` of a configuration object to\n    :cli:synopsis:`<value>`.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_connopts.rst",
    "content": ".. _ref_cli_gel_connopts:\n\n================\nConnection flags\n================\n\nThe |gelcmd| CLI supports a standard set of connection flags used to specify\nthe *target* of a given command. The CLI always respects any connection\nparameters passed explicitly using flags.\n\n- If no flags are provided, then environment variables will be\n  used to determine the instance.\n- If no environment variables are present, the CLI will check if the working\n  directory is within an instance-linked project directory.\n- If none of the above are present, the command fails.\n\nFor a detailed breakdown of how connection information is resolved, read the\n:ref:`Connection Parameter Resolution <ref_reference_connection>` docs.\n\n################\nConnection flags\n################\n\n:cli:synopsis:`-I <name>, --instance=<name>`\n    Specifies the named instance to connect to. The actual connection\n    parameters for local and self-hosted instances are stored in\n    ``<gel_config_dir>/credentials`` and are usually created by\n    :ref:`ref_cli_gel_instance_create` or similar commands. Run\n    :gelcmd:`info` to see the location of ``<gel_config_dir>`` on your\n    machine.\n\n    |Gel| Cloud instance names are in the format\n    ``<org-name>/<instance-name>``.\n\n    This option overrides host and port.\n\n:cli:synopsis:`--dsn=<dsn>`\n    Specifies the DSN for |Gel| to connect to.\n\n    This option overrides all other options except password.\n\n:cli:synopsis:`--credentials-file /path/to/file`\n    Path to JSON file containing credentials.\n\n:cli:synopsis:`-H <hostname>, --host=<hostname>`\n    Specifies the host name of the machine on which the server is running.\n    Defaults to the value of the :gelenv:`HOST` environment variable.\n\n:cli:synopsis:`-P <port>, --port=<port>`\n    Specifies the TCP port on which the server is listening for connections.\n    Defaults to the value of the :gelenv:`PORT` environment variable or,\n    if not set, to ``5656``.\n\n:cli:synopsis:`--unix-path /path/to/socket`\n    Specifies a path to a Unix socket for a |Gel| connection. If the path is\n    a directory, the actual path will be computed using the ``port`` and\n    ``admin`` parameters.\n\n:cli:synopsis:`--admin`\n    Connect to a password-less Unix socket (specified by the ``unix-path``)\n    with superuser privileges by default.\n\n:cli:synopsis:`-u <username>, --user=<username>`\n    Connect to the database as the user :cli:synopsis:`<username>`.\n    Defaults to the value of the :gelenv:`USER` environment variable, or,\n    if not set, |admin|.\n\n:cli:synopsis:`-b <branch_name>, --branch=<branch_name>`\n    Specifies the name of the branch to connect to. Defaults to the value of\n    the :gelenv:`BRANCH` environment variable. If that variable isn't set,\n    local instances will default to the most recently switched branch or the\n    |main| branch, while remote instances will default to the name provided\n    when the link was created. This also includes |Gel| Cloud instance links\n    created via :ref:`ref_cli_gel_project_init`.\n\n    .. note::\n        Prior to |EdgeDB| 5, branches were called databases.\n\n        The name of the database could be specified using either:\n\n        * flags ``-d <dbname>, --database=<dbname>``,\n        * or the :gelenv:`DATABASE` environment variable.\n\n        Gel supports those options for backwards compatibility.\n\n\n:cli:synopsis:`--password | --no-password`\n    If :cli:synopsis:`--password` is specified, force |gelcmd| to prompt\n    for a password before connecting to the database. This is usually not\n    necessary, since |gelcmd| will prompt for a password automatically\n    if the server requires it.\n\n    Specifying :cli:synopsis:`--no-password` disables all password prompts.\n\n:cli:synopsis:`--password-from-stdin`\n    Use the first line of standard input as the password.\n\n:cli:synopsis:`--tls-ca-file /path/to/cert`\n    Certificate to match server against.\n\n    This might either be full self-signed server certificate or\n    certificate authority (CA) certificate that server certificate is\n    signed with.\n\n:cli:synopsis:`--tls-security mode`\n    Set the TLS security mode.\n\n    ``default``\n        Resolves to ``strict`` if no custom certificate is supplied via\n        :cli:synopsis:`--tls-ca-file`, environment variable, etc. Otherwise,\n        resolves to ``no_host_verification``.\n\n    ``strict``\n        Verify TLS certificate and hostname.\n\n    ``no_host_verification``\n        This allows using any certificate for any hostname. However,\n        certificate must be present and match the root certificate specified\n        with  :cli:synopsis:`--tls-ca-file`, credentials file, or system root\n        certificates.\n\n    ``insecure``\n        Disable all TLS security measures.\n\n:cli:synopsis:`--secret-key <key>`\n    Specifies the secret key to use for authentication with |Gel| Cloud\n    instances. This is not required when connecting to your own Gel Cloud\n    instance if you have logged in with :ref:`ref_cli_gel_cloud_login`.\n\n:cli:synopsis:`--wait-until-available=<wait_time>`\n    In case |Gel| connection can't be established, keep retrying up\n    to :cli:synopsis:`<wait_time>` (e.g. ``30s``). The\n    :cli:synopsis:`<timeout>` value must be given using time units (e.g.\n    ``hr``, ``min``, ``sec``, ``ms``, etc.).\n\n:cli:synopsis:`--connect-timeout=<timeout>`\n    Specifies a :cli:synopsis:`<timeout>` period. In the event |Gel| doesn't\n    respond in this period, the command will fail (or retry if\n    :cli:synopsis:`--wait-until-available` is also specified). The\n    :cli:synopsis:`<timeout>` value must be given using time units (e.g.\n    ``hr``, ``min``, ``sec``, ``ms``, etc.). The default value is ``10s``.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_database/gel_database_create.rst",
    "content": ".. _ref_cli_gel_database_create:\n\n\n===================\ngel database create\n===================\n\n.. warning::\n\n    This command is deprecated in |Gel|.\n    Use :ref:`ref_cli_gel_branch_create` instead.\n\nCreate a new :ref:`database <ref_datamodel_databases>`.\n\n.. cli:synopsis::\n\n    gel database create [<options>] <name>\n\n.. note::\n\n    |EdgeDB| 5.0 introduced :ref:`branches <ref_datamodel_branches>` to\n    replace databases. This command works on instances running versions\n    prior to |EdgeDB| 5.0. If you are running a newer version of\n    Gel, you will instead use :ref:`ref_cli_gel_branch_create`.\n\n\nDescription\n===========\n\n:gelcmd:`database create` is a terminal command equivalent to\n:eql:stmt:`create database`.\n\n\nOptions\n=======\n\nThe ``database create`` command runs in the |Gel| instance it is\nconnected to. For specifying the connection target see\n:ref:`connection options <ref_cli_gel_connopts>`.\n\n:cli:synopsis:`<name>`\n    The name of the new database.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_database/gel_database_drop.rst",
    "content": ".. _ref_cli_gel_database_drop:\n\n\n=================\ngel database drop\n=================\n\n.. warning::\n\n    This command is deprecated in |Gel|.\n    Use :ref:`ref_cli_gel_branch_drop` instead.\n\nDrop a :ref:`database <ref_datamodel_databases>`.\n\n.. cli:synopsis::\n\n    gel database drop [<options>] <name>\n\n.. note::\n\n    |EdgeDB| 5.0 introduced :ref:`branches <ref_datamodel_branches>` to\n    replace databases. This command works on instances running versions\n    prior to |EdgeDB| 5.0. If you are running a newer version of\n    Gel, you will instead use :ref:`ref_cli_gel_branch_drop`.\n\n\nDescription\n===========\n\n:gelcmd:`database drop` is a terminal command equivalent to\n:eql:stmt:`drop database`.\n\n\nOptions\n=======\n\nThe ``database drop`` command runs in the Gel instance it is\nconnected to. For specifying the connection target see\n:ref:`connection options <ref_cli_gel_connopts>`.\n\n:cli:synopsis:`<name>`\n    The name of the database to drop.\n:cli:synopsis:`--non-interactive`\n    Drop the database without asking for confirmation.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_database/gel_database_wipe.rst",
    "content": ".. _ref_cli_gel_database_wipe:\n\n\n=================\ngel database wipe\n=================\n\n.. warning::\n\n    This command is deprecated in |Gel|.\n    Use :ref:`ref_cli_gel_branch_wipe` instead.\n\nDestroy the contents of a :ref:`database <ref_datamodel_databases>`\n\n.. cli:synopsis::\n\n    gel database wipe [<options>]\n\n.. note::\n\n    |EdgeDB| 5.0 introduced :ref:`branches <ref_datamodel_branches>` to\n    replace databases. This command works on instances running versions\n    prior to |EdgeDB| 5.0. If you are running a newer version of\n    |EdgeDB| or Gel, you will instead use :ref:`ref_cli_gel_branch_wipe`.\n\n\nDescription\n===========\n\n:gelcmd:`database wipe` is a terminal command equivalent to\n:eql:stmt:`reset schema to initial`.\n\nThe database wiped will be one of these values: the value passed for the\n``--database``/``-d`` option, the value of :gelenv:`DATABASE`, or |main|.\nThe contents of the database will be destroyed and the schema reset to its\nstate before any migrations, but the database itself will be preserved.\n\n\nOptions\n=======\n\nThe ``database wipe`` command runs in the |Gel| instance it is\nconnected to. For specifying the connection target see\n:ref:`connection options <ref_cli_gel_connopts>`.\n\n:cli:synopsis:`--non-interactive`\n    Destroy the data without asking for confirmation.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_database/index.rst",
    "content": ".. _ref_cli_gel_database:\n\n\n============\ngel database\n============\n\nThe :gelcmd:`database` group of commands contains various database\nmanipulation tools.\n\n.. note::\n\n    |EdgeDB| 5.0 introduced :ref:`branches <ref_datamodel_branches>` to\n    replace databases. These commands work on instances running versions\n    prior to |EdgeDB| 5.0. If you are running a newer version of\n    Gel, you will instead use the :ref:`ref_cli_gel_branch` suite of\n    commands.\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    gel_database_create\n    gel_database_drop\n    gel_database_wipe\n\n.. list-table::\n    :class: funcoptable\n\n    * - :ref:`ref_cli_gel_database_create`\n      - Create a new database\n    * - :ref:`ref_cli_gel_database_drop`\n      - Drop a database\n    * - :ref:`ref_cli_gel_database_wipe`\n      - Destroy the contents of a database\n"
  },
  {
    "path": "docs/reference/using/cli/gel_describe/gel_describe_object.rst",
    "content": ".. _ref_cli_gel_describe_object:\n\n\n===================\ngel describe object\n===================\n\nDescribe a named schema object.\n\n.. cli:synopsis::\n\n    gel describe object [<options>] <name>\n\n\nDescription\n===========\n\n:gelcmd:`describe` is a terminal command equivalent to\n:eql:stmt:`describe object <describe>` introspection command.\n\n\nOptions\n=======\n\nThe ``describe`` command runs in the database it is connected to. For\nspecifying the connection target see :ref:`connection options\n<ref_cli_gel_connopts>`.\n\n:cli:synopsis:`--verbose`\n    This is equivalent to running :eql:stmt:`describe object ... as\n    text verbose <describe>` command, which enables displaying\n    additional details, such as annotations and constraints, which are\n    otherwise omitted.\n\n:cli:synopsis:`<name>`\n    Name of the schema object to describe.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_describe/gel_describe_schema.rst",
    "content": ".. _ref_cli_gel_describe_schema:\n\n\n===================\ngel describe schema\n===================\n\nGive an :ref:`SDL <ref_eql_sdl>` description of the schema of the\ndatabase specified by the connection options.\n\n.. cli:synopsis::\n\n    gel describe schema [<options>]\n\n\nDescription\n===========\n\n:gelcmd:`describe schema` is a terminal command equivalent to\n:eql:stmt:`describe schema as sdl <describe>` introspection command.\n\n\nOptions\n=======\n\nThe ``describe`` command runs in the database it is connected to. For\nspecifying the connection target see :ref:`connection options\n<ref_cli_gel_connopts>`.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_describe/index.rst",
    "content": ".. _ref_cli_gel_describe:\n\n\n============\ngel describe\n============\n\nThe :gelcmd:`describe` group of commands contains various schema\nintrospection tools.\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    gel_describe_object\n    gel_describe_schema\n\n.. list-table::\n    :class: funcoptable\n\n    * - :ref:`ref_cli_gel_describe_object`\n      - Describe a named schema object\n    * - :ref:`ref_cli_gel_describe_schema`\n      - Describe schema of the current database |branch|\n"
  },
  {
    "path": "docs/reference/using/cli/gel_dump.rst",
    "content": ".. _ref_cli_gel_dump:\n\n\n========\ngel dump\n========\n\nBackup a |Gel| |branch| to a file.\n\n.. cli:synopsis::\n\n    gel dump [<options>] <path>\n\n\nOptions\n=======\n\nThe ``dump`` command creates a backup of the currently active database |branch|.\nFor specifying the connection target see :ref:`connection options\n<ref_cli_gel_connopts>`.\n\n:cli:synopsis:`<path>`\n    The name of the file to backup the database branch into.\n\n:cli:synopsis:`--all`\n    Dump all |branches| and the server configuration using the\n    directory specified by the :cli:synopsis:`<path>`.\n\n:cli:synopsis:`--format=<format>`\n    Choose dump format. For normal dumps this parameter should be\n    omitted. For :cli:synopsis:`--all` only\n    :cli:synopsis:`--format=dir` is required.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_extension/index.rst",
    "content": ".. _ref_cli_gel_extension:\n\n=============\ngel extension\n=============\n\nThe :gelcmd:`extension` command group contains commands for managing standalone extensions.\n\n.. toctree::\n   :maxdepth: 2\n   :hidden:\n\n   list\n   list-available\n   install\n   uninstall\n\nSubcommands\n===========\n\n.. list-table::\n   :class: synopsis\n\n   * - :ref:`ref_cli_gel_extension_list`\n     - List installed extensions.\n   * - :ref:`ref_cli_gel_extension_list_available`\n     - List available extensions.\n   * - :ref:`ref_cli_gel_extension_install`\n     - Install an extension.\n   * - :ref:`ref_cli_gel_extension_uninstall`\n     - Uninstall an extension.\n\nSee :ref:`Connection options <ref_cli_gel_connopts>` for options applicable to all subcommands."
  },
  {
    "path": "docs/reference/using/cli/gel_extension/install.rst",
    "content": ".. _ref_cli_gel_extension_install:\n\n=====================\ngel extension install\n=====================\n\nInstall an extension.\n\n.. cli:synopsis::\n\n  gel extension install <extension> [<options>]\n\nArguments\n=========\n\n:cli:synopsis:`<extension>`\n    The name of the extension to install.\n\nOptions\n=======\n\nSee :ref:`Connection options <ref_cli_gel_connopts>` for global options.\n\nExamples\n========\n\nInstall the ``postgis`` extension:\n\n.. code-block:: bash\n\n  $ gel extension install postgis\n  Found extension package: postgis version 3.4.3+6b82d77\n  00:00:03 [====================] 22.49 MiB/22.49 MiB\n  Extension 'postgis' installed successfully.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_extension/list-available.rst",
    "content": ".. _ref_cli_gel_extension_list_available:\n\n============================\ngel extension list-available\n============================\n\nList available extensions.\n\n.. cli:synopsis::\n\n  gel extension list-available [<options>]\n\nOptions\n=======\n\nSee :ref:`Connection options <ref_cli_gel_connopts>` for global options.\n\nExamples\n========\n\n.. code-block:: bash\n\n  $ gel extension list-available\n  ┌─────────┬───────────────┐\n  │ Name    │ Version       │\n  │ postgis │ 3.4.3+6b82d77 │\n  └─────────┴───────────────┘\n"
  },
  {
    "path": "docs/reference/using/cli/gel_extension/list.rst",
    "content": ".. _ref_cli_gel_extension_list:\n\n==================\ngel extension list\n==================\n\nList installed extensions.\n\n.. cli:synopsis::\n\n  gel extension list [<options>]\n\nOptions\n=======\n\nSee :ref:`Connection options <ref_cli_gel_connopts>` for global options.\n\nExamples\n========\n\n.. code-block:: bash\n\n  $ gel extension list\n  ┌─────────┬───────────────┐\n  │ Name    │ Version       │\n  │ postgis │ 3.4.3+6b82d77 │\n  └─────────┴───────────────┘\n"
  },
  {
    "path": "docs/reference/using/cli/gel_extension/uninstall.rst",
    "content": ".. _ref_cli_gel_extension_uninstall:\n\n=======================\ngel extension uninstall\n=======================\n\nUninstall an extension.\n\n.. cli:synopsis::\n\n  gel extension uninstall <extension> [<options>]\n\nArguments\n=========\n\n:cli:synopsis:`<extension>`\n    The name of the extension to uninstall.\n\nOptions\n=======\n\nSee :ref:`Connection options <ref_cli_gel_connopts>` for global options.\n\nExamples\n========\n\nUninstall the ``postgis`` extension:\n\n.. code-block:: bash\n\n  $ gel extension uninstall postgis\n  Extension 'postgis' uninstalled successfully.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_info.rst",
    "content": ".. _ref_cli_gel_info:\n\n\n========\ngel info\n========\n\nDisplay information about the |Gel| installation. Currently this command\ndisplays the filesystem paths used by Gel.\n\n.. cli:synopsis::\n\n\tgel info [<options>]\n\n\n.. _ref_cli_gel_paths:\n\nPaths\n-----\n\n|Gel| uses several directories, each storing different kinds of information.\nThe exact path to these directories is determined by your operating system.\nThroughout the documentation, these paths are referred to as \"Gel config\ndirectory\", \"Gel data directory\", etc.\n\n- **Config**: contains auto-generated credentials for all local instances and\n  project metadata.\n- **Data**: contains the *contents* of all local Gel instances.\n- **CLI Binary**: contains the CLI binary, if installed.\n- **Service**: the home for running processes/daemons.\n- **Cache**: a catchall for logs and various caches.\n\nOptions\n=======\n\n:cli:synopsis:`--get <path-name>`\n    Return only a single path. ``<path-name>`` can be any of ``config-dir``,\n    ``cache-dir``, ``data-dir``, or ``service-dir``.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_init.rst",
    "content": ".. _ref_cli_gel_init:\n\n========\ngel init\n========\n\n.. code-block:: bash\n\n  $ gel init\n\nThis is an alias for :ref:`gel project init <ref_cli_gel_project_init>`, and takes all of the same arguments."
  },
  {
    "path": "docs/reference/using/cli/gel_instance/gel_instance_create.rst",
    "content": ".. _ref_cli_gel_instance_create:\n\n\n===================\ngel instance create\n===================\n\nInitialize a new |Gel| instance.\n\n.. cli:synopsis::\n\n     gel instance create [<options>] [<name>] [<default-branch-or-database>]\n\n\nDescription\n===========\n\n:gelcmd:`instance create` is a terminal command for making a new Gel\ninstance and creating a corresponding credentials file in\n``<gel_config_dir>/credentials``. Run :gelcmd:`info` to see the path to\n``<gel_config_dir>`` on your machine.\n\n.. note::\n\n    The :gelcmd:`instance create` command is not intended for use with\n    self-hosted instances. You can follow one of our :ref:`deployment guides\n    <ref_guide_deployment>` for information on how to create one of these\n    instances.\n\n\nGel Cloud\n---------\n\nGel Cloud users may use this command to create a Cloud instance after\nlogging in using :ref:`ref_cli_gel_cloud_login`.\n\nTo create a Cloud instance, your instance name should be in the format\n``<org-name>/<instance-name>``. Cloud instance names may contain alphanumeric\ncharacters and hyphens (i.e., ``-``).\n\n.. note::\n\n    Please be aware of the following restrictions on |Gel| Cloud instance\n    names:\n\n    * can contain only Latin alpha-numeric characters or ``-``\n    * cannot start with a dash (``-``) or contain double dashes (``--``)\n    * maximum instance name length is 61 characters minus the length of your\n      organization name (i.e., length of organization name + length of instance\n      name must be fewer than 62 characters)\n\n\nOptions\n=======\n\n:cli:synopsis:`<name>`\n    The new |Gel| instance name. Asked interactively if not specified.\n\n:cli:synopsis:`<branch-or-database-name>`\n    The default |branch| name on the new instance. Defaults\n    to |main| or, when creating a pre-v5 instance, ``edgedb``.\n\n:cli:synopsis:`--nightly`\n    Use the nightly server for this instance.\n\n:cli:synopsis:`--default-user=<default-user>`\n    Specifies the default user name (created during initialization,\n    and saved in credentials file). Defaults to: ``admin``,\n    or, when creating a pre-v6 instance, ``edgedb``.\n\n:cli:synopsis:`--port=<port>`\n    Specifies which port should the instance be configured on. By\n    default a random port will be used and recorded in the credentials\n    file.\n\n:cli:synopsis:`--start-conf=<start-conf>`\n    Configures how the new instance should start: ``auto`` for\n    automatic start with the system or user session, ``manual`` to\n    turn that off so that the instance can be manually started with\n    :ref:`ref_cli_gel_instance_start` on demand. Defaults to:\n    ``auto``.\n\n:cli:synopsis:`--channel=<channel>`\n    Indicate the channel of the new instance. Possible values are ``stable``,\n    ``testing``, or ``nightly``.\n\n:cli:synopsis:`--version=<version>`\n    Specifies the version of the |Gel| server to be used to run the\n    new instance. To list the currently available options use\n    :ref:`ref_cli_gel_server_list_versions`.\n\n    By default, when you specify a version, the CLI will use the latest release\n    in the major version specified. This command, for example, will install the\n    latest X.Y release:\n\n    .. code-block:: bash\n\n        $ gel instance create --version X.0 demoxy\n\n    You may pin to a specific version by prepending the version number with an\n    equals sign. This command will install version X.Y:\n\n    .. code-block:: bash\n\n        $ gel instance create --version =X.Y demoxy\n\n    .. note::\n\n        Some shells like ZSH may require you to escape the equals sign (e.g.,\n        ``\\=X.Y``) or quote the version string (e.g., ``\"=X.Y\"``).\n\nGel Cloud options\n-----------------\n\n:cli:synopsis:`--region=<region>`\n    The region in which to create the instance (for |Gel| Cloud instances).\n    Possible values are ``aws-us-west-2``, ``aws-us-east-2``, and\n    ``aws-eu-west-1``.\n\n:cli:synopsis:`--tier=<tier>`\n    Cloud instance subscription tier for the new instance. Possible values are\n    ``pro`` and ``free``.\n\n:cli:synopsis:`--compute-size=<number>`\n    The size of compute to be allocated for the Gel Cloud instance (in\n    Compute Units)\n\n:cli:synopsis:`--storage-size=<GiB>`\n    The size of storage to be allocated for the Cloud instance (in Gigabytes)\n"
  },
  {
    "path": "docs/reference/using/cli/gel_instance/gel_instance_credentials.rst",
    "content": ".. _ref_cli_gel_instance_credentials:\n\n\n========================\ngel instance credentials\n========================\n\nDisplay instance credentials.\n\n.. cli:synopsis::\n\n     gel instance credentials [options] [connection-options]\n\n\nDescription\n===========\n\n:gelcmd:`instance credentials` is a terminal command for displaying the\ncredentials of a |Gel| instance.\n\n\nOptions\n=======\n\n:cli:synopsis:`--json`\n    Output in JSON format. In addition to formatting the credentials as JSON,\n    this option also includes the password in cleartext and the TLS\n    certificates.\n\n:cli:synopsis:`--insecure-dsn`\n    Output a DSN with password in cleartext.\n\nConnection Options\n==================\n\nBy default, the |gel.toml| connection is used.\n\n:cli:synopsis:`<connection-options>`\n    See :ref:`connection options <ref_cli_gel_connopts>`.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_instance/gel_instance_destroy.rst",
    "content": ".. _ref_cli_gel_instance_destroy:\n\n\n====================\ngel instance destroy\n====================\n\nRemove a |Gel| instance.\n\n.. cli:synopsis::\n\n     gel instance destroy [<options>] <name>\n\n\nDescription\n===========\n\n:gelcmd:`instance destroy` is a terminal command for removing an (or |gel.toml|)\ninstance and all its data.\n\n.. note::\n\n    The :gelcmd:`instance destroy` command is not intended for use with\n    self-hosted instances.\n\n\nOptions\n=======\n\n:cli:synopsis:`<name>`\n    The |Gel| instance name.\n\n:cli:synopsis:`--force`\n    Destroy the instance even if it is referred to by a project.\n\n:cli:synopsis:`-v, --verbose`\n    Verbose output.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_instance/gel_instance_link.rst",
    "content": ".. _ref_cli_gel_instance_link:\n\n=================\ngel instance link\n=================\n\nAuthenticate a connection to a remote |Gel| instance and assign an\ninstance name to simplify future connections.\n\n.. cli:synopsis::\n\n    gel instance link [<options>] <name>\n\n\nDescription\n===========\n\n:gelcmd:`instance link` is a terminal command used to bind a set of\nconnection credentials to an instance name. This is typically used as\na way to simplify connecting to remote |Gel| database instances.\nUsually there's no need to do this for local instances as\n:ref:`ref_cli_gel_project_init` will already set up a named\ninstance.\n\n.. note::\n\n    Unlike other :gelcmd:`instance` sub-commands, :gelcmd:`instance link` is\n    recommended to link self-hosted instances. This can make other operations\n    like migrations, dumps, and restores more convenient.\n\n    Linking is not required for |Gel| Cloud instances. They can always be\n    accessed via CLI using ``<org-name>/<instance-name>``.\n\nOptions\n=======\n\nThe ``instance link`` command uses the standard :ref:`connection\noptions <ref_cli_gel_connopts>` for specifying the instance to be\nlinked.\n\n:cli:synopsis:`<name>`\n    Specifies a new instance name to associate with the connection\n    options. If not present, the interactive mode will ask for the\n    name.\n\n:cli:synopsis:`--non-interactive`\n    Run in non-interactive mode (accepting all defaults).\n\n:cli:synopsis:`--quiet`\n    Reduce command verbosity.\n\n:cli:synopsis:`--trust-tls-cert`\n    Trust peer certificate.\n\n:cli:synopsis:`--overwrite`\n    Overwrite existing credential file if any.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_instance/gel_instance_list.rst",
    "content": ".. _ref_cli_gel_instance_list:\n\n\n=================\ngel instance list\n=================\n\nShow all |Gel| instances.\n\n.. cli:synopsis::\n\n    gel instance list [<options>]\n\n\nDescription\n===========\n\n:gelcmd:`instance list` is a terminal command that shows all the\nregistered |Gel| instances and some relevant information about them\n(status, port, etc.).\n\n\nOptions\n=======\n\n:cli:synopsis:`--extended`\n    Output more debug info about each instance.\n\n:cli:synopsis:`-j, --json`\n    Output in JSON format.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_instance/gel_instance_logs.rst",
    "content": ".. _ref_cli_gel_instance_logs:\n\n\n=================\ngel instance logs\n=================\n\nShow instance logs.\n\n.. cli:synopsis::\n\n     gel instance logs [<options>] <name>\n\n\nDescription\n===========\n\n:gelcmd:`instance logs` is a terminal command for displaying the logs\nfor a given |Gel| instance.\n\n.. note::\n\n    The :gelcmd:`instance logs` command is not intended for use with\n    self-hosted instances.\n\n\nOptions\n=======\n\n:cli:synopsis:`<name>`\n    The name of the |Gel| instance.\n\n:cli:synopsis:`-n, --tail=<tail>`\n    Number of the most recent lines to show.\n\n:cli:synopsis:`-f, --follow`\n    Show log's tail and the continue watching for the new entries.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_instance/gel_instance_reset_password.rst",
    "content": ".. _ref_cli_gel_instance_reset_auth:\n\n\n===========================\ngel instance reset-password\n===========================\n\nReset password for a user in the |Gel| instance.\n\n.. cli:synopsis::\n\n     gel instance reset-password [<options>] <name>\n\n\nDescription\n===========\n\n:gelcmd:`instance reset-password` is a terminal command for resetting\nor updating the password for a user of a |Gel| instance.\n\n.. note::\n\n    The :gelcmd:`instance reset-password` command is not intended for use with\n    self-hosted instances.\n\n\nOptions\n=======\n\n:cli:synopsis:`<name>`\n    The name of the |Gel| instance.\n\n:cli:synopsis:`--user=<user>`\n    User to change password for. Defaults to the user in the\n    credentials file.\n\n:cli:synopsis:`--password`\n    Read the password from the terminal rather than generating a new one.\n\n:cli:synopsis:`--password-from-stdin`\n    Read the password from stdin rather than generating a new one.\n\n:cli:synopsis:`--save-credentials`\n    Save new user and password into a credentials file. By default\n    credentials file is updated only if user name matches.\n\n:cli:synopsis:`--no-save-credentials`\n    Do not save generated password into a credentials file even if\n    user name matches.\n\n:cli:synopsis:`--quiet`\n    Do not print any messages, only indicate success by exit status.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_instance/gel_instance_restart.rst",
    "content": ".. _ref_cli_gel_instance_restart:\n\n\n====================\ngel instance restart\n====================\n\nRestart a |Gel| instance.\n\n.. cli:synopsis::\n\n     gel instance restart <name>\n\n\nDescription\n===========\n\n:gelcmd:`instance restart` is a terminal command for restarting an\n|Gel| instance.\n\n.. note::\n\n    The :gelcmd:`instance restart` command is not intended for use with\n    self-hosted instances.\n\n\nOptions\n=======\n\n:cli:synopsis:`<name>`\n    The |Gel| instance name.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_instance/gel_instance_revert.rst",
    "content": ".. _ref_cli_gel_instance_revert:\n\n\n===================\ngel instance revert\n===================\n\nRevert a major instance upgrade.\n\n.. cli:synopsis::\n\n     gel instance revert [<options>] <name>\n\n\nDescription\n===========\n\nWhen :ref:`ref_cli_gel_instance_upgrade` performs a major version\nupgrade on an instance the old instance data is kept around. The\n:gelcmd:`instance revert` command removes the new instance version and\nreplaces it with the old copy. It also ensures that the previous\nversion of |Gel| server is used to run it.\n\n.. note::\n\n    The :gelcmd:`instance revert` command is not intended for use with\n    self-hosted instances.\n\n\nOptions\n=======\n\n:cli:synopsis:`<name>`\n    The name of the |Gel| instance to revert.\n\n:cli:synopsis:`--ignore-pid-check`\n    Do not check if upgrade is in progress.\n\n:cli:synopsis:`-y, --no-confirm`\n    Do not ask for a confirmation.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_instance/gel_instance_start.rst",
    "content": ".. _ref_cli_gel_instance_start:\n\n\n==================\ngel instance start\n==================\n\nStart a |Gel| instance.\n\n.. cli:synopsis::\n\n     gel instance start [--foreground] <name>\n\n\nDescription\n===========\n\n:gelcmd:`instance start` is a terminal command for starting a new\n|Gel| instance.\n\n.. note::\n\n    The :gelcmd:`instance start` command is not intended for use with\n    self-hosted instances.\n\n\nOptions\n=======\n\n:cli:synopsis:`<name>`\n    The |Gel| instance name.\n\n:cli:synopsis:`--foreground`\n    Start the instance in the foreground rather than using systemd to\n    manage the process (note you might need to stop non-foreground\n    instance first).\n"
  },
  {
    "path": "docs/reference/using/cli/gel_instance/gel_instance_status.rst",
    "content": ".. _ref_cli_gel_instance_status:\n\n\n===================\ngel instance status\n===================\n\nShow instance information.\n\n.. cli:synopsis::\n\n     gel instance status [<options>] [<name>]\n\n\nDescription\n===========\n\n:gelcmd:`instance status` is a terminal command for displaying the\ninformation about |Gel| instances.\n\n\nOptions\n=======\n\n:cli:synopsis:`<name>`\n    Show only the status of the specific |Gel| instance.\n\n:cli:synopsis:`--json`\n    Format output as JSON.\n\n:cli:synopsis:`--extended`\n    Output more debug info about each instance.\n\n:cli:synopsis:`--service`\n    Show current systems service information.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_instance/gel_instance_stop.rst",
    "content": ".. _ref_cli_gel_instance_stop:\n\n\n=================\ngel instance stop\n=================\n\nStop a |Gel| instance.\n\n.. cli:synopsis::\n\n     gel instance stop <name>\n\n\nDescription\n===========\n\n:gelcmd:`instance stop` is a terminal command for stopping a running\n|Gel| instance. This is a necessary step before\n:ref:`destroying <ref_cli_gel_instance_destroy>` an instance.\n\n.. note::\n\n    The :gelcmd:`instance stop` command is not intended for use with\n    self-hosted instances.\n\n\nOptions\n=======\n\n:cli:synopsis:`<name>`\n    The |Gel| instance name.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_instance/gel_instance_unlink.rst",
    "content": ".. _ref_cli_gel_instance_unlink:\n\n===================\ngel instance unlink\n===================\n\nUnlink from a previously linked remote Gel instance.\n\n.. cli:synopsis::\n\n    gel instance unlink <name>\n\n\nDescription\n===========\n\n:gelcmd:`instance unlink` is a terminal command used to unlink a\nremote instance. This removes the instance name from the list of valid\ninstances.\n\n\nOptions\n=======\n\n:cli:synopsis:`<name>`\n    Specifies the name of the remote instance to be unlinked.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_instance/gel_instance_upgrade.rst",
    "content": ".. _ref_cli_gel_instance_upgrade:\n\n\n====================\ngel instance upgrade\n====================\n\nUpgrade |Gel| instance or installation.\n\n.. cli:synopsis::\n\n    gel instance upgrade [<options>] [<name>]\n\n\nDescription\n===========\n\nThis command is used to upgrade |Gel| instances individually or in\nbulk.\n\n.. note::\n\n    The :gelcmd:`instance upgrade` command is not intended for use with\n    self-hosted instances.\n\n\nOptions\n=======\n\n:cli:synopsis:`<name>`\n    The |Gel| instance name to upgrade.\n\n:cli:synopsis:`--force`\n    Force upgrade process even if there is no new version.\n\n:cli:synopsis:`--to-latest`\n    Upgrade specified instance to the latest major version.\n\n:cli:synopsis:`--to-nightly`\n    Upgrade specified instance to a latest nightly version.\n\n:cli:synopsis:`--local-minor`\n    Upgrade all local instances to the latest minor versions.\n\n:cli:synopsis:`--to-version=<version>`\n    Upgrade to a specified major version.\n\n:cli:synopsis:`-v, --verbose`\n    Verbose output.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_instance/index.rst",
    "content": ".. _ref_cli_gel_instance:\n\n============\ngel instance\n============\n\nThe :gelcmd:`instance` group of commands contains all sorts of tools\nfor managing |Gel| instances.\n\n.. note::\n\n    Most commands in the :gelcmd:`instance` command group are not intended to\n    manage self-hosted instances. See individual commands for more details.\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    gel_instance_create\n    gel_instance_credentials\n    gel_instance_destroy\n    gel_instance_link\n    gel_instance_list\n    gel_instance_logs\n    gel_instance_start\n    gel_instance_status\n    gel_instance_stop\n    gel_instance_reset_password\n    gel_instance_restart\n    gel_instance_revert\n    gel_instance_unlink\n    gel_instance_upgrade\n\n.. list-table::\n    :class: funcoptable\n\n    * - :ref:`ref_cli_gel_instance_create`\n      - Initialize a new server instance\n    * - :ref:`ref_cli_gel_instance_credentials`\n      - Display instance credentials\n    * - :ref:`ref_cli_gel_instance_destroy`\n      - Destroy a server instance and remove the data stored\n    * - :ref:`ref_cli_gel_instance_link`\n      - Link a remote instance\n    * - :ref:`ref_cli_gel_instance_list`\n      - Show all instances\n    * - :ref:`ref_cli_gel_instance_logs`\n      - Show logs of an instance\n    * - :ref:`ref_cli_gel_instance_start`\n      - Start an instance\n    * - :ref:`ref_cli_gel_instance_status`\n      - Show statuses of all or of a matching instance\n    * - :ref:`ref_cli_gel_instance_stop`\n      - Stop an instance\n    * - :ref:`ref_cli_gel_instance_reset_auth`\n      - Reset password for a user in the instance\n    * - :ref:`ref_cli_gel_instance_restart`\n      - Restart an instance\n    * - :ref:`ref_cli_gel_instance_revert`\n      - Revert a major instance upgrade\n    * - :ref:`ref_cli_gel_instance_unlink`\n      - Unlink a remote instance\n    * - :ref:`ref_cli_gel_instance_upgrade`\n      - Upgrade installations and instances\n"
  },
  {
    "path": "docs/reference/using/cli/gel_list.rst",
    "content": ".. _ref_cli_gel_list:\n\n\n========\ngel list\n========\n\nList matching database objects by name and type.\n\n.. cli:synopsis::\n\n    gel list <type> [<options>] <pattern>\n\n\nDescription\n===========\n\nThe :gelcmd:`list` group of commands contains tools for listing\ndatabase objects by matching name or type. The sub-commands are\norganized by the type of the objects listed.\n\nTypes\n=====\n\n:cli:synopsis:`gel list aliases`\n    Display list of aliases defined in the schema.\n\n:cli:synopsis:`gel list casts`\n    Display list of casts defined in the schema.\n\n:cli:synopsis:`gel list branches`\n    Display list of branches.\n\n:cli:synopsis:`gel list indexes`\n    Display list of indexes defined in the schema.\n\n:cli:synopsis:`gel list modules`\n    Display list of modules defined in the schema.\n\n:cli:synopsis:`gel list roles`\n    Display list of roles in the server instance.\n\n:cli:synopsis:`gel list scalars`\n    Display list of scalar types defined in the schema.\n\n:cli:synopsis:`gel list types`\n    Display list of object types defined in the schema.\n\nOptions\n=======\n\nThe ``list`` command runs in the |branch| it is connected to. For\nspecifying the connection target see :ref:`connection options\n<ref_cli_gel_connopts>`.\n\n:cli:synopsis:`-c, --case-sensitive`\n    Indicates that the pattern should be treated in a case-sensitive\n    manner.\n\n:cli:synopsis:`-s, --system`\n    Indicates that built-in and objects should be included in the list.\n\n:cli:synopsis:`-v, --verbose`\n    Include more details in the output.\n\n:cli:synopsis:`<pattern>`\n    The pattern that the name should match. If omitted all objects of\n    a particular type will be listed.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_migrate.rst",
    "content": ".. _ref_cli_gel_migrate:\n\n===========\ngel migrate\n===========\n\nThis command is an alias for :ref:`ref_cli_gel_migration_apply`.\nOnce the migration scripts are in place, the changes can be applied to the\ndatabase using this command.\n\n.. warning:: Gel Cloud CI users and scripters\n\n    When scripting a ``migrate``/``migration apply`` for a |Gel| Cloud\n    instance, do not use :gelcmd:`login` to authenticate. Instead, you should\n    generate a secret key in the Gel Cloud UI or by running\n    :ref:`ref_cli_gel_cloud_secretkey_create` and set the\n    :gelenv:`SECRET_KEY` environment variable to your secret key. Once this\n    variable is set to your secret key, logging in is no longer required.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_migration/gel_migration_apply.rst",
    "content": ".. _ref_cli_gel_migration_apply:\n\n\n===================\ngel migration apply\n===================\n\nOnce the migration scripts are in place the changes can be applied to\nthe database by this command:\n\n.. cli:synopsis::\n\n    gel migration apply [<options>]\n\nThe tool will find all the unapplied migrations in\n``dbschema/migrations/`` directory and sequentially run them on the\ntarget instance.\n\n.. warning:: Gel Cloud CI users and scripters\n\n    When scripting a ``migrate``/``migration apply`` for a |Gel| Cloud\n    instance, do not use :gelcmd:`login` to authenticate. Instead, you should\n    generate a secret key in the Gel Cloud UI or by running\n    :ref:`ref_cli_gel_cloud_secretkey_create` and set the\n    :gelenv:`SECRET_KEY` environment variable to your secret key. Once this\n    variable is set to your secret key, logging in is no longer required.\n\nOptions\n=======\n\nThe ``migration apply`` command runs on the database it is connected\nto. For specifying the connection target see :ref:`connection options\n<ref_cli_gel_connopts>`.\n\n:cli:synopsis:`--quiet`\n    Do not print any messages, only indicate success by exit status.\n\n:cli:synopsis:`--schema-dir=<schema-dir>`\n    Directory where the schema files are located. Defaults to\n    ``./dbschema``.\n\n:cli:synopsis:`--to-revision=<to-revision>`\n    Upgrade to a specified revision.\n\n    Unique prefix of the revision can be specified instead of full\n    revision name.\n\n    If this revision is applied, the command is no-op. The command\n    ensures that this revision present, but it's not an error if more\n    revisions are applied on top.\n\n:cli:synopsis:`--dev-mode`\n    Apply the current schema changes on top of the current migration history,\n    without having created a new migration. This works the same way as\n    :ref:`gel watch --migrate <ref_cli_gel_watch>` but without starting a\n    long-running watch task.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_migration/gel_migration_create.rst",
    "content": ".. _ref_cli_gel_migration_create:\n\n\n====================\ngel migration create\n====================\n\nThe next step after setting up the desired target schema is creating a\nmigration script. This is done by invoking the following command:\n\n.. cli:synopsis::\n\n    gel migration create [<options>]\n\nThis will start an interactive tool that will provide the user with\nsuggestions based on the differences between the current |branch|\nand the schema file. The prompts will look something like this:\n\n.. code-block::\n\n    did you create object type 'default::User'? [y,n,l,c,b,s,q,?]\n    ?\n\n    y - confirm the prompt, use the DDL statements\n    n - reject the prompt\n    l - list the DDL statements associated with prompt\n    c - list already confirmed EdgeQL statements\n    b - revert back to previous save point, perhaps previous question\n    s - stop and save changes (splits migration into multiple)\n    q - quit without saving changes\n    h or ? - print help\n\nOptions\n=======\n\nThe ``migration create`` command runs on the database it is connected\nto. For specifying the connection target see :ref:`connection options\n<ref_cli_gel_connopts>`.\n\n:cli:synopsis:`--non-interactive`\n    Do not prompt user for input. By default this works only if all\n    the changes are \"safe\" unless :cli:synopsis:`--allow-unsafe` is\n    also specified.\n\n:cli:synopsis:`--allow-unsafe`\n    Apply the most probable unsafe changes in case there are any.\n    This is only useful in non-interactive mode.\n\n:cli:synopsis:`--allow-empty`\n    Create a new migration even if there are no changes. This is\n    useful for creating migration stubs for data-only migrations.\n\n:cli:synopsis:`--schema-dir=<schema-dir>`\n    Directory where the schema files are located. Defaults to\n    ``./dbschema``.\n\n:cli:synopsis:`--squash`\n    Squashes all your migrations into a single migration.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_migration/gel_migration_edit.rst",
    "content": ".. _ref_cli_gel_migration_edit:\n\n\n==================\ngel migration edit\n==================\n\nEdit migration file.\n\n.. cli:synopsis::\n\n    gel migration edit [<options>]\n\nInvokes ``$EDITOR`` on the last migration file, and then fixes migration id\nafter editor exits. Usually should be used for migrations that haven't been\napplied yet.\n\nOptions\n=======\n\nThe ``migration edit`` command runs on the database it is connected to. For\nspecifying the connection target see :ref:`connection options\n<ref_cli_gel_connopts>`.\n\n:cli:synopsis:`--schema-dir=<schema-dir>`\n    Directory where the schema files are located. Defaults to\n    ``./dbschema``.\n\n:cli:synopsis:`--no-check`\n    Do not check migration within the database connection.\n\n:cli:synopsis:`--non-interactive`\n    Fix migration id non-interactively, and don't run editor.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_migration/gel_migration_extract.rst",
    "content": ".. _ref_cli_gel_migration_extract:\n\n\n=====================\ngel migration extract\n=====================\n\nExtract migration history from the database and write it to\n``/dbschema/migrations``. Useful when a direct DDL command has been used to\nchange the schema and now :gelcmd:`migrate` will not comply because the\ndatabase migration history is ahead of the migration history inside\n``/dbschema/migrations``.\n\nThis can also be useful if the migrations on the file system have been lost or\ndeleted.\n\nOptions\n=======\n\nThe ``migration extract`` command runs on the database it is connected\nto. For specifying the connection target see :ref:`connection options\n<ref_cli_gel_connopts>`.\n\n\n:cli:synopsis:`--tls-server-name <TLS_SERVER_NAME>`\n    Override server name used for TLS connections and certificate verification.\n\n    Useful when the server hostname cannot be used as it does not resolve, or\n    resolves to a wrong IP address, and a different name or IP address is used\n    in ``--host``.\n\n:cli:synopsis:`--non-interactive`\n    Don't ask questions, only add missing files, abort if mismatching\n\n:cli:synopsis`--force`\n    Force overwrite existing migration files\n\n:cli:synopsis:`--schema-dir=<schema-dir>`\n    Directory where the schema files are located. Defaults to\n    ``./dbschema``.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_migration/gel_migration_log.rst",
    "content": ".. _ref_cli_gel_migration_log:\n\n\n=================\ngel migration log\n=================\n\nShow all migration versions.\n\n.. cli:synopsis::\n\n    gel migration log [<options>]\n\nThe tool will display the migration history either by reading it from\nthe |Gel| instance or from the schema directory.\n\nOptions\n=======\n\nThe ``migration log`` command runs on the database it is connected\nto. For specifying the connection target see :ref:`connection options\n<ref_cli_gel_connopts>`.\n\n:cli:synopsis:`--from-fs`\n    Print revisions from the schema directory (no database connection\n    required). At least one of :cli:synopsis:`--from-db` or\n    :cli:synopsis:`--from-fs` is required for ``migration log``\n    command.\n\n:cli:synopsis:`--from-db`\n    Print revisions from the database (no schema files required). At\n    least one of :cli:synopsis:`--from-db` or\n    :cli:synopsis:`--from-fs` is required for ``migration log``\n    command.\n\n:cli:synopsis:`--newest-first`\n    Sort migrations starting from newer to older, by default older\n    revisions go first.\n\n:cli:synopsis:`--schema-dir=<schema-dir>`\n    Directory where the schema files are located. Defaults to\n    ``./dbschema``.\n\n:cli:synopsis:`--limit=<N>`\n    Show maximum of :cli:synopsis:`N` revisions (default is unlimited).\n"
  },
  {
    "path": "docs/reference/using/cli/gel_migration/gel_migration_status.rst",
    "content": ".. _ref_cli_gel_migration_status:\n\n\n====================\ngel migration status\n====================\n\nShow current migration state.\n\n.. cli:synopsis::\n\n    gel migration status [<options>]\n\nThe tool will show how the state of the schema in the |Gel| instance\ncompares to the migrations stored in the schema directory.\n\nOptions\n=======\n\nThe ``migration status`` command runs on the database it is connected\nto. For specifying the connection target see :ref:`connection options\n<ref_cli_gel_connopts>`.\n\n:cli:synopsis:`--quiet`\n    Do not print any messages, only indicate success by exit status.\n\n:cli:synopsis:`--schema-dir=<schema-dir>`\n    Directory where the schema files are located. Defaults to\n    ``./dbschema``.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_migration/gel_migration_upgrade_check.rst",
    "content": ".. _ref_cli_gel_migration_upgrade_check:\n\n\n===========================\ngel migration upgrade-check\n===========================\n\nChecks your schema against a different |Gel| version.\n\n.. cli:synopsis::\n\n    gel migration upgrade-check [<options>]\n\n.. note::\n\n    The upgrade check is performed automatically when you perform an upgrade.\n\nDescription\n===========\n\nBy default, ``upgrade-check`` checks your schema against the latest stable\nrelease of |Gel|. You can add ``--to-version <version>``, ``--to-testing``,\n``--to-nightly``, or ``--to-channel <channel>`` to check against a specific\nversion.\n\nOptions\n=======\n\nThe ``migration upgrade-check`` command runs on the database it is connected\nto. For specifying the connection target see :ref:`connection options\n<ref_cli_gel_connopts>`.\n\n\n:cli:synopsis:`--schema-dir=<schema-dir>`\n    Directory where the schema files are located. Defaults to ``./dbschema``.\n\n:cli:synopsis:`--to-version <to_version>`\n    Check the upgrade to a specified version\n\n:cli:synopsis:`--to-nightly`\n    Check the upgrade to a latest nightly version\n\n:cli:synopsis:`--to-testing`\n    Check the upgrade to a latest testing version\n\n:cli:synopsis:`--to-channel <to_channel>`\n    Check the upgrade to the latest version in the channel [possible values:\n    stable, testing, nightly]\n\n:cli:synopsis:`--watch`\n    Monitor schema changes and check again on change\n"
  },
  {
    "path": "docs/reference/using/cli/gel_migration/index.rst",
    "content": ".. _ref_cli_gel_migration:\n\n\n=============\ngel migration\n=============\n\n|Gel| provides schema migration tools as server-side tools. This means that,\nfrom the point of view of the application, migrations are language- and\nplatform-agnostic and don't require additional libraries.\n\nUsing the migration tools is the recommended way to make schema changes.\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    gel_migration_apply\n    gel_migration_create\n    gel_migration_edit\n    gel_migration_extract\n    gel_migration_log\n    gel_migration_status\n    gel_migration_upgrade_check\n\nSetup\n=====\n\nFirst of all, the migration tools need a place to store the schema and\nmigration information. By default they will look in the ``dbschema``\ndirectory, but it's also possible to specify any other location by\nusing the :cli:synopsis:`schema-dir` option.\n\nInside this directory, you will find an |.gel| file with an :ref:`SDL\n<ref_eql_sdl>` schema description. You may split your schema across multiple\n|.gel| files. The migration tools will read all of them and treat them as a\nsingle SDL document.\n\n.. list-table::\n    :class: funcoptable\n\n    * - :ref:`ref_cli_gel_migration_apply`\n      - Bring current |branch| to the latest or a specified revision\n    * - :ref:`ref_cli_gel_migration_create`\n      - Create a migration script\n    * - :ref:`ref_cli_gel_migration_edit`\n      - Edit migration file\n    * - :ref:`ref_cli_gel_migration_extract`\n      - Extract migration history and write it to ``/migrations``.\n    * - :ref:`ref_cli_gel_migration_log`\n      - Show all migration versions\n    * - :ref:`ref_cli_gel_migration_status`\n      - Show current migration state\n    * - :ref:`ref_cli_gel_migration_upgrade_check`\n      - Checks your schema against a different |Gel| version.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_project/gel_project_info.rst",
    "content": ".. _ref_cli_gel_project_info:\n\n\n================\ngel project info\n================\n\nDisplay various metadata about the project.\n\n.. cli:synopsis::\n\n    gel project info [OPTIONS]\n\n\nDescription\n============\n\nThis command provides information about the project instance, such as\nname and the project path.\n\n\nOptions\n=======\n\n:cli:synopsis:`--instance-name`\n    Display only the instance name.\n\n:cli:synopsis:`-j, --json`\n    Output in JSON format.\n\n:cli:synopsis:`--project-dir=<project-dir>`\n    The project directory can be specified explicitly. Defaults to the\n    current directory.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_project/gel_project_init.rst",
    "content": ".. _ref_cli_gel_project_init:\n\n\n================\ngel project init\n================\n\nSetup a new project.\n\n.. cli:synopsis::\n\n    gel project init [<options>]\n\n\nDescription\n===========\n\nThis command sets up a new project, creating an instance, a schema directory,\nand an :ref:`gel.toml <ref_reference_gel_toml>` file. It can also be used\nto convert an existing directory to a project directory, connecting the\nexisting instance to the project. Typically this tool will prompt for specific\ndetails about how the project should be setup.\n\n\nGel Cloud\n---------\n\n|Gel| Cloud users may use this command to create a Cloud instance after\nlogging in using :ref:`ref_cli_gel_cloud_login`.\n\nTo create a Cloud instance, your instance name should be in the format\n``<org-name>/<instance-name>``. Cloud instance names may contain alphanumeric\ncharacters and hyphens (i.e., ``-``). You can provide this Cloud instance name\nthrough the interactive project initiation by running :gelcmd:`project init`\nor by providing it via the ``--server-instance`` option.\n\n.. note::\n\n    Please be aware of the following restrictions on |Gel| Cloud instance\n    names:\n\n    * can contain only Latin alpha-numeric characters or ``-``\n    * cannot start with a dash (``-``) or contain double dashes (``--``)\n    * maximum instance name length is 61 characters minus the length of your\n      organization name (i.e., length of organization name + length of instance\n      name must be fewer than 62 characters)\n\n\nOptions\n=======\n\n:cli:synopsis:`--link`\n    Specifies whether the existing |Gel| server instance should be\n    linked with the project.\n\n    This option is useful for initializing a copy of a project freshly\n    downloaded from a repository with a pre-existing project database.\n\n:cli:synopsis:`--no-migrations`\n    Skip running migrations.\n\n    There are two main use cases for this option:\n\n    1. With :cli:synopsis:`--link` option to connect to a datastore\n       with existing data.\n    2. To initialize a new instance but then restore dump to it.\n\n:cli:synopsis:`--non-interactive`\n    Run in non-interactive mode (accepting all defaults).\n\n:cli:synopsis:`--project-dir=<project-dir>`\n    The project directory can be specified explicitly. Defaults to the\n    current directory.\n\n:cli:synopsis:`--server-instance=<server-instance>`\n    Specifies the |Gel| server instance to be associated with the\n    project.\n\n:cli:synopsis:`--server-version=<server-version>`\n    Specifies the Gel server instance to be associated with the\n    project.\n\n    By default, when you specify a version, the CLI will use the latest release\n    in the major version specified. This command, for example, will install the\n    latest 6.x release:\n\n    .. code-block:: bash\n\n        $ gel project init --server-version 6.1\n\n    You may pin to a specific version by prepending the version number with an\n    equals sign. This command will install version 6.1:\n\n    .. code-block:: bash\n\n        $ gel project init --server-version =6.1\n\n    .. note::\n\n        Some shells like ZSH may require you to escape the equals sign (e.g.,\n        ``\\=6.1``) or quote the version string (e.g., ``\"=6.1\"``).\n"
  },
  {
    "path": "docs/reference/using/cli/gel_project/gel_project_unlink.rst",
    "content": ".. _ref_cli_gel_project_unlink:\n\n\n==================\ngel project unlink\n==================\n\nRemove association with and optionally destroy the linked |Gel|\ninstance.\n\n.. cli:synopsis::\n\n    gel project unlink [<options>]\n\n\nDescription\n===========\n\nThis command unlinks the project directory from the instance. By\ndefault the |Gel| instance remains untouched, but it can also be\ndestroyed with an explicit option.\n\n\nOptions\n=======\n\n:cli:synopsis:`-D, --destroy-server-instance`\n    If specified, the associated |Gel| instance is destroyed by\n    running :ref:`ref_cli_gel_instance_destroy`.\n\n:cli:synopsis:`--non-interactive`\n    Do not prompts user for input.\n\n:cli:synopsis:`--project-dir=<project-dir>`\n    The project directory can be specified explicitly. Defaults to the\n    current directory.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_project/gel_project_upgrade.rst",
    "content": ".. _ref_cli_gel_project_upgrade:\n\n\n===================\ngel project upgrade\n===================\n\nUpgrade |Gel| instance used for the current project\n\n.. cli:synopsis::\n\n    gel project upgrade [<options>]\n\n\nDescription\n===========\n\nThis command has two modes of operation.\n\n1) Upgrade instance to a version specified in :ref:`ref_reference_gel_toml`.\n   This happens when the command is invoked without any explicit target\n   version.\n2) Update |gel.toml| to a new version and upgrade the instance.\n   Which happens when one of the options for providing the target\n   version is used.\n\nIn all cases your data is preserved and converted using dump/restore\nmechanism. This might fail if lower version is specified (for example\nif upgrading from nightly to the stable version).\n\n.. note::\n\n    The :gelcmd:`project upgrade` command is not intended for use with\n    self-hosted instances.\n\n\nOptions\n=======\n\n:cli:synopsis:`--force`\n    Force upgrade process even if there is no new version.\n\n:cli:synopsis:`--to-latest`\n    Upgrade to a latest stable version.\n\n:cli:synopsis:`--to-nightly`\n    Upgrade to a latest nightly version.\n\n:cli:synopsis:`--to-version=<version>`\n    Upgrade to a specified major version.\n\n:cli:synopsis:`--project-dir=<project-dir>`\n    The project directory can be specified explicitly. Defaults to the\n    current directory.\n\n:cli:synopsis:`-v, --verbose`\n    Verbose output.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_project/index.rst",
    "content": ".. _ref_cli_gel_project:\n\n\n===========\ngel project\n===========\n\n|Gel| provides a way to quickly setup a project. This way the project\ndirectory gets associated with a specific Gel instance and thus\nmakes it the default instance to connect to. This is done by creating\nan :ref:`ref_reference_gel_toml` file in the project directory.\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    gel_project_init\n    gel_project_info\n    gel_project_unlink\n    gel_project_upgrade\n\n.. list-table::\n    :class: funcoptable\n\n    * - :ref:`ref_cli_gel_project_init`\n      - Initialize a new or existing project\n    * - :ref:`ref_cli_gel_project_info`\n      - Get various metadata about the project\n    * - :ref:`ref_cli_gel_project_unlink`\n      - Remove project association with an instance\n    * - :ref:`ref_cli_gel_project_upgrade`\n      - Upgrade |Gel| instance used for the current project\n"
  },
  {
    "path": "docs/reference/using/cli/gel_query.rst",
    "content": ".. _ref_cli_gel_query:\n\n\n=========\ngel query\n=========\n\nExecute one or more EdgeQL queries.\n\n.. cli:synopsis::\n\n    gel query [<options>] <edgeql-query>...\n\n\nDescription\n===========\n\n:gelcmd:`query` is a terminal command used to execute EdgeQL queries\nprovided as space-separated strings.\n\n\nOptions\n=======\n\nThe ``query`` command runs on the database it is connected\nto. For specifying the connection target see :ref:`connection options\n<ref_cli_gel_connopts>`.\n\n:cli:synopsis:`-F, --output-format=<output_format>`\n    Output format: ``json``, ``json-pretty``, ``json-lines``,\n    ``tab-separated``. Default is ``json-pretty``.\n\n:cli:synopsis:`-f, --file=<file>`\n    Filename to execute queries from. Pass ``--file -`` to execute\n    queries from stdin.\n\n:cli:synopsis:`<edgeql-query>`\n    Any valid EdgeQL query to be executed.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_restore.rst",
    "content": ".. _ref_cli_gel_restore:\n\n\n===========\ngel restore\n===========\n\nRestore a |Gel| |branch| from a backup file.\n\n.. cli:synopsis::\n\n    gel restore [<options>] <path>\n\n\nDescription\n===========\n\n:gelcmd:`restore` is a terminal command used to restore a Gel |branch|\n|branch| from a backup file. The backup is restored to the\ncurrently active branch.\n\n.. note::\n\n    The backup cannot be restored to a |branch| with any\n    existing schema. As a result, you should restore to one of these targets:\n\n    - a new empty |branch| which can be created using\n      :ref:`ref_cli_gel_branch_create` with the ``--empty`` option\n    - a new empty |branch| if your instance is running |EdgeDB| versions\n      prior to 5\n    - an existing |branch| that has been wiped with the appropriate\n      ``wipe`` command (either :ref:`ref_cli_gel_branch_wipe` or\n      :ref:`ref_cli_gel_database_wipe`; note that this will destroy all data\n      and schema currently in that branch/database)\n\n\nOptions\n=======\n\nThe ``restore`` command restores the backup file into the active |branch|.\nFor specifying the connection target see :ref:`connection options\n<ref_cli_gel_connopts>`.\n\n:cli:synopsis:`<path>`\n    The name of the backup file to restore the |branch| from.\n\n:cli:synopsis:`--all`\n    Restore all |branches| and the server configuration\n    using the directory specified by the :cli:synopsis:`<path>`.\n\n:cli:synopsis:`-v, --verbose`\n    Verbose output.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_server/gel_server_info.rst",
    "content": ".. _ref_cli_gel_server_info:\n\n\n===============\ngel server info\n===============\n\nShow server information.\n\n.. cli:synopsis::\n\n     gel server info [<options>]\n\n\nDescription\n===========\n\n:gelcmd:`server info` is a terminal command for displaying the\ninformation about installed |Gel| servers.\n\n\nOptions\n=======\n\n:cli:synopsis:`--json`\n    Format output as JSON.\n\n:cli:synopsis:`--nightly`\n    Display the information about the nightly server version.\n\n:cli:synopsis:`--latest`\n    Display the information about the latest server version.\n\n:cli:synopsis:`--bin-path`\n    Display only the server binary path (if applicable).\n\n:cli:synopsis:`--version=<version>`\n    Display the information about a specific server version.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_server/gel_server_install.rst",
    "content": ".. _ref_cli_gel_server_install:\n\n\n==================\ngel server install\n==================\n\nInstall |Gel| server.\n\n.. cli:synopsis::\n\n     gel server install [<options>]\n\n\nDescription\n===========\n\n:gelcmd:`server install` is a terminal command for installing a\nspecific |Gel| server version.\n\n\nOptions\n=======\n\n:cli:synopsis:`-i, --interactive`\n    Performs the installation in interactive mode, similar to how\n    :ref:`downloading and installing <ref_cli_gel_install>` works.\n\n:cli:synopsis:`--nightly`\n    Installs the nightly server version.\n\n:cli:synopsis:`--version=<version>`\n    Specifies the version of the server to be installed. Defaults to\n    the most recent release.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_server/gel_server_list_versions.rst",
    "content": ".. _ref_cli_gel_server_list_versions:\n\n\n========================\ngel server list-versions\n========================\n\nList available and installed versions of the |Gel| server.\n\n.. cli:synopsis::\n\n     gel server list-versions [<options>]\n\n\nDescription\n===========\n\n:gelcmd:`server list-versions` is a terminal command for displaying\nall the available |Gel| server versions along with indicating whether\nor not and how they are currently installed.\n\n\nOptions\n=======\n\n:cli:synopsis:`--json`\n    Format output as JSON.\n\n:cli:synopsis:`--installed-only`\n    Display only the installed versions.\n\n:cli:synopsis:`--column=<column>`\n    Format output as a single column displaying only one aspect of the\n    server: ``major-version``, ``installed``, ``available``.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_server/gel_server_uninstall.rst",
    "content": ".. _ref_cli_gel_server_uninstall:\n\n\n====================\ngel server uninstall\n====================\n\nUninstall |Gel| server.\n\n.. cli:synopsis::\n\n     gel server uninstall [<options>]\n\n\nDescription\n===========\n\n:gelcmd:`server uninstall` is a terminal command for removing a\nspecific |Gel| server version from your system.\n\n\nOptions\n=======\n\n:cli:synopsis:`--all`\n    Uninstalls all server versions.\n\n:cli:synopsis:`--nightly`\n    Uninstalls the nightly server version.\n\n:cli:synopsis:`--unused`\n    Uninstalls server versions that are not used to run any instances.\n\n:cli:synopsis:`--version=<version>`\n    Specifies the version of the server to be uninstalled.\n\n:cli:synopsis:`-v, --verbose`\n    Produce a more verbose output.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_server/index.rst",
    "content": ".. _ref_cli_gel_server:\n\n==========\ngel server\n==========\n\nThe :gelcmd:`server` group of commands contains all sorts of tools\nfor managing |Gel| server versions.\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    gel_server_info\n    gel_server_install\n    gel_server_list_versions\n    gel_server_uninstall\n\n.. list-table::\n    :class: funcoptable\n\n    * - :ref:`ref_cli_gel_server_info`\n      - Show server information\n    * - :ref:`ref_cli_gel_server_install`\n      - Install |Gel| server\n    * - :ref:`ref_cli_gel_server_list_versions`\n      - List available and installed versions of the server\n    * - :ref:`ref_cli_gel_server_uninstall`\n      - Uninstall |Gel| server\n"
  },
  {
    "path": "docs/reference/using/cli/gel_ui.rst",
    "content": ".. _ref_cli_gel_ui:\n\n\n======\ngel ui\n======\n\nOpen the |Gel| UI of the current instance in your default browser.\n\n.. cli:synopsis::\n\n    gel ui [<options>]\n\n\nDescription\n===========\n\n:gelcmd:`ui` is a terminal command used to open the |Gel| UI in your default\nbrowser. Alternatively, it can be used to print the UI URL with the\n``--print-url`` option.\n\nThe Gel UI is a tool that allows you to graphically manage and query your\nGel database. It contains a REPL, a textual and graphical view of your\ndatabase schemas, and a data explorer which allows for viewing your data as a\ntable.\n\n.. note::\n\n    The UI is served by default by development instances. To enable the UI on a\n    production instance, use the ``--admin-ui`` option with |gel-server|\n    or set the :gelenv:`SERVER_ADMIN_UI` :ref:`environment variable\n    <ref_reference_envvar_admin_ui>` to ``enabled``.\n\n\nOptions\n=======\n\nThe ``ui`` command runs on the |branch| it is connected to. For specifying the\nconnection target see :ref:`connection options <ref_cli_gel_connopts>`.\n\n:cli:synopsis:`--print-url`\n    Print URL in console instead of opening in the browser. This is useful if\n    you prefer to open the Gel UI in a browser other than your default\n    browser.\n\n:cli:synopsis:`--no-server-check`\n    Skip probing the UI endpoint of the server instance. The endpoint probe is\n    in place to provide a friendly error if you try to connect to a UI on a\n    remote instance that does not have the UI enabled.\n"
  },
  {
    "path": "docs/reference/using/cli/gel_watch.rst",
    "content": ".. _ref_cli_gel_watch:\n\n\n=========\ngel watch\n=========\n\nStart a long-running process that watches for changes as specified in the\n:ref:`gel.toml <ref_reference_gel_toml>` file. This process will monitor the\nproject for changes specified in the ``[[watch]]`` table array and run the\nassociated scripts in response to those changes.\n\nWhen multiple changes target the same ``[[watch]]`` element, the corresponding\nscript will be triggered only once. All triggered watch scripts will be\nexecuted in parallel. If the same script is triggered before it finishes\nexecuting, the next execution will wait for the already running script to\nterminate (i.e. only one instance of the same script will be runing at the\nsame time).\n\n.. note::\n\n    Any output that the triggered scripts produce will be shown in the\n    :gelcmd:`watch` console. This includes any error messages. So if you're\n    not seeing a change you've expected, check on the watch process to make\n    sure there aren't any unexpected errors in the triggered scripts.\n\n\nOptions\n=======\n\n.. warning::\n\n    This command changed in version 6. In older versions it only monitored the\n    schema file changes and it had no additional options.\n\n.. versionadded:: 6.0\n\n:cli:synopsis:`--migrate`\n    Watches for changes in schema files in your project's ``dbschema``\n    directory and applies those changes to your current |branch| in real time.\n\n    If a schema change cannot be applied, you will see an error in the\n    :gelcmd:`watch` console. You will also receive the error when you\n    try to run a query with any |Gel| client binding.\n\n    .. note::\n\n        If you want to apply a migration in the same manner as ``watch\n        --migrate`` but without the long-running process, use :gelcmd:`migrate\n        --dev-mode`. See :ref:`ref_cli_gel_migration_apply` for more details.\n\n    To learn about our recommended development migration workflow using\n    :gelcmd:`watch`, read our :ref:`intro to migrations\n    <ref_intro_migrations>`.\n\n:cli:synopsis:`-v, --verbose`\n    Verbose output.\n"
  },
  {
    "path": "docs/reference/using/cli/index.rst",
    "content": ".. _ref_cli_overview:\n\n===\nCLI\n===\n\n:edb-alt-title: The Gel CLI\n\nThe |gelcmd| command-line interface (CLI) provides an idiomatic way to spin up local instances, open a REPL, execute queries, manage auth roles, introspect schema, create migrations, and more.\n\nIf you're using JavaScript or Python, our client libraries will handle downloading and running the CLI for you using tools like ``npx`` and ``uvx``.\n\nFor everyone else, or if you wish to install the CLI globally, you can install using our bash installer or your operating system's package manager.\n\n.. tabs::\n\n  .. code-tab:: bash\n    :caption: bash\n\n    $ curl https://www.geldata.com/sh --proto \"=https\" -sSf1 | sh\n\n  .. code-tab:: powershell\n    :caption: Powershell\n\n    PS> irm https://www.geldata.com/ps1 | iex\n\n  .. code-tab:: bash\n    :caption: Homebrew\n\n    $ brew install geldata/tap/gel-cli\n\n  .. code-tab:: bash\n    :caption: Nixpkgs\n\n    $ nix-shell -p gel\n\n  .. code-tab:: bash\n    :caption: JavaScript\n\n    $ npx gel --version\n\n  .. code-tab:: bash\n    :caption: Python\n\n    $ uvx gel --version\n\n.. rubric:: Connection options\n\nAll commands respect a common set of\n:ref:`connection options <ref_cli_gel_connopts>`, which let you specify\na target instance. This instance can be local to your machine or hosted\nremotely.\n\n\n.. _ref_cli_gel_uninstall:\n\n.. rubric:: Uninstallation\n\nCommand-line tools contain just one binary, so to remove it on Linux or\nmacOS run:\n\n.. code-block:: bash\n\n   $ rm \"$(which gel)\"\n\nTo remove all configuration files, run :gelcmd:`info` to list the directories\nwhere |Gel| stores data, then use ``rm -rf <dir>`` to delete those\ndirectories.\n\nIf the command-line tool was installed by the user (recommended) then it\nwill also remove the binary.\n\nIf you've used ``gel`` commands you can also delete\n:ref:`instances <ref_cli_gel_instance_destroy>` and :ref:`server\n<ref_cli_gel_server_uninstall>` packages, prior to removing the\ntool:\n\n.. code-block:: bash\n\n   $ gel instance destroy <instance_name>\n\nTo list instances and server versions use the following commands\nrespectively:\n\n.. code-block:: bash\n\n   $ gel instance status\n   $ gel server list-versions --installed-only\n\n\n.. _ref_cli_gel_config:\n\n.. rubric:: Configure CLI and REPL\n\nYou can customize the behavior of the |gelcmd| CLI and REPL with a\nglobal configuration file. The file is called ``cli.toml`` and its\nlocation differs between operating systems. Use\n:ref:`ref_cli_gel_info` to find the \"Config\" directory on your\nsystem.\n\nThe ``cli.toml`` has the following structure. All fields are optional:\n\n.. code-block::\n\n    [shell]\n    expand-strings = true         # Stop escaping newlines in quoted strings\n    history-size = 10000          # Set number of entries retained in history\n    implicit-properties = false   # Print implicit properties of objects\n    limit = 100                   # Set implicit LIMIT\n                                  # Defaults to 100, specify 0 to disable\n    input-mode = \"emacs\"          # Set input mode. One of: vi, emacs\n    output-format = \"default\"     # Set output format.\n                                  # One of: default, json, json-pretty,\n                                  # json-lines\n    print-stats = \"off\"           # Print statistics on each query.\n                                  # One of: off, query, detailed\n    verbose-errors = false        # Print all errors with maximum verbosity\n\n\n:ref:`Notes on network usage <ref_cli_gel_network>`\n\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    gel_connopts\n    network\n    gel\n    gel_init\n    gel_project/index\n    gel_ui\n    gel_watch\n    gel_migrate\n    gel_migration/index\n    gel_cloud/index\n    gel_branch/index\n    gel_dump\n    gel_restore\n    gel_configure\n    gel_query\n    gel_analyze\n    gel_list\n    gel_info\n    gel_cli_upgrade\n    gel_extension/index\n    gel_server/index\n    gel_describe/index\n    gel_instance/index\n    gel_database/index\n"
  },
  {
    "path": "docs/reference/using/cli/network.rst",
    "content": ".. _ref_cli_gel_network:\n\n=============\nNetwork usage\n=============\n\nGenerally command-line tools connect only to the database host with a few\nexceptions:\n\n1. When the command-line tool starts, it checks if its version is up to\n   date. :ref:`Details <ref_cli_gel_version_check>`\n2. The :ref:`ref_cli_gel_server` family of commands and\n   :ref:`ref_cli_gel_cli_upgrade` discover package versions and\n   docker images and also invoke package managers and the docker\n   engine to do :ref:`index updates and related data.\n   <ref_cli_gel_net_server>`\n3. The CLI communicates with the |Gel| Cloud API to provide easy access to\n   your Gel Cloud instances.\n\n\n.. _ref_cli_gel_version_check:\n\nVersion Check\n=============\n\nVersion check checks the current version of command-line tool by fetching\n``https://packages.geldata.com/.jsonindexes/*.json``.\n\nHere is how such a request looks like::\n\n    GET /archive/.jsonindexes/linux-x86_64.json HTTP/1.1\n    host: packages.geldata.com\n    content-length: 0\n    user-agent: gel\n\nThe ``User-Agent`` header only specifies that request is done by\n``gel`` command-line tool (without version number). The platform,\narchitecture and whether nightly is used can be devised from the URL of\nthe query.\n\nLatest version number is cached for the random duration from 16 to 32\nhours (this randomization is done both for spreading the load and for\nbetter anonymizing the data). A failure is cached for the random\nduration from 6 to 12 hours.\n\n\nDisabling Version Check\n=======================\n\nTo disable version check do one of two things:\n\n1. Use ``--no-cli-update-check`` command-line parameter to disable just\n   for this invocation\n2. Export :gelenv:`RUN_VERSION_CHECK=never` in the environment.\n\n.. XXX: edgedb::version_check=debug\n\nTo verify that check is skipped and no network access is being done\nlogging facility can be used::\n\n   $ export RUST_LOG=edgedb::version_check=debug\n   $ gel --no-cli-update-check\n   [..snip..] Skipping version check due to --no-cli-update-check\n   gel>\n   $ GEL_RUN_VERSION_CHECK=never gel\n   [..snip..] Skipping version check due to GEL_RUN_VERSION_CHECK=never\n   gel>\n\n\n.. _ref_cli_gel_net_server:\n\n\"gel server\" and \"gel self upgrade\"\n===================================\n\nGenerally these commands do requests with exactly the headers\nlike :ref:`version check <ref_cli_gel_version_check>`.\n\nData sources for the commands directly:\n\n1. Package indexes and packages at ``https://packages.geldata.com``\n2. Docker image index at ``https://registry.hub.docker.com``\n\nData sources that can be used indirectly:\n\n1. Docker engine may fetch indexes and images. Currently the only\n   images used are at Docker Hub. More specifically\n   are ``gel/*`` and ``busybox`` (Docker's official image).\n2. Package managers (currently ``apt-get``, ``yum``) can fetch indexes\n   and install packages from ``https://packages.geldata.com``. And\n   as we use generic commands (e.g. ``apt-get update``) and system\n   dependencies, package manager can fetch package indexes and package\n   data from any sources listed in repositories configured in the\n   system.\n\nTo avoid reaching these hosts, avoid using: :gelcmd:`server` and\n:gelcmd:`self upgrade` subcommands. These commands only simplify\ninstallation and maintenance of the installations. All |Gel| features\nare available without using them.\n"
  },
  {
    "path": "docs/reference/using/clients.rst",
    "content": ".. _ref_clients_index:\n.. _ref_using_clients:\n\n=================\nClient philosophy\n=================\n\nTo connect your application to a |Gel| instance, you can use one of our official client libraries that speaks the Gel binary protocol. The client libraries are higher level than typical database drivers, and are designed to provide a fully-featured API for working with |Gel| instances.\n\n* Connecting is easy\n* Transactions are robust\n* Many client instances, single connection pool\n\nConnecting is easy\n==================\n\nTypical database drivers require you to figure out how to pass the correct connection string (called a DSN, or data source name) to the driver. This is a bit of a pain, and is error prone.\n\nOur client libraries take a different approach. Instead of needing to pass a DSN into our client creation function, the client libraries use a convention to discover the connection information in a way that allows you to not have to vary the code you write based on where you run your application.\n\n.. tabs::\n\n  .. code-tab:: typescript\n    :caption: TypeScript\n\n    import { createClient } from \"gel\";\n\n    const client = createClient();\n\n    const result = await client.queryRequiredSingle(\"select 42\");\n\n  .. code-tab:: python\n    :caption: Python\n\n    import gel\n\n    client = gel.create_client()\n\n    result = client.query_required_single(\"select 42\")\n\n    client.close()\n\n  .. code-tab:: go\n    :caption: Go\n\n    package main\n\n    import (\n        \"context\"\n\n        \"github.com/geldata/gel-go\"\n        \"github.com/geldata/gel-go/gelcfg\"\n    )\n\n    func main() {\n        ctx := context.Background()\n        client, err := gel.CreateClient(gelcfg.Options{})\n        if err != nil {\n            log.Fatal(err)\n        }\n        defer client.Close()\n\n        var (\n            answer int64\n        )\n\n        query := \"select 42\"\n        err = client.QuerySingle(ctx, query, &answer)\n    }\n\n  .. code-tab:: rust\n    :caption: Rust\n\n    #[tokio::main]\n    async fn main() -> anyhow::Result<()> {\n        let conn = gel_tokio::create_client().await?;\n        let val = conn.query_required_single::<i64, _>(\n            \"select 42\",\n            &(),\n        ).await?;\n\n        Ok(())\n    }\n\n\n* Local development: We suggest using our :ref:`projects <ref_guide_using_projects>` feature to manage your local instance. We handle creating credentials, and clients automatically know how to resolve the connection information for projects initialized in your project directory. We also support Docker Compose.\n* Production: For production, we recommend using environment variables to specify connection information to your |Gel| Cloud instance or remote self-hosted instance.\n\nThis approach allows you to write code that does not need to contain any error-prone conditional logic. For more information on how to configure your connection for development and production, see :ref:`the reference for connection environments <ref_reference_connection_environments>`.\n\nTransactions are robust\n=======================\n\nTransactions are an important part of working with databases. In Gel, all queries are automatically run in an implicit transaction, ensuring atomicity and isolation for individual operations. We use the safest isolation level of ``SERIALIZABLE`` to ensure consistent results across high-concurrency scenarios. Additionally, our client libraries provide a higher-level API for working with explicit transactions when you need to group multiple operations together.\n\nOur client libraries are designed to automatically retry failures for certain classes of transaction errors. For instance, one common failure mode is a serialization error, which can happen when two transactions that are trying to modify the same data attempt to commit at the same time. The database will pick one of the transactions to commit, and the other will fail. In typical database drivers, you would need to handle this in your application code, but in our client libraries, you don't need to worry about it: we will simply retry the transaction for you in this case.\n\nThe behavior of transaction retries can be customized in the client configuration, which we will detail in full in the documentation for each client library.\n\n.. tabs::\n\n  .. code-tab:: typescript\n    :caption: TypeScript\n\n    import { createClient } from \"gel\";\n\n    const client = createClient();\n\n    await client.transaction(async (tx) => {\n      await tx.execute(\"insert User { name := 'Don' }\");\n    });\n\n  .. code-tab:: python\n    :caption: Python\n\n    import gel\n\n    client = gel.create_client()\n\n    for tx in client.transaction():\n        with tx:\n            tx.execute(\"insert User { name := 'Don' }\")\n\n  .. code-tab:: go\n    :caption: Go\n\n    package main\n\n    import (\n        \"context\"\n        \"log\"\n\n        \"github.com/geldata/gel-go\"\n        \"github.com/geldata/gel-go/geltypes\"\n    )\n\n    err := client.Tx(ctx, func(ctx context.Context, tx geltypes.Tx) error {\n      return tx.Execute(ctx, \"insert User { name := 'Don' }\")\n    })\n    if err != nil {\n      log.Println(err)\n    }\n\n  .. code-tab:: rust\n    :caption: Rust\n\n    let client = gel_tokio::create_client().await?;\n\n    client\n        .transaction(|mut conn| async move {\n            conn.execute(\"insert User { name := 'Don' }\", &()).await?;\n            Ok(())\n        })\n        .await?;\n\nMany client instances, single connection pool\n=============================================\n\nWhen you create a client, you also establish a connection pool to the Gel server. Since this is resource-intensive, you can create a single client instance and then derive lightweight instances that share the same connection pool. Each derived instance can have different configurations, allowing customization without additional overhead.\n\nYou can configure various aspects of these client instances at runtime, including:\n- Setting global variables\n- Adjusting query timeouts\n- Disabling access policies\n- And other client-specific options\n\n.. image:: images/client-config-layers.png\n  :alt: Diagram showing how client instances share a connection pool\n  :width: 100%\n"
  },
  {
    "path": "docs/reference/using/connection.rst",
    "content": ".. _gel_client_connection:\n.. _ref_reference_connection:\n\n=====================\nConnection parameters\n=====================\n\nThe CLI and client libraries (collectively referred to as \"clients\" below) must connect to a |Gel| instance to run queries or commands. Ultimately, configuration works to specify a specific |Gel| branch on a specific |Gel| instance, and any required credentials. Additionally, client connection behavior can also be specified.\n\nThere are multiple places where the configuration can be specified, and the clients all share the same resolution logic and priority order. Let's first look at how to specify the connection configuration, and then we'll look at the different environments your own applications may run in and what the common practices are for specifying the configuration for each type of environment.\n\n.. _ref_reference_connection_instance:\n\nConnecting to a Gel branch\n==========================\n\nThe first job of the configuration system is to specify a |Gel| branch on a specific |Gel| instance. The parts that make up the full configuration are:\n\n* **Host**: defaults to ``\"localhost\"``\n    The host name or IP address of the |Gel| instance.\n* **Port**: defaults to ``5656``\n    The port number of the |Gel| instance.\n* **Branch**: defaults to |main|\n    The name of the |Gel| branch to connect to.\n* **User**: defaults to |admin|\n    The user name to connect as.\n* **Password**: defaults to unset\n    The password for the user.\n* **TLS certificate**: defaults to unset\n    The TLS certificate to use for the connection, if any.\n* **TLS security**: defaults to ``\"strict\"``\n    The TLS security mode to use for the connection.\n\nThere are several ways to specify these parameters:\n\n.. _ref_reference_connection_instance_name:\n\nInstance name\n-------------\n\nAll local instances created on your local machine using the :gelcmd:`project init` command are associated with a name. This name is what's needed to connect; under the hood, the CLI stores the instance location and credentials (host, port, username, password, etc) on your file system in the Gel :ref:`config directory <ref_cli_gel_paths>`. The clients look up these credentials to connect.\n\nYou can also assign names to remote instances using :ref:`gel instance link <ref_cli_gel_instance_link>`. The CLI will save the credentials locally, so you can connect to a remote instance using just its name, just like a local instance.\n\nIf you have authenticated with Gel Cloud in the CLI using the :ref:`ref_cli_gel_cloud_login` command, you can address your own Gel Cloud instances using the instance name format ``<org-name>/<instance-name>``. When connecting a deployed application instead of logging in, you will need to provide an instance name and secret key, which you can create using the :gelcmd:`cloud secretkey create` command or in the |Gel| Cloud web UI.\n\nEach named instance will also have a branch associated with the credentials, which defaults to |main|. You can create and switch branches using the :gelcmd:`branch create` and :gelcmd:`branch switch` commands or by specifying the branch name explicitly in the :ref:`branch connection parameter <ref_reference_connection_parameters_branch>`.\n\n.. _ref_dsn:\n.. _ref_reference_connection_dsn:\n\nDSN\n---\n\nDSNs (data source names) are a convenient and flexible way to specify connection information with a simple string. It takes the following form:\n\n.. code-block:: text\n\n  gel://<user>:<password>@<host>:<port>/<branch>\n\nAll components of the DSN are optional; in fact, gel:// is a valid DSN. Any unspecified values will fall back to the defaults. DSNs also support URL query parameters (``?host=myhost.com``) to support advanced use cases and :ref:`additional connection parameters <ref_reference_connection_parameters>`. The value for a given parameter can be specified in three ways:\n\n**Plain params**\n  .. code-block::\n\n    gel://hostname.com:1234?tls_security=insecure\n\n  These \"plain\" parameters can be used to provide values for options that can't otherwise be reflected in the DSN, like TLS settings (described in more detail below).\n\n  You can't specify the same setting both in the body of the DSN and in a query parameter. For instance, this DSN is invalid, as the port is ambiguous: :geluri:`hostname.com:1234?port=5678`.\n\n**File params**\n  .. code-block::\n\n    gel://hostname.com:1234?tls_security_file=./tls_security.txt\n\n    # ./tls_security.txt\n    insecure\n\n  If you prefer to store sensitive credentials in local files, you can use file params to specify a path to a local UTF-8 encoded file. This file should contain a single line containing the relevant value.\n\n  Relative params are resolved relative to the current working directory at the time of connection.\n\n**Environment params**\n  .. code-block::\n\n    MY_TLS_SECURITY=insecure\n    GEL_DSN=gel://hostname.com:1234?tls_security_env=MY_TLS_SECURITY\n\n  Environment params lets you specify a *pointer* to another environment variable. At runtime, the specified environment variable will be read. If it isn't set, an error will be thrown.\n\n  Note that this is not a shell-style variable substitution, but rather a way to specify the name of another environment variable that contains the value.\n\nHost and port\n-------------\n\nIn general, we recommend using a fully-qualified DSN when connecting to the database. For convenience, it's possible to individually specify a host and/or a port.\n\nCredentials file\n----------------\n\n.. warning::\n\n  Checking this file into version control could present a security risk and is not recommended.\n\nIf you wish, you can store your credentials as a JSON file like the :gelcmd:`instance link` command does, and then pass the path to the file to the client libraries or CLI.\n\n.. code-block:: json\n\n  {\n    \"host\": \"localhost\",\n    \"port\": 10702,\n    \"user\": \"testuser\",\n    \"password\": \"testpassword\",\n    \"branch\": \"main\",\n    \"tls_cert_data\": \"-----BEGIN CERTIFICATE-----\\nabcdef...\"\n  }\n\nRelative paths are resolved relative to the current working directory.\n\n.. _ref_reference_connection_environments:\n\nEnvironments\n============\n\nThere are two common scenarios or environments for applications connecting to a |Gel| branch:\n\n* **Development**: When you are developing your application and running it locally, you will typically want to connect to a |Gel| instance running on the same machine, or at least on the same network.\n* **Deployed**: When you are running your application in a production, or production-like environment, the database instance might be running on a separate network. You also typically don't interact with the application environment directly, but rather through some kind of platform or production system. Examples of a deployed environment are running tests in a CI pipeline, a staging environment, or a production environment.\n\nDevelopment environments\n------------------------\n\n* **CLI-managed local instances**: When you initialize a project using :gelcmd:`project init`, the CLI will create a local instance, and create a local credentials file. Clients will detect that there is a local project, and resolve the DSN and authentication credentials automatically. You can use the CLI to create and switch local branches using the :gelcmd:`branch create` and :gelcmd:`branch switch` commands.\n* **Cloud instances**: Use the :gelcmd:`cloud login` command to authenticate with |Gel| Cloud, and then use the :gelcmd:`project init --server-instance org/instance-name` command to create a local project-linked instance that is linked to an Gel Cloud instance. Once you've linked your |Gel| Cloud instance as a project, you can use the :gelcmd:`branch create` and :gelcmd:`branch switch` commands to create and switch branches.\n* **Self-hosted instances**: When you have a |Gel| instance running on a remote machine or in a container, you can use the :gelcmd:`instance link` command to create a name corresponding to a remote instance. Once you have a linked instance, you can use :gelcmd:`project init --server-instance <instance-name>` to create a local project that is linked to the named remote instance.\n\nDeployed environments\n---------------------\n\n* **CLI-managed local instances**: It's not recommended to use CLI-managed local instances in production, but this can be useful for CI pipelines. If you use GitHub Actions, you can use the `setup-gel action <https://github.com/geldata/setup-gel>`_ to automatically create a local instance and initialize a project.\n* **Cloud instances**: To identify and authenticate with a |Gel| Cloud instance, you will need to provide the instance name and secret key. Your instance name will be of the form ``<org-name>/<instance-name>``. For each environment you deploy to, you should create a new secret key, which you can do locally using the :gelcmd:`cloud secretkey create` command or in the |Gel| Cloud web UI. You will need to also provide the branch name if you use multiple branches in your Cloud instance, for instance to share a single Cloud instance between testing, staging, and production environments. We recommend that you use environment variables in your runtime environment to configure these values.\n* **Self-hosted instances**: When you have a |Gel| instance running on a machine or in a container, you can connect to it using a DSN that specifies the host, port, branch, and authentication credentials of the instance. We recommend that you use environment variables in your runtime environment to configure these values.\n\n.. _ref_reference_connection_priority:\n\nPriority levels\n===============\n\nThe section above describes the various ways of specifying a Gel instance.  There are also several ways to provide this configuration information to the client. From highest to lowest priority, you can pass them explicitly as parameters/flags (useful for debugging), use environment variables (recommended for production), or rely on :gelcmd:`project` (recommended for development).\n\n1. **Explicit connection parameters**. For security reasons, hard-coding connection information or credentials in your codebase is not recommended, though it may be useful for debugging or testing purposes. As such, explicitly provided parameters are given the highest priority.\n\n   In the context of the client libraries, this means passing an option explicitly into the client creation call. Here's how this looks using the JavaScript library as an example:\n\n   .. code-block:: javascript\n\n      import { createClient } from \"gel\";\n\n      const pool = createClient({\n        instance: \"my_instance\"\n      });\n\n   In the context of the CLI, this means using the appropriate command-line flags:\n\n   .. code-block:: bash\n\n      $ gel --instance my_instance\n      Gel x.x\n      Type \\help for help, \\quit to quit.\n      main>\n\n\n2. **Environment variables**.\n\n   This is the recommended mechanism for providing connection information to your Gel client, especially in production or when running Gel inside a container. All clients read the following variables from the environment:\n\n   - :gelenv:`DSN`\n   - :gelenv:`INSTANCE`\n   - :gelenv:`CREDENTIALS_FILE`\n   - :gelenv:`HOST` / :gelenv:`PORT`\n\n   When one of these environment variables is defined, there's no need to pass\n   any additional information to the client. The CLI and client libraries will\n   be able to connect without any additional information. You can execute CLI\n   commands without any additional flags, like so:\n\n   .. code-block:: bash\n\n      $ gel  # no flags needed\n      Gel x.x\n      Type \\help for help, \\quit to quit.\n      gel>\n\n   Using the JavaScript client library:\n\n   .. code-block:: javascript\n\n      import { createClient } from \"gel\";\n\n      const client = createClient();\n      const result = await client.querySingle(\"select 2 + 2;\");\n      console.log(result); // 4\n\n   .. warning::\n\n      Ambiguity is not permitted. For instance, specifying both\n      :gelenv:`INSTANCE` and :gelenv:`DSN` will result in an error. You *can*\n      use :gelenv:`HOST` and :gelenv:`PORT` simultaneously.\n\n\n3. **Project-linked credentials**\n\n   If you are using :gelcmd:`project` (which we recommend!) and haven't otherwise specified any connection parameters, clients will connect to the instance that's been linked to your project.\n\n   This makes it easy to get up and running with Gel. Once you've run :gelcmd:`project init`, clients will be able to connect to your database without any explicit flags or parameters, as long as you're inside the project directory.\n\n\nIf no connection information can be detected using the above mechanisms, the connection fails.\n\n.. warning::\n\n   Within a given priority level, you cannot specify multiple instances of \"instance selection parameters\" simultaneously. For instance, specifying both :gelenv:`INSTANCE` and :gelenv:`DSN` environment variables will result in an error.\n\n.. _ref_reference_connection_granular_override:\n\nOverride behavior\n-----------------\n\nWhen specified, the connection parameters (user, password, and |branch|) will *override* the corresponding element of a DSN, credentials file, etc.  For instance, consider the following environment variables:\n\n.. code-block::\n\n  GEL_DSN=gel://olduser:oldpass@hostname.com:5656\n  GEL_USER=newuser\n  GEL_PASSWORD=newpass\n\nIn this scenario, ``newuser`` will override ``olduser`` and ``newpass`` will override ``oldpass``. The client library will try to connect using this modified DSN: :geluri:`newuser:newpass@hostname.com:5656`.\n\nOverriding across priority levels\n---------------------------------\n\nOverride behavior can only happen at the *same or lower priority level*. For instance:\n\n- :gelenv:`PASSWORD` **will** override the password specified in :gelenv:`DSN`\n\n- :gelenv:`PASSWORD` **will be ignored** if a DSN is passed explicitly using the ``--dsn`` flag. Explicit parameters take precedence over environment variables. To override the password of an explicit DSN, you need to pass it explicitly as well:\n\n  .. code-block:: bash\n\n     $ gel --dsn gel://username:oldpass@hostname.com --password qwerty\n     # connects to gel://username:qwerty@hostname.com\n\n- :gelenv:`PASSWORD` **will** override the stored password associated with a project-linked instance. (This is unlikely to be desirable.)\n\n.. _ref_reference_connection_granular:\n.. _ref_reference_connection_parameters:\n\nParameter Reference\n===================\n\nThe following is a list of all of the connection parameters and their corresponding environment variables, CLI flags, and client library parameters. Different language clients may have different parameter casing depending on the idiomatic conventions of the language, so see the specific client documentation for details.\n\n.. _ref_reference_connection_parameters_dsn:\n\nDSN\n---\n\n* Environment variable: :gelenv:`DSN`\n* CLI flag: ``--dsn/-d <dsn>``\n* Client library parameter: ``dsn``\n\nDSNs (data source names) are a convenient and flexible way to specify connection information with a simple string. It takes the following form:\n\n.. code-block:: text\n\n  gel://<user>:<password>@<host>:<port>/<branch>\n\nFor more details, see :ref:`ref_reference_connection_dsn` above.\n\n.. _ref_reference_connection_parameters_host:\n\nHost\n----\n\n* Environment variable: :gelenv:`HOST`\n* CLI flag: ``--host/-h <host>``\n* Client library parameter: ``host``\n* DSN query parameter: ``host``, ``host_file``, ``host_env``\n* Default value: ``\"localhost\"``\n\nThe host name or IP address of the |Gel| instance.\n\n.. _ref_reference_connection_parameters_port:\n\nPort\n----\n\n* Environment variable: :gelenv:`PORT`\n* CLI flag: ``--port/-P <port>``\n* Client library parameter: ``port``\n* DSN query parameter: ``port``, ``port_file``, ``port_env``\n* Default value: ``5656``\n\nThe port number of the |Gel| instance.\n\n.. _ref_reference_connection_parameters_instance_name:\n\nInstance name\n-------------\n\n* Environment variable: :gelenv:`INSTANCE`\n* CLI flag: ``--instance/-i <name>``\n* Client library parameter: ``instance``\n\nDescribed above in :ref:`ref_reference_connection_instance_name`. This name is used to look up the instance credentials in the |Gel| :ref:`config directory <ref_cli_gel_paths>`. If the instance name is a |Gel| Cloud instance, you will either need to be signed into your |Gel| Cloud account or provide a secret key.\n\n.. _ref_reference_connection_secret_key:\n.. _ref_reference_connection_parameters_secret_key:\n\nSecret key\n----------\n\n* Environment variable: :gelenv:`SECRET_KEY`\n* CLI flag: ``--secret-key/-k <key>``\n* Client library parameter: ``secretKey`` or ``secret_key``\n\nThis |Gel| Cloud specific parameter is used to authenticate with a |Gel| Cloud instance. It is required when connecting to a |Gel| Cloud instance that is not the one you are currently signed into or when connecting from a deployed application.\n\n.. _ref_reference_connection_parameters_branch:\n\nBranch\n------\n\n* Environment variable: :gelenv:`BRANCH`\n* CLI flag: ``--branch/-b <name>``\n* Client library parameter: ``branch``\n* DSN query parameter: ``branch``, ``branch_file``, ``branch_env``\n* Default value: |main|\n\nEach Gel instance can contain multiple branches. Each branch can be related to other branches on the same instance by sharing some or all of the schema, or be completely independent. The data for all branches are isolated from each other. For more information on branches, see :ref:`the branches reference section <ref_datamodel_branches>`.\n\nWhen an instance is created, a default branch named |main| is created. For CLI-managed linked instances, connections are made to the currently active branch. In other cases, incoming connections connect to the |main| branch by default.\n\n.. _ref_reference_connection_parameters_user:\n\nUser\n----\n\n* Environment variable: :gelenv:`USER`\n* CLI flag: ``--user/-u <user>``\n* Client library parameter: ``user``\n* DSN query parameter: ``user``, ``user_file``, ``user_env``\n* Default value: |admin|\n\nWhen using authentication, the user/role name to connect as.\n\n.. _ref_reference_connection_parameters_password:\n\nPassword\n--------\n\n* Environment variable: :gelenv:`PASSWORD`\n* CLI flag: ``--password/-p <pass>``\n* Client library parameter: ``password``\n* DSN query parameter: ``password``, ``password_file``, ``password_env``\n\nThe password for the :ref:`ref_reference_connection_parameters_user`.\n\n.. _ref_reference_connection_parameters_tls_ca_file:\n\nTLS CA file\n-----------\n\n* Environment variable: :gelenv:`TLS_CA_FILE`\n* CLI flag: ``--tls-ca-file <path>``\n* Client library parameter: ``tlsCAFile`` or ``tls_ca_file``\n* DSN query parameter: ``tls_ca_file``, ``tls_ca_file_file``, ``tls_ca_file_env``\n\nTLS is required to connect to any Gel instance. To do so, the client needs a reference to the root certificate of your instance's certificate chain.  Typically this will be handled for you when you create a local instance or ``link`` a remote one.\n\nIf you're using a globally trusted CA like Let's Encrypt, the root certificate will almost certainly exist already in your system's global certificate pool. In this case, you won't need to specify this path; it will be discovered automatically by the client.\n\nIf you're self-issuing certificates, you must download the root certificate and provide a path to its location on the filesystem. Otherwise TLS will fail to connect.\n\n.. _ref_reference_connection_parameters_tls_server_name:\n\nTLS server name\n---------------\n\n* Environment variable: :gelenv:`TLS_SERVER_NAME`\n* CLI flag: ``--tls-server-name <name>``\n* Client library parameter: ``tlsServerName`` or ``tls_server_name``\n* DSN query parameter: ``tls_server_name``, ``tls_server_name_file``, ``tls_server_name_env``\n\nIf for some reason target instance IP address can't be resolved from the hostname, you can provide the SNI (server name indication) to use for TLS connections.\n\n.. _ref_reference_connection_parameters_tls_security:\n\nTLS security\n------------\n\n* Environment variable: :gelenv:`CLIENT_TLS_SECURITY`\n* CLI flag: ``--tls-security <mode>``\n* Client library parameter: ``tlsSecurity`` or ``tls_security``\n* DSN query parameter: ``tls_security``, ``tls_security_file``, ``tls_security_env``\n* Default value: ``\"strict\"`` or ``\"no_host_verification\"`` depending on whether a custom certificate is supplied\n\nSets the TLS security mode. Determines whether certificate and hostname verification is enabled. Possible values:\n\n- ``\"strict\"`` — certificates and hostnames will be verified\n- ``\"no_host_verification\"`` — verify certificates but not hostnames\n- ``\"insecure\"`` — client libraries will trust self-signed TLS certificates. Useful for self-signed or custom certificates.\n\n.. _ref_reference_connection_parameters_client_security:\n\nClient security\n---------------\n\n* Environment variable: :gelenv:`CLIENT_SECURITY`\n* CLI flag: ``--client-security <mode>``\n* Client library parameter: ``clientSecurity`` or ``client_security``\n\nProvides some simple \"security presets\".\n\nCurrently there is only one valid value: ``insecure_dev_mode``. Setting ``insecure_dev_mode`` disables all TLS security measures. Currently it is equivalent to setting :ref:`ref_reference_connection_parameters_tls_security` to ``insecure`` but it may encompass additional configuration settings later.  This is most commonly used when developing locally with Docker.\n\n.. _ref_reference_connection_parameters_wait_until_available:\n\nWait until available\n--------------------\n\n* Environment variable: :gelenv:`WAIT_UNTIL_AVAILABLE`\n* CLI flag: ``--wait-until-available/-w <timeout>``\n* Client library parameter: ``waitUntilAvailable`` or ``wait_until_available``\n* DSN query parameter: ``wait_until_available``, ``wait_until_available_file``, ``wait_until_available_env``\n* Default value: ``10s``\n\nIf the connection can't be established, keep retrying up to the given timeout value. The timeout value must be given using time units (e.g. ``1hr``, ``10min``, ``30sec``, ``500ms``, etc.) or ISO 8601 duration strings (e.g. ``PT1H``, ``PT10M``, ``PT30S``, ``PT0.5S``, etc.).\n\n.. _ref_reference_connection_parameters_connect_timeout:\n\nConnect timeout\n---------------\n\n* Environment variable: :gelenv:`CONNECT_TIMEOUT`\n* CLI flag: ``--connect-timeout/-c <timeout>``\n* Client library parameter: ``connectTimeout`` or ``connect_timeout``\n* DSN query parameter: ``connect_timeout``, ``connect_timeout_file``, ``connect_timeout_env``\n* Default value: ``10s``\n\nSpecifies a timeout period. In the event Gel doesn't respond in this period, the command will fail (or retry if :ref:`ref_reference_connection_parameters_wait_until_available` is also specified). The timeout value must be given using time units (e.g. ``1hr``, ``10min``, ``30sec``, ``500ms``, etc.) or ISO 8601 duration strings (e.g. ``PT1H``, ``PT10M``, ``PT30S``, ``PT0.5S``, etc.)."
  },
  {
    "path": "docs/reference/using/datetime.rst",
    "content": ".. _ref_bindings_datetime:\n\n==================\nDate/Time Handling\n==================\n\n|Gel| has 6 types related to date and time handling:\n\n* :eql:type:`datetime` (:ref:`binary format <ref_protocol_fmt_datetime>`)\n* :eql:type:`duration` (:ref:`binary format <ref_protocol_fmt_duration>`)\n* :eql:type:`cal::local_datetime` (:ref:`binary format <ref_protocol_fmt_local_datetime>`)\n* :eql:type:`cal::local_date` (:ref:`binary format <ref_protocol_fmt_local_date>`)\n* :eql:type:`cal::relative_duration` (:ref:`binary format <ref_protocol_fmt_relative_duration>`)\n* :eql:type:`cal::date_duration` (:ref:`binary format <ref_protocol_fmt_date_duration>`)\n\nUsually we try to map those types to the respective language-native types, with the following caveats:\n\n* The type is in the standard library\n* It covers the range of years (Gel has timestamps from year 1 to 9999)\n* It has good enough precision (at least microseconds)\n\nIf any of the above criteria is not met, we usually provide a custom type in the client library itself that can be converted to a type from the language's standard library or from a popular third-party library.\n\n\nPrecision\n=========\n\n:eql:type:`datetime`, :eql:type:`duration`, :eql:type:`cal::local_datetime` and :eql:type:`cal::relative_duration` all have precision of **1 microsecond**.\n\nThis means that if language-native type have a bigger precision such as nanosecond, client library has to round that timestamp when encoding it for |Gel|.\n\nWe use **rounding to the nearest even** for that operation. Here are some examples of timestamps with high precision, and how they are stored in the database:\n\n.. code-block:: text\n\n  2022-02-24T05:43:03.123456789Z → 2022-02-24T05:43:03.123457Z\n  2022-02-24T05:43:03.000002345Z → 2022-02-24T05:43:03.000002Z\n  2022-02-24T05:43:03.000002500Z → 2022-02-24T05:43:03.000002Z\n  2022-02-24T05:43:03.000002501Z → 2022-02-24T05:43:03.000003Z\n  2022-02-24T05:43:03.000002499Z → 2022-02-24T05:43:03.000002Z\n  2022-02-24T05:43:03.000001234Z → 2022-02-24T05:43:03.000001Z\n  2022-02-24T05:43:03.000001500Z → 2022-02-24T05:43:03.000002Z\n  2022-02-24T05:43:03.000001501Z → 2022-02-24T05:43:03.000002Z\n  2022-02-24T05:43:03.000001499Z → 2022-02-24T05:43:03.000001Z\n\n.. note::\n\n  A quick refresher on rounding types: If we perform multiple operations of summing while rounding half-up or rounding half-down, the error margin of the resulting value tends to increase. If we round half-to-even instead, the expected value of summing tends to be more accurate.\n\nNote as described in :ref:`datetime protocol documentation <ref_protocol_fmt_datetime>` the value is encoded as a *signed* microseconds delta since a fixed time. Some care must be taken when rounding negative microsecond values.\n\nRounding to the nearest even applies to all operations that client libraries perform, in particular:\n\n1. Encoding timestamps *and* time deltas (see the :ref:`list of types <ref_bindings_datetime>`) to the binary format if precision of the native type is higher than microseconds.\n2. Decoding timestamps *and* time deltas from the binary format is precision of native type is lower than microseconds (applies for JavaScript for example)\n3. Converting from Gel specific type (if there is one) to native type and back (depending on the difference in precision)\n4. Parsing a string to a Gel specific type (this operation is optional to implement, but if it is implemented, it must obey the rules)\n"
  },
  {
    "path": "docs/reference/using/graphql/cheatsheet.rst",
    "content": ".. _ref_cheatsheet_graphql:\n\nCheatsheet\n==========\n\n.. note::\n\n    The types used as examples in these queries are defined :ref:`in our\n    \"Object types\" cheatsheet <ref_cheatsheet_object_types>`.\n\n\n----------\n\nIn order to set up GraphQL access to the database, add the following to\nthe schema:\n\n.. code-block:: sdl\n\n    using extension graphql;\n\nThen create a new migration and apply it using\n:ref:`ref_cli_gel_migration_create` and\n:ref:`ref_cli_gel_migrate`, respectively.\n\nSelect all users in the system:\n\n.. code-block:: graphql\n\n    {\n        User {\n            id\n            name\n            image\n        }\n    }\n\n\n----------\n\n\nSelect a movie by title and release year with associated actors\nordered by last name:\n\n.. code-block:: graphql\n\n    {\n        Movie(\n            filter: {\n                title: {eq: \"Dune\"},\n                year: {eq: 2020}\n            }\n        ) {\n            id\n            title\n            year\n            description\n\n            directors {\n                id\n                full_name\n            }\n\n            actors(order: {last_name: {dir: ASC}}) {\n                id\n                full_name\n            }\n        }\n    }\n\n\n----------\n\n\nSelect movies with Keanu Reeves:\n\n.. code-block:: graphql\n\n    {\n        Movie(\n            filter: {\n                actors: {full_name: {eq: \"Keanu Reeves\"}}\n            }\n        ) {\n            id\n            title\n            year\n            description\n        }\n    }\n\n\n\n----------\n\n\nSelect a movie by title and year with top 3 most recent reviews (this\nuses :ref:`MovieAlias <ref_cheatsheet_aliases>` in order to access\nreviews):\n\n.. code-block:: graphql\n\n    {\n        MovieAlias(\n            filter: {\n                title: {eq: \"Dune\"},\n                year: {eq: 2020}\n            }\n        ) {\n            id\n            title\n            year\n            description\n            reviews(\n                order: {creation_time: {dir: DESC}},\n                first: 3\n            ) {\n                id\n                body\n                rating\n                creation_time\n                author {\n                    id\n                    name\n                }\n            }\n        }\n    }\n\n\n----------\n\n\nUse :ref:`MovieAlias <ref_cheatsheet_aliases>` in order to find\nmovies that have no reviews:\n\n.. code-block:: graphql\n\n    {\n        MovieAlias(\n            filter: {\n                reviews: {exists: false},\n            }\n        ) {\n            id\n            title\n            year\n            description\n        }\n    }\n\n\n----------\n\n\nUse a GraphQL :ref:`mutation <ref_graphql_mutations>` to add a user:\n\n.. code-block:: graphql\n\n    mutation add_user {\n        insert_User(\n            data: {name: \"Atreides\", image: \"atreides.jpg\"}\n        ) {\n            id\n        }\n    }\n\n\n----------\n\n\nUse a GraphQL :ref:`mutation <ref_graphql_mutations>` to add a review\nby an existing user:\n\n.. code-block:: graphql\n\n    mutation add_review {\n        insert_Review(\n            data: {\n                # Since the movie already exists,\n                # we select it using the same filter\n                # mechanism as for queries.\n                movie: {\n                    filter: {title: {eq: \"Dune\"}, year: {eq: 2020}},\n                    first: 1\n                },\n                body: \"Yay!\",\n                rating: 5,\n                # Similarly to the movie we select\n                # the existing user.\n                author: {\n                    filter: {name: {eq: \"Atreides\"}},\n                    first: 1\n                }\n            }\n        ) {\n            id\n            body\n        }\n    }\n\n\n----------\n\n\nUse a GraphQL :ref:`mutation <ref_graphql_mutations>` to add an\nactress to a movie:\n\n.. code-block:: graphql\n\n    mutation add_actor {\n        update_Movie(\n            # Specify which movie needs to be updated.\n            filter: {title: {eq: \"Dune\"}, year: {eq: 2020}},\n            # Specify the movie data to be updated.\n            data: {\n                actors: {\n                    add: [{\n                        filter: {\n                            full_name: {eq: \"Charlotte Rampling\"}\n                        }\n                    }]\n                }\n            }\n        ) {\n            id\n            actors {\n                id\n            }\n        }\n    }\n"
  },
  {
    "path": "docs/reference/using/graphql/graphql.rst",
    "content": ".. _ref_graphql_overview:\n\n\nBasics\n======\n\nFor the purposes of this section, we will consider a ``default`` module\ncontaining the following schema:\n\n.. code-block:: sdl\n\n    type Author {\n        name: str;\n    }\n\n    type Book {\n        # to make the examples simpler only the title is\n        # a required property\n        required title: str;\n        synopsis: str;\n        author: Author;\n        isbn: str {\n            constraint max_len_value(10);\n        }\n    }\n\nFrom the schema above, |Gel| will expose to GraphQL:\n\n* object types ``Author`` and ``Book``\n* scalars ``String`` and ``ID``\n\nIn addition to this, the ``Query`` will have two fields — ``Author``, and\n``Book`` — to query these types respectively.\n\n\nQueries\n+++++++\n\nConsider this example:\n\n.. table::\n    :class: codeblocks\n\n    +---------------------------------+---------------------------------+\n    | GraphQL                         | EdgeQL equivalent               |\n    +=================================+=================================+\n    | .. code-block:: graphql         | .. code-block:: edgeql          |\n    |                                 |                                 |\n    |     {                           |     select                      |\n    |         Book {                  |         Book {                  |\n    |             title               |             title,              |\n    |             synopsis            |             synopsis,           |\n    |             author {            |             author: {           |\n    |                 name            |                 name            |\n    |             }                   |             }                   |\n    |         }                       |         };                      |\n    |     }                           |                                 |\n    +---------------------------------+---------------------------------+\n\nThe top-level field of the GraphQL query must be a valid\nname of an object type or an expression alias of something returning an\nobject type. Nested fields must be valid links or properties.\n\nThere are some specific conventions as to how *arguments* in GraphQL\nqueries are used to allow filtering, ordering, and paginating data.\n\n\n.. _ref_graphql_overview_filter:\n\nFiltering\n---------\n\nFiltering the retrieved data is done by specifying a ``filter``\nargument. The ``filter`` argument is customized to each specific type\nbased on the available fields. In case of the sample schema, here are\nthe specifications for available filter arguments for querying ``Book``:\n\n.. code-block:: graphql-schema\n\n    # this is Book-specific\n    input FilterBook {\n        # basic boolean operators that combine conditions\n        and: [FilterBook!]\n        or: [FilterBook!]\n        not: FilterBook\n\n        # fields available for filtering (properties in EdgeQL)\n        title: FilterString\n        synopsis: FilterString\n        isbn: FilterString\n        author: NestedFilterAuthor\n    }\n\n    # this is Author-specific\n    input NestedFilterAuthor {\n        # instead of boolean operations, \"exists\" check is available\n        # for links\n        exists: Boolean\n\n        # fields available for filtering (properties in EdgeQL)\n        name: FilterString\n    }\n\n    # this is generic\n    input FilterString {\n        # \"exists\" check is available for every property, too\n        exists: Boolean\n\n        # equality\n        eq: String\n        neq: String\n\n        # lexicographical comparison\n        gt: String\n        gte: String\n        lt: String\n        lte: String\n\n        # other useful operations\n        like: String\n        ilike: String\n    }\n\nHere are some examples of using a filter:\n\n.. table::\n    :class: codeblocks\n\n    +---------------------------------+---------------------------------+\n    | GraphQL                         | EdgeQL equivalent               |\n    +=================================+=================================+\n    | .. code-block:: graphql         | .. code-block:: edgeql          |\n    |                                 |                                 |\n    |     {                           |     select                      |\n    |         Book(                   |         Book {                  |\n    |             filter: {           |             title,              |\n    |                 title: {        |             synopsis            |\n    |                     eq: \"Spam\"  |         }                       |\n    |                 }               |     filter                      |\n    |             }                   |         Book.title = 'Spam';    |\n    |         ) {                     |                                 |\n    |             title               |                                 |\n    |             synopsis            |                                 |\n    |         }                       |                                 |\n    |     }                           |                                 |\n    +---------------------------------+---------------------------------+\n    | .. code-block:: graphql         | .. code-block:: edgeql          |\n    |                                 |                                 |\n    |     {                           |     select                      |\n    |         Book(                   |         Book {                  |\n    |             filter: {           |             title,              |\n    |                 author: {       |             synopsis            |\n    |                     name: {     |         }                       |\n    |                         eq:     |     filter                      |\n    |                 \"Lewis Carroll\" |         Book.author.name =      |\n    |                     }           |             'Lewis Carroll';    |\n    |                 }               |                                 |\n    |             }                   |                                 |\n    |         ) {                     |                                 |\n    |             title               |                                 |\n    |             synopsis            |                                 |\n    |         }                       |                                 |\n    |     }                           |                                 |\n    +---------------------------------+---------------------------------+\n\nIt is legal to provide multiple input fields in the same input object.\nThey are all implicitly combined using a logical conjunction. For\nexample:\n\n.. table::\n    :class: codeblocks\n\n    +---------------------------------+---------------------------------+\n    | GraphQL                         | EdgeQL equivalent               |\n    +=================================+=================================+\n    | .. code-block:: graphql         | .. code-block:: edgeql          |\n    |                                 |                                 |\n    |     {                           |     select                      |\n    |         Book(                   |         Book {                  |\n    |             filter: {           |             title,              |\n    |                 title: {        |         }                       |\n    |                     gte: \"m\",   |     filter                      |\n    |                     lt: \"o\"     |         Book.title >= 'm'       |\n    |                 }               |         and                     |\n    |             }                   |         Book.title < 'o';       |\n    |         ) {                     |                                 |\n    |             title               |                                 |\n    |         }                       |                                 |\n    |     }                           |                                 |\n    +---------------------------------+---------------------------------+\n\n\nIt is possible to search for books that don't specify the author:\n\n.. table::\n    :class: codeblocks\n\n    +---------------------------------+---------------------------------+\n    | GraphQL                         | EdgeQL equivalent               |\n    +=================================+=================================+\n    | .. code-block:: graphql         | .. code-block:: edgeql          |\n    |                                 |                                 |\n    |     {                           |     select                      |\n    |         Book(                   |         Book {                  |\n    |             filter: {           |             id,                 |\n    |                 author: {       |             title               |\n    |                   exists: false |         }                       |\n    |                 }               |     filter                      |\n    |             }                   |         not exists              |\n    |         ) {                     |             Book.author;        |\n    |             id                  |                                 |\n    |             title               |                                 |\n    |         }                       |                                 |\n    |     }                           |                                 |\n    +---------------------------------+---------------------------------+\n\n\n.. _ref_graphql_overview_order:\n\nOrdering\n--------\n\nOrdering the retrieved data is done by specifying an ``order``\nargument. The ``order`` argument is customized to each specific type\nbased on the available fields, much like the ``filter``. In case of\nthe sample schema, here are the specifications for the available\nfilter arguments:\n\n.. code-block:: graphql-schema\n\n    # this is Author-specific\n    input OrderAuthor {\n        # fields available for ordering (properties in EdgeQL)\n        name: Ordering\n    }\n\n    # this is Book-specific\n    input OrderBook {\n        # fields available for ordering (properties in EdgeQL)\n        title: Ordering\n        synopsis: Ordering\n        isbn: Ordering\n    }\n\n    # this is generic\n    input Ordering {\n        dir: directionEnum\n        nulls: nullsOrderingEnum\n    }\n\n    enum directionEnum {\n        ASC\n        DESC\n    }\n\n    enum nullsOrderingEnum {\n        SMALLEST    # null < any other value\n        BIGGEST     # null > any other value\n    }\n\nIf the value of ``nulls`` is not specified it is assumed to be\n``SMALLEST``.\n\n.. table::\n    :class: codeblocks\n\n    +------------------------------------+------------------------------+\n    | GraphQL                            | EdgeQL equivalent            |\n    +====================================+==============================+\n    | .. code-block:: graphql            | .. code-block:: edgeql       |\n    |                                    |                              |\n    |     {                              |     select                   |\n    |         Author(                    |         Author {             |\n    |             order: {               |             name,            |\n    |                 name: {            |         }                    |\n    |                     dir: ASC,      |     order by                 |\n    |                     nulls: BIGGEST |         Author.name asc      |\n    |                 }                  |             empty last;      |\n    |             }                      |                              |\n    |         ) {                        |                              |\n    |             name                   |                              |\n    |         }                          |                              |\n    |     }                              |                              |\n    +------------------------------------+------------------------------+\n\n\n.. _ref_graphql_overview_pagination:\n\nPaginating\n----------\n\nPaginating the retrieved data is done by providing one or more of the\nfollowing arguments: ``first``, ``last``, ``before``, and ``after``.\nThe pagination works in a similar way to Relay Connections. In case of\nthe sample schema, here are the specifications for the available\nfilter arguments:\n\n.. code-block:: graphql-schema\n\n    # a relevant Query definition snippet\n    type Query {\n        Author(\n            filter: FilterAuthor,\n            order: OrderAuthor,\n\n            after: String,\n            before: String,\n            first: Int,\n            last: Int,\n        ): [Author!]\n\n        # ... other Query fields\n    }\n\nThe ``after`` and ``before`` strings are, in fact, string\nrepresentations of numeric indices under the particular filter and\nordering (starting at \"0\"). This makes the usage fairly intuitive even\nwithout having Relay Connection edges and cursors.\n\nThe objects corresponding to the indices specified by ``before`` or\n``after`` are not included.\n\n.. table::\n    :class: codeblocks\n\n    +---------------------------------+---------------------------------+\n    | GraphQL                         | EdgeQL equivalent               |\n    +=================================+=================================+\n    | .. code-block:: graphql         | .. code-block:: edgeql          |\n    |                                 |                                 |\n    |     {                           |     select                      |\n    |         Author(                 |         Author {                |\n    |             order: {            |             name,               |\n    |                 name: {         |         }                       |\n    |                     dir: ASC    |     order by                    |\n    |                 }               |         Author.name asc         |\n    |             },                  |     limit 10;                   |\n    |             first: 10           |                                 |\n    |         ) {                     |                                 |\n    |             name                |                                 |\n    |         }                       |                                 |\n    |     }                           |                                 |\n    +---------------------------------+---------------------------------+\n    | .. code-block:: graphql         | .. code-block:: edgeql          |\n    |                                 |                                 |\n    |     {                           |     select                      |\n    |         Author(                 |         Author {                |\n    |             order: {            |             name,               |\n    |                 name: {         |         }                       |\n    |                     dir: ASC    |     order by                    |\n    |                 }               |         Author.name asc         |\n    |             },                  |     offset 20 limit 10;         |\n    |             after: \"19\",        |                                 |\n    |             first: 10           |                                 |\n    |         ) {                     |                                 |\n    |             name                |                                 |\n    |         }                       |                                 |\n    |     }                           |                                 |\n    +---------------------------------+---------------------------------+\n    | .. code-block:: graphql         | .. code-block:: edgeql          |\n    |                                 |                                 |\n    |     {                           |     select                      |\n    |         Author(                 |         Author {                |\n    |             order: {            |             name,               |\n    |                 name: {         |         }                       |\n    |                     dir: ASC    |     order by                    |\n    |                 }               |         Author.name asc         |\n    |             },                  |     offset 20 limit 10;         |\n    |             after: \"19\",        |                                 |\n    |             before: \"30\"        |                                 |\n    |         ) {                     |                                 |\n    |             name                |                                 |\n    |         }                       |                                 |\n    |     }                           |                                 |\n    +---------------------------------+---------------------------------+\n\n\nVariables\n---------\n\nIt is possible to use variables within GraphQL queries. They are\nmapped to variables in EdgeQL.\n\n.. table::\n    :class: codeblocks\n\n    +---------------------------------+---------------------------------+\n    | GraphQL                         | EdgeQL equivalent               |\n    +=================================+=================================+\n    | .. code-block:: graphql         | .. code-block:: edgeql          |\n    |                                 |                                 |\n    |     query ($title: String!) {   |     select                      |\n    |         Book(                   |        Book {                   |\n    |           filter: {             |            title,               |\n    |             title: {            |            synopsis,            |\n    |               eq: $title        |        }                        |\n    |             }                   |     filter                      |\n    |           }                     |         .title = $title;        |\n    |         ) {                     |                                 |\n    |             title               |                                 |\n    |             synopsis            |                                 |\n    |         }                       |                                 |\n    |     }                           |                                 |\n    |                                 |                                 |\n    +---------------------------------+---------------------------------+\n"
  },
  {
    "path": "docs/reference/using/graphql/index.rst",
    "content": ".. _ref_graphql_index:\n\n=======\nGraphQL\n=======\n\n\n.. toctree::\n    :maxdepth: 2\n    :hidden:\n\n    graphql\n    mutations\n    introspection\n    cheatsheet\n\n\n|Gel| supports `GraphQL queries`__ via the built-in ``graphql`` extension. A\nfull CRUD API for all object types, their properties (both material and\ncomputed), their links, and all :ref:`aliases <ref_datamodel_aliases>` is\nreflected in the GraphQL schema.\n\nSetting up the extension\n------------------------\n\nIn order to set up GraphQL access to the database, add the following to the\nschema:\n\n.. code-block:: sdl\n\n    using extension graphql;\n\nThen create a new migration and apply it.\n\n.. code-block:: bash\n\n  $ gel migration create\n  $ gel migrate\n\nRefer to the :ref:`connection docs <gel_client_connection>` for various\nmethods of running these commands against remotely-hosted instances.\n\nConnection\n----------\n\nOnce you've activated the extension, your instance will listen for incoming\nGraphQL queries via HTTP at the following URL.\n\n``http://<instance-hostname><instance-port>/branch/<branch-name>/graphql``\n\nThe default ``branch-name`` will be |main|, and after initializing your\ndatabase, all queries are executed against it by default. If you want to query\nanother branch instead, simply use that branch name in the URL.\n\nTo find the port number associated with a local instance, run\n:gelcmd:`instance list`.\n\n.. code-block:: bash\n\n  $ gel instance list\n  ┌────────┬──────────────┬──────────┬───────────────┬─────────────┐\n  │ Kind   │ Name         │ Port     │ Version       │ Status      │\n  ├────────┼──────────────┼──────────┼───────────────┼─────────────┤\n  │ local  │ inst1        │ 10700    │ 6.x           │ running     │\n  │ local  │ inst2        │ 10702    │ 6.x           │ running     │\n  │ local  │ inst3        │ 10703    │ 6.x           │ running     │\n  └────────┴──────────────┴──────────┴───────────────┴─────────────┘\n\nTo execute a GraphQL query against the |main| branch on the instance\nnamed ``inst2``, we would send an HTTP request to\n``http://localhost:10702/branch/gel/main``.\n\nTo determine the URL of a |Gel| Cloud instance, find the host by running\n:gelcmd:`instance credentials -I <org-name>/<instance-name>`. Use the\n``host`` and ``port`` from that table in the URL format at the top of this\nsection. Change the protocol to ``https`` since Gel Cloud instances are\nsecured with TLS.\n\n.. note::\n\n  The endpoint also provides a `GraphiQL`_ interface to explore the GraphQL\n  schema and write queries. Take the GraphQL query endpoint, append\n  ``/explore``, and visit that URL in the browser. Under the above example,\n  the GraphiQL endpoint is available at\n  ``http://localhost:10702/branch/main/graphql/explore``.\n\nAuthentication\n--------------\n\n.. versionadded:: 4.0\n\nAuthentication for the GraphQL endpoint is identical to that for the\n:ref:`EdgeQL HTTP endpoint <ref_http_auth>`.\n\n.. _ref_graphql_protocol:\n\nThe protocol\n------------\n\n|Gel| can recieve GraphQL queries via both ``GET`` and ``POST`` requests.\nRequests can contain the following fields:\n\n- ``query`` - the GraphQL query string\n- ``variables`` - a JSON object containing a set of variables. **Optional**\n  If the GraphQL query string contains variables, the ``variables`` object is\n  required.\n- ``variables[\"__globals__\"]`` - a JSON object containing global variables.\n  **Optional**. The\n  keys must be the fully qualified names of the globals to set (e.g.,\n  ``default::current_user`` for the global ``current_user`` in the ``default``\n  module).\n- ``variables[\"__config__\"]`` - a JSON object containing session configuration values. **Optional**.  **Added in 7.0.**\n- ``operationName`` - the name of the operation that must be\n  executed. **Optional** If the GraphQL query contains several named\n  operations, it is required.\n- ``globals`` - **Deprecated** and non-standard synonym for ``variables[\"__globals__\"]``.\n\n.. note::\n\n  The protocol implementations conform to the official GraphQL `HTTP protocol\n  <https://graphql.org/learn/serving-over-http/>`_. The protocol supports\n  ``HTTP Keep-Alive``.\n\n\nPOST request (recommended)\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nThe POST request should use ``application/json`` content type and\nsubmit the following JSON-encoded form with the necessary fields.\n\n.. lint-off\n\n.. code-block:: bash\n\n  $ curl \\\n      -H \"Content-Type: application/json\" \\\n      -X POST http://localhost:10787/branch/main/graphql \\\n      -d '{ \"query\": \"query getMovie($title: String!) { Movie(filter: {title:{eq: $title}}) { id title }}\", \"variables\": { \"title\": \"The Batman\" , \"__globals__\": {\"default::current_user\": \"04e52807-6835-4eaa-999b-952804ab40a5\"}}}'\n  {\"data\": {...}}\n\n.. lint-on\n\n\nGET request\n^^^^^^^^^^^\n\nWhen using ``GET`` requests, any values for ``query``, ``variables``,\n``globals``, or ``operationName`` should be passed as query parameters in the\nURL.\n\n.. lint-off\n\n.. code-block:: bash\n\n  $ curl \\\n      -H application/x-www-form-urlencoded \\\n      -X GET http://localhost:10787/branch/main/graphql \\\n      -G \\\n      --data-urlencode 'query=query getMovie($title: String!) { Movie(filter: {title:{eq: $title}}) { id title }}' \\\n      --data-urlencode 'variables={ \"title\": \"The Batman\" }'\n      --data-urlencode 'globals={ \"default::current_user\": \"04e52807-6835-4eaa-999b-952804ab40a5\" }'\n  {\"data\": {...}}\n\n.. lint-on\n\n\nResponse format\n^^^^^^^^^^^^^^^\n\nThe body of the response is JSON in the following format:\n\n.. code-block::\n\n  {\n    \"data\": { ... },\n    \"errors\": [\n      { \"message\": \"Error message\"}, ...\n    ]\n  }\n\nNote that the ``errors`` field will only be present if some errors\nactually occurred.\n\n.. note::\n\n    Caution is advised when reading ``decimal`` or ``bigint`` values\n    (mapped onto ``Decimal`` and ``Bigint`` GraphQL custom scalar\n    types) using HTTP protocol because the results are provides in\n    JSON format. The JSON specification does not have a limit on\n    significant digits, so a ``decimal`` or a ``bigint`` number can be\n    losslessly represented in JSON. However, JSON decoders in many\n    languages will read all such numbers as some kind of of 32- or\n    64-bit number type, which may result in errors or precision loss.\n    If such loss is unacceptable, then consider creating a computed\n    property which casts the value into ``str`` and decoding it on the\n    client side into a more appropriate type.\n\n.. _ref_graphql_limitations:\n\nKnown limitations\n-----------------\n\nWe provide this GraphQL extension to support users who are accustomed to\nwriting queries in GraphQL. That said, GraphQL is quite limited and verbose\nrelative to EdgeQL.\n\nThere are also some additional limitations:\n\n- Variables can only correspond to *scalar types*; you can't use\n  GraphQL ``input`` types. Under the hood, query variables are mapped onto\n  EdgeQL parameters, which only support scalar types.\n\n  As a consequence of this, you must declare top-level variables for each\n  property for a GraphQL insertion mutation, which can make queries more\n  verbose.\n\n- Due to the differences between EdgeQL and GraphQL syntax,\n  :eql:type:`enum <std::enum>` types which have values that cannot be\n  represented as GraphQL identifiers (e.g. ```N/A``` or ```NOT\n  APPLICABLE```) cannot be properly reflected into GraphQL enums.\n\n- Inserting or updating tuple properties is not yet supported.\n\n- :ref:`Link properties<ref_datamodel_link_properties>` are not reflected, as\n  GraphQL has no such concept.\n\n- Every non-abstract |Gel| object type is simultaneously an interface\n  and an object in terms of the GraphQL type system, which means that, for\n  every one object type name, two names are needed in reflected\n  GraphQL. This potentially results in name clashes if the convention\n  of using camel-case names for user types is not followed in Gel.\n\n\n.. __: http://graphql.org/docs/queries/\n\n.. _`GraphiQL`: https://github.com/graphql/graphiql\n"
  },
  {
    "path": "docs/reference/using/graphql/introspection.rst",
    "content": ".. _ref_graphql_introspection:\n\n\nIntrospection\n=============\n\nGraphQL introspection can be used to explore the exposed |Gel| types\nand expresssion aliases. Note that there are certain types like\n:eql:type:`tuple` that cannot be expressed in terms of the GraphQL\ntype system (a ``tuple`` can be like a heterogeneous \"List\").\n\nConsider the following GraphQL introspection query:\n\n.. code-block:: graphql\n\n    {\n        __type(name: \"Query\") {\n            name\n            fields {\n                name\n                args {\n                    name\n                    type {\n                        kind\n                        name\n                    }\n                }\n            }\n        }\n    }\n\nProduces:\n\n.. code-block:: json\n\n    {\n        \"__type\": {\n            \"name\": \"Query\",\n            \"fields\": [\n                {\n                    \"name\": \"Author\",\n                    \"args\": [\n                        {\n                            \"name\": \"id\",\n                            \"type\": {\n                                \"kind\": \"SCALAR\",\n                                \"name\": \"ID\"\n                            }\n                        },\n                        {\n                            \"name\": \"name\",\n                            \"type\": {\n                                \"kind\": \"SCALAR\",\n                                \"name\": \"String\"\n                            }\n                        }\n                    ]\n                },\n                {\n                    \"name\": \"Book\",\n                    \"args\": [\n                        {\n                            \"name\": \"id\",\n                            \"type\": {\n                                \"kind\": \"SCALAR\",\n                                \"name\": \"ID\"\n                            }\n                        },\n                        {\n                            \"name\": \"isbn\",\n                            \"type\": {\n                                \"kind\": \"SCALAR\",\n                                \"name\": \"String\"\n                            }\n                        },\n                        {\n                            \"name\": \"synopsis\",\n                            \"type\": {\n                                \"kind\": \"SCALAR\",\n                                \"name\": \"String\"\n                            }\n                        },\n                        {\n                            \"name\": \"title\",\n                            \"type\": {\n                                \"kind\": \"SCALAR\",\n                                \"name\": \"String\"\n                            }\n                        }\n                    ]\n                }\n            ]\n        }\n    }\n\nThe above example shows what has been exposed for querying with GraphQL.\n"
  },
  {
    "path": "docs/reference/using/graphql/mutations.rst",
    "content": ".. _ref_graphql_mutations:\n\n\nMutations\n=========\n\n|Gel| provides GraphQL mutations to perform ``delete``, ``insert``\nand ``update`` operations.\n\n\nDelete\n------\n\nThe \"delete\" mutation is very similar in structure to a query.\nBasically, it works the same way as a query, using the\n:ref:`filter <ref_graphql_overview_filter>`,\n:ref:`order <ref_graphql_overview_order>`, and various\n:ref:`pagination parameters <ref_graphql_overview_pagination>` to\ndefine a set of objects to be deleted. These objects are also\nreturned as the result of the delete mutation. Each object type\nhas a corresponding ``delete_<type>`` mutation:\n\n.. table::\n    :class: codeblocks\n\n    +---------------------------------+---------------------------------+\n    | GraphQL                         | EdgeQL equivalent               |\n    +=================================+=================================+\n    | .. code-block:: graphql         | .. code-block:: edgeql          |\n    |                                 |                                 |\n    |     mutation delete_all_books { |     select (                    |\n    |         delete_Book {           |         delete Book             |\n    |             title               |     ) {                         |\n    |             synopsis            |         title,                  |\n    |             author {            |         synopsis,               |\n    |                 name            |         author: {               |\n    |             }                   |             name                |\n    |         }                       |         }                       |\n    |     }                           |     };                          |\n    +---------------------------------+---------------------------------+\n    | .. code-block:: graphql         | .. code-block:: edgeql          |\n    |                                 |                                 |\n    |     mutation delete_book_spam { |     select (                    |\n    |         delete_Book(            |         delete Book             |\n    |             filter: {           |         filter                  |\n    |                 title: {        |             Book.title = 'Spam' |\n    |                     eq: \"Spam\"  |     ) {                         |\n    |                 }               |         title,                  |\n    |             }                   |         synopsis                |\n    |         ) {                     |     };                          |\n    |             title               |                                 |\n    |             synopsis            |                                 |\n    |         }                       |                                 |\n    |     }                           |                                 |\n    +---------------------------------+---------------------------------+\n    | .. code-block:: graphql         | .. code-block:: edgeql          |\n    |                                 |                                 |\n    |     mutation delete_one_book {  |     select (                    |\n    |         delete_Book(            |         delete Book             |\n    |             filter: {           |         filter                  |\n    |                 author: {       |             Book.author.name =  |\n    |                     name: {     |                 'Lewis Carroll' |\n    |                         eq:     |         order by                |\n    |                 \"Lewis Carroll\" |             Book.title ASC      |\n    |                     }           |         limit 1                 |\n    |                 }               |     ) {                         |\n    |             },                  |         title,                  |\n    |             order: {            |         synopsis                |\n    |                 title: {        |     };                          |\n    |                     dir: ASC    |                                 |\n    |                 }               |                                 |\n    |             },                  |                                 |\n    |             first: 1            |                                 |\n    |         ) {                     |                                 |\n    |             title               |                                 |\n    |             synopsis            |                                 |\n    |         }                       |                                 |\n    |     }                           |                                 |\n    +---------------------------------+---------------------------------+\n\nInsert\n------\n\nThe \"insert\" mutation exists for every object type. It allows creating\nnew objects and supports nested insertions, too. The objects to be\ninserted are specified via the ``data`` parameter, which takes a list\nof specifications. Each such specification has the same structure as\nthe object being inserted with required and optional fields (although\nif a field is required in the object but has a default, it's optional\nin the insert specification):\n\n.. table::\n    :class: codeblocks\n\n    +---------------------------------+---------------------------------+\n    | GraphQL                         | EdgeQL equivalent               |\n    +=================================+=================================+\n    | .. code-block:: graphql         | .. code-block:: edgeql          |\n    |                                 |                                 |\n    |     mutation insert_books {     |     select {                    |\n    |         insert_Book(            |         (insert Book {          |\n    |             data: [{            |             title := \"One\"      |\n    |                 title: \"One\"    |         }),                     |\n    |             }, {                |         (insert Book {          |\n    |                 title: \"Two\"    |             title := \"Two\"      |\n    |             }]                  |         })                      |\n    |         ) {                     |     } {                         |\n    |             id                  |         id,                     |\n    |             title               |         title                   |\n    |         }                       |     };                          |\n    |     }                           |                                 |\n    +---------------------------------+---------------------------------+\n\nIt's possible to insert a nested structure all at once (e.g., a new\nbook and a new author):\n\n.. table::\n    :class: codeblocks\n\n    +---------------------------------+---------------------------------+\n    | GraphQL                         | EdgeQL equivalent               |\n    +=================================+=================================+\n    | .. code-block:: graphql         | .. code-block:: edgeql          |\n    |                                 |                                 |\n    |     mutation insert_books {     |     select (                    |\n    |         insert_Book(            |         insert Book {           |\n    |             data: [{            |             title := \"Three\",   |\n    |                 title: \"Three\", |             author := (         |\n    |                 author: {       |                 insert Author { |\n    |                     data: {     |                     name :=     |\n    |                         name:   |                     \"Unknown\"   |\n    |                     \"Unknown\"   |                 }               |\n    |                     }           |             )                   |\n    |                 }               |        }                        |\n    |             }]                  |     ) {                         |\n    |         ) {                     |         id,                     |\n    |             id                  |         title                   |\n    |             title               |     };                          |\n    |         }                       |                                 |\n    |     }                           |                                 |\n    +---------------------------------+---------------------------------+\n\nIt's also possible to insert a new object that's connected to an\nexisting object (e.g. a new book by an existing author). In this case\nthe nested object is specified using :ref:`filter\n<ref_graphql_overview_filter>`,\n:ref:`order <ref_graphql_overview_order>`, and various\n:ref:`pagination parameters <ref_graphql_overview_pagination>` to\ndefine a set of objects to be connected:\n\n.. table::\n    :class: codeblocks\n\n    +---------------------------------+---------------------------------+\n    | GraphQL                         | EdgeQL equivalent               |\n    +=================================+=================================+\n    | .. code-block:: graphql         | .. code-block:: edgeql          |\n    |                                 |                                 |\n    |     mutation insert_book {      |     select (                    |\n    |         insert_Book(            |         insert Book {           |\n    |             data: [{            |             title := \"Four\",    |\n    |                 title: \"Four\",  |             author := (         |\n    |                 author: {       |                 select Author   |\n    |                     filter: {   |                 filter          |\n    |         name: {eq: \"Unknown\"}   |                 Author.name =   |\n    |                     }           |                     \"Unknown\"   |\n    |                 }               |             )                   |\n    |             }]                  |         }                       |\n    |         ) {                     |     ) {                         |\n    |             id                  |         id,                     |\n    |             title               |         title                   |\n    |         }                       |     };                          |\n    |     }                           |                                 |\n    +---------------------------------+---------------------------------+\n\nUpdate\n------\n\nThe \"update\" mutation has features that are similar to both an\n\"insert\" mutation and a query. On one hand, the mutation takes\n:ref:`filter <ref_graphql_overview_filter>`,\n:ref:`order <ref_graphql_overview_order>`, and various\n:ref:`pagination parameters <ref_graphql_overview_pagination>` to\ndefine a set of objects to be updated. On the other hand, the ``data``\nparameter is used to specify what and how should be updated.\n\nThe ``data`` parameter contains the fields that should be altered as\nwell as what type of update operation must be performed (``set``,\n``increment``, ``append``, etc.). The particular operations available\ndepend on the type of field being updated.\n\n.. table::\n    :class: codeblocks\n\n    +---------------------------------+---------------------------------+\n    | GraphQL                         | EdgeQL equivalent               |\n    +=================================+=================================+\n    | .. code-block:: graphql         | .. code-block:: edgeql          |\n    |                                 |                                 |\n    |     mutation update_book {      |     with                        |\n    |         update_Book(            |         Upd := (                |\n    |             filter: {           |             update Book         |\n    |                 title: {        |             filter              |\n    |                     eq: \"One\"   |                 Book.title =    |\n    |                 }               |                     \"One\"       |\n    |             }                   |             set {               |\n    |             data: {             |                 synopsis :=     |\n    |                 synopsis: {     |                     \"TBD\",      |\n    |                     set: \"TBD\"  |                 author := (     |\n    |                 }               |                 select Author   |\n    |                 author: {       |                 filter          |\n    |                     set: {      |                 Author.name =   |\n    |             filter: {           |                     \"Unknown\"   |\n    |                 name: {         |                 )               |\n    |                     eq:         |             }                   |\n    |                     \"Unknown\"   |         )                       |\n    |                 }               |     select Upd {                |\n    |             }                   |         id,                     |\n    |                     }           |         title                   |\n    |                 }               |     };                          |\n    |             }                   |                                 |\n    |         ) {                     |                                 |\n    |             id                  |                                 |\n    |             title               |                                 |\n    |         }                       |                                 |\n    |     }                           |                                 |\n    +---------------------------------+---------------------------------+\n"
  },
  {
    "path": "docs/reference/using/http.rst",
    "content": ".. _ref_reference_http_querying:\n.. _ref_edgeql_http:\n\n================\nEdgeQL over HTTP\n================\n\n|Gel| can expose an HTTP endpoint for EdgeQL queries. Since HTTP is a stateless protocol, no :ref:`DDL <ref_eql_ddl>`, :ref:`transaction commands <ref_eql_statements_start_tx>`, can be executed using this endpoint.  Only one query per request can be executed.\n\nSetup\n=====\n\nIn order to set up HTTP access to the database add the following to the schema:\n\n.. code-block:: sdl\n\n    using extension edgeql_http;\n\nThen create a new migration and apply it using :ref:`ref_cli_gel_migration_create` and :ref:`ref_cli_gel_migrate`, respectively.\n\nYour instance can now receive EdgeQL queries over HTTP at ``https://<hostname>:<port>/branch/<branch-name>/edgeql``.\n\n.. note::\n\n  Here's how to determine your local |Gel| instance's HTTP server URL:\n\n  - The ``hostname`` will be ``localhost``\n  - Find the ``port`` by running :gelcmd:`instance list`. This will print a table of all |Gel| instances on your machine, including their associated port number.\n  - The default ``branch-name`` will be |main|, and after initializing your database, all queries are executed against it by default. If you want to query another branch instead, simply use that branch name in the URL.\n\n  To determine the URL of a |Gel| Cloud instance, find the host by running :gelcmd:`instance credentials -I <org-name>/<instance-name>`. Use the ``host`` and ``port`` from that table in the URL format above this note.  Change the protocol to ``https`` since Gel Cloud instances are secured with TLS.\n\n  To determine the URL of a self-hosted remote instance you have linked with the CLI, you can get both the hostname and port of the instance from the \"Port\" column of the :gelcmd:`instance list` table (formatted as ``<hostname>:<port>``). The same guidance on local branch names applies here.\n\n.. _ref_http_auth:\n\nAuthentication\n==============\n\n.. versionadded:: 4.0\n\n.. lint-off\n\nBy default, the HTTP endpoint uses :eql:type:`cfg::Password` based\nauthentication, in which\n`HTTP Basic Authentication\n<https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme>`_\nis used to provide a |Gel| username and password.\n\n.. lint-on\n\nThis is configurable, however: the HTTP endpoint's authentication\nmechanism can be configured by adjusting which\n:eql:type:`cfg::AuthMethod` applies to the ``SIMPLE_HTTP``\n:eql:type:`cfg::ConnectionTransport`.\n\nIf :eql:type:`cfg::JWT` is used, the requests should contain these headers:\n\n* ``X-EdgeDB-User``: The |Gel| username.\n\n* ``Authorization``: The JWT authorization token prefixed by ``Bearer``.\n\n\nIf :eql:type:`cfg::Trust` is used, no authentication is done at all. This\nis not generally recommended, but can be used to recover the pre-4.0\nbehavior::\n\n    db> configure instance insert cfg::Auth {\n    ...     priority := -1,\n    ...     method := (insert cfg::Trust { transports := \"SIMPLE_HTTP\" }),\n    ... };\n    OK: CONFIGURE INSTANCE\n\nTo authenticate to your |Gel| Cloud instance, first create a secret key using\nthe Gel Cloud UI or :ref:`ref_cli_gel_cloud_secretkey_create`. Use the\nsecret key as your token with the bearer authentication method. Here is an\nexample showing how you might send the query ``select Person {*};`` using cURL:\n\n.. lint-off\n\n.. code-block:: bash\n\n    $ curl -G https://<cloud-instance-host>:<cloud-instance-port>/branch/main/edgeql \\\n       -H \"Authorization: Bearer <secret-key> \\\n       --data-urlencode \"query=select Person {*};\"\n\n.. lint-on\n\n.. _ref_edgeql_protocol:\n\nQuerying\n========\n\n|Gel| supports GET and POST methods for handling EdgeQL over HTTP protocol. Both GET and POST methods use the following fields:\n\n- ``query`` - contains the EdgeQL query string\n- ``variables``- contains a JSON object where the keys are the parameter names from the query and the values are the arguments to be used in this execution of the query.  **Optional**\n- ``globals``- contains a JSON object where the keys are the fully qualified global names and the values are the desired values for those globals.  **Optional**\n\n- ``config`` - contains a JSON object where the keys are configuration option names and the values are the desired values for those configs.  **Optional**. **Added in 7.0.**\n\nThe protocol supports HTTP Keep-Alive.\n\nGET request\n-----------\n\nThe HTTP GET request passes the fields as query parameters: ``query`` string and JSON-encoded ``variables``, ``globals``, and ``config`` mappings.\n\n\nPOST request\n------------\n\nThe POST request should use ``application/json`` content type and submit the following JSON-encoded form with the necessary fields:\n\n.. code-block:: json\n\n  {\n    \"query\": \"select Person {*} filter .name = <str>$name;\",\n    \"variables\": { \"name\": \"John\" },\n    \"globals\": { \"default::global_name\": \"value\" },\n    \"config\": { \"default_transaction_isolation\": \"RepeatableRead\" }\n  }\n\n\nResponse\n--------\n\nThe response format is the same for both methods. The body of the response is JSON of the following form:\n\n.. code-block:: json\n\n  {\n    \"data\": [\n      {\n        \"id\": \"00000000-0000-0000-0000-000000000000\",\n        \"name\": \"John\"\n      }\n    ],\n    \"error\": {\n      \"message\": \"Error message\",\n      \"type\": \"ErrorType\",\n      \"code\": 123456\n    }\n  }\n\nThe ``data`` response field will contain the response set as a JSON array.\n\nNote that the ``error`` field will only be present if an error actually occurred. The ``error`` will further contain the ``message`` field with the error message string, the ``type`` field with the name of the type of error and the ``code`` field with an integer :ref:`error code <ref_protocol_error_codes>`.\n\n.. note::\n\n  Caution is advised when reading ``decimal`` or ``bigint`` values using HTTP protocol because the results are provided in JSON format. The JSON specification does not have a limit on significant digits, so a ``decimal`` or a ``bigint`` number can be losslessly represented in JSON. However, JSON decoders in many languages will read all such numbers as some kind of of 32- or 64-bit number type, which may result in errors or precision loss.  If such loss is unacceptable, then consider casting the value into ``str`` and decoding it on the client side into a more appropriate type.\n"
  },
  {
    "path": "docs/reference/using/index.rst",
    "content": ".. _ref_using_index:\n\n=========\nUsing Gel\n=========\n\n.. toctree::\n    :maxdepth: 2\n    :hidden:\n\n    connection\n    projects\n    cli/index\n    clients\n    js/index\n    python/index\n    Go <https://pkg.go.dev/github.com/geldata/gel-go>\n    Rust <https://docs.rs/gel-tokio/latest/gel_tokio/>\n    sql_adapter\n    http\n    graphql/index\n    datetime\n\nThis section covers how to connect to and interact with a running Gel instance, whether it's hosted locally or on a remote server like our Cloud service. This section covers how to establish connections, send queries to retrieve and manipulate data, and use the Gel CLI to manage your running database.\n\nConnecting to Gel\n=================\n\nGel clients and tools have a unique system for resolving connections to the appropriate Gel instance.\n\nWorking with Projects\n=====================\n\nFor local development, Gel offers project-based workflows that simplify connection configuration  and streamline development tasks. Tools like the Gel CLI, the language server, and the language-specific client libraries all use the concept of projects to manage connections. You can use projects to run a local instance, or link your local environment to a remote instance.\n\nCommand Line interface\n======================\n\nThe Gel CLI provides powerful tools for database administration, schema management, REPL-based data exploration, and more - all from your terminal. It's an essential companion for both development and operational tasks.\n\nClient Libraries\n================\n\nChoose from a variety of language-specific client libraries (JavaScript, Python, Go, Rust) to integrate Gel into your applications. Our clients are higher-level than most database drivers, providing integrated pooling, connection management, transaction retries, and more. Many languages also offer code generation tools that integrate your queries more tightly into your codebase."
  },
  {
    "path": "docs/reference/using/js/client.rst",
    "content": ".. _gel-js-driver:\n.. _gel-js-client:\n\n======\nClient\n======\n\nThe ``Client`` class implements the basic functionality required to establish a pool of connections to your database, execute queries with some context and parameters, manage transactions, and decode results into JavaScript types.\n\nCreating a client\n=================\n\nThe ``gel`` package exposes a :ref:`createClient <gel-js-create-client>` function that can be used to create a new :ref:`Client <gel-js-api-client>` instance. This client instance manages a pool of connections to the database which it discovers automatically from either being in a :gelcmd:`project init` directory or being provided connection details via Environment Variables. See :ref:`the environment section of the connection reference <ref_reference_connection_environments>` for more details and options.\n\n.. note::\n\n  If you're using |Gel| Cloud to host your development instance, you can use the :gelcmd:`cloud login` command to authenticate with |Gel| Cloud and then use the :gelcmd:`project init --server-instance <instance-name>` command to create a local project-linked instance that is linked to an Gel Cloud instance. For more details, see :ref:`the Gel Cloud guide <ref_guide_cloud>`.\n\n.. code-block:: typescript\n\n  import { createClient } from \"gel\";\n\n  const client = createClient();\n\n  const answer = await client.queryRequiredSingle<number>(\"select 2 + 2;\");\n  console.log(answer); // number: 4\n\nChecking connection status\n--------------------------\n\nThe client maintains a dynamically sized *pool* of connections under the hood.  These connections are initialized *lazily*, so no connection will be established until the first time you execute a query.\n\nIf you want to explicitly ensure that the client is connected without running a query, use the ``.ensureConnected()`` method. This can be useful to catch any errors resulting from connection mis-configuration by triggering the first connection attempt explicitly.\n\n.. code-block:: typescript\n\n  import { createClient } from \"gel\";\n\n  const client = createClient();\n\n  async function main() {\n    await client.ensureConnected();\n    const answer = await client.queryRequiredSingle<number>(\"select 2 + 2;\");\n    console.log(answer); // number: 4\n  }\n\n  main();\n\n.. _gel-js-running-queries:\n\nRunning queries\n===============\n\nThe ``Client`` class provides a number of methods for running queries. The simplest is ``query``, which runs a query and returns the result as an array of results. The function signature is generic over the type of the result element, so you can provide a type to receive a strongly typed result.\n\n.. code-block:: typescript\n\n  import { createClient } from \"gel\";\n\n  const client = createClient();\n\n  const result = await client.query<number>(\"select 2 + 2;\");\n  console.log(result); // number[]: [4]\n\nParameters\n----------\n\nIf your query contains parameters (e.g. ``$foo``), you can pass in values as the second argument.\n\n.. code-block:: typescript\n\n  const result = await client.querySingle<{ id: string }>(\n    `insert Movie { title := <str>$title }`,\n    { title: \"Iron Man\" }\n  );\n  console.log(result);\n  // {id: \"047c5893...\"}\n\n.. note::\n\n  Parameters can only be scalars or arrays of scalars. See :ref:`parameters <ref_eql_params>` for more details.\n\nCardinality\n-----------\n\nThe ``.query`` method always returns an array of results. It places no constraints on cardinality.\n\n.. code-block:: typescript\n\n  await client.query<number>(\"select 2 + 2;\"); // number[]: [4]\n  await client.query<number>(\"select <int64>{};\"); // number[]: []\n  await client.query<number>(\"select {1, 2, 3};\"); // number[]: [1, 2, 3]\n\nIf you know your query will only return a single element, you can tell |Gel| to expect a *singleton result* by using the ``.querySingle`` method. This is intended for queries that return *zero or one* elements. If the query returns a set with more than one elements, the ``Client`` will throw a runtime error.\n\n.. note::\n\n  Remember that arrays and tuples are considered an element of the result set, so if you're returning exactly one array or tuple, the result will be an array.\n\n.. code-block:: typescript\n\n  await client.querySingle<number>(\"select 2 + 2;\"); // number | null: 4\n  await client.querySingle<number[]>(\"select [1, 2, 3];\"); // number[] | null: [1, 2, 3]\n  await client.querySingle<number>(\"select <int64>{};\"); // number | null: null\n  await client.querySingle<number>(\"select {1, 2, 3};\"); // Throws a ResultCardinalityMismatchError\n\nUse ``queryRequiredSingle`` for queries that return *exactly one* element. If the query returns an empty set or a set with multiple elements, the ``Client`` will throw a runtime error.\n\n.. code-block:: typescript\n\n  await client.queryRequiredSingle<number>(\"select 2 + 2;\"); // number: 4\n  await client.queryRequiredSingle<number>(\"select <int64>{};\"); // Throws a NoDataError\n  await client.queryRequiredSingle<number>(\"select {1, 2, 3};\"); // Throws a ResultCardinalityMismatchError\n\nUse ``queryRequired`` for queries that return *one or more* elements. If the query returns an empty set, the ``Client`` will throw a runtime error.\n\n.. code-block:: typescript\n\n  await client.queryRequired<number>(\"select 2 + 2;\"); // [number, ...number[]]: 4\n  await client.queryRequired<number>(\"select {1, 2, 3};\"); // [number, ...number[]]: [1, 2, 3]\n  await client.queryRequired<number>(\"select <int64>{};\"); // Throws a ResultCardinalityMismatchError\n\nIf you do not need or expect a result, you can use ``execute`` which will return ``void``. This is often useful for mutations where you do not need to retrieve a result.\n\n.. code-block:: typescript\n\n  await client.execute(`insert Movie { title := \"Iron Man\" }`); // void\n\nJSON results\n------------\n\nClient provide additional methods for running queries and retrieving results as a *serialized JSON string*. This serialization happens inside the database and is typically more performant than running ``JSON.stringify`` yourself.\n\n.. code-block:: javascript\n\n  await client.queryJSON(`select {1, 2, 3};`);\n  // \"[1, 2, 3]\"\n\n  await client.querySingleJSON(`select <int64>{};`);\n  // \"null\"\n\n  await client.queryRequiredSingleJSON(`select 3.14;`);\n  // \"3.14\"\n\n  await client.queryRequiredJSON(`select 3.14;`);\n  // \"3.14\"\n\n.. warning::\n\n  Caution is advised when reading ``decimal`` or ``bigint`` values using these methods. The JSON specification does not have a limit on significant digits, so a ``decimal`` or a ``bigint`` number can be losslessly represented in JSON.  However, JSON decoders in JavaScript will often read all such numbers as ``number`` values, which may result in precision loss. If such loss is unacceptable, then consider casting the value into ``str`` and decoding it on the client side into a more appropriate type, such as BigInt_.\n\nSQL queries\n-----------\n\n.. versionadded:: 6.0\n\nThe ``querySQL`` method allows you to run a SQL query and return the result as an array of objects. This method is also generic over the type of the result element, so you can provide a type to receive a strongly typed result.\n\n.. code-block:: typescript\n\n  const result = await client.querySQL<{ result: number }>(`select 2 + 2 as result;`);\n  console.log(result); // [{result: 4}]\n\nIf you don't need the result, you can use ``executeSQL`` which will return ``void``.\n\n.. code-block:: typescript\n\n  await client.executeSQL(`insert into \"Movie\" (name) values (\"Iron Man\")`); // void\n\nScripts\n-------\n\nBoth ``execute`` and the ``query*`` methods support scripts (queries containing multiple statements). The statements, like all queries, are run in an implicit transaction (unless already in an explicit transaction), so the whole script remains atomic. For the ``query*`` methods only the result of the final statement in the script will be returned.\n\n.. code-block:: typescript\n\n  const result = await client.query<{ id: string }>(`\n    insert Movie {\n      title := <str>$title\n    };\n    insert Person {\n      name := <str>$name\n    };\n  `, {\n    title: \"Thor: Ragnarok\",\n    name: \"Anson Mount\"\n  });\n  result; // { id: string }[]: the result of the `insert Person` statement\n\nFor more fine grained control of atomic execution of multiple statements, use the ``transaction()`` API.\n\n.. _gel-js-api-transaction:\n\nTransactions\n------------\n\nFor more fine grained control of atomic execution of multiple statements, use the ``transaction()`` API.\n\n.. code-block:: typescript\n\n    await client.transaction(async (tx) => {\n      await tx.execute(\"insert Movie { title := <str>$title }\", { title: \"Iron Man\" });\n      await tx.execute(\"insert Person { name := <str>$name }\", { name: \"Anson Mount\" });\n    });\n\nNote that we execute queries on the ``tx`` object in the above example, rather than on the original ``client`` object.\n\nThe ``transaction()`` API guarantees that:\n\n1. Transactions are executed atomically;\n2. If a transaction fails due to retryable error (like a network failure or a concurrent update error), the transaction would be retried;\n3. If any other, non-retryable error occurs, the transaction is rolled back and the ``transaction()`` block throws.\n\nThe *transaction* object exposes ``query()``, ``execute()``, ``querySQL()``, ``executeSQL()``, and other ``query*()`` methods that *clients* expose, with the only difference that queries will run within the current transaction and can be retried automatically.\n\nDefault isolation level is serializable. You can change it via ``Client.withTransactionOptions``.\n\n.. warning::\n\n  In transactions, the entire nested code block can be re-run, including any non-querying JavaScript code. In general, the code inside the transaction block **should not have side effects or run for a significant amount of time**. Consider the following example:\n\n  .. code-block:: typescript\n\n      const email = \"timmy@example.com\";\n\n      await client.transaction(async (tx) => {\n        await tx.execute(\n          `insert User { email := <str>$email }`,\n          { email },\n        );\n\n        await sendWelcomeEmail(email);\n\n        await tx.execute(\n          `insert LoginHistory {\n            user := (select User filter .email = <str>$email),\n            timestamp := datetime_current()\n          }`,\n          { email },\n        );\n      });\n\n  In the above example, the welcome email may be sent multiple times if the transaction block is retried. Additionally, transactions allocate expensive server resources. Having too many concurrently running long-running transactions will negatively impact the performance of the DB server.\n\n\nConfiguring clients\n===================\n\nClients can be configured using a set of methods that start with ``with``. One you'll likely use often in application code is the ``withGlobals`` which sets the global variables in the query.\n\n.. code-block:: typescript\n\n  const client = createClient();\n  await client\n    .withGlobals({\n      current_user_id: \"00000000-0000-0000-0000-000000000000\",\n    })\n    .querySingle(\n      \"select User { * } filter .id ?= global current_user_id;\"\n    );\n\n.. note::\n\n  These methods return a *new Client instance* that *shares a connection pool* with the original client. This is important. Each call to ``createClient`` instantiates a new connection pool, so in typical usage you should create a single shared client instance and configure it at runtime as needed.\n\n\n.. _gel-js-api-client:\n\nClient Reference\n================\n\n.. _gel-js-create-client:\n\n``createClient`` function\n-------------------------\n\n.. js:function:: createClient( \\\n      options?: string | ConnectOptions | null | undefined \\\n    ): Client\n\n    Creates a new :js:class:`Client` instance.\n\n    :param options:\n        This is an optional parameter. We recommend omitting it in all but the most unusual circumstances. When it is not specified the client will connect to the current |Gel| Project instance or discover connection parameters from the environment.\n\n        If this parameter is a string it can represent either a DSN or an instance name. When the string does not start with |geluri| it is parsed as the :ref:`name of an instance <ref_reference_connection_instance_name>`; otherwise it specifies a single string in the DSN format: :geluri:`user:password@host:port/database?option=value`.\n\n        Alternatively the parameter can be a ``ConnectOptions`` config; see the documentation of valid options below, and the full :ref:`connection parameter reference <ref_reference_connection_parameters>` for details.\n\n    :param string options.dsn:\n        Specifies the DSN of the instance.\n\n    :param string options.credentialsFile:\n        Path to a file containing credentials.\n\n    :param string options.host:\n        Instance host address as either an IP address or a domain name.\n\n    :param number options.port:\n        Port number to connect to at the server host.\n\n    :param string options.branch:\n        The name of the branch to connect to.\n\n    :param string options.user:\n        The name of the database role used for authentication.\n\n    :param string options.password:\n        Password to be used for authentication, if the server requires one.\n\n    :param string options.tlsCAFile:\n        Path to a file containing the root certificate of the server.\n\n    :param string options.tlsSecurity:\n        Determines whether certificate and hostname verification is enabled.  Valid values are ``'strict'`` (certificate will be fully validated), ``'no_host_verification'`` (certificate will be validated, but hostname may not match), ``'insecure'`` (certificate not validated, self-signed certificates will be trusted), or ``'default'`` (acts as ``strict`` by default, or ``no_host_verification`` if ``tlsCAFile`` is set).\n\n    The above connection options can also be specified by their corresponding environment variable. If none of ``dsn``, ``credentialsFile``, ``host`` or ``port`` are explicitly specified, the client will connect to your linked project instance, if it exists. For full details, see the :ref:`Connection Parameters <ref_reference_connection>` docs.\n\n    :param number options.timeout:\n        Connection timeout in milliseconds.\n\n    :param number options.waitUntilAvailable:\n        If first connection fails, the number of milliseconds to keep retrying to connect. Useful if your development instance and app are started together, to allow the server time to be ready.\n\n    :param number options.concurrency:\n        The maximum number of connections the ``Client`` will create in it's connection pool. If not specified the concurrency will be controlled by the server. This is recommended as it allows the server to better manage the number of client connections based on it's own available resources.\n\n    :returns:\n        Returns an instance of :js:class:`Client`.\n\n    Example:\n\n    .. code-block:: typescript\n\n      import { createClient } from \"gel\";\n      import assert from \"node:assert\";\n\n      async function main() {\n        const client = createClient();\n\n        const data: number = await client.queryRequiredSingle<number>(\n          \"select 1 + 1\"\n        );\n\n        assert(data === 2, \"Result is exactly the number 2\");\n      }\n\n      main();\n\n``Client`` class\n----------------\n\n.. js:class:: Client\n\n    A ``Client`` allows you to run queries on a |Gel| instance.\n\n    Since opening connections is an expensive operation, ``Client`` also maintains a internal pool of connections to the instance, allowing connections to be automatically reused, and you to run multiple queries on the client simultaneously, enhancing the performance of database interactions.\n\n    :js:class:`Client` is not meant to be instantiated directly; :js:func:`createClient` should be used instead.\n\n\n    .. _gel-js-api-async-optargs:\n\n    .. note::\n\n        Some methods take query arguments as an *args* parameter. The type of the *args* parameter depends on the query:\n\n        * If the query uses positional query arguments, the *args* parameter must be an ``array`` of values of the types specified by each query argument's type cast.\n        * If the query uses named query arguments, the *args* parameter must be an ``object`` with property names and values corresponding to the query argument names and type casts.\n\n        If a query argument is defined as ``optional``, the key/value can be either omitted from the *args* object or be a ``null`` value.\n\n    .. js:method:: execute(query: string, args?: QueryArgs): Promise<void>\n\n        Execute an EdgeQL command or script of commands. Does not return any results.\n\n        :param query: Query text.\n        :param args: (optional) :ref:`query arguments <gel-js-api-async-optargs>`.\n\n        :returns: ``Promise<void>``\n\n        Example:\n\n        .. code-block:: typescript\n\n          await client.execute(`\n            for x in {100, 200, 300}\n            insert MyType { a := x };\n          `)\n\n    .. js:method:: query<T>(query: string, args?: QueryArgs): Promise<T[]>\n\n        Run an EdgeQL query and return the results as an array.\n        This method **always** returns an array.\n\n        :param query: Query text.\n        :param args: (optional) :ref:`query arguments <gel-js-api-async-optargs>`.\n\n        :returns: ``Promise<T[]>``\n\n        Example:\n\n        .. code-block:: typescript\n\n          const result = await client.query<number>(\"select 2 + 2;\"); // number[]: [4]\n          const result = await client.query<number>(\"select {1, 2, 3};\"); // number[]: [1, 2, 3]\n          const result = await client.query<number>(\"select <int64>{};\"); // number[]: []\n\n    .. js:method:: queryRequired<T>( \\\n            query: string, \\\n            args?: QueryArgs \\\n        ): Promise<[T, ...T[]]>\n\n        Run a query that returns at least one element and return the result as an\n        array. The *query* must return at least one element. If the query less than one\n        element, a ``ResultCardinalityMismatchError`` error is thrown.\n\n        :param query: Query text.\n        :param args: (optional) :ref:`query arguments <gel-js-api-async-optargs>`.\n\n        :returns: ``Promise<[T, ...T[]]>``\n        :throws: ``ResultCardinalityMismatchError`` if the query returns less than one element.\n\n        Example:\n\n        .. code-block:: typescript\n\n          await client.queryRequired<number>(\"select 2 + 2;\"); // [number, ...number[]]: [4]\n          await client.queryRequired<number>(\"select {1, 2, 3};\"); // [number, ...number[]]: [1, 2, 3]\n          await client.queryRequired<number>(\"select <int64>{};\"); // Throws a ResultCardinalityMismatchError\n\n    .. js:method:: querySingle<T>( \\\n            query: string, \\\n            args?: QueryArgs \\\n        ): Promise<T | null>\n\n        Run an optional singleton-returning query and return the result. The *query* must return no more than one element. If the query returns more than one element, a ``ResultCardinalityMismatchError`` error is thrown.\n\n        :param query: Query text.\n        :param args: (optional) :ref:`query arguments <gel-js-api-async-optargs>`.\n\n        :returns: ``Promise<T | null>``\n        :throws: ``ResultCardinalityMismatchError`` if the query returns more than one element.\n\n        Example:\n\n        .. code-block:: typescript\n\n          const result = await client.querySingle<number>(\"select 2 + 2;\"); // number | null: 4\n          await client.querySingle<number>(\"select <int64>{};\"); // number | null: null\n          await client.querySingle<number>(\"select {1, 2, 3};\"); // Throws a ResultCardinalityMismatchError\n\n    .. js:method:: queryRequiredSingle<T>( \\\n            query: string, \\\n            args?: QueryArgs \\\n        ): Promise<T>\n\n        Run a singleton-returning query and return the result. The *query* must return exactly one element. If the query returns more than one element, a ``ResultCardinalityMismatchError`` error is thrown. If the query returns an empty set, a ``NoDataError`` error is thrown.\n\n        :param query: Query text.\n        :param args: (optional) :ref:`query arguments <gel-js-api-async-optargs>`.\n\n        :returns: ``Promise<T>``\n        :throws: ``ResultCardinalityMismatchError`` if the query returns more than one element.\n        :throws: ``NoDataError`` if the query returns an empty set.\n\n        Example:\n\n        .. code-block:: typescript\n\n          await client.queryRequiredSingle<number>(\"select 2 + 2;\"); // number: 4\n          await client.queryRequiredSingle<number>(\"select <int64>{};\"); // Throws a NoDataError\n          await client.queryRequiredSingle<number>(\"select {1, 2, 3};\"); // Throws a ResultCardinalityMismatchError\n\n\n    .. js:method:: queryJSON(query: string, args?: QueryArgs): Promise<string>\n\n        Run a query and return the results as a JSON-encoded string.\n\n        :param query: Query text.\n        :param args: (optional) :ref:`query arguments <gel-js-api-async-optargs>`.\n\n        :returns: ``Promise<string>``\n\n        .. note::\n\n          Caution is advised when reading ``decimal`` or ``bigint`` values using this method. The JSON specification does not have a limit on significant digits, so a ``decimal`` or a ``bigint`` number can be losslessly represented in JSON.  However, JSON decoders in JavaScript will often read all such numbers as ``number`` values, which may result in precision loss. If such loss is unacceptable, then consider casting the value into ``str`` and decoding it on the client side into a more appropriate type, such as BigInt_.\n\n    .. js:method:: queryRequiredJSON( \\\n            query: string, \\\n            args?: QueryArgs \\\n        ): Promise<string>\n\n        Run a query that returns at least one element and return the result as a JSON-encoded string. The *query* must return at least one element. If the query less than one element, a ``ResultCardinalityMismatchError`` error is thrown.\n\n        :param query: Query text.\n        :param args: (optional) :ref:`query arguments <gel-js-api-async-optargs>`.\n\n        :returns: ``Promise<string>``\n        :throws: ``ResultCardinalityMismatchError`` if the query returns less than one element.\n\n        Example:\n\n        .. code-block:: typescript\n\n          const result = await client.queryRequiredJSON(\"select 2 + 2;\"); // string: \"4\"\n          const result = await client.queryRequiredJSON(\"select <int64>{};\"); // Throws a ResultCardinalityMismatchError\n          const result = await client.queryRequiredJSON(\"select {1, 2, 3};\"); // Throws a ResultCardinalityMismatchError\n\n\n        .. warning::\n\n          Caution is advised when reading ``decimal`` or ``bigint`` values using this method. The JSON specification does not have a limit on significant digits, so a ``decimal`` or a ``bigint`` number can be losslessly represented in JSON.  However, JSON decoders in JavaScript will often read all such numbers as ``number`` values, which may result in precision loss. If such loss is unacceptable, then consider casting the value into ``str`` and decoding it on the client side into a more appropriate type, such as BigInt_.\n\n    .. js:method:: querySingleJSON( \\\n            query: string, \\\n            args?: QueryArgs \\\n        ): Promise<string>\n\n        Run an optional singleton-returning query and return its element as a JSON-encoded string.  The *query* must return at most one element.  If the query returns more than one element, an ``ResultCardinalityMismatchError`` error is thrown.\n\n        :param query: Query text.\n        :param args: (optional) :ref:`query arguments <gel-js-api-async-optargs>`.\n\n        :returns: ``Promise<string>``\n        :throws: ``ResultCardinalityMismatchError`` if the query returns more than one element.\n\n        Example:\n\n        .. code-block:: typescript\n\n          const result = await client.querySingleJSON(\"select 2 + 2;\"); // string: \"4\"\n          await client.querySingleJSON(\"select <int64>{};\"); // Throws a ResultCardinalityMismatchError\n          await client.querySingleJSON(\"select {1, 2, 3};\"); // Throws a ResultCardinalityMismatchError\n\n        .. warning::\n\n          Caution is advised when reading ``decimal`` or ``bigint`` values using this method. The JSON specification does not have a limit on significant digits, so a ``decimal`` or a ``bigint`` number can be losslessly represented in JSON.  However, JSON decoders in JavaScript will often read all such numbers as ``number`` values, which may result in precision loss. If such loss is unacceptable, then consider casting the value into ``str`` and decoding it on the client side into a more appropriate type, such as BigInt_.\n\n    .. js:method:: queryRequiredSingleJSON( \\\n            query: string, \\\n            args?: QueryArgs \\\n        ): Promise<string>\n\n        Run a singleton-returning query and return its element as a JSON-encoded string. The *query* must return exactly one element. If the query returns more than one element, a ``ResultCardinalityMismatchError`` error is thrown. If the query returns an empty set, a ``NoDataError`` error is thrown.\n\n        :param query: Query text.\n        :param args: (optional) :ref:`query arguments <gel-js-api-async-optargs>`.\n\n        :returns: ``Promise<string>``\n        :throws: ``ResultCardinalityMismatchError`` if the query returns more than one element.\n        :throws: ``NoDataError`` if the query returns an empty set.\n\n        Example:\n\n        .. code-block:: typescript\n\n          const result = await client.queryRequiredSingleJSON(\"select 2 + 2;\"); // string: \"4\"\n          await client.queryRequiredSingleJSON(\"select <int64>{};\"); // Throws a ResultCardinalityMismatchError\n          await client.queryRequiredSingleJSON(\"select {1, 2, 3};\"); // Throws a ResultCardinalityMismatchError\n\n\n        .. warning::\n\n            Caution is advised when reading ``decimal`` or ``bigint`` values using this method. The JSON specification does not have a limit on significant digits, so a ``decimal`` or a ``bigint`` number can be losslessly represented in JSON.  However, JSON decoders in JavaScript will often read all such numbers as ``number`` values, which may result in precision loss. If such loss is unacceptable, then consider casting the value into ``str`` and decoding it on the client side into a more appropriate type, such as BigInt_.\n\n    .. js:method:: executeSQL(query: string, args?: unknown[]): Promise<void>\n\n        Execute a SQL command.\n\n        :param query: SQL query text.\n        :param args: (optional) :ref:`query arguments <gel-js-api-async-optargs>`.\n\n        :returns: ``Promise<void>``\n\n        Example:\n\n        .. code-block:: typescript\n\n          await client.executeSQL(`\n            INSERT INTO \"MyType\" (prop) VALUES (\"value\");\n          `)\n\n    .. js:method:: querySQL<T>(query: string, args?: unknown[]): Promise<T[]>\n\n        Run a SQL query and return the results as an array. This method **always** returns an array.\n\n        The array will contain the returned rows. By default, rows are ``Objects`` with columns addressable by name, and the type of the object as the generic type parameter ``T``. You can also opt into ``array`` mode, where the array contains arrays of values by calling ``client.withSQLRowMode('array')``.\n\n        :param query: SQL query text.\n        :param args: (optional) :ref:`query arguments <gel-js-api-async-optargs>`.\n\n        :returns: ``Promise<T[]>``\n\n        Example:\n\n        .. code-block:: typescript\n\n            const sqlQuery = `SELECT 1 as foo, \"hello\" as bar`;\n            await client.querySQL<{foo: number; bar: string }>(sqlQuery);\n            // { foo: number; bar: string }[]: [{'foo': 1, 'bar': 'hello'}]\n\n            const arrayModeClient = client.withSQLRowMode('array');\n            await arrayModeClient.querySQL<[number, string]>(sqlQuery);\n            // [number, string][]: [[1, 'hello']]\n\n    .. js:method:: transaction<T>( \\\n            action: (tx: Transaction) => Promise<T> \\\n        ): Promise<T>\n\n        Execute a retryable transaction. The ``Transaction`` object passed to the ``action`` callback function has the same ``execute`` and ``query*`` methods as ``Client``.\n\n        The ``transaction()`` method will attempt to re-execute the transaction body if a transient error occurs, such as a network error or a transaction serialization error.  The number of times ``transaction()`` will attempt to execute the transaction, and the backoff timeout between retries can be configured with :js:meth:`Client.withRetryOptions`.\n\n        See :ref:`gel-js-api-transaction` for more details.\n\n        :arg action: A callback function that takes a ``Transaction`` object as an argument and returns a ``Promise`` that resolves to the result of the transaction.\n\n        :returns: ``Promise<T>``\n\n        Example:\n\n        .. code-block:: javascript\n\n            await client.transaction(async (tx) => {\n              const value = await tx.queryRequiredSingle<number>(\"select Counter.value\");\n              await tx.execute(\n                `update Counter set { value := <int64>$value }`,\n                {value: value + 1},\n              );\n            });\n\n        By default, transactions will be executed in the strictest, serializable isolation level.\n        To change the isolation level, use the ``Client.withTransactionOptions``.\n\n    .. js:method:: ensureConnected(): Promise<Client>\n\n        If the client does not yet have any open connections in its pool, attempts to open a connection, else returns immediately.\n\n        Since the client lazily creates new connections as needed (up to the configured ``concurrency`` limit), the first connection attempt will only occur when the first query is run a client. ``ensureConnected`` can be useful to catch any errors resulting from connection mis-configuration by triggering the first connection attempt explicitly.\n\n        :returns: ``Promise<Client>``\n\n        Example:\n\n        .. code-block:: javascript\n\n            import { createClient } from \"gel\";\n\n            async function getClient() {\n              try {\n                return await createClient().ensureConnected();\n              } catch (err) {\n                // handle connection error\n              }\n            }\n\n            async function main() {\n              const client = await getClient();\n\n              await client.query(\"select 2 + 2;\");\n            }\n\n    .. js:method:: withGlobals(globals: {[name: string]: any}): Client\n\n        Returns a clone of the ``Client`` instance with the specified global values. The ``globals`` argument object is merged with any existing globals defined on the current client instance. The new client instance will share the same connection pool as the client it's created from.\n\n        Equivalent to using the ``set global`` command.\n\n        :arg globals: An object mapping global names to values.\n\n        :returns: ``Client``\n\n        Example:\n\n        .. code-block:: typescript\n\n            const user = await client.withGlobals({\n              userId: \"00000000-0000-0000-0000-000000000000\"\n            }).querySingle<{ name: string }>(`\n              select User { name } filter .id = global userId;\n            `);\n\n    .. js:method:: withModuleAliases(aliases: {[name: string]: string}): Client\n\n        Returns a clone of the ``Client`` instance with the specified module aliases. The ``aliases`` argument object is merged with any existing module aliases defined on the current client instance. The new client instance will share the same connection pool as the client it's created from.\n\n        If the alias ``name`` is ``module`` this is equivalent to using the ``set module`` command, otherwise it is equivalent to the ``set alias`` command.\n\n        :arg aliases: An object mapping alias names to values.\n\n        :returns: ``Client``\n\n        Example:\n\n        .. code-block:: javascript\n\n            const user = await client.withModuleAliases({\n              module: \"sys\"\n            }).queryRequiredSingle<string>(`\n              select get_version_as_str();\n            `);\n            // \"6.4\"\n\n    .. js:method:: withConfig(config: {[name: string]: any}): Client\n\n        Returns a clone of the ``Client`` instance with the specified client session configuration. The ``config`` argument object is merged with any existing session config defined on the current client instance. The new client instance will share the same connection pool as the client it's created from.\n\n        Equivalent to using the ``configure session`` command. For available configuration parameters refer to the :ref:`Config documentation <ref_std_cfg>`.\n\n        :arg config: An object mapping configuration parameter names to values.\n\n        :returns: ``Client``\n\n        Example:\n\n        .. code-block:: typescript\n\n            const user = await client\n              .withConfig({ \"query_timeout\": 10000 })\n              .query<{ name: string }>(`\n                select User { name };\n              `);\n\n    .. js:method:: withRetryOptions(opts: { \\\n            attempts?: number, \\\n            backoff?: (attempt: number) => number \\\n        }): Client\n\n        Returns a clone of the ``Client`` instance with the specified retry attempts number and backoff time function (the time that retrying methods will wait between retry attempts, in milliseconds), where options not given are inherited from the current client instance.\n\n        The default number of attempts is ``3``. The default backoff function returns a random time between 100 and 200ms multiplied by ``2 ^ attempt number``.\n\n        :arg opts: An object mapping retry options to values.\n\n        :returns: ``Client``\n\n        Example:\n\n        .. code-block:: javascript\n\n            const nonRetryingClient = client.withRetryOptions({\n              attempts: 1\n            });\n\n            // This transaction will not retry\n            await nonRetryingClient.transaction(async (tx) => {\n              // ...\n            });\n\n    .. js:method:: withTransactionOptions(opts: {\n          isolation?: IsolationLevel, \\\n          readonly?: boolean, \\\n          deferrable?: boolean, \\\n        }): Client\n\n        Returns a clone of the ``Client`` instance with the specified transaction options.\n\n        Available isolation levels are ``Serializable``, ``RepeatableRead``, and ``PreferRepeatableRead``. ``PreferRepeatableRead`` uses repeatable read isolation level if server analysis concludes that it is supported for a given query. Otherwise, uses serializable isolation level.\n\n        When readonly is set, the transaction is now allowed to execute ``insert``, ``update``, or ``delete`` statements.\n\n        When deferrable is set, and isolation is serializable and readonly is set, the transaction only block when first acquiring its snapshot, after which it is able to run without the normal overhead of a serializable transaction and without any risk of contributing to or being canceled by a serialization failure. This mode is well suited for long-running reports or backups.\n\n        For more information, see :ref:`the transaction options reference <ref_eql_statements_start_tx>`.\n\n        :arg opts: An object mapping transaction options to values.\n\n        :returns: ``Client``\n\n        Example:\n\n        .. code-block:: javascript\n\n            const readonlyClient = client.withTransactionOptions({\n              isolation: IsolationLevel.PreferRepeatableRead,\n              readonly: true,\n            });\n\n            // This transaction is less likely to raise a serialization error\n            await readonlyClient.transaction(async (tx) => {\n              // ...\n            });\n\n    .. js:method:: withWarningHandler(handler: (warnings: errors.GelError[]) => void): Client\n\n        Returns a clone of the ``Client`` instance with the specified warning handler. Some queries may generate warnings while still returning a result. The ``handler`` function will be called with an array of ``GelError`` objects whenever the client encounters warnings during query execution.\n\n        By default, the warnings are logged to the console with ``console.warn``.\n\n        :arg handler: A function that takes an array of ``GelError`` objects as its argument.\n\n        :returns: ``Client``\n\n        Example:\n\n        .. code-block:: typescript\n\n            const warningHandler = (warnings: errors.GelError[]) => {\n              warnings.forEach((gelError) => {\n                console.warn(\"Warning:\", gelError.message);\n              });\n            };\n\n            const clientWithWarningHandler = client.withWarningHandler(warningHandler);\n\n            await clientWithWarningHandler.query(`\n              select User filter .friends.name = \"John\";\n            `);\n            // Warning: Gel warning: possibly more than one element returned by an expression in a FILTER clause\n\n    .. js:method:: close(): Promise<void>\n\n        Close the client's open connections gracefully. When a client is closed, all its underlying connections are awaited to complete their pending operations, then closed. A warning is produced if the pool takes more than 60 seconds to close.\n\n        .. note::\n\n            Clients will not prevent Node.js from exiting once all of it's open connections are idle and Node.js has no further tasks it is awaiting on, so it is not necessary to explicitly call ``close()`` if it is more convenient for your application.\n\n    .. js:method:: isClosed(): boolean\n\n        Returns true if ``close()`` has been called on the client.\n\n    .. js:method:: terminate(): void\n\n        Terminate all connections in the client, closing all connections non gracefully. If the client is already closed, return without doing anything.\n\n.. _BigInt:\n    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt\n"
  },
  {
    "path": "docs/reference/using/js/datatypes.rst",
    "content": ".. _gel-js-datatypes:\n\n==========\nData types\n==========\n\nWhen producing results, the client automatically decodes |Gel| types to the corresponding JavaScript types. When you pass scalar values to the client as arguments, the client automatically encodes them to the corresponding |Gel| types.\n\nThe table below shows the correspondence between |Gel| and JavaScript data types.\n\n.. list-table::\n\n  * - **Gel Type**\n    - **JavaScript Type**\n  * - ``multi`` set\n    - ``Array``\n  * - ``array<anytype>``\n    - ``Array``\n  * - ``anytuple``\n    - ``Array``\n  * - ``anyenum``\n    - ``string``\n  * - ``Object``\n    - ``object``\n  * - ``bool``\n    - ``boolean``\n  * - ``bytes``\n    - ``Uint8Array``\n  * - ``str``\n    - ``string``\n  * - ``float32``,  ``float64``, ``int16``, ``int32``, ``int64``\n    - ``number``\n  * - ``bigint``\n    - ``BigInt``\n  * - ``decimal``\n    - n/a\n  * - ``json``\n    - ``unknown``\n  * - ``uuid``\n    - ``string``\n  * - ``datetime``\n    - ``Date``\n  * - ``cal::local_date``\n    - :js:class:`LocalDate`\n  * - ``cal::local_time``\n    - :js:class:`LocalTime`\n  * - ``cal::local_datetime``\n    - :js:class:`LocalDateTime`\n  * - ``duration``\n    - :js:class:`Duration`\n  * - ``cal::relative_duration``\n    - :js:class:`RelativeDuration`\n  * - ``cal::date_duration``\n    - :js:class:`DateDuration`\n  * - ``range<anytype>``\n    - :js:class:`Range`\n  * - ``cfg::memory``\n    - :js:class:`ConfigMemory`\n\n.. note::\n\n    Inexact single-precision ``float`` values may have a different representation when decoded into a JavaScript number.  This is inherent to the implementation of limited-precision floating point types.  If you need the decimal representation to match, cast the expression to ``float64`` in your query.\n\n.. note::\n\n    Due to precision limitations the ``decimal`` type cannot be decoded to a JavaScript number. Use an explicit cast to ``float64`` if the precision degradation is acceptable or a cast to ``str`` for an exact decimal representation.\n\nArrays\n======\n\nGel ``array``  maps onto the JavaScript ``Array``.\n\n.. code-block:: javascript\n\n  await client.querySingle<number[]>(\"select [1, 2, 3];\");\n  // number[]: [1, 2, 3]\n\n\n.. _gel-js-types-object:\n\nObjects\n=======\n\n``Object`` represents an object instance returned from a query. The value of an\nobject property or a link can be accessed through a corresponding object key:\n\n.. code-block:: typescript\n\n    await client.query<{\n      title: string;\n      characters: {\n        name: string;\n        \"@character_name\": string;\n      }[];\n    }>(`\n      select Movie {\n        title,\n        characters: {\n          name,\n          @character_name\n        }\n      };\n    `);\n\nTuples\n======\n\nA regular |Gel| ``tuple`` becomes an ``Array`` in JavaScript. A |Gel| ``namedtuple`` becomes an object with properties corresponding to the tuple elements.\n\n.. code-block:: typescript\n\n  await client.queryRequiredSingle<[number, string]>(`select (1, \"hello\");`);\n  // [number, string]: [1, 'hello']\n\n  await client.queryRequiredSingle<{\n    foo: number;\n    bar: string;\n  }>(`select (foo := 1, bar := \"hello\");`);\n  // { foo: number; bar: string }: { foo: 1; bar: 'hello' }\n\nLocal Date\n==========\n\n.. js:class:: LocalDate(\\\n        year: number, \\\n        month: number, \\\n        day: number)\n\n    A JavaScript representation of a |Gel| ``local_date`` value. Implements a subset of the `TC39 Temporal Proposal`_ ``PlainDate`` type.\n\n    Assumes the calendar is always `ISO 8601`_.\n\n    .. js:attribute:: year: number\n\n        The year value of the local date.\n\n    .. js:attribute:: month: number\n\n        The numerical month value of the local date.\n\n        .. note::\n\n            Unlike the JS ``Date`` object, months in ``LocalDate`` start at 1.  ie. Jan = 1, Feb = 2, etc.\n\n    .. js:attribute:: day: number\n\n        The day of the month value of the local date (starting with 1).\n\n    .. js:attribute:: dayOfWeek: number\n\n        The weekday number of the local date. Returns a value between 1 and 7 inclusive, where 1 = Monday and 7 = Sunday.\n\n    .. js:attribute:: dayOfYear: number\n\n        The ordinal day of the year of the local date. Returns a value between 1 and 365 (or 366 in a leap year).\n\n    .. js:attribute:: weekOfYear: number\n\n        The ISO week number of the local date. Returns a value between 1 and 53, where ISO week 1 is defined as the week containing the first Thursday of the year.\n\n    .. js:attribute:: daysInWeek: number\n\n        The number of days in the week of the local date. Always returns 7.\n\n    .. js:attribute:: daysInMonth: number\n\n        The number of days in the month of the local date. Returns a value between 28 and 31 inclusive.\n\n    .. js:attribute:: daysInYear: number\n\n        The number of days in the year of the local date. Returns either 365 or 366 if the year is a leap year.\n\n    .. js:attribute:: monthsInYear: number\n\n        The number of months in the year of the local date. Always returns 12.\n\n    .. js:attribute:: inLeapYear: boolean\n\n        Return whether the year of the local date is a leap year.\n\n    .. js:method:: toString(): string\n\n        Get the string representation of the ``LocalDate`` in the ``YYYY-MM-DD`` format.\n\n    .. js:method:: toJSON(): number\n\n        Same as :js:meth:`~LocalDate.toString`.\n\n    .. js:method:: valueOf(): never\n\n        Always throws an Error. ``LocalDate`` objects are not comparable.\n\n\nLocal Time\n==========\n\n.. js:class:: LocalTime(\\\n        hour: number = 0, \\\n        minute: number = 0, \\\n        second: number = 0, \\\n        millisecond: number = 0, \\\n        microsecond: number = 0, \\\n        nanosecond: number = 0)\n\n    A JavaScript representation of a Gel ``local_time`` value. Implements a subset of the `TC39 Temporal Proposal`_ ``PlainTime`` type.\n\n    .. note::\n\n        The Gel ``local_time`` type only has microsecond precision, any nanoseconds specified in the ``LocalTime`` will be ignored when encoding to a Gel ``local_time``.\n\n    .. js:attribute:: hour: number\n\n        The hours component of the local time in 0-23 range.\n\n    .. js:attribute:: minute: number\n\n        The minutes component of the local time in 0-59 range.\n\n    .. js:attribute:: second: number\n\n        The seconds component of the local time in 0-59 range.\n\n    .. js:attribute:: millisecond: number\n\n        The millisecond component of the local time in 0-999 range.\n\n    .. js:attribute:: microsecond: number\n\n        The microsecond component of the local time in 0-999 range.\n\n    .. js:attribute:: nanosecond: number\n\n        The nanosecond component of the local time in 0-999 range.\n\n    .. js:method:: toString(): string\n\n        Get the string representation of the ``local_time`` in the ``HH:MM:SS`` 24-hour format.\n\n    .. js:method:: toJSON(): string\n\n        Same as :js:meth:`~LocalTime.toString`.\n\n    .. js:method:: valueOf(): never\n\n        Always throws an Error. ``LocalTime`` objects are not comparable.\n\n\nLocal Date and Time\n===================\n\n.. js:class:: LocalDateTime(\\\n        year: number, \\\n        month: number, \\\n        day: number, \\\n        hour: number = 0, \\\n        minute: number = 0, \\\n        second: number = 0, \\\n        millisecond: number = 0, \\\n        microsecond: number = 0, \\\n        nanosecond: number = 0) extends LocalDate, LocalTime\n\n    A JavaScript representation of a |Gel| ``local_datetime`` value.  Implements a subset of the `TC39 Temporal Proposal`_ ``PlainDateTime`` type.\n\n    Inherits all properties from the :js:class:`~LocalDate` and :js:class:`~LocalTime` types.\n\n    .. js:method:: toString(): string\n\n        Get the string representation of the ``local_datetime`` in the ``YYYY-MM-DDTHH:MM:SS`` 24-hour format.\n\n    .. js:method:: toJSON(): string\n\n        Same as :js:meth:`~LocalDateTime.toString`.\n\n    .. js:method:: valueOf(): never\n\n        Always throws an Error. ``LocalDateTime`` objects are not comparable.\n\n\nDuration\n========\n\n.. js:class:: Duration(\\\n        years: number = 0, \\\n        months: number = 0, \\\n        weeks: number = 0, \\\n        days: number = 0, \\\n        hours: number = 0, \\\n        minutes: number = 0, \\\n        seconds: number = 0, \\\n        milliseconds: number = 0, \\\n        microseconds: number = 0, \\\n        nanoseconds: number = 0)\n\n    A JavaScript representation of a Gel ``duration`` value. This class attempts to conform to the `TC39 Temporal Proposal`_ ``Duration`` type as closely as possible.\n\n    No arguments may be infinite and all must have the same sign. Any non-integer arguments will be rounded towards zero.\n\n    .. note::\n\n        The Temporal ``Duration`` type can contain both absolute duration components, such as hours, minutes, seconds, etc. and relative duration components, such as years, months, weeks, and days, where their absolute duration changes depending on the exact date they are relative to (eg. different months have a different number of days).\n\n        The Gel ``duration`` type only supports absolute durations, so any ``Duration`` with non-zero years, months, weeks, or days will throw an error when trying to encode them.\n\n    .. note::\n\n        The Gel ``duration`` type only has microsecond precision, any nanoseconds specified in the ``Duration`` will be ignored when encoding to a Gel ``duration``.\n\n    .. note::\n\n        Temporal ``Duration`` objects can be unbalanced_, (ie. have a greater value in any property than it would naturally have, eg. have a seconds property greater than 59), but Gel ``duration`` objects are always balanced.\n\n        Therefore in a round-trip of a ``Duration`` object to Gel and back, the returned object, while being an equivalent duration, may not have exactly the same property values as the sent object.\n\n    .. js:attribute:: years: number\n\n        The number of years in the duration.\n\n    .. js:attribute:: months: number\n\n        The number of months in the duration.\n\n    .. js:attribute:: weeks: number\n\n        The number of weeks in the duration.\n\n    .. js:attribute:: days: number\n\n        The number of days in the duration.\n\n    .. js:attribute:: hours: number\n\n        The number of hours in the duration.\n\n    .. js:attribute:: minutes: number\n\n        The number of minutes in the duration.\n\n    .. js:attribute:: seconds: number\n\n        The number of seconds in the duration.\n\n    .. js:attribute:: milliseconds: number\n\n        The number of milliseconds in the duration.\n\n    .. js:attribute:: microseconds: number\n\n        The number of microseconds in the duration.\n\n    .. js:attribute:: nanoseconds: number\n\n        The number of nanoseconds in the duration.\n\n    .. js:attribute:: sign: number\n\n        Returns -1, 0, or 1 depending on whether the duration is negative, zero or positive.\n\n    .. js:attribute:: blank: boolean\n\n        Returns ``true`` if the duration is zero.\n\n    .. js:method:: toString(): string\n\n        Get the string representation of the duration in `ISO 8601 duration`_ format.\n\n    .. js:method:: toJSON(): number\n\n        Same as :js:meth:`~Duration.toString`.\n\n    .. js:method:: valueOf(): never\n\n        Always throws an Error. ``Duration`` objects are not comparable.\n\n\nRelative Duration\n=================\n\n.. js:class:: RelativeDuration(\\\n        years: number = 0, \\\n        months: number = 0, \\\n        weeks: number = 0, \\\n        days: number = 0, \\\n        hours: number = 0, \\\n        minutes: number = 0, \\\n        seconds: number = 0, \\\n        milliseconds: number = 0, \\\n        microseconds: number = 0)\n\n  A JavaScript representation of a Gel :eql:type:`cal::relative_duration` value. This type represents a non-definite span of time such as \"2 years 3 days\". This cannot be represented as a :eql:type:`duration` because a year has no absolute duration; for instance, leap years are longer than non-leap years.\n\n  This class attempts to conform to the `TC39 Temporal Proposal`_ ``Duration`` type as closely as possible.\n\n  Internally, a ``cal::relative_duration`` value is represented as an integer number of months, days, and seconds. During encoding, other units will be normalized to these three. Sub-second units like ``microseconds`` will be ignored.\n\n  .. js:attribute:: years: number\n\n      The number of years in the relative duration.\n\n  .. js:attribute:: months: number\n\n      The number of months in the relative duration.\n\n  .. js:attribute:: weeks: number\n\n      The number of weeks in the relative duration.\n\n  .. js:attribute:: days: number\n\n      The number of days in the relative duration.\n\n  .. js:attribute:: hours: number\n\n      The number of hours in the relative duration.\n\n  .. js:attribute:: minutes: number\n\n      The number of minutes in the relative duration.\n\n  .. js:attribute:: seconds: number\n\n      The number of seconds in the relative duration.\n\n  .. js:attribute:: milliseconds: number\n\n      The number of milliseconds in the relative duration.\n\n  .. js:attribute:: microseconds: number\n\n      The number of microseconds in the relative duration.\n\n  .. js:method:: toString(): string\n\n      Get the string representation of the duration in `ISO 8601 duration`_\n      format.\n\n  .. js:method:: toJSON(): string\n\n      Same as :js:meth:`~Duration.toString`.\n\n  .. js:method:: valueOf(): never\n\n      Always throws an Error. ``RelativeDuration`` objects are not\n      comparable.\n\n\nDate Duration\n=============\n\n.. js:class:: DateDuration( \\\n      years: number = 0, \\\n      months: number = 0, \\\n      weeks: number = 0, \\\n      days: number = 0, \\\n    )\n\n  A JavaScript representation of a Gel :eql:type:`cal::date_duration` value. This type represents a non-definite span of time consisting of an integer number of *months* and *days*.\n\n  This type is primarily intended to simplify logic involving :eql:type:`cal::local_date` values.\n\n  .. code-block:: edgeql-repl\n\n    db> select <cal::date_duration>'5 days';\n    {<cal::date_duration>'P5D'}\n    db> select <cal::local_date>'2022-06-25' + <cal::date_duration>'5 days';\n    {<cal::local_date>'2022-06-30'}\n    db> select <cal::local_date>'2022-06-30' - <cal::local_date>'2022-06-25';\n    {<cal::date_duration>'P5D'}\n\n  Internally, a ``cal::relative_duration`` value is represented as an integer number of months and days. During encoding, other units will be normalized to these two.\n\n  .. js:attribute:: years: number\n\n      The number of years in the relative duration.\n\n  .. js:attribute:: months: number\n\n      The number of months in the relative duration.\n\n  .. js:attribute:: weeks: number\n\n      The number of weeks in the relative duration.\n\n  .. js:attribute:: days: number\n\n      The number of days in the relative duration.\n\n  .. js:method:: toString(): string\n\n      Get the string representation of the duration in `ISO 8601 duration`_ format.\n\n  .. js:method:: toJSON(): string\n\n      Same as :js:meth:`~Duration.toString`.\n\n  .. js:method:: valueOf(): never\n\n      Always throws an Error. ``DateDuration`` objects are not comparable.\n\n\nMemory\n======\n\n.. js:class:: ConfigMemory(bytes: BigInt)\n\n  A JavaScript representation of a Gel ``cfg::memory`` value.\n\n  .. js:attribute:: bytes: number\n\n      The memory value in bytes (B).\n\n      .. note::\n\n          The Gel ``cfg::memory`` represents a number of bytes stored as an ``int64``. Since JS the ``number`` type is a ``float64``, values above ``~8191TiB`` will lose precision when represented as a JS ``number``. To keep full precision use the ``bytesBigInt`` property.\n\n  .. js::attribute:: bytesBigInt: BigInt\n\n      The memory value in bytes represented as a ``BigInt``.\n\n  .. js:attribute:: kibibytes: number\n\n      The memory value in kibibytes (KiB).\n\n  .. js:attribute:: mebibytes: number\n\n      The memory value in mebibytes (MiB).\n\n  .. js:attribute:: gibibytes: number\n\n      The memory value in gibibytes (GiB).\n\n  .. js:attribute:: tebibytes: number\n\n      The memory value in tebibytes (TiB).\n\n  .. js:attribute:: pebibytes: number\n\n      The memory value in pebibytes (PiB).\n\n  .. js:method:: toString(): string\n\n      Get the string representation of the memory value. Format is the same as returned by string casting a ``cfg::memory`` value in Gel.\n\nRange\n=====\n\n.. js:class:: Range(\\\n        lower: T | null, \\\n        upper: T | null, \\\n        incLower: boolean = true, \\\n        incUpper: boolean = false \\\n    )\n\n  A JavaScript representation of a Gel ``std::range`` value. This is a generic TypeScript class with the following type signature.\n\n  .. code-block:: typescript\n\n      class Range<\n          T extends number | Date | LocalDate | LocalDateTime | Duration\n      >{\n          // ...\n      }\n\n  .. js:attribute:: lower: T\n\n      The lower bound of the range value.\n\n  .. js:attribute:: upper: T\n\n      The upper bound of the range value.\n\n  .. js:attribute:: incLower: boolean\n\n      Whether the lower bound is inclusive.\n\n  .. js:attribute:: incUpper: boolean\n\n      Whether the upper bound is inclusive.\n\n  .. js:attribute:: empty: boolean\n\n      Whether the range is empty.\n\n  .. js:method:: toJSON(): { \\\n        lower: T | null; \\\n        upper: T | null; \\\n        inc_lower: boolean; \\\n        inc_upper: boolean; \\\n        empty?: undefined; \\\n      }\n\n      Returns a JSON-encodable representation of the range.\n\n  .. js:method:: empty(): Range\n\n      A static method to declare an empty range (no bounds).\n\n      .. code-block:: typescript\n\n          Range.empty();\n\n.. _TC39 Temporal Proposal: https://tc39.es/proposal-temporal/docs/\n.. _ISO 8601: https://en.wikipedia.org/wiki/ISO_8601#Dates\n.. _ISO 8601 duration: https://en.wikipedia.org/wiki/ISO_8601#Durations\n.. _unbalanced: https://tc39.es/proposal-temporal/docs/balancing.html\n"
  },
  {
    "path": "docs/reference/using/js/generation.rst",
    "content": ".. _gel-js-generators:\n\n===============\nCode Generation\n===============\n\nThe ``@gel/generate`` package provides a set of code generation tools that are useful when developing a Gel-backed applications with TypeScript/JavaScript.\n\nSetup\n=====\n\nTo install the ``@gel/generate`` package, run the following command.\n\n.. tabs::\n\n    .. code-tab:: bash\n      :caption: npm\n\n      $ npm install --save-dev @gel/generate\n\n    .. code-tab:: bash\n      :caption: yarn\n\n      $ yarn add --dev @gel/generate\n\n    .. code-tab:: bash\n      :caption: pnpm\n\n      $ pnpm add --save-dev @gel/generate\n\n    .. code-tab:: bash\n      :caption: bun\n\n      $ bun add --dev @gel/generate\n\n    .. code-tab:: bash\n      :caption: deno\n\n      $ deno add --dev npm:@gel/generate\n\nSince the generators work by connecting to the database to introspect your schema and analyze queries, you'll need to have a database connection available when running the generators and to rerun generators any time the schema changes.\n\nLike the CLI, the generators use the connection details from your initialized project, environment, or :ref:`connection flags <ref_cli_gel_connopts>` to connect to the database. See :ref:`the connection parameters reference <ref_reference_connection_parameters>` for more details.\n\nYou can ensure that the generators are always up-to-date locally by adding them to your |gel.toml|'s :ref:`schema.update.after hook <ref_reference_gel_toml_hooks>` as in this example:\n\n.. code-block:: toml\n  :caption: gel.toml\n\n  [instance]\n  server-version = \"6.4\"\n\n  [hooks]\n  schema.update.after = \"npx @gel/generate queries --file\"\n\n\nBasic usage\n===========\n\nRun a generator with the following command.\n\n.. tabs::\n\n  .. code-tab:: bash\n    :caption: npm\n\n    $ npx @gel/generate <generator> [options]\n\n  .. code-tab:: bash\n    :caption: yarn\n\n    $ yarn run -B generate <generator> [options]\n\n  .. code-tab:: bash\n    :caption: pnpm\n\n    $ pnpm exec generate <generator> [options]\n\n  .. code-tab:: bash\n    :caption: Deno\n\n    $ deno run \\\n      --allow-all \\\n      npm:@gel/generate <generator> [options]\n\n  .. code-tab:: bash\n    :caption: bun\n\n    $ bunx @gel/generate <generator> [options]\n\nThe value of ``<generator>`` should be one of the following:\n\n.. list-table::\n   :class: funcoptable\n\n   * - ``edgeql-js``\n     - Generates the query builder which provides a **code-first** way to write **fully-typed** EdgeQL queries with TypeScript. We recommend it for TypeScript users, or anyone who prefers writing queries with code.\n     - :ref:`docs <gel-js-qb>`\n\n   * - ``queries``\n     - Scans your project for ``*.edgeql`` files and generates functions that allow you to execute these queries in a typesafe way.\n     - :ref:`docs <gel-js-queries>`\n\n   * - ``interfaces``\n     - Introspects your schema and generates file containing *TypeScript interfaces* that correspond to each object type. This is useful for writing typesafe code to interact with |Gel|.\n     - :ref:`docs <gel-js-interfaces>`\n\n.. _gel_qb_target:\n\nTargets\n-------\n\nAll generators look at your environment and guess what kind of files to generate (``.ts`` vs ``.js + .d.ts``) and what module system to use (CommonJS vs ES modules). You can override this with the ``--target`` flag.\n\n.. list-table::\n\n  * - ``--target ts``\n    - Generate TypeScript files (``.ts``)\n  * - ``--target mts``\n    - Generate TypeScript files (``.mts``) with extensioned ESM imports\n  * - ``--target esm``\n    - Generate ``.js`` with ESM syntax and ``.d.ts`` declaration files\n  * - ``--target cjs``\n    - Generate JavaScript with CommonJS syntax and and ``.d.ts`` declaration files\n  * - ``--target deno``\n    - Generate TypeScript files with Deno-style ESM imports\n\nHelp\n----\n\nTo see helptext for the ``@gel/generate`` command, run the following.\n\n.. code-block:: bash\n\n  $ npx @gel/generate --help\n"
  },
  {
    "path": "docs/reference/using/js/index.rst",
    "content": ".. _gel-js-intro:\n\n==========\nTypeScript\n==========\n\n.. toctree::\n   :maxdepth: 3\n   :hidden:\n\n   client\n   datatypes\n   generation\n   queries\n   interfaces\n   querybuilder\n\n.. _gel-js-installation:\n\nInstallation\n============\n\nInstall the `client <https://www.npmjs.com/package/gel>`_ and optional (but recommended!) `generator <https://www.npmjs.com/package/@gel/generate>`_ packages from npm using your package manager of choice.\n\n.. tabs::\n\n    .. code-tab:: bash\n      :caption: npm\n\n      $ npm install --save-prod gel          # database client\n      $ npm install --save-dev @gel/generate # generators\n\n    .. code-tab:: bash\n      :caption: yarn\n\n      $ yarn add gel                 # database client\n      $ yarn add --dev @gel/generate # generators\n\n    .. code-tab:: bash\n      :caption: pnpm\n\n      $ pnpm add --save-prod gel          # database client\n      $ pnpm add --save-dev @gel/generate # generators\n\n    .. code-tab:: bash\n      :caption: bun\n\n      $ bun add gel                 # database client\n      $ bun add --dev @gel/generate # generators\n\n    .. code-tab:: bash\n      :caption: deno\n\n      $ deno add npm:gel                 # database client\n      $ deno add --dev npm:@gel/generate # generators\n\n.. _gel-js-examples:\n\nBasic Usage\n===========\n\nThe ``gel`` package exposes a :ref:`createClient <gel-js-create-client>` function that can be used to create a new :ref:`Client <gel-js-api-client>` instance. This client instance manages a pool of connections to the database which it discovers automatically from either being in a :gelcmd:`project init` directory or being provided connection details via Environment Variables. See :ref:`the environment section of the connection reference <ref_reference_connection_environments>` for more details and options.\n\n.. note::\n\n  If you're using |Gel| Cloud to host your development instance, you can use the :gelcmd:`cloud login` command to authenticate with |Gel| Cloud and then use the :gelcmd:`project init --server-instance <instance-name>` command to create a local project-linked instance that is linked to an Gel Cloud instance. For more details, see :ref:`the Gel Cloud guide <ref_guide_cloud>`.\n\nOnce you have a client instance, you can use the various :ref:`query methods <gel-js-running-queries>` to execute queries. Each of these methods has an implied cardinality of the result, and if you're using TypeScript, you can provide a type parameter to receive a strongly typed result.\n\n.. code-block:: bash\n\n  $ mkdir gel-js-example\n  $ cd gel-js-example\n  $ npm init -y\n  $ npm install gel\n  $ npm install --save-dev @gel/generate\n  $ npx gel project init --non-interactive\n  $ touch index.mjs\n\n.. code-block:: javascript\n  :caption: index.mjs\n\n  import { createClient } from \"gel\";\n  import assert from \"node:assert\";\n\n  const client = createClient(); // get connection details automatically\n\n  // Query always returns an array of result, even for single object queries\n  const queryResult = await client.query(\"select 1\");\n  assert.equal(queryResult, [1]);\n\n  // querySingle will throw an error if the query returns more than one row\n  const singleQueryResult = await client.querySingle(\"select 1\");\n  assert.equal(singleQueryResult, 1);\n\n  // queryRequired will throw an error if the query returns no rows\n  const requiredQueryResult = await client.queryRequired(\"select 1\");\n  assert.equal(requiredQueryResult, 1);\n\n  // queryRequiredSingle will throw an error if\n  // - the query returns more than one row\n  // - the query returns no rows\n  const requiredSingleQueryResult = await client.queryRequiredSingle(\"select 1\");\n  assert.equal(requiredSingleQueryResult, 1);\n\nCode generation\n===============\n\nThe ``@gel/generate`` npm package provides a set of generators that can make querying the database a bit more pleasant than manually constructing strings and passing explicit query element types to the query methods.\n\n``interfaces`` generator\n------------------------\n\nThe :ref:`interfaces <gel-js-interfaces>` generator will create TypeScript interfaces for the object types in your database.\n\n.. code-block:: bash\n\n  $ npx @gel/generate interfaces\n\n.. code-block:: typescript\n  :caption: main.mts\n\n  import { createClient } from \"gel\";\n  import { Movie } from \"./dbschema/interfaces\";\n\n  const client = createClient();\n\n  const result = await client.query<Movie[]>(`\n    select Movie {\n      **,\n      actors: { ** },\n    };\n  `);\n\n  console.log(result);\n\n``queries`` generator\n----------------------\n\nThe :ref:`queries <gel-js-queries>` generator will create TypeScript functions for any EdgeQL queries defined in your project in separate ``.edgeql`` files.\n\n.. code-block:: edgeql\n  :caption: get-movies.edgeql\n\n  select Movie {\n    **,\n    actors: { ** },\n  };\n\n.. code-block:: bash\n\n  $ npx @gel/generate queries\n\n.. code-block:: typescript\n  :caption: main.mts\n\n  import { createClient } from \"gel\";\n  import { getMovies } from \"./get-movies.query\";\n\n  const client = createClient();\n\n  const result = await getMovies(client);\n\n  console.log(result);\n\n``edgeql-js`` generator\n-----------------------\n\nThe :ref:`edgeql-js <gel-js-qb>` generator will create a fully type-safe query builder that you can use to write code-first queries in TypeScript. This is the recommended way to write dynamic queries and many people prefer it even for static queries.\n\n.. code-block:: bash\n\n  $ npx @gel/generate edgeql-js\n\n.. code-block:: typescript\n  :caption: main.mts\n\n  import { createClient } from \"gel\";\n  import e from \"./dbschema/edgeql-js\";\n\n  const client = createClient();\n\n  const result = await e\n    .params({ title: e.str }, (params) =>\n      e.select(e.Movie, (m) => ({\n        filter_single: e.op(m.title, \"=\", params.title),\n        id: true,\n        title: true,\n        actors: { name: true },\n      })),\n    )\n    .run(client, {\n      title: \"The Matrix\",\n    });\n\n  console.log(result);\n\n\nNext steps\n==========\n\nIf you haven't already done so, you can go through our :ref:`quickstart tutorial <ref_quickstart>` to have a guided tour of using Gel as the data layer for a complex web application.\n\nYou will also find full reference information in this section of the documentation for the various generators and public APIs that the :ref:`gel <gel-js-client>` and :ref:`@gel/generate <gel-js-generators>` packages provide.\n"
  },
  {
    "path": "docs/reference/using/js/interfaces.rst",
    "content": ".. _gel-js-interfaces:\n\n====================\nInterfaces Generator\n====================\n\nThe ``interfaces`` generator introspects your schema and generates file containing *TypeScript interfaces* that correspond to each object type. This is useful for writing typesafe code to interact with |Gel|.\n\nInstallation\n------------\n\nTo get started, install the following packages.\n\nInstall the ``gel`` package.\n\n.. code-block:: bash\n\n  $ npm install gel       # npm users\n  $ yarn add gel          # yarn users\n  $ pnpm add gel          # pnpm users\n  $ bun add gel           # bun users\n  $ deno add npm:gel      # deno users\n\nThen install ``@gel/generate`` as a dev dependency.\n\n.. code-block:: bash\n\n  $ npm install @gel/generate --save-dev      # npm users\n  $ yarn add @gel/generate --dev              # yarn users\n  $ pnpm add --dev @gel/generate              # pnpm users\n  $ bun add --dev @gel/generate               # bun users\n  $ deno add --dev npm:@gel/generate          # deno users\n\nGeneration\n----------\n\nAssume your database contains the following Gel schema.\n\n.. code-block:: sdl\n\n  module default {\n    type Person {\n      required name: str;\n    }\n\n    scalar type Genre extending enum<Horror, Comedy, Drama>;\n\n    type Movie {\n      required title: str;\n      genre: Genre;\n      multi actors: Person;\n    }\n  }\n\nThe following command will run the ``interfaces`` generator.\n\n.. tabs::\n\n  .. code-tab:: bash\n    :caption: Node.js\n\n    $ npx @gel/generate interfaces\n\n  .. code-tab:: bash\n    :caption: Deno\n\n    $ deno run --allow-all npm:@gel/generate interfaces\n\n  .. code-tab:: bash\n    :caption: Bun\n\n    $ bunx @gel/generate interfaces\n\nThis will introspect your schema and generate TypeScript interfaces that correspond to each object type. By default, these interfaces will be written to a single file called ``interfaces.ts`` into the ``dbschema`` directory in your project root. The file will contain the following contents (roughly):\n\n.. code-block:: typescript\n\n  export interface Person {\n    id: string;\n    name: string;\n  }\n\n  export type Genre = \"Horror\" | \"Comedy\" | \"Drama\";\n\n  export interface Movie {\n    id: string;\n    title: string;\n    genre?: Genre | null;\n    actors: Person[];\n  }\n\nAny types declared in a non-``default`` module  will be generated into an accordingly named ``namespace``.\n\n.. note::\n\n  Generators work by connecting to the database to get information about the current state of the schema. Make sure you run the generators again any time the schema changes so that the generated code is in-sync with the current state of the schema. The easiest way to do this is to add the generator command to the :ref:`schema.update.after hook <ref_reference_gel_toml_hooks>` in your :ref:`gel.toml <ref_reference_gel_toml>`.\n\n\nCustomize file path\n~~~~~~~~~~~~~~~~~~~\n\nPass a ``--file`` flag to specify the output file path.\n\n.. code-block:: bash\n\n  $ npx @gel/generate interfaces --file schema.ts\n\nIf the value passed as ``--file`` is a relative path, it will be evaluated relative to the current working directory (``process.cwd()``). If the value is an absolute path, it will be used as-is.\n\n.. note::\n\n  Because this generator is TypeScript-specific, the ``--target`` flag is not supported as in other generators.\n\n\nVersion control\n~~~~~~~~~~~~~~~\n\nTo exclude the generated file, add the following lines to your ``.gitignore`` file.\n\n.. code-block:: text\n\n  dbschema/interfaces.ts\n\nUsage\n-----\n\nThe generated interfaces can be imported like so.\n\n.. code-block:: typescript\n\n  import {Genre, Movie} from \"./dbschema/interfaces\";\n\nYou will need to manipulate the generated interfaces to match your application's needs. For example, you may wish to strip the ``id`` property for a ``createMovie`` mutation.\n\n.. code-block:: typescript\n\n  function createMovie(data: Omit<Movie, \"id\">) {\n    // ...\n  }\n\n.. note::\n\n  Refer to the `TypeScript docs <https://www.typescriptlang.org/docs/handbook/utility-types.html>`_ for information about built-in utility types like ``Pick``, ``Omit``, and ``Partial``.\n\nFor convenience, the file also exports a namespace called ``helper`` containing a couple useful utilities for extracting the properties or links from an object type interface.\n\n.. code-block:: typescript\n\n  import type { Movie, helper } from \"./dbschema/interfaces\";\n\n  type MovieProperties = helper.Props<Movie>;\n  // { id: string; title: string; ... }\n\n  type MovieLinks = helper.Links<Movie>;\n  // { actors: Person[]; }\n\n\nEnums\n~~~~~\n\nNote that an ``enum`` in your schema will be represented in the generated code as a union of string literals.\n\n.. code-block:: typescript\n\n  export type Genre = \"Horror\" | \"Comedy\" | \"Drama\";\n\nWe do *not* generate TypeScript enums for a number of reasons.\n\n- In TypeScript, enums are nominally typed. Two identically named enums are not considered equal, even if they have the same members.\n- Enums are both a runtime and static construct. Hovever, for simplicity we want the ``interfaces`` generator to produce exclusively static (type-level) code.\n"
  },
  {
    "path": "docs/reference/using/js/queries.rst",
    "content": ".. _gel-js-queries:\n\n=================\nQueries Generator\n=================\n\nThe ``queries`` generator scans your project for ``*.edgeql`` files and generates functions that allow you to execute these queries with fully defined input parameters and return types.\n\nInstallation\n============\n\nTo get started, install the following packages.\n\nInstall the ``gel`` package.\n\n.. code-block:: bash\n\n  $ npm install gel       # npm users\n  $ yarn add gel          # yarn users\n  $ bun add gel           # bun users\n  $ deno add npm:gel      # deno users\n\nThen install ``@gel/generate`` as a dev dependency.\n\n.. code-block:: bash\n\n  $ npm install @gel/generate --save-dev      # npm users\n  $ yarn add @gel/generate --dev              # yarn users\n  $ bun add --dev @gel/generate               # bun users\n  $ deno add --dev npm:@gel/generate          # deno users\n\n\nGeneration\n==========\n\nConsider the following file tree.\n\n.. code-block:: text\n\n  .\n  ├── package.json\n  ├── gel.toml\n  ├── index.ts\n  ├── dbschema\n  └── queries\n      └── getUser.edgeql\n\n\nThe following command will run the ``queries`` generator.\n\n.. tabs::\n\n  .. code-tab:: bash\n    :caption: Node.js\n\n    $ npx @gel/generate queries\n\n  .. code-tab:: bash\n    :caption: Deno\n\n    $ deno run --allow-all npm:@gel/generate queries\n\n  .. code-tab:: bash\n    :caption: Bun\n\n    $ bunx @gel/generate queries\n\nThe generator will detect the project root by looking for an ``gel.toml``, then scan the directory for ``*.edgeql`` files. In this case, there's only one: ``queries/getUser.edgeql``.\n\n.. code-block:: edgeql\n  :caption: getUser.edgeql\n\n  select User { name, email } filter .id = <uuid>$user_id;\n\nFor each ``.edgeql`` file, the generator will read the contents and send the query to the database, which returns type information about its parameters and return type. The generator uses this information to create a new file ``getUser.query.ts`` alongside the original ``getUser.edgeql`` file.\n\n.. code-block:: text\n\n  .\n  ├── package.json\n  ├── gel.toml\n  ├── index.ts\n  ├── dbschema\n  └── queries\n      └── getUser.edgeql\n      └── getUser.query.ts    <-- generated file\n\n\n.. note::\n\n  This example assumes you are using TypeScript. The generator tries to auto-detect the language you're using; you can also specify the language with the ``--target`` flag. See the :ref:`Targets <gel_qb_target>` section for more information.\n\nThe generated file will look something like this:\n\n.. code-block:: typescript\n\n  import type { Client } from \"gel\";\n\n  export type GetUserArgs = {\n    user_id: string;\n  };\n\n  export type GetUserReturns = {\n    name: string;\n    email: string;\n  } | null;\n\n  export async function getUser(\n    client: Client,\n    args: GetUserArgs\n  ): Promise<GetUserReturns> {\n    return await client.querySingle(\n      `select User { name, email } filter .id = <uuid>$user_id;`,\n      args\n    );\n  }\n\nSome things to note:\n\n- The first argument is a ``Client`` instance. This is the same client you would use to execute a query manually. You can use the same client for both manual and generated queries.\n- The second argument is a parameter object. The keys of this object are the names of the parameters in the query.\n- The code uses the ``querySingle`` method, since the query is only expected to return a single result.\n- We export the type of the parameter object and the return value unwrapped from the promise.\n\nWe can now use this function in our code.\n\n.. code-block:: typescript\n\n  import { getUser } from \"./queries/getUser.query\";\n  import {\n    createClient,\n    type GetUserArgs,\n    type GetUserReturns,\n  } from \"gel\";\n\n  const client = await createClient();\n\n  const newUser: GetUserArgs = {\n    user_id: \"00000000-0000-0000-0000-000000000000\"\n  };\n\n  const user = await getUser(client, newUser); // GetUserReturns\n\n  if (user) {\n    user.name; // string\n    user.email; // string\n  }\n\n.. note::\n\n  Generators work by connecting to the database to get information about the current state of the schema. Make sure you run the generators again any time the schema changes so that the generated code is in-sync with the current state of the schema. The easiest way to do this is to add the generator command to the :ref:`schema.update.after hook <ref_reference_gel_toml_hooks>` in your :ref:`gel.toml <ref_reference_gel_toml>`.\n\nSingle-file mode\n================\n\nPass the ``--file`` flag to generate a single file that contains functions for all detected ``.edgeql`` files. This lets you import all your queries from a single file.\n\nLet's say we start with the following file tree.\n\n.. code-block:: text\n\n  .\n  ├── package.json\n  ├── gel.toml\n  ├── index.ts\n  ├── dbschema\n  └── queries\n      └── getUser.edgeql\n      └── getMovies.edgeql\n\nThe following command will run the generator in ``--file`` mode.\n\n.. code-block:: bash\n\n  $ npx @gel/generate queries --file\n\nA single file will be generated that exports two functions, ``getUser`` and ``getMovies``. By default this file is generated into the ``dbschema`` directory.\n\n.. code-block:: text\n\n  .\n  ├── package.json\n  ├── gel.toml\n  ├── index.ts\n  ├── dbschema\n  │   └── queries.ts  <-- generated file\n  └── queries\n      └── getUser.edgeql\n      └── getMovies.edgeql\n\n\nWe can now use these functions in our code.\n\n.. code-block:: typescript\n\n  import * as queries from \"./dbschema/queries\";\n  import { createClient } from \"gel\";\n\n  const client = await createClient();\n\n  const movies = await queries.getMovies(client);\n  const user = await queries.getUser(client, {\n    user_id: \"00000000-0000-0000-0000-000000000000\"\n  });\n\nTo override the file path and name, you can optionally pass a value to the ``--file`` flag. Note that you should *exclude the extension*.\n\n.. code-block:: bash\n\n  $ npx @gel/generate queries --file path/to/myqueries\n\nThe file extension is determined by the generator ``--target`` and will be automatically appended to the provided path. Extensionless \"absolute\" paths will work; relative paths will be resolved relative to the current working directory.\n\nThis will result in the following file tree.\n\n.. code-block:: text\n\n  .\n  ├── package.json\n  ├── gel.toml\n  ├── path\n  │   └── to\n  │       └── myqueries.ts\n  ├── queries\n  │   └── getUser.edgeql\n  │   └── getMovies.edgeql\n  └── index.ts\n\nVersion control\n===============\n\nTo exclude the generated files, add the following lines to your ``.gitignore`` file.\n\n.. code-block:: text\n\n  **/*.query.ts\n  dbschema/queries.*\n"
  },
  {
    "path": "docs/reference/using/js/querybuilder.rst",
    "content": ".. _gel-js-qb:\n\n=======================\nQuery Builder Generator\n=======================\n\n.. index:: querybuilder, generator, typescript\n\nOverview\n========\n\nThe |Gel| query builder provides a **code-first** way to write **fully-typed** EdgeQL queries with TypeScript. Unlike traditional ORMs that offer a limited set of operations, the query builder leverages EdgeQL's composable nature, allowing developers to dynamically construct complex queries by combining expressions. This approach is particularly useful in scenarios where queries need to change dynamically at runtime. Even for static queries, the query builder offers the benefits of type safety and autocompletion, helping you write correct queries with confidence while maintaining the full expressiveness of EdgeQL.\n\nUsage Example\n-------------\n\nAll queries on this page assume the following schema.\n\n.. code-block:: sdl\n  :class: collapsible\n\n  module default {\n    type Person {\n      required name: str;\n    }\n\n    abstract type Content {\n      required title: str {\n        constraint exclusive;\n      };\n      multi actors: Person {\n        character_name: str;\n      };\n    }\n\n    type Movie extending Content {\n      release_year: int64;\n    }\n\n    type TVShow extending Content {\n      num_seasons: int64;\n    }\n  }\n\n\n.. code-block:: typescript\n\n  import { createClient } from \"gel\";\n  import e from \"./dbschema/edgeql-js\";\n\n  const client = createClient();\n\n  const query = e.select(e.Movie, () => ({\n    id: true,\n    title: true,\n    actors: {\n      name: true,\n    },\n  }));\n\n  const result = await query.run(client)\n  /*\n    {\n      id: string;\n      title: string;\n      actors: { name: string; }[];\n    }[]\n  */\n\nIs it an ORM?\n-------------\n\nNo—it's better! Like any modern TypeScript ORM, the query builder gives you full typesafety and autocompletion, but without the power and `performance <https://github.com/geldata/imdbench>`_ tradeoffs. You have access to the **full power** of EdgeQL and can write EdgeQL queries of arbitrary complexity. And since |Gel| compiles each EdgeQL query into a single, highly-optimized SQL query, your queries stay fast, even when they're complex.\n\nWhy use the query builder?\n--------------------------\n\n* **Type inference!**\n    If you're using TypeScript, the result type of *all queries* is automatically inferred for you. For the first time, you don't need an ORM to write strongly typed queries.\n\n* **Auto-completion!**\n    You can write queries full autocompletion on EdgeQL keywords, standard library functions, and link/property names.\n\n* **Type checking!**\n    In the vast majority of cases, the query builder won't let you construct invalid queries. This eliminates an entire class of bugs and helps you write valid queries the first time.\n\n* **Close to EdgeQL!**\n    The goal of the query builder is to provide an API that is as close as possible to EdgeQL itself while feeling like idiomatic TypeScript.\n\nInstallation\n------------\n\nInstall the ``gel`` package as a production dependency and the ``@gel/generate`` package as a development dependency.\n\n.. tabs::\n\n    .. code-tab:: bash\n      :caption: npm\n\n      $ npm install --save-prod gel          # database client\n      $ npm install --save-dev @gel/generate # generators\n\n    .. code-tab:: bash\n      :caption: yarn\n\n      $ yarn add gel                 # database client\n      $ yarn add --dev @gel/generate # generators\n\n    .. code-tab:: bash\n      :caption: pnpm\n\n      $ pnpm add --save-prod gel          # database client\n      $ pnpm add --save-dev @gel/generate # generators\n\n    .. code-tab:: bash\n      :caption: bun\n\n      $ bun add gel                 # database client\n      $ bun add --dev @gel/generate # generators\n\n    .. code-tab:: bash\n      :caption: deno\n\n      $ deno add npm:gel                 # database client\n      $ deno add --dev npm:@gel/generate # generators\n\n\nGeneration\n----------\n\nThe following command will run the ``edgeql-js`` query builder generator.\n\n.. tabs::\n\n  .. code-tab:: bash\n    :caption: npm\n\n    $ npx @gel/generate edgeql-js\n\n  .. code-tab:: bash\n    :caption: yarn\n\n    $ yarn run -B generate edgeql-js\n\n  .. code-tab:: bash\n    :caption: pnpm\n\n    $ pnpm exec generate edgeql-js\n\n  .. code-tab:: bash\n    :caption: Deno\n\n    $ deno run --allow-all npm:@gel/generate edgeql-js\n\n  .. code-tab:: bash\n    :caption: Bun\n\n    $ bunx @gel/generate edgeql-js\n\nThe generation command is configurable in a number of ways.\n\n``--output-dir <path>``\n  Sets the output directory for the generated files.\n\n``--target <ts|cjs|esm|mts>``\n  What type of files to generate.\n\n``--force-overwrite``\n  To avoid accidental changes, you'll be prompted to confirm whenever the\n  ``--target`` has changed from the previous run. To avoid this prompt, pass\n  ``--force-overwrite``.\n\nThe generator also supports all the :ref:`connection flags\n<ref_cli_gel_connopts>` supported by the |Gel| CLI. These aren't\nnecessary when using a project or environment variables to configure a\nconnection.\n\n.. note::\n\n  Generators work by connecting to the database to get information about the current state of the schema. Make sure you run the generators again any time the schema changes so that the generated code is in-sync with the current state of the schema. The easiest way to do this is to add the generator command to the :ref:`schema.update.after hook <ref_reference_gel_toml_hooks>` in your :ref:`gel.toml <ref_reference_gel_toml>`.\n\n.. _gel-js-execution:\n\nExpressions\n-----------\n\nThroughout the documentation, we use the term \"expression\" a lot. This is a catch-all term that refers to *any query or query fragment* you define with the query builder. They all conform to an interface called ``Expression`` with some common functionality.\n\nMost importantly, any expression can be executed with the ``.run()`` method, which accepts a ``Client`` or ``Transaction`` instance as the first argument. The result is ``Promise<T>``, where ``T`` is the inferred type of the query.\n\n.. code-block:: typescript\n\n  await e.str(\"hello world\").run(client);\n  // => \"hello world\"\n\n  await e.set(e.int64(1), e.int64(2), e.int64(3)).run(client);\n  // => [1, 2, 3]\n\n  await e\n    .select(e.Movie, () => ({\n      title: true,\n      actors: { name: true },\n    }))\n    .run(client);\n  // => [{ title: \"The Avengers\", actors: [...]}]\n\n.. _gel-js-objects:\n\nObjects and Paths\n-----------------\n\nAll object types in your schema are reflected into the query builder, properly namespaced by module.\n\n.. code-block:: typescript\n\n  e.default.Person;\n  e.default.Movie;\n  e.default.TVShow;\n  e.my_module.SomeType;\n\nFor convenience, the contents of the ``default`` module are also available at the top-level of ``e``.\n\n.. code-block:: typescript\n\n  e.Person;\n  e.Movie;\n  e.TVShow;\n\nPaths\n^^^^^\n\nEdgeQL-style *paths* are supported on object type references.\n\n.. code-block:: typescript\n\n  e.Person.name;              // Person.name\n  e.Movie.title;              // Movie.title\n  e.TVShow.actors.name;          // Movie.actors.name\n\nPaths can be constructed from any object expression, not just the root types.\n\n.. code-block:: typescript\n\n  e.select(e.Person).name;\n  // EdgeQL: (select Person).name\n\n  e.op(e.Movie, \"union\", e.TVShow).actors;\n  // EdgeQL: (Movie union TVShow).actors\n\n  const ironMan = e.insert(e.Movie, {\n    title: \"Iron Man\"\n  });\n  ironMan.title;\n  // EdgeQL: (insert Movie { title := \"Iron Man\" }).title\n\n\n.. _gel-js-objects-type-intersections:\n\nType intersections\n^^^^^^^^^^^^^^^^^^\n\nUse the type intersection operator to narrow the type of a set of objects. For instance, to represent the elements of an Account's watchlist that are of type ``TVShow``:\n\n.. code-block:: typescript\n\n  e.Person.acted_in.is(e.TVShow);\n  // Person.acted_in[is TVShow]\n\n\nBacklinks\n^^^^^^^^^\n\nAll possible backlinks are auto-generated and can be auto-completed by TypeScript. They behave just like forward links. However, because they contain a special character (``<``), you must use bracket syntax instead of simple dot notation.\n\n.. code-block:: typescript\n\n  e.Person[\"<director[is Movie]\"]\n  // Person.<director[is Movie]\n\nFor convenience, these backlinks automatically combine the backlink operator and type intersection into a single key. However, the query builder also provides \"plain\" backlinks; these can be refined with the ``.is`` type intersection method.\n\n.. code-block:: typescript\n\n  e.Person['<director'].is(e.Movie);\n  // Person.<director[is Movie]\n\nConverting to EdgeQL\n--------------------\n\n.. index:: querybuilder, toedgeql\n\nYou can extract an EdgeQL representation of any expression calling the ``.toEdgeQL()`` method. Below is a number of expressions and the logical EdgeQL they produce. The query builder does some optimizing and scoping of the query, so the actual EdgeQL will look slightly different, but it's equivalent.\n\n.. code-block:: typescript\n\n  e.str(\"hello world\").toEdgeQL();\n  // => select \"hello world\"\n\n  e.set(e.int64(1), e.int64(2), e.int64(3)).toEdgeQL();\n  // => select {1, 2, 3}\n\n  e.select(e.Movie, () => ({\n    title: true,\n    actors: { name: true }\n  })).toEdgeQL();\n  // => select Movie { title, actors: { name }}\n\nExtracting the inferred type\n----------------------------\n\nThe query builder *automatically infers* the TypeScript type that best represents the result of a given expression. This inferred type can be extracted with the ``$infer`` type helper.\n\n.. code-block:: typescript\n\n  import e, { type $infer } from \"./dbschema/edgeql-js\";\n\n  const query = e.select(e.Movie, () => ({ id: true, title: true }));\n  type result = $infer<typeof query>;\n  // { id: string; title: string }[]\n\nIt even infers the cardinality of the query based on things like filtering on exclusive properties and usage of our cardinality assertion functions.\n\n.. code-block:: typescript\n\n  import e, { type $infer } from \"./dbschema/edgeql-js\";\n\n  const query = e.select(e.Movie, () => ({\n    filter_single: { id: \"00000000-0000-0000-0000-000000000000\" },\n    id: true,\n    title: true,\n  }));\n  type result = $infer<typeof query>;\n  // { id: string; title: string } | null\n\nBasic usage\n===========\n\nBelow is a set of examples to get you started with the query builder. It is not intended to be comprehensive, but it should provide a good starting point.\n\nInsert an object\n----------------\n\n.. code-block:: typescript\n\n  const query = e.insert(e.Movie, {\n    title: 'Doctor Strange 2',\n    release_year: 2022\n  });\n\n  const result = await query.run(client);\n  // { id: string }\n  // by default INSERT only returns the id of the new object\n\nSee also:\n* :ref:`EdgeQL <ref_eql_insert>`\n\n.. _gel-js-qb-transaction:\n\nTransaction\n-----------\n\nWe can also run the same query as above, build with the query builder, in a transaction.\n\n.. code-block:: typescript\n\n  const query = e.insert(e.Movie, {\n    title: 'Doctor Strange 2',\n    release_year: 2022\n  });\n\n  await client.transaction(async (tx) => {\n    const result = await query.run(tx);\n    // { id: string }\n  });\n\nSee also:\n\n* :ref:`EdgeQL <ref_eql_transactions>`\n\n.. _gel-js-parameters:\n\nParameters\n----------\n\nYou can pass strongly-typed parameters into your query with ``e.params``.\n\n.. code-block:: typescript\n\n  const helloQuery = e.params({name: e.str}, (params) =>\n    e.op('Yer a wizard, ', '++', params.name)\n  );\n  /*  with name := <str>$name\n      select name;\n  */\n\n\nThe first argument is an object defining the parameter names and their corresponding types. The second argument is a closure that returns an expression; use the ``params`` argument to construct the rest of your query.\n\nSee also:\n\n* :ref:`EdgeQL <ref_eql_params>`\n\nPassing parameter data\n^^^^^^^^^^^^^^^^^^^^^^\n\nTo execute a query with parameters, pass the data as the second argument to ``.run()``; this argument is *fully typed*!\n\n.. code-block:: typescript\n\n  await helloQuery.run(client, { name: \"Harry Styles\" })\n  // => \"Yer a wizard, Harry Styles\"\n\n  await helloQuery.run(client, { name: 16 })\n  // => TypeError: number is not assignable to string\n\nTop-level usage\n^^^^^^^^^^^^^^^\n\nNote that you must call ``.run`` on the result of ``e.params``; in other words, you can only use ``e.params`` at the *top level* of your query, not as an expression inside a larger query.\n\n.. code-block:: typescript\n\n  // ❌ TypeError\n  const wrappedQuery = e.select(helloQuery);\n  wrappedQuery.run(client, {name: \"Harry Styles\"});\n\n\n.. _gel-js-optional-parameters:\n\nOptional parameters\n^^^^^^^^^^^^^^^^^^^\n\nA type can be made optional with the ``e.optional`` function.\n\n.. code-block:: typescript\n\n  const query = e.params(\n    {\n      title: e.str,\n      duration: e.optional(e.duration),\n    },\n    (params) => {\n      return e.insert(e.Movie, {\n        title: params.title,\n        duration: params.duration,\n      });\n    }\n  );\n\n  // works with duration\n  const result = await query.run(client, {\n    title: \"The Eternals\",\n    duration: Duration.from({hours: 2, minutes: 3})\n  });\n\n  // or without duration\n  const result = await query.run(client, { title: \"The Eternals\" });\n\nComplex types\n^^^^^^^^^^^^^\n\nIn EdgeQL, parameters can only be primitives or arrays of primitives. That's not true with the query builder! Parameter types can be arbitrarily complex. If you need to pass optional data in a nested parameter, you can use ``e.json`` and cast the data to the correct type in the query.\n\n.. code-block:: typescript\n\n  const insertMovie = e.params(\n    {\n      title: e.str,\n      release_year: e.int64,\n      actors: e.json,\n    },\n    (params) =>\n      e.insert(e.Movie, {\n        title: params.title,\n        release_year: params.release_year,\n        actors: e.for(e.json_array_unpack(params.actors), (actor) =>\n          e.insert(e.Person, {\n            name: e.cast(e.str, actor.name),\n          })\n        ),\n      })\n  );\n\n  await insertMovie.run(client, {\n    title: \"Dune\",\n    release_year: 2021,\n    actors: [{ name: \"Timmy\" }, { name: \"JMo\" }],\n  });\n\nInsert multiple objects\n-----------------------\n\nYou can iterate over an array of input values to insert multiple objects at once by unpacking an array of named tuples into a set and passing that set to the ``e.for`` function.\n\n.. code-block:: typescript\n\n  const movies = [\n    {\n      title: \"Doctor Strange 2\",\n      release_year: 2022,\n    },\n    {\n      title: \"The Avengers\",\n      release_year: 2012,\n    },\n  ];\n  const query = e.params(\n    {\n      movies: e.array(e.tuple({\n        title: e.str,\n        release_year: e.int64,\n      }))\n    },\n    (params) => e.for(\n      e.array_unpack(params.movies),\n      (movie) => e.insert(e.Movie, {\n        title: movie.title,\n        release_year: movie.release_year,\n      })\n    )\n  );\n\n  const result = await query.run(client, { movies });\n  // { id: string }[]\n\nSelect objects\n--------------\n\n.. code-block:: typescript\n\n  const query = e.select(e.Movie, () => ({\n    id: true,\n    title: true,\n  }));\n\n  const result = await query.run(client);\n  // { id: string; title: string; }[]\n\nTo select all properties of an object, use the spread operator with the special ``*`` property:\n\n.. code-block:: typescript\n\n  const query = e.select(e.Movie, () => ({\n    ...e.Movie['*']\n  }));\n\n  const result = await query.run(client);\n  /*\n    {\n      id: string;\n      title: string;\n      release_year: number | null;  # optional property\n    }[]\n  */\n\nNested shapes\n-------------\n\n.. code-block:: typescript\n\n  const query = e.select(e.Movie, () => ({\n    id: true,\n    title: true,\n    actors: {\n      name: true,\n    }\n  }));\n\n  const result = await query.run(client);\n  /*\n    {\n      id: string;\n      title: string;\n      actors: { name: string; }[];\n    }[]\n  */\n\nIf you need to create computed properties on the nested object, you can pass a closure to the nested object.\n\n.. code-block:: typescript\n\n  const query = e.select(e.Movie, () => ({\n    id: true,\n    title: true,\n    actors: (a) => ({\n      id: true,\n      name: true,\n      lower_name: e.str_lower(a.name),\n      upper_name: e.str_upper(a.name),\n    }),\n  }));\n\n  const result = await query.run(client);\n  /*\n    {\n      id: string;\n      title: string;\n      actors: {\n        id: string;\n        name: string;\n        lower_name: string;\n        upper_name: string;\n      }[];\n    }[]\n  */\n\nFiltering\n---------\n\nPass a boolean expression as the special key ``filter`` to filter the results. You can even filter nested objects.\n\n.. code-block:: typescript\n\n  const query = e.select(e.Movie, (movie) => ({\n    // special \"filter\" key\n    filter: e.op(movie.release_year, \">\", 1999),\n\n    id: true,\n    title: true,\n    actors: (a) => ({\n      // nested filter\n      filter: e.op(a.name, \"ilike\", \"a%\"),\n      name: true,\n      id: true,\n    }),\n  }));\n\n  const result = await query.run(client);\n  // { id: string; title: number }[]\n\nSince ``filter`` is a reserved keyword in EdgeQL, the special ``filter`` key can live alongside your property keys without a risk of collision.\n\n.. note::\n\n  The ``e.op`` function is used to express EdgeQL operators. It is documented in more detail below and on the :ref:`Functions and operators <gel-js-funcops>` page.\n\nSelect a single object\n----------------------\n\nTo select a particular object, use the ``filter_single`` key and filter on an exclusive property. This tells the query builder to expect a singleton result.\n\n.. code-block:: typescript\n\n  const query = e.select(e.Movie, (movie) => ({\n    id: true,\n    title: true,\n    release_year: true,\n\n    filter_single: e.op(\n      movie.id,\n      \"=\",\n      e.uuid(\"2053a8b4-49b1-437a-84c8-e1b0291ccd9f\")\n    ),\n  }));\n\n  const result = await query.run(client);\n  // { id: string; title: string; release_year: number | null }\n\nFor convenience ``filter_single`` also supports a simplified syntax that eliminates the need for ``e.op`` when used on exclusive properties:\n\n.. code-block:: typescript\n\n  e.select(e.Movie, (movie) => ({\n    id: true,\n    title: true,\n    release_year: true,\n\n    filter_single: { id: \"2053a8b4-49b1-437a-84c8-e1b0291ccd9f\" },\n  }));\n\nThis also works if an object type has a composite exclusive constraint. Each property in the object will be combined with an ``and`` to form the final filter expression that matches the composite exclusive constraint.\n\n.. code-block:: typescript\n\n  /*\n    type Movie {\n      ...\n      constraint exclusive on (.title, .release_year);\n    }\n  */\n\n  e.select(e.Movie, (movie) => ({\n    title: true,\n    filter_single: {\n      title: \"The Avengers\",\n      release_year: 2012\n    },\n  }));\n\n\nOrdering and pagination\n-----------------------\n\nThe special keys ``order_by``, ``limit``, and ``offset`` correspond to equivalent EdgeQL clauses.\n\n.. code-block:: typescript\n\n  const query = e.select(e.Movie, (movie) => ({\n    id: true,\n    title: true,\n\n    order_by: movie.title,\n    limit: 10,\n    offset: 10\n  }));\n\n  const result = await query.run(client);\n  // { id: true; title: true }[]\n\nOperators and functions\n-----------------------\n\nNote that the filter expression above uses ``e.op`` function, which is how to\nuse *operators* like ``=``, ``>=``, ``++``, and ``and``.\n\n.. code-block:: typescript\n\n  // prefix (unary) operators\n  e.op(\"not\", e.bool(true));      // not true\n  e.op(\"exists\", e.set(\"hi\"));    // exists {\"hi\"}\n\n  // infix (binary) operators\n  e.op(e.int64(2), \"+\", e.int64(2)); // 2 + 2\n  e.op(e.str(\"Hello \"), \"++\", e.str(\"World!\")); // \"Hello \" ++ \"World!\"\n\n  // ternary operator (if/else)\n  e.op(e.str(\"😄\"), \"if\", e.bool(true), \"else\", e.str(\"😢\"));\n  // \"😄\" if true else \"😢\"\n\nFunctions are also available as functions on the ``e`` object.\n\n.. code-block:: typescript\n\n  e.datetime_of_statement();\n  e.sum(e.set(e.int64(1), e.int64(2), e.int64(3)));\n  e.assert_single(e.select(/* some query */));\n\n\nUpdate objects\n--------------\n\n.. code-block:: typescript\n\n  const query = e.update(e.Movie, (movie) => ({\n    filter_single: { title: \"Doctor Strange 2\" },\n    set: {\n      title: \"Doctor Strange in the Multiverse of Madness\",\n    },\n  }));\n\n  const result = await query.run(client);\n\nDelete objects\n--------------\n\n.. code-block:: typescript\n\n  const query = e.delete(e.Movie, (movie) => ({\n    filter: e.op(movie.title, 'ilike', \"the avengers%\"),\n  }));\n\n  const result = await query.run(client);\n  // { id: string }[]\n\nDelete multiple objects using an array of properties:\n\n.. code-block:: typescript\n\n  const titles = [\"The Avengers\", \"Doctor Strange 2\"];\n  const query = e.delete(e.Movie, (movie) => ({\n    filter: e.op(\n      movie.title,\n      \"in\",\n      e.array_unpack(e.literal(e.array(e.str), titles))\n    )\n  }));\n  const result = await query.run(client);\n  // { id: string }[]\n\nNote that we have to use ``array_unpack`` to cast our ``array<str>`` into a ``set<str>`` since the ``in`` operator works on sets. And we use ``literal`` to create a custom literal since we're inlining the titles array into our query.\n\nTypically you'll want to pass data into a query using params. Here's an example of how to do this with params:\n\n.. code-block:: typescript\n\n  const titles = [\"The Avengers\", \"Doctor Strange 2\"];\n  const query = e.params(\n    { titles: e.array(e.str) },\n    (params) => e.delete(e.Movie, (movie) => ({\n      filter: e.op(movie.title, \"in\", e.array_unpack(params.titles)),\n    }))\n  );\n\n  const result = await query.run(client, { titles });\n  // { id: string }[]\n\nCompose queries\n---------------\n\nAll query expressions are fully composable; this is one of the major differentiators between this query builder and a typical ORM. For instance, we can ``select`` an ``insert`` query in order to fetch properties of the object we just inserted.\n\n\n.. code-block:: typescript\n\n  const newMovie = e.insert(e.Movie, {\n    title: \"Iron Man\",\n    release_year: 2008\n  });\n\n  const query = e.select(newMovie, () => ({\n    title: true,\n    release_year: true,\n    num_actors: e.count(newMovie.actors)\n  }));\n\n  const result = await query.run(client);\n  // { title: string; release_year: number; num_actors: number }\n\nOr we can use subqueries inside mutations.\n\n.. code-block:: typescript\n\n  // select Doctor Strange\n  const drStrange = e.select(e.Movie, (movie) => ({\n    filter_single: { title: \"Doctor Strange\" }\n  }));\n\n  // select actors\n  const actors = e.select(e.Person, (person) => ({\n    filter: e.op(\n      person.name,\n      \"in\",\n      e.set(\"Benedict Cumberbatch\", \"Rachel McAdams\")\n    )\n  }));\n\n  // add actors to cast of drStrange\n  const query = e.update(drStrange, () => ({\n    actors: { \"+=\": actors }\n  }));\n\n  const result = await query.run(client);\n\n\n.. _ref_geljs_globals:\n\nGlobals\n-------\n\nReference global variables.\n\n.. code-block:: typescript\n\n  e.global.user_id;\n  e.default.global.user_id;  // same as above\n  e.my_module.global.some_value;\n\nOther modules\n-------------\n\nReference entities in modules other than ``default``.\n\nThe ``Vampire`` type in a module named ``characters``:\n\n.. code-block:: typescript\n\n  e.characters.Vampire;\n\nAs shown in \"Globals,\" a global ``some_value`` in a module ``my_module``:\n\n.. code-block:: typescript\n\n  e.my_module.global.some_value;\n\nAdvanced usage\n==============\n\ne.for vs JS for or .forEach\n---------------------------\n\nYou may be tempted to use JavaScript's ``for`` or the JavaScript array's ``.forEach`` method to avoid having to massage your data into a set for consumption by ``e.for``. This approach comes at a cost of performance.\n\nIf you use ``for`` or ``.forEach`` to iterate over a standard JavaScript data structure and run separate queries for each item in your iterable, you're doing just that: running separate queries for each item in your iterable. By iterating inside your query using ``e.for``, you're guaranteed everything will happen in a single query.\n\nIn addition to the performance implications, a single query means that either everything succeeds or everything fails. You will never end up with only some of your data inserted. This ensures your data integrity is maintained. You could achieve this yourself by wrapping your batch queries with :ref:`a transaction <gel-js-qb-transaction>`, but a single query is already atomic without any additional work on your part.\n\nUsing ``e.for`` to run a single query is generally the best approach. When dealing with extremely large datasets, you can define the query once, chunk the data, and run the query in batches.\n\n.. _gel-js-for-bulk-inserts:\n\nBulk inserts\n------------\n\nIt's common to use ``for`` expressions to perform bulk inserts. In this example, the raw data is passed in as a ``json`` parameter, converted to a set of ``json`` objects with ``json_array_unpack``, then passed into a ``for`` expression for insertion.\n\n.. code-block:: typescript\n\n  const query = e.params(\n    { items: e.json },\n    (params) => e.for(\n      e.json_array_unpack(params.items),\n      (item) => e.insert(e.Movie, {\n        title: e.cast(e.str, item.title),\n        release_year: e.cast(e.int64, item.release_year),\n      })\n    )\n  );\n\n  const result = await query.run(client, {\n    items: [\n      { title: \"Deadpool\", release_year: 2016 },\n      { title: \"Deadpool 2\", release_year: 2018 },\n      { title: \"Deadpool 3\", release_year: 2024 },\n      { title: \"Deadpool 4\", release_year: null },\n    ],\n  });\n\nNote that any optional properties values must be explicitly set to ``null``.  They cannot be set to ``undefined`` or omitted; doing so will cause a runtime error.\n\n.. _gel-js-for-bulk-inserts-conflicts:\n\nHandling conflicts in bulk inserts\n----------------------------------\n\nHere's a more complex example, demonstrating how to complete a nested insert with conflicts on the inner items. First, let's recall that the ``Movie`` type's ``title`` property has an exclusive constraint.\n\nHere's the data we want to bulk insert:\n\n.. code-block:: javascript\n\n    [\n      {\n        portrayed_by: \"Robert Downey Jr.\",\n        name: \"Iron Man\",\n        movies: [\"Iron Man\", \"Iron Man 2\", \"Iron Man 3\"]\n      },\n      {\n        portrayed_by: \"Chris Evans\",\n        name: \"Captain America\",\n        movies: [\n          \"Captain America: The First Avenger\",\n          \"The Avengers\",\n          \"Captain America: The Winter Soldier\",\n        ]\n      },\n      {\n        portrayed_by: \"Mark Ruffalo\",\n        name: \"The Hulk\",\n        movies: [\"The Avengers\", \"Iron Man 3\", \"Avengers: Age of Ultron\"]\n      }\n    ]\n\nThis is potentially a problem because some of the characters appear in the same movies. We can't just naively insert all the movies because we'll eventually hit a conflict. Since we're going to write this as a single query, chaining ``.unlessConflict`` on our query won't help. It only handles conflicts with objects that existed *before* the current query.\n\nLet's look at a query that can accomplish this insert, and then we'll break it down.\n\n.. code-block:: typescript\n\n  const query = e.params(\n    {\n      characters: e.array(\n        e.tuple({\n          portrayed_by: e.str,\n          name: e.str,\n          movies: e.array(e.str),\n        }),\n      ),\n    },\n    (params) => {\n      const movies = e.for(\n        e.op(\n          \"distinct\",\n          e.array_unpack(e.array_unpack(params.characters).movies),\n        ),\n        (movieTitle) =>\n          e.insert(e.Movie, { title: movieTitle }).unlessConflict((movie) => ({\n            on: movie.title,\n            else: movie,\n          })),\n      );\n      return e.with(\n        [movies],\n        e.for(e.array_unpack(params.characters), (character) =>\n          e.insert(e.Character, {\n            name: character.name,\n            portrayed_by: character.portrayed_by,\n            movies: e.assert_distinct(\n              e.select(movies, (movie) => ({\n                filter: e.op(movie.title, \"in\", e.array_unpack(character.movies)),\n              })),\n            ),\n          }),\n        ),\n      );\n    },\n  );\n\n\n.. _gel-js-for-bulk-inserts-conflicts-params:\n\nStructured params\n^^^^^^^^^^^^^^^^^\n\n.. code-block:: typescript\n\n  const query = e.params(\n    {\n      characters: e.array(\n        e.tuple({\n          portrayed_by: e.str,\n          name: e.str,\n          movies: e.array(e.str),\n        }),\n      ),\n    },\n    (params) => { ...\n\nIn raw EdgeQL, you can only have scalar types as parameters. We could mirror that here with something like this: ``e.params({characters: e.json})``, but this would then require us to cast all the values inside the JSON like ``portrayed_by`` and ``name``.\n\nBy doing it this way — typing ``characters`` with ``e.array`` and the character objects as named tuples by passing an object to ``e.tuple`` — all the data in the array will be properly cast for us. It will also better type check the data you pass to the query's ``run`` method. The restriction here is that the data must be non-optional, since tuples cannot contain optional values.\n\n.. _gel-js-for-bulk-inserts-conflicting-data:\n\nInserting the inner conflicting data\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. code-block:: typescript\n\n  ...\n    (params) => {\n      const movies = e.for(\n        e.op(\n          \"distinct\",\n          e.array_unpack(e.array_unpack(params.characters).movies),\n        ),\n        (movieTitle) =>\n          e.insert(e.Movie, { title: movieTitle }).unlessConflict((movie) => ({\n            on: movie.title,\n            else: movie,\n          })),\n      );\n  ...\n\nWe need to separate this movie insert query so that we can use ``distinct`` on it. We could just nest an insert inside our character insert if movies weren't duplicated across characters (e.g., two characters have \"The Avengers\" in ``movies``). Even though the query is separated from the character inserts here, it will still be built as part of a single EdgeQL query using ``with`` which we'll get to a bit later.\n\nThe ``distinct`` operator can only operate on sets. We use ``array_unpack`` to make these arrays into sets. We need to call it twice because ``params.characters`` is an array and ``.movies`` is an array nested inside each character.\n\nChaining ``unlessConflict`` takes care of any movies that already exist in the database *before* we run this query, but it won't handle conflicts that come about over the course of this query. The ``distinct`` operator we used earlier pro-actively eliminates any conflicts we might have had among this data.\n\n.. _gel-js-for-bulk-inserts-outer-data:\n\nInserting the outer data\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. code-block:: typescript\n\n  ...\n      return e.with(\n        [movies],\n        e.for(e.array_unpack(params.characters), (character) =>\n          e.insert(e.Character, {\n            name: character.name,\n            portrayed_by: character.portrayed_by,\n            movies: e.assert_distinct(\n              e.select(movies, (movie) => ({\n                filter: e.op(movie.title, \"in\", e.array_unpack(character.movies)),\n              })),\n            ),\n          }),\n        ),\n      );\n    },\n  );\n  ...\n\nThe query builder will try to automatically use EdgeQL's ``with``, but in this instance, it doesn't know where to place the ``with``. By using ``e.with`` explicitly, we break our movie insert out to the top-level of the query. By default, it would be scoped *inside* the query, so our ``distinct`` operator would be applied only to each character's movies instead of to all of the movies. This would have caused the query to fail.\n\nThe rest of the query is relatively straightforward. We unpack ``params.characters`` to a set so that we can pass it to ``e.for`` to iterate over the characters. For each character, we build an ``insert`` query with their ``name`` and ``portrayed_by`` values.\n\nFor the character's ``movies``, we ``select`` everything in the ``movies`` insert query we wrote previously, filtering for those with titles that match values in the ``character.movies`` array.\n\nAll that's left is to run the query, passing the data to the query's ``run`` method!\n\n.. _gel-js-for-bulk-updates:\n\nBulk updates\n------------\n\nJust like with inserts, you can run bulk updates using a ``for`` loop. Pass in your data, iterate over it, and build an ``update`` query for each item.\n\nIn this example, we use ``name`` to filter for the character to be updated since ``name`` has an exclusive constraint in the schema (meaning a given name will correspond to, at most, a single object). That filtering is done using the ``filter_single`` property of the object returned from your ``update`` callback. Then the ``last_appeared`` value is updated by including it in the nested ``set`` object.\n\n.. code-block:: typescript\n\n  const query = e.params(\n    {\n      characters: e.array(\n        e.tuple({\n          name: e.str,\n          last_appeared: e.int64,\n        }),\n      ),\n    },\n    (params) =>\n      e.for(e.array_unpack(params.characters), (character) =>\n        e.update(e.Character, () => ({\n          filter_single: { name: character.name },\n          set: {\n            last_appeared: character.last_appeared,\n          },\n        })),\n      ),\n  );\n\n  await query.run(client, {\n    characters: [\n      { name: \"Iron Man\", last_appeared: 2019 },\n      { name: \"Captain America\", last_appeared: 2019 },\n      { name: \"The Hulk\", last_appeared: 2021 },\n    ],\n  });\n\nAPI Reference\n=============\n\n.. _gel-js-types-and-casting:\n.. _gel-js-literals:\n\nTypes and Literals\n------------------\n\nThe query builder provides a set of \"helper functions\" that convert JavaScript literals into *expressions* that can be used in queries. For the most part, these helper functions correspond to the *name* of the type.\n\nPrimitives\n^^^^^^^^^^\n\nPrimitive literal expressions are created using constructor functions that correspond to Gel datatypes. Each expression below is accompanied by the EdgeQL it produces.\n\n.. code-block:: typescript\n\n  e.str(\"asdf\")            // \"asdf\"\n  e.int64(123)             // 123\n  e.float64(123.456)       // 123.456\n  e.bool(true)             // true\n  e.bigint(12345n)         // 12345n\n  e.decimal(\"1234.1234n\")  // 1234.1234n\n  e.uuid(\"599236a4...\")    // <uuid>\"599236a4...\"\n\n  e.bytes(Uint8Array.from('binary data'));\n  // b'binary data'\n\n.. _ref_qb_casting:\n\nCasting\n^^^^^^^\n\nThese types can be used to *cast* one expression to another type.\n\n.. code-block:: typescript\n\n  e.cast(e.json, e.int64('123'));\n  // <json>'123'\n\n  e.cast(e.duration, e.str('127 hours'));\n  // <duration>'127 hours'\n\n.. note::\n\n  Scalar types like ``e.str`` serve a dual purpose. They can be used as functions to instantiate literals (``e.str(\"hi\")``) or used as variables (``e.cast(e.str, e.int64(123))``).\n\nStrings\n^^^^^^^\n\nString expressions have some special functionality: they support indexing and slicing, as in EdgeQL.\n\n.. code-block:: typescript\n\n  const myString = e.str(\"hello world\");\n\n  myString[5];         //  \"hello world\"[5]\n  myString['2:5'];     //  \"hello world\"[2:5]\n  myString[':5'];      //  \"hello world\"[:5]\n  myString['2:'];      //  \"hello world\"[2:]\n\nThere are also equivalent ``.index`` and ``.slice`` methods that can accept integer expressions as arguments.\n\n.. code-block:: typescript\n\n  const myString = e.str(\"hello world\");\n  const start = e.int64(2);\n  const end = e.int64(5);\n\n  myString.index(start);          //  \"hello world\"[2]\n  myString.slice(start, end);     //  \"hello world\"[2:5]\n  myString.slice(null, end);      //  \"hello world\"[:5]\n  myString.slice(start, null);    //  \"hello world\"[2:]\n\nEnums\n^^^^^\n\nEnum literals are available as properties defined on the enum type.\n\n.. code-block:: typescript\n\n  e.Colors.green;\n  // Colors.green;\n\n  e.sys.VersionStage.beta;\n  // sys::VersionStage.beta\n\nDates and times\n^^^^^^^^^^^^^^^\n\nTo create an instance of ``datetime``, pass a JavaScript ``Date`` object into ``e.datetime``:\n\n.. code-block:: typescript\n\n  e.datetime(new Date('1999-01-01'));\n  // <datetime>'1999-01-01T00:00:00.000Z'\n\nGel's other temporal datatypes don't have equivalents in the JavaScript type system: ``duration``, ``cal::relative_duration``, ``cal::date_duration``, ``cal::local_date``, ``cal::local_time``, and ``cal::local_datetime``.\n\nTo resolve this, each of these datatypes can be represented with an instance of a corresponding class, as defined in ``gel`` module. Clients use these classes to represent these values in query results; they are documented on the :ref:`Client API <gel-js-datatypes>` docs.\n\n.. list-table::\n\n  * - ``e.duration``\n    - :js:class:`Duration`\n  * - ``e.cal.relative_duration``\n    - :js:class:`RelativeDuration`\n  * - ``e.cal.date_duration``\n    - :js:class:`DateDuration`\n  * - ``e.cal.local_date``\n    - :js:class:`LocalDate`\n  * - ``e.cal.local_time``\n    - :js:class:`LocalTime`\n  * - ``e.cal.local_datetime``\n    - :js:class:`LocalDateTime`\n  * - ``e.cal.local_datetime``\n    - :js:class:`LocalDateTime`\n  * - ``e.cal.local_datetime``\n    - :js:class:`LocalDateTime`\n\nThe code below demonstrates how to declare each kind of temporal literal, along with the equivalent EdgeQL.\n\n.. code-block:: typescript\n\n  import * as gel from \"gel\";\n\n  const myDuration = new gel.Duration(0, 0, 0, 0, 1, 2, 3);\n  e.duration(myDuration);\n\n  const myLocalDate = new gel.LocalDate(1776, 7, 4);\n  e.cal.local_date(myLocalDate);\n\n  const myLocalTime = new gel.LocalTime(13, 15, 0);\n  e.cal.local_time(myLocalTime);\n\n  const myLocalDateTime = new gel.LocalDateTime(1776, 7, 4, 13, 15, 0);\n  e.cal.local_datetime(myLocalDateTime);\n\n\nYou can also declare these literals by casting an appropriately formatted ``str`` expression, as in EdgeQL. Casting :ref:`is documented <ref_qb_casting>` in more detail later in the docs.\n\n.. code-block:: typescript\n\n  e.cast(e.duration, e.str('5 minutes'));\n  // <std::duration>'5 minutes'\n\n  e.cast(e.cal.local_datetime, e.str('1999-03-31T15:17:00'));\n  // <cal::local_datetime>'1999-03-31T15:17:00'\n\n  e.cast(e.cal.local_date, e.str('1999-03-31'));\n  // <cal::local_date>'1999-03-31'\n\n  e.cast(e.cal.local_time, e.str('15:17:00'));\n  // <cal::local_time>'15:17:00'\n\n\nJSON\n^^^^\n\nJSON literals are created with the ``e.json`` function. You can pass in any Gel-compatible data structure.\n\n.. note::\n\n  What does \"Gel-compatible\" mean? It means any JavaScript data structure with an equivalent in Gel: strings, number, booleans, ``bigint``\\ s, ``Uint8Array``\\ s, ``Date``\\ s, and instances of Gel's built-in classes: (``LocalDate`` ``LocalTime``, ``LocalDateTime``, ``DateDuration``, ``Duration``, and ``RelativeDuration``), and any array or object of these types. Other JavaScript data structures like symbols, instances of custom classes, sets, maps, and `typed arrays <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays>`_ are not supported.\n\n.. code-block:: typescript\n\n  const query = e.json({ name: \"Billie\" })\n  // to_json('{\"name\": \"Billie\"}')\n\n  const data = e.json({\n    name: \"Billie\",\n    numbers: [1,2,3],\n    nested: { foo: \"bar\"},\n    duration: new gel.Duration(1, 3, 3)\n  })\n\nJSON expressions support indexing, as in EdgeQL. The returned expression also has a ``json`` type.\n\n.. code-block:: typescript\n\n  const query = e.json({ numbers: [0,1,2] });\n\n  query.toEdgeQL(); // to_json((numbers := [0,1,2]))\n\n  query.numbers[0].toEdgeQL();\n  // to_json('{\"numbers\":[0,1,2]}')['numbers'][0]\n\nThe inferred type associated with a ``json`` expression is ``unknown``.\n\n.. code-block:: typescript\n\n  const result = await query.run(client)\n  // unknown\n\nArrays\n^^^^^^\n\nDeclare array expressions by passing an array of expressions into ``e.array``.\n\n.. code-block:: typescript\n\n  e.array([e.str(\"a\"), e.str(\"b\"), e.str(\"b\")]);\n  // [\"a\", \"b\", \"c\"]\n\nEdgeQL semantics are enforced by TypeScript, so arrays can't contain elements with incompatible types.\n\n.. code-block:: typescript\n\n  e.array([e.int64(5), e.str(\"foo\")]);\n  // TypeError!\n\nFor convenience, the ``e.array`` can also accept arrays of plain JavaScript data as well.\n\n.. code-block:: typescript\n\n  e.array(['a', 'b', 'c']);\n  // ['a', 'b', 'c']\n\n  // you can intermixing expressions and plain data\n  e.array([1, 2, e.int64(3)]);\n  // [1, 2, 3]\n\nArray expressions also support indexing and slicing operations.\n\n.. code-block:: typescript\n\n  const myArray = e.array(['a', 'b', 'c', 'd', 'e']);\n  // ['a', 'b', 'c', 'd', 'e']\n\n  myArray[1];\n  // ['a', 'b', 'c', 'd', 'e'][1]\n\n  myArray['1:3'];\n  // ['a', 'b', 'c', 'd', 'e'][1:3]\n\nThere are also equivalent ``.index`` and ``.slice`` methods that can accept other expressions as arguments.\n\n.. code-block:: typescript\n\n  const start = e.int64(1);\n  const end = e.int64(3);\n\n  myArray.index(start);\n  // ['a', 'b', 'c', 'd', 'e'][1]\n\n  myArray.slice(start, end);\n  // ['a', 'b', 'c', 'd', 'e'][1:3]\n\nTuples\n^^^^^^\n\nDeclare tuples with ``e.tuple``. Pass in an array to declare a \"regular\" (unnamed) tuple; pass in an object to declare a named tuple.\n\n.. code-block:: typescript\n\n  e.tuple([e.str(\"Peter Parker\"), e.int64(18)]);\n  // (\"Peter Parker\", 18)\n\n  e.tuple({\n    name: e.str(\"Peter Parker\"),\n    age: e.int64(18)\n  });\n  // (name := \"Peter Parker\", age := 18)\n\nTuple expressions support indexing.\n\n.. code-block:: typescript\n\n  // Unnamed tuples\n  const spidey = e.tuple([\n    e.str(\"Peter Parker\"),\n    e.int64(18)\n  ]);\n  spidey[0];                 // => (\"Peter Parker\", 18)[0]\n\n  // Named tuples\n  const spidey = e.tuple({\n    name: e.str(\"Peter Parker\"),\n    age: e.int64(18)\n  });\n  spidey.name;\n  // (name := \"Peter Parker\", age := 18).name\n\nSet literals\n^^^^^^^^^^^^\n\nDeclare sets with ``e.set``.\n\n.. code-block:: typescript\n\n  e.set(e.str(\"asdf\"), e.str(\"qwer\"));\n  // {'asdf', 'qwer'}\n\nAs in EdgeQL, sets can't contain elements with incompatible types. These\nsemantics are enforced by TypeScript.\n\n.. code-block:: typescript\n\n  e.set(e.int64(1234), e.str('sup'));\n  // TypeError\n\nEmpty sets\n^^^^^^^^^^\n\nTo declare an empty set, cast an empty set to the desired type. As in EdgeQL, empty sets are not allowed without a cast.\n\n.. code-block:: typescript\n\n  e.cast(e.int64, e.set());\n  // <std::int64>{}\n\n\nRange literals\n^^^^^^^^^^^^^^\n\nAs in EdgeQL, declare range literals with the built-in ``range`` function.\n\n.. code-block:: typescript\n\n  const myRange = e.range(0, 8);\n\n  myRange.toEdgeQL();\n  // => std::range(0, 8);\n\nRanges can be created for all numerical types, as well as ``datetime``, ``local_datetime``, and ``local_date``.\n\n.. code-block:: typescript\n\n  e.range(e.decimal('100'), e.decimal('200'));\n  e.range(Date.parse(\"1970-01-01\"), Date.parse(\"2022-01-01\"));\n  e.range(new LocalDate(1970, 1, 1), new LocalDate(2022, 1, 1));\n\nSupply named parameters as the first argument.\n\n.. code-block:: typescript\n\n  e.range({inc_lower: true, inc_upper: true, empty: true}, 0, 8);\n  // => std::range(0, 8, true, true);\n\nJavaScript doesn't have a native way to represent range values. Any range value returned from a query will be encoded as an instance of the :js:class:`Range` class, which is exported from the ``gel`` package.\n\n.. code-block:: typescript\n\n  const query = e.range(0, 8);\n  const result = await query.run(client);\n  // => Range<number>;\n\n  console.log(result.lower);       // 0\n  console.log(result.upper);       // 8\n  console.log(result.isEmpty);     // false\n  console.log(result.incLower);    // true\n  console.log(result.incUpper);    // false\n\nCustom literals\n^^^^^^^^^^^^^^^\n\nYou can use ``e.literal`` to create literals corresponding to collection types like tuples, arrays, and primitives. The first argument expects a type, the second expects a *value* of that type.\n\n.. code-block:: typescript\n\n  e.literal(e.str, \"sup\");\n  // equivalent to: e.str(\"sup\")\n\n  e.literal(e.array(e.int16), [1, 2, 3]);\n  // <array<int16>>[1, 2, 3]\n\n  e.literal(e.tuple([e.str, e.int64]), ['baz', 9000]);\n  // <tuple<str, int64>>(\"Goku\", 9000)\n\n  e.literal(\n    e.tuple({name: e.str, power_level: e.int64}),\n    {name: 'Goku', power_level: 9000}\n  );\n  // <tuple<name: str, power_level: bool>>(\"asdf\", false)\n\n.. _gel-js-funcops:\n\nFunctions and Operators\n-----------------------\n\nThe Gel :ref:`standard library <ref_std>` contains many functions and operators that you will use in your queries.\n\nFunction syntax\n^^^^^^^^^^^^^^^\n\nAll built-in standard library functions are reflected as functions in ``e``.\n\n.. code-block:: typescript\n\n  e.str_upper(e.str(\"hello\"));\n  // str_upper(\"hello\")\n\n  e.op(e.int64(2), '+', e.int64(2));\n  // 2 + 2\n\n  const nums = e.set(e.int64(3), e.int64(5), e.int64(7))\n  e.op(e.int64(4), 'in', nums);\n  // 4 in {3, 5, 7}\n\n  e.math.mean(nums);\n  // math::mean({3, 5, 7})\n\n\n.. _gel-js-funcops-prefix:\n\nPrefix operators\n^^^^^^^^^^^^^^^^\n\nUnlike functions, operators do *not* correspond to a top-level function on the ``e`` object. Instead, they are expressed with the ``e.op`` function.\n\nPrefix operators operate on a single argument: ``OPERATOR <arg>``.\n\n.. code-block:: typescript\n\n  e.op('not', e.bool(true));      // not true\n  e.op('exists', e.set('hi'));    // exists {'hi'}\n  e.op('distinct', e.set('hi', 'hi'));    // distinct {'hi', 'hi'}\n\n.. list-table::\n\n  * - ``\"exists\"`` ``\"distinct\"`` ``\"not\"``\n\n\n.. _gel-js-funcops-infix:\n\nInfix operators\n^^^^^^^^^^^^^^^\n\nInfix operators operate on two arguments: ``<arg> OPERATOR <arg>``.\n\n.. code-block:: typescript\n\n  e.op(e.str('Hello '), '++', e.str('World!'));\n  // 'Hello ' ++ 'World!'\n\n.. list-table::\n\n  * - ``\"=\"`` ``\"?=\"`` ``\"!=\"`` ``\"?!=\"`` ``\">=\"`` ``\">\"`` ``\"<=\"`` ``\"<\"``\n      ``\"or\"`` ``\"and\"`` ``\"+\"`` ``\"-\"`` ``\"*\"`` ``\"/\"`` ``\"//\"`` ``\"%\"``\n      ``\"^\"`` ``\"in\"`` ``\"not in\"`` ``\"union\"`` ``\"??\"`` ``\"++\"`` ``\"like\"``\n      ``\"ilike\"`` ``\"not like\"`` ``\"not ilike\"``\n\n\n.. _gel-js-funcops-ternary:\n\nTernary operators\n^^^^^^^^^^^^^^^^^\n\nTernary operators operate on three arguments.\n\n.. code-block:: typescript\n\n  e.op(e.str('😄'), 'if', e.bool(true), 'else', e.str('😢'));\n  // 😄 if true else 😢\n\n  e.op(\"if\", e.bool(true), \"then\", e.str('😄'), \"else\", e.str('😢'));\n  // if true then 😄 else 😢\n\n.. _gel-js-select:\n\nSelect\n------\n\nThe full power of the EdgeQL ``select`` statement is available as a top-level ``e.select`` function.\n\nScalars\n^^^^^^^\n\nAny scalar expression be passed into ``e.select``, though it's often unnecessary, since expressions are ``run``\\ able without being wrapped by ``e.select``.\n\n.. code-block:: typescript\n\n  e.select(e.str('Hello world'));\n  // select 1234;\n\n  e.select(e.op(e.int64(2), '+', e.int64(2)));\n  // select 2 + 2;\n\n\nObjects\n^^^^^^^\n\nAs in EdgeQL, selecting an set of objects will return their ``id`` property only. This is reflected in the TypeScript type of the result.\n\n.. code-block:: typescript\n\n  const query = e.select(e.Movie);\n  // select Movie;\n\n  const result = await query.run(client);\n  // {id:string}[]\n\nShapes\n^^^^^^\n\nTo specify a shape, pass a function as the second argument. This function should return an object that specifies which properties to include in the result. This roughly corresponds to a *shape* in EdgeQL.\n\n.. code-block:: typescript\n\n  const query = e.select(e.Movie, () => ({\n    id: true,\n    title: true,\n    release_year: true,\n  }));\n  /*\n    EdgeQL:\n    select Movie {\n      id,\n      title,\n      release_year\n    }\n  */\n  /*\n    Inferred type:\n    {\n      id: string;\n      title: string;\n      release_year: number | null;\n    }[]\n  */\n\nAs you can see, the type of ``release_year`` is ``number | null`` since it's an optional property, whereas ``id`` and ``title`` are required.\n\nPassing a ``boolean`` value (as opposed to a ``true`` literal), which will make the property optional. Passing ``false`` will exclude that property which is generally used to exclude properties when using the special ``*`` property.\n\n.. code-block:: typescript\n\n  e.select(e.Movie, () => ({\n    id: true,\n    title: Math.random() > 0.5,\n    release_year: false,\n  }));\n\n  const result = await query.run(client);\n  // { id: string; title: string | undefined; }[]\n\nSelecting all properties\n************************\n\nFor convenience, the query builder provides a shorthand for selecting all properties of a given object.\n\n.. code-block:: typescript\n\n  e.select(e.Movie, movie => ({\n    ...e.Movie['*']\n  }));\n\n  const result = await query.run(client);\n  // { id: string; title: string; release_year: number | null }[]\n\nThis ``*`` property is just a strongly-typed, plain object:\n\n.. code-block:: typescript\n\n  e.Movie['*'];\n  // => { id: true, title: true, release_year: true }\n\nSelect a single object\n^^^^^^^^^^^^^^^^^^^^^^\n\nTo select a particular object, use the ``filter_single`` key. This tells the query builder to expect a result with zero or one elements.\n\n.. code-block:: typescript\n\n  e.select(e.Movie, () => ({\n    id: true,\n    title: true,\n    release_year: true,\n\n    filter_single: { id: \"00000000-0000-0000-0000-000000000000\" },\n  }));\n\nThis also works if an object type has a composite exclusive constraint:\n\n.. code-block:: typescript\n\n  /*\n    type Movie {\n      ...\n      constraint exclusive on (.title, .release_year);\n    }\n  */\n\n  e.select(e.Movie, () => ({\n    title: true,\n    filter_single: { title: \"The Avengers\", release_year: 2012 },\n  }));\n\nYou can also pass a boolean expression like from ``e.op`` or a function in the standard library to ``filter_single`` if you prefer.\n\n.. code-block:: typescript\n\n  const query = e.select(e.Movie, (movie) => ({\n    id: true,\n    title: true,\n    release_year: true,\n    filter_single: e.op(\n      movie.id,\n      \"=\",\n      e.uuid(\"00000000-0000-0000-0000-000000000000\"),\n    ),\n  }));\n\n  const result = await query.run(client);\n  // { id: string; title: string; release_year: number | null } | null\n\nNotice that we must explicitly cast the string literal to a ``uuid`` expression using the ``e.uuid`` function. We can also use ``e.params`` to explicitly pass in the ``id`` as a parameter, which will make the query more reusable and also not require the explicit cast.\n\n.. code-block:: typescript\n\n  const id = \"00000000-0000-0000-0000-000000000000\";\n  const query = e.params(\n    { id: e.uuid },\n    (params) => e.select(e.Movie, (movie) => ({\n      id: true,\n      title: true,\n      release_year: true,\n      filter_single: e.op(movie.id, \"=\", params.id),\n    }))\n  );\n\n  const result = await query.run(client, { id });\n  // { id: string; title: string; release_year: number | null } | null\n\nSelect many objects by ID\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. code-block:: typescript\n\n  const query = e.params(\n    { ids: e.array(e.uuid) },\n    (params) =>\n      e.select(e.Movie, (movie) => ({\n        id: true,\n        title: true,\n        release_year: true,\n        filter: e.op(movie.id, \"in\", e.array_unpack(params.ids)),\n      }))\n  );\n\n  const result = await query.run(client, {\n    ids: [\n      \"00000000-0000-0000-0000-000000000000\",\n      \"00000000-0000-0000-0000-000000000000\",\n    ],\n  })\n  // {id: string; title: string; release_year: number | null}[]\n\nNesting shapes\n^^^^^^^^^^^^^^\n\nAs in EdgeQL, shapes can be nested to fetch deeply related objects.\n\n.. code-block:: typescript\n\n  const query = e.select(e.Movie, () => ({\n    id: true,\n    title: true,\n    actors: {\n      name: true\n    }\n  }));\n\n  const result = await query.run(client);\n  /* {\n    id: string;\n    title: string;\n    actors: { name: string }[]\n  }[] */\n\n\nPortable shapes\n^^^^^^^^^^^^^^^\n\nYou can use ``e.shape`` to define a \"portable shape\" that can be defined independently and used in multiple queries. The result of ``e.shape`` is a *function*. When you use the shape in your final queries, be sure to pass in the *scope variable* (e.g. ``movie`` in the example below). This is required for the query builder to correctly resolve the query.\n\nYou can also use the ``$infer`` type helper to extract the inferred type of the portable shape. Note that the cardinality of the shape will affect the inferred type, just like an ``e.select`` expression, so if you are trying to get to the element type, you will need to use TypeScript to get the correct type based on the cardinality of the shape.\n\n.. code-block:: typescript\n\n  const baseShape = e.shape(e.Movie, (movie) => ({\n    title: true,\n    num_actors: e.count(movie.actors),\n  }));\n\n  type MovieShape = $infer<typeof baseShape>;\n  // { title: true; num_actors: true }[]\n  type MovieShapeSingle = MovieShape[number];\n  // { title: true; num_actors: true }\n\n  const query = e.select(e.Movie, (movie) => ({\n    ...baseShape(movie),\n    release_year: true,\n    filter_single: {title: 'The Avengers'}\n  }))\n\n  type QueryResult = $infer<typeof query>;\n  // { title: string; num_actors: number; release_year: number | null } | null\n\nWhy closures?\n^^^^^^^^^^^^^\n\nIn EdgeQL, a ``select`` statement introduces a new *scope*; within the clauses of a select statement, you can refer to fields of the *elements being selected* using leading dot notation.\n\n.. code-block:: edgeql\n\n  select Movie { id, title }\n  filter .title = \"The Avengers\";\n\nHere, ``.title`` is shorthand for the ``title`` property of the selected ``Movie`` elements. All properties/links on the ``Movie`` type can be referenced using this shorthand anywhere in the ``select`` expression. In other words, the ``select`` expression is *scoped* to the ``Movie`` type.\n\nTo represent this scoping in the query builder, we use function scoping. This is a powerful pattern that makes it painless to represent filters, ordering, computed fields, and other expressions. Let's see it in action.\n\n\nFiltering\n^^^^^^^^^\n\nTo add a filtering clause, just include a ``filter`` key in the returned\nparams object. This should correspond to a boolean expression.\n\n.. code-block:: typescript\n\n  e.select(e.Movie, (movie) => ({\n    id: true,\n    title: true,\n    filter: e.op(movie.title, \"ilike\", \"The Matrix%\")\n  }));\n  /*\n    select Movie {\n      id,\n      title\n    } filter .title ilike \"The Matrix%\"\n  */\n\n.. note::\n\n  Since ``filter`` is a :ref:`reserved keyword <ref_eql_lexical_names>` in |Gel|, there is minimal danger of conflicting with a property or link named ``filter``. All shapes can contain filter clauses, even nested ones.\n\nIf you have many conditions you want to test for, your filter can start to get difficult to read.\n\n.. code-block:: typescript\n\n  e.select(e.Movie, (movie) => ({\n    id: true,\n    title: true,\n    filter: e.op(\n      e.op(\n        e.op(movie.title, \"ilike\", \"The Matrix%\"),\n        \"and\",\n        e.op(movie.release_year, \"=\", 1999)\n      ),\n      \"or\",\n      e.op(movie.title, \"=\", \"Iron Man\")\n    )\n  }));\n\nTo improve readability, we recommend breaking these operations out into named variables and composing them.\n\n.. code-block:: typescript\n\n  e.select(e.Movie, (movie) => {\n    const isAMatrixMovie = e.op(movie.title, \"ilike\", \"The Matrix%\");\n    const wasReleased1999 = e.op(movie.release_year, \"=\", 1999);\n    const isIronMan = e.op(movie.title, \"=\", \"Iron Man\");\n    return {\n      id: true,\n      title: true,\n      filter: e.op(\n        e.op(\n          isAMatrixMovie,\n          \"and\",\n          wasReleased1999\n        ),\n        \"or\",\n        isIronMan\n      )\n    }\n  });\n\nYou can combine compound conditions as much or as little as makes sense for\nyour application.\n\n.. code-block:: typescript\n\n  e.select(e.Movie, (movie) => {\n    const isAMatrixMovie = e.op(movie.title, \"ilike\", \"The Matrix%\");\n    const wasReleased1999 = e.op(movie.release_year, \"=\", 1999);\n    const isAMatrixMovieReleased1999 = e.op(\n      isAMatrixMovie,\n      \"and\",\n      wasReleased1999\n    );\n    const isIronMan = e.op(movie.title, \"=\", \"Iron Man\");\n    return {\n      id: true,\n      title: true,\n      filter: e.op(\n        isAMatrixMovieReleased1999,\n        \"or\",\n        isIronMan\n      )\n    }\n  });\n\nFilters on links\n^^^^^^^^^^^^^^^^\n\nLinks can be filtered using traditional filters.\n\n.. code-block:: typescript\n\n  e.select(e.Movie, (movie) => ({\n    title: true,\n    actors: (actor) => ({\n      name: true,\n      filter: e.op(actor.name.slice(0, 1), \"=\", \"A\"),\n    }),\n    filter_single: { title: \"Iron Man\" },\n  }));\n\n\nYou can also use the :ref:`type intersection <gel-js-objects-type-intersections>` operator to filter a link based on its type. For example, since ``actor.roles`` might be of type ``Movie`` or ``TVShow``, to only return ``roles`` that are ``Movie`` types, you would use the ``.is`` type intersection operator:\n\n.. code-block:: typescript\n\n  e.select(e.Actor, (actor) => ({\n    movies: actor.roles.is(e.Movie),\n  }));\n\nThis is how you would use the EdgeQL :eql:op:`[is type] <isintersect>` type intersection operator via the TypeScript query builder.\n\n\nFilters on link properties\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. code-block:: typescript\n\n  e.select(e.Movie, (movie) => ({\n    title: true,\n    actors: (actor) => ({\n      name: true,\n      filter: e.op(actor[\"@character_name\"], \"ilike\", \"Tony Stark\"),\n    }),\n    filter_single: { title: \"Iron Man\" },\n  }));\n\n\nOrdering\n^^^^^^^^\n\nAs with ``filter``, you can pass a value with the special ``order_by`` key. To simply order by a property:\n\n.. code-block:: typescript\n\n  e.select(e.Movie, (movie) => ({\n    order_by: movie.title,\n  }));\n\n.. note::\n\n  Unlike ``filter``, ``order_by`` is *not* a reserved word in |Gel|. Using ``order_by`` as a link or property name will create a naming conflict and likely cause bugs.\n\nThe ``order_by`` key can correspond to an arbitrary expression.\n\n.. code-block:: typescript\n\n  // order by length of title\n  e.select(e.Movie, (movie) => ({\n    order_by: e.len(movie.title),\n  }));\n  /*\n    select Movie\n    order by len(.title)\n  */\n\n  // order by number of actors\n  e.select(e.Movie, (movie) => ({\n    order_by: e.count(movie.actors),\n  }));\n  /*\n    select Movie\n    order by count(.actors)\n  */\n\nYou can customize the sort direction and empty-handling behavior by passing an object into ``order_by``.\n\n.. code-block:: typescript\n\n  e.select(e.Movie, (movie) => ({\n    order_by: {\n      expression: movie.title,\n      direction: e.DESC,\n      empty: e.EMPTY_FIRST,\n    },\n  }));\n  /*\n    select Movie\n    order by .title desc empty first\n  */\n\n.. list-table::\n\n  * - Order direction\n    - ``e.DESC`` ``e.ASC``\n  * - Empty handling\n    - ``e.EMPTY_FIRST`` ``e.EMPTY_LAST``\n\nPass an array of objects for compound ordering.\n\n.. code-block:: typescript\n\n  e.select(e.Movie, (movie) => ({\n    title: true,\n    order_by: [\n      {\n        expression: movie.title,\n        direction: e.DESC,\n      },\n      {\n        expression: e.count(movie.actors),\n        direction: e.ASC,\n        empty: e.EMPTY_LAST,\n      },\n    ],\n  }));\n\n\nOffset and limit\n^^^^^^^^^^^^^^^^\n\nYou can pass an expression with an integer type or a plain JS number.\n\n.. code-block:: typescript\n\n  e.select(e.Movie, (movie) => ({\n    offset: 50,\n    limit: e.int64(10),\n  }));\n  /*\n    select Movie\n    offset 50\n    limit 10\n  */\n\nComputed properties\n^^^^^^^^^^^^^^^^^^^\n\nTo select a computed property, just add it to the returned shape alongside the other elements. All reflected functions are typesafe, so the output type will be correctly inferred.\n\n.. code-block:: typescript\n\n  const query = e.select(e.Movie, movie => ({\n    title: true,\n    uppercase_title: e.str_upper(movie.title),\n    title_length: e.len(movie.title),\n  }));\n\n  const result = await query.run(client);\n  /* =>\n    [\n      {\n        title:\"Iron Man\",\n        uppercase_title: \"IRON MAN\",\n        title_length: 8\n      },\n      ...\n    ]\n  */\n  // {name: string; uppercase_title: string, title_length: number}[]\n\n\nComputed fields can \"override\" an actual link/property as long as the type signatures agree.\n\n.. code-block:: typescript\n\n  e.select(e.Movie, movie => ({\n    title: e.str_upper(movie.title), // this works\n    release_year: e.str(\"2012\"), // TypeError\n\n    // you can override links too\n    actors: e.Person,\n  }));\n\n\n.. _ref_qb_polymorphism:\n\nPolymorphism\n^^^^^^^^^^^^\n\nEdgeQL supports polymorphic queries using the ``[is type]`` prefix.\n\n.. code-block:: edgeql\n\n  select Content {\n    title,\n    [is Movie].release_year,\n    [is TVShow].num_seasons\n  }\n\nIn the query builder, this is represented with the ``e.is`` function.\n\n.. code-block:: typescript\n\n  e.select(e.Content, content => ({\n    title: true,\n    ...e.is(e.Movie, { release_year: true }),\n    ...e.is(e.TVShow, { num_seasons: true }),\n  }));\n\n  const result = await query.run(client);\n  /* {\n    title: string;\n    release_year: number | null;\n    num_seasons: number | null;\n  }[] */\n\nThe ``release_year`` and ``num_seasons`` properties are nullable to reflect the fact that they will only occur in certain objects.\n\n.. note::\n\n  In EdgeQL it is not valid to select the ``id`` property in a polymorphic field. So for convenience when using the ``['*']`` all properties shorthand with ``e.is``, the ``id`` property will be filtered out of the polymorphic shape object.\n\n\nDetached\n^^^^^^^^\n\nSometimes you need to \"detach\" a set reference from the current scope. (Read the :ref:`reference docs <ref_edgeql_with_detached>` for details.) You can achieve this in the query builder with the top-level ``e.detached`` function.\n\n.. code-block:: typescript\n\n  const query = e.select(e.Person, (outer) => ({\n    name: true,\n    castmates: e.select(e.detached(e.Person), (inner) => ({\n      name: true,\n      filter: e.op(outer.acted_in, 'in', inner.acted_in)\n    })),\n  }));\n  /*\n    with outer := Person\n    select Person {\n      name,\n      castmates := (\n        select detached Person { name }\n        filter .acted_in in Person.acted_in\n      )\n    }\n  */\n\nSelecting free objects\n^^^^^^^^^^^^^^^^^^^^^^\n\nSelect a free object by passing an object into ``e.select``. Notice that this is an object literal rather than a function like in the previous examples.\n\n.. code-block:: typescript\n\n  const movies = e.select(e.Movie, (movie) => ({\n    ...movie[\"*\"],\n  }));\n\n  e.select({\n    of: e.str(\"Movie\"),\n    count: e.count(movies),\n    data: movies,\n  });\n  /*\n  with movies := (select Movie { * })\n  select {\n    of := \"Movie\",\n    count := count(movies),\n    data := movies\n  }\n  */\n  // { of: string; count: number; data: Movie[] }\n\n.. _gel-js-insert:\n\nInsert\n------\n\nInsert new data with ``e.insert``.\n\n.. code-block:: typescript\n\n  e.insert(e.Movie, {\n    title: e.str(\"Spider-Man: No Way Home\"),\n    release_year: e.int64(2021),\n  });\n\nFor convenience, the second argument of ``e.insert`` function can also accept plain JS data or a named tuple.\n\n.. code-block:: typescript\n\n  e.params(\n    {\n      movie: e.tuple({\n        title: e.str,\n        release_year: e.int64,\n      })\n    },\n    (params) => e.insert(e.Movie, params.movie)\n  );\n\n\nLink properties\n^^^^^^^^^^^^^^^\n\nAs in EdgeQL, link properties are inserted inside the shape of a subquery.\n\n.. code-block:: typescript\n\n  const query = e.insert(e.Movie, {\n    title: \"Iron Man\",\n    actors: e.select(e.Person, person => ({\n      filter_single: { name: \"Robert Downey Jr.\" },\n      \"@character_name\": e.str(\"Tony Stark\")\n\n      // link props must correspond to expressions\n      \"@character_name\": \"Tony Stark\"  // invalid\n    }))\n  });\n\n\n.. note::\n\n  For technical reasons, link properties must correspond to query builder expressions, not plain JS data.\n\nSimilarly you can directly include link properties inside nested ``e.insert`` queries:\n\n.. code-block:: typescript\n\n  const query = e.insert(e.Movie, {\n    title: \"Iron Man\",\n    release_year: 2008,\n    actors: e.insert(e.Person, {\n      name: \"Robert Downey Jr.\",\n      \"@character_name\": e.str(\"Tony Stark\")\n    }),\n  });\n\nHandling conflicts\n^^^^^^^^^^^^^^^^^^\n.. index:: querybuilder, unlessconflict, unless conflict, constraint\n\nIn EdgeQL, \"upsert\" functionality is achieved by handling **conflicts** on ``insert`` statements with the ``unless conflict`` clause. In the query builder, this is possible with the ``.unlessConflict`` method (available only on ``insert`` expressions).\n\nIn the simplest case, adding ``.unlessConflict`` with no arguments will prevent Gel from throwing an error if the insertion would violate an exclusivity constraint. Instead, the query returns an empty set.\n\n.. code-block:: typescript\n\n  const query = e.insert(e.Movie, {\n    title: \"Spider-Man: No Way Home\",\n    release_year: 2021\n  }).unlessConflict();\n  // => { id: string } | null\n\n\nProvide an ``on`` clause to \"catch\" conflicts only on a specific property/link.\n\n.. code-block:: typescript\n\n  const query = e\n    .insert(e.Movie, {\n      title: \"Spider-Man: No Way Home\",\n      release_year: 2021\n    })\n    .unlessConflict((movie) => ({\n      on: movie.title, // can be any expression\n    }));\n\n\nYou can also provide an ``else`` expression which will be executed and returned in case of a conflict. You must specify an ``on`` clause in order to use ``else``.\n\nThe following query simply returns the conflicting object.\n\n.. code-block:: typescript\n\n  const query = e\n    .insert(e.Movie, {\n      title: \"Spider-Man: Homecoming\",\n      release_year: 2021\n    })\n    .unlessConflict((movie) => ({\n      on: movie.title,\n      else: movie,\n    }));\n\nOr you can perform an upsert operation with an ``e.update`` in the ``else``.\n\n.. code-block:: typescript\n\n  const query = e\n    .insert(e.Movie, {\n      title: \"Spider-Man: Homecoming\",\n      release_year: 2021\n    })\n    .unlessConflict((movie) => ({\n      on: movie.title,\n      else: e.update(movie, () => ({\n        set: {\n          release_year: 2021\n        }\n      })),\n  });\n\n\nIf the constraint you're targeting is a composite constraint, wrap the properties in a tuple.\n\n.. code-block:: typescript\n\n  const query = e\n    .insert(e.Movie, {\n      title: \"Spider-Man: No Way Home\",\n      release_year: 2021\n    })\n    .unlessConflict((movie) => ({\n      on: e.tuple([movie.title, movie.release_year])\n    }));\n\n.. _gel-js-update:\n\nUpdate\n------\n\nUpdate objects with the ``e.update`` function.\n\n.. code-block:: typescript\n\n  e.update(e.Movie, () => ({\n    filter_single: { title: \"Avengers 4\" },\n    set: {\n      title: \"Avengers: Endgame\"\n    }\n  }))\n\nYou can reference the current value of the object's properties.\n\n.. code-block:: typescript\n\n  e.update(e.Movie, (movie) => ({\n    filter: e.op(movie.title[0], '=', ' '),\n    set: {\n      title: e.str_trim(movie.title)\n    }\n  }))\n\nYou can conditionally update a property by using an :ref:`optional parameter <gel-js-optional-parameters>` and the :ref:`coalescing infix operator <gel-js-funcops-infix>`.\n\n.. code-block:: typescript\n\n  e.params({ id: e.uuid, title: e.optional(e.str) }, (params) =>\n    e.update(e.Movie, (movie) => ({\n      filter_single: { id: params.id },\n      set: {\n        title: e.op(params.title, \"??\", movie.title),\n      }\n    }))\n  );\n\nNote that ``e.update`` will return just the ``{ id: true }`` of the updated object. If you want to select further properties, you can wrap the update in a ``e.select`` call. This is still just a single query to the database.\n\n.. code-block:: typescript\n\n  e.params({ id: e.uuid, title: e.optional(e.str) }, (params) => {\n    const updated = e.update(e.Movie, (movie) => ({\n      filter_single: { id: params.id },\n      set: {\n        title: e.op(params.title, \"??\", movie.title),\n      },\n    }));\n    return e.select(updated, (movie) => ({\n      title: movie.title,\n    }));\n  });\n\nUpdating links\n^^^^^^^^^^^^^^\n\nEdgeQL supports some convenient syntax for appending to, subtracting from, and overwriting links.  In the query builder this is represented with the following syntax:\n\n**Overwrite a link**\n\n.. code-block:: typescript\n\n  const actors = e.select(e.Person, ...);\n  e.update(e.Movie, movie => ({\n    filter_single: {title: 'The Eternals'},\n    set: {\n      actors: actors,\n    }\n  }))\n\n**Add to a link**\n\n.. code-block:: typescript\n\n  const actors = e.select(e.Person, ...);\n  e.update(e.Movie, movie => ({\n    filter_single: {title: 'The Eternals'},\n    set: {\n      actors: { \"+=\": actors },\n    }\n  }))\n\n\n**Subtract from a link**\n\n.. code-block:: typescript\n\n  const actors = e.select(e.Person, ...);\n  e.update(e.Movie, movie => ({\n    filter_single: {title: 'The Eternals'},\n    set: {\n      actors: { \"-=\": actors },\n    }\n  }))\n\n**Updating a single link property**\n\n.. code-block:: typescript\n\n  e.update(e.Movie, (movie) => ({\n    filter_single: { title: \"The Eternals\" },\n    set: {\n      actors: {\n        \"+=\": e.select(movie.actors, (actor) => ({\n          \"@character_name\": e.str(\"Sersi\"),\n          filter: e.op(actor.name, \"=\", \"Gemma Chan\")\n        }))\n      }\n    }\n  }));\n\n**Updating many link properties**\n\n.. code-block:: typescript\n\n  const q = e.params(\n    {\n      cast: e.array(e.tuple({ name: e.str, character_name: e.str })),\n    },\n    (params) =>\n      e.update(e.Movie, (movie) => ({\n        filter_single: { title: \"The Eternals\" },\n        set: {\n          actors: {\n            \"+=\": e.for(e.array_unpack(params.cast), (cast) =>\n              e.select(movie.characters, (character) => ({\n                \"@character_name\": cast.character_name,\n                filter: e.op(cast.name, \"=\", character.name),\n              })),\n            ),\n          },\n        },\n      })),\n  ).run(client, {\n    cast: [\n      { name: \"Gemma Chan\", character_name: \"Sersi\" },\n      { name: \"Richard Madden\", character_name: \"Ikaris\" },\n      { name: \"Angelina Jolie\", character_name: \"Thena\" },\n      { name: \"Salma Hayek\", character_name: \"Ajak\" },\n    ],\n  });\n\n.. _gel-js-delete:\n\nDelete\n------\n\nDelete objects with ``e.delete``.\n\n.. code-block:: typescript\n\n  e.delete(e.Movie, (movie) => ({\n    filter_single: { id: \"00000000-0000-0000-0000-000000000000\" },\n    order_by: movie.title,\n    offset: 10,\n    limit: 10\n  }));\n\nThe only supported keys are ``filter``, ``filter_single``, ``order_by``, ``offset``, and ``limit``.\n\n.. _gel-js-for:\n\nFor\n---\n\n``for`` expressions let you create an expression that represents iterating over any set of values.\n\n.. code-block:: typescript\n\n  const query = e.for(e.set(1, 2, 3, 4), (number) => {\n    return e.op(2, '^', number);\n  });\n  /*\n    for number in {1, 2, 3, 4}\n    2 ^ number\n  */\n  const result = query.run(client);\n  // [2, 4, 8, 16]\n\n.. _gel-js-group:\n\nGroup\n-----\n\nThe ``group`` statement provides a powerful mechanism for categorizing a set of objects (e.g., movies) into *groups*. You can group by properties, expressions, or combinatations thereof.\n\nSimple grouping\n^^^^^^^^^^^^^^^\n\nSort a set of objects by a simple property.\n\n.. tabs::\n\n  .. code-tab:: typescript\n\n    e.group(e.Movie, movie => {\n      return {\n        by: {release_year: movie.release_year}\n      }\n    });\n    /*\n      [\n        {\n          key: {release_year: 2008},\n          grouping: [\"release_year\"],\n          elements: [{id: \"...\"}, {id: \"...\"}]\n        },\n        {\n          key: { release_year: 2009 },\n          grouping: [\"release_year\"],\n          elements: [{id: \"...\"}, {id: \"...\"}]\n        },\n        // ...\n      ]\n    */\n\n  .. code-tab:: edgeql\n\n    group Movie\n    by .release_year\n\nAdd a shape that will be applied to ``elements``. The ``by`` key is a special key, similar to ``filter``, etc. in ``e.select``. All other keys are interpreted as *shape elements* and support the same functionality as ``e.select`` (nested shapes, computeds, etc.).\n\n.. tabs::\n\n  .. code-tab:: typescript\n\n    e.group(e.Movie, (movie) => ({\n      title: true,\n      actors: { name: true },\n      num_actors: e.count(movie.characters),\n      by: { release_year: movie.release_year },\n    }));\n    /* [\n      {\n        key: {release_year: 2008},\n        grouping: [\"release_year\"],\n        elements: [{\n          title: \"Iron Man\",\n          actors: [...],\n          num_actors: 5\n        }, {\n          title: \"The Incredible Hulk\",\n          actors: [...],\n          num_actors: 3\n        }]\n      },\n      // ...\n    ] */\n\n  .. code-tab:: edgeql\n\n    group Movie {\n      title,\n      num_actors := count(.actors)\n    }\n    by .release_year\n\nGroup by a tuple of properties.\n\n.. tabs::\n\n  .. code-tab:: typescript\n\n    e.group(e.Movie, (movie) => {\n      const release_year = movie.release_year;\n      const first_letter = movie.title[0];\n      return {\n        title: true,\n        by: { release_year, first_letter }\n      };\n    });\n    /*\n      [\n        {\n          key: {release_year: 2008, first_letter: \"I\"},\n          grouping: [\"release_year\", \"first_letter\"],\n          elements: [{title: \"Iron Man\"}]\n        },\n        {\n          key: {release_year: 2008, first_letter: \"T\"},\n          grouping: [\"release_year\", \"first_letter\"],\n          elements: [{title: \"The Incredible Hulk\"}]\n        },\n        // ...\n      ]\n    */\n\n  .. code-tab:: edgeql\n\n    group Movie { title }\n    using first_letter := .title[0]\n    by .release_year, first_letter\n\nUsing grouping sets to group by several expressions simultaneously.\n\n.. tabs::\n\n  .. code-tab:: typescript\n\n    e.group(e.Movie, (movie) => {\n      const release_year = movie.release_year;\n      const first_letter = movie.title[0];\n      return {\n        title: true,\n        by: e.group.set({release_year, first_letter})\n      };\n    });\n    /* [\n      {\n        key: {release_year: 2008},\n        grouping: [\"release_year\"],\n        elements: [{title: \"Iron Man\"}, {title: \"The Incredible Hulk\"}]\n      },\n      {\n        key: {first_letter: \"I\"},\n        grouping: [\"first_letter\"],\n        elements: [{title: \"Iron Man\"}, {title: \"Iron Man 2\"}, {title: \"Iron Man 3\"}],\n      },\n      // ...\n    ] */\n\n  .. code-tab:: edgeql\n\n    group Movie { title }\n    using first_letter := .title[0]\n    by {.release_year, first_letter}\n\n\nUsing a combination of tuples and grouping sets.\n\n.. tabs::\n\n  .. code-tab:: typescript\n\n    e.group(e.Movie, (movie) => {\n      const release_year = movie.release_year;\n      const first_letter = movie.title[0];\n      const cast_size = e.count(movie.actors);\n      return {\n        title: true,\n        by: e.group.tuple(release_year, e.group.set({ first_letter, cast_size }))\n      };\n    });\n    /* [\n      {\n        key: {release_year: 2008, first_letter: \"I\"},\n        grouping: [\"release_year\", \"first_letter\"],\n        elements: [{title: \"Iron Man\"}]\n      },\n      {\n        key: {release_year: 2008, cast_size: 3},\n        grouping: [\"release_year\", \"cast_size\"],\n        elements: [{title: \"The Incredible Hulk\"}]\n      },\n      // ...\n    ] */\n\n  .. code-tab:: edgeql\n\n    group Movie { title }\n    using\n      first_letter := .title[0],\n      cast_size := count(.actors)\n    by .release_year, {first_letter, cast_size}\n\n\n\nThe ``group`` statement provides a syntactic sugar for defining certain common grouping sets: ``cube`` and ``rollup``. Here's a quick primer on how they work:\n\n.. code-block::\n\n  ROLLUP (a, b, c)\n  is equivalent to\n  {(), (a), (a, b), (a, b, c)}\n\n  CUBE (a, b)\n  is equivalent to\n  {(), (a), (b), (a, b)}\n\nTo use these in the query builder use the ``e.group.cube`` and ``e.group.rollup`` functions.\n\n\n.. tabs::\n\n  .. code-tab:: typescript\n\n    e.group(e.Movie, (movie) => {\n      const release_year = movie.release_year;\n      const first_letter = movie.title[0];\n      const cast_size = e.count(movie.actors);\n      return {\n        title: true,\n        by: e.group.rollup({release_year, first_letter, cast_size})\n      };\n    });\n\n  .. code-tab:: edgeql\n\n    group Movie { title }\n    using\n      first_letter := .title[0],\n      cast_size := count(.actors)\n    by rollup(.release_year, first_letter, cast_size)\n\n.. tabs::\n\n  .. code-tab:: typescript\n\n    e.group(e.Movie, (movie) => {\n      const release_year = movie.release_year;\n      const first_letter = movie.title[0];\n      const cast_size = e.count(movie.actors);\n      return {\n        title: true,\n        by: e.group.cube({release_year, first_letter, cast_size})\n      };\n    });\n\n  .. code-tab:: edgeql\n\n    group Movie { title }\n    using\n      first_letter := .title[0],\n      cast_size := count(.actors)\n    by cube(.release_year, first_letter, cast_size)\n\n.. _gel-js-with:\n\nWith Blocks\n-----------\n\nDuring the query rendering step, the number of occurrences of each expression are tracked. If an expression occurs more than once it is automatically extracted into a ``with`` block.\n\n.. code-block:: typescript\n\n  const x = e.int64(3);\n  const y = e.select(e.op(x, '^', x));\n\n  y.toEdgeQL();\n  // with x := 3\n  // select x ^ x\n\n  const result = await y.run(client);\n  // => 27\n\nThis hold for expressions of arbitrary complexity.\n\n.. code-block:: typescript\n\n  const robert = e.insert(e.Person, {\n    name: \"Robert Pattinson\"\n  });\n  const colin = e.insert(e.Person, {\n    name: \"Colin Farrell\"\n  });\n  const newMovie = e.insert(e.Movie, {\n    title: \"The Batman\",\n    actors: e.set(colin, robert)\n  });\n\n  /*\n  with\n    robert := (insert Person { name := \"Robert Pattinson\"}),\n    colin := (insert Person { name := \"Colin Farrell\"}),\n  insert Movie {\n    title := \"The Batman\",\n    actors := {robert, colin}\n  }\n  */\n\nNote that ``robert`` and ``colin`` were pulled out into a top-level with block. To force these variables to occur in an internal ``with`` block, you can short-circuit this logic with ``e.with``.\n\n\n.. code-block:: typescript\n\n  const robert = e.insert(e.Person, {\n    name: \"Robert Pattinson\"\n  });\n  const colin = e.insert(e.Person, {\n    name: \"Colin Farrell\"\n  });\n  const newMovie = e.insert(e.Movie, {\n    actors: e.with([robert, colin], // list \"dependencies\"\n      e.select(e.set(robert, colin))\n    )\n  })\n\n  /*\n  insert Movie {\n    title := \"The Batman\",\n    actors := (\n      with\n        robert := (insert Person { name := \"Robert Pattinson\"}),\n        colin := (insert Person { name := \"Colin Farrell\"})\n      select {robert, colin}\n    )\n  }\n  */\n\n\n.. note::\n\n  It's an error to pass an expression into multiple ``e.with``\\ s, or use an expression passed to ``e.with`` outside of that block.\n\nTo explicitly create a detached \"alias\" of another expression, use ``e.alias``.\n\n.. code-block:: typescript\n\n  const a = e.set(1, 2, 3);\n  const b = e.alias(a);\n\n  const query = e.select(e.op(a, '*', b))\n  // WITH\n  //   a := {1, 2, 3},\n  //   b := a\n  // SELECT a + b\n\n  const result = await query.run(client);\n  // => [1, 2, 3, 2, 4, 6, 3, 6, 9]\n\n"
  },
  {
    "path": "docs/reference/using/projects.rst",
    "content": ".. _ref_guide_using_projects:\n\n========\nProjects\n========\n\nA Gel project represents a codebase that shares a single Gel database instance. Projects make local development simpler by automatically managing database connections without requiring you to specify credentials each time you run a command.\n\n**Key concepts:**\n\n* A project is marked by a |gel.toml| file in your codebase's root directory\n* Projects are \"linked\" to a specific Gel database instance\n* This link stores connection information in Gel's config directory\n* When you run Gel commands within a project directory, they automatically connect to the linked instance\n* All client libraries use the same mechanism to auto-connect inside project directories\n\n**Benefits of projects:**\n\n* Run CLI commands without connection flags (e.g., use :gelcmd:`migrate` instead of :gelcmd:`-I my_instance migrate`)\n* Make applications portable - teammates can quickly set up matching database instances\n* Separate connection details from code - no hard-coded credentials needed or complex conditional environment-based connection logic\n\n.. note::\n\n  Projects are intended only for local development. In production environments, you should provide instance credentials using environment variables. See :ref:`Connection parameters <ref_reference_connection>` for details.\n\nCreating a new Gel project\n==========================\n\nTo get started, navigate to the root directory of your codebase in a shell and run :gelcmd:`project init`. You'll see something like this:\n\n.. code-block:: bash\n\n  $ gel project init\n  No `gel.toml` found in this repo or above.\n  Do you want to initialize a new project? [Y/n]\n  > Y\n  Checking Gel versions...\n  Specify the version of Gel to use with this project [6.4]:\n  > # left blank for default\n  Specify the name of Gel instance to use with this project:\n  > my_instance\n  Initializing Gel instance...\n  Bootstrap complete. Server is up and running now.\n  Project initialialized.\n\nThis process:\n\n1. Asks you to specify a Gel version (defaulting to the latest stable version)\n2. Prompts for an instance name (creating a new instance if needed)\n3. Links your current directory to that instance by storing connection metadata in Gel's :ref:`config directory <ref_cli_gel_paths>`\n4. Creates a |gel.toml| file marking this directory as a Gel project\n5. Sets up a ``dbschema`` directory with a :dotgel:`dbschema/default` schema file if they don't already exist\n\nWorking with existing projects\n==============================\n\nIf you've cloned a repository that already contains a |gel.toml| file, simply run :gelcmd:`project init` in the project directory. This will:\n\n1. Install the required Gel version if needed\n2. Create a new local instance with the appropriate name\n3. Apply any existing migrations\n4. Link the project to the new instance\n\nThis makes it easy to begin working with Gel-backed applications without manual configuration.\n\nUnlinking a project\n===================\n\nTo remove the link between your project and its instance, run :gelcmd:`project unlink` anywhere inside the project. This doesn't affect the instance itself, which continues running. After unlinking, you can run :gelcmd:`project init` again to link to a different instance.\n\nUsing projects with remote instances\n====================================\n\nYou can also link a project to a non-local Gel instance (such as a shared staging database). First, create a link to the remote instance:\n\n.. code-block:: bash\n\n  $ gel instance link\n  Specify the host of the server [default: localhost]:\n  > 192.168.4.2\n  Specify the port of the server [default: 5656]:\n  > 10818\n  Specify the database user [default: admin]:\n  > admin\n  Specify the branch name [default: main]:\n  > main\n  Unknown server certificate: SHA1:c38a7a90429b033dfaf7a81e08112a9d58d97286. Trust? [y/N]\n  > y\n  Password for 'admin':\n  Specify a new instance name for the remote server [default: 192_168_4_2_10818]:\n  > staging_db\n  Successfully linked to remote instance. To connect run:\n    gel -I staging_db\n\nThen run :gelcmd:`project init` and specify ``staging_db`` as the instance name.\n\n.. note::\n\n  When using an existing instance, make sure that the project source tree is in sync with the current migration revision of the instance. If the current revision in the database doesn't exist under ``dbschema/migrations/``, it'll raise an error when trying to migrate or create new migrations. In this case, update your local source tree to the revision that matches the current revision of the database.\n\n.. _ref_reference_gel_toml:\n\ngel.toml\n========\n\nThe |gel.toml| file is created in the project root after running :ref:`ref_cli_gel_project_init`. If this file is present in a directory, it signals to the CLI and client bindings that the directory is an instance-linked |Gel| project. It supports the following configuration settings:\n\nExample\n-------\n\n.. code-block:: toml\n\n    [instance]\n    server-version = \"6.0\"\n\n    [project]\n    schema-dir = \"db/schema\"\n\n    [hooks]\n    project.init.after = \"setup_dsn.sh\"\n    branch.switch.after = \"setup_dsn.sh\"\n    schema.update.after = \"gel-orm sqlalchemy --mod compat --out compat\"\n\n    [[watch]]\n    files = [\"queries/*.edgeql\"]\n    script = \"npx @edgedb/generate queries\"\n\n\n[instance] table\n----------------\n\n.. versionchanged:: 6.0\n\n    For versions of |Gel| prior to 6.0 use ``[edgedb]`` table instead of ``[instance]``.\n\n- ``server-version``- version of Gel server to use with this project.\n\n  .. note::\n\n      The version specification is assumed to be **a minimum version**, but the CLI will *not* upgrade to subsequent major versions. This means if the version specified is ``6.1`` and versions 6.2 and 6.3 are available, 6.3 will be installed, even if version 7.0 is also available.\n\n      To specify an exact version, prepend with ``=`` like this: ``=6.1``. We support `all of the same version specifications as Cargo`_, Rust's package manager.\n\n\n\n[project] table\n---------------\n\n- ``schema-dir``- directory where schema files will be stored.\n  Defaults to ``dbschema``.\n\n\n.. _ref_reference_gel_toml_hooks:\n\n[hooks] table\n-------------\n\n.. versionadded:: 6\n\nThis table may contain the following keys, all of which are optional:\n\n- ``project.init.before``\n- ``project.init.after``\n- ``branch.switch.before``\n- ``branch.wipe.before``\n- ``migration.apply.before``\n- ``schema.update.before``\n- ``branch.switch.after``\n- ``branch.wipe.after``\n- ``migration.apply.after``\n- ``schema.update.after``\n\nEach key represents a command hook that will be executed together with a CLI\ncommand. All keys have a string value which is going to be executed as a shell\ncommand when the corresponding hook is triggered.\n\nHooks are divided into two categories: ``before`` and ``after`` as indicated\nby their names. All of the ``before`` hooks are executed prior to their\ncorresponding commands, so they happen before any changes are made. All of the\n``after`` hooks run after the CLI command and thus the effects from the\ncommand are already in place. Any error during the hook script execution will\nterminate the CLI command (thus ``before`` hooks are able to prevent their\ncommands from executing if certain conditions are not met).\n\nOverall, when multiple hooks are triggered they all execute sequentially in\nthe order they are listed above.\n\nHere is a breakdown of which command trigger which hooks:\n\n- :ref:`ref_cli_gel_project_init` command triggers the ``project.init.before``\n  and ``project.init.after`` hook. If the migrations are applied at the end of\n  the initialization, then the ``migration.apply.before``,\n  ``schema.update.before``, ``migration.apply.after``, and\n  ``schema.update.after`` hooks are also triggered.\n- :ref:`ref_cli_gel_branch_switch` command triggers ``branch.switch.before``,\n  ``schema.update.before``, ``branch.switch.after``, and ``schema.update.after``\n  hooks in that relative order.\n- :ref:`ref_cli_gel_branch_wipe` command triggers the ``branch.wipe.before``,\n  ``schema.update.before``, ``branch.wipe.after``, and ``schema.update.after``\n  hooks in that relative order.\n- :ref:`ref_cli_gel_branch_rebase` and :ref:`ref_cli_gel_branch_merge`\n  commands trigger ``migration.apply.before``, ``schema.update.before``,\n  ``migration.apply.after``, and ``schema.update.after`` hooks in that\n  relative order. Notice that although these are branch commands, but they do\n  not change the current branch, instead they modify and apply migrations.\n  That's why they trigger the ``migration.apply`` hooks.\n- :ref:`ref_cli_gel_migration_apply` command triggers\n  ``migration.apply.before``, ``schema.update.before``,\n  ``migration.apply.after``, and ``schema.update.after`` hooks in that\n  relative order.\n\n  .. note::\n\n    All of these hooks are intended as project management tools. For this\n    reason they will only be triggered by the CLI commands that *don't\n    override* default project settings. Any CLI command that uses\n    :ref:`connection options <ref_cli_gel_connopts>` will not trigger any\n    hooks.\n\nThis is implementing `RFC 1028 <rfc1028_>`_.\n\n[[watch]] table array\n---------------------\n\n.. versionadded:: 6\n\nEach element of this table array may contain the following required keys:\n\n- ``files = [\"<path-string>\", ...]`` - specify file(s) being watched.\n\n  The paths must use ``/`` (\\*nix-style) as path separators. They can also contain glob pattrens (``*``, ``**``, ``?``, etc.) in order to specify multiple files at one.\n\n- ``script = \"<command>\"`` - command to be executed by the shell.\n\nThe watch mode can be activated by the :ref:`ref_cli_gel_watch` command.\n\nThis is implementing `RFC 1028 <rfc1028_>`_.\n\n.. _all of the same version specifications as Cargo:\n   https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies\n\n.. _rfc1028:\n    https://github.com/edgedb/rfcs/blob/master/text/1028-cli-hooks.rst\n"
  },
  {
    "path": "docs/reference/using/python/api/advanced.rst",
    "content": ".. _gel-python-advanced:\n\n==============\nAdvanced Usage\n==============\n\n.. py:currentmodule:: gel\n\n\n.. _gel-python-transaction-options:\n\nTransaction Options\n===================\n\nTransactions can be customized with different options:\n\n.. py:class:: TransactionOptions(isolation=IsolationLevel.Serializable, readonly=False, deferrable=False)\n\n    :param IsolationLevel isolation: transaction isolation level\n    :param bool readonly: if true the transaction will be readonly\n    :param bool deferrable: if true the transaction will be deferrable\n\n    .. py:method:: defaults()\n        :classmethod:\n\n        Returns the default :py:class:`TransactionOptions`.\n\n.. py:class:: IsolationLevel\n\n    Isolation level for transaction\n\n    .. py:attribute:: Serializable\n\n        Serializable isolation level\n\n    .. py:attribute:: RepeatableRead\n\n        Repeatable read isolation level (supported in read-only transactions)\n\n    .. py:attribute:: PreferRepeatableRead\n\n        Uses repeatable read isolation level if server analysis concludes that it is supported for a given query.\n        Otherwise, uses serializable isolation level.\n\n:py:class:`TransactionOptions` can be set on :py:class:`~gel.Client` or :py:class:`~gel.AsyncIOClient` using one of these methods:\n\n* :py:meth:`gel.Client.with_transaction_options`\n* :py:meth:`gel.AsyncIOClient.with_transaction_options`\n\nThese methods return a \"shallow copy\" of the current client object with modified transaction options. Both ``self`` and the returned object can be used, but different transaction options will applied respectively.\n\nTransaction options are used by the future calls to the method :py:meth:`gel.Client.transaction` or :py:meth:`gel.AsyncIOClient.transaction`.\n\n\n.. _gel-python-retry-options:\n\nRetry Options\n=============\n\nIndividual EdgeQL commands or whole transaction blocks are automatically retried on retryable errors. By default, gel-python will try at most 3 times, with an exponential backoff time interval starting from 100ms, plus a random hash under 100ms.\n\nRetry rules can be granularly customized with different retry options:\n\n.. py:class:: RetryOptions(attempts, backoff=default_backoff)\n\n    :param int attempts: the default number of attempts\n    :param Callable[[int], Union[float, int]] backoff: the default backoff function\n\n    .. py:method:: with_rule(condition, attempts=None, backoff=None)\n\n        Adds a backoff rule for a particular condition\n\n        :param RetryCondition condition: condition that will trigger this rule\n        :param int attempts: number of times to retry\n        :param Callable[[int], Union[float, int]] backoff:\n          function taking the current attempt number and returning the number\n          of seconds to wait before the next attempt\n\n    .. py:method:: defaults()\n        :classmethod:\n\n        Returns the default :py:class:`RetryOptions`.\n\n.. py:class:: RetryCondition\n\n    Specific condition to retry on for fine-grained control\n\n    .. py:attribute:: TransactionConflict\n\n        Triggered when a TransactionConflictError occurs.\n\n    .. py:attribute:: NetworkError\n\n        Triggered when a ClientError occurs.\n\n:py:class:`RetryOptions` can be set on :py:class:`~gel.Client` or\n:py:class:`~gel.AsyncIOClient` using one of these methods:\n\n* :py:meth:`gel.Client.with_retry_options`\n* :py:meth:`gel.AsyncIOClient.with_retry_options`\n\nThese methods return a \"shallow copy\" of the current client object with modified retry options. Both ``self`` and the returned object can be used, but different retry options will applied respectively.\n\n\n.. _gel-python-state:\n\nState\n=====\n\nState is an execution context that affects the execution of EdgeQL commands in different ways: default module, module aliases, session config and global values.\n\n.. py:class:: State(default_module=None, module_aliases={}, config={}, globals_={})\n\n    :type default_module: str or None\n    :param default_module:\n        The *default module* that the future commands will be executed with.  ``None`` means the default *default module* on the server-side, which is usually just ``default``.\n\n    :param dict[str, str] module_aliases:\n        Module aliases mapping of alias -> target module.\n\n    :param dict[str, object] config:\n        Non system-level config settings mapping of config name -> config value.\n\n        For available configuration parameters refer to the :ref:`Config documentation <ref_std_cfg>`.\n\n    :param dict[str, object] globals_:\n        Global values mapping of global name -> global value.\n\n        .. note::\n            The global name can be either a qualified name like ``my_mod::glob2``, or a simple name under the default module.  Simple names will be prefixed with the default module, while module aliases in qualified names - if any - will be resolved into actual module names.\n\n    .. py:method:: with_default_module(module=None)\n\n        Returns a new :py:class:`State` copy with adjusted default module.\n\n        .. note::\n            This will not affect the globals that are already stored in this state using simple names, because their names were resolved before this call to ``with_default_module()``, which affects only the future calls to the :py:meth:`with_globals` method.\n\n        This is equivalent to using the ``set module`` command, or using the ``reset module`` command when giving ``None``.\n\n        :type module: str or None\n        :param module:\n            Adjust the *default module*. If ``module`` is ``None``, the *default module* will be reset to default.\n\n    .. py:method:: with_module_aliases(aliases_dict=None, /, **aliases)\n\n        Returns a new :py:class:`State` copy with adjusted module aliases.\n\n        .. note::\n            This will not affect the globals that are already stored in this state using module aliases, because their names were resolved before this call to ``with_module_aliases()``, which affects only the future calls to the :py:meth:`with_globals` method.\n\n        This is equivalent to using the ``set alias`` command.\n\n        :type aliases_dict: dict[str, str] or None\n        :param aliases_dict:\n            Adjust the module aliases by merging with the given alias -> target module mapping. This is an optional positional-only argument.\n\n        :param dict[str, str] aliases:\n            Adjust the module aliases by merging with the given alias -> target module mapping, after applying ``aliases_dict`` if set.\n\n    .. py:method:: without_module_aliases(*aliases)\n\n        Returns a new :py:class:`State` copy without specified module aliases.\n\n        .. note::\n            This will not affect the globals that are already stored in this state using module aliases, because their names were resolved before this call to ``without_module_aliases()``, which affects only the future calls to the :py:meth:`with_globals` method.\n\n        This is equivalent to using the ``reset alias`` command.\n\n        :param tuple[str] aliases:\n            Adjust the module aliases by dropping the specified aliases if they were set, no errors will be raised if they weren't.\n\n            If no aliases were given, all module aliases will be dropped.\n\n    .. py:method:: with_config(config_dict=None, /, **config)\n\n        Returns a new :py:class:`State` copy with adjusted session config.\n\n        This is equivalent to using the ``configure session set`` command.\n\n        :type config_dict: dict[str, object] or None\n        :param config_dict:\n            Adjust the config settings by merging with the given config name -> config value mapping. This is an optional positional-only argument.\n\n        :param dict[str, object] config:\n            Adjust the config settings by merging with the given config name -> config value mapping, after applying ``config_dict`` if set.\n\n    .. py:method:: without_config(*config_names)\n\n        Returns a new :py:class:`State` copy without specified session config.\n\n        This is equivalent to using the ``configure session reset`` command.\n\n        :param tuple[str] config_names:\n            Adjust the config settings by resetting the specified config to default if they were set, no errors will be raised if they weren't.\n\n            If no names were given, all session config will be reset.\n\n    .. py:method:: with_globals(globals_dict=None, /, **globals_)\n\n        Returns a new :py:class:`State` copy with adjusted global values.\n\n        .. note::\n            The globals are stored with their names resolved into the actual fully-qualified names using the current default module and module aliases set on this state.\n\n        This is equivalent to using the ``set global`` command.\n\n        :type globals_dict: dict[str, object] or None\n        :param globals_dict:\n            Adjust the global values by merging with the given global name -> global value mapping. This is an optional positional-only argument.\n\n        :param dict[str, object] globals_:\n            Adjust the global values by merging with the given global name -> global value mapping, after applying ``globals_dict`` if set.\n\n    .. py:method:: without_globals(*global_names)\n\n        Returns a new :py:class:`State` copy without specified globals.\n\n        This is equivalent to using the ``reset global`` command.\n\n        :param tuple[str] global_names:\n            Adjust the globals by resetting the specified globals to default if they were set, no errors will be raised if they weren't.\n\n            If no names were given, all globals will be reset.\n\n:py:class:`State` can be set on :py:class:`~gel.Client` or :py:class:`~gel.AsyncIOClient` using one of these methods:\n\n* :py:meth:`gel.Client.with_state`\n* :py:meth:`gel.AsyncIOClient.with_state`\n\nThese methods return a \"shallow copy\" of the current client object with modified state, affecting all future commands executed using the returned copy.  Both ``self`` and the returned object can be used, but different state will applied respectively.\n\nAlternatively, shortcuts are available on client objects:\n\n* :py:meth:`gel.Client.with_default_module`\n* :py:meth:`gel.Client.with_module_aliases`\n* :py:meth:`gel.Client.without_module_aliases`\n* :py:meth:`gel.Client.with_config`\n* :py:meth:`gel.Client.without_config`\n* :py:meth:`gel.Client.with_globals`\n* :py:meth:`gel.Client.without_globals`\n* :py:meth:`gel.AsyncIOClient.with_default_module`\n* :py:meth:`gel.AsyncIOClient.with_module_aliases`\n* :py:meth:`gel.AsyncIOClient.without_module_aliases`\n* :py:meth:`gel.AsyncIOClient.with_config`\n* :py:meth:`gel.AsyncIOClient.without_config`\n* :py:meth:`gel.AsyncIOClient.with_globals`\n* :py:meth:`gel.AsyncIOClient.without_globals`\n\nThey work the same way as ``with_state``, and adjusts the corresponding state values.\n"
  },
  {
    "path": "docs/reference/using/python/api/codegen.rst",
    "content": ".. _gel-python-codegen:\n\n===============\nCode Generation\n===============\n\n.. py:currentmodule:: gel\n\nThe ``gel-python`` package exposes a command-line tool to generate various kinds of type-safe code from EdgeQL queries and your schema:\n\n- ``queries``: typesafe functions from ``*.edgeql`` files, using :py:mod:`dataclasses` for objects primarily.\n- ``models``: a programmatic query-builder and Pydantic-based models generator.\n\n.. code-block:: bash\n\n  $ uvx gel generate py/queries\n  $ uvx gel generate py/models\n\nThe :gelcmd:`generate` commands supports the same set of :ref:`connection options <ref_cli_gel_connopts>` as the ``gel`` CLI.\n\n.. code-block::\n\n    -I, --instance <instance>\n    --dsn <dsn>\n    --credentials-file <path/to/credentials.json>\n    -H, --host <host>\n    -P, --port <port>\n    -b, --branch <branch>\n    -u, --user <user>\n    --password\n    --password-from-stdin\n    --tls-ca-file <path/to/certificate>\n    --tls-security <insecure | no_host_verification | strict | default>\n\nQueries\n=======\n\nConsider a simple query that lives in a file called ``get_number.edgeql``:\n\n.. code-block:: edgeql\n\n  select <int64>$arg;\n\nRunning the code generator will generate a new file called ``get_number_async_edgeql.py`` containing the following code (roughly):\n\n.. code-block:: python\n\n  from __future__ import annotations\n  import gel\n\n\n  async def get_number(\n      client: gel.AsyncIOClient,\n      *,\n      arg: int,\n  ) -> int:\n      return await client.query_single(\n          \"\"\"\\\n          select <int64>$arg\\\n          \"\"\",\n          arg=arg,\n      )\n\nTarget\n~~~~~~\n\nBy default, the generated code uses an ``async`` API. The generator supports additional targets via the ``--target`` flag.\n\n.. code-block:: bash\n\n  $ gel generate py/queries --target async        # generate async function (default)\n  $ gel generate py/queries --target blocking     # generate blocking code\n\nThe names of the generated files will differ accordingly: ``{query_filename}_{target}_edgeql.py``.\n\nSingle-file mode\n~~~~~~~~~~~~~~~~\n\nIt may be preferable to generate a single file containing all the generated functions. This can be done by passing the ``--file`` flag.\n\n.. code-block:: bash\n\n  $ gel generate py/queries --file\n\nThis generates a single file called ``generated_{target}_edgeql.py`` in the root of your project.\n\nModels\n======\n\nThe ``models`` generator will generate Pydantic classes and a programmatic query builder. It reflects your full schema, as well as our standard library into functions and Pydantic classes which we've enhanced to make a truly powerful type-safe programmatic data layer.\n\n.. code-block:: python\n\n    import datetime\n    from models import User, std\n    from gel import create_client\n\n    def main():\n        client = create_client()\n\n        # Create a new User instance and save it to the database\n        bob = User(name='Bob', dob=datetime.date(1984, 3, 1))\n        client.save(bob)\n\n        # Select all Users\n        users = client.query(User)\n\n        # Select all users with names like \"Bob\"\n        bob_like = client.query(User.filter(lambda u: std.ilike(u.name, '%bob%')))\n\n        # Update an object\n        bob.name = 'Robert'\n        client.save(bob)\n\n        # Delete an object\n        client.execute(User.filter(id=bob.id).delete())\n\n        client.close()\n\n    if __name__ == '__main__':\n        main()\n"
  },
  {
    "path": "docs/reference/using/python/api/types.rst",
    "content": ".. _gel-python-datatypes:\n.. _gel-python-data-types:\n\n==========\nData types\n==========\n\n.. py:currentmodule:: gel\n\n\ngel-python automatically converts |Gel| types to the corresponding Python types and vice versa.\n\nThe table below shows the correspondence between Gel and Python types.\n\n+----------------------------+-----------------------------------------------------+\n| Gel Type                   |  Python Type                                        |\n+============================+=====================================================+\n| ``Set``                    | :py:class:`gel.Set`                                 |\n+----------------------------+-----------------------------------------------------+\n| ``array<anytype>``         | :py:class:`gel.Array`                               |\n+----------------------------+-----------------------------------------------------+\n| ``anytuple``               | :py:class:`gel.Tuple` or                            |\n|                            | :py:class:`gel.NamedTuple`                          |\n+----------------------------+-----------------------------------------------------+\n| ``anyenum``                | :py:class:`gel.EnumValue`                           |\n+----------------------------+-----------------------------------------------------+\n| ``Object``                 | :py:class:`gel.Object`                              |\n+----------------------------+-----------------------------------------------------+\n| ``bool``                   | :py:class:`bool <python:bool>`                      |\n+----------------------------+-----------------------------------------------------+\n| ``bytes``                  | :py:class:`bytes <python:bytes>`                    |\n+----------------------------+-----------------------------------------------------+\n| ``str``                    | :py:class:`str <python:str>`                        |\n+----------------------------+-----------------------------------------------------+\n| ``cal::local_date``        | :py:class:`datetime.date <python:datetime.date>`    |\n+----------------------------+-----------------------------------------------------+\n| ``cal::local_time``        | offset-naive :py:class:`datetime.time \\             |\n|                            | <python:datetime.time>`                             |\n+----------------------------+-----------------------------------------------------+\n| ``cal::local_datetime``    | offset-naive :py:class:`datetime.datetime \\         |\n|                            | <python:datetime.datetime>`                         |\n+----------------------------+-----------------------------------------------------+\n| ``cal::relative_duration`` | :py:class:`gel.RelativeDuration`                    |\n+----------------------------+-----------------------------------------------------+\n| ``cal::date_duration``     | :py:class:`gel.DateDuration`                        |\n+----------------------------+-----------------------------------------------------+\n| ``datetime``               | offset-aware :py:class:`datetime.datetime \\         |\n|                            | <python:datetime.datetime>`                         |\n+----------------------------+-----------------------------------------------------+\n| ``duration``               | :py:class:`datetime.timedelta \\                     |\n|                            | <python:datetime.timedelta>`                        |\n+----------------------------+-----------------------------------------------------+\n| ``float32``,               | :py:class:`float <python:float>`                    |\n| ``float64``                |                                                     |\n+----------------------------+-----------------------------------------------------+\n| ``int16``,                 | :py:class:`int <python:int>`                        |\n| ``int32``,                 |                                                     |\n| ``int64``,                 |                                                     |\n| ``bigint``                 |                                                     |\n+----------------------------+-----------------------------------------------------+\n| ``decimal``                | :py:class:`Decimal <python:decimal.Decimal>`        |\n+----------------------------+-----------------------------------------------------+\n| ``json``                   | :py:class:`str <python:str>`                        |\n+----------------------------+-----------------------------------------------------+\n| ``uuid``                   | :py:class:`uuid.UUID <python:uuid.UUID>`            |\n+----------------------------+-----------------------------------------------------+\n\n.. note::\n\n    Inexact single-precision ``float`` values may have a different representation when decoded into a Python float.  This is inherent to the implementation of limited-precision floating point types.  If you need the decimal representation to match, cast the expression to ``float64`` or ``decimal`` in your query.\n\n\n.. _gel-python-types-set:\n\nSets\n====\n\n.. py:class:: Set()\n\n    This is :py:class:`list <python:list>` since version 1.0.\n\n\n.. _gel-python-types-object:\n\nObjects\n=======\n\n.. py:class:: Object()\n\n    An immutable representation of an object instance returned from a query.\n\n    .. versionchanged:: 1.0\n\n        ``gel.Object`` instances are dataclass-compatible since version 1.0, for example, ``dataclasses.is_dataclass()`` will return ``True``, and ``dataclasses.asdict()`` will work on ``gel.Object`` instances.\n\n    .. versionchanged:: 1.0\n\n        ``gel.Object.__hash__`` is just ``object.__hash__`` in version 1.0.  Similarly, ``==`` is equivalent to the ``is`` operator comparing ``gel.Object`` instances, and ``<``, ``<=``, ``>``, ``>=`` are not allowed on ``gel.Object`` instances.\n\n    The value of an object property or a link can be accessed through a corresponding attribute:\n\n    .. code-block:: pycon\n\n        >>> import gel\n        >>> client = gel.create_client()\n        >>> r = client.query_single('''\n        ...     SELECT schema::ObjectType {name}\n        ...     FILTER .name = 'std::Object'\n        ...     LIMIT 1''')\n        >>> r\n        Object{name := 'std::Object'}\n        >>> r.name\n        'std::Object'\n\n    .. describe:: obj[linkname]\n\n       Return a :py:class:`gel.Link` or a :py:class:`gel.LinkSet` instance representing the instance(s) of link *linkname* associated with *obj*.\n\n       Example:\n\n       .. code-block:: pycon\n\n          >>> import gel\n          >>> client = gel.create_client()\n          >>> r = client.query_single('''\n          ...     SELECT schema::Property {name, annotations: {name, @value}}\n          ...     FILTER .name = 'listen_port'\n          ...            AND .source.name = 'cfg::Config'\n          ...     LIMIT 1''')\n          >>> r\n          Object {\n              name: 'listen_port',\n              annotations: {\n                  Object {\n                      name: 'cfg::system',\n                      @value: 'true'\n                  }\n              }\n          }\n          >>> r['annotations']\n          LinkSet(name='annotations')\n          >>> l = list(r['annotations])[0]\n          >>> l.value\n          'true'\n\n\nLinks\n=====\n\n.. py:class:: Link\n\n    An immutable representation of an object link.\n\n    Links are created when :py:class:`gel.Object` is accessed via a ``[]`` operator.  Using Link objects explicitly is useful for accessing link properties.\n\n\n.. py:class:: LinkSet\n\n    An immutable representation of a set of Links.\n\n    LinkSets are created when a multi link on :py:class:`gel.Object` is accessed via a ``[]`` operator.\n\n\nTuples\n======\n\n.. py:class:: Tuple()\n\n    This is :py:class:`tuple <python:tuple>` since version 1.0.\n\n\nNamed Tuples\n============\n\n.. py:class:: NamedTuple()\n\n    An immutable value representing a Gel named tuple value.\n\n    .. versionchanged:: 1.0\n\n        ``gel.NamedTuple`` is a subclass of :py:class:`tuple <python:tuple>` and is duck-type compatible with ``collections.namedtuple`` since version 1.0.\n\n    Instances of ``gel.NamedTuple`` generally behave similarly to :py:func:`namedtuple <python:collections.namedtuple>`:\n\n    .. code-block:: pycon\n\n        >>> import gel\n        >>> client = gel.create_client()\n        >>> r = client.query_single('''SELECT (a := 1, b := 'a', c := [3])''')\n        >>> r\n        (a := 1, b := 'a', c := [3])\n        >>> r.b\n        'a'\n        >>> r[0]\n        1\n        >>> r == (1, 'a', [3])\n        True\n        >>> r._fields\n        ('a', 'b', 'c')\n\n\nArrays\n======\n\n.. py:class:: Array()\n\n    This is :py:class:`list <python:list>` since version 1.0.\n\n\nRelativeDuration\n================\n\n.. py:class:: RelativeDuration()\n\n    An immutable value representing a Gel ``cal::relative_duration`` value.\n\n    .. code-block:: pycon\n\n        >>> import gel\n        >>> client = gel.create_client()\n        >>> r = client.query_single('''SELECT <cal::relative_duration>\"1 year 2 days 3 seconds\"''')\n        >>> r\n        <gel.RelativeDuration \"P1Y2DT3S\">\n        >>> r.months\n        12\n        >>> r.days\n        2\n        >>> r.microseconds\n        3000000\n\n\nDateDuration\n============\n\n.. py:class:: DateDuration()\n\n    An immutable value representing a Gel ``cal::date_duration`` value.\n\n    .. code-block:: pycon\n\n        >>> import gel\n        >>> client = gel.create_client()\n        >>> r = client.query_single('''SELECT <cal::date_duration>\"1 year 2 days\"''')\n        >>> r\n        <gel.DateDuration \"P1Y2D\">\n        >>> r.months\n        12\n        >>> r.days\n        2\n\n\nEnumValue\n=========\n\n.. py:class:: EnumValue()\n\n    An immutable value representing a Gel enum value.\n\n    .. versionchanged:: 1.0\n\n        Since version 1.0, ``gel.EnumValue`` is a subclass of :py:class:`enum.Enum <python:enum.Enum>`. Actual enum values are instances of ad-hoc enum classes created by the codecs to represent the actual members defined in your Gel schema.\n\n    .. code-block:: pycon\n\n        >>> import gel\n        >>> client = gel.create_client()\n        >>> r = client.query_single(\"\"\"SELECT <Color>'red'\"\"\")\n        >>> r\n        <gel.EnumValue 'red'>\n        >>> str(r)\n        'red'\n        >>> r.value  # added in 1.0\n        'red'\n        >>> r.name  # added in 1.0, simply str.upper() of r.value\n        'RED'\n"
  },
  {
    "path": "docs/reference/using/python/client.rst",
    "content": ".. _gel-python-client:\n\n.. py:currentmodule:: gel\n\n\n======\nClient\n======\n\nThe ``gel.Client`` class implements the basic functionality required to establish a pool of connections to your database, execute queries with some context and parameters, manage transactions, and decode results into Python types.\n\nWe provide both a :ref:`blocking <gel-python-blocking-api-client>` and an :ref:`asyncio <gel-python-async-api-client>` implementation of the client. For the following examples we will use the :ref:`asyncio <gel-python-async-api-client>` implementation, but the blocking API is fundamentally identical.\n\nCreating a client\n=================\n\nThe ``gel`` package exposes a :py:func:`~gel.create_async_client` function that can be used to create a new :py:class:`~gel.AsyncIOClient` instance. This client instance manages a pool of connections to the database which it discovers automatically from either being in a :gelcmd:`project init` directory or being provided connection details via Environment Variables. See :ref:`the environment section of the connection reference <ref_reference_connection_environments>` for more details and options.\n\n.. note::\n\n  If you're using |Gel| Cloud to host your development instance, you can use the :gelcmd:`cloud login` command to authenticate with |Gel| Cloud and then use the :gelcmd:`project init --server-instance <instance-name>` command to create a local project-linked instance that is linked to an Gel Cloud instance. For more details, see :ref:`the Gel Cloud guide <ref_guide_cloud>`.\n\n.. code-block:: python\n\n  import asyncio\n  import gel\n\n  client = gel.create_async_client()\n\nChecking connection status\n--------------------------\n\nThe client maintains a dynamically sized *pool* of connections under the hood.  These connections are initialized *lazily*, so no connection will be established until the first time you execute a query.\n\nIf you want to explicitly ensure that the client is connected without running a query, use the ``.ensure_connected()`` method. This can be useful to catch any errors resulting from connection mis-configuration by triggering the first connection attempt explicitly.\n\n.. code-block:: python\n\n  import asyncio\n  import gel\n\n  client = gel.create_async_client()\n\n  async def main():\n    await client.ensure_connected()\n\nRunning queries\n===============\n\nThe ``gel.Client`` class provides a number of methods for running queries. The simplest is ``query``, which runs a query and returns the result as a list of results.\n\n.. code-block:: python\n\n  import asyncio\n  import gel\n\n  client = gel.create_async_client()\n\n  async def main():\n    await client.ensure_connected()\n    result = await client.query(\"select 2 + 2;\")\n    print(result)\n\n  asyncio.run(main())\n\n  # Output:\n  # [4]\n\nParameters\n----------\n\nIf your query contains parameters (e.g. ``$foo``), you can pass in values. Positional parameters are passed as positional arguments, and named parameters are passed as keyword arguments. You cannot mix positional and named parameters in the same query.\n\n.. code-block:: python\n\n  import asyncio\n  import gel\n\n  client = gel.create_async_client()\n\n  async def main():\n    await client.ensure_connected()\n    result = await client.query(\"select 2 + $addend;\", addend=2)\n    print(result)\n\n  asyncio.run(main())\n\n  # Output:\n  # [4]\n\n.. note::\n\n  Parameters can only be scalars or arrays of scalars. See :ref:`parameters <ref_eql_params>` for more details.\n\nCardinality\n-----------\n\nThe ``query`` method always returns a list of results. It places no constraints on cardinality.\n\n.. code-block:: python\n\n  await client.query(\"select 2 + 2;\") # list[int64]: [4]\n  await client.query(\"select <int64>{};\") # list[int64]: []\n  await client.query(\"select {1, 2, 3};\") # list[int64]: [1, 2, 3]\n\nIf you know your query will only return a single element, you can tell |Gel| to expect a *singleton result* by using the ``query_single`` method. This is intended for queries that return *zero or one* elements. If the query returns a set with more than one elements, the ``Client`` will raise a runtime error.\n\n.. note::\n\n  Remember that arrays and tuples are considered an element of the result set, so if you're returning exactly one array or tuple, the result will be an array.\n\n.. code-block:: python\n\n  await client.query_single(\"select 2 + 2;\") # int64 | None: 4\n  await client.query_single(\"select [1, 2, 3];\") # list[int64] | None: [1, 2, 3]\n  await client.query_single(\"select <int64>{};\") # int64 | None: None\n  await client.query_single(\"select {1, 2, 3};\") # Raises a ResultCardinalityMismatchError\n\nUse ``query_required_single`` for queries that return *exactly one* element. If the query returns an empty set or a set with multiple elements, the ``Client`` will raise a runtime error.\n\n.. code-block:: python\n\n  await client.query_required_single(\"select 2 + 2;\") # int64: 4\n  await client.query_required_single(\"select <int64>{};\") # Raises a NoDataError\n  await client.query_required_single(\"select {1, 2, 3};\") # Raises a ResultCardinalityMismatchError\n\nIf you do not need or expect a result, you can use ``execute`` which will return ``None``. This is often useful for mutations where you do not need to retrieve a result.\n\n.. code-block:: python\n\n  await client.execute(\"insert Movie { title := 'Iron Man' }\") # None\n\nJSON results\n------------\n\nThe ``Client`` provides additional methods for running queries and retrieving results as a *serialized JSON string*. This serialization happens inside the database and is typically more performant than running ``JSON.stringify`` yourself.\n\n\n.. code-block:: python\n\n  await client.query_json(\"select 2 + 2;\")\n  # \"[4]\"\n\n  await client.query_single_json(\"select <int64>{};\")\n  # \"null\"\n\n  await client.query_required_single_json(\"select 3.14;\")\n  # \"3.14\"\n\n  await client.query_required_json(\"select 3.14;\")\n  # \"3.14\"\n\n.. warning::\n\n  Caution is advised when reading ``decimal`` values using this method. The JSON specification does not have a limit on significant digits, so a ``decimal`` number can be losslessly represented in JSON. However, the default JSON decoder in Python will read all such numbers as ``float`` values, which may result in errors or precision loss. If such loss is unacceptable, then consider casting the value into ``str`` and decoding it on the client side into a more appropriate type, such as ``Decimal``.\n\nSQL queries\n-----------\n\n.. versionadded:: 6.0\n\nThe ``querySQL`` method allows you to run a SQL query and return the result as list of dictionaries.\n\n.. code-block:: python\n\n  await client.query_sql(\"select 2 + 2;\")\n  # [{'col~1': 4}]\n\n  await client.query_sql(\"select 42 as a;\")\n  # [{'a': 42}]\n\nScripts\n-------\n\nBoth ``execute`` and the ``query*`` methods support scripts (queries containing multiple statements). The statements, like all queries, are run in an implicit transaction (unless already in an explicit transaction), so the whole script remains atomic. For the ``query*`` methods only the result of the final statement in the script will be returned.\n\n.. code-block:: python\n\n  result = await client.query(\"\"\"\n    insert Movie { title := 'Iron Man' };\n    insert Person { name := 'Robert Downey Jr.' };\n  \"\"\")\n  print(result)\n  # [{\"id\": \"00000000-0000-0000-0000-000000000000\"}]\n\nFor more fine grained control of atomic exectution of multiple statements, use the :py:meth:`transaction() <gel.AsyncIOClient.transaction>` API.\n\nTransactions\n------------\n\nWe execute queries on the ``tx`` object given in for expression, rather than on the original ``client`` object.\n\n.. code-block:: python\n\n  async for tx in client.transaction():\n      async with tx:\n          await tx.execute(\"insert Movie { title := 'Iron Man' }\")\n          await tx.execute(\"insert Person { name := 'Robert Downey Jr.' }\")\n\nThe ``transaction()`` API guarantees that:\n\n1. Transactions are executed atomically;\n2. If a transaction fails due to retryable error (like a network failure or a concurrent update error), the transaction would be retried;\n3. If any other, non-retryable error occurs, the transaction is rolled back and the ``transaction()`` block throws.\n\nThe transaction object exposes the same ``query`` and ``execute`` methods as the ``Client`` object, with the only difference that queries will run within the current transaction and can be retried automatically.\n\n.. warning::\n\n  In transactions, the entire nested code block can be re-run, including any non-querying Python code. In general, the code inside the transaction block **should not have side effects or run for a significant amount of time**. Consider the following example:\n\n  .. code-block:: python\n    :caption: Don't do this\n\n      email = \"timmy@example.com\";\n\n      async for tx in client.transaction():\n          async with tx:\n              await tx.execute(\n                  'insert User { email := <str>$email }',\n                  email=email,\n              )\n\n              await sendWelcomeEmail(email)\n\n              await tx.execute(\n                  \"\"\"\n                  insert LoginHistory {\n                    user := (select User filter .email = <str>$email),\n                    timestamp := datetime_current()\n                  }\n                  \"\"\",\n                  email=email\n              )\n\n  In the above example, the welcome email may be sent multiple times if the transaction block is retried. Additionally, transactions allocate expensive server resources. Having too many concurrently running long-running transactions will negatively impact the performance of the DB server.\n\nTo rollback a transaction that is in progress raise an exception.\n\n.. code-block:: python\n\n   class RollBack(Exception):\n       \"A user defined exception.\"\n\n   try:\n       for tx in client.transaction():\n           with tx:\n               raise RollBack\n   except RollBack:\n       pass\n\nSee also:\n\n* RFC1004_\n* :py:meth:`Client.transaction()`\n\nConfiguring clients\n===================\n\nClients can be configured using a set of methods that start with ``with``. One you'll likely use often in application code is the ``with_globals`` which sets the global variables in the query.\n\n.. code-block:: python\n\n    client = gel.create_async_client()\n    await client.with_globals(\n        current_user_id=\"00000000-0000-0000-0000-000000000000\",\n    ).query_single(\n        \"select User { * } filter .id ?= global current_user_id;\"\n    )\n\n.. note::\n\n  These methods return a *new Client instance* that *shares a connection pool* with the original client. This is important. Each call to ``create_async_client`` instantiates a new connection pool, so in typical usage you should create a single shared client instance and configure it at runtime as needed.\n\n\n.. _gel-python-blocking-api-reference:\n\nBlocking client reference\n=========================\n\n\n.. _gel-python-blocking-api-client:\n\nClient\n------\n\n.. py:function:: create_client(dsn=None, *, \\\n            host=None, port=None, \\\n            user=None, password=None, \\\n            secret_key=None, \\\n            database=None, \\\n            timeout=60, \\\n            concurrency=None)\n\n    Create a blocking client with a lazy connection pool.\n\n    The connection parameters may be specified either as a connection URI in *dsn*, or as specific keyword arguments, or both.  If both *dsn* and keyword arguments are specified, the latter override the corresponding values parsed from the connection URI.\n\n    If no connection parameter is specified, the client will try to search in environment variables and then the current project, see :ref:`Client Library Connection <gel_client_connection>` docs for more information.\n\n    Returns a new :py:class:`Client` object.\n\n    :param dsn:\n        If this parameter does not start with |geluri| then this is interpreted as the :ref:`name of a local instance <ref_reference_connection_instance_name>`.\n\n        Otherwise it specifies a single string in the following format: :geluri:`user:password@host:port/database?option=value`.  The following options are recognized: host, port, user, database, password. For a complete reference on DSN, see the :ref:`DSN Specification <ref_dsn>`.\n\n    :param host:\n        Database host address as an IP address or a domain name;\n\n        If not specified, the following will be tried, in order:\n\n        - host address(es) parsed from the *dsn* argument,\n        - the value of the :gelenv:`HOST` environment variable,\n        - ``\"localhost\"``.\n\n    :param port:\n        Port number to connect to at the server host. If multiple host addresses were specified, this parameter may specify a sequence of port numbers of the same length as the host sequence, or it may specify a single port number to be used for all host addresses.\n\n        If not specified, the value parsed from the *dsn* argument is used, or the value of the :gelenv:`PORT` environment variable, or ``5656`` if neither is specified.\n\n    :param user:\n        The name of the database role used for authentication.\n\n        If not specified, the value parsed from the *dsn* argument is used, or the value of the :gelenv:`USER` environment variable, or the operating system name of the user running the application.\n\n    :param database:\n        The name of the database to connect to.\n\n        If not specified, the value parsed from the *dsn* argument is used, or the value of the :gelenv:`DATABASE` environment variable, or the operating system name of the user running the application.\n\n    :param password:\n        Password to be used for authentication, if the server requires one. If not specified, the value parsed from the *dsn* argument is used, or the value of the :gelenv:`PASSWORD` environment variable.  Note that the use of the environment variable is discouraged as other users and applications may be able to read it without needing specific privileges.\n\n    :param secret_key:\n        Secret key to be used for authentication, if the server requires one. If not specified, the value parsed from the *dsn* argument is used, or the value of the :gelenv:`SECRET_KEY` environment variable.  Note that the use of the environment variable is discouraged as other users and applications may be able to read it without needing specific privileges.\n\n    :param float timeout:\n        Connection timeout in seconds.\n\n    :return: An instance of :py:class:`Client`.\n\n    The APIs on the returned client instance can be safely used by different threads, because under the hood they are checking out different connections from the pool to run the queries:\n\n    * :py:meth:`Client.query()`\n    * :py:meth:`Client.query_single()`\n    * :py:meth:`Client.query_required_single()`\n    * :py:meth:`Client.query_json()`\n    * :py:meth:`Client.query_single_json()`\n    * :py:meth:`Client.query_required_single_json()`\n    * :py:meth:`Client.execute()`\n    * :py:meth:`Client.transaction()`\n\n    .. code-block:: python\n\n        client = gel.create_client()\n        client.query('SELECT {1, 2, 3}')\n\n    The same for transactions:\n\n    .. code-block:: python\n\n        client = gel.create_client()\n        for tx in client.transaction():\n            with tx:\n                tx.query('SELECT {1, 2, 3}')\n\n\n\n.. py:class:: Client\n\n    A thread-safe blocking client with a connection pool.\n\n    Blocking clients are created by calling :py:func:`create_client`.\n\n\n    .. py:method:: query(query, *args, **kwargs)\n\n        Acquire a connection and use it to run a query and return the results as an :py:class:`gel.Set` instance. The temporary connection is automatically returned back to the pool.\n\n        :param str query: Query text.\n        :param args: Positional query arguments.\n        :param kwargs: Named query arguments.\n\n        :return:\n            An instance of :py:class:`gel.Set` containing the query result.\n\n        Note that positional and named query arguments cannot be mixed.\n\n\n    .. py:method:: query_single(query, *args, **kwargs)\n\n        Acquire a connection and use it to run an optional singleton-returning query and return its element. The temporary connection is automatically returned back to the pool.\n\n        :param str query: Query text.\n        :param args: Positional query arguments.\n        :param kwargs: Named query arguments.\n\n        :return:\n            Query result.\n\n        The *query* must return no more than one element.  If the query returns more than one element, an ``gel.ResultCardinalityMismatchError`` is raised, if it returns an empty set, ``None`` is returned.\n\n        Note, that positional and named query arguments cannot be mixed.\n\n\n    .. py:method:: query_required_single(query, *args, **kwargs)\n\n        Acquire a connection and use it to run a singleton-returning query and return its element. The temporary connection is automatically returned back to the pool.\n\n        :param str query: Query text.\n        :param args: Positional query arguments.\n        :param kwargs: Named query arguments.\n\n        :return:\n            Query result.\n\n        The *query* must return exactly one element.  If the query returns more than one element, an ``gel.ResultCardinalityMismatchError`` is raised, if it returns an empty set, an ``gel.NoDataError`` is raised.\n\n        Note, that positional and named query arguments cannot be mixed.\n\n\n    .. py:method:: query_json(query, *args, **kwargs)\n\n        Acquire a connection and use it to run a query and return the results as JSON. The temporary connection is automatically returned back to the pool.\n\n        :param str query: Query text.\n        :param args: Positional query arguments.\n        :param kwargs: Named query arguments.\n\n        :return:\n            A JSON string containing an array of query results.\n\n        Note, that positional and named query arguments cannot be mixed.\n\n        .. note::\n\n            Caution is advised when reading ``decimal`` values using this method. The JSON specification does not have a limit on significant digits, so a ``decimal`` number can be losslessly represented in JSON. However, the default JSON decoder in Python will read all such numbers as ``float`` values, which may result in errors or precision loss. If such loss is unacceptable, then consider casting the value into ``str`` and decoding it on the client side into a more appropriate type, such as ``Decimal``.\n\n\n    .. py:method:: query_single_json(query, *args, **kwargs)\n\n        Acquire a connection and use it to run an optional singleton-returning query and return its element in JSON. The temporary connection is automatically returned back to the pool.\n\n        :param str query: Query text.\n        :param args: Positional query arguments.\n        :param kwargs: Named query arguments.\n\n        :return:\n            Query result encoded in JSON.\n\n        The *query* must return no more than one element.  If the query returns more than one element, an ``gel.ResultCardinalityMismatchError`` is raised, if it returns an empty set, ``\"null\"`` is returned.\n\n        Note, that positional and named query arguments cannot be mixed.\n\n        .. note::\n\n            Caution is advised when reading ``decimal`` values using this method. The JSON specification does not have a limit on significant digits, so a ``decimal`` number can be losslessly represented in JSON. However, the default JSON decoder in Python will read all such numbers as ``float`` values, which may result in errors or precision loss. If such loss is unacceptable, then consider casting the value into ``str`` and decoding it on the client side into a more appropriate type, such as ``Decimal``.\n\n\n    .. py:method:: query_required_single_json(query, *args, **kwargs)\n\n        Acquire a connection and use it to run a singleton-returning query and return its element in JSON. The temporary connection is automatically returned back to the pool.\n\n        :param str query: Query text.\n        :param args: Positional query arguments.\n        :param kwargs: Named query arguments.\n\n        :return:\n            Query result encoded in JSON.\n\n        The *query* must return exactly one element.  If the query returns more than one element, an ``gel.ResultCardinalityMismatchError`` is raised, if it returns an empty set, an ``gel.NoDataError`` is raised.\n\n        Note, that positional and named query arguments cannot be mixed.\n\n        .. note::\n\n            Caution is advised when reading ``decimal`` values using this method. The JSON specification does not have a limit on significant digits, so a ``decimal`` number can be losslessly represented in JSON. However, the default JSON decoder in Python will read all such numbers as ``float`` values, which may result in errors or precision loss. If such loss is unacceptable, then consider casting the value into ``str`` and decoding it on the client side into a more appropriate type, such as ``Decimal``.\n\n\n    .. py:method:: execute(query)\n\n        Acquire a connection and use it to execute an EdgeQL command (or commands).  The temporary connection is automatically returned back to the pool.\n\n        :param str query: Query text.\n\n        The commands must take no arguments.\n\n        Example:\n\n        .. code-block:: pycon\n\n            >>> client.execute('''\n            ...     CREATE TYPE MyType {\n            ...         CREATE PROPERTY a -> int64\n            ...     };\n            ...     FOR x IN {100, 200, 300}\n            ...     UNION INSERT MyType { a := x };\n            ... ''')\n\n        .. note::\n            If the results of *query* are desired, :py:meth:`query`, :py:meth:`query_single` or :py:meth:`query_required_single` should be used instead.\n\n    .. py:method:: transaction()\n\n        Open a retryable transaction loop.\n\n        This is the preferred method of initiating and running a database transaction in a robust fashion.  The ``transaction()`` transaction loop will attempt to re-execute the transaction loop body if a transient error occurs, such as a network error or a transaction serialization error.\n\n        Returns an instance of :py:class:`Retry`.\n\n        See :ref:`gel-python-blocking-api-transaction` for more details.\n\n        Example:\n\n        .. code-block:: python\n\n            for tx in client.transaction():\n                with tx:\n                    value = tx.query_single(\"SELECT Counter.value\")\n                    tx.execute(\n                        \"UPDATE Counter SET { value := <int64>$value }\",\n                        value=value + 1,\n                    )\n\n        Note that we are executing queries on the ``tx`` object rather than on the original connection.\n\n        .. note::\n            The transaction starts lazily. A connection is only acquired from the pool when the first query is issued on the transaction instance.\n\n\n    .. py:method:: close(timeout=None)\n\n        Attempt to gracefully close all connections in the pool.\n\n        Wait until all pool connections are released, close them and shut down the pool.  If any error (including timeout) occurs in ``close()`` the pool will terminate by calling :py:meth:`~gel.Client.terminate`.\n\n        :param float timeout: Seconds to wait, ``None`` for wait forever.\n\n\n    .. py:method:: terminate()\n\n        Terminate all connections in the pool.\n\n\n    .. py:method:: ensure_connected()\n\n        If the client does not yet have any open connections in its pool, attempts to open a connection, else returns immediately.\n\n        Since the client lazily creates new connections as needed (up to the configured ``concurrency`` limit), the first connection attempt will only occur when the first query is run on a client. ``ensureConnected`` can be useful to catch any errors resulting from connection mis-configuration by triggering the first connection attempt explicitly.\n\n    .. py:method:: with_transaction_options(options=None)\n\n        Returns a shallow copy of the client with adjusted transaction options.\n\n        :param TransactionOptions options:\n            Object that encapsulates transaction options.\n\n        See :ref:`gel-python-transaction-options` for details.\n\n    .. py:method:: with_retry_options(options=None)\n\n        Returns a shallow copy of the client with adjusted retry options.\n\n        :param RetryOptions options: Object that encapsulates retry options.\n\n        See :ref:`gel-python-retry-options` for details.\n\n    .. py:method:: with_state(state)\n\n        Returns a shallow copy of the client with adjusted state.\n\n        :param State state: Object that encapsulates state.\n\n        See :ref:`gel-python-state` for details.\n\n    .. py:method:: with_default_module(module=None)\n\n        Returns a shallow copy of the client with adjusted default module.\n\n        This is equivalent to using the ``set module`` command, or using the ``reset module`` command when giving ``None``.\n\n        :type module: str or None\n        :param module: Adjust the *default module*.\n\n        See :py:meth:`State.with_default_module` for details.\n\n    .. py:method:: with_module_aliases(aliases_dict=None, /, **aliases)\n\n        Returns a shallow copy of the client with adjusted module aliases.\n\n        This is equivalent to using the ``set alias`` command.\n\n        :type aliases_dict: dict[str, str] or None\n        :param aliases_dict: This is an optional positional-only argument.\n\n        :param dict[str, str] aliases:\n            Adjust the module aliases after applying ``aliases_dict`` if set.\n\n        See :py:meth:`State.with_module_aliases` for details.\n\n    .. py:method:: without_module_aliases(*aliases)\n\n        Returns a shallow copy of the client without specified module aliases.\n\n        This is equivalent to using the ``reset alias`` command.\n\n        :param tuple[str] aliases: Module aliases to reset.\n\n        See :py:meth:`State.without_module_aliases` for details.\n\n    .. py:method:: with_config(config_dict=None, /, **config)\n\n        Returns a shallow copy of the client with adjusted session config.\n\n        This is equivalent to using the ``configure session set`` command.\n\n        :type config_dict: dict[str, object] or None\n        :param config_dict: This is an optional positional-only argument.\n\n        :param dict[str, object] config:\n            Adjust the config settings after applying ``config_dict`` if set.\n\n        See :py:meth:`State.with_config` for details.\n\n    .. py:method:: without_config(*config_names)\n\n        Returns a shallow copy of the client without specified session config.\n\n        This is equivalent to using the ``configure session reset`` command.\n\n        :param tuple[str] config_names: Config to reset.\n\n        See :py:meth:`State.without_config` for details.\n\n    .. py:method:: with_globals(globals_dict=None, /, **globals_)\n\n        Returns a shallow copy of the client with adjusted global values.\n\n        This is equivalent to using the ``set global`` command.\n\n        :type globals_dict: dict[str, object] or None\n        :param globals_dict: This is an optional positional-only argument.\n\n        :param dict[str, object] globals_:\n            Adjust the global values after applying ``globals_dict`` if set.\n\n        See :py:meth:`State.with_globals` for details.\n\n    .. py:method:: without_globals(*global_names)\n\n        Returns a shallow copy of the client without specified globals.\n\n        This is equivalent to using the ``reset global`` command.\n\n        :param tuple[str] global_names: Globals to reset.\n\n        See :py:meth:`State.without_globals` for details.\n\n\n.. _gel-python-blocking-api-transaction:\n\nTransactions\n------------\n\n.. py:class:: Transaction()\n\n    Represents a transaction.\n\n    Instances of this type are yielded by a :py:class:`Retry` iterator.\n\n    .. describe:: with c:\n\n        start and commit/rollback the transaction\n        automatically when entering and exiting the code inside the\n        context manager block.\n\n    .. py:method:: query(query, *args, **kwargs)\n\n        Acquire a connection if the current transaction doesn't have one yet, and use it to run a query and return the results as an :py:class:`gel.Set` instance. The temporary connection is automatically returned back to the pool when exiting the transaction block.\n\n        See :py:meth:`Client.query()\n        <gel.Client.query>` for details.\n\n    .. py:method:: query_single(query, *args, **kwargs)\n\n        Acquire a connection if the current transaction doesn't have one yet, and use it to run an optional singleton-returning query and return its element. The temporary connection is automatically returned back to the pool when exiting the transaction block.\n\n        See :py:meth:`Client.query_single()\n        <gel.Client.query_single>` for details.\n\n    .. py:method:: query_required_single(query, *args, **kwargs)\n\n        Acquire a connection if the current transaction doesn't have one yet, and use it to run a singleton-returning query and return its element. The temporary connection is automatically returned back to the pool when exiting the transaction block.\n\n        See :py:meth:`Client.query_required_single()\n        <gel.Client.query_required_single>` for details.\n\n    .. py:method:: query_json(query, *args, **kwargs)\n\n        Acquire a connection if the current transaction doesn't have one yet, and use it to run a query and return the results as JSON. The temporary connection is automatically returned back to the pool when exiting the transaction block.\n\n        See :py:meth:`Client.query_json()\n        <gel.Client.query_json>` for details.\n\n    .. py:method:: query_single_json(query, *args, **kwargs)\n\n        Acquire a connection if the current transaction doesn't have one yet, and use it to run an optional singleton-returning query and return its element in JSON. The temporary connection is automatically returned back to the pool when exiting the transaction block.\n\n        See :py:meth:`Client.query_single_json()\n        <gel.Client.query_single_json>` for details.\n\n    .. py:method:: query_required_single_json(query, *args, **kwargs)\n\n        Acquire a connection if the current transaction doesn't have one yet, and use it to run a singleton-returning query and return its element in JSON. The temporary connection is automatically returned back to the pool when exiting the transaction block.\n\n        See :py:meth:`Client.query_requried_single_json()\n        <gel.Client.query_required_single_json>` for details.\n\n    .. py:method:: execute(query)\n\n        Acquire a connection if the current transaction doesn't have one yet, and use it to execute an EdgeQL command (or commands).  The temporary connection is automatically returned back to the pool when exiting the transaction block.\n\n        See :py:meth:`Client.execute()\n        <gel.Client.execute>` for details.\n\n.. py:class:: Retry\n\n    Represents a wrapper that yields :py:class:`Transaction`\n    object when iterating.\n\n    See :py:meth:`Client.transaction()` method for\n    an example.\n\n    .. py:method:: __next__()\n\n        Yields :py:class:`Transaction` object every time transaction has to\n        be repeated.\n\n\n.. _gel-python-asyncio-api-reference:\n\nAsyncIO client reference\n========================\n\n.. _gel-python-async-api-client:\n\nClient\n------\n\n.. py:function:: create_async_client(dsn=None, *, \\\n            host=None, port=None, \\\n            user=None, password=None, \\\n            secret_key=None, \\\n            database=None, \\\n            timeout=60, \\\n            concurrency=None)\n\n    Create an asynchronous client with a lazy connection pool.\n\n    The connection parameters may be specified either as a connection URI in *dsn*, or as specific keyword arguments, or both.  If both *dsn* and keyword arguments are specified, the latter override the corresponding values parsed from the connection URI.\n\n    If no connection parameter is specified, the client will try to search in environment variables and then the current project, see :ref:`Client Library Connection <gel_client_connection>` docs for more information.\n\n    Returns a new :py:class:`AsyncIOClient` object.\n\n    :param str dsn:\n        If this parameter does not start with |geluri| then this is interpreted as the :ref:`name of a local instance <ref_reference_connection_instance_name>`.\n\n        Otherwise it specifies a single string in the following format: :geluri:`user:password@host:port/database?option=value`.  The following options are recognized: host, port, user, database, password. For a complete reference on DSN, see the :ref:`DSN Specification <ref_dsn>`.\n\n    :param host:\n        Database host address as an IP address or a domain name;\n\n        If not specified, the following will be tried, in order:\n\n        - host address(es) parsed from the *dsn* argument,\n        - the value of the :gelenv:`HOST` environment variable,\n        - ``\"localhost\"``.\n\n    :param port:\n        Port number to connect to at the server host. If multiple host addresses were specified, this parameter may specify a sequence of port numbers of the same length as the host sequence, or it may specify a single port number to be used for all host addresses.\n\n        If not specified, the value parsed from the *dsn* argument is used, or the value of the :gelenv:`PORT` environment variable, or ``5656`` if neither is specified.\n\n    :param user:\n        The name of the database role used for authentication.\n\n        If not specified, the value parsed from the *dsn* argument is used, or the value of the :gelenv:`USER` environment variable, or the operating system name of the user running the application.\n\n    :param database:\n        The name of the database to connect to.\n\n        If not specified, the value parsed from the *dsn* argument is used, or the value of the :gelenv:`DATABASE` environment variable, or the operating system name of the user running the application.\n\n    :param password:\n        Password to be used for authentication, if the server requires one.  If not specified, the value parsed from the *dsn* argument is used, or the value of the :gelenv:`PASSWORD` environment variable.  Note that the use of the environment variable is discouraged as other users and applications may be able to read it without needing specific privileges.\n\n    :param secret_key:\n        Secret key to be used for authentication, if the server requires one.  If not specified, the value parsed from the *dsn* argument is used, or the value of the :gelenv:`SECRET_KEY` environment variable.  Note that the use of the environment variable is discouraged as other users and applications may be able to read it without needing specific privileges.\n\n    :param float timeout:\n        Connection timeout in seconds.\n\n    :param int concurrency:\n        Max number of connections in the pool. If not set, the suggested concurrency value provided by the server is used.\n\n    :return: An instance of :py:class:`AsyncIOClient`.\n\n    The APIs on the returned client instance can be safely used by different :py:class:`asyncio.Task`/coroutines, because under the hood they are checking out different connections from the pool to run the queries:\n\n    * :py:meth:`AsyncIOClient.query()`\n    * :py:meth:`AsyncIOClient.query_single()`\n    * :py:meth:`AsyncIOClient.query_required_single()`\n    * :py:meth:`AsyncIOClient.query_json()`\n    * :py:meth:`AsyncIOClient.query_single_json()`\n    * :py:meth:`AsyncIOClient.query_required_single_json()`\n    * :py:meth:`AsyncIOClient.execute()`\n    * :py:meth:`AsyncIOClient.transaction()`\n\n    .. code-block:: python\n\n        client = gel.create_async_client()\n        await client.query('SELECT {1, 2, 3}')\n\n    The same for transactions:\n\n    .. code-block:: python\n\n        client = gel.create_async_client()\n        async for tx in client.transaction():\n            async with tx:\n                await tx.query('SELECT {1, 2, 3}')\n\n\n\n.. py:class:: AsyncIOClient()\n\n    An asynchronous client with a connection pool, safe for concurrent use.\n\n    Async clients are created by calling :py:func:`~gel.create_async_client`.\n\n    .. py:coroutinemethod:: query(query, *args, **kwargs)\n\n        Acquire a connection and use it to run a query and return the results as an :py:class:`gel.Set` instance. The temporary connection is automatically returned back to the pool.\n\n        :param str query: Query text.\n        :param args: Positional query arguments.\n        :param kwargs: Named query arguments.\n\n        :return:\n            An instance of :py:class:`gel.Set` containing the query result.\n\n        Note that positional and named query arguments cannot be mixed.\n\n\n    .. py:coroutinemethod:: query_single(query, *args, **kwargs)\n\n        Acquire a connection and use it to run an optional singleton-returning query and return its element. The temporary connection is automatically returned back to the pool.\n\n        :param str query: Query text.\n        :param args: Positional query arguments.\n        :param kwargs: Named query arguments.\n\n        :return:\n            Query result.\n\n        The *query* must return no more than one element.  If the query returns more than one element, an ``gel.ResultCardinalityMismatchError`` is raised, if it returns an empty set, ``None`` is returned.\n\n        Note, that positional and named query arguments cannot be mixed.\n\n\n    .. py:coroutinemethod:: query_required_single(query, *args, **kwargs)\n\n        Acquire a connection and use it to run a singleton-returning query and return its element. The temporary connection is automatically returned back to the pool.\n\n        :param str query: Query text.\n        :param args: Positional query arguments.\n        :param kwargs: Named query arguments.\n\n        :return:\n            Query result.\n\n        The *query* must return exactly one element.  If the query returns more than one element, an ``gel.ResultCardinalityMismatchError`` is raised, if it returns an empty set, an ``gel.NoDataError`` is raised.\n\n        Note, that positional and named query arguments cannot be mixed.\n\n\n    .. py:coroutinemethod:: query_json(query, *args, **kwargs)\n\n        Acquire a connection and use it to run a query and return the results as JSON. The temporary connection is automatically returned back to the pool.\n\n        :param str query: Query text.\n        :param args: Positional query arguments.\n        :param kwargs: Named query arguments.\n\n        :return:\n            A JSON string containing an array of query results.\n\n        Note, that positional and named query arguments cannot be mixed.\n\n        .. note::\n\n            Caution is advised when reading ``decimal`` values using this method. The JSON specification does not have a limit on significant digits, so a ``decimal`` number can be losslessly represented in JSON. However, the default JSON decoder in Python will read all such numbers as ``float`` values, which may result in errors or precision loss. If such loss is unacceptable, then consider casting the value into ``str`` and decoding it on the client side into a more appropriate type, such as ``Decimal``.\n\n\n    .. py:coroutinemethod:: query_single_json(query, *args, **kwargs)\n\n        Acquire a connection and use it to run an optional singleton-returning query and return its element in JSON. The temporary connection is automatically returned back to the pool.\n\n        :param str query: Query text.\n        :param args: Positional query arguments.\n        :param kwargs: Named query arguments.\n\n        :return:\n            Query result encoded in JSON.\n\n        The *query* must return no more than one element.  If the query returns more than one element, an ``gel.ResultCardinalityMismatchError`` is raised, if it returns an empty set, ``\"null\"`` is returned.\n\n        Note, that positional and named query arguments cannot be mixed.\n\n        .. note::\n\n            Caution is advised when reading ``decimal`` values using this method. The JSON specification does not have a limit on significant digits, so a ``decimal`` number can be losslessly represented in JSON. However, the default JSON decoder in Python will read all such numbers as ``float`` values, which may result in errors or precision loss. If such loss is unacceptable, then consider casting the value into ``str`` and decoding it on the client side into a more appropriate type, such as ``Decimal``.\n\n\n    .. py:coroutinemethod:: query_required_single_json(query, *args, **kwargs)\n\n        Acquire a connection and use it to run a singleton-returning query and return its element in JSON. The temporary connection is automatically returned back to the pool.\n\n        :param str query: Query text.\n        :param args: Positional query arguments.\n        :param kwargs: Named query arguments.\n\n        :return:\n            Query result encoded in JSON.\n\n        The *query* must return exactly one element.  If the query returns more than one element, an ``gel.ResultCardinalityMismatchError`` is raised, if it returns an empty set, an ``gel.NoDataError`` is raised.\n\n        Note, that positional and named query arguments cannot be mixed.\n\n        .. note::\n\n            Caution is advised when reading ``decimal`` values using this method. The JSON specification does not have a limit on significant digits, so a ``decimal`` number can be losslessly represented in JSON. However, the default JSON decoder in Python will read all such numbers as ``float`` values, which may result in errors or precision loss. If such loss is unacceptable, then consider casting the value into ``str`` and decoding it on the client side into a more appropriate type, such as ``Decimal``.\n\n\n    .. py:coroutinemethod:: execute(query)\n\n        Acquire a connection and use it to execute an EdgeQL command (or commands).  The temporary connection is automatically returned back to the pool.\n\n        :param str query: Query text.\n\n        The commands must take no arguments.\n\n        Example:\n\n        .. code-block:: pycon\n\n            >>> await con.execute('''\n            ...     CREATE TYPE MyType {\n            ...         CREATE PROPERTY a -> int64\n            ...     };\n            ...     FOR x IN {100, 200, 300}\n            ...     UNION INSERT MyType { a := x };\n            ... ''')\n\n        .. note::\n            If the results of *query* are desired, :py:meth:`query`, :py:meth:`query_single` or :py:meth:`query_required_single` should be used instead.\n\n    .. py:method:: transaction()\n\n        Open a retryable transaction loop.\n\n        This is the preferred method of initiating and running a database transaction in a robust fashion.  The ``transaction()`` transaction loop will attempt to re-execute the transaction loop body if a transient error occurs, such as a network error or a transaction serialization error.\n\n        Returns an instance of :py:class:`AsyncIORetry`.\n\n        See :ref:`gel-python-asyncio-api-transaction` for more details.\n\n        Example:\n\n        .. code-block:: python\n\n            async for tx in con.transaction():\n                async with tx:\n                    value = await tx.query_single(\"SELECT Counter.value\")\n                    await tx.execute(\n                        \"UPDATE Counter SET { value := <int64>$value }\",\n                        value=value + 1,\n                    )\n\n        Note that we are executing queries on the ``tx`` object rather than on the original connection.\n\n        .. note::\n            The transaction starts lazily. A connection is only acquired from the pool when the first query is issued on the transaction instance.\n\n\n    .. py:coroutinemethod:: aclose()\n\n        Attempt to gracefully close all connections in the pool.\n\n        Wait until all pool connections are released, close them and shut down the pool.  If any error (including cancellation) occurs in ``aclose()`` the pool will terminate by calling :py:meth:`~gel.AsyncIOClient.terminate`.\n\n        It is advisable to use :py:func:`python:asyncio.wait_for` to set a timeout.\n\n    .. py:method:: terminate()\n\n        Terminate all connections in the pool.\n\n\n    .. py:coroutinemethod:: ensure_connected()\n\n        If the client does not yet have any open connections in its pool, attempts to open a connection, else returns immediately.\n\n        Since the client lazily creates new connections as needed (up to the configured ``concurrency`` limit), the first connection attempt will only occur when the first query is run on a client. ``ensureConnected`` can be useful to catch any errors resulting from connection mis-configuration by triggering the first connection attempt explicitly.\n\n    .. py:method:: with_transaction_options(options=None)\n\n        Returns a shallow copy of the client with adjusted transaction options.\n\n        :param TransactionOptions options:\n            Object that encapsulates transaction options.\n\n        See :ref:`gel-python-transaction-options` for details.\n\n    .. py:method:: with_retry_options(options=None)\n\n        Returns a shallow copy of the client with adjusted retry options.\n\n        :param RetryOptions options: Object that encapsulates retry options.\n\n        See :ref:`gel-python-retry-options` for details.\n\n    .. py:method:: with_state(state)\n\n        Returns a shallow copy of the client with adjusted state.\n\n        :param State state: Object that encapsulates state.\n\n        See :ref:`gel-python-state` for details.\n\n    .. py:method:: with_default_module(module=None)\n\n        Returns a shallow copy of the client with adjusted default module.\n\n        This is equivalent to using the ``set module`` command, or using the ``reset module`` command when giving ``None``.\n\n        :type module: str or None\n        :param module: Adjust the *default module*.\n\n        See :py:meth:`State.with_default_module` for details.\n\n    .. py:method:: with_module_aliases(aliases_dict=None, /, **aliases)\n\n        Returns a shallow copy of the client with adjusted module aliases.\n\n        This is equivalent to using the ``set alias`` command.\n\n        :type aliases_dict: dict[str, str] or None\n        :param aliases_dict: This is an optional positional-only argument.\n\n        :param dict[str, str] aliases:\n            Adjust the module aliases after applying ``aliases_dict`` if set.\n\n        See :py:meth:`State.with_module_aliases` for details.\n\n    .. py:method:: without_module_aliases(*aliases)\n\n        Returns a shallow copy of the client without specified module aliases.\n\n        This is equivalent to using the ``reset alias`` command.\n\n        :param tuple[str] aliases: Module aliases to reset.\n\n        See :py:meth:`State.without_module_aliases` for details.\n\n    .. py:method:: with_config(config_dict=None, /, **config)\n\n        Returns a shallow copy of the client with adjusted session config.\n\n        This is equivalent to using the ``configure session set`` command.\n\n        :type config_dict: dict[str, object] or None\n        :param config_dict: This is an optional positional-only argument.\n\n        :param dict[str, object] config:\n            Adjust the config settings after applying ``config_dict`` if set.\n\n        See :py:meth:`State.with_config` for details.\n\n    .. py:method:: without_config(*config_names)\n\n        Returns a shallow copy of the client without specified session config.\n\n        This is equivalent to using the ``configure session reset`` command.\n\n        :param tuple[str] config_names: Config to reset.\n\n        See :py:meth:`State.without_config` for details.\n\n    .. py:method:: with_globals(globals_dict=None, /, **globals_)\n\n        Returns a shallow copy of the client with adjusted global values.\n\n        This is equivalent to using the ``set global`` command.\n\n        :type globals_dict: dict[str, object] or None\n        :param globals_dict: This is an optional positional-only argument.\n\n        :param dict[str, object] globals_:\n            Adjust the global values after applying ``globals_dict`` if set.\n\n        See :py:meth:`State.with_globals` for details.\n\n    .. py:method:: without_globals(*global_names)\n\n        Returns a shallow copy of the client without specified globals.\n\n        This is equivalent to using the ``reset global`` command.\n\n        :param tuple[str] global_names: Globals to reset.\n\n        See :py:meth:`State.without_globals` for details.\n\n\n.. _gel-python-asyncio-api-transaction:\n\nTransactions\n------------\n\n.. py:class:: AsyncIORetry\n\n    Represents a wrapper that yields :py:class:`AsyncIOTransaction` object when iterating.\n\n    See :py:meth:`AsyncIOClient.transaction()` method for an example.\n\n    .. py:coroutinemethod:: __anext__()\n\n        Yields :py:class:`AsyncIOTransaction` object every time transaction has to be repeated.\n\n.. py:class:: AsyncIOTransaction\n\n    Represents a transaction.\n\n    Instances of this type are yielded by a :py:class:`AsyncIORetry` iterator.\n\n    .. describe:: async with c:\n\n        Start and commit/rollback the transaction automatically when entering and exiting the code inside the context manager block.\n\n    .. py:coroutinemethod:: query(query, *args, **kwargs)\n\n        Acquire a connection if the current transaction doesn't have one yet, and use it to run a query and return the results as an :py:class:`gel.Set` instance. The temporary connection is automatically returned back to the pool when exiting the transaction block.\n\n        See :py:meth:`AsyncIOClient.query() <gel.AsyncIOClient.query>` for details.\n\n    .. py:coroutinemethod:: query_single(query, *args, **kwargs)\n\n        Acquire a connection if the current transaction doesn't have one yet, and use it to run an optional singleton-returning query and return its element. The temporary connection is automatically returned back to the pool when exiting the transaction block.\n\n        See :py:meth:`AsyncIOClient.query_single() <gel.AsyncIOClient.query_single>` for details.\n\n    .. py:coroutinemethod:: query_required_single(query, *args, **kwargs)\n\n        Acquire a connection if the current transaction doesn't have one yet, and use it to run a singleton-returning query and return its element. The temporary connection is automatically returned back to the pool when exiting the transaction block.\n\n        See :py:meth:`AsyncIOClient.query_required_single() <gel.AsyncIOClient.query_required_single>` for details.\n\n    .. py:coroutinemethod:: query_json(query, *args, **kwargs)\n\n        Acquire a connection if the current transaction doesn't have one yet, and use it to run a query and return the results as JSON. The temporary connection is automatically returned back to the pool when exiting the transaction block.\n\n        See :py:meth:`AsyncIOClient.query_json() <gel.AsyncIOClient.query_json>` for details.\n\n    .. py:coroutinemethod:: query_single_json(query, *args, **kwargs)\n\n        Acquire a connection if the current transaction doesn't have one yet, and use it to run an optional singleton-returning query and return its element in JSON. The temporary connection is automatically returned back to the pool when exiting the transaction block.\n\n        See :py:meth:`AsyncIOClient.query_single_json() <gel.AsyncIOClient.query_single_json>` for details.\n\n    .. py:coroutinemethod:: query_required_single_json(query, *args, **kwargs)\n\n        Acquire a connection if the current transaction doesn't have one yet, and use it to run a singleton-returning query and return its element in JSON. The temporary connection is automatically returned back to the pool when exiting the transaction block.\n\n        See :py:meth:`AsyncIOClient.query_requried_single_json() <gel.AsyncIOClient.query_required_single_json>` for details.\n\n    .. py:coroutinemethod:: execute(query)\n\n        Acquire a connection if the current transaction doesn't have one yet, and use it to execute an EdgeQL command (or commands).  The temporary connection is automatically returned back to the pool when exiting the transaction block.\n\n        See :py:meth:`AsyncIOClient.execute() <gel.AsyncIOClient.execute>` for details.\n\n.. _RFC1004: https://github.com/gel/rfcs/blob/master/text/1004-transactions-api.rst\n"
  },
  {
    "path": "docs/reference/using/python/index.rst",
    "content": ".. _gel-python-intro:\n\n======\nPython\n======\n\n.. toctree::\n   :maxdepth: 3\n   :hidden:\n\n   client\n   api/types\n   api/codegen\n   api/advanced\n\n**gel-python** is the official |Gel| driver for Python.  It provides both :ref:`blocking IO <gel-python-blocking-api-reference>` and :ref:`asyncio <gel-python-asyncio-api-reference>` implementations.\n\nInstallation\n============\n\nInstall the `client package from PyPI <https://pypi.org/project/gel/>`_ using your package manager of choice.\n\n.. tabs::\n\n  .. code-tab:: bash\n    :caption: pip\n\n    $ pip install gel\n\n  .. code-tab:: bash\n    :caption: uv\n\n    $ uv add gel\n\nBasic Usage\n===========\n\nTo start using Gel in Python, create an :py:class:`gel.Client` instance using :py:func:`gel.create_client` or :py:func:`gel.create_async_client` for AsyncIO. This client instance manages a pool of connections to the database which it discovers automatically from either being in a :gelcmd:`project init` directory or being provided connection details via Environment Variables. See :ref:`the environment section of the connection reference <ref_reference_connection_environments>` for more details and options.\n\n.. note::\n\n  If you're using |Gel| Cloud to host your development instance, you can use the :gelcmd:`cloud login` command to authenticate with |Gel| Cloud and then use the :gelcmd:`project init --server-instance <instance-name>` command to create a local project-linked instance that is linked to an Gel Cloud instance. For more details, see :ref:`the Gel Cloud guide <ref_guide_cloud>`.\n\n.. tabs::\n\n  .. code-tab:: python\n    :caption: Blocking\n\n    import datetime\n    import gel\n\n    client = gel.create_client()\n\n    client.query(\"\"\"\n        INSERT User {\n            name := <str>$name,\n            dob := <cal::local_date>$dob\n        }\n    \"\"\", name=\"Bob\", dob=datetime.date(1984, 3, 1))\n\n    user_set = client.query(\n        \"SELECT User {name, dob} FILTER .name = <str>$name\", name=\"Bob\")\n    # *user_set* now contains\n    # Set{Object{name := 'Bob', dob := datetime.date(1984, 3, 1)}}\n\n    client.close()\n\n  .. code-tab:: python\n    :caption: AsyncIO\n\n    import asyncio\n    import datetime\n    import gel\n\n    client = gel.create_async_client()\n\n    async def main():\n        await client.query(\"\"\"\n            INSERT User {\n                name := <str>$name,\n                dob := <cal::local_date>$dob\n            }\n        \"\"\", name=\"Bob\", dob=datetime.date(1984, 3, 1))\n\n        user_set = await client.query(\n            \"SELECT User {name, dob} FILTER .name = <str>$name\", name=\"Bob\")\n        # *user_set* now contains\n        # Set{Object{name := 'Bob', dob := datetime.date(1984, 3, 1)}}\n\n        await client.aclose()\n\n    asyncio.run(main())\n\n.. _gel-python-connection-pool:\n\nClient connection pools\n-----------------------\n\nFor server-type applications that handle frequent requests and need the database connection for a short period of time while handling a request, you will want to use a connection pool. Both :py:class:`gel.Client` and :py:class:`gel.AsyncIOClient` come with such a pool.\n\nFor :py:class:`gel.Client`, all methods are thread-safe. You can share the same client instance safely across multiple threads, and run queries concurrently. Likewise, :py:class:`~gel.AsyncIOClient` is designed to be shared among different :py:class:`asyncio.Task`/coroutines for concurrency.\n\nBelow is an example of a web API server running `fastapi <https://fastapi.tiangolo.com/>`_:\n\n.. code-block:: python\n\n    import asyncio\n    import gel\n    from fastapi import FastAPI, Query\n\n    app = FastAPI()\n\n    @app.on_event(\"startup\")\n    async def startup_event():\n        \"\"\"Initialize the database client on startup.\"\"\"\n        app.state.client = gel.create_async_client()\n        # Optional: explicitly start up the connection pool\n        await app.state.client.ensure_connected()\n\n    @app.get(\"/users\")\n    async def handle(\n      name: str = Query(None)\n    ):\n        \"\"\"Handle incoming requests.\"\"\"\n        client = app.state.client\n\n        # Execute the query on any pool connection\n        if name:\n          result = await client.query_single(\n              '''\n                  SELECT User {first_name, email, bio}\n                  FILTER .name = <str>$username\n              ''', username=name)\n        else:\n          result = await client.query(\n              '''\n                  SELECT User {first_name, email, bio}\n              ''')\n        return result\n\n    if __name__ == \"__main__\":\n        import uvicorn\n        uvicorn.run(app, host=\"0.0.0.0\", port=8000)\n\nNote that the client instance itself is created synchronously. Pool connections are created lazily as they are needed. If you want to explicitly connect to the database in ``startup_event()``, use the ``ensure_connected()`` method on the client.\n\nFor more information, see API documentation of :ref:`the blocking client <gel-python-blocking-api-client>` and :ref:`the asynchronous client <gel-python-async-api-client>`.\n\n\nTransactions\n------------\n\nTo create a :ref:`transaction <ref_eql_transactions>` use the ``transaction()`` method on the client instance:\n\n* :py:meth:`AsyncIOClient.transaction() <gel.AsyncIOClient.transaction>`\n* :py:meth:`Client.transaction() <gel.Client.transaction>`\n\n\nExample:\n\n.. tabs::\n\n  .. code-tab:: python\n    :caption: Blocking\n\n    for tx in client.transaction():\n        with tx:\n            tx.execute(\"INSERT User {name := 'Don'}\")\n\n  .. code-tab:: python\n    :caption: AsyncIO\n\n    async for tx in client.transaction():\n        async with tx:\n            await tx.execute(\"INSERT User {name := 'Don'}\")\n\n.. note::\n\n  When not in an explicit transaction block, any changes to the database will be applied immediately.\n\nFor more information, see API documentation of transactions for :ref:`the blocking client <gel-python-blocking-api-transaction>` and :ref:`the AsyncIO client <gel-python-asyncio-api-transaction>`.\n\n\nCode generation\n===============\n\n.. py:currentmodule:: gel\n\nThe ``gel-python`` package exposes a command-line tool to generate typesafe functions from ``*.edgeql`` files, using :py:mod:`dataclasses` for objects primarily.\n\n.. code-block:: edgeql\n  :caption: queries/get_user_by_name.edgeql\n\n  with\n      name := <str>$name,\n  select User { first_name, email, bio }\n  filter .name = name;\n\n.. code-block:: bash\n\n  $ gel-py\n  # or\n  $ python -m gel.codegen\n\n.. code-block:: python\n\n  import gel\n  from .queries import get_user_by_name_sync_edgeql as get_user_by_name_qry\n\n  client = gel.create_async_client()\n\n  async def main():\n    result = await get_user_by_name_qry.get_user_by_name(client, name=\"John\")\n    print(result)\n\n  asyncio.run(main())\n\n"
  },
  {
    "path": "docs/reference/using/sql_adapter.rst",
    "content": ".. _ref_sql_adapter:\n\n===========\nSQL adapter\n===========\n\nConnecting\n==========\n\n|Gel| server supports PostgreSQL connection interface. It implements PostgreSQL\nwire protocol as well as SQL query language.\n\nAs of |Gel| 6.0, it also supports a subset of Data Modification Language,\nnamely INSERT, DELETE and UPDATE statements.\n\nIt does not, however, support PostgreSQL Data Definition Language\n(e.g. ``CREATE TABLE``). This means that it is not possible to use SQL\nconnections to Gel to modify its schema. Instead, the schema should be\nmanaged in |.gel| files using Gel Schema Definition Language and migration\ncommands.\n\nAny Postgres-compatible client can connect to a Gel database by using the\nsame port that is used for the Gel protocol and the |branch| name, username,\nand password already used for the database.\n\n.. versionchanged:: 5.0\n\n    Here's how you might connect to a local instance on port 10701 (determined\n    by running :gelcmd:`instance list`) on a branch |main| using the\n    ``psql`` CLI:\n\n    .. code-block:: bash\n\n        $ psql -h localhost -p 10701 -U admin -d main\n\n    You'll then be prompted for a password. If you don't have it, you can run\n    :gelcmd:`instance credentials --insecure-dsn` and grab it out of the DSN\n    the command returns. (It's the string between the second colon and the \"at\"\n    symbol: :geluri:`admin:PASSWORD_IS_HERE@<host>:<port>/<branch>`)\n\n.. note::\n\n    The insecure DSN returned by the CLI for Gel Cloud instances will not\n    contain the password. You will need to either :ref:`create a new role and\n    set the password <ref_sql_adapter_new_role>`, using those values to connect\n    to your SQL client, or change the password of the existing role, using that\n    role name along with the newly created password.\n\n    .. code-block:: edgeql-repl\n\n        db> alter role admin {\n        ...   set password := 'my-password'\n        ... };\n        OK: ALTER ROLE\n\n.. warning::\n\n    Connecting to a Gel Cloud instance via a Postgres client requires SNI\n    support which was introduced in libpq v14. If a Postgres client uses your\n    system's libpq (``psql`` does), you can connect as long as your libpq\n    version is 14+. To check your version, run ``psql --version`` or\n    ``pg_config --version``.\n\n    If you're on Windows and these do not work for you, you can instead\n    navigate to ``bin`` under your Postgres installation location, right-click\n    ``libpq.dll``, click \"Properties,\" and find the version on the \"Details\"\n    tab.\n\n.. _ref_sql_adapter_new_role:\n\nCreating a new role\n-------------------\n\nThis works well to test SQL support, but if you are going to be using it on an\nongoing basis, you may want to create a new role and use it to authenticate\nyour SQL clients. Set a password when you create your role. Then, use the role\nname as your user name when you connect via your SQL client.\n\n.. code-block:: edgeql\n\n    create superuser role sql {\n      set password := 'your-password'\n    };\n\n.. code-block:: bash\n\n    $ psql -h localhost -p 10701 -U sql -d main\n\nIn this example, when prompted for the password, you would enter\n``your-password``.\n\n.. warning::\n\n    Gel server requires TLS by default, and this is also true for our SQL\n    support. Make sure to require SSL encryption in your SQL tool or client\n    when using Gel's SQL support. Alternatively, you can disable the TLS\n    requirement by setting the :gelenv:`SERVER_BINARY_ENDPOINT_SECURITY`\n    environment variable to ``optional``.\n\n\nQuerying\n========\n\nObject types in your Gel schema are exposed as regular SQL tables containing\nall the data you store in your Gel database.\n\nIf you have a database with the following schema:\n\n.. code-block:: sdl\n\n    module default {\n        type Person {\n            name: str;\n        };\n\n        type Movie extending common::Content {\n            release_year: int32;\n            director: Person;\n            star: Person {\n                role: str;\n            };\n            multi actors: Person {\n                role: str;\n            };\n            multi labels: str;\n        };\n    }\n    module common {\n        type Content {\n            title: str;\n        };\n    }\n\nyou can access your data after connecting using the following SQL queries:\n\n.. code-block:: sql\n\n    SELECT id, name FROM \"Person\";\n    SELECT id, title, release_year, director_id, star_id FROM \"Movie\";\n\nBecause the link ``star`` has link properties, it has its own table.\n``source`` is the ``id`` of the ``Movie``. ``target`` is the ``id`` of the\n``Person``.\n\n.. code-block:: sql\n\n    SELECT source, target, role FROM \"Movie.star\";\n\nLinks are in separate tables.\n\n.. code-block:: sql\n\n    SELECT source, target, role FROM \"Movie.actors\";\n\nMulti properties are in separate tables. ``source`` is the ``id`` of the Movie.\n``target`` is the value of the property.\n\n.. code-block:: sql\n\n    SELECT source, target FROM \"Movie.labels\";\n\nWhen using inheritance, parent object types' tables will by default contain\nall objects of both the parent type and any child types. The query below will\nreturn all ``common::Content`` objects as well as all ``Movie`` objects.\n\n.. code-block:: sql\n\n    SELECT id, title FROM common.\"Content\";\n\nTo omit objects of child types, use ``ONLY``. This query will return\n``common::Content`` objects but not ``Movie`` objects.\n\n.. code-block:: sql\n\n    SELECT id, title FROM ONLY common.\"Content\";\n\nThe SQL adapter supports a large majority of SQL language, including:\n\n- ``SELECT`` and all read-only constructs (``WITH``, sub-query, ``JOIN``, ...),\n- ``INSERT`` / ``UPDATE`` / ``DELETE``,\n- ``COPY ... FROM``,\n- ``SET`` / ``RESET`` / ``SHOW``,\n- transaction commands,\n- ``PREPARE`` / ``EXECUTE`` / ``DEALLOCATE``.\n\n.. code-block:: sql\n\n    SELECT id, 'Title is: ' || tittle\n    FROM \"Movie\" m\n    JOIN \"Person\" d ON m.director_id = d.id\n    WHERE EXISTS (\n        SELECT 1\n        FROM \"Movie.actors\" act\n        WHERE act.source = m.id\n    );\n\nThe SQL adapter emulates the ``information_schema`` and ``pg_catalog`` views to\nmimic the catalogs provided by Postgres 13.\n\n.. note::\n\n    Learn more about the Postgres information schema from `the Postgres\n    information schema documentation\n    <https://www.postgresql.org/docs/13/information-schema.html>`_.\n\n.. warning::\n\n    Some tables may be truncated and may not contain all objects you would\n    expect a true Postgres instance to contain. This may be a source of\n    problems when using tools that introspect the database and rely on internal\n    Postgres features.\n\n\nTested SQL tools\n================\n\n- `pg_dump <https://www.postgresql.org/docs/13/app-pgdump.html>`_\n- `Metabase <https://www.metabase.com/>`_\n- `Cluvio <https://www.cluvio.com/>`_\n- `Tableau <https://www.tableau.com/>`_\n- `DataGrip <https://www.jetbrains.com/datagrip/>`_\n- `Airbyte <https://airbyte.com/>`_ [1]_\n- `Fivetran <https://www.fivetran.com/>`_ [1]_\n- `Hevo <https://hevodata.com/>`_ [1]_\n- `Stitch <https://www.stitchdata.com/>`_ [1]_\n- `dbt <https://www.getdbt.com/>`_ [2]_\n\n\n.. [1] At the moment, Gel does not support \"Log replication\" (i.e., using\n   the `Postgres replication mechanism`_). Supported replication methods\n   include `XMIN Replication`_, incremental updates using \"a user-defined\n   monotonically increasing id,\" and full table updates.\n.. [2] dbt models are built and stored in the database as either tables or\n   views. Because the Gel SQL adapter does not allow writing or even\n   creating schemas, view, or tables, any attempt to materialize dbt models\n   will result in errors. If you want to build the models, we suggest first\n   transferring your data to a true Postgres instance via pg_dump or Airbyte.\n   Tests and previews can still be run directy against the Gel instance.\n\n.. _Postgres replication mechanism:\n   https://www.postgresql.org/docs/current/runtime-config-replication.html\n.. _XMIN Replication:\n   https://www.postgresql.org/docs/15/ddl-system-columns.html\n\n\nFor problems with COPY command, see\n:ref:`known limitations<ref_sql_adapter_copy>`.\n\n\nGel to PostgreSQL\n=================\n\nAs mentioned, the SQL schema of the database is managed trough Gel Schema\nDefinition Language. Here is a breakdown of how each of its\nconstructs is mapped to PostgreSQL schema:\n\n- Objects types are mapped into tables.\n  Each table has columns ``id UUID`` and ``__type__ UUID`` and one column for\n  each single property or link.\n\n- Single properties are mapped to tables columns.\n\n- Single links are mapped to table columns with suffix ``_id`` and are of type\n  ``UUID``. They contain the ids of the link's target type.\n\n- Multi properties are mapped to tables with two columns:\n\n  - ``source UUID``, which contains the id of the property's source object type,\n  - ``target``, which contains values of the property.\n\n- Multi links are mapped to tables with columns:\n\n  - ``source UUID``, which contains the id of the property's source object type,\n  - ``target UUID``, which contains the ids of the link's target object type,\n  - one column for each link property, using the same rules as properties on\n    object types.\n\n- Aliases are not mapped to PostgreSQL schema.\n\n.. versionadded:: 6.0\n\n    - Globals are mapped to connection settings, prefixed with ``global``.\n      For example, a ``global default::username: str`` can be accessed using:\n\n      .. code-block:: sql\n\n          SET \"global default::username\" TO 'Tom'``;\n          SHOW \"global default::username\";\n\n    - Access policies are applied to object type tables when setting\n      ``apply_access_policies_pg`` is set to ``true``.\n\n    - Mutation rewrites and triggers are applied to all DML commands.\n\nDML commands\n============\n\n.. versionadded:: 6.0\n\nWhen using ``INSERT``, ``DELETE`` or ``UPDATE`` on any table, mutation\nrewrites and triggers are applied. These commands do not have a\nstraight-forward translation to EdgeQL DML commands, but instead use the\nfollowing mapping:\n\n- ``INSERT INTO \"Foo\"`` object table maps to ``insert Foo``,\n\n- ``INSERT INTO \"Foo.keywords\"`` link/property table maps to an\n    ``update Foo { keywords += ... }``,\n\n- ``DELETE FROM \"Foo\"`` object table maps to ``delete Foo``,\n\n- ``DELETE FROM \"Foo.keywords\"`` link property/table maps to\n    ``update Foo { keywords -= ... }``,\n\n- ``UPDATE \"Foo\"`` object table maps to ``update Foo set { ... }``,\n\n- ``UPDATE \"Foo.keywords\"`` is not supported.\n\n\nConnection settings\n===================\n\nSQL adapter supports most of PostgreSQL connection settings\n(for example ``search_path``), in the same manner as plain PostgreSQL:\n\n.. code-block:: sql\n\n    SET search_path TO my_module;\n\n    SHOW search_path;\n\n    RESET search_path;\n\n.. versionadded:: 6.0\n\n    In addition, there are the following Gel-specific settings:\n\n    - settings prefixed with ``\"global \"`` set the values of globals.\n\n      Because SQL syntax allows only string, integer and float constants in\n      ``SET`` command, globals of other types such as ``datetime`` cannot be set\n      this way.\n\n      .. code-block:: sql\n\n          SET \"global my_module::hello\" TO 'world';\n\n      Special handling is in place to enable setting:\n        - ``bool`` types via integers 0 or 1),\n        - ``uuid`` types via hex-encoded strings.\n\n      .. code-block:: sql\n\n          SET \"global my_module::current_user_id\"\n           TO \"592c62c6-73dd-4b7b-87ba-46e6d34ec171\";\n          SET \"global my_module::is_admin\" TO 1;\n\n      To set globals of other types via SQL, it is recommended to change the\n      global to use one of the simple types instead, and use appropriate casts\n      where the global is used.\n\n\n    - ``allow_user_specified_id`` (default ``false``),\n\n    - ``apply_access_policies_pg`` (default ``true``),\n\n    Note that if ``allow_user_specified_id`` or ``apply_access_policies_pg`` are\n    unset, they default to configuration set by ``configure current database``\n    EdgeQL command.\n\n\n.. note::\n\n    When using external tools that do not support settings, it is possible to\n    disable access policies via ``apply_access_policies_pg_default`` role\n    property:\n\n    .. code-block:: edgeql\n\n        CREATE SUPERUSER ROLE pg_connector {\n            # ...\n            SET apply_access_policies_pg_default := false;\n        }\n\n\nIntrospection\n=============\n\nThe adapter emulates introspection schemas of PostgreSQL: ``information_schema``\nand ``pg_catalog``.\n\nBoth schemas are not perfectly emulated, since they are quite large and\ncomplicated stores of information, that also changed between versions of\nPostgreSQL.\n\nBecause of that, some tools might show objects that are not queryable or might\nreport problems when introspecting. In such cases, please report the problem on\nGitHub so we can track the incompatibility down.\n\nNote that since the two information schemas are emulated, querying them may\nperform worse compared to other tables in the database. As a result, tools like\n``pg_dump`` and other introspection utilities might seem slower.\n\n\nLocking\n=======\n\n.. versionadded:: 6.0\n\nSQL adapter supports LOCK command with the following limitations:\n\n- it cannot be used on tables that represent object types with access\n    properties or links of such objects,\n- it cannot be used on tables that represent object types that have child\n    types extending them.\n\nQuery cache\n===========\n\nAn SQL query is issued to Gel, it is compiled to an internal SQL query, which\nis then issued to the backing PostgreSQL instance. The compiled query is then\ncached, so each following issue of the same query will not perform any\ncompilation, but just pass through the cached query.\n\n.. versionadded:: 6.0\n\n    Additionally, most queries are \"normalized\" before compilation. This process\n    extracts constant values and replaces them by internal query parameters.\n    This allows sharing of compilation cache between queries that differ in\n    only constant values. This process is totally opaque and is fully handled by\n    Gel. For example:\n\n    .. code-block:: sql\n\n        SELECT $1, 42;\n\n    ... is normalized to:\n\n    .. code-block:: sql\n\n        SELECT $1, $2;\n\n    This way, when a similar query is issued to Gel:\n\n    .. code-block:: sql\n\n        SELECT $1, 500;\n\n    ... it normalizes to the same query as before, so it can reuse the query\n    cache.\n\n    Note that normalization process does not (yet) remove any whitespace, so\n    queries ``SELECT 1;`` and ``SELECT 1 ;`` are compiled separately.\n\n\nKnown limitations\n=================\n\nFollowing SQL statements are not supported:\n\n- ``CREATE``, ``ALTER``, ``DROP``,\n\n- ``TRUNCATE``, ``COMMENT``, ``SECURITY LABEL``, ``IMPORT FOREIGN SCHEMA``,\n\n- ``GRANT``, ``REVOKE``,\n\n- ``OPEN``, ``FETCH``, ``MOVE``, ``CLOSE``, ``DECLARE``, ``RETURN``,\n\n- ``CHECKPOINT``, ``DISCARD``, ``CALL``,\n\n- ``REINDEX``, ``VACUUM``, ``CLUSTER``, ``REFRESH MATERIALIZED VIEW``,\n\n- ``LISTEN``, ``UNLISTEN``, ``NOTIFY``,\n\n- ``LOAD``.\n\nFollowing functions are not supported:\n\n- ``set_config``,\n- ``pg_filenode_relation``,\n- most of system administration functions.\n\n\nCOPY\n----\n\n.. _ref_sql_adapter_copy:\n\nCommand ``COPY`` does not support ``FROM STDIN`` (i.e. to insert rows in bulk).\nYou should use prepared ``INSERT`` statements instead.\n\nWhen exporting data using ``COPY ... TO STDOUT``, using query parameters or\n:ref:`global variables <ref_datamodel_globals>` is not supported.\nThis includes globals of :ref:`role's permissions<ref_datamodel_permissions>`\nand accessing global trough access policies or computed properties.\n\nFor example, it is not possible to ``COPY`` either ``A`` or ``B``:\n\n.. code-block:: sdl\n\n    type A {\n        access policy my_policy\n            allow select\n            using (global my_global);\n    }\n\n    type B {\n        property x := global my_global;\n    }\n\nRecommended workaround is to disable access policies either\nvia ``apply_access_policies_pg`` connection setting or\nvia ``apply_access_policies_pg_default`` property of roles.\n\n\nExample: gradual transition from ORMs to Gel\n============================================\n\nWhen a project is using Object-Relational Mappings (e.g. SQLAlchemy, Django,\nHibernate ORM, TypeORM) and is considering the migration to Gel, it might\nwant to execute the transition gradually, as opposed to a total rewrite of the\nproject.\n\nIn this case, the project can start the transition by migrating the ORM models\nto Gel Schema Definition Language.\n\nFor example, such Hibernate ORM model in Java:\n\n.. code-block::\n\n    @Entity\n    class Movie {\n        @Id\n        @GeneratedValue(strategy = GenerationType.UUID)\n        UUID id;\n\n        private String title;\n\n        @NotNull\n        private Integer releaseYear;\n\n        // ... getters and setters ...\n    }\n\n... would be translated to the following Gel SDL:\n\n.. code-block:: sdl\n\n    type Movie {\n        title: str;\n\n        required releaseYear: int32;\n    }\n\nA new Gel instance can now be created and migrated to the translated schema.\nAt this stage, Gel will allow SQL connections to write into the ``\"Movie\"``\ntable, just as it would have been created with the following DDL command:\n\n.. code-block:: sql\n\n    CREATE TABLE \"Movie\" (\n        id UUID PRIMARY KEY DEFAULT (...),\n        __type__ UUID NOT NULL DEFAULT (...),\n        title TEXT,\n        releaseYear INTEGER NOT NULL\n    );\n\nWhen translating the old ORM model to Gel SDL, one should aim to make the\nSQL schema of Gel match the SQL schema that the ORM expects.\n\nWhen this match is accomplished, any query that used to work with the old, plain\nPostgreSQL, should now also work with the Gel. For example, we can execute\nthe following query:\n\n.. code-block:: sql\n\n    INSERT INTO \"Movie\" (title, releaseYear)\n    VALUES (\"Madagascar\", 2012)\n    RETURNING id, title, releaseYear;\n\nTo complete the migration, the data can be exported from our old database into\nan ``.sql`` file, which can be import it into Gel:\n\n.. code-block:: bash\n\n    $ pg_dump {your PostgreSQL connection params} \\\n        --data-only --inserts --no-owner --no-privileges \\\n        > dump.sql\n\n    $ psql {your Gel connection params} --file dump.sql\n\nNow, the ORM can be pointed to Gel instead of the old PostgreSQL database,\nwhich has been fully replaced.\n\nArguably, the development of new features with the ORM is now more complex for\nthe duration of the transition, since the developer has to modify two model\ndefinitions: the ORM and the Gel schema.\n\nBut it allows any new models to use Gel schema, EdgeQL and code generators\nfor the client language of choice. The ORM-based code can now also be gradually\nrewritten to use EdgeQL, one model at the time.\n\nFor a detailed migration example, see repository\n`geldata/hibernate-example <https://github.com/geldata/hibernate-example>`_.\n"
  },
  {
    "path": "docs/resources/changelog/1_0_a2.rst",
    "content": ":orphan:\n\n.. _ref_changelog_alpha2:\n\n===========\n1.0 Alpha 2\n===========\n\nThis changelog summarizes new features and breaking changes in\n`EdgeDB 1.0 alpha 2 <https://www.edgedb.com/blog/edgedb-1-0-alpha-2>`_.\n\n\nNew JavaScript Driver\n=====================\n\n|EdgeDB| has a new high-performance native\n`EdgeDB driver <https://github.com/edgedb/edgedb-js>`_ for NodeJS 10+.\n\nThe driver is written in strict TypeScript, thoroughly tested, and has\nfirst-class async/await support.  It is at least **twice** as efficient as\ncomparable current PostgreSQL JavaScript drivers.\n\nInstall it with ``npm`` or ``yarn``:\n\n.. code-block:: bash\n\n   $ npm install gel\n\nand it is ready for use:\n\n.. code-block:: javascript\n\n    const edgedb = require(\"edgedb\");\n\n    async function main() {\n      const conn = await edgedb.connect({\n        user: \"edgedb\",\n        host: \"127.0.0.1\",\n      });\n\n      try {\n        console.log(await conn.fetchOne(\"select 1 + 1\"));\n      } finally {\n        await conn.close();\n      }\n    }\n\n    main();\n\nThe documentation can be found :ref:`here <gel-js-intro>`.\n\n\nStandard Library\n================\n\nstd::bigint\n-----------\n\nThe new :eql:type:`std::bigint` scalar type is an arbitrary precision integral\ntype.  The motivation for the new type is that many platforms lack a true\nmulti-precision decimal type, but implement an arbitrary-precision integer\ntype (JavaScript is a prominent example).  The ``n`` suffix on numeric literals\ncan now be used to express both ``std::bigint`` and :eql:type:`std::decimal`:\n\n.. code-block:: edgeql-repl\n\n    db> select 1n is std::bigint;\n    {true}\n\n    db> select 1.0n is std::decimal;\n    {true}\n\nThe ``std::bigint`` and ``std::decimal`` maintain the\nrelationship that is similar to the relationship between :eql:type:`std::int64`\nand :eql:type:`std::float64`.  All sized integer types are implicitly\ncastable to ``bigint``, and ``bigint`` itself can implicitly cast to\n``std::decimal``.\n\n\nNew \"cal\" Module\n----------------\n\nNon-timezone aware date/time types and functions are moved into the new\n``cal`` module.  This separation promotes :eql:type:`std::datetime`\nto be the default safe choice for most use cases.  The types in the ``cal::``\nmodule are useful to implement calendars, alarms, reminders, and other cases\nwhere time is relative and imprecise.  The updated date/time types are\nlisted below:\n\n.. list-table::\n    :class: funcoptable\n\n    * - :eql:type:`std::datetime`\n      - A timezone-aware date/time type.\n\n    * - :eql:type:`std::duration`\n      - An absolute time interval. Can be unambiguously used with\n        both :eql:type:`std::datetime` and :eql:type:`cal::local_datetime`\n        types.\n\n    * - :eql:type:`cal::local_datetime`\n      - Represents date and time without time zone.\n\n    * - :eql:type:`cal::local_date`\n      - Represents date without time zone and time components.\n\n    * - :eql:type:`cal::local_time`\n      - Represents time without time zone and date components.\n\nType conversion between timezone-aware ``std::datetime`` and local date/time\nvalues (types in the ``cal::`` module) is always\n:ref:`explicit and unambiguous <ref_std_datetime>`.\n\n(See :eql:gh:`#902` for details.)\n\nOther Fixes and Enhancements\n----------------------------\n\n* Prohibit ``NaN`` as a ``std::decimal`` value (:eql:gh:`5e16ace1`).\n\n* Rename ``std::datetime_trunc`` to ``std::datetime_truncate``\n  (:eql:gh:`#952`).\n\n* Make :eql:func:`datetime_get` and :eql:func:`datetime_truncate` stricter\n  (:eql:gh:`#958`).\n\n* Disable days and months units in :eql:type:`duration` (:eql:gh:`#947`).\n\n* Rename ``sys::transaction_isolation_t`` to ``sys::TransactionIsolation``\n  (:eql:gh:`c45ee4ba`).\n\n* Rename ``schema::cardinality_t`` to ``schema::Cardinality``\n  (:eql:gh:`b2ceaa61`).\n\n* Rename ``schema::target_delete_action_t`` to ``schema::TargetDeleteAction``\n  (:eql:gh:`6a7c6787`).\n\n* Rename ``schema::operator_kind_t`` to ``schema::OperatorKind``\n  (:eql:gh:`3a01f616`).\n\n* Rename ``schema::volatility_t`` to ``schema::Volatility``\n  (:eql:gh:`16e263cc`).\n\n\nDump / Restore\n==============\n\nThe new :ref:`edgedb dump <ref_cli_gel_dump>` and\n:ref:`edgedb restore <ref_cli_gel_restore>` commands can be used to\nsafely dump and restore |EdgeDB| databases, including when upgrading to new\nversions of |EdgeDB|.\n\n\nEdgeQL\n======\n\n[is ...] Operator\n-----------------\n\nThe :eql:op:`[is ...] <isintersect>` operator is now used to specify the\nlink target type in shapes.  Consider the following query that fetches a\n``User`` along with everything linked to it via the ``favorites`` link:\n\n.. code-block:: edgeql\n\n    select User {\n        favorites: {\n            title\n        }\n    }\n    filter .id = <uuid>$id;\n\nUsing the :eql:op:`[is ...] <isintersect>` operator we can filter the\nset of user favorites:\n\n.. code-block:: edgeql\n\n    select User {\n        # the old syntax was \"favorites: Book {...}\"\n        favorites[is Book]: {\n            title\n        }\n    }\n    filter .id = <uuid>$id;\n\nThis change makes the shape construct consistent with the paths syntax\nand removes potential confusion with the similarly looking computed\nexpressions in shapes.\n\nAnother change is related to backlink navigation. Starting with Alpha 2\nit is required to use the :eql:op:`[is ...] <isintersect>` operator in order\nto access target objects' properties and links:\n\n.. code-block:: edgeql\n\n    select User.<profile[is Profile].settings;\n\n(See :eql:gh:`#969` for details.)\n\nOther Fixes and Enhancements\n----------------------------\n\n* Update the semantics of line continuation (trailing ``\\``) in strings\n  (:eql:gh:`#921`).\n* Remove the ``.>`` alternate syntax for forward link navigation\n  (:eql:gh:`#982`).\n* Fix interaction of the :eql:stmt:`for` statement and nested shapes\n  (:eql:gh:`#834`).\n* Place restrictions on the use of DML statements (:eql:gh:`#741`).\n* Fix queries with unions with overlapping subtypes (:eql:gh:`#1010`).\n* Allow trailing commas in the :ref:`with <ref_eql_with>` clause\n  (:eql:gh:`#868`).\n* Ban use of ``::`` in quoted names (:eql:gh:`#840`).\n* Add syntax for quoting backticks in quoted names (:eql:gh:`#632`).\n* Remove ``select``-like clauses from the :eql:stmt:`for` statement\n  (:eql:gh:`#743`).\n* Fix implicit ``id`` and ``__tid__`` properties injection in\n  DML statements (:eql:gh:`#664`).\n* Make type variants made by shapes consistent with schema inheritance\n  rules (:eql:gh:`36e86d56`).\n* Implement rudimentary support for type intersection (:eql:gh:`177aa1f8`).\n* Optimize single link type indirections when possible (:eql:gh:`48cdfa54`).\n* Stop enforcing common prefix ambiguity restriction on tuple dereference\n  (:eql:gh:`9011c821`).\n* Add an error hint for incorrect string line continuation\n  (:eql:gh:`7b982e09`).\n* Enable comparison of collections of distinct (but compatible) types\n  (:eql:gh:`c913df11`).\n* Implement std::IN as a derivative of std::= (:eql:gh:`f3682e92`).\n\n\nGraphQL\n=======\n\nEnhanced Filtering\n------------------\n\nIt is now possible to filter by traversing arbitrarily deep links,\nnot just immediate properties:\n\n.. code-block:: graphql\n\n    query {\n      UserGroup(\n        filter: {settings: {name: {eq: \"setting06\"}}}\n      ) {\n        name\n        settings {\n          name\n          value\n        }\n      }\n    }\n\nMutations\n---------\n\nInsert, update, and delete :ref:`mutations <ref_graphql_mutations>`\nare now supported.\n\nMutations support all the same parameters as a query like ``filter``,\n``order``, ``first``, ``last``, ``after``, and ``before``.\n\nInsert and update mutations accept a ``data`` parameter that allows to\nspecify what data to insert or how to update the existing data:\n\n.. using \"graphql-schema\" because the graphql syntax below is invalid\n.. code-block:: graphql-schema\n\n    mutation update_Foo(\n      filter: ...,\n      order: ...,\n      first: ...,\n      last: ...,\n      before: ...,\n      after: ...,\n\n      data: {\n        prop1: {clear: true},\n        prop2: {set: \"new value\"},\n        link1: {set:\n          [{\n            # objects can be specified via\n            # the same interface as a query\n            filter: ...,\n            order: ...,\n            first: ...,\n            last: ...,\n            before: ...,\n            after: ...\n          }]\n        }\n      }\n    ) {\n      id\n      prop1\n      ...\n    }\n\nOther Fixes and Enhancements\n----------------------------\n\n* Fix backlinks in aliases (:eql:gh:`#990`).\n* Fix covariant types support (:eql:gh:`#709`).\n* Implement explicit handling of 64-bit integers, and arbitrary precision\n  integers and decimals (:eql:gh:`#1138`).\n\n\nDDL / SDL / Schema\n==================\n\nDDL and SDL layers are heavily refactored in alpha 2. A lot of issues were\nfixed; this section lists only new features and backwards incompatible\nchanges:\n\n* Rename \"views\" to \"expression aliases\" (:eql:gh:`#989`).\n* Add a :ref:`\"module\" <ref_eql_sdl_modules>` block to SDL (:eql:gh:`#907`).\n* Rename SDL keyword \"inherited\" to\n  :ref:`\"overloaded\" <ref_eql_sdl_links_overloading>`. (:eql:gh:`#806`).\n* Reimplement :ref:`SDL <ref_eql_sdl>` through :ref:`DDL <ref_eql_ddl>`.\n  (:eql:gh:`824f14a6`).\n* Rename the DDL ``from`` clause to ``using`` (:eql:gh:`4194ab46`).\n* Add support for collection type views :eql:gh:`367820ba`.\n* Prohibit ``multi`` or ``required`` link properties (:eql:gh:`#994`).\n* Forbid redefinition of read-only flag. (:eql:gh:`#1048`).\n* Change ``set annotation`` to ``create/alter annotation``\n  (:eql:gh:`0e53e2ff`).\n* Implement ``create module if not exists`` (:eql:gh:`27924c10`.)\n* Allow indexes to be annotated (:eql:gh:`50d8809a`).\n* Remove explicit index names (:eql:gh:`e0f462c2`).\n* Enforce correct expression cardinality and type in link/property default\n  (:eql:gh:`2f6039fc` and :eql:gh:`9fa18afb`).\n\n\nIntrospection\n=============\n\nGeneric Describe\n----------------\n\nThe new :eql:stmt:`describe` introspection command can generate DDL,\nSDL, or a descriptive text summary of any schema object in |EdgeDB|.  A\nfew examples:\n\n.. code-block:: edgeql-repl\n\n    db> describe type Movie as ddl;\n    {\n      'CREATE TYPE default::Movie EXTENDING default::HasImage {\n        CREATE SINGLE PROPERTY avg_rating := (WITH\n          MODULE default\n        SELECT\n          math::mean(.<movie[is Review].rating)\n        );\n\n        ...\n      };'\n    }\n\n    db> describe type Movie as text verbose;\n    {\n      'type default::Movie extending default::HasImage {\n        index on (__subject__.image);\n\n        required single link __type__ -> schema::Type {\n          readonly := true;\n        };\n\n        required single property id -> std::uuid {\n          readonly := true;\n          constraint std::exclusive;\n        };\n\n        required single property image -> std::str;\n\n        ...\n      };'\n    }\n\n(Issue :eql:gh:`#790`.)\n\n\nOther Enhancements\n------------------\n\n* ``schema::bases`` and ``schema::ancestors`` are now ordered via the\n  ``@order`` link property (:eql:gh:`#854`).\n* Add ``schema::Module.builtin`` attribute (:eql:gh:`64f88a01`).\n\n\nREPL\n====\n\nIntrospection\n-------------\n\nThe REPL now recognizes a number of introspection commands:\n\n.. code-block::\n\n  (options: S = show system objects, I = case-sensitive match)\n  \\d[+] NAME               describe schema object\n  \\l                       list databases\n  \\lr[I] [PATTERN]         list roles\n  \\lm[I] [PATTERN]         list modules\n  \\lT[IS] [PATTERN]        list scalar types\n  \\lt[IS] [PATTERN]        list object types\n  \\la[IS+] [PATTERN]       list expression aliases\n  \\lc[I] [PATTERN]         list casts\n\nFor example:\n\n.. code-block:: edgeql-repl\n\n    db> \\lt\n    ------------------- Object Types -------------------\n     Name              | Extending\n    -------------------+--------------------------------\n     default::HasImage | std::Object\n     default::Movie    | default::HasImage, std::Object\n     default::Person   | default::HasImage, std::Object\n     default::Review   | std::Object\n     default::User     | default::HasImage, std::Object\n\n\n    db> \\d HasImage\n    abstract type default::HasImage {\n      required single link __type__ -> schema::Type {\n        readonly := true;\n      };\n      required single property id -> std::uuid {\n        readonly := true;\n      };\n      required single property image -> std::str;\n    };\n\n(Issue :eql:gh:`#179`.)\n\nAuto Limit\n----------\n\nThe REPL now automatically injects limits to user queries so that a simple\n``select Log`` does not fetch all data from the database.  Auto limits are only\nenabled in parts of the query that return visible data; auto limits are\ndisabled inside aggregate functions, so analytical queries work as expected.\n\nThe auto-limit can be disabled with a ``\\limit 0`` command, or the limit\ncan be changed with ``\\limit 42`` command.\n\n(Issue :eql:gh:`#846`.)\n\n\nServer\n======\n\nPostgres 12\n-----------\n\n|EdgeDB| is now based on PostgreSQL 12.\n\nOther Fixes and Enhancements\n----------------------------\n\n* Add an explicit database instance compatibility check (:eql:gh:`251517c0`).\n* Initial support for using a remote Postgres cluster as a backend\n  (:eql:gh:`b0db89b2`).\n* Protocol: prohibit tuples as query arguments (:eql:gh:`#745`).\n* Protocol: differentiate SASL message types (:eql:gh:`d52885c8`).\n* Protocol: Add \"Terminate\" message for graceful shutdown (:eql:gh:`d699352a`).\n* Protocol: use 32-bit length-prefixed strings everywhere.\n* Drop reliance on a custom PostgreSQL C extension.\n\n\nMisc\n====\n\n* Command-line tools now use ``-h`` for help; ``-H`` for hostname.\n  (:eql:gh:`#1039`).\n* ``edgedb`` subcommands were renamed to have dashes in their names instead\n  of spaces, e.g. ``edgedb create role`` became ``edgedb create-role``\n  (:eql:gh:`#1039`).\n* Rename the ``--pidfile`` argument of ``edgedb-server`` to ``--pidfile-dir``.\n  (:eql:gh:`#1093`).\n* Add command line arguments to ``edgedb-server`` for automatic temporary\n  cluster bootstrap to simplify CI (:eql:gh:`5161de72`).\n* Add developer tools for memory and performance profiling\n  (:eql:gh:`#1032`, :eql:gh:`#835`, and :eql:gh:`#858`).\n* Improve query compilation performance by ~30%.\n* Strictly type-annotate SQL and IR compilers, run ``mypy`` in\n  strict mode in CI for critical modules.\n* Upgrade to Python 3.8.\n"
  },
  {
    "path": "docs/resources/changelog/1_0_a3.rst",
    "content": ":orphan:\n\n.. _ref_changelog_alpha3:\n\n===========\n1.0 Alpha 3\n===========\n\nThis changelog summarizes new features and breaking changes in\n`EdgeDB 1.0 alpha 3 \"Proxima Centauri\"\n<https://www.edgedb.com/blog/edgedb-1-0-alpha-3-proxima-centauri>`_.\n\n\nCLI\n===\n\nThe EdgeDB command-line tools are now written in Rust. The tools are\na single binary now and can run on multiple different platforms,\nincluding Windows.\n\nA few more commands have been added to REPL:\n\n.. code-block::\n\n      \\dump FILENAME           dump current database into a file\n      \\restore FILENAME        restore the database from file into\n                               the current one\n      \\s, \\history             show history\n      \\e, \\edit [N]            spawn $EDITOR to edit history entry N\n                               then use the output as the input\n      \\set [OPTION [VALUE]]    show/change setting\n      \\set                     Show setting descriptions (without\n                               arguments)\n\nExisting introspection commands have been slightly updated to use a\ndifferent way of supplying command options. Commands now also have\nlong-form names that are more human-readable.\n\nAlso, there are a few settings that control the look and feel of the\nREPL (and can be changed via the ``\\set`` command:\n\n.. code-block::\n\n    expand-strings             Stop escaping newlines in quoted\n                               strings\n    implicit-properties        Print implicit properties of objects:\n                               id, type id\n    input-mode                 Set input mode. One of: vi, emacs\n    introspect-types           Print type names instead of `Object`\n                               in default output mode (may fail if\n                               schema is updated after enabling\n                               option)\n    limit                      Set implicit LIMIT. Defaults to 100,\n                               specify 0 to disable\n    output-mode                Set output mode. One of: json, json-\n                               elements, default, tab-separated\n    verbose-errors             Print all errors with maximum\n                               verbosity\n\n\nEdgeQL\n======\n\n* Add ``+=`` and ``-=`` operations for use in :eql:stmt:`update`\n  (:eql:gh:`#165`).\n* Distinguish ``required`` and ``optional`` query parameters\n  (:eql:gh:`#1352`, :eql:gh:`#1355`)\n* Allow tuple arrays in schema definitions (:eql:gh:`73125882`).\n* Get rid of ``schema::CompoundType`` in favor of ``is_compound_type``\n  property (:eql:gh:`c30c1c4f`).\n* Add :eql:type:`anytype` and :eql:type:`anytuple` as proper abstract\n  types to the schema (:eql:gh:`affb65de`).\n* ``schema::Constraint`` no longer has ``args``, use ``params`` instead\n  (:eql:gh:`14cec6f1`).\n* Make ``std::Object`` the implicit base type for all user-defined\n  object types only.\n* Make ``std::BaseObject`` the root type for all object types both\n  user-defined and system object types.\n* Accept ``1e100n`` format as a valid ``bigint`` constant rather than\n  ``decimal`` (:eql:gh:`#1372`)\n* Make self-referencing (recursive) aliases forbidden.\n* Fix dependency tracking affecting index creation (:eql:gh:`#1181`).\n* Fix derivation of link targets in certain cases of multiple\n  inheritance (:eql:gh:`52c6b2d4`).\n* Fix handling of ad-hoc tuples (:eql:gh:`#1255`).\n* Fix incorrect implicit limit injection in subqueries in computed\n  expressions (:eql:gh:`#1271`).\n* Computables cardinality must now be declared explicitly as\n  ``required``, ``single`` or ``multi``. The expression is validated\n  to be within the upper and lower limits implied by the declaration\n  (:eql:gh:`#1201`, :eql:gh:`#349`).\n* Prohibit database names longer than 63 characters (:eql:gh:`#1158`).\n* Prohibit duplicate elements in shapes (:eql:gh:`#1368`).\n* Implement :eql:stmt:`alter function` (:eql:gh:`#1433`).\n\n\nGraphQL\n=======\n\n* Reflect \"description\" annotations into GraphQL descriptions\n  (:eql:gh:`#1228`).\n* Change the auto-generated type name template to use \"\\_Type\" suffix\n  (:eql:gh:`#1175`).\n* Fix incorrect reflection of enums (:eql:gh:`#1227`).\n* Fix deeply nested insert issue (:eql:gh:`#1243`).\n\n\nStandard Library\n================\n\n* Add :eql:func:`sys::get_current_database`\n* Rename ``std::to_str()`` array-joining function to\n  :eql:func:`array_join`.\n* Rename ``std::to_array()`` string-splitting function to\n  :eql:func:`str_split`.\n\n\nServer\n======\n\n* Convert EdgeQL lexer (:eql:gh:`#1178`)\n* Convert GraphQL parser to Rust and perform constant extraction\n  (:eql:gh:`#1299`).\n* Constant extraction from EdgeQL queries (:eql:gh:`#1356`)\n* Make it possible to use different EdgeDB servers connected to the\n  same Postgres cluster (:eql:gh:`#1197`).\n* Protocol: implement ``JSON_ELEMENTS`` IO format for responses\n  (:eql:gh:`#1169`).\n\n\n\nMisc\n====\n\n* Add :ref:`cheatsheet <ref_cheatsheet_repl>` to documentation.\n* Strictly type-annotate the core schema module.\n* Clean-up and improve schema and introspection code to make the\n  internal APIs more consistent (:eql:gh:`#1408`).\n* Update the `edgedb-js <https://github.com/edgedb/edgedb-js>`_ driver\n  to v0.7.3.\n* Update the `edgedb-python <https://github.com/edgedb/edgedb-python>`_\n  driver to v0.8.0.\n"
  },
  {
    "path": "docs/resources/changelog/1_0_a4.rst",
    "content": ":orphan:\n\n.. _ref_changelog_alpha4:\n\n===========\n1.0 Alpha 4\n===========\n\nThis changelog summarizes new features and breaking changes in\n`EdgeDB 1.0 alpha 4 \"Barnard's Star\"\n<https://www.edgedb.com/blog/edgedb-1-0-alpha-4-barnard-s-star>`_.\n\n\nEdgeQL\n======\n\n* Add ``__std__`` alias for ``std`` module so that there's a way to refer\n  to the standard module which cannot be masked by a ``with std as\n  module foo`` construct (:eql:gh:`#1457`).\n* Allow explicit ``optional`` qualifier for links and properties in\n  DDL and SDL. In particular, use it in :eql:stmt:`describe` command\n  output. (:eql:gh:`#1342`).\n* Only show link properties on computed links that are aliases.\n* :eql:stmt:`describe` command now shows matches that are\n  potentially masked by the user-defined types or functions (:eql:gh:`#1439`).\n* Add ``describe roles`` and ``describe instance config`` to the\n  :eql:stmt:`describe` command.\n* Allow underscore in numeric literals and forbids float and decimal\n  constants to end in ``.`` (:eql:gh:`#920`).\n* Validate the ``required`` flag on computed links and properties\n  (:eql:gh:`#217`).\n* Forbid reference to link properties outside of a path expression\n  (:eql:gh:`#1512`).\n* Allow annotations to be renamed (:eql:gh:`#762`).\n* Initial implementation of the new migration syntax\n  (`RFC 1000 <migrations_>`_).\n\n\nCLI\n===\n\n* We have a new ``edgedb server`` group of commands that ships with\n  the default |EdgeDB| CLI. Check out the details in this `RFC 1001\n  <edbserver_>`_.\n* The ``edgedb`` REPL prompt now has a transaction indicator:\n\n  .. code-block:: edgeql-repl\n\n    edgedb> start transaction;\n    OK: START TRANSACTION\n    edgedb[T]>\n\n\nServer\n======\n\n* Remove reliance on Postgres table inheritance (:eql:gh:`#1468`)\n* The ``edgedb-server`` installed by official installers is now called\n  ``edgedb-server-1-alpha4``.\n\n\nMisc\n====\n\n* Rename ``fetch`` to ``query`` in the `edgedb-js\n  <https://github.com/edgedb/edgedb-js>`_ and the `edgedb-python\n  <https://github.com/edgedb/edgedb-python>`_ drivers.\n* Add connection pooling to `edgedb-js <https://github.com/edgedb/edgedb-js>`_\n  driver.\n* Update the `edgedb-js <https://github.com/edgedb/edgedb-js>`_ driver\n  to v0.8.0.\n* Update the `edgedb-python <https://github.com/edgedb/edgedb-python>`_\n  driver to v0.9.0.\n\n\n\n.. _migrations:\n    https://github.com/edgedb/rfcs/blob/master/text/1000-migrations.rst\n.. _edbserver:\n    https://github.com/edgedb/rfcs/blob/master/text/\n    1001-edgedb-server-control.rst\n"
  },
  {
    "path": "docs/resources/changelog/1_0_a5.rst",
    "content": ":orphan:\n\n.. _ref_changelog_alpha5:\n\n===========\n1.0 Alpha 5\n===========\n\nThis changelog summarizes new features and breaking changes in\n`EdgeDB 1.0 alpha 5 \"Luhman\" <https://www.edgedb.com/blog/edgedb-1-0-alpha-5-luhman>`_.\n\n\nEdgeQL\n======\n\n* Implement casts between JSON and enums, arrays and tuples\n  (:eql:gh:`#251`). For example, now there's a way to unpack JSON\n  input into a tuple, which can then be used to populate a new User\n  record:\n\n  .. code-block:: edgeql-repl\n\n      db> with\n      ...     data := <tuple<\n      ...        first_name: str,\n      ...        last_name: str,\n      ...        interests: array<str>\n      ...     >> <json>$input\n      ... insert User {\n      ...     first_name := data.first_name,\n      ...     last_name := data.last_name,\n      ...     interests := (\n      ...         select\n      ...             Interest\n      ...         filter\n      ...             .label in array_unpack(data.interests)\n      ...     )\n      ... };\n      Parameter <json>$input:\n      {\n        \"first_name\": \"Phil\",\n        \"last_name\": \"Emarg\",\n        \"interests\": [\"fishing\", \"skiing\"]\n      }\n\n* Allow constraints on tuple types (:eql:gh:`#1576`).\n* Allow constraints directly on object types in SDL (:eql:gh:`#1164`)\n* Disallow using :eql:constraint:`exclusive` on scalar types\n  (:eql:gh:`#1575`).\n* Proper implementation of ``set/drop owned``.\n* Fix issues with some ``for`` statements (:eql:gh:`#1594`).\n* Use fully-qualified names to disambiguate the expressions produced\n  by :eql:stmt:`describe` (:eql:gh:`#1254`).\n* Initial implementation of :eql:stmt:`insert ... unless conflict ...\n  else <insert>` (:eql:gh:`#1639`)\n* Implementation of more of the features of the new migration syntax\n  (`RFC 1000 <migrations_>`_).\n\n\nGraphQL\n=======\n\n* Allow several mutation operations in a single mutation query\n  (:eql:gh:`#1569`).\n* Reflect nested aliased types (:eql:gh:`#722`).\n* Enable sorting on non-trivial path (:eql:gh:`#1642`). Here's an\n  example of sorting movies by the director's last name and then by\n  the movie's title:\n\n  .. code-block:: graphql\n\n      {\n        Movie(\n          order: {\n            director: {last_name: {dir: ASC}},\n            title: {dir: ASC}\n          }\n        ) {\n          id\n          title\n        }\n      }\n\n* Add an ``exists`` filter operation (:eql:gh:`#1655`). Here's an\n  example of using it to get records with missing data:\n\n  .. code-block:: graphql\n\n      {\n        Movie(\n          filter: {director: {exists: false}}\n        ) {\n          id\n          title\n        }\n      }\n\n\nCLI\n===\n\n* Reworked auth setup via ``edgedb server init`` (`#91\n  <https://github.com/edgedb/edgedb-cli/issues/91>`_).\n* Initial support for the migrations CLI.\n* Add ``edgedb server status --all`` command to list all instances.\n\n\nBindings\n========\n\n* Add transaction :ref:`API <gel-js-api-transaction>` to JS binding\n  (`#61 <https://github.com/edgedb/edgedb-js/pull/61>`_). Here's an\n  example of using transactions:\n\n  .. code-block:: javascript\n\n    await con.transaction(async () => {\n        await con.execute(`\n            insert Example {\n                name := 'Test Transaction 1'\n            };\n        `);\n        await con.execute(\"select 1 / 0;\");\n    });\n\n    // nested transactions are supported\n    // and handle save points\n    await con.transaction(async () => {\n\n        // nested transaction\n        await con.transaction(async () => {\n            await con.execute(`\n                insert Example {\n                    name := 'Test Transaction 2'\n                };\n            `);\n        });\n    });\n\n* Add support of connecting to instance by a name (`#112\n  <https://github.com/edgedb/edgedb-python/pull/113>`_).\n* Update the `edgedb-js <https://github.com/edgedb/edgedb-js>`_ driver\n  to v0.9.0.\n* Update the `edgedb-python <https://github.com/edgedb/edgedb-python>`_\n  driver to v0.10.0.\n\n\n\n.. _migrations:\n    https://github.com/edgedb/rfcs/blob/master/text/1000-migrations.rst\n"
  },
  {
    "path": "docs/resources/changelog/1_0_a6.rst",
    "content": ":orphan:\n\n.. _ref_changelog_alpha6:\n\n===========\n1.0 Alpha 6\n===========\n\nThis changelog summarizes new features and breaking changes in\n|EdgeDB| 1.0 alpha 6 \"Wolf\".\n\n\nEdgeQL\n======\n\n* Introduce new enum definition syntax (:eql:gh:`#1843`).\n\n  Instead of using strings, the enums are now defined using identifiers:\n\n  .. code-block:: edgeql\n\n    create scalar type schema::TypeModifier\n        extending enum<SetOfType, OptionalType, SingletonType>;\n\n* Rename built-in schema enums to use CamelCase names (:eql:gh:`#1843`).\n* Rename string handling functions to use ``start`` and ``end`` in\n  their name (:eql:gh:`#1752`):\n\n  - Deprecate :eql:func:`str_lpad` in favor of :eql:func:`str_pad_start`\n  - Deprecate :eql:func:`str_rpad` in favor of :eql:func:`str_pad_end`\n  - Deprecate :eql:func:`str_ltrim` in favor of :eql:func:`str_trim_start`\n  - Deprecate :eql:func:`str_rtrim` in favor of :eql:func:`str_trim_end`\n\n* Improve :eql:stmt:`update` functionality (:eql:gh:`#1746`).\n* Forbid usage of DML in some special cases (:eql:gh:`#1726`).\n* Add tracking of DML inside function bodies (:eql:gh:`#1741`).\n* Allow functions with non-statement bodies (:eql:gh:`#1723`).\n* Allow the usage of :eql:op:`exists` in constraints (:eql:gh:`#1750`).\n* Allow partial paths in object constraints (:eql:gh:`#1704`)\n* Allow trailing commas in collection types (:eql:gh:`#1749`).\n* Fix :eql:stmt:`insert ... unless conflict ... else <insert>` when\n  combined with shape (:eql:gh:`#1743`).\n* Fix how :eql:stmt:`for` statement correlates values (:eql:gh:`#1776`).\n* Fix handling of collections of newly created types in SDL (:eql:gh:`#1730`).\n* Fix handling of function definitions in SDL (:eql:gh:`#1649`).\n* Fix interactions of ``set of`` and ``optional`` arguments (:eql:gh:`#1640`).\n* Implementation of more of the features of the new migration syntax\n  (`RFC 1000 <migrations_>`_).\n\n\nCommand-Line Tools\n==================\n\n* Require instance name for most ``edgedb server`` commands.\n* Add version check to show a warning when tools should be updated\n  (`#158 <https://github.com/edgedb/edgedb-cli/pull/158>`_).\n* Add edgedb self-upgrade command (`#159\n  <https://github.com/edgedb/edgedb-cli/pull/159>`_).\n* Add support for installing server using Docker (``edgedb server\n  install --method=docker``)\n\n\nServer Command-Line\n===================\n\n* Stop treating \"edgedb\" as special (:eql:gh:`#1729`)\n* Improve database initialization (:eql:gh:`#1755`).\n* Import setuptools before distutils in setup.py (:eql:gh:`#1734`).\n\n\nBindings\n========\n\n* Switch UUID decoding from a rich object to a string (\n  `#72 <https://github.com/edgedb/edgedb-js/pull/72>`_).\n* Add ``EdgeDBDateTimeCodec`` for handling various datetime scalars\n  (`#68 <https://github.com/edgedb/edgedb-js/pull/68>`_).\n* Update the `edgedb-js <https://github.com/edgedb/edgedb-js>`_ driver\n  to 0.11.0.\n* Update the `edgedb-python <https://github.com/edgedb/edgedb-python>`_ driver\n  to 0.11.0.\n\n\n.. _migrations:\n    https://github.com/edgedb/rfcs/blob/master/text/1000-migrations.rst\n"
  },
  {
    "path": "docs/resources/changelog/1_0_a7.rst",
    "content": ":orphan:\n\n.. _ref_changelog_alpha7:\n\n===========\n1.0 Alpha 7\n===========\n\nThis changelog summarizes new features and breaking changes in\nEdgeDB 1.0 alpha 7 \"Lalande\".\n\n\nEdgeQL\n======\n\n* Add support for per-database configuration by adding a new\n  :eql:stmt:`current database <configure>` configuration scope\n  (:eql:gh:`#1867`).\n\n  This allows to make changes that are more broad\n  that :eql:stmt:`session <configure>` scope and less broad that\n  :eql:stmt:`system <configure>`:\n\n  .. code-block:: edgeql\n\n    configure current database set query_work_mem := '4MB';\n\n* Add support for altering :eql:type:`enums <std::enum>` in order to\n  add new labels (:eql:gh:`#1956`).\n\n  .. code-block:: edgeql-repl\n\n    db> create scalar type Color\n    ...     extending enum<Red, Green, Blue>;\n    OK: CREATE\n    db> alter scalar type Color\n    ...     extending enum<Red, Green, Blue, Magic>;\n    OK: ALTER\n\n* Update :eql:type:`std::decimal` and :eql:type:`std::bigint` criteria\n  to be sensitive to the presence of ``.`` so that ``123.0e100n`` is a\n  decimal, but ``123e100n`` is a bigint (:eql:gh:`#1804`).\n\n* Functions can now return a set of tuples (:eql:gh:`#2010`).\n\n  .. code-block:: edgeql-repl\n\n    db> create function enumerate_letters(word: str)\n    ...     -> set of tuple<int64, str>\n    ...     using (\n    ...         enumerate(\n    ...             array_unpack(\n    ...                 str_split(word, '')))\n    ...     );\n    OK: CREATE\n    db> select enumerate_letters('hello');\n    {(0, 'h'), (1, 'e'), (2, 'l'), (3, 'l'), (4, 'o')}\n\n* Functions can no longer share a shortname with types in order to\n  avoid name resolution issues (:eql:gh:`#1465`).\n* Forbid taking implicit cross products with volatile operations to\n  avoid unintuitive behavior (:eql:gh:`#1784`).\n\n  .. code-block:: edgeql-repl\n\n    db> select ({1, 2}, random());\n    error: can not take cross product of volatile operation\n      ┌─ query:1:17\n      │\n    1 │ select ({1, 2}, random());\n      │                 ^^^^^^^^^ error\n    db> for x in {1, 2} union (x, random());\n    {(1, 0.25724045818607166), (2, 0.7268530965023459)}\n\n* Forbid scalar types from having more than one concrete base\n  (:eql:gh:`#1790`).\n* Forbid partial path expressions in :eql:stmt:`limit <select>`/\n  :eql:stmt:`offset <select>` clauses (:eql:gh:`#1919`).\n* Forbid changing cardinality via inheritance (:eql:gh:`#1772`).\n* Remove legacy unused ``.>`` token (:eql:gh:`#1648`).\n* Fix cardinality inference on operators (:eql:gh:`#2001`).\n\n\nMigrations\n==========\n\nWe've made a lot of progress in implementing features of the `RFC 1000\n<migrations_>`_ migrations, although this is still a feature under\ndevelopment. Some of the works can be broadly categorized as overall\nimprovement of the proposed migration DDL and the granularity of the\ncontrol the user has over these proposed changes. More specifically\nwe've made a lot of improvements in migrations that alter or remove\nthings from the schema.\n\nHere's an example of creating a schema with a type that has a property\nwith a default value:\n\n.. code-block:: edgeql-repl\n\n    db> start migration to {\n    ...   module default {\n    ...     type Foo {\n    ...       property val -> str {\n    ...         default := 'n/a'\n    ...       }\n    ...     }\n    ...   }\n    ... };\n\nWe use :eql:stmt:`describe current migration as json <describe current\nmigration>` to see what |EdgeDB| is proposing. The JSON format makes it\neasier to potentially integrate this with other tools. For this\nexample it's worth turning on ``json`` output mode for edgedb REPL:\n\n.. code-block:: edgeql-repl\n\n    db> \\set output-mode json\n    db[tx]> describe current migration as json;\n    [\n      {\n        \"complete\": false,\n        \"confirmed\": [],\n        \"parent\": \"m16wif5skjyqd6dbp5uwa67qrgw422qcwa3vctx77z7r34yx5mbigq\",\n        \"proposed\": {\n          \"confidence\": 1.0,\n          \"operation_id\": \"CREATE TYPE default::Foo\",\n          \"prompt\": \"did you create object type 'default::Foo'?\",\n          \"statements\": [{\"text\": \"CREATE TYPE default::Foo {\\n\n          CREATE OPTIONAL SINGLE PROPERTY val -> std::str {\\n\n          SET default := 'n/a';\\n    };\\n};\"}]\n        }\n      }\n    ]\n\nSince proposed statements look OK, we can go ahead and just apply the\nwhole migration.\n\n.. code-block:: edgeql-repl\n\n    db[tx]> populate migration;\n    OK: POPULATE MIGRATION\n    db[tx]> commit migration;\n    OK: COMMIT MIGRATION\n\nNow, let's remove that ``default``, after all the property is optional.\n\n.. code-block:: edgeql-repl\n\n    db> start migration to {\n    ...   module default {\n    ...     type Foo {\n    ...       property val -> str;\n    ...     }\n    ...   }\n    ... };\n    db[tx]> describe current migration as json;\n    [\n      {\n        \"complete\": false,\n        \"confirmed\": [],\n        \"parent\": \"initial\",\n        \"proposed\": {\n          \"confidence\": 0.9956623333333332,\n          \"operation_id\": \"ALTER TYPE default::Foo\",\n          \"prompt\": \"did you alter object type 'default::Foo'?\",\n          \"statements\": [{\"text\": \"ALTER TYPE default::Foo {\\n\n          ALTER PROPERTY val {\\n        DROP default;\\n    };\\n};\"}]\n        }\n      }\n    ]\n\nThe proposed statements will ``drop default`` for our property, so all\nseems to be in order and we can apply this migration, too, using\n:eql:stmt:`populate migration` and :eql:stmt:`commit migration`.\n\nWe're currently working on a CLI tool for managing migrations more\ngracefully and without the need for the user to rely on these\nlow-level commands (like``start migration`` or ``describe current\nmigration as json``). The migration tool is going to use these\ncommands behind the scenes, though.\n\nWe've also made improvements to the following migration features:\n\n* Better overall dependency tracking to make sure that migration to\n  the new state can be resolved and produces valid command sequence.\n* Type, index and alias renaming while keeping track of affected\n  expressions to make sure they don't become invalid (:eql:gh:`#1841`)\n* Function renaming (:eql:gh:`#1971`)\n* Moving a type between modules (:eql:gh:`#1890`).\n* Changing base types and changing where constraints are defined\n  (:eql:gh:`#1996`).\n\n\nCommand-Line Tools\n==================\n\n* Default user and default database are now simply ``edgedb`` and no\n  longer named after the system user.\n* Add ``--connect-timeout`` to control how long to wait for |EdgeDB|\n  response (`#191 <https://github.com/edgedb/edgedb-cli/pull/191>`_).\n* Add ``--dsn`` as a connection option (`#176\n  <https://github.com/edgedb/edgedb-cli/issues/176>`_).\n* Add ``migration-log`` command to view applied migrations (`#200\n  <https://github.com/edgedb/edgedb-cli/pull/200>`_).\n* Non-interactive error messages are prefixed by ``edgedb error:\n  ...``, to quickly spot which tool has errored in scripts.\n* Improve accuracy of syntax error reporting in REPL (:eql:gh:`#1959`).\n* REPL now supports full range of datetime values (`#192\n  <https://github.com/edgedb/edgedb-cli/pull/192>`_).\n* ``\\lt`` in REPL doesn't show implicit internal types (unions and\n  intersections) (`#169 <https://github.com/edgedb/edgedb-cli/issues/169>`_).\n* Remove ``\\set introspect-types`` in REPL and show typenames by\n  default.\n\n\nServer Command-Line\n===================\n\n* Make ``edgedb server install`` friendlier on linuxes without systemd\n  allowing foreground run (`#171\n  <https://github.com/edgedb/edgedb-cli/pull/171>`_).\n* When installing server ``DEBIAN_FRONTEND`` is now ``noninteractive`` by\n  default and is overridable (`#188\n  <https://github.com/edgedb/edgedb-cli/pull/188>`_).\n* Add ``edgedb server logs`` (`#172\n  <https://github.com/edgedb/edgedb-cli/pull/172>`_).\n* Add ``edgedb server info`` command.\n* Deprecate ``--default-database`` and ``--default-database-user``\n  (:eql:gh:`#1879`).\n\n\nBindings\n========\n\nWe now have an improved spec for client API (`RFC 1004 <robust_>`_).\nRolling out the support for the full spec will be done in the next\nrelease, but some implementation work has already started.\n\n* Move request methods into Executor interface (`#76\n  <https://github.com/edgedb/edgedb-js/pull/76>`_) as part of the `RFC\n  1004 <robust_>`_ changes.\n* Update the `edgedb-python <https://github.com/edgedb/edgedb-python>`_ driver\n  to 0.12.0.\n\n\n.. _robust:\n    https://github.com/edgedb/rfcs/blob/master/text/1004-transactions-api.rst\n\n.. _migrations:\n    https://github.com/edgedb/rfcs/blob/master/text/1000-migrations.rst\n"
  },
  {
    "path": "docs/resources/changelog/1_0_b1.rst",
    "content": ":orphan:\n\n.. _ref_changelog_beta1:\n\n==========\n1.0 Beta 1\n==========\n\nThis changelog summarizes new features and breaking changes in\n|EdgeDB| 1.0 beta 1 \"Sirius\".\n\n\nMigrations\n==========\n\nThis Beta release features the fully implemented `RFC 1000\n<migrations_>`_ for the first time. It is now possible to manage\nmigrations from the CLI, by supplying the new schema file and\nfollowing interactive prompts.\n\nLet's say we start with the following schema for a simple chat app:\n\n.. code-block:: sdl\n\n    module default {\n        type User {\n            required property name -> str;\n            required property email -> str;\n            required property password_hash -> str;\n        }\n\n        type Message {\n            required link author -> User;\n            required property body -> str;\n            required property timestamp -> datetime {\n                default := datetime_current()\n            }\n        }\n    };\n\nWe create a directory ``app_schema`` and write the above into a\n``app_schema/schema.esdl`` file inside it. Then we can initialize our\nproject database by running :ref:`edgedb create-migration\n<ref_cli_gel_migration_create>`:\n\n.. code-block:: bash\n\n    $ gel -I chatapp create-migration --schema-dir app_schema\n    did you create object type 'default::User'? [y,n,l,c,b,s,q,?]\n    ?\n\n    y - confirm the prompt, use the DDL statements\n    n - reject the prompt\n    l - list the DDL statements associated with prompt\n    c - list already confirmed EdgeQL statements\n    b - revert back to previous save point, perhaps previous question\n    s - stop and save changes (splits migration into multiple)\n    q - quit without saving changes\n    h or ? - print help\n    did you create object type 'default::User'? [y,n,l,c,b,s,q,?]\n    y\n    did you create object type 'default::Message'? [y,n,l,c,b,s,q,?]\n    y\n    Created app_schema/migrations/00001.edgeql, id:\n    m1ufwaxcqiwcq3ttcujnxv6f3jewhfrywc442z6gjk3sm3e5fgyr4q\n\nThis creates the first migration file\n``app_schema/migrations/00001.edgeql``. After reviewing it to make\nsure everything is in order, we can apply the migration with the\nfollowing command:\n\n.. code-block:: bash\n\n    $ gel -I chatapp migrate --schema-dir app_schema\n    Applied m1ufwaxcqiwcq3ttcujnxv6f3jewhfrywc442z6gjk3sm3e5fgyr4q\n    (00001.edgeql)\n\nIn the course of implementing our app we decide to add more features,\nsuch as a friends list and multiple chat channels, so we alter our\nschema to be:\n\n.. code-block:: sdl\n\n    module default {\n        type User {\n            required property name -> str;\n            required property email -> str;\n            required property password_hash -> str;\n\n            multi link friends -> User;\n        }\n\n        type Message {\n            required link author -> User;\n            required property body -> str;\n            required property timestamp -> datetime {\n                default := datetime_current()\n            }\n\n            link channel -> Channel;\n        }\n\n        type Channel {\n            required property title -> str;\n            property description -> str;\n        }\n    };\n\nAnd we apply the changes by using :ref:`edgedb\ncreate-migration <ref_cli_gel_migration_create>` and :ref:`edgedb\nmigrate <ref_cli_gel_migrate>` commands again:\n\n.. code-block:: bash\n\n    $ gel -I chatapp create-migration --schema-dir app_schema\n    did you create object type 'default::Channel'? [y,n,l,c,b,s,q,?]\n    y\n    did you create link 'channel' of object type 'default::Message'?\n    [y,n,l,c,b,s,q,?]\n    y\n    did you create link 'friends' of object type 'default::User'?\n    [y,n,l,c,b,s,q,?]\n    y\n    Created app_schema/migrations/00002.edgeql, id:\n    m1grkbj7z3fwvj6qe7ib72xdc6urj6ih5aynx3ammlrunh6tfefnaa\n    $ gel -I chatapp migrate --schema-dir app_schema\n    Applied m1grkbj7z3fwvj6qe7ib72xdc6urj6ih5aynx3ammlrunh6tfefnaa\n    (00002.edgeql)\n\nAt this point we may want to actually create a default channel \"Main\"\nand make the ``channel`` link required. So we alter the schema to make\nthe link required and run :ref:`edgedb create-migration\n<ref_cli_gel_migration_create>` again:\n\n.. code-block:: bash\n\n    $ gel -I chatapp create-migration --schema-dir app_schema\n    did you make link 'channel' of object type 'default::Message'\n    required? [y,n,l,c,b,s,q,?]\n    y\n    Please specify an expression to populate existing objects in\n    order to make link 'channel' required:\n    fill_expr> select Channel filter .title = 'Main' limit 1\n    Created app_schema/migrations/00003.edgeql, id:\n    m1ur35mvstn5wafse2kqwmjy4but3l7nigh4cqktxy6kt2j2wuz65a\n\nHowever, before applying this migration we also add the line ``insert\ndefault::Channel {title := 'Main'};`` at the beginning of the\nmigration block in the ``app_schema/migrations/00003.edgeql`` file.\nNow we can actually apply the changes:\n\n.. code-block:: bash\n\n    $ gel -I chatapp migrate --schema-dir app_schema\n    edgedb error: could not read migrations in app_schema/migrations:\n    could not read migration file app_schema/migrations/00003.edgeql:\n    migration name should be `\n    m1jmrmawu4uty53clhbat7nvzjbogexyarh2zue6w6ind2kpfalwva` but\n    `m1ur35mvstn5wafse2kqwmjy4but3l7nigh4cqktxy6kt2j2wuz65a` is used\n    instead.\n    Migration names are computed from the hash of the migration\n    contents. To proceed you must fix the statement to read as:\n      CREATE MIGRATION\n      m1jmrmawu4uty53clhbat7nvzjbogexyarh2zue6w6ind2kpfalwva ONTO ...\n    if this migration is not applied to any database. Alternatively,\n    revert the changes to the file.\n\nUh-oh! The migration failed, but the error message actually explains\nthat we need to adjust the migration hash in order to proceed and even\nsupplies us with the new hash. After adjusting the migration file, we\ncan now apply it:\n\n.. code-block:: bash\n\n    $ gel -I chatapp migrate --schema-dir app_schema\n    Applied m1jmrmawu4uty53clhbat7nvzjbogexyarh2zue6w6ind2kpfalwva\n    (00003.edgeql)\n\nSo let's make a minor tweak by renaming the ``friends`` link into\n``circle``. After updating our ``app_schema/schema.esdl`` file we can\napply the changes:\n\n.. code-block:: bash\n\n    $ gel -I chatapp create-migration --schema-dir app_schema\n    did you rename link 'friends' of object type 'default::User' to\n    'circle'? [y,n,l,c,b,s,q,?]\n    y\n    Created app_schema/migrations/00004.edgeql, id:\n    m1lh5julmw2msveqrchwly4qrbpyiof3hevze35d3x35ydrz3fsv3a\n    $ gel -I chatapp migrate --schema-dir app_schema\n    Applied m1lh5julmw2msveqrchwly4qrbpyiof3hevze35d3x35ydrz3fsv3a\n    (00004.edgeql)\n\nThe above example shows some of the interactions with the EdgeDB\nmigration management tools. We will keep improving the inference\nengine that guides the prompts of :ref:`edgedb create-migration\n<ref_cli_gel_migration_create>`. However, if the suggestion engine\nfails to provide a perfect fit, the option of adjusting the migration\nfile is always available.\n\n\nEdgeQL\n======\n\n* Deprecate ``Port`` and replace it with a more general\n  :ref:`extension <ref_datamodel_extensions>` mechanism\n  (:eql:gh:`#2228`).\n* Limit :eql:type:`datetime`, :eql:type:`cal::local_datetime` and\n  :eql:type:`cal::local_date` to the 1-9999 year range\n  (:eql:gh:`#2252`).\n* Make the format of :eql:type:`duration` less ambiguous by\n  restricting the usage of ``-`` sign (:eql:gh:`#2229`).\n* Record non-DDL commands during migrations instead of executing them\n  immediately as per `RFC 1000 <migrations_>`_ (:eql:gh:`#2138`).\n* Add more details to the DDL command status (:eql:gh:`#2138`).\n\n  .. code-block:: edgeql-repl\n\n    db> create type Foo;\n    OK: CREATE TYPE\n    db> create function foo() -> bool\n    ... using (select random() > 0.5);\n    OK: CREATE FUNCTION\n\n* Stop using ``drop`` to change field value, introduce ``reset`` and\n  ``set`` syntax to do that (:eql:gh:`#2031`).\n\n  .. code-block:: edgeql\n\n    alter type Foo {\n        alter property a {\n            reset default;\n        }\n    };\n\n* ``alter ... set type`` now requires an explicit conversion\n  expression specified in the ``using`` clause, if the new type is not\n  assignment-castable from the old type (:eql:gh:`#2115`).\n\n  .. code-block:: edgeql-repl\n\n    db> create type Foo {\n    ...     create property bar -> int64\n    ... };\n    OK: CREATE TYPE\n    db> insert Foo {bar := 3};\n    {default::Foo {id: efcffce4-6471-11eb-8be5-ff6b1f4c46ee}}\n    db> alter type Foo alter property bar {\n    ...    set type str using (<str>.bar ++ '!')\n    ... };\n    OK: ALTER TYPE\n    db> select Foo {bar};\n    {default::Foo {bar: '3!'}}\n\n* Add a ``using`` clause for ``set required`` so that en expression to\n  fill in missing values can be specified (:eql:gh:`#2130`).\n\n  .. code-block:: edgeql-repl\n\n    db> create type Foo {\n    ...     create property bar -> str\n    ... };\n    OK: CREATE TYPE\n    db> insert Foo;\n    {default::Foo {id: efcffce4-6471-11eb-8be5-ff6b1f4c46ee}}\n    db> select Foo {bar};\n    {default::Foo {bar: {}}}\n    db> alter type Foo alter property bar {\n    ...    set required using ('init')\n    ... };\n    OK: ALTER TYPE\n    db> select Foo {bar};\n    {default::Foo {bar: 'init'}}\n\n* Expose link/property ``readonly`` aspect in introspection schema\n  (:eql:gh:`#2147`).\n* Drop ``is_`` prefixes from boolean fields in introspection schema.\n  The old field names are kept for backwards compatibility to be\n  deprecated later (:eql:gh:`#1793`).\n* Add support for computed link properties (:eql:gh:`#2067`).\n* Infer and validate volatility for functions (:eql:gh:`#1937`).\n* Allow trailing commas in functions (:eql:gh:`#1462`).\n* Fix handling of implicit path prefix in the ``else`` part of\n  ``unless conflict`` so that it properly refers to existing object\n  (:eql:gh:`#2091`).\n* Fix issues with :eql:func:`enumerate` when applied to objects\n  (:eql:gh:`#1815`) and results of function calls (:eql:gh:`#1816`).\n* Fix ``drop property`` for ``multi`` properties (:eql:gh:`#2059`).\n* Make sure computed links and properties don't appear in dump\n  (:eql:gh:`#2057`).\n* Fix accessing links on objects that come from functions and other\n  sources that aren't simple paths (:eql:gh:`#1887`).\n\n\nCommand-Line Tools\n==================\n\n* Add ``create-migration`` command.\n\n\nBindings\n========\n\n* Release `edgedb-go <https://github.com/edgedb/edgedb-go>`_ driver.\n* Update the `edgedb-python <https://github.com/edgedb/edgedb-python>`_ driver\n  to v0.13.0.\n* Update the `edgedb-js <https://github.com/edgedb/edgedb-js>`_ driver\n  to v0.13.0.\n* Implement `RFC 1004 <robust_>`_ features for Python and JavaScript drivers.\n\n  - Add ``retrying_transaction()`` method for automatically retrying\n    transactions (``retryingTransaction()`` in JavaScript,\n    ``RetryingTx()`` in Go):\n\n    .. code-block:: python\n\n        for tx in con.retrying_transaction():\n            with tx:\n                tx.execute('''\n                    insert Message {\n                        body := 'Hello'\n                    };\n                ''')\n\n  - Add ``raw_transaction()`` method and deprecate ``transaction()`` for\n    single-use transactions that will not be automatically retried\n    (``rawTransaction()`` in JavaScript, ``RawTx()`` in Go):\n\n    .. code-block:: python\n\n        tr = con.raw_transaction()\n        with tr as with_tr:\n            with_tr.execute('''\n                insert Message {\n                    body := 'Hello'\n                };\n            ''')\n\n  - Add ``wait_until_available`` (measured in seconds) configuration\n    parameter (``waitUntilAvailable`` in JavaScript):\n\n    .. code-block:: python\n\n        con = edgedb.connect(\n            user='edgedeb',\n            wait_until_available=10\n        )\n\n.. _robust:\n    https://github.com/edgedb/rfcs/blob/master/text/1004-transactions-api.rst\n\n.. _migrations:\n    https://github.com/edgedb/rfcs/blob/master/text/1000-migrations.rst\n"
  },
  {
    "path": "docs/resources/changelog/1_0_b2.rst",
    "content": ":orphan:\n\n.. _ref_changelog_beta2:\n\n==========\n1.0 Beta 2\n==========\n\nThis changelog summarizes new features and breaking changes in\n|EdgeDB| 1.0 beta 2 \"Luyten\".\n\n\nMigrations\n==========\n\nWe've been working a lot on our migrations tools with the goal of\nimproving the developer experience. Here's a highlight of recent\nchanges and fixes:\n\n* Fix :eql:type:`sequence` default values getting lost after some\n  migrations (:eql:gh:`#2389`).\n* Fix moving indexes around the type hierarchy (:eql:gh:`#2380`).\n* Fix issues with renaming (:eql:gh:`#2353`).\n* Fix an issue using a symbol defined in a ``with`` block in SDL\n  (:eql:gh:`#2320`).\n* Fix multiple issues with migrations to an empty schema\n  (:eql:gh:`#2296`).\n* Make it possible to add new values to :eql:type:`enums <enum>` in\n  migrations (:eql:gh:`#2328`).\n* Fix tuples in ``multi`` properties and :eql:op:`in` expressions\n  (:eql:gh:`#2255`).\n* Fix SDL handling of types with two or more indexes\n  (:eql:gh:`#2301`).\n* Fix a regression with array aliasing that made array aliases\n  indistinguishable (:eql:gh:`#2287`).\n* Fix a number of issues with constraints and provide\n  better context information for constraint definition errors\n  (:eql:gh:`#1370`, :eql:gh:`#2250`, :eql:gh:`#2305`, :eql:gh:`#2307`,\n  :eql:gh:`#2311`, :eql:gh:`#2410`).\n* Fix ``drop owned`` on links and properties with defaults\n  (:eql:gh:`#2306`).\n* Fix ``set type`` on links and properties with constraints\n  (:eql:gh:`#2309`).\n\n\nEdgeQL\n======\n\n* Make sure sequence state gets included in dumps (:eql:gh:`#2441`).\n* Implement functions to explicitly advance or reset a sequence value\n  (:eql:gh:`#2508`).\n\n  :eql:func:`sequence_next` returns the next value for the specified\n  sequence type.\n\n  :eql:func:`sequence_reset` resets the *current* value of the\n  specified sequence, the next call to ``sequence_next`` will\n  return the next value in sequence.\n\n* Drop the deprecated ``Port``. The more general\n  :ref:`extension <ref_datamodel_extensions>` mechanism introduced in\n  |EdgeDB| 1.0 beta 1 should be used (:eql:gh:`#2262`).\n* Reduce the maximum length for names of databases and roles to 51\n  characters (:eql:gh:`#2465`).\n* Enable ``br`` (or ``rb``) as a valid bytes literal prefix\n  (:eql:gh:`#2332`).\n* Enable ``describe schema as sdl`` (:eql:gh:`#2481`).\n* Support ``unless conflict on`` with two or more properties or links\n  (:eql:gh:`#1939`).\n\n  This clause allows performing an alternative query when a conflict\n  due to a constraint occurs during an ``insert``:\n\n  .. code-block:: edgeql\n\n    insert Person { name := \"Alice\" }\n    unless conflict on .name  # If a Person with this name exists,\n    else (select Person)      # select that existing Person instead.\n\n* Make :eql:func:`min` and :eql:func:`max` work more consistently\n  across all supported types (:eql:gh:`#1920`).\n* Improve cardinality inference (:eql:gh:`#2097`).\n* Disallow use of ``Volatile`` functions in schema-defined computed\n  expressions (:eql:gh:`#2467`).\n* Fix handling of collection types of non-builtin scalars in dumps\n  (:eql:gh:`#2349`).\n* Fix inconsistent handling of ``{}`` by the :eql:op:`if <if..else>`\n  operator (:eql:gh:`#2460`).\n* Fix duplicate values appearing when using :eql:stmt:`+= <update>`\n  (:eql:gh:`#2455`).\n* Fix an issue with empty sets (i.e. ``{}``) inside set literals\n  (:eql:gh:`#2154`).\n* Fix backlinks when multiple types with the same link name exist\n  (:eql:gh:`#2360`).\n* Fix :eql:op:`distinct` on empty and nested tuples (:eql:gh:`#2333`).\n* Fix some serialization issues of shapes inside arrays and tuples\n  (:eql:gh:`#1818`).\n* Make sure :eql:stmt:`delete` also applies to all sub-types of the\n  selected type (:eql:gh:`#2265`).\n\nGraphQL\n=======\n\n* Fix usage of :eql:type:`enums <enum>` as input variables in GraphQL\n  (:eql:gh:`#2415`).\n* Fix querying ``BaseObject`` via GraphQL (:eql:gh:`#2214`).\n\n\nCommand-Line Tools\n==================\n\nWe've added ``edgedb project init`` command to help manage |EdgeDB|\ncredentials for your project. Running this in a new project directory\nwill setup an EdgeDB instance, create a schema and migrations\ndirectory and link the credentials for that instance to the project\ndirectory.\n\n.. code-block:: bash\n\n    $ gel project init\n    No `edgedb.toml` found in `/home/username/dev/hw` or above\n    Do you want to initialize a new project? [Y/n]\n    > Y\n    Specify the name of EdgeDB instance to use with this project\n    [default: myproject]:\n    > myproject\n    Type a number to select an option:\n    How would you like to run EdgeDB for this project?\n    1. Local (docker)\n    > 1\n    Checking EdgeDB versions...\n    Specify the version of EdgeDB to use with this project\n    [default: 1-beta2]:\n    > 1-beta2\n    ┌─────────────────────┬──────────────────────────────────────────┐\n    │ Project directory   │ /home/username/dev/myproject             │\n    │ Project config      │ /home/username/dev/myproject/edgedb.toml │\n    │ Schema dir (empty)  │ /home/username/dev/myproject/dbschema    │\n    │ Installation method │ Docker Container                         │\n    │ Version             │ 1-beta2-c23b7a1                          │\n    │ Instance name       │ myproject                                │\n    └─────────────────────┴──────────────────────────────────────────┘\n    Initializing EdgeDB instance...\n    e740091d317687d1628f96e43a77ec02f098de68df3b8b95b3bd987f7c30080d\n    Applying migrations...\n    Everything is up to date. Revision initial\n    Project initialialized.\n    To connect to myproject, just run `edgedb`\n\nAs the last line indicates it is no longer necessary to supply the\ninstance name explicitly to connect to the project instance, just\n``edgedb`` will do the trick. This is also true for using any of\nEdgeDB's client libraries, the instance name is no longer required.\n\nExisting projects can be converted to use this feature by simply\nrunning ``edgedb project init`` in the existing project's directory:\n\n.. code-block:: bash\n\n    $ gel project init\n    No `edgedb.toml` found in `/home/username/dev/myproject` or above\n    Do you want to initialize a new project? [Y/n]\n    > Y\n    Specify the name of EdgeDB instance to use with this project\n    [default: myproject_uuyg1cr]:\n    > myproject\n    Do you want to use existing instance \"myproject\" for the project?\n    [y/n]\n    > y\n    Applying migrations...\n    Everything is up to date.\n    Revision m1lsdptp5qk4sway5vc6ttknwignhm34xncyxwrus2fygnj6nuo7ra\n    Project initialialized.\n    To connect to myproject, just run `edgedb`\n\n\nBindings\n========\n\n* Implement `RFC 1004 <robust_>`_ features for `edgedb-go\n  <https://github.com/edgedb/edgedb-go>`_ driver.\n* Update the `edgedb-python\n  <https://github.com/edgedb/edgedb-python>`_ driver to v0.14.0.\n* Update the `edgedb-js <https://github.com/edgedb/edgedb-js>`_ driver\n  to v0.14.0.\n* Release `Deno <https://github.com/edgedb/edgedb-deno>`_ driver.\n* Implement ``with_transaction_options`` and ``with_retry_options``\n  from `RFC 1004 <robust_>`_ for `edgedb-python\n  <https://github.com/edgedb/edgedb-python>`_ and `edgedb-js\n  <https://github.com/edgedb/edgedb-js>`_.\n\n  These methods on the connection object allow obtaining a new\n  connection with modified options.\n\n.. _robust:\n    https://github.com/edgedb/rfcs/blob/master/text/1004-transactions-api.rst\n"
  },
  {
    "path": "docs/resources/changelog/1_0_b3.rst",
    "content": ":orphan:\n\n.. _ref_changelog_beta3:\n\n==========\n1.0 Beta 3\n==========\n\nThis changelog summarizes new features and breaking changes in\n|EdgeDB| 1.0 beta 3 \"Ross\".\n\n\nMigrations\n==========\n\nWe continue working on improving our schema and migration tools:\n\n* Prohibit mixing computed and regular links or properties\n  (:eql:gh:`#2099`).\n* Don't ask for conversion expressions when changing type of\n  computed link or property (:eql:gh:`#2658`).\n* Fix some migration issues involving rebasing (:eql:gh:`#2536`).\n* Fix backlink processing in SDL schemas (:eql:gh:`#1824`).\n* Improve migration prompts (:eql:gh:`#2547`, :eql:gh:`#2591`).\n\n\nEdgeQL\n======\n\n* Add :ref:`free shapes <ref_datamodel_objects_free>`\n  (:eql:gh:`#2533`).\n\n  This construct provides a way to arbitrarily structure data without\n  providing a specific underlying object. It is easier to package data\n  that potentially contains empty sets this way, rather than using a\n  :eql:type:`tuple`:\n\n  .. code-block:: edgeql\n\n    with U := (select User filter .name like '%user%')\n    select {\n        matches := U {name},\n        total := count(U),\n        total_users := count(User),\n    };\n\n* Add :eql:type:`cal::relative_duration`, which is similar to duration\n  but instead uses fuzzy units like years, months and days in addition\n  to the more standard units like seconds (:eql:gh:`#2559`).\n* Enforce :eql:type:`duration` constraints in SQL as opposed to only\n  during casting (:eql:gh:`#2539`).\n\n* Implement path-like syntax for accessing enum types' members\n  (:eql:gh:`#2625`):\n\n  .. code-block:: edgeql\n\n    select Color.Red;\n\n  is equivalent to\n\n  .. code-block:: edgeql\n\n    select <Color>'Red';\n\n* Allow removal and reordering of :eql:type:`enum` elements\n  (:eql:gh:`#2564`).\n\n* Implement :eql:func:`assert_single` which allows to perform a\n  cardinality check in run-time (:eql:gh:`#2695`):\n\n  .. code-block:: edgeql-repl\n\n    db> select assert_single((select User filter .name = \"Unique\"));\n    {default::User {id: ...}}\n\n    db> select assert_single((select User))\n    ERROR: CardinalityViolationError: assert_single violation: more than\n           one element returned by an expression\n\n* Adjust the precedence of :eql:op:`detached` to match that of\n  :eql:op:`exists` (:eql:gh:`#2638`).\n\n  This makes it apply to shapes in a more intuitive fashion:\n\n  .. code-block:: edgeql\n\n    select detached User {\n        exclamation := User.name ++ '!'\n    }\n\n  The above expression will now interpret the ``User`` inside that\n  shape as the same ``detached`` User as mentioned at the root of the\n  shape.\n\n* Prohibit backlink syntax for computed links (:eql:gh:`#2619`).\n* Prohibit \"$\" as the first character in identifiers (:eql:gh:`#2595`).\n* Fix how :eql:op:`?? <coalesce>` works with :eql:type:`tuples <tuple>`\n  (:eql:gh:`#2602`).\n* Fix cardinality inference of computed links and properties\n  (:eql:gh:`#2585`).\n* Fix how :eql:op:`distinct` applies to collections of shapes\n  (:eql:gh:`#2540`).\n* Fix some cases of nested ``unless conflict`` bugs (:eql:gh:`#2555`).\n* Fix how nested volatile computed expressions get executed\n  (:eql:gh:`#2545`).\n* Fix how ``using`` expressions propagate to subtypes (:eql:gh:`#2543`).\n\n\nGraphQL\n=======\n\n* Reflect :eql:type:`json` into a custom GraphQL type (:eql:gh:`#2782`).\n\n  The :eql:type:`json` values will be reflected into a custom JSON\n  type that renders as seamless JSON. Assuming ``additional_data`` is\n  a JSON value, here's how a GraphQL query would work:\n\n  .. code-block:: graphql\n\n    {\n        Book {\n            additional_data\n        }\n    }\n\n  producing:\n\n  .. code-block:: json\n\n    {\n        \"data\": {\n            \"Book\": [\n                {\n                    \"additional_data\": {\n                        \"dimensions\": \"6 x 8 in\",\n                        \"# of illustrations\": 5,\n                        \"illustrator\": \"Alice White\"\n                    }\n                }\n            ]\n        }\n    }\n\n  There's a limitation that in order for GraphQL type validation to\n  work JSON values have to be passed as variables.\n\n* Remove the helper ``stdgraphql`` module as it is no loger needed\n  (:eql:gh:`#2692`).\n* Fix an issue with inline fragments (:eql:gh:`#1800`).\n\n\nTLS\n===\n\nWe've implemented `RFC 1008: TLS and ALPN <rfc1008_>`_. With Transport\nLayer Security (TLS) Protocol enabled by default, it is possible to\nleverage the TLS Application-Layer Protocol Negotiation (ALPN)\nExtension for secure and reliable protocol selection on top of the TLS\ntransport, allowing EdgeDB to multiplex different frontend protocols\nlike the binary protocol and the HTTP-based protocol on the same port.\n\n\nCommand-Line Tools\n==================\n\nWe've changed some of the CLI `commands and groupings <rfc1006_>`_.\nThere are some top-level \"frequently used\" commands such as\n:ref:`ref_cli_gel_dump`, :ref:`ref_cli_gel_restore`,\n:ref:`ref_cli_gel_migrate`, :ref:`ref_cli_gel_query`,\n:ref:`ref_cli_gel_info` and :ref:`ref_cli_gel_cli_upgrade`. Other\ncommands are grouped into categories:\n:ref:`ref_cli_gel_configure`, :ref:`ref_cli_gel_migration`,\n:ref:`ref_cli_gel_list`, :ref:`ref_cli_gel_describe`,\n:ref:`ref_cli_gel_instance`, :ref:`ref_cli_gel_project` and\n:ref:`ref_cli_gel_server`.\n\nHere's a more comprehensive list of the CLI commands:\n\n.. list-table::\n    :widths: auto\n    :header-rows: 1\n\n    * - SUBCOMMAND\n      - DESCRIPTION\n    * - ``dump``\n      - Create a database backup\n    * - ``restore``\n      - Restore a database backup from file\n    * - ``configure``\n      - Modify database configuration\n    * - ``migration apply``\n      - Bring current database to the latest or a specified revision\n    * - ``migration create``\n      - Create a migration script\n    * - ``migration status``\n      - Show current migration state\n    * - ``migration log``\n      - Show all migration versions\n    * - ``migrate``\n      - An alias for edgedb migration apply\n    * - ``database create``\n      - Create a new DB\n    * - ``describe object``\n      - Describe a database object\n    * - ``describe schema``\n      - Describe schema of the current database\n    * - ``list``\n      - List matching database objects by name and type\n    * - ``query``\n      - Execute EdgeQL queries\n    * - ``info``\n      - Show information about the EdgeDB installation\n    * - ``project init``\n      - Initialize a new or existing project\n    * - ``project unlink``\n      - Clean-up the project configuration\n    * - ``project info``\n      - Get various metadata about the project\n    * - ``project upgrade``\n      - Upgrade EdgeDB instance used for the current project\n    * - ``instance create``\n      - Initialize a new EdgeDB instance\n    * - ``instance list``\n      - Show all instances\n    * - ``instance status``\n      - Show status of a matching instance\n    * - ``instance start``\n      - Start an instance\n    * - ``instance stop``\n      - Stop an instance\n    * - ``instance restart``\n      - Restart an instance\n    * - ``instance destroy``\n      - Destroy an instance and remove the data\n    * - ``instance link``\n      - Link a remote instance\n    * - ``instance unlink``\n      - Unlink a remote instance\n    * - ``instance logs``\n      - Show logs of an instance\n    * - ``instance upgrade``\n      - Upgrade installations and instances\n    * - ``instance revert``\n      - Revert a major instance upgrade\n    * - ``instance reset-password``\n      - Reset password for a user in the instance\n    * - ``server``\n      - Manage local EdgeDB installations\n    * - ``cli upgrade``\n      - Upgrade the ``edgedb`` command-line tool\n\n\nBindings\n========\n\nWe've changed the location where EdgeDB stores credentials and other\ninstance information, so all the bindings need to be updated to their\nlatest versions in order to properly work with this release.\n\n* Implement `RFC 1008 <rfc1008_>`_ features for `edgedb-python\n  <https://github.com/edgedb/edgedb-python>`_ and release v0.17.x\n  driver.\n* Implement `RFC 1008 <rfc1008_>`_ features for `edgedb-js\n  <https://github.com/edgedb/edgedb-js>`_ and release v0.15.x driver.\n* Implement `RFC 1008 <rfc1008_>`_ features for `edgedb-go\n  <https://github.com/edgedb/edgedb-go>`_ and release v0.8.0 driver.\n\n.. _rfc1006:\n    https://github.com/edgedb/rfcs/blob/master/text/1006-simplified-cli.rst\n\n.. _rfc1008:\n    https://github.com/edgedb/rfcs/blob/master/text/1008-tls-and-alpn.rst\n"
  },
  {
    "path": "docs/resources/changelog/1_0_rc1.rst",
    "content": ":orphan:\n\n.. _ref_changelog_rc1:\n\n========\n1.0 RC 1\n========\n\nThis changelog summarizes new features and breaking changes in\n|EdgeDB| 1.0 Release Candidate 1 \"Epsilon Eridani\".\n\n\nMigrations\n==========\n\nWe continue fixing bugs that affect schema definitions and migrations:\n\n* Always escape newlines when string literals appear in automatically\n  generated code, such as in migrations (:eql:gh:`#2704`).\n* Fix behavior of ``set multi`` to make sure that it does not imply\n  ``set required`` (:eql:gh:`#2778`).\n* Make properties non-distinct by default (:eql:gh:`#2853`).\n\n  All values in EdgeQL are considered to be :ref:`multisets\n  <ref_eql_everything_is_a_set>`, which are a generalization of the set\n  concept that allows duplicate elements. For brevity we refer to\n  multisets as just \"sets\", and use \"proper set\" or \"distinct set\"\n  when referring to proper mathematical sets.\n\n  In EdgeDB the data is modeled as a directed graph, where objects are\n  vertices and links are edges.  Query path expressions\n  (``Foo.bar.baz``) are graph traversal operators and, by definition,\n  always return a *distinct set*.  This means that there can be no\n  link of the same name between a pair of objects, and this also means\n  that a set of objects pointed to by a ``multi`` link is always\n  distinct.  Computed links must follow this rule too so as to behave\n  exactly like materialized links externally.  To illustrate, consider\n  a common case of finding a set of \"friends of friends\" of a\n  particular user.  In EdgeQL this is simply ``select\n  User.friends.friends``.\n\n  Properties are different, because their value is scalar, and scalars\n  lack true identity, which makes any proper set mechanics on scalar\n  sets expensive due to explicit elimination of duplicates.\n  Furthermore, non-distinct scalar sets are actually *desirable* in\n  many queries, especially where analytics and tuples are involved.\n  Finally, we should enforce consistent multiplicity rules on computed\n  properties, and static inference of multiplicity on scalars is weak\n  and will effectively force users to pollute queries with pointless\n  and expensive :eql:op:`distinct`.\n\n  Here we change EdgeDB behavior to allow multiplicity *greater than\n  one* in ``multi`` properties.\n\n* Fix the error when an object function is created alongside its\n  object (:eql:gh:`#2834`).\n\n* Fix handling of function overloads on object type parameters\n  (:eql:gh:`#2889`).\n\n  The current approach places a number of restrictions on overloads\n  of object type functions:\n\n  - there must be no difference in any but one parameter type, i.e.\n    this is single dispatch\n  - the names of all parameters must match\n\n  These restrictions apply only to functions that are actually\n  overloaded with a different object type. Overloading with scalar\n  types is perfectly OK even if the rest of the parameter types differ:\n\n  .. code-block:: sdl\n\n    function func(a: Foo, b: int32) -> int64 using ( BODY );\n    function func(a: Bar, b: int32) -> int64 using ( BODY );\n    function func(a: str, b: int32, c: int64) -> int64 using ( BODY );\n\n* Make computed links and properties defined in the schema behave the\n  same way as the equivalent computed links and properties defined\n  ad-hoc directly in the query (:eql:gh:`#2558`).\n\n\nEdgeQL\n======\n\n* Drop ``final`` from the syntax (:eql:gh:`#2607`).\n* Restore the precedence of :eql:op:`detached` to no longer match that\n  of :eql:op:`exists` undoing the change :eql:gh:`#2638`.\n* Fix ``unless conflict`` with inheritance hierarchies (:eql:gh:`#2525`).\n* Always use serializable isolation (:eql:gh:`#2877`).\n* Correctly raise exclusive constraints when performing an ``insert``\n  or an ``update`` on related types (:eql:gh:`#2845`).\n* Enforce link or property cardinality in mutations (:eql:gh:`#2827`).\n* Fix cardinality inference in \"optionality-preserving\" functions\n  (:eql:gh:`#2844`).\n\n  A certain number of standard aggregate functions are guaranteed to\n  preserve the lower cardinality bound of their argument.  These\n  functions are :eql:func:`assert_single`, :eql:func:`enumerate`,\n  :eql:func:`min`, and :eql:func:`max`.\n\n  Unfortunately, there is currently no way to signal that lower\n  cardinality is preserved for user-defined functions. We introduce\n  the new std-only ``preserves_cardinality`` field, which is also\n  exposed in the public introspection schema for the benefit of query\n  builders and such.\n\n* Implement :eql:func:`assert_exists` which allows to perform a\n  cardinality check in run-time (:eql:gh:`#2901`):\n\n  .. code-block:: edgeql-repl\n\n    db> select assert_exists((select User filter .name = \"Administrator\"));\n    {default::User {id: ...}}\n\n    db> select assert_exists((select User filter .name = \"Nonexistent\"));\n    ERROR: CardinalityViolationError: assert_exists violation: expression\n           returned an empty set.\n\n* Fix deletion issues for multi properties (:eql:gh:`#2883`).\n\n* Disallow some broken DML/volatile interactions (:eql:gh:`#2811`).\n* Fix volatility inference of functions taking object arguments to be\n  at most ``Stable`` because it triggers a table scan\n  (:eql:gh:`#2890`).\n* Fix a regression when objects from opaque sources are passed to\n  functions (:eql:gh:`#2924`).\n\n* Fix accessing link properties on union types (:eql:gh:`#2513`).\n* Fix some introspection issues with backlinks (:eql:gh:`#2599`)\n* Fix bare backlink schema-defined computed link (:eql:gh:`#2605`).\n* Make filtered backlinks work even when there exists a computed\n  backlink (:eql:gh:`#2698`).\n\n* Fix handling of the ``optional`` specifier in shapes to be similar\n  to SDL (:eql:gh:`#2900`).\n\n\nCommand-Line Tools\n==================\n\n* Add the :ref:`cli.toml <ref_cli_gel_config>` global configuration\n  file for customizing the CLI and REPL behavior. The location of the\n  file is shown as \"Config\" by :ref:`ref_cli_gel_info`.\n\n* Make SCRAM the default auth method (:eql:gh:`#2848`).\n* Add a server option to specify the default authentication method\n  (:eql:gh:`#2936`).\n\n  This patch adds the new ``--default-auth-method`` argument as well as\n  its companion ``EDGEDB_SERVER_DEFAULT_AUTH_METHOD`` environment\n  variable to set the default authentication method.\n\n\nBindings\n========\n\nWe've updated the binary protocol to version 0.12 which brings some\nperformance and other improvements.\n\n* Support protocol 0.12 features for `edgedb-python\n  <https://github.com/edgedb/edgedb-python>`_ and release v0.18.0\n  driver.\n* Support protocol 0.12 features for `edgedb-js\n  <https://github.com/edgedb/edgedb-js>`_ and release v0.15.2\n  driver.\n* Support protocol 0.12 features for `edgedb-go\n  <https://github.com/edgedb/edgedb-go>`_ and release v0.8.2\n  driver.\n\n"
  },
  {
    "path": "docs/resources/changelog/1_0_rc2.rst",
    "content": ":orphan:\n\n.. _ref_changelog_rc2:\n\n========\n1.0 RC 2\n========\n\nThis changelog summarizes new features and breaking changes in\nEdgeDB 1.0 Release Candidate 2 \"Lacaille\".\n\nMigrations\n==========\n\nWe continue fixing bugs that affect schema definitions and migrations.\nThe most notable changes involve static cardinality inference, so that\ncomputed links and properties can be defined to mimic their regular\ncounterparts:\n\n* Fix creation of required computed links (:eql:gh:`#2985`).\n\n  This allows having ``required`` links in the schema even if they are\n  computed:\n\n  .. code-block:: sdl\n\n    type User {\n        required link profile -> Profile;\n    }\n\n    type Profile {\n        required link user := assert_exists(.<profile[is User]);\n    }\n\n* Fix issues when migrating between regular and computed links and\n  properties (:eql:gh:`#2411`).\n\n* Fix issues when changing cardinality of computed links and\n  properties (:eql:gh:`#3003`).\n\n* Fix how ``alter`` suggestions are rejected when processing\n  migrations, making sure that they don't repeat (:eql:gh:`#3111`).\n\n\nEdgeQL\n======\n\n* Make static analysis correctly handle the cardinality of things\n  wrapped in :eql:func:`enumerate` (:eql:gh:`#3014`).\n\n  This means that if you wrap a single property into\n  :eql:func:`enumerate` it still is correctly inferred to produce no\n  more than one result:\n\n  .. code-block:: edgeql-repl\n\n    db> select User {comp := enumerate(.name)};\n    {default::User {comp: (0, 'alice')}}\n\n* Fix some issues with :eql:func:`enumerate` applied to the results of\n  another function (:eql:gh:`#3025`).\n\n* Fix :eql:func:`assert_distinct` on tuples (:eql:gh:`#2990`).\n\n* Fix a case where :eql:stmt:`for` over an empty set erroneously\n  produced a non-empty result (:eql:gh:`#3012`).\n\n* Fix :eql:constraint:`exclusive` constraint issue when conflicting\n  entries are inserted at the same time (:eql:gh:`#3022`).\n\n* Fix issues with how tuple elements are correlated (:eql:gh:`#3001`).\n\n* Fix how functions or operators with ``optional`` parameters such as\n  :eql:op:`?? <coalesce>` interact with shape subqueries\n  (:eql:gh:`#3008`).\n\n  This makes it possible to have a shape as one element of a tuple to\n  be correlated with the expression in the other tuple element:\n\n  .. code-block:: edgeql-repl\n\n    db> select (\n    ...   User {name, friends: {name}},\n    ...   User.friends.name ?? 'n/a'\n    ... );\n    {\n      (\n        default::User {\n          name: 'Alice',\n          friends: {default::User {name: 'Billie'}},\n        },\n        'Billie',\n      ),\n      (\n        default::User {\n          name: 'Alice',\n          friends: {default::User {name: 'Cameron'}},\n        },\n        'Cameron',\n      ),\n      (\n        default::User {\n          name: 'Alice',\n          friends: {default::Bot {name: 'Dana'}},\n        },\n        'Dana',\n      ),\n      (default::User {name: 'Billie', friends: {}}, 'n/a'),\n      (default::User {name: 'Cameron', friends: {}}, 'n/a'),\n      (\n        default::Bot {\n          name: 'Dana',\n          friends: {default::User {name: 'Billie'}},\n        },\n        'Billie',\n      ),\n    }\n\n* Fix issues with arrays of objects of related types (:eql:gh:`#2256`).\n\n  Trying to build an array where elements are not of the same object\n  type, but of related types no longer produces an error:\n\n  .. code-block:: edgeql-repl\n\n    db> select [(select User filter .name = 'alice'), (select SystemUser)];\n    {\n      [\n        default::User {id: 8f69777e-3129-11ec-ba91-0f55d65fd8d7},\n        default::SystemUser {id: 89c0e596-3129-11ec-ba91-2f631728aea3},\n      ],\n    }\n\n  Conceptually, there's no difference between first creating a set via\n  :eql:op:`union` and using :eql:func:`array_agg` on it or using an\n  array constructor directly and supply the individual elements as\n  subqueries.\n\n* Fix many bugs with objects inside arrays and tuples (:eql:gh:`#2992`).\n\n  Packing and unpacking objects into arrays and tuples now works more\n  reliably. You can make use of the different way of selecting the\n  data, aggregate it using :eql:func:`array_agg` or by some other\n  means and still be able to access the elements to get their nested\n  contents in queries:\n\n  .. code-block:: edgeql-repl\n\n    db> select [(User,)][0];\n    {\n      (default::User {id: 8f69777e-3129-11ec-ba91-0f55d65fd8d7}),\n    }\n    db> select ([User],).0;\n    {\n      [default::User {id: 8f69777e-3129-11ec-ba91-0f55d65fd8d7}],\n    }\n\n* Fix some issues with ad-hoc computed links or properties inside\n  arrays (:eql:gh:`#2979`).\n\n* Fix :eql:op:`and` to consistently apply to properties and produce\n  an ``{}`` even for the case of ``False and {}`` (:eql:gh:`#3121`).\n\n* Restrict :eql:op:`| <typeor>` operator to object types only\n  (:eql:gh:`#3116`).\n\n* Reserve ``never`` as a keyword for future use (:eql:gh:`#3102`).\n\n\nGraphQL\n=======\n\n* Make ``and`` and ``or`` in GraphQL use \"short-circuiting\" logic.\n\n  In EdgeQL :eql:op:`and` and :eql:op:`or` produce ``{}`` if any of\n  the operands are ``{}``. There are different ways of handling the\n  ``{}`` by using :eql:op:`?? <coalesce>` or :eql:op:`if..else` operator.\n  However, within the restricted context of operations reflected to\n  GraphQL ``filter`` it makes sense to use \"short-circuiting\" versions\n  of the operators and enforce that if any of the operands to ``or``\n  are ``true``, so is the result and similarly if any of the operands\n  to ``and`` are ``false`` so is the result.\n\n* Hide ``id`` as well as computed links and properties from mutation\n  (:eql:gh:`#3109`).\n\n\nConfiguration\n=============\n\nA lot of the :ref:`configuration <ref_std_cfg>` settings used to be\nstrings that simply mapped to Postgres settings. We now use more\nappropriate types for expressing configuration values:\n:eql:type:`bool`, :eql:type:`int64`, :eql:type:`duration`, in addition\nto :eql:type:`str`.\n\nWe also introduce a new scalar :eql:type:`cfg::memory` to represent\nsize of various memory storage in a clear and unambiguous way.\n\n\nBindings\n========\n\nWe now have the *client* abstraction superceding the *connection* and\n*pool* abstractions. There's no conceptual difference between a pool\nof size 1 and a single connection, so we decided that the API should\nreflect that. So we introduce a single concept of *client* to send\nqueries to the database and let this *client* encapsulate the handling\nof concurrent connections.\n\nWe're making our binding more robust by adding \"retry options\" to our\ninterface. What it means is that read-only queries and all\ntransactions can be automatically retried in case of certain kinds of\nerrors (such as network errors) without the need for any extra code on\nthe part of the developers. This also means that we no longer have\n\"raw\" and \"retrying\" transactions in the APIs, but all transactions\nare retrying. To get the same behavior as for \"raw\" transactions the\nmaximum number of attempts can be explicitly specified as ``1``.\n\nWe're also in the process of cleaning up our APIs and removing the\ndeprecated functions.\n\n* Support connection pooling by default and retry features for\n  `edgedb-python <https://github.com/edgedb/edgedb-python>`_ and\n  release v0.18.0a2 driver.\n* Support connection pooling by default and retry features for\n  `edgedb-js <https://github.com/edgedb/edgedb-js>`_ and release\n  v0.15.3 driver.\n* Support connection pooling by default and retry features for\n  `edgedb-go <https://github.com/edgedb/edgedb-go>`_ and release\n  v0.8.3 driver.\n\nEnvironment variable renames\n============================\n\nFor clarity and simplicity, several environment variables have been renamed\nand the set of supported values has been modified. The old variables have been\ndeprecated and will be removed in a future release.\n\n.. list-table::\n\n  * - **Old name**\n    - **New name**\n  * - ``EDGEDB_TLS_VERIFY_HOSTNAME``\n    - ``EDGEDB_CLIENT_TLS_SECURITY``\n  * - ``EDGEDB_NO_VERSION_CHECK``\n    - ``EDGEDB_RUN_VERSION_CHECK``\n  * - ``EDGEDB_SKIP_DOCKER_CHECK``\n    - ``EDGEDB_INSTALL_IN_DOCKER``\n  * - ``EDGEDB_SERVER_ALLOW_INSECURE_BINARY_CLIENTS``\n    - ``EDGEDB_SERVER_BINARY_ENDPOINT_SECURITY``\n  * - ``EDGEDB_SERVER_ALLOW_INSECURE_HTTP_CLIENTS``\n    - ``EDGEDB_SERVER_HTTP_ENDPOINT_SECURITY``\n  * - ``EDGEDB_SERVER_INSECURE_DEV_MODE``\n    - ``EDGEDB_SERVER_SECURITY``\n  * - ``EDGEDB_SERVER_SKIP_MIGRATIONS``\n    - ``EDGEDB_DOCKER_APPLY_MIGRATIONS``\n  * - ``EDGEDB_SERVER_GENERATE_SELF_SIGNED_CERT``\n    - ``EDGEDB_SERVER_TLS_CERT_MODE``\n  * - ``EDGEDB_HIDE_GENERATED_CERT``\n    - ``EDGEDB_DOCKER_SHOW_GENERATED_CERT``\n\n\nClient configuration\n--------------------\n\n``EDGEDB_CLIENT_TLS_SECURITY``\n    - ``insecure``\n    - ``no_host_verification``\n    - ``strict``\n    - ``default``: ``no_host_verification`` if custom certificate is supplied,\n      otherwise ``strict``\n\n**New**: ``EDGEDB_CLIENT_SECURITY``\n    - ``default``\n    - ``insecure_dev_mode``: for now, equivalent to setting\n      ``EDGEDB_CLIENT_TLS_SECURITY=insecure`` though this mode may\n      encapsulate other behavior later\n\n``EDGEDB_RUN_VERSION_CHECK``\n    - ``cached == default`` — run occasionally\n    - ``never`` — skips the check\n\n``EDGEDB_INSTALL_IN_DOCKER`` (CLI only)\n  - ``forbid == default``\n  - ``allow`` — skips the check\n\nServer configuration\n--------------------\n\n``EDGEDB_SERVER_BINARY_ENDPOINT_SECURITY``\n    - ``tls == default``\n    - ``optional`` — allow no TLS\n\n``EDGEDB_SERVER_HTTP_ENDPOINT_SECURITY``\n    - ``tls == default``\n    - ``optional`` — allow no TLS\n\n``EDGEDB_SERVER_SECURITY``\n    - ``strict == default``\n    - ``insecure_dev_mode`` — disable password-based authentication and allow\n      unencrypted HTTP traffic\n\n``EDGEDB_DOCKER_APPLY_MIGRATIONS`` (Docker only)\n    - ``always == default``\n    - ``never``\n\n``EDGEDB_SERVER_TLS_CERT_MODE``\n    - ``require_file`` — requires a valid TLS certificate and key to be\n      specified\n    - ``generate_self_signed`` generate self-signed certificate and private\n      key on bootstrap if certificate or key are not specified or missing\n    - ``default`` (equals to ``require_file`` if ``EDGEDB_SERVER_SECURITY``\n      is set to ``strict``, equals to ``generate_self_signed`` if\n      ``EDGEDB_SERVER_SECURITY`` is set to ``insecure_dev_mode``)\n\n``EDGEDB_DOCKER_SHOW_GENERATED_CERT``\n    - ``always == default``\n    - ``never``\n"
  },
  {
    "path": "docs/resources/changelog/1_0_rc3.rst",
    "content": ":orphan:\n\n.. _ref_changelog_rc3:\n\n========\n1.0 RC 3\n========\n\nThis changelog summarizes changes and bugfixes in |EdgeDB| 1.0 Release\nCandidate 3 \"Cygni\". This release is focusing on fixing existing\nissues rather than introducing new features.\n\n\nPackaging\n=========\n\nThe biggest change in this release is in how EdgeDB is packaged. We\nnow have a \"portable build\" that runs on any non-legacy Linux or macOS\nsystem. The purpose is to avoid restrictions that supporting native\npackaging systems imposes. Because of this we recommend a more\nexplicit ``dump``/``restore`` upgrade workflow to get your instances\nto RC3.\n\nBefore you start you may want to backup the instance credentials\n(which you can find in the \"Config\" directory indicated by\n:ref:`ref_cli_gel_info`). The first step in the upgrade process\nwill be backing up all the instance data and that will include the\nlogin credentials that the instance expects. After restoring the\ndatabase dump, you may need to restore old credentials file as\nwell.\n\nProject upgrade\n---------------\n\nIf you have an EdgeDB project, you can upgrade it to RC3 using the\nfollowing steps from the project directory:\n\n.. code-block:: bash\n\n    $ gel dump --all --format=dir ./upgrade_backup\n    $ gel project unlink -D\n    $ gel project upgrade --to-latest\n    $ gel project init --no-migrations\n    $ gel restore --admin --all ./upgrade_backup\n\nNow that the project has been re-initialized and data dump is\nrestored, we need to restore the login credentials. Which presents a\ncouple of options. If there's no code outside the project directory\nthat actually requires access to the database, the recommended option\nis to reset the password:\n\n.. code-block:: bash\n\n    $ gel instance reset-password my_instance\n\nAlternatively we can restore the old credentials:\n\n.. code-block:: bash\n\n    $ gel instance reset-password my_instance --password\n\n\nInstance upgrade\n----------------\n\nIf you have an EdgeDB instance that's not part of a project, you can\nupgrade it to RC3 using the following steps:\n\n.. code-block:: bash\n\n    $ gel dump -I my_instance --all --format=dir ./my_instance_backup\n    $ gel instance destroy my_instance\n    $ gel instance create my_instance\n    $ gel restore -I my_instance --admin --all ./my_instance_backup\n\nNow that the instance has been re-initialized and data dump is\nrestored, we need to restore the login credentials. Which presents a\ncouple of options. If there's no code that uses the login credentials\nexplicitly, but rather relies on the instance name only to access to\nthe database, the recommended approach is to reset the password:\n\n.. code-block:: bash\n\n    $ gel instance reset-password my_instance\n\nAlternatively we can restore the old credentials:\n\n.. code-block:: bash\n\n    $ gel instance reset-password my_instance --password\n\n\nEdgeQL\n======\n\n* Support path prefix in link constraints (:eql:gh:`#3206`).\n\n  It is now possible to use shorthand path expression referring to\n  link properties in constraint expressions:\n\n  .. code-block:: sdl\n\n    type Foo {\n        link bar -> Object {\n            property linkprop -> str;\n            constraint expression on (@linkprop != \"lol\");\n        }\n    }\n\n* Support path prefix in link indexes (:eql:gh:`#3202`).\n\n  It is now possible to use shorthand path expression referring to\n  link properties in index expressions:\n\n  .. code-block:: sdl\n\n    abstract link friendship {\n        property strength -> float64;\n        index on (@strength);\n    }\n\n* Implement index and slicing of JSON strings (:eql:gh:`#3203`).\n\n  We already implement :eql:op:`++ <jsonplus>` for JSON strings, but\n  indexing operations were left out until now. This change fixes this\n  oversight:\n\n  .. code-block:: edgeql-repl\n\n    db> select to_json('\"hello\"')[0];\n    {\"\\\"h\\\"\"}\n    db> select to_json('\"hello\"')[1:3];\n    {\"\\\"el\\\"\"}\n\n* Fix some corner cases of functions without ``optional`` or ``set\n  of`` arguments being called on empty input (:eql:gh:`#3195`).\n\n* Fix some corner cases of changing link type (:eql:gh:`#3183`).\n\n\nBindings\n========\n\nWe are in the process of dropping old deprecated APIs before rolling\nout production-ready EdgeDB.\n\n* Clean up the API for `edgedb-python\n  <https://github.com/edgedb/edgedb-python>`_ and release v0.19.0\n  driver.\n* Clean up the API for `edgedb-js\n  <https://github.com/edgedb/edgedb-js>`_ and release v0.17.0 driver.\n* Clean up the API for `edgedb-go\n  <https://github.com/edgedb/edgedb-go>`_ and release v0.9.0 driver.\n"
  },
  {
    "path": "docs/resources/changelog/1_0_rc4.rst",
    "content": ":orphan:\n\n.. _ref_changelog_rc4:\n\n========\n1.0 RC 4\n========\n\nThis changelog summarizes changes and bugfixes in |EdgeDB| 1.0 Release\nCandidate 4 \"Procyon\". This release is focusing on fixing existing\nissues rather than introducing new features.\n\n\nMigrations\n==========\n\n* Disallow cardinality mismatch in function return and body (:eql:gh:`#2137`).\n* Disallow :eql:type:`anytype` and :eql:type:`anytuple` in SDL\n  (:eql:gh:`#2137`).\n* Fix issues that prevented migrations to an empty clean module\n  (:eql:gh:`#3273`, :eql:gh:`#3280`).\n* Fix altering parent types in migrations (:eql:gh:`#3276`).\n* Fix migration issue when a constraint alters cardinality of an\n  expression (:eql:gh:`#3275`).\n\n\nEdgeQL\n======\n\n* :eql:stmt:`for` loops no longer require wrapping everything\n  into ``{ }``. Simple expressions using a single path or a function\n  call can be used directly (:eql:gh:`#3243`):\n\n  .. code-block:: edgeql-repl\n\n    db> for x in array_unpack([1, 5]) union x * 10;\n    {10, 50}\n    db> for x in User.name union x[0];\n    {'a', 'b', 'c', 'd'}\n\n\nBindings\n========\n\nWhile we are getting ready for a stable release of EdgeDB, we continue\nthe cleanup of our bindings, with a handful of improvements. We now\nsupport non-file variants of ``tls_ca`` and ``credentials`` options.\n\n* Release `edgedb-python <https://github.com/edgedb/edgedb-python>`_\n  v0.21.0 driver. This version will now automatically retry on\n  ``TransactionConflictError``.\n* Release `edgedb-js <https://github.com/edgedb/edgedb-js>`_ v0.18.0 driver.\n* Release `edgedb-go <https://github.com/edgedb/edgedb-go>`_ v0.9.1 driver.\n"
  },
  {
    "path": "docs/resources/changelog/1_0_rc5.rst",
    "content": ":orphan:\n\n.. _ref_changelog_rc5:\n\n========\n1.0 RC 5\n========\n\nThis changelog summarizes changes and bugfixes in |EdgeDB| 1.0 Release\nCandidate 5 \"Tau Ceti\". Compared to other releases this is a very\nsmall one as we're closing in on the stable version.\n\n\nChanges\n=======\n\n* Downgrade some keywords from being reserved to unreserved:\n  ``abort``, ``declare``, ``empty``, ``order``, ``populate``,\n  ``release``, ``reset``, ``start`` (:eql:gh:`#3387`).\n* Fix an issue with removing types from the schema (:eql:gh:`#3309`).\n* Improve cardinality inference for ``offset`` and ``limit`` clauses\n  (:eql:gh:`#3375`).\n* Enable :eql:type:`enums <enum>` for some configuration values\n  (:eql:gh:`#3320`).\n"
  },
  {
    "path": "docs/resources/changelog/1_x.rst",
    "content": "====\nv1.0\n====\n\n:edb-alt-title: EdgeDB v1 (Nova)\n\n.. image:: images/v1_nova.jpg\n    :width: 100%\n\n\nEdgeDB 1.0 was released on February 10, 2022. Read the announcement\nblog post `here <v1anno_>`_.\n\nWe would like to thank our community for reporting issues and\ncontributing fixes. You are awesome! ❤️\n\n\n1.4\n===\n\n* Avoid unnecessary updates to parent views and triggers (:eql:gh:`#3771`)\n\n* Drop broken special case for nested insert ``FOR`` (:eql:gh:`#3797`)\n\n* Fix ``IN array_unpack`` for bigints (:eql:gh:`#3820`)\n\n* Put more parens around index expressions in generated DDL (:eql:gh:`#3822`)\n\n* Fix a weird computable/alias interaction (:eql:gh:`#3828`)\n\n* Support linkprops on backlinks (:eql:gh:`#3841`)\n\n* Fix generation of dummy_pathid in nonconflict ctes (:eql:gh:`#3848`)\n\n* Always correctly handle variadic arguments when producing AST from migrations\n  (:eql:gh:`#3855`)\n\n\n1.3\n===\n\n* Fix a multiplicity computation bug for tuples (:eql:gh:`#3632``)\n\n* Fix a multiplicity issue with doubly nested loops (:eql:gh:`#3636`)\n\n* Fix a bug involving computable shadowing (:eql:gh:`#3652`)\n\n* Make replace_prefix change PointerRefs to make types line up\n  (:eql:gh:`#3642`)\n\n* Search the source_rvar for materialized refs (:eql:gh:`#3657`)\n\n* Fix min/max when no source aspect is present (:eql:gh:`#3664`)\n\n* Don't create UnionTypeShell for views of unions (:eql:gh:`#3670`)\n\n* Fix a collection of json casting bugs (:eql:gh:`#3676`)\n\n* ha/stolon: Don't attempt to parse unsuccessful Consul responses\n  (:eql:gh:`#3698`)\n\n* ha/stolon: Add exponential backoff on unsuccessful Consul KV responses\n  (:eql:gh:`#3699`)\n\n* Make sure the sets in conflict clauses are have correct scope\n  (:eql:gh:`#3686`)\n\n* Produce an error instead of malformed SQL on ``insert foo { name }``\n  (:eql:gh:`#3687`)\n\n* Don't treat everything with a binding as STABLE (:eql:gh:`#3689`)\n\n* Check that index expressions are immutable (:eql:gh:`#3690`)\n\n* Fix fetching computed propery of UNION of same type (:eql:gh:`#3691`)\n\n* Fix references to ``__subject__`` in object constraints (:eql:gh:`#3695`)\n\n* Fix unions of DML overlays on reverse inline pointers (:eql:gh:`#3694`)\n\n* Fix some inheritance issues with renames and expr refs (:eql:gh:`#3696`)\n\n* Fix a card inference bug when dealing with computables (:eql:gh:`#3628`)\n\n* Fix a collection of nested shape path reference issues (:eql:gh:`#3700`)\n\n* Produce a proper error on ``describe`` of nonexisting function, module\n  (:eql:gh:`#3701`)\n\n* Pin the version of edgedb-cli that we use\n  (:eql:gh:`#3705`)\n\n* Fix ``[NOT] IN array_unpack(foo)`` when foo is empty (:eql:gh:`#3752`)\n\n* Inject ``SYNC`` between state restore & ``START TRANSACTION`` in\n  [execute] flow. (:eql:gh:`#3749``)\n\n* Fail if local Postgres cluster fails to start\n\n\n1.2\n===\n\n* Add on-demand compiler pool scaling (:eql:gh:`#3550`).\n\n  Use ``--compiler-pool-mode=on_demand`` to switch to the new mode,\n  which will a spawn new compiler worker process every 3 seconds if\n  the compiling requests keep queueing up. The upper limit on the\n  number of these spawned processes is the number of CPUs. After 60\n  seconds without compiling requests, the compiler pool will scale\n  down to ``--compiler-pool-size`` with a default of ``1`` under\n  on-demand mode.\n\n* Fix an issue with default module and module aliases inside transactions\n  (:eql:gh:`#3604`).\n* Add ``reset on target delete`` to :ref:`DDL <ref_eql_ddl_links>` in\n  order to fix some migration bugs concerning links (:eql:gh:`#3611`).\n* Enforce newly created :eql:constraint:`exclusive` constraints across\n  existing data (:eql:gh:`#3613`).\n* Fix an issue with a self-referencing :eql:stmt:`update` (:eql:gh:`#3605`).\n* Fix an issue with a constraint bug in :eql:stmt:`update` (:eql:gh:`#3603`).\n* Fix a constraint bug in :eql:stmt:`update` (:eql:gh:`#3603`).\n* Fix an SDL issue with computed links referencing each other\n  (:eql:gh:`#3499`).\n* Fix self-referencing nested mutations in GraphQL (:eql:gh:`#3470`).\n* Fix GraphQL fragments for types (:eql:gh:`#3514`).\n* Add ``in`` operator to GraphQL (:eql:gh:`#3443`).\n* Fix cardinality inference in some special cases (:eql:gh:`#3590`).\n* Fix inheritance from enum types (:eql:gh:`#3578`).\n* Fix cardinality inference bug and extend cardinality restrictions to\n  longer paths (:eql:gh:`#3566`).\n\n  Specifically, correctly infer that filtering on a long path where\n  each hop is :eql:constraint:`exclusive` produces at most one result.\n\n  For example: ``select Foo filter .bar.baz = 'key'`` should have\n  cardinality of at most one if both ``bar`` and ``baz`` have\n  ``exclusive`` constraints.\n\n* Fix issues involving scalar set identity (:eql:gh:`#3525`).\n* Fix exclusive constraints on tuple properties (:eql:gh:`#3559`).\n\n\n1.1\n===\n\n* Fix a migration issue with handling ``default`` on inherited\n  :ref:`links <ref_datamodel_links>` or :ref:`properties\n  <ref_datamodel_props>` (:eql:gh:`#3544`).\n* Fix a migration issue with an inherited :ref:`property\n  <ref_datamodel_props>` (:eql:gh:`#3542`).\n* Fix a migration issue with dropping a type (:eql:gh:`#3521`).\n* Fix a migration issue with changing a :ref:`link\n  <ref_datamodel_links>` from ``single`` to ``multi``\n  (:eql:gh:`#3392`).\n* Provide a more detailed message for :ref:`constraints\n  <ref_datamodel_constraints>` errors (:eql:gh:`#3522`).\n* Produce an error on an invalid regex (:eql:gh:`#3412`).\n* Produce a proper error when an expression is invalid in a certain\n  special contexts, such as ``default`` (:eql:gh:`#3494`).\n* Format the database name correctly in\n  ``DuplicateDatabaseDefinitionError`` (:eql:gh:`#3228`)\n* Fix an error when working with a composite :eql:constraint:`exclusive`\n  constraint (:eql:gh:`#3502`).\n* Correctly infer cardinality of a property on a multi link in the\n  context of the :ref:`constraint <ref_datamodel_constraints>` on that\n  link (:eql:gh:`#3536`).\n* Disable changing the concrete base of a :ref:`scalar type\n  <ref_datamodel_scalar_types>` (:eql:gh:`#3529`).\n* Avoid generating pointless self joins (:eql:gh:`#2567`).\n* Fix IPv6 address parsing (:eql:gh:`#3454`).\n* If a type has object instances, it cannot be made ``abstract``\n  (:eql:gh:`#3399`).\n* Fix an issue that sometimes caused :eql:kw:`with` block variables to\n  be unusable (:eql:gh:`#3385`).\n\n\nPre-releases\n============\n\nEdgeDB 1.0 had a series of pre-preleases. Read the full history here:\n\n:ref:`Alpha 2 <ref_changelog_alpha2>`,\n:ref:`alpha 3 <ref_changelog_alpha3>`,\n:ref:`alpha 4 <ref_changelog_alpha4>`,\n:ref:`alpha 5 <ref_changelog_alpha5>`,\n:ref:`alpha 6 <ref_changelog_alpha6>`,\n:ref:`alpha 7 <ref_changelog_alpha7>`,\n:ref:`beta 1 <ref_changelog_beta1>`,\n:ref:`beta 2 <ref_changelog_beta2>`,\n:ref:`beta 3 <ref_changelog_beta3>`,\n:ref:`RC 1 <ref_changelog_rc1>`,\n:ref:`RC 2 <ref_changelog_rc2>`,\n:ref:`RC 3 <ref_changelog_rc3>`,\n:ref:`RC 4 <ref_changelog_rc4>`,\n:ref:`RC 5 <ref_changelog_rc5>`.\n\n\n.. _v1anno:\n    https://www.edgedb.com/blog/edgedb-1-0\n"
  },
  {
    "path": "docs/resources/changelog/2_x.rst",
    "content": "====\nv2.0\n====\n\n:edb-alt-title: EdgeDB v2 (Sagittarius)\n\n.. image:: images/v2_sagittarius.jpg\n    :width: 100%\n\n|EdgeDB| 2.0 was released on July 28th, 2022. Read the announcement\nblog post `here <v2anno_>`_.\n\nWe would like to thank our community for reporting issues and contributing\nfixes. You are awesome! ❤️\n\n\nTo play with the new features, install `the CLI\n<https://www.edgedb.com/install>`_ and initialize a new project. For an\ninteresting schema with test data, check out the `MCU Sandbox\n<https://github.com/edgedb/mcu-sandbox>`_ repo.\n\n.. code-block:: bash\n\n  $ edgedb project init\n\n\nUpgrading\n=========\n\n**Local instances**\n\nTo upgrade a local project, run the following command inside the project\ndirectory.\n\n.. code-block:: bash\n\n  $ edgedb project upgrade --to-latest\n\nAlternatively, specify an instance name if you aren't using a project.\n\n.. code-block:: bash\n\n  $ edgedb project upgrade --to-latest -I my_instance\n\n**Hosted instances**\n\nTo upgrade a remote (hosted) instance, we recommend the following\ndump-and-restore process.\n\n1. Spin up an empty 2.0 instance by following one of our :ref:`deployment\nguides <ref_guide_deployment>`. These guides have been updated to 2.0. Keep\nthe DSN of the newly created instance handy.\n\n2. Take your application offline, then dump your v1.x database with the CLI\n\n   .. code-block:: bash\n\n     $ gel dump --dsn <old dsn> --all my_database.dump/\n\n   This will dump the schema and contents of your current database to a file\n   on your local disk called ``my_database.dump``. The file name isn't\n   important.\n\n3. Restore the empty v2.x instance from the dump\n\n   .. code-block:: bash\n\n     $ gel restore --all my_database.dump/ --dsn <new dsn>\n\n   Once the restore is complete, update your application to connect to the new\n   instance.\n\n.. note::\n\n    If your Postgres cluster is also backing other versions of EdgeDB, make\n    sure you start your new instance with the ``--ignore-other-tenants``\n    option when bootstrapping your new instance.\n\nThis process will involve some downtime, specifically during steps 2 and 3. We\nare working on an in-place upgrade workflow that will reduce the amount of\ndowntime involved and avoid the need to spin up a new instance. We'll publish\nthat soon; join the Discord for updates. Though for most applications the\ndump-and-restore workflow will be simpler and less error-prone.\n\n\nClient libraries\n----------------\n\nWe've released new versions of our JavaScript and Python client libraries that\nsupport all 2.0 features and implement the updated protocol. These versions\nare backwards compatible with v1.x instances, so we encourage all users to\nupgrade.\n\n.. list-table::\n\n  * - :ref:`TypeScript/JS <gel-js-intro>`\n    - ``edgedb@0.21.0``\n  * - :ref:`Python <gel-python-intro>`\n    - ``edgedb@0.24.0``\n  * - `Golang <https://github.com/geldata/gel-go>`_\n    - ``edgedb@0.12.0``\n  * - `Rust <https://github.com/geldata/gel-rust>`_\n    - ``edgedb-tokio@0.3.0``\n  * - `.NET <https://github.com/quinchs/EdgeDB.Net>`_ (community-maintained)\n    - ``EdgeDB.Net.Driver@0.3.0``\n  * - `Elixir <https://github.com/nsidnev/edgedb-elixir>`_\n      (community-maintained)\n    - ``edgedb@0.4.0``\n\nNew features\n============\n\nIntegrated admin UI\n-------------------\n\nAll v2 instances ship with a built-in rich admin GUI. Access it by running\n``edgedb ui`` inside any :ref:`EdgeDB project <ref_guide_using_projects>`, or\nspecify a local instance name with ``edgedb ui -I my_inst``. The\ncommand opens the instance's admin UI using the default system browser.\n\nThe current iteration of the GUI has\n\n- a data browser and editor\n- a REPL for writing and executing EdgeQL queries\n- a schema introspection tool with text-based and graphical visualizations of\n  the instance's current schema\n\n.. image:: images/v2_ui.jpg\n    :width: 100%\n\n\nAnalytical queries with ``GROUP``\n---------------------------------\n\nThe new :ref:`GROUP <ref_eql_group>` expression can be used to partition\nand aggregate data. The output of ``GROUP`` are :ref:`free objects\n<ref_eql_select_free_objects>` representing each group, including the\ngrouping, the grouping *key*, and the set of elements.\n\n.. code-block:: edgeql-repl\n\n  db> group Movie { title } by .release_year;\n  {\n    {\n      key: {release_year: 2017},\n      grouping: {'release_year'},\n      elements: {\n        default::Movie {title: 'Guardians of the Galaxy Vol. 2'},\n        default::Movie {title: 'Spider-Man: Homecoming'},\n        default::Movie {title: 'Thor: Ragnarok'},\n      },\n    },\n    {\n      key: {release_year: 2013},\n      grouping: {'release_year'},\n      elements: {\n        default::Movie {title: 'Iron Man 3'},\n        default::Movie {title: 'Thor: The Dark World'},\n      },\n    },\n    ...\n  }\n\nBrowse the :ref:`docs <ref_eql_group>` for more details and examples, or refer\nto the original `RFC 1009 <group_>`_.\n\n\nGlobal variables\n----------------\n\nYour schema can now contain :ref:`global variables <ref_datamodel_globals>`.\nThese are contextual variables that are provided by the client and can be\nreferenced in your queries and schema.\n\n.. code-block:: sdl\n\n  global current_user -> uuid;\n\n.. code-block:: edgeql\n\n  select User filter .id = global current_user;\n\nClient libraries have been updated to provide method for attaching global\nvariables to a ``Client`` instance; these values are sent along with all\nqueries originating from that ``Client``.\n\n.. tabs::\n\n  .. code-tab:: typescript\n\n    import {createClient} from 'edgedb';\n\n    const client = createClient().withGlobals({\n      current_user: '2141a5b4-5634-4ccc-b835-437863534c51',\n    });\n\n    await client.query(`select global current_user;`);\n\n  .. code-tab:: python\n\n    from edgedb import create_client\n\n    client = create_client().with_globals({\n        'current_user': '580cc652-8ab8-4a20-8db9-4c79a4b1fd81'\n    })\n\n    result = client.query(\"\"\"\n        select global current_user;\n    \"\"\")\n\n  .. code-tab:: go\n\n    package main\n\n    import (\n      \"context\"\n      \"fmt\"\n      \"log\"\n\n      \"github.com/edgedb/edgedb-go\"\n    )\n\n    func main() {\n      ctx := context.Background()\n      client, err := edgedb.CreateClient(ctx, edgedb.Options{})\n      if err != nil {\n        log.Fatal(err)\n      }\n      defer client.Close()\n\n      id, err := edgedb.ParseUUID(\"2141a5b4-5634-4ccc-b835-437863534c51\")\n      if err != nil {\n        log.Fatal(err)\n      }\n\n      var result edgedb.UUID\n      err = client.\n        WithGlobals(map[string]interface{}{\"current_user\": id}).\n        QuerySingle(ctx, \"SELECT global current_user;\", &result)\n      if err != nil {\n        log.Fatal(err)\n      }\n\n      fmt.Println(result)\n    }\n\n\n\nGlobals are primarily intended as an enabling mechanism for object-level\nsecurity.\n\nObject-level security\n---------------------\n\nObject types can now be augmented with object-level access policies. When\ncombined with global variables, access policies can be used to\npush authorization logic into the database.\n\n.. code-block:: sdl\n\n  global current_user -> uuid;\n\n  type User {\n    required property email -> str { constraint exclusive; };\n  }\n\n  type BlogPost {\n    required property title -> str;\n    link author -> User;\n    access policy own_posts allow all using (\n      .author.id ?= global current_user\n    )\n  }\n\nRefer to :ref:`the docs <ref_datamodel_access_policies>` or `RFC 1011\n<acls_>`_ for full details.\n\n\nRange types\n-----------\n\nEdgeDB now supports :ref:`range types <ref_std_range>` representing intervals\nof values.\n\n.. code-block:: edgeql-repl\n\n  db> select range(1, 10);\n  {range(1, 10, inc_lower := true, inc_upper := false)}\n  db> select range_unpack(range(1, 10))\n  {1, 2, 3, 4, 5, 6, 7, 8, 9}\n\nThe ``cal::date_duration`` type\n-------------------------------\n\nThis release also introduces a new datatype :eql:type:`cal::date_duration` to\nrepresent a span of *months/days*. It is nearly equivalent to the existing\n:eql:type:`cal::relative_duration` but cannot represent sub-day durations.\n\nThis type is primarily intended to simplify :eql:type:`cal::local_date` logic.\n\n.. code-block::\n\n  db> select <cal::local_date>'2022-06-25' +\n  ...   <cal::date_duration>'5 days';\n  {<cal::local_date>'2022-06-30'}\n  db> select <cal::local_date>'2022-06-30' -\n  ...   <cal::local_date>'2022-06-25';\n  {<cal::date_duration>'P5D'}\n\nSource deletion policies\n------------------------\n\nAdd deletion cascade functionality with ``on source delete``.\n\n.. code-block:: sdl\n\n  type BlogPost {\n    property title -> str;\n  }\n\n  type Person {\n    multi link posts -> BlogPost {\n      on source delete delete target;\n    }\n  }\n\nUnder this policy, deleting a ``User`` will unconditionally delete its\n``posts`` as well.\n\nTo avoid deleting a ``Post`` that is linked to by other schema entities,\nappend ``if orphan``.\n\n.. code-block:: sdl-diff\n\n    type Person {\n      multi link posts -> BlogPost {\n  -     on source delete delete target;\n  +     on source delete delete target if orphan;\n      }\n    }\n\n\n\nAdditional changes\n==================\n\nEdgeQL\n------\n\n* Support additional operations on local date and time types,\n  including :eql:func:`duration_get`,\n  :eql:func:`cal::duration_normalize_hours`, and\n  :eql:func:`cal::duration_normalize_days`. Per `RFC 1013 <dates_>`_.\n\n\n* Support user-provided values for the ``id`` property when inserting objects\n  (:eql:gh:`#3895`). This can be useful when migrating data from\n  an existing database.\n\n  .. code-block::\n\n    insert User {\n      id := <uuid>\"5abf67cc-9f9f-4bbc-b009-d117d463a12e\",\n      email := \"jayz@example.com\"\n    }\n\n* Support partial constraints and indexes (:eql:gh:`#3949`,\n  :ref:`docs <ref_datamodel_constraints_partial>`).\n\n* Add the new :eql:func:`json_set` function (:eql:gh:`#4118`).\n\n\nServer\n------\n\n* Support socket activation to reduce memory footprint on developer\n  machines (:eql:gh:`#3899`).\n\n* Introduce edgedb+http, a which tunnels the binary protocol over HTTP\n  using JWT for authentication (:eql:gh:`#3979`).\n\n* Support using JWT to authenticate to local instances (:eql:gh:`#3991`).\n\n\nBug fixes\n---------\n\n* Generate unique ``id`` fields for each free shape object,\n  and don't use an actual in-database object to represent it,\n  and make multiplicity inference understand free shapes better\n  (:eql:gh:`#3631`, :eql:gh:`#3633`, :eql:gh:`#3634`).\n\n* Fail if local Postgres cluster fails to start.\n\n* Add ``cfg::memory`` to base types descriptor IDs table (:eql:gh:`#3882`).\n\n* Fix a cross-type exclusive constraint bug that could allow exclusive\n  constraints to be violated in some complex type hierarchies\n  (:eql:gh:`#3887`).\n\n* Fix issue where server might attempt to acquire one more connection\n  than it is configured to permit (:eql:gh:`#3901`).\n\n* Fix use of ``assert_exists`` on properties that are being directly output\n  (:eql:gh:`#3911`).\n\n* Fix a scope leakage that could cause a link referenced inside a computable\n  to improperly correlate with something outside the computable\n  (:eql:gh:`#3912`).\n\n* Fix a number of issues with the floordiv (``//``) and modulus (``%``)\n  operators where we could return incorrect values or produce spurious\n  errors, especially on very large values (:eql:gh:`#3909`).\n\n* Allow adding annotations to ``abstract annotation`` definitions\n  (:eql:gh:`#3929`).\n\n* Expose ``body`` and ``language`` fields on ``schema::Function``\n  (:eql:gh:`#3944`).\n\n* Make indexes extend from ``schema::InheritingObject`` (:eql:gh:`#3942`).\n\n* Fix some mis-compilations of nested shapes inside calls to functions\n  like ``assert_single`` (:eql:gh:`#3927`).\n\n* Fix ``SET TYPE`` on properties with default values (:eql:gh:`#3954`).\n\n* Fix ``describe``/``populate``/``describe`` sequence (:eql:gh:`#3959`).\n\n* Upgrade many casts and functions from \"Stable\" to \"Immutable\"\n  (:eql:gh:`#3975`).\n\n* Fix link properties in type filtered shape links (:eql:gh:`#3987`).\n\n* Allow DML statements in free shapes (:eql:gh:`#4002`).\n\n* Allow customizing assertion messages in ``assert_exists`` and friends\n  (:eql:gh:`#4019`).\n\nProtocol overhaul\n-----------------\n\n* A new version of the protocol---version 1.0---has been introduced.\n  It eliminates all server state associated with connections that\n  do not use transactions.\n\n* Support passing parameters to and returning values from multi-statement\n  scripts.\n\n2.1\n===\n* Fix global defaults with nontrivial computation\n  (:eql:gh:`#4182`)\n\n* Fix migration that removes policy using clause\n  (:eql:gh:`#4183`)\n\n* Support ELSE-less UNLESS CONFLICT on explicit id INSERT\n  (:eql:gh:`#4185`)\n\n* Don't create constraints on derived views when adding a pointer to a type\n  (:eql:gh:`#4187`)\n\n* Fix a bunch of missing source contexts in declarative\n  (:eql:gh:`#4188`)\n\n* Fix an ISE when a computed link is directly a property reference\n  (:eql:gh:`#4193`)\n\n* Fix an ISE when using an empty shape in some contexts\n  (:eql:gh:`#4194`)\n\n* Fix a number of error messages involving collection types in schemas\n  (:eql:gh:`#4195`)\n\n* Avoid doing semi-joins after a sequence of single links\n  (:eql:gh:`#4196`)\n\n* Make range() properly strict in its non-optional arguments\n  (:eql:gh:`#4207`)\n\n* Allow multiple FDs per socket in activation\n  (:eql:gh:`#4189`)\n\n* Add SCRAM authentication over HTTP\n  (:eql:gh:`#4197`)\n\n* Always arm auto-shutdown timer when it's greater than zero\n  (:eql:gh:`#4214`)\n\n* Fix json -> array<json> cast of '[]'\n  (:eql:gh:`#4217`)\n\n2.2\n===\n* Support UNLESS CONFLICT ON for pointers with DML in them\n  (:eql:gh:`#4357`)\n\n* Fix cardinality in CommandDataDescription\n  (:eql:gh:`#4347`)\n\n* Prevent access rule hidden ids from leaking when accessed directly\n  (:eql:gh:`#4339`)\n\n* Better messages for required links hidden by policies\n  (:eql:gh:`#4338`)\n\n* Fix access policies on DELETE of a UNION type\n  (:eql:gh:`#4337`)\n\n* Strip out all views from DML subjects when computing what tables to use\n  (:eql:gh:`#4336`, :eql:gh:`#4333`)\n\n* Fix interaction between access policies and omitted fields in insert\n  (:eql:gh:`#4332`, :eql:gh:`#4219`)\n\n* Fix a tracer issue with reverse links and IS\n  (:eql:gh:`#4331`)\n\n* Don't include union types in link triggers\n  (:eql:gh:`#4329`, :eql:gh:`#4320`)\n\n  If you encounter this issue, after upgrading to a version with this\n  patch, it can be fixed by doing a dump/restore or by adding a new link\n  to the affected type.\n\n* Require ON for constraints on objects\n  (:eql:gh:`#4324`, :eql:gh:`#4268`)\n\n* Fix interaction between DETACHED and aliases/globals\n  (:eql:gh:`#4321`, :eql:gh:`#4258`)\n\n* Disable access policy rewrite when compiling constraints\n  (:eql:gh:`#4248`, :eql:gh:`#4245`)\n\n* Expose ``--admin-ui`` as an environment variable and document it\n  (:eql:gh:`#4255`)\n\n* Prevent ``HttpProtocol.close`` from crashing on closed client connection\n  (:eql:gh:`#4238`)\n\n* Fix permitted JSON null in nested array cast\n  (:eql:gh:`#4221`)\n\n* Fix ``range_unpack`` boundary bug.\n\n  The ``range_unpack`` function was incorrectly excluding values close to\n  boundary, especially when the boundary was not itself inclusive.\n  (:eql:gh:`#4282`)\n\n* UI: Allow selection of read-only properties in data editor\n  (:eql:gh:`edgedb/edgedb-ui/#65`)\n\n* UI: Hide subtype columns in data editor by default;\n  add a toggle to show them.\n  (:eql:gh:`edgedb/edgedb-ui/#43`)\n\n* UI: Add \"create example database\" to the database selection\n  screen.\n  (:eql:gh:`edgedb/edgedb-ui/#61`)\n\n* UI: Fix navigation from being reset on switching\n  the UI panes.\n  (:eql:gh:`edgedb/edgedb-ui/#61`)\n\n* UI: Fix rendering of range types.\n  (:eql:gh:`edgedb/edgedb-ui/#61`)\n\n* UI: Fix the data editor UI to render types that have\n  some properties or links masked by an access policy.\n  (:eql:gh:`edgedb/edgedb-ui/#61`)\n\n* UI: Implement login page for remote instances.\n  (:eql:gh:`edgedb/edgedb-ui/#40`)\n\n2.3\n===\n\n* Clarify error message when UI is not enabled\n  (:eql:gh:`#4256`)\n\n* Fix an issue with inherited computeds\n  (:eql:gh:`#4371`)\n\n* Fix bug in diamond pattern constraint inheritance\n  (:eql:gh:`#4379`)\n\n* When finding common parent for arrays, never use expr alias arrays\n  (:eql:gh:`#4080`)\n\n* Properly quote numeric names when in codegen\n  (:eql:gh:`#4344`)\n\n* Fix computed global scoping behavior\n  (:eql:gh:`#4388`)\n\n* Fix DDL performance issues on databases with lots of data\n  (:eql:gh:`#4401`)\n\n* Fix potentially missed constraints on DML\n  (:eql:gh:`#4410`)\n\n* Fix slicing with an empty set\n  (:eql:gh:`#4404`)\n\n* Fix slicing array of tuples\n  (:eql:gh:`#4391`)\n\n* Don't apply access policies when compiling indexes\n  (:eql:gh:`#4420`)\n\n* Fix slicing of tuple arrays with null inputs\n  (:eql:gh:`#4421`)\n\n* Propagate database creation and deletion events to adjacent servers\n  (:eql:gh:`#4415`)\n\n2.4\n===\n\n* Fix database initialization on hosted environments like Heroku.\n  (:eql:gh:`#4432`)\n\n* Prevent spurious errors when using backlinks on types that have\n  properties with the same name but different types\n  (:eql:gh:`#4443`)\n\n* Fix some spurious errors when removing a link from the schema.\n  (:eql:gh:`#4451`)\n\n* For query_single, only check that the *last* query in a script is single.\n  (:eql:gh:`#4453`)\n\n* Catch when POPULATE MIGRATION generates incorrect DDL. This should prevent\n  bugs where the schema can get into wedged states.\n  (:eql:gh:`#4484`)\n\n* workflows: Publish multiarch Docker images\n  (:eql:gh:`#4486`)\n\n* Make unused param insertion in the sql compiler more reliable\n  (:eql:gh:`#4497`)\n\n* Properly propagate creation and deletion of extensions\n\n* Fix potential exclusive constraint violations when doing an UPDATE\n  on a union\n  (:eql:gh:`#4507`)\n\n* Don't lose type from inheritance views when rebasing\n  (:eql:gh:`#4509`)\n\n* Make object type descriptor ids be derived from type name\n  (:eql:gh:`#4503`)\n\n* Check for invalid arrays arguments at the protocol level\n  (:eql:gh:`#4511`)\n\n* Fix SET REQUIRED on newly created properties with alias subtypes\n  (:eql:gh:`#4513`)\n\n* Make newly created link properties get added to the relevant alias types\n  (:eql:gh:`#4512`)\n\n* Fix handling of link properties named ``id``\n  (:eql:gh:`#4514`)\n\n* Disallow queries using conflict machinery on a link property. This\n  prevents certain potential exclusive constraint violations that were\n  not handled correctly.\n  (:eql:gh:`#4515`)\n\n* Fix performing multiple deletions at once in the UI\n  (:eql:gh:`#4523`)\n\n* Fix casting empty sets to built in enum types\n  (:eql:gh:`#4532`)\n\n* Produce better error messages when using ``enum`` incorrectly\n  (:eql:gh:`#4527`)\n\n* Make ``'\\b'`` produce the correct value in string and bytes literals\n  (:eql:gh:`#4535`)\n\n2.5\n===\n\n* Properly infer cardinality of empty array as ONE\n  (:eql:gh:`#4533`)\n\n* Fix several issues that manifest when using GROUP BY\n  (:eql:gh:`#4549`, :eql:gh:`#4439`)\n\n* Fix migration scripts when combined with access policies\n  (:eql:gh:`#4553`)\n\n* Fix failure when a ``ALTER ... EXTENDING`` doesn't change the\n  set of ancestors\n  (:eql:gh:`#4554`)\n\n* Fix ``UNLESS CONFLICT ON`` for a not-inserted property\n  (:eql:gh:`#4556`)\n\n* Fix access policies that use shapes internally\n  (:eql:gh:`#4555`)\n\n* Allow overloading ``__type__`` with a computed in shapes\n  (:eql:gh:`#4557`)\n\n2.6\n===\n\nNonrecursive access policies and future behaviors\n-------------------------------------------------\n\nStarting with EdgeDB 3.0, access policy restrictions will **not** be applied\nwhile evaluating other access policy expressions (:eql:gh:`#4574`).\n\nIt is possible (and recommended) to enable this :ref:`future\n<ref_eql_sdl_future>` behavior in EdgeDB 2.6 by adding the\nfollowing to the schema: ``using future nonrecursive_access_policies;``\n\nFor more details, see :ref:`the docs <nonrecursive>`.\n\nTo enable opting in to this behavior, 2.6 adds a general mechanism\nto opt into :ref:`future <ref_eql_sdl_future>` behavior changes\n(:eql:gh:`#4574`, :eql:gh:`#4606`).\n\nOther changes\n-------------\n\n* Fix passing zero dimensional array as arguments. This was a regression\n  introduced in 2.4, and affected passing empty arrays from the the Rust\n  bindings.\n  (:eql:gh:`#4511`)\n\n* Require that constraint expressions be immutable\n  (:eql:gh:`#4593`)\n\n* Only permit valid UUID-generation functions to be made the default value\n  for ``id``\n  (:eql:gh:`#4616`)\n\n* UI: New mechanism for copying data in REPL and in Data Editor. Hover over\n  a data line and click the context \"COPY\" button.\n\n  .. image:: images/v2_ui_copy.jpg\n    :width: 100%\n\n* UI: \"Disable Access Policies\" and \"Persist Query\" options in REPL remember\n  their state between page refreshes.\n\n* UI: Basic autocomplete now works for ``INSERT``, ``UPDATE``, and ``DELETE``\n  queries.\n\n2.7\n===\n\n* Improve error messages when compiling pointer default\n  (:eql:gh:`#4624`)\n\n* Fix using WITH-bound DML from an UPDATE in an ELSE clause\n  (:eql:gh:`#4641`)\n\n* Allow WITH MODULE in ddl in CREATE MIGRATION\n  (:eql:gh:`#4668`)\n\n* Fix some broken casts from object types to JSON\n  (:eql:gh:`#4663`)\n\n* Fix putting a statement as the body of an access policy\n  (:eql:gh:`#4667`)\n\n* Loosen the rules on when we produce a \"would change the\n  interpretation\" error. It is not only produced when a link is being\n  used, not a property. (:eql:gh:`#4643`)\n\n* Fix certain errors involving default values in access policies\n  (:eql:gh:`#4679`)\n\n* Avoid ISE when pickling DynamicRangeVar\n  (:eql:gh:`#4681`)\n\n* Fix ``max_ex_value`` constraint.\n  (:eql:gh:`#4671`)\n\n* Fix SET GLOBAL capabilities to no longer break in the CLI.\n  (:eql:gh:`#4688`)\n\n* Fix links to ``schema::ObjectType`` breaking DROP TYPE.  If you have\n  a link in your schema to ``schema::ObjectType`` or one of its\n  ancestors and you encounter internal server errors when trying to\n  drop a type, it should be possible to repair your database by\n  creating and then deleting a new link to ``schema::ObjectType``.\n  (:eql:gh:`#4670`)\n\n* Don't insert unnecessary ``assert_exists`` calls on required links\n  inside access policies bodies in some cases.\n  (:eql:gh:`#4695`)\n\n2.8\n===\n\n* Fix DML access policies that use shapes internally\n  (:eql:gh:`#4589`)\n\n* Give a proper error message creating a migration with a USING that has DML\n  (:eql:gh:`#4707`)\n\n* Don't incorrectly evaluate DML access policies when elements are also DML.\n  This fixes some cases in which policies would pass incorrectly.\n  (:eql:gh:`#4745`)\n\n* Fix direct use of ``__subject__`` from insert access policies\n  (:eql:gh:`#4752`)\n\n* Produce an error message on casts to and literal references of enum types\n  from indexes and constraints. Currently we generate an internal server\n  error. A real fix unfortunately must wait for 3.0 for technical reasons.\n  (:eql:gh:`#4754`)\n\n* Only apply filter cardinality inference to unique sets\n  (:eql:gh:`#4763`)\n\n* Fix changing a link to non-computed and single at the same time\n  (:eql:gh:`#4764`)\n\n* Fix error message for function calls on derived types\n  (:eql:gh:`#4757`)\n\n* Fix deleting certain complex aliases\n  (:eql:gh:`#4777`)\n\n* Fix link properties on inherited backlinks\n  (:eql:gh:`#4788`)\n\n* Fix using array/string/bytes/json subscripting inside of indexes and\n  constraints.\n  (:eql:gh:`#4760`)\n\n* Fix apparent startup hangs due to a lock fd leaking into postgres\n  (:eql:gh:`#4797`)\n\n* Fix some migrations with tricky constraint/computed interactions\n  (:eql:gh:`#4794`)\n\n2.9\n===\n\n* Fix broken DROPs of pointers in some multiple-inheritance situations\n  (:eql:gh:`#4809`)\n\n* Properly execute function calls in UPDATE once per object\n  (:eql:gh:`#4810`)\n\n* Fix accessing tuple elements on link properties\n  (:eql:gh:`#4811`)\n\n* Fix ``assert_exists()`` not firing on some tuple values\n  (:eql:gh:`#4812`)\n\n* Fix GROUP on the result of enumerate\n  (:eql:gh:`#4813`)\n\n* Support more env vars in args.py, standardize docs\n  (:eql:gh:`#4387`)\n\n* Fix computed properties that just copy id\n  (:eql:gh:`#4807`)\n\n* Fix backlinks on derived union types\n  (:eql:gh:`#4818`)\n\n* Fix references to the enclosing type in schema-defined computeds\n  (:eql:gh:`#4826`)\n\n* UI: Fix regression introduced in 2.8 when editing empty string\n  fields in data explorer\n\n* UI: Improvements to handling of union link targets in schema and\n  data explorer views\n\n* UI: Fix loading indicators on tabs\n\n2.10\n====\n\n* Fix mismatch in session state after a ``ROLLBACK``\n\n* Fix ISE when doing set default on an abstract pointer\n  (:eql:gh:`#4843`)\n\n* Fix accesses to ``__type__`` from insert access policies\n  (:eql:gh:`#4865`)\n\n* Properly forbid aggregation in index expressions\n  (:eql:gh:`#4869`)\n\n* Fix ``grouping`` field when grouping by one key or nothing\n  (:eql:gh:`#4906`)\n\n* Fix two issues with mutation in free objects\n  (:eql:gh:`#4902`)\n\n* Only allow type names as the subject of an insert. (Previously dotted paths\n  were allowed, with nonsensical behavior.)\n  (:eql:gh:`#4922`)\n\n* Fix array arguments in HTTP interface\n  (:eql:gh:`#4956`)\n\n* Support multi properties in ``UNLESS CONFLICT ON``\n  (:eql:gh:`#4955`)\n\n* Fix polymorphic type tests on result of update\n  (:eql:gh:`#4954`)\n\n* Optimize trivial ``WITH`` -bound ``GROUP`` uses\n  (:eql:gh:`#4978`)\n\n* Fix a category of confusing scoping related bugs in access policies\n  (:eql:gh:`#4994`)\n\n* Get rid of the \"unused alias definition\" error.\n  (:eql:gh:`#4819`)\n\n* Support mutation in ``USING`` expressions when changing a link to\n  ``required`` or to ``single`` during a migration\n  (:eql:gh:`#4873`)\n\n* Fix custom function calls on the HTTP interface\n  (:eql:gh:`#4998`)\n\n* Avoid infinite recursion in some do-nothing intersection cases\n  (:eql:gh:`#5007`)\n\n* Don't mangle cast error messages when the cast value contains a type name\n  (:eql:gh:`#5008`)\n\n* Allow JWT token auth in binary protocol\n  (:eql:gh:`#4830`)\n\n* Use prepared statement cache in EdgeQL script execution\n  (:eql:gh:`#4931`)\n\n* Fix non-transactional commands like ``DROP DATABASE`` when using\n  Postgres 14.7\n  (:eql:gh:`#5026`)\n\n* Update packaged Postgres to 14.7\n\n* Fix ``set single`` on required properties\n  (:eql:gh:`#5031`)\n\n* Fix a ISE when using assert_exists and linkprops using query builder\n  (:eql:gh:`#4961`)\n\n2.11\n====\n\n* Fix adding a link property with a default value to an existing link\n  (a regression in 2.10)\n  (:eql:gh:`#5061`)\n\n2.12\n====\n\n* Fix GROUP regression with some query-builder queries\n  (a regression in 2.10)\n  (:eql:gh:`#5073`)\n\n2.13\n====\n* Implement a MIGRATION REWRITE system. This provides a mechanism for\n  safely rewriting the migration history of a database while ensuring\n  that the new history produces the same result as the old\n  history. CLI tooling to take advantage of this feature is coming\n  soon.\n  (:eql:gh:`#4585`)\n\n* Fix DigitalOcean support: allow its custom error in bootstrap\n  (:eql:gh:`#5139`)\n\n* Add a hint to the error message about link targets.\n  (:eql:gh:`#5131`)\n\n* Infer cardinality of ``required multi`` pointers as AT_LEAST_ONE\n  (:eql:gh:`#5180`)\n\n* Fix dump/restore of migrations with messages on them\n  (:eql:gh:`#5171`)\n\n* Fix interaction of link properties and ``assert_exists`` and similar\n  (:eql:gh:`#5182`)\n\n* Make ``assert_single`` and similar not lose track of values updated\n  in an ``UPDATE`` in their argument.\n  (:eql:gh:`#5088`)\n\n* Add a test for assert_exists+assert_single+UPDATE\n  (:eql:gh:`#5242`)\n\n* Add support for new JWT layout\n  (:eql:gh:`#5197`)\n\n* Fix uses of volatile expressions in update write access policies\n  (:eql:gh:`#5256`)\n\n* Allow globals to be used in defaults\n  (:eql:gh:`#5268`)\n\n* Fix errmessage interpolation to not produce an internal server error on\n  braces in a message. Allow ``{{`` and ``}}`` to be used to escape braces.\n  (:eql:gh:`#5295`)\n\n2.14\n====\n\nSchema repair on upgrades\n-------------------------\n\nPreviously, certain bug fixes and changes (such as the fix to\ncardinality inferenced of ``required multi`` pointers released in 2.13\n(:eql:gh:`#5180`)), could cause schemas to enter an inconsistent state\nfrom which many migrations were not possible.\n\nThe cause of this problem is that an incorrectly inferred value (such\nas the cardinality of a computed property) may have been computed and\nstored in a previous version. When a newer version is used, there will\nbe a mismatch between the correctly inferred value on the new version,\nand the incorrectly stored value in the database's schema.\n\nThe most straightforward way to fix such problems was to perform a dump\nand then a restore.\n\nTo fix this, we have introduced a schema repair mechanism that will\nrun when upgrading a database to 2.14. This repair mechanism will fix\nany incorrectly inferred fields that are stored in the schema.\n\nOne particular hazard in this, however, is that the repair is not\neasily reversible if you need to downgrade to an earlier version.\n**We recommend performing a dump before upgrading to 2.14.**\n\nThese changes were made in (:eql:gh:`#5337`); more discussion of the issue\ncan be found in (:eql:gh:`#5321`).\n\nOther changes\n-------------\n\n* Correctly display constraint errors on ``id``\n  (:eql:gh:`#5344`)\n\n* Fix adding certain computed links to a type with an alias\n  (:eql:gh:`#5329`)\n\n2.15\n====\n* In multi-server instances, properly reload schema after a restore\n  (:eql:gh:`#5463`)\n\n* Fix several bugs synchronizing configuration state\n\n* Fix dropping a pointer's constraint and making it computed at the same time\n  (:eql:gh:`#5411`)\n\n* Don't claim that making a pointer computed is data-safe\n  (:eql:gh:`#5412`)\n\n* Prohibit NUL character in query source\n  (:eql:gh:`#5414`)\n\n* Fix migration that delete an link alias computed in a parent and child\n  (:eql:gh:`#5428`)\n\n* Fix GraphQL updates for multi links.\n  (:eql:gh:`#4260`)\n\n* Fix altering enum that is used in a tuple\n  (:eql:gh:`#5445`)\n\n* Fix changing cardinality of properties on types used in unions\n  (:eql:gh:`#5457`)\n\n* Enable GraphQL support for type unions.\n\n* Fix making pointer non-computed and giving it an abstract base at the\n  same time\n  (:eql:gh:`#5458`)\n\n* Make json casts of object arrays not include extra fields\n  (:eql:gh:`#5484`)\n\n* Make coalesce infer a union type\n  (:eql:gh:`#5472`)\n\n2.16\n====\n* Fix ISEs involving single link with only computed link properties\n  (:eql:gh:`#5499`)\n\n* Don't ISE on free shape in insert\n  (:eql:gh:`#5438`)\n\n* Always set cardinality of derived ``__tname__`` properties\n  (:eql:gh:`#5508`)\n\n* Work around mysterious postgres crashes on Digital Ocean\n  (:eql:gh:`#5505`)\n\n* Make reestablishing system postgres connections more resilient\n  (:eql:gh:`#5511`)\n\n* Fix graphql queries made against a freshly started server\n  (:eql:gh:`#5456`)\n\n* Support dumping databases with ``std::duration`` config vals\n  (:eql:gh:`#5528`)\n\n* Fix ISE for TypeExprs in function returns\n  (:eql:gh:`#5540`)\n\n* Support comparisons on durations in static eval\n\n* Fix create database conflicts with name in schema\n  (:eql:gh:`#5515`)\n\n* Fix range and bytes output in json-lines/json-pretty mode\n  (:eql:gh:`#5572`)\n\n* Include source locations in more scoping error messages\n  (:eql:gh:`#5573`)\n\n* Fix an optional scoping bug with important access policy implications\n  (:eql:gh:`#5575`)\n\n.. lint-off\n\n.. _group:\n    https://github.com/edgedb/rfcs/blob/master/text/1009-group.rst\n.. _globals:\n    https://github.com/edgedb/rfcs/blob/master/text/1010-global-vars.rst\n.. _acls:\n    https://github.com/edgedb/rfcs/blob/master/text/1011-object-level-security.rst\n.. _range:\n    https://github.com/edgedb/rfcs/blob/master/text/1012-range-types.rst\n.. _dates:\n    https://github.com/edgedb/rfcs/blob/master/text/1013-datetime-arithmetic.rst\n.. _v2anno:\n    https://www.edgedb.com/blog/edgedb-2-0\n\n.. lint-on\n\n\n.. _v2anno:\n    https://www.edgedb.com/blog/edgedb-2-0\n"
  },
  {
    "path": "docs/resources/changelog/3_x.rst",
    "content": "====\nv3.0\n====\n\n:edb-alt-title: EdgeDB v3\n\n.. image:: images/v3_betelgeuse.jpg\n    :width: 100%\n\n|EdgeDB| 3.0 was released on June 22nd, 2023. Read the announcement\nblog post `here <v3anno_>`_.\n\nTo play with the new features, install the CLI using `our installation guide\n<https://www.edgedb.com/install>`_ and initialize a new project.\n\n.. code-block:: bash\n\n  $ edgedb project init\n\n\nUpgrading\n=========\n\n**Local instances**\n\nTo upgrade a local project, first ensure that your CLI is up to date with\n``edgedb cli upgrade``. Then run the following command inside the project\ndirectory.\n\n.. code-block:: bash\n\n  $ edgedb project upgrade\n\nAlternatively, specify an instance name if you aren't using a project.\n\n.. code-block:: bash\n\n  $ edgedb instance upgrade -I my_instance\n\nThe CLI will first check to see if your schema will migrate cleanly to EdgeDB\n3.0.\n\n.. note::\n\n    EdgeDB 3.0 fixes a bug that will cause it to care about the ordering of\n    your ancestors in multiple inheritence. This used to work before 3.0:\n\n    .. code-block:: sdl\n\n        type A;\n        type B extending A;\n        type C extending A, B;\n\n    but as of 3.0, the order of ancestors must be changed to match the order of\n    the bases:\n\n    .. code-block:: sdl\n\n        type A;\n        type B extending A;\n        type C extending B, A;\n\n    This is a key instance where schemas may be incompatible with 3.0.\n\nIf the upgrade check finds any problems, it will report them back to you.\nIf you receive the following error, you will need to check the syntax of your\nschema:\n\n.. code-block::\n\n    SchemaError: <some error>\n    Schema incompatibilities found. Please fix the errors above to proceed.\n    Hint: For faster feedback loop use:\n        edgedb migration upgrade-check --watch\n\nYou might also receive a different error which indicates the issue is within\nyour migrations. In this case, the error can be fixed by squashing, as\nindicated by the error:\n\n.. code-block::\n\n    The current schema is compatible. But some of the migrations are outdated.\n    Please squash all migrations to fix the issue:\n        edgedb migration create --squash\n\n**Hosted instances**\n\nTo upgrade a remote (hosted) instance, we recommend the following\ndump-and-restore process.\n\n1. Spin up an empty 3.0 instance. You can use one of our :ref:`deployment\n   guides <ref_guide_deployment>`, but you will need to modify some of the\n   commands to use our testing channel and the beta release.\n\n   Under Debian/Ubuntu, when adding the EdgeDB package repository, use this\n   command instead:\n\n   .. code-block:: bash\n\n       $ echo deb [signed-by=/usr/local/share/keyrings/edgedb-keyring.gpg] \\\n           https://packages.edgedb.com/apt \\\n           $(grep \"VERSION_CODENAME=\" /etc/os-release | cut -d= -f2) main \\\n           | sudo tee /etc/apt/sources.list.d/edgedb.list\n\n   Use this command for installation under Debian/Ubuntu:\n\n   .. code-block:: bash\n\n       $ sudo apt-get update && sudo apt-get install edgedb-3\n\n   Under CentOS/RHEL, use this installation command:\n\n   .. code-block:: bash\n\n       $ sudo yum install edgedb-3\n\n   In any required ``systemctl`` commands, replace ``edgedb-server-2`` with\n   ``edgedb-server-3``.\n\n   Under any Docker setups, supply the ``3.0`` tag.\n\n2. Take your application offline, then dump your v2.x database with the CLI\n\n   .. code-block:: bash\n\n       $ gel dump --dsn <old dsn> --all --format dir my_database.dump/\n\n   This will dump the schema and contents of your current database to a\n   directory on your local disk called ``my_database.dump``. The directory name\n   isn't important.\n\n3. Restore the empty v3.x instance from the dump\n\n   .. code-block:: bash\n\n       $ gel restore --all my_database.dump/ --dsn <new dsn>\n\n   Once the restore is complete, update your application to connect to the new\n   instance.\n\n   This process will involve some downtime, specifically during steps 2 and 3.\n\n.. note::\n\n    If your Postgres cluster is also backing other versions of EdgeDB, make\n    sure you start your new instance with the ``--ignore-other-tenants``\n    option when bootstrapping your new instance.\n\n**Pre-1.0 Instances**\n\nIf you're still running pre-1.0 EdgeDB instances (e.g., 1.0-beta3) and want to\nupgrade to 3.0, we recommend you upgrade to version 2.x first, followed by\nanother upgrade to 3.0, both using the same dump-and-restore process.\n\n\nClient libraries\n----------------\n\nMany of the client libraries have gained code generation capabilities since our\n2.0 release. Look for new releases of all of our client libraries soon which\nwill support all 3.0 features.\n\n\nNew features\n============\n\nSimplified SDL syntax\n---------------------\n\nAs part of our commitment to delivering the best developer experience in\ndatabases, we've made our schema definition language (or SDL) easier to use.\nYou're no longer required to use the ``property`` or ``link`` keywords for\nnon-computed properties and links. Also, we've replaced arrows with colons for\na cleaner look that's easier to type.\n\n.. note::\n\n    If you prefer the arrow syntax of pre-3.0, feel free to keep using it. That\n    syntax is still fully supported.\n\nThis change paves the way for a future syntax for declaring ad-hoc types in\nqueries and functions. (Read more about it in `the free types RFC\n<https://github.com/edgedb/rfcs/blob/master/text/1022-freetypes.rst>`_.)\n\nThat means that this type definition:\n\n.. code-block:: sdl\n\n    type User {\n      required property email -> str;\n      multi link friends -> User;\n    }\n\ncould be replaced with this equivalent one in EdgeDB 3+:\n\n.. code-block:: sdl\n\n    type User {\n      required email: str;\n      multi friends: User;\n    }\n\nSelecting \"v3\" from the version dropdown in the sidebar will update SDL code in\nversioned sections of the documentation to the new syntax.\n\n\nQuery performance analysis\n--------------------------\n\n.. edb:youtube-embed:: WoHJu0nq5z0\n\nAmong other improvements, the UI now includes a visual query analyzer to help\nyou tweak performance on your EdgeQL queries. Just drop the ``analyze`` keyword\nin front of your query in the UI's \"Query Editor\" tab to see the query analyzer\nin action.\n\n.. image:: images/v3_ui_query_planner.jpg\n    :width: 100%\n\nQuery analysis is available in the CLI REPL by prepending your query with\n``analyze`` or using the ``\\analyze`` backslash command, and in the CLI\ndirectly using the ``edgedb analyze <query>`` command.\n\n\nUI improvements\n---------------\n\n.. edb:youtube-embed:: iwnP_6tkKgc\n\nThe EdgeDB UI got a lot of love in this release. In addition to the visual\nquery planning shown above, you'll see a number of improvements.\n\nNew UI for setting globals and configuration\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nWe've made it easier to set your globals and change configuration.\n\n.. image:: images/v3_ui_query_config.png\n    :width: 100%\n\nNew UI REPL\n^^^^^^^^^^^\n\nThe UI's redesigned REPL makes it easy to drill into values and copy parts of\nyour query results to the clipboard.\n\n.. image:: images/v3_ui_repl.png\n    :width: 100%\n\nQuery editor and visual builder\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nThe query editor has a great new on-demand UI for setting parameters.\n\n.. image:: images/v3_ui_query_editor.png\n    :width: 100%\n\nIt also comes with a visual query builder which makes it easy to write queries,\neven when you're just learning EdgeQL.\n\n.. image:: images/v3_ui_query_builder.png\n    :width: 100%\n\n\n``edgedb watch`` and a new development workflow\n-----------------------------------------------\n\n.. edb:youtube-embed:: _IUSPBm2xEA\n\nThe new ``edgedb watch`` CLI command starts a long-running process that watches\nfor changes in schema files in your project's ``dbschema`` directory and\napplies those changes to your database in real time. This command opens up an\nentirely new workflow for prototyping schema that will result in less migration\nclutter in your repositories.\n\n1. Start the ``watch`` command\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. code-block:: bash\n\n    $ gel watch\n    Initialized. Monitoring \"/projects/my-edgedb-project\".\n\n\n2. Write an initial schema\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nJust start writing your schema in your ``default.esdl`` file in your project's\n``dbschema`` directory. Once you save your initial schema, assuming it is\nvalid, the ``watch`` command will pick it up and apply it to your database.\n\n\n3. Edit your schema files\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\nAs your application evolves, directly edit your schema files to reflect your\ndesired data model. When you save your changes, ``watch`` will immediately\nbegin applying your new schema to the database.\n\nOnce you have the schema the way you want it, you're ready to lock it in\nby generating a migration.\n\n\n4. Generate a migration\n^^^^^^^^^^^^^^^^^^^^^^^\n\nTo generate a migration that reflects all your changes, run ``edgedb migration\ncreate``.\n\n.. code-block:: bash\n\n  $ edgedb migration create\n\nThis \"locks in\" the changes you prototyped using the ``watch`` command. Now,\nthese are ready to commit and push to your remote to share with your team.\n\n\nTriggers\n--------\n\n.. edb:youtube-embed:: ImgMfb_jCJQ?start=41\n\nOur new triggers feature is one of the most anticipated 3.0 features! Triggers\nallow you to define an expression to be executed whenever a given query type is\nrun on an object type. The original query will *trigger* your pre-defined\nexpression to run in a transaction along with the original query. These can be\ndefined in your schema.\n\n.. code-block:: sdl\n\n    type Person {\n      required name: str;\n\n      trigger log_insert after insert for each do (\n        insert Log {\n          action := 'insert',\n          target_name := __new__.name\n        }\n      );\n    }\n\nThe trigger above inserts a ``Log`` object any time a ``Person`` object is\ninserted.\n\nYou can read more about our triggers implementation in `the triggers RFC\n<https://github.com/edgedb/rfcs/blob/master/text/1020-triggers.rst>`_.\n\n\nMutation rewrites\n-----------------\n\n.. edb:youtube-embed:: ImgMfb_jCJQ?end=41\n\nThe mutation rewrites feature is the sibling, or at least the first cousin, of\ntriggers. Both are automatically invoked when a write operation occurs on the\ntype they're on, but triggers are not able to make changes to the object that\ninvoked them. Mutation rewrites are built to do just that!\n\n.. code-block:: sdl\n\n    type Post {\n      required title: str;\n      required body: str;\n      modified: datetime {\n        rewrite insert, update using (datetime_of_statement())\n      }\n    }\n\nThis shows one reason mutation rewrites is one of our most wanted features:\nmodified timestamps! When a user inserts or updates a ``Post``, the rewrite\nwill set the value of the ``modified`` property to that value of\n``datetime_of_statement()``. There are tons of other uses too. Give them a try!\n\nLearn about our mutation rewrites implementation in `the mutation rewrites RFC\n<https://github.com/edgedb/rfcs/blob/master/text/1021-rewrites.rst>`_.\n\n\nSplats\n------\n\n.. edb:youtube-embed:: 9-I1qjIp3KI\n\nThis is one of the most fun features in 3.0, both to say _and_ to use! With\nsplats, you can easily select all properties in your queries without typing all\nof them out.\n\nBefore splats, you would have needed this query to select ``Movie`` objects\nalong with all their properties:\n\n.. code-block:: edgeql\n\n    select Movie {id, release_year, title, region, director, studio};\n\nNow, you can simplify down to this query instead using a splat:\n\n.. code-block:: edgeql\n\n    select Movie {*};\n\nIf you wanted to select the movie and its characters before splats, you would\nhave needed this:\n\n.. code-block:: edgeql\n\n    select Movie {\n      id,\n      release_year,\n      title,\n      region,\n      director: {id, name, birth_year},\n      actors: {id, name, birth_year},\n      characters: { id, name }\n    };\n\nNow, you can get it done with just a double-splat to select all the object's\nproperties and the properties of any linked objects nested a single layer\nwithin it.\n\n.. code-block:: edgeql-repl\n\n    db> select Movie {**};\n\nIt's a super-handy way to quickly explore your data.\n\nRead more about splats in `our splats RFC\n<https://github.com/edgedb/rfcs/blob/master/text/1023-splats.rst>`_.\n\n\next::pgvector\n-------------\n\nThe new ``ext::pgvector`` extension can be used to efficiently store\nand query vectors (commonly used to store so called text embeddings.)\n\nUsing the extension is quite straightforward and self-explanatory:\n\n.. tabs::\n\n  .. code-tab:: sdl\n    :caption: In your schema\n\n    using extension pgvector;\n\n    module default {\n      scalar type GPTEmbedding extending\n        ext::pgvector::vector<1536>;\n\n      type Document {\n        required content: str;\n        embedding: GPTEmbedding;\n\n        index ext::pgvector::ivfflat_cosine(lists := 100)\n          on (.embedding);\n      }\n    }\n\n  .. code-tab:: edgeql\n    :caption: In your queries\n\n    with\n      vec as module ext::pgvector,\n      target := <GPTEmbedding>$target_embedding,\n      threshold := <float64>$threshold\n\n    select Document {\n      *,\n      dist := vec::cosine_distance(target, .embedding)\n    }\n    filter .dist < threshold\n    order by .dist empty last\n    limit 5\n\nFor more details about ``ext::pgvector``, please refer to :ref:`the dedicated\next::pgvector documentation <ref_ext_pgvector>`.\n\n\nSQL support\n-----------\n\n.. edb:youtube-embed:: 0KdY2MPb2oc\n\nEdgeDB supports running read-only SQL queries via the Postgres protocol to\nenable connecting EdgeDB to existing BI and analytics solutions. Any\nPostgres-compatible client can connect to your EdgeDB database by using the\nsame port that is used for the EdgeDB protocol and the same database name,\nusername, and password you already use for your database.\n\n.. code-block:: bash\n\n    $ psql -h localhost -p 10701 -U edgedb -d edgedb\n\nOur SQL support has been tested against a number of SQL tools:\n\n- `pg_dump <https://www.postgresql.org/docs/13/app-pgdump.html>`_\n- `Metabase <https://www.metabase.com/>`_\n- `Cluvio <https://www.cluvio.com/>`_\n- `Tableau <https://www.tableau.com/>`_\n- `DataGrip <https://www.jetbrains.com/datagrip/>`_\n- `Airbyte <https://airbyte.com/>`_\n- `Fivetran <https://www.fivetran.com/>`_\n- `Hevo <https://hevodata.com/>`_\n- `Stitch <https://www.stitchdata.com/>`_\n- `dbt <https://www.getdbt.com/>`_\n\n\nNested modules\n--------------\n\nYou can now put a module inside another module to let you organize your schema\nin any way that makes sense to you.\n\n.. code-block:: sdl\n\n    module momma_module {\n      module baby_module {\n        # <schema-declarations>\n      }\n    }\n\nIn EdgeQL, you can reference entities inside nested modules like this:\n``momma_module::baby_module::<entity-name>``\n\nAside from giving you additional flexibility, it will also allow us to expand\nour list of standard modules in a backwards-compatible way.\n\n\n``intersect`` and ``except`` operators\n--------------------------------------\n\nSlice and dice your sets in new ways with the ``intersect`` and ``except``\noperators. Use ``intersect`` to find common members between sets.\n\n.. code-block:: edgeql-repl\n\n    db> select {1, 2, 3, 4, 5} intersect {3, 4, 5, 6, 7};\n    {3, 5, 4}\n\nUse ``except`` to find members of the first set that are not in the second.\n\n.. code-block:: edgeql-repl\n\n    db> select {1, 2, 3, 4, 5} except {3, 4, 5, 6, 7};\n    {1, 2}\n\nThese work with sets of anything, including sets of objects.\n\n.. code-block:: edgeql-repl\n\n    db> with big_cities := (select City filter .population > 1000000),\n    ...   s_cities := (select City filter .name like 'S%')\n    ... select (big_cities intersect s_cities) {name};\n    {default::City {name: 'San Antonio'}, default::City {name: 'San Diego'}}\n    db> with big_cities := (select City filter .population > 1000000),\n    ...   s_cities := (select City filter .name like 'S%')\n    ... select (big_cities except s_cities) {name};\n    {\n      default::City {name: 'New York'},\n      default::City {name: 'Los Angeles'},\n      default::City {name: 'Chicago'},\n      default::City {name: 'Houston'},\n      default::City {name: 'Phoenix'},\n      default::City {name: 'Philadelphia'},\n      default::City {name: 'Dallas'}\n    }\n\n\n``assert`` function\n-------------------\n\nThe new ``assert`` function lets you do handy things like create powerful\nconstraints when paired with triggers:\n\n.. code-block:: sdl\n\n      type Person {\n        required name: str;\n        multi friends: Person;\n        multi enemies: Person;\n\n        trigger prohibit_frenemies after insert, update for each do (\n          assert(\n            not exists (__new__.friends intersect __new__.enemies),\n            message := \"Invalid frenemies\",\n          )\n        )\n      }\n\n.. code-block:: edgeql-repl\n\n    db> insert Person {name := 'Quincey Morris'};\n    {default::Person {id: e4a55480-d2de-11ed-93bd-9f4224fc73af}}\n    db> insert Person {name := 'Dracula'};\n    {default::Person {id: e7f2cff0-d2de-11ed-93bd-279780478afb}}\n    db> update Person\n    ... filter .name = 'Quincey Morris'\n    ... set {\n    ...   enemies := (select detached Person filter .name = 'Dracula')\n    ... };\n    {default::Person {id: e4a55480-d2de-11ed-93bd-9f4224fc73af}}\n    db> update Person\n    ... filter .name = 'Quincey Morris'\n    ... set {\n    ...   friends := (select detached Person filter .name = 'Dracula')\n    ... };\n    edgedb error: EdgeDBError: Invalid frenemies\n\nYou can use it in other contexts too — any time you want to throw an error when\nthings don't go as planned.\n\n\nAdditional changes\n==================\n\nEdgeQL\n------\n\n* Support custom user-defined error messages for access policies\n  (:eql:gh:`#4529`)\n\n  .. code-block:: sdl\n\n    type User {\n      required email: str { constraint exclusive; };\n      required is_admin: bool { default := false };\n      access policy admin_only\n        allow all\n        using (global current_user.is_admin ?? false) {\n          errmessage := 'Only admins may query Users'\n        };\n    }\n\n* Support casting a UUID to a type (:eql:gh:`#4469`). This is a handy way to\n  select an object, assuming the type you cast into has an object with the UUID\n  being cast.\n\n  .. code-block:: edgeql-repl\n\n      db> select <Hero><uuid>'01d9cc22-b776-11ed-8bef-73f84c7e91e7';\n      {default::Hero {id: 01d9cc22-b776-11ed-8bef-73f84c7e91e7}}\n\n* Add the :eql:func:`json_object_pack` function to construct JSON from an array\n  of key/value tuples. (:eql:gh:`#4474`)\n\n  .. code-block:: edgeql-repl\n\n      db> select json_object_pack({(\"hello\", <json>\"world\")});\n      {Json(\"{\\\"hello\\\": \\\"world\\\"}\")}\n\n* Support tuples as query arguments (:eql:gh:`#4489`)\n\n  .. code-block:: edgeql\n\n      select <tuple<str, bool>>$var;\n      select <optional tuple<str, bool>>$var;\n      select <tuple<name: str, flag: bool>>$var;\n      select <optional tuple<name: str, flag: bool>>$var;\n      select <array<tuple<int64, str>>>$var;\n      select <optional array<tuple<int64, str>>>$var;\n\n* Add the syntax for abstract indexes (:eql:gh:`#4691`)\n\n  Exposes some Postgres indexes that you can use in your schemas. These are\n  exposed through the ``pg`` module.\n\n  * ``pg::hash``- Index based on a 32-bit hash derived from the indexed value\n\n  * ``pg::btree``- B-tree index can be used to retrieve data in sorted order\n\n  * ``pg::gin``- GIN is an \"inverted index\" appropriate for data values that\n    contain multiple elements, such as arrays and JSON\n\n  * ``pg::gist``- GIST index can be used to optimize searches involving ranges\n\n  * ``pg::spgist``- SP-GIST index can be used to optimize searches involving\n    ranges and strings\n\n  * ``pg::brin``- BRIN (Block Range INdex) index works with summaries about the\n    values stored in consecutive physical block ranges in the database\n\n  Learn more about the index types we expose `in the Postgres documentation\n  <https://www.postgresql.org/docs/current/indexes-types.html>`_.\n\n  You can use them like this:\n\n  .. code-block:: sdl\n\n      type User {\n        required name: str;\n        index pg::spgist on (.name);\n      };\n\n* Implement migration rewrites (:eql:gh:`#4585`)\n\n* Implement schema reset (:eql:gh:`#4714`)\n\n* Support link properties on computed backlinks (:eql:gh:`#5227`)\n\n\n\nCLI\n---\n\n* Add the ``edgedb migration upgrade-check`` command\n\n  Checks your schema against the new EdgeDB version. You can add ``--to-version\n  <version>``, ``--to-testing``, ``--to-nightly``, or ``--to-channel\n  <channel>`` to check against a specific version.\n\n* Add the ``--squash`` option to the ``edgedb migration create`` command\n\n  This squashes all your migrations into a single migration.\n\n* Change the backslash command ``\\d object <name>`` to ``\\d <name>``\n\n* Add the ``edgedb migration edit`` command (:ref:`docs\n  <ref_cli_gel_migration_edit>`; released in 2.1)\n\n* Add the ``--get`` option to the ``edgedb info`` command (released in 2.1)\n\n  Adding the ``--get`` option followed by a name of one of the info values —\n  ``config-dir``, ``cache-dir``, ``data-dir``, or ``service-dir`` — returns\n  only the requested path. This makes scripting with the ``edgedb info``\n  command more convenient.\n\n\nBug fixes\n---------\n\n.. edb:collapsed::\n\n* Fix crash on cycle between defaults in insert (:eql:gh:`#5355`)\n\n* Improvements to top-level server error reporting (:eql:gh:`#5349`)\n\n* Forbid ranges of user-defined scalars (:eql:gh:`#5345`)\n\n* Forbid DML in non-scalar function args (:eql:gh:`#5310`)\n\n* Don't let \"owned\" affect how we calculate backlinks (:eql:gh:`#5306`)\n\n* Require inheritance order to be consistent with the specified base order\n  (:eql:gh:`#5276`)\n\n* Support using non-strict functions in simple expressions (:eql:gh:`#5271`)\n\n* Don't duplicate the computation of single links with link properties\n  (:eql:gh:`#5264`)\n\n* Properly rebase computed links when changing their definition\n  (:eql:gh:`#5222`)\n\n* Fix 3-way unions of certain types with policies (:eql:gh:`#5205`)\n\n* Fix simultaneous deletion of objects related by multi links (:eql:gh:`#5201`)\n\n* Respect ``enforce_access_policies := false``\n  inside functions (:eql:gh:`#5199`)\n\n* Fix inferred link/property kind when extending abstract link\n  (:eql:gh:`#5196`)\n\n* Forbid ``on target delete deferred restrict`` on required links.\n  (:eql:gh:`#5189`)\n\n* Make uuidgen properly set versions in uuid4/uuid5 (:eql:gh:`#5188`)\n\n* Disallow variadic arguments with optional types in user code.\n  (:eql:gh:`#5110`)\n\n* Support casting between scalars with a common concrete base (:eql:gh:`#5108`)\n\n* Fix GROUP regression with some query-builder queries (:eql:gh:`#5071`)\n\n* Fix a ISE when using ``assert_exists`` and linkprops using query builder\n  (:eql:gh:`#5036`)\n\n* Fix bug that dropping non-existing db leaves with unaccessible state\n  (:eql:gh:`#5032`)\n\n* Fix non-transactional errors in Postgres 14.7 (:eql:gh:`#5028`)\n\n* Properly cast to containers of enums when loading from the schema\n  (:eql:gh:`#4988`)\n\n* Implement manual error override configuration (:eql:gh:`#4974`)\n\n* Fix protocol state confusion after rollback\n  (:eql:gh:`#4970`), (:eql:gh:`#4953`)\n\n* Support disabling dynamic configuration of system config\n  (:eql:gh:`#5425`)\n\n* In multi-server instances, properly reload schema after a restore\n  (:eql:gh:`#5463`)\n\n* Fix several bugs synchronizing configuration state\n\n* Fix dropping a pointer's constraint and making it computed at the same time\n  (:eql:gh:`#5411`)\n\n* Don't claim that making a pointer computed is data-safe\n  (:eql:gh:`#5412`)\n\n* Prohibit NUL character in query source\n  (:eql:gh:`#5414`)\n\n* Fix migration that delete an link alias computed in a parent and child\n  (:eql:gh:`#5428`)\n\n* Fix GraphQL updates for multi links.\n  (:eql:gh:`#4260`)\n\n* Fix altering enum that is used in a tuple\n  (:eql:gh:`#5445`)\n\n* Fix changing cardinality of properties on types used in unions\n  (:eql:gh:`#5457`)\n\n* Enable GraphQL support for type unions.\n\n* Fix making pointer non-computed and giving it an abstract base at the\n  same time\n  (:eql:gh:`#5458`)\n\n* Make json casts of object arrays not include extra fields\n  (:eql:gh:`#5484`)\n\n* Make coalesce infer a union type\n  (:eql:gh:`#5472`)\n\n* Fix graphql queries made against a freshly started server\n  (:eql:gh:`#5456`)\n\n* Fix version for project init\n  (:eql:gh:`#5460`)\n\n* Produce a proper error for too many constraint args\n  (:eql:gh:`#5454`)\n\n* Prevent using leading dot notation in inserts\n  (:eql:gh:`#5142`)\n\n* Fix operations on single link with only computable link properties\n  (:eql:gh:`#5499`)\n\n* Don't ISE on free shape in insert\n  (:eql:gh:`#5438`)\n\n* Work around postgres server crashes on Digital Ocean during edgedb setup\n  (:eql:gh:`#5505`)\n\n* Always set cardinality of derived ``__tname__`` and ``__tid__`` pointers\n  (:eql:gh:`#5508`)\n\n* Make failovers more resilient\n  (:eql:gh:`#5511`)\n\n* Fix support for Postgres 15\n  (:eql:gh:`#5534`)\n\n* Fix constraints with zero references to the table\n\n* Add a read-only mode\n  (:eql:gh:`#5543`)\n\n* Improve error messages for GraphQL\n  (:eql:gh:`#3816`)\n\n* Allow creating databases that share names with standard library functions\n  (:eql:gh:`#5515`)\n\n* Fix error messages for invalid constraint parameters\n  (:eql:gh:`#5525`)\n\n* Support comparisons on durations in static eval\n\n* Do not allow link property on properties\n  (:eql:gh:`#5537`)\n\n* Fix ISE for TypeExprs in function returns\n  (:eql:gh:`#5540`)\n\n* Support dumping databases with ``std::duration`` config vals\n  (:eql:gh:`#5528`)\n\n* Type unions should not appear when computing the source of a back link\n  (:eql:gh:`#5533`)\n\n* Fix range and bytes output in json-lines/json-pretty mode\n  (:eql:gh:`#5572`)\n\n* Include source locations in more scoping error messages\n  (:eql:gh:`#5573`)\n\n* Fix an optional scoping bug with important access policy implications\n  (:eql:gh:`#5575`)\n\n* Fix several ordering issues in schema loading\n  (:eql:gh:`#5565`)\n\n* Produce proper error messages when creating a migration that drops\n  an object but not all of its uses\n  (:eql:gh:`#5579`)\n\n* Sort composite type display names consistently\n  (:eql:gh:`#5580`)\n\n* Make stdlib objects immutable\n  (:eql:gh:`#5599`)\n\n* Slightly improve generated SQL around parameter uses\n  (:eql:gh:`#5605`)\n\n* Fix coalesce with type intersection\n  (:eql:gh:`#5600`)\n\n* Do not corrupt cardinality inference results when doing group analysis\n  (:eql:gh:`#5611`)\n\n* Fix some obscure optional bugs in the presence of tuple projections\n  (:eql:gh:`#5610`)\n\n* Fix casts from json to custom scalars\n  (:eql:gh:`#5624`)\n\n* Fix ``pg_namespace`` emulation in SQL layer\n  (:eql:gh:`#5594`)\n\n* Fix json output of arrays of ranges\n  (:eql:gh:`#5629`)\n\n* Fix ISE when using ``__subject__`` in default\n  (:eql:gh:`#5633`)\n\n* Make default expressions in the schema always compiled\n  as ``detached``\n  (:eql:gh:`#5606`)\n\n* Bump ``max_locks_per_transaction`` to 1024\n  (:eql:gh:`#5636`)\n\n* Disallow partial paths in the defaults of link properties\n  (:eql:gh:`#5560`)\n\n* Expose the ``maintenance_work_mem`` database configuration variable.\n  This may need to be tuned in order to build large pgvector indexes\n\n* Add an ``ADMINISTER reindex(...)`` function to reindex objects.\n  ``ADMINISTER reindex(Obj)`` will reindex all indexes on the table\n  backing ``Obj`` while ``ADMINISTER reindex(Obj.ptr)`` will reindex\n  any indexes referencing ``ptr``.\n  (:eql:gh:`#5659`)\n\n* Fix referring to ``__source__`` in defaults\n  (:eql:gh:`#5667`)\n\n* Fix constraints on custom abstract scalars\n  (:eql:gh:`#5666`)\n\n* Ensure changing scalar base types throws appropriately\n  (:eql:gh:`#5664`)\n\n\nDeprecations\n------------\n\nThe support of version pre-1.0 binary protocol is deprecated in EdgeDB 3.0, and\nwill be completely dropped in EdgeDB 4.0. If you're still using a deprecated\nversion of the binary protocol or any client libraries that *only* support the\npre-1.0 binary protocol as listed below, please consider upgrading to a newer\nversion.\n\n* edgedb-js / edgedb-deno v0.20 or lower\n* edgedb-python v0.23 or lower\n* edgedb-go v0.10 or lower\n* edgedb-tokio (Rust) v0.2 or lower\n* EdgeDB.NET v0.2 or lower\n* edgedb-elixir v0.3 or lower\n\n\nNew release schedule\n====================\n\nUnfortunately, the 3.0 release will not include full-text search. We have many\nrequirements for this new API (see `the FTS RFC\n<https://github.com/edgedb/rfcs/blob/master/text/1015-full-text-search.rst>`_\nfor details), and, while we've made significant progress, we have unfortunately\nrun out of time to be 100% sure that it is ready for prime time.\n\nWe don't want this delay to hold back the release of EdgeDB 3.0, which includes\nmany other exciting features that are ready for you to start using right now.\nThat's why we've decided to delay only the FTS feature rather than delaying the\nentire 3.0 release.\n\nThat said we're working hard to get FTS ready as soon as possible. After the\nrelease of 3.0, we'll be moving to a much more frequent release cycle so that\nfeatures like FTS can be in your hands as soon as they're ready.\n\nGoing forward, expect EdgeDB releases every four months. These releases will\nnaturally incorporate fewer features than our past releases, but we think the\nmore predictable cadence will be worth it. Every third release starting with\n3.0 will be a long-term support (LTS) release. These releases will continue to\nreceive support for a year and a half after their initial release.\n\n.. _v3anno:\n    https://www.edgedb.com/blog/edgedb-3-0\n\n3.1\n===\n\n* Add support for \"offline\" readiness state\n  (:eql:gh:`#5730`)\n\n* Bump ``max_pred_locks_per_transaction`` to 1024\n  (:eql:gh:`#5694`)\n\n* Be more permissive about differing postgres versions when using\n  remote compiler pool\n  (:eql:gh:`#5698`)\n\n* Do not lose constraint ``errmessage`` when a constraint is ALTERed\n  (:eql:gh:`#5701`)\n\n* Properly track implicit dependance on ``__type__`` in function calls\n  (:eql:gh:`#5715`)\n\n* Avoid very slow error formatting in non-error cases\n  (:eql:gh:`#5706`)\n\n* Push Docker images to ghcr.io also\n  (:eql:gh:`#5700`)\n\n* Fix tuple derefencing and top-level casting in exclusive constraints\n  (:eql:gh:`#5724`)\n\n* Add support for statement variants of prepared statement comands in\n  SQL mode\n  (:eql:gh:`#5685`)\n\n* Respect ``cfg::`` annotations on object configuration\n  (:eql:gh:`#5746`)\n\n* Avoid leaking database existence information to unauthenticated users\n  (:eql:gh:`#5744`)\n\n\n3.2\n===\n* Fix empty queries in the SQL layer\n  (:eql:gh:`#5776`)\n\n* Expand list of supported SQL range functions\n  (:eql:gh:`#5777`)\n\n* Fix psql ``\\dt``\n  (:eql:gh:`#5779`)\n\n* Fix bugs in casting between ranges and json on empty set inputs\n  (:eql:gh:`#5792`)\n\n* Fix globals of scalar types defined later in the schema\n  (:eql:gh:`#5764`)\n\n* Fix moving access policy from child to parent\n  (:eql:gh:`#5786`)\n\n* Modestly speed up type creation in large schemas\n  (:eql:gh:`#5814`)\n\n3.3\n===\n* Fix creating roles in transactions\n  (:eql:gh:`#5925`)\n\n* Fix database check in JWT authentication\n  (:eql:gh:`#5928`)\n\n* Create extensions conditionally\n  (:eql:gh:`#5894`)\n\n* Stop putting deletes of self before alters of self in when making schema\n  deltas\n  (:eql:gh:`#5897`)\n\n* Fix dropping of extensions with nontrivial schemas\n  (:eql:gh:`#5897`)\n\n* Fix healthchecks on single-database instances\n  (:eql:gh:`#5900`)\n\n* Fix a bug in SQL name resolving\n  (:eql:gh:`#5860`)\n\n* Add hacky support for pgadmin\n  (:eql:gh:`#5877`)\n\n* Fix some issues with nontrivial function parameter defaults\n  (:eql:gh:`#5970`)\n\n* Fix named-tuple params that also appear in the schema\n  (:eql:gh:`#5964`)\n\n* Disallow more broken sorts of function overloads\n  (:eql:gh:`#5992`)\n\n* Prevent user modifications to extension modules\n  (:eql:gh:`#5993`)\n\n* Fix use of certain empty sets from multiple optional arguments\n  (:eql:gh:`#5990`)\n\n* Fix a casting bug.\n\n* Do not stack overflow on long arrays and similar\n  (:eql:gh:`#6004`)\n\n* Fix collisions between function parameters named id and the property\n  (:eql:gh:`#6000`)\n\n* Fix an infinite recursion when casting object->json in some cases\n  (:eql:gh:`#6007`)\n\n* Fix object to json casts inside functions\n  (:eql:gh:`#6016`)\n\n* Produce better errors on compiler stack overflows and properly interpret\n  errors raised from SQL when using HTTP protocols\n  (:eql:gh:`#6005`)\n\n\n3.4\n===\n\n* Repair instances created with rc1/rc2 to fix an issue that can\n  prevent creating migrations.\n\n* GraphQL: Fix bug with objects without editable fields.\n  (:eql:gh:`#6039`)\n\n* GraphQL: Fix issues with deeply nested modules.\n  (:eql:gh:`#6039`)\n\n* GraphQL: Fix ``__typename`` for non-default modules.\n  (:eql:gh:`#5985`)\n\n* GraphQL: Fix fragments on types from non-default module.\n  (:eql:gh:`#5985`)\n\n* GraphQL: Handle ``__typename`` in mutations.\n  (:eql:gh:`#5985`)\n\n* Make ``SET default`` of trivial values much faster\n  (:eql:gh:`#6134`)\n\n* Fix DELETE/UPDATE on parent types with children that have no policies\n  (:eql:gh:`#6136`)\n\n* Skip generating updates on abstract types, as a minor optimization\n  (:eql:gh:`#6135`)\n\n* Fix cast of empty set to type with access policy\n  (:eql:gh:`#6151`)\n\n* Fix ``group`` on backlinks in some situations\n  (:eql:gh:`#6152`)\n\n* Support using query parameters in ``SET GLOBAL`` commands\n  (:eql:gh:`#5670`)\n\n* Make using globals as defaults work\n  (:eql:gh:`#6153`)\n\n* Fix schema ordering bugs with rewrites\n  (:eql:gh:`#6154`)\n\n* Fix function params whose name collide with SQL type names\n  (:eql:gh:`#6150`)\n\n* Fix schema confusion with objects that shadow std objects\n  (:eql:gh:`#6155`)\n\n* Fix migration incomplete issues with functions that use multiple globals\n  (:eql:gh:`#6157`)\n\n* Fix several bugs in ``analyze``\n  (:eql:gh:`#5758`)\n\n* Fix spurious compiler cache misses when duration config values\n  are set in a session\n  (:eql:gh:`#6168`)\n\n* Properly reject indexes on non-immutable computeds\n  (:eql:gh:`#6170`)\n\n3.5\n===\n* Fix migrations modifying WHEN clauses on access policies\n\n* Produce proper error messages when defining a field twice\n  (:eql:gh:`#5785`)\n\n* Fix a protocol bug involving ``CONFIGURE INSTANCE``\n  (:eql:gh:`#6258`)\n\n* Fix some regressions involving property defaults\n  (:eql:gh:`#6265`)\n\n* Fix scope bugs in ``SET ... USING`` statements\n  (:eql:gh:`#6267`)\n\n* Handle decimal params in edgeql_http protocol without loss of\n  precision\n  (:eql:gh:`#5848`)\n\n* Mark md5 as not being used for security, allowing use of EdgeDB\n  in a FIPS-complaint environment.\n  (:eql:gh:`#6268`)\n\n* Fix compilation of explicit ids in inserts that can be empty\n  (:eql:gh:`#6281`)\n\n* Fix some migration crashes due to missing source contexts\n  (:eql:gh:`#6288`)\n\n* Fix unused WITH DML when body is an INSERT\n  (:eql:gh:`#6287`)\n\n* Fix configuring effective_io_concurrency\n  (:eql:gh:`#6289`)\n\n* Fix type name injection on computed backlinks without a shape\n  (:eql:gh:`#6290`)\n\n* Fix renames of types with rewrites on them\n  (:eql:gh:`#6293`)\n\n* Fix renames of properties that have computeds aliasing them\n  (:eql:gh:`#6294`)\n\n* Fix use of UNLESS CONFLICT in triggers\n  (:eql:gh:`#6297`)\n\n* Fix uuid->object casts in access policies in nested INSERTs\n  (:eql:gh:`#6313`)\n\n* Make 'reference to a non-existent schema item <id>' an ISE.\n  It's always a bug, and that should be made clear to users.\n  (:eql:gh:`#6314`)\n\n* Make edgedb+http tunneled protocol not leak db existance before auth\n  (:eql:gh:`#6350`)\n\n* Substantially speed up restoring of dumps with large schemas\n  (:eql:gh:`#6362`)\n\n* Make migration deadlocks less likely in some common cases\n  (:eql:gh:`#6379`)\n\n* Support calling functions that use globals in property/link defaults\n  (:eql:gh:`#6381`)\n\n* Correctly update functions when globals/aliases/computeds they depend on\n  change\n  (:eql:gh:`#6382`)\n\n3.6\n===\n* Fix UPDATE with rewrites on tuples\n  (:eql:gh:`#6337`)\n\n* Fix DML with constraints on abstract types\n  (:eql:gh:`#6421`)\n\n* Fix some interactions between FOR and optional values\n  (:eql:gh:`#6526`)\n\n* Fix inserts silently failing when a json->array cast handles 'null'\n  (:eql:gh:`#6544`)\n\n* Fix tracing of enums in type expressions\n  (:eql:gh:`#6548`)\n\n* Allow indexes that use user-defined functions to actually get hit\n  (:eql:gh:`#6551`)\n\n* Fix schema delta for RESET EXPRESSION\n  (:eql:gh:`#6463`)\n\n* Fix changing a pointer to be computed when there is a subtype\n  (:eql:gh:`#6565`)\n\n* Fix migrating between aliased and non-aliased computeds\n  (:eql:gh:`#6566`)\n\n* Fix some bugs involving union and coalescing of optional values\n  (:eql:gh:`#6590`)\n"
  },
  {
    "path": "docs/resources/changelog/4_x.rst",
    "content": "====\nv4.0\n====\n\n:edb-alt-title: EdgeDB v4\n\nThis release cycle is much shorter than the previous ones. It reflects our new\napproach at |EdgeDB| where the goal is to provide improvements at a steady\nregular pace rather than in big, but infrequent batches. Going forward we\nexpect to maintain this shorter release cadence focusing on a few features at\na time.\n\nTo play with the new features, install the CLI using `our installation guide\n<https://www.edgedb.com/install>`_ and initialize a new project.\n\n.. code-block:: bash\n\n  $ edgedb project init\n\n\nUpgrading\n=========\n\n**Local and Cloud instances**\n\nTo upgrade a local project, first ensure that your CLI is up to date with\n``edgedb cli upgrade``. Then run the following command inside the project\ndirectory.\n\n.. code-block:: bash\n\n  $ edgedb project upgrade\n\nAlternatively, specify an instance name if you aren't using a project.\n\n.. code-block:: bash\n\n  $ edgedb instance upgrade -I my_instance\n\nThe CLI will first check to see if your schema will migrate cleanly to EdgeDB\n4.0. If the upgrade check finds any problems, it will report them back to you.\n\n**Hosted instances**\n\nTo upgrade a remote (hosted) instance, we recommend the following\ndump-and-restore process.\n\n1. EdgeDB v4.0 only supports PostgreSQL 14 (or above). So check the version of\n   PostgreSQL you are using before upgrading EdgeDB. If you're using Postgres\n   13 or below, you should upgrade Postgres first.\n\n2. Spin up an empty 4.0 instance. You can use one of our :ref:`deployment\n   guides <ref_guide_deployment>`.\n\n   Under Debian/Ubuntu, when adding the EdgeDB package repository, use this\n   command instead:\n\n   .. code-block:: bash\n\n       $ echo deb [signed-by=/usr/local/share/keyrings/edgedb-keyring.gpg] \\\n           https://packages.edgedb.com/apt \\\n           $(grep \"VERSION_CODENAME=\" /etc/os-release | cut -d= -f2) main \\\n           | sudo tee /etc/apt/sources.list.d/edgedb.list\n\n   Use this command for installation under Debian/Ubuntu:\n\n   .. code-block:: bash\n\n       $ sudo apt-get update && sudo apt-get install edgedb-4\n\n   Under CentOS/RHEL, use this installation command:\n\n   .. code-block:: bash\n\n       $ sudo yum install edgedb-4\n\n   In any required ``systemctl`` commands, replace ``edgedb-server-3`` with\n   ``edgedb-server-4``.\n\n   Under any Docker setups, supply the ``4.0`` tag.\n\n3. Take your application offline, then dump your v3.x database with the CLI\n\n   .. code-block:: bash\n\n       $ gel dump --dsn <old dsn> --all --format dir my_database.dump/\n\n   This will dump the schema and contents of your current database to a\n   directory on your local disk called ``my_database.dump``. The directory name\n   isn't important.\n\n4. Restore the empty v4.x instance from the dump\n\n   .. code-block:: bash\n\n       $ gel restore --all my_database.dump/ --dsn <new dsn>\n\n   Once the restore is complete, update your application to connect to the new\n   instance.\n\n   This process will involve some downtime, specifically during steps 2 and 3.\n\n.. note::\n\n    If your Postgres cluster is also backing other versions of EdgeDB, make\n    sure you start your new instance with the ``--ignore-other-tenants``\n    option when bootstrapping your new instance.\n\n\nNew features\n============\n\nFull-text Search\n----------------\n\nEdgeDB 4.0 adds :ref:`full-text search <ref_std_fts>` functionality packaged\nin the ``fts`` module. By adding an ``fts::index`` to an object type you can\ntransform any object into a searchable document:\n\n.. code-block:: sdl\n\n    type Item {\n      required available: bool {\n        default := false;\n      };\n      required name: str;\n      required description: str;\n\n      index fts::index on (\n        fts::with_options(\n          .name,\n          language := fts::Language.eng\n        )\n      );\n    }\n\nThe ``fts::index`` indicates to EdgeDB that this object type is a valid target\nfor full-text search. The property that will be searched as well as the\nlanguage is provided in the index.\n\nThe :eql:func:`fts::search` function allows searching objects for a particular\nphrase:\n\n.. code-block:: edgeql-repl\n\n  db> select fts::search(Item, 'candy corn', language := 'eng');\n  {\n    (\n      object := default::Item {id: 9da06b18-69b2-11ee-96b9-1bedbe75ad4f},\n      score := 0.30396354,\n    ),\n    (\n      object := default::Item {id: 92375624-69b2-11ee-96b9-675b9b87ac70},\n      score := 0.6079271,\n    ),\n  }\n\nThe search results are provided as a tuple containing the matching document\nobject and a score. Higher score indicates a better match. So we can use these\nvalues to order the results:\n\n.. code-block:: edgeql-repl\n\n  db> with res := (\n  ...   select fts::search(Item, 'candy corn', language := 'eng')\n  ... )\n  ... select res.object {name, score := res.score}\n  ... order by res.score desc;\n  {\n    default::Item {name: 'Candy corn', score: 0.6079271},\n    default::Item {name: 'Canned corn', score: 0.30396354},\n  }\n\nYou can only have at most one ``fts::index`` defined for any particular type.\nSo if there are multiple properties that should be searchable, they can all be\nspecified in that one index:\n\n.. code-block:: sdl\n\n    type Item {\n      required available: bool {\n        default := false;\n      };\n      required name: str;\n      required description: str;\n\n      index fts::index on ((\n        fts::with_options(\n          .name,\n          language := fts::Language.eng\n        ),\n        fts::with_options(\n          .description,\n          language := fts::Language.eng\n        )\n      ));\n    }\n\nThe above schema declares both ``name`` and ``description`` as searchable\nfields:\n\n.. code-block:: edgeql-repl\n\n  db> with res := (\n  ...   select fts::search(Item, 'trick or treat', language := 'eng')\n  ... )\n  ... select res.object {name, description, score := res.score}\n  ... order by res.score desc;\n  {\n    default::Item {\n      name: 'Candy corn',\n      description: 'A great Halloween treat',\n      score: 0.30396354,\n    },\n  }\n\n\nMultiranges\n-----------\n\nWe've made it easier to work with ranges by adding a :ref:`multirange\n<ref_std_range>` datatype. Multiranges consist of one or more ranges and allow\nexpressing intervals that are not contiguous. Multiranges are automatically\nnormalized to contain non-overlapping ranges that are ordered according to\ntheir boundaries. All the usual range operators and functions like\n``overlaps`` or ``contains`` work with any combination of ranges and\nmultiranges, providing more flexibility in expressions.\n\n.. code-block:: edgeql-repl\n\n    db> select multirange([range(8, 10)]) + range(1, 5) - range(3, 4);\n    {[range(1, 3), range(4, 5), range(8, 10)]}\n\n\nGraphQL and HTTP authentication\n===============================\n\n..\n   We'll drop the rc1 mention after the real release\n\nStarting in rc1, the :ref:`EdgeQL over HTTP <ref_edgeql_http>`\nand :ref:`GraphQL <ref_graphql_index>` endpoints support\n(and by default require) authentication.\n\n.. lint-off\n\nBy default,\n`HTTP Basic Authentication\n<https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme>`_ is used.\n\n.. lint-on\n\nFull details are available in the :ref:`EdgeQL over HTTP documentation\n<ref_http_auth>`.\n\nThis is a backwards-incompatible change. It is possible to opt-in to\nthe old behavior, but not recommended.\n\nExtensions\n==========\n\nauth\n----\n\nThe new ``auth`` extension adds a full authentication service that runs\nalongside your database instance, saving you the hassle of having to learn and\nimplement the intricacies of OAuth or secure password storage.\n\n- OAuth Integration: Seamlessly authenticate with GitHub, Google, Apple, and\n  Azure/Microsoft.\n- Email & Password Support: Includes robust email+password authentication with\n  reset password functionality.\n- Easy Configuration: Set up via our configuration system.\n- Hosted UI: Use our hosted authentication UI to quickly add authentication to\n  your app.\n\nWhen a user signs up, we create a new object of type ``ext::auth::Identity``,\nwhich you can link to in your own schema. We then provide you with a token that\ncan be set as the global ``ext::auth::client_token`` which will automatically\npopulate another computed global called ``ext::auth::ClientTokenIdentity``\nwhich you can use directly in your access policies, or in your own globals.\n\n.. code-block:: sdl\n\n    using extension auth;\n\n    module default {\n        global current_customer := (\n            assert_single((\n                select Customer\n                filter .identity = global ext::auth::ClientTokenIdentity\n            ))\n        );\n\n        type Customer {\n            required text: str;\n            required identity: ext::auth::Identity;\n        }\n\n        type Item {\n            required sku: str;\n            required description: str;\n        }\n\n        type Cart {\n            required customer: Customer;\n            multi items: Item {\n                quantity: int32;\n            };\n\n            access policy customer_has_full_access\n                allow all\n                using (global current_customer ?= .customer);\n        }\n    }\n\n\nHere's an example query using the TypeScript client:\n\n.. code-block:: typescript\n\n    import { createClient } from \"edgedb\";\n\n    declare const tokenFromAuthServer: string;\n    const client = createClient()\n      .withGlobals({\n        \"ext::auth::client_token\": tokenFromAuthServer\n      });\n\n    const carts = await client.query(`select Cart { * };`);\n\n\n\npgcrypto\n--------\n\nWe've added :ref:`pgcrypto <ref_ext_pgcrypto>` to our extensions. This exposes\n``digest``, ``hmac``, ``gen_salt`` and ``crypt`` functions for your hashing,\nencrypting and salting needs.\n\n.. code-block:: edgeql-repl\n\n  db> select ext::pgcrypto::digest('encrypt this', 'sha1');\n  {b'\\x05\\x82\\xd8YLF\\xe7\\xd4\\x12\\x91\\n\\xdb$\\xf1!v\\xf9\\xd4\\x89\\xc4'}\n  db> select ext::pgcrypto::gen_salt('md5');\n  {'$1$FjNlXgX7'}\n\nStandard algorithms are \"md5\", \"sha1\", \"sha224\", \"sha256\", \"sha384\" and\n\"sha512\". Moreover, any digest algorithm OpenSSL supports is automatically\npicked up.\n\n\npg_trgm\n-------\n\nThe :ref:`pg_trgm <ref_ext_pgtrgm>` extension provides functionality used to\ndetermine string similarity, which makes it a good text search alternative for\nsome use cases:\n\n.. code-block:: edgeql-repl\n\n  db> with x := {'hello world', 'word hero', 'help the world'}\n  ... select res := (x, ext::pg_trgm::word_similarity(x, 'hello world'))\n  ... order by res.1 desc;\n  {('hello world', 1), ('help the world', 0.5), ('word hero', 0.35714287)}\n\n\n\nAdditional changes\n==================\n\nPerformance\n-----------\n\nWe've made a few internal changes affecting performance, the biggest of which\nwas rewriting EdgeQL parser in Rust. Overall we've manged to reduce the\nbaseline server memory consumption by 40%.\n\nEdgeQL\n------\n\n* Add new style of ``if``/``then``/``else`` syntax.\n  (:eql:gh:`#6074`)\n\n  Many people find it more natural to write \"if ... then .. else ...\" for\n  conditional expressions because it mirrors the conditional statement from\n  other familiar programming languages.\n\n  .. code-block:: edgeql-repl\n\n    db> select if count(Object) > 0 then 'got data' else 'no data';\n    {'got data'}\n\n* Support conditional DML.\n  (:eql:gh:`#6181`)\n\n  It can be useful to be able to create, update or delete different objects\n  based on some condition:\n\n  .. code-block:: edgeql\n\n    with\n      name := <str>$0,\n      admin := <bool>$1\n    select if admin then (\n        insert AdminUser { name := name }\n    ) else (\n        insert User { name := name }\n    )\n\n  A different use-case of conditional DML is using a :eql:op:`coalesce`\n  operator to express things like \"select or insert if missing\":\n\n  .. code-block:: edgeql\n\n    select (select User filter .name = 'Alice') ??\n           (insert User { name := 'Alice' });\n\n* Add ``contains`` for JSON so that it can be used with ``pg::gin`` index.\n  (:eql:gh:`#5910`)\n\n* Add :eql:func:`to_bytes` to convert :eql:type:`str` into :eql:type:`bytes`\n  using UTF-8 encoding.\n  (:eql:gh:`#5960`)\n\n* Add :eql:func:`to_str` to convert :eql:type:`bytes` into :eql:type:`str`\n  using UTF-8 encoding.\n  (:eql:gh:`#5960`)\n\n* Add ``enc::base64_encode`` and ``enc::base64_decode`` functions.\n  (:eql:gh:`#5963`)\n\n  .. code-block:: edgeql-repl\n\n    db> select enc::base64_encode(b'hello');\n    {'aGVsbG8='}\n    db> select enc::base64_decode('aGVsbG8=');\n    {b'hello'}\n\n* Add ``when`` clause to triggers to enable them to be conditional.\n  (:eql:gh:`#6184`)\n\n* Allow empty arrays without cast in ``insert``.\n  (:eql:gh:`#6218`)\n\n\nGraphQL\n-------\n\n* Change how globals are passed in GraphQL queries.\n  (:eql:gh:`#5864`)\n\n  Instead of using a separate ``globals`` field (which is non-standard), use\n  ``variables`` to add a ``__globals__`` object to pass the global variables.\n\n  In order to ensure backwards compatibility, the old way of passing globals\n  is still valid. In case both the new and the old methods are used the\n  globals being passed in them must match or else the query will be rejected.\n\n* Fix GraphQL bug with objects without editable fields.\n  (:eql:gh:`#6056`)\n\n* Fix GraphQL issues with deeply nested modules.\n  (:eql:gh:`#6056`)\n\n* Fix GraphQL ``__typename`` for non-default modules and mutations.\n  (:eql:gh:`#6035`)\n\n* Fix GraphQL fragments on types from non-default module.\n  (:eql:gh:`#6035`)\n\n\n\nBug fixes\n---------\n\n* Fix a casting bug for some aliased expressions.\n  (:eql:gh:`#5788`)\n\n* Fix cardinality inference of calls to functions with ``optional`` args.\n  (:eql:gh:`#5867`)\n\n* Fix the undefined order of columns in  SQL ``COPY``.\n  (:eql:gh:`#6036`)\n\n* Fix drop of union links when source has a subtype.\n  (:eql:gh:`#6044`)\n\n* Fix link deletion policies on links to union types.\n  (:eql:gh:`#6033`)\n\n* Fix deletion issues of aliases that use ``with``\n  (:eql:gh:`#6052`)\n\n* Make ``id`` of schema objects stable.\n  (:eql:gh:`#6058`)\n\n* Allow computed pointers on types to omit link/property kind specification.\n  (:eql:gh:`#6073`)\n\n* Support ``listen_ports`` greater than 32767.\n  (:eql:gh:`#6194`)\n\n* Fix migration issues with some overloaded indexes/constraints in SDL.\n  (:eql:gh:`#6172`)\n\n* Support DML on right hand side of coalesce expressions.\n  (:eql:gh:`#6202`)\n\n* Fix cardinality inference of polymorphic shape elements.\n  (:eql:gh:`#6255`)\n\n* Fix migration issue involving property defaults.\n  (:eql:gh:`#6265`)\n\n* Fix bugs in ``set ... using`` statements with ``assert_exists`` and similar.\n  (:eql:gh:`#6267`)\n\n* Fix cardinality bug when a property appears in multiple splats.\n  (:eql:gh:`#6255`)\n\n* Make comparison operators non-associative\n  (:eql:gh:`#6327`)\n\n* Fix an obscure parser bug caused by constant extraction\n  (:eql:gh:`#6328`)\n\n* Cap the size of sets in ``multi`` configuration values to ``128``\n  (:eql:gh:`#6402`)\n\n4.1\n===\n* Fix dump and restore of auth config\n  (:eql:gh:`#6414`)\n\n* Clear ``_config_cache`` in ``sys_pgcon`` before reloading reported config\n  (:eql:gh:`#6427`)\n\n* Fix access policy type rewrites of ``std::Object``\n  (:eql:gh:`#6420`)\n\n* Fix an error message of affected refs\n  (:eql:gh:`#6425`)\n\n* Fix DML with constraints on abstract types\n  (:eql:gh:`#6421`)\n\n4.2\n===\n* Fix schema delta for RESET EXPRESSION\n  (:eql:gh:`#6463`)\n\n* Fix plain references to __old__ in rewrites\n  (:eql:gh:`#6470`)\n\n* Fix std::range in singleton mode\n  (:eql:gh:`#6475`)\n\n* Treat password reset as a verification event\n  (:eql:gh:`#6504`)\n\n* Fixes to auth redirect urls\n  (:eql:gh:`#6469`)\n\n* Fix SQL introspection of __fts_document__\n  (:eql:gh:`#6507`)\n\n* Type check mutation rewrites at migration time\n  (:eql:gh:`#6466`)\n\n  Mutation rewrite's ``using`` expression are now required to be of correct\n  type when the rewrite is created. Up until now, it was possible to migrate to\n  a schema that contained a rewrite rule that would always throw a type error\n  when an object was being inserted or updated.\n\n  This might be considered a breaking change, but as it is clearly a bug in\n  user schema and as it could also be considered a bug in the compiler, we are\n  fixing it in a minor version.\n\n4.3\n===\n* Fix non-rewritten tuple literals\n  (:eql:gh:`#6521`)\n\n* Support ``connect_timeout`` in backend DSN\n  (:eql:gh:`#6531`)\n\n* Fix coalesced DML in FOR loops over objects\n  (:eql:gh:`#6526`)\n\n* Fix inserts silently failing when a ``json->array`` handles 'null'\n  (:eql:gh:`#6544`)\n\n* Fix tracing of enums in type expressions\n  (:eql:gh:`#6548`)\n\n* Fix dumps with FTS indexes\n  (:eql:gh:`#6560`)\n\n* Allow indexes that use user-defined functions to actually get hit\n  (:eql:gh:`#6551`)\n\n* Fix reloading readiness state and JWT ``*_list`` files in multi-tenant mode\n  (:eql:gh:`#6562`)\n\n* Handle OPTIONS in the extension path which is needed for CORS preflight\n  (:eql:gh:`#6577`)\n\n* Optimize the compiler and reduce time of an update test by ~52%\n  (:eql:gh:`#6579`)\n\n* Always retry system connection on any BackendError\n  (:eql:gh:`#6588`)\n\n* Properly support @source/@target on multi-link constraints\n  (:eql:gh:`#6585`)\n\n* Fix constant extraction's interaction with the new if-then-else\n  (:eql:gh:`#6591`)\n\n* Fix migrating between aliased and non-aliased computeds\n  (:eql:gh:`#6566`)\n\n* Improve error message for illegal casts and parameters.\n  (:eql:gh:`#6511`)\n\n* Don't eval ext::auth::ClientTokenIdentity on every row in a filter\n  (:eql:gh:`#6607`)\n\n* Expose __type__ via SQL adapter\n  (:eql:gh:`#6519`)\n\n* Fix some bugs involving union and coalescing of optional values\n  (:eql:gh:`#6590`)\n\n* Avoid doing a join when injecting type ids\n  (:eql:gh:`#6601`)\n\n* Generate better code for ?? with multi RHS or object type\n  (:eql:gh:`#6532`)\n\n* Fix pgast nullability inference for subqueries and coalesce\n  (:eql:gh:`#6529`)\n\n* Fix changing a pointer to be computed when there is a subtype\n  (:eql:gh:`#6565`)\n\n4.4\n===\n* Fix DML-containing FOR when the iterator is an object set with duplicates\n  (:eql:gh:`#6609`)\n\n* Fix very slow global introspection query when there are lots of databases\n  (:eql:gh:`#6633`)\n\n* Use correct signature for in-memory cache method\n  (:eql:gh:`#6643`)\n\n4.5\n===\n\n* Fix spurious delete/create of constraints with arguments in migrations\n  (:eql:gh:`#6712`)\n\n* Speed up introspection of system objects like Database and Role\n  (:eql:gh:`#6665`)\n\n* Fix some alter+rename combos on pointers in POPULATE MIGRATION\n  (:eql:gh:`#6666`)\n\n* Fix issue with schema changes when trigger depends on a rewrite\n  (:eql:gh:`#6706`)\n\n* Get verification data from request body\n  (:eql:gh:`#6723`)\n\n* Make overloading a link when omitting ``link`` keyword work\n  (:eql:gh:`#6718`)\n\n4.6\n===\n\n* Fix issues with empty sets leaking out of optional scopes\n  (:eql:gh:`#6747`)\n\n* Fix empty array literals in SQL adapter\n  (:eql:gh:`#6806`)\n\n* Fix duration/memory config in config objects\n  (:eql:gh:`#6827`)\n\n* Fix hanging backend pool with fast connect\n  (:eql:gh:`#6813`)\n\n* Fix changing index expressions in migrations\n  (:eql:gh:`#6843`)\n\n* Properly report errors involving newly created types\n  (:eql:gh:`#6852`)\n\n* Make constraint error details contain useful information for developers\n  (:eql:gh:`#6796`)\n\n* Fix DML coalesce inside of IF/ELSE\n  (:eql:gh:`#6917`)\n\n* Allow trailing comma for GROUP's BY clause\n  (:eql:gh:`#7002`)\n\n* Fix computed single scalar globals\n  (:eql:gh:`#6999`)\n\n* Fix query cache dbver issue with concurrent DDL\n  (:eql:gh:`#6819`)\n\n* Several UI fixes\n\n  * Update behaviour of datatable editor to save input by default on closing\n    (:eql:gh:`edgedb/edgedb-ui/#325`)\n\n  * Store instance version with schema data, so schema is refreshed when\n    instance is upgraded (:eql:gh:`edgedb/edgedb-ui/#333`)\n\n  * De-duplicate queries when navigating repl history with ctrl+up/down\n    (:eql:gh:`edgedb/edgedb-ui/7916ee70`)\n\n  * Add max height to inline editor in dataview\n    (:eql:gh:`edgedb/edgedb-ui/b2fedb72`)\n\n  * Always order ``id`` column first in dataview\n    (:eql:gh:`edgedb/edgedb-ui/9a7c352e`)\n\n  * Improve rendering of long/multiline strings in dataview changes preview\n    (:eql:gh:`edgedb/edgedb-ui/13511ebd`)\n\n  * Fix link props in visual query builder\n    (:eql:gh:`edgedb/edgedb-ui/42492465`)\n\n  * And `many more UI fixes <https://github.com/edgedb/edgedb-ui/commits/4.x/?since=2024-02-25&until=2024-03-15>`__\n\n4.7\n===\n\n* UI: revert a few changes that were meant for 5.x only.\n\n* Revert \"Compile single globals into materialized CTEs\", since it introduced\n  multiple bugs. These bugs were already fixed in 5.x.\n  (:eql:gh:`#6613`)\n\n* Add optional PKCE challenge in email verification\n  (:eql:gh:`#7037`)\n\n* Fix very slow parsing performance of large files with unrecoverable errors\n  (:eql:gh:`#7046`)\n\n* Fix update rewrites on types that are children of updated type\n  (:eql:gh:`#7073`)\n\n4.8\n===\n\n* Use SQL's ``ON CONFLICT`` to implement ``UNLESS CONFLICT`` more often\n  (:eql:gh:`#7472`)\n\n* Prevent dump hangups from leaving stray Postgres queries.\n  (:eql:gh:`#7262`)\n"
  },
  {
    "path": "docs/resources/changelog/5_x.rst",
    "content": "====\nv5.0\n====\n\n:edb-alt-title: EdgeDB v5\n\nTo play with the new features, make sure to specify version 5.0 when\ninitializing the project as pre-release versions are not considered stable\nand will not be automatically suggested:\n\n.. code-block:: bash\n\n  $ edgedb project init\n\n\nUpgrading\n=========\n\n**Local and Cloud instances**\n\nTo upgrade a local project, first ensure that your CLI is up to date with\n``edgedb cli upgrade``. Then run the following command inside the project\ndirectory.\n\n.. code-block:: bash\n\n  $ edgedb project upgrade\n\nAlternatively, specify an instance name if you aren't using a project.\n\n.. code-block:: bash\n\n  $ edgedb instance upgrade -I my_instance\n\nThe CLI will first check to see if your schema will migrate cleanly to |EdgeDB|\n5.0. If the upgrade check finds any problems, it will report them back to you.\n\n**Hosted instances**\n\nTo upgrade a remote (hosted) instance, we recommend the following\ndump-and-restore process.\n\n1. |EdgeDB| v5.0 supports PostgreSQL 14 (or above). So check the version of\n   PostgreSQL you are using before upgrading EdgeDB. If you're using Postgres\n   13 or below, you should upgrade Postgres first.\n\n2. Spin up an empty 5.0 instance. You can use one of our :ref:`deployment\n   guides <ref_guide_deployment>`.\n\n   Under Debian/Ubuntu, when adding the |EdgeDB| package repository, use this\n   command instead:\n\n   .. code-block:: bash\n\n       $ echo deb [signed-by=/usr/local/share/keyrings/edgedb-keyring.gpg] \\\n           https://packages.edgedb.com/apt \\\n           $(grep \"VERSION_CODENAME=\" /etc/os-release | cut -d= -f2) main \\\n           | sudo tee /etc/apt/sources.list.d/edgedb.list\n\n   Use this command for installation under Debian/Ubuntu:\n\n   .. code-block:: bash\n\n       $ sudo apt-get update && sudo apt-get install edgedb-5\n\n   Under CentOS/RHEL, use this installation command:\n\n   .. code-block:: bash\n\n       $ sudo yum install edgedb-5\n\n   In any required ``systemctl`` commands, replace ``edgedb-server-4`` with\n   ``edgedb-server-5``.\n\n   Under any Docker setups, supply the ``5.0`` tag.\n\n3. Take your application offline, then dump your v4.x database with the CLI\n\n   .. code-block:: bash\n\n       $ gel dump --dsn <old dsn> --all --format dir my_database.dump/\n\n   This will dump the schema and contents of your current database to a\n   directory on your local disk called ``my_database.dump``. The directory name\n   isn't important.\n\n4. Restore the empty v5.x instance from the dump\n\n   .. code-block:: bash\n\n       $ gel restore --all my_database.dump/ --dsn <new dsn>\n\n   Once the restore is complete, update your application to connect to the new\n   instance.\n\n   This process will involve some downtime, specifically during steps 2 and 3.\n\n\nNew features\n============\n\nEdgeDB + AI\n-----------\n\nWe've added an ``ext::ai`` extension for handling the integration of |EdgeDB|\nwith various AI backends such as: OpenAI, Mistral and Anthropic.\n\nThere is a special ``ext::ai::index`` that can be used to delegate the search\nfunctionality of |EdgeDB| objects to a specific AI search provider.\n\nThe function ``ext::ai::to_context(object: anyobject)`` evaluates the\nexpression of the specific ``ext::ai::index`` defined on the passed object\ntype and returns it.\n\nThe function ``ext::ai:search(object: anyobject, query: array<float32>)``\nsearches the specified objects using the associated AI search provider and the\nspecified semantic query representation.\n\nThere are also two HTTP API points for interacting with the data:\n\n* ``/ai/embeddings``\n* ``/ai/rag``\n\nEdgeDB branches\n---------------\n\n|EdgeDB| 5.0 adds branching functionality in order to help bridge the gap between\nthe code (and schema) managed by version control systems and the actual\ndevelopment database.\n\nThe first thing to note is that we're updating our terminology and replacing\nthe old (and potentially confusing) term ``database`` with ``branch``. This\nmeans that the old ``create database`` and ``drop database`` commands are\nconsidered deprecated in favor of ``create empty branch`` and ``drop branch``,\nrespectively. However, the new ``branch`` commands provide more options and\nfunctionality than the old ``database`` commands:\n\n1) ``create empty branch <newbranch>``\n\n   The most basic command creates a new branch with an empty schema\n   (exactly the same as ``create database``).\n\n2) ``create schema branch <newbranch> from <oldbranch>``\n\n   This command creates a new branch and copies the schema of an existing\n   branch to it. Only the schema is copied; the data is still empty and needs\n   to be populated separately.\n\n3) ``create data branch <newbranch> from <oldbranch>``\n\n   This command creates a new branch and copies both the schema and the data\n   of an existing branch to it.\n\n4) ``drop branch <oldbranch>``\n\n   Removes an existing branch from the instance.\n\n5) ``alter branch <oldname> rename to <newname>``\n\n   The command to rename a branch.\n\nThe intent is to provide a mechanism that helps developers keep branches of\nthe database corresponding to the code branches that introduce certain schema\nchanges.\n\nWith these new commands, here's how we envision developers using them to\nmanage \"feature\" branches:\n\n1) Create a new \"feature\" VCS branch (a clone of the \"main\" branch) and a\n   corresponding \"feature\" |EdgeDB| branch.\n\n2) Work on the \"feature\" branch, add migrations, etc.\n\n3) When it is time to merge the feature work back into the main branch we want\n   to arrange things so that the \"feature\" branch is in a state that is a\n   simple fast-forward w.r.t the \"main\" branch.\n\n4) In order to achieve the above state we need to make sure the \"main\" code\n   branch as well as the EdgeDB branch are both up-to-date.\n\n5) Then we want to rebase the \"feature\" branch code on top of the \"main\"\n   branch code.\n\n6) After that we need to replicate the same rebase operation with the |EdgeDB|\n   branch. Our CLI tools may need to first clone the \"main\" branch with the\n   data into a \"temp\" branch. Then we can introspect the migration histories\n   of the \"temp\" and \"feature\" branches so that we can establish where they\n   diverge. Take all the divergent migrations from the \"feature\" branch and\n   apply them to the \"temp\" branch. If the operation is successful, drop the\n   \"feature\" branch and rename \"temp\" to \"feature\". We now have successfully\n   rebased \"feature\" branch on top of \"main\".\n\n7) Since the state of \"feature\" is now a straightforward fast-forward w.r.t.\n   the \"main\" branch we can finally merge \"feature\" back into main in VCS and\n   then merge the EdgeDB branch as well (or rename the \"feature\" EdgeDB branch\n   to \"main\", if the old branch is no longer needed).\n\nWe've added :ref:`edgedb branch commands <ref_cli_gel_branch>` to our CLI\nas well that create, copy, rename, drop, and rebase |EdgeDB| branches.\n\n\nUpdated pgvector extension\n--------------------------\n\nA new HNSW (Hierarchical Navigable Small Worlds) index has been added to the\n``pgvector`` extension. Just like IVFFlat indexes there are three flavors of\nHNSW corresponding to different operations:\n\n* ``ext::pgvector::hnsw_euclidean``\n* ``ext::pgvector::hnsw_ip``\n* ``ext::pgvector::hnsw_cosine``\n\nWe have also updated the mechanism for tuning all of the indexes provided in\nthis extension. The ``probes`` (for IVFFlat) and ``ef_search`` (for HNSW)\nparameters can now be accessed via the ``ext::pgvector::Config`` object.\n\nThe current config values can be found by examining the ``extensions`` link of\nthe ``cfg::Config`` object. Notice that in order to see the specific extension\nconfig properties you need to use the type filter :eql:op:`[is\next::pgvector::Config] <isintersect>`:\n\n.. code-block:: edgeql-repl\n\n    db> select cfg::Config.extensions[is ext::pgvector::Config]{*};\n    {\n      ext::pgvector::Config {\n        id: 12b5c70f-0bb8-508a-845f-ca3d41103b6f,\n        probes: 1,\n        ef_search: 40,\n      },\n    }\n\nUpdating the value can be done using the ``configure session`` command:\n\n.. code-block:: edgeql-repl\n\n    db> configure session\n    ... set ext::pgvector::Config::probes := 5;\n    OK: CONFIGURE SESSION\n\nIt is also possible to restore the default config value:\n\n.. code-block:: edgeql-repl\n\n    db> configure session reset ext::pgvector::Config::probes;\n    OK: CONFIGURE SESSION\n\n\nAuthentication\n--------------\n\nWe're bringing two popular \"passwordless\" authentication schemes to our\n``auth`` extension: the Web Authentication API (commonly known as WebAuthn or\nPasskeys), as well as email-based \"magic links\".\n\nWe've also added two popular chat platforms to our list of supported OAuth\nproviders: Slack and Discord.\n\nWe also have the following updates:\n\n* Allow passing WebAuthn ``user_handle`` in request body\n  (:eql:gh:`#6942`)\n\n* Handle WebAuthn challenge having multiple factors\n  (:eql:gh:`#6945`)\n\n* Explicitly pass WebAuthn credential properties\n  (:eql:gh:`#6975`)\n\n* Return JSON for magic link register\n  (:eql:gh:`#6974`)\n\n* Ensure built-in UI verification redirect includes code\n  (:eql:gh:`#6982`)\n\n* Ensure WebAuthn redirect matches expected shape\n  (:eql:gh:`#6987`)\n\n* Fallback to PKCE RFC parameter names\n  (:eql:gh:`#7034`)\n\n* Add optional PKCE challenge in email verification\n  (:eql:gh:`#7037`)\n\n\nAdditional changes\n==================\n\nPerformance\n-----------\n\nThe query compilation cache is now persisted across restarts, and cached\nqueries are automatically recompiled after migrations are applied.\n\nWe've also improved processing of large schemas and migrations.\n\n\nEdgeQL\n------\n\n* Allow omitting ``union`` in ``for`` if the body is a statement.\n  (:eql:gh:`#6810`)\n\n  If the ``for`` query body involves a statement such as ``insert``,\n  ``update``, ``delete``, etc., you no longer need to write the ``union``\n  keyword and add parentheses around the statement expression:\n\n  .. code-block:: edgeql-diff\n\n      for name in {'Alice', 'Billie', 'Cameron'}\n    - union (\n      insert User { name := name }\n    - )\n\n* Add ``administer vacuum()`` command.\n  (:eql:gh:`#6663`)\n\n  The command ``administer vacuum()`` can take a list of object types, multi\n  properties, multi links or links with link properties. There is also a named\n  only argument ``full`` that reclaims storage to the OS rather than just to\n  the database. All of the arguments can be omitted. In case no target types\n  are specified, everything accessible to the user will be vacuumed.\n\n  The vacuum command will use already allocated space better so that it will\n  reduce the growth rate of the database, or will reclaim storage space to the\n  operating system with ``full``. Since certain aspects such as multi\n  properties and links as well as links with link properties require additional\n  underlying tables they can be listed separately when reclaiming storage\n  space.\n\n  If the ``full`` option is set to ``true``, reclaimed storage is returned to\n  the OS, but it can take much longer and will exclusively lock the underlying\n  tables.\n\n  For example, the following command will vacuum the ``User`` type reclaiming\n  storage to the OS:\n\n  .. code-block:: edgeql\n\n    administer vacuum(User, full := true)\n\n* Integer/UUID to bytes conversion.\n  (:eql:gh:`#6553`)\n\n  It is now possible to convert :eql:type:`int16`, :eql:type:`int32`,\n  :eql:type:`int64`, and :eql:type:`uuid` to :eql:type:`bytes` and vice-versa\n  using the corresponding conversion functions.\n\n  Use the :eql:func:`to_bytes` to convert values into :eql:type:`bytes`:\n\n  .. code-block:: edgeql-repl\n\n      db> select to_bytes(<int32>31, Endian.Big);\n      {b'\\x00\\x00\\x00\\x1f'}\n      db> select to_int32(b'\\x01\\x02\\x00\\x07', Endian.Big);\n      {16908295}\n\n      db> select to_bytes(<uuid>'1d70c86e-cc92-11ee-b4c7-a7aa0a34e2ae');\n      {b'\\x1dp\\xc8n\\xcc\\x92\\x11\\xee\\xb4\\xc7\\xa7\\xaa\\n4\\xe2\\xae'}\n      db> select to_uuid(\n      ...   b'\\x92\\x67\\x3a\\xfc\\\n      ...     \\x9c\\x4f\\\n      ...     \\x42\\xb3\\\n      ...     \\x82\\x73\\\n      ...     \\xaf\\xe0\\x05\\x3f\\x0f\\x48');\n      {92673afc-9c4f-42b3-8273-afe0053f0f48}\n\n\n* Add ``bytes`` option to ``array_join``.\n  (:eql:gh:`#6918`)\n\n  The :eql:func:`array_join` can now operate on :eql:type:`bytes` the same way\n  it operates on :eql:type:`str`:\n\n  .. code-block:: edgeql-repl\n\n      db> select array_join([b'\\x01', b'\\x02', b'\\x03'], b'\\xff');\n      {b'\\x01\\xff\\x02\\xff\\x03'}\n\n* Support closing all connections to a database on ``drop database``.\n  (:eql:gh:`#6780`)\n\n* Add a ``std::get_current_branch()`` function.\n  (:eql:gh:`#7001`)\n\n* Add ``cfg::Config.query_cache_mode``\n  (:eql:gh:`#7158`)\n\nBug fixes\n---------\n\n* Fix issues with empty sets leaking out of optional scopes\n  (:eql:gh:`#6747`)\n\n* Fix an SDL scalar type dependency bug\n\n* Suppress idle transaction timeout during migrations\n  (:eql:gh:`#6760`)\n\n* Use a consistent interface for ``ext::auth`` errors\n  (:eql:gh:`#6751`)\n\n* Stop recording extension version in dumps\n  (:eql:gh:`#6787`)\n\n* For any index changes don't attempt to update the index, drop and recreate\n  instead\n  (:eql:gh:`#6797`, :eql:gh:`#6843`)\n\n* Fix duration/memory config in config objects\n  (:eql:gh:`#6827`)\n\n* Properly report errors involving newly created types\n  (:eql:gh:`#6852`)\n\n* Changes to vector length in migrations result in suggesting a\n  ``drop``/``create``\n  (:eql:gh:`#6882`)\n\n* Report topological cycle errors in migrations as real errors\n  (:eql:gh:`#6883`)\n\n* Make constraint error details contain useful information for developers\n  (:eql:gh:`#6796`)\n\n* Fix interaction between DML and ``if...then...else``\n  (:eql:gh:`#6917`)\n\n* Don't leak objects out of access policies when used in a computed global\n  (:eql:gh:`#6926`)\n\n* Allow grouping to have trailing comma\n  (:eql:gh:`#7002`)\n\n* Fix computed single scalar globals\n  (:eql:gh:`#6999`)\n\n* Fix ISE when creating an alias with a name that already exists\n  (:eql:gh:`#6946`)\n\n* Fix parser at unrecoverable errors\n  (:eql:gh:`#7046`)\n\n* Improve error when applying a shape to a parameter\n  (:eql:gh:`#7044`)\n\n* Skip creating @source/@target on derived views improving performance\n  (:eql:gh:`#7051`)\n\n* Fix issues with cached global shapes and global cardinality inference\n  (:eql:gh:`#7062`)\n\n* Add error when a constant set is used in singleton mode\n  (:eql:gh:`#7065`)\n\n* Fix update rewrites on types that are children of updated type\n  (:eql:gh:`#7073`)\n\n* Make escaping strings more consistent\n  (:eql:gh:`#7059`)\n\n* Allow an update to trigger an insert of the same type, and vice versa\n  (:eql:gh:`#7082`)\n\n* Set \"Connection: close\" for non-keep-alive requests\n  (:eql:gh:`#7087`)\n\n* Fix volatility of ``fts::search``\n  (:eql:gh:`#7106`)\n\n* Allow trailing commas and semicolons in most places\n  (:eql:gh:`#6963`)\n\n* Drop special handling of type intersection in cardinality inference\n  (:eql:gh:`#7089`)\n\n* Add error when :eql:type:`enum` length exceeds 63\n  (:eql:gh:`#7123`)\n\n* Fix two issues directly reading pointers from a group\n  (:eql:gh:`#7130`)\n\n* Check singleton expressions in constraints and indexes\n  (:eql:gh:`#7128`)\n\n* Fix two bugs affecting unions in computed links\n  (:eql:gh:`#7139`)\n\n* Fix two ``group`` bugs involving ``using`` clauses\n  (:eql:gh:`#7143`)\n\n* Fix deserialization of persistent cache entries after upgrade\n  (:eql:gh:`#7203`)\n\n* Accept session changes in transactions\n  (:eql:gh:`#7187`)\n\n* Fix ISEs in constant detection for ``fts::with_options``\n  (:eql:gh:`#7192`)\n\n* pg_ext: don't yield NoData in SimpleQuery\n  (:eql:gh:`#7200`)\n\n* Make changing ``fts`` and ``ai`` indexes work consistently in migrations\n  (:eql:gh:`#7218`)\n\n* Include ``fts`` and ``ai`` shadow index columns in dumps\n  (:eql:gh:`#7235`)\n\n5.1\n===\n\n* Make ai::search have integrated sort and hit indexes\n  (:eql:gh:`#7242`)\n\n* Fix upgrading from rc1 that had been updated itself from a beta\n  (:eql:gh:`#7245`)\n\n5.2\n===\n\n* Allow multiple authentication methods per transport in\n  ``--default-auth-method``.\n  (:eql:gh:`#7224`)\n\n  We now allow multiple authentication methods to be tried in sequence\n  (according to the specified order in ``--default-auth-method``).\n\n* Drop ad-hoc TLS requirement from ``JWT`` and ``Password`` auth\n  (:eql:gh:`#7231`)\n\n* Reject ``ai`` indexes that have different parameters than in parent types\n  (:eql:gh:`#7229`)\n\n* Allow except in link constraints.\n  (:eql:gh:`#7250`)\n\n5.3\n===\n\n* Force return cast on range get upper and lower functions.\n  (:eql:gh:`#7251`)\n\n* Prevent dump hangups from leaving stray Postgres queries.\n  (:eql:gh:`#7262`)\n\n* Switch ``EDGEDB_DEBUG_EDGEQL_TEXT_IN_SQL`` to encode string as\n  :eql:type:`json`\n  (:eql:gh:`#7267`)\n\n* Don't inject exclusive conflict checks for updates without children.\n  (:eql:gh:`#7271`)\n\n* Fix doing a no-op ``update`` to an exclusive multi pointer with children.\n  (:eql:gh:`#7272`)\n\n* Fix constraint handling when pointer has cardinality or computedness\n  changed.\n  (:eql:gh:`#7279`)\n\n* Fix regression in using some :eql:type:`tuple` literals as a default.\n  (:eql:gh:`#7281`)\n\n* Make link properties (including @source/@target) work in conflict selects.\n  (:eql:gh:`#7284`)\n\n* Create key derivation function for signing each different kind of JWTs in\n  the ``auth`` extension\n  (:eql:gh:`#7285`)\n\n  This avoids accidentally being able to use other (short-lived) JWT tokens\n  as the ``auth_token`` JWT directly.\n\n5.4\n===\n\n* Improve error message when creating union with incompatible types.\n  (:eql:gh:`#7278`)\n\n* Fix handling of enums in arrays and multi properties for GraphQL.\n  (:eql:gh:`#3990`)\n\n* Fix modifying global that is used in a policy on a type it refers to.\n  (:eql:gh:`#7310`)\n\n* Fix a bug involving globals in a somewhat complex interaction with policies.\n  (:eql:gh:`#7314`)\n\n* Set content-type header for AI extension errors.\n  (:eql:gh:`#7324`)\n\n* Fix an ``UNLESS CONFLICT`` on links performance regression.\n  (:eql:gh:`#7349`)\n\n* Fix ``EDGEDB_SERVER_CONFIG`` configuration of enum values.\n  (:eql:gh:`#7350`)\n\n* Only decode url encoded slashes in db/branch name after splitting path into\n  parts.\n  (:eql:gh:`#7352`)\n\n* Fix an issue with collection types that affected some migrations.\n  (:eql:gh:`#7375`)\n\n5.5\n===\n\n* Fix recompilation slowdowns after several migrations\n  (:eql:gh:`#7099`)\n\n* Fix dumps of types that have both AI and FTS indexes\n  (:eql:gh:`#7405`)\n\n* Fix single-tenant metrics not to filter by tenant\n  (:eql:gh:`#7385`)\n\n* Fix rewrite expressions using ``__specified__`` sometimes\n  generating InvalidReferenceError.\n  (:eql:gh:`#7392`)\n\n* Raise error when using query params in schema.\n  (:eql:gh:`#7400`)\n\n* Add expression to index friendly name.\n  (:eql:gh:`#7401`)\n\n* Treat target and link properties as different when expanding splats.\n  (:eql:gh:`#7402`)\n\n* Remove redundant primary key constraint on 'id'.\n  (:eql:gh:`#7418`)\n\n* workflows: Use an explicit label when selecting runners for builds\n  (:eql:gh:`#7416`)\n\n* Log errors raised when handling ext::ai HTTP requests\n  (:eql:gh:`#7436`)\n\n* ai: Properly forward non-successful responses from non-streaming chat\n  (:eql:gh:`#7440`)\n\n* Fix ai::to_context duplication\n  (:eql:gh:`#7464`)\n\n* Support configuring more arguments with env vars\n  (:eql:gh:`#7470`)\n\n* Use SQL's ``ON CONFLICT`` to implement ``UNLESS CONFLICT`` more often\n  (:eql:gh:`#7472`)\n\n* Fix a race condition where older database configs can overwrite newer ones\n  (:eql:gh:`#7485`)\n\n* Allow subdomains in redirects in the ``auth`` extension\n  (:eql:gh:`#7488`)\n\n* Fix tenant shutdown in multi-tenant mode\n  (:eql:gh:`#7495`)\n\n* Fix migration creation when adding a new base class with certain constraints\n  (:eql:gh:`#7508`)\n\n* Fix ``pg_dump`` of an empty schema using the SQL adapter\n  (:eql:gh:`#7445`)\n\n* Fix SQL adapter ``COPY`` command\n  (:eql:gh:`#7446`)\n\n5.6\n===\n\n* Fix interaction of implicit limit with explicit OFFSET\n  (:eql:gh:`#7509`)\n\n* Make persistent query cache work for queries that have no constant\n  literals in them\n  (:eql:gh:`#7237`)\n\n* Unbreak cache recompilation after a restart\n  (:eql:gh:`#7515`, :eql:gh:`#7520`)\n\n* Add a ``auto_rebuild_query_cache_timeout`` config setting that controls\n  how long the server will spend recompiling cached queries after a migration.\n  The default is one minute.\n  (:eql:gh:`#7518`)\n\n5.7\n===\n* Include secrets in config objects when dumping with --include-secrets\n\n* Forbid certain system functions over SQL adapter\n  (:eql:gh:`#7829`)\n\n* Update bundled PostgreSQL to 16.4\n  (:eql:gh:`#7804`)\n\n* Fix PgFunc compiled query cache\n  (:eql:gh:`#7422`)\n\n* Fix SQL connections with errors by dropping send_sync_on_error\n  (:eql:gh:`#7560`)\n\n* func cache: fix dropping extension with scalar type\n  (:eql:gh:`#7564`)\n\n* Fix inconsistent prepared statement in script\n  (:eql:gh:`#7571`)\n\n* Fix pg_get_serial_sequence in SQL adapter\n  (:eql:gh:`#7581`)\n\n* Fix ISE involving UNLESS CONFLICT and WITH interaction\n  (:eql:gh:`#7785`)\n\n* Fix remaining failing scalar type drops due to cache\n  (:eql:gh:`#7607`)\n\n* Drop dependent cache function of tuple type\n  (:eql:gh:`#7616`)\n\n5.8\n===\n* Some more cleanup of implicit limits\n  (:eql:gh:`#7517`)\n\n* Fix email button background\n  (:eql:gh:`#7974`)\n\n* Fix schema ordering issue with Trigger when clauses\n  (:eql:gh:`#8060`)\n\n* Fix config bugs with env vars and default checking\n  (:eql:gh:`#8078`)\n\n* Fix static evaluation TypeCast str->bool bug\n  (:eql:gh:`#8113`)\n\n* Add some more functions to list of allowed postgres admin functions\n  (:eql:gh:`#8139`, :eql:gh:`#8298`)\n\n* Fix strchrnul build failures on recent glibc\n  (:eql:gh:`#8154`)\n\n* Fix NULLs in re_match/re_match_all returns\n  (:eql:gh:`#8069`)\n\n* Send ``identity_id`` on require_verification\n  (:eql:gh:`#8170`)\n\n* Monitor open FDs\n  (:eql:gh:`#8217`)\n\n* Fix dump and MIGRATION REWRITE when there are many (>1000) migrations\n  (:eql:gh:`#8240`)\n\n* multitenant: retry adding tenants more eagerly\n  (:eql:gh:`#8236`)\n\n* Allow dropping isolation level to REPEATABLE READ in a READ ONLY tx\n  (:eql:gh:`#8237`)\n\n* Use better histogram buckets for metrics\n  (:eql:gh:`#8263`)\n\n* Use sys_pgcon for long-term advisory locks\n  (:eql:gh:`#8320`)\n"
  },
  {
    "path": "docs/resources/changelog/6_x.rst",
    "content": "====\nv6.0\n====\n\n:edb-alt-title: Gel v6\n\nTo explore the new features, ensure you specify version 6.0 when initializing\nyour project. Pre-release versions are not considered stable and will not be\nautomatically suggested:\n\n.. code-block:: bash\n\n  $ gel project init --server-version 6.0-rc.2\n\n\nUpgrading\n=========\n\n.. edb:collapsed::\n\n**Local instances**\n\nTo upgrade a local project, first ensure that your CLI is up to date with\n:gelcmd:`cli upgrade`. Then run the following command inside the project\ndirectory.\n\n.. code-block:: bash\n\n  $ gel project upgrade --to-testing\n\nAlternatively, specify an instance name if you aren't using a project:\n\n.. code-block:: bash\n\n  $ gel instance upgrade -I my_instance\n\nThe CLI will check if your schema can migrate cleanly to Gel 6.0. If any\nissues are found, they will be reported.\n\n**Hosted instances**\n\nTo upgrade a remote instance, we recommend the following dump-and-restore\nprocess:\n\n1. Gel v6.0 supports PostgreSQL 14 or above. Verify your PostgreSQL version\n   before upgrading Gel. If you're using Postgres 13 or below, upgrade\n   Postgres first.\n\n2. Spin up an empty 6.0 instance. You can use one of our :ref:`deployment\n   guides <ref_guide_deployment>`.\n\n   For Debian/Ubuntu, when adding the Gel package repository, use this\n   command:\n\n   .. code-block:: bash\n\n       $ echo deb [signed-by=/usr/local/share/keyrings/gel-keyring.gpg] \\\n           https://packages.geldata.com/apt \\\n           $(grep \"VERSION_CODENAME=\" /etc/os-release | cut -d= -f2) main \\\n           | sudo tee /etc/apt/sources.list.d/gel.list\n       $ sudo apt-get update && sudo apt-get install gel-6\n\n   For CentOS/RHEL, use this installation command:\n\n   .. code-block:: bash\n\n       $ sudo yum install gel-6\n\n   In any required ``systemctl`` commands, replace ``edgedb-server-5`` with\n   ``gel-server-6``.\n\n   For Docker setups, use the ``6.0`` tag.\n\n3. Take your application offline, then dump your v5.x database with the CLI:\n\n   .. code-block:: bash\n\n       $ gel dump --dsn <old dsn> --all --format dir my_database.dump/\n\n   This will dump the schema and contents of your current database to a\n   directory on your local disk called ``my_database.dump``. The directory name\n   isn't important.\n\n4. Restore the empty v6.x instance from the dump:\n\n   .. code-block:: bash\n\n       $ gel restore --all my_database.dump/ --dsn <new dsn>\n\n   Once the restore is complete, update your application to connect to the new\n   instance.\n\n   This process will involve some downtime, specifically during steps 2 and 3.\n\n\nNew features\n============\n\nSQL write support\n-----------------\n\nYou can now use SQL DML (``insert``, ``update``, ``delete``) when connecting to\nyour |Gel| instance via the PostgreSQL protocol. Our aim is to support most\ntypical use cases from tools like SQL ORMs and SQL clients.\n\nThis allows more developers to use Gel, leveraging our advanced data model,\ntooling, and high-performance connection management. Teams can migrate their\nexisting SQL codebases to Gel without rewriting their queries. Once adopted,\nyou can gradually take advantage of EdgeQL's powerful query capabilities.\n\nExisting Gel users who already use EdgeQL can benefit too. While some SQL\nfeatures like window functions, recursive queries, and explicit locking are not\nyet supported, you can use these features in SQL today. We will continue to add\nsupport for more features in the future.\n\nIn-place upgrade\n----------------\n\nWe aim for this version to be the last requiring a full dump and restore\nprocess for major version upgrades. We understand that dump-and-restore is\ndisruptive, so enabling in-place upgrades will make it easier for teams to\nupgrade more frequently.\n\nQuery performance observability\n-------------------------------\n\nWe now store statistics about query performance. These statistics are available\nin ``sys::QueryStats`` objects.\n\n.. code-block:: edgeql\n\n  select sys::QueryStats {\n    query,\n    queryType,\n    tag,\n    plans,\n    total_plan_time,\n    mean_plan_time,\n    calls,\n    total_exec_time,\n    mean_exec_time,\n  } filter .branch.name = sys::get_current_branch();\n\nMore details to come in the reference documentation.\n\next::postgis\n----------------\n\nWe've added support for the popular PostGIS extension for PostgreSQL. This\nextension adds support for geographic objects and spatial data types.\n\nstd::net\n------------\n\nWe've introduced a new standard library module for sending network requests,\ninitially supporting HTTP. This module schedules asynchronous requests and\nallows you to poll for responses.\n\next::auth\n-------------\n\nWe've introduced several new features to our authentication extension:\n\n- You can now configure generic OpenID Connect providers.\n- If using an OAuth provider that returns an ``id_token`` (like an OpenID\n  Connect compatible provider), you will now receive that validated token in\n  your callback. This simplifies using some of that data for your own User or\n  Profile objects, saving a roundtrip to the identity provider.\n- As an alternative (or in addition) to configuring SMTP for sending emails,\n  you can now configure a webhook for various authentication lifecycle events.\n  Use these webhooks to send custom emails, update analytics, or trigger other\n  workflows.\n- Previously, a missing PKCE session during email verification was treated as\n  an error. Now, we support verifying end-user emails from a different device\n  than the one used to start the sign-up or sign-in process. To enable\n  verification without PKCE, direct the end-user to attempt a login after\n  verifying their email, which will initiate a new flow.\n\n  Previously, the application couldn't identify which identity was being\n  created during sign-up until email verification was successful. When\n  verification occurred on the same device, it concluded with an auth token,\n  allowing the creation of a new ``User`` based on that token's identity. With\n  the new process, where users are directed to sign in after email\n  verification, there's no clear distinction between a regular sign-in (which\n  shouldn't create a new ``User``) and an interrupted sign-up (which should\n  create a new ``User``). To address this, we now return an ``identity_id`` in\n  the sign-up response, enabling you to create a ``User`` type before the email\n  is verified.\n- We now configure a development-only SMTP provider for instances hosted on\n  our Cloud. This SMTP proxy is heavily rate limited, and requires a fixed\n  sender email address. It is intended to be used for development and testing\n  purposes. Once you're ready to start sending real emails, you can configure\n  your own SMTP provider. We hope this will make it easier to get started with\n  a simple email-based authentication flow during early development.\n- Handle multiple WebAuthn email factors.\n  (:eql:gh:`#7861`)\n- Add logs to ``auth`` extension.\n  (:eql:gh:`#7944`)\n- Migrate ``ext::auth::SMTPConfig`` to ``cfg::EmailProvider``.\n  (:eql:gh:`#7942`)\n- Allow Magic Link to specify a custom link URL.\n  (:eql:gh:`#8030`)\n- Do not fail if SMTP provider is not configured.\n  (:eql:gh:`#8228`)\n\n**Breaking changes**\n\n- We have moved our SMTP configuration into a new top-level\n  ``cfg::SMTPProviderConfig`` configuration object. During the upgrade process,\n  your existing SMTP configuration will be migrated to this new object. If you\n  have any scripts that configure SMTP directly, update them to use the new\n  object.\n\next::ai\n-----------\n\n- We've updated the built-in list of models from our first-party LLM providers\n  to match the latest offerings from OpenAI, Anthropic, and Mistral.\n- We now pass LLM configuration query parameters through to the downstream\n  provider.\n- Add delays to AI embeddings requests based on rate limits provided by\n  provider.\n- Allow specifying underlying vector dimensions when creating an index.\n  (:eql:gh:`#8068`)\n\nSimpler scoping rules\n---------------------\n\nWe've simplified the scoping rules for queries. See `our RFC 1027 outlining the\nchanges <https://github.com/geldata/rfcs/blob/master/text/1027-no-factoring.rst>`_.\n\nThe RFC highlights two main reasons for removing path factoring: the need to\nsimplify and enhance the language, and concerns about implementation. Path\nfactoring is complex and makes it hard to quickly understand a query's\nbehavior. It also undermines several key design principles of EdgeQL. Although\nEdgeQL is intended to be read from top to bottom, path factoring allows later\nparts of a query to change its meaning significantly.\n\nBy default in 6.0, we will generate new schemas that opt-in to the new scoping\nrules. Existing schemas will continue to use the old rules and emit warnings\nwhen queries that trigger the old behavior are encountered at query time.\n\nCommand Hooks\n-------------\n\nWe've added hooks for |gelcmd| CLI operations. Certain operations like\nswitching branches or applying migrations have a profound effect on the state\nof the database. These types of changes may occasionally need to be\nsynchronized with the rest of the project codebase. Whether it's a need to\nre-run some schema introspection tools or some fixture validation or\nre-generation tools, we now have a way to add hooks that will automatically\nexecute after certain commands in your project.\n\nThese hooks are declared in the |gel.toml| file. For example:\n\n.. code-block::\n\n    [hooks]\n    schema.update.after=\"scripts/extract_schema_docs.sh\"\n\nThis would run ``scripts/extract_schema_docs.sh`` script any time the schema\nchanges (whether due to branch switch or applying a migration). In this\nexample the script is meant to introspect the schema annotations and\nautomatically generate some documentation files used in the project. But the\nmechanism is flexible enough to be used for automating a variety of project\ntasks.\n\nSee `our RFC 1028 for more details on the changes\n<https://github.com/edgedb/rfcs/blob/master/text/1028-cli-hooks.rst>`_.\n\nFile Watchers\n-------------\n\nWe've also added a way to respond to certain file changes within a project.\nThe |gel.toml| now supports ``[[watch]]`` configuration to specify the files\nbeing watched and the script to be executed when changes occur. In order to\nenable this mode the |gelcmd| ``watch`` command is used.\n\nFor example, the following configuration will watch for changes in the queries\nfiles and automatically attempt to re-generate the functions that allow\nexecuting these queries in a type-safe way:\n\n.. code-block::\n\n    [[watch]]\n    files = [\"queries/*.edgeql\"]\n    script = \"npx @edgedb/generate queries\"\n\nMultiple ``[[watch]]`` entries can be added to the |gel.toml| file, so that\nyou can fine-tune how your project responds to important file changes.\n\nThis changes how ``watch`` command functions. By default, ``gel watch`` will\nstart the watch process and monitor files specified in |gel.toml|. In order to\naccess the old functionality of ``edgedb watch`` (which was monitoring schema\nfile changes and automatically applying them to the database) you now need to\nrun ``gel watch --migrate``.\n\nSee `our RFC 1028 for more details on the changes\n<https://github.com/edgedb/rfcs/blob/master/text/1028-cli-hooks.rst>`_.\n\n\nAdditional changes\n==================\n\nEdgeQL\n------\n\n* Free objects no longer have an ``id`` property. This is a breaking\n  change, though we expect the real-world impact to be minor. If you\n  need a dynamically generated new ID for free objects, it can be\n  added manually, using one of our UUID generation functions.\n\n  .. code-block:: edgeql\n\n    select {\n        id := uuid_generate_v1mc(),\n        property := 'Some string',\n    }\n\n* Add ``__default__`` keyword to refer to default value.\n  (:eql:gh:`#7214`)\n\n  This keyword allows referring to the default value in ``insert`` and\n  ``update`` statements. For example, consider the following schema:\n\n  .. code-block:: sdl\n\n      type Item {\n          name: str { default := 'New item' }\n      }\n\n  We can then insert a 'New item #1' by using the ``__default__`` value:\n\n  .. code-block:: edgeql-repl\n\n      db> insert Item {name := __default__ ++ ' #1'};\n      {default::Item {id: ebcfff62-eb91-11ef-a6b9-5ffb2f0b2940}}\n      db> select Item{name};\n      {default::Item {name: 'New item #1'}}\n\n* Add support for type expressions in intersections.\n  (:eql:gh:`#7172`)\n\n  Allow using ``&`` and ``|`` in expressions like this:\n\n  .. code-block:: edgeql\n\n      select Shape[is Circle | Triangle & HasRightAngle];\n\n* Add array modifying functions :eql:func:`array_set` and\n  :eql:func:`array_insert`.\n  (:eql:gh:`#7427`)\n\n* Add trigonometry functions.\n  (:eql:gh:`#8071`)\n\n  Add :eql:func:`math::pi`, :eql:func:`math::acos`, :eql:func:`math::asin`,\n  :eql:func:`math::atan`, :eql:func:`math::atan2`, :eql:func:`math::cos`,\n  :eql:func:`math::cot`, :eql:func:`math::sin`, :eql:func:`math::tan`.\n\n* Allow ``update`` and ``delete`` on type intersections.\n  (:eql:gh:`#7655`)\n\n  Given types ``A``, ``B``, and ``C``, allows expressions such as:\n\n  .. code-block:: edgeql\n\n      update A[is B & C]\n      set { foo := 123 }\n\n* Implement ``\\(expr)``-style string interpolation.\n  (:eql:gh:`#8210`)\n\n  This enables the following expression:\n\n  .. code-block:: edgeql-repl\n\n      db> select \"1 + 1 = \\(1 + 1)\"\n      {'1 + 1 = 2'}\n\n* Allow complex types as function params.\n  (:eql:gh:`#7759`)\n\n  Allow functions such as:\n\n  .. code-block:: sdl\n\n      function foo(x: A | B) -> int64 using (x.n);\n\n* Search ``std`` for module name when using ``with`` clause.\n  (:eql:gh:`#7753`, :eql:gh:`#7836`, :eql:gh:`#7743`)\n\n  We've consolidated many of the built-in modules as sub-modules under the\n  ``std`` umbrella.\n\n* Support accessing link properties through :eql:stmt:`for` bindings.\n  (:eql:gh:`#7805`)\n\n  Now you can write something like this:\n\n  .. code-block:: edgeql\n\n      select User {\n          cards := ((\n              for c in .deck[is HeartsCard]\n              select (c.name, c@order)\n          )),\n      }\n\n* Enable DML in user-defined functions.\n  (:eql:gh:`#7945`)\n\n  It is now possible to create this kind of function:\n\n  .. code-block:: sdl\n\n      function add_foo(x: int64) -> Foo using ((\n          insert Foo { val := x }\n      ));\n\n  A new ``Modifying`` volatility level is introduced to represent this.\n  (:eql:gh:`#7808`)\n\n* Support ``drop extension package`` of user-installed extensions.\n  (:eql:gh:`#7926`)\n\n* Warn when a ``filter`` clause has ``Many`` cardinality.\n  (:eql:gh:`#8089`)\n\n* Mark :eql:func:`assert_exists`, :eql:func:`assert_single`, and\n  :eql:func:`assert_distinct`  functions as being ``Immutable``.\n  (:eql:gh:`#8292`)\n\n* Expose ``administer statistics_update()``.\n  (:eql:gh:`#8335`)\n\n\nOther changes\n-------------\n\n* Require extension modules to live in ``ext::``.\n  (:eql:gh:`#7526`)\n\n* Use LRU/MRU to improve connection re-use.\n  (:eql:gh:`#7583`)\n\n* Change how globals affect internal alias names that may appear in\n  introspection.\n  (:eql:gh:`#7641`)\n\n* Rename ``sys::Database`` to ``sys::Branch``.\n  (:eql:gh:`#7653`)\n\n* Add ``std::net`` and ``std::net::http`` modules.\n  (:eql:gh:`#7676`, :eql:gh:`#7736`)\n\n* Add ``sys::Branch.last_migration``.\n  (:eql:gh:`#7654`)\n\n* Record SDL in ``schema::Migration`` object.\n  (:eql:gh:`#7673`)\n\n* Get ``std::net::http`` max connections from config.\n  (:eql:gh:`#7767`)\n\n* Add extension ``ext::pg_unaccent``.\n  (:eql:gh:`#7741`)\n\n* Implement warnings in the server.\n  (:eql:gh:`#7823`)\n\n* Update the ``ext::pgvector`` extension.\n  (:eql:gh:`#7812`)\n\n  Add ``sparcevec`` and ``halfvec`` types and update indexes and operators.\n\n* Avoid computing globals json if not needed for inlined function.\n  (:eql:gh:`#7920`)\n\n* Allow volatile ``with`` in DML statements.\n  (:eql:gh:`#7969`)\n\n* Make ``admin`` the default role instead of ``edgedb``.\n  (:eql:gh:`#8010`)\n\n* Support extension upgrades.\n  (:eql:gh:`#7998`)\n\n* Replace headers with annotations in Parse/Execute.\n  (:eql:gh:`#8037`)\n\n* Add TOML config file support.\n  (:eql:gh:`#8121`)\n\n* Allow tuples in GIN, GIST and BRIN indexes.\n  (:eql:gh:`#8232`)\n\n* Add a ``cors-always-allowed-origins`` option.\n  (:eql:gh:`#8233`)\n\n\nBug fixes\n---------\n\n* Tweak parser to correctly report certain missing semicolons.\n  (:eql:gh:`#7252`)\n\n* Fix regression in using some tuple literals as a default.\n  (:eql:gh:`#7281`)\n\n* Fix handling of enums in arrays and multi properties in GraphQL.\n  (:eql:gh:`#3990`)\n\n* Improve error message when casting to collections.\n  (:eql:gh:`#7300`)\n\n* Improve :eql:type:`json` cast error messages.\n  (:eql:gh:`#7312`)\n\n* Improve error when accessing a non-existent tuple field.\n  (:eql:gh:`#7373`)\n\n* Fix an ISE on some specific operations with arrays.\n  (:eql:gh:`#7363`)\n\n* Catch illegal aggregate calls in constraints and indexes during\n  ``migration create``.\n  (:eql:gh:`#7343`)\n\n* Raise error when computed ``global`` is set or reset.\n  (:eql:gh:`#7374`)\n\n* Improve error messages for casts, :eql:op:`is`, and :eql:op:`introspect`.\n  (:eql:gh:`#7351`)\n\n* Fix recursive definition error when computed property refers to different\n  object's computed property.\n  (:eql:gh:`#7431`)\n\n* Fix issue with abstract types, exclusive constraints, and ``analyze``.\n  (:eql:gh:`#7454`)\n\n* Fix an issue with deletion policies affecting properties.\n  (:eql:gh:`#7675`)\n\n* Fix errors when resolving type intersections.\n  (:eql:gh:`#7662`)\n\n* Fix issues with ``branch`` commands that arise from network issues.\n  (:eql:gh:`#7773`)\n\n* Fix a regression involving optional arguments and :eql:func:`assert_exists`.\n  (:eql:gh:`#7798`)\n\n* Include more information in HTTP protocol errors.\n  (:eql:gh:`#7817`)\n\n* Raise error when passing multi cardinality args to modifying functions.\n  (:eql:gh:`#7816`)\n\n* Fix an issue with cardinality of :eql:type:`json` parameters.\n  (:eql:gh:`#7843`)\n\n* Encode offset positions as integers in json error encoding.\n  (:eql:gh:`#7842`)\n\n* Make ``configure current database`` block until configuration is actually\n  set.\n  (:eql:gh:`#7865`)\n\n* Fix shape not being copied when inlining parameters.\n  (:eql:gh:`#7872`)\n\n* Garbage collect ``std::net::http::ScheduleRequest``.\n  (:eql:gh:`#7888`)\n\n* Fix error when dropping non overloaded function.\n  (:eql:gh:`#7899`)\n\n* Fix embedding data being stored with the wrong entry.\n  (:eql:gh:`#7932`)\n\n* Fix an obscure bug deleting doubly nested alias tuples.\n  (:eql:gh:`#7956`)\n\n* Fix an schema issue with deleting collections.\n  (:eql:gh:`#7957`)\n\n* Automatically create array types for all scalars to avoid introspection\n  issues.\n  (:eql:gh:`#7970`)\n\n* Fix an edge case of calling value functions from range vars.\n  (:eql:gh:`#7982`)\n\n* Fix ISE when enumerating a call to an aggregate function.\n  (:eql:gh:`#7988`)\n\n* Fix free objects being materialized as if they are volatile.\n  (:eql:gh:`#8000`)\n\n* Fix aliases always being considered ``Immutable``.\n  (:eql:gh:`#8009`)\n\n* Fix ISE when taking intersection of types with pointers of the same name.\n  (:eql:gh:`#8012`)\n\n* Fix static types in subtypes under ``sysconfig``.\n  (:eql:gh:`#8054`)\n\n* Fix extension package installation on non ``--testmode`` servers.\n  (:eql:gh:`#8096`)\n\n* Fix cache key of source.\n  (:eql:gh:`#8103`)\n\n* Fix handling of invalid link properties.\n  (:eql:gh:`#8156`)\n\n* Delete old ``.s.EDGEDB.admin.XXX`` sockets.\n  (:eql:gh:`#8248`)\n\n* Fix broken error messages for type mismatches in a number of schema objects.\n  (:eql:gh:`#8294`)\n\n* Don't emit instance configs matching their defaults in\n  ``describe instance config``.\n  (:eql:gh:`#8316`)\n\n\n6.1\n===\n\n* Report migration count and object count as metrics\n  (:eql:gh:`#8369`)\n\n* Increase recursion limit in compiler worker processes.\n  This should fix dumps in some cases.\n  (:eql:gh:`#8379`)\n\n\n6.2\n===\n\n* Fix systemd service descriptions in distro packages\n\n* Fix tagging SQL over binary protocol\n  (:eql:gh:`#8371`)\n\n* Improve error message for $0 over SQL adapter\n  (:eql:gh:`#8388`)\n\n* Fix referencing ``ext::auth::ClientTokenIdentity`` from a link default\n  (:eql:gh:`#8394`)\n\n* Add default value for ``ai::EmbeddingModel::embedding_model_max_batch_tokens``\n  (:eql:gh:`#8406`)\n\n* Prevent ai extension connecting to branch being dropped\n  (:eql:gh:`#8416`)\n\n* Fix ``ON TARGET DELETE ALLOW`` behavior when link optionality is changed\n  (:eql:gh:`#8419`)\n\n* Fix server error when trying to register a new WebAuthn identity in the auth extension\n  (:eql:gh:`#8428`)\n\n* Fix schema reflection losing track of changes made while ``using future``\n  (:eql:gh:`#8435`)\n\n* Use correct docker repository when checking tags\n  (:eql:gh:`#8407`)\n\n* Fix parser span over SQL adapter\n  (:eql:gh:`#8399`)\n\n* Fix ``IS`` when the left-hand side is not just a path\n  (:eql:gh:`#8410`)\n\n6.3\n===\n\n* Make ``std::net`` polling output quieter\n  (:eql:gh:`#8450`)\n\n* Fix regression with ``UPDATE`` on a complex path with rewrite\n  (:eql:gh:`#8463`)\n\n* Make free objects always considered UNIQUE. This also fixes a regression\n  seen when using ``assert_distinct`` on free objects.\n  (:eql:gh:`#8464`)\n\n* Fix regression when using ``fts::index`` and certain inheritance patterns\n  (:eql:gh:`#8468`)\n\n* Fix function cache storage of queries that take enum parameters\n  (:eql:gh:`#8471`)\n\n6.4\n===\n\n* Use ``read committed`` when restoring dumps to improve restore performance\n  (:eql:gh:`#8496`)\n\n* Use ``COPY ... FREEZE`` when restoring dumps to improve restore performance\n  (:eql:gh:`#8494`)\n\n* pg-ext: add table name parsing in ``has_*privilege`` functions and fix ``pg_database`` view\n  (:eql:gh:`#8488`, fixes :eql:gh:`#8457`)\n\n* Fixed computed ptr with group by incorrectly inferring multiplicity\n  (:eql:gh:`#8485`, regression from :eql:gh:`#8464` in 6.3)\n\n6.5\n===\n\nMajor changes\n-------------\n\n* Allow DML in REPEATABLE READ transactions, but only when it is safe\n  to do.  Performing DML in REPEATABLE READ transactions is safe as\n  long as the query does not affect exclusive constraints that are\n  shared between multiple types (through inheritance).\n  (:eql:gh:`#8561`)\n\n\nFeatures\n--------\n\n* Update bundled PostgreSQL to version 17.4\n  (:eql:gh:`#8543`)\n\n* Send a LogMessage to the client about query cache recompilation\n  (:eql:gh:`#8540`, :eql:gh:`#8552`)\n\n* Support ON CONFLICT over SQL adapter\n  (:eql:gh:`#8560`, :eql:gh:`#8587`, :eql:gh:`#8595`)\n\n\nFixes\n-----\n\n* Fix SQL introspection on old PostgreSQL versions\n  (:eql:gh:`#8501`)\n\n* Make HTTP requests delay the auto shutdown\n  (:eql:gh:`#8506`)\n\n* Prevent server auto shutdown during building AI index\n  (:eql:gh:`#8507`)\n\n* pg_ext: fix ``to_regclass()``, ``::regclass`` and ``pg_settings``\n  (:eql:gh:`#8511`)\n\n* pg_ext: information_schema.columns.ordinal_position should be int\n  (:eql:gh:`#8550`)\n\n* Add replication to pg_hba and wal summarization\n  (:eql:gh:`#8556`)\n\n* Fix UPDATE and DELETE on types that use inheritance over SQL adapter\n  (:eql:gh:`#8562`)\n\n* Abort tenant initialization with incompatible backend\n  (:eql:gh:`#8569`)\n\n* Fix computed link properties over SQL adapter\n  (:eql:gh:`#8158`)\n\n* Fix backlinks in computeds over SQL adapter\n  (:eql:gh:`#8570`)\n\n* Fix access policies SELECT issue with diamond-pattern inheritance\n  (:eql:gh:`#8572`)\n\n* Fix dropping types in some situations where the type is used in a function\n  (:eql:gh:`#8574`)\n\n* Fix AI ext OpenAI streaming\n  (:eql:gh:`#8553`)\n\n* Fix removal of path factoring breaking ai embedding views.\n  (:eql:gh:`#8576`)\n\n* Fix a cross-type conflict bug that can occur with rewrites\n  (:eql:gh:`#8584`)\n\n* Optimize compilation in transactions\n  (:eql:gh:`#8082`)\n\n* Fix memory leak in pgrust-pgcon\n  (:eql:gh:`#8590`)\n\n* Fix CREATE ... BRANCH FROM ... when source uses ai extension\n  (:eql:gh:`#8594`)\n\n\n6.6\n===\n\nDue to an error in our release infrastructure, 6.6 was inadvertently\nreleased from our master branch (commit\n7db665af18d22d90d5ec682159353552c67ce1c5).\n\nIt has been removed from our package servers, and was never available\nin our cloud.\n\nIf you have a 6.6 instance, you will need to perform a dump and\nrestore to upgrade to 6.7. We apologize for the inconvenience.\n\n\n6.7\n===\n\nFeatures\n--------\n\n* Add text search to ai extension. ``ext::ai::search()`` may now be\n  called with string search parameters, instead of only arrays of\n  floats.  The string must be a constant or a query parameter. The AI\n  provider will be queried to convert the string to a an embedding\n  vector.\n  (:eql:gh:`#8521`)\n\n* Add Ollama support.\n  (:eql:gh:`#8646`)\n\nFixes\n-----\n\n* Fix wrong values for multi links in triggers and some other places\n  (:eql:gh:`#8615`)\n\n* Fix ``SET GLOBAL`` when right hand side needs to use access policies\n  (:eql:gh:`#8619`)\n\n* Fix proto hang running script after setting ``default_transaction_*``\n  (:eql:gh:`#8623`)\n\n* Avoid hanging onto Schema objects with lru_caches\n  (:eql:gh:`#8628`)\n\n* Enable queries with server param conversions to be cached.\n  (:eql:gh:`#8598`)\n\n* Fix ISE when with binding in dml function contains a set function/operator\n  (:eql:gh:`#8634`)\n\n* Only run the net worker query when DML has happened on the branch\n  (:eql:gh:`#8637`)\n\n* Fix our optimization for skipping optional checks based on card inference\n  (:eql:gh:`#8647`)\n\n* Try to optimize away a per-object check in access policies\n  (:eql:gh:`#8650`)\n\n* Fix access policies warnings when ``future warn_old_scoping`` exists\n  (:eql:gh:`#8643`)\n\n* Fix some object-returning functions when using ``future warn_old_scoping``\n  (:eql:gh:`#8653`)\n\n* server: add cached branch limit to the compiler pool\n  (:eql:gh:`#8621`)\n\n* server: add ``--compiler-worker-max-rss``\n  (:eql:gh:`#8627`)\n\n* server: add more compiler pool metrics\n  (:eql:gh:`#8655`)\n\n* compiler server: add envvar to click options\n  (:eql:gh:`#8666`)\n\n* Start invalidating evicted cache entries on other frontends\n  (:eql:gh:`#8638`)\n\n* Fix error when using computed pointer in ai index.\n  (:eql:gh:`#8663`)\n\n* Fix a bug in COPY with inheritance over SQL adapter\n  (:eql:gh:`#8668`)\n\n* Fix exponential time (in inheritance depth) migrations when altering things\n  (:eql:gh:`#8671`)\n\n\n6.8\n===\n\n* Fix having unnecessary constraint triggers after restore\n  (:eql:gh:`#8685`)\n\n* Fix bogus warnings when using certain functions in access policies\n  (:eql:gh:`#8712`)\n\n* Re-disable doing certain UPDATEs on just INSERTed objects\n  (:eql:gh:`#8713`)\n\n* Fix ``administer schema_repair()``\n  (:eql:gh:`#8730`)\n\n* Fix ``IN`` when left hand side is a SQL NULL\n  (:eql:gh:`#8737`)\n\n* Do better multiplicity inference in nested ``FOR`` expressions\n  (:eql:gh:`#8754`)\n\n* Add json implementation for ``to_bytes`` function.\n  (:eql:gh:`#8723`)\n\n* Speed up use of globals from graphql and http endpoints via caching\n  (:eql:gh:`#8753`)\n\n* Cache param conversion in HTTP and graphql endpoints\n  (:eql:gh:`#8795`)\n\n* Fix issue with some recursive triggers\n  (:eql:gh:`#8775`)\n\n* Fix interaction between inlining, globals, and the HTTP protocols\n  (:eql:gh:`#8781`)\n\n* Fix some situations where ``__tid__`` wasn't appearing first\n  (:eql:gh:`#8783`)\n\n* Fix the command status of BEGIN in the SQL protocol, which should\n  fix JDBC connections\n  (:eql:gh:`#8794`)\n\n* Add a test case for mutation rewrites\n  (:eql:gh:`#8589`)\n\n* Fix ``FOR`` + ``UPDATE`` + rewrite interaction\n  (:eql:gh:`#8796`)\n\n* Improve system API responsiveness\n  (:eql:gh:`#8477`)\n\n* Send ``IdentityCreated`` webhook for OAuth sign up\n  (:eql:gh:`#8799`)\n\n* Use provided callback URL in OAuth code exchange\n  (:eql:gh:`#8801`)\n\n* Fix rewrites making ``UPDATE`` infer as ``ONE``\n  (:eql:gh:`#8800`)\n\n* Increase performance of array/json indexing and json to scalar casts\n  (:eql:gh:`#8804`)\n\n6.9\n===\n\n* Fix array-of-tuple parameter decoding with empty arrays from some bindings\n  (:eql:gh:`#8816`)\n\n* Improve performance of array-of-tuple query parameters\n  (:eql:gh:`#8818`)\n\n* Improve some parser error messages\n  (:eql:gh:`#8819`)\n\n\n6.10\n====\n\n* Fix casting from JSON in indexes/constraints\n  (:eql:gh:`#8920`)\n\n* Fix ISE when computed global is introspected using GraphQL.\n  (:eql:gh:`#8839`)\n\n* Fix policy object duplication in degenerate multiple inheritance cases\n  (:eql:gh:`#8881`)\n\n* Fix bad HTTP cache response when cache expires and upstream is not modified\n  (:eql:gh:`#8891`)\n\n* Fix accessing single link properties through FOR bindings\n  (:eql:gh:`#8911`)\n\n* Fix UPDATING a link table field with reference to non-link field in some cases\n  (:eql:gh:`#8912`)\n\n* Add ``administer fixup_backend_upgrade()`` command for repairing\n  certain support views after a separately managed Postgres backend\n  has been upgraded.\n  (:eql:gh:`#8926`)\n\n* pg-ext: fix bug in query reparse\n  (:eql:gh:`#8938`)\n\n* Fix updating of two single links with linkprops at once\n  (:eql:gh:`#8932`)\n\n\n6.11\n====\n\n* Improve auth email headers and alt content\n  (:eql:gh:`#8919`)\n\n* Fix SQL ``COLLATE`` clauses that require quotation\n  (:eql:gh:`#8960`)\n\n* Improve SQL adapter fallback for ``has_column_privilege``\n  (:eql:gh:`#8962`)\n\n* Fix introspection of ``cfg::AuthMethod.transports``\n  (:eql:gh:`#8971`)\n\n* Fix referencing a computed from a conversion ``USING`` expression\n  (:eql:gh:`#8981`)\n\n* Fix ``FOR`` iteration over a set of DML expressions\n  (:eql:gh:`#8994`)\n\n* Fix casting to multi-ranges accidentally aggregating\n  (:eql:gh:`#8995`)\n\n* Fix ``__default__`` when nested statements have defaults\n  (:eql:gh:`#9006`)\n\n* Bump the compatible extension version for pgvector.\n  (:eql:gh:`#9028`)\n\n"
  },
  {
    "path": "docs/resources/changelog/7_x.rst",
    "content": "====\nv7.0\n====\n\n:edb-alt-title: Gel v7\n\nTo explore the new features, ensure you specify version 7.0 when initializing\nyour project. Pre-release versions are not considered stable and will not be\nautomatically suggested:\n\n.. code-block:: bash\n\n  $ gel project init --server-version 7.0\n\n\nUpgrading\n=========\n\n.. edb:collapsed::\n\n**Local instances**\n\nTo upgrade a local project, first ensure that your CLI is up to date with\n:gelcmd:`cli upgrade`. Then run the following command inside the project\ndirectory.\n\n.. code-block:: bash\n\n  $ gel project upgrade --to-testing\n\nAlternatively, specify an instance name if you aren't using a project:\n\n.. code-block:: bash\n\n  $ gel instance upgrade -I my_instance\n\nThe CLI will check if your schema can migrate cleanly to Gel 7.0. If any\nissues are found, they will be reported.\n\n**Hosted instances**\n\nTo upgrade a remote instance, we recommend the following dump-and-restore\nprocess:\n\n1. Gel v7.0 supports PostgreSQL 14 or above. Verify your PostgreSQL version\n   before upgrading Gel. If you're using Postgres 13 or below, upgrade\n   Postgres first.\n\n2. Spin up an empty 7.0 instance. You can use one of our :ref:`deployment\n   guides <ref_guide_deployment>`.\n\n   For Debian/Ubuntu, when adding the Gel package repository, use this\n   command:\n\n   .. code-block:: bash\n\n       $ echo deb [signed-by=/usr/local/share/keyrings/gel-keyring.gpg] \\\n           https://packages.geldata.com/apt \\\n           $(grep \"VERSION_CODENAME=\" /etc/os-release | cut -d= -f2) main \\\n           | sudo tee /etc/apt/sources.list.d/gel.list\n       $ sudo apt-get update && sudo apt-get install gel-7\n\n   For CentOS/RHEL, use this installation command:\n\n   .. code-block:: bash\n\n       $ sudo yum install gel-7\n\n   In any required ``systemctl`` commands, replace ``edgedb-server-6`` with\n   ``gel-server-7``.\n\n   For Docker setups, use the ``7.0`` tag.\n\n3. Take your application offline, then dump your v5.x database with the CLI:\n\n   .. code-block:: bash\n\n       $ gel dump --dsn <old dsn> --all --format dir my_database.dump/\n\n   This will dump the schema and contents of your current database to a\n   directory on your local disk called ``my_database.dump``. The directory name\n   isn't important.\n\n4. Restore the empty v6.x instance from the dump:\n\n   .. code-block:: bash\n\n       $ gel restore --all my_database.dump/ --dsn <new dsn>\n\n   Once the restore is complete, update your application to connect to the new\n   instance.\n\n   This process will involve some downtime, specifically during steps 2 and 3.\n\n\nBreaking Changes\n================\n\nSQL adapter access policies on by default\n-----------------------------------------\n\nThe default value of ``apply_access_policies_pg`` has been changed\nfrom ``false`` to ``true``.\n\nThis means that by default, EdgeQL access policies will be applied\nwhen running SQL queries over the SQL protocol connection.\n\nTo accomodate third-party tools that cannot be configured to run\nconfiguration commands, we have introduced the\n``apply_access_policies_pg_default`` field to ``Role`` in order to\noverride this:\n\n.. code-block:: edgeql\n\n  CREATE SUPERUSER ROLE pg_connector {\n      # ...\n      SET apply_access_policies_pg_default := false;\n  }\n\nThird-party SQL tools can use the pg_connector role and access\npolicies will be disable.\n\n\nSession configuration for GraphQL and EdgeQL HTTP interfaces\n------------------------------------------------------------\n\nThe :ref:`GraphQL <ref_graphql_protocol>` and :ref:`EdgeQL HTTP <ref_edgeql_protocol>` interfaces now support passing in session configuration variables.\n\nIf you are exposing HTTP/GraphQL endpoints to users and relying on them not being able to perform configuration, you will need to instead have them use roles that have not been given permission to modify sensitive configs.\n\n\nNew features\n============\n\nRole Based Access Control (RBAC)\n--------------------------------\n\nGel 7.0 introduces more fine-grained access controls. It is now\npossible to create *non*-SUPERUSER roles, with limited permissions.\n\nNon-SUPERUSER roles deliberately choose the \"secure by default\" end of\nthe security-vs-convenience tradeoff, and are extremely locked down by\ndefault.\n\n.. code-block:: edgeql\n\n  CREATE ROLE my_role {\n      # ...\n      SET permissions := {\n          sys::perm::data_modifiction,\n          sys::perm::query_stats,\n          cfg::perm::configure_timeouts,\n          cfg::perm::configure_apply_access_policies,\n          ext::auth::perm::auth_read,\n          ext::auth::perm::auth_write,\n      };\n  };\n\n\nWill create a user that can do DDL, look at query stats, configure\ntimeouts and whether to use access policies, and read and write the\nauth extension tables.\n\nSee `our RFC 1029 for more details on the changes\n<https://github.com/geldata/rfcs/blob/master/text/1029-rbac.rst>`_.\n\n\n* Look up role permissions when executing queries.\n  (:eql:gh:`#8760`)\n\n* Make role permission computation look at all ancestors\n  (:eql:gh:`#8784`)\n\n* Add sys::data_modification to grant non-superusers the ability to run DML.\n  (:eql:gh:`#8771`)\n\n* rbac: Implement RBAC permissions for session configs\n  (:eql:gh:`#8806`)\n\n* rbac: Make the HTTP interfaces aware of the current role\n  (:eql:gh:`#8809`)\n\n* rbac: Support required_permissions for function\n  (:eql:gh:`#8812`)\n\n* rbac: Restrict dump, restore, ADMINISTER, DESCRIBE, ANALYZE\n  (:eql:gh:`#8810`)\n\n* rbac: Make a branch_config permission but require superuser for system\n  (:eql:gh:`#8822`)\n\n* rbac: Make system-wide DDL require SUPERUSER\n  (:eql:gh:`#8823`)\n\n* rbac: Add a branches field to Role to restrict a role to certain branches\n  (:eql:gh:`#8830`)\n\n* rbac: Add sys::perm::superuser which is granted to superusers only.\n  (:eql:gh:`#8853`)\n\n* rbac: Add permissions to stdlib objects and functions.\n  (:eql:gh:`#8846`)\n\n* Add permissions to sys types and functions.\n  (:eql:gh:`#8865`)\n\n* Add ``global sys::current_role``\n  (:eql:gh:`#8889`)\n\nAuth extension: One-time code-based flow\n----------------------------------------\n\nGel's ``ext::auth`` module now supports one-time codes (OTCs) as an alternative to email links for all email-based authentication flows. This feature dramatically simplifies the end user experience by allowing users to enter a short numeric code from their email instead of clicking a link. The primary benefit is solving the common cross-device usability problem where users start authentication on one device (like their desktop) but check email on another device (like their phone). With one-time codes, users can easily transcribe the code between devices without worrying about PKCE session mismatches that would cause link-based authentication to fail.\n\nThe one-time code verification method covers all forms of email-based flows in EdgeDB that traditionally use links, including email verification for account registration, password reset flows, magic link authentication, and email verification for WebAuthn registration. When configured to use the ``Code`` verification method, the system decouples the PKCE session from the initial authentication request - the code generation is PKCE-agnostic, while the verification step handles PKCE creation and completion. This allows for both same-device flows (where the existing PKCE session is reused) and cross-device flows (where a new PKCE session is created during verification). The codes are securely hashed, include rate limiting protection, and automatically expire after 10 minutes.\n\nThe feature is fully backwards compatible, with the default ``verification_method`` remaining as ``Link`` to ensure existing projects continue working without changes. To enable one-time codes for magic link authentication, you can configure your provider like this:\n\n.. code-block:: edgeql\n\n   configure current branch insert ext::auth::MagicLinkProviderConfig {\n       verification_method := ext::auth::VerificationMethod.Code,\n   };\n\nThe same ``verification_method`` property is also available for ``EmailPasswordProviderConfig`` and ``WebAuthnProviderConfig``, allowing you to standardize the verification experience across all your email-based authentication methods. See `RFC 1043: Email-Based One-Time Code Authentication <https://github.com/geldata/rfcs/blob/master/text/1043-ext-auth-otc.rst>`_ for more details on the motivations, and the :ref:`Auth docs <ref_guide_auth>`.\n\nOther features\n--------------\n\n* Support required link properties\n  (:eql:gh:`#8735`)\n\n* Allow (most) unparenthesized statements in calls, etc\n  (:eql:gh:`#8763`)\n\n* Implement sys::approximate_count()\n  (:eql:gh:`#8692`)\n\n* Implement ``splat_strategy`` qualifier for pointers\n  (:eql:gh:`#8757`)\n\n* Add http schedule_request with json body.\n  (:eql:gh:`#8724`)\n\n* Support graphql over the binary protocol\n  (:eql:gh:`#8878`)\n\n* One-time code implementation for ``ext::auth``\n  (:eql:gh:`#8905`)\n\n* Add ``math::exp`` and ``math::e``\n  (:eql:gh:`#8982`)\n\n* Support ``FOR OPTIONAL`` as a \"semi-undocumented\" feature\n  (:eql:gh:`#8991`)\n\n* Add support for max batch size to AI embedding models.\n  (:eql:gh:`#9040`)\n\n* Add a ``.?>link`` operator to dereference link while suppressing \"hidden by policy\"\n  (:eql:gh:`#9101`)\n\n* Configurable query cache size\n  (:eql:gh:`#9104`)\n\n* Add Discord OAuth ``prompt`` parameter with default\n  (:eql:gh:`#9111`)\n\n* Support specifying session configs in http and graphql endpoints\n  (:eql:gh:`#9110`)\n\n\nAdditional changes\n==================\n\n\nFixes\n-----\n\n\n* Fix schema type inconsistencies with collection aliases\n  (:eql:gh:`#8672`)\n\n* Fix mangled type names leaking into introspectable names\n  (:eql:gh:`#8772`)\n\n* Drop bionic support\n  (:eql:gh:`#8328`)\n\n* Check that function parameter and return types exist when migrating.\n  (:eql:gh:`#8386`)\n\n* Add hint to partial path errors if anchor is available.\n  (:eql:gh:`#8380`)\n\n* Implement permissions in access policies over SQL adapter\n  (:eql:gh:`#8837`)\n\n* Support creating indexes concurrently\n  (:eql:gh:`#8747`)\n\n* Fix WITH computation duplication in auth module\n  (:eql:gh:`#8851`)\n\n* Access policies for link tables over SQL adapter\n  (:eql:gh:`#8849`)\n\n* Fix params with union types producing ISEs\n  (:eql:gh:`#8863`)\n\n* Support intersection and complex composite types in SDL parameters.\n  (:eql:gh:`#8864`)\n\n* Add ``std::identifier`` annotations to abstract operators\n  (:eql:gh:`#8862`)\n\n* Fix modifying and dropping inherited AI indexes\n  (:eql:gh:`#9041`)\n\n* Fix some issues with overloading AI and FTS indexes\n  (:eql:gh:`#9049`)\n\n* Fix /rag on subtypes\n  (:eql:gh:`#9044`)\n\n* Make SET REQUIRED USING (...) work on link properties\n  (:eql:gh:`#9015`)\n\n* Fix OTC flow for built-in auth UI\n  (:eql:gh:`#8913`)\n\n* Loosen Magic Code register/email data requirements\n  (:eql:gh:`#9000`)\n\n* Fix missing return_data in no email found case\n  (:eql:gh:`#9004`)\n\n* Fix inplace upgrade when ``auth`` is being used\n  (:eql:gh:`#9008`)\n\n* Allow Magic Link sign in to register new user\n  (:eql:gh:`#9007`)\n\n* Return json from magic code authenticate endpoint if no callback_url is given\n  (:eql:gh:`#9012`)\n\n* Return identity_id when registering new Magic Link\n  (:eql:gh:`#9039`)\n\n* Fix in-place upgrade repairs\n  (:eql:gh:`#9073`)\n\n* Fix creating branch when a schema has nested arrays\n  (:eql:gh:`#9074`)\n\n* Check for explicit id write presence in a slightly more resilient way\n  (:eql:gh:`#9090`)\n\n* Fix work_mem setting over SQL adapter\n  (:eql:gh:`#9094`)\n\n* Fix applying type intersection to aliases and iterators\n  (:eql:gh:`#9096`)\n\n* Fix an interaction between aliases and optional globals with a default\n  (:eql:gh:`#9109`)\n\n\nOther\n-----\n\n* Add ``apply_access_policies_pg_default`` flag to ``Role``\n  (:eql:gh:`#8918`)\n\n* Expose ``protected`` on ``schema::Property``\n  (:eql:gh:`#8930`)\n\n* Retry serialization errors that occur internal to the auth extension.\n  (:eql:gh:`#8942`)\n\n* Make indexes get hit more when ordering by something with exclusive constraint\n  (:eql:gh:`#9080`)\n\n* In 7.0, we emit a warning when creating a migration if the schema does\n  not contain ``using simple_scoping;`` or ``using warn_old_scoping;``.\n  We originally planned to remove the old scoping in 8.0.\n\n\n7.1\n===\n\n* Remove the deprecated scoping warning for not using ``simple_scoping``\n  that was present in 7.0.\n"
  },
  {
    "path": "docs/resources/changelog/deprecation.rst",
    "content": ".. _ref_changelog_deprecation:\n\n==================\nDeprecation Policy\n==================\n\n* We continue to support one previous version of |Gel| with critical bug\n  fixes.\n* Client bindings will support the current and the previous major version.\n* CLI supports all versions from version 1.\n"
  },
  {
    "path": "docs/resources/changelog/index.rst",
    "content": "\n=========\nChangelog\n=========\n\nChanges introduced in all of the releases of |Gel| so far:\n\n\n.. toctree::\n    :maxdepth: 1\n\n    1_x\n    2_x\n    3_x\n    4_x\n    5_x\n    6_x\n    7_x\n    deprecation\n"
  },
  {
    "path": "docs/resources/cheatsheets/admin.rst",
    "content": ".. _ref_cheatsheet_admin:\n\nAdministering an instance\n=========================\n\nCreate a schema branch:\n\n.. code-block:: edgeql-repl\n\n    db> create schema branch my_new_feature from main;\n    OK: CREATE BRANCH\n\n\nCreate a data branch:\n\n.. code-block:: edgeql-repl\n\n    db> create data branch my_new_feature from main;\n    OK: CREATE BRANCH\n\n\nCreate an empty branch:\n\n.. code-block:: edgeql-repl\n\n    db> create empty branch my_new_feature;\n    OK: CREATE BRANCH\n\n\n.. note::\n    Prior to |Gel| and |EdgeDB| 5.0 *branches* were called *databases*.\n    A command to create a new empty *database* is ``create database``\n    (still supported for backwards compatibility).\n\n    .. code-block:: edgeql-repl\n\n        db> create database my_new_feature;\n        OK: CREATE DATABASE\n\n\nCreate a role:\n\n.. code-block:: edgeql-repl\n\n    db> create superuser role project;\n    OK: CREATE ROLE\n\n\n\nConfigure passwordless access (such as to a local development database):\n\n.. code-block:: edgeql-repl\n\n    db> configure instance insert Auth {\n    ...     # Human-oriented comment helps figuring out\n    ...     # what authentication methods have been setup\n    ...     # and makes it easier to identify them.\n    ...     comment := 'passwordless access',\n    ...     priority := 1,\n    ...     method := (insert Trust),\n    ... };\n    OK: CONFIGURE INSTANCE\n\n\n\nSet a password for a role:\n\n.. code-block:: edgeql-repl\n\n    db> alter role project\n    ...     set password := 'super-password';\n    OK: ALTER ROLE\n\n\n\nConfigure access that checks password (with a higher priority):\n\n.. code-block:: edgeql-repl\n\n    db> configure instance insert Auth {\n    ...     comment := 'password is required',\n    ...     priority := 0,\n    ...     method := (insert SCRAM),\n    ... };\n    OK: CONFIGURE INSTANCE\n\n\n\nRemove a specific authentication method:\n\n.. code-block:: edgeql-repl\n\n    db> configure instance reset Auth\n    ... filter .comment = 'password is required';\n    OK: CONFIGURE INSTANCE\n\n\n\nRun a script from command line:\n\n.. cli:synopsis::\n\n    cat myscript.edgeql | gel [<connection-option>...]\n"
  },
  {
    "path": "docs/resources/cheatsheets/aliases.rst",
    "content": ".. _ref_cheatsheet_aliases:\n\nDeclaring aliases\n=================\n\n\nDefine an alias that merges some information from links as computed\nproperties, this is a way of flattening a nested structure:\n\n.. code-block:: sdl\n\n    alias ReviewAlias := Review {\n        # It will already have all the Review\n        # properties and links.\n        author_name := .author.name,\n        movie_title := .movie.title,\n    }\n\n\n----------\n\n\nDefine an alias for traversing a :ref:`backlink\n<ref_datamodel_links>`, this is especially useful for GraphQL access:\n\n.. code-block:: sdl\n\n    alias MovieAlias := Movie {\n        # A computed link for accessing all the\n        # reviews for this movie.\n        reviews := .<movie[is Review]\n    }\n\n.. note::\n\n    Aliases allow to use the full power of EdgeQL (expressions,\n    aggregate functions, :ref:`backlink <ref_datamodel_links>`\n    navigation) from :ref:`GraphQL <ref_graphql_index>`.\n\nThe aliases defined above allow you to query ``MovieAlias`` with\n:ref:`GraphQL <ref_cheatsheet_graphql>`.\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Schema > Aliases <ref_datamodel_aliases>`\n  * - :ref:`SDL > Aliases <ref_eql_sdl_aliases>`\n  * - :ref:`DDL > Aliases <ref_eql_ddl_aliases>`\n"
  },
  {
    "path": "docs/resources/cheatsheets/annotations.rst",
    "content": ".. _ref_cheatsheet_annotations:\n\nDeclaring annotations\n=====================\n\nUse annotations to add descriptions to types and links:\n\n.. code-block:: sdl\n\n    type Label {\n        annotation description :=\n            'Special label to stick on reviews';\n        required comments: str;\n        review: Review {\n            annotation description :=\n                'This review needs some attention';\n        };\n    }\n\n    type Review {\n        content: str;\n    }\n\n\n----------\n\n\nRetrieving the annotations can be done via an introspection query:\n\n.. code-block:: edgeql-repl\n\n    db> select introspect(Label) {\n    ...     name,\n    ...     annotations: {name, @value},\n    ...     links: {name, annotations: {name, @value}}\n    ... };\n    {\n        schema::ObjectType {\n            name: 'default::Label',\n            annotations: {\n                schema::Annotation {\n                    name: 'std::description',\n                    @value: 'Special label to stick on reviews'\n                },\n            },\n            links: {\n                schema::Link {\n                    name: '__type__',\n                    annotations: {}\n                },\n                schema::Link {\n                    name: 'review',\n                    annotations: {\n                        schema::Annotation {\n                            name: 'std::description',\n                            @value: 'This review needs some attention',\n                        },\n                    },\n                },\n            },\n        },\n    }\n\n----------\n\n\nAlternatively, the annotations can be viewed by the following REPL\ncommand:\n\n.. code-block:: edgeql-repl\n\n    db> \\d -v Label\n    type default::Label {\n        annotation std::description := 'Special label to stick on reviews';\n        required single link __type__: schema::ObjectType {\n            readonly := true;\n        };\n        optional single link review: default::Review {\n            annotation std::description := 'This review needs some attention';\n        };\n        required single property comments: std::str;\n        required single property id: std::uuid {\n            readonly := true;\n            constraint std::exclusive;\n        };\n    };\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Schema > Annotations <ref_datamodel_annotations>`\n  * - :ref:`SDL > Annotations <ref_eql_sdl_annotations>`\n  * - :ref:`DDL > Annotations <ref_eql_ddl_annotations>`\n  * - :ref:`Introspection > Object types\n      <ref_datamodel_introspection_object_types>`\n"
  },
  {
    "path": "docs/resources/cheatsheets/boolean.rst",
    "content": ".. _ref_cheatsheet_boolean:\n\nWorking with booleans\n=====================\n\nBoolean expressions can be tricky sometimes, so here are a handful of\ntips and gotchas.\n\n\n----------\n\n\nThere's a fundamental difference in how ``{}`` is treated by\n:eql:op:`and` and :eql:op:`or` vs :eql:func:`all` and :eql:func:`any`.\nThe operators :eql:op:`and` and :eql:op:`or` require both operands\nto produce a result, which means that an ``{}`` as one of the\ninputs necessarily produces an ``{}`` as the output:\n\n.. code-block:: edgeql-repl\n\n    db> select false and <bool>{};\n    {}\n    db> select true or <bool>{};\n    {}\n\nThe functions :eql:func:`all` and :eql:func:`any`, however, produce a\nresult for all possible input sets, regardless of the number of\nelements:\n\n.. code-block:: edgeql-repl\n\n    db> select all({false, {}});\n    {false}\n    db> select any({true, {}});\n    {true}\n\nNote that expressions like ``{false, {}}`` are equivalent to\n``{false}`` and so the above are just generalizations of boolean\noperators :eql:op:`and` and :eql:op:`or` to a set of 1 element. So the\nresult for 1 element is fairly intuitive. However, the results\nproduced by these functions for ``{}`` may be surprising (even though\nthey are mathematically consistent):\n\n.. code-block:: edgeql-repl\n\n    db> select all(<bool>{});\n    {true}\n    db> select any(<bool>{});\n    {false}\n\n\n----------\n\n\nThere's no direct analogue to the boolean operator \"short-circuiting\"\nthat's implemented in many other languages because in EdgeQL the order\nof evaluation of subexpressions is generally not defined. However,\nthere are expressions that achieve the same end goal for which\n\"short-circuiting\" is used.\n\n\n----------\n\n\nThe most basic filtering doesn't even require any \"short-circuiting\"\nguards because these are already implied by EdgeQL. For example, *\"get\nall accounts that completed 5 steps of the process\"*:\n\n.. code-block:: edgeql\n\n    select Account filter .steps = 5;\n\n\n----------\n\n\nWhen there's a need to express that a field is initialized, but not\nequal to some particular value \"short-circuiting\" is often used to\ndiscard non-initialized values (e.g. ``acc.steps is not None and\nacc.steps != 5``). This is another case where EdgeQL doesn't require\nany additional guards. For example *\"get all initialized accounts that\nhave not completed 5 steps of the process\"*:\n\n.. code-block:: edgeql\n\n    select Account filter .steps != 5;\n\n\n----------\n\n\nIf the task boils down to annotating every element as opposed to\nselecting specific ones, the use of :eql:op:`?= <coaleq>` instead\nof the plain :eql:op:`= <eq>` helps to deal with optional properties.\nFor example, *\"get all accounts and annotate them with their\ncompleteness status\"*:\n\n.. code-block:: edgeql\n\n    select Account {\n        completed := .steps ?= 5\n    };\n\n\n----------\n\n\nSometimes the condition that needs to be evaluated is not a simple\nequality comparison. The :eql:op:`?? <coalesce>` can help out in these\ncases. For example, *\"get all accounts and annotate them on whether or\nnot they are half-way completed\"*:\n\n.. code-block:: edgeql\n\n    select Account {\n        completed := (.steps > 2) ?? false\n    };\n\n\n----------\n\n\nThe above trick can also be useful for filtering based on some boolean\ncondition that's not just a plain equality. For example, *\"get only the\naccounts that are less than half-way completed\"*:\n\n.. code-block:: edgeql\n\n    select Account {\n        too_few_steps := (.steps <= 2) ?? true\n    } filter .too_few_steps;\n\n\n----------\n\n\nThe above will end up including the computed flag ``too_few_steps``\nin the output, but this is sometimes undesirable. In order to avoid\nincluding it, the query can be refactored like this:\n\n.. code-block:: edgeql\n\n    select Account {\n        name,\n        email,\n        # whatever other relevant data is needed\n    } filter (.steps <= 2) ?? true;\n\n\n----------\n\n\nWhen using :eql:op:`?=<coaleq>`, :eql:op:`?=<coalneq>`, or\n:eql:op:`?? <coalesce>` it is important to keep in mind how they\ninteract with :ref:`path expressions <ref_eql_paths>` that\ncan sometimes be ``{}``. Basically, these operators don't actually\naffect the path expression, they only act on the *results* of the\npath expression. Consider the following two queries:\n\n.. code-block:: edgeql\n\n    select Account {\n        too_few_steps := (.steps <= 2) ?? true\n    }.too_few_steps;\n\n    select (Account.steps <= 2) ?? true;\n\nThe first query is going to output ``true`` or ``false`` for every\naccount, based on the specified criteria. It's important to note that\nthe number of the results is going to be exactly the same as the\nnumber of the accounts in the system. The second query may look like a\nmore compact version of the first query, but it behaves completely\ndifferently. If all of the account are \"uninitialized\" (``steps :=\n{}``) or there are no accounts at all, it will produce a single result\n``true``. That's because the expression ``Account.steps <= 2``\nproduces an empty set in this case and so the :eql:op:`?? <coalesce>`\nreturns the second operand. On the other hand, if there are any\naccounts with some concrete number of ``steps``, then the expression\n``Account.steps <= 2`` will produce a result for *those accounts\nonly*. The :eql:op:`?? <coalesce>` won't change that result because the\nresult is already non-empty and so no coalescing will take place.\n\nComputeds in shapes get evaluated *for each object*, whereas path\nexpressions only produce as many values as are *reachable* by the\npath. So when all objects must be considered, computed links and\nproperties in shapes are a good way to handle complex expressions or\nfilters. When only objects with specific properties are relevant, path\nexpressions are a good compact way of handling this.\n\n\n----------\n\n\nThere's also another way to evaluate something on a per-object basis\nand that's by using a :eql:stmt:`for` query. For example, let's\nrewrite the query that outputs ``true`` or ``false`` for every\naccount, based on the number of completed steps:\n\n.. code-block:: edgeql\n\n    for A in Account\n    union (A.steps <= 2) ?? true;\n\n\n----------\n\n\nExpressions specified in shapes, :eql:stmt:`for`, or ``filter``\nclauses are all evaluated on a per-item basis. The gotchas in these\ncases can arise from using longer path expressions combined with\n:eql:op:`?? <coalesce>`, :eql:op:`?= <coaleq>`, or :eql:op:`?!=\n<coalneq>`. For example, let's say that in addition to accounts\nand steps we also have different \"projects\" with a multi-link of\n``accounts`` marking progress in them. So keeping that in mind,\nlet's try writing a query to *\"get all projects that have linked\naccounts which made little progress (fewer than 3 ``steps``)\"*:\n\n.. code-block:: edgeql\n\n    select Project\n    filter .accounts.steps < 3;\n\nWell, that's not right. Projects that have accounts without any\n``steps`` of progress are not reported by the above query. So maybe\nadding a :eql:op:`?? <coalesce>` will help?\n\n.. code-block:: edgeql\n\n    select Project\n    filter (.accounts.steps < 3) ?? true;\n\nThis is better as the results now include projects where none of the\naccounts made any progress. However, any project that has a mix of\naccounts that made more than 2 steps of progress and accounts that\nhaven't even started is still missing from the results. So we can\neither use the trick we used before with shapes or we can add another\n:eql:stmt:`for` subquery:\n\n.. code-block:: edgeql\n\n    select Project\n    filter (\n        for A in .accounts\n        union (A.steps < 3) ?? true\n    );\n\n\n----------\n\n\nNote that the :ref:`filter <ref_eql_statements_select_filter>` clause\nbehaves as an implicit :eql:func:`any`. This means that the following\nare semantically equivalent:\n\n.. code-block:: edgeql\n\n    select User\n    filter .friends.name = 'Alice';\n\n    select User\n    filter any(.friends.name = 'Alice');\n"
  },
  {
    "path": "docs/resources/cheatsheets/cli.rst",
    "content": ".. _ref_cheatsheet_cli:\n\nUsing the CLI\n=============\n\nTo initialize a new project:\n\n.. code-block:: bash\n\n    $ gel project init\n\nIf an :ref:`ref_reference_gel_toml` file exists in the current directory, it\nwill initialize a new project according to the settings defined in it.\n\nOtherwise, a new project will be initialized and an |gel.toml| file and\n``dbschema`` directory will be generated. For details on using projects, see\nthe :ref:`dedicated guide <ref_guide_using_projects>`.\n\nOnce initialized, you can run the CLI commands below without additional\nconnection options. If you don't set up a project, you'll need to use\n:ref:`flags <ref_cli_gel_connopts>` to specify the target instance for each\ncommand.\n\n----------\n\n\nExplicitly create a new |Gel| instance ``my_instance``:\n\n.. code-block:: bash\n\n    $ gel instance create my_instance\n\n\n----------\n\n\nCreate a branch:\n\n.. code-block:: bash\n\n    $ gel branch create feature\n    OK: CREATE\n\n\n----------\n\n\nConfigure passwordless access (such as to a local development database):\n\n.. code-block:: bash\n\n    $ gel configure insert Auth \\\n    > --comment 'passwordless access' \\\n    > --priority 1 \\\n    > --method Trust\n    OK: CONFIGURE INSTANCE\n\n\n----------\n\n\nConfigure access that checks password (with a higher priority):\n\n.. code-block:: bash\n\n    $ gel configure insert Auth \\\n    > --comment 'password is required' \\\n    > --priority 0 \\\n    > --method SCRAM\n    OK: CONFIGURE INSTANCE\n\n\n----------\n\n\nConnect to the default project branch:\n\n.. code-block:: bash\n\n    $ gel\n                        ▄██▄\n      ▄▄▄▄▄      ▄▄▄    ████\n    ▄███████▄ ▄███████▄ ████\n    ▀███████▀ ▀███▀▀▀▀▀ ████\n      ▀▀▀▀▀      ▀▀▀     ▀▀\n     ▀▄▄▄▄▄▀\n       ▀▀▀\n    Gel 6.0-rc.1+673117d (repl 6.2.0-dev)\n    Type \\help for help, \\quit to quit.\n\n----------\n\n\nConnect to some specific branch:\n\n.. code-block:: bash\n\n    $ gel -b feature\n    Gel 6.0-rc.1+673117d (repl 6.2.0-dev)\n    Type \\help for help, \\quit to quit.\n    special_db>\n"
  },
  {
    "path": "docs/resources/cheatsheets/delete.rst",
    "content": ".. _ref_cheatsheet_delete:\n\nDeleting data\n=============\n\n.. note::\n\n    The types used in these queries are defined :ref:`here\n    <ref_cheatsheet_object_types>`.\n\n\n----------\n\n\nDelete all reviews from a specific user:\n\n.. code-block:: edgeql\n\n    delete Review\n    filter .author.name = 'trouble2020'\n\n\n----------\n\n\nAlternative way to delete all reviews from a specific user:\n\n.. code-block:: edgeql\n\n    delete (\n        select User\n        filter .name = 'troll2020'\n    ).<author[is Review]\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`EdgeQL > Delete <ref_eql_delete>`\n  * - :ref:`Reference > Commands > Delete <ref_eql_statements_delete>`\n"
  },
  {
    "path": "docs/resources/cheatsheets/functions.rst",
    "content": ".. _ref_cheatsheet_functions:\n\nDeclaring functions\n===================\n\nDefine a function for counting reviews given a user name:\n\n.. code-block:: edgeql\n\n    create function review_count(name: str) -> int64\n    using (\n        with module default\n        select count(\n            (\n                select Review\n                filter .author.name = name\n            )\n        )\n    )\n\n\n----------\n\n\nDrop a user-defined function:\n\n.. code-block:: edgeql\n\n    drop function review_count(name: str);\n\n\n----------\n\n\nDefine and use polymorphic function:\n\n.. code-block:: edgeql-repl\n\n    db> create function make_name(name: str) -> str\n    ... using ('my_name_' ++ name);\n    CREATE FUNCTION\n    db> create function make_name(name: int64) -> str\n    ... using ('my_name_' ++ <str>name);\n    CREATE FUNCTION\n    q> select make_name('Alice');\n    {'my_name_Alice'}\n    q> select make_name(42);\n    {'my_name_42'}\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Schema > Functions <ref_datamodel_functions>`\n  * - :ref:`SDL > Functions <ref_eql_sdl_functions>`\n  * - :ref:`DDL > Functions <ref_eql_ddl_functions>`\n  * - :ref:`Reference > Function calls <ref_reference_function_call>`\n  * - :ref:`Introspection > Functions <ref_datamodel_introspection_functions>`\n\n"
  },
  {
    "path": "docs/resources/cheatsheets/index.rst",
    "content": ".. _ref_cheatsheets:\n\n===========\nCheatsheets\n===========\n\n:edb-alt-title: Cheatsheets: Gel by example\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n\n    select\n    insert\n    update\n    delete\n    boolean\n    objects\n    functions\n    aliases\n    annotations\n    cli\n    repl\n    admin\n\n\nJust getting started? Keep an eye on this collection of cheatsheets with\nhandy examples for what you'll need to get started with |Gel|.\n\nEdgeQL\n======\n\n* :ref:`select <ref_cheatsheet_select>` -- Retrieve or compute a set of values.\n* :ref:`insert <ref_cheatsheet_insert>` -- Create new database objects.\n* :ref:`update <ref_cheatsheet_update>` -- Update database objects.\n* :ref:`delete <ref_cheatsheet_delete>` -- Remove objects from the database.\n* :ref:`GraphQL <ref_cheatsheet_graphql>` -- GraphQL queries supported natively\n  out of the box.\n\nSchema\n======\n\n* :ref:`Booleans <ref_cheatsheet_boolean>` -- Boolean expressions can be tricky\n  sometimes, so here are a handful of tips and gotchas.\n* :ref:`Object Types <ref_cheatsheet_object_types>` -- Make your own object\n  and abstract types on top of existing system types.\n* :ref:`User Defined Functions <ref_cheatsheet_functions>` -- Write and\n  overload your own strongly typed functions.\n* :ref:`Expression Aliases <ref_cheatsheet_aliases>` -- Use aliases to create\n  new types and modify existing ones on the fly.\n* :ref:`Schema Annotations <ref_cheatsheet_annotations>` -- Add human readable\n  descriptions to items in your schema.\n* :ref:`Link Properties <ref_datamodel_linkprops>` -- Links can contain properties\n  used to store metadata about the link.\n\nCLI/Admin\n=========\n\n* :ref:`CLI Usage <ref_cheatsheet_cli>` -- Getting your database started.\n* :ref:`Interactive Shell <ref_cheatsheet_repl>` -- Shortcuts for\n  frequently used commands in the Gel Interactive Shell.\n* :ref:`Administration <ref_cheatsheet_admin>` -- Branch and role creation,\n  passwords, port configuration, etc.\n"
  },
  {
    "path": "docs/resources/cheatsheets/insert.rst",
    "content": ".. _ref_cheatsheet_insert:\n\nInserting data\n==============\n\n.. note::\n\n    The types used in these queries are defined :ref:`here\n    <ref_cheatsheet_object_types>`.\n\n\n----------\n\n\nInsert basic movie stub:\n\n.. code-block:: edgeql\n\n    insert Movie {\n        title := 'Dune',\n        year := 2020,\n        image := 'dune2020.jpg',\n        directors := (\n            select Person\n            filter\n                .full_name = 'Denis Villeneuve'\n        )\n    }\n\n\n----------\n\n\nAlternatively, insert a movie using JSON input value:\n\n.. code-block:: edgeql\n\n    with\n        # Cast the JSON $input into a tuple, which we will\n        # use to populate the Person record.\n        data := <tuple<\n            title: str,\n            year: int64,\n            image: str,\n            directors: array<str>,\n            actors: array<str>\n        >> <json>$input\n    insert Movie {\n        title := data.title,\n        year := data.year,\n        image := data.image,\n        directors := (\n            select Person\n            filter\n                .full_name in array_unpack(data.directors)\n        ),\n        actors := (\n            select Person\n            filter\n                .full_name in array_unpack(data.actors)\n        )\n    }\n\n\n----------\n\n\nInsert several nested objects at once:\n\n.. code-block:: edgeql\n\n    # Create a new review and a new user in one step.\n    insert Review {\n        body := 'Dune is cool',\n        rating := 5,\n        # The movie record already exists, so select it.\n        movie := (\n            select Movie\n            filter\n                .title = 'Dune'\n                and\n                .year = 2020\n            # the limit is needed to satisfy the single\n            # link requirement validation\n            limit 1\n        ),\n        # This is a new user, so insert one.\n        author := (\n            insert User {\n                name := 'dune_fan_2020',\n                image := 'default_avatar.jpg',\n            }\n        )\n    }\n\n\n----------\n\n\nSometimes it's necessary to check whether some object exists and\ncreate it if it doesn't. If this type of object has an exclusive\nproperty, the ``unless conflict`` clause can make the ``insert``\ncommand indempotent. So running such a command would guarantee that a\ncopy of the object exists without the need for more complex logic:\n\n.. code-block:: edgeql\n\n    # Try to create a new User\n    insert User {\n        name := \"Alice\",\n        image := \"default_avatar.jpg\",\n    }\n    # and do nothing if a User with this name already exists\n    unless conflict\n\nIf more than one property is exclusive, it is possible to specify\nwhich one of them is considered when a conflict is detected:\n\n.. code-block:: edgeql\n\n    # Try to create a new User\n    insert User {\n        name := \"Alice\",\n        image := \"default_avatar.jpg\",\n    }\n    # and do nothing if a User with this name already exists\n    unless conflict on .name\n\n\n----------\n\n\n\"Upserts\" can be performed by using the ``unless conflict`` clause and\nspecifying what needs to be updated:\n\n.. code-block:: edgeql\n\n    select (\n        # Try to create a new User,\n        insert User {\n            name := \"Alice\",\n            image := \"my_face.jpg\",\n        }\n\n        # but if a User with this name already exists,\n        unless conflict on .name\n        else (\n            # update that User's record instead.\n            update User\n            set {\n                image := \"my_face.jpg\"\n            }\n        )\n    ) {\n        name,\n        image\n    }\n\n\n----------\n\n\nRather than acting as an \"upsert\", the ``unless conflict`` clause can\nbe used to insert or select an existing record, which is handy for\ninserting nested structures:\n\n.. code-block:: edgeql\n\n    # Create a new review and a new user in one step.\n    insert Review {\n        body := 'Loved it!!!',\n        rating := 5,\n        # The movie record already exists, so select it.\n        movie := (\n            select Movie\n            filter\n                .title = 'Dune'\n                and\n                .year = 2020\n            # the limit is needed to satisfy the single\n            # link requirement validation\n            limit 1\n        ),\n\n        # This might be a new user or an existing user. Some\n        # other part of the app handles authentication, this\n        # endpoint is used as a generic way to post a review.\n        author := (\n            # Try to create a new User,\n            insert User {\n                name := \"dune_fan_2020\",\n                image := \"default_avatar.jpg\",\n            }\n\n            # but if a User with this name already exists,\n            unless conflict on .name\n            # just pick that existing User as the author.\n            else User\n        )\n    }\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`EdgeQL > Insert <ref_eql_insert>`\n  * - :ref:`Reference > Commands > Insert <ref_eql_statements_insert>`\n"
  },
  {
    "path": "docs/resources/cheatsheets/objects.rst",
    "content": ".. _ref_cheatsheet_object_types:\n\nObject types\n============\n\n\nDefine an abstract type:\n\n.. code-block:: sdl\n\n    abstract type HasImage {\n        # just a URL to the image\n        required image: str;\n        index on (.image);\n    }\n\n\n----------\n\n\nDefine a type extending from the abstract:\n\n.. code-block:: sdl\n\n    type User extending HasImage {\n        required name: str {\n            # Ensure unique name for each User.\n            constraint exclusive;\n        }\n    }\n\n\n----------\n\n\nDefine a type with constraints and defaults for properties:\n\n.. code-block:: sdl\n\n    type Review {\n        required body: str;\n        required rating: int64 {\n            constraint min_value(0);\n            constraint max_value(5);\n        }\n        required flag: bool {\n            default := False;\n        }\n\n        required author: User;\n        required movie: Movie;\n\n        required creation_time: datetime {\n            default := datetime_current();\n        }\n    }\n\n\n----------\n\n\nDefine a type with a property that is computed from the combination of\nthe other properties:\n\n.. code-block:: sdl\n\n    type Person extending HasImage {\n        required first_name: str {\n            default := '';\n        }\n        required middle_name: str {\n            default := '';\n        }\n        required last_name: str;\n        full_name :=\n            (\n                (\n                    (.first_name ++ ' ')\n                    if .first_name != '' else\n                    ''\n                ) ++\n                (\n                    (.middle_name ++ ' ')\n                    if .middle_name != '' else\n                    ''\n                ) ++\n                .last_name\n            );\n        bio: str;\n    }\n\n\n\n----------\n\n\nDefine an abstract links:\n\n.. code-block:: sdl\n\n    abstract link crew {\n        # Provide a way to specify some \"natural\"\n        # ordering, as relevant to the movie. This\n        # may be order of importance, appearance, etc.\n        list_order: int64;\n    }\n\n    abstract link directors {\n        extending crew;\n    };\n\n    abstract link actors {\n        extending crew;\n    };\n\n\n----------\n\n\nDefine a type using abstract links and a computed property that\naggregates values from another linked type:\n\n.. code-block:: sdl\n\n    type Movie extending HasImage {\n        required title: str;\n        required year: int64;\n\n        # Add an index for accessing movies by title and year,\n        # separately and in combination.\n        index on (.title);\n        index on (.year);\n        index on ((.title, .year));\n\n        description: str;\n\n        multi directors: Person {\n            extending crew;\n        };\n        multi actors: Person {\n            extending crew\n        };\n\n        avg_rating := math::mean(.<movie[is Review].rating);\n    }\n\n\n\n----------\n\n\nDefine an :eql:type:`auto-incrementing <sequence>` scalar type and an\nobject type using it as a property:\n\n.. code-block:: sdl\n\n    scalar type TicketNo extending sequence;\n\n    type Ticket {\n        number: TicketNo {\n            constraint exclusive;\n        }\n    }\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`Schema > Object types <ref_datamodel_object_types>`\n  * - :ref:`SDL > Object types <ref_eql_sdl_object_types>`\n  * - :ref:`DDL > Object types <ref_eql_ddl_object_types>`\n  * - :ref:`Introspection > Object types\n      <ref_datamodel_introspection_object_types>`\n"
  },
  {
    "path": "docs/resources/cheatsheets/repl.rst",
    "content": ".. _ref_cheatsheet_repl:\n\nUsing the REPL\n==============\n\nExecute a query. To execute a query in the REPL, terminate the statement with\na semicolon and press \"ENTER\".\n\n.. code-block:: edgeql-repl\n\n    db> select 5;\n    {5}\n\nAlternatively, you can run the query without a semicolon by hitting Alt-Enter\non Windows/Linux, or Esc+Return on macOS.\n\n.. code-block:: edgeql-repl\n\n    db> select 5\n    {5}\n\n\nType ``Alt-Enter`` to run the query without having the cursor to the end of\nthe query.\n\n.. note::\n\n    This doesn't work by default on macOS, however it's possible to enable it\n    with a quick fix.\n\n    * in Terminal.app: Settings → Profiles → Keyboard → Check\n      \"Use Option as Meta key\"\n    * in iTerm: Settings → Profiles → Keys → Let Option key: ESC+\n\n    Alternatively you can use the ``esc+return`` shortcut.\n\n\n----------\n\nUse query parameters. If your query contains a parameter, you will be prompted\nfor a value.\n\n.. code-block:: edgeql-repl\n\n    db> select 5 + <int64>$num;\n    Parameter <int64>$num: 6\n    {11}\n\n----------\n\nCommands\n^^^^^^^^\n\n\n.. list-table::\n\n    * - Options\n      - ``-v`` = verbose\n\n        ``-s`` = show system objects\n\n        ``-I`` = case-sensitive match\n\n    * - ``\\d [-v] NAME``\n      - Describe a schema object.\n\n    * - ``\\ds, \\describe schema``\n      - Describe the entire schema.\n\n    * - ``\\list branches``\n\n        ``alias: \\l``\n      - List |branches|.\n    * - ``\\list scalars [-sI] [pattern]``\n\n        ``alias: \\ls``\n      - List scalar types.\n    * - ``\\list types [-sI] [pattern]``\n\n        ``alias: \\lt``\n      - List object types.\n    * - ``\\list roles [-I]``\n\n        ``alias: \\lr``\n      - List roles.\n    * - ``\\list modules [-I]``\n\n        ``alias: \\lm``\n      - List modules.\n    * - ``\\list aliases [-Isv] [pattern]``\n\n        ``alias: \\la``\n      - List expression aliases.\n    * - ``\\list casts [-I] [pattern]``\n\n        ``alias: \\lc``\n      - List casts.\n    * - ``\\list indexes [-Isv] [pattern]``\n\n        ``alias: \\li``\n      - List indexes.\n\n    * - ``\\dump <filename>``\n      - Dump the current |branch| to file.\n\n    * - ``\\restore <filename>``\n      - Restore the |branch| from a dump file.\n\n    * - ``\\s, \\history``\n      - Show query history\n\n    * - ``\\e, \\edit [N]``\n      - Spawn $EDITOR to edit history entry N.\n\n        Then use the output as the input.\n\n    * - ``\\set [<option> [<value>]]``\n      - View/change a setting.\n\n        Type ``\\set`` to see all available settings.\n\n    * - ``\\c, \\connect [<dbname>]``\n      - Connect to a particular |branch|.\n\n\nSample usage\n^^^^^^^^^^^^\n\nList |branches|:\n\n.. code-block:: edgeql-repl\n\n    db> \\ls\n    List of branches:\n      db\n      tutorial\n\n\n\n----------\n\n\nConnect to a |branch|:\n\n.. code-block:: edgeql-repl\n\n    db> \\c my_new_project\n    my_new_project>\n\n\n----------\n\n\nDescribe an object type:\n\n.. code-block:: edgeql-repl\n\n    db> \\d object Object\n    abstract type std::Object extending std::BaseObject {\n        required single link __type__ -> schema::Type {\n            readonly := true;\n        };\n        required single property id -> std::uuid {\n            readonly := true;\n        };\n    };\n\n\n----------\n\n\nDescribe a scalar type:\n\n.. code-block:: edgeql-repl\n\n    db> \\d object decimal\n    scalar type std::decimal extending std::anynumeric;\n\n\n----------\n\n\nDescribe a function:\n\n.. code-block:: edgeql-repl\n\n    db> \\d object sum\n    function std::sum(s: set of std::bigint) ->  std::bigint {\n        volatility := 'Immutable';\n        annotation std::description := 'Return the sum of the set of numbers.';\n        using sql function 'sum'\n    ;};\n    function std::sum(s: set of std::int32) ->  std::int64 {\n        volatility := 'Immutable';\n        annotation std::description := 'Return the sum of the set of numbers.';\n        using sql function 'sum'\n    ;};\n    function std::sum(s: set of std::decimal) ->  std::decimal {\n        volatility := 'Immutable';\n        annotation std::description := 'Return the sum of the set of numbers.';\n        using sql function 'sum'\n    ;};\n    function std::sum(s: set of std::float32) ->  std::float32 {\n        volatility := 'Immutable';\n        annotation std::description := 'Return the sum of the set of numbers.';\n        using sql function 'sum'\n    ;};\n    function std::sum(s: set of std::int64) ->  std::int64 {\n        volatility := 'Immutable';\n        annotation std::description := 'Return the sum of the set of numbers.';\n        using sql function 'sum'\n    ;};\n    function std::sum(s: set of std::float64) ->  std::float64 {\n        volatility := 'Immutable';\n        annotation std::description := 'Return the sum of the set of numbers.';\n        using sql function 'sum'\n    ;};\n"
  },
  {
    "path": "docs/resources/cheatsheets/select.rst",
    "content": ".. _ref_cheatsheet_select:\n\nSelecting data\n==============\n\n.. note::\n\n    The types used in these queries are defined :ref:`here\n    <ref_cheatsheet_object_types>`.\n\n\n----------\n\n\nSelect a Movie with associated actors and reviews with their authors:\n\n.. code-block:: edgeql\n\n    select Movie {\n        id,\n        title,\n        year,\n        description,\n\n        actors: {\n            id,\n            full_name,\n        },\n\n        reviews := .<movie[is Review] {\n            id,\n            body,\n            rating,\n            author: {\n                id,\n                name,\n            }\n        },\n    }\n    filter .id = <uuid>'09c34154-4148-11ea-9c68-5375ca908326'\n\n\n----------\n\n\nSelect movies with Keanu Reeves:\n\n.. code-block:: edgeql\n\n    select Movie {\n        id,\n        title,\n        year,\n        description,\n    }\n    filter .actors.full_name = 'Keanu Reeves'\n\n\n----------\n\n\nSelect all actors that share the last name with other actors and\ninclude the same-last-name actor list as well:\n\n.. code-block:: edgeql\n\n    select Person {\n        id,\n        full_name,\n        same_last_name := (\n            with\n                P := detached Person\n            select P {\n                id,\n                full_name,\n            }\n            filter\n                # same last name\n                P.last_name = Person.last_name\n                and\n                # not the same person\n                P != Person\n        ),\n    }\n    filter exists .same_last_name\n\n\n----------\n\n\nThe same query can be refactored moving the ``with`` block to the\ntop-level:\n\n.. code-block:: edgeql\n\n    with\n        # don't need detached at top-level\n        P := Person\n    select Person {\n        id,\n        full_name,\n        same_last_name := (\n            select P {\n                id,\n                full_name,\n            }\n            filter\n                # same last name\n                P.last_name = Person.last_name\n                and\n                # not the same person\n                P != Person\n        ),\n    }\n    filter exists .same_last_name\n\n\n----------\n\n\nSelect user names and the number of reviews they have:\n\n.. code-block:: edgeql\n\n    select (\n        User.name,\n        count(User.<author[is Review])\n    )\n\n\n----------\n\n\nFor every user and movie combination, select whether the user has\nreviewed the movie (beware, in practice this maybe a very large\nresult):\n\n.. code-block:: edgeql\n\n    select (\n        User.name,\n        Movie.title,\n        Movie in User.<author[is Review].movie\n    )\n\n\n----------\n\n\nPerform a set intersection of all actors with all directors:\n\n.. code-block:: edgeql\n\n    with\n        # get the set of actors and set of directors\n        Actor := Movie.actors,\n        Director := Movie.director,\n    # set intersection is done via the filter clause\n    select Actor filter Actor in Director;\n\n\n----------\n\n\nTo order a set of scalars first assign the set to a variable and use the\nvariable in the order by clause.\n\n.. code-block:: edgeql\n\n    select numbers := {3, 1, 2} order by numbers;\n\n    # alternatively\n    with numbers := {3, 1, 2}\n    select numbers order by numbers;\n\n\n----------\n\n\n.. _ref_datamodel_objects_free:\n\nSelecting free objects.\n\nIt is also possible to package data into a *free object*.\n*Free objects* are meant to be transient and used either to more\nefficiently store some intermediate results in a query or for\nre-shaping the output. The advantage of using *free objects* over\n:eql:type:`tuples <tuple>` is that it is easier to package data that\npotentially contains empty sets as links or properties of the\n*free object*. The underlying type of a *free object* is\n``std::FreeObject``.\n\nConsider the following query:\n\n.. code-block:: edgeql\n\n    with U := (select User filter .name like '%user%')\n    select {\n        matches := U {name},\n        total := count(U),\n        total_users := count(User),\n    };\n\nThe ``matches`` are potentially ``{}``, yet the query will always\nreturn a single *free object* with ``results``, ``total``, and\n``total_users``. To achieve the same using a :eql:type:`named tuple\n<tuple>`, the query would have to be modified like this:\n\n.. code-block:: edgeql\n\n    with U := (select User filter .name like '%user%')\n    select (\n        matches := array_agg(U {name}),\n        total := count(U),\n        total_users := count(User),\n    );\n\nWithout the :eql:func:`array_agg` the above query would return ``{}``\ninstead of the named tuple if no ``matches`` are found.\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`EdgeQL > Select <ref_eql_select>`\n  * - :ref:`Reference > Commands > Select <ref_eql_statements_select>`\n"
  },
  {
    "path": "docs/resources/cheatsheets/update.rst",
    "content": ".. _ref_cheatsheet_update:\n\nUpdating data\n=============\n\n.. note::\n\n    The types used in these queries are defined :ref:`here\n    <ref_cheatsheet_object_types>`.\n\n\n----------\n\n\nFlag all reviews to a specific movie:\n\n.. code-block:: edgeql\n\n    update Review\n    filter\n        Review.movie.title = 'Dune'\n        and\n        Review.movie.director.last_name = 'Villeneuve'\n    set {\n        flag := True\n    }\n\n\n----------\n\n\nAdd an actor with a specific ``list_order`` link property to a movie:\n\n.. code-block:: edgeql\n\n    update Movie\n    filter\n        .title = 'Dune'\n        and\n        .directors.last_name = 'Villeneuve'\n    set {\n        actors := (\n            insert Person {\n                first_name := 'Timothee',\n                last_name := 'Chalamet',\n                image := 'tchalamet.jpg',\n                @list_order := 1,\n            }\n        )\n    }\n\n\n----------\n\n\nUsing a ``for`` query to set a specific ``list_order`` link property\nfor the actors list:\n\n.. code-block:: edgeql\n\n    update Movie\n    filter\n        .title = 'Dune'\n        and\n        .directors.last_name = 'Villeneuve'\n    set {\n        actors := (\n            for x in {\n                ('Timothee Chalamet', 1),\n                ('Zendaya', 2),\n                ('Rebecca Ferguson', 3),\n                ('Jason Momoa', 4),\n            }\n            union (\n                select Person {@list_order := x.1}\n                filter .full_name = x.0\n            )\n        )\n    }\n\n\n----------\n\n\nUpdating a multi link by adding one more item:\n\n.. code-block:: edgeql\n\n    update Movie\n    filter\n        .title = 'Dune'\n        and\n        .directors.last_name = 'Villeneuve'\n    set {\n        actors += (\n            insert Person {\n                first_name := 'Dave',\n                last_name := 'Bautista',\n                image := 'dbautista.jpg',\n            }\n        )\n    }\n\n\n----------\n\n\nUpdating a multi link by removing an item:\n\n.. code-block:: edgeql\n\n    update Movie\n    filter\n        .title = 'Dune'\n        and\n        .directors.last_name = 'Villeneuve'\n    set {\n        actors -= (\n            select Person\n            filter\n                .full_name = 'Jason Momoa'\n        )\n    }\n\n\n----------\n\n\nUpdate the ``list_order`` link property for a specific link:\n\n.. code-block:: edgeql\n\n    update Movie\n    filter\n        .title = 'Dune'\n        and\n        .directors.last_name = 'Villeneuve'\n    set {\n        # The += operator will allow updating only the\n        # specified actor link.\n        actors += (\n            select Person {\n                @list_order := 5,\n            }\n            filter .full_name = 'Jason Momoa'\n        )\n    }\n\n\n.. list-table::\n  :class: seealso\n\n  * - **See also**\n  * - :ref:`EdgeQL > Update <ref_eql_update>`\n  * - :ref:`Reference > Commands > Update <ref_eql_statements_update>`\n"
  },
  {
    "path": "docs/resources/guides/contributing/code.rst",
    "content": ".. _ref_guide_contributing_code:\n\n====\nCode\n====\n\n:edb-alt-title: Developing Gel\n\nThis section describes how to build Gel locally, how to use its\ninternal tools, and how to contribute to it.\n\n.. warning::\n\n    Code-changing pull requests without adding new tests might take\n    longer time to be reviewed and merged.\n\n.. _ref_guide_contributing_code_build:\n\nBuilding Locally\n================\n\nThe following instructions should be used to create a \"dev\" build on\nLinux or macOS.  Windows is not currently supported.\n\n.. rubric:: Build Requirements\n\n* GNU make version 3.80 or newer;\n* C compiler (GCC or clang);\n* Rust compiler and Cargo 1.80 or later;\n* autotools;\n* Python 3.12 dev package;\n* Bison 1.875 or later;\n* Flex 2.5.31 or later;\n* Perl 5.8.3 or later;\n* icu4c 4.6 or later;\n* Zlib (zlibg1-dev on Ubuntu);\n* Readline dev package;\n* Libuuid dev package;\n* Node.js 14 or later;\n* Yarn 1\n* Protobuf & C bindings for Protobuf\n\n.. zlib, readline and libuuid are required to build postgres. Should be removed\n   when custom postgres build is no longer needed.\n\nOn Ubuntu 24.04, these can be installed by running:\n\n.. code-block:: bash\n\n   $ apt install make gcc rust-all autotools-dev python3.12-dev \\\n     python3.12-venv bison flex libreadline-dev perl zlib1g-dev \\\n     protobuf-compiler libprotobuf-c-dev uuid-dev nodejs npm\n   $ npm i -g corepack\n   $ corepack enable && corepack prepare yarn@stable --activate\n\nOn macOS, these can be installed by running:\n\n.. code-block:: bash\n\n   $ brew install rustup autoconf libtool python@3.12 readline zlib nodejs icu4c protobuf protobuf-c\n\nTo build Postgres on macOS, you'll need to set ``PKG_CONFIG_PATH`` so it can find\nthe ``icu4c`` libraries. This can be done manually each time you rebuild Postgres,\nor set in your ``.profile`` or virtual environment.\n\n.. code-block:: bash\n\n   $ export PKG_CONFIG_PATH=\"$(brew --prefix icu4c)/lib/pkgconfig\"\n\nA Nix shell with all dependencies and a Python virtual environment can\nbe built with the following ``shell.nix`` file.\n\n.. code::\n\n   with import <nixpkgs> {};\n   pkgs.mkShell {\n       name = \"gel dev shell\";\n       venvDir = \"./venv\";\n\n       buildInputs = with pkgs; [\n           python312Packages.python\n           python312Packages.venvShellHook\n           rustup\n           autoconf\n           automake\n           bison\n           flex\n           perl\n           zlib\n           readline\n           libuuid\n           nodejs\n           yarn\n           openssl\n           pkg-config\n           icu\n           protobuf\n           protobufc\n       ];\n       LD_LIBRARY_PATH = lib.makeLibraryPath [ pkgs.stdenv.cc.cc ];\n       LIBCLANG_PATH = \"${llvmPackages.libclang.lib}/lib\";\n\n       # If you are using NixOS:\n       # Postgres configure script uses /bin/pwd,\n       # which does not exist on NixOS.\n       #\n       # I had a workaround for replacing /bin/pwd with pwd,\n       # but it was annoying that postgres/ was dirty.\n       # So my fix now is:\n       # $ sudo sh -c \"echo 'pwd' > /bin/pwd\"\n       # $ sudo chmod +x /bin/pwd\n   }\n\n.. rubric:: Instructions\n\nThe easiest way to set up a development environment is to create a\nPython \"venv\" with all dependencies and commands installed into it.\n\n#. Make a new directory that will contain checkouts of `gel <gel_>`_\n   and `gel-python <gelpy_>`_.  The name of the directory is\n   arbitrary, we will use \"dev\" in this guide:\n\n   .. code-block:: bash\n\n      $ mkdir ~/dev\n      $ cd ~/dev\n\n#. Clone the gel repository using ``--recursive``\n   to clone all submodules:\n\n   .. code-block:: bash\n\n      $ git clone --recursive https://github.com/geldata/gel.git\n\n#. Create a Python 3.12 virtual environment and activate it:\n\n   .. code-block:: bash\n\n      $ python3.12 -m venv gel-dev\n      $ source gel-dev/bin/activate\n\n#. Build gel (the build will take a while):\n\n   .. code-block:: bash\n\n      $ cd gel\n      $ pip install -v -e \".[test]\"\n\n   In addition to compiling Gel and all dependencies, this will also\n   install the ``edb`` and |gelcmd| command line tools into the current\n   Python virtual environment.\n\n   It will also install libraries used during development.\n\n#. Run tests:\n\n   .. code-block:: bash\n\n      $ edb test\n\nThe new virtual environment is now ready for development and can be\nactivated at any time.\n\n\nRunning Tests\n=============\n\nTo run all Gel tests simply use the ``$ edb test`` command without\narguments.\n\nThe command also supports running a few selected tests.  To run all\ntests in a test case file:\n\n.. code-block:: bash\n\n   $ edb test tests/test_edgeql_calls.py\n\n   # or run two files:\n   $ edb test tests/test_edgeql_calls.py tests/test_edgeql_for.py\n\nTo pattern-match a test by its name:\n\n.. code-block:: bash\n\n   $ edb test -k test_edgeql_calls_01\n\n   # or run all tests that contain \"test_edgeql_calls\":\n   $ edb test -k test_edgeql_calls\n\nSee ``$ edb test --help`` for more options.\n\n\nDev Server\n==========\n\nUse the ``$ edb server`` command to start the development server.\n\nYou can then use another terminal to open a REPL to the server using the\n|gelcmd| command, or connect to it using one of the language bindings.\n\n\nTest Branches\n=============\n\nUse the ``$ edb inittestdb`` command to create and populate branches that are\nused by unit tests.\n\n.. _rst: https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html\n.. _gelpy: https://github.com/geldata/gel-python\n.. _gel: https://github.com/geldata/gel\n"
  },
  {
    "path": "docs/resources/guides/contributing/documentation.rst",
    "content": ".. _ref_guide_contributing_documentation:\n\n=============\nDocumentation\n=============\n\n:edb-alt-title: Writing Gel Documentation\n\nWe pride ourselves on having some of the best documentation around, but we want\nyou to help us make it even better. Documentation is a great way to get started\ncontributing to Gel. Improvements to our documentation create a better\nexperience for every developer coming through the door behind you.\n\nFollow our general and style guidelines to make for a smooth contributing\nexprience, both for us and for you. You may notice that the existing\ndocumentation doesn't always follow the guidelines laid out here. They are\naspirational, so there are times when we intentionally break with them. Other\ntimes, bits of documentation may not be touched for a while and so may not\nreflect our current guidelines. These are great \"low-hanging fruit\"\nopportunities for your contributions!\n\n\nGuidelines\n==========\n\n- **Avoid changes that don't fix an obvious mistake or add clarity.** This is\n  subjective, but try to look at your changes with a critical eye. Do they fix\n  errors in the original like misspellings or typos? Do they make existing\n  prose more clear or accessible while maintaining accuracy? If you answered\n  \"yes\" to either of those questions, this might be a great addition to our\n  docs! If not, consider starting a discussion instead to see if your changes\n  might be the exception to this guideline before submitting.\n- **Keep commits and pull requests small.** We get it. It's more convenient to\n  throw all your changes into a single pull request or even into a single\n  commit. The problem is that, if some of the changes are good and others don't\n  quite work, having everything in one bucket makes it harder to filter out the\n  great changes from those that need more work.\n- **Make spelling and grammar fixes in a separate pull request from any content\n  changes.** These changes are quick to check and important to anyone reading\n  the docs. We want to make sure they hit the live documentation as quickly as\n  possible without being bogged down by other changes that require more\n  intensive review.\n\nStyle\n=====\n\n- **Remove trailing whitespace or whitespace on empty lines.**\n- **Surround references to parameter named with asterisks.** You may be tempted\n  to surround parameter names with double backticks (````param````). We avoid\n  that in favor of ``*param*``, in order to distinguish between parameter\n  references and inline code (which *should* be surrounded by double\n  backticks).\n- **Gel is singular.** Choose \"Gel is\" over \"Gel are\" and \"Gel\n  does\" over \"Gel do.\"\n- **Use American English spellings.** Choose \"color\" over \"colour\" and\n  \"organize\" over \"organise.\"\n- **Use the Oxford comma.** When delineating a series, place a comma between\n  each item in the series, even the one with the conjunction. Use \"eggs, bacon,\n  and juice\" rather than \"eggs, bacon and juice.\"\n- **Write in the simplest prose that is still accurate and expresses everything\n  you need to convey.** You may be tempted to write documentation that sounds\n  like a computer science textbook. Sometimes that's necessary, but in most\n  cases, it isn't. Prioritize accuracy first and accessibility a close second.\n- **Be careful using words that have a special meaning in the context of\n  Gel.** In casual speech or writing, you might talk about a \"set\" of\n  something in a generic sense. Using the word this way in Gel documentation\n  might easily be interpreted as a reference to Gel's :ref:`sets\n  <ref_eql_sets>`. Avoid this kind of casual usage of key terms.\n\n\nWhere to Find It\n================\n\nMost of our documentation (including this guide) lives in `the geldata\nrepository <https://github.com/geldata/gel/>`_ in `the docs directory\n<https://github.com/geldata/gel/tree/master/docs>`_.\n\n\nHow to Build It\n===============\n\ngeldata/gel\n-----------\n\nThe ``geldata`` repository contains all of its documentation in the ``docs/``\ndirectory. Run ``make docs`` to build the documentation in the geldata repo. The\nrepository contains a ``Makefile`` for all of Sphinx's necessary build options.\nThe documentation will be built to ``docs/build``.\n\nTo run tests, first :ref:`build Gel locally\n<ref_guide_contributing_code_build>`. Then run ``edb test -k doc``.\n\nBuilding the docs from this repo will not give you a high-fidelity\nrepresentation of what users will see on the web site. For that, you may want\nto do a full documentation build.\n\nFull Documentation Build\n------------------------\n\nA full documentation build requires more setup, but it is the only way to see\nyour documentation exactly as the user will see it. This is not required, but\nit can be useful to help us review and approve your request more quickly by\navoiding mistakes that would be easier to spot when they are fully rendered.\n\nTo build, clone `our website repository <https://github.com/geldata/website>`_\nand `follow the installation instructions\n<https://github.com/geldata/website#installation>`_. Then run ``yarn dev`` to\nstart a development server which also triggers a build of all the\ndocumentation.\n\n.. note::\n\n    The watch task builds documentation changes, but it cannot trigger\n    auto-reload in the browser. You will need to manually reload the browser to\n    see changes made to the documentation.\n\nSphinx and reStructuredText\n===========================\n\nOur documentation is first built through `Sphinx\n<https://www.sphinx-doc.org/>`_ and is written in `reStructuredText\n<https://docutils.sourceforge.io/rst.html>`_. If you're unfamiliar with\nreStructuredText, `the official primer\n<https://docutils.sourceforge.io/docs/user/rst/quickstart.html>`_ is a good\nplace to start. `The official cheatsheet\n<https://docutils.sourceforge.io/docs/user/rst/quickref.html>`_ serves as a\ngreat companion reference while you write. Sphinx also offers their own\n`reStructuredText primer\n<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html>`_.\n\nSphinx not only builds the documentation but also extends reStructuredText to\nallow for a more ergonomic experience when crafting docs.\n\nReStructuredText is an easy-to-learn markup language built for documentation.\nHere are the most commonly used elements across our documentation.\n\nreStructuredText Basics\n-----------------------\n\nHeadings\n^^^^^^^^\n\nReStructuredText headings are underlined (and sometimes overlined) with various\ncharacters. It's flexible about which characters map to which heading levels\nand will automatically assign heading levels to characters based on the\nhierarchy of the document.\n\nTo make it easier to quickly discern the level of a heading across our\ndocumentation, we use a consistent hierarchy across all pages.\n\n1. ``=`` under and over- Used for the top-level heading which is usually the\n   page title.\n2. ``=`` under only\n3. ``-``\n4. ``^``\n\n**Example**\n\n.. code-block::\n\n    ==========\n    Page Title\n    ==========\n\n    Section\n    =======\n\n    Sub-Section\n    -----------\n\n    Sub-Sub-Section\n    ^^^^^^^^^^^^^^^\n\nIf you need additional heading levels, you may use the ``.. rubric::``\ndirective and pass it your heading by adding the heading text on the same line.\n\n**Example**\n\n.. code-block::\n\n    .. rubric:: Yet Another Heading\n\n\nInline Formatting\n^^^^^^^^^^^^^^^^^\n\nText can be *italicized* by surrounding it with asterisks.\n\n.. code-block::\n\n    *italicized*\n\n**Bold** text by surrounding it with double asterisks.\n\n.. code-block::\n\n    **Bold**\n\nLabels and Links\n^^^^^^^^^^^^^^^^\n\nLabels make it easy to link across our documentation.\n\n.. code-block::\n\n    .. _ref_eql_select_objects:\n\nAll pages must have a label at the top, but inner labels are added only when we\nneed to link to them. Feel free to add a label to a section you need to link\nto. Follow the convention of ``_ref_<main-section>_<page>_<section>`` when\nnaming labels. Check the page's main label at the top if you're not sure how to\nname your label. Append an underscore and the name of the section to the page's\nlabel. If you create a page, make sure you add a main label to the top of it.\n\nCreate internal links using the ``:ref:`` role. First find the label you want\nto link to. Reference the label's name in your role inside backticks (``\\```)\nremoving the leading underscore as in the example below.\n\n**Example**\n\n.. code-block::\n\n    :ref:`ref_eql_select_objects`\n\n**Rendered**\n\n:ref:`ref_eql_select_objects`\n\nThe label being linked can be on the same page as the link or on an entirely\ndifferent page. Sphinx will find the label and link to the appropriate page and\nsection.\n\nYou may also customize the link text.\n\n**Example**\n\n.. code-block::\n\n    :ref:`our documentation on selecting objects <ref_eql_select_objects>`\n\n**Rendered**\n\n:ref:`our documentation on selecting objects <ref_eql_select_objects>`\n\nTo link to documentation for EdgeQL functions, statements, types, operators, or\nkeywords, see the instructions in\n:ref:`ref_guide_contributing_documentation_edgeql`.\n\nSpecial Paragraphs\n^^^^^^^^^^^^^^^^^^\n\nCall out a paragraph as a note or warning using the appropriate directives.\n\n**Example**\n\n.. code-block::\n\n    .. note::\n\n        This paragraph is a note.\n\n**Rendered**\n\n.. note::\n\n    This paragraph is a note.\n\n**Example**\n\n.. code-block::\n\n    .. warning::\n\n        This paragraph is a warning.\n\n**Rendered**\n\n.. warning::\n\n    This paragraph is a warning.\n\nYou may also add a title to any of these paragraphs by passing it to the\ndirective by placing it on the same line.\n\n**Example**\n\n.. code-block::\n\n    .. note:: A Note\n\n        This paragraph is a note.\n\n**Rendered**\n\n.. note:: A Note\n\n    This paragraph is a note.\n\nReusing Documentation\n^^^^^^^^^^^^^^^^^^^^^\n\nIf you have documentation that will be reused in multiple contexts, you can\nwrite it in a separate ``.rst`` file and include that file everywhere it should\nappear.\n\n.. code-block::\n\n    .. include:: ../stdlib/constraint_table.rst\n\nTables and Lists\n^^^^^^^^^^^^^^^^\n\nWe use tables and lists in a few different contexts.\n\n**Example**\n\n.. code-block::\n\n    .. list-table::\n\n        * - Arrays\n          - ``array<str>``\n        * - Tuples (unnamed)\n          - ``tuple<str, int64, bool>``\n        * - Tuples (named)\n          - ``tuple<name: str, age: int64, is_awesome: bool>``\n        * - Ranges\n          - ``range<float64>``\n\n**Rendered**\n\n.. list-table::\n\n    * - Arrays\n      - ``array<str>``\n    * - Tuples (unnamed)\n      - ``tuple<str, int64, bool>``\n    * - Tuples (named)\n      - ``tuple<name: str, age: int64, is_awesome: bool>``\n    * - Ranges\n      - ``range<float64>``\n\n**Example**\n\n.. code-block::\n\n    .. list-table::\n        :class: seealso\n\n        * - **See also**\n        * - :ref:`Schema > Access policies <ref_datamodel_access_policies>`\n        * - :ref:`SDL > Access policies <ref_eql_sdl_access_policies>`\n\n**Rendered**\n\n.. list-table::\n    :class: seealso\n\n    * - **See also**\n    * - :ref:`Schema > Access policies <ref_datamodel_access_policies>`\n    * - :ref:`SDL > Access policies <ref_eql_sdl_access_policies>`\n\n.. note::\n\n    The ``seealso`` class adds a spacer above the table to push the table\n    away from the main page content.\n\n**Example**\n\n.. code-block::\n\n    ====================================== =============================\n    Syntax                                 Inferred type\n    ====================================== =============================\n    :eql:code:`select 3;`                  :eql:type:`int64`\n    :eql:code:`select 3.14;`               :eql:type:`float64`\n    :eql:code:`select 314e-2;`             :eql:type:`float64`\n    :eql:code:`select 42n;`                :eql:type:`bigint`\n    :eql:code:`select 42.0n;`              :eql:type:`decimal`\n    :eql:code:`select 42e+100n;`           :eql:type:`decimal`\n    ====================================== =============================\n\n**Rendered**\n\n====================================== =============================\nSyntax                                 Inferred type\n====================================== =============================\n:eql:code:`select 3;`                  :eql:type:`int64`\n:eql:code:`select 3.14;`               :eql:type:`float64`\n:eql:code:`select 314e-2;`             :eql:type:`float64`\n:eql:code:`select 42n;`                :eql:type:`bigint`\n:eql:code:`select 42.0n;`              :eql:type:`decimal`\n:eql:code:`select 42e+100n;`           :eql:type:`decimal`\n====================================== =============================\n\nSphinx Basics\n-------------\n\nTables of Contents\n^^^^^^^^^^^^^^^^^^\n\nSphinx requires that every page in the documentation be referenced from a table\nof contents. Use the ``.. toctree::`` directive to create a table of contents.\n\n**Example**\n\n.. code-block::\n\n    .. toctree::\n        :maxdepth: 3\n        :hidden:\n\n        code\n        documentation\n\nMost of our tables of contents use the roles you see in this example to set a\nmaximum depth of 3 and to hide the table of contents. This is not required\nthough if other options make sense in your context. Even though the tables are\nhidden, their content still gets rendered in the left sidebar navigation.\n\nWe generally use relative references in the ``toctree`` directive which\nreference the pages relative to the location of the page that contains the\ndirective. The order of the references in the directive determines their order\nin the sidebar navigation.\n\nIf any document is not included in any ``toctree``, it will cause Sphinx to\nerror on the build unless you add the ``:orphan:`` role to the top of the page.\nWe don't want to use this technique for most pages although there are\nexceptions.\n\nRendering Code\n==============\n\nUse these tools to render code in your documentation contribution.\n\nInline Code\n-----------\n\nRender inline code by surrounding it with double backticks:\n\n**Example**\n\n.. code-block::\n\n    With the help of a ``with`` block, we can add filters, ordering, and\n    pagination clauses.\n\n**Rendered**\n\nWith the help of a ``with`` block, we can add filters, ordering, and\npagination clauses.\n\n.. warning::\n\n    Marking up inline code with single backticks a la Markdown will throw an\n    error in Sphinx when building the documentation.\n\nCode Blocks\n-----------\n\n.. code-block::\n\n    .. code-block:: [<language>]\n\n        <code goes here>\n\nRender a block of code. You can optionally provide a language argument.\nBelow are the most common languages used in our docs:\n\n* ``bash``- Include the prompt and optionally the output. When a user clicks\n  the \"copy\" button to copy the code, it will copy only the input without the\n  prompt and output.\n\n  **Example**\n\n  .. code-block::\n\n      .. code-block:: bash\n\n          $ gel configure set listen_addresses 127.0.0.1 ::1\n\n  **Rendered**\n\n  .. code-block:: bash\n\n      $ gel configure set listen_addresses 127.0.0.1 ::1\n\n* ``edgeql``- Used for queries.\n\n  **Example**\n\n  .. code-block::\n\n      .. code-block:: edgeql\n\n          select BlogPost filter .id = <uuid>$blog_id;\n\n  **Rendered**\n\n  .. code-block:: edgeql\n\n      select BlogPost filter .id = <uuid>$blog_id;\n\n* ``edgeql-repl``- An alternative to vanilla ``edgeql``. Include the prompt and\n  optionally the output. When a user clicks the \"copy\" button to copy the code,\n  it will copy only the input without the prompt and output.\n\n  **Example**\n\n  .. code-block::\n\n      .. code-block:: edgeql-repl\n\n          db> insert Person { name := <str>$name };\n          Parameter <str>$name: Pat\n          {default::Person {id: e9009b00-8d4e-11ed-a556-c7b5bdd6cf7a}}\n\n  **Rendered**\n\n  .. code-block:: edgeql-repl\n\n      db> insert Person { name := <str>$name };\n      Parameter <str>$name: Pat\n      {default::Person {id: e9009b00-8d4e-11ed-a556-c7b5bdd6cf7a}}\n\n* ``go``\n* ``javascript``\n* ``python``\n\n  **Example**\n\n  .. code-block::\n\n      .. code-block:: javascript\n\n          await client.query(\"select 'I ❤️ ' ++ <str>$name ++ '!';\", {\n            name: \"rock and roll\"\n          });\n\n  **Rendered**\n\n  .. code-block:: javascript\n\n      await client.query(\"select 'I ❤️ ' ++ <str>$name ++ '!';\", {\n        name: \"rock and roll\"\n      });\n\n* ``sdl``- Used for defining schema.\n\n  **Example**\n\n  .. code-block::\n\n      .. code-block:: sdl\n\n          module default {\n            type Person {\n              required property name -> str { constraint exclusive };\n            }\n          }\n\n  **Rendered**\n\n  .. code-block:: sdl\n\n      module default {\n        type Person {\n          required property name -> str { constraint exclusive };\n        }\n      }\n\n* ``<language>-diff``- Shows changes in a code block. Each line of code in\n  these blocks must be prefixed by a character: ``+`` for an added line, ``-``\n  for a removed line, or an empty space for an unchanged line.\n\n  **Example**\n\n  .. code-block::\n\n      .. code-block:: sdl-diff\n\n              type Movie {\n          -     property title -> str;\n          +     required property title -> str;\n                multi link actors -> Person;\n              }\n\n  **Rendered**\n\n  .. code-block:: sdl-diff\n\n          type Movie {\n      -     property title -> str;\n      +     required property title -> str;\n            multi link actors -> Person;\n          }\n\n* No language- Formats the text as a code block but without syntax\n  highlighting. Use this for syntaxes that do not offer highlighting or in\n  cases where highlighting is unnecessary.\n\n  **Example**\n\n  .. code-block::\n\n      .. code-block::\n\n          [\n            {\"id\": \"ea7bad4c-35d6-11ec-9519-0361f8abd380\"},\n            {\"id\": \"6ddbb04a-3c23-11ec-b81f-7b7516f2a868\"},\n            {\"id\": \"b233ca98-3c23-11ec-b81f-6ba8c4f0084e\"},\n          ]\n\n  **Rendered**\n\n  .. code-block::\n\n    [\n      {\"id\": \"ea7bad4c-35d6-11ec-9519-0361f8abd380\"},\n      {\"id\": \"6ddbb04a-3c23-11ec-b81f-7b7516f2a868\"},\n      {\"id\": \"b233ca98-3c23-11ec-b81f-6ba8c4f0084e\"},\n    ]\n\n  .. note::\n\n      Code blocks without a language specified do not have a \"copy\" button.\n\nCode Tabs\n---------\n\n``.. tabs::``\n\nTabs are used to present code examples in multiple languages. This can be\nuseful when you want to show a query in, for example, both EdgeQL and the\nTypeScript query builder.\n\n**Example**\n\n.. code-block::\n\n    .. tabs::\n\n        .. code-tab:: edgeql\n\n            insert Movie {\n              title := 'Doctor Strange 2',\n              release_year := 2022\n            };\n\n        .. code-tab:: typescript\n\n            const query = e.insert(e.Movie, {\n              title: 'Doctor Strange 2',\n              release_year: 2022\n            });\n\n            const result = await query.run(client);\n\n**Rendered**\n\n.. tabs::\n\n    .. code-tab:: edgeql\n\n        insert Movie {\n          title := 'Doctor Strange 2',\n          release_year := 2022\n        };\n\n    .. code-tab:: typescript\n\n        const query = e.insert(e.Movie, {\n          title: 'Doctor Strange 2',\n          release_year: 2022\n        });\n\n        const result = await query.run(client);\n\n.. _ref_guide_contributing_documentation_edgeql:\n\nDocumenting EdgeQL\n==================\n\nTools to help document EdgeQL are in the ``:eql:`` domain.\n\nFunctions\n---------\n\nTo document a function use a ``.. eql:function::`` directive. Include these\nelements:\n\n* Specify the full function signature with a fully qualified name on the same\n  line as the directive.\n* Add a description of each parameter using ``:param $<name>: description:``.\n  *$<name>* must match the the name of the parameter in function's signature.\n  If a parameter is positional rather than named, its number should be used\n  instead (e.g. ``$1``).\n* Add a type for each parameter using ``:paramtype $<name>: <type>``. For\n  example: ``:paramtype $<name>: int64`` declares that the type of the\n  *$<name>* parameter is ``int64``. If a parameter has more than one valid\n  type, list them separated by \"or\" like this: ``:paramtype $<name>: int64 or\n  str``.\n* Document the return value of the function with ``:return:`` and\n  ``:returntype:``. ``:return:`` marks a description of the return value and\n  ``:returntype:`` its type.\n* Finish with a few descriptive paragraphs and code samples. The first\n  paragraph must be a single sentence no longer than 79 characters describing\n  the function.\n\n**Example**\n\n.. code-block::\n\n    .. eql:function:: std::array_agg(set of any, $a: any) -> array<any>\n\n        :param $1: input set\n        :paramtype $1: set of any\n\n        :param $a: description of this param\n        :paramtype $a: int64 or str\n\n        :return: array made of input set elements\n        :returntype: array<any>\n\n        Return the array made from all of the input set elements.\n\n        The ordering of the input set will be preserved if specified.\n\nYou can link to a function's documentation by using the ``:eql:func:`` role.\nFor instance:\n\n* ``:eql:func:`array_agg```;\n* ``:eql:func:`std::array_agg```;\n\nThese will link to a function using the function's name as you have written in\nbetween the backticks followed by parentheses. Here are the above links\nrendered:\n\n* :eql:func:`array_agg`;\n* :eql:func:`std::array_agg`;\n\nYou can customize a link's label with this syntax: ``:eql:func:`aggregate a set\nas an array <array_agg>```. Here's the rendered output: :eql:func:`aggregate a\nset as an array <array_agg>`\n\nOperators\n---------\n\nUse the ``.. eql:operator::`` directive to document an operator. On the same\nline as the directive, provide a string argument of the format ``<operator-id>:\n<operator-signature>``\n\nAdd a ``:optype <operand-name>: <type>`` field for each of the operator\nsignature's operands to declare their types.\n\n**Example**\n\n.. code-block::\n\n    .. eql:operator:: PLUS: A + B\n\n        :optype A: int64 or str or bytes\n        :optype B: any\n        :resulttype: any\n\n        Arithmetic addition.\n\nYou can link to an operator's documentation by using the ``:eql:op:`` role,\nfollowed by the operator's ID you specified in your argument to ``..\neql:operator::``. For instance: ``:eql:op:`plus``` which renders as\n:eql:op:`plus`. You can customize the link label like this: ``:eql:op:`+\n<plus>```, which renders as :eql:op:`+ <plus>`.\n\nStatements\n----------\n\nUse the ``:eql-statement:`` field to sections that describe a statement. Add\nthe ``:eql-haswith:`` field if the statement supports a :eql:kw:`with` block.\n\n.. code-block::\n\n    Select\n    ======\n\n    :eql-statement:\n    :eql-haswith:\n\n    ``select``--retrieve or compute a set of values.\n\n    .. eql:synopsis::\n\n        [ with <with-item> [, ...] ]\n\n        select <expr>\n\n        [ filter <filter-expr> ]\n\n        [ order by <order-expr> [direction] [then ...] ]\n\n        [ offset <offset-expr> ]\n\n        [ limit  <limit-expr> ] ;\n\nAfter laying out the formal syntax, describe the function of each clause with a\nsynopsis like this:\n\n.. code-block::\n\n    :eql:synopsis:`filter <filter-expr>`\n        The optional ``filter`` clause, where :eql:synopsis:`<filter-expr>`\n        is any expression that has a result of type :eql:type:`bool`.\n        The condition is evaluated for every element in the set produced by\n        the ``select`` clause.  The result of the evaluation of the\n        ``filter`` clause is a set of boolean values.  If at least one value\n        in this set is ``true``, the input element is included, otherwise\n        it is eliminated from the output.\n\nThese descriptions can each contain as many paragraphs as needed to adequately\ndescribe the clause. Follow the format used in the PostgreSQL documentation.\nSee `the PostgreSQL SELECT statement reference page\n<https://www.postgresql.org/docs/10/static/sql-select.html>`_ for an example.\n\nUse ``:eql:stmt:`select``` to link to the statement's documentation. When\nrendered the link looks like this: :eql:stmt:`select`. Customize the label with\n``:eql:stmt:`the select statement <select>``` which renders as this:\n:eql:stmt:`the select statement <select>`.\n\nTypes\n-----\n\nTo document a type, use the ``.. eql:type::`` directive. Follow the directive\nwith the fully-qualified name of the type on the same line. The block should\ncontain the type's description.\n\n.. code-block::\n\n    .. eql:type:: std::bytes\n\n        A sequence of bytes.\n\nTo link to a type's documentation, use ``:eql:type:`bytes``` which renders as\n:eql:type:`bytes`. You may use the fully qualified name in your reference —\n``:eql:type:`std::bytes``` — which renders as :eql:type:`std::bytes`. Both\nforms reference the same location in the documentation. Link labels can be\ncustomized with ``:eql:type:`the bytes type <bytes>``` which renders like this:\n:eql:type:`the bytes type <bytes>`.\n\nKeywords\n--------\n\nDocument a keyword using the ``.. eql:keyword::`` directive.\n\n.. code-block::\n\n    .. eql:keyword:: with\n\n        The ``with`` block in EdgeQL is used to define aliases.\n\nIf a keyword is compound use a hyphen between each word.\n\n.. code-block::\n\n    .. eql:keyword:: set-of\n\nTo link to a keyword's documentation, use the ``:eql:kw:`` role like this:\n``:eql:kw:`detached``` which renders as :eql:kw:`detached`. You can customize\nthe link label like this: ``:eql:kw:`the \"detached\" keyword <detached>``` which\nrenders as :eql:kw:`the \"detached\" keyword <detached>`.\n\nDocumenting the EdgeQL CLI\n==========================\n\n\"gelcmd\" role\n-------------\n\n.. lint-off\n\nUse ``:gelcmd:`` role to document or refer to CLI commands, use ``|gelcmd|`` to\nrender just ``gel`` in the text.\n\n.. lint-on\n\n**Example**\n\n.. code-block::\n\n    Use |gelcmd| to cook yourself a delicious meal. Use :gelcmd:`eat` to\n    eat it.\n\n**Rendered**\n\nUse |gelcmd| to cook yourself a delicious meal. Use :gelcmd:`eat` to\neat it.\n\ncli:synopsis\n------------\n\nDocument a CLI command using the ``cli:synopsis`` directive like this:\n\n**Example**\n\n.. code-block::\n\n    .. cli:synopsis::\n\n        gel dump [<options>] <path>\n\n**Rendered**\n\n.. cli:synopsis::\n\n    gel dump [<options>] <path>\n\nThe synopsis should follow the format used in the PostgreSQL documentation. See\n`the PostgreSQL SELECT statement reference page\n<https://www.postgresql.org/docs/10/static/sql-select.html>`_ for an example.\n\nYou can then document arguments and options using the ``:cli:synopsis:`` role.\n\n**Example**\n\n.. code-block::\n\n    :cli:synopsis:`<path>`\n        The name of the file to backup the database into.\n\n**Rendered**\n\n:cli:synopsis:`<path>`\n    The name of the file to backup the database into.\n\n\nSubstitutions\n=============\n\nThere's a few substitutions and ReST toles that are useful when documenting\ncertain terms and concepts:\n\n.. lint-off\n\n.. list-table::\n\n    * - **ReST markup**\n      - **Description**\n\n    * - ``|gelcmd|`` and ``:gelcmd:`blah```\n      - Renders to ``$ gel`` and ``$ gel blah``, with a context\n        tooltip explaining that the CLI command used to be called ``edgedb``.\n\n    * - ``|geluri|`` and ``:geluri:`u:p@h:p/b```\n      - Renders to ``gel://`` and ``gel://u:p@h:p/b``,\n        with a context tooltip explaining that the URI used to be ``edgedb://``.\n\n    * - ``|Gel|`` and ``|Gel's|``\n      - Renders to \"Gel\" and \"Gel's\", with a context tooltip explaining that\n        the Gel used to be called \"EdgeDB\".\n\n    * - ``|branch|`` and ``|branches|``\n      - Renders to \"branch\" and \"branches\", with a context tooltip explaining\n        that the term used to be called \"database\" in EdgeDB < 5.\n\n    * - ``|EdgeDB|``\n      - Renders to \"EdgeDB\", with a context tooltip explaining that\n        EdgeDB was renamed to Gel.\n\n    * - ``|.gel|``\n      - Renders to ``.gel``, with a context tooltip explaining that the file\n        extension used to be ``.esdl``.\n\n    * - ``:dotgel:`foo```\n      - Renders to ``foo.gel``, with a context tooltip explaining that the file\n        extension used to be ``.esdl``.\n\n    * - ``:gelenv:`BLAH```\n      - Renders to ``GEL_BLAH``, with a context tooltip explaining that this\n        environment variable used to be ``EDGEDB_BLAH``.\n\n    * - ``|gel.toml|``\n      - Renders to ``gel.toml``, with a context tooltip explaining that this\n        file used to be called ``edgedb.toml``.\n\n    * - ``|gel-server|``\n      - Renders to ``$ gel-server``, with a context tooltip explaining that\n        this command used to be called ``edgedb-server``.\n\n    * - ``|admin|``\n      - Renders to ``admin``, with a context tooltip explaining that this\n        username used to be called ``edgedb``.\n\n    * - ``|main|``\n      - Renders to ``main``, with a context tooltip explaining that this\n        branch used to be called ``edgedb``.\n\n.. lint-on\n\nDocumentation Versioning\n========================\n\nSince |Gel| functionality is mostly consistent across versions, we offer a\nsimple method of versioning documentation using two directives.\n\n.. warning::\n\n    Although these are directives included in Sphinx, we have customized them\n    to behave differently. Please read this documentation even if you're\n    already familiar with the Sphinx directives mentioned here.\n\nNew in Version\n--------------\n\nContent addressing anything new in a given version are marked with the\n``versionadded`` directive. Provide the applicable version as an argument by\nplacing it just after the directive on the same line.\n\nThe directive behaves differently depending on the context.\n\n* When the directive has content (i.e., an indented paragraphs below the\n  directive), that content will be shown or hidden based on the version switch.\n* When the directive is placed immediately after a section header or inside a\n  description block for a function, type, operator, statement, or keyword, that\n  entire section or block is marked to be shown or hidden based on the version\n  selected.\n* When the directive is placed on the top line of any page before any content\n  or reStructuredText labels (e.g., ``.. _ref_eql_select:``), it applies to the\n  entire page.\n\n**Example with Content**\n\n.. code-block::\n\n    .. versionadded:: 7.0\n\n        This is a new feature that was added in Gel 7.0.\n\n**Rendered**\n\n.. versionadded:: 7.0\n\n    This is a new feature that was added in Gel 7.0.\n\n.. note::\n\n    Change the version in the version selector dropdown to see how the rendered\n    example changes.\n\n**Section Example**\n\n.. code-block::\n\n    Source deletion\n    ^^^^^^^^^^^^^^^\n\n    .. versionadded:: 7.0\n\n    Source deletion policies determine what action should be taken when the\n    *source* of a given link is deleted. They are declared with the ``on source\n    delete`` clause.\n    ...\n\n**Rendered**\n\nSee :ref:`the \"Source deletion\" section of the \"Links\" documentation\n<ref_datamodel_links_source_deletion>` for a rendered section example of ``..\nversionadded:: 2.0``.\n\n**Description Block Example**\n\n.. code-block::\n\n    .. eql:type:: cal::date_duration\n\n        A type for representing a span of time in days.\n\n**Rendered**\n\nSee :eql:type:`cal::date_duration` for a rendered description block example of\n``.. versionadded:: 2.0``.\n\n**Full-Page Example**\n\n.. code-block::\n\n    .. versionadded:: 2.0\n\n    .. _ref_datamodel_globals:\n\n    =======\n    Globals\n    =======\n    ...\n\n**Rendered**\n\nSee :ref:`the \"Globals\" documentation page <ref_datamodel_globals>` for a\nfull-page example of ``.. versionadded:: 2.0``.\n\nChanged in Version\n------------------\n\nUse the ``versionchanged`` directive to mark content related to a change in\nexisting functionality across |Gel| versions. Provide the applicable version\nas an argument by placing it just after the directive on the same line.\n\n**Example**\n\n.. lint-off\n\n.. code-block::\n\n    .. versionchanged:: 8.0\n\n        Starting with the upcoming Gel 8.0, access policy restrictions will\n        **not** apply to any access policy expression. This means that when\n        reasoning about access policies it is no longer necessary to take other\n        policies into account. Instead, all data is visible for the purpose of\n        *defining* an access policy.\n\n.. lint-on\n\n**Rendered**\n\n.. versionchanged:: 8.0\n\n    Starting with the upcoming Gel 8.0, access policy restrictions will\n    **not** apply to any access policy expression. This means that when\n    reasoning about access policies it is no longer necessary to take other\n    policies into account. Instead, all data is visible for the purpose of\n    *defining* an access policy.\n\n.. note::\n\n    Change the version in the version selector dropdown to see how the rendered\n    example changes.\n\nOther Useful Tricks\n===================\n\nEmbedding a YouTube Video\n-------------------------\n\nEmbed only videos from `the Gel YouTube channel\n<https://www.youtube.com/edgedb>`_\n\n.. code-block::\n\n    .. edb:youtube-embed:: OZ_UURzDkow\n"
  },
  {
    "path": "docs/resources/guides/contributing/index.rst",
    "content": ".. _ref_guide_contributing:\n\n============\nContributing\n============\n\n:edb-alt-title: Contributing to Gel\n\n|Gel| is an open-source project, and we welcome contributions from our\ncommunity. You can contribute by writing code or by helping us improve our\ndocumentation.\n\n.. toctree::\n    :maxdepth: 1\n\n    code\n    documentation\n\nTo make sure the project can continue to improve quickly, we have a few\nguidelines designed to make it easier for your contributions to make it into\nthe project. General guidelines are presented here. You will find guidelines\nrelevant only to code or documentation in those sections of the guide.\n\nThese are guidelines rather than hard rules. If you want to submit a pull\nrequest that strays from these, it might be a good idea to start a discussion\nabout it first. Otherwise, it's possible your pull request might not be merged.\n\nGeneral Guidelines\n==================\n\n- **Avoid making pull requests that do not have an associated Github Issue.**\n  This could be an already existing issue or one you create yourself when you\n  discover the problem. This will allow the team to help you scope your\n  solution, warn you of potential gotchas, or give you a heads-up on solutions\n  that are likely not feasible. It's a good idea to mention in the issue that\n  you'd like to contribute code to resolve the issue.  **If you're fixing\n  something trivial like a typo,** an associated issue isn't necessary.\n- **Write good commit messages.** The subject of your commit message — that's\n  the first line — should tell us *what* you did. The body of your message —\n  that's the rest of it — should tell us *why* you did it (unless that's\n  self-evident).\n\nThank You!\n==========\n\nThank you for contributing to Gel! We love our open source community and\nwant to foster a healthy contributor ecosystem. We're happy to have you as a\npart of it.\n\n"
  },
  {
    "path": "docs/resources/guides/datamigrations/index.rst",
    "content": ".. _ref_guide_data_migrations:\n\n================\nSwitching to Gel\n================\n\nPerhaps you like Gel so much you want to migrate an existing project to it.\nHere we'll show you some possible ways to import your data.\n\n.. toctree::\n    :maxdepth: 3\n\n    postgres\n"
  },
  {
    "path": "docs/resources/guides/datamigrations/postgres.rst",
    "content": ".. _ref_guide_data_migrations_postgres:\n\n========\nPostgres\n========\n\nIn this guide, we show how to move your data from Postgres to Gel. However,\nmost of the approaches covered here should be applicable to other SQL\ndatabases as well.\n\nAs an example we'll use an app that allowed users to chat. The main features\nof this app revolve around posting and responding to messages. Once the data\nis moved, it is then possible to use :ref:`EdgeQL <ref_edgeql>` instead of SQL\nto fetch the desired data in the app's API calls. Here we'll mainly focus on\nthe data structures used for that, how to reflect them into Gel, and how to\nscript moving the data across to the Gel database.\n\n\nSchema modeling\n---------------\n\nLet's start from an overview of the SQL tables we have:\n\n.. code-block::\n\n    social=> \\d\n                  List of relations\n     Schema |       Name       |   Type   | Owner\n    --------+------------------+----------+-------\n     public | badges           | table    | myapp\n     public | bookmarks        | table    | myapp\n     public | bookmarks_id_seq | sequence | myapp\n     public | posts            | table    | myapp\n     public | posts_id_seq     | sequence | myapp\n     public | statuses         | table    | myapp\n     public | statuses_id_seq  | sequence | myapp\n     public | threads          | table    | myapp\n     public | threads_id_seq   | sequence | myapp\n     public | users            | table    | myapp\n     public | users_id_seq     | sequence | myapp\n    (11 rows)\n\nThe ``users`` and ``posts`` tables store the bulk of the content, whereas the\n``badges``, ``bookmarks``, ``statuses``, and ``threads`` tables store some\nmetadata that helps us tie everything together. When importing data, we want\nto start with tables that are standalone and don't reference anything else and\nthen move on to more complex data. In this case, ``badges`` and ``statuses``\nare such tables, so let's look at them:\n\n.. lint-off\n\n.. code-block::\n\n    social=> \\d badges\n                    Table \"public.badges\"\n       Column    | Type | Collation | Nullable | Default\n    -------------+------+-----------+----------+---------\n     name        | text |           | not null |\n     description | text |           | not null |\n    Indexes:\n        \"badges_pkey\" PRIMARY KEY, btree (name)\n    Referenced by:\n        TABLE \"users\" CONSTRAINT \"users_badge_name_fkey\" FOREIGN KEY (badge_name) REFERENCES badges(name)\n\n    social=> \\d statuses\n                                Table \"public.statuses\"\n     Column |  Type   | Collation | Nullable |               Default\n    --------+---------+-----------+----------+--------------------------------------\n     id     | integer |           | not null | nextval('statuses_id_seq'::regclass)\n     title  | text    |           | not null |\n    Indexes:\n        \"statuses_pkey\" PRIMARY KEY, btree (id)\n        \"statuses_title_key\" UNIQUE CONSTRAINT, btree (title)\n    Referenced by:\n        TABLE \"users\" CONSTRAINT \"users_status_id_fkey\" FOREIGN KEY (status_id) REFERENCES statuses(id)\n\n.. lint-on\n\nThe ``badges`` table uses the ``name`` of the badge as a primary key as\nopposed to a separate ``id``. In order to reflect that properly in Gel, we\nwould have to add an :eql:constraint:`exclusive` constraint to this property.\nMeanwhile ``not null`` makes the property ``required``, leaving us with a type\nlike this:\n\n.. code-block:: sql\n\n    type Badge {\n        required name: str {\n            constraint exclusive;\n        }\n        required description: str;\n    }\n\nThe ``statuses`` table has a dedicated ``id`` column in addition to ``title``.\nHowever, the automatic ``id`` in Gel is a :eql:type:`uuid`, whereas in our\noriginal dataset it is an ``integer``. Let's assume that for this table we\nnever actually use the ``id`` in our code, relying instead on the fact that\n``title`` is ``UNIQUE`` and serves as a much more descriptive identifier. We\ncan use the unique title to correctly map the data during our migration\nwithout the need to also copy the old ``id``. This leaves us with the\nfollowing type:\n\n.. code-block:: sql\n\n    type Status {\n        required title: str {\n            constraint exclusive;\n        }\n    }\n\nNext, we can look at the ``users`` table:\n\n.. lint-off\n\n.. code-block::\n\n    social=> \\d users\n                                     Table \"public.users\"\n         Column      |  Type   | Collation | Nullable |              Default\n    -----------------+---------+-----------+----------+-----------------------------------\n     id              | integer |           | not null | nextval('users_id_seq'::regclass)\n     name            | text    |           | not null |\n     email           | text    |           | not null |\n     password        | text    |           | not null |\n     client_settings | jsonb   |           |          |\n     badge_name      | text    |           |          |\n     status_id       | integer |           |          |\n    Indexes:\n        \"users_pkey\" PRIMARY KEY, btree (id)\n        \"users_email_key\" UNIQUE CONSTRAINT, btree (email)\n        \"users_name_key\" UNIQUE CONSTRAINT, btree (name)\n    Foreign-key constraints:\n        \"users_badge_name_fkey\" FOREIGN KEY (badge_name) REFERENCES badges(name)\n        \"users_status_id_fkey\" FOREIGN KEY (status_id) REFERENCES statuses(id)\n    Referenced by:\n        TABLE \"bookmarks\" CONSTRAINT \"bookmarks_user_id_fkey\" FOREIGN KEY (user_id) REFERENCES users(id)\n        TABLE \"posts\" CONSTRAINT \"posts_user_id_fkey\" FOREIGN KEY (user_id) REFERENCES users(id)\n\n.. lint-on\n\nThe ``users`` table, like ``statuses``, has an ``id`` column, which is not a\n:eql:type:`uuid`. Instead of omitting the ``id`` data, we'll record it as\n``app_id`` in Gel to facilitate the transition. We may still want to\neventually drop it in favor of the built-in ``id`` from Gel, but we need it\nfor now. Incidentally, even if the ``id`` was specified as a ``uuid`` value\nthe recommended process is to record it as ``app_id`` as opposed to try and\nreplicate it as the main object ``id``. It is, however, also possible to bring\nit over as the main ``id`` by adjusting certain :ref:`client connection\nsettings <ref_std_cfg_client_connections>`. The column ``client_settings``\nwould become a :eql:type:`json` property. The columns ``badge_name`` and\n``status_id`` reference ``badges`` and ``statuses`` respectively and will\nbecome *links* in Gel instead of *properties*, even though a property would\nmore closely mirror how they are stored in Postgres:\n\n.. code-block:: sql\n\n    type User {\n        required app_id: int32 {\n            # It was unique originally, so this should be preserved.\n            constraint exclusive;\n        }\n        required name: str {\n            constraint exclusive;\n        }\n        required email: str {\n            constraint exclusive;\n        }\n        required password: str;\n        client_settings: json;\n\n        # Both badge and status are optional.\n        badge: Badge;\n        status: Status;\n    }\n\nThe next table to consider is ``threads``, which provides a way to group posts\nby referring to it:\n\n.. lint-off\n\n.. code-block::\n\n    social=> \\d threads\n                                Table \"public.threads\"\n     Column |  Type   | Collation | Nullable |               Default\n    --------+---------+-----------+----------+-------------------------------------\n     id     | integer |           | not null | nextval('threads_id_seq'::regclass)\n     title  | text    |           |          |\n    Indexes:\n        \"threads_pkey\" PRIMARY KEY, btree (id)\n    Referenced by:\n        TABLE \"posts\" CONSTRAINT \"posts_thread_id_fkey\" FOREIGN KEY (thread_id) REFERENCES threads(id)\n\n.. lint-on\n\nThis table has very simple structure that we've seen before, so we can model\nit similarly to our ``Status`` type in the new database. However, just like we\ndid for the ``users`` table, we may want to preserve the original ``id`` as\n``app_id``:\n\n.. code-block:: sql\n\n    type Thread {\n        required app_id: int32 {\n            constraint exclusive;\n        }\n        title: str;\n    }\n\nThen we look at the ``posts`` table:\n\n.. lint-off\n\n.. code-block::\n\n    social=> \\d posts\n                                            Table \"public.posts\"\n        Column     |           Type           | Collation | Nullable |              Default\n    ---------------+--------------------------+-----------+----------+-----------------------------------\n     id            | integer                  |           | not null | nextval('posts_id_seq'::regclass)\n     body          | text                     |           | not null |\n     creation_time | timestamp with time zone |           | not null |\n     edited_time   | timestamp with time zone |           |          |\n     user_id       | integer                  |           | not null |\n     thread_id     | integer                  |           |          |\n     reply_to_id   | integer                  |           |          |\n    Indexes:\n        \"posts_pkey\" PRIMARY KEY, btree (id)\n    Foreign-key constraints:\n        \"posts_reply_to_id_fkey\" FOREIGN KEY (reply_to_id) REFERENCES posts(id)\n        \"posts_thread_id_fkey\" FOREIGN KEY (thread_id) REFERENCES threads(id)\n        \"posts_user_id_fkey\" FOREIGN KEY (user_id) REFERENCES users(id)\n    Referenced by:\n        TABLE \"bookmarks\" CONSTRAINT \"bookmarks_post_id_fkey\" FOREIGN KEY (post_id) REFERENCES posts(id)\n        TABLE \"posts\" CONSTRAINT \"posts_reply_to_id_fkey\" FOREIGN KEY (reply_to_id) REFERENCES posts(id)\n\n.. lint-on\n\nThe ``posts`` table also has an ``id`` that we want to keep around, at least\nduring the transition. We have a couple of columns using a ``timestamp with\ntime zone`` value, so they'll become :eql:type:`datetime` properties in\n|Gel|. The ``user_id``, ``thread_id``, and ``reply_to_id`` columns will\nbecome *links* to ``User``, ``Thread``, and ``Post`` respectively:\n\n.. code-block:: sql\n\n    type Post {\n        required app_id: int32 {\n            constraint exclusive;\n        }\n        required body: str {\n            constraint exclusive;\n        }\n        required creation_time: datetime {\n            # We might as well provide a default here so we don't have\n            # to pass it all the time when making a new post.\n            default := datetime_current();\n        }\n        edited_time: datetime;\n\n        required user: User;\n        thread: Thread;\n        reply_to: Post;\n    }\n\nFinally, we get to ``bookmarks``, which refers to both ``users`` and\n``posts``:\n\n.. lint-off\n\n.. code-block::\n\n    social=> \\d bookmarks\n                                 Table \"public.bookmarks\"\n     Column  |  Type   | Collation | Nullable |                Default\n    ---------+---------+-----------+----------+---------------------------------------\n     id      | integer |           | not null | nextval('bookmarks_id_seq'::regclass)\n     user_id | integer |           | not null |\n     post_id | integer |           | not null |\n     note    | text    |           |          |\n    Indexes:\n        \"bookmarks_pkey\" PRIMARY KEY, btree (id)\n    Foreign-key constraints:\n        \"bookmarks_post_id_fkey\" FOREIGN KEY (post_id) REFERENCES posts(id)\n        \"bookmarks_user_id_fkey\" FOREIGN KEY (user_id) REFERENCES users(id)\n\n.. lint-on\n\nThis is expressing a many-to-many relationship between ``users`` and\n``posts``, which we can model as a *multi link*. We can then declare the\n``note`` as a *link property*. Since we're likely to want to fetch bookmarks\ngiven a particular user, it makes sense to place the link on the ``User``\ntype. In the end we end up with a schema that looks something like this:\n\n.. code-block:: sql\n\n    type Badge {\n        required name: str {\n            constraint exclusive;\n        }\n        required description: str;\n    }\n\n    type Status {\n        required title: str {\n            constraint exclusive;\n        }\n    }\n\n    type User {\n        required app_id: int32 {\n            constraint exclusive;\n        }\n        required name: str {\n            constraint exclusive;\n        }\n        required email: str {\n            constraint exclusive;\n        }\n        required password: str;\n        client_settings: json;\n\n        badge: Badge;\n        status: Status;\n\n        # Multi link to the Post objects\n        multi bookmark: Post {\n            note: str;\n        }\n    }\n\n    type Thread {\n        required app_id: int32 {\n            constraint exclusive;\n        }\n        title: str;\n\n        # Let's add a computed link back to posts.\n        posts := .<thread[is Post];\n    }\n\n    type Post {\n        required app_id: int32 {\n            constraint exclusive;\n        }\n        required body: str {\n            constraint exclusive;\n        }\n        required creation_time: datetime {\n            default := datetime_current();\n        }\n        edited_time: datetime;\n\n        required user: User;\n        thread: Thread;\n        reply_to: Post;\n    }\n\n\nCopying the data\n----------------\n\nNow that we have a schema, we can use :ref:`ref_cli_gel_project_init` to\nset up our new Gel database. A new schema migration is added via\n:ref:`ref_cli_gel_migration_create` and then :ref:`gel migrate\n<ref_cli_gel_migration_apply>` applies the schema changes to the database.\nAfter the schema migration, we'll still need to copy the existing data from\nPostgres. JSON is a pretty good intermediate format for this operation. Gel\ncan cast data from :eql:type:`json` to all of the built-in scalar types, so we\nshould be able to use a JSON dump with minimal additional processing when\nimporting all the data.\n\nWe will dump ``badges`` and ``statuses`` first:\n\n.. lint-off\n\n.. code-block::\n\n    social=> SELECT ROW_TO_JSON(t) FROM badges AS t;\n                                    row_to_json\n    ---------------------------------------------------------------------------\n     {\"name\":\"admin\",\"description\":\"Superuser who can do anything\"}\n     {\"name\":\"moderator\",\"description\":\"User who can edit other user's posts\"}\n    (2 rows)\n\n    social=> SELECT ROW_TO_JSON(t) FROM statuses AS t;\n            row_to_json\n    ----------------------------\n     {\"id\":1,\"title\":\"happy\"}\n     {\"id\":2,\"title\":\"sad\"}\n     {\"id\":3,\"title\":\"excited\"}\n     {\"id\":4,\"title\":\"mad\"}\n    (4 rows)\n\n.. lint-on\n\nThese tables can be dumped directly to a file using a ``COPY ... TO\n<filename>`` command. We can then read the files and use a simple loop to\nimport the data into Gel.\n\n.. note::\n\n    When Postgres dumps the JSON data as text files there will be a known\n    gotcha causing all the backslashes used to escape characters inside JSON\n    string values to be doubled. This is because the *text* format causes\n    backslashes themselves to be escaped. This needs to be accounted for when\n    reading the resulting files. Accomplishing this through the Python client\n    library would look like the following:\n\n.. code-block:: python\n\n    for line in open('badges.json'):\n        client.query('''\n            with data := to_json(<str>$line)\n            insert Badge {\n                name := <str>data['name'],\n                description := <str>data['description'],\n            }\n        ''',\n        line=line.replace(r'\\\\', '\\\\'))\n\n    for line in open('statuses.json'):\n        client.query('''\n            with data := to_json(<str>$line)\n            insert Status {\n                title := <str>data['title'],\n            }\n        ''',\n        line=line.replace(r'\\\\', '\\\\'))\n\nThe ``threads`` table can likewise be dumped directly as JSON with the only\nminor difference being that we want to change the ``id`` to ``app_id`` when we\nmove the data to Gel:\n\n.. code-block:: python\n\n    for line in open('threads.json'):\n        client.query('''\n            with data := to_json(<str>$line)\n            insert Thread {\n                app_id := <int32>data['id'],\n                title := <str>data['title'],\n            }\n        ''',\n        line=line.replace(r'\\\\', '\\\\'))\n\nTo copy the ``users`` table, we may want to use a more complex ``SELECT`` that\njoins the statuses so we can match them by their unique names:\n\n.. code-block::\n\n    SELECT ROW_TO_JSON(t)\n    FROM (\n      SELECT\n        users.id, name, email, password, client_settings,\n        badge_name, statuses.title\n      FROM users\n      LEFT JOIN statuses ON status_id = statuses.id\n    ) AS t;\n\nWhen we run our import script, we can convert the ``badge_name`` and\n``status`` JSON values into the corresponding objects by using sub-queries:\n\n.. code-block:: python\n\n    for line in open('users.json'):\n        client.query('''\n            with data := to_json(<str>$line)\n            insert User {\n                app_id := <int32>data['id'],\n                name := <str>data['name'],\n                email := <str>data['email'],\n                password := <str>data['password'],\n                client_settings := data['client_settings'],\n                badge := (\n                    select Badge filter .name = <str>data['badge_name']\n                ),\n                status := (\n                    select Status filter .title = <str>data['status']\n                ),\n            }\n        ''',\n        line=line.replace(r'\\\\', '\\\\'))\n\nThe ``posts`` table can be dumped as JSON directly, but we'll need to write\nsub-queries in the import script to correctly link ``Post`` objects. In order\nto make this simpler, we can order the original data by ``creation_time`` so\nwe know any ``Post`` object that is referenced by the ``reply_to_id`` has\nalready been re-created in Gel.\n\n.. code-block:: python\n\n    for line in open('posts.json'):\n        client.query('''\n            with data := to_json(<str>$line)\n            insert Post {\n                app_id := <int32>data['id'],\n                body := <str>data['body'],\n                creation_time := <datetime>data['creation_time'],\n                edited_time := <datetime>data['edited_time'],\n                user := (\n                    select User filter .app_id = <int32>data['user_id']\n                ),\n                thread := (\n                    select Thread filter .app_id = <int32>data['thread_id']\n                ),\n                reply_to := (\n                    select detached Post\n                    filter .app_id = <int32>data['reply_to_id']\n                ),\n            }\n        ''',\n        line=line.replace(r'\\\\', '\\\\'))\n\nFinally, we can deal with the bookmarks since we've imported both the users\nand the posts. The ``bookmarks`` table can be dumped as JSON directly, and\nthen we can write appropriate ``update`` query to add this data to Gel:\n\n.. code-block:: python\n\n    for line in open('bookmarks.json'):\n        client.query('''\n            with data := to_json(<str>$line)\n            update User\n            filter .app_id = <int32>data['user_id']\n            set {\n                bookmark += (\n                    select Post {\n                        @note := <str>data['note']\n                    }\n                    filter .app_id = <int32>data['post_id']\n                ),\n            }\n        ''',\n        line=line)\n\nWe use ``+=`` in our ``update`` query to add data incrementally. This way we\ndon't need to further organize the bookmarks when importing. This approach\nalso mimics how the bookmarks might be created in the app going forward.\n\nAfter all the import scripts, we end up with data that looks something like\nthis:\n\n.. lint-off\n\n.. code-block:: edgeql-repl\n\n    local:db> select User {\n    .........   name,\n    .........   email,\n    .........   status: {title},\n    .........   badge: {name},\n    .........   bookmark: {\n    .........     @note,\n    .........     body,\n    .........     user: {name}\n    .........   },\n    ......... } filter .name = 'Cameron';\n    {\n      default::User {\n        name: 'Cameron',\n        email: 'cameron@example.com',\n        status: {},\n        badge: default::Badge {name: 'admin'},\n        bookmark: {\n          default::Post {\n            body: 'Hey everyone! How\\'s your day going?',\n            user: default::User {name: 'Alice'},\n            @note: 'rendering glitch',\n          },\n          default::Post {\n            body: 'Funny you ask, Alice. I actually work at Gel!',\n            user: default::User {name: 'Dana'},\n            @note: 'follow-up',\n          },\n          default::Post {\n            body: 'Pineapple on pizza? No way! It\\'s a crime against taste buds.',\n            user: default::User {name: 'Billie'},\n            @note: {},\n          },\n        },\n      },\n    }\n\n.. lint-on\n"
  },
  {
    "path": "docs/resources/guides/index.rst",
    "content": ".. _ref_guides:\n\n======\nGuides\n======\n\nThese guides contain tutorials introducing Gel to newcomers and\nhow-tos providing more experienced users with examples and advice for\ntackling some common tasks.\n\nIf you are new to Gel check out our :ref:`Quickstart <ref_quickstart>`\nguide!\n\n\n.. toctree::\n    :maxdepth: 1\n\n    datamigrations/index\n    tutorials/index\n    migrations/index\n    contributing/index\n"
  },
  {
    "path": "docs/resources/guides/migrations/guide.rst",
    "content": ".. _ref_migration_guide:\n\n======\nBasics\n======\n\n:edb-alt-title: Schema migration basics\n\nEdgeQL is a strongly-typed language, which means that it moves checks\nand verification of your code to compile time as much as possible\ninstead of performing them at run time. Gel's view is that a schema\nshould allow you to set types, constraints, expressions, and more so that\nyou can confidently know what sort of behavior to expect from your data.\nLaying a type-safe foundation means a bit more thinking up front, but saves\nyou all kinds of headaches down the road.\n\nIt's unlikely though that you'll define your schema perfectly on your first\ntry, or that you'll build an application that never needs its schema revised.\nWhen you *do* eventually need to make a change, you'll need to migrate\nyour schema from its current state to a new state.\n\nThe basics of creating a project, modifying its schema, and migrating\nit in Gel are pretty easy:\n\n- Type :gelcmd:`project init` to start a project,\n- Open the newly created empty schema at :dotgel:`dbschema/default` and add\n  a simple type like  ``SomeType { name: str; }`` inside the empty ``module``\n- Run :gelcmd:`migration create`, type ``y`` to confirm the change,\n  then run :gelcmd:`migrate`, and you are done! You can now\n  ``insert SomeType;`` in your database to your heart's content.\n\n.. note::\n\n   If you ever feel like outright removing and creating an instance anew\n   during this migration guide, you can use the command\n   :gelcmd:`instance destroy -I <instancename> --force`. And if you want to\n   remove all existing migrations as well, you can manually delete them inside\n   your ``/migrations`` folder (otherwise, the CLI will try to apply the\n   migrations again when you recreate your instance with\n   :gelcmd:`migration create`). Once that is done, you will have a blank\n   slate on which to start over again.\n\nBut many Gel users have needs that go beyond these basics. In addition,\nschema migrations are pretty interesting and teach you a lot about\nwhat Gel does behind the scenes. This guide will turn you from\na casual migration user into one with a lot more tools at hand, along\nwith a deeper understanding of the internals of Gel at the same\ntime.\n\n|Gel's| built-in tools are what make schema migrations easy, and\nthe way they work is through a pretty interesting interaction between\nGel's SDL (Schema Definition Language) and DDL (Data Definition\nLanguage). The first thing to understand about migrations is the difference\nbetween SDL and DDL, and how they are used.\n\nSDL: For humans\n===============\n\nSDL, not DDL, is the primary way for you to create and migrate your\nschema in Gel. You don't need to work with DDL to use Gel any\nmore than you need to know how to change a tire to drive a car.\n\nSDL is built for humans to read, which is why it is said to be *declarative*.\nThe 'clar' inside *declarative* is the same word as *clear*, and this\nis exactly what declarative means: making it *clear* what you want\nthe final result to be. An example of a declarative instruction in\nreal life would be telling a friend to show up at your house at 6416\nRiverside Way. You've declared what the final result should be, but\nit's up to your friend to find how to achieve it.\n\nNow let's look at some real SDL and think about its role in Gel.\nHere is a simple example of a schema:\n\n.. code-block:: sdl\n\n   module default {\n     type User {\n       name: str;\n     }\n   }\n\nIf you have Gel installed and want to follow along, run\n:gelcmd:` project init` and copy the above schema into your :dotgel:`default`\nfile inside the ``/dbschema`` folder it creates. Then save the file.\n\n.. note::\n\n    While schema is usually contained inside the :dotgel:`default` file,\n    you can divide a schema over multiple files if you like. Gel will\n    combine all |.gel| files inside the ``/dbschema`` folder into a\n    single schema.\n\nType |gelcmd| to start the Gel REPL, and, into the REPL,  type\n``describe schema as sdl``. The output will be ``{'module default{};'}``\n— nothing more than the empty ``default`` module. What happened?\nOur ``type User`` is nowhere to be found.\n\nThis is the first thing to know about SDL. Like an address to a\nperson's house, it doesn't *do* anything on its own. With SDL you are\ndeclaring what you want the final result to be: a schema containing a single\ntype called ``User``, with a property of type ``str`` called ``name``.\n\nIn order for a migration to happen, the Gel server needs to receive\nDDL statements telling it what changes to make, in the exact same\nway that you give instructions like \"turn right at the next intersection\"\nto your friend who is trying to find your house. In Gel's case,\nthese commands will start with words like ``create`` and ``drop``\nand ``alter`` to tell it what changes to make. Gel accomplishes\nthese changes by knowing how to turn your declarative SDL into a schema\nmigration file that contains the DDL statements to accomplish the\nnecessary changes.\n\nDDL: For computers (mostly)\n===========================\n\nTo see what a schema migration file looks like, type :gelcmd:`migration\ncreate`. Now look inside your ``/dbschema/migrations`` folder. You should\nsee a file called ``00001.edgeql`` with the following, our first view into\nwhat DDL looks like.\n\n.. code-block::\n\n    CREATE TYPE default::User {\n        CREATE PROPERTY name: std::str;\n    };\n\nThe declarative schema has now been turned into *imperative* DDL (imperative\nmeaning \"giving orders\"), specifically commands telling the database how\nto get from the current state to the desired state. Note that, in\ncontrast to SDL, this code says nothing about the current schema or\nits final state. This command would work with the schema of any database\nat all that doesn't already have a type called ``User``.\n\nLet's try one more small migration, in which we decide that we don't\nwant the ``name`` property anymore. Once again, we are declaring the\nfinal state: a ``User`` type with nothing inside. Update your :dotgel:`default`\nto look like this:\n\n.. code-block:: sdl\n\n    module default {\n      type User;\n    }\n\nAs before, typing :gelcmd:`migration create` will create a DDL statement to\nchange the schema from the current state to the one we have declared. This\ntime we aren't starting from a blank schema, so the stakes are a bit higher.\nAfter all, dropping a property from a type will also drop all existing data\nunder that property name. Thus, the schema planner will first ask a question\nto confirm the change with us. We will learn a lot more about working with\nthese questions very soon, but in the meantime just press ``y`` to confirm\nthe change.\n\n.. code-block::\n\n    db> did you drop property 'name' of object type 'default::User'?\n    [y,n,l,c,b,s,q,?]\n    > y\n\nYour ``/dbschema/migrations`` folder will now have a new file that contains\nthe following:\n\n.. code-block::\n\n  ALTER TYPE default::User {\n      DROP PROPERTY name;\n  };\n\nThe difference between SDL and DDL is even clearer this time. The DDL\nstatement alone doesn't give us any indication what the schema looks like;\nall anyone could know from this migration script alone is that there is\na ``User`` type inside a module called ``default`` that *doesn't* have\na property called ``name`` anymore.\n\n.. note::\n\n    Gel commands inside the REPL use a backslash instead of the |gelcmd|\n    command, so you can migrate your schema inside the REPL by typing\n    ``\\migration create`` , followed by ``\\migrate``. Not only are the comands\n    shorter, but they also execute faster. This is because the database client\n    is already connected to your database when you're inside the REPL, which\n    is not the case when creating and applying the migration via the CLI.\n\nOrder matters in DDL\n--------------------\n\nThe analogy of a person driving along the road tells us another detail\nabout DDL: order matters. If you need to first drive two blocks forward\nand then turn to the right to reach a destination, that doesn't mean\nthat you can switch the order around; you can't turn right and *then*\ndrive two blocks forward and expect to reach the same spot.\n\nSimilarly, if you want add a property to an existing type and the\nproperty's type is a new scalar type, the database will need to create\nthe new scalar type first.\n\nLet's take a look at this by first getting Gel to describe our\nschema to us. Typing ``describe schema;`` inside the REPL will display\nthe following DDL statements:\n\n.. code-block::\n\n  {\n    'create module default if not exists;\n     create type default::User;',\n  }\n\nThankfully, the DDL statements here are simply the minimum needed\nto produce our current schema, not a collection of all the statements\nin all of our previous migrations. So while this is a collection of\nDDL statements, the DDL produced by ``describe schema`` is just about\nas readable as the SDL in your schema.\n\nIf we type ``describe schema as sdl;`` then we'll see the SDL version\nof the DDL above: a declarative schema as opposed to statements.\n\n.. code-block:: sdl\n\n  module default {\n    type User;\n  };\n\nNow let's add the new scalar type mentioned above and give it to the\n``User`` type. Our schema will now look like this:\n\n.. code-block:: sdl-diff\n\n      module default {\n        type User {\n    +     name: Name;\n        }\n    +   scalar type Name extending str;\n      }\n\nNote that we are able to define the custom scalar type ``Name`` after we\ndefine the ``User`` type even though we use ``Name`` within that object\nbecause order doesn't matter in SDL. Let's migrate to this new schema\nand then use ``describe schema;`` again. You will see the following\nstatements:\n\n.. code-block::\n\n    create module default if not exists;\n    create scalar type default::Name extending std::str;\n    create type default::User {\n        create property name: default::Name;\n    };\n\nThe output shows us that the database has gone in the necessary order\nto make the schema: first it creates the module, then a scalar type\ncalled ``Name``, and finally the ``User`` type which is now able to\nhave a property of type ``Name``.\n\nThe output with ``describe schema as sdl;`` is also somewhat similar.\nIt's SDL, but the order matches that of the DDL statements.\n\n.. code-block:: sdl\n\n    module default {\n        scalar type Name extending std::str;\n        type User {\n            property name: default::Name;\n        };\n    };\n\nAlthough the schema produced with ``describe schema as sdl;`` may not match\nthe schema you've written inside :dotgel:`default`, it will\nshow you the order in which statements were needed to reach this final\nschema.\n\nNon-interactive migrations\n--------------------------\n\nLet's move back to the most basic schema with a single type that\nhas no properties.\n\n.. code-block:: sdl\n\n    module default {\n      type User;\n    }\n\nCreating a migration with :gelcmd:`migration create` will result\nin two questions, one to confirm that we wanted to drop the ``name``\nproperty, and another to drop the ``Name`` type.\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you drop property 'name' of object type 'default::User'?\n    [y,n,l,c,b,s,q,?]\n    > y\n    did you drop scalar type 'default::Name'? [y,n,l,c,b,s,q,?]\n    > y\n\nThis didn't take very long, but you can imagine that it could get\nannoying if we had decided to drop ten or more types or properties\nand had to say yes to every change. In a case like this, we can use\na non-interactive migration. Let's give that a try.\n\nFirst go into your ``/dbschema/migrations`` folder and delete the\nmost recent ``.edgeql`` file that drops the property ``name`` and\nthe scalar type ``Name``. Don't worry - the migration hasn't been\napplied yet, so you won't confuse the database by deleting it at this\npoint. And now type :gelcmd:`migration create --non-interactive`.\n\nYou'll see the same file generated, except that this time there weren't\nany questions to answer. A non-interactive migration will work as\nlong as the database has a high degree of confidence about every change\nmade, and will fail otherwise.\n\nA non-interactive migration will fail if we make changes to our schema\nthat are ambiguous. Let's see if we can make a non-interactive migration\nfail by doing just that. Delete the most recent ``.edgeql`` migration\nfile again, and change the schema to the following that only differs by\na single letter. Can you spot the difference?\n\n.. code-block:: sdl\n\n    module default {\n      type User {\n        nam: Name;\n      }\n      scalar type Name extending str;\n    }\n\nThe only difference from the current schema is that we would like\nto change the property name ``name`` to ``nam``, but this time Gel isn't\nsure what change we wanted to make. Did we intend to:\n\n- Change ``name`` to ``nam`` and keep the existing data?\n- Drop ``name`` and create a new property called ``nam``?\n- Do something else?\n\nBecause of the ambiguity, this non-interactive migration will fail, but with\nsome pretty helpful output:\n\n.. code-block:: edgeql-repl\n\n    db> \\migration create --non-interactive\n    Gel intended to apply the following migration:\n        ALTER TYPE default::User {\n            ALTER PROPERTY name {\n                RENAME TO nam;\n            };\n        };\n    But confidence is 0.67, below minimum threshold of 0.99999\n    Error executing command: Gel is unable to make a decision.\n\n    Please run in interactive mode to confirm changes, or use\n    `--allow-unsafe`\n\nAs the output suggests, you can add ``--allow-unsafe`` to a non-interactive\nmigration if you truly want to push the suggestions through regardless\nof the migration tool's confidence, but it's more likely in this case\nthat you would like to interact with the CLI's questions to help it\nmake a decision. For example, if we had intended to drop the property\n``name`` and create a new property ``nam``, we would simply answer\n``n`` when it asks us if we intended to *rename* the property. It\nthen confirms that we are altering the ``User`` type, and finishes\nthe migration script.\n\n.. code-block:: edgeql-repl\n\n    db> \\migration create\n    did you rename property 'name' of object type 'default::User'\n    to 'nam'? [y,n,l,c,b,s,q,?]\n    > n\n    did you alter object type 'default::User'? [y,n,l,c,b,s,q,?]\n    > y\n\nAfterwards, you can go into the ``.edgeql`` file that was just created\nto confirm that these were the changes we wanted to make. It will\nlook like this:\n\n.. code-block::\n\n    CREATE MIGRATION m15hu2pbez5od7fe3shlxwcprbqhvctnfavadccjgjszboy26grgka\n        ONTO m17m6qjjhtslfkqojvjb4g2vqtzasv5mlbtrqbp6mhwlzv57p5f2uq\n    {\n      ALTER TYPE default::User {\n        CREATE PROPERTY nam: default::Name;\n        DROP PROPERTY name;\n      };\n    };\n\n.. note::\n\n    See the section on\n    :ref:`data migrations <ref_migration_guide_migrations_and_hashes>`\n    and migration hashes if you are curious about how migrations are named.\n\nThis migration will alter the ``User`` type by creating a new property and\ndropping the old one. If that is what we wanted, then we can now type\n``\\migrate`` in the REPL or :gelcmd:`migrate` at the command line to complete\nthe migration.\n\nQuestions from the CLI\n======================\n\nSo far we've only learned how to say \"yes\" or \"no\" to the CLI's questions\nwhen we migrate a schema, but quite a few other options are presented\nwhen the CLI asks us a question:\n\n.. code-block::\n\n    did you create object type 'default::PlayerCharacter'? [y,n,l,c,b,s,q,?]\n    > y\n\nThe choices ``y`` and ``n`` are obviously \"yes\" and \"no,\" and you can\nprobably guess that ``?`` will output help for the available response options,\nbut the others aren't so clear. Let's go over every option to make sure we\nunderstand them.\n\n``y`` (or ``yes``)\n------------------\n\nThis will accept the proposed change and move on to the next step.\nIf it's the last proposed change, the migration will now be created.\n\n``n`` (or ``no``)\n-----------------\n\nThis will reject the proposed change. At this point, the migration\ntool will try to suggest a different change if it can, but it won't\nalways be able to do so.\n\nWe can see this behavior with the same tiny schema change we made\nabove where we changed a property name from ``name`` to ``nam``. In\nthe output of that ``migration create``, we see the following:\n\n- The CLI first asks us if we renamed the property, to which we say \"no\".\n- It then tries to confirm that we have altered the ``User`` type.\n  We say \"no\" again.\n- The CLI then guesses that maybe we are dropping and creating the\n  whole ``User`` type instead. This time, we say \"yes.\"\n- It then asks us to confirm that we are creating a ``User`` type,\n  since we have decided to drop the existing one.\n\nIf we say \"no\" again to the final question, the CLI will throw its hands\nup and tell us that it doesn't know what we are trying to do because\nthere is no way left for it to migrate to the schema that we have\ntold it to move to.\n\nHere is what that would look like:\n\n.. code-block::\n\n    did you rename property 'name' of object type 'default::User'\n    to 'nam'?\n    [y,n,l,c,b,s,q,?]\n    > n\n    did you alter object type 'default::User'? [y,n,l,c,b,s,q,?]\n    > n\n    did you drop object type 'default::User'? [y,n,l,c,b,s,q,?]\n    > y\n    did you create object type 'default::User'? [y,n,l,c,b,s,q,?]\n    > n\n    Error executing command: Gel could not resolve migration with\n    the provided answers. Please retry with different answers.\n\n``l`` (or ``list``)\n-------------------\n\nThis is used to see (list) the actual DDL statements that are being proposed.\nWhen asked the question ``did you alter object type 'default::User'?``\nin the example above, we might be wondering exactly what changes will\nbe made here. How exactly does the database intend to alter the ``User``\ntype if we say \"yes?\" Simply pressing ``l`` will show it:\n\n.. code-block::\n\n    The following DDL statements will be applied:\n      ALTER TYPE default::User {\n          CREATE PROPERTY nam: std::str;\n          DROP PROPERTY name;\n      };\n\nThis shows us clear as day that saying \"yes\" will result in creating\na new property called ``nam`` and dropping the existing ``name`` property.\n\nSo when doubts dwell, press the letter \"l!\"\n\n``c`` (or ``confirmed``)\n------------------------\n\nThis simply shows the entire list of statements that have been confirmed.\nIn other words, this is the migration as it stands at this point.\n\n``b`` (or ``back``)\n-------------------\n\nThis will undo the last confirmation you agreed to and move you back\na step in the migration. If you haven't confirmed any statements yet,\na message will simply appear to let you know that there is nowhere\nfurther back to move to. So pressing ``b`` will never abort a migration.\n\nThe following two keys will stop the migration, but in different ways:\n\n``s`` (or ``stop``)\n-------------------\n\nThis is also known as a 'split'. Pressing ``s`` will complete the\nmigration at the current point. Any statements that you have applied\nwill be applied, but the schema will not yet match the schema in your\n|.gel| file(s). You can easily start another migration to complete\nthe remaining changes once you have applied the migration that was\njust created. This effectively splits the migration into two or more\nfiles.\n\n``q`` (or ``quit``)\n-------------------\n\nPressing ``q`` will simply quit without saving any of your progress.\n\n.. _ref_migration_guide_migrations_and_hashes:\n\nData migrations and migration hashes\n====================================\n\nSometimes you may want to initialize a database with some default\ndata, or add some data to a migration that you have just created before\nyou apply it.\n\n|Gel| assumes by default that a migration involves a change to your\nschema, so it won't create a migration for you if it doesn't see a\nschema change:\n\n.. code-block:: bash\n\n    $ gel migration create\n    No schema changes detected.\n\nSo how do you create a migration with only data? To do this, just\nadd ``--allow-empty`` to the command:\n\n.. code-block:: bash\n\n    $ gel migration create --allow-empty\n    Created myproject/dbschema/migrations/00002.edgeql,\n    id: m1xseswmheqzxutr55cu66ko4oracannpddujg7gkna2zsjpqm2g3a\n\nYou will now see an empty migration in ``dbschema/migrations`` in which you\ncan enter some queries. It will look something like this:\n\n.. code-block::\n\n    CREATE MIGRATION m1xseswmheqzxutr55cu66ko4oracannpddujg7gkna2zsjpqm2g3a\n        ONTO m1n5lfw7n74626cverbjwdhcafnhmbezjhwec2rbt46gh3ztoo7mqa\n    {\n    };\n\nLet's see what happens if we add some queries inside the braces. Assuming\na schema with a simple ``User`` type, we could then add a bunch of queries\nsuch as the following:\n\n.. code-block::\n\n    CREATE MIGRATION m1xseswmheqzxutr55cu66ko4oracannpddujg7gkna2zsjpqm2g3a\n        ONTO m1n5lfw7n74626cverbjwdhcafnhmbezjhwec2rbt46gh3ztoo7mqa\n    {\n        insert User { name := 'User 1'};\n        insert User { name := 'User 2'};\n        delete User filter .name = 'User 2';\n    };\n\nThe problem is, if you save that migration and run :gelcmd:`migrate`, the CLI\nwill complain that the migration hash doesn't match what it is supposed to be.\nHowever, it helpfully provides the reason: \"Migration names are computed from\nthe hash of the migration contents.\"\n\nFortunately, it also tells you exactly what the hash (the migration name)\nwill need to be:\n\n.. code-block::\n\n    Error executing command: could not read migrations in\n    myproject/dbschema/migrations:\n\n    could not read migration file myproject/dbschema/migrations/00002.edgeql:\n\n    Migration name should be:\n    m13g7j2tqu23yaffv6wkn2adp6hayp76su2qtg2lutdh3mmj5xyk6q, but\n    m1xseswmheqzxutr55cu66ko4oracannpddujg7gkna2zsjpqm2g3a found instead.\n\n\n    Migration names are computed from the hash of the migration contents.\n\n    To proceed you must fix the statement to read as:\n    CREATE MIGRATION m13g7j2tqu23yaffv6wkn2adp6hayp76su2qtg2lutdh3mmj5xyk6q\n    ONTO ...\n    Alternatively, revert the changes to the file.\n\nIf you change the statement to read in exactly the way the output suggests,\nthe migration will now work.\n\nThat's the manual way to do a data migration, but Gel also has an\n:gelcmd:`migration edit` command that will automate the process for you.\nUsing :gelcmd:`migration edit` will open up the most recent migration for\nyou to change, and update the migration hash when you close the window.\n\nAside from exclusive data migrations, you can also create a migration that\ncombines schema changes *and* data. This is even easier, since it doesn't even\nrequire appending ``--allow-empty`` to the command. Just do the following:\n\n1. Change your schema\n2. Type :gelcmd:`migration create` and respond to the CLI's questions\n3. Add your queries to the file (best done on the bottom after the\n   DDL statements have changed the schema) either manually or using\n   :gelcmd:`migration edit`\n4. Type :gelcmd:`migrate` to migrate the schema. If you have changed the\n   schema file manually, copy the suggested name into the migration hash\n   and type :gelcmd:`migrate` again.\n\nThe Gel tutorial is a good example of a database\nset up with both a schema migration and a data migration. Setting\nup a database with `schema changes in one file and default data in\na second file <tutorial_files_>`_ is a nice way to separate the two operations\nand maintain high readability at the same time.\n\nSquashing migrations\n====================\n\nUsers often end up making many changes to their schema because\nof how effortless it is to do. (And in the next section we will learn\nabout :gelcmd:`watch --migrate`, which is even more effortless!) This leads to\nan interesting side effect: lots of ``.edgeql`` files, many of which\nrepresent trials and approaches that don't end up making it to the\nfinal schema.\n\nOnce you are done, you might want to squash the migrations into a\nsingle file. This is especially nice if you need to frequently initialize\ndatabase instances using the same schema, because all migrations are\napplied when an instance starts up. You can imagine that the output\nwould be pretty long if you had dozens and dozens of migration files\nto work through:\n\n.. code-block::\n\n    Initializing Gel instance...\n    Applying migrations...\n    Applied m13brvdizqpva6icpcvmsc3fee2yt5j267uba6jugy6iugcbs2djkq\n    (00001.edgeql)\n    Applied m1aildofb3gvhv3jaa5vjlre4pe26locxevqok4semmlgqwu3xayaa\n    (00002.edgeql)\n    Applied m1ixxlsdgrlinfijnrbmxdicmpfav33snidudqi7fu4yfhg4nngoza\n    (00003.edgeql)\n    Applied m1tsi4amrdbcfjypu72duyckrlvvyb46r3wybd7qnbmem4rjvnbcla\n    (00004.edgeql)\n    ...and so on...\n    Project initialized.\n\nTo squash your migrations, just run :gelcmd:`migration create` with the\n``--squash`` option. Running this command will first display some helpful\ninfo to keep in mind before committing to the operation:\n\n.. code-block::\n\n    Current database revision is:\n    m16ixoukn7ulqdn7tp6lvx2754hviopanufv2lm6wf4x2borgc3g6a\n    While squashing migrations is non-destructive,\n    it may lead to manual work if done incorrectly.\n\n    Items to check before using --squash:\n    1. Ensure that `./dbschema` dir is comitted\n    2. Ensure that other users of the database have the revision\n    above or can create database from scratch.\n        To check a specific instance, run:\n        gel -I <name> migration log --from-db --newest-first --limit 1\n    1. Merge version control branches that contain schema changes\n    if possible.\n\n    Proceed? [y/n]\n\nPress ``y`` to squash all of your existing migrations into\na single file.\n\nFixups during a squash\n----------------------\n\nIf your schema doesn't match the schema in the database, Gel will\nprompt you to create a *fixup* file, which can be useful to, as the CLI\nsays, \"automate upgrading other instances to a squashed revision\".\nYou'll see fixups inside ``/dbschema/fixups``. Their file names\nare extremely long because they are simply two migration hashes joined\ntogether by a dash. This means a fixup that begins with\n\n.. code-block::\n\n    CREATE MIGRATION\n    m1v3vqmwif4ml3ucbzi555mjgm4myxs2husqemopo2sz2m7otr22ka\n    ONTO m16awk2tzhtbupjrzoc4fikgw5okxpfnaazupb6rxudxwin2qfgy5q\n\nwill have a file name a full 116 characters in length.\n\nThe CLI output when using squash along with a fixup is pretty informative\non its own, so let's just walk through the output as you'll see it\nin practice. First we'll begin with this schema:\n\n.. code-block:: sdl\n\n  type User {\n    name: str;\n  }\n\nThen remove ``name: str;`` from the ``User`` type, migrate, put it back\nagain, and migrate. You can repeat this as many times as you like.\nOne quick way to \"remove\" items from your schema that you might want\nto restore later is to simply use a ``#`` to comment out the entire line:\n\n.. code-block:: sdl\n\n  type User {\n   # name: str;\n  }\n\nAfter a few of these simple migrations, you'll now have multiple files\nin your ``/migrations`` folder — none of which were all that useful — and\nmay be in the mood to squash them into one.\n\nNext, change to this schema **without migrating it**:\n\n.. code-block:: sdl\n\n  type User {\n    name: str;\n    nickname: str;\n  }\n\nNow run :gelcmd:`migration create --squash`. The output is first\nthe same as with our previous squash:\n\n.. code-block:: bash\n\n    $ gel migration create --squash\n    Current database revision:\n    m16awk2tzhtbupjrzoc4fikgw5okxpfnaazupb6rxudxwin2qfgy5q\n    While squashing migrations is non-destructive,\n    it may lead to manual work if done incorrectly.\n\n    Items to check before using --squash:\n    1. Ensure that `./dbschema` dir is comitted\n    2. Ensure that other users of the database have the revision\n    above or can create database from scratch.\n        To check a specific instance, run:\n        gel -I <name> migration log --from-db --newest-first --limit 1\n    3. Merge version control branches that contain schema changes\n    if possible.\n\n    Proceed? [y/n]\n    > y\n\nBut after typing ``y``, the CLI will notice that the existing schema\ndiffers from what you have and offers to make a fixup file:\n\n.. code-block::\n\n    Your schema differs from the last revision.\n    A fixup file can be created\n    to automate upgrading other instances to a squashed revision.\n    This starts the usual migration creation process.\n\n    Feel free to skip this step if you don't have\n    other instances to migrate\n\n    Create a fixup file? [y/n]\n    > y\n\nYou will then see the the same questions that would otherwise show up in\na standard migration:\n\n.. code-block::\n\n    db> did you create property 'nickname' of object type 'default::User'?\n    [y,n,l,c,b,s,q,?]\n    > y\n    Squash is complete.\n\nFinally, the CLI will give some advice on recommended commands when\nworking with git after doing a squash with a fixup.\n\n.. code-block::\n\n    Remember to commit the `dbschema` directory including deleted files\n    and `fixups` subdirectory. Recommended command:\n        git add dbschema\n\n    The normal migration process will update your migration history:\n        gel migrate\n\nWe'll take its suggestion to apply the migration:\n\n.. code-block:: bash\n\n    $ gel migrate\n\n    Applied m1v3vqmwif4ml3ucbzi555mjgm4myxs2husqemopo2sz2m7otr22ka\n    (m16awk2tzhtbupjrzoc4fikgw5okxpfnaazupb6rxudxwin2qfgy5q-\n    m1oih6aevfcftysukvofwuth2bsuj5aahkdnpabscry7p7ljkgbxma.edgeql)\n\n\n.. note::\n\n    Squashing is limited to schema changes, so queries inside\n    data migrations will be discarded during a squash.\n\nGel Watch\n=========\n\nAnother option when quickly iterating over schema changes is :gelcmd:`watch --migrate`.\nThis will create a long-running process that keeps track of every time you\nsave a |.gel| file inside your ``/migrations`` folder, letting you know\nif your changes have successfully compiled or not. The :gelcmd:`watch --migrate`\ncommand itself will show the following input when the process starts up:\n\n.. code-block::\n\n    Connecting to Gel instance 'anything' at localhost:10700...\n    Hint: --migrate will apply any changes from your schema files to the database.\n    When ready to commit your changes, use:\n    1) `gel migration create` to write those changes to a migration file,\n    2) `gel migrate --dev-mode` to replace all synced changes with the migration.\n\n    Monitoring /home/instancename for changes in:\n\n      --migrate: gel migration apply --dev-mode\n\n\nUnseen to the user, :gelcmd:`watch --migrate` will begin creating individual\nmigration scripts for every time you save a change to one of your files. These\nare stored as separate \"dev mode\" migrations, which are sort of like\npreliminary migrations that haven't been turned into a standalone migration\nscript yet.\n\nWe can test this out by starting with this schema:\n\n.. code-block:: sdl\n\n    module default {\n      type User {\n        name: str;\n      }\n    }\n\nNow let's add a single property. Keep an eye on your terminal output and\nhit after making a change to the following schema:\n\n.. code-block:: sdl\n\n    module default {\n      type User {\n        name: str;\n        number: int32;\n      }\n    }\n\nYou will see a quick \"calculating diff\" show up as :gelcmd:`watch --migrate` checks\nto see that the change we made was a valid one. As the change we made was\nto a valid schema, the \"calculating diff\" message will disappear pretty\nquickly.\n\nHowever, if the schema file you save is incorrect, the output will be a lot\nmore verbose. Let's add some incorrect syntax to the existing schema:\n\n.. code-block:: sdl\n\n    module default {\n      type User {\n        name: str;\n        number: int32;\n        wrong_property: i32; # Should say int32, not i32\n      }\n    }\n\nOnce you hit save, :gelcmd:`watch --migrate` will suddenly pipe up and inform\nyou that the schema can't be resolved:\n\n.. code-block::\n\n    error: type 'default::i32' does not exist\n    ┌─ myproject/dbschema/default.gel:5:25\n    │\n    5 │         wrong_property: i32;\n    │                         ^^^ error\n\n    Schema migration error:\n    cannot proceed until schema files are fixed\n\nOnce you correct the ``i32`` type to ``int32``, you will see a message\nletting you know that things are okay now.\n\n.. code-block::\n\n    Schema is up to date.\n\nThe process will once again quieten down, but will continue to watch your\nschema and apply migrations to any changes you make to your schema.\n\n:gelcmd:`watch --migrate` is best run in a separate instance of your command\nline so that you can take care of other tasks — including officially migrating\nwhen you are satisfied with your current schema — without having to stop the\nprocess.\n\nIf you are curious what is happening as :gelcmd:`watch --migrate` does its\nthing, try the following query after you have made some changes. It will\nreturn a few lists of applied migrations, grouped by the way they were\ngenerated.\n\n.. code-block::\n\n    group schema::Migration {\n        name,\n        script\n    } by .generated_by;\n\nSome migrations will contain nothing in their ``generated_by`` property, while\nthose generated by :gelcmd:`watch --migrate` will have a\n``MigrationGeneratedBy.DevMode``.\n\n.. note::\n\n    The final option (aside from ``DevMode`` and the empty set) for\n    ``generated_by`` is ``MigrationGeneratedBy.DDLStatement``, which will\n    show up if you directly change your schema by using DDL, which is\n    generally not recommended.\n\nOnce you are satisfied with your changes while running :gelcmd:`watch\n--migrate`, just create the migration with :gelcmd:`migration create` to\nrecord the current changes to the file system.\n\nBranches\n========\n\n|Gel's| branches can be a useful part of your schema migrations, especially\nwhen you're developing new features or prototyping experimental features. By\ncreating a new branch, you can isolate schema changes from your other branches.\n\nImagine a scenario in which your main branch is |main| and your feature branch\nis called ``feature``. This is the ideal workflow for using a Gel branch\nalongside a feature branch in your VCS to develop a new feature:\n\n1. Create a new feature branch with :ref:`ref_cli_gel_branch_create`\n2. Build your feature\n3. Pull any changes on |main|\n4. Rebase your feature branch on |main| with\n   :ref:`ref_cli_gel_branch_rebase`\n5. Merge ``feature`` onto |main| with :ref:`ref_cli_gel_branch_merge`\n\nThe workflow is outlined in detail in :ref:`the branches guide in our \"Get\nstarted\" section <ref_intro_branches>`.\n\n.. _ref_migration_guide_branches_rebasing:\n\nHow rebasing works\n------------------\n\nRebasing the branch in step 4 above happens with a single command —\n:gelcmd:`branch rebase main` — but that one command has quite a bit going on\nunder the hood. Here's how it works:\n\n1. The CLI first clones the |main| branch with the data into a ``temp``\n   branch.\n2. It introspects the migration histories of ``temp`` and the ``feature``\n   branch to establish where they diverge.\n3. It applies all the divergent migrations from the ``feature`` branch\n   on the ``temp`` branch.\n4. If the operation is successful, it drops the ``feature``\n   branch and renames ``temp`` to ``feature``.\n\nWith the deceptively complicated rebase completed with just that single\ncommand, you've stacked the dominoes perfectly for your merge to succeed!\n\n\nSo, you really want to use DDL?\n===============================\n\nYou might have a good reason to use a direct DDL statement or two\nto change your schema. How do you make that happen? Gel disables\nthe usage of DDL by default if you have already carried out a migration\nthrough the recommended migration commands, so this attempt to use DDL\nwill not work:\n\n.. code-block:: edgeql-repl\n\n    db> create type MyType;\n    error: QueryError: bare DDL statements are not\n    allowed on this database branch\n    ┌─ <query>:1:1\n    │\n    1 │ create type MyType;\n    │ ^^^^^^^^^^^^^^^^^^ Use the migration commands instead.\n    │\n    = The `allow_bare_ddl` configuration variable is set to\n    'NeverAllow'.  The `gel migrate` command normally sets\n    this to avoid accidental schema changes outside of the\n    migration flow.\n\nThis configuration can be overridden by the following command which\nchanges the enum ``allow_bare_ddl`` from the default ``NeverAllow``\nto the other option, ``AlwaysAllow``.\n\n.. code-block:: edgeql-repl\n\n    db> configure current branch set allow_bare_ddl := 'AlwaysAllow';\n\nNote that the command is ``configure current branch`` and not ``configure\ninstance``, as ``allow_bare_ddl`` is evaluated on the branch level.\n\nThat wasn't so bad, so why did the CLI tell us to try to \"avoid accidental\nschema changes outside of the migration flow?\" Why is DDL disabled\nafter running a migration in the first place?\n\nSo, you really wanted to use DDL but now regret it?\n===================================================\n\nLet's start out with a very simple schema to see what happens after\nDDL is used to directly modify a schema.\n\n.. code-block:: sdl\n\n    module default {\n      type User {\n          name: str;\n      }\n    }\n\nNext, we'll set the current branch to allow bare DDL:\n\n.. code-block:: edgeql-repl\n\n    db> configure current branch set allow_bare_ddl := 'AlwaysAllow';\n\nAnd then create a type called ``SomeType`` without any properties:\n\n.. code-block:: edgeql-repl\n\n    db> create type SomeType;\n    OK: CREATE TYPE\n\nYour schema now contains this type, as you can see by typing ``describe\nschema`` or ``describe schema as sdl``:\n\n.. code-block::\n\n    {\n    'module default {\n        type SomeType;\n        type User {\n            property name: std::str;\n        };\n    };',\n    }\n\nGreat! This type is now inside your schema and you can do whatever\nyou like with it.\n\nBut this has also ruined the migration flow. Watch what happens when\nyou try to apply the change:\n\n.. code-block:: edgeql-repl\n\n    db> \\migration create\n    Error executing command: Database must be updated to\n    the last migration on the filesystem for\n    `migration create`. Run:\n    gel migrate\n\n    db> \\migrate\n    Error executing command: database applied migration\n    history is ahead of migration history in\n    \"myproject/dbschema/migrations\" by 1 revision\n\nSneakily adding ``SomeType`` into your schema to match won't work\neither. The problem is that there *is* a migration already present,\nit just doesn't exist inside your ``/migrations`` folder. You can\nsee it with the following query:\n\n.. code-block:: edgeql-repl\n\n    db> select schema::Migration {*}\n    ...  filter\n    ...  .generated_by = schema::MigrationGeneratedBy.DDLStatement;\n    {\n    schema::Migration {\n        id: 3882894a-8bb7-11ee-b009-ad814ec6a5f5,\n        name: 'm1s6oniru3zqepiaxeljt7vcgyynxuwh4ki3zdfr4hfavjozsndfua',\n        internal: false,\n        builtin: false,\n        computed_fields: [],\n        script: 'SET generated_by :=\n            (schema::MigrationGeneratedBy.DDLStatement);\n    CREATE TYPE SomeType;',\n        message: {},\n        generated_by: DDLStatement,\n    },\n    }\n\nFortunately, the fix is not too hard: we can use the command\n:gelcmd:`migration extract`. This command will retrieve the migration(s)\ncreated using DDL and assign each of them a proper file name and hash\ninside the ``/dbschema/migrations`` folder, effectively giving them a proper\nposition inside the migration flow.\n\nNote that at this point your |.gel| schema will still not match\nthe database schema, so if you were to type :gelcmd:`migration create`\nthe CLI would then ask you if you want to drop the type that you just\ncreated - because it doesn't exist inside there. So be sure to change\nyour schema to match the schema inside the database that you have\nmanually changed via DDL. If in doubt, use ``describe schema as sdl``\nto compare or use :gelcmd:`migration create` and check the output.\nIf the CLI is asking you if you want to drop a type, that means that\nyou forgot to add it to the schema inside your |.gel| file(s).\n\n\nMultiple migrations to keep data\n================================\n\nSometimes you may want to change your schema in a complex way that doesn't\nallow you to keep existing data. For example, what if you decide that you\ndon't need a ``multi`` link anymore but would like to keep some of the\ninformation in the currently linked to objects as an array instead? One\nway to make this happen is by migrating more than once.\n\nLet's give this a try by starting with with a simple ``User`` type that has\na ``friends`` link to other ``User`` objects. (If you've been following along\nall this time, a quick migration to this schema will be a breeze.)\n\n.. code-block:: sdl\n\n    module default {\n      type User {\n          name: str;\n          multi friends: User;\n      }\n    }\n\nFirst let's insert three ``User`` objects, followed by an update to\nmake each ``User`` friends with all of the others:\n\n.. code-block:: edgeql-repl\n\n    db> insert User {\n    ... name := 'User 1'\n    ... };\n    {default::User {id: d44a19bc-8bc1-11ee-8f28-47d7ec5238fe}}\n    db> insert User {\n    ... name := 'User 2'\n    ... };\n    {default::User {id: d5f941c0-8bc1-11ee-8f28-b3f56009a7b0}}\n    db> insert User {\n    ... name := 'User 3'\n    ... };\n    {default::User {id: d79cb03e-8bc1-11ee-8f28-43fe3f68004c}}\n    db> update User set {\n    ...    friends := (select detached User filter User.name != .name)\n    ...  };\n\nNow what happens if we now want to change ``multi friends`` to an\n``array<str>``? If we were simply changing a scalar property to another\nproperty it would be easy, because Gel would prompt us for a conversion\nexpression, but a change from a link to a property is different:\n\n.. code-block:: sdl\n\n    module default {\n      type User {\n          name: str;\n          multi friends: array<str>;\n      }\n    }\n\nDoing a migration as such will just drop the ``friends`` link (along\nwith its data) and create a new ``friends`` property - without any\ndata at all.\n\nTo solve this problem, we can do two migrations instead of one. First\nwe will keep the ``friends`` link, while adding a new property called\n``friend_names``:\n\n.. code-block:: sdl\n\n    module default {\n      type User {\n        name: str;\n        multi friends: User;\n        friend_names: array<str>;\n      }\n    }\n\nUpon using :gelcmd:`migration create`, the CLI will simply ask us if we\ncreated a property called ``friend_names``. We haven't applied the migration\nyet, so we might as well put the data inside the same migration. A simple\n``update`` will do the job! As we learned previously,\n:gelcmd:`migration edit` is the easiest way to add data to a migration. Or\nyou can manually add the ``update``, try to apply the migration, and change\nthe migration hash to the output suggested by the CLI.\n\n.. code-block::\n\n    CREATE MIGRATION m1hvciatdgpo3a74wagbmwhbunxbridda4qvdbrr3z2a34opks63rq\n        ONTO m1vktopcva7l6spiinh5e5nnc4dtje4ygw2fhismbmczbyaqbws7jq\n    {\n    ALTER TYPE default::User {\n        CREATE PROPERTY friend_names: array<std::str>;\n    };\n    update User set { friend_names := array_agg(.friends.name) };\n    };\n\nOnce the migration is applied, we can do a query to confirm that the data\ninside ``.friends.name`` when converted to an array is indeed the same as\nthe data inside the ``friend_names`` property:\n\n.. code-block:: edgeql-repl\n\n    db> select User { f:= array_agg(.friends.name), friend_names };\n    {\n    default::User {\n      f: ['User 2', 'User 3'],\n      friend_names: ['User 2', 'User 3']\n      },\n    default::User {\n      f: ['User 1', 'User 3'],\n      friend_names: ['User 1', 'User 3']\n      },\n    default::User {\n      f: ['User 1', 'User 2'],\n      friend_names: ['User 1', 'User 2']\n      },\n    }\n\nOr we could also use the ``all()`` function to confirm that this is the case.\n\n.. code-block:: edgeql-repl\n\n    db> select all(array_agg(User.friends.name) = User.friend_names);\n    {true}\n\nLooks good! And now we can simply remove ``multi friends: User;``\nfrom our schema and do a final migration.\n\nMigration internals\n===================\n\nWe've now reached the most optional part of the migrations tutorial,\nbut an interesting one for those curious about what goes on behind\nthe scenes during a migration.\n\nMigrations in Gel before the advent of the :gelcmd:`project` flow\nwere still automated but required more manual work if you didn't\nwant to accept all of the suggestions provided by the server. This\nprocess is in fact still used to migrate even today; the CLI just\nfacilitates it by making it easy to respond to the generated suggestions.\n\n:ref:`Early Gel migrations took place inside a transaction <ref_eql_ddl_migrations>`\nhandled by the user that essentially went like this:\n\n.. code-block::\n\n    db> start migration to { <your schema goes here> };\n\nThis starts the migration, after which the quickest process was to\ntype ``populate migration`` to accept the statements suggested by\nthe server, and then ``commit migration`` to finish the process.\n\nNow, there is another option besides simply typing ``populate migration``\nthat allows you to look at and handle the suggestions every step of\nthe way (in the same way the CLI does today), and this is what we\nare going to have some fun with. You can see\n`the original migrations RFC <rfc_>`_ if you are curious.\n\nIt is *very* finicky compared to the CLI, resulting in a failed transaction\nif any step along the way is different from the expected behavior,\nbut is an entertaining challenge to try to get right if you want to\ntruly understand how migrations work in Gel.\n\nThis process requires looking at the server's proposed solutions every\nstep of the way, and these steps are best seen in JSON format. We can make\nthis format as readable as possible with the following command:\n\n.. code-block:: edgeql-repl\n\n    db> \\set output-format json-pretty\n\nFirst, let's begin with the same same simple schema used in the previous\nexamples, via the regular :gelcmd:`migration create` and :gelcmd:`migrate`\ncommands.\n\n.. code-block:: sdl\n\n    module default {\n      type User {\n        name: str;\n      }\n    }\n\nAnd, as before, we will make a somewhat ambiguous change by changing\n``name`` to ``nam``.\n\n.. code-block:: sdl\n\n    module default {\n      type User {\n        nam: str;\n      }\n    }\n\nAnd now it's time to give the older migration method a try! To move to this\nschema using the old method, we will need to start a migration by pasting our\ndesired schema into a ``start migration to {};`` block:\n\n.. code-block:: edgeql-repl\n\n    db> start migration to {\n    ...   module default {\n    ...     type User {\n    ...       nam: str;\n    ...     }\n    ...   }\n    ... };\n\nYou should get the output ``OK: START MIGRATION``, followed by a prompt\nthat ends with ``[tx]`` to show that we are inside of a transaction.\nAnything we do here will have no effect on the current registered\nschema until we finally commit the migration.\n\nSo now what do we do? We could simply type ``populate migration``\nto accept the server's suggested changes, but let's instead take a\nlook at them one step at a time. To see the current described change,\ntype ``describe current migration as json;``. This will generate the\nfollowing output:\n\n.. code-block::\n\n    {\n    \"parent\": \"m14opov4ymcbd34x7csurz3mu4u6sik3r7dosz32gist6kpayhdg4q\",\n    \"complete\": false,\n    \"proposed\": {\n    \"prompt\": \"did you rename property 'name' of object type 'default::User'\n        to 'nam'?\",\n    \"data_safe\": true,\n    \"prompt_id\": \"RenameProperty PROPERTY default::__|name@default|User\n        TO default::__|nam@default|User\",\n    \"confidence\": 0.67,\n    \"statements\": [{\"text\": \"ALTER TYPE default::User {\\n    ALTER\n        PROPERTY name {\\n        RENAME TO nam;\\n    };\\n};\"}],\n    \"required_user_input\": []\n    },\n    \"confirmed\": []\n    }\n\nThe server is telling us with ``\"complete\": false`` that this suggestion\nis not the final step in the migration, that it is 67% confident that\nits suggestion is correct, and that we should probably type the following\nstatement:\n\n.. code-block::\n\n    ALTER TYPE default::User { ALTER PROPERTY name { RENAME TO nam; };};\n\nDon't forget to remove the newlines (``\\n``) from inside the original\nsuggestion; the transaction will fail if you don't take them out. If the\nmigration fails at any step, you will see ``[tx]`` change to ``[tx:failed]``\nand you will have to type ``abort migration`` to leave the transaction\nand begin the migration again.\n\nTechnically, at this point you are permitted to write any DDL statement\nyou like and the migration tool will adapt its suggestions to reach\nthe desired schema. Doing so though is bad practice and is more than likely\nto generate an error when you try to commit the migration.\n(Even so, give it a try if you're curious.)\n\nLet's dutifully type the suggested statement above, and then use\n``describe current migration as json`` again to see what the current\nstatus of the migration is. This time we see two major differences:\n\"complete\" is now ``true``, meaning that we are at the end of the\nproposed migration, and \"proposed\" does not contain anything. We can\nalso see our confirmed statement inside \"confirmed\" at the bottom.\n\n.. code-block::\n\n    {\n    \"parent\": \"m1fgpuxbvd74m6pb72rdikakjv3fv7cftrez7r56qjgonboimp5zoa\",\n    \"complete\": true,\n    \"proposed\": null,\n    \"confirmed\": [\"ALTER TYPE default::User {\\n ALTER PROPERTY name\n    {\\n RENAME TO nam;\\n };\\n};\"]\n    }\n\nWith this done, you can commit the migration and the migration\nwill be complete.\n\n.. code-block:: edgeql-repl\n\n    db[tx]> commit migration;\n    OK: COMMIT MIGRATION\n\nSince this migration was created using direct DDL statements,\nyou will need to use :gelcmd:`migration extract` to extract the latest\nmigration and give it a proper ``.edgeql`` file in the same way we\ndid above in the \"So you really wanted to use DDL but now regret it?\"\nsection.\n\n.. _rfc: https://github.com/geldata/rfcs/blob/master/text/1000-migrations.rst\n.. _tutorial_files: https://github.com/geldata/website/tree/main/content/tutorial/dbschema/migrations\n"
  },
  {
    "path": "docs/resources/guides/migrations/index.rst",
    "content": ".. _ref_guide_advanced_migrations:\n\n=================\nSchema migrations\n=================\n\nWelcome to the guide to Gel migrations! Let's get started.\n\n.. toctree::\n    :maxdepth: 2\n\n    guide\n    tips\n"
  },
  {
    "path": "docs/resources/guides/migrations/tips.rst",
    "content": ".. _ref_migration_tips:\n\n====\nTips\n====\n\n:edb-alt-title: Schema migration tips\n\nAdding backlinks\n----------------\n\nThis example shows how to handle a schema that makes use of a\nbacklink. We'll use a linked-list structure to represent a sequence of\nevents.\n\nWe'll start with this schema:\n\n.. code-block:: sdl\n\n    type Event {\n      required name: str;\n      prev: Event;\n\n      # ... more properties and links\n    }\n\nWe specify a ``prev`` link because that will make adding a new\n``Event`` at the end of the chain easier, since we'll be able to\nspecify the payload and the chain the ``Event`` should be appended to\nin a single :eql:stmt:`insert`. Once we've updated the schema\nfile we proceed with our first migration:\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you create object type 'default::Event'? [y,n,l,c,b,s,q,?]\n    > y\n    Created ./dbschema/migrations/00001.edgeql, id:\n    m1v3ahcx5f43y6mlsdmlz2agnf6msbc7rt3zstiqmezaqx4ev2qovq\n    $ gel migrate\n    Applied m1v3ahcx5f43y6mlsdmlz2agnf6msbc7rt3zstiqmezaqx4ev2qovq\n    (00001.edgeql)\n\nWe now have a way of chaining events together. We might create a few\nevents like these:\n\n.. code-block:: edgeql-repl\n\n    db> select Event {\n    ...     name,\n    ...     prev: { name },\n    ... };\n    {\n      default::Event {name: 'setup', prev: {}},\n      default::Event {name: 'work', prev: default::Event {name: 'setup'}},\n      default::Event {name: 'cleanup', prev: default::Event {name: 'work'}},\n    }\n\nIt seems like having a ``next`` link would be useful, too. So we can\ndefine it as a computed link by using :ref:`backlink\n<ref_datamodel_links>` notation:\n\n.. code-block:: sdl\n\n    type Event {\n      required name: str;\n\n      prev: Event;\n      next := .<prev[is Event];\n    }\n\nThe migration is straightforward enough:\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you create link 'next' of object type 'default::Event'?\n    [y,n,l,c,b,s,q,?]\n    > y\n    Created ./dbschema/migrations/00002.edgeql, id:\n    m1qpukyvw2m4lmomoseni7vdmevk4wzgsbviojacyrqgiyqjp5sdsa\n    $ gel migrate\n    Applied m1qpukyvw2m4lmomoseni7vdmevk4wzgsbviojacyrqgiyqjp5sdsa\n    (00002.edgeql)\n\nTrying out the new link on our existing data gives us:\n\n.. code-block:: edgeql-repl\n\n    db> select Event {\n    ...     name,\n    ...     prev_name := .prev.name,\n    ...     next_name := .next.name,\n    ... };\n    {\n      default::Event {\n        name: 'setup',\n        prev_name: {},\n        next_name: {'work'},\n      },\n      default::Event {\n        name: 'work',\n        prev_name: 'setup',\n        next_name: {'cleanup'},\n      },\n      default::Event {\n        name: 'cleanup',\n        prev_name: 'work',\n        next_name: {},\n      },\n    }\n\nThat's not quite right. The value of ``next_name`` appears to be a set\nrather than a singleton. This is because the link ``prev`` is\nmany-to-one and so ``next`` is one-to-many, making it a *multi* link.\nLet's fix that by making the link ``prev`` a one-to-one, after all\nwe're interested in building event chains, not trees.\n\n.. code-block:: sdl\n\n    type Event {\n      required name: str;\n\n      prev: Event {\n        constraint exclusive;\n      };\n      next := .<prev[is Event];\n    }\n\nSince the ``next`` link is computed, the migration should not need any\nadditional user input even though we're reducing the link's\ncardinality:\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you create constraint 'std::exclusive' of link 'prev'?\n    [y,n,l,c,b,s,q,?]\n    > y\n    Created ./dbschema/migrations/00003.edgeql, id:\n    m17or2bfywuckdqeornjmjh7c2voxgatspcewyefcd4p2vbdepimoa\n    $ gel migrate\n    Applied m17or2bfywuckdqeornjmjh7c2voxgatspcewyefcd4p2vbdepimoa\n    (00003.edgeql)\n\nThe new ``next`` computed link is now inferred as a ``single`` link\nand so the query results for ``next_name`` and ``prev_name`` are\nsymmetrical:\n\n.. code-block:: edgeql-repl\n\n    db> select Event {\n    ...     name,\n    ...     prev_name := .prev.name,\n    ...     next_name := .next.name,\n    ... };\n    {\n      default::Event {name: 'setup', prev_name: {}, next_name: 'work'},\n      default::Event {name: 'work', prev_name: 'setup', next_name: 'cleanup'},\n      default::Event {name: 'cleanup', prev_name: 'work', next_name: {}},\n    }\n\nMaking a property required\n--------------------------\n\nThis example shows how a property may evolve to be more and more\nstrict over time by looking at a user name field. However, similar\nevolution may be applicable to other properties that start off with\nfew restrictions and gradually become more constrained and formalized\nas the needs of the project evolve.\n\nWe'll start with a fairly simple schema:\n\n.. code-block:: sdl\n\n    type User {\n      name: str;\n    }\n\nAt this stage we don't think that this property needs to be unique or\neven required. Perhaps it's only used as a screen name and not as a\nway of identifying users.\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you create object type 'default::User'? [y,n,l,c,b,s,q,?]\n    > y\n    Created ./dbschema/migrations/00001.edgeql, id:\n    m14gwyorqqipfg7riexvbdq5dhgv7x6buqw2jaaulilcmywinmakzq\n    $ gel migrate\n    Applied m14gwyorqqipfg7riexvbdq5dhgv7x6buqw2jaaulilcmywinmakzq\n    (00001.edgeql)\n\nWe've got our first migration to set up the schema. Now after using\nthat for a little while we realize that we want to make ``name`` a\n*required property*. So we make the following change in the schema\nfile:\n\n.. code-block:: sdl\n\n    type User {\n      required name: str;\n    }\n\nNext we try to migrate:\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you make property 'name' of object type 'default::User' required?\n    [y,n,l,c,b,s,q,?]\n    > y\n    Please specify an expression to populate existing objects in order to make\n    property 'name' of object type 'default::User' required:\n    fill_expr> 'change me'\n\nOh! That's right, we can't just make ``name`` *required* because there\ncould be existing ``User`` objects without a ``name`` at all. So we\nneed to provide some kind of placeholder value for those cases. We\ntype ``'change me'`` (although any other string would do, too). This is\ndifferent from specifying a ``default`` value since it will be applied\nto *existing* objects, whereas the ``default`` applies to *new ones*.\n\nUnseen to us (unless we take a look at the automatically generated\n``.edgeql`` files inside our ``/dbschema`` folder), Gel has created\na migration script that includes the following command to make our\nschema change happen.\n\n.. code-block:: edgeql\n\n  ALTER TYPE default::User {\n      ALTER PROPERTY name {\n          SET REQUIRED USING (<std::str>'change me');\n      };\n  };\n\nWe then run :ref:`ref_cli_gel_migrate` to apply the changes.\n\nNext we realize that we actually want to make names unique, perhaps to\navoid confusion or to use them as reliable human-readable identifiers\n(unlike ``id``). We update the schema again:\n\n.. code-block:: sdl\n\n    type User {\n      required name: str {\n        constraint exclusive;\n      }\n    }\n\nNow we proceed with the migration:\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you create constraint 'std::exclusive' of property 'name'?\n    [y,n,l,c,b,s,q,?]\n    > y\n    Created ./dbschema/migrations/00003.edgeql, id:\n    m1dxs3xbk4f3vhmqh6mjzetojafddtwlphp5a3kfbfuyvupjafevya\n    $ gel migrate\n    gel error: ConstraintViolationError: name violates exclusivity\n    constraint\n\nSome objects must have the same ``name``, so the migration can't be\napplied. We have a couple of options for fixing this:\n\n1) Review the existing data and manually :eql:stmt:`update` the\n   entries with duplicate names so that they are unique.\n2) Edit the migration to add an :eql:stmt:`update` which will\n   de-duplicate ``name`` for any potential existing ``User`` objects.\n\nThe first option is good for situations where we want to signal to any\nother maintainer of a copy of this project that they need to make a\ndecision about handling name duplicates in whatever way is appropriate\nto them without making an implicit decision once and for all.\n\nHere we will go with the second option, which is good for situations\nwhere we know enough about the situation that we can make a decision\nnow and never have to duplicate this effort for any other potential\ncopies of our project.\n\nWe edit the last migration file ``00003.edgeql``:\n\n.. code-block:: edgeql-diff\n\n      CREATE MIGRATION m1dxs3xbk4f3vhmqh6mjzetojafddtwlphp5a3kfbfuyvupjafevya\n          ONTO m1ndhbxx7yudb2dv7zpypl2su2oygyjlggk3olryb5uszofrfml4uq\n      {\n    +   with U := default::User\n    +   update default::User\n    +   filter U.name = .name and U != default::User\n    +   set {\n    +     # De-duplicate names by appending a random uuid.\n    +     name := .name ++ '_' ++ <str>uuid_generate_v1mc()\n    +   };\n    +\n        ALTER TYPE default::User {\n            ALTER PROPERTY name {\n                CREATE CONSTRAINT std::exclusive;\n            };\n        };\n      };\n\nAnd then we apply the migration:\n\n.. code-block:: bash\n\n    $ gel migrate\n    gel error: could not read migrations in ./dbschema/migrations: could not\n    read migration file ./dbschema/migrations/00003.edgeql: migration name\n    should be `m1t6slgcfne35vir2lcgnqkmaxsxylzvn2hanr6mijbj5esefsp7za` but `\n    m1dxs3xbk4f3vhmqh6mjzetojafddtwlphp5a3kfbfuyvupjafevya` is used instead.\n    Migration names are computed from the hash of the migration contents. To\n    proceed you must fix the statement to read as:\n      CREATE MIGRATION m1t6slgcfne35vir2lcgnqkmaxsxylzvn2hanr6mijbj5esefsp7za\n      ONTO ...\n    if this migration is not applied to any database. Alternatively, revert the\n    changes to the file.\n\nThe migration tool detected that we've altered the file and asks us to\nupdate the migration name (acting as a checksum) if this was\ndeliberate. This is done as a precaution against accidental changes.\nSince we've done this on purpose, we can update the file and run\n:ref:`ref_cli_gel_migrate` again.\n\nFinally, we evolved our schema all the way from having an optional\nproperty ``name`` all the way to making it both *required* and\n*exclusive*. We've worked with the Gel :ref:`migration tools\n<ref_cli_gel_migration>` to iron out the kinks throughout the\nmigration process. At this point we take a quick look at the way\nduplicate ``User`` objects were resolved to decide whether we need to\ndo anything more. We can use :eql:func:`re_test` to find names that\nlook like they are ending in a UUID:\n\n.. code-block:: edgeql-repl\n\n    db> select User { name }\n    ... filter\n    ...     re_test('.* [a-z0-9]{8}(-[a-z0-9]{4}){3}-[a-z0-9]{12}$', .name);\n    {\n      default::User {name: 'change me bc30d45a-2bcf-11ec-a6c2-6ff21f33a302'},\n      default::User {name: 'change me bc30d8a6-2bcf-11ec-a6c2-4f739d559598'},\n    }\n\nLooks like the only duplicates are the users that had no names\noriginally and that never updated the ``'change me'`` placeholders, so\nwe can probably let them be for now. In hindsight, it may have been a\ngood idea to use UUID-based names to populate the empty properties\nfrom the very beginning.\n\nChanging a property to a link\n-----------------------------\n\nThis example shows how to change a property into a link. We'll use a\ncharacter in an adventure game as the type of data we will evolve.\n\nLet's start with this schema:\n\n.. code-block:: sdl\n\n    scalar type CharacterClass extending enum<warrior, scholar, rogue>;\n\n    type Character {\n      required name: str;\n      required class: CharacterClass;\n    }\n\nWe edit the schema file and perform our first migration:\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you create scalar type 'default::CharacterClass'? [y,n,l,c,b,s,q,?]\n    > y\n    did you create object type 'default::Character'? [y,n,l,c,b,s,q,?]\n    > y\n    Created ./dbschema/migrations/00001.edgeql, id:\n    m1fg76t7fbvguwhkmzrx7jwki6jxr6dvkswzeepd5v66oxg27ymkcq\n    $ gel migrate\n    Applied m1fg76t7fbvguwhkmzrx7jwki6jxr6dvkswzeepd5v66oxg27ymkcq\n    (00001.edgeql)\n\nThe initial setup may look something like this:\n\n.. code-block:: edgeql-repl\n\n    db> select Character {name, class};\n    {\n      default::Character {name: 'Alice', class: warrior},\n      default::Character {name: 'Billie', class: scholar},\n      default::Character {name: 'Cameron', class: rogue},\n    }\n\nAfter some development work we decide to add more details about the\navailable classes and encapsulate that information into its own type.\nThis way instead of a property ``class`` we want to end up with a link\n``class`` to the new data structure. Since we cannot just\n:eql:op:`cast <cast>` a scalar into an object, we'll need to convert\nbetween the two explicitly. This means that we will need to have both\nthe old and the new \"class\" information to begin with:\n\n.. code-block:: sdl\n\n    scalar type CharacterClass extending enum<warrior, scholar, rogue>;\n\n    type NewClass {\n      required name: str;\n      multi skills: str;\n    }\n\n    type Character {\n      required name: str;\n      required class: CharacterClass;\n      new_class: NewClass;\n    }\n\nWe update the schema file and migrate to the new state:\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you create object type 'default::NewClass'? [y,n,l,c,b,s,q,?]\n    > y\n    did you create link 'new_class' of object type 'default::Character'?\n    [y,n,l,c,b,s,q,?]\n    > y\n    Created ./dbschema/migrations/00002.edgeql, id:\n    m1uttd6f7fpiwiwikhdh6qyijb6pcji747ccg2cyt5357i3wsj3l3q\n    $ gel migrate\n    Applied m1uttd6f7fpiwiwikhdh6qyijb6pcji747ccg2cyt5357i3wsj3l3q\n    (00002.edgeql)\n\nIt makes sense to add a data migration as a way of consistently\ncreating ``NewClass`` objects as well as populating ``new_class``\nlinks based on the existing ``class`` property. So we first create an\nempty migration:\n\n.. code-block:: bash\n\n    $ gel migration create --allow-empty\n    Created ./dbschema/migrations/00003.edgeql, id:\n    m1iztxroh3ifoeqmvxncy77whnaei6tp5j3sewyxtrfysronjkxgga\n\nAnd then edit the ``00003.edgeql`` file to create and update objects:\n\n.. code-block:: edgeql-diff\n\n      CREATE MIGRATION m1iztxroh3ifoeqmvxncy77whnaei6tp5j3sewyxtrfysronjkxgga\n          ONTO m1uttd6f7fpiwiwikhdh6qyijb6pcji747ccg2cyt5357i3wsj3l3q\n      {\n    +    insert default::NewClass {\n    +        name := 'Warrior',\n    +        skills := {'punch', 'kick', 'run', 'jump'},\n    +    };\n    +    insert default::NewClass {\n    +        name := 'Scholar',\n    +        skills := {'read', 'write', 'analyze', 'refine'},\n    +    };\n    +    insert default::NewClass {\n    +        name := 'Rogue',\n    +        skills := {'impress', 'sing', 'steal', 'run', 'jump'},\n    +    };\n    +\n    +    update default::Character\n    +    set {\n    +        new_class := assert_single((\n    +            select default::NewClass\n    +            filter .name ilike <str>default::Character.class\n    +        )),\n    +    };\n      };\n\nTrying to apply the data migration will produce the following\nreminder:\n\n.. code-block:: bash\n\n    $ gel migrate\n    gel error: could not read migrations in ./dbschema/migrations:\n    could not read migration file ./dbschema/migrations/00003.edgeql:\n    migration name should be\n    `m1e3d3eg3j2pr7acie4n5rrhaddyhkiy5kgckd5l7h5ysrpmgwxl5a` but\n    `m1iztxroh3ifoeqmvxncy77whnaei6tp5j3sewyxtrfysronjkxgga` is used\n    instead.\n    Migration names are computed from the hash of the migration\n    contents. To proceed you must fix the statement to read as:\n      CREATE MIGRATION m1e3d3eg3j2pr7acie4n5rrhaddyhkiy5kgckd5l7h5ysrpmgwxl5a\n      ONTO ...\n    if this migration is not applied to any database. Alternatively,\n    revert the changes to the file.\n\nThe migration tool detected that we've altered the file and asks us to\nupdate the migration name (acting as a checksum) if this was\ndeliberate. This is done as a precaution against accidental changes.\nSince we've done this on purpose, we can update the file and run\n:ref:`ref_cli_gel_migrate` again.\n\nWe can see the changes after the data migration is complete:\n\n.. code-block:: edgeql-repl\n\n    db> select Character {\n    ...     name,\n    ...     class,\n    ...     new_class: {\n    ...         name,\n    ...     }\n    ... };\n    {\n      default::Character {\n        name: 'Alice',\n        class: warrior,\n        new_class: default::NewClass {name: 'Warrior'},\n      },\n      default::Character {\n        name: 'Billie',\n        class: scholar,\n        new_class: default::NewClass {name: 'Scholar'},\n      },\n      default::Character {\n        name: 'Cameron',\n        class: rogue,\n        new_class: default::NewClass {name: 'Rogue'},\n      },\n    }\n\nEverything seems to be in order. It is time to clean up the old\nproperty and ``CharacterClass`` :eql:type:`enum`:\n\n.. code-block:: sdl\n\n    type NewClass {\n      required name: str;\n      multi skills: str;\n    }\n\n    type Character {\n      required name: str;\n      new_class: NewClass;\n    }\n\nThe migration tools should have no trouble detecting the things we\njust removed:\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you drop property 'class' of object type 'default::Character'?\n    [y,n,l,c,b,s,q,?]\n    > y\n    did you drop scalar type 'default::CharacterClass'? [y,n,l,c,b,s,q,?]\n    > y\n    Created ./dbschema/migrations/00004.edgeql, id:\n    m1jdnz5bxjj6kjz2pylvudli5rvw4jyr2ilpb4hit3yutwi3bq34ha\n    $ gel migrate\n    Applied m1jdnz5bxjj6kjz2pylvudli5rvw4jyr2ilpb4hit3yutwi3bq34ha\n    (00004.edgeql)\n\nNow that the original property and scalar type are gone, we can rename\nthe \"new\" components, so that they become ``class`` link and\n``CharacterClass`` type, respectively:\n\n.. code-block:: sdl\n\n    type CharacterClass {\n      required name: str;\n      multi skills: str;\n    }\n\n    type Character {\n      required name: str;\n      class: CharacterClass;\n    }\n\nThe migration tools pick up the changes without any issues again. It\nmay seem tempting to combine the last two steps, but deleting and\nrenaming in a single step would cause the migration tools to report a\nname clash. As a general rule, it is a good idea to never mix renaming\nand deleting of closely interacting entities in the same migration.\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you rename object type 'default::NewClass' to\n    'default::CharacterClass'? [y,n,l,c,b,s,q,?]\n    > y\n    did you rename link 'new_class' of object type 'default::Character' to\n    'class'? [y,n,l,c,b,s,q,?]\n    > y\n    Created ./dbschema/migrations/00005.edgeql, id:\n    m1ra4fhx2erkygbhi7qjxt27yup5aw5hkr5bekn5y5jeam5yn57vsa\n    $ gel migrate\n    Applied m1ra4fhx2erkygbhi7qjxt27yup5aw5hkr5bekn5y5jeam5yn57vsa\n    (00005.edgeql)\n\nFinally, we have replaced the original ``class`` property with a link:\n\n.. code-block:: edgeql-repl\n\n    db> select Character {\n    ...     name,\n    ...     class: {\n    ...         name,\n    ...         skills,\n    ...     }\n    ... };\n    {\n      default::Character {\n        name: 'Alice',\n        class: default::CharacterClass {\n          name: 'Warrior',\n          skills: {'punch', 'kick', 'run', 'jump'},\n        },\n      },\n      default::Character {\n        name: 'Billie',\n        class: default::CharacterClass {\n          name: 'Scholar',\n          skills: {'read', 'write', 'analyze', 'refine'},\n        },\n      },\n      default::Character {\n        name: 'Cameron',\n        class: default::CharacterClass {\n          name: 'Rogue',\n          skills: {'impress', 'sing', 'steal', 'run', 'jump'},\n        },\n      },\n    }\n\nChanging the type of a property\n-------------------------------\n\nThis example shows how to change the type of a property. We'll use a\ncharacter in an adventure game as the type of data we will evolve.\n\nLet's start with this schema:\n\n.. code-block:: sdl\n\n    type Character {\n      required name: str;\n      required description: str;\n    }\n\nWe edit the schema file and perform our first migration:\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you create object type 'default::Character'? [y,n,l,c,b,s,q,?]\n    > y\n    Created ./dbschema/migrations/00001.edgeql, id:\n    m1paw3ogpsdtxaoywd6pl6beg2g64zj4ykhd43zby4eqh64yjad47a\n    $ gel migrate\n    Applied m1paw3ogpsdtxaoywd6pl6beg2g64zj4ykhd43zby4eqh64yjad47a\n    (00001.edgeql)\n\nThe intent is for the ``description`` to provide some text which\nserves both as something to be shown to the player as well as\ndetermining some game actions. Se we end up with something like this:\n\n.. code-block:: edgeql-repl\n\n    db> select Character {name, description};\n    {\n      default::Character {name: 'Alice', description: 'Tall and strong'},\n      default::Character {name: 'Billie', description: 'Smart and aloof'},\n      default::Character {name: 'Cameron', description: 'Dashing and smooth'},\n    }\n\nHowever, as we keep developing our game it becomes apparent that this\nis less of a \"description\" and more of a \"character class\", so at\nfirst we just rename the property to reflect that:\n\n.. code-block:: sdl\n\n    type Character {\n      required name: str;\n      required class: str;\n    }\n\nThe migration gives us this:\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you rename property 'description' of object type 'default::Character'\n    to 'class'? [y,n,l,c,b,s,q,?]\n    > y\n    Created ./dbschema/migrations/00002.edgeql, id:\n    m1ljrgrofsqkvo5hsxc62mnztdhlerxp6ucdto262se6dinhuj4mqq\n    $ gel migrate\n    Applied m1ljrgrofsqkvo5hsxc62mnztdhlerxp6ucdto262se6dinhuj4mqq\n    (00002.edgeql)\n\n|Gel| detected that the change looked like a property was being\nrenamed, which we confirmed. Since this was an existing property being\nrenamed, the data is all preserved:\n\n.. code-block:: edgeql-repl\n\n    db> select Character {name, class};\n    {\n      default::Character {name: 'Alice', class: 'Tall and strong'},\n      default::Character {name: 'Billie', class: 'Smart and aloof'},\n      default::Character {name: 'Cameron', class: 'Dashing and smooth'},\n    }\n\nThe contents of the ``class`` property are a bit too verbose, so we\ndecide to update them. In order for this update to be consistently\napplied across several developers, we will make it in the form of a\n*data migration*:\n\n.. code-block:: bash\n\n    $ gel migration create --allow-empty\n    Created ./dbschema/migrations/00003.edgeql, id:\n    m1qv2pdksjxxzlnujfed4b6to2ppuodj3xqax4p3r75yfef7kd7jna\n\nNow we can edit the file ``00003.edgeql`` directly:\n\n.. code-block:: edgeql-diff\n\n      CREATE MIGRATION m1qv2pdksjxxzlnujfed4b6to2ppuodj3xqax4p3r75yfef7kd7jna\n          ONTO m1ljrgrofsqkvo5hsxc62mnztdhlerxp6ucdto262se6dinhuj4mqq\n      {\n    +     update default::Character\n    +     set {\n    +         class :=\n    +             'warrior' if .class = 'Tall and strong' else\n    +             'scholar' if .class = 'Smart and aloof' else\n    +             'rogue'\n    +     };\n      };\n\nWe're ready to apply the migration:\n\n.. code-block:: bash\n\n    $ gel migrate\n    gel error: could not read migrations in ./dbschema/migrations:\n    could not read migration file ./dbschema/migrations/00003.edgeql:\n    migration name should be\n    `m1ryafvp24g5eqjeu65zr4bqf6m3qath3lckfdhoecfncmr7zshehq`\n    but `m1qv2pdksjxxzlnujfed4b6to2ppuodj3xqax4p3r75yfef7kd7jna` is used\n    instead.\n    Migration names are computed from the hash of the migration\n    contents. To proceed you must fix the statement to read as:\n      CREATE MIGRATION m1ryafvp24g5eqjeu65zr4bqf6m3qath3lckfdhoecfncmr7zshehq\n      ONTO ...\n    if this migration is not applied to any database. Alternatively,\n    revert the changes to the file.\n\nThe migration tool detected that we've altered the file and asks us to\nupdate the migration name (acting as a checksum) if this was\ndeliberate. This is done as a precaution against accidental changes.\nSince we've done this on purpose, we can update the file and run\n:ref:`ref_cli_gel_migrate` again.\n\nAs the game becomes more stable there's no reason for the ``class`` to\nbe a :eql:type:`str` anymore, instead we can use an :eql:type:`enum`\nto make sure that we don't accidentally use some invalid value for it.\n\n.. code-block:: sdl\n\n    scalar type CharacterClass extending enum<warrior, scholar, rogue>;\n\n    type Character {\n      required name: str;\n      required class: CharacterClass;\n    }\n\nFortunately, we've already updated the ``class`` strings to match the\n:eql:type:`enum` values, so that a simple cast will convert all the\nvalues. If we had not done this earlier we would need to do it now in\norder for the type change to work.\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you create scalar type 'default::CharacterClass'? [y,n,l,c,b,s,q,?]\n    > y\n    did you alter the type of property 'class' of object type\n    'default::Character'? [y,n,l,c,b,s,q,?]\n    > y\n    Created ./dbschema/migrations/00004.edgeql, id:\n    m1hc4yynkejef2hh7fvymvg3f26nmynpffksg7yvfksqufif6lulgq\n    $ gel migrate\n    Applied m1hc4yynkejef2hh7fvymvg3f26nmynpffksg7yvfksqufif6lulgq\n    (00004.edgeql)\n\nThe final migration converted all the ``class`` property values:\n\n.. code-block:: edgeql-repl\n\n    db> select Character {name, class};\n    {\n      default::Character {name: 'Alice', class: warrior},\n      default::Character {name: 'Billie', class: scholar},\n      default::Character {name: 'Cameron', class: rogue},\n    }\n\nAdding a required link\n----------------------\n\nThis example shows how to setup a required link. We'll use a\ncharacter in an adventure game as the type of data we will evolve.\n\nLet's start with this schema:\n\n.. code-block:: sdl\n\n    type Character {\n      required name: str;\n    }\n\nWe edit the schema file and perform our first migration:\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you create object type 'default::Character'? [y,n,l,c,b,s,q,?]\n    > y\n    Created ./dbschema/migrations/00001.edgeql, id:\n    m1xvu7o4z5f5xfwuun2vee2cryvvzh5lfilwgkulmqpifo5m3dnd6a\n    $ gel migrate\n    Applied m1xvu7o4z5f5xfwuun2vee2cryvvzh5lfilwgkulmqpifo5m3dnd6a\n    (00001.edgeql)\n\nThis time around let's practice performing a data migration and set up\nour character data. For this purpose we can create an empty migration\nand fill it out as we like:\n\n.. code-block:: bash\n\n    $ gel migration create --allow-empty\n    Created ./dbschema/migrations/00002.edgeql, id:\n    m1lclvwdpwitjj4xqm45wp74y4wjyadljct5o6bsctlnh5xbto74iq\n\nWe edit the ``00002.edgeql`` file by simply adding the query to add\ncharacters to it. We can use :eql:stmt:`for` to add multiple characters\nlike this:\n\n.. code-block:: edgeql-diff\n\n      CREATE MIGRATION m1lclvwdpwitjj4xqm45wp74y4wjyadljct5o6bsctlnh5xbto74iq\n          ONTO m1xvu7o4z5f5xfwuun2vee2cryvvzh5lfilwgkulmqpifo5m3dnd6a\n      {\n    +     for name in {'Alice', 'Billie', 'Cameron', 'Dana'}\n    +     union (\n    +         insert default::Character {\n    +             name := name\n    +         }\n    +     );\n      };\n\nTrying to apply the data migration will produce the following\nreminder:\n\n.. code-block:: bash\n\n    $ gel migrate\n    gel error: could not read migrations in ./dbschema/migrations:\n    could not read migration file ./dbschema/migrations/00002.edgeql:\n    migration name should be\n    `m1juin65wriqmb4vwg23fiyajjxlzj2jyjv5qp36uxenit5y63g2iq` but\n    `m1lclvwdpwitjj4xqm45wp74y4wjyadljct5o6bsctlnh5xbto74iq` is used instead.\n    Migration names are computed from the hash of the migration contents. To\n    proceed you must fix the statement to read as:\n      CREATE MIGRATION m1juin65wriqmb4vwg23fiyajjxlzj2jyjv5qp36uxenit5y63g2iq\n      ONTO ...\n    if this migration is not applied to any database. Alternatively,\n    revert the changes to the file.\n\nThe migration tool detected that we've altered the file and asks us to\nupdate the migration name (acting as a checksum) if this was\ndeliberate. This is done as a precaution against accidental changes.\nSince we've done this on purpose, we can update the file and run\n:ref:`ref_cli_gel_migrate` again.\n\n.. code-block:: edgeql-diff\n\n    - CREATE MIGRATION m1lclvwdpwitjj4xqm45wp74y4wjyadljct5o6bsctlnh5xbto74iq\n    + CREATE MIGRATION m1juin65wriqmb4vwg23fiyajjxlzj2jyjv5qp36uxenit5y63g2iq\n          ONTO m1xvu7o4z5f5xfwuun2vee2cryvvzh5lfilwgkulmqpifo5m3dnd6a\n      {\n          # ...\n      };\n\nAfter we apply the data migration we should be able to see the added\ncharacters:\n\n.. code-block:: edgeql-repl\n\n    db> select Character {name};\n    {\n      default::Character {name: 'Alice'},\n      default::Character {name: 'Billie'},\n      default::Character {name: 'Cameron'},\n      default::Character {name: 'Dana'},\n    }\n\nLet's add a character ``class`` represented by a new type to our\nschema and data. Unlike in the scenario when changing a property\nto a link, we will add the ``required link class`` right away,\nwithout any intermediate properties. So we end up with a schema\nlike this:\n\n.. code-block:: sdl\n\n    type CharacterClass {\n      required name: str;\n      multi skills: str;\n    }\n\n    type Character {\n      required name: str;\n      required class: CharacterClass;\n    }\n\nWe go ahead and try to apply this new schema:\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you create object type 'default::CharacterClass'? [y,n,l,c,b,s,q,?]\n    > y\n    did you create link 'class' of object type 'default::Character'?\n    [y,n,l,c,b,s,q,?]\n    > y\n    Please specify an expression to populate existing objects in order to make\n    link 'class' of object type 'default::Character' required:\n    fill_expr>\n\nUh-oh! Unlike in a situation with a required property, it's not a good\nidea to just :eql:stmt:`insert` a new ``CharacterClass`` object for\nevery character. So we should abort this migration attempt and rethink\nour strategy. We need a separate step where the ``class`` link is\nnot *required* so that we can write some custom queries to handle\nthe character classes:\n\n.. code-block:: sdl\n\n    type CharacterClass {\n      required name: str;\n      multi skills: str;\n    }\n\n    type Character {\n      required name: str;\n      class: CharacterClass;\n    }\n\nWe can now create a migration for our new schema, but we won't apply\nit right away:\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you create object type 'default::CharacterClass'? [y,n,l,c,b,s,q,?]\n    > y\n    did you create link 'class' of object type 'default::Character'?\n    [y,n,l,c,b,s,q,?]\n    > y\n    Created ./dbschema/migrations/00003.edgeql, id:\n    m1jie3xamsm2b7ygqccwfh2degdi45oc7mwuyzjkanh2qwgiqvi2ya\n\nWe don't need to create a blank migration to add data, we can add our\nmodifications into the migration that adds the ``class`` link\ndirectly. Doing this makes sense when the schema changes seem to\nrequire the data migration and the two types of changes logically go\ntogether. We will need to create some ``CharacterClass`` objects as\nwell as :eql:stmt:`update` the ``class`` link on existing\n``Character`` objects:\n\n.. code-block:: edgeql-diff\n\n      CREATE MIGRATION m1jie3xamsm2b7ygqccwfh2degdi45oc7mwuyzjkanh2qwgiqvi2ya\n          ONTO m1juin65wriqmb4vwg23fiyajjxlzj2jyjv5qp36uxenit5y63g2iq\n      {\n        CREATE TYPE default::CharacterClass {\n            CREATE REQUIRED PROPERTY name -> std::str;\n            CREATE MULTI PROPERTY skills -> std::str;\n        };\n        ALTER TYPE default::Character {\n            CREATE LINK class -> default::CharacterClass;\n        };\n\n    +   insert default::CharacterClass {\n    +       name := 'Warrior',\n    +       skills := {'punch', 'kick', 'run', 'jump'},\n    +   };\n    +   insert default::CharacterClass {\n    +       name := 'Scholar',\n    +       skills := {'read', 'write', 'analyze', 'refine'},\n    +   };\n    +   insert default::CharacterClass {\n    +       name := 'Rogue',\n    +       skills := {'impress', 'sing', 'steal', 'run', 'jump'},\n    +   };\n    +   # All warriors\n    +   update default::Character\n    +   filter .name in {'Alice'}\n    +   set {\n    +       class := assert_single((\n    +           select default::CharacterClass\n    +           filter .name = 'Warrior'\n    +       )),\n    +   };\n    +   # All scholars\n    +   update default::Character\n    +   filter .name in {'Billie'}\n    +   set {\n    +       class := assert_single((\n    +           select default::CharacterClass\n    +           filter .name = 'Scholar'\n    +       )),\n    +   };\n    +   # All rogues\n    +   update default::Character\n    +   filter .name in {'Cameron', 'Dana'}\n    +   set {\n    +       class := assert_single((\n    +           select default::CharacterClass\n    +           filter .name = 'Rogue'\n    +       )),\n    +   };\n      };\n\nIn a real game we might have a lot more characters and so a good way\nto update them all is to update characters of the same class in bulk.\n\nJust like before we'll be reminded to fix the migration name since\nwe've altered the migration file. After fixing the migration hash we\ncan apply it. Now all our characters should have been assigned their\nclasses:\n\n.. code-block:: edgeql-repl\n\n    db> select Character {\n    ...     name,\n    ...     class: {\n    ...         name\n    ...     }\n    ... };\n    {\n      default::Character {\n        name: 'Alice',\n        class: default::CharacterClass {name: 'Warrior'},\n      },\n      default::Character {\n        name: 'Billie',\n        class: default::CharacterClass {name: 'Scholar'},\n      },\n      default::Character {\n        name: 'Cameron',\n        class: default::CharacterClass {name: 'Rogue'},\n      },\n      default::Character {\n        name: 'Dana',\n        class: default::CharacterClass {name: 'Rogue'},\n      },\n    }\n\nWe're finally ready to make the ``class`` link *required*. We update\nthe schema:\n\n.. code-block:: sdl\n\n    type CharacterClass {\n      required name: str;\n      multi skills: str;\n    }\n\n    type Character {\n      required name: str;\n      required class: CharacterClass;\n    }\n\nAnd we perform our final migration:\n\n.. code-block:: bash\n\n    $ gel migration create\n    did you make link 'class' of object type 'default::Character' required?\n    [y,n,l,c,b,s,q,?]\n    > y\n    Please specify an expression to populate existing objects in order to\n    make link 'class' of object type 'default::Character' required:\n    fill_expr> assert_exists(.class)\n    Created ./dbschema/migrations/00004.edgeql, id:\n    m14yblybdo77c7bjtm6nugiy5cs6pl6rnuzo5b27gamy4zhuwjifia\n\nThe migration system doesn't know that we've already assigned ``class`` values\nto all the ``Character`` objects, so it still asks us for an expression to be\nused in case any of the objects need it. We can use ``assert_exists(.class)``\nhere as a way of being explicit about the fact that we expect the values to\nalready be present. Missing values would have caused an error even without the\n``assert_exists`` wrapper, but being explicit may help us capture the intent\nand make debugging a little easier if anyone runs into a problem at this step.\n\nIn fact, before applying this migration, let's actually add a new\n``Character`` to see what happens:\n\n.. code-block:: edgeql-repl\n\n    db> insert Character {name := 'Eric'};\n    {\n      default::Character {\n        id: 9f4ac7a8-ac38-11ec-b076-afefd12d7e66,\n      },\n    }\n\nOur attempt at migrating fails as we expected:\n\n.. code-block:: bash\n\n    $ gel migrate\n    gel error: MissingRequiredError: missing value for required link\n    'class' of object type 'default::Character'\n      Detail: Failing object id is 'ee604992-c1b1-11ec-ad59-4f878963769f'.\n\nAfter removing the bugged ``Character``, we can migrate without any problems:\n\n.. code-block:: bash\n\n    $ gel migrate\n    Applied m14yblybdo77c7bjtm6nugiy5cs6pl6rnuzo5b27gamy4zhuwjifia\n    (00004.edgeql)\n\nRecovering lost migrations\n--------------------------\n\nYou can recover lost migration files, writing the database's current\nmigration history to ``/dbschema/migrations`` by using the\n:ref:`ref_cli_gel_migration_extract`.\n\nGetting the current migration\n-----------------------------\n\nThe following query will return the most current migration:\n\n.. code-block:: edgeql-repl\n\n    db> with\n    ...  module schema,\n    ...  lastMigration := (\n    ...    select Migration filter not exists .<parents[is Migration]\n    ...  )\n    ... select lastMigration {*};\n"
  },
  {
    "path": "docs/resources/guides/tutorials/chatgpt_bot.rst",
    "content": ".. _ref_guide_chatgpt_bot:\n\n=======\nChatGPT\n=======\n\n:edb-alt-title: Build your own documentation chatbot with ChatGPT and Gel\n\n*For additional context, check out* `our blog post about why and how we use\nChatGPT via embeddings`_ *to create our “Ask AI” bot which answers questions\nrelated to the Gel docs.*\n\n.. lint-off\n\n.. _our blog post about why and how we use ChatGPT via embeddings:\n  https://www.geldata.com/blog/chit-chatting-with-edgedb-docs-via-chatgpt-and-pgvector\n\n.. lint-on\n\nIn this tutorial we're going to build a documentation chatbot with\n`Next.js <https://nextjs.org/>`_, `OpenAI <https://openai.com/>`_, and Gel.\n\n.. warning::\n\n    This project makes calls to OpenAI's APIs. At the time of publication, new\n    users are granted $5 in API credits to use within the first three months\n    after registration. Trials are granted per phone number, not per account.\n    If you exhaust your trial credits or if your three months lapse, you will\n    need to switch to a paid account in order to build the tutorial project.\n\nHow it works\n============\n\n*tl;dr- Training a language model is hard, but using embeddings to give it\naccess to information beyond what it's trained on is easy… so we will do that!\nNow,* :ref:`skip ahead to get started building <ref_guide_chatgpt_bot_start>`\n*or read on for more detail.*\n\nOur chatbot is backed by `OpenAI's ChatGPT <https://openai.com/blog/chatgpt>`_.\nChatGPT is an advanced large language model (LLM) that uses machine learning\nalgorithms to generate human-like responses based on the input it's given.\n\nThere are two options when integrating ChatGPT and language models in general:\nfine-tuning the model or using `embeddings\n<https://platform.openai.com/docs/guides/embeddings/what-are-embeddings>`_.\nFine-tuning produces the best result, but it needs more of everything: more\nmoney, more time, more resources, and more training data. That's why many\npeople and businesses use embeddings instead to provide additional context to\nan existing language model.\n\nEmbeddings are a way to convert words, phrases, or other types of data into a\nnumerical form that a computer can do math with. All of this is built on top\nof the foundation of natural language processing (NLP) which allows computers\nto fake an understanding of human language. In the context of NLP, word\nembeddings are used to transform words into vectors. These vectors define a\nword's position in space where the computer sorts them based on their\nsyntactic and semantic similarity. For instance, synonyms are closer to each\nother, and words that often appear in similar contexts are grouped together.\n\nWhen using embeddings we are not training the language model. Instead we're\ncreating embedding vectors for every piece of documentation which will later\nhelp us find which piece of documentation likely answers a user's question.\nWhen a user asks a question, we create a new embedding for that question and\ncompare it against the embeddings generated from our documentation to find the\nmost similar embeddings. The answer is generated using the content that\ncorresponds to these similar embeddings.\n\nWith that out of the way, let's walk through how the pieces fit together.\n\n\nImplementation overview\n-----------------------\n\nBroadly, the app does two things: it generates embeddings from documentation,\nand it uses those embeddings to answer user questions. The first is triggered\nmanually in this implementation. We'll want to trigger it whenever the\ndocumentation is updated. The second is triggered automatically when the user\nasks a question.\n\nEmbedding generation requires two steps:\n\n1. create embeddings for each section using `OpenAI's embeddings API\n   <https://platform.openai.com/docs/guides/embeddings>`_\n2. store the embeddings data in Gel using pgvector\n\nEach time a user asks a question, our app will:\n\n1. query the database for the documentation sections most relevant to\n   the question using a similarity function\n2. inject these sections as a context into the prompt — together with user's\n   question — and submit this request to OpenAI\n3. stream the OpenAI response back to the user in real time\n\n\nPrerequisites\n=============\n\nThis tutorial assumes you have `Node.js <https://nodejs.org/>`_ installed. If\nyou don't, please install it before continuing.\n\nThe build requires other software too, but we'll help you install it as part of\nthe tutorial.\n\n.. _ref_guide_chatgpt_bot_start:\n\n\nInitial setup\n=============\n\nLet's start by scaffolding our app with the Next.js ``create-next-app`` tool.\nRun this wherever you would like to create the new directory for this project.\n\n.. code-block:: bash\n\n    $ npx create-next-app --typescript docs-chatbot\n    Need to install the following packages:\n      create-next-app@13.4.12\n    Ok to proceed? (y) y\n    ✔ Would you like to use ESLint? … No / Yes\n    ✔ Would you like to use Tailwind CSS? … No / Yes\n    ✔ Would you like to use `src/` directory? … No / Yes\n    ✔ Would you like to use App Router? (recommended) … No / Yes\n    ✔ Would you like to customize the default import alias? … No / Yes\n    Creating a new Next.js app in /<path>/<to>/<project>/docs-chatbot.\n\nChoose \"Yes\" for all questions except \"Would you like to use \\`src/\\`\ndirectory?\" and \"Would you like to customize the default import alias?\"\n\nOnce bootstrapping is complete, you should see a success message:\n\n.. code-block::\n\n    Success! Created docs-chatbot at\n    /<path>/<to>/<project>/docs-chatbot\n\nChange into the new directory so we can get started!\n\n.. code-block:: bash\n\n    $ cd docs-chatbot\n\nLet's make two changes to the ``tsconfig.json`` generated by\n``create-next-app``. Change the ``target`` to ``\"es6\"`` because we will use\nsome data structures that are only available in ES6. Update the\n``compilerOptions`` object by setting the ``baseUrl`` property to the root with\n``\"baseUrl\": \".\"``. Later when we add modules to the root of the project, this\nwill make it easier to import them.\n\n.. lint-off\n\n.. code-block:: json-diff\n    :caption: tsconfig.json\n\n      {\n        \"compilerOptions\": {\n    -     \"target\": \"es5\",\n    +     \"target\": \"es6\",\n          \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n          \"allowJs\": true,\n          \"skipLibCheck\": true,\n          \"strict\": true,\n          \"forceConsistentCasingInFileNames\": true,\n          \"noEmit\": true,\n          \"esModuleInterop\": true,\n          \"module\": \"esnext\",\n          \"moduleResolution\": \"bundler\",\n          \"resolveJsonModule\": true,\n          \"isolatedModules\": true,\n          \"jsx\": \"preserve\",\n          \"incremental\": true,\n          \"plugins\": [\n            {\n              \"name\": \"next\"\n            }\n          ],\n          \"paths\": {\n            \"@/*\": [\"./*\"]\n    -     }\n    +     },\n    +     \"baseUrl\": \".\"\n        },\n        \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n        \"exclude\": [\"node_modules\"]\n      }\n\n.. lint-on\n\nNow, we'll create an instance of Gel for our project.\n\n\nCreate a local Gel instance\n---------------------------\n\nTo create our instance, let's initialize our project as a Gel project. Run\nthe following in the root of the project:\n\n.. code-block:: bash\n\n    $ npx gel project init\n    No `gel.toml` found in `/<path>/<to>/<project>/docs-chatbot`\n    or above\n\n    Do you want to initialize a new project? [Y/n]\n    > Y\n\n    Specify the name of Gel instance to use with this project\n    [default: docs_chatbot]:\n    > docs_chatbot\n\n    Checking Gel versions...\n    Specify the version of Gel to use with this project\n    [default: 3.2]:\n    > 3.2\n\nThe CLI should set up a Gel project, an instance, and a default branch on\nthat instance.\n\n- Confirm project creation by checking for an |gel.toml| file and a\n  ``dbschema`` directory in the project root.\n- Confirm the instance is running with the :gelcmd:`instance list` command.\n  Search for the name of the instance you've just created (``docs_chatbot`` if\n  you're following along) and check the status. (Don't worry if the status is\n  \"inactive\"; the status will change to \"running\" automatically when you\n  connect to the instance.)\n- Confirm you can connect to the created instance by running |gelcmd| in the\n  terminal to connect to it via REPL or by running :gelcmd:`ui` to connect\n  using the UI.\n\n\nConfigure the environment\n-------------------------\n\nCreate a ``.env.local`` file in the root of your new Next.js project.\n\n.. code-block:: bash\n\n    $ touch .env.local\n\nWe're going to add a couple of variables to that file to configure the Gel\nclient. We'll need to run a command on our new instance to get the value for\none of those. Since we'll be using the `Edge runtime\n<https://nextjs.org/docs/app/api-reference/edge>`_ in our Next.js project, the\ngel-js client won't be able to access the Node.js filesystem APIs it usually\nuses to automatically find your instance, so we need to provide the DSN for the\ninstance instead. To get that, run this command:\n\n.. code-block:: bash\n\n    $ npx gel instance credentials --insecure-dsn\n\nCopy what it logs out. Open the ``.env.local`` file in your text editor and add\nthis to it:\n\n.. code-block:: typescript\n\n    GEL_DSN=<your-dsn>\n    GEL_CLIENT_TLS_SECURITY=\"insecure\"\n\nReplace ``<your-dsn>`` with the value you copied earlier.\n\nWe're going to be using the Gel HTTP client a bit later to connect to our\ndatabase, but it requires a trusted TLS/SSL certificate. Local development\ninstances use self signed certificates, and using HTTPS with these certificates\nwill result in an error. To work around this error, we allow the client to\nignore TLS by setting the :gelenv:`CLIENT_TLS_SECURITY` variable to\n``\"insecure\"``. Bear in mind that this is only for local development, and you\nshould always use TLS in production.\n\nWe need to set one more environment variable, but first we have to get an API\nkey.\n\n\nPrepare the OpenAI API client\n-----------------------------\n\nWe need an API key from OpenAI in order to make the calls we need to make this\napp work. To get one:\n\n1. Log in or sign up to the `OpenAI platform\n   <https://platform.openai.com/account/api-keys>`_.\n2. Create new `secret key <https://platform.openai.com/account/api-keys>`_.\n\n.. warning::\n\n    Don't forget: you may need to start a paid account if you do not have any\n    API free trial credits remaining.\n\nCopy the new key. Re-open your ``.env.local`` file and add it like this:\n\n.. code-block:: typescript-diff\n\n      GEL_DSN=<your-dsn>\n      GEL_CLIENT_TLS_SECURITY=\"insecure\"\n    + OPENAI_API_KEY=\"<your-openai-api-key>\"\n\nInstead of ``<your-openai-api-key>``, paste in the key you just created.\n\nWhile we're here, let's get that key ready to be used. We will be making calls\nto the OpenAI API. We'll create a ``utils`` module and export a function from\nit that initializes an OpenAI API client. We can import and call the function\nto create a new client anywhere we need to make OpenAI API calls.\n\n.. code-block:: typescript\n\n    import OpenAI from \"openai\";\n\n    export function initOpenAIClient() {\n      if (!process.env.OPENAI_API_KEY)\n        throw new Error(\"Missing environment variable OPENAI_API_KEY\");\n\n      return new OpenAI({\n        apiKey: process.env.OPENAI_API_KEY,\n      });\n    }\n\nIt's pretty simple. It makes sure the API key was provided in the environment\nvariable and returns a new API client initialized with that key.\n\nNow, let's create error messages we will use in a couple of places if these API\ncalls go wrong. Create a file ``app/constants.ts`` and fill it with this:\n\n.. code-block:: typescript\n\n    export const errors = {\n      flagged: `OpenAI has declined to answer your question due to their\n              [usage-policies](https://openai.com/policies/usage-policies).\n              Please try another question.`,\n      default: \"There was an error processing your request. Please try again.\",\n    };\n\nThis exports an object ``errors`` with a couple of error messages.\n\nNow, let's get the documentation ready!\n\n\nPut the documentation in place\n==============================\n\nFor this project, we will be using documentation written as Markdown files\nsince they are straightforward for OpenAI's language models to use.\n\nCreate a ``docs`` folder in the root of the project. Here we will place our\nMarkdown documentation files. You can grab the files we use from `the example\nproject's GitHub repo\n<https://github.com/geldata/gel-examples/tree/main/docs-chatbot/docs>`_ or\nadd your own. (If you use your own, you may also want to adjust the system\nmessage we send to OpenAI later.)\n\n.. note:: On using formats other than Markdown\n\n    We *could* opt to use other simple formats like plain text files or more\n    complex ones like HTML. Since more complex formats can include additional\n    data beyond what we want the language model to consume (like HTML's tags\n    and their attributes), we may first want to clean those files and extract\n    the content before sending it to OpenAI. (We can write our own logic for\n    this or use libraries that are available online for conversion, to Markdown\n    for example.)\n\n    It's possible to use more complex formats *without* cleaning them, but then\n    we're paying for extra tokens that don't improve the answers our chatbot\n    will give users.\n\n.. note:: On longer documentation sections\n\n    In this tutorial project, our documentation pages are short, but in\n    practice, documentation files can get quite long and may need to be split\n    into multiple sections because of the LLM's token limit. LLMs divide text\n    into tokens. For English text, 1 token is approximately 4 characters or\n    0.75 words. LLMs have limits on the number of tokens they can receive and\n    send back.\n\n    One approach to mitigate this is to parse your documentation files and\n    create new sections every time you encounter a header. If you use this\n    approach, consider section lengths when writing your documentation. If you\n    find a section is too long, consider ways you might break it up with\n    additional headings. This will probably make it easier to read for your\n    users too!\n\n    To generate embeddings, we will use the ``text-embedding-ada-002`` model.\n    Its input token limit is 8,191 tokens. Later, when answering a user's\n    questions we will use the `chat completions\n    <https://platform.openai.com/docs/guides/gpt/chat-completions-api>`_ model\n    ``gpt-3.5-turbo``. Its token limit is 4,096 tokens. This limit covers not\n    only our input, but also the API's response.\n\n    Later, when we send the user's question, we will also send related sections\n    from our documentation as part of the input to the chat completions API.\n    This is why it's important to keep our sections short: we want to leave\n    enough space for the answer.\n\n    If the related sections are too long and, together with the user's\n    question, exceed the 4,096 token limit, we will get an error back from\n    OpenAI. If the length of the question and related sections are too close to\n    the token limit but not over it, the API will send an answer, but the\n    answer will be cut off when the limit is reached.\n\n    We want to avoid either of these outcomes by making sure we always have\n    enough token headroom for all the input and the LLM's response. That's why\n    we will later set 1,500 tokens as the maximum number of tokens we will use\n    for our related sections, and it's also why it's important that sections be\n    relatively short.\n\n    If your application has longer documentation files, make sure to figure out\n    a strategy for splitting those before you generate your embeddings.\n\n\nCreate the schema to store embeddings\n=====================================\n\nTo be able to store data in the database, we have to create its schema first.\nWe want to make the schema as simple as possible and store only the relevant\ndata. We need to store the section's embeddings, content, and the number of\ntokens. The embeddings allow us to match content to questions. The content\ngives us context to feed to the LLM. We will need the token count later when\ncalculating how many related sections fit inside the prompt context while\nstaying under the model's token limit.\n\nOpen the empty schema file that was generated when we initialized the Gel\nproject (located at :dotgel:`dbschema/default` from the project directory).\nWe'll walk through what we'll add to it, one step at a time. First, add this at\nthe top of the file (above ``module default {``):\n\n.. code-block:: sdl\n    :caption: dbschema/default.gel\n\n    using extension pgvector;\n    module default {\n      # Schema will go here\n    }\n\nWe are able to store embeddings and find similar embeddings in the Gel\ndatabase because of the ``pgvector`` extension. In order to use it in our\nschema, we have to activate the ``ext::pgvector`` module with ``using extension\npgvector`` at the beginning of the schema file. This module gives us access to\nthe ``ext::pgvector::vector`` data type as well as few similarity functions and\nindexes we can use later to retrieve embeddings. Read our :ref:`pgvector\ndocumentation <ref_ext_pgvector>` for more details\non the extension.\n\nJust below that, we can start building our module by creating a new scalar\ntype.\n\n.. code-block:: sdl\n    :caption: dbschema/default.gel\n\n    using extension pgvector;\n    module default {\n      scalar type OpenAIEmbedding extending\n        ext::pgvector::vector<1536>;\n\n      type Section {\n        # We will build this out next\n      }\n    }\n\nWith the extension active, we may now add properties to our object types using\nthe included ext::pgvector::vector data type. However, in order to be able to\nuse indexes, the vectors in question need to be a of a fixed length. This can\nbe achieved by creating a custom scalar extending the vector and specifying the\ndesired length. OpenAI embeddings have length of 1,536, so that's what we use\nin our schema for this custom scalar.\n\nNow, the ``Section`` type:\n\n.. code-block:: sdl\n    :caption: dbschema/default.gel\n\n    using extension pgvector;\n    module default {\n      scalar type OpenAIEmbedding extending\n        ext::pgvector::vector<1536>;\n\n      type Section {\n        required content: str;\n        required tokens: int16;\n        required embedding: OpenAIEmbedding;\n\n        index ext::pgvector::ivfflat_cosine(lists := 1)\n          on (.embedding);\n      }\n    }\n\nThe ``Section`` contains properties to store the content, a count of tokens,\nand the embedding, which is of the custom scalar type we created in the\nprevious step.\n\nWe've also added an index inside the ``Section`` type to speed up queries. In\norder for this to work properly, the index should correspond to the\n``cosine_similarity`` function we're going to use to find sections related to\nthe user's question. That corresponding index is ``ivfflat_cosine``.\n\nWe are using the value ``1`` for the ``lists`` parameter because we will have\nvery few items in our database — three, to be exact 😅. Best practice\nis to use the number of objects divided by 1,000 for up to 1,000,000 objects.\n\nIn our case indexing does not have much impact, but if you plan to store and\nquery a large number of entries, you'll see performance gains by adding this\nindex.\n\nPut that all together, and your entire schema file should look like this:\n\n.. code-block:: sdl\n    :caption: dbschema/default.gel\n\n    using extension pgvector;\n\n    module default {\n      scalar type OpenAIEmbedding extending\n        ext::pgvector::vector<1536>;\n\n      type Section {\n        required content: str;\n        required tokens: int16;\n        required embedding: OpenAIEmbedding;\n\n        index ext::pgvector::ivfflat_cosine(lists := 1)\n          on (.embedding);\n      }\n    }\n\nWe apply this schema by creating and running a migration.\n\n.. code-block:: bash\n\n    $ npx gel migration create\n    $ npx gel migrate\n\n.. note::\n\n    In this tutorial we will regenerate all embeddings every time we run the\n    embeddings generation script, wiping all data and saving new ``Section``\n    objects for all of the documentation. This might be a reasonable approach\n    if you don't have much documentation, but if you have a lot of\n    documentation, you may want a more sophisticated approach that operates on\n    only documentation sections which have changed.\n\n    You can achieve this by saving content checksums and a unique identifier\n    for each section — in our production implementation, we use section paths —\n    as part of your ``Section`` objects. The next time you run generation,\n    compare the section's current checksum with the one you stored in the\n    database, finding it by its unique identifier. You don't need to generate\n    embeddings and update the database for a given section unless the two\n    checksums are different indicating something has changed.\n\n    If you decide to go this route, here's one way you could modify your schema\n    to support this:\n\n    .. code-block:: sdl-diff\n        :caption: dbschema/default.gel\n\n          type Section {\n        +   required path: str {\n        +     constraint exclusive;\n        +   }\n        +   required checksum: str;\n            # The rest of the Section type\n          }\n\n    You'll also need to store your unique identifier, calculate and compare\n    checksums, and update objects conditionally based on the outcome of those\n    comparisons.\n\n\nCreate and store embeddings\n===========================\n\nBefore we can script the creation of embeddings, we need to install some\nlibraries that will help us.\n\n.. code-block:: bash\n\n    $ npm install openai gel\n    $ npm install \\\n        @gel/generate \\\n        gpt-tokenizer \\\n        dotenv \\\n        tsx \\\n        --save-dev\n\nThe ``@gel/generate`` package provides a set of code generation tools that\nare useful when developing a Gel-backed applications with\nTypeScript/JavaScript. We're going to write queries using our\n:ref:`query builder <gel-js-qb>`, but before we can, we\nneed to run the query builder generator.\n\n.. code-block:: bash\n\n    $ npx @gel/generate edgeql-js\n\nAnswer \"y\" when asked about adding the query builder to ``.gitignore``.\n\nThis generator gives us a code-first way to write fully-typed EdgeQL queries\nwith TypeScript. After running the generator, you should see a new\n``edgeql-js`` folder inside ``dbschema``.\n\nFinally, we're ready to create embeddings for all sections and store them in\nthe database we created earlier. Let's make a ``generate-embeddings.ts`` file\ninside the project root.\n\n.. code-block:: bash\n\n    $ touch generate-embeddings.ts\n\nLet's look at the script's skeleton and get an understanding of the flow of\ntasks we need to perform.\n\n.. note::\n\n    Rather than trying to build this incrementally as we go, you may just want\n    to read through to understand all the code. We'll put the entire script\n    together at the end of the section, and you can copy/paste that into your\n    file.\n\n.. code-block:: typescript\n    :caption: generate-embeddings.ts\n\n    import { promises as fs } from \"fs\";\n    import { join } from \"path\";\n    import dotenv from \"dotenv\";\n    import { encode } from \"gpt-tokenizer\";\n    import * as gel from \"gel\";\n    import e from \"dbschema/edgeql-js\";\n    import { initOpenAIClient } from \"./utils\";\n\n    dotenv.config({ path: \".env.local\" });\n\n    const openai = initOpenAIClient();\n\n    interface Section {\n      id?: string;\n      tokens: number;\n      content: string;\n      embedding: number[];\n    }\n\n    async function walk(dir: string): Promise<string[]> {\n      // …\n    }\n\n    async function prepareSectionsData(\n      sectionPaths: string[]\n    ): Promise<Section[]> {\n      // …\n    }\n\n\n    async function storeEmbeddings() {\n      // …\n    }\n\n    (async function main() {\n      await storeEmbeddings();\n    })();\n\n\nAt the top are all imports we will need throughout the file. The second to last\nimport is the query builder we generated earlier, and the last one is the\nfunction that initializes our OpenAI API client.\n\nAfter the imports, we use the ``dotenv`` library to import environment\nvariables from the ``.env.local`` file.\n\nThen, we initialize our OpenAI API client by calling ``initOpenAIClient``.\n\nNext, we define a ``Section`` TypeScript interface that corresponds to\nthe ``Section`` type we have defined in the schema.\n\nThen we have a few function definitions:\n\n* ``walk`` and ``prepareSectionsData`` will be called from inside\n  ``storeEmbeddings``. ``walk`` returns an array of all documentation page\n  paths relative to the project root. ``prepareSectionsData`` takes care of\n  preparing the ``Section`` objects we will insert into the database and\n  returns those as an array.\n\n* ``storeEmbeddings`` coordinates everything.\n\nTo finish the script, we await a call to our coordinating function which kicks\noff everything else as needed.\n\n\nGetting section paths\n---------------------\n\nIn order to get the sections' content, we first need to know where the files\nare that need to be read. The ``walk`` function finds them for us and returns\nall the paths. It builds an array of all paths relative to the project root.\n\n.. code-block:: typescript\n    :caption: generate-embeddings.ts\n\n    // …\n    async function walk(dir: string): Promise<string[]> {\n      const entries = await fs.readdir(dir, { withFileTypes: true });\n\n      return (\n        await Promise.all(\n          entries.map((entry) => {\n            const path = join(dir, entry.name);\n            if (entry.isFile()) return [path];\n            else if (entry.isDirectory()) return walk(path);\n            return [];\n          })\n        )\n      ).flat();\n    }\n    // …\n\nThe output it produces looks like this:\n\n.. code-block:: typescript\n\n    [\n      'docs/edgeql/design-goals.md',\n      'docs/edgeql/overview.md',\n      'docs/edgeql/try-edgeql.md',\n    ]\n\n\nPreparing the ``Section`` objects\n---------------------------------\n\nThis function will be responsible for collecting the data we need for each\n``Section`` object we will store, including making the OpenAI API calls to\ngenerate the embeddings. Let's walk through it one piece at a time.\n\n.. code-block:: typescript\n    :caption: generate-embeddings.ts\n\n    // …\n    async function prepareSectionsData(\n      sectionPaths: string[]\n    ): Promise<Section[]> {\n      const contents: string[] = [];\n      const sections: Section[] = [];\n\n      for (const path of sectionPaths) {\n        const content = await fs.readFile(path, \"utf8\");\n        // OpenAI recommends replacing newlines with spaces for best results\n        // when generating embeddings\n        const contentTrimmed = content.replace(/\\n/g, \" \");\n        contents.push(contentTrimmed);\n        sections.push({\n          content,\n          tokens: encode(content).length,\n          embedding: [],\n        });\n      }\n      // The rest of the function\n    }\n    // …\n\nWe start with a parameter: an array of section paths. We create a couple of\nempty arrays for storing information about our sections (which will later\nbecome ``Section`` objects in the database) and their contents. We iterate\nthrough the paths, loading each file to get its content.\n\nIn the database we will save the content as is, but when calling the embedding\nAPI, OpenAI suggests that all newlines should be replaced with a single space\nfor the best results. ``contentTrimmed`` is the content with newlines replaced.\nWe push that onto our ``contents`` array and the un-trimmed content onto\n``sections``, along with a token count (obtained by calling the ``encode``\nfunction imported from ``gpt-tokenizer``) and an empty array we will later\nreplace with the actual embeddings.\n\nOnto the next bit!\n\n.. code-block:: typescript\n    :caption: generate-embeddings.ts\n\n    // …\n    async function prepareSectionsData(\n      sectionPaths: string[]\n    ): Promise<Section[]> {\n      // Part we just talked about\n\n      const embeddingResponse = await openai.embeddings.create({\n        model: \"text-embedding-ada-002\",\n        input: contents,\n      });\n\n      // The rest\n    }\n    // …\n\nNow, we generate embeddings from the content. We need to be careful about how\nwe approach the API calls to generate the embeddings since they could have a\nbig impact on how long generation takes, especially as your documentation\ngrows. The simplest solution would be to make a single request to the API for\neach section, but in the case of Gel's documentation, which has around 3,000\npages, this would take about half an hour.\n\nSince OpenAI's embeddings API can take not only a *single* string but also an\n*array* of strings, we can leverage this to batch up all our content and\ngenerate the embeddings with a single request! You can see that single API call\nwhen we set ``embeddingResponse`` to the result of the call to\n``openai.embeddings.create``, specifying the model and passing the entire array\nof contents.\n\n.. note::\n\n    One downside to this one-shot embedding generation approach is that we do\n    *not* get back token counts with the result where we *would* generating\n    embeddings for only a single string. Token counts are important because\n    they determine how many relevant sections we can send along with our input\n    to the chat completions API — the one that answers the user's question —\n    and still be within the model's token limit. To stay within the limit, we\n    need to know how many tokens each section has. Since we don't get them back\n    on a batched embedding generation, we used the `gpt-tokenizer\n    <https://www.npmjs.com/package/gpt-tokenizer>`_ library's ``encode``\n    function earlier to count them ourselves.\n\nNow, it's time to put those embeddings into our section objects by iterating\nthrough the response data.\n\n.. code-block:: typescript\n    :caption: generate-embeddings.ts\n\n    // …\n    async function prepareSectionsData(\n      sectionPaths: string[]\n    ): Promise<Section[]> {\n      // The stuff we already talked about\n\n      embeddingResponse.data.forEach((item, i) => {\n        sections[i].embedding = item.embedding;\n      });\n\n      return sections;\n    }\n    // …\n\nWe iterate through all the embeddings we got back, adding the embedding to its\nrespective section. This final piece of data makes the section fully ready to\nstore in the database, so we can now return the fully-formed sections from the\nfunction.\n\nHere's the entire function assembled:\n\n.. code-block:: typescript\n    :caption: generate-embeddings.ts\n\n    // …\n    async function prepareSectionsData(\n      sectionPaths: string[]\n    ): Promise<Section[]> {\n      const contents: string[] = [];\n      const sections: Section[] = [];\n\n      for (const path of sectionPaths) {\n        const content = await fs.readFile(path, \"utf8\");\n        // OpenAI recommends replacing newlines with spaces for best results\n        // when generating embeddings\n        const contentTrimmed = content.replace(/\\n/g, \" \");\n        contents.push(contentTrimmed);\n        sections.push({\n          content,\n          tokens: encode(content).length,\n          embedding: [],\n        });\n      }\n\n      const embeddingResponse = await openai.embeddings.create({\n        model: \"text-embedding-ada-002\",\n        input: contents,\n      });\n\n      embeddingResponse.data.forEach((item, i) => {\n        sections[i].embedding = item.embedding;\n      });\n\n      return sections;\n    }\n    // …\n\n.. note::\n\n    This is not the only approach to keeping track of tokens. We could choose\n    *not* to save token counts in the database and to instead count section\n    tokens later on the client after we find the relevant sections.\n\nNow that we have sections ready to be stored in the database, let's tie\neverything together with the ``storeEmbeddings`` function.\n\n\nStoring the ``Section`` objects\n-------------------------------\n\nAgain, we'll break the ``storeEmbeddings`` function apart and walk through it.\n\n.. code-block:: typescript\n    :caption: generate-embeddings.ts\n\n    // …\n    async function storeEmbeddings() {\n      const client = gel.createClient();\n\n      const sectionPaths = await walk(\"docs\");\n\n      console.log(`Discovered ${sectionPaths.length} sections`);\n\n      const sections = await prepareSectionsData(sectionPaths);\n\n      // The rest of the function\n    }\n    // …\n\nWe create our Gel client and get our documentation paths by calling\n``walk``. We also log out some debug information showing how many sections were\ndiscovered. Then, we prep our ``Section`` objects by calling the\n``prepareSectionsData`` function we just walked through and passing in the\ndocumentation paths we got back from ``walk``.\n\nNext, we'll store this data.\n\n.. code-block:: typescript\n    :caption: generate-embeddings.ts\n\n    // …\n    async function storeEmbeddings() {\n      // The parts we just talked about\n\n      // Delete old data from the DB.\n      await e.delete(e.Section).run(client);\n\n      // Bulk-insert all data into Gel database.\n      const query = e.params({ sections: e.json }, ({ sections }) => {\n        return e.for(e.json_array_unpack(sections), (section) => {\n          return e.insert(e.Section, {\n            content: e.cast(e.str, section.content!),\n            tokens: e.cast(e.int16, section.tokens!),\n            embedding: e.cast(e.OpenAIEmbedding, section.embedding!),\n          });\n        });\n      });\n\n      await query.run(client, { sections });\n      console.log(\"Embedding generation complete\");\n    }\n    // …\n\nThe comments do a good job of explaining here, but let's go into a little more\ndetail. First, we build and run a query that deletes all ``Section`` objects\ncurrently in the database. Then, we build another query that will insert the\nnew ``Section`` data we just prepared. We await a call to that query's ``run``\nmethod, passing in the sections we just prepared.\n\nHere's what the whole function looks like:\n\n.. code-block:: typescript\n    :caption: generate-embeddings.ts\n\n    // …\n    async function storeEmbeddings() {\n      const client = gel.createClient();\n\n      const sectionPaths = await walk(\"docs\");\n\n      console.log(`Discovered ${sectionPaths.length} sections`);\n\n      const sections = await prepareSectionsData(sectionPaths);\n\n      // Delete old data from the DB.\n      await e.delete(e.Section).run(client);\n\n      // Bulk-insert all data into Gel database.\n      const query = e.params({ sections: e.json }, ({ sections }) => {\n        return e.for(e.json_array_unpack(sections), (section) => {\n          return e.insert(e.Section, {\n            content: e.cast(e.str, section.content!),\n            tokens: e.cast(e.int16, section.tokens!),\n            embedding: e.cast(e.OpenAIEmbedding, section.embedding!),\n          });\n        });\n      });\n\n      await query.run(client, { sections });\n      console.log(\"Embedding generation complete\");\n    }\n    // …\n\n\nPutting it all together\n-----------------------\n\nHere's the entire embeddings generation script. Copy and paste the whole thing\ninto your ``generate-embeddings.ts`` file.\n\n.. code-block:: typescript\n    :caption: generate-embeddings.ts\n\n    import { promises as fs } from \"fs\";\n    import { join } from \"path\";\n    import dotenv from \"dotenv\";\n    import { encode } from \"gpt-tokenizer\";\n    import * as gel from \"gel\";\n    import e from \"dbschema/edgeql-js\";\n    import { initOpenAIClient } from \"@/utils\";\n\n    dotenv.config({ path: \".env.local\" });\n\n    const openai = initOpenAIClient();\n\n    interface Section {\n      id?: string;\n      tokens: number;\n      content: string;\n      embedding: number[];\n    }\n\n    async function walk(dir: string): Promise<string[]> {\n      const entries = await fs.readdir(dir, { withFileTypes: true });\n\n      return (\n        await Promise.all(\n          entries.map((entry) => {\n            const path = join(dir, entry.name);\n            if (entry.isFile()) return [path];\n            else if (entry.isDirectory()) return walk(path);\n            return [];\n          })\n        )\n      ).flat();\n    }\n\n    async function prepareSectionsData(\n      sectionPaths: string[]\n    ): Promise<Section[]> {\n      const contents: string[] = [];\n      const sections: Section[] = [];\n\n      for (const path of sectionPaths) {\n        const content = await fs.readFile(path, \"utf8\");\n        // OpenAI recommends replacing newlines with spaces for best results\n        // when generating embeddings\n        const contentTrimmed = content.replace(/\\n/g, \" \");\n        contents.push(contentTrimmed);\n        sections.push({\n          content,\n          tokens: encode(content).length,\n          embedding: [],\n        });\n      }\n\n      const embeddingResponse = await openai.embeddings.create({\n        model: \"text-embedding-ada-002\",\n        input: contents,\n      });\n\n      embeddingResponse.data.forEach((item, i) => {\n        sections[i].embedding = item.embedding;\n      });\n\n      return sections;\n    }\n\n    async function storeEmbeddings() {\n      const client = gel.createClient();\n\n      const sectionPaths = await walk(\"docs\");\n\n      console.log(`Discovered ${sectionPaths.length} sections`);\n\n      const sections = await prepareSectionsData(sectionPaths);\n\n      // Delete old data from the DB.\n      await e.delete(e.Section).run(client);\n\n      // Bulk-insert all data into Gel database.\n      const query = e.params({ sections: e.json }, ({ sections }) => {\n        return e.for(e.json_array_unpack(sections), (section) => {\n          return e.insert(e.Section, {\n            content: e.cast(e.str, section.content!),\n            tokens: e.cast(e.int16, section.tokens!),\n            embedding: e.cast(e.OpenAIEmbedding, section.embedding!),\n          });\n        });\n      });\n\n      await query.run(client, { sections });\n      console.log(\"Embedding generation complete\");\n    }\n\n    (async function main() {\n      await storeEmbeddings();\n    })();\n\n\nRunning the script\n------------------\n\nLet's add a script to ``package.json`` that will invoke and execute\n``generate-embeddings.ts``.\n\n.. XXX gel version - fix\n.. code-block:: json-diff\n\n      {\n        \"name\": \"docs-chatbot\",\n        \"version\": \"0.1.0\",\n        \"private\": true,\n        \"scripts\": {\n          \"dev\": \"next dev\",\n          \"build\": \"next build\",\n          \"start\": \"next start\",\n    -     \"lint\": \"next lint\"\n    +     \"lint\": \"next lint\",\n    +     \"embeddings\": \"tsx generate-embeddings.ts\"\n        },\n        \"dependencies\": {\n          \"gel\": \"^1.3.5\",\n          \"next\": \"^13.4.19\",\n          \"openai\": \"^4.0.1\",\n          \"react\": \"18.2.0\",\n          \"react-dom\": \"18.2.0\",\n          \"typescript\": \"5.1.6\"\n        },\n        \"devDependencies\": {\n          \"@gel/generate\": \"^0.3.3\",\n          \"@types/node\": \"20.4.8\",\n          \"@types/react\": \"18.2.18\",\n          \"@types/react-dom\": \"18.2.7\",\n          \"autoprefixer\": \"10.4.14\",\n          \"dotenv\": \"^16.3.1\",\n          \"eslint\": \"8.46.0\",\n          \"eslint-config-next\": \"13.4.13\",\n          \"gpt-tokenizer\": \"^2.1.1\",\n          \"postcss\": \"8.4.27\",\n          \"tailwindcss\": \"3.3.3\",\n          \"tsx\": \"^3.12.7\"\n        }\n      }\n\nNow we can invoke the ``generate-embeddings.ts`` script from our terminal using\na simple command:\n\n.. code-block:: bash\n\n   $ npm run embeddings\n\nAfter the script finishes, open the Gel UI.\n\n.. code-block:: bash\n\n   $ npx gel ui\n\nOpen your \"main\" branch and switch to the Data Explorer tab. You should see\nthat the database has been updated with the embeddings and other relevant data.\n\n\nAnswering user questions\n========================\n\nNow that we have the content's embeddings stored, we can start working on the\nhandler for user questions. The user will submit a question to our server, and\nthe handler will send them an answer back. We will define a route and an HTTP\nrequest handler for this task. Thanks to the power of Next.js, we can do all of\nthis within our project using a `route handler`_.\n\n.. _route handler:\n  https://nextjs.org/docs/app/building-your-application/routing/route-handlers\n\nAs we write our handler, one important consideration is that answers can be\nquite long. We could wait on the server side to get the whole answer from\nOpenAI and then send it to the client, but that would feel slow to the user.\nOpenAI supports streaming, so instead we can send the answer to the client in\nchunks, as they arrive to the server. With this approach, the user doesn't have\nto wait for the entire response before they start getting feedback and our API\nseems faster.\n\nIn order to stream responses, we will use the browser's `server-sent events\n(SSE) API`_. Server-sent events enable a client to receive automatic updates\nfrom a server via an HTTP connection, and describes how the server maintains\ndata transmissions to a client once an initial client connection has been\nestablished. The client sends a request and with that request initiates a\nconnection with the server. The server then sends data back to the client in\nchunks until all of the data is sent, at which point it closes the connection.\n\n.. lint-off\n\n.. _server-sent events (SSE) API:\n  https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events\n\n.. lint-on\n\n\nNext.js route handler\n---------------------\n\nWhen using `Next.js's App Router <https://nextjs.org/docs/app>`_, route\nhandlers should be written inside an ``app/api`` folder. Every route should\nhave its own folder within that, and the handlers should be defined inside a\n``route.ts`` file inside the route's folder.\n\nLet's create a new folder for the answer generation route inside ``app/api``.\n\n.. code-block:: bash\n\n    $ mkdir app/api && cd app/api\n    $ mkdir generate-answer && touch generate-answer/route.ts\n\nWe also need to install the ``common-tags`` NPM package (and its corresponding\ntypes package) which gives us some useful template tags that we will use later\nwhen we create the prompt from user's question and related sections.\n\n.. code-block:: bash\n\n    $ npm install common-tags\n    $ npm install @types/common-tags --save-dev\n\nLet's talk briefly about runtimes. In the context of Next.js, \"runtime\" refers\nto the set of libraries, APIs, and general functionality available to your code\nduring execution. Next.js supports `Node.js and Edge runtimes`_. (The \"Edge\"\nruntime is coincidentally named but is not related to Gel.)\n\nStreaming is supported within both runtimes, but the implementation is a bit\nsimpler when using Edge, so that's what we will use here. The Edge runtime is\nbased on Web APIs. It has very low latency thanks to its minimal use of\nresources, but the downside is that it doesn't support native Node.js APIs.\n\n.. lint-off\n\n.. _Node.js and Edge runtimes:\n  https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes\n\n.. lint-on\n\nWe'll start by importing the modules we will need in the handler and\nwriting some configuration.\n\n.. note::\n\n    Like before, you may want to read along for understanding and copy/paste\n    the completed route at the end of this section.\n\n.. code-block:: typescript\n    :caption: app/api/generate-answer/route.ts\n\n    import { stripIndents, oneLineTrim } from \"common-tags\";\n    import * as gel from \"gel\";\n    import e from \"dbschema/edgeql-js\";\n    import { errors } from \"../../constants\";\n    import { initOpenAIClient } from \"@/utils\";\n\n    export const runtime = \"edge\";\n\n    const openai = initOpenAIClient();\n\n    const client = gel.createHttpClient();\n\n    export async function POST(req: Request) {\n        // …\n    }\n\n    // other functions that are called inside POST handler\n\n\nThe first imports are templates from the ``common-tags`` library we installed\nearlier. Then, we import the Gel binding. The third import is the query\nbuilder we described previously. We also import our errors and our OpenAI API\nclient initializer function.\n\nBy exporting ``runtime``, we override the Next.js default for this handler so\nthat Next.js will use the Edge runtime instead of the default Node.js runtime.\n\nWe're ready now to write the handler function for HTTP POST requests. To do\nthis in Next.js, you export a function named for the request method you want it\nto handle.\n\nOur POST handler calls other functions that we won't define just yet, but we'll\ncircle back to them later.\n\n.. code-block:: typescript\n    :caption: app/api/generate-answer/route.ts\n\n    // …\n\n    export async function POST(req: Request) {\n      try {\n        const { query } = await req.json();\n        const sanitizedQuery = query.trim();\n\n        const flagged = await isQueryFlagged(query);\n\n        if (flagged) throw new Error(errors.flagged);\n\n        const embedding = await getEmbedding(query);\n\n        const context = await getContext(embedding);\n\n        const prompt = createFullPrompt(sanitizedQuery, context);\n\n        const answer = await getOpenAiAnswer(prompt);\n\n        return new Response(answer.body, {\n          headers: {\n            \"Content-Type\": \"text/event-stream\",\n          },\n        });\n      } catch (error: any) {\n        console.error(error);\n\n        const uiError = error.message || errors.default;\n\n        return new Response(uiError, {\n          status: 500,\n          headers: { \"Content-Type\": \"application/json\" },\n        });\n      }\n    }\n\nOur handler will run the user's question through a few different steps as we\nbuild toward an answer.\n\n1. We check that the query complies with the OpenAI's `usage policies\n   <https://openai.com/policies/usage-policies>`_, which means that it should\n   not include any hateful, harassing, or violent content. This is handled by\n   our ``isQueryFlagged`` function.\n2. If the query fails, we throw. If it passes, we generate embeddings for it\n   using the OpenAI embedding API. This is handled by our ``getEmbedding``\n   function.\n3. We get related documentation sections from the Gel database. This is\n   handled by ``getContext``.\n4. We create the full prompt as our input to the chat completions API by\n   combining the question, related documentation sections, and a system\n   message.\n\n.. note::\n\n   The system message is a general instruction to the language model that it\n   should follow when answering any question.\n\nWith the input fully prepared, we call the chat completions API using the\npreviously generated prompt, and we stream the response we get from OpenAI\nto the user. In order to use streaming we need to provide the appropriate\n``content-type`` header: ``\"text/event-stream\"``. (You can see that in the\noptions object passed to the ``Response`` constructor.)\n\nTo keep things simple, we've wrapped most of these in a single\n``try``/``catch`` block. If any error occurs we send the error message to the\nuser with status 500. In practice, you may want to split this up and respond\nwith different status codes based on the outcome. For example, in the case the\nmoderation request returns an error, you may want to send back a ``400``\nresponse status (\"Bad Request\") instead of a ``500`` (\"Internal Server Error\").\n\nNow that you can see broadly what we're doing in this handler, let's dig into\neach of the functions we've called in it.\n\n\nModeration request\n^^^^^^^^^^^^^^^^^^\n\nLet's look at our moderation request function: ``isQueryFlagged``. We will use\nthe ``openai.moderations.create`` method.\n\n.. code-block:: typescript\n    :caption: app/api/generate-answer/route.ts\n\n    async function isQueryFlagged(query: string) {\n      const moderation = await openai.moderations.create({\n        input: query,\n      });\n\n      const [{ flagged }] = moderation.results;\n\n      return flagged;\n    }\n\nThe function is pretty straightforward: it takes the question (the ``query``\nparameter), fires off a moderation request to the API, unpacks ``flagged`` from\nthe results, and returns it.\n\nIf the API finds an issue with the user's question, the response will have the\n``flagged`` property set to ``true``. In that case we will throw a general\nerror back in the handler, but you could also inspect the response to find what\ncategories are problematic and include more info in the error.\n\nIf the question passes moderation then we can generate the embeddings for the\nquestion.\n\n\nEmbeddings generation request\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nFor the embeddings request, we will call the ``openai.embeddings.create``\nmethod, in a new function called ``getEmbedding``.\n\n.. code-block:: typescript\n    :caption: app/api/generate-answer/route.ts\n\n    async function getEmbedding(query: string) {\n      const embeddingResponse = await openai.embeddings.create({\n        model: \"text-embedding-ada-002\",\n        input: query.replaceAll(\"\\n\", \" \"),\n      });\n\n      const [{ embedding }] = embeddingResponse.data;\n\n      return embedding;\n    }\n\nThis new function again takes the question (as ``query``). We call the OpenAI\nlibrary's ``embeddings.create`` method, specifying the model to use for\ngeneration (the ``model`` property of the options passed to the method) and\npassing the input (``query`` with all newlines replaced by single spaces).\n\n\nGet related documentation sections request\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nLet's look at the database query that will give us back the related sections in\na variable named ``getSectionsQuery``.\n\n.. code-block:: typescript\n    :caption: app/api/generate-answer/route.ts\n\n    const getSectionsQuery = e.params(\n        {\n            target: e.OpenAIEmbedding,\n            matchThreshold: e.float64,\n            matchCount: e.int16,\n            minContentLength: e.int16,\n        },\n        (params) => {\n            return e.select(e.Section, (section) => {\n            const dist = e.ext.pgvector.cosine_distance(\n                section.embedding,\n                params.target\n            );\n            return {\n                content: true,\n                tokens: true,\n                dist,\n                filter: e.op(\n                    e.op(\n                      e.len(section.content),\n                      \">\",\n                      params.minContentLength\n                    ),\n                    \"and\",\n                    e.op(dist, \"<\", params.matchThreshold)\n                ),\n                order_by: {\n                    expression: dist,\n                    empty: e.EMPTY_LAST,\n                },\n                limit: params.matchCount,\n            };\n            });\n        }\n    );\n\nIn the above code, we use Gel's TypeScript query builder to create a query.\nThe query takes a few parameters:\n\n* ``target``: Embedding array to compare against to find related sections. In\n  this case, these will be the questions's embeddings we just generated.\n* ``matchThreshold``: Similarity threshold. Only matches with a similarity\n  score below this threshold will be returned. This will be a number between\n  ``0.0`` and ``2.0``. Values closer to ``0.0`` mean the documentation sections\n  must be very similar to the question while values closer to ``2.0`` allow for\n  more variance.\n* ``matchCount``: Maximum number of sections to return\n* ``minContentLength``: Minimum number of characters the sections should have\n  in order to be considered\n\nWe write a select query by calling ``e.select`` and passing it the type we want\nto select (``e.Section``). We return from that function an object representing\nthe shape we want back plus any other clauses we need: in this case, a filter,\nordering, and limit clause.\n\nWe use the ``cosine_distance`` function to calculate the similarity between the\nuser's question and our documentation sections. We have access to this function\nthrough Gel's pgvector extension. We then filter on that property by\ncomparing it to the ``matchThreshold`` value we will pass when executing the\nquery.\n\nWe want to get back the content and number of tokens for every related section\nthat passes the filter clause (i.e., has more than ``minContentLength`` tokens\nand a distance from the question embedding less than our ``matchThreshold``).\nWe want to order results in ascending order (which is the default) by how\nrelated they are to the question (represented as ``dist``) and to get back, at\nmost, ``matchCount`` sections.\n\nWe've written the query, but it won't help us until we execute it. We'll do\nthat in the ``getContext`` function.\n\n.. code-block:: typescript\n    :caption: app/api/generate-answer/route.ts\n\n    async function getContext(embedding: number[]) {\n        const sections = await getSectionsQuery.run(client, {\n            target: embedding,\n            matchThreshold: 0.3,\n            matchCount: 8,\n            minContentLength: 20,\n        });\n\n        let tokenCount = 0;\n        let context = \"\";\n\n        for (let i = 0; i < sections.length; i++) {\n            const section = sections[i];\n            const content = section.content;\n            tokenCount += section.tokens;\n\n            if (tokenCount >= 1500) {\n                tokenCount -= section.tokens;\n                break;\n            }\n\n            context += `${content.trim()}\\n---\\n`;\n        }\n\n        return context;\n    }\n\nThis function takes the embeddings of the question (the ``embedding``\nparameter) and returns the related documentation sections.\n\nWe start by running the query and passing in some values for the parameters:\n\n- the question embeddings that were passed to the function\n- a ``matchThreshold`` value of ``0.3``. You can tinker with this if you don't\n  like the results.\n- a ``matchCount``. We've chosen ``8`` here which represents the most sections\n  we'll get back.\n- a ``minContentLength`` of 20 characters\n\nWe then iterate through the sections that came back to prepare them to send on\nto the chat completions API. This involves incrementing the token count for the\ncurrent section, making sure the overall token count doesn't exceed our maximum\nof 1,500 for the context (to stay under the LLM's token limit), and, if the\ntoken count isn't exceeded, adding the trimmed content of this section to\n``context`` which we will ultimately return. Since we ordered this query by\n``dist`` ascending, and since lower ``dist`` values mean more similar sections,\nwe will be sure to get the most similar sections before we hit our token limit.\n\nWith our context ready, it's time to get our user their answer.\n\n\nChat completions request\n^^^^^^^^^^^^^^^^^^^^^^^^\n\nBefore we make our completion request, we will build the full input which\nconsists of the user's question, the related documentation, and the system\nmessage. The system message should tell the language model what tone to use\nwhen answering question and some general instructions on what is expected from\nit. With that you can give it some personality that it will bake into every\nresponse. We'll combine all of these parts in a function called\n``createFullPrompt``.\n\n.. lint-off\n\n.. code-block:: typescript\n    :caption: app/api/generate-answer/route.ts\n\n    function createFullPrompt(query: string, context: string) {\n        const systemMessage = `\n            As an enthusiastic Gel expert keen to assist,\n            respond to questions referencing the given Gel\n            sections.\n\n            If unable to help based on documentation, respond\n            with: \"Sorry, I don't know how to help with that.\"`;\n\n        return stripIndents`\n            ${oneLineTrim`${systemMessage}`}\n\n            Gel sections: \"\"\"\n            ${context}\n            \"\"\"\n\n            Question: \"\"\"\n            ${query}\n            \"\"\"`;\n    }\n\n.. lint-on\n\nThis function takes the question (as ``query``) and the related documentation\n(as ``context``), combines them with a system message, and formats it all\nnicely for easy consumption by the chat completions API.\n\nWe'll pass the prompt returned from that function as an argument to a new\nfunction (``getOpenAiAnswer``) that will get the answer from the OpenAI and\nreturn it.\n\n.. code-block:: typescript\n    :caption: app/api/generate-answer/route.ts\n\n    async function getOpenAiAnswer(prompt: string) {\n      const completion = await openai.chat.completions\n        .create({\n          model: \"gpt-3.5-turbo\",\n          messages: [{ role: \"user\", content: prompt }],\n          max_tokens: 1024,\n          temperature: 0.1,\n          stream: true,\n        })\n        .asResponse();\n\n      return completion;\n    }\n\nLet's take a look at the options we're sending through:\n\n* ``model``: The language model we want the chat completions API to use when\n  answering the question. (You can alternatively use ``gpt-4`` to if you have\n  access to it.)\n\n* ``messages``: We send the prompt as part of the messages property. It is\n  possible to send the system message on the first object of the array, with\n  ``role: system``, but since we also have the context sections as part of the\n  input, we will just send everything with the role ``user``.\n\n* ``max_tokens``: Maximum number of tokens to use for the answer.\n\n* ``temperature``: Number between 0 and 2. From `OpenAI's create chat\n  completion endpoint documentation`_: \"Higher values like 0.8 will make the\n  output more random, while lower values like 0.2 will make it more focused and\n  deterministic.\"\n\n* ``stream``: Setting this to ``true`` will have the API stream the response\n\n.. lint-off\n\n.. _OpenAI's create chat completion endpoint documentation:\n  https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature\n\n.. lint-on\n\n\nThe completed route\n^^^^^^^^^^^^^^^^^^^\n\nNow, let's take a look at the whole thing. Copy and paste this into your\n``app/api/generate-answer/route.ts`` file.\n\n.. code-block:: typescript\n    :caption: app/api/generate-answer/route.ts\n\n    import { stripIndents, oneLineTrim } from \"common-tags\";\n    import * as gel from \"gel\";\n    import e from \"dbschema/edgeql-js\";\n    import { errors } from \"../../constants\";\n    import { initOpenAIClient } from \"@/utils\";\n\n    export const runtime = \"edge\";\n\n    const openai = initOpenAIClient();\n\n    const client = gel.createHttpClient();\n\n    export async function POST(req: Request) {\n      try {\n        const { query } = await req.json();\n        const sanitizedQuery = query.trim();\n\n        const flagged = await isQueryFlagged(query);\n\n        if (flagged) throw new Error(errors.flagged);\n\n        const embedding = await getEmbedding(query);\n\n        const context = await getContext(embedding);\n\n        const prompt = createFullPrompt(sanitizedQuery, context);\n\n        const answer = await getOpenAiAnswer(prompt);\n\n        return new Response(answer.body, {\n          headers: {\n            \"Content-Type\": \"text/event-stream\",\n          },\n        });\n      } catch (error: any) {\n        console.error(error);\n\n        const uiError = error.message || errors.default;\n\n        return new Response(uiError, {\n          status: 500,\n          headers: { \"Content-Type\": \"application/json\" },\n        });\n      }\n    }\n\n    async function isQueryFlagged(query: string) {\n      const moderation = await openai.moderations.create({\n        input: query,\n      });\n\n      const [{ flagged }] = moderation.results;\n\n      return flagged;\n    }\n\n    async function getEmbedding(query: string) {\n      const embeddingResponse = await openai.embeddings.create({\n        model: \"text-embedding-ada-002\",\n        input: query.replaceAll(\"\\n\", \" \"),\n      });\n\n      const [{ embedding }] = embeddingResponse.data;\n\n      return embedding;\n    }\n\n    const getSectionsQuery = e.params(\n      {\n        target: e.OpenAIEmbedding,\n        matchThreshold: e.float64,\n        matchCount: e.int16,\n        minContentLength: e.int16,\n      },\n      (params) => {\n        return e.select(e.Section, (section) => {\n          const dist = e.ext.pgvector.cosine_distance(\n            section.embedding,\n            params.target\n          );\n          return {\n            content: true,\n            tokens: true,\n            dist,\n            filter: e.op(\n              e.op(\n                e.len(section.content),\n                \">\",\n                params.minContentLength\n              ),\n              \"and\",\n              e.op(dist, \"<\", params.matchThreshold)\n            ),\n            order_by: {\n              expression: dist,\n              empty: e.EMPTY_LAST,\n            },\n            limit: params.matchCount,\n          };\n        });\n      }\n    );\n\n    async function getContext(embedding: number[]) {\n      const sections = await getSectionsQuery.run(client, {\n        target: embedding,\n        matchThreshold: 0.3,\n        matchCount: 8,\n        minContentLength: 20,\n      });\n\n      let tokenCount = 0;\n      let context = \"\";\n\n      for (let i = 0; i < sections.length; i++) {\n        const section = sections[i];\n        const content = section.content;\n        tokenCount += section.tokens;\n\n        if (tokenCount >= 1500) {\n          tokenCount -= section.tokens;\n          break;\n        }\n\n        context += `${content.trim()}\\n---\\n`;\n      }\n\n      return context;\n    }\n\n    function createFullPrompt(query: string, context: string) {\n      const systemMessage = `\n            As an enthusiastic Gel expert keen to assist,\n            respond to questions referencing the given Gel\n            sections.\n\n            If unable to help based on documentation, respond\n            with: \"Sorry, I don't know how to help with that.\"`;\n\n      return stripIndents`\n            ${oneLineTrim`${systemMessage}`}\n\n            Gel sections: \"\"\"\n            ${context}\n            \"\"\"\n\n            Question: \"\"\"\n            ${query}\n            \"\"\"`;\n    }\n\n    async function getOpenAiAnswer(prompt: string) {\n      const completion = await openai.chat.completions\n        .create({\n          model: \"gpt-3.5-turbo\",\n          messages: [{ role: \"user\", content: prompt }],\n          max_tokens: 1024,\n          temperature: 0.1,\n          stream: true,\n        })\n        .asResponse();\n\n      return completion;\n    }\n\nWith the route complete, we can build the UI and connect everything together.\n\nBuilding the UI\n===============\n\nTo make things as simple as possible, we will just update the ``Home``\ncomponent that's inside ``app/page.tsx`` file. By default all components inside\nthe App Router are server components, but we want to have client-side\ninteractivity and dynamic updates. In order to do that we have to use a client\ncomponent for our ``Home`` component. The way to accomplish that is to convert\nthe ``page.tsx`` file to use the client component. We do that by adding the\n``use client`` directive to the top of the file.\n\n.. note::\n\n    Follow along for understanding and copy/paste the full component code at\n    the end of the section.\n\n.. code-block:: typescript\n    :caption: app/page.tsx\n\n    \"use client\";\n\nNow we build a simple UI for the chatbot.\n\n.. lint-off\n\n.. code-block:: typescript\n    :caption: app/page.tsx\n\n    import { useState } from \"react\";\n    import { errors } from \"./constants\";\n\n    export default function Home() {\n        const [prompt, setPrompt] = useState(\"\");\n        const [question, setQuestion] = useState(\"\");\n        const [answer, setAnswer] = useState<string>(\"\");\n        const [isLoading, setIsLoading] = useState(false);\n        const [error, setError] = useState<string | undefined>(undefined);\n\n        const handleSubmit = () => {};\n\n        return (\n        <main className=\"w-screen h-screen flex items-center justify-center bg-[#2e2e2e]\">\n            <form className=\"bg-[#2e2e2e] w-[540px] relative\">\n            <input\n                className={`py-5 pl-6 pr-[40px] rounded-md bg-[#1f1f1f] w-full\n                outline-[#1f1f1f] focus:outline outline-offset-2 text-[#b3b3b3]\n                mb-8 placeholder-[#4d4d4d]`}\n                placeholder=\"Ask a question...\"\n                value={prompt}\n                onChange={(e) => {\n                  setPrompt(e.target.value);\n                }}\n            ></input>\n            <button\n                onClick={handleSubmit}\n                className=\"absolute top-[25px] right-4\"\n                disabled={!prompt}\n            >\n                <ReturnIcon\n                className={`${!prompt ? \"fill-[#4d4d4d]\" : \"fill-[#1b9873]\"}`}\n                />\n            </button>\n            <div className=\"h-96 px-6\">\n                {question && (\n                <p className=\"text-[#b3b3b3] pb-4 mb-8 border-b border-[#525252] \">\n                    {question}\n                </p>\n                )}\n                {(isLoading && <LoadingDots />) ||\n                (error && <p className=\"text-[#b3b3b3]\">{error}</p>) ||\n                (answer && <p className=\"text-[#b3b3b3]\">{answer}</p>)}\n            </div>\n            </form>\n        </main>\n        );\n    }\n\n    function ReturnIcon({ className }: { className?: string }) {\n        return (\n            <svg\n                width=\"20\"\n                height=\"12\"\n                viewBox=\"0 0 20 12\"\n                fill=\"none\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n                className={className}\n            >\n                <path\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d={`M12 0C11.4477 0 11 0.447715 11 1C11 1.55228 11.4477 2 12\n                2H17C17.5523 2 18 2.44771 18 3V6C18 6.55229 17.5523 7 17\n                7H3.41436L4.70726 5.70711C5.09778 5.31658 5.09778 4.68342 4.70726\n                4.29289C4.31673 3.90237 3.68357 3.90237 3.29304 4.29289L0.306297\n                7.27964L0.292893 7.2928C0.18663 7.39906 0.109281 7.52329 0.0608469\n                7.65571C0.0214847 7.76305 0 7.87902 0 8C0 8.23166 0.078771 8.44492\n                0.210989 8.61445C0.23874 8.65004 0.268845 8.68369 0.30107\n                8.71519L3.29289 11.707C3.68342 12.0975 4.31658 12.0975 4.70711\n                11.707C5.09763 11.3165 5.09763 10.6833 4.70711 10.2928L3.41431\n                9H17C18.6568 9 20 7.65685 20 6V3C20 1.34315 18.6568 0 17 0H12Z`}\n                />\n            </svg>\n        );\n    }\n\n    function LoadingDots() {\n        return (\n            <div className=\"grid gap-2\">\n                <div className=\"flex items-center space-x-2 animate-pulse\">\n                <div className=\"w-1 h-1 bg-[#b3b3b3] rounded-full\"></div>\n                <div className=\"w-1 h-1 bg-[#b3b3b3] rounded-full\"></div>\n                <div className=\"w-1 h-1 bg-[#b3b3b3] rounded-full\"></div>\n                </div>\n            </div>\n        );\n    }\n\n.. lint-on\n\nWe have created an input field where the user can enter a question. The text\nthe user types in the input field is captured as ``prompt``. ``question`` is\nthe submitted prompt that we show under the input when user submits their\nquestion. We clear the input and delete the prompt when user submits it, but\nkeep the ``question`` value so the user can reference it.\n\nLet's look at the fleshed-out form submission handler function that we stubbed\nin earlier:\n\n.. code-block:: typescript\n    :caption: app/page.tsx\n\n    const handleSubmit = (\n      e: KeyboardEvent | React.MouseEvent<HTMLButtonElement>\n    ) => {\n      e.preventDefault();\n\n      setIsLoading(true);\n      setQuestion(prompt);\n      setAnswer(\"\");\n      setPrompt(\"\");\n      generateAnswer(prompt);\n    };\n\nWhen the user submits a question, we set the ``isLoading`` state to ``true``\nand show the loading indicator. We clear the prompt state and set the question\nstate. We also clear the answer state because the answer may hold an answer to\na previous question, but we want to start with an empty answer.\n\nAt this point we want to create a server-sent event and send a request to our\n``api/generate-answer`` route. We will do this inside the ``generateAnswer``\nfunction.\n\nThe browser-native SSE API doesn't allow the client to send a payload to the\nserver; the client is only able to open a connection to the server to begin\nreceiving events from it via a GET request. In order for the client to be able\nto send a payload via a POST request to open the SSE connection, we will use\nthe `sse.js <https://npm.io/package/sse.js>`_ package, so let's install it.\n\n.. code-block:: bash\n\n    $ npm install sse.js\n\nThis package doesn't have a corresponding types package, so we need to add them\nmanually. Let's create a new folder named ``types`` in the project root and\nan ``sse.d.ts`` file inside it.\n\n.. code-block:: bash\n\n    $ mkdir types && touch types/sse.d.ts\n\nOpen ``sse.d.ts`` and add this code:\n\n.. code-block:: typescript\n    :caption: types/sse.d.ts\n\n    type SSEOptions = EventSourceInit & {\n        payload?: string;\n    };\n\n    declare module \"sse.js\" {\n        class SSE extends EventSource {\n            constructor(url: string | URL, sseOptions?: SSEOptions);\n            stream(): void;\n        }\n    }\n\nThis extends the native ``EventStream`` by adding a payload to the constructor.\nWe also added the ``stream`` function to it which is used to activate the\nstream in the sse.js library.\n\nThis allows us to import ``SSE`` in ``page.tsx`` and use it to open a\nconnection to our handler route while also sending the user's query.\n\n.. code-block:: typescript-diff\n\n      \"use client\";\n\n    - import { useState } from \"react\";\n    + import { useState, useRef } from \"react\";\n    + import { SSE } from \"sse.js\";\n      import { errors } from \"./constants\";\n\n      export default function Home() {\n    +     const eventSourceRef = useRef<SSE>();\n    +\n          const [prompt, setPrompt] = useState(\"\");\n          const [question, setQuestion] = useState(\"\");\n          const [answer, setAnswer] = useState<string>(\"\");\n          const [isLoading, setIsLoading] = useState(false);\n          const [error, setError] = useState<string | undefined>(undefined);\n\n          const handleSubmit = () => {};\n    +\n    +     const generateAnswer = async (query: string) => {\n    +         if (eventSourceRef.current) eventSourceRef.current.close();\n    +\n    +         const eventSource = new SSE(`api/generate-answer`, {\n    +             payload: JSON.stringify({ query }),\n    +         });\n    +         eventSourceRef.current = eventSource;\n    +\n    +         eventSource.onerror = handleError;\n    +         eventSource.onmessage = handleMessage;\n    +         eventSource.stream();\n    +     };\n    +\n    +     handleError() { /* … */ }\n    +     handleMessage() { /* … */ }\n      // …\n\nNote that we save a reference to the ``eventSource`` object. We need this in\ncase a user submits a new question while answer to the previous one is still\nassembling on the client. If we don't close the existing connection to the\nserver before opening the new one, this could cause problems since two\nconnections will be open and trying to receive data.\n\nWe opened a connection to the server, and we are now ready to receive events\nfrom it. We just need to write handlers for those events so the UI knows what\nto do with them. We will get the answer as part of a message event, and if an\nerror is returned, the server will send an error event to the client.\n\nLet's break down these handlers.\n\n.. code-block:: typescript\n    :caption: app/page.tsx\n\n    // …\n\n    function handleError(err: any) {\n        setIsLoading(false);\n\n        const errMessage =\n        err.data === errors.flagged ? errors.flagged : errors.default;\n\n        setError(errMessage);\n    }\n\n\n    function handleMessage(e: MessageEvent<any>) {\n        try {\n            setIsLoading(false);\n            if (e.data === \"[DONE]\") return;\n\n            const chunkResponse = JSON.parse(e.data);\n            const chunk = chunkResponse.choices[0].delta?.content || \"\";\n            setAnswer((answer) => answer + chunk);\n        } catch (err) {\n            handleError(err);\n        }\n    }\n\nWhen we get the message event, we extract the data from it and add it to the\n``answer`` state until we receive all chunks. This is indicated when the data\nis equal to ``[DONE]``, meaning the whole answer has been received and the\nconnection to the server will be closed. There is no data to be parsed in this\ncase, so we return instead of trying to parse it. (An error will be thrown if\nwe try to parse it in this case.)\n\n\nThe completed UI\n----------------\n\nPut all that together, and you have this (which can be copy/pasted to\n``app/page.tsx``):\n\n.. lint-off\n\n.. code-block:: typescript\n    :caption: app/page.tsx\n\n    \"use client\";\n\n    import { useState, useRef } from \"react\";\n    import { SSE } from \"sse.js\";\n    import { errors } from \"./constants\";\n\n    export default function Home() {\n      const eventSourceRef = useRef<SSE>();\n\n      const [prompt, setPrompt] = useState(\"\");\n      const [question, setQuestion] = useState(\"\");\n      const [answer, setAnswer] = useState<string>(\"\");\n      const [isLoading, setIsLoading] = useState(false);\n      const [error, setError] = useState<string | undefined>(undefined);\n\n      const handleSubmit = (\n        e: KeyboardEvent | React.MouseEvent<HTMLButtonElement>\n      ) => {\n        e.preventDefault();\n\n        setIsLoading(true);\n        setQuestion(prompt);\n        setAnswer(\"\");\n        setPrompt(\"\");\n        generateAnswer(prompt);\n      };\n\n      const generateAnswer = async (query: string) => {\n        if (eventSourceRef.current) eventSourceRef.current.close();\n\n        const eventSource = new SSE(`api/generate-answer`, {\n          payload: JSON.stringify({ query }),\n        });\n        eventSourceRef.current = eventSource;\n\n        eventSource.onerror = handleError;\n        eventSource.onmessage = handleMessage;\n        eventSource.stream();\n      };\n\n      function handleError(err: any) {\n        setIsLoading(false);\n\n        const errMessage =\n          err.data === errors.flagged ? errors.flagged : errors.default;\n\n        setError(errMessage);\n      }\n\n      function handleMessage(e: MessageEvent<any>) {\n        try {\n          setIsLoading(false);\n          if (e.data === \"[DONE]\") return;\n\n          const chunkResponse = JSON.parse(e.data);\n          const chunk = chunkResponse.choices[0].delta?.content || \"\";\n          setAnswer((answer) => answer + chunk);\n        } catch (err) {\n          handleError(err);\n        }\n      }\n\n      return (\n        <main className=\"w-screen h-screen flex items-center justify-center bg-[#2e2e2e]\">\n          <form className=\"bg-[#2e2e2e] w-[540px] relative\">\n            <input\n              className={`py-5 pl-6 pr-[40px] rounded-md bg-[#1f1f1f] w-full\n                outline-[#1f1f1f] focus:outline outline-offset-2 text-[#b3b3b3]\n                mb-8 placeholder-[#4d4d4d]`}\n              placeholder=\"Ask a question...\"\n              value={prompt}\n              onChange={(e) => {\n                setPrompt(e.target.value);\n              }}\n            ></input>\n            <button\n              onClick={handleSubmit}\n              className=\"absolute top-[25px] right-4\"\n              disabled={!prompt}\n            >\n              <ReturnIcon\n                className={`${!prompt ? \"fill-[#4d4d4d]\" : \"fill-[#1b9873]\"}`}\n              />\n            </button>\n            <div className=\"h-96 px-6\">\n              {question && (\n                <p className=\"text-[#b3b3b3] pb-4 mb-8 border-b border-[#525252] \">\n                  {question}\n                </p>\n              )}\n              {(isLoading && <LoadingDots />) ||\n                (error && <p className=\"text-[#b3b3b3]\">{error}</p>) ||\n                (answer && <p className=\"text-[#b3b3b3]\">{answer}</p>)}\n            </div>\n          </form>\n        </main>\n      );\n    }\n\n    function ReturnIcon({ className }: { className?: string }) {\n      return (\n        <svg\n          width=\"20\"\n          height=\"12\"\n          viewBox=\"0 0 20 12\"\n          fill=\"none\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          className={className}\n        >\n          <path\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d={`M12 0C11.4477 0 11 0.447715 11 1C11 1.55228 11.4477 2 12\n                2H17C17.5523 2 18 2.44771 18 3V6C18 6.55229 17.5523 7 17\n                7H3.41436L4.70726 5.70711C5.09778 5.31658 5.09778 4.68342 4.70726\n                4.29289C4.31673 3.90237 3.68357 3.90237 3.29304 4.29289L0.306297\n                7.27964L0.292893 7.2928C0.18663 7.39906 0.109281 7.52329 0.0608469\n                7.65571C0.0214847 7.76305 0 7.87902 0 8C0 8.23166 0.078771 8.44492\n                0.210989 8.61445C0.23874 8.65004 0.268845 8.68369 0.30107\n                8.71519L3.29289 11.707C3.68342 12.0975 4.31658 12.0975 4.70711\n                11.707C5.09763 11.3165 5.09763 10.6833 4.70711 10.2928L3.41431\n                9H17C18.6568 9 20 7.65685 20 6V3C20 1.34315 18.6568 0 17 0H12Z`}\n          />\n        </svg>\n      );\n    }\n\n    function LoadingDots() {\n      return (\n        <div className=\"grid gap-2\">\n          <div className=\"flex items-center space-x-2 animate-pulse\">\n            <div className=\"w-1 h-1 bg-[#b3b3b3] rounded-full\"></div>\n            <div className=\"w-1 h-1 bg-[#b3b3b3] rounded-full\"></div>\n            <div className=\"w-1 h-1 bg-[#b3b3b3] rounded-full\"></div>\n          </div>\n        </div>\n      );\n    }\n\n.. lint-on\n\nWith that, the UI can now get answers from the Next.js route. The build is\ncomplete, and it's time to try it out!\n\n\nTesting it out\n==============\n\nYou should now be able to run the project to test it.\n\n.. code-block:: bash\n\n    $ npm run dev\n\nIf you used our example documentation, the chatbot will know a few things about\nEdgeQL along with whatever it was trained on.\n\nSome questions you might try:\n\n- \"What is EdgeQL?\"\n- \"Who is EdgeQL for?\"\n- \"How should I get started with EdgeQL?\"\n\nIf you don't like the responses you're getting, here are a few things you might\ntry tweaking:\n\n- ``systemMessage`` in the ``createFullPrompt`` function in\n  ``app/api/generate-answer/route.ts``\n- ``temperature`` in the ``getOpenAiAnswer`` in\n  ``app/api/generate-answer/route.ts``\n- the ``matchThreshold`` value passed to the query from the ``getContext``\n  function in ``app/api/generate-answer/route.ts``\n\nYou can see the finished source code for this build in `our examples repo on\nGitHub <https://github.com/geldata/gel-examples/tree/main/docs-chatbot>`_.\nYou might also find our actual implementation interesting. You'll find it in\n`our website repo <https://github.com/geldata/website>`_. Pay close attention to\nthe contents of `buildTools/gpt\n<https://github.com/geldata/website/tree/main/buildTools/gpt>`_, where the\nembedding generation happens and `components/gpt\n<https://github.com/geldata/website/tree/main/components/gpt>`_, which contains\nmost of the UI for our chatbot.\n\nIf you have trouble with the build or just want to hang out with other Gel\nusers, please join `our awesome community on Discord\n<https://discord.gg/umUueND6ag>`_!\n"
  },
  {
    "path": "docs/resources/guides/tutorials/cloudflare_workers.rst",
    "content": ".. _ref_guide_cloudflare_workers:\n\n==================\nCloudflare Workers\n==================\n\n:edb-alt-title: Using Gel in Cloudflare Workers\n\n\nThis guide demonstrates how to integrate Gel with Cloudflare Workers to\nbuild serverless applications that can interact with Gel.\n\nIt covers the following:\n\n- Setting up a new Cloudflare Worker project\n- Configuring Gel\n- Using Gel in a Cloudflare Worker\n- Deploying the Worker to Cloudflare\n\nYou can use this project as a reference: `Gel Cloudflare Workers Example`_.\n\nPrerequisites\n-------------\n\n`Sign up for a Cloudflare account`_ to later deploy your worker.\n\nEnsure you have the following installed:\n\n- `Node.js`_\n- :ref:`Gel CLI <ref_intro_cli>` or juse ``$ npx gel``\n\n.. _Sign up for a Cloudflare account: https://dash.cloudflare.com/sign-up\n.. _Node.js: https://nodejs.org/en/\n\nSetup and configuration\n-----------------------\n\nInitialize a New Cloudflare Worker Project\n===========================================\n\nUse the `create-cloudflare`_ package to create a new Cloudflare Worker project.\n\n.. _create-cloudflare: https://www.npmjs.com/package/create-cloudflare\n\n.. code-block:: bash\n\n    $ npm create cloudflare@latest # or pnpm, yarn, bun\n\n    # or\n    $ npx create-cloudflare@latest\n\nAnswer the prompts to create a new project. Pick the *\"Hello World\" Worker*\ntemplate to get started.\n\nYou'll be asked if you want to put your project on Cloudflare.\nIf you say yes, you'll need to sign in (if you haven't already).\nIf you don't want to deploy right away, switch to the project folder\nyou just made to start writing your code. When you're ready to deploy your\nproject on Cloudflare, you can run ``npx wrangler deploy`` to push it.\n\n.. note:: Using Wrangler CLI\n\n    If you prefer using `Wrangler`_ to set up your worker, you can use the\n    :code:`wrangler generate` command to create a new project.\n\n.. _Wrangler: https://developers.cloudflare.com/workers/cli-wrangler\n\n\nConfigure Gel\n=============\n\nYou can use `Gel Cloud`_ for a managed service or run Gel locally.\n\n.. _`Gel Cloud`: https://www.geldata.com/cloud\n\n**Local Gel Setup (Optional for Gel Cloud Users)**\n\nIf you're running Gel locally, you can use the following command\nto create a new instance:\n\n.. code-block:: bash\n\n    $ gel project init\n\nIt creates an |gel.toml| config file and a schema file\n:code:`dbschema/default.gel`.\n\nIt also spins up a Gel instance and associates it with the current\ndirectory.\nAs long as you're inside the project directory, all CLI commands will\nbe executed against this instance.\n\nYou can run |gelcmd| in your terminal to open an interactive REPL to your\ninstance.\n\n.. code-block:: bash\n\n    $ gel\n\n    # or\n    $ npx gel\n\n**Install the Gel npm package**\n\n.. code-block:: bash\n\n    $ npm install gel # or pnpm, yarn, bun\n\n**Extend The Default Schema (Optional)**\n\nYou can extend the default schema, :code:`dbschema/default.gel`, to define\nyour data model, and then try it out in the Cloudflare Worker code.\n\nAdd new types to the schema file:\n\n.. code-block:: sdl\n\n    module default {\n      type Movie {\n        required title: str {\n          constraint exclusive;\n        };\n        multi actors: Person;\n      }\n\n      type Person {\n        required name: str;\n      }\n    }\n\nThen apply the schema schema to your Gel instance:\n\n.. code-block:: bash\n\n    $ gel migration create\n    $ gel migrate\n\nUsing Gel in a Cloudflare Worker\n================================\n\nOpen the :code:`index.ts` file from the :code:`src` directory in your project,\nand remove the default code.\n\nTo interact with your **local Gel instance**, use the following code:\n\n.. code-block:: typescript\n\n    import * as gel from \"gel\";\n\n    export default {\n      async fetch(\n        _request: Request,\n        env: Env,\n        ctx: ExecutionContext,\n      ): Promise<Response> {\n        const client = gel.createHttpClient({\n          tlsSecurity: \"insecure\",\n          dsn: \"<your-gel-dsn>\",\n        });\n        const movies = await client.query(`select Movie { title }`);\n        return new Response(JSON.stringify(movies, null, 2), {\n          headers: {\n            \"content-type\": \"application/json;charset=UTF-8\",\n          },\n        });\n      },\n    } satisfies ExportedHandler<Env>;\n\n\n.. note:: Gel DSN\n\n    Replace :code:`<your-gel-dsn>` with your Gel DSN.\n    You can obtain your Gel DSN from the command line by running:\n\n    .. code-block:: bash\n\n        $ gel instance credentials --insecure-dsn\n\n.. note:: tlsSecurity\n\n    The :code:`tlsSecurity` option is set to :code:`insecure` to allow\n    connections to a local Gel instance. This lets you test your\n    Cloudflare Worker locally. **Don't use this option in production.**\n\n**Client Setup with Gel Cloud**\n\nIf you're using Gel Cloud, you can instead use the following code to\nset up the client:\n\n.. code-block:: typescript\n\n   const client = gel.createHttpClient({\n     instanceName: env.GEL_INSTANCE,\n     secretKey: env.GEL_SECRET_KEY,\n   });\n\n.. note:: Environment variables\n\n    You can obtain :gelenv:`INSTANCE` and :gelenv:`SECRET_KEY`\n    values from the Gel Cloud dashboard.\n\nYou will need to set the :gelenv:`INSTANCE` and :gelenv:`SECRET_KEY`\nenvironment variables in your Cloudflare Worker project.\n\nAdd the following to your :code:`wrangler.toml` file:\n\n.. code-block:: toml\n\n    [vars]\n    GEL_INSTANCE = \"your-gel-instance\"\n    GEL_SECRET_KEY = \"your-gel-secret-key\"\n\nNext, you can run :code:`wrangler types` to generate the types for your\nenvironment variables.\n\n**Running the Worker**\n\n.. note:: Adding polyfills for Node.js\n\n    The :code:`gel` package currently uses Node.js built-in modules\n    that are not available in the Cloudflare Worker environment.\n    You have to add the following line to your :code:`wrangler.toml` file\n    to include the polyfills:\n\n    .. code-block:: toml\n\n        node_compat = true\n\nTo run the worker locally, use the following command:\n\n.. code-block:: bash\n\n    $ npm run dev # or pnpm, yarn, bun\n\nThis will start a local server at :code:`http://localhost:8787`.\nRun :code:`curl http://localhost:8787` to see the response.\n\n**Deploying the Worker to Cloudflare**\n\nTo deploy the worker to Cloudflare, use the following command:\n\n.. code-block:: bash\n\n    $ npm run deploy # or pnpm, yarn, bun\n\nThis will deploy the worker to Cloudflare and provide you with a URL\nto access your worker.\n\nWrapping up\n===========\n\nCongratulations! You have successfully integrated Gel with\nCloudflare Workers.\n\nHere's a minimal starter project that you can use as a\nreference: `Gel Cloudflare Workers Example`_.\n\nCheck out the `Cloudflare Workers documentation`_ for more information and\nto learn about the various features and capabilities of Cloudflare Workers.\n\n.. _`Gel Cloudflare Workers Example`:\n  https://github.com/geldata/gel-examples/tree/main/cloudflare-workers\n.. _`Cloudflare Workers documentation`:\n  https://developers.cloudflare.com/workers\n"
  },
  {
    "path": "docs/resources/guides/tutorials/graphql_apis_with_strawberry.rst",
    "content": "==========\nStrawberry\n==========\n\n:edb-alt-title: Building a GraphQL API with Gel and Strawberry\n\n|Gel| allows you to query your database with GraphQL via the built-in GraphQL\nextension. It enables you to expose GraphQL-driven CRUD APIs for all object\ntypes, their properties, links, and aliases. This opens up the scope for\ncreating backend-less applications where the users will directly communicate\nwith the database. You can learn more about that in the\n:ref:`GraphQL <ref_graphql_index>` section of the docs.\n\nHowever, as of now, Gel is not ready to be used as a standalone backend. You\nshouldn't expose your Gel instance directly to the application's frontend;\nthis is insecure and will give all users full read/write access to your\ndatabase. So, in this tutorial, we'll see how you can quickly create a simple\nGraphQL API without using the built-in extension, which will give the users\nrestricted access to the database schema. Also, we'll implement HTTP basic\nauthentication and demonstrate how you can write your own GraphQL validators\nand resolvers. This tutorial assumes you're already familiar with GraphQL terms\nlike schema, query, mutation, resolver, validator, etc, and have used GraphQL\nwith some other technology before.\n\nWe'll build the same movie organization system that we used in the Flask\n:ref:`tutorial <ref_guide_rest_apis_with_flask>`\nand expose the objects and relationships as a GraphQL API. Using the GraphQL\ninterface, you'll be able to fetch, create, update, and delete movie and actor\nobjects in the database. `Strawberry <https://strawberry.rocks/>`_ is a Python\nlibrary that takes a code-first approach where you'll write your object schema\nas Python classes. This allows us to focus more on how you can integrate Gel\ninto your workflow and less on the idiosyncrasies of GraphQL itself. We'll also\nuse the Gel client to communicate with the database,\n`FastAPI <https://fastapi.tiangolo.com/>`_ to build the authentication layer,\nand Uvicorn as the webserver.\n\nPrerequisites\n=============\n\nBefore we start, make sure you have :ref:`installed <ref_admin_install>` the\n|gelcmd| command-line tool. Here, we'll use Python 3.10 and a few of its\nlatest features while building the APIs. A working version of this tutorial can\nbe found `on Github\n<https://github.com/geldata/gel-examples/tree/main/strawberry-gql>`_.\n\n\nInstall the dependencies\n^^^^^^^^^^^^^^^^^^^^^^^^\n\nTo follow along, clone the repository and head over to the ``strawberry-gql``\ndirectory.\n\n\n.. code-block:: bash\n\n    $ git clone git@github.com:geldata/gel-examples.git\n    $ cd gel-examples/strawberry-gql\n\nCreate a Python 3.10 virtual environment, activate it, and install the\ndependencies with this command:\n\n.. code-block:: bash\n\n    $ python3.10 -m venv .venv\n    $ source .venv/bin/activate\n    $ pip install gel fastapi strawberry-graphql uvicorn[standard]\n\n\nInitialize the database\n^^^^^^^^^^^^^^^^^^^^^^^\n\nNow, let's initialize a Gel project. From the project's root directory:\n\n.. code-block:: bash\n\n    $ gel project init\n    Initializing project...\n\n    Specify the name of Gel instance to use with this project\n    [default: strawberry_crud]:\n    > strawberry_crud\n\n    Do you want to start instance automatically on login? [y/n]\n    > y\n    Checking Gel versions...\n\nOnce you've answered the prompts, a new Gel instance called\n``strawberry_crud`` will be created and started.\n\n\nConnect to the database\n^^^^^^^^^^^^^^^^^^^^^^^\n\nLet's test that we can connect to the newly started instance. To do so, run:\n\n.. code-block:: bash\n\n    $ gel\n\nYou should be connected to the database instance and able to see a prompt\nsimilar to this:\n\n::\n\n    Gel x.x (repl x.x)\n    Type \\help for help, \\quit to quit.\n    gel>\n\nYou can start writing queries here. However, the database is currently\nempty. Let's start designing the data model.\n\nSchema design\n=============\n\nThe movie organization system will have two object types—**movies** and\n**actors**. Each *movie* can have links to multiple *actors*. The goal is to\ncreate a GraphQL API suite that'll allow us to fetch, create, update, and\ndelete the objects while maintaining their relationships.\n\n|Gel| allows us to declaratively define the structure of the objects. The\nschema lives inside |.gel| file in the ``dbschema`` directory. It's\ncommon to declare the entire schema in a single file :dotgel:`dbschema/default`.\nThis is how our datatypes look:\n\n.. code-block:: sdl\n\n    # dbschema/default.gel\n\n    module default {\n      abstract type Auditable {\n        property created_at -> datetime {\n          readonly := true;\n          default := datetime_current();\n        }\n      }\n\n      type Actor extending Auditable {\n        required property name -> str {\n          constraint max_len_value(50);\n        }\n        property age -> int16 {\n          constraint min_value(0);\n          constraint max_value(100);\n        }\n        property height -> int16 {\n          constraint min_value(0);\n          constraint max_value(300);\n        }\n      }\n\n      type Movie extending Auditable {\n        required property name -> str {\n          constraint max_len_value(50);\n        }\n        property year -> int16{\n          constraint min_value(1850);\n        };\n        multi link actors -> Actor;\n      }\n    }\n\n\nHere, we've defined an ``abstract`` type called ``Auditable`` to take advantage\nof Gel's schema mixin system. This allows us to add a ``created_at``\nproperty to multiple types without repeating ourselves.\n\nThe ``Actor`` type extends ``Auditable`` and inherits the ``created_at``\nproperty as a result. This property is auto-filled via the ``datetime_current``\nfunction. Along with the inherited type, the actor type also defines a few\nadditional properties like called ``name``, ``age``, and ``height``. The\nconstraints on the properties make sure that actor names can't be longer than\n50 characters, age must be between 0 to 100 years, and finally, height must be\nbetween 0 to 300 centimeters.\n\nWe also define a ``Movie`` type that extends the ``Auditable`` abstract type.\nIt also contains some additional concrete properties and links: ``name``,\n``year``, and an optional multi-link called ``actors`` which refers to the\n``Actor`` objects.\n\n\nBuild the GraphQL API\n=====================\n\nThe API endpoints are defined in the ``app`` directory. The directory structure\nlooks as follows:\n\n::\n\n    app\n    ├── __init__.py\n    ├── main.py\n    └── schemas.py\n\nThe ``schemas.py`` module contains the code that defines the GraphQL schema and\nbuilds the queries and mutations for ``Actor`` and ``Movie`` objects. The\n``main.py`` module then registers the GraphQL schema, adds the authentication\nlayer, and exposes the API to the webserver.\n\n\nWrite the GraphQL schema\n^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nAlong with the database schema, to expose Gel's object relational model as a\nGraphQL API, you'll also have to define a GraphQL schema that mirrors the\nobject structure in the database. Strawberry allows us to express this schema\nvia type annotated Python classes. We define the Strawberry schema in the\n``schema.py`` file as follows:\n\n.. code-block:: python\n\n    # strawberry-gql/app/schema.py\n    from __future__ import annotations\n\n    import json # will be used later for serialization\n\n    import gel\n    import strawberry\n\n    client = gel.create_async_client()\n\n\n    @strawberry.type\n    class Actor:\n        name: str | None\n        age: int | None = None\n        height: int | None = None\n\n\n    @strawberry.type\n    class Movie:\n        name: str | None\n        year: int | None = None\n        actors: list[Actor] | None = None\n\nHere, the GraphQL schema mimics our database schema. Similar to the ``Actor``\nand ``Movie`` types in the Gel schema, here, both the ``Actor`` and\n``Movie`` models have three attributes. Likewise, the ``actors`` attribute in\nthe ``Movie`` model represents the link between movies and actors.\n\n\nQuery actors\n^^^^^^^^^^^^\n\nIn this section, we'll write the resolver to create the queries that'll\nallow us to fetch the actor objects from the database. You'll need to write the\nquery resolvers as methods in a class decorated with the ``@strawberry.type``\ndecorator. Each method will also need to be decorated with the\n``@strawberry.field`` decorator to mark them as resolvers. Resolvers can be\neither sync or async. In this particular case, we'll write asynchronous\nresolvers that'll act in a non-blocking manner. The query to fetch the actors\nis built in the ``schema.py`` file as follows:\n\n.. code-block:: python\n\n    # strawberry-gql/app/schema.py\n    ...\n\n    @strawberry.type\n    class Query:\n        @strawberry.field\n        async def get_actors(\n            self, filter_name: str | None = None\n        ) -> list[Actor]:\n\n            if filter_name:\n                actors_json = await client.query_json(\n                    \"\"\"\n                    select Actor {name, age, height}\n                    filter .name=<str>$filter_name\n                \"\"\",\n                    filter_name=filter_name,\n                )\n            else:\n                actors_json = await client.query_json(\n                    \"\"\"\n                    select Actor {name, age, height}\n                \"\"\"\n                )\n            actors = json.loads(actors_json)\n            return [\n                Actor(name, age, height)\n                for (name, age, height) in (\n                    d.values() for d in actors\n                )\n            ]\n\n    # Register the Query.\n    schema = strawberry.Schema(query=Query)\n\nHere, the ``get_actors`` resolver method accepts an optional ``filter_name``\nparameter and returns a list of ``Actor`` type objects. The optional\n``filter_name`` parameter allows us to build the capability of filtering the\nactor objects by name. Inside the method, we use the Gel client to\nasynchronously query the data. The ``client.query_json`` method returns JSON\nserialized data which we use to create the ``Actor`` instances. Finally, we\nreturn the list of actor instances and the rest of the work is done by\nStrawberry. Then in the last line of the above snippet, we register the\n``Query`` class to build the ``Schema`` instance.\n\nAfterward, in the ``main.py`` module, we use FastAPI to expose the ``/graphql``\nendpoint. Also, we add a basic HTTP authentication layer to demonstrate how you\ncan easily protect your GraphQL endpoint by leveraging FastAPI's dependency\ninjection system. Here's how the content of the ``main.py`` looks:\n\n.. code-block:: python\n\n    # strawberry-gql/app/main.py\n    from __future__ import annotations\n\n    import secrets\n    from typing import Literal\n\n    from fastapi import (\n        Depends, FastAPI, HTTPException, Request,\n        Response, status\n    )\n    from fastapi.security import HTTPBasic, HTTPBasicCredentials\n    from strawberry.fastapi import GraphQLRouter\n\n    from app.schema import schema\n\n    app = FastAPI()\n    router = GraphQLRouter(schema)\n    security = HTTPBasic()\n\n\n    def auth(\n        credentials: HTTPBasicCredentials = Depends(security)\n    ) -> Literal[True]:\n\n        \"\"\"Simple HTTP Basic Auth.\"\"\"\n\n        correct_username = secrets.compare_digest(\n            credentials.username, \"ubuntu\"\n        )\n        correct_password = secrets.compare_digest(\n            credentials.password, \"debian\"\n        )\n\n        if not (correct_username and correct_password):\n            raise HTTPException(\n                status_code=status.HTTP_401_UNAUTHORIZED,\n                detail=\"Incorrect email or password\",\n                headers={\"WWW-Authenticate\": \"Basic\"},\n            )\n        return True\n\n\n    @router.api_route(\"/\", methods=[\"GET\", \"POST\"])\n    async def graphql(request: Request) -> Response:\n        return await router.handle_graphql(request=request)\n\n    app.include_router(\n        router, prefix=\"/graphql\", dependencies=[Depends(auth)]\n    )\n\nFirst, we initialize the ``FastAPI`` app instance which will communicate with\nthe Uvicorn webserver. Then we attach the initialized ``schema`` instance to\nthe ``GraphQLRouter``. The ``HTTPBasic`` class provides the machinery required\nto add the authentication layer. The ``auth`` function houses the\nimplementation details of how we're comparing the incoming and expected\nusername and passwords as well as how the webserver is going to handle\nunauthorized requests. The ``graphql`` handler function is the one that handles\nthe incoming HTTP requests. Finally, the router instance and the security\nhandler are registered to the app instance via the ``app.include_router``\nmethod.\n\nWe can now start querying the ``/graphql`` endpoint. We'll use the built-in\nGraphiQL interface to perform the queries. Before that, let's start the Uvicorn\nwebserver first. Run:\n\n.. code-block:: bash\n\n    $ uvicorn app.main:app --port 5000 --reload\n\nThis exposes the webserver in port 5000. Now, in your browser, go to\n`http://localhost:5000/graphql <http://localhost:5000/graphql>`_. Here, you'll\nfind that the HTTP basic auth requires us to provide the username and password.\n\n.. image::\n    /docs/tutorials/strawberry/http_basic.png\n    :alt: HTTP basic auth prompt\n    :width: 100%\n\n\nCurrently, the allowed username and password is ``ubuntu`` and ``debian``\nrespectively. Provide the credentials and you'll be taken to a page that looks\nlike this:\n\n\n.. image::\n    /docs/tutorials/strawberry/graphiql.png\n    :alt: GraphiQL interface\n    :width: 100%\n\nYou can write your GraphQL queries here. Let's write a query that'll fetch all\nthe actors in the database and show all three of their attributes. The\nfollowing query does that:\n\n.. code-block:: graphql\n\n    query ActorQuery {\n      getActors {\n        age\n        height\n        name\n      }\n    }\n\nThe following response will appear on the right panel of the GraphiQL explorer:\n\n.. image::\n    /docs/tutorials/strawberry/query_actors.png\n    :alt: Query actors\n    :width: 100%\n\nSince as of now, the database doesn't have any data, the payload is returning\nan empty list. Let's write a mutation and create some actors.\n\nMutate actors\n^^^^^^^^^^^^^^^\n\nMutations are also written in the ``schema.py`` file. To write a mutation,\nyou'll have to create a separate class where you'll write the mutation\nresolvers. The resolver methods will need to be decorated with\nthe ``@strawberry.mutation`` decorator. You can write the mutation that'll\ncreate an actor object in the database as follows:\n\n.. code-block:: python\n\n    # strawberry-gql/app/schema.py\n    ...\n\n    @strawberry.type\n    class Mutation:\n        @strawberry.mutation\n        async def create_actor(\n            self, name: str,\n            age: int | None = None,\n            height: int | None = None\n        ) -> ResponseActor:\n\n            actor_json = await client.query_single_json(\n                \"\"\"\n                with new_actor := (\n                    insert Actor {\n                        name := <str>$name,\n                        age := <optional int16>$age,\n                        height := <optional int16>$height\n                    }\n                )\n                select new_actor {name, age, height}\n            \"\"\",\n                name=name,\n                age=age,\n                height=height,\n            )\n\n            actor = json.loads(actor_json)\n            return Actor(\n                actor.get(\"name\"),\n                actor.get(\"age\"),\n                actor.get(\"height\")\n            )\n\n\t# Mutation class needs to be registered here.\n        schema = strawberry.Schema(query=Query, mutation=Mutation)\n\n\nCreating a mutation also includes data validation. By type annotating the\n``Mutation`` class, we're implicitly asking Strawberry to perform data\nvalidation on the incoming request payload. Strawberry will raise an HTTP 400\nerror if the validation fails. Let's create an actor. Submit the following\nGraphQL query in the GraphiQL interface:\n\n.. code-block:: graphql\n\n    mutation ActorMutation {\n      __typename\n      createActor(\n          name: \"Robert Downey Jr.\",\n          age: 57,\n          height: 173\n      ) {\n          age\n          height\n          name\n       }\n    }\n\nIn the above mutation, ``name`` is a required field and the remaining two are\noptional fields. This mutation will create an actor named ``Robert Downey Jr.``\nand show all three attributes— ``name``, ``age``, and ``height`` of the created\nactor in the response payload. Here's the response:\n\n.. image::\n    /docs/tutorials/strawberry/create_actor.png\n    :alt: Create an actor\n    :width: 100%\n\nNow that we've created an actor object, we can run the previously created query\nto fetch the actors. Running the ``ActorQuery`` will give you the following\nresponse:\n\n.. image::\n    /docs/tutorials/strawberry/query_actors_2.png\n    :alt: Query actors\n    :width: 100%\n\nYou can also filter actors by their names. To do so, you'd leverage the\n``filterName`` parameter of the ``getActors`` resolver:\n\n.. code-block:: graphql\n\n    query ActorQuery {\n      __typename\n      getActors(filterName: \"Robert Downey Jr.\") {\n        age\n        height\n        name\n      }\n    }\n\nThis will only display the filtered results. Similarly, as shown above, you can\nwrite the mutations to update and delete actors. Their implementations can be\nfound in the ``schema.py`` file. Check out ``update_actors`` and\n``delete_resolvers`` to learn more about their implementation details. You can\nupdate one or more attributes of an actor with the following mutation:\n\n.. code-block:: graphql\n\n    mutation ActorMutation {\n      __typename\n      updateActors(filterName: \"Robert Downey Jr.\", age: 60) {\n        name\n        age\n        height\n      }\n    }\n\nRunning this mutation will update the ``age`` of ``Robert Downey Jr.``. First,\nwe filter the objects that we want to mutate via the ``filterName`` parameter\nand then we update the relevant attributes; in this case, we updated the\n``age`` of the object. Finally, we show all the fields in the return payload.\nUse the GraphiQL explorer to interactively play with the full API suite.\n\nQuery movies\n^^^^^^^^^^^^\n\nIn the ``schema.py`` file, the query to fetch movies is constructed as\nfollows:\n\n.. code-block:: python\n\n    # strawberry-gql/app/schema.py\n    ...\n\n    @strawberry.type\n    class Query:\n        ...\n\n        @strawberry.field\n        async def get_movies(\n            self, filter_name: str | None = None,\n        ) -> list[Movie]:\n\n            if filter_name:\n                movies_json = await client.query_json(\n                    \"\"\"\n                    select Movie {name, year, actors: {name, age, height}}\n                    filter .name=<str>$filter_name\n                \"\"\",\n                    filter_name=filter_name,\n                )\n            else:\n                movies_json = await client.query_json(\n                    \"\"\"\n                    select Movie {name, year, actors: {name, age, height}}\n                \"\"\"\n                )\n\n            # Deserialize.\n            movies = json.loads(movies_json)\n            for idx, movie in enumerate(movies):\n                actors = [\n                    Actor(name) for d in movie.get(\"actors\", [])\n                    for name in d.values()\n                ]\n\n                movies[idx] = Movie(\n                                movie.get(\"name\"),\n                                movie.get(\"year\"), actors\n                            )\n            return movies\n\nSimilar to the actor query, this also allows you to either fetch all or filter\nmovies by the movie names. Execute the following query to see the movies in the\ndatabase:\n\n.. code-block:: graphql\n\n    query MovieQuery {\n      __typename\n      getMovies {\n        actors {\n          age\n          height\n          name\n        }\n        name\n        year\n      }\n    }\n\nThis will return an empty list since the database doesn't contain any movies.\nIn the next section, we'll create a mutation to create the movies and query\nthem afterward.\n\nMutate movies\n^^^^^^^^^^^^^\n\nBefore running any query to fetch the movies, let's see how you'd construct\na mutation that allows you to create movies. You can build the mutation similar\nto how we've constructed the create actor mutation. It looks like this:\n\n.. code-block:: python\n\n    # strawberry-gql/app/schema.py\n    ...\n\n    @strawberry.type\n    class Mutation:\n        ...\n\n        @strawberry.mutation\n        async def create_movie(\n            self,\n            name: str,\n            year: int | None = None,\n            actor_names: list[str] | None = None,\n        ) -> Movie:\n            movie_json = await client.query_single_json(\n                \"\"\"\n                with\n                    name := <str>$name,\n                    year := <optional int16>$year,\n                    actor_names := <optional array<str>>$actor_names,\n                    new_movie := (\n                        insert Movie {\n                            name := name,\n                            year := year,\n                            actors := (\n                                select detached Actor\n                                filter .name in array_unpack(actor_names)\n                            )\n                        }\n                    )\n                select new_movie {\n                    name,\n                    year,\n                    actors: {name, age, height}\n                }\n            \"\"\",\n                name=name,\n                year=year,\n                actor_names=actor_names,\n            )\n\n            movie = json.loads(movie_json)\n            actors = [\n                Actor(name) for d in movie.get(\"actors\", [])\n                for name in d.values()]\n\n            return Movie(\n                movie.get(\"name\"),\n                movie.get(\"year\"),\n                actors\n            )\n\nYou can submit a request to this mutation to create a movie. While creating a\nmovie, you must provide the name of the movie as it's a required field. Also,\nyou can optionally provide the ``year`` the movie was released and an array\ncontaining the names of the actors. If the values of the ``actor_names`` field\nmatch any existing actor in the database, the above snippet makes sure that the\nmovie will be linked with the corresponding actors. In the GraphiQL explorer,\nrun the following mutation to create a movie named ``Avengers`` and link the\nactor ``Robert Downey Jr.`` with the movie:\n\n.. code-block:: graphql\n\n    mutation MovieMutation {\n      __typename\n      createMovie(\n        name: \"Avengers\",\n        actorNames: [\"Robert Downey Jr.\"],\n        year: 2012\n      ) {\n          actors {\n            name\n        }\n      }\n    }\n\nIt'll return:\n\n.. image::\n    /docs/tutorials/strawberry/create_movie.png\n    :alt: Create a movie\n    :width: 100%\n\n\nNow you can fetch the movies with a simple query like this one:\n\n.. code-block:: graphql\n\n    query MovieQuery {\n      __typename\n      getMovies {\n        name\n        year\n        actors {\n          name\n        }\n      }\n    }\n\nYou'll then see an output similar to this:\n\n.. image::\n    /docs/tutorials/strawberry/query_movies.png\n    :alt: Query movies\n    :width: 100%\n\nTake a look at the ``update_movies`` and ``delete_movies`` resolvers to gain\nmore insights into the implementation details of those mutations.\n\n\nConclusion\n==========\n\nIn this tutorial, you've seen how can use Strawberry and Gel together to\nquickly build a fully-featured GraphQL API. Also, you have seen how FastAPI\nallows you add an authentication layer and serve the API in a secure manner.\nOne thing to keep in mind here is, ideally, you'd only use GraphQL if you're\ninterfacing with something that already expects a GraphQL API. Otherwise,\nEdgeQL is always going to be more powerful and expressive than GraphQL's query\nsyntax.\n"
  },
  {
    "path": "docs/resources/guides/tutorials/index.rst",
    "content": ".. _ref_guide_tutorials:\n\n\n=================\nUsing Gel with...\n=================\n\n.. toctree::\n    :maxdepth: 1\n\n    nextjs_app_router\n    nextjs_pages_router\n    rest_apis_with_fastapi\n    rest_apis_with_flask\n    jupyter_notebook\n    graphql_apis_with_strawberry\n    chatgpt_bot\n    cloudflare_workers\n    trpc\n    Bun <https://bun.sh/guides/ecosystem/edgedb>\n"
  },
  {
    "path": "docs/resources/guides/tutorials/jupyter_notebook.rst",
    "content": ".. _ref_guide_jupyter_notebook:\n\n================\nJupyter Notebook\n================\n\n:edb-alt-title: Using Gel with Jupyter Notebook\n\n1. `Install Jupyter Notebook\n   <https://docs.jupyter.org/en/latest/install/notebook-classic.html>`__\n\n2. Install the Gel Python library with ``pip install gel``\n\n3. Set the appropriate :ref:`connection environment variables\n   <ref_reference_connection>` required for your\n   Gel instance\n\n   **For Gel Cloud instances**\n\n   - :gelenv:`INSTANCE`- your instance name (``<org-name>/<instance-name>``)\n   - :gelenv:`SECRET_KEY`- a secret key with permissions for the selected instance.\n\n     .. note::\n\n         You may create a secret key with the CLI by running :gelcmd:`cloud\n         secretkey create` or in the `Gel Cloud UI\n         <https://cloud.geldata.com/>`__.\n\n   **For other remote instances**\n\n   - :gelenv:`DSN`- the DSN of your remote instance\n\n     .. note::\n\n        DSNs take the following format:\n        :geluri:`<username>:<password>@<hostname-or-ip>:<port>/<branch>`.\n        Omit any segment, and Gel will fall back to a default value listed\n        in :ref:`our DSN specification <ref_dsn>`\n\n   **For local Gel instances**\n\n   - :gelenv:`INSTANCE`- your instance name\n   - :gelenv:`USER` & :gelenv:`PASSWORD`\n\n   .. note :: Usernames and passwords\n\n      Gel creates an |admin| user by default, but the password is\n      randomized. You may set the password for this role by running ``alter\n      role admin { set password := '<password>'; };`` or you may create a new\n      role using ``create superuser role <name> { set password := '<password>';\n      };``.\n\n4. Start your notebook by running ``jupyter notebook``. Make sure this process\n   runs in the same environment that contains the variables you set in step 3.\n\n5. Create a new notebook.\n\n6. In one of your notebook's blocks, import the Gel library and run a query.\n\n   .. code-block:: python\n\n      import gel\n\n      client = gel.create_client()\n\n      def main():\n          query = \"SELECT 1 + 1;\" # Swap in any query you want\n          result = client.query(query)\n          print(result[0])\n\n      main()\n\n      client.close()\n"
  },
  {
    "path": "docs/resources/guides/tutorials/nextjs_app_router.rst",
    "content": ".. _ref_guide_nextjs_app_router:\n\n====================\nNext.js (App Router)\n====================\n\n:edb-alt-title: Building a simple blog application with\n   Gel and Next.js (App Router)\n\nWe're going to build a simple blog application with\n`Next.js <https://nextjs.org/>`_ and Gel. Let's start by scaffolding our\napp with Next.js's ``create-next-app`` tool.\n\nYou'll be prompted to provide a name (we'll use ``nextjs-blog``) for your\napp and choose project options. For this tutorial, we'll go with the\nrecommended settings including TypeScript, App Router, and\n**opt-ing out** of the ``src/`` directory.\n\n.. code-block:: bash\n\n  $ npx create-next-app@latest\n    ✔ Would you like to use TypeScript? Yes\n    ✔ Would you like to use ESLint? Yes\n    ✔ Would you like to use Tailwind CSS? Yes\n    ✔ Would you like to use src/ directory? No\n    ✔ Would you like to use App Router? (recommended) Yes\n    ✔ Would you like to customize the default import alias (@/*) Yes\n\nThe scaffolding tool will create a simple Next.js app and install its\ndependencies. Once it's done, you can navigate to the app's directory and\nstart the development server.\n\n.. code-block:: bash\n\n  $ cd nextjs-blog\n  $ npm dev # or yarn dev or pnpm dev or bun run dev\n\nWhen the dev server starts, it will log out a local URL.\nVisit that URL to see the default Next.js homepage. At this\npoint the app's file structure looks like this:\n\n.. code-block::\n\n  README.md\n  tsconfig.json\n  package.json\n  next.config.js\n  next-env.d.ts\n  postcss.config.js\n  tailwind.config.js\n  app\n  ├── page.tsx\n  ├── layout.tsx\n  ├── globals.css\n  └── favicon.ico\n  public\n  ├── next.tsx\n  └── vercel.svg\n\nThere's an async function ``Home`` defined in ``app/page.tsx`` that renders\nthe homepage. It's a\n`Server Component <https://nextjs.org/docs/app/building-your-application/\nrendering/server-components>`_\nwhich lets you integrate server-side logic directly\ninto your React components. Server Components are executed on the server and\ncan fetch data from a database or an API. We'll use this feature to load blog\nposts from a Gel database.\n\nUpdating the homepage\n---------------------\n\nLet's start by implementing a simple homepage for our blog application using\nstatic data. Replace the contents of ``app/page.tsx`` with the following.\n\n.. code-block:: tsx\n  :caption: app/page.tsx\n\n  import Link from 'next/link'\n\n  type Post = {\n    id: string\n    title: string\n    content: string\n  }\n\n  export default async function Home() {\n    const posts: Post[] = [\n      {\n        id: 'post1',\n        title: 'This one weird trick makes using databases fun',\n        content: 'Use Gel',\n      },\n      {\n        id: 'post2',\n        title: 'How to build a blog with Gel and Next.js',\n        content: \"Let's start by scaffolding our app with `create-next-app`.\",\n      },\n    ]\n\n    return (\n      <div className=\"container mx-auto p-4 bg-black text-white\">\n        <h1 className=\"text-3xl font-bold mb-4\">Posts</h1>\n        <ul>\n          {posts.map((post) => (\n            <li\n              key={post.id}\n              className=\"mb-4\"\n            >\n              <Link\n                href={`/post/${post.id}`}\n                className=\"text-blue-500\"\n              >\n                {post.title}\n              </Link>\n            </li>\n          ))}\n        </ul>\n      </div>\n    )\n  }\n\n\nAfter saving, you can refresh the page to see the blog posts. Clicking on a\npost title will take you to a page that doesn't exist yet. We'll create that\npage later in the tutorial.\n\nInitializing Gel\n----------------\n\nNow let's spin up a database for the app. You have two options to initialize\na Gel project: using ``$ npx gel`` without installing the CLI, or\ninstalling the gel CLI directly. In this tutorial, we'll use the first\noption. If you prefer to install the CLI, see the\n:ref:`Gel CLI guide <ref_cli_overview>` for more information.\n\nFrom the application's root directory, run the following command:\n\n.. code-block:: bash\n\n  $ npx gel project init\n  No `gel.toml` found in `~/nextjs-blog` or above\n  Do you want to initialize a new project? [Y/n]\n  > Y\n  Specify the name of Gel instance to use with this project [default:\n  nextjs_blog]:\n  > nextjs_blog\n  Checking Gel versions...\n  Specify the version of Gel to use with this project [default: x.x]:\n  >\n  ┌─────────────────────┬──────────────────────────────────────────────┐\n  │ Project directory   │ ~/nextjs-blog                                │\n  │ Project config      │ ~/nextjs-blog/gel.toml                       │\n  │ Schema dir (empty)  │ ~/nextjs-blog/dbschema                       │\n  │ Installation method │ portable package                             │\n  │ Start configuration │ manual                                       │\n  │ Version             │ x.x                                          │\n  │ Instance name       │ nextjs_blog                                  │\n  └─────────────────────┴──────────────────────────────────────────────┘\n  Initializing Gel instance...\n  Applying migrations...\n  Everything is up to date. Revision initial.\n  Project initialized.\n\nThis process has spun up a Gel instance called ``nextjs_blog`` and\nassociated it with your current directory. As long as you're inside that\ndirectory, CLI commands and client libraries will be able to connect to the\nlinked instance automatically, without additional configuration.\n\nTo test this, run the |gelcmd| command to open a REPL to the linked instance.\n\n.. code-block:: bash\n\n  $ gel\n  Gel x.x (repl x.x)\n  Type \\help for help, \\quit to quit.\n  gel> select 2 + 2;\n  {4}\n  >\n\nFrom inside this REPL, we can execute EdgeQL queries against our database. But\nthere's not much we can do currently, since our database is schemaless. Let's\nchange that.\n\nThe project initialization process also created a new subdirectory in our\nproject called ``dbschema``. This is folder that contains everything\npertaining to Gel. Currently it looks like this:\n\n.. code-block::\n\n  dbschema\n  ├── default.gel\n  └── migrations\n\nThe :dotgel:`default` file will contain our schema. The ``migrations``\ndirectory is currently empty, but will contain our migration files. Let's\nupdate the contents of :dotgel:`default` with the following simple blog schema.\n\n.. code-block:: sdl\n  :caption: dbschema/default.gel\n\n  module default {\n    type BlogPost {\n      required title: str;\n      required content: str {\n        default := \"\"\n      }\n    }\n  }\n\n.. note::\n\n  Gel lets you split up your schema into different ``modules`` but it's\n  common to keep your entire schema in the ``default`` module.\n\nSave the file, then let's create our first migration.\n\n.. code-block:: bash\n\n  $ npx gel migration create\n  did you create object type 'default::BlogPost'? [y,n,l,c,b,s,q,?]\n  > y\n  Created ./dbschema/migrations/00001.edgeql\n\nThe ``dbschema/migrations`` directory now contains a migration file called\n``00001.edgeql``. Currently though, we haven't applied this migration against\nour database. Let's do that.\n\n.. code-block:: bash\n\n  $ npx gel migrate\n  Applied m1fee6oypqpjrreleos5hmivgfqg6zfkgbrowx7sw5jvnicm73hqdq (00001.edgeql)\n\nOur database now has a schema consisting of the ``BlogPost`` type. We can\ncreate some sample data from the REPL. Run the |gelcmd| command to re-open\nthe REPL.\n\n.. code-block:: bash\n\n  $ gel\n  Gel x.x (repl x.x)\n  Type \\help for help, \\quit to quit.\n  gel>\n\n\nThen execute the following ``insert`` statements.\n\n.. code-block:: edgeql-repl\n\n  gel> insert BlogPost {\n  ....   title := \"This one weird trick makes using databases fun\",\n  ....   content := \"Use Gel\"\n  .... };\n  {default::BlogPost {id: 7f301d02-c780-11ec-8a1a-a34776e884a0}}\n  gel> insert BlogPost {\n  ....   title := \"How to build a blog with Gel and Next.js\",\n  ....   content := \"Let's start by scaffolding our app...\"\n  .... };\n  {default::BlogPost {id: 88c800e6-c780-11ec-8a1a-b3a3020189dd}}\n\n\nLoading posts with React Server Components\n------------------------------------------\n\nNow that we have a couple posts in the database, let's load them into our\nNext.js app.\nTo do that, we'll need the ``gel`` client library. Let's install that from\nNPM:\n\n.. code-block:: bash\n\n  $ npm install gel\n  # or 'yarn add gel' or 'pnpm add gel' or 'bun add gel'\n\nThen go to the ``app/page.tsx`` file to replace the static data with\nthe blogposts fetched from the database.\n\nTo fetch these from the homepage, we'll create a Gel client and use the\n``.query()`` method to fetch all the posts in the database with a\n``select`` statement.\n\n.. code-block:: tsx-diff\n  :caption: app/page.tsx\n\n    import Link from 'next/link'\n  + import { createClient } from 'gel';\n\n    type Post = {\n      id: string\n      title: string\n      content: string\n    }\n  + const client = createClient();\n\n    export default async function Home() {\n  -   const posts: Post[] = [\n  -     {\n  -       id: 'post1',\n  -       title: 'This one weird trick makes using databases fun',\n  -       content: 'Use Gel',\n  -     },\n  -     {\n  -       id: 'post2',\n  -       title: 'How to build a blog with Gel and Next.js',\n  -       content: \"Start by scaffolding our app with `create-next-app`.\",\n  -     },\n  -   ]\n  +   const posts = await client.query<Post>(`\\\n  +    select BlogPost {\n  +      id,\n  +      title,\n  +      content\n  +   };`)\n\n      return (\n        <div className=\"container mx-auto p-4 bg-black text-white\">\n          <h1 className=\"text-3xl font-bold mb-4\">Posts</h1>\n          <ul>\n            {posts.map((post) => (\n              <li\n                key={post.id}\n                className=\"mb-4\"\n              >\n                <Link\n                  href={`/post/${post.id}`}\n                  className=\"text-blue-500\"\n                >\n                  {post.title}\n                </Link>\n              </li>\n            ))}\n          </ul>\n        </div>\n      )\n    }\n\nWhen you refresh the page, you should see the blog posts.\n\nGenerating the query builder\n----------------------------\n\nSince we're using TypeScript, it makes sense to use Gel's powerful query\nbuilder. This provides a schema-aware client API that makes writing strongly\ntyped EdgeQL queries easy and painless. The result type of our queries will be\nautomatically inferred, so we won't need to manually type something like\n``type Post = { id: string; ... }``.\n\nFirst, install the generator to your project.\n\n.. code-block:: bash\n\n  $ npm install --save-dev @gel/generate\n  $ # or yarn add --dev @gel/generate\n  $ # or pnpm add --dev @gel/generate\n  $ # or bun add --dev @gel/generate\n\nThen generate the query builder with the following command.\n\n.. code-block:: bash\n\n  $ npx @gel/generate edgeql-js\n  Generating query builder...\n  Detected tsconfig.json, generating TypeScript files.\n     To override this, use the --target flag.\n     Run `npx @gel/generate --help` for full options.\n  Introspecting database schema...\n  Writing files to ./dbschema/edgeql-js\n  Generation complete! 🤘\n  Checking the generated query builder into version control\n  is not recommended. Would you like to update .gitignore to ignore\n  the query builder directory? The following line will be added:\n\n     dbschema/edgeql-js\n\n  [y/n] (leave blank for \"y\")\n  > y\n\n\nThis command introspected the schema of our database and generated some code\nin the ``dbschema/edgeql-js`` directory. It also asked us if we wanted to add\nthe generated code to our ``.gitignore``; typically it's not good practice to\ninclude generated files in version control.\n\nBack in ``app/page.tsx``, let's update our code to use the query builder\ninstead.\n\n.. code-block:: typescript-diff\n  :caption: app/page.tsx\n\n    import Link from 'next/link'\n    import { createClient } from 'gel';\n  + import e from '@/dbschema/edgeql-js';\n\n  - type Post = {\n  -   id: string\n  -   title: string\n  -   content: string\n  - }\n    const client = createClient();\n\n    export default async function Home() {\n  -   const posts = await client.query(`\\\n  -    select BlogPost {\n  -      id,\n  -      title,\n  -      content\n  -   };`)\n  +   const selectPosts = e.select(e.BlogPost, () => ({\n  +     id: true,\n  +     title: true,\n  +     content: true,\n  +   }));\n  +   const posts = await selectPosts.run(client);\n\n      return (\n        <div className=\"container mx-auto p-4 bg-black text-white\">\n          <h1 className=\"text-3xl font-bold mb-4\">Posts</h1>\n          <ul>\n            {posts.map((post) => (\n              <li\n                key={post.id}\n                className=\"mb-4\"\n              >\n                <Link\n                  href={`/post/${post.id}`}\n                  className=\"text-blue-500\"\n                >\n                  {post.title}\n                </Link>\n              </li>\n            ))}\n          </ul>\n        </div>\n      )\n    }\n\nInstead of writing our query as a plain string, we're now using the query\nbuilder to declare our query in a code-first way. As you can see, we import the\nquery builder as a single default import ``e`` from the ``dbschema/edgeql-js``\ndirectory.\n\nNow, when we update our ``selectPosts`` query, the type of our dynamically\nloaded ``posts`` variable will update automatically — no need to keep\nour type definitions in sync with our API logic!\n\nRendering blog posts\n--------------------\n\nOur homepage renders a list of links to each of our blog posts, but we haven't\nimplemented the page that actually displays the posts. Let's create a new page\nat ``app/post/[id]/page.tsx``. This is a\n`dynamic route <https://nextjs.org/docs/app/building-your-application/\nrouting/dynamic-routes>`_ that\nincludes an ``id`` URL parameter. We'll use this parameter to fetch the\nappropriate post from the database.\n\nAdd the following code in ``app/post/[id]/page.tsx``:\n\n.. code-block:: tsx\n  :caption: app/post/[id]/page.tsx\n\n  import { createClient } from 'gel'\n  import e from '@/dbschema/edgeql-js'\n  import Link from 'next/link'\n\n  const client = createClient()\n\n  export default async function Post({ params }: { params: { id: string } }) {\n    const post = await e\n      .select(e.BlogPost, (post) => ({\n        id: true,\n        title: true,\n        content: true,\n        filter_single: e.op(post.id, '=', e.uuid(params.id)),\n      }))\n      .run(client)\n\n    if (!post) {\n      return <div>Post not found</div>\n    }\n\n    return (\n      <div className=\"container mx-auto p-4 bg-black text-white\">\n        <nav>\n          <Link\n            href=\"/\"\n            className=\"text-blue-500 mb-4 block\"\n            replace\n          >\n            Back to list\n          </Link>\n        </nav>\n        <h1 className=\"text-3xl font-bold mb-4\">{post.title}</h1>\n        <p>{post.content}</p>\n      </div>\n    )\n  }\n\nWe are again using a Server Component to fetch the post from the database.\nThis time, we're using the ``filter_single`` method to filter the\n``BlogPost`` type by its ``id``. We're also using the ``uuid`` function\nfrom the query builder to convert the ``id`` parameter to a UUID.\n\nNow, click on one of the blog post links on the homepage. This should bring\nyou to ``/post/<uuid>``.\n\nDeploying to Vercel\n-------------------\n\nYou can deploy a Gel instance on the Gel Cloud or\non your preferred cloud provider. We'll cover both options here.\n\nWith Gel Cloud\n==============\n\n**#1 Deploy Gel**\n\nFirst, sign up for an account at\n`cloud.geldata.com <https://cloud.geldata.com>`_ and create a new instance.\nCreate and make note of a secret key for your Gel Cloud instance. You\ncan create a new secret key from the \"Secret Keys\" tab in the Gel Cloud\nconsole. We'll need this later to connect to the database from Vercel.\n\nRun the following command to migrate the project to the Gel Cloud:\n\n.. code-block:: bash\n\n  $ npx gel migrate -I <org>/<instance-name>\n\n.. note::\n\n  Alternatively, if you want to restore your data from a local instance to\n  the cloud, you can use the :gelcmd:`dump` and :gelcmd:`restore` commands.\n\n.. code-block:: bash\n\n  $ npx gel dump <your-dump.dump>\n  $ npx gel restore -I <org>/<instance-name> <your-dump.dump>\n\nThe migrations and schema will be automatically applied to the\ncloud instance.\n\n**#2 Set up a `prebuild` script**\n\nAdd the following ``prebuild`` script to your ``package.json``. When Vercel\ninitializes the build, it will trigger this script which will generate the\nquery builder. The ``npx @gel/generate edgeql-js`` command will read the\nvalue of the :gelenv:`SECRET_KEY` and :gelenv:`INSTANCE` variables,\nconnect to the database, and generate the query builder before Vercel\nstarts building the project.\n\n.. code-block:: javascript-diff\n\n    // package.json\n    \"scripts\": {\n      \"dev\": \"next dev\",\n      \"build\": \"next build\",\n      \"start\": \"next start\",\n      \"lint\": \"next lint\",\n  +   \"prebuild\": \"npx @gel/generate edgeql-js\"\n    },\n\n**#3 Deploy to Vercel**\n\nPush your project to GitHub or some other Git remote repository. Then deploy\nthis app to Vercel with the button below.\n\n\n.. XXX -- update URL\n.. lint-off\n\n.. image:: https://vercel.com/button\n  :width: 150px\n  :target: https://vercel.com/new/git/external?repository-url=https://github.com/geldata/gel-examples/tree/main/nextjs-blog&project-name=nextjs-edgedb-blog&repository-name=nextjs-edgedb-blog&env=EDGEDB_DSN,EDGEDB_CLIENT_TLS_SECURITY\n\n.. lint-on\n\nIn \"Configure Project,\" expand \"Environment Variables\" to add two variables:\n\n- :gelenv:`INSTANCE` containing your Gel Cloud instance name (in\n  ``<org>/<instance-name>`` format)\n- :gelenv:`SECRET_KEY` containing the secret key you created and noted\n  previously.\n\n**#4 View the application**\n\nOnce deployment has completed, view the application at the deployment URL\nsupplied by Vercel.\n\nWith other cloud providers\n===========================\n\n**#1 Deploy Gel**\n\nCheck out the following guides for deploying Gel to your preferred cloud\nprovider:\n\n- :ref:`AWS <ref_guide_deployment_aws_aurora_ecs>`\n- :ref:`Google Cloud <ref_guide_deployment_gcp>`\n- :ref:`Azure <ref_guide_deployment_azure_flexibleserver>`\n- :ref:`DigitalOcean <ref_guide_deployment_digitalocean>`\n- :ref:`Fly.io <ref_guide_deployment_fly_io>`\n- :ref:`Docker <ref_guide_deployment_docker>`\n  (cloud-agnostic)\n\n**#2 Find your instance's DSN**\n\nThe DSN is also known as a connection string. It will have the format\n:geluri:`username:password@hostname:port`. The exact instructions for this\ndepend on which cloud you are deploying to.\n\n**#3 Apply migrations**\n\nUse the DSN to apply migrations against your remote instance.\n\n.. code-block:: bash\n\n  $ npx gel migrate --dsn <your-instance-dsn> --tls-security insecure\n\n.. note::\n\n  You have to disable TLS checks with ``--tls-security insecure``. All Gel\n  instances use TLS by default, but configuring it is out of scope of this\n  project.\n\nOnce you've applied the migrations, consider creating some sample data in your\ndatabase. Open a REPL and ``insert`` some blog posts:\n\n.. code-block:: bash\n\n  $ npx gel --dsn <your-instance-dsn> --tls-security insecure\n  Gel x.x (repl x.x)\n  Type \\help for help, \\quit to quit.\n  gel> insert BlogPost { title := \"Test post\" };\n  {default::BlogPost {id: c00f2c9a-cbf5-11ec-8ecb-4f8e702e5789}}\n\n\n**#4 Set up a `prebuild` script**\n\nAdd the following ``prebuild`` script to your ``package.json``. When Vercel\ninitializes the build, it will trigger this script which will generate the\nquery builder. The ``npx @gel/generate edgeql-js`` command will read the\nvalue of the :gelenv:`DSN` variable, connect to the database, and generate\nthe query builder before Vercel starts building the project.\n\n.. code-block:: javascript-diff\n\n    // package.json\n    \"scripts\": {\n      \"dev\": \"next dev\",\n      \"build\": \"next build\",\n      \"start\": \"next start\",\n      \"lint\": \"next lint\",\n  +   \"prebuild\": \"npx @gel/generate edgeql-js\"\n    },\n\n**#5 Deploy to Vercel**\n\nDeploy this app to Vercel with the button below.\n\n.. lint-off\n\n.. image:: https://vercel.com/button\n  :width: 150px\n  :target: https://vercel.com/new/git/external?repository-url=https://github.com/geldata/gel-examples/tree/main/nextjs-blog&project-name=nextjs-edgedb-blog&repository-name=nextjs-edgedb-blog&env=EDGEDB_DSN,EDGEDB_CLIENT_TLS_SECURITY\n\n.. lint-on\n\nWhen prompted:\n\n- Set :gelenv:`DSN` to your database's DSN\n- Set :gelenv:`CLIENT_TLS_SECURITY` to ``insecure``. This will disable\n  Gel's default TLS checks; configuring TLS is beyond the scope of this\n  tutorial.\n\n.. XXX -- update URL\n\n.. image::\n    https://www.geldata.com/docs/tutorials/nextjs/env.png\n    :alt: Setting environment variables in Vercel\n    :width: 100%\n\n\n**#6 View the application**\n\nOnce deployment has completed, view the application at the deployment URL\nsupplied by Vercel.\n\nWrapping up\n-----------\n\nThis tutorial demonstrates how to work with Gel in a\nNext.js app, using the App Router. We've created a simple blog application\nthat loads posts from a database and displays them on the homepage.\nWe've also created a dynamic route that fetches a single post from the\ndatabase and displays it on a separate page.\n\nThe next step is to add a ``/newpost`` page with a form for writing new blog\nposts and saving them into Gel. That's left as an exercise for the reader.\n\nTo see the final code for this tutorial, refer to\n`github.com/geldata/gel-examples/tree/main/nextjs-blog\n<https://github.com/geldata/gel-examples/tree/main/\nnextjs-blog-app-router>`_.\n"
  },
  {
    "path": "docs/resources/guides/tutorials/nextjs_pages_router.rst",
    "content": ".. _ref_guide_nextjs_pages_router:\n\n======================\nNext.js (Pages Router)\n======================\n\n:edb-alt-title: Building a simple blog application with Gel and\n   Next.js (Pages Router)\n\nWe're going to build a simple blog application with\n`Next.js <https://nextjs.org/>`_ and Gel. Let's start by scaffolding our\napp with Next.js's ``create-next-app`` tool. We'll be using TypeScript for\nthis tutorial.\n\n.. code-block:: bash\n\n  $ npx create-next-app --typescript nextjs-blog\n\nThis will take a minute to run. The scaffolding tool is creating a simple\nNext.js app and installing all our dependencies for us. Once it's complete,\nlet's navigate into the directory and start the dev server.\n\n.. code-block:: bash\n\n  $ cd nextjs-blog\n  $ yarn dev\n\nOpen `localhost:3000 <http://localhost:3000>`_ to see the default Next.js\nhomepage. At this point the app's file structure looks like this:\n\n.. code-block::\n\n  README.md\n  tsconfig.json\n  package.json\n  next.config.js\n  next-env.d.ts\n  pages\n  ├── _app.tsx\n  ├── api\n  │   └── hello.ts\n  └── index.tsx\n  public\n  ├── favicon.ico\n  └── vercel.svg\n  styles\n  ├── Home.module.css\n  └── globals.css\n\nThere's a custom App component defined in ``pages/_app.tsx`` that loads some\nglobal CSS, plus the homepage at ``pages/index.tsx`` and a single API route at\n``pages/api/hello.ts``. The ``styles`` and ``public`` directories contain some\nother assets.\n\nUpdating the homepage\n---------------------\n\nLet's start by implementing a simple homepage for our blog application using\nstatic data. Replace the contents of ``pages/index.tsx`` with the following.\n\n.. code-block:: tsx\n\n  // pages/index.tsx\n\n  import type {NextPage} from 'next';\n  import Head from 'next/head';\n  import styles from '../styles/Home.module.css';\n\n  type Post = {\n    id: string;\n    title: string;\n    content: string;\n  };\n\n  const HomePage: NextPage = () => {\n    const posts: Post[] = [\n      {\n        id: 'post1',\n        title: 'This one weird trick makes using databases fun',\n        content: 'Use Gel',\n      },\n      {\n        id: 'post2',\n        title: 'How to build a blog with Gel and Next.js',\n        content: \"Let's start by scaffolding our app with `create-next-app`.\",\n      },\n    ];\n\n    return (\n      <div className={styles.container}>\n        <Head>\n          <title>My Blog</title>\n          <meta name=\"description\" content=\"An awesome blog\" />\n          <link rel=\"icon\" href=\"/favicon.ico\" />\n        </Head>\n\n        <main className={styles.main}>\n          <h1 className={styles.title}>Blog</h1>\n          <div style={{height: '50px'}}></div>\n          {posts.map((post) => {\n            return (\n              <a href={`/post/${post.id}`} key={post.id}>\n                <div className={styles.card}>\n                  <p>{post.title}</p>\n                </div>\n              </a>\n            );\n          })}\n        </main>\n      </div>\n    );\n  };\n\n  export default HomePage;\n\nAfter saving, Next.js should hot-reload, and the homepage should look\nsomething like this.\n\n\n.. image::\n    /docs/tutorials/nextjs/basic_home.png\n    :alt: Basic blog homepage with static content\n    :width: 100%\n\nInitializing Gel\n----------------\n\nNow let's spin up a database for the app. You have two options to initialize\na Gel project: using ``$ npx gel`` without installing the CLI, or\ninstalling the gel CLI directly. In this tutorial, we'll use the first\noption. If you prefer to install the CLI, see the\n:ref:`Gel CLI guide <ref_cli_overview>` for more information.\nFrom the application's root directory, run the following command:\n\n.. code-block:: bash\n\n  $ npx gel project init\n  No `gel.toml` found in `~/nextjs-blog` or above\n  Do you want to initialize a new project? [Y/n]\n  > Y\n  Specify the name of Gel instance to use with this project [default:\n  nextjs_blog]:\n  > nextjs_blog\n  Checking Gel versions...\n  Specify the version of Gel to use with this project [default: x.x]:\n  >\n  ┌─────────────────────┬──────────────────────────────────────────────┐\n  │ Project directory   │ ~/nextjs-blog                                │\n  │ Project config      │ ~/nextjs-blog/gel.toml                       │\n  │ Schema dir (empty)  │ ~/nextjs-blog/dbschema                       │\n  │ Installation method │ portable package                             │\n  │ Start configuration │ manual                                       │\n  │ Version             │ x.x                                          │\n  │ Instance name       │ nextjs_blog                                  │\n  └─────────────────────┴──────────────────────────────────────────────┘\n  Initializing Gel instance...\n  Applying migrations...\n  Everything is up to date. Revision initial.\n  Project initialized.\n\nThis process has spun up a Gel instance called ``nextjs-blog`` and\n\"linked\" it with your current directory. As long as you're inside that\ndirectory, CLI commands and client libraries will be able to connect to the\nlinked instance automatically, without additional configuration.\n\nTo test this, run the |gelcmd| command to open a REPL to the linked instance.\n\n.. code-block:: bash\n\n  $ gel\n  Gel x.x (repl x.x)\n  Type \\help for help, \\quit to quit.\n  gel> select 2 + 2;\n  {4}\n  >\n\nFrom inside this REPL, we can execute EdgeQL queries against our database. But\nthere's not much we can do currently, since our database is schemaless. Let's\nchange that.\n\nThe project initialization process also created a new subdirectory in our\nproject called ``dbschema``. This is folder that contains everything\npertaining to Gel. Currently it looks like this:\n\n.. code-block::\n\n  dbschema\n  ├── default.gel\n  └── migrations\n\nThe :dotgel:`default` file will contain our schema. The ``migrations``\ndirectory is currently empty, but will contain our migration files. Let's\nupdate the contents of :dotgel:`default` with the following simple blog schema.\n\n.. code-block:: sdl\n\n  # dbschema/default.gel\n\n  module default {\n    type BlogPost {\n      required property title -> str;\n      required property content -> str {\n        default := \"\"\n      };\n    }\n  }\n\n.. note::\n\n  Gel lets you split up your schema into different ``modules`` but it's\n  common to keep your entire schema in the ``default`` module.\n\nSave the file, then let's create our first migration.\n\n.. code-block:: bash\n\n  $ npx gel migration create\n  did you create object type 'default::BlogPost'? [y,n,l,c,b,s,q,?]\n  > y\n  Created ./dbschema/migrations/00001.edgeql\n\nThe ``dbschema/migrations`` directory now contains a migration file called\n``00001.edgeql``. Currently though, we haven't applied this migration against\nour database. Let's do that.\n\n.. code-block:: bash\n\n  $ npx gel migrate\n  Applied m1fee6oypqpjrreleos5hmivgfqg6zfkgbrowx7sw5jvnicm73hqdq (00001.edgeql)\n\nOur database now has a schema consisting of the ``BlogPost`` type. We can\ncreate some sample data from the REPL. Run the |gelcmd| command to re-open\nthe REPL.\n\n.. code-block:: bash\n\n  $ gel\n  Gel x.x (repl x.x)\n  Type \\help for help, \\quit to quit.\n  gel>\n\n\nThen execute the following ``insert`` statements.\n\n.. code-block:: edgeql-repl\n\n  gel> insert BlogPost {\n  ....   title := \"This one weird trick makes using databases fun\",\n  ....   content := \"Use Gel\"\n  .... };\n  {default::BlogPost {id: 7f301d02-c780-11ec-8a1a-a34776e884a0}}\n  gel> insert BlogPost {\n  ....   title := \"How to build a blog with Gel and Next.js\",\n  ....   content := \"Let's start by scaffolding our app...\"\n  .... };\n  {default::BlogPost {id: 88c800e6-c780-11ec-8a1a-b3a3020189dd}}\n\n\nLoading posts with an API route\n-------------------------------\n\nNow that we have a couple posts in the database, let's load them dynamically\nwith a Next.js `API route <https://nextjs.org/docs/api-routes/introduction>`_.\nTo do that, we'll need the ``gel`` client library. Let's install that from\nNPM:\n\n.. code-block:: bash\n\n  $ npm install gel\n\nThen create a new file at ``pages/api/post.ts`` and copy in the following code.\n\n.. code-block:: typescript\n\n  // pages/api/post.ts\n\n  import type {NextApiRequest, NextApiResponse} from 'next';\n  import {createClient} from 'gel';\n\n  export const client = createClient();\n\n  export default async function handler(\n    req: NextApiRequest,\n    res: NextApiResponse\n  ) {\n    const posts = await client.query(`select BlogPost {\n      id,\n      title,\n      content\n    };`);\n    res.status(200).json(posts);\n  }\n\nThis file initializes a Gel client, which manages a pool of connections to\nthe database and provides an API for executing queries. We're using the\n``.query()`` method to fetch all the posts in the database with a simple\n``select`` statement.\n\nIf you visit `localhost:3000/api/post <http://localhost:3000/api/post>`_ in\nyour browser, you should see a plaintext JSON representation of the blog posts\nwe inserted earlier.\n\nTo fetch these from the homepage, we'll use ``useState``, ``useEffect``, and\nthe built-in ``fetch`` API. At the top of the ``HomePage`` component in\n``pages/index.tsx``, replace the static data and add the missing imports.\n\n.. code-block:: tsx-diff\n\n     // pages/index.tsx\n  +  import {useState, useEffect} from 'react';\n\n     type Post = {\n       id: string;\n       title: string;\n       content: string;\n     };\n\n     const HomePage: NextPage = () => {\n  -    const posts: Post[] = [\n  -      {\n  -        id: 'post1',\n  -        title: 'This one weird trick makes using databases fun',\n  -        content: 'Use Gel',\n  -      },\n  -      {\n  -        id: 'post2',\n  -        title: 'How to build a blog with Gel and Next.js',\n  -        content: \"Let's start by scaffolding our app...\",\n  -      },\n  -    ];\n\n  +    const [posts, setPosts] = useState<Post[] | null>(null);\n  +    useEffect(() => {\n  +      fetch(`/api/post`)\n  +        .then((result) => result.json())\n  +        .then(setPosts);\n  +    }, []);\n  +    if (!posts) return <p>Loading...</p>;\n\n       return <div>...</div>;\n     }\n\nWhen you refresh the page, you should briefly see a ``Loading...`` indicator\nbefore the homepage renders the (dynamically loaded!) blog posts.\n\nGenerating the query builder\n----------------------------\n\nSince we're using TypeScript, it makes sense to use Gel's powerful query\nbuilder. This provides a schema-aware client API that makes writing strongly\ntyped EdgeQL queries easy and painless. The result type of our queries will be\nautomatically inferred, so we won't need to manually type something like\n``type Post = { id: string; ... }``.\n\nFirst, install the generator to your project.\n\n.. code-block:: bash\n\n  $ yarn add --dev @gel/generate\n\nThen generate the query builder with the following command.\n\n.. code-block:: bash\n\n  $ npx @gel/generate edgeql-js\n  Generating query builder...\n  Detected tsconfig.json, generating TypeScript files.\n     To override this, use the --target flag.\n     Run `npx @gel/generate --help` for full options.\n  Introspecting database schema...\n  Writing files to ./dbschema/edgeql-js\n  Generation complete! 🤘\n  Checking the generated query builder into version control\n  is not recommended. Would you like to update .gitignore to ignore\n  the query builder directory? The following line will be added:\n\n     dbschema/edgeql-js\n\n  [y/n] (leave blank for \"y\")\n  > y\n\n\nThis command introspected the schema of our database and generated some code\nin the ``dbschema/edgeql-js`` directory. It also asked us if we wanted to add\nthe generated code to our ``.gitignore``; typically it's not good practice to\ninclude generated files in version control.\n\nBack in ``pages/api/post.ts``, let's update our code to use the query builder\ninstead.\n\n.. code-block:: typescript-diff\n\n    // pages/api/post.ts\n\n    import type {NextApiRequest, NextApiResponse} from 'next';\n    import {createClient} from 'gel';\n  + import e, {$infer} from '../../dbschema/edgeql-js';\n\n    export const client = createClient();\n\n  + const selectPosts = e.select(e.BlogPost, () => ({\n  +   id: true,\n  +   title: true,\n  +   content: true,\n  + }));\n\n  + export type Posts = $infer<typeof selectPosts>;\n\n    export default async function handler(\n      req: NextApiRequest,\n      res: NextApiResponse\n    ) {\n  -   const posts = await client.query(`select BlogPost {\n  -     id,\n  -     title,\n  -     content\n  -   };`);\n  +   const posts = await selectPosts.run(client);\n      res.status(200).json(posts);\n    }\n\nInstead of writing our query as a plain string, we're now using the query\nbuilder to declare our query in a code-first way. As you can see we import the\nquery builder as a single default import ``e`` from the ``dbschema/edgeql-js``\ndirectory.\n\nWe're also using a utility called ``$infer`` to extract the inferred type of\nthis query. In VSCode you can hover over ``Posts`` to see what this type is.\n\n.. image::\n    /docs/tutorials/nextjs/inference.png\n    :alt: Inferred type of posts query\n    :width: 100%\n\nBack in ``pages/index.tsx``, let's update our code to use the inferred\n``Posts`` type instead of our manual type declaration.\n\n.. code-block:: typescript-diff\n\n     // pages/index.tsx\n\n     import type {NextPage} from 'next';\n     import Head from 'next/head';\n     import {useEffect, useState} from 'react';\n     import styles from '../styles/Home.module.css';\n  +  import {Posts} from \"./api/post\";\n\n  -  type Post = {\n  -    id: string;\n  -    title: string;\n  -    content: string;\n  -  };\n\n     const Home: NextPage = () => {\n\n  +    const [posts, setPosts] = useState<Posts | null>(null);\n       // ...\n\n     }\n\nNow, when we update our ``selectPosts`` query, the type of our dynamically\nloaded ``posts`` variable will update automatically—no need to keep\nour type definitions in sync with our API logic!\n\nRendering blog posts\n--------------------\n\nOur homepage renders a list of links to each of our blog posts, but we haven't\nimplemented the page that actually displays the posts. Let's create a new page\nat ``pages/post/[id].tsx``. This is a\n`dynamic route <https://nextjs.org/docs/routing/dynamic-routes>`_ that\nincludes an ``id`` URL parameter. We'll use this parameter to fetch the\nappropriate post from the database.\n\nCreate ``pages/post/[id].tsx`` and add the following code. We're using\n``getServerSideProps`` to load the blog post data server-side, to avoid\nloading spinners and ensure the page loads fast.\n\n.. code-block:: tsx\n\n  import React from 'react';\n  import {GetServerSidePropsContext, InferGetServerSidePropsType} from 'next';\n\n  import {client} from '../api/post';\n  import e from '../../dbschema/edgeql-js';\n\n  export const getServerSideProps = async (\n    context?: GetServerSidePropsContext\n  ) => {\n    const post = await e\n      .select(e.BlogPost, (post) => ({\n        id: true,\n        title: true,\n        content: true,\n        filter_single: e.op(\n          post.id,\n          '=',\n          e.uuid(context!.params!.id as string)\n        ),\n      }))\n      .run(client);\n    return {props: {post: post!}};\n  };\n\n  export type GetPost = InferGetServerSidePropsType<typeof getServerSideProps>;\n\n  const Post: React.FC<GetPost> = (props) => {\n    return (\n      <div\n        style={{\n          margin: 'auto',\n          width: '100%',\n          maxWidth: '600px',\n        }}\n      >\n        <h1 style={{padding: '50px 0px'}}>{props.post.title}</h1>\n        <p style={{color: '#666'}}>{props.post.content}</p>\n      </div>\n    );\n  };\n\n  export default Post;\n\n\nInside ``getServerSideProps`` we're extracting the ``id`` parameter from\n``context.params`` and using it in our EdgeQL query. The query is a ``select``\nquery that fetches the ``id``, ``title``, and ``content`` of the post with a\nmatching ``id``.\n\nWe're using Next's ``InferGetServerSidePropsType`` utility to extract the\ninferred type of our query and pass it into ``React.FC``. Now, if we update\nour query, the type of the component props will automatically update too. In\nfact, this entire application is end-to-end typesafe.\n\nNow, click on one of the blog post links on the homepage. This should bring\nyou to ``/post/<uuid>``, which should display something like this:\n\n.. image::\n    /docs/tutorials/nextjs/post.png\n    :alt: Basic blog homepage with static content\n    :width: 100%\n\nDeploying to Vercel\n-------------------\n\n**#1 Deploy Gel**\n\nFirst deploy a Gel instance on your preferred cloud provider:\n\n- :ref:`AWS <ref_guide_deployment_aws_aurora_ecs>`\n- :ref:`Azure <ref_guide_deployment_azure_flexibleserver>`\n- :ref:`DigitalOcean <ref_guide_deployment_digitalocean>`\n- :ref:`Fly.io <ref_guide_deployment_fly_io>`\n- :ref:`Google Cloud <ref_guide_deployment_gcp>`\n\nor use a cloud-agnostic deployment method:\n\n- :ref:`Docker <ref_guide_deployment_docker>`\n- :ref:`Bare metal <ref_guide_deployment_bare_metal>`\n\n**#2. Find your instance's DSN**\n\nThe DSN is also known as a connection string. It will have the format\n:geluri:`username:password@hostname:port`. The exact instructions for this\ndepend on which cloud you are deploying to.\n\n**#3 Apply migrations**\n\nUse the DSN to apply migrations against your remote instance.\n\n.. code-block:: bash\n\n  $ npx gel migrate --dsn <your-instance-dsn> --tls-security insecure\n\n.. note::\n\n  You have to disable TLS checks with ``--tls-security insecure``. All Gel\n  instances use TLS by default, but configuring it is out of scope of this\n  project.\n\nOnce you've applied the migrations, consider creating some sample data in your\ndatabase. Open a REPL and ``insert`` some blog posts:\n\n.. code-block:: bash\n\n  $ npx gel --dsn <your-instance-dsn> --tls-security insecure\n  Gel x.x (repl x.x)\n  Type \\help for help, \\quit to quit.\n  gel> insert BlogPost { title := \"Test post\" };\n  {default::BlogPost {id: c00f2c9a-cbf5-11ec-8ecb-4f8e702e5789}}\n\n\n**#4 Set up a `prebuild` script**\n\nAdd the following ``prebuild`` script to your ``package.json``. When Vercel\ninitializes the build, it will trigger this script which will generate the\nquery builder. The ``npx @gel/generate edgeql-js`` command will read the\nvalue of the :gelenv:`DSN` variable, connect to the database, and generate the\nquery builder before Vercel starts building the project.\n\n.. code-block:: javascript-diff\n\n    // package.json\n    \"scripts\": {\n      \"dev\": \"next dev\",\n      \"build\": \"next build\",\n      \"start\": \"next start\",\n      \"lint\": \"next lint\",\n  +   \"prebuild\": \"npx @gel/generate edgeql-js\"\n    },\n\n**#5 Deploy to Vercel**\n\nDeploy this app to Vercel with the button below.\n\n.. XXX -- update URL\n.. lint-off\n\n.. image:: https://vercel.com/button\n  :width: 150px\n  :target: https://vercel.com/new/git/external?repository-url=https://github.com/geldata/gel-examples/tree/main/nextjs-blog&project-name=nextjs-edgedb-blog&repository-name=nextjs-edgedb-blog&env=GEL_DSN,GEL_CLIENT_TLS_SECURITY\n\n.. lint-on\n\nWhen prompted:\n\n- Set :gelenv:`DSN` to your database's DSN\n- Set :gelenv:`CLIENT_TLS_SECURITY` to ``insecure``. This will disable\n  Gel's default TLS checks; configuring TLS is beyond the scope of this\n  tutorial.\n\n.. image::\n    /docs/tutorials/nextjs/env.png\n    :alt: Setting environment variables in Vercel\n    :width: 100%\n\n\n**#6 View the application**\n\nOnce deployment has completed, view the application at the deployment URL\nsupplied by Vercel.\n\nWrapping up\n-----------\n\nAdmittedly this isn't the prettiest blog of all time, or the most\nfeature-complete. But this tutorial demonstrates how to work with Gel in a\nNext.js app, including data fetching with API routes and\n``getServerSideProps``.\n\nThe next step is to add a ``/newpost`` page with a form for writing new blog\nposts and saving them into Gel. That's left as an exercise for the reader.\n\nTo see the final code for this tutorial, refer to\n`github.com/geldata/gel-examples/tree/main/nextjs-blog\n<https://github.com/geldata/gel-examples/tree/main/nextjs-blog>`_.\n"
  },
  {
    "path": "docs/resources/guides/tutorials/rest_apis_with_fastapi.rst",
    "content": ".. _ref_guide_rest_apis_with_fastapi:\n\n=======\nFastAPI\n=======\n\n:edb-alt-title: Building a REST API with Gel and FastAPI\n\nBecause FastAPI encourages and facilitates strong typing, it's a natural\npairing with Gel. Our Python code generation generates not only typed\nquery functions but result types you can use to annotate your endpoint handler\nfunctions.\n\n|Gel| can help you quickly build REST APIs in Python without getting into the\nrigmarole of using ORM libraries to handle your data effectively. Here, we'll\nbe using `FastAPI <https://fastapi.tiangolo.com/>`_ to expose the API endpoints\nand Gel to store the content.\n\nWe'll build a simple event management system where you'll be able to fetch,\ncreate, update, and delete *events* and *event hosts* via RESTful API\nendpoints.\n\nPrerequisites\n=============\n\nBefore we start, make sure you've :ref:`installed <ref_admin_install>` the\n|gelcmd| command line tool. For this tutorial, we'll use Python 3.10 to\ntake advantage of the asynchronous I/O paradigm to communicate with the\ndatabase more efficiently. You can use newer versions of Python if you prefer,\nbut you may need to adjust the code accordingly. If you want to skip ahead,\nthe completed source code for this API can be found `in our examples repo\n<https://github.com/geldata/gel-examples/tree/main/fastapi-crud>`_. If you\nwant to check out an example with Gel Auth, you can find that in the same\nrepo in the `fastapi-crud-auth folder\n<https://github.com/geldata/gel-examples/tree/main/fastapi-crud-auth>`_.\n\n\nCreate a project directory\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nTo get started, create a directory for your project and change into it.\n\n.. code-block:: bash\n\n    $ mkdir fastapi-crud\n    $ cd fastapi-crud\n\n\nInstall the dependencies\n^^^^^^^^^^^^^^^^^^^^^^^^\n\nCreate a Python virtual environment, activate it, and\ninstall the dependencies with this command (in Linux/macOS; see the following\nnote for help with Windows):\n\n.. code-block:: bash\n\n    $ python -m venv myvenv\n    $ source myvenv/bin/activate\n    $ pip install gel fastapi 'httpx[cli]' uvicorn\n\n.. note::\n\n    Make sure you run ``source myvenv/bin/activate`` any time you want to come\n    back to this project to activate its virtual environment. If not, you may\n    start working under your system's default Python environment which could be\n    the incorrect version or not have the dependencies installed. If you want\n    to confirm you're using the right environment, run ``which python``. You\n    should see that the current ``python`` is inside your venv directory.\n\n.. note::\n\n    The commands will differ for Windows/Powershell users; `this guide\n    <https://realpython.com/python-virtual-environments-a-primer/>`_ provides\n    instructions for working with virtual environments across a range of OSes,\n    including Windows.\n\nInitialize the database\n^^^^^^^^^^^^^^^^^^^^^^^\n\nNow, let's initialize a Gel project. From the project's root directory:\n\n.. code-block:: bash\n\n    $ gel project init\n    No `gel.toml` found in `<project-path>` or above\n    Do you want to initialize a new project? [Y/n]\n    > Y\n    Specify the name of Gel instance to use with this project [default:\n    fastapi_crud]:\n    > fastapi_crud\n    Checking Gel versions...\n    Specify the version of Gel to use with this project [default: 2.7]:\n    > 2.7\n\nOnce you've answered the prompts, a new Gel instance called ``fastapi_crud``\nwill be created and started. If you see ``Project initialized``, you're ready.\n\n\nConnect to the database\n^^^^^^^^^^^^^^^^^^^^^^^\n\nLet's test that we can connect to the newly started instance. To do so, run:\n\n.. code-block:: bash\n\n    $ gel\n\nYou should see this prompt indicating you are now connected to your new\ndatabase instance:\n\n::\n\n    Gel x.x (repl x.x)\n    Type \\help for help, \\quit to quit.\n    gel>\n\nYou can start writing queries here. Since this database is empty, that won't\nget you very far, so let's start designing our data model instead.\n\nSchema design\n=============\n\nThe event management system will have two entities: **events** and **users**.\nEach *event* can have an optional link to a *user* who is that event's host.\nThe goal is to create API endpoints that'll allow us to fetch, create, update,\nand delete the entities while maintaining their relationships.\n\n|Gel| allows us to declaratively define the structure of the entities. If\nyou've worked with SQLAlchemy or Django ORM, you might refer to these\ndeclarative schema definitions as *models*. In Gel we call them\n\"object types\".\n\nThe schema lives inside |.gel| files in the ``dbschema`` directory. It's\ncommon to declare the entire schema in a single file\n:dotgel:`dbschema/default`. This file is created for you when you run\n:gelcmd:`project init`, but you'll need to fill it with your schema.\nThis is what our datatypes look like:\n\n.. code-block:: sdl\n    :caption: dbschema/default.gel\n\n    module default {\n      abstract type Auditable {\n        required created_at: datetime {\n          readonly := true;\n          default := datetime_current();\n        }\n      }\n\n      type User extending Auditable {\n        required name: str {\n          constraint exclusive;\n          constraint max_len_value(50);\n        };\n      }\n\n      type Event extending Auditable {\n        required name: str {\n          constraint exclusive;\n          constraint max_len_value(50);\n        }\n        address: str;\n        schedule: datetime;\n        link host: User;\n      }\n    }\n\nHere, we've defined an ``abstract`` type called ``Auditable`` to take advantage\nof Gel's schema mixin system. This allows us to add a ``created_at``\nproperty to multiple types without repeating ourselves. Abstract types\ndon't have any concrete footprints in the database, as they don't hold any\nactual data. Their only job is to propagate properties, links, and constraints\nto the types that extend them.\n\nThe ``User`` type extends ``Auditable`` and inherits the ``created_at``\nproperty as a result. Since ``created_at`` has a ``default`` value, it's\nauto-filled with the return value of the ``datetime_current`` function. Along\nwith the property conveyed to it by the extended type, the ``User`` type\ndefines its own concrete required property called ``name``. We impose two\nconstraints on this property: names should be unique (``constraint exclusive``)\nand shorter than 50 characters (``constraint max_len_value(50)``).\n\nWe also define an ``Event`` type that extends the ``Auditable`` abstract type.\nIt contains its own concrete properties and links: ``address``, ``schedule``,\nand an optional link called ``host`` that corresponds to a ``User``.\n\nRun a migration\n===============\n\nWith the schema created, it's time to lock it in. The first step is to create a\nmigration.\n\n.. code-block:: bash\n\n    $ gel migration create\n\nWhen this step is successful, you'll see\n``Created dbschema/migrations/00001.edgeql``.\n\nNow run the migration we just created.\n\n.. code-block:: bash\n\n    $ gel migrate\n\nOnce this is done, you'll see ``Applied`` along with the migration's ID. I like\nto go one step further in verifying success and see the schema applied to my\ndatabase. To do that, first fire up the Gel console:\n\n.. code-block:: bash\n\n    $ gel\n\nIn the console, type ``\\ds`` (for \"describe schema\"). If everything worked, we\nshould output very close to the schema we added in the :dotgel:`default` file:\n\n::\n\n    module default {\n        abstract type Auditable {\n            required property created_at: std::datetime {\n                default := (std::datetime_current());\n                readonly := true;\n            };\n        };\n        type Event extending default::Auditable {\n            link host: default::User;\n            property address: std::str;\n            required property name: std::str {\n                constraint std::exclusive;\n                constraint std::max_len_value(50);\n            };\n            property schedule: std::datetime;\n        };\n        type User extending default::Auditable {\n            required property name: std::str {\n                constraint std::exclusive;\n                constraint std::max_len_value(50);\n            };\n        };\n    };\n\nBuild the API endpoints\n=======================\n\nWith the schema established, we're ready to start building out the app. Let's\nstart by creating an ``app`` directory inside our project:\n\n.. code-block:: bash\n\n    $ mkdir app\n\nWithin this ``app`` directory, we're going to create three modules:\n``events.py`` and ``users.py`` which represent the events and users APIs\nrespectively, and ``main.py`` that registers all the endpoints and exposes them\nto the `uvicorn <https://www.uvicorn.org>`_ webserver. We also need an\n``__init__.py`` to mark this directory as a package so we can easily import\nfrom it. Go ahead and create that file now in your editor or via the command\nline like this (from the project root):\n\n.. code-block:: bash\n\n    $ touch app/__init__.py\n\nWe'll work on the users API first since it's the simpler of the two.\n\n\nUsers API\n^^^^^^^^^\n\nWe want this app to be type safe, end to end. To achieve this, instead of\nhard-coding string queries into the app, we'll use code generation to generate\ntypesafe functions from queries we write in ``.edgeql`` files. These files are\nsimple text files containing the queries we want our app to be able to run.\n\nThe code generator will search through our project for all files with the\n``.edgeql`` extension and generate those functions for us as individual Python\nmodules. When you installed the Gel client (via ``pip install gel``), the\ncode generator was installed alongside it, so you're already ready to go. We\njust need to write those queries!\n\nWe'll write queries for one endpoint at a time to start so you can see how the\npieces fit together. To keep things organized, create a new directory inside\n``app`` called ``queries``. Create a new file in ``app/queries`` named\n``get_users.edgeql`` and open it in your editor. Write the query into this\nfile. It's the same one we would have written inline in our Python code as\nshown in the code block above:\n\n.. code-block:: edgeql\n    :caption: app/queries/get_users.edgeql\n\n    select User {name, created_at};\n\nWe need one more query to finish off this endpoint. Create another file inside\n``app/queries`` named ``get_user_by_name.edgeql`` and open it in your editor.\nAdd this query:\n\n.. code-block:: edgeql\n\n    select User {name, created_at}\n    filter User.name = <str>$name\n\nSave that file and get ready to kick off the magic that is code generation! 🪄\n\n.. code-block:: bash\n\n    $ gel-py\n    Found Gel project: <project-path>\n    Processing <project-path>/app/queries/get_user_by_name.edgeql\n    Processing <project-path>/app/queries/get_users.edgeql\n    Generating <project-path>/app/queries/get_user_by_name.py\n    Generating <project-path>/app/queries/get_users.py\n\nThe code generator creates one module per query file by default and places them\nat the same path as the query files.\n\nWith code generated, we're ready to write an endpoint. Let's create the ``GET\n/users`` endpoint so that we can request the ``User`` objects saved in the\ndatabase. Create a new file ``app/users.py``, open it in your editor, and add\nthe following code:\n\n.. lint-off\n\n.. code-block:: python\n    :caption: app/users.py\n\n    from __future__ import annotations\n\n    import datetime\n    from http import HTTPStatus\n    from typing import List\n\n    import gel\n    from fastapi import APIRouter, HTTPException, Query\n    from pydantic import BaseModel\n\n    from .queries import get_user_by_name_async_edgeql as get_user_by_name_qry\n    from .queries import get_users_async_edgeql as get_users_qry\n\n    router = APIRouter()\n    client = gel.create_async_client()\n\n\n    class RequestData(BaseModel):\n        name: str\n\n\n    @router.get(\"/users\")\n    async def get_users(\n        name: str = Query(None, max_length=50)\n    ) -> List[get_users_qry.GetUsersResult] | get_user_by_name_qry.GetUserByNameResult:\n\n        if not name:\n            users = await get_users_qry.get_users(client)\n            return users\n        else:\n            user = await get_user_by_name_qry.get_user_by_name(client, name=name)\n            return user\n\n.. lint-on\n\nWe've imported the generated code and aliased it (using ``as <new-name>``) to\nmake the module names we use in our code a bit neater.\n\nThe ``APIRouter`` instance does the actual work of exposing the API. We also\ncreate an async Gel client instance to communicate with the database.\n\nBy default, this API will return a list of all users, but you can also filter\nthe user objects by name. We have the ``RequestData`` class to handle the data\nan API consumer will need to send in case they want to get only a single user.\nThe types we're using in the return annotation have been generated by the\n|Gel| code generation based on the queries we wrote and our database's schema.\n\nNote that we're also calling the appropriate generated function based on\nwhether or not the API consumer passes an argument for ``name``.\n\nThis nearly gets us there but not quite. We have one potential outcome not\naccounted for: a query for a user by name that returns no results. In that\ncase, we'll want to return a 404 (not found).\n\nTo fix it, we'll check in the else case whether we got anything back\nfrom the single user query. If not, we'll go ahead and raise an exception. This\nwill send the 404 (not found) response to the user.\n\n.. lint-off\n\n.. code-block:: python\n    :caption: app/users.py\n\n    ...\n    if not name:\n        users = await get_users_qry.get_users(client)\n        return users\n    else:\n        user = await get_user_by_name_qry.get_user_by_name(client, name=name)\n        if not user:\n            raise HTTPException(\n                status_code=HTTPStatus.NOT_FOUND,\n                detail={\"error\": f\"Username '{name}' does not exist.\"},\n            )\n        return user\n    ...\n\n.. lint-on\n\nTo summarize, in the ``get_users`` function, we use our generated code to\nperform asynchronous queries via the ``gel`` client. Then we return the\nquery results. Afterward, the JSON serialization part is taken care of by\nFastAPI.\n\nBefore we can use this endpoint, we need to expose it to the server. We'll do\nthat in the ``main.py`` module. Create ``app/main.py`` and open it in your\neditor. Here's the content of the module:\n\n.. code-block:: python\n    :caption: app/main.py\n\n    from __future__ import annotations\n\n    from fastapi import FastAPI\n    from starlette.middleware.cors import CORSMiddleware\n\n    from app import users\n\n    fast_api = FastAPI()\n\n    # Set all CORS enabled origins.\n    fast_api.add_middleware(\n        CORSMiddleware,\n        allow_origins=[\"*\"],\n        allow_credentials=True,\n        allow_methods=[\"*\"],\n        allow_headers=[\"*\"],\n    )\n\n\n    fast_api.include_router(users.router)\n\nHere, we import everything we need, including our own ``users`` module\ncontaining the router and endpoint logic for the users API. We instantiate the\nAPI, give it a permissive CORS configuration, and give it the users router.\n\nTo test the endpoint, go to the project root and run:\n\n.. code-block:: bash\n\n    $ uvicorn app.main:fast_api --port 5001 --reload\n\nThis will start a ``uvicorn`` server and you'll be able to start making\nrequests against it. Earlier, we installed the\n`HTTPx <https://www.python-httpx.org/>`_ client library to make HTTP requests\nprogrammatically. It also comes with a neat command-line tool that we'll use to\ntest our API.\n\nWhile the ``uvicorn`` server is running, bring up a new console. Activate your\nvirtual environment by running ``source myenv/bin/activate`` and run:\n\n.. code-block:: bash\n\n    $ httpx -m GET http://localhost:5001/users\n\nYou'll see the following output on the console:\n\n::\n\n    HTTP/1.1 200 OK\n    date: Sat, 16 Apr 2022 22:58:11 GMT\n    server: uvicorn\n    content-length: 2\n    content-type: application/json\n\n    []\n\n.. note::\n\n    If you find yourself with a result you don't expect when making a request\n    to your API, switch over to the uvicorn server console. You should find a\n    traceback that will point you to the problem area in your code.\n\nIf you see this result, that means the API is working! It's not especially\nuseful though. Our request yields an empty list because the database is\ncurrently empty. Let's create the ``POST /users`` endpoint in ``app/users.py``\nto start saving users in the database. Before we do that though, let's go ahead\nand create the new query we'll need.\n\nCreate and open ``app/queries/create_user.edgeql`` and fill it with this query:\n\n.. code-block:: edgeql\n    :caption: app/queries/create_user.edgeql\n\n    select (insert User {\n        name := <str>$name\n    }) {\n        name,\n        created_at\n    };\n\n.. note::\n\n    We're running our ``insert`` inside a ``select`` here so that we can return\n    the ``name`` and ``created_at`` properties. If we just ran the ``insert``\n    bare, it would return only the ``id``.\n\nSave the file and run ``gel-py`` to generate the new function. Now,\nwe're ready to open ``app/users.py`` again and add the POST endpoint. First,\nimport the generated function for the new query:\n\n.. code-block:: python\n    :caption: app/users.py\n\n    ...\n    from .queries import create_user_async_edgeql as create_user_qry\n    from .queries import get_user_by_name_async_edgeql as get_user_by_name_qry\n    from .queries import get_users_async_edgeql as get_users_qry\n    ...\n\nThen write the endpoint to call that function:\n\n.. lint-off\n\n.. code-block:: python\n    :caption: app/users.py\n\n    ...\n    @router.post(\"/users\", status_code=HTTPStatus.CREATED)\n    async def post_user(user: RequestData) -> create_user_qry.CreateUserResult:\n\n        try:\n            created_user = await create_user_qry.create_user(client, name=user.name)\n        except gel.errors.ConstraintViolationError:\n            raise HTTPException(\n                status_code=HTTPStatus.BAD_REQUEST,\n                detail={\"error\": f\"Username '{user.name}' already exists.\"},\n            )\n        return created_user\n\n.. lint-on\n\nIn the above snippet, we ingest data with the shape dictated by the\n``RequestData`` model and return a payload of the query results. The\n``try...except`` block gracefully handles the situation where the API consumer\nmight try to create multiple users with the same name. A successful request\nwill yield the status code HTTP 201 (created) along with the new user's\n``id``, ``name``, and ``created_at`` as JSON.\n\nTo test it out, make a request as follows:\n\n.. code-block:: bash\n\n    $ httpx -m POST http://localhost:5001/users \\\n            --json '{\"name\" : \"Jonathan Harker\"}'\n\nThe output should look similar to this:\n\n::\n\n    HTTP/1.1 201 Created\n    ...\n    {\n      \"id\": \"53771f56-6f57-11ed-8729-572f5fba7ddc\",\n      \"name\": \"Jonathan Harker\",\n      \"created_at\": \"2022-04-16T23:09:30.929664+00:00\"\n    }\n\n.. note::\n\n    Since IDs are generated, your ``id`` values probably won't match the values\n    in this guide. This is not a problem.\n\nIf you try to make the same request again, it'll throw an HTTP 400\n(bad request) error:\n\n::\n\n    HTTP/1.1 400 Bad Request\n    ...\n    {\n    \"detail\": {\n      \"error\": \"Username 'Jonathan Harker' already exists.\"\n      }\n    }\n\nBefore we move on to the next step, create 2 more users called\n``Count Dracula`` and ``Mina Murray``. Once you've done that, we can move on to\nthe next step of building the ``PUT /users`` endpoint to update existing user\ndata.\n\nWe'll start again with the query. Create a new file in ``app/queries`` named\n``update_user.edgeql``. Open it in your editor and enter this query:\n\n.. code-block:: edgeql\n    :caption: app/queries/update_user.edgeql\n\n    select (\n        update User filter .name = <str>$current_name\n            set {name := <str>$new_name}\n    ) {name, created_at};\n\nSave the file and generate again using ``gel-py``. Now, we'll import that\nand add the endpoint over in ``app/users.py``.\n\n.. lint-off\n\n.. code-block:: python\n    :caption: app/users.py\n\n    ...\n    from .queries import create_user_async_edgeql as create_user_qry\n    from .queries import get_user_by_name_async_edgeql as get_user_by_name_qry\n    from .queries import get_users_async_edgeql as get_users_qry\n    from .queries import update_user_async_edgeql as update_user_qry\n    ...\n    @router.put(\"/users\")\n    async def put_user(\n        user: RequestData, current_name: str\n    ) -> update_user_qry.UpdateUserResult:\n        try:\n            updated_user = await update_user_qry.update_user(\n                client,\n                new_name=user.name,\n                current_name=current_name,\n            )\n        except gel.errors.ConstraintViolationError:\n            raise HTTPException(\n                status_code=HTTPStatus.BAD_REQUEST,\n                detail={\"error\": f\"Username '{user.name}' already exists.\"},\n            )\n\n        if not updated_user:\n            raise HTTPException(\n                status_code=HTTPStatus.NOT_FOUND,\n                detail={\"error\": f\"User '{current_name}' was not found.\"},\n            )\n        return updated_user\n\n.. lint-on\n\nNot much new happening here. We wrote our query with a ``current_name``\nparameter for finding the user to be updated. The ``user`` argument will give\nus the changes to make to that user, which in this case can only be the\n``name`` since that's the only property a user has. We pull the name out of\n``user`` and pass it as our ``new_name`` argument to the generated function.\nThe endpoint calls the generated function passing the client and those two\nvalues, and the user is updated.\n\nWe've accounted for the possibility of a user trying to change a user's name to\na new name that conflicts with a different user. That will return a 400 (bad\nrequest) error. We've also accounted for the possibility of a user trying to\nupdate a user that doesn't exist, which will return a 404 (not found).\n\nLet's save everything and test this out.\n\n.. code-block:: bash\n\n    $ httpx -m PUT http://localhost:5001/users \\\n            -p 'current_name' 'Jonathan Harker' \\\n            --json '{\"name\" : \"Dr. Van Helsing\"}'\n\nThis will return:\n\n::\n\n    HTTP/1.1 200 OK\n    ...\n    [\n      {\n        \"id\": \"53771f56-6f57-11ed-8729-572f5fba7ddc\",\n        \"name\": \"Dr. Van Helsing\",\n        \"created_at\": \"2022-04-16T23:09:30.929664+00:00\"\n      }\n    ]\n\nIf you try to change the name of a user to match that of an existing user, the\nendpoint will throw an HTTP 400 (bad request) error:\n\n.. code-block:: bash\n\n    $ httpx -m PUT http://localhost:5001/users \\\n            -p 'current_name' 'Count Dracula' \\\n            --json '{\"name\" : \"Dr. Van Helsing\"}'\n\nThis returns:\n\n::\n\n    HTTP/1.1 400 Bad Request\n    ...\n    {\n      \"detail\": {\n        \"error\": \"Username 'Dr. Van Helsing' already exists.\"\n      }\n    }\n\nSince we've verified that endpoint is working, let's move on to the ``DELETE\n/users`` endpoint. It'll allow us to query the name of the targeted object to\ndelete it.\n\nStart by creating ``app/queries/delete_user.edgeql`` and filling it with this\nquery:\n\n.. code-block:: edgeql\n    :caption: app/queries/delete_user.edgeql\n\n    select (\n        delete User filter .name = <str>$name\n    ) {name, created_at};\n\nGenerate the new function by again running ``gel-py``. Then re-open\n``app/users.py``. This endpoint's code will look similar to the endpoints\nwe've already written:\n\n.. lint-off\n\n.. code-block:: python\n    :caption: app/users.py\n\n    ...\n    from .queries import create_user_async_edgeql as create_user_qry\n    from .queries import delete_user_async_edgeql as delete_user_qry\n    from .queries import get_user_by_name_async_edgeql as get_user_by_name_qry\n    from .queries import get_users_async_edgeql as get_users_qry\n    from .queries import update_user_async_edgeql as update_user_qry\n    ...\n    @router.delete(\"/users\")\n    async def delete_user(name: str) -> delete_user_qry.DeleteUserResult:\n        try:\n            deleted_user = await delete_user_qry.delete_user(\n                client,\n                name=name,\n            )\n        except gel.errors.ConstraintViolationError:\n            raise HTTPException(\n                status_code=HTTPStatus.BAD_REQUEST,\n                detail={\"error\": \"User attached to an event. Cannot delete.\"},\n            )\n\n        if not deleted_user:\n            raise HTTPException(\n                status_code=HTTPStatus.NOT_FOUND,\n                detail={\"error\": f\"User '{name}' was not found.\"},\n            )\n        return deleted_user\n\n.. lint-on\n\nThis endpoint will simply delete the requested user if the user isn't attached\nto any event. If the targeted object *is* attached to an event, the API will\nthrow an HTTP 400 (bad request) error and refuse to delete the object. To\ntest it out by deleting ``Count Dracula``, on your console, run:\n\n.. code-block:: bash\n\n    $ httpx -m DELETE http://localhost:5001/users \\\n            -p 'name' 'Count Dracula'\n\nIf it worked, you should see this result:\n\n::\n\n    HTTP/1.1 200 OK\n    ...\n    [\n      {\n        \"id\": \"e6837562-6f55-11ed-8744-ff1b295ed864\",\n        \"name\": \"Count Dracula\",\n        \"created_at\": \"2022-04-16T23:23:56.630101+00:00\"\n      }\n    ]\n\nWith that, you've written the entire users API! Now, we move onto the events\nAPI which is slightly more complex. (Nothing you can't handle though. 😁)\n\nEvents API\n^^^^^^^^^^\n\nLet's start with the ``POST /events`` endpoint, and then we'll fetch the\nobjects created via POST using the ``GET /events`` endpoint.\n\nFirst, we need a query. Create a file ``app/queries/create_event.edgeql`` and\ndrop this query into it:\n\n.. code-block:: edgeql\n    :caption: app/queries/create_event.edgeql\n\n    with name := <str>$name,\n        address := <str>$address,\n        schedule := <str>$schedule,\n        host_name := <str>$host_name\n\n    select (\n        insert Event {\n            name := name,\n            address := address,\n            schedule := <datetime>schedule,\n            host := assert_single(\n                (select detached User filter .name = host_name)\n            )\n        }\n    ) {name, address, schedule, host: {name}};\n\nRun ``gel-py`` to generate a function from that query.\n\nCreate a file in ``app`` named ``events.py`` and open it in your editor. It's\ntime to code up the endpoint to use that freshly generated query.\n\n.. lint-off\n\n.. code-block:: python\n    :caption: app/events.py\n\n    from __future__ import annotations\n\n    from http import HTTPStatus\n    from typing import List\n\n    import gel\n    from fastapi import APIRouter, HTTPException, Query\n    from pydantic import BaseModel\n\n    from .queries import create_event_async_edgeql as create_event_qry\n\n    router = APIRouter()\n    client = gel.create_async_client()\n\n\n    class RequestData(BaseModel):\n        name: str\n        address: str\n        schedule: str\n        host_name: str\n\n\n    @router.post(\"/events\", status_code=HTTPStatus.CREATED)\n    async def post_event(event: RequestData) -> create_event_qry.CreateEventResult:\n        try:\n            created_event = await create_event_qry.create_event(\n                client,\n                name=event.name,\n                address=event.address,\n                schedule=event.schedule,\n                host_name=event.host_name,\n            )\n\n        except gel.errors.InvalidValueError:\n            raise HTTPException(\n                status_code=HTTPStatus.BAD_REQUEST,\n                detail={\n                    \"error\": \"Invalid datetime format. \"\n                    \"Datetime string must look like this: \"\n                    \"'2010-12-27T23:59:59-07:00'\",\n                },\n            )\n\n        except gel.errors.ConstraintViolationError:\n            raise HTTPException(\n                status_code=HTTPStatus.BAD_REQUEST,\n                detail=f\"Event name '{event.name}' already exists,\",\n            )\n\n        return created_event\n\n.. lint-on\n\nLike the ``POST /users`` endpoint, the incoming and outgoing shape of the\n``POST /events`` endpoint's data are defined by the ``RequestData`` model and\nthe generated ``CreateEventResult`` model respectively. The ``post_events``\nfunction asynchronously inserts the data into the database and returns the\nfields defined in the ``select`` query we wrote earlier, along with the new\nevent's ``id``.\n\nThe exception handling logic validates the shape of the incoming data. For\nexample, just as before in the users API, the events API will complain if you\ntry to create multiple events with the same name. Also, the field ``schedule``\naccepts data as an `ISO 8601 <https://en.wikipedia.org/wiki/ISO_8601>`_\ntimestamp string. Values not adhering to that will incur an HTTP 400 (bad\nrequest) error.\n\nIt's almost time to test, but before we can do that, we need to expose this new\nAPI in ``app/main.py``. Open that file, and update the import on line 6 to also\nimport ``events``:\n\n.. code-block:: python\n    :caption: app/main.py\n\n    ...\n    from app import users, events\n    ...\n\nDrop down to the bottom of ``main.py`` and include the events router:\n\n.. code-block:: python\n    :caption: app/main.py\n\n    ...\n    fast_api.include_router(events.router)\n\nLet's try it out. Here's how you'd create an event:\n\n.. code-block:: bash\n\n    $ httpx -m POST http://localhost:5001/events \\\n            --json '{\n                      \"name\":\"Resuscitation\",\n                      \"address\":\"Britain\",\n                      \"schedule\":\"1889-07-27T23:59:59-07:00\",\n                      \"host_name\":\"Mina Murray\"\n                    }'\n\nIf everything worked, you'll see output like this:\n\n::\n\n    HTTP/1.1 200 OK\n    ...\n    {\n      \"id\": \"0b1847f4-6f3d-11ed-9f27-6fcdf20ffe22\",\n      \"name\": \"Resuscitation\",\n      \"address\": \"Britain\",\n      \"schedule\": \"1889-07-28T06:59:59+00:00\",\n      \"host\": {\n        \"name\": \"Mina Murray\"\n      }\n    }\n\nTo speed this up a bit, we'll go ahead and write all the remaining queries in\none shot. Then we can flip back to ``app/events.py`` and code up all the\nendpoints. Start by creating a file in ``app/queries`` named\n``get_events.edgeql``. This one is really straightforward:\n\n.. code-block:: edgeql\n    :caption: app/queries/get_events.edgeql\n\n    select Event {name, address, schedule, host : {name}};\n\nSave that one and create ``app/queries/get_event_by_name.edgeql`` with this\nquery:\n\n.. code-block:: edgeql\n    :caption: app/queries/get_event_by_name.edgeql\n\n    select Event {\n        name, address, schedule,\n        host : {name}\n    } filter .name = <str>$name;\n\nThose two will handle queries for ``GET /events``. Next, create\n``app/queries/update_event.edgeql`` with this query:\n\n.. code-block:: edgeql\n    :caption: app/queries/update_event.edgeql\n\n    with current_name := <str>$current_name,\n        new_name := <str>$name,\n        address := <str>$address,\n        schedule := <str>$schedule,\n        host_name := <str>$host_name\n\n    select (\n        update Event filter .name = current_name\n        set {\n            name := new_name,\n            address := address,\n            schedule := <datetime>schedule,\n            host := (select User filter .name = host_name)\n        }\n    ) {name, address, schedule, host: {name}};\n\nThat query will handle PUT requests. The last method left is DELETE. Create\n``app/queries/delete_event.edgeql`` and put this query in it:\n\n.. code-block:: edgeql\n    :caption: app/queries/delete_event.edgeql\n\n    select (\n        delete Event filter .name = <str>$name\n    ) {name, address, schedule, host : {name}};\n\nRun ``gel-py`` to generate the new functions. Open ``app/events.py``\nso we can start getting these functions implemented in the API! We'll start by\ncoding GET. Import the newly generated queries and write the GET endpoint in\n``events.py``:\n\n.. lint-off\n\n.. code-block:: python\n    :caption: app/events.py\n\n    ...\n    from .queries import create_event_async_edgeql as create_event_qry\n    from .queries import delete_event_async_edgeql as delete_event_qry\n    from .queries import get_event_by_name_async_edgeql as get_event_by_name_qry\n    from .queries import get_events_async_edgeql as get_events_qry\n    from .queries import update_event_async_edgeql as update_event_qry\n    ...\n    @router.get(\"/events\")\n    async def get_events(\n        name: str = Query(None, max_length=50)\n    ) -> List[get_events_qry.GetEventsResult] | get_event_by_name_qry.GetEventByNameResult:\n        if not name:\n            events = await get_events_qry.get_events(client)\n            return events\n        else:\n            event = await get_event_by_name_qry.get_event_by_name(client, name=name)\n            if not event:\n                raise HTTPException(\n                    status_code=HTTPStatus.NOT_FOUND,\n                    detail={\"error\": f\"Event '{name}' does not exist.\"},\n                )\n            return event\n\n.. lint-on\n\nSave that file and test it like this:\n\n.. code-block:: bash\n\n    $ httpx -m GET http://localhost:5001/events\n\nWe should get back an array containing all our events (which, at the moment,\nis just the one):\n\n::\n\n    HTTP/1.1 200 OK\n    ...\n    [\n        {\n            \"id\": \"0b1847f4-6f3d-11ed-9f27-6fcdf20ffe22\",\n            \"name\": \"Resuscitation\",\n            \"address\": \"Britain\",\n            \"schedule\": \"1889-07-28T06:59:59+00:00\",\n            \"host\": {\n                \"name\": \"Mina Murray\"\n            }\n        }\n    ]\n\nYou can also use the ``GET /events`` endpoint to return a single event object\nby name. To locate the ``Resuscitation`` event, you'd use the ``name``\nparameter with the GET API as follows:\n\n.. code-block:: bash\n\n    $ httpx -m GET http://localhost:5001/events \\\n            -p 'name' 'Resuscitation'\n\nThat'll return a result that looks like the response we just got without the\n``name`` parameter, except that it's a single object instead of an array.\n\n::\n\n    HTTP/1.1 200 OK\n    ...\n    {\n      \"id\": \"0b1847f4-6f3d-11ed-9f27-6fcdf20ffe22\",\n      \"name\": \"Resuscitation\",\n      \"address\": \"Britain\",\n      \"schedule\": \"1889-07-28T06:59:59+00:00\",\n      \"host\": {\n        \"name\": \"Mina Murray\"\n      }\n    }\n\nIf we'd had multiple events, the response to our first test would have given us\nall of them.\n\nLet's finish off the events API with the PUT and DELETE endpoints. Open\n``app/events.py`` and add this code:\n\n.. lint-off\n\n.. code-block:: python\n    :caption: app/events.py\n\n    ...\n    @router.put(\"/events\")\n    async def put_event(\n        event: RequestData, current_name: str\n    ) -> update_event_qry.UpdateEventResult:\n        try:\n            updated_event = await update_event_qry.update_event(\n                client,\n                current_name=current_name,\n                name=event.name,\n                address=event.address,\n                schedule=event.schedule,\n                host_name=event.host_name,\n            )\n\n        except gel.errors.InvalidValueError:\n            raise HTTPException(\n                status_code=HTTPStatus.BAD_REQUEST,\n                detail={\n                    \"error\": \"Invalid datetime format. \"\n                    \"Datetime string must look like this: '2010-12-27T23:59:59-07:00'\",\n                },\n            )\n\n        except gel.errors.ConstraintViolationError:\n            raise HTTPException(\n                status_code=HTTPStatus.BAD_REQUEST,\n                detail={\"error\": f\"Event name '{event.name}' already exists.\"},\n            )\n\n        if not updated_event:\n            raise HTTPException(\n                status_code=HTTPStatus.INTERNAL_SERVER_ERROR,\n                detail={\"error\": f\"Update event '{event.name}' failed.\"},\n            )\n\n        return updated_event\n\n\n    @router.delete(\"/events\")\n    async def delete_event(name: str) -> delete_event_qry.DeleteEventResult:\n        deleted_event = await delete_event_qry.delete_event(client, name=name)\n\n        if not deleted_event:\n            raise HTTPException(\n                status_code=HTTPStatus.INTERNAL_SERVER_ERROR,\n                detail={\"error\": f\"Delete event '{name}' failed.\"},\n            )\n\n        return deleted_event\n\n.. lint-on\n\nThe events API is now ready to handle updates and deletion. Let's try out a\ncool alternative way to test these new endpoints.\n\n\nBrowse the endpoints using the native OpenAPI doc\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nFastAPI automatically generates OpenAPI schema from the API endpoints and uses\nthose to build the API docs. While the ``uvicorn`` server is running, go to\nyour browser and head over to\n`http://localhost:5001/docs <http://localhost:5001/docs>`_. You should see an\nAPI navigator like this:\n\n.. image::\n    /docs/tutorials/fastapi/openapi.png\n    :alt: FastAPI docs navigator\n    :width: 100%\n\nThis documentation allows you to play with the APIs interactively. Let's try to\nmake a request to the ``PUT /events``. Click on the API that you want to try\nand then click on the **Try it out** button. You can do it in the UI as\nfollows:\n\n.. image::\n    /docs/tutorials/fastapi/put.png\n    :alt: FastAPI docs PUT events API\n    :width: 100%\n\nClicking the **execute** button will make the request and return the following\npayload:\n\n.. image::\n    /docs/tutorials/fastapi/put_result.png\n    :alt: FastAPI docs PUT events API result\n    :width: 100%\n\nYou can do the same to test ``DELETE /events``, just make sure you give it\nwhatever name you set for the event in your previous test of the PUT method.\n\nIntegrating Gel Auth\n====================\n\n|Gel| Auth provides a built-in authentication solution that is deeply\nintegrated with the Gel server. This section outlines how to enable and\nconfigure Gel Auth in your application schema, manage authentication\nproviders, and set key configuration parameters.\n\nSetting up Gel Auth\n^^^^^^^^^^^^^^^^^^^\n\nTo start using Gel Auth, you must first enable it in your schema. Add the\nfollowing to your schema definition:\n\n.. code-block:: sdl\n\n    using extension auth;\n\nOnce added, make sure to apply the schema changes by migrating your database\nschema.\n\n.. code-block:: bash\n\n    $ gel migration create\n    $ gel migrate\n\n\nConfiguring Gel Auth\n--------------------\n\nThe configuration of Gel Auth involves setting various parameters to secure\nand tailor authentication to your needs. For now, we'll focus on the essential\nparameters to get started. You can configure these settings through a Python\nscript, which is recommended for scalability, or you can use the Gel UI for\na more user-friendly approach.\n\n**Auth Signing Key**\n\nThis key is used to sign the JWTs for internal operations. Although it's not\nnecessary for your application's functionality, it's essential for secure\ntoken handling. To generate a secure key, you can use OpenSSL or Python with\nthe following commands:\n\nUsing OpenSSL:\n\n.. code-block:: bash\n\n    $ openssl rand -base64 32\n\nUsing Python:\n\n.. code-block:: python\n\n    import secrets\n    print(secrets.token_urlsafe(32))\n\nOnce you have generated your key, configure it in Gel like this:\n\n.. code-block:: edgeql\n\n    CONFIGURE CURRENT BRANCH SET\n    ext::auth::AuthConfig::auth_signing_key := '<your-generated-key>';\n\n**Allowed redirect URLs**\n\nThis configuration ensures that redirections are limited to domains under your\ncontrol. The ``allowed_redirect_urls`` setting specifies URLs that the Auth\nextension can safely redirect to after authentication. A URL must exactly match\nor be a sub-path of a URL in the list to be considered valid.\n\nTo configure this in your application:\n\n.. code-block:: edgeql\n\n    CONFIGURE CURRENT BRANCH SET\n    ext::auth::AuthConfig::allowed_redirect_urls := {\n        'http://localhost:8000',\n        'http://localhost:8000/auth'\n    };\n\nEnabling authentication providers\n---------------------------------\n\nYou need to configure at least one authentication provider to use Gel Auth.\nThis can be done via the Gel UI or directly through queries.\n\nIn this example, we'll configure a email and password provider. You can add\nit with the following query:\n\n.. code-block:: edgeql\n\n    CONFIGURE CURRENT BRANCH\n    INSERT ext::auth::EmailPasswordProviderConfig {\n        require_verification := false,\n    };\n\n.. note::\n\n    ``require_verification`` defaults to ``true``. In this example, we're\n    setting it to ``false`` to simplify the setup. In a production environment,\n    you should set it to ``true`` to ensure that users verify their email\n    addresses before they can log in.\n\nIf you use the Email and Password provider, in addition to the\n``require_verification`` configuration, you'll need to configure SMTP to allow\n|Gel| to send email verification and password reset emails on your behalf.\n\nHere is an example of setting a local SMTP server, in this case using a\nproduct called `Mailpit <https://mailpit.axllent.org/docs/>`__ which is\ngreat for testing in development:\n\n.. code-block:: edgeql\n\n    CONFIGURE CURRENT BRANCH SET\n    ext::auth::SMTPConfig::sender := 'hello@example.com';\n\n    CONFIGURE CURRENT BRANCH SET\n    ext::auth::SMTPConfig::host := 'localhost';\n\n    CONFIGURE CURRENT BRANCH SET\n    ext::auth::SMTPConfig::port := <int32>1025;\n\n    CONFIGURE CURRENT BRANCH SET\n    ext::auth::SMTPConfig::security := 'STARTTLSOrPlainText';\n\n    CONFIGURE CURRENT BRANCH SET\n    ext::auth::SMTPConfig::validate_certs := false;\n\nYou can query the database configuration to discover which providers are\nconfigured with the following query:\n\n.. code-block:: edgeql\n\n  select cfg::Config.extensions[is ext::auth::AuthConfig].providers {\n      name,\n      [is ext::auth::OAuthProviderConfig].display_name,\n  };\n\nImplementing authentication with FastAPI\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nBelow, we provide a detailed guide to setting up authentication using FastAPI,\nincluding both sign-in and sign-up functionalities.\n\nPKCE flow for enhanced security\n-------------------------------\n\nThe PKCE (Proof Key for Code Exchange) flow enhances security in server-to-server\nauthentication by generating a unique verifier and its corresponding challenge.\nFirst, your server creates a 32-byte Base64 URL-encoded verifier, stores it in an\nHttpOnly cookie, hashes it with SHA256, and then encodes it to form the challenge.\n\nThis implementation ensures enhanced security by preventing token leakage and is\ntailored specifically for server-to-server interactions.\n\nAdd the following code to your FastAPI application to generate the PKCE:\n\n.. code-block:: python\n    :caption: app/auth.py\n\n    import secrets\n    import hashlib\n    import base64\n\n    def generate_pkce():\n        verifier = secrets.token_urlsafe(32)\n        challenge = hashlib.sha256(verifier.encode()).digest()\n        challenge_base64 = base64.urlsafe_b64encode(challenge).decode('utf-8').rstrip('=')\n        return verifier, challenge_base64\n\nUser registration and authentication\n------------------------------------\n\nNext, we're going to create endpoints in FastAPI to handle user registration\n(sign-up) and user login (sign-in):\n\n**Sign-up endpoint**\n\n.. code-block:: python\n    :caption: app/auth.py\n\n    from fastapi import APIRouter, HTTPException, Request\n    from fastapi.responses import JSONResponse\n    import httpx\n\n    router = APIRouter()\n\n    # Value should be:\n    # {protocol}://${host}:${port}/branch/${branch}/ext/auth/\n    GEL_AUTH_BASE_URL = os.getenv('GEL_AUTH_BASE_URL')\n\n    @router.post(\"/auth/signup\")\n    async def handle_signup(request: Request):\n        body = await request.json()\n        email = body.get(\"email\")\n        password = body.get(\"password\")\n\n        if not email or not password:\n            raise HTTPException(status_code=400, detail=\"Missing email or password\")\n\n        verifier, challenge = generate_pkce()\n        register_url = f\"{GEL_AUTH_BASE_URL}/register\"\n        register_response = httpx.post(register_url, json={\n            \"challenge\": challenge,\n            \"email\": email,\n            \"password\": password,\n            \"provider\": \"builtin::local_emailpassword\",\n            \"verify_url\": \"http://localhost:8000/auth/verify\",\n        })\n\n        if register_response.status_code != 200 and register_response.status_code != 201:\n            return JSONResponse(status_code=400, content={\"message\": \"Registration failed\"})\n\n        code = register_response.json().get(\"code\")\n        token_url = f\"{GEL_AUTH_BASE_URL}/token\"\n        token_response = httpx.get(token_url, params={\"code\": code, \"verifier\": verifier})\n\n        if token_response.status_code != 200:\n            return JSONResponse(status_code=400, content={\"message\": \"Token exchange failed\"})\n\n        auth_token = token_response.json().get(\"auth_token\")\n\n        response = JSONResponse(content={\"message\": \"User registered\"})\n        response.set_cookie(key=\"gel-auth-token\", value=auth_token, httponly=True, secure=True, samesite='strict')\n        return response\n\nThe sign-up endpoint sends a POST request to the Gel Auth server to register\na new user. It also sets the auth token as an HttpOnly cookie in the response.\n\n**Sign-in endpoint**\n\n.. code-block:: python\n    :caption: app/auth.py\n\n    @router.post(\"/auth/signin\")\n    async def handle_signin(request: Request):\n        body = await request.json()\n        email = body.get(\"email\")\n        password = body.get(\"password\")\n        provider = body.get(\"provider\")\n\n        if not email or not password or not provider:\n            raise HTTPException(status_code=400, detail=\"Missing email, password, or provider.\")\n\n        verifier, challenge = generate_pkce()\n        authenticate_url = f\"{GEL_AUTH_BASE_URL}/authenticate\"\n        response = httpx.post(authenticate_url, json={\n            \"challenge\": challenge,\n            \"email\": email,\n            \"password\": password,\n            \"provider\": provider,\n        })\n\n        if response.status_code != 200:\n            return JSONResponse(status_code=400, content={\"message\": \"Authentication failed\"})\n\n        code = response.json().get(\"code\")\n        token_url = f\"{GEL_AUTH_BASE_URL}/token\"\n        token_response = httpx.get(token_url, params={\"code\": code, \"verifier\": verifier})\n\n        if token_response.status_code != 200:\n            return JSONResponse(status_code=400, content={\"message\": \"Token exchange failed\"})\n\n        auth_token = token_response.json().get(\"auth_token\")\n        response = JSONResponse(content={\"message\": \"Authentication successful\"})\n        response.set_cookie(key=\"gel-auth-token\", value=auth_token, httponly=True, secure=True, samesite='strict')\n        return response\n\nThe sign-in endpoint sends a POST request to the Gel Auth server to authenticate\na user. It then retrieves the code from the response and exchanges it for an auth\ntoken. The token is set as an HttpOnly cookie in the response.\n\n**Add the auth endpoints to the FastAPI application**\n\nFinally, add the auth endpoints to the FastAPI application:\n\n.. code-block:: python-diff\n    :caption: app/main.py\n\n    + fast_api.include_router(events.router)\n\nCreating a new user in the sign-up endpoint\n-------------------------------------------\n\nNow, let's automatically create a new user in the database when a user signs up.\nWe'll use the ``create_user_async_edgeql`` query we generated earlier\nto achieve this, but we'll need to modify it slightly to link it to the\n|Gel| Auth identity.\n\nFirst, let's update the Gel schema to include a new field in the User type\nto store the Gel Auth identity and a new ``current_user`` type.\n\n.. code-block:: sdl-diff\n    :caption: dbschema/default.gel\n\n    + global current_user := assert_single(\n    +     ((\n    +         select User\n    +         filter .identity = global ext::auth::ClientTokenIdentity\n    +     ))\n    + );\n\n      type User extending Auditable {\n    +    required identity: ext::auth::Identity;\n         required name: str {\n            constraint exclusive;\n            constraint max_len_value(50);\n         };\n      }\n\nAfter updating the schema, run the following command to apply the changes:\n\n.. code-block:: bash\n\n    $ gel migration create\n    $ gel migrate\n\nNext, update the ``create_user_async_edgeql`` query to include the identity:\n\n.. code-block:: edgeql-diff\n    :caption: app/queries/create_user.edgeql\n\n      select (\n          insert User {\n            name := <str>$name,\n    +       identity := <ext::auth::Identity><uuid>$identity_id,\n          }) {\n          name,\n          created_at,\n      };\n\nRun ``gel-py`` to generate the new function. Now, let's update the sign-up\nendpoint to create a new user in the database. We need to do a few things:\n\n1. Import ``gel``.\n\n2. Create a Gel client.\n\n3. Get the identity ID from the Gel Auth server response.\n\n4. Create a new user in the database using the ``create_user_async_edgeql``\n   query.\n\n\n.. code-block:: python-diff\n\n    + import gel\n    + client = gel.create_async_client()\n\n      @router.post(\"/auth/signup\")\n      async def handle_signup(request: Request):\n          body = await request.json()\n          email = body.get(\"email\")\n    +     name = body.get(\"name\")\n          password = body.get(\"password\")\n\n    -     if not email or not password:\n    +     if not email or not password or not name:\n    -         raise HTTPException(status_code=400, detail=\"Missing email or password.\")\n    +         raise HTTPException(status_code=400, detail=\"Missing email, password, or name.\")\n\n          verifier, challenge = generate_pkce()\n          register_url = f\"{GEL_AUTH_BASE_URL}/register\"\n          register_response = httpx.post(register_url, json={\n              \"challenge\": challenge,\n              \"email\": email,\n              \"password\": password,\n              \"provider\": \"builtin::local_emailpassword\",\n              \"verify_url\": \"http://localhost:8000/auth/verify\",\n          })\n\n          if register_response.status_code != 200 and register_response.status_code != 201:\n              return JSONResponse(status_code=400, content={\"message\": \"Registration failed\"})\n\n          code = register_response.json().get(\"code\")\n          token_url = f\"{GEL_AUTH_BASE_URL}/token\"\n          token_response = httpx.get(token_url, params={\"code\": code, \"verifier\": verifier})\n\n          if token_response.status_code != 200:\n              return JSONResponse(status_code=400, content={\"message\": \"Token exchange failed\"})\n\n          auth_token = token_response.json().get(\"auth_token\")\n    +     identity_id = token_response.json().get(\"identity_id\")\n    +     try:\n    +         created_user = await create_user_qry.create_user(client, name=name, identity_id=identity_id)\n    +     except gel.errors.ConstraintViolationError:\n    +         raise HTTPException(\n    +             status_code=400,\n    +             detail={\"error\": f\"User with email '{email}' already exists.\"},\n    +         )\n\n          response = JSONResponse(content={\"message\": \"User registered\"})\n          response.set_cookie(key=\"gel-auth-token\", value=auth_token, httponly=True, secure=True, samesite='strict')\n          return response\n\nYou can now test the sign-up endpoint by sending a POST request to\n``http://localhost:8000/auth/signup`` with the following payload:\n\n.. code-block:: json\n\n    {\n        \"email\": \"jonathan@example.com\",\n        \"name\": \"Jonathan Harker\",\n        \"password\": \"password\"\n    }\n\nIf the request is successful, you should see a response with the message\n``User registered``.\n\n\nWrapping up\n===========\n\nNow you have a fully functioning events API in FastAPI backed by Gel. If you\nwant to see all the source code for the completed project, you'll find it in\n`our examples repo\n<https://github.com/geldata/gel-examples/tree/main/fastapi-crud>`_. We also\nhave a separate example that demonstrates how to integrate Gel Auth with\nFastAPI in the same repo. Check it out\n`here <https://github.com/geldata/gel-examples/tree/main/fastapi-crud-auth>`_.\nIf you're stuck or if you just want to show off what you've built, come talk\nto us `on Discord <https://discord.gg/umUueND6ag>`_. It's a great community of\nhelpful folks, all passionate about being part of the next generation of\ndatabases.\n"
  },
  {
    "path": "docs/resources/guides/tutorials/rest_apis_with_flask.rst",
    "content": ".. _ref_guide_rest_apis_with_flask:\n\n=====\nFlask\n=====\n\n:edb-alt-title: Building a REST API with Gel and Flask\n\nThe Gel Python client makes it easy to integrate Gel into your preferred\nweb development stack. In this tutorial, we'll see how you can quickly start\nbuilding RESTful APIs with `Flask <https://flask.palletsprojects.com>`_ and\n|Gel|.\n\nWe'll build a simple movie organization system where you'll be able to fetch,\ncreate, update, and delete *movies* and *movie actors* via RESTful API\nendpoints.\n\nPrerequisites\n=============\n\nBefore we start, make sure you've :ref:`installed <ref_admin_install>` the\n|gelcmd| command-line tool. Here, we'll use Python 3.10 and a few of its\nlatest features while building the APIs. A working version of this tutorial can\nbe found `on Github\n<https://github.com/geldata/gel-examples/tree/main/flask-crud>`_.\n\n\nInstall the dependencies\n^^^^^^^^^^^^^^^^^^^^^^^^\n\nTo follow along, clone the repository and head over to the ``flask-crud``\ndirectory.\n\n\n.. code-block:: bash\n\n    $ git clone git@github.com:geldata/gel-examples.git\n    $ cd gel-examples/flask-crud\n\nCreate a Python 3.10 virtual environment, activate it, and install the\ndependencies with this command:\n\n.. code-block:: bash\n\n    $ python -m venv myvenv\n    $ source myvenv/bin/activate\n    $ pip install gel flask 'httpx[cli]'\n\n\nInitialize the database\n^^^^^^^^^^^^^^^^^^^^^^^\n\nNow, let's initialize a Gel project. From the project's root directory:\n\n.. code-block:: bash\n\n    $ gel project init\n    Initializing project...\n\n    Specify the name of Gel instance to use with this project\n    [default: flask_crud]:\n    > flask_crud\n\n    Do you want to start instance automatically on login? [y/n]\n    > y\n    Checking Gel versions...\n\nOnce you've answered the prompts, a new Gel instance called ``flask_crud``\nwill be created and started.\n\n\nConnect to the database\n^^^^^^^^^^^^^^^^^^^^^^^\n\nLet's test that we can connect to the newly started instance. To do so, run:\n\n.. code-block:: bash\n\n    $ gel\n\nYou should be connected to the database instance and able to see a prompt\nsimilar to this:\n\n::\n\n    Gel x.x (repl x.x)\n    Type \\help for help, \\quit to quit.\n    gel>\n\nYou can start writing queries here. However, the database is currently\nempty. Let's start designing the data model.\n\nSchema design\n=============\n\nThe movie organization system will have two object types—**movies** and\n**actors**. Each *movie* can have links to multiple *actors*. The goal is to\ncreate API endpoints that'll allow us to fetch, create, update, and delete the\nobjects while maintaining their relationships.\n\n|Gel| allows us to declaratively define the structure of the objects. The\nschema lives inside |.gel| file in the ``dbschema`` directory. It's\ncommon to declare the entire schema in a single file :dotgel:`dbschema/default`.\nThis is how our datatypes look:\n\n.. code-block:: sdl\n\n    # dbschema/default.gel\n\n    module default {\n      abstract type Auditable {\n        property created_at -> datetime {\n          readonly := true;\n          default := datetime_current();\n        }\n      }\n\n      type Actor extending Auditable {\n        required property name -> str {\n          constraint max_len_value(50);\n        }\n        property age -> int16 {\n          constraint min_value(0);\n          constraint max_value(100);\n        }\n        property height -> int16 {\n          constraint min_value(0);\n          constraint max_value(300);\n        }\n      }\n\n      type Movie extending Auditable {\n        required property name -> str {\n          constraint max_len_value(50);\n        }\n        property year -> int16{\n          constraint min_value(1850);\n        };\n        multi link actors -> Actor;\n      }\n    }\n\n\nHere, we've defined an ``abstract`` type called ``Auditable`` to take advantage\nof Gel's schema mixin system. This allows us to add a ``created_at``\nproperty to multiple types without repeating ourselves.\n\nThe ``Actor`` type extends ``Auditable`` and inherits the ``created_at``\nproperty as a result. This property is auto-filled via the ``datetime_current``\nfunction. Along with the inherited type, the actor type also defines a few\nadditional properties like called ``name``, ``age``, and ``height``. The\nconstraints on the properties make sure that actor names can't be longer than\n50 characters, age must be between 0 to 100 years, and finally, height must be\nbetween 0 to 300 centimeters.\n\nWe also define a ``Movie`` type that extends the ``Auditable`` abstract type.\nIt also contains some additional concrete properties and links: ``name``,\n``year``, and an optional multi-link called ``actors`` which refers to the\n``Actor`` objects.\n\nBuild the API endpoints\n=======================\n\nThe API endpoints are defined in the ``app`` directory. The directory structure\nlooks as follows:\n\n::\n\n    app\n    ├── __init__.py\n    ├── actors.py\n    ├── main.py\n    └── movies.py\n\nThe ``actors.py`` and ``movies.py`` modules contain the code to build the\n``Actor`` and ``Movie`` APIs respectively. The ``main.py`` module then\nregisters all the endpoints and exposes them to the webserver.\n\n\nFetch actors\n^^^^^^^^^^^^\n\nSince the ``Actor`` type is simpler, we'll start with that. Let's\ncreate a ``GET /actors`` endpoint so that we can see the ``Actor``\nobjects saved in the database. You can create the API in Flask like this:\n\n.. code-block:: python\n\n    # flask-crud/app/actors.py\n    from __future__ import annotations\n\n    import json\n    from http import HTTPStatus\n\n    import gel\n    from flask import Blueprint, request\n\n    actor = Blueprint(\"actor\", __name__)\n    client = gel.create_client()\n\n\n    @actor.route(\"/actors\", methods=[\"GET\"])\n    def get_actors() -> tuple[dict, int]:\n        filter_name = request.args.get(\"filter_name\")\n\n        if not filter_name:\n            actors = client.query_json(\n                \"\"\"\n                select Actor {\n                    name,\n                    age,\n                    height\n                }\n                \"\"\"\n            )\n        else:\n            actors = client.query_json(\n                \"\"\"\n                select Actor {\n                    name,\n                    age,\n                    height\n                }\n                filter .name = <str>$filter_name\n                \"\"\",\n                filter_name=filter_name,\n            )\n\n        response_payload = {\"result\": json.loads(actors)}\n        return response_payload, HTTPStatus.OK\n\n\nThe ``Blueprint`` instance does the actual work of exposing the API. We also\ncreate a blocking Gel client instance to communicate with the database. By\ndefault, this API will return a list of actors, but you can also filter the\nobjects by name.\n\nIn the ``get_actors`` function, we perform the database query via the\n``gel`` client. Here, the ``client.query_json`` method conveniently returns\n``JSON`` serialized objects. We deserialize the returned data in the\n``response_payload`` dictionary and then return it. Afterward, the final JSON\nserialization part is taken care of by Flask. This endpoint is exposed to the\nserver in the ``main.py`` module. Here's the content of the module:\n\n.. code-block:: python\n\n    # flask-crud/app/main.py\n    from __future__ import annotations\n\n    from flask import Flask\n\n    from app.actors import actor\n    from app.movies import movie\n\n    app = Flask(__name__)\n\n    app.register_blueprint(actor)\n    app.register_blueprint(movie)\n\n\nTo test the endpoint, go to the ``flask-crud`` directory and run:\n\n.. code-block:: bash\n\n    $ export FLASK_APP=app.main:app && flask run --reload\n\nThis will start the development server and make it accessible via port 5000.\nEarlier, we installed the `HTTPx <https://www.python-httpx.org/>`_ client\nlibrary to make HTTP requests programmatically. It also comes with a neat\ncommand-line tool that we'll use to test our API.\n\nWhile the development server is running, on a new console, run:\n\n.. code-block:: bash\n\n    $ httpx -m GET http://localhost:5000/actors\n\nYou'll see the following output on the console:\n\n::\n\n    HTTP/1.1 200 OK\n    Server: Werkzeug/2.1.1 Python/3.10.4\n    Date: Wed, 27 Apr 2022 18:58:38 GMT\n    Content-Type: application/json\n    Content-Length: 2\n\n    {\n      \"result\": []\n    }\n\nOur request yielded an empty list because the database is currently empty.\nLet's create the ``POST /actors`` endpoint to start saving actors in the\ndatabase.\n\nCreate actor\n^^^^^^^^^^^^\n\nThe POST endpoint can be built similarly:\n\n.. code-block:: python\n\n    # flask-crud/app/actors.py\n    ...\n    @actor.route(\"/actors\", methods=[\"POST\"])\n    def post_actor() -> tuple[dict, int]:\n        incoming_payload = request.json\n\n        # Data validation.\n        if not incoming_payload:\n            return {\n                \"error\": \"Bad request\"\n            }, HTTPStatus.BAD_REQUEST\n\n        if not (name := incoming_payload.get(\"name\")):\n            return {\n                \"error\": \"Field 'name' is required.\"\n            }, HTTPStatus.BAD_REQUEST\n\n        if len(name) > 50:\n            return {\n                \"error\": \"Field 'name' cannot be longer than 50 \"\n                         \"characters.\"\n            }, HTTPStatus.BAD_REQUEST\n\n        if age := incoming_payload.get(\"age\"):\n            if 0 <= age <= 100:\n                return {\n                    \"error\": \"Field 'age' must be between 0 \"\n                    \"and 100.\"\n                }, HTTPStatus.BAD_REQUEST\n\n        if height := incoming_payload.get(\"height\"):\n            if not 0 <= height <= 300:\n                return {\n                    \"error\": \"Field 'height' must between 0 and \"\n                             \"300 cm.\"\n                }, HTTPStatus.BAD_REQUEST\n\n        # Create object.\n        actor = client.query_single_json(\n            \"\"\"\n            with\n                name := <str>$name,\n                age := <optional int16>$age,\n                height := <optional int16>$height\n            select (\n                insert Actor {\n                    name := name,\n                    age := age,\n                    height := height\n                }\n            ){ name, age, height };\n            \"\"\",\n            name=name,\n            age=age,\n            height=height,\n        )\n        response_payload = {\"result\": json.loads(actor)}\n        return response_payload, HTTPStatus.CREATED\n\n\nIn the above snippet, we perform data validation in the conditional blocks and\nthen make the query to create the object in the database. For now, we'll only\nallow creating a single object per request. The ``client.query_single_json``\nensures that we're creating and returning only one object. Inside the query\nstring, notice, how we're using ``<optional type>`` to deal with the optional\nfields. If the user doesn't provide the value of an optional field like ``age``\nor ``height``, it'll be defaulted to ``null``.\n\nTo test it out, make a request as follows:\n\n.. code-block:: bash\n\n    $ httpx -m POST http://localhost:5000/actors \\\n            -j '{\"name\" : \"Robert Downey Jr.\"}'\n\nThe output should look similar to this:\n\n::\n\n    HTTP/1.1 201 CREATED\n    ...\n\n    {\n      \"result\": {\n        \"age\": null,\n        \"height\": null,\n        \"name\": \"Robert Downey Jr.\"\n      }\n    }\n\n\nBefore we move on to the next step, create 2 more actors called ``Chris Evans``\nand ``Natalie Portman``. Now that we have some data in the database, let's\nmake a ``GET`` request to see the objects:\n\n.. code-block:: bash\n\n    $ httpx -m GET http://localhost:5000/actors\n\nThe response looks as follows:\n\n::\n\n    HTTP/1.1 200 OK\n    ...\n\n    {\n      \"result\": [\n        {\n          \"age\": null,\n          \"height\": null,\n          \"name\": \"Robert Downey Jr.\"\n        },\n        {\n          \"age\": null,\n          \"height\": null,\n          \"name\": \"Chris Evans\"\n        },\n        {\n          \"age\": null,\n          \"height\": null,\n          \"name\": \"Natalie Portman\"\n        }\n      ]\n    }\n\nYou can filter the output of the ``GET /actors`` by ``name``. To do so, use the\n``filter_name`` query parameter like this:\n\n.. code-block:: bash\n\n    $ httpx -m GET http://localhost:5000/actors \\\n            -p filter_name \"Robert Downey Jr.\"\n\nDoing this will only display the data of a single object:\n\n::\n\n    HTTP/1.1 200 OK\n\n    {\n      \"result\": [\n        {\n          \"age\": null,\n          \"height\": null,\n          \"name\": \"Robert Downey Jr.\"\n        }\n      ]\n    }\n\nOnce you've done that, we can move on to the next step of building the\n``PUT /actors`` endpoint to update the actor data.\n\n\nUpdate actor\n^^^^^^^^^^^^\n\nIt can be built like this:\n\n\n.. code-block:: python\n\n    # flask-crud/app/actors.py\n\n    # ...\n\n    @actor.route(\"/actors\", methods=[\"PUT\"])\n    def put_actors() -> tuple[dict, int]:\n        incoming_payload = request.json\n        filter_name = request.args.get(\"filter_name\")\n\n        # Data validation.\n        if not incoming_payload:\n            return {\n                \"error\": \"Bad request\"\n            }, HTTPStatus.BAD_REQUEST\n\n        if not filter_name:\n            return {\n                \"error\": \"Query parameter 'filter_name' must \"\n                \"be provided\",\n            }, HTTPStatus.BAD_REQUEST\n\n        if (name:=incoming_payload.get(\"name\")) and len(name) > 50:\n            return {\n                \"error\": \"Field 'name' cannot be longer than \"\n                \"50 characters.\"\n            }, HTTPStatus.BAD_REQUEST\n\n        if age := incoming_payload.get(\"age\"):\n            if age <= 0:\n                return {\n                    \"error\": \"Field 'age' cannot be less than \"\n                    \"or equal to 0.\"\n                }, HTTPStatus.BAD_REQUEST\n\n        if height := incoming_payload.get(\"height\"):\n            if not 0 <= height <= 300:\n                return {\n                    \"error\": \"Field 'height' must between 0 \"\n                    \"and 300 cm.\"\n                }, HTTPStatus.BAD_REQUEST\n\n        # Update object.\n        actors = client.query_json(\n            \"\"\"\n            with\n                filter_name := <str>$filter_name,\n                name := <optional str>$name,\n                age := <optional int16>$age,\n                height := <optional int16>$height\n            select (\n                update Actor\n                filter .name = filter_name\n                set {\n                    name := name ?? .name,\n                    age := age ?? .age,\n                    height := height ?? .height\n                }\n            ){ name, age, height };\"\"\",\n            filter_name=filter_name,\n            name=name,\n            age=age,\n            height=height,\n        )\n        response_payload = {\"result\": json.loads(actors)}\n        return response_payload, HTTPStatus.OK\n\nHere, we'll isolate the intended object that we want to update by filtering the\nactors with the ``filter_name`` parameter. For example, if you wanted to update\nthe properties of ``Robert Downey Jr.``, the value of the ``filter_name``\nquery parameter would be ``Robert Downey Jr.``. The coalesce operator ``??``\nin the query string makes sure that the API user can selectively update the\nproperties of the target object and the other properties keep their existing\nvalues.\n\nThe following command updates the ``age`` and ``height`` of\n``Robert Downey Jr.``.\n\n.. code-block:: bash\n\n    $ httpx -m PUT http://localhost:5000/actors \\\n            -p filter_name \"Robert Downey Jr.\" \\\n            -j '{\"age\": 57, \"height\": 173}'\n\nThis will return:\n\n::\n\n    HTTP/1.1 200 OK\n    ...\n    {\n      \"result\": [\n        {\n          \"age\": 57,\n          \"height\": 173,\n          \"name\": \"Robert Downey Jr.\"\n        }\n      ]\n    }\n\n\nDelete actor\n^^^^^^^^^^^^\n\nAnother API that we'll need to cover is the ``DELETE /actors`` endpoint. It'll\nallow us to query the name of the targeted object and delete that. The code\nlooks similar to the ones you've already seen:\n\n.. code-block:: python\n\n    # flask-crud/app/actors.py\n    ...\n\n    @actor.route(\"/actors\", methods=[\"DELETE\"])\n    def delete_actors() -> tuple[dict, int]:\n        if not (filter_name := request.args.get(\"filter_name\")):\n            return {\n                \"error\": \"Query parameter 'filter_name' must \"\n                \"be provided\",\n            }, HTTPStatus.BAD_REQUEST\n\n        try:\n            actors = client.query_json(\n                \"\"\"select (\n                    delete Actor\n                    filter .name = <str>$filter_name\n                ) {name}\n                \"\"\",\n                filter_name=filter_name,\n            )\n        except gel.errors.ConstraintViolationError:\n            return (\n                {\n                    \"error\": f\"Cannot delete '{filter_name}. \"\n                    \"Actor is associated with at least one movie.\"\n                },\n                HTTPStatus.BAD_REQUEST,\n            )\n\n        response_payload = {\"result\": json.loads(actors)}\n        return response_payload, HTTPStatus.OK\n\n\nThis endpoint will simply delete the requested actor if the actor isn't\nattached to any movie. If the targeted object is attached to a movie, then API\nwill throw an HTTP 400 (bad request) error and refuse to delete the object. To\ndelete ``Natalie Portman``, on your console, run:\n\n.. code-block:: bash\n\n    $ httpx -m DELETE http://localhost:5000/actors \\\n            -p filter_name \"Natalie Portman\"\n\nThat'll return:\n\n::\n\n    HTTP/1.1 200 OK\n    ...\n\n    {\n      \"result\": [\n        {\n          \"name\": \"Natalie Portman\"\n        }\n      ]\n    }\n\n\nNow let's move on to building the ``Movie`` API.\n\nCreate movie\n^^^^^^^^^^^^\n\nHere's how we'll implement the ``POST /movie`` endpoint:\n\n.. code-block:: python\n\n    # flask-crud/app/movies.py\n    from __future__ import annotations\n\n    import json\n    from http import HTTPStatus\n\n    import gel\n    from flask import Blueprint, request\n\n    movie = Blueprint(\"movie\", __name__)\n    client = gel.create_client()\n\n    @movie.route(\"/movies\", methods=[\"POST\"])\n    def post_movie() -> tuple[dict, int]:\n        incoming_payload = request.json\n\n        # Data validation.\n        if not incoming_payload:\n            return {\n                \"error\": \"Bad request\"\n            }, HTTPStatus.BAD_REQUEST\n\n        if not (name := incoming_payload.get(\"name\")):\n            return {\n                \"error\": \"Field 'name' is required.\"\n            }, HTTPStatus.BAD_REQUEST\n\n        if len(name) > 50:\n            return {\n                \"error\": \"Field 'name' cannot be longer than \"\n                \"50 characters.\"\n            }, HTTPStatus.BAD_REQUEST\n\n        if year := incoming_payload.get(\"year\"):\n            if year < 1850:\n                return {\n                    \"error\": \"Field 'year' cannot be less \"\n                    \"than 1850.\"\n                }, HTTPStatus.BAD_REQUEST\n\n        actor_names = incoming_payload.get(\"actor_names\")\n\n        # Create object.\n        movie = client.query_single_json(\n            \"\"\"\n            with\n                name := <str>$name,\n                year := <optional int16>$year,\n                actor_names := <optional array<str>>$actor_names\n            select (\n                insert Movie {\n                    name := name,\n                    year := year,\n                    actors := (\n                        select Actor\n                        filter .name in array_unpack(actor_names)\n                    )\n                }\n            ){ name, year, actors: {name, age, height} };\n            \"\"\",\n            name=name,\n            year=year,\n            actor_names=actor_names,\n        )\n        response_payload = {\"result\": json.loads(movie)}\n        return response_payload, HTTPStatus.CREATED\n\nLike the ``POST /actors`` API, conditional blocks validate the shape of the\nincoming data and the ``client.query_json`` method creates the object in the\ndatabase. EdgeQL allows us to perform insertion and selection of data fields\nat the same time in a single query. One thing that's different here is that the\n``POST /movies`` API also accepts an optional field called ``actor_names``\nwhere the user can provide an array of actor names. The backend will associate\nthe actors with the movie object if those actors exist in the database.\n\nHere's how you'd create a movie:\n\n\n.. lint-off\n\n.. code-block:: bash\n\n    $ httpx -m POST http://localhost:5000/movies \\\n            -j '{ \"name\": \"The Avengers\", \"year\": 2012, \"actor_names\": [ \"Robert Downey Jr.\", \"Chris Evans\" ] }'\n\n.. lint-on\n\nThat'll return:\n\n::\n\n    HTTP/1.1 201 CREATED\n    ...\n    {\n      \"result\": {\n        \"actors\": [\n          {\n            \"age\": null,\n            \"height\": null,\n            \"name\": \"Chris Evans\"\n          },\n          {\n            \"age\": 57,\n            \"height\": 173,\n            \"name\": \"Robert Downey Jr.\"\n          }\n        ],\n        \"name\": \"The Avengers\",\n        \"year\": 2012\n      }\n    }\n\nAdditional movie endpoints\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nThe implementation of the ``GET /movie``, ``PATCH /movie`` and\n``DELETE /movie`` endpoints are provided in the sample codebase in\n``app/movies.py``. But try to write them on your own using the Actor endpoints\nas a starting point! Once you're done, you should be able to fetch a movie by\nits title from your database with the ``filter_name`` parameter and\nthe GET API as follows:\n\n.. code-block:: bash\n\n    $ httpx -m GET http://localhost:5000/movies \\\n            -p 'filter_name' 'The Avengers'\n\nThat'll return:\n\n::\n\n    HTTP/1.1 200 OK\n    ...\n    {\n      \"result\": [\n        {\n          \"actors\": [\n            {\n              \"age\": null,\n              \"name\": \"Chris Evans\"\n            },\n            {\n              \"age\": 57,\n              \"name\": \"Robert Downey Jr.\"\n            }\n          ],\n          \"name\": \"The Avengers\",\n          \"year\": 2012\n        }\n      ]\n    }\n\n\n\nConclusion\n==========\n\nWhile building REST APIs, the Gel client allows you to leverage Gel with\nany microframework of your choice. Whether it's\n`FastAPI <https://fastapi.tiangolo.com>`_,\n`Flask <https://flask.palletsprojects.com>`_,\n`AIOHTTP <https://docs.aiohttp.org/en/stable>`_,\n`Starlette <https://www.starlette.io>`_,\nor `Tornado <https://www.tornadoweb.org/en/stable>`_,\nthe core workflow is quite similar to the one demonstrated above; you'll query\nand serialize data with the client and then return the payload for your\nframework to process.\n"
  },
  {
    "path": "docs/resources/guides/tutorials/trpc.rst",
    "content": ".. _ref_guide_trpc:\n\n====\ntRPC\n====\n\n:edb-alt-title: Integrating Gel with tRPC\n\nThis guide explains how to integrate **Gel** with **tRPC** for a modern,\ntype-safe API. We'll cover setting up database interactions, API routing,\nand implementing authentication, all while ensuring type safety across the\nclient and server.\n\nYou can reference the following repositories for more context:\n\n- `create-t3-turbo-gel <https://github.com/geldata/create-t3-turbo-gel>`_ -\n  A monorepo template using the `T3 stack <https://init.tips/>`_,\n  `Turborepo <https://turbo.build/>`_, and Gel.\n- `LookFeel Project <https://github.com/LewTrn/lookfeel>`_ - A real-world\n  example using **Gel** and **tRPC**.\n\nStep 1: Gel setup\n=================\n\n|Gel| will serve as the database layer for your application.\n\nInstall and initialize Gel\n--------------------------\n\nTo initialize **Gel**, run the following command using your preferred\npackage manager:\n\n.. code-block:: bash\n\n   $ pnpm dlx gel project init # or `npx gel project init`\n\nThis will create a Gel project and set up a schema to start with.\n\nDefine the Gel Schema\n---------------------\n\nThe previous command generated a schema file in the ``dbschema`` directory.\n\nHere's an example schema that defines a ``User`` model:\n\n.. code-block:: sdl\n   :caption: dbschema/default.gel\n\n   module default {\n     type User {\n       required name: str;\n       required email: str;\n     }\n   }\n\nApply schema migrations\n-----------------------\n\nOnce schema changes are made, apply migrations with:\n\n.. code-block:: bash\n\n   $ pnpm dlx gel migration create # or npx gel migration create\n   $ pnpm dlx gel migration apply # or npx gel migration apply\n\nStep 2: Configure Gel Client\n============================\n\nTo interact with **Gel** from your application, you need to configure the\nclient.\n\nInstall Gel Client\n------------------\n\nFirst, install the **Gel** client using your package manager:\n\n.. code-block:: bash\n\n   $ pnpm add gel\n   $ # or yarn add gel\n   $ # or npm install gel\n   $ # or bun add gel\n\nThen, create a client instance in a ``gel.ts`` file:\n\n.. code-block:: typescript\n   :caption: src/gel.ts\n\n   import { createClient } from 'gel';\n\n   const gelClient = createClient();\n   export default gelClient;\n\nThis client will be used to interact with the database and execute queries.\n\nStep 3: tRPC setup\n==================\n\n**tRPC** enables type-safe communication between the frontend and\nbackend.\n\nInstall tRPC dependencies\n-------------------------\n\nInstall the required tRPC dependencies:\n\n.. code-block:: bash\n\n   $ pnpm add @trpc/server @trpc/client\n   $ # or yarn add @trpc/server @trpc/client\n   $ # or npm install @trpc/server @trpc/client\n   $ # or bun add @trpc/server @trpc/client\n\nIf you're using React and would like to use React Query with tRPC, also\ninstall a wrapper around the `@tanstack/react-query <https://tanstack.com/query/latest>`_.\n\n.. code-block:: bash\n\n   $ pnpm add @trpc/react-query\n   $ # or yarn add @trpc/react-query\n   $ # or npm install @trpc/react-query\n   $ # or bun add @trpc/react-query\n\nDefine the tRPC Router\n-----------------------\n\nHere's how to define a simple tRPC query that interacts with **Gel**:\n\n.. code-block:: typescript\n   :caption: server/routers/_app.ts\n\n   import { initTRPC } from '@trpc/server';\n   import gelClient from './gel';\n\n   const t = initTRPC.create();\n\n   export const appRouter = t.router({\n     getUsers: t.procedure.query(async () => {\n       const users = await gelClient.query('SELECT User { name, email }');\n       return users;\n     }),\n   });\n\n   export type AppRouter = typeof appRouter;\n\nThis example defines a query that fetches user data from Gel, ensuring\ntype safety in both the query and response.\n\nStep 4: Use tRPC Client\n========================\n\nNow that the server is set up, you can use the tRPC client to interact with\nthe API from the frontend. We will demonstrate how to integrate tRPC with\n**Next.js** and **Express**.\n\nWith Next.js\n------------\n\nIf you're working with **Next.js**, here's how to integrate **tRPC**:\n\nCreate a tRPC API Handler\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\nInside ``api/trpc/[trpc].ts``, create the following handler to connect\n**tRPC** with Next.js:\n\n.. code-block:: typescript\n   :caption: pages/api/trpc/[trpc].ts\n\n   import { createNextApiHandler } from '@trpc/server/adapters/next';\n   import { appRouter } from '../../../server/routers/_app';\n\n   export default createNextApiHandler({\n     router: appRouter,\n   });\n\nCreate a tRPC Client\n~~~~~~~~~~~~~~~~~~~~\n\nNext, create a **tRPC** client to interact with the API:\n\n.. code-block:: typescript\n   :caption: utils/trpc.ts\n\n   import { createTRPCReact } from \"@trpc/react-query\";\n   import { AppRouter } from './routers/_app';\n\n   export const api = createTRPCReact<AppRouter>();\n\nClient-Side Usage in Next.js\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nYou can then use **tRPC** hooks to query the API from the client:\n\n.. code-block:: typescript\n   :caption: components/UsersComponent.tsx\n\n   import { trpc } from '../utils/trpc';\n\n   const UsersComponent = () => {\n     const { data, isLoading } = trpc.getUsers.useQuery();\n\n     if (isLoading) return <div>Loading...</div>;\n\n     return (\n       <div>\n         {data?.map(user => (\n           <p key={user.email}>{user.name}</p>\n         ))}\n       </div>\n     );\n   };\n\n   export default UsersComponent;\n\nAlternative Path: Use tRPC with Express\n---------------------------------------\n\nIf you're not using **Next.js**, here's how you can integrate **tRPC** with\n**Express**.\n\nSet up Express server with tRPC\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nHere's how you can create an Express server and integrate **tRPC**:\n\n.. code-block:: typescript\n\n   import express from 'express';\n   import { appRouter } from './routers/_app';\n   import * as trpcExpress from '@trpc/server/adapters/express';\n\n   const app = express();\n\n   app.use(\n     '/trpc',\n     trpcExpress.createExpressMiddleware({\n       router: appRouter,\n     })\n   );\n\n   app.listen(4000, () => {\n     console.log('Server is running on port 4000');\n   });\n\nClient-side usage\n-----------------\n\nIn non-Next.js apps, use the tRPC client to interact with the server:\n\n.. code-block:: typescript\n\n   import { createTRPCClient, httpBatchLink } from '@trpc/client';\n   import { AppRouter } from './routers/_app';\n\n   const trpc = createTRPCClient<AppRouter>({\n     links: [\n       httpBatchLink({\n         url: 'http://localhost:4000/trpc',\n       }),\n     ],\n   });\n\n   async function fetchUsers() {\n     const users = await trpc.getUsers.query();\n     console.log(users);\n   }\n\nStep 5: Set up authentication with Gel Auth\n===========================================\n\nIn this section, we will cover how to integrate **Gel Auth** with **tRPC**\nand context in both **Next.js** and **Express** environments. This will ensure\nthat user authentication is handled securely and that both server-side and\nclient-side tRPC calls can access the user's session.\n\nGel Auth with tRPC and tRPC context in Next.js\n----------------------------------------------\n\nIn **Next.js**, integrating **Gel Auth** with **tRPC** involves creating a\ncontext that provides the user session and Gel client to the tRPC API.\n\n1. **Initialize Gel Client and Auth**\n\n   First, initialize the **Gel** client and **Gel Auth**:\n\n   .. code-block:: typescript\n\n      import { createClient } from \"gel\";\n      import createAuth from \"@gel/auth-nextjs/app\";\n\n      // Initialize Gel client\n      export const gelClient = createClient();\n\n      // Initialize Gel Auth\n      export const auth = createAuth(gelClient, {\n        baseUrl: process.env.VERCEL_ENV === \"production\"\n          ? \"https://production.yourapp.com\"\n          : \"http://localhost:3000\",\n      });\n\n2. **Create tRPC Context**\n\n   The **tRPC** context provides the Gel Auth session to the tRPC\n   procedures:\n\n   .. code-block:: typescript\n      :caption: src/trpc.ts\n\n      import { initTRPC } from '@trpc/server';\n      import { headers } from \"next/headers\";\n      import { auth } from \"src/gel.ts\";\n\n      // Create tRPC context with session and Gel client\n      export const createTRPCContext = async () => {\n        const session = await auth.getSession(); // Retrieve session from Gel Auth\n\n        return {\n          session, // Pass the session to the context\n        };\n      };\n\n      // Initialize tRPC with context\n      const t = initTRPC.context<typeof createTRPCContext>().create({});\n\n3. **Use tRPC Context in API Handler**\n\n   In **Next.js**, set up an API handler to connect your **tRPC router** with\n   the context:\n\n   .. code-block:: typescript\n      :caption: pages/api/trpc/[trpc].ts\n\n      import { createNextApiHandler } from '@trpc/server/adapters/next';\n      import { createTRPCContext } from 'src/trpc.ts';\n      import { appRouter } from 'src/routers/_app';\n\n      export default createNextApiHandler({\n        router: appRouter, // Your tRPC router\n        createContext: createTRPCContext,\n      });\n\n4. **Example tRPC Procedure**\n\n   You can now write procedures in your tRPC router, making use of the\n   **Gel Auth** session and the **Gel** client:\n\n   .. code-block:: typescript\n\n      export const appRouter = t.router({\n        getUserData: t.procedure.query(async ({ ctx }) => {\n          if (!(await ctx.session.isSignedIn())) {\n            throw new Error(\"Not authenticated\");\n          }\n          // Fetch data from Gel using the authenticated client\n          const userData = await ctx.session.client.query(`\n            select User { name, email }\n          `);\n\n          return userData;\n        }),\n      });\n\nGel Auth with tRPC and Context in Express\n-----------------------------------------\n\nIn **Express**, the process involves setting up middleware to manage the\nauthentication and context for tRPC procedures.\n\n1. **Initialize Gel Client and Auth for Express**\n\n   Just like in **Next.js**, you first initialize the **Gel** client and\n   **Gel Auth**:\n\n   .. code-block:: typescript\n\n      import { createClient } from \"gel\";\n      import createExpressAuth from \"@gel/auth-express\";\n\n      // Initialize Gel client\n      const gelClient = createClient();\n\n      // Initialize Gel Auth for Express\n      export const auth = createExpressAuth(gelClient, {\n        baseUrl: `http://localhost:${process.env.PORT || 3000}`,\n      });\n\n2. **Create tRPC Context Middleware for Express**\n\n   In **Express**, create middleware to pass the authenticated session and\n   Gel client to the tRPC context:\n\n   .. code-block:: typescript\n\n      import { type AuthRequest, type Response, type NextFunction } from \"express\";\n\n      // Middleware to set up tRPC context in Express\n      export const createTRPCContextMiddleware = async (\n        req: AuthRequest,\n        res: Response,\n        next: NextFunction\n      ) => {\n        const session = req.auth?.session(); // Get authenticated session\n        req.context = {\n          session, // Add session to context\n          gelClient, // Add Gel client to context\n        };\n        next();\n      };\n\n3. **Set up tRPC Router in Express**\n\n   Use the **tRPC router** in **Express** by including the context middleware\n   and **Gel Auth** middleware:\n\n   .. code-block:: typescript\n\n      import express from \"express\";\n      import { appRouter } from \"./path-to-router\";\n      import { auth } from \"./path-to-auth\";\n      import { createTRPCContextMiddleware } from \"./path-to-context\";\n      import { createExpressMiddleware } from \"@trpc/server/adapters/express\";\n\n      const app = express();\n\n      // Gel Auth middleware to handle sessions\n      app.use(auth.middleware);\n\n      // Custom middleware to pass tRPC context\n      app.use(createTRPCContextMiddleware);\n\n      // tRPC route setup\n      app.use(\n        \"/trpc\",\n        createExpressMiddleware({\n          router: appRouter,\n          createContext: (req) => req.context, // Use context from middleware\n        })\n      );\n\n      app.listen(4000, () => {\n        console.log('Server running on port 4000');\n      });\n\n4. **Example tRPC Procedure in Express**\n\n   Once the context is set, you can define tRPC procedures that use both the\n   session and Gel client:\n\n   .. code-block:: typescript\n\n      export const appRouter = t.router({\n        getUserData: t.procedure.query(async ({ ctx }) => {\n          if (!(await ctx.session.isSignedIn())) {\n            throw new Error(\"Not authenticated\");\n          }\n          // Fetch data from Gel using the authenticated client\n          const userData = await ctx.session.client.query(`\n            select User { name, email }\n          `);\n\n          return userData;\n        }),\n      });\n\nConclusion\n----------\n\nBy integrating **Gel Auth** into the tRPC context, you ensure that\nauthenticated sessions are securely passed to API procedures, enabling\nuser authentication and protecting routes.\n\nYou can also reference these projects for further examples:\n\n- `create-t3-turbo-gel <https://github.com/geldata/create-t3-turbo-gel>`_ -\n  A monorepo template using the `T3 stack <https://init.tips/>`_,\n  `Turborepo <https://turbo.build/>`_, and Gel.\n- `LookFeel Project <https://github.com/LewTrn/lookfeel>`_ - A real-world\n  example using **Gel** and **tRPC**.\n"
  },
  {
    "path": "docs/resources/index.rst",
    "content": "=========\nResources\n=========\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    upgrading\n    guides/index\n    protocol/index\n    cheatsheets/index\n    changelog/index\n"
  },
  {
    "path": "docs/resources/protocol/dataformats.rst",
    "content": ".. _ref_proto_dataformats:\n\n=================\nData wire formats\n=================\n\nThis section describes the data wire format of standard Gel types.\n\n\n.. _ref_protocol_fmt_array:\n\nSets and array<>\n================\n\nSet and array values are represented as the following structure:\n\n.. code-block:: c\n\n    struct SetOrArrayValue {\n        // Number of dimensions, currently must\n        // always be 0 or 1. 0 indicates an empty set or array.\n        int32       ndims;\n\n        // Reserved.\n        int32       reserved0;\n\n        // Reserved.\n        int32       reserved1;\n\n        // Dimension data.\n        Dimension   dimensions[ndims];\n\n        // Element data, the number of elements\n        // in this array is the sum of dimension sizes:\n        // sum((d.upper - d.lower + 1) for d in dimensions)\n        Element     elements[];\n    };\n\n    struct Dimension {\n        // Upper dimension bound, inclusive,\n        // number of elements in the dimension\n        // relative to the lower bound.\n        int32       upper;\n\n        // Lower dimension bound, always 1.\n        int32       lower;\n    };\n\n    struct Element {\n        // Encoded element data length in bytes.\n        int32       length;\n\n        // Element data.\n        uint8       data[length];\n    };\n\n\nNote: zero-length arrays (and sets) are represented as a 12-byte value where\n``dims`` equal to zero regardless of the shape in type descriptor.\n\n\nSets of arrays are a special case. Every array within a set is wrapped in an\nEnvelope. The full structure follows:\n\n.. code-block:: c\n\n    struct SetOfArrayValue {\n        // Number of dimensions, currently must\n        // always be 0 or 1. 0 indicates an empty set.\n        int32 ndims;\n\n        // Reserved.\n        int32 reserved0;\n\n        // Reserved.\n        int32 reserved1;\n\n        // Dimension data. Same layout as above.\n        Dimension dimensions[ndims];\n\n        // Envelope data, the number of elements\n        // in this array is the sum of dimension sizes:\n        // sum((d.upper - d.lower + 1) for d in dimensions)\n        Envelope elements[];\n    };\n\n    struct Envelope {\n        // Encoded envelope element length in bytes.\n        int32 length;\n\n        // Number of elements, currently must\n        // always be 1.\n        int32 nelems;\n\n        // Reserved.\n        int32 reserved\n\n        // Element data. Same layout as above.\n        Element element[nelems];\n    };\n\n\n.. _ref_protocol_fmt_tuple:\n\ntuple<>,  namedtuple<>, and object<>\n====================================\n\nTuple, namedtuple and object values are represented as the\nfollowing structure:\n\n.. code-block:: c\n\n    struct TupleOrNamedTupleOrObjectValue {\n        // Number of elements\n        int32       nelems;\n\n        // Element data.\n        Element     elements[nelems];\n    };\n\n    struct Element {\n        // Reserved.\n        int32       reserved;\n\n        // Encoded element data length in bytes.\n        int32       length;\n\n        // Element data.\n        uint8       data[length];\n    };\n\n\nNote that for objects, ``Element.length`` can be set to ``-1``, which\nmeans an empty set.\n\n\n.. _ref_protocol_fmt_sparse_obj:\n\nSparse Objects\n==============\n\nSparse object values are represented as the following structure:\n\n.. code-block:: c\n\n    struct SparseObjectValue {\n        // Number of elements\n        int32       nelems;\n\n        // Element data.\n        Element     elements[nelems];\n    };\n\n    struct Element {\n        // Index of the element in the input shape.\n        int32       index;\n\n        // Encoded element data length in bytes.\n        int32       length;\n\n        // Element data.\n        uint8       data[length];\n    };\n\n\n.. _ref_protocol_fmt_range:\n\nRanges\n======\n\nRange values are represented as the following structure:\n\n.. code-block:: c\n\n    struct Range {\n        // A bit mask of range definition.\n        uint8<RangeFlag> flags;\n\n        // Lower boundary data.\n        Boundary         lower;\n\n        // Upper boundary data.\n        Boundary         upper;\n    };\n\n    struct Boundary {\n        // Encoded boundary data length in bytes.\n        int32       length;\n\n        // Boundary data.\n        uint8       data[length];\n    };\n\n    enum RangeFlag {\n        // Empty range.\n        EMPTY   = 0x0001;\n\n        // Included lower boundary.\n        LB_INC  = 0x0002;\n\n        // Included upper boundary.\n        UB_INC  = 0x0004;\n\n        // Inifinity (excluded) lower boundary.\n        LB_INF  = 0x0008;\n\n        // Infinity (excluded) upper boundary.\n        UB_INF  = 0x0010;\n    };\n\n\n.. _ref_protocol_fmt_uuid:\n\nstd::uuid\n=========\n\n:eql:type:`std::uuid` values are represented as a sequence of 16 unsigned\nbyte values.\n\nFor example, the UUID value ``b9545c35-1fe7-485f-a6ea-f8ead251abd3`` is\nrepresented as:\n\n.. code-block:: c\n\n    0xb9 0x54 0x5c 0x35 0x1f 0xe7 0x48 0x5f\n    0xa6 0xea 0xf8 0xea 0xd2 0x51 0xab 0xd3\n\n\n.. _ref_protocol_fmt_str:\n\nstd::str\n========\n\n:eql:type:`std::str` values are represented as a UTF-8 encoded byte string.\nFor example, the ``str`` value ``'Hello! 🙂'`` is encoded as:\n\n.. code-block:: c\n\n    0x48 0x65 0x6c 0x6c 0x6f 0x21 0x20 0xf0 0x9f 0x99 0x82\n\n\n.. _ref_protocol_fmt_bytes:\n\nstd::bytes\n==========\n\n:eql:type:`std::bytes` values are represented as is.\n\n\n.. _ref_protocol_fmt_int16:\n\nstd::int16\n==========\n\n:eql:type:`std::int16` values are represented as two bytes, most\nsignificant byte first.\n\nFor example, the ``int16`` value ``6556`` is represented as:\n\n.. code-block:: c\n\n    0x19 0x9c\n\n\n.. _ref_protocol_fmt_int32:\n\nstd::int32\n==========\n\n:eql:type:`std::int32` values are represented as four bytes, most\nsignificant byte first.\n\nFor example, the ``int32`` value ``655665`` is represented as:\n\n.. code-block:: c\n\n    0x00 0x0a 0x01 0x31\n\n\n.. _ref_protocol_fmt_int64:\n\nstd::int64\n==========\n\n:eql:type:`std::int64` values are represented as eight bytes, most\nsignificant byte first.\n\nFor example, the ``int64`` value ``123456789987654321`` is represented as:\n\n.. code-block:: c\n\n    0x01 0xb6 0x9b 0x4b 0xe0 0x52 0xfa 0xb1\n\n\n.. _ref_protocol_fmt_float32:\n\nstd::float32\n============\n\n:eql:type:`std::float32` values are represented as an IEEE 754-2008 binary\n32-bit value, most significant byte first.\n\nFor example, the ``float32`` value ``-15.625`` is represented as:\n\n.. code-block:: c\n\n    0xc1 0x7a 0x00 0x00\n\n\n.. _ref_protocol_fmt_float64:\n\nstd::float64\n============\n\n:eql:type:`std::float64` values are represented as an IEEE 754-2008 binary\n64-bit value, most significant byte first.\n\nFor example, the ``float64`` value ``-15.625`` is represented as:\n\n.. code-block:: c\n\n    0xc0 0x2f 0x40 0x00 0x00 0x00 0x00 0x00\n\n\n.. _ref_protocol_fmt_decimal:\n\nstd::decimal\n============\n\n:eql:type:`std::decimal` values are represented as the following structure:\n\n.. code-block:: c\n\n    struct Decimal {\n        // Number of digits in digits[], can be 0.\n        uint16               ndigits;\n\n        // Weight of first digit.\n        int16                weight;\n\n        // Sign of the value\n        uint16<DecimalSign>  sign;\n\n        // Value display scale.\n        uint16               dscale;\n\n        // base-10000 digits.\n        uint16                digits[ndigits];\n    };\n\n    enum DecimalSign {\n        // Positive value.\n        POS     = 0x0000;\n\n        // Negative value.\n        NEG     = 0x4000;\n    };\n\nDecimal values are represented as a sequence of base-10000 *digits*.  The\nfirst digit is assumed to be multiplied by *weight* * 10000, i.e. there might\nbe up to weight + 1 digits before the decimal point. Trailing zeros may be\nabsent. It is possible to have negative weight.\n\n*dscale*, or display scale, is the nominal precision expressed as number of\nbase-10 digits after the decimal point. It is always non-negative. *dscale*\nmay be more than the number of physically present fractional digits, implying\nsignificant trailing zeroes. The actual number of digits physically present\nin the *digits* array contains trailing zeros to the next 4-byte increment\n(meaning that integer and fractional part are always distinct base-10000\ndigits).\n\nFor example, the decimal value ``-15000.6250000`` is represented as:\n\n.. code-block:: c\n\n    // ndigits\n    0x00 0x04\n\n    // weight\n    0x00 0x01\n\n    // sign\n    0x40 0x00\n\n    // dscale\n    0x00 0x07\n\n    // digits\n    0x00 0x01 0x13 0x88 0x18 0x6a 0x00 0x00\n\n\n.. _ref_protocol_fmt_bool:\n\nstd::bool\n=========\n\n:eql:type:`std::bool` values are represented as an int8 with\nonly two valid values: ``0x01`` for ``true`` and ``0x00`` for ``false``.\n\n\n.. _ref_protocol_fmt_datetime:\n\nstd::datetime\n=============\n\n:eql:type:`std::datetime` values are represented as a 64-bit integer,\nmost sigificant byte first. The value is the number of *microseconds*\nbetween the encoded datetime and January 1st 2000, 00:00 UTC. A Unix\ntimestamp can be converted into a Gel ``datetime`` value using this\nformula:\n\n.. code-block:: c\n\n    edb_datetime = (unix_ts + 946684800) * 1000000\n\nFor example, the ``datetime`` value ``'2019-05-06T12:00+00:00'`` is\nencoded as:\n\n.. code-block:: c\n\n    0x00 0x02 0x2b 0x35 0x9b 0xc4 0x10 0x00\n\nSee the :ref:`client libraries <ref_bindings_datetime>` section for more info\nabout how to handle different precision when encoding data.\n\n\n.. _ref_protocol_fmt_local_datetime:\n\ncal::local_datetime\n===================\n\n:eql:type:`cal::local_datetime` values are represented as a 64-bit integer,\nmost sigificant byte first.  The value is the number of *microseconds*\nbetween the encoded datetime and January 1st 2000, 00:00.\n\nFor example, the ``local_datetime`` value ``'2019-05-06T12:00'`` is\nencoded as:\n\n.. code-block:: c\n\n    0x00 0x02 0x2b 0x35 0x9b 0xc4 0x10 0x00\n\nSee the :ref:`client libraries <ref_bindings_datetime>` section for more info\nabout how to handle different precision when encoding data.\n\n\n.. _ref_protocol_fmt_local_date:\n\ncal::local_date\n===============\n\n:eql:type:`cal::local_date` values are represented as a 32-bit integer,\nmost sigificant byte first. The value is the number of *days*\nbetween the encoded date and January 1st 2000.\n\nFor example, the ``local_date`` value ``'2019-05-06'`` is\nencoded as:\n\n.. code-block:: c\n\n    0x00 0x00 0x1b 0x99\n\n\n.. _ref_protocol_fmt_local_time:\n\ncal::local_time\n===============\n\n:eql:type:`cal::local_time` values are represented as a 64-bit integer,\nmost sigificant byte first. The value is the number of *microseconds*\nsince midnight.\n\nFor example, the ``local_time`` value ``'12:10'`` is\nencoded as:\n\n.. code-block:: c\n\n    0x00 0x00 0x00 0x0a 0x32 0xae 0xf6 0x00\n\nSee the :ref:`client libraries <ref_bindings_datetime>` section for more info\nabout how to handle different precision when encoding data.\n\n\n.. _ref_protocol_fmt_duration:\n\nstd::duration\n=============\n\nThe :eql:type:`std::duration` values are represented as the following\nstructure:\n\n.. code-block:: c\n\n    struct Duration {\n        int64   microseconds;\n\n        // deprecated, is always 0\n        int32   days;\n\n        // deprecated, is always 0\n        int32   months;\n    };\n\nFor example, the ``duration`` value ``'48 hours 45 minutes 7.6 seconds'`` is\nencoded as:\n\n.. code-block:: c\n\n    // microseconds\n    0x00 0x00 0x00 0x28 0xdd 0x11 0x72 0x80\n\n    // days\n    0x00 0x00 0x00 0x00\n\n    // months\n    0x00 0x00 0x00 0x00\n\nSee the :ref:`client libraries <ref_bindings_datetime>` section for more info\nabout how to handle different precision when encoding data.\n\n\n.. _ref_protocol_fmt_relative_duration:\n\ncal::relative_duration\n======================\n\nThe :eql:type:`cal::relative_duration` values are represented as the following\nstructure:\n\n.. code-block:: c\n\n    struct Duration {\n        int64   microseconds;\n        int32   days;\n        int32   months;\n    };\n\nFor example, the ``cal::relative_duration`` value\n``'2 years 7 months 16 days 48 hours 45 minutes 7.6 seconds'`` is encoded as:\n\n.. code-block:: c\n\n    // microseconds\n    0x00 0x00 0x00 0x28 0xdd 0x11 0x72 0x80\n\n    // days\n    0x00 0x00 0x00 0x10\n\n    // months\n    0x00 0x00 0x00 0x1f\n\nSee the :ref:`client libraries <ref_bindings_datetime>` section for more info\nabout how to handle different precision when encoding data.\n\n\n.. _ref_protocol_fmt_date_duration:\n\ncal::date_duration\n==================\n\n:eql:type:`cal::date_duration` values are represented as the following\nstructure:\n\n.. code-block:: c\n\n    struct DateDuration {\n        int64   reserved;\n        int32   days;\n        int32   months;\n    };\n\nFor example, the ``cal::date_duration`` value ``'1 years 2 days'`` is encoded\nas:\n\n.. code-block:: c\n\n    // reserved\n    0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00\n\n    // days\n    0x00 0x00 0x00 0x02\n\n    // months\n    0x00 0x00 0x00 0x0c\n\n\n.. _ref_protocol_fmt_json:\n\nstd::json\n=========\n\n:eql:type:`std::json` values are represented as the following structure:\n\n.. code-block:: c\n\n    struct JSON {\n        uint8   format;\n        uint8   jsondata[];\n    };\n\n*format* is currently always ``1``, and *jsondata* is a UTF-8 encoded JSON\nstring.\n\n\n.. _ref_protocol_fmt_bigint:\n\nstd::bigint\n===========\n\n:eql:type:`std::bigint` values are represented as the following structure:\n\n.. code-block:: c\n\n    struct BigInt {\n        // Number of digits in digits[], can be 0.\n        uint16               ndigits;\n\n        // Weight of first digit.\n        int16                weight;\n\n        // Sign of the value\n        uint16<DecimalSign>  sign;\n\n        // Reserved value, must be zero\n        uint16               reserved;\n\n        // base-10000 digits.\n        uint16                digits[ndigits];\n    };\n\n    enum BigIntSign {\n        // Positive value.\n        POS     = 0x0000;\n\n        // Negative value.\n        NEG     = 0x4000;\n    };\n\nDecimal values are represented as a sequence of base-10000 *digits*.\nThe first digit is assumed to be multiplied by *weight* * 10000, i.e. there\nmight be up to weight + 1 digits. Trailing zeros may be absent.\n\nFor example, the bigint value ``-15000`` is represented as:\n\n.. code-block:: c\n\n    // ndigits\n    0x00 0x02\n\n    // weight\n    0x00 0x01\n\n    // sign\n    0x40 0x00\n\n    // reserved\n    0x00 0x00\n\n    // digits\n    0x00 0x01 0x13 0x88\n\n\n.. _ref_protocol_fmt_memory:\n\ncfg::memory\n===========\n\n:eql:type:`cfg::memory` values are represented as a number of *bytes*\nencoded as a 64-bit integer, most sigificant byte first.\n\nFor example, the ``cfg::memory`` value ``123MiB`` is represented as:\n\n.. code-block:: c\n\n    0x00 0x00 0x00 0x00 0x07 0xb0 0x00 0x00\n"
  },
  {
    "path": "docs/resources/protocol/dump_format.rst",
    "content": "Dump file format\n================\n\nThis description uses the same :ref:`conventions <ref_protocol_conventions>`\nas the protocol description.\n\n\nGeneral Structure\n-----------------\n\nDump file is structure as follows:\n\n1. Dump file format marker ``\\xFF\\xD8\\x00\\x00\\xD8EDGEDB\\x00DUMP\\x00``\n   (17 bytes)\n2. Format version number ``\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01`` (8 bytes)\n3. Header block\n4. Any number of data blocks\n\n\nGeneral Dump Block\n------------------\n\nBoth header and data blocks are formatted as follows:\n\n.. code-block:: c\n\n    struct DumpHeader {\n        int8            mtype;\n\n        // SHA1 hash sum of block data\n        byte            sha1sum[20];\n\n        // Length of message contents in bytes,\n        // including self.\n        int32           message_length;\n\n        // Block data. Should be treated in opaque way by a client.\n        byte            data[message_length];\n    }\n\nUpon receiving a protocol dump data message, the dump client should:\n\n* Replace packet type:\n    * ``@`` (0x40) → ``H`` (0x48)\n    * ``=`` (0x3d) → ``D`` (0x44)\n* Prepend SHA1 checksum to the block\n* Append the entire dump protocol message disregarding the\n  first byte (the message type).\n\n\nHeader Block\n------------\n\nFormat:\n\n.. code-block:: c\n\n    struct DumpHeader {\n        // Message type ('H')\n        int8            mtype = 0x48;\n\n        // SHA1 hash sum of block data\n        byte            sha1sum[20];\n\n        // Length of message contents in bytes,\n        // including self.\n        int32           message_length;\n\n        // A set of message headers.\n        Headers         headers;\n\n        // Protocol version of the dump\n        int16           major_ver;\n        int16           minor_ver;\n\n        // Schema data\n        string          schema_ddl;\n\n        // Type identifiers\n        int32           num_types;\n        TypeInfo        types[num_types];\n\n        // Object descriptors\n        int32           num_descriptors;\n        ObjectDesc      descriptors[num_descriptors]\n    };\n\n    struct TypeInfo {\n        string          type_name;\n        string          type_class;\n        byte            type_id[16];\n    }\n\n    struct ObjectDesc {\n        byte            object_id[16];\n        bytes           description;\n\n        int16           num_dependencies;\n        byte            dependency_id[num_dependencies][16];\n    }\n\nKnown headers:\n\n* 101 ``BLOCK_TYPE`` -- block type, always \"I\"\n* 102 ``SERVER_TIME`` -- server time when dump is started as a floating point\n  unix timestamp stringified\n* 103 ``SERVER_VERSION`` -- full version of server as string\n* 105 ``SERVER_CATALOG_VERSION`` -- the catalog version of the server, as\n  a 64-bit integer. The catalog version is an identifier that is incremented\n  whenever a change is made to the database layout or standard library.\n\n\nData Block\n----------\n\nFormat:\n\n.. code-block:: c\n\n    struct DumpBlock {\n        // Message type ('=')\n        int8            mtype = 0x3d;\n\n        // Length of message contents in bytes,\n        // including self.\n        int32           message_length;\n\n        // A set of message headers.\n        Headers         headers;\n    }\n\nKnown headers:\n\n* 101 ``BLOCK_TYPE`` -- block type, always \"D\"\n* 110 ``BLOCK_ID`` -- block identifier (16 bytes of UUID)\n* 111 ``BLOCK_NUM`` -- integer block index stringified\n* 112 ``BLOCK_DATA`` -- the actual block data\n"
  },
  {
    "path": "docs/resources/protocol/errors.rst",
    "content": ".. _ref_protocol_errors:\n\n======\nErrors\n======\n\nErrors inheritance\n==================\n\nEach error in Gel consists of a code, a name, and optionally tags. Errors\nin Gel can inherit from other errors. This is denoted by matching code\nprefixes. For example, ``TransactionConflictError`` (``0x_05_03_01_00``) is\nthe parent error for ``TransactionSerializationError`` (``0x_05_03_01_01``)\nand ``TransactionDeadlockError`` (``0x_05_03_01_02``). The matching prefix\nhere is ``0x_05_03_01``.\n\nWhen the Gel client expects a more general error and Gel returns a more\nspecific error that inherits from the general error, the check in the client\nmust take this into account. This can be expressed by the ``binary and``\noperation or ``&`` opeator in most programming languages:\n\n.. code-block::\n\n  (expected_error_code & server_error_code) == expected_error_code\n\n\nNote that although it is not explicitly stated in the ``edb/api/errors.txt``\nfile, each inherited error must contain all tags of the parent error. Given\nthat, ``TransactionSerializationError`` and ``TransactionDeadlockError``, for\nexample, must contain the ``SHOULD_RETRY`` tag that is defined for\n``TransactionConflictError``.\n\n\n.. _ref_protocol_error_codes:\n\nError codes\n===========\n\nError codes and names as specified in ``edb/api/errors.txt``:\n\n.. raw:: text\n    :file: errors.txt\n"
  },
  {
    "path": "docs/resources/protocol/index.rst",
    "content": ".. _ref_protocol_overview:\n\n===============\nBinary protocol\n===============\n\n|Gel| uses a message-based binary protocol for communication between\nclients and servers.  The protocol is supported over TCP/IP.\n\n\n.. toctree::\n    :maxdepth: 3\n    :hidden:\n\n    messages\n    errors\n    typedesc\n    dataformats\n    dump_format\n\n\n.. _ref_protocol_connecting:\n\nConnecting to Gel\n=================\n\nThe Gel binary protocol has two modes of operation: sockets and HTTP\ntunnelling. When connecting to Gel, the client can specify an accepted\n`ALPN Protocol`_ to use. If the client does not specify an ALPN protocol,\nHTTP tunnelling is assumed.\n\nSockets\n-------\n\nWhen using the ``edgedb-binary`` ALPN protocol, the client and server\ncommunicate over a raw TCP/IP socket, following the :ref:`message format\n<ref_message_format>` and :ref:`message flow <ref_message_flow>` described\nbelow.\n\n.. _ref_http_tunnelling:\n\nHTTP Tunnelling\n---------------\n\nHTTP tunnelling differs in a few ways:\n\n*  Authentication is handled at ``/auth/token``.\n\n*  Query execution is handled at ``/branch/{BRANCH}``.\n\n   .. note::\n      Prior to |Gel| and |EdgeDB| 5.0 *branches* were called *databases*.\n      If you're making a request against an older version of |EdgeDB|\n      you should change ``/branch/`` options to ``/db/``.\n\n*  Transactions are not supported.\n\nThe :ref:`authentication <ref_authentication>` phase is handled by sending\n``GET`` requests to ``/auth/token`` with the ``Authorization`` header\ncontaining the authorization payload with the format:\n\n.. code-block::\n\n  Authorization: {AUTH METHOD} data={PAYLOAD}\n\nThe client then reads the ``www-authenticate`` response header with the\nfollowing format:\n\n.. code-block::\n\n  www-authenticate: {AUTH METHOD} {AUTH PAYLOAD}\n\nThe auth payload's format is described by the auth method, usually\n``SCRAM-SHA-256``. If the auth method differs from the requested method,\nthe client should abort the authentication attempt.\n\n\nOnce the :ref:`authentication <ref_authentication>` phase is complete, the\nfinal response's body will contain an authorization token used to authenticate\nthe HTTP connection. The client then sends any following message to\n``/branch/{BRANCH}`` (or ``/db/{DATABASE}`` if you're using a version of\n|EdgeDB| < 5) with the following headers:\n\n* ``X-EdgeDB-User``: The username specified in the\n  :ref:`connection parameters <ref_reference_connection>`.\n\n* ``Authorization``: The authorization token received from the\n  :ref:`authentication <ref_authentication>` phase, prefixed by ``Bearer``.\n\n* ``Content-Type``: Always ``application/x.edgedb.v_1_0.binary``.\n\nThe response should be checked to match the content type, and the body should\nbe parsed as the :ref:`message format <ref_message_format>` described below;\nmultiple message can be included in the response body, and should be parsed in\norder.\n\n.. _ALPN Protocol:\n    https://github.com/geldata/rfcs/blob/master/text/\n    1008-tls-and-alpn.rst#alpn-and-protocol-changes\n\n.. _ref_protocol_conventions:\n\nConventions and data Types\n==========================\n\nThe message format descriptions in this section use a C-like struct definitions\nto describe their layout.  The structs are *packed*, i.e. there are never\nany alignment gaps.\n\nThe following data types are used in the descriptions:\n\n.. list-table::\n    :class: funcoptable\n\n    * - ``int8``\n      - 8-bit integer\n\n    * - ``int16``\n      - 16-bit integer, most significant byte first\n\n    * - ``int32``\n      - 32-bit integer, most significant byte first\n\n    * - ``int64``\n      - 64-bit integer, most significant byte first\n\n    * - ``uint8``\n      - 8-bit unsigned integer\n\n    * - ``uint16``\n      - 16-bit unsigned integer, most significant byte first\n\n    * - ``uint32``\n      - 32-bit unsigned integer, most significant byte first\n\n    * - ``uint64``\n      - 64-bit unsigned integer, most significant byte first\n\n    * - ``int8<T>`` or ``uint8<T>``\n      - an 8-bit signed or unsigned integer enumeration,\n        where *T* denotes the name of the enumeration\n\n    * - ``string``\n      - a UTF-8 encoded text string prefixed with its byte length as ``uint32``\n\n    * - ``bytes``\n      - a byte string prefixed with its length as ``uint32``\n\n    * - ``KeyValue``\n      - .. eql:struct:: edb.protocol.KeyValue\n\n    * - ``Annotation``\n      - .. eql:struct:: edb.protocol.Annotation\n\n    * - ``uuid``\n      - an array of 16 bytes with no length prefix, equivalent to\n        ``byte[16]``\n\n\n.. _ref_message_format:\n\nMessage Format\n==============\n\nAll messages in the Gel wire protocol have the following format:\n\n.. code-block:: c\n\n    struct {\n        uint8    message_type;\n        int32    payload_length;\n        uint8    payload[payload_length - 4];\n    };\n\nThe server and the client *MUST* not fragment messages. I.e the complete\nmessage must be sent before starting a new message. It's advised that whole\nmessage should be buffered before initiating a network call (but this\nrequirement is neither observable nor enforceable at the other side). It's\nalso common to buffer the whole message on the receiver side before starting\nto process it.\n\nErrors\n======\n\nAt any point the server may send an :ref:`ref_protocol_msg_error` indicating\nan error condition.  This is implied in the message flow documentation, and\nonly successful paths are explicitly documented.  The handling of the\n``ErrorResponse`` message depends on the connection phase, as well as the\nseverity of the error.\n\nIf the server is not able to recover from an error, the connection is closed\nimmediately after an ``ErrorResponse`` message is sent.\n\n\nLogs\n====\n\nSimilarly to ``ErrorResponse`` the server may send a\n:ref:`ref_protocol_msg_log` message.  The client should handle the\nmessage and continue as before.\n\n.. _ref_message_flow:\n\nMessage Flow\n============\n\nThere are two main phases in the lifetime of a Gel connection: the\nconnection phase, and the command phase.  The connection phase is responsible\nfor negotiating the protocol and connection parameters, including\nauthentication.  The command phase is the regular operation phase where the\nserver is processing queries sent by the client.\n\n\nConnection Phase\n----------------\n\nTo begin a session, a client opens a connection to the server, and sends\nthe :ref:`ref_protocol_msg_client_handshake`.  The server responds in one\nof three ways:\n\n1. One of the authentication messages (see :ref:`below <ref_authentication>`);\n2. :ref:`ref_protocol_msg_server_handshake` followed by one of the\n   authentication messages;\n3. :ref:`ref_protocol_msg_error` which indicates an invalid client handshake\n   message.\n\n:ref:`ref_protocol_msg_server_handshake` is only sent if the requested\nconnection parameters cannot be fully satisfied; the server responds to\noffer the protocol parameters it is willing to support. Client may proceed\nby noting lower protocol version and/or absent extensions. Client *MUST* close\nthe connection if protocol version is unsupported. Server *MUST* send subset\nof the extensions received in :ref:`ref_protocol_msg_client_handshake` (i.e.\nit never adds extra ones).\n\nWhile it's not required by the protocol specification itself, Gel server\ncurrently requires setting the following params in\n:ref:`ref_protocol_msg_client_handshake`:\n\n* ``user`` -- username for authentication\n* ``branch`` -- branch to connect to\n\n\n.. _ref_authentication:\n\nAuthentication\n--------------\n\n\nThe server then initiates the authentication cycle by sending an authentication\nrequest message, to which the client must respond with an appropriate\nauthentication response message.\n\nThe following messages are sent by the server in the authentication cycle:\n\n:ref:`ref_protocol_msg_auth_ok`\n    Authentication is successful.\n\n:ref:`ref_protocol_msg_auth_sasl`\n    The client must now initiate a SASL negotiation, using one of the\n    SASL mechanisms listed in the message.  The client will send an\n    :ref:`ref_protocol_msg_auth_sasl_initial_response` with the name of the\n    selected mechanism, and the first part of the SASL data stream in\n    response to this.  If further messages are needed, the server will\n    respond with :ref:`ref_protocol_msg_auth_sasl_continue`.\n\n:ref:`ref_protocol_msg_auth_sasl_continue`\n    This message contains challenge data from the previous step of SASL\n    negotiation (:ref:`ref_protocol_msg_auth_sasl`, or a previous\n    :ref:`ref_protocol_msg_auth_sasl_continue`).  The client must respond\n    with an :ref:`ref_protocol_msg_auth_sasl_response` message.\n\n:ref:`ref_protocol_msg_auth_sasl_final`\n    SASL authentication has completed with additional mechanism-specific\n    data for the client.  The server will next send\n    :ref:`ref_protocol_msg_auth_ok` to indicate successful authentication,\n    or an :ref:`ref_protocol_msg_error` to indicate failure. This message is\n    sent only if the SASL mechanism specifies additional data to be sent\n    from server to client at completion.\n\nIf the frontend does not support the authentication method requested by the\nserver, then it should immediately close the connection.\n\nOnce the server has confirmed successful authentication with\n:ref:`ref_protocol_msg_auth_ok`, it then sends one or more of the following\nmessages:\n\n:ref:`ref_protocol_msg_server_key_data`\n    This message provides per-connection secret-key data that the client\n    must save if it wants to be able to issue certain requests later.  The\n    client should not respond to this message.\n\n:ref:`ref_protocol_msg_server_parameter_status`\n    This message informs the frontend about the setting of certain server\n    parameters.  The client can ignore this message, or record the settings\n    for its future use.  The client should not respond to this message.\n\nThe connection phase ends when the server sends the first\n:ref:`ref_protocol_msg_ready_for_command` message, indicating the start of\na command cycle.\n\n\nCommand Phase\n-------------\n\nIn the command phase, the server expects the client to send one of the\nfollowing messages:\n\n:ref:`ref_protocol_msg_parse`\n    Instructs the server to parse the provided command or commands for\n    execution.  The server responds with a\n    :ref:`ref_protocol_msg_command_data_description` containing the\n    :ref:`type descriptor <ref_proto_typedesc>` data necessary to perform\n    data I/O for this command.\n\n:ref:`ref_protocol_msg_execute`\n    Execute the provided command or commands.  This message expects the\n    client to declare a correct :ref:`type descriptor <ref_proto_typedesc>`\n    identifier for command arguments.  If the declared input type descriptor\n    does not match the expected value, a\n    :ref:`ref_protocol_msg_command_data_description` message is returned\n    followed by a ``ParameterTypeMismatchError`` in an ``ErrorResponse``\n    message.\n\n    If the declared output type descriptor does not match, the server\n    will send a :ref:`ref_protocol_msg_command_data_description` prior to\n    sending any :ref:`ref_protocol_msg_data` messages.\n\nThe client could attach state data in both messages. When doing so, the client\nmust also set a correct :ref:`type descriptor <ref_proto_typedesc>` identifier\nfor the state data.  If the declared state type descriptor does not match the\nexpected value, a :ref:`ref_protocol_msg_state_data_description` message is\nreturned followed by a ``StateMismatchError`` in an ``ErrorResponse`` message.\nHowever, the special type id of zero ``00000000-0000-0000-0000-000000000000``\nfor empty/default state is always a match.\n\nEach of the messages could contain one or more EdgeQL commands separated\nby a semicolon (``;``).  If more than one EdgeQL command is found in a single\nmessage, the server will treat the commands as an EdgeQL script. EdgeQL scripts\nare always atomic, they will be executed in an implicit transaction block if no\nexplicit transaction is currently active. Therefore, EdgeQL scripts have\nlimitations on the kinds of EdgeQL commands they can contain:\n\n* Transaction control commands are not allowed, like ``start transaction``,\n  ``commit``, ``declare savepoint``, or ``rollback to savepoint``.\n* Non-transactional commands, like ``create branch``,\n  ``configure instance``, or ``create database`` are not allowed.\n\nIn the command phase, the server can be in one of the three main states:\n\n* *idle*: server is waiting for a command;\n* *busy*: server is executing a command;\n* *error*: server encountered an error and is discarding incoming messages.\n\nWhenever a server switches to the *idle* state, it sends a\n:ref:`ref_protocol_msg_ready_for_command` message.\n\nWhenever a server encounters an error, it sends an\n:ref:`ref_protocol_msg_error` message and switches into\nthe *error* state.\n\nTo switch a server from the *error* state into the *idle* state, a\n:ref:`ref_protocol_msg_sync` message must be sent by the client.\n\n\n.. _ref_protocol_dump_flow:\n\nDump Flow\n---------\n\nBackup flow goes as following:\n\n1. Client sends :ref:`ref_protocol_msg_dump` message\n2. Server sends :ref:`ref_protocol_msg_dump_header` message\n3. Server sends one or more :ref:`ref_protocol_msg_dump_block` messages\n4. Server sends :ref:`ref_protocol_msg_command_complete` message\n\nUsually client should send :ref:`ref_protocol_msg_sync` after ``Dump`` message\nto finish implicit transaction.\n\n\n.. _ref_protocol_restore_flow:\n\nRestore Flow\n------------\n\nRestore procedure fills up the |branch| the\nclient is connected to with the schema and data from the dump file.\n\nFlow is the following:\n\n1. Client sends :ref:`ref_protocol_msg_restore` message with the dump header\n   block\n2. Server sends :ref:`ref_protocol_msg_restore_ready` message as a confirmation\n   that it has accepted the header, restored schema and ready to receive data\n   blocks\n3. Clients sends one or more :ref:`ref_protocol_msg_restore_block` messages\n4. Client sends :ref:`ref_protocol_msg_restore_eof` message\n5. Server sends :ref:`ref_protocol_msg_command_complete` message\n\nNote: :ref:`ref_protocol_msg_error` may be sent from the server at\nany time. In case of error, :ref:`ref_protocol_msg_sync` must be sent and all\nsubsequent messages ignored until :ref:`ref_protocol_msg_ready_for_command` is\nreceived.\n\nRestore protocol doesn't require a :ref:`ref_protocol_msg_sync` message except\nfor error cases.\n\n\nTermination\n===========\n\nThe normal termination procedure is that the client sends a\n:ref:`ref_protocol_msg_terminate` message and immediately closes the\nconnection.  On receipt of this message, the server cleans up the\nconnection resources and closes the connection.\n\nIn some cases the server might disconnect without a client request to do so.\nIn such cases the server will attempt to send an :ref:`ref_protocol_msg_error`\nor a :ref:`ref_protocol_msg_log` message to indicate the reason for the\ndisconnection.\n"
  },
  {
    "path": "docs/resources/protocol/messages.rst",
    "content": "========\nMessages\n========\n\n\n.. list-table::\n    :class: funcoptable\n\n    * - **Server Messages**\n      -\n\n    * - :ref:`ref_protocol_msg_auth_ok`\n      - Authentication is successful.\n\n    * - :ref:`ref_protocol_msg_auth_sasl`\n      - SASL authentication is required.\n\n    * - :ref:`ref_protocol_msg_auth_sasl_continue`\n      - SASL authentication challenge.\n\n    * - :ref:`ref_protocol_msg_auth_sasl_final`\n      - SASL authentication final message.\n\n    * - :ref:`ref_protocol_msg_command_complete`\n      - Successful completion of a command.\n\n    * - :ref:`ref_protocol_msg_command_data_description`\n      - Description of command data input and output.\n\n    * - :ref:`ref_protocol_msg_state_data_description`\n      - Description of state data.\n\n    * - :ref:`ref_protocol_msg_data`\n      - Command result data element.\n\n    * - :ref:`ref_protocol_msg_dump_header`\n      - Initial message of the database backup protocol\n\n    * - :ref:`ref_protocol_msg_dump_block`\n      - Single chunk of database backup data\n\n    * - :ref:`ref_protocol_msg_error`\n      - Server error.\n\n    * - :ref:`ref_protocol_msg_log`\n      - Server log message.\n\n    * - :ref:`ref_protocol_msg_server_parameter_status`\n      - Server parameter value.\n\n    * - :ref:`ref_protocol_msg_ready_for_command`\n      - Server is ready for a command.\n\n    * - :ref:`ref_protocol_msg_restore_ready`\n      - Successful response to the :ref:`ref_protocol_msg_restore` message\n\n    * - :ref:`ref_protocol_msg_server_handshake`\n      - Initial server connection handshake.\n\n    * - :ref:`ref_protocol_msg_server_key_data`\n      - Opaque token identifying the server connection.\n\n    * - **Client Messages**\n      -\n\n    * - :ref:`ref_protocol_msg_auth_sasl_initial_response`\n      - SASL authentication initial response.\n\n    * - :ref:`ref_protocol_msg_auth_sasl_response`\n      - SASL authentication response.\n\n    * - :ref:`ref_protocol_msg_client_handshake`\n      - Initial client connection handshake.\n\n    * - :ref:`ref_protocol_msg_dump`\n      - Initiate database backup\n\n    * - :ref:`ref_protocol_msg_parse`\n      - Parse EdgeQL command(s).\n\n    * - :ref:`ref_protocol_msg_execute`\n      - Parse and/or execute a query.\n\n    * - :ref:`ref_protocol_msg_restore`\n      - Initiate database restore\n\n    * - :ref:`ref_protocol_msg_restore_block`\n      - Next block of database dump\n\n    * - :ref:`ref_protocol_msg_restore_eof`\n      - End of database dump\n\n    * - :ref:`ref_protocol_msg_sync`\n      - Provide an explicit synchronization point.\n\n    * - :ref:`ref_protocol_msg_terminate`\n      - Terminate the connection.\n\n\n.. _ref_protocol_msg_error:\n\nErrorResponse\n=============\n\nSent by: server.\n\nFormat:\n\n.. eql:struct:: edb.protocol.ErrorResponse\n\n.. eql:struct:: edb.protocol.ErrorSeverity\n\n\nSee the :ref:`list of error codes <ref_protocol_error_codes>` for all possible\nerror codes.\n\nKnown attributes:\n\n* 0x0001 ``HINT``: ``str`` -- error hint.\n* 0x0002 ``DETAILS``: ``str`` -- error details.\n* 0x0101 ``SERVER_TRACEBACK``: ``str`` -- error traceback from server\n  (is only sent in dev mode).\n* 0xFFF1 ``POSITION_START`` -- byte offset of the start of the error span.\n* 0xFFF2 ``POSITION_END`` -- byte offset of the end of the error span.\n* 0xFFF3 ``LINE_START`` -- one-based line number of the start of the\n  error span.\n* 0xFFF4 ``COLUMN_START`` -- one-based column number of the start of the\n  error span.\n* 0xFFF5 ``UTF16_COLUMN_START`` -- zero-based column number in UTF-16\n  encoding of the start of the error span.\n* 0xFFF6 ``LINE_END`` -- one-based line number of the start of the\n  error span.\n* 0xFFF7 ``COLUMN_END`` -- one-based column number of the start of the\n  error span.\n* 0xFFF8 ``UTF16_COLUMN_END`` -- zero-based column number in UTF-16\n  encoding of the end of the error span.\n* 0xFFF9 ``CHARACTER_START`` -- zero-based offset of the error span in\n  terms of Unicode code points.\n* 0xFFFA ``CHARACTER_END`` -- zero-based offset of the end of the error\n  span.\n\nNotes:\n\n1. Error span is the range of characters (or equivalent bytes) of the\n   original query that compiler points to as the source of the error.\n2. ``COLUMN_*`` is defined in terms of width of characters defined by\n   Unicode Standard Annex #11, in other words, the column number in the\n   text if rendered with monospace font, e.g. in a terminal.\n3. ``UTF16_COLUMN_*`` is defined as number of UTF-16 code units (i.e. two\n   byte-pairs) that precede target character on the same line.\n4. ``*_END`` points to a next character after the last character of the\n   error span.\n\n\n.. _ref_protocol_msg_log:\n\nLogMessage\n==========\n\nSent by: server.\n\nFormat:\n\n.. eql:struct:: edb.protocol.LogMessage\n\n.. eql:struct:: edb.protocol.MessageSeverity\n\nSee the :ref:`list of error codes <ref_protocol_error_codes>` for all possible\nlog message codes.\n\n\n.. _ref_protocol_msg_ready_for_command:\n\nReadyForCommand\n===============\n\nSent by: server.\n\nFormat:\n\n.. eql:struct:: edb.protocol.ReadyForCommand\n\n.. eql:struct:: edb.protocol.TransactionState\n\n.. eql:struct:: edb.protocol.Annotation\n\n.. _ref_protocol_msg_restore_ready:\n\nRestoreReady\n============\n\nSent by: server.\n\nInitial :ref:`ref_protocol_msg_restore` message accepted, ready to receive\ndata. See :ref:`ref_protocol_restore_flow`.\n\nFormat:\n\n.. eql:struct:: edb.protocol.RestoreReady\n\n.. eql:struct:: edb.protocol.Annotation\n\n.. _ref_protocol_msg_command_complete:\n\nCommandComplete\n===============\n\nSent by: server.\n\nFormat:\n\n.. eql:struct:: edb.protocol.CommandComplete\n\n.. eql:struct:: edb.protocol.Annotation\n\n\n.. _ref_protocol_msg_dump:\n\nDump\n====\n\nSent by: client.\n\nInitiates a database backup. See :ref:`ref_protocol_dump_flow`.\n\nFormat:\n\n.. eql:struct:: edb.protocol.Dump\n\n.. eql:struct:: edb.protocol.Annotation\n\n.. eql:struct:: edb.protocol.DumpFlag\n\nUse:\n\n* ``DUMP_SECRETS`` to include secrets in the backup. By default, secrets are\n  not included.\n\n\n.. _ref_protocol_msg_command_data_description:\n\nCommandDataDescription\n======================\n\nSent by: server.\n\nFormat:\n\n.. eql:struct:: edb.protocol.CommandDataDescription\n\n.. eql:struct:: edb.protocol.enums.Cardinality\n\n.. eql:struct:: edb.protocol.Annotation\n\n\nThe format of the *input_typedesc* and *output_typedesc* fields is described\nin the :ref:`ref_proto_typedesc` section.\n\n\n.. _ref_protocol_msg_state_data_description:\n\nStateDataDescription\n====================\n\nSent by: server.\n\nFormat:\n\n.. eql:struct:: edb.protocol.StateDataDescription\n\n\nThe format of the *typedesc* fields is described in the\n:ref:`ref_proto_typedesc` section.\n\n\n.. _ref_protocol_msg_sync:\n\nSync\n====\n\nSent by: client.\n\nFormat:\n\n.. eql:struct:: edb.protocol.Sync\n\n\n.. _ref_protocol_msg_restore:\n\nRestore\n=======\n\nSent by: client.\n\nInitiate restore to the current |branch|. See :ref:`ref_protocol_restore_flow`.\n\nFormat:\n\n.. eql:struct:: edb.protocol.Restore\n\n.. _ref_protocol_msg_restore_block:\n\nRestoreBlock\n============\n\nSent by: client.\n\nSend dump file data block.\nSee :ref:`ref_protocol_restore_flow`.\n\nFormat:\n\n.. eql:struct:: edb.protocol.RestoreBlock\n\n\n.. _ref_protocol_msg_restore_eof:\n\nRestoreEof\n==========\n\nSent by: client.\n\nNotify server that dump is fully uploaded.\nSee :ref:`ref_protocol_restore_flow`.\n\nFormat:\n\n.. eql:struct:: edb.protocol.RestoreEof\n\n\n.. _ref_protocol_msg_execute:\n\nExecute\n=======\n\nSent by: client.\n\nFormat:\n\n.. eql:struct:: edb.protocol.Execute\n\n.. eql:struct:: edb.protocol.OutputFormat\n\n.. eql:struct:: edb.protocol.Annotation\n\nUse:\n\n* ``BINARY`` to return data encoded in binary.\n\n* ``JSON`` to return data as single row and single field that contains\n  the resultset as a single JSON array\".\n\n* ``JSON_ELEMENTS`` to return a single JSON string per top-level set element.\n  This can be used to iterate over a large result set efficiently.\n\n* ``NONE`` to prevent the server from returning data, even if the EdgeQL\n  command does.\n\nThe data in *arguments* must be encoded as a\n:ref:`tuple value <ref_protocol_fmt_tuple>` described by\na type descriptor identified by *input_typedesc_id*.\n\n\n.. eql:struct:: edb.protocol.enums.Cardinality\n\n\n.. _ref_protocol_msg_parse:\n\nParse\n=====\n\nSent by: client.\n\n.. eql:struct:: edb.protocol.Parse\n\n.. eql:struct:: edb.protocol.Capability\n\n.. eql:struct:: edb.protocol.Annotation\n\nSee RFC1004_ for more information on capability flags.\n\n.. eql:struct:: edb.protocol.CompilationFlag\n\nUse:\n\n* ``0x0000_0000_0000_0001`` (``INJECT_OUTPUT_TYPE_IDS``) -- if set, all\n  returned objects have a ``__tid__`` property set to their type ID\n  (equivalent to having an implicit ``__tid__ := .__type__.id`` computed\n  property.)\n\n* ``0x0000_0000_0000_0002`` (``INJECT_OUTPUT_TYPE_NAMES``) -- if set all\n  returned objects have a ``__tname__`` property set to their type name\n  (equivalent to having an implicit ``__tname__ := .__type__.name`` computed\n  property.)  Note that specifying this flag might slow down queries.\n\n* ``0x0000_0000_0000_0004`` (``INJECT_OUTPUT_OBJECT_IDS``) -- if set all\n  returned objects have an ``id`` property set to their identifier, even if\n  not specified explicitly in the output shape.\n\n.. eql:struct:: edb.protocol.OutputFormat\n\nUse:\n\n* ``BINARY`` to return data encoded in binary.\n\n* ``JSON`` to return data as single row and single field that contains\n  the resultset as a single JSON array\".\n\n* ``JSON_ELEMENTS`` to return a single JSON string per top-level set element.\n  This can be used to iterate over a large result set efficiently.\n\n* ``NONE`` to prevent the server from returning data, even if the EdgeQL\n  statement does.\n\n.. eql:struct:: edb.protocol.enums.Cardinality\n\n\n.. _ref_protocol_msg_data:\n\nData\n====\n\nSent by: server.\n\nFormat:\n\n.. eql:struct:: edb.protocol.Data\n\n.. eql:struct:: edb.protocol.DataElement\n\nThe exact encoding of ``DataElement.data`` is defined by the query output\n:ref:`type descriptor <ref_proto_typedesc>`.\n\nWire formats for the standard scalar types and collections are documented in\n:ref:`ref_proto_dataformats`.\n\n\n.. _ref_protocol_msg_dump_header:\n\nDump Header\n===========\n\nSent by: server.\n\nInitial message of database backup protocol.\nSee :ref:`ref_protocol_dump_flow`.\n\nFormat:\n\n.. eql:struct:: edb.protocol.DumpHeader\n\n.. eql:struct:: edb.protocol.DumpTypeInfo\n\n.. eql:struct:: edb.protocol.DumpObjectDesc\n\nKnown attributes:\n\n* 101 ``BLOCK_TYPE`` -- block type, always \"I\"\n* 102 ``SERVER_TIME`` -- server time when dump is started as a floating point\n  unix timestamp stringified\n* 103 ``SERVER_VERSION`` -- full version of server as string\n* 105 ``SERVER_CATALOG_VERSION`` -- the catalog version of the server, as\n  a 64-bit integer. The catalog version is an identifier that is incremented\n  whenever a change is made to the database layout or standard library.\n\n\n.. _ref_protocol_msg_dump_block:\n\nDump Block\n==========\n\nSent by: server.\n\nThe actual protocol data in the backup protocol.\nSee :ref:`ref_protocol_dump_flow`.\n\nFormat:\n\n.. eql:struct:: edb.protocol.DumpBlock\n\n\nKnown attributes:\n\n* 101 ``BLOCK_TYPE`` -- block type, always \"D\"\n* 110 ``BLOCK_ID`` -- block identifier (16 bytes of UUID)\n* 111 ``BLOCK_NUM`` -- integer block index stringified\n* 112 ``BLOCK_DATA`` -- the actual block data\n\n\n.. _ref_protocol_msg_server_key_data:\n\nServerKeyData\n=============\n\nSent by: server.\n\nFormat:\n\n.. eql:struct:: edb.protocol.ServerKeyData\n\n\n.. _ref_protocol_msg_server_parameter_status:\n\nParameterStatus\n===============\n\nSent by: server.\n\nFormat:\n\n.. eql:struct:: edb.protocol.ParameterStatus\n\nKnown statuses:\n\n* ``suggested_pool_concurrency`` -- suggested default size for clients\n  connection pools. Serialized as UTF-8 encoded string.\n\n* ``system_config`` -- a set of instance-level configuration settings\n  exposed to clients on connection. Serialized as:\n\n  .. eql:struct:: edb.protocol.ParameterStatus_SystemConfig\n\n  Where ``DataElement`` is defined in the same way as for the\n  :ref:`Data <ref_protocol_msg_data>` message:\n\n  .. eql:struct:: edb.protocol.DataElement\n\n\n.. _ref_protocol_msg_client_handshake:\n\nClientHandshake\n===============\n\nSent by: client.\n\nFormat:\n\n.. eql:struct:: edb.protocol.ClientHandshake\n\n.. eql:struct:: edb.protocol.ConnectionParam\n\n.. eql:struct:: edb.protocol.ProtocolExtension\n\nThe ``ClientHandshake`` message is the first message sent by the client\nupon connecting to the server.  It is the first phase of protocol negotiation,\nwhere the client sends the requested protocol version and extensions.\nCurrently, the only defined ``major_ver`` is ``1``, and ``minor_ver`` is ``0``.\nNo protocol extensions are currently defined.  The server always responds\nwith the :ref:`ref_protocol_msg_server_handshake`.\n\n\n.. _ref_protocol_msg_server_handshake:\n\nServerHandshake\n===============\n\nSent by: server.\n\nFormat:\n\n.. eql:struct:: edb.protocol.ServerHandshake\n\n.. eql:struct:: edb.protocol.ProtocolExtension\n\n\nThe ``ServerHandshake`` message is a direct response to the\n:ref:`ref_protocol_msg_client_handshake` message and is sent by the server\nin the case where the server does not support the protocol version or\nprotocol extensions requested by the client.  It contains the maximum\nprotocol version supported by the server, considering the version requested\nby the client.  It also contains the intersection of the client-requested and\nserver-supported protocol extensions.  Any requested extensions not listed\nin the ``Server Handshake`` message are considered unsupported.\n\n\n.. _ref_protocol_msg_auth_ok:\n\nAuthenticationOK\n================\n\nSent by: server.\n\nFormat:\n\n.. eql:struct:: edb.protocol.AuthenticationOK\n\nThe ``AuthenticationOK`` message is sent by the server once it considers\nthe authentication to be successful.\n\n\n.. _ref_protocol_msg_auth_sasl:\n\nAuthenticationSASL\n==================\n\nSent by: server.\n\nFormat:\n\n.. eql:struct:: edb.protocol.AuthenticationRequiredSASLMessage\n\nThe ``AuthenticationSASL`` message is sent by the server if\nit determines that a SASL-based authentication method is required in\norder to connect using the connection parameters specified in the\n:ref:`ref_protocol_msg_client_handshake`.  The message contains a list\nof *authentication methods* supported by the server in the order preferred\nby the server.\n\n.. note::\n    At the moment, the only SASL authentication method supported\n    by Gel is ``SCRAM-SHA-256``\n    (`RFC 7677 <https://tools.ietf.org/html/rfc7677>`_).\n\nThe client must select an appropriate authentication method from the list\nreturned by the server and send an\n:ref:`ref_protocol_msg_auth_sasl_initial_response`.\nOne or more server-challenge and client-response message follow.  Each\nserver-challenge is sent in an :ref:`ref_protocol_msg_auth_sasl_continue`,\nfollowed by a response from the client in an\n:ref:`ref_protocol_msg_auth_sasl_response` message.  The particulars of the\nmessages are mechanism specific.  Finally, when the authentication\nexchange is completed successfully, the server sends an\n:ref:`ref_protocol_msg_auth_sasl_final`, followed immediately\nby an :ref:`ref_protocol_msg_auth_ok`.\n\n\n.. _ref_protocol_msg_auth_sasl_continue:\n\nAuthenticationSASLContinue\n==========================\n\nSent by: server.\n\nFormat:\n\n.. eql:struct:: edb.protocol.AuthenticationSASLContinue\n\n.. _ref_protocol_msg_auth_sasl_final:\n\nAuthenticationSASLFinal\n=======================\n\nSent by: server.\n\nFormat:\n\n.. eql:struct:: edb.protocol.AuthenticationSASLFinal\n\n.. _ref_protocol_msg_auth_sasl_initial_response:\n\nAuthenticationSASLInitialResponse\n=================================\n\nSent by: client.\n\nFormat:\n\n.. eql:struct:: edb.protocol.AuthenticationSASLInitialResponse\n\n.. _ref_protocol_msg_auth_sasl_response:\n\nAuthenticationSASLResponse\n==========================\n\nSent by: client.\n\nFormat:\n\n.. eql:struct:: edb.protocol.AuthenticationSASLResponse\n\n\n.. _ref_protocol_msg_terminate:\n\nTerminate\n=========\n\nSent by: client.\n\nFormat:\n\n.. eql:struct:: edb.protocol.Terminate\n\n.. _RFC1004:\n    https://github.com/geldata/rfcs/blob/master/text/1004-transactions-api.rst\n"
  },
  {
    "path": "docs/resources/protocol/typedesc.rst",
    "content": ".. _ref_proto_typedesc:\n\n================\nType descriptors\n================\n\nThis section describes how type information for query input and results\nis encoded.  Specifically, this is needed to decode the server response to\nthe :ref:`ref_protocol_msg_command_data_description` message.\n\nThe type descriptor is essentially a list of type information *blocks*:\n\n* each *block* encodes one type;\n\n* *blocks* can reference other *blocks*.\n\nWhile parsing the *blocks*, a database driver can assemble an\n*encoder* or a *decoder* of the Gel binary data.\n\nAn *encoder* is used to encode objects, native to the driver's runtime,\nto binary data that Gel can decode and work with.\n\nA *decoder* is used to decode data from Gel native format to\ndata types native to the driver.\n\nThere is one special type with *type id* of zero:\n``00000000-0000-0000-0000-000000000000``. The describe result of this type\ncontains zero *blocks*. It's used when a statement returns no meaningful\nresults, e.g. the ``CREATE BRANCH example`` statement.  It is also used to\nrepresent the input descriptor when a query does not receive any arguments,\nor the state descriptor for an empty/default state.\n\n.. versionadded:: 6.0\n\n   Added ``SQLRecordDescriptor``.\n\n\nDescriptor and type IDs\n=======================\n\nThe descriptor and type IDs in structures below are intended to be semi-stable\nunique identifiers of a type.  Fundamental types have globally stable known\nIDs, and type IDs for schema-defined types (i.e. with\n``schema_defined = true``) persist.  Ephemeral type ids are derived from\ntype structure and are not guaranteed to be stable, but are still useful\nas cache keys.\n\n\nSet Descriptor\n==============\n\n.. code-block:: c\n\n    struct SetDescriptor {\n        // Indicates that this is a Set value descriptor.\n        uint8   tag = 0;\n\n        // Descriptor ID.\n        uuid    id;\n\n        // Set element type descriptor index.\n        uint16  type;\n    };\n\nSet values are encoded on the wire as\n:ref:`single-dimensional arrays <ref_protocol_fmt_array>`.\n\n\nScalar Type Descriptor\n======================\n\n.. code-block:: c\n\n    struct ScalarTypeDescriptor {\n        // Indicates that this is a\n        // Scalar Type descriptor.\n        uint8   tag = 3;\n\n        // Schema type ID.\n        uuid    id;\n\n        // Schema type name.\n        string  name;\n\n        // Whether the type is defined in the schema\n        // or is ephemeral.\n        bool    schema_defined;\n\n        // Number of ancestor scalar types.\n        uint16  ancestors_count;\n\n        // Indexes of ancestor scalar type descriptors\n        // in ancestor resolution order (C3).\n        uint16  ancestors[ancestors_count];\n    };\n\nThe descriptor IDs for fundamental scalar types are constant.\nThe following table lists all Gel fundamental type descriptor IDs:\n\n.. list-table::\n   :header-rows: 1\n\n   * - ID\n     - Type\n\n   * - ``00000000-0000-0000-0000-000000000100``\n     - :ref:`std::uuid <ref_protocol_fmt_uuid>`\n\n   * - ``00000000-0000-0000-0000-000000000101``\n     - :ref:`std::str <ref_protocol_fmt_str>`\n\n   * - ``00000000-0000-0000-0000-000000000102``\n     - :ref:`std::bytes <ref_protocol_fmt_bytes>`\n\n   * - ``00000000-0000-0000-0000-000000000103``\n     - :ref:`std::int16 <ref_protocol_fmt_int16>`\n\n   * - ``00000000-0000-0000-0000-000000000104``\n     - :ref:`std::int32 <ref_protocol_fmt_int32>`\n\n   * - ``00000000-0000-0000-0000-000000000105``\n     - :ref:`std::int64 <ref_protocol_fmt_int64>`\n\n   * - ``00000000-0000-0000-0000-000000000106``\n     - :ref:`std::float32 <ref_protocol_fmt_float32>`\n\n   * - ``00000000-0000-0000-0000-000000000107``\n     - :ref:`std::float64 <ref_protocol_fmt_float64>`\n\n   * - ``00000000-0000-0000-0000-000000000108``\n     - :ref:`std::decimal <ref_protocol_fmt_decimal>`\n\n   * - ``00000000-0000-0000-0000-000000000109``\n     - :ref:`std::bool <ref_protocol_fmt_bool>`\n\n   * - ``00000000-0000-0000-0000-00000000010A``\n     - :ref:`std::datetime <ref_protocol_fmt_datetime>`\n\n   * - ``00000000-0000-0000-0000-00000000010E``\n     - :ref:`std::duration <ref_protocol_fmt_duration>`\n\n   * - ``00000000-0000-0000-0000-00000000010F``\n     - :ref:`std::json <ref_protocol_fmt_json>`\n\n   * - ``00000000-0000-0000-0000-00000000010B``\n     - :ref:`cal::local_datetime <ref_protocol_fmt_local_datetime>`\n\n   * - ``00000000-0000-0000-0000-00000000010C``\n     - :ref:`cal::local_date <ref_protocol_fmt_local_date>`\n\n   * - ``00000000-0000-0000-0000-00000000010D``\n     - :ref:`cal::local_time <ref_protocol_fmt_local_time>`\n\n   * - ``00000000-0000-0000-0000-000000000110``\n     - :ref:`std::bigint <ref_protocol_fmt_bigint>`\n\n   * - ``00000000-0000-0000-0000-000000000111``\n     - :ref:`cal::relative_duration <ref_protocol_fmt_relative_duration>`\n\n   * - ``00000000-0000-0000-0000-000000000112``\n     - :ref:`cal::date_duration <ref_protocol_fmt_date_duration>`\n\n   * - ``00000000-0000-0000-0000-000000000130``\n     - :ref:`cfg::memory <ref_protocol_fmt_memory>`\n\n\nTuple Type Descriptor\n=====================\n\n.. code-block:: c\n\n    struct TupleTypeDescriptor {\n        // Indicates that this is a\n        // Tuple Type descriptor.\n        uint8     tag = 4;\n\n        // Schema type ID.\n        uuid      id;\n\n        // Schema type name.\n        string    name;\n\n        // Whether the type is defined in the schema\n        // or is ephemeral.\n        bool      schema_defined;\n\n        // Number of ancestor scalar types.\n        uint16    ancestors_count;\n\n        // Indexes of ancestor scalar type descriptors\n        // in ancestor resolution order (C3).\n        uint16    ancestors[ancestors_count];\n\n        // The number of elements in tuple.\n        uint16    element_count;\n\n        // Indexes of element type descriptors.\n        uint16    element_types[element_count];\n    };\n\nAn empty tuple type descriptor has an ID of\n``00000000-0000-0000-0000-0000000000FF``.\n\n\nNamed Tuple Type Descriptor\n===========================\n\n.. code-block:: c\n\n    struct NamedTupleTypeDescriptor {\n        // Indicates that this is a\n        // Named Tuple Type descriptor.\n        uint8         tag = 5;\n\n        // Schema type ID.\n        uuid          id;\n\n        // Schema type name.\n        string        name;\n\n        // Whether the type is defined in the schema\n        // or is ephemeral.\n        bool          schema_defined;\n\n        // Number of ancestor scalar types.\n        uint16        ancestors_count;\n\n        // Indexes of ancestor scalar type descriptors\n        // in ancestor resolution order (C3).\n        uint16        ancestors[ancestors_count];\n\n        // The number of elements in tuple.\n        uint16        element_count;\n\n        // Indexes of element descriptors.\n        TupleElement  elements[element_count];\n    };\n\n    struct TupleElement {\n        // Field name.\n        string  name;\n\n        // Field type descriptor index.\n        int16   type;\n    };\n\n\nArray Type Descriptor\n=====================\n\n.. code-block:: c\n\n    struct ArrayTypeDescriptor {\n        // Indicates that this is an\n        // Array Type descriptor.\n        uint8   tag = 6;\n\n        // Schema type ID.\n        uuid    id;\n\n        // Schema type name.\n        string  name;\n\n        // Whether the type is defined in the schema\n        // or is ephemeral.\n        bool    schema_defined;\n\n        // Number of ancestor scalar types.\n        uint16  ancestors_count;\n\n        // Indexes of ancestor scalar type descriptors\n        // in ancestor resolution order (C3).\n        uint16  ancestors[ancestors_count];\n\n        // Array element type.\n        uint16  type;\n\n        // The number of array dimensions, at least 1.\n        uint16  dimension_count;\n\n        // Sizes of array dimensions, -1 indicates\n        // unbound dimension.\n        int32   dimensions[dimension_count];\n    };\n\n\nEnumeration Type Descriptor\n===========================\n\n.. code-block:: c\n\n    struct EnumerationTypeDescriptor {\n        // Indicates that this is an\n        // Enumeration Type descriptor.\n        uint8   tag = 7;\n\n        // Schema type ID.\n        uuid    id;\n\n        // Schema type name.\n        string  name;\n\n        // Whether the type is defined in the schema\n        // or is ephemeral.\n        bool    schema_defined;\n\n        // Number of ancestor scalar types.\n        uint16  ancestors_count;\n\n        // Indexes of ancestor scalar type descriptors\n        // in ancestor resolution order (C3).\n        uint16  ancestors[ancestors_count];\n\n        // The number of enumeration members.\n        uint16  member_count;\n\n        // Names of enumeration members.\n        string  members[member_count];\n    };\n\n\nRange Type Descriptor\n=====================\n\n.. code-block:: c\n\n    struct RangeTypeDescriptor {\n        // Indicates that this is a\n        // Range Type descriptor.\n        uint8   tag = 9;\n\n        // Schema type ID.\n        uuid    id;\n\n        // Schema type name.\n        string  name;\n\n        // Whether the type is defined in the schema\n        // or is ephemeral.\n        bool    schema_defined;\n\n        // Number of ancestor scalar types.\n        uint16  ancestors_count;\n\n        // Indexes of ancestor scalar type descriptors\n        // in ancestor resolution order (C3).\n        uint16  ancestors[ancestors_count];\n\n        // Range type descriptor index.\n        uint16  type;\n    };\n\nRanges are encoded on the wire as :ref:`ranges <ref_protocol_fmt_range>`.\n\n\nObject Type Descriptor\n======================\n\n.. code-block:: c\n\n    struct ObjectTypeDescriptor {\n        // Indicates that this is an\n        // object type descriptor.\n        uint8   tag = 10;\n\n        // Schema type ID.\n        uuid    id;\n\n        // Schema type name (can be empty for ephemeral free object types).\n        string  name;\n\n        // Whether the type is defined in the schema\n        // or is ephemeral.\n        bool    schema_defined;\n    };\n\n\nCompound Type Descriptor\n========================\n\n.. code-block:: c\n\n    struct CompoundTypeDescriptor {\n        // Indicates that this is a\n        // compound type descriptor.\n        uint8                 tag = 11;\n\n        // Schema type ID.\n        uuid                  id;\n\n        // Schema type name.\n        string                name;\n\n        // Whether the type is defined in the schema\n        // or is ephemeral.\n        bool                  schema_defined;\n\n        // Compound type operation, see TypeOperation below.\n        uint8<TypeOperation>  op;\n\n        // Number of compound type components.\n        uint16                component_count;\n\n        // Compound type component type descriptor indexes.\n        uint16                components[component_count];\n    };\n\n    enum TypeOperation {\n        // Foo | Bar\n        UNION         = 1;\n\n        // Foo & Bar\n        INTERSECTION  = 2;\n    };\n\n\nObject Output Shape Descriptor\n==============================\n\n.. code-block:: c\n\n    struct ObjectShapeDescriptor {\n        // Indicates that this is an\n        // Object Shape descriptor.\n        uint8         tag = 1;\n\n        // Descriptor ID.\n        uuid          id;\n\n        // Whether is is an ephemeral free shape,\n        // if true, then `type` would always be 0\n        // and should not be interpreted.\n        bool          ephemeral_free_shape;\n\n        // Object type descriptor index.\n        uint16        type;\n\n        // Number of elements in shape.\n        uint16        element_count;\n\n        // Array of shape elements.\n        ShapeElement  elements[element_count];\n    };\n\n    struct ShapeElement {\n        // Field flags:\n        //   1 << 0: the field is implicit\n        //   1 << 1: the field is a link property\n        //   1 << 2: the field is a link\n        uint32              flags;\n\n        // The cardinality of the shape element.\n        uint8<Cardinality>  cardinality;\n\n        // Element name.\n        string              name;\n\n        // Element type descriptor index.\n        uint16              type;\n\n        // Source schema type descriptor index\n        // (useful for polymorphic queries).\n        uint16              source_type;\n    };\n\n.. eql:struct:: edb.protocol.enums.Cardinality\n\nObjects are encoded on the wire as :ref:`tuples <ref_protocol_fmt_tuple>`.\n\n\nInput Shape Descriptor\n======================\n\n.. code-block:: c\n\n    struct InputShapeDescriptor {\n        // Indicates that this is an\n        // Object Shape descriptor.\n        uint8              tag = 8;\n\n        // Descriptor ID.\n        uuid               id;\n\n        // Number of elements in shape.\n        uint16             element_count;\n\n        // Shape elements.\n        InputShapeElement  elements[element_count];\n    };\n\n    struct InputShapeElement {\n        // Field flags, currently always zero.\n        uint32              flags;\n\n        // The cardinality of the shape element.\n        uint8<Cardinality>  cardinality;\n\n        // Element name.\n        string              name;\n\n        // Element type descriptor index.\n        uint16              type;\n    };\n\nInput objects are encoded on the wire as\n:ref:`sparse objects <ref_protocol_fmt_sparse_obj>`.\n\n\nType Annotation Text Descriptor\n===============================\n\n.. code-block:: c\n\n    struct TypeAnnotationDescriptor {\n        // Indicates that this is an\n        // Type Annotation descriptor.\n        uint8   tag = 127;\n\n        // Index of the descriptor the\n        // annotation is for.\n        uint16  descriptor;\n\n        // Annotation key.\n        string  key;\n\n        // Annotation value.\n        string  value;\n    };\n\n\nSQL Record Descriptor\n=====================\n\n.. code-block:: c\n\n    struct SQLRecordDescriptor {\n        // Indicates that this is a\n        // SQL Record descriptor.\n        uint8         tag = 13;\n\n        // Descriptor ID.\n        uuid          id;\n\n        // Number of elements in record.\n        uint16        element_count;\n\n        // Array of shape elements.\n        SQLRecordElement  elements[element_count];\n    };\n\n    struct SQLRecordElement {\n        // Element name.\n        string              name;\n\n        // Element type descriptor index.\n        uint16              type;\n    };\n"
  },
  {
    "path": "docs/resources/upgrading.rst",
    "content": ".. _ref_upgrading:\n\n=================\nUpgrading from v5\n=================\n\nWith the release of Gel v6, we have introduced a number of changes that affect your workflow. The most obvious change is that the CLI and client libraries are now named after Gel, not EdgeDB. But, there are a number of other smaller changes and enhancements that are worth understanding as you bring your EdgeDB database up-to-date with the latest release.\n\nCLI\n===\n\n.. lint-off\n\nFor a few versions we've been shipping an alias to the ``edgedb`` CLI named ``gel`` as we've been working on the rename. For the most part, you can now just use ``gel`` instead of ``edgedb`` as the CLI name. Make sure you are using the latest version of the CLI by running :gelcmd:`cli upgrade`. If you see a note about not being able to upgrade, you can try running ``edgedb cli upgrade`` and then after that :gelcmd:`cli upgrade`.\n\nDon't forget to update any scripts that use the ``edgedb`` CLI to use ``gel`` instead.\n\n.. lint-on\n\nProject Configuration File\n==========================\n\n.. lint-off\n\nGel CLI and client libraries use a configuration file configure various things about your project, such as the location of the schema directory, and the target version of the Gel server. Previously, this file was named ``edgedb.toml``, but it is now named |gel.toml|.\n\n.. lint-on\n\nIn addition to the name change, we have also renamed the TOML table for configuring the server version from ``[edgedb]`` to ``[instance]``.\n\n.. tabs::\n\n  .. code-tab:: toml\n    :caption: (Before) edgedb.toml\n\n    [edgedb]\n    server-version = \"5.7\"\n\n  .. code-tab:: toml-diff\n    :caption: (After) gel.toml\n\n    - [edgedb]\n    + [instance]\n      server-version = \"5.7\"\n\nWe continue to support the old file and table name, but we recommend updating to the new name as soon as possible.\n\nThere are also a number of useful new workflow features in the CLI that are configured in this file that are worth exploring as well. See the `announcement blog post <https://www.geldata.com/blog/gel-s-new-edgeql-features-and-cli-workflows>`_ for more details.\n\nClient Libraries\n================\n\nWe've started publishing our various client libraries under Gel-flavored names, and will only be publishing to these new packages going forward.\n\n.. list-table::\n  :header-rows: 1\n\n  * - Language\n    - New Package\n  * - Python\n    - `gel on PyPI <https://pypi.org/project/gel/>`_\n  * - TypeScript\n    - `gel on npm <https://www.npmjs.com/package/gel>`_\n  * - Go\n    - `gel-go on GitHub <https://github.com/geldata/gel-go>`_\n  * - Rust\n    - `gel-rust on GitHub <https://github.com/geldata/gel-rust>`_\n\nIf you're using the TypeScript client library, you can use our codemod to automatically update your codebase to point at the new packages:\n\n.. code-block:: bash\n\n  $ npx @gel/codemod@latest\n\nCode generation\n===============\n\nSome of the languages we support include code generation tools that can generate code from your schema. Here is a table of how those tools have been renamed:\n\n.. list-table::\n  :header-rows: 1\n\n  * - Language\n    - Previous\n    - Current\n  * - Python\n    - ``edgedb-py``\n    - ``gel-py``\n  * - TypeScript\n    - ``@edgedb/generate``\n    - ``@gel/generate``\n\nCheck your project task runners and update them accordingly.\n\nUpgrading instances\n===================\n\nTo take advantage of the new features in Gel v6, you'll need to upgrade your instances to the latest version.\n\nCloud instances\n---------------\n\nIf you're using a hosted instance on Gel Cloud, you can upgrade your instance by clicking on the \"Upgrade\" button in the Gel Cloud console, or with the CLI.\n\n.. code-block:: bash\n\n  $ gel instance upgrade <my-org/my-instance-name> --to-latest\n\nLocal instances\n---------------\n\nIf you have local instances that you've intialized with the CLI using :gelcmd:`project init`, you can upgrade them easily with the CLI.\n\n.. code-block:: bash\n\n  gel project upgrade --to-latest\n\nThis will upgrade the project instance to the latest version of Gel and also update the |gel.toml| server-version value to the latest version.\n\nRemote instances\n----------------\n\nTo upgrade a remote instance, we recommend the following dump-and-restore process:\n\n1. Gel v6.0 supports PostgreSQL 14 or above. Verify your PostgreSQL version before upgrading Gel. If you're using Postgres 13 or below, upgrade Postgres first.\n\n2. Spin up an empty 6.0 instance. You can use one of our :ref:`deployment guides <ref_guide_deployment>`.\n\n   For Debian/Ubuntu, when adding the Gel package repository, use this command:\n\n   .. code-block:: bash\n\n       $ echo deb [signed-by=/usr/local/share/keyrings/gel-keyring.gpg] \\\n           https://packages.geldata.com/apt \\\n           $(grep \"VERSION_CODENAME=\" /etc/os-release | cut -d= -f2) main \\\n           | sudo tee /etc/apt/sources.list.d/gel.list\n       $ sudo apt-get update && sudo apt-get install gel-6\n\n   For CentOS/RHEL, use this installation command:\n\n   .. code-block:: bash\n\n       $ sudo yum install gel-6\n\n   In any required ``systemctl`` commands, replace ``edgedb-server-5`` with ``gel-server-6``.\n\n   For Docker setups, use the ``6`` or other appropriate tag.\n\n   .. note::\n\n     The new instance will have a different DSN, including a different port number. Take note of the full DSN of the new instance as you'll need it to restore your database, and update your application to use the new DSN in further steps.\n\n3. Take your application offline, then dump your v5.x database with the CLI:\n\n   .. code-block:: bash\n\n       $ gel dump --dsn <old dsn> --all --format dir my_database.dump/\n\n   This will dump the schema and contents of your current database to a directory on your local disk called ``my_database.dump``. The directory name isn't important.\n\n4. Restore to the new, empty v6 instance from the dump:\n\n   .. code-block:: bash\n\n       $ gel restore --all my_database.dump/ --dsn <new dsn>\n\n   Once the restore is complete, update your application to connect to the new instance.\n\n   This process will involve some downtime, specifically during steps 2 and 3.\n\nGitHub Action\n=============\n\nWe publish a GitHub action for accessing a Gel instance in your GitHub Actions workflows. This action has been updated to work with Gel v6. If you're using the action in your workflow, update it to use the latest version.\n\n.. code-block:: yaml-diff\n\n  - - uses: edgedb/setup-edgedb@v1\n  + - uses: geldata/setup-gel@v1\n  - - run: edgedb query 'select sys::get_version_as_str()'\n  + - run: gel query 'select sys::get_version_as_str()'\n"
  },
  {
    "path": "edb/.gitignore",
    "content": "# Ignore Cython debug files\n*.html\n\n# Grammar spec file encoded as BitCode\n*.bc\n\n# Grammar spec file encoded as EBNF\n*.ebnf\n"
  },
  {
    "path": "edb/README.md",
    "content": "## Directory overview\n\nHere is a list of most of the important directories in EdgeDB, along\nwith some of the key files and subdirectories in them.\n\nThis list is *partial*, focused on the compiler, and ordered conceptually.\n\n - `schema/` - Representation of the schema, and implementation of schema modifications (both SDL migrations and DDL statements).\n   The schema is an immutable object (implemented as a bunch of immutable maps), and making changes to it produces a new schema object.\n   Objects stored in the schema are represented in the compiler as proxy objects, and fetching attributes from them requires passing in a schema.\n\n - `edgeql/` - EdgeQL frontend tools - AST, parser, first stage compiler, etc\n   - `edgeql/ast.py` - EdgeQL AST\n   - `edgeql/parser/` - Parser. Uses https://github.com/MagicStack/parsing\n   - `edgeql/compiler/` - Compiler from EdgeQL to our IR\n   - `edgeql/tracer.py` and `edgeql/declarative.py` - Analysis to convert SDL schema descriptions to DDL that will create them.\n\n - `ir/` - Intermediate Representation (IR) and tools for operating on it\n   - `edgeql/ir/pathid.py` - Definition of \"path ids\", which are used to identify sets\n   - `edgeql/ir/ast.py` - Primary AST of intermediate representation.\n   The IR contains no direct references to schema objects; information from\n   the schema that is needed in the IR needs to be explicitly placed there.\n   There are `TypeRef` and `PointerRef` objects that do this for types and pointers.\n   - `edgeql/ir/scopetree.py` - Representation of \"scope tree\", which computes and tracks where sets are \"bound\". The IR output of the compiler consists of both an IR AST and a scope tree, needed to interpret it.\n\n - `pgsql/` - PostgreSQL backend tools - AST, codegen, second stage compiler, etc\n   - `pgsql/ast.py` - SQL AST. The AST contains both information for the actual SQL AST, along with a large collection of metadata that is used during the compilation process.\n   - `pgsql/codegen.py` - SQL codegen. Converts AST to SQL.\n   - `pgsql/compiler/` - IR to SQL compiler.\n   - `pgsql/delta.py` - Generates SQL DDL from delta trees.\n\n - `lib/` - Definition of EdgeDB's standard library\n   - `lib/schema.edgeql` - Definition of the parts of the schema that are exposed publically.\n\n - `graphql/` - GraphQL to EdgeQL compiler\n\n - `server/` - Implementation of the EdgeDB server and protocol handling\n   - `server/compiler` - The interface between the server and the compiler\n"
  },
  {
    "path": "edb/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n# DO NOT ADD ANYTHING TO THIS FILE:\n# we might want to make \"edb\" a namespace\n# package at some point.\n"
  },
  {
    "path": "edb/_edgeql_parser.pyi",
    "content": "import typing\n\nclass SyntaxError(Exception): ...\n\nclass ParserResult:\n    out: typing.Optional[CSTNode | list[OpaqueToken]]\n    errors: list[\n        tuple[\n            str,\n            tuple[int, typing.Optional[int]],\n            typing.Optional[str],\n            typing.Optional[str],\n        ]\n    ]\n\n    def pack(self) -> bytes: ...\n\nclass Hasher:\n    @staticmethod\n    def start_migration(parent_id: str) -> Hasher: ...\n    def add_source(self, data: str) -> None: ...\n    def make_migration_id(self) -> str: ...\n\nunreserved_keywords: frozenset[str]\npartial_reserved_keywords: frozenset[str]\nfuture_reserved_keywords: frozenset[str]\ncurrent_reserved_keywords: frozenset[str]\n\nclass Entry:\n    key: bytes\n\n    tokens: list[OpaqueToken]\n\n    extra_blobs: list[bytes]\n\n    first_extra: typing.Optional[int]\n\n    extra_counts: list[int]\n\n    def get_variables(self) -> dict[str, typing.Any]: ...\n    def pack(self) -> bytes: ...\n\ndef normalize(text: str) -> Entry: ...\ndef parse(\n    start_token_name: str, tokens: list[OpaqueToken]\n) -> tuple[\n    ParserResult, list[tuple[type, typing.Callable]]\n]: ...\ndef suggest_next_keywords(\n    start_token_name: str, tokens: list[OpaqueToken]\n) -> tuple[list[str], bool]: ...\ndef preload_spec(spec_filepath: str) -> None: ...\ndef save_spec(spec_json: str, dst: str) -> None: ...\n\nclass CSTNode:\n    production: typing.Optional[Production]\n    terminal: typing.Optional[Terminal]\n\nclass Production:\n    id: int\n    args: list[CSTNode]\n    start: int | None\n    end: int | None\n\nclass Terminal:\n    text: str\n    value: typing.Any\n    start: int\n    end: int\n\nclass SourcePoint:\n    line: int\n    zero_based_line: int\n    column: int\n    utf16column: int\n    offset: int\n    char_offset: int\n\n    @staticmethod\n    def from_offsets(\n        data: bytes, offsets: list[int]\n    ) -> list[SourcePoint]: ...\n\n    @staticmethod\n    def from_lines_cols(\n        data: bytes, lines_cols: list[tuple[int, int]]\n    ) -> list[SourcePoint]: ...\n\ndef offset_of_line(text: str, target: int) -> int: ...\n\nclass OpaqueToken:\n\n    def span_start(self) -> int: ...\n    def span_end(self) -> int: ...\n    def is_ident(self) -> bool: ...\n\ndef tokenize(s: str) -> ParserResult: ...\ndef unpickle_token(bytes: bytes) -> OpaqueToken: ...\ndef unpack(serialized: bytes) -> Entry | list[OpaqueToken]: ...\n"
  },
  {
    "path": "edb/api/errors.txt",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n####\n\n0x_01_00_00_00   InternalServerError\n\n####\n\n0x_02_00_00_00   UnsupportedFeatureError\n\n\n####\n\n0x_03_00_00_00   ProtocolError\n\n0x_03_01_00_00   BinaryProtocolError\n0x_03_01_00_01   UnsupportedProtocolVersionError\n0x_03_01_00_02   TypeSpecNotFoundError\n0x_03_01_00_03   UnexpectedMessageError\n\n0x_03_02_00_00   InputDataError\n0x_03_02_01_00   ParameterTypeMismatchError\n0x_03_02_02_00   StateMismatchError  #SHOULD_RETRY\n\n0x_03_03_00_00   ResultCardinalityMismatchError\n\n0x_03_04_00_00   CapabilityError\n0x_03_04_01_00   UnsupportedCapabilityError\n0x_03_04_02_00   DisabledCapabilityError\n0x_03_04_03_00   UnsafeIsolationLevelError\n\n\n####\n\n0x_04_00_00_00   QueryError\n\n0x_04_01_00_00   InvalidSyntaxError\n0x_04_01_01_00   EdgeQLSyntaxError\n0x_04_01_02_00   SchemaSyntaxError\n0x_04_01_03_00   GraphQLSyntaxError\n\n0x_04_02_00_00   InvalidTypeError\n0x_04_02_01_00   InvalidTargetError\n0x_04_02_01_01   InvalidLinkTargetError\n0x_04_02_01_02   InvalidPropertyTargetError\n\n0x_04_03_00_00   InvalidReferenceError\n0x_04_03_00_01   UnknownModuleError\n0x_04_03_00_02   UnknownLinkError\n0x_04_03_00_03   UnknownPropertyError\n0x_04_03_00_04   UnknownUserError\n0x_04_03_00_05   UnknownDatabaseError\n0x_04_03_00_06   UnknownParameterError\n0x_04_03_00_07   DeprecatedScopingError\n\n0x_04_04_00_00   SchemaError\n\n0x_04_05_00_00   SchemaDefinitionError\n\n0x_04_05_01_00   InvalidDefinitionError\n0x_04_05_01_01   InvalidModuleDefinitionError\n0x_04_05_01_02   InvalidLinkDefinitionError\n0x_04_05_01_03   InvalidPropertyDefinitionError\n0x_04_05_01_04   InvalidUserDefinitionError\n0x_04_05_01_05   InvalidDatabaseDefinitionError\n0x_04_05_01_06   InvalidOperatorDefinitionError\n0x_04_05_01_07   InvalidAliasDefinitionError\n0x_04_05_01_08   InvalidFunctionDefinitionError\n0x_04_05_01_09   InvalidConstraintDefinitionError\n0x_04_05_01_0A   InvalidCastDefinitionError\n\n0x_04_05_02_00   DuplicateDefinitionError\n0x_04_05_02_01   DuplicateModuleDefinitionError\n0x_04_05_02_02   DuplicateLinkDefinitionError\n0x_04_05_02_03   DuplicatePropertyDefinitionError\n0x_04_05_02_04   DuplicateUserDefinitionError\n0x_04_05_02_05   DuplicateDatabaseDefinitionError\n0x_04_05_02_06   DuplicateOperatorDefinitionError\n0x_04_05_02_07   DuplicateViewDefinitionError\n0x_04_05_02_08   DuplicateFunctionDefinitionError\n0x_04_05_02_09   DuplicateConstraintDefinitionError\n0x_04_05_02_0A   DuplicateCastDefinitionError\n0x_04_05_02_0B   DuplicateMigrationError\n\n####\n\n0x_04_06_00_00   SessionTimeoutError\n\n0x_04_06_01_00   IdleSessionTimeoutError #SHOULD_RETRY\n\n0x_04_06_02_00   QueryTimeoutError\n\n0x_04_06_0A_00   TransactionTimeoutError\n0x_04_06_0A_01   IdleTransactionTimeoutError\n\n####\n\n0x_05_00_00_00   ExecutionError\n\n0x_05_01_00_00   InvalidValueError\n0x_05_01_00_01   DivisionByZeroError\n0x_05_01_00_02   NumericOutOfRangeError\n0x_05_01_00_03   AccessPolicyError\n0x_05_01_00_04   QueryAssertionError\n\n0x_05_02_00_00   IntegrityError\n0x_05_02_00_01   ConstraintViolationError\n0x_05_02_00_02   CardinalityViolationError\n0x_05_02_00_03   MissingRequiredError\n\n0x_05_03_00_00   TransactionError\n0x_05_03_01_00   TransactionConflictError  #SHOULD_RETRY\n0x_05_03_01_01   TransactionSerializationError\n0x_05_03_01_02   TransactionDeadlockError\n0x_05_03_01_03   QueryCacheInvalidationError\n\n0x_05_04_00_00   WatchError\n\n\n####\n\n0x_06_00_00_00   ConfigurationError\n\n\n####\n\n0x_07_00_00_00   AccessError\n\n0x_07_01_00_00   AuthenticationError\n\n\n####\n\n0x_08_00_00_00   AvailabilityError\n\n0x_08_00_00_01   BackendUnavailableError  #SHOULD_RETRY\n0x_08_00_00_02   ServerOfflineError  #SHOULD_RECONNECT #SHOULD_RETRY\n0x_08_00_00_03   UnknownTenantError  #SHOULD_RECONNECT #SHOULD_RETRY\n0x_08_00_00_04   ServerBlockedError\n\n####\n\n0x_09_00_00_00   BackendError\n\n0x_09_00_01_00   UnsupportedBackendFeatureError\n\n\n####\n\n0x_F0_00_00_00   LogMessage\n\n0x_F0_01_00_00   WarningMessage\n\n0x_F0_02_00_00   StatusMessage\n0x_F0_02_00_01   MigrationStatusMessage\n\n\n#### Suggested errors for Gel clients\n\n0x_FF_00_00_00   ClientError\n0x_FF_01_00_00   ClientConnectionError\n0x_FF_01_01_00   ClientConnectionFailedError\n0x_FF_01_01_01   ClientConnectionFailedTemporarilyError  #SHOULD_RECONNECT #SHOULD_RETRY\n0x_FF_01_02_00   ClientConnectionTimeoutError  #SHOULD_RECONNECT #SHOULD_RETRY\n0x_FF_01_03_00   ClientConnectionClosedError  #SHOULD_RECONNECT #SHOULD_RETRY\n\n0x_FF_02_00_00   InterfaceError\n\n0x_FF_02_01_00   QueryArgumentError\n0x_FF_02_01_01   MissingArgumentError\n0x_FF_02_01_02   UnknownArgumentError\n0x_FF_02_01_03   InvalidArgumentError\n\n0x_FF_03_00_00   NoDataError\n\n0x_FF_04_00_00   InternalClientError\n"
  },
  {
    "path": "edb/api/types.txt",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# Use `edb gen-types` to regenerate `edb/schema/_types.py` based on this file.\n\n# Base Scalar Types\n\n00000000-0000-0000-0000-000000000001 anytype\n00000000-0000-0000-0000-000000000002 anytuple\n00000000-0000-0000-0000-000000000003 anyobject\n\n00000000-0000-0000-0000-0000000000F0 std\n00000000-0000-0000-0000-0000000000FF empty-tuple\n\n00000000-0000-0000-0000-000000000100 std::uuid\n00000000-0000-0000-0000-000000000101 std::str\n00000000-0000-0000-0000-000000000102 std::bytes\n00000000-0000-0000-0000-000000000103 std::int16\n00000000-0000-0000-0000-000000000104 std::int32\n00000000-0000-0000-0000-000000000105 std::int64\n00000000-0000-0000-0000-000000000106 std::float32\n00000000-0000-0000-0000-000000000107 std::float64\n00000000-0000-0000-0000-000000000108 std::decimal\n00000000-0000-0000-0000-000000000109 std::bool\n00000000-0000-0000-0000-00000000010A std::datetime\n00000000-0000-0000-0000-00000000010E std::duration\n00000000-0000-0000-0000-00000000010F std::json\n00000000-0000-0000-0000-000000000110 std::bigint\n\n00000000-0000-0000-0000-00000000010B std::cal::local_datetime\n00000000-0000-0000-0000-00000000010C std::cal::local_date\n00000000-0000-0000-0000-00000000010D std::cal::local_time\n00000000-0000-0000-0000-000000000111 std::cal::relative_duration\n00000000-0000-0000-0000-000000000112 std::cal::date_duration\n\n00000000-0000-0000-0000-000000000130 cfg::memory\n\n00000000-0000-0000-0000-000001000001 std::pg::json\n00000000-0000-0000-0000-000001000002 std::pg::timestamptz\n00000000-0000-0000-0000-000001000003 std::pg::timestamp\n00000000-0000-0000-0000-000001000004 std::pg::date\n00000000-0000-0000-0000-000001000005 std::pg::interval\n"
  },
  {
    "path": "edb/buildmeta.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    Mapping,\n    Sequence,\n    NamedTuple,\n    TypedDict,\n    cast,\n)\n\n# DO NOT put any imports here other than from stdlib\n# or modules from edb.common that themselves have only stdlib imports.\n\nimport base64\nimport datetime\nimport hashlib\nimport importlib.util\nimport json\nimport logging\nimport os\nimport pathlib\nimport pickle\nimport platform\nimport re\nimport subprocess\nimport sys\nimport tempfile\n\nfrom edb.common import debug\nfrom edb.common import devmode\nfrom edb.common import verutils\n\n\n# Increment this whenever the database layout or stdlib changes.\n#\n# WARNING: DO NOT INCREMENT THIS WHEN BACKPORTING CHANGES TO A RELEASE BRANCH.\n# The merge conflict there is a nice reminder that you probably need\n# to write a patch in edb/pgsql/patches.py, and then you should preserve\n# the old value.\nEDGEDB_CATALOG_VERSION = 2025_11_03_00_00\nEDGEDB_MAJOR_VERSION = 8\n\n\nclass MetadataError(Exception):\n    pass\n\n\nclass BackendVersion(NamedTuple):\n    major: int\n    minor: int\n    micro: int\n    releaselevel: str\n    serial: int\n    string: str\n\n\nclass VersionMetadata(TypedDict):\n    build_date: datetime.datetime | None\n    build_hash: str | None\n    scm_revision: str | None\n    source_date: datetime.datetime | None\n    target: str | None\n\n\ndef get_build_metadata_value(prop: str) -> str:\n    env_val = os.environ.get(f'_GEL_BUILDMETA_{prop}')\n    if env_val:\n        return env_val\n    env_val = os.environ.get(f'_EDGEDB_BUILDMETA_{prop}')\n    if env_val:\n        return env_val\n\n    try:\n        from . import _buildmeta  # type: ignore\n        return getattr(_buildmeta, prop)\n    except (ImportError, AttributeError):\n        raise MetadataError(\n            f'could not find {prop} in Gel distribution metadata') from None\n\n\ndef _get_devmode_pg_config_path() -> pathlib.Path:\n    root = pathlib.Path(__file__).parent.parent.resolve()\n    pg_config = root / 'build' / 'postgres' / 'install' / 'bin' / 'pg_config'\n    if not pg_config.is_file():\n        try:\n            pg_config = pathlib.Path(\n                get_build_metadata_value('PG_CONFIG_PATH'))\n        except MetadataError:\n            pass\n\n    if not pg_config.is_file():\n        raise MetadataError('DEV mode: Could not find PostgreSQL build, '\n                            'run `pip install -e .`')\n\n    return pg_config\n\n\ndef get_pg_config_path() -> pathlib.Path:\n    if devmode.is_in_dev_mode():\n        pg_config = _get_devmode_pg_config_path()\n    else:\n        try:\n            pg_config = pathlib.Path(\n                get_build_metadata_value('PG_CONFIG_PATH'))\n        except MetadataError:\n            pg_config = _get_devmode_pg_config_path()\n        else:\n            if not pg_config.is_file():\n                raise MetadataError(\n                    f'invalid pg_config path: {pg_config!r}: file does not '\n                    f'exist or is not a regular file')\n\n    return pg_config\n\n\n_pg_version_regex = re.compile(\n    r\"(Postgre[^\\s]*)?\\s*\"\n    r\"(?P<major>[0-9]+)\\.?\"\n    r\"((?P<minor>[0-9]+)\\.?)?\"\n    r\"(?P<micro>[0-9]+)?\"\n    r\"(?P<releaselevel>[a-z]+)?\"\n    r\"(?P<serial>[0-9]+)?\"\n)\n\n\ndef parse_pg_version(version_string: str) -> BackendVersion:\n    version_match = _pg_version_regex.search(version_string)\n    if version_match is None:\n        raise ValueError(\n            f\"malformed Postgres version string: {version_string!r}\")\n    version = version_match.groupdict()\n    return BackendVersion(\n        major=int(version[\"major\"]),\n        minor=0,\n        micro=int(version.get(\"minor\") or 0),\n        releaselevel=version.get(\"releaselevel\") or \"final\",\n        serial=int(version.get(\"serial\") or 0),\n        string=version_string,\n    )\n\n\n_bundled_pg_version: Optional[BackendVersion] = None\n\n\ndef get_pg_version() -> BackendVersion:\n    global _bundled_pg_version\n    if _bundled_pg_version is not None:\n        return _bundled_pg_version\n\n    pg_config = subprocess.run(\n        [get_pg_config_path()],\n        capture_output=True,\n        text=True,\n        check=True,\n    )\n\n    for line in pg_config.stdout.splitlines():\n        k, eq, v = line.partition('=')\n        if eq and k.strip().lower() == 'version':\n            v = v.strip()\n            parsed_ver = parse_pg_version(v)\n            _bundled_pg_version = BackendVersion(\n                major=parsed_ver.major,\n                minor=parsed_ver.minor,\n                micro=parsed_ver.micro,\n                releaselevel=parsed_ver.releaselevel,\n                serial=parsed_ver.serial,\n                string=v,\n            )\n            return _bundled_pg_version\n    else:\n        raise MetadataError(\n            \"could not find version information in pg_config output\")\n\n\ndef get_runstate_path(data_dir: pathlib.Path) -> pathlib.Path:\n    if devmode.is_in_dev_mode():\n        return data_dir\n    else:\n        runstate_dir = get_build_metadata_value('RUNSTATE_DIR')\n        if runstate_dir is not None:\n            return pathlib.Path(runstate_dir)\n        else:\n            return data_dir\n\n\ndef get_shared_data_dir_path() -> pathlib.Path:\n    if devmode.is_in_dev_mode():\n        return devmode.get_dev_mode_cache_dir()  # type: ignore[return-value]\n    else:\n        return pathlib.Path(get_build_metadata_value('SHARED_DATA_DIR'))\n\n\ndef get_extension_dir_path() -> pathlib.Path:\n    # TODO: Do we want a special metadata value??\n    return get_shared_data_dir_path() / \"extensions\"\n\n\ndef hash_dirs(\n    dirs: Sequence[tuple[str, str]],\n    *,\n    extra_files: Optional[Sequence[str | pathlib.Path]]=None,\n    extra_data: Optional[bytes] = None,\n) -> bytes:\n    def hash_dir(dirname, ext, paths):\n        with os.scandir(dirname) as it:\n            for entry in it:\n                if entry.is_file() and entry.name.endswith(ext):\n                    paths.append(entry.path)\n                elif entry.is_dir():\n                    hash_dir(entry.path, ext, paths)\n\n    paths: list[str] = []\n    for dirname, ext in dirs:\n        hash_dir(dirname, ext, paths)\n\n    if extra_files:\n        for extra_file in extra_files:\n            if isinstance(extra_file, pathlib.Path):\n                extra_file = str(extra_file.resolve())\n            paths.append(extra_file)\n\n    h = hashlib.sha1()  # sha1 is the fastest one.\n    for path in sorted(paths):\n        with open(path, 'rb') as f:\n            h.update(f.read())\n    h.update(str(sys.version_info[:2]).encode())\n    if extra_data is not None:\n        h.update(extra_data)\n    return h.digest()\n\n\ndef read_data_cache(\n    cache_key: bytes,\n    path: str,\n    *,\n    pickled: bool=True,\n    source_dir: Optional[pathlib.Path] = None,\n) -> Any:\n    if source_dir is None:\n        source_dir = get_shared_data_dir_path()\n    full_path = source_dir / path\n\n    if full_path.exists():\n        with open(full_path, 'rb') as f:\n            src_hash = f.read(len(cache_key))\n            if src_hash == cache_key or debug.flags.bootstrap_cache_yolo:\n                if pickled:\n                    data = f.read()\n                    try:\n                        return pickle.loads(data)\n                    except Exception:\n                        logging.exception(f'could not unpickle {path}')\n                else:\n                    return f.read()\n\n\ndef write_data_cache(\n    obj: Any,\n    cache_key: bytes,\n    path: str,\n    *,\n    pickled: bool = True,\n    target_dir: Optional[pathlib.Path] = None,\n):\n    if target_dir is None:\n        target_dir = get_shared_data_dir_path()\n    full_path = target_dir / path\n\n    try:\n        with tempfile.NamedTemporaryFile(\n                mode='wb', dir=full_path.parent, delete=False) as f:\n            f.write(cache_key)\n            if pickled:\n                pickle.dump(obj, file=f, protocol=pickle.HIGHEST_PROTOCOL)\n            else:\n                f.write(obj)\n    except Exception:\n        try:\n            os.unlink(f.name)\n        except OSError:\n            pass\n        finally:\n            raise\n    else:\n        os.rename(f.name, full_path)\n\n\ndef get_version() -> verutils.Version:\n    if devmode.is_in_dev_mode():\n        root = pathlib.Path(__file__).parent.parent.resolve()\n        version = verutils.parse_version(get_version_from_scm(root))\n    else:\n        vertuple: list[Any] = list(get_build_metadata_value('VERSION'))\n        vertuple[2] = verutils.VersionStage(vertuple[2])\n        version = verutils.Version(*vertuple)\n\n    return version\n\n\n_version_dict: Optional[Mapping[str, Any]] = None\n\n\ndef get_version_build_id(\n    v: verutils.Version,\n    short: bool = True,\n) -> tuple[str, ...]:\n    parts = []\n    if v.local:\n        if short:\n            build_hash = None\n            build_kind = None\n            for segment in v.local:\n                if segment[0] == \"s\":\n                    build_hash = segment[1:]\n                elif segment[0] == \"b\":\n                    build_kind = segment[1:]\n\n            if build_kind == \"official\":\n                if build_hash:\n                    parts.append(build_hash)\n            elif build_kind:\n                parts.append(build_kind)\n        else:\n            parts.extend(v.local)\n\n    return tuple(parts)\n\n\ndef get_version_dict() -> Mapping[str, Any]:\n    global _version_dict\n\n    if _version_dict is None:\n        ver = get_version()\n        _version_dict = {\n            'major': ver.major,\n            'minor': ver.minor,\n            'stage': ver.stage.name.lower(),\n            'stage_no': ver.stage_no,\n            'local': get_version_build_id(ver),\n        }\n\n    return _version_dict\n\n\n_version_json: Optional[str] = None\n\n\ndef get_version_json() -> str:\n    global _version_json\n    if _version_json is None:\n        _version_json = json.dumps(get_version_dict())\n    return _version_json\n\n\ndef get_version_string(short: bool = True) -> str:\n    v = get_version()\n    string = f'{v.major}.{v.minor}'\n    if v.stage is not verutils.VersionStage.FINAL:\n        string += f'-{v.stage.name.lower()}.{v.stage_no}'\n    build_id = get_version_build_id(v, short=short)\n    if build_id:\n        string += \"+\" + \".\".join(build_id)\n    return string\n\n\ndef get_version_metadata() -> VersionMetadata:\n    v = get_version()\n    pfx_map = {\n        \"b\": \"build_type\",\n        \"r\": \"build_date\",\n        \"s\": \"build_hash\",\n        \"g\": \"scm_revision\",\n        \"d\": \"source_date\",\n        \"t\": \"target\",\n    }\n\n    result = {}\n\n    for segment in v.local:\n        key = pfx_map.get(segment[0])\n        if key:\n            raw_val = segment[1:]\n            val: str | datetime.datetime\n            if key == \"target\":\n                val = _decode_build_target(raw_val)\n            elif key in {\"build_date\", \"source_date\"}:\n                val = _decode_build_date(raw_val)\n            else:\n                val = raw_val\n\n            result[key] = val\n\n    return cast(VersionMetadata, result)\n\n\ndef _decode_build_target(val: str) -> str:\n    return (\n        base64.b32decode(val + \"=\" * (-len(val) % 8), casefold=True).decode()\n    )\n\n\ndef _decode_build_date(val: str) -> datetime.datetime:\n    return datetime.datetime.strptime(val, r\"%Y%m%d%H%M\").replace(\n        tzinfo=datetime.timezone.utc)\n\n\ndef get_version_from_scm(root: pathlib.Path) -> str:\n    pretend = os.environ.get('SETUPTOOLS_SCM_PRETEND_VERSION')\n    if pretend:\n        return pretend\n\n    posint = r'(0|[1-9]\\d*)'\n    pep440_version_re = re.compile(\n        rf\"\"\"\n        ^\n        (?P<major>{posint})\n        \\.\n        (?P<minor>{posint})\n        (\n            \\.\n            (?P<micro>{posint})\n        )?\n        (\n            (?P<prekind>a|b|rc)\n            (?P<preval>{posint})\n        )?\n        $\n        \"\"\",\n        re.X,\n    )\n\n    proc = subprocess.run(\n        ['git', 'tag', '--list', 'v*'],\n        stdout=subprocess.PIPE,\n        universal_newlines=True,\n        check=True,\n        cwd=root,\n    )\n    all_tags = {\n        v[1:]\n        for v in proc.stdout.strip().split('\\n')\n        if pep440_version_re.match(v[1:])\n    }\n\n    proc = subprocess.run(\n        ['git', 'tag', '--points-at', 'HEAD'],\n        stdout=subprocess.PIPE,\n        universal_newlines=True,\n        check=True,\n        cwd=root,\n    )\n    head_tags = {\n        v[1:]\n        for v in proc.stdout.strip().split('\\n')\n        if pep440_version_re.match(v[1:])\n    }\n\n    if all_tags & head_tags:\n        tag = max(head_tags)\n    else:\n        tag = max(all_tags)\n\n    m = pep440_version_re.match(tag)\n    assert m is not None\n    major = EDGEDB_MAJOR_VERSION\n    minor = m.group('minor')\n    micro = m.group('micro') or ''\n    microkind = '.' if micro else ''\n    prekind = m.group('prekind') or ''\n    preval = m.group('preval') or ''\n\n    if os.environ.get(\"EDGEDB_BUILD_IS_RELEASE\"):\n        # Release build.\n        ver = f'{major}.{minor}{microkind}{micro}{prekind}{preval}'\n    else:\n        # Dev/nightly build.\n        microkind = ''\n        micro = ''\n        minor = '0'\n\n        incremented_ver = f'{major}.{minor}{microkind}{micro}'\n\n        proc = subprocess.run(\n            ['git', 'rev-list', '--count', 'HEAD'],\n            stdout=subprocess.PIPE,\n            universal_newlines=True,\n            check=True,\n            cwd=root,\n        )\n        commits_on_branch = proc.stdout.strip()\n        ver = f'{incremented_ver}.dev{commits_on_branch}'\n\n    proc = subprocess.run(\n        ['git', 'rev-parse', '--verify', '--quiet', 'HEAD^{commit}'],\n        stdout=subprocess.PIPE,\n        universal_newlines=True,\n        check=True,\n        cwd=root,\n    )\n    commitish = proc.stdout.strip()\n\n    env = dict(os.environ)\n    env['TZ'] = 'UTC'\n    proc = subprocess.run(\n        ['git', 'show', '-s', '--format=%cd',\n         '--date=format-local:%Y%m%d%H', commitish],\n        stdout=subprocess.PIPE,\n        universal_newlines=True,\n        check=True,\n        cwd=root,\n        env=env,\n    )\n    rev_date = proc.stdout.strip()\n\n    catver = EDGEDB_CATALOG_VERSION\n\n    full_version = f'{ver}+d{rev_date}.g{commitish[:9]}.cv{catver}'\n\n    build_target = os.environ.get(\"EDGEDB_BUILD_TARGET\")\n    if build_target:\n        # Check that build target is encoded correctly\n        _decode_build_target(build_target)\n    else:\n        plat = sys.platform\n        if plat == \"win32\":\n            plat = \"windows\"\n        ident = [\n            platform.machine(),\n            \"pc\" if plat == \"windows\" else\n            \"apple\" if plat == \"darwin\" else\n            \"unknown\",\n            plat,\n        ]\n        if hasattr(platform, \"libc_ver\"):\n            libc, _ = platform.libc_ver()\n            if libc == \"glibc\":\n                ident.append(\"gnu\")\n            elif libc == \"musl\":\n                ident.append(\"musl\")\n        build_target = base64.b32encode(\n            \"-\".join(ident).encode()).decode().rstrip(\"=\").lower()\n    build_date = os.environ.get(\"EDGEDB_BUILD_DATE\")\n    if build_date:\n        # Validate\n        _decode_build_date(build_date)\n    else:\n        now = datetime.datetime.now(tz=datetime.timezone.utc)\n        build_date = now.strftime(r\"%Y%m%d%H%M\")\n    version_line = f'{full_version}.r{build_date}.t{build_target}'\n    if not os.environ.get(\"EDGEDB_BUILD_OFFICIAL\"):\n        build_type = \"local\"\n    else:\n        build_type = \"official\"\n    version_line += f'.b{build_type}'\n    version_hash = hashlib.sha256(version_line.encode(\"utf-8\")).hexdigest()\n    full_version = f\"{version_line}.s{version_hash[:7]}\"\n\n    return full_version\n\n\ndef get_cache_src_dirs():\n    find_spec = importlib.util.find_spec\n\n    edgeql = pathlib.Path(find_spec('edb.edgeql').origin).parent\n    return (\n        (pathlib.Path(find_spec('edb.schema').origin).parent, '.py'),\n        (edgeql / 'compiler', '.py'),\n        (edgeql / 'parser', '.py'),\n        (pathlib.Path(find_spec('edb.lib').origin).parent, '.edgeql'),\n        (pathlib.Path(find_spec('edb.pgsql.metaschema').origin).parent, '.py'),\n    )\n\n\ndef get_default_tenant_id() -> str:\n    return 'E'\n\n\ndef get_version_line() -> str:\n    ver_meta = get_version_metadata()\n\n    extras = []\n    source = \"\"\n    if build_date := ver_meta[\"build_date\"]:\n        nice_date = build_date.strftime(\"%Y-%m-%dT%H:%MZ\")\n        source += f\" on {nice_date}\"\n    if ver_meta[\"scm_revision\"]:\n        source += f\" from revision {ver_meta['scm_revision']}\"\n        if source_date := ver_meta[\"source_date\"]:\n            nice_date = source_date.strftime(\"%Y-%m-%dT%H:%MZ\")\n            source += f\" ({nice_date})\"\n    if source:\n        extras.append(f\", built{source}\")\n    if ver_meta[\"target\"]:\n        extras.append(f\"for {ver_meta['target']}\")\n\n    return get_version_string() + \" \".join(extras)\n"
  },
  {
    "path": "edb/cli/.gitignore",
    "content": "/edgedb\n/gel"
  },
  {
    "path": "edb/cli/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Optional, NoReturn\n\nimport os\nimport sys\n\n\ndef rustcli(*, args: Optional[list[str]]=None) -> NoReturn:\n    if args is None:\n        args = [*sys.argv]\n\n    os.execvpe('gel', args, os.environ)\n"
  },
  {
    "path": "edb/cli/__main__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Stub to allow invoking builtin `edgedb` CLI as `python -m edb.cli`.\"\"\"\n\nfrom __future__ import annotations\n\nimport sys\n\nfrom edb import cli\n\n\nif __name__ == '__main__':\n    sys.exit(cli.rustcli())\n"
  },
  {
    "path": "edb/common/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n# DO NOT ADD ANYTHING TO THIS FILE.\n# Importing packages like `edb.common.debug` must not\n# cause any side-effects.\n"
  },
  {
    "path": "edb/common/_typing_inspect.py",
    "content": "# The MIT License (MIT)\n#\n# Portions Copyright (c) 2017-2019 Ivan Levkivskyi\n# Portions Copyright (c) 2021 MagicStack Inc.\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n\n\"\"\"\nThis is a micro-implementation of a subset of `typing-inspect` API that Gel\nrelies on that only works on Python 3.9+.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport collections\nfrom types import GenericAlias, UnionType  # type: ignore\nfrom typing import _GenericAlias  # type: ignore\nfrom typing import Any, ClassVar, Generic, Optional, TypeVar, Union\n\n\n__all__ = [\n    \"is_classvar\",\n    \"is_typevar\",\n    \"is_generic_type\",\n    \"is_union_type\",\n    \"is_tuple_type\",\n    \"get_args\",\n    \"get_generic_bases\",\n    \"get_parameters\",\n    \"get_origin\",\n]\n\n\ndef is_classvar(t) -> bool:\n    return t is ClassVar or _is_genericalias(t) and t.__origin__ is ClassVar\n\n\ndef is_typevar(t) -> bool:\n    return type(t) is TypeVar\n\n\ndef is_generic_type(t) -> bool:\n    return (\n        isinstance(t, type)\n        and issubclass(t, Generic)  # type: ignore\n        or _is_genericalias(t)\n        and t.__origin__\n        not in (Union, tuple, ClassVar, collections.abc.Callable)\n    )\n\n\ndef is_union_type(t) -> bool:\n    return (\n        t is Union\n        or (_is_genericalias(t) and t.__origin__ is Union)\n        or isinstance(t, UnionType)\n    )\n\n\ndef is_tuple_type(t) -> bool:\n    return (\n        t is tuple\n        or _is_genericalias(t)\n        and t.__origin__ is tuple\n        or isinstance(t, type)\n        and issubclass(t, Generic)  # type: ignore\n        and issubclass(t, tuple)\n    )\n\n\ndef get_args(t, evaluate: bool = True) -> Any:\n    if evaluate is not None and not evaluate:\n        raise ValueError(\"evaluate can only be True in Python >= 3.7\")\n    if _is_genericalias(t) or isinstance(t, UnionType):\n        res = t.__args__\n        if (\n            get_origin(t) is collections.abc.Callable\n            and res[0] is not Ellipsis\n        ):\n            res = (list(res[:-1]), res[-1])\n        return res\n    return ()\n\n\ndef get_generic_bases(t) -> tuple[type, ...]:\n    return getattr(t, \"__orig_bases__\", ())\n\n\ndef get_parameters(t) -> tuple[TypeVar, ...]:\n    if (\n        _is_genericalias(t)\n        or isinstance(t, type)\n        and issubclass(t, Generic)  # type: ignore\n        and t is not Generic\n    ):\n        return t.__parameters__\n    else:\n        return ()\n\n\ndef get_origin(t) -> Optional[type]:\n    if _is_genericalias(t):\n        return t.__origin__ if t.__origin__ is not ClassVar else None\n    if t is Generic:\n        return Generic  # type: ignore\n    return None\n\n\ndef _is_genericalias(t) -> bool:\n    return isinstance(t, (GenericAlias, _GenericAlias))\n"
  },
  {
    "path": "edb/common/adapter.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional\n\n\nclass AdapterError(Exception):\n    pass\n\n\n_adapters: dict[Any, dict[type, Adapter]] = {}\n\n\nclass Adapter(type):\n    __edb_adaptee__: Optional[type]\n\n    def __new__[Adapter_T: Adapter](\n        mcls: type[Adapter_T],\n        name: str,\n        bases: tuple[type, ...],\n        clsdict: dict[str, Any],\n        *,\n        adapts: Optional[type] = None,\n        **kwargs: Any,\n    ) -> Adapter_T:\n        if adapts is not None:\n            bases = bases + (adapts,)\n\n        clsdict['__edb_adaptee__'] = adapts\n\n        result = super().__new__(mcls, name, bases, clsdict, **kwargs)\n\n        if adapts is not None:\n            assert issubclass(mcls, Adapter) and mcls is not Adapter\n\n            try:\n                adapters = _adapters[mcls]\n            except KeyError:\n                adapters = _adapters[mcls] = {}\n\n            assert adapts not in adapters\n            adapters[adapts] = result\n\n        return result\n\n    def __init__(\n        cls,\n        name: str,\n        bases: tuple[type, ...],\n        clsdict: dict[str, Any],\n        *,\n        adapts: Optional[type] = None,\n        **kwargs: Any,\n    ):\n        super().__init__(name, bases, clsdict, **kwargs)\n\n    @classmethod\n    def _match_adapter(\n        mcls,\n        obj: type,\n        adaptee: type,\n        adapter: Adapter,\n    ) -> Optional[Adapter]:\n        if issubclass(obj, adapter) and obj is not adapter:\n            # mypy bug below\n            return obj  # type: ignore\n        elif issubclass(obj, adaptee):\n            return adapter\n        else:\n            return None\n\n    @classmethod\n    def _get_adapter(\n        mcls,\n        reversed_mro: tuple[type, ...],\n    ) -> Optional[Adapter]:\n        adapters = _adapters.get(mcls)\n        if adapters is None:\n            return None\n\n        result = None\n        seen: set[Adapter] = set()\n        for base in reversed_mro:\n            for adaptee, adapter in adapters.items():\n                found = mcls._match_adapter(base, adaptee, adapter)\n\n                if found and found not in seen:\n                    result = found\n                    seen.add(found)\n\n        return result\n\n    @classmethod\n    def get_adapter(mcls, obj: Any) -> Optional[Adapter]:\n        mro = obj.__mro__\n\n        reversed_mro = tuple(reversed(mro))\n\n        result = mcls._get_adapter(reversed_mro)\n        if result is not None:\n            return result\n\n        for mc in mcls.__subclasses__(mcls):\n            result = mc._get_adapter(reversed_mro)\n            if result is not None:\n                return result\n\n        return None\n\n    @classmethod\n    def adapt[T](mcls, obj: T) -> T:\n        adapter = mcls.get_adapter(obj.__class__)\n        if adapter is None:\n            raise AdapterError(\n                'could not find {}.{} adapter for {}'.format(\n                    mcls.__module__, mcls.__name__, obj.__class__.__name__\n                )\n            )\n        elif adapter is not obj.__class__:  # type: ignore\n            return adapter.adapt(obj)\n        else:\n            return obj\n\n    def get_adaptee(cls) -> type:\n        adaptee = cls.__edb_adaptee__\n        if adaptee is None:\n            raise LookupError(f'adapter {cls} has no adaptee type')\n        return adaptee\n\n    def has_adaptee(cls) -> bool:\n        return cls.__edb_adaptee__ is not None\n"
  },
  {
    "path": "edb/common/assert_data_shape.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n\nimport datetime\nimport decimal\nimport math\nimport pprint\nimport uuid\nimport unittest\n\nimport edgedb\n\n\nclass bag(list):\n    \"\"\"Wrapper for list that tells assert_query_result to ignore order\"\"\"\n    def __repr__(self):\n        return f'bag({list.__repr__(self)})'\n\n\ndef sort_results(results, sort):\n    if sort is True:\n        sort = lambda x: x\n    # don't bother sorting empty things\n    if results:\n        # sort can be either a key function or a dict\n        if isinstance(sort, dict):\n            # the keys in the dict indicate the fields that\n            # actually must be sorted\n            for key, val in sort.items():\n                # '.' is a special key referring to the base object\n                if key == '.':\n                    sort_results(results, val)\n                else:\n                    if isinstance(results, list):\n                        for r in results:\n                            sort_results(r[key], val)\n                    else:\n                        sort_results(results[key], val)\n\n        else:\n            results.sort(key=sort)\n\n\ndef assert_data_shape(\n    data, shape, fail,\n    message=None, from_sql=False, rel_tol=None, abs_tol=None,\n):\n    try:\n        import asyncpg\n        from asyncpg import types as pgtypes\n    except ImportError:\n        if from_sql:\n            raise unittest.SkipTest(\n                'SQL tests skipped: asyncpg not installed')\n\n    base_fail = fail\n    rel_tol = 1e-04 if rel_tol is None else rel_tol\n    abs_tol = 1e-15 if abs_tol is None else abs_tol\n\n    def fail(msg):\n        base_fail(f'{msg}\\nshape: {shape!r}\\ndata: {data!r}')\n\n    _void = object()\n\n    def _format_path(path):\n        if path:\n            return 'PATH: ' + ''.join(str(p) for p in path)\n        else:\n            return 'PATH: <top-level>'\n\n    def _assert_type_shape(path, data, shape):\n        if shape in (int, float):\n            if not isinstance(data, shape):\n                fail(\n                    f'{message}: expected {shape}, got {data!r} '\n                    f'{_format_path(path)}')\n        else:\n            try:\n                shape(data)\n            except (ValueError, TypeError):\n                fail(\n                    f'{message}: expected {shape}, got {data!r} '\n                    f'{_format_path(path)}')\n\n    def _assert_dict_shape(path, data, shape):\n        if not isinstance(data, dict):\n            fail(\n                f'{message}: expected dict '\n                f'{_format_path(path)}')\n\n        # TODO: should we also check that there aren't *extra* keys\n        # (other than id, __tname__?)\n        for sk, sv in shape.items():\n            if not data or sk not in data:\n                fail(\n                    f'{message}: key {sk!r} '\n                    f'is missing\\n{pprint.pformat(data)} '\n                    f'{_format_path(path)}')\n\n            _assert_generic_shape(path + (f'[\"{sk}\"]',), data[sk], sv)\n\n    def _list_shape_iter(shape):\n        last_shape = _void\n\n        for item in shape:\n            if item is Ellipsis:\n                if last_shape is _void:\n                    raise ValueError(\n                        'invalid shape spec: Ellipsis cannot be the'\n                        'first element')\n\n                while True:\n                    yield last_shape\n\n            last_shape = item\n\n            yield item\n\n    def _assert_list_shape(path, data, shape):\n        if not isinstance(data, (list, tuple)):\n            fail(\n                f'{message}: expected list got {type(data)} '\n                f'{_format_path(path)}')\n\n        if not data and shape:\n            fail(\n                f'{message}: expected non-empty list got {type(data)} '\n                f'{_format_path(path)}')\n\n        shape_iter = _list_shape_iter(shape)\n\n        _data_count = 0\n        for _data_count, el in enumerate(data):\n            try:\n                el_shape = next(shape_iter)\n            except StopIteration:\n                fail(\n                    f'{message}: unexpected trailing elements in list '\n                    f'{_format_path(path)}')\n\n            _assert_generic_shape(\n                path + (f'[{_data_count}]',),\n                el,\n                el_shape)\n\n        if len(shape) > _data_count + 1:\n            if shape[_data_count + 1] is not Ellipsis:\n                fail(\n                    f'{message}: expecting more elements in list '\n                    f'{_format_path(path)}')\n\n    def _assert_set_shape(path, data, shape):\n        if not isinstance(data, (list, set)):\n            fail(\n                f'{message}: expected list or set '\n                f'{_format_path(path)}')\n\n        if not data and shape:\n            fail(\n                f'{message}: expected non-empty set '\n                f'{_format_path(path)}')\n\n        shape_iter = _list_shape_iter(sorted(shape))\n\n        _data_count = 0\n        for _data_count, el in enumerate(sorted(data)):\n            try:\n                el_shape = next(shape_iter)\n            except StopIteration:\n                fail(\n                    f'{message}: unexpected trailing elements in set '\n                    f'[path {_format_path(path)}]')\n\n            _assert_generic_shape(\n                path + (f'{{{_data_count}}}',), el, el_shape)\n\n        if len(shape) > _data_count + 1:\n            if Ellipsis not in shape:\n                fail(\n                    f'{message}: expecting more elements in set '\n                    f'{_format_path(path)}')\n\n    def _assert_bag_shape(path, data, shape):\n        # A bag is treated like a set except that we want it to work\n        # on objects, which can't be hashed or sorted.\n\n        if not isinstance(data, (list, set)):\n            fail(\n                f'{message}: expected list or set '\n                f'{_format_path(path)}')\n\n        if Ellipsis in shape:\n            raise ValueError(\n                f\"{message}: can't use ellipsis in set/bag shape\")\n\n        data = list(data)\n\n        if len(data) > len(shape):\n            fail(\n                f'{message}: too many elements in list '\n                f'{_format_path(path)}')\n\n        # this is all very O(n^2) but n should be small\n        for el_shape in shape:\n            for data_count, el in enumerate(data):\n                try:\n                    _assert_generic_shape(\n                        path + (f'[{data_count}]',),\n                        el,\n                        el_shape)\n                except AssertionError:\n                    # oh well\n                    pass\n                else:\n                    data.pop(data_count)\n                    break\n            else:\n                fail(\n                    f'{message}: missing elements in list '\n                    f'{_format_path(path)}: {el_shape!r}')\n\n    def _assert_generic_shape(path, data, shape):\n        if from_sql:\n            if isinstance(shape, bag):\n                return _assert_bag_shape(path, data, shape)\n            elif isinstance(shape, list):\n                # NULL is acceptable substitute for the empty set, so we'll\n                # assume that in our tests None satisfies the [] expected\n                # result.\n                if data is not None or len(shape) > 0:\n                    return _assert_list_shape(path, data, shape)\n            elif isinstance(shape, tuple):\n                assert isinstance(data, asyncpg.Record)\n                return _assert_list_shape(\n                    path, [d for d in data.values()], shape)\n            elif isinstance(shape, set):\n                return _assert_set_shape(path, data, shape)\n            elif isinstance(shape, dict):\n                assert isinstance(data, asyncpg.Record)\n\n                # If the record has \"target\" pop the \"id\" from the expected\n                # results as we expect it to be a \"target\" duplicate.\n                rec = {k: v for k, v in data.items()}\n                if 'target' in rec:\n                    if 'id' in shape and shape['id'] == shape.get('target'):\n                        shape.pop('id')\n\n                return _assert_dict_shape(path, rec, shape)\n            elif isinstance(shape, type):\n                return _assert_type_shape(path, data, shape)\n            elif isinstance(shape, float):\n                if not math.isclose(data, shape,\n                                    rel_tol=rel_tol, abs_tol=abs_tol):\n                    fail(\n                        f'{message}: not isclose({data}, {shape}) '\n                        f'{_format_path(path)}')\n            elif isinstance(shape, uuid.UUID):\n                # If data comes from SQL, we expect UUID.\n                if data != shape:\n                    fail(\n                        f'{message}: {data!r} != {shape!r} '\n                        f'{_format_path(path)}')\n            elif isinstance(shape, (str, int, bytes, datetime.timedelta,\n                                    decimal.Decimal)):\n                if data != shape:\n                    fail(\n                        f'{message}: {data!r} != {shape!r} '\n                        f'{_format_path(path)}')\n            elif isinstance(shape, edgedb.RelativeDuration):\n                if data != datetime.timedelta(\n                    days=shape.months * 30 + shape.days,\n                    microseconds=shape.microseconds,\n                ):\n                    fail(\n                        f'{message}: {data!r} != {shape!r} '\n                        f'{_format_path(path)}')\n            elif isinstance(shape, edgedb.DateDuration):\n                if data != datetime.timedelta(\n                    days=shape.months * 30 + shape.days,\n                ):\n                    fail(\n                        f'{message}: {data!r} != {shape!r} '\n                        f'{_format_path(path)}')\n            elif isinstance(shape, edgedb.Range):\n                if data != pgtypes.Range(\n                    lower=shape.lower,\n                    upper=shape.upper,\n                    lower_inc=shape.inc_lower,\n                    upper_inc=shape.inc_upper,\n                    empty=shape.is_empty(),\n                ):\n                    fail(\n                        f'{message}: {data!r} != {shape!r} '\n                        f'{_format_path(path)}')\n            elif isinstance(shape, edgedb.EnumValue):\n                if data != str(shape):\n                    fail(\n                        f'{message}: {data!r} != {shape!r} '\n                        f'{_format_path(path)}')\n            elif shape is None:\n                if data is not None:\n                    fail(\n                        f'{message}: {data!r} is expected to be None '\n                        f'{_format_path(path)}')\n            else:\n                if data != shape:\n                    fail(\n                        f'{message}: ({type(data)}) {data!r} != '\n                        f'({type(shape)}) {shape!r} '\n                        f'{_format_path(path)}')\n\n        else:\n            if isinstance(shape, bag):\n                return _assert_bag_shape(path, data, shape)\n            elif isinstance(shape, (list, tuple)):\n                return _assert_list_shape(path, data, shape)\n            elif isinstance(shape, set):\n                return _assert_set_shape(path, data, shape)\n            elif isinstance(shape, dict):\n                return _assert_dict_shape(path, data, shape)\n            elif isinstance(shape, type):\n                return _assert_type_shape(path, data, shape)\n            elif isinstance(shape, float):\n                if math.isnan(shape):\n                    if not math.isnan(shape):\n                        fail(\n                            f'NaN mismatch {_format_path(path)}'\n                        )\n                elif not math.isclose(data, shape,\n                                    rel_tol=rel_tol, abs_tol=abs_tol):\n                    fail(\n                        f'{message}: not isclose({data}, {shape}) '\n                        f'{_format_path(path)}')\n            elif isinstance(shape, uuid.UUID):\n                # We expect a str from JSON.\n                if data != str(shape):\n                    fail(\n                        f'{message}: {data!r} != {shape!r} '\n                        f'{_format_path(path)}')\n            elif isinstance(shape, (str, int, bytes, datetime.timedelta,\n                                    decimal.Decimal)):\n                if data != shape:\n                    fail(\n                        f'{message}: {data!r} != {shape!r} '\n                        f'{_format_path(path)}')\n            elif isinstance(shape, edgedb.RelativeDuration):\n                if data != shape:\n                    fail(\n                        f'{message}: {data!r} != {shape!r} '\n                        f'{_format_path(path)}')\n            elif isinstance(shape, edgedb.DateDuration):\n                if data != shape:\n                    fail(\n                        f'{message}: {data!r} != {shape!r} '\n                        f'{_format_path(path)}')\n            elif shape is None:\n                if data is not None:\n                    fail(\n                        f'{message}: {data!r} is expected to be None '\n                        f'{_format_path(path)}')\n            else:\n                raise ValueError(f'unsupported shape type {shape}')\n\n    message = message or 'data shape differs'\n    return _assert_generic_shape((), data, shape)\n"
  },
  {
    "path": "edb/common/ast/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom .base import *  # NOQA\nfrom .visitor import *  # NOQA\nfrom .transformer import *  # NOQA\nfrom .codegen import *  # NOQA\n"
  },
  {
    "path": "edb/common/ast/base.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport copy\nimport collections.abc\nimport functools\nimport re\nimport sys\nfrom typing import (\n    Any,\n    Callable,\n    cast,\n    get_type_hints,\n    TYPE_CHECKING,\n    AbstractSet  # NoQA\n)\n\nfrom edb.common import debug\nfrom edb.common import markup\nfrom edb.common import typing_inspect\n\n\nclass ASTError(Exception):\n    pass\n\n\nclass _Field:\n    def __init__(\n        self,\n        name,\n        type_,\n        default,\n        factory,\n        field_hidden=False,\n        field_meta=False,\n    ):\n        self.name = name\n        self.type = type_\n        self.default = default\n        self.factory = factory\n        self.hidden = field_hidden\n        self.meta = field_meta\n\n\nclass _FieldSpec:\n    def __init__(self, factory):\n        self.factory = factory\n\n\ndef field[T](*, factory: Callable[[], T]) -> T:\n    return cast(T, _FieldSpec(factory=factory))\n\n\ndef _check_type_passthrough(type_, value, raise_error):\n    pass\n\n\ndef _check_type_real(type_, value, raise_error):\n    if type_ is None:\n        return\n\n    if typing_inspect.is_union_type(type_):\n        for t in typing_inspect.get_args(type_, evaluate=True):\n            try:\n                _check_type(t, value, raise_error)\n            except TypeError:\n                pass\n            else:\n                break\n        else:\n            raise_error(str(type_), value)\n\n    elif typing_inspect.is_tuple_type(type_):\n        _check_tuple_type(type_, value, raise_error, tuple)\n\n    elif typing_inspect.is_generic_type(type_):\n        ot = typing_inspect.get_origin(type_)\n\n        if ot in (list, list, collections.abc.Sequence):\n            _check_container_type(type_, value, raise_error, list)\n\n        elif ot in (set, set):\n            _check_container_type(type_, value, raise_error, set)\n\n        elif ot in (frozenset, frozenset):\n            _check_container_type(type_, value, raise_error, frozenset)\n\n        elif ot in (dict, dict):\n            _check_mapping_type(type_, value, raise_error, dict)\n\n        elif ot is not None:\n            raise TypeError(f'unsupported typing type: {type_!r}')\n\n    elif type_ is not Any:\n        if value is not None and not isinstance(value, type_):\n            raise_error(type_.__name__, value)\n\n\nif debug.flags.typecheck:\n    _check_type = _check_type_real\nelse:\n    _check_type = _check_type_passthrough\n\n\nclass AST:\n    # These use type comments because type annotations are interpreted\n    # by the AST system and so annotating them would interfere!\n    __ast_frozen_fields__ = frozenset()  # type: AbstractSet[str]\n\n    # Class setup stuff:\n    @classmethod\n    def _collect_direct_fields(cls):\n        dct = cls.__dict__\n        cls.__abstract_node__ = bool(dct.get('__abstract_node__'))\n        cls.__rust_ignore__ = bool(dct.get('__rust_ignore__'))\n\n        if '__annotations__' not in dct:\n            cls._direct_fields = []\n            return cls\n\n        globalns = sys.modules[cls.__module__].__dict__.copy()\n        globalns[cls.__name__] = cls\n\n        try:\n            while True:\n                try:\n                    annos = get_type_hints(cls, globalns)\n                except NameError as e:\n                    # Forward type declaration.  Generally, we try\n                    # to avoid these as much as possible, but when\n                    # there's a cycle it's better to have correct\n                    # static type analysis even though the runtime\n                    # validation infrastructure does not support\n                    # cyclic references.\n                    # XXX: This is a horrible hack, need to find\n                    # a better way.\n                    m = re.match(r\"name '(\\w+)' is not defined\", e.args[0])\n                    if not m:\n                        raise\n                    globalns[m.group(1)] = AST\n                else:\n                    break\n\n        except Exception:\n            raise RuntimeError(\n                f'unable to resolve type annotations for '\n                f'{cls.__module__}.{cls.__qualname__}')\n\n        if annos:\n            annos = {k: v for k, v in annos.items()\n                     if k in dct['__annotations__']}\n\n            hidden = ()\n            if '__ast_hidden__' in dct:\n                hidden = set(dct['__ast_hidden__'])\n\n            meta = ()\n            if '__ast_meta__' in dct:\n                meta = set(dct['__ast_meta__'])\n\n            fields = []\n            for f_name, f_type in annos.items():\n                if f_type is object:\n                    f_type = None\n\n                factory = None\n                if f_name in dct:\n                    f_default = dct[f_name]\n                    if isinstance(f_default, _FieldSpec):\n                        factory = f_default.factory\n                        f_default = None\n                        delattr(cls, f_name)\n                else:\n                    f_default = None\n\n                f_hidden = f_name in hidden\n                f_meta = f_name in meta\n\n                fields.append(_Field(\n                    f_name, f_type, f_default, factory, f_hidden, f_meta\n                ))\n\n            cls._direct_fields = fields\n\n        return cls\n\n    def __init_subclass__(cls, **kwargs):\n        super().__init_subclass__(**kwargs)\n\n        cls._collect_direct_fields()\n\n        fields = collections.OrderedDict()\n\n        for parent in reversed(cls.__mro__):\n            lst = getattr(parent, '_direct_fields', [])\n            for field in lst:\n                fields[field.name] = field\n\n        cls._fields = fields\n        cls._field_factories = tuple(\n            (k, v.factory) for k, v in fields.items()\n            if v.factory and not isinstance(getattr(cls, k, None), property)\n        )\n\n        # Push the default values down in the MRO\n        for k, v in cls._fields.items():\n            if (\n                not v.factory\n                and not isinstance(getattr(cls, k, None), property)\n                and k not in cls.__dict__\n            ):\n                setattr(cls, k, v.default)\n\n    @classmethod\n    def get_field(cls, name):\n        return cls._fields.get(name)\n\n    # Actual object level code\n    def __init__(self, **kwargs):\n        if type(self).__abstract_node__:\n            raise ASTError(\n                f'cannot instantiate abstract AST node '\n                f'{self.__class__.__name__!r}')\n\n        # Make kwargs directly into our __dict__\n        for field_name, factory in self._field_factories:\n            if field_name not in kwargs:\n                kwargs[field_name] = factory()\n\n        should_check_types = __debug__ and _check_type is _check_type_real\n        if should_check_types:\n            for k, v in kwargs.items():\n                self.check_field_type(self._fields[k], v)\n\n        self.__dict__ = kwargs\n\n    def __copy__(self):\n        copied = self._init_copy()\n        for field, value in iter_fields(self, include_meta=True):\n            try:\n                object.__setattr__(copied, field, value)\n            except AttributeError:\n                # don't mind not setting getter_only attrs.\n                continue\n        return copied\n\n    def __deepcopy__(self, memo):\n        copied = self._init_copy()\n        for field, value in iter_fields(self, include_meta=True):\n            object.__setattr__(copied, field, copy.deepcopy(value, memo))\n        return copied\n\n    def _init_copy(self):\n        return self.__class__()\n\n    def replace[T](self: T, **changes) -> T:\n        copied = copy.copy(self)\n        for field, value in changes.items():\n            object.__setattr__(copied, field, value)\n        return copied\n\n    def _checked_setattr(self, name, value):\n        super().__setattr__(name, value)\n        field = self._fields.get(name)\n        if field:\n            self.check_field_type(field, value)\n            if name in self.__ast_frozen_fields__:\n                raise TypeError(f'cannot set immutable {name} on {self!r}')\n\n    if __debug__ and _check_type is _check_type_real:\n        __setattr__ = _checked_setattr\n\n    def check_field_type(self, field, value):\n        def raise_error(field_type_name, value):\n            raise TypeError(\n                '%s.%s.%s: expected %s but got %s' % (\n                    self.__class__.__module__, self.__class__.__name__,\n                    field.name, field_type_name, value.__class__.__name__))\n\n        _check_type(field.type, value, raise_error)\n\n    def dump(self, *, meta=True):\n        markup.dump(self, _ast_include_meta=meta)\n\n\nclass ImmutableASTMixin:\n    __frozen = False\n    # This uses type comments because type annotations are interpreted\n    # by the AST system and so annotating them would interfere!\n    __ast_mutable_fields__ = frozenset()  # type: AbstractSet[str]\n\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        self.__frozen = True\n\n    # mypy gets mad about this if there isn't a __setattr__ in AST.\n    # I don't know why.\n    if not TYPE_CHECKING:\n\n        def __setattr__(self, name, value):\n            if self.__frozen and name not in self.__ast_mutable_fields__:\n                raise TypeError(f'cannot set {name} on immutable {self!r}')\n            else:\n                super().__setattr__(name, value)\n\n\n@markup.serializer.serializer.register(AST)\ndef serialize_to_markup(ast, *, ctx):\n    node = markup.elements.lang.TreeNode(id=id(ast), name=type(ast).__name__)\n    include_meta = ctx.kwargs.get('_ast_include_meta', True)\n    exclude_unset = ctx.kwargs.get('_ast_exclude_unset', True)\n\n    if debug.flags.ast_span:\n        s = getattr(ast, 'span', None)\n        if s:\n            node.add_child(label='span', node=markup.serialize(str(s), ctx=ctx))\n\n    fields = iter_fields(\n        ast, include_meta=include_meta, exclude_unset=exclude_unset)\n    for fieldname, field in fields:\n        if ast._fields[fieldname].hidden:\n            continue\n        if field is None:\n            if ast._fields[fieldname].meta:\n                continue\n        node.add_child(label=fieldname, node=markup.serialize(field, ctx=ctx))\n\n    return node\n\n\n@functools.lru_cache(1024)\ndef _is_ast_node_type(cls):\n    return issubclass(cls, AST)\n\n\ndef is_ast_node(obj):\n    return _is_ast_node_type(obj.__class__)\n\n\n_marker = object()\n\n\ndef iter_fields(node, *, include_meta=True, exclude_unset=False):\n    exclude_meta = not include_meta\n    for field_name, field in node._fields.items():\n        if exclude_meta and field.meta:\n            continue\n        field_val = getattr(node, field_name, _marker)\n        if field_val is _marker:\n            continue\n        if exclude_unset:\n            if field.factory:\n                default = field.factory()\n            else:\n                default = field.default\n            if field_val == default:\n                continue\n        yield field_name, field_val\n\n\ndef _is_optional(type_):\n    return (typing_inspect.is_union_type(type_) and\n            type(None) in typing_inspect.get_args(type_, evaluate=True))\n\n\ndef _check_container_type(type_, value, raise_error, instance_type):\n    if not isinstance(value, instance_type):\n        raise_error(str(type_), value)\n\n    type_args = typing_inspect.get_args(type_, evaluate=True)\n    eltype = type_args[0]\n    for el in value:\n        _check_type(eltype, el, raise_error)\n\n\ndef _check_tuple_type(type_, value, raise_error, instance_type):\n    if not isinstance(value, instance_type):\n        raise_error(str(type_), value)\n\n    eltype = None\n    ellipsis = False\n    type_args = typing_inspect.get_args(type_, evaluate=True)\n\n    for i, el in enumerate(value):\n        if not ellipsis:\n            new_eltype = type_args[i]\n            if new_eltype is Ellipsis:\n                ellipsis = True\n            else:\n                eltype = new_eltype\n        if eltype is not None:\n            _check_type(eltype, el, raise_error)\n\n\ndef _check_mapping_type(type_, value, raise_error, instance_type):\n    if not isinstance(value, instance_type):\n        raise_error(str(type_), value)\n\n    type_args = typing_inspect.get_args(type_, evaluate=True)\n    ktype = type_args[0]\n    vtype = type_args[1]\n    for k, v in value.items():\n        _check_type(ktype, k, raise_error)\n        if not k and not _is_optional(ktype):\n            raise RuntimeError('empty key in map')\n        _check_type(vtype, v, raise_error)\n"
  },
  {
    "path": "edb/common/ast/codegen.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, Iterable, Sequence\nfrom dataclasses import dataclass\n\nimport itertools\nimport textwrap\n\nfrom . import base\nfrom .visitor import NodeVisitor\n\n\n@dataclass(kw_only=True, eq=False, match_args=False, slots=True, frozen=True)\nclass Options:\n    indent_with: str = ' ' * 4\n    add_line_information: bool = False\n    pretty: bool = True\n\n\nclass SourceGenerator(NodeVisitor):\n    \"\"\"Generate source code from an AST tree.\"\"\"\n\n    result: list[str]\n\n    def __init__(\n        self,\n        indent_with: str = ' ' * 4,\n        add_line_information: bool = False,\n        pretty: bool = True\n    ) -> None:\n        self.result = []\n        self.indent_with = indent_with\n        self.add_line_information = add_line_information\n        self.indentation = 0\n        self.char_indentation = 0\n        self.new_lines = 0\n        self.current_line = 1\n        self.pretty = pretty\n\n    def node_visit(self, node: base.AST) -> None:\n        method = 'visit_' + node.__class__.__name__\n        visitor = getattr(self, method, self.generic_visit)\n        return visitor(node)\n\n    def visit_indented(\n        self, node: base.AST, indent: bool = True, nest: bool = False\n    ) -> None:\n        if nest:\n            self.write(\"(\")\n        if indent:\n            self.new_lines = 1\n            self.char_indentation += 1\n        res = self.visit(node)\n        if indent:\n            self.char_indentation -= 1\n        if nest:\n            self.write(\")\")\n            self.new_lines = 1\n        return res\n\n    def write(self, *x: str, delimiter: Optional[str] = None) -> None:\n        if not x:\n            return\n        if self.new_lines:\n            if self.result and self.pretty:\n                self.current_line += self.new_lines\n                self.result.append('\\n' * self.new_lines)\n            if self.pretty:\n                self.result.append(self.indent_with * self.indentation)\n                self.result.append(' ' * self.char_indentation)\n            else:\n                self.result.append(' ')\n            self.new_lines = 0\n        if delimiter:\n            self.result.append(x[0])\n            chain = itertools.chain.from_iterable\n            chunks: Iterable[str] = chain((delimiter, v) for v in x[1:])\n        else:\n            chunks = x\n\n        for chunk in chunks:\n            if not isinstance(chunk, str):\n                raise ValueError(\n                    'invalid text chunk in codegen: {!r}'.format(chunk))\n            self.result.append(chunk)\n\n    def visit_list(\n        self,\n        items: Sequence[base.AST],\n        *,\n        separator: str = ',',\n        terminator: Optional[str] = None,\n        newlines: bool = True,\n        **kwargs: Any\n    ) -> None:\n        # terminator overrides separator setting\n        #\n        separator = terminator if terminator is not None else separator\n        size = len(items)\n        for i, item in enumerate(items):\n            self.visit(item, **kwargs)  # type: ignore\n            if i < size - 1 or terminator is not None:\n                self.write(separator)\n                if newlines:\n                    self.new_lines = 1\n                else:\n                    self.write(' ')\n\n    def newline(self, node=None, extra=0):\n        self.new_lines = max(self.new_lines, 1 + extra)\n        if node is not None and self.add_line_information:\n            self.write('# line: %s' % node.lineno)\n            self.new_lines = 1\n\n    def finish(self) -> str:\n        return ''.join(self.result)\n\n    @classmethod\n    def to_source(\n        cls,\n        node: base.AST | Sequence[base.AST],\n        indent_with: str = ' ' * 4,\n        add_line_information: bool = False,\n        pretty: bool = True,\n        **kwargs: Any\n    ) -> str:\n        generator = cls(indent_with, add_line_information,  # type: ignore\n                        pretty=pretty, **kwargs)\n        generator.visit(node)\n        return generator.finish()\n\n    def indent_text(self, text: str) -> str:\n        return textwrap.indent(text, self.indent_with * self.indentation)\n"
  },
  {
    "path": "edb/common/ast/transformer.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom edb.common import typeutils\n\nfrom . import base\nfrom . import visitor\n\n\nclass NodeTransformer(visitor.NodeVisitor):\n    \"\"\"Walks the abstract syntax tree and allows modification of nodes.\n\n    The `NodeTransformer` will walk the AST and use the return value of the\n    visitor methods to replace or remove the old node.  If the return value of\n    the visitor method is ``None``, the node will be removed from its location,\n    otherwise it is replaced with the return value.  The return value may be\n    the original node in which case no replacement takes place.\n\n    Here is an example transformer that rewrites all occurrences of name\n    lookups (``foo``) to ``data['foo']``::\n\n       class RewriteName(NodeTransformer):\n\n           def visit_Name(self, node):\n               return copy_location(Subscript(\n                   value=Name(id='data', ctx=Load()),\n                   slice=Index(value=Str(s=node.id)),\n                   ctx=node.ctx\n               ), node)\n\n    Keep in mind that if the node you're operating on has child nodes you must\n    either transform the child nodes yourself or call the :meth:`generic_visit`\n    method for the node first.\n\n    For nodes that were part of a collection of statements (that applies to all\n    statement nodes), the visitor may also return a list of nodes rather than\n    just a single node.\n\n    Usually you use the transformer like this::\n\n       node = YourTransformer().visit(node)\n    \"\"\"\n\n    def generic_visit(self, node):\n        if isinstance(node, base.ImmutableASTMixin):\n            changes = {}\n\n            for field, old_value in base.iter_fields(node, include_meta=False):\n                field_spec = node._fields[field]\n                if self.skip_hidden and field_spec.hidden:\n                    continue\n                if field in self.extra_skips:\n                    continue\n\n                old_value = getattr(node, field, None)\n\n                if typeutils.is_container(old_value):\n                    new_values = old_value.__class__(self.visit(old_value))\n                    changes[field] = old_value.__class__(new_values)\n\n                elif isinstance(old_value, base.AST):\n                    new_node = self.visit(old_value)\n                    if new_node is not old_value:\n                        changes[field] = new_node\n\n            node = node.replace(**changes)\n\n        else:\n            for field, old_value in base.iter_fields(node, include_meta=False):\n                field_spec = node._fields[field]\n                if self.skip_hidden and field_spec.hidden:\n                    continue\n                if field in self.extra_skips:\n                    continue\n\n                old_value = getattr(node, field, None)\n\n                if typeutils.is_container(old_value):\n                    new_values = old_value.__class__(self.visit(old_value))\n                    setattr(node, field, old_value.__class__(new_values))\n\n                elif isinstance(old_value, base.AST):\n                    new_node = self.visit(old_value)\n                    if new_node is not old_value:\n                        setattr(node, field, new_node)\n\n        return node\n"
  },
  {
    "path": "edb/common/ast/visitor.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import (\n    AbstractSet, Any, Callable, Collection, Optional, Iterable\n)\n\nfrom edb.common import typeutils\n\nfrom . import base\n\n\nclass SkipNode(Exception):\n    pass\n\n\ndef find_children[_T](\n    node: base.AST | Collection[base.AST],\n    type: type[_T],\n    test_func: Optional[Callable[[_T], bool]] = None,\n    terminate_early=False,\n    extra_skips: AbstractSet[str] = frozenset(),\n    extra_skip_types: tuple[type, ...] = (),\n) -> list[_T]:\n    visited = set()\n    result = []\n\n    def _find_children(node):\n        if isinstance(node, extra_skip_types):\n            return False\n        elif isinstance(node, (tuple, list, set, frozenset)):\n            for n in node:\n                if _find_children(n):\n                    return True\n            return False\n        elif isinstance(node, dict):\n            for n in node.values():\n                if _find_children(n):\n                    return True\n            return False\n        elif not base.is_ast_node(node):\n            return False\n\n        if node in visited:\n            return False\n        else:\n            visited.add(node)\n\n        try:\n            if isinstance(node, type) and (not test_func or test_func(node)):\n                result.append(node)\n                if terminate_early:\n                    return True\n        except SkipNode:\n            return False\n\n        for field, value in base.iter_fields(node, include_meta=False):\n            field_spec = node._fields[field]\n            if field_spec.hidden or field_spec.name in extra_skips:\n                continue\n\n            if _find_children(value):\n                return True\n\n        return False\n\n    _find_children(node)\n    return result\n\n\nclass NodeVisitor:\n    \"\"\"Walk the AST and call a visitor function for every node found.\n\n    This class is meant to be subclassed, with the subclass adding visitor\n    methods.\n\n    Per default the visitor functions for the nodes are ``'visit_'`` +\n    class name of the node.  So a `TryFinally` node visit function would\n    be `visit_TryFinally`.  This behavior can be changed by overriding\n    the `visit` method.  If no visitor function exists for a node\n    (return value `None`) the `generic_visit` visitor is used instead.\n\n    Don't use the `NodeVisitor` if you want to apply changes to nodes during\n    traversing.  For this a special visitor exists (`NodeTransformer`) that\n    allows modifications.\n    \"\"\"\n\n    skip_hidden = False\n    extra_skips: AbstractSet[str] = frozenset()\n\n    def __init__(self, *, context=None, memo=None):\n        if memo is not None:\n            self._memo = memo\n        else:\n            self._memo = {}\n        self._context = context\n\n    @property\n    def memo(self):\n        return self._memo\n\n    @classmethod\n    def run(cls, node, **kwargs):\n        visitor = cls(**kwargs)\n        return visitor.visit(node)\n\n    def container_visit(self, node) -> dict[Any, Any] | Iterable[Any]:\n        def _visit_element(elem):\n            if base.is_ast_node(elem) or typeutils.is_container(elem):\n                return self.visit(elem)\n            else:\n                return elem\n\n        result: dict[Any, Any] | Iterable[Any]\n\n        if isinstance(node, dict):\n            result = {}\n            for key, value in node.items():\n                result[key] = _visit_element(value)\n\n        elif isinstance(node, tuple):\n            result = ()\n            for elem in node:\n                result += (_visit_element(elem),)\n\n        else:\n            result = []\n            for elem in node:\n                result.append(_visit_element(elem))\n\n        return result\n\n    def repeated_node_visit(self, node):\n        result = self.memo[node]\n        if result is None:\n            return node\n        else:\n            return result\n\n    def node_visit(self, node):\n        if node in self.memo:\n            return self.repeated_node_visit(node)\n        else:\n            self.memo[node] = None\n\n        for cls in node.__class__.__mro__:\n            method = 'visit_' + cls.__name__\n            visitor = getattr(self, method, None)\n            if visitor is not None:\n                break\n        else:\n            visitor = self.generic_visit\n        result = visitor(node)\n        self.memo[node] = result\n        return result\n\n    def visit(self, node):\n        if typeutils.is_container(node):\n            return self.container_visit(node)\n        elif base.is_ast_node(node):\n            return self.node_visit(node)\n\n    def generic_visit(self, node, *, combine_results=None):\n        field_results = []\n\n        for field, value in base.iter_fields(node, include_meta=False):\n            field_spec = node._fields[field]\n            if self.skip_hidden and field_spec.hidden:\n                continue\n            if field in self.extra_skips:\n                continue\n\n            res = self.visit(value)\n            if res is not None:\n                field_results.append(res)\n\n        if combine_results is not None:\n            return combine_results(field_results)\n        else:\n            return self.combine_field_results(field_results)\n\n    def combine_field_results(self, results):\n        return results\n\n\ndef nodes_equal(n1, n2):\n    if type(n1) is not type(n2):\n        return False\n\n    for field, _value in base.iter_fields(n1, include_meta=False):\n        if not n1._fields[field].hidden:\n            n1v = getattr(n1, field)\n            n2v = getattr(n2, field)\n\n            if typeutils.is_container(n1v):\n                n1v = list(n1v)\n                if typeutils.is_container(n2v):\n                    n2v = list(n2v)\n                else:\n                    return False\n\n                if len(n1v) != len(n2v):\n                    return False\n\n                for i, item1 in enumerate(n1v):\n                    try:\n                        item2 = n2v[i]\n                    except IndexError:\n                        return False\n\n                    if base.is_ast_node(item1):\n                        if not nodes_equal(item1, item2):\n                            return False\n                    else:\n                        if item1 != item2:\n                            return False\n\n            elif base.is_ast_node(n1v):\n                if not nodes_equal(n1v, n2v):\n                    return False\n\n            else:\n                if n1v != n2v:\n                    return False\n\n    return True\n"
  },
  {
    "path": "edb/common/asyncutil.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Awaitable,\n    Callable,\n    cast,\n    overload,\n    Self,\n    TypeVar,\n)\n\nimport asyncio\nimport inspect\nimport warnings\n\n\nasync def deferred_shield[T](arg: Awaitable[T]) -> T:\n    '''Wait for a future, deferring cancellation until it is complete.\n\n    If you do\n        await deferred_shield(something())\n\n    it is approximately equivalent to\n        await something()\n\n    except that if the coroutine containing it is cancelled,\n    something() is protected from cancellation, and *additionally*\n    CancelledError is not raised in the caller until something()\n    completes.\n\n    This can be useful if something() contains something that\n    shouldn't be interrupted but also can't be safely left running\n    asynchronously.\n    '''\n    task = asyncio.ensure_future(arg)\n\n    ex = None\n    while not task.done():\n        try:\n            await asyncio.shield(task)\n        except asyncio.CancelledError as cex:\n            if ex is not None:\n                cex.__context__ = ex\n            ex = cex\n        except Exception:\n            if ex:\n                raise ex from None\n            raise\n\n    if ex:\n        raise ex\n    return task.result()\n\n\nasync def debounce[T](\n    input: Callable[[], Awaitable[T]],\n    output: Callable[[list[T]], Awaitable[None]],\n    *,\n    max_wait: float,\n    delay_amt: float,\n    max_batch_size: int,\n) -> None:\n    '''Debounce and batch async events.\n\n    Loops forever unless an operation fails, so should probably be run\n    from a task.\n\n    The basic algorithm is that if an event comes in less than\n    `delay_amt` since the previous one, then instead of sending it\n    immediately, we wait an additional `delay_amt` from then. If we are\n    already waiting, any message also extends the wait, up to\n    `max_wait`.\n\n    Also, cap the maximum batch size to `max_batch_size`.\n    '''\n    # I think the algorithm reads more clearly with the params\n    # capitalized as constants, though we don't want them like that in\n    # the argument list, so reassign them.\n    MAX_WAIT, DELAY_AMT, MAX_BATCH_SIZE = max_wait, delay_amt, max_batch_size\n\n    loop = asyncio.get_running_loop()\n\n    batch = []\n    last_signal = -MAX_WAIT\n    target_time = None\n\n    while True:\n        try:\n            if target_time is None:\n                v = await input()\n            else:\n                async with asyncio.timeout_at(target_time):\n                    v = await input()\n        except TimeoutError:\n            t = loop.time()\n        else:\n            batch.append(v)\n\n            t = loop.time()\n\n            # If we aren't current waiting, and we got a\n            # notification recently, arrange to wait some before\n            # sending it.\n            if (\n                target_time is None\n                and t - last_signal < DELAY_AMT\n            ):\n                target_time = t + DELAY_AMT\n            # If we were already waiting, wait a little longer, though\n            # not longer than MAX_WAIT.\n            elif (\n                target_time is not None\n            ):\n                target_time = min(\n                    max(t + DELAY_AMT, target_time),\n                    last_signal + MAX_WAIT,\n                )\n\n        # Skip sending the event if we need to wait longer.\n        if (\n            target_time is not None\n            and t < target_time\n            and len(batch) < MAX_BATCH_SIZE\n        ):\n            continue\n\n        await output(batch)\n        batch = []\n        last_signal = t\n        target_time = None\n\n\n_Owner = TypeVar(\"_Owner\")\nHandlerFunction = Callable[[], Awaitable[None]]\nHandlerMethod = Callable[[Any], Awaitable[None]]\n\n\nclass ExclusiveTask:\n    \"\"\"Manages to run a repeatable task once at a time.\"\"\"\n\n    _handler: HandlerFunction\n    _task: asyncio.Task | None\n    _scheduled: bool\n    _stop_requested: bool\n\n    def __init__(self, handler: HandlerFunction) -> None:\n        self._handler = handler\n        self._task = None\n        self._scheduled = False\n        self._stop_requested = False\n\n    @property\n    def scheduled(self) -> bool:\n        return self._scheduled\n\n    async def _run(self) -> None:\n        if self._scheduled and not self._stop_requested:\n            self._scheduled = False\n        else:\n            return\n        try:\n            await self._handler()\n        finally:\n            if self._scheduled and not self._stop_requested:\n                self._task = asyncio.create_task(self._run())\n            else:\n                self._task = None\n\n    def schedule(self) -> None:\n        \"\"\"Schedule to run the task as soon as possible.\n\n        If already scheduled, nothing happens; it won't queue up.\n\n        If the task is already running, it will be scheduled to run again as\n        soon as the running task is done.\n        \"\"\"\n        if not self._stop_requested:\n            self._scheduled = True\n            if self._task is None:\n                self._task = asyncio.create_task(self._run())\n\n    async def stop(self) -> None:\n        \"\"\"Cancel scheduled task and wait for the running one to finish.\n\n        After an ExclusiveTask is stopped, no more new schedules are allowed.\n        Note: \"cancel scheduled task\" only means setting self._scheduled to\n        False; if an asyncio task is scheduled, stop() will still wait for it.\n        \"\"\"\n        self._scheduled = False\n        self._stop_requested = True\n        if self._task is not None:\n            await self._task\n\n\nclass ExclusiveTaskProperty:\n    _method: HandlerMethod\n    _name: str | None\n\n    def __init__(\n        self, method: HandlerMethod, *, slot: str | None = None\n    ) -> None:\n        self._method = method\n        self._name = slot\n\n    def __set_name__(self, owner: type[_Owner], name: str) -> None:\n        if (slots := getattr(owner, \"__slots__\", None)) is not None:\n            if self._name is None:\n                raise TypeError(\"missing slot in @exclusive_task()\")\n            if self._name not in slots:\n                raise TypeError(\n                    f\"slot {self._name!r} must be defined in __slots__\"\n                )\n\n        if self._name is None:\n            self._name = name\n\n    @overload\n    def __get__(self, instance: None, owner: type[_Owner]) -> Self: ...\n\n    @overload\n    def __get__(\n        self, instance: _Owner, owner: type[_Owner]\n    ) -> ExclusiveTask: ...\n\n    def __get__(\n        self, instance: _Owner | None, owner: type[_Owner]\n    ) -> ExclusiveTask | Self:\n        # getattr on the class\n        if instance is None:\n            return self\n\n        assert self._name is not None\n\n        # getattr on an object with __dict__\n        if (d := getattr(instance, \"__dict__\", None)) is not None:\n            if rv := d.get(self._name, None):\n                return rv\n            rv = ExclusiveTask(self._method.__get__(instance, owner))\n            d[self._name] = rv\n            return rv\n\n        # getattr on an object with __slots__\n        else:\n            if rv := getattr(instance, self._name, None):\n                return rv\n            rv = ExclusiveTask(self._method.__get__(instance, owner))\n            setattr(instance, self._name, rv)\n            return rv\n\n\nExclusiveTaskDecorator = Callable[\n    [HandlerFunction | HandlerMethod], ExclusiveTask | ExclusiveTaskProperty\n]\n\n\ndef _exclusive_task(\n    handler: HandlerFunction | HandlerMethod, *, slot: str | None\n) -> ExclusiveTask | ExclusiveTaskProperty:\n    sig = inspect.signature(handler)\n    params = list(sig.parameters.values())\n    if len(params) == 0:\n        handler = cast(HandlerFunction, handler)\n        if slot is not None:\n            warnings.warn(\n                \"slot is specified but unused in @exclusive_task()\",\n                stacklevel=2,\n            )\n        return ExclusiveTask(handler)\n    elif len(params) == 1 and params[0].kind in (\n        inspect.Parameter.POSITIONAL_ONLY,\n        inspect.Parameter.POSITIONAL_OR_KEYWORD,\n    ):\n        handler = cast(HandlerMethod, handler)\n        return ExclusiveTaskProperty(handler, slot=slot)\n    else:\n        raise TypeError(\"bad signature\")\n\n\n@overload\ndef exclusive_task(handler: HandlerFunction) -> ExclusiveTask: ...\n\n\n@overload\ndef exclusive_task(\n    handler: HandlerMethod, *, slot: str | None = None\n) -> ExclusiveTaskProperty: ...\n\n\n@overload\ndef exclusive_task(*, slot: str | None = None) -> ExclusiveTaskDecorator: ...\n\n\ndef exclusive_task(\n    handler: HandlerFunction | HandlerMethod | None = None,\n    *,\n    slot: str | None = None,\n) -> ExclusiveTask | ExclusiveTaskProperty | ExclusiveTaskDecorator:\n    \"\"\"Convert an async function into an ExclusiveTask.\n\n    This decorator can be applied to either top-level functions or methods\n    in a class. In the latter case, the exclusiveness is bound to each object\n    of the owning class. If the owning class defines __slots__, you must also\n    define an extra slot to store the exclusive state and tell exclusive_task()\n    by providing the `slot` argument.\n    \"\"\"\n    if handler is None:\n\n        def decorator(\n            handler: HandlerFunction | HandlerMethod,\n        ) -> ExclusiveTask | ExclusiveTaskProperty:\n            return _exclusive_task(handler, slot=slot)\n\n        return decorator\n\n    return _exclusive_task(handler, slot=slot)\n"
  },
  {
    "path": "edb/common/asyncwatcher.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright EdgeDB Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Optional\n\nimport asyncio\nimport logging\n\nfrom . import retryloop\n\n\nlogger = logging.getLogger(\"edb.server.asyncwatcher\")\n\n\nclass AsyncWatcherProtocol(asyncio.Protocol):\n    def __init__(\n        self,\n        watcher: AsyncWatcher,\n    ) -> None:\n        self._transport: Optional[asyncio.Transport] = None\n        self._watcher = watcher\n\n    def connection_made(self, transport: asyncio.BaseTransport) -> None:\n        self._transport = transport  # type: ignore [assignment]\n        self.request()\n\n    def connection_lost(self, exc: Optional[Exception]) -> None:\n        self._watcher.incr_metrics_counter(\"watch-disconnect\")\n        self._watcher.on_connection_lost()\n\n    def request(self) -> None:\n        raise NotImplementedError\n\n    def close(self) -> None:\n        raise NotImplementedError\n\n\nclass AsyncWatcher:\n    def __init__(self) -> None:\n        self._waiter: Optional[asyncio.Future] = None\n        self._stop_waiter: Optional[asyncio.Future] = None\n        self._protocol: Optional[AsyncWatcherProtocol] = None\n        self._watching = False\n        self._retry_attempt = 0\n        self._backoff = retryloop.exp_backoff()\n\n    async def start_watching(self) -> bool:\n        if not self._watching:\n            self._watching = True\n            try:\n                self._protocol = await self._start_watching()\n                return True\n            except BaseException:\n                self.incr_metrics_counter(\"watch-start-err\")\n                self._watching = False\n                raise\n        return False\n\n    async def retry_watching(self) -> None:\n        self._retry_attempt += 1\n        delay = self._backoff(self._retry_attempt)\n        await asyncio.sleep(delay)\n        try:\n            await self.start_watching()\n        except Exception:\n            logger.warning(\n                \"%s failed to start watching, will retry.\",\n                type(self).__name__,\n                exc_info=True,\n            )\n            asyncio.create_task(self.retry_watching())\n\n    def stop_watching(self) -> None:\n        self._watching = False\n        protocol, self._protocol = self._protocol, None\n        if protocol is not None:\n            self._stop_waiter = asyncio.get_running_loop().create_future()\n            protocol.close()\n\n    async def wait_stopped_watching(self) -> None:\n        if self._stop_waiter is not None:\n            await self._stop_waiter\n\n    def on_connection_lost(self) -> None:\n        self._protocol = None\n        if self._watching:\n            self.stop_watching()\n            asyncio.create_task(self.retry_watching())\n        else:\n            waiter, self._stop_waiter = self._stop_waiter, None\n            if waiter is not None:\n                waiter.set_result(None)\n\n    def on_update(self, data: bytes) -> None:\n        self._retry_attempt = 0\n        self._on_update(data)\n\n    def _on_update(self, data: bytes) -> None:\n        raise NotImplementedError\n\n    async def _start_watching(self) -> AsyncWatcherProtocol:\n        raise NotImplementedError\n\n    def consume_tokens(self, tokens: int) -> float:\n        # For rate limit - tries to consume the given number of tokens, returns\n        # non-zero values as seconds to wait if unsuccessful\n        return 0\n\n    def incr_metrics_counter(self, event: str, value: float = 1.0) -> None:\n        pass\n"
  },
  {
    "path": "edb/common/binwrapper.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport io\nimport struct\n\n\nclass BinWrapper:\n    \"\"\"A utility binary-reader wrapper over any io.BytesIO object.\"\"\"\n\n    i64 = struct.Struct('!q')\n    i32 = struct.Struct('!l')\n    i16 = struct.Struct('!h')\n    i8 = struct.Struct('!b')\n\n    ui64 = struct.Struct('!Q')\n    ui32 = struct.Struct('!L')\n    ui16 = struct.Struct('!H')\n    ui8 = struct.Struct('!B')\n\n    def __init__(self, buf: io.BytesIO) -> None:\n        self.buf = buf\n\n    def write_ui64(self, val: int) -> None:\n        self.buf.write(self.ui64.pack(val))\n\n    def write_ui32(self, val: int) -> None:\n        self.buf.write(self.ui32.pack(val))\n\n    def write_ui16(self, val: int) -> None:\n        self.buf.write(self.ui16.pack(val))\n\n    def write_ui8(self, val: int) -> None:\n        self.buf.write(self.ui8.pack(val))\n\n    def write_i64(self, val: int) -> None:\n        self.buf.write(self.i64.pack(val))\n\n    def write_i32(self, val: int) -> None:\n        self.buf.write(self.i32.pack(val))\n\n    def write_i16(self, val: int) -> None:\n        self.buf.write(self.i16.pack(val))\n\n    def write_i8(self, val: int) -> None:\n        self.buf.write(self.i8.pack(val))\n\n    def write_len32_prefixed_bytes(self, val: bytes) -> None:\n        self.write_ui32(len(val))\n        self.buf.write(val)\n\n    def write_bytes(self, val: bytes) -> None:\n        self.buf.write(val)\n\n    def read_ui64(self) -> int:\n        data = self.buf.read(8)\n        return self.ui64.unpack(data)[0]\n\n    def read_ui32(self) -> int:\n        data = self.buf.read(4)\n        return self.ui32.unpack(data)[0]\n\n    def read_ui16(self) -> int:\n        data = self.buf.read(2)\n        return self.ui16.unpack(data)[0]\n\n    def read_ui8(self) -> int:\n        data = self.buf.read(1)\n        return self.ui8.unpack(data)[0]\n\n    def read_i64(self) -> int:\n        data = self.buf.read(8)\n        return self.i64.unpack(data)[0]\n\n    def read_i32(self) -> int:\n        data = self.buf.read(4)\n        return self.i32.unpack(data)[0]\n\n    def read_i16(self) -> int:\n        data = self.buf.read(2)\n        return self.i16.unpack(data)[0]\n\n    def read_i8(self) -> int:\n        data = self.buf.read(1)\n        return self.i8.unpack(data)[0]\n\n    def read_bytes(self, size: int) -> bytes:\n        data = self.buf.read(size)\n        if len(data) != size:\n            raise BufferError(f'cannot read bytes with len={size}')\n        return data\n\n    def read_len32_prefixed_bytes(self) -> bytes:\n        size = self.read_ui32()\n        return self.read_bytes(size)\n\n    def read_nullable_len32_prefixed_bytes(self) -> bytes | None:\n        size = self.read_i32()\n        if size == -1:\n            return None\n        else:\n            return self.read_bytes(size)\n\n    def tell(self) -> int:\n        return self.buf.tell()\n"
  },
  {
    "path": "edb/common/checked.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    ClassVar,\n    Optional,\n    AbstractSet,\n    Iterable,\n    Iterator,\n    MutableMapping,\n    MutableSequence,\n    MutableSet,\n    Sequence,\n    cast,\n    overload,\n)\n\nimport collections.abc\nimport itertools\nimport types\n\nfrom edb.common import debug\nfrom edb.common import parametric\n\n\n__all__ = [\n    \"CheckedList\",\n    \"CheckedDict\",\n    \"CheckedSet\",\n    \"FrozenCheckedList\",\n    \"FrozenCheckedSet\",\n]\n\n\nclass ParametricContainer:\n\n    types: ClassVar[Optional[tuple[type, ...]]] = None\n\n    def __reduce__(self) -> tuple[Any, ...]:\n        assert self.types is not None, f'missing parameters in {type(self)}'\n        cls: type[ParametricContainer] = self.__class__\n        container = getattr(self, \"_container\", ())\n        if cls.__name__.endswith(\"]\"):\n            # Parametrized type.\n            cls = cls.__bases__[0]\n        else:\n            # A subclass of a parametrized type.\n            return cls, (container,)\n\n        args = self.types[0] if len(self.types) == 1 else self.types\n        return cls.__restore__, (args, container)\n\n    @classmethod\n    def __restore__(\n        cls, params: tuple[type, ...], data: Iterable[Any]\n    ) -> ParametricContainer:\n        return cls[params](data)  # type: ignore\n\n\nclass AbstractCheckedList[T]:\n    type: type\n    _container: list[T]\n\n    @classmethod\n    def _check_type(cls, value: Any) -> T:\n        \"\"\"Ensure `value` is of type T and return it.\"\"\"\n        if not isinstance(value, cls.type):\n            raise ValueError(\n                f\"{cls!r} accepts only values of type {cls.type!r}, \"\n                f\"got {type(value)!r}\"\n            )\n        return cast(T, value)\n\n    def __init__(self, iterable: Iterable[T] = ()) -> None:\n        pass\n\n    def __lt__(self, other: list[T]) -> bool:\n        return self._container < self._cast(other)\n\n    def __le__(self, other: list[T]) -> bool:\n        return self._container <= self._cast(other)\n\n    def __gt__(self, other: list[T]) -> bool:\n        return self._container > self._cast(other)\n\n    def __ge__(self, other: list[T]) -> bool:\n        return self._container >= self._cast(other)\n\n    def _cast(self, other: list[T]) -> list[T]:\n        if isinstance(other, (CheckedList, FrozenCheckedList)):\n            return other._container\n\n        return other\n\n    def __eq__(self, other: object) -> bool:\n        if isinstance(other, (CheckedList, FrozenCheckedList)):\n            other = other._container\n        return self._container == other\n\n    def __str__(self) -> str:\n        return repr(self._container)\n\n    def __repr__(self) -> str:\n        return f\"{_type_repr(type(self))}({repr(self._container)})\"\n\n\nclass FrozenCheckedList[T](\n    ParametricContainer,\n    parametric.SingleParametricType[T],\n    AbstractCheckedList[T],\n    Sequence[T],\n):\n    def __init__(self, iterable: Iterable[T] = ()) -> None:\n        super().__init__()\n        self._container = [self._check_type(element) for element in iterable]\n        self._hash_cache = -1\n\n    def __hash__(self) -> int:\n        if self._hash_cache == -1:\n            self._hash_cache = hash(tuple(self._container))\n        return self._hash_cache\n\n    #\n    # Sequence\n    #\n\n    @overload\n    def __getitem__(self, index: int) -> T: ...\n\n    @overload\n    def __getitem__(self, index: slice) -> FrozenCheckedList[T]: ...\n\n    def __getitem__(self, index: int | slice) -> Any:\n        if isinstance(index, slice):\n            return self.__class__(self._container[index])\n\n        return self._container[index]\n\n    def __len__(self) -> int:\n        return len(self._container)\n\n    #\n    # List-specific\n    #\n\n    def __add__(self, other: Iterable[T]) -> FrozenCheckedList[T]:\n        return self.__class__(itertools.chain(self, other))\n\n    def __radd__(self, other: Iterable[T]) -> FrozenCheckedList[T]:\n        return self.__class__(itertools.chain(other, self))\n\n    def __mul__(self, n: int) -> FrozenCheckedList[T]:\n        return self.__class__(self._container * n)\n\n    __rmul__ = __mul__\n\n\nclass CheckedList[T](\n    ParametricContainer,\n    parametric.SingleParametricType[T],\n    AbstractCheckedList[T],\n    MutableSequence[T],\n):\n    def __init__(self, iterable: Iterable[T] = ()) -> None:\n        super().__init__()\n        self._container = [self._check_type(element) for element in iterable]\n\n    #\n    # Sequence\n    #\n\n    @overload\n    def __getitem__(self, index: int) -> T: ...\n\n    @overload\n    def __getitem__(self, index: slice) -> CheckedList[T]: ...\n\n    def __getitem__(self, index: int | slice) -> Any:\n        if isinstance(index, slice):\n            return self.__class__(self._container[index])\n\n        return self._container[index]\n\n    #\n    # MutableSequence\n    #\n\n    @overload\n    def __setitem__(self, index: int, value: T) -> None: ...\n\n    @overload\n    def __setitem__(self, index: slice, value: Iterable[T]) -> None: ...\n\n    def __setitem__(self, index: int | slice, value: Any) -> None:\n        if isinstance(index, int):\n            self._container[index] = self._check_type(value)\n            return\n\n        _slice = index\n        self._container[_slice] = filter(self._check_type, value)\n\n    @overload\n    def __delitem__(self, index: int) -> None: ...\n\n    @overload\n    def __delitem__(self, index: slice) -> None: ...\n\n    def __delitem__(self, index: int | slice) -> None:\n        del self._container[index]\n\n    def insert(self, index: int, value: T) -> None:\n        self._container.insert(index, self._check_type(value))\n\n    def __len__(self) -> int:\n        return len(self._container)\n\n    #\n    # List-specific\n    #\n\n    def __add__(self, other: Iterable[T]) -> CheckedList[T]:\n        return self.__class__(itertools.chain(self, other))\n\n    def __radd__(self, other: Iterable[T]) -> CheckedList[T]:\n        return self.__class__(itertools.chain(other, self))\n\n    def __iadd__(self, other: Iterable[T]) -> CheckedList[T]:\n        self._container.extend(filter(self._check_type, other))\n        return self\n\n    def __mul__(self, n: int) -> CheckedList[T]:\n        return self.__class__(self._container * n)\n\n    __rmul__ = __mul__\n\n    def __imul__(self, n: int) -> CheckedList[T]:\n        self._container *= n\n        return self\n\n    def sort(self, *, key: Any = None, reverse: bool = False) -> None:\n        self._container.sort(key=key, reverse=reverse)\n\n\nclass AbstractCheckedSet[T](AbstractSet[T]):\n    type: type\n    _container: AbstractSet[T]\n\n    def __init__(self, iterable: Iterable[T] = ()) -> None:\n        pass\n\n    @classmethod\n    def _check_type(cls, value: Any) -> T:\n        \"\"\"Ensure `value` is of type T and return it.\"\"\"\n        if not isinstance(value, cls.type):\n            raise ValueError(\n                f\"{cls!r} accepts only values of type {cls.type!r}, \"\n                f\"got {type(value)!r}\"\n            )\n        return cast(T, value)\n\n    def _cast(self, other: Any) -> AbstractSet[T]:\n        if isinstance(other, (FrozenCheckedSet, CheckedSet)):\n            return other._container\n\n        if isinstance(other, collections.abc.Set):\n            return other\n\n        return set(other)\n\n    def __eq__(self, other: object) -> bool:\n        if isinstance(other, (CheckedSet, FrozenCheckedSet)):\n            other = other._container\n        return self._container == other\n\n    def __str__(self) -> str:\n        return repr(self._container)\n\n    def __repr__(self) -> str:\n        return f\"{_type_repr(type(self))}({repr(self._container)})\"\n\n    #\n    # collections.abc.Set aka typing.AbstractSet\n    #\n\n    def __contains__(self, value: Any) -> bool:\n        return value in self._container\n\n    def __iter__(self) -> Iterator[T]:\n        return iter(self._container)\n\n    def __len__(self) -> int:\n        return len(self._container)\n\n    #\n    # Specific to set() and frozenset()\n    #\n\n    def issubset(self, other: AbstractSet[Any]) -> bool:\n        return self.__le__(other)\n\n    def issuperset(self, other: AbstractSet[Any]) -> bool:\n        return self.__ge__(other)\n\n\nclass FrozenCheckedSet[T](\n    ParametricContainer,\n    parametric.SingleParametricType[T],\n    AbstractCheckedSet[T],\n):\n    def __init__(self, iterable: Iterable[T] = ()) -> None:\n        super().__init__()\n        self._container = {self._check_type(element) for element in iterable}\n        self._hash_cache = -1\n\n    def __hash__(self) -> int:\n        if self._hash_cache == -1:\n            self._hash_cache = hash(frozenset(self._container))\n        return self._hash_cache\n\n    #\n    # Replaced mixins of collections.abc.Set\n    #\n\n    # NOTE: The type ignores on function signatures below are because we are\n    # deliberately breaking the Liskov Substitute Principle: we want the type\n    # checker to warn the user if a checked set of a type is __or__'d, or\n    # __and__'d  with a set of an incompatible type.  If the user wanted this,\n    # they should convert the checked set into a regular set or a differently\n    # typed checked set first.\n\n    def __and__(self, other: AbstractSet[T]) -> FrozenCheckedSet[T]:\n        other_set = self._cast(other)\n        for elem in other_set:\n            # We need the explicit type check to reject nonsensical\n            # & operations that must always result in an empty new set.\n            self._check_type(elem)\n        return self.__class__(other_set & self._container)\n\n    __rand__ = __and__\n\n    def __or__(  # type: ignore\n        self, other: AbstractSet[T]\n    ) -> FrozenCheckedSet[T]:\n        other_set = self._cast(other)\n        return self.__class__(other_set | self._container)\n\n    __ror__ = __or__\n\n    def __sub__(self, other: AbstractSet[T]) -> FrozenCheckedSet[T]:\n        other_set = self._cast(other)\n        for elem in other_set:\n            # We need the explicit type check to reject nonsensical\n            # - operations that always return the original checked set.\n            self._check_type(elem)\n        return self.__class__(self._container - other_set)\n\n    def __rsub__(self, other: AbstractSet[T]) -> FrozenCheckedSet[T]:\n        other_set = self._cast(other)\n        return self.__class__(other_set - self._container)\n\n    def __xor__(  # type: ignore\n        self, other: AbstractSet[T]\n    ) -> FrozenCheckedSet[T]:\n        other_set = self._cast(other)\n        return self.__class__(self._container ^ other_set)\n\n    __rxor__ = __xor__\n\n    #\n    # Specific to set() and frozenset()\n    #\n\n    union = __and__\n    intersection = __or__\n    difference = __sub__\n    symmetric_difference = __xor__\n\n\nclass CheckedSet[T](\n    ParametricContainer,\n    parametric.SingleParametricType[T],\n    AbstractCheckedSet[T],\n    MutableSet[T],\n):\n    _container: set[T]\n\n    def __init__(self, iterable: Iterable[T] = ()) -> None:\n        super().__init__()\n        self._container = {self._check_type(element) for element in iterable}\n\n    #\n    # Replaced mixins of collections.abc.Set\n    #\n\n    # NOTE: The type ignores on function signatures below are because we are\n    # deliberately breaking the Liskov Substitute Principle: we want the type\n    # checker to warn the user if a checked set of a type is __or__'d, or\n    # __and__'d  with a set of an incompatible type.  If the user wanted this,\n    # they should convert the checked set into a regular set or a differently\n    # typed checked set first.\n\n    def __and__(self, other: AbstractSet[T]) -> CheckedSet[T]:\n        other_set = self._cast(other)\n        for elem in other_set:\n            # We need the explicit type check to reject nonsensical\n            # & operations that must always result in an empty new set.\n            self._check_type(elem)\n        return self.__class__(other_set & self._container)\n\n    __rand__ = __and__\n\n    def __or__(self, other: AbstractSet[T]) -> CheckedSet[T]:  # type: ignore\n        other_set = self._cast(other)\n        return self.__class__(other_set | self._container)\n\n    __ror__ = __or__\n\n    def __sub__(self, other: AbstractSet[T]) -> CheckedSet[T]:\n        other_set = self._cast(other)\n        for elem in other_set:\n            # We need the explicit type check to reject nonsensical\n            # - operations that always return the original checked set.\n            self._check_type(elem)\n        return self.__class__(self._container - other_set)\n\n    def __rsub__(self, other: AbstractSet[T]) -> CheckedSet[T]:\n        other_set = self._cast(other)\n        return self.__class__(other_set - self._container)\n\n    def __xor__(self, other: AbstractSet[T]) -> CheckedSet[T]:  # type: ignore\n        other_set = self._cast(other)\n        return self.__class__(self._container ^ other_set)\n\n    __rxor__ = __xor__\n\n    #\n    # MutableSet\n    #\n\n    def add(self, value: T) -> None:\n        self._container.add(self._check_type(value))\n\n    def discard(self, value: T) -> None:\n        self._container.discard(self._check_type(value))\n\n    #\n    # Replaced mixins of collections.abc.MutableSet\n    #\n\n    def __ior__(self, other: AbstractSet[T]) -> CheckedSet[T]:  # type: ignore\n        self._container |= set(filter(self._check_type, other))\n        return self\n\n    def __iand__(self, other: AbstractSet[T]) -> CheckedSet[T]:\n        # We do the type check here to reject nonsensical\n        # & operations that always clear the checked set.\n        self._container &= set(filter(self._check_type, other))\n        return self\n\n    def __ixor__(self, other: AbstractSet[T]) -> CheckedSet[T]:  # type: ignore\n        self._container ^= set(filter(self._check_type, other))\n        return self\n\n    def __isub__(self, other: AbstractSet[T]) -> CheckedSet[T]:\n        # We do the type check here to reject nonsensical\n        # - operations that could never affect the checked set.\n        self._container -= set(filter(self._check_type, other))\n        return self\n\n    #\n    # Specific to set() and frozenset()\n    #\n\n    union = __and__\n    intersection = __or__\n    difference = __sub__\n    symmetric_difference = __xor__\n\n    #\n    # Specific to set()\n    #\n\n    update = __ior__\n    intersection_update = __iand__\n    difference_update = __isub__\n    symmetric_difference_update = __ixor__\n\n\ndef _type_repr(obj: Any) -> str:\n    if isinstance(obj, type):\n        if obj.__module__ == \"builtins\":\n            return obj.__qualname__\n        return f\"{obj.__module__}.{obj.__qualname__}\"\n    if isinstance(obj, types.FunctionType):\n        return obj.__name__\n    return repr(obj)\n\n\nclass AbstractCheckedDict[K, V]:\n    keytype: type\n    valuetype: type\n    _container: dict[K, V]\n\n    @classmethod\n    def _check_key_type(cls, key: Any) -> K:\n        \"\"\"Ensure `key` is of type K and return it.\"\"\"\n        if not isinstance(key, cls.keytype):\n            raise KeyError(\n                f\"{cls!r} accepts only keys of type {cls.keytype!r}, \"\n                f\"got {type(key)!r}\"\n            )\n        return cast(K, key)\n\n    @classmethod\n    def _check_value_type(cls, value: Any) -> V:\n        \"\"\"Ensure `value` is of type V and return it.\"\"\"\n        if not isinstance(value, cls.valuetype):\n            raise ValueError(\n                f\"{cls!r} accepts only values of type \"\n                \"{cls.valuetype!r}, got {type(value)!r}\"\n            )\n        return cast(V, value)\n\n    def __eq__(self, other: object) -> bool:\n        if isinstance(other, CheckedDict):\n            other = other._container\n        return self._container == other\n\n    def __str__(self) -> str:\n        return repr(self._container)\n\n    def __repr__(self) -> str:\n        return f\"{_type_repr(type(self))}({repr(self._container)})\"\n\n\nclass CheckedDict[K, V](\n    ParametricContainer,\n    parametric.KeyValueParametricType[K, V],\n    AbstractCheckedDict[K, V],\n    MutableMapping[K, V],\n):\n    def __init__(self, *args: Any, **kwargs: V) -> None:\n        super().__init__()\n        self._container = {}\n        if len(args) == 1:\n            self.update(args[0])\n        if len(args) > 1:\n            raise ValueError(\n                f\"{type(self)!r} expected at most 1 argument, got {len(args)}\"\n            )\n\n        if len(kwargs):\n            # Mypy is right below that the type of kwargs is Dict[str, V]\n            # but we are deliberately letting this through for it to blow up\n            # on runtime type checking if K is not a string.\n            self.update(kwargs)  # type: ignore\n\n    #\n    # collections.abc.Mapping\n    #\n\n    def __getitem__(self, key: K) -> V:\n        return self._container[key]\n\n    def __iter__(self) -> Iterator[K]:\n        return iter(self._container)\n\n    def __len__(self) -> int:\n        return len(self._container)\n\n    #\n    # collections.abc.MutableMapping\n    #\n\n    def __setitem__(self, key: K, value: V) -> None:\n        self._check_key_type(key)\n        self._container[key] = self._check_value_type(value)\n\n    def __delitem__(self, key: K) -> None:\n        del self._container[key]\n\n    #\n    # Dict-specific\n    #\n\n    @classmethod\n    def fromkeys(\n        cls, iterable: Iterable[K], value: Optional[V] = None\n    ) -> CheckedDict[K, V]:\n        new: CheckedDict[K, V] = cls()\n        for key in iterable:\n            new[cls._check_key_type(key)] = cls._check_value_type(value)\n        return new\n\n\ndef _identity[T](cls: type, value: T) -> T:\n    return value\n\n\n_type_checking = {\n    CheckedList: [\"_check_type\"],\n    CheckedDict: [\"_check_key_type\", \"_check_value_type\"],\n    CheckedSet: [\"_check_type\"],\n    FrozenCheckedList: [\"_check_type\"],\n    FrozenCheckedSet: [\"_check_type\"],\n}\n\n\ndef disable_typechecks() -> None:\n    for type_, methods in _type_checking.items():\n        for method in methods:\n            setattr(type_, method, _identity)\n\n\ndef enable_typechecks() -> None:\n    for type_, methods in _type_checking.items():\n        for method in methods:\n            try:\n                delattr(type_, method)\n            except AttributeError:\n                continue\n\n\nif not debug.flags.typecheck:\n    disable_typechecks()\n"
  },
  {
    "path": "edb/common/colorsys.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"An extension to standard library module :mod:`colorsys`.\n\nContains additional functions, with the most notable - :func:`rgb_distance`.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom math import sqrt as _sqrt\nfrom colorsys import (\n    rgb_to_yiq, yiq_to_rgb, rgb_to_hls, hls_to_rgb, rgb_to_hsv, hsv_to_rgb\n)\n\n\n__all__ = 'rgb_to_yiq', 'yiq_to_rgb', 'rgb_to_hls', 'hls_to_rgb', \\\n          'rgb_to_hsv', 'hsv_to_rgb', 'rgb_to_xyz', 'xyz_to_lab', \\\n          'rgb_distance', 'Color'\n\n\nclass Color:\n    colors = {\n        'aliceblue': '#f0f8ff',\n        'antiquewhite': '#faebd7',\n        'aqua': '#00ffff',\n        'aquamarine': '#7fffd4',\n        'azure': '#f0ffff',\n        'beige': '#f5f5dc',\n        'bisque': '#ffe4c4',\n        'black': '#000000',\n        'blanchedalmond': '#ffebcd',\n        'blue': '#0000ff',\n        'blueviolet': '#8a2be2',\n        'brown': '#a52a2a',\n        'burlywood': '#deb887',\n        'cadetblue': '#5f9ea0',\n        'chartreuse': '#7fff00',\n        'chocolate': '#d2691e',\n        'coral': '#ff7f50',\n        'cornflowerblue': '#6495ed',\n        'cornsilk': '#fff8dc',\n        'crimson': '#dc143c',\n        'cyan': '#00ffff',\n        'darkblue': '#00008b',\n        'darkcyan': '#008b8b',\n        'darkgoldenrod': '#b8860b',\n        'darkgray': '#a9a9a9',\n        'darkgreen': '#006400',\n        'darkkhaki': '#bdb76b',\n        'darkmagenta': '#8b008b',\n        'darkolivegreen': '#556b2f',\n        'darkorange': '#ff8c00',\n        'darkorchid': '#9932cc',\n        'darkred': '#8b0000',\n        'darksalmon': '#e9967a',\n        'darkseagreen': '#8fbc8f',\n        'darkslateblue': '#483d8b',\n        'darkslategray': '#2f4f4f',\n        'darkturquoise': '#00ced1',\n        'darkviolet': '#9400d3',\n        'deeppink': '#ff1493',\n        'deepskyblue': '#00bfff',\n        'dimgray': '#696969',\n        'dodgerblue': '#1e90ff',\n        'firebrick': '#b22222',\n        'floralwhite': '#fffaf0',\n        'forestgreen': '#228b22',\n        'fuchsia': '#ff00ff',\n        'gainsboro': '#dcdcdc',\n        'ghostwhite': '#f8f8ff',\n        'gold': '#ffd700',\n        'goldenrod': '#daa520',\n        'gray': '#808080',\n        'green': '#008000',\n        'greenyellow': '#adff2f',\n        'honeydew': '#f0fff0',\n        'hotpink': '#ff69b4',\n        'indianred': '#cd5c5c',\n        'indigo': '#4b0082',\n        'ivory': '#fffff0',\n        'khaki': '#f0e68c',\n        'lavender': '#e6e6fa',\n        'lavenderblush': '#fff0f5',\n        'lawngreen': '#7cfc00',\n        'lemonchiffon': '#fffacd',\n        'lightblue': '#add8e6',\n        'lightcoral': '#f08080',\n        'lightcyan': '#e0ffff',\n        'lightgoldenrodyellow': '#fafad2',\n        'lightgreen': '#90ee90',\n        'lightgrey': '#d3d3d3',\n        'lightpink': '#ffb6c1',\n        'lightsalmon': '#ffa07a',\n        'lightseagreen': '#20b2aa',\n        'lightskyblue': '#87cefa',\n        'lightslategray': '#778899',\n        'lightsteelblue': '#b0c4de',\n        'lightyellow': '#ffffe0',\n        'lime': '#00ff00',\n        'limegreen': '#32cd32',\n        'linen': '#faf0e6',\n        'magenta': '#ff00ff',\n        'maroon': '#800000',\n        'mediumaquamarine': '#66cdaa',\n        'mediumblue': '#0000cd',\n        'mediumorchid': '#ba55d3',\n        'mediumpurple': '#9370db',\n        'mediumseagreen': '#3cb371',\n        'mediumslateblue': '#7b68ee',\n        'mediumspringgreen': '#00fa9a',\n        'mediumturquoise': '#48d1cc',\n        'mediumvioletred': '#c71585',\n        'midnightblue': '#191970',\n        'mintcream': '#f5fffa',\n        'mistyrose': '#ffe4e1',\n        'moccasin': '#ffe4b5',\n        'navajowhite': '#ffdead',\n        'navy': '#000080',\n        'oldlace': '#fdf5e6',\n        'olive': '#808000',\n        'olivedrab': '#6b8e23',\n        'orange': '#ffa500',\n        'orangered': '#ff4500',\n        'orchid': '#da70d6',\n        'palegoldenrod': '#eee8aa',\n        'palegreen': '#98fb98',\n        'paleturquoise': '#afeeee',\n        'palevioletred': '#db7093',\n        'papayawhip': '#ffefd5',\n        'peachpuff': '#ffdab9',\n        'peru': '#cd853f',\n        'pink': '#ffc0cb',\n        'plum': '#dda0dd',\n        'powderblue': '#b0e0e6',\n        'purple': '#800080',\n        'red': '#ff0000',\n        'rosybrown': '#bc8f8f',\n        'royalblue': '#4169e1',\n        'saddlebrown': '#8b4513',\n        'salmon': '#fa8072',\n        'sandybrown': '#f4a460',\n        'seagreen': '#2e8b57',\n        'seashell': '#fff5ee',\n        'sienna': '#a0522d',\n        'silver': '#c0c0c0',\n        'skyblue': '#87ceeb',\n        'slateblue': '#6a5acd',\n        'slategray': '#708090',\n        'snow': '#fffafa',\n        'springgreen': '#00ff7f',\n        'steelblue': '#4682b4',\n        'tan': '#d2b48c',\n        'teal': '#008080',\n        'thistle': '#d8bfd8',\n        'tomato': '#ff6347',\n        'turquoise': '#40e0d0',\n        'violet': '#ee82ee',\n        'wheat': '#f5deb3',\n        'white': '#ffffff',\n        'whitesmoke': '#f5f5f5',\n        'yellow': '#ffff00',\n        'yellowgreen': '#9acd32'\n    }\n\n    def __init__(self, r, g, b, a=1.0):\n        r = int(r)\n        g = int(g)\n        b = int(b)\n        a = float(a)\n        if r > 255 or r < 0 or g < 0 or g > 255 or b < 0 or b > 255:\n            raise ValueError('color component should belong to [0, 255]')\n        if a < 0 or a > 1:\n            raise ValueError('alpha component should belong to [0, 1]')\n        self.r, self.g, self.b, self.a = r, g, b, a\n\n    @classmethod\n    def from_color(cls, color):\n        return cls(color.r, color.g, color.b, color.a)\n\n    @classmethod\n    def from_string(cls, value, alpha=1.0):\n        if not value.startswith('#'):\n            if value == 'transparent':\n                return cls(0, 0, 0, 0)\n            else:\n                try:\n                    value = cls.colors[str(value)]\n                except KeyError:\n                    raise ValueError('Unknown color name')\n        value = value[1:]\n        try:\n            if len(value) == 3:\n                r, g, b = [int(x * 2, 16) for x in value]\n            elif len(value) == 6:\n                r, g, b = [int(value[i:i + 2], 16) for i in range(0, 6, 2)]\n            else:\n                raise ValueError\n        except ValueError:\n            raise ValueError('Invalid color value')\n        return cls(r, g, b, a=alpha)\n\n    @classmethod\n    def from_hls(cls, h, l, s, alpha=1.0):  # NoQA: E741\n        return cls(*(int(c * 255) for c in hls_to_rgb(h, l, s)), a=alpha)\n\n    def rgb_channels(self, *, as_floats=False):\n        if as_floats:\n            return (self.r / 255.0, self.g / 255.0, self.b / 255.0)\n        else:\n            return (self.r, self.g, self.b)\n\n    def rgba_channels(self, *, as_floats=False):\n        if as_floats:\n            return (self.r / 255.0, self.g / 255.0, self.b / 255.0, self.a)\n        else:\n            return (self.r, self.g, self.b, self.a)\n\n    def hls_channels(self):\n        return rgb_to_hls(*(c / 255 for c in self.rgb_channels()))\n\n\n# Relative to RGB max white\nXYZ_MAX_X = 95.047\nXYZ_MAX_Y = 100.0\nXYZ_MAX_Z = 108.883\n\n\ndef rgb_to_xyz(r, g, b):\n    \"\"\"Converts RGB color to XYZ\n\n    :param float r: Red value in ``0..1`` range\n    :param float g: Green value in ``0..1`` range\n    :param float b: Blue value in ``0..1`` range\n    :returns: ``(x, y, z)``, all values normalized to\n              the ``(0..1, 0..1, 0..1)`` range\n    \"\"\"\n\n    # Formulae from http://www.easyrgb.com/index.php?X=MATH\n\n    if r > 0.04045:\n        r = ((r + 0.055) / 1.055) ** 2.4\n    else:\n        r /= 12.92\n\n    if g > 0.04045:\n        g = ((g + 0.055) / 1.055) ** 2.4\n    else:\n        g /= 12.92\n\n    if b > 0.04045:\n        b = ((b + 0.055) / 1.055) ** 2.4\n    else:\n        b /= 12.92\n\n    r *= 100.0\n    g *= 100.0\n    b *= 100.0\n\n    x = min((r * 0.4124 + g * 0.3576 + b * 0.1805) / XYZ_MAX_X, 1.0)\n    y = min((r * 0.2126 + g * 0.7152 + b * 0.0722) / XYZ_MAX_Y, 1.0)\n    z = min((r * 0.0193 + g * 0.1192 + b * 0.9505) / XYZ_MAX_Z, 1.0)\n\n    return (x, y, z)\n\n\n_1_3 = 1.0 / 3.0\n_16_116 = 16.0 / 116.0\n\n\ndef xyz_to_lab(x, y, z):\n    \"\"\"Converts XYZ color to LAB\n\n    :param float x: Value from ``0..1``\n    :param float y: Value from ``0..1``\n    :param float z: Value from ``0..1``\n    :returns: ``(L, a, b)``, values in\n              range ``(0..100, -127..128, -127..128)``\n    \"\"\"\n\n    # Formulae from http://www.easyrgb.com/index.php?X=MATH\n\n    if x > 0.008856:\n        x **= _1_3\n    else:\n        x = (7.787 * x) + _16_116\n\n    if y > 0.008856:\n        y **= _1_3\n    else:\n        y = (7.787 * y) + _16_116\n\n    if z > 0.008856:\n        z **= _1_3\n    else:\n        z = (7.787 * z) + _16_116\n\n    lum = 116.0 * y - 16.0\n    a = 500 * (x - y)\n    b = 200 * (y - z)\n\n    return (lum, a, b)\n\n\ndef rgb_distance(r1, g1, b1, r2, g2, b2):\n    \"\"\"Calculates numerical distance between two colors in RGB color space.\n\n    The distance is calculated by CIE94 formula.\n\n    :params: Two colors with ``r, g, b`` values in ``0..1`` range\n    :returns: A number in ``0..100`` range.  The lesser - the\n              closer colors are.\n    \"\"\"\n\n    # Formulae from wikipedia article re CIE94\n\n    L1, A1, B1 = xyz_to_lab(*rgb_to_xyz(r1, b1, g1))\n    L2, A2, B2 = xyz_to_lab(*rgb_to_xyz(r2, b2, g2))\n\n    dL = L1 - L2\n    C1 = _sqrt(A1 * A1 + B1 * B1)\n    C2 = _sqrt(A2 * A2 + B2 * B2)\n    dCab = C1 - C2\n    dA = A1 - A2\n    dB = B1 - B2\n\n    dEab = _sqrt(dL ** 2 + dA ** 2 + dB ** 2)\n\n    dHab = _sqrt(max(dEab ** 2 - dL ** 2 - dCab ** 2, 0.0))\n\n    dE = _sqrt((dL ** 2) + ((dCab / (1 + 0.045 * C1)) ** 2) + (\n        dHab / (1 + 0.015 * C1)) ** 2)\n    return dE\n"
  },
  {
    "path": "edb/common/compiler.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import (\n    Any,\n    Optional,\n    ContextManager,\n    Self,\n)\n\nimport collections\nimport re\n\n\nclass ContextLevel:\n    _stack: CompilerContext[Self]\n\n    def __init__(self, prevlevel: Optional[Self], mode: Any) -> None:\n        pass\n\n    def on_pop(\n        self: Self,\n        prevlevel: Optional[Self],\n    ) -> None:\n        pass\n\n    def new(\n        self: Self,\n        mode: Any=None,\n    ) -> CompilerContextManager[Self]:\n        return self._stack.new(mode, self)\n\n    def reenter(\n        self: Self,\n    ) -> CompilerReentryContextManager[Self]:\n        return CompilerReentryContextManager(self._stack, self)\n\n\nclass CompilerContextManager[ContextLevel_T: ContextLevel](\n    ContextManager[ContextLevel_T]\n):\n    def __init__(\n        self,\n        context: CompilerContext[ContextLevel_T],\n        mode: Any,\n        prevlevel: Optional[ContextLevel_T],\n    ) -> None:\n        self.context = context\n        self.mode = mode\n        self.prevlevel = prevlevel\n\n    def __enter__(self) -> ContextLevel_T:\n        return self.context.push(self.mode, self.prevlevel)\n\n    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:\n        self.context.pop()\n\n\nclass CompilerReentryContextManager[ContextLevel_T: ContextLevel](\n    ContextManager[ContextLevel_T]\n):\n    def __init__(\n        self,\n        context: CompilerContext[ContextLevel_T],\n        level: ContextLevel_T,\n    ) -> None:\n        self.context = context\n        self.level = level\n\n    def __enter__(self) -> ContextLevel_T:\n        return self.context._push(None, initial=self.level)\n\n    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:\n        self.context.pop()\n\n\nclass CompilerContext[ContextLevel_T: ContextLevel]:\n    stack: list[ContextLevel_T]\n    ContextLevelClass: type[ContextLevel_T]\n    default_mode: Any\n\n    def __init__(self, initial: ContextLevel_T) -> None:\n        self.stack = []\n        self._push(None, initial=initial)\n\n    def push(\n        self,\n        mode: Any,\n        prevlevel: Optional[ContextLevel_T] = None,\n    ) -> ContextLevel_T:\n        return self._push(mode, prevlevel)\n\n    def _push(\n        self,\n        mode: Any,\n        prevlevel: Optional[ContextLevel_T] = None,\n        *,\n        initial: Optional[ContextLevel_T] = None,\n    ) -> ContextLevel_T:\n        if initial is not None:\n            level = initial\n        else:\n            if prevlevel is None:\n                prevlevel = self.current\n            elif prevlevel is not self.current:\n                # In the past, we always used self.current as the\n                # previous level and simply ignored the prevlevel\n                # parameter. Actually using prevlevel makes more sense\n                # and has fewer gotchas, but enough code had grown to\n                # depend on the old behavior that changing it required\n                # asserting that they were the same.  We can consider\n                # dropping the assertion if it proves tedious.\n                raise AssertionError(\n                    'Calling new() on a context other than the current one')\n            level = self.ContextLevelClass(prevlevel, mode)\n        level._stack = self\n        self.stack.append(level)\n        return level\n\n    def pop(self) -> None:\n        level = self.stack.pop()\n        level.on_pop(self.stack[-1] if self.stack else None)\n\n    def new(\n        self,\n        mode: Any = None,\n        prevlevel: Optional[ContextLevel_T] = None,\n    ) -> CompilerContextManager[ContextLevel_T]:\n        if mode is None:\n            mode = self.default_mode\n        return CompilerContextManager(self, mode, prevlevel)\n\n    @property\n    def current(self) -> ContextLevel_T:\n        return self.stack[-1]\n\n\nclass SimpleCounter:\n    counts: collections.defaultdict[str, int]\n\n    def __init__(self) -> None:\n        self.counts = collections.defaultdict(int)\n\n    def nextval(self, name: str = 'default') -> int:\n        self.counts[name] += 1\n        return self.counts[name]\n\n\nclass AliasGenerator(SimpleCounter):\n    def get(self, hint: str = '') -> str:\n        if not hint:\n            hint = 'v'\n        m = re.search(r'~\\d+$', hint)\n        if m:\n            hint = hint[:m.start()]\n\n        idx = self.nextval(hint)\n        alias = f'{hint}~{idx}'\n\n        return alias\n"
  },
  {
    "path": "edb/common/debug.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Debug flags and output facilities.\n\nAn example code using this module:\n\n    if debug.flags.some_sql_flag:\n        debug.header('SQL')\n        debug.dump(sql_ast)\n\nUse `debug.header()`, `debug.print()`, `debug.dump()` and `debug.dump_code()`\nfunctions as opposed to using 'print' built-in directly.  This gives us\nflexibility to redirect debug output if needed.\n\"\"\"\n\n\nfrom __future__ import annotations\n\nimport builtins\nimport contextlib\nimport os\nimport sys\nimport time\nimport warnings\n\n# Don't import anything from \"edb.*\" as it will wreck coverage.\n\n\n__all__ = ()  # Don't.\n\n\nclass FlagsMeta(type):\n    def __new__(mcls, name, bases, dct):\n        flags = {}\n        for flagname, flag in dct.items():\n            if not isinstance(flag, Flag):\n                continue\n            flag.name = flagname\n            flags[flagname] = flag\n            dct[flagname] = flag.default\n\n        dct['_items'] = flags\n        return super().__new__(mcls, name, bases, dct)\n\n    def __iter__(cls):\n        return iter(cls._items.values())\n\n\nclass Flag:\n    def __init__(self, *, doc: str, default: bool=False):\n        self.name = None\n        self.doc = doc\n        self.default = default\n\n\nclass flags(metaclass=FlagsMeta):\n    pgsql_parser = Flag(\n        doc=\"Debug SQL parser.\")\n\n    bootstrap = Flag(\n        doc=\"Debug server catalog bootstrap.\")\n\n    bootstrap_cache_yolo = Flag(\n        doc=\"Disable bootstrap cache consistency check.\")\n\n    edgeql_parser = Flag(\n        doc=\"Debug EdgeQL parser (rebuild grammar verbosly).\")\n\n    edgeql_compile = Flag(\n        doc=\"Dump EdgeQL/IR/SQL ASTs.\")\n\n    edgeql_compile_edgeql_text = Flag(\n        doc=\"Dump EdgeQL Text (subset of `edgeql_compile').\")\n\n    edgeql_compile_edgeql_ast = Flag(\n        doc=\"Dump EdgeQL AST (subset of `edgeql_compile').\")\n\n    edgeql_compile_scope = Flag(\n        doc=\"Dump EdgeQL scope tree (subset of `edgeql_compile').\")\n\n    edgeql_compile_ir = Flag(\n        doc=\"Dump EdgeQL IR (subset of `edgeql_compile').\")\n\n    edgeql_compile_sql_ast = Flag(\n        doc=\"Dump generated SQL AST (subset of `edgeql_compile').\")\n\n    edgeql_compile_sql_ast_meta = Flag(\n        doc=\"Whether to include the metadata fields when dumping the SQL AST.\")\n\n    edgeql_compile_sql_text = Flag(\n        doc=\"Dump generated SQL text (subset of `edgeql_compile').\")\n\n    edgeql_compile_sql_reordered_text = Flag(\n        doc=\"Dump generated SQL-like text that might better reflect scoping.\")\n\n    edgeql_explain = Flag(\n        doc=\"Dump extra debug info when doing EXPLAIN\")\n\n    edgeql_disable_normalization = Flag(\n        doc=\"Disable EdgeQL normalization (constant extraction etc)\")\n\n    graphql_compile = Flag(\n        doc=\"Debug GraphQL compiler.\")\n\n    sdl_loading = Flag(\n        doc=\"Print applied DDL when loading SDL.\")\n\n    delta_plan = Flag(\n        doc=\"Print expanded delta command tree prior to processing.\")\n\n    delta_pgsql_plan = Flag(\n        doc=\"Print delta command tree annortated with DB ops.\")\n\n    delta_execute = Flag(\n        doc=\"Output SQL commands as executed during migration.\")\n\n    delta_execute_ddl = Flag(\n        doc=\"Output just the DDL commands as executed during migration.\")\n\n    delta_validate_reflection = Flag(\n        doc=\"Whether to do expensive validation of reflection correctness.\")\n\n    server = Flag(\n        doc=\"Print server errors.\")\n\n    server_proto = Flag(\n        doc=\"Print server protocol querying messages.\")\n\n    server_clobber_pg_conns = Flag(\n        doc=\"Discard Postgres connections when releasing them to the pool.\")\n\n    edgeql_text_in_sql = Flag(\n        doc=\"Include the EdgeQL query text in the SQL sent to Postgres.\")\n\n    print_locals = Flag(\n        doc=\"Include values of local variables in tracebacks.\")\n\n    disable_qcache = Flag(\n        doc=\"Disable server query cache. Parse/Execute will always recompile.\")\n\n    typecheck = Flag(\n        doc=\"Perform runtime type checking.\")\n\n    pgserver = Flag(\n        doc=\"Show PostgreSQL server logs and log all statements.\")\n\n    log_metrics = Flag(\n        doc=\"Log verbose statistics on connections and compiler behavior.\")\n\n    disable_docs_edgeql_validation = Flag(\n        doc=\"Disable validation of edgeql in docs (for site build)\")\n\n    pydebug_listen = Flag(\n        doc=\"Enable listening for Debug Adapter Protocol connections. \"\n            \"Requires pydebug to be installed.\"\n    )\n\n    sql_input = Flag(\n        doc=\"Enable logging of SQL incoming requests (pg compiler input).\"\n    )\n\n    sql_output = Flag(\n        doc=\"Enable logging of SQL requests, compiled to the internal SQL\"\n            \"(pg compiler output).\"\n    )\n\n    sql_text_in_sql = Flag(\n        doc=\"Include the original SQL query text in the SQL sent to Postgres.\"\n    )\n\n    zombodb = Flag(doc=\"Enabled zombodb and disables postgres FTS\")\n\n    ast_span = Flag(doc=\"Enables spans in markup of ASTs\")\n\n\n@contextlib.contextmanager\ndef timeit(title='block'):\n    st = time.monotonic()\n    try:\n        yield\n    finally:\n        print(f'{title} took {time.monotonic() - st:.4f}s')\n\n\ndef header(*args):\n    print('=' * 80)\n    print(*args)\n    print('=' * 80)\n\n\ndef dump(*args, **kwargs):\n    from . import markup as _markup\n    _markup.dump(*args, **kwargs)\n\n\ndef dumps(*args, **kwargs):\n    from . import markup as _markup\n    return _markup.dumps(*args, **kwargs)\n\n\ndef dump_code(*args, **kwargs):\n    from . import markup as _markup\n    _markup.dump_code(*args, **kwargs)\n\n\ndef dump_sql(sql, *args, **kwargs):\n    import edb.pgsql.codegen\n    dump_code(\n        edb.pgsql.codegen.generate_source(sql, *args, **kwargs), lexer='SQL'\n    )\n\n\ndef dump_edgeql(eql, *args, **kwargs):\n    import edb.edgeql.codegen\n    dump_code(edb.edgeql.codegen.generate_source(eql, *args, **kwargs))\n\n\ndef set_trace(**kwargs):\n    \"\"\"Debugger hook that works inside worker processes.\n\n    Set PYTHONBREAKPOINT=edb.common.debug.set_trace, and this will be triggered\n    by `breakpoint()`.\n\n    Unfortunately readline doesn't work when not using stdin itself,\n    so try running the server wrapped with `rlwrap.`\n    \"\"\"\n    from pdb import Pdb\n    new_stdin = open(\"/dev/tty\", \"r\")\n    Pdb(stdin=new_stdin, stdout=sys.stdout).set_trace(\n        sys._getframe().f_back, **kwargs)\n\n\ndef print(*args):\n    builtins.print(*args)\n\n\ndef init_debug_flags():\n    prefix = 'EDGEDB_DEBUG_'\n\n    for env_name, env_val in os.environ.items():\n        if not env_name.startswith(prefix):\n            continue\n\n        name = env_name[len(prefix):].lower()\n        if not hasattr(flags, name):\n            warnings.warn(f'Unknown debug flag: {env_name!r}', stacklevel=2)\n            continue\n\n        value = env_val.strip() not in {'', '0'}\n        setattr(flags, name, value)\n\n\ninit_debug_flags()\n"
  },
  {
    "path": "edb/common/devmode.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Optional, NamedTuple\n\nimport contextlib\nimport json\nimport logging\nimport os\nimport pathlib\n\n\nlogger = logging.getLogger('edb.devmode.cache')\n\n\nclass CoverageConfig(NamedTuple):\n\n    config: str\n    datadir: str\n    paths: list[str]\n\n    def to_json(self) -> str:\n        return json.dumps(self._asdict())\n\n    @classmethod\n    def from_json(cls, js: str):\n        dct = json.loads(js)\n        return cls(**dct)\n\n    def save_to_environ(self):\n        os.environ.update({\n            'EDGEDB_TEST_COVERAGE': self.to_json()\n        })\n\n    @classmethod\n    def from_environ(cls) -> Optional['CoverageConfig']:\n        config = os.environ.get('EDGEDB_TEST_COVERAGE')\n        if config is None:\n            return None\n        else:\n            return cls.from_json(config)\n\n    @classmethod\n    def new_custom_coverage_object(cls, **conf):\n        import coverage\n\n        cov = coverage.Coverage(**conf)\n\n        cov._warn_no_data = False\n        cov._warn_unimported_source = False\n        cov._warn_preimported_source = False\n\n        return cov\n\n    def new_coverage_object(self):\n        return self.new_custom_coverage_object(\n            config_file=self.config,\n            source=self.paths,\n            data_file=os.path.join(self.datadir, f'cov-{os.getpid()}'),\n        )\n\n    @classmethod\n    def start_coverage_if_requested(cls):\n        cov_config = cls.from_environ()\n        if cov_config is not None:\n            cov = cov_config.new_coverage_object()\n            cov.start()\n            return cov\n        else:\n            return None\n\n    @classmethod\n    @contextlib.contextmanager\n    def enable_coverage_if_requested(cls):\n        cov_config = cls.from_environ()\n        if cov_config is None:\n            yield\n        else:\n            cov = cov_config.new_coverage_object()\n            cov.start()\n            try:\n                yield\n            finally:\n                cov.stop()\n                cov.save()\n\n\ndef enable_dev_mode(enabled: bool = True):\n    os.environ['__EDGEDB_DEVMODE'] = '1' if enabled else ''\n\n\ndef is_in_dev_mode() -> bool:\n    devmode = os.environ.get('__EDGEDB_DEVMODE', '0')\n    return devmode.lower() not in ('0', '', 'false')\n\n\ndef get_dev_mode_cache_dir() -> pathlib.Path:\n    if is_in_dev_mode():\n        root = pathlib.Path(__file__).parent.parent.parent\n        cache_dir = (root / 'build' / 'cache')\n        cache_dir.mkdir(exist_ok=True)\n        return cache_dir\n    else:\n        raise RuntimeError('server is not running in dev mode')\n\n\ndef get_dev_mode_data_dir() -> pathlib.Path:\n    data_dir_env = os.environ.get(\"EDGEDB_SERVER_DEV_DIR\")\n    if data_dir_env:\n        data_dir = pathlib.Path(data_dir_env)\n    else:\n        root = pathlib.Path(__file__).parent.parent.parent\n        data_dir = root / \"tmp\" / \"devdatadir\"\n\n    return data_dir\n"
  },
  {
    "path": "edb/common/english.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2009-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n\ndef add_a(word):\n    article = 'an' if word[0] in 'aeiou' else 'a'\n    return f'{article} {word}'\n"
  },
  {
    "path": "edb/common/enum.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport enum\nimport functools\n\n\nclass StrEnum(str, enum.Enum):\n    \"\"\"A version of string enum with reasonable __str__.\"\"\"\n    def __str__(self):\n        return self._value_\n\n\n@functools.total_ordering\nclass OrderedEnumMixin():\n    @classmethod\n    @functools.lru_cache(None)\n    def _index_of(cls, value):\n        return list(cls).index(value)\n\n    def __lt__(self, other):\n        if self.__class__ is other.__class__:\n            return self._index_of(self) < self._index_of(other)\n        return NotImplemented\n"
  },
  {
    "path": "edb/common/exceptions.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2010-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport sys\n\n\ndef _get_contexts(ex, *, auto_init=False):\n    try:\n        return ex.__sx_error_contexts__\n    except AttributeError:\n        if auto_init:\n            cs = ex.__sx_error_contexts__ = {}\n            return cs\n        else:\n            return {}\n\n\ndef add_context(ex, context):\n    assert isinstance(context, ExceptionContext)\n\n    contexts = _get_contexts(ex, auto_init=True)\n\n    cls = context.__class__\n    if cls in contexts:\n        raise ValueError(\n            'context {}.{} is already present in '\n            'exception'.format(cls.__module__, cls.__name__))\n\n    contexts[cls] = context\n\n\ndef replace_context(ex, context):\n    contexts = _get_contexts(ex, auto_init=True)\n    contexts[context.__class__] = context\n\n\ndef get_context(ex, context_class):\n    contexts = _get_contexts(ex)\n    try:\n        return contexts[context_class]\n    except KeyError as ex:\n        raise LookupError(\n            '{} context class is not '\n            'found'.format(context_class)) from ex\n\n\ndef iter_contexts(ex, ctx_class=None):\n    contexts = _get_contexts(ex)\n    if ctx_class is None:\n        return iter(contexts.values())\n    else:\n        assert issubclass(ctx_class, ExceptionContext)\n        return (\n            context for context in contexts.values()\n            if isinstance(context, ctx_class))\n\n\nclass ExceptionContext:\n    title = 'Exception Context'\n\n\nclass DefaultExceptionContext(ExceptionContext):\n    title = 'Details'\n\n    def __init__(self, hint=None, details=None):\n        super().__init__()\n\n        self.details = details\n        self.hint = hint\n\n\n_old_excepthook = sys.excepthook\n\n\ndef _is_internal_error(exc):\n    if isinstance(exc, ExceptionGroup):\n        return any(_is_internal_error(e) for e in exc.exceptions)\n    # This is pretty cheesy but avoids needing to import our edgedb\n    # exceptions or do anything elaborate with contexts.\n    return type(exc).__name__ == 'InternalServerError'\n\n\ndef excepthook(exctype, exc, tb):\n    try:\n        from edb.common import markup\n        markup.dump(exc, file=sys.stderr)\n\n        if _is_internal_error(exc):\n            # TODO(rename): change URL once we can\n            print(\n                f'This is most likely a bug in Gel. '\n                f'Please consider opening an issue ticket '\n                f'at https://github.com/edgedb/edgedb/issues/new'\n                f'?template=bug_report.md'\n            )\n\n    except Exception as ex:\n        print('!!! exception in edb.excepthook !!!', file=sys.stderr)\n\n        # Attach the original exception as a context to top of the new chain,\n        # but only if it's not already there.  Take some care to avoid looping\n        # forever.\n        visited = set()\n        parent = ex\n        while parent.__cause__ or (\n                not parent.__suppress_context__ and parent.__context__):\n            if (parent in visited or parent.__context__ is exc or\n                    parent.__cause__ is exc):\n                break\n            visited.add(parent)\n            parent = parent.__cause__ or parent.__context__\n        parent.__context__ = exc\n        parent.__cause__ = None\n\n        _old_excepthook(type(ex), ex, ex.__traceback__)\n\n\ndef install_excepthook():\n    sys.excepthook = excepthook\n\n\ndef uninstall_excepthook():\n    sys.excepthook = _old_excepthook\n"
  },
  {
    "path": "edb/common/levenshtein.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n\ndef distance(s: str, t: str) -> int:\n    \"\"\"Calculates Levenshtein distance between s and t.\"\"\"\n\n    m, n = len(s), len(t)\n\n    if m > n:\n        s, t = t, s\n        m, n = n, m\n\n    ri = list(range(m + 1))\n\n    for i in range(1, n + 1):\n        ri_1, ri = ri, [i] + [0] * m\n\n        for j in range(1, m + 1):\n            ri[j] = min(ri_1[j] + 1,\n                        ri[j - 1] + 1,\n                        ri_1[j - 1] + int(s[j - 1] != t[i - 1]))\n\n    return ri[m]\n"
  },
  {
    "path": "edb/common/log.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n# DON'T IMPORT asyncio or any package that creates their own logger here,\n# or the \"tenant\" value cannot be injected.\nimport contextvars\nimport logging\n\n\ncurrent_tenant = contextvars.ContextVar(\"current_tenant\", default=\"-\")\n\n\nclass EdgeDBLogger(logging.Logger):\n\n    def makeRecord(\n        self,\n        name,\n        level,\n        fn,\n        lno,\n        msg,\n        args,\n        exc_info,\n        func=None,\n        extra=None,\n        sinfo=None,\n    ):\n        # Unlike the standard Logger class, we allow overwriting\n        # all attributes of the log record with stuff from *extra*.\n        factory = logging.getLogRecordFactory()\n        rv = factory(name, level, fn, lno, msg, args, exc_info, func, sinfo)\n        rv.__dict__[\"tenant\"] = current_tenant.get()\n        if extra is not None:\n            rv.__dict__.update(extra)\n        return rv\n\n\ndef early_setup():\n    logging.setLoggerClass(EdgeDBLogger)\n"
  },
  {
    "path": "edb/common/lru.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport collections.abc\nimport functools\n\n\nfrom typing import Callable, Optional\nfrom types import MethodType\n\n\nclass LRUMapping(collections.abc.MutableMapping):\n    # We use an OrderedDict for LRU implementation.  Operations:\n    #\n    # * We use a simple `__setitem__` to push a new entry:\n    #       `entries[key] = new_entry`\n    #   That will push `new_entry` to the *end* of the entries dict.\n    #\n    # * When we have a cache hit, we call\n    #       `entries.move_to_end(key, last=True)`\n    #   to move the entry to the *end* of the entries dict.\n    #\n    # * When we need to remove entries to maintain `max_size`, we call\n    #       `entries.popitem(last=False)`\n    #   to remove an entry from the *beginning* of the entries dict.\n    #\n    # So new entries and hits are always promoted to the end of the\n    # entries dict, whereas the unused one will group in the\n    # beginning of it.\n\n    def __init__(self, *, maxsize):\n        if maxsize <= 0:\n            raise ValueError(\n                f'maxsize is expected to be greater than 0, got {maxsize}'\n            )\n\n        self._dict = collections.OrderedDict()\n        self._maxsize = maxsize\n\n    def __getitem__(self, key):\n        o = self._dict[key]\n        self._dict.move_to_end(key, last=True)\n        return o\n\n    def __setitem__(self, key, o):\n        if key in self._dict:\n            self._dict[key] = o\n            self._dict.move_to_end(key, last=True)\n        else:\n            self._dict[key] = o\n            if len(self._dict) > self._maxsize:\n                self._dict.popitem(last=False)\n\n    def __delitem__(self, key):\n        del self._dict[key]\n\n    def __contains__(self, key):\n        return key in self._dict\n\n    def __len__(self):\n        return len(self._dict)\n\n    def __iter__(self):\n        return iter(self._dict)\n\n\nclass _NoPickle:\n    def __init__(self, obj):\n        self.obj = obj\n\n    def __bool__(self):\n        return bool(self.obj)\n\n    def __getstate__(self):\n        return ()\n\n    def __setstate__(self, _d):\n        self.obj = None\n\n\ndef lru_method_cache[Tf: Callable](\n    maxsize: int | None = 128,\n) -> Callable[[Tf], Tf]:\n    \"\"\"A version of lru_cache for methods that shouldn't leak memory.\n\n    Basically the idea is that we generate a per-object lru-cached\n    partially applied method.\n\n    Since pickling an lru_cache of a lambda or a functools.partial\n    doesn't work, we wrap it in a _NoPickle object that doesn't pickle\n    its contents.\n    \"\"\"\n\n    def transformer(f: Tf) -> Tf:\n        key = f'__{f.__name__}_cached'\n\n        @functools.wraps(f)\n        def func(self, *args, **kwargs):\n            _m = getattr(self, key, None)\n            if not _m:\n                _m = _NoPickle(\n                    functools.lru_cache(maxsize)(functools.partial(f, self))\n                )\n                setattr(self, key, _m)\n            return _m.obj(*args, **kwargs)\n\n        return func  # type: ignore\n\n    return transformer\n\n\ndef method_cache[Tf: Callable](f: Tf) -> Tf:\n    return lru_method_cache(None)(f)\n\n\ndef clear_method_cache[Tf](method: Tf) -> None:\n    assert isinstance(method, MethodType)\n    key = f'__{method.__func__.__name__}_cached'\n    _m: Optional[_NoPickle] = getattr(method.__self__, key, None)\n    if _m is not None:\n        _m.obj.cache_clear()\n\n\n_LRU_CACHES: list[functools._lru_cache_wrapper] = []\n\n\ndef per_job_lru_cache[Tf: Callable](\n    maxsize: int | None = 128,\n) -> Callable[[Tf], Tf]:\n    \"\"\"A version of lru_cache that can be cleared en masse.\n\n    All the caches will be tracked and calling clear_lru_caches()\n    will clear them all.\n    \"\"\"\n\n    def transformer(f: Tf) -> Tf:\n        wrapped = functools.lru_cache(maxsize)(f)\n        _LRU_CACHES.append(wrapped)\n        return wrapped  # type: ignore\n\n    return transformer\n\n\ndef clear_lru_caches():\n    for cache in _LRU_CACHES:\n        cache.cache_clear()\n"
  },
  {
    "path": "edb/common/markup/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport abc\nimport sys\n\nfrom edb.common import exceptions\nfrom . import elements, serializer, renderers\nfrom .serializer import serialize\nfrom .serializer import base as _base_serializer\nfrom .serializer.base import Context  # noqa\nfrom .elements.base import Markup  # noqa\n\n\nclass MarkupCapableMixin:\n\n    def __init_subclass__(cls, **kwargs):\n        super().__init_subclass__(**kwargs)\n        if hasattr(cls, 'as_markup'):\n            serializer.serializer.register(cls)(cls.as_markup)\n\n\nclass MarkupExceptionContext(\n    exceptions.ExceptionContext,\n    MarkupCapableMixin,\n):\n\n    @abc.abstractclassmethod  # type: ignore\n    def as_markup(cls, *, ctx):\n        pass\n\n\ndef _serialize(obj, trim=True, kwargs=None):\n    ctx = _base_serializer.Context(trim=trim, kwargs=kwargs)\n    try:\n        return serialize(obj, ctx=ctx)\n    finally:\n        ctx.reset()\n\n\ndef dumps(obj, header=None, trim=True):\n    markup = _serialize(obj, trim=trim)\n    if header is not None:\n        markup = elements.doc.Section(title=header, body=[markup])\n    return renderers.terminal.renders(markup)\n\n\ndef _dump(markup, header, file):\n    if header is not None:\n        markup = elements.doc.Section(title=header, body=[markup])\n    renderers.terminal.render(markup, file=file)\n\n\ndef dump(*objs, file=None, trim=True, marker=None, **kwargs):\n    for obj in objs:\n        if marker:\n            markup = elements.doc.Marker(text=marker)\n            renderers.terminal.render(markup, file=file, ensure_newline=False)\n\n        markup = _serialize(obj, trim=trim, kwargs=kwargs)\n        _dump(markup, None, file)\n\n\ndef dump_code(code: str, *, lexer='python', header=None, file=None):\n    markup = serializer.serialize_code(code, lexer=lexer)\n    _dump(markup, header, file)\n\n\ndef dump_callstack(f=None, *, limit=None, header=None, file=None, trim=True):\n    if f is None:\n        try:\n            raise ZeroDivisionError\n        except ZeroDivisionError:\n            f = sys.exc_info()[2].tb_frame.f_back\n\n    if limit is None:\n        limit = getattr(sys, 'tracebacklimit', None)\n\n    result = []\n    i = 0\n    start_frame = f\n\n    ctx = _base_serializer.Context(trim=trim)\n\n    while f is not None and (limit is None or i < limit):\n        result.append(_base_serializer.serialize_callstack_point(f, ctx=ctx))\n        f = f.f_back\n        i += 1\n\n    result.reverse()\n    markup = elements.lang.Traceback(items=result, id=id(start_frame))\n    _dump(markup, header, file)\n"
  },
  {
    "path": "edb/common/markup/elements/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom . import base, lang, doc, code  # NOQA\n"
  },
  {
    "path": "edb/common/markup/elements/base.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom edb.common.struct import RTStruct, StructMeta, Field\nfrom edb.common import checked\n\n\nclass MarkupMeta(StructMeta):\n    def __new__(mcls, name, bases, dct, ns=None, **kwargs):\n        cls = super().__new__(mcls, name, bases, dct, **kwargs)\n        cls._markup_ns = ns\n\n        ns_name = [name]\n        for base in cls.__mro__:\n            try:\n                base_ns = base._markup_ns\n            except AttributeError:\n                pass\n            else:\n                if base_ns is not None:\n                    ns_name.append(base_ns)\n\n        cls._markup_name = '.'.join(reversed(ns_name))\n        cls._markup_name_safe = '_'.join(reversed(ns_name))\n\n        return cls\n\n    def __init__(cls, name, bases, dct, ns=None, **kwargs):\n        super().__init__(name, bases, dct, **kwargs)\n\n    def __instancecheck__(cls, inst):\n        # We make OverflowBarier and SerializationError be instanceof\n        # and subclassof any Markup class.  This avoids errors when\n        # they are being added to various CheckedList & CheckedDict\n        # collections.\n        parent_check = type(RTStruct).__instancecheck__\n        if parent_check(cls, inst):\n            return True\n        return type(inst) in (OverflowBarier, SerializationError)\n\n    def __subclasscheck__(cls, subcls):\n        parent_check = type(RTStruct).__subclasscheck__\n        if parent_check(cls, subcls):\n            return True\n        return subcls in (OverflowBarier, SerializationError)\n\n\nclass Markup(RTStruct, metaclass=MarkupMeta, use_slots=True):\n    \"\"\"Base class for all markup elements.\"\"\"\n\n\nMarkupList = checked.CheckedList[Markup]\nMarkupMapping = checked.CheckedDict[str, Markup]\n\n\nclass OverflowBarier(Markup):\n    \"\"\"Represents that the nesting level of objects was too big.\"\"\"\n\n\nclass SerializationError(Markup):\n    \"\"\"An error during object serialization occurred.\"\"\"\n\n    text = Field(str)\n    cls = Field(str)\n"
  },
  {
    "path": "edb/common/markup/elements/code.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom edb.common import checked\nfrom edb.common import struct\nfrom . import base\n\n\nclass BaseCode(base.Markup, ns='code'):\n    pass\n\n\nclass Token(BaseCode):\n    val = struct.Field(str)\n\n\nclass Code(BaseCode):\n    tokens = struct.Field(\n        checked.CheckedList[Token], default=None, coerce=True\n    )\n\n\nclass Whitespace(Token):\n    pass\n\n\nclass Comment(Token):\n    pass\n\n\nclass Keyword(Token):\n    pass\n\n\nclass Type(Token):\n    pass\n\n\nclass Operator(Token):\n    pass\n\n\nclass Name(Token):\n    pass\n\n\nclass Constant(Name):\n    pass\n\n\nclass BuiltinName(Name):\n    pass\n\n\nclass FunctionName(Name):\n    pass\n\n\nclass ClassName(Name):\n    pass\n\n\nclass Decorator(Token):\n    pass\n\n\nclass Attribute(Token):\n    pass\n\n\nclass Tag(Token):\n    pass\n\n\nclass Literal(Token):\n    pass\n\n\nclass String(Literal):\n    pass\n\n\nclass Number(Literal):\n    pass\n\n\nclass Punctuation(Token):\n    \"\"\"Characters ',', ':', '[', etc.\"\"\"\n\n\nclass Error(Token):\n    pass\n"
  },
  {
    "path": "edb/common/markup/elements/doc.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport difflib\n\nfrom edb.common import checked\nfrom edb.common.struct import Field\nfrom . import base\n\n\nclass DocMarkup(base.Markup, ns='doc'):\n    pass\n\n\nclass Marker(DocMarkup):\n    text = Field(str)\n\n\nclass Section(DocMarkup):\n    title = Field(str, coerce=True, default=None)\n    body = Field(base.MarkupList, coerce=True)\n    collapsed = Field(bool, coerce=True, default=False)\n\n\nclass SubNode(DocMarkup):\n    body = Field(base.Markup)\n\n\nclass Text(DocMarkup):\n    text = Field(str)\n\n\nclass SourceCode(DocMarkup):\n    text = Field(str)\n\n\nclass Diff(DocMarkup):\n    lines = Field(checked.CheckedList[str], coerce=True)\n\n    @classmethod\n    def get_diff(\n        cls, a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=10\n    ):\n\n        lines = difflib.unified_diff(\n            a, b, fromfile, tofile, fromfiledate, tofiledate, n)\n        lines = [line.rstrip() for line in lines]\n\n        if lines:\n            return cls(lines=lines)\n        else:\n            return Text(text='No differences')\n\n\nclass ValueDiff(DocMarkup):\n    before = Field(str)\n    after = Field(str)\n    comment = Field(str, default=None)\n"
  },
  {
    "path": "edb/common/markup/elements/lang.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport linecache\n\nfrom edb.common.struct import Field\nfrom edb.common import checked\nfrom . import base\n\n\nclass LangMarkup(base.Markup, ns='lang'):\n    pass\n\n\nclass Number(LangMarkup):\n    num = Field(str, default=None, coerce=True)\n\n\nclass String(LangMarkup):\n    str = Field(str, default=None, coerce=True)\n\n\nclass MultilineString(LangMarkup):\n    str = Field(str, default=None, coerce=True)\n\n\nclass Ref(LangMarkup):\n    ref = Field(int, coerce=True)\n    refname = Field(str, default=None)\n\n    def __repr__(self):\n        return '<{} {} {}>'.format('Ref', self.refname, self.ref)\n\n\nclass BaseObject(LangMarkup):\n    \"\"\"Base language object with ``id``, but without ``attributes``.\"\"\"\n\n    id = Field(int, default=None, coerce=True)\n\n\nclass Object(BaseObject):\n    class_module = Field(str)\n    classname = Field(str)\n    repr = Field(str, default=None)\n    attributes = Field(base.MarkupMapping, default=None, coerce=True)\n\n\nclass List(BaseObject):\n    items = Field(  # type: ignore[assignment]\n        base.MarkupList, default=base.MarkupList, coerce=True)\n    trimmed = Field(bool, default=False)\n    brackets = Field(str, default=\"[]\")\n\n\nclass Dict(BaseObject):\n    items = Field(  # type: ignore[assignment]\n        base.MarkupMapping, default=base.MarkupMapping, coerce=True)\n    trimmed = Field(bool, default=False)\n\n\nclass TreeNodeChild(BaseObject):\n    label = Field(str, default=None)\n    node = Field(base.Markup)\n\n\nTreeNodeChildrenList = checked.CheckedList[TreeNodeChild]\n\n\nclass TreeNode(BaseObject):\n    name = Field(str)\n    children = Field(\n        TreeNodeChildrenList, default=TreeNodeChildrenList, coerce=True)\n\n    def add_child(self, *, label=None, node):\n        self.children.append(TreeNodeChild(label=label, node=node))\n\n\nclass NoneConstantType(LangMarkup):\n    pass\n\n\nclass TrueConstantType(LangMarkup):\n    pass\n\n\nclass FalseConstantType(LangMarkup):\n    pass\n\n\nclass Constants:\n    none = NoneConstantType()\n    true = TrueConstantType()\n    false = FalseConstantType()\n\n\nclass TracebackPoint(BaseObject):\n    name = Field(str, default=None)\n    filename = Field(str, default=None)\n    lineno = Field(int, default=None)\n    colno = Field(int, default=None)\n    end_colno = Field(int, default=None)\n    address = Field(str, default=None)\n    context = Field(bool, default=False)\n\n    lines = Field(checked.CheckedList[str], default=None, coerce=True)\n    line_numbers = Field(checked.CheckedList[int], default=None, coerce=True)\n\n    locals = Field(Dict, default=None)\n\n    def load_source(self, window=3, lines=None):\n        self.lines = self.line_numbers = None\n\n        if (self.lineno and\n                ((self.filename and not self.filename.startswith('<') and\n                    not self.filename.endswith('>')) or lines)):\n\n            lineno = self.lineno\n\n            if not lines:\n                linecache.checkcache(self.filename)\n                sourcelines = linecache.getlines(self.filename, globals())\n            else:\n                sourcelines = lines\n\n            lines = []\n            line_numbers = []\n\n            start = max(1, lineno - window)\n            end = min(len(sourcelines), lineno + window) + 1\n            for i in range(start, end):\n                lines.append(sourcelines[i - 1].rstrip())\n                line_numbers.append(i)\n\n            if lines:\n                self.lines = checked.CheckedList[str](lines)\n                self.line_numbers = checked.CheckedList[int](line_numbers)\n\n\nTracebackPointList = checked.CheckedList[TracebackPoint]\n\n\nclass Traceback(BaseObject):\n    items = Field(  # type: ignore[assignment]\n        TracebackPointList, default=TracebackPointList, coerce=True)\n\n\nclass ExceptionContext(BaseObject):\n    title = Field(str, default='Context')\n    body = Field(base.MarkupList, coerce=True)\n\n\nExceptionContextList = checked.CheckedList[ExceptionContext]\n\n\nclass _Exception(Object):\n    pass\n\n\nclass Exception(_Exception):\n    msg = Field(str)\n\n    # NB: Traceback is just an exception context\n    #\n    contexts = Field(ExceptionContextList, default=None, coerce=True)\n\n    context = Field(_Exception, None)\n    cause = Field(_Exception, None)\n"
  },
  {
    "path": "edb/common/markup/format.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2012-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n\ndef xrepr(obj, *, max_len=None):\n    \"\"\"Extended ``builtins.repr`` function.\n\n    Examples:\n\n    .. code-block:: pycon\n\n        >>> xrepr('1234567890', max_len=7)\n        '12'...\n\n    :param int max_len: When defined limits maximum length of the result\n                        string representation.\n\n    :returns str:\n    \"\"\"\n    result = str(repr(obj))\n\n    if max_len is not None and len(result) > max_len:\n        ext = '...'\n        if result[0] in ('\"', \"'\"):\n            ext = result[0] + ext\n        elif result[0] == '<':\n            ext = '>' + ext\n        result = result[:(max_len - len(ext))] + ext\n\n    return result\n"
  },
  {
    "path": "edb/common/markup/renderers/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom . import terminal  # NOQA\n"
  },
  {
    "path": "edb/common/markup/renderers/styles.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom edb.common.term import Style16, Style256\n\n\nclass StylesTable:\n    def __getattr__(self, key):\n        # If we're querying some non-existing style, pretend it's empty\n        #\n        return Style16()\n\n\nclass Dark16(StylesTable):\n    Style = Style16\n\n    id = Style()\n    bracket = Style()\n\n    header1 = Style(color='white')\n    header2 = Style(color='black', bold=True)\n    exc_title = Style(color='red', bold=True)\n    marker = Style(color='red', bold=True)\n\n    tb_name = Style(color='yellow')\n    tb_filename = Style()\n    tb_lineno = Style()\n    tb_current_line = Style()\n    tb_code = Style()\n    tb_pos_caret = Style(color='white', bold=True)\n\n    attribute = Style(color='black', bold=True)\n    key = Style(color='yellow')\n\n    tree_node = Style(color='red', bold=True)\n\n    constant = Style(color='cyan')\n    literal = Style(color='green')\n\n    ref = Style(color='red')\n\n    unknown_object = Style(color='blue', bold=True)\n    serialization_error = unknown_markup = overflow = Style(\n        color='white', bgcolor='red')\n\n    diff_anno = Style(color='white')\n    diff_after = Style(color='green')\n    diff_before = Style(color='red')\n\n    code = Style(color='white')\n    code_decorator = Style(color='black', bold=True)\n    code_comment = attribute\n    code_string = literal\n    code_number = Style(color='green')\n    code_constant = constant\n    code_punctuation = bracket\n    code_keyword = constant\n    code_decl_name = tree_node\n    code_tag = code_keyword\n    code_attribute = Style(color=attribute.color)\n\n\nclass Dark256(StylesTable):\n    Style = Style256\n\n    id = Style(color='#3d6559')\n    bracket = Style(color='#a7a963')\n\n    header1 = Style(color='#656565')\n    header2 = Style(color='#474747')\n    exc_title = Style(color='#d84903', bold=True)\n    marker = Style(color='#bbb', bgcolor='#582a70', bold=True)\n\n    tb_name = Style(color='#5f5f87', bold=True)\n    tb_filename = Style()\n    tb_lineno = Style()\n    tb_current_line = Style(color='#fff', bold=True)\n    tb_code = Style()\n    tb_pos_caret = Style(color='white', bold=True)\n\n    attribute = Style(color='#565656', bold=True)\n    key = Style(color='#5f875f', bold=True)\n\n    tree_node = Style(color='#bc74d7', bold=True)\n\n    constant = Style(color='#1dbdd0')\n    literal = Style(color='#4aa336')\n\n    ref = Style(color='#586c9e')\n\n    unknown_object = Style(color='#707070')\n    unknown_markup = overflow = Style(color='white', bgcolor='#84345a')\n    serialization_error = Style(color='white', bgcolor='#900')\n\n    diff_anno = Style(color='#777')\n    diff_after = Style(color='#4aa336')\n    diff_before = Style(color='#A00')\n\n    code = Style(color='#aaa')\n    code_decorator = Style(color='#af5f00')\n    code_comment = attribute\n    code_string = literal\n    code_number = Style(color='#af5f5f')\n    code_constant = constant\n    code_punctuation = bracket\n    code_keyword = constant\n    code_decl_name = tree_node\n    code_tag = code_keyword\n    code_attribute = Style(color=attribute.color)\n"
  },
  {
    "path": "edb/common/markup/renderers/terminal.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport sys\nimport contextlib\n\nfrom edb.common import term\nfrom edb.common.markup.format import xrepr\n\nfrom .. import elements\nfrom . import styles as styles_module\n\nLINE_BREAK = 1\nFOLDABLE_LINES_START = 2\nFOLDABLE_LINES_END = 3\nNON_FOLDED_SPACE = 4\nFOLDED_SPACE = 5\n\nINDENT = 10\nINDENT_NO_NL = 11\nDEDENT = 20\nDEDENT_NO_NL = 21\n\nNEW_LINE = 30\n\nDATA = 100\nHEADER = 101\n\n\nclass Buffer:\n    def __init__(\n        self,\n        *,\n        max_width=None,\n        styled=False,\n        indentation=0,\n        indent_with=' ' * 4,\n    ):\n        self.data = []\n        self.indentation = 0\n        self.indent_with = indent_with\n\n        self.max_width = max_width\n        self.styled = styled\n\n    def new_line(self, lines=1):\n        for _ in range(lines):\n            self.data.append((NEW_LINE, ))\n\n    @contextlib.contextmanager\n    def indent(self, auto_new_line=True):\n        if auto_new_line:\n            self.data.append((INDENT, ))\n            yield\n            self.data.append((DEDENT, ))\n        else:\n            self.data.append((INDENT_NO_NL, ))\n            yield\n            self.data.append((DEDENT_NO_NL, ))\n\n    def non_folded_space(self, space=' '):\n        self.data.append((NON_FOLDED_SPACE, space))\n\n    def folded_space(self, space=' '):\n        self.data.append((FOLDED_SPACE, space))\n\n    @contextlib.contextmanager\n    def foldable_lines(self):\n        self.data.append((FOLDABLE_LINES_START, ))\n        yield\n        self.data.append((FOLDABLE_LINES_END, ))\n\n    @contextlib.contextmanager\n    def non_foldable_lines(self):\n        self.data.append((FOLDABLE_LINES_END, ))\n        yield\n        self.data.append((FOLDABLE_LINES_START, ))\n\n    def mark_line_break(self):\n        self.data.append((LINE_BREAK, ))\n\n    def write(self, s, style=None):\n        st = None\n        if self.styled and style is not None and not style.empty:\n            st = style\n\n        self.data.append((DATA, str(s), st))\n\n    def header(self, s, style=None, level=1):\n        st = None\n        if self.styled and style is not None and not style.empty:\n            st = style\n\n        self.data.append((HEADER, str(s), st, level))\n\n    def flush(self):\n        data = self.data\n        self.data = None\n\n        indentation = self.indentation\n        indent_with = self.indent_with\n        indent_with_len = len(indent_with)\n        max_width = self.max_width\n\n        result = []\n        folded_mode = 0\n        offset = 0\n\n        def check_folded_fit(pos, data, width):\n            _len = 0\n            smlines = 0\n            smlines_max = 0\n\n            for item in data[pos:]:\n                code = item[0]\n                if code == FOLDABLE_LINES_START:\n                    smlines += 1\n                    smlines_max += 1\n                elif code == FOLDABLE_LINES_END:\n                    smlines -= 1\n                    if not smlines:\n                        break\n                elif code == DATA or code == HEADER:\n                    _len += len(item[1])\n                elif code == LINE_BREAK:\n                    _len += 1\n                elif code == FOLDED_SPACE:\n                    _len += 1\n\n                if _len > width:\n                    return 0\n\n            if _len < width:\n                return smlines_max\n            else:\n                return 0\n\n        for pos, item in enumerate(data):\n            el = item[0]\n\n            if el == INDENT:\n                indentation += 1\n                if not folded_mode:\n                    result.append('\\n' + indent_with * indentation)\n                    offset = indent_with_len * indentation\n            elif el == DEDENT:\n                indentation -= 1\n                if not folded_mode:\n                    result.append('\\n' + indent_with * indentation)\n                    offset = indent_with_len * indentation\n            elif el == INDENT_NO_NL:\n                indentation += 1\n            elif el == DEDENT_NO_NL:\n                indentation -= 1\n            elif el == NEW_LINE:\n                if not folded_mode:\n                    result.append('\\n' + indent_with * indentation)\n                    offset = indent_with_len * indentation\n            elif el == FOLDABLE_LINES_START:\n                if (not folded_mode) and (max_width is not None) and (\n                        max_width - offset > 20):\n                    folded_mode = check_folded_fit(\n                        pos, data, max_width - offset)\n            elif el == FOLDABLE_LINES_END:\n                if folded_mode:\n                    folded_mode -= 1\n            elif el == LINE_BREAK:\n                if folded_mode:\n                    result.append(' ')\n                    offset += 1\n                else:\n                    result.append('\\n' + indent_with * indentation)\n                    offset = indent_with_len * indentation\n            elif el == NON_FOLDED_SPACE:\n                if not folded_mode:\n                    result.append(item[1])\n            elif el == FOLDED_SPACE:\n                if folded_mode:\n                    result.append(item[1])\n            elif el == DATA or el == HEADER:\n                # ``item[1]`` -- text to output, ``item[2]`` -- its style\n                # ``item[3]`` -- its level, for headers\n                #\n                text, style = item[1], item[2]\n\n                if el == HEADER:\n                    text = ' {} '.format(text)\n                    strlevel = '=' if item[3] == 0 else '-'\n                    if self.max_width:\n                        width = self.max_width - offset\n                        text = '{{str:{strlevel}^{width:d}s}}'.format(\n                            strlevel=strlevel, width=width).format(str=text)\n                    else:\n                        text = strlevel * 4 + text + strlevel * 4\n\n                if style is None:\n                    result.append(text)\n                else:\n                    # If there's a style object - let's apply it\n                    #\n                    result.append(style.apply(text))\n                offset += len(text)\n            elif el == HEADER:\n                # ``item[1]`` -- text to output, ``item[2]`` -- its style,\n                #\n                _, text, style, _level = el\n                if item[2] is None:\n                    result.append(item[1])\n                else:\n                    # If there's a style object - let's apply it\n                    #\n                    result.append(item[2].apply(item[1]))\n                offset += len(item[1])\n            else:\n                raise AssertionError(f\"Unexpected element: {el}\")\n\n        return ''.join(result)\n\n\nclass BaseRenderer:\n    def __init__(self, *, indent_with=' ' * 4, max_width=None, styles=None):\n        self.renderers_cache = {}\n        self.buffer = Buffer(\n            max_width=max_width, styled=styles, indent_with=indent_with)\n        self.max_width = max_width\n        self.styles = styles or styles_module.StylesTable()\n\n    def _render(self, markup):\n        cls = markup.__class__\n        renderer = None\n\n        if not issubclass(cls, elements.base.Markup):\n            return self._render_unknown(markup)\n\n        try:\n            renderer = self.renderers_cache[cls]\n        except KeyError:\n            cls_name = markup.__class__._markup_name_safe\n\n            try:\n                renderer = getattr(self, '_render_{}'.format(cls_name))\n            except AttributeError:\n                for base in markup.__class__.__mro__:\n                    if issubclass(base, elements.base.Markup):\n                        try:\n                            renderer = getattr(\n                                self,\n                                '_render_{}'.format(base._markup_name_safe))\n                        except AttributeError:\n                            pass\n                        else:\n                            self.renderers_cache[cls] = renderer\n                            break\n            else:\n                self.renderers_cache[cls] = renderer\n\n        if renderer is None:\n            raise Exception('no renderer found for {!r}'.format(markup))\n\n        return renderer(markup)\n\n    def _render_header(self, str, style=None, level=1):\n        self.buffer.header(str, style=style, level=level)\n\n    def _render_unknown(self, element):\n        self.buffer.write(\n            xrepr(element, max_len=120), style=self.styles.unknown_markup)\n\n    def _render_Markup(self, element):\n        self.buffer.write(\n            xrepr(element, max_len=120), style=self.styles.unknown_markup)\n\n    def _render_OverflowBarier(self, element):\n        self.buffer.write('<...>', style=self.styles.overflow)\n\n    def _render_SerializationError(self, element):\n        self.buffer.write(\n            'Exception during serialization to markup: <{}: {}>'.format(\n                element.cls, element.text),\n            style=self.styles.serialization_error)\n\n    @classmethod\n    def renders(cls, markup, styles=None, max_width=None):\n        renderer = cls(max_width=max_width, styles=styles)\n        renderer._render(markup)\n        return renderer.buffer.flush()\n\n\nclass DocRenderer(BaseRenderer):\n    def _render_doc_Text(self, element):\n        self.buffer.write(element.text)\n\n    def _render_doc_SourceCode(self, element):\n        self.buffer.write(element.text)\n\n    def _render_doc_Marker(self, element):\n        self.buffer.write(element.text, style=self.styles.marker)\n        self.buffer.write(' ')\n\n    def _render_doc_SubNode(self, element):\n        with self.buffer.indent():\n            self._render(element.body)\n\n    def _render_doc_Section(self, element):\n        if element.title:\n            self._render_header(element.title, style=self.styles.header1)\n            self.buffer.new_line(2)\n\n        for el in element.body:\n            self._render(el)\n            self.buffer.new_line()\n\n    def _render_doc_ValueDiff(self, element):\n        self.buffer.write(element.before, style=self.styles.diff_before)\n        self.buffer.write(' | ')\n        self.buffer.write(element.after, style=self.styles.diff_after)\n        if element.comment:\n            self.buffer.write(\n                f' # {element.comment}', style=self.styles.code_comment)\n\n    def _render_doc_Diff(self, element):\n        total_lines = len(element.lines)\n        for linenum, line in enumerate(element.lines):\n            style = None\n            if line.startswith('+'):\n                style = self.styles.diff_after\n            elif line.startswith('-'):\n                style = self.styles.diff_before\n            elif line.startswith('@@'):\n                style = self.styles.diff_anno\n\n            self.buffer.write(line, style=style)\n            if linenum < total_lines - 1:\n                self.buffer.new_line()\n\n\nclass LangRenderer(BaseRenderer):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.ex_depth = 0\n\n    def _render_lang_TreeNode(self, element):\n        with self.buffer.foldable_lines():\n            self.buffer.write(element.name, style=self.styles.tree_node)\n\n            self.buffer.non_folded_space()\n            if element.id:\n                self.buffer.write(\n                    '<0x{:x}>'.format(int(element.id)), style=self.styles.id)\n                self.buffer.non_folded_space()\n            self.buffer.write('(', style=self.styles.id)\n\n            child_count = len(element.children)\n            if child_count:\n                key = lambda child: (len(child.label) if child.label else 0)\n                longest_lbl = max(element.children, key=key).label\n                padding = min(len(longest_lbl) if longest_lbl else 0, 20)\n\n                with self.buffer.indent():\n                    for idx, child in enumerate(element.children):\n                        if child.label:\n                            self.buffer.write(\n                                child.label, style=self.styles.attribute)\n                            self.buffer.non_folded_space(\n                                ' ' * (max(0, padding - len(child.label)) + 1))\n                            self.buffer.write('=')\n                            self.buffer.non_folded_space()\n\n                        self._render(child.node)\n\n                        if idx < (child_count - 1):\n                            self.buffer.write(',')\n                            self.buffer.mark_line_break()\n\n            self.buffer.write(')', style=self.styles.id)\n\n    def _render_lang_Ref(self, element):\n        self.buffer.write(\n            '<Ref {}>'.format(element.refname),\n            style=self.styles.ref)\n\n    def _render_lang_List(self, element):\n        with self.buffer.foldable_lines():\n            self.buffer.write(element.brackets[0], style=self.styles.bracket)\n\n            item_count = len(element.items)\n            if item_count:\n                with self.buffer.indent():\n                    for idx, item in enumerate(element.items):\n                        self._render(item)\n\n                        if idx < (item_count - 1):\n                            self.buffer.write(',')\n                            self.buffer.mark_line_break()\n\n            if element.trimmed:\n                self.buffer.write('...')\n\n            self.buffer.write(element.brackets[1], style=self.styles.bracket)\n\n    def _render_mapping_(self, mapping, trimmed=False):\n        self.buffer.write('{', style=self.styles.bracket)\n\n        item_count = len(mapping)\n        if item_count:\n            with self.buffer.indent():\n                for idx, (key, value) in enumerate(mapping.items()):\n                    self.buffer.write(key, style=self.styles.key)\n                    self.buffer.write(': ')\n\n                    self._render(value)\n\n                    if idx < (item_count - 1):\n                        self.buffer.write(',')\n                        self.buffer.mark_line_break()\n\n        if trimmed:\n            self.buffer.write('...')\n\n        self.buffer.write('}', style=self.styles.bracket)\n\n    def _render_lang_Dict(self, element):\n        with self.buffer.foldable_lines():\n            self._render_mapping_(element.items, trimmed=element.trimmed)\n\n    def _render_lang_Object(self, element):\n        if element.attributes or element.repr is None:\n            self.buffer.write(\n                '<{}.{} at 0x{:x}'.format(\n                    element.class_module, element.classname, element.id),\n                style=self.styles.unknown_object)\n\n            if element.attributes:\n                self.buffer.write(' ')\n                self._render_mapping_(element.attributes)\n\n            self.buffer.write('>', style=self.styles.unknown_object)\n\n        else:\n            self.buffer.write(element.repr, style=self.styles.unknown_object)\n\n    def _render_lang_String(self, element):\n        self.buffer.write(\n            xrepr(element.str, max_len=120), style=self.styles.literal)\n\n    def _render_lang_MultilineString(self, element):\n        with self.buffer.non_foldable_lines():\n            for line in element.str.splitlines():\n                self.buffer.new_line()\n                self.buffer.write(\n                    line,\n                    style=self.styles.literal\n                )\n            self.buffer.data.append((DEDENT_NO_NL, ))\n            self.buffer.new_line()\n            self.buffer.data.append((INDENT_NO_NL, ))\n\n    def _render_lang_Number(self, element):\n        self.buffer.write(element.num, style=self.styles.literal)\n\n    def _render_lang_NoneConstantType(self, element):\n        self.buffer.write('None', self.styles.constant)\n\n    def _render_lang_TrueConstantType(self, element):\n        self.buffer.write('True', self.styles.constant)\n\n    def _render_lang_FalseConstantType(self, element):\n        self.buffer.write('False', self.styles.constant)\n\n    def _render_lang_TracebackPoint(self, element):\n        with self.buffer.indent(False):\n            self.buffer.new_line()\n\n            self.buffer.write(element.filename, style=self.styles.tb_filename)\n            if element.lineno:\n                self.buffer.write(', line ')\n                self.buffer.write(element.lineno, style=self.styles.tb_lineno)\n            if element.address:\n                self.buffer.write(', at ')\n                self.buffer.write(element.address, style=self.styles.tb_lineno)\n            self.buffer.write(', in ')\n            self.buffer.write(element.name, style=self.styles.tb_name)\n\n            with self.buffer.indent(False):\n                self.buffer.new_line()\n\n                if element.lines and element.line_numbers:\n                    for lineno, line in zip(\n                            element.line_numbers, element.lines):\n                        if lineno == element.lineno:\n                            if element.context:\n                                stripped_spaces = 0\n                                stripped_line = line\n                            else:\n                                stripped_spaces = len(line) - len(\n                                    line.lstrip())\n                                stripped_line = line.strip()\n\n                            self.buffer.write(\n                                '> ', style=self.styles.tb_current_line)\n                            self.buffer.write(\n                                stripped_line or '???',\n                                style=self.styles.tb_code)\n\n                            if element.colno:\n                                # Render column caret\n                                _caret_indent = ' ' * (\n                                    element.colno - stripped_spaces)\n                                self.buffer.new_line()\n                                self.buffer.write(' ', style=self.styles.code)\n                                self.buffer.write(\n                                    _caret_indent + '^',\n                                    style=self.styles.tb_pos_caret)\n                                if element.end_colno is not None:\n                                    cnt = element.end_colno - element.colno - 1\n                                    self.buffer.write(\n                                        '^' * cnt,\n                                        style=self.styles.tb_pos_caret)\n\n                                self.buffer.new_line()\n                            if not element.context:\n                                break\n                        elif element.context:\n                            self.buffer.write('| ', style=self.styles.code)\n                            self.buffer.write(\n                                line.rstrip(), style=self.styles.code)\n                            self.buffer.new_line()\n                    else:\n                        if not element.context:\n                            self.buffer.write('???', style=self.styles.tb_code)\n\n                if element.locals:\n                    self.buffer.new_line(2)\n                    self.buffer.write('Locals: ', style=self.styles.attribute)\n                    self._render(element.locals)\n                    self.buffer.new_line()\n\n    def _render_lang_Traceback(self, element):\n        for item in element.items:\n            self._render(item)\n\n    def _render_lang_ExceptionContext(self, element):\n        self.buffer.new_line(2)\n        self._render_header(element.title, level=2, style=self.styles.header2)\n        self.buffer.new_line()\n\n        if element.body:\n            for el in element.body:\n                self._render(el)\n\n    def _render_lang_Exception(self, element):\n        self.ex_depth += 1\n        try:\n            if self.ex_depth == 1:\n                msg = 'Exception occurred'\n                if element.msg:\n                    msg = '{}: {}'.format(msg, element.msg)\n\n                self._render_header(msg, style=self.styles.header1)\n                self.buffer.new_line(2)\n\n            if (element.cause or element.context) is not None:\n                if element.cause is None:\n                    self._render(element.context)\n                    msg = ('During handling of the above exception, '\n                           'another exception occurred')\n                else:\n                    self._render(element.cause)\n                    msg = ('The above exception was the direct cause '\n                           'of the following exception')\n\n                self.buffer.new_line(2)\n                self._render_header(msg, style=self.styles.header1)\n                self.buffer.new_line(2)\n\n            if element.class_module == 'builtins':\n                excclass = element.classname\n            else:\n                excclass = '{}.{}'.format(\n                    element.class_module, element.classname)\n            base_excline = '{}: {}'.format(excclass, element.msg)\n            self.buffer.write(\n                '{}. {}'.format(self.ex_depth, base_excline),\n                style=self.styles.exc_title)\n\n            if element.contexts:\n                for context in element.contexts:\n                    self._render(context)\n\n                self.buffer.new_line()\n\n            self.buffer.new_line()\n            self.buffer.write(base_excline, style=self.styles.exc_title)\n        finally:\n            self.ex_depth -= 1\n\n\nclass CodeRenderer(BaseRenderer):\n    def _write_code_token(self, val, style):\n        parts = val.split('\\n')\n        for chunk in parts[:-1]:\n            self.buffer.write(chunk, style=style)\n            self.buffer.new_line()\n        self.buffer.write(parts[-1], style=style)\n\n    def _render_code_Token(self, element):\n        self._write_code_token(element.val, style=self.styles.code)\n\n    def _render_code_Comment(self, element):\n        self._write_code_token(element.val, style=self.styles.code_comment)\n\n    def _render_code_Decorator(self, element):\n        self._write_code_token(element.val, style=self.styles.code_decorator)\n\n    def _render_code_String(self, element):\n        self._write_code_token(element.val, style=self.styles.code_string)\n\n    def _render_code_Number(self, element):\n        self._write_code_token(element.val, style=self.styles.code_number)\n\n    def _render_code_ClassName(self, element):\n        self._write_code_token(element.val, style=self.styles.code_decl_name)\n\n    def _render_code_FunctionName(self, element):\n        self._write_code_token(element.val, style=self.styles.code_decl_name)\n\n    def _render_code_Constant(self, element):\n        self._write_code_token(element.val, style=self.styles.code_constant)\n\n    def _render_code_Keyword(self, element):\n        self._write_code_token(element.val, style=self.styles.code_keyword)\n\n    def _render_code_Punctuation(self, element):\n        self._write_code_token(element.val, style=self.styles.code_punctuation)\n\n    def _render_code_Tag(self, element):\n        self._write_code_token(element.val, style=self.styles.code_tag)\n\n    def _render_code_Attribute(self, element):\n        self._write_code_token(element.val, style=self.styles.code_attribute)\n\n    def _render_code_Code(self, element):\n        if len(element.tokens) > 20:\n            with self.buffer.indent():\n                for token in element.tokens:\n                    self._render(token)\n        else:\n            for token in element.tokens:\n                self._render(token)\n\n\nclass Renderer(DocRenderer, LangRenderer, CodeRenderer):\n    pass\n\n\nrenders = Renderer.renders\n\n\ndef render(markup, *, ensure_newline=True, file=None, renderer=Renderer):\n    if file is None:\n        file = sys.stdout\n\n    try:\n        fileno = file.fileno()\n    except OSError:\n        # This is a hack to try to get nice colorized dump output over\n        # a remote-pdb connection. If the output is redirected to\n        # something without fileno, use what ought to be stdout's fileno\n        # to decide on color, etc.\n        fileno = 1\n    max_width = term.size(fileno)[1]\n\n    style_table = None\n    if term.use_colors(fileno):\n        max_colors = term.max_colors()\n        if max_colors > 255:\n            style_table = styles_module.Dark256\n        elif max_colors > 6:\n            style_table = styles_module.Dark16\n\n    rendered = renderer.renders(\n        markup, styles=style_table, max_width=max_width)\n    if ensure_newline and not rendered.endswith('\\n'):\n        rendered += '\\n'\n\n    print(rendered, file=file, end='')\n"
  },
  {
    "path": "edb/common/markup/serializer/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n\nclass settings:\n    censor_sensitive_vars = True\n    censor_list = ['secret', 'password']\n\n\nfrom .base import serialize, serializer, serialize_traceback_point  # NOQA\nfrom .base import Context  # NOQA\nfrom .base import no_ref_detect  # NOQA\nfrom .code import serialize_code  # NOQA\nfrom . import logging  # NOQA\n"
  },
  {
    "path": "edb/common/markup/serializer/base.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport collections\nimport collections.abc\nimport decimal\nimport functools\nimport types\nimport weakref\n\nfrom .. import elements\n\nfrom edb.common import exceptions\nfrom edb.common.markup.format import xrepr\nfrom edb.common import debug\n\nfrom . import settings\n\n#: Maximum level of nested structures that we can serialize.\n#: If we reach it - we'll just stop traversing the objects\n#: tree at that point and yield 'elements.base.OverflowBarier'\n#:\nOVERFLOW_BARIER = 100  # XXX Configurable?\n\n#: Maximum number of total 'serialize' calls.\n#: If we reach it - we'll just stop traversing the objects\n#: tree at that point and yield 'elements.base.OverflowBarier'\n#:\nRUN_OVERFLOW_BARIER = 5000  # XXX Configurable?\n\n__all__ = 'serialize',\n\n\ndef no_ref_detect[T](func: T) -> T:\n    \"\"\"Serializer decorated with ``no_ref_detect`` will be executed without\n    prior checking the memo if object was already serialized\"\"\"\n\n    func.no_ref_detect = True  # type: ignore\n    return func\n\n\n@functools.singledispatch\ndef serializer(obj, *, ctx):\n    \"\"\"Markup serializers dispatcher\"\"\"\n    raise NotImplementedError\n\n\nclass Context:\n    \"\"\"Markup serialization context.  Holds the ``memo`` set, which\n    is used to avoid serializing objects that already have been serialized,\n    and ``depth`` - recursion depth\"\"\"\n\n    def __init__(self, trim=True, kwargs=None):\n        self.reset()\n        self.trim = trim\n        self.kwargs = kwargs or {}\n        if settings.censor_sensitive_vars:\n            self.censor_set = set(settings.censor_list)\n        else:\n            self.censor_set = set()\n\n    def censored(self, key):\n        return key in self.censor_set\n\n    def reset(self):\n        self.memo = set()\n        self.keep_alive = []\n        self.level = 0\n        self.run_cnt = 0\n\n\ndef serialize(obj, *, ctx):\n    \"\"\"Serialize arbitrary python object to Markup elements\"\"\"\n\n    tobj = type(obj)\n\n    sr = serializer.dispatch(tobj)\n    if sr is serializer:\n        raise LookupError(f'unable to find serializer for object {obj!r}')\n\n    if (sr is serialize_unknown_object and\n            hasattr(tobj, '__dataclass_fields__')):\n        sr = serialize_dataclass\n\n    ctx.level += 1\n    ctx.run_cnt += 1\n    try:\n        if ctx.level >= OVERFLOW_BARIER or ctx.run_cnt >= RUN_OVERFLOW_BARIER:\n            return elements.base.OverflowBarier()\n\n        ref_detect = True\n        try:\n            # Was the serializer decorated with ``@no_ref_detect``?\n            #\n            ref_detect = not sr.no_ref_detect\n        except AttributeError:\n            pass\n\n        if ref_detect:\n            # OK, so if we've already serialized obj, don't do that again, just\n            # return ``markup.Ref`` element.\n            #\n            obj_id = id(obj)\n            if obj_id in ctx.memo:\n                return elements.lang.Ref(ref=obj_id, refname=repr(obj))\n            else:\n                ctx.memo.add(obj_id)\n                ctx.keep_alive.append(obj)\n\n        try:\n            return sr(obj, ctx=ctx)\n        except Exception as ex:\n            return elements.base.SerializationError(\n                text=str(ex), cls='{}.{}'.format(\n                    ex.__class__.__module__, ex.__class__.__name__))\n    finally:\n        ctx.level -= 1\n\n\n@no_ref_detect\ndef _serialize_traceback_point(\n    obj,\n    frame,\n    lineno,\n    *,\n    ctx,\n    include_source=True,\n    source_window_size=2,\n    include_locals=False,\n    point_cls=elements.lang.TracebackPoint,\n):\n    assert source_window_size >= 0\n\n    name = frame.f_code.co_name\n    filename = frame.f_code.co_filename\n\n    locals = None\n    if include_locals or debug.flags.print_locals:\n        locals = serialize(dict(frame.f_locals), ctx=ctx)\n\n    if filename.startswith('.'):\n        frame_fn = frame.f_globals.get('__file__')\n        if frame_fn and frame_fn.endswith(filename[2:]):\n            filename = frame_fn\n\n    point = point_cls(\n        name=name, lineno=lineno, filename=filename, locals=locals, id=id(obj))\n\n    if include_source:\n        point.load_source(window=source_window_size)\n\n    return point\n\n\n@no_ref_detect\ndef serialize_traceback_point(\n    obj,\n    *,\n    ctx,\n    include_source=True,\n    source_window_size=2,\n    include_locals=False,\n    point_cls=elements.lang.TracebackPoint,\n):\n    assert isinstance(obj, types.TracebackType)\n\n    return _serialize_traceback_point(\n        obj, obj.tb_frame, obj.tb_lineno, ctx=ctx,\n        include_source=include_source, source_window_size=source_window_size,\n        include_locals=include_locals, point_cls=point_cls)\n\n\n@no_ref_detect\ndef serialize_callstack_point(\n    obj,\n    *,\n    ctx,\n    include_source=True,\n    source_window_size=2,\n    include_locals=False,\n    point_cls=elements.lang.TracebackPoint,\n):\n    assert isinstance(obj, types.FrameType)\n\n    return _serialize_traceback_point(\n        obj, obj, obj.f_lineno, ctx=ctx, include_source=include_source,\n        source_window_size=source_window_size, include_locals=include_locals,\n        point_cls=point_cls)\n\n\n@serializer.register(types.TracebackType)\ndef serialize_traceback(obj, *, ctx):\n    result = []\n\n    current = obj\n    while current is not None:\n        result.append(serialize_traceback_point(current, ctx=ctx))\n        current = current.tb_next\n\n    return elements.lang.Traceback(items=result, id=id(obj))\n\n\n@serializer.register(BaseException)\ndef serialize_exception(obj, *, ctx):\n    cause = context = None\n    if obj.__cause__ is not None and obj.__cause__ is not obj:\n        cause = serialize(obj.__cause__, ctx=ctx)\n    elif (\n            not obj.__suppress_context__ and obj.__context__ is not None and\n            obj.__context__ is not obj):\n        context = serialize(obj.__context__, ctx=ctx)\n\n    details_context = None\n    contexts = []\n    for ex_context in exceptions.iter_contexts(obj):\n        if isinstance(ex_context, exceptions.DefaultExceptionContext):\n            details_context = ex_context\n        else:\n            contexts.append(serialize(ex_context, ctx=ctx))\n\n    obj_traceback = obj.__traceback__\n    if obj_traceback:\n        traceback = elements.lang.ExceptionContext(\n            title='Traceback', body=[serialize(obj_traceback, ctx=ctx)])\n\n        if isinstance(obj, SyntaxError):\n            point = elements.lang.TracebackPoint(\n                name='<parser>', lineno=obj.lineno, colno=obj.offset,\n                filename=obj.filename or '<buffer>')\n            point.load_source()\n            traceback.body[0].items.append(point)\n\n        contexts.append(traceback)\n\n    if details_context is not None:\n        contexts.append(serialize(details_context, ctx=ctx))\n\n    markup = elements.lang.Exception(\n        class_module=obj.__class__.__module__,\n        classname=obj.__class__.__name__, msg=str(obj), contexts=contexts,\n        cause=cause, context=context, id=id(obj))\n\n    if isinstance(obj, BaseExceptionGroup):\n        markup = elements.doc.Section(\n            body=[\n                markup,\n                elements.doc.Section(\n                    title='Grouped exceptions',\n                    body=[\n                        elements.doc.SubNode(body=serializer(sub, ctx=ctx))\n                        for sub in obj.exceptions\n                    ]\n                )\n            ],\n        )\n\n    return markup\n\n\n@serializer.register(exceptions.ExceptionContext)\ndef serialize_generic_exception_context(obj, *, ctx):\n    msg = 'No markup serializer for {!r} context'.format(obj)\n    return elements.lang.ExceptionContext(\n        title=obj.title, body=[elements.doc.Text(text=msg)])\n\n\n@serializer.register(exceptions.DefaultExceptionContext)\ndef serialize_default_exception_context(obj, *, ctx):\n    body = []\n\n    if obj.details:\n        txt = elements.doc.Text(text='Details: {}'.format(obj.details))\n        body.append(elements.doc.Section(body=[txt]))\n\n    if obj.hint:\n        txt = elements.doc.Text(text='Hint: {}'.format(obj.hint))\n        body.append(elements.doc.Section(body=[txt]))\n\n    return elements.lang.ExceptionContext(title=obj.title, body=body)\n\n\n@serializer.register(type(None))\n@no_ref_detect\ndef serialize_none(obj, *, ctx):\n    return elements.lang.Constants.none\n\n\n@serializer.register(bool)\n@no_ref_detect\ndef serialize_bool(obj, *, ctx):\n    if obj:\n        return elements.lang.Constants.true\n    else:\n        return elements.lang.Constants.false\n\n\n@serializer.register(int)\n@serializer.register(float)\n@serializer.register(decimal.Decimal)\n@no_ref_detect\ndef serialize_number(obj, *, ctx):\n    return elements.lang.Number(num=obj)\n\n\n@serializer.register(str)\n@no_ref_detect\ndef serialize_str(obj, *, ctx):\n    return elements.lang.String(str=obj)\n\n\n@serializer.register(collections.UserList)\n@serializer.register(list)\n@serializer.register(tuple)\n@serializer.register(collections.abc.Set)\n@serializer.register(weakref.WeakSet)\n@serializer.register(set)\n@serializer.register(frozenset)\n@no_ref_detect\ndef serialize_sequence(obj, *, ctx, trim_at=100):\n    els = []\n    cnt = 0\n    trim = ctx.trim\n\n    if isinstance(obj, tuple):\n        brackets = \"()\"\n    elif isinstance(obj,\n                    (collections.abc.Set, weakref.WeakSet, set, frozenset)):\n        brackets = \"{}\"\n    else:\n        brackets = \"[]\"\n\n    for cnt, item in enumerate(obj):\n        els.append(serialize(item, ctx=ctx))\n        if trim and cnt >= trim_at:\n            break\n    return elements.lang.List(\n        items=els, id=id(obj), brackets=brackets,\n        trimmed=(trim and cnt >= trim_at))\n\n\n@serializer.register(dict)\n@serializer.register(collections.abc.Mapping)\n@no_ref_detect\ndef serialize_mapping(obj, *, ctx, trim_at=100):\n    map = collections.OrderedDict()\n    cnt = 0\n    trim = ctx.trim\n    for cnt, (key, value) in enumerate(obj.items()):\n        if not isinstance(key, str):\n            key = repr(key)\n        if ctx.censored(key) and value is not None:\n            value = '********'\n        map[key] = serialize(value, ctx=ctx)\n        if trim and cnt >= trim_at:\n            break\n    return elements.lang.Dict(\n        items=map, id=id(obj), trimmed=(trim and cnt >= trim_at))\n\n\ndef serialize_dataclass(obj, *, ctx):\n    fields = type(obj).__dataclass_fields__\n\n    node = elements.lang.TreeNode(\n        id=id(obj),\n        name=f'{type(obj).__name__}')\n\n    for fieldname, field in fields.items():\n        try:\n            val = getattr(obj, fieldname)\n        except AttributeError:\n            continue\n\n        if not field.repr:\n            continue\n\n        node.add_child(\n            label=fieldname,\n            node=serialize(val, ctx=ctx))\n\n    return node\n\n\n@serializer.register(object)\n@no_ref_detect\ndef serialize_unknown_object(obj, *, ctx):\n    return elements.lang.Object(\n        id=id(obj), class_module=type(obj).__module__,\n        classname=type(obj).__name__, repr=xrepr(obj, max_len=200))\n\n\ndef _serialize_known_object(obj, attrs, *, ctx):\n    map = collections.OrderedDict()\n    for attr in attrs:\n        map[attr] = serialize(getattr(obj, attr, None), ctx=ctx)\n    return elements.lang.Object(\n        id=id(obj), class_module=obj.__class__.__module__,\n        classname=obj.__class__.__name__, attributes=map)\n"
  },
  {
    "path": "edb/common/markup/serializer/code.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom edb.common.markup.elements import code as code_el\n\ntry:\n    from pygments import token, lexers\n\nexcept ImportError:\n    # No pygments\n\n    def serialize_code(code, lexer='does not matter'):\n        return code_el.Code(tokens=[code_el.Token(val=code)])\n\nelse:\n    import functools\n\n    _TOKEN_MAP = {\n        token.Token: code_el.Token,\n        token.Whitespace: code_el.Whitespace,\n        token.Comment: code_el.Comment,\n        token.Keyword: code_el.Keyword,\n        token.Keyword.Type: code_el.Type,\n        token.Keyword.Constant: code_el.Constant,\n        token.Operator: code_el.Operator,\n        token.Operator.Word: code_el.Keyword,\n        token.Name: code_el.Name,\n        token.Name.Builtin: code_el.BuiltinName,\n        token.Name.Function: code_el.FunctionName,\n        token.Name.Class: code_el.ClassName,\n        token.Name.Constant: code_el.Constant,\n        token.Name.Decorator: code_el.Decorator,\n        token.Name.Attribute: code_el.Attribute,\n        token.Name.Tag: code_el.Tag,\n        token.Name.Builtin.Pseudo: code_el.Constant,\n        token.Punctuation: code_el.Punctuation,\n        token.String: code_el.String,\n        token.Number: code_el.Number,\n        token.Error: code_el.Error\n    }\n\n    @functools.lru_cache(100)\n    def get_code_class(token_type):\n        cls = _TOKEN_MAP.get(token_type)\n        while cls is None:\n            token_type = token_type[:-1]\n            cls = _TOKEN_MAP.get(token_type)\n\n        if cls is None:\n            cls = code_el.Token\n\n        return cls\n\n    class MarkupFormatter:\n        def format(self, tokens):\n            result = []\n\n            for token_type, value in tokens:\n                cls = get_code_class(token_type)\n                result.append(cls(val=value))\n\n            return code_el.Code(tokens=result)\n\n    def serialize_code(code, lexer='python'):\n        lexer = lexers.get_lexer_by_name(lexer, stripall=True)\n        return MarkupFormatter().format(lexer.get_tokens(code))\n"
  },
  {
    "path": "edb/common/markup/serializer/logging.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport logging\n\nfrom . import base\n\n\n@base.serializer.register(logging.LogRecord)  # type: ignore\ndef serialize_logging_record(obj, *, ctx):\n    return base._serialize_known_object(\n        obj, (attr for attr in dir(obj) if not attr.startswith('_')), ctx=ctx)\n"
  },
  {
    "path": "edb/common/ordered.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    Hashable,\n    Iterable,\n    Iterator,\n    MutableSet,\n)\n\nimport collections\nimport collections.abc\n\n\nclass OrderedSet[K: Hashable](MutableSet[K]):\n\n    map: dict[K, None]\n\n    def __init__(self, iterable: Optional[Iterable[K]] = None) -> None:\n        if iterable is not None:\n            self.map = {v: None for v in iterable}\n        else:\n            self.map = {}\n\n    def add(self, item: K) -> None:\n        self.map[item] = None\n\n    def discard(self, item: K) -> None:\n        self.map.pop(item, None)\n\n    def update(self, iterable: Iterable[K]) -> None:\n        for item in iterable:\n            self.map[item] = None\n\n    def replace(self, existing: K, new: K) -> None:\n        if existing not in self.map:\n            raise LookupError(f'{existing!r} is not in set')\n        self.map[existing] = None\n\n    difference_update = collections.abc.MutableSet.__isub__\n    symmetric_difference_update = collections.abc.MutableSet.__ixor__\n    intersection_update = collections.abc.MutableSet.__iand__\n\n    def __len__(self) -> int:\n        return len(self.map)\n\n    def __contains__(self, item: Any) -> bool:\n        return item in self.map\n\n    def __iter__(self) -> Iterator[K]:\n        return iter(self.map)\n\n    def __reversed__(self) -> Iterator[K]:\n        return reversed(self.map.keys())\n\n    def __repr__(self) -> str:\n        if not self:\n            return '%s()' % (self.__class__.__name__, )\n        return '%s(%r)' % (self.__class__.__name__, list(self))\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, self.__class__):\n            return len(self) == len(other) and self.map == other.map\n        elif other is None:\n            return False\n        else:\n            return not self.isdisjoint(other)\n\n    def copy(self) -> OrderedSet[K]:\n        return self.__class__(self)\n\n    def clear(self) -> None:\n        self.map.clear()\n"
  },
  {
    "path": "edb/common/ordered.pyi",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"\nThis stub file is needed so that __and__, __or__, __sub__, __xor__, and\nso on properly return the instance of the *current class*, not the abstract\nversions.\n\"\"\"\nfrom __future__ import annotations\n\nfrom typing import (\n    AbstractSet,\n    Any,\n    Hashable,\n    Iterable,\n    Iterator,\n    MutableSet,\n    Optional,\n)\n\nclass OrderedSet[_H: Hashable](MutableSet[_H]):\n    def __init__(self, iterable: Optional[Iterable[_H]] = None) -> None: ...\n    def __and__(self, s: AbstractSet[Any]) -> OrderedSet[_H]: ...\n    def __or__[_T](self, s: AbstractSet[_T]) -> OrderedSet[_H | _T]: ...\n    def __sub__(self, s: AbstractSet[Any]) -> OrderedSet[_H]: ...\n    def __xor__[_T](self, s: AbstractSet[_T]) -> OrderedSet[_H | _T]: ...\n    def __ior__[_S](self, s: AbstractSet[_S]) -> OrderedSet[_H | _S]: ...\n    def __iand__[_T](self, s: AbstractSet[Any]) -> OrderedSet[_T]: ...\n    def __ixor__[_S](self, s: AbstractSet[_S]) -> OrderedSet[_H | _S]: ...\n    def __isub__[_T](self, s: AbstractSet[Any]) -> OrderedSet[_H]: ...\n    difference_update = MutableSet.__isub__\n    symmetric_difference_update = MutableSet.__ixor__\n    intersection_update = MutableSet.__iand__\n    def add(self, item: _H) -> None: ...\n    def discard(self, item: _H) -> None: ...\n    def update(self, s: Iterable[_H]) -> None: ...\n    def replace(self, existing: _H, new: _H) -> None: ...\n    def __len__(self) -> int: ...\n    def __contains__(self, item: Any) -> bool: ...\n    def __iter__(self) -> Iterator[_H]: ...\n    def __reversed__(self) -> Iterator[_H]: ...\n    def copy(self) -> OrderedSet[_H]: ...\n    def clear(self) -> None: ...\n"
  },
  {
    "path": "edb/common/parametric.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    ClassVar,\n    Generic,\n    Optional,\n    TypeVar,\n    get_type_hints,\n)\n\nimport functools\nimport types\nimport sys\n\nfrom edb.common import typing_inspect\n\n\n__all__ = [\n    \"ParametricType\",\n    \"SingleParametricType\",\n    \"KeyValueParametricType\",\n]\n\n\nT = TypeVar(\"T\")\nV = TypeVar(\"V\")\n\n\ntry:\n    from types import GenericAlias\nexcept ImportError:\n    from typing import _GenericAlias as GenericAlias  # type: ignore\n\n\nclass ParametricType:\n\n    types: ClassVar[Optional[tuple[type, ...]]] = None\n    orig_args: ClassVar[Optional[tuple[type, ...]]] = None\n    _forward_refs: ClassVar[dict[str, tuple[int, str]]] = {}\n    _type_param_map: ClassVar[dict[Any, str]] = {}\n    _non_type_params: ClassVar[dict[int, type]] = {}\n\n    def __init_subclass__(cls) -> None:\n        super().__init_subclass__()\n\n        if cls.types is not None:\n            return\n        elif ParametricType in cls.__bases__:\n            cls._init_parametric_base()\n        elif any(issubclass(b, ParametricType) for b in cls.__bases__):\n            cls._init_parametric_user()\n\n    @classmethod\n    def _init_parametric_base(cls) -> None:\n        \"\"\"Initialize a direct subclass of ParametricType\"\"\"\n\n        # Direct subclasses of ParametricType must declare\n        # ClassVar attributes corresponding to the Generic type vars.\n        # For example:\n        #     class P(ParametricType, Generic[T, V]):\n        #         t: ClassVar[Type[T]]\n        #         v: ClassVar[Type[V]]\n\n        params = getattr(cls, '__parameters__', None)\n\n        if not params:\n            raise TypeError(\n                f'{cls} must be declared as Generic'\n            )\n\n        mod = sys.modules[cls.__module__]\n        annos = get_type_hints(cls, mod.__dict__)\n        param_map = {}\n\n        for attr, t in annos.items():\n            if not typing_inspect.is_classvar(t):\n                continue\n\n            args = typing_inspect.get_args(t)\n            # ClassVar constructor should have the check, but be extra safe.\n            assert len(args) == 1\n\n            arg = args[0]\n            if typing_inspect.get_origin(arg) is not type:\n                continue\n\n            arg_args = typing_inspect.get_args(arg)\n            # Likewise, rely on Type checking its stuff in the constructor\n            assert len(arg_args) == 1\n\n            if not typing_inspect.is_typevar(arg_args[0]):\n                continue\n\n            if arg_args[0] in params:\n                param_map[arg_args[0]] = attr\n\n        for param in params:\n            if param not in param_map:\n                raise TypeError(\n                    f'{cls.__name__}: missing ClassVar for'\n                    f' generic parameter {param}'\n                )\n\n        cls._type_param_map = param_map\n\n    @classmethod\n    def _init_parametric_user(cls) -> None:\n        \"\"\"Initialize an indirect descendant of ParametricType.\"\"\"\n\n        # For ParametricType grandchildren we have to deal with possible\n        # TypeVar remapping and generally check for type sanity.\n\n        ob = getattr(cls, '__orig_bases__', ())\n        generic_params: list[type] = []\n\n        for b in ob:\n            if (\n                isinstance(b, type)\n                and not isinstance(b, GenericAlias)\n                and issubclass(b, ParametricType)\n                and b is not ParametricType\n            ):\n                raise TypeError(\n                    f'{cls.__name__}: missing one or more type arguments for'\n                    f' base {b.__name__!r}'\n                )\n\n            if not typing_inspect.is_generic_type(b):\n                continue\n\n            org = typing_inspect.get_origin(b)\n            if not isinstance(org, type):\n                continue\n            if not issubclass(org, ParametricType):\n                generic_params.extend(getattr(b, '__parameters__', ()))\n                continue\n\n            base_params = getattr(org, '__parameters__', ())\n            base_non_type_params = getattr(org, '_non_type_params', {})\n            args = typing_inspect.get_args(b)\n            expected = len(base_params)\n            if len(args) != expected:\n                raise TypeError(\n                    f'{b.__name__} expects {expected} type arguments'\n                    f' got {len(args)}'\n                )\n\n            base_map = dict(cls._type_param_map)\n            subclass_map = {}\n\n            for i, arg in enumerate(args):\n                if i in base_non_type_params:\n                    continue\n                if not typing_inspect.is_typevar(arg):\n                    raise TypeError(\n                        f'{b.__name__} expects all arguments to be'\n                        f' TypeVars'\n                    )\n\n                base_typevar = base_params[i]\n                attr = base_map.get(base_typevar)\n                if attr is not None:\n                    subclass_map[arg] = attr\n\n            if len(subclass_map) != len(base_map):\n                raise TypeError(\n                    f'{cls.__name__}: missing one or more type arguments for'\n                    f' base {org.__name__!r}'\n                )\n\n            cls._type_param_map = subclass_map\n\n        cls._non_type_params = {\n            i: p for i, p in enumerate(generic_params)\n            if p not in cls._type_param_map\n        }\n\n    def __init__(self) -> None:\n        if self._forward_refs:\n            raise TypeError(\n                f\"{type(self)!r} unresolved type parameters\"\n            )\n        if self.types is None:\n            raise TypeError(\n                f\"{type(self)!r} must be parametrized to instantiate\"\n            )\n\n        super().__init__()\n\n    @classmethod\n    @functools.lru_cache()\n    def __class_getitem__(\n        cls, params: type | str | tuple[type | str, ...]\n    ) -> type[ParametricType]:\n        \"\"\"Return a dynamic subclass parametrized with `params`.\n\n        We cannot use `_GenericAlias` provided by `Generic[T]` because the\n        default `__class_getitem__` on `_GenericAlias` is not a real type and\n        so it doesn't retain information on generics on the class.  Even on\n        the object, it adds the relevant `__orig_class__` link too late, after\n        `__init__()` is called.  That means we wouldn't be able to type-check\n        in the initializer using built-in `Generic[T]`.\n        \"\"\"\n        if cls.types is not None:\n            raise TypeError(f\"{cls!r} is already parametrized\")\n\n        if not isinstance(params, tuple):\n            params = (params,)\n        all_params = params\n        type_params = []\n        for i, param in enumerate(all_params):\n            if i not in cls._non_type_params:\n                type_params.append(param)\n        params_str = \", \".join(_type_repr(a) for a in all_params)\n        name = f\"{cls.__name__}[{params_str}]\"\n        bases = (cls,)\n        type_dict: dict[str, Any] = {\n            \"types\": tuple(type_params),\n            \"orig_args\": all_params,\n            \"__module__\": cls.__module__,\n        }\n        forward_refs: dict[str, tuple[int, str]] = {}\n        tuple_to_attr: dict[int, str] = {}\n\n        if cls._type_param_map:\n            gen_params = getattr(cls, '__parameters__', ())\n            for i, gen_param in enumerate(gen_params):\n                attr = cls._type_param_map.get(gen_param)\n                if attr:\n                    tuple_to_attr[i] = attr\n\n            expected = len(gen_params)\n            actual = len(params)\n            if expected != actual:\n                raise TypeError(\n                    f\"type {cls.__name__!r} expects {expected} type\"\n                    f\" parameter{'s' if expected != 1 else ''},\"\n                    f\" got {actual}\"\n                )\n\n            for i, attr in tuple_to_attr.items():\n                type_dict[attr] = all_params[i]\n\n        if not all(isinstance(param, type) for param in type_params):\n            if all(\n                type(param) is TypeVar  # type: ignore[comparison-overlap]\n                for param in type_params\n            ):\n                # All parameters are type variables: return the regular generic\n                # alias to allow proper subclassing.\n                generic = super(ParametricType, cls)\n                return generic.__class_getitem__(all_params)  # type: ignore\n            else:\n                forward_refs = {\n                    param: (i, tuple_to_attr[i])\n                    for i, param in enumerate(type_params)\n                    if isinstance(param, str)\n                }\n\n                if not forward_refs:\n                    raise TypeError(\n                        f\"{cls!r} expects types as type parameters\")\n\n        result = type(name, bases, type_dict)\n        assert issubclass(result, ParametricType)\n        result._forward_refs = forward_refs\n        return result\n\n    @classmethod\n    def is_fully_resolved(cls) -> bool:\n        return not cls._forward_refs\n\n    @classmethod\n    def resolve_types(cls, globalns: dict[str, Any]) -> None:\n        if cls.types is None:\n            raise TypeError(\n                f\"{cls!r} is not parametrized\"\n            )\n\n        if not cls._forward_refs:\n            return\n\n        types = list(cls.types)\n\n        for ut, (idx, attr) in cls._forward_refs.items():\n            t = eval(ut, globalns, {})\n            if isinstance(t, type) and not isinstance(t, GenericAlias):\n                types[idx] = t\n                setattr(cls, attr, t)\n            else:\n                raise TypeError(\n                    f\"{cls!r} expects types as type parameters, got {t!r:.100}\"\n                )\n\n        cls.types = tuple(types)\n        cls._forward_refs = {}\n\n    @classmethod\n    def is_anon_parametrized(cls) -> bool:\n        return cls.__name__.endswith(']')\n\n    def __reduce__(self) -> tuple[Any, ...]:\n        raise NotImplementedError(\n            'must implement explicit __reduce__ for ParametricType subclass'\n        )\n\n\nclass SingleParametricType(ParametricType, Generic[T]):  # noqa: UP046\n    # We ignore UP046 (Generic[T]) because Python 3.12.2 typing.get_type_hints\n    # has problems with resolving `T` when it is defined using the new syntax.\n\n    type: ClassVar[type[T]]  # type: ignore\n\n\nclass KeyValueParametricType(ParametricType, Generic[T, V]):  # noqa: UP046\n    # We ignore UP046 (Generic[T]) because Python 3.12.2 typing.get_type_hints\n    # has problems with resolving `T` when it is defined using the new syntax.\n\n    keytype: ClassVar[type[T]]  # type: ignore\n    valuetype: ClassVar[type[V]]  # type: ignore\n\n\ndef _type_repr(obj: Any) -> str:\n    if isinstance(obj, type):\n        if obj.__module__ == \"builtins\":\n            return obj.__qualname__\n        return f\"{obj.__module__}.{obj.__qualname__}\"\n    if isinstance(obj, types.FunctionType):\n        return obj.__name__\n    return repr(obj)\n"
  },
  {
    "path": "edb/common/parsing.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2010-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any, Callable, Optional, cast\n)\n\nimport json\nimport logging\nimport os\nimport sys\nimport types\n\nimport parsing\n\nfrom edb.common import debug, span\n\nSpan = span.Span\n\nlogger = logging.getLogger('edb.common.parsing')\n\n\nclass ParserSpecIncompatibleError(Exception):\n    pass\n\n\nclass Token(parsing.Token):\n    token_map: dict[Any, Any] = {}\n    _token: str = \"\"\n\n    def __init_subclass__(\n        cls, *, token=None, lextoken=None, is_internal=False, **kwargs\n    ):\n        super().__init_subclass__(**kwargs)\n\n        if is_internal:\n            return\n\n        if token is None:\n            if not cls.__name__.startswith('T_'):\n                raise Exception(\n                    'Token class names must either start with T_ or have '\n                    'a token parameter')\n            token = cls.__name__[2:]\n\n        if lextoken is None:\n            lextoken = token\n\n        cls._token = token\n        Token.token_map[lextoken] = cls\n\n        if not cls.__doc__:\n            doc = '%%token %s' % token\n\n            prec = Precedence.for_token(token)\n            if prec:\n                doc += ' [%s]' % prec.__name__\n\n            cls.__doc__ = doc\n\n    def __init__(self, val, clean_value, span=None):\n        super().__init__()\n        self.val = val\n        self.clean_value = clean_value\n        self.span = span\n\n    def __repr__(self):\n        return '<Token %s \"%s\">' % (self.__class__._token, self.val)\n\n\ndef inline(argument_index: int):\n    \"\"\"\n    When added to grammar productions, it makes the method equivalent to:\n\n    self.val = kids[argument_index].val\n    \"\"\"\n\n    def decorator(func: Any):\n        func.inline_index = argument_index\n        return func\n    return decorator\n\n\nclass Nonterm(parsing.Nonterm):\n    span: Span\n\n    def __init_subclass__(cls, *, is_internal=False, **kwargs):\n        \"\"\"Add docstrings to class and reduce functions\n\n        If no class docstring is present, set it to '%nonterm'.\n\n        If any reduce function (ie. of the form `reduce(_\\\\w+)+` does not\n        have a docstring, a new one is generated based on the function name.\n\n        See https://github.com/MagicStack/parsing for more information.\n\n        Keyword arguments:\n        is_internal -- internal classes do not need docstrings and processing\n                       can be skipped\n        \"\"\"\n        super().__init_subclass__(**kwargs)\n\n        if is_internal:\n            return\n\n        if not cls.__doc__:\n            cls.__doc__ = '%nonterm'\n\n        for name, attr in cls.__dict__.items():\n            if (name.startswith('reduce_') and\n                    isinstance(attr, types.FunctionType)):\n\n                if attr.__doc__ is None:\n                    tokens = name.split('_')\n                    if name == 'reduce_empty':\n                        tokens = ['reduce', '<e>']\n\n                    doc = r'%reduce {}'.format(' '.join(tokens[1:]))\n\n                    prec = getattr(attr, '__parsing_precedence__', None)\n                    if prec is not None:\n                        doc += ' [{}]'.format(prec)\n\n                    inline_index = getattr(attr, 'inline_index', None)\n                    attr = lambda self, *args, meth=attr: meth(self, *args)\n                    attr.__doc__ = doc\n                    attr.inline_index = inline_index\n                    setattr(cls, name, attr)\n\n\nclass ListNonterm(Nonterm, is_internal=True):\n    def __init_subclass__(\n        cls,\n        *,\n        element,\n        separator=None,\n        is_internal=False,\n        allow_trailing_separator=False,\n        **kwargs,\n    ):\n        \"\"\"Create reductions for list classes.\n\n        If trailing separator is not allowed, the class can handle all\n        reductions directly.\n\n            L := E\n            L := L S E\n\n        If trailing separator is allowed, create an inner class to handle\n        all non-trailing reductions. Then the class handles the trailing\n        separator.\n\n            I := E\n            I := I S E\n            L := I\n            L := I S\n\n        The inner class is added to the same module as the class.\n        \"\"\"\n        if not is_internal:\n            if not allow_trailing_separator:\n                # directly handle the list\n                ListNonterm.add_list_reductions(\n                    cls, element=element, separator=separator\n                )\n\n            else:\n                # create inner list class and add to same module\n                mod = sys.modules[cls.__module__]\n\n                def inner_cls_exec(ns):\n                    ns['__module__'] = mod.__name__\n                    return ns\n\n                inner_cls_name = cls.__name__ + 'Inner'\n                inner_cls_kwds = dict(element=element, separator=separator)\n                inner_cls = types.new_class(inner_cls_name, (ListNonterm,),\n                                            inner_cls_kwds, inner_cls_exec)\n                setattr(mod, inner_cls_name, inner_cls)\n\n                # create reduce_inner function\n                separator_name = ListNonterm.component_name(separator)\n\n                setattr(cls,\n                    'reduce_{}'.format(inner_cls_name),\n                    lambda self, inner: (\n                        ListNonterm._reduce_inner(self, inner)\n                    ))\n                setattr(cls,\n                    'reduce_{}_{}'.format(inner_cls_name, separator_name),\n                    lambda self, inner, sep: (\n                        ListNonterm._reduce_inner(self, inner)\n                    ))\n\n        # reduce functions must be present before calling superclass\n        super().__init_subclass__(is_internal=is_internal, **kwargs)\n\n    def __iter__(self):\n        return iter(self.val)\n\n    def __len__(self):\n        return len(self.val)\n\n    @staticmethod\n    def add_list_reductions(cls, *, element, separator=None):\n        element_name = ListNonterm.component_name(element)\n        separator_name = ListNonterm.component_name(separator)\n\n        if separator_name:\n            tail_prod = lambda self, lst, sep, el: (\n                ListNonterm._reduce_list(self, lst, el)\n            )\n            tail_prod_name = 'reduce_{}_{}_{}'.format(\n                cls.__name__, separator_name, element_name)\n        else:\n            tail_prod = lambda self, lst, el: (\n                ListNonterm._reduce_list(self, lst, el)\n            )\n            tail_prod_name = 'reduce_{}_{}'.format(\n                cls.__name__, element_name)\n        setattr(cls, tail_prod_name, tail_prod)\n\n        setattr(cls, 'reduce_' + element_name,\n            lambda self, el: ListNonterm._reduce_el(self, el))\n\n    @staticmethod\n    def component_name(component: type) -> Optional[str]:\n        if component is None:\n            return None\n        elif issubclass(component, Token):\n            return component._token\n        elif issubclass(component, Nonterm):\n            return component.__name__\n        else:\n            raise Exception(\n                'List component must be a Token or Nonterm')\n\n    @staticmethod\n    def _reduce_list(self, lst, el):\n        if el.val is None:\n            tail = []\n        else:\n            tail = [el.val]\n\n        self.val = lst.val + tail\n\n    @staticmethod\n    def _reduce_el(self, el):\n        if el.val is None:\n            tail = []\n        else:\n            tail = [el.val]\n\n        self.val = tail\n\n    @staticmethod\n    def _reduce_inner(self, inner):\n        self.val = inner.val\n\n\ndef precedence(precedence):\n    \"\"\"Decorator to set production precedence.\"\"\"\n    def decorator(func):\n        func.__parsing_precedence__ = precedence.__name__\n        return func\n\n    return decorator\n\n\nclass Precedence(parsing.Precedence):\n    token_prec_map: dict[Any, Any] = {}\n    last: dict[Any, Any] = {}\n\n    def __init_subclass__(\n        cls,\n        *,\n        assoc,\n        tokens=None,\n        prec_group=None,\n        rel_to_last='>',\n        is_internal=False,\n        **kwargs,\n    ):\n        super().__init_subclass__(**kwargs)\n\n        if is_internal:\n            return\n\n        if not cls.__doc__:\n            doc = '%%%s' % assoc\n\n            last = Precedence.last.get(prec_group)\n            if last:\n                doc += ' %s%s' % (rel_to_last, last.__name__)\n\n            cls.__doc__ = doc\n\n        if tokens:\n            for token in tokens:\n                existing = None\n                try:\n                    existing = Precedence.token_prec_map[token]\n                except KeyError:\n                    Precedence.token_prec_map[token] = cls\n                else:\n                    raise Exception(\n                        'token {} has already been set precedence {}'.format(\n                            token, existing))\n\n        Precedence.last[prec_group] = cls\n\n    @classmethod\n    def for_token(cls, token_name):\n        return Precedence.token_prec_map.get(token_name)\n\n\ndef load_parser_spec(mod: types.ModuleType) -> parsing.Spec:\n    return parsing.Spec(\n        mod,\n        skinny=not debug.flags.edgeql_parser,\n        logFile=_localpath(mod, \"log\"),\n        verbose=bool(debug.flags.edgeql_parser),\n    )\n\n\ndef _localpath(mod, type):\n    return os.path.join(\n        os.path.dirname(mod.__file__),\n        mod.__name__.rpartition('.')[2] + '.' + type)\n\n\ndef load_spec_productions(\n    production_names: list[tuple[str, str]], mod: types.ModuleType\n) -> list[tuple[type, Callable]]:\n    productions: list[tuple[Any, Callable]] = []\n    for class_name, method_name in production_names:\n        cls = mod.__dict__.get(class_name, None)\n        if not cls:\n            # for NontermStart\n            productions.append((parsing.Nonterm(), lambda *args: None))\n            continue\n\n        method = cls.__dict__[method_name]\n        productions.append((cls, method))\n    return productions\n\n\ndef spec_to_json(spec: parsing.Spec) -> str:\n    # Converts a ParserSpec into JSON. Called from edgeql-parser Rust crate.\n    assert spec.pureLR\n\n    token_map: dict[str, str] = {\n        v._token: c for c, v in Token.token_map.items()\n    }\n\n    # productions\n    productions_all: set[Any] = set()\n    for st_actions in spec.actions():\n        for _, acts in st_actions.items():\n            act = cast(Any, acts[0])\n            if 'ReduceAction' in str(type(act)):\n                prod = act.production\n                productions_all.add(prod)\n    productions, production_id = sort_productions(productions_all)\n\n    # actions\n    actions = []\n    for st_actions in spec.actions():\n        out_st_actions = []\n        for tok, acts in st_actions.items():\n            act = cast(Any, acts[0])\n\n            str_tok = token_map.get(str(tok), str(tok))\n            if 'ShiftAction' in str(type(act)):\n                action_obj: Any = {\n                    'Shift': int(act.nextState)\n                }\n            else:\n                prod = act.production\n                action_obj = {\n                    'Reduce': {\n                        'production_id': production_id[prod],\n                        'non_term': str(prod.lhs),\n                        'cnt': len(prod.rhs),\n                    }\n                }\n\n            out_st_actions.append((str_tok, action_obj))\n\n        out_st_actions.sort(key=lambda item: item[0])\n        actions.append(out_st_actions)\n\n    # goto\n    goto = []\n    for st_goto in spec.goto():\n        out_goto = []\n        for nterm, action in st_goto.items():\n            out_goto.append((str(nterm), action))\n\n        goto.append(out_goto)\n\n    # inlines\n    inlines = []\n    for prod in productions:\n        id = production_id[prod]\n        inline = getattr(prod.method, 'inline_index', None)\n        if inline is not None:\n            assert isinstance(inline, int)\n            inlines.append((id, inline))\n\n    res = {\n        'actions': actions,\n        'goto': goto,\n        'start': str(spec.start_sym()),\n        'inlines': inlines,\n        'production_names': list(map(production_name, productions)),\n    }\n    return json.dumps(res)\n\n\ndef sort_productions(\n    productions_all: set[Any],\n) -> tuple[list[Any], dict[Any, int]]:\n    productions = list(productions_all)\n    productions.sort(key=production_name)\n\n    productions_id = {prod: id for id, prod in enumerate(productions)}\n    return (productions, productions_id)\n\n\ndef production_name(prod: Any) -> tuple[str, ...]:\n    return tuple(prod.qualified.split('.')[-2:])\n"
  },
  {
    "path": "edb/common/prometheus.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"An implementation of prometheus metrics protocol.\n\nKey differences from the official \"prometheus_client\" package:\n\n1. Not thread-safe. We're an async application and don't use threads\n   so there's no need for thread-safety.\n\n2. The code is as simple as possible; no complicated polymorphism\n   that can slow down metrics collection in runtime.\n\n3. It's more than 4x faster, likely because of (1) and (2).\n\n4. No global state. All metrics are explicitly contained in an\n   explicitly created \"registry\" instance.\n\n5. This code can be potentially cythonized (or mypyc-ified) for extra\n   performance.\n\n6. The tests (tests/common/test_prometheus.py) ensure that the output\n   is exactly equal to what prometheus_client generates. It's a bug\n   otherwise.\n\nSee more for details:\n\n* Open Metrics standard:\n  https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md\n\n* Prometheus documentation:\n  https://prometheus.io/docs/practices/naming/\n\n* Prometheus official Python client:\n  https://github.com/prometheus/client_python\n\"\"\"\n\nfrom __future__ import annotations\n\nimport bisect\nimport enum\nimport functools\nimport math\nimport time\nimport typing\n\n\n__all__ = ('Registry', 'Unit', 'calc_buckets')\n\n\ndef calc_buckets(\n    start: float, upper_bound: float, /, *, increment_ratio: float = 1.20\n) -> tuple[float, ...]:\n    \"\"\"Calculate histogram buckets on a logarithmic scale.\"\"\"\n    # See https://amplitude.com/blog/2014/08/06/optimal-streaming-histograms\n    # for more details.\n    # (Says a long standing comment, but this isn't what that post recommends!)\n    result: list[float] = []\n    while start <= upper_bound:\n        result.append(start)\n        start *= increment_ratio\n    return tuple(result)\n\n\ndef per_order_buckets(\n    start: float, end: float,\n    *,\n    base: float=10.0,\n    entries_per_order=4,\n) -> tuple[float, ...]:\n    # See https://amplitude.com/blog/2014/08/06/optimal-streaming-histograms\n    # for more details.\n    # (Actually, for this one.)\n    result: list[float] = [start]\n\n    next = start * base\n    while next <= end:\n        for i in range(1, entries_per_order):\n            val = next / entries_per_order * i\n            if val > result[-1]:\n                result.append(val)\n        result.append(next)\n        next *= base\n    return tuple(result)\n\n\nclass Unit(enum.Enum):\n\n    # https://prometheus.io/docs/practices/naming/#base-units\n\n    SECONDS = 'seconds'\n    CELSIUS = 'celsius'\n    METERS = 'meters'\n    BYTES = 'bytes'\n    RATIO = 'ratio'\n    VOLTS = 'volts'\n    AMPERES = 'amperes'\n    JOULES = 'joules'\n    GRAMS = 'grams'\n\n\nclass Registry:\n\n    _metrics: list[BaseMetric]\n    _metrics_names: set[str]\n    _prefix: str | None\n\n    def __init__(self, *, prefix: str | None = None):\n        self._metrics = []\n        self._metrics_names = set()\n        self._prefix = prefix\n\n    def _add_metric(self, metric: BaseMetric) -> None:\n        name = metric.get_name()\n        if name in self._metrics_names:\n            raise ValueError(\n                f'a metric with a name {name!r} has already been registered')\n        self._metrics.append(metric)\n        self._metrics_names.add(name)\n\n    def now(self) -> float:\n        return time.time()\n\n    def set_info(self, name: str, desc: str, /, **kwargs: str) -> None:\n        self._add_metric(Info(self, name, desc, **kwargs))\n\n    def new_counter(\n        self,\n        name: str,\n        desc: str,\n        /,\n        *,\n        unit: Unit | None = None,\n    ) -> Counter:\n        counter = Counter(self, name, desc, unit)\n        self._add_metric(counter)\n        return counter\n\n    def new_labeled_counter(\n        self,\n        name: str,\n        desc: str,\n        /,\n        *,\n        labels: tuple[str, ...],\n        unit: Unit | None = None,\n    ) -> LabeledCounter:\n        counter = LabeledCounter(self, name, desc, unit, labels=labels)\n        self._add_metric(counter)\n        return counter\n\n    def new_gauge(\n        self,\n        name: str,\n        desc: str,\n        /,\n        *,\n        unit: Unit | None = None,\n    ) -> Gauge:\n        gauge = Gauge(self, name, desc, unit)\n        self._add_metric(gauge)\n        return gauge\n\n    def new_labeled_gauge(\n        self,\n        name: str,\n        desc: str,\n        /,\n        *,\n        unit: Unit | None = None,\n        labels: tuple[str, ...],\n    ) -> LabeledGauge:\n        gauge = LabeledGauge(self, name, desc, unit, labels=labels)\n        self._add_metric(gauge)\n        return gauge\n\n    def new_histogram(\n        self,\n        name: str,\n        desc: str,\n        /,\n        *,\n        unit: Unit | None = None,\n        buckets: typing.Sequence[float] | None = None,\n    ) -> Histogram:\n        hist = Histogram(self, name, desc, unit, buckets=buckets)\n        self._add_metric(hist)\n        return hist\n\n    def new_labeled_histogram(\n        self,\n        name: str,\n        desc: str,\n        /,\n        *,\n        unit: Unit | None = None,\n        buckets: typing.Sequence[float] | None = None,\n        labels: tuple[str, ...],\n    ) -> LabeledHistogram:\n        hist = LabeledHistogram(\n            self, name, desc, unit, buckets=buckets, labels=labels\n        )\n        self._add_metric(hist)\n        return hist\n\n    def generate(self, **label_filters: str) -> str:\n        buffer: list[str] = []\n        for metric in self._metrics:\n            metric._generate(buffer, **label_filters)\n        buffer.append('')\n        return '\\n'.join(buffer)\n\n\nclass BaseMetric:\n\n    _type: str\n\n    _name: str\n    _desc: str\n    _unit: Unit | None\n    _created: float\n\n    _registry: Registry\n\n    PROHIBITED_SUFFIXES = (\n        '_count', '_created', '_total', '_sum', '_bucket',\n        '_gcount', '_gsum', '_info',\n    )\n\n    PROHIBITED_PREFIXES = (\n        '_', 'python_', 'prometheus_',\n    )\n\n    PROHIBITED_LABELS = (\n        'quantile', 'le'\n    )\n\n    def __init__(\n        self,\n        registry: Registry,\n        name: str,\n        desc: str,\n        unit: Unit | None = None,\n        /,\n    ) -> None:\n        self._registry = registry\n        name = self._augment_metric_name(name)\n        self._validate_name(name)\n        if unit is not None:\n            name += '_' + unit.value\n        self._name = name\n        self._desc = desc\n        self._unit = unit\n        self._created = registry.now()\n\n    def _augment_metric_name(self, name: str) -> str:\n        if self._registry._prefix is not None:\n            name = f'{self._registry._prefix}_{name}'\n        return name\n\n    def get_name(self) -> str:\n        return self._name\n\n    def _validate_name(self, name: str) -> None:\n        if (name.startswith(self.PROHIBITED_PREFIXES) or\n                name.endswith(self.PROHIBITED_SUFFIXES)):\n            raise ValueError(f'invalid metrics name: {name!r}')\n\n    def _validate_label_names(self, labels: tuple[str, ...]) -> None:\n        for label in labels:\n            if label.startswith('_') or label in self.PROHIBITED_LABELS:\n                raise ValueError(f'invalid label name: {label!r}')\n\n    def _validate_label_values(\n        self, labels: tuple[str, ...], values: tuple[str, ...]\n    ) -> None:\n        if len(values) != len(labels):\n            raise ValueError(\n                f'missing values for labels: {labels[len(values):]!r}')\n        for name, val in zip(labels, values):\n            if not val:\n                raise ValueError(f'empty value for label {name!r}')\n\n    def _make_label_filter(\n        self,\n        labels: tuple[str, ...],\n        label_filters: dict[str, str],\n    ) -> typing.Callable[[tuple[str, ...]], bool]:\n        if not label_filters:\n            return lambda _: True\n\n        try:\n            label_by_idx = [\n                (labels.index(label), label_val)\n                for label, label_val in label_filters.items()\n            ]\n        except ValueError:\n            return lambda _: False\n\n        def label_filter(label_values: tuple[str, ...]) -> bool:\n            for idx, label_val in label_by_idx:\n                if label_values[idx] != label_val:\n                    return False\n            return True\n\n        return label_filter\n\n    def _generate(self, buffer: list[str], **label_filters: str) -> None:\n        raise NotImplementedError\n\n\nclass Info(BaseMetric):\n\n    _type = 'info'\n\n    _name: str\n    _desc: str\n    _registry: Registry\n    _labels: dict[str, str]\n\n    def __init__(self, *args: typing.Any, **labels: str) -> None:\n        super().__init__(*args)\n        self._validate_label_names(tuple(labels.keys()))\n        self._labels = labels\n\n    def _generate(self, buffer: list[str], **label_filters: str) -> None:\n        if label_filters:\n            return\n\n        desc = _format_desc(self._desc)\n\n        buffer.append(f'# HELP {self._name}_info {desc}')\n        buffer.append(f'# TYPE {self._name}_info gauge')\n\n        fmt_label = ','.join(\n            f'{label}=\"{_format_label_val(value)}\"'\n            for label, value in self._labels.items()\n        )\n        buffer.append(f'{self._name}_info{{{fmt_label}}} 1.0')\n\n\nclass BaseCounter(BaseMetric):\n\n    _type = 'counter'\n\n    _suffix = '_total'\n    _render_created = True\n\n    _value: float\n\n    def __init__(self, *args: typing.Any) -> None:\n        super().__init__(*args)\n        self._value = 0\n\n    def inc(self, value: float = 1.0) -> None:\n        if value < 0:\n            raise ValueError(\n                'counter cannot be incremented with a negative value')\n        self._value += value\n\n    def _generate(self, buffer: list[str], **label_filters: str) -> None:\n        if label_filters:\n            return\n\n        desc = _format_desc(self._desc)\n\n        buffer.append(f'# HELP {self._name}{self._suffix} {desc}')\n        buffer.append(f'# TYPE {self._name}{self._suffix} {self._type}')\n        buffer.append(f'{self._name}{self._suffix} {float(self._value)}')\n\n        if self._render_created:\n            buffer.append(f'# HELP {self._name}_created {desc}')\n            buffer.append(f'# TYPE {self._name}_created gauge')\n            buffer.append(f'{self._name}_created {float(self._created)}')\n\n\nclass BaseLabeledCounter(BaseMetric):\n\n    _type = 'counter'\n\n    _suffix = '_total'\n    _render_created = True\n\n    _labels: tuple[str, ...]\n    _metric_values: dict[tuple[str, ...], float]\n    _metric_created: dict[tuple[str, ...], float]\n\n    def __init__(self, *args: typing.Any, labels: tuple[str, ...]) -> None:\n        super().__init__(*args)\n        self._validate_label_names(labels)\n        self._labels = labels\n        self._metric_values = {}\n        self._metric_created = {}\n\n    def inc(self, value: float = 1.0, *labels: str) -> None:\n        self._validate_label_values(self._labels, labels)\n        if value < 0:\n            raise ValueError(\n                'counter cannot be incremented with a negative value')\n        try:\n            self._metric_values[labels] += value\n        except KeyError:\n            self._metric_values[labels] = value\n            self._metric_created[labels] = self._registry.now()\n\n    def _generate(self, buffer: list[str], **label_filters: str) -> None:\n        desc = _format_desc(self._desc)\n\n        buffer.append(f'# HELP {self._name}{self._suffix} {desc}')\n        buffer.append(f'# TYPE {self._name}{self._suffix} {self._type}')\n\n        filter_func = self._make_label_filter(self._labels, label_filters)\n        for labels, value in self._metric_values.items():\n            if not filter_func(labels):\n                continue\n            fmt_label = ','.join(\n                f'{label}=\"{_format_label_val(label_val)}\"'\n                for label, label_val in zip(self._labels, labels)\n            )\n            buffer.append(\n                f'{self._name}{self._suffix}{{{fmt_label}}} {float(value)}'\n            )\n\n        if self._render_created and self._metric_values:\n            buffer.append(f'# HELP {self._name}_created {desc}')\n            buffer.append(f'# TYPE {self._name}_created gauge')\n\n            for labels, value in self._metric_created.items():\n                if not filter_func(labels):\n                    continue\n                fmt_label = ','.join(\n                    f'{label}=\"{_format_label_val(label_val)}\"'\n                    for label, label_val in zip(self._labels, labels)\n                )\n                buffer.append(\n                    f'{self._name}_created{{{fmt_label}}} {float(value)}'\n                )\n\n    def clear(self, label_filter: typing.Callable[..., bool]) -> None:\n        for label in list(self._metric_values):\n            if label_filter(*label):\n                self._metric_values.pop(label)\n                self._metric_created.pop(label, None)\n\n\nclass _TotalMixin(BaseMetric):\n\n    def _augment_metric_name(self, name: str) -> str:\n        name = super()._augment_metric_name(name)\n        if not name.endswith('_total'):\n            raise TypeError('counter metric name require the \"_total\" suffix')\n        name = name[:-len('_total')]\n        return name\n\n\nclass Counter(_TotalMixin, BaseCounter):\n    pass\n\n\nclass LabeledCounter(_TotalMixin, BaseLabeledCounter):\n    pass\n\n\nclass Gauge(BaseCounter):\n\n    _type = 'gauge'\n\n    _render_created = False\n    _suffix = ''\n\n    def inc(self, value: float = 1.0) -> None:\n        self._value += value\n\n    def dec(self, value: float = 1.0) -> None:\n        self._value -= value\n\n    def set(self, value: float) -> None:\n        self._value = value\n\n\nclass LabeledGauge(BaseLabeledCounter):\n\n    _type = 'gauge'\n\n    _render_created = False\n    _suffix = ''\n\n    def inc(self, value: float = 1.0, *labels: str) -> None:\n        self._validate_label_values(self._labels, labels)\n        try:\n            self._metric_values[labels] += value\n        except KeyError:\n            self._metric_values[labels] = value\n            self._metric_created[labels] = self._registry.now()\n\n    def dec(self, value: float = 1.0, *labels: str) -> None:\n        self.inc(-value, *labels)\n\n    def set(self, value: float = 1.0, *labels: str) -> None:\n        self._validate_label_values(self._labels, labels)\n        self._metric_values[labels] = value\n        try:\n            self._metric_created[labels]\n        except KeyError:\n            self._metric_created[labels] = self._registry.now()\n\n\nclass BaseHistogram(BaseMetric):\n\n    _type = 'histogram'\n\n    _buckets: list[float]\n\n    # Default buckets that many standard prometheus client libraries use.\n    DEFAULT_BUCKETS = [\n        0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75,\n        1.0, 2.5, 5.0, 7.5, 10.0,\n    ]\n\n    def __init__(\n        self, *args: typing.Any, buckets: typing.Sequence[float] | None = None\n    ) -> None:\n        if buckets is None:\n            buckets = self.DEFAULT_BUCKETS\n        else:\n            buckets = list(buckets)  # copy, just in case\n\n        if buckets != sorted(buckets):\n            raise ValueError('*buckets* must be sorted')\n        if len(buckets) < 2:\n            raise ValueError('*buckets* must have at least 2 numbers')\n        if not math.isinf(buckets[-1]):\n            buckets += [float('+inf')]\n\n        super().__init__(*args)\n\n        self._buckets = buckets\n\n\nclass Histogram(BaseHistogram):\n\n    _values: list[float]\n    _sum: float\n\n    def __init__(\n        self, *args: typing.Any, buckets: typing.Sequence[float] | None = None\n    ) -> None:\n        super().__init__(*args, buckets=buckets)\n        self._sum = 0.0\n        self._values = [0.0] * len(self._buckets)\n\n    def observe(self, value: float) -> None:\n        idx = bisect.bisect_left(self._buckets, value)\n        self._values[idx] += 1.0\n        self._sum += value\n\n    def _generate(self, buffer: list[str], **label_filters: str) -> None:\n        if label_filters:\n            return\n\n        desc = _format_desc(self._desc)\n\n        buffer.append(f'# HELP {self._name} {desc}')\n        buffer.append(f'# TYPE {self._name} histogram')\n\n        accum = 0.0\n        for buck, val in zip(self._buckets, self._values):\n            accum += val\n\n            if math.isinf(buck):\n                if buck > 0:\n                    buckf = '+Inf'\n                else:\n                    buckf = '-Inf'\n            else:\n                buckf = str(buck)\n\n            buffer.append(f'{self._name}_bucket{{le=\"{buckf}\"}} {accum}')\n\n        buffer.append(f'{self._name}_count {accum}')\n        buffer.append(f'{self._name}_sum {self._sum}')\n\n        buffer.append(f'# HELP {self._name}_created {desc}')\n        buffer.append(f'# TYPE {self._name}_created gauge')\n        buffer.append(f'{self._name}_created {float(self._created)}')\n\n\nclass LabeledHistogram(BaseHistogram):\n\n    _labels: tuple[str, ...]\n    _metric_values: dict[tuple[str, ...], list[float | list[float]]]\n    _metric_created: dict[tuple[str, ...], float]\n\n    def __init__(\n        self,\n        *args: typing.Any,\n        buckets: typing.Sequence[float] | None = None,\n        labels: tuple[str, ...],\n    ) -> None:\n        super().__init__(*args, buckets=buckets)\n        self._labels = labels\n        self._metric_values = {}\n        self._metric_created = {}\n\n    def observe(self, value: float, *labels: str) -> None:\n        self._validate_label_values(self._labels, labels)\n\n        try:\n            metric = self._metric_values[labels]\n        except KeyError:\n            metric = [0.0, [0.0] * len(self._buckets)]\n            self._metric_values[labels] = metric\n            self._metric_created[labels] = self._registry.now()\n\n        idx = bisect.bisect_left(self._buckets, value)\n        metric[1][idx] += 1.0  # type: ignore\n        metric[0] += value  # type: ignore\n\n    def _generate(self, buffer: list[str], **label_filters: str) -> None:\n        desc = _format_desc(self._desc)\n\n        buffer.append(f'# HELP {self._name} {desc}')\n        buffer.append(f'# TYPE {self._name} histogram')\n\n        filter_func = self._make_label_filter(self._labels, label_filters)\n        for labels, values in self._metric_values.items():\n            if not filter_func(labels):\n                continue\n            fmt_label = ','.join(\n                f'{label}=\"{_format_label_val(label_val)}\"'\n                for label, label_val in zip(self._labels, labels)\n            )\n            accum = 0.0\n            for buck, val in zip(self._buckets, values[1]):  # type: ignore\n                accum += val\n\n                if math.isinf(buck):\n                    if buck > 0:\n                        buckf = '+Inf'\n                    else:\n                        buckf = '-Inf'\n                else:\n                    buckf = str(buck)\n\n                buffer.append(\n                    f'{self._name}_bucket{{le=\"{buckf}\",{fmt_label}}} {accum}'\n                )\n\n            buffer.append(f'{self._name}_count{{{fmt_label}}} {accum}')\n            buffer.append(f'{self._name}_sum{{{fmt_label}}} {values[0]}')\n\n        if self._metric_values:\n            buffer.append(f'# HELP {self._name}_created {desc}')\n            buffer.append(f'# TYPE {self._name}_created gauge')\n            for labels, value in self._metric_created.items():\n                if not filter_func(labels):\n                    continue\n                fmt_label = ','.join(\n                    f'{label}=\"{_format_label_val(label_val)}\"'\n                    for label, label_val in zip(self._labels, labels)\n                )\n                buffer.append(\n                    f'{self._name}_created{{{fmt_label}}} {float(value)}'\n                )\n\n\n@functools.lru_cache(maxsize=1024)\ndef _format_desc(desc: str) -> str:\n    return desc.replace('\\\\', r'\\\\').replace('\\n', r'\\n')\n\n\n@functools.lru_cache(maxsize=1024)\ndef _format_label_val(desc: str) -> str:\n    return (\n        desc.replace('\\\\', r'\\\\').replace('\\n', r'\\n').replace('\"', r'\\\"')\n    )\n"
  },
  {
    "path": "edb/common/retryloop.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Callable,\n    Optional,\n)\n\nimport asyncio\nimport random\nimport re\nimport time\nimport types\n\n\ndef const_backoff(delay: float) -> Callable[[int], float]:\n    return lambda _: delay\n\n\ndef exp_backoff(\n    *,\n    factor: float = 0.1,\n    jitter_scale: float = 0.001,\n) -> Callable[[int], float]:\n    def _f(i: int) -> float:\n        delay: int = 2 ** i\n        return delay * factor + random.randrange(100) * jitter_scale\n    return _f\n\n\nclass RetryLoop:\n\n    def __init__(\n        self,\n        *,\n        backoff: Callable[[int], float] = const_backoff(0.5),\n        timeout: float,\n        ignore: type[Exception] | tuple[type[Exception], ...] | None = None,\n        ignore_regexp: str | None = None,\n        wait_for: type[Exception] | tuple[type[Exception], ...] | None = None,\n        wait_for_regexp: str | None = None,\n        retry_cb: Callable[[Optional[BaseException]], None] | None = None,\n    ) -> None:\n        self._iteration = 0\n        self._backoff = backoff\n        self._timeout = timeout\n        self._ignore = ignore\n        if ignore_regexp is None:\n            self._ignore_regexp = None\n        else:\n            self._ignore_regexp = re.compile(ignore_regexp)\n        self._wait_for = wait_for\n        if wait_for_regexp is None:\n            self._wait_for_regexp = None\n        else:\n            self._wait_for_regexp = re.compile(wait_for_regexp)\n        self._started_at = 0.0\n        self._stop_request = False\n        self._retry_cb = retry_cb\n\n    def __aiter__(self) -> RetryLoop:\n        return self\n\n    async def __anext__(self) -> RetryIteration:\n        if self._stop_request:\n            raise StopAsyncIteration\n\n        if self._started_at == 0:\n            # First run\n            self._started_at = time.monotonic()\n        else:\n            # Second or greater run -- delay before yielding\n            delay = self._backoff(self._iteration)\n            await asyncio.sleep(delay)\n\n        self._iteration += 1\n\n        return RetryIteration(self)\n\n\nclass RetryIteration:\n\n    def __init__(self, loop: RetryLoop) -> None:\n        self._loop = loop\n\n    async def __aenter__(self) -> RetryIteration:\n        return self\n\n    async def __aexit__(\n        self,\n        et: type[BaseException],\n        e: BaseException,\n        _tb: types.TracebackType,\n    ) -> bool:\n        elapsed = time.monotonic() - self._loop._started_at\n\n        if (\n            self._loop._ignore is not None or\n            self._loop._ignore_regexp is not None\n        ):\n            # Mode 1: Try until we don't get errors matching `ignore`\n\n            if et is None:\n                self._loop._stop_request = True\n                return False\n\n            # Propagate if it's not the error we expected.\n            if self._loop._ignore is not None:\n                if not isinstance(e, self._loop._ignore):\n                    return False\n            if self._loop._ignore_regexp is not None:\n                if not self._loop._ignore_regexp.search(str(e)):\n                    return False\n\n            if elapsed > self._loop._timeout:\n                # Propagate -- we've run it enough times.\n                return False\n\n            if self._loop._retry_cb is not None:\n                self._loop._retry_cb(e)\n\n            # Ignore the exception until next run.\n            return True\n\n        else:\n            # Mode 2: Try until we fail with an error matching `wait_for`\n\n            assert (\n                self._loop._wait_for is not None or\n                self._loop._wait_for_regexp is not None\n            )\n\n            if et is not None:\n                if (\n                    self._loop._wait_for is None or\n                    isinstance(e, self._loop._wait_for)\n                ) and (\n                    self._loop._wait_for_regexp is None\n                    or self._loop._wait_for_regexp.search(str(e))\n                ):\n                    # We're done, we've got what we waited for.\n                    self._loop._stop_request = True\n                    return True\n                else:\n                    # Propagate, it's not the error we expected.\n                    return False\n\n            if elapsed > self._loop._timeout:\n                raise TimeoutError(\n                    f'exception matching {self._loop._wait_for!r} '\n                    f'has not happen in {self._loop._timeout} seconds')\n\n            # Ignore the exception until next run.\n            return True\n"
  },
  {
    "path": "edb/common/secretkey.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright EdgeDB Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Iterable\n\nimport pathlib\nimport uuid\n\nfrom datetime import datetime, timedelta\n\n\ndef generate_tls_cert(\n    tls_cert_file: pathlib.Path,\n    tls_key_file: pathlib.Path,\n    listen_hosts: Iterable[str]\n) -> None:\n    from cryptography import x509\n    from cryptography.hazmat import backends\n    from cryptography.hazmat.primitives import hashes\n    from cryptography.hazmat.primitives import serialization\n    from cryptography.hazmat.primitives.asymmetric import rsa\n    from cryptography.x509 import oid\n\n    backend = backends.default_backend()\n    private_key = rsa.generate_private_key(\n        public_exponent=65537, key_size=2048, backend=backend\n    )\n    subject = x509.Name(\n        [x509.NameAttribute(oid.NameOID.COMMON_NAME, \"Gel Server\")]\n    )\n    certificate = (\n        x509.CertificateBuilder()\n        .subject_name(subject)\n        .public_key(private_key.public_key())\n        .serial_number(int(uuid.uuid4()))\n        .issuer_name(subject)\n        .not_valid_before(\n            datetime.today() - timedelta(days=1)\n        )\n        .not_valid_after(\n            datetime.today() + timedelta(weeks=1000)\n        )\n        .add_extension(\n            x509.SubjectAlternativeName(\n                [\n                    x509.DNSName(name) for name in listen_hosts\n                    if name not in {'0.0.0.0', '::'}\n                ]\n            ),\n            critical=False,\n        )\n        .sign(\n            private_key=private_key,\n            algorithm=hashes.SHA256(),\n            backend=backend,\n        )\n    )\n    with tls_cert_file.open(\"wb\") as f:\n        f.write(certificate.public_bytes(encoding=serialization.Encoding.PEM))\n    tls_cert_file.chmod(0o644)\n    with tls_key_file.open(\"wb\") as f:\n        f.write(\n            private_key.private_bytes(\n                encoding=serialization.Encoding.PEM,\n                format=serialization.PrivateFormat.TraditionalOpenSSL,\n                encryption_algorithm=serialization.NoEncryption(),\n            )\n        )\n    tls_key_file.chmod(0o600)\n"
  },
  {
    "path": "edb/common/signalctl.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport asyncio\nimport functools\nimport signal as mod_signal\nimport warnings\n\n\ndef _release_waiter(waiter, *args):\n    if not waiter.done():\n        waiter.set_result(None)\n\n\nclass SignalError(Exception):\n    def __init__(self, signo):\n        self.signo = signo\n\n    def __str__(self):\n        if isinstance(self.signo, mod_signal.Signals):\n            return self.signo._name_\n        else:\n            return str(self.signo)\n\n\nclass SignalHandler:\n    def __init__(self, callback, signals, controller):\n        self._cancelled = False\n        self._callback = callback\n        self._signals = signals\n        self._controller = controller\n        for signal in signals:\n            controller._register_waiter(signal, self)\n\n    def done(self):\n        return self._cancelled\n\n    def cancelled(self):\n        return self._cancelled\n\n    def set_result(self, result):\n        asyncio.get_running_loop().call_soon(self._callback, result)\n\n    def cancel(self):\n        self._cancelled = True\n        for signal in self._signals:\n            self._controller._discard_waiter(signal, self)\n\n\nclass SignalController:\n    _registry: dict[\n        asyncio.AbstractEventLoop,\n        dict[int, set[SignalController]],\n    ] = {}\n    _waiters: dict[int, set[asyncio.Future]]\n\n    def __init__(self, *signals):\n        self._signals = signals\n        self._loop = asyncio.get_running_loop()\n        self._waiters = {}\n\n    def __enter__(self):\n        registry = self._registry.setdefault(self._loop, {})\n        for signal in self._signals:\n            controllers = registry.setdefault(signal, set())\n            if not controllers:\n                self._loop.add_signal_handler(\n                    signal, self._signal_callback, signal\n                )\n            controllers.add(self)\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        handlers = [\n            waiter\n            for waiters in self._waiters.values()\n            for waiter in waiters\n            if isinstance(waiter, SignalHandler)\n        ]\n        for handler in handlers:\n            handler.cancel()\n        if self._waiters:\n            warnings.warn(\n                \"SignalController exited before wait_for() completed.\",\n                stacklevel=1,\n            )\n        registry = self._registry[self._loop]\n        for signal in self._signals:\n            controllers = registry[signal]\n            controllers.discard(self)\n            if not controllers:\n                del registry[signal]\n                self._loop.remove_signal_handler(signal)\n                if not registry:\n                    del self._registry[self._loop]\n\n    def _on_signal(self, signal):\n        for waiter in self._waiters.get(signal, []):\n            if not waiter.done():\n                waiter.set_result(signal)\n\n    def _register_waiter(self, signal, waiter):\n        self._waiters.setdefault(signal, set()).add(waiter)\n\n    def _discard_waiter(self, signal, waiter):\n        waiters = self._waiters.get(signal)\n        if waiters:\n            waiters.discard(waiter)\n            if not waiters:\n                del self._waiters[signal]\n\n    async def wait_for(self, fut, *, cancel_on=None):\n        fut = asyncio.ensure_future(fut)\n        # early check: if for any reason fut is already done, just return\n        if fut.done():\n            return fut.result()\n\n        # by default, capture all signals configured in this controller\n        if cancel_on is None:\n            cancel_on = self._signals\n\n        cancelled_by = None\n        outer_cancelled_at_last = False\n\n        # The design here: we'll wait on a separate Future \"waiter\" for clean\n        # cancellation. The waiter might be woken up by 3 different events:\n        #   1. The given \"fut\" is done\n        #   2. A signal is captured\n        #   3. The \"waiter\" is cancelled by outer code.\n        # For 2, we'll cancel the given \"fut\" and record the signal in\n        # cancelled_by as a __context__ chain to raise in the next step; for 3,\n        # we cancel the given \"fut\" and propagate the CancelledError later.\n        #\n        # The complexity of this design is: because our cancellation might be\n        # intercepted in the \"fut\" code - e.g. a finally block or except block\n        # that traps (and hopefully re-raises) the CancelledError or\n        # BaseException, we need a loop here to ensure all the nested blocks\n        # are exhaustively executed until the \"fut\" is done, meanwhile the\n        # signals may keep hitting the \"fut\" code blocks, and \"wait_for\" is\n        # ready to handle them properly, and return all the SignalError objects\n        # in a __context__ chain preserving the order as they happen.\n        while not fut.done():\n            waiter = self._loop.create_future()\n            cb = functools.partial(_release_waiter, waiter)\n            fut.add_done_callback(cb)\n\n            for signal in cancel_on:\n                self._register_waiter(signal, waiter)\n            try:\n                try:\n                    signal = await waiter\n                except asyncio.CancelledError:\n                    # Event 3: cancelled by outer code.\n                    if not fut.done():\n                        fut.cancel()\n                        outer_cancelled_at_last = True\n                else:\n                    # Event 2: \"fut\" is still running, which means that\n                    # \"waiter\" was woken up by a signal.\n                    if not fut.done():\n                        assert signal is not None\n                        fut.cancel()\n                        err = SignalError(signal)\n                        err.__context__ = cancelled_by\n                        cancelled_by = err\n                        outer_cancelled_at_last = False\n\n                # Event 1: \"fut\" is done - exit the loop naturally.\n\n            finally:\n                fut.remove_done_callback(cb)\n                # In any case, the \"waiter\" is done at this time, it needs to\n                # be removed from the signal callback chain, even if we still\n                # need to wait for the signal in the next loop, with a new\n                # \"waiter\" object.\n                for signal in cancel_on:\n                    self._discard_waiter(signal, waiter)\n\n        # Now that the \"fut\" is done, let's check its result. It may end up in\n        # 3 different scenarios, listed below inline:\n        try:\n            # 1. \"fut\" finished happily without raising errors (event 1), just\n            #    return the result. Even if we've previously recorded signals\n            #    (event 2) or cancellations (event 3), it's now handled by the\n            #    user, and we shall simply dispose the recorded cancelled_by.\n            return fut.result()\n\n        except asyncio.CancelledError as ex:\n            # 2. \"fut\" is cancelled - this usually means we caught a signal,\n            #    but it could also be other reasons, see below.\n            if cancelled_by is not None:\n                # Event 2 happened at least once\n                if outer_cancelled_at_last:\n                    # If event 3 is the last event, the outer code is probably\n                    # expecting a CancelledError, e.g. asyncio.wait_for().\n                    # Therefore, we just raise it with signal errors attached.\n                    ex.__context__ = cancelled_by\n                    raise\n                else:\n                    # If event 2 is the last event, simply raise the grouped\n                    # signal errors, attaching the CancelledError to reveal\n                    # where the signals hit the user code. We cannot raise\n                    # directly here because cancelled_by.__context__ may have\n                    # previously-captured signal errors.\n                    cancelled_by.__cause__ = ex\n            else:\n                # Neither event 2 nor 3 happened, the user code cancelled\n                # itself, simply propagate the same error.\n                raise\n\n        except Exception as e:\n            # 3. For any other errors, we just raise it with the signal errors\n            #    attached as __context__ if event 2 happened.\n            if cancelled_by is not None:\n                e.__context__ = cancelled_by\n            raise\n\n        assert cancelled_by is not None\n        raise cancelled_by\n\n    def add_handler(self, callback, signals=None) -> SignalHandler:\n        if signals is None:\n            signals = self._signals\n        return SignalHandler(callback, signals, self)\n\n    @classmethod\n    def _signal_callback(cls, signal):\n        registry = cls._registry.get(asyncio.get_running_loop())\n        if not registry:\n            return\n        controllers = registry.get(signal)\n        if not controllers:\n            return\n        for controller in controllers:\n            controller._on_signal(signal)\n"
  },
  {
    "path": "edb/common/span.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"This module contains tools for maintaining parser context.\n\nMaintaining parser context explicitly is significant overhead and can\nbe difficult in the face of changing AST structures. Certain parser\nproductions require various nesting or unnesting of previously parsed\nnodes. Both of these operations can result in the parser context not\nbeing correctly updated.\n\nThe tools in this module attempt to automatically maintain parser\ncontext based on the context information found in lexer tokens. The\ngeneral approach is to infer context information by propagating known\ncontexts through the AST structure.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Iterable, Any, Optional\nimport re\nimport bisect\n\nimport edb._edgeql_parser as ql_parser\n\nfrom edb.common import ast\nfrom edb.common import markup\nfrom edb.common import typeutils\n\n\nNEW_LINE = re.compile(rb'\\r\\n?|\\n')\n\n\nclass Span(markup.MarkupExceptionContext):\n    '''\n    Parser Source Context\n    '''\n\n    def __init__(\n        self,\n        filename: Optional[str],\n        buffer: str,\n        start: int,\n        end: int,\n        *,\n        context_lines=1,\n    ):\n        assert start is not None\n        assert end is not None\n\n        self.filename = filename\n        self.buffer = buffer\n        self.start = start\n        self.end = end\n        self.context_lines = context_lines\n\n        self._points = None\n\n    @classmethod\n    def empty(cls) -> Span:\n        return Span(\n            filename=None,\n            buffer='',\n            start=0,\n            end=0,\n        )\n\n    def __str__(self):\n        if self.filename:\n            return f'{self.filename}:{self.start}..{self.end}'\n        return f'{self.start}..{self.end}'\n\n    def __getstate__(self):\n        dic = self.__dict__.copy()\n        dic['_points'] = None\n        return dic\n\n    def _calc_points(self):\n        # HACK: If we don't have an actual buffer (probably because we\n        # are recompiling after a schema change), just fake something\n        # long enough. Line numbers will be wrong but positions will\n        # still be right...\n        buffer = self.buffer.encode('utf-8') if self.buffer else b' ' * self.end\n        self._points = ql_parser.SourcePoint.from_offsets(\n            buffer, [self.start, self.end]\n        )\n\n    @property\n    def start_point(self):\n        if self._points is None:\n            self._calc_points()\n        return self._points[0]\n\n    @property\n    def end_point(self):\n        if self._points is None:\n            self._calc_points()\n        return self._points[1]\n\n    @classmethod\n    @markup.serializer.no_ref_detect\n    def as_markup(cls, self, *, ctx):\n        me = markup.elements\n\n        start = self.start_point\n        # TODO: do more with end?\n        end = self.end_point\n\n        buf_bytes = self.buffer.encode('utf-8')\n        offset = 0\n        buf_lines = []\n        line_offsets = [0]\n        for match in NEW_LINE.finditer(buf_bytes):\n            buf_lines.append(buf_bytes[offset : match.start()].decode('utf-8'))\n            offset = match.end()\n            line_offsets.append(offset)\n\n        line_no = bisect.bisect_right(line_offsets, start.offset) - 1\n\n        context_start = max(0, line_no - self.context_lines)\n        context_end = min(line_no + self.context_lines + 1, len(buf_lines))\n\n        endcol = end.column if start.line == end.line else None\n        tbp = me.lang.TracebackPoint(\n            name=self.filename,\n            filename=self.filename,\n            lineno=start.line,\n            colno=start.column,\n            end_colno=endcol,\n            lines=buf_lines[context_start:context_end],\n            # Line numbers are 1 indexed here\n            line_numbers=list(range(context_start + 1, context_end + 1)),\n            context=True,\n        )\n\n        return me.lang.ExceptionContext(title=self.title, body=[tbp])\n\n\ndef _get_span(items, *, reverse=False) -> Optional[Span]:\n    ctx = None\n\n    items = reversed(items) if reverse else items\n    # find non-empty start and end\n    #\n    for item in items:\n        if isinstance(item, (list, tuple)):\n            ctx = _get_span(item, reverse=reverse)\n            if ctx:\n                return ctx\n        else:\n            ctx = getattr(item, 'span', None)\n            if ctx:\n                return ctx\n\n    return None\n\n\ndef get_span(*kids: list[ast.AST]):\n    start_ctx = _get_span(kids)\n    end_ctx = _get_span(kids, reverse=True)\n\n    if not start_ctx or not end_ctx:\n        return None\n\n    return Span(\n        filename=start_ctx.filename,\n        buffer=start_ctx.buffer,\n        start=start_ctx.start,\n        end=end_ctx.end,\n    )\n\n\ndef merge_spans(spans: Iterable[Span]) -> Span | None:\n    span_list = list(spans)\n    if not span_list:\n        return None\n\n    span_list.sort(key=lambda x: (x.start, x.end))\n\n    # assume same name and buffer apply to all\n    #\n    return Span(\n        filename=span_list[0].filename,\n        buffer=span_list[0].buffer,\n        start=span_list[0].start,\n        end=span_list[-1].end,\n    )\n\n\nclass SpanPropagator(ast.NodeVisitor):\n    \"\"\"Propagate span from children to root.\n\n    It is assumed that if a node has a span, all of its children\n    also have correct span. For a node that has no span, its\n    span is derived as a superset of all of the spans of its\n    descendants.\n\n    If full_pass is True, nodes with span will still recurse into\n    children and their new span will also be superset of the existing span.\n    \"\"\"\n\n    def __init__(self, default=None, full_pass=False):\n        super().__init__()\n        self._default = default\n        self._full_pass = full_pass\n\n    def repeated_node_visit(self, node):\n        return self.memo[node]\n\n    def container_visit(self, node) -> list[Span | None]:\n        span_list: list[Span | None] = []\n        for el in node:\n            if isinstance(el, ast.AST) or typeutils.is_container(el):\n                span = self.visit(el)\n\n                if not span:\n                    pass\n                elif isinstance(span, (list, tuple)):\n                    span_list.extend(span)\n                elif isinstance(span, dict):\n                    span_list.extend(span.values())\n                else:\n                    span_list.append(span)\n        return span_list\n\n    def generic_visit(self, node):\n        # base case: we already have span\n        if not self._full_pass and getattr(node, 'span', None) is not None:\n            return node.span\n\n        # recurse into children fields\n        span_list = self.container_visit(v for _, v in ast.iter_fields(node))\n\n        # also include own span (this can only happen in full_pass)\n        if existing := getattr(node, 'span', None):\n            span_list.append(existing)\n\n        # merge spans into one\n        node.span = merge_spans(s for s in span_list if s) or self._default\n\n        return node.span\n\n\nclass SpanValidator(ast.NodeVisitor):\n    def generic_visit(self, node):\n        if getattr(node, 'span', None) is None:\n            from edb.edgeql import ast as qlast\n\n            # some nodes are allowed to not have span, because they are not\n            # always produced by the parser (i.e. ShapeOperation is created as\n            # a default value in the ast node)\n            if not isinstance(node, (qlast.ShapeOperation, qlast.Options)):\n                raise RuntimeError('node {} has no span'.format(node))\n        super().generic_visit(node)\n\n\n# Finds the node in AST by position within the source.\n# It returns the first node whose span contains the target offset in a\n# post-order traversal of the AST.\n# To be exact, it returns a path from that node to the tree root.\n# Or None if not found. Path is never empty.\ndef find_by_source_position[T: ast.AST](\n    node: T, target_offset: int\n) -> list[T] | None:\n    finder = SpanFinder(target_offset)\n    finder.visit(node)\n    return finder.found_path\n\n\nclass SpanFinder(ast.NodeVisitor):\n    target_offset: int\n    found_path: list[Any] | None\n\n    def __init__(self, target_offset: int):\n        super().__init__()\n        self.target_offset = target_offset\n        self.found_path = None\n\n    def generic_visit(self, node, *, combine_results=None) -> Any:\n        if self.found_path is not None:\n            return\n\n        has_span = False\n        if node_span := getattr(node, 'span', None):\n            has_span = True\n            if not span_contains(node_span, self.target_offset):\n                return\n\n        super().generic_visit(node)\n        if self.found_path is None:\n            if has_span:\n                self.found_path = [node]\n        else:\n            self.found_path.append(node)\n\n\ndef span_contains(span: Span, target_offset: int) -> bool:\n    return span.start <= target_offset and target_offset <= span.end\n"
  },
  {
    "path": "edb/common/struct.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2009-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    cast,\n    Final,\n    Iterable,\n    Iterator,\n    Mapping,\n    Optional,\n    Self,\n)\n\nimport collections\nimport enum\n\nfrom . import checked\n\n\nclass ProtoField:\n    __slots__ = ()\n\n\nclass NoDefaultT(enum.Enum):\n    NoDefault = 0\n\n\nNoDefault: Final = NoDefaultT.NoDefault\n\n\nclass Field[T](ProtoField):\n    \"\"\"``Field`` objects: attributes of :class:`Struct`.\"\"\"\n\n    __slots__ = ('name', 'type', 'default', 'coerce', 'formatters',\n                 'frozen')\n\n    name: str\n\n    def __init__(\n        self,\n        type_: type[T],\n        default: T | NoDefaultT = NoDefault,\n        *,\n        coerce: bool = False,\n        str_formatter: Callable[[T], str] = str,\n        repr_formatter: Callable[[T], str] = repr,\n        frozen: bool = False,\n    ) -> None:\n        \"\"\"\n        :param type:\n            The type of the value in the field.\n        :param default:\n            Default field value.  If not specified, the field would\n            be considered required and a failure to specify its\n            value when initializing a ``Struct`` will raise\n            :exc:`TypeError`.  `default` can be a callable taking\n            no arguments.\n        :param bool coerce:\n            If set to ``True`` - coerce field's value to its type.\n        \"\"\"\n        self.type = type_\n        self.default = default\n        self.coerce = coerce\n        self.frozen = frozen\n        self.formatters = {'str': str_formatter, 'repr': repr_formatter}\n\n    def copy(self) -> Field[T]:\n        return self.__class__(\n            self.type, self.default, coerce=self.coerce,\n            str_formatter=self.formatters['str'],\n            repr_formatter=self.formatters['repr'])\n\n    def adapt(self, value: Any) -> T:\n        # cast() below due to https://github.com/python/mypy/issues/7920\n        ctype = cast(type, self.type)\n\n        if not isinstance(value, ctype):\n            value = ctype(value)\n\n        # Type ignore below because with ctype we lost information that\n        # it is indeed a Type[T].\n        return value  # type: ignore\n\n    @property\n    def required(self) -> bool:\n        return self.default is NoDefault\n\n\nclass StructMeta(type):\n\n    _fields: dict[str, Field[Any]]\n    _sorted_fields: dict[str, Field[Any]]\n\n    def __new__[StructMeta_T: StructMeta](\n        mcls: type[StructMeta_T],\n        name: str,\n        bases: tuple[type, ...],\n        clsdict: dict[str, Any],\n        *,\n        use_slots: bool = True,\n        **kwargs: Any,\n    ) -> StructMeta_T:\n        fields = {}\n        myfields = {}\n\n        for k, v in clsdict.items():\n            if not isinstance(v, ProtoField):\n                continue\n            if not isinstance(v, Field):\n                raise TypeError(\n                    f'cannot create {name} class: struct.Field expected, '\n                    f'got {type(v)}')\n\n            v.name = k\n            myfields[k] = v\n\n        if '__slots__' not in clsdict:\n            if use_slots is None:\n                for base in bases:\n                    sa = '{}.{}_slots'.format(base.__module__, base.__name__)\n                    if isinstance(base, StructMeta) and hasattr(base, sa):\n                        use_slots = True\n                        break\n\n            if use_slots:\n                clsdict['__slots__'] = tuple(myfields.keys())\n                for key in myfields.keys():\n                    del clsdict[key]\n\n        cls = super().__new__(mcls, name, bases, clsdict, **kwargs)\n\n        if use_slots:\n            sa = '{}.{}_slots'.format(cls.__module__, cls.__name__)\n            setattr(cls, sa, True)\n\n        for parent in reversed(cls.__mro__):\n            if parent is cls:\n                fields.update(myfields)\n            elif isinstance(parent, StructMeta):\n                fields.update(parent.get_ownfields())\n\n        for field in fields.values():\n            if field.coerce and not issubclass(cls, RTStruct):\n                raise TypeError(\n                    f'{cls.__name__}.{field.name} cannot be declared '\n                    f'with coerce=True: {cls.__name__} is not an RTStruct',\n                )\n            if field.frozen and not issubclass(cls, RTStruct):\n                raise TypeError(\n                    f'{cls.__name__}.{field.name} cannot be declared '\n                    f'with frozen=True: {cls.__name__} is not an RTStruct',\n                )\n\n        cls._fields = fields\n        cls._sorted_fields = collections.OrderedDict(\n            sorted(fields.items(), key=lambda e: e[0]))\n        fa = '{}.{}_fields'.format(cls.__module__, cls.__name__)\n        setattr(cls, fa, myfields)\n        return cls\n\n    def get_field(cls, name: str) -> Optional[Field[Any]]:\n        return cls._fields.get(name)\n\n    def get_fields(cls, sorted: bool = False) -> dict[str, Field[Any]]:\n        return cls._sorted_fields if sorted else cls._fields\n\n    def get_ownfields(cls) -> dict[str, Field[Any]]:\n        return getattr(  # type: ignore\n            cls, '{}.{}_fields'.format(cls.__module__, cls.__name__))\n\n\nclass Struct(metaclass=StructMeta):\n    \"\"\"A base class allowing implementation of attribute objects protocols.\n\n    Each struct has a collection of ``Field`` objects, which should be defined\n    as class attributes of the ``Struct`` subclass.  Unlike\n    ``collections.namedtuple``, ``Struct`` is much easier to mix in and define.\n    Furthermore, fields are strictly typed and can be declared as required.  By\n    default, Struct will reject attributes, which have not been declared as\n    fields.  A ``MixedStruct`` subclass does have this restriction.\n\n    .. code-block:: pycon\n\n        >>> from edb.common.struct import Struct, Field\n\n        >>> class MyStruct(Struct):\n        ...    name = Field(type=str)\n        ...    description = Field(type=str, default=None)\n        ...\n        >>> MyStruct(name='Spam')\n        <MyStruct name=Spam>\n        >>> MyStruct(name='Ham', description='Good Ham')\n        <MyStruct name=Ham, description=Good Ham>\n\n    If ``use_slots`` is set to ``True`` in a class signature, ``__slots__``\n    will be used to create dictless instances, with reduced memory footprint:\n\n    .. code-block:: pycon\n\n        >>> class S1(Struct, use_slots=True):\n        ...     foo = Field(str, None)\n\n        >>> class S2(S1):\n        ...     bar = Field(str, None)\n\n        >>> S2().foo = '1'\n        >>> S2().bar = '2'\n\n        >>> S2().spam = '2'\n        AttributeError: 'S2' object has no attribute 'spam'\n    \"\"\"\n\n    def __init__(self, **kwargs: Any) -> None:\n        \"\"\"\n        :raises: TypeError if invalid field value was provided or a value was\n                 not provided for a field without a default value.\n        \"\"\"\n        self._check_init_argnames(kwargs)\n        self._init_fields(kwargs)\n\n    def __setstate__(self, state: Mapping[str, Any]) -> None:\n        if isinstance(state, tuple) and len(state) == 2:\n            state, slotstate = state\n        else:\n            slotstate = None\n\n        if state:\n            self.update(**state)\n\n        if slotstate:\n            self.update(**slotstate)\n\n    def update(self, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Update the field values.\"\"\"\n        values: dict[str, Any] = {}\n        values.update(*args, **kwargs)\n\n        self._check_init_argnames(values)\n\n        for k, v in values.items():\n            setattr(self, k, v)\n\n    def setdefaults(self) -> list[str]:\n        \"\"\"Initialize unset fields with default values.\"\"\"\n        fields_set = []\n        for field_name, field in self.__class__._fields.items():\n            value = getattr(self, field_name)\n            if value is None and field.default is not None:\n                value = self._getdefault(field_name, field)\n                self.set_default_value(field_name, value)\n                fields_set.append(field_name)\n        return fields_set\n\n    def set_default_value(self, field_name: str, value: Any) -> None:\n        setattr(self, field_name, value)\n\n    def formatfields(\n        self,\n        formatter: str = 'str',\n    ) -> Iterator[tuple[str, str]]:\n        \"\"\"Return an iterator over fields formatted using `formatter`.\"\"\"\n        for name, field in self.__class__._fields.items():\n            formatter_obj = field.formatters.get(formatter)\n            if formatter_obj:\n                yield (name, formatter_obj(getattr(self, name)))\n\n    def _copy_and_replace[Struct_T: Struct](\n        self,\n        cls: type[Struct_T],\n        **replacements: Any,\n    ) -> Struct_T:\n        args = {f: getattr(self, f) for f in cls._fields.keys()}\n        if replacements:\n            args.update(replacements)\n        return cls(**args)\n\n    def copy_with_class[Struct_T: Struct](\n        self, cls: type[Struct_T]\n    ) -> Struct_T:\n        return self._copy_and_replace(cls)\n\n    def copy(self: Self) -> Self:\n        return self.copy_with_class(type(self))\n\n    def replace(self: Self, **replacements: Any) -> Self:\n        return self._copy_and_replace(type(self), **replacements)\n\n    def items(self) -> Iterator[tuple[str, Any]]:\n        for field in self.__class__._fields:\n            yield field, getattr(self, field, None)\n\n    def as_tuple(self) -> tuple[Any, ...]:\n        result = []\n        for field in self.__class__._fields:\n            result.append(getattr(self, field, None))\n        return tuple(result)\n\n    __copy__ = copy\n\n    def __iter__(self) -> Iterator[str]:\n        return iter(self.__class__._fields)\n\n    def __str__(self) -> str:\n        fields = ', '.join(\n            f'{name}={value}'\n            for name, value in self.formatfields('str')\n        )\n        if fields:\n            fields = f' {fields}'\n        return f'<{self.__class__.__name__}{fields} at {id(self):#x}>'\n\n    def __repr__(self) -> str:\n        fields = ', '.join(\n            f'{name}={value}'\n            for name, value in self.formatfields('repr')\n        )\n        if fields:\n            fields = f' {fields}'\n        return f'<{self.__class__.__name__}{fields} at {id(self):#x}>'\n\n    def _init_fields(\n        self,\n        values: Mapping[str, Any],\n    ) -> None:\n        for field_name, field in self.__class__._fields.items():\n            value = values.get(field_name)\n\n            if value is None and field.default is not None:\n                value = self._getdefault(field_name, field)\n\n            setattr(self, field_name, value)\n\n    def _check_init_argnames(self, args: Iterable[str]) -> None:\n        extra = set(args) - set(self.__class__._fields) - {'_in_init_'}\n        if extra:\n            fmt = '{} {} invalid argument{} for struct {}.{}'\n            plural = len(extra) > 1\n            msg = fmt.format(\n                ', '.join(extra), 'are' if plural else 'is an', 's' if plural\n                else '', self.__class__.__module__, self.__class__.__name__)\n            raise TypeError(msg)\n\n    def _getdefault[T](\n        self,\n        field_name: str,\n        field: Field[T],\n    ) -> T:\n        ftype = field.type\n        if field.default == ftype:\n            value = field.default()  # type: ignore\n        elif field.default is NoDefault:\n            raise TypeError(\n                '%s.%s.%s is required' % (\n                    self.__class__.__module__, self.__class__.__name__,\n                    field_name))\n        else:\n            value = field.default\n\n        return value  # type: ignore\n\n    def get_field_value(self, field_name: str) -> Any:\n        try:\n            return self.__dict__[field_name]\n        except KeyError as e:\n            field = self.__class__.get_field(field_name)\n            if field is None:\n                raise TypeError(\n                    f'{field_name} is not a valid field in this struct')\n            try:\n                return self._getdefault(field_name, field)\n            except TypeError:\n                raise e\n\n\nclass RTStruct(Struct):\n    \"\"\"A variant of Struct with runtime type validation\"\"\"\n\n    __slots__ = ('_in_init_',)\n\n    def __init__(self, **kwargs: Any) -> None:\n        \"\"\"\n        :raises: TypeError if invalid field value was provided or a value was\n                 not provided for a field without a default value.\n        \"\"\"\n        self._check_init_argnames(kwargs)\n\n        self._in_init_ = True\n        try:\n            self._init_fields(kwargs)\n        finally:\n            self._in_init_ = False\n\n    def __setstate__(self, state: Mapping[str, Any]) -> None:\n        self._in_init_ = True\n        try:\n            super().__setstate__(state)\n        finally:\n            self._in_init_ = False\n\n    def __setattr__(self, name: str, value: Any) -> None:\n        field = type(self)._fields.get(name)\n        if field is not None:\n            value = self._check_field_type(field, name, value)\n            if field.frozen and not self._in_init_:\n                raise ValueError(f'cannot assign to frozen field {name!r}')\n        super().__setattr__(name, value)\n\n    def _check_field_type[T](self, field: Field[T], name: str, value: Any) -> T:\n        if (field.type and value is not None and\n                not isinstance(value, field.type)):\n            if field.coerce:\n                ftype = field.type\n\n                if issubclass(ftype, (checked.AbstractCheckedList,\n                                      checked.AbstractCheckedSet)):\n                    val_list = []\n                    for v in value:\n                        if v is not None and not isinstance(v, ftype.type):\n                            v = ftype.type(v)\n                        val_list.append(v)\n                    value = val_list\n                elif issubclass(ftype, checked.CheckedDict):\n                    val_dict = {}\n                    for k, v in value.items():\n                        if k is not None and not isinstance(k, ftype.keytype):\n                            k = ftype.keytype(k)\n                        if (v is not None and\n                                not isinstance(v, ftype.valuetype)):\n                            v = ftype.valuetype(v)\n                        val_dict[k] = v\n\n                    value = val_dict\n\n                try:\n                    return ftype(value)  # type: ignore\n                except Exception as ex:\n                    raise TypeError(\n                        'cannot coerce {!r} value {!r} '\n                        'to {}'.format(name, value, ftype)) from ex\n\n            raise TypeError(\n                '{}.{}.{}: expected {} but got {!r}'.format(\n                    self.__class__.__module__, self.__class__.__name__, name,\n                    field.type.__name__, value))\n\n        return value  # type: ignore\n\n\nclass MixedStructMeta(StructMeta):\n    def __new__(\n        mcls,\n        name: str,\n        bases: tuple[type, ...],\n        clsdict: dict[str, Any],\n        *,\n        use_slots: bool = False,\n        **kwargs: Any,\n    ) -> MixedStructMeta:\n        return super().__new__(\n            mcls,\n            name,\n            bases,\n            clsdict,\n            use_slots=use_slots,\n            **kwargs,\n        )\n\n\nclass MixedStruct(Struct, metaclass=MixedStructMeta):\n    def _check_init_argnames(self, args: Iterable[Any]) -> None:\n        pass\n\n\nclass MixedRTStruct(RTStruct, metaclass=MixedStructMeta):\n    def _check_init_argnames(self, args: Iterable[Any]) -> None:\n        pass\n"
  },
  {
    "path": "edb/common/supervisor.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Optional\n\nimport asyncio\nimport itertools\n\n\nclass Supervisor:\n\n    def __init__(self, *, _name, _loop, _private):\n        if _name is None:\n            self._name = f'sup#{_name_counter()}'\n        else:\n            self._name = str(_name)\n\n        self._loop = _loop\n        self._unfinished_tasks = 0\n        self._cancelled = False\n        self._tasks = set()\n        self._errors = []\n        self._base_error = None\n        self._on_completed_fut = None\n\n    @classmethod\n    async def create(cls, *, name: Optional[str] = None):\n        loop = asyncio.get_running_loop()\n        return cls(_loop=loop, _name=name, _private=True)\n\n    def __repr__(self):\n        msg = f'<Supervisor {self._name!r}'\n        if self._tasks:\n            msg += f' tasks:{len(self._tasks)}'\n        if self._unfinished_tasks:\n            msg += f' unfinished:{self._unfinished_tasks}'\n        if self._errors:\n            msg += f' errors:{len(self._errors)}'\n        if self._cancelled:\n            msg += ' cancelling'\n        msg += '>'\n        return msg\n\n    def create_task(self, coro):\n        if self._cancelled:\n            raise RuntimeError(\n                f'supervisor {self!r} has already been cancelled')\n\n        task = self._loop.create_task(coro)\n        task.add_done_callback(self._on_task_done)\n\n        self._unfinished_tasks += 1\n        self._tasks.add(task)\n\n        return task\n\n    async def cancel(self):\n        self._cancel()\n\n        if self._unfinished_tasks:\n            was_cancelled = await self._wait()\n            if was_cancelled:\n                raise asyncio.CancelledError()\n\n    async def wait(self):\n        if self._unfinished_tasks:\n            was_cancelled = await self._wait()\n            if was_cancelled:\n                raise asyncio.CancelledError()\n\n        if self._base_error is not None:\n            raise self._base_error\n\n        if self._errors:\n            # Exceptions are heavy objects that can have object\n            # cycles (bad for GC); let's not keep a reference to\n            # a bunch of them.\n            errors = self._errors\n            self._errors = []\n\n            me = ExceptionGroup('unhandled errors in a Supervisor', errors)\n            raise me from None\n\n    async def _wait(self):\n        was_cancelled = False\n\n        # We use while-loop here because \"self._on_completed_fut\"\n        # can be cancelled multiple times if our parent task\n        # is being cancelled repeatedly (or even once, when\n        # our own cancellation is already in progress)\n        while self._unfinished_tasks:\n            if self._on_completed_fut is None:\n                self._on_completed_fut = self._loop.create_future()\n\n            try:\n                await self._on_completed_fut\n            except asyncio.CancelledError:\n                was_cancelled = True\n                self._cancel()\n\n            self._on_completed_fut = None\n\n        assert self._unfinished_tasks == 0\n        self._on_completed_fut = None  # no longer needed\n\n        return was_cancelled\n\n    def _on_task_done(self, task):\n        self._unfinished_tasks -= 1\n\n        assert self._unfinished_tasks >= 0\n\n        if self._on_completed_fut is not None and not self._unfinished_tasks:\n            if not self._on_completed_fut.done():\n                self._on_completed_fut.set_result(True)\n\n        if task.cancelled():\n            return\n\n        exc = task.exception()\n        if exc is None:\n            return\n\n        self._errors.append(exc)\n        if self._is_base_error(exc) and self._base_error is None:\n            self._base_error = exc\n\n        self._cancel()\n\n    def _cancel(self):\n        self._cancelled = True\n\n        for t in self._tasks:\n            if not t.done():\n                t.cancel()\n\n    def _is_base_error(self, exc):\n        assert isinstance(exc, BaseException)\n        return not isinstance(exc, Exception)\n\n\n_name_counter = itertools.count(1).__next__\n"
  },
  {
    "path": "edb/common/term.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"A collection of functions and classes to simplify output to terminal.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Optional\n\nimport os\nimport sys\nimport fcntl\nimport termios\nimport struct\nimport functools\n\nfrom edb.common.colorsys import rgb_distance as color_distance\nfrom edb.common.colorsys import Color\n\n\ndef isatty(fileno):\n    return os.isatty(fileno)\n\n\n_COLORS: Optional[int] = None\n\n_colorize = 'auto'\n\n\ndef set_colorization_option(option):\n    global _colorize\n    _colorize = option\n\n\ndef max_colors():\n    \"\"\"Max colors current terminal supports.\n\n    :returns: Integer. For instance, for 'xterm' it is usually 256\n\n    .. note:: Uses :mod:`curses`\n    \"\"\"\n    global _COLORS\n\n    if _COLORS is None:\n        try:\n            import curses\n            try:\n                curses.setupterm()\n                _COLORS = curses.tigetnum('colors')\n            except (OSError, curses.error):\n                pass\n        except ImportError:\n            pass\n\n    if _COLORS is None:\n        _COLORS = 1\n\n    return _COLORS\n\n\ndef supports_colors(fileno):\n    \"\"\"Check if ``fileno`` file-descriptor supports colored output.\n\n    :params int fileno: file-descriptor\n    :returns: bool\n    \"\"\"\n    return (\n        isatty(fileno) and os.getenv('TERM') != 'dumb' and\n        os.getenv('ANSI_COLORS_DISABLED') is None)\n\n\ndef size(fileno):\n    \"\"\"Current terminal height and width (lines and columns).\n\n    :params int fileno: file-descriptor\n    :returns: Tuple of two integers - lines and columns respectively.\n              ``(None, None)`` if ``fileno`` is not a terminal\n    \"\"\"\n    if not isatty(fileno):\n        return None, None\n\n    try:\n        size = struct.unpack(\n            '2h', fcntl.ioctl(fileno, termios.TIOCGWINSZ, '    '))\n    except Exception:\n        size = (os.getenv('LINES', 25), os.getenv('COLUMNS', 80))\n\n    return size\n\n\ndef use_colors(fileno=None):\n    \"\"\"Check on whether use colored output or not.\n\n    Checks ``shell.MainCommand.colorize`` config setting and\n    ``fileno`` for being capable of displaying colors.\n\n    :param int fileno: File-descriptor. If ``None``, checks on ``sys.stdout``\n    :returns bool: Whether you can or can not use color terminal output\n    \"\"\"\n    if _colorize == 'on':\n        return True\n\n    if _colorize == 'off':\n        return False\n\n    assert _colorize == 'auto'\n\n    if fileno is None:\n        try:\n            fileno = sys.stdout.fileno()\n        except OSError:\n            return False\n\n    return supports_colors(fileno)\n\n\n# XTerm 256 colors table.\n#\n_MAP256 = {\n    16: '#000000',\n    17: '#00005f',\n    18: '#000087',\n    19: '#0000af',\n    20: '#0000d7',\n    21: '#0000ff',\n    22: '#005f00',\n    23: '#005f5f',\n    24: '#005f87',\n    25: '#005faf',\n    26: '#005fd7',\n    27: '#005fff',\n    28: '#008700',\n    29: '#00875f',\n    30: '#008787',\n    31: '#0087af',\n    32: '#0087d7',\n    33: '#0087ff',\n    34: '#00af00',\n    35: '#00af5f',\n    36: '#00af87',\n    37: '#00afaf',\n    38: '#00afd7',\n    39: '#00afff',\n    40: '#00d700',\n    41: '#00d75f',\n    42: '#00d787',\n    43: '#00d7af',\n    44: '#00d7d7',\n    45: '#00d7ff',\n    46: '#00ff00',\n    47: '#00ff5f',\n    48: '#00ff87',\n    49: '#00ffaf',\n    50: '#00ffd7',\n    51: '#00ffff',\n    52: '#5f0000',\n    53: '#5f005f',\n    54: '#5f0087',\n    55: '#5f00af',\n    56: '#5f00d7',\n    57: '#5f00ff',\n    58: '#5f5f00',\n    59: '#5f5f5f',\n    60: '#5f5f87',\n    61: '#5f5faf',\n    62: '#5f5fd7',\n    63: '#5f5fff',\n    64: '#5f8700',\n    65: '#5f875f',\n    66: '#5f8787',\n    67: '#5f87af',\n    68: '#5f87d7',\n    69: '#5f87ff',\n    70: '#5faf00',\n    71: '#5faf5f',\n    72: '#5faf87',\n    73: '#5fafaf',\n    74: '#5fafd7',\n    75: '#5fafff',\n    76: '#5fd700',\n    77: '#5fd75f',\n    78: '#5fd787',\n    79: '#5fd7af',\n    80: '#5fd7d7',\n    81: '#5fd7ff',\n    82: '#5fff00',\n    83: '#5fff5f',\n    84: '#5fff87',\n    85: '#5fffaf',\n    86: '#5fffd7',\n    87: '#5fffff',\n    88: '#870000',\n    89: '#87005f',\n    90: '#870087',\n    91: '#8700af',\n    92: '#8700d7',\n    93: '#8700ff',\n    94: '#875f00',\n    95: '#875f5f',\n    96: '#875f87',\n    97: '#875faf',\n    98: '#875fd7',\n    99: '#875fff',\n    100: '#878700',\n    101: '#87875f',\n    102: '#878787',\n    103: '#8787af',\n    104: '#8787d7',\n    105: '#8787ff',\n    106: '#87af00',\n    107: '#87af5f',\n    108: '#87af87',\n    109: '#87afaf',\n    110: '#87afd7',\n    111: '#87afff',\n    112: '#87d700',\n    113: '#87d75f',\n    114: '#87d787',\n    115: '#87d7af',\n    116: '#87d7d7',\n    117: '#87d7ff',\n    118: '#87ff00',\n    119: '#87ff5f',\n    120: '#87ff87',\n    121: '#87ffaf',\n    122: '#87ffd7',\n    123: '#87ffff',\n    124: '#af0000',\n    125: '#af005f',\n    126: '#af0087',\n    127: '#af00af',\n    128: '#af00d7',\n    129: '#af00ff',\n    130: '#af5f00',\n    131: '#af5f5f',\n    132: '#af5f87',\n    133: '#af5faf',\n    134: '#af5fd7',\n    135: '#af5fff',\n    136: '#af8700',\n    137: '#af875f',\n    138: '#af8787',\n    139: '#af87af',\n    140: '#af87d7',\n    141: '#af87ff',\n    142: '#afaf00',\n    143: '#afaf5f',\n    144: '#afaf87',\n    145: '#afafaf',\n    146: '#afafd7',\n    147: '#afafff',\n    148: '#afd700',\n    149: '#afd75f',\n    150: '#afd787',\n    151: '#afd7af',\n    152: '#afd7d7',\n    153: '#afd7ff',\n    154: '#afff00',\n    155: '#afff5f',\n    156: '#afff87',\n    157: '#afffaf',\n    158: '#afffd7',\n    159: '#afffff',\n    160: '#d70000',\n    161: '#d7005f',\n    162: '#d70087',\n    163: '#d700af',\n    164: '#d700d7',\n    165: '#d700ff',\n    166: '#d75f00',\n    167: '#d75f5f',\n    168: '#d75f87',\n    169: '#d75faf',\n    170: '#d75fd7',\n    171: '#d75fff',\n    172: '#d78700',\n    173: '#d7875f',\n    174: '#d78787',\n    175: '#d787af',\n    176: '#d787d7',\n    177: '#d787ff',\n    178: '#d7af00',\n    179: '#d7af5f',\n    180: '#d7af87',\n    181: '#d7afaf',\n    182: '#d7afd7',\n    183: '#d7afff',\n    184: '#d7d700',\n    185: '#d7d75f',\n    186: '#d7d787',\n    187: '#d7d7af',\n    188: '#d7d7d7',\n    189: '#d7d7ff',\n    190: '#d7ff00',\n    191: '#d7ff5f',\n    192: '#d7ff87',\n    193: '#d7ffaf',\n    194: '#d7ffd7',\n    195: '#d7ffff',\n    196: '#ff0000',\n    197: '#ff005f',\n    198: '#ff0087',\n    199: '#ff00af',\n    200: '#ff00d7',\n    201: '#ff00ff',\n    202: '#ff5f00',\n    203: '#ff5f5f',\n    204: '#ff5f87',\n    205: '#ff5faf',\n    206: '#ff5fd7',\n    207: '#ff5fff',\n    208: '#ff8700',\n    209: '#ff875f',\n    210: '#ff8787',\n    211: '#ff87af',\n    212: '#ff87d7',\n    213: '#ff87ff',\n    214: '#ffaf00',\n    215: '#ffaf5f',\n    216: '#ffaf87',\n    217: '#ffafaf',\n    218: '#ffafd7',\n    219: '#ffafff',\n    220: '#ffd700',\n    221: '#ffd75f',\n    222: '#ffd787',\n    223: '#ffd7af',\n    224: '#ffd7d7',\n    225: '#ffd7ff',\n    226: '#ffff00',\n    227: '#ffff5f',\n    228: '#ffff87',\n    229: '#ffffaf',\n    230: '#ffffd7',\n    231: '#ffffff',\n    232: '#080808',\n    233: '#121212',\n    234: '#1c1c1c',\n    235: '#262626',\n    236: '#303030',\n    237: '#3a3a3a',\n    238: '#444444',\n    239: '#4e4e4e',\n    240: '#585858',\n    241: '#606060',\n    242: '#666666',\n    243: '#767676',\n    244: '#808080',\n    245: '#8a8a8a',\n    246: '#949494',\n    247: '#9e9e9e',\n    248: '#a8a8a8',\n    249: '#b2b2b2',\n    250: '#bcbcbc',\n    251: '#c6c6c6',\n    252: '#d0d0d0',\n    253: '#dadada',\n    254: '#e4e4e4',\n    255: '#eeeeee'\n}\n\n\ndef _is_opt_getter(name: str):\n    return lambda self: self._is_opt(name)\n\n\ndef _set_opt_setter(name: str):\n    return lambda self, value: self._set_opt(name, value)\n\n\nclass AbstractStyle:\n    \"\"\"Encapsulates information about text-style.\n\n    For instance, what color should text be, should it be\n    underlined or bold etc.\n\n    Use instances of :class:`Style16` or :class:`Style256`,\n    this class is abstract.\n    \"\"\"\n\n    __slots__ = (\n        '_opts', '_color', '_bgcolor', '_term_prefix', '_term_postfix')\n\n    _opts_table = {\n        'bold': '1',\n        'faint': '2',\n        'italic': '3',\n        'underline': '4',\n        'blink': '5',\n        'overline': '6',\n        'reverse': '7'\n    }\n    _ropts_table = {v: k for k, v in _opts_table.items()}\n\n    def __init__(\n        self,\n        *,\n        color=None,\n        bgcolor=None,\n        bold=False,\n        faint=False,\n        italic=False,\n        underline=False,\n        overline=False,\n        reverse=False,\n    ):\n\n        self._opts = set()\n        self._color = None\n        self._bgcolor = None\n\n        self.color = color\n        self.bgcolor = bgcolor\n\n        self.bold = bold\n        self.faint = faint\n        self.italic = italic\n        self.underline = underline\n        self.overline = overline\n        self.reverse = reverse\n\n    def _filter_color(self, color):\n        raise NotImplementedError\n\n    def _get_color(self):\n        return self._rcolor_table[self._color]\n\n    def _set_color(self, color):\n        self._color = self._filter_color(color)\n        self._recalc()\n\n    color = property(_get_color, _set_color)\n\n    def _get_bgcolor(self):\n        return self._rcolor_table[self._bgcolor]\n\n    def _set_bgcolor(self, color):\n        self._bgcolor = self._filter_color(color)\n        self._recalc()\n\n    bgcolor = property(_get_bgcolor, _set_bgcolor)\n\n    @property\n    def empty(self):\n        return not bool(self._term_prefix)\n\n    def _is_opt(self, name: str) -> bool:\n        assert name in self._opts_table\n        return self._opts_table[name] in self._opts\n\n    def _set_opt(self, name, value):\n        try:\n            tr_name = self._opts_table[name]\n        except KeyError:\n            raise ValueError('unknown style option {!r}'.format(name))\n\n        if value:\n            self._opts.add(tr_name)\n        else:\n            if tr_name in self._opts:\n                self._opts.discard(tr_name)\n        self._recalc()\n\n    bold = property(_is_opt_getter('bold'), _set_opt_setter('bold'))\n    faint = property(_is_opt_getter('faint'), _set_opt_setter('faint'))\n    italic = property(_is_opt_getter('italic'), _set_opt_setter('italic'))\n    underline = property(\n        _is_opt_getter('underline'), _set_opt_setter('underline'))\n    blink = property(_is_opt_getter('blink'), _set_opt_setter('blink'))\n    overline = property(\n        _is_opt_getter('overline'), _set_opt_setter('overline'))\n    reverse = property(_is_opt_getter('reverse'), _set_opt_setter('reverse'))\n\n    def _recalc(self):\n        cmd = []\n\n        if self._color is not None:\n            if self._color > 15:\n                cmd.append('38;5;{}'.format(self._color))\n            else:\n                cmd.append('3{}'.format(self._color))\n\n        if self._bgcolor is not None:\n            if self._bgcolor > 15:\n                cmd.append('48;5;{}'.format(self._bgcolor))\n            else:\n                cmd.append('4{}'.format(self._bgcolor))\n\n        cmd.extend(self._opts)\n\n        if cmd:\n            self._term_prefix = '\\x1B[{}m'.format(';'.join(cmd))\n            self._term_postfix = '\\x1B[0m'\n        else:\n            self._term_prefix = ''\n            self._term_postfix = ''\n\n    def apply(self, str):\n        \"\"\"Apply ANSI escape sequences to :param:str.\n\n        If the result can be printed to a terminal that supports styling.\n        \"\"\"\n        return self._term_prefix + str + self._term_postfix\n\n\nclass Style16(AbstractStyle):\n    \"\"\"16-color style.\"\"\"\n\n    _color_table = {\n        'black': 0,\n        'red': 1,\n        'green': 2,\n        'yellow': 3,\n        'blue': 4,\n        'magenta': 5,\n        'cyan': 6,\n        'white': 7\n    }\n    _rcolor_table = {v: k for k, v in _color_table.items()}\n\n    def _filter_color(self, color):\n        if color is None:\n            return None\n\n        try:\n            return self._color_table[color]\n        except KeyError as ex:\n            raise ValueError('unknown color {!r}'.format(color)) from ex\n\n\nclass Style256(AbstractStyle):\n    \"\"\"256-color style.\n\n    Accepts any rgb color in hex format, for instance:\n\n    .. code-block:: pycon\n\n        >>> Style256(color='#abcdef')\n\n    Or by css name:\n\n    .. code-block:: pycon\n\n        >>> Style256(color='chocolate')\n\n    In case of a color being outside of standard xterm 256 color palette,\n    it'll try to locate the closest color in it.\n    \"\"\"\n\n    _color_table = {v: k for k, v in _MAP256.items()}\n    _rcolor_table = _MAP256\n\n    _rgb_color_table = {\n        Color.from_string(v).rgb_channels(as_floats=True): k\n        for k, v in _MAP256.items()\n    }\n\n    @staticmethod\n    @functools.lru_cache(500)\n    def _filter_color(color):\n        if color is None:\n            return None\n\n        try:\n            return Style256._color_table[color]\n        except KeyError:\n            pass\n\n        c = Color.from_string(color).rgb_channels(as_floats=True)\n        return min(\n            Style256._rgb_color_table.items(),\n            key=lambda item: color_distance(item[0][0], item[0][1],\n                                            item[0][2], *c))[1]\n\n\nclass StylesTable:\n    \"\"\"Base class for simple style tables.\"\"\"\n\n    def __getattr__(self, key):\n        # If we're querying some non-existing style, pretend it's empty\n        #\n        return Style16()\n\n    def dump(self):\n        for name, style in self.__class__.__dict__.items():\n            if isinstance(style, AbstractStyle):\n                print(style.apply(name))\n"
  },
  {
    "path": "edb/common/token_bucket.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport time\n\n\nclass TokenBucket:\n    _capacity: float\n    _token_per_sec: float\n    _tokens: float\n    _last_fill_time: float\n\n    def __init__(self, capacity: float, token_per_sec: float):\n        self._capacity = capacity\n        self._token_per_sec = token_per_sec\n        self._tokens = capacity\n        self._last_fill_time = time.monotonic()\n\n    def consume(self, tokens: int) -> float:\n        if tokens <= 0:\n            return True\n        now = time.monotonic()\n        tokens_to_add = (now - self._last_fill_time) * self._token_per_sec\n        self._tokens = min(self._capacity, self._tokens + tokens_to_add)\n        self._last_fill_time = now\n        left = self._tokens - tokens\n        if left >= 0:\n            self._tokens -= tokens\n            return 0\n        else:\n            return -left / (tokens * self._token_per_sec)\n"
  },
  {
    "path": "edb/common/topological.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    Protocol,\n    Iterable,\n    Iterator,\n    Mapping,\n    MutableSet,\n    TYPE_CHECKING,\n)\n\nfrom collections import defaultdict\n\nfrom edb.common.ordered import OrderedSet\n\n\nclass UnresolvedReferenceError(Exception):\n    pass\n\n\nclass CycleError(Exception):\n    def __init__(\n        self,\n        msg: str,\n        item: Any,\n        path: tuple[Any, ...] = (),\n    ) -> None:\n        super().__init__(msg)\n        self.item = item\n        self.path = path\n\n\nclass DepGraphEntry[K, V, T]:\n\n    #: The graph node\n    item: V\n    #: An optional set of dependencies for the graph node as lookup keys.\n    deps: MutableSet[K]\n    #: An optional set of *weak* dependencies for the graph node as\n    #: lookup keys.  The difference from regular deps is that weak deps\n    #: that cause cycles are ignored.  Essentially, weak deps dictate\n    #: a _preference_ in order rather than a requirement.\n    weak_deps: MutableSet[K]\n    merge: Optional[MutableSet[K]]\n    loop_control: MutableSet[K]\n    extra: Optional[T]\n\n    def __init__(\n        self,\n        item: V,\n        deps: Optional[MutableSet[K]] = None,\n        merge: Optional[MutableSet[K]] = None,\n        loop_control: Optional[MutableSet[K]] = None,\n        extra: Optional[T] = None,\n        weak_deps: Optional[MutableSet[K]] = None,\n    ) -> None:\n        self.item = item\n        if deps is None:\n            deps = set()\n        self.deps = deps\n        self.merge = merge\n        if loop_control is None:\n            loop_control = set()\n        self.loop_control = loop_control\n        self.extra = extra\n        if weak_deps is None:\n            weak_deps = set()\n        self.weak_deps = weak_deps\n\n\ndef sort_ex[K, V, T](\n    graph: Mapping[K, DepGraphEntry[K, V, T]],\n    *,\n    allow_unresolved: bool = False,\n) -> Iterator[tuple[K, DepGraphEntry[K, V, T]]]:\n\n    adj: dict[K, OrderedSet[K]] = defaultdict(OrderedSet)\n    weak_adj: dict[K, OrderedSet[K]] = defaultdict(OrderedSet)\n    loop_control: dict[K, OrderedSet[K]] = defaultdict(OrderedSet)\n\n    for item_name, item in graph.items():\n        if item.weak_deps:\n            for dep in item.weak_deps:\n                if dep in graph:\n                    weak_adj[item_name].add(dep)\n                elif not allow_unresolved:\n                    raise UnresolvedReferenceError(\n                        'reference to an undefined item {} in {}'.format(\n                            dep, item_name))\n\n        if item.merge is not None:\n            for merge in item.merge:\n                if merge in graph:\n                    adj[item_name].add(merge)\n                elif not allow_unresolved:\n                    raise UnresolvedReferenceError(\n                        'reference to an undefined item {} in {}'.format(\n                            merge, item_name))\n\n        if item.deps:\n            for dep in item.deps:\n                if dep in graph:\n                    adj[item_name].add(dep)\n                elif not allow_unresolved:\n                    raise UnresolvedReferenceError(\n                        'reference to an undefined item {} in {}'.format(\n                            dep, item_name))\n\n        if item.loop_control:\n            for ctrl in item.loop_control:\n                if ctrl in graph:\n                    loop_control[item_name].add(ctrl)\n                elif not allow_unresolved:\n                    raise UnresolvedReferenceError(\n                        'reference to an undefined item {} in {}'.format(\n                            ctrl, item_name))\n\n    visiting: OrderedSet[K] = OrderedSet()\n    visiting_weak: MutableSet[K] = set()\n    visited = set()\n    order = []\n\n    def visit(\n        item: K,\n        for_control: bool = False,\n        weak_link: bool = False,\n    ) -> None:\n        if item in visiting:\n            # Separate the matching item from the rest of the visiting\n            # set for error reporting.\n            vis_list = tuple(visiting - {item})\n            cycle_item = item if len(vis_list) == 0 else vis_list[-1]\n            raise CycleError(\n                f\"dependency cycle between {cycle_item!r} \"\n                f\"and {item!r}\",\n                path=vis_list,\n                item=item,\n            )\n        if item not in visited:\n            visiting.add(item)\n            if weak_link:\n                visiting_weak.add(item)\n\n            try:\n                for n in weak_adj[item]:\n                    try:\n                        visit(n, weak_link=True)\n                    except CycleError:\n                        if len(visiting_weak) == 0:\n                            pass\n                        else:\n                            raise\n                for n in adj[item]:\n                    visit(n, weak_link=weak_link)\n                for n in loop_control[item]:\n                    visit(n, weak_link=weak_link, for_control=True)\n                if not for_control:\n                    order.append(item)\n                    visited.add(item)\n            except CycleError:\n                if len(visiting_weak) == 1:\n                    pass\n                else:\n                    raise\n            finally:\n                visiting.remove(item)\n                if weak_link:\n                    visiting_weak.remove(item)\n\n    for key in graph:\n        visit(key)\n\n    return ((key, graph[key]) for key in order)\n\n\ndef sort[K, V, T](\n    graph: Mapping[K, DepGraphEntry[K, V, T]],\n    *,\n    allow_unresolved: bool = False,\n) -> tuple[V, ...]:\n    items = sort_ex(graph, allow_unresolved=allow_unresolved)\n    return tuple(i[1].item for i in items)\n\n\nif TYPE_CHECKING:\n\n    class MergeFunction[V](Protocol):\n\n        def __call__(\n            self,\n            item: V,\n            parent: V,\n            **kwargs: Any,\n        ) -> V:\n            ...\n\n\ndef normalize[K, V, T](\n    graph: Mapping[K, DepGraphEntry[K, V, T]],\n    merger: MergeFunction[V],\n    **merger_kwargs: Any,\n) -> Iterable[V]:\n    merged: dict[K, V] = {}\n\n    for name, item in sort_ex(graph):\n        merge = item.merge\n        if merge:\n            for m in merge:\n                merger(item.item, merged[m], **merger_kwargs)\n\n        merged.setdefault(name, item.item)\n\n    return merged.values()\n"
  },
  {
    "path": "edb/common/traceback.py",
    "content": "# mypy: disable-error-code=\"attr-defined\"\n\n# Portions copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n# Portions copyright 2001-2019 Python Software Foundation.\n# License: PSFL.\n\n\"\"\"\nProvides stack trace formatting that prints `{filename}:{line}`, instead of\n`\"{filename}\", line {line}`.\n\nStolen from Python's traceback module.\n\"\"\"\n\nimport traceback\nimport typing\nfrom contextlib import suppress\n\nStackSummaryLike = (\n    traceback.StackSummary\n    | list[tuple[str, typing.Any, str, typing.Any]]\n)\n\n\ndef format_exception(e: BaseException) -> str:\n    exctype = type(e)\n    value = e\n    tb = e.__traceback__\n    tb_e = traceback.TracebackException(\n        exctype, value, tb, compact=True\n    )\n    tb_e.stack = StandardStackSummary(tb_e.stack)\n    return '\\n'.join(tb_e.format())\n\n\ndef format_stack_summary(stack: StackSummaryLike) -> list[str]:\n    return _format_stack_summary(_into_list_of_frames(stack))\n\n\nclass StandardStackSummary(traceback.StackSummary):\n    def format(self) -> list[str]:\n        return format_stack_summary(self)\n\n\ndef _into_list_of_frames(a_list: StackSummaryLike):\n    \"\"\"\n    Create a StackSummary object from a supplied list of\n    FrameSummary objects or old-style list of tuples.\n    \"\"\"\n    # While doing a fast-path check for isinstance(a_list, StackSummary) is\n    # appealing, idlelib.run.cleanup_traceback and other similar code may\n    # break this by making arbitrary frames plain tuples, so we need to\n    # check on a frame by frame basis.\n    result = []\n    for frame in a_list:\n        if isinstance(frame, traceback.FrameSummary):\n            result.append(frame)\n        else:\n            filename, lineno, name, line = frame\n            result.append(\n                traceback.FrameSummary(filename, lineno, name, line=line)\n            )\n    return result\n\n\ndef _format_stack_summary(stack: list[traceback.FrameSummary]):\n    \"\"\"Format the stack ready for printing.\n\n    Returns a list of strings ready for printing.  Each string in the\n    resulting list corresponds to a single frame from the stack.\n    Each string ends in a newline; the strings may contain internal\n    newlines as well, for those items with source text lines.\n\n    For long sequences of the same frame and line, the first few\n    repetitions are shown, followed by a summary line stating the exact\n    number of further repetitions.\n    \"\"\"\n    result = []\n    last_file = None\n    last_line = None\n    last_name = None\n    count = 0\n    for frame_summary in stack:\n        formatted_frame = _format_frame_summary(frame_summary)\n        if formatted_frame is None:\n            continue\n        if (\n            last_file is None\n            or last_file != frame_summary.filename\n            or last_line is None\n            or last_line != frame_summary.lineno\n            or last_name is None\n            or last_name != frame_summary.name\n        ):\n            if count > traceback._RECURSIVE_CUTOFF:\n                count -= traceback._RECURSIVE_CUTOFF\n                result.append(\n                    f'  [Previous line repeated {count} more '\n                    f'time{\"s\" if count > 1 else \"\"}]\\n'\n                )\n            last_file = frame_summary.filename\n            last_line = frame_summary.lineno\n            last_name = frame_summary.name\n            count = 0\n        count += 1\n        if count > traceback._RECURSIVE_CUTOFF:\n            continue\n        result.append(formatted_frame)\n\n    if count > traceback._RECURSIVE_CUTOFF:\n        count -= traceback._RECURSIVE_CUTOFF\n        result.append(\n            f'  [Previous line repeated {count} more '\n            f'time{\"s\" if count > 1 else \"\"}]\\n'\n        )\n    return result\n\n\ndef _format_frame_summary(frame: traceback.FrameSummary):\n    \"\"\"Format the lines for a single FrameSummary.\n\n    Returns a string representing one frame involved in the stack. This\n    gets called for every frame to be printed in the stack summary.\n    \"\"\"\n    row = [f'  {frame.filename}:{frame.lineno}, in {frame.name}\\n']\n    if frame.line:\n        stripped_line = frame.line.strip()\n        row.append('    {}\\n'.format(stripped_line))\n\n        orig_line_len = len(frame._original_line)\n        frame_line_len = len(frame.line.lstrip())\n        stripped_characters = orig_line_len - frame_line_len\n        if frame.colno is not None and frame.end_colno is not None:\n            start_offset = (\n                traceback._byte_offset_to_character_offset(\n                    frame._original_line, frame.colno\n                )\n                + 1\n            )\n            end_offset = (\n                traceback._byte_offset_to_character_offset(\n                    frame._original_line, frame.end_colno\n                )\n                + 1\n            )\n\n            anchors = None\n            if frame.lineno == frame.end_lineno:\n                with suppress(Exception):\n                    anchors = (\n                        traceback._extract_caret_anchors_from_line_segment(\n                            frame._original_line[\n                                start_offset - 1 : end_offset - 1\n                            ]\n                        )\n                    )\n            else:\n                end_offset = stripped_characters + len(stripped_line)\n\n            # show indicators if primary char doesn't span the frame line\n            if end_offset - start_offset < len(stripped_line) or (\n                anchors\n                and anchors.right_start_offset - anchors.left_end_offset > 0\n            ):\n                row.append('    ')\n                row.append(' ' * (start_offset - stripped_characters))\n\n                if anchors:\n                    row.append(anchors.primary_char * (anchors.left_end_offset))\n                    row.append(\n                        anchors.secondary_char\n                        * (anchors.right_start_offset - anchors.left_end_offset)\n                    )\n                    row.append(\n                        anchors.primary_char\n                        * (\n                            end_offset\n                            - start_offset\n                            - anchors.right_start_offset\n                        )\n                    )\n                else:\n                    row.append('^' * (end_offset - start_offset))\n\n                row.append('\\n')\n\n    if frame.locals:\n        for name, value in sorted(frame.locals.items()):\n            row.append('    {name} = {value}\\n'.format(name=name, value=value))\n\n    return ''.join(row)\n"
  },
  {
    "path": "edb/common/turbo_uuid.pyi",
    "content": "from __future__ import annotations\n\nimport uuid\n\nclass UUID(uuid.UUID):\n    def __init__(self, inp: bytes | str) -> None:\n        ...\n"
  },
  {
    "path": "edb/common/typeutils.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Any, Callable, Optional, Sequence\n\nimport collections.abc\nimport functools\n\n\ndef chain_decorators[TC: Callable](\n    funcs: Sequence[Callable[[TC], TC]]\n) -> Callable[[TC], TC]:\n    def f(func: TC) -> TC:\n        for dec in reversed(funcs):\n            func = dec(func)\n        return func\n\n    return f\n\n\ndef downcast[T](typ: type[T], x: Any) -> T:\n    assert isinstance(x, typ)\n    return x\n\n\ndef not_none[T](x: Optional[T]) -> T:\n    assert x is not None\n    return x\n\n\n@functools.lru_cache(1024)\ndef _is_container_type(cls):\n    return (\n        issubclass(cls, (collections.abc.Container))\n        and not issubclass(cls, (str, bytes, bytearray, memoryview))\n        # not namedtuple, either\n        and not (issubclass(cls, tuple) and hasattr(cls, '_fields'))\n    )\n\n\n@functools.lru_cache(1024)\ndef _is_iterable_type(cls):\n    return (\n        issubclass(cls, collections.abc.Iterable)\n    )\n\n\ndef is_container(obj):\n    cls = obj.__class__\n    return _is_container_type(cls) and _is_iterable_type(cls)\n\n\ndef is_container_type(type_):\n    return isinstance(type_, type) and _is_container_type(type_)\n"
  },
  {
    "path": "edb/common/typing_inspect.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n# Portions copyright 2017-2020 Ivan Levkivskyi\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ntry:\n    # this will fail on Python 3.8 because it doesn't have `types.GenericAlias`\n    from edb.common._typing_inspect import *  # NoQA\nexcept ImportError:\n    from typing_inspect import *  # NoQA\n"
  },
  {
    "path": "edb/common/uuidgen.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport hashlib\nimport os\nimport re\nimport uuid\n\nfrom . import turbo_uuid\n\n\nUUID = turbo_uuid.UUID\n\n\nUUID_RE_S = r'[a-f0-9]{8}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{12}'\nUUID_RE = re.compile(UUID_RE_S, re.I)\n\n\ndef uuid1mc() -> uuid.UUID:\n    \"\"\"Generate a v1 UUID using a pseudo-random multicast node address.\"\"\"\n\n    # Note: cannot use pgproto.UUID since it's UUID v1\n    node = int.from_bytes(os.urandom(6), byteorder='little') | (1 << 40)\n    return UUID(uuid.uuid1(node=node).bytes)\n\n\n# type-ignores below because the first argument to uuid.UUID is a string\n# called `hex` which is not something that pgproto.UUID supports.\n\n\ndef uuid4() -> uuid.UUID:\n    \"\"\"Generate a random UUID.\"\"\"\n    return UUID(uuid.uuid4().bytes)\n\n\ndef uuid5_bytes(namespace: uuid.UUID, name: bytes | bytearray) -> uuid.UUID:\n    \"\"\"Generate a UUID from the SHA-1 hash of a namespace UUID and a name.\"\"\"\n    # Do the hashing ourselves because the stdlib version only supports str\n    hasher = hashlib.sha1(namespace.bytes)\n    hasher.update(name)\n    return UUID(uuid.UUID(bytes=hasher.digest()[:16], version=5).bytes)\n\n\ndef uuid5(namespace: uuid.UUID, name: str) -> uuid.UUID:\n    \"\"\"Generate a UUID from the SHA-1 hash of a namespace UUID and a name.\"\"\"\n    return uuid5_bytes(namespace, name.encode(\"utf-8\"))\n\n\ndef from_bytes(data: bytes) -> uuid.UUID:\n    return UUID(data)\n"
  },
  {
    "path": "edb/common/value_dispatch.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Any, Callable, Protocol, Iterable\n\nimport functools\nimport inspect\nimport types\n\n\nclass _ValueDispatchCallable[_T](Protocol):\n    registry: types.MappingProxyType[Any, Callable[..., _T]]\n\n    def register(\n        self,\n        val: Any,\n    ) -> Callable[[Callable[..., _T]], Callable[..., _T]]:\n        ...\n\n    def register_for_all(\n        self,\n        val: Iterable[Any],\n    ) -> Callable[[Callable[..., _T]], Callable[..., _T]]:\n        ...\n\n    def __call__(__self, *args: Any, **kwargs: Any) -> _T: ...\n\n\ndef value_dispatch[_T](func: Callable[..., _T]) -> _ValueDispatchCallable[_T]:\n    \"\"\"Like singledispatch() but dispatches by value of the first arg.\n\n    Example:\n\n      @value_dispatch\n      def eat(fruit):\n          return f\"I don't want a {fruit}...\"\n\n      @eat.register('apple')\n      def _eat_apple(fruit):\n          return \"I love apples!\"\n\n      @eat.register('eggplant')\n      @eat.register('squash')\n      def _eat_what(fruit):\n          return f\"I didn't know {fruit} is a fruit!\"\n\n    An alternative to applying multuple `register` decorators is to\n    use the `register_for_all` helper:\n\n      @eat.register_for_all({'eggplant', 'squash'})\n      def _eat_what(fruit):\n          return f\"I didn't know {fruit} is a fruit!\"\n    \"\"\"\n\n    registry: dict[Any, Callable[..., _T]] = {}\n\n    @functools.wraps(func)\n    def wrapper(arg0: Any, *args: Any, **kwargs: Any) -> _T:\n        try:\n            delegate = registry[arg0]\n        except KeyError:\n            pass\n        else:\n            return delegate(arg0, *args, **kwargs)\n\n        return func(arg0, *args, **kwargs)\n\n    def register(\n        value: Any,\n    ) -> Callable[[Callable[..., _T]], Callable[..., _T]]:\n        if inspect.isfunction(value):\n            raise TypeError(\n                \"value_dispatch.register() decorator requires a value\")\n\n        def wrap(func: Callable[..., _T]) -> Callable[..., _T]:\n            if value in registry:\n                raise ValueError(\n                    f'@value_dispatch: there is already a handler '\n                    f'registered for {value!r}'\n                )\n            registry[value] = func\n            return func\n        return wrap\n\n    def register_for_all(\n        values: Iterable[Any],\n    ) -> Callable[[Callable[..., _T]], Callable[..., _T]]:\n        def wrap(func: Callable[..., _T]) -> Callable[..., _T]:\n            for value in values:\n                if value in registry:\n                    raise ValueError(\n                        f'@value_dispatch: there is already a handler '\n                        f'registered for {value!r}'\n                    )\n                registry[value] = func\n            return func\n        return wrap\n\n    wrapper.register = register  # type: ignore [attr-defined]\n    wrapper.register_for_all = register_for_all  # type: ignore [attr-defined]\n    return wrapper  # type: ignore [return-value]\n"
  },
  {
    "path": "edb/common/verutils.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, NamedTuple\n\nimport enum\nimport re\n\n\nVERSION_PATTERN = re.compile(r\"\"\"\n    ^\n    (?P<release>[0-9]+(?:\\.[0-9]+)*)\n    (?P<pre>\n        [-\\.]?\n        (?P<pre_l>(a|b|c|rc|alpha|beta|dev))\n        [\\.]?\n        (?P<pre_n>[0-9]+)?\n    )?\n    (?:\\+(?P<local>[a-z0-9]+(?:[\\.][a-z0-9]+)*))?\n    $\n\"\"\", re.X)\n\n\nclass VersionStage(enum.IntEnum):\n    DEV = 0\n    ALPHA = 10\n    BETA = 20\n    RC = 30\n    FINAL = 40\n\n\nclass Version(NamedTuple):\n    major: int\n    minor: int\n    stage: VersionStage\n    stage_no: int\n    local: tuple[str, ...]\n\n    def __str__(self):\n        ver = f'{self.major}.{self.minor}'\n        if self.stage is not VersionStage.FINAL:\n            ver += f'-{self.stage.name.lower()}.{self.stage_no}'\n        if self.local:\n            ver += f'{(\"+\" + \".\".join(self.local)) if self.local else \"\"}'\n\n        return ver\n\n\ndef parse_version(ver: str) -> Version:\n    v = VERSION_PATTERN.match(ver)\n    if v is None:\n        raise ValueError(f'cannot parse version: {ver}')\n    local: list[str] = []\n    if v.group('pre'):\n        pre_l = v.group('pre_l')\n        if pre_l in {'a', 'alpha'}:\n            stage = VersionStage.ALPHA\n        elif pre_l in {'b', 'beta'}:\n            stage = VersionStage.BETA\n        elif pre_l in {'c', 'rc'}:\n            stage = VersionStage.RC\n        elif pre_l in {'dev'}:\n            stage = VersionStage.DEV\n        else:\n            raise ValueError(f'cannot determine release stage from {ver}')\n\n        stage_no = int(v.group('pre_n'))\n    else:\n        stage = VersionStage.FINAL\n        stage_no = 0\n    if v.group('local'):\n        local.extend(v.group('local').split('.'))\n\n    release = [int(r) for r in v.group('release').split('.')]\n\n    return Version(\n        major=release[0],\n        minor=release[1],\n        stage=stage,\n        stage_no=stage_no,\n        local=tuple(local),\n    )\n\n\ndef from_json(data: dict[str, Any]) -> Version:\n    return Version(\n        data['major'],\n        data['minor'],\n        VersionStage[data['stage'].upper()],\n        data['stage_no'],\n        tuple(data['local']),\n    )\n"
  },
  {
    "path": "edb/common/view_patterns.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Hacky implementation of \"view patterns\" with Python match\n\nA \"view pattern\" is one that does some transformation on the data\nbeing matched before attempting to match it. This can be super useful,\nas it allows writing \"helper functions\" for pattern matching.\n\nWe provide a class, ViewPattern, that can be subclassed with custom\n`match` methods that performs a transformation on the scrutinee,\nreturning a transformed value or raising NoMatch if a match is not\npossible.\n\nFor example, you could write:\n  @dataclasses.dataclass\n  class IntPair:\n      lhs: int\n      rhs: int\n\n  class sum_view(ViewPattern[int], targets=(IntPair,)):\n      @staticmethod\n      def match(obj: object) -> int:\n          match obj:\n              case IntPair(lhs, rhs):\n                  return lhs + rhs\n          raise view_patterns.NoMatch\n\nand then write code like:\n\n  match IntPair(lhs=10, rhs=15):\n      case sum_view(10):\n          print(\"NO!\")\n      case sum_view(25):\n          print(\"YES!\")\n\n----\n\nTo understand how this is implemented, we first discuss how pattern\nmatching a value `v` against a pattern like `C(<expr>)` is performed:\n 1. isinstance(v, C) is called. If it is False, the match fails\n 2. C.__match_args__ is fetched; it should contain a tuple of\n    attribute names to be used for positional matching.\n 3. In our case, there should be only one attribute in it, `attr`,\n    and v.attr is fetched. If fetching v.attr raises AttributeError,\n    the match fails.\n\nOur implementation strategy, then, is:\n a. Overload C's isinstance check by implementing `__instancecheck__`\n    in a metaclass. Return True if the instance is an instance of\n    one of the target classes.\n b. Make C's __match_args__ `('_view_result_<unique_name>',)`\n c. Arrange for `_view_result_<unique_name>` on the matched object to\n    call match and return that value. If match raises NoMatch, transform\n    it into AttributeError, so that the match fails.\n\nCalling match from the *getter* lets us avoid the need to save the\nvalue somewhere between steps a and c, but requires us to install one\nmethod per view in the scrutinee's class.\n\nHopefully Python will add __match__ and we can delete all this code!\n\"\"\"\n\n\nclass NoMatch(Exception):\n    pass\n\n\nclass ViewPatternMeta(type):\n    def __new__(mcls, name, bases, clsdict, *, targets=(), **kwargs):\n        cls = super().__new__(mcls, name, bases, clsdict, **kwargs)\n\n        @property  # type: ignore\n        def _view_result_getter(self):\n            try:\n                return cls.match(self)\n            except NoMatch:\n                raise AttributeError\n\n        fname = f'_view_result_{cls.__module__}.{cls.__qualname__}'\n        mangled = fname.replace(\"___\", \"___3_\").replace(\".\", \"___\")\n\n        cls.__match_args__ = (mangled,)  # type: ignore\n        cls._view_result_getter = _view_result_getter\n        cls._targets = targets\n\n        # Install the getter onto all target classes\n        for target in targets:\n            setattr(target, mangled, _view_result_getter)\n\n        return cls\n\n    def __instancecheck__(self, instance):\n        return isinstance(instance, self._targets)\n\n\nclass ViewPattern[_T](metaclass=ViewPatternMeta):\n    __match_args__ = ('result',)\n    result: _T\n\n    @classmethod\n    def match(cls, obj: object) -> _T:\n        raise NoMatch\n"
  },
  {
    "path": "edb/common/windowedsum.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nimport collections\nimport time\n\n\nclass WindowedSum:\n    \"\"\"Keeps the sum of incremented values from the last minute.\n\n    The sum is kept with second precision.\n\n    >>> s = WindowedSum()\n    >>> s += 1\n    >>> s += 1\n    >>> time.sleep(30)\n    >>> s += 1\n    >>> s += 1\n    >>> int(s)\n    4\n    >>> time.sleep(30)\n    >>> int(s)\n    2\n    \"\"\"\n\n    def __init__(self) -> None:\n        self._maxlen = 60\n        init: float = 0\n        self._buckets = collections.deque([init], maxlen=self._maxlen)\n        self._last_shift_at = 0.0\n\n    def __iadd__(self, val: float) -> WindowedSum:\n        self.shift()\n        self._buckets[-1] += val\n        return self\n\n    def __int__(self) -> int:\n        self.shift()\n        return int(sum(self._buckets))\n\n    def __float__(self) -> float:\n        self.shift()\n        return float(sum(self._buckets))\n\n    def shift(self) -> None:\n        now = time.monotonic()\n        shift_by = int(min(now - self._last_shift_at, self._maxlen))\n        if shift_by:\n            self._buckets.extend(shift_by * [0])\n            self._last_shift_at = now\n"
  },
  {
    "path": "edb/common/xdedent.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Library for building nicely indented output using f-strings.\n\ntextwrap.dedent allows removing extra indentation, but it performs\npoorly when strings get interpolated in before dedenting, especially\nif those strings were produced at a different level of indentation.\n\nThe `escape` function escapes a string for interpolation. Notionally,\nthe an interpolated escaped string has all of its leading indentation\nstripped, and when it is interpolated in, lines after the first are\nindented at the level the interpolated string appears in the output.\n\nInterpolating an escaped `LINE_BLANK` deletes a newline that appears\ndirectly before it. This can be useful when a branch might produce\nnothing, but it is interpolated nonconditionally.\n\nThe `xdedent` function takes a string with interpolated escaped\nstrings and properly formats it.\n\n\nThe system uses escape delimeters for maintaining a nesting structure\nin strings that the user produces. The `xdedent` function then parses apart\nthe nesting structure and interprets it.\nObviously, as with all schemes for\nin-band signalling, all hell can break loose if the signals appear in\nthe input data unescaped.\n\nOur signal sequences contain a null byte and both kinds of quote\ncharacter, so you should be fine unless the untrusted data:\n * has null bytes and\n * does not have any kind of quote character escaped in it somehow\n\n\"\"\"\n\nfrom __future__ import annotations\n\n\nimport textwrap\nfrom typing import Any\n\n_LEFT_ESCAPE = \"\\0'\\\"<<{<[<[{{<!!!\"\n_RIGHT_ESCAPE = \"\\0'\\\"!!!>}}]>]>}>>\"\n_ESCAPE_LEN = len(_LEFT_ESCAPE)\nassert len(_RIGHT_ESCAPE) == _ESCAPE_LEN\n\nLINE_BLANK = _LEFT_ESCAPE[:-1] + \"||||||\" + _RIGHT_ESCAPE[1:]\n\n\ndef escape(s: str) -> str:\n    return _LEFT_ESCAPE + s.strip('\\n') + _RIGHT_ESCAPE\n\n\nRep = list[str | list[Any]]\n\n\ndef _parse(s: str, start: int) -> tuple[Rep, int]:\n    frags: Rep = []\n    while start < len(s):\n        nleft = s.find(_LEFT_ESCAPE, start)\n        nright = s.find(_RIGHT_ESCAPE, start)\n        if nleft == nright == -1:\n            frags.append(s[start:])\n            start = len(s)\n        elif nleft != -1 and nleft < nright:\n            if nleft > start:\n                frags.append(s[start:nleft])\n            subfrag, start = _parse(s, nleft + _ESCAPE_LEN)\n            # If it is the special magic line blanking fragment,\n            # delete up through the last newline. Otherwise collect it.\n            if subfrag == [LINE_BLANK] and frags and isinstance(frags[-1], str):\n                frags[-1] = frags[-1].rsplit('\\n', 1)[0]\n            else:\n                frags.append(subfrag)\n        else:\n            assert nright >= 0\n            frags.append(s[start:nright])\n            start = nright + _ESCAPE_LEN\n            break\n\n    return frags, start\n\n\ndef _format_rep(rep: Rep) -> str:\n    # cpython does some really dubious things to make appending in place\n    # to a string efficient, and we depend on them here\n    out_str = \"\"\n\n    # TODO: I think there ought to be a more complicated algorithm\n    # that builds a list of lines + indentation metadata and then\n    # fixes it all up in one go?\n\n    for frag in rep:\n        if isinstance(frag, str):\n            out_str += frag\n        else:\n            fixed_frag = _format_rep(frag)\n            # If there is a newline in the final result, we need to indent\n            # it to our current position on the current line.\n            if '\\n' in fixed_frag:\n                last_nl = out_str.rfind('\\n')\n                indent = (\n                    len(out_str) if last_nl < 0\n                    else len(out_str) - last_nl - 1\n                )\n                # Indent all the lines but the first (since that goes\n                # onto our current line)\n                fixed_frag = textwrap.indent(fixed_frag, ' ' * indent)[indent:]\n\n            out_str += fixed_frag\n\n    return textwrap.dedent(out_str).removesuffix('\\n')\n\n\ndef xdedent(s: str) -> str:\n    # unlike regular dedent, xdedent trims a leading newline\n    s = s.removeprefix('\\n')\n    parsed, _ = _parse(s, 0)\n    res = _format_rep(parsed)\n    assert _LEFT_ESCAPE not in res\n    return res\n"
  },
  {
    "path": "edb/edgeql/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom . import ast  # NOQA\nfrom .tokenizer import Source, NormalizedSource  # NOQA\nfrom .codegen import generate_source  # NOQA\nfrom .parser import parse_fragment, parse_block, parse_query  # NOQA\nfrom .parser.grammar import keywords  # NOQA\nfrom .quote import quote_literal, quote_ident  # NOQA\n"
  },
  {
    "path": "edb/edgeql/ast.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n# Do not import \"from typing *\"; this module contains\n# AST classes that name-clash with classes from the typing module.\n\nimport typing\n\nfrom edb.common import enum as s_enum\nfrom edb.common import ast, span\n\nfrom . import qltypes\n\nSpan = span.Span\n\nDDLCommand_T = typing.TypeVar(\n    'DDLCommand_T',\n    bound='DDLCommand',\n    covariant=True,\n)\n\nObjectDDL_T = typing.TypeVar(\n    'ObjectDDL_T',\n    bound='ObjectDDL',\n    covariant=True,\n)\n\n\nBase_T = typing.TypeVar(\n    'Base_T',\n    bound='Base',\n)\n\n\nclass SortOrder(s_enum.StrEnum):\n    Asc = 'ASC'\n    Desc = 'DESC'\n\n\nSortAsc = SortOrder.Asc\nSortDesc = SortOrder.Desc\nSortDefault = SortAsc\n\n\nclass NonesOrder(s_enum.StrEnum):\n    First = 'first'\n    Last = 'last'\n\n\nNonesFirst = NonesOrder.First\nNonesLast = NonesOrder.Last\n\n\nclass CardinalityModifier(s_enum.StrEnum):\n    Optional = 'OPTIONAL'\n    Required = 'REQUIRED'\n\n\nclass DescribeGlobal(s_enum.StrEnum):\n    Schema = 'SCHEMA'\n    DatabaseConfig = 'DATABASE CONFIG'\n    InstanceConfig = 'INSTANCE CONFIG'\n    Roles = 'ROLES'\n\n    def to_edgeql(self) -> str:\n        return self.value\n\n\nclass Base(ast.AST):\n    __abstract_node__ = True\n    __ast_hidden__ = {'span', 'system_comment'}\n\n    span: typing.Optional[Span] = None\n\n    # System-generated comment.\n    system_comment: typing.Optional[str] = None\n\n    def dump_edgeql(self) -> None:\n        from edb.common.debug import dump_edgeql\n\n        dump_edgeql(self)\n\n\nclass GrammarEntryPoint(Base):\n    \"\"\"Mixin denoting nodes that are entry points for EdgeQL grammar\"\"\"\n    __abstract_node__ = True\n\n\nclass OptionValue(Base):\n    \"\"\"An option value resulting from a syntax.\"\"\"\n    __abstract_node__ = True\n\n    name: str\n\n\nclass OptionFlag(OptionValue):\n\n    val: bool\n\n\nclass Options(Base):\n\n    options: dict[str, OptionValue] = ast.field(factory=dict)\n\n    def get_flag(self, k: str) -> OptionFlag:\n        try:\n            flag = self[k]\n        except KeyError:\n            return OptionFlag(name=k, val=False)\n        else:\n            assert isinstance(flag, OptionFlag)\n            return flag\n\n    def __getitem__(self, k: str) -> OptionValue:\n        return self.options[k]\n\n    def __iter__(self) -> typing.Iterator[str]:\n        return iter(self.options)\n\n    def __len__(self) -> int:\n        return len(self.options)\n\n\nclass Expr(GrammarEntryPoint, Base):\n    \"\"\"Abstract parent for all query expressions.\"\"\"\n\n    __abstract_node__ = True\n\n\nclass Placeholder(Expr):\n    \"\"\"An interpolation placeholder used in expression templates.\"\"\"\n\n    name: str\n\n\nclass SortExpr(Base):\n    path: Expr\n    direction: typing.Optional[SortOrder] = None\n    nones_order: typing.Optional[NonesOrder] = None\n\n\nclass Alias(Base):\n    __abstract_node__ = True\n\n\nclass AliasedExpr(Alias):\n    alias: str\n    expr: Expr\n\n\nclass ModuleAliasDecl(Alias):\n    module: str\n    alias: typing.Optional[str]\n\n\nclass GroupingAtom(Base):\n    __abstract_node__ = True\n\n\nclass BaseObjectRef(Base):\n    __abstract_node__ = True\n\n\nclass ObjectRef(BaseObjectRef, GroupingAtom):\n    name: str\n    module: typing.Optional[str] = None\n    itemclass: typing.Optional[qltypes.SchemaObjectClass] = None\n\n\nclass PseudoObjectRef(BaseObjectRef):\n    '''anytype, anytuple or anyobject'''\n    name: str\n\n\nclass Anchor(Expr):\n    '''Identifier that resolves to some pre-compiled expression.\n       For example in shapes, the anchor __subject__ refers to object that the\n       shape is defined on.\n    '''\n    __abstract_node__ = True\n    name: str\n\n\nclass IRAnchor(Anchor):\n    has_dml: bool = False\n    # Whether, when the anchor is referenced, to move the entire\n    # referenced scope tree of the anchor to wherever it is referenced.\n    #\n    # This is important when the anchor is being used to substitute an\n    # expression in, being used only once, and we want it to behave\n    # like it was written at the point it is being substituted.\n    #\n    # (Sometimes we have anchors that get used repeatedly and which we\n    # *want* to have be bound above, basically. I'd like to get rid of\n    # all of those uses, though.)\n    # (And also the scope tree.)\n    move_scope: bool = False\n\n\nclass SpecialAnchor(Anchor):\n    pass\n\n\nclass Cursor(Expr):\n    '''A special node that halts compilation and returns all names visible in\n       the current scope. Used for LSP completions.\n    '''\n\n\nclass DetachedExpr(Expr):  # DETACHED Expr\n    expr: Expr\n    preserve_path_prefix: bool = False\n\n\nclass GlobalExpr(Expr):  # GLOBAL Name\n    name: ObjectRef\n\n\nclass Index(Base):\n    index: Expr\n\n\nclass Slice(Base):\n    start: typing.Optional[Expr]\n    stop: typing.Optional[Expr]\n\n\nclass Indirection(Expr):\n    arg: Expr\n    indirection: list[Index | Slice]\n\n\nclass BinOp(Expr):\n    left: Expr\n    op: str\n    right: Expr\n\n    rebalanced: bool = False\n    set_constructor: bool = False\n\n\nclass WindowSpec(Base):\n    orderby: list[SortExpr]\n    partition: list[Expr]\n\n\nclass FunctionCall(Expr):\n    func: tuple[str, str] | str\n    args: list[Expr] = ast.field(factory=list)\n    kwargs: dict[str, Expr] = ast.field(factory=dict)\n    window: typing.Optional[WindowSpec] = None\n\n\nclass StrInterpFragment(Base):\n    expr: Expr\n    suffix: str\n\n\nclass StrInterp(Expr):\n    prefix: str\n    interpolations: list[StrInterpFragment]\n\n\nclass BaseConstant(Expr):\n    \"\"\"Constant (a literal value).\"\"\"\n    __abstract_node__ = True\n\n\nclass Constant(BaseConstant):\n    \"\"\"Constant whose value we can store in a string.\"\"\"\n    kind: ConstantKind\n    value: str\n\n    @classmethod\n    def string(cls, value: str, span: Span | None = None) -> Constant:\n        return Constant(kind=ConstantKind.STRING, value=value, span=span)\n\n    @classmethod\n    def boolean(cls, b: bool, span: Span | None = None) -> Constant:\n        return Constant(\n            kind=ConstantKind.BOOLEAN, value=str(b).lower(), span=span\n        )\n\n    @classmethod\n    def integer(cls, i: int) -> Constant:\n        return Constant(kind=ConstantKind.INTEGER, value=str(i))\n\n    @classmethod\n    def float(cls, f: float) -> Constant:\n        return Constant(kind=ConstantKind.FLOAT, value=str(f))\n\n    @classmethod\n    def make(cls, n: object) -> Constant:\n        if isinstance(n, str):\n            return cls.string(n)\n        elif isinstance(n, bool):\n            return cls.boolean(n)\n        elif isinstance(n, int):\n            return cls.integer(n)\n        elif isinstance(n, float):\n            return cls.float(n)\n        else:\n            raise AssertionError('unsupported constant type')\n\n\nclass ConstantKind(s_enum.StrEnum):\n    STRING = 'STRING'\n    BOOLEAN = 'BOOLEAN'\n    INTEGER = 'INTEGER'\n    FLOAT = 'FLOAT'\n    BIGINT = 'BIGINT'\n    DECIMAL = 'DECIMAL'\n\n\nclass BytesConstant(BaseConstant):\n    value: bytes\n\n    @classmethod\n    def from_python(cls, s: bytes) -> BytesConstant:\n        return BytesConstant(value=s)\n\n\nclass QueryParameter(Expr):\n    name: str\n\n\nclass FunctionParameter(Expr):\n    name: str\n\n\nclass UnaryOp(Expr):\n    op: str\n    operand: Expr\n\n\nclass TypeExpr(Base):\n    __abstract_node__ = True\n\n    name: typing.Optional[str] = None  # name is used for types in named tuples\n\n\nclass TypeOf(TypeExpr):\n    expr: Expr\n\n\nclass TypeExprLiteral(TypeExpr):\n    # Literal type exprs are used in enum declarations.\n    val: Constant\n\n\nclass TypeName(TypeExpr):\n    maintype: BaseObjectRef\n    subtypes: typing.Optional[list[TypeExpr]] = None\n    dimensions: typing.Optional[list[int]] = None\n\n\nclass TypeOpName(s_enum.StrEnum):\n    OR = '|'\n    AND = '&'\n\n\nclass TypeOp(TypeExpr):\n    __rust_box__ = {'left', 'right'}\n\n    left: TypeExpr\n    op: TypeOpName\n    right: TypeExpr\n\n\nclass FuncParamDecl(Base):\n    name: str\n    type: TypeExpr\n    typemod: qltypes.TypeModifier = qltypes.TypeModifier.SingletonType\n    kind: qltypes.ParameterKind\n    default: typing.Optional[Expr] = None\n\n\nclass IsOp(Expr):\n    left: Expr\n    op: str\n    right: TypeExpr\n\n\nclass TypeIntersection(Base):\n    type: TypeExpr\n\n\nclass Ptr(Base):\n    name: str\n    direction: typing.Optional[str] = None\n    # @ptr has type 'property'\n    # .?>ptr has type 'optional'\n    type: typing.Optional[typing.Literal['optional', 'property']] = None\n\n\nclass Splat(Base):\n    \"\"\"Represents a splat operation (expansion to all props/links) in shapes\"\"\"\n\n    #: Expansion depth\n    depth: int\n    #: Source type expression, e.g in Type.**\n    type: typing.Optional[TypeExpr] = None\n    #: Type intersection on the source which would result\n    #: in polymorphic expansion, e.g. [is Type].**\n    intersection: typing.Optional[TypeIntersection] = None\n\n\nPathElement = Expr | Ptr | TypeIntersection | ObjectRef | Splat\n\n\nclass Path(Expr, GroupingAtom):\n    steps: list[PathElement]\n    partial: bool = False\n    allow_factoring: bool = False\n\n\nclass TypeCast(Expr):\n    expr: Expr\n    type: TypeExpr\n    cardinality_mod: typing.Optional[CardinalityModifier] = None\n\n\nclass Introspect(Expr):\n    type: TypeExpr\n\n\nclass IfElse(Expr):\n    condition: Expr\n    if_expr: Expr\n    else_expr: Expr\n    # Just affects pretty-printing\n    python_style: bool = False\n\n\nclass TupleElement(Base):\n    # This stores the name in another node instead of as a str just so\n    # that the name can have a separate source context.\n    name: Ptr\n    val: Expr\n\n\nclass NamedTuple(Expr):\n    elements: list[TupleElement]\n\n\nclass Tuple(Expr):\n    elements: list[Expr]\n\n\nclass Array(Expr):\n    elements: list[Expr]\n\n\nclass Set(Expr):\n    elements: list[Expr]\n\n\n# Statements\n#\n\nclass Command(Base):\n    \"\"\"\n    A top-level node that is evaluated by our server and\n    cannot be a part of a sub expression.\n    \"\"\"\n\n    __abstract_node__ = True\n    aliases: typing.Optional[list[Alias]] = None\n\n\nclass Commands(GrammarEntryPoint, Base):\n    commands: list[Command]\n\n\nclass SessionSetAliasDecl(Command):\n    decl: ModuleAliasDecl\n\n\nclass SessionResetAliasDecl(Command):\n    alias: str\n\n\nclass SessionResetModule(Command):\n    pass\n\n\nclass SessionResetAllAliases(Command):\n    pass\n\n\nSessionCommand = (\n    SessionSetAliasDecl\n    | SessionResetAliasDecl\n    | SessionResetModule\n    | SessionResetAllAliases\n)\n\n\nclass ShapeOp(s_enum.StrEnum):\n    APPEND = 'APPEND'\n    SUBTRACT = 'SUBTRACT'\n    ASSIGN = 'ASSIGN'\n    MATERIALIZE = 'MATERIALIZE'  # This is an internal implementation artifact\n\n\n# Need indirection over ShapeOp to preserve the source context.\nclass ShapeOperation(Base):\n    op: ShapeOp\n\n\nclass ShapeOrigin(s_enum.StrEnum):\n    EXPLICIT = 'EXPLICIT'\n    DEFAULT = 'DEFAULT'\n    SPLAT_EXPANSION = 'SPLAT_EXPANSION'\n    MATERIALIZATION = 'MATERIALIZATION'\n\n\nclass ShapeElement(Expr):\n    expr: Path\n    elements: typing.Optional[list[ShapeElement]] = None\n    compexpr: typing.Optional[Expr] = None\n    cardinality: typing.Optional[qltypes.SchemaCardinality] = None\n    required: typing.Optional[bool] = None\n    operation: ShapeOperation = ShapeOperation(op=ShapeOp.ASSIGN)\n    origin: ShapeOrigin = ShapeOrigin.EXPLICIT\n\n    where: typing.Optional[Expr] = None\n\n    orderby: typing.Optional[list[SortExpr]] = None\n\n    offset: typing.Optional[Expr] = None\n    limit: typing.Optional[Expr] = None\n\n\nclass Shape(Expr):\n    expr: typing.Optional[Expr]\n    elements: list[ShapeElement]\n    allow_factoring: bool = False\n\n\nclass Query(Expr, GrammarEntryPoint, Command):\n    __abstract_node__ = True\n\n    aliases: typing.Optional[list[Alias]] = None\n\n\nclass SelectQuery(Query):\n    result_alias: typing.Optional[str] = None\n    result: Expr\n\n    where: typing.Optional[Expr] = None\n\n    orderby: typing.Optional[list[SortExpr]] = None\n\n    offset: typing.Optional[Expr] = None\n    limit: typing.Optional[Expr] = None\n\n    # This is a hack, indicating that rptr should be forwarded through\n    # this select. Used when we generate implicit selects that need to\n    # not interfere with linkprops.\n    rptr_passthrough: bool = False\n\n    implicit: bool = False\n\n\nclass GroupingIdentList(GroupingAtom, Base):\n    elements: tuple[GroupingAtom, ...]\n\n\nclass GroupingElement(Base):\n    __abstract_node__ = True\n\n\nclass GroupingSimple(GroupingElement):\n    element: GroupingAtom\n\n\nclass GroupingSets(GroupingElement):\n    sets: list[GroupingElement]\n\n\nclass GroupingOperation(GroupingElement):\n    oper: str\n    elements: list[GroupingAtom]\n\n\nclass GroupQuery(Query):\n    subject_alias: typing.Optional[str] = None\n    using: typing.Optional[list[AliasedExpr]]\n    by: list[GroupingElement]\n\n    subject: Expr\n\n\nclass InternalGroupQuery(Query):\n    subject_alias: typing.Optional[str] = None\n    using: typing.Optional[list[AliasedExpr]]\n    by: list[GroupingElement]\n\n    subject: Expr\n\n    group_alias: str\n    grouping_alias: typing.Optional[str]\n    from_desugaring: bool = False\n\n    result_alias: typing.Optional[str] = None\n    result: Expr\n\n    where: typing.Optional[Expr] = None\n\n    orderby: typing.Optional[list[SortExpr]] = None\n\n\nclass InsertQuery(Query):\n    subject: ObjectRef\n    shape: list[ShapeElement]\n    unless_conflict: typing.Optional[\n        tuple[typing.Optional[Expr], typing.Optional[Expr]]\n    ] = None\n\n\nclass UpdateQuery(Query):\n    shape: list[ShapeElement]\n\n    subject: Expr\n\n    where: typing.Optional[Expr] = None\n\n\nclass DeleteQuery(Query):\n    subject: Expr\n\n    where: typing.Optional[Expr] = None\n\n    orderby: typing.Optional[list[SortExpr]] = None\n\n    offset: typing.Optional[Expr] = None\n    limit: typing.Optional[Expr] = None\n\n\nclass ForQuery(Query):\n    from_desugaring: bool = False\n    has_union: bool = True  # whether UNION was used in the syntax\n\n    optional: bool = False\n    iterator: Expr\n    iterator_alias: str\n\n    result_alias: typing.Optional[str] = None\n    result: Expr\n\n\n# Transactions\n#\n\n\nclass Transaction(Base):\n    '''Abstract parent for all transaction operations.'''\n\n    __abstract_node__ = True\n\n\nclass StartTransaction(Transaction):\n    isolation: typing.Optional[qltypes.TransactionIsolationLevel] = None\n    access: typing.Optional[qltypes.TransactionAccessMode] = None\n    deferrable: typing.Optional[qltypes.TransactionDeferMode] = None\n\n\nclass CommitTransaction(Transaction):\n    pass\n\n\nclass RollbackTransaction(Transaction):\n    pass\n\n\nclass DeclareSavepoint(Transaction):\n\n    name: str\n\n\nclass RollbackToSavepoint(Transaction):\n\n    name: str\n\n\nclass ReleaseSavepoint(Transaction):\n\n    name: str\n\n\n# DDL\n#\n\n\nclass DDL(Base):\n    '''A mixin denoting DDL nodes.'''\n    __abstract_node__ = True\n\n\nclass Position(DDL):\n    ref: typing.Optional[ObjectRef] = None\n    position: str\n\n\nclass DDLOperation(DDL):\n    '''A change to schema'''\n\n    __abstract_node__ = True\n    commands: list[DDLOperation] = ast.field(factory=list)\n\n\nclass DDLCommand(DDLOperation, Command):\n    __abstract_node__ = True\n\n\nclass DDLQuery(DDLCommand):\n    '''A query wrapped in DDL. Appears in migrations.'''\n    query: Query\n\n\nclass NonTransactionalDDLCommand(DDLCommand):\n    __abstract_node__ = True\n\n\nclass AlterAddInherit(DDLOperation):\n    position: typing.Optional[Position] = None\n    bases: list[TypeName]\n\n\nclass AlterDropInherit(DDLOperation):\n    bases: list[TypeName]\n\n\nclass OnTargetDelete(DDLOperation):\n    cascade: typing.Optional[qltypes.LinkTargetDeleteAction]\n\n\nclass OnSourceDelete(DDLOperation):\n    cascade: typing.Optional[qltypes.LinkSourceDeleteAction]\n\n\nclass SetField(DDLOperation):\n    name: str\n    value: Expr | TypeExpr | None\n    #: Indicates that this AST originated from a special DDL syntax\n    #: rather than from a generic `SET field := value` statement, and\n    #: so must not be subject to the \"allow_ddl_set\" constraint.\n    #: This attribute is also considered by the codegen to emit appropriate\n    #: syntax.\n    special_syntax: bool = False\n\n\nclass SetPointerType(SetField):\n    name: str = 'target'\n    special_syntax: bool = True\n    value: typing.Optional[TypeExpr]\n    cast_expr: typing.Optional[Expr] = None\n\n\nclass SetPointerCardinality(SetField):\n    name: str = 'cardinality'\n    special_syntax: bool = True\n    conv_expr: typing.Optional[Expr] = None\n\n\nclass SetPointerOptionality(SetField):\n    name: str = 'required'\n    special_syntax: bool = True\n    fill_expr: typing.Optional[Expr] = None\n\n\nclass ObjectDDL(DDLCommand):\n    __abstract_node__ = True\n\n    name: ObjectRef\n\n\nclass CreateObject(ObjectDDL):\n    __abstract_node__ = True\n\n    abstract: bool = False\n    sdl_alter_if_exists: bool = False\n    create_if_not_exists: bool = False\n\n\nclass AlterObject(ObjectDDL):\n    __abstract_node__ = True\n\n\nclass DropObject(ObjectDDL):\n    __abstract_node__ = True\n\n\nclass CreateExtendingObject(CreateObject):\n    __abstract_node__ = True\n\n    # final is not currently implemented, and the syntax is not\n    # supported except in old dumps. We track it only to allow us to\n    # error on it.\n    final: bool = False\n    bases: list[TypeName]\n\n\nclass Rename(ObjectDDL):\n    new_name: ObjectRef\n\n    @property\n    def name(self) -> ObjectRef:  # type: ignore[override]  # mypy bug?\n        return self.new_name\n\n\nclass NestedQLBlock(DDL):\n\n    commands: list[DDLOperation]\n    text: typing.Optional[str] = None\n\n\nclass MigrationCommand(DDLCommand):\n\n    __abstract_node__ = True\n\n\nclass CreateMigration(CreateObject, MigrationCommand, GrammarEntryPoint):\n\n    body: NestedQLBlock\n    parent: typing.Optional[ObjectRef] = None\n    metadata_only: bool = False\n\n    # Sometimes the target SDL of a migration can be known in advance.\n    # eg. when doing `start migration to`\n    target_sdl: typing.Optional[str] = None\n\n\nclass CommittedSchema(DDL):\n    pass\n\n\nclass StartMigration(MigrationCommand):\n\n    target: Schema | CommittedSchema\n\n\nclass AbortMigration(MigrationCommand):\n    pass\n\n\nclass PopulateMigration(MigrationCommand):\n    pass\n\n\nclass AlterCurrentMigrationRejectProposed(MigrationCommand):\n    pass\n\n\nclass DescribeCurrentMigration(MigrationCommand):\n\n    language: qltypes.DescribeLanguage\n\n\nclass CommitMigration(MigrationCommand):\n    pass\n\n\nclass AlterMigration(AlterObject, MigrationCommand):\n    pass\n\n\nclass DropMigration(DropObject, MigrationCommand):\n    pass\n\n\nclass ResetSchema(MigrationCommand):\n\n    target: ObjectRef\n\n\nclass StartMigrationRewrite(MigrationCommand):\n    pass\n\n\nclass AbortMigrationRewrite(MigrationCommand):\n    pass\n\n\nclass CommitMigrationRewrite(MigrationCommand):\n    pass\n\n\nclass UnqualifiedObjectCommand(ObjectDDL):\n\n    __abstract_node__ = True\n\n\nclass GlobalObjectCommand(UnqualifiedObjectCommand):\n\n    __abstract_node__ = True\n\n\nclass BranchType(s_enum.StrEnum):\n    EMPTY = 'EMPTY'\n    SCHEMA = 'SCHEMA'\n    DATA = 'DATA'\n    TEMPLATE = 'TEMPLATE'\n\n\nclass DatabaseCommand(GlobalObjectCommand, NonTransactionalDDLCommand):\n\n    __abstract_node__ = True\n    flavor: qltypes.SchemaObjectClass = qltypes.SchemaObjectClass.BRANCH\n\n\nclass CreateDatabase(CreateObject, DatabaseCommand):\n\n    template: typing.Optional[ObjectRef] = None\n    branch_type: BranchType\n\n\nclass AlterDatabase(AlterObject, DatabaseCommand):\n    force: bool = False\n\n\nclass DropDatabase(DropObject, DatabaseCommand):\n    force: bool = False\n\n\nclass ExtensionPackageCommand(GlobalObjectCommand):\n\n    __abstract_node__ = True\n    version: Constant\n\n\nclass CreateExtensionPackage(CreateObject, ExtensionPackageCommand):\n\n    body: NestedQLBlock\n\n\nclass DropExtensionPackage(DropObject, ExtensionPackageCommand):\n    pass\n\n\nclass ExtensionPackageMigrationCommand(GlobalObjectCommand):\n    __abstract_node__ = True\n\n\nclass CreateExtensionPackageMigration(\n    CreateObject, ExtensionPackageMigrationCommand\n):\n    from_version: Constant\n    to_version: Constant\n    body: NestedQLBlock\n\n\nclass DropExtensionPackageMigration(\n    DropObject, ExtensionPackageMigrationCommand\n):\n    from_version: Constant\n    to_version: Constant\n\n\nclass ExtensionCommand(UnqualifiedObjectCommand):\n    __abstract_node__ = True\n\n\nclass CreateExtension(CreateObject, ExtensionCommand):\n    version: typing.Optional[Constant] = None\n\n\nclass AlterExtension(DropObject, ExtensionCommand):\n    version: typing.Optional[Constant] = None\n    to_version: Constant\n\n\nclass DropExtension(DropObject, ExtensionCommand):\n    version: typing.Optional[Constant] = None\n\n\nclass FutureCommand(UnqualifiedObjectCommand):\n\n    __abstract_node__ = True\n\n\nclass CreateFuture(CreateObject, FutureCommand):\n    pass\n\n\nclass DropFuture(DropObject, FutureCommand):\n    pass\n\n\nclass ModuleCommand(UnqualifiedObjectCommand):\n\n    __abstract_node__ = True\n\n\nclass CreateModule(ModuleCommand, CreateObject):\n    pass\n\n\nclass AlterModule(ModuleCommand, AlterObject):\n    pass\n\n\nclass DropModule(ModuleCommand, DropObject):\n    pass\n\n\nclass RoleCommand(GlobalObjectCommand):\n    __abstract_node__ = True\n\n\nclass CreateRole(CreateObject, RoleCommand):\n    superuser: bool = False\n    bases: list[TypeName]\n\n\nclass AlterRole(AlterObject, RoleCommand):\n    pass\n\n\nclass DropRole(DropObject, RoleCommand):\n    pass\n\n\nclass AnnotationCommand(ObjectDDL):\n\n    __abstract_node__ = True\n\n\nclass CreateAnnotation(CreateExtendingObject, AnnotationCommand):\n    type: typing.Optional[TypeExpr]\n    inheritable: bool\n\n\nclass AlterAnnotation(AlterObject, AnnotationCommand):\n    pass\n\n\nclass DropAnnotation(DropObject, AnnotationCommand):\n    pass\n\n\nclass PseudoTypeCommand(ObjectDDL):\n\n    __abstract_node__ = True\n\n\nclass CreatePseudoType(CreateObject, PseudoTypeCommand):\n    pass\n\n\nclass ScalarTypeCommand(ObjectDDL):\n\n    __abstract_node__ = True\n\n\nclass CreateScalarType(CreateExtendingObject, ScalarTypeCommand):\n    pass\n\n\nclass AlterScalarType(AlterObject, ScalarTypeCommand):\n    pass\n\n\nclass DropScalarType(DropObject, ScalarTypeCommand):\n    pass\n\n\nclass PropertyCommand(ObjectDDL):\n\n    __abstract_node__ = True\n\n\nclass CreateProperty(CreateExtendingObject, PropertyCommand):\n    pass\n\n\nclass AlterProperty(AlterObject, PropertyCommand):\n    pass\n\n\nclass DropProperty(DropObject, PropertyCommand):\n    pass\n\n\nclass CreateConcretePointer(CreateObject):\n    __abstract_node__ = True\n\n    is_required: typing.Optional[bool] = None\n    declared_overloaded: bool = False\n    target: typing.Optional[Expr | TypeExpr]\n    cardinality: qltypes.SchemaCardinality\n    bases: list[TypeName]\n\n\nclass CreateConcreteUnknownPointer(CreateConcretePointer):\n    pass\n\n\nclass AlterConcreteUnknownPointer(AlterObject, PropertyCommand):\n    pass\n\n\nclass CreateConcreteProperty(CreateConcretePointer, PropertyCommand):\n    pass\n\n\nclass AlterConcreteProperty(AlterObject, PropertyCommand):\n    pass\n\n\nclass DropConcreteProperty(DropObject, PropertyCommand):\n    pass\n\n\nclass ObjectTypeCommand(ObjectDDL):\n\n    __abstract_node__ = True\n\n\nclass CreateObjectType(CreateExtendingObject, ObjectTypeCommand):\n    pass\n\n\nclass AlterObjectType(AlterObject, ObjectTypeCommand):\n    pass\n\n\nclass DropObjectType(DropObject, ObjectTypeCommand):\n    pass\n\n\nclass AliasCommand(ObjectDDL):\n\n    __abstract_node__ = True\n\n\nclass CreateAlias(CreateObject, AliasCommand):\n    pass\n\n\nclass AlterAlias(AlterObject, AliasCommand):\n    pass\n\n\nclass DropAlias(DropObject, AliasCommand):\n    pass\n\n\nclass GlobalCommand(ObjectDDL):\n\n    __abstract_node__ = True\n\n\nclass CreateGlobal(CreateObject, GlobalCommand):\n    is_required: typing.Optional[bool] = None\n    target: typing.Optional[Expr | TypeExpr]\n    cardinality: typing.Optional[qltypes.SchemaCardinality]\n\n\nclass AlterGlobal(AlterObject, GlobalCommand):\n    pass\n\n\nclass DropGlobal(DropObject, GlobalCommand):\n    pass\n\n\nclass SetGlobalType(SetField):\n    name: str = 'target'\n    special_syntax: bool = True\n    value: typing.Optional[TypeExpr]\n    cast_expr: typing.Optional[Expr] = None\n    reset_value: bool = False\n\n\nclass PermissionCommand(ObjectDDL):\n\n    __abstract_node__ = True\n\n\nclass CreatePermission(CreateObject, PermissionCommand):\n    pass\n\n\nclass AlterPermission(AlterObject, PermissionCommand):\n    pass\n\n\nclass DropPermission(DropObject, PermissionCommand):\n    pass\n\n\nclass LinkCommand(ObjectDDL):\n\n    __abstract_node__ = True\n\n\nclass CreateLink(CreateExtendingObject, LinkCommand):\n    pass\n\n\nclass AlterLink(AlterObject, LinkCommand):\n    pass\n\n\nclass DropLink(DropObject, LinkCommand):\n    pass\n\n\nclass CreateConcreteLink(\n    CreateExtendingObject,\n    CreateConcretePointer,\n    LinkCommand,\n):\n    pass\n\n\nclass AlterConcreteLink(AlterObject, LinkCommand):\n    pass\n\n\nclass DropConcreteLink(DropObject, LinkCommand):\n    pass\n\n\nclass ConstraintCommand(ObjectDDL):\n\n    __abstract_node__ = True\n\n\nclass CreateConstraint(\n    CreateExtendingObject,\n    ConstraintCommand,\n):\n    subjectexpr: typing.Optional[Expr]\n    abstract: bool = True\n    params: list[FuncParamDecl] = ast.field(factory=list)\n\n\nclass AlterConstraint(AlterObject, ConstraintCommand):\n    pass\n\n\nclass DropConstraint(DropObject, ConstraintCommand):\n    pass\n\n\nclass ConcreteConstraintOp(ConstraintCommand):\n\n    __abstract_node__ = True\n    args: list[Expr]\n    subjectexpr: typing.Optional[Expr]\n    except_expr: typing.Optional[Expr] = None\n\n\nclass CreateConcreteConstraint(ConcreteConstraintOp, CreateObject):\n    delegated: bool = False\n\n\nclass AlterConcreteConstraint(ConcreteConstraintOp, AlterObject):\n    pass\n\n\nclass DropConcreteConstraint(ConcreteConstraintOp, DropObject):\n    pass\n\n\nclass IndexType(DDL):\n    name: ObjectRef\n    args: list[Expr] = ast.field(factory=list)\n    kwargs: dict[str, Expr] = ast.field(factory=dict)\n\n\nclass IndexCommand(ObjectDDL):\n\n    __abstract_node__ = True\n\n\nclass IndexCode(DDL):\n    language: Language\n    code: str\n\n\nclass CreateIndex(\n    CreateExtendingObject,\n    IndexCommand,\n):\n    kwargs: dict[str, Expr] = ast.field(factory=dict)\n    index_types: list[IndexType]\n    code: typing.Optional[IndexCode] = None\n    params: list[FuncParamDecl] = ast.field(factory=list)\n\n\nclass AlterIndex(AlterObject, IndexCommand):\n    pass\n\n\nclass DropIndex(DropObject, IndexCommand):\n    pass\n\n\nclass IndexMatchCommand(ObjectDDL):\n\n    __abstract_node__ = True\n    valid_type: TypeName\n\n\nclass CreateIndexMatch(CreateObject, IndexMatchCommand):\n    pass\n    # XXX: we might want to have a code field to potentially customize the\n    # default index code (to account for operator classes and similar custom\n    # type-based syntax)\n\n\nclass DropIndexMatch(DropObject, IndexMatchCommand):\n    pass\n\n\nclass ConcreteIndexCommand(IndexCommand):\n\n    __abstract_node__ = True\n    kwargs: dict[str, Expr] = ast.field(factory=dict)\n    expr: Expr\n    except_expr: typing.Optional[Expr] = None\n    deferred: bool = False\n\n\nclass CreateConcreteIndex(ConcreteIndexCommand, CreateObject):\n    pass\n\n\nclass AlterConcreteIndex(ConcreteIndexCommand, AlterObject):\n    pass\n\n\nclass DropConcreteIndex(ConcreteIndexCommand, DropObject):\n    pass\n\n\nclass CreateAnnotationValue(AnnotationCommand, CreateObject):\n    value: Expr\n\n\nclass AlterAnnotationValue(AnnotationCommand, AlterObject):\n    value: typing.Optional[Expr]\n\n\nclass DropAnnotationValue(AnnotationCommand, DropObject):\n    pass\n\n\nclass AccessPolicyCommand(ObjectDDL):\n\n    __abstract_node__ = True\n\n\nclass CreateAccessPolicy(CreateObject, AccessPolicyCommand):\n    condition: typing.Optional[Expr]\n    action: qltypes.AccessPolicyAction\n    access_kinds: list[qltypes.AccessKind]\n    expr: typing.Optional[Expr]\n\n\nclass SetAccessPerms(DDLOperation):\n    access_kinds: list[qltypes.AccessKind]\n    action: qltypes.AccessPolicyAction\n\n\nclass AlterAccessPolicy(AlterObject, AccessPolicyCommand):\n    pass\n\n\nclass DropAccessPolicy(DropObject, AccessPolicyCommand):\n    pass\n\n\nclass TriggerCommand(ObjectDDL):\n\n    __abstract_node__ = True\n\n\nclass CreateTrigger(CreateObject, TriggerCommand):\n    timing: qltypes.TriggerTiming\n    kinds: list[qltypes.TriggerKind]\n    scope: qltypes.TriggerScope\n    expr: Expr\n    condition: typing.Optional[Expr]\n\n\nclass AlterTrigger(AlterObject, TriggerCommand):\n    pass\n\n\nclass DropTrigger(DropObject, TriggerCommand):\n    pass\n\n\nclass RewriteCommand(ObjectDDL):\n    \"\"\"\n    Mutation rewrite command.\n\n    Note that kinds are basically identifiers of the command, so they need to\n    be present for all commands.\n\n    List of kinds is converted into multiple commands when creating delta\n    commands in `_cmd_tree_from_ast`.\n    \"\"\"\n\n    __abstract_node__ = True\n\n    kinds: list[qltypes.RewriteKind]\n\n\nclass CreateRewrite(CreateObject, RewriteCommand):\n    expr: Expr\n\n\nclass AlterRewrite(AlterObject, RewriteCommand):\n    pass\n\n\nclass DropRewrite(DropObject, RewriteCommand):\n    pass\n\n\nclass Language(s_enum.StrEnum):\n    SQL = 'SQL'\n    EdgeQL = 'EdgeQL'\n\n\nclass FunctionCode(DDL):\n    language: Language = Language.EdgeQL\n    code: typing.Optional[str] = None\n    nativecode: typing.Optional[Expr] = None\n    from_function: typing.Optional[str] = None\n    from_expr: bool = False\n\n\nclass FunctionCommand(DDLCommand):\n\n    __abstract_node__ = True\n    params: list[FuncParamDecl] = ast.field(factory=list)\n\n\nclass CreateFunction(CreateObject, FunctionCommand):\n\n    returning: TypeExpr\n    code: FunctionCode\n    nativecode: typing.Optional[Expr]\n    returning_typemod: qltypes.TypeModifier = qltypes.TypeModifier.SingletonType\n\n\nclass AlterFunction(AlterObject, FunctionCommand):\n\n    code: FunctionCode = FunctionCode  # type: ignore\n    nativecode: typing.Optional[Expr]\n\n\nclass DropFunction(DropObject, FunctionCommand):\n    pass\n\n\nclass OperatorCode(DDL):\n    language: Language\n    from_operator: typing.Optional[tuple[str, ...]]\n    from_function: typing.Optional[tuple[str, ...]]\n    from_expr: bool\n    code: typing.Optional[str]\n\n\nclass OperatorCommand(DDLCommand):\n\n    __abstract_node__ = True\n    kind: qltypes.OperatorKind\n    params: list[FuncParamDecl] = ast.field(factory=list)\n\n\nclass CreateOperator(CreateObject, OperatorCommand):\n    returning: TypeExpr\n    returning_typemod: qltypes.TypeModifier = qltypes.TypeModifier.SingletonType\n    code: OperatorCode\n\n\nclass AlterOperator(AlterObject, OperatorCommand):\n    pass\n\n\nclass DropOperator(DropObject, OperatorCommand):\n    pass\n\n\nclass CastCode(DDL):\n    language: Language\n    from_function: str\n    from_expr: bool\n    from_cast: bool\n    code: str\n\n\nclass CastCommand(ObjectDDL):\n\n    __abstract_node__ = True\n    from_type: TypeName\n    to_type: TypeName\n\n\nclass CreateCast(CreateObject, CastCommand):\n    code: CastCode\n    allow_implicit: bool\n    allow_assignment: bool\n\n\nclass AlterCast(AlterObject, CastCommand):\n    pass\n\n\nclass DropCast(DropObject, CastCommand):\n    pass\n\n\nclass OptionalExpr(Expr):\n    \"\"\"Internally used in ELSE clause of IF statement.\"\"\"\n\n    expr: Expr\n\n\n#\n# Config\n#\n\n\nclass ConfigOp(Base):\n    __abstract_node__ = True\n    name: ObjectRef\n    scope: qltypes.ConfigScope\n\n\nclass ConfigSet(ConfigOp):\n\n    expr: Expr\n\n\nclass ConfigInsert(ConfigOp):\n\n    shape: list[ShapeElement]\n\n\nclass ConfigReset(ConfigOp):\n    where: typing.Optional[Expr] = None\n\n\n#\n# Describe\n#\n\n\nclass DescribeStmt(Command):\n\n    language: qltypes.DescribeLanguage\n    object: ObjectRef | DescribeGlobal\n    options: Options\n\n\n#\n# Explain\n#\n\n\nclass ExplainStmt(Command):\n\n    args: typing.Optional[NamedTuple]\n    query: Query\n\n\n#\n# Administer\n#\n\n\nclass AdministerStmt(Command):\n\n    expr: FunctionCall\n\n\n#\n# SDL\n#\n\n\nclass SDL(Base):\n    '''A mixin denoting SDL nodes.'''\n\n    __abstract_node__ = True\n\n\nclass ModuleDeclaration(SDL):\n    # The 'name' is treated same as in CreateModule, for consistency,\n    # since this declaration also implies creating a module.\n    name: ObjectRef\n    declarations: list[ObjectDDL | ModuleDeclaration]\n\n\nclass Schema(SDL, GrammarEntryPoint, Base):\n    declarations: list[ObjectDDL | ModuleDeclaration]\n\n\n#\n# These utility functions work on EdgeQL AST nodes\n#\n\n\ndef get_ddl_field_command(\n    ddlcmd: DDLOperation,\n    name: str,\n) -> typing.Optional[SetField]:\n    for cmd in ddlcmd.commands:\n        if isinstance(cmd, SetField) and cmd.name == name:\n            return cmd\n\n    return None\n\n\ndef get_ddl_field_value(\n    ddlcmd: DDLOperation,\n    name: str,\n) -> Expr | TypeExpr | None:\n    cmd = get_ddl_field_command(ddlcmd, name)\n    return cmd.value if cmd is not None else None\n\n\ndef get_ddl_subcommand(\n    ddlcmd: DDLOperation,\n    cmdtype: type[DDLOperation],\n) -> typing.Optional[DDLOperation]:\n    for cmd in ddlcmd.commands:\n        if isinstance(cmd, cmdtype):\n            return cmd\n    else:\n        return None\n\n\ndef has_ddl_subcommand(\n    ddlcmd: DDLOperation,\n    cmdtype: type[DDLOperation],\n) -> bool:\n    return bool(get_ddl_subcommand(ddlcmd, cmdtype))\n\n\nReturningQuery = SelectQuery | ForQuery | InternalGroupQuery\n\n\nFilteringQuery = (\n    SelectQuery | DeleteQuery | ShapeElement | UpdateQuery | ConfigReset\n)\n\n\nSubjectQuery = DeleteQuery | UpdateQuery | GroupQuery\n\n\nOffsetLimitQuery = SelectQuery | DeleteQuery | ShapeElement\n\n\nBasedOn = (\n    AlterAddInherit\n    | AlterDropInherit\n    | CreateExtendingObject\n    | CreateRole\n    | CreateConcretePointer\n)\n\nCallableObjectCommand = (\n    CreateConstraint | CreateIndex | FunctionCommand | OperatorCommand\n)\n\n# A node that can have a WITH block\nStatement = Query | Command\n"
  },
  {
    "path": "edb/edgeql/codegen.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n    TypeVar,\n    AbstractSet,\n    Sequence,\n    Match,\n    TYPE_CHECKING,\n)\n\nimport itertools\nimport re\n\nfrom edb import errors\nfrom edb.common.ast import codegen, base\nfrom edb.common import typeutils\n\nfrom . import ast as qlast\nfrom . import quote as edgeql_quote\nfrom . import qltypes\n\n\n_BYTES_ESCAPE_RE = re.compile(b'[\\\\\\'\\x00-\\x1f\\x7e-\\xff]')\n_NON_PRINTABLE_RE = re.compile(\n    r'[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\u007F\\u0080-\\u009F\\n]')\n_ESCAPES = {\n    b'\\\\': b'\\\\\\\\',\n    b'\\'': b'\\\\\\'',\n    b'\\t': b'\\\\t',\n    b'\\n': b'\\\\n',\n}\n\n\nif TYPE_CHECKING:\n    import enum\n    Enum_T = TypeVar('Enum_T', bound=enum.Enum)\n\n\ndef _bytes_escape(match: Match[bytes]) -> bytes:\n    char = match.group(0)\n    try:\n        return _ESCAPES[char]\n    except KeyError:\n        return b'\\\\x%02x' % char[0]\n\n\ndef param_to_str(ident: str) -> str:\n    return '$' + edgeql_quote.quote_ident(\n        ident, allow_reserved=True, allow_num=True)\n\n\ndef ident_to_str(ident: str, allow_num: bool=False) -> str:\n    return '::'.join([\n        edgeql_quote.quote_ident(part, allow_num=allow_num)\n        for part in ident.split('::')\n    ])\n\n\nclass EdgeQLSourceGeneratorError(errors.InternalServerError):\n    pass\n\n\nclass EdgeSchemaSourceGeneratorError(errors.InternalServerError):\n    pass\n\n\nclass EdgeQLSourceGenerator(codegen.SourceGenerator):\n\n    def __init__(\n        self,\n        *args: Any,\n        sdlmode: bool = False,\n        descmode: bool = False,\n        # Uppercase keywords for backwards compatibility with older migrations.\n        uppercase: bool = False,\n        unsorted: bool = False,\n        limit_ref_classes:\n            Optional[AbstractSet[qltypes.SchemaObjectClass]] = None,\n        **kwargs: Any\n    ) -> None:\n        super().__init__(*args, **kwargs)\n        self.sdlmode = sdlmode\n        self.descmode = descmode\n        self.uppercase = uppercase\n        self.unsorted = unsorted\n        self.limit_ref_classes = limit_ref_classes\n\n    def visit(\n        self, node: qlast.Base | list[qlast.Base], **kwargs: Any\n    ) -> None:\n        if isinstance(node, list):\n            self.visit_list(node, terminator=';')\n        else:\n            method = 'visit_' + node.__class__.__name__\n            visitor = getattr(self, method, self.generic_visit)\n            visitor(node, **kwargs)\n\n    def _kw_case(self, *kws: str) -> str:\n        kwstring = ' '.join(kws)\n        if self.uppercase:\n            kwstring = kwstring.upper()\n        else:\n            kwstring = kwstring.lower()\n        return kwstring\n\n    def _write_keywords(self, *kws: str) -> None:\n        self.write(self._kw_case(*kws))\n\n    def _needs_parentheses(self, node: Any) -> bool:\n        # The \"parent\" attribute is set by calling `_fix_parent_links`\n        # before traversing the AST.  Since it's not an attribute that\n        # can be inferred by static typing we ignore typing for this\n        # function.\n        parent: Optional[qlast.Base] = node._parent\n        return (\n            parent is not None\n            and not isinstance(parent, (qlast.Commands, qlast.DDL))\n            # Non-union FOR bodies can't have parens\n            and not (\n                isinstance(parent, qlast.ForQuery)\n                and not parent.has_union\n                and parent.result is node\n            )\n        )\n\n    def generic_visit(\n        self, node: qlast.Base, *args: Any, **kwargs: Any\n    ) -> None:\n        if isinstance(node, qlast.SDL):\n            raise EdgeQLSourceGeneratorError(\n                f'No method to generate code for {node.__class__.__name__}')\n        else:\n            raise EdgeQLSourceGeneratorError(\n                f'No method to generate code for {node.__class__.__name__}'\n            )\n\n    def _block_ws(self, change: int, newlines: bool = True) -> None:\n        \"\"\"Block whitespace\"\"\"\n        if newlines:\n            self.indentation += change\n            self.new_lines = 1\n        else:\n            self.write(' ')\n\n    def _visit_aliases(self, node: qlast.Statement) -> None:\n        if node.aliases:\n            self._write_keywords('WITH')\n            self._block_ws(1)\n            if node.aliases:\n                self.visit_list(node.aliases)\n            self._block_ws(-1)\n\n    def _visit_filter(\n        self, node: qlast.FilteringQuery, newlines: bool = True\n    ) -> None:\n        if node.where:\n            self._write_keywords('FILTER')\n            self._block_ws(1, newlines)\n            self.visit(node.where)\n            self._block_ws(-1, newlines)\n\n    def _visit_order(\n        self,\n        node: qlast.SelectQuery | qlast.DeleteQuery,\n        newlines: bool = True,\n    ) -> None:\n        if node.orderby:\n            self._write_keywords('ORDER BY')\n            self._block_ws(1, newlines)\n            self.visit_list(\n                node.orderby,\n                separator=self._kw_case(' THEN'), newlines=newlines\n            )\n            self._block_ws(-1, newlines)\n\n    def _visit_offset_limit(\n        self, node: qlast.OffsetLimitQuery, newlines: bool = True\n    ) -> None:\n        if node.offset is not None:\n            self._write_keywords('OFFSET')\n            self._block_ws(1, newlines)\n            self.visit(node.offset)\n            self._block_ws(-1, newlines)\n        if node.limit is not None:\n            self._write_keywords('LIMIT')\n            self._block_ws(1, newlines)\n            self.visit(node.limit)\n            self._block_ws(-1, newlines)\n\n    def visit_Commands(self, node: qlast.Commands) -> None:\n        self.visit_list(node.commands, separator=';', terminator=';')\n\n    def visit_AliasedExpr(self, node: qlast.AliasedExpr) -> None:\n        self.write(ident_to_str(node.alias))\n        self.write(' := ')\n        self._block_ws(1)\n\n        self.visit(node.expr)\n\n        self._block_ws(-1)\n\n    def visit_InsertQuery(self, node: qlast.InsertQuery) -> None:\n        # need to parenthesise when INSERT appears as an expression\n        parenthesise = self._needs_parentheses(node)\n\n        if parenthesise:\n            self.write('(')\n        self._visit_aliases(node)\n        self._write_keywords('INSERT')\n        self._block_ws(1)\n        self.visit(node.subject)\n        self._block_ws(-1)\n\n        if node.shape:\n            self.indentation += 1\n            self._visit_shape(node.shape)\n            self.indentation -= 1\n\n        if node.unless_conflict:\n            on_expr, else_expr = node.unless_conflict\n            self._write_keywords('UNLESS CONFLICT')\n\n            if on_expr:\n                self._write_keywords(' ON ')\n                self.visit(on_expr)\n\n                if else_expr:\n                    self._write_keywords(' ELSE ')\n                    self.visit(else_expr)\n\n        if parenthesise:\n            self.write(')')\n\n    def visit_UpdateQuery(self, node: qlast.UpdateQuery) -> None:\n        # need to parenthesise when UPDATE appears as an expression\n        parenthesise = self._needs_parentheses(node)\n\n        if parenthesise:\n            self.write('(')\n        self._visit_aliases(node)\n        self._write_keywords('UPDATE')\n        self._block_ws(1)\n        self.visit(node.subject)\n        self._block_ws(-1)\n\n        self._visit_filter(node)\n\n        self.new_lines = 1\n        self._write_keywords('SET ')\n        self._visit_shape(node.shape, always_emit_braces=True)\n\n        if parenthesise:\n            self.write(')')\n\n    def visit_DeleteQuery(self, node: qlast.DeleteQuery) -> None:\n        # need to parenthesise when DELETE appears as an expression\n        parenthesise = self._needs_parentheses(node)\n\n        if parenthesise:\n            self.write('(')\n\n        self._visit_aliases(node)\n\n        self._write_keywords('DELETE')\n        self._block_ws(1)\n        self.visit(node.subject)\n        self._block_ws(-1)\n        self._visit_filter(node)\n        self._visit_order(node)\n        self._visit_offset_limit(node)\n        if parenthesise:\n            self.write(')')\n\n    def visit_SelectQuery(self, node: qlast.SelectQuery) -> None:\n        # XXX: need to parenthesise when SELECT appears as an expression,\n        # the actual passed value is ignored.\n        parenthesise = self._needs_parentheses(node)\n        if node.implicit:\n            parenthesise = parenthesise and bool(node.aliases)\n\n        if parenthesise:\n            self.write('(')\n\n        if not node.implicit or node.aliases:\n            self._visit_aliases(node)\n            self._write_keywords('SELECT')\n            self._block_ws(1)\n\n        if node.result_alias:\n            self.write(node.result_alias, ' := ')\n        self.visit(node.result)\n        if not node.implicit or node.aliases:\n            self._block_ws(-1)\n        else:\n            self.write(' ')\n        self._visit_filter(node)\n        self._visit_order(node)\n        self._visit_offset_limit(node)\n        if parenthesise:\n            self.write(')')\n\n    def visit_ForQuery(self, node: qlast.ForQuery) -> None:\n        # need to parenthesize when FOR appears as an expression\n        parenthesise = self._needs_parentheses(node)\n\n        if parenthesise:\n            self.write('(')\n\n        self._visit_aliases(node)\n\n        self._write_keywords('FOR ')\n        self.write(ident_to_str(node.iterator_alias))\n        self._write_keywords(' IN ')\n        self.visit(node.iterator)\n        # guarantee an newline here\n        self.new_lines = 1\n        if node.has_union:\n            self._write_keywords('UNION ')\n            self._block_ws(1)\n            self.visit(node.result)\n            self.indentation -= 1\n        else:\n            self.visit(node.result)\n\n        if parenthesise:\n            self.write(')')\n\n    def visit_GroupingIdentList(self, atom: qlast.GroupingIdentList) -> None:\n        self.write('(')\n        self.visit_list(atom.elements, newlines=False)\n        self.write(')')\n\n    def visit_GroupingSimple(self, node: qlast.GroupingSimple) -> None:\n        self.visit(node.element)\n\n    def visit_GroupingSets(self, node: qlast.GroupingSets) -> None:\n        self.write('{')\n        self.visit_list(node.sets, newlines=False)\n        self.write('}')\n\n    def visit_GroupingOperation(self, node: qlast.GroupingOperation) -> None:\n        self._write_keywords(node.oper)\n        self.write(' (')\n        self.visit_list(node.elements, newlines=False)\n        self.write(')')\n\n    def visit_GroupQuery(\n        self,\n        node: qlast.GroupQuery | qlast.InternalGroupQuery,\n        no_paren: bool = False\n    ) -> None:\n        # need to parenthesise when GROUP appears as an expression\n        parenthesise = self._needs_parentheses(node) and not no_paren\n\n        if parenthesise:\n            self.write('(')\n\n        self._visit_aliases(node)\n\n        if isinstance(node, qlast.InternalGroupQuery):\n            self._write_keywords('FOR ')\n        self._write_keywords('GROUP')\n        self._block_ws(1)\n        if node.subject_alias:\n            self.write(ident_to_str(node.subject_alias), ' := ')\n        self.visit(node.subject)\n        self._block_ws(-1)\n        if node.using:\n            self._write_keywords('USING')\n            self._block_ws(1)\n            self.visit_list(node.using, newlines=False)\n            self._block_ws(-1)\n        self._write_keywords('BY ')\n        self.visit_list(node.by)\n\n        if parenthesise:\n            self.write(')')\n\n    def visit_InternalGroupQuery(self, node: qlast.InternalGroupQuery) -> None:\n        parenthesise = self._needs_parentheses(node)\n        if parenthesise:\n            self.write('(')\n\n        self.visit_GroupQuery(node, no_paren=True)\n        self._block_ws(0)\n        self._write_keywords('IN ')\n        self.write(ident_to_str(node.group_alias))\n        if node.grouping_alias:\n            self.write(', ')\n            self.write(ident_to_str(node.grouping_alias))\n        self.write(' ')\n        self._block_ws(0)\n        self._write_keywords('UNION ')\n        self.visit(node.result)\n\n        if node.where:\n            self._write_keywords(' FILTER ')\n            self.visit(node.where)\n\n        if node.orderby:\n            self._write_keywords(' ORDER BY ')\n            self.visit_list(\n                node.orderby,\n                separator=self._kw_case(' THEN'), newlines=False\n            )\n\n        if parenthesise:\n            self.write(')')\n\n    def visit_ModuleAliasDecl(self, node: qlast.ModuleAliasDecl) -> None:\n        if node.alias:\n            self.write(ident_to_str(node.alias))\n            self._write_keywords(' AS ')\n        self._write_keywords('MODULE ')\n        self.write(ident_to_str(node.module))\n\n    def visit_SortExpr(self, node: qlast.SortExpr) -> None:\n        self.visit(node.path)\n        if node.direction:\n            self.write(' ')\n            self.write(node.direction)\n        if node.nones_order:\n            self._write_keywords(' EMPTY ')\n            self.write(node.nones_order.upper())\n\n    def visit_DetachedExpr(self, node: qlast.DetachedExpr) -> None:\n        self._write_keywords('DETACHED ')\n        self.visit(node.expr)\n\n    def visit_GlobalExpr(self, node: qlast.GlobalExpr) -> None:\n        self._write_keywords('GLOBAL ')\n        self.visit(node.name)\n\n    def visit_StrInterp(self, node: qlast.StrInterp) -> None:\n        self.write(\"'\")\n        self.write(edgeql_quote.escape_string(node.prefix))\n        for fragment in node.interpolations:\n            self.write(\"\\\\(\")\n            self.visit(fragment.expr)\n            self.write(\")\")\n            self.write(edgeql_quote.escape_string(fragment.suffix))\n        self.write(\"'\")\n\n    def visit_UnaryOp(self, node: qlast.UnaryOp) -> None:\n        op = str(node.op).upper()\n        self.write(op)\n        if op.isalnum():\n            self.write(' (')\n        self.visit(node.operand)\n        if op.isalnum():\n            self.write(')')\n\n    def visit_BinOp(self, node: qlast.BinOp) -> None:\n        self.write('(')\n        self.visit(node.left)\n        self.write(' ' + str(node.op).upper() + ' ')\n        self.visit(node.right)\n        self.write(')')\n\n    def visit_IsOp(self, node: qlast.IsOp) -> None:\n        self.write('(')\n        self.visit(node.left)\n        self.write(' ' + str(node.op).upper() + ' ')\n        self.visit(node.right)\n        self.write(')')\n\n    def visit_TypeOp(self, node: qlast.TypeOp) -> None:\n        self.write('(')\n        self.visit(node.left)\n        self.write(' ' + str(node.op).upper() + ' ')\n        self.visit(node.right)\n        self.write(')')\n\n    def visit_IfElse(self, node: qlast.IfElse) -> None:\n        parent = node._parent  # type: ignore\n        parenthesize = not (\n            isinstance(parent, qlast.SelectQuery)\n            and parent.implicit\n            and isinstance(parent._parent, qlast.Commands)  # type: ignore\n        )\n        if parenthesize:\n            self.write('(')\n        if node.python_style:\n            self.visit(node.if_expr)\n            self._write_keywords(' IF ')\n            self.visit(node.condition)\n            self._write_keywords(' ELSE ')\n            self.visit(node.else_expr)\n        else:\n            self._write_keywords('IF ')\n            self.visit(node.condition)\n            self._write_keywords(' THEN ')\n            self.visit(node.if_expr)\n            self._write_keywords(' ELSE ')\n            self.visit(node.else_expr)\n        if parenthesize:\n            self.write(')')\n\n    def visit_Tuple(self, node: qlast.Tuple) -> None:\n        self.write('(')\n        count = len(node.elements)\n        self.visit_list(node.elements, newlines=False)\n        if count == 1:\n            self.write(',')\n\n        self.write(')')\n\n    def visit_Set(self, node: qlast.Set) -> None:\n        self.write('{')\n        self.visit_list(node.elements, newlines=False)\n        self.write('}')\n\n    def visit_Array(self, node: qlast.Array) -> None:\n        self.write('[')\n        self.visit_list(node.elements, newlines=False)\n        self.write(']')\n\n    def visit_NamedTuple(self, node: qlast.NamedTuple) -> None:\n        self.write('(')\n        self._block_ws(1)\n        self.visit_list(node.elements, newlines=True, separator=',')\n        self._block_ws(-1)\n        self.write(')')\n\n    def visit_TupleElement(self, node: qlast.TupleElement) -> None:\n        self.visit(node.name)\n        self.write(' := ')\n        self.visit(node.val)\n\n    def visit_Path(self, node: qlast.Path) -> None:\n        for i, e in enumerate(node.steps):\n            if i > 0 or node.partial:\n                if (getattr(e, 'type', None) != 'property'\n                        and not isinstance(e, qlast.TypeIntersection)):\n                    self.write('.')\n\n            if i == 0:\n                if isinstance(\n                    e,\n                    (\n                        qlast.ObjectRef,\n                        qlast.Anchor,\n                        qlast.Splat,\n                        qlast.Ptr,\n                        qlast.Set,\n                        qlast.Tuple,\n                        qlast.NamedTuple,\n                        qlast.TypeIntersection,\n                        qlast.QueryParameter,\n                        qlast.FunctionParameter,\n                    ),\n                ):\n                    self.visit(e)\n                else:\n                    self.write('(')\n                    self.visit(e)\n                    self.write(')')\n            else:\n                self.visit(e)\n\n    def visit_Shape(self, node: qlast.Shape) -> None:\n        if node.expr is not None:\n            self.visit(node.expr)\n            self.write(' ')\n        self._visit_shape(node.elements)\n\n    def _visit_shape(\n        self,\n        shape: Sequence[qlast.ShapeElement],\n        always_emit_braces: bool=False,\n    ) -> None:\n        if shape or always_emit_braces:\n            self.write('{')\n            self._block_ws(1)\n            self.visit_list(shape)\n            self._block_ws(-1)\n            self.write('}')\n\n    def visit_Ptr(self, node: qlast.Ptr, *, quote: bool = True) -> None:\n        if node.type == 'property':\n            self.write('@')\n        elif node.type == 'optional':\n            self.write('?>')\n        elif node.direction and node.direction != '>':\n            self.write(node.direction)\n\n        self.write(ident_to_str(node.name, allow_num=True))\n\n    def visit_Splat(self, node: qlast.Splat) -> None:\n        if node.type is not None:\n            self.visit(node.type)\n        if node.intersection is not None:\n            self.visit(node.intersection)\n        if node.type is not None or node.intersection is not None:\n            self.write('.')\n        if node.depth == 1:\n            self.write('*')\n        elif node.depth == 2:\n            self.write('**')\n        else:\n            raise AssertionError(f\"unexpected splat depth: {node.depth}\")\n\n    def visit_TypeIntersection(self, node: qlast.TypeIntersection) -> None:\n        self._write_keywords('[IS ')\n        self.visit(node.type)\n        self.write(']')\n\n    def visit_ShapeElement(self, node: qlast.ShapeElement) -> None:\n        # PathSpec can only contain LinkExpr or LinkPropExpr,\n        # and must not be quoted.\n\n        quals = []\n        if node.required is not None:\n            if node.required:\n                quals.append('required')\n            else:\n                quals.append('optional')\n\n        if node.cardinality:\n            quals.append(node.cardinality.as_ptr_qual())\n\n        if quals:\n            self.write(*quals, delimiter=' ')\n            self.write(' ')\n\n        if len(node.expr.steps) == 1:\n            self.visit(node.expr)\n        else:\n            self.visit(node.expr.steps[0])\n            if not isinstance(node.expr.steps[1], qlast.TypeIntersection):\n                self.write('.')\n            self.visit(node.expr.steps[1])\n            if len(node.expr.steps) == 3:\n                self.visit(node.expr.steps[2])\n\n        if not node.compexpr and node.elements:\n            self.write(': ')\n            self._visit_shape(node.elements)\n\n        if node.where:\n            self._write_keywords(' FILTER ')\n            self.visit(node.where)\n\n        if node.orderby:\n            self._write_keywords(' ORDER BY ')\n            self.visit_list(\n                node.orderby,\n                separator=self._kw_case(' THEN'), newlines=False\n            )\n\n        if node.offset:\n            self._write_keywords(' OFFSET ')\n            self.visit(node.offset)\n\n        if node.limit:\n            self._write_keywords(' LIMIT ')\n            self.visit(node.limit)\n\n        if node.compexpr:\n            if node.operation is None:\n                raise AssertionError(\n                    f'ShapeElement.operation is unexpectedly None'\n                )\n\n            if node.operation.op is qlast.ShapeOp.ASSIGN:\n                self.write(' := ')\n            elif node.operation.op is qlast.ShapeOp.APPEND:\n                self.write(' += ')\n            elif node.operation.op is qlast.ShapeOp.SUBTRACT:\n                self.write(' -= ')\n            else:\n                raise NotImplementedError(\n                    f'unexpected shape operation: {node.operation.op!r}'\n                )\n            self.visit(node.compexpr)\n\n    def visit_QueryParameter(self, node: qlast.QueryParameter) -> None:\n        self.write(param_to_str(node.name))\n\n    def visit_FunctionParameter(self, node: qlast.FunctionParameter) -> None:\n        self.write(param_to_str(node.name))\n\n    def visit_Placeholder(self, node: qlast.Placeholder) -> None:\n        self.write('\\\\(')\n        self.write(node.name)\n        self.write(')')\n\n    def visit_Constant(self, node: qlast.Constant) -> None:\n        if node.kind == qlast.ConstantKind.STRING:\n            if not _NON_PRINTABLE_RE.search(node.value):\n                for d in (\"'\", '\"', '$$'):\n                    if d not in node.value:\n                        if '\\\\' in node.value and d != '$$':\n                            self.write('r', d, node.value, d)\n                        else:\n                            self.write(d, node.value, d)\n                        return\n                self.write(edgeql_quote.dollar_quote_literal(node.value))\n                return\n            self.write(repr(node.value))\n        else:\n            self.write(node.value)\n\n    def visit_BytesConstant(self, node: qlast.BytesConstant) -> None:\n        val = _BYTES_ESCAPE_RE.sub(_bytes_escape, node.value)\n        self.write(\"b'\", val.decode('utf-8', 'backslashreplace'), \"'\")\n\n    def visit_FunctionCall(self, node: qlast.FunctionCall) -> None:\n        if isinstance(node.func, tuple):\n            self.write(\n                f'{ident_to_str(node.func[0])}::{ident_to_str(node.func[1])}')\n        else:\n            self.write(ident_to_str(node.func))\n\n        self.write('(')\n\n        for i, arg in enumerate(node.args):\n            if i > 0:\n                self.write(', ')\n            self.visit(arg)\n\n        if node.kwargs:\n            if node.args:\n                self.write(', ')\n\n            for i, (name, arg) in enumerate(node.kwargs.items()):\n                if i > 0:\n                    self.write(', ')\n                self.write(f'{edgeql_quote.quote_ident(name)} := ')\n                self.visit(arg)\n\n        self.write(')')\n\n        if node.window:\n            self._write_keywords(' OVER (')\n            self._block_ws(1)\n\n            if node.window.partition:\n                self._write_keywords('PARTITION BY ')\n                self.visit_list(node.window.partition, newlines=False)\n                self.new_lines = 1\n\n            if node.window.orderby:\n                self._write_keywords('ORDER BY ')\n                self.visit_list(\n                    node.window.orderby, separator=self._kw_case(' THEN'))\n\n            self._block_ws(-1)\n            self.write(')')\n\n    def visit_PseudoObjectRef(self, node: qlast.PseudoObjectRef) -> None:\n        self.write(node.name)\n\n    def visit_TypeCast(self, node: qlast.TypeCast) -> None:\n        self.write('<')\n        if node.cardinality_mod is qlast.CardinalityModifier.Optional:\n            self.write('optional ')\n        self.visit(node.type)\n        self.write('>')\n        self.visit(node.expr)\n\n    def visit_Indirection(self, node: qlast.Indirection) -> None:\n        self.write('(')\n        self.visit(node.arg)\n        self.write(')')\n        for indirection in node.indirection:\n            self.visit(indirection)\n\n    def visit_Slice(self, node: qlast.Slice) -> None:\n        self.write('[')\n        if node.start:\n            self.visit(node.start)\n        self.write(':')\n        if node.stop:\n            self.visit(node.stop)\n        self.write(']')\n\n    def visit_Index(self, node: qlast.Index) -> None:\n        self.write('[')\n        self.visit(node.index)\n        self.write(']')\n\n    def visit_ObjectRef(self, node: qlast.ObjectRef) -> None:\n        if node.itemclass:\n            self.write(node.itemclass)\n            self.write(' ')\n        if node.module:\n            self.write(ident_to_str(node.module))\n            self.write('::')\n        self.write(ident_to_str(node.name))\n\n    def visit_SpecialAnchor(self, node: qlast.Anchor) -> None:\n        self.write(node.name)\n\n    def visit_IRAnchor(self, node: qlast.Anchor) -> None:\n        self.write(node.name)\n\n    def visit_TypeExprLiteral(self, node: qlast.TypeExprLiteral) -> None:\n        self.visit(node.val)\n\n    def visit_TypeName(self, node: qlast.TypeName) -> None:\n        parenthesize = (\n            isinstance(\n                node._parent,  # type: ignore\n                (qlast.IsOp, qlast.TypeOp, qlast.Introspect),\n            )\n            and node.subtypes is not None\n        )\n        if parenthesize:\n            self.write('(')\n        if node.name is not None:\n            self.write(ident_to_str(node.name), ': ')\n\n        self.visit(node.maintype)\n        if node.subtypes is not None:\n            self.write('<')\n            self.visit_list(node.subtypes, newlines=False)\n            if node.dimensions is not None:\n                for dim in node.dimensions:\n                    if dim is None:\n                        self.write('[]')\n                    else:\n                        self.write('[', str(dim), ']')\n            self.write('>')\n        if parenthesize:\n            self.write(')')\n\n    def visit_Introspect(self, node: qlast.Introspect) -> None:\n        self.write('INTROSPECT ')\n        self.visit(node.type)\n\n    def visit_TypeOf(self, node: qlast.TypeOf) -> None:\n        self.write('TYPEOF ')\n        self.visit(node.expr)\n\n    # DDL nodes\n\n    def visit_DDLQuery(self, node: qlast.DDLQuery) -> None:\n        self.visit(node.query)\n\n    def visit_Position(self, node: qlast.Position) -> None:\n        self.write(node.position)\n        if node.ref:\n            self.write(' ')\n            self.visit(node.ref)\n\n    def _ddl_visit_bases(self, node: qlast.BasedOn) -> None:\n        if node.bases:\n            self._write_keywords(' EXTENDING ')\n            self.visit_list(node.bases, newlines=False)\n\n    PointerNode = TypeVar(\n        'PointerNode',\n        qlast.CreateConcretePointer,\n        qlast.CreateLink,\n        qlast.CreateProperty\n    )\n\n    def _ddl_add_pointer_bases(\n        self,\n        node: PointerNode,\n    ) -> PointerNode:\n        # We very carefully strained EXTENDING clauses out of subcommands\n        # when parsing, but now that we're printing, we want to print it\n        # back in the commands block. Do that by \"faking\" an extending\n        # node in the subcommands of a scoped copy of this node.\n        if node.bases:\n            return node.replace(commands=(\n                [qlast.AlterAddInherit(bases=node.bases)] + node.commands\n            ))\n        else:\n            return node\n\n    def _ddl_clean_up_commands(\n        self,\n        commands: Sequence[qlast.Base],\n    ) -> Sequence[qlast.Base]:\n        # Always omit orig_expr fields from output since we are\n        # using the original expression in TEXT output\n        # already.\n        return [\n            c for c in commands\n            if (\n                not isinstance(c, qlast.SetField)\n                or not c.name.startswith('orig_')\n            )\n        ]\n\n    def _ddl_visit_body(\n        self,\n        commands: Sequence[qlast.Base],\n        group_by_system_comment: bool = False,\n        *,\n        allow_short: bool = False\n    ) -> None:\n        if self.limit_ref_classes:\n            commands = [\n                c for c in commands\n                if (\n                    not isinstance(c, qlast.ObjectDDL)\n                    or c.name.itemclass in self.limit_ref_classes\n                )\n            ]\n\n        commands = self._ddl_clean_up_commands(commands)\n        if len(commands) == 1 and allow_short and not (\n            isinstance(commands[0], qlast.ObjectDDL)\n            and not isinstance(commands[0], qlast.Rename)\n        ):\n            self.write(' ')\n            self.visit(commands[0])\n        elif len(commands) > 0:\n            self.write(' {')\n            self._block_ws(1)\n\n            if group_by_system_comment:\n                sort_key = lambda c: (\n                    c.system_comment or '',\n                    c.name.name if isinstance(c.name, qlast.ObjectRef)\n                    else c.name\n                )\n                group_key = lambda c: c.system_comment or ''\n                if not self.unsorted:\n                    commands = sorted(commands, key=sort_key)\n                groups = itertools.groupby(commands, group_key)\n                for i, (comment, items) in enumerate(groups):\n                    if i > 0:\n                        self.new_lines = 2\n                    if comment:\n                        self.write('#')\n                        self.new_lines = 1\n                        self.write(f'# {comment}')\n                        self.new_lines = 1\n                        self.write('#')\n                        self.new_lines = 1\n                    self.visit_list(list(items), terminator=';')\n            elif self.descmode or self.sdlmode:\n                def sort_desc_or_sdl(\n                    c: qlast.Base,\n                ) -> tuple[str, ...]:\n                    # The sort key is a tuple of parts of the command which will\n                    # be rendered to text.\n                    #\n                    # Commands will be ordered generally as:\n                    # 1. General DDL Operations\n                    # 2. Set Field Operations\n                    # 3. Object DDL Operations\n                    #\n                    # Empty strings are used to achieve this general ordering.\n                    #\n                    # General DDL Operations are sorted by command class name.\n                    # This works because these commands can each appear once per\n                    # body.\n                    # eg. ('', '', '', 'AlterAddInherit')\n                    #\n                    # Set Field Operations are sorted by field name.\n                    # eg. ('', '', 'readonly')\n                    #\n                    # Object DDL Operations are sorted first by itemclass then\n                    # name.\n                    # eg. ('TYPE', 'Foo')\n                    #\n                    # For constraints and indexes, the expression and except\n                    # expression are included.\n                    # eg. ('CONSTRAINT', 'exclusive', '.a', '.b')\n\n                    if isinstance(c, qlast.ObjectDDL):\n                        if isinstance(c, qlast.ConcreteConstraintOp):\n                            subject_expr = (\n                                self.generate_isolated_text(c.subjectexpr)\n                                if c.subjectexpr is not None else\n                                ''\n                            )\n                            except_expr = (\n                                self.generate_isolated_text(c.except_expr)\n                                if c.except_expr is not None else\n                                ''\n                            )\n                            return (\n                                typeutils.not_none(c.name.itemclass),\n                                c.name.name,\n                                subject_expr,\n                                except_expr,\n                            )\n\n                        if isinstance(c, qlast.ConcreteIndexCommand):\n                            expr = (\n                                self.generate_isolated_text(c.expr)\n                                if c.expr is not None else\n                                ''\n                            )\n                            except_expr = (\n                                self.generate_isolated_text(c.except_expr)\n                                if c.except_expr is not None else\n                                ''\n                            )\n                            return (\n                                typeutils.not_none(c.name.itemclass),\n                                c.name.name,\n                                expr,\n                                except_expr,\n                            )\n\n                        return (c.name.itemclass or '', c.name.name)\n\n                    if isinstance(c, qlast.SetField):\n                        return ('', '', c.name)\n\n                    return ('', '', '', c.__class__.__name__)\n\n                if not self.unsorted:\n                    commands = sorted(commands, key=sort_desc_or_sdl)\n\n                self.visit_list(list(commands), terminator=';')\n\n            else:\n                self.visit_list(list(commands), terminator=';')\n\n            self._block_ws(-1)\n            self.write('}')\n\n    def _visit_CreateObject(\n        self,\n        node: qlast.CreateObject,\n        *object_keywords: str,\n        after_name: Optional[Callable[[], None]] = None,\n        render_commands: bool = True,\n        unqualified: bool = False,\n        named: bool = True,\n        group_by_system_comment: bool = False,\n    ) -> None:\n        self._visit_aliases(node)\n        if self.sdlmode:\n            self.write(*[kw.lower() for kw in object_keywords], delimiter=' ')\n        else:\n            self._write_keywords('CREATE', *object_keywords)\n        if named:\n            self.write(' ')\n            if unqualified or not node.name.module:\n                self.write(ident_to_str(node.name.name))\n            else:\n                self.write(ident_to_str(node.name.module), '::',\n                           ident_to_str(node.name.name))\n        if after_name:\n            after_name()\n        if node.create_if_not_exists and not self.sdlmode:\n            self._write_keywords(' IF NOT EXISTS')\n\n        commands = node.commands\n        if commands and render_commands:\n            self._ddl_visit_body(\n                commands,\n                group_by_system_comment=group_by_system_comment,\n            )\n\n    def _visit_AlterObject(\n        self,\n        node: qlast.AlterObject,\n        *object_keywords: str,\n        allow_short: bool = True,\n        after_name: Optional[Callable[[], None]] = None,\n        unqualified: bool = False,\n        named: bool = True,\n        ignored_cmds: Optional[AbstractSet[qlast.DDLOperation]] = None,\n        group_by_system_comment: bool = False,\n    ) -> None:\n        self._visit_aliases(node)\n        if self.sdlmode:\n            self.write(*[kw.lower() for kw in object_keywords], delimiter=' ')\n        else:\n            self._write_keywords('ALTER', *object_keywords)\n        if named:\n            self.write(' ')\n            if unqualified or not node.name.module:\n                self.write(ident_to_str(node.name.name))\n            else:\n                self.write(ident_to_str(node.name.module), '::',\n                           ident_to_str(node.name.name))\n        if after_name:\n            after_name()\n\n        commands = node.commands\n        if ignored_cmds:\n            commands = [cmd for cmd in commands\n                        if cmd not in ignored_cmds]\n\n        if commands:\n            self._ddl_visit_body(\n                commands,\n                group_by_system_comment=group_by_system_comment,\n                allow_short=allow_short,\n            )\n\n    def _visit_DropObject(\n        self,\n        node: qlast.DropObject,\n        *object_keywords: str,\n        unqualified: bool = False,\n        after_name: Optional[Callable[[], None]] = None,\n        named: bool = True,\n    ) -> None:\n        self._visit_aliases(node)\n        self._write_keywords('DROP', *object_keywords)\n        if named:\n            self.write(' ')\n            if unqualified or not node.name.module:\n                self.write(ident_to_str(node.name.name))\n            else:\n                self.write(ident_to_str(node.name.module), '::',\n                           ident_to_str(node.name.name))\n        if after_name:\n            after_name()\n        if node.commands:\n            self.write(' {')\n            self._block_ws(1)\n            self.visit_list(node.commands, terminator=';')\n            self.indentation -= 1\n            self.write('}')\n\n    def visit_Rename(self, node: qlast.Rename) -> None:\n        self._write_keywords('RENAME TO ')\n        self.visit(node.new_name)\n\n    def visit_AlterAddInherit(self, node: qlast.AlterAddInherit) -> None:\n        if node.bases:\n            self._write_keywords('EXTENDING ')\n            self.visit_list(node.bases)\n            if node.position is not None:\n                self.write(' ')\n                self.visit(node.position)\n\n    def visit_AlterDropInherit(self, node: qlast.AlterDropInherit) -> None:\n        if node.bases:\n            self._write_keywords('DROP EXTENDING ')\n            self.visit_list(node.bases)\n\n    def visit_CreateDatabase(self, node: qlast.CreateDatabase) -> None:\n        if node.flavor == qltypes.SchemaObjectClass.BRANCH:\n            if node.branch_type == qlast.BranchType.EMPTY:\n                self._visit_CreateObject(node, 'EMPTY BRANCH')\n            else:\n\n                def after_name() -> None:\n                    self._write_keywords(' FROM ')\n                    assert node.template\n                    self.visit(node.template)\n                self._visit_CreateObject(\n                    node, f'{node.branch_type} BRANCH', after_name=after_name)\n        elif node.flavor == qltypes.SchemaObjectClass.DATABASE:\n            self._visit_CreateObject(node, 'DATABASE')\n        else:\n            raise EdgeQLSourceGeneratorError(\n                f'unknown branch command flavor: {node.flavor!r}'\n            )\n\n    def visit_AlterDatabase(self, node: qlast.AlterDatabase) -> None:\n        self._visit_AlterObject(node, node.flavor)\n\n    def visit_DropDatabase(self, node: qlast.DropDatabase) -> None:\n        self._visit_DropObject(node, node.flavor)\n\n    def visit_CreateRole(self, node: qlast.CreateRole) -> None:\n        after_name = lambda: self._ddl_visit_bases(node)\n        keywords = []\n        if node.superuser:\n            keywords.append('SUPERUSER')\n        keywords.append('ROLE')\n        self._visit_CreateObject(node, *keywords, after_name=after_name)\n\n    def visit_AlterRole(self, node: qlast.AlterRole) -> None:\n        self._visit_AlterObject(node, 'ROLE')\n\n    def visit_DropRole(self, node: qlast.DropRole) -> None:\n        self._visit_DropObject(node, 'ROLE')\n\n    def visit_CreateExtensionPackage(\n        self,\n        node: qlast.CreateExtensionPackage,\n    ) -> None:\n        self._write_keywords('CREATE EXTENSION PACKAGE')\n        self.write(' ')\n        self.write(ident_to_str(node.name.name))\n        self._write_keywords(' VERSION ')\n        self.visit(node.version)\n        if node.body.text:\n            self.write(' {')\n            self._block_ws(1)\n            self.write(self.indent_text(node.body.text))\n            self._block_ws(-1)\n            self.write('}')\n        elif node.body.commands:\n            self._ddl_visit_body(node.body.commands)\n\n    def visit_DropExtensionPackage(\n        self,\n        node: qlast.DropExtensionPackage,\n    ) -> None:\n        def after_name() -> None:\n            self._write_keywords(' VERSION ')\n            self.visit(node.version)\n\n        self._visit_DropObject(node, 'EXTENSION PACKAGE', after_name=after_name)\n\n    def visit_CreateExtensionPackageMigration(\n        self,\n        node: qlast.CreateExtensionPackageMigration,\n    ) -> None:\n        self._write_keywords('CREATE EXTENSION PACKAGE')\n        self.write(' ')\n        self.write(ident_to_str(node.name.name))\n        self._write_keywords(' MIGRATION FROM ')\n        self._write_keywords(' VERSION ')\n        self.visit(node.from_version)\n        self._write_keywords(' TO ')\n        self.visit(node.to_version)\n\n        if node.body.text:\n            self.write(' {')\n            self._block_ws(1)\n            self.write(self.indent_text(node.body.text))\n            self._block_ws(-1)\n            self.write('}')\n        elif node.body.commands:\n            self._ddl_visit_body(node.body.commands)\n\n    def visit_DropExtensionPackageMigration(\n        self,\n        node: qlast.DropExtensionPackageMigration,\n    ) -> None:\n        self._write_keywords('DROP EXTENSION PACKAGE')\n        self.write(' ')\n        self.write(ident_to_str(node.name.name))\n        self._write_keywords(' MIGRATION FROM ')\n        self._write_keywords(' VERSION ')\n        self.visit(node.from_version)\n        self._write_keywords(' TO ')\n        self.visit(node.to_version)\n\n    def visit_CreateExtension(\n        self,\n        node: qlast.CreateExtension,\n    ) -> None:\n        if self.sdlmode or self.descmode:\n            self._write_keywords('using extension')\n        else:\n            self._write_keywords('CREATE EXTENSION')\n        self.write(' ')\n        self.write(ident_to_str(node.name.name))\n        if node.version is not None:\n            self._write_keywords(' version ')\n            self.visit(node.version)\n        if node.commands:\n            self._ddl_visit_body(node.commands)\n\n    def visit_AlterExtension(self, node: qlast.AlterExtension) -> None:\n        self._write_keywords('ALTER EXTENSION')\n        self.write(' ')\n        self.write(ident_to_str(node.name.name))\n        self._write_keywords(' TO VERSION ')\n        self.visit(node.to_version)\n\n    def visit_DropExtension(\n        self,\n        node: qlast.DropExtension,\n    ) -> None:\n        self._visit_DropObject(node, 'EXTENSION')\n\n    def visit_CreateFuture(\n        self,\n        node: qlast.CreateFuture,\n    ) -> None:\n        if self.sdlmode or self.descmode:\n            self._write_keywords('using future')\n        else:\n            self._write_keywords('CREATE FUTURE')\n        self.write(' ')\n        self.write(ident_to_str(node.name.name))\n\n    def visit_DropFuture(\n        self,\n        node: qlast.DropFuture,\n    ) -> None:\n        self._visit_DropObject(node, 'FUTURE')\n\n    def visit_CreateMigration(self, node: qlast.CreateMigration) -> None:\n        self._write_keywords('CREATE')\n        if node.metadata_only:\n            self._write_keywords(' APPLIED')\n        self._write_keywords(' MIGRATION')\n        if node.name is not None:\n            self.write(' ')\n            self.write(ident_to_str(node.name.name))\n            self._write_keywords(' ONTO ')\n            if node.parent is not None:\n                self.write(ident_to_str(node.parent.name))\n            else:\n                self._write_keywords('initial')\n        if node.body.text:\n            self.write(' {')\n            self._block_ws(1)\n            self.write(self.indent_text(node.body.text))\n            self._block_ws(-1)\n            self.write('}')\n        elif node.commands or node.body.commands:\n            commands = [*node.commands, *node.body.commands]\n            self._ddl_visit_body(commands)\n\n    def visit_StartMigration(self, node: qlast.StartMigration) -> None:\n        if isinstance(node.target, qlast.CommittedSchema):\n            self._write_keywords('START MIGRATION TO COMMITTED SCHEMA')\n        else:\n            self._write_keywords('START MIGRATION TO {')\n            self.new_lines = 1\n            self.indentation += 1\n            self.visit(node.target)\n            self.indentation -= 1\n            self.new_lines = 1\n            self.write('}')\n\n    def visit_CommitMigration(self, node: qlast.CommitMigration) -> None:\n        self._write_keywords('COMMIT MIGRATION')\n\n    def visit_AbortMigration(self, node: qlast.AbortMigration) -> None:\n        self._write_keywords('ABORT MIGRATION')\n\n    def visit_PopulateMigration(self, node: qlast.PopulateMigration) -> None:\n        self._write_keywords('POPULATE MIGRATION')\n\n    def visit_StartMigrationRewrite(\n        self, node: qlast.StartMigrationRewrite\n    ) -> None:\n        self._write_keywords('START MIGRATION REWRITE')\n\n    def visit_CommitMigrationRewrite(\n        self, node: qlast.CommitMigrationRewrite\n    ) -> None:\n        self._write_keywords('COMMIT MIGRATION REWRITE')\n\n    def visit_AbortMigrationRewrite(\n        self, node: qlast.AbortMigrationRewrite\n    ) -> None:\n        self._write_keywords('ABORT MIGRATION REWRITE')\n\n    def visit_DescribeCurrentMigration(\n        self,\n        node: qlast.DescribeCurrentMigration,\n    ) -> None:\n        self._write_keywords('DESCRIBE CURRENT MIGRATION AS ')\n        self.write(node.language.upper())\n\n    def visit_AlterCurrentMigrationRejectProposed(\n        self,\n        node: qlast.AlterCurrentMigrationRejectProposed,\n    ) -> None:\n        self._write_keywords('ALTER CURRENT MIGRATION REJECT PROPOSED')\n\n    def visit_AlterMigration(self, node: qlast.AlterMigration) -> None:\n        self._visit_AlterObject(node, 'MIGRATION')\n\n    def visit_DropMigration(self, node: qlast.DropMigration) -> None:\n        self._visit_DropObject(node, 'MIGRATION')\n\n    def visit_ResetSchema(self, node: qlast.ResetSchema) -> None:\n        self._write_keywords(f'RESET SCHEMA TO {node.target}')\n\n    def visit_CreateModule(self, node: qlast.CreateModule) -> None:\n        self._visit_CreateObject(node, 'MODULE')\n        # Hack to handle the SDL version of this with an empty block.\n        if self.sdlmode and not node.commands:\n            self.write('{}')\n\n    def visit_AlterModule(self, node: qlast.AlterModule) -> None:\n        self._visit_AlterObject(node, 'MODULE')\n\n    def visit_DropModule(self, node: qlast.DropModule) -> None:\n        self._visit_DropObject(node, 'MODULE')\n\n    def visit_CreateAlias(self, node: qlast.CreateAlias) -> None:\n        if (\n            len(node.commands) == 1\n            and isinstance(node.commands[0], qlast.SetField)\n            and node.commands[0].name == 'expr'\n        ):\n\n            self._visit_CreateObject(node, 'ALIAS', render_commands=False)\n            self.write(' := (')\n            self.new_lines = 1\n            self.indentation += 1\n            expr = node.commands[0].value\n            assert expr is not None\n            self.visit(expr)\n            self.indentation -= 1\n            self.new_lines = 1\n            self.write(')')\n        else:\n            self._visit_CreateObject(node, 'ALIAS')\n\n    def visit_AlterAlias(self, node: qlast.AlterAlias) -> None:\n        self._visit_AlterObject(node, 'ALIAS')\n\n    def visit_DropAlias(self, node: qlast.DropAlias) -> None:\n        self._visit_DropObject(node, 'ALIAS')\n\n    def visit_SetField(self, node: qlast.SetField) -> None:\n        if node.special_syntax:\n            if node.name == 'expr':\n                if node.value is None:\n                    self._write_keywords('RESET', 'EXPRESSION')\n                else:\n                    self._write_keywords('USING')\n                    self.write(' (')\n                    self.visit(node.value)\n                    self.write(')')\n            elif node.name == 'condition':\n                if node.value is None:\n                    self._write_keywords('RESET', 'WHEN')\n                else:\n                    self._write_keywords('WHEN')\n                    self.write(' (')\n                    self.visit(node.value)\n                    self.write(')')\n            elif node.name == 'target':\n                if node.value is None:\n                    self._write_keywords('RESET', 'TYPE')\n                else:\n                    self._write_keywords('SET', 'TYPE ')\n                    self.visit(node.value)\n            else:\n                keywords = self._process_special_set(node)\n                self.write(*keywords, delimiter=' ')\n        elif node.value:\n            if not self.sdlmode:\n                self._write_keywords('SET ')\n            self.write(f'{node.name} := ')\n            if not isinstance(node.value, (qlast.BaseConstant, qlast.Set)):\n                self.write('(')\n            self.visit(node.value)\n            if not isinstance(node.value, (qlast.BaseConstant, qlast.Set)):\n                self.write(')')\n        elif not self.sdlmode:\n            self._write_keywords('RESET ')\n            self.write(node.name)\n\n    def _eval_bool_expr(\n        self,\n        expr: qlast.Expr | qlast.TypeExpr,\n    ) -> bool:\n        if (not isinstance(expr, qlast.Constant)\n            or expr.kind != qlast.ConstantKind.BOOLEAN\n        ):\n            raise AssertionError(f'expected BooleanConstant, got {expr!r}')\n        return expr.value == 'true'\n\n    def _eval_enum_expr(\n        self,\n        expr: qlast.Expr | qlast.TypeExpr,\n        enum_type: type[Enum_T],\n    ) -> Enum_T:\n        if (\n            not isinstance(expr, qlast.Constant)\n            or expr.kind != qlast.ConstantKind.STRING\n        ):\n            raise AssertionError(f'expected StringConstant, got {expr!r}')\n        return enum_type(expr.value)\n\n    def _process_special_set(self, node: qlast.SetField) -> list[str]:\n\n        keywords: list[str] = []\n        fname = node.name\n\n        if fname == 'required':\n            if node.value is None:\n                keywords.extend(('RESET', 'OPTIONALITY'))\n            elif self._eval_bool_expr(node.value):\n                keywords.extend(('SET', 'REQUIRED'))\n            else:\n                keywords.extend(('SET', 'OPTIONAL'))\n        elif fname == 'abstract':\n            if node.value is None:\n                keywords.extend(('RESET', 'ABSTRACT'))\n            elif self._eval_bool_expr(node.value):\n                keywords.extend(('SET', 'ABSTRACT'))\n            else:\n                keywords.extend(('SET', 'NOT', 'ABSTRACT'))\n        elif fname == 'delegated':\n            if node.value is None:\n                keywords.extend(('RESET', 'DELEGATED'))\n            elif self._eval_bool_expr(node.value):\n                keywords.extend(('SET', 'DELEGATED'))\n            else:\n                keywords.extend(('SET', 'NOT', 'DELEGATED'))\n        elif fname == 'cardinality':\n            if node.value is None:\n                keywords.extend(('RESET', 'CARDINALITY'))\n            elif node.value:\n                value = self._eval_enum_expr(\n                    node.value, qltypes.SchemaCardinality)\n                keywords.extend(('SET', value.to_edgeql()))\n        elif fname == 'owned':\n            if node.value is None:\n                keywords.extend(('DROP', 'OWNED'))\n            elif self._eval_bool_expr(node.value):\n                keywords.extend(('SET', 'OWNED'))\n            else:\n                keywords.extend(('DROP', 'OWNED'))\n        elif fname == 'deferred':\n            if node.value is None:\n                keywords.extend(('RESET', 'DEFERRED'))\n            elif self._eval_bool_expr(node.value):\n                keywords.extend(('SET', 'DEFERRED'))\n            else:\n                keywords.extend(('DROP', 'DEFERRED'))\n        else:\n            raise EdgeQLSourceGeneratorError(\n                'unknown special field: {!r}'.format(fname))\n\n        return keywords\n\n    def visit_CreateAnnotation(self, node: qlast.CreateAnnotation) -> None:\n        after_name = lambda: self._ddl_visit_bases(node)\n        if node.inheritable:\n            tag = 'ABSTRACT INHERITABLE ANNOTATION'\n        else:\n            tag = 'ABSTRACT ANNOTATION'\n        self._visit_CreateObject(node, tag, after_name=after_name)\n\n    def visit_AlterAnnotation(self, node: qlast.AlterAnnotation) -> None:\n        self._visit_AlterObject(node, 'ABSTRACT ANNOTATION')\n\n    def visit_DropAnnotation(self, node: qlast.DropAnnotation) -> None:\n        self._visit_DropObject(node, 'ABSTRACT ANNOTATION')\n\n    def visit_CreateAnnotationValue(\n        self, node: qlast.CreateAnnotationValue\n    ) -> None:\n        if self.sdlmode:\n            self._write_keywords('annotation ')\n        else:\n            self._write_keywords('CREATE ANNOTATION ')\n        self.visit(node.name)\n        self.write(' := ')\n        self.visit(node.value)\n\n    def visit_AlterAnnotationValue(\n        self, node: qlast.AlterAnnotationValue\n    ) -> None:\n        self._write_keywords('ALTER ANNOTATION ')\n        self.visit(node.name)\n        self.write(' ')\n        if node.value:\n            self.write(':= ')\n            self.visit(node.value)\n        else:\n            # The command should be a DROP OWNED\n            assert len(node.commands) == 1\n            self.visit(node.commands[0])\n\n    def visit_DropAnnotationValue(\n        self, node: qlast.DropAnnotationValue\n    ) -> None:\n        self._write_keywords('DROP ANNOTATION ')\n        self.visit(node.name)\n\n    def visit_CreateConstraint(self, node: qlast.CreateConstraint) -> None:\n        def after_name() -> None:\n            if node.params:\n                self.write('(')\n                self.visit_list(node.params, newlines=False)\n                self.write(')')\n            if node.subjectexpr:\n                self._write_keywords(' ON ')\n                self.write('(')\n                self.visit(node.subjectexpr)\n                self.write(')')\n\n            self._ddl_visit_bases(node)\n\n        self._visit_CreateObject(\n            node, 'ABSTRACT CONSTRAINT', after_name=after_name\n        )\n\n    def visit_AlterConstraint(self, node: qlast.AlterConstraint) -> None:\n        self._visit_AlterObject(node, 'ABSTRACT CONSTRAINT')\n\n    def visit_DropConstraint(self, node: qlast.DropConstraint) -> None:\n        self._visit_DropObject(node, 'ABSTRACT CONSTRAINT')\n\n    def _after_constraint(self, node: qlast.ConcreteConstraintOp) -> None:\n        if node.args:\n            self.write('(')\n            self.visit_list(node.args, newlines=False)\n            self.write(')')\n        if node.subjectexpr:\n            self._write_keywords(' ON ')\n            self.write('(')\n            self.visit(node.subjectexpr)\n            self.write(')')\n        if node.except_expr:\n            self._write_keywords(' EXCEPT ')\n            self.write('(')\n            self.visit(node.except_expr)\n            self.write(')')\n\n    def visit_CreateConcreteConstraint(\n        self, node: qlast.CreateConcreteConstraint\n    ) -> None:\n        keywords = []\n        if node.delegated:\n            keywords.append('DELEGATED')\n        keywords.append('CONSTRAINT')\n        self._visit_CreateObject(\n            node, *keywords, after_name=lambda: self._after_constraint(node)\n        )\n\n    def visit_AlterConcreteConstraint(\n        self, node: qlast.AlterConcreteConstraint\n    ) -> None:\n        self._visit_AlterObject(\n            node,\n            'CONSTRAINT',\n            allow_short=False,\n            after_name=lambda: self._after_constraint(node),\n        )\n\n    def visit_DropConcreteConstraint(\n        self, node: qlast.DropConcreteConstraint\n    ) -> None:\n        self._visit_DropObject(\n            node, 'CONSTRAINT', after_name=lambda: self._after_constraint(node)\n        )\n\n    def _format_access_kinds(self, kinds: list[qltypes.AccessKind]) -> str:\n        # Canonicalize the order, since the schema loses track\n        kinds = [k for k in list(qltypes.AccessKind) if k in kinds]\n        if kinds == list(qltypes.AccessKind):\n            return 'all'\n        skinds = ', '.join(str(kind).lower() for kind in kinds)\n        skinds = skinds.replace(\"update\", \"update \")\n        skinds = skinds.replace(\"update read, update write\", \"update\")\n        return skinds\n\n    def visit_CreateAccessPolicy(self, node: qlast.CreateAccessPolicy) -> None:\n        def after_name() -> None:\n            if node.condition:\n                self._block_ws(1)\n                self._write_keywords('WHEN ')\n                self.write('(')\n                self.visit(node.condition)\n                self.write(')')\n                self._block_ws(-1)\n            self._block_ws(1)\n            self._write_keywords(str(node.action) + ' ')\n            if node.access_kinds:\n                self._write_keywords(\n                    self._format_access_kinds(node.access_kinds) + ' ')\n            if node.expr:\n                self._write_keywords('USING ')\n                self.write('(')\n                self.visit(node.expr)\n                self.write(')')\n\n        keywords = []\n        keywords.extend(['ACCESS', 'POLICY'])\n        self._visit_CreateObject(\n            node, *keywords, after_name=after_name, unqualified=True)\n        # This is left hanging from after_name, so that subcommands\n        # get double indented\n        self.indentation -= 1\n\n    def visit_SetAccessPerms(self, node: qlast.SetAccessPerms) -> None:\n        self._write_keywords(str(node.action) + ' ')\n        self._write_keywords(self._format_access_kinds(node.access_kinds))\n\n    def visit_AlterAccessPolicy(self, node: qlast.AlterAccessPolicy) -> None:\n        self._visit_AlterObject(node, 'ACCESS POLICY', unqualified=True)\n\n    def visit_DropAccessPolicy(self, node: qlast.DropAccessPolicy) -> None:\n        self._visit_DropObject(node, 'ACCESS POLICY', unqualified=True)\n\n    def _format_trigger_kinds(self, kinds: list[qltypes.TriggerKind]) -> str:\n        # Canonicalize the order, since the schema loses track\n        kinds = [k for k in list(qltypes.TriggerKind) if k in kinds]\n        skinds = ', '.join(str(kind).lower() for kind in kinds)\n        return skinds\n\n    def visit_CreateTrigger(self, node: qlast.CreateTrigger) -> None:\n        def after_name() -> None:\n            self._block_ws(1)\n            self._write_keywords(str(node.timing) + ' ')\n            self._write_keywords(\n                self._format_trigger_kinds(node.kinds) + ' ')\n\n            self._block_ws(0)\n            self._write_keywords('FOR ' + str(node.scope) + ' ')\n\n            if node.condition:\n                self._block_ws(1)\n                self._write_keywords('WHEN ')\n                self.write('(')\n                self.visit(node.condition)\n                self.write(')')\n                self._block_ws(-1)\n\n            self._write_keywords('DO ')\n            self.write('(')\n            self.visit(node.expr)\n            self.write(')')\n\n        keywords = []\n        keywords.extend(['TRIGGER'])\n        self._visit_CreateObject(\n            node, *keywords, after_name=after_name, unqualified=True)\n        # This is left hanging from after_name, so that subcommands\n        # get double indented\n        self.indentation -= 1\n\n    def visit_AlterTrigger(self, node: qlast.AlterTrigger) -> None:\n        self._visit_AlterObject(node, 'TRIGGER', unqualified=True)\n\n    def visit_DropTrigger(self, node: qlast.DropTrigger) -> None:\n        self._visit_DropObject(node, 'TRIGGER', unqualified=True)\n\n    def _format_rewrite_kinds(self, kinds: list[qltypes.RewriteKind]) -> str:\n        # Canonicalize the order, since the schema loses track\n        kinds = [k for k in list(qltypes.RewriteKind) if k in kinds]\n        skinds = ', '.join(str(kind).lower() for kind in kinds)\n        return skinds\n\n    def visit_CreateRewrite(self, node: qlast.CreateRewrite) -> None:\n        def an() -> None:\n            self._block_ws(1)\n            self._write_keywords(self._format_rewrite_kinds(node.kinds) + ' ')\n\n            self._block_ws(0)\n\n            self._write_keywords('USING ')\n            self.write('(')\n            self.visit(node.expr)\n            self.write(')')\n\n        keywords = []\n        keywords.extend(['REWRITE'])\n        self._visit_CreateObject(\n            node, *keywords, after_name=an, unqualified=True, named=False\n        )\n        # This is left hanging from after_name, so that subcommands\n        # get double indented\n        self.indentation -= 1\n\n    def visit_AlterRewrite(self, node: qlast.AlterRewrite) -> None:\n        def an() -> None:\n            self._block_ws(1)\n            self._write_keywords(self._format_rewrite_kinds(node.kinds) + ' ')\n\n        self._visit_AlterObject(\n            node, 'REWRITE', after_name=an, unqualified=True, named=False\n        )\n\n    def visit_DropRewrite(self, node: qlast.DropRewrite) -> None:\n        def an() -> None:\n            self._block_ws(1)\n            self._write_keywords(self._format_rewrite_kinds(node.kinds) + ' ')\n\n        self._visit_DropObject(\n            node, 'REWRITE', after_name=an, unqualified=True, named=False\n        )\n\n    def visit_CreateScalarType(self, node: qlast.CreateScalarType) -> None:\n        keywords = []\n        if node.abstract:\n            keywords.append('ABSTRACT')\n        keywords.append('SCALAR')\n        keywords.append('TYPE')\n\n        after_name = lambda: self._ddl_visit_bases(node)\n        self._visit_CreateObject(node, *keywords, after_name=after_name)\n\n    def visit_AlterScalarType(self, node: qlast.AlterScalarType) -> None:\n        self._visit_AlterObject(node, 'SCALAR TYPE')\n\n    def visit_DropScalarType(self, node: qlast.DropScalarType) -> None:\n        self._visit_DropObject(node, 'SCALAR TYPE')\n\n    def visit_CreatePseudoType(self, node: qlast.CreatePseudoType) -> None:\n        keywords = []\n        keywords.append('PSEUDO')\n        keywords.append('TYPE')\n        self._visit_CreateObject(node, *keywords)\n\n    def visit_CreateProperty(self, node: qlast.CreateProperty) -> None:\n        node = self._ddl_add_pointer_bases(node)\n        self._visit_CreateObject(node, 'ABSTRACT PROPERTY')\n\n    def visit_AlterProperty(self, node: qlast.AlterProperty) -> None:\n        self._visit_AlterObject(node, 'ABSTRACT PROPERTY')\n\n    def visit_DropProperty(self, node: qlast.DropProperty) -> None:\n        self._visit_DropObject(node, 'ABSTRACT PROPERTY')\n\n    def visit_CreateConcreteProperty(\n        self, node: qlast.CreateConcreteProperty\n    ) -> None:\n        self.visit_CreateConcretePointer(node, kind='PROPERTY')\n\n    def _process_AlterConcretePointer_for_SDL(\n        self,\n        node: qlast.AlterObject,\n    ) -> tuple[list[str], frozenset[qlast.DDLOperation]]:\n        keywords = []\n        specials = set()\n\n        for command in node.commands:\n            if isinstance(command, qlast.SetField) and command.special_syntax:\n                kw = self._process_special_set(command)\n                specials.add(command)\n                if kw[0] == 'SET':\n                    keywords.append(kw[1])\n\n        order = ['OPTIONAL', 'REQUIRED', 'SINGLE', 'MULTI']\n        keywords.sort(key=lambda i: order.index(i))\n\n        return keywords, frozenset(specials)\n\n    def visit_AlterConcreteProperty(\n        self, node: qlast.AlterConcreteProperty\n    ) -> None:\n        self.visit_AlterConcretePointer(node, kind='PROPERTY')\n\n    def visit_DropConcreteProperty(\n        self, node: qlast.DropConcreteProperty\n    ) -> None:\n        self._visit_DropObject(node, 'PROPERTY', unqualified=True)\n\n    def visit_CreateLink(self, node: qlast.CreateLink) -> None:\n        node = self._ddl_add_pointer_bases(node)\n        self._visit_CreateObject(node, 'ABSTRACT LINK')\n\n    def visit_AlterLink(self, node: qlast.AlterLink) -> None:\n        self._visit_AlterObject(node, 'ABSTRACT LINK')\n\n    def visit_DropLink(self, node: qlast.DropLink) -> None:\n        self._visit_DropObject(node, 'ABSTRACT LINK')\n\n    def visit_CreateConcretePointer(\n        self,\n        node: qlast.CreateConcretePointer,\n        kind: Optional[str],\n    ) -> None:\n        keywords = []\n\n        if self.sdlmode and node.declared_overloaded:\n            keywords.append('OVERLOADED')\n            if node.is_required:\n                keywords.append('REQUIRED')\n        else:\n            if node.is_required is True:\n                keywords.append(\"REQUIRED\")\n            elif node.is_required is False:\n                keywords.append(\"OPTIONAL\")\n            # else: node.is_required is None\n        if node.cardinality:\n            keywords.append(node.cardinality.as_ptr_qual().upper())\n        if kind:\n            keywords.append(kind)\n\n        def after_name() -> None:\n            if node.target is not None:\n                if isinstance(node.target, qlast.TypeExpr):\n                    self.write(': ')\n                    self.visit(node.target)\n                elif pure_computable:\n                    # computable\n                    self.write(' := (')\n                    self.visit(node.target)\n                    self.write(')')\n\n        node = self._ddl_add_pointer_bases(node)\n\n        pure_computable = (\n            len(node.commands) == 0\n            or (\n                len(node.commands) == 1\n                and isinstance(node.commands[0], qlast.SetField)\n                and node.commands[0].name == 'expr'\n                and not isinstance(node.target, qlast.TypeExpr)\n            )\n        )\n\n        self._visit_CreateObject(\n            node,\n            *keywords,\n            after_name=after_name,\n            unqualified=True,\n            render_commands=not pure_computable,\n        )\n\n    def visit_CreateConcreteUnknownPointer(\n        self, node: qlast.CreateConcreteLink\n    ) -> None:\n        self.visit_CreateConcretePointer(node, kind=None)\n\n    def visit_AlterConcreteUnknownPointer(\n        self, node: qlast.AlterConcreteLink\n    ) -> None:\n        self.visit_AlterConcretePointer(node, kind=None)\n\n    def visit_CreateConcreteLink(self, node: qlast.CreateConcreteLink) -> None:\n        self.visit_CreateConcretePointer(node, kind='LINK')\n\n    def visit_AlterConcretePointer(\n        self,\n        node: qlast.AlterObject,\n        kind: Optional[str],\n    ) -> None:\n        keywords = []\n        ignored_cmds: set[qlast.DDLOperation] = set()\n\n        after_name: Optional[Callable[[], None]]\n\n        if self.sdlmode:\n            if (not self.descmode\n                    or not node.system_comment\n                    or 'inherited from' not in node.system_comment):\n                keywords.append('OVERLOADED')\n            quals, ignored_cmds_r = self._process_AlterConcretePointer_for_SDL(\n                node)\n            keywords.extend(quals)\n            ignored_cmds.update(ignored_cmds_r)\n\n            type_cmd = None\n            inherit_cmd = None\n            for cmd in node.commands:\n                if isinstance(cmd, qlast.SetPointerType):\n                    ignored_cmds.add(cmd)\n                    type_cmd = cmd\n                elif isinstance(cmd, qlast.AlterAddInherit):\n                    ignored_cmds.add(cmd)\n                    inherit_cmd = cmd\n\n            def after_name() -> None:\n                if inherit_cmd:\n                    self._ddl_visit_bases(inherit_cmd)\n                if type_cmd is not None:\n                    self.write(' -> ')\n                    assert type_cmd.value\n                    self.visit(type_cmd.value)\n        else:\n            after_name = None\n\n        if kind:\n            keywords.append(kind)\n        self._visit_AlterObject(\n            node,\n            *keywords,\n            ignored_cmds=ignored_cmds,\n            allow_short=False,\n            unqualified=True,\n            after_name=after_name,\n        )\n\n    def visit_AlterConcreteLink(self, node: qlast.AlterConcreteLink) -> None:\n        self.visit_AlterConcretePointer(node, kind='LINK')\n\n    def visit_DropConcreteLink(self, node: qlast.DropConcreteLink) -> None:\n        self._visit_DropObject(node, 'LINK', unqualified=True)\n\n    def visit_SetPointerType(self, node: qlast.SetPointerType) -> None:\n        if node.value is None:\n            self._write_keywords('RESET TYPE')\n        else:\n            self._write_keywords('SET TYPE ')\n            self.visit(node.value)\n            if node.cast_expr is not None:\n                self._write_keywords(' USING (')\n                self.visit(node.cast_expr)\n                self.write(')')\n\n    def visit_SetPointerCardinality(\n        self,\n        node: qlast.SetPointerCardinality,\n    ) -> None:\n        if node.value is None:\n            self._write_keywords('RESET CARDINALITY')\n        else:\n            value = self._eval_enum_expr(node.value, qltypes.SchemaCardinality)\n            self._write_keywords('SET ')\n            self.write(value.to_edgeql())\n        if node.conv_expr is not None:\n            self._write_keywords(' USING (')\n            self.visit(node.conv_expr)\n            self.write(')')\n\n    def visit_SetPointerOptionality(\n        self,\n        node: qlast.SetPointerOptionality,\n    ) -> None:\n        if node.value is None:\n            self._write_keywords('RESET OPTIONALITY')\n        else:\n            if self._eval_bool_expr(node.value):\n                self._write_keywords('SET REQUIRED')\n            else:\n                self._write_keywords('SET OPTIONAL')\n            if node.fill_expr is not None:\n                self._write_keywords(' USING (')\n                self.visit(node.fill_expr)\n                self.write(')')\n\n    def visit_OnTargetDelete(self, node: qlast.OnTargetDelete) -> None:\n        if node.cascade is None:\n            self._write_keywords('RESET ON TARGET DELETE')\n        else:\n            self._write_keywords('ON TARGET DELETE', node.cascade.to_edgeql())\n\n    def visit_OnSourceDelete(self, node: qlast.OnSourceDelete) -> None:\n        if node.cascade is None:\n            self._write_keywords('RESET ON SOURCE DELETE')\n        else:\n            self._write_keywords('ON SOURCE DELETE', node.cascade.to_edgeql())\n\n    def visit_CreateObjectType(self, node: qlast.CreateObjectType) -> None:\n        keywords = []\n\n        if node.abstract:\n            keywords.append('ABSTRACT')\n        keywords.append('TYPE')\n\n        after_name = lambda: self._ddl_visit_bases(node)\n        self._visit_CreateObject(node, *keywords, after_name=after_name)\n\n    def visit_AlterObjectType(self, node: qlast.AlterObjectType) -> None:\n        self._visit_AlterObject(node, 'TYPE')\n\n    def visit_DropObjectType(self, node: qlast.DropObjectType) -> None:\n        self._visit_DropObject(node, 'TYPE')\n\n    def _after_index(self, node: qlast.ConcreteIndexCommand) -> None:\n        if node.kwargs:\n            self.write('(')\n            for i, (name, arg) in enumerate(node.kwargs.items()):\n                if i > 0:\n                    self.write(', ')\n                self.write(f'{edgeql_quote.quote_ident(name)} := ')\n                self.visit(arg)\n            self.write(')')\n\n        self._write_keywords(' ON ')\n        self.write('(')\n        self.visit(node.expr)\n        self.write(')')\n\n        if node.except_expr:\n            self._write_keywords(' EXCEPT ')\n            self.write('(')\n            self.visit(node.except_expr)\n            self.write(')')\n\n    def visit_IndexType(self, node: qlast.IndexType) -> None:\n        self.visit(node.name)\n\n        if node.kwargs:\n            self.write('(')\n            for i, (name, arg) in enumerate(node.kwargs.items()):\n                if i > 0:\n                    self.write(', ')\n                self.write(f'{edgeql_quote.quote_ident(name)} := ')\n                self.visit(arg)\n            self.write(')')\n\n    def visit_CreateIndex(self, node: qlast.CreateIndex) -> None:\n        def after_name() -> None:\n            if node.params:\n                self.write('(')\n                self.visit_list(node.params, newlines=False)\n                self.write(')')\n\n            if node.kwargs:\n                self.write('(')\n                for i, (name, arg) in enumerate(node.kwargs.items()):\n                    if i > 0:\n                        self.write(', ')\n                    self.write(f'{edgeql_quote.quote_ident(name)} := ')\n                    self.visit(arg)\n                self.write(')')\n\n            if node.index_types:\n                self._write_keywords(' USING ')\n                self.visit_list(node.index_types, newlines=False)\n\n            self._ddl_visit_bases(node)\n\n        self._visit_CreateObject(node, 'ABSTRACT INDEX', after_name=after_name)\n\n    def visit_AlterIndex(self, node: qlast.AlterIndex) -> None:\n        self._visit_AlterObject(node, 'ABSTRACT INDEX')\n\n    def visit_DropIndex(self, node: qlast.DropIndex) -> None:\n        self._visit_DropObject(node, 'ABSTRACT INDEX')\n\n    def visit_IndexCode(self, node: qlast.IndexCode) -> None:\n        self._write_keywords('USING', node.language)\n        self.write(edgeql_quote.dollar_quote_literal(node.code))\n\n    def visit_CreateConcreteIndex(\n        self, node: qlast.CreateConcreteIndex\n    ) -> None:\n        keywords = ['DEFERRED', 'INDEX'] if node.deferred else ['INDEX']\n        self._visit_CreateObject(\n            node,\n            *keywords,\n            named=node.name.name != 'idx',\n            after_name=lambda: self._after_index(node),\n        )\n\n    def visit_AlterConcreteIndex(self, node: qlast.AlterConcreteIndex) -> None:\n        self._visit_AlterObject(\n            node, 'INDEX', named=node.name.name != 'idx',\n            after_name=lambda: self._after_index(node))\n\n    def visit_DropConcreteIndex(self, node: qlast.DropConcreteIndex) -> None:\n        self._visit_DropObject(\n            node, 'INDEX', named=node.name.name != 'idx',\n            after_name=lambda: self._after_index(node))\n\n    def visit_CreateIndexMatch(self, node: qlast.CreateIndexMatch) -> None:\n        def after_name() -> None:\n            self.visit(node.valid_type)\n            self._write_keywords(' using ')\n            self.visit(node.name)\n\n        self._visit_CreateObject(\n            node, 'index match', 'for',\n            named=False, after_name=after_name,\n        )\n\n    def visit_DropIndexMatch(self, node: qlast.DropIndexMatch) -> None:\n        def after_name() -> None:\n            self.visit(node.valid_type)\n            self._write_keywords(' using ')\n            self.visit(node.name)\n        self._visit_DropObject(\n            node, 'index match', 'for',\n            named=False, after_name=after_name,\n        )\n\n    def visit_CreateOperator(self, node: qlast.CreateOperator) -> None:\n        def after_name() -> None:\n            self.write('(')\n            self.visit_list(node.params, newlines=False)\n            self.write(')')\n            self.write(' -> ')\n            self.write(node.returning_typemod.to_edgeql(), ' ')\n            self.visit(node.returning)\n\n            if node.abstract:\n                return\n\n            if node.commands:\n                self.write(' {')\n                self._block_ws(1)\n                commands = self._ddl_clean_up_commands(node.commands)\n                self.visit_list(commands, terminator=';')\n                self.new_lines = 1\n            else:\n                self.write(' ')\n\n            if node.code.from_operator:\n                from_clause = f'USING {node.code.language} OPERATOR '\n                self._write_keywords(from_clause)\n                op, *types = node.code.from_operator\n                op_str = op\n                if types:\n                    op_str += f'({\",\".join(types)})'\n                self.write(f'{op_str!r}', ';')\n            if node.code.from_function:\n                from_clause = f'USING {node.code.language} OPERATOR '\n                self._write_keywords(from_clause)\n                op, *types = node.code.from_function\n                op_str = op\n                if types:\n                    op_str += f'({\",\".join(types)})'\n                self.write(f'{op_str!r}', ';')\n            if node.code.from_expr:\n                from_clause = f'USING {node.code.language} EXPRESSION'\n                self._write_keywords(from_clause, ';')\n            elif node.code.code:\n                from_clause = f'USING {node.code.language} '\n                self._write_keywords(from_clause)\n                self.write(\n                    edgeql_quote.dollar_quote_literal(\n                        node.code.code),\n                    ';'\n                )\n\n            self._block_ws(-1)\n            if node.commands:\n                self.write('}')\n\n        op_type = []\n        if node.abstract:\n            op_type.append('ABSTRACT')\n        if node.kind:\n            op_type.append(node.kind.upper())\n        op_type.append('OPERATOR')\n\n        self._visit_CreateObject(node, *op_type, after_name=after_name,\n                                 render_commands=False)\n\n    def visit_AlterOperator(self, node: qlast.AlterOperator) -> None:\n        def after_name() -> None:\n            self.write('(')\n            self.visit_list(node.params, newlines=False)\n            self.write(')')\n\n        op_type = []\n        if node.kind:\n            op_type.append(node.kind.upper())\n        op_type.append('OPERATOR')\n        self._visit_AlterObject(node, *op_type, after_name=after_name)\n\n    def visit_DropOperator(self, node: qlast.DropOperator) -> None:\n        def after_name() -> None:\n            self.write('(')\n            self.visit_list(node.params, newlines=False)\n            self.write(')')\n\n        op_type = []\n        if node.kind:\n            op_type.append(node.kind.upper())\n        op_type.append('OPERATOR')\n        self._visit_DropObject(node, *op_type, after_name=after_name)\n\n    def _function_after_name(\n        self, node: qlast.CreateFunction | qlast.AlterFunction\n    ) -> None:\n        self.write('(')\n        self.visit_list(node.params, newlines=False)\n        self.write(')')\n        if isinstance(node, qlast.CreateFunction):\n            self.write(' -> ')\n            self._write_keywords(node.returning_typemod.to_edgeql(), '')\n            self.visit(node.returning)\n\n        if node.commands:\n            self.write(' {')\n            self._block_ws(1)\n            commands = self._ddl_clean_up_commands(node.commands)\n            self.visit_list(commands, terminator=';')\n            self.new_lines = 1\n        else:\n            self.write(' ')\n\n        had_using = True\n        if node.code.from_function:\n            from_clause = f'USING {node.code.language} FUNCTION '\n            self._write_keywords(from_clause)\n            self.write(f'{node.code.from_function!r}')\n        elif node.code.language is qlast.Language.EdgeQL:\n            if node.nativecode:\n                self._write_keywords('USING')\n                self.write(' (')\n                self.visit(node.nativecode)\n                self.write(')')\n            elif node.code.code:\n                self._write_keywords('USING')\n                self.write(f' ({node.code.code})')\n            else:\n                had_using = False\n        else:\n            if node.code.from_expr:\n                from_clause = f'USING {node.code.language} EXPRESSION'\n                self._write_keywords(from_clause)\n            elif node.code.code:\n                from_clause = f'USING {node.code.language} '\n                self._write_keywords(from_clause)\n                self.write(\n                    edgeql_quote.dollar_quote_literal(\n                        node.code.code))\n            else:\n                from_clause = f'USING {node.code.language} '\n                self._write_keywords(from_clause)\n\n        if node.commands:\n            self._block_ws(-1)\n            if had_using:\n                self.write(';')\n            self.write('}')\n\n    def visit_CreateFunction(self, node: qlast.CreateFunction) -> None:\n        self._visit_CreateObject(\n            node, 'FUNCTION',\n            after_name=lambda: self._function_after_name(node),\n            render_commands=False)\n\n    def visit_AlterFunction(self, node: qlast.AlterFunction) -> None:\n        def after_name() -> None:\n            self.write('(')\n            self.visit_list(node.params, newlines=False)\n            self.write(')')\n\n        self._visit_AlterObject(\n            node, 'FUNCTION',\n            after_name=lambda: self._function_after_name(node),\n            ignored_cmds=set(node.commands))\n\n    def visit_DropFunction(self, node: qlast.DropFunction) -> None:\n        def after_name() -> None:\n            self.write('(')\n            self.visit_list(node.params, newlines=False)\n            self.write(')')\n        self._visit_DropObject(node, 'FUNCTION', after_name=after_name)\n\n    def visit_FuncParamDecl(self, node: qlast.FuncParamDecl) -> None:\n        kind = node.kind.to_edgeql()\n        if kind:\n            self._write_keywords(kind, '')\n\n        if node.name is not None:\n            self.write(ident_to_str(node.name), ': ')\n\n        typemod = node.typemod.to_edgeql()\n        if typemod:\n            self._write_keywords(typemod, '')\n\n        self.visit(node.type)\n\n        if node.default:\n            self.write(' = ')\n            self.visit(node.default)\n\n    def visit_CreateCast(self, node: qlast.CreateCast) -> None:\n        def after_name() -> None:\n            self.write(' ')\n            self.visit(node.from_type)\n            self._write_keywords(' to ')\n            self.visit(node.to_type)\n\n            self.write(' {')\n            self._block_ws(1)\n\n            if node.commands:\n                commands = self._ddl_clean_up_commands(node.commands)\n                self.visit_list(commands, terminator=';')\n                self.new_lines = 1\n\n            from_clause = f'USING {node.code.language} '\n            code = ''\n\n            if node.code.from_function:\n                from_clause += 'FUNCTION'\n                code = f'{node.code.from_function!r}'\n            elif node.code.from_cast:\n                from_clause += 'CAST'\n            elif node.code.from_expr:\n                from_clause += 'EXPRESSION'\n            elif node.code.code:\n                code = edgeql_quote.dollar_quote_literal(node.code.code)\n\n            self._write_keywords(from_clause)\n            if code:\n                self.write(' ', code)\n            self.write(';')\n            self.new_lines = 1\n\n            if node.allow_assignment:\n                self._write_keywords('ALLOW ASSIGNMENT;')\n                self.new_lines = 1\n            if node.allow_implicit:\n                self._write_keywords('ALLOW IMPLICIT;')\n                self.new_lines = 1\n\n            self._block_ws(-1)\n            self.write('}')\n\n        self._visit_CreateObject(\n            node, 'CAST', 'FROM',\n            named=False, after_name=after_name, render_commands=False\n        )\n\n    def visit_AlterCast(self, node: qlast.AlterCast) -> None:\n        def after_name() -> None:\n            self._write_keywords('FROM ')\n            self.visit(node.from_type)\n            self._write_keywords(' TO ')\n            self.visit(node.to_type)\n        self._visit_AlterObject(\n            node,\n            'CAST',\n            named=False,\n            after_name=after_name,\n        )\n\n    def visit_DropCast(self, node: qlast.DropCast) -> None:\n        def after_name() -> None:\n            self._write_keywords('FROM ')\n            self.visit(node.from_type)\n            self._write_keywords(' TO ')\n            self.visit(node.to_type)\n        self._visit_DropObject(\n            node,\n            'CAST',\n            named=False,\n            after_name=after_name,\n        )\n\n    def visit_SetGlobalType(self, node: qlast.SetGlobalType) -> None:\n        if node.value is None:\n            self._write_keywords('RESET TYPE')\n        else:\n            self._write_keywords('SET TYPE ')\n            self.visit(node.value)\n            if node.cast_expr is not None:\n                self._write_keywords(' USING (')\n                self.visit(node.cast_expr)\n                self.write(')')\n            elif node.reset_value:\n                self._write_keywords(' RESET TO DEFAULT')\n\n    def visit_CreateGlobal(self, node: qlast.CreateGlobal) -> None:\n        keywords = []\n        if node.is_required is True:\n            keywords.append('REQUIRED')\n        elif node.is_required is False:\n            keywords.append('OPTIONAL')\n        if node.cardinality:\n            keywords.append(node.cardinality.as_ptr_qual().upper())\n        keywords.append('GLOBAL')\n\n        pure_computable = (\n            len(node.commands) == 0\n            or (\n                len(node.commands) == 1\n                and isinstance(node.commands[0], qlast.SetField)\n                and node.commands[0].name == 'expr'\n                and not isinstance(node.target, qlast.TypeExpr)\n            )\n        )\n\n        def after_name() -> None:\n            if node.target is not None:\n                if isinstance(node.target, qlast.TypeExpr):\n                    self.write(' -> ')\n                    self.visit(node.target)\n                elif pure_computable:\n                    # computable\n                    self.write(' := (')\n                    self.visit(node.target)\n                    self.write(')')\n\n        self._visit_CreateObject(\n            node, *keywords, after_name=after_name,\n            render_commands=not pure_computable)\n\n    def visit_AlterGlobal(self, node: qlast.AlterGlobal) -> None:\n        self._visit_AlterObject(node, 'GLOBAL')\n\n    def visit_DropGlobal(self, node: qlast.DropGlobal) -> None:\n        self._visit_DropObject(node, 'GLOBAL')\n\n    def visit_CreatePermission(self, node: qlast.CreatePermission) -> None:\n        self._visit_CreateObject(node, 'PERMISSION')\n\n    def visit_AlterPermission(self, node: qlast.AlterPermission) -> None:\n        self._visit_AlterObject(node, 'PERMISSION')\n\n    def visit_DropPermission(self, node: qlast.DropPermission) -> None:\n        self._visit_DropObject(node, 'PERMISSION')\n\n    def visit_ConfigSet(self, node: qlast.ConfigSet) -> None:\n        if node.scope == qltypes.ConfigScope.GLOBAL:\n            self._write_keywords('SET GLOBAL ')\n        else:\n            self._write_keywords('CONFIGURE ')\n            self.write(node.scope.to_edgeql())\n            self._write_keywords(' SET ')\n        self.visit(node.name)\n        self.write(' := ')\n        self.visit(node.expr)\n\n    def visit_ConfigInsert(self, node: qlast.ConfigInsert) -> None:\n        self._write_keywords('CONFIGURE ')\n        self.write(node.scope.to_edgeql())\n        self._write_keywords(' INSERT ')\n        self.visit(node.name)\n        self.indentation += 1\n        self._visit_shape(node.shape)\n        self.indentation -= 1\n\n    def visit_ConfigReset(self, node: qlast.ConfigReset) -> None:\n        if node.scope == qltypes.ConfigScope.GLOBAL:\n            self._write_keywords('RESET GLOBAL ')\n        else:\n            self._write_keywords('CONFIGURE ')\n            self.write(node.scope.to_edgeql())\n            self._write_keywords(' RESET ')\n        self.visit(node.name)\n        self._visit_filter(node)\n\n    def visit_SessionSetAliasDecl(\n        self, node: qlast.SessionSetAliasDecl\n    ) -> None:\n        self._write_keywords('SET ')\n        if node.decl.alias:\n            self._write_keywords('ALIAS ')\n        self.visit_ModuleAliasDecl(node.decl)\n\n    def visit_SessionResetAllAliases(\n        self, node: qlast.SessionResetAllAliases\n    ) -> None:\n        self._write_keywords('RESET ALIAS *')\n\n    def visit_SessionResetModule(self, node: qlast.SessionResetModule) -> None:\n        self._write_keywords('RESET MODULE')\n\n    def visit_SessionResetAliasDecl(\n        self, node: qlast.SessionResetAliasDecl\n    ) -> None:\n        self._write_keywords('RESET ALIAS ')\n        self.write(node.alias)\n\n    def visit_StartTransaction(self, node: qlast.StartTransaction) -> None:\n        self._write_keywords('START TRANSACTION')\n\n        mods = []\n\n        if node.isolation is not None:\n            mods.append(f'ISOLATION {node.isolation.value}')\n\n        if node.access is not None:\n            mods.append(node.access.value)\n\n        if node.deferrable is not None:\n            mods.append(node.deferrable.value)\n\n        if mods:\n            self._write_keywords(' ' + ', '.join(mods))\n\n    def visit_RollbackTransaction(\n        self, node: qlast.RollbackTransaction\n    ) -> None:\n        self._write_keywords('ROLLBACK')\n\n    def visit_CommitTransaction(self, node: qlast.CommitTransaction) -> None:\n        self._write_keywords('COMMIT')\n\n    def visit_DeclareSavepoint(self, node: qlast.DeclareSavepoint) -> None:\n        self._write_keywords('DECLARE SAVEPOINT ')\n        self.write(node.name)\n\n    def visit_RollbackToSavepoint(\n        self, node: qlast.RollbackToSavepoint\n    ) -> None:\n        self._write_keywords('ROLLBACK TO SAVEPOINT ')\n        self.write(node.name)\n\n    def visit_ReleaseSavepoint(self, node: qlast.ReleaseSavepoint) -> None:\n        self._write_keywords('RELEASE SAVEPOINT ')\n        self.write(node.name)\n\n    def visit_DescribeStmt(self, node: qlast.DescribeStmt) -> None:\n        self._write_keywords('DESCRIBE ')\n        if isinstance(node.object, qlast.DescribeGlobal):\n            self.write(node.object.to_edgeql())\n        else:\n            self.visit(node.object)\n        if node.language:\n            self._write_keywords(' AS ')\n            self.write(node.language)\n        if node.options:\n            self.write(' ')\n            self.visit(node.options)\n\n    def visit_Options(self, node: qlast.Options) -> None:\n        first = True\n        for opt in node.options.values():\n            if isinstance(opt, qlast.OptionFlag) and not opt.val:\n                continue\n            if first:\n                self.write(' ')\n            first = False\n\n            self.write(opt.name)\n\n    # SDL nodes\n    def copy_generator(self) -> EdgeQLSourceGenerator:\n        return self.__class__(\n            indent_with=self.indent_with,\n            add_line_information=self.add_line_information,\n            pretty=self.pretty,\n            unsorted=self.unsorted,\n            sdlmode=True,\n            descmode=self.descmode,\n            limit_ref_classes=self.limit_ref_classes\n        )\n\n    def generate_isolated_text(self, node: qlast.Base) -> str:\n        sdl_codegen = self.copy_generator()\n        sdl_codegen.visit(node)\n        return ''.join(sdl_codegen.result)\n\n    def visit_Schema(self, node: qlast.Schema) -> None:\n        sdl_codegen = self.copy_generator()\n        sdl_codegen.indentation = self.indentation\n        sdl_codegen.current_line = self.current_line\n        sdl_codegen.visit_list(node.declarations, terminator=';')\n        self.result.extend(sdl_codegen.result)\n\n    def visit_ModuleDeclaration(self, node: qlast.ModuleDeclaration) -> None:\n        self._write_keywords('module ')\n        # the name is always unqualified here\n        self.write(ident_to_str(node.name.name))\n        self.write('{')\n        self._block_ws(1)\n        self.visit_list(node.declarations, terminator=';')\n        self._block_ws(-1)\n        self.write('}')\n\n    @classmethod\n    def to_source(  # type: ignore\n        cls,\n        node: qlast.Base | Sequence[qlast.Base],\n        indent_with: str = ' ' * 4,\n        add_line_information: bool = False,\n        pretty: bool = True,\n        sdlmode: bool = False,\n        descmode: bool = False,\n        # Uppercase keywords for backwards compatibility with older migrations.\n        uppercase: bool = False,\n        limit_ref_classes:\n            Optional[AbstractSet[qltypes.SchemaObjectClass]] = None,\n        unsorted: bool = False,\n    ) -> str:\n        if isinstance(node, (list, tuple)):\n            for n in node:\n                _fix_parent_links(n)\n        else:\n            assert isinstance(node, qlast.Base)\n            _fix_parent_links(node)\n\n        return super().to_source(\n            node,\n            indent_with,\n            add_line_information,\n            pretty,\n            sdlmode=sdlmode,\n            descmode=descmode,\n            uppercase=uppercase,\n            limit_ref_classes=limit_ref_classes,\n            unsorted=unsorted,\n        )\n\n\ndef _fix_parent_links(node: qlast.Base) -> qlast.Base:\n    # NOTE: Do not use this legacy function in new code!\n    # Using AST.parent is an anti-pattern. Instead write code\n    # that uses singledispatch and maintains a proper context.\n\n    node._parent = None  # type: ignore\n\n    for _field, value in base.iter_fields(node):\n        if isinstance(value, dict):\n            for n in value.values():\n                if base.is_ast_node(n):\n                    _fix_parent_links(n)\n                    n._parent = node\n\n        elif typeutils.is_container(value):\n            for n in value:\n                if base.is_ast_node(n):\n                    _fix_parent_links(n)\n                    n._parent = node\n\n        elif base.is_ast_node(value):\n            _fix_parent_links(value)\n            value._parent = node\n\n    return node\n\n\ngenerate_source = EdgeQLSourceGenerator.to_source\n"
  },
  {
    "path": "edb/edgeql/compiler/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL to IR compiler.\n\nThe purpose of this compilation phase is to produce a canonical, self-contained\nrepresentation of an EdgeQL expression, aka the IR.  The validity of the\nexpression and other schema-level checks and resolutions happen at this stage.\nOnce the IR generation is successful, the expression is considered valid.\n\nThe finalized IR consists of two tree structures: the expression tree and\nthe scope tree.\n\nThe *expression tree* is, essentially, another AST form that generally\nresembles the overall shape of the original EdgeQL AST annotated with type\ninformation and other metadata that is necessary to compile the IR into the\nbackend query language.  The *scope tree* tracks the visibility of variables\nand determines how the aggregation functions are arranged in the expression.\n\nEvery EdgeQL expression is essentially a giant functional map-reduce\nconstruct, or, Pythonically, a bunch of nested set comprehensions.\nIn those terms, the expression tree encodes expressions inside comprehensions,\nand the scope tree determines how the comprehensions are nested, and at which\ncomprehension level the variables are defined.\n\nThe :mod:`ir.ast` and the :mod:`ir.scopetree` modules have more comments on\nthe organization of the IR expression and scope trees, correspondingly.\n\nOperation\n---------\n\nThe compiler has several entry points, are all in this file.  Each entry\npoint sets the compilation context and then calls the generic compilation\ndispatch.  The compilation process is a straightforward EdgeQL AST traversal,\nwhere most AST nodes have a dedicated handler function, and the routing is\ndone by singledispatch based on the AST node type.\n\nContext\n-------\n\nThe compilation context object is passed to the vast majority of the compiler\nfunctions and contains the information necessary to correctly process an AST\nnode in a given situation.  It is organized as a stack that resembles a\nChainMap, albeit the elements are objects instead of dicts, and the chaining\nlogic is controlled by the context itself.  See context.py for details.\n\nOrganization\n------------\n\nThe compiler code is organized into the following modules (in rough order\nof control flow):\n\n__init__.py\n    This file, contains compiler entry points that initialize\n    the compilation context and call into compilation dispatch.\n\nstmt.py\n    Handlers for statement expressions, like ``SELECT``, ``INSERT``.\n\nexpr.py\n    Handlers for the majority of expressions that aren't statements or\n    that are handled elsewhere.\n\nfunc.py\n    Handlers for function calls and operator expressions.\n\ncasts.py\n    Handlers for type cast expressions.\n\nclauses.py\n    Handlers for common statement clauses like ``FILTER`` and ``ORDER BY``.\n\npolyres.py\n    Logic for function, operator, and cast lookup via multiple dispatch\n    and generic type specialization.\n\nconfig.py\n    Handlers for ``CONFIGURE`` commands.\n\nsetgen.py\n    Functions to generate ``ir.ast.Set`` nodes and process path expressions.\n\nviewgen.py\n    Functions that process shape expressions into view types.\n\ntypegen.py\n    Helpers for type expressions.\n\ncontext.py\n    Compilation context definition.\n\nstmtctx.py\n    Functions to set up the overall compilation context as well as finalize\n    the result IR.\n\npathctx.py\n    PathId and scope helpers.\n\nschemactx.py\n    Helpers that interface with the schema, such as object lookup and\n    derivation.\n\nastutils.py\n    Various helpers for EdgeQL AST analysis.\n\ndispatch.py\n    Compiler singledispatch decorator (separate module for ease of import).\n\n\"\"\"\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n    AbstractSet,\n    Mapping,\n    Sequence,\n    cast,\n    overload,\n    TYPE_CHECKING,\n)\n\n# WARNING: this package is in a tight import loop with various modules\n# in edb.schema, so no direct imports from either this package or\n# edb.schema are allowed at the top-level.  If absolutely necessary,\n# use the lazy-loading mechanism.\n\nimport functools\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import codegen as qlcodegen\nfrom edb.edgeql import qltypes\nfrom edb.edgeql import parser as qlparser\n\nfrom edb.common import debug\n\nfrom .options import CompilerOptions as CompilerOptions  # \"as\" for reexport\n\nif TYPE_CHECKING:\n    from edb.schema import schema as s_schema\n\n    from edb.ir import ast as irast\n    from edb.ir import staeval as ireval\n\n    from . import dispatch as dispatch_mod\n    from . import inference as inference_mod\n    from . import normalization as norm_mod\n    from . import stmtctx as stmtctx_mod\nelse:\n    # Modules will be loaded lazily in _load().\n    dispatch_mod = None\n    inference_mod = None\n    irast = None\n    ireval = None\n    norm_mod = None\n    stmtctx_mod = None\n\n\n#: Compiler modules lazy-load guard.\n_LOADED = False\n\n\ndef compiler_entrypoint[Tf: Callable[..., Any]](func: Tf) -> Tf:\n    @functools.wraps(func)\n    def wrapper(*args: Any, **kwargs: Any) -> Any:\n        if not _LOADED:\n            _load()\n        return func(*args, **kwargs)\n\n    return cast(Tf, wrapper)\n\n\n@overload\ndef compile_ast_to_ir(\n    tree: qlast.Expr | qlast.Command,\n    schema: s_schema.Schema,\n    *,\n    script_info: Optional[irast.ScriptInfo] = None,\n    options: Optional[CompilerOptions] = None,\n) -> irast.Statement:\n    pass\n\n\n@overload\ndef compile_ast_to_ir(\n    tree: qlast.ConfigOp,\n    schema: s_schema.Schema,\n    *,\n    script_info: Optional[irast.ScriptInfo] = None,\n    options: Optional[CompilerOptions] = None,\n) -> irast.ConfigCommand:\n    pass\n\n\n@overload\ndef compile_ast_to_ir(\n    tree: qlast.Base,\n    schema: s_schema.Schema,\n    *,\n    script_info: Optional[irast.ScriptInfo] = None,\n    options: Optional[CompilerOptions] = None,\n) -> irast.Statement | irast.ConfigCommand:\n    pass\n\n\n@compiler_entrypoint\ndef compile_ast_to_ir(\n    tree: qlast.Base,\n    schema: s_schema.Schema,\n    *,\n    script_info: Optional[irast.ScriptInfo] = None,\n    options: Optional[CompilerOptions] = None,\n) -> irast.Statement | irast.ConfigCommand:\n    \"\"\"Compile given EdgeQL AST into Gel IR.\n\n    This is the normal compiler entry point.  It assumes that *tree*\n    represents a complete statement.\n\n    Args:\n        tree:\n            EdgeQL AST.\n\n        schema:\n            Schema instance.  Must contain definitions for objects\n            referenced by the AST *tree*.\n\n        options:\n            An optional :class:`edgeql.compiler.options.CompilerOptions`\n            instance specifying compilation options.\n\n        allow_writing_protected_ptrs:\n            If ``True``, allows protected object properties or links to\n            be overwritten in `INSERT` shapes.\n\n    Returns:\n        An instance of :class:`ir.ast.Command`.  Most frequently, this\n        would be an instance of :class:`ir.ast.Statement`.\n    \"\"\"\n    if options is None:\n        options = CompilerOptions()\n\n    if debug.flags.edgeql_compile or debug.flags.edgeql_compile_edgeql_text:\n        debug.header('EdgeQL Text')\n        debug.dump_code(qlcodegen.generate_source(tree, pretty=True))\n\n    if debug.flags.edgeql_compile or debug.flags.edgeql_compile_edgeql_ast:\n        debug.header('Compiler Options')\n        debug.dump(options.__dict__)\n        debug.header('EdgeQL AST')\n        debug.dump(tree, schema=schema)\n\n    ctx = stmtctx_mod.init_context(schema=schema, options=options)\n\n    if isinstance(tree, qlast.Expr) and ctx.implicit_limit:\n        tree = qlast.SelectQuery(result=tree, implicit=True)\n        tree.limit = qlast.Constant.integer(ctx.implicit_limit)\n\n    if not script_info:\n        script_info = stmtctx_mod.preprocess_script([tree], ctx=ctx)\n\n    ctx.env.script_params = script_info.params\n\n    ir_set = dispatch_mod.compile(tree, ctx=ctx)\n    ir_expr = stmtctx_mod.fini_expression(ir_set, ctx=ctx)\n\n    if debug.flags.edgeql_compile or debug.flags.edgeql_compile_scope:\n        debug.header('Scope Tree')\n        print(ctx.path_scope.pdebugformat())\n\n        # Also build and dump a mapping from scope ids to\n        # paths that appear directly at them.\n        scopes: dict[int, set[irast.PathId]] = {\n            k: set() for k in\n            sorted(node.unique_id\n                   for node in ctx.path_scope.descendants\n                   if node.unique_id)\n        }\n        for ir_set in ctx.env.set_types:\n            if ir_set.path_scope_id and ir_set.path_scope_id in scopes:\n                scopes[ir_set.path_scope_id].add(ir_set.path_id)\n        debug.dump(scopes)\n\n    if debug.flags.edgeql_compile or debug.flags.edgeql_compile_ir:\n        debug.header('Gel IR')\n        debug.dump(ir_expr, schema=getattr(ir_expr, 'schema', None))\n\n    return ir_expr\n\n\n@compiler_entrypoint\ndef compile_ast_fragment_to_ir(\n    tree: qlast.Base,\n    schema: s_schema.Schema,\n    *,\n    options: Optional[CompilerOptions] = None,\n) -> irast.Statement:\n    \"\"\"Compile given EdgeQL AST fragment into Gel IR.\n\n    Unlike :func:`~compile_ast_to_ir` above, this does not assume\n    that the AST *tree* is a complete statement.  The expression\n    doesn't even have to resolve to a specific type.\n\n    Args:\n        tree:\n            EdgeQL AST fragment.\n\n        schema:\n            Schema instance.  Must contain definitions for objects\n            referenced by the AST *tree*.\n\n        options:\n            An optional :class:`edgeql.compiler.options.CompilerOptions`\n            instance specifying compilation options.\n\n    Returns:\n        An instance of :class:`ir.ast.Statement`.\n    \"\"\"\n    if options is None:\n        options = CompilerOptions()\n\n    ctx = stmtctx_mod.init_context(schema=schema, options=options)\n    ir_set = dispatch_mod.compile(tree, ctx=ctx)\n\n    result_type = ctx.env.set_types[ir_set]\n\n    return irast.Statement(\n        expr=ir_set,\n        schema=ctx.env.schema,\n        stype=result_type,\n        dml_exprs=ctx.env.dml_exprs,\n        views={},\n        params=[],\n        globals=[],\n        required_permissions=set(),\n        server_param_conversions=[],\n        server_param_conversion_params=[],\n        # These values are nonsensical, but ideally the caller does not care\n        cardinality=qltypes.Cardinality.UNKNOWN,\n        multiplicity=qltypes.Multiplicity.EMPTY,\n        volatility=qltypes.Volatility.Volatile,\n        view_shapes={},\n        view_shapes_metadata={},\n        schema_refs=frozenset(),\n        schema_ref_exprs=None,\n        scope_tree=ctx.path_scope,\n        type_rewrites={},\n        singletons=[],\n        triggers=(),\n        warnings=tuple(ctx.env.warnings),\n        unsafe_isolation_dangers=tuple(ctx.env.unsafe_isolation_dangers),\n    )\n\n\n@compiler_entrypoint\ndef preprocess_script(\n    stmts: Sequence[qlast.Base],\n    schema: s_schema.Schema,\n    *,\n    options: CompilerOptions,\n) -> irast.ScriptInfo:\n    ctx = stmtctx_mod.init_context(schema=schema, options=options)\n    return stmtctx_mod.preprocess_script(stmts, ctx=ctx)\n\n\ndef evaluate_to_python_val(\n    expr: str,\n    schema: s_schema.Schema,\n    *,\n    modaliases: Optional[Mapping[Optional[str], str]] = None,\n) -> Any:\n    \"\"\"Evaluate the given EdgeQL string as a constant expression.\n\n    Args:\n        expr:\n            EdgeQL expression as a string.\n\n        schema:\n            Schema instance.  Must contain definitions for objects\n            referenced by *expr*.\n\n        modaliases:\n            Module name resolution table.  Useful when this EdgeQL\n            expression is part of some other construct, such as a\n            DDL statement.\n\n    Returns:\n        The result of the evaluation as a Python value.\n\n    Raises:\n        If the expression is not constant, or is otherwise not supported by\n        the const evaluator, the function will raise\n        :exc:`ir.staeval.UnsupportedExpressionError`.\n    \"\"\"\n    tree = qlparser.parse_fragment(expr)\n    return evaluate_ast_to_python_val(tree, schema, modaliases=modaliases)\n\n\ndef evaluate_ir_statement_to_python_val(\n    ir: irast.Statement,\n) -> Any:\n    \"\"\"Evaluate the given EdgeQL IR AST as a constant expression.\n\n    Args:\n        ir:\n            EdgeQL IR Statement AST.\n\n    Returns:\n        The result of the evaluation as a Python value and the associated IR.\n\n    Raises:\n        If the expression is not constant, or is otherwise not supported by\n        the const evaluator, the function will raise\n        :exc:`ir.staeval.UnsupportedExpressionError`.\n    \"\"\"\n    return ireval.evaluate_to_python_val(ir.expr, schema=ir.schema)\n\n\ndef evaluate_ast_to_python_val_and_ir(\n    tree: qlast.Base,\n    schema: s_schema.Schema,\n    *,\n    modaliases: Optional[Mapping[Optional[str], str]] = None,\n) -> tuple[Any, irast.Statement]:\n    \"\"\"Evaluate the given EdgeQL AST as a constant expression.\n\n    Args:\n        tree:\n            EdgeQL AST.\n\n        schema:\n            Schema instance.  Must contain definitions for objects\n            referenced by AST *tree*.\n\n        modaliases:\n            Module name resolution table.  Useful when this EdgeQL\n            expression is part of some other construct, such as a\n            DDL statement.\n\n    Returns:\n        The result of the evaluation as a Python value and the associated IR.\n\n    Raises:\n        If the expression is not constant, or is otherwise not supported by\n        the const evaluator, the function will raise\n        :exc:`ir.staeval.UnsupportedExpressionError`.\n    \"\"\"\n    if modaliases is None:\n        modaliases = {}\n    ir = compile_ast_fragment_to_ir(\n        tree,\n        schema,\n        options=CompilerOptions(\n            modaliases=modaliases,\n        ),\n    )\n    return ireval.evaluate_to_python_val(ir.expr, schema=ir.schema), ir\n\n\ndef evaluate_ast_to_python_val(\n    tree: qlast.Base,\n    schema: s_schema.Schema,\n    *,\n    modaliases: Optional[Mapping[Optional[str], str]] = None,\n) -> Any:\n    \"\"\"Evaluate the given EdgeQL AST as a constant expression.\n\n    Args:\n        tree:\n            EdgeQL AST.\n\n        schema:\n            Schema instance.  Must contain definitions for objects\n            referenced by AST *tree*.\n\n        modaliases:\n            Module name resolution table.  Useful when this EdgeQL\n            expression is part of some other construct, such as a\n            DDL statement.\n\n    Returns:\n        The result of the evaluation as a Python value.\n\n    Raises:\n        If the expression is not constant, or is otherwise not supported by\n        the const evaluator, the function will raise\n        :exc:`ir.staeval.UnsupportedExpressionError`.\n    \"\"\"\n    return evaluate_ast_to_python_val_and_ir(\n        tree, schema, modaliases=modaliases\n    )[0]\n\n\n@compiler_entrypoint\ndef compile_constant_tree_to_ir(\n    const: qlast.BaseConstant,\n    schema: s_schema.Schema,\n    *,\n    styperef: Optional[irast.TypeRef] = None,\n) -> irast.Expr:\n    \"\"\"Compile an EdgeQL constant into an IR ConstExpr.\n\n    Args:\n        const:\n            An EdgeQL AST representing a constant.\n\n        schema:\n            A schema instance.  Must contain the definition of the\n            constant type.\n\n        styperef:\n            Optionally overrides an IR type descriptor for the returned\n            ConstExpr.  If not specified, the inferred type of the constant\n            is used.\n\n    Returns:\n        An instance of :class:`ir.ast.ConstExpr` representing the\n        constant.\n    \"\"\"\n    ctx = stmtctx_mod.init_context(schema=schema, options=CompilerOptions())\n    if not isinstance(const, qlast.BaseConstant):\n        raise ValueError(f'unexpected input: {const!r} is not a constant')\n\n    ir_set = dispatch_mod.compile(const, ctx=ctx)\n    assert isinstance(ir_set, irast.Set)\n    result = ir_set.expr\n    assert isinstance(result, irast.BaseConstant)\n    if styperef is not None and result.typeref.id != styperef.id:\n        result = type(result)(value=result.value, typeref=styperef)\n\n    return result\n\n\n@compiler_entrypoint\ndef normalize(\n    tree: qlast.Base,\n    *,\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    localnames: AbstractSet[str] = frozenset(),\n) -> None:\n    \"\"\"Normalize the given AST *tree* by explicitly qualifying identifiers.\n\n    This helper takes an arbitrary EdgeQL AST tree together with the current\n    module alias mapping and produces an equivalent expression, in which\n    all identifiers representing schema object references are properly\n    qualified with the module name.\n\n    NOTE: the tree is mutated *in-place*.\n    \"\"\"\n    return norm_mod.normalize(\n        tree,\n        schema=schema,\n        modaliases=modaliases,\n        localnames=localnames,\n    )\n\n\n@compiler_entrypoint\ndef renormalize_compat(\n    tree: qlast.Base_T,\n    orig_text: str,\n    *,\n    schema: s_schema.Schema,\n    localnames: AbstractSet[str] = frozenset(),\n) -> qlast.Base_T:\n    \"\"\"Renormalize an expression normalized with imprint_expr_context().\n\n    This helper takes the original, unmangled expression, an EdgeQL AST\n    tree of the same expression mangled with `imprint_expr_context()`\n    (which injects extra WITH MODULE clauses), and produces a normalized\n    expression with explicitly qualified identifiers instead.  Old dumps\n    are the main user of this facility.\n    \"\"\"\n    return norm_mod.renormalize_compat(\n        tree,\n        orig_text,\n        schema=schema,\n        localnames=localnames,\n    )\n\n\ndef _load() -> None:\n    \"\"\"Load the compiler modules.  This is done once per process.\"\"\"\n\n    global _LOADED\n    global dispatch_mod, inference_mod, irast, ireval, norm_mod, stmtctx_mod\n\n    from edb.ir import ast as _irast\n    from edb.ir import staeval as _ireval\n\n    from . import expr as _expr_compiler  # NOQA\n    from . import config as _config_compiler  # NOQA\n    from . import stmt as _stmt_compiler  # NOQA\n\n    from . import dispatch\n    from . import inference\n    from . import normalization\n    from . import stmtctx\n\n    dispatch_mod = dispatch\n    inference_mod = inference\n    irast = _irast\n    ireval = _ireval\n    norm_mod = normalization\n    stmtctx_mod = stmtctx\n    _LOADED = True\n"
  },
  {
    "path": "edb/edgeql/compiler/astutils.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL compiler helpers for AST classification and basic transforms.\"\"\"\n\n\nfrom __future__ import annotations\nfrom dataclasses import dataclass, field\nfrom typing import Optional, TYPE_CHECKING\n\nfrom edb.common import ast\nfrom edb.common import view_patterns\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom edb.schema import name as sn\nfrom edb.schema import functions as s_func\n\nif TYPE_CHECKING:\n\n    from edb.schema import functions as s_func\n\n    from . import context\n\n\ndef extend_binop(\n    binop: Optional[qlast.Expr],\n    *exprs: qlast.Expr,\n    op: str = 'AND',\n) -> qlast.Expr:\n    exprlist = list(exprs)\n\n    if binop is None:\n        result = exprlist.pop(0)\n    else:\n        result = binop\n\n    for expr in exprlist:\n        if expr is not None and expr is not result:\n            result = qlast.BinOp(\n                left=result,\n                right=expr,\n                op=op,\n            )\n\n    return result\n\n\ndef ensure_ql_query(expr: qlast.Expr) -> qlast.Query:\n\n    # a sanity check added after refactoring AST\n    assert isinstance(expr, qlast.Expr)\n\n    if not isinstance(expr, qlast.Query):\n        expr = qlast.SelectQuery(\n            result=expr,\n            implicit=True,\n        )\n    return expr\n\n\ndef ensure_ql_select(expr: qlast.Expr) -> qlast.SelectQuery:\n    if not isinstance(expr, qlast.SelectQuery):\n        expr = qlast.SelectQuery(\n            result=expr,\n            implicit=True,\n        )\n    return expr\n\n\ndef is_ql_empty_set(expr: qlast.Expr) -> bool:\n    return isinstance(expr, qlast.Set) and len(expr.elements) == 0\n\n\ndef is_ql_empty_array(expr: qlast.Expr) -> bool:\n    return isinstance(expr, qlast.Array) and len(expr.elements) == 0\n\n\ndef is_nontrivial_shape_element(shape_el: qlast.ShapeElement) -> bool:\n    return bool(\n        shape_el.where\n        or shape_el.orderby\n        or shape_el.offset\n        or shape_el.limit\n        or shape_el.compexpr\n        or (\n            shape_el.elements and\n            any(is_nontrivial_shape_element(el) for el in shape_el.elements)\n        )\n    )\n\n\ndef extend_path(expr: qlast.Expr, field: str) -> qlast.Path:\n    step = qlast.Ptr(name=field)\n\n    if isinstance(expr, qlast.Path):\n        return qlast.Path(\n            steps=[*expr.steps, step],\n            partial=expr.partial,\n        )\n    else:\n        return qlast.Path(steps=[expr, step])\n\n\n@dataclass\nclass Params:\n    cast_params: list[\n        tuple[qlast.TypeCast, dict[Optional[str], str]]\n    ] = field(default_factory=list)\n    shaped_params: list[\n        tuple[qlast.QueryParameter, qlast.Shape]\n    ] = field(default_factory=list)\n    loose_params: list[qlast.QueryParameter] = field(default_factory=list)\n\n\nclass FindParams(ast.NodeVisitor):\n    \"\"\"Visitor to find all the parameters.\n\n    The annoying bit is that we also need all the modaliases.\n    \"\"\"\n    def __init__(self, modaliases: dict[Optional[str], str]) -> None:\n        super().__init__()\n        self.params: Params = Params()\n        self.modaliases = modaliases\n\n    def visit_Command(self, n: qlast.Command) -> None:\n        self._visit_with_stmt(n)\n\n    def visit_Query(self, n: qlast.Query) -> None:\n        self._visit_with_stmt(n)\n\n    def _visit_with_stmt(self, n: qlast.Statement) -> None:\n        old = self.modaliases\n        for with_entry in (n.aliases or ()):\n            if isinstance(with_entry, qlast.ModuleAliasDecl):\n                self.modaliases = self.modaliases.copy()\n                self.modaliases[with_entry.alias] = with_entry.module\n            else:\n                self.visit(with_entry)\n\n        # The memoization will prevent us from redoing the aliases\n        self.generic_visit(n)\n        self.modaliases = old\n\n    def visit_TypeCast(self, n: qlast.TypeCast) -> None:\n        if isinstance(n.expr, qlast.QueryParameter):\n            self.params.cast_params.append((n, self.modaliases))\n        elif isinstance(n.expr, qlast.Shape):\n            if isinstance(n.expr.expr, qlast.QueryParameter):\n                self.params.shaped_params.append((n.expr.expr, n.expr))\n            else:\n                self.generic_visit(n)\n        else:\n            self.generic_visit(n)\n\n    def visit_QueryParameter(self, n: qlast.QueryParameter) -> None:\n        self.params.loose_params.append(n)\n\n    def visit_CreateFunction(self, n: qlast.CreateFunction) -> None:\n        pass\n\n    def visit_CreateConstraint(self, n: qlast.CreateFunction) -> None:\n        pass\n\n\ndef find_parameters(\n    ql: qlast.Base, modaliases: dict[Optional[str], str]\n) -> Params:\n    \"\"\"Get all query parameters\"\"\"\n    v = FindParams(modaliases)\n    v.visit(ql)\n    return v.params\n\n\nclass alias_view(\n    view_patterns.ViewPattern[tuple[str, list[qlast.PathElement]]],\n    targets=(qlast.Base,),\n):\n    @staticmethod\n    def match(obj: object) -> tuple[str, list[qlast.PathElement]]:\n        match obj:\n            case qlast.Path(\n                steps=[qlast.ObjectRef(module=None, name=alias), *rest],\n                partial=False,\n            ):\n                return alias, rest\n        raise view_patterns.NoMatch\n\n\ndef contains_dml(\n    ql_expr: qlast.Base,\n    *,\n    ctx: context.ContextLevel\n    ) -> bool:\n    \"\"\"Check whether a expression contains any DML in a subtree.\"\"\"\n    # If this ends up being a perf problem, we can use a visitor\n    # directly and cache.\n    dml_types = (qlast.InsertQuery, qlast.UpdateQuery, qlast.DeleteQuery)\n    if isinstance(ql_expr, dml_types):\n        return True\n\n    res = ast.find_children(\n        ql_expr, qlast.Base,\n        lambda x: (\n            isinstance(x, dml_types)\n            or (isinstance(x, qlast.IRAnchor) and x.has_dml)\n            or (\n                isinstance(x, qlast.FunctionCall)\n                and any(\n                    (\n                        func.get_volatility(ctx.env.schema)\n                        == qltypes.Volatility.Modifying\n                    )\n                    for func in _get_functions_from_call(x, ctx=ctx)\n                )\n            )\n        ),\n        terminate_early=True,\n    )\n\n    return bool(res)\n\n\ndef _get_functions_from_call(\n    expr: qlast.FunctionCall,\n    *,\n    ctx: context.ContextLevel,\n) -> tuple[s_func.Function, ...]:\n    funcname: sn.Name\n    if isinstance(expr.func, str):\n        funcname = sn.UnqualName(expr.func)\n    else:\n        funcname = sn.QualName(*expr.func)\n\n    return s_func.lookup_functions(\n        funcname,\n        default=(),\n        module_aliases=ctx.modaliases,\n        schema=ctx.env.schema,\n    )\n"
  },
  {
    "path": "edb/edgeql/compiler/casts.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL compiler routines for type casts.\"\"\"\n\n\nfrom __future__ import annotations\n\nimport json\nfrom typing import (\n    Optional,\n    Iterable,\n    Mapping,\n    cast,\n    TYPE_CHECKING,\n)\n\nfrom edb import errors\n\nfrom edb.common import parsing\n\nfrom edb.ir import ast as irast\nfrom edb.ir import utils as irutils\n\nfrom edb.schema import casts as s_casts\nfrom edb.schema import constraints as s_constr\nfrom edb.schema import functions as s_func\nfrom edb.schema import indexes as s_indexes\nfrom edb.schema import name as sn\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import types as s_types\nfrom edb.schema import utils as s_utils\nfrom edb.schema import name as s_name\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes as ft\n\nfrom . import astutils\nfrom . import context\nfrom . import dispatch\nfrom . import pathctx\nfrom . import polyres\nfrom . import setgen\nfrom . import typegen\nfrom . import viewgen\n\nif TYPE_CHECKING:\n    from edb.schema import schema as s_schema\n\n\ndef compile_cast(\n    ir_expr: irast.Set | irast.Expr,\n    new_stype: s_types.Type,\n    *,\n    span: Optional[parsing.Span],\n    ctx: context.ContextLevel,\n    cardinality_mod: Optional[qlast.CardinalityModifier] = None,\n) -> irast.Set:\n\n    if new_stype.is_polymorphic(ctx.env.schema) and span is not None:\n        # If we have no span we don't know whether this is a direct cast\n        # or some implicit cast being processed.\n        raise errors.QueryError(\n            f'cannot cast into generic type '\n            f'{new_stype.get_displayname(ctx.env.schema)!r}',\n            hint=\"Please ensure you don't use generic \"\n                 '\"any\" types or abstract scalars.',\n            span=span)\n\n    if (\n        isinstance(ir_expr, irast.Set)\n        and isinstance(ir_expr.expr, irast.EmptySet)\n    ):\n        # For the common case of casting an empty set, we simply\n        # generate a new empty set node of the requested type.\n        return setgen.new_empty_set(\n            stype=new_stype,\n            alias=ir_expr.path_id.target_name_hint.name,\n            ctx=ctx,\n            span=ir_expr.span)\n\n    if isinstance(new_stype, s_types.Array) and (\n        irutils.is_untyped_empty_array_expr(ir_expr)\n        or (\n            isinstance(ir_expr, irast.Set)\n            and irutils.is_untyped_empty_array_expr(\n                irutils.unwrap_set(ir_expr).expr)\n        )\n    ):\n        # Ditto for empty arrays.\n        new_typeref = typegen.type_to_typeref(new_stype, ctx.env)\n        return setgen.ensure_set(\n            irast.Array(elements=[], typeref=new_typeref), ctx=ctx)\n\n    ir_set = setgen.ensure_set(ir_expr, ctx=ctx)\n    orig_stype = setgen.get_set_type(ir_set, ctx=ctx)\n\n    if new_stype.is_polymorphic(ctx.env.schema):\n        raise errors.QueryError(\n            f'expression returns value of indeterminate type',\n            span=span)\n\n    if (orig_stype == new_stype and\n            cardinality_mod is not qlast.CardinalityModifier.Required):\n        return ir_set\n    if orig_stype.is_object_type() and new_stype.is_object_type():\n        # Object types cannot be cast between themselves,\n        # as cast is a _constructor_ operation, and the only\n        # valid way to construct an object is to INSERT it.\n        raise errors.QueryError(\n            f'cannot cast object type '\n            f'{orig_stype.get_displayname(ctx.env.schema)!r} '\n            f'to {new_stype.get_displayname(ctx.env.schema)!r}, use '\n            f'`...[IS {new_stype.get_displayname(ctx.env.schema)}]` instead',\n            span=span)\n\n    # The only valid object type cast other than <uuid> is from anytype,\n    # and thus it must be an empty set.\n    if (\n        orig_stype.is_any(ctx.env.schema)\n        and new_stype.is_object_type()\n    ):\n        return setgen.new_empty_set(\n            stype=new_stype,\n            ctx=ctx,\n            span=ir_expr.span)\n\n    uuid_t = ctx.env.get_schema_type_and_track(sn.QualName('std', 'uuid'))\n    if (\n        orig_stype.issubclass(ctx.env.schema, uuid_t)\n        and new_stype.is_object_type()\n    ):\n        return _find_object_by_id(ir_expr, new_stype, ctx=ctx)\n\n    json_t = ctx.env.get_schema_type_and_track(sn.QualName('std', 'json'))\n    if (\n        isinstance(ir_set.expr, irast.Array)\n        and (\n            isinstance(new_stype, s_types.Array)\n            or new_stype.issubclass(ctx.env.schema, json_t)\n        )\n    ):\n        cast_element = ('array', None)\n        if ctx.collection_cast_info is not None:\n            ctx.collection_cast_info.path_elements.append(cast_element)\n\n        result = _cast_array_literal(\n            ir_set, orig_stype, new_stype, span=span, ctx=ctx)\n\n        if ctx.collection_cast_info is not None:\n            ctx.collection_cast_info.path_elements.pop()\n\n        return result\n\n    if orig_stype.is_tuple(ctx.env.schema):\n        return _cast_tuple(\n            ir_set, orig_stype, new_stype, span=span, ctx=ctx)\n\n    if isinstance(orig_stype, s_types.Array):\n        if not s_types.is_type_compatible(\n            orig_stype, new_stype, schema=ctx.env.schema\n        ) and (\n            not isinstance(new_stype, s_types.Array)\n            and isinstance(\n                (el_type := orig_stype.get_subtypes(ctx.env.schema)[0]),\n                s_scalars.ScalarType,\n            )\n        ):\n            # We're not casting to another array, so for purposes of matching\n            # the right cast we want to reduce orig_stype to an array of the\n            # built-in base type as that's what the cast will actually\n            # expect.\n            ir_set = _cast_to_base_array(\n                ir_set, el_type, orig_stype, ctx=ctx)\n\n        if isinstance(new_stype, s_types.Array):\n            cast_element = ('array', None)\n            if ctx.collection_cast_info is not None:\n                ctx.collection_cast_info.path_elements.append(cast_element)\n\n            result = _cast_array(\n                ir_set, orig_stype, new_stype, span=span, ctx=ctx)\n\n            if ctx.collection_cast_info is not None:\n                ctx.collection_cast_info.path_elements.pop()\n\n            return result\n\n        else:\n            return _cast_array(\n                ir_set, orig_stype, new_stype, span=span, ctx=ctx)\n\n    if isinstance(orig_stype, s_types.Range):\n        if s_types.is_type_compatible(\n            orig_stype, new_stype, schema=ctx.env.schema\n        ):\n            # Casting between compatible types is unnecessary. It is important\n            # to catch things like RangeExprAlias and Range being of the same\n            # type and not neding a cast.\n            return ir_set\n        else:\n            if isinstance(new_stype, s_types.MultiRange):\n                # For multirange target type we might need to first upcast the\n                # range into corresponding multirange and then do a separate\n                # cast for the subtype.\n                if (\n                    (ost := orig_stype.get_subtypes(schema=ctx.env.schema)) !=\n                        new_stype.get_subtypes(schema=ctx.env.schema)\n                ):\n                    ctx.env.schema, mr_stype = \\\n                        s_types.MultiRange.from_subtypes(ctx.env.schema, ost)\n                    ir_set = _inheritance_cast_to_ir(\n                        ir_set, orig_stype, mr_stype,\n                        cardinality_mod=cardinality_mod, ctx=ctx)\n                    return _cast_multirange(\n                        ir_set, mr_stype, new_stype, span=span, ctx=ctx)\n\n                else:\n                    # The subtypes match, so this is a direct upcast from\n                    # range to multirange.\n                    return _inheritance_cast_to_ir(\n                        ir_set, orig_stype, new_stype,\n                        cardinality_mod=cardinality_mod, ctx=ctx)\n\n            return _cast_range(\n                ir_set, orig_stype, new_stype, span=span, ctx=ctx)\n\n    if orig_stype.is_multirange():\n        if s_types.is_type_compatible(\n            orig_stype, new_stype, schema=ctx.env.schema\n        ):\n            # Casting between compatible types is unnecessary. It is important\n            # to catch things like MultiRangeExprAlias and MultiRange being of\n            # the same type and not neding a cast.\n            return ir_set\n        else:\n            return _cast_multirange(\n                ir_set, orig_stype, new_stype, span=span, ctx=ctx)\n\n    if orig_stype.issubclass(ctx.env.schema, new_stype):\n        # The new type is a supertype of the old type,\n        # and is always a wider domain, so we simply reassign\n        # the stype.\n        return _inheritance_cast_to_ir(\n            ir_set, orig_stype, new_stype,\n            cardinality_mod=cardinality_mod, ctx=ctx)\n\n    if (\n        new_stype.issubclass(ctx.env.schema, orig_stype)\n        or _has_common_concrete_scalar(orig_stype, new_stype, ctx=ctx)\n    ):\n        # The new type is a subtype or a sibling type of a shared\n        # ancestor, so may potentially have a more restrictive domain,\n        # generate a cast call.\n        return _inheritance_cast_to_ir(\n            ir_set, orig_stype, new_stype,\n            cardinality_mod=cardinality_mod, ctx=ctx)\n\n    if (\n        new_stype.issubclass(ctx.env.schema, json_t)\n        and ir_set.path_id.is_objtype_path()\n    ):\n        # JSON casts of objects are special: we want the full shape\n        # and not just an identity.\n        viewgen.late_compile_view_shapes(ir_set, ctx=ctx)\n    elif orig_stype.issubclass(ctx.env.schema, json_t):\n\n        if base_stype := _get_concrete_scalar_base(new_stype, ctx):\n            # Casts from json to custom scalars may have special handling.\n            # So we turn the type cast json->x into json->base and base->x.\n            base_ir = compile_cast(ir_expr, base_stype, span=span, ctx=ctx)\n\n            return compile_cast(\n                base_ir,\n                new_stype,\n                cardinality_mod=cardinality_mod,\n                span=span,\n                ctx=ctx,\n            )\n\n        elif isinstance(\n            new_stype, s_types.Array\n        ) and not new_stype.get_subtypes(ctx.env.schema)[0].issubclass(\n            ctx.env.schema, json_t\n        ):\n            # Turn casts from json->array<T> into json->array<json>\n            # and array<json>->array<T>.\n            ctx.env.schema, json_array_typ = s_types.Array.from_subtypes(\n                ctx.env.schema, [json_t]\n            )\n            json_array_ir = compile_cast(\n                ir_expr,\n                json_array_typ,\n                cardinality_mod=cardinality_mod,\n                span=span,\n                ctx=ctx,\n            )\n            return compile_cast(\n                json_array_ir, new_stype, span=span, ctx=ctx\n            )\n\n        elif isinstance(new_stype, s_types.Tuple):\n            return _cast_json_to_tuple(\n                ir_set,\n                orig_stype,\n                new_stype,\n                cardinality_mod,\n                span=span,\n                ctx=ctx,\n            )\n\n        elif isinstance(new_stype, s_types.Range):\n            return _cast_json_to_range(\n                ir_set,\n                orig_stype,\n                new_stype,\n                cardinality_mod,\n                span=span,\n                ctx=ctx,\n            )\n\n        elif isinstance(new_stype, s_types.MultiRange):\n            return _cast_json_to_multirange(\n                ir_set,\n                orig_stype,\n                new_stype,\n                cardinality_mod,\n                span=span,\n                ctx=ctx,\n            )\n\n    # Constraints and indexes require an immutable expression, but pg cast is\n    # only stable. In this specific case, we use cast wrapper function that\n    # is declared to be immutable.\n    if orig_stype.is_enum(ctx.env.schema) or new_stype.is_enum(ctx.env.schema):\n        objctx = ctx.env.options.schema_object_context\n        if objctx in (s_constr.Constraint, s_indexes.Index):\n\n            str_typ = ctx.env.schema.get(\n                sn.QualName(\"std\", \"str\"),\n                type=s_types.Type,\n            )\n            orig_str = orig_stype.issubclass(ctx.env.schema, str_typ)\n            new_str = new_stype.issubclass(ctx.env.schema, str_typ)\n            if orig_str or new_str:\n                return _cast_enum_str_immutable(\n                    ir_expr, orig_stype, new_stype, ctx=ctx\n                )\n\n    return _compile_cast(\n        ir_expr,\n        orig_stype,\n        new_stype,\n        cardinality_mod=cardinality_mod,\n        span=span,\n        ctx=ctx,\n    )\n\n\ndef _has_common_concrete_scalar(\n    orig_stype: s_types.Type,\n    new_stype: s_types.Type,\n    *,\n    ctx: context.ContextLevel,\n) -> bool:\n    schema = ctx.env.schema\n    return bool(\n        isinstance(orig_stype, s_scalars.ScalarType)\n        and isinstance(new_stype, s_scalars.ScalarType)\n        and (orig_base := orig_stype.maybe_get_topmost_concrete_base(schema))\n        and (new_base := new_stype.maybe_get_topmost_concrete_base(schema))\n        and orig_base == new_base\n    )\n\n\ndef _get_concrete_scalar_base(\n    stype: s_types.Type, ctx: context.ContextLevel\n) -> Optional[s_types.Type]:\n    \"\"\"Returns None if stype is not scalar or if it is already topmost\"\"\"\n\n    if stype.is_enum(ctx.env.schema):\n        return ctx.env.get_schema_type_and_track(sn.QualName('std', 'str'))\n\n    if not isinstance(stype, s_scalars.ScalarType):\n        return None\n    if topmost := stype.maybe_get_topmost_concrete_base(ctx.env.schema):\n        if topmost != stype:\n            return topmost\n    return None\n\n\ndef _compile_cast(\n    ir_expr: irast.Set | irast.Expr,\n    orig_stype: s_types.Type,\n    new_stype: s_types.Type,\n    *,\n    span: Optional[parsing.Span],\n    ctx: context.ContextLevel,\n    cardinality_mod: Optional[qlast.CardinalityModifier],\n) -> irast.Set:\n\n    ir_set = setgen.ensure_set(ir_expr, ctx=ctx)\n    cast = _find_cast(orig_stype, new_stype, span=span, ctx=ctx)\n\n    if cast is None:\n        raise errors.QueryError(\n            f'cannot cast '\n            f'{orig_stype.get_displayname(ctx.env.schema)!r} to '\n            f'{new_stype.get_displayname(ctx.env.schema)!r}',\n            span=span or ir_set.span)\n\n    return _cast_to_ir(ir_set, cast, orig_stype, new_stype,\n                       cardinality_mod, ctx=ctx)\n\n\ndef _cast_to_ir(\n    ir_set: irast.Set,\n    cast: s_casts.Cast,\n    orig_stype: s_types.Type,\n    new_stype: s_types.Type,\n    cardinality_mod: Optional[qlast.CardinalityModifier] = None,\n    *,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    orig_typeref = typegen.type_to_typeref(orig_stype, env=ctx.env)\n    new_typeref = typegen.type_to_typeref(new_stype, env=ctx.env)\n    cast_name = cast.get_name(ctx.env.schema)\n    cast_ir = irast.TypeCast(\n        expr=ir_set,\n        from_type=orig_typeref,\n        to_type=new_typeref,\n        cardinality_mod=cardinality_mod,\n        cast_name=cast_name,\n        sql_function=cast.get_from_function(ctx.env.schema),\n        sql_cast=cast.get_from_cast(ctx.env.schema),\n        sql_expr=bool(cast.get_code(ctx.env.schema)),\n        error_message_context=cast_message_context(ctx),\n    )\n\n    return setgen.ensure_set(cast_ir, ctx=ctx)\n\n\ndef _inheritance_cast_to_ir(\n    ir_set: irast.Set,\n    orig_stype: s_types.Type,\n    new_stype: s_types.Type,\n    *,\n    cardinality_mod: Optional[qlast.CardinalityModifier],\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    orig_typeref = typegen.type_to_typeref(orig_stype, env=ctx.env)\n    new_typeref = typegen.type_to_typeref(new_stype, env=ctx.env)\n    cast_ir = irast.TypeCast(\n        expr=ir_set,\n        from_type=orig_typeref,\n        to_type=new_typeref,\n        cardinality_mod=cardinality_mod,\n        cast_name=None,\n        sql_function=None,\n        sql_cast=True,\n        sql_expr=False,\n        error_message_context=cast_message_context(ctx),\n    )\n\n    return setgen.ensure_set(cast_ir, ctx=ctx)\n\n\nclass CastParamListWrapper(s_func.ParameterLikeList):\n\n    def __init__(self, params: Iterable[s_func.ParameterDesc]) -> None:\n        self._params = tuple(params)\n\n    def get_by_name(\n        self,\n        schema: s_schema.Schema,\n        name: str,\n    ) -> s_func.ParameterDesc:\n        raise NotImplementedError\n\n    def as_str(self, schema: s_schema.Schema) -> str:\n        raise NotImplementedError\n\n    def find_named_only(\n        self,\n        schema: s_schema.Schema,\n    ) -> Mapping[str, s_func.ParameterDesc]:\n        return {}\n\n    def find_variadic(\n        self,\n        schema: s_schema.Schema,\n    ) -> Optional[s_func.ParameterDesc]:\n        return None\n\n    def has_polymorphic(\n        self,\n        schema: s_schema.Schema,\n    ) -> bool:\n        return False\n\n    def has_objects(\n        self,\n        schema: s_schema.Schema,\n    ) -> bool:\n        return False\n\n    def has_set_of(\n        self,\n        schema: s_schema.Schema,\n    ) -> bool:\n        return False\n\n    def objects(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[s_func.ParameterDesc, ...]:\n        return self._params\n\n    def has_required_params(self, schema: s_schema.Schema) -> bool:\n        return True\n\n    def get_in_canonical_order(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[s_func.ParameterDesc, ...]:\n        return self._params\n\n\nclass CastCallableWrapper(s_func.CallableLike):\n    # A wrapper around a cast object to make it quack like a callable\n    # for the purposes of polymorphic resolution.\n    def __init__(self, cast: s_casts.Cast) -> None:\n        self._cast = cast\n\n    def has_inlined_defaults(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def get_params(\n        self,\n        schema: s_schema.Schema,\n    ) -> s_func.ParameterLikeList:\n        from_type_param = s_func.ParameterDesc(\n            num=0,\n            name=sn.UnqualName('val'),\n            type=self._cast.get_from_type(schema).as_shell(schema),\n            typemod=ft.TypeModifier.SingletonType,\n            kind=ft.ParameterKind.PositionalParam,\n            default=None,\n        )\n\n        to_type_param = s_func.ParameterDesc(\n            num=0,\n            name=sn.UnqualName('_'),\n            type=self._cast.get_to_type(schema).as_shell(schema),\n            typemod=ft.TypeModifier.SingletonType,\n            kind=ft.ParameterKind.PositionalParam,\n            default=None,\n        )\n\n        return CastParamListWrapper((from_type_param, to_type_param))\n\n    def get_return_type(self, schema: s_schema.Schema) -> s_types.Type:\n        return self._cast.get_to_type(schema)\n\n    def get_return_typemod(self, schema: s_schema.Schema) -> ft.TypeModifier:\n        return ft.TypeModifier.SingletonType\n\n    def get_verbosename(self, schema: s_schema.Schema) -> str:\n        return self._cast.get_verbosename(schema)\n\n    def get_abstract(self, schema: s_schema.Schema) -> bool:\n        return False\n\n\ndef _find_cast(\n    orig_stype: s_types.Type,\n    new_stype: s_types.Type,\n    *,\n    span: Optional[parsing.Span],\n    ctx: context.ContextLevel,\n) -> Optional[s_casts.Cast]:\n\n    # Don't try to pick up casts when there is a direct subtyping\n    # relationship.\n    if (orig_stype.issubclass(ctx.env.schema, new_stype)\n            or new_stype.issubclass(ctx.env.schema, orig_stype)\n            or _has_common_concrete_scalar(orig_stype, new_stype, ctx=ctx)):\n        return None\n\n    casts = ctx.env.schema.get_casts_to_type(new_stype)\n    if not casts and isinstance(new_stype, s_types.InheritingType):\n        ancestors = new_stype.get_ancestors(ctx.env.schema)\n        for t in ancestors.objects(ctx.env.schema):\n            casts = ctx.env.schema.get_casts_to_type(t)\n            if casts:\n                break\n        else:\n            return None\n\n    dummy_set = irast.DUMMY_SET\n    args = [\n        (orig_stype, dummy_set),\n        (new_stype, dummy_set),\n    ]\n\n    matched = polyres.find_callable(\n        (CastCallableWrapper(c) for c in casts), args=args, kwargs={}, ctx=ctx)\n\n    if len(matched) == 1:\n        return cast(CastCallableWrapper, matched[0].func)._cast\n    elif len(matched) > 1:\n        raise errors.QueryError(\n            f'cannot unambiguously cast '\n            f'{orig_stype.get_displayname(ctx.env.schema)!r} '\n            f'to {new_stype.get_displayname(ctx.env.schema)!r}',\n            span=span)\n    else:\n        return None\n\n\ndef _cast_json_to_tuple(\n    ir_set: irast.Set,\n    orig_stype: s_types.Type,\n    new_stype: s_types.Tuple,\n    cardinality_mod: Optional[qlast.CardinalityModifier],\n    *,\n    span: Optional[parsing.Span],\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    with ctx.new() as subctx:\n        subctx.allow_factoring()\n        pathctx.register_set_in_scope(ir_set, ctx=subctx)\n\n        subctx.anchors = subctx.anchors.copy()\n        source_path = subctx.create_anchor(ir_set, 'a')\n\n        # Top-level json->tuple casts should produce an empty set on\n        # null inputs, but error on missing fields or null subelements\n        allow_null = cardinality_mod != qlast.CardinalityModifier.Required\n\n        # Only json arrays or objects can be cast to tuple.\n        # If not in the top level cast, raise an exception here\n        json_object_args: list[qlast.Expr] = [\n            source_path,\n            qlast.Constant.boolean(allow_null),\n        ]\n        if error_message_context := cast_message_context(subctx):\n            json_object_args.append(qlast.Constant.string(\n                json.dumps({\n                    \"error_message_context\": error_message_context\n                })\n            ))\n\n        # Don't validate NULLs. They are filtered out with the json nulls.\n        json_objects = qlast.IfElse(\n            condition=qlast.UnaryOp(\n                op='EXISTS',\n                operand=source_path,\n            ),\n            if_expr=qlast.FunctionCall(\n                func=('__std__', '__tuple_validate_json'),\n                args=json_object_args,\n            ),\n            else_expr=qlast.TypeCast(\n                expr=qlast.Set(elements=[]),\n                type=typegen.type_to_ql_typeref(orig_stype, ctx=ctx),\n            ),\n        )\n\n        json_objects_ir = dispatch.compile(json_objects, ctx=subctx)\n\n    with ctx.new() as subctx:\n        pathctx.register_set_in_scope(json_objects_ir, ctx=subctx)\n        subctx.anchors = subctx.anchors.copy()\n        source_path = subctx.create_anchor(json_objects_ir, 'a')\n\n        # Filter out json nulls and postgress NULLs.\n        # Nulls at the top level cast can be ignored.\n        filtered = qlast.SelectQuery(\n            result=source_path,\n            where=qlast.BinOp(\n                left=qlast.FunctionCall(\n                    func=('__std__', 'json_typeof'), args=[source_path]\n                ),\n                op='!=',\n                right=qlast.Constant.string('null'),\n            ),\n        )\n        filtered_ir = dispatch.compile(filtered, ctx=subctx)\n        source_path = subctx.create_anchor(filtered_ir, 'a')\n\n        # TODO: try using jsonb_to_record instead of a bunch of\n        # json_get calls and see if that is faster.\n        elements = []\n        for new_el_name, new_st in new_stype.iter_subtypes(ctx.env.schema):\n            cast_element = ('tuple', new_el_name)\n            if subctx.collection_cast_info is not None:\n                subctx.collection_cast_info.path_elements.append(cast_element)\n\n            json_get_kwargs: dict[str, qlast.Expr] = {}\n            if error_message_context := cast_message_context(subctx):\n                json_get_kwargs['detail'] = qlast.Constant.string(\n                    json.dumps({\n                        \"error_message_context\": error_message_context\n                    })\n                )\n            val_e = qlast.FunctionCall(\n                func=('__std__', '__json_get_not_null'),\n                args=[\n                    source_path,\n                    qlast.Constant.string(new_el_name),\n                ],\n                kwargs=json_get_kwargs\n            )\n\n            val = dispatch.compile(val_e, ctx=subctx)\n\n            val = compile_cast(\n                val, new_st,\n                cardinality_mod=qlast.CardinalityModifier.Required,\n                ctx=subctx, span=span)\n\n            if subctx.collection_cast_info is not None:\n                subctx.collection_cast_info.path_elements.pop()\n\n            elements.append(irast.TupleElement(name=new_el_name, val=val))\n\n        return setgen.new_tuple_set(\n            elements,\n            named=new_stype.is_named(ctx.env.schema),\n            ctx=subctx,\n        )\n\n\ndef _cast_tuple(\n    ir_set: irast.Set,\n    orig_stype: s_types.Type,\n    new_stype: s_types.Type,\n    *,\n    span: Optional[parsing.Span],\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    assert isinstance(orig_stype, s_types.Tuple)\n\n    # Make sure the source tuple expression is pinned in the scope,\n    # so that we don't generate a cross-product of it by evaluating\n    # the tuple indirections.\n    pathctx.register_set_in_scope(ir_set, ctx=ctx)\n\n    direct_cast = _find_cast(orig_stype, new_stype, span=span, ctx=ctx)\n    orig_subtypes = dict(orig_stype.iter_subtypes(ctx.env.schema))\n\n    if direct_cast is not None:\n        # Direct casting to non-tuple involves casting each tuple\n        # element and also keeping the cast around the whole tuple.\n        # This is to trigger the downstream logic of casting\n        # objects (in elements of the tuple).\n        elements = []\n        for n in orig_subtypes:\n            val = setgen.tuple_indirection_set(\n                ir_set,\n                source=orig_stype,\n                ptr_name=n,\n                ctx=ctx,\n            )\n            val_type = setgen.get_set_type(val, ctx=ctx)\n            # Element cast\n            cast_element = ('tuple', n)\n            if ctx.collection_cast_info is not None:\n                ctx.collection_cast_info.path_elements.append(cast_element)\n\n            val = compile_cast(val, new_stype, ctx=ctx, span=span)\n\n            if ctx.collection_cast_info is not None:\n                ctx.collection_cast_info.path_elements.pop()\n\n            elements.append(irast.TupleElement(name=n, val=val))\n\n        new_tuple = setgen.new_tuple_set(\n            elements,\n            named=orig_stype.is_named(ctx.env.schema),\n            ctx=ctx,\n        )\n\n        return _cast_to_ir(\n            new_tuple, direct_cast, orig_stype, new_stype, ctx=ctx)\n\n    if not new_stype.is_tuple(ctx.env.schema):\n        raise errors.QueryError(\n            f'cannot cast {orig_stype.get_displayname(ctx.env.schema)!r} '\n            f'to {new_stype.get_displayname(ctx.env.schema)!r}',\n            span=span)\n\n    assert isinstance(new_stype, s_types.Tuple)\n    new_subtypes = list(new_stype.iter_subtypes(ctx.env.schema))\n    if len(orig_subtypes) != len(new_subtypes):\n        raise errors.QueryError(\n            f'cannot cast {orig_stype.get_displayname(ctx.env.schema)!r} '\n            f'to {new_stype.get_displayname(ctx.env.schema)!r}: '\n            f'the number of elements is not the same',\n            span=span)\n\n    # For tuple-to-tuple casts we generate a new tuple\n    # to simplify things on sqlgen side.\n    elements = []\n    for i, n in enumerate(orig_subtypes):\n        val = setgen.tuple_indirection_set(\n            ir_set,\n            source=orig_stype,\n            ptr_name=n,\n            ctx=ctx,\n        )\n        val_type = setgen.get_set_type(val, ctx=ctx)\n        new_el_name, new_st = new_subtypes[i]\n        if val_type != new_st:\n            # Element cast\n            cast_element = ('tuple', new_el_name)\n            if ctx.collection_cast_info is not None:\n                ctx.collection_cast_info.path_elements.append(cast_element)\n\n            val = compile_cast(val, new_st, ctx=ctx, span=span)\n\n            if ctx.collection_cast_info is not None:\n                ctx.collection_cast_info.path_elements.pop()\n\n        elements.append(irast.TupleElement(name=new_el_name, val=val))\n\n    return setgen.new_tuple_set(\n        elements,\n        named=new_stype.is_named(ctx.env.schema),\n        ctx=ctx,\n    )\n\n\ndef _cast_range(\n    ir_set: irast.Set,\n    orig_stype: s_types.Type,\n    new_stype: s_types.Type,\n    *,\n    span: Optional[parsing.Span],\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    assert isinstance(orig_stype, s_types.Range)\n\n    direct_cast = _find_cast(orig_stype, new_stype, span=span, ctx=ctx)\n    if direct_cast is not None:\n        return _cast_to_ir(\n            ir_set, direct_cast, orig_stype, new_stype, ctx=ctx\n        )\n\n    if not new_stype.is_range():\n        raise errors.QueryError(\n            f'cannot cast {orig_stype.get_displayname(ctx.env.schema)!r} '\n            f'to {new_stype.get_displayname(ctx.env.schema)!r}',\n            span=span)\n    assert isinstance(new_stype, s_types.Range)\n    el_type = new_stype.get_subtypes(ctx.env.schema)[0]\n    orig_el_type = orig_stype.get_subtypes(ctx.env.schema)[0]\n    ql_el_type = typegen.type_to_ql_typeref(el_type, ctx=ctx)\n\n    el_cast = _find_cast(orig_el_type, el_type, span=span, ctx=ctx)\n    if el_cast is None:\n        raise errors.QueryError(\n            f'cannot cast {orig_stype.get_displayname(ctx.env.schema)!r} '\n            f'to {new_stype.get_displayname(ctx.env.schema)!r}',\n            span=span)\n\n    with ctx.new() as subctx:\n        subctx.allow_factoring()\n        subctx.anchors = subctx.anchors.copy()\n        source_path = subctx.create_anchor(ir_set, 'a')\n\n        cast = qlast.FunctionCall(\n            func=('__std__', 'range'),\n            args=[\n                qlast.TypeCast(\n                    expr=qlast.FunctionCall(\n                        func=('__std__', 'range_get_lower'),\n                        args=[source_path],\n                    ),\n                    type=ql_el_type,\n                ),\n                qlast.TypeCast(\n                    expr=qlast.FunctionCall(\n                        func=('__std__', 'range_get_upper'),\n                        args=[source_path],\n                    ),\n                    type=ql_el_type,\n                ),\n            ],\n            kwargs={\n                \"inc_lower\": qlast.FunctionCall(\n                    func=('__std__', 'range_is_inclusive_lower'),\n                    args=[source_path],\n                ),\n                \"inc_upper\": qlast.FunctionCall(\n                    func=('__std__', 'range_is_inclusive_upper'),\n                    args=[source_path],\n                ),\n                \"empty\": qlast.FunctionCall(\n                    func=('__std__', 'range_is_empty'),\n                    args=[source_path],\n                ),\n            }\n        )\n\n        if el_type.contains_json(subctx.env.schema):\n            subctx.implicit_limit = 0\n\n        return dispatch.compile(cast, ctx=subctx)\n\n\ndef _cast_multirange(\n    ir_set: irast.Set,\n    orig_stype: s_types.Type,\n    new_stype: s_types.Type,\n    *,\n    span: Optional[parsing.Span],\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    assert isinstance(orig_stype, s_types.MultiRange)\n\n    direct_cast = _find_cast(orig_stype, new_stype, span=span, ctx=ctx)\n    if direct_cast is not None:\n        return _cast_to_ir(\n            ir_set, direct_cast, orig_stype, new_stype, ctx=ctx\n        )\n\n    if not new_stype.is_multirange():\n        raise errors.QueryError(\n            f'cannot cast {orig_stype.get_displayname(ctx.env.schema)!r} '\n            f'to {new_stype.get_displayname(ctx.env.schema)!r}',\n            span=span)\n    assert isinstance(new_stype, s_types.MultiRange)\n    el_type = new_stype.get_subtypes(ctx.env.schema)[0]\n    orig_el_type = orig_stype.get_subtypes(ctx.env.schema)[0]\n\n    el_cast = _find_cast(orig_el_type, el_type, span=span, ctx=ctx)\n    if el_cast is None:\n        raise errors.QueryError(\n            f'cannot cast {orig_stype.get_displayname(ctx.env.schema)!r} '\n            f'to {new_stype.get_displayname(ctx.env.schema)!r}',\n            span=span)\n\n    ctx.env.schema, new_range_type = s_types.Range.from_subtypes(\n        ctx.env.schema, [el_type])\n    ql_range_type = typegen.type_to_ql_typeref(new_range_type, ctx=ctx)\n    with ctx.new() as subctx:\n        subctx.allow_factoring()\n        subctx.anchors = subctx.anchors.copy()\n        pathctx.register_set_in_scope(ir_set, ctx=subctx)\n        source_path = subctx.create_anchor(ir_set, 'a')\n\n        # multirange(\n        #     array_agg(\n        #         <range<el_type>>multirange_unpack(orig)\n        #     )\n        # )\n        cast = qlast.FunctionCall(\n            func=('__std__', 'multirange'),\n            args=[\n                qlast.FunctionCall(\n                    func=('__std__', 'array_agg'),\n                    args=[\n                        qlast.TypeCast(\n                            expr=qlast.FunctionCall(\n                                func=('__std__', 'multirange_unpack'),\n                                args=[source_path],\n                            ),\n                            type=ql_range_type,\n                        ),\n                    ],\n                ),\n            ],\n        )\n\n        if el_type.contains_json(subctx.env.schema):\n            subctx.implicit_limit = 0\n\n        return dispatch.compile(cast, ctx=subctx)\n\n\ndef _cast_json_to_range(\n    ir_set: irast.Set,\n    orig_stype: s_types.Type,\n    new_stype: s_types.Range,\n    cardinality_mod: Optional[qlast.CardinalityModifier],\n    *,\n    span: Optional[parsing.Span],\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    with ctx.new() as subctx:\n        subctx.anchors = subctx.anchors.copy()\n        source_path = subctx.create_anchor(ir_set, 'a')\n\n        check_args: list[qlast.Expr] = [source_path]\n        if error_message_context := cast_message_context(subctx):\n            check_args.append(qlast.Constant.string(\n                json.dumps({\n                    \"error_message_context\": error_message_context\n                })\n            ))\n        check = qlast.FunctionCall(\n            func=('__std__', '__range_validate_json'),\n            args=check_args\n        )\n\n        check_ir = dispatch.compile(check, ctx=subctx)\n        source_path = subctx.create_anchor(check_ir, 'b')\n\n        range_el_t = new_stype.get_element_type(ctx.env.schema)\n        ql_range_el_t = typegen.type_to_ql_typeref(range_el_t, ctx=subctx)\n        bool_t = ctx.env.get_schema_type_and_track(sn.QualName('std', 'bool'))\n        ql_bool_t = typegen.type_to_ql_typeref(bool_t, ctx=subctx)\n\n        def compile_with_range_element(\n            expr: qlast.Expr,\n            element_name: str,\n        ) -> irast.Set:\n            cast_element = ('range', element_name)\n            if subctx.collection_cast_info is not None:\n                subctx.collection_cast_info.path_elements.append(cast_element)\n\n            expr_ir = dispatch.compile(expr, ctx=subctx)\n\n            if subctx.collection_cast_info is not None:\n                subctx.collection_cast_info.path_elements.pop()\n\n            return expr_ir\n\n        lower: qlast.Expr = qlast.TypeCast(\n            expr=qlast.FunctionCall(\n                func=('__std__', 'json_get'),\n                args=[\n                    source_path,\n                    qlast.Constant.string('lower'),\n                ],\n            ),\n            type=ql_range_el_t,\n        )\n        lower_ir = compile_with_range_element(lower, 'lower')\n        lower = subctx.create_anchor(lower_ir, 'lower')\n\n        upper: qlast.Expr = qlast.TypeCast(\n            expr=qlast.FunctionCall(\n                func=('__std__', 'json_get'),\n                args=[\n                    source_path,\n                    qlast.Constant.string('upper'),\n                ],\n            ),\n            type=ql_range_el_t,\n        )\n        upper_ir = compile_with_range_element(upper, 'upper')\n        upper = subctx.create_anchor(upper_ir, 'upper')\n\n        inc_lower: qlast.Expr = qlast.TypeCast(\n            expr=qlast.FunctionCall(\n                func=('__std__', 'json_get'),\n                args=[\n                    source_path,\n                    qlast.Constant.string('inc_lower'),\n                ],\n                kwargs={\n                    'default': qlast.FunctionCall(\n                        func=('__std__', 'to_json'),\n                        args=[qlast.Constant.string(\"true\")],\n                    ),\n                },\n            ),\n            type=ql_bool_t,\n        )\n        inc_lower_ir = compile_with_range_element(inc_lower, 'inc_lower')\n        inc_lower = subctx.create_anchor(inc_lower_ir, 'inc_lower')\n\n        inc_upper: qlast.Expr = qlast.TypeCast(\n            expr=qlast.FunctionCall(\n                func=('__std__', 'json_get'),\n                args=[\n                    source_path,\n                    qlast.Constant.string('inc_upper'),\n                ],\n                kwargs={\n                    'default': qlast.FunctionCall(\n                        func=('__std__', 'to_json'),\n                        args=[qlast.Constant.string(\"false\")],\n                    ),\n                },\n            ),\n            type=ql_bool_t,\n        )\n        inc_upper_ir = compile_with_range_element(inc_upper, 'inc_upper')\n        inc_upper = subctx.create_anchor(inc_upper_ir, 'inc_upper')\n\n        empty: qlast.Expr = qlast.TypeCast(\n            expr=qlast.FunctionCall(\n                func=('__std__', 'json_get'),\n                args=[\n                    source_path,\n                    qlast.Constant.string('empty'),\n                ],\n                kwargs={\n                    'default': qlast.FunctionCall(\n                        func=('__std__', 'to_json'),\n                        args=[qlast.Constant.string(\"false\")],\n                    ),\n                },\n            ),\n            type=ql_bool_t,\n        )\n        empty_ir = compile_with_range_element(empty, 'empty')\n        empty = subctx.create_anchor(empty_ir, 'empty')\n\n        cast = qlast.FunctionCall(\n            func=('__std__', 'range'),\n            args=[lower, upper],\n            # inc_lower and inc_upper are required to be present for\n            # non-empty casts from json, and this is checked in\n            # __range_validate_json. We still need to provide default\n            # arguments when fetching them, though, since if those\n            # arguments to range are {} it will cause {\"empty\": true}\n            # to evaluate to {}.\n            kwargs={\n                \"inc_lower\": inc_lower,\n                \"inc_upper\": inc_upper,\n                \"empty\": empty,\n            }\n        )\n\n        return dispatch.compile(cast, ctx=subctx)\n\n\ndef _cast_json_to_multirange(\n    ir_set: irast.Set,\n    orig_stype: s_types.Type,\n    new_stype: s_types.MultiRange,\n    cardinality_mod: Optional[qlast.CardinalityModifier],\n    *,\n    span: Optional[parsing.Span],\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    ctx.env.schema, new_range_type = s_types.Range.from_subtypes(\n        ctx.env.schema, new_stype.get_subtypes(ctx.env.schema))\n    ctx.env.schema, new_array_type = s_types.Array.from_subtypes(\n        ctx.env.schema, [new_range_type])\n    ql_array_range_type = typegen.type_to_ql_typeref(new_array_type, ctx=ctx)\n    with ctx.new() as subctx:\n        # We effectively want to do the following:\n        # multirange(<array<range<subtype>>>a)\n        subctx.anchors = subctx.anchors.copy()\n        source_path = subctx.create_anchor(ir_set, 'a')\n\n        cast = qlast.FunctionCall(\n            func=('__std__', 'multirange'),\n            args=[\n                qlast.TypeCast(\n                    expr=source_path,\n                    type=ql_array_range_type,\n                ),\n            ],\n        )\n\n        return dispatch.compile(cast, ctx=subctx)\n\n\ndef _cast_to_base_array(\n    ir_set: irast.Set,\n    el_stype: s_scalars.ScalarType,\n    orig_stype: s_types.Array,\n    ctx: context.ContextLevel,\n    cardinality_mod: Optional[qlast.CardinalityModifier]=None\n) -> irast.Set:\n\n    base_stype = el_stype.get_base_for_cast(ctx.env.schema)\n    assert isinstance(base_stype, s_types.Type)\n    ctx.env.schema, new_stype = s_types.Array.from_subtypes(\n        ctx.env.schema, [base_stype])\n\n    return _inheritance_cast_to_ir(\n        ir_set, orig_stype, new_stype,\n        cardinality_mod=cardinality_mod, ctx=ctx)\n\n\ndef _cast_array(\n    ir_set: irast.Set,\n    orig_stype: s_types.Type,\n    new_stype: s_types.Type,\n    *,\n    span: Optional[parsing.Span],\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    assert isinstance(orig_stype, s_types.Array)\n\n    direct_cast = _find_cast(orig_stype, new_stype, span=span, ctx=ctx)\n\n    if direct_cast is None:\n        if not new_stype.is_array():\n            raise errors.QueryError(\n                f'cannot cast {orig_stype.get_displayname(ctx.env.schema)!r} '\n                f'to {new_stype.get_displayname(ctx.env.schema)!r}',\n                span=span)\n        assert isinstance(new_stype, s_types.Array)\n        el_type = new_stype.get_subtypes(ctx.env.schema)[0]\n    elif new_stype.is_json(ctx.env.schema):\n        el_type = new_stype\n    else:\n        # We're casting an array into something that's not an array (e.g. a\n        # vector), so we don't need to match element types.\n        return _cast_to_ir(\n            ir_set, direct_cast, orig_stype, new_stype, ctx=ctx)\n\n    orig_el_type = orig_stype.get_subtypes(ctx.env.schema)[0]\n\n    el_cast = _find_cast(orig_el_type, el_type, span=span, ctx=ctx)\n\n    if el_cast is not None and el_cast.get_from_cast(ctx.env.schema):\n        # Simple cast\n        return _cast_to_ir(\n            ir_set, el_cast, orig_stype, new_stype, ctx=ctx)\n    else:\n        with ctx.new() as subctx:\n            subctx.allow_factoring()\n\n            subctx.anchors = subctx.anchors.copy()\n            source_path = subctx.create_anchor(ir_set, 'a')\n\n            unpacked = qlast.FunctionCall(\n                func=('__std__', 'array_unpack'),\n                args=[source_path],\n            )\n\n            enumerated = dispatch.compile(\n                qlast.FunctionCall(\n                    func=('__std__', 'enumerate'),\n                    args=[unpacked],\n                ),\n                ctx=subctx,\n            )\n\n            enumerated_ref = subctx.create_anchor(enumerated, 'e')\n\n            elements = qlast.FunctionCall(\n                func=('__std__', 'array_agg'),\n                args=[\n                    qlast.SelectQuery(\n                        result=qlast.TypeCast(\n                            expr=astutils.extend_path(enumerated_ref, '1'),\n                            type=typegen.type_to_ql_typeref(\n                                el_type,\n                                ctx=subctx,\n                            ),\n                            cardinality_mod=qlast.CardinalityModifier.Required,\n                            span=span,\n                        ),\n                        orderby=[\n                            qlast.SortExpr(\n                                path=astutils.extend_path(enumerated_ref, '0'),\n                                direction=qlast.SortOrder.Asc,\n                            ),\n                        ],\n                    ),\n                ],\n            )\n\n            # Force the elements to be correlated with whatever the\n            # anchor was. (Doing it this way ensures a NULL check,\n            # and just registering it in the scope would not.)\n            correlated_elements = astutils.extend_path(\n                qlast.Tuple(elements=[source_path, elements]), '1'\n            )\n            correlated_query = qlast.SelectQuery(result=correlated_elements)\n\n            if el_type.contains_json(subctx.env.schema):\n                subctx.implicit_limit = 0\n\n            array_ir = dispatch.compile(correlated_query, ctx=subctx)\n            assert isinstance(array_ir, irast.Set)\n\n            if direct_cast is not None:\n                ctx.env.schema, array_stype = s_types.Array.from_subtypes(\n                    ctx.env.schema, [el_type])\n                return _cast_to_ir(\n                    array_ir, direct_cast, array_stype, new_stype, ctx=ctx\n                )\n            else:\n                return array_ir\n\n\ndef _cast_array_literal(\n    ir_set: irast.Set,\n    orig_stype: s_types.Type,\n    new_stype: s_types.Type,\n    *,\n    span: Optional[parsing.Span],\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    assert isinstance(ir_set.expr, irast.Array)\n\n    orig_typeref = typegen.type_to_typeref(orig_stype, env=ctx.env)\n    new_typeref = typegen.type_to_typeref(new_stype, env=ctx.env)\n    direct_cast = _find_cast(orig_stype, new_stype, span=span, ctx=ctx)\n\n    if direct_cast is None:\n        if not new_stype.is_array():\n            raise errors.QueryError(\n                f'cannot cast {orig_stype.get_displayname(ctx.env.schema)!r} '\n                f'to {new_stype.get_displayname(ctx.env.schema)!r}',\n                span=span) from None\n        assert isinstance(new_stype, s_types.Array)\n        el_type = new_stype.get_subtypes(ctx.env.schema)[0]\n        intermediate_stype = orig_stype\n\n    else:\n        el_type = new_stype\n        ctx.env.schema, intermediate_stype = s_types.Array.from_subtypes(\n            ctx.env.schema, [el_type])\n\n    intermediate_typeref = typegen.type_to_typeref(\n        intermediate_stype, env=ctx.env)\n    casted_els = []\n    for el in ir_set.expr.elements:\n        el = compile_cast(el, el_type,\n                          cardinality_mod=qlast.CardinalityModifier.Required,\n                          ctx=ctx, span=span)\n        casted_els.append(el)\n\n    new_array = setgen.ensure_set(\n        irast.Array(elements=casted_els, typeref=intermediate_typeref),\n        ctx=ctx)\n\n    if direct_cast is not None:\n        return _cast_to_ir(\n            new_array, direct_cast, intermediate_stype, new_stype, ctx=ctx)\n\n    else:\n        cast_ir = irast.TypeCast(\n            expr=new_array,\n            from_type=orig_typeref,\n            to_type=new_typeref,\n            sql_cast=True,\n            sql_expr=False,\n            span=span,\n            error_message_context=cast_message_context(ctx),\n        )\n\n    return setgen.ensure_set(cast_ir, ctx=ctx)\n\n\ndef _cast_enum_str_immutable(\n    ir_expr: irast.Set | irast.Expr,\n    orig_stype: s_types.Type,\n    new_stype: s_types.Type,\n    *,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n    \"\"\"\n    Compiles cast between an enum and std::str\n    under the assumption that this expression must be immutable.\n    \"\"\"\n\n    if new_stype.is_enum(ctx.env.schema):\n        enum_stype = new_stype\n        suffix = \"_from_str\"\n    else:\n        enum_stype = orig_stype\n        suffix = \"_into_str\"\n\n    name: s_name.Name = enum_stype.get_name(ctx.env.schema)\n    name = cast(s_name.QualName, name)\n    cast_name = s_name.QualName(\n        module=name.module, name=str(enum_stype.id) + suffix\n    )\n\n    orig_typeref = typegen.type_to_typeref(orig_stype, env=ctx.env)\n    new_typeref = typegen.type_to_typeref(new_stype, env=ctx.env)\n\n    cast_ir = irast.TypeCast(\n        expr=setgen.ensure_set(ir_expr, ctx=ctx),\n        from_type=orig_typeref,\n        to_type=new_typeref,\n        cardinality_mod=None,\n        cast_name=cast_name,\n        sql_function=None,\n        sql_cast=False,\n        sql_expr=True,\n        error_message_context=cast_message_context(ctx),\n    )\n\n    return setgen.ensure_set(cast_ir, ctx=ctx)\n\n\ndef _find_object_by_id(\n    ir_expr: irast.Set | irast.Expr,\n    new_stype: s_types.Type,\n    *,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n    with ctx.new() as subctx:\n        subctx.anchors = subctx.anchors.copy()\n\n        ir_set = setgen.ensure_set(ir_expr, ctx=subctx)\n        uuid_anchor = subctx.create_anchor(ir_set, name='a')\n\n        object_name = s_utils.name_to_ast_ref(\n            new_stype.get_name(ctx.env.schema)\n        )\n\n        select_id = qlast.SelectQuery(\n            result=qlast.DetachedExpr(expr=qlast.Path(steps=[object_name])),\n            where=qlast.BinOp(\n                left=qlast.Path(\n                    steps=[qlast.Ptr(name='id', direction='>')],\n                    partial=True,\n                ),\n                op='=',\n                right=qlast.Path(steps=[qlast.ObjectRef(name='_id')]),\n            ),\n        )\n\n        error_message = qlast.BinOp(\n            left=qlast.Constant.string(\n                value=(\n                    repr(new_stype.get_displayname(ctx.env.schema))\n                    + ' with id \\''\n                )\n            ),\n            op='++',\n            right=qlast.BinOp(\n                left=qlast.TypeCast(\n                    expr=qlast.Path(steps=[qlast.ObjectRef(name='_id')]),\n                    type=qlast.TypeName(maintype=qlast.ObjectRef(name='str')),\n                ),\n                op='++',\n                right=qlast.Constant.string('\\' does not exist'),\n            ),\n        )\n\n        exists_ql = qlast.FunctionCall(\n            func='assert_exists',\n            args=[select_id],\n            kwargs={'message': error_message},\n        )\n\n        for_query = qlast.ForQuery(\n            iterator=uuid_anchor, iterator_alias='_id', result=exists_ql\n        )\n\n        return dispatch.compile(for_query, ctx=subctx)\n\n\ndef cast_message_context(ctx: context.ContextLevel) -> Optional[str]:\n    if (\n        ctx.collection_cast_info is not None\n        and ctx.collection_cast_info.path_elements\n    ):\n        from_name = (\n            ctx.collection_cast_info.from_type.get_displayname(ctx.env.schema)\n        )\n        to_name = (\n            ctx.collection_cast_info.to_type.get_displayname(ctx.env.schema)\n        )\n        path_msg = ''.join(\n            _collection_element_message_context(path_element)\n            for path_element in ctx.collection_cast_info.path_elements\n        )\n        return (\n            f\"while casting '{from_name}' to '{to_name}', {path_msg}\"\n        )\n    else:\n        return None\n\n\ndef _collection_element_message_context(\n    path_element: tuple[str, Optional[str]]\n) -> str:\n    if path_element[0] == 'tuple':\n        return f\"at tuple element '{path_element[1]}', \"\n    elif path_element[0] == 'array':\n        return f'in array elements, '\n    elif path_element[0] == 'range':\n        return f\"in range parameter '{path_element[1]}', \"\n    else:\n        raise NotImplementedError\n"
  },
  {
    "path": "edb/edgeql/compiler/clauses.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL compiler functions to process shared clauses.\"\"\"\n\n\nfrom __future__ import annotations\n\nfrom typing import Optional, Sequence\n\nfrom edb.edgeql import ast as qlast\nfrom edb.ir import ast as irast\n\nfrom edb import errors\nfrom edb.ir import utils as irutils\nfrom edb.schema import name as sn\nfrom edb.schema import operators as s_oper\n\nfrom . import context\nfrom . import dispatch\nfrom . import polyres\nfrom . import schemactx\nfrom . import setgen\nfrom . import typegen\nfrom . import pathctx\n\n\ndef compile_where_clause(\n    where: Optional[qlast.Base], *, ctx: context.ContextLevel\n) -> Optional[irast.Set]:\n\n    if where is None:\n        return None\n\n    if ctx.partial_path_prefix:\n        pathctx.register_set_in_scope(ctx.partial_path_prefix, ctx=ctx)\n\n    with ctx.newscope(fenced=True) as subctx:\n        subctx.expr_exposed = context.Exposure.UNEXPOSED\n        subctx.path_scope.unnest_fence = True\n        subctx.disallow_dml = \"in a FILTER clause\"\n        ir_expr = dispatch.compile(where, ctx=subctx)\n        bool_t = ctx.env.get_schema_type_and_track(sn.QualName('std', 'bool'))\n        ir_set = setgen.scoped_set(ir_expr, typehint=bool_t, ctx=subctx)\n\n    return ir_set\n\n\ndef adjust_nones_order(\n    ir_sortexpr: irast.Set,\n    sort: qlast.SortExpr,\n    *,\n    ctx: context.ContextLevel,\n) -> Optional[qlast.NonesOrder]:\n    if sort.nones_order:\n        return sort.nones_order\n\n    # If we are doing an ORDER BY on a required property that has an\n    # exclusive constraint and no nones_order specified, we want to\n    # defualt to EMPTY LAST (or EMPTY FIRST for DESC).  Since the\n    # property is required, this doesn't have a semantic impact, but\n    # our exclusive constraints (sigh.) use a UNIQUE constraint,\n    # which is always NULLS LAST.\n    #\n    # Postgres seems to *sometimes* be able to use the indexes without\n    # this intervention, but not always?\n    # See #8035.\n    ir = irutils.unwrap_set(ir_sortexpr)\n    expr = ir.expr\n    if (\n        isinstance(expr, irast.Pointer)\n        and expr.source == ctx.partial_path_prefix\n        and expr.dir_cardinality\n        and not expr.dir_cardinality.can_be_zero()\n        and isinstance(expr.ptrref, irast.PointerRef)\n        and (ptr := typegen.ptrcls_from_ptrref(\n            expr.ptrref, ctx=ctx,\n        ))\n        and bool(ptr.get_exclusive_constraints(ctx.env.schema))\n    ):\n        if sort.direction == qlast.SortOrder.Desc:\n            return qlast.NonesOrder.First\n        else:\n            return qlast.NonesOrder.Last\n\n    return None\n\n\ndef compile_orderby_clause(\n    sortexprs: Optional[Sequence[qlast.SortExpr]], *, ctx: context.ContextLevel\n) -> Optional[list[irast.SortExpr]]:\n\n    if not sortexprs:\n        return None\n\n    result: list[irast.SortExpr] = []\n\n    if ctx.partial_path_prefix:\n        pathctx.register_set_in_scope(ctx.partial_path_prefix, ctx=ctx)\n\n    with ctx.new() as subctx:\n        subctx.expr_exposed = context.Exposure.UNEXPOSED\n        subctx.disallow_dml = \"in an ORDER BY clause\"\n        for sortexpr in sortexprs:\n            with subctx.newscope(fenced=True) as exprctx:\n                exprctx.path_scope.unnest_fence = True\n                ir_sortexpr = dispatch.compile(sortexpr.path, ctx=exprctx)\n                ir_sortexpr = setgen.scoped_set(\n                    ir_sortexpr, force_reassign=True, ctx=exprctx)\n                ir_sortexpr.span = sortexpr.span\n\n                # Check that the sortexpr type is actually orderable\n                # with either '>' or '<' based on the DESC or ASC sort\n                # order.\n                env = exprctx.env\n                sort_type = setgen.get_set_type(ir_sortexpr, ctx=ctx)\n                # Postgres by default treats ASC as using '<' and DESC\n                # as using '>'. We should do the same.\n                if sortexpr.direction == qlast.SortDesc:\n                    op_name = '>'\n                else:\n                    op_name = '<'\n                opers = s_oper.lookup_operators(\n                    op_name,\n                    module_aliases=exprctx.modaliases,\n                    schema=env.schema\n                )\n\n                # Verify that a comparison operator is defined for 2\n                # sort_type expressions.\n                matched = polyres.find_callable(\n                    opers,\n                    args=[(sort_type, ir_sortexpr), (sort_type, ir_sortexpr)],\n                    kwargs={},\n                    ctx=exprctx)\n                if len(matched) != 1:\n                    sort_type_name = schemactx.get_material_type(\n                        sort_type, ctx=ctx).get_displayname(env.schema)\n                    if len(matched) == 0:\n                        raise errors.QueryError(\n                            f'type {sort_type_name!r} cannot be used in '\n                            f'ORDER BY clause because ordering is not '\n                            f'defined for it',\n                            span=sortexpr.span)\n\n                    elif len(matched) > 1:\n                        raise errors.QueryError(\n                            f'type {sort_type_name!r} cannot be used in '\n                            f'ORDER BY clause because ordering is '\n                            f'ambiguous for it',\n                            span=sortexpr.span)\n\n            result.append(\n                irast.SortExpr(\n                    expr=ir_sortexpr,\n                    direction=sortexpr.direction,\n                    nones_order=adjust_nones_order(\n                        ir_sortexpr,\n                        sortexpr,\n                        ctx=ctx,\n                    ),\n                ))\n\n    return result\n\n\ndef compile_limit_offset_clause(\n    expr: Optional[qlast.Base], *, ctx: context.ContextLevel\n) -> Optional[irast.Set]:\n    if expr is None:\n        ir_set = None\n    else:\n        with ctx.newscope(fenced=True) as subctx:\n            subctx.expr_exposed = context.Exposure.UNEXPOSED\n            # Clear out the partial_path_prefix, since we aren't in\n            # the scope of the select subject\n            subctx.partial_path_prefix = None\n\n            ir_expr = dispatch.compile(expr, ctx=subctx)\n            int_t = ctx.env.get_schema_type_and_track(\n                sn.QualName('std', 'int64'))\n            ir_set = setgen.scoped_set(\n                ir_expr, force_reassign=True, typehint=int_t, ctx=subctx)\n            ir_set.span = expr.span\n\n    return ir_set\n"
  },
  {
    "path": "edb/edgeql/compiler/config.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"CONFIGURE statement compilation functions.\"\"\"\n\n\nfrom __future__ import annotations\nfrom typing import Optional, NamedTuple\n\nimport json\n\nfrom edb import errors\n\nfrom edb.edgeql import qltypes\n\nfrom edb.ir import ast as irast\nfrom edb.ir import staeval as ireval\nfrom edb.ir import statypes as statypes\nfrom edb.ir import typeutils as irtyputils\n\nfrom edb.schema import constraints as s_constr\nfrom edb.schema import globals as s_globals\nfrom edb.schema import links as s_links\nfrom edb.schema import name as sn\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import schema as s_schema\nfrom edb.schema import types as s_types\nfrom edb.schema import utils as s_utils\nfrom edb.schema import expr as s_expr\n\nfrom edb.edgeql import ast as qlast\n\nfrom . import casts\nfrom . import context\nfrom . import dispatch\nfrom . import setgen\nfrom . import typegen\n\n\nclass SettingInfo(NamedTuple):\n    param_name: str\n    param_type: s_types.Type\n    cardinality: qltypes.SchemaCardinality\n    required: bool\n    requires_restart: bool\n    backend_setting: str | None\n    affects_compilation: bool\n    is_system_config: bool\n    ptr: Optional[s_pointers.Pointer]\n\n\n@dispatch.compile.register\ndef compile_ConfigSet(\n    expr: qlast.ConfigSet,\n    *,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    info = _validate_op(expr, ctx=ctx)\n    param_val = dispatch.compile(expr.expr, ctx=ctx)\n    param_type = info.param_type\n    val_type = setgen.get_set_type(param_val, ctx=ctx)\n    compatible = s_types.is_type_compatible(\n        val_type, param_type, schema=ctx.env.schema)\n    if not compatible:\n        if not val_type.assignment_castable_to(param_type, ctx.env.schema):\n            raise errors.ConfigurationError(\n                f'invalid setting value type for {info.param_name}: '\n                f'{val_type.get_displayname(ctx.env.schema)!r} '\n                f'(expecting {param_type.get_displayname(ctx.env.schema)!r})'\n            )\n        else:\n            param_val = casts.compile_cast(\n                param_val, param_type, span=None, ctx=ctx)\n\n    try:\n        if expr.scope != qltypes.ConfigScope.GLOBAL:\n            val = ireval.evaluate_to_python_val(\n                param_val, schema=ctx.env.schema)\n        else:\n            val = None\n    except ireval.UnsupportedExpressionError as e:\n        raise errors.QueryError(\n            f'non-constant expression in CONFIGURE {expr.scope} SET',\n            span=expr.expr.span\n        ) from e\n    else:\n        if isinstance(val, statypes.ScalarType) and info.backend_setting:\n            backend_expr = dispatch.compile(\n                qlast.Constant.string(val.to_backend_str()),\n                ctx=ctx,\n            )\n        else:\n            backend_expr = None\n\n    if info.ptr:\n        _enforce_pointer_constraints(\n            info.ptr, param_val, ctx=ctx, for_obj=False)\n\n    config_set = irast.ConfigSet(\n        name=info.param_name,\n        cardinality=info.cardinality,\n        required=info.required,\n        scope=expr.scope,\n        requires_restart=info.requires_restart,\n        backend_setting=info.backend_setting,\n        is_system_config=info.is_system_config,\n        span=expr.span,\n        expr=param_val,\n        backend_expr=backend_expr,\n    )\n    return setgen.ensure_set(config_set, ctx=ctx)\n\n\n@dispatch.compile.register\ndef compile_ConfigReset(\n    expr: qlast.ConfigReset,\n    *,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    info = _validate_op(expr, ctx=ctx)\n    filter_expr = expr.where\n    select_ir = None\n\n    if not info.param_type.is_object_type() and filter_expr is not None:\n        raise errors.QueryError(\n            'RESET of a primitive configuration parameter '\n            'must not have a FILTER clause',\n            span=expr.span,\n        )\n\n    elif isinstance(info.param_type, s_objtypes.ObjectType):\n        param_type_name = info.param_type.get_name(ctx.env.schema)\n        param_type_ref = qlast.ObjectRef(\n            name=param_type_name.name,\n            module=param_type_name.module,\n        )\n        body = qlast.Shape(\n            expr=qlast.Path(steps=[param_type_ref]),\n            elements=s_utils.get_config_type_shape(\n                ctx.env.schema, info.param_type, path=[param_type_ref]),\n        )\n        # The body needs to have access to secrets, since they get put\n        # into the shape and are necessary for compiling the deletion\n        # code, so compile the body in a way that we allow it.\n        # The filter should *not* be able to access secret pointers, though.\n        with ctx.new() as sctx:\n            sctx.current_schema_views += (info.param_type,)\n            body_ir = dispatch.compile(body, ctx=sctx)\n\n        with ctx.new() as sctx:\n            sctx.anchors = sctx.anchors.copy()\n            select = qlast.SelectQuery(\n                result=sctx.create_anchor(body_ir, 'a'),\n                where=filter_expr,\n            )\n\n            sctx.modaliases = ctx.modaliases.copy()\n            sctx.modaliases[None] = 'cfg'\n            select_ir = setgen.ensure_set(\n                dispatch.compile(select, ctx=sctx), ctx=sctx)\n\n    config_reset = irast.ConfigReset(\n        name=info.param_name,\n        cardinality=info.cardinality,\n        scope=expr.scope,\n        requires_restart=info.requires_restart,\n        backend_setting=info.backend_setting,\n        is_system_config=info.is_system_config,\n        span=expr.span,\n        selector=select_ir,\n    )\n    return setgen.ensure_set(config_reset, ctx=ctx)\n\n\n@dispatch.compile.register\ndef compile_ConfigInsert(\n    expr: qlast.ConfigInsert, *, ctx: context.ContextLevel\n) -> irast.Set:\n\n    info = _validate_op(expr, ctx=ctx)\n\n    if expr.scope not in (\n        qltypes.ConfigScope.INSTANCE, qltypes.ConfigScope.DATABASE\n    ):\n        raise errors.UnsupportedFeatureError(\n            f'CONFIGURE {expr.scope} INSERT is not supported'\n        )\n\n    subject = info.param_type\n    insert_stmt = qlast.InsertQuery(\n        subject=s_utils.name_to_ast_ref(subject.get_name(ctx.env.schema)),\n        shape=expr.shape,\n    )\n\n    _inject_tname(insert_stmt, ctx=ctx)\n\n    with ctx.newscope(fenced=False) as subctx:\n        subctx.expr_exposed = context.Exposure.EXPOSED\n        subctx.modaliases = ctx.modaliases.copy()\n        subctx.modaliases[None] = 'cfg'\n        subctx.special_computables_in_mutation_shape |= {'_tname'}\n        insert_ir = dispatch.compile(insert_stmt, ctx=subctx)\n        insert_ir_set = setgen.ensure_set(insert_ir, ctx=subctx)\n        assert isinstance(insert_ir_set.expr, irast.InsertStmt)\n        insert_subject = insert_ir_set.expr.subject\n\n        _validate_config_object(insert_subject, scope=expr.scope, ctx=subctx)\n\n    return setgen.ensure_set(\n        irast.ConfigInsert(\n            name=info.param_name,\n            cardinality=info.cardinality,\n            scope=expr.scope,\n            requires_restart=info.requires_restart,\n            backend_setting=info.backend_setting,\n            is_system_config=info.is_system_config,\n            expr=insert_subject,\n            span=expr.span,\n        ),\n        ctx=ctx,\n    )\n\n\ndef _inject_tname(\n    insert_stmt: qlast.InsertQuery, *, ctx: context.ContextLevel\n) -> None:\n\n    for el in insert_stmt.shape:\n        if isinstance(el.compexpr, qlast.InsertQuery):\n            _inject_tname(el.compexpr, ctx=ctx)\n\n    assert isinstance(insert_stmt.subject, qlast.BaseObjectRef)\n    insert_stmt.shape.append(\n        qlast.ShapeElement(\n            expr=qlast.Path(\n                steps=[qlast.Ptr(name='_tname')],\n            ),\n            compexpr=qlast.Path(\n                steps=[\n                    qlast.Introspect(\n                        type=qlast.TypeName(\n                            maintype=insert_stmt.subject,\n                        ),\n                    ),\n                    qlast.Ptr(name='name'),\n                ],\n            ),\n        ),\n    )\n\n\ndef _validate_config_object(\n    expr: irast.Set, *, scope: str, ctx: context.ContextLevel\n) -> None:\n\n    for element, _ in expr.shape:\n        assert isinstance(element.expr, irast.Pointer)\n        if element.expr.ptrref.shortname.name == 'id':\n            continue\n\n        ptr = typegen.ptrcls_from_ptrref(\n            element.expr.ptrref.real_material_ptr,\n            ctx=ctx,\n        )\n        if isinstance(ptr, s_pointers.Pointer):\n            _enforce_pointer_constraints(\n                ptr, element, ctx=ctx, for_obj=True)\n\n        if (irtyputils.is_object(element.typeref)\n                and isinstance(element.expr, irast.InsertStmt)):\n            _validate_config_object(element, scope=scope, ctx=ctx)\n\n\ndef _validate_global_op(\n    expr: qlast.ConfigOp, *, ctx: context.ContextLevel\n) -> SettingInfo:\n    glob_name = s_utils.ast_ref_to_name(expr.name)\n    glob = ctx.env.get_schema_object_and_track(\n        glob_name, expr.name,\n        modaliases=ctx.modaliases, type=s_globals.Global)\n    assert isinstance(glob, s_globals.Global)\n\n    fullname = glob.get_name(ctx.env.schema)\n    if sn.UnqualName(fullname.module) in s_schema.STD_MODULES:\n        raise errors.ConfigurationError(\n            f\"system global '{glob_name}' may not be explicitly specified\",\n            span=expr.name.span\n        )\n\n    if isinstance(expr, (qlast.ConfigSet, qlast.ConfigReset)):\n        if glob.get_expr(ctx.env.schema):\n            raise errors.ConfigurationError(\n                f\"global '{glob_name}' is computed from an expression and \"\n                f\"cannot be modified\",\n                span=expr.name.span\n            )\n\n    param_type = glob.get_target(ctx.env.schema)\n\n    return SettingInfo(param_name=str(glob.get_name(ctx.env.schema)),\n                       param_type=param_type,\n                       cardinality=glob.get_cardinality(ctx.env.schema),\n                       required=glob.get_required(ctx.env.schema),\n                       requires_restart=False,\n                       backend_setting=None,\n                       is_system_config=False,\n                       affects_compilation=False,\n                       ptr=None)\n\n\ndef _enforce_pointer_constraints(\n    ptr: s_pointers.Pointer,\n    expr: irast.Set,\n    *,\n    ctx: context.ContextLevel,\n    for_obj: bool,\n) -> None:\n    constraints = ptr.get_constraints(ctx.env.schema)\n    for constraint in constraints.objects(ctx.env.schema):\n        if constraint.issubclass(\n            ctx.env.schema,\n            ctx.env.schema.get('std::exclusive', type=s_constr.Constraint),\n        ):\n            continue\n\n        with ctx.detached() as sctx:\n            sctx.partial_path_prefix = expr\n            sctx.anchors = ctx.anchors.copy()\n            sctx.anchors['__subject__'] = expr\n\n            final_expr: Optional[s_expr.Expression] = (\n                constraint.get_finalexpr(ctx.env.schema)\n            )\n            assert final_expr is not None and final_expr.parse() is not None\n            ir = dispatch.compile(final_expr.parse(), ctx=sctx)\n\n        result = ireval.evaluate(ir, schema=ctx.env.schema)\n        assert isinstance(result, irast.BooleanConstant)\n        if result.value != 'true':\n            if for_obj:\n                name = ptr.get_verbosename(ctx.env.schema, with_parent=True)\n            else:\n                name = repr(ptr.get_shortname(ctx.env.schema).name)\n            raise errors.ConfigurationError(\n                f'invalid setting value for {name}'\n            )\n\n\ndef _validate_op(\n    expr: qlast.ConfigOp, *, ctx: context.ContextLevel\n) -> SettingInfo:\n\n    if expr.scope == qltypes.ConfigScope.GLOBAL:\n        return _validate_global_op(expr, ctx=ctx)\n\n    cfg_host_type = None\n    is_ext_config = False\n    if expr.name.module:\n        cfg_host_name = sn.name_from_string(expr.name.module)\n        cfg_host_type = ctx.env.get_schema_type_and_track(\n            cfg_host_name, default=None)\n        is_ext_config = bool(cfg_host_type)\n\n    abstract_config = ctx.env.get_schema_type_and_track(\n        sn.QualName('cfg', 'AbstractConfig'))\n    ext_config = ctx.env.get_schema_type_and_track(\n        sn.QualName('cfg', 'ExtensionConfig'))\n\n    if not cfg_host_type:\n        cfg_host_type = abstract_config\n\n    name = fullname = expr.name.name\n    if is_ext_config:\n        fullname = f'{cfg_host_type.get_name(ctx.env.schema)}::{name}'\n\n    assert isinstance(cfg_host_type, s_objtypes.ObjectType)\n    cfg_type = None\n    ptr = None\n\n    if isinstance(expr, (qlast.ConfigSet, qlast.ConfigReset)):\n        if is_ext_config and expr.scope == qltypes.ConfigScope.INSTANCE:\n            raise errors.ConfigurationError(\n                'INSTANCE configuration of extension-defined config variables '\n                'is not allowed'\n            )\n\n        # expr.name is the actual name of the property.\n        ptr = cfg_host_type.maybe_get_ptr(ctx.env.schema, sn.UnqualName(name))\n        if ptr is not None:\n            cfg_type = ptr.get_target(ctx.env.schema)\n\n    if cfg_type is None:\n        if isinstance(expr, qlast.ConfigSet):\n            raise errors.ConfigurationError(\n                f'unrecognized configuration parameter {name!r}',\n                span=expr.span\n            )\n\n        cfg_type = ctx.env.get_schema_type_and_track(\n            s_utils.ast_ref_to_name(expr.name), default=None)\n        if not cfg_type and not expr.name.module:\n            # expr.name is the name of the configuration type\n            cfg_type = ctx.env.get_schema_type_and_track(\n                sn.QualName('cfg', name), default=None)\n        if not cfg_type:\n            raise errors.ConfigurationError(\n                f'unrecognized configuration object {name!r}',\n                span=expr.span\n            )\n\n        assert isinstance(cfg_type, s_objtypes.ObjectType)\n        ptr_candidate: Optional[s_pointers.Pointer] = None\n\n        mro = [cfg_type] + list(\n            cfg_type.get_ancestors(ctx.env.schema).objects(ctx.env.schema))\n        for ct in mro:\n            ptrs = ctx.env.schema.get_referrers(\n                ct, scls_type=s_links.Link, field_name='target')\n\n            if ptrs:\n                pointer_link = next(iter(ptrs))\n                assert isinstance(pointer_link, s_links.Link)\n                ptr_candidate = pointer_link\n                break\n\n        if (\n            ptr_candidate is None\n            or (ptr_source := ptr_candidate.get_source(ctx.env.schema)) is None\n            or not ptr_source.issubclass(\n                ctx.env.schema, (abstract_config, ext_config))\n        ):\n            raise errors.ConfigurationError(\n                f'{name!r} cannot be configured directly'\n            )\n\n        ptr = ptr_candidate\n\n        fullname = ptr.get_shortname(ctx.env.schema).name\n        if ptr_source.issubclass(ctx.env.schema, ext_config):\n            fullname = f'{ptr_source.get_name(ctx.env.schema)}::{fullname}'\n\n    assert isinstance(ptr, s_pointers.Pointer)\n\n    sys_attr = ptr.get_annotations(ctx.env.schema).get(\n        ctx.env.schema, sn.QualName('cfg', 'system'), None)\n\n    system = (\n        sys_attr is not None\n        and sys_attr.get_value(ctx.env.schema) == 'true'\n    )\n\n    cardinality = ptr.get_cardinality(ctx.env.schema)\n    assert cardinality is not None\n\n    restart_attr = ptr.get_annotations(ctx.env.schema).get(\n        ctx.env.schema, sn.QualName('cfg', 'requires_restart'), None)\n\n    requires_restart = (\n        restart_attr is not None\n        and restart_attr.get_value(ctx.env.schema) == 'true'\n    )\n\n    backend_attr = ptr.get_annotations(ctx.env.schema).get(\n        ctx.env.schema, sn.QualName('cfg', 'backend_setting'), None)\n\n    if backend_attr is not None:\n        backend_setting = json.loads(backend_attr.get_value(ctx.env.schema))\n    else:\n        backend_setting = None\n\n    system_attr = ptr.get_annotations(ctx.env.schema).get(\n        ctx.env.schema, sn.QualName('cfg', 'system'), None)\n\n    is_system_config = (\n        system_attr is not None\n        and system_attr.get_value(ctx.env.schema) == 'true'\n    )\n\n    compilation_attr = ptr.get_annotations(ctx.env.schema).get(\n        ctx.env.schema, sn.QualName('cfg', 'affects_compilation'), None)\n\n    if compilation_attr is not None:\n        affects_compilation = (\n            json.loads(compilation_attr.get_value(ctx.env.schema)))\n    else:\n        affects_compilation = False\n\n    if system and expr.scope is not qltypes.ConfigScope.INSTANCE:\n        raise errors.ConfigurationError(\n            f'{name!r} is a system-level configuration parameter; '\n            f'use \"CONFIGURE INSTANCE\"')\n\n    return SettingInfo(param_name=fullname,\n                       param_type=cfg_type,\n                       cardinality=cardinality,\n                       required=False,\n                       requires_restart=requires_restart,\n                       backend_setting=backend_setting,\n                       is_system_config=is_system_config,\n                       affects_compilation=affects_compilation,\n                       ptr=ptr)\n"
  },
  {
    "path": "edb/edgeql/compiler/config_desc.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Implementation of DESCRIBE ... CONFIG\"\"\"\n\nfrom __future__ import annotations\n\nimport textwrap\n\nfrom edb.edgeql import parser as qlparser\nfrom edb.edgeql import qltypes\nfrom edb.edgeql import quote as qlquote\n\nfrom . import context\nfrom . import dispatch\n\nfrom edb.ir import ast as irast\n\nfrom edb.schema import name as s_name\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import schema as s_schema\nfrom edb.schema import types as s_types\n\nfrom edb.pgsql import common\n\nql = common.quote_literal\n\n\ndef compile_describe_config(\n    scope: qltypes.ConfigScope, ctx: context.ContextLevel\n) -> irast.Set:\n    config_edgeql = _describe_config(\n        ctx.env.schema, scope, ctx.env.options.testmode)\n    config_ast = qlparser.parse_fragment(config_edgeql)\n\n    with ctx.new() as subctx:\n        subctx.allow_factoring()\n        return dispatch.compile(config_ast, ctx=subctx)\n\n\ndef _describe_config(\n    schema: s_schema.Schema,\n    scope: qltypes.ConfigScope,\n    testmode: bool,\n) -> str:\n    \"\"\"Generate an EdgeQL query to render config as DDL.\"\"\"\n\n    if scope is qltypes.ConfigScope.INSTANCE:\n        source = 'system override'\n        config_object_name = 'cfg::InstanceConfig'\n    elif scope is qltypes.ConfigScope.DATABASE:\n        source = 'database'\n        config_object_name = 'cfg::DatabaseConfig'\n    else:\n        raise AssertionError(f'unexpected configuration source: {scope!r}')\n\n    cfg = schema.get(config_object_name, type=s_objtypes.ObjectType)\n    items = []\n    items.extend(_describe_config_inner(\n        schema, scope, config_object_name, cfg, testmode\n    ))\n    ext = schema.get('cfg::ExtensionConfig', type=s_objtypes.ObjectType)\n    for ext_cfg in sorted(\n        ext.descendants(schema), key=lambda x: x.get_name(schema)\n    ):\n        items.extend(_describe_config_inner(\n            schema, scope, config_object_name, ext_cfg, testmode\n        ))\n\n    testmode_check = (\n        \"<bool>json_get(cfg::get_config_json(),'__internal_testmode','value')\"\n        \" ?? false\"\n    )\n    query = (\n        \"assert_exists(assert_single((\"\n        + f\"FOR conf IN {{cfg::get_config_json(sources := [{ql(source)}])}} \"\n        + \"UNION (\\n\"\n        + (f\"FOR testmode IN {{{testmode_check}}} UNION (\\n\"\n           if testmode else \"\")\n        + \"SELECT array_join([\" + ', '.join(items) + \"], '')\"\n        + (\")\" if testmode else \"\")\n        + \")\"\n        + \")))\"\n    )\n    return query\n\n\ndef _describe_config_inner(\n    schema: s_schema.Schema,\n    scope: qltypes.ConfigScope,\n    config_object_name: str,\n    cfg: s_objtypes.ObjectType,\n    testmode: bool,\n) -> list[str]:\n    \"\"\"Generate an EdgeQL query to render config as DDL.\"\"\"\n\n    actual_name = str(cfg.get_name(schema))\n    cast = (\n        f'.extensions[is {actual_name}]' if actual_name != config_object_name\n        else ''\n    )\n\n    items = []\n    for ptr_name, p in sorted(\n        cfg.get_pointers(schema).items(schema),\n        key=lambda x: x[0],\n    ):\n        pn = str(ptr_name)\n        if (\n            pn == 'id'\n            or p.get_computable(schema)\n            or p.get_protected(schema)\n        ):\n            continue\n\n        is_internal = (\n            p.get_annotation(\n                schema,\n                s_name.QualName('cfg', 'internal')\n            ) == 'true'\n        )\n        if is_internal and not testmode:\n            continue\n\n        ptype = p.get_target(schema)\n        assert ptype is not None\n\n        # Skip backlinks to the base object. The will get plenty of\n        # special treatment.\n        if str(ptype.get_name(schema)) == 'cfg::AbstractConfig':\n            continue\n\n        ptr_card = p.get_cardinality(schema)\n        mult = ptr_card.is_multi()\n        psource = f'{config_object_name}{cast}.{qlquote.quote_ident(pn)}'\n        if isinstance(ptype, s_objtypes.ObjectType):\n            item = textwrap.indent(\n                _render_config_object(\n                    schema=schema,\n                    valtype=ptype,\n                    value_expr=psource,\n                    scope=scope,\n                    join_term='',\n                    level=1,\n                ),\n                ' ' * 4,\n            )\n        else:\n            fn = (\n                pn if actual_name == config_object_name\n                else f'{actual_name}::{pn}'\n            )\n            renderer = (\n                _render_config_redacted if p.get_secret(schema)\n                else _render_config_set if mult\n                else _render_config_scalar\n            )\n            item = textwrap.indent(\n                renderer(\n                    schema=schema,\n                    valtype=ptype,\n                    value_expr=psource,\n                    name=fn,\n                    scope=scope,\n                    level=1,\n                ),\n                ' ' * 4,\n            )\n\n        fpn = f'{actual_name}::{pn}' if cast else pn\n\n        condition = f'EXISTS json_get(conf, {ql(fpn)})'\n        if is_internal:\n            condition = f'({condition}) AND testmode'\n        # For INSTANCE, filter out configs that are set to the default.\n        # This is because we currently implement the defaults by\n        # setting them with CONFIGURE INSTANCE, so we can't detect\n        # defaults by seeing what is unset.\n        if (\n            scope == qltypes.ConfigScope.INSTANCE\n            and (default := p.get_default(schema))\n        ):\n            condition = f'({condition}) AND {psource} ?!= ({default.text})'\n\n        items.append(f\"(\\n{item}\\n    IF {condition} ELSE ''\\n  )\")\n\n    return items\n\n\ndef _render_config_value(\n    *,\n    schema: s_schema.Schema,\n    valtype: s_types.Type,\n    value_expr: str,\n) -> str:\n    if valtype.issubclass(\n        schema,\n        schema.get('std::anyreal', type=s_scalars.ScalarType),\n    ):\n        val = f'<str>{value_expr}'\n    elif valtype.issubclass(\n        schema,\n        schema.get('std::bool', type=s_scalars.ScalarType),\n    ):\n        val = f'<str>{value_expr}'\n    elif valtype.issubclass(\n        schema,\n        schema.get('std::duration', type=s_scalars.ScalarType),\n    ):\n        val = f'\"<std::duration>\" ++ cfg::_quote(<str>{value_expr})'\n    elif valtype.issubclass(\n        schema,\n        schema.get('cfg::memory', type=s_scalars.ScalarType),\n    ):\n        val = f'\"<cfg::memory>\" ++ cfg::_quote(<str>{value_expr})'\n    elif valtype.issubclass(\n        schema,\n        schema.get('std::str', type=s_scalars.ScalarType),\n    ):\n        val = f'cfg::_quote({value_expr})'\n    elif valtype.is_enum(schema):\n        tn = valtype.get_name(schema)\n        val = f'\"<{str(tn)}>\" ++ cfg::_quote(<str>{value_expr})'\n    else:\n        raise AssertionError(\n            f'unexpected configuration value type: '\n            f'{valtype.get_displayname(schema)}'\n        )\n\n    return val\n\n\ndef _render_config_redacted(\n    *,\n    schema: s_schema.Schema,\n    valtype: s_types.Type,\n    value_expr: str,\n    scope: qltypes.ConfigScope,\n    name: str,\n    level: int,\n) -> str:\n    if level == 1:\n        return (\n            f\"'CONFIGURE {scope.to_edgeql()} \"\n            f\"SET {qlquote.quote_ident(name)} := {{}};  # REDACTED\\\\n'\"\n        )\n    else:\n        indent = ' ' * (4 * (level - 1))\n        return f\"'{indent}{qlquote.quote_ident(name)} := {{}},  # REDACTED'\"\n\n\ndef _render_config_set(\n    *,\n    schema: s_schema.Schema,\n    valtype: s_types.Type,\n    value_expr: str,\n    scope: qltypes.ConfigScope,\n    name: str,\n    level: int,\n) -> str:\n    assert isinstance(valtype, s_scalars.ScalarType)\n    v = _render_config_value(\n        schema=schema, valtype=valtype, value_expr=value_expr)\n    if level == 1:\n        return (\n            f\"'CONFIGURE {scope.to_edgeql()} \"\n            f\"SET {qlquote.quote_ident(name)} := {{' ++ \"\n            f\"array_join(array_agg((select _ := {v} order by _)), ', ') \"\n            f\"++ '}};\\\\n'\"\n        )\n    else:\n        indent = ' ' * (4 * (level - 1))\n        return (\n            f\"'{indent}{qlquote.quote_ident(name)} := {{' ++ \"\n            f\"array_join(array_agg((SELECT _ := {v} ORDER BY _)), ', ') \"\n            f\"++ '}},'\"\n        )\n\n\ndef _render_config_scalar(\n    *,\n    schema: s_schema.Schema,\n    valtype: s_types.Type,\n    value_expr: str,\n    scope: qltypes.ConfigScope,\n    name: str,\n    level: int,\n) -> str:\n    assert isinstance(valtype, s_scalars.ScalarType)\n    v = _render_config_value(\n        schema=schema, valtype=valtype, value_expr=value_expr)\n    if level == 1:\n        return (\n            f\"'CONFIGURE {scope.to_edgeql()} \"\n            f\"SET {qlquote.quote_ident(name)} := ' ++ {v} ++ ';\\\\n'\"\n        )\n    else:\n        indent = ' ' * (4 * (level - 1))\n        return f\"'{indent}{qlquote.quote_ident(name)} := ' ++ {v} ++ ','\"\n\n\ndef _render_config_object(\n    *,\n    schema: s_schema.Schema,\n    valtype: s_objtypes.ObjectType,\n    value_expr: str,\n    scope: qltypes.ConfigScope,\n    join_term: str,\n    level: int,\n) -> str:\n    # Generate a valid `CONFIGURE <SCOPE> INSERT ConfigObject`\n    # shape for a given configuration object type or\n    # `INSERT ConfigObject` for a nested configuration type.\n    sub_layouts = _describe_config_object(\n        schema=schema, valtype=valtype, level=level + 1, scope=scope)\n    sub_layouts_items = []\n    if level == 1:\n        decor = [f'CONFIGURE {scope.to_edgeql()} INSERT ', ';\\\\n']\n    else:\n        decor = ['(INSERT ', ')']\n\n    indent = ' ' * (4 * (level - 1))\n\n    for type_name, type_layout in sub_layouts.items():\n        if type_layout:\n            sub_layout_item = (\n                f\"'{indent}{decor[0]}{type_name} {{\\\\n'\\n++ \"\n                + \"\\n++ \".join(type_layout)\n                + f\" ++ '{indent}}}{decor[1]}'\"\n            )\n        else:\n            sub_layout_item = (\n                f\"'{indent}{decor[0]}{type_name}{decor[1]}'\"\n            )\n\n        if len(sub_layouts) > 1:\n            if type_layout:\n                sub_layout_item = (\n                    f'(WITH item := item[IS {type_name}]'\n                    f' SELECT {sub_layout_item}) '\n                    f'IF item.__type__.name = {ql(str(type_name))}'\n                )\n            else:\n                sub_layout_item = (\n                    f'{sub_layout_item} '\n                    f'IF item.__type__.name = {ql(str(type_name))}'\n                )\n\n        sub_layouts_items.append(sub_layout_item)\n\n    if len(sub_layouts_items) > 1:\n        sli_render = '\\nELSE '.join(sub_layouts_items) + \"\\nELSE ''\"\n    else:\n        sli_render = sub_layouts_items[0]\n\n    return '\\n'.join((\n        f\"array_join(array_agg((SELECT _ := (\",\n        f\"  FOR item IN {{ {value_expr} }}\",\n        f\"  UNION (\",\n        f\"{textwrap.indent(sli_render, ' ' * 4)}\",\n        f\"  )\",\n        f\") ORDER BY _)), {ql(join_term)})\",\n    ))\n\n\ndef _describe_config_object(\n    *,\n    schema: s_schema.Schema,\n    valtype: s_objtypes.ObjectType,\n    level: int,\n    scope: qltypes.ConfigScope,\n) -> dict[s_name.QualName, list[str]]:\n    cfg_types = [valtype]\n    cfg_types.extend(cfg_types[0].descendants(schema))\n    layouts = {}\n    for cfg in cfg_types:\n        items = []\n        for ptr_name, p in sorted(\n            cfg.get_pointers(schema).items(schema),\n            key=lambda x: x[0],\n        ):\n            pn = str(ptr_name)\n            if (\n                pn == 'id'\n                or p.get_protected(schema)\n                or p.get_annotation(\n                    schema,\n                    s_name.QualName('cfg', 'internal'),\n                ) == 'true'\n            ):\n                continue\n\n            ptype = p.get_target(schema)\n            assert ptype is not None\n            if str(ptype.get_name(schema)) == 'cfg::AbstractConfig':\n                continue\n\n            ptr_card = p.get_cardinality(schema)\n            mult = ptr_card.is_multi()\n            psource = f'item.{qlquote.quote_ident(pn)}'\n\n            if isinstance(ptype, s_objtypes.ObjectType):\n                rval = textwrap.indent(\n                    _render_config_object(\n                        schema=schema,\n                        valtype=ptype,\n                        value_expr=psource,\n                        scope=scope,\n                        join_term=' UNION ',\n                        level=level + 1,\n                    ),\n                    ' ' * 2,\n                ).strip()\n                indent = ' ' * (4 * (level - 1))\n                item = (\n                    f\"'{indent}{qlquote.quote_ident(pn)} \"\n                    f\":= (\\\\n'\\n++ {rval} ++ '\\\\n{indent}),\\\\n'\"\n                )\n                condition = None\n            else:\n                render = (\n                    _render_config_redacted if p.get_secret(schema)\n                    else _render_config_set if mult\n                    else _render_config_scalar\n                )\n                item = render(\n                    schema=schema,\n                    valtype=ptype,\n                    value_expr=psource,\n                    scope=scope,\n                    name=pn,\n                    level=level,\n                )\n                if p.get_secret(schema):\n                    condition = 'true'\n                else:\n                    condition = f'EXISTS {psource}'\n\n            if condition is not None:\n                item = f\"({item} ++ '\\\\n' IF {condition} ELSE '')\"\n\n            items.append(item)\n\n        layouts[cfg.get_name(schema)] = items\n\n    return layouts\n"
  },
  {
    "path": "edb/edgeql/compiler/conflicts.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Compilation of DML exclusive constraint conflict handling.\"\"\"\n\n\nfrom __future__ import annotations\nfrom typing import Optional, Iterable, Sequence\n\nfrom edb import errors\n\nfrom edb.ir import ast as irast\nfrom edb.ir import utils as irutils\nfrom edb.ir import typeutils\n\nfrom edb.schema import constraints as s_constr\nfrom edb.schema import name as s_name\nfrom edb.schema import links as s_links\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import utils as s_utils\nfrom edb.schema import expr as s_expr\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import utils as qlutils\nfrom edb.edgeql import qltypes\n\nfrom . import astutils\nfrom . import context\nfrom . import dispatch\nfrom . import inference\nfrom . import pathctx\nfrom . import schemactx\nfrom . import setgen\nfrom . import typegen\n\n\ndef _get_needed_ptrs(\n    subject_typ: s_objtypes.ObjectType,\n    obj_constrs: Sequence[s_constr.Constraint],\n    initial_ptrs: Iterable[str],\n    rewrite_kind: Optional[qltypes.RewriteKind],\n    ctx: context.ContextLevel,\n) -> tuple[set[str], dict[str, qlast.Expr]]:\n    \"\"\"Find all the pointers needed by a list of constraints and pointers.\n\n    This chases down computed pointer definitions, rewrites, and\n    constraint expressions.\n    \"\"\"\n    needed_ptrs = set(initial_ptrs)\n    for constr in obj_constrs:\n        subjexpr: Optional[s_expr.Expression] = (\n            constr.get_subjectexpr(ctx.env.schema)\n        )\n        assert subjexpr\n        needed_ptrs |= qlutils.find_subject_ptrs(subjexpr.parse())\n        if except_expr := constr.get_except_expr(ctx.env.schema):\n            assert isinstance(except_expr, s_expr.Expression)\n            needed_ptrs |= qlutils.find_subject_ptrs(except_expr.parse())\n\n    wl = list(needed_ptrs)\n    ptr_anchors = {}\n    while wl:\n        p = wl.pop()\n        ptr = subject_typ.getptr(ctx.env.schema, s_name.UnqualName(p))\n        exprs = []\n        if expr := ptr.get_expr(ctx.env.schema):\n            exprs.append(expr)\n        if rewrite_kind and (\n            rewrite := ptr.get_rewrite(ctx.env.schema, rewrite_kind)\n        ):\n            exprs.append(rewrite.get_expr(ctx.env.schema))\n        for expr in exprs:\n            assert isinstance(expr.parse(), qlast.Expr)\n            ptr_anchors[p] = expr.parse()\n            for ref in qlutils.find_subject_ptrs(expr.parse()):\n                if ref not in needed_ptrs:\n                    wl.append(ref)\n                    needed_ptrs.add(ref)\n\n    return needed_ptrs, ptr_anchors\n\n\ndef _get_rewrite_kind(stmt: irast.MutatingStmt) -> qltypes.RewriteKind | None:\n    return (\n        qltypes.RewriteKind.Insert\n        if isinstance(stmt, irast.InsertStmt)\n        else qltypes.RewriteKind.Update\n        if isinstance(stmt, irast.UpdateStmt)\n        else None\n    )\n\n\ndef _get_rewritten_ptrs(\n    stmt: irast.MutatingStmt,\n    subject_typ: s_objtypes.ObjectType,\n    *,\n    ctx: context.ContextLevel,\n) -> set[str]:\n    schema = ctx.env.schema\n    rewrite_kind = _get_rewrite_kind(stmt)\n\n    rewritten = set()\n    for ptr in subject_typ.get_pointers(schema).objects(schema):\n        if rewrite_kind:\n            rewrite = ptr.get_rewrite(ctx.env.schema, rewrite_kind)\n            if rewrite:\n                rewritten.add(ptr.get_shortname(schema).name)\n\n    return rewritten\n\n\ndef _compile_conflict_select_for_obj_type(\n    stmt: irast.MutatingStmt,\n    subject_typ: s_objtypes.ObjectType,\n    *,\n    for_inheritance: bool,\n    fake_dml_set: Optional[irast.Set],\n    obj_constrs: Sequence[s_constr.Constraint],\n    constrs: dict[str, tuple[s_pointers.Pointer, list[s_constr.Constraint]]],\n    span: Optional[irast.Span],\n    ctx: context.ContextLevel,\n) -> tuple[Optional[qlast.Expr], bool]:\n    \"\"\"Synthesize a select of conflicting objects\n\n    ... for a single object type. This gets called once for each ancestor\n    type that provides constraints to the type being inserted.\n\n    `cnstrs` contains the constraints to consider.\n    \"\"\"\n    # We have a fake_dml_set to represent the root exactly when we are\n    # compiling this for inheritance checking reasons (and not for\n    # real UNLESS CONFLICTs)\n    assert for_inheritance == bool(fake_dml_set)\n\n    rewrite_kind = _get_rewrite_kind(stmt)\n\n    # Find which pointers we need to grab\n    needed_ptrs, ptr_anchors = _get_needed_ptrs(\n        subject_typ, obj_constrs, constrs.keys(), rewrite_kind, ctx=ctx\n    )\n\n    # Check that no pointers in constraints are rewritten\n    if rewrite_kind and not fake_dml_set:\n        for p in needed_ptrs:\n            ptr = subject_typ.getptr(ctx.env.schema, s_name.UnqualName(p))\n            rewrite = ptr.get_rewrite(ctx.env.schema, rewrite_kind)\n            if rewrite:\n                raise errors.UnsupportedFeatureError(\n                    \"INSERT UNLESS CONFLICT cannot be used on properties or \"\n                    \"links that have a rewrite rule specified\",\n                    span=span,\n                )\n\n    ctx.anchors = ctx.anchors.copy()\n\n    # If we are given a fake_dml_set to directly represent the result\n    # of our DML, use that instead of populating the result.\n    # TODO: XXX: always use fake_dml_set??\n    # (would we need to still disallow MUTATING properties?)\n    if fake_dml_set:\n        for p in needed_ptrs | {'id', '__type__'}:\n            ptr = subject_typ.getptr(ctx.env.schema, s_name.UnqualName(p))\n            val = setgen.extend_path(fake_dml_set, ptr, ctx=ctx)\n\n            ptr_anchors[p] = ctx.create_anchor(val, p)\n\n    # Find the IR corresponding to the fields we care about and\n    # produce anchors for them\n    ptrs_in_shape = set()\n    for elem, _ in stmt.subject.shape:\n        rptr = elem.expr\n        name = rptr.ptrref.shortname.name\n        ptrs_in_shape.add(name)\n        if name in needed_ptrs and name not in ptr_anchors:\n            assert rptr.expr\n            # We don't properly support hoisting volatile properties out of\n            # UNLESS CONFLICT, so disallow it. We *do* support handling DML\n            # there, since that gets hoisted into CTEs via its own mechanism.\n            # See issue #1699.\n            if inference.infer_volatility(\n                rptr.expr, ctx.env, exclude_dml=True\n            ).is_volatile():\n                assert not for_inheritance\n                raise errors.UnsupportedFeatureError(\n                    'INSERT UNLESS CONFLICT ON does not support volatile '\n                    'properties',\n                    span=span,\n                )\n\n            # We want to use the same path_scope_id as the original\n            elem_set = setgen.ensure_set(rptr.expr, ctx=ctx)\n            elem_set.path_scope_id = elem.path_scope_id\n\n            # FIXME: The wrong thing will definitely happen if there are\n            # volatile entries here\n            ptr_anchors[name] = ctx.create_anchor(elem_set, name)\n\n    if for_inheritance and not ptrs_in_shape:\n        return None, False\n\n    # Fill in empty sets for pointers that are needed but not present\n    present_ptrs = set(ptr_anchors)\n    for p in (needed_ptrs - present_ptrs):\n        ptr = subject_typ.getptr(ctx.env.schema, s_name.UnqualName(p))\n        typ = ptr.get_target(ctx.env.schema)\n        assert typ\n        ptr_anchors[p] = qlast.TypeCast(\n            expr=qlast.Set(elements=[]),\n            type=typegen.type_to_ql_typeref(typ, ctx=ctx))\n\n    if not ptr_anchors:\n        raise errors.QueryError(\n            'INSERT UNLESS CONFLICT property requires matching shape',\n            span=span,\n        )\n\n    conds: list[qlast.Expr] = []\n    for ptrname, (ptr, ptr_cnstrs) in constrs.items():\n        if ptrname not in present_ptrs:\n            continue\n        anchor = qlutils.subject_paths_substitute(\n            ptr_anchors[ptrname], ptr_anchors)\n        ptr_val = qlast.Path(partial=True, steps=[\n            qlast.Ptr(name=ptrname)\n        ])\n        ptr, ptr_cnstrs = constrs[ptrname]\n        ptr_card = ptr.get_cardinality(ctx.env.schema)\n\n        for cnstr in ptr_cnstrs:\n            lhs: qlast.Expr = anchor\n            rhs: qlast.Expr = ptr_val\n            # If there is a subjectexpr, substitute our lhs and rhs in\n            # for __subject__ in the subjectexpr and compare *that*\n            if (subjectexpr := cnstr.get_subjectexpr(ctx.env.schema)):\n                assert isinstance(subjectexpr, s_expr.Expression)\n                assert isinstance(subjectexpr.parse(), qlast.Expr)\n                lhs = qlutils.subject_substitute(subjectexpr.parse(), lhs)\n                rhs = qlutils.subject_substitute(subjectexpr.parse(), rhs)\n\n            conds.append(qlast.BinOp(\n                op='=' if ptr_card.is_single() else 'IN',\n                left=lhs, right=rhs,\n            ))\n\n    # If the type we are looking at is BaseObject, then this must a\n    # conflict check we are synthesizing for an explicit .id. We need\n    # to ignore access policies in that case, since there is no\n    # trigger to back us up.\n    # (We can't insert directly into the abstract BaseObject, so this\n    # is a safe assumption.)\n    ignore_rewrites = (\n        str(subject_typ.get_name(ctx.env.schema)) == 'std::BaseObject')\n    if ignore_rewrites:\n        assert not obj_constrs\n        assert len(constrs) == 1 and len(constrs['id'][1]) == 1\n    insert_subject = ctx.create_anchor(setgen.class_set(\n        subject_typ, ignore_rewrites=ignore_rewrites, ctx=ctx\n    ))\n\n    for constr in obj_constrs:\n        subject_expr: Optional[s_expr.Expression] = (\n            constr.get_subjectexpr(ctx.env.schema)\n        )\n        assert subject_expr and isinstance(subject_expr.parse(), qlast.Expr)\n        lhs = qlutils.subject_paths_substitute(\n            subject_expr.parse(), ptr_anchors\n        )\n        rhs = qlutils.subject_substitute(\n            subject_expr.parse(), insert_subject\n        )\n        op = qlast.BinOp(op='=', left=lhs, right=rhs)\n\n        # If there is an except expr, we need to add in those checks also\n        if except_expr := constr.get_except_expr(ctx.env.schema):\n            assert isinstance(except_expr, s_expr.Expression)\n\n            e_lhs = qlutils.subject_paths_substitute(\n                except_expr.parse(), ptr_anchors)\n            e_rhs = qlutils.subject_substitute(\n                except_expr.parse(), insert_subject)\n\n            true_ast = qlast.Constant.boolean(True)\n            on = qlast.BinOp(\n                op='AND',\n                left=qlast.BinOp(op='?!=', left=e_lhs, right=true_ast),\n                right=qlast.BinOp(op='?!=', left=e_rhs, right=true_ast),\n            )\n            op = qlast.BinOp(op='AND', left=op, right=on)\n\n        conds.append(op)\n\n    if not conds:\n        return None, False\n\n    # We use `any` to compute the disjunction here because some might\n    # be empty.\n    if len(conds) == 1:\n        cond = conds[0]\n    else:\n        cond = qlast.FunctionCall(\n            func='any',\n            args=[qlast.Set(elements=conds)],\n        )\n\n    # For the result filtering we ignore any objects from the same type.\n    if fake_dml_set:\n        anchor = qlutils.subject_paths_substitute(\n            ptr_anchors['__type__'], ptr_anchors)\n        anchor_val = qlast.Path(steps=[anchor, qlast.Ptr(name='id')])\n        ptr_val = qlast.Path(\n            partial=True,\n            steps=[qlast.Ptr(name='__type__'), qlast.Ptr(name='id')],\n        )\n        cond = qlast.BinOp(\n            op='AND',\n            left=cond,\n            right=qlast.BinOp(op='!=', left=anchor_val, right=ptr_val),\n        )\n\n    # Produce a query that finds the conflicting objects\n    select_ast = qlast.DetachedExpr(\n        expr=qlast.SelectQuery(result=insert_subject, where=cond)\n    )\n\n    # If one of the pointers we care about is multi, then we have to always\n    # use a conflict CTE check instead of trying to use a constraint.\n    has_multi = False\n    for ptrname in needed_ptrs:\n        ptr = subject_typ.getptr(ctx.env.schema, s_name.UnqualName(ptrname))\n        if not ptr.get_cardinality(ctx.env.schema).is_single():\n            has_multi = True\n\n    return select_ast, has_multi\n\n\ndef _constr_matters(\n    constr: s_constr.Constraint,\n    *,\n    only_local: bool = False,\n    ctx: context.ContextLevel,\n) -> bool:\n    schema = ctx.env.schema\n    return (\n        not constr.is_non_concrete(schema)\n        and not constr.get_delegated(schema)\n        and (\n            # In some use sites we always process ancestor constraints\n            # too, so in those cases a constraint only matters if it\n            # is the \"top\" constraint where it actually starts\n            # applying.\n            not only_local\n            or constr.get_owned(schema)\n            or all(\n                anc.get_delegated(schema) or anc.is_non_concrete(schema)\n                for anc in constr.get_ancestors(schema).objects(schema)\n            )\n        )\n    )\n\n\nPointerConstraintMap = dict[\n    str,\n    tuple[s_pointers.Pointer, list[s_constr.Constraint]],\n]\nConstraintPair = tuple[PointerConstraintMap, list[s_constr.Constraint]]\nConflictTypeMap = dict[s_objtypes.ObjectType, ConstraintPair]\n\n\ndef _split_constraints(\n    obj_constrs: Sequence[s_constr.Constraint],\n    constrs: PointerConstraintMap,\n    ctx: context.ContextLevel,\n) -> ConflictTypeMap:\n    schema = ctx.env.schema\n\n    type_maps: ConflictTypeMap = {}\n\n    # Split up pointer constraints by what object types they come from\n    for name, (_, p_constrs) in constrs.items():\n        for p_constr in p_constrs:\n            ancs = (p_constr,) + p_constr.get_ancestors(schema).objects(schema)\n            for anc in ancs:\n                if not _constr_matters(anc, only_local=True, ctx=ctx):\n                    continue\n                p_ptr = anc.get_subject(schema)\n                assert isinstance(p_ptr, s_pointers.Pointer)\n                obj = p_ptr.get_source(schema)\n                assert isinstance(obj, s_objtypes.ObjectType)\n                map, _ = type_maps.setdefault(obj, ({}, []))\n                _, entry = map.setdefault(name, (p_ptr, []))\n                entry.append(anc)\n\n    # Split up object constraints by what object types they come from\n    for obj_constr in obj_constrs:\n        ancs = (obj_constr,) + obj_constr.get_ancestors(schema).objects(schema)\n        for anc in ancs:\n            if not _constr_matters(anc, only_local=True, ctx=ctx):\n                continue\n            obj = anc.get_subject(schema)\n            assert isinstance(obj, s_objtypes.ObjectType)\n            _, o_constr_entry = type_maps.setdefault(obj, ({}, []))\n            o_constr_entry.append(anc)\n\n    return type_maps\n\n\ndef _compile_conflict_select(\n    stmt: irast.MutatingStmt,\n    subject_typ: s_objtypes.ObjectType,\n    *,\n    for_inheritance: bool=False,\n    fake_dml_set: Optional[irast.Set]=None,\n    obj_constrs: Sequence[s_constr.Constraint],\n    constrs: PointerConstraintMap,\n    span: Optional[irast.Span],\n    ctx: context.ContextLevel,\n) -> tuple[irast.Set, bool, bool]:\n    \"\"\"Synthesize a select of conflicting objects\n\n    This teases apart the constraints we care about based on which\n    type they originate from, generates a SELECT for each type, and\n    unions them together.\n\n    `cnstrs` contains the constraints to consider.\n    \"\"\"\n    schema = ctx.env.schema\n\n    if for_inheritance:\n        type_maps = {subject_typ: (constrs, list(obj_constrs))}\n    else:\n        type_maps = _split_constraints(obj_constrs, constrs, ctx=ctx)\n\n    always_check = False\n\n    # Generate a separate query for each type\n    from_parent = False\n    frags = []\n    for a_obj, (a_constrs, a_obj_constrs) in type_maps.items():\n        frag, frag_always_check = _compile_conflict_select_for_obj_type(\n            stmt, a_obj, obj_constrs=a_obj_constrs, constrs=a_constrs,\n            for_inheritance=for_inheritance,\n            fake_dml_set=fake_dml_set,\n            span=span, ctx=ctx,\n        )\n        always_check |= frag_always_check\n        if frag:\n            if a_obj != subject_typ:\n                from_parent = True\n            frags.append(frag)\n\n    always_check |= from_parent or any(\n        not child.is_view(schema) for child in subject_typ.children(schema)\n    )\n\n    # Union them all together\n    select_ast = qlast.Set(elements=frags)\n    with ctx.new() as ectx:\n        ectx.allow_factoring()\n\n        ectx.implicit_limit = 0\n        ectx.allow_endpoint_linkprops = True\n        select_ir = dispatch.compile(select_ast, ctx=ectx)\n        select_ir = setgen.scoped_set(\n            select_ir, force_reassign=True, ctx=ectx)\n        assert isinstance(select_ir, irast.Set)\n\n    # If we have an empty set, remake it with the right type\n    if isinstance(select_ir.expr, irast.EmptySet):\n        select_ir = setgen.new_empty_set(stype=subject_typ, ctx=ctx)\n\n    return select_ir, always_check, from_parent\n\n\ndef _get_exclusive_ptr_constraints(\n    typ: s_objtypes.ObjectType,\n    include_id: bool,\n    *, ctx: context.ContextLevel,\n) -> dict[str, tuple[s_pointers.Pointer, list[s_constr.Constraint]]]:\n    schema = ctx.env.schema\n    pointers = {}\n\n    exclusive_constr = schema.get('std::exclusive', type=s_constr.Constraint)\n    for ptr in typ.get_pointers(schema).objects(schema):\n        ptr = ptr.get_nearest_non_derived_parent(schema)\n        ex_cnstrs = [c for c in ptr.get_constraints(schema).objects(schema)\n                     if c.issubclass(schema, exclusive_constr)]\n        if ex_cnstrs:\n            name = ptr.get_shortname(schema).name\n            if name != 'id' or include_id:\n                pointers[name] = ptr, ex_cnstrs\n\n    return pointers\n\n\ndef compile_insert_unless_conflict(\n    stmt: irast.InsertStmt,\n    typ: s_objtypes.ObjectType,\n    *, ctx: context.ContextLevel,\n) -> irast.OnConflictClause:\n    \"\"\"Compile an UNLESS CONFLICT clause with no ON\n\n    This requires synthesizing a conditional based on all the exclusive\n    constraints on the object.\n    \"\"\"\n    has_id_write = _has_explicit_id_write(stmt)\n    pointers = _get_exclusive_ptr_constraints(\n        typ, include_id=has_id_write, ctx=ctx)\n    obj_constrs = typ.get_constraints(ctx.env.schema).objects(ctx.env.schema)\n\n    select_ir, always_check, _ = _compile_conflict_select(\n        stmt, typ,\n        constrs=pointers,\n        obj_constrs=obj_constrs,\n        span=stmt.span, ctx=ctx)\n\n    return irast.OnConflictClause(\n        constraint=None, select_ir=select_ir, always_check=always_check,\n        else_ir=None)\n\n\ndef compile_insert_unless_conflict_on(\n    stmt: irast.InsertStmt,\n    typ: s_objtypes.ObjectType,\n    constraint_spec: qlast.Expr,\n    else_branch: Optional[qlast.Expr],\n    *, ctx: context.ContextLevel,\n) -> irast.OnConflictClause:\n\n    with ctx.new() as constraint_ctx:\n        constraint_ctx.partial_path_prefix = setgen.class_set(typ, ctx=ctx)\n\n        # We compile the name here so we can analyze it, but we don't do\n        # anything else with it.\n        cspec_res = dispatch.compile(constraint_spec, ctx=constraint_ctx)\n\n    # We accept a property, link, or a list of them in the form of a\n    # tuple.\n    if isinstance(cspec_res.expr, irast.Tuple):\n        cspec_args = [elem.val for elem in cspec_res.expr.elements]\n    else:\n        cspec_args = [cspec_res]\n\n    cspec_args = [irutils.unwrap_set(arg) for arg in cspec_args]\n\n    for cspec_arg in cspec_args:\n        if not isinstance(cspec_arg.expr, irast.Pointer):\n            raise errors.QueryError(\n                'UNLESS CONFLICT argument must be a property, link, '\n                'or tuple of properties and links',\n                span=constraint_spec.span,\n            )\n\n        if cspec_arg.expr.source.path_id != stmt.subject.path_id:\n            raise errors.QueryError(\n                'UNLESS CONFLICT argument must be a property of the '\n                'type being inserted',\n                span=constraint_spec.span,\n            )\n\n    schema = ctx.env.schema\n\n    ptrs = []\n    exclusive_constr = schema.get('std::exclusive', type=s_constr.Constraint)\n    for cspec_arg in cspec_args:\n        assert isinstance(cspec_arg.expr, irast.Pointer)\n        schema, ptr = (\n            typeutils.ptrcls_from_ptrref(cspec_arg.expr.ptrref, schema=schema))\n        if not isinstance(ptr, s_pointers.Pointer):\n            raise errors.QueryError(\n                'UNLESS CONFLICT argument must be a property, link, '\n                'or tuple of properties and links',\n                span=constraint_spec.span,\n            )\n\n        ptr = ptr.get_nearest_non_derived_parent(schema)\n        ptrs.append(ptr)\n\n    obj_constrs = inference.cardinality.get_object_exclusive_constraints(\n        typ, set(ptrs), ctx.env)\n\n    field_constrs = []\n    if len(ptrs) == 1:\n        field_constrs = [\n            c for c in ptrs[0].get_constraints(schema).objects(schema)\n            if c.issubclass(schema, exclusive_constr)]\n\n    all_constrs = list(obj_constrs) + field_constrs\n    if len(all_constrs) != 1:\n        raise errors.QueryError(\n            'UNLESS CONFLICT property must have a single exclusive constraint',\n            span=constraint_spec.span,\n        )\n\n    ds = {ptr.get_shortname(schema).name: (ptr, field_constrs)\n          for ptr in ptrs}\n    select_ir, always_check, from_anc = _compile_conflict_select(\n        stmt, typ, constrs=ds, obj_constrs=list(obj_constrs),\n        span=stmt.span, ctx=ctx)\n\n    # Compile an else branch\n    else_ir = None\n    if else_branch:\n        # TODO: We should support this, but there is some semantic and\n        # implementation trickiness.\n        if from_anc:\n            raise errors.UnsupportedFeatureError(\n                'UNLESS CONFLICT can not use ELSE when constraint is from a '\n                'parent type',\n                details=(\n                    f\"The existing object can't be exposed in the ELSE clause \"\n                    f\"because it may not have type {typ.get_name(schema)}\"),\n                span=constraint_spec.span,\n            )\n\n        with ctx.new() as ectx:\n            # The ELSE needs to be able to reference the subject in an\n            # UPDATE, even though that would normally be prohibited.\n            ectx.iterator_path_ids |= {stmt.subject.path_id}\n\n            pathctx.ban_inserting_path(\n                stmt.subject.path_id, location='else', ctx=ectx)\n\n            # Compile else\n            else_ir = dispatch.compile(\n                astutils.ensure_ql_query(else_branch), ctx=ectx\n            )\n        assert isinstance(else_ir, irast.Set)\n\n    return irast.OnConflictClause(\n        constraint=irast.ConstraintRef(id=all_constrs[0].id),\n        select_ir=select_ir,\n        always_check=always_check,\n        else_ir=else_ir\n    )\n\n\ndef _has_explicit_id_write(stmt: irast.MutatingStmt) -> bool:\n    for elem, _ in stmt.subject.shape:\n        if elem.expr.ptrref.shortname.name == 'id':\n            # We want to make sure it isn't an implicit id (which\n            # won't have an expr) or a default value (which won't have\n            # a span).\n            #\n            # ... it is at least a little dodgy to check for default\n            # value by span presence.\n            return elem.span is not None and elem.expr.expr is not None\n    return False\n\n\ndef _disallow_exclusive_linkprops(\n    stmt: irast.MutatingStmt,\n    typ: s_objtypes.ObjectType,\n    *, ctx: context.ContextLevel,\n\n) -> None:\n    # TODO: It should be possible to support this, but we don't deal\n    # with it yet, so disallow it for safety reasons.\n    schema = ctx.env.schema\n    exclusive_constr = schema.get('std::exclusive', type=s_constr.Constraint)\n    for ptr in typ.get_pointers(schema).objects(schema):\n        if not isinstance(ptr, s_links.Link):\n            continue\n        ptr = ptr.get_nearest_non_derived_parent(schema)\n        for lprop in ptr.get_pointers(schema).objects(schema):\n            ex_cnstrs = [\n                c for c in lprop.get_constraints(schema).objects(schema)\n                if c.issubclass(schema, exclusive_constr)]\n            if ex_cnstrs:\n                raise errors.UnsupportedFeatureError(\n                    'INSERT/UPDATE do not support exclusive constraints on '\n                    'link properties when another statement in '\n                    'the same query modifies a related type',\n                    span=stmt.span,\n                )\n\n\ndef _get_type_conflict_constraint_entries(\n    stmt: irast.MutatingStmt,\n    typ: s_objtypes.ObjectType,\n    *, ctx: context.ContextLevel,\n) -> list[tuple[s_constr.Constraint, ConstraintPair]]:\n    # TODO: why do we return this in such a hinky way?\n    rewrite_kind = _get_rewrite_kind(stmt)\n\n    has_id_write = _has_explicit_id_write(stmt)\n    pointers = _get_exclusive_ptr_constraints(\n        typ, include_id=has_id_write, ctx=ctx)\n    exclusive = ctx.env.schema.get('std::exclusive', type=s_constr.Constraint)\n    obj_constrs = [\n        constr for constr in\n        typ.get_constraints(ctx.env.schema).objects(ctx.env.schema)\n        if constr.issubclass(ctx.env.schema, exclusive)\n    ]\n\n    shape_ptrs = set()\n    for elem, op in stmt.subject.shape:\n        if op != qlast.ShapeOp.MATERIALIZE:\n            shape_ptrs.add(elem.expr.ptrref.shortname.name)\n    shape_ptrs |= _get_rewritten_ptrs(stmt, typ, ctx=ctx)\n\n    # This is a little silly, but for *this* we need to do one per\n    # constraint (so that we can properly identify which constraint\n    # failed in the error messages)\n    entries: list[tuple[s_constr.Constraint, ConstraintPair]] = []\n    for name, (ptr, ptr_constrs) in pointers.items():\n        for ptr_constr in ptr_constrs:\n            # For updates, we only need to emit the check if we actually\n            # modify a pointer used by the constraint. For inserts, though\n            # everything must be in play, since constraints can depend on\n            # nonexistence also.\n            if (\n                _constr_matters(ptr_constr, ctx=ctx)\n                and (\n                    isinstance(stmt, irast.InsertStmt)\n                    or (\n                        _get_needed_ptrs(typ, (), [name], rewrite_kind, ctx)[0]\n                        & shape_ptrs\n                    )\n                )\n            ):\n                entries.append((ptr_constr, ({name: (ptr, [ptr_constr])}, [])))\n    for obj_constr in obj_constrs:\n        # See note above about needed ptrs check\n        if (\n            _constr_matters(obj_constr, ctx=ctx)\n            and (\n                isinstance(stmt, irast.InsertStmt)\n                or (_get_needed_ptrs(\n                    typ, [obj_constr], (), rewrite_kind, ctx)[0] & shape_ptrs)\n            )\n        ):\n            entries.append((obj_constr, ({}, [obj_constr])))\n\n    return entries\n\n\ndef _compile_inheritance_conflict_selects(\n    stmt: irast.MutatingStmt,\n    conflict: irast.MutatingStmt,\n    typ: s_objtypes.ObjectType,\n    subject_type: s_objtypes.ObjectType,\n    *, ctx: context.ContextLevel,\n) -> list[irast.OnConflictClause]:\n    \"\"\"Compile the selects needed to resolve multiple DML to related types\n\n    Generate a SELECT that finds all objects of type `typ` that conflict with\n    the insert `stmt`. The backend will use this to explicitly check that\n    no conflicts exist, and raise an error if they do.\n\n    This is needed because we mostly use triggers to enforce these\n    cross-type exclusive constraints, and they use a snapshot\n    beginning at the start of the statement.\n    \"\"\"\n    _disallow_exclusive_linkprops(stmt, typ, ctx=ctx)\n    entries = _get_type_conflict_constraint_entries(stmt, typ, ctx=ctx)\n\n    # We need to pull from the actual result overlay,\n    # since the final row can depend on things not in the query\n    # (on updates always, on inserts due to rewrites).\n    fake_subject = qlast.DetachedExpr(expr=qlast.Path(steps=[\n        s_utils.name_to_ast_ref(subject_type.get_name(ctx.env.schema))]))\n\n    fake_dml_set = dispatch.compile(fake_subject, ctx=ctx)\n\n    clauses = []\n    for cnstr, (p, o) in entries:\n        select_ir, _, _ = _compile_conflict_select(\n            stmt, typ,\n            for_inheritance=True,\n            fake_dml_set=fake_dml_set,\n            constrs=p,\n            obj_constrs=o,\n            span=stmt.span, ctx=ctx)\n        if isinstance(select_ir.expr, irast.EmptySet):\n            continue\n        cnstr_ref = irast.ConstraintRef(id=cnstr.id)\n        clauses.append(\n            irast.OnConflictClause(\n                constraint=cnstr_ref, select_ir=select_ir, always_check=False,\n                else_ir=None, else_fail=conflict,\n                check_anchor=fake_dml_set.path_id)\n        )\n    return clauses\n\n\ndef compile_inheritance_conflict_checks(\n    stmt: irast.MutatingStmt,\n    subject_stype: s_objtypes.ObjectType,\n    *, ctx: context.ContextLevel,\n) -> Optional[list[irast.OnConflictClause]]:\n\n    has_id_write = _has_explicit_id_write(stmt)\n\n    relevant_dml = [\n        dml for dml in ctx.env.dml_stmts\n        if not isinstance(dml, irast.DeleteStmt)\n    ]\n    # Updates can conflict with themselves\n    if isinstance(stmt, irast.UpdateStmt):\n        relevant_dml.append(stmt)\n\n    if not relevant_dml and not has_id_write:\n        return None\n\n    assert isinstance(subject_stype, s_objtypes.ObjectType)\n    modified_ancestors = set()\n    base_object = ctx.env.schema.get(\n        'std::BaseObject', type=s_objtypes.ObjectType)\n\n    subject_stype = subject_stype.get_nearest_non_derived_parent(\n        ctx.env.schema)\n    subject_stype = schemactx.concretify(subject_stype, ctx=ctx)\n    # For updates, we need to also consider all descendants, because\n    # those could also have interesting constraints of their own.\n    if isinstance(stmt, irast.UpdateStmt):\n        subject_stypes = list(\n            schemactx.get_all_concrete(subject_stype, ctx=ctx))\n    else:\n        subject_stypes = [subject_stype]\n\n    for ir in relevant_dml:\n        # N.B that for updates, the update itself will be in dml_stmts,\n        # since an update can conflict with itself if there are subtypes.\n        # If there aren't subtypes, though, skip it.\n        if ir is stmt and len(subject_stypes) == 1:\n            continue\n\n        typ = setgen.get_set_type(ir.subject, ctx=ctx)\n        assert isinstance(typ, s_objtypes.ObjectType)\n        typ = schemactx.concretify(typ, ctx=ctx)\n\n        # As mentioned above, need to consider descendants of updates\n        if isinstance(ir, irast.UpdateStmt):\n            typs = list(schemactx.get_all_concrete(typ, ctx=ctx))\n        else:\n            typs = [typ]\n\n        for typ in typs:\n            for subject_stype in subject_stypes:\n                # If the earlier DML has a shared ancestor that isn't\n                # BaseObject and isn't the same type, then we need to\n                # see if we need a conflict select.\n                #\n                # Note that two DMLs on the same type *can* require a\n                # conflict select if at least one of them is an UPDATE\n                # and there are children, but that is accounted for by\n                # the above loops over all descendants when ir is an\n                # UPDATE.\n                if subject_stype == typ:\n                    continue\n\n                ancs = s_utils.get_class_nearest_common_ancestors(\n                    ctx.env.schema, [subject_stype, typ])\n                for anc in ancs:\n                    if anc != base_object:\n                        modified_ancestors.add((subject_stype, anc, ir))\n\n    # If `id` is explicitly written to, synthesize a check against\n    # BaseObject to ensure that it doesn't conflict with anything,\n    # since we disable the trigger for id's exclusive constraint for\n    # performance reasons.\n    if has_id_write:\n        modified_ancestors.add((subject_stype, base_object, stmt))\n\n    conflicters = []\n    for subject_stype, anc_type, ir in modified_ancestors:\n\n        # don't enforce any constraints for abstract object type\n        if subject_stype.get_abstract(schema=ctx.env.schema):\n            continue\n\n        conflicters.extend(\n            _compile_inheritance_conflict_selects(\n                stmt, ir, anc_type, subject_stype, ctx=ctx\n            )\n        )\n\n    return conflicters or None\n\n\ndef check_for_isolation_conflicts(\n    stmt: irast.MutatingStmt,\n    typ: s_objtypes.ObjectType,\n    update_typ: Optional[s_objtypes.ObjectType] = None,\n    *, ctx: context.ContextLevel,\n) -> None:\n    \"\"\"Check for conflicts on a DML stmt that cause isolation dangers.\n\n    Cross-table exclusive constraints are implemented by triggers that\n    read the other tables looking for conflicting rows. This works\n    fine in SERIALIZABLE mode, but in REPEATABLE READ mode, this can\n    miss two concurrent transactions creating conflicting objects.\n\n    Analyze the type involved in `stmt` to see if there are isolation\n    dangers, and log them if so.  These will be reported to the client\n    and will generate an error if the query is executed in REPEATABLE\n    READ mode.\n\n    This function is called for every subtype in an UPDATE.  In that\n    case, `typ` is the subtype and `update_typ` is the base type being\n    UDPATEd.\n    \"\"\"\n\n    schema = ctx.env.schema\n\n    entries = _get_type_conflict_constraint_entries(stmt, typ, ctx=ctx)\n    constrs = [cnstr for cnstr, _ in entries]\n\n    op = 'INSERT' if isinstance(stmt, irast.InsertStmt) else 'UPDATE'\n    base_msg = f'{op} to {typ.get_verbosename(schema)} '\n\n    for constr in constrs:\n        subject = constr.get_subject(schema)\n        assert subject\n\n        # Find the origin type; if we are the only origin type and we\n        # don't have children, then we are good.\n        all_objs = []\n        match constr.get_constraint_origins(schema):\n            case []:\n                continue\n            case [root_constr, *_]:\n                root_subject = root_constr.get_subject(schema)\n                if isinstance(root_subject, s_pointers.Pointer):\n                    root_subject_obj = root_subject.get_source(schema)\n                else:\n                    root_subject_obj = root_subject\n\n                if isinstance(root_subject_obj, s_objtypes.ObjectType):\n                    all_objs = list(\n                        schemactx.get_all_concrete(root_subject_obj, ctx=ctx)\n                    )\n                    if root_subject_obj == typ and len(all_objs) == 1:\n                        continue\n                    if root_subject_obj == typ and constr.get_delegated(schema):\n                        continue\n                    # If this is an UPDATE and we are processing some\n                    # subtype, and the actual type being updated is\n                    # covered by this same constraint, don't report it for\n                    # the child: it will be reported for an ancestor,\n                    # which is less noisy.\n                    if (\n                        update_typ\n                        and update_typ != typ\n                        and update_typ in all_objs\n                    ):\n                        continue\n\n        subj_vn = subject.get_verbosename(schema, with_parent=True)\n        vn = f'{base_msg}affects an exclusive constraint on {subj_vn}'\n        if expr := constr.get_subjectexpr(schema):\n            vn += f\" with expression '{expr.text}'\"\n\n        if not root_subject_obj:\n            msg = (\n                f\"{vn} that is defined on \"\n                f\"{root_subject.get_verbosename(schema)}\"\n            )\n        elif root_subject_obj != typ:\n            msg = (\n                f\"{vn} that is defined in ancestor \"\n                f\"{root_subject_obj.get_verbosename(schema)}\"\n            )\n        else:\n            all_objs_s = ', '.join(sorted(\n                f\"'{o.get_displayname(schema)}'\" for o in all_objs if o != typ\n            ))\n            msg = (\n                f\"{vn} that is shared with \"\n                f\"descendant types: {all_objs_s}\"\n            )\n\n        ctx.log_repeatable_read_danger(\n            errors.UnsafeIsolationLevelError(msg, span=stmt.span)\n        )\n"
  },
  {
    "path": "edb/edgeql/compiler/context.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL to IR compiler context.\"\"\"\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    Literal,\n    Optional,\n    Mapping,\n    MutableMapping,\n    Sequence,\n    ChainMap,\n    NamedTuple,\n    cast,\n    overload,\n    TYPE_CHECKING,\n)\n\nimport collections\nimport dataclasses\nimport enum\nimport uuid\nimport weakref\n\nfrom edb.common import compiler\nfrom edb.common import ordered\nfrom edb.common import parsing\n\nfrom edb import errors\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom edb.ir import ast as irast\nfrom edb.ir import utils as irutils\nfrom edb.ir import typeutils as irtyputils\n\nfrom edb.schema import expraliases as s_aliases\nfrom edb.schema import name as s_name\nfrom edb.schema import objects as s_obj\nfrom edb.schema import permissions as s_permissions\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import schema as s_schema\nfrom edb.schema import types as s_types\n\nfrom .options import GlobalCompilerOptions\n\nif TYPE_CHECKING:\n    from edb.schema import objtypes as s_objtypes\n    from edb.schema import sources as s_sources\n\n\nclass Exposure(enum.IntEnum):\n    UNEXPOSED = 0\n    BINDING = 1\n    EXPOSED = 2\n\n    def __bool__(self) -> bool:\n        return self == Exposure.EXPOSED\n\n\nclass ContextSwitchMode(enum.Enum):\n    NEW = enum.auto()\n    SUBQUERY = enum.auto()\n    NEWSCOPE = enum.auto()\n    NEWFENCE = enum.auto()\n    DETACHED = enum.auto()\n\n\n@dataclasses.dataclass(kw_only=True)\nclass ViewRPtr:\n    source: s_sources.Source\n    ptrcls: Optional[s_pointers.Pointer]\n    ptrcls_name: Optional[s_name.QualName] = None\n    base_ptrcls: Optional[s_pointers.Pointer] = None\n    ptrcls_is_linkprop: bool = False\n    ptrcls_is_alias: bool = False\n    exprtype: s_types.ExprType = s_types.ExprType.Select\n    rptr_dir: Optional[s_pointers.PointerDirection] = None\n\n\n@dataclasses.dataclass(kw_only=True, frozen=True)\nclass SecurityContext:\n    # N.B: Whether we are compiling a trigger is not included here\n    # since we clear cached rewrites when compiling them in the\n    # *pgsql* compiler.\n    suppress_policies: bool\n\n    def toggle_policies(self) -> SecurityContext:\n        return dataclasses.replace(\n            self, suppress_policies=not self.suppress_policies\n        )\n\n\n@dataclasses.dataclass\nclass ScopeInfo:\n    path_scope: irast.ScopeTreeNode\n    binding_kind: Optional[irast.BindingKind]\n    pinned_path_id_ns: Optional[frozenset[str]] = None\n\n\nclass PointerRefCache(dict[irtyputils.PtrRefCacheKey, irast.BasePointerRef]):\n\n    _rcache: dict[irast.BasePointerRef, s_pointers.PointerLike]\n\n    def __init__(self) -> None:\n        super().__init__()\n        self._rcache = {}\n\n    def __setitem__(\n        self,\n        key: irtyputils.PtrRefCacheKey,\n        val: irast.BasePointerRef,\n    ) -> None:\n        super().__setitem__(key, val)\n        self._rcache[val] = key\n\n    def get_ptrcls_for_ref(\n        self,\n        ref: irast.BasePointerRef,\n    ) -> Optional[s_pointers.PointerLike]:\n        return self._rcache.get(ref)\n\n\n# Volatility inference computes two volatility results:\n# A basic one, and one for consumption by materialization\nInferredVolatility = (\n    qltypes.Volatility\n    | tuple[qltypes.Volatility, qltypes.Volatility]\n)\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass ServerParamConversion:\n    path_id: irast.PathId\n    ir_param: irast.Param\n    additional_info: tuple[str, ...]\n\n    volatility: qltypes.Volatility\n\n    # If the parameter is a query parameter, track its script params index.\n    script_param_index: Optional[int] = None\n\n    # If the parameter is a constant value, pass to directly to the server.\n    constant_value: Optional[Any] = None\n\n\nclass Environment:\n    \"\"\"Compilation environment.\"\"\"\n\n    schema: s_schema.Schema\n    \"\"\"A Schema instance to use for class resolution.\"\"\"\n\n    orig_schema: s_schema.Schema\n    \"\"\"A Schema as it was at the start of the compilation.\"\"\"\n\n    options: GlobalCompilerOptions\n    \"\"\"Compiler options.\"\"\"\n\n    path_scope: irast.ScopeTreeNode\n    \"\"\"Overrall expression path scope tree.\"\"\"\n\n    schema_view_cache: dict[\n        tuple[s_types.Type, object],\n        tuple[s_types.Type, irast.Set],\n    ]\n    \"\"\"Type cache used by schema-level views.\"\"\"\n\n    query_parameters: dict[str, irast.Param]\n    \"\"\"A mapping of query parameters to their types.  Gets populated during\n    the compilation.\"\"\"\n\n    query_globals: dict[s_name.QualName, irast.Global]\n    \"\"\"A mapping of query globals.  Gets populated during\n    the compilation.\"\"\"\n    query_globals_types: dict[s_name.QualName, s_types.Type]\n    \"\"\"Injected dummy types for caching globals when the input\n    encoding is JSON\"\"\"\n\n    required_permissions: set[s_permissions.Permission]\n    \"\"\"Permissions *required* to run this query.\"\"\"\n\n    server_param_conversions: dict[\n        str,\n        dict[str, ServerParamConversion],\n    ]\n    \"\"\"A mapping of query parameters and the server param conversions which are\n    needed by the query.\n\n    This indicates that the server will compute and provide an additional\n    parameter based on a user provided parameter.\n\n    Used by ext::ai:search to get embeddings from text before running a query.\n    \"\"\"\n\n    server_param_conversion_calls: list[tuple[str, Optional[parsing.Span]]]\n    \"\"\"Used to generate errors related to server param conversions.\"\"\"\n\n    set_types: dict[irast.Set, s_types.Type]\n    \"\"\"A dictionary of all Set instances and their schema types.\"\"\"\n\n    type_origins: dict[s_types.Type, Optional[parsing.Span]]\n    \"\"\"A dictionary of notable types and their source origins.\n\n    This is used to trace where a particular type instance originated in\n    order to provide useful diagnostics for type errors.\n    \"\"\"\n\n    inferred_volatility: dict[\n        irast.Base,\n        InferredVolatility]\n    \"\"\"A dictionary of expressions and their inferred volatility.\"\"\"\n\n    view_shapes: dict[\n        s_types.Type | s_pointers.PointerLike,\n        list[tuple[s_pointers.Pointer, qlast.ShapeOp]]\n    ]\n    \"\"\"Object output or modification shapes.\"\"\"\n\n    pointer_derivation_map: dict[\n        s_pointers.Pointer,\n        list[s_pointers.Pointer],\n    ]\n    \"\"\"A parent: children mapping of derived pointer classes.\"\"\"\n\n    pointer_specified_info: dict[\n        s_pointers.Pointer,\n        tuple[\n            Optional[qltypes.SchemaCardinality],\n            Optional[bool],\n            Optional[parsing.Span],\n        ],\n    ]\n    \"\"\"Cardinality/source context for pointers with unclear cardinality.\"\"\"\n\n    view_shapes_metadata: dict[s_types.Type, irast.ViewShapeMetadata]\n\n    schema_refs: set[s_obj.Object]\n    \"\"\"A set of all schema objects referenced by an expression.\"\"\"\n\n    schema_ref_exprs: Optional[dict[s_obj.Object, set[qlast.Base]]]\n    \"\"\"Map from all schema objects referenced to the ast referants.\n\n    This is used for rewriting expressions in the schema after a rename. \"\"\"\n\n    # Caches for costly operations in edb.ir.typeutils\n    ptr_ref_cache: PointerRefCache\n    type_ref_cache: dict[irtyputils.TypeRefCacheKey, irast.TypeRef]\n\n    dml_exprs: list[qlast.Base]\n    \"\"\"A list of DML expressions (statements and DML-containing\n    functions) that appear in a function body.\n    \"\"\"\n\n    dml_stmts: list[irast.MutatingStmt]\n    \"\"\"A list of DML statements in the query\"\"\"\n\n    #: A list of bindings that should be assumed to be singletons.\n    singletons: list[irast.PathId]\n\n    scope_tree_nodes: MutableMapping[int, irast.ScopeTreeNode]\n    \"\"\"Map from unique_id to nodes.\"\"\"\n\n    materialized_sets: dict[\n        s_types.Type | s_pointers.PointerLike,\n        tuple[qlast.Statement, Sequence[irast.MaterializeReason]],\n    ]\n    \"\"\"A mapping of computed sets that must be computed only once.\"\"\"\n\n    compiled_stmts: dict[qlast.Statement, irast.Stmt]\n    \"\"\"A mapping of from input edgeql to compiled IR\"\"\"\n\n    alias_result_view_name: Optional[s_name.QualName]\n    \"\"\"The name of a view being defined as an alias.\"\"\"\n\n    script_params: dict[str, irast.Param]\n    \"\"\"All parameter definitions from an enclosing multi-statement script.\n\n    Used to make sure the types are consistent.\"\"\"\n\n    source_map: dict[s_pointers.PointerLike, irast.ComputableInfo]\n    \"\"\"A mapping of computable pointers to QL source AST and context.\"\"\"\n\n    type_rewrites: dict[\n        tuple[s_types.Type, bool], irast.Set | None | Literal[True]]\n    \"\"\"Access policy rewrites for schema-level types.\n\n    None indicates no rewrite, True indicates a compound type\n    that had rewrites in its components.\n    \"\"\"\n\n    expr_view_cache: dict[tuple[qlast.Base, s_name.Name], irast.Set]\n    \"\"\"Type cache used by expression-level views.\"\"\"\n\n    shape_type_cache: dict[\n        tuple[\n            s_objtypes.ObjectType,\n            s_types.ExprType,\n            tuple[qlast.ShapeElement, ...],\n        ],\n        s_objtypes.ObjectType,\n    ]\n    \"\"\"Type cache for shape expressions.\"\"\"\n\n    path_scope_map: dict[irast.Set, ScopeInfo]\n    \"\"\"A dictionary of scope info that are appropriate for a given view.\"\"\"\n\n    dml_rewrites: dict[irast.Set, irast.Rewrites]\n    \"\"\"Compiled rewrites that should be attached to InsertStmt or UpdateStmt\"\"\"\n\n    warnings: list[errors.EdgeDBError]\n    \"\"\"List of warnings to emit\"\"\"\n\n    unsafe_isolation_dangers: list[errors.UnsafeIsolationLevelError]\n    \"\"\"List of repeatable read DML dangers\"\"\"\n\n    policy_use_count: int\n    \"\"\"A count of the number of times that policies have been referenced.\n\n    Can be used to detect if a sub-compilation referenced a policy.\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        schema: s_schema.Schema,\n        path_scope: Optional[irast.ScopeTreeNode] = None,\n        alias_result_view_name: Optional[s_name.QualName] = None,\n        options: Optional[GlobalCompilerOptions] = None,\n    ) -> None:\n        if options is None:\n            options = GlobalCompilerOptions()\n\n        if path_scope is None:\n            path_scope = irast.new_scope_tree()\n\n        self.options = options\n        self.schema = schema\n        self.orig_schema = schema\n        self.path_scope = path_scope\n        self.schema_view_cache = {}\n        self.query_parameters = {}\n        self.query_globals = {}\n        self.query_globals_types = {}\n        self.required_permissions = set()\n        self.server_param_conversions = {}\n        self.server_param_conversion_calls = []\n        self.set_types = {}\n        self.type_origins = {}\n        self.inferred_volatility = {}\n        self.view_shapes = collections.defaultdict(list)\n        self.view_shapes_metadata = collections.defaultdict(\n            irast.ViewShapeMetadata)\n        self.schema_refs = set()\n        self.schema_ref_exprs = {} if options.track_schema_ref_exprs else None\n        self.ptr_ref_cache = PointerRefCache()\n        self.type_ref_cache = {}\n        self.dml_exprs = []\n        self.dml_stmts = []\n        self.pointer_derivation_map = collections.defaultdict(list)\n        self.pointer_specified_info = {}\n        self.singletons = []\n        self.scope_tree_nodes = weakref.WeakValueDictionary()\n        self.materialized_sets = {}\n        self.compiled_stmts = {}\n        self.alias_result_view_name = alias_result_view_name\n        self.script_params = {}\n        self.source_map = {}\n        self.type_rewrites = {}\n        self.shape_type_cache = {}\n        self.expr_view_cache = {}\n        self.path_scope_map = {}\n        self.dml_rewrites = {}\n        self.warnings = []\n        self.unsafe_isolation_dangers = []\n        self.policy_use_count = 0\n\n    def add_schema_ref(\n        self, sobj: s_obj.Object, expr: Optional[qlast.Base]\n    ) -> None:\n        self.schema_refs.add(sobj)\n        if self.schema_ref_exprs is not None and expr:\n            self.schema_ref_exprs.setdefault(sobj, set()).add(expr)\n\n    @overload\n    def get_schema_object_and_track(\n        self,\n        name: s_name.Name,\n        expr: Optional[qlast.Base],\n        *,\n        modaliases: Optional[Mapping[Optional[str], str]] = None,\n        type: Optional[type[s_obj.Object]] = None,\n        default: s_obj.Object | s_obj.NoDefaultT = s_obj.NoDefault,\n        label: Optional[str] = None,\n        condition: Optional[Callable[[s_obj.Object], bool]] = None,\n    ) -> s_obj.Object:\n        ...\n\n    @overload\n    def get_schema_object_and_track(\n        self,\n        name: s_name.Name,\n        expr: Optional[qlast.Base],\n        *,\n        modaliases: Optional[Mapping[Optional[str], str]] = None,\n        type: Optional[type[s_obj.Object]] = None,\n        default: s_obj.Object | s_obj.NoDefaultT | None = s_obj.NoDefault,\n        label: Optional[str] = None,\n        condition: Optional[Callable[[s_obj.Object], bool]] = None,\n    ) -> Optional[s_obj.Object]:\n        ...\n\n    def get_schema_object_and_track(\n        self,\n        name: s_name.Name,\n        expr: Optional[qlast.Base],\n        *,\n        modaliases: Optional[Mapping[Optional[str], str]] = None,\n        type: Optional[type[s_obj.Object]] = None,\n        default: s_obj.Object | s_obj.NoDefaultT | None = s_obj.NoDefault,\n        label: Optional[str] = None,\n        condition: Optional[Callable[[s_obj.Object], bool]] = None,\n    ) -> Optional[s_obj.Object]:\n        sobj = self.schema.get(\n            name, module_aliases=modaliases, type=type,\n            condition=condition, label=label,\n            default=default)\n        if sobj is not None and sobj is not default:\n            self.add_schema_ref(sobj, expr)\n\n            if (\n                isinstance(sobj, s_types.Type)\n                and sobj.get_expr(self.schema) is not None\n            ):\n                # If the type is derived from an ALIAS declaration,\n                # make sure we record the reference to the Alias object\n                # as well for correct delta ordering.\n                alias_objs = self.schema.get_referrers(\n                    sobj,\n                    scls_type=s_aliases.Alias,\n                    field_name='type',\n                )\n                for obj in alias_objs:\n                    self.add_schema_ref(obj, expr)\n\n        return sobj\n\n    def get_schema_type_and_track(\n        self,\n        name: s_name.Name,\n        expr: Optional[qlast.Base]=None,\n        *,\n        modaliases: Optional[Mapping[Optional[str], str]] = None,\n        default: None | s_obj.Object | s_obj.NoDefaultT = s_obj.NoDefault,\n        label: Optional[str]=None,\n        condition: Optional[Callable[[s_obj.Object], bool]]=None,\n    ) -> s_types.Type:\n\n        stype = self.get_schema_object_and_track(\n            name, expr, modaliases=modaliases, default=default, label=label,\n            condition=condition, type=s_types.Type,\n        )\n\n        return cast(s_types.Type, stype)\n\n\nclass ContextLevel(compiler.ContextLevel):\n\n    env: Environment\n    \"\"\"Compilation environment common for all context levels.\"\"\"\n\n    derived_target_module: Optional[str]\n    \"\"\"The name of the module for classes derived by views.\"\"\"\n\n    anchors: dict[\n        str | type[qlast.SpecialAnchor],\n        irast.Set,\n    ]\n    \"\"\"A mapping of anchor variables (aliases to path expressions passed\n    to the compiler programmatically).\n    \"\"\"\n\n    modaliases: dict[Optional[str], str]\n    \"\"\"A combined list of module name aliases declared in the WITH block,\n    or passed to the compiler programmatically.\n    \"\"\"\n\n    view_nodes: dict[s_name.Name, s_types.Type]\n    \"\"\"A dictionary of newly derived Node classes representing views.\"\"\"\n\n    view_sets: dict[s_obj.Object, irast.Set]\n    \"\"\"A dictionary of IR expressions for views declared in the query.\"\"\"\n\n    suppress_rewrites: frozenset[s_types.Type]\n    \"\"\"Types to suppress using rewrites on\"\"\"\n\n    aliased_views: ChainMap[s_name.Name, irast.Set]\n    \"\"\"A dictionary of views aliased in a statement body.\"\"\"\n\n    class_view_overrides: dict[uuid.UUID, s_types.Type]\n    \"\"\"Object mapping used by implicit view override in SELECT.\"\"\"\n\n    clause: Optional[str]\n    \"\"\"Statement clause the compiler is currently in.\"\"\"\n\n    toplevel_stmt: Optional[irast.Stmt]\n    \"\"\"Top-level statement.\"\"\"\n\n    stmt: Optional[irast.Stmt]\n    \"\"\"Statement node currently being built.\"\"\"\n\n    qlstmt: Optional[qlast.Statement]\n    \"\"\"Statement source node currently being built.\"\"\"\n\n    path_id_namespace: frozenset[str]\n    \"\"\"A namespace to use for all path ids.\"\"\"\n\n    pending_stmt_own_path_id_namespace: frozenset[str]\n    \"\"\"A path id namespace to add to the fence of the next statement.\"\"\"\n\n    pending_stmt_full_path_id_namespace: frozenset[str]\n    \"\"\"A set of path id namespaces to use in path ids in the next statement.\"\"\"\n\n    inserting_paths: dict[irast.PathId, Literal['body'] | Literal['else']]\n    \"\"\"A set of path ids that are currently being inserted.\"\"\"\n\n    view_map: ChainMap[\n        irast.PathId,\n        tuple[tuple[irast.PathId, irast.Set], ...],\n    ]\n    \"\"\"Set translation map.  Used for mapping computable sources..\n\n    When compiling a computable, we need to be able to map references to\n    the source back to the correct source set.\n\n    This maps from a namespace-stripped source path_id to the expected\n    computable-internal path_id and the actual source set.\n\n    The namespace stripping is necessary to handle the case where\n    bindings have added more namespaces to the source set reference.\n    (See test_edgeql_scope_computables_13.)\n    \"\"\"\n\n    path_scope: irast.ScopeTreeNode\n    \"\"\"Path scope tree, with per-lexical-scope levels.\"\"\"\n\n    iterator_ctx: Optional[ContextLevel]\n    \"\"\"The context of the statement where all iterators should be placed.\"\"\"\n\n    iterator_path_ids: frozenset[irast.PathId]\n    \"\"\"The path ids of all in scope iterator variables\"\"\"\n\n    scope_id_ctr: compiler.SimpleCounter\n    \"\"\"Path scope id counter.\"\"\"\n\n    view_rptr: Optional[ViewRPtr]\n    \"\"\"Pointer information for the top-level view of the substatement.\"\"\"\n\n    view_scls: Optional[s_types.Type]\n    \"\"\"Schema class for the top-level set of the substatement.\"\"\"\n\n    toplevel_result_view_name: Optional[s_name.QualName]\n    \"\"\"The name to use for the view that is the result of the top statement.\"\"\"\n\n    partial_path_prefix: Optional[irast.Set]\n    \"\"\"The set used as a prefix for partial paths.\"\"\"\n\n    implicit_id_in_shapes: bool\n    \"\"\"Whether to include the id property in object shapes implicitly.\"\"\"\n\n    implicit_tid_in_shapes: bool\n    \"\"\"Whether to include the type id property in object shapes implicitly.\"\"\"\n\n    implicit_tname_in_shapes: bool\n    \"\"\"Whether to include the type name property in object shapes\n       implicitly.\"\"\"\n\n    implicit_limit: int\n    \"\"\"Implicit LIMIT clause in SELECT statements.\"\"\"\n\n    special_computables_in_mutation_shape: frozenset[str]\n    \"\"\"A set of \"special\" computable pointers allowed in mutation shape.\"\"\"\n\n    empty_result_type_hint: Optional[s_types.Type]\n    \"\"\"Type to use if the statement result expression is an empty set ctor.\"\"\"\n\n    defining_view: Optional[s_objtypes.ObjectType]\n    \"\"\"Whether a view is currently being defined (as opposed to be compiled)\"\"\"\n\n    current_schema_views: tuple[s_types.Type, ...]\n    \"\"\"Which schema views are currently being compiled\"\"\"\n\n    recompiling_schema_alias: bool\n    \"\"\"Whether we are currently recompiling a schema-level expression alias.\"\"\"\n\n    compiling_update_shape: bool\n    \"\"\"Whether an UPDATE shape is currently being compiled.\"\"\"\n\n    active_computeds: ordered.OrderedSet[s_pointers.Pointer]\n    \"\"\"A ordered set of currently compiling computeds\"\"\"\n\n    allow_endpoint_linkprops: bool\n    \"\"\"Whether to allow references to endpoint linkpoints (@source, @target).\"\"\"\n\n    disallow_dml: Optional[str]\n    \"\"\"Whether we are currently in a place where no dml is allowed,\n        if not None, then it is of the form `in a FILTER clause`  \"\"\"\n\n    active_rewrites: frozenset[s_objtypes.ObjectType]\n    \"\"\"For detecting cycles in rewrite rules\"\"\"\n\n    active_defaults: frozenset[s_objtypes.ObjectType]\n    \"\"\"For detecting cycles in defaults\"\"\"\n\n    collection_cast_info: Optional[CollectionCastInfo]\n    \"\"\"For generating errors messages when casting to collections.\n\n    This will be set by the outermost cast and then shared between all\n    sub-casts.\n\n    Some casts (eg. arrays) will generate select statements containing other\n    type casts. These will also share the outermost cast info.\n    \"\"\"\n\n    no_factoring: bool\n\n    def __init__(\n        self,\n        prevlevel: Optional[ContextLevel],\n        mode: ContextSwitchMode,\n        *,\n        env: Optional[Environment] = None,\n    ) -> None:\n\n        self.mode = mode\n\n        if prevlevel is None:\n            assert env is not None\n            self.env = env\n            self.derived_target_module = None\n            self.aliases = compiler.AliasGenerator()\n            self.anchors = {}\n            self.modaliases = {}\n\n            self.view_nodes = {}\n            self.view_sets = {}\n            self.suppress_rewrites = frozenset()\n            self.aliased_views = collections.ChainMap()\n            self.class_view_overrides = {}\n\n            self.toplevel_stmt = None\n            self.stmt = None\n            self.qlstmt = None\n            self.path_id_namespace = frozenset()\n            self.pending_stmt_own_path_id_namespace = frozenset()\n            self.pending_stmt_full_path_id_namespace = frozenset()\n            self.inserting_paths = {}\n            self.view_map = collections.ChainMap()\n            self.path_scope = env.path_scope\n            self.iterator_path_ids = frozenset()\n            self.scope_id_ctr = compiler.SimpleCounter()\n            self.view_scls = None\n            self.expr_exposed = Exposure.UNEXPOSED\n\n            self.partial_path_prefix = None\n\n            self.view_rptr = None\n            self.toplevel_result_view_name = None\n            self.implicit_id_in_shapes = False\n            self.implicit_tid_in_shapes = False\n            self.implicit_tname_in_shapes = False\n            self.implicit_limit = 0\n            self.special_computables_in_mutation_shape = frozenset()\n            self.empty_result_type_hint = None\n            self.defining_view = None\n            self.current_schema_views = ()\n            self.compiling_update_shape = False\n            self.active_computeds = ordered.OrderedSet()\n            self.recompiling_schema_alias = False\n            self.active_rewrites = frozenset()\n            self.active_defaults = frozenset()\n\n            self.allow_endpoint_linkprops = False\n            self.disallow_dml = None\n            self.no_factoring = False\n\n            self.collection_cast_info = None\n\n        else:\n            self.env = prevlevel.env\n            self.derived_target_module = prevlevel.derived_target_module\n            self.aliases = prevlevel.aliases\n\n            self.view_nodes = prevlevel.view_nodes\n            self.view_sets = prevlevel.view_sets\n            self.suppress_rewrites = prevlevel.suppress_rewrites\n\n            self.iterator_path_ids = prevlevel.iterator_path_ids\n            self.path_id_namespace = prevlevel.path_id_namespace\n            self.pending_stmt_own_path_id_namespace = \\\n                prevlevel.pending_stmt_own_path_id_namespace\n            self.pending_stmt_full_path_id_namespace = \\\n                prevlevel.pending_stmt_full_path_id_namespace\n            self.inserting_paths = prevlevel.inserting_paths\n            self.view_map = prevlevel.view_map\n            if prevlevel.path_scope is None:\n                prevlevel.path_scope = self.env.path_scope\n            self.path_scope = prevlevel.path_scope\n            self.scope_id_ctr = prevlevel.scope_id_ctr\n            self.view_scls = prevlevel.view_scls\n            self.expr_exposed = prevlevel.expr_exposed\n            self.partial_path_prefix = prevlevel.partial_path_prefix\n            self.toplevel_stmt = prevlevel.toplevel_stmt\n            self.implicit_id_in_shapes = prevlevel.implicit_id_in_shapes\n            self.implicit_tid_in_shapes = prevlevel.implicit_tid_in_shapes\n            self.implicit_tname_in_shapes = prevlevel.implicit_tname_in_shapes\n            self.implicit_limit = prevlevel.implicit_limit\n            self.special_computables_in_mutation_shape = \\\n                prevlevel.special_computables_in_mutation_shape\n            self.empty_result_type_hint = prevlevel.empty_result_type_hint\n            self.defining_view = prevlevel.defining_view\n            self.current_schema_views = prevlevel.current_schema_views\n            self.compiling_update_shape = prevlevel.compiling_update_shape\n            self.active_computeds = prevlevel.active_computeds\n            self.recompiling_schema_alias = prevlevel.recompiling_schema_alias\n            self.active_rewrites = prevlevel.active_rewrites\n            self.active_defaults = prevlevel.active_defaults\n\n            self.allow_endpoint_linkprops = prevlevel.allow_endpoint_linkprops\n            self.disallow_dml = prevlevel.disallow_dml\n            self.no_factoring = prevlevel.no_factoring\n\n            self.collection_cast_info = prevlevel.collection_cast_info\n\n            if mode == ContextSwitchMode.SUBQUERY:\n                self.anchors = prevlevel.anchors.copy()\n                self.modaliases = prevlevel.modaliases.copy()\n                self.aliased_views = prevlevel.aliased_views.new_child()\n                self.class_view_overrides = \\\n                    prevlevel.class_view_overrides.copy()\n\n                self.pending_stmt_own_path_id_namespace = frozenset()\n                self.pending_stmt_full_path_id_namespace = frozenset()\n                self.inserting_paths = prevlevel.inserting_paths.copy()\n\n                self.view_rptr = None\n                self.view_scls = None\n                self.stmt = None\n                self.qlstmt = None\n\n                self.view_rptr = None\n                self.toplevel_result_view_name = None\n\n            elif mode == ContextSwitchMode.DETACHED:\n                self.anchors = prevlevel.anchors.copy()\n                self.modaliases = prevlevel.modaliases.copy()\n                self.aliased_views = collections.ChainMap()\n                self.view_map = collections.ChainMap()\n                self.class_view_overrides = {}\n                self.expr_exposed = prevlevel.expr_exposed\n\n                self.view_nodes = {}\n                self.view_sets = {}\n                self.path_id_namespace = frozenset({self.aliases.get('ns')})\n                self.pending_stmt_own_path_id_namespace = frozenset()\n                self.pending_stmt_full_path_id_namespace = frozenset()\n                self.inserting_paths = {}\n\n                self.view_rptr = None\n                self.view_scls = None\n                self.stmt = prevlevel.stmt\n                self.qlstmt = prevlevel.qlstmt\n\n                self.partial_path_prefix = None\n\n                self.view_rptr = None\n                self.toplevel_result_view_name = None\n            else:\n                self.anchors = prevlevel.anchors\n                self.modaliases = prevlevel.modaliases\n                self.aliased_views = prevlevel.aliased_views\n                self.class_view_overrides = prevlevel.class_view_overrides\n\n                self.stmt = prevlevel.stmt\n                self.qlstmt = prevlevel.qlstmt\n\n                self.view_rptr = prevlevel.view_rptr\n                self.toplevel_result_view_name = \\\n                    prevlevel.toplevel_result_view_name\n\n            if mode == ContextSwitchMode.NEWFENCE:\n                self.path_scope = self.path_scope.attach_fence()\n\n            if mode == ContextSwitchMode.NEWSCOPE:\n                self.path_scope = self.path_scope.attach_branch()\n\n    def subquery(self) -> compiler.CompilerContextManager[ContextLevel]:\n        return self.new(ContextSwitchMode.SUBQUERY)\n\n    def newscope(\n        self,\n        *,\n        fenced: bool,\n    ) -> compiler.CompilerContextManager[ContextLevel]:\n        if fenced:\n            mode = ContextSwitchMode.NEWFENCE\n        else:\n            mode = ContextSwitchMode.NEWSCOPE\n\n        return self.new(mode)\n\n    def detached(self) -> compiler.CompilerContextManager[ContextLevel]:\n        return self.new(ContextSwitchMode.DETACHED)\n\n    def create_anchor(\n        self,\n        ir: irast.Set,\n        name: str = 'v', *,\n        check_dml: bool = False,\n        move_scope: bool = False,\n    ) -> qlast.Path:\n        alias = self.aliases.get(name)\n        # TODO: We should probably always check for DML, but I'm\n        # concerned about perf, since we don't cache it at all.\n        has_dml = check_dml and irutils.contains_dml(ir)\n        self.anchors[alias] = ir\n        if move_scope:\n            assert ir.path_scope_id is not None\n        return qlast.Path(\n            steps=[qlast.IRAnchor(\n                name=alias, has_dml=has_dml, move_scope=move_scope\n            )],\n        )\n\n    def maybe_create_anchor(\n        self,\n        ir: irast.Set | qlast.Expr,\n        name: str = 'v',\n    ) -> qlast.Expr:\n        if isinstance(ir, irast.Set):\n            return self.create_anchor(ir, name)\n        else:\n            return ir\n\n    def get_security_context(self) -> SecurityContext:\n        '''Compute an additional compilation cache key.\n\n        Return an additional key for any compilation caches that may\n        vary based on \"security contexts\" such as whether we are in an\n        access policy.\n        '''\n        # N.B: Whether we are compiling a trigger is not included here\n        # since we clear cached rewrites when compiling them in the\n        # *pgsql* compiler.\n        return SecurityContext(\n            suppress_policies=bool(self.suppress_rewrites),\n        )\n\n    def log_warning(self, warning: errors.EdgeDBError) -> None:\n        self.env.warnings.append(warning)\n\n    def log_repeatable_read_danger(\n        self, d: errors.UnsafeIsolationLevelError\n    ) -> None:\n        self.env.unsafe_isolation_dangers.append(d)\n\n    def allow_factoring(self) -> None:\n        self.no_factoring = False\n\n\nclass CompilerContext(compiler.CompilerContext[ContextLevel]):\n    ContextLevelClass = ContextLevel\n    default_mode = ContextSwitchMode.NEW\n\n\nclass CollectionCastInfo(NamedTuple):\n    \"\"\"For generating errors messages when casting to collections.\"\"\"\n\n    from_type: s_types.Type\n    to_type: s_types.Type\n\n    path_elements: list[tuple[str, Optional[str]]]\n    \"\"\"Represents a path to the current collection element being cast.\n\n    A path element is a tuple of the collection type and an optional\n    element name. eg. ('tuple', 'a') or ('array', None)\n\n    The list is shared between the outermost context and all its sub contexts.\n    When casting a collection, each element's path should be pushed before\n    entering the \"sub-cast\" and popped immediately after.\n\n    In the event of a cast error, the list is preserved at the outermost cast.\n    \"\"\"\n"
  },
  {
    "path": "edb/edgeql/compiler/dispatch.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n\nimport functools\n\nfrom edb.edgeql import ast as qlast\nfrom edb.ir import ast as irast\n\nfrom . import context\n\n\n@functools.singledispatch\ndef compile(node: qlast.Base, *, ctx: context.ContextLevel) -> irast.Set:\n    raise NotImplementedError(\n        f'no EdgeQL compiler handler for {node.__class__}')\n"
  },
  {
    "path": "edb/edgeql/compiler/eta_expand.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"η-expansion of tuples and arrays.\n\nOur shape compiler is only able to produce shape outputs for objects\nat places in the program that get fairly directly routed into the\noutput. To compensate for this, when we have an expression like\n`[User {name}][0]`, the shape output is actually computed *after* the\narray indexing. This works well, but fails when the thing we need to\ndo late shape injection into is a collection type that cannot have a\nshape put on it directly: `[(User {name}, 20)][0]`.\nTo solve this problem, we use η-expansion.\n\nη-expansion is a technique coming from the study of the lambda\ncaluclus, where it means to expand an expression `e` into `λx.ex`,\nwhere `x` is a variable that does not appear in `e` (or, in Python-speak\n`lambda x: e(x)`). Setting aside questions about what happens if `e` does\nnot terminate, this new expression `λx.ex` will be equivalent to `e`.\n\nIn the traditional untyped lambda calculus, where everything is a\nfunction (from functions to functions), this is the whole story.\nBut the world of *typed* lambda calculi introduce some interesting\nnew considerations:\n  1. Instead of it being possible to η-expand *any* expression,\n     now only expressions actually of function type may be expanded.\n     This allows us to define of notion of an expression being \"η-long\",\n     which means that it is \"fully η-expanded\" (and that any new expansion\n     would create a reducible expression, where a lambda appears directly\n     on the LHS of an application).\n  2. If other types are introduced, we can define notions of η-expansion\n     for them as well. The key idea is that expanded expression should\n     explicitly construct an object of the desired type.\n\n     For a pair type, for example, we would expand `e` into `(e[0], e[1])`.\n     This also can be done to produce an \"η-long\" form: for example,\n     if we have the type `Tuple[int, Tuple[int, int]]`, we would expand\n     that into `(e[0], (e[1][0], e[1][1]))`.\n\nThis key idea, of expanding a term into one that explicitly constructs\nan object of the desired type, is exactly what we need to ensure that\nwe can inject shapes into the output.\n\nAs a set-based query language, we also need to do some extra work to\npreserve the ordering of elements.\n\nOur core rules are:\n    EXPAND_ORDERED(t, e) =\n        WITH enum := enumerate(e)\n        SELECT EXPAND(t, enum.1) ORDER BY enum.0\n\n    EXPAND(tuple<t, s>, p) = (EXPAND(t, p.0), EXPAND(s, p.1))\n\n    EXPAND(array<t>, p) =\n        (p, array_agg(EXPAND_ORDERED(t, array_unpack(p)))).1\n\n    EXPAND(non_collection_type, p) = p\n\nThey are discussed in more detail at the implementation sites.\n\"\"\"\n\n\nfrom __future__ import annotations\n\n\nfrom edb.ir import ast as irast\n\nfrom edb.schema import name as sn\nfrom edb.schema import types as s_types\n\nfrom edb.edgeql import ast as qlast\n\nfrom . import astutils\nfrom . import context\nfrom . import dispatch\nfrom . import setgen\n\n\n# If true, we disregard the optimizations meant to avoid unnecessary\n# expansions. This is useful as a bug-finding tool, since η-expansion\n# found lots of bugs, but mostly in test cases that didn't *really*\n# need it.\nALWAYS_EXPAND = False\n\n\ndef needs_eta_expansion_expr(\n    ir: irast.Expr,\n    stype: s_types.Type,\n    *,\n    ctx: context.ContextLevel,\n) -> bool:\n    \"\"\"Determine if an expr is in need of η-expansion\n\n    In general, any expression of an object-containing\n    tuple or array type needs expansion unless it is:\n        * A tuple literal\n        * An empty array literal\n        * A one-element array literal\n        * A call to array_agg\n    in which none of the arguments are sets that need expansion.\n    \"\"\"\n    if isinstance(ir, irast.SelectStmt):\n        return needs_eta_expansion(\n            ir.result, has_clauses=bool(ir.where or ir.orderby), ctx=ctx)\n\n    if isinstance(stype, s_types.Array):\n        if isinstance(ir, irast.Array):\n            return bool(ir.elements) and (\n                len(ir.elements) != 1\n                or needs_eta_expansion(ir.elements[0], ctx=ctx)\n            )\n        elif (\n            isinstance(ir, irast.FunctionCall)\n            and ir.func_shortname == sn.QualName('std', 'array_agg')\n        ):\n            return needs_eta_expansion(ir.args[0].expr, ctx=ctx)\n        else:\n            return True\n\n    elif isinstance(stype, s_types.Tuple):\n        if isinstance(ir, irast.Tuple):\n            return any(\n                needs_eta_expansion(el.val, ctx=ctx) for el in ir.elements\n            )\n        else:\n            return True\n\n    else:\n        return False\n\n\ndef needs_eta_expansion(\n    ir: irast.Set,\n    *,\n    has_clauses: bool = False,\n    ctx: context.ContextLevel,\n) -> bool:\n    \"\"\"Determine if a set is in need of η-expansion\"\"\"\n    stype = setgen.get_set_type(ir, ctx=ctx)\n\n    if not (\n        isinstance(stype, (s_types.Array, s_types.Tuple))\n        and stype.contains_object(ctx.env.schema)\n    ):\n        return False\n\n    if ALWAYS_EXPAND:\n        return True\n\n    # Object containing arrays always need to be eta expanded if they\n    # might be processed by a clause. This is because the pgsql side\n    # will produce *either* a value or serialized for array_agg/array\n    # literals.\n    if has_clauses and (\n        (subarray := stype.find_array(ctx.env.schema))\n        and subarray.contains_object(ctx.env.schema)\n    ):\n        return True\n\n    # If we are directly projecting an element out of a tuple, we can just\n    # look through to the relevant tuple element. This is probably not\n    # an important optimization to support, but our expansion can generate\n    # this idiom, so on principle I wanted to support it.\n    if (\n        isinstance(ir.expr, irast.TupleIndirectionPointer)\n        and isinstance(ir.expr.source.expr, irast.Tuple)\n    ):\n        name = ir.expr.ptrref.shortname.name\n        els = [x for x in ir.expr.source.expr.elements if x.name == name]\n        if len(els) == 1:\n            return needs_eta_expansion(els[0].val, ctx=ctx)\n\n    if not ir.expr or (\n        ir.is_binding and ir.is_binding != irast.BindingKind.Select\n    ):\n        return True\n\n    return needs_eta_expansion_expr(ir.expr, stype, ctx=ctx)\n\n\ndef _get_alias(\n    name: str, *, ctx: context.ContextLevel\n) -> tuple[str, qlast.Path]:\n    alias = ctx.aliases.get(name)\n    return alias, qlast.Path(\n        steps=[qlast.ObjectRef(name=alias)],\n    )\n\n\ndef eta_expand_ir(\n    ir: irast.Set,\n    *,\n    toplevel: bool=False,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n    \"\"\"η-expansion of an IR set.\n\n    Our core implementation of η-expansion operates on an AST,\n    so this mostly just checks that we really want to expand\n    and then sets up an anchor for the AST based implementation\n    to run on.\n    \"\"\"\n    if (\n        ctx.env.options.schema_object_context\n        or ctx.env.options.func_params\n        or ctx.env.options.schema_view_mode\n    ):\n        return ir\n\n    if not needs_eta_expansion(ir, ctx=ctx):\n        return ir\n\n    with ctx.new() as subctx:\n        subctx.allow_factoring()\n\n        subctx.anchors = subctx.anchors.copy()\n        source_ref = subctx.create_anchor(ir)\n\n        alias, path = _get_alias('eta', ctx=subctx)\n        qry = qlast.SelectQuery(\n            result=eta_expand_ordered(\n                path, setgen.get_set_type(ir, ctx=subctx), ctx=subctx\n            ),\n            aliases=[\n                qlast.AliasedExpr(alias=alias, expr=source_ref)\n            ],\n        )\n        if toplevel:\n            subctx.toplevel_stmt = None\n        return dispatch.compile(qry, ctx=subctx)\n\n\ndef eta_expand_ordered(\n    expr: qlast.Expr,\n    stype: s_types.Type,\n    *,\n    ctx: context.ContextLevel,\n) -> qlast.Expr:\n    \"\"\"Do an order-preserving η-expansion\n\n    Unlike in the lambda calculus, edgeql is a set-based language\n    with a notion of ordering, which we need to preserve.\n    We do this by using enumerate and ORDER BY on it:\n        EXPAND_ORDERED(t, e) =\n            WITH enum := enumerate(e)\n            SELECT EXPAND(t, enum.1) ORDER BY enum.0\n    \"\"\"\n    enumerated = qlast.FunctionCall(\n        func=('__std__', 'enumerate'), args=[expr]\n    )\n\n    enumerated_alias, enumerated_path = _get_alias('enum', ctx=ctx)\n\n    element_path = astutils.extend_path(enumerated_path, '1')\n    result_expr = eta_expand(element_path, stype, ctx=ctx)\n\n    return qlast.SelectQuery(\n        result=result_expr,\n        orderby=[\n            qlast.SortExpr(path=astutils.extend_path(enumerated_path, '0'))\n        ],\n        aliases=[\n            qlast.AliasedExpr(alias=enumerated_alias, expr=enumerated)\n        ],\n    )\n\n\ndef eta_expand(\n    path: qlast.Path,\n    stype: s_types.Type,\n    *,\n    ctx: context.ContextLevel,\n) -> qlast.Expr:\n    \"\"\"η-expansion of an AST path\"\"\"\n    if not ALWAYS_EXPAND and not stype.contains_object(ctx.env.schema):\n        # This isn't strictly right from a \"fully η expanding\" perspective,\n        # but for our uses, we only need to make sure that objects are\n        # exposed to the output, so we can skip anything not containing one.\n        return path\n\n    if isinstance(stype, s_types.Array):\n        return eta_expand_array(path, stype, ctx=ctx)\n\n    elif isinstance(stype, s_types.Tuple):\n        return eta_expand_tuple(path, stype, ctx=ctx)\n\n    else:\n        return path\n\n\ndef eta_expand_tuple(\n    path: qlast.Path,\n    stype: s_types.Tuple,\n    *,\n    ctx: context.ContextLevel,\n) -> qlast.Expr:\n    \"\"\"η-expansion of tuples\n\n    η-expansion of tuple types is straightforward and traditional:\n        EXPAND(tuple<t, s>, p) = (EXPAND(t, p.0), EXPAND(s, p.1))\n    is the case for pairs. n-ary and named cases are generalized in the\n    obvious way.\n    The one exception is that the expansion of the empty tuple type is\n    `p` and not `()`, to ensure that the path appears in the output.\n    \"\"\"\n    if not stype.get_subtypes(ctx.env.schema):\n        return path\n\n    els = [\n        qlast.TupleElement(\n            name=qlast.Ptr(name=name),\n            val=eta_expand(astutils.extend_path(path, name), subtype, ctx=ctx),\n        )\n        for name, subtype in stype.iter_subtypes(ctx.env.schema)\n    ]\n\n    if stype.is_named(ctx.env.schema):\n        return qlast.NamedTuple(elements=els)\n    else:\n        return qlast.Tuple(elements=[el.val for el in els])\n\n\ndef eta_expand_array(\n    path: qlast.Path,\n    stype: s_types.Array,\n    *,\n    ctx: context.ContextLevel,\n) -> qlast.Expr:\n    \"\"\"η-expansion of arrays\n\n    η-expansion of array types is is a little peculiar to edgeql and less\n    grounded in typed lambda calculi:\n        EXPAND(array<t>, p) =\n            (p, array_agg(EXPAND_ORDERED(t, array_unpack(p)))).1\n\n    We use a similar approach for compiling casts.\n\n    The tuple projection trick serves to make sure that we iterate over\n    `p` *outside* of the array_agg (or else all the arrays would get\n    aggregated together) as well as ensuring that `p` appears in the expansion\n    in a non-fenced position (or else sorting it from outside wouldn't work).\n\n    (If it wasn't for the latter requirement, we could just use a FOR.\n    I find it a little unsatisfying that our η-expansion needs to use this\n    trick, and the pgsql compiler needed to be hacked to make it work.)\n    \"\"\"\n\n    unpacked = qlast.FunctionCall(\n        func=('__std__', 'array_unpack'), args=[path]\n    )\n\n    expanded = eta_expand_ordered(\n        unpacked, stype.get_element_type(ctx.env.schema), ctx=ctx)\n\n    agg_expr = qlast.FunctionCall(\n        func=('__std__', 'array_agg'), args=[expanded]\n    )\n\n    return astutils.extend_path(\n        qlast.Tuple(elements=[path, agg_expr]), '1'\n    )\n"
  },
  {
    "path": "edb/edgeql/compiler/expr.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL non-statement expression compilation functions.\"\"\"\n\n\nfrom __future__ import annotations\n\nfrom typing import Callable, Optional, Sequence, cast\n\nfrom edb import errors\n\nfrom edb.common import parsing\nfrom edb.common import span as edb_span\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtyputils\nfrom edb.ir import utils as irutils\n\nfrom edb.schema import constraints as s_constr\nfrom edb.schema import functions as s_func\nfrom edb.schema import globals as s_globals\nfrom edb.schema import indexes as s_indexes\nfrom edb.schema import name as sn\nfrom edb.schema import objects as so\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import permissions as s_permissions\nfrom edb.schema import pseudo as s_pseudo\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import schema as s_schema\nfrom edb.schema import types as s_types\nfrom edb.schema import utils as s_utils\n\nfrom edb.edgeql import ast as qlast\n\nfrom . import astutils\nfrom . import casts\nfrom . import context\nfrom . import dispatch\nfrom . import pathctx\nfrom . import schemactx\nfrom . import setgen\nfrom . import stmt\nfrom . import tuple_args\nfrom . import typegen\n\nfrom . import func  # NOQA\n\n\n@dispatch.compile.register(qlast.OptionalExpr)\ndef compile__Optional(\n    expr: qlast.OptionalExpr, *, ctx: context.ContextLevel\n) -> irast.Set:\n\n    result = dispatch.compile(expr.expr, ctx=ctx)\n\n    pathctx.register_set_in_scope(result, optional=True, ctx=ctx)\n\n    return result\n\n\n@dispatch.compile.register(qlast.Path)\ndef compile_Path(expr: qlast.Path, *, ctx: context.ContextLevel) -> irast.Set:\n    if ctx.no_factoring and not expr.allow_factoring:\n        res = dispatch.compile(\n            qlast.SelectQuery(\n                result=expr.replace(allow_factoring=True),\n                implicit=True,\n            ),\n            ctx=ctx,\n        )\n        # Mark the nodes as having been protected from factoring. At\n        # the end of compilation, we see if we can eliminate the\n        # scopes without inducing factoring.\n        #\n        # Don't do this if the head of the path is an expression\n        # (instead of an ObjectRef), though, because that interacts\n        # badly with function inlining in some cases??\n        # (test_edgeql_functions_inline_object_06).\n        # My hope is to just destroy all this machinery instead of tracking\n        # that interaction down, though.\n        if expr.partial or not isinstance(expr.steps[0], qlast.Expr):\n            res.is_factoring_protected = True\n        return res\n\n    return stmt.maybe_add_view(setgen.compile_path(expr, ctx=ctx), ctx=ctx)\n\n\ndef _balance(\n    elements: Sequence[qlast.Expr],\n    ctor: Callable[\n        [qlast.Expr, qlast.Expr, Optional[qlast.Span]],\n        qlast.Expr\n    ],\n    span: Optional[qlast.Span],\n) -> qlast.Expr:\n    mid = len(elements) // 2\n    ls, rs = elements[:mid], elements[mid:]\n    ls_span = rs_span = None\n    if len(ls) > 1 and ls[0].span and ls[-1].span:\n        ls_span = edb_span.merge_spans([\n            ls[0].span, ls[-1].span\n        ])\n    if len(rs) > 1 and rs[0].span and rs[-1].span:\n        rs_span = edb_span.merge_spans([\n            rs[0].span, rs[-1].span])\n\n    return ctor(\n        (\n            _balance(ls, ctor, ls_span)\n            if len(ls) > 1 else ls[0]\n        ),\n        (\n            _balance(rs, ctor, rs_span)\n            if len(rs) > 1 else rs[0]\n        ),\n        span,\n    )\n\n\nREBALANCED_OPS = {'UNION'}\nREBALANCE_THRESHOLD = 10\n\n\n@dispatch.compile.register(qlast.BinOp)\ndef compile_BinOp(expr: qlast.BinOp, *, ctx: context.ContextLevel) -> irast.Set:\n    # Rebalance some associative operations to avoid deeply nested ASTs\n    if expr.op in REBALANCED_OPS and not expr.rebalanced:\n        elements = collect_binop(expr)\n        # Don't bother rebalancing small groups\n        if len(elements) >= REBALANCE_THRESHOLD:\n            balanced = _balance(\n                elements,\n                lambda l, r, s: qlast.BinOp(\n                    left=l, right=r, op=expr.op, rebalanced=True, span=s\n                ),\n                expr.span\n            )\n            return dispatch.compile(balanced, ctx=ctx)\n\n    if expr.op == '??' and astutils.contains_dml(expr.right, ctx=ctx):\n        return _compile_dml_coalesce(expr, ctx=ctx)\n\n    return func.compile_operator(\n        expr, op_name=expr.op, qlargs=[expr.left, expr.right], ctx=ctx\n    )\n\n\n@dispatch.compile.register(qlast.IsOp)\ndef compile_IsOp(expr: qlast.IsOp, *, ctx: context.ContextLevel) -> irast.Set:\n    op_node = compile_type_check_op(expr, ctx=ctx)\n    return setgen.ensure_set(op_node, ctx=ctx)\n\n\n@dispatch.compile.register\ndef compile_StrInterp(\n    expr: qlast.StrInterp, *, ctx: context.ContextLevel\n) -> irast.Set:\n    strs: list[qlast.Expr] = []\n    strs.append(qlast.Constant.string(expr.prefix))\n\n    str_type = qlast.TypeName(\n        maintype=qlast.ObjectRef(module='__std__', name='str')\n    )\n    for fragment in expr.interpolations:\n        strs.append(qlast.TypeCast(\n            expr=fragment.expr, type=str_type\n        ))\n        strs.append(qlast.Constant.string(fragment.suffix))\n\n    call = qlast.FunctionCall(\n        func=('__std__', 'array_join'),\n        args=[qlast.Array(elements=strs), qlast.Constant.string('')],\n    )\n\n    return dispatch.compile(call, ctx=ctx)\n\n\n@dispatch.compile.register(qlast.DetachedExpr)\ndef compile_DetachedExpr(\n    expr: qlast.DetachedExpr,\n    *,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n    with ctx.detached() as subctx:\n        if expr.preserve_path_prefix:\n            subctx.partial_path_prefix = ctx.partial_path_prefix\n\n        ir = dispatch.compile(expr.expr, ctx=subctx)\n    # Wrap the result in another set, so that the inner namespace\n    # doesn't leak out into any shapes (since computable computation\n    # will pull namespaces from the source path_ids.)\n    return setgen.ensure_set(setgen.ensure_stmt(ir, ctx=ctx), ctx=ctx)\n\n\n@dispatch.compile.register(qlast.Set)\ndef compile_Set(expr: qlast.Set, *, ctx: context.ContextLevel) -> irast.Set:\n    # after flattening the set may still end up with 0 or 1 element,\n    # which are treated as a special case\n    elements = flatten_set(expr)\n\n    if elements:\n        if len(elements) == 1:\n            # From the scope perspective, single-element set\n            # literals are equivalent to a binary UNION with\n            # an empty set, not to the element.\n            return dispatch.compile(\n                astutils.ensure_ql_query(elements[0]), ctx=ctx\n            )\n        else:\n            # Turn it into a tree of UNIONs so we only blow up the nesting\n            # depth logarithmically.\n            # TODO: Introduce an N-ary operation that handles the whole thing?\n            bigunion = _balance(\n                elements,\n                lambda l, r, s: qlast.BinOp(\n                    left=l, op='UNION', right=r,\n                    rebalanced=True, set_constructor=True, span=s\n                ),\n                expr.span\n            )\n            res = dispatch.compile(bigunion, ctx=ctx)\n            if cres := try_constant_set(res):\n                res = setgen.ensure_set(cres, span=res.span, ctx=ctx)\n            return res\n    else:\n        return setgen.new_empty_set(\n            alias=ctx.aliases.get('e'),\n            ctx=ctx,\n            span=expr.span,\n        )\n\n\n@dispatch.compile.register(qlast.Constant)\ndef compile_Constant(\n    expr: qlast.Constant, *, ctx: context.ContextLevel\n) -> irast.Set:\n    value = expr.value\n\n    node_cls: type[irast.BaseConstant]\n\n    if expr.kind == qlast.ConstantKind.STRING:\n        std_type = sn.QualName('std', 'str')\n        node_cls = irast.StringConstant\n    elif expr.kind == qlast.ConstantKind.INTEGER:\n        value = value.replace(\"_\", \"\")\n        std_type = sn.QualName('std', 'int64')\n        node_cls = irast.IntegerConstant\n    elif expr.kind == qlast.ConstantKind.FLOAT:\n        value = value.replace(\"_\", \"\")\n        std_type = sn.QualName('std', 'float64')\n        node_cls = irast.FloatConstant\n    elif expr.kind == qlast.ConstantKind.DECIMAL:\n        assert value[-1] == 'n'\n        value = value[:-1].replace(\"_\", \"\")\n        std_type = sn.QualName('std', 'decimal')\n        node_cls = irast.DecimalConstant\n    elif expr.kind == qlast.ConstantKind.BIGINT:\n        assert value[-1] == 'n'\n        value = value[:-1].replace(\"_\", \"\")\n        std_type = sn.QualName('std', 'bigint')\n        node_cls = irast.BigintConstant\n    elif expr.kind == qlast.ConstantKind.BOOLEAN:\n        std_type = sn.QualName('std', 'bool')\n        node_cls = irast.BooleanConstant\n    else:\n        raise RuntimeError(f'unexpected constant type: {expr.kind}')\n\n    ct = typegen.type_to_typeref(\n        ctx.env.get_schema_type_and_track(std_type),\n        env=ctx.env,\n    )\n    ir_expr = node_cls(value=value, typeref=ct, span=expr.span)\n    return setgen.ensure_set(ir_expr, ctx=ctx)\n\n\n@dispatch.compile.register(qlast.BytesConstant)\ndef compile_BytesConstant(\n    expr: qlast.BytesConstant, *, ctx: context.ContextLevel\n) -> irast.Set:\n    std_type = sn.QualName('std', 'bytes')\n\n    ct = typegen.type_to_typeref(\n        ctx.env.get_schema_type_and_track(std_type),\n        env=ctx.env,\n    )\n    return setgen.ensure_set(\n        irast.BytesConstant(value=expr.value, typeref=ct), ctx=ctx\n    )\n\n\n@dispatch.compile.register(qlast.NamedTuple)\ndef compile_NamedTuple(\n    expr: qlast.NamedTuple, *, ctx: context.ContextLevel\n) -> irast.Set:\n\n    names = set()\n    elements = []\n    for el in expr.elements:\n        name = el.name.name\n        if name in names:\n            raise errors.QueryError(\n                f\"named tuple has duplicate field '{name}'\",\n                span=el.span)\n        names.add(name)\n\n        element = irast.TupleElement(\n            name=name,\n            val=dispatch.compile(el.val, ctx=ctx),\n        )\n        elements.append(element)\n\n    return setgen.new_tuple_set(elements, named=True, ctx=ctx)\n\n\n@dispatch.compile.register(qlast.Tuple)\ndef compile_Tuple(expr: qlast.Tuple, *, ctx: context.ContextLevel) -> irast.Set:\n\n    elements = []\n    for i, el in enumerate(expr.elements):\n        element = irast.TupleElement(\n            name=str(i),\n            val=dispatch.compile(el, ctx=ctx),\n        )\n        elements.append(element)\n\n    return setgen.new_tuple_set(elements, named=False, ctx=ctx)\n\n\n@dispatch.compile.register(qlast.Array)\ndef compile_Array(expr: qlast.Array, *, ctx: context.ContextLevel) -> irast.Set:\n    elements = [dispatch.compile(e, ctx=ctx) for e in expr.elements]\n    return setgen.new_array_set(elements, ctx=ctx, span=expr.span)\n\n\ndef _compile_dml_coalesce(\n    expr: qlast.BinOp, *, ctx: context.ContextLevel\n) -> irast.Set:\n    \"\"\"Transform a coalesce that contains DML into FOR loops\n\n    The basic approach is to extract the pieces from the ?? and\n    rewrite them into:\n        for optional x in (LHS) union (\n          {\n            x,\n            (for _ in (select () filter not exists x) union (RHS)),\n          }\n        )\n\n    Optional for is needed because the LHS needs to be bound in a for\n    in order to get put in a CTE and only executed once, but the RHS\n    needs to be dependent on the LHS being empty.\n    \"\"\"\n    with ctx.newscope(fenced=False) as subctx:\n        # We have to compile it under a factoring fence to prevent\n        # correlation with outside things. We can't just rely on the\n        # factoring fences inserted when compiling the FORs, since we\n        # are going to need to explicitly exempt the iterator\n        # expression from that.\n        subctx.path_scope.factoring_fence = True\n        subctx.path_scope.factoring_allowlist.update(ctx.iterator_path_ids)\n\n        ir = func.compile_operator(\n            expr, op_name=expr.op, qlargs=[expr.left, expr.right], ctx=subctx)\n\n        # Extract the IR parts from the ??\n        # Note that lhs_ir will be unfenced while rhs_ir\n        # will have been compiled under fences.\n        match ir.expr:\n            case irast.OperatorCall(args={\n                0: irast.CallArg(expr=lhs_ir),\n                1: irast.CallArg(expr=rhs_ir),\n            }):\n                pass\n            case _:\n                raise AssertionError('malformed DML ??')\n\n        subctx.anchors = subctx.anchors.copy()\n\n        alias = ctx.aliases.get('_coalesce_x')\n        cond_path = qlast.Path(\n            steps=[qlast.ObjectRef(name=alias)],\n        )\n\n        rhs_b = qlast.ForQuery(\n            iterator_alias=ctx.aliases.get('_coalesce_dummy'),\n            iterator=qlast.SelectQuery(\n                result=qlast.Tuple(elements=[]),\n                where=qlast.UnaryOp(\n                    op='NOT',\n                    operand=qlast.UnaryOp(op='EXISTS', operand=cond_path),\n                ),\n            ),\n            result=subctx.create_anchor(\n                rhs_ir, move_scope=True, check_dml=True\n            ),\n        )\n\n        full = qlast.ForQuery(\n            iterator_alias=alias,\n            iterator=subctx.create_anchor(lhs_ir, 'b'),\n            result=qlast.Set(elements=[cond_path, rhs_b]),\n            optional=True,\n            from_desugaring=True,\n        )\n\n        subctx.iterator_path_ids |= {lhs_ir.path_id}\n        res = dispatch.compile(full, ctx=subctx)\n        # Indicate that the original ?? code should determine the\n        # cardinality/multiplicity.\n        assert isinstance(res.expr, irast.SelectStmt)\n        res.expr.card_inference_override = ir\n\n        return res\n\n\ndef _compile_dml_ifelse(\n    expr: qlast.IfElse, *, ctx: context.ContextLevel\n) -> irast.Set:\n    \"\"\"Transform an IF/ELSE that contains DML into FOR loops\n\n    The basic approach is to extract the pieces from the if/then/else and\n    rewrite them into:\n        for b in COND union (\n          {\n            (for _ in (select () filter b) union (IF_BRANCH)),\n            (for _ in (select () filter not b) union (ELSE_BRANCH)),\n          }\n        )\n    \"\"\"\n\n    with ctx.newscope(fenced=False) as subctx:\n        # We have to compile it under a factoring fence to prevent\n        # correlation with outside things. We can't just rely on the\n        # factoring fences inserted when compiling the FORs, since we\n        # are going to need to explicitly exempt the iterator\n        # expression from that.\n        subctx.path_scope.factoring_fence = True\n        subctx.path_scope.factoring_allowlist.update(ctx.iterator_path_ids)\n\n        ir = func.compile_operator(\n            expr, op_name='std::IF',\n            qlargs=[expr.if_expr, expr.condition, expr.else_expr], ctx=subctx)\n\n        # Extract the IR parts from the IF/THEN/ELSE\n        # Note that cond_ir will be unfenced while if_ir and else_ir\n        # will have been compiled under fences.\n        match ir.expr:\n            case irast.OperatorCall(args={\n                0: irast.CallArg(expr=if_ir),\n                1: irast.CallArg(expr=cond_ir),\n                2: irast.CallArg(expr=else_ir),\n            }):\n                pass\n            case _:\n                raise AssertionError('malformed DML IF/ELSE')\n\n        subctx.anchors = subctx.anchors.copy()\n\n        alias = ctx.aliases.get('_ifelse_b')\n        cond_path = qlast.Path(\n            steps=[qlast.ObjectRef(name=alias)],\n        )\n\n        els: list[qlast.Expr] = []\n\n        if not isinstance(irutils.unwrap_set(if_ir).expr, irast.EmptySet):\n            if_b = qlast.ForQuery(\n                iterator_alias=ctx.aliases.get('_ifelse_true_dummy'),\n                iterator=qlast.SelectQuery(\n                    result=qlast.Tuple(elements=[]),\n                    where=cond_path,\n                ),\n                result=subctx.create_anchor(\n                    if_ir, move_scope=True, check_dml=True\n                ),\n            )\n            els.append(if_b)\n\n        if not isinstance(irutils.unwrap_set(else_ir).expr, irast.EmptySet):\n            else_b = qlast.ForQuery(\n                iterator_alias=ctx.aliases.get('_ifelse_false_dummy'),\n                iterator=qlast.SelectQuery(\n                    result=qlast.Tuple(elements=[]),\n                    where=qlast.UnaryOp(op='NOT', operand=cond_path),\n                ),\n                result=subctx.create_anchor(\n                    else_ir, move_scope=True, check_dml=True\n                ),\n            )\n            els.append(else_b)\n\n        full = qlast.ForQuery(\n            iterator_alias=alias,\n            iterator=subctx.create_anchor(cond_ir, 'b'),\n            result=qlast.Set(elements=els) if len(els) != 1 else els[0],\n        )\n\n        subctx.iterator_path_ids |= {cond_ir.path_id}\n        res = dispatch.compile(full, ctx=subctx)\n        # Indicate that the original IF/ELSE code should determine the\n        # cardinality/multiplicity.\n        assert isinstance(res.expr, irast.SelectStmt)\n        res.expr.card_inference_override = ir\n\n        return res\n\n\n@dispatch.compile.register(qlast.IfElse)\ndef compile_IfElse(\n    expr: qlast.IfElse, *, ctx: context.ContextLevel\n) -> irast.Set:\n\n    if (\n        astutils.contains_dml(expr.if_expr, ctx=ctx)\n        or astutils.contains_dml(expr.else_expr, ctx=ctx)\n    ):\n        return _compile_dml_ifelse(expr, ctx=ctx)\n\n    res = func.compile_operator(\n        expr, op_name='std::IF',\n        qlargs=[expr.if_expr, expr.condition, expr.else_expr], ctx=ctx)\n\n    return res\n\n\n@dispatch.compile.register(qlast.UnaryOp)\ndef compile_UnaryOp(\n    expr: qlast.UnaryOp, *, ctx: context.ContextLevel\n) -> irast.Set:\n\n    return func.compile_operator(\n        expr, op_name=expr.op, qlargs=[expr.operand], ctx=ctx)\n\n\ndef _cache_as_type_rewrite(\n    target: irast.Set,\n    stype: s_types.Type,\n    populate: Callable[[], irast.Set],\n    *,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n    key = (stype, False)\n    if not ctx.env.type_rewrites.get(key):\n        ctx.env.type_rewrites[key] = populate()\n    rewrite_target = ctx.env.type_rewrites[key]\n\n    # We need to have the set with expr=None, so that the rewrite\n    # will be applied, but we also need to wrap it with a\n    # card_inference_override so that we use the real cardinality\n    # instead of assuming it is MANY.\n    assert isinstance(rewrite_target, irast.Set)\n    typeref = typegen.type_to_typeref(stype, env=ctx.env)\n    target = setgen.new_set_from_set(\n        target,\n        expr=irast.TypeRoot(typeref=typeref, is_cached_global=True),\n        stype=stype,\n        ctx=ctx,\n    )\n    wrap = irast.SelectStmt(\n        result=target,\n        card_inference_override=rewrite_target,\n    )\n    return setgen.new_set_from_set(target, expr=wrap, ctx=ctx)\n\n\n@dispatch.compile.register(qlast.GlobalExpr)\ndef compile_GlobalExpr(\n    expr: qlast.GlobalExpr, *, ctx: context.ContextLevel\n) -> irast.Set:\n    # The expr object can be either a Permission or Global.\n    # Get an Object and manually check for correct type and None.\n    expr_schema_name = s_utils.ast_ref_to_name(expr.name)\n    glob = ctx.env.get_schema_object_and_track(\n        expr_schema_name,\n        expr.name,\n        default=None,\n        modaliases=ctx.modaliases,\n        type=so.Object,\n    )\n\n    # Check for None first.\n    if glob is None:\n        # If no object is found, we want to raise an error with 'global' as\n        # the desired type.\n        # If we let `get_schema_object_and_track`, the error will contain\n        # 'object' instead.\n        s_schema.Schema.raise_bad_reference(\n            expr_schema_name,\n            module_aliases=ctx.modaliases,\n            span=expr.span,\n            type=s_globals.Global,\n        )\n\n    # Check for incorrect type\n    if not isinstance(glob, (s_globals.Global, s_permissions.Permission)):\n        s_schema.Schema.raise_wrong_type(\n            expr_schema_name,\n            glob.__class__,\n            s_globals.Global,\n            span=expr.span,\n        )\n        # Raise an error here so mypy knows that expr_obj can only be a global\n        # or permission past this point.\n        raise AssertionError('should never happen')\n\n    if (\n        # Computed global\n        isinstance(glob, s_globals.Global)\n        and glob.is_computable(ctx.env.schema)\n    ):\n        obj_ref = s_utils.name_to_ast_ref(\n            glob.get_target(ctx.env.schema).get_name(ctx.env.schema))\n        # Wrap the reference in a subquery so that it does not get\n        # factored out or go directly into the scope tree.\n        qry = qlast.SelectQuery(result=qlast.Path(steps=[obj_ref]))\n        target = dispatch.compile(qry, ctx=ctx)\n\n        # If the global is single, use type_rewrites to make sure it\n        # is computed only once in the SQL query.\n        if glob.get_cardinality(ctx.env.schema).is_single():\n            def _populate() -> irast.Set:\n                with ctx.detached() as dctx:\n                    # The official rewrite needs to be in a detached\n                    # scope to avoid collisions; this won't really\n                    # recompile the whole thing, it will hit a cache\n                    # of the view.\n                    return dispatch.compile(qry, ctx=dctx)\n\n            target = _cache_as_type_rewrite(\n                target,\n                setgen.get_set_type(target, ctx=ctx),\n                populate=_populate,\n                ctx=ctx,\n            )\n\n        return target\n\n    default_ql: Optional[qlast.Expr] = None\n    if isinstance(glob, s_globals.Global):\n        if default_expr := glob.get_default(ctx.env.schema):\n            default_ql = default_expr.parse()\n\n    # If we are compiling with globals suppressed but still allowed, always\n    # treat it as being empty.\n    if ctx.env.options.make_globals_empty:\n        if isinstance(glob, s_permissions.Permission):\n            return dispatch.compile(qlast.Constant.boolean(False), ctx=ctx)\n        elif default_ql:\n            return dispatch.compile(default_ql, ctx=ctx)\n        else:\n            return setgen.new_empty_set(\n                stype=glob.get_target(ctx.env.schema), ctx=ctx\n            )\n\n    objctx = ctx.env.options.schema_object_context\n    if objctx in (s_constr.Constraint, s_indexes.Index):\n        typname = objctx.get_schema_class_displayname()\n        raise errors.SchemaDefinitionError(\n            f'global variables cannot be referenced from {typname}',\n            span=expr.span)\n\n    param_set: qlast.Expr | irast.Set\n    present_set: qlast.Expr | irast.Set | None\n    if (\n        ctx.env.options.func_params is None\n        and not ctx.env.options.json_parameters\n    ):\n        param_set, present_set = setgen.get_global_param_sets(\n            glob, ctx=ctx,\n        )\n    else:\n        param_set, present_set = setgen.get_func_global_param_sets(\n            glob, ctx=ctx\n        )\n\n        if isinstance(glob, s_permissions.Permission):\n            # Globals are assumed to be optional within functions. However,\n            # permissions always have a value. Provide a default value to\n            # reassure the cardinality checks.\n            default_ql = qlast.Constant.boolean(False)\n\n    if default_ql and not present_set:\n        # If we have a default value and the global is required,\n        # then we can use the param being {} as a signal to use\n        # the default.\n        with ctx.new() as subctx:\n            subctx.anchors = subctx.anchors.copy()\n            main_param = subctx.maybe_create_anchor(param_set, 'glob')\n            param_set = func.compile_operator(\n                expr,\n                op_name='std::??',\n                qlargs=[main_param, default_ql],\n                ctx=subctx\n            )\n    elif default_ql and present_set:\n        # ... but if {} is a valid value for the global, we need to\n        # stick in an extra parameter to indicate whether to use\n        # the default.\n        with ctx.new() as subctx:\n            subctx.anchors = subctx.anchors.copy()\n            main_param = subctx.maybe_create_anchor(param_set, 'glob')\n\n            present_param = subctx.maybe_create_anchor(present_set, 'present')\n\n            param_set = func.compile_operator(\n                expr,\n                op_name='std::IF',\n                qlargs=[main_param, present_param, default_ql],\n                ctx=subctx\n            )\n    elif not isinstance(param_set, irast.Set):\n        param_set = dispatch.compile(param_set, ctx=ctx)\n\n    # When we are compiling a global as something we are pulling out\n    # of JSON, arrange to cache it as a type rewrite. This can have\n    # big performance wins.\n    if (\n        not ctx.env.options.schema_object_context\n        and not (\n            ctx.env.options.func_params is None\n            and not ctx.env.options.json_parameters\n        )\n        # TODO: support this for permissions too?\n        # OR! Don't put the permissions into the globals JSON?\n        and isinstance(glob, s_globals.Global)\n    ):\n        name = glob.get_name(ctx.env.schema)\n        if name not in ctx.env.query_globals_types:\n            # HACK: We have mechanism for caching based on type... so\n            # make up a type.\n            # I would like us to be less type-forward though.\n            ctx.env.query_globals_types[name] = (\n                schemactx.derive_view(glob.get_target(ctx.env.schema), ctx=ctx)\n            )\n        ty = ctx.env.query_globals_types[name]\n        param_set = _cache_as_type_rewrite(\n            param_set, ty, lambda: param_set, ctx=ctx\n        )\n\n    return param_set\n\n\n@dispatch.compile.register(qlast.TypeCast)\ndef compile_TypeCast(\n    expr: qlast.TypeCast, *, ctx: context.ContextLevel\n) -> irast.Set:\n    try:\n        target_stype = typegen.ql_typeexpr_to_type(expr.type, ctx=ctx)\n    except errors.InvalidReferenceError as e:\n        if (\n            e.hint is None\n            and isinstance(expr.type, qlast.TypeName)\n            and isinstance(expr.type.maintype, qlast.ObjectRef)\n        ):\n            s_utils.enrich_schema_lookup_error(\n                e,\n                s_utils.ast_ref_to_name(expr.type.maintype),\n                modaliases=ctx.modaliases,\n                schema=ctx.env.schema,\n                suggestion_limit=1,\n                item_type=s_func.Function,\n                hint_text='did you mean to call'\n            )\n        raise\n\n    ir_expr: irast.Set | irast.Expr\n\n    if isinstance(expr.expr, (qlast.QueryParameter, qlast.FunctionParameter)):\n        if (\n            # generic types not explicitly allowed\n            not ctx.env.options.allow_generic_type_output and\n            # not compiling a function which hadles its own generic types\n            ctx.env.options.func_name is None and\n            target_stype.is_polymorphic(ctx.env.schema)\n        ):\n            raise errors.QueryError(\n                f'parameter cannot be a generic type '\n                f'{target_stype.get_displayname(ctx.env.schema)!r}',\n                hint=\"Please ensure you don't use generic \"\n                     '\"any\" types or abstract scalars.',\n                span=expr.span)\n\n        pt = typegen.ql_typeexpr_to_type(expr.type, ctx=ctx)\n\n        param_name = expr.expr.name\n        if expr.cardinality_mod:\n            if expr.cardinality_mod == qlast.CardinalityModifier.Optional:\n                required = False\n            elif expr.cardinality_mod == qlast.CardinalityModifier.Required:\n                required = True\n            else:\n                raise NotImplementedError(\n                    f\"cardinality modifier {expr.cardinality_mod}\")\n        else:\n            required = True\n\n        parameter_type = (\n            irast.QueryParameter\n            if isinstance(expr.expr, qlast.QueryParameter) else\n            irast.FunctionParameter\n        )\n\n        typeref = typegen.type_to_typeref(pt, env=ctx.env)\n        param = setgen.ensure_set(\n            parameter_type(\n                typeref=typeref,\n                name=param_name,\n                required=required,\n                span=expr.expr.span,\n            ),\n            ctx=ctx,\n        )\n\n        if ex_param := ctx.env.script_params.get(param_name):\n            # N.B. Accessing the schema_type from the param is unreliable\n            ctx.env.schema, param_first_type = irtyputils.ir_typeref_to_type(\n                ctx.env.schema, ex_param.ir_type)\n            if param_first_type != pt:\n                raise errors.QueryError(\n                    f'parameter type '\n                    f'{pt.get_displayname(ctx.env.schema)} '\n                    f'does not match original type '\n                    f'{param_first_type.get_displayname(ctx.env.schema)}',\n                    span=expr.expr.span)\n\n        if param_name not in ctx.env.query_parameters:\n            sub_params = None\n            if ex_param and ex_param.sub_params:\n                sub_params = tuple_args.finish_sub_params(\n                    ex_param.sub_params, ctx=ctx)\n\n            ctx.env.query_parameters[param_name] = irast.Param(\n                name=param_name,\n                required=required,\n                schema_type=pt,\n                ir_type=typeref,\n                sub_params=sub_params,\n            )\n\n        return param\n\n    with ctx.new() as subctx:\n        if target_stype.contains_json(subctx.env.schema):\n            # JSON wants type shapes and acts as an output sink.\n            subctx.expr_exposed = context.Exposure.EXPOSED\n            subctx.implicit_limit = 0\n            subctx.implicit_id_in_shapes = False\n            subctx.implicit_tid_in_shapes = False\n            subctx.implicit_tname_in_shapes = False\n\n        ir_expr = dispatch.compile(expr.expr, ctx=subctx)\n        orig_stype = setgen.get_set_type(ir_expr, ctx=ctx)\n\n        use_message_context = False\n        if target_stype.is_collection() and subctx.collection_cast_info is None:\n            subctx.collection_cast_info = context.CollectionCastInfo(\n                from_type=orig_stype,\n                to_type=target_stype,\n                path_elements=[]\n            )\n\n            use_message_context = (\n                orig_stype.is_array() and target_stype.is_array()\n                or (\n                    orig_stype.is_tuple(ctx.env.schema)\n                    and target_stype.is_tuple(ctx.env.schema)\n                )\n            )\n\n        try:\n            res = casts.compile_cast(\n                ir_expr,\n                target_stype,\n                cardinality_mod=expr.cardinality_mod,\n                ctx=subctx,\n                span=expr.span,\n            )\n\n        except errors.QueryError as e:\n            if (\n                (message_context := casts.cast_message_context(subctx))\n                and use_message_context\n            ):\n                e.args = (\n                    (message_context + e.args[0],)\n                    + e.args[1:]\n                )\n            raise e\n\n    return stmt.maybe_add_view(res, ctx=ctx)\n\n\ndef _infer_type_introspection(\n    typeref: irast.TypeRef,\n    env: context.Environment,\n    span: Optional[parsing.Span]=None,\n) -> s_types.Type:\n    if irtyputils.is_scalar(typeref):\n        return env.schema.get_by_name(\n            'schema::ScalarType', type=s_objtypes.ObjectType\n        )\n    elif irtyputils.is_object(typeref):\n        return env.schema.get_by_name(\n            'schema::ObjectType', type=s_objtypes.ObjectType\n        )\n    elif irtyputils.is_array(typeref):\n        return env.schema.get_by_name(\n            'schema::Array', type=s_objtypes.ObjectType\n        )\n    elif irtyputils.is_tuple(typeref):\n        return env.schema.get_by_name(\n            'schema::Tuple', type=s_objtypes.ObjectType\n        )\n    elif irtyputils.is_range(typeref):\n        return env.schema.get_by_name(\n            'schema::Range', type=s_objtypes.ObjectType\n        )\n    elif irtyputils.is_multirange(typeref):\n        return env.schema.get_by_name(\n            'schema::MultiRange', type=s_objtypes.ObjectType\n        )\n    else:\n        raise errors.QueryError(\n            'unexpected type in INTROSPECT', span=span)\n\n\n@dispatch.compile.register(qlast.Introspect)\ndef compile_Introspect(\n    expr: qlast.Introspect, *, ctx: context.ContextLevel\n) -> irast.Set:\n\n    typeref = typegen.ql_typeexpr_to_ir_typeref(expr.type, ctx=ctx)\n    if typeref.material_type and not irtyputils.is_object(typeref):\n        typeref = typeref.material_type\n    if typeref.is_opaque_union:\n        typeref = typegen.type_to_typeref(\n            ctx.env.schema.get_by_name(\n                'std::BaseObject', type=s_objtypes.ObjectType\n            ),\n            env=ctx.env,\n        )\n\n    if irtyputils.is_view(typeref):\n        raise errors.QueryError(\n            f'cannot introspect transient type variant',\n            span=expr.type.span)\n    if irtyputils.is_collection(typeref):\n        raise errors.QueryError(\n            f'cannot introspect collection types',\n            span=expr.type.span)\n    if irtyputils.is_generic(typeref):\n        raise errors.QueryError(\n            f'cannot introspect generic types',\n            span=expr.type.span)\n\n    result_typeref = typegen.type_to_typeref(\n        _infer_type_introspection(typeref, ctx.env, expr.span), env=ctx.env\n    )\n    ir = setgen.ensure_set(\n        irast.TypeIntrospection(output_typeref=typeref, typeref=result_typeref),\n        ctx=ctx,\n    )\n    return stmt.maybe_add_view(ir, ctx=ctx)\n\n\ndef _infer_index_type(\n    expr: irast.Set | irast.Expr,\n    index: irast.Set,\n    *, ctx: context.ContextLevel,\n) -> s_types.Type:\n    env = ctx.env\n    node_type = setgen.get_expr_type(expr, ctx=ctx)\n    index_type = setgen.get_set_type(index, ctx=ctx)\n\n    str_t = env.schema.get_by_name('std::str', type=s_scalars.ScalarType)\n    bytes_t = env.schema.get_by_name('std::bytes', type=s_scalars.ScalarType)\n    int_t = env.schema.get_by_name('std::int64', type=s_scalars.ScalarType)\n    json_t = env.schema.get_by_name('std::json', type=s_scalars.ScalarType)\n\n    result: s_types.Type\n\n    if node_type.issubclass(env.schema, str_t):\n\n        if not index_type.implicitly_castable_to(int_t, env.schema):\n            raise errors.QueryError(\n                f'cannot index string by '\n                f'{index_type.get_displayname(env.schema)}, '\n                f'{int_t.get_displayname(env.schema)} was expected',\n                span=index.span)\n\n        result = str_t\n\n    elif node_type.issubclass(env.schema, bytes_t):\n\n        if not index_type.implicitly_castable_to(int_t, env.schema):\n            raise errors.QueryError(\n                f'cannot index bytes by '\n                f'{index_type.get_displayname(env.schema)}, '\n                f'{int_t.get_displayname(env.schema)} was expected',\n                span=index.span)\n\n        result = bytes_t\n\n    elif node_type.issubclass(env.schema, json_t):\n\n        if not (index_type.implicitly_castable_to(int_t, env.schema) or\n                index_type.implicitly_castable_to(str_t, env.schema)):\n\n            raise errors.QueryError(\n                f'cannot index json by '\n                f'{index_type.get_displayname(env.schema)}, '\n                f'{int_t.get_displayname(env.schema)} or '\n                f'{str_t.get_displayname(env.schema)} was expected',\n                span=index.span)\n\n        result = json_t\n\n    elif isinstance(node_type, s_types.Array):\n\n        if not index_type.implicitly_castable_to(int_t, env.schema):\n            raise errors.QueryError(\n                f'cannot index array by '\n                f'{index_type.get_displayname(env.schema)}, '\n                f'{int_t.get_displayname(env.schema)} was expected',\n                span=index.span)\n\n        result = node_type.get_subtypes(env.schema)[0]\n\n    elif (node_type.is_any(env.schema) or\n            (node_type.is_scalar() and\n                str(node_type.get_name(env.schema)) == 'std::anyscalar') and\n            (index_type.implicitly_castable_to(int_t, env.schema) or\n                index_type.implicitly_castable_to(str_t, env.schema))):\n        result = s_pseudo.PseudoType.get(env.schema, 'anytype')\n\n    else:\n        raise errors.QueryError(\n            f'index indirection cannot be applied to '\n            f'{node_type.get_verbosename(env.schema)}',\n            span=expr.span)\n\n    return result\n\n\ndef _infer_slice_type(\n    expr: irast.Set,\n    start: Optional[irast.Set],\n    stop: Optional[irast.Set],\n    *, ctx: context.ContextLevel,\n) -> s_types.Type:\n    env = ctx.env\n    node_type = setgen.get_set_type(expr, ctx=ctx)\n\n    str_t = env.schema.get_by_name('std::str', type=s_scalars.ScalarType)\n    int_t = env.schema.get_by_name('std::int64', type=s_scalars.ScalarType)\n    json_t = env.schema.get_by_name('std::json', type=s_scalars.ScalarType)\n    bytes_t = env.schema.get_by_name('std::bytes', type=s_scalars.ScalarType)\n\n    if node_type.issubclass(env.schema, str_t):\n        base_name = 'string'\n    elif node_type.issubclass(env.schema, json_t):\n        base_name = 'JSON array'\n    elif node_type.issubclass(env.schema, bytes_t):\n        base_name = 'bytes'\n    elif isinstance(node_type, s_types.Array):\n        base_name = 'array'\n    elif node_type.is_any(env.schema):\n        base_name = 'anytype'\n    else:\n        # the base type is not valid\n        raise errors.QueryError(\n            f'{node_type.get_verbosename(env.schema)} cannot be sliced',\n            span=expr.span)\n\n    for index in [start, stop]:\n        if index is not None:\n            index_type = setgen.get_set_type(index, ctx=ctx)\n\n            if not index_type.implicitly_castable_to(int_t, env.schema):\n                raise errors.QueryError(\n                    f'cannot slice {base_name} by '\n                    f'{index_type.get_displayname(env.schema)}, '\n                    f'{int_t.get_displayname(env.schema)} was expected',\n                    span=index.span)\n\n    return node_type\n\n\n@dispatch.compile.register(qlast.Indirection)\ndef compile_Indirection(\n    expr: qlast.Indirection, *, ctx: context.ContextLevel\n) -> irast.Set:\n    node: irast.Set | irast.Expr = dispatch.compile(expr.arg, ctx=ctx)\n    for indirection_el in expr.indirection:\n        if isinstance(indirection_el, qlast.Index):\n            idx = dispatch.compile(indirection_el.index, ctx=ctx)\n            idx.span = indirection_el.index.span\n            typeref = typegen.type_to_typeref(\n                _infer_index_type(node, idx, ctx=ctx), env=ctx.env\n            )\n\n            node = irast.IndexIndirection(\n                expr=node, index=idx, typeref=typeref, span=expr.span\n            )\n        elif isinstance(indirection_el, qlast.Slice):\n            start: Optional[irast.Base]\n            stop: Optional[irast.Base]\n\n            if indirection_el.start:\n                start = dispatch.compile(indirection_el.start, ctx=ctx)\n            else:\n                start = None\n\n            if indirection_el.stop:\n                stop = dispatch.compile(indirection_el.stop, ctx=ctx)\n            else:\n                stop = None\n\n            node_set = setgen.ensure_set(node, ctx=ctx)\n            typeref = typegen.type_to_typeref(\n                _infer_slice_type(node_set, start, stop, ctx=ctx), env=ctx.env\n            )\n            node = irast.SliceIndirection(\n                expr=node_set, start=start, stop=stop, typeref=typeref,\n            )\n        else:\n            raise ValueError(\n                'unexpected indirection node: ' '{!r}'.format(indirection_el)\n            )\n\n    return setgen.ensure_set(node, ctx=ctx)\n\n\ndef compile_type_check_op(\n    expr: qlast.IsOp, *, ctx: context.ContextLevel\n) -> irast.TypeCheckOp:\n    # <Expr> IS <TypeExpr>\n    left = dispatch.compile(expr.left, ctx=ctx)\n    ltype = setgen.get_set_type(left, ctx=ctx)\n    typeref = typegen.ql_typeexpr_to_ir_typeref(expr.right, ctx=ctx)\n\n    if ltype.is_object_type() and not ltype.is_free_object_type(ctx.env.schema):\n        # Argh, what a mess path factoring and deduplication is!  We\n        # need to dereference __type__, and <Expr> needs to be visible\n        # in the scope when we do it, or else it will get\n        # deduplicated.\n        pathctx.register_set_in_scope(left, ctx=ctx)\n\n        left = setgen.ptr_step_set(\n            left, expr=None, source=ltype, ptr_name='__type__',\n            span=expr.span, ctx=ctx\n        )\n        result = None\n    else:\n        if (ltype.is_collection()\n                and cast(s_types.Collection, ltype).contains_object(\n                    ctx.env.schema)):\n            raise errors.QueryError(\n                f'type checks on non-primitive collections are not supported'\n            )\n\n        ctx.env.schema, test_type = (\n            irtyputils.ir_typeref_to_type(ctx.env.schema, typeref)\n        )\n        result = ltype.issubclass(ctx.env.schema, test_type)\n\n    output_typeref = typegen.type_to_typeref(\n        ctx.env.schema.get_by_name('std::bool', type=s_types.Type),\n        env=ctx.env,\n    )\n\n    return irast.TypeCheckOp(\n        left=left, right=typeref, op=expr.op, result=result,\n        typeref=output_typeref)\n\n\ndef flatten_set(expr: qlast.Set) -> list[qlast.Expr]:\n    elements = []\n    for el in expr.elements:\n        if isinstance(el, qlast.Set):\n            elements.extend(flatten_set(el))\n        else:\n            elements.append(el)\n\n    return elements\n\n\ndef collect_binop(expr: qlast.BinOp) -> list[qlast.Expr]:\n    elements = []\n\n    stack = [expr.right, expr.left]\n    while stack:\n        el = stack.pop()\n        if isinstance(el, qlast.BinOp) and el.op == expr.op:\n            stack.extend([el.right, el.left])\n        else:\n            elements.append(el)\n\n    return elements\n\n\ndef try_constant_set(expr: irast.Base) -> Optional[irast.ConstantSet]:\n    elements = []\n\n    stack: list[Optional[irast.Base]] = [expr]\n    while stack:\n        el = stack.pop()\n        if isinstance(el, irast.Set):\n            stack.append(el.expr)\n        elif (\n            isinstance(el, irast.OperatorCall)\n            and str(el.func_shortname) == 'std::UNION'\n        ):\n            stack.extend([el.args[1].expr.expr, el.args[0].expr.expr])\n        elif el and irutils.is_trivial_select(el):\n            stack.append(el.result)\n        elif isinstance(el, (irast.BaseConstant, irast.BaseParameter)):\n            elements.append(el)\n        else:\n            return None\n\n    if elements:\n        return irast.ConstantSet(\n            elements=tuple(elements), typeref=elements[0].typeref\n        )\n    else:\n        return None\n\n\nclass IdentCompletionException(BaseException):\n    \"\"\"An exception that is raised to halt the compilation and return a list of\n    suggested idents to be used at the location of qlast.Cursor node.\n    \"\"\"\n\n    def __init__(self, suggestions: list[str]):\n        self.suggestions = suggestions\n\n\n@dispatch.compile.register(qlast.Cursor)\ndef compile_Cursor(\n    expr: qlast.Cursor, *, ctx: context.ContextLevel\n) -> irast.Set:\n    suggestions = []\n\n    # with bindings\n    name: sn.Name\n    for name in ctx.aliased_views.keys():\n        suggestions.append(name.name)\n\n    # names in current module\n    if cur_mod := ctx.modaliases.get(None):\n        obj_types = ctx.env.schema.get_objects(\n            included_modules=[sn.UnqualName(cur_mod)],\n            type=s_objtypes.ObjectType,\n        )\n        obj_type_names = [\n            obj_type.get_name(ctx.env.schema).name\n            for obj_type in obj_types\n        ]\n        obj_type_names.sort()\n        suggestions.extend(obj_type_names)\n\n    raise IdentCompletionException(suggestions)\n"
  },
  {
    "path": "edb/edgeql/compiler/func.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL compiler routines for function calls and operators.\"\"\"\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Callable,\n    Final,\n    Optional,\n    Protocol,\n    Iterable,\n    Sequence,\n    cast,\n    TYPE_CHECKING,\n)\n\nfrom edb import errors\nfrom edb.common import ast\nfrom edb.common import parsing\nfrom edb.common.typeutils import not_none\n\nfrom edb.ir import ast as irast\nfrom edb.ir import staeval\nfrom edb.ir import utils as irutils\nfrom edb.ir import typeutils as irtyputils\n\nfrom edb.schema import constraints as s_constr\nfrom edb.schema import delta as sd\nfrom edb.schema import functions as s_func\nfrom edb.schema import globals as s_globals\nfrom edb.schema import modules as s_mod\nfrom edb.schema import name as sn\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import operators as s_oper\nfrom edb.schema import permissions as s_permissions\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import types as s_types\nfrom edb.schema import indexes as s_indexes\nfrom edb.schema import schema as s_schema\nfrom edb.schema import utils as s_utils\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes as ft\nfrom edb.edgeql import parser as qlparser\n\nfrom . import casts\nfrom . import context\nfrom . import dispatch\nfrom . import pathctx\nfrom . import polyres\nfrom . import schemactx\nfrom . import setgen\nfrom . import stmt\nfrom . import typegen\n\nif TYPE_CHECKING:\n    import uuid\n\n\n@dispatch.compile.register(qlast.FunctionCall)\ndef compile_FunctionCall(\n    expr: qlast.FunctionCall, *, ctx: context.ContextLevel\n) -> irast.Set:\n\n    env = ctx.env\n\n    funcname: sn.Name\n    if isinstance(expr.func, str):\n        if (\n            ctx.env.options.func_params is not None\n            and ctx.env.options.func_params.get_by_name(\n                env.schema, expr.func\n            )\n        ):\n            raise errors.QueryError(\n                f'parameter `{expr.func}` is not callable',\n                span=expr.span)\n\n        funcname = sn.UnqualName(expr.func)\n    else:\n        funcname = sn.QualName(*expr.func)\n\n    try:\n        funcs = s_func.lookup_functions(\n            funcname,\n            module_aliases=ctx.modaliases,\n            schema=env.schema,\n        )\n    except errors.InvalidReferenceError as e:\n        s_utils.enrich_schema_lookup_error(\n            e,\n            funcname,\n            modaliases=ctx.modaliases,\n            schema=env.schema,\n            suggestion_limit=1,\n            item_type=s_types.Type,\n            span=expr.span,\n            hint_text='did you mean to cast to'\n        )\n        raise\n\n    prefer_subquery_args = any(\n        func.get_prefer_subquery_args(env.schema) for func in funcs\n    )\n\n    if funcs is None:\n        raise errors.QueryError(\n            f'could not resolve function name {funcname}',\n            span=expr.span)\n\n    in_polymorphic_func = (\n        ctx.env.options.func_params is not None and\n        ctx.env.options.func_params.has_polymorphic(env.schema)\n    )\n\n    in_abstract_constraint = (\n        in_polymorphic_func and\n        ctx.env.options.schema_object_context is s_constr.Constraint\n    )\n\n    typemods = polyres.find_callable_typemods(\n        funcs, num_args=len(expr.args), kwargs_names=expr.kwargs.keys(),\n        ctx=ctx)\n    args, kwargs = compile_func_call_args(\n        expr, funcname, typemods, prefer_subquery_args=prefer_subquery_args,\n        ctx=ctx)\n    with errors.ensure_span(expr.span):\n        matched = polyres.find_callable(\n            funcs, args=args, kwargs=kwargs, ctx=ctx)\n    if not matched:\n        alts = [f.get_signature_as_str(env.schema) for f in funcs]\n        sig: list[str] = []\n        # This is used to generate unique arg names.\n        argnum = 0\n        for argtype, _ in args:\n            # Skip any name colliding with kwargs.\n            while f'arg{argnum}' in kwargs:\n                argnum += 1\n            ty = schemactx.get_material_type(argtype, ctx=ctx)\n            sig.append(\n                f'arg{argnum}: {ty.get_displayname(env.schema)}'\n            )\n            argnum += 1\n        for kwname, (kwtype, _) in kwargs.items():\n            ty = schemactx.get_material_type(kwtype, ctx=ctx)\n            sig.append(\n                f'NAMED ONLY {kwname}: {kwtype.get_displayname(env.schema)}'\n            )\n\n        signature = f'{funcname}({\", \".join(sig)})'\n\n        if not funcs:\n            hint = None\n        elif len(alts) == 1:\n            hint = f'Did you want \"{alts[0]}\"?'\n        else:  # Multiple alternatives\n            hint = (\n                f'Did you want one of the following functions instead:\\n' +\n                ('\\n'.join(alts))\n            )\n\n        raise errors.QueryError(\n            f'function \"{signature}\" does not exist',\n            hint=hint,\n            span=expr.span)\n    elif len(matched) > 1:\n        if in_abstract_constraint:\n            matched_call = matched[0]\n        else:\n            alts = [m.func.get_signature_as_str(env.schema) for m in matched]\n            raise errors.QueryError(\n                f'function {funcname} is not unique',\n                hint=f'Please disambiguate between the following '\n                     f'alternatives:\\n' +\n                     ('\\n'.join(alts)),\n                span=expr.span)\n    else:\n        matched_call = matched[0]\n\n    func = matched_call.func\n    assert isinstance(func, s_func.Function)\n\n    if matched_call.server_param_conversions:\n        for param_name, conversions in (\n            matched_call.server_param_conversions.items()\n        ):\n            if param_name not in ctx.env.server_param_conversions:\n                ctx.env.server_param_conversions[param_name] = {}\n            ctx.env.server_param_conversions[param_name].update(\n                conversions\n            )\n        ctx.env.server_param_conversion_calls.append((\n            func.get_signature_as_str(env.schema),\n            expr.span,\n        ))\n\n    inline_func = None\n    if (\n        func.get_language(ctx.env.schema) == qlast.Language.EdgeQL\n        and (\n            func.get_volatility(ctx.env.schema) == ft.Volatility.Modifying\n            or func.get_is_inlined(ctx.env.schema)\n        )\n    ):\n        inline_func = s_func.compile_function_inline(\n            schema=ctx.env.schema,\n            context=sd.CommandContext(\n                schema=ctx.env.schema,\n            ),\n            body=not_none(func.get_nativecode(ctx.env.schema)),\n            func_name=func.get_name(ctx.env.schema),\n            params=func.get_params(ctx.env.schema),\n            language=not_none(func.get_language(ctx.env.schema)),\n            return_type=func.get_return_type(ctx.env.schema),\n            return_typemod=func.get_return_typemod(ctx.env.schema),\n            track_schema_ref_exprs=False,\n            inlining_context=ctx,\n        )\n\n    # Record this node in the list of potential DML expressions.\n    if func.get_volatility(env.schema) == ft.Volatility.Modifying:\n        ctx.env.dml_exprs.append(expr)\n\n        # This is some kind of mutation, so we need to check if it is\n        # allowed.\n        if ctx.env.options.in_ddl_context_name is not None:\n            raise errors.SchemaDefinitionError(\n                f'mutations are invalid in '\n                f'{ctx.env.options.in_ddl_context_name}',\n                span=expr.span,\n            )\n        elif (\n            (dv := ctx.defining_view) is not None\n            and dv.get_expr_type(ctx.env.schema) is s_types.ExprType.Select\n            and not irutils.is_trivial_free_object(\n                not_none(ctx.partial_path_prefix))\n        ):\n            # This is some shape in a regular query. Although\n            # DML is not allowed in the computable, but it may\n            # be possible to refactor it.\n            raise errors.QueryError(\n                f\"mutations are invalid in a shape's computed expression\",\n                hint=(\n                    f'To resolve this try to factor out the mutation '\n                    f'expression into the top-level WITH block.'\n                ),\n                span=expr.span,\n            )\n\n    func_name = func.get_shortname(env.schema)\n\n    matched_func_params = func.get_params(env.schema)\n    variadic_param = matched_func_params.find_variadic(env.schema)\n    variadic_param_type = None\n    if variadic_param is not None:\n        variadic_param_type = typegen.type_to_typeref(\n            variadic_param.get_type(env.schema),\n            env=env,\n        )\n\n    matched_func_ret_type = func.get_return_type(env.schema)\n    is_polymorphic = (\n        any(p.get_type(env.schema).is_polymorphic(env.schema)\n            for p in matched_func_params.objects(env.schema)) and\n        matched_func_ret_type.is_polymorphic(env.schema)\n    )\n\n    matched_func_initial_value = func.get_initial_value(env.schema)\n\n    final_args, param_name_to_arg_key = finalize_args(\n        matched_call,\n        guessed_typemods=typemods,\n        is_polymorphic=is_polymorphic,\n        ctx=ctx,\n    )\n\n    # Forbid DML in non-scalar function args\n    if func.get_nativecode(env.schema):\n        # We are sure that there is no such functions implemented with SQL\n\n        for arg in final_args.values():\n            if arg.expr.typeref.is_scalar:\n                continue\n            if not irutils.contains_dml(arg.expr):\n                continue\n            raise errors.UnsupportedFeatureError(\n                'newly created or updated objects cannot be passed to '\n                'functions',\n                span=arg.expr.span\n            )\n\n    if not in_abstract_constraint:\n        # We cannot add strong references to functions from\n        # abstract constraints, since we cannot know which\n        # form of the function is actually used.\n        env.add_schema_ref(func, expr)\n\n    ctx.env.required_permissions.update(\n        func.get_required_permissions(ctx.env.schema).objects(ctx.env.schema)\n    )\n\n    func_initial_value: Optional[irast.Set]\n\n    if matched_func_initial_value is not None:\n        frag = qlparser.parse_fragment(matched_func_initial_value.text)\n        assert isinstance(frag, qlast.Expr)\n        iv_ql = qlast.TypeCast(\n            expr=frag,\n            type=typegen.type_to_ql_typeref(matched_call.return_type, ctx=ctx),\n        )\n        func_initial_value = dispatch.compile(iv_ql, ctx=ctx)\n    else:\n        func_initial_value = None\n\n    rtype = matched_call.return_type\n    path_id = pathctx.get_expression_path_id(rtype, ctx=ctx)\n\n    if rtype.is_tuple(env.schema):\n        rtype = cast(s_types.Tuple, rtype)\n        tuple_path_ids = []\n        nested_path_ids = []\n        for n, st in rtype.iter_subtypes(ctx.env.schema):\n            elem_path_id = pathctx.get_tuple_indirection_path_id(\n                path_id, n, st, ctx=ctx)\n\n            if isinstance(st, s_types.Tuple):\n                nested_path_ids.append([\n                    pathctx.get_tuple_indirection_path_id(\n                        elem_path_id, nn, sst, ctx=ctx)\n                    for nn, sst in st.iter_subtypes(ctx.env.schema)\n                ])\n\n            tuple_path_ids.append(elem_path_id)\n        for nested in nested_path_ids:\n            tuple_path_ids.extend(nested)\n    else:\n        tuple_path_ids = []\n\n    global_args = None\n    if not inline_func:\n        global_args = get_globals(\n            expr, matched_call, candidates=funcs, ctx=ctx\n        )\n\n    volatility = (\n        # Incorporate the volatility of any server param conversions\n        max([\n            func.get_volatility(env.schema),\n            *(\n                conversion.volatility\n                for conversions in (\n                    matched_call.server_param_conversions.values()\n                )\n                for conversion in conversions.values()\n            )\n        ])\n        if matched_call.server_param_conversions else\n        func.get_volatility(env.schema)\n    )\n\n    fcall = irast.FunctionCall(\n        args=final_args,\n        func_shortname=func_name,\n        backend_name=func.get_backend_name(env.schema),\n        func_polymorphic=is_polymorphic,\n        func_sql_function=func.get_from_function(env.schema),\n        func_sql_expr=func.get_from_expr(env.schema),\n        force_return_cast=func.get_force_return_cast(env.schema),\n        volatility=volatility,\n        sql_func_has_out_params=func.get_sql_func_has_out_params(env.schema),\n        error_on_null_result=func.get_error_on_null_result(env.schema),\n        preserves_optionality=func.get_preserves_optionality(env.schema),\n        preserves_upper_cardinality=func.get_preserves_upper_cardinality(\n            env.schema),\n        typeref=typegen.type_to_typeref(\n            rtype, env=env,\n        ),\n        typemod=matched_call.func.get_return_typemod(env.schema),\n        has_empty_variadic=(matched_call.variadic_arg_count == 0),\n        variadic_param_type=variadic_param_type,\n        func_initial_value=func_initial_value,\n        tuple_path_ids=tuple_path_ids,\n        impl_is_strict=(\n            func.get_impl_is_strict(env.schema)\n            # Inlined functions should always check for null arguments.\n            and not inline_func\n        ),\n        prefer_subquery_args=func.get_prefer_subquery_args(env.schema),\n        is_singleton_set_of=func.get_is_singleton_set_of(env.schema),\n        global_args=global_args,\n        span=expr.span,\n        return_polymorphism=matched_call.return_polymorphism,\n    )\n\n    # Apply special function handling\n    if special_func := _SPECIAL_FUNCTIONS.get(str(func_name)):\n        res = special_func(fcall, ctx=ctx)\n    elif inline_func:\n        res = fcall\n\n        # TODO: Global parameters still use the implicit globals parameter.\n        # They should be directly substituted in whenever possible.\n\n        inline_args: dict[str, irast.CallArg | irast.Set] = {}\n\n        # Collect non-default call args to inline\n        for param_shortname, arg_key in param_name_to_arg_key.items():\n            if (\n                isinstance(arg_key, int)\n                and matched_call.variadic_arg_id is not None\n                and arg_key >= matched_call.variadic_arg_id\n            ):\n                continue\n\n            arg = final_args[arg_key]\n            if arg.is_default:\n                continue\n\n            inline_args[param_shortname] = arg\n\n        # Package variadic arguments into an array\n        if variadic_param is not None:\n            assert variadic_param_type is not None\n            assert matched_call.variadic_arg_id is not None\n            assert matched_call.variadic_arg_count is not None\n\n            param_shortname = variadic_param.get_parameter_name(env.schema)\n            inline_args[param_shortname] = ir_set = setgen.ensure_set(\n                irast.Array(\n                    elements=[\n                        final_args[arg_key].expr\n                        for arg_key in range(\n                            matched_call.variadic_arg_id,\n                            matched_call.variadic_arg_id\n                            + matched_call.variadic_arg_count\n                        )\n                    ],\n                    typeref=variadic_param_type,\n                ),\n                ctx=ctx,\n            )\n\n        # Compile default args if necessary\n        for param in matched_func_params.objects(env.schema):\n            param_shortname = param.get_parameter_name(env.schema)\n\n            if param_shortname in inline_args:\n                continue\n\n            else:\n                # Missing named only args have their default values already\n                # compiled in try_bind_call_args.\n                if bound_args := [\n                    bound_arg\n                    for bound_arg in matched_call.args\n                    if (\n                        isinstance(bound_arg, polyres.DefaultArg)\n                        and bound_arg.name == param_shortname\n                    )\n                ]:\n                    assert len(bound_args) == 1\n                    inline_args[param_shortname] = bound_args[0].val\n                    continue\n\n                # Check if default is available\n                p_default = param.get_default(env.schema)\n                if p_default is None:\n                    continue\n\n                # Compile default\n                assert isinstance(param, s_func.Parameter)\n                p_ir_default = dispatch.compile(p_default.parse(), ctx=ctx)\n                inline_args[param_shortname] = p_ir_default\n\n        argument_inliner = ArgumentInliner(inline_args, ctx=ctx)\n        res.body = argument_inliner.visit(inline_func)\n\n    else:\n        res = fcall\n\n    if isinstance(res, irast.FunctionCall) and res.body:\n        # If we are generating a special-cased inlined function call,\n        # make sure to register all the arguments in the scope tree\n        # to ensure that the compiled arguments get picked up when\n        # compiling the body.\n        for arg in res.args.values():\n            pathctx.register_set_in_scope(\n                arg.expr,\n                optional=(\n                    arg.param_typemod == ft.TypeModifier.OptionalType\n                ),\n                ctx=ctx,\n            )\n\n    ir_set = setgen.ensure_set(res, typehint=rtype, path_id=path_id, ctx=ctx)\n    return stmt.maybe_add_view(ir_set, ctx=ctx)\n\n\nclass ArgumentInliner(ast.NodeTransformer):\n\n    # Don't look through hidden nodes, they may contain references to nodes\n    # which should not be modified. For example, irast.Stmt.parent_stmt.\n    skip_hidden = True\n\n    mapped_args: dict[irast.PathId, irast.PathId]\n    inlined_arg_keys: list[int | str]\n\n    def __init__(\n        self,\n        inline_args: dict[str, irast.CallArg | irast.Set],\n        ctx: context.ContextLevel,\n    ) -> None:\n        super().__init__()\n        self.inline_args = inline_args\n        self.ctx = ctx\n        self.mapped_args = {}\n\n    def visit_Set(self, node: irast.Set) -> irast.Base:\n        if (\n            isinstance(node.expr, irast.FunctionParameter)\n            and node.expr.name in self.inline_args\n        ):\n            arg = self.inline_args[node.expr.name]\n            if isinstance(arg, irast.CallArg):\n                # Inline param as an expr ref. The pg compiler will find the\n                # appropriate rvar.\n                self.mapped_args[node.path_id] = arg.expr.path_id\n                inlined_param_expr = setgen.ensure_set(\n                    irast.InlinedParameterExpr(\n                        typeref=arg.expr.typeref,\n                        required=node.expr.required,\n                        is_global=node.expr.is_global,\n                    ),\n                    path_id=arg.expr.path_id,\n                    ctx=self.ctx,\n                )\n                inlined_param_expr.shape = node.shape\n                return inlined_param_expr\n            else:\n                # Directly inline the set.\n                # Used for default values, which are constants.\n                return arg\n\n        elif isinstance(node.expr, irast.Pointer):\n            # The set and source path ids must match in order for the pointer\n            # to find the correct rvar. If a pointer's source path was modified\n            # because of an inlined parameter, modify the pointer's path as\n            # well.\n            prev_source_path_id = node.expr.source.path_id\n            result = cast(irast.Set, self.generic_visit(node))\n\n            if prev_source_path_id in self.mapped_args:\n                result = setgen.new_set_from_set(\n                    result,\n                    path_id=irtyputils.replace_pathid_prefix(\n                        result.path_id,\n                        prev_source_path_id,\n                        self.mapped_args[prev_source_path_id],\n                    ),\n                    ctx=self.ctx,\n                )\n                self.mapped_args[node.path_id] = result.path_id\n\n            return result\n\n        return cast(irast.Base, self.generic_visit(node))\n\n    # Don't transform pointer refs.\n    # They are updated in other places, such as cardinality inference.\n    def visit_PointerRef(\n        self, node: irast.PointerRef\n    ) -> irast.Base:\n        return node\n\n    def visit_TupleIndirectionPointerRef(\n        self, node: irast.TupleIndirectionPointerRef\n    ) -> irast.Base:\n        return node\n\n    def visit_SpecialPointerRef(\n        self, node: irast.SpecialPointerRef\n    ) -> irast.Base:\n        return node\n\n    def visit_TypeIntersectionPointerRef(\n        self, node: irast.TypeIntersectionPointerRef\n    ) -> irast.Base:\n        return node\n\n\nclass _SpecialCaseFunc(Protocol):\n    def __call__(\n        self, call: irast.FunctionCall, *, ctx: context.ContextLevel\n    ) -> irast.Expr:\n        pass\n\n\n_SPECIAL_FUNCTIONS: dict[str, _SpecialCaseFunc] = {}\n\n\ndef _special_case(name: str) -> Callable[[_SpecialCaseFunc], _SpecialCaseFunc]:\n    def func(f: _SpecialCaseFunc) -> _SpecialCaseFunc:\n        _SPECIAL_FUNCTIONS[name] = f\n        return f\n\n    return func\n\n\ndef compile_operator(\n    qlexpr: qlast.Expr,\n    op_name: str,\n    qlargs: list[qlast.Expr],\n    *,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    env = ctx.env\n    schema = env.schema\n    opers = s_oper.lookup_operators(\n        op_name, module_aliases=ctx.modaliases, schema=schema\n    )\n\n    if opers is None:\n        raise errors.QueryError(\n            f'no operator matches the given name and argument types',\n            span=qlexpr.span)\n\n    typemods = polyres.find_callable_typemods(\n        opers, num_args=len(qlargs), kwargs_names=set(), ctx=ctx)\n\n    prefer_subquery_args = any(\n        oper.get_prefer_subquery_args(env.schema) for oper in opers\n    )\n\n    args = []\n\n    for ai, qlarg in enumerate(qlargs):\n        arg_ir = polyres.compile_arg(\n            qlarg,\n            typemods[ai],\n            prefer_subquery_args=prefer_subquery_args,\n            ctx=ctx,\n        )\n\n        arg_type = setgen.get_set_type(arg_ir, ctx=ctx)\n        if arg_type is None:\n            raise errors.QueryError(\n                f'could not resolve the type of operand '\n                f'#{ai} of {op_name}',\n                span=qlarg.span)\n\n        args.append((arg_type, arg_ir))\n\n    # Check if the operator is a derived operator, and if so,\n    # find the origins.\n    origin_op = opers[0].get_derivative_of(env.schema)\n    derivative_op: Optional[s_oper.Operator]\n    if origin_op is not None:\n        # If this is a derived operator, there should be\n        # exactly one form of it.  This is enforced at the DDL\n        # level, but check again to be sure.\n        if len(opers) > 1:\n            raise errors.InternalServerError(\n                f'more than one derived operator of the same name: {op_name}',\n                span=qlarg.span)\n\n        derivative_op = opers[0]\n        opers = s_oper.lookup_operators(origin_op, schema=schema)\n        if not opers:\n            raise errors.InternalServerError(\n                f'cannot find the origin operator for {op_name}',\n                span=qlarg.span)\n        actual_typemods = [\n            param.get_typemod(schema)\n            for param in derivative_op.get_params(schema).objects(schema)\n        ]\n    else:\n        derivative_op = None\n        actual_typemods = []\n\n    matched = None\n    # Some 2-operand operators are special when their operands are\n    # arrays or tuples.\n    if len(args) == 2:\n        coll_opers = None\n        # If both of the args are arrays or tuples, potentially\n        # compile the operator for them differently than for other\n        # combinations.\n        if args[0][0].is_tuple(env.schema) and args[1][0].is_tuple(env.schema):\n            # Out of the candidate operators, find the ones that\n            # correspond to tuples.\n            coll_opers = [\n                op for op in opers\n                if all(\n                    param.get_type(schema).is_tuple(schema)\n                    for param in op.get_params(schema).objects(schema)\n                )\n            ]\n\n        elif args[0][0].is_array() and args[1][0].is_array():\n            # Out of the candidate operators, find the ones that\n            # correspond to arrays.\n            coll_opers = [\n                op for op in opers\n                if all(\n                    param.get_type(schema).is_array()\n                    for param in op.get_params(schema).objects(schema)\n                )\n            ]\n\n        # Proceed only if we have a special case of collection operators.\n        if coll_opers:\n            # Then check if they are recursive (i.e. validation must be\n            # done recursively for the subtypes). We rely on the fact that\n            # it is forbidden to define an operator that has both\n            # recursive and non-recursive versions.\n            if not coll_opers[0].get_recursive(schema):\n                # The operator is non-recursive, so regular processing\n                # is needed.\n                matched = polyres.find_callable(\n                    coll_opers, args=args, kwargs={}, ctx=ctx)\n\n            else:\n                # The recursive operators are usually defined as\n                # being polymorphic on all parameters, and so this has\n                # a side-effect of forcing both operands to be of\n                # the same type (via casting) before the operator is\n                # applied.  This might seem suboptmial, since there might\n                # be a more specific operator for the types of the\n                # elements, but the current version of Postgres\n                # actually requires tuples and arrays to be of the\n                # same type in comparison, so this behavior is actually\n                # what we want.\n                matched = polyres.find_callable(\n                    coll_opers,\n                    args=args,\n                    kwargs={},\n                    ctx=ctx,\n                )\n\n                # Now that we have an operator, we need to validate that it\n                # can be applied to the tuple or array elements.\n                submatched = validate_recursive_operator(\n                    opers, args[0], args[1], ctx=ctx)\n\n                if len(submatched) != 1:\n                    # This is an error. We want the error message to\n                    # reflect whether no matches were found or too\n                    # many, so we preserve the submatches found for\n                    # this purpose.\n                    matched = submatched\n\n    # No special handling match was necessary, find a normal match.\n    if matched is None:\n        matched = polyres.find_callable(opers, args=args, kwargs={}, ctx=ctx)\n\n    in_polymorphic_func = (\n        ctx.env.options.func_params is not None and\n        ctx.env.options.func_params.has_polymorphic(env.schema)\n    )\n\n    in_abstract_constraint = (\n        in_polymorphic_func and\n        ctx.env.options.schema_object_context is s_constr.Constraint\n    )\n\n    if not in_polymorphic_func:\n        matched = [call for call in matched\n                   if not call.func.get_abstract(env.schema)]\n\n    if len(matched) == 1:\n        matched_call = matched[0]\n    else:\n        args_ty = [schemactx.get_material_type(a[0], ctx=ctx) for a in args]\n        args_dn = [repr(a.get_displayname(env.schema)) for a in args_ty]\n\n        if len(args_dn) == 2:\n            types = f'{args_dn[0]} and {args_dn[1]}'\n        else:\n            types = ', '.join(a for a in args_dn)\n\n        if not matched:\n            hint = ('Consider using an explicit type cast or a conversion '\n                    'function.')\n\n            if op_name == 'std::IF':\n                hint = (f\"The IF and ELSE result clauses must be of \"\n                        f\"compatible types, while the condition clause must \"\n                        f\"be 'std::bool'. {hint}\")\n            elif op_name == '+':\n                str_t = env.schema.get('std::str', type=s_scalars.ScalarType)\n                bytes_t = env.schema.get('std::bytes',\n                                         type=s_scalars.ScalarType)\n                if (\n                    all(t.issubclass(env.schema, str_t) for t in args_ty) or\n                    all(t.issubclass(env.schema, bytes_t) for t in args_ty) or\n                    all(t.is_array() for t in args_ty)\n                ):\n                    hint = 'Consider using the \"++\" operator for concatenation'\n\n            if isinstance(qlexpr, qlast.BinOp) and qlexpr.set_constructor:\n                msg = (\n                    f'set constructor has arguments of incompatible types '\n                    f'{types}'\n                )\n            else:\n                msg = (\n                    f'operator {str(op_name)!r} cannot be applied to '\n                    f'operands of type {types}'\n                )\n            raise errors.InvalidTypeError(\n                msg,\n                hint=hint,\n                span=qlexpr.span)\n        elif len(matched) > 1:\n            if in_abstract_constraint:\n                matched_call = matched[0]\n            else:\n                detail = ', '.join(\n                    f'`{m.func.get_verbosename(ctx.env.schema)}`'\n                    for m in matched\n                )\n                raise errors.QueryError(\n                    f'operator {str(op_name)!r} is ambiguous for '\n                    f'operands of type {types}',\n                    hint=f'Possible variants: {detail}.',\n                    span=qlexpr.span)\n\n    oper = matched_call.func\n    assert isinstance(oper, s_oper.Operator)\n    env.add_schema_ref(oper, expr=qlexpr)\n    oper_name = oper.get_shortname(env.schema)\n    str_oper_name = str(oper_name)\n\n    is_singleton_set_of = oper.get_is_singleton_set_of(env.schema)\n\n    matched_params = oper.get_params(env.schema)\n    rtype = matched_call.return_type\n    matched_rtype = oper.get_return_type(env.schema)\n\n    is_polymorphic = (\n        any(p.get_type(env.schema).is_polymorphic(env.schema)\n            for p in matched_params.objects(env.schema)) and\n        matched_rtype.is_polymorphic(env.schema)\n    )\n\n    final_args, _ = finalize_args(\n        matched_call,\n        actual_typemods=actual_typemods,\n        guessed_typemods=typemods,\n        is_polymorphic=is_polymorphic,\n        ctx=ctx,\n    )\n\n    if str_oper_name in {\n        'std::UNION', 'std::IF', 'std::??'\n    } and rtype.is_object_type():\n        # Special case for the UNION, IF and ?? operators: instead of common\n        # parent type, we return a union type.\n        if str_oper_name == 'std::IF':\n            larg, _, rarg = (a.expr for a in final_args.values())\n        else:\n            larg, rarg = (a.expr for a in final_args.values())\n\n        left_type = setgen.get_set_type(larg, ctx=ctx)\n        right_type = setgen.get_set_type(rarg, ctx=ctx)\n        rtype = schemactx.get_union_type(\n            [left_type, right_type],\n            preserve_derived=True,\n            ctx=ctx,\n            span=qlexpr.span\n        )\n\n    from_op = oper.get_from_operator(env.schema)\n    sql_operator = None\n    if (\n        from_op is not None\n        and oper.get_code(env.schema) is None\n        and oper.get_from_function(env.schema) is None\n    ):\n        sql_operator = tuple(from_op)\n\n    origin_name: Optional[sn.QualName]\n    origin_module_id: Optional[uuid.UUID]\n    if derivative_op is not None:\n        origin_name = oper_name\n        origin_module_id = env.schema.get_global(\n            s_mod.Module, origin_name.module).id\n        oper_name = derivative_op.get_shortname(env.schema)\n        is_singleton_set_of = derivative_op.get_is_singleton_set_of(env.schema)\n    else:\n        origin_name = None\n        origin_module_id = None\n\n    from_func = oper.get_from_function(env.schema)\n    if from_func is None:\n        sql_func = None\n    else:\n        sql_func = tuple(from_func)\n\n    node = irast.OperatorCall(\n        args=final_args,\n        func_shortname=oper_name,\n        func_polymorphic=is_polymorphic,\n        origin_name=origin_name,\n        origin_module_id=origin_module_id,\n        sql_function=sql_func,\n        func_sql_expr=oper.get_from_expr(env.schema),\n        sql_operator=sql_operator,\n        force_return_cast=oper.get_force_return_cast(env.schema),\n        volatility=oper.get_volatility(env.schema),\n        operator_kind=oper.get_operator_kind(env.schema),\n        typeref=typegen.type_to_typeref(rtype, env=env),\n        typemod=oper.get_return_typemod(env.schema),\n        tuple_path_ids=[],\n        impl_is_strict=oper.get_impl_is_strict(env.schema),\n        prefer_subquery_args=oper.get_prefer_subquery_args(env.schema),\n        is_singleton_set_of=is_singleton_set_of,\n        span=qlexpr.span,\n        return_polymorphism=matched_call.return_polymorphism,\n    )\n\n    _check_free_shape_op(node, ctx=ctx)\n\n    return stmt.maybe_add_view(\n        setgen.ensure_set(node, typehint=rtype, ctx=ctx),\n        ctx=ctx)\n\n\n# These ops are all footguns when used with free shapes,\n# so we ban them\nINVALID_FREE_SHAPE_OPS: Final = {\n    sn.QualName('std', x) for x in [\n        'DISTINCT', '=', '!=', '?=', '?!=', 'IN', 'NOT IN',\n        'assert_distinct',\n    ]\n}\n\n\ndef _check_free_shape_op(ir: irast.Call, *, ctx: context.ContextLevel) -> None:\n    if ir.func_shortname not in INVALID_FREE_SHAPE_OPS:\n        return\n\n    virt_obj = ctx.env.schema.get(\n        'std::FreeObject', type=s_objtypes.ObjectType)\n    for arg in ir.args.values():\n        typ = setgen.get_set_type(arg.expr, ctx=ctx)\n        if typ.issubclass(ctx.env.schema, virt_obj):\n            raise errors.QueryError(\n                f'cannot use {ir.func_shortname.name} on free shape',\n                span=ir.span)\n\n\ndef validate_recursive_operator(\n    opers: Iterable[s_func.CallableObject],\n    larg: tuple[s_types.Type, irast.Set],\n    rarg: tuple[s_types.Type, irast.Set],\n    *,\n    ctx: context.ContextLevel,\n) -> list[polyres.BoundCall]:\n\n    matched: list[polyres.BoundCall] = []\n\n    # if larg and rarg are tuples or arrays, recurse into their subtypes\n    if (\n        (\n            larg[0].is_tuple(ctx.env.schema)\n            and rarg[0].is_tuple(ctx.env.schema)\n        ) or (\n            larg[0].is_array()\n            and rarg[0].is_array()\n        )\n    ):\n        assert isinstance(larg[0], s_types.Collection)\n        assert isinstance(rarg[0], s_types.Collection)\n        for rsub, lsub in zip(larg[0].get_subtypes(ctx.env.schema),\n                              rarg[0].get_subtypes(ctx.env.schema)):\n            matched = validate_recursive_operator(\n                opers, (lsub, larg[1]), (rsub, rarg[1]), ctx=ctx)\n            if len(matched) != 1:\n                # this is an error already\n                break\n\n    else:\n        # we just have a pair of non-containers to compare\n        matched = polyres.find_callable(\n            opers, args=[larg, rarg], kwargs={}, ctx=ctx)\n\n    return matched\n\n\ndef compile_func_call_args(\n    expr: qlast.FunctionCall,\n    funcname: sn.Name,\n    typemods: dict[int | str, ft.TypeModifier],\n    *,\n    prefer_subquery_args: bool=False,\n    ctx: context.ContextLevel\n) -> tuple[\n    list[tuple[s_types.Type, irast.Set]],\n    dict[str, tuple[s_types.Type, irast.Set]],\n]:\n    args = []\n    kwargs = {}\n\n    for ai, arg in enumerate(expr.args):\n        arg_ir = polyres.compile_arg(\n            arg, typemods[ai], prefer_subquery_args=prefer_subquery_args,\n            ctx=ctx)\n        arg_type = setgen.get_set_type(arg_ir, ctx=ctx)\n        if arg_type is None:\n            raise errors.QueryError(\n                f'could not resolve the type of positional argument '\n                f'#{ai} of function {funcname}',\n                span=arg.span)\n\n        args.append((arg_type, arg_ir))\n\n    for aname, arg in expr.kwargs.items():\n        arg_ir = polyres.compile_arg(\n            arg, typemods[aname], prefer_subquery_args=prefer_subquery_args,\n            ctx=ctx)\n\n        arg_type = setgen.get_set_type(arg_ir, ctx=ctx)\n        if arg_type is None:\n            raise errors.QueryError(\n                f'could not resolve the type of named argument '\n                f'${aname} of function {funcname}',\n                span=arg.span)\n\n        kwargs[aname] = (arg_type, arg_ir)\n\n    return args, kwargs\n\n\ndef get_globals(\n    expr: qlast.FunctionCall,\n    bound_call: polyres.BoundCall,\n    candidates: Sequence[s_func.Function],\n    *, ctx: context.ContextLevel,\n) -> list[irast.Set]:\n    assert isinstance(bound_call.func, s_func.Function)\n\n    func_language = bound_call.func.get_language(ctx.env.schema)\n    if func_language is not qlast.Language.EdgeQL:\n        return []\n\n    schema = ctx.env.schema\n\n    globs: set[s_globals.Global | s_permissions.Permission] = set()\n    if bound_call.func.get_params(schema).has_objects(schema):\n        # We look at all the candidates since it might be used in a\n        # subtype's overload.\n        # TODO: be careful and only do this in the needed cases\n        for func in candidates:\n            globs.update(set(func.get_used_globals(schema).objects(schema)))\n            globs.update(set(func.get_used_permissions(schema).objects(schema)))\n    else:\n        globs.update(bound_call.func.get_used_globals(schema).objects(schema))\n        globs.update(\n            bound_call.func.get_used_permissions(schema).objects(schema)\n        )\n\n    if (\n        (\n            ctx.env.options.func_name is None\n            or ctx.env.options.func_params is None\n        )\n        and not ctx.env.options.json_parameters\n    ):\n        glob_set = setgen.get_globals_as_json(\n            tuple(globs), ctx=ctx, span=expr.span)\n    else:\n        # Make sure that we properly track the globals we use in functions\n        for glob in globs:\n            setgen.get_global_param(glob, ctx=ctx)\n\n        glob_set = setgen.get_func_global_json_arg(ctx=ctx)\n\n    return [glob_set]\n\n\ndef finalize_args(\n    bound_call: polyres.BoundCall,\n    *,\n    actual_typemods: Sequence[ft.TypeModifier] = (),\n    guessed_typemods: dict[int | str, ft.TypeModifier],\n    is_polymorphic: bool = False,\n    ctx: context.ContextLevel,\n) -> tuple[dict[int | str, irast.CallArg], dict[str, int | str]]:\n\n    args: dict[int | str, irast.CallArg] = {}\n    param_name_to_arg: dict[str, int | str] = {}\n    position_index: int = 0\n\n    for i, barg in enumerate(bound_call.args):\n        arg_val = barg.val\n        arg_type_path_id: Optional[irast.PathId] = None\n        if isinstance(barg, polyres.DefaultBitmask):\n            # defaults bitmask\n            param_name_to_arg['__defaults_mask__'] = -1\n            args[-1] = irast.CallArg(\n                expr=arg_val,\n                param_typemod=ft.TypeModifier.SingletonType,\n            )\n            continue\n        assert isinstance(barg, polyres.ValueArg)\n\n        if actual_typemods:\n            param_mod = actual_typemods[i]\n        else:\n            param_mod = barg.param_typemod\n\n        if param_mod is not ft.TypeModifier.SetOfType:\n            param_shortname = barg.name\n\n            if param_shortname in bound_call.null_args:\n                pathctx.register_set_in_scope(arg_val, optional=True, ctx=ctx)\n\n            # If we guessed the argument was optional but it wasn't,\n            # try to go back and make it *not* optional.\n            elif (\n                param_mod is ft.TypeModifier.SingletonType\n                and isinstance(barg, polyres.PassedArg)\n                and param_mod is not guessed_typemods[barg.arg_id]\n            ):\n                for child in ctx.path_scope.children:\n                    if (\n                        child.path_id == arg_val.path_id\n                        or (\n                            arg_val.path_scope_id is not None\n                            and child.unique_id == arg_val.path_scope_id\n                        )\n                    ):\n                        child.optional = False\n\n            # Object type arguments to functions may be overloaded, so\n            # we populate a path id field so that we can also pass the\n            # type as an argument on the pgsql side. If the param type\n            # is \"anytype\", though, then it can't be overloaded on\n            # that argument.\n            arg_type = setgen.get_set_type(arg_val, ctx=ctx)\n            if (\n                isinstance(arg_type, s_objtypes.ObjectType)\n                and not barg.orig_param_type.is_any(ctx.env.schema)\n            ):\n                arg_type_path_id = pathctx.extend_path_id(\n                    arg_val.path_id,\n                    ptrcls=setgen.resolve_ptr(\n                        arg_type, '__type__', track_ref=None, ctx=ctx\n                    ),\n                    ctx=ctx,\n                )\n        else:\n            is_array_agg = (\n                isinstance(bound_call.func, s_func.Function)\n                and (\n                    bound_call.func.get_shortname(ctx.env.schema)\n                    == sn.QualName('std', 'array_agg')\n                )\n            )\n\n            if (\n                # Ideally, we should implicitly slice all array values,\n                # but in practice, the vast majority of large arrays\n                # will come from array_agg, and so we only care about\n                # that.\n                is_array_agg\n                and ctx.expr_exposed\n                and ctx.implicit_limit\n                and isinstance(arg_val.expr, irast.SelectStmt)\n                and arg_val.expr.limit is None\n            ):\n                arg_val.expr.limit = dispatch.compile(\n                    qlast.Constant.integer(ctx.implicit_limit),\n                    ctx=ctx,\n                )\n\n        paramtype = barg.param_type\n        param_kind = barg.param_kind\n        if param_kind is ft.ParameterKind.VariadicParam:\n            # For variadic params, paramtype would be array<T>,\n            # and we need T to cast the arguments.\n            assert isinstance(paramtype, s_types.Array)\n            paramtype = list(paramtype.get_subtypes(ctx.env.schema))[0]\n\n        # Check if we need to cast the argument value before passing\n        # it to the callable.\n        compatible = s_types.is_type_compatible(\n            paramtype, barg.valtype, schema=ctx.env.schema,\n        )\n\n        if not compatible:\n            # The callable form was chosen via an implicit cast,\n            # cast the arguments so that the backend has no\n            # wiggle room to apply its own (potentially different)\n            # casting.\n            orig_arg_val = arg_val\n            arg_val = casts.compile_cast(\n                arg_val, paramtype, span=None, ctx=ctx)\n            if ctx.path_scope.is_optional(orig_arg_val.path_id):\n                pathctx.register_set_in_scope(arg_val, optional=True, ctx=ctx)\n\n        arg = irast.CallArg(\n            expr=arg_val,\n            expr_type_path_id=arg_type_path_id,\n            is_default=isinstance(barg, polyres.DefaultArg),\n            param_typemod=param_mod,\n            polymorphism=barg.polymorphism,\n        )\n        param_shortname = barg.name\n        if param_kind is ft.ParameterKind.NamedOnlyParam:\n            args[param_shortname] = arg\n            param_name_to_arg[param_shortname] = param_shortname\n        else:\n            args[position_index] = arg\n            if (\n                # Variadic args will all have the same name, but different\n                # indexes. We want to take the first index.\n                param_shortname not in param_name_to_arg\n            ):\n                param_name_to_arg[param_shortname] = position_index\n            position_index += 1\n\n    return args, param_name_to_arg\n\n\n@_special_case('ext::ai::search')\ndef compile_ext_ai_search(\n    call: irast.FunctionCall, *, ctx: context.ContextLevel\n) -> irast.Expr:\n    indexes = _validate_object_search_call(\n        call,\n        context=\"ext::ai::search()\",\n        object_arg=call.args[0],\n        index_name=sn.QualName(\"ext::ai\", \"index\"),\n        ctx=ctx,\n    )\n\n    schema = ctx.env.schema\n\n    index_metadata = {}\n    for typeref, index in indexes.items():\n        dimensions = index.must_get_json_annotation(\n            schema,\n            sn.QualName(\"ext::ai\", \"embedding_dimensions\"),\n            int,\n        )\n        kwargs = index.get_concrete_kwargs(schema)\n        df_expr = kwargs.get(\"distance_function\")\n        if df_expr is not None:\n            df = df_expr.ensure_compiled(\n                schema,\n                as_fragment=True,\n                context=None,\n            ).as_python_value()\n        else:\n            df = \"Cosine\"\n\n        match df:\n            case \"Cosine\":\n                distance_fname = \"cosine_distance\"\n            case \"InnerProduct\":\n                distance_fname = \"neg_inner_product\"\n            case \"L2\":\n                distance_fname = \"euclidean_distance\"\n            case _:\n                raise RuntimeError(f\"unsupported distance_function: {df}\")\n\n        distance_func = schema.get_by_shortname(\n            s_func.Function, sn.QualName(\"ext::pgvector\", distance_fname)\n        )[0]\n\n        index_metadata[typeref] = {\n            \"id\": s_indexes.get_ai_index_id(schema, index),\n            \"dimensions\": dimensions,\n            \"distance_function\": (\n                distance_func.get_shortname(schema),\n                distance_func.get_backend_name(schema),\n            ),\n        }\n    call.extras = {\"index_metadata\": index_metadata}\n\n    return call\n\n\n@_special_case('ext::ai::to_context')\ndef compile_ext_ai_to_str(\n    call: irast.FunctionCall, *, ctx: context.ContextLevel\n) -> irast.Expr:\n    indexes = _validate_object_search_call(\n        call,\n        context=\"ext::ai::to_context()\",\n        object_arg=call.args[0],\n        index_name=sn.QualName(\"ext::ai\", \"index\"),\n        ctx=ctx,\n    )\n\n    index = next(iter(indexes.values()))\n    index_expr = index.get_expr(ctx.env.schema)\n    assert index_expr is not None\n\n    with ctx.detached() as subctx:\n        subctx.partial_path_prefix = call.args[0].expr\n        subctx.anchors[\"__subject__\"] = call.args[0].expr\n        call.body = dispatch.compile(\n            qlast.FunctionCall(\n                func=('__std__', 'assert_exists'),\n                args=[index_expr.parse()],\n                kwargs={\n                    'message': qlast.Constant.string(\n                        'Object context is not set.'\n                    ),\n                }\n            ),\n            ctx=subctx,\n        )\n\n    return call\n\n\n@_special_case('std::fts::search')\ndef compile_fts_search(\n    call: irast.FunctionCall, *, ctx: context.ContextLevel\n) -> irast.Expr:\n    _validate_object_search_call(\n        call,\n        context=\"std::fts::search()\",\n        object_arg=call.args[0],\n        index_name=sn.QualName(\"std::fts\", \"index\"),\n        ctx=ctx,\n    )\n\n    return call\n\n\ndef _validate_object_search_call(\n    call: irast.FunctionCall,\n    *,\n    context: str,\n    object_arg: irast.CallArg,\n    index_name: sn.QualName,\n    ctx: context.ContextLevel,\n) -> dict[irast.TypeRef, s_indexes.Index]:\n    # validate that object has std::fts::index index\n    object_typeref = object_arg.expr.typeref\n    object_typeref = object_typeref.material_type or object_typeref\n    stype_id = object_typeref.id\n\n    schema = ctx.env.schema\n    span = object_arg.span\n\n    stype = schema.get_by_id(stype_id, type=s_types.Type)\n    indexes = {}\n\n    if union_variants := stype.get_union_of(schema):\n        for variant in union_variants.objects(schema):\n            schema, variant = variant.material_type(schema)\n            idx = _validate_has_object_index(\n                variant, schema, span, context, index_name)\n            indexes[typegen.type_to_typeref(variant, ctx.env)] = idx\n    else:\n        idx = _validate_has_object_index(\n            stype, schema, span, context, index_name)\n        indexes[object_typeref] = idx\n\n    return indexes\n\n\ndef _validate_has_object_index(\n    stype: s_types.Type,\n    schema: s_schema.Schema,\n    span: Optional[parsing.Span],\n    context: str,\n    index_name: sn.QualName,\n) -> s_indexes.Index:\n    if isinstance(stype, s_indexes.IndexableSubject):\n        (obj_index, _) = s_indexes.get_effective_object_index(\n            schema, stype, index_name\n        )\n    else:\n        obj_index = None\n\n    if not obj_index:\n        raise errors.InvalidReferenceError(\n            f\"{context} requires an {index_name} index on type \"\n            f\"'{stype.get_displayname(schema)}'\",\n            span=span,\n        )\n\n    return obj_index\n\n\n@_special_case('std::fts::with_options')\ndef compile_fts_with_options(\n    call: irast.FunctionCall, *, ctx: context.ContextLevel\n) -> irast.Expr:\n    # language has already been typechecked to be an enum\n    lang = call.args['language'].expr\n    assert lang.typeref\n    lang_ty_id = lang.typeref.id\n    lang_ty = ctx.env.schema.get_by_id(lang_ty_id, type=s_scalars.ScalarType)\n    assert lang_ty\n\n    lang_domain = set()  # languages that the fts index needs to support\n    try:\n        lang_const = staeval.evaluate_to_python_val(lang, ctx.env.schema)\n    except staeval.UnsupportedExpressionError:\n        lang_const = None\n\n    if lang_const is not None:\n        # language is constant\n        # -> determine its only value at compile time\n        lang_domain.add(lang_const.lower())\n    else:\n        # language is not constant\n        # -> use all possible values of the enum\n        enum_values = lang_ty.get_enum_values(ctx.env.schema)\n        assert enum_values\n        for enum_value in enum_values:\n            lang_domain.add(enum_value.lower())\n\n    # weight_category\n    weight_expr = call.args['weight_category'].expr\n    try:\n        weight: str = staeval.evaluate_to_python_val(\n            weight_expr, ctx.env.schema)\n    except staeval.UnsupportedExpressionError:\n        raise errors.InvalidValueError(\n            f\"std::fts::search weight_category must be a constant\",\n            span=weight_expr.span,\n        ) from None\n\n    return irast.FTSDocument(\n        text=call.args[0].expr,\n        language=lang,\n        language_domain=lang_domain,\n        weight=weight,\n        typeref=typegen.type_to_typeref(\n            ctx.env.schema.get('std::fts::document', type=s_scalars.ScalarType),\n            env=ctx.env,\n        )\n    )\n\n\n@_special_case('std::_warn_on_call')\ndef compile_warn_on_call(\n    call: irast.FunctionCall, *, ctx: context.ContextLevel\n) -> irast.Expr:\n    ctx.log_warning(\n        errors.QueryError('Test warning please ignore', span=call.span)\n    )\n    return call\n"
  },
  {
    "path": "edb/edgeql/compiler/group.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Any, Optional, Sequence\n\nfrom edb.common import ast as ast_visitor\n\nfrom edb.edgeql import qltypes\nfrom edb.ir import ast as irast\n\nfrom . import context\nfrom . import inference\nfrom . import setgen\n\n\nclass FindAggregatingUses(ast_visitor.NodeVisitor):\n    \"\"\"\n    Find aggregated uses of a target node that can be hoisted.\n    \"\"\"\n    skip_hidden = True\n    extra_skips = frozenset(['materialized_sets'])\n\n    def __init__(\n        self,\n        target: irast.PathId,\n        *,\n        ctx: context.ContextLevel,\n    ) -> None:\n        super().__init__()\n        self.target = target\n        self.aggregate: Optional[irast.Set] = None\n        self.sightings: set[Optional[irast.Set]] = set()\n        self.ctx = ctx\n        # Track pathids that we've seen. pathids that we are interested\n        # in but haven't seen get marked as False.\n        self.seen: dict[irast.PathId, bool] = {}\n        self.skippable: dict[\n            Optional[irast.Set], frozenset[irast.PathId]] = {}\n        self.scope_tree = ctx.path_scope\n        # We don't bother trying to reuse the existing inference\n        # context because we make singleton assumptions that it\n        # wouldn't and because ignore_computed_cards could invalidate\n        # it.\n        self.infctx = inference.make_ctx(ctx.env)._replace(\n            singletons=frozenset({target}),\n            ignore_computed_cards=True,\n            # Don't update the IR with the results!\n            make_updates=False,\n        )\n\n    def visit_Stmt(self, stmt: irast.Stmt) -> Any:\n        # Sometimes there is sharing, so we want the official scope\n        # for a node to be based on its appearance in the result,\n        # not in a subquery.\n        # I think it might not actually matter, though.\n\n        old = self.aggregate\n\n        # Can't handle ORDER/LIMIT/OFFSET which operate on the whole set\n        # TODO: but often we probably could with arguments to the\n        # aggregates, as long as the argument to the aggregate is just\n        # a reference\n        if isinstance(stmt, irast.SelectStmt) and (\n            stmt.orderby or stmt.limit or stmt.offset or stmt.materialized_sets\n        ):\n            self.aggregate = None\n\n        self.visit(stmt.bindings)\n        if stmt.iterator_stmt:\n            self.visit(stmt.iterator_stmt)\n        if isinstance(stmt, (irast.MutatingStmt, irast.GroupStmt)):\n            self.visit(stmt.subject)\n        if isinstance(stmt, irast.GroupStmt):\n            for v in stmt.using.values():\n                self.visit(v)\n        self.visit(stmt.result)\n\n        res = self.generic_visit(stmt)\n\n        self.aggregate = old\n\n        return res\n\n    def repeated_node_visit(self, node: irast.Base) -> None:\n        if isinstance(node, irast.Set):\n            self.seen[node.path_id] = True\n\n    def visit_Set(self, node: irast.Set, skip_rptr: bool = False) -> None:\n        self.seen[node.path_id] = True\n\n        if node.path_id == self.target:\n            self.sightings.add(self.aggregate)\n            return\n\n        old_scope = self.scope_tree\n        if node.path_scope_id is not None:\n            self.scope_tree = self.ctx.env.scope_tree_nodes[node.path_scope_id]\n\n        # We also can't handle references inside of a semi-join,\n        # because the bodies are executed one at a time, and so the\n        # semi-join deduplication doesn't work.\n        is_semijoin = (\n            isinstance(node.expr, irast.Pointer)\n            and node.path_id.is_objtype_path()\n            and not self.scope_tree.is_visible(node.expr.source.path_id)\n        )\n\n        old = self.aggregate\n        if is_semijoin:\n            self.aggregate = None\n\n        self.visit(node.shape)\n\n        if isinstance(node.expr, irast.Pointer):\n            sub_expr = node.expr.expr\n            if not sub_expr:\n                self.visit(node.expr.source)\n            else:\n                if node.expr.source.path_id not in self.seen:\n                    self.seen[node.expr.source.path_id] = False\n        else:\n            sub_expr = node.expr\n\n        if isinstance(sub_expr, irast.Call):\n            self.process_call(sub_expr, node)\n        else:\n            self.visit(sub_expr)\n\n        self.aggregate = old\n        self.scope_tree = old_scope\n\n    def process_call(self, node: irast.Call, ir_set: irast.Set) -> None:\n        # It needs to be backed by an actual SQL function and must\n        # not return SET OF\n        returns_set = node.typemod == qltypes.TypeModifier.SetOfType\n        calls_sql_func = (\n            isinstance(node, irast.FunctionCall)\n            and node.func_sql_function\n        )\n        for arg in node.args.values():\n            typemod = arg.param_typemod\n            old = self.aggregate\n            # If this *returns* a set, it is going to mess things up since\n            # the operation can't actually run on multiple things...\n\n            old_seen = None\n\n            # TODO: we would like to do better in some cases with\n            # DISTINCT and the like where there are built in features\n            # to do it in a GROUP\n            if returns_set:\n                self.aggregate = None\n            elif (\n                calls_sql_func\n                and typemod == qltypes.TypeModifier.SetOfType\n                # Don't hoist aggregates whose outputs contain objects\n                # (I think this can only be array_agg).\n                #\n                # We have to eta-expand to put a shape on them anyway,\n                # so there's no real point, and we mishandled that\n                # case in a few places.  Eventually we'll want to properly\n                # be able to serialize in the first place, though.\n                and not setgen.get_set_type(\n                    ir_set, ctx=self.ctx).contains_object(self.ctx.env.schema)\n            ):\n                old_seen = self.seen\n                self.seen = {}\n                self.aggregate = ir_set\n            self.visit(arg)\n            self.aggregate = old\n\n            force_fail = False\n            if old_seen is not None:\n                self.skippable[ir_set] = frozenset({\n                    k for k, v in self.seen.items() if not v\n                    and self.scope_tree.is_visible(k)\n                })\n                for k, was_seen in self.seen.items():\n                    # If we referred to some visible set and also\n                    # spotted the target, we can't actually compile\n                    # the target separately, so ditch it.\n                    if (\n                        was_seen\n                        and self.scope_tree.is_visible(k)\n                        and ir_set in self.sightings\n                    ):\n                        force_fail = True\n                        self.sightings.discard(ir_set)\n                        self.sightings.add(None)\n                    old_seen[k] = self.seen.get(k, False) | was_seen\n\n                # If, assuming the target is single, the aggregate is\n                # still multi, then we can't extract it, since that\n                # would lead to actually return multiple elements in a\n                # SQL subquery.\n                if (\n                    ir_set in self.sightings\n                    and inference.infer_cardinality(\n                        arg.expr, scope_tree=self.scope_tree,\n                        ctx=self.infctx).is_multi()\n                ):\n                    force_fail = True\n\n                self.seen = old_seen\n\n            if force_fail:\n                self.sightings.discard(ir_set)\n                self.sightings.add(None)\n\n\ndef infer_group_aggregates(\n    irs: Sequence[irast.Base],\n    *,\n    ctx: context.ContextLevel,\n) -> None:\n    groups = ast_visitor.find_children(irs, irast.GroupStmt)\n    for stmt in groups:\n        visitor = FindAggregatingUses(\n            stmt.group_binding.path_id,\n            ctx=ctx,\n        )\n        visitor.visit(stmt.result)\n        stmt.group_aggregate_sets = {\n            k: visitor.skippable.get(k, frozenset())\n            for k in visitor.sightings\n        }\n"
  },
  {
    "path": "edb/edgeql/compiler/inference/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2015-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n__all__ = (\n    'infer_cardinality',\n    'infer_volatility',\n    'infer_multiplicity',\n    'InfCtx',\n    'make_ctx',\n)\n\nfrom .cardinality import infer_cardinality  # NOQA\nfrom .context import InfCtx, make_ctx  # NOQA\nfrom .multiplicity import infer_multiplicity  # NOQA\nfrom .volatility import infer_volatility  # NOQA\n"
  },
  {
    "path": "edb/edgeql/compiler/inference/cardinality.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL cardinality inference.\n\nA top-down cardinality inferer that traverses the full AST populating\ncardinality fields and performing cardinality checks.\n\"\"\"\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Optional,\n    Iterable,\n    Sequence,\n    NamedTuple,\n)\n\nimport enum\nimport functools\nimport uuid\n\nfrom edb import errors\nfrom edb.common import parsing\n\nfrom edb.edgeql import qltypes\n\nfrom edb.schema import name as sn\nfrom edb.schema import types as s_types\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import constraints as s_constraints\n\nfrom edb.ir import ast as irast\nfrom edb.ir import utils as irutils\nfrom edb.ir import typeutils\nfrom edb.edgeql import ast as qlast\n\nfrom . import context as inference_context\nfrom . import utils as inf_utils\nfrom . import volatility\nfrom . import multiplicity\n\nfrom .. import context\n\n\nAT_MOST_ONE = qltypes.Cardinality.AT_MOST_ONE\nONE = qltypes.Cardinality.ONE\nMANY = qltypes.Cardinality.MANY\nAT_LEAST_ONE = qltypes.Cardinality.AT_LEAST_ONE\n\n\nclass CardinalityBound(int, enum.Enum):\n    '''This enum is used to perform some of the cardinality operations.'''\n    ZERO = 0\n    ONE = 1\n    MANY = 2\n\n    def __add__(self, other: int) -> CardinalityBound:\n        return CardinalityBound(min(int(self) + other, CB_MANY))\n\n    def __mul__(self, other: int) -> CardinalityBound:\n        return CardinalityBound(min(int(self) * other, CB_MANY))\n\n    def as_required(self) -> bool:\n        return self >= CB_ONE\n\n    def as_schema_cardinality(self) -> qltypes.SchemaCardinality:\n        if self >= CB_MANY:\n            return qltypes.SchemaCardinality.Many\n        else:\n            return qltypes.SchemaCardinality.One\n\n    @classmethod\n    def from_required(cls, required: bool) -> CardinalityBound:\n        return CB_ONE if required else CB_ZERO\n\n    @classmethod\n    def from_schema_value(\n        cls, card: qltypes.SchemaCardinality\n    ) -> CardinalityBound:\n        if card >= qltypes.SchemaCardinality.Many:\n            return CB_MANY\n        else:\n            return CB_ONE\n\n\nCB_ZERO = CardinalityBound.ZERO\nCB_ONE = CardinalityBound.ONE\nCB_MANY = CardinalityBound.MANY\n\n\nclass CardinalityBounds(NamedTuple):\n    lower: CardinalityBound\n    upper: CardinalityBound\n\n\ndef _card_to_bounds(card: qltypes.Cardinality) -> CardinalityBounds:\n    lower, upper = card.to_schema_value()\n    return CardinalityBounds(\n        CardinalityBound.from_required(lower),\n        CardinalityBound.from_schema_value(upper),\n    )\n\n\ndef _bounds_to_card(\n    lower: CardinalityBound,\n    upper: CardinalityBound,\n) -> qltypes.Cardinality:\n    return qltypes.Cardinality.from_schema_value(\n        lower.as_required(),\n        upper.as_schema_cardinality(),\n    )\n\n\ndef _card_unzip(\n    args: Iterable[qltypes.Cardinality],\n) -> tuple[tuple[CardinalityBound, ...], tuple[CardinalityBound, ...]]:\n    card = list(zip(*(_card_to_bounds(a) for a in args)))\n    lower, upper = card if card else ((), ())\n    return lower, upper\n\n\ndef product(arg: Iterable[CardinalityBound]) -> CardinalityBound:\n    res = CB_ONE\n    for x in arg:\n        res *= x\n    return res\n\n\ndef cartesian_cardinality(\n    args: Iterable[qltypes.Cardinality],\n) -> qltypes.Cardinality:\n    '''Cardinality of Cartesian product of multiple args.'''\n    lower, upper = _card_unzip(args)\n    return _bounds_to_card(product(lower), product(upper))\n\n\ndef max_cardinality(\n    args: Iterable[qltypes.Cardinality],\n) -> qltypes.Cardinality:\n    '''Maximum lower and upper bound of specified cardinalities.'''\n\n    lower, upper = _card_unzip(args)\n    assert lower, \"cannot take max cardinality of no elements\"\n    return _bounds_to_card(max(lower), max(upper))\n\n\ndef min_cardinality(\n    args: Iterable[qltypes.Cardinality],\n) -> qltypes.Cardinality:\n    '''Minimum lower and upper bound of specified cardinalities.'''\n\n    lower, upper = _card_unzip(args)\n    assert lower, \"cannot take min cardinality of no elements\"\n    return _bounds_to_card(min(lower), min(upper))\n\n\ndef _union_cardinality(\n    args: Iterable[qltypes.Cardinality],\n) -> qltypes.Cardinality:\n    '''Cardinality of UNION of multiple args.'''\n    lower, upper = _card_unzip(args)\n    return _bounds_to_card(\n        sum(lower, start=CB_ZERO), sum(upper, start=CB_ZERO))\n\n\nVOLATILE = qltypes.Volatility.Volatile\nMODIFYING = qltypes.Volatility.Modifying\n\n\ndef _check_op_volatility(\n    args: Sequence[irast.Base],\n    cards: Sequence[qltypes.Cardinality],\n    ctx: inference_context.InfCtx,\n) -> None:\n    vols = [volatility.infer_volatility(a, env=ctx.env) for a in args]\n\n    # Check the rules on volatility correlation: volatile operations\n    # can't be cross producted with any potentially multi set. We\n    # check this by assuming that a voltile operation is AT_MOST_ONE\n    # and making sure that the resulting cartesian cardinality isn't\n    # multi.\n    for i, vol in enumerate(vols):\n        if vol.is_volatile():\n            cards2 = list(cards)\n            cards2[i] = AT_MOST_ONE\n            if cartesian_cardinality(cards2).is_multi():\n                raise errors.QueryError(\n                    \"can not take cross product of volatile operation\",\n                    span=args[i].span\n                )\n\n\ndef _common_cardinality(\n    args: Sequence[irast.Base],\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    cards = [\n        infer_cardinality(\n            a, scope_tree=scope_tree, ctx=ctx\n        ) for a in args\n    ]\n    _check_op_volatility(args, cards, ctx=ctx)\n\n    return cartesian_cardinality(cards)\n\n\n@functools.singledispatch\ndef _infer_cardinality(\n    ir: irast.Base,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    raise ValueError(f'infer_cardinality: cannot handle {ir!r}')\n\n\n@_infer_cardinality.register\ndef __infer_statement(\n    ir: irast.Statement,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    return infer_cardinality(\n        ir.expr, scope_tree=scope_tree, ctx=ctx)\n\n\n@_infer_cardinality.register\ndef __infer_config_insert(\n    ir: irast.ConfigInsert,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    return infer_cardinality(\n        ir.expr, scope_tree=scope_tree, ctx=ctx)\n\n\n@_infer_cardinality.register\ndef __infer_config_set(\n    ir: irast.ConfigSet,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    card = infer_cardinality(\n        ir.expr, scope_tree=scope_tree, ctx=ctx)\n    if ir.required and card.can_be_zero():\n        raise errors.QueryError(\n            f\"possibly an empty set returned for \"\n            f\"a global declared as 'required'\",\n            span=ir.span,\n        )\n    if ir.cardinality.is_single() and not card.is_single():\n        raise errors.QueryError(\n            f\"possibly more than one element returned for \"\n            f\"a global declared as 'single'\",\n            span=ir.span,\n        )\n\n    return card\n\n\n@_infer_cardinality.register\ndef __infer_config_reset(\n    ir: irast.ConfigReset,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    if ir.selector:\n        return infer_cardinality(\n            ir.selector, scope_tree=scope_tree, ctx=ctx)\n    else:\n        return ONE\n\n\n@_infer_cardinality.register\ndef __infer_empty_set(\n    ir: irast.EmptySet,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    return AT_MOST_ONE\n\n\n@_infer_cardinality.register\ndef __infer_typeref(\n    ir: irast.TypeRef,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    return AT_MOST_ONE\n\n\n@_infer_cardinality.register\ndef __infer_type_introspection(\n    ir: irast.TypeIntrospection,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    return ONE\n\n\n@_infer_cardinality.register\ndef __infer_type_root(\n    ir: irast.TypeRoot,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    if typeutils.is_exactly_free_object(ir.typeref):\n        return ONE\n    else:\n        return MANY\n\n\n@_infer_cardinality.register\ndef __infer_cleared(\n    ir: irast.RefExpr,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    return MANY\n\n\ndef _infer_pointer_cardinality(\n    *,\n    ptrcls: s_pointers.Pointer,\n    ptrref: Optional[irast.BasePointerRef],\n    irexpr: irast.Base,\n    specified_required: Optional[bool] = None,\n    specified_card: Optional[qltypes.SchemaCardinality] = None,\n    is_mut_assignment: bool = False,\n    shape_op: qlast.ShapeOp = qlast.ShapeOp.ASSIGN,\n    source_ctx: Optional[parsing.Span] = None,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> None:\n\n    env = ctx.env\n\n    if specified_required is None:\n        spec_lower_bound = None\n    else:\n        spec_lower_bound = CardinalityBound.from_required(specified_required)\n\n    if specified_card is None:\n        spec_upper_bound = None\n    else:\n        spec_upper_bound = CardinalityBound.from_schema_value(specified_card)\n\n    expr_card = infer_cardinality(\n        irexpr, scope_tree=scope_tree, ctx=ctx)\n\n    ptrcls_schema_card = ptrcls.get_cardinality(env.schema)\n\n    # Infer cardinality and convert it back to schema values of \"ONE/MANY\".\n    if shape_op is qlast.ShapeOp.APPEND:\n        # += in shape always means MANY\n        inferred_card = qltypes.Cardinality.MANY\n    elif shape_op is qlast.ShapeOp.SUBTRACT:\n        # -= does not increase cardinality, but it may result in an empty set,\n        # hence AT_MOST_ONE.\n        inferred_card = qltypes.Cardinality.AT_MOST_ONE\n    else:\n        # Pull cardinality from the ptrcls, if it exists.\n        # (This generally will have been populated by the source_map\n        # handling in infer_toplevel_cardinality().)\n        if ptrcls_schema_card.is_known():\n            inferred_card = qltypes.Cardinality.from_schema_value(\n                not expr_card.can_be_zero(), ptrcls_schema_card\n            )\n        else:\n            inferred_card = expr_card\n\n    if spec_upper_bound is None and spec_lower_bound is None:\n        # Common case of no explicit specifier and no overloading.\n        ptr_card = inferred_card\n    else:\n        # Verify that the explicitly specified (or inherited) cardinality is\n        # within the cardinality bounds inferred from the expression, except\n        # for mutations we punt the lower cardinality bound check to the\n        # runtime DML constraint as that would produce a more meaningful error.\n        inf_lower_bound, inf_upper_bound = _card_to_bounds(inferred_card)\n\n        if spec_upper_bound is None:\n            upper_bound = inf_upper_bound\n        else:\n            if inf_upper_bound > spec_upper_bound:\n                desc = ptrcls.get_verbosename(env.schema)\n                if not is_mut_assignment:\n                    desc = f\"computed {desc}\"\n                raise errors.QueryError(\n                    f\"possibly more than one element returned by an \"\n                    f\"expression for a {desc} declared as 'single'\",\n                    span=source_ctx,\n                )\n            upper_bound = spec_upper_bound\n\n        if spec_lower_bound is None:\n            lower_bound = inf_lower_bound\n        else:\n            if inf_lower_bound < spec_lower_bound:\n                if is_mut_assignment:\n                    lower_bound = inf_lower_bound\n                else:\n                    desc = f\"computed {ptrcls.get_verbosename(env.schema)}\"\n                    raise errors.QueryError(\n                        f\"possibly an empty set returned by an \"\n                        f\"expression for a {desc} declared as 'required'\",\n                        span=source_ctx,\n                    )\n            else:\n                lower_bound = spec_lower_bound\n\n        ptr_card = _bounds_to_card(lower_bound, upper_bound)\n\n    if (\n        not ptrcls_schema_card.is_known()\n        or ptrcls in ctx.env.pointer_specified_info\n    ):\n        if ptrcls_schema_card.is_known():\n            # If we are overloading an existing pointer, take the _maximum_\n            # of the cardinalities.  In practice this only means that we might\n            # raise the lower bound, since the other redefinitions of bounds\n            # are prohibited above and in viewgen.\n            ptrcls_card = qltypes.Cardinality.from_schema_value(\n                ptrcls.get_required(env.schema),\n                ptrcls_schema_card,\n            )\n            if is_mut_assignment:\n                ptr_card = cartesian_cardinality((ptrcls_card, ptr_card))\n            else:\n                ptr_card = max_cardinality((ptrcls_card, ptr_card))\n        required, card = ptr_card.to_schema_value()\n        env.schema = ptrcls.set_field_value(env.schema, 'cardinality', card)\n        env.schema = ptrcls.set_field_value(env.schema, 'required', required)\n        if ctx.make_updates:\n            _update_cardinality_in_derived(ptrcls, env=ctx.env)\n\n    if ptrref and ctx.make_updates:\n        out_card, in_card = typeutils.cardinality_from_ptrcls(\n            env.schema, ptrcls)\n        assert in_card is not None\n        assert out_card is not None\n        ptrref.in_cardinality = in_card\n        ptrref.out_cardinality = out_card\n\n\ndef _update_cardinality_in_derived(\n    ptrcls: s_pointers.Pointer, *, env: context.Environment\n) -> None:\n\n    children = env.pointer_derivation_map.get(ptrcls)\n    if children:\n        ptrcls_cardinality = ptrcls.get_cardinality(env.schema)\n        ptrcls_required = ptrcls.get_required(env.schema)\n        assert ptrcls_cardinality.is_known()\n        for child in children:\n            env.schema = child.set_field_value(\n                env.schema, 'cardinality', ptrcls_cardinality)\n            env.schema = child.set_field_value(\n                env.schema, 'required', ptrcls_required)\n            _update_cardinality_in_derived(child, env=env)\n\n\ndef _infer_shape(\n    ir: irast.Set,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> None:\n    # Mark the source of the shape as being a singleton. We can't just\n    # rely on the scope tree, where it might appear as optional\n    # (giving us AT_MOST_ONE instead of ONE).\n    ctx = ctx._replace(singletons=ctx.singletons | {ir.path_id})\n\n    for shape_set, shape_op in ir.shape:\n        new_scope = inf_utils.get_set_scope(shape_set, scope_tree, ctx=ctx)\n        rptr = shape_set.expr\n        if rptr.expr:\n            ptrref = rptr.ptrref\n\n            ctx.env.schema, ptrcls = typeutils.ptrcls_from_ptrref(\n                ptrref, schema=ctx.env.schema)\n            assert isinstance(ptrcls, s_pointers.Pointer)\n            specified_card, specified_required, _ = (\n                ctx.env.pointer_specified_info.get(ptrcls,\n                                                   (None, False, None)))\n            assert isinstance(rptr.expr, irast.Stmt)\n\n            _infer_pointer_cardinality(\n                ptrcls=ptrcls,\n                ptrref=ptrref,\n                source_ctx=shape_set.span,\n                irexpr=rptr.expr,\n                is_mut_assignment=rptr.is_mutation,\n                specified_card=specified_card,\n                specified_required=specified_required,\n                shape_op=shape_op,\n                scope_tree=new_scope,\n                ctx=ctx,\n            )\n\n        _infer_shape(shape_set, scope_tree=scope_tree,\n                     ctx=ctx)\n\n\ndef _infer_set(\n    ir: irast.Set,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n\n    # First compute (or look up) the \"intrinsic\" cardinality of the set\n    if not (result := ctx.inferred_cardinality.get(ir)):\n        result = _infer_set_inner(\n            ir, scope_tree=scope_tree, ctx=ctx)\n\n        # We need to cache the main result before doing the shape,\n        # since sometimes the shape will refer to the enclosing set.\n        ctx.inferred_cardinality[ir] = result\n\n        new_scope = inf_utils.get_set_scope(ir, scope_tree, ctx=ctx)\n        _infer_shape(\n            ir, scope_tree=new_scope, ctx=ctx)\n\n    # With that in hand, compute the cardinality of a *reference* to the\n    # set from this location in the tree.\n    if ir.path_id in ctx.singletons:\n        return ONE\n    elif (node := inf_utils.find_visible(ir, scope_tree)) is not None:\n        if not node.optional:\n            return ONE\n        # If the set is visible, but optional, it must have upper bound ONE\n        # but we still want to compute the lower bound.\n        else:\n            return _bounds_to_card(_card_to_bounds(result).lower, CB_ONE)\n    else:\n        return result\n\n\n@_infer_cardinality.register\ndef _infer_pointer(\n    ir: irast.Pointer,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    raise AssertionError('TODO: properly infer Pointer-as-Expr ')\n\n\ndef _infer_set_inner(\n    ir: irast.Set,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    new_scope = inf_utils.get_set_scope(ir, scope_tree, ctx=ctx)\n\n    # TODO: Migrate to Pointer-as-Expr well, and not half-assedly.\n    sub_expr = irutils.sub_expr(ir)\n    if sub_expr:\n        expr_card = infer_cardinality(sub_expr, scope_tree=new_scope, ctx=ctx)\n\n    if isinstance(ir.expr, irast.Pointer) and not ir.expr.is_phony:\n        ptr = ir.expr\n\n        assert ir is not ptr.source, \"self-referential pointer\"\n        # FIXME: The thing blocking extracting Pointer inference from\n        # here is that this source inference relies on using the old\n        # scope_tree. I think this is probably fixable.\n        source_card = infer_cardinality(\n            ptr.source, scope_tree=scope_tree, ctx=ctx,\n        )\n\n        ctx.env.schema, ptrcls = typeutils.ptrcls_from_ptrref(\n            ptr.ptrref, schema=ctx.env.schema)\n        if ptr.expr:\n            assert isinstance(ptrcls, s_pointers.Pointer)\n            _infer_pointer_cardinality(\n                ptrcls=ptrcls,\n                ptrref=ptr.ptrref,\n                irexpr=ptr.expr,\n                scope_tree=scope_tree,\n                ctx=ctx,\n            )\n\n        if ptr.ptrref.union_components:\n            # We use cartesian cardinality instead of union cardinality\n            # because the union of pointers in this context is disjoint\n            # in a sense that for any specific source only a given union\n            # component is used.\n            rptrref_card = cartesian_cardinality(\n                c.dir_cardinality(ptr.direction)\n                for c in ptr.ptrref.union_components\n            )\n        elif ctx.ignore_computed_cards and ptr.expr:\n            rptrref_card = expr_card\n        elif isinstance(ptr.ptrref, irast.TypeIntersectionPointerRef):\n            rptrref_card = AT_MOST_ONE\n        else:\n            rptrref_card = ptr.ptrref.dir_cardinality(ptr.direction)\n\n        card = cartesian_cardinality((source_card, rptrref_card))\n\n        # \"Optional derefs\" (.?>) always produce an optional result.\n        if ptr.optional_deref:\n            card = cartesian_cardinality((AT_MOST_ONE, card))\n\n    elif sub_expr is not None:\n        card = expr_card\n    else:\n        # The only things that should be here without an expression or\n        # an rptr are certain visible_binding_refs (typically from\n        # GROUP). We report them as MANY, but that might be refined\n        # based on the scope tree in the enclosing context.\n        assert ir.is_visible_binding_ref\n        card = MANY\n\n    # If this node is an optional argument bound at this location,\n    # but it can't actually be zero, clear the optionality to avoid\n    # subpar codegen.\n    if (\n        new_scope.parent_fence\n        and (node := new_scope.parent_fence.find_child(\n            ir.path_id, in_branches=True\n        ))\n        and node.optional\n        and not card.can_be_zero()\n    ):\n        node.optional = False\n\n    return card\n\n\ndef _typemod_to_card(typemod: qltypes.TypeModifier) -> qltypes.Cardinality:\n    return (\n        MANY if typemod is qltypes.TypeModifier.SetOfType else\n        AT_MOST_ONE if typemod is qltypes.TypeModifier.OptionalType else\n        ONE\n    )\n\n\ndef _standard_call_cardinality(\n    ir: irast.Call,\n    cards: Sequence[qltypes.Cardinality],\n    *,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    # For regular functions and operators, the general rule of\n    # Cartesian cardinality of arguments applies, although we still\n    # have to account for the declared return cardinality, as the\n    # function might be OPTIONAL or SET OF in its return type.\n    #\n    # We compute the Cartesian cardinality of the functions's\n    # _non-SET OF_ arguments and its return, but with the lower bound\n    # of any optional arguments set to CB_ONE.\n    non_aggregate_args = []\n    non_aggregate_arg_cards = []\n\n    for arg, card in zip(ir.args.values(), cards):\n        typemod = arg.param_typemod\n        if typemod is qltypes.TypeModifier.SingletonType:\n            non_aggregate_args.append(arg.expr)\n            non_aggregate_arg_cards.append(card)\n        elif typemod is qltypes.TypeModifier.OptionalType:\n            non_aggregate_args.append(arg.expr)\n            non_aggregate_arg_cards.append(\n                _bounds_to_card(CB_ONE, _card_to_bounds(card).upper)\n            )\n\n    _check_op_volatility(\n        non_aggregate_args, non_aggregate_arg_cards, ctx=ctx)\n\n    return cartesian_cardinality(\n        non_aggregate_arg_cards + [_typemod_to_card(ir.typemod)])\n\n\n@_infer_cardinality.register\ndef __infer_func_call(\n    ir: irast.FunctionCall,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n\n    for glob_arg in (ir.global_args or ()):\n        infer_cardinality(glob_arg, scope_tree=scope_tree, ctx=ctx)\n\n    cards: list[qltypes.Cardinality] = []\n    arg_typemods: list[qltypes.TypeModifier] = []\n    for arg in ir.args.values():\n        card = infer_cardinality(arg.expr, scope_tree=scope_tree, ctx=ctx)\n        cards.append(card)\n        arg_typemods.append(arg.param_typemod)\n        if ctx.make_updates:\n            arg.cardinality = card\n\n    if ir.preserves_optionality or ir.preserves_upper_cardinality:\n        ret_lower_bound, ret_upper_bound = _card_to_bounds(\n            _typemod_to_card(ir.typemod))\n\n        # This is a generic aggregate function which preserves the\n        # optionality and/or upper cardinality of its generic\n        # argument.  For simplicity we are deliberately not checking\n        # the parameters here as that would have been done at the time\n        # of declaration.\n        arg_cards = []\n        force_multi = False\n\n        for arg, card in zip(ir.args.values(), cards):\n            typemod = arg.param_typemod\n            if typemod is not qltypes.TypeModifier.OptionalType:\n                arg_cards.append(card)\n            else:\n                force_multi |= card.is_multi()\n\n        arg_card = zip(*(_card_to_bounds(card) for card in arg_cards))\n        arg_lower, arg_upper = arg_card\n        lower = (\n            min(arg_lower) if ir.preserves_optionality else\n            CB_ONE if ir.func_shortname == sn.QualName('std', 'assert_exists')\n            else ret_lower_bound\n        )\n        upper = (CB_MANY if force_multi\n                 else max(arg_upper) if ir.preserves_upper_cardinality\n                 else ret_upper_bound)\n        call_card = _bounds_to_card(lower, upper)\n\n    else:\n        call_card = _standard_call_cardinality(ir, cards, ctx=ctx)\n\n    if ir.body is not None:\n        body_card = infer_cardinality(ir.body, scope_tree=scope_tree, ctx=ctx)\n        # Check that inline body cardinality does not disagree with\n        # declared function cardinality.\n        if body_card.can_be_zero() and not call_card.can_be_zero():\n            raise errors.QueryError(\n                'inline function body expression returns a possibly empty '\n                'result while the function is not declared as returning '\n                'OPTIONAL',\n                span=ir.span,\n            )\n        if body_card.is_multi() and not call_card.is_multi():\n            raise errors.QueryError(\n                'inline function body expression possibly returns more '\n                'than one element, while the function is not declared as '\n                'returning SET OF',\n                span=ir.span,\n            )\n\n    if ir.volatility == MODIFYING:\n        if any(card.is_multi() for card in cards):\n            raise errors.QueryError(\n                'possibly more than one element passed into modifying function',\n                span=ir.span,\n            )\n\n        if any(\n            (\n                card.can_be_zero()\n                and typemod == qltypes.TypeModifier.SingletonType\n            )\n            for card, typemod in zip(cards, arg_typemods)\n        ):\n            raise errors.QueryError(\n                'possibly an empty set passed as non-optional argument '\n                'into modifying function',\n                span=ir.span,\n            )\n\n    return call_card\n\n\n@_infer_cardinality.register\ndef __infer_oper_call(\n    ir: irast.OperatorCall,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    cards = []\n    for arg in ir.args.values():\n        card = infer_cardinality(arg.expr, scope_tree=scope_tree, ctx=ctx)\n        cards.append(card)\n        if ctx.make_updates:\n            arg.cardinality = card\n\n    if str(ir.func_shortname) == 'std::UNION':\n        # UNION needs to \"add up\" cardinalities.\n        return _union_cardinality(cards)\n    elif str(ir.func_shortname) == 'std::EXCEPT':\n        # EXCEPT cardinality cannot be greater than the first argument, but\n        # the lower bound can be ZERO.\n        _lower, upper = _card_to_bounds(cards[0])\n        return _bounds_to_card(CB_ZERO, upper)\n    elif str(ir.func_shortname) == 'std::INTERSECT':\n        # INTERSECT takes the minimum of cardinalities and makes the lower\n        # bound ZERO.\n        _lower, upper = _card_to_bounds(min_cardinality(cards))\n        return _bounds_to_card(CB_ZERO, upper)\n    elif str(ir.func_shortname) == 'std::??':\n        # Coalescing takes the maximum of both lower and upper bounds.\n        return max_cardinality(cards)\n    elif str(ir.func_shortname) in ('std::DISTINCT', 'std::IF'):\n        return cartesian_cardinality(cards)\n    else:\n        return _standard_call_cardinality(ir, cards, ctx=ctx)\n\n\n@_infer_cardinality.register\ndef __infer_const(\n    ir: irast.BaseConstant,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    return ONE\n\n\n@_infer_cardinality.register\ndef __infer_param(\n    ir: irast.QueryParameter,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    return ONE if ir.required else AT_MOST_ONE\n\n\n@_infer_cardinality.register\ndef __infer_function_param(\n    ir: irast.FunctionParameter,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    return ONE if ir.required else AT_MOST_ONE\n\n\n@_infer_cardinality.register\ndef __infer_inlined_param(\n    ir: irast.InlinedParameterExpr,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    return ONE if ir.required else AT_MOST_ONE\n\n\n@_infer_cardinality.register\ndef __infer_const_set(\n    ir: irast.ConstantSet,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    return ONE if len(ir.elements) == 1 else AT_LEAST_ONE\n\n\n@_infer_cardinality.register\ndef __infer_typecheckop(\n    ir: irast.TypeCheckOp,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    return infer_cardinality(\n        ir.left, scope_tree=scope_tree, ctx=ctx,\n    )\n\n\n@_infer_cardinality.register\ndef __infer_typecast(\n    ir: irast.TypeCast,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    card = infer_cardinality(\n        ir.expr, scope_tree=scope_tree, ctx=ctx,\n    )\n    # json values can be 'null', which produces an empty set, which we\n    # need to reflect in the cardinality.\n    if (\n        typeutils.is_json(ir.from_type)\n        and not ir.cardinality_mod == qlast.CardinalityModifier.Required\n    ):\n        card = _bounds_to_card(CB_ZERO, _card_to_bounds(card).upper)\n    return card\n\n\ndef _is_ptr_or_self_ref(\n    set: irast.Base,\n    result_expr: irast.Set,\n    env: context.Environment,\n) -> bool:\n    if not isinstance(set, irast.Set):\n        return False\n\n    srccls = env.set_types[result_expr]\n    if not isinstance(srccls, s_objtypes.ObjectType):\n        return False\n\n    if set.path_id == result_expr.path_id:\n        return True\n\n    if isinstance(set.expr, irast.Pointer):\n        rptr = set.expr\n        return (\n            isinstance(rptr.ptrref, irast.PointerRef)\n            and not rptr.ptrref.is_computable\n            and _is_ptr_or_self_ref(rptr.source, result_expr, env)\n        )\n    elif irutils.is_implicit_wrapper(set.expr):\n        return _is_ptr_or_self_ref(set.expr.result, result_expr, env)\n    else:\n        return False\n\n\ndef extract_filters(\n    result_set: irast.Set,\n    filter_set: irast.Set,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> Sequence[tuple[Sequence[s_pointers.Pointer], irast.Set]]:\n\n    env = ctx.env\n    schema = env.schema\n    scope_tree = inf_utils.get_set_scope(filter_set, scope_tree, ctx=ctx)\n\n    expr = filter_set.expr\n    if isinstance(expr, irast.OperatorCall):\n        if str(expr.func_shortname) == 'std::=':\n            left, right = [a.expr for a in expr.args.values()]\n            op_card = _common_cardinality(\n                [left, right], scope_tree=scope_tree, ctx=ctx\n            )\n            result_stype = env.set_types[result_set]\n\n            if op_card.is_multi():\n                pass\n\n            elif (\n                (left_matches := _is_ptr_or_self_ref(left, result_set, env))\n                or _is_ptr_or_self_ref(right, result_set, env)\n            ):\n                # If the match was on the right, flip the args\n                if not left_matches:\n                    left, right = right, left\n\n                if infer_cardinality(\n                    right, scope_tree=scope_tree, ctx=ctx,\n                ).is_single():\n                    pointers = []\n                    left_stype = env.set_types[left]\n                    if left_stype == result_stype:\n                        assert isinstance(left_stype, s_objtypes.ObjectType)\n                        ptr = left_stype.getptr(schema, sn.UnqualName('id'))\n                        pointers.append(ptr)\n                    else:\n                        while left.path_id != result_set.path_id:\n                            if irutils.is_implicit_wrapper(left.expr):\n                                left = left.expr.result\n                                continue\n\n                            assert isinstance(left.expr, irast.Pointer)\n                            ptr = env.schema.get(\n                                left.expr.ptrref.name,\n                                type=s_pointers.Pointer\n                            )\n                            pointers.append(ptr)\n                            left = left.expr.source\n                        pointers.reverse()\n\n                    return [(pointers, right)]\n\n        elif str(expr.func_shortname) == 'std::AND':\n            left, right = (\n                irutils.unwrap_set(a.expr)\n                for a in expr.args.values()\n            )\n\n            left_filters = extract_filters(\n                result_set, left, scope_tree, ctx\n            )\n            right_filters = extract_filters(\n                result_set, right, scope_tree, ctx\n            )\n\n            return [*left_filters, *right_filters]\n\n    return []\n\n\ndef _all_have_exclusive(\n    ptrs: Sequence[s_pointers.Pointer],\n    ctx: inference_context.InfCtx,\n) -> bool:\n    return all(\n        bool(ptr.get_exclusive_constraints(ctx.env.schema))\n        for ptr in ptrs\n    )\n\n\ndef _track_all_constraint_refs(\n    ptrs: Sequence[s_pointers.Pointer],\n    ctx: inference_context.InfCtx,\n) -> None:\n    for ptr in ptrs:\n        for constr in ptr.get_exclusive_constraints(ctx.env.schema):\n            # We need to track all schema refs, since an expression\n            # in the schema needs to depend on any constraint\n            # that affects its cardinality.\n            ctx.env.add_schema_ref(constr, None)\n\n\ndef extract_exclusive_filters(\n    result_set: irast.Set,\n    filter_set: irast.Set,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> list[tuple[tuple[s_pointers.Pointer, irast.Set], ...]]:\n\n    filtered_ptrs = extract_filters(result_set, filter_set, scope_tree, ctx)\n\n    results: list[tuple[tuple[s_pointers.Pointer, irast.Set], ...]] = []\n    if filtered_ptrs:\n        schema = ctx.env.schema\n        # Only look at paths where all trailing pointers are exclusive;\n        # that is, if we see `.foo.bar`, `bar` must be exclusive.\n        # If that's the case, then we can look at whether `.foo` is\n        # exclusive or used in an exclusive object constraint.\n        filtered_ptrs_map = {\n            ptrs[0].get_nearest_non_derived_parent(schema): (ptrs, expr)\n            for ptrs, expr in filtered_ptrs\n            if _all_have_exclusive(ptrs[1:], ctx)\n        }\n        ptr_set = set(filtered_ptrs_map)\n        # First look at each referenced pointer and see if it has\n        # an exclusive constraint.\n        for ptr, (ptrs, expr) in filtered_ptrs_map.items():\n            if _all_have_exclusive([ptr], ctx):\n                # Bingo, got an equality filter on a pointer with a\n                # unique constraint\n                results.append(((ptr, expr),))\n                _track_all_constraint_refs(ptrs, ctx)\n\n        # Then look at all the object exclusive constraints\n        result_stype = ctx.env.set_types[result_set]\n        obj_exclusives = get_object_exclusive_constraints(\n            result_stype, ptr_set, ctx.env)\n        for constr, obj_exc_ptrs in obj_exclusives.items():\n            results.append(\n                tuple((ptr, filtered_ptrs_map[ptr][1]) for ptr in obj_exc_ptrs)\n            )\n            ctx.env.add_schema_ref(constr, None)\n            for ptr in obj_exc_ptrs:\n                _track_all_constraint_refs(filtered_ptrs_map[ptr][0], ctx)\n\n    return results\n\n\ndef get_object_exclusive_constraints(\n    typ: s_types.Type,\n    ptr_set: set[s_pointers.Pointer],\n    env: context.Environment,\n) -> dict[s_constraints.Constraint, frozenset[s_pointers.Pointer]]:\n    \"\"\"Collect any exclusive object constraints that apply.\n\n    An object constraint applies if all of the pointers referenced\n    in it are filtered on in the query.\n    \"\"\"\n\n    if not isinstance(typ, s_objtypes.ObjectType):\n        return {}\n\n    schema = env.schema\n    exclusive = schema.get('std::exclusive', type=s_constraints.Constraint)\n\n    cnstrs = {}\n    typ = typ.get_nearest_non_derived_parent(schema)\n    for constr in typ.get_constraints(schema).objects(schema):\n        if (\n            constr.issubclass(schema, exclusive)\n            and (subjectexpr := constr.get_subjectexpr(schema))\n            # We ignore constraints with except expressions, because\n            # they can't actually ensure cardinality\n            and not constr.get_except_expr(schema)\n            # And delegated constraints can't either\n            and not constr.get_delegated(schema)\n        ):\n            if subjectexpr.refs is None:\n                continue\n            pointer_refs = frozenset({\n                x for x in subjectexpr.refs.objects(schema)\n                if isinstance(x, s_pointers.Pointer)\n            })\n            # If all of the referenced pointers are filtered on,\n            # we match.\n            if pointer_refs.issubset(ptr_set):\n                cnstrs[constr] = pointer_refs\n\n    return cnstrs\n\n\ndef _analyse_filter_clause(\n    result_set: irast.Set,\n    result_card: qltypes.Cardinality,\n    filter_clause: irast.Set,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    if extract_exclusive_filters(result_set, filter_clause, scope_tree, ctx):\n        return AT_MOST_ONE\n    else:\n        return result_card\n\n\ndef _infer_matset_cardinality(\n    materialized_sets: Optional[dict[uuid.UUID, irast.MaterializedSet]],\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> None:\n    if not materialized_sets:\n        return\n    if not ctx.make_updates:\n        return\n\n    for mat_set in materialized_sets.values():\n        if (len(mat_set.uses) <= 1\n                or mat_set.cardinality != qltypes.Cardinality.UNKNOWN):\n            continue\n        assert mat_set.materialized\n        # set it to something to prevent recursion\n        mat_set.cardinality = MANY\n        new_scope = inf_utils.get_set_scope(\n            mat_set.materialized, scope_tree, ctx=ctx)\n        mat_set.cardinality = infer_cardinality(\n            mat_set.materialized, scope_tree=new_scope, ctx=ctx,\n        )\n\n\ndef _infer_dml_check_cardinality(\n    ir: irast.MutatingStmt,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> None:\n    if not ctx.make_updates:\n        return\n    pctx = ctx._replace(singletons=ctx.singletons | {ir.result.path_id})\n    for read_pol in ir.read_policies.values():\n        read_pol.cardinality = infer_cardinality(\n            read_pol.expr, scope_tree=scope_tree, ctx=pctx\n        )\n\n    for write_pol in ir.write_policies.values():\n        for p in write_pol.policies:\n            p.cardinality = infer_cardinality(\n                p.expr, scope_tree=scope_tree, ctx=pctx\n            )\n\n    if ir.conflict_checks:\n        for on_conflict in ir.conflict_checks:\n            _infer_on_conflict_cardinality(\n                on_conflict, type_has_rewrites=False,\n                scope_tree=scope_tree, ctx=ctx,\n            )\n\n    if ir.rewrites:\n        for rewrites in ir.rewrites.by_type.values():\n            for rewrite, _ in rewrites.values():\n                infer_cardinality(\n                    rewrite,\n                    scope_tree=scope_tree,\n                    ctx=ctx,\n                )\n\n\ndef _infer_stmt_cardinality(\n    ir: irast.FilteredStmt,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    for part, _ in (ir.bindings or []):\n        infer_cardinality(part, scope_tree=scope_tree, ctx=ctx)\n\n    result = ir.subject if isinstance(ir, irast.MutatingStmt) else ir.result\n    result_card = infer_cardinality(\n        result,\n        scope_tree=scope_tree,\n        ctx=ctx,\n    )\n    if ir.where:\n        ir.where_card = infer_cardinality(\n            ir.where, scope_tree=scope_tree, ctx=ctx,\n        )\n\n        if (\n            ir.where_card.is_multi()\n            # Don't generate warnings against internally generated code\n            and ir.where.span\n        ):\n            ctx.env.warnings.append(errors.QueryError(\n                'possibly more than one element returned by an expression '\n                'in a FILTER clause',\n                hint='If this is intended, try using any()',\n                span=ir.where.span,\n            ))\n\n        # Cross with AT_MOST_ONE to ensure result can be empty\n        result_card = cartesian_cardinality([result_card, AT_MOST_ONE])\n\n    if result_card.is_multi() and ir.where:\n        result_mult = multiplicity.infer_multiplicity(\n            result, scope_tree=scope_tree, ctx=ctx)\n\n        # We can only apply filter clause restrictions when the result\n        # is a unique set, because if the set has duplicates we can\n        # also pick out duplicates.\n        if result_mult.is_unique():\n            result_card = _analyse_filter_clause(\n                ir.result, result_card, ir.where, scope_tree, ctx)\n\n    _infer_matset_cardinality(\n        ir.materialized_sets, scope_tree=scope_tree, ctx=ctx)\n\n    if isinstance(ir, irast.MutatingStmt):\n        _infer_dml_check_cardinality(ir, scope_tree=scope_tree, ctx=ctx)\n\n    return result_card\n\n\ndef _infer_singleton_only(\n    part: irast.Set,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    new_scope = inf_utils.get_set_scope(part, scope_tree, ctx=ctx)\n    card = infer_cardinality(part, scope_tree=new_scope, ctx=ctx)\n    if card.is_multi():\n        raise errors.QueryError(\n            'possibly more than one element returned by an expression '\n            'where only singletons are allowed',\n            span=part.span)\n    return card\n\n\ndef _infer_on_conflict_cardinality(\n    on_conflict: irast.OnConflictClause,\n    *,\n    type_has_rewrites: bool,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    # Note: If we start supporting ELSE without ON, we'll need to\n    # factor the cardinality of this into the else_card below\n    infer_cardinality(\n        on_conflict.select_ir, scope_tree=scope_tree, ctx=ctx)\n\n    card = AT_MOST_ONE\n    if on_conflict.else_ir:\n        else_card = infer_cardinality(\n            on_conflict.else_ir, scope_tree=scope_tree, ctx=ctx)\n        card = max_cardinality((card, else_card))\n        if type_has_rewrites:\n            card = _bounds_to_card(CB_ZERO, _card_to_bounds(card).upper)\n\n    return card\n\n\n@_infer_cardinality.register\ndef __infer_select_stmt(\n    ir: irast.SelectStmt,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n\n    if ir.iterator_stmt:\n        iter_card = infer_cardinality(\n            ir.iterator_stmt, scope_tree=scope_tree, ctx=ctx,\n        )\n\n    stmt_card = _infer_stmt_cardinality(ir, scope_tree=scope_tree, ctx=ctx)\n\n    for part in [ir.limit, ir.offset] + [\n            sort.expr for sort in (ir.orderby or ())]:\n        if part:\n            _infer_singleton_only(part, scope_tree=scope_tree, ctx=ctx)\n\n    if ir.limit is not None:\n        if (\n            isinstance(ir.limit.expr, irast.IntegerConstant)\n            and ir.limit.expr.value == '1'\n        ):\n            # Explicit LIMIT 1 clause.\n            stmt_card = _bounds_to_card(\n                _card_to_bounds(stmt_card).lower, CB_ONE)\n        elif (\n            not isinstance(ir.limit.expr, irast.IntegerConstant)\n            or ir.limit.expr.value == '0'\n        ):\n            # LIMIT 0 or a non-static LIMIT that could be 0\n            stmt_card = _bounds_to_card(\n                CB_ZERO, _card_to_bounds(stmt_card).upper)\n\n    if ir.offset is not None:\n        stmt_card = _bounds_to_card(\n            CB_ZERO, _card_to_bounds(stmt_card).upper)\n\n    if ir.iterator_stmt:\n        stmt_card = cartesian_cardinality((stmt_card, iter_card))\n\n    # But actually! Check if it is overridden\n    if ir.card_inference_override:\n        stmt_card = infer_cardinality(\n            ir.card_inference_override, scope_tree=scope_tree, ctx=ctx)\n\n    return stmt_card\n\n\n@_infer_cardinality.register\ndef __infer_insert_stmt(\n    ir: irast.InsertStmt,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    for part, _ in (ir.bindings or []):\n        infer_cardinality(part, scope_tree=scope_tree, ctx=ctx)\n\n    infer_cardinality(\n        ir.subject, scope_tree=scope_tree, ctx=ctx\n    )\n    new_scope = inf_utils.get_set_scope(ir.result, scope_tree, ctx=ctx)\n    infer_cardinality(\n        ir.result, scope_tree=new_scope, ctx=ctx\n    )\n\n    assert not ir.iterator_stmt, \"InsertStmt shouldn't ever have an iterator\"\n\n    _infer_matset_cardinality(\n        ir.materialized_sets, scope_tree=scope_tree, ctx=ctx)\n\n    _infer_dml_check_cardinality(ir, scope_tree=scope_tree, ctx=ctx)\n\n    # INSERT without a FOR is always a singleton.\n    if not ir.on_conflict:\n        return ONE\n    # ... except if UNLESS CONFLICT is used\n    else:\n        return _infer_on_conflict_cardinality(\n            ir.on_conflict,\n            type_has_rewrites=bool(ir.write_policies),\n            scope_tree=scope_tree,\n            ctx=ctx,\n        )\n\n\n@_infer_cardinality.register\ndef __infer_update_stmt(\n    ir: irast.UpdateStmt,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    infer_cardinality(\n        ir.result, scope_tree=scope_tree, ctx=ctx,\n    )\n\n    return _infer_stmt_cardinality(ir, scope_tree=scope_tree, ctx=ctx)\n\n\n@_infer_cardinality.register\ndef __infer_delete_stmt(\n    ir: irast.DeleteStmt,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    infer_cardinality(\n        ir.result, scope_tree=scope_tree, ctx=ctx,\n    )\n\n    return _infer_stmt_cardinality(ir, scope_tree=scope_tree, ctx=ctx)\n\n\n@_infer_cardinality.register\ndef __infer_group_stmt(\n    ir: irast.GroupStmt,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    infer_cardinality(ir.subject, scope_tree=scope_tree, ctx=ctx)\n    for key, (binding, _) in ir.using.items():\n        binding_card = _infer_singleton_only(\n            binding, scope_tree=scope_tree, ctx=ctx)\n        ir.using[key] = binding, binding_card\n\n    infer_cardinality(ir.group_binding, scope_tree=scope_tree, ctx=ctx)\n\n    _infer_stmt_cardinality(ir, scope_tree=scope_tree, ctx=ctx)\n\n    for part in (ir.orderby or ()):\n        _infer_singleton_only(part.expr, scope_tree=scope_tree, ctx=ctx)\n\n    return MANY\n\n\n@_infer_cardinality.register\ndef __infer_slice(\n    ir: irast.SliceIndirection,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    # slice indirection cardinality depends on the cardinality of\n    # the base expression and the slice index expressions\n    args: list[irast.Base] = [ir.expr]\n    if ir.start is not None:\n        args.append(ir.start)\n    if ir.stop is not None:\n        args.append(ir.stop)\n\n    return _common_cardinality(args, scope_tree=scope_tree, ctx=ctx)\n\n\n@_infer_cardinality.register\ndef __infer_index(\n    ir: irast.IndexIndirection,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    # index indirection cardinality depends on both the cardinality of\n    # the base expression and the index expression\n    return _common_cardinality(\n        [ir.expr, ir.index], scope_tree=scope_tree, ctx=ctx,\n    )\n\n\n@_infer_cardinality.register\ndef __infer_array(\n    ir: irast.Array,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    return _common_cardinality(ir.elements, scope_tree=scope_tree, ctx=ctx)\n\n\n@_infer_cardinality.register\ndef __infer_tuple(\n    ir: irast.Tuple,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    return _common_cardinality(\n        [el.val for el in ir.elements], scope_tree=scope_tree, ctx=ctx\n    )\n\n\n@_infer_cardinality.register\ndef __infer_trigger_anchor(\n    ir: irast.TriggerAnchor,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    return MANY\n\n\n@_infer_cardinality.register\ndef __infer_searchable_string(\n    ir: irast.FTSDocument,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    return _common_cardinality(\n        (ir.text, ir.language), scope_tree=scope_tree, ctx=ctx\n    )\n\n\ndef infer_cardinality(\n    ir: irast.Base,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inference_context.InfCtx,\n) -> qltypes.Cardinality:\n    key = (ir, scope_tree, ctx.singletons)\n    result = ctx.inferred_cardinality.get(key)\n    if result is not None:\n        return result\n\n    if isinstance(ir, irast.Set):\n        result = _infer_set(\n            ir, scope_tree=scope_tree, ctx=ctx,\n        )\n    else:\n        result = _infer_cardinality(ir, scope_tree=scope_tree, ctx=ctx)\n\n    if result not in {AT_MOST_ONE, ONE, MANY, AT_LEAST_ONE}:\n        raise errors.QueryError(\n            'could not determine the cardinality of '\n            'set produced by expression',\n            span=ir.span)\n\n    ctx.inferred_cardinality[key] = result\n\n    return result\n\n\ndef is_subset_cardinality(\n    card0: qltypes.Cardinality, card1: qltypes.Cardinality\n) -> bool:\n    '''Determine if card0 is a subset of card1.'''\n    l0, u0 = _card_to_bounds(card0)\n    l1, u1 = _card_to_bounds(card1)\n\n    return l0 >= l1 and u0 <= u1\n"
  },
  {
    "path": "edb/edgeql/compiler/inference/context.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Optional, NamedTuple\n\nimport dataclasses\n\nfrom edb.ir import ast as irast\nfrom edb.edgeql import qltypes\n\nfrom .. import context\n\n\n@dataclasses.dataclass(frozen=True, eq=False)\nclass MultiplicityInfo:\n    \"\"\"Extended multiplicity descriptor\"\"\"\n\n    #: Actual multiplicity number\n    own: qltypes.Multiplicity\n    #: Whether this multiplicity descriptor describes\n    #: part of a disjoint set.\n    disjoint_union: bool = False\n\n    def is_empty(self) -> bool:\n        return self.own.is_empty()\n\n    def is_unique(self) -> bool:\n        return self.own.is_unique()\n\n    def is_duplicate(self) -> bool:\n        return self.own.is_duplicate()\n\n\nclass InfCtx(NamedTuple):\n    env: context.Environment\n    inferred_cardinality: dict[\n        tuple[irast.Base, irast.ScopeTreeNode, frozenset[irast.PathId]]\n        | irast.Base,\n        qltypes.Cardinality,\n    ]\n    inferred_multiplicity: dict[\n        tuple[irast.Base, irast.ScopeTreeNode, Optional[irast.PathId]],\n        MultiplicityInfo,\n    ]\n    singletons: frozenset[irast.PathId]\n    distinct_iterator: Optional[irast.PathId]\n    ignore_computed_cards: bool\n    # Whether to make updates to the cardinality fields in the IR/schema.\n    # This is used in cases where we need to do a \"hypothetical\"\n    # inference, but don't want to affect real state.\n    make_updates: bool\n\n\ndef make_ctx(env: context.Environment) -> InfCtx:\n    return InfCtx(\n        env=env,\n        inferred_cardinality={},\n        inferred_multiplicity={},\n        singletons=frozenset(env.singletons),\n        distinct_iterator=None,\n        ignore_computed_cards=False,\n        make_updates=True,\n    )\n"
  },
  {
    "path": "edb/edgeql/compiler/inference/multiplicity.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL multiplicity inference.\n\nA top-down multiplicity inferer that traverses the full AST populating\nmultiplicity fields and performing multiplicity checks.\n\"\"\"\n\n\nfrom __future__ import annotations\nfrom typing import Iterable\n\nimport dataclasses\nimport functools\nimport itertools\n\nfrom edb.common.typeutils import downcast\n\nfrom edb import errors\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import pointers as s_pointers\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtyputils\nfrom edb.ir import utils as irutils\n\nfrom . import cardinality\nfrom . import context as inf_ctx\nfrom . import utils as inf_utils\n\n\nEMPTY = inf_ctx.MultiplicityInfo(own=qltypes.Multiplicity.EMPTY)\nUNIQUE = inf_ctx.MultiplicityInfo(own=qltypes.Multiplicity.UNIQUE)\nDUPLICATE = inf_ctx.MultiplicityInfo(own=qltypes.Multiplicity.DUPLICATE)\nDISTINCT_UNION = inf_ctx.MultiplicityInfo(\n    own=qltypes.Multiplicity.UNIQUE,\n    disjoint_union=True,\n)\n\n\n@dataclasses.dataclass(frozen=True, eq=False)\nclass ContainerMultiplicityInfo(inf_ctx.MultiplicityInfo):\n    \"\"\"Multiplicity descriptor for an expression returning a container\"\"\"\n\n    #: Individual multiplicity values for container elements.\n    elements: tuple[inf_ctx.MultiplicityInfo, ...] = ()\n\n\ndef _max_multiplicity(\n    args: Iterable[inf_ctx.MultiplicityInfo],\n) -> inf_ctx.MultiplicityInfo:\n    arg_list = [a.own for a in args]\n    if not arg_list:\n        max_mult = qltypes.Multiplicity.UNIQUE\n    else:\n        max_mult = max(arg_list)\n\n    return inf_ctx.MultiplicityInfo(own=max_mult)\n\n\ndef _min_multiplicity(\n    args: Iterable[inf_ctx.MultiplicityInfo],\n) -> inf_ctx.MultiplicityInfo:\n    arg_list = [a.own for a in args]\n    if not arg_list:\n        min_mult = qltypes.Multiplicity.UNIQUE\n    else:\n        min_mult = min(arg_list)\n\n    return inf_ctx.MultiplicityInfo(own=min_mult)\n\n\ndef _common_multiplicity(\n    args: Iterable[irast.Base],\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    return _max_multiplicity(\n        infer_multiplicity(a, scope_tree=scope_tree, ctx=ctx) for a in args)\n\n\n@functools.singledispatch\ndef _infer_multiplicity(\n    ir: irast.Base,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    # return DUPLICATE\n    raise ValueError(f'infer_multiplicity: cannot handle {ir!r}')\n\n\n@_infer_multiplicity.register\ndef __infer_config_insert(\n    ir: irast.ConfigInsert,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    return infer_multiplicity(\n        ir.expr, scope_tree=scope_tree, ctx=ctx)\n\n\n@_infer_multiplicity.register\ndef __infer_config_set(\n    ir: irast.ConfigSet,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    return infer_multiplicity(\n        ir.expr, scope_tree=scope_tree, ctx=ctx)\n\n\n@_infer_multiplicity.register\ndef __infer_config_reset(\n    ir: irast.ConfigReset,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    if ir.selector:\n        return infer_multiplicity(\n            ir.selector, scope_tree=scope_tree, ctx=ctx)\n    else:\n        return UNIQUE\n\n\n@_infer_multiplicity.register\ndef __infer_empty_set(\n    ir: irast.EmptySet,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    return EMPTY\n\n\n@_infer_multiplicity.register\ndef __infer_type_introspection(\n    ir: irast.TypeIntrospection,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    # TODO: The result is always UNIQUE, but we still want to actually\n    # introspect the expression. Unfortunately, currently the\n    # expression is not available at this stage.\n    #\n    # E.g. consider:\n    #   WITH X := Foo {bar := {Bar, Bar}}\n    #   SELECT INTROSPECT TYPEOF X.bar;\n    return UNIQUE\n\n\n@_infer_multiplicity.register\ndef __infer_type_root(\n    ir: irast.TypeRoot,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    return UNIQUE\n\n\n@_infer_multiplicity.register\ndef __infer_cleared(\n    ir: irast.RefExpr,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    return DUPLICATE\n\n\ndef _infer_shape(\n    ir: irast.Set,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> None:\n    for shape_set, shape_op in ir.shape:\n        new_scope = inf_utils.get_set_scope(shape_set, scope_tree, ctx=ctx)\n\n        rptr = shape_set.expr\n        if rptr.expr:\n            expr_mult = infer_multiplicity(\n                rptr.expr, scope_tree=new_scope, ctx=ctx)\n\n            ptrref = rptr.ptrref\n            if (\n                expr_mult.is_duplicate()\n                and shape_op is not qlast.ShapeOp.APPEND\n                and shape_op is not qlast.ShapeOp.SUBTRACT\n                and irtyputils.is_object(ptrref.out_target)\n            ):\n                ctx.env.schema, ptrcls = irtyputils.ptrcls_from_ptrref(\n                    ptrref, schema=ctx.env.schema)\n                assert isinstance(ptrcls, s_pointers.Pointer)\n                desc = ptrcls.get_verbosename(ctx.env.schema)\n                if not rptr.is_mutation:\n                    desc = f\"computed {desc}\"\n                raise errors.QueryError(\n                    f'possibly not a distinct set returned by an '\n                    f'expression for a {desc}',\n                    hint=(\n                        f'You can use assert_distinct() around the expression '\n                        f'to turn this into a runtime assertion, or the '\n                        f'DISTINCT operator to silently discard duplicate '\n                        f'elements.'\n                    ),\n                    span=shape_set.span\n                )\n\n        _infer_shape(\n            shape_set, scope_tree=scope_tree, ctx=ctx)\n\n\ndef _infer_set(\n    ir: irast.Set,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    result = _infer_set_inner(\n        ir, scope_tree=scope_tree, ctx=ctx\n    )\n    ctx.inferred_multiplicity[ir, scope_tree, ctx.distinct_iterator] = result\n\n    # The shape doesn't affect multiplicity, but requires validation.\n    _infer_shape(ir, scope_tree=scope_tree, ctx=ctx)\n\n    return result\n\n\ndef _infer_set_inner(\n    ir: irast.Set,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    new_scope = inf_utils.get_set_scope(ir, scope_tree, ctx=ctx)\n\n    # TODO: Migrate to Pointer-as-Expr well, and not half-assedly.\n    sub_expr = irutils.sub_expr(ir)\n    if sub_expr is None:\n        expr_mult = None\n    else:\n        expr_mult = infer_multiplicity(sub_expr, scope_tree=new_scope, ctx=ctx)\n\n    if isinstance(ir.expr, irast.Pointer):\n        ptr = ir.expr\n        src_mult = infer_multiplicity(\n            ptr.source, scope_tree=new_scope, ctx=ctx\n        )\n\n        if isinstance(ptr.ptrref, irast.TupleIndirectionPointerRef):\n            if isinstance(src_mult, ContainerMultiplicityInfo):\n                idx = irtyputils.get_tuple_element_index(ptr.ptrref)\n                path_mult = src_mult.elements[idx]\n            else:\n                # All bets are off for tuple elements coming from\n                # opaque tuples.\n                path_mult = DUPLICATE\n        elif not irtyputils.is_object(ir.typeref):\n            # This is not an expression and is some kind of scalar, so\n            # multiplicity cannot be guaranteed to be UNIQUE (most scalar\n            # expressions don't have an implicit requirement to be sets)\n            # unless we also have an exclusive constraint.\n            if (\n                expr_mult is not None\n                and inf_utils.find_visible(ptr.source, new_scope) is not None\n            ):\n                path_mult = expr_mult\n            else:\n                schema = ctx.env.schema\n                # We should only have some kind of path terminating in a\n                # property here.\n                assert isinstance(ptr.ptrref, irast.PointerRef)\n                pointer = schema.get_by_id(\n                    ptr.ptrref.id, type=s_pointers.Pointer\n                )\n                if pointer.is_exclusive(schema):\n                    # Got an exclusive constraint\n                    path_mult = UNIQUE\n                else:\n                    path_mult = DUPLICATE\n        else:\n            # This is some kind of a link at the end of a path.\n            # Therefore the target is a proper set.\n            path_mult = UNIQUE\n\n    elif expr_mult is not None:\n        path_mult = expr_mult\n\n    else:\n        # Evidently this is not a pointer, expression, or a scalar.\n        # This is an object type and therefore a proper set.\n        path_mult = UNIQUE\n\n    if (\n        not path_mult.is_duplicate()\n        and irutils.get_path_root(ir).path_id == ctx.distinct_iterator\n    ):\n        path_mult = dataclasses.replace(path_mult, disjoint_union=True)\n\n    if irtyputils.is_free_object(ir.typeref):\n        path_mult = UNIQUE\n\n    return path_mult\n\n\n@_infer_multiplicity.register\ndef __infer_func_call(\n    ir: irast.FunctionCall,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    card = cardinality.infer_cardinality(ir, scope_tree=scope_tree, ctx=ctx)\n    args_mult = []\n    for arg in ir.args.values():\n        arg_mult = infer_multiplicity(arg.expr, scope_tree=scope_tree, ctx=ctx)\n        args_mult.append(arg_mult)\n        arg.multiplicity = arg_mult.own\n\n    if ir.global_args:\n        for g_arg in ir.global_args:\n            _infer_set(g_arg, scope_tree=scope_tree, ctx=ctx)\n\n    if ir.body:\n        infer_multiplicity(ir.body, scope_tree=scope_tree, ctx=ctx)\n\n    if card.is_single():\n        return UNIQUE\n    elif str(ir.func_shortname) == 'std::assert_distinct':\n        return UNIQUE\n    elif str(ir.func_shortname) == 'std::assert_exists':\n        return args_mult[1]\n    elif str(ir.func_shortname) == 'std::enumerate':\n        # The output of enumerate is always of multiplicity UNIQUE because\n        # it's a set of tuples with first elements being guaranteed to be\n        # distinct.\n        return ContainerMultiplicityInfo(\n            own=qltypes.Multiplicity.UNIQUE,\n            elements=(UNIQUE,) + tuple(args_mult),\n        )\n    else:\n        # If the function returns a set (for any reason), all bets are off\n        # and the maximum multiplicity cannot be inferred.\n        return DUPLICATE\n\n\n@_infer_multiplicity.register\ndef __infer_oper_call(\n    ir: irast.OperatorCall,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    card = cardinality.infer_cardinality(ir, scope_tree=scope_tree, ctx=ctx)\n    mult: list[inf_ctx.MultiplicityInfo] = []\n    cards: list[qltypes.Cardinality] = []\n    for arg in ir.args.values():\n        cards.append(\n            cardinality.infer_cardinality(\n                arg.expr, scope_tree=scope_tree, ctx=ctx\n            )\n        )\n\n        m = infer_multiplicity(arg.expr, scope_tree=scope_tree, ctx=ctx)\n        arg.multiplicity = m.own\n        mult.append(m)\n\n    op_name = str(ir.func_shortname)\n\n    if op_name == 'std::UNION':\n        # UNION will produce multiplicity DUPLICATE unless most or all of\n        # the elements multiplicity is ZERO (from an empty set), or\n        # all of the elements are sets of unrelated object types of\n        # multiplicity at most UNIQUE, or if all elements have been\n        # proven to be disjoint (e.g. a UNION of INSERTs).\n        result = EMPTY\n\n        arg_type = ctx.env.set_types[ir.args[0].expr]\n        if isinstance(arg_type, s_objtypes.ObjectType):\n            types: list[s_objtypes.ObjectType] = [\n                downcast(s_objtypes.ObjectType, ctx.env.set_types[arg.expr])\n                for arg in ir.args.values()\n            ]\n\n            lineages = [\n                (t,) + tuple(t.descendants(ctx.env.schema))\n                for t in types\n            ]\n            flattened = tuple(itertools.chain.from_iterable(lineages))\n            types_disjoint = len(flattened) == len(frozenset(flattened))\n        else:\n            types_disjoint = False\n\n        for m in mult:\n            if m.is_unique():\n                if (\n                    result.is_empty()\n                    or types_disjoint\n                    or (result.disjoint_union and m.disjoint_union)\n                ):\n                    result = m\n                else:\n                    result = DUPLICATE\n                    break\n            elif m.is_duplicate():\n                result = DUPLICATE\n                break\n            else:\n                # ZERO\n                pass\n\n        return result\n\n    elif op_name == 'std::EXCEPT':\n        # EXCEPT will produce multiplicity no greater than that of its first\n        # argument.\n        return mult[0]\n\n    elif op_name == 'std::INTERSECT':\n        # INTERSECT will produce the minimum multiplicity of its arguments.\n        return _min_multiplicity((mult[0], mult[1]))\n\n    elif op_name == 'std::DISTINCT':\n        if mult[0] == EMPTY:\n            return EMPTY\n        else:\n            return UNIQUE\n    elif op_name == 'std::IF':\n        # If the cardinality of the condition is more than ONE, then\n        # the multiplicity cannot be inferred.\n        if cards[1].is_single():\n            # Now it's just a matter of the multiplicity of the\n            # possible results.\n            return _max_multiplicity((mult[0], mult[2]))\n        else:\n            return DUPLICATE\n    elif op_name == 'std::??':\n        return _max_multiplicity((mult[0], mult[1]))\n    elif card.is_single():\n        return UNIQUE\n    elif op_name in ('std::++', 'std::+'):\n        # Operators known to be injective.\n        # Basically just done to avoid breaking backward compatability\n        # more than was necessary, because we used to *always* use this\n        # path, which was wrong.\n        result = _max_multiplicity(mult)\n        if result.is_duplicate():\n            return result\n\n        # Even when arguments are of multiplicity UNIQUE, we cannot\n        # exclude the possibility of the result being of multiplicity\n        # DUPLICATE. We need to check that at most one argument has\n        # cardinality more than ONE.\n\n        if len([card for card in cards if card.is_multi()]) > 1:\n            return DUPLICATE\n        else:\n            return result\n    else:\n        # Everything else.\n        return DUPLICATE\n\n\n@_infer_multiplicity.register\ndef __infer_const(\n    ir: irast.BaseConstant,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    return UNIQUE\n\n\n@_infer_multiplicity.register\ndef __infer_param(\n    ir: irast.QueryParameter,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    return UNIQUE\n\n\n@_infer_multiplicity.register\ndef __infer_function_param(\n    ir: irast.FunctionParameter,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    return UNIQUE\n\n\n@_infer_multiplicity.register\ndef __infer_inlined_param(\n    ir: irast.InlinedParameterExpr,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    return UNIQUE\n\n\n@_infer_multiplicity.register\ndef __infer_const_set(\n    ir: irast.ConstantSet,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    # Is it worth doing this? It won't trigger in the common case of having\n    # performed constant extraction.\n    els = set()\n    for el in ir.elements:\n        if isinstance(el, irast.BaseConstant):\n            els.add(el.value)\n        else:\n            return DUPLICATE\n\n    if len(ir.elements) == len(els):\n        return UNIQUE\n    else:\n        return DUPLICATE\n\n\n@_infer_multiplicity.register\ndef __infer_typecheckop(\n    ir: irast.TypeCheckOp,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    # Unless this is a singleton, multiplicity cannot be assumed to be UNIQUE\n    card = cardinality.infer_cardinality(\n        ir, scope_tree=scope_tree, ctx=ctx)\n\n    infer_multiplicity(ir.left, scope_tree=scope_tree, ctx=ctx)\n\n    if card is not None and card.is_single():\n        return UNIQUE\n    else:\n        return DUPLICATE\n\n\n@_infer_multiplicity.register\ndef __infer_typecast(\n    ir: irast.TypeCast,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    return infer_multiplicity(\n        ir.expr, scope_tree=scope_tree, ctx=ctx,\n    )\n\n\ndef _infer_stmt_multiplicity(\n    ir: irast.FilteredStmt,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    # WITH block bindings need to be validated; they don't have to\n    # have multiplicity UNIQUE, but their sub-expressions must be valid.\n    for part, _ in (ir.bindings or []):\n        infer_multiplicity(part, scope_tree=scope_tree, ctx=ctx)\n\n    subj = ir.subject if isinstance(ir, irast.MutatingStmt) else ir.result\n    result = infer_multiplicity(\n        subj,\n        scope_tree=scope_tree,\n        ctx=ctx,\n    )\n\n    if ir.where:\n        infer_multiplicity(ir.where, scope_tree=scope_tree, ctx=ctx)\n        filtered_ptrs = cardinality.extract_filters(\n            subj, ir.where, scope_tree, ctx)\n        for _, flt_expr in filtered_ptrs:\n            # Check if any of the singleton filter expressions in FILTER\n            # reference enclosing iterators with multiplicity UNIQUE, and\n            # if so, indicate to the enclosing iterator that this UNION\n            # is guaranteed to be disjoint.\n            if (\n                irutils.get_path_root(flt_expr).path_id\n                == ctx.distinct_iterator\n                or irutils.get_path_root(irutils.unwrap_set(flt_expr)).path_id\n                == ctx.distinct_iterator\n            ) and not infer_multiplicity(\n                flt_expr, scope_tree=scope_tree, ctx=ctx\n            ).is_duplicate():\n                return DISTINCT_UNION\n\n    return result\n\n\ndef _infer_for_multiplicity(\n    ir: irast.SelectStmt,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    itset = ir.iterator_stmt\n    assert itset is not None\n    itexpr = itset.expr\n    assert itexpr is not None\n    itmult = infer_multiplicity(itset, scope_tree=scope_tree, ctx=ctx)\n\n    if itmult != DUPLICATE:\n        ctx = ctx._replace(distinct_iterator=itset.path_id)\n    result_mult = infer_multiplicity(ir.result, scope_tree=scope_tree, ctx=ctx)\n\n    if isinstance(ir.result.expr, irast.InsertStmt):\n        # A union of inserts always has multiplicity UNIQUE\n        return UNIQUE\n    elif itmult.is_duplicate():\n        return DUPLICATE\n    else:\n        if result_mult.disjoint_union:\n            # If we know the union was disjoint wrt this FOR, then our\n            # set is unique (or empty maybe), but we have to clear\n            # disjoint_union since we it was only with respect to this\n            # FOR, so we can't have it leak.\n            return dataclasses.replace(result_mult, disjoint_union=False)\n        else:\n            return DUPLICATE\n\n\n@_infer_multiplicity.register\ndef __infer_select_stmt(\n    ir: irast.SelectStmt,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n\n    if ir.iterator_stmt is not None:\n        stmt_mult = _infer_for_multiplicity(ir, scope_tree=scope_tree, ctx=ctx)\n    else:\n        stmt_mult = _infer_stmt_multiplicity(\n            ir, scope_tree=scope_tree, ctx=ctx)\n\n        clauses = (\n            [ir.limit, ir.offset]\n            + [sort.expr for sort in (ir.orderby or ())]\n        )\n\n        for clause in filter(None, clauses):\n            new_scope = inf_utils.get_set_scope(clause, scope_tree, ctx=ctx)\n            infer_multiplicity(clause, scope_tree=new_scope, ctx=ctx)\n\n    if ir.card_inference_override:\n        stmt_mult = infer_multiplicity(\n            ir.card_inference_override, scope_tree=scope_tree, ctx=ctx)\n\n    return stmt_mult\n\n\n@_infer_multiplicity.register\ndef __infer_insert_stmt(\n    ir: irast.InsertStmt,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    # WITH block bindings need to be validated, they don't have to\n    # have multiplicity UNIQUE, but their sub-expressions must be valid.\n    for part, _ in (ir.bindings or []):\n        infer_multiplicity(part, scope_tree=scope_tree, ctx=ctx)\n\n    # INSERT will always return a proper set, but we still want to\n    # process the sub-expressions.\n    infer_multiplicity(\n        ir.subject, scope_tree=scope_tree, ctx=ctx\n    )\n    new_scope = inf_utils.get_set_scope(ir.result, scope_tree, ctx=ctx)\n    infer_multiplicity(\n        ir.result, scope_tree=new_scope, ctx=ctx\n    )\n\n    if ir.on_conflict:\n        _infer_on_conflict_clause(\n            ir.on_conflict, scope_tree=scope_tree, ctx=ctx\n        )\n\n    _infer_mutating_stmt(ir, scope_tree=scope_tree, ctx=ctx)\n\n    return DISTINCT_UNION\n\n\n@_infer_multiplicity.register\ndef __infer_update_stmt(\n    ir: irast.UpdateStmt,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    # Presumably UPDATE will always return a proper set, even if it's\n    # fed something with higher multiplicity, but we still want to\n    # process the expression being updated.\n    infer_multiplicity(\n        ir.result, scope_tree=scope_tree, ctx=ctx,\n    )\n    result = _infer_stmt_multiplicity(ir, scope_tree=scope_tree, ctx=ctx)\n\n    _infer_mutating_stmt(ir, scope_tree=scope_tree, ctx=ctx)\n\n    if result is EMPTY:\n        return EMPTY\n    else:\n        return UNIQUE\n\n\n@_infer_multiplicity.register\ndef __infer_delete_stmt(\n    ir: irast.DeleteStmt,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    # Presumably DELETE will always return a proper set, even if it's\n    # fed something with higher multiplicity, but we still want to\n    # process the expression being deleted.\n    infer_multiplicity(\n        ir.result, scope_tree=scope_tree, ctx=ctx,\n    )\n    result = _infer_stmt_multiplicity(ir, scope_tree=scope_tree, ctx=ctx)\n\n    _infer_mutating_stmt(ir, scope_tree=scope_tree, ctx=ctx)\n\n    if result is EMPTY:\n        return EMPTY\n    else:\n        return UNIQUE\n\n\ndef _infer_mutating_stmt(\n    ir: irast.MutatingStmt,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> None:\n    if ir.conflict_checks:\n        for clause in ir.conflict_checks:\n            _infer_on_conflict_clause(clause, scope_tree=scope_tree, ctx=ctx)\n\n    for write_pol in ir.write_policies.values():\n        for policy in write_pol.policies:\n            infer_multiplicity(policy.expr, scope_tree=scope_tree, ctx=ctx)\n\n    for read_pol in ir.read_policies.values():\n        infer_multiplicity(read_pol.expr, scope_tree=scope_tree, ctx=ctx)\n\n    if ir.rewrites:\n        for rewrites in ir.rewrites.by_type.values():\n            for rewrite, _ in rewrites.values():\n                infer_multiplicity(\n                    rewrite,\n                    scope_tree=scope_tree,\n                    ctx=ctx,\n                )\n\n\ndef _infer_on_conflict_clause(\n    ir: irast.OnConflictClause,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> None:\n    for part in [ir.select_ir, ir.else_ir]:\n        if part:\n            infer_multiplicity(part, scope_tree=scope_tree, ctx=ctx)\n\n\n@_infer_multiplicity.register\ndef __infer_group_stmt(\n    ir: irast.GroupStmt,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    infer_multiplicity(ir.subject, scope_tree=scope_tree, ctx=ctx)\n    for binding, _ in ir.using.values():\n        infer_multiplicity(binding, scope_tree=scope_tree, ctx=ctx)\n    _infer_stmt_multiplicity(ir, scope_tree=scope_tree, ctx=ctx)\n\n    for clause in (ir.orderby or ()):\n        new_scope = inf_utils.get_set_scope(clause.expr, scope_tree, ctx=ctx)\n        infer_multiplicity(clause.expr, scope_tree=new_scope, ctx=ctx)\n\n    infer_multiplicity(ir.group_binding, scope_tree=scope_tree, ctx=ctx)\n    if ir.grouping_binding:\n        infer_multiplicity(ir.grouping_binding, scope_tree=scope_tree, ctx=ctx)\n\n    for set in ir.group_aggregate_sets:\n        if set:\n            infer_multiplicity(set, scope_tree=scope_tree, ctx=ctx)\n\n    # N.B: The type is usually a free object (except in some\n    # internal tests), which are always unique\n    if irtyputils.is_free_object(ir.typeref):\n        return UNIQUE\n\n    return DUPLICATE\n\n\n@_infer_multiplicity.register\ndef __infer_slice(\n    ir: irast.SliceIndirection,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    # Slice indirection multiplicity is guaranteed to be UNIQUE as long\n    # as the cardinality of this expression is at most one, otherwise\n    # the results of index indirection can contain values with\n    # multiplicity > 1.\n\n    infer_multiplicity(ir.expr, scope_tree=scope_tree, ctx=ctx)\n    if ir.start:\n        infer_multiplicity(ir.start, scope_tree=scope_tree, ctx=ctx)\n    if ir.stop:\n        infer_multiplicity(ir.stop, scope_tree=scope_tree, ctx=ctx)\n\n    card = cardinality.infer_cardinality(\n        ir, scope_tree=scope_tree, ctx=ctx)\n    if card is not None and card.is_single():\n        return UNIQUE\n    else:\n        return DUPLICATE\n\n\n@_infer_multiplicity.register\ndef __infer_index(\n    ir: irast.IndexIndirection,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    # Index indirection multiplicity is guaranteed to be UNIQUE as long\n    # as the cardinality of this expression is at most one, otherwise\n    # the results of index indirection can contain values with\n    # multiplicity > 1.\n    card = cardinality.infer_cardinality(\n        ir, scope_tree=scope_tree, ctx=ctx)\n\n    infer_multiplicity(ir.expr, scope_tree=scope_tree, ctx=ctx)\n    infer_multiplicity(ir.index, scope_tree=scope_tree, ctx=ctx)\n\n    if card is not None and card.is_single():\n        return UNIQUE\n    else:\n        return DUPLICATE\n\n\n@_infer_multiplicity.register\ndef __infer_array(\n    ir: irast.Array,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    return _common_multiplicity(ir.elements, scope_tree=scope_tree, ctx=ctx)\n\n\n@_infer_multiplicity.register\ndef __infer_tuple(\n    ir: irast.Tuple,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    els = tuple(\n        infer_multiplicity(el.val, scope_tree=scope_tree, ctx=ctx)\n        for el in ir.elements\n    )\n    cards = [\n        cardinality.infer_cardinality(el.val, scope_tree=scope_tree, ctx=ctx)\n        for el in ir.elements\n    ]\n\n    num_many = sum(card.is_multi() for card in cards)\n    new_els = []\n    for el, card in zip(els, cards):\n        # If more than one tuple element is many, everything has DUPLICATE\n        # multiplicity.\n        if num_many > 1:\n            el = DUPLICATE\n        # If exactly one tuple element is many, then *that* element\n        # can keep its underlying multiplicity, while everything else\n        # becomes DUPLICATE.\n        elif num_many == 1 and card.is_single():\n            el = DUPLICATE\n        new_els.append(el)\n\n    return ContainerMultiplicityInfo(\n        own=_max_multiplicity(els).own,\n        elements=tuple(new_els),\n    )\n\n\n@_infer_multiplicity.register\ndef __infer_trigger_anchor(\n    ir: irast.TriggerAnchor,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    return UNIQUE\n\n\n@_infer_multiplicity.register\ndef __infer_searchable_string(\n    ir: irast.FTSDocument,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    return _common_multiplicity(\n        (ir.text, ir.language), scope_tree=scope_tree, ctx=ctx\n    )\n\n\ndef infer_multiplicity(\n    ir: irast.Base,\n    *,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> inf_ctx.MultiplicityInfo:\n    assert ctx.make_updates, (\n        \"multiplicity inference hasn't implemented make_updates=False yet\")\n\n    result = ctx.inferred_multiplicity.get(\n        (ir, scope_tree, ctx.distinct_iterator))\n    if result is not None:\n        return result\n\n    # We can use cardinality as a helper in determining multiplicity,\n    # since singletons have multiplicity one.\n    card = cardinality.infer_cardinality(ir, scope_tree=scope_tree, ctx=ctx)\n\n    if isinstance(ir, irast.Set):\n        result = _infer_set(ir, scope_tree=scope_tree, ctx=ctx)\n    else:\n        result = _infer_multiplicity(ir, scope_tree=scope_tree, ctx=ctx)\n\n    if card is not None and card.is_single() and result.is_duplicate():\n        # We've validated multiplicity, so now we can just override it\n        # safely.\n        result = UNIQUE\n\n    if not isinstance(result, inf_ctx.MultiplicityInfo):\n        raise errors.QueryError(\n            'could not determine the multiplicity of '\n            'set produced by expression',\n            span=ir.span)\n\n    ctx.inferred_multiplicity[ir, scope_tree, ctx.distinct_iterator] = result\n\n    return result\n"
  },
  {
    "path": "edb/edgeql/compiler/inference/utils.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Common utilities used in inferers.\"\"\"\n\n\nfrom __future__ import annotations\nfrom typing import Optional\n\nfrom edb import errors\nfrom edb.ir import ast as irast\n\nfrom . import context as inf_ctx\n\n\ndef get_set_scope(\n    ir_set: irast.Set,\n    scope_tree: irast.ScopeTreeNode,\n    ctx: inf_ctx.InfCtx,\n) -> irast.ScopeTreeNode:\n\n    if ir_set.path_scope_id:\n        new_scope = ctx.env.scope_tree_nodes.get(ir_set.path_scope_id)\n        if new_scope is None:\n            raise errors.InternalServerError(\n                f'dangling scope pointer to node with uid'\n                f':{ir_set.path_scope_id} in {ir_set!r}'\n            )\n    else:\n        new_scope = scope_tree\n\n    return new_scope\n\n\ndef find_visible(\n    ir: irast.Set,\n    scope_tree: irast.ScopeTreeNode,\n) -> Optional[irast.ScopeTreeNode]:\n    # We want to look one fence up from whatever our current fence is.\n    # (Most of the time, scope_tree will be a fence, so this is equivalent\n    # to parent_fence, but sometimes it will be a branch.)\n    outer_fence = scope_tree.fence.parent_fence\n    if outer_fence is not None:\n        if scope_tree.namespaces:\n            path_id = ir.path_id.strip_namespace(scope_tree.namespaces)\n        else:\n            path_id = ir.path_id\n\n        return outer_fence.find_visible(path_id)\n    else:\n        return None\n"
  },
  {
    "path": "edb/edgeql/compiler/inference/volatility.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Iterable\n\nimport functools\n\nfrom edb import errors\n\nfrom edb.edgeql import qltypes\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtyputils\n\nfrom .. import context\n\n\nInferredVolatility = context.InferredVolatility\n\n\nIMMUTABLE = qltypes.Volatility.Immutable\nSTABLE = qltypes.Volatility.Stable\nVOLATILE = qltypes.Volatility.Volatile\nMODIFYING = qltypes.Volatility.Modifying\n\n\n# Volatility inference computes two volatility results:\n# A basic one, and one for consumption by materialization.\n#\n# The one for consumption by materialization differs in that it\n# (counterintuitively) does not consider DML to be volatile/modifying\n# (since DML has its own \"materialization\" mechanism).\n#\n# We represent this output as a pair, but for ergonomics, inference\n# functions are allowed to still produce a single volatility value,\n# which is normalized when necessary.\n\n\ndef _normalize_volatility(\n    vol: InferredVolatility,\n) -> tuple[qltypes.Volatility, qltypes.Volatility]:\n    if not isinstance(vol, tuple):\n        return (vol, vol)\n    else:\n        return vol\n\n\ndef _max_volatility(args: Iterable[InferredVolatility]) -> InferredVolatility:\n    arg_list = list(args)\n    if not arg_list:\n        return IMMUTABLE\n    else:\n        nargs = [_normalize_volatility(x) for x in arg_list]\n        return (\n            max(x[0] for x in nargs),\n            max(x[1] for x in nargs),\n        )\n\n\ndef _common_volatility(\n    args: Iterable[irast.Base],\n    env: context.Environment,\n) -> InferredVolatility:\n    return _max_volatility(\n        _infer_volatility(a, env) for a in args)\n\n\n@functools.singledispatch\ndef _infer_volatility_inner(\n    ir: irast.Base,\n    env: context.Environment,\n) -> InferredVolatility:\n    raise ValueError(f'infer_volatility: cannot handle {ir!r}')\n\n\n@_infer_volatility_inner.register(type(None))\ndef __infer_none(\n    ir: None,\n    env: context.Environment,\n) -> InferredVolatility:\n    # Here for debugging purposes.\n    raise ValueError('invalid infer_volatility(None, schema) call')\n\n\n@_infer_volatility_inner.register\ndef __infer_statement(\n    ir: irast.Statement,\n    env: context.Environment,\n) -> InferredVolatility:\n    return _infer_volatility(ir.expr, env)\n\n\n@_infer_volatility_inner.register\ndef __infer_config_command(\n    ir: irast.ConfigCommand,\n    env: context.Environment,\n) -> InferredVolatility:\n    return VOLATILE\n\n\n@_infer_volatility_inner.register\ndef __infer_emptyset(\n    ir: irast.EmptySet,\n    env: context.Environment,\n) -> InferredVolatility:\n    return IMMUTABLE\n\n\n@_infer_volatility_inner.register\ndef __infer_typeref(\n    ir: irast.TypeRef,\n    env: context.Environment,\n) -> InferredVolatility:\n    return IMMUTABLE\n\n\n@_infer_volatility_inner.register\ndef __infer_type_introspection(\n    ir: irast.TypeIntrospection,\n    env: context.Environment,\n) -> InferredVolatility:\n    return IMMUTABLE\n\n\n@_infer_volatility_inner.register\ndef __infer_type_root(\n    ir: irast.TypeRoot,\n    env: context.Environment,\n) -> InferredVolatility:\n    return STABLE\n\n\n@_infer_volatility_inner.register\ndef __infer_cleared_expr(\n    ir: irast.RefExpr,\n    env: context.Environment,\n) -> InferredVolatility:\n    return IMMUTABLE\n\n\n@_infer_volatility_inner.register\ndef _infer_pointer(\n    ir: irast.Pointer,\n    env: context.Environment,\n) -> InferredVolatility:\n    vol = _infer_volatility(ir.source, env)\n    # If there's an expression on an rptr, and it comes from\n    # the schema, we need to actually infer it, since it won't\n    # have been processed at a shape declaration.\n    if ir.expr is not None and not ir.ptrref.defined_here:\n        vol = _max_volatility((\n            vol,\n            _infer_volatility(ir.expr, env),\n        ))\n\n    # If source is an object, then a pointer reference implies\n    # a table scan, and so we can assume STABLE at the minimum.\n    #\n    # A single dereference of a singleton path can be IMMUTABLE,\n    # though, which we need in order to enforce that indexes\n    # don't call STABLE functions.\n    if (\n        irtyputils.is_object(ir.source.typeref)\n        and ir.source.path_id not in env.singletons\n    ):\n        vol = _max_volatility((vol, STABLE))\n\n    return vol\n\n\n@_infer_volatility_inner.register\ndef __infer_set(\n    ir: irast.Set,\n    env: context.Environment,\n) -> InferredVolatility:\n    vol: InferredVolatility\n\n    if ir.path_id in env.singletons:\n        vol = IMMUTABLE\n    else:\n        vol = _infer_volatility(ir.expr, env)\n\n    # Cache our best-known as to this point volatility, to prevent\n    # infinite recursion.\n    env.inferred_volatility[ir] = vol\n\n    if ir.shape:\n        vol = _max_volatility([\n            _common_volatility(\n                (el.expr.expr for el, _ in ir.shape if el.expr.expr), env\n            ),\n            vol,\n        ])\n\n    if ir.is_binding and ir.is_binding != irast.BindingKind.Schema:\n        vol = IMMUTABLE\n\n    return vol\n\n\n@_infer_volatility_inner.register\ndef __infer_func_call(\n    ir: irast.FunctionCall,\n    env: context.Environment,\n) -> InferredVolatility:\n    func_volatility = (\n        _infer_volatility(ir.body, env) if ir.body else ir.volatility\n    )\n\n    if ir.args:\n        return _max_volatility([\n            _common_volatility((arg.expr for arg in ir.args.values()), env),\n            func_volatility\n        ])\n    else:\n        return func_volatility\n\n\n@_infer_volatility_inner.register\ndef __infer_oper_call(\n    ir: irast.OperatorCall,\n    env: context.Environment,\n) -> InferredVolatility:\n    if ir.args:\n        return _max_volatility([\n            _common_volatility((arg.expr for arg in ir.args.values()), env),\n            ir.volatility\n        ])\n    else:\n        return ir.volatility\n\n\n@_infer_volatility_inner.register\ndef __infer_const(\n    ir: irast.BaseConstant,\n    env: context.Environment,\n) -> InferredVolatility:\n    return IMMUTABLE\n\n\n@_infer_volatility_inner.register\ndef __infer_param(\n    ir: irast.QueryParameter,\n    env: context.Environment,\n) -> InferredVolatility:\n    return STABLE if ir.is_global else IMMUTABLE\n\n\n@_infer_volatility_inner.register\ndef __infer_function_param(\n    ir: irast.FunctionParameter,\n    env: context.Environment,\n) -> InferredVolatility:\n    return STABLE if ir.is_global else IMMUTABLE\n\n\n@_infer_volatility_inner.register\ndef __infer_inlined_param(\n    ir: irast.InlinedParameterExpr,\n    env: context.Environment,\n) -> InferredVolatility:\n    return STABLE if ir.is_global else IMMUTABLE\n\n\n@_infer_volatility_inner.register\ndef __infer_const_set(\n    ir: irast.ConstantSet,\n    env: context.Environment,\n) -> InferredVolatility:\n    return IMMUTABLE\n\n\n@_infer_volatility_inner.register\ndef __infer_typecheckop(\n    ir: irast.TypeCheckOp,\n    env: context.Environment,\n) -> InferredVolatility:\n    return _infer_volatility(ir.left, env)\n\n\n@_infer_volatility_inner.register\ndef __infer_typecast(\n    ir: irast.TypeCast,\n    env: context.Environment,\n) -> InferredVolatility:\n    return _infer_volatility(ir.expr, env)\n\n\n@_infer_volatility_inner.register\ndef __infer_select_stmt(\n    ir: irast.SelectStmt,\n    env: context.Environment,\n) -> InferredVolatility:\n    components = []\n\n    if ir.iterator_stmt is not None:\n        components.append(ir.iterator_stmt)\n\n    components.append(ir.result)\n\n    if ir.where is not None:\n        components.append(ir.where)\n\n    if ir.orderby:\n        components.extend(o.expr for o in ir.orderby)\n\n    if ir.offset is not None:\n        components.append(ir.offset)\n\n    if ir.limit is not None:\n        components.append(ir.limit)\n\n    if ir.bindings is not None:\n        components.extend(part for part, _ in ir.bindings)\n\n    return _common_volatility(components, env)\n\n\n@_infer_volatility_inner.register\ndef __infer_group_stmt(\n    ir: irast.GroupStmt,\n    env: context.Environment,\n) -> InferredVolatility:\n    components = [ir.subject, ir.result] + [v for v, _ in ir.using.values()]\n    return _common_volatility(components, env)\n\n\n@_infer_volatility_inner.register\ndef __infer_trigger_anchor(\n    ir: irast.TriggerAnchor,\n    env: context.Environment,\n) -> InferredVolatility:\n    return STABLE, STABLE\n\n\n@_infer_volatility_inner.register\ndef __infer_searchable_string(\n    ir: irast.FTSDocument,\n    env: context.Environment,\n) -> InferredVolatility:\n    return _common_volatility([ir.text, ir.language], env)\n\n\n@_infer_volatility_inner.register\ndef __infer_dml_stmt(\n    ir: irast.MutatingStmt,\n    env: context.Environment,\n) -> InferredVolatility:\n    # For materialization purposes, DML is not volatile.  (Since it\n    # has its *own* elaborate mechanism using top-level CTEs).\n    return MODIFYING, STABLE\n\n\n@_infer_volatility_inner.register\ndef __infer_slice(\n    ir: irast.SliceIndirection,\n    env: context.Environment,\n) -> InferredVolatility:\n    # slice indirection volatility depends on the volatility of\n    # the base expression and the slice index expressions\n    args: list[irast.Base] = [ir.expr]\n    if ir.start is not None:\n        args.append(ir.start)\n    if ir.stop is not None:\n        args.append(ir.stop)\n\n    return _common_volatility(args, env)\n\n\n@_infer_volatility_inner.register\ndef __infer_index(\n    ir: irast.IndexIndirection,\n    env: context.Environment,\n) -> InferredVolatility:\n    # index indirection volatility depends on both the volatility of\n    # the base expression and the index expression\n    return _common_volatility([ir.expr, ir.index], env)\n\n\n@_infer_volatility_inner.register\ndef __infer_array(\n    ir: irast.Array,\n    env: context.Environment,\n) -> InferredVolatility:\n    return _common_volatility(ir.elements, env)\n\n\n@_infer_volatility_inner.register\ndef __infer_tuple(\n    ir: irast.Tuple,\n    env: context.Environment,\n) -> InferredVolatility:\n    return _common_volatility(\n        [el.val for el in ir.elements], env)\n\n\ndef _infer_volatility(\n    ir: irast.Base,\n    env: context.Environment,\n) -> InferredVolatility:\n    result = env.inferred_volatility.get(ir)\n    if result is not None:\n        return result\n\n    result = _infer_volatility_inner(ir, env)\n\n    env.inferred_volatility[ir] = result\n\n    return result\n\n\ndef infer_volatility(\n    ir: irast.Base,\n    env: context.Environment,\n    *,\n    exclude_dml: bool=False,\n) -> qltypes.Volatility:\n    result = _normalize_volatility(_infer_volatility(ir, env))[exclude_dml]\n\n    if result not in {VOLATILE, STABLE, IMMUTABLE, MODIFYING}:\n        raise errors.QueryError(\n            'could not determine the volatility of '\n            'set produced by expression',\n            span=ir.span)\n\n    return result\n"
  },
  {
    "path": "edb/edgeql/compiler/normalization.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either nodeess or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL expression normalization functions.\"\"\"\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    AbstractSet,\n    Mapping,\n    Collection,\n    cast,\n)\n\nimport functools\n\nfrom edb.common.ast import base\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import parser as qlparser\n\nfrom edb.schema import name as sn\nfrom edb.schema import schema as s_schema\nfrom edb.schema import functions as s_func\nfrom edb.schema import utils as s_utils\n\n\n@functools.singledispatch\ndef normalize(\n    node: Any,\n    *,\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    localnames: AbstractSet[str] = frozenset(),\n) -> None:\n    raise AssertionError(f'normalize: cannot handle {node!r}')\n\n\ndef renormalize_compat(\n    norm_qltree: qlast.Base_T,\n    orig_text: str,\n    *,\n    schema: s_schema.Schema,\n    localnames: AbstractSet[str] = frozenset(),\n) -> qlast.Base_T:\n    \"\"\"Renormalize an expression normalized with imprint_expr_context().\n\n    This helper takes the original, unmangled expression, an EdgeQL AST\n    tree of the same expression mangled with `imprint_expr_context()`\n    (which injects extra WITH MODULE clauses), and produces a normalized\n    expression with explicitly qualified identifiers instead.  Old dumps\n    are the main user of this facility.\n    \"\"\"\n    orig_qltree = qlparser.parse_fragment(orig_text)\n\n    norm_aliases: dict[Optional[str], str] = {}\n    assert isinstance(norm_qltree, (\n        qlast.Query, qlast.Command, qlast.DDLCommand\n    ))\n    for alias in (norm_qltree.aliases or ()):\n        if isinstance(alias, qlast.ModuleAliasDecl):\n            norm_aliases[alias.alias] = alias.module\n\n    if isinstance(orig_qltree, (\n        qlast.Query, qlast.Command, qlast.DDLCommand\n    )):\n        orig_aliases: dict[Optional[str], str] = {}\n        for alias in (orig_qltree.aliases or ()):\n            if isinstance(alias, qlast.ModuleAliasDecl):\n                orig_aliases[alias.alias] = alias.module\n\n        modaliases = {\n            k: v\n            for k, v in norm_aliases.items()\n            if k not in orig_aliases\n        }\n    else:\n        modaliases = norm_aliases\n\n    normalize(\n        orig_qltree,\n        schema=schema,\n        modaliases=modaliases,\n        localnames=localnames,\n    )\n\n    assert isinstance(orig_qltree, type(norm_qltree))\n    return cast(qlast.Base_T, orig_qltree)\n\n\ndef _normalize_recursively(\n    node: qlast.Base,\n    value: Any,\n    *,\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    localnames: AbstractSet[str] = frozenset(),\n) -> None:\n    # We only want to handle fields that need to be traversed\n    # recursively: Base AST and lists. Other fields are essentially\n    # expected to be processed by the more specific handlers.\n    if isinstance(value, qlast.Base):\n        normalize(\n            value,\n            schema=schema,\n            modaliases=modaliases,\n            localnames=localnames,\n        )\n    elif isinstance(value, (tuple, list)):\n        if value and isinstance(value[0], qlast.Base):\n            for el in value:\n                normalize(\n                    el,\n                    schema=schema,\n                    modaliases=modaliases,\n                    localnames=localnames,\n                )\n\n\n@normalize.register\ndef normalize_generic(\n    node: qlast.Base,\n    *,\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    localnames: AbstractSet[str] = frozenset(),\n    skip: Collection[str] = frozenset(),\n) -> None:\n    for field, value in base.iter_fields(node):\n        if field not in skip:\n            _normalize_recursively(\n                node,\n                value,\n                schema=schema,\n                modaliases=modaliases,\n                localnames=localnames,\n            )\n\n\n# This is the heart of the whole thing.\n@normalize.register\ndef normalize_ObjectRef(\n    ref: qlast.ObjectRef,\n    *,\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    localnames: AbstractSet[str] = frozenset(),\n) -> None:\n    if ref.name not in localnames:\n        obj = schema.get(\n            s_utils.ast_ref_to_name(ref),\n            default=None,\n            module_aliases=modaliases,\n        )\n        if obj is not None:\n            name = obj.get_name(schema)\n            assert isinstance(name, sn.QualName)\n            ref.module = name.module\n        elif ref.module in modaliases:\n            # Even if the name was not resolved in the\n            # schema it may be the name of the object\n            # being defined, as such the default module\n            # should be used. Names that must be ignored\n            # (like aliases and parameters) have already\n            # been filtered by the localnames.\n            ref.module = modaliases[ref.module]\n\n\ndef _normalize_with_block(\n    node: qlast.Query,\n    *,\n    field: str='aliases',\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    localnames: AbstractSet[str] = frozenset(),\n) -> tuple[Mapping[Optional[str], str], AbstractSet[str]]:\n\n    # Update the default aliases, modaliases, and localnames.\n    modaliases = dict(modaliases)\n    newaliases: list[qlast.AliasedExpr | qlast.ModuleAliasDecl] = []\n\n    aliases: Optional[list[qlast.AliasedExpr]] = getattr(node, field)\n    for alias in (aliases or ()):\n        if isinstance(alias, qlast.ModuleAliasDecl):\n            if alias.alias:\n                modaliases[alias.alias] = alias.module\n            else:\n                modaliases[None] = alias.module\n        else:\n            assert isinstance(alias, qlast.AliasedExpr)\n            normalize(\n                alias.expr,\n                schema=schema,\n                modaliases=modaliases,\n                localnames=localnames,\n            )\n            newaliases.append(alias)\n            localnames = {alias.alias} | localnames\n\n    setattr(node, field, newaliases)\n\n    return modaliases, localnames\n\n\ndef _normalize_aliased_field(\n    node: qlast.SubjectQuery | qlast.ReturningQuery,\n    fname: str,\n    *,\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    localnames: AbstractSet[str] = frozenset(),\n) -> AbstractSet[str]:\n\n    # Potentially the result defines an alias that is visible in other\n    # clauses\n    val = getattr(node, fname)\n    normalize(\n        val,\n        schema=schema,\n        modaliases=modaliases,\n        localnames=localnames,\n    )\n    alias = getattr(node, f'{fname}_alias', None)\n    if alias:\n        localnames = {alias} | localnames\n\n    return localnames\n\n\n@normalize.register\ndef normalize_SelectQuery(\n    node: qlast.SelectQuery,\n    *,\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    localnames: AbstractSet[str] = frozenset(),\n) -> None:\n\n    # Process WITH block\n    modaliases, localnames = _normalize_with_block(\n        node,\n        schema=schema,\n        modaliases=modaliases,\n        localnames=localnames,\n    )\n\n    # Process the result expression\n    localnames = _normalize_aliased_field(\n        node,\n        'result',\n        schema=schema,\n        modaliases=modaliases,\n        localnames=localnames,\n    )\n\n    normalize_generic(\n        node,\n        schema=schema,\n        modaliases=modaliases,\n        localnames=localnames,\n        skip=('aliases', 'result'),\n    )\n\n\n@normalize.register(qlast.InsertQuery)\n@normalize.register(qlast.UpdateQuery)\n@normalize.register(qlast.DeleteQuery)\ndef normalize_DML(\n    node: qlast.InsertQuery | qlast.UpdateQuery | qlast.DeleteQuery,\n    *,\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    localnames: AbstractSet[str] = frozenset(),\n) -> None:\n\n    # Process WITH block\n    modaliases, localnames = _normalize_with_block(\n        node,\n        schema=schema,\n        modaliases=modaliases,\n        localnames=localnames,\n    )\n\n    normalize_generic(\n        node,\n        schema=schema,\n        modaliases=modaliases,\n        localnames=localnames,\n        skip=('aliases',),\n    )\n\n\n@normalize.register\ndef normalize_ForQuery(\n    node: qlast.ForQuery,\n    *,\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    localnames: AbstractSet[str] = frozenset(),\n) -> None:\n\n    # Process WITH block\n    modaliases, localnames = _normalize_with_block(\n        node,\n        schema=schema,\n        modaliases=modaliases,\n        localnames=localnames,\n    )\n\n    # Process the iterator expression\n    localnames = _normalize_aliased_field(\n        node,\n        'iterator',\n        schema=schema,\n        modaliases=modaliases,\n        localnames=localnames,\n    )\n\n    # Process the rest\n    normalize_generic(\n        node,\n        schema=schema,\n        modaliases=modaliases,\n        localnames=localnames,\n        skip=('aliases', 'iterator'),\n    )\n\n\n@normalize.register\ndef normalize_GroupQuery(\n    node: qlast.GroupQuery,\n    *,\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    localnames: AbstractSet[str] = frozenset(),\n) -> None:\n    # Process WITH block\n    modaliases, localnames = _normalize_with_block(\n        node,\n        schema=schema,\n        modaliases=modaliases,\n        localnames=localnames,\n    )\n\n    # Process the result expression\n    localnames = _normalize_aliased_field(\n        node,\n        'subject',\n        schema=schema,\n        modaliases=modaliases,\n        localnames=localnames,\n    )\n\n    modaliases, localnames = _normalize_with_block(\n        node,\n        field='using',\n        schema=schema,\n        modaliases=modaliases,\n        localnames=localnames,\n    )\n\n    normalize_generic(\n        node,\n        schema=schema,\n        modaliases=modaliases,\n        localnames=localnames,\n        skip=('aliases', 'subject', 'using'),\n    )\n\n\n@normalize.register\ndef normalize_FunctionCall(\n    node: qlast.FunctionCall,\n    *,\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    localnames: AbstractSet[str] = frozenset(),\n) -> None:\n\n    if node.func not in localnames:\n        name = (\n            sn.UnqualName(node.func) if isinstance(node.func, str)\n            else sn.QualName(*node.func)\n        )\n        funcs = s_func.lookup_functions(\n            name, default=tuple(), module_aliases=modaliases, schema=schema,\n        )\n        if funcs:\n            # As long as we found some functions, they will be from\n            # the same module (the first valid resolved module for the\n            # function name will mask \"std\").\n            sname = funcs[0].get_shortname(schema)\n            node.func = (sname.module, sname.name)\n\n        elif modaliases and isinstance(name, sn.QualName):\n            # Even if no function was found, apply the modaliases.\n            # It is possible that a function without the modalias exists but\n            # we don't want to find that.\n            #\n            # Eg.\n            # module A {\n            #     function foo() -> int64 using (1);\n            # }\n            # module B {}\n            # module default {\n            #     alias query := (with A as module B select A::foo() );\n            # }\n            module = s_schema.apply_module_aliases(name.module, modaliases)\n            if module is not None:\n                node.func = (module, name.name)\n\n        # It's odd we don't find a function, but this will be picked up\n        # by the compiler with a more appropriate error message.\n\n    for arg in node.args:\n        normalize(\n            arg,\n            schema=schema,\n            modaliases=modaliases,\n            localnames=localnames,\n        )\n\n    for val in node.kwargs.values():\n        normalize(\n            val,\n            schema=schema,\n            modaliases=modaliases,\n            localnames=localnames,\n        )\n\n\n@normalize.register\ndef compile_TypeName(\n    node: qlast.TypeName,\n    *,\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    localnames: AbstractSet[str] = frozenset(),\n) -> None:\n\n    # Resolve the main type\n    if isinstance(node.maintype, qlast.ObjectRef):\n        # This is a specific path root, resolve it.\n        if (\n            # maintype names 'array', 'tuple', 'range', and 'multirange'\n            # specifically should also be ignored\n            node.maintype.name not in {\n                'array', 'tuple', 'range', 'multirange',\n            }\n        ):\n            normalize(\n                node.maintype,\n                schema=schema,\n                modaliases=modaliases,\n                localnames=localnames,\n            )\n\n    normalize_generic(\n        node,\n        schema=schema,\n        modaliases=modaliases,\n        localnames=localnames,\n        skip=('maintype',),\n    )\n"
  },
  {
    "path": "edb/edgeql/compiler/options.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL compiler options.\"\"\"\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, Mapping, Collection, TYPE_CHECKING\n\nfrom dataclasses import dataclass, field as dc_field\n\nif TYPE_CHECKING:\n    from edb.schema import functions as s_func\n    from edb.schema import objects as s_obj\n    from edb.schema import name as s_name\n    from edb.schema import types as s_types\n    from edb.schema import pointers as s_pointers\n    from edb.ir import pathid\n    from edb.edgeql import qltypes\n\n    SourceOrPathId = s_types.Type | s_pointers.Pointer | pathid.PathId\n\n\n@dataclass(kw_only=True)\nclass GlobalCompilerOptions:\n    \"\"\"Compiler toggles that affect compilation as a whole.\"\"\"\n\n    #: Whether to allow the expression to be of a generic type.\n    allow_generic_type_output: bool = False\n\n    #: Whether to apply various query rewrites, including access policy.\n    apply_query_rewrites: bool = True\n\n    #: Whether to apply user-specified access policies\n    apply_user_access_policies: bool = True\n\n    #: Whether to allow specifying 'id' explicitly in INSERT\n    allow_user_specified_id: bool = False\n\n    #: Force types of all parameters to std::json\n    json_parameters: bool = False\n\n    #: Use material types for pointer targets in schema views.\n    schema_view_mode: bool = False\n\n    #: True in compile_bootstrap_script().\n    bootstrap_mode: bool = False\n\n    #: Whether to track which subexpressions reference each schema object.\n    track_schema_ref_exprs: bool = False\n\n    #: If the expression is being processed in the context of a certain\n    #: schema object, i.e. a constraint expression, or a pointer default,\n    #: this contains the type of the schema object.\n    schema_object_context: Optional[type[s_obj.Object]] = None\n\n    #: When compiling a function body, the function name.\n    func_name: Optional[s_name.QualName] = None\n\n    #: When compiling a function body, specifies function parameter\n    #: definitions.\n    func_params: Optional[s_func.ParameterLikeList] = None\n\n    #: Should the backend compiler expand inheritance CTEs in place.\n    #: This is needed by EXPLAIN to maintain alias names in\n    #: the query plan.\n    is_explain: bool = False\n\n    #: The name that can be used in a \"DML is disallowed in ...\"\n    #: error. When this is not None, any DML should cause an error.\n    in_ddl_context_name: Optional[str] = None\n\n    #: Whether to just treat all globals as empty instead of compiling them.\n    #: This is used when populating something using `SET default` in DDL.\n    make_globals_empty: bool = False\n\n    #: Is the compiler running in testmode\n    testmode: bool = False\n\n    # Is the compiler running in the server's schema reflection mode\n    schema_reflection_mode: bool = False\n\n    # are we invoking the compiler from inside a CONFIGURE?\n    in_server_config_op: bool = False\n\n    # This this restoring a dump?\n    dump_restore_mode: bool = False\n\n\n@dataclass(kw_only=True)\nclass CompilerOptions(GlobalCompilerOptions):\n\n    #: Module name aliases.\n    modaliases: Mapping[Optional[str], str] = dc_field(default_factory=dict)\n\n    #: External symbol table.\n    anchors: Mapping[str, Any] = dc_field(default_factory=dict)\n\n    #: The symbol to assume as the prefix for abbreviated paths.\n    path_prefix_anchor: Optional[str] = None\n\n    #: Module to put derived schema objects to.\n    derived_target_module: Optional[str] = None\n\n    #: The name to use for the top-level type variant.\n    result_view_name: Optional[s_name.QualName] = None\n\n    #: If > 0, Inject implicit LIMIT to every SELECT query.\n    implicit_limit: int = 0\n\n    #: Include id property in every shape implicitly.\n    implicit_id_in_shapes: bool = False\n\n    #: Include __tid__ computable (.__type__.id) in every shape implicitly.\n    implicit_tid_in_shapes: bool = False\n\n    #: Include __tname__ computable (.__type__.name) in every shape implicitly.\n    implicit_tname_in_shapes: bool = False\n\n    #: A set of schema types and links that should be treated\n    #: as singletons in the context of this compilation.\n    #: If a tuple is provided, the boolean argument indicates it is optional.\n    singletons: Collection[\n        SourceOrPathId | tuple[SourceOrPathId, bool]\n    ] = frozenset()\n\n    #: Type references that should be remaped to another type.  This\n    #: is for dealing with remapping explicit type names in schema\n    #: expressions to their subtypes when necessary.\n    type_remaps: dict[s_obj.Object, s_obj.Object] = dc_field(\n        default_factory=dict\n    )\n\n    detached: bool = False\n\n    #: In order to prevent recursive triggers, these fields are used to track\n    #: the sources of a given trigger. These will only be present if\n    #: schema_object_context is set to Trigger.\n    trigger_type: Optional[s_types.Type] = None\n    trigger_kinds: Optional[Collection[qltypes.TriggerKind]] = None\n"
  },
  {
    "path": "edb/edgeql/compiler/pathctx.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL compiler path scope helpers.\"\"\"\n\n\nfrom __future__ import annotations\n\nfrom typing import Literal, Optional, AbstractSet\n\nfrom edb import errors\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtyputils\n\nfrom edb.schema import name as s_name\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import types as s_types\n\nfrom . import context\n\n\ndef get_path_id(\n    stype: s_types.Type,\n    *,\n    typename: Optional[s_name.QualName] = None,\n    ctx: context.ContextLevel,\n) -> irast.PathId:\n    return irast.PathId.from_type(\n        ctx.env.schema,\n        stype,\n        typename=typename,\n        env=ctx.env,\n        namespace=ctx.path_id_namespace)\n\n\ndef get_tuple_indirection_path_id(\n    tuple_path_id: irast.PathId,\n    element_name: str,\n    element_type: s_types.Type,\n    *,\n    ctx: context.ContextLevel,\n) -> irast.PathId:\n\n    ctx.env.schema, src_t = irtyputils.ir_typeref_to_type(\n        ctx.env.schema, tuple_path_id.target)\n    ptrcls = irast.TupleIndirectionLink(\n        src_t,\n        element_type,\n        element_name=element_name,\n    )\n\n    ptrref = irtyputils.ptrref_from_ptrcls(\n        schema=ctx.env.schema,\n        ptrcls=ptrcls,\n        cache=ctx.env.ptr_ref_cache,\n        typeref_cache=ctx.env.type_ref_cache,\n    )\n\n    return tuple_path_id.extend(ptrref=ptrref)\n\n\ndef get_expression_path_id(\n    stype: s_types.Type,\n    alias: Optional[str] = None,\n    *,\n    ctx: context.ContextLevel,\n) -> irast.PathId:\n    if alias is None:\n        alias = ctx.aliases.get('expr')\n    typename = s_name.QualName(module='__derived__', name=alias)\n    return get_path_id(stype, typename=typename, ctx=ctx)\n\n\ndef register_set_in_scope(\n    ir_set: irast.Set,\n    *,\n    path_scope: Optional[irast.ScopeTreeNode] = None,\n    optional: bool = False,\n    ctx: context.ContextLevel,\n) -> None:\n    if path_scope is None:\n        path_scope = ctx.path_scope\n\n    path_scope.attach_path(\n        ir_set.path_id,\n        optional=optional,\n        span=ir_set.span,\n        ctx=ctx,\n    )\n\n\ndef assign_set_scope(\n    ir_set: irast.Set,\n    scope: Optional[irast.ScopeTreeNode],\n    *,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n    if scope is None:\n        ir_set.path_scope_id = None\n    else:\n        if scope.unique_id is None:\n            scope.unique_id = ctx.scope_id_ctr.nextval()\n            ctx.env.scope_tree_nodes[scope.unique_id] = scope\n        ir_set.path_scope_id = scope.unique_id\n        if scope.find_child(ir_set.path_id):\n            raise RuntimeError('scoped set must not contain itself')\n\n    return ir_set\n\n\ndef get_set_scope(\n    ir_set: irast.Set,\n    *,\n    ctx: context.ContextLevel,\n) -> Optional[irast.ScopeTreeNode]:\n    if ir_set.path_scope_id is None:\n        return None\n    else:\n        scope = ctx.env.scope_tree_nodes.get(ir_set.path_scope_id)\n        if scope is None:\n            raise errors.InternalServerError(\n                f'dangling scope pointer to node with uid'\n                f':{ir_set.path_scope_id} in {ir_set!r}'\n            )\n        return scope\n\n\ndef extend_path_id(\n    path_id: irast.PathId,\n    *,\n    ptrcls: s_pointers.PointerLike,\n    direction: s_pointers.PointerDirection = (\n        s_pointers.PointerDirection.Outbound),\n    ns: AbstractSet[str] = frozenset(),\n    ctx: context.ContextLevel,\n) -> irast.PathId:\n    \"\"\"A wrapper over :meth:`ir.pathid.PathId.extend` that also ensures\n       the cardinality of *ptrcls* is known at the end of compilation.\n    \"\"\"\n\n    ptrref = irtyputils.ptrref_from_ptrcls(\n        schema=ctx.env.schema,\n        ptrcls=ptrcls,\n        cache=ctx.env.ptr_ref_cache,\n        typeref_cache=ctx.env.type_ref_cache,\n    )\n\n    return path_id.extend(ptrref=ptrref, direction=direction, ns=ns)\n\n\ndef ban_inserting_path(\n    path_id: irast.PathId,\n    *,\n    location: Literal['body'] | Literal['else'],\n    ctx: context.ContextLevel,\n) -> None:\n\n    ctx.inserting_paths = ctx.inserting_paths.copy()\n    ctx.inserting_paths[path_id] = location\n\n\ndef path_is_inserting(\n    path_id: irast.PathId, *, ctx: context.ContextLevel\n) -> bool:\n\n    node = ctx.path_scope.find_visible(path_id)\n    return bool(\n        node\n        and node.path_id\n        and ctx.inserting_paths.get(node.path_id) == 'body'\n    )\n"
  },
  {
    "path": "edb/edgeql/compiler/policies.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL access policy compilation.\"\"\"\n\n\nfrom __future__ import annotations\n\nfrom typing import Optional\n\nfrom edb.ir import ast as irast\n\nfrom edb.schema import name as s_name\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import policies as s_policies\nfrom edb.schema import schema as s_schema\nfrom edb.schema import types as s_types\nfrom edb.schema import expr as s_expr\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom edb.ir import typeutils as irtyputils\n\nfrom . import astutils\nfrom . import context\nfrom . import dispatch\nfrom . import setgen\n\n\ndef should_ignore_rewrite(\n    stype: s_types.Type,\n    *,\n    ctx: context.ContextLevel,\n) -> bool:\n    if not ctx.suppress_rewrites:\n        return False\n\n    if stype in ctx.suppress_rewrites:\n        return True\n\n    # If we are in any access policy at all, suppress all\n    # policies except the stdlib ones.\n    #\n    # (Eventually will might do a generalization of this based on\n    # RBAC ownership of schema objects.)\n    # XXX: extension modules???\n    schema = ctx.env.schema\n    if (\n        isinstance(stype, s_objtypes.ObjectType)\n        and s_name.UnqualName(stype.get_name(schema).module)\n            not in s_schema.STD_MODULES\n    ):\n        return True\n\n    return False\n\n\ndef get_access_policies(\n    stype: s_objtypes.ObjectType,\n    *,\n    ctx: context.ContextLevel,\n) -> tuple[s_policies.AccessPolicy, ...]:\n    schema = ctx.env.schema\n    if not ctx.env.options.apply_query_rewrites:\n        return ()\n\n    # The apply_access_policies config flag disables user-specified\n    # access polices, but not stdlib ones\n    if (\n        not ctx.env.options.apply_user_access_policies\n        and s_name.UnqualName(stype.get_name(schema).module)\n            not in s_schema.STD_MODULES\n    ):\n        return ()\n\n    return stype.get_access_policies(schema).objects(schema)\n\n\ndef has_own_policies(\n    *,\n    stype: s_objtypes.ObjectType,\n    skip_from: Optional[s_objtypes.ObjectType]=None,\n    ctx: context.ContextLevel,\n) -> bool:\n    # TODO: some kind of caching or precomputation\n\n    schema = ctx.env.schema\n    for pol in get_access_policies(stype, ctx=ctx):\n        if not any(\n            skip_from == base.get_subject(schema)\n            for base in pol.get_bases(schema).objects(schema)\n        ):\n            return True\n\n    return any(\n        has_own_policies(stype=child, skip_from=stype, ctx=ctx)\n        for child in stype.children(schema)\n        if not irtyputils.is_excluded_cfg_view(\n            child, ancestor=stype, schema=schema\n        )\n    )\n\n\ndef compile_pol(\n    pol: s_policies.AccessPolicy,\n    *,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n    \"\"\"Compile the condition from an individual policy.\n\n    A policy is evaluated in a context where it is allowed to access\n    the *original subject type of the policy* and *all of its\n    descendants*.\n\n    Because it is based on the original source of the policy,\n    we need to compile each policy separately.\n    \"\"\"\n    schema = ctx.env.schema\n\n    expr_field: Optional[s_expr.Expression] = pol.get_expr(schema)\n    if expr_field:\n        expr = expr_field.parse()\n    else:\n        expr = qlast.Constant.boolean(True)\n\n    if condition := pol.get_condition(schema):\n        assert isinstance(condition, s_expr.Expression)\n        expr = qlast.BinOp(op='AND', left=condition.parse(), right=expr)\n\n    # Find all descendants of the original subject of the rule\n    subject = pol.get_original_subject(schema)\n    descs = {subject} | {\n        desc for desc in subject.descendants(schema)\n        if desc.is_material_object_type(schema)\n    }\n\n    # Compile it with all of the\n    with ctx.newscope(fenced=True) as _, _.detached() as dctx:\n        dctx.partial_path_prefix = ctx.partial_path_prefix\n        dctx.expr_exposed = context.Exposure.UNEXPOSED\n        dctx.suppress_rewrites = frozenset(descs)\n\n        return setgen.scoped_set(dispatch.compile(expr, ctx=dctx), ctx=dctx)\n\n\ndef get_extra_function_rewrite_filter(ctx: context.ContextLevel) -> qlast.Expr:\n    # Functions need to check whether access policies are disabled,\n    # which is signalled through a field in globals json object.\n    # It's only populated when policies are disabled.\n    #\n    # We could also have done this by checking\n    # cfg::Config.apply_access_policies, but that's probably slower,\n    # and we have this mechanism anyway.\n    json_type = qlast.TypeName(maintype=qlast.ObjectRef(\n        module='__std__', name='json'))\n    glob_set = setgen.get_func_global_json_arg(ctx=ctx)\n    func_override = qlast.FunctionCall(\n        func=('__std__', 'json_get'),\n        args=[\n            ctx.create_anchor(glob_set, 'a'),\n            qlast.Constant.string(value=\"__disable_access_policies\"),\n        ],\n        kwargs={\n            'default': qlast.TypeCast(\n                expr=qlast.Constant.boolean(False),\n                type=json_type,\n            )\n        },\n    )\n    return qlast.TypeCast(\n        expr=func_override,\n        type=qlast.TypeName(maintype=qlast.ObjectRef(\n            module='__std__', name='bool'))\n    )\n\n\ndef get_rewrite_filter(\n    stype: s_objtypes.ObjectType,\n    *,\n    mode: qltypes.AccessKind,\n    ctx: context.ContextLevel,\n) -> Optional[qlast.Expr]:\n    schema = ctx.env.schema\n    pols = get_access_policies(stype, ctx=ctx)\n    if not pols:\n        return None\n\n    ctx.anchors = ctx.anchors.copy()\n\n    allow, deny = [], []\n    for pol in pols:\n        if mode not in pol.get_access_kinds(schema):\n            continue\n\n        ir_set = compile_pol(pol, ctx=ctx)\n        expr: qlast.Expr = ctx.create_anchor(ir_set, move_scope=True)\n\n        is_allow = pol.get_action(schema) == qltypes.AccessPolicyAction.Allow\n        if is_allow:\n            allow.append(expr)\n        else:\n            deny.append(expr)\n\n    if ctx.env.options.func_params is not None:\n        allow.append(get_extra_function_rewrite_filter(ctx))\n\n    if allow:\n        filter_expr = astutils.extend_binop(None, *allow, op='OR')\n    else:\n        filter_expr = qlast.Constant.boolean(False)\n\n    if deny:\n        deny_expr = qlast.UnaryOp(\n            op='NOT',\n            operand=astutils.extend_binop(None, *deny, op='OR')\n        )\n        filter_expr = astutils.extend_binop(filter_expr, deny_expr)\n\n    # We compile the expression again so we can do an IR based\n    # analysis on it below.\n    with ctx.newscope(fenced=False) as dctx:\n        # HACK: to prevent filter_ir from being warning fenced\n        dctx.allow_factoring()\n        dctx.expr_exposed = context.Exposure.UNEXPOSED\n        filter_ir = dispatch.compile(filter_expr, ctx=dctx)\n        filter_expr = setgen.moveable_anchor(filter_ir, ctx=dctx)\n\n    # This is a bad hack, but add an always false condition that\n    # postgres does not *know* is always false. This prevents postgres\n    # from bogusly optimizing away the entire type CTE if it can prove\n    # it empty (which could then result in assert_exists on links to\n    # the type not always firing).\n    #\n    # As an optimization, we try to only do it when the object might\n    # not be referenced.\n    if (\n        mode == qltypes.AccessKind.Select\n        and not (\n            ctx.partial_path_prefix\n            and _always_references_set(filter_ir, ctx.partial_path_prefix)\n        )\n    ):\n        bogus_check = qlast.BinOp(\n            op='?=',\n            left=qlast.Path(partial=True, steps=[qlast.Ptr(name='id')]),\n            right=qlast.TypeCast(\n                type=qlast.TypeName(maintype=qlast.ObjectRef(\n                    module='__std__', name='uuid')),\n                expr=qlast.Set(elements=[]),\n            )\n        )\n        filter_expr = astutils.extend_binop(filter_expr, bogus_check, op='OR')\n\n    return filter_expr\n\n\ndef _always_references_set(\n    ir: irast.Set | irast.Expr,\n    ref: irast.Set,\n    inverted: bool=False,\n) -> bool:\n    \"\"\"Return whether *ir* \"always references\" *ref*\n\n    The idea here is to check whether *ir* references *ref* in a way\n    that ensures that postgres will actually look at *ref* when\n    executing.\n\n    Fortunately postgres doesn't seem to do anything too crazy here(??),\n    so we mostly just have to understand how it works with boolean\n    operators, IF, and coalesce.\n    But we also need to be able to propagate it through other operations.\n\n    We need *ref* to be referenced in *every* conjunct of an AND.\n    We need it referenced by *at least one* disjunct of an OR.\n    But because of DeMorgan's law (which postgres understands),\n    OR sometimes needs to be treated like AND.\n\n    We track the invertedness and invert the AND and OR behavior when\n    underneath a NOT, kind of for fun.\n    \"\"\"\n    if isinstance(ir, irast.Set):\n        if ir is ref:\n            return True\n        ir = ir.expr\n\n    match ir:\n        case irast.SelectStmt(result=result):\n            return _always_references_set(result, ref, inverted)\n\n        case irast.OperatorCall(\n            func_shortname=s_name.QualName('std', 'OR'), args=args\n        ):\n            check = all if inverted else any\n            return check(\n                _always_references_set(x.expr, ref, inverted)\n                for x in args.values()\n            )\n\n        case irast.OperatorCall(\n            func_shortname=s_name.QualName('std', 'AND'), args=args\n        ):\n            check = any if inverted else all\n            return check(\n                _always_references_set(x.expr, ref, inverted)\n                for x in args.values()\n            )\n\n        case irast.OperatorCall(\n            func_shortname=s_name.QualName('std', 'NOT'), args={0: arg}\n        ):\n            return _always_references_set(arg.expr, ref, not inverted)\n\n        case irast.OperatorCall(\n            func_shortname=s_name.QualName('std', '??'), args={0: lhs, 1: _},\n        ):\n            # LHS always evaluated; RHS might not be\n            return _always_references_set(lhs.expr, ref, inverted)\n\n        case irast.OperatorCall(\n            func_shortname=s_name.QualName('std', 'IF'),\n            args={0: t, 1: c, 2: f},\n        ):\n            return (\n                _always_references_set(c.expr, ref, inverted)\n                or (\n                    _always_references_set(t.expr, ref, inverted)\n                    and _always_references_set(f.expr, ref, inverted)\n                )\n            )\n\n        # Any other call, we use 'any' semantics.\n        case irast.Call(args=args):\n            return any(\n                _always_references_set(x.expr, ref, inverted)\n                for x in args.values()\n            )\n\n        case irast.Pointer(expr=expr) as p:\n            if expr is not None:\n                return _always_references_set(expr, ref, inverted)\n            else:\n                return _always_references_set(p.source, ref, inverted)\n\n        case irast.TypeCast(expr=expr):\n            return _always_references_set(expr, ref, inverted)\n\n        case _:\n            return False\n\n\ndef try_type_rewrite(\n    stype: s_objtypes.ObjectType,\n    *,\n    skip_subtypes: bool,\n    ctx: context.ContextLevel,\n) -> None:\n    schema = ctx.env.schema\n    rw_key = (stype, skip_subtypes)\n    type_rewrites = ctx.env.type_rewrites\n\n    # Make sure the base types in unions and intersections have their\n    # rewrites compiled\n    if stype.is_compound_type(schema):\n        type_rewrites[rw_key] = None\n        objs = (\n            stype.get_union_of(schema).objects(schema) +\n            stype.get_intersection_of(schema).objects(schema)\n        )\n        for obj in objs:\n            srw_key = (obj, skip_subtypes)\n            if srw_key not in type_rewrites:\n                try_type_rewrite(\n                    stype=obj, skip_subtypes=skip_subtypes, ctx=ctx)\n                # Mark this as having a real rewrite if any parts do\n                if type_rewrites[srw_key]:\n                    type_rewrites[rw_key] = True\n        return\n\n    # What we *hope* to do, is to just directly select from the view\n    # for our type and apply filters to it.\n    #\n    # Note that this is mostly optimizing the size/complexity of the\n    # output *text*, by using views instead of expanding it out\n    # manually.\n    #\n    # If some of our children have their own policies, though, we want\n    # to instead union together all of our children.\n    #\n    # But if that is the case, and some of our children have\n    # overlapping descendants, then we can't do that either, so we\n    # need to explicitly list out *all* of the descendants.\n    children_have_policies = not skip_subtypes and any(\n        has_own_policies(stype=child, skip_from=stype, ctx=ctx)\n        for child in stype.children(schema)\n        if not irtyputils.is_excluded_cfg_view(\n            child, ancestor=stype, schema=schema\n        )\n    )\n\n    pols = get_access_policies(stype, ctx=ctx)\n    if not pols and not children_have_policies:\n        type_rewrites[rw_key] = None\n        return\n\n    # TODO: caching?\n    children_overlap = False\n    if children_have_policies:\n        all_child_descs = [\n            x\n            for child in stype.children(schema)\n            for x in [child, *child.descendants(schema)]\n        ]\n\n        # Children overlap\n        child_descs = set(all_child_descs)\n        if len(child_descs) != len(all_child_descs):\n            children_overlap = True\n\n    # Put a placeholder to prevent recursion.\n    type_rewrites[rw_key] = None\n\n    sets = []\n    # Generate the the filters for the base type we are actually considering.\n    # If the type is abstract, though, and there are policies on the children,\n    # then we skip it.\n    if not (children_have_policies and stype.get_abstract(schema)):\n        with ctx.detached() as subctx:\n            # We skip looking at subtypes in two cases:\n            # 1. When some children have policies of their own, and thus\n            #    need to be handled separately\n            # 2. When skip_subtypes was set, and so we must\n            base_set = setgen.class_set(\n                stype=stype,\n                skip_subtypes=children_have_policies or skip_subtypes,\n                ctx=subctx)\n\n            if children_have_policies:\n                # If children have policies, then all of the filtered sets\n                # will be generated on skip_subtypes sets, so we don't have\n                # any work to do.\n                filtered_set = base_set\n            else:\n                # Otherwise, do the actual work of filtering.\n                from . import clauses\n\n                filtered_stmt = irast.SelectStmt(result=base_set)\n                subctx.anchors['__subject__'] = base_set\n                subctx.partial_path_prefix = base_set\n                subctx.path_scope = subctx.env.path_scope.root.attach_fence()\n\n                filtered_stmt.where = clauses.compile_where_clause(\n                    get_rewrite_filter(\n                        stype, mode=qltypes.AccessKind.Select, ctx=subctx),\n                    ctx=subctx)\n\n                filtered_set = setgen.scoped_set(filtered_stmt, ctx=subctx)\n\n            sets.append(filtered_set)\n\n    if children_have_policies and not skip_subtypes:\n        # N.B: we don't filter here, we just generate references\n        # they will go in their own CTEs\n        children = (\n            stype.children(schema)\n            if not children_overlap\n            else stype.descendants(schema)\n        )\n        # We need to explicitly exclude cfg views to prevent them from\n        # from showing up in type rewrites. Normally this happens in\n        # inheritance.get_inheritance_view, but needs to happen here\n        # when descendants are expanded.\n        children = frozenset(\n            child\n            for child in children\n            if not irtyputils.is_excluded_cfg_view(\n                child, ancestor=stype, schema=schema\n            )\n        )\n        sets += [\n            # We need to wrap it in a type override so that unioning\n            # them all together works...\n            setgen.expression_set(\n                setgen.ensure_stmt(\n                    setgen.class_set(\n                        stype=child, skip_subtypes=children_overlap, ctx=ctx),\n                    ctx=ctx),\n                type_override=stype,\n                ctx=ctx,\n            )\n            for child in children\n            if child.is_material_object_type(schema)\n        ]\n\n    # If we have multiple sets, union them together\n    rewritten_set: Optional[irast.Set]\n    if len(sets) > 1:\n        with ctx.new() as subctx:\n            subctx.expr_exposed = context.Exposure.UNEXPOSED\n            subctx.anchors = subctx.anchors.copy()\n            parts: list[qlast.Expr] = [subctx.create_anchor(x) for x in sets]\n            rewritten_set = dispatch.compile(\n                qlast.Set(elements=parts), ctx=subctx)\n    elif len(sets) > 0:\n        rewritten_set = sets[0]\n    else:\n        rewritten_set = None\n\n    type_rewrites[rw_key] = rewritten_set\n\n\ndef compile_dml_write_policies(\n    stype: s_objtypes.ObjectType,\n    result: irast.Set,\n    mode: qltypes.AccessKind, *,\n    ctx: context.ContextLevel,\n) -> Optional[irast.WritePolicies]:\n    \"\"\"Compile policy filters and wrap them into irast.WritePolicies\"\"\"\n    pols = get_access_policies(stype, ctx=ctx)\n    if not pols:\n        return None\n\n    with ctx.detached() as _, _.newscope(fenced=True) as subctx:\n        # TODO: can we make sure to always avoid generating needless\n        # select filters\n        _prepare_dml_policy_context(stype, result, ctx=subctx)\n\n        schema = subctx.env.schema\n        subctx.anchors = subctx.anchors.copy()\n\n        policies = []\n        for pol in pols:\n            if mode not in pol.get_access_kinds(schema):\n                continue\n\n            ir_set = compile_pol(pol, ctx=subctx)\n\n            action = pol.get_action(schema)\n            name = str(pol.get_shortname(schema))\n\n            policies.append(\n                irast.WritePolicy(\n                    expr=ir_set,\n                    action=action,\n                    name=name,\n                    error_msg=pol.get_errmessage(schema),\n                )\n            )\n\n        return irast.WritePolicies(policies=policies)\n\n\ndef compile_dml_read_policies(\n    stype: s_objtypes.ObjectType,\n    result: irast.Set,\n    mode: qltypes.AccessKind,\n    *,\n    ctx: context.ContextLevel,\n) -> Optional[irast.ReadPolicyExpr]:\n    \"\"\"Compile a policy filter for a DML statement at a particular type\"\"\"\n    if not get_access_policies(stype, ctx=ctx):\n        return None\n\n    with ctx.detached() as _, _.newscope(fenced=True) as subctx:\n        # TODO: can we make sure to always avoid generating needless\n        # select filters\n        _prepare_dml_policy_context(stype, result, ctx=subctx)\n\n        condition = get_rewrite_filter(stype, mode=mode, ctx=subctx)\n        if not condition:\n            return None\n\n        return irast.ReadPolicyExpr(\n            expr=setgen.scoped_set(\n                dispatch.compile(condition, ctx=subctx), ctx=subctx\n            ),\n        )\n\n\ndef _prepare_dml_policy_context(\n    stype: s_objtypes.ObjectType,\n    result: irast.Set,\n    *,\n    ctx: context.ContextLevel,\n) -> None:\n    # It doesn't matter whether we skip subtypes here, so don't skip\n    # subtypes if it has already been compiled that way, otherwise do.\n    skip_subtypes = (stype, False) not in ctx.env.type_rewrites\n    result = setgen.class_set(\n        stype, path_id=result.path_id, skip_subtypes=skip_subtypes, ctx=ctx\n    )\n\n    ctx.anchors['__subject__'] = result\n    ctx.partial_path_prefix = result\n"
  },
  {
    "path": "edb/edgeql/compiler/polyres.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL compiler routines for polymorphic call resolution.\"\"\"\n\n\nfrom __future__ import annotations\n\nfrom typing import (\n    Any,\n    Optional,\n    AbstractSet,\n    Iterable,\n    Mapping,\n    Sequence,\n    cast,\n)\nimport dataclasses\n\nimport hashlib\nimport json\n\nfrom edb import errors\n\nfrom edb.ir import ast as irast\nfrom edb.ir import utils as irutils\n\nfrom edb.schema import functions as s_func\nfrom edb.schema import name as sn\nfrom edb.schema import types as s_types\nfrom edb.schema import pseudo as s_pseudo\nfrom edb.schema import expr as s_expr\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import schema as s_schema\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes as ft\n\nfrom . import context\nfrom . import dispatch\nfrom . import pathctx\nfrom . import setgen\nfrom . import tuple_args\nfrom . import typegen\n\n\n@dataclasses.dataclass(kw_only=True, frozen=True)\nclass BoundArg:\n    \"\"\"The base type for bound arguments for BoundCall.\"\"\"\n\n    val: irast.Set\n\n\n@dataclasses.dataclass(kw_only=True, frozen=True)\nclass DefaultBitmask(BoundArg):\n    \"\"\"The default bitmask argument, if defaults are present.\"\"\"\n    pass\n\n\n@dataclasses.dataclass(kw_only=True, frozen=True)\nclass ValueArg(BoundArg):\n    \"\"\"A bound argument with an actual value.\"\"\"\n\n    name: str\n\n    orig_param_type: s_types.Type\n    param_type: s_types.Type\n    param_typemod: ft.TypeModifier\n    param_kind: ft.ParameterKind\n\n    val: irast.Set\n    valtype: s_types.Type\n\n    polymorphism: ft.Polymorphism = ft.Polymorphism.NotUsed\n\n\n@dataclasses.dataclass(kw_only=True, frozen=True)\nclass DefaultArg(ValueArg):\n    \"\"\"A bound argument whose value comes from a default.\"\"\"\n    pass\n\n\n@dataclasses.dataclass(kw_only=True, frozen=True)\nclass PassedArg(ValueArg):\n    \"\"\"A bound argument whose value comes from a passed argument.\"\"\"\n\n    cast_distance: int\n    arg_id: int | str\n\n\n@dataclasses.dataclass(frozen=True)\nclass MissingArg:\n\n    param: Optional[s_func.ParameterLike]\n    param_type: s_types.Type\n\n\n@dataclasses.dataclass(kw_only=True, frozen=True)\nclass BoundCall:\n\n    func: s_func.CallableLike\n    args: list[BoundArg]\n    null_args: set[str]\n    return_type: s_types.Type\n    variadic_arg_id: Optional[int]\n    variadic_arg_count: Optional[int]\n    return_polymorphism: ft.Polymorphism = ft.Polymorphism.NotUsed\n\n    server_param_conversions: Optional[dict[\n        str,\n        dict[str, context.ServerParamConversion],\n    ]] = None\n\n\n_VARIADIC = ft.ParameterKind.VariadicParam\n_NAMED_ONLY = ft.ParameterKind.NamedOnlyParam\n_POSITIONAL = ft.ParameterKind.PositionalParam\n\n_SET_OF = ft.TypeModifier.SetOfType\n_OPTIONAL = ft.TypeModifier.OptionalType\n_SINGLETON = ft.TypeModifier.SingletonType\n\n\ndef find_callable_typemods(\n    candidates: Sequence[s_func.CallableLike],\n    *,\n    num_args: int,\n    kwargs_names: AbstractSet[str],\n    ctx: context.ContextLevel,\n) -> dict[int | str, ft.TypeModifier]:\n    \"\"\"Find the type modifiers for a callable.\n\n    We do this early, before we've compiled/checked the arguments,\n    so that we can compile the arguments with the proper fences.\n    \"\"\"\n\n    typ: s_types.Type = s_pseudo.PseudoType.get(ctx.env.schema, 'anytype')\n    dummy = irast.DUMMY_SET\n    args = [(typ, dummy)] * num_args\n    kwargs = {k: (typ, dummy) for k in kwargs_names}\n    options = find_callable(\n        candidates, basic_matching_only=True, args=args, kwargs=kwargs, ctx=ctx\n    )\n\n    # No options means an error is going to happen later, but for now,\n    # just return some placeholders so that we can make it to the\n    # error later.\n    if not options:\n        return {k: _SINGLETON for k in set(range(num_args)) | kwargs_names}\n\n    fts: dict[int | str, ft.TypeModifier] = {}\n    for choice in options:\n        for barg in choice.args:\n            if not isinstance(barg, PassedArg):\n                continue\n            ft = barg.param_typemod\n            if barg.arg_id in fts and fts[barg.arg_id] != ft:\n                if ft == _SET_OF or fts[barg.arg_id] == _SET_OF:\n                    raise errors.QueryError(\n                        f'argument could be SET OF or not in call to '\n                        f'{candidates[0].get_verbosename(ctx.env.schema)}: '\n                        f'seems like a stdlib bug!')\n                else:\n                    # If there is a mix between OPTIONAL and SINGLETON\n                    # arguments in possible call sites, we just call it\n                    # optional. Generated code quality will be a little\n                    # worse but still correct.\n                    fts[barg.arg_id] = _OPTIONAL\n            else:\n                fts[barg.arg_id] = ft\n\n    return fts\n\n\ndef find_callable(\n    candidates: Iterable[s_func.CallableLike],\n    *,\n    args: list[tuple[s_types.Type, irast.Set]],\n    kwargs: dict[str, tuple[s_types.Type, irast.Set]],\n    basic_matching_only: bool = False,\n    ctx: context.ContextLevel,\n) -> list[BoundCall]:\n\n    implicit_cast_distance = None\n    matched = []\n\n    candidates = list(candidates)\n    for candidate in candidates:\n        call = None\n        if (\n            not basic_matching_only\n            and (conversion := _check_server_arg_conversion(\n                candidate, args, kwargs, ctx=ctx\n            ))\n        ):\n            # If there is a server param conversion, the argument should be\n            # treated as if it has already been converted.\n            #\n            # This means we need to check the other candidates to see if they\n            # match the converted args.\n            converted_args, converted_kwargs, converted_params = conversion\n\n            for alt_candidate in candidates:\n                if alt_candidate is candidate:\n                    continue\n                if call := try_bind_call_args(\n                    converted_args,\n                    converted_kwargs,\n                    alt_candidate,\n                    basic_matching_only,\n                    ctx=ctx,\n                    server_param_conversions=converted_params,\n                ):\n                    # A call which matches the conversion exists.\n                    # Add the server param conversions to the env.\n                    break\n\n        else:\n            call = try_bind_call_args(\n                args, kwargs, candidate, basic_matching_only, ctx=ctx)\n\n        if call is None:\n            continue\n\n        total_cd = sum(\n            barg.cast_distance\n            for barg in call.args\n            if isinstance(barg, PassedArg)\n        )\n\n        if implicit_cast_distance is None:\n            implicit_cast_distance = total_cd\n            matched.append(call)\n        elif implicit_cast_distance == total_cd:\n            matched.append(call)\n        elif implicit_cast_distance > total_cd:\n            implicit_cast_distance = total_cd\n            matched = [call]\n\n    if len(matched) <= 1:\n        # Unambiguios resolution\n        return matched\n\n    else:\n        # Ambiguous resolution, try to disambiguate by\n        # checking for total type distance.\n        type_dist = None\n        remaining = []\n\n        for call in matched:\n            call_type_dist = 0\n\n            for barg in call.args:\n                if not isinstance(barg, PassedArg):\n                    # Skip injected bitmask argument.\n                    continue\n\n                arg_type_dist = barg.valtype.get_common_parent_type_distance(\n                    barg.orig_param_type, ctx.env.schema\n                )\n                call_type_dist += arg_type_dist\n\n            if type_dist is None:\n                type_dist = call_type_dist\n                remaining.append(call)\n            elif type_dist == call_type_dist:\n                remaining.append(call)\n            elif type_dist > call_type_dist:\n                type_dist = call_type_dist\n                remaining = [call]\n\n        return remaining\n\n\ndef try_bind_call_args(\n    args: Sequence[tuple[s_types.Type, irast.Set]],\n    kwargs: Mapping[str, tuple[s_types.Type, irast.Set]],\n    func: s_func.CallableLike,\n    basic_matching_only: bool,\n    *,\n    ctx: context.ContextLevel,\n    server_param_conversions: Optional[\n        dict[str, dict[str, context.ServerParamConversion]]\n    ] = None,\n) -> Optional[BoundCall]:\n\n    return_type = func.get_return_type(ctx.env.schema)\n    is_abstract = func.get_abstract(ctx.env.schema)\n    resolved_poly_base_type: Optional[s_types.Type] = None\n\n    def _get_cast_distance(\n        arg: irast.Set,\n        arg_type: s_types.Type,\n        param_type: s_types.Type,\n    ) -> int:\n        nonlocal resolved_poly_base_type\n        if basic_matching_only:\n            return 0\n\n        if in_polymorphic_func:\n            # Compiling a body of a polymorphic function.\n\n            if arg_type.is_polymorphic(schema):\n                if param_type.is_polymorphic(schema):\n                    if arg_type.test_polymorphic(schema, param_type):\n                        return 0\n                    else:\n                        return -1\n                else:\n                    if arg_type.resolve_polymorphic(schema, param_type):\n                        return 0\n                    else:\n                        return -1\n\n        if param_type.is_polymorphic(schema):\n            if not arg_type.test_polymorphic(schema, param_type):\n                return -1\n\n            resolved = param_type.resolve_polymorphic(schema, arg_type)\n            if resolved is None:\n                return -1\n\n            if resolved_poly_base_type is None:\n                resolved_poly_base_type = resolved\n\n            if resolved_poly_base_type == resolved:\n                if is_abstract:\n                    return s_types.MAX_TYPE_DISTANCE\n                elif arg_type.is_range() and param_type.is_multirange():\n                    # Ranges are implicitly cast into multiranges of the same\n                    # type, so they are compatible as far as polymorphic\n                    # resolution goes, but it's still 1 cast.\n                    return 1\n                else:\n                    return 0\n\n            ctx.env.schema, ct = (\n                resolved_poly_base_type.find_common_implicitly_castable_type(\n                    resolved,\n                    ctx.env.schema,\n                )\n            )\n\n            if ct is not None:\n                # If we found a common implicitly castable type, we\n                # refine our resolved_poly_base_type to be that as the\n                # more general case.\n                resolved_poly_base_type = ct\n            else:\n                # Try resolving a polymorphic argument type against the\n                # resolved base type. This lets us handle cases like\n                #  - if b then x else {}\n                #  - if b then [1] else []\n                # Though it is still unfortunately not smart enough\n                # to handle the reverse case.\n                if resolved.is_polymorphic(schema):\n                    ct = resolved.resolve_polymorphic(\n                        schema, resolved_poly_base_type)\n\n            if ct is not None:\n                return s_types.MAX_TYPE_DISTANCE if is_abstract else 0\n            else:\n                return -1\n\n        if arg_type.issubclass(schema, param_type):\n            return 0\n\n        return arg_type.get_implicit_cast_distance(param_type, schema)\n\n    schema = ctx.env.schema\n\n    in_polymorphic_func = (\n        ctx.env.options.func_params is not None and\n        ctx.env.options.func_params.has_polymorphic(schema)\n    )\n\n    variadic_arg_id: Optional[int] = None\n    variadic_arg_count: Optional[int] = None\n    no_args_call = not args and not kwargs\n    has_inlined_defaults = (\n        func.has_inlined_defaults(schema)\n        and not (\n            isinstance(func, s_func.Function)\n            and (\n                func.get_volatility(schema) == ft.Volatility.Modifying\n                or func.get_is_inlined(schema)\n            )\n        )\n    )\n\n    func_params = func.get_params(schema)\n\n    if not func_params:\n        if no_args_call:\n            # Match: `func` is a function without parameters\n            # being called with no arguments.\n            bargs: list[BoundArg] = []\n            if has_inlined_defaults:\n                bytes_t = ctx.env.get_schema_type_and_track(\n                    sn.QualName('std', 'bytes'))\n                typeref = typegen.type_to_typeref(bytes_t, env=ctx.env)\n                argval = setgen.ensure_set(\n                    irast.BytesConstant(value=b'\\x00', typeref=typeref),\n                    typehint=bytes_t,\n                    ctx=ctx)\n                bargs = [\n                    DefaultBitmask(\n                        val=argval,\n                    )\n                ]\n            return BoundCall(\n                func=func,\n                args=bargs,\n                null_args=set(),\n                return_type=return_type,\n                variadic_arg_id=None,\n                variadic_arg_count=None,\n                server_param_conversions=server_param_conversions,\n            )\n        else:\n            # No match: `func` is a function without parameters\n            # being called with some arguments.\n            return None\n\n    named_only = func_params.find_named_only(schema)\n\n    if no_args_call and func_params.has_required_params(schema):\n        # A call without arguments and there is at least\n        # one parameter without default.\n        return None\n\n    bound_args_prep: list[MissingArg | PassedArg] = []\n\n    params = func_params.get_in_canonical_order(schema)\n    nparams = len(params)\n    nargs = len(args)\n    has_missing_args = False\n\n    ai = 0\n    pi = 0\n    matched_kwargs = 0\n\n    # Bind NAMED ONLY arguments (they are compiled as first set of arguments).\n    while True:\n        if pi >= nparams:\n            break\n\n        param = params[pi]\n        if param.get_kind(schema) is not _NAMED_ONLY:\n            break\n\n        pi += 1\n\n        param_shortname = param.get_parameter_name(schema)\n        param_type = param.get_type(schema)\n        param_typemod = param.get_typemod(schema)\n        param_kind = param.get_kind(schema)\n        if param_shortname in kwargs:\n            matched_kwargs += 1\n\n            arg_type, arg_val = kwargs[param_shortname]\n            cd = _get_cast_distance(arg_val, arg_type, param_type)\n            if cd < 0:\n                return None\n\n            bound_args_prep.append(\n                PassedArg(\n                    name=param_shortname,\n                    orig_param_type=param_type,\n                    param_type=param_type,\n                    param_typemod=param_typemod,\n                    param_kind=param_kind,\n                    val=arg_val,\n                    valtype=arg_type,\n                    cast_distance=cd,\n                    arg_id=param_shortname,\n                )\n            )\n\n        else:\n            if param.get_default(schema) is None:\n                # required named parameter without default and\n                # without a matching argument\n                return None\n\n            has_missing_args = True\n            bound_args_prep.append(MissingArg(param, param_type))\n\n    if matched_kwargs != len(kwargs):\n        # extra kwargs?\n        return None\n\n    # Bind POSITIONAL arguments (compiled to go after NAMED ONLY arguments).\n    while True:\n        if ai < nargs:\n            arg_type, arg_val = args[ai]\n            ai += 1\n\n            if pi >= nparams:\n                # too many positional arguments\n                return None\n            param = params[pi]\n            param_shortname = param.get_parameter_name(schema)\n            param_type = param.get_type(schema)\n            param_typemod = param.get_typemod(schema)\n            param_kind = param.get_kind(schema)\n            pi += 1\n\n            if param_kind is _NAMED_ONLY:\n                # impossible condition\n                raise RuntimeError('unprocessed NAMED ONLY parameter')\n\n            if param_kind is _VARIADIC:\n                param_type = cast(s_types.Array, param_type)\n                var_type = param_type.get_subtypes(schema)[0]\n                cd = _get_cast_distance(arg_val, arg_type, var_type)\n                if cd < 0:\n                    return None\n\n                bound_args_prep.append(\n                    PassedArg(\n                        name=param_shortname,\n                        orig_param_type=param_type,\n                        param_type=param_type,\n                        param_typemod=param_typemod,\n                        param_kind=param_kind,\n                        val=arg_val,\n                        valtype=arg_type,\n                        cast_distance=cd,\n                        arg_id=ai - 1,\n                    )\n                )\n\n                for di, (arg_type, arg_val) in enumerate(args[ai:]):\n                    cd = _get_cast_distance(arg_val, arg_type, var_type)\n                    if cd < 0:\n                        return None\n\n                    bound_args_prep.append(\n                        PassedArg(\n                            name=param_shortname,\n                            orig_param_type=param_type,\n                            param_type=param_type,\n                            param_typemod=param_typemod,\n                            param_kind=param_kind,\n                            val=arg_val,\n                            valtype=arg_type,\n                            cast_distance=cd,\n                            arg_id=ai + di,\n                        )\n                    )\n\n                variadic_arg_id = ai - 1\n                variadic_arg_count = nargs - ai + 1\n\n                break\n\n            cd = _get_cast_distance(arg_val, arg_type, param_type)\n            if cd < 0:\n                return None\n\n            bound_args_prep.append(\n                PassedArg(\n                    name=param_shortname,\n                    orig_param_type=param_type,\n                    param_type=param_type,\n                    param_typemod=param_typemod,\n                    param_kind=param_kind,\n                    val=arg_val,\n                    valtype=arg_type,\n                    cast_distance=cd,\n                    arg_id=ai - 1,\n                )\n            )\n\n        else:\n            break\n\n    # Handle yet unprocessed POSITIONAL & VARIADIC arguments.\n    for i in range(pi, nparams):\n        param = params[i]\n        param_type = param.get_type(schema)\n        param_kind = param.get_kind(schema)\n\n        if param_kind is _POSITIONAL:\n            if param.get_default(schema) is None:\n                # required positional parameter that we don't have a\n                # positional argument for.\n                return None\n\n            has_missing_args = True\n            bound_args_prep.append(MissingArg(param, param_type))\n\n        elif param_kind is _VARIADIC:\n            variadic_arg_id = i\n            variadic_arg_count = 0\n\n        elif param_kind is _NAMED_ONLY:\n            # impossible condition\n            raise RuntimeError('unprocessed NAMED ONLY parameter')\n\n    # Populate defaults.\n    defaults_mask = 0\n    null_args: set[str] = set()\n    bound_param_args: list[BoundArg] = []\n    if has_missing_args:\n        if has_inlined_defaults or named_only:\n            for i, prep_barg in enumerate(bound_args_prep):\n                if isinstance(prep_barg, PassedArg):\n                    bound_param_args.append(prep_barg)\n                    continue\n                if prep_barg.param is None:\n                    # Shouldn't be possible; the code above takes care of this.\n                    raise RuntimeError(\n                        f'failed to resolve the parameter for the arg #{i}')\n\n                param = prep_barg.param\n                param_shortname = param.get_parameter_name(schema)\n                param_type = param.get_type(schema)\n                param_typemod = param.get_typemod(schema)\n                param_kind = param.get_kind(schema)\n\n                null_args.add(param_shortname)\n\n                defaults_mask |= 1 << i\n\n                if not has_inlined_defaults:\n                    param_default: Optional[s_expr.Expression] = (\n                        param.get_default(schema)\n                    )\n                    assert param_default is not None\n                    default = compile_arg(\n                        param_default.parse(), param_typemod, ctx=ctx)\n\n                empty_default = (\n                    has_inlined_defaults or\n                    irutils.is_empty(default)\n                )\n\n                if empty_default and not basic_matching_only:\n                    default_type = None\n\n                    if param_type.is_any(schema):\n                        if resolved_poly_base_type is None:\n                            raise errors.QueryError(\n                                f'could not resolve \"anytype\" type for the '\n                                f'${param_shortname} parameter')\n                        else:\n                            default_type = resolved_poly_base_type\n                    else:\n                        default_type = param_type\n\n                else:\n                    default_type = param_type\n\n                if has_inlined_defaults:\n                    default = compile_arg(\n                        qlast.TypeCast(\n                            expr=qlast.Set(elements=[]),\n                            type=typegen.type_to_ql_typeref(\n                                default_type,\n                                ctx=ctx,\n                            ),\n                        ),\n                        ft.TypeModifier.OptionalType,\n                        ctx=ctx,\n                    )\n\n                default = setgen.ensure_set(\n                    default,\n                    typehint=default_type,\n                    ctx=ctx,\n                )\n\n                bound_param_args.append(\n                    DefaultArg(\n                        name=param_shortname,\n                        orig_param_type=param_type,\n                        param_type=param_type,\n                        param_typemod=param_typemod,\n                        param_kind=param_kind,\n                        val=default,\n                        valtype=param_type,\n                    )\n                )\n\n        else:\n            bound_param_args = [\n                barg for barg in bound_args_prep if isinstance(barg, PassedArg)\n            ]\n    else:\n        bound_param_args = cast(list[BoundArg], bound_args_prep)\n\n    if has_inlined_defaults:\n        # If we are compiling an EdgeQL function, inject the defaults\n        # bit-mask as a first argument.\n        bytes_t = ctx.env.get_schema_type_and_track(\n            sn.QualName('std', 'bytes'))\n        bm = defaults_mask.to_bytes(nparams // 8 + 1, 'little')\n        typeref = typegen.type_to_typeref(bytes_t, env=ctx.env)\n        bm_set = setgen.ensure_set(\n            irast.BytesConstant(value=bm, typeref=typeref),\n            typehint=bytes_t, ctx=ctx)\n        bound_param_args.insert(\n            0,\n            DefaultBitmask(\n                val=bm_set,\n            ),\n        )\n\n    return_polymorphism = ft.Polymorphism.NotUsed\n    if return_type.is_polymorphic(schema):\n        return_polymorphism = ft.Polymorphism.from_schema_type(return_type)\n\n        if resolved_poly_base_type is not None:\n            ctx.env.schema, return_type = return_type.to_nonpolymorphic(\n                ctx.env.schema, resolved_poly_base_type)\n        elif not in_polymorphic_func and not basic_matching_only:\n            return None\n\n    # resolved_poly_base_type may be legitimately None within\n    # bodies of polymorphic functions\n    if resolved_poly_base_type is not None:\n        for i, barg in enumerate(bound_param_args):\n            if (\n                isinstance(barg, ValueArg)\n                and barg.param_type.is_polymorphic(schema)\n            ):\n                ctx.env.schema, ptype = barg.param_type.to_nonpolymorphic(\n                    ctx.env.schema, resolved_poly_base_type)\n                polymorphism = ft.Polymorphism.from_schema_type(barg.param_type)\n                bound_param_args[i] = dataclasses.replace(\n                    barg,\n                    param_type=ptype,\n                    polymorphism=polymorphism,\n                )\n\n    return BoundCall(\n        func=func,\n        args=bound_param_args,\n        null_args=null_args,\n        return_type=return_type,\n        variadic_arg_id=variadic_arg_id,\n        variadic_arg_count=variadic_arg_count,\n        return_polymorphism=return_polymorphism,\n        server_param_conversions=server_param_conversions,\n    )\n\n\ndef _check_server_arg_conversion(\n    func: s_func.CallableLike,\n    args: list[tuple[s_types.Type, irast.Set]],\n    kwargs: dict[str, tuple[s_types.Type, irast.Set]],\n    *,\n    ctx: context.ContextLevel,\n) -> Optional[tuple[\n    Sequence[tuple[s_types.Type, irast.Set]],\n    Mapping[str, tuple[s_types.Type, irast.Set]],\n    dict[str, dict[str, context.ServerParamConversion]],\n]]:\n    \"\"\"Check if there is a server param conversion and get the effective args.\n\n    Server param conversion allows the server to replace a function arg with\n    another parameter which it computes before executing the query.\n\n    For example when `ext::ai::search(anyobject, str)` is called, the server\n    gets an embedding vector for string arg which it then substitutes into a\n    call to `ext::ai::search(anyobject, array<float32>)`.\n\n    If any conversions are applied, returns (args, kwargs) with new query\n    parameters representing the converted parameters.\n    \"\"\"\n    schema = ctx.env.schema\n\n    func_params: s_func.FuncParameterList = cast(\n        s_func.FuncParameterList,\n        func.get_params(schema),\n    )\n\n    if arg_conversions_json := (\n        isinstance(func, s_func.Function)\n        and func.get_server_param_conversions(schema)\n    ):\n        curr_server_param_conversions: dict[\n            str,\n            dict[str, context.ServerParamConversion],\n        ] = {}\n\n        arg_conversions: dict[str, str | list[str]] = json.loads(\n            arg_conversions_json\n        )\n        for arg_name, conversion_info in arg_conversions.items():\n            if isinstance(conversion_info, str):\n                conversion_name = conversion_info\n            else:\n                conversion_name = conversion_info[0]\n\n            # Get the arg being converted\n            arg_key: int | str\n            arg: tuple[s_types.Type, irast.Set]\n            param, arg_key, arg = _get_arg(\n                func_params,\n                arg_name,\n                args,\n                kwargs,\n                error_msg=f'Server param conversion {conversion_name} error',\n                schema=schema,\n            )\n\n            if arg[1].expr is None:\n                # Dummy set, do nothing\n                continue\n\n            original_type: s_types.Type = arg[0].material_type(schema)[1]\n            if original_type != param.get_type(schema):\n                # Wrong param type, function candidate doesn't apply.\n                # TODO: Check \"any\" params\n                return None\n\n            is_param_query_parameter = (\n                isinstance(arg[1].expr, irast.QueryParameter)\n                and not arg[1].expr.is_global\n            )\n            is_param_ir_constant = isinstance(arg[1].expr, irast.BaseConstant)\n            if (\n                not original_type.is_array()\n                and not is_param_query_parameter\n                and not is_param_ir_constant\n            ):\n                raise errors.QueryError(\n                    f\"Argument '{arg_name}' \"\n                    f\"must be a constant or query parameter\",\n                    span=arg[1].expr.span,\n                )\n            elif (\n                original_type.is_array()\n                and not is_param_query_parameter\n            ):\n                # Array literals are normalized as expressions\n                # For now, don't support them as constants\n                raise errors.QueryError(\n                    f\"Argument '{arg_name}' must be a query parameter\",\n                    span=arg[1].expr.span,\n                )\n\n            # Get info about the conversion\n            converted_type, additional_info, conversion_volatility = (\n                _resolve_server_param_conversion(\n                    func_params,\n                    args,\n                    kwargs,\n                    conversion_name,\n                    schema=schema,\n                    conversion_info=(\n                        conversion_info\n                        if isinstance(conversion_info, list)\n                        else None\n                    )\n                )\n            )\n\n            query_param_name: str\n            constant_value: Optional[Any] = None\n            if isinstance(arg[1].expr, irast.BaseConstant):\n                # Currently only support str constants\n                constant_expr = arg[1].expr\n                if isinstance(constant_expr, irast.StringConstant):\n                    constant_value = constant_expr.value\n                elif isinstance(\n                    constant_expr, (irast.IntegerConstant, irast.BigintConstant)\n                ):\n                    constant_value = int(constant_expr.value)\n                elif isinstance(\n                    constant_expr, (irast.FloatConstant, irast.DecimalConstant)\n                ):\n                    constant_value = float(constant_expr.value)\n                else:\n                    raise RuntimeError(\n                        f'Unsupported constant argument: {arg_name}'\n                    )\n                # Use a hash of the text value as the name\n                value_hash = (\n                    hashlib.sha1(constant_expr.value.encode()).hexdigest()\n                )\n                query_param_name = f'const_{value_hash}'\n            elif isinstance(arg[1].expr, irast.QueryParameter):\n                query_param_name = arg[1].expr.name\n            else:\n                raise RuntimeError('Server param conversion has no parameter')\n\n            # Create a substitute parameter set with the correct type\n            existing_converted_path_id = None\n            if (\n                (curr_conversions := (\n                    ctx.env.server_param_conversions.get(query_param_name, None)\n                ))\n                and (\n                    existing_param_conversion := (\n                        curr_conversions.get(conversion_name, None)\n                    )\n                )\n            ):\n                # If the param was converted in another call, reuse its path id\n                existing_converted_path_id = existing_param_conversion.path_id\n\n            converted_param_name = f'{query_param_name}~{conversion_name}'\n            converted_required = (\n                isinstance(arg[1].expr, irast.QueryParameter)\n                and arg[1].expr.required\n            )\n            converted_typeref = typegen.type_to_typeref(\n                converted_type, ctx.env\n            )\n            conversion_set: irast.Set = setgen.ensure_set(\n                irast.QueryParameter(\n                    name=converted_param_name,\n                    required=converted_required,\n                    typeref=converted_typeref,\n                    span=arg[1].span,\n                ),\n                path_id=existing_converted_path_id,\n                ctx=ctx,\n            )\n\n            if query_param_name not in curr_server_param_conversions:\n                curr_server_param_conversions[query_param_name] = {}\n            curr_conversions = (\n                curr_server_param_conversions[query_param_name]\n            )\n\n            if existing_converted_path_id is None:\n                # If this is the first time this conversion was applied to this\n                # query param, save the conversion to be possibly reused by\n                # another call.\n\n                # Create the sub-params in case the resulting converted param\n                # is a tuple. Currently, no such conversion exists, but this\n                # is here to prepare for that distant future.\n                sub_params = tuple_args.create_sub_params(\n                    converted_param_name,\n                    converted_required,\n                    typeref=converted_typeref,\n                    pt=converted_type,\n                    is_func_param=True,\n                    ctx=ctx\n                )\n\n                curr_conversions[conversion_name] = (\n                    context.ServerParamConversion(\n                        path_id=conversion_set.path_id,\n                        ir_param=irast.Param(\n                            name=converted_param_name,\n                            required=converted_required,\n                            schema_type=converted_type,\n                            ir_type=converted_typeref,\n                            sub_params=sub_params,\n                        ),\n                        additional_info=additional_info,\n                        volatility=conversion_volatility,\n                        script_param_index=(\n                            list(ctx.env.script_params.keys()).index(\n                                query_param_name\n                            )\n                            if query_param_name in ctx.env.script_params else\n                            None\n                        ),\n                        constant_value=constant_value,\n                    )\n                )\n\n                # Don't include the newly created irast.Param in\n                # ctx.env.query_parameters.\n                # Such parameters need to have a corresponding entry in\n                # compiler.Context.Environment.script_params\n                #\n                # The parameters will be handled separately in fini_expression\n                # and compile_ir_to_sql_tree.\n\n            # Substitute the old arg\n            if isinstance(arg_key, int):\n                args = args.copy()\n                args[arg_key] = (converted_type, conversion_set)\n            else:\n                kwargs = kwargs.copy()\n                kwargs[arg_key] = (converted_type, conversion_set)\n\n        if len(curr_server_param_conversions) != len(arg_conversions):\n            # Not all conversions were applied, function candidate doesn't\n            # apply.\n            return None\n\n        return args, kwargs, curr_server_param_conversions\n\n    else:\n        return None\n\n\ndef _resolve_server_param_conversion(\n    func_params: s_func.FuncParameterList,\n    args: list[tuple[s_types.Type, irast.Set]],\n    kwargs: dict[str, tuple[s_types.Type, irast.Set]],\n    conversion_name: str,\n    *,\n    schema: s_schema.Schema,\n    conversion_info: Optional[list[str]] = None,\n) -> tuple[\n    s_types.Type,\n    tuple[str, ...],\n    ft.Volatility,\n]:\n    converted_type: s_types.Type\n    additional_info: tuple[str, ...] = tuple()\n    conversion_volatility: ft.Volatility\n\n    if conversion_name == 'cast_int64_to_str':\n        converted_type = schema.get(\n            'std::str', type=s_scalars.ScalarType\n        )\n        conversion_volatility = ft.Volatility.Immutable\n\n    elif conversion_name == 'cast_int64_to_str_volatile':\n        converted_type = schema.get(\n            'std::str', type=s_scalars.ScalarType\n        )\n        conversion_volatility = ft.Volatility.Volatile\n\n    elif conversion_name == 'cast_int64_to_float64':\n        converted_type = schema.get(\n            'std::float64', type=s_scalars.ScalarType\n        )\n        conversion_volatility = ft.Volatility.Immutable\n\n    elif conversion_name == 'join_str_array':\n        assert conversion_info is not None\n        separator = conversion_info[1]\n\n        converted_type = schema.get(\n            'std::str', type=s_scalars.ScalarType\n        )\n        additional_info = (separator,)\n        conversion_volatility = ft.Volatility.Immutable\n\n    elif conversion_name == 'ai_text_embedding':\n        assert isinstance(conversion_info, list)\n        object_param_name = conversion_info[1]\n\n        converted_type = schema.get_global(\n            s_types.Array,\n            s_types.Array.generate_name(\n                sn.QualName('std', 'float32')\n            )\n        )\n\n        _, _, object_arg = _get_arg(\n            func_params,\n            object_param_name,\n            args,\n            kwargs,\n            error_msg=f'Server param conversion {conversion_name} '\n            f'error finding object argument',\n            schema=schema,\n        )\n\n        object_type = object_arg[0].material_type(schema)[1]\n        additional_info = (str(object_type.get_id(schema)),)\n        conversion_volatility = ft.Volatility.Volatile\n\n    else:\n        raise RuntimeError(\n            f'Unknown server param conversion: {conversion_name}'\n        )\n\n    return (\n        converted_type,\n        additional_info,\n        conversion_volatility,\n    )\n\n\ndef _get_arg(\n    func_params: s_func.FuncParameterList,\n    param_name: str,\n    args: Sequence[tuple[s_types.Type, irast.Set]],\n    kwargs: Mapping[str, tuple[s_types.Type, irast.Set]],\n    *,\n    error_msg: str,\n    schema: s_schema.Schema,\n) -> tuple[\n    s_func.Parameter,\n    int | str,\n    tuple[s_types.Type, irast.Set],\n]:\n    param = func_params.get_by_name(name=param_name, schema=schema)\n    if param is None:\n        raise RuntimeError(\n            f'{error_msg}: missing param \"{param_name}\"'\n        )\n\n    param_kind = param.get_kind(schema)\n    if param_kind == ft.ParameterKind.PositionalParam:\n        param_index: int = param.get_num(schema)\n        return param, param_index, args[param_index]\n    elif param_kind == ft.ParameterKind.NamedOnlyParam:\n        return param, param_name, kwargs[param_name]\n    else:\n        raise RuntimeError(\n            f'{error_msg}: variadic param \"{param_name}\" not allowed'\n        )\n\n\ndef compile_arg(\n    arg_ql: qlast.Expr,\n    typemod: ft.TypeModifier,\n    *,\n    prefer_subquery_args: bool=False,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n    fenced = typemod is ft.TypeModifier.SetOfType\n    optional = typemod is ft.TypeModifier.OptionalType\n\n    # Create a branch for OPTIONAL ones. The OPTIONAL branch is to\n    # have a place to mark as optional in the scope tree.\n    # For fenced arguments we instead wrap it in a SELECT below.\n    #\n    # We also put a branch when we are trying to compile the argument\n    # into a subquery, so that things it uses get bound locally.\n    branched = optional or (prefer_subquery_args and not fenced)\n\n    new = ctx.newscope(fenced=False) if branched else ctx.new()\n    with new as argctx:\n        if optional:\n            argctx.path_scope.mark_as_optional()\n\n        if fenced:\n            arg_ql = qlast.SelectQuery(\n                result=arg_ql, span=arg_ql.span,\n                implicit=True, rptr_passthrough=True)\n\n        argctx.implicit_limit = 0\n\n        arg_ir = dispatch.compile(arg_ql, ctx=argctx)\n\n        if optional:\n            pathctx.register_set_in_scope(arg_ir, optional=True, ctx=ctx)\n\n            if arg_ir.path_scope_id is None:\n                pathctx.assign_set_scope(arg_ir, argctx.path_scope, ctx=argctx)\n\n        elif branched:\n            arg_ir = setgen.scoped_set(arg_ir, ctx=argctx)\n\n        return arg_ir\n"
  },
  {
    "path": "edb/edgeql/compiler/schemactx.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL compiler schema helpers.\"\"\"\n\n\nfrom __future__ import annotations\n\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n    Iterable,\n    Sequence,\n    NamedTuple,\n    cast,\n)\n\nfrom edb import errors\n\nfrom edb.common import parsing\nfrom edb.ir import typeutils\n\nfrom edb.schema import links as s_links\nfrom edb.schema import name as sn\nfrom edb.schema import objects as s_obj\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import pseudo as s_pseudo\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import sources as s_sources\nfrom edb.schema import types as s_types\nfrom edb.schema import utils as s_utils\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom . import context\n\n\ndef get_schema_object(\n    ref: qlast.BaseObjectRef,\n    module: Optional[str]=None,\n    *,\n    item_type: Optional[type[s_obj.Object]]=None,\n    condition: Optional[Callable[[s_obj.Object], bool]]=None,\n    label: Optional[str]=None,\n    ctx: context.ContextLevel,\n    span: Optional[parsing.Span] = None,\n) -> s_obj.Object:\n\n    if isinstance(ref, qlast.ObjectRef):\n        if span is None:\n            span = ref.span\n        module = ref.module\n        lname = ref.name\n    elif isinstance(ref, qlast.PseudoObjectRef):\n        return s_pseudo.PseudoType.get(ctx.env.schema, ref.name)\n    else:\n        raise AssertionError(f\"Unhandled BaseObjectRef subclass: {ref!r}\")\n\n    name: sn.Name\n    if module:\n        name = sn.QualName(module=module, name=lname)\n    else:\n        name = sn.UnqualName(name=lname)\n\n    try:\n        stype = ctx.env.get_schema_object_and_track(\n            name=name,\n            expr=ref,\n            modaliases=ctx.modaliases,\n            type=item_type,\n            condition=condition,\n            label=label,\n        )\n\n    except errors.QueryError as e:\n        s_utils.enrich_schema_lookup_error(\n            e,\n            name,\n            modaliases=ctx.modaliases,\n            schema=ctx.env.schema,\n            item_type=item_type,\n            pointer_parent=_get_partial_path_prefix_type(ctx),\n            condition=condition,\n            span=span,\n        )\n        raise\n\n    if stype == ctx.defining_view:\n        # stype is the view in process of being defined and as such is\n        # not yet a valid schema object\n        raise errors.SchemaDefinitionError(\n            f'illegal self-reference in definition of {str(name)!r}',\n            span=span)\n\n    return stype\n\n\ndef _get_partial_path_prefix_type(\n    ctx: context.ContextLevel,\n) -> Optional[s_types.Type]:\n    if ctx is None:\n        return None\n    ppp = ctx.partial_path_prefix\n    if ppp is None or ppp.typeref is None:\n        return None\n\n    _, type = typeutils.ir_typeref_to_type(ctx.env.schema, ppp.typeref)\n    return type\n\n\ndef get_schema_type(\n    name: qlast.BaseObjectRef,\n    module: Optional[str] = None,\n    *,\n    ctx: context.ContextLevel,\n    label: Optional[str] = None,\n    condition: Optional[Callable[[s_obj.Object], bool]] = None,\n    item_type: Optional[type[s_obj.Object]] = None,\n    span: Optional[parsing.Span] = None,\n) -> s_types.Type:\n    if item_type is None:\n        item_type = s_types.Type\n    obj = get_schema_object(name, module, item_type=item_type,\n                            condition=condition, label=label,\n                            ctx=ctx, span=span)\n    assert isinstance(obj, s_types.Type)\n    return obj\n\n\ndef resolve_schema_name(\n    name: str, module: str, *, ctx: context.ContextLevel\n) -> Optional[sn.QualName]:\n    schema_module = ctx.modaliases.get(module)\n    if schema_module is None:\n        return None\n    else:\n        return sn.QualName(name=name, module=schema_module)\n\n\ndef preserve_view_shape(\n    base: s_types.Type | s_pointers.Pointer,\n    derived: s_types.Type | s_pointers.Pointer,\n    *,\n    derived_name_base: Optional[sn.Name] = None,\n    ctx: context.ContextLevel,\n) -> None:\n    \"\"\"Copy a view shape to a child type, updating the pointers\"\"\"\n    new = []\n    schema = ctx.env.schema\n    for ptr, op in ctx.env.view_shapes[base]:\n        target = ptr.get_target(ctx.env.schema)\n        assert target\n        schema, nptr = ptr.get_derived(\n            schema, cast(s_sources.Source, derived), target,\n            derived_name_base=derived_name_base)\n        new.append((nptr, op))\n    ctx.env.view_shapes[derived] = new\n    if isinstance(base, s_types.Type) and isinstance(derived, s_types.Type):\n        ctx.env.view_shapes_metadata[derived] = (\n            ctx.env.view_shapes_metadata[base]).replace()\n\n    # All of the pointers should already exist, so nothing should have\n    # been created.\n    assert schema is ctx.env.schema\n\n\ndef derive_view(\n    stype: s_types.Type,\n    *,\n    derived_name: Optional[sn.QualName] = None,\n    derived_name_quals: Optional[Sequence[str]] = (),\n    preserve_shape: bool = False,\n    exprtype: s_types.ExprType = s_types.ExprType.Select,\n    inheritance_merge: bool = True,\n    attrs: Optional[dict[str, Any]] = None,\n    ctx: context.ContextLevel,\n) -> s_types.Type:\n\n    if derived_name is None:\n        if isinstance(stype, s_obj.DerivableObject):\n            derived_name = derive_view_name(\n                stype=stype, derived_name_quals=derived_name_quals,\n                ctx=ctx)\n        else:\n            derived_name = sn.QualName('__derived__', ctx.aliases.get('v'))\n\n    if attrs is None:\n        attrs = {}\n    else:\n        attrs = dict(attrs)\n\n    attrs['expr_type'] = exprtype\n\n    derived: s_types.Type\n\n    if isinstance(stype, s_types.Collection):\n        ctx.env.schema, derived = stype.derive_subtype(\n            ctx.env.schema,\n            name=derived_name,\n            attrs=attrs,\n        )\n\n    elif isinstance(stype, (s_objtypes.ObjectType, s_scalars.ScalarType)):\n        existing = ctx.env.schema.get(\n            derived_name, default=None, type=type(stype))\n        if existing is not None:\n            if ctx.recompiling_schema_alias:\n                # When recompiling schema alias, we, essentially\n                # re-derive the already-existing objects exactly.\n                derived = existing\n            else:\n                raise AssertionError(\n                    f'{type(stype).get_schema_class_displayname()}'\n                    f' {derived_name!r} already exists',\n                )\n        else:\n            ctx.env.schema, derived = stype.derive_subtype(\n                ctx.env.schema,\n                name=derived_name,\n                inheritance_merge=inheritance_merge,\n                inheritance_refdicts={'pointers'},\n                mark_derived=True,\n                transient=True,\n                # When compiling aliases, we can't elide\n                # @source/@target pointers, which normally we would\n                # when creating a view.\n                preserve_endpoint_ptrs=ctx.env.options.schema_view_mode,\n                attrs=attrs,\n                stdmode=ctx.env.options.bootstrap_mode,\n            )\n\n        if (\n            stype.is_view(ctx.env.schema)\n            # XXX: Previously, the main check here was just for\n            # (not stype.is_non_concrete(...)). is_non_concrete isn't really the\n            # right way to figure out if something is a view, since\n            # some aliases will be generic. On changing it to is_view\n            # instead, though, two GROUP BY tests that grouped\n            # on the result of a group broke\n            # (test_edgeql_group_by_group_by_03{a,b}).\n            #\n            # It's probably a bug that this matters in that case, and\n            # it is an accident that group bindings are named in such\n            # a way that they count as being generic, but for now\n            # preserve that behavior.\n            and not (\n                stype.is_non_concrete(ctx.env.schema)\n                and (view_ir := ctx.view_sets.get(stype))\n                and (scope_info := ctx.env.path_scope_map.get(view_ir))\n                and scope_info.binding_kind\n            )\n            and isinstance(derived, s_objtypes.ObjectType)\n        ):\n            assert isinstance(stype, s_objtypes.ObjectType)\n            scls_pointers = stype.get_pointers(ctx.env.schema)\n            derived_own_pointers = derived.get_pointers(ctx.env.schema)\n\n            for pn, ptr in derived_own_pointers.items(ctx.env.schema):\n                # This is a view of a view.  Make sure query-level\n                # computable expressions for pointers are carried over.\n                src_ptr = scls_pointers.get(ctx.env.schema, pn)\n                computable_data = (\n                    ctx.env.source_map.get(src_ptr) if src_ptr else None)\n                if computable_data is not None:\n                    ctx.env.source_map[ptr] = computable_data\n\n                if src_ptr in ctx.env.pointer_specified_info:\n                    ctx.env.pointer_derivation_map[src_ptr].append(ptr)\n\n    else:\n        raise TypeError(\"unsupported type in derive_view\")\n\n    ctx.view_nodes[derived.get_name(ctx.env.schema)] = derived\n\n    if preserve_shape and stype in ctx.env.view_shapes:\n        preserve_view_shape(stype, derived, ctx=ctx)\n\n    return derived\n\n\ndef derive_ptr(\n    ptr: s_pointers.Pointer,\n    source: s_sources.Source,\n    target: Optional[s_types.Type] = None,\n    *qualifiers: str,\n    derived_name: Optional[sn.QualName] = None,\n    derived_name_quals: Optional[Sequence[str]] = (),\n    preserve_shape: bool = False,\n    derive_backlink: bool = False,\n    inheritance_merge: bool = True,\n    attrs: Optional[dict[str, Any]] = None,\n    ctx: context.ContextLevel,\n) -> s_pointers.Pointer:\n\n    if derived_name is None and ctx.derived_target_module:\n        derived_name = derive_view_name(\n            stype=ptr, derived_name_quals=derived_name_quals, ctx=ctx)\n\n    if ptr.get_name(ctx.env.schema) == derived_name:\n        qualifiers = qualifiers + (ctx.aliases.get('d'),)\n\n    # If we are deriving a backlink, we just register that instead of\n    # actually deriving from it.\n    if derive_backlink:\n        attrs = attrs.copy() if attrs else {}\n        attrs['computed_link_alias'] = ptr\n        attrs['computed_link_alias_is_backward'] = True\n        ptr = ctx.env.schema.get('std::link', type=s_pointers.Pointer)\n\n    ctx.env.schema, derived = ptr.derive_ref(\n        ctx.env.schema,\n        source,\n        *qualifiers,\n        target=target,\n        name=derived_name,\n        inheritance_merge=inheritance_merge,\n        inheritance_refdicts={'pointers'},\n        mark_derived=True,\n        transient=True,\n        # When compiling aliases, we can't elide\n        # @source/@target pointers, which normally we would\n        # when creating a view.\n        preserve_endpoint_ptrs=ctx.env.options.schema_view_mode,\n        attrs=attrs,\n    )\n\n    if not ptr.is_non_concrete(ctx.env.schema):\n        if isinstance(derived, s_sources.Source):\n            ptr = cast(s_links.Link, ptr)\n            scls_pointers = ptr.get_pointers(ctx.env.schema)\n            derived_own_pointers = derived.get_pointers(ctx.env.schema)\n\n            for pn, ptr in derived_own_pointers.items(ctx.env.schema):\n                # This is a view of a view.  Make sure query-level\n                # computable expressions for pointers are carried over.\n                src_ptr = scls_pointers.get(ctx.env.schema, pn)\n                # mypy somehow loses the type argument in the\n                # \"pointers\" ObjectIndex.\n                assert isinstance(src_ptr, s_pointers.Pointer)\n                computable_data = ctx.env.source_map.get(src_ptr)\n                if computable_data is not None:\n                    ctx.env.source_map[ptr] = computable_data\n\n    if preserve_shape and ptr in ctx.env.view_shapes:\n        preserve_view_shape(ptr, derived, ctx=ctx)\n\n    return derived\n\n\ndef derive_view_name(\n    stype: Optional[s_obj.DerivableObject],\n    derived_name_quals: Optional[Sequence[str]] = (),\n    derived_name_base: Optional[sn.Name] = None,\n    *,\n    ctx: context.ContextLevel,\n) -> sn.QualName:\n    if not derived_name_quals:\n        derived_name_quals = (ctx.aliases.get('view'),)\n\n    if ctx.derived_target_module:\n        derived_name_module = ctx.derived_target_module\n    else:\n        derived_name_module = '__derived__'\n\n    return s_obj.derive_name(\n        ctx.env.schema,\n        *derived_name_quals,\n        module=derived_name_module,\n        derived_name_base=derived_name_base,\n        parent=stype,\n    )\n\n\ndef get_union_type[TypeT: s_types.Type](\n    types: Sequence[TypeT],\n    *,\n    opaque: bool = False,\n    preserve_derived: bool = False,\n    ctx: context.ContextLevel,\n    span: Optional[parsing.Span] = None,\n) -> TypeT:\n\n    targets: Sequence[s_types.Type]\n    if preserve_derived:\n        targets = s_utils.simplify_union_types_preserve_derived(\n            ctx.env.schema, types\n        )\n    else:\n        targets = s_utils.simplify_union_types(\n            ctx.env.schema, types\n        )\n\n    try:\n        ctx.env.schema, union, _ = s_utils.ensure_union_type(\n            ctx.env.schema, targets,\n            opaque=opaque, transient=True)\n    except errors.SchemaError as e:\n        union_name = (\n            '(' + ' | '.join(sorted(\n            t.get_displayname(ctx.env.schema)\n            for t in types\n            )) + ')'\n        )\n        e.args = (\n            (f'cannot create union {union_name} {e.args[0]}',)\n            + e.args[1:]\n        )\n        e.set_span(span)\n        raise e\n\n    if (\n        not isinstance(union, s_obj.QualifiedObject)\n        or union.get_name(ctx.env.schema).module != '__derived__'\n    ):\n        ctx.env.add_schema_ref(union, expr=None)\n\n    return cast(TypeT, union)\n\n\ndef get_intersection_type[TypeT: s_types.Type](\n    types: Sequence[TypeT],\n    *,\n    ctx: context.ContextLevel,\n) -> TypeT:\n\n    targets: Sequence[s_types.Type]\n    targets = s_utils.simplify_intersection_types(ctx.env.schema, types)\n    ctx.env.schema, intersection, _ = s_utils.ensure_intersection_type(\n        ctx.env.schema, targets, transient=True\n    )\n\n    if (\n        not isinstance(intersection, s_obj.QualifiedObject)\n        or intersection.get_name(ctx.env.schema).module != '__derived__'\n    ):\n        ctx.env.add_schema_ref(intersection, expr=None)\n\n    return cast(TypeT, intersection)\n\n\ndef get_material_type[TypeT: s_types.Type](\n    t: TypeT,\n    *,\n    ctx: context.ContextLevel,\n) -> TypeT:\n\n    ctx.env.schema, mtype = t.material_type(ctx.env.schema)\n    return mtype\n\n\ndef concretify[TypeT: s_types.Type](\n    t: TypeT,\n    *,\n    ctx: context.ContextLevel,\n) -> TypeT:\n    \"\"\"Produce a version of t with all views removed.\n\n    This procedes recursively through unions and intersections,\n    which can result in major simplifications with intersection types\n    in particular.\n    \"\"\"\n    t = get_material_type(t, ctx=ctx)\n    if els := t.get_union_of(ctx.env.schema):\n        ts = [concretify(e, ctx=ctx) for e in els.objects(ctx.env.schema)]\n        return get_union_type(ts, ctx=ctx)\n    if els := t.get_intersection_of(ctx.env.schema):\n        ts = [concretify(e, ctx=ctx) for e in els.objects(ctx.env.schema)]\n        return get_intersection_type(ts, ctx=ctx)\n    return t\n\n\ndef get_all_concrete(\n    stype: s_objtypes.ObjectType, *, ctx: context.ContextLevel\n) -> set[s_objtypes.ObjectType]:\n    if union := stype.get_union_of(ctx.env.schema):\n        return {\n            x\n            for t in union.objects(ctx.env.schema)\n            for x in get_all_concrete(t, ctx=ctx)\n        }\n    elif intersection := stype.get_intersection_of(ctx.env.schema):\n        return set.intersection(*(\n            get_all_concrete(t, ctx=ctx)\n            for t in intersection.objects(ctx.env.schema)\n        ))\n    return {stype} | {\n        x for x in stype.descendants(ctx.env.schema)\n        if x.is_material_object_type(ctx.env.schema)\n    }\n\n\nclass TypeIntersectionResult(NamedTuple):\n\n    stype: s_types.Type\n    is_empty: bool = False\n    is_subtype: bool = False\n\n\ndef apply_intersection(\n    left: s_types.Type, right: s_types.Type, *, ctx: context.ContextLevel\n) -> TypeIntersectionResult:\n    \"\"\"Compute an intersection of two types: *left* and *right*.\n\n    Returns:\n        A :class:`~TypeIntersectionResult` named tuple containing the\n        result intersection type, whether the type system considers\n        the intersection empty and whether *left* is related to *right*\n        (i.e either is a subtype of another).\n    \"\"\"\n\n    if left.issubclass(ctx.env.schema, right):\n        # The intersection type is a proper *superclass*\n        # of the argument, then this is, effectively, a NOP.\n        return TypeIntersectionResult(stype=left)\n\n    if right.issubclass(ctx.env.schema, left):\n        # The intersection type is a proper *subclass* and can be directly\n        # narrowed.\n        return TypeIntersectionResult(\n            stype=right,\n            is_empty=False,\n            is_subtype=True,\n        )\n\n    if (\n        left.get_is_opaque_union(ctx.env.schema)\n        and (left_union := left.get_union_of(ctx.env.schema))\n    ):\n        # Expose any opaque union types before continuing with the intersection.\n        # The schema does not yet fully implement type intersections since there\n        # is no `IntersectionTypeShell`. As a result, some intersections\n        # produced while compiling the standard library cannot be resolved.\n        left = get_union_type(left_union.objects(ctx.env.schema), ctx=ctx)\n\n    int_type: s_types.Type = get_intersection_type([left, right], ctx=ctx)\n    is_empty: bool = (\n        not s_utils.expand_type_expr_descendants(int_type, ctx.env.schema)\n    )\n    is_subtype: bool = int_type.issubclass(ctx.env.schema, left)\n\n    return TypeIntersectionResult(\n        stype=int_type,\n        is_empty=is_empty,\n        is_subtype=is_subtype,\n    )\n\n\ndef derive_dummy_ptr(\n    ptr: s_pointers.Pointer,\n    *,\n    ctx: context.ContextLevel,\n) -> s_pointers.Pointer:\n    stdobj = ctx.env.schema.get('std::BaseObject', type=s_objtypes.ObjectType)\n    derived_obj_name = stdobj.get_derived_name(\n        ctx.env.schema, stdobj, module='__derived__')\n    derived_obj = ctx.env.schema.get(\n        derived_obj_name, None, type=s_obj.QualifiedObject)\n    if derived_obj is None:\n        ctx.env.schema, derived_obj = stdobj.derive_subtype(\n            ctx.env.schema, name=derived_obj_name)\n\n    derived_name = ptr.get_derived_name(\n        ctx.env.schema, derived_obj)\n\n    derived: s_pointers.Pointer\n    derived = cast(s_pointers.Pointer, ctx.env.schema.get(derived_name, None))\n    if derived is None:\n        ctx.env.schema, derived = ptr.derive_ref(\n            ctx.env.schema,\n            derived_obj,\n            target=derived_obj,\n            attrs={\n                'cardinality': qltypes.SchemaCardinality.One,\n            },\n            name=derived_name,\n            mark_derived=True,\n        )\n\n    return derived\n\n\ndef get_union_pointer(\n    *,\n    ptrname: sn.UnqualName,\n    source: s_sources.Source,\n    direction: s_pointers.PointerDirection,\n    components: Iterable[s_pointers.Pointer],\n    opaque: bool = False,\n    modname: Optional[str] = None,\n    ctx: context.ContextLevel,\n) -> s_pointers.Pointer:\n\n    ctx.env.schema, ptr = s_pointers.get_or_create_union_pointer(\n        ctx.env.schema,\n        ptrname,\n        source,\n        direction=direction,\n        components=components,\n        opaque=opaque,\n        modname=modname,\n        transient=True,\n    )\n    return ptr\n"
  },
  {
    "path": "edb/edgeql/compiler/setgen.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL set compilation functions.\"\"\"\n\n\nfrom __future__ import annotations\n\nfrom typing import (\n    Any,\n    Callable,\n    Final,\n    Literal,\n    Optional,\n    AbstractSet,\n    ContextManager,\n    Iterable,\n    Iterator,\n    Sequence,\n    NoReturn,\n    TYPE_CHECKING,\n)\n\nimport contextlib\nimport enum\n\nfrom edb import errors\n\nfrom edb.common import levenshtein\nfrom edb.common.typeutils import downcast, not_none\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtyputils\nfrom edb.ir import utils as irutils\n\nfrom edb.schema import constraints as s_constr\nfrom edb.schema import globals as s_globals\nfrom edb.schema import indexes as s_indexes\nfrom edb.schema import links as s_links\nfrom edb.schema import name as s_name\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import permissions as s_permissions\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import pseudo as s_pseudo\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import sources as s_sources\nfrom edb.schema import types as s_types\nfrom edb.schema import utils as s_utils\nfrom edb.schema import expr as s_expr\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\nfrom edb.edgeql import parser as qlparser\n\nfrom . import astutils\nfrom . import casts\nfrom . import context\nfrom . import dispatch\nfrom . import inference\nfrom . import pathctx\nfrom . import schemactx\nfrom . import stmtctx\nfrom . import typegen\n\nif TYPE_CHECKING:\n    from edb.schema import objects as s_obj\n\n\nPtrDir = s_pointers.PointerDirection\n\n\ndef new_set(\n    *,\n    stype: s_types.Type,\n    expr: irast.Expr,\n    ctx: context.ContextLevel,\n    ircls: type[irast.Set] = irast.Set,\n    **kwargs: Any,\n) -> irast.Set:\n    \"\"\"Create a new ir.Set instance with given attributes.\n\n    Absolutely all ir.Set instances must be created using this\n    constructor.\n    \"\"\"\n\n    ignore_rewrites: bool = kwargs.get('ignore_rewrites', False)\n\n    skip_subtypes = False\n    if isinstance(expr, irast.TypeRoot):\n        skip_subtypes = expr.skip_subtypes\n\n    rw_key = (stype, skip_subtypes)\n\n    if not ignore_rewrites and ctx.suppress_rewrites:\n        from . import policies\n        ignore_rewrites = kwargs['ignore_rewrites'] = (\n            policies.should_ignore_rewrite(stype, ctx=ctx))\n\n    if (\n        not ignore_rewrites\n        and rw_key not in ctx.env.type_rewrites\n        and isinstance(stype, s_objtypes.ObjectType)\n        and ctx.env.options.apply_query_rewrites\n    ):\n        from . import policies\n        policies.try_type_rewrite(stype, skip_subtypes=skip_subtypes, ctx=ctx)\n\n    if (\n        not ignore_rewrites\n        and ctx.env.type_rewrites.get(rw_key)\n    ):\n        ctx.env.policy_use_count += 1\n\n    typeref = typegen.type_to_typeref(stype, env=ctx.env)\n    ir_set = ircls(typeref=typeref, expr=expr, **kwargs)\n    ctx.env.set_types[ir_set] = stype\n    return ir_set\n\n\ndef new_empty_set(\n    *,\n    stype: Optional[s_types.Type]=None, alias: str='e',\n    ctx: context.ContextLevel,\n    span: Optional[qlast.Span]=None\n) -> irast.Set:\n    if stype is None:\n        stype = s_pseudo.PseudoType.get(ctx.env.schema, 'anytype')\n        if span is not None:\n            ctx.env.type_origins[stype] = span\n\n    typeref = typegen.type_to_typeref(stype, env=ctx.env)\n    path_id = pathctx.get_expression_path_id(stype, alias, ctx=ctx)\n    ir_set = irast.Set(\n        path_id=path_id,\n        typeref=typeref,\n        expr=irast.EmptySet(typeref=typeref),\n    )\n    ctx.env.set_types[ir_set] = stype\n    return ir_set\n\n\ndef get_set_type(\n    ir_set: irast.Set, *, ctx: context.ContextLevel\n) -> s_types.Type:\n    return ctx.env.set_types[ir_set]\n\n\ndef get_expr_type(\n    ir: irast.Set | irast.Expr, *, ctx: context.ContextLevel\n) -> s_types.Type:\n    return typegen.type_from_typeref(ir.typeref, env=ctx.env)\n\n\nclass KeepCurrentT(enum.Enum):\n    KeepCurrent = 0\n\n\nKeepCurrent: Final = KeepCurrentT.KeepCurrent\n\n\ndef new_set_from_set(\n        ir_set: irast.Set, *,\n        merge_current_ns: bool=False,\n        path_scope_id: Optional[int | KeepCurrentT]=KeepCurrent,\n        path_id: Optional[irast.PathId]=None,\n        stype: Optional[s_types.Type]=None,\n        expr: irast.Expr | KeepCurrentT=KeepCurrent,\n        span: Optional[qlast.Span]=None,\n        is_binding: Optional[irast.BindingKind]=None,\n        is_schema_alias: Optional[bool]=None,\n        is_materialized_ref: Optional[bool]=None,\n        is_visible_binding_ref: Optional[bool]=None,\n        ignore_rewrites: Optional[bool]=None,\n        is_factoring_protected: Optional[bool]=None,\n        ctx: context.ContextLevel) -> irast.Set:\n    \"\"\"Create a new ir.Set from another ir.Set.\n\n    The new Set inherits source everything from the old set that\n    is not overriden.\n\n    If *merge_current_ns* is set, the new Set's path_id will be\n    namespaced with the currently active scope namespace.\n    \"\"\"\n    if path_id is None:\n        path_id = ir_set.path_id\n    if merge_current_ns:\n        path_id = path_id.merge_namespace(ctx.path_id_namespace)\n    if stype is None:\n        stype = get_set_type(ir_set, ctx=ctx)\n    if path_scope_id == KeepCurrent:\n        path_scope_id = ir_set.path_scope_id\n    if expr == KeepCurrent:\n        expr = ir_set.expr\n    if span is None:\n        span = ir_set.span\n    if is_binding is None:\n        is_binding = ir_set.is_binding\n    if is_schema_alias is None:\n        is_schema_alias = ir_set.is_schema_alias\n    if is_materialized_ref is None:\n        is_materialized_ref = ir_set.is_materialized_ref\n    if is_visible_binding_ref is None:\n        is_visible_binding_ref = ir_set.is_visible_binding_ref\n    if ignore_rewrites is None:\n        ignore_rewrites = ir_set.ignore_rewrites\n    if is_factoring_protected is None:\n        is_factoring_protected = ir_set.is_factoring_protected\n    return new_set(\n        path_id=path_id,\n        path_scope_id=path_scope_id,\n        stype=stype,\n        expr=expr,\n        span=span,\n        is_binding=is_binding,\n        is_schema_alias=is_schema_alias,\n        is_materialized_ref=is_materialized_ref,\n        is_visible_binding_ref=is_visible_binding_ref,\n        ignore_rewrites=ignore_rewrites,\n        is_factoring_protected=is_factoring_protected,\n        ircls=type(ir_set),\n        ctx=ctx,\n    )\n\n\ndef new_tuple_set(\n    elements: list[irast.TupleElement],\n    *,\n    named: bool,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    element_types = {el.name: get_set_type(el.val, ctx=ctx) for el in elements}\n    ctx.env.schema, stype = s_types.Tuple.create(\n        ctx.env.schema, element_types=element_types, named=named)\n    result_path_id = pathctx.get_expression_path_id(stype, ctx=ctx)\n\n    final_elems = []\n    for elem in elements:\n        elem_path_id = pathctx.get_tuple_indirection_path_id(\n            result_path_id, elem.name, get_set_type(elem.val, ctx=ctx),\n            ctx=ctx)\n        final_elems.append(irast.TupleElement(\n            name=elem.name,\n            val=elem.val,\n            path_id=elem_path_id,\n        ))\n\n    typeref = typegen.type_to_typeref(stype, env=ctx.env)\n    tup = irast.Tuple(elements=final_elems, named=named, typeref=typeref)\n    return ensure_set(tup, path_id=result_path_id,\n                      type_override=stype, ctx=ctx)\n\n\ndef new_array_set(\n    elements: Sequence[irast.Set],\n    *,\n    stype: Optional[s_types.Type] = None,\n    ctx: context.ContextLevel,\n    span: Optional[qlast.Span]=None\n) -> irast.Set:\n\n    if elements:\n        element_type = typegen.infer_common_type(elements, ctx.env)\n        if element_type is None:\n            raise errors.QueryError('could not determine array type',\n                                    span=span)\n    elif stype is not None:\n        # When constructing an empty array, we should skip explicit cast any\n        # time that we would skip it for an empty set because we can infer it\n        # from the context.\n        assert stype.is_array()\n    else:\n        element_type = s_pseudo.PseudoType.get(ctx.env.schema, 'anytype')\n        if span is not None:\n            ctx.env.type_origins[element_type] = span\n\n    if stype is None:\n        assert element_type\n        ctx.env.schema, stype = s_types.Array.create(\n            ctx.env.schema, element_type=element_type, dimensions=[-1]\n        )\n    typeref = typegen.type_to_typeref(stype, env=ctx.env)\n    arr = irast.Array(elements=elements, typeref=typeref)\n    return ensure_set(arr, type_override=stype, ctx=ctx)\n\n\ndef raise_self_insert_error(\n    stype: s_obj.Object,\n    span: Optional[qlast.Span],\n    *,\n    ctx: context.ContextLevel,\n) -> NoReturn:\n    dname = stype.get_displayname(ctx.env.schema)\n    raise errors.QueryError(\n        f'invalid reference to {dname}: '\n        f'self-referencing INSERTs are not allowed',\n        hint=(\n            f'Use DETACHED if you meant to refer to an '\n            f'uncorrelated {dname} set'\n        ),\n        span=span,\n    )\n\n\ndef raise_invalid_property_reference(\n    source: s_obj.Object,\n    span: Optional[qlast.Span],\n    *,\n    ctx: context.ContextLevel,\n) -> NoReturn:\n    if isinstance(source, s_types.Type):\n        source = schemactx.get_material_type(source, ctx=ctx)\n    raise errors.InvalidReferenceError(\n        f\"invalid property reference on an expression of primitive type \"\n        f\"'{source.get_displayname(ctx.env.schema)}'\",\n        span=span,\n    )\n\n\ndef compile_path(expr: qlast.Path, *, ctx: context.ContextLevel) -> irast.Set:\n    \"\"\"Create an ir.Set representing the given EdgeQL path expression.\"\"\"\n    anchors = ctx.anchors\n\n    if expr.partial:\n        if ctx.partial_path_prefix is not None:\n            path_tip = ctx.partial_path_prefix\n        else:\n            hint = None\n\n            # If there are anchors, suggest one\n            if anchors:\n                anchor_names: list[str] = [\n                    key if isinstance(key, str) else key.name\n                    for key in anchors\n                ]\n\n                import edb.edgeql.codegen\n                suggestion = (\n                    f'{anchor_names[0]}'\n                    f'{edb.edgeql.codegen.generate_source(expr)}'\n                )\n\n                if len(anchor_names) == 1:\n                    hint = (\n                        f'Did you mean {suggestion}?'\n                    )\n                else:\n                    hint = (\n                        f'Did you mean to use one of: {anchor_names}? '\n                        f'eg. {suggestion}'\n                    )\n\n            raise errors.QueryError(\n                'could not resolve partial path ',\n                span=expr.span,\n                hint=hint\n            )\n\n    computables: list[irast.Set] = []\n    path_sets: list[irast.Set] = []\n\n    for i, step in enumerate(expr.steps):\n        is_computable = False\n        skip_register_set = False\n\n        if isinstance(step, qlast.SpecialAnchor):\n            path_tip = resolve_special_anchor(step, ctx=ctx)\n\n        elif isinstance(step, qlast.IRAnchor):\n            # Check if the starting path label is a known anchor\n            refnode = anchors.get(step.name)\n            if not refnode:\n                raise AssertionError(f'anchor {step.name} is missing')\n            path_tip = new_set_from_set(refnode, ctx=ctx)\n\n            if step.move_scope:\n                assert refnode.path_scope_id is not None\n                node = next(iter(\n                    x for x in ctx.path_scope.root.descendants\n                    if x.unique_id == refnode.path_scope_id\n                ))\n                node.remove()\n                ctx.path_scope.attach_child(node)\n\n                skip_register_set = True\n\n        elif isinstance(step, qlast.ObjectRef):\n            if i > 0:  # pragma: no cover\n                raise RuntimeError(\n                    'unexpected ObjectRef as a non-first path item')\n\n            refnode = None\n\n            if (\n                not step.module\n                and s_name.UnqualName(step.name) not in ctx.aliased_views\n            ):\n                # Check if the starting path label is a known anchor\n                refnode = anchors.get(step.name)\n\n            if refnode is not None:\n                path_tip = new_set_from_set(refnode, ctx=ctx)\n            else:\n                (view_set, stype) = resolve_name(step, ctx=ctx)\n\n                if (stype.is_enum(ctx.env.schema) and\n                        not stype.is_view(ctx.env.schema)):\n                    return compile_enum_path(expr, source=stype, ctx=ctx)\n\n                if (stype.get_expr_type(ctx.env.schema) is not None and\n                        stype.get_name(ctx.env.schema) not in ctx.view_nodes):\n                    if not stype.get_expr(ctx.env.schema):\n                        raise errors.InvalidReferenceError(\n                            f\"cannot refer to alias link helper type \"\n                            f\"'{stype.get_name(ctx.env.schema)}'\",\n                            span=step.span,\n                        )\n\n                    # This is a schema-level view, as opposed to\n                    # a WITH-block or inline alias view.\n                    stype = stmtctx.declare_view_from_schema(stype, ctx=ctx)\n\n                if not view_set:\n                    view_set = ctx.view_sets.get(stype)\n                if view_set is not None:\n                    view_scope_info = ctx.env.path_scope_map[view_set]\n                    path_tip = new_set_from_set(\n                        view_set,\n                        merge_current_ns=(\n                            view_scope_info.pinned_path_id_ns is None\n                        ),\n                        is_binding=view_scope_info.binding_kind,\n                        span=step.span,\n                        ctx=ctx,\n                    )\n\n                    maybe_materialize(stype, path_tip, ctx=ctx)\n\n                else:\n                    path_tip = class_set(stype, ctx=ctx)\n\n                view_scls = ctx.class_view_overrides.get(stype.id)\n                if (view_scls is not None\n                        and view_scls != get_set_type(path_tip, ctx=ctx)):\n                    path_tip = ensure_set(\n                        path_tip, type_override=view_scls, ctx=ctx)\n\n        elif isinstance(step, qlast.Ptr):\n            # Pointer traversal step\n            ptr_expr = step\n            if ptr_expr.direction is not None:\n                direction = s_pointers.PointerDirection(ptr_expr.direction)\n            else:\n                direction = s_pointers.PointerDirection.Outbound\n\n            ptr_name = ptr_expr.name\n\n            source: s_obj.Object\n            ptr: s_pointers.PointerLike\n\n            if ptr_expr.type == 'property':\n                # Link property reference; the source is the\n                # link immediately preceding this step in the path.\n\n                if isinstance(path_tip.expr, irast.Pointer):\n                    ptrref = path_tip.expr.ptrref\n                    fake_tip = path_tip\n                elif (\n                    path_tip.is_binding == irast.BindingKind.For\n                    and (new := irutils.unwrap_set(path_tip))\n                    and isinstance(new.expr, irast.Pointer)\n                ):\n                    # When accessing variables bound with FOR, allow\n                    # looking through to the underlying link.  N.B:\n                    # This relies on the FOR bindings still having an\n                    # expr that lets us look at their\n                    # definition. Eventually I'd like to stop doing\n                    # that, and then we'll need to store it as part of\n                    # the binding/type metadata.\n                    ptrref = new.expr.ptrref\n                    fake_tip = new\n\n                    ind_prefix, _ = typegen.collapse_type_intersection_rptr(\n                        fake_tip,\n                        ctx=ctx,\n                    )\n                    # Don't allow using the iterator to access\n                    # linkprops if the source of the link isn't\n                    # visible, because then there will be a semi-join\n                    # that prevents access to the props.  (This is\n                    # pretty similar to how \"changes the\n                    # interpretation\" errors).\n                    assert isinstance(ind_prefix.expr, irast.Pointer)\n                    if not ctx.path_scope.is_visible(\n                        ind_prefix.expr.source.path_id\n                    ):\n                        # Better message\n                        raise errors.QueryError(\n                            'improper reference to link property on '\n                            'a non-link object',\n                            span=step.span,\n                        )\n\n                    # Mark the underlying pointer as needing a link table,\n                    # so that we access the mapped table to begin with.\n                    assert isinstance(fake_tip.expr, irast.Pointer)\n                    fake_tip.expr.force_link_table = True\n\n                else:\n                    raise errors.EdgeQLSyntaxError(\n                        f\"unexpected reference to link property {ptr_name!r} \"\n                        \"outside of a path expression\",\n                        span=ptr_expr.span,\n                    )\n\n                # The backend can't really handle @source/@target\n                # outside of the singleton mode compiler, and they\n                # aren't really particularly useful outside that\n                # anyway, so disallow them.\n                if (\n                    ptr_expr.name in ('source', 'target')\n                    and not ctx.allow_endpoint_linkprops\n                    and (\n                        ctx.env.options.schema_object_context\n                        not in (s_constr.Constraint, s_indexes.Index)\n                    )\n                ):\n                    raise errors.QueryError(\n                        f'@{ptr_expr.name} may only be used in index and '\n                        'constraint definitions',\n                        span=step.span)\n\n                if isinstance(\n                    ptrref, irast.TypeIntersectionPointerRef\n                ):\n                    ind_prefix, ptrs = typegen.collapse_type_intersection_rptr(\n                        fake_tip,\n                        ctx=ctx,\n                    )\n\n                    assert isinstance(ind_prefix.expr, irast.Pointer)\n                    prefix_type = get_set_type(ind_prefix.expr.source, ctx=ctx)\n                    assert isinstance(prefix_type, s_objtypes.ObjectType)\n\n                    if not ptrs:\n                        tip_type = get_set_type(path_tip, ctx=ctx)\n                        s_vn = prefix_type.get_verbosename(ctx.env.schema)\n                        t_vn = tip_type.get_verbosename(ctx.env.schema)\n                        pn = ind_prefix.expr.ptrref.shortname.name\n                        if direction is s_pointers.PointerDirection.Inbound:\n                            s_vn, t_vn = t_vn, s_vn\n                        raise errors.InvalidReferenceError(\n                            f\"property '{ptr_name}' does not exist because\"\n                            f\" there are no '{pn}' links between\"\n                            f\" {s_vn} and {t_vn}\",\n                            span=ptr_expr.span,\n                        )\n\n                    prefix_ptr_name = (\n                        next(iter(ptrs)).get_local_name(ctx.env.schema))\n\n                    ptr = schemactx.get_union_pointer(\n                        ptrname=prefix_ptr_name,\n                        source=prefix_type,\n                        direction=ind_prefix.expr.direction,\n                        components=ptrs,\n                        ctx=ctx,\n                    )\n                else:\n                    ptr = typegen.ptrcls_from_ptrref(\n                        ptrref, ctx=ctx)\n\n                if isinstance(ptr, s_links.Link):\n                    source = ptr\n                else:\n                    raise errors.QueryError(\n                        'improper reference to link property on '\n                        'a non-link object',\n                        span=step.span,\n                    )\n            else:\n                source = get_set_type(path_tip, ctx=ctx)\n\n            # If this is followed by type intersections, collect\n            # them up, since we need them in ptr_step_set.\n            upcoming_intersections = []\n            for j in range(i + 1, len(expr.steps)):\n                nstep = expr.steps[j]\n                if (isinstance(nstep, qlast.TypeIntersection)\n                        and isinstance(nstep.type, qlast.TypeName)):\n                    upcoming_intersections.append(\n                        schemactx.get_schema_type(\n                            nstep.type.maintype, ctx=ctx))\n                else:\n                    break\n\n            if isinstance(source, s_types.Tuple):\n                path_tip = tuple_indirection_set(\n                    path_tip, source=source, ptr_name=ptr_name,\n                    span=step.span, ctx=ctx)\n\n            else:\n                path_tip = ptr_step_set(\n                    path_tip, expr=step, source=source, ptr_name=ptr_name,\n                    direction=direction,\n                    upcoming_intersections=upcoming_intersections,\n                    ignore_computable=True,\n                    optional_deref=step.type == 'optional',\n                    span=step.span, ctx=ctx)\n\n                assert isinstance(path_tip.expr, irast.Pointer)\n                ptrcls = typegen.ptrcls_from_ptrref(\n                    path_tip.expr.ptrref, ctx=ctx)\n                if _is_computable_ptr(ptrcls, path_tip.expr, ctx=ctx):\n                    is_computable = True\n\n        elif isinstance(step, qlast.TypeIntersection):\n            arg_type = get_set_type(path_tip, ctx=ctx)\n            if not isinstance(arg_type, s_objtypes.ObjectType):\n                raise errors.QueryError(\n                    f'cannot apply type intersection operator to '\n                    f'{arg_type.get_verbosename(ctx.env.schema)}: '\n                    f'it is not an object type',\n                    span=step.span)\n\n            typ: s_types.Type = typegen.ql_typeexpr_to_type(step.type, ctx=ctx)\n\n            try:\n                path_tip = type_intersection_set(\n                    path_tip, typ, optional=False, span=step.span,\n                    ctx=ctx)\n            except errors.SchemaError as e:\n                e.set_span(step.type.span)\n                raise\n\n        else:\n            # Arbitrary expression\n            if i > 0:  # pragma: no cover\n                raise RuntimeError(\n                    'unexpected expression as a non-first path item')\n\n            # We need to fence this if the head is a mutating\n            # statement, to make sure that the factoring allowlist\n            # works right.\n            is_subquery = isinstance(step, qlast.Query)\n            with ctx.newscope(fenced=is_subquery) as subctx:\n                subctx.view_rptr = None\n                path_tip = dispatch.compile(step, ctx=subctx)\n\n                # If the head of the path is a direct object\n                # reference, wrap it in an expression set to give it a\n                # new path id. This prevents the object path from being\n                # spuriously visible to computable paths defined in a shape\n                # at the root of a path. (See test_edgeql_select_tvariant_04\n                # for an example).\n                if (\n                    path_tip.path_id.is_objtype_path()\n                    and not path_tip.path_id.is_view_path()\n                    and path_tip.path_id.src_path() is None\n                ):\n                    path_tip = expression_set(\n                        ensure_stmt(path_tip, ctx=subctx),\n                        ctx=subctx)\n\n                if path_tip.path_id.is_type_intersection_path():\n                    assert isinstance(path_tip.expr, irast.Pointer)\n                    scope_set = path_tip.expr.source\n                else:\n                    scope_set = path_tip\n\n                scope_set = scoped_set(scope_set, ctx=subctx)\n\n        # We compile computables under namespaces, but we need to have\n        # the source of the computable *not* under that namespace,\n        # so we need to do some remapping.\n        if mapped := get_view_map_remapping(path_tip.path_id, ctx):\n            path_tip = new_set_from_set(\n                path_tip, path_id=mapped.path_id, ctx=ctx)\n            # If we are remapping a source path, then we know that\n            # the path is visible, so we shouldn't recompile it\n            # if it is a computable path.\n            is_computable = False\n\n        if is_computable:\n            computables.append(path_tip)\n\n        if pathctx.path_is_inserting(path_tip.path_id, ctx=ctx):\n            stype = ctx.env.schema.get_by_id(\n                path_tip.typeref.id, type=s_types.Type\n            )\n            assert stype\n            raise_self_insert_error(stype, step.span, ctx=ctx)\n\n        # Don't track this step of the path if it didn't change the set\n        # (probably because of do-nothing intersection)\n        if not path_sets or path_sets[-1] != path_tip:\n            path_sets.append(path_tip)\n\n    if expr.span:\n        path_tip.span = expr.span\n    # Register the set in the scope tree. We only skip it when the\n    # path was an IRAnchor with move_scope set, and so instead of\n    # registering the set we moved its whole scoped set over.\n    # (I think it would be *correct* to always register it, but we\n    # get better generated code quality in some of those cases, when\n    # we want the computation to occur down in a subquery.)\n    if not skip_register_set:\n        pathctx.register_set_in_scope(path_tip, ctx=ctx)\n\n    for ir_set in computables:\n        # Compile the computables in sibling scopes to the subpaths\n        # they are computing. Note that the path head will be visible\n        # from inside the computable scope. That's fine.\n\n        scope = ctx.path_scope.find_descendant(ir_set.path_id)\n        if scope is None:\n            scope = ctx.path_scope.find_visible(ir_set.path_id)\n        # We skip recompiling if we can't find a scope for it.\n        # This whole mechanism seems a little sketchy, unfortunately.\n        if scope is None:\n            continue\n\n        with ctx.new() as subctx:\n            subctx.path_scope = scope\n            assert isinstance(ir_set.expr, irast.Pointer)\n            comp_ir_set = computable_ptr_set(\n                ir_set.expr, ir_set.path_id, span=ir_set.span, ctx=subctx\n            )\n            i = path_sets.index(ir_set)\n            if i != len(path_sets) - 1:\n                prptr = path_sets[i + 1].expr\n                assert isinstance(prptr, irast.Pointer)\n                prptr.source = comp_ir_set\n            else:\n                path_tip = comp_ir_set\n            path_sets[i] = comp_ir_set\n\n    return path_tip\n\n\ndef resolve_name(\n    name: qlast.ObjectRef, *, ctx: context.ContextLevel\n) -> tuple[Optional[irast.Set], s_types.Type]:\n\n    view_set = None\n    stype = None\n    if not name.module:\n        view_set = ctx.aliased_views.get(s_name.UnqualName(name.name))\n        if view_set:\n            stype = get_set_type(view_set, ctx=ctx)\n            return (view_set, stype)\n\n    stype = schemactx.get_schema_type(\n        name,\n        condition=lambda o: (\n            isinstance(o, s_types.Type)\n            and (\n                o.is_object_type() or\n                o.is_view(ctx.env.schema) or\n                o.is_enum(ctx.env.schema)\n            )\n        ),\n        label='object type or alias',\n        item_type=s_types.QualifiedType,\n        span=name.span,\n        ctx=ctx,\n    )\n    return (None, stype)\n\n\ndef resolve_special_anchor(\n    anchor: qlast.SpecialAnchor, *, ctx: context.ContextLevel\n) -> irast.Set:\n\n    # '__source__' and '__subject__` can only appear as the\n    # starting path label syntactically and must be pre-populated\n    # by the compile() caller.\n\n    assert isinstance(anchor, qlast.SpecialAnchor)\n    token = anchor.name\n\n    path_tip = ctx.anchors.get(token)\n\n    if not path_tip:\n        raise errors.InvalidReferenceError(\n            f'{token} cannot be used in this expression',\n            span=anchor.span,\n        )\n\n    return path_tip\n\n\ndef ptr_step_set(\n    path_tip: irast.Set,\n    *,\n    upcoming_intersections: Sequence[s_types.Type] = (),\n    source: s_obj.Object,\n    expr: Optional[qlast.Base],\n    ptr_name: str,\n    direction: PtrDir = PtrDir.Outbound,\n    span: Optional[qlast.Span],\n    ignore_computable: bool = False,\n    optional_deref: bool = False,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n    ptrcls, path_id_ptrcls = resolve_ptr_with_intersections(\n        source,\n        ptr_name,\n        upcoming_intersections=upcoming_intersections,\n        track_ref=expr,\n        direction=direction,\n        span=span,\n        ctx=ctx)\n\n    return extend_path(\n        path_tip, ptrcls, direction,\n        path_id_ptrcls=path_id_ptrcls,\n        ignore_computable=ignore_computable,\n        optional_deref=optional_deref,\n        span=span,\n        ctx=ctx)\n\n\ndef _add_target_schema_refs(\n    stype: Optional[s_obj.Object],\n    ctx: context.ContextLevel,\n) -> None:\n    \"\"\"Add the appropriate schema dependencies for a pointer target.\n\n    The only annoying bit is we need to handle unions/intersections also.\"\"\"\n    if not isinstance(stype, s_objtypes.ObjectType):\n        return\n    ctx.env.add_schema_ref(stype, None)\n    schema = ctx.env.schema\n    for obj in (\n        stype.get_union_of(schema).objects(schema) +\n        stype.get_intersection_of(schema).objects(schema)\n    ):\n        ctx.env.add_schema_ref(obj, None)\n\n\ndef resolve_ptr(\n    near_endpoint: s_obj.Object,\n    pointer_name: str,\n    *,\n    direction: s_pointers.PointerDirection = (\n        s_pointers.PointerDirection.Outbound\n    ),\n    span: Optional[qlast.Span] = None,\n    track_ref: Optional[qlast.Base | Literal[False]],\n    ctx: context.ContextLevel,\n) -> s_pointers.Pointer:\n    return resolve_ptr_with_intersections(\n        near_endpoint, pointer_name,\n        direction=direction, span=span,\n        track_ref=track_ref, ctx=ctx)[0]\n\n\ndef resolve_ptr_with_intersections(\n    near_endpoint: s_obj.Object,\n    pointer_name: str,\n    *,\n    upcoming_intersections: Sequence[s_types.Type] = (),\n    far_endpoints: Iterable[s_obj.Object] = (),\n    direction: s_pointers.PointerDirection = (\n        s_pointers.PointerDirection.Outbound\n    ),\n    span: Optional[qlast.Span] = None,\n    track_ref: Optional[qlast.Base | Literal[False]],\n    ctx: context.ContextLevel,\n) -> tuple[s_pointers.Pointer, s_pointers.Pointer]:\n    \"\"\"Resolve a pointer, taking into account upcoming intersections.\n\n    The key trickiness here is that *two* pointers are returned:\n      * one that (for backlinks) includes just the pointers that actually\n        may be used\n      * one for use in path ids, that does not do that filtering, so that\n        path factoring works properly.\n    \"\"\"\n\n    if not isinstance(near_endpoint, s_sources.Source):\n        # Reference to a property on non-object\n        raise_invalid_property_reference(near_endpoint, span, ctx=ctx)\n\n    ptr: Optional[s_pointers.Pointer] = None\n\n    if direction is s_pointers.PointerDirection.Outbound:\n        path_id_ptr = ptr = near_endpoint.maybe_get_ptr(\n            ctx.env.schema,\n            s_name.UnqualName(pointer_name),\n        )\n\n        # If we couldn't anything, but the source is a computed link\n        # that aliases some other link, look for a link property on\n        # it. This allows us to access link properties in both\n        # directions on links, including when the backlink has been\n        # stuck in a computed.\n        if (\n            ptr is None\n            and isinstance(near_endpoint, s_links.Link)\n            and (back := near_endpoint.get_computed_link_alias(ctx.env.schema))\n            and isinstance(back, s_links.Link)\n            and (nptr := back.maybe_get_ptr(\n                ctx.env.schema,\n                s_name.UnqualName(pointer_name),\n            ))\n            # We can't handle computeds yet, since we would need to switch\n            # around a bunch of stuff inside them.\n            and not nptr.is_pure_computable(ctx.env.schema)\n        ):\n            src_type = downcast(\n                s_types.Type, near_endpoint.get_source(ctx.env.schema)\n            )\n            if not src_type.is_view(ctx.env.schema):\n                # HACK: If the source is in the standard library, and\n                # not a view, we can't add a derived pointer.  For\n                # consistency, just always require it be a view.\n                new_source = downcast(\n                    s_objtypes.ObjectType,\n                    schemactx.derive_view(src_type, ctx=ctx),\n                )\n                new_endpoint = downcast(s_links.Link, schemactx.derive_ptr(\n                    near_endpoint, new_source, ctx=ctx))\n            else:\n                new_endpoint = near_endpoint\n\n            ptr = schemactx.derive_ptr(nptr, new_endpoint, ctx=ctx)\n            path_id_ptr = nptr\n\n        if ptr is not None:\n            ref = ptr.get_nearest_non_derived_parent(ctx.env.schema)\n            if track_ref is not False:\n                ctx.env.add_schema_ref(ref, track_ref)\n                _add_target_schema_refs(\n                    ref.get_target(ctx.env.schema), ctx=ctx)\n\n    else:\n        assert isinstance(near_endpoint, s_types.Type)\n        concrete_near_endpoint = schemactx.concretify(near_endpoint, ctx=ctx)\n        ptrs = concrete_near_endpoint.getrptrs(\n            ctx.env.schema, pointer_name, sources=far_endpoints)\n        if ptrs:\n            # If this reverse pointer access is followed by\n            # intersections, we filter out any pointers that\n            # couldn't be picked up by the intersections.\n            # If a pointer doesn't get picked up, we look to see\n            # if any of its children might.\n            #\n            # This both allows us to avoid creating spurious\n            # dependencies when reverse links are used in schemas\n            # and to generate a precise set of possible pointers.\n            dep_ptrs = set()\n            wl = list(ptrs)\n            while wl:\n                ptr = wl.pop()\n                if (src := ptr.get_source(ctx.env.schema)):\n                    if all(\n                        src.issubclass(ctx.env.schema, typ)\n                        for typ in upcoming_intersections\n                    ):\n                        dep_ptrs.add(ptr)\n                    else:\n                        wl.extend(ptr.children(ctx.env.schema))\n\n            if track_ref is not False:\n                for p in dep_ptrs:\n                    p = p.get_nearest_non_derived_parent(ctx.env.schema)\n                    ctx.env.add_schema_ref(p, track_ref)\n                    _add_target_schema_refs(\n                        p.get_source(ctx.env.schema), ctx=ctx)\n\n            # We can only compute backlinks for non-computed pointers,\n            # but we need to make sure that a computed pointer doesn't\n            # break properly-filtered backlinks.\n            concrete_ptrs = [\n                ptr for ptr in ptrs\n                if not ptr.is_pure_computable(ctx.env.schema)]\n\n            for ptr in ptrs:\n                if (\n                    ptr.is_pure_computable(ctx.env.schema)\n                    and (ptr in dep_ptrs or not concrete_ptrs)\n                ):\n                    vname = ptr.get_verbosename(ctx.env.schema,\n                                                with_parent=True)\n                    raise errors.InvalidReferenceError(\n                        f'cannot follow backlink {pointer_name!r} because '\n                        f'{vname} is computed',\n                        span=span\n                    )\n\n            opaque = not far_endpoints\n            concrete_ptr = schemactx.get_union_pointer(\n                ptrname=s_name.UnqualName(pointer_name),\n                source=near_endpoint,\n                direction=direction,\n                components=concrete_ptrs,\n                opaque=opaque,\n                modname=ctx.derived_target_module,\n                ctx=ctx,\n            )\n            path_id_ptr = ptr = concrete_ptr\n            # If we have an upcoming intersection that has actual\n            # pointer targets, we want to put the filtered down\n            # version into the AST, so that we can more easily use\n            # that information in compilation.  But we still need the\n            # *full* union in the path_ids, for factoring.\n            if dep_ptrs and upcoming_intersections:\n                ptr = schemactx.get_union_pointer(\n                    ptrname=s_name.UnqualName(pointer_name),\n                    source=near_endpoint,\n                    direction=direction,\n                    components=dep_ptrs,\n                    opaque=opaque,\n                    modname=ctx.derived_target_module,\n                    ctx=ctx,\n                )\n\n    if ptr and path_id_ptr:\n        return ptr, path_id_ptr\n\n    if isinstance(near_endpoint, s_links.Link):\n        vname = near_endpoint.get_verbosename(ctx.env.schema, with_parent=True)\n        msg = f'{vname} has no property {pointer_name!r}'\n\n    elif direction == s_pointers.PointerDirection.Outbound:\n        msg = (f'{near_endpoint.get_verbosename(ctx.env.schema)} '\n               f'has no link or property {pointer_name!r}')\n\n    else:\n        nep_name = near_endpoint.get_displayname(ctx.env.schema)\n        path = f'{nep_name}.{direction}{pointer_name}'\n        msg = f'{path!r} does not resolve to any known path'\n\n    err = errors.InvalidReferenceError(msg, span=span)\n\n    if (\n        direction is s_pointers.PointerDirection.Outbound\n        # In some call sites, we call resolve_ptr \"experimentally\",\n        # not tracking references and swallowing failures. Don't do an\n        # expensive (30% of compilation time in some benchmarks!)\n        # error enrichment for cases that won't really error.\n        and track_ref is not False\n    ):\n        s_utils.enrich_schema_lookup_error(\n            err,\n            s_name.UnqualName(pointer_name),\n            modaliases=ctx.modaliases,\n            item_type=s_pointers.Pointer,\n            pointer_parent=near_endpoint,\n            schema=ctx.env.schema,\n        )\n\n    raise err\n\n\ndef _check_secret_ptr(\n    ptrcls: s_pointers.Pointer,\n    *,\n    span: Optional[qlast.Span]=None,\n    ctx: context.ContextLevel,\n) -> None:\n    module = ptrcls.get_name(ctx.env.schema).module\n\n    # HACK: Workaround for #8974. Aliases/globals have expr duplicated\n    # in their associated Type, and sometimes recompilation of the\n    # Type is triggered.\n    # Skip producing secret errors there, since we don't have the\n    # result_view_name available.\n    #\n    # The errors will get produced when actually compiling the\n    # Global/Alias itself.\n    if (\n        ctx.env.options.schema_object_context\n        and issubclass(ctx.env.options.schema_object_context, s_types.Type)\n    ):\n        return\n\n    func_name = ctx.env.options.func_name\n    if func_name and func_name.module == module:\n        return\n\n    view_name = ctx.env.options.result_view_name  # type: ignore\n    if view_name and view_name.module == module:\n        return\n\n    if ctx.current_schema_views:\n        view_name = ctx.current_schema_views[-1].get_name(ctx.env.schema)\n        if view_name.module == module:\n            return\n\n    vn = ptrcls.get_verbosename(ctx.env.schema, with_parent=True)\n    raise errors.QueryError(\n        f\"cannot access {vn} because it is secret\",\n        span=span,\n    )\n\n\ndef extend_path(\n    source_set: irast.Set,\n    ptrcls: s_pointers.Pointer,\n    direction: PtrDir = PtrDir.Outbound,\n    *,\n    path_id_ptrcls: Optional[s_pointers.Pointer] = None,\n    ignore_computable: bool = False,\n    same_computable_scope: bool = False,\n    optional_deref: bool = False,\n    span: Optional[qlast.Span]=None,\n    ctx: context.ContextLevel,\n) -> irast.SetE[irast.Pointer]:\n    \"\"\"Return a Set node representing the new path tip.\"\"\"\n\n    if ptrcls.is_link_property(ctx.env.schema):\n        src_path_id = source_set.path_id.ptr_path()\n    else:\n        if direction is not s_pointers.PointerDirection.Inbound:\n            source = ptrcls.get_near_endpoint(ctx.env.schema, direction)\n            assert isinstance(source, s_types.Type)\n            stype = get_set_type(source_set, ctx=ctx)\n            if not stype.issubclass(ctx.env.schema, source):\n                # Polymorphic link reference\n                source_set = type_intersection_set(\n                    source_set, source, optional=True, span=span,\n                    ctx=ctx)\n\n        src_path_id = source_set.path_id\n\n    orig_ptrcls = ptrcls\n\n    # If there is a particular specified ptrcls for the pathid, use\n    # it, otherwise use the actual ptrcls. This comes up with\n    # intersections on backlinks, where we want to use a precise ptr\n    # in the IR for compilation reasons but need a path_id that is\n    # independent of intersections.\n    path_id_ptrcls = path_id_ptrcls or ptrcls\n\n    # Find the pointer definition site.\n    # This makes it so that views don't change path ids unless they are\n    # introducing some computation.\n    ptrcls = ptrcls.get_nearest_defined(ctx.env.schema)\n    path_id_ptrcls = path_id_ptrcls.get_nearest_defined(ctx.env.schema)\n\n    path_id = pathctx.extend_path_id(\n        src_path_id,\n        ptrcls=path_id_ptrcls,\n        direction=direction,\n        ns=ctx.path_id_namespace,\n        ctx=ctx,\n    )\n\n    if ptrcls.get_secret(ctx.env.schema):\n        _check_secret_ptr(ptrcls, span=span, ctx=ctx)\n\n    target = orig_ptrcls.get_far_endpoint(ctx.env.schema, direction)\n    assert isinstance(target, s_types.Type)\n    ptr = irast.Pointer(\n        source=source_set,\n        direction=direction,\n        ptrref=typegen.ptr_to_ptrref(ptrcls, ctx=ctx),\n        is_definition=False,\n        optional_deref=optional_deref,\n    )\n    target_set = new_set(\n        stype=target, path_id=path_id, span=span, expr=ptr, ctx=ctx)\n\n    is_computable = _is_computable_ptr(ptrcls, ptr, ctx=ctx)\n    if not ignore_computable and is_computable:\n        target_set = computable_ptr_set(\n            ptr,\n            path_id,\n            same_computable_scope=same_computable_scope,\n            span=span,\n            ctx=ctx,\n        )\n\n    assert irutils.is_set_instance(target_set, irast.Pointer)\n    return target_set\n\n\ndef needs_rewrite_existence_assertion(\n    ptrcls: s_pointers.PointerLike,\n    rptr: irast.Pointer,\n    *,\n    ctx: context.ContextLevel,\n) -> bool:\n    \"\"\"Determines if we need to inject an assert_exists for a pointer\n\n    Required pointers to types with access policies need to have an\n    assert_exists added\n    \"\"\"\n\n    return bool(\n        not ctx.suppress_rewrites\n        # We *don't* need to do the rewrite when using .?>\n        and not rptr.optional_deref\n        and ptrcls.get_required(ctx.env.schema)\n        and rptr.direction == PtrDir.Outbound\n        and (target := ptrcls.get_target(ctx.env.schema))\n        and ctx.env.type_rewrites.get((target, False))\n        and ptrcls.get_shortname(ctx.env.schema).name != '__type__'\n    )\n\n\ndef is_injected_computable_ptr(\n    ptrcls: s_pointers.PointerLike,\n    rptr: irast.Pointer,\n    *,\n    ctx: context.ContextLevel,\n) -> bool:\n    return (\n        ctx.env.options.apply_query_rewrites\n        and ptrcls not in ctx.active_computeds\n        and (\n            bool(ptrcls.get_schema_reflection_default(ctx.env.schema))\n            or needs_rewrite_existence_assertion(ptrcls, rptr, ctx=ctx)\n        )\n    )\n\n\ndef _is_computable_ptr(\n    ptrcls: s_pointers.PointerLike,\n    rptr: irast.Pointer,\n    *,\n    ctx: context.ContextLevel,\n) -> bool:\n    try:\n        qlexpr = ctx.env.source_map[ptrcls].qlexpr\n    except KeyError:\n        pass\n    else:\n        return qlexpr is not None\n\n    return (\n        bool(ptrcls.get_expr(ctx.env.schema))\n        or is_injected_computable_ptr(ptrcls, rptr, ctx=ctx)\n    )\n\n\ndef compile_enum_path(\n    expr: qlast.Path, *, source: s_types.Type, ctx: context.ContextLevel\n) -> irast.Set:\n\n    assert isinstance(source, s_scalars.ScalarType)\n    enum_values = source.get_enum_values(ctx.env.schema)\n    assert enum_values\n\n    nsteps = len(expr.steps)\n    if nsteps == 1:\n        raise errors.QueryError(\n            f\"'{source.get_displayname(ctx.env.schema)}' enum \"\n            f\"path expression lacks an enum member name, as in \"\n            f\"'{source.get_displayname(ctx.env.schema)}.{enum_values[0]}'\",\n            span=expr.steps[0].span,\n        )\n\n    step2 = expr.steps[1]\n    if not isinstance(step2, qlast.Ptr):\n        raise errors.QueryError(\n            f\"an enum member name must follow enum type name in the path, \"\n            f\"as in \"\n            f\"'{source.get_displayname(ctx.env.schema)}.{enum_values[0]}'\",\n            span=step2.span,\n        )\n\n    ptr_name = step2.name\n\n    step2_direction = s_pointers.PointerDirection.Outbound\n    if step2.direction is not None:\n        step2_direction = s_pointers.PointerDirection(step2.direction)\n    if step2_direction is not s_pointers.PointerDirection.Outbound:\n        raise errors.QueryError(\n            f\"enum types do not support backlink navigation\",\n            span=step2.span,\n        )\n    if step2.type == 'property':\n        raise errors.QueryError(\n            f\"unexpected reference to link property '{ptr_name}' \"\n            f\"outside of a path expression\",\n            span=step2.span,\n        )\n\n    if nsteps > 2:\n        raise_invalid_property_reference(\n            source, span=expr.steps[2].span, ctx=ctx\n        )\n\n    if ptr_name not in enum_values:\n        rec_name = sorted(\n            enum_values,\n            key=lambda name: levenshtein.distance(name, ptr_name)\n        )[0]\n        src_name = source.get_displayname(ctx.env.schema)\n        raise errors.InvalidReferenceError(\n            f\"'{src_name}' enum has no member called {ptr_name!r}\",\n            hint=f\"did you mean {rec_name!r}?\",\n            span=step2.span,\n        )\n\n    return enum_indirection_set(\n        source=source,\n        ptr_name=step2.name,\n        span=expr.span,\n        ctx=ctx,\n    )\n\n\ndef enum_indirection_set(\n    *,\n    source: s_types.Type,\n    ptr_name: str,\n    span: Optional[qlast.Span],\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    strref = typegen.type_to_typeref(\n        ctx.env.get_schema_type_and_track(s_name.QualName('std', 'str')),\n        env=ctx.env,\n    )\n\n    return casts.compile_cast(\n        irast.StringConstant(value=ptr_name, typeref=strref),\n        source,\n        span=span,\n        ctx=ctx,\n    )\n\n\ndef tuple_indirection_set(\n    path_tip: irast.Set,\n    *,\n    source: s_types.Type,\n    ptr_name: str,\n    span: Optional[qlast.Span] = None,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    assert isinstance(source, s_types.Tuple)\n\n    try:\n        el_name = ptr_name\n        el_norm_name = source.normalize_index(ctx.env.schema, el_name)\n        el_type = source.get_subtype(ctx.env.schema, el_name)\n    except errors.InvalidReferenceError as e:\n        if span and not e.has_span():\n            e.set_span(span)\n        raise e\n\n    path_id = pathctx.get_tuple_indirection_path_id(\n        path_tip.path_id, el_norm_name, el_type, ctx=ctx)\n\n    ptr = irast.TupleIndirectionPointer(\n        source=path_tip,\n        ptrref=downcast(irast.TupleIndirectionPointerRef, path_id.rptr()),\n        direction=not_none(path_id.rptr_dir()),\n    )\n    ti_set = new_set(stype=el_type, path_id=path_id, expr=ptr, ctx=ctx)\n\n    return ti_set\n\n\ndef type_intersection_set(\n    source_set: irast.Set,\n    stype: s_types.Type,\n    *,\n    optional: bool,\n    span: Optional[qlast.Span] = None,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n    \"\"\"Return an interesection of *source_set* with type *stype*.\"\"\"\n\n    arg_type = get_set_type(source_set, ctx=ctx)\n\n    result = schemactx.apply_intersection(arg_type, stype, ctx=ctx)\n    if result.stype == arg_type:\n        return source_set\n\n    rptr_specialization = []\n\n    if (\n        isinstance(source_set.expr, irast.Pointer)\n        and source_set.expr.ptrref.union_components\n    ):\n        rptr = source_set.expr\n\n        # This is a type intersection of a union pointer, most likely\n        # a reverse link path specification.  If so, test the union\n        # components against the type expression and record which\n        # components match.  This information will be used later\n        # when evaluating the path cardinality, as well as to\n        # route link property references accordingly.\n        for component in source_set.expr.ptrref.union_components:\n            component_endpoint_ref = component.dir_target(rptr.direction)\n            ctx.env.schema, component_endpoint = irtyputils.ir_typeref_to_type(\n                ctx.env.schema, component_endpoint_ref)\n            if component_endpoint.issubclass(ctx.env.schema, stype):\n                assert isinstance(component, irast.PointerRef)\n                rptr_specialization.append(component)\n            elif stype.issubclass(ctx.env.schema, component_endpoint):\n                assert isinstance(stype, s_objtypes.ObjectType)\n                if rptr.direction is s_pointers.PointerDirection.Inbound:\n                    narrow_ptr = stype.getptr(\n                        ctx.env.schema,\n                        component.shortname.get_local_name(),\n                    )\n                    rptr_specialization.append(\n                        irtyputils.ptrref_from_ptrcls(\n                            schema=ctx.env.schema,\n                            ptrcls=narrow_ptr,\n                            cache=ctx.env.ptr_ref_cache,\n                            typeref_cache=ctx.env.type_ref_cache,\n                        ),\n                    )\n                else:\n                    assert isinstance(component, irast.PointerRef)\n                    rptr_specialization.append(component)\n\n    ptrcls = irast.TypeIntersectionLink(\n        arg_type,\n        result.stype,\n        optional=optional,\n        is_empty=result.is_empty,\n        is_subtype=result.is_subtype,\n        rptr_specialization=rptr_specialization,\n        # The type intersection cannot increase the cardinality\n        # of the input set, so semantically, the cardinality\n        # of the type intersection \"link\" is, at most, ONE.\n        cardinality=qltypes.SchemaCardinality.One,\n    )\n\n    ptrref = irtyputils.ptrref_from_ptrcls(\n        schema=ctx.env.schema,\n        ptrcls=ptrcls,\n        cache=ctx.env.ptr_ref_cache,\n        typeref_cache=ctx.env.type_ref_cache,\n    )\n\n    poly_set = new_set(\n        stype=result.stype,\n        path_id=source_set.path_id.extend(ptrref=ptrref),\n        expr=irast.TypeIntersectionPointer(\n            source=source_set,\n            ptrref=downcast(irast.TypeIntersectionPointerRef, ptrref),\n            direction=s_pointers.PointerDirection.Outbound,\n            optional=optional,\n        ),\n        span=span,\n        ctx=ctx,\n    )\n\n    return poly_set\n\n\ndef class_set(\n    stype: s_types.Type,\n    *,\n    path_id: Optional[irast.PathId] = None,\n    skip_subtypes: bool = False,\n    ignore_rewrites: bool = False,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n    \"\"\"Nominally, create a set representing selecting some type.\n\n    That is, create a set with a TypeRoot expr.\n\n    TODO(ir): In practice, a lot of call sites really want some kind\n    of handle to something that will be bound elsewhere, and we should\n    clean those up to use a different node.\n    \"\"\"\n\n    if path_id is None:\n        path_id = pathctx.get_path_id(stype, ctx=ctx)\n    return new_set(\n        path_id=path_id,\n        stype=stype,\n        ignore_rewrites=ignore_rewrites,\n        expr=irast.TypeRoot(\n            typeref=typegen.type_to_typeref(stype, env=ctx.env),\n            skip_subtypes=skip_subtypes,\n        ),\n        ctx=ctx,\n    )\n\n\ndef expression_set(\n    expr: irast.Expr,\n    path_id: Optional[irast.PathId] = None,\n    *,\n    type_override: Optional[s_types.Type] = None,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    if isinstance(expr, irast.Set):  # pragma: no cover\n        raise errors.InternalServerError(f'{expr!r} is already a Set')\n\n    if type_override is not None:\n        stype = type_override\n    else:\n        stype = get_expr_type(expr, ctx=ctx)\n\n    if path_id is None:\n        path_id = getattr(expr, 'path_id', None)\n        if path_id is None:\n            path_id = pathctx.get_expression_path_id(stype, ctx=ctx)\n\n    return new_set(\n        path_id=path_id,\n        stype=stype,\n        expr=expr,\n        span=expr.span,\n        ctx=ctx\n    )\n\n\ndef scoped_set(\n    expr: irast.Set | irast.Expr,\n    *,\n    type_override: Optional[s_types.Type] = None,\n    typehint: Optional[s_types.Type] = None,\n    path_id: Optional[irast.PathId] = None,\n    force_reassign: bool = False,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    if not isinstance(expr, irast.Set):\n        ir_set = expression_set(\n            expr, type_override=type_override,\n            path_id=path_id, ctx=ctx)\n        pathctx.assign_set_scope(ir_set, ctx.path_scope, ctx=ctx)\n    else:\n        if typehint is not None or type_override is not None:\n            ir_set = ensure_set(\n                expr, typehint=typehint,\n                type_override=type_override,\n                path_id=path_id, ctx=ctx)\n        else:\n            ir_set = expr\n\n        if ir_set.path_scope_id is None or force_reassign:\n            if ctx.path_scope.find_child(ir_set.path_id) and path_id is None:\n                # Protect from scope recursion in the common case by\n                # wrapping the set into a subquery.\n                ir_set = expression_set(\n                    ensure_stmt(ir_set, ctx=ctx),\n                    type_override=type_override,\n                    ctx=ctx)\n\n            pathctx.assign_set_scope(ir_set, ctx.path_scope, ctx=ctx)\n\n    return ir_set\n\n\ndef moveable_anchor(\n    expr: irast.Set,\n    name: str = 'v',\n    *,\n    check_dml: bool = False,\n    ctx: context.ContextLevel,\n) -> qlast.Path:\n    return ctx.create_anchor(\n        scoped_set(expr, ctx=ctx),\n        name=name,\n        check_dml=check_dml,\n        move_scope=True,\n    )\n\n\ndef ensure_set(\n    expr: irast.Set | irast.Expr,\n    *,\n    type_override: Optional[s_types.Type] = None,\n    typehint: Optional[s_types.Type] = None,\n    path_id: Optional[irast.PathId] = None,\n    span: Optional[qlast.Span] = None,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    if not isinstance(expr, irast.Set):\n        ir_set = expression_set(\n            expr, type_override=type_override,\n            path_id=path_id, ctx=ctx)\n    else:\n        ir_set = expr\n\n    stype = get_set_type(ir_set, ctx=ctx)\n\n    if type_override is not None and stype != type_override:\n        ir_set = new_set_from_set(ir_set, stype=type_override, ctx=ctx)\n\n        stype = type_override\n\n    if span is not None:\n        ir_set = new_set_from_set(ir_set, span=span, ctx=ctx)\n\n    if (irutils.is_set_instance(ir_set, irast.EmptySet)\n            and (stype is None or stype.is_any(ctx.env.schema))\n            and typehint is not None):\n        typegen.amend_empty_set_type(ir_set, typehint, env=ctx.env)\n        stype = get_set_type(ir_set, ctx=ctx)\n\n    if (\n        typehint is not None\n        and stype != typehint\n        and not stype.implicitly_castable_to(typehint, ctx.env.schema)\n    ):\n        raise errors.QueryError(\n            f'expecting expression of type '\n            f'{typehint.get_displayname(ctx.env.schema)}, '\n            f'got {stype.get_displayname(ctx.env.schema)}',\n            span=expr.span\n        )\n\n    return ir_set\n\n\ndef ensure_stmt(\n    expr: irast.Set | irast.Expr, *, ctx: context.ContextLevel\n) -> irast.Stmt:\n    if not isinstance(expr, irast.Stmt):\n        expr = irast.SelectStmt(\n            result=ensure_set(expr, ctx=ctx),\n            implicit_wrapper=True,\n        )\n    return expr\n\n\ndef fixup_computable_source_set(\n    source_set: irast.Set,\n    *,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n    source_scls = get_set_type(source_set, ctx=ctx)\n    # process_view() may generate computable pointer expressions\n    # in the form \"self.linkname\".  To prevent infinite recursion,\n    # self must resolve to the parent type of the view NOT the view\n    # type itself.  Similarly, when resolving computable link properties\n    # make sure that we use the parent of derived ptrcls.\n    if source_scls.is_view(ctx.env.schema):\n        source_set_stype = source_scls.peel_view(ctx.env.schema)\n        source_set = new_set_from_set(\n            source_set, stype=source_set_stype, ctx=ctx)\n        source_set.shape = ()\n\n        if isinstance(source_set.expr, irast.Pointer):\n            source_rptrref = source_set.expr.ptrref\n            if source_rptrref.base_ptr is not None:\n                source_rptrref = source_rptrref.base_ptr\n            source_set.expr = source_set.expr.replace(\n                ptrref=source_rptrref,\n                is_definition=True,\n            )\n    return source_set\n\n\ndef computable_ptr_set(\n    rptr: irast.Pointer,\n    path_id: irast.PathId,\n    *,\n    same_computable_scope: bool=False,\n    span: Optional[qlast.Span]=None,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n    \"\"\"Return ir.Set for a pointer defined as a computable.\"\"\"\n    ptrcls = typegen.ptrcls_from_ptrref(rptr.ptrref, ctx=ctx)\n    source_scls = get_set_type(rptr.source, ctx=ctx)\n    source_set = fixup_computable_source_set(rptr.source, ctx=ctx)\n    ptrcls_to_shadow = None\n\n    qlctx: Optional[context.ContextLevel]\n\n    try:\n        comp_info = ctx.env.source_map[ptrcls]\n        qlexpr = comp_info.qlexpr\n        assert isinstance(comp_info.context, context.ContextLevel)\n        qlctx = comp_info.context\n        inner_source_path_id = comp_info.path_id\n        path_id_ns = comp_info.path_id_ns\n    except KeyError:\n        comp_expr: Optional[s_expr.Expression] = ptrcls.get_expr(ctx.env.schema)\n        schema_qlexpr: Optional[qlast.Expr] = None\n        if comp_expr is None and ctx.env.options.apply_query_rewrites:\n            assert isinstance(ptrcls, s_pointers.Pointer)\n            ptrcls_n = ptrcls.get_shortname(ctx.env.schema).name\n            path = qlast.Path(\n                steps=[\n                    qlast.SpecialAnchor(name='__source__'),\n                    qlast.Ptr(\n                        name=ptrcls_n,\n                        direction=s_pointers.PointerDirection.Outbound,\n                        type=(\n                            'property'\n                            if ptrcls.is_link_property(ctx.env.schema)\n                            else None\n                        )\n                    )\n                ],\n            )\n\n            schema_deflt = ptrcls.get_schema_reflection_default(ctx.env.schema)\n            if schema_deflt is not None:\n                schema_qlexpr = qlast.BinOp(\n                    left=path,\n                    right=qlparser.parse_fragment(schema_deflt),\n                    op='??',\n                )\n\n            if needs_rewrite_existence_assertion(ptrcls, rptr, ctx=ctx):\n                # Wrap it in a dummy select so that we can't optimize away\n                # the assert_exists.\n                # TODO: do something less bad\n                arg = qlast.SelectQuery(\n                    result=path, where=qlast.Constant.boolean(True))\n                vname = ptrcls.get_verbosename(\n                    ctx.env.schema, with_parent=True)\n                msg = f'required {vname} is hidden by access policy'\n                if ctx.active_computeds:\n                    cur = next(reversed(ctx.active_computeds))\n                    vname = cur.get_verbosename(\n                        ctx.env.schema, with_parent=True)\n                    msg += f' (while evaluating computed {vname})'\n\n                schema_qlexpr = qlast.FunctionCall(\n                    func=('__std__', 'assert_exists'),\n                    args=[arg],\n                    kwargs={'message': qlast.Constant.string(value=msg)},\n                )\n\n            # Is this is a view, we want to shadow the underlying\n            # ptrcls, since otherwise we will generate this default\n            # code *twice*.\n            if rptr.ptrref.base_ptr:\n                ptrcls_to_shadow = typegen.ptrcls_from_ptrref(\n                    rptr.ptrref.base_ptr, ctx=ctx)\n\n        if schema_qlexpr is None:\n            if comp_expr is None:\n                ptrcls_sn = ptrcls.get_shortname(ctx.env.schema)\n                raise errors.InternalServerError(\n                    f'{ptrcls_sn!r} is not a computed pointer')\n\n            comp_qlexpr = comp_expr.parse()\n            assert isinstance(comp_qlexpr, qlast.Expr), 'expected qlast.Expr'\n            schema_qlexpr = comp_qlexpr\n\n        # NOTE: Validation of the expression type is not the concern\n        # of this function. For any non-object pointer target type,\n        # the default expression must be assignment-cast into that\n        # type.\n        target_scls = ptrcls.get_target(ctx.env.schema)\n        assert target_scls is not None\n        if not target_scls.is_object_type():\n            schema_qlexpr = qlast.TypeCast(\n                type=typegen.type_to_ql_typeref(\n                    target_scls, ctx=ctx),\n                expr=schema_qlexpr,\n            )\n        qlexpr = astutils.ensure_ql_query(schema_qlexpr)\n        qlctx = None\n        path_id_ns = None\n\n    newctx: Callable[[], ContextManager[context.ContextLevel]]\n\n    if qlctx is None:\n        # Schema-level computed link or property, the context should\n        # still have a source.\n        newctx = _get_schema_computed_ctx(\n            rptr=rptr,\n            source=source_set,\n            ctx=ctx)\n\n    else:\n        newctx = _get_computable_ctx(\n            rptr=rptr,\n            source=source_set,\n            source_scls=source_scls,\n            inner_source_path_id=inner_source_path_id,\n            path_id_ns=path_id_ns,\n            same_scope=same_computable_scope,\n            qlctx=qlctx,\n            ctx=ctx)\n\n    result_stype = ptrcls.get_target(ctx.env.schema)\n    base_object = ctx.env.schema.get('std::BaseObject', type=s_types.Type)\n    with newctx() as subctx:\n        assert isinstance(source_scls, s_sources.Source)\n        assert isinstance(ptrcls, s_pointers.Pointer)\n\n        subctx.active_computeds = subctx.active_computeds.copy()\n        if ptrcls_to_shadow:\n            assert isinstance(ptrcls_to_shadow, s_pointers.Pointer)\n            subctx.active_computeds.add(ptrcls_to_shadow)\n        subctx.active_computeds.add(ptrcls)\n        if result_stype != base_object:\n            subctx.view_scls = result_stype\n        subctx.view_rptr = context.ViewRPtr(\n            source=source_scls, ptrcls=ptrcls)\n        subctx.anchors['__source__'] = source_set\n        if qlctx and '__default__' in qlctx.anchors:\n            subctx.anchors['__default__'] = qlctx.anchors['__default__']\n        subctx.empty_result_type_hint = ptrcls.get_target(ctx.env.schema)\n        subctx.partial_path_prefix = source_set\n        # On a mutation, make the expr_exposed. This corresponds with\n        # a similar check on is_mutation in _normalize_view_ptr_expr.\n        if (source_scls.get_expr_type(ctx.env.schema)\n                != s_types.ExprType.Select):\n            subctx.expr_exposed = context.Exposure.EXPOSED\n\n        comp_ir_set = dispatch.compile(qlexpr, ctx=subctx)\n\n    # XXX: or should we update rptr in place??\n    rptr = rptr.replace(expr=comp_ir_set.expr)\n    comp_ir_set = new_set_from_set(\n        comp_ir_set, path_id=path_id, expr=rptr, span=span,\n        merge_current_ns=True,\n        ctx=ctx)\n\n    maybe_materialize(ptrcls, comp_ir_set, ctx=ctx)\n\n    return comp_ir_set\n\n\ndef _get_schema_computed_ctx(\n    *, rptr: irast.Pointer, source: irast.Set, ctx: context.ContextLevel\n) -> Callable[[], ContextManager[context.ContextLevel]]:\n\n    @contextlib.contextmanager\n    def newctx() -> Iterator[context.ContextLevel]:\n        with ctx.detached() as subctx:\n            source_scope = pathctx.get_set_scope(rptr.source, ctx=ctx)\n            if source_scope and source_scope.namespaces:\n                subctx.path_id_namespace |= source_scope.namespaces\n\n            # Get the type of the actual location where the computed pointer\n            # was defined in the schema, since that is the type that must\n            # be used in the view map, since that is the type that might\n            # be *referenced in the definition*.\n            ptr = typegen.ptrcls_from_ptrref(rptr.ptrref, ctx=ctx)\n            assert isinstance(ptr, s_pointers.Pointer)\n            ptr = ptr.maybe_get_topmost_concrete_base(ctx.env.schema) or ptr\n            src = ptr.get_source(ctx.env.schema)\n\n            # If the source is an abstract pointer, then we don't have\n            # a full path to bind in the computed. Otherwise use a\n            # path derived from the pointer source.\n            if not (\n                isinstance(src, s_pointers.Pointer)\n                and src.is_non_concrete(ctx.env.schema)\n            ):\n                inner_path_id = not_none(irast.PathId.from_pointer(\n                    ctx.env.schema, ptr, namespace=subctx.path_id_namespace,\n                    env=ctx.env,\n                ).src_path())\n\n                # XXX: THIS IS DODGY - wait, this is a no-op\n                remapped_source = new_set_from_set(\n                    rptr.source, expr=rptr.source.expr, ctx=ctx\n                )\n                update_view_map(inner_path_id, remapped_source, ctx=subctx)\n\n            yield subctx\n\n    return newctx\n\n\ndef update_view_map(\n    path_id: irast.PathId,\n    remapped_source: irast.Set,\n    *,\n    ctx: context.ContextLevel\n) -> None:\n    ctx.view_map = ctx.view_map.new_child()\n    key = path_id.strip_namespace(path_id.namespace)\n    old = ctx.view_map.get(key, ())\n    ctx.view_map[key] = ((path_id, remapped_source),) + old\n\n\ndef get_view_map_remapping(\n    path_id: irast.PathId, ctx: context.ContextLevel\n) -> Optional[irast.Set]:\n    \"\"\"Perform path_id remapping based on outer views\n\n    This is a little fiddly, since we may have\n    picked up *additional* namespaces.\n    \"\"\"\n    key = path_id.strip_namespace(path_id.namespace)\n    entries = ctx.view_map.get(key, ())\n    fixed_path_id = path_id.merge_namespace(ctx.path_id_namespace, deep=True)\n    for inner_path_id, mapped in entries:\n        fixed_inner = inner_path_id.merge_namespace(\n            ctx.path_id_namespace, deep=True)\n\n        if fixed_inner == fixed_path_id:\n            return mapped\n    return None\n\n\ndef remap_path_id(\n    path_id: irast.PathId, ctx: context.ContextLevel\n) -> irast.PathId:\n    \"\"\"Remap a path_id based on the view_map, one step at a time.\n\n    This is intended to mirror what happens to paths in compile_path.\n    \"\"\"\n    new_id = None\n    hit = False\n    for prefix in path_id.iter_prefixes():\n        if not new_id:\n            new_id = prefix\n        else:\n            nrptr, dir = prefix.rptr(), prefix.rptr_dir()\n            assert nrptr and dir\n            new_id = new_id.extend(\n                ptrref=nrptr, direction=dir, ns=prefix.namespace)\n\n        if mapped := get_view_map_remapping(new_id, ctx):\n            hit = True\n            new_id = mapped.path_id\n\n    assert new_id and (new_id == path_id or hit)\n    return new_id\n\n\ndef _get_computable_ctx(\n    *,\n    rptr: irast.Pointer,\n    source: irast.Set,\n    source_scls: s_types.Type,\n    inner_source_path_id: irast.PathId,\n    path_id_ns: Optional[irast.Namespace],\n    same_scope: bool,\n    qlctx: context.ContextLevel,\n    ctx: context.ContextLevel\n) -> Callable[[], ContextManager[context.ContextLevel]]:\n\n    @contextlib.contextmanager\n    def newctx() -> Iterator[context.ContextLevel]:\n        with ctx.new() as subctx:\n            subctx.class_view_overrides = {}\n            subctx.partial_path_prefix = None\n\n            subctx.modaliases = qlctx.modaliases.copy()\n            subctx.aliased_views = qlctx.aliased_views.new_child()\n\n            subctx.view_nodes = qlctx.view_nodes.copy()\n            subctx.view_map = ctx.view_map.new_child()\n            subctx.view_sets = ctx.view_sets.copy()\n\n            source_scope = pathctx.get_set_scope(rptr.source, ctx=ctx)\n            if source_scope and source_scope.namespaces:\n                subctx.path_id_namespace |= source_scope.namespaces\n\n            if path_id_ns is not None:\n                subctx.path_id_namespace |= {path_id_ns}\n\n            pending_pid_ns = {ctx.aliases.get('ns')}\n\n            if path_id_ns is not None and same_scope:\n                pending_pid_ns.add(path_id_ns)\n\n            subctx.pending_stmt_own_path_id_namespace = (\n                frozenset(pending_pid_ns))\n\n            subns = set(pending_pid_ns)\n            subns.add(ctx.aliases.get('ns'))\n\n            # Include the namespace from the source in the namespace\n            # we compile under. This helps make sure the remapping\n            # lines up.\n            subns |= qlctx.path_id_namespace\n\n            subctx.pending_stmt_full_path_id_namespace = frozenset(subns)\n\n            # If one of the sources present at the definition site is still\n            # visible, make sure to hang on to the remapping.\n            for entry in qlctx.view_map.values():\n                for map_path_id, remapped in entry:\n                    if subctx.path_scope.is_visible(map_path_id):\n                        update_view_map(map_path_id, remapped, ctx=subctx)\n\n            inner_path_id = inner_source_path_id.merge_namespace(subns)\n            with subctx.new() as remapctx:\n                remapctx.path_id_namespace |= subns\n                # We need to run the inner_path_id through the same\n                # remapping process that happens in compile_path, or\n                # else the path id won't match, since the prefix will\n                # get remapped first.\n                inner_path_id = remap_path_id(inner_path_id, remapctx)\n\n            # XXX: THIS IS DODGY - wait, this is a no-op\n            remapped_source = new_set_from_set(\n                rptr.source, expr=rptr.source.expr, ctx=ctx)\n            update_view_map(inner_path_id, remapped_source, ctx=subctx)\n\n            yield subctx\n\n    return newctx\n\n\ndef maybe_materialize(\n    stype: s_types.Type | s_pointers.PointerLike,\n    ir: irast.Set,\n    *,\n    ctx: context.ContextLevel,\n) -> None:\n    if isinstance(stype, s_pointers.PseudoPointer):\n        return\n\n    # Search for a materialized_sets entry\n    while True:\n        if mat_entry := ctx.env.materialized_sets.get(stype):\n            break\n        # Search up for parent pointers, if applicable\n        if not isinstance(stype, s_pointers.Pointer):\n            return\n        bases = stype.get_bases(ctx.env.schema).objects(ctx.env.schema)\n        if not bases:\n            return\n        stype = bases[0]\n\n    # We've found an entry, populate it.\n    mat_qlstmt, reason = mat_entry\n    materialize_in_stmt = ctx.env.compiled_stmts[mat_qlstmt]\n    if materialize_in_stmt.materialized_sets is None:\n        materialize_in_stmt.materialized_sets = {}\n\n    assert not isinstance(stype, s_pointers.PseudoPointer)\n    if stype.id not in materialize_in_stmt.materialized_sets:\n        materialize_in_stmt.materialized_sets[stype.id] = (\n            irast.MaterializedSet(\n                materialized=ir, reason=reason, use_sets=[]))\n\n    mat_set = materialize_in_stmt.materialized_sets[stype.id]\n    mat_set.use_sets.append(ir)\n\n\ndef should_materialize(\n    ir: irast.Base,\n    *,\n    ptrcls: Optional[s_pointers.Pointer] = None,\n    materialize_visible: bool = False,\n    skipped_bindings: AbstractSet[irast.PathId] = frozenset(),\n    ctx: context.ContextLevel,\n) -> Sequence[irast.MaterializeReason]:\n    volatility = inference.infer_volatility(ir, ctx.env, exclude_dml=True)\n    reasons: list[irast.MaterializeReason] = []\n\n    if volatility.is_volatile():\n        reasons.append(irast.MaterializeVolatile())\n\n    if not isinstance(ir, irast.Set):\n        return reasons\n\n    typ = get_set_type(ir, ctx=ctx)\n\n    assert ir.path_scope_id is not None\n\n    # For shape elements, we need to materialize when they reference\n    # bindings that are visible from that point. This means that doing\n    # WITH/FOR bindings internally is fine, but referring to\n    # externally bound things needs materialization. We can't actually\n    # do this visibility analysis until we are done, though, so\n    # instead we just store the bindings.\n    if (\n        materialize_visible\n        and (vis := irutils.find_potentially_visible(\n            ir,\n            ctx.env.scope_tree_nodes[ir.path_scope_id],\n            ctx.env.scope_tree_nodes, skipped_bindings))\n    ):\n        reasons.append(irast.MaterializeVisible(\n            sets=vis, path_scope_id=ir.path_scope_id))\n\n    if ptrcls and ptrcls in ctx.env.source_map:\n        reasons += ctx.env.source_map[ptrcls].should_materialize\n\n    for r in should_materialize_type(typ, ctx=ctx):\n        # Rewrite visibility reasons from the typ to reflect this,\n        # the real bind point.\n        if isinstance(r, irast.MaterializeVolatile):\n            reasons.append(r)\n        else:\n            reasons.append(\n                irast.MaterializeVisible(\n                    sets=r.sets, path_scope_id=ir.path_scope_id))\n\n    return reasons\n\n\ndef should_materialize_type(\n    typ: s_types.Type, *, ctx: context.ContextLevel\n) -> list[irast.MaterializeReason]:\n    schema = ctx.env.schema\n    reasons: list[irast.MaterializeReason] = []\n    if isinstance(\n            typ, (s_objtypes.ObjectType, s_pointers.Pointer)):\n        for pointer in typ.get_pointers(schema).objects(schema):\n            if pointer in ctx.env.source_map:\n                reasons += ctx.env.source_map[pointer].should_materialize\n    elif isinstance(typ, s_types.Collection):\n        for sub in typ.get_subtypes(schema):\n            reasons += should_materialize_type(sub, ctx=ctx)\n\n    return reasons\n\n\ndef get_global_param(\n    glob: s_globals.Global | s_permissions.Permission,\n    *,\n    ctx: context.ContextLevel\n) -> irast.Global:\n    name = glob.get_name(ctx.env.schema)\n\n    if name not in ctx.env.query_globals:\n        param_name = f'__edb_global_{len(ctx.env.query_globals)}__'\n\n        if isinstance(glob, s_globals.Global):\n            # Globals\n            target = glob.get_target(ctx.env.schema)\n            target_typeref = typegen.type_to_typeref(target, env=ctx.env)\n\n            ctx.env.query_globals[name] = irast.Global(\n                name=param_name,\n                required=False,\n                schema_type=target,\n                ir_type=target_typeref,\n                global_name=name,\n                has_present_arg=glob.needs_present_arg(ctx.env.schema),\n                is_permission=False,\n            )\n\n        else:\n            # Permissions\n            target = ctx.env.schema.get('std::bool', type=s_types.Type)\n            target_typeref = typegen.type_to_typeref(target, env=ctx.env)\n\n            ctx.env.query_globals[name] = irast.Global(\n                name=param_name,\n                required=True,\n                schema_type=target,\n                ir_type=target_typeref,\n                global_name=name,\n                has_present_arg=False,\n                is_permission=True,\n            )\n\n    return ctx.env.query_globals[name]\n\n\ndef get_global_param_sets(\n    glob: s_globals.Global | s_permissions.Permission,\n    *,\n    ctx: context.ContextLevel,\n    is_implicit_global: bool = False,\n) -> tuple[irast.Set, Optional[irast.Set]]:\n    param = get_global_param(glob, ctx=ctx)\n    default = (\n        glob.get_default(ctx.env.schema)\n        if isinstance(glob, s_globals.Global) else\n        None\n    )\n\n    # This function is called to compile either a global expr or the global\n    # params for a function call. Both are compiled as QueryParameter.\n    assert ctx.env.options.func_params is None\n\n    param_set = ensure_set(\n        irast.QueryParameter(\n            name=param.name,\n            required=param.required and not bool(default),\n            typeref=param.ir_type,\n            is_implicit_global=is_implicit_global,\n        ),\n        ctx=ctx,\n    )\n\n    if (\n        isinstance(glob, s_globals.Global)\n        and glob.needs_present_arg(ctx.env.schema)\n    ):\n        present_set = ensure_set(\n            irast.QueryParameter(\n                name=param.name + \"present__\",\n                required=True,\n                typeref=typegen.type_to_typeref(\n                    ctx.env.schema.get('std::bool', type=s_types.Type),\n                    env=ctx.env,\n                ),\n                is_implicit_global=is_implicit_global,\n            ),\n            ctx=ctx,\n        )\n    else:\n        present_set = None\n\n    return param_set, present_set\n\n\ndef get_func_global_json_arg(*, ctx: context.ContextLevel) -> irast.Set:\n    json_type = ctx.env.schema.get('std::json', type=s_types.Type)\n    json_typeref = typegen.type_to_typeref(json_type, env=ctx.env)\n    name = '__edb_json_globals__'\n\n    is_func_param = ctx.env.options.func_params is not None\n    parameter_type = (\n        irast.FunctionParameter if is_func_param else irast.QueryParameter\n    )\n\n    # If this is because we have json params, not because we're in a\n    # function, we need to register it.\n    if ctx.env.options.json_parameters:\n        qname = s_name.QualName('__', name)\n        ctx.env.query_globals[qname] = irast.Global(\n            name=name,\n            required=False,\n            schema_type=json_type,\n            ir_type=json_typeref,\n            global_name=qname,\n            has_present_arg=False,\n            is_permission=False,\n        )\n\n    return ensure_set(\n        parameter_type(\n            name=name,\n            required=True,\n            typeref=json_typeref,\n        ),\n        ctx=ctx,\n    )\n\n\ndef get_func_global_param_sets(\n    glob: s_globals.Global | s_permissions.Permission,\n    *,\n    ctx: context.ContextLevel,\n) -> tuple[qlast.Expr, Optional[qlast.Expr]]:\n    # NB: updates ctx anchors\n\n    # Make sure that we properly track the globals we use in functions\n    get_global_param(glob, ctx=ctx)\n\n    with ctx.new() as subctx:\n        name = str(glob.get_name(ctx.env.schema))\n\n        glob_set = get_func_global_json_arg(ctx=ctx)\n        glob_anchor = qlast.FunctionCall(\n            func=('__std__', 'json_get'),\n            args=[\n                subctx.create_anchor(glob_set, 'a'),\n                qlast.Constant.string(value=str(name)),\n            ],\n        )\n\n        if isinstance(glob, s_globals.Global):\n            target = glob.get_target(ctx.env.schema)\n\n        else:\n            # Permissions\n            target = ctx.env.schema.get('std::bool', type=s_types.Type)\n\n        type = typegen.type_to_ql_typeref(target, ctx=ctx)\n        main_set = qlast.TypeCast(expr=glob_anchor, type=type)\n\n        if (\n            isinstance(glob, s_globals.Global)\n            and glob.needs_present_arg(ctx.env.schema)\n        ):\n            present_set = qlast.UnaryOp(\n                op='EXISTS',\n                operand=glob_anchor,\n            )\n        else:\n            present_set = None\n\n    return main_set, present_set\n\n\ndef get_globals_as_json(\n    globs: Sequence[s_globals.Global | s_permissions.Permission],\n    *,\n    ctx: context.ContextLevel,\n    span: Optional[qlast.Span],\n) -> irast.Set:\n    \"\"\"Build a json object that contains the values of `globs`\n\n    The format of the object is simply\n       {\"<glob name 1>\": <json>glob_val_1, ...},\n    where values that are unset or set to {} are represented as null,\n    with one catch:\n       for globals that need \"present\" arguments (that is, optional globals\n       with default values), we need to distinguish between the global\n       being unset and being set to {}. In that case, we represent being\n       set to {} with null and being unset by omitting it from the object.\n    \"\"\"\n    # TODO: arrange to compute this once per query, in a CTE or some such?\n\n    # If globals are empty, arrange to still pass in the argument but\n    # don't put anything in it.\n    if ctx.env.options.make_globals_empty:\n        globs = ()\n\n    objctx = ctx.env.options.schema_object_context\n    is_constraint_like = objctx in (s_constr.Constraint, s_indexes.Index)\n    if globs and is_constraint_like:\n        assert objctx\n        typname = objctx.get_schema_class_displayname()\n        # XXX: or should we pass in empty globals, in this situation?\n        raise errors.SchemaDefinitionError(\n            f'functions that reference global variables cannot be called '\n            f'from {typname}',\n            span=span)\n\n    null_expr = qlast.FunctionCall(\n        func=('__std__', 'to_json'),\n        args=[qlast.Constant.string(value=\"null\")],\n    )\n\n    with ctx.new() as subctx:\n        subctx.anchors = subctx.anchors.copy()\n        normal_els = []\n        full_objs: list[qlast.Expr] = []\n\n        json_type = qlast.TypeName(maintype=qlast.ObjectRef(\n            module='__std__', name='json'))\n\n        for glob in globs:\n            param, present = get_global_param_sets(\n                glob, is_implicit_global=True, ctx=ctx)\n            # The name of the global isn't syntactically a valid identifier\n            # for a namedtuple element but nobody can stop us!\n            name = str(glob.get_name(ctx.env.schema))\n\n            main_param = subctx.create_anchor(param, 'a')\n            tuple_el = qlast.TupleElement(\n                name=qlast.Ptr(name=name),\n                val=qlast.BinOp(\n                    op='??',\n                    left=qlast.TypeCast(expr=main_param, type=json_type),\n                    right=null_expr,\n                )\n            )\n\n            if not present:\n                # For normal globals, just stick the element in the tuple.\n                normal_els.append(tuple_el)\n            else:\n                # For globals with a present arg, we conditionally\n                # construct a one-element object if it is present\n                # and an empty object if it is not. These are\n                # be combined using ++.\n                present_param = subctx.create_anchor(present, 'a')\n                tup = qlast.TypeCast(\n                    expr=qlast.NamedTuple(elements=[tuple_el]),\n                    type=json_type,\n                )\n\n                full_objs.append(qlast.IfElse(\n                    condition=present_param,\n                    if_expr=tup,\n                    else_expr=qlast.FunctionCall(\n                        func=('__std__', 'to_json'),\n                        args=[qlast.Constant.string(value=\"{}\")],\n                    )\n                ))\n\n        # If access policies are disabled, stick a value in the blob\n        # to indicate that.  We do this using a full object so it\n        # works in constraints and the like, where the tuple->json cast\n        # isn't supported yet.\n        if (\n            not ctx.env.options.apply_user_access_policies\n            or not ctx.env.options.apply_query_rewrites\n        ) and not is_constraint_like:\n            full_objs.append(qlast.FunctionCall(\n                func=('__std__', 'to_json'),\n                args=[qlast.Constant.string(\n                    value='{\"__disable_access_policies\": true}'\n                )],\n            ))\n\n        full_expr: qlast.Expr\n        if not normal_els and not full_objs:\n            full_expr = null_expr\n        else:\n            simple_obj = None\n            if normal_els or not full_objs:\n                simple_obj = qlast.TypeCast(\n                    expr=qlast.NamedTuple(elements=normal_els),\n                    type=json_type,\n                )\n\n            full_expr = astutils.extend_binop(simple_obj, *full_objs, op='++')\n\n        return dispatch.compile(full_expr, ctx=subctx)\n"
  },
  {
    "path": "edb/edgeql/compiler/stmt.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL statement compilation routines.\"\"\"\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    Sequence,\n    cast\n)\n\nfrom collections import defaultdict\nimport textwrap\nimport itertools\n\nfrom edb import errors\nfrom edb.common import ast\nfrom edb.common import span as edb_span\nfrom edb.common.typeutils import not_none\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils\nfrom edb.ir import utils as irutils\n\nfrom edb.schema import ddl as s_ddl\nfrom edb.schema import functions as s_func\nfrom edb.schema import links as s_links\nfrom edb.schema import properties as s_props\nfrom edb.schema import modules as s_mod\nfrom edb.schema import name as s_name\nfrom edb.schema import objects as s_obj\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import pseudo as s_pseudo\nfrom edb.schema import schema as s_schema\nfrom edb.schema import types as s_types\nfrom edb.schema import utils as s_utils\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import utils as qlutils\nfrom edb.edgeql import qltypes\nfrom edb.edgeql import desugar_group\n\nfrom . import astutils\nfrom . import clauses\nfrom . import context\nfrom . import config_desc\nfrom . import dispatch\nfrom . import inference\nfrom . import pathctx\nfrom . import policies\nfrom . import setgen\nfrom . import viewgen\nfrom . import schemactx\nfrom . import stmtctx\nfrom . import typegen\nfrom . import conflicts\n\n\ndef try_desugar(\n    expr: qlast.Query, *, ctx: context.ContextLevel\n) -> Optional[irast.Set]:\n    new_syntax = desugar_group.try_group_rewrite(expr, aliases=ctx.aliases)\n    if new_syntax:\n        return dispatch.compile(new_syntax, ctx=ctx)\n    return None\n\n\ndef _protect_expr(\n    expr: Optional[qlast.Expr], *, ctx: context.ContextLevel\n) -> None:\n    if ctx.no_factoring:\n        while isinstance(expr, qlast.Shape):\n            expr.allow_factoring = True\n            expr = expr.expr\n        if isinstance(expr, qlast.Path):\n            expr.allow_factoring = True\n\n\n@dispatch.compile.register(qlast.SelectQuery)\ndef compile_SelectQuery(\n    expr: qlast.SelectQuery, *, ctx: context.ContextLevel\n) -> irast.Set:\n    if rewritten := try_desugar(expr, ctx=ctx):\n        return rewritten\n\n    _protect_expr(expr.result, ctx=ctx)\n\n    with ctx.subquery() as sctx:\n        stmt = irast.SelectStmt()\n        init_stmt(stmt, expr, ctx=sctx, parent_ctx=ctx)\n        if expr.implicit:\n            # Make sure path prefix does not get blown away by\n            # implicit subqueries.\n            sctx.partial_path_prefix = ctx.partial_path_prefix\n            stmt.implicit_wrapper = True\n\n        # If there is an offset or a limit, this query was a wrapper\n        # around something else, and we need to forward_rptr\n\n        forward_rptr = (\n            bool(expr.offset)\n            or bool(expr.limit)\n            or expr.rptr_passthrough\n            # We need to preserve view_rptr if this SELECT is just\n            # an implicit wrapping of a single DISTINCT, because otherwise\n            # using a DISTINCT to satisfy link multiplicity requirement\n            # will kill the link properties.\n            #\n            # This includes problems with initializing the schema itself.\n            or (\n                isinstance(expr.result, qlast.UnaryOp)\n                and expr.result.op == 'DISTINCT'\n            )\n            or (\n                isinstance(expr.result, qlast.FunctionCall)\n                and expr.result.func in (\n                    'assert_distinct', 'assert_single', 'assert_exists')\n            )\n        )\n\n        stmt.result = compile_result_clause(\n            expr.result,\n            view_scls=ctx.view_scls,\n            view_rptr=ctx.view_rptr,\n            result_alias=expr.result_alias,\n            view_name=ctx.toplevel_result_view_name,\n            forward_rptr=forward_rptr,\n            ctx=sctx)\n\n        stmt.where = clauses.compile_where_clause(expr.where, ctx=sctx)\n\n        stmt.orderby = clauses.compile_orderby_clause(expr.orderby, ctx=sctx)\n\n        stmt.offset = clauses.compile_limit_offset_clause(\n            expr.offset, ctx=sctx)\n\n        stmt.limit = clauses.compile_limit_offset_clause(\n            expr.limit, ctx=sctx)\n\n        result = fini_stmt(stmt, ctx=sctx, parent_ctx=ctx)\n\n    return result\n\n\n@dispatch.compile.register(qlast.ForQuery)\ndef compile_ForQuery(\n    qlstmt: qlast.ForQuery, *, ctx: context.ContextLevel\n) -> irast.Set:\n    if rewritten := try_desugar(qlstmt, ctx=ctx):\n        return rewritten\n\n    with ctx.subquery() as sctx:\n        stmt = irast.SelectStmt(span=qlstmt.span)\n        init_stmt(stmt, qlstmt, ctx=sctx, parent_ctx=ctx)\n\n        # As an optimization, if the iterator is a singleton set, use\n        # the element directly.\n        iterator = qlstmt.iterator\n        if isinstance(iterator, qlast.Set) and len(iterator.elements) == 1:\n            iterator = iterator.elements[0]\n\n        contains_dml = astutils.contains_dml(qlstmt.result, ctx=ctx)\n\n        with sctx.new() as ectx:\n            if ectx.expr_exposed:\n                ectx.expr_exposed = context.Exposure.BINDING\n            iterator_view = stmtctx.declare_view(\n                astutils.ensure_ql_select(iterator),\n                s_name.UnqualName(qlstmt.iterator_alias),\n                factoring_fence=contains_dml,\n                path_id_namespace=sctx.path_id_namespace,\n                binding_kind=irast.BindingKind.For,\n                ctx=ectx,\n            )\n\n        iterator_stmt = setgen.new_set_from_set(iterator_view, ctx=sctx)\n        iterator_view.is_visible_binding_ref = True\n        stmt.iterator_stmt = iterator_stmt\n\n        iterator_type = setgen.get_set_type(iterator_stmt, ctx=ctx)\n        if iterator_type.is_any(ctx.env.schema):\n            raise errors.QueryError(\n                'FOR statement has iterator of indeterminate type',\n                span=ctx.env.type_origins.get(iterator_type),\n            )\n\n        view_scope_info = sctx.env.path_scope_map[iterator_view]\n\n        pathctx.register_set_in_scope(\n            iterator_stmt,\n            path_scope=sctx.path_scope,\n            optional=qlstmt.optional,\n            ctx=sctx,\n        )\n\n        sctx.iterator_path_ids |= {stmt.iterator_stmt.path_id}\n        node = sctx.path_scope.find_descendant(iterator_stmt.path_id)\n        if node is not None:\n            # If the body contains DML, then we need to prohibit\n            # correlation between the iterator and the enclosing\n            # query, since the correlation imposes compilation issues\n            # we aren't willing to tackle.\n            #\n            # Do this by sticking the iterator subtree onto a branch\n            # with a factoring fence.\n            if contains_dml:\n                node = node.attach_branch()\n                node.factoring_fence = True\n                node.factoring_allowlist.update(ctx.iterator_path_ids)\n                node = node.attach_branch()\n\n            node.attach_subtree(\n                view_scope_info.path_scope,\n                span=iterator.span,\n                ctx=ctx,\n            )\n\n        # Compile the body\n        with sctx.newscope(fenced=True) as bctx:\n            stmt.result = setgen.scoped_set(\n                compile_result_clause(\n                    # Make sure it is a stmt, so that shapes inside the body\n                    # get resolved there.\n                    astutils.ensure_ql_query(qlstmt.result),\n                    view_scls=ctx.view_scls,\n                    view_rptr=ctx.view_rptr,\n                    view_name=ctx.toplevel_result_view_name,\n                    forward_rptr=True,\n                    ctx=bctx,\n                ),\n                ctx=bctx,\n            )\n\n        # Inject an implicit limit if appropriate\n        if ((ctx.expr_exposed or sctx.stmt is ctx.toplevel_stmt)\n                and ctx.implicit_limit):\n            stmt.limit = dispatch.compile(\n                qlast.Constant.integer(ctx.implicit_limit),\n                ctx=sctx,\n            )\n\n        result = fini_stmt(stmt, ctx=sctx, parent_ctx=ctx)\n\n    return result\n\n\ndef _make_group_binding(\n    stype: s_types.Type,\n    alias: str,\n    *,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n    \"\"\"Make a binding for one of the \"dummy\" bindings used in group\"\"\"\n    binding_type = schemactx.derive_view(\n        stype,\n        derived_name=s_name.QualName('__derived__', alias),\n        preserve_shape=True, ctx=ctx)\n\n    binding_set = setgen.class_set(binding_type, ctx=ctx)\n    binding_set.is_visible_binding_ref = True\n\n    name = s_name.UnqualName(alias)\n    ctx.aliased_views[name] = binding_set\n    ctx.view_sets[binding_type] = binding_set\n    ctx.env.path_scope_map[binding_set] = context.ScopeInfo(\n        path_scope=ctx.path_scope,\n        binding_kind=irast.BindingKind.For,\n        pinned_path_id_ns=ctx.path_id_namespace,\n    )\n\n    return binding_set\n\n\n@dispatch.compile.register(qlast.InternalGroupQuery)\ndef compile_InternalGroupQuery(\n    expr: qlast.InternalGroupQuery, *, ctx: context.ContextLevel\n) -> irast.Set:\n    # We disallow use of FOR GROUP except for when running in test mode.\n    if not expr.from_desugaring and not ctx.env.options.testmode:\n        raise errors.UnsupportedFeatureError(\n            \"'FOR GROUP' is an internal testing feature\",\n            span=expr.span,\n        )\n\n    _protect_expr(expr.subject, ctx=ctx)\n    _protect_expr(expr.result, ctx=ctx)\n\n    with ctx.subquery() as sctx:\n        stmt = irast.GroupStmt(by=expr.by)\n        init_stmt(stmt, expr, ctx=sctx, parent_ctx=ctx)\n\n        with sctx.newscope(fenced=True) as topctx:\n            # N.B: Subject is exposed because we want any shape on the\n            # subject to be exposed on bare references to the group\n            # alias.  This is frankly pretty dodgy behavior for\n            # FOR GROUP to have but the real GROUP needs to\n            # maintain shapes, and this is the easiest way to handle\n            # that.\n            stmt.subject = compile_result_clause(\n                expr.subject,\n                result_alias=expr.subject_alias,\n                exprtype=s_types.ExprType.Group,\n                ctx=topctx)\n\n            if topctx.partial_path_prefix:\n                pathctx.register_set_in_scope(\n                    topctx.partial_path_prefix, ctx=topctx)\n\n            # compile the USING\n            assert expr.using is not None\n\n            for using_entry in expr.using:\n                # Fail on keys named 'id', since we can't put them\n                # in the output free object.\n                if using_entry.alias == 'id':\n                    raise errors.UnsupportedFeatureError(\n                        \"may not name a grouping alias 'id'\",\n                        span=using_entry.span,\n                    )\n                elif desugar_group.key_name(using_entry.alias) == 'id':\n                    raise errors.UnsupportedFeatureError(\n                        \"may not group by a field named id\",\n                        span=using_entry.expr.span,\n                        hint=\"try 'using id_ := .id'\",\n                    )\n\n                with topctx.newscope(fenced=True) as scopectx:\n                    if scopectx.expr_exposed:\n                        scopectx.expr_exposed = context.Exposure.BINDING\n                    binding = stmtctx.declare_view(\n                        using_entry.expr,\n                        s_name.UnqualName(using_entry.alias),\n                        binding_kind=irast.BindingKind.With,\n                        path_id_namespace=scopectx.path_id_namespace,\n                        ctx=scopectx,\n                    )\n                    binding.span = using_entry.expr.span\n                    stmt.using[using_entry.alias] = (\n                        setgen.new_set_from_set(binding, ctx=sctx),\n                        qltypes.Cardinality.UNKNOWN)\n                    binding.is_visible_binding_ref = True\n\n            subject_stype = setgen.get_set_type(stmt.subject, ctx=topctx)\n            stmt.group_binding = _make_group_binding(\n                subject_stype, expr.group_alias, ctx=topctx)\n\n            # # Compile the shape on the group binding, in case we need it\n            # viewgen.late_compile_view_shapes(stmt.group_binding, ctx=topctx)\n\n            if expr.grouping_alias:\n                ctx.env.schema, grouping_stype = s_types.Array.create(\n                    ctx.env.schema,\n                    element_type=(\n                        ctx.env.schema.get('std::str', type=s_types.Type)\n                    )\n                )\n                stmt.grouping_binding = _make_group_binding(\n                    grouping_stype, expr.grouping_alias, ctx=topctx)\n\n        # Check that the by clause is legit\n        by_refs = ast.find_children(stmt.by, qlast.ObjectRef)\n        for by_ref in by_refs:\n            if by_ref.name not in stmt.using:\n                raise errors.InvalidReferenceError(\n                    f\"variable '{by_ref.name}' referenced in BY but not \"\n                    f\"declared in USING\",\n                    span=by_ref.span,\n                )\n\n        # compile the output\n        # newscope because we don't want the result to get assigned the\n        # same statement scope as the subject and elements, which we\n        # need to stick in the real GROUP BY\n        with sctx.newscope(fenced=True) as bctx:\n            pathctx.register_set_in_scope(\n                stmt.group_binding, path_scope=bctx.path_scope, ctx=bctx\n            )\n\n            # Compile the shape on the group binding, in case we need it\n            viewgen.late_compile_view_shapes(stmt.group_binding, ctx=bctx)\n\n            node = bctx.path_scope.find_descendant(stmt.group_binding.path_id)\n            not_none(node).is_group = True\n            for using_value, _ in stmt.using.values():\n                pathctx.register_set_in_scope(\n                    using_value, path_scope=bctx.path_scope, ctx=bctx\n                )\n\n            if stmt.grouping_binding:\n                pathctx.register_set_in_scope(\n                    stmt.grouping_binding, path_scope=bctx.path_scope, ctx=bctx\n                )\n\n            stmt.result = compile_result_clause(\n                astutils.ensure_ql_query(expr.result),\n                result_alias=expr.result_alias,\n                ctx=bctx)\n\n            stmt.where = clauses.compile_where_clause(expr.where, ctx=bctx)\n\n            stmt.orderby = clauses.compile_orderby_clause(\n                expr.orderby, ctx=bctx)\n\n        result = fini_stmt(stmt, ctx=sctx, parent_ctx=ctx)\n\n    return result\n\n\n@dispatch.compile.register(qlast.GroupQuery)\ndef compile_GroupQuery(\n    expr: qlast.GroupQuery, *, ctx: context.ContextLevel\n) -> irast.Set:\n    return dispatch.compile(\n        desugar_group.desugar_group(expr, ctx.aliases),\n        ctx=ctx,\n    )\n\n\n@dispatch.compile.register(qlast.InsertQuery)\ndef compile_InsertQuery(\n    expr: qlast.InsertQuery, *, ctx: context.ContextLevel\n) -> irast.Set:\n\n    if ctx.disallow_dml:\n        raise errors.QueryError(\n            f'INSERT statements cannot be used {ctx.disallow_dml}',\n            hint=(\n                f'To resolve this try to factor out the mutation '\n                f'expression into the top-level WITH block.'\n            ),\n            span=expr.span,\n        )\n\n    # Record this node in the list of potential DML expressions.\n    ctx.env.dml_exprs.append(expr)\n\n    with ctx.subquery() as ictx:\n        stmt = irast.InsertStmt(span=expr.span)\n        init_stmt(stmt, expr, ctx=ictx, parent_ctx=ctx)\n\n        with ictx.new() as ectx:\n            ectx.expr_exposed = context.Exposure.UNEXPOSED\n            subject = dispatch.compile(\n                qlast.Path(steps=[expr.subject], allow_factoring=True), ctx=ectx\n            )\n        assert isinstance(subject, irast.Set)\n\n        subject_stype = setgen.get_set_type(subject, ctx=ictx)\n\n        # If we are INSERTing a type that we are in the ELSE block of,\n        # we need to error out.\n        if ictx.inserting_paths.get(subject.path_id) == 'else':\n            setgen.raise_self_insert_error(\n                subject_stype, expr.subject.span, ctx=ctx)\n\n        if subject_stype.get_abstract(ctx.env.schema):\n            raise errors.QueryError(\n                f'cannot insert into abstract '\n                f'{subject_stype.get_verbosename(ctx.env.schema)}',\n                span=expr.subject.span)\n\n        if subject_stype.is_free_object_type(ctx.env.schema):\n            raise errors.QueryError(\n                f'free objects cannot be inserted',\n                span=expr.subject.span)\n\n        if subject_stype.is_view(ctx.env.schema):\n            raise errors.QueryError(\n                f'cannot insert into expression alias '\n                f'{str(subject_stype.get_shortname(ctx.env.schema))!r}',\n                span=expr.subject.span)\n\n        if _is_forbidden_stdlib_type_for_mod(subject_stype, ctx):\n            raise errors.QueryError(\n                f'cannot insert standard library type '\n                f'{subject_stype.get_displayname(ctx.env.schema)}',\n                span=expr.subject.span)\n\n        with ictx.new() as bodyctx:\n            # Self-references in INSERT are prohibited.\n            pathctx.ban_inserting_path(\n                subject.path_id, location='body', ctx=bodyctx)\n\n            bodyctx.class_view_overrides = ictx.class_view_overrides.copy()\n            bodyctx.implicit_id_in_shapes = False\n            bodyctx.implicit_tid_in_shapes = False\n            bodyctx.implicit_tname_in_shapes = False\n            bodyctx.implicit_limit = 0\n\n            stmt.subject = compile_query_subject(\n                subject,\n                shape=expr.shape,\n                view_rptr=ctx.view_rptr,\n                compile_views=True,\n                exprtype=s_types.ExprType.Insert,\n                ctx=bodyctx,\n                span=expr.span,\n            )\n\n        stmt_subject_stype = setgen.get_set_type(subject, ctx=ictx)\n        assert isinstance(stmt_subject_stype, s_objtypes.ObjectType)\n\n        stmt.conflict_checks = conflicts.compile_inheritance_conflict_checks(\n            stmt, stmt_subject_stype, ctx=ictx)\n\n        if expr.unless_conflict is not None:\n            constraint_spec, else_branch = expr.unless_conflict\n\n            if constraint_spec:\n                stmt.on_conflict = conflicts.compile_insert_unless_conflict_on(\n                    stmt, stmt_subject_stype, constraint_spec, else_branch,\n                    ctx=ictx)\n            else:\n                stmt.on_conflict = conflicts.compile_insert_unless_conflict(\n                    stmt, stmt_subject_stype, ctx=ictx)\n\n        conflicts.check_for_isolation_conflicts(\n            stmt, stmt_subject_stype, ctx=ictx)\n\n        mat_stype = schemactx.get_material_type(stmt_subject_stype, ctx=ctx)\n        result = setgen.class_set(\n            mat_stype, path_id=stmt.subject.path_id, ctx=ctx\n        )\n\n        with ictx.new() as resultctx:\n            stmt.result = compile_query_subject(\n                result,\n                view_scls=ctx.view_scls,\n                view_name=ctx.toplevel_result_view_name,\n                compile_views=ictx.stmt is ictx.toplevel_stmt,\n                ctx=resultctx,\n                span=expr.span,\n            )\n\n        if pol_condition := policies.compile_dml_write_policies(\n            mat_stype, result, mode=qltypes.AccessKind.Insert, ctx=ictx\n        ):\n            stmt.write_policies[mat_stype.id] = pol_condition\n\n        # Compute the unioned output type if needed\n        if stmt.on_conflict and stmt.on_conflict.else_ir:\n            final_typ = typegen.infer_common_type(\n                [stmt.result, stmt.on_conflict.else_ir], ctx.env)\n            if final_typ is None:\n                raise errors.QueryError('could not determine INSERT type',\n                                        span=stmt.span)\n            stmt.final_typeref = typegen.type_to_typeref(final_typ, env=ctx.env)\n\n        # Wrap the statement.\n        result = fini_stmt(stmt, ctx=ictx, parent_ctx=ctx)\n\n        # If we have an ELSE clause, and this is a toplevel statement,\n        # we need to compile_query_subject *again* on the outer query,\n        # in order to produce a view for the joined output, which we\n        # need to have to generate the proper type descriptor.  This\n        # feels like somewhat of a hack; I think it might be possible\n        # to do something more general elsewhere.\n        if (\n            expr.unless_conflict\n            and expr.unless_conflict[1]\n            and ictx.stmt is ctx.toplevel_stmt\n        ):\n            with ictx.new() as resultctx:\n                resultctx.expr_exposed = context.Exposure.EXPOSED\n                result = compile_query_subject(\n                    result,\n                    view_name=ctx.toplevel_result_view_name,\n                    compile_views=ictx.stmt is ctx.toplevel_stmt,\n                    ctx=resultctx,\n                    span=result.span,\n                )\n\n    return result\n\n\n@dispatch.compile.register(qlast.UpdateQuery)\ndef compile_UpdateQuery(\n    expr: qlast.UpdateQuery, *, ctx: context.ContextLevel\n) -> irast.Set:\n\n    if ctx.disallow_dml:\n        raise errors.QueryError(\n            f'UPDATE statements cannot be used {ctx.disallow_dml}',\n            hint=(\n                f'To resolve this try to factor out the mutation '\n                f'expression into the top-level WITH block.'\n            ),\n            span=expr.span,\n        )\n\n    _protect_expr(expr.subject, ctx=ctx)\n\n    # Record this node in the list of DML statements.\n    ctx.env.dml_exprs.append(expr)\n\n    with ctx.subquery() as ictx:\n        stmt = irast.UpdateStmt(\n            span=expr.span,\n        )\n        init_stmt(stmt, expr, ctx=ictx, parent_ctx=ctx)\n\n        with ictx.new() as ectx:\n            ectx.expr_exposed = context.Exposure.UNEXPOSED\n            subject = dispatch.compile(expr.subject, ctx=ectx)\n        assert isinstance(subject, irast.Set)\n\n        subj_type = setgen.get_set_type(subject, ctx=ictx)\n        if not isinstance(subj_type, s_objtypes.ObjectType):\n            raise errors.QueryError(\n                f'cannot update non-ObjectType objects',\n                span=expr.subject.span\n            )\n\n        if subj_type.is_free_object_type(ctx.env.schema):\n            raise errors.QueryError(\n                f'free objects cannot be updated',\n                span=expr.subject.span)\n\n        mat_stype = schemactx.concretify(subj_type, ctx=ctx)\n\n        if _is_forbidden_stdlib_type_for_mod(mat_stype, ctx):\n            raise errors.QueryError(\n                f'cannot update standard library type '\n                f'{subj_type.get_displayname(ctx.env.schema)}',\n                span=expr.subject.span)\n\n        stmt._material_type = typeutils.type_to_typeref(\n            ctx.env.schema,\n            mat_stype,\n            include_children=True,\n            include_ancestors=True,\n            cache=ctx.env.type_ref_cache,\n        )\n\n        ictx.partial_path_prefix = subject\n\n        stmt.where = clauses.compile_where_clause(expr.where, ctx=ictx)\n\n        with ictx.new() as bodyctx:\n            bodyctx.class_view_overrides = ictx.class_view_overrides.copy()\n            bodyctx.implicit_id_in_shapes = False\n            bodyctx.implicit_tid_in_shapes = False\n            bodyctx.implicit_tname_in_shapes = False\n            bodyctx.implicit_limit = 0\n\n            stmt.subject = compile_query_subject(\n                subject,\n                shape=expr.shape,\n                view_rptr=ctx.view_rptr,\n                compile_views=True,\n                exprtype=s_types.ExprType.Update,\n                ctx=bodyctx,\n                span=expr.span,\n            )\n\n        result = setgen.class_set(\n            mat_stype, path_id=stmt.subject.path_id, ctx=ctx,\n        )\n\n        with ictx.new() as resultctx:\n            stmt.result = compile_query_subject(\n                result,\n                view_scls=ctx.view_scls,\n                view_name=ctx.toplevel_result_view_name,\n                compile_views=ictx.stmt is ictx.toplevel_stmt,\n                ctx=resultctx,\n                span=expr.span,\n            )\n\n        for dtype in schemactx.get_all_concrete(mat_stype, ctx=ctx):\n            if read_pol := policies.compile_dml_read_policies(\n                dtype, result, mode=qltypes.AccessKind.UpdateRead, ctx=ictx\n            ):\n                stmt.read_policies[dtype.id] = read_pol\n            if write_pol := policies.compile_dml_write_policies(\n                dtype, result, mode=qltypes.AccessKind.UpdateWrite, ctx=ictx\n            ):\n                stmt.write_policies[dtype.id] = write_pol\n\n            conflicts.check_for_isolation_conflicts(\n                stmt, dtype, mat_stype, ctx=ictx)\n\n        stmt.conflict_checks = conflicts.compile_inheritance_conflict_checks(\n            stmt, mat_stype, ctx=ictx)\n\n        result = fini_stmt(stmt, ctx=ictx, parent_ctx=ctx)\n\n    return result\n\n\n@dispatch.compile.register(qlast.DeleteQuery)\ndef compile_DeleteQuery(\n    expr: qlast.DeleteQuery, *, ctx: context.ContextLevel\n) -> irast.Set:\n\n    if ctx.disallow_dml:\n        raise errors.QueryError(\n            f'DELETE statements cannot be used {ctx.disallow_dml}',\n            hint=(\n                f'To resolve this try to factor out the mutation '\n                f'expression into the top-level WITH block.'\n            ),\n            span=expr.span,\n        )\n\n    _protect_expr(expr.subject, ctx=ctx)\n\n    # Record this node in the list of potential DML expressions.\n    ctx.env.dml_exprs.append(expr)\n\n    with ctx.subquery() as ictx:\n        stmt = irast.DeleteStmt(span=expr.span)\n        # Expand the DELETE from sugar into full DELETE (SELECT ...)\n        # form, if there's any additional clauses.\n        if any([expr.where, expr.orderby, expr.offset, expr.limit]):\n            if expr.offset or expr.limit:\n                subjql = qlast.SelectQuery(\n                    result=qlast.SelectQuery(\n                        result=expr.subject,\n                        where=expr.where,\n                        orderby=expr.orderby,\n                        span=expr.span,\n                        implicit=True,\n                    ),\n                    limit=expr.limit,\n                    offset=expr.offset,\n                    span=expr.span,\n                )\n            else:\n                subjql = qlast.SelectQuery(\n                    result=expr.subject,\n                    where=expr.where,\n                    orderby=expr.orderby,\n                    offset=expr.offset,\n                    limit=expr.limit,\n                    span=expr.span,\n                )\n\n            expr = qlast.DeleteQuery(\n                aliases=expr.aliases,\n                span=expr.span,\n                subject=subjql,\n            )\n\n        init_stmt(stmt, expr, ctx=ictx, parent_ctx=ctx)\n\n        # DELETE Expr is a delete(SET OF X), so we need a scope fence.\n        with ictx.newscope(fenced=True) as scopectx:\n            scopectx.implicit_limit = 0\n            scopectx.expr_exposed = context.Exposure.UNEXPOSED\n            subject = setgen.scoped_set(\n                dispatch.compile(expr.subject, ctx=scopectx), ctx=scopectx)\n\n        subj_type = setgen.get_set_type(subject, ctx=ictx)\n        if not isinstance(subj_type, s_objtypes.ObjectType):\n            raise errors.QueryError(\n                f'cannot delete non-ObjectType objects',\n                span=expr.subject.span\n            )\n\n        if subj_type.is_free_object_type(ctx.env.schema):\n            raise errors.QueryError(\n                f'free objects cannot be deleted',\n                span=expr.subject.span)\n\n        mat_stype = schemactx.concretify(subj_type, ctx=ctx)\n\n        if _is_forbidden_stdlib_type_for_mod(mat_stype, ctx):\n            raise errors.QueryError(\n                f'cannot delete standard library type '\n                f'{subj_type.get_displayname(ctx.env.schema)}',\n                span=expr.subject.span)\n\n        stmt._material_type = typeutils.type_to_typeref(\n            ctx.env.schema,\n            mat_stype,\n            include_children=True,\n            include_ancestors=True,\n            cache=ctx.env.type_ref_cache,\n        )\n\n        with ictx.new() as bodyctx:\n            bodyctx.implicit_id_in_shapes = False\n            bodyctx.implicit_tid_in_shapes = False\n            bodyctx.implicit_tname_in_shapes = False\n\n            stmt.subject = compile_query_subject(\n                subject,\n                shape=None,\n                exprtype=s_types.ExprType.Delete,\n                ctx=bodyctx,\n                span=expr.span,\n            )\n\n        result = setgen.class_set(\n            mat_stype, path_id=stmt.subject.path_id, ctx=ctx\n        )\n\n        with ictx.new() as resultctx:\n            stmt.result = compile_query_subject(\n                result,\n                view_scls=ctx.view_scls,\n                view_name=ctx.toplevel_result_view_name,\n                compile_views=ictx.stmt is ictx.toplevel_stmt,\n                ctx=resultctx,\n                span=expr.span,\n            )\n\n        for dtype in schemactx.get_all_concrete(mat_stype, ctx=ctx):\n            # Compile policies for every concrete type\n            if pol_cond := policies.compile_dml_read_policies(\n                dtype, result, mode=qltypes.AccessKind.Delete, ctx=ictx\n            ):\n                stmt.read_policies[dtype.id] = pol_cond\n\n            schema = ctx.env.schema\n            # And find any pointers to delete\n            ptrs = []\n            for ptr in dtype.get_pointers(schema).objects(schema):\n                # If there is a pointer that has a real table and doesn't\n                # have a special ON SOURCE DELETE policy, arrange to\n                # delete it in the query itself.\n                if not ptr.is_pure_computable(schema) and (\n                    not ptr.singular(schema)\n                    or ptr.has_user_defined_properties(schema)\n                ) and (\n                    not isinstance(ptr, s_links.Link)\n                    or ptr.get_on_source_delete(schema) ==\n                    s_links.LinkSourceDeleteAction.Allow\n                ):\n                    ptrs.append(typegen.ptr_to_ptrref(ptr, ctx=ctx))\n\n            stmt.links_to_delete[dtype.id] = tuple(ptrs)\n\n        result = fini_stmt(stmt, ctx=ictx, parent_ctx=ctx)\n\n    return result\n\n\n@dispatch.compile.register\ndef compile_DescribeStmt(\n    ql: qlast.DescribeStmt, *, ctx: context.ContextLevel\n) -> irast.Set:\n    with ctx.subquery() as ictx:\n        stmt = irast.SelectStmt()\n        init_stmt(stmt, ql, ctx=ictx, parent_ctx=ctx)\n\n        if ql.object is qlast.DescribeGlobal.Schema:\n            if ql.language is qltypes.DescribeLanguage.DDL:\n                # DESCRIBE SCHEMA AS DDL\n                text = s_ddl.ddl_text_from_schema(\n                    ctx.env.schema,\n                )\n            elif ql.language is qltypes.DescribeLanguage.SDL:\n                # DESCRIBE SCHEMA AS SDL\n                text = s_ddl.sdl_text_from_schema(\n                    ctx.env.schema,\n                )\n            else:\n                raise errors.QueryError(\n                    f'cannot describe full schema as {ql.language}')\n\n            ct = typegen.type_to_typeref(\n                ctx.env.get_schema_type_and_track(\n                    s_name.QualName('std', 'str')),\n                env=ctx.env,\n            )\n\n            stmt.result = setgen.ensure_set(\n                irast.StringConstant(value=text, typeref=ct),\n                ctx=ictx,\n            )\n\n        elif ql.object is qlast.DescribeGlobal.DatabaseConfig:\n            if ql.language is qltypes.DescribeLanguage.DDL:\n                stmt.result = config_desc.compile_describe_config(\n                    qltypes.ConfigScope.DATABASE, ctx=ictx)\n            else:\n                raise errors.QueryError(\n                    f'cannot describe config as {ql.language}')\n\n        elif ql.object is qlast.DescribeGlobal.InstanceConfig:\n            if ql.language is qltypes.DescribeLanguage.DDL:\n                stmt.result = config_desc.compile_describe_config(\n                    qltypes.ConfigScope.INSTANCE, ctx=ictx)\n            else:\n                raise errors.QueryError(\n                    f'cannot describe config as {ql.language}')\n\n        elif ql.object is qlast.DescribeGlobal.Roles:\n            if ql.language is qltypes.DescribeLanguage.DDL:\n                function_call = dispatch.compile(\n                    qlast.FunctionCall(\n                        func=('sys', '_describe_roles_as_ddl'),\n                    ),\n                    ctx=ictx)\n                stmt.result = function_call\n            else:\n                raise errors.QueryError(\n                    f'cannot describe roles as {ql.language}')\n\n        else:\n            assert isinstance(ql.object, qlast.ObjectRef), ql.object\n            modules = []\n            items: defaultdict[str, list[s_name.Name]] = defaultdict(list)\n            referenced_classes: list[s_obj.ObjectMeta] = []\n\n            objref = ql.object\n            itemclass = objref.itemclass\n\n            if itemclass is qltypes.SchemaObjectClass.MODULE:\n                mod = s_name.UnqualName(str(s_utils.ast_ref_to_name(objref)))\n                if not ctx.env.schema.get_global(\n                        s_mod.Module, mod, None):\n                    raise errors.InvalidReferenceError(\n                        f\"module '{mod}' does not exist\",\n                        span=objref.span,\n                    )\n\n                modules.append(mod)\n            else:\n                itemtype: Optional[type[s_obj.Object]] = None\n\n                name = s_utils.ast_ref_to_name(objref)\n                if itemclass is not None:\n                    if itemclass is qltypes.SchemaObjectClass.ALIAS:\n                        # Look for underlying derived type.\n                        itemtype = s_types.Type\n                    else:\n                        itemtype = (\n                            s_obj.ObjectMeta.get_schema_metaclass_for_ql_class(\n                                itemclass)\n                        )\n\n                last_exc = None\n                # Search in the current namespace AND in std. We do\n                # this to avoid masking a `std` object/function by one\n                # in a default module.\n                search_ns = [ictx.modaliases]\n                # Only check 'std' separately if the current\n                # modaliases don't already include it.\n                if ictx.modaliases.get(None, 'std') != 'std':\n                    search_ns.append({None: 'std'})\n\n                # Search in the current namespace AND in std.\n                for aliases in search_ns:\n                    # Use the specific modaliases instead of the\n                    # context ones.\n                    with ictx.subquery() as newctx:\n                        newctx.modaliases = aliases\n                        # Get the default module name\n                        modname = aliases[None]\n                        # Is the current item a function\n                        is_function = (itemclass is\n                                       qltypes.SchemaObjectClass.FUNCTION)\n\n                        # We need to check functions if we're looking for them\n                        # specifically or if this is a broad search. They are\n                        # handled separately because they allow multiple\n                        # matches for the same name.\n                        if (itemclass is None or is_function):\n                            funcs = s_func.lookup_functions(\n                                name,\n                                tuple(),\n                                module_aliases=aliases,\n                                schema=newctx.env.schema,\n                            )\n                            for func in funcs:\n                                items[f'function_{modname}'].append(\n                                    func.get_name(newctx.env.schema)\n                                )\n\n                        # Also find an object matching the name as long as\n                        # it's not a function we're looking for specifically.\n                        if not is_function:\n                            try:\n                                if itemclass is not \\\n                                        qltypes.SchemaObjectClass.ALIAS:\n                                    condition = None\n                                    label = None\n                                else:\n                                    condition = (\n                                        lambda obj:\n                                        obj.get_alias_is_persistent(\n                                            ctx.env.schema\n                                        )\n                                    )\n                                    label = 'alias'\n                                obj = schemactx.get_schema_object(\n                                    objref,\n                                    item_type=itemtype,\n                                    condition=condition,\n                                    label=label,\n                                    ctx=newctx,\n                                )\n                                items[f'other_{modname}'].append(\n                                    obj.get_name(newctx.env.schema))\n                            except errors.InvalidReferenceError as exc:\n                                # Record the exception to be possibly\n                                # raised if no matches are found\n                                last_exc = exc\n\n                # If we already have some results, suppress the exception,\n                # otherwise raise the recorded exception.\n                if not items and last_exc:\n                    raise last_exc\n\n                if not items:\n                    raise errors.InvalidReferenceError(\n                        f\"{str(itemclass).lower()} '{objref.name}' \"\n                        f\"does not exist\",\n                        span=objref.span,\n                    )\n\n            verbose = ql.options.get_flag('VERBOSE')\n\n            method: Any\n            if ql.language is qltypes.DescribeLanguage.DDL:\n                method = s_ddl.ddl_text_from_schema\n            elif ql.language is qltypes.DescribeLanguage.SDL:\n                method = s_ddl.sdl_text_from_schema\n            elif ql.language is qltypes.DescribeLanguage.TEXT:\n                method = s_ddl.descriptive_text_from_schema\n                if not verbose.val:\n                    referenced_classes = [s_links.Link, s_props.Property]\n            else:\n                raise errors.InternalServerError(\n                    f'cannot handle describe language {ql.language}'\n                )\n\n            # Based on the items found generate main text and a\n            # potential comment about masked items.\n            defmod = ictx.modaliases.get(None, 'std')\n            default_items = []\n            masked_items = set()\n            for objtype in ['function', 'other']:\n                defkey = f'{objtype}_{defmod}'\n                mskkey = f'{objtype}_std'\n\n                default_items += items.get(defkey, [])\n                if defkey in items and mskkey in items:\n                    # We have a match in default module and some masked.\n                    masked_items.update(items.get(mskkey, []))\n                else:\n                    default_items += items.get(mskkey, [])\n\n            # Throw out anything in the masked set that's already in\n            # the default.\n            masked_items.difference_update(default_items)\n\n            text = method(\n                ctx.env.schema,\n                included_modules=modules,\n                included_items=default_items,\n                included_ref_classes=referenced_classes,\n                include_module_ddl=False,\n                include_std_ddl=True,\n            )\n            if masked_items:\n                text += ('\\n\\n'\n                         '# The following builtins are masked by the above:'\n                         '\\n\\n')\n                masked = method(\n                    ctx.env.schema,\n                    included_modules=modules,\n                    included_items=masked_items,\n                    included_ref_classes=referenced_classes,\n                    include_module_ddl=False,\n                    include_std_ddl=True,\n                )\n                masked = textwrap.indent(masked, '# ')\n                text += masked\n\n            ct = typegen.type_to_typeref(\n                ctx.env.get_schema_type_and_track(\n                    s_name.QualName('std', 'str')),\n                env=ctx.env,\n            )\n\n            stmt.result = setgen.ensure_set(\n                irast.StringConstant(value=text, typeref=ct),\n                ctx=ictx,\n            )\n\n        result = fini_stmt(stmt, ctx=ictx, parent_ctx=ctx)\n\n    return result\n\n\n@dispatch.compile.register(qlast.Shape)\ndef compile_Shape(\n    shape: qlast.Shape, *, ctx: context.ContextLevel\n) -> irast.Set:\n\n    if ctx.no_factoring and not shape.allow_factoring:\n        return dispatch.compile(\n            qlast.SelectQuery(result=shape, implicit=True),\n            ctx=ctx,\n        )\n\n    shape_expr = shape.expr or qlutils.FREE_SHAPE_EXPR\n    with ctx.new() as subctx:\n        subctx.qlstmt = astutils.ensure_ql_query(shape)\n        subctx.stmt = stmt = irast.SelectStmt()\n        ctx.env.compiled_stmts[subctx.qlstmt] = stmt\n        subctx.class_view_overrides = subctx.class_view_overrides.copy()\n\n        with subctx.new() as exposed_ctx:\n            exposed_ctx.expr_exposed = context.Exposure.UNEXPOSED\n            expr = dispatch.compile(shape_expr, ctx=exposed_ctx)\n\n        expr_stype = setgen.get_set_type(expr, ctx=ctx)\n        if not isinstance(expr_stype, s_objtypes.ObjectType):\n            raise errors.QueryError(\n                f'shapes cannot be applied to '\n                f'{expr_stype.get_verbosename(ctx.env.schema)}',\n                span=shape.span,\n            )\n\n        stmt.result = compile_query_subject(\n            expr,\n            shape=shape.elements,\n            compile_views=False,\n            ctx=subctx,\n            span=expr.span)\n\n        ir_result = setgen.ensure_set(stmt, ctx=subctx)\n\n    return ir_result\n\n\ndef init_stmt(\n    irstmt: irast.Stmt,\n    qlstmt: qlast.Statement,\n    *,\n    ctx: context.ContextLevel,\n    parent_ctx: context.ContextLevel,\n) -> None:\n\n    ctx.env.compiled_stmts[qlstmt] = irstmt\n\n    irstmt.span = qlstmt.span\n\n    if isinstance(irstmt, irast.MutatingStmt):\n        # This is some kind of mutation, so we need to check if it is\n        # allowed.\n        if ctx.env.options.in_ddl_context_name is not None:\n            raise errors.SchemaDefinitionError(\n                f'mutations are invalid in '\n                f'{ctx.env.options.in_ddl_context_name}',\n                span=qlstmt.span,\n            )\n        elif (\n            (dv := ctx.defining_view) is not None\n            and dv.get_expr_type(ctx.env.schema) is s_types.ExprType.Select\n            and not (\n                # We allow DML in trivial *top-level* free objects\n                ctx.partial_path_prefix\n                and irutils.is_trivial_free_object(\n                    irutils.unwrap_set(ctx.partial_path_prefix))\n                # Find the enclosing context at the point the free object\n                # was defined.\n                and (outer_ctx := next((\n                    x for x in reversed(ctx._stack.stack)\n                    if isinstance(x, context.ContextLevel)\n                    and x.partial_path_prefix != ctx.partial_path_prefix\n                ), None))\n                and outer_ctx.expr_exposed\n            )\n        ):\n            # This is some shape in a regular query. Although\n            # DML is not allowed in the computable, but it may\n            # be possible to refactor it.\n            raise errors.QueryError(\n                f\"mutations are invalid in a shape's computed expression\",\n                hint=(\n                    f'To resolve this try to factor out the mutation '\n                    f'expression into the top-level WITH block.'\n                ),\n                span=qlstmt.span,\n            )\n\n    ctx.stmt = irstmt\n    ctx.qlstmt = qlstmt\n    if ctx.toplevel_stmt is None:\n        parent_ctx.toplevel_stmt = ctx.toplevel_stmt = irstmt\n\n    ctx.path_scope = parent_ctx.path_scope.attach_fence()\n\n    pending_own_ns = parent_ctx.pending_stmt_own_path_id_namespace\n    if pending_own_ns:\n        ctx.path_scope.add_namespaces(pending_own_ns)\n\n    pending_full_ns = parent_ctx.pending_stmt_full_path_id_namespace\n    if pending_full_ns:\n        ctx.path_id_namespace |= pending_full_ns\n\n    irstmt.parent_stmt = parent_ctx.stmt\n\n    irstmt.bindings = process_with_block(\n        qlstmt, ctx=ctx, parent_ctx=parent_ctx)\n\n    if isinstance(irstmt, irast.MutatingStmt):\n        ctx.path_scope.factoring_fence = True\n        ctx.path_scope.factoring_allowlist.update(ctx.iterator_path_ids)\n\n\ndef fini_stmt(\n    irstmt: irast.Stmt | irast.Set,\n    *,\n    ctx: context.ContextLevel,\n    parent_ctx: context.ContextLevel,\n) -> irast.Set:\n\n    view_name = parent_ctx.toplevel_result_view_name\n    t = setgen.get_expr_type(irstmt, ctx=ctx)\n\n    view: Optional[s_types.Type]\n    path_id: Optional[irast.PathId]\n\n    if isinstance(irstmt, irast.MutatingStmt):\n        ctx.env.dml_stmts.append(irstmt)\n        irstmt.rewrites = ctx.env.dml_rewrites.pop(irstmt.subject, None)\n\n    if (isinstance(t, s_pseudo.PseudoType)\n            and t.is_any(ctx.env.schema)):\n        # Need to produce something valid. Should get caught as an\n        # error later.\n        view = None\n        path_id = None\n\n    elif t.get_name(ctx.env.schema) == view_name:\n        # The view statement did contain a view declaration and\n        # generated a view class with the requested name.\n        view = t\n        path_id = pathctx.get_path_id(view, ctx=parent_ctx)\n    elif view_name is not None:\n        # The view statement did _not_ contain a view declaration,\n        # but we still want the correct path_id.\n        view_obj = ctx.env.schema.get(view_name, None)\n        if view_obj is not None:\n            assert isinstance(view_obj, s_types.Type)\n            view = view_obj\n        else:\n            view = schemactx.derive_view(\n                t,\n                derived_name=view_name,\n                preserve_shape=True,\n                attrs={'span': irstmt.span},\n                ctx=parent_ctx\n            )\n        path_id = pathctx.get_path_id(view, ctx=parent_ctx)\n    else:\n        view = None\n        path_id = None\n\n    type_override = view if view is not None else None\n    result = setgen.scoped_set(\n        irstmt, type_override=type_override, path_id=path_id, ctx=ctx)\n    if irstmt.span and not result.span:\n        result = setgen.new_set_from_set(\n            result, span=irstmt.span, ctx=ctx)\n\n    if view is not None:\n        parent_ctx.view_sets[view] = result\n\n    return result\n\n\ndef process_with_block(\n    edgeql_tree: qlast.Statement,\n    *,\n    ctx: context.ContextLevel,\n    parent_ctx: context.ContextLevel,\n) -> list[tuple[irast.Set, qltypes.Volatility]]:\n    if edgeql_tree.aliases is None:\n        return []\n\n    had_materialized = False\n    results = []\n    for with_entry in edgeql_tree.aliases:\n        if isinstance(with_entry, qlast.ModuleAliasDecl):\n            ctx.modaliases[with_entry.alias] = with_entry.module\n\n        elif isinstance(with_entry, qlast.AliasedExpr):\n            with ctx.new() as scopectx:\n                if scopectx.expr_exposed:\n                    scopectx.expr_exposed = context.Exposure.BINDING\n                binding = stmtctx.declare_view(\n                    with_entry.expr,\n                    s_name.UnqualName(with_entry.alias),\n                    binding_kind=irast.BindingKind.With,\n                    ctx=scopectx,\n                )\n                volatility = inference.infer_volatility(\n                    binding, ctx.env, exclude_dml=True\n                )\n                results.append((binding, volatility))\n\n                if reason := setgen.should_materialize(binding, ctx=ctx):\n                    had_materialized = True\n                    typ = setgen.get_set_type(binding, ctx=ctx)\n                    ctx.env.materialized_sets[typ] = edgeql_tree, reason\n                    setgen.maybe_materialize(typ, binding, ctx=ctx)\n\n        else:\n            raise RuntimeError(\n                f'unexpected expression in WITH block: {with_entry}')\n\n    if had_materialized:\n        # If we had to materialize, put the body of the statement into\n        # its own fence, to avoid potential spurious factoring when we\n        # compile view sets for materialized sets.\n        # (We could just *always* do this, but don't, to avoid cluttering\n        # up the scope tree more.)\n        ctx.path_scope = ctx.path_scope.attach_fence()\n\n    return results\n\n\ndef compile_result_clause(\n    result: qlast.Expr,\n    *,\n    view_scls: Optional[s_types.Type] = None,\n    view_rptr: Optional[context.ViewRPtr] = None,\n    view_name: Optional[s_name.QualName] = None,\n    exprtype: s_types.ExprType = s_types.ExprType.Select,\n    result_alias: Optional[str] = None,\n    forward_rptr: bool = False,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n    with ctx.new() as sctx:\n        if forward_rptr:\n            sctx.view_rptr = view_rptr\n            # sctx.view_scls = view_scls\n\n        if result_alias:\n            # `SELECT foo := expr` is equivalent to\n            # `WITH foo := expr SELECT foo`\n            rexpr = astutils.ensure_ql_select(result)\n\n            stmtctx.declare_view(\n                rexpr,\n                alias=s_name.UnqualName(result_alias),\n                binding_kind=irast.BindingKind.Select,\n                ctx=sctx,\n            )\n\n            result = qlast.Path(\n                steps=[qlast.ObjectRef(name=result_alias)],\n                allow_factoring=True,\n            )\n\n        result_expr: qlast.Expr\n        shape: Optional[Sequence[qlast.ShapeElement]]\n\n        if isinstance(result, qlast.Shape):\n            result_expr = result.expr or qlutils.FREE_SHAPE_EXPR\n            shape = result.elements\n        else:\n            result_expr = result\n            shape = None\n\n        if astutils.is_ql_empty_set(result_expr):\n            expr = setgen.new_empty_set(\n                stype=sctx.empty_result_type_hint,\n                alias=ctx.aliases.get('e'),\n                ctx=sctx,\n                span=result_expr.span,\n            )\n        elif astutils.is_ql_empty_array(result_expr):\n            type_hint: Optional[s_types.Type] = None\n            if (\n                sctx.empty_result_type_hint is not None\n                and sctx.empty_result_type_hint.is_array()\n            ):\n                type_hint = sctx.empty_result_type_hint\n\n            expr = setgen.new_array_set(\n                [],\n                stype=type_hint,\n                ctx=sctx,\n                span=result_expr.span,\n            )\n        else:\n            with sctx.new() as ectx:\n                if shape is not None:\n                    ectx.expr_exposed = context.Exposure.UNEXPOSED\n                expr = dispatch.compile(result_expr, ctx=ectx)\n\n        ctx.partial_path_prefix = expr\n\n        ir_result = compile_query_subject(\n            expr, shape=shape, view_rptr=view_rptr, view_name=view_name,\n            forward_rptr=forward_rptr,\n            result_alias=result_alias,\n            view_scls=view_scls,\n            allow_select_shape_inject=False,\n            exprtype=exprtype,\n            compile_views=ctx.stmt is ctx.toplevel_stmt,\n            ctx=sctx,\n            span=result.span\n        )\n\n        ctx.partial_path_prefix = ir_result\n\n    return ir_result\n\n\ndef compile_query_subject(\n        set: irast.Set,\n        *,\n        shape: Optional[list[qlast.ShapeElement]]=None,\n        view_rptr: Optional[context.ViewRPtr]=None,\n        view_name: Optional[s_name.QualName]=None,\n        result_alias: Optional[str]=None,\n        view_scls: Optional[s_types.Type]=None,\n        compile_views: bool=True,\n        exprtype: s_types.ExprType = s_types.ExprType.Select,\n        allow_select_shape_inject: bool=True,\n        forward_rptr: bool=False,\n        span: Optional[qlast.Span],\n        ctx: context.ContextLevel) -> irast.Set:\n\n    set_stype = setgen.get_set_type(set, ctx=ctx)\n\n    set_expr = set.expr\n    while isinstance(set_expr, irast.TypeIntersectionPointer):\n        set_expr = set_expr.source.expr\n\n    is_ptr_alias = (\n        view_rptr is not None\n        and view_rptr.ptrcls is None\n        and view_rptr.ptrcls_name is not None\n        and isinstance(set_expr, irast.Pointer)\n        and not isinstance(set_expr.source.expr, irast.Pointer)\n        and (\n            view_rptr.source.get_bases(ctx.env.schema).first(ctx.env.schema).id\n            == set_expr.source.typeref.id\n        )\n        and (\n            view_rptr.ptrcls_is_linkprop\n            == (set_expr.ptrref.source_ptr is not None)\n        )\n    )\n\n    if is_ptr_alias:\n        assert view_rptr is not None\n        set_rptr = cast(irast.Pointer, set_expr)\n        # We are inside an expression that defines a link alias in\n        # the parent shape, ie. Spam { alias := Spam.bar }, so\n        # `Spam.alias` should be a subclass of `Spam.bar` inheriting\n        # its properties.\n        #\n        # We also try to detect reverse aliases like `.<bar[IS Spam]`\n        # and arange to inherit the linkprops from those if it resolves\n        # to a unique type.\n        ptrref = set_rptr.ptrref\n        if (\n            isinstance(set.expr, irast.Pointer)\n            and isinstance(set.expr.ptrref, irast.TypeIntersectionPointerRef)\n            and len(set.expr.ptrref.rptr_specialization) == 1\n        ):\n            ptrref = list(set.expr.ptrref.rptr_specialization)[0]\n\n        if (\n            set_rptr.direction is not s_pointers.PointerDirection.Outbound\n            and ptrref.out_source.is_opaque_union\n        ):\n            base_ptrcls = None\n        else:\n            base_ptrcls = typegen.ptrcls_from_ptrref(ptrref, ctx=ctx)\n\n        if isinstance(base_ptrcls, s_pointers.Pointer):\n            view_rptr.base_ptrcls = base_ptrcls\n            view_rptr.ptrcls_is_alias = True\n            view_rptr.rptr_dir = set_rptr.direction\n\n    if (\n        (\n            (\n                ctx.expr_exposed >= context.Exposure.BINDING\n                and allow_select_shape_inject\n\n                and not forward_rptr\n                and viewgen.has_implicit_type_computables(\n                    set_stype,\n                    is_mutation=exprtype.is_mutation(),\n                    ctx=ctx,\n                )\n                and not set_stype.is_view(ctx.env.schema)\n            )\n            or exprtype.is_mutation()\n            or (\n                exprtype == s_types.ExprType.Group\n                and not set_stype.is_view(ctx.env.schema)\n            )\n        )\n        and set_stype.is_object_type()\n        and shape is None\n    ):\n        # Force the subject to be compiled as a view in these cases:\n        # a) a __tid__ insertion is anticipated (the actual\n        #    decision about this is taken by the\n        #    compile_view_shapes() flow);\n        #    we also skip doing this when forward_rptr is true, because\n        #    generating an extra type in those cases can cause issues,\n        #    and we can just do the insertion on whatever the inner thing is\n        #\n        #    Note that we do this when exposed or when potentially exposed\n        #    because we are in a binding. This is because types that\n        #    appear in bindings might get put into the output\n        #    and need a __tid__ injection without having a chance to have\n        #    a shape put on them.\n        # b) this is a mutation without an explicit shape,\n        #    such as a DELETE, because mutation subjects are\n        #    always expected to be derived types.\n        shape = []\n\n    if shape is not None and view_scls is None:\n        if (view_name is None and\n                isinstance(result_alias, s_name.QualName)):\n            view_name = result_alias\n\n        if not isinstance(set_stype, s_objtypes.ObjectType):\n            raise errors.QueryError(\n                f'shapes cannot be applied to '\n                f'{set_stype.get_verbosename(ctx.env.schema)}',\n                span=span,\n            )\n\n        view_scls, set = viewgen.process_view(\n            set,\n            stype=set_stype,\n            elements=shape,\n            view_rptr=view_rptr,\n            view_name=view_name,\n            exprtype=exprtype,\n            ctx=ctx,\n            span=span,\n        )\n\n    if view_scls is not None:\n        set = setgen.ensure_set(set, type_override=view_scls, ctx=ctx)\n        set_stype = view_scls\n\n    if compile_views:\n        viewgen.late_compile_view_shapes(set, ctx=ctx)\n\n    if (shape is not None or view_scls is not None) and len(set.path_id) == 1:\n        ctx.class_view_overrides[set.path_id.target.id] = set_stype\n\n    if shape:\n        # make sure that an applied shape expands the span of the set\n        set.span = edb_span.merge_spans(\n            itertools.chain(\n                (s.span for s in [set] if s.span),\n                (el.span for el in shape if el.span)\n            )\n        )\n\n    return set\n\n\ndef maybe_add_view(ir: irast.Set, *, ctx: context.ContextLevel) -> irast.Set:\n    \"\"\"Possibly wrap ir in a new view, if needed for tid/tname injection\n\n    This should be called by every ast leaf compilation that can originate\n    an object type.\n    \"\"\"\n\n    # We call compile_query_subject in order to create a new view for\n    # injecting properties if needed. This will only happen if\n    # expr_exposed, so stmt code paths that don't want a new view\n    # created (because there is a shape already specified or because\n    # it wants to create its own new view in its compile_query_subject call)\n    # should make sure expr_exposed is false.\n    #\n    # The checks here are microoptimizations.\n    if (\n        ctx.expr_exposed >= context.Exposure.BINDING\n        and ir.path_id.is_objtype_path()\n    ):\n        return compile_query_subject(\n            ir, allow_select_shape_inject=True, compile_views=False, ctx=ctx,\n            span=ir.span)\n    else:\n        return ir\n\n\ndef _is_forbidden_stdlib_type_for_mod(\n    t: s_types.Type, ctx: context.ContextLevel\n) -> bool:\n    o = ctx.env.options\n    if o.bootstrap_mode or o.schema_reflection_mode:\n        return False\n\n    schema = ctx.env.schema\n\n    assert isinstance(t, s_objtypes.ObjectType)\n    assert not t.is_view(schema)\n\n    if intersection := t.get_intersection_of(schema):\n        return all((_is_forbidden_stdlib_type_for_mod(it, ctx)\n                    for it in intersection.objects(schema)))\n    elif union := t.get_union_of(schema):\n        return any((_is_forbidden_stdlib_type_for_mod(ut, ctx)\n                    for ut in union.objects(schema)))\n\n    name = t.get_name(schema)\n    mod_name = name.get_module_name()\n\n    if (\n        mod_name == s_name.UnqualName('cfg')\n        and o.in_server_config_op\n    ):\n        # Config ops include various internally generated statements for cfg::\n        return False\n    if name == s_name.QualName('std', 'Object'):\n        # Allow people to mess with the baseclass of user-defined objects to\n        # their hearts' content\n        return False\n    if mod_name == s_name.UnqualName('std::net::http'):\n        # Allow users to insert net module types\n        return False\n    return mod_name in s_schema.STD_MODULES\n"
  },
  {
    "path": "edb/edgeql/compiler/stmtctx.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL compiler statement-level context management.\"\"\"\n\n\nfrom __future__ import annotations\n\nfrom typing import (\n    Any,\n    Optional,\n    Mapping,\n    Sequence,\n)\n\nimport copy\nimport uuid\n\nfrom edb import errors\n\nfrom edb.ir import ast as irast\nfrom edb.ir import utils as irutils\nfrom edb.ir import typeutils as irtyputils\n\nfrom edb.schema import constraints as s_constr\nfrom edb.schema import modules as s_mod\nfrom edb.schema import name as s_name\nfrom edb.schema import objects as s_obj\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import rewrites as s_rewrites\nfrom edb.schema import schema as s_schema\nfrom edb.schema import sources as s_sources\nfrom edb.schema import types as s_types\nfrom edb.schema import expr as s_expr\n\nfrom edb.edgeql import ast as qlast\n\nfrom edb.common.ast import visitor as ast_visitor\nfrom edb.common import ordered\nfrom edb.common.typeutils import not_none\n\nfrom . import astutils\nfrom . import context\nfrom . import dispatch\nfrom . import eta_expand\nfrom . import group\nfrom . import inference\nfrom . import options as coptions\nfrom . import pathctx\nfrom . import setgen\nfrom . import viewgen\nfrom . import schemactx\nfrom . import triggers\nfrom . import tuple_args\nfrom . import typegen\n\n\ndef init_context(\n    *,\n    schema: s_schema.Schema,\n    options: coptions.CompilerOptions,\n    inlining_context: Optional[context.ContextLevel] = None,\n) -> context.ContextLevel:\n\n    if not schema.get_global(s_mod.Module, '__derived__', None):\n        schema, _ = s_mod.Module.create_in_schema(\n            schema,\n            name=s_name.UnqualName('__derived__'),\n        )\n\n    if inlining_context:\n        env = copy.copy(inlining_context.env)\n        env.options = options\n        env.path_scope = inlining_context.path_scope\n        env.alias_result_view_name = options.result_view_name\n        env.query_parameters = {}\n        env.server_param_conversions = {}\n        env.server_param_conversion_calls = []\n        env.script_params = {}\n\n        ctx = context.ContextLevel(\n            inlining_context, mode=context.ContextSwitchMode.DETACHED\n        )\n        ctx.env = env\n\n    else:\n        env = context.Environment(\n            schema=schema,\n            options=options,\n            alias_result_view_name=options.result_view_name,\n        )\n        ctx = context.ContextLevel(None, context.ContextSwitchMode.NEW, env=env)\n    _ = context.CompilerContext(initial=ctx)\n\n    if options.singletons:\n        # The caller wants us to treat these type and pointer\n        # references as singletons for the purposes of the overall\n        # expression cardinality inference, so we set up the scope\n        # tree in the necessary fashion.\n        had_optional = False\n        for singleton_ent in options.singletons:\n            singleton, optional = (\n                singleton_ent if isinstance(singleton_ent, tuple)\n                else (singleton_ent, False)\n            )\n            had_optional |= optional\n            path_id = compile_anchor('__', singleton, ctx=ctx).path_id\n            ctx.env.path_scope.attach_path(\n                path_id, optional=optional, span=None, ctx=ctx\n            )\n            if not optional:\n                ctx.env.singletons.append(path_id)\n            ctx.iterator_path_ids |= {path_id}\n\n        # If we installed any optional singletons, run the rest of the\n        # compilation under a fence to protect them.\n        if had_optional:\n            ctx.path_scope = ctx.path_scope.attach_fence()\n\n    for orig, remapped in options.type_remaps.items():\n        rset = compile_anchor('__', remapped, ctx=ctx)\n        ctx.view_sets[orig] = rset\n        ctx.env.path_scope_map[rset] = context.ScopeInfo(\n            path_scope=ctx.path_scope, binding_kind=None\n        )\n\n    ctx.modaliases.update(options.modaliases)\n\n    if options.anchors:\n        with ctx.newscope(fenced=True) as subctx:\n            populate_anchors(options.anchors, ctx=subctx)\n\n    if options.path_prefix_anchor is not None:\n        path_prefix = options.anchors[options.path_prefix_anchor]\n        ctx.partial_path_prefix = compile_anchor(\n            options.path_prefix_anchor, path_prefix, ctx=ctx)\n        ctx.partial_path_prefix.anchor = options.path_prefix_anchor\n        ctx.partial_path_prefix.show_as_anchor = options.path_prefix_anchor\n\n    if options.detached:\n        ctx.path_id_namespace = frozenset({ctx.aliases.get('ns')})\n\n    if options.schema_object_context is s_rewrites.Rewrite:\n        assert ctx.partial_path_prefix\n        typ = setgen.get_set_type(ctx.partial_path_prefix, ctx=ctx)\n        assert isinstance(typ, s_objtypes.ObjectType)\n        ctx.active_rewrites |= {typ, *typ.descendants(ctx.env.schema)}\n\n    ctx.derived_target_module = options.derived_target_module\n    ctx.toplevel_result_view_name = options.result_view_name\n    ctx.implicit_id_in_shapes = options.implicit_id_in_shapes\n    ctx.implicit_tid_in_shapes = options.implicit_tid_in_shapes\n    ctx.implicit_tname_in_shapes = options.implicit_tname_in_shapes\n    ctx.implicit_limit = options.implicit_limit\n    ctx.expr_exposed = context.Exposure.EXPOSED\n\n    ctx.no_factoring = True\n\n    return ctx\n\n\ndef fini_expression(\n    ir: irast.Set, *, ctx: context.ContextLevel\n) -> irast.Statement | irast.ConfigCommand:\n\n    ctx.path_scope = ctx.env.path_scope\n\n    ir = eta_expand.eta_expand_ir(ir, toplevel=True, ctx=ctx)\n\n    if (\n        isinstance(ir, irast.Set)\n        and pathctx.get_set_scope(ir, ctx=ctx) is None\n    ):\n        ir = setgen.scoped_set(ir, ctx=ctx)\n\n    # Compile any triggers that were triggered by the query\n    ir_triggers = triggers.compile_triggers(ctx=ctx)\n\n    # Collect all of the expressions stored in various side sets\n    # that can make it into the output, so that we can make sure\n    # to catch them all in our fixups and analyses.\n    # IMPORTANT: Any new expressions that are sent to the backend\n    # but don't appear in `ir` must be added here.\n    extra_exprs: list[irast.Set] = []\n    extra_exprs += [\n        rw for rw in ctx.env.type_rewrites.values()\n        if isinstance(rw, irast.Set)\n    ]\n    extra_exprs += [\n        p.sub_params.decoder_ir for p in ctx.env.query_parameters.values()\n        if p.sub_params and p.sub_params.decoder_ir\n    ]\n    extra_exprs += [\n        conversion.ir_param.sub_params.decoder_ir\n        for conversions in ctx.env.server_param_conversions.values()\n        for conversion in conversions.values()\n        if (\n            conversion.ir_param.sub_params\n            and conversion.ir_param.sub_params.decoder_ir\n        )\n    ]\n    extra_exprs += [trigger.expr for stage in ir_triggers for trigger in stage]\n\n    all_exprs = [ir] + extra_exprs\n\n    # exprs_to_clear collects sets where we should never need to use\n    # their expr in pgsql compilation, so we strip it out to make this\n    # more evident in debug output. We have to do the clearing at the\n    # end, because multiplicity/cardinality inference needs to be able\n    # to look through those pointers.\n    exprs_to_clear = _fixup_materialized_sets(all_exprs, ctx=ctx)\n    for expr in all_exprs:\n        exprs_to_clear.extend(_find_visible_binding_refs(expr, ctx=ctx))\n\n    # The inference context object will be shared between\n    # cardinality and multiplicity inferrers.\n    inf_ctx = inference.make_ctx(env=ctx.env)\n    cardinality = inference.infer_cardinality(\n        ir, scope_tree=ctx.path_scope, ctx=inf_ctx\n    )\n    multiplicity = inference.infer_multiplicity(\n        ir, scope_tree=ctx.path_scope, ctx=inf_ctx\n    )\n\n    for extra in extra_exprs:\n        inference.infer_cardinality(\n            extra, scope_tree=ctx.path_scope, ctx=inf_ctx)\n        inference.infer_multiplicity(\n            extra, scope_tree=ctx.path_scope, ctx=inf_ctx)\n\n    # Fix up weak namespaces\n    _rewrite_weak_namespaces(all_exprs, ctx)\n\n    _collapse_factoring_protected(all_exprs, ctx)\n\n    ctx.path_scope.validate_unique_ids()\n\n    # Collect query parameters\n    params = collect_params(ctx)\n    server_param_conversions, server_param_conversion_params = (\n        collect_server_param_conversions(ctx)\n    )\n\n    # ConfigSet and ConfigReset don't like being part of a Set, so bail early\n    if isinstance(ir.expr, (irast.ConfigSet, irast.ConfigReset)):\n        ir.expr.scope_tree = ctx.path_scope\n        ir.expr.globals = list(ctx.env.query_globals.values())\n        ir.expr.params = params\n        ir.expr.schema = ctx.env.schema\n        ir.expr.type_rewrites = _get_type_rewrites(ctx)\n\n        if ctx.env.server_param_conversion_calls:\n            func_name, func_span = ctx.env.server_param_conversion_calls[0]\n            raise errors.QueryError(\n                f\"Function '{func_name}' is not allowed in a config statement.\",\n                span=func_span,\n            )\n\n        return ir.expr\n\n    volatility = inference.infer_volatility(ir, env=ctx.env)\n    expr_type = setgen.get_set_type(ir, ctx=ctx)\n\n    in_polymorphic_func = (\n        ctx.env.options.func_params is not None and\n        ctx.env.options.func_params.has_polymorphic(ctx.env.schema)\n    )\n    if (\n        not in_polymorphic_func\n        and not ctx.env.options.allow_generic_type_output\n    ):\n        anytype = expr_type.find_generic(ctx.env.schema)\n        if anytype is not None:\n            raise errors.QueryError(\n                'expression returns value of indeterminate type',\n                hint='Consider using an explicit type cast.',\n                span=ctx.env.type_origins.get(anytype))\n\n    # Clear out exprs that we decided to omit from the IR\n    for ir_set in exprs_to_clear:\n        new = (\n            irast.MaterializedExpr(typeref=ir_set.typeref)\n            if ir_set.is_materialized_ref\n            else irast.VisibleBindingExpr(typeref=ir_set.typeref)\n        )\n        if isinstance(ir_set.expr, irast.Pointer):\n            ir_set.expr.expr = new\n        else:\n            ir_set.expr = new\n\n    # Analyze GROUP statements to find aggregates that can be optimized\n    group.infer_group_aggregates(all_exprs, ctx=ctx)\n\n    # If we are producing a schema view, clean up the result types\n    if ctx.env.options.schema_view_mode:\n        _fixup_schema_view(ctx=ctx)\n\n    result = irast.Statement(\n        expr=ir,\n        params=params,\n        globals=list(ctx.env.query_globals.values()),\n        required_permissions=set(ctx.env.required_permissions),\n        server_param_conversions=server_param_conversions,\n        server_param_conversion_params=server_param_conversion_params,\n        views=ctx.view_nodes,\n        scope_tree=ctx.env.path_scope,\n        cardinality=cardinality,\n        volatility=volatility,\n        multiplicity=multiplicity.own,\n        stype=expr_type,\n        view_shapes={\n            src: [ptr for ptr, op in ptrs if op != qlast.ShapeOp.MATERIALIZE]\n            for src, ptrs in ctx.env.view_shapes.items()\n            if isinstance(src, s_obj.Object)\n        },\n        view_shapes_metadata=ctx.env.view_shapes_metadata,\n        schema=ctx.env.schema,\n        schema_refs=frozenset(\n            {\n                r\n                for r in ctx.env.schema_refs\n                # filter out newly derived objects\n                if ctx.env.orig_schema.has_object(r.id)\n            }\n        ),\n        schema_ref_exprs=ctx.env.schema_ref_exprs,\n        type_rewrites=_get_type_rewrites(ctx),\n        dml_exprs=ctx.env.dml_exprs,\n        singletons=ctx.env.singletons,\n        triggers=ir_triggers,\n        warnings=tuple(ctx.env.warnings),\n        unsafe_isolation_dangers=tuple(ctx.env.unsafe_isolation_dangers),\n    )\n    return result\n\n\ndef _get_type_rewrites(ctx: context.ContextLevel) -> dict[\n    tuple[uuid.UUID, bool], irast.Set\n]:\n    return {\n        (typ.id, not skip_subtypes): s\n        for (typ, skip_subtypes), s in ctx.env.type_rewrites.items()\n        if isinstance(s, irast.Set)\n    }\n\n\ndef collect_params(ctx: context.ContextLevel) -> list[irast.Param]:\n    lparams = [\n        p for p in ctx.env.query_parameters.values() if not p.is_sub_param\n    ]\n    if ctx.env.script_params:\n        script_ordering = {k: i for i, k in enumerate(ctx.env.script_params)}\n        lparams.sort(key=lambda x: script_ordering[x.name])\n\n    params = []\n    # Now flatten it out, including all sub_params, making sure subparams\n    # appear in the right order.\n    for p in lparams:\n        params.append(p)\n        if p.sub_params:\n            params.extend(p.sub_params.params)\n    return params\n\n\ndef collect_server_param_conversions(\n    ctx: context.ContextLevel\n) -> tuple[\n    list[irast.ServerParamConversion],\n    list[irast.Param],\n]:\n    \"\"\"Gather converted parameters for use in the ir Statement.\n\n    Returns ServerParamConversion which will eventually be sent to the server\n    as well as the irast.Params which should be used to generate the pgast.\n    \"\"\"\n    lparams = [\n        (\n            param_name,\n            conversion_name,\n            conversion,\n        )\n        for param_name, conversions in ctx.env.server_param_conversions.items()\n        for conversion_name, conversion in conversions.items()\n        if not conversion.ir_param.is_sub_param\n    ]\n    script_ordering = {k: i for i, k in enumerate(ctx.env.script_params)}\n\n    # Add ordering for param conversions which don't match query params.\n    # This can happen for constants.\n    extra_ordering: dict[str, int] = {}\n    for param_name in sorted(ctx.env.server_param_conversions.keys()):\n        if param_name not in script_ordering:\n            extra_ordering[param_name] = (\n                len(script_ordering) + len(extra_ordering)\n            )\n    script_ordering.update(extra_ordering)\n\n    lparams.sort(key=lambda x: (script_ordering[x[0]], x[1]))\n\n    conversions = []\n    params = []\n    # Now flatten it out, including all sub_params, making sure subparams\n    # appear in the right order.\n    for param_name, conversion_name, conversion in lparams:\n        conversions.append(irast.ServerParamConversion(\n            param_name=param_name,\n            conversion_name=conversion_name,\n            additional_info=conversion.additional_info,\n            script_param_index=conversion.script_param_index,\n            constant_value=conversion.constant_value,\n        ))\n        params.append(conversion.ir_param)\n        if conversion.ir_param.sub_params:\n            params.extend(conversion.ir_param.sub_params.params)\n    return conversions, params\n\n\ndef _fixup_materialized_sets(\n    irs: Sequence[irast.Base], *, ctx: context.ContextLevel\n) -> list[irast.Set]:\n    # Make sure that all materialized sets have their views compiled\n    skips = {'materialized_sets'}\n    children = []\n    for ir in irs:\n        children += ast_visitor.find_children(\n            ir, irast.Stmt, extra_skips=skips)\n\n    to_clear = []\n    for stmt in ordered.OrderedSet(children):\n        if not stmt.materialized_sets:\n            continue\n        for key in list(stmt.materialized_sets):\n            mat_set = stmt.materialized_sets[key]\n            assert not mat_set.finalized\n\n            if len(mat_set.uses) <= 1:\n                del stmt.materialized_sets[key]\n                continue\n\n            ir_set = mat_set.materialized\n            assert ir_set.path_scope_id is not None\n            new_scope = ctx.env.scope_tree_nodes[ir_set.path_scope_id]\n            parent = not_none(new_scope.parent)\n\n            good_reason = False\n            for x in mat_set.reason:\n                if isinstance(x, irast.MaterializeVolatile):\n                    good_reason = True\n                elif isinstance(x, irast.MaterializeVisible):\n                    reason_scope = ctx.env.scope_tree_nodes[x.path_scope_id]\n                    reason_parent = not_none(reason_scope.parent)\n\n                    # If any of the bindings that the set uses are\n                    # *visible* at the definition point and *not\n                    # visible* from at least one use point, we need to\n                    # materialize, to make sure that the use site sees\n                    # the same value for the binding as the definition\n                    # point. If it's not visible, then it's just being\n                    # used internally and we don't need any special\n                    # work.\n                    use_scopes = [\n                        ctx.env.scope_tree_nodes.get(x.path_scope_id)\n                        if x.path_scope_id is not None\n                        else None\n                        for x in mat_set.use_sets\n                    ]\n                    for b, _ in x.sets:\n                        if (\n                            reason_parent.is_visible(b, allow_group=True)\n                        ) and not all(\n                            use_scope and use_scope.parent\n                            and use_scope.parent.is_visible(\n                                b, allow_group=True)\n                            for use_scope in use_scopes\n                        ):\n                            good_reason = True\n                            break\n\n            if not good_reason:\n                del stmt.materialized_sets[key]\n                continue\n\n            # Compile the view shapes in the set\n            with ctx.new() as subctx:\n                subctx.implicit_tid_in_shapes = False\n                subctx.implicit_tname_in_shapes = False\n                subctx.path_scope = new_scope\n                subctx.path_scope = parent.attach_fence()\n                viewgen.late_compile_view_shapes(ir_set, ctx=subctx)\n\n            for use_set in mat_set.use_sets:\n                if use_set != mat_set.materialized:\n                    use_set.is_materialized_ref = True\n                    # XXX: Deleting it on linkprops breaks a bunch of\n                    # linkprop related DML...\n                    if not use_set.path_id.is_linkprop_path():\n                        to_clear.append(use_set)\n\n            assert (\n                not any(use.src_path() for use in mat_set.uses)\n                or isinstance(mat_set.materialized.expr, irast.Pointer)\n            ), f\"materialized ptr {mat_set.uses} missing pointer\"\n            mat_set.finalized = True\n\n    return to_clear\n\n\ndef _find_visible_binding_refs(\n    ir: irast.Base, *, ctx: context.ContextLevel\n) -> list[irast.Set]:\n    children = ast_visitor.find_children(\n        ir, irast.Set, lambda n: n.is_visible_binding_ref)\n    return children\n\n\ndef _try_namespace_fix(\n    scope: irast.ScopeTreeNode,\n    path_id: irast.PathId,\n) -> irast.PathId:\n    for prefix in path_id.iter_prefixes():\n        replacement = scope.find_visible(prefix, allow_group=True)\n        if (\n            replacement and replacement.path_id\n            and prefix != replacement.path_id\n        ):\n            new = irtyputils.replace_pathid_prefix(\n                path_id, prefix, replacement.path_id)\n\n            return new\n\n    return path_id\n\n\ndef _rewrite_weak_namespaces(\n    irs: Sequence[irast.Base], ctx: context.ContextLevel\n) -> None:\n    \"\"\"Rewrite weak namespaces in path ids to be usable by the backend.\n\n    Weak namespaces in path ids in the frontend are \"relative\", and\n    their interpretation depends on the current scope tree node and\n    the namespaces on the parent nodes. The IR->pgsql compiler does\n    not do this sort of interpretation, and needs path IDs that are\n    \"absolute\".\n\n    To accomplish this, we go through all the path ids and rewrite\n    them: using the scope tree, we try to find the binding of the path\n    ID (using a prefix if necessary) and drop all namespace parts that\n    don't appear in the binding.\n    \"\"\"\n\n    tree = ctx.path_scope\n\n    for node in tree.strict_descendants:\n        if node.path_id:\n            node.path_id = _try_namespace_fix(node, node.path_id)\n\n    scopes = irutils.find_path_scopes(irs)\n\n    for ir_set in ctx.env.set_types:\n        path_scope_id: Optional[int] = scopes.get(ir_set)\n        if path_scope_id is not None:\n            # Some entries in set_types are from compiling views\n            # in temporary scopes, so we need to just skip those.\n            if scope := ctx.env.scope_tree_nodes.get(path_scope_id):\n                ir_set.path_id = _try_namespace_fix(scope, ir_set.path_id)\n\n\ndef _get_all_pathids(irs: Sequence[irast.Base]) -> set[\n    tuple[irast.PathId, irast.Set | None]\n]:\n    all_ids: set[tuple[irast.PathId, irast.Set | None]] = set()\n    for ir in irs:\n        for ir_set in ast_visitor.find_children(ir, irast.Set):\n            all_ids.add((ir_set.path_id, ir_set))\n        for arg in ast_visitor.find_children(ir, irast.CallArg):\n            if arg.expr_type_path_id:\n                all_ids.add((arg.expr_type_path_id, None))\n\n    return all_ids\n\n\ndef _collapse_factoring_protected(\n    irs: Sequence[irast.Base], ctx: context.ContextLevel\n) -> None:\n    \"\"\"Try to remove the Selects inserted for simple_scoping.\n\n    In simple_scoping mode, we protect certain paths by wrapping them\n    in selects so that they don't participate in path factoring.\n\n    This generates more verbose SQL in all cases and inhibits\n    important optimizations in others -- in particular, our efforts to\n    make ORDER BY clauses simple enough for postgres to optimize.\n\n    To remedy this, we try to collapse away those selects and their\n    fences in the scope tree by checking if removing them would lead\n    to any path factoring. If not, we can drop it.\n\n    Note that *some* new-school factoring may still have happened.\n    If we have `select User filter User.name = 'Elvis'`, the outer `User`\n    will be unprotected and the inner `User` will be factored out to it,\n    leaving just `User.name` in a protected inner scope.\n    That's fine, and we will see User.name doesn't have anything\n    to factor with and remove the select that was injected.\n    \"\"\"\n    children = []\n    for ir in irs:\n        children += ast_visitor.find_children(\n            ir, irast.Set, lambda x: x.is_factoring_protected\n        )\n    all_ids = _get_all_pathids(irs)\n\n    for ir_set in ordered.OrderedSet(children):\n        if (\n            ir_set.path_scope_id is None\n            or not irutils.is_implicit_wrapper(ir_set.expr)\n        ):\n            continue\n\n        node = ctx.env.scope_tree_nodes[ir_set.path_scope_id]\n        if not (parent := node.parent):\n            continue\n\n        # If the underlying thing has already been factored fully, we\n        # skip it, because it might be no-factor fenced?\n        if parent.find_visible(ir_set.expr.result.path_id):\n            continue\n\n        # If collapsing this node would lead to any factoring, we\n        # obviously can't do it.\n        # We check by seeing if there are some factorable nodes\n        # *other* than the ones we are starting from.\n        if any(\n            parent.find_factorable_nodes(path_id, child_to_skip=node)\n            for path_id in node.get_all_paths()\n        ):\n            continue\n\n        # If the path is referenced at all, we can't do it.\n        # PERF: Should we build a dict with all prefixes as keys, instead\n        # of this O(n*m) loop?\n        if any(\n            x is not ir_set and path_id.startswith(ir_set.path_id)\n            for path_id, x in all_ids\n        ):\n            continue\n\n        del ctx.env.scope_tree_nodes[ir_set.path_scope_id]\n\n        # Merge the node up into its parent\n        node.optional |= parent.optional\n        parent.fuse_subtree(node, self_fenced=True, ctx=ctx)\n\n        # Mark the new path as optional if the old path was optional.\n        orig = None\n        gparent = parent.parent\n        if (\n            gparent\n            and (orig := gparent.find_child(ir_set.path_id))\n            and parent.optional\n            and orig.optional\n        ):\n            pathctx.register_set_in_scope(\n                ir_set.expr.result, optional=True, path_scope=gparent, ctx=ctx\n            )\n        if orig:\n            orig.remove()\n        # Yeeeee haw. Replace the old set with the inner one.\n        ir_set.__dict__ = ir_set.expr.result.__dict__\n\n\ndef _fixup_schema_view(*, ctx: context.ContextLevel) -> None:\n    \"\"\"Finalize schema view types for inclusion in the real schema.\n\n    This includes setting from_alias flags and collapsing opaque\n    unions to BaseObject.\n    \"\"\"\n    for view in ctx.view_nodes.values():\n        if view.is_collection():\n            continue\n\n        assert isinstance(view, s_types.InheritingType)\n        _elide_derived_ancestors(view, ctx=ctx)\n\n        if not isinstance(view, s_sources.Source):\n            continue\n\n        view_own_pointers = view.get_pointers(ctx.env.schema)\n        for vptr in view_own_pointers.objects(ctx.env.schema):\n            _elide_derived_ancestors(vptr, ctx=ctx)\n            ctx.env.schema = vptr.set_field_value(\n                ctx.env.schema,\n                'from_alias',\n                True,\n            )\n\n            tgt = vptr.get_target(ctx.env.schema)\n            assert tgt is not None\n\n            if (tgt.is_union_type(ctx.env.schema)\n                    and tgt.get_is_opaque_union(ctx.env.schema)):\n                # Opaque unions should manifest as std::BaseObject\n                # in schema views.\n                ctx.env.schema = vptr.set_target(\n                    ctx.env.schema,\n                    ctx.env.schema.get(\n                        'std::BaseObject', type=s_types.Type),\n                )\n\n            if not isinstance(vptr, s_sources.Source):\n                continue\n\n            vptr_own_pointers = vptr.get_pointers(ctx.env.schema)\n            for vlprop in vptr_own_pointers.objects(ctx.env.schema):\n                _elide_derived_ancestors(vlprop, ctx=ctx)\n                ctx.env.schema = vlprop.set_field_value(\n                    ctx.env.schema,\n                    'from_alias',\n                    True,\n                )\n\n\ndef _get_nearest_non_source_derived_parent(\n    obj: s_obj.DerivableInheritingObjectT, ctx: context.ContextLevel\n) -> s_obj.DerivableInheritingObjectT:\n    \"\"\"Find the nearest ancestor of obj whose \"root source\" is not derived\"\"\"\n    schema = ctx.env.schema\n    while (\n        (src := s_pointers.get_root_source(obj, schema))\n        and isinstance(src, s_obj.DerivableInheritingObject)\n        and src.get_is_derived(schema)\n    ):\n        obj = obj.get_bases(schema).first(schema)\n    return obj\n\n\ndef _elide_derived_ancestors(\n    obj: s_types.InheritingType | s_pointers.Pointer,\n    *,\n    ctx: context.ContextLevel,\n) -> None:\n    \"\"\"Collapse references to derived objects in bases.\n\n    When compiling a schema view expression, make sure we don't\n    expose any ephemeral derived objects, as these wouldn't be\n    present in the schema outside of the compilation context.\n    \"\"\"\n\n    pbase = obj.get_bases(ctx.env.schema).first(ctx.env.schema)\n    new_pbase = _get_nearest_non_source_derived_parent(pbase, ctx)\n    if pbase != new_pbase:\n        ctx.env.schema = obj.set_field_value(\n            ctx.env.schema,\n            'bases',\n            s_obj.ObjectList.create(ctx.env.schema, [new_pbase]),\n        )\n\n        ctx.env.schema = obj.set_field_value(\n            ctx.env.schema,\n            'ancestors',\n            s_obj.compute_ancestors(ctx.env.schema, obj)\n        )\n\n\ndef compile_anchor(\n    name: str,\n    anchor: qlast.Expr | irast.Base | s_obj.Object | irast.PathId,\n    *,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    show_as_anchor = True\n\n    if isinstance(anchor, s_types.Type):\n        # Anchors should not receive type rewrites; we are already\n        # evaluating in their context.\n        ctx.env.type_rewrites[anchor, False] = None\n        step = setgen.class_set(anchor, ctx=ctx)\n\n    elif (isinstance(anchor, s_pointers.Pointer) and\n            not anchor.is_link_property(ctx.env.schema)):\n        src = anchor.get_source(ctx.env.schema)\n        if src is not None:\n            assert isinstance(src, s_objtypes.ObjectType)\n            ctx.env.type_rewrites[src, False] = None\n            path = setgen.extend_path(\n                setgen.class_set(src, ctx=ctx),\n                anchor,\n                s_pointers.PointerDirection.Outbound,\n                ctx=ctx,\n            )\n        else:\n            ptrcls = schemactx.derive_dummy_ptr(anchor, ctx=ctx)\n            src = ptrcls.get_source(ctx.env.schema)\n            assert isinstance(src, s_types.Type)\n            ctx.env.type_rewrites[src, False] = None\n            path = setgen.extend_path(\n                setgen.class_set(src, ctx=ctx),\n                ptrcls,\n                s_pointers.PointerDirection.Outbound,\n                ctx=ctx)\n\n        step = path\n\n    elif (isinstance(anchor, s_pointers.Pointer) and\n            anchor.is_link_property(ctx.env.schema)):\n\n        anchor_source = anchor.get_source(ctx.env.schema)\n        assert isinstance(anchor_source, s_pointers.Pointer)\n        anchor_source_source = anchor_source.get_source(ctx.env.schema)\n\n        if anchor_source_source:\n            assert isinstance(anchor_source_source, s_objtypes.ObjectType)\n            path = setgen.extend_path(\n                setgen.class_set(anchor_source_source, ctx=ctx),\n                anchor_source,\n                s_pointers.PointerDirection.Outbound,\n                ctx=ctx,\n            )\n        else:\n            ptrcls = schemactx.derive_dummy_ptr(anchor_source, ctx=ctx)\n            src = ptrcls.get_source(ctx.env.schema)\n            assert isinstance(src, s_types.Type)\n            path = setgen.extend_path(\n                setgen.class_set(src, ctx=ctx),\n                ptrcls,\n                s_pointers.PointerDirection.Outbound,\n                ctx=ctx)\n\n        step = setgen.extend_path(\n            path,\n            anchor,\n            s_pointers.PointerDirection.Outbound,\n            ctx=ctx)\n\n    elif isinstance(anchor, qlast.Base):\n        step = dispatch.compile(anchor, ctx=ctx)\n\n    elif isinstance(anchor, (irast.QueryParameter, irast.FunctionParameter)):\n        step = setgen.ensure_set(anchor, ctx=ctx)\n\n    elif isinstance(anchor, irast.PathId):\n        stype = typegen.type_from_typeref(anchor.target, env=ctx.env)\n        step = setgen.class_set(\n            stype, path_id=anchor, ignore_rewrites=True, ctx=ctx)\n\n    else:\n        raise RuntimeError(f'unexpected anchor value: {anchor!r}')\n\n    if show_as_anchor:\n        step.anchor = name\n        step.show_as_anchor = name\n\n    return step\n\n\ndef populate_anchors(\n    anchors: Mapping[str, Any],\n    *,\n    ctx: context.ContextLevel,\n) -> None:\n\n    for name, val in anchors.items():\n        ctx.anchors[name] = compile_anchor(name, val, ctx=ctx)\n\n\ndef declare_view(\n    expr: qlast.Expr,\n    alias: s_name.Name,\n    *,\n    factoring_fence: bool=False,\n    fully_detached: bool=False,\n    binding_kind: irast.BindingKind,\n    path_id_namespace: Optional[frozenset[str]]=None,\n    ctx: context.ContextLevel,\n) -> irast.Set:\n\n    pinned_pid_ns = path_id_namespace\n\n    with ctx.newscope(fenced=True) as subctx:\n        if factoring_fence:\n            subctx.path_scope.factoring_fence = True\n            subctx.path_scope.factoring_allowlist.update(ctx.iterator_path_ids)\n\n        if path_id_namespace is not None:\n            subctx.path_id_namespace = path_id_namespace\n\n        if not fully_detached:\n            cached_view_set = ctx.env.expr_view_cache.get((expr, alias))\n            # Detach the view namespace and record the prefix\n            # in the parent statement's fence node.\n            view_path_id_ns = {ctx.aliases.get('ns')}\n            # if view_path_id_ns == {'ns~3'}:\n            #     view_path_id_ns = set()\n            subctx.path_id_namespace |= view_path_id_ns\n            ctx.path_scope.add_namespaces(view_path_id_ns)\n        else:\n            cached_view_set = None\n\n        if ctx.stmt is not None:\n            subctx.stmt = ctx.stmt.parent_stmt\n\n        if cached_view_set is not None:\n            subctx.view_scls = setgen.get_set_type(cached_view_set, ctx=ctx)\n            view_name = subctx.view_scls.get_name(ctx.env.schema)\n            assert isinstance(view_name, s_name.QualName)\n        else:\n            if (\n                isinstance(alias, s_name.QualName)\n                and subctx.env.options.schema_view_mode\n            ):\n                view_name = alias\n                subctx.recompiling_schema_alias = True\n            else:\n                view_name = s_name.QualName(\n                    module=ctx.derived_target_module or '__derived__',\n                    name=s_name.get_specialized_name(\n                        alias,\n                        ctx.aliases.get('w')\n                    )\n                )\n\n        subctx.toplevel_result_view_name = view_name\n\n        view_set = dispatch.compile(astutils.ensure_ql_query(expr), ctx=subctx)\n        assert isinstance(view_set, irast.Set)\n\n        ctx.env.path_scope_map[view_set] = context.ScopeInfo(\n            path_scope=subctx.path_scope,\n            pinned_path_id_ns=pinned_pid_ns,\n            binding_kind=binding_kind,\n        )\n\n        if not fully_detached:\n            # The view path id _itself_ should not be in the nested namespace.\n            # The fully_detached case should be handled by the caller.\n            if path_id_namespace is None:\n                path_id_namespace = ctx.path_id_namespace\n            view_set.path_id = view_set.path_id.replace_namespace(\n                path_id_namespace)\n\n        ctx.aliased_views[alias] = view_set\n        ctx.env.expr_view_cache[expr, alias] = view_set\n\n    return view_set\n\n\ndef _declare_view_from_schema(\n    viewcls: s_types.Type, *, ctx: context.ContextLevel\n) -> tuple[s_types.Type, irast.Set]:\n    # We need to include \"security context\" things (currently just\n    # access policy state) in the cache key, here.\n    #\n    # See below for an optimization in the case where polices are not\n    # used.\n    security_context = ctx.get_security_context()\n    key = viewcls, security_context\n    e = ctx.env.schema_view_cache.get(key)\n    if e is not None:\n        return e\n\n    orig_policy_count = ctx.env.policy_use_count\n\n    # N.B: This takes a context, which we need to use to create a\n    # subcontext to compile in, but it should avoid depending on the\n    # context, because of the cache.\n    with ctx.detached() as subctx:\n        subctx.current_schema_views += (viewcls,)\n        subctx.expr_exposed = context.Exposure.UNEXPOSED\n        view_expr: s_expr.Expression | None = viewcls.get_expr(ctx.env.schema)\n        assert view_expr is not None\n        view_ql = view_expr.parse()\n        viewcls_name = viewcls.get_name(ctx.env.schema)\n        assert isinstance(view_ql, qlast.Expr), 'expected qlast.Expr'\n        view_set = declare_view(\n            view_ql,\n            alias=viewcls_name,\n            binding_kind=irast.BindingKind.Schema,\n            fully_detached=True,\n            ctx=subctx,\n        )\n        # The view path id _itself_ should not be in the nested namespace.\n        view_set.path_id = view_set.path_id.replace_namespace(frozenset())\n        view_set.is_schema_alias = True\n\n        vs = subctx.aliased_views[viewcls_name]\n        assert vs is not None\n        vc = setgen.get_set_type(vs, ctx=ctx)\n\n        # If policies weren't actually used, see if we already\n        # compiled this global/alias with policy suppression in the\n        # other state, to avoid generating two CTEs for a cached\n        # global pointlessly.\n        if orig_policy_count == ctx.env.policy_use_count:\n            key2 = viewcls, security_context.toggle_policies()\n            if key2 in ctx.env.schema_view_cache:\n                vc, view_set = ctx.env.schema_view_cache[key2]\n\n        ctx.env.schema_view_cache[key] = vc, view_set\n\n    return vc, view_set\n\n\ndef declare_view_from_schema(\n    viewcls: s_types.Type, *, ctx: context.ContextLevel\n) -> s_types.Type:\n    vc, view_set = _declare_view_from_schema(viewcls, ctx=ctx)\n\n    viewcls_name = viewcls.get_name(ctx.env.schema)\n\n    ctx.aliased_views[viewcls_name] = view_set\n    ctx.view_nodes[vc.get_name(ctx.env.schema)] = vc\n    ctx.view_sets[vc] = view_set\n\n    return vc\n\n\ndef check_params(params: dict[str, irast.Param]) -> None:\n    first_argname = next(iter(params))\n    for param in params.values():\n        # FIXME: context?\n        if param.name.isdecimal() != first_argname.isdecimal():\n            raise errors.QueryError(\n                f'cannot combine positional and named parameters '\n                f'in the same query')\n\n    if first_argname.isdecimal():\n        args_decnames = {int(arg) for arg in params}\n        args_tpl = set(range(len(params)))\n        if args_decnames != args_tpl:\n            missing_args = args_tpl - args_decnames\n            missing_args_repr = ', '.join(f'${a}' for a in missing_args)\n            raise errors.QueryError(\n                f'missing {missing_args_repr} positional argument'\n                f'{\"s\" if len(missing_args) > 1 else \"\"}')\n\n\ndef throw_on_shaped_param(\n    param: qlast.QueryParameter, shape: qlast.Shape, ctx: context.ContextLevel\n) -> None:\n    raise errors.QueryError(\n        f'cannot apply a shape to the parameter',\n        hint='Consider adding parentheses around the parameter and type cast',\n        span=shape.span\n    )\n\n\ndef throw_on_loose_param(\n    param: qlast.QueryParameter, ctx: context.ContextLevel\n) -> None:\n    if ctx.env.options.func_params is not None:\n        if ctx.env.options.schema_object_context is s_constr.Constraint:\n            raise errors.InvalidConstraintDefinitionError(\n                f'dollar-prefixed \"$parameters\" cannot be used here',\n                span=param.span)\n        else:\n            raise errors.InvalidFunctionDefinitionError(\n                f'dollar-prefixed \"$parameters\" cannot be used here',\n                span=param.span)\n    raise errors.QueryError(\n        f'missing a type cast before the parameter',\n        span=param.span)\n\n\ndef preprocess_script(\n    stmts: Sequence[qlast.Base], *, ctx: context.ContextLevel\n) -> irast.ScriptInfo:\n    \"\"\"Extract parameters from all statements in a script.\n\n    Doing this in advance makes it easy to check that they have\n    consistent types.\n    \"\"\"\n    params_lists = [\n        astutils.find_parameters(stmt, ctx.modaliases)\n        for stmt in stmts\n    ]\n\n    if loose_params := [\n        loose for params in params_lists\n        for loose in params.loose_params\n    ]:\n        throw_on_loose_param(loose_params[0], ctx)\n\n    if shaped_params := [\n        shaped for params in params_lists\n        for shaped in params.shaped_params\n    ]:\n        throw_on_shaped_param(shaped_params[0][0], shaped_params[0][1], ctx)\n\n    casts = [\n        cast for params in params_lists for cast in params.cast_params\n    ]\n    params = {}\n    for cast, modaliases in casts:\n        assert isinstance(cast.expr, qlast.QueryParameter)\n        name = cast.expr.name\n        if name in params:\n            continue\n        with ctx.new() as mctx:\n            mctx.modaliases = modaliases\n            target_stype = typegen.ql_typeexpr_to_type(cast.type, ctx=mctx)\n\n        if ctx.env.options.json_parameters:\n            # Rule check on JSON-input parameters.\n            # The actual casting of the the parameter happens in\n            if name.isdecimal():\n                raise errors.QueryError(\n                    'queries compiled to accept JSON parameters do not '\n                    'accept positional parameters',\n                    span=cast.expr.span)\n\n        # for ObjectType parameters, we inject intermediate cast to uuid,\n        # so parameter is uuid and then cast to ObjectType\n        if target_stype.is_object_type():\n            uuid_cast = qlast.TypeCast(\n                type=qlast.TypeName(maintype=qlast.ObjectRef(name='uuid')),\n                expr=cast.expr,\n                cardinality_mod=cast.cardinality_mod,\n            )\n            cast.expr = uuid_cast\n            cast = cast.expr\n\n            with ctx.new() as mctx:\n                mctx.modaliases = modaliases\n                target_stype = typegen.ql_typeexpr_to_type(cast.type, ctx=mctx)\n\n        target_typeref = typegen.type_to_typeref(target_stype, env=ctx.env)\n        required = cast.cardinality_mod != qlast.CardinalityModifier.Optional\n\n        # This handles processing of tuple arguments, nested arrays, and\n        # all json-mode parameters.\n        sub_params = tuple_args.create_sub_params(\n            name,\n            required,\n            typeref=target_typeref,\n            pt=target_stype,\n            is_func_param=False,\n            ctx=ctx,\n        )\n        params[name] = irast.Param(\n            name=name,\n            required=required,\n            schema_type=target_stype,\n            ir_type=target_typeref,\n            sub_params=sub_params,\n        )\n\n    if params:\n        check_params(params)\n\n        def _arg_key(k: tuple[str, object]) -> int:\n            name = k[0]\n            arg_prefix = '__edb_arg_'\n            # Positional arguments should just be sorted numerically,\n            # while for named arguments, injected args should be sorted and\n            # need to come after normal ones. Normal named arguments can have\n            # any order.\n            if name.isdecimal():\n                return int(name)\n            elif name.startswith(arg_prefix):\n                return int(k[0][len(arg_prefix):])\n            else:\n                return -1\n\n        params = dict(sorted(params.items(), key=_arg_key))\n\n    return irast.ScriptInfo(params=params, schema=ctx.env.schema)\n"
  },
  {
    "path": "edb/edgeql/compiler/triggers.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL trigger compilation.\"\"\"\n\n\nfrom __future__ import annotations\n\nfrom typing import Optional, Collection\n\nfrom edb import errors\n\nfrom edb.ir import ast as irast\n\nfrom edb.schema import name as sn\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import triggers as s_triggers\nfrom edb.schema import types as s_types\nfrom edb.schema import expr as s_expr\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom . import context\nfrom . import dispatch\nfrom . import options\nfrom . import schemactx\nfrom . import setgen\nfrom . import typegen\n\n\nTRIGGER_KINDS = {\n    irast.UpdateStmt: qltypes.TriggerKind.Update,\n    irast.DeleteStmt: qltypes.TriggerKind.Delete,\n    irast.InsertStmt: qltypes.TriggerKind.Insert,\n}\n\n\ndef compile_trigger(\n    trigger: s_triggers.Trigger,\n    affected: set[tuple[s_objtypes.ObjectType, irast.MutatingStmt]],\n    all_typs: set[s_objtypes.ObjectType],\n    *,\n    ctx: context.ContextLevel,\n) -> irast.Trigger:\n    schema = ctx.env.schema\n\n    scope = trigger.get_scope(schema)\n    kinds = set(trigger.get_kinds(schema))\n    source = trigger.get_subject(schema)\n\n    with ctx.detached() as tc, tc.newscope(fenced=True) as sctx:\n        sctx.anchors = sctx.anchors.copy()\n\n        anchors = {}\n        new_path = irast.PathId.from_type(\n            schema,\n            source,\n            typename=sn.QualName(\n                module='__derived__', name=ctx.aliases.get('__new__')\n            ),\n            env=ctx.env,\n        )\n        new_set = setgen.class_set(\n            source, path_id=new_path, ignore_rewrites=True, ctx=sctx)\n        new_set.expr = irast.TriggerAnchor(typeref=new_set.typeref)\n\n        old_set = None\n        if qltypes.TriggerKind.Insert not in kinds:\n            old_path = irast.PathId.from_type(\n                schema,\n                source,\n                typename=sn.QualName(\n                    module='__derived__', name=ctx.aliases.get('__old__')\n                ),\n                env=ctx.env,\n            )\n            old_set = setgen.class_set(\n                source, path_id=old_path, ignore_rewrites=True, ctx=sctx)\n            old_set.expr = irast.TriggerAnchor(typeref=old_set.typeref)\n            anchors['__old__'] = old_set\n        if qltypes.TriggerKind.Delete not in kinds:\n            anchors['__new__'] = new_set\n\n        for name, ir in anchors.items():\n            if scope == qltypes.TriggerScope.Each:\n                sctx.path_scope.attach_path(ir.path_id, span=None, ctx=sctx)\n                sctx.iterator_path_ids |= {ir.path_id}\n            sctx.anchors[name] = ir\n\n        trigger_expr: Optional[s_expr.Expression] = trigger.get_expr(schema)\n        assert trigger_expr\n        trigger_ast = trigger_expr.parse()\n\n        # A conditional trigger desugars to a FOR query that puts the\n        # condition in the FILTER of a trivial SELECT.\n        condition: Optional[s_expr.Expression] = trigger.get_condition(schema)\n        if condition:\n            trigger_ast = qlast.ForQuery(\n                iterator_alias='__',\n                iterator=qlast.SelectQuery(\n                    result=qlast.Tuple(elements=[]),\n                    where=condition.parse(),\n                ),\n                result=trigger_ast,\n            )\n\n        trigger_set = dispatch.compile(trigger_ast, ctx=sctx)\n\n    typeref = typegen.type_to_typeref(source, env=ctx.env)\n    taffected = {\n        (typegen.type_to_typeref(t, env=ctx.env), ir) for t, ir in affected\n    }\n    tall = {\n        typegen.type_to_typeref(t, env=ctx.env) for t in all_typs\n    }\n\n    return irast.Trigger(\n        expr=trigger_set,\n        kinds=kinds,\n        scope=scope,\n        source_type=typeref,\n        affected=taffected,\n        all_affected_types=tall,\n        new_set=new_set,\n        old_set=old_set,\n    )\n\n\ndef compile_triggers_phase(\n    dml_stmts: Collection[irast.MutatingStmt],\n    defining_trigger_on: Optional[s_types.Type],\n    defining_trigger_kinds: Optional[Collection[qltypes.TriggerKind]],\n    *,\n    ctx: context.ContextLevel,\n) -> tuple[irast.Trigger, ...]:\n    schema = ctx.env.schema\n\n    trigger_map: dict[\n        s_triggers.Trigger,\n        tuple[\n            set[tuple[s_objtypes.ObjectType, irast.MutatingStmt]],\n            set[s_objtypes.ObjectType],\n        ],\n    ] = {}\n    for stmt in dml_stmts:\n        kind = TRIGGER_KINDS[type(stmt)]\n\n        stype = schemactx.concretify(\n            setgen.get_set_type(stmt.result, ctx=ctx), ctx=ctx)\n        assert isinstance(stype, s_objtypes.ObjectType)\n        # For updates and deletes, we need to look to see if any\n        # descendant types have triggers.\n        if isinstance(stmt, irast.InsertStmt):\n            stypes = {stype}\n        else:\n            stypes = schemactx.get_all_concrete(stype, ctx=ctx)\n\n        # Process all the types, starting with the base type\n        for subtype in sorted(stypes, key=lambda t: t != stype):\n            if (defining_trigger_on and defining_trigger_kinds\n                and kind in defining_trigger_kinds\n                and subtype.issubclass(ctx.env.schema, defining_trigger_on)\n            ):\n                name = str(defining_trigger_on.get_name(ctx.env.schema))\n                raise errors.SchemaDefinitionError(\n                    f\"trigger on {name} after {kind.lower()} is recursive\"\n                )\n\n            for trigger in subtype.get_relevant_triggers(kind, schema):\n                mro = (trigger, *trigger.get_ancestors(schema).objects(schema))\n                base = mro[-1]\n                tmap, all_typs = trigger_map.setdefault(base, (set(), set()))\n                # N.B: If the *base type* of the DML appears, that\n                # suffices, because it covers everything, and we don't\n                # need to duplicate.  This is a specific interaction\n                # with how dml.compile_trigger is implemented, where\n                # processing the base type of a DML naturally covers\n                # all subtypes, but processing a child does not cover\n                # a grandchild.\n                if (stype, stmt) not in tmap:\n                    tmap.add((subtype, stmt))\n                all_typs.add(subtype)\n\n    # sort these by name just to avoid weird nondeterminism\n    return tuple(\n        compile_trigger(trigger, affected, all_typs, ctx=ctx)\n        for trigger, (affected, all_typs)\n        in sorted(trigger_map.items(), key=lambda t: t[0].get_name(schema))\n    )\n\n\ndef compile_triggers(\n    *,\n    ctx: context.ContextLevel,\n) -> tuple[tuple[irast.Trigger, ...], ...]:\n    defining_trigger = (\n        ctx.env.options.schema_object_context == s_triggers.Trigger)\n    defining_trigger_on = None\n    defining_trigger_kinds = None\n    if (\n        defining_trigger and\n        isinstance(ctx.env.options, options.CompilerOptions)\n    ):\n        defining_trigger_on = ctx.env.options.trigger_type\n        defining_trigger_kinds = ctx.env.options.trigger_kinds\n\n    ir_triggers: list[tuple[irast.Trigger, ...]] = []\n    start = 0\n    all_trigger_causes: set[tuple[irast.TypeRef, qltypes.TriggerKind]] = set()\n    while start < len(ctx.env.dml_stmts):\n        end = len(ctx.env.dml_stmts)\n        compiled_triggers = compile_triggers_phase(\n            ctx.env.dml_stmts[start:],\n            defining_trigger_on,\n            defining_trigger_kinds,\n            ctx=ctx\n        )\n        new_causes: set[tuple[irast.TypeRef, qltypes.TriggerKind]] = {\n            (affected_type, kind)\n            for compiled_trigger in compiled_triggers\n            for affected_type in compiled_trigger.all_affected_types\n            for kind in compiled_trigger.kinds\n        }\n\n        # Any given type is allowed allowed to have its triggers fire\n        # in *one* phase of trigger execution, since the semantics get\n        # a little unclear otherwise. We might relax this later.\n        overlap = new_causes & all_trigger_causes\n        if overlap:\n            names: Collection[str] = sorted(\n                f\"{str(cause[0].name_hint)} after {cause[1].lower()}\"\n                for cause in overlap\n            )\n            raise errors.QueryError(\n                f\"trigger would need to be executed in multiple stages on \"\n                f\"{', '.join(names)}\"\n            )\n        all_trigger_causes |= new_causes\n        ir_triggers.append(compiled_triggers)\n        start = end\n\n    return tuple(ir_triggers)\n"
  },
  {
    "path": "edb/edgeql/compiler/tuple_args.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Implementation of tuple argument decoding compiler.\n\nPostgres does not support passing records (tuples in edgeql) as query\nparameters, and so we need to go to some length to work around this.\n\nAll of the trickyness here comes from the interaction with arrays;\nwithout arrays, we could just split a tuple into multiple parameters.\nThe singly-nested case is also still fairly simple: turn an array of\ntuples into multiple parallel arrays, such that `array<tuple<str, int64>>`\nbecomes `array<str>` and `array<int64>`.\n\nThe doubly-nested case, in which the tuple itself contains an array\n(for example, `array<tuple<str, array<int64>>>`), is trickier:\nPostgres does not allow nested arrays (except if there is an intervening\nrecord type).\n\nThe key insight to resolve this dilemma is that a nested array type\n`array<array<T>>` can be transformed into two non-nested arrays with\ntypes `array<T>` and `array<int32>`, where the `array<T>` contains all\nof the elements of the nested arrays flattened out and the\n`array<int>` contains the indexes into the flattened array indicating where\neach of the nested arrays begins (followed by the length of the flattened\narray, so that pairs of adjacent elements form the slice indexes into\nthe flattened array).\n\nAs an example, consider a parameter of type `array<tuple<str, array<int64>>>`,\nwith the value:\n[\n    ('foo', [100]),\n    ('bar', [101, 102]),\n    ('baz', [103, 104, 105]),\n]\nWe will encode this into three arguments, with types `array<str>`,\n`array<int32>`, and `array<int64>`, with values:\n  ['foo', 'bar', 'baz']\n  [0, 1, 3, 6]\n  [100, 101, 102, 103, 104, 105]\n\n\nThe encoding algorithm is straightforward: we traverse the type and\nthe input data in tandem, appending data into the appropriate argument\narrays and tracking array lengths. This is implemented in our cython\nprotocol server, in edb.server.protocol.args_ser, and operates directly\non the wire encodings.\n\nThe decoding needs to be done as part of the SQL query we execute, so\nwe generate an EdgeQL query that decodes to the proper type. The generated\ncode operates in a top-down manner, looping over the arrays and constructing\nthe value in a single pass.\n\nThe code we generate for our running example could look something like:\n  with v0 := <array<str>>$0, v1 := <array<int32>>$1, v2 := <array<int64>>$2,\n  select array_agg((for i in range_unpack(range(0, len(v0))) union (\n    (\n      v0[i],\n      array_agg((for j in range_unpack(v1[i], v1[i + 1]) union (v2[i]))),\n    )\n  )))\nIn this case, since the nested array is simply an array of a scalar, we can\ndo an optimization and use slicing instead of an array_agg+for:\n  with v0 := <array<str>>$0, v1 := <array<int32>>$1, v2 := <array<int64>>$2,\n  select array_agg((for i in range_unpack(range(0, len(v0))) union (\n    (\n      v0[i],\n      v2[v1[i] : v1[i + 1]],\n    )\n  )))\n\nThe decoder queries will get placed in a CTE in the generated SQL.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport dataclasses\n\nfrom typing import Optional, Sequence, TYPE_CHECKING\n\nfrom edb import errors\nfrom edb.common.typeutils import not_none\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtypeutils\n\nfrom edb.schema import name as sn\nfrom edb.schema import types as s_types\nfrom edb.schema import utils as s_utils\n\nfrom edb.edgeql import ast as qlast\n\nfrom . import context\nfrom . import dispatch\nfrom . import typegen\n\nif TYPE_CHECKING:\n    from edb.schema import schema as s_schema\n\n# Since we process tuple types recusively in our cython server, insert\n# a recursion depth check here, to be confident that this won't blow\n# our C stack. (Though in practice I would expect anything that might\n# blow it to blow the python stack while compiling the translation.)\nMAX_NESTING = 20\n\n\ndef _lmost_is_array(typ: irast.ParamTransType) -> bool:\n    while isinstance(typ, irast.ParamTuple):\n        _, typ = typ.typs[0]\n    return isinstance(typ, irast.ParamArray)\n\n\ndef translate_type(\n    typeref: irast.TypeRef,\n    *,\n    schema: s_schema.Schema,\n) -> tuple[irast.ParamTransType, tuple[irast.TypeRef, ...]]:\n    \"\"\"Translate the type of a tuple-containing param to multiple params.\n\n    This computes a list of parameter types, as well as a\n    ParamTransType that clones the type information but augments each\n    node in the type with indexes that correspond to which parameter\n    data is drawn from. This is used to drive the encoder and the\n    decoder generator.\n    \"\"\"\n\n    typs: list[irast.TypeRef] = []\n\n    def trans(\n        typ: irast.TypeRef, in_array: bool, depth: int\n    ) -> irast.ParamTransType:\n        if depth > MAX_NESTING:\n            raise errors.QueryError(\n                f'type of parameter is too deeply nested')\n\n        start = len(typs)\n\n        if irtypeutils.is_array(typ):\n            # If our array is appearing already inside another array,\n            # we need to add an extra parameter\n            if in_array:\n                int_typeref = schema.get(\n                    sn.QualName('std', 'int32'), type=s_types.Type)\n                nschema, array_styp = s_types.Array.from_subtypes(\n                    schema, [int_typeref])\n                typs.append(irtypeutils.type_to_typeref(\n                    nschema, array_styp, cache=None))\n\n            if irtypeutils.is_array(typ.subtypes[0]):\n                # Treat nested arrays as if they are arrays of tuples of arrays\n                nschema, inner_array_styp = irtypeutils.ir_typeref_to_type(\n                    schema, typ.subtypes[0]\n                )\n                nschema, wrapper_tuple_styp = s_types.Tuple.from_subtypes(\n                    schema, {'f1': inner_array_styp}\n                )\n                wrapper_tuple_typ = irtypeutils.type_to_typeref(\n                    nschema, wrapper_tuple_styp, cache=None\n                )\n                return irast.ParamArray(\n                    typeref=typ,\n                    idx=start,\n                    typ=trans(\n                        wrapper_tuple_typ, in_array=True, depth=depth + 1\n                    ),\n                )\n            else:\n                return irast.ParamArray(\n                    typeref=typ,\n                    idx=start,\n                    typ=trans(typ.subtypes[0], in_array=True, depth=depth + 1),\n                )\n\n        elif irtypeutils.is_tuple(typ):\n            return irast.ParamTuple(\n                typeref=typ,\n                idx=start,\n                typs=tuple(\n                    (\n                        t.element_name,\n                        trans(t, in_array=in_array, depth=depth + 1),\n                    )\n                    for t in typ.subtypes\n                ),\n            )\n\n        else:\n            nt = typ\n            # If this appears in an array, the param needs to be an array\n            if in_array:\n                nschema, styp = irtypeutils.ir_typeref_to_type(schema, typ)\n                nschema, styp = s_types.Array.from_subtypes(nschema, [styp])\n                nt = irtypeutils.type_to_typeref(nschema, styp, cache=None)\n            typs.append(nt)\n            return irast.ParamScalar(typeref=typ, idx=start)\n\n    t = trans(typeref, in_array=False, depth=0)\n    return t, tuple(typs)\n\n\ndef _ref_to_ast(\n    typeref: irast.TypeRef, *, ctx: context.ContextLevel\n) -> qlast.TypeExpr:\n    ctx.env.schema, styp = irtypeutils.ir_typeref_to_type(\n        ctx.env.schema, typeref)\n    return s_utils.typeref_to_ast(ctx.env.schema, styp)\n\n\ndef _get_alias(\n    name: str, *, ctx: context.ContextLevel\n) -> tuple[str, qlast.Path]:\n    alias = ctx.aliases.get(name)\n    return alias, qlast.Path(\n        steps=[qlast.ObjectRef(name=alias)],\n    )\n\n\ndef _plus_const(expr: qlast.Expr, val: int) -> qlast.Expr:\n    if val == 0:\n        return expr\n    return qlast.BinOp(\n        left=expr,\n        op='+',\n        right=qlast.Constant.integer(val),\n    )\n\n\ndef _index(expr: qlast.Expr, idx: qlast.Expr) -> qlast.Indirection:\n    return qlast.Indirection(arg=expr, indirection=[qlast.Index(index=idx)])\n\n\ndef _make_tuple(\n    fields: Sequence[tuple[Optional[str], qlast.Expr]]\n) -> qlast.NamedTuple | qlast.Tuple:\n    is_named = fields and fields[0][0]\n    if is_named:\n        return qlast.NamedTuple(elements=[\n            qlast.TupleElement(name=qlast.Ptr(name=not_none(f)), val=e)\n            for f, e in fields\n        ])\n    else:\n        return qlast.Tuple(\n            elements=[e for _, e in fields]\n        )\n\n\ndef make_decoder(\n    ptyp: irast.ParamTransType,\n    qparams: tuple[irast.Param, ...],\n    *,\n    ctx: context.ContextLevel,\n) -> qlast.Expr:\n    \"\"\"Generate a decoder for tuple parameters.\n\n    More details in the module docstring.\n    \"\"\"\n    params: list[qlast.Expr] = [\n        qlast.TypeCast(\n            expr=qlast.QueryParameter(name=param.name),\n            type=_ref_to_ast(param.ir_type, ctx=ctx),\n            cardinality_mod=(\n                qlast.CardinalityModifier.Optional if not param.required\n                else None\n            ),\n        )\n        for param in qparams\n    ]\n\n    def mk(typ: irast.ParamTransType, idx: Optional[qlast.Expr]) -> qlast.Expr:\n        if isinstance(typ, irast.ParamScalar):\n            expr = params[typ.idx]\n            if idx is not None:\n                expr = _index(expr, idx)\n            if typ.cast_to:\n                expr = qlast.TypeCast(\n                    expr=expr,\n                    type=_ref_to_ast(typ.cast_to, ctx=ctx),\n                )\n            return expr\n\n        elif isinstance(typ, irast.ParamTuple):\n            return _make_tuple([(f, mk(t, idx=idx)) for f, t in typ.typs])\n\n        elif isinstance(typ, irast.ParamArray):\n            inner_idx_alias, inner_idx = _get_alias('idx', ctx=ctx)\n\n            lo: qlast.Expr\n            hi: qlast.Expr\n            if idx is None:\n                lo = qlast.Constant.integer(0)\n                hi = qlast.FunctionCall(\n                    func=('__std__', 'len'), args=[params[typ.idx]])\n                # If the leftmost element inside a toplevel array is\n                # itself an array, subtract 1 from the length (since\n                # array params have an extra element). We also need to\n                # call `max` to prevent generating an invalid range.\n                if _lmost_is_array(typ.typ):\n                    hi = qlast.FunctionCall(\n                        func=('__std__', 'max'), args=[\n                            qlast.Set(elements=[lo, _plus_const(hi, -1)])])\n            else:\n                lo = _index(params[typ.idx], idx)\n                hi = _index(params[typ.idx], _plus_const(idx, 1))\n\n            # If the contents is just a scalar, then we can take\n            # values directly from the scalar array parameter, without\n            # needing to iterate over the array directly.\n            # This is an optimization, and not necessary for correctness.\n            if isinstance(typ.typ, irast.ParamScalar) and not typ.typ.cast_to:\n                sub = params[typ.typ.idx]\n                # If we are in an array, do a slice\n                if idx is not None:\n                    sub = qlast.Indirection(\n                        arg=sub,\n                        indirection=[qlast.Slice(start=lo, stop=hi)],\n                    )\n                return sub\n\n            sub_expr = mk(typ.typ, idx=inner_idx)\n\n            loop = qlast.ForQuery(\n                iterator_alias=inner_idx_alias,\n                iterator=qlast.FunctionCall(\n                    func=('__std__', '__pg_generate_series'),\n                    args=[lo, _plus_const(hi, -1)],\n                ),\n                result=sub_expr,\n            )\n            res: qlast.Expr = qlast.FunctionCall(\n                func=('__std__', 'array_agg'), args=[loop],\n            )\n\n            # If the param is optional, and we are still at the\n            # top-level, insert a filter so that our aggregate doesn't\n            # create something from nothing.\n            if not qparams[typ.idx].required and idx is None:\n                res = qlast.SelectQuery(\n                    result=res,\n                    where=qlast.UnaryOp(op='EXISTS', operand=params[typ.idx]),\n                )\n\n            return res\n\n        else:\n            raise AssertionError(f'bogus type {typ}')\n\n    decoder = mk(ptyp, idx=None)\n\n    return decoder\n\n\ndef create_sub_params(\n    name: str,\n    required: bool,\n    typeref: irast.TypeRef,\n    pt: s_types.Type,\n    is_func_param: bool=False,\n    *,\n    ctx: context.ContextLevel,\n) -> Optional[irast.SubParams]:\n    \"\"\"Create sub parameters for a new param, if needed.\n\n    We need to do this if there is a tuple in the type.\n\n    We do this for nested arrays as well since array<array<...> is handled\n    as array<tuple<array>>.\n    \"\"\"\n    json_cast = ctx.env.options.json_parameters and not is_func_param\n    if not (\n        (\n            pt.is_tuple(ctx.env.schema)\n            or pt.is_anytuple(ctx.env.schema)\n            or pt.contains_array_of_array(ctx.env.schema)\n            or pt.contains_array_of_tuples(ctx.env.schema)\n        )\n        and not ctx.env.options.func_params\n    ) and not json_cast:\n        return None\n\n    pdt: irast.ParamTransType\n    arg_typs: tuple[irast.TypeRef, ...]\n    if json_cast:\n        json = typegen.type_to_typeref(\n            ctx.env.get_schema_type_and_track(sn.QualName('std', 'json')),\n            env=ctx.env,\n        )\n        pdt = irast.ParamScalar(typeref=json, cast_to=typeref, idx=0)\n        arg_typs = (json,)\n    else:\n        pdt, arg_typs = translate_type(typeref, schema=ctx.env.schema)\n\n    params = tuple([\n        irast.Param(\n            name=f'__edb_decoded_{name}_{i}__',\n            required=required,\n            ir_type=arg_typeref,\n            schema_type=typegen.type_from_typeref(arg_typeref, env=ctx.env),\n        )\n        for i, arg_typeref in enumerate(arg_typs)\n    ])\n\n    decode_ql = make_decoder(pdt, params, ctx=ctx)\n\n    return irast.SubParams(\n        trans_type=pdt, decoder_edgeql=decode_ql, params=params)\n\n\ndef finish_sub_params(\n    subps: irast.SubParams,\n    *,\n    ctx: context.ContextLevel,\n) -> Optional[irast.SubParams]:\n    \"\"\"Finalize the subparams by compiling the IR in the proper context.\n\n    We can't just compile it when doing create_sub_params, since that is\n    called from preprocessing and so is shared between queries.\n    \"\"\"\n    with ctx.newscope(fenced=True) as subctx:\n        decode_ir = dispatch.compile(subps.decoder_edgeql, ctx=subctx)\n\n    return dataclasses.replace(subps, decoder_ir=decode_ir)\n"
  },
  {
    "path": "edb/edgeql/compiler/typegen.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL compiler type-related helpers.\"\"\"\n\n\nfrom __future__ import annotations\n\nfrom typing import Optional, Sequence, cast, overload\n\nfrom edb import errors\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtyputils\nfrom edb.ir import utils as irutils\n\nfrom edb.schema import name as s_name\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import types as s_types\nfrom edb.schema import utils as s_utils\n\nfrom edb.edgeql import ast as qlast\n\nfrom . import context\nfrom . import dispatch\nfrom . import schemactx\nfrom . import setgen\n\n\ndef amend_empty_set_type(\n    es: irast.SetE[irast.EmptySet],\n    t: s_types.Type,\n    env: context.Environment\n) -> None:\n    env.set_types[es] = t\n    alias = es.path_id.target_name_hint.name\n    typename = s_name.QualName(module='__derived__', name=alias)\n    es.path_id = irast.PathId.from_type(\n        env.schema, t, env=env, typename=typename,\n        namespace=es.path_id.namespace,\n    )\n\n\ndef infer_common_type(\n    irs: Sequence[irast.Set], env: context.Environment\n) -> Optional[s_types.Type]:\n    if not irs:\n        raise errors.QueryError(\n            'cannot determine common type of an empty set',\n            span=irs[0].span)\n\n    types = []\n    empties = []\n\n    seen_object = False\n    seen_scalar = False\n    seen_coll = False\n\n    for i, arg in enumerate(irs):\n        if (\n            isinstance(arg.expr, irast.EmptySet)\n            and env.set_types[arg] is None\n        ):\n            empties.append(i)\n            continue\n\n        t = env.set_types[arg]\n        if isinstance(t, s_types.Collection):\n            seen_coll = True\n        elif isinstance(t, s_scalars.ScalarType):\n            seen_scalar = True\n        else:\n            seen_object = True\n        types.append(t)\n\n    if seen_coll + seen_scalar + seen_object > 1:\n        raise errors.QueryError(\n            'cannot determine common type',\n            span=irs[0].span)\n\n    if not types:\n        raise errors.QueryError(\n            'cannot determine common type of an empty set',\n            span=irs[0].span)\n\n    common_type = None\n    if seen_scalar or seen_coll:\n        it = iter(types)\n        common_type = next(it)\n        while True:\n            next_type = next(it, None)\n            if next_type is None:\n                break\n            env.schema, common_type = (\n                common_type.find_common_implicitly_castable_type(\n                    next_type,\n                    env.schema,\n                )\n            )\n            if common_type is None:\n                break\n    else:\n        common_types = s_utils.get_class_nearest_common_ancestors(\n            env.schema,\n            cast(Sequence[s_types.InheritingType], types),\n        )\n        # We arbitrarily select the first nearest common ancestor\n        common_type = common_types[0] if common_types else None\n\n    if common_type is None:\n        return None\n\n    for i in empties:\n        amend_empty_set_type(\n            cast(irast.SetE[irast.EmptySet], irs[i]), common_type, env)\n\n    return common_type\n\n\ndef type_to_ql_typeref(\n    t: s_types.Type,\n    *,\n    _name: Optional[str] = None,\n    ctx: context.ContextLevel,\n) -> qlast.TypeExpr:\n    return s_utils.typeref_to_ast(\n        ctx.env.schema,\n        t,\n        disambiguate_std='std' in ctx.modaliases,\n    )\n\n\ndef ql_typeexpr_to_ir_typeref(\n    ql_t: qlast.TypeExpr, *, ctx: context.ContextLevel\n) -> irast.TypeRef:\n\n    stype = ql_typeexpr_to_type(ql_t, ctx=ctx)\n    return irtyputils.type_to_typeref(\n        ctx.env.schema, stype, cache=ctx.env.type_ref_cache\n    )\n\n\ndef ql_typeexpr_to_type(\n    ql_t: qlast.TypeExpr, *, ctx: context.ContextLevel\n) -> s_types.Type:\n\n    (op, _, types) = (\n        _ql_typeexpr_get_types(ql_t, ctx=ctx)\n    )\n    return _ql_typeexpr_combine_types(op, types, ctx=ctx)\n\n\ndef _ql_typeexpr_combine_types(\n        op: Optional[str], types: list[s_types.Type], *,\n        ctx: context.ContextLevel\n) -> s_types.Type:\n    if len(types) == 1:\n        return types[0]\n    elif op == '|':\n        return schemactx.get_union_type(types, ctx=ctx)\n    elif op == '&':\n        return schemactx.get_intersection_type(types, ctx=ctx)\n    else:\n        raise errors.InternalServerError('This should never happen')\n\n\ndef _ql_typeexpr_get_types(\n    ql_t: qlast.TypeExpr, *, ctx: context.ContextLevel\n) -> tuple[Optional[str], bool, list[s_types.Type]]:\n\n    if isinstance(ql_t, qlast.TypeOf):\n        with ctx.new() as subctx:\n            # Use an empty scope tree, to avoid polluting things pointlessly\n            subctx.path_scope = irast.ScopeTreeNode()\n            subctx.expr_exposed = context.Exposure.UNEXPOSED\n            orig_rewrites = ctx.env.type_rewrites.copy()\n            ir_set = dispatch.compile(ql_t.expr, ctx=subctx)\n            stype = setgen.get_set_type(ir_set, ctx=subctx)\n            ctx.env.type_rewrites = orig_rewrites\n\n        return (None, True, [stype])\n\n    elif isinstance(ql_t, qlast.TypeOp):\n        if ql_t.op in [qlast.TypeOpName.OR, qlast.TypeOpName.AND]:\n            (left_op, left_leaf, left_types) = (\n                _ql_typeexpr_get_types(ql_t.left, ctx=ctx)\n            )\n            (right_op, right_leaf, right_types) = (\n                _ql_typeexpr_get_types(ql_t.right, ctx=ctx)\n            )\n\n            # We need to validate that type ops are applied only to\n            # object types. So we check the base case here, when the\n            # left or right operand is a single type, because if it's\n            # a longer list, then we know that it was already composed\n            # of \"|\" or \"&\", or it is the result of inference by\n            # \"typeof\" and is a list of object types anyway.\n            if left_leaf and not left_types[0].is_object_type():\n                raise errors.UnsupportedFeatureError(\n                    f\"cannot use type operator '{ql_t.op}' with non-object \"\n                    f\"type {left_types[0].get_displayname(ctx.env.schema)}\",\n                    span=ql_t.left.span)\n            if right_leaf and not right_types[0].is_object_type():\n                raise errors.UnsupportedFeatureError(\n                    f\"cannot use type operator '{ql_t.op}' with non-object \"\n                    f\"type {right_types[0].get_displayname(ctx.env.schema)}\",\n                    span=ql_t.right.span)\n\n            # if an operand is either a single type or uses the same operator,\n            # flatten it into the result types list.\n            # if an operand has a different operator is used, its types should\n            # be combined into a new type before appending to the result types.\n            types: list[s_types.Type] = []\n            types += (\n                left_types\n                if left_op is None or left_op == ql_t.op else\n                [_ql_typeexpr_combine_types(left_op, left_types, ctx=ctx)]\n            )\n            types += (\n                right_types\n                if right_op is None or right_op == ql_t.op else\n                [_ql_typeexpr_combine_types(right_op, right_types, ctx=ctx)]\n            )\n\n            return (ql_t.op, False, types)\n\n        raise errors.UnsupportedFeatureError(\n            f'type operator {ql_t.op!r} is not implemented',\n            span=ql_t.span)\n\n    elif isinstance(ql_t, qlast.TypeName):\n        return (None, True, [_ql_typename_to_type(ql_t, ctx=ctx)])\n\n    else:\n        raise errors.EdgeQLSyntaxError(\"Unexpected type expression\",\n                                       span=ql_t.span)\n\n\ndef _ql_typename_to_type(\n    ql_t: qlast.TypeName, *, ctx: context.ContextLevel\n) -> s_types.Type:\n    if ql_t.subtypes:\n        assert isinstance(ql_t.maintype, qlast.ObjectRef)\n        coll = s_types.Collection.get_class(ql_t.maintype.name)\n        ct: s_types.Type\n\n        if issubclass(coll, s_types.Tuple):\n            t_subtypes = {}\n            named = False\n            for si, st in enumerate(ql_t.subtypes):\n                if st.name:\n                    named = True\n                    type_name = st.name\n                else:\n                    type_name = str(si)\n\n                t_subtypes[type_name] = ql_typeexpr_to_type(st, ctx=ctx)\n\n            ctx.env.schema, ct = coll.from_subtypes(\n                ctx.env.schema, t_subtypes, {'named': named})\n            return ct\n        else:\n            a_subtypes = []\n            for st in ql_t.subtypes:\n                a_subtypes.append(ql_typeexpr_to_type(st, ctx=ctx))\n\n            ctx.env.schema, ct = coll.from_subtypes(ctx.env.schema, a_subtypes)\n            return ct\n    else:\n        return schemactx.get_schema_type(ql_t.maintype, ctx=ctx)\n\n\n@overload\ndef ptrcls_from_ptrref(\n    ptrref: irast.PointerRef,\n    *,\n    ctx: context.ContextLevel,\n) -> s_pointers.Pointer:\n    ...\n\n\n@overload\ndef ptrcls_from_ptrref(\n    ptrref: irast.TupleIndirectionPointerRef,\n    *,\n    ctx: context.ContextLevel,\n) -> irast.TupleIndirectionLink:\n    ...\n\n\n@overload\ndef ptrcls_from_ptrref(\n    ptrref: irast.TypeIntersectionPointerRef,\n    *,\n    ctx: context.ContextLevel,\n) -> irast.TypeIntersectionLink:\n    ...\n\n\n@overload\ndef ptrcls_from_ptrref(\n    ptrref: irast.BasePointerRef,\n    *,\n    ctx: context.ContextLevel,\n) -> s_pointers.PointerLike:\n    ...\n\n\ndef ptrcls_from_ptrref(\n    ptrref: irast.BasePointerRef,\n    *,\n    ctx: context.ContextLevel,\n) -> s_pointers.PointerLike:\n\n    cached = ctx.env.ptr_ref_cache.get_ptrcls_for_ref(ptrref)\n    if cached is not None:\n        return cached\n\n    ctx.env.schema, ptr = irtyputils.ptrcls_from_ptrref(\n        ptrref, schema=ctx.env.schema)\n\n    return ptr\n\n\ndef ptr_to_ptrref(\n    ptrcls: s_pointers.Pointer,\n    *,\n    ctx: context.ContextLevel,\n) -> irast.PointerRef:\n    return irtyputils.ptrref_from_ptrcls(\n        schema=ctx.env.schema,\n        ptrcls=ptrcls,\n        cache=ctx.env.ptr_ref_cache,\n        typeref_cache=ctx.env.type_ref_cache,\n    )\n\n\ndef collapse_type_intersection_rptr(\n    ir_set: irast.Set,\n    *,\n    ctx: context.ContextLevel,\n) -> tuple[irast.Set, list[s_pointers.Pointer]]:\n\n    ind_prefix, ind_ptrs = irutils.collapse_type_intersection(ir_set)\n    if not ind_ptrs:\n        return ir_set, []\n\n    rptr_specialization: set[irast.PointerRef] = set()\n    for ind_ptr in ind_ptrs:\n        for ind_ptr in ind_ptrs:\n            if ind_ptr.ptrref.rptr_specialization:\n                rptr_specialization.update(\n                    ind_ptr.ptrref.rptr_specialization)\n            elif (\n                not ind_ptr.ptrref.is_empty\n                and isinstance(ind_ptr.source.expr, irast.Pointer)\n            ):\n                assert isinstance(ind_ptr.source.expr.ptrref, irast.PointerRef)\n                rptr_specialization.add(ind_ptr.source.expr.ptrref)\n\n    ptrs = [ptrcls_from_ptrref(ptrref, ctx=ctx)\n            for ptrref in rptr_specialization]\n\n    return ind_prefix, ptrs\n\n\ndef type_from_typeref(\n    t: irast.TypeRef,\n    env: context.Environment,\n) -> s_types.Type:\n    env.schema, styp = irtyputils.ir_typeref_to_type(env.schema, t)\n    return styp\n\n\ndef type_to_typeref(\n    t: s_types.Type,\n    env: context.Environment,\n) -> irast.TypeRef:\n    schema = env.schema\n    cache = env.type_ref_cache\n    expr_type = t.get_expr_type(env.schema)\n    include_children = (\n        expr_type is s_types.ExprType.Update\n        or expr_type is s_types.ExprType.Delete\n        or isinstance(t, s_objtypes.ObjectType)\n    )\n    include_ancestors = (\n        expr_type is s_types.ExprType.Insert\n        or expr_type is s_types.ExprType.Update\n        or expr_type is s_types.ExprType.Delete\n    )\n    return irtyputils.type_to_typeref(\n        schema,\n        t,\n        include_children=include_children,\n        include_ancestors=include_ancestors,\n        cache=cache,\n    )\n"
  },
  {
    "path": "edb/edgeql/compiler/viewgen.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"EdgeQL shape compilation functions.\"\"\"\n\n\nfrom __future__ import annotations\n\nimport collections\nimport dataclasses\nimport functools\nfrom typing import (\n    Callable,\n    Optional,\n    AbstractSet,\n    Mapping,\n    Sequence,\n    NamedTuple,\n    cast,\n    TYPE_CHECKING,\n)\n\nfrom edb import errors\nfrom edb.common import ast\nfrom edb.common import parsing\nfrom edb.common import topological\nfrom edb.common.typeutils import downcast, not_none\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils\nfrom edb.ir import utils as irutils\nimport edb.ir.typeutils as irtypeutils\n\nfrom edb.schema import futures as s_futures\nfrom edb.schema import links as s_links\nfrom edb.schema import name as sn\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import objects as s_objects\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import properties as s_props\nfrom edb.schema import types as s_types\nfrom edb.schema import expr as s_expr\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom . import astutils\nfrom . import context\nfrom . import dispatch\nfrom . import eta_expand\nfrom . import pathctx\nfrom . import schemactx\nfrom . import setgen\nfrom . import typegen\n\nif TYPE_CHECKING:\n    from edb.schema import sources as s_sources\n\n\nclass ShapeElementDesc(NamedTuple):\n    \"\"\"Annotated QL shape element for processing convenience\"\"\"\n\n    #: Shape element AST\n    ql: qlast.ShapeElement\n    #: Canonical Path AST for the shape element\n    path_ql: qlast.Path\n    #: The underlying pointer AST\n    ptr_ql: qlast.Ptr\n    #: The name of the pointer\n    ptr_name: str\n    #: Pointer source object\n    source: s_sources.Source\n    #: Target type intersection (if any)\n    target_typexpr: Optional[qlast.TypeExpr]\n    #: Whether the source is a type intersection\n    is_polymorphic: bool\n    #: Whether the pointer is a link property\n    is_linkprop: bool\n\n\nclass EarlyShapePtr(NamedTuple):\n    \"\"\"Stage 1 shape processing result element\"\"\"\n    ptrcls: s_pointers.Pointer\n    target_set: Optional[irast.Set]\n    shape_origin: qlast.ShapeOrigin\n    span: Optional[parsing.Span]\n\n\nclass ShapePtr(NamedTuple):\n    \"\"\"Stage 2 shape processing result element\"\"\"\n    source_set: irast.Set\n    ptrcls: s_pointers.Pointer\n    shape_op: qlast.ShapeOp\n    target_set: Optional[irast.Set]\n    span: Optional[parsing.Span]\n\n\n@dataclasses.dataclass(kw_only=True, frozen=True)\nclass ShapeContext:\n    # a helper object for passing shape compile parameters\n\n    path_id_namespace: Optional[irast.Namespace] = None\n\n    view_rptr: Optional[context.ViewRPtr] = None\n\n    view_name: Optional[sn.QualName] = None\n\n    exprtype: s_types.ExprType = s_types.ExprType.Select\n\n\ndef process_view(\n    ir_set: irast.Set,\n    *,\n    stype: s_objtypes.ObjectType,\n    elements: Sequence[qlast.ShapeElement],\n    view_rptr: Optional[context.ViewRPtr] = None,\n    view_name: Optional[sn.QualName] = None,\n    exprtype: s_types.ExprType = s_types.ExprType.Select,\n    ctx: context.ContextLevel,\n    span: Optional[parsing.Span],\n) -> tuple[s_objtypes.ObjectType, irast.Set]:\n\n    cache_key = (stype, exprtype, tuple(elements))\n    view_scls = ctx.env.shape_type_cache.get(cache_key)\n    if view_scls is not None:\n        return view_scls, ir_set\n\n    # XXX: This is an unfortunate hack to ensure that \"cannot\n    # reference correlated set\" errors get produced correctly,\n    # since there needs to be an intervening branch for a\n    # factoring fence to be respected.\n    hackscope = ctx.path_scope.attach_branch()\n    pathctx.register_set_in_scope(ir_set, path_scope=hackscope, ctx=ctx)\n    hackscope.remove()\n    ctx.path_scope.attach_subtree(hackscope, ctx=ctx)\n\n    # Make a snapshot of aliased_views that can't be mutated\n    # in any parent scopes.\n    ctx.aliased_views = collections.ChainMap(dict(ctx.aliased_views))\n\n    s_ctx = ShapeContext(\n        path_id_namespace=None,\n        view_rptr=view_rptr,\n        view_name=view_name,\n        exprtype=exprtype,\n    )\n\n    view_scls, ir = _process_view(\n        ir_set,\n        stype=stype,\n        elements=elements,\n        ctx=ctx,\n        s_ctx=s_ctx,\n        span=span,\n    )\n\n    ctx.env.shape_type_cache[cache_key] = view_scls\n\n    return view_scls, ir\n\n\ndef _process_view(\n    ir_set: irast.Set,\n    *,\n    stype: s_objtypes.ObjectType,\n    elements: Optional[Sequence[qlast.ShapeElement]],\n    s_ctx: ShapeContext,\n    ctx: context.ContextLevel,\n    span: Optional[parsing.Span],\n) -> tuple[s_objtypes.ObjectType, irast.Set]:\n    path_id = ir_set.path_id\n    view_rptr = s_ctx.view_rptr\n\n    view_name = s_ctx.view_name\n    needs_real_name = view_name is None and ctx.env.options.schema_view_mode\n    generated_name = None\n    if needs_real_name and view_rptr is not None:\n        # Make sure persistent schema expression aliases have properly formed\n        # names as opposed to the usual mangled form of the ephemeral\n        # aliases.  This is needed for introspection readability, as well\n        # as helps in maintaining proper type names for schema\n        # representations that require alphanumeric names, such as\n        # GraphQL.\n        #\n        # We use the name of the source together with the name\n        # of the inbound link to form the name, so in e.g.\n        #    CREATE ALIAS V := (SELECT Foo { bar: { baz: { ... } })\n        # The name of the innermost alias would be \"__V__bar__baz\".\n        source_name = view_rptr.source.get_name(ctx.env.schema).name\n        if not source_name.startswith('__'):\n            source_name = f'__{source_name}'\n        if view_rptr.ptrcls_name is not None:\n            ptr_name = view_rptr.ptrcls_name.name\n        elif view_rptr.ptrcls is not None:\n            ptr_name = view_rptr.ptrcls.get_shortname(ctx.env.schema).name\n        else:\n            raise errors.InternalServerError(\n                '_process_view in schema mode received view_rptr with '\n                'neither ptrcls_name, not ptrcls'\n            )\n\n        generated_name = f'{source_name}__{ptr_name}'\n    elif needs_real_name and ctx.env.alias_result_view_name:\n        # If this is a persistent schema expression but we aren't just\n        # obviously sitting on an rptr (e.g CREATE ALIAS V := (Foo { x }, 10)),\n        # we create a name like __V__Foo__2.\n        source_name = ctx.env.alias_result_view_name.name\n        type_name = stype.get_name(ctx.env.schema).name\n        generated_name = f'__{source_name}__{type_name}'\n\n    if generated_name:\n        # If there are multiple, we want to stick a number on, but we'd\n        # like to skip the number if there aren't.\n        name = ctx.aliases.get(\n            generated_name).replace('~1', '').replace('~', '__')\n        view_name = sn.QualName(\n            module=ctx.derived_target_module or '__derived__',\n            name=name,\n        )\n\n    view_scls = schemactx.derive_view(\n        stype,\n        exprtype=s_ctx.exprtype,\n        derived_name=view_name,\n        ctx=ctx,\n    )\n    assert isinstance(view_scls, s_objtypes.ObjectType), view_scls\n    is_mutation = s_ctx.exprtype.is_insert() or s_ctx.exprtype.is_update()\n    is_defining_shape = ctx.expr_exposed or is_mutation\n\n    ir_set = setgen.ensure_set(ir_set, type_override=view_scls, ctx=ctx)\n    # Maybe rematerialize the set. The old ir_set might have already\n    # been materialized, but the new version would be missing from the\n    # use_sets.\n    if isinstance(ir_set.expr, irast.Pointer):\n        ctx.env.schema, remat_ptrcls = typeutils.ptrcls_from_ptrref(\n            ir_set.expr.ptrref, schema=ctx.env.schema\n        )\n        setgen.maybe_materialize(remat_ptrcls, ir_set, ctx=ctx)\n\n    if view_rptr is not None and view_rptr.ptrcls is None:\n        target_scls = stype if is_mutation else view_scls\n        derive_ptrcls(view_rptr, target_scls=target_scls, ctx=ctx)\n\n    pointers: dict[s_pointers.Pointer, EarlyShapePtr] = {}\n\n    if elements is None:\n        elements = []\n\n    shape_desc: list[ShapeElementDesc] = []\n    # First, find all explicit pointers (i.e. non-splat elements)\n    for shape_el in elements:\n        if isinstance(shape_el.expr.steps[0], qlast.Splat):\n            continue\n\n        shape_desc.append(\n            _shape_el_ql_to_shape_el_desc(\n                shape_el, source=view_scls, s_ctx=s_ctx, ctx=ctx\n            )\n        )\n\n    explicit_ptr_names = {\n        desc.ptr_name for desc in shape_desc if not desc.is_linkprop\n    }\n\n    explicit_lprop_names = {\n        desc.ptr_name for desc in shape_desc if desc.is_linkprop\n    }\n\n    # Now look for any splats and expand them.\n    # Track descriptions by name and whether they are link properties.\n    splat_descs: dict[tuple[str, bool], ShapeElementDesc] = {}\n    for shape_el in elements:\n        if not isinstance(shape_el.expr.steps[0], qlast.Splat):\n            continue\n\n        if s_ctx.exprtype is not s_types.ExprType.Select:\n            raise errors.QueryError(\n                \"unexpected splat operator in non-SELECT shape\",\n                span=shape_el.expr.span,\n            )\n\n        if ctx.env.options.func_params is not None:\n            raise errors.UnsupportedFeatureError(\n                \"splat operators in function bodies are not supported\",\n                span=shape_el.expr.span,\n            )\n\n        splat = shape_el.expr.steps[0]\n        if splat.type is not None:\n            splat_type = typegen.ql_typeexpr_to_type(splat.type, ctx=ctx)\n            if not isinstance(splat_type, s_objtypes.ObjectType):\n                vn = splat_type.get_verbosename(schema=ctx.env.schema)\n                raise errors.QueryError(\n                    f\"splat operator expects an object type, got {vn}\",\n                    span=splat.type.span,\n                )\n\n            if not stype.issubclass(ctx.env.schema, splat_type):\n                vn = stype.get_verbosename(ctx.env.schema)\n                vn2 = splat_type.get_verbosename(schema=ctx.env.schema)\n                raise errors.QueryError(\n                    f\"splat type must be {vn} or its parent type, \"\n                    f\"got {vn2}\",\n                    span=splat.type.span,\n                )\n\n            if splat.intersection is not None:\n                intersector_type = typegen.ql_typeexpr_to_type(\n                    splat.intersection.type, ctx=ctx)\n                splat_type = schemactx.apply_intersection(\n                    splat_type,\n                    intersector_type,\n                    ctx=ctx,\n                ).stype\n                assert isinstance(splat_type, s_objtypes.ObjectType)\n\n        elif splat.intersection is not None:\n            splat_type = typegen.ql_typeexpr_to_type(\n                splat.intersection.type, ctx=ctx)\n            if not isinstance(splat_type, s_objtypes.ObjectType):\n                vn = splat_type.get_verbosename(schema=ctx.env.schema)\n                raise errors.QueryError(\n                    f\"splat operator expects an object type, got {vn}\",\n                    span=splat.intersection.type.span,\n                )\n        else:\n            splat_type = stype\n\n        if (\n            view_rptr is not None\n            and isinstance(view_rptr.ptrcls, s_links.Link)\n        ):\n            splat_rlink = view_rptr.ptrcls\n        else:\n            splat_rlink = None\n\n        expanded_splat = _expand_splat(\n            splat_type,\n            depth=splat.depth,\n            intersection=splat.intersection,\n            rlink=splat_rlink,\n            skip_ptrs=explicit_ptr_names,\n            skip_lprops=explicit_lprop_names,\n            ctx=ctx,\n        )\n\n        for splat_el in expanded_splat:\n            desc = _shape_el_ql_to_shape_el_desc(\n                splat_el, source=view_scls, s_ctx=s_ctx, ctx=ctx\n            )\n            desc_key: tuple[str, bool] = (desc.ptr_name, desc.is_linkprop)\n            if old_desc := splat_descs.get(desc_key):\n                # If pointers appear in multiple splats, we take the\n                # one from the ancestor class. If neither class is an\n                # ancestor, we reject it.\n                # TODO: Accept it instead, if the types are the same.\n                new_source: object = desc.source\n                old_source: object = old_desc.source\n                if isinstance(new_source, s_links.Link):\n                    new_source = new_source.get_source(ctx.env.schema)\n                assert isinstance(new_source, s_objtypes.ObjectType)\n                if isinstance(old_source, s_links.Link):\n                    old_source = old_source.get_source(ctx.env.schema)\n                assert isinstance(old_source, s_objtypes.ObjectType)\n                new_source = schemactx.concretify(new_source, ctx=ctx)\n                old_source = schemactx.concretify(old_source, ctx=ctx)\n\n                if new_source.issubclass(ctx.env.schema, old_source):\n                    # Do nothing.\n                    pass\n                elif old_source.issubclass(ctx.env.schema, new_source):\n                    # Take the new one\n                    splat_descs[desc_key] = desc\n                else:\n                    vn1 = old_source.get_verbosename(schema=ctx.env.schema)\n                    vn2 = new_source.get_verbosename(schema=ctx.env.schema)\n                    raise errors.QueryError(\n                        f\"link or property '{desc.ptr_name}' appears in splats \"\n                        f\"for unrelated types: {vn1} and {vn2}\",\n                        span=splat.span,\n                    )\n\n            else:\n                splat_descs[desc_key] = desc\n\n    shape_desc.extend(splat_descs.values())\n\n    for shape_el_desc in shape_desc:\n        with ctx.new() as scopectx:\n            # when doing insert or update with a compexpr, generate the\n            # the anchor for __default__\n            if (\n                (\n                    # mutating statement, ptrcls guaranteed to exist\n                    (s_ctx.exprtype.is_insert() or s_ctx.exprtype.is_update())\n                    # linkprops are used in non-mutating statements as part of\n                    # mutating statemnts, ptrcls not guaranteed to exist\n                    or (\n                        shape_el_desc.is_linkprop\n                        # check that the linkprop actually exists in the source\n                        and (\n                            sn.UnqualName(shape_el_desc.ptr_name) in (\n                                source_ptr.get_local_name(scopectx.env.schema)\n                                for source_ptr in (\n                                    shape_el_desc.source\n                                    .get_pointers(scopectx.env.schema)\n                                    .objects(scopectx.env.schema)\n                                )\n                            )\n                        )\n                    )\n                )\n                and shape_el_desc.ql.compexpr is not None\n                and shape_el_desc.ptr_name not in (\n                    ctx.special_computables_in_mutation_shape\n                )\n            ):\n                ptrcls = setgen.resolve_ptr(\n                    shape_el_desc.source,\n                    shape_el_desc.ptr_name,\n                    track_ref=shape_el_desc.ptr_ql,\n                    ctx=scopectx\n                )\n\n                compexpr_uses_default = False\n                compexpr_default_span: Optional[parsing.Span] = None\n                for path_node in (\n                    ast.find_children(\n                        shape_el_desc.ql.compexpr,\n                        qlast.Path,\n                        extra_skip_types=(qlast.Query, qlast.Shape),\n                    )\n                    if not isinstance(\n                        shape_el_desc.ql.compexpr,\n                        (qlast.Query, qlast.Shape)\n                    ) else\n                    ()\n                ):\n                    for step in path_node.steps:\n                        if not isinstance(step, qlast.SpecialAnchor):\n                            continue\n                        if step.name != '__default__':\n                            continue\n\n                        compexpr_uses_default = True\n                        compexpr_default_span = step.span\n                        break\n\n                    if compexpr_uses_default:\n                        break\n\n                if compexpr_uses_default:\n                    def make_error(\n                        span: Optional[parsing.Span], hint: str\n                    ) -> errors.InvalidReferenceError:\n                        return errors.InvalidReferenceError(\n                            f'__default__ cannot be used in this expression',\n                            span=span,\n                            hint=hint,\n                        )\n\n                    default_expr: Optional[s_expr.Expression] = (\n                        ptrcls.get_default(scopectx.env.schema)\n                    )\n                    if default_expr is None:\n                        raise make_error(\n                            compexpr_default_span,\n                            'No default expression exists',\n                        )\n\n                    default_ast_expr = default_expr.parse()\n\n                    if any(\n                        any(\n                            (\n                                isinstance(step, qlast.SpecialAnchor)\n                                and step.name == '__source__'\n                            )\n                            for step in path_node.steps\n                        )\n                        for path_node in ast.find_children(\n                            default_ast_expr, qlast.Path\n                        )\n                    ):\n                        raise make_error(\n                            compexpr_default_span,\n                            'Default expression uses __source__',\n                        )\n\n                    if astutils.contains_dml(default_ast_expr, ctx=ctx):\n                        raise make_error(\n                            compexpr_default_span,\n                            'Default expression uses DML',\n                        )\n\n                    default_set = dispatch.compile(\n                        default_ast_expr, ctx=scopectx\n                    )\n\n                    scopectx.anchors['__default__'] = default_set\n\n            pointer, ptr_set = _normalize_view_ptr_expr(\n                ir_set,\n                shape_el_desc,\n                view_scls,\n                path_id=path_id,\n                pending_pointers=pointers,\n                s_ctx=s_ctx,\n                ctx=scopectx,\n            )\n\n            pointers[pointer] = EarlyShapePtr(\n                pointer, ptr_set, shape_el_desc.ql.origin, shape_el_desc.ql.span\n            )\n\n    # If we are not defining a shape (so we might care about\n    # materialization), look through our parent view (if one exists)\n    # for materialized properties that are not present in this shape.\n    # If any are found, inject them.\n    # (See test_edgeql_volatility_rebind_flat_01 for an example.)\n    schema = ctx.env.schema\n    base = view_scls.get_bases(schema).objects(schema)[0]\n    base_ptrs = (view_scls.get_pointers(schema).objects(schema)\n                 if not is_defining_shape else ())\n    for ptrcls in base_ptrs:\n        if ptrcls in pointers or base not in ctx.env.view_shapes:\n            continue\n        pptr = ptrcls.get_bases(schema).objects(schema)[0]\n        if (pptr, qlast.ShapeOp.MATERIALIZE) not in ctx.env.view_shapes[base]:\n            continue\n\n        # Make up a dummy shape element\n        name = ptrcls.get_shortname(schema).name\n        dummy_el = qlast.ShapeElement(expr=qlast.Path(\n            steps=[qlast.Ptr(name=name)]))\n        dummy_el_desc = _shape_el_ql_to_shape_el_desc(\n            dummy_el, source=view_scls, s_ctx=s_ctx, ctx=ctx\n        )\n\n        with ctx.new() as scopectx:\n            pointer, ptr_set = _normalize_view_ptr_expr(\n                ir_set,\n                dummy_el_desc,\n                view_scls,\n                path_id=path_id,\n                s_ctx=s_ctx,\n                ctx=scopectx,\n            )\n\n        pointers[pointer] = EarlyShapePtr(\n            pointer, ptr_set, qlast.ShapeOrigin.MATERIALIZATION, None\n        )\n\n    specified_ptrs = {\n        ptrcls.get_local_name(ctx.env.schema) for ptrcls in pointers\n    }\n\n    # defaults\n    if s_ctx.exprtype.is_insert():\n        defaults_ptrs = _gen_pointers_from_defaults(\n            specified_ptrs, view_scls, ir_set, stype, s_ctx, ctx\n        )\n        pointers.update(defaults_ptrs)\n\n    # rewrites\n    rewrite_kind = (\n        qltypes.RewriteKind.Insert\n        if s_ctx.exprtype.is_insert()\n        else qltypes.RewriteKind.Update\n        if s_ctx.exprtype.is_update()\n        else None\n    )\n\n    if rewrite_kind:\n        rewrites = _compile_rewrites(\n            specified_ptrs, rewrite_kind, view_scls, ir_set, stype, s_ctx, ctx\n        )\n        if rewrites:\n            ctx.env.dml_rewrites[ir_set] = rewrites\n    else:\n        rewrites = None\n\n    if s_ctx.exprtype.is_insert():\n        _raise_on_missing(pointers, stype, rewrites, ctx, span=span)\n\n    set_shape = []\n    shape_ptrs: list[ShapePtr] = []\n\n    for ptrcls, ptr_set, _, span in pointers.values():\n        source: s_types.Type | s_pointers.PointerLike\n\n        if ptrcls.is_link_property(ctx.env.schema):\n            assert view_rptr is not None and view_rptr.ptrcls is not None\n            source = view_rptr.ptrcls\n        else:\n            source = view_scls\n\n        if is_defining_shape:\n            cinfo = ctx.env.source_map.get(ptrcls)\n            if cinfo is not None:\n                shape_op = cinfo.shape_op\n            else:\n                shape_op = qlast.ShapeOp.ASSIGN\n        elif ptrcls.get_computable(ctx.env.schema):\n            shape_op = qlast.ShapeOp.MATERIALIZE\n        else:\n            continue\n\n        ctx.env.view_shapes[source].append((ptrcls, shape_op))\n        shape_ptrs.append(ShapePtr(ir_set, ptrcls, shape_op, ptr_set, span))\n\n    rptrcls = view_rptr.ptrcls if view_rptr else None\n    shape_ptrs = _get_early_shape_configuration(\n        ir_set, shape_ptrs, rptrcls=rptrcls, ctx=ctx)\n\n    # Produce the shape. The main thing here is that we need to fixup\n    # all of the rptrs to properly point back at ir_set.\n    for _, ptrcls, shape_op, ptr_set, ptr_span in shape_ptrs:\n        if ptrcls in ctx.env.pointer_specified_info:\n            _, _, ptr_span = ctx.env.pointer_specified_info[ptrcls]\n\n        if ptr_set:\n            src_path_id = path_id\n            is_linkprop = ptrcls.is_link_property(ctx.env.schema)\n            if is_linkprop:\n                src_path_id = src_path_id.ptr_path()\n\n            ptr_set.path_id = pathctx.extend_path_id(\n                src_path_id,\n                ptrcls=ptrcls,\n                ns=ctx.path_id_namespace,\n                ctx=ctx,\n            )\n            assert not isinstance(ptr_set.expr, irast.Pointer)\n            ptr_set.expr = irast.Pointer(\n                source=ir_set,\n                expr=ptr_set.expr,\n                direction=s_pointers.PointerDirection.Outbound,\n                ptrref=not_none(ptr_set.path_id.rptr()),\n                is_definition=True,\n\n                is_mutation=(\n                    is_mutation\n                    or (\n                        is_linkprop\n                        and s_ctx.view_rptr is not None\n                        and s_ctx.view_rptr.exprtype.is_mutation()\n                    )\n                ),\n            )\n            # XXX: We would maybe like to *not* do this when it\n            # already has a context, since for explain output that\n            # seems nicer, but this is what we want for producing\n            # actual error messages.\n            ptr_set.span = ptr_span\n\n        else:\n            # The set must be something pretty trivial, so just do it\n            ptr_set = setgen.extend_path(\n                ir_set,\n                ptrcls,\n                same_computable_scope=True,\n                span=ptr_span,\n                ctx=ctx,\n            )\n\n        assert irutils.is_set_instance(ptr_set, irast.Pointer)\n        set_shape.append((ptr_set, shape_op))\n\n    ir_set.shape = tuple(set_shape)\n\n    if (view_rptr is not None and view_rptr.ptrcls is not None and\n            view_scls != stype):\n        ctx.env.schema = view_scls.set_field_value(\n            ctx.env.schema, 'rptr', view_rptr.ptrcls)\n\n    return view_scls, ir_set\n\n\ndef _shape_el_ql_to_shape_el_desc(\n    shape_el: qlast.ShapeElement,\n    *,\n    source: s_sources.Source,\n    s_ctx: ShapeContext,\n    ctx: context.ContextLevel,\n) -> ShapeElementDesc:\n    \"\"\"Look at ShapeElement AST and annotate it for more convenient handing.\"\"\"\n\n    steps = shape_el.expr.steps\n    is_linkprop = False\n    is_polymorphic = False\n    plen = len(steps)\n    target_typexpr = None\n    source_intersection = []\n\n    if plen >= 2 and isinstance(steps[-1], qlast.TypeIntersection):\n        # Target type intersection: foo: Type\n        target_typexpr = steps[-1].type\n        plen -= 1\n        steps = steps[:-1]\n\n    if plen == 1:\n        # regular shape\n        lexpr = steps[0]\n        assert isinstance(lexpr, qlast.Ptr)\n        is_linkprop = lexpr.type == 'property'\n        if is_linkprop:\n            view_rptr = s_ctx.view_rptr\n            if view_rptr is None or view_rptr.ptrcls is None:\n                raise errors.QueryError(\n                    'invalid reference to link property '\n                    'in top level shape', span=lexpr.span)\n            assert isinstance(view_rptr.ptrcls, s_links.Link)\n            source = view_rptr.ptrcls\n    elif plen == 2 and isinstance(steps[0], qlast.TypeIntersection):\n        # Source type intersection: [IS Type].foo\n        source_intersection = [steps[0]]\n        lexpr = steps[1]\n        ptype = steps[0].type\n        source_spec = typegen.ql_typeexpr_to_type(ptype, ctx=ctx)\n        if not isinstance(source_spec, s_objtypes.ObjectType):\n            raise errors.QueryError(\n                f\"expected object type, got \"\n                f\"{source_spec.get_verbosename(ctx.env.schema)}\",\n                span=ptype.span,\n            )\n        source = source_spec\n        is_polymorphic = True\n    else:  # pragma: no cover\n        raise RuntimeError(\n            f'unexpected path length in view shape: {len(steps)}')\n\n    assert isinstance(lexpr, qlast.Ptr)\n    ptrname = lexpr.name\n\n    if target_typexpr is None:\n        path_ql = qlast.Path(\n            steps=[\n                *source_intersection,\n                lexpr,\n            ],\n            partial=True,\n            span=shape_el.span,\n        )\n    else:\n        path_ql = qlast.Path(\n            steps=[\n                *source_intersection,\n                lexpr,\n                qlast.TypeIntersection(type=target_typexpr),\n            ],\n            partial=True,\n            span=shape_el.span,\n        )\n\n    return ShapeElementDesc(\n        ql=shape_el,\n        path_ql=path_ql,\n        ptr_ql=lexpr,\n        ptr_name=ptrname,\n        source=source,\n        target_typexpr=target_typexpr,\n        is_polymorphic=is_polymorphic,\n        is_linkprop=is_linkprop,\n    )\n\n\ndef _expand_splat(\n    stype: s_objtypes.ObjectType,\n    *,\n    depth: int,\n    skip_ptrs: AbstractSet[str] = frozenset(),\n    skip_lprops: AbstractSet[str] = frozenset(),\n    rlink: Optional[s_links.Link] = None,\n    intersection: Optional[qlast.TypeIntersection] = None,\n    ctx: context.ContextLevel,\n) -> list[qlast.ShapeElement]:\n    \"\"\"Expand a splat (possibly recursively) into a list of ShapeElements\"\"\"\n    elements = []\n    pointers = stype.get_pointers(ctx.env.schema)\n    path: list[qlast.PathElement] = []\n    if intersection is not None:\n        path.append(intersection)\n    for ptr in pointers.objects(ctx.env.schema):\n        if ptr.get_secret(ctx.env.schema):\n            continue\n        splat_strat = ptr.get_splat_strategy(ctx.env.schema)\n        if (\n            splat_strat == qltypes.SplatStrategy.Explicit\n            or (\n                splat_strat == qltypes.SplatStrategy.Default\n                and (\n                    ptr.get_linkful(ctx.env.schema)\n                    if s_futures.future_enabled(\n                        ctx.env.schema, 'no_linkful_computed_splats'\n                    )\n                    else isinstance(ptr, s_links.Link)\n                )\n            )\n        ):\n            continue\n        sname = ptr.get_shortname(ctx.env.schema)\n        # Skip any dunder properties; these are injected properties like\n        # __tid__ and __tname__, and we want to manage injecting them\n        # ourselves, in the correct positions.\n        if (\n            (sname.name.startswith('__') and sname.name.endswith('__'))\n            or sname.name in skip_ptrs\n        ):\n            continue\n        step = qlast.Ptr(name=sname.name)\n        # Make sure not to overwrite the id property.\n        if not ptr.is_id_pointer(ctx.env.schema):\n            steps = path + [step]\n        else:\n            steps = [step]\n        elements.append(qlast.ShapeElement(\n            expr=qlast.Path(steps=steps),\n            origin=qlast.ShapeOrigin.SPLAT_EXPANSION,\n        ))\n\n    if rlink is not None:\n        for prop in rlink.get_pointers(ctx.env.schema).objects(ctx.env.schema):\n            if prop.is_endpoint_pointer(ctx.env.schema):\n                continue\n            assert isinstance(prop, s_props.Property), \\\n                \"non-property pointer on link?\"\n            sname = prop.get_shortname(ctx.env.schema)\n            if sname.name in skip_lprops:\n                continue\n            elements.append(\n                qlast.ShapeElement(\n                    expr=qlast.Path(\n                        steps=[qlast.Ptr(\n                            name=sname.name,\n                            type='property',\n                        )]\n                    ),\n                    origin=qlast.ShapeOrigin.SPLAT_EXPANSION,\n                )\n            )\n\n    if depth > 1:\n        for ptr in pointers.objects(ctx.env.schema):\n            if not isinstance(ptr, s_links.Link):\n                continue\n            if ptr.get_secret(ctx.env.schema):\n                continue\n            splat_strat = ptr.get_splat_strategy(ctx.env.schema)\n            if splat_strat == qltypes.SplatStrategy.Explicit:\n                continue\n            pn = ptr.get_shortname(ctx.env.schema)\n            if (\n                (pn.name.startswith('__') and pn.name.endswith('__'))\n                or pn.name in skip_ptrs\n            ):\n                continue\n            elements.append(\n                qlast.ShapeElement(\n                    expr=qlast.Path(steps=path + [qlast.Ptr(name=pn.name)]),\n                    elements=_expand_splat(\n                        ptr.get_target(ctx.env.schema),\n                        rlink=ptr,\n                        depth=depth - 1,\n                        ctx=ctx,\n                    ),\n                    origin=qlast.ShapeOrigin.SPLAT_EXPANSION,\n                )\n            )\n\n    return elements\n\n\ndef _gen_pointers_from_defaults(\n    specified_ptrs: set[sn.UnqualName],\n    view_scls: s_objtypes.ObjectType,\n    ir_set: irast.Set,\n    stype: s_objtypes.ObjectType,\n    s_ctx: ShapeContext,\n    ctx: context.ContextLevel,\n) -> dict[s_pointers.Pointer, EarlyShapePtr]:\n    path_id = ir_set.path_id\n    result: list[EarlyShapePtr] = []\n\n    if stype in ctx.active_defaults:\n        vn = stype.get_verbosename(ctx.env.schema)\n        raise errors.QueryError(\n            f\"default on property of {vn} is part of a default cycle\",\n        )\n\n    scls_pointers = stype.get_pointers(ctx.env.schema)\n    for pn, ptrcls in scls_pointers.items(ctx.env.schema):\n        if (\n            (pn in specified_ptrs or ptrcls.is_pure_computable(ctx.env.schema))\n            and not ptrcls.get_protected(ctx.env.schema)\n        ):\n            continue\n\n        default_expr: Optional[s_expr.Expression] = (\n            ptrcls.get_default(ctx.env.schema)\n        )\n        if not default_expr:\n            continue\n\n        ptrcls_sn = ptrcls.get_shortname(ctx.env.schema)\n        default_ql = qlast.ShapeElement(\n            expr=qlast.Path(\n                steps=[qlast.Ptr(name=ptrcls_sn.name)],\n            ),\n            compexpr=qlast.DetachedExpr(\n                expr=default_expr.parse(),\n                preserve_path_prefix=True,\n            ),\n            origin=qlast.ShapeOrigin.DEFAULT,\n        )\n        default_ql_desc = _shape_el_ql_to_shape_el_desc(\n            default_ql, source=view_scls, s_ctx=s_ctx, ctx=ctx\n        )\n\n        with ctx.new() as scopectx:\n            scopectx.active_defaults |= {stype}\n\n            # add __source__ to anchors\n            source_set = ir_set\n            scopectx.path_scope.attach_path(\n                source_set.path_id, span=None,\n                optional=False,\n                ctx=ctx,\n            )\n            scopectx.iterator_path_ids |= {source_set.path_id}\n            scopectx.anchors['__source__'] = source_set\n\n            pointer, ptr_set = _normalize_view_ptr_expr(\n                ir_set,\n                default_ql_desc,\n                view_scls,\n                path_id=path_id,\n                from_default=True,\n                s_ctx=s_ctx,\n                ctx=scopectx,\n            )\n\n            result.append(EarlyShapePtr(\n                pointer, ptr_set, qlast.ShapeOrigin.DEFAULT, None\n            ))\n\n    schema = ctx.env.schema\n\n    # Toposort defaults\n    # This is required because defaults may reference each other\n    # (and even contain cyclical dependencies).\n    # We cannot check or preprocess this at migration time, because some\n    # defaults may not be used for some inserts.\n    pointer_indexes = {}\n    for (index, (pointer, _, _, _)) in enumerate(result):\n        p = pointer.get_nearest_non_derived_parent(schema)\n        pointer_indexes[p.get_name(schema).name] = index\n    graph = {}\n    for (index, (_, irset, _, _)) in enumerate(result):\n        assert irset\n        dep_pointers = ast.find_children(irset, irast.Pointer)\n        dep_rptrs = (\n            # pointer.target_path_id.rptr() for pointer in dep_pointers\n            pointer.ptrref for pointer in dep_pointers\n            if pointer.source.typeref.id == stype.id\n        )\n        deps = {\n            pointer_indexes[rpts.name.name] for rpts in dep_rptrs\n            if rpts and rpts.name.name in pointer_indexes\n        }\n        graph[index] = topological.DepGraphEntry(\n            item=index, deps=deps, extra=False,\n        )\n\n    ordered = [\n        result[i] for i in topological.sort(graph, allow_unresolved=True)\n    ]\n\n    return {v.ptrcls: v for v in ordered}\n\n\ndef _raise_on_missing(\n    pointers: dict[s_pointers.Pointer, EarlyShapePtr],\n    stype: s_objtypes.ObjectType,\n    rewrites: Optional[irast.Rewrites],\n    ctx: context.ContextLevel,\n    span: Optional[parsing.Span],\n) -> None:\n    pointer_names = {\n        ptr.get_local_name(ctx.env.schema) for ptr in pointers\n    }\n\n    scls_pointers = stype.get_pointers(ctx.env.schema)\n    for pn, ptrcls in scls_pointers.items(ctx.env.schema):\n        if pn == sn.UnqualName(\"__type__\"):\n            continue\n\n        if pn in pointer_names or ptrcls.is_pure_computable(ctx.env.schema):\n            continue\n\n        if not ptrcls.get_required(ctx.env.schema):\n            continue\n\n        # is it rewritten?\n        if rewrites:\n            # (inserts must produce rewrites only for stype)\n            assert len(rewrites.by_type) == 1\n            if pn.name in next(iter(rewrites.by_type.values())):\n                continue\n\n        if ptrcls.is_property():\n            # If the target is a sequence, there's no need\n            # for an explicit value.\n            ptrcls_target = ptrcls.get_target(ctx.env.schema)\n            assert ptrcls_target is not None\n            if ptrcls_target.issubclass(\n                ctx.env.schema,\n                ctx.env.schema.get(\n                    \"std::sequence\", type=s_objects.SubclassableObject\n                ),\n            ):\n                continue\n\n        vn = ptrcls.get_verbosename(ctx.env.schema, with_parent=True)\n        msg = f\"missing value for required {vn}\"\n        # If this is happening in the context of DDL, report a\n        # QueryError because it is weird to report an ExecutionError\n        # (MissingRequiredError) when nothing is really executing.\n        if ctx.env.options.schema_object_context:\n            raise errors.SchemaDefinitionError(msg, span=span)\n        else:\n            raise errors.MissingRequiredError(msg, span=span)\n\n\n@dataclasses.dataclass(kw_only=True, repr=False, eq=False)\nclass RewriteContext:\n    specified_ptrs: set[sn.UnqualName]\n    kind: qltypes.RewriteKind\n\n    base_type: s_objtypes.ObjectType\n    shape_type: s_objtypes.ObjectType\n\n\ndef _compile_rewrites(\n    specified_ptrs: set[sn.UnqualName],\n    kind: qltypes.RewriteKind,\n    view_scls: s_objtypes.ObjectType,\n    ir_set: irast.Set,\n    stype: s_objtypes.ObjectType,\n    s_ctx: ShapeContext,\n    ctx: context.ContextLevel,\n) -> Optional[irast.Rewrites]:\n    # init\n    r_ctx = RewriteContext(\n        specified_ptrs=specified_ptrs,\n        kind=kind,\n        base_type=stype,\n        shape_type=view_scls,\n    )\n\n    # Computing anchors isn't cheap, so we want to only do it once,\n    # and only do it when it is necessary.\n    anchors: dict[s_objtypes.ObjectType, RewriteAnchors] = {}\n\n    def get_anchors(stype: s_objtypes.ObjectType) -> RewriteAnchors:\n        if stype not in anchors:\n            anchors[stype] = prepare_rewrite_anchors(\n                stype, ir_set.path_id, r_ctx, s_ctx, ctx)\n        return anchors[stype]\n\n    rewrites = _compile_rewrites_for_stype(\n        stype, kind, ir_set, get_anchors, s_ctx, ctx=ctx\n    )\n\n    if kind == qltypes.RewriteKind.Insert:\n        type_ref = typegen.type_to_typeref(stype, ctx.env)\n        rewrites_by_type = {type_ref: rewrites}\n\n    elif kind == qltypes.RewriteKind.Update:\n        # Update may also change objects that are children of stype\n        # Here we build a dict of rewrites for each descendent type for each\n        # of its pointers.\n\n        # This dict is stored in the context and pulled into the update\n        # statement later.\n\n        rewrites_by_type = _compile_rewrites_of_children(\n            stype, rewrites, kind, ir_set, get_anchors, s_ctx, ctx\n        )\n\n    else:\n        raise NotImplementedError()\n\n    schema = ctx.env.schema\n    by_type: dict[irast.TypeRef, irast.RewritesOfType] = {}\n    for ty, rewrites_of_type in rewrites_by_type.items():\n        ty = ty.real_material_type\n\n        by_type[ty] = {}\n        for element in rewrites_of_type.values():\n            target = element.target_set\n            assert target\n\n            ptrref = typegen.ptr_to_ptrref(element.ptrcls, ctx=ctx)\n            actual_ptrref = irtypeutils.find_actual_ptrref(ty, ptrref)\n            pn = actual_ptrref.shortname.name\n            path_id = irast.PathId.from_pointer(\n                schema, element.ptrcls, env=ctx.env\n            )\n\n            # construct a new set with correct path_id\n            ptr_set = setgen.new_set_from_set(\n                target,\n                path_id=path_id,\n                ctx=ctx,\n            )\n\n            # construct a new set with correct path_id\n            ptr_set.expr = irast.Pointer(\n                source=ir_set,\n                expr=ptr_set.expr,\n                direction=s_pointers.PointerDirection.Outbound,\n                ptrref=actual_ptrref,\n                is_definition=True,\n            )\n            assert irutils.is_set_instance(ptr_set, irast.Pointer)\n\n            by_type[ty][pn] = (ptr_set, ptrref.real_material_ptr)\n\n    anc = next(iter(anchors.values()), None)\n    if not anc:\n        return None\n\n    return irast.Rewrites(\n        old_path_id=anc.old_set.path_id if anc.old_set else None,\n        by_type=by_type,\n    )\n\n\ndef _compile_rewrites_of_children(\n    stype: s_objtypes.ObjectType,\n    parent_rewrites: dict[sn.UnqualName, EarlyShapePtr],\n    kind: qltypes.RewriteKind,\n    ir_set: irast.Set,\n    get_anchors: Callable[[s_objtypes.ObjectType], RewriteAnchors],\n    s_ctx: ShapeContext,\n    ctx: context.ContextLevel,\n) -> dict[irast.TypeRef, dict[sn.UnqualName, EarlyShapePtr]]:\n    rewrites_for_type: dict[\n        irast.TypeRef, dict[sn.UnqualName, EarlyShapePtr]\n    ] = {}\n\n    # save parent to result\n    type_ref = typegen.type_to_typeref(stype, ctx.env)\n    rewrites_for_type[type_ref] = parent_rewrites.copy()\n\n    for child in stype.children(ctx.env.schema):\n        if child.get_is_derived(ctx.env.schema):\n            continue\n\n        # base on parent rewrites\n        child_rewrites = parent_rewrites.copy()\n        # override with rewrites defined here\n        rewrites_defined_here = _compile_rewrites_for_stype(\n            child, kind, ir_set, get_anchors, s_ctx,\n            already_defined_rewrites=child_rewrites,\n            ctx=ctx\n        )\n        child_rewrites.update(rewrites_defined_here)\n\n        # recurse for children\n        rewrites_for_type.update(\n            _compile_rewrites_of_children(\n                child,\n                child_rewrites,\n                kind,\n                ir_set,\n                get_anchors,\n                s_ctx,\n                ctx=ctx,\n            )\n        )\n\n    return rewrites_for_type\n\n\ndef _compile_rewrites_for_stype(\n    stype: s_objtypes.ObjectType,\n    kind: qltypes.RewriteKind,\n    ir_set: irast.Set,\n    get_anchors: Callable[[s_objtypes.ObjectType], RewriteAnchors],\n    s_ctx: ShapeContext,\n    *,\n    already_defined_rewrites: Optional[\n        Mapping[sn.UnqualName, EarlyShapePtr]] = None,\n    ctx: context.ContextLevel,\n) -> dict[sn.UnqualName, EarlyShapePtr]:\n    schema = ctx.env.schema\n\n    path_id = ir_set.path_id\n\n    res = {}\n\n    if stype in ctx.active_rewrites:\n        vn = stype.get_verbosename(ctx.env.schema)\n        raise errors.QueryError(\n            f\"rewrite rule on {vn} is part of a rewrite rule cycle\",\n        )\n\n    scls_pointers = stype.get_pointers(schema)\n    for pn, ptrcls in scls_pointers.items(schema):\n        if ptrcls.is_pure_computable(schema):\n            continue\n\n        rewrite = ptrcls.get_rewrite(schema, kind)\n        if not rewrite:\n            continue\n        rewrite_pointer = downcast(\n            s_pointers.Pointer, rewrite.get_subject(schema))\n\n        # Because rewrites are not duplicated on inherited properties, the\n        # subject this pointer will not be on stype, but on one of its\n        # ancestors. Mitigation is to pick the correct pointer from the stype.\n        rewrite_pointer = downcast(\n            s_pointers.Pointer, stype.get_pointers(schema).get(schema, pn)\n        )\n\n        # get_rewrite searches in ancestors for rewrites, but if the rewrite\n        # for that ancestor has already been compiled, skip it to avoid\n        # duplicating work\n        if (\n            already_defined_rewrites\n            and (existing := already_defined_rewrites.get(pn))\n            and (existing[0].get_nearest_non_derived_parent(schema)\n                 == rewrite_pointer)\n        ):\n            continue\n\n        anchors = get_anchors(stype)\n\n        rewrite_expr: Optional[s_expr.Expression] = (\n            rewrite.get_expr(ctx.env.schema)\n        )\n        assert rewrite_expr\n\n        with ctx.newscope(fenced=True) as scopectx:\n            scopectx.active_rewrites |= {stype}\n\n            # prepare context\n            scopectx.partial_path_prefix = anchors.subject_set\n            nanchors = {}\n            nanchors[\"__specified__\"] = anchors.specified_set\n            nanchors[\"__subject__\"] = anchors.subject_set\n            if anchors.old_set:\n                nanchors[\"__old__\"] = anchors.old_set\n\n            for key, anchor in nanchors.items():\n                scopectx.path_scope.attach_path(\n                    anchor.path_id,\n                    optional=(anchor is anchors.subject_set),\n                    span=None,\n                    ctx=ctx,\n                )\n                scopectx.iterator_path_ids |= {anchor.path_id}\n                scopectx.anchors[key] = anchor\n\n            ctx.path_scope.factoring_allowlist.add(anchors.subject_set.path_id)\n\n            # prepare expression\n            ptrcls_sn = ptrcls.get_shortname(ctx.env.schema)\n            shape_ql = qlast.ShapeElement(\n                expr=qlast.Path(\n                    steps=[qlast.Ptr(name=ptrcls_sn.name)],\n                ),\n                compexpr=qlast.DetachedExpr(\n                    expr=rewrite_expr.parse(),\n                    preserve_path_prefix=True,\n                ),\n            )\n            shape_ql_desc = _shape_el_ql_to_shape_el_desc(\n                shape_ql,\n                source=anchors.rewrite_type,\n                s_ctx=s_ctx,\n                ctx=scopectx,\n            )\n\n            # compile as normal shape element\n            pointer, ptr_set = _normalize_view_ptr_expr(\n                anchors.subject_set,\n                shape_ql_desc,\n                anchors.rewrite_type,\n                path_id=path_id,\n                from_default=True,\n                s_ctx=s_ctx,\n                ctx=scopectx,\n            )\n            res[pn] = EarlyShapePtr(\n                pointer, ptr_set, qlast.ShapeOrigin.DEFAULT, None\n            )\n    return res\n\n\n@dataclasses.dataclass(kw_only=True, repr=False, eq=False)\nclass RewriteAnchors:\n    subject_set: irast.Set\n    specified_set: irast.Set\n    old_set: Optional[irast.Set]\n\n    rewrite_type: s_objtypes.ObjectType\n\n\ndef prepare_rewrite_anchors(\n    stype: s_objtypes.ObjectType,\n    subject_path_id: irast.PathId,\n    r_ctx: RewriteContext,\n    s_ctx: ShapeContext,\n    ctx: context.ContextLevel,\n) -> RewriteAnchors:\n    schema = ctx.env.schema\n\n    # init set for __subject__\n    subject_set = setgen.class_set(\n        stype, path_id=subject_path_id, ctx=ctx\n    )\n\n    # init reference to std::bool\n    bool_type = schema.get(\"std::bool\", type=s_types.Type)\n    bool_path = irast.PathId.from_type(\n        schema,\n        bool_type,\n        typename=sn.QualName(module=\"std\", name=\"bool\"),\n        env=ctx.env,\n    )\n\n    # init set for __specified__\n    specified_pointers: list[irast.TupleElement] = []\n    for pn, _ in stype.get_pointers(schema).items(schema):\n        pointer_path_id = irast.PathId.from_type(\n            schema,\n            bool_type,\n            typename=sn.QualName(\n                module=\"__derived__\", name=ctx.aliases.get(pn.name)\n            ),\n            namespace=ctx.path_id_namespace,\n            env=ctx.env,\n        )\n\n        specified_pointers.append(\n            irast.TupleElement(\n                name=pn.name,\n                val=setgen.ensure_set(\n                    irast.BooleanConstant(\n                        value=str(pn in r_ctx.specified_ptrs),\n                        typeref=bool_path.target,\n                    ),\n                    ctx=ctx\n                ),\n                path_id=pointer_path_id\n            )\n        )\n    specified_set = setgen.new_tuple_set(\n        specified_pointers, named=True, ctx=ctx\n    )\n\n    # init set for __old__\n    if r_ctx.kind == qltypes.RewriteKind.Update:\n        old_name = sn.QualName(\"__derived__\", ctx.aliases.get(\"__old__\"))\n        old_path_id = irast.PathId.from_type(\n            schema, stype, typename=old_name,\n            namespace=ctx.path_id_namespace, env=ctx.env,\n        )\n        old_set = setgen.new_set(\n            stype=stype, path_id=old_path_id, ctx=ctx,\n            expr=irast.TriggerAnchor(\n                typeref=typegen.type_to_typeref(stype, env=ctx.env)),\n        )\n    else:\n        old_set = None\n\n    rewrite_type = r_ctx.shape_type\n    if stype != r_ctx.shape_type.get_nearest_non_derived_parent(schema):\n        rewrite_type = downcast(\n            s_objtypes.ObjectType,\n            schemactx.derive_view(\n                stype,\n                exprtype=s_ctx.exprtype,\n                ctx=ctx,\n            )\n        )\n        subject_set = setgen.class_set(\n            rewrite_type, path_id=subject_set.path_id, ctx=ctx)\n        if old_set:\n            old_set = setgen.class_set(\n                rewrite_type, path_id=old_set.path_id, ctx=ctx)\n\n    return RewriteAnchors(\n        subject_set=subject_set,\n        specified_set=specified_set,\n        old_set=old_set,\n        rewrite_type=rewrite_type,\n    )\n\n\ndef _compile_qlexpr(\n    ir_source: irast.Set,\n    qlexpr: qlast.Base,\n    view_scls: s_objtypes.ObjectType,\n    *,\n    ptrcls: Optional[s_pointers.Pointer],\n    ptrsource: s_sources.Source,\n    ptr_name: sn.QualName,\n    is_linkprop: bool,\n    should_set_partial_prefix: bool,\n    s_ctx: ShapeContext,\n    ctx: context.ContextLevel,\n) -> tuple[irast.Set, context.ViewRPtr]:\n\n    with ctx.newscope(fenced=True) as shape_expr_ctx:\n        # Put current pointer class in context, so\n        # that references to link properties in sub-SELECT\n        # can be resolved.  This is necessary for proper\n        # evaluation of link properties on computable links,\n        # most importantly, in INSERT/UPDATE context.\n        shape_expr_ctx.view_rptr = context.ViewRPtr(\n            source=ptrsource if is_linkprop else view_scls,\n            ptrcls=ptrcls,\n            ptrcls_name=ptr_name,\n            ptrcls_is_linkprop=is_linkprop,\n            exprtype=s_ctx.exprtype,\n        )\n\n        shape_expr_ctx.defining_view = view_scls\n        shape_expr_ctx.path_scope.unnest_fence = True\n        source_set = setgen.fixup_computable_source_set(\n            ir_source, ctx=shape_expr_ctx\n        )\n\n        if should_set_partial_prefix:\n            shape_expr_ctx.partial_path_prefix = source_set\n\n        if ptrcls is not None:\n            if s_ctx.exprtype.is_mutation():\n                shape_expr_ctx.expr_exposed = context.Exposure.EXPOSED\n\n            shape_expr_ctx.empty_result_type_hint = \\\n                ptrcls.get_target(ctx.env.schema)\n\n        shape_expr_ctx.view_map = ctx.view_map.new_child()\n        setgen.update_view_map(\n            source_set.path_id, source_set, ctx=shape_expr_ctx)\n\n        irexpr = dispatch.compile(qlexpr, ctx=shape_expr_ctx)\n\n    if ctx.expr_exposed:\n        irexpr = eta_expand.eta_expand_ir(irexpr, ctx=ctx)\n\n    return irexpr, shape_expr_ctx.view_rptr\n\n\ndef _normalize_view_ptr_expr(\n    ir_source: irast.Set,\n    shape_el_desc: ShapeElementDesc,\n    view_scls: s_objtypes.ObjectType,\n    *,\n    path_id: irast.PathId,\n    from_default: bool = False,\n    pending_pointers: Mapping[s_pointers.Pointer, EarlyShapePtr] | None = None,\n    s_ctx: ShapeContext,\n    ctx: context.ContextLevel,\n) -> tuple[s_pointers.Pointer, Optional[irast.Set]]:\n    is_mutation = s_ctx.exprtype.is_insert() or s_ctx.exprtype.is_update()\n\n    materialized = None\n    qlexpr: Optional[qlast.Expr] = None\n    base_ptrcls_is_alias = False\n    irexpr = None\n\n    shape_el = shape_el_desc.ql\n    ptrsource = shape_el_desc.source\n    ptrname = shape_el_desc.ptr_name\n    is_linkprop = shape_el_desc.is_linkprop\n    is_polymorphic = shape_el_desc.is_polymorphic\n    target_typexpr = shape_el_desc.target_typexpr\n\n    is_independent_polymorphic = False\n\n    compexpr: Optional[qlast.Expr] = shape_el.compexpr\n    if compexpr is None and is_mutation:\n        raise errors.QueryError(\n            \"mutation queries must specify values with ':='\",\n            span=shape_el.expr.steps[-1].span,\n        )\n\n    ptrcls: Optional[s_pointers.Pointer]\n\n    if compexpr is None:\n        ptrcls = setgen.resolve_ptr(\n            ptrsource,\n            ptrname,\n            track_ref=shape_el_desc.ptr_ql,\n            ctx=ctx,\n            span=shape_el.span,\n        )\n        real_ptrcls = None\n        if is_polymorphic:\n            # For polymorphic pointers, we need to see if the *real*\n            # base class has the pointer, because if so we need to use\n            # that when doing cardinality inference (since it may need\n            # to raise an error, if it is required). If it isn't\n            # present on the real type, take note of that so that we\n            # suppress the inherited cardinality.\n            try:\n                real_ptrcls = setgen.resolve_ptr(\n                    view_scls,\n                    ptrname,\n                    track_ref=shape_el_desc.ptr_ql,\n                    ctx=ctx,\n                    span=shape_el.span,\n                )\n            except errors.InvalidReferenceError:\n                is_independent_polymorphic = True\n            ptrcls = schemactx.derive_ptr(ptrcls, view_scls, ctx=ctx)\n        real_ptrcls = real_ptrcls or ptrcls\n\n        base_ptrcls = real_ptrcls.get_bases(\n            ctx.env.schema).first(ctx.env.schema)\n        base_ptr_is_computable = base_ptrcls in ctx.env.source_map\n        ptr_name = sn.QualName(\n            module='__',\n            name=ptrcls.get_shortname(ctx.env.schema).name,\n        )\n\n        # Schema computables that point to opaque unions will just have\n        # BaseObject as their target, but in order to properly compile\n        # it, we need to know the actual type here, so we recompute it.\n        # XXX: This is a hack, though, and hopefully we can fix it once\n        # the computable/alias rework lands.\n        is_opaque_schema_computable = (\n            ptrcls.is_pure_computable(ctx.env.schema)\n            and (t := ptrcls.get_target(ctx.env.schema))\n            and t.get_name(ctx.env.schema) == sn.QualName('std', 'BaseObject')\n        )\n\n        base_required = base_ptrcls.get_required(ctx.env.schema)\n        base_cardinality = _get_base_ptr_cardinality(base_ptrcls, ctx=ctx)\n        base_is_singleton = False\n        if base_cardinality is not None and base_cardinality.is_known():\n            base_is_singleton = base_cardinality.is_single()\n\n        is_nontrivial = astutils.is_nontrivial_shape_element(shape_el)\n        is_obj = not_none(ptrcls.get_target(ctx.env.schema)).is_object_type()\n\n        if (\n            is_obj\n            or is_nontrivial\n            or shape_el.elements\n\n            or base_ptr_is_computable\n            or is_polymorphic\n            or target_typexpr is not None\n            or (ctx.implicit_limit and not base_is_singleton)\n            or is_opaque_schema_computable\n        ):\n            qlexpr = shape_el_desc.path_ql\n            if shape_el.elements:\n                qlexpr = qlast.Shape(expr=qlexpr, elements=shape_el.elements)\n\n            qlexpr = astutils.ensure_ql_query(qlexpr)\n            assert isinstance(qlexpr, qlast.SelectQuery)\n            qlexpr.where = shape_el.where\n            qlexpr.orderby = shape_el.orderby\n\n            if shape_el.offset or shape_el.limit:\n                qlexpr = qlast.SelectQuery(result=qlexpr, implicit=True)\n                qlexpr.offset = shape_el.offset\n                qlexpr.limit = shape_el.limit\n\n            if (\n                ctx.expr_exposed\n                and ctx.implicit_limit\n                and not base_is_singleton\n            ):\n                qlexpr = qlast.SelectQuery(result=qlexpr, implicit=True)\n                qlexpr.limit = qlast.Constant.integer(ctx.implicit_limit)\n\n        if target_typexpr is not None:\n            assert isinstance(target_typexpr, qlast.TypeName)\n            intersector_type = schemactx.get_schema_type(\n                target_typexpr.maintype, ctx=ctx)\n\n            int_result = schemactx.apply_intersection(\n                ptrcls.get_target(ctx.env.schema),  # type: ignore\n                intersector_type,\n                ctx=ctx,\n            )\n\n            ptr_target = int_result.stype\n        else:\n            _ptr_target = ptrcls.get_target(ctx.env.schema)\n            assert _ptr_target\n            ptr_target = _ptr_target\n\n        ptr_required = base_required\n        ptr_cardinality = base_cardinality\n        if shape_el.where or is_polymorphic:\n            # If the shape has a filter on it, we need to force a reinference\n            # of the cardinality, to produce an error if needed.\n            ptr_cardinality = None\n        if ptr_cardinality is None or not ptr_cardinality.is_known():\n            # We do not know the parent's pointer cardinality yet.\n            ctx.env.pointer_derivation_map[base_ptrcls].append(ptrcls)\n            ctx.env.pointer_specified_info[ptrcls] = (\n                shape_el.cardinality, shape_el.required, shape_el.span)\n\n        # If we generated qlexpr for the element, we process the\n        # subview by just compiling the qlexpr. This is so that we can\n        # figure out if it needs materialization and also so that\n        # `qlexpr is not None` always implies that we did the\n        # compilation.\n        if qlexpr:\n            irexpr, _ = _compile_qlexpr(\n                ir_source,\n                qlexpr,\n                view_scls,\n                ptrcls=ptrcls,\n                ptrsource=ptrsource,\n                ptr_name=ptr_name,\n                is_linkprop=is_linkprop,\n                should_set_partial_prefix=True,\n                s_ctx=s_ctx,\n                ctx=ctx,\n            )\n            materialized = setgen.should_materialize(\n                irexpr, ptrcls=ptrcls,\n                materialize_visible=True, skipped_bindings={path_id},\n                ctx=ctx)\n            ptr_target = setgen.get_set_type(irexpr, ctx=ctx)\n\n    # compexpr is not None\n    else:\n        base_ptrcls = ptrcls = None\n\n        if (is_mutation\n                and ptrname not in ctx.special_computables_in_mutation_shape):\n            # If this is a mutation, the pointer must exist.\n            ptrcls = setgen.resolve_ptr(\n                ptrsource, ptrname, track_ref=shape_el_desc.ptr_ql, ctx=ctx)\n            if ptrcls.is_pure_computable(ctx.env.schema) and not from_default:\n                ptr_vn = ptrcls.get_verbosename(ctx.env.schema,\n                                                with_parent=True)\n                raise errors.QueryError(\n                    f'modification of computed {ptr_vn} is prohibited',\n                    span=shape_el.span)\n\n            base_ptrcls = ptrcls.get_bases(\n                ctx.env.schema).first(ctx.env.schema)\n\n            ptr_name = sn.QualName(\n                module='__',\n                name=ptrcls.get_shortname(ctx.env.schema).name,\n            )\n\n        else:\n            ptr_name = sn.QualName(\n                module='__',\n                name=ptrname,\n            )\n\n            try:\n                is_linkprop_mutation = (\n                    is_linkprop\n                    and s_ctx.view_rptr is not None\n                    and s_ctx.view_rptr.exprtype.is_mutation()\n                )\n\n                ptrcls = setgen.resolve_ptr(\n                    ptrsource,\n                    ptrname,\n                    track_ref=(\n                        False if not is_linkprop_mutation\n                        else shape_el_desc.ptr_ql\n                    ),\n                    ctx=ctx,\n                )\n\n                base_ptrcls = ptrcls.get_bases(\n                    ctx.env.schema).first(ctx.env.schema)\n            except errors.InvalidReferenceError:\n                # Check if we aren't inside of modifying statement\n                # for link property, otherwise this is a NEW\n                # computable pointer, it's fine.\n                if is_linkprop_mutation:\n                    raise\n\n        qlexpr = astutils.ensure_ql_query(compexpr)\n        # HACK: For scope tree related reasons, DML inside of free objects\n        # needs to be wrapped in a SELECT. This is probably fixable.\n        if irutils.is_trivial_free_object(ir_source):\n            qlexpr = astutils.ensure_ql_select(qlexpr)\n\n        if (\n            ctx.expr_exposed\n            and ctx.implicit_limit\n        ):\n            qlexpr = qlast.SelectQuery(result=qlexpr, implicit=True)\n            qlexpr.limit = qlast.Constant.integer(ctx.implicit_limit)\n\n        irexpr, sub_view_rptr = _compile_qlexpr(\n            ir_source,\n            qlexpr,\n            view_scls,\n            ptrcls=ptrcls,\n            ptrsource=ptrsource,\n            ptr_name=ptr_name,\n            is_linkprop=is_linkprop,\n            # do not set partial path prefix if in the insert\n            # shape but not in defaults\n            should_set_partial_prefix=(\n                not s_ctx.exprtype.is_insert() or from_default),\n            s_ctx=s_ctx,\n            ctx=ctx,\n        )\n        materialized = setgen.should_materialize(\n            irexpr, ptrcls=ptrcls,\n            materialize_visible=True, skipped_bindings={path_id},\n            ctx=ctx)\n        ptr_target = setgen.get_set_type(irexpr, ctx=ctx)\n\n        if (\n            shape_el.operation.op is qlast.ShapeOp.APPEND\n            or shape_el.operation.op is qlast.ShapeOp.SUBTRACT\n        ):\n            if not s_ctx.exprtype.is_update():\n                op = (\n                    '+=' if shape_el.operation.op is qlast.ShapeOp.APPEND\n                    else '-='\n                )\n                raise errors.EdgeQLSyntaxError(\n                    f\"unexpected '{op}'\",\n                    span=shape_el.operation.span,\n                )\n\n        irexpr.span = compexpr.span\n\n        is_inbound_alias = False\n        if base_ptrcls is None:\n            base_ptrcls = sub_view_rptr.base_ptrcls\n            base_ptrcls_is_alias = sub_view_rptr.ptrcls_is_alias\n            is_inbound_alias = (\n                sub_view_rptr.rptr_dir is s_pointers.PointerDirection.Inbound)\n\n        if ptrcls is not None:\n            ctx.env.schema = ptrcls.set_field_value(\n                ctx.env.schema, 'owned', True)\n\n        ptr_cardinality = None\n        ptr_required = False\n\n        _record_created_collection_types(ptr_target, ctx)\n\n        generic_type = ptr_target.find_generic(ctx.env.schema)\n        if generic_type is not None:\n            raise errors.QueryError(\n                'expression returns value of indeterminate type',\n                span=ctx.env.type_origins.get(generic_type),\n            )\n\n        # Validate that the insert/update expression is\n        # of the correct class.\n        if is_mutation and ptrcls is not None:\n            base_target = ptrcls.get_target(ctx.env.schema)\n            assert base_target is not None\n            if ptr_target.assignment_castable_to(\n                    base_target,\n                    schema=ctx.env.schema):\n                # Force assignment casts if the target type is not a\n                # subclass of the base type and the cast is not to an\n                # object type.\n                if not (\n                    base_target.is_object_type()\n                    or s_types.is_type_compatible(\n                        base_target, ptr_target, schema=ctx.env.schema\n                    )\n                ):\n                    qlexpr = astutils.ensure_ql_query(\n                        qlast.TypeCast(\n                            type=typegen.type_to_ql_typeref(\n                                base_target, ctx=ctx\n                            ),\n                            expr=compexpr,\n                        )\n                    )\n                    ptr_target = base_target\n                    # We also need to compile the cast to IR.\n                    with ctx.new() as subctx:\n                        subctx.anchors = subctx.anchors.copy()\n                        source_path = subctx.create_anchor(irexpr, 'a')\n                        cast_qlexpr = astutils.ensure_ql_query(\n                            qlast.TypeCast(\n                                type=typegen.type_to_ql_typeref(\n                                    base_target, ctx=ctx\n                                ),\n                                expr=source_path,\n                            )\n                        )\n\n                        # HACK: This is mad dodgy. Hide the Pointer\n                        # when compiling.\n                        old_expr = irexpr.expr\n                        if isinstance(old_expr, irast.Pointer):\n                            assert old_expr.expr\n                            irexpr.expr = old_expr.expr\n                        irexpr = dispatch.compile(cast_qlexpr, ctx=subctx)\n                        if isinstance(old_expr, irast.Pointer):\n                            old_expr.expr = irexpr.expr\n                            irexpr.expr = old_expr\n\n            else:\n                expected = [\n                    repr(str(base_target.get_displayname(ctx.env.schema)))\n                ]\n\n                ercls: type[errors.EdgeDBError]\n                if ptrcls.is_property():\n                    ercls = errors.InvalidPropertyTargetError\n                else:\n                    ercls = errors.InvalidLinkTargetError\n\n                ptr_vn = ptrcls.get_verbosename(ctx.env.schema,\n                                                with_parent=True)\n\n                raise ercls(\n                    f'invalid target for {ptr_vn}: '\n                    f'{str(ptr_target.get_displayname(ctx.env.schema))!r} '\n                    f'(expecting {\" or \".join(expected)})'\n                )\n\n    # Prohibit update of readonly\n    if (\n        s_ctx.exprtype.is_update()\n        and ptrcls\n        and ptrcls.get_readonly(ctx.env.schema)\n    ):\n        raise errors.QueryError(\n            f'cannot update {ptrcls.get_verbosename(ctx.env.schema)}: '\n            f'it is declared as read-only',\n            span=compexpr.span if compexpr else None,\n        )\n\n    if (\n        s_ctx.exprtype.is_mutation()\n        and ptrcls\n        and ptrcls.get_protected(ctx.env.schema)\n        and not from_default\n    ):\n        # 4.0 shipped with a bug where dumps included protected fields\n        # in config values, so we need to suppress the error in that\n        # case.  Default value injection is set up to *always* inject\n        # on protected pointers.\n        if ctx.env.options.dump_restore_mode:\n            return ptrcls, None\n        raise errors.QueryError(\n            f'cannot assign to {ptrcls.get_verbosename(ctx.env.schema)}: '\n            f'it is protected',\n            span=compexpr.span if compexpr else None,\n        )\n\n    # Prohibit invalid operations on id\n    id_access = (\n        ptrcls\n        and ptrcls.is_id_pointer(ctx.env.schema)\n        and (\n            not ctx.env.options.allow_user_specified_id\n            or not s_ctx.exprtype.is_mutation()\n        )\n    )\n    if (\n        (compexpr is not None or is_polymorphic)\n        and id_access and not from_default and ptrcls\n    ):\n        vn = ptrcls.get_verbosename(ctx.env.schema)\n        if is_polymorphic:\n            msg = (f'cannot access {vn} on a polymorphic '\n                   f'shape element')\n        else:\n            msg = f'cannot assign to {vn}'\n        if (\n            not ctx.env.options.allow_user_specified_id\n            and s_ctx.exprtype.is_mutation()\n        ):\n            hint = (\n                'consider enabling the \"allow_user_specified_id\" '\n                'configuration parameter to allow setting custom object ids'\n            )\n        else:\n            hint = None\n\n        raise errors.QueryError(msg, span=shape_el.span, hint=hint)\n\n    # Common code for computed/not computed\n\n    if (\n        pending_pointers is not None and ptrcls is not None\n        and (prev := pending_pointers.get(ptrcls)) is not None\n        and prev.shape_origin is not qlast.ShapeOrigin.SPLAT_EXPANSION\n    ):\n        vnp = ptrcls.get_verbosename(ctx.env.schema, with_parent=True)\n        raise errors.QueryError(\n            f'duplicate definition of {vnp}',\n            span=shape_el.span)\n\n    if qlexpr is not None or ptrcls is None:\n        src_scls: s_sources.Source\n\n        if is_linkprop:\n            # Proper checking was done when is_linkprop is defined.\n            assert s_ctx.view_rptr is not None\n            assert isinstance(s_ctx.view_rptr.ptrcls, s_links.Link)\n            src_scls = s_ctx.view_rptr.ptrcls\n        else:\n            src_scls = view_scls\n\n        if ptr_target.is_object_type():\n            base = ctx.env.get_schema_object_and_track(\n                sn.QualName('std', 'link'), expr=None)\n        else:\n            base = ctx.env.get_schema_object_and_track(\n                sn.QualName('std', 'property'), expr=None)\n\n        if base_ptrcls is not None:\n            derive_from = base_ptrcls\n        else:\n            derive_from = base\n\n        derived_name = schemactx.derive_view_name(\n            base_ptrcls,\n            derived_name_base=ptr_name,\n            derived_name_quals=[str(src_scls.get_name(ctx.env.schema))],\n            ctx=ctx,\n        )\n\n        existing = ctx.env.schema.get(\n            derived_name, default=None, type=s_pointers.Pointer)\n        if existing is not None:\n            existing_target = existing.get_target(ctx.env.schema)\n            assert existing_target is not None\n            if ctx.recompiling_schema_alias:\n                ptr_cardinality = existing.get_cardinality(ctx.env.schema)\n                ptr_required = existing.get_required(ctx.env.schema)\n            if ptr_target == existing_target:\n                ptrcls = existing\n            elif ptr_target.implicitly_castable_to(\n                    existing_target, ctx.env.schema):\n\n                ctx.env.ptr_ref_cache.pop(existing, None)\n                ctx.env.schema = existing.set_target(\n                    ctx.env.schema, ptr_target)\n                ptrcls = existing\n            else:\n                vnp = existing.get_verbosename(\n                    ctx.env.schema, with_parent=True)\n\n                t1_vn = existing_target.get_verbosename(ctx.env.schema)\n                t2_vn = ptr_target.get_verbosename(ctx.env.schema)\n\n                if compexpr is not None:\n                    span = compexpr.span\n                else:\n                    span = shape_el.expr.steps[-1].span\n                raise errors.SchemaError(\n                    f'cannot redefine {vnp} as {t2_vn}',\n                    details=f'{vnp} is defined as {t1_vn}',\n                    span=span,\n                )\n        else:\n            ptrcls = schemactx.derive_ptr(\n                derive_from, src_scls, ptr_target,\n                derive_backlink=is_inbound_alias,\n                derived_name=derived_name,\n                ctx=ctx)\n\n    elif ptrcls.get_target(ctx.env.schema) != ptr_target:\n        ctx.env.ptr_ref_cache.pop(ptrcls, None)\n        ctx.env.schema = ptrcls.set_target(ctx.env.schema, ptr_target)\n\n    assert ptrcls is not None\n\n    if materialized and not is_mutation and ctx.qlstmt:\n        assert ptrcls not in ctx.env.materialized_sets\n        ctx.env.materialized_sets[ptrcls] = ctx.qlstmt, materialized\n\n        if irexpr:\n            setgen.maybe_materialize(ptrcls, irexpr, ctx=ctx)\n\n    if qlexpr is not None:\n        ctx.env.schema = ptrcls.set_field_value(\n            ctx.env.schema, 'defined_here', True\n        )\n\n    if qlexpr is not None:\n        ctx.env.source_map[ptrcls] = irast.ComputableInfo(\n            qlexpr=qlexpr,\n            irexpr=irexpr,\n            context=ctx,\n            path_id=path_id,\n            path_id_ns=s_ctx.path_id_namespace,\n            shape_op=shape_el.operation.op,\n            should_materialize=materialized or [],\n        )\n\n    if compexpr is not None or is_polymorphic or materialized:\n        if (old_ptrref := ctx.env.ptr_ref_cache.get(ptrcls)):\n            old_ptrref.is_computable = True\n\n        ctx.env.schema = ptrcls.set_field_value(\n            ctx.env.schema,\n            'computable',\n            True,\n        )\n\n        ctx.env.schema = ptrcls.set_field_value(\n            ctx.env.schema,\n            'owned',\n            True,\n        )\n\n    if ptr_cardinality is not None:\n        ctx.env.schema = ptrcls.set_field_value(\n            ctx.env.schema, 'cardinality', ptr_cardinality)\n        ctx.env.schema = ptrcls.set_field_value(\n            ctx.env.schema, 'required', ptr_required)\n    else:\n        if qlexpr is None and ptrcls is not base_ptrcls:\n            ctx.env.pointer_derivation_map[base_ptrcls].append(ptrcls)\n\n        base_cardinality = None\n        base_required = None\n        if (\n            base_ptrcls is not None\n            and not base_ptrcls_is_alias\n            and not is_independent_polymorphic\n        ):\n            base_cardinality = _get_base_ptr_cardinality(base_ptrcls, ctx=ctx)\n            base_required = base_ptrcls.get_required(ctx.env.schema)\n\n        if base_cardinality is None or not base_cardinality.is_known():\n            # If the base cardinality is not known the we can't make\n            # any checks here and will rely on validation in the\n            # cardinality inferer.\n            specified_cardinality = shape_el.cardinality\n            specified_required = shape_el.required\n        else:\n            specified_cardinality = base_cardinality\n\n            # Inferred optionality overrides that of the base pointer\n            # if base pointer is not `required`, hence the is True check.\n            if shape_el.required is not None:\n                specified_required = shape_el.required\n            elif base_required is True:\n                specified_required = base_required\n            else:\n                specified_required = None\n\n            if (\n                shape_el.cardinality is not None\n                and base_ptrcls is not None\n                and shape_el.cardinality != base_cardinality\n            ):\n                base_src = base_ptrcls.get_source(ctx.env.schema)\n                assert base_src is not None\n                base_src_name = base_src.get_verbosename(ctx.env.schema)\n                raise errors.SchemaError(\n                    f'cannot redefine the cardinality of '\n                    f'{ptrcls.get_verbosename(ctx.env.schema)}: '\n                    f'it is defined as {base_cardinality.as_ptr_qual()!r} '\n                    f'in the base {base_src_name}',\n                    span=compexpr.span if compexpr else None,\n                )\n\n            if (\n                shape_el.required is False\n                and base_ptrcls is not None\n                and base_required\n            ):\n                base_src = base_ptrcls.get_source(ctx.env.schema)\n                assert base_src is not None\n                base_src_name = base_src.get_verbosename(ctx.env.schema)\n                raise errors.SchemaError(\n                    f'cannot redefine '\n                    f'{ptrcls.get_verbosename(ctx.env.schema)} '\n                    f'as optional: it is defined as required '\n                    f'in the base {base_src_name}',\n                    span=compexpr.span if compexpr else None,\n                )\n\n        ctx.env.pointer_specified_info[ptrcls] = (\n            specified_cardinality, specified_required, shape_el.span)\n\n        ctx.env.schema = ptrcls.set_field_value(\n            ctx.env.schema, 'cardinality', qltypes.SchemaCardinality.Unknown)\n\n    if irexpr and not irexpr.span:\n        irexpr.span = shape_el.span\n\n    return ptrcls, irexpr\n\n\ndef derive_ptrcls(\n    view_rptr: context.ViewRPtr,\n    *,\n    target_scls: s_types.Type,\n    ctx: context.ContextLevel\n) -> s_pointers.Pointer:\n\n    if view_rptr.ptrcls is None:\n        if view_rptr.base_ptrcls is None:\n            if target_scls.is_object_type():\n                base = ctx.env.get_schema_object_and_track(\n                    sn.QualName('std', 'link'), expr=None)\n                view_rptr.base_ptrcls = cast(s_links.Link, base)\n            else:\n                base = ctx.env.get_schema_object_and_track(\n                    sn.QualName('std', 'property'), expr=None)\n                view_rptr.base_ptrcls = cast(s_props.Property, base)\n\n        derived_name = schemactx.derive_view_name(\n            view_rptr.base_ptrcls,\n            derived_name_base=view_rptr.ptrcls_name,\n            derived_name_quals=(\n                str(view_rptr.source.get_name(ctx.env.schema)),\n            ),\n            ctx=ctx)\n\n        is_inbound_alias = (\n            view_rptr.rptr_dir is s_pointers.PointerDirection.Inbound)\n        view_rptr.ptrcls = schemactx.derive_ptr(\n            view_rptr.base_ptrcls, view_rptr.source, target_scls,\n            derived_name=derived_name,\n            derive_backlink=is_inbound_alias,\n            ctx=ctx\n        )\n\n    else:\n        view_rptr.ptrcls = schemactx.derive_ptr(\n            view_rptr.ptrcls, view_rptr.source, target_scls,\n            derived_name_quals=(\n                str(view_rptr.source.get_name(ctx.env.schema)),\n            ),\n            ctx=ctx\n        )\n\n    return view_rptr.ptrcls\n\n\ndef _link_has_shape(\n    ptrcls: s_pointers.PointerLike, *, ctx: context.ContextLevel\n) -> bool:\n    if not isinstance(ptrcls, s_links.Link):\n        return False\n\n    ptr_shape = {p for p, _ in ctx.env.view_shapes[ptrcls]}\n    for p in ptrcls.get_pointers(ctx.env.schema).objects(ctx.env.schema):\n        if p.is_special_pointer(ctx.env.schema) or p not in ptr_shape:\n            continue\n        else:\n            return True\n\n    return False\n\n\ndef _get_base_ptr_cardinality(\n    ptrcls: s_pointers.Pointer,\n    *,\n    ctx: context.ContextLevel,\n) -> Optional[qltypes.SchemaCardinality]:\n    ptr_name = ptrcls.get_name(ctx.env.schema)\n    if ptr_name in {\n        sn.QualName('std', 'link'),\n        sn.QualName('std', 'property')\n    }:\n        return None\n    else:\n        return ptrcls.get_cardinality(ctx.env.schema)\n\n\ndef has_implicit_tid(\n    stype: s_types.Type, *, is_mutation: bool, ctx: context.ContextLevel\n) -> bool:\n\n    return (\n        stype.is_object_type()\n        and not stype.is_free_object_type(ctx.env.schema)\n        and not is_mutation\n        and ctx.implicit_tid_in_shapes\n    )\n\n\ndef has_implicit_tname(\n    stype: s_types.Type, *, is_mutation: bool, ctx: context.ContextLevel\n) -> bool:\n\n    return (\n        stype.is_object_type()\n        and not stype.is_free_object_type(ctx.env.schema)\n        and not is_mutation\n        and ctx.implicit_tname_in_shapes\n    )\n\n\ndef has_implicit_type_computables(\n    stype: s_types.Type, *, is_mutation: bool, ctx: context.ContextLevel\n) -> bool:\n\n    return (\n        has_implicit_tid(stype, is_mutation=is_mutation, ctx=ctx)\n        or has_implicit_tname(stype, is_mutation=is_mutation, ctx=ctx)\n    )\n\n\ndef _inline_type_computable(\n    ir_set: irast.Set,\n    stype: s_objtypes.ObjectType,\n    compname: str,\n    propname: str,\n    *,\n    shape_ptrs: list[ShapePtr],\n    ctx: context.ContextLevel,\n) -> None:\n    assert isinstance(stype, s_objtypes.ObjectType)\n    # Injecting into non-view objects /almost/ works, but it fails if the\n    # object is in the std library, and is dodgy always.\n    # Prevent it in general to find bugs faster.\n    assert stype.is_view(ctx.env.schema)\n\n    ptr: Optional[s_pointers.Pointer]\n    try:\n        ptr = setgen.resolve_ptr(stype, compname, track_ref=False, ctx=ctx)\n        # The pointer might exist on the base type. That doesn't count,\n        # and we need to re-inject it.\n        if ptr not in ctx.env.source_map:\n            ptr = None\n    except errors.InvalidReferenceError:\n        ptr = None\n\n    ptr_set = None\n    if ptr is None:\n        ql = qlast.ShapeElement(\n            required=True,\n            expr=qlast.Path(\n                steps=[qlast.Ptr(\n                    name=compname,\n                    direction=s_pointers.PointerDirection.Outbound,\n                )],\n            ),\n            compexpr=qlast.Path(\n                steps=[\n                    qlast.SpecialAnchor(name='__source__'),\n                    qlast.Ptr(\n                        name='__type__',\n                        direction=s_pointers.PointerDirection.Outbound,\n                    ),\n                    qlast.Ptr(\n                        name=propname,\n                        direction=s_pointers.PointerDirection.Outbound,\n                    )\n                ]\n            )\n        )\n        ql_desc = _shape_el_ql_to_shape_el_desc(\n            ql, source=stype, s_ctx=ShapeContext(), ctx=ctx\n        )\n\n        with ctx.new() as scopectx:\n            scopectx.anchors = scopectx.anchors.copy()\n            # Use the actual base type as the root of the injection, so that\n            # if a user has overridden `__type__` in a computable,\n            # we see through that.\n            base_stype = stype.get_nearest_non_derived_parent(ctx.env.schema)\n            base_ir_set = setgen.ensure_set(\n                ir_set, type_override=base_stype, ctx=scopectx)\n\n            scopectx.anchors['__source__'] = base_ir_set\n            ptr, ptr_set = _normalize_view_ptr_expr(\n                base_ir_set,\n                ql_desc,\n                stype,\n                path_id=ir_set.path_id,\n                s_ctx=ShapeContext(),\n                ctx=scopectx\n            )\n\n    # even if the pointer was not created here, or was already present in\n    # the shape, we set defined_here, so it is not inlined in `extend_path`.\n    ctx.env.schema = ptr.set_field_value(\n        ctx.env.schema, 'defined_here', True\n    )\n\n    view_shape = ctx.env.view_shapes[stype]\n    view_shape_ptrs = {p for p, _ in view_shape}\n    if ptr not in view_shape_ptrs:\n        if ptr not in ctx.env.pointer_specified_info:\n            ctx.env.pointer_specified_info[ptr] = (None, None, None)\n        view_shape.insert(0, (ptr, qlast.ShapeOp.ASSIGN))\n        shape_ptrs.insert(\n            0, ShapePtr(ir_set, ptr, qlast.ShapeOp.ASSIGN, ptr_set, None)\n        )\n\n\ndef _get_shape_configuration_inner(\n    ir_set: irast.Set,\n    shape_ptrs: list[ShapePtr],\n    stype: s_types.Type,\n    *,\n    parent_view_type: Optional[s_types.ExprType]=None,\n    ctx: context.ContextLevel\n) -> None:\n    is_objtype = ir_set.path_id.is_objtype_path()\n    all_materialize = all(\n        op == qlast.ShapeOp.MATERIALIZE for _, _, op, _, _ in shape_ptrs\n    )\n\n    if is_objtype:\n        assert isinstance(stype, s_objtypes.ObjectType)\n\n        view_type = stype.get_expr_type(ctx.env.schema)\n        is_mutation = view_type in (s_types.ExprType.Insert,\n                                    s_types.ExprType.Update)\n        is_parent_update = parent_view_type is s_types.ExprType.Update\n\n        implicit_id = (\n            # shape is not specified at all\n            not shape_ptrs\n            # implicit ids are always wanted\n            or (ctx.implicit_id_in_shapes and not is_mutation)\n            # we are inside an UPDATE shape and this is\n            # an explicit expression (link target update)\n            or (is_parent_update and irutils.sub_expr(ir_set) is not None)\n            or all_materialize\n        )\n        # We actually *always* inject an implicit id, but it's just\n        # there in case materialization needs it, in many cases.\n        implicit_op = qlast.ShapeOp.ASSIGN\n        if not implicit_id:\n            implicit_op = qlast.ShapeOp.MATERIALIZE\n\n        # We want the id in this shape and it's not already there,\n        # so insert it in the first position.\n        pointers = stype.get_pointers(ctx.env.schema).objects(\n            ctx.env.schema)\n        view_shape = ctx.env.view_shapes[stype]\n        view_shape_ptrs = {p for p, _ in view_shape}\n        for ptr in pointers:\n            if ptr.is_id_pointer(ctx.env.schema):\n                if ptr not in view_shape_ptrs:\n                    shape_metadata = ctx.env.view_shapes_metadata[stype]\n                    view_shape.insert(0, (ptr, implicit_op))\n                    shape_metadata.has_implicit_id = True\n                    shape_ptrs.insert(\n                        0, ShapePtr(ir_set, ptr, implicit_op, None, None)\n                    )\n                break\n\n    is_mutation = parent_view_type in {\n        s_types.ExprType.Insert,\n        s_types.ExprType.Update\n    }\n\n    if (\n        stype is not None\n        and has_implicit_tid(stype, is_mutation=is_mutation, ctx=ctx)\n    ):\n        # HACK: Make sure set is here first, to avoid potential\n        # warn_old_scoping warnings.\n        pathctx.register_set_in_scope(ir_set, ctx=ctx)\n        assert isinstance(stype, s_objtypes.ObjectType)\n        _inline_type_computable(\n            ir_set, stype, '__tid__', 'id', ctx=ctx, shape_ptrs=shape_ptrs)\n\n    if (\n        stype is not None\n        and has_implicit_tname(stype, is_mutation=is_mutation, ctx=ctx)\n    ):\n        # HACK: Make sure set is here first, to avoid potential\n        # warn_old_scoping warnings.\n        pathctx.register_set_in_scope(ir_set, ctx=ctx)\n        assert isinstance(stype, s_objtypes.ObjectType)\n        _inline_type_computable(\n            ir_set, stype, '__tname__', 'name', ctx=ctx, shape_ptrs=shape_ptrs)\n\n\ndef _get_early_shape_configuration(\n    ir_set: irast.Set,\n    in_shape_ptrs: list[ShapePtr],\n    *,\n    rptrcls: Optional[s_pointers.Pointer],\n    parent_view_type: Optional[s_types.ExprType]=None,\n    ctx: context.ContextLevel\n) -> list[ShapePtr]:\n    \"\"\"Return a list of (source_set, ptrcls) pairs as a shape for a given set.\n    \"\"\"\n\n    stype = setgen.get_set_type(ir_set, ctx=ctx)\n\n    # HACK: For some reason, all the link properties need to go last or\n    # things choke in native output mode?\n    shape_ptrs = sorted(\n        in_shape_ptrs,\n        key=lambda arg: arg.ptrcls.is_link_property(ctx.env.schema),\n    )\n\n    _get_shape_configuration_inner(\n        ir_set, shape_ptrs, stype, parent_view_type=parent_view_type, ctx=ctx)\n\n    return shape_ptrs\n\n\ndef _get_late_shape_configuration(\n    ir_set: irast.Set,\n    *,\n    rptr: Optional[irast.Pointer]=None,\n    parent_view_type: Optional[s_types.ExprType]=None,\n    ctx: context.ContextLevel\n) -> list[ShapePtr]:\n\n    \"\"\"Return a list of (source_set, ptrcls) pairs as a shape for a given set.\n    \"\"\"\n\n    stype = setgen.get_set_type(ir_set, ctx=ctx)\n\n    sources: list[s_types.Type | s_pointers.PointerLike] = []\n    link_view = False\n    is_objtype = ir_set.path_id.is_objtype_path()\n\n    if rptr is None:\n        if isinstance(ir_set.expr, irast.Pointer):\n            rptr = ir_set.expr\n    elif ir_set.expr and not isinstance(ir_set.expr, irast.Pointer):\n        # If we have a specified rptr but set is not a pointer itself,\n        # construct a version of the set that is pointer so it can be used\n        # as the path tip for applying pointers. This ensures that\n        # we can find link properties on late shapes.\n        ir_set = setgen.new_set_from_set(\n            ir_set, expr=rptr.replace(expr=ir_set.expr, is_phony=True), ctx=ctx\n        )\n\n    rptrcls: Optional[s_pointers.PointerLike]\n    if rptr is not None:\n        rptrcls = typegen.ptrcls_from_ptrref(rptr.ptrref, ctx=ctx)\n    else:\n        rptrcls = None\n\n    link_view = (\n        rptrcls is not None and\n        not rptrcls.is_link_property(ctx.env.schema) and\n        _link_has_shape(rptrcls, ctx=ctx)\n    )\n\n    if is_objtype or not link_view:\n        sources.append(stype)\n\n    if link_view:\n        assert rptrcls is not None\n        sources.append(rptrcls)\n\n    shape_ptrs: list[ShapePtr] = []\n\n    for source in sources:\n        for ptr, shape_op in ctx.env.view_shapes[source]:\n            shape_ptrs.append(ShapePtr(ir_set, ptr, shape_op, None, None)\n        )\n\n    _get_shape_configuration_inner(\n        ir_set, shape_ptrs, stype, parent_view_type=parent_view_type, ctx=ctx)\n\n    return shape_ptrs\n\n\n@functools.singledispatch\ndef late_compile_view_shapes(\n    expr: irast.Base,\n    *,\n    rptr: Optional[irast.Pointer] = None,\n    parent_view_type: Optional[s_types.ExprType] = None,\n    ctx: context.ContextLevel,\n) -> None:\n    \"\"\"Do a late insertion of any unprocessed shapes.\n\n    We mainly compile shapes in process_view, but late_compile_view_shapes\n    is responsible for compiling implicit exposed shapes (containing\n    only id) and in cases like accessing a semi-joined shape.\n\n    \"\"\"\n    pass\n\n\n@late_compile_view_shapes.register(irast.Set)\ndef _late_compile_view_shapes_in_set(\n        ir_set: irast.Set, *,\n        rptr: Optional[irast.Pointer] = None,\n        parent_view_type: Optional[s_types.ExprType] = None,\n        ctx: context.ContextLevel) -> None:\n\n    shape_ptrs = _get_late_shape_configuration(\n        ir_set, rptr=rptr, parent_view_type=parent_view_type, ctx=ctx)\n\n    # We want to push down the shape to better correspond with where it\n    # appears in the query (rather than lifting it up to the first\n    # place the view_type appears---this is a little hacky, because\n    # letting it be lifted up is the natural thing with our view type-driven\n    # shape compilation).\n    #\n    # This is to avoid losing subquery distinctions (in cases\n    # like test_edgeql_scope_tuple_15), and generally seems more natural.\n    is_definition_or_not_pointer = (\n        not isinstance(ir_set.expr, irast.Pointer) or ir_set.expr.is_definition\n    )\n    expr = irutils.sub_expr(ir_set)\n    if (\n        isinstance(expr, (irast.SelectStmt, irast.GroupStmt))\n        and is_definition_or_not_pointer\n        and (setgen.get_set_type(ir_set, ctx=ctx) ==\n             setgen.get_set_type(expr.result, ctx=ctx))\n    ):\n        child = expr.result\n        set_scope = pathctx.get_set_scope(ir_set, ctx=ctx)\n\n        if shape_ptrs:\n            pathctx.register_set_in_scope(ir_set, ctx=ctx)\n        with ctx.new() as scopectx:\n            if set_scope is not None:\n                scopectx.path_scope = set_scope\n\n            if not rptr and isinstance(ir_set.expr, irast.Pointer):\n                rptr = ir_set.expr\n            late_compile_view_shapes(\n                child,\n                rptr=rptr,\n                parent_view_type=parent_view_type,\n                ctx=scopectx)\n\n        ir_set.shape_source = child if child.shape else child.shape_source\n        return\n\n    if shape_ptrs:\n        pathctx.register_set_in_scope(ir_set, ctx=ctx)\n        stype = setgen.get_set_type(ir_set, ctx=ctx)\n\n        # If the shape has already been populated (because the set is\n        # referenced multiple times), then we've got nothing to do.\n        if ir_set.shape:\n            # We want to make sure anything inside of the shape gets\n            # processed, though, so we do need to look through the\n            # internals.\n            for element, _ in ir_set.shape:\n                element_scope = pathctx.get_set_scope(element, ctx=ctx)\n                with ctx.new() as scopectx:\n                    if element_scope:\n                        scopectx.path_scope = element_scope\n                    late_compile_view_shapes(\n                        element,\n                        parent_view_type=stype.get_expr_type(ctx.env.schema),\n                        ctx=scopectx)\n\n            return\n\n        shape = []\n        for path_tip, ptr, shape_op, _, ptr_span in shape_ptrs:\n            ptr_span = None\n            if ptr in ctx.env.pointer_specified_info:\n                _, _, ptr_span = ctx.env.pointer_specified_info[ptr]\n\n            element = setgen.extend_path(\n                path_tip,\n                ptr,\n                same_computable_scope=True,\n                span=ptr_span,\n                ctx=ctx,\n            )\n\n            element_scope = pathctx.get_set_scope(element, ctx=ctx)\n\n            if element_scope is None:\n                element_scope = ctx.path_scope.attach_fence()\n                pathctx.assign_set_scope(element, element_scope, ctx=ctx)\n\n            if element_scope.namespaces:\n                element.path_id = element.path_id.merge_namespace(\n                    element_scope.namespaces)\n\n            with ctx.new() as scopectx:\n                scopectx.path_scope = element_scope\n                late_compile_view_shapes(\n                    element,\n                    parent_view_type=stype.get_expr_type(ctx.env.schema),\n                    ctx=scopectx)\n\n            shape.append((element, shape_op))\n\n        ir_set.shape = tuple(shape)\n\n    elif expr is not None:\n        set_scope = pathctx.get_set_scope(ir_set, ctx=ctx)\n        if set_scope is not None:\n            with ctx.new() as scopectx:\n                scopectx.path_scope = set_scope\n                late_compile_view_shapes(expr, ctx=scopectx)\n        else:\n            late_compile_view_shapes(expr, ctx=ctx)\n\n    elif isinstance(ir_set.expr, irast.TupleIndirectionPointer):\n        late_compile_view_shapes(ir_set.expr.source, ctx=ctx)\n\n\n@late_compile_view_shapes.register(irast.SelectStmt)\ndef _late_compile_view_shapes_in_select(\n    stmt: irast.SelectStmt,\n    *,\n    rptr: Optional[irast.Pointer] = None,\n    parent_view_type: Optional[s_types.ExprType] = None,\n    ctx: context.ContextLevel,\n) -> None:\n    late_compile_view_shapes(\n        stmt.result, rptr=rptr, parent_view_type=parent_view_type, ctx=ctx)\n\n\n@late_compile_view_shapes.register(irast.Call)\ndef _late_compile_view_shapes_in_call(\n    expr: irast.Call,\n    *,\n    rptr: Optional[irast.Pointer] = None,\n    parent_view_type: Optional[s_types.ExprType] = None,\n    ctx: context.ContextLevel,\n) -> None:\n\n    if expr.func_polymorphic:\n        for call_arg in expr.args.values():\n            arg = call_arg.expr\n            arg_scope = pathctx.get_set_scope(arg, ctx=ctx)\n            if arg_scope is not None:\n                with ctx.new() as scopectx:\n                    scopectx.path_scope = arg_scope\n                    late_compile_view_shapes(arg, ctx=scopectx)\n            else:\n                late_compile_view_shapes(arg, ctx=ctx)\n\n\n@late_compile_view_shapes.register(irast.Tuple)\ndef _late_compile_view_shapes_in_tuple(\n    expr: irast.Tuple,\n    *,\n    rptr: Optional[irast.Pointer] = None,\n    parent_view_type: Optional[s_types.ExprType] = None,\n    ctx: context.ContextLevel,\n) -> None:\n    for element in expr.elements:\n        late_compile_view_shapes(element.val, ctx=ctx)\n\n\n@late_compile_view_shapes.register(irast.Array)\ndef _late_compile_view_shapes_in_array(\n    expr: irast.Array,\n    *,\n    rptr: Optional[irast.Pointer] = None,\n    parent_view_type: Optional[s_types.ExprType] = None,\n    ctx: context.ContextLevel,\n) -> None:\n    for element in expr.elements:\n        late_compile_view_shapes(element, ctx=ctx)\n\n\ndef _record_created_collection_types(\n    type: s_types.Type, ctx: context.ContextLevel\n) -> None:\n    \"\"\"\n    Record references to implicitly defined collection types,\n    so that the alias delta machinery can pick them up.\n    \"\"\"\n\n    if isinstance(\n        type, s_types.Collection\n    ) and not ctx.env.orig_schema.get_by_id(type.id, default=None):\n        for sub_type in type.get_subtypes(ctx.env.schema):\n            _record_created_collection_types(sub_type, ctx)\n"
  },
  {
    "path": "edb/edgeql/declarative.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"SDL loader.\n\nThe purpose of this module is to take a set of SDL documents and\ntransform them into schema modules.  The crux of the task is to\nbreak the SDL declarations into a correct sequence of DDL commands,\nconsidering all possible cyclic references.  The dependency tracking\nis complicated by the presence of expressions in schema definitions.\nIn those cases we make a best-effort tracing using a rudimentary\nEdgeQL AST visitor.\n\"\"\"\n\nfrom __future__ import annotations\nfrom typing import (\n    Optional,\n    AbstractSet,\n    Iterable,\n    Mapping,\n    MutableSet,\n    TypedDict,\n    cast,\n)\n\nimport copy\nimport functools\nfrom collections import defaultdict\n\nfrom edb import errors\n\nfrom edb.common import parsing\nfrom edb.common import topological\nfrom edb.common import english\nfrom edb.common.ordered import OrderedSet\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import codegen as qlcodegen\nfrom edb.edgeql import parser as qlparser\nfrom edb.edgeql import tracer as qltracer\nfrom edb.edgeql import utils as qlutils\n\nfrom edb.schema import annos as s_anno\nfrom edb.schema import constraints as s_constr\nfrom edb.schema import indexes as s_indexes\nfrom edb.schema import links as s_links\nfrom edb.schema import name as s_name\nfrom edb.schema import objects as s_obj\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import properties as s_props\nfrom edb.schema import pseudo as s_pseudo\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import schema as s_schema\nfrom edb.schema import sources as s_sources\nfrom edb.schema import types as s_types\nfrom edb.schema import utils as s_utils\n\n\nclass TraceContextBase:\n\n    schema: s_schema.Schema\n    module: str\n    depstack: list[tuple[qlast.DDLOperation, s_name.QualName]]\n    modaliases: dict[Optional[str], str]\n    objects: dict[s_name.QualName, Optional[qltracer.ObjectLike]]\n    pointers: dict[s_name.UnqualName, set[s_name.QualName]]\n    parents: dict[s_name.QualName, set[s_name.QualName]]\n    ancestors: dict[s_name.QualName, set[s_name.QualName]]\n    defdeps: dict[s_name.QualName, set[s_name.QualName]]\n    constraints: dict[s_name.QualName, set[s_name.QualName]]\n    local_modules: AbstractSet[str]\n\n    def __init__(\n        self,\n        schema: s_schema.Schema,\n        local_modules: AbstractSet[str],\n    ) -> None:\n        self.schema = schema\n        self.module = '__not_set__'\n        self.depstack = []\n        self.modaliases = {}\n        self.objects = {}\n        self.pointers = {}\n        self.parents = {}\n        self.ancestors = {}\n        self.defdeps = defaultdict(set)\n        self.constraints = defaultdict(set)\n        self.local_modules = local_modules\n\n    def set_module(self, module: str) -> None:\n        self.module = module\n        self.modaliases = {None: module}\n\n    def get_local_name(\n        self,\n        ref: qlast.ObjectRef,\n        declaration: bool=False,\n    ) -> s_name.QualName:\n        return qltracer.resolve_name(\n            ref,\n            current_module=self.module,\n            schema=self.schema,\n            objects=self.objects,\n            modaliases=None,\n            local_modules=self.local_modules,\n            declaration=declaration,\n        )\n\n    def get_ref_name(self, ref: qlast.BaseObjectRef) -> s_name.QualName:\n        if isinstance(ref, qlast.ObjectRef):\n            return self.get_local_name(ref)\n        elif isinstance(ref, qlast.PseudoObjectRef):\n            # We pretend `anytype` has a fully-qualified name here, because\n            # the tracing machinery really wants to work with fully-qualified\n            # names and wants to distinguish between objects from the standard\n            # library and the user-defines ones.\n            # Ditto for `anytuple` and `anyobject`.\n            return s_name.QualName('std', ref.name)\n        else:\n            raise TypeError(\n                \"ObjectRef expected \"\n                \"(got type {!r})\".format(type(ref).__name__)\n            )\n\n    def get_fq_name(\n        self,\n        decl: qlast.DDLOperation,\n        declaration: bool=False,\n    ) -> tuple[str, s_name.QualName]:\n        # Get the basic name form.\n        if isinstance(decl, qlast.CreateConcretePointer):\n            name = decl.name.name\n            parent_expected = True\n        elif isinstance(decl, qlast.SetField):\n            name = decl.name\n            parent_expected = True\n        elif isinstance(decl, qlast.ObjectDDL):\n            fq_name = self.get_local_name(decl.name, declaration=declaration)\n            name = str(fq_name)\n            parent_expected = False\n        else:\n            raise AssertionError(f'unexpected DDL node: {decl!r}')\n\n        if self.depstack:\n            parent_name = self.depstack[-1][1]\n            fq_name = s_name.QualName(\n                module=parent_name.module,\n                name=f'{parent_name.name}@{name}'\n            )\n        elif parent_expected:\n            raise AssertionError(\n                f'missing expected parent context for {decl!r}')\n\n        # Additionally, functions and concrete constraints may need an\n        # extra name piece.\n        extra_name = None\n        if isinstance(decl, qlast.CreateFunction):\n            # Functions are defined by their name + call signature, so we\n            # need to add that to the \"extra_name\".\n            extra_name = f'({qlcodegen.generate_source(decl.params)})'\n\n        elif isinstance(decl, qlast.CreateConcreteConstraint):\n            # Concrete constraints are defined by their expr, so we need\n            # to add that to the \"extra_name\".\n            exprs = list(decl.args)\n            if decl.subjectexpr:\n                exprs.append(decl.subjectexpr)\n            if decl.except_expr:\n                # Add an extra dummy argument to distinguish between\n                # ON and EXCEPT, when only one is present\n                exprs.append(qlast.Set(elements=[]))\n                exprs.append(decl.except_expr)\n\n            for cmd in decl.commands:\n                if isinstance(cmd, qlast.SetField) and cmd.name == \"expr\":\n                    assert cmd.value, \"sdl SetField should always have value\"\n                    assert isinstance(cmd.value, qlast.Expr)\n                    exprs.append(cmd.value)\n\n            extra_name = '|'.join(qlcodegen.generate_source(e) for e in exprs)\n\n        elif isinstance(decl, qlast.CreateConcreteIndex):\n            # Indexes are defined by what they are an index over, so we need\n            # to add that to the \"extra_name\".\n            extra_name = f'({qlcodegen.generate_source(decl.expr)})'\n            if decl.except_expr:\n                except_bit = f'({qlcodegen.generate_source(decl.except_expr)})'\n                extra_name = f'{extra_name}/{except_bit}'\n\n        if extra_name:\n            fq_name = s_name.QualName(\n                module=fq_name.module,\n                name=f'{fq_name.name}@@{extra_name}',\n            )\n\n        return name, fq_name\n\n\ndef get_verbosename_from_fqname(\n    fq_name: s_name.QualName,\n    ctx: DepTraceContext | LayoutTraceContext,\n) -> str:\n    traceobj = ctx.objects[fq_name]\n    assert traceobj is not None\n\n    name = str(fq_name)\n    clsname = traceobj.get_schema_class_displayname()\n    ofobj = ''\n\n    if isinstance(traceobj, qltracer.Alias):\n        clsname = 'alias'\n    elif isinstance(traceobj, qltracer.ObjectType):\n        clsname = 'object'\n    elif isinstance(traceobj, qltracer.ScalarType):\n        clsname = 'scalar'\n    elif isinstance(traceobj, qltracer.Function):\n        name = str(fq_name).split('@@', 1)[0]\n        if isinstance(ctx, DepTraceContext):\n            node = ctx.ddlgraph[fq_name].item\n            assert isinstance(node, qlast.FunctionCommand)\n            params = ','.join(\n                qlcodegen.generate_source(param, sdlmode=True)\n                for param in node.params\n            )\n            name = f\"{name}({params})\"\n    elif isinstance(traceobj, qltracer.Pointer):\n        ofobj, name = str(fq_name).split('@', 1)\n        ofobj = f\" of object type '{ofobj}'\"\n    elif isinstance(traceobj, qltracer.AccessPolicy):\n        clsname = 'access policy'\n        ofobj, name = str(fq_name).split('@', 1)\n        _, name = name.split('::')\n        ofobj = f\" of object type '{ofobj}'\"\n    elif isinstance(traceobj, qltracer.Trigger):\n        clsname = 'trigger'\n        ofobj, name = str(fq_name).split('@', 1)\n        _, name = name.split('::')\n        ofobj = f\" of object type '{ofobj}'\"\n    elif isinstance(traceobj, qltracer.ConcreteIndex):\n        clsname = 'index'\n        ofobj, name = str(fq_name).split('@', 1)\n        name, _ = name.split('@@', 1)\n        if name == str(s_indexes.DEFAULT_INDEX):\n            name = ''\n        ofobj = f\" of object type '{ofobj}'\"\n    elif isinstance(traceobj, qltracer.Field):\n        clsname = 'field'\n        obj, name = fq_name.name.rsplit('@', 1)\n        ofobj = ' of ' + get_verbosename_from_fqname(\n            s_name.QualName(fq_name.module, obj), ctx)\n\n    if name:\n        return f\"{clsname} '{name}'{ofobj}\"\n    else:\n        return f\"{clsname}{ofobj}\"\n\n\nclass InheritanceGraphEntry(TypedDict):\n\n    item: qltracer.NamedObject\n    deps: AbstractSet[s_name.Name]\n    merge: AbstractSet[s_name.Name]\n\n\nclass LayoutTraceContext(TraceContextBase):\n\n    inh_graph: dict[\n        s_name.QualName,\n        topological.DepGraphEntry[\n            s_name.QualName,\n            qltracer.NamedObject,\n            bool,\n        ],\n    ]\n\n    def __init__(\n        self,\n        schema: s_schema.Schema,\n        local_modules: AbstractSet[str],\n    ) -> None:\n        super().__init__(schema, local_modules)\n        self.inh_graph = {}\n\n\nDDLGraph = dict[\n    s_name.QualName,\n    topological.DepGraphEntry[s_name.QualName, qlast.DDLCommand, bool],\n]\n\n\nclass DepTraceContext(TraceContextBase):\n\n    def __init__(\n        self,\n        schema: s_schema.Schema,\n        ddlgraph: DDLGraph,\n        objects: dict[s_name.QualName, Optional[qltracer.ObjectLike]],\n        pointers: dict[s_name.UnqualName, set[s_name.QualName]],\n        parents: dict[s_name.QualName, set[s_name.QualName]],\n        ancestors: dict[s_name.QualName, set[s_name.QualName]],\n        defdeps: dict[s_name.QualName, set[s_name.QualName]],\n        constraints: dict[s_name.QualName, set[s_name.QualName]],\n        local_modules: AbstractSet[str],\n    ) -> None:\n        super().__init__(schema, local_modules)\n        self.ddlgraph = ddlgraph\n        self.objects = objects\n        self.pointers = pointers\n        self.parents = parents\n        self.ancestors = ancestors\n        self.defdeps = defdeps\n        self.constraints = constraints\n\n\nclass Dependency:\n    pass\n\n\nclass TypeDependency(Dependency):\n\n    texpr: qlast.TypeExpr\n\n    def __init__(self, texpr: qlast.TypeExpr) -> None:\n        self.texpr = texpr\n\n\nclass ExprDependency(Dependency):\n\n    expr: qlast.Expr\n\n    def __init__(self, expr: qlast.Expr) -> None:\n        self.expr = expr\n\n\nclass FunctionDependency(ExprDependency):\n\n    params: Mapping[str, qlast.TypeExpr]\n\n    def __init__(\n        self,\n        expr: qlast.Expr,\n        params: Mapping[str, qlast.TypeExpr],\n    ) -> None:\n        super().__init__(expr=expr)\n        self.params = params\n\n\ndef sdl_to_ddl(\n    schema: s_schema.Schema,\n    documents: Mapping[str, list[qlast.DDLCommand]],\n) -> tuple[qlast.DDLCommand, ...]:\n\n    ddlgraph: DDLGraph = {}\n    mods: list[qlast.DDLCommand] = []\n\n    ctx = LayoutTraceContext(schema, frozenset(mod for mod in documents))\n\n    ctx.objects[s_name.QualName('std', 'anytype')] = (\n        schema.get_global(s_pseudo.PseudoType, 'anytype'))\n    ctx.objects[s_name.QualName('std', 'anytuple')] = (\n        schema.get_global(s_pseudo.PseudoType, 'anytuple'))\n    ctx.objects[s_name.QualName('std', 'anyobject')] = (\n        schema.get_global(s_pseudo.PseudoType, 'anyobject'))\n\n    for module_name, declarations in documents.items():\n        ctx.set_module(module_name)\n        for decl_ast in declarations:\n            if isinstance(decl_ast, qlast.CreateObject):\n                _, fq_name = ctx.get_fq_name(decl_ast, declaration=True)\n\n                if isinstance(decl_ast, qlast.CreateObjectType):\n                    ctx.objects[fq_name] = qltracer.ObjectType(fq_name)\n                elif isinstance(decl_ast, qlast.CreateAlias):\n                    ctx.objects[fq_name] = qltracer.Alias(fq_name)\n                elif isinstance(decl_ast, qlast.CreateScalarType):\n                    ctx.objects[fq_name] = qltracer.ScalarType(fq_name)\n                elif isinstance(decl_ast, qlast.CreateLink):\n                    ctx.objects[fq_name] = qltracer.Link(\n                        fq_name, source=None, target=None)\n                elif isinstance(decl_ast, qlast.CreateProperty):\n                    ctx.objects[fq_name] = qltracer.Property(\n                        fq_name, source=None, target=None)\n                elif isinstance(decl_ast, qlast.CreateFunction):\n                    ctx.objects[fq_name] = qltracer.Function(fq_name)\n                elif isinstance(decl_ast, qlast.CreateConstraint):\n                    ctx.objects[fq_name] = qltracer.Constraint(fq_name)\n                elif isinstance(decl_ast, qlast.CreateAnnotation):\n                    ctx.objects[fq_name] = qltracer.Annotation(fq_name)\n                elif isinstance(decl_ast, qlast.CreateGlobal):\n                    ctx.objects[fq_name] = qltracer.Global(fq_name)\n                elif isinstance(decl_ast, qlast.CreatePermission):\n                    ctx.objects[fq_name] = qltracer.Permission(fq_name)\n                elif isinstance(decl_ast, qlast.CreateIndex):\n                    ctx.objects[fq_name] = qltracer.Index(fq_name)\n                else:\n                    raise AssertionError(\n                        f'unexpected SDL declaration: {decl_ast}')\n\n    for module_name, declarations in documents.items():\n        ctx.set_module(module_name)\n        for decl_ast in declarations:\n            trace_layout(decl_ast, ctx=ctx)\n\n    # compute the ancestors graph\n    for obj_name in ctx.parents.keys():\n        ctx.ancestors[obj_name] = get_ancestors(\n            obj_name, ctx.ancestors, ctx.parents)\n\n    topological.normalize(\n        ctx.inh_graph,\n        merger=_graph_merge_cb,  # type: ignore\n        schema=schema,\n    )\n\n    tracectx = DepTraceContext(\n        schema, ddlgraph, ctx.objects, ctx.pointers, ctx.parents, ctx.ancestors,\n        ctx.defdeps, ctx.constraints, ctx.local_modules,\n    )\n\n    created_modules = set()\n    for module_name, declarations in documents.items():\n        tracectx.set_module(module_name)\n        # module (and any enclosing modules) needs to be created\n        # regardless of whether its contents are empty or not\n        parts = module_name.split('::')\n        for i in range(len(parts)):\n            n = '::'.join(parts[:i + 1])\n            if n not in created_modules:\n                created_modules.add(n)\n                mods.append(qlast.CreateModule(name=qlast.ObjectRef(name=n)))\n        for decl_ast in declarations:\n            trace_dependencies(decl_ast, ctx=tracectx)\n\n    for ddlentry in ddlgraph.values():\n        # Filter out deps that are in the schema but not in ctx.objects.\n        # Deps that are in neither get left in, so that we catch the bug.\n        deps = {\n            x for x in ddlentry.deps\n            if x in ctx.objects or not schema.get(x, default=None)\n        }\n        weak_deps = {\n            x for x in ddlentry.weak_deps\n            if x in ctx.objects or not schema.get(x, default=None)\n        }\n\n        # Before sorting normalize all ordering, to make sure that errors\n        # are consistent.\n        ddlentry.deps = OrderedSet(sorted(deps))\n        ddlentry.weak_deps = OrderedSet(sorted(weak_deps))\n\n    try:\n        ordered = topological.sort(ddlgraph, allow_unresolved=False)\n    except topological.CycleError as e:\n        assert isinstance(e.item, s_name.QualName)\n        node = tracectx.ddlgraph[e.item].item\n        item_vn = get_verbosename_from_fqname(e.item, tracectx)\n\n        if e.path is not None and len(e.path):\n            # Recursion involving more than one schema object.\n            rec_vn = get_verbosename_from_fqname(e.path[-1], tracectx)\n            msg = (\n                f'definition dependency cycle between {rec_vn} '\n                f'and {item_vn}'\n            )\n        else:\n            # A single schema object with a recursive definition.\n            msg = f'{item_vn} is defined recursively'\n\n        raise errors.InvalidDefinitionError(msg, span=node.span) from e\n\n    return tuple(mods) + tuple(ordered)\n\n\ndef _graph_merge_cb(\n    item: qltracer.NamedObject,\n    parent: qltracer.NamedObject,\n    *,\n    schema: s_schema.Schema,\n) -> qltracer.NamedObject:\n    if (\n        isinstance(item, (qltracer.Source, s_sources.Source))\n        and isinstance(parent, (qltracer.Source, s_sources.Source))\n    ):\n        return _merge_items(item, parent, schema=schema)\n    else:\n        return item\n\n\ndef _merge_items(\n    item: qltracer.Source_T,\n    parent: qltracer.SourceLike_T,\n    *,\n    schema: s_schema.Schema,\n) -> qltracer.Source_T:\n\n    item_ptrs = dict(item.get_pointers(schema).items(schema))\n\n    for pn, ptr in parent.get_pointers(schema).items(schema):\n        if not isinstance(ptr, (qltracer.Pointer, s_sources.Source)):\n            continue\n\n        if pn not in item_ptrs:\n            PointerType = (qltracer.Property if ptr.is_property(schema)\n                           else qltracer.Link)\n            ptr_copy = PointerType(\n                s_name.QualName('__', pn.name),\n                source=ptr.get_source(schema),\n                target=ptr.get_target(schema),\n            )\n            ptr_copy.pointers = dict(\n                ptr.get_pointers(schema).items(schema))\n            item.pointers[pn] = ptr_copy\n        else:\n            item_ptr = item.getptr(schema, pn)\n            assert isinstance(item_ptr, (qltracer.Pointer, s_sources.Source))\n            PointerType = (qltracer.Property if item_ptr.is_property(schema)\n                           else qltracer.Link)\n            ptr_copy = PointerType(\n                s_name.QualName('__', pn.name),\n                source=item,\n                target=item_ptr.get_target(schema),\n            )\n            ptr_copy.pointers = dict(\n                item_ptr.get_pointers(schema).items(schema))\n            item.pointers[pn] = _merge_items(ptr_copy, ptr, schema=schema)\n\n    return item\n\n\n@functools.singledispatch\ndef trace_layout(\n    node: qlast.Base,\n    *,\n    ctx: LayoutTraceContext,\n) -> None:\n    pass\n\n\n@trace_layout.register\ndef trace_layout_Schema(\n    node: qlast.Schema,\n    *,\n    ctx: LayoutTraceContext,\n) -> None:\n    for decl in node.declarations:\n        trace_layout(decl, ctx=ctx)\n\n\n@trace_layout.register\ndef trace_layout_CreateScalarType(\n    node: qlast.CreateScalarType,\n    *,\n    ctx: LayoutTraceContext,\n) -> None:\n    _trace_item_layout(node, ctx=ctx)\n\n\n@trace_layout.register\ndef trace_layout_CreateObjectType(\n    node: qlast.CreateObjectType,\n    *,\n    ctx: LayoutTraceContext,\n) -> None:\n    _trace_item_layout(node, ctx=ctx)\n\n\n@trace_layout.register\ndef trace_layout_CreateLink(\n    node: qlast.CreateLink,\n    *,\n    ctx: LayoutTraceContext,\n) -> None:\n    _trace_item_layout(node, ctx=ctx)\n\n\n@trace_layout.register\ndef trace_layout_CreateProperty(\n    node: qlast.CreateProperty,\n    *,\n    ctx: LayoutTraceContext,\n) -> None:\n    _trace_item_layout(node, ctx=ctx)\n\n\n@trace_layout.register\ndef trace_layout_CreateConstraint(\n    node: qlast.CreateConstraint,\n    *,\n    ctx: LayoutTraceContext,\n) -> None:\n    _trace_item_layout(node, ctx=ctx)\n\n\ndef _trace_item_layout(\n    node: qlast.CreateObject,\n    *,\n    obj: Optional[qltracer.NamedObject] = None,\n    fq_name: Optional[s_name.QualName] = None,\n    ctx: LayoutTraceContext,\n) -> None:\n    if obj is None:\n        fq_name = ctx.get_local_name(node.name)\n        local_obj = ctx.objects[fq_name]\n        assert isinstance(local_obj, qltracer.NamedObject)\n        obj = local_obj\n\n    assert fq_name is not None\n    PointerType: type[qltracer.Pointer]\n\n    if isinstance(node, qlast.BasedOn):\n        bases = []\n        # construct the parents set, used later in ancestors graph\n        parents = set()\n\n        for ref in _get_bases(node, ctx=ctx):\n            bases.append(ref)\n\n            # ignore std modules dependencies\n            if ref.get_module_name() not in s_schema.STD_MODULES:\n                parents.add(ref)\n\n            if (\n                ref.module not in ctx.local_modules\n                and ref not in ctx.inh_graph\n            ):\n                base_obj = type(obj)(name=ref)\n                ctx.inh_graph[ref] = topological.DepGraphEntry(item=base_obj)\n\n                base = ctx.schema.get(ref)\n                if isinstance(base, s_sources.Source):\n                    assert isinstance(base_obj, qltracer.Source)\n                    base_pointers = base.get_pointers(ctx.schema)\n                    for pn, p in base_pointers.items(ctx.schema):\n                        PointerType = (\n                            qltracer.Property\n                            if p.is_property() else\n                            qltracer.Link\n                        )\n                        base_obj.pointers[pn] = PointerType(\n                            s_name.QualName('__', pn.name),\n                            source=base,\n                            target=p.get_target(ctx.schema),\n                        )\n\n        ctx.parents[fq_name] = parents\n        ctx.inh_graph[fq_name] = topological.DepGraphEntry(\n            item=obj,\n            deps=set(bases),\n            merge=set(bases),\n        )\n\n    for decl in node.commands:\n        if isinstance(decl, qlast.CreateConcretePointer):\n            assert isinstance(obj, qltracer.Source)\n\n            target: Optional[qltracer.TypeLike]\n            target_expr: Optional[qlast.Expr]\n            if isinstance(decl.target, qlast.TypeExpr):\n                target = _resolve_type_expr(decl.target, ctx=ctx)\n                target_expr = _get_expr_field(decl)\n            else:\n                target = None\n                target_expr = decl.target\n\n            pn = s_utils.ast_ref_to_unqualname(decl.name)\n\n            PointerType = (\n                qltracer.Property\n                if isinstance(decl, qlast.CreateConcreteProperty) else\n                qltracer.Link\n                if isinstance(decl, qlast.CreateConcreteProperty) else\n                qltracer.UnknownPointer\n            )\n            ptr = PointerType(\n                s_name.QualName('__', pn.name),\n                source=obj,\n                target=target,\n                target_expr=target_expr,\n            )\n            obj.pointers[pn] = ptr\n            ptr_name = s_name.QualName(\n                module=fq_name.module,\n                name=f'{fq_name.name}@{decl.name.name}',\n            )\n            ctx.objects[ptr_name] = ptr\n            ctx.defdeps[fq_name].add(ptr_name)\n            ctx.pointers.setdefault(pn, set()).add(ptr_name)\n\n            _trace_item_layout(\n                decl, obj=ptr, fq_name=ptr_name, ctx=ctx)\n\n        elif isinstance(decl, qlast.CreateConcreteConstraint):\n            # Validate that the constraint exists at all.\n            _validate_schema_ref(decl, ctx=ctx)\n            _, con_fq_name = ctx.get_fq_name(decl)\n\n            con_name = s_name.QualName(\n                module=fq_name.module,\n                name=f'{fq_name.name}@{con_fq_name}',\n            )\n            ctx.objects[con_name] = qltracer.ConcreteConstraint(con_name)\n            ctx.constraints[fq_name].add(con_name)\n\n        elif isinstance(decl, qlast.CreateAnnotationValue):\n            # Validate that the annotation exists at all.\n            _validate_schema_ref(decl, ctx=ctx)\n            _, anno_fq_name = ctx.get_fq_name(decl)\n\n            anno_name = s_name.QualName(\n                module=fq_name.module,\n                name=f'{fq_name.name}@{anno_fq_name}',\n            )\n            ctx.objects[anno_name] = qltracer.AnnotationValue(anno_name)\n\n        elif isinstance(decl, qlast.CreateAccessPolicy):\n            _, pol_fq_name = ctx.get_fq_name(decl)\n\n            pol_name = s_name.QualName(\n                module=fq_name.module,\n                name=f'{fq_name.name}@{pol_fq_name}',\n            )\n            assert isinstance(obj, qltracer.Source)\n            ctx.objects[pol_name] = qltracer.AccessPolicy(pol_name, source=obj)\n\n        # XXX: name conflict with triggers, other things??\n        elif isinstance(decl, qlast.CreateTrigger):\n            _, trigger_fq_name = ctx.get_fq_name(decl)\n\n            trigger_name = s_name.QualName(\n                module=fq_name.module,\n                name=f'{fq_name.name}@{trigger_fq_name}',\n            )\n            assert isinstance(obj, qltracer.Source)\n            ctx.objects[trigger_name] = qltracer.Trigger(\n                trigger_name, source=obj)\n\n        elif isinstance(decl, qlast.CreateConcreteIndex):\n            # Validate that the index exists at all.\n            _validate_schema_ref(decl, ctx=ctx)\n            _, idx_fq_name = ctx.get_fq_name(decl)\n\n            idx_name = s_name.QualName(\n                module=fq_name.module,\n                name=f'{fq_name.name}@{idx_fq_name}',\n            )\n            ctx.objects[idx_name] = qltracer.ConcreteIndex(idx_name)\n\n        elif isinstance(decl, qlast.SetField):\n            field_name = s_name.QualName(\n                module=fq_name.module,\n                name=f'{fq_name.name}@{decl.name}',\n            )\n\n            # Trivial fields don't get added to the ddlgraph, which is\n            # where duplication checks are normally done, so do the\n            # check here instead.\n            if field_name in ctx.objects:\n                vn = get_verbosename_from_fqname(field_name, ctx)\n                msg = f'{vn} was already declared'\n                raise errors.InvalidDefinitionError(msg, span=decl.span)\n\n            ctx.objects[field_name] = qltracer.Field(field_name)\n\n\nRECURSION_GUARD: set[s_name.QualName] = set()\n\n\ndef get_ancestors(\n    fq_name: s_name.QualName,\n    ancestors: dict[s_name.QualName, set[s_name.QualName]],\n    parents: Mapping[s_name.QualName, AbstractSet[s_name.QualName]],\n) -> set[s_name.QualName]:\n    \"\"\"Recursively compute ancestors (in place) from the parents graph.\"\"\"\n\n    # value already computed\n    result = ancestors.get(fq_name, set())\n    if result is RECURSION_GUARD:\n        raise errors.InvalidDefinitionError(\n            f'{str(fq_name)!r} is defined recursively')\n    elif result:\n        return result\n\n    ancestors[fq_name] = RECURSION_GUARD\n\n    parent_set = parents.get(fq_name, set())\n    # base case: include the parents\n    result = set(parent_set)\n    for fq_parent in parent_set:\n        # recursive step: include parents' ancestors\n        result |= get_ancestors(fq_parent, ancestors, parents)\n\n    ancestors[fq_name] = result\n\n    return result\n\n\n@functools.singledispatch\ndef trace_dependencies(\n    node: qlast.Base,\n    *,\n    ctx: DepTraceContext,\n) -> None:\n    raise NotImplementedError(\n        f\"no SDL dep tracer handler for {node.__class__}\")\n\n\n@trace_dependencies.register\ndef trace_SetField(\n    node: qlast.SetField,\n    *,\n    ctx: DepTraceContext,\n) -> None:\n    deps = set()\n    exprs = []\n\n    assert node.value, \"sdl SetField should always have value\"\n    if node.name == 'default':\n        assert isinstance(node.value, qlast.Expr)\n        exprs.append(ExprDependency(expr=node.value))\n    else:\n        for dep in qltracer.trace_refs(\n            node.value,\n            schema=ctx.schema,\n            module=ctx.module,\n            objects=ctx.objects,\n            pointers=ctx.pointers,\n            local_modules=ctx.local_modules,\n            params={},\n        )[0]:\n            # ignore std module dependencies\n            if dep.get_module_name() not in s_schema.STD_MODULES:\n                deps.add(dep)\n\n    _register_item(node, deps=deps, hard_dep_exprs=exprs, ctx=ctx)\n\n\n@trace_dependencies.register\ndef trace_ConcreteConstraint(\n    node: qlast.CreateConcreteConstraint,\n    *,\n    ctx: DepTraceContext,\n) -> None:\n    deps = set()\n\n    base_name = ctx.get_ref_name(node.name)\n    if base_name.get_module_name() not in s_schema.STD_MODULES:\n        deps.add(base_name)\n\n    exprs = [ExprDependency(expr=arg) for arg in node.args]\n    if node.subjectexpr:\n        exprs.append(ExprDependency(expr=node.subjectexpr))\n    if node.except_expr:\n        exprs.append(ExprDependency(expr=node.except_expr))\n\n    if (expr := _get_expr_field(node)):\n        exprs.append(ExprDependency(expr=expr))\n\n    loop_control: Optional[s_name.QualName]\n    if isinstance(ctx.depstack[-1][0], qlast.AlterScalarType):\n        # Scalars are tightly bound to their constraints, so\n        # we must prohibit any possible reference to this scalar\n        # type from within the constraint.\n        loop_control = ctx.depstack[-1][1]\n    else:\n        loop_control = None\n\n    _register_item(\n        node,\n        deps=deps,\n        hard_dep_exprs=exprs,\n        loop_control=loop_control,\n        source=ctx.depstack[-1][1],\n        subject=ctx.depstack[-1][1],\n        ctx=ctx,\n    )\n\n\n@trace_dependencies.register\ndef trace_AccessPolicy(\n    node: qlast.CreateAccessPolicy,\n    *,\n    ctx: DepTraceContext,\n) -> None:\n    exprs = []\n    if node.expr:\n        exprs.append(ExprDependency(expr=node.expr))\n    if node.condition:\n        exprs.append(ExprDependency(expr=node.condition))\n\n    _register_item(\n        node,\n        deps=set(),\n        hard_dep_exprs=exprs,\n        source=ctx.depstack[-1][1],\n        subject=ctx.depstack[-1][1],\n        ctx=ctx,\n    )\n\n\n@trace_dependencies.register\ndef trace_Trigger(\n    node: qlast.CreateTrigger,\n    *,\n    ctx: DepTraceContext,\n) -> None:\n    exprs = [ExprDependency(expr=node.expr)]\n    if node.condition:\n        exprs.append(ExprDependency(expr=node.condition))\n\n    obj = ctx.depstack[-1][1]\n    _register_item(\n        node,\n        deps=set(),\n        hard_dep_exprs=exprs,\n        source=obj,\n        subject=obj,\n        anchors={'__new__': obj, '__old__': obj},\n        ctx=ctx,\n    )\n\n\n@trace_dependencies.register\ndef trace_Rewrite(\n    node: qlast.CreateRewrite,\n    *,\n    ctx: DepTraceContext,\n) -> None:\n    exprs = [ExprDependency(expr=node.expr)]\n\n    obj = ctx.depstack[-2][1]\n    _register_item(\n        node,\n        deps=set(),\n        hard_dep_exprs=exprs,\n        source=obj,\n        subject=obj,\n        anchors={'__old__': obj},\n        ctx=ctx,\n    )\n\n\n@trace_dependencies.register\ndef trace_Index(\n    node: qlast.CreateConcreteIndex,\n    *,\n    ctx: DepTraceContext,\n) -> None:\n    exprs = [ExprDependency(expr=node.expr)]\n    if node.except_expr:\n        exprs.append(ExprDependency(expr=node.except_expr))\n    deps = set()\n    if node.kwargs:\n        for kwarg in node.kwargs:\n            # HACK: Search all objects and depend on any ext::ai annotations.\n            # FIXME: Can we make this more general and less slow?\n            if kwarg == \"embedding_model\":\n                for n, v in ctx.objects.items():\n                    if (\n                        \"@ext::ai::\" in n.name\n                        and isinstance(v, qltracer.AnnotationValue)\n                    ):\n                        deps.add(n)\n    _register_item(\n        node,\n        deps=deps,\n        hard_dep_exprs=exprs,\n        source=ctx.depstack[-1][1],\n        subject=ctx.depstack[-1][1],\n        ctx=ctx,\n    )\n\n\n@trace_dependencies.register\ndef trace_ConcretePointer(\n    node: qlast.CreateConcretePointer,\n    *,\n    ctx: DepTraceContext,\n) -> None:\n    deps: list[Dependency] = []\n    if isinstance(node.target, qlast.TypeExpr):\n        deps.append(TypeDependency(texpr=node.target))\n    elif isinstance(node.target, qlast.Expr):\n        deps.append(ExprDependency(expr=node.target))\n    elif node.target is None:\n        pass\n    else:\n        raise AssertionError(\n            f'unexpected CreateConcretePointer.target: {node.target!r}')\n\n    if (target_expr := _get_expr_field(node)):\n        deps.append(ExprDependency(expr=target_expr))\n\n    _register_item(\n        node,\n        hard_dep_exprs=deps,\n        source=ctx.depstack[-1][1],\n        ctx=ctx,\n    )\n\n\n@trace_dependencies.register\ndef trace_Alias(\n    node: qlast.CreateAlias,\n    *,\n    ctx: DepTraceContext,\n) -> None:\n    hard_dep_exprs = []\n\n    if (expr := _get_expr_field(node)):\n        hard_dep_exprs.append(ExprDependency(expr=expr))\n\n    _register_item(node, hard_dep_exprs=hard_dep_exprs, ctx=ctx)\n\n\n@trace_dependencies.register\ndef trace_Global(\n    node: qlast.CreateGlobal,\n    *,\n    ctx: DepTraceContext,\n) -> None:\n    deps: list[Dependency] = []\n\n    if isinstance(node.target, qlast.TypeExpr):\n        deps.append(TypeDependency(texpr=node.target))\n    elif isinstance(node.target, qlast.Expr):\n        deps.append(ExprDependency(expr=node.target))\n\n    _register_item(node, hard_dep_exprs=deps, ctx=ctx)\n\n\n@trace_dependencies.register\ndef trace_Permission(\n    node: qlast.CreatePermission,\n    *,\n    ctx: DepTraceContext,\n) -> None:\n    deps: list[Dependency] = [\n        TypeDependency(texpr=qlast.TypeName(\n            maintype=qlast.ObjectRef(module='__std__', name='bool')\n        ))\n    ]\n\n    _register_item(node, hard_dep_exprs=deps, ctx=ctx)\n\n\n@trace_dependencies.register\ndef trace_Function(\n    node: qlast.CreateFunction,\n    *,\n    ctx: DepTraceContext,\n) -> None:\n    # We also need to add all the signature types as dependencies\n    # to make sure that DDL linearization of SDL will define the types\n    # before the function.\n    deps: list[Dependency] = []\n\n    # We don't actually care to resolve these, but we do need to check for\n    # tracing errors.\n    for param in node.params:\n        if (\n            isinstance(param.type, qlast.TypeName)\n            and isinstance(param.type.maintype, qlast.PseudoObjectRef)\n        ):\n            # generic types are handled elsewhere\n            continue\n        _resolve_type_expr(param.type, ctx=ctx)\n    _resolve_type_expr(node.returning, ctx=ctx)\n\n    deps.extend(TypeDependency(texpr=param.type) for param in node.params)\n    deps.append(TypeDependency(texpr=node.returning))\n\n    params: dict[str, qlast.TypeExpr] = {}\n    for param in node.params:\n        params[param.name] = param.type\n\n    if node.nativecode is not None:\n        deps.append(FunctionDependency(expr=node.nativecode, params=params))\n    elif (\n        node.code is not None\n        and node.code.language is qlast.Language.EdgeQL\n        and node.code.code\n    ):\n        # Need to parse the actual code string and use that as the dependency.\n        fcode = qlparser.parse_query(node.code.code)\n        assert isinstance(fcode, qlast.Expr)\n        deps.append(FunctionDependency(expr=fcode, params=params))\n\n    # XXX: hard_dep_expr is used because it ultimately calls the\n    # _get_hard_deps helper that extracts the proper dependency list\n    # from types.\n    _register_item(node, ctx=ctx, hard_dep_exprs=deps)\n\n\n@trace_dependencies.register\ndef trace_default(\n    node: qlast.CreateObject,\n    *,\n    ctx: DepTraceContext,\n) -> None:\n    # Generic DDL catchall\n    _register_item(node, ctx=ctx)\n\n\ndef _clear_nonessential_subcommands(node: qlast.DDLOperation) -> None:\n    node.commands = [\n        cmd for cmd in node.commands\n        if isinstance(cmd, qlast.SetField) and cmd.name.startswith('orig_')\n    ]\n\n\ndef _register_item(\n    decl: qlast.DDLOperation,\n    *,\n    deps: Optional[AbstractSet[s_name.QualName]] = None,\n    hard_dep_exprs: Optional[Iterable[Dependency]] = None,\n    loop_control: Optional[s_name.QualName] = None,\n    anchors: Optional[Mapping[str, s_name.QualName]] = None,\n    source: Optional[s_name.QualName] = None,\n    subject: Optional[s_name.QualName] = None,\n    ctx: DepTraceContext,\n) -> None:\n\n    name, fq_name = ctx.get_fq_name(decl)\n\n    if fq_name in ctx.ddlgraph:\n        vn = get_verbosename_from_fqname(fq_name, ctx)\n        msg = f'{vn} was already declared'\n        raise errors.InvalidDefinitionError(msg, span=decl.span)\n\n    if deps:\n        deps = set(deps)\n    else:\n        deps = set()\n\n    weak_deps: set[s_name.QualName] = set()\n\n    op = orig_op = copy.copy(decl)\n\n    if ctx.depstack:\n        if isinstance(op, qlast.CreateObject):\n            op.sdl_alter_if_exists = True\n        top_parent = parent = copy.copy(ctx.depstack[0][0])\n        _clear_nonessential_subcommands(parent)\n        for entry, _ in ctx.depstack[1:]:\n            entry_op = copy.copy(entry)\n            parent.commands.append(entry_op)\n            parent = entry_op\n            _clear_nonessential_subcommands(parent)\n\n        parent.commands.append(op)\n        op = top_parent\n    else:\n        assert isinstance(op, (qlast.Query, qlast.Command, qlast.DDLCommand))\n        op.aliases = [qlast.ModuleAliasDecl(alias=None, module=ctx.module)]\n\n    assert isinstance(op, qlast.DDLCommand)\n    node = topological.DepGraphEntry(\n        item=op,\n        deps={n for _, n in ctx.depstack if n != loop_control},\n        extra=False,\n    )\n    ctx.ddlgraph[fq_name] = node\n\n    if hasattr(decl, \"bases\"):\n        # add parents to dependencies\n        parents = ctx.parents.get(fq_name)\n        if parents is not None:\n            deps.update(parents)\n\n    if ctx.depstack:\n        # all ancestors should be seen as dependencies\n        ancestor_bases = ctx.ancestors.get(ctx.depstack[-1][1])\n        if ancestor_bases:\n            for ancestor_base in ancestor_bases:\n                base_item = qltracer.qualify_name(ancestor_base, name)\n                if base_item in ctx.objects:\n                    deps.add(base_item)\n\n    ast_subcommands = getattr(decl, 'commands', [])\n    commands = []\n    if ast_subcommands:\n        subcmds: list[qlast.DDLOperation] = []\n        for cmd in ast_subcommands:\n            # include dependency on constraints or annotations if present\n            if isinstance(cmd, qlast.CreateConcreteConstraint):\n                cmd_name = ctx.get_local_name(cmd.name)\n                if cmd_name.get_module_name() not in s_schema.STD_MODULES:\n                    deps.add(cmd_name)\n            elif isinstance(cmd, qlast.CreateAnnotationValue):\n                cmd_name = ctx.get_local_name(cmd.name)\n                if cmd_name.get_module_name() not in s_schema.STD_MODULES:\n                    deps.add(cmd_name)\n\n            if (isinstance(cmd, qlast.ObjectDDL)\n                    # HACK: functions don't have alters at the moment\n                    and not isinstance(decl, qlast.CreateFunction)):\n                subcmds.append(cmd)\n            elif (isinstance(cmd, qlast.SetField)\n                  and not cmd.special_syntax\n                  and not isinstance(cmd.value, qlast.BaseConstant)\n                  and not isinstance(\n                      op, (qlast.CreateAlias, qlast.CreateGlobal))):\n                subcmds.append(cmd)\n            else:\n                commands.append(cmd)\n\n        if subcmds:\n            assert isinstance(decl, qlast.ObjectDDL)\n            alter_name = f\"Alter{decl.__class__.__name__[len('Create'):]}\"\n            alter_cls = getattr(qlast, alter_name)\n            alter_cmd: qlast.ObjectDDL = alter_cls(name=decl.name)\n\n            # indexes need to preserve their \"on\" expression\n            if isinstance(decl, qlast.CreateConcreteIndex):\n                assert isinstance(alter_cmd, qlast.ConcreteIndexCommand)\n                alter_cmd.expr = decl.expr\n                alter_cmd.kwargs = decl.kwargs\n\n            # constraints need to preserve their \"on\" expression\n            if isinstance(decl, qlast.CreateConcreteConstraint):\n                assert isinstance(alter_cmd, qlast.ConcreteConstraintOp)\n                alter_cmd.subjectexpr = decl.subjectexpr\n                alter_cmd.args = decl.args\n\n            # functions need to preserve arguments\n            if isinstance(decl, qlast.CreateFunction):\n                assert isinstance(alter_cmd, qlast.FunctionCommand)\n                alter_cmd.params = decl.params\n\n            if not ctx.depstack:\n                alter_cmd.aliases = [\n                    qlast.ModuleAliasDecl(alias=None, module=ctx.module)\n                ]\n\n            ctx.depstack.append((alter_cmd, fq_name))\n\n            for cmd in subcmds:\n                trace_dependencies(cmd, ctx=ctx)\n\n            ctx.depstack.pop()\n\n    if hard_dep_exprs:\n        anchors = dict(anchors or {})\n        if source:\n            anchors['__source__'] = source\n        if subject or (\n            fq_name\n            and not (\n                isinstance(decl, qlast.SetField) and decl.name == 'default'\n            )\n        ):\n            anchors['__subject__'] = subject or fq_name\n\n        for expr in hard_dep_exprs:\n            if isinstance(expr, TypeDependency):\n                deps |= _get_hard_deps(expr.texpr, ctx=ctx)\n            elif isinstance(expr, ExprDependency):\n                qlexpr = expr.expr\n                params: Mapping[str, qlast.TypeExpr]\n                if isinstance(expr, FunctionDependency):\n                    params = expr.params\n                else:\n                    params = {}\n\n                strong_tdeps, weak_tdeps = qltracer.trace_refs(\n                    qlexpr,\n                    schema=ctx.schema,\n                    module=ctx.module,\n                    path_prefix=source,\n                    anchors=anchors,\n                    objects=ctx.objects,\n                    pointers=ctx.pointers,\n                    local_modules=ctx.local_modules,\n                    params=params,\n                )\n\n                for tdeps, strong in (\n                    (strong_tdeps, True), (weak_tdeps, False)\n                ):\n                    pdeps: MutableSet[s_name.QualName] = set()\n                    for dep in tdeps:\n                        # ignore std module dependencies\n                        if dep.get_module_name() not in s_schema.STD_MODULES:\n                            # First check if the dep is a pointer that's\n                            # defined explicitly. If it's not explicitly\n                            # defined, check for ancestors and use them\n                            # instead.\n                            #\n                            # FIXME: Ideally we should use the closest\n                            # ancestor, instead of all of them, but\n                            # including all is still correct.\n                            if '@' in dep.name:\n                                pdeps |= _get_pointer_deps(dep, ctx=ctx)\n                            else:\n                                pdeps.add(dep)\n\n                    # Handle the pre-processed deps now.\n                    cdeps = deps if strong else weak_deps\n                    for dep in pdeps:\n                        cdeps.add(dep)\n\n                        if isinstance(\n                                decl, (qlast.CreateAlias, qlast.CreateGlobal)):\n                            # If the declaration is a view, we need to be\n                            # dependent on all the types and their props\n                            # used in the view.\n                            vdeps = {dep} | ctx.ancestors.get(dep, set())\n                            for vdep in vdeps:\n                                cdeps |= ctx.defdeps.get(vdep, set())\n\n                        if (\n                            isinstance(decl, (\n                                qlast.CreateConcretePointer,\n                                qlast.CreateGlobal))\n                            and isinstance(decl.target, qlast.Expr)\n                        ) or isinstance(\n                            decl, (\n                                qlast.CreateAccessPolicy, qlast.CreateTrigger)\n                        ):\n                            # If the declaration is a computable pointer/global\n                            # or access policy (XXX: trigger?),\n                            # we need to include the\n                            # possible constraints for every dependency\n                            # that it lists. This is so that any other\n                            # links/props that this computable uses has\n                            # all of their constraints defined before the\n                            # computable and the cardinality can be\n                            # inferred correctly.\n                            con_deps = {dep} | ctx.ancestors.get(dep, set())\n                            for con_dep in con_deps:\n                                cdeps |= ctx.constraints.get(con_dep, set())\n            else:\n                raise AssertionError(f'unexpected dependency type: {expr!r}')\n\n    orig_op.commands = commands\n\n    if loop_control:\n        parent_node = ctx.ddlgraph[loop_control]\n        parent_node.loop_control.add(fq_name)\n\n    node.deps |= deps\n    node.weak_deps |= weak_deps - {fq_name}\n\n\ndef _get_pointer_deps(\n    pointer: s_name.QualName,\n    *,\n    ctx: DepTraceContext,\n) -> MutableSet[s_name.QualName]:\n    result: MutableSet[s_name.QualName] = set()\n    owner_name, ptr_name = pointer.name.split('@', 1)\n    # For every ancestor of the type, where\n    # the pointer is defined, see if there are\n    # ancestors of the pointer itself defined.\n    for tansc in ctx.ancestors.get(\n            s_name.QualName(\n                module=pointer.module, name=owner_name\n            ), set()):\n        ptr_ansc = s_name.QualName(\n            module=tansc.module,\n            name=f'{tansc.name}@{ptr_name}',\n        )\n\n        # Only add the pointer's ancestor if\n        # it is explicitly defined.\n        if ptr_ansc in ctx.objects:\n            result.add(ptr_ansc)\n\n    # Only add the pointer if it is explicitly defined.\n    if pointer in ctx.objects:\n        result.add(pointer)\n\n    # HACK: Add all pointers that have this pointer (link, actually)\n    # as their prefix. As a rule, the assumption is that depending on\n    # a link typically comes as a package of depending on the link's\n    # property.\n    # This will *also* grab any constraints on the pointer, which\n    # is is important for properly doing cardinality inference\n    # on expressions involving it.\n    # PERF: We should avoid actually searching all the objects.\n    for propname, prop in ctx.objects.items():\n        if (\n            str(propname).startswith(str(pointer) + '@')\n            and not isinstance(prop, qltracer.Field)\n        ):\n            result.add(propname)\n\n    return result\n\n\ndef _get_hard_deps(\n    expr: qlast.TypeExpr, *, ctx: DepTraceContext\n) -> MutableSet[s_name.QualName]:\n    deps: MutableSet[s_name.QualName] = set()\n\n    if isinstance(expr, qlast.TypeName):\n\n        # Special case for `enum<VariantA, VariantB>`\n        # Don't trace at all, neither `enum` or `VariantA` are resolvable names.\n        # This case will fail later, saying that you need to declare a new type.\n        if qlutils.is_enum(expr):\n            return deps\n\n        # We care about subtypes dependencies, because\n        # they can either be custom scalars or illegal\n        # ObjectTypes (then error message will depend on\n        # dependency tracing)\n        if expr.subtypes:\n            for subtype in expr.subtypes:\n                deps |= _get_hard_deps(subtype, ctx=ctx)\n\n        else:\n            # Base case.\n            name = ctx.get_ref_name(expr.maintype)\n            if name.get_module_name() not in s_schema.STD_MODULES:\n                deps.add(name)\n\n    elif isinstance(expr, qlast.TypeExprLiteral):\n        pass\n\n    elif isinstance(expr, qlast.TypeOf):\n        # TODO: maybe we should also recurse into the inner expr?\n        pass\n\n    elif isinstance(expr, qlast.TypeOp):\n        deps |= _get_hard_deps(expr.left, ctx=ctx)\n        deps |= _get_hard_deps(expr.right, ctx=ctx)\n\n    return deps\n\n\ndef _get_bases(\n    decl: qlast.CreateObject, *, ctx: LayoutTraceContext\n) -> list[s_name.QualName]:\n    \"\"\"Resolve object bases from the \"extends\" declaration.\"\"\"\n    if not isinstance(decl, qlast.BasedOn):\n        return []\n\n    bases = []\n\n    if decl.bases:\n        # Explicit inheritance\n        has_enums = any(qlutils.is_enum(br) for br in decl.bases)\n\n        if has_enums:\n            if len(decl.bases) > 1:\n                raise errors.SchemaError(\n                    f\"invalid scalar type definition, enumeration must \"\n                    f\"be the only supertype specified\",\n                    span=decl.bases[0].span,\n                )\n\n            bases = [s_name.QualName(\"std\", \"anyenum\")]\n\n        else:\n            for base_ref in decl.bases:\n                # Validate that the base actually exists.\n                tracer_type = _get_tracer_type(decl)\n                assert tracer_type is not None\n                obj = _resolve_type_name(\n                    base_ref.maintype,\n                    tracer_type=tracer_type,\n                    ctx=ctx\n                )\n                name = obj.get_name(ctx.schema)\n                if not isinstance(name, s_name.QualName):\n                    qname = s_name.QualName.from_string(name.name)\n                else:\n                    qname = name\n                bases.append(qname)\n\n    return bases\n\n\ndef _resolve_type_expr(\n    texpr: qlast.TypeExpr,\n    *,\n    ctx: LayoutTraceContext | DepTraceContext,\n) -> qltracer.TypeLike:\n\n    if isinstance(texpr, qlast.TypeName):\n        if texpr.subtypes:\n            return qltracer.Type(\n                name=s_name.QualName(module='__coll__', name=texpr.name or ''),\n            )\n        else:\n            return cast(\n                qltracer.TypeLike,\n                _resolve_type_name(\n                    texpr.maintype,\n                    tracer_type=qltracer.Type,\n                    ctx=ctx,\n                )\n            )\n\n    elif isinstance(texpr, qlast.TypeOp):\n\n        if texpr.op == qlast.TypeOpName.OR:\n            return qltracer.UnionType([\n                _resolve_type_expr(texpr.left, ctx=ctx),\n                _resolve_type_expr(texpr.right, ctx=ctx),\n            ])\n\n        if texpr.op == qlast.TypeOpName.AND:\n            return qltracer.IntersectionType([\n                _resolve_type_expr(texpr.left, ctx=ctx),\n                _resolve_type_expr(texpr.right, ctx=ctx),\n            ])\n\n        else:\n            raise NotImplementedError(\n                f'unsupported type operation: {texpr.op}')\n\n    else:\n        raise NotImplementedError(\n            f'unsupported type expression: {texpr!r}'\n        )\n\n\nTRACER_TO_REAL_TYPE_MAP = {\n    qltracer.Type: s_types.Type,\n    qltracer.ObjectType: s_objtypes.ObjectType,\n    qltracer.ScalarType: s_scalars.ScalarType,\n    qltracer.Constraint: s_constr.Constraint,\n    qltracer.Annotation: s_anno.Annotation,\n    qltracer.Property: s_props.Property,\n    qltracer.Link: s_links.Link,\n    qltracer.Index: s_indexes.Index,\n}\n\n\ndef _get_local_obj(\n    refname: s_name.QualName,\n    tracer_type: type[qltracer.NamedObject],\n    span: Optional[parsing.Span],\n    *,\n    ctx: LayoutTraceContext | DepTraceContext,\n) -> Optional[qltracer.NamedObject]:\n\n    obj = ctx.objects.get(refname)\n\n    if isinstance(obj, s_pseudo.PseudoType):\n        raise errors.SchemaError(\n            f'invalid type: {obj.get_verbosename(ctx.schema)} is a generic '\n            f'type and they are not supported in user-defined schema',\n            span=span,\n        )\n\n    elif obj is not None and not isinstance(obj, tracer_type):\n        obj_type = TRACER_TO_REAL_TYPE_MAP[type(obj)]\n        real_type = TRACER_TO_REAL_TYPE_MAP[tracer_type]\n        raise errors.InvalidReferenceError(\n            f'{str(refname)!r} exists, but is '\n            f'{english.add_a(obj_type.get_schema_class_displayname())}, '\n            f'not {english.add_a(real_type.get_schema_class_displayname())}',\n            span=span,\n        )\n\n    return obj\n\n\ndef _resolve_type_name(\n    ref: qlast.BaseObjectRef,\n    *,\n    tracer_type: type[qltracer.NamedObject],\n    ctx: LayoutTraceContext | DepTraceContext,\n) -> qltracer.ObjectLike:\n\n    refname = ctx.get_ref_name(ref)\n    local_obj = _get_local_obj(refname, tracer_type, ref.span, ctx=ctx)\n    obj: qltracer.ObjectLike\n    if local_obj is not None:\n        obj = local_obj\n    else:\n        obj = _resolve_schema_ref(\n            refname,\n            type=tracer_type,\n            span=ref.span,\n            ctx=ctx,\n        )\n\n    return obj\n\n\ndef _get_tracer_type(\n    decl: qlast.CreateObject,\n) -> Optional[type[qltracer.NamedObject]]:\n\n    tracer_type: Optional[type[qltracer.NamedObject]] = None\n\n    if isinstance(decl, qlast.CreateObjectType):\n        tracer_type = qltracer.ObjectType\n    elif isinstance(decl, qlast.CreateScalarType):\n        tracer_type = qltracer.ScalarType\n    elif isinstance(decl, (qlast.CreateConstraint,\n                           qlast.CreateConcreteConstraint)):\n        tracer_type = qltracer.Constraint\n    elif isinstance(decl, (qlast.CreateAnnotation,\n                           qlast.CreateAnnotationValue)):\n        tracer_type = qltracer.Annotation\n    elif isinstance(decl, qlast.CreateConcreteUnknownPointer):\n        tracer_type = qltracer.Pointer\n    elif isinstance(decl, (qlast.CreateProperty,\n                           qlast.CreateConcreteProperty)):\n        tracer_type = qltracer.Property\n    elif isinstance(decl, (qlast.CreateLink,\n                           qlast.CreateConcreteLink)):\n        tracer_type = qltracer.Link\n    elif isinstance(decl, qlast.CreatePermission):\n        tracer_type = qltracer.Permission\n    elif isinstance(decl, (qlast.CreateIndex,\n                           qlast.CreateConcreteIndex)):\n        tracer_type = qltracer.Index\n\n    return tracer_type\n\n\ndef _validate_schema_ref(\n    decl: qlast.CreateObject,\n    *,\n    ctx: LayoutTraceContext,\n) -> None:\n    refname = ctx.get_ref_name(decl.name)\n    tracer_type = _get_tracer_type(decl)\n    if tracer_type is None:\n        # Bail out and rely on some other validation mechanism\n        return\n\n    local_obj = _get_local_obj(refname, tracer_type, decl.span, ctx=ctx)\n\n    if local_obj is None:\n        if (tracer_type is qltracer.Index and\n                refname == s_indexes.DEFAULT_INDEX):\n            return\n\n        _resolve_schema_ref(\n            refname,\n            type=tracer_type,\n            span=decl.span,\n            ctx=ctx,\n        )\n\n\ndef _resolve_schema_ref(\n    name: s_name.Name,\n    type: type[qltracer.NamedObject],\n    span: Optional[parsing.Span],\n    *,\n    ctx: LayoutTraceContext | DepTraceContext,\n) -> s_obj.SubclassableObject:\n    real_type = TRACER_TO_REAL_TYPE_MAP[type]\n    try:\n        return ctx.schema.get(name, type=real_type, span=span)\n    except errors.InvalidReferenceError as e:\n        s_utils.enrich_schema_lookup_error(\n            e,\n            name,\n            schema=ctx.schema,\n            modaliases=ctx.modaliases,\n            item_type=real_type,\n            span=span,\n        )\n        raise\n\n\ndef _get_expr_field(decl: qlast.DDLOperation) -> Optional[qlast.Expr]:\n    for cmd in decl.commands:\n        if isinstance(cmd, qlast.SetField) and cmd.name == \"expr\":\n            assert cmd.value, \"sdl SetField should always have value\"\n            assert isinstance(cmd.value, qlast.Expr)\n            return cmd.value\n    return None\n"
  },
  {
    "path": "edb/edgeql/desugar_group.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Desugar GROUP queries into internal FOR GROUP queries.\n\nThis code is called by both the model and the real implementation,\nthough if that starts becoming a problem it should just be abandoned.\n\"\"\"\n\nfrom __future__ import annotations\n\n\nfrom typing import Optional, AbstractSet\n\nfrom edb import errors\n\nfrom edb.common import ast\nfrom edb.common import ordered\nfrom edb.common.compiler import AliasGenerator\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql.compiler import astutils\n\n\ndef key_name(s: str) -> str:\n    return s.split('~')[0]\n\n\ndef name_path(name: str) -> qlast.Path:\n    return qlast.Path(steps=[qlast.ObjectRef(name=name)])\n\n\ndef make_free_object(els: dict[str, qlast.Expr]) -> qlast.Shape:\n    return qlast.Shape(\n        expr=None,\n        elements=[\n            qlast.ShapeElement(\n                expr=qlast.Path(steps=[qlast.Ptr(name=name)]),\n                compexpr=expr\n            )\n            for name, expr in els.items()\n        ],\n    )\n\n\ndef collect_grouping_atoms(\n    els: list[qlast.GroupingElement],\n) -> AbstractSet[str]:\n    atoms: ordered.OrderedSet[str] = ordered.OrderedSet()\n\n    def _collect_atom(el: qlast.GroupingAtom) -> None:\n        if isinstance(el, qlast.GroupingIdentList):\n            for at in el.elements:\n                _collect_atom(at)\n\n        else:\n            assert isinstance(el, qlast.ObjectRef)\n            atoms.add(el.name)\n\n    def _collect_el(el: qlast.GroupingElement) -> None:\n        if isinstance(el, qlast.GroupingSets):\n            for sub in el.sets:\n                _collect_el(sub)\n        elif isinstance(el, qlast.GroupingOperation):\n            for at in el.elements:\n                _collect_atom(at)\n        elif isinstance(el, qlast.GroupingSimple):\n            _collect_atom(el.element)\n        else:\n            raise AssertionError('Unknown GroupingElement')\n\n    for el in els:\n        _collect_el(el)\n\n    return atoms\n\n\ndef desugar_group(\n    node: qlast.GroupQuery,\n    aliases: AliasGenerator,\n) -> qlast.InternalGroupQuery:\n    assert not isinstance(node, qlast.InternalGroupQuery)\n    by_alias_map: dict[str, tuple[str, qlast.Path]] = {}\n\n    def rewrite_atom(el: qlast.GroupingAtom) -> qlast.GroupingAtom:\n        if isinstance(el, qlast.ObjectRef):\n            return el\n        elif isinstance(el, qlast.Path):\n            assert isinstance(el.steps[0], qlast.Ptr)\n            ptrname = el.steps[0].name\n            ptrtype = el.steps[0].type\n            if ptrname not in by_alias_map:\n                alias = aliases.get(ptrname)\n                by_alias_map[ptrname] = (alias, el)\n            else:\n                alias = by_alias_map[ptrname][0]\n                aliased_el = by_alias_map[ptrname][1]\n                assert isinstance(aliased_el.steps[0], qlast.Ptr)\n                aliased_el_ptrtype = aliased_el.steps[0].type\n                if ptrtype != aliased_el_ptrtype:\n                    raise errors.QueryError(\n                        f\"BY clause cannot refer to link property and object \"\n                        f\"property with the same name\",\n                        span=el.span,\n                    )\n            return qlast.ObjectRef(name=alias)\n        else:\n            assert isinstance(el, qlast.GroupingIdentList)\n            return qlast.GroupingIdentList(\n                span=el.span,\n                elements=tuple(rewrite_atom(at) for at in el.elements),\n            )\n\n    def rewrite(el: qlast.GroupingElement) -> qlast.GroupingElement:\n        if isinstance(el, qlast.GroupingSimple):\n            return qlast.GroupingSimple(\n                span=el.span, element=rewrite_atom(el.element))\n        elif isinstance(el, qlast.GroupingSets):\n            return qlast.GroupingSets(\n                span=el.span, sets=[rewrite(s) for s in el.sets])\n        elif isinstance(el, qlast.GroupingOperation):\n            return qlast.GroupingOperation(\n                span=el.span,\n                oper=el.oper,\n                elements=[rewrite_atom(a) for a in el.elements])\n        raise AssertionError\n\n    # The rewrite calls on the grouping elements populate alias_map\n    # with any bindings for pointers the by clause refers to directly.\n    by = [rewrite(by_el) for by_el in node.by]\n\n    alias_map: dict[str, tuple[str, qlast.Expr]] = {\n        k: v for k, v in by_alias_map.items()\n    }\n\n    for using_clause in (node.using or ()):\n        if using_clause.alias in alias_map:\n            # TODO: This would be a great place to allow multiple spans!\n            raise errors.QueryError(\n                f\"USING clause binds a variable '{using_clause.alias}' \"\n                f\"but a property with that name is used directly in the BY \"\n                f\"clause\",\n                span=alias_map[using_clause.alias][1].span,\n            )\n        alias_map[using_clause.alias] = (using_clause.alias, using_clause.expr)\n\n    using = []\n    for alias, path in alias_map.values():\n        using.append(qlast.AliasedExpr(alias=alias, expr=path))\n\n    actual_keys = collect_grouping_atoms(by)\n\n    g_alias = aliases.get('g')\n    grouping_alias = aliases.get('grouping')\n    output_dict = {\n        'key': make_free_object({\n            name: name_path(alias)\n            for name, (alias, _) in alias_map.items()\n            if alias in actual_keys\n        }),\n        'grouping': qlast.FunctionCall(\n            func='array_unpack',\n            args=[name_path(grouping_alias)],\n        ),\n        'elements': name_path(g_alias),\n    }\n    output_shape = make_free_object(output_dict)\n\n    return qlast.InternalGroupQuery(\n        span=node.span,\n        aliases=node.aliases,\n        subject_alias=node.subject_alias,\n        subject=node.subject,\n        # rewritten parts!\n        using=using,\n        by=by,\n        group_alias=g_alias,\n        grouping_alias=grouping_alias,\n        result=output_shape,\n        from_desugaring=True,\n    )\n\n\ndef _count_alias_uses(\n    node: qlast.Expr,\n    alias: str,\n) -> int:\n    uses = 0\n    for child in ast.find_children(node, qlast.Path):\n        match child:\n            case astutils.alias_view((alias2, _)) if alias == alias2:\n                uses += 1\n    return uses\n\n\ndef try_group_rewrite(\n    node: qlast.Query,\n    aliases: AliasGenerator,\n) -> Optional[qlast.Query]:\n    \"\"\"\n    Try to apply some syntactic rewrites of GROUP expressions so we\n    can generate better code.\n\n    The two key desugarings are:\n\n    * Sink a shape into the internal group result\n\n        SELECT (GROUP ...) <shape>\n        [filter-clause] [order-clause] [other clauses]\n        =>\n        SELECT (\n          FOR GROUP ...\n          UNION <igroup-body> <shape>\n          [filter-clause]\n          [order-clause]\n        ) [other clauses]\n\n    * Convert a FOR over a group into just an internal group (and\n      a trivial FOR)\n\n        FOR g in (GROUP ...) UNION <body>\n        =>\n        FOR GROUP ...\n        UNION (\n            FOR g IN (<group-body>)\n            UNION <body>\n        )\n    \"\"\"\n\n    # Inline trivial uses of aliases bound to a group and then\n    # immediately used, so that we can apply the other optimizations.\n    match node:\n        case qlast.SelectQuery(\n            aliases=[\n                *_,\n                qlast.AliasedExpr(alias=alias, expr=qlast.GroupQuery() as grp)\n            ] as qaliases,\n            result=qlast.Shape(\n                expr=astutils.alias_view((alias2, [])),\n                elements=elements,\n            ) as result,\n        ) if alias == alias2 and _count_alias_uses(result, alias) == 1:\n            node = node.replace(\n                aliases=qaliases[:-1],\n                result=qlast.Shape(expr=grp, elements=elements),\n            )\n\n        case qlast.ForQuery(\n            aliases=[\n                *_,\n                qlast.AliasedExpr(alias=alias, expr=qlast.GroupQuery() as grp)\n            ] as qaliases,\n            iterator=astutils.alias_view((alias2, [])),\n            result=result,\n        ) if alias == alias2 and _count_alias_uses(result, alias) == 0:\n            node = node.replace(\n                aliases=qaliases[:-1],\n                iterator=grp,\n            )\n\n    # Sink shapes into the GROUP\n    if (\n        isinstance(node, qlast.SelectQuery)\n        and isinstance(node.result, qlast.Shape)\n        and isinstance(node.result.expr, qlast.GroupQuery)\n    ):\n        igroup = desugar_group(node.result.expr, aliases)\n        igroup = igroup.replace(result=qlast.Shape(\n            expr=igroup.result, elements=node.result.elements))\n\n        # FILTER gets sunk into the body of the FOR GROUP\n        if node.where or node.orderby:\n            igroup = igroup.replace(\n                # We need to move the result_alias in case\n                # the FILTER depends on it.\n                result_alias=node.result_alias,\n                where=node.where,\n                orderby=node.orderby,\n            )\n\n        return node.replace(\n            result=igroup, result_alias=None, where=None, orderby=None)\n\n    # Eliminate FORs over GROUPs\n    if (\n        isinstance(node, qlast.ForQuery)\n        and isinstance(node.iterator, qlast.GroupQuery)\n    ):\n        igroup = desugar_group(node.iterator, aliases)\n        new_result = qlast.ForQuery(\n            iterator_alias=node.iterator_alias,\n            iterator=igroup.result,\n            result=node.result,\n        )\n        return igroup.replace(result=new_result, aliases=node.aliases)\n\n    return None\n"
  },
  {
    "path": "edb/edgeql/parser/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Any, Callable, Optional, Mapping\nimport pathlib\n\nfrom edb import errors\nfrom edb.common import parsing\n\nimport edb._edgeql_parser as rust_parser\n\nfrom .grammar import tokens\n\nfrom .. import ast as qlast\nfrom .. import tokenizer as qltokenizer\n\n\nSPEC_LOADED = False\n\n\ndef append_module_aliases(\n    command: qlast.Command, aliases: Mapping[Optional[str], str]\n):\n    modaliases: list[qlast.Alias] = []\n    for alias, module in aliases.items():\n        decl = qlast.ModuleAliasDecl(module=module, alias=alias)\n        modaliases.append(decl)\n\n    if not command.aliases:\n        command.aliases = modaliases\n    else:\n        command.aliases = modaliases + command.aliases\n\n\ndef parse_fragment(\n    source: qltokenizer.Source | str,\n    filename: Optional[str] = None,\n) -> qlast.Expr:\n    res = parse(tokens.T_STARTFRAGMENT, source, filename=filename)\n    assert isinstance(res, qlast.Expr)\n    return res\n\n\ndef parse_query(\n    source: qltokenizer.Source | str,\n    module_aliases: Optional[Mapping[Optional[str], str]] = None,\n) -> qlast.Query:\n    \"\"\"Parse some EdgeQL potentially adding some module aliases.\n\n    This will parse EdgeQL queries and expressions. If the source is an\n    expression, the result will be wrapped into a SelectQuery.\n    \"\"\"\n\n    tree = parse_fragment(source)\n    if not isinstance(tree, qlast.Query):\n        tree = qlast.SelectQuery(result=tree)\n\n    if module_aliases:\n        append_module_aliases(tree, module_aliases)\n\n    return tree\n\n\ndef parse_block(\n    source: qltokenizer.Source | str,\n    module_aliases: Optional[Mapping[Optional[str], str]] = None,\n) -> list[qlast.Command]:\n    node = parse(tokens.T_STARTBLOCK, source)\n    assert isinstance(node, qlast.Commands), node\n    if module_aliases:\n        for command in node.commands:\n            append_module_aliases(command, module_aliases)\n    return node.commands\n\n\ndef parse_migration_body_block(\n    source: str,\n) -> tuple[qlast.NestedQLBlock, list[qlast.SetField]]:\n    # For parser-internal technical reasons, we don't have a\n    # production that means \"just the *inside* of a migration block\n    # (without braces)\", so we just hack around this by adding braces.\n    # This is only really workable because we only use this in a place\n    # where the source contexts don't matter anyway.\n    return parse(tokens.T_STARTMIGRATION, f\"{{{source}}}\")\n\n\ndef parse_extension_package_body_block(\n    source: str,\n) -> tuple[qlast.NestedQLBlock, list[qlast.SetField]]:\n    # For parser-internal technical reasons, we don't have a\n    # production that means \"just the *inside* of a migration block\n    # (without braces)\", so we just hack around this by adding braces.\n    # This is only really workable because we only use this in a place\n    # where the source contexts don't matter anyway.\n    return parse(tokens.T_STARTEXTENSION, f\"{{{source}}}\")\n\n\ndef parse_sdl(expr: str):\n    return parse(tokens.T_STARTSDLDOCUMENT, expr)\n\n\ndef parse(\n    start_token: type[tokens.Token],\n    source: str | qltokenizer.Source,\n    filename: Optional[str] = None,\n):\n    if not SPEC_LOADED:\n        preload_spec()\n\n    if isinstance(source, str):\n        source = qltokenizer.Source.from_string(source)\n\n    start_name = start_token.__name__[2:]\n    result, productions = rust_parser.parse(start_name, source.tokens())\n\n    if len(result.errors) > 0:\n        # TODO: emit multiple errors\n\n        # Heuristic to pick the error:\n        # - the only Unexpected, if it is a keyword\n        # - first encountered,\n        # - Unexpected before Missing,\n        # - original order.\n        errs = result.errors\n        unexpected = [e for e in errs if e[0].startswith('Unexpected')]\n        if len(unexpected) == 1 and unexpected[0][0].startswith(\n            'Unexpected keyword'\n        ):\n            error = unexpected[0]\n        else:\n            errs.sort(key=lambda e: (e[1][0], -ord(e[0][1])))\n            error = errs[0]\n\n        message, span, hint, details = error\n        position = qltokenizer.inflate_position(source.text(), span)\n\n        parsing_span = parsing.Span(\n            'query',\n            source.text(),\n            start=position[2],\n            end=position[3] or position[2],\n            context_lines=10,\n        )\n        raise errors.EdgeQLSyntaxError(\n            message,\n            position=position,\n            hint=hint,\n            details=details,\n            span=parsing_span,\n        )\n\n    assert isinstance(result.out, rust_parser.CSTNode)\n    return _cst_to_ast(\n        result.out,\n        productions,\n        source,\n        filename,\n    ).val\n\n\ndef _cst_to_ast(\n    cst: rust_parser.CSTNode,\n    productions: list[tuple[type, Callable]],\n    source: qltokenizer.Source,\n    filename: Optional[str],\n) -> Any:\n    # Converts CST into AST by calling methods from the grammar classes.\n    #\n    # This function was originally written as a simple recursion.\n    # Then I had to unfold it, because it was hitting recursion limit.\n    # Stack here contains all remaining things to do:\n    # - CST node means the node has to be processed and pushed onto the\n    #   result stack,\n    # - production means that all args of production have been processed\n    #   are are ready to be passed to the production method. The result is\n    #   obviously pushed onto the result stack\n\n    stack: list[rust_parser.CSTNode | rust_parser.Production] = [cst]\n    result: list[Any] = []\n\n    while len(stack) > 0:\n        node = stack.pop()\n\n        if isinstance(node, rust_parser.CSTNode):\n            # this would be the body of the original recursion function\n\n            if terminal := node.terminal:\n                # Terminal is simple: just convert to parsing.Token\n                span = parsing.Span(\n                    filename=filename,\n                    buffer=source.text(),\n                    start=terminal.start,\n                    end=terminal.end,\n                )\n                result.append(\n                    parsing.Token(terminal.text, terminal.value, span)\n                )\n\n            elif production := node.production:\n                # Production needs to first process all args, then\n                # call the appropriate method.\n                # (this is all in reverse, because stacks)\n                stack.append(production)\n                args = list(production.args)\n                args.reverse()\n                stack.extend(args)\n            else:\n                raise NotImplementedError(node)\n\n        elif isinstance(node, rust_parser.Production):\n            # production args are done, get them out of result stack\n            len_args = len(node.args)\n            split_at = len(result) - len_args\n            args = result[split_at:]\n            result = result[0:split_at]\n\n            # find correct method to call\n            production_id = node.id\n            non_term_type, method = productions[production_id]\n            sym = non_term_type()\n\n            # init the span onto the Nonterm object, so it can be accessed by\n            # production methods to construct nodes\n            if node.start is not None and node.end is not None:\n                sym.span = parsing.Span(\n                    filename=filename,\n                    buffer=source.text(),\n                    start=node.start,\n                    end=node.end,\n                )\n            else:\n                sym.span = None\n\n            method(sym, *args)\n\n            # a helper to set the span of each constructed node, so we don't\n            # have to manually set the span things assigned to nonterm.val\n            if sym.span and isinstance(sym.val, qlast.Base):\n                sym.val.span = sym.span\n\n            # push into result stack\n            result.append(sym)\n\n    return result.pop()\n\n\ndef preload_spec() -> None:\n    global SPEC_LOADED\n    path = get_spec_filepath()\n    rust_parser.preload_spec(path)\n    SPEC_LOADED = True\n\n\ndef get_spec_filepath():\n    \"Returns an absolute path to the serialized grammar spec file\"\n\n    edgeql_dir = pathlib.Path(__file__).parent.parent\n    return str(edgeql_dir / 'grammar.bc')\n"
  },
  {
    "path": "edb/edgeql/parser/grammar/.gitignore",
    "content": "*.log\n*.pickle\n*.dot\n"
  },
  {
    "path": "edb/edgeql/parser/grammar/__init__.py",
    "content": "##\n# Copyright (c) 2015-present MagicStack Inc.\n# All rights reserved.\n#\n# See LICENSE for details.\n##\n\nfrom __future__ import annotations\n\nfrom . import start as start  # noqa\n"
  },
  {
    "path": "edb/edgeql/parser/grammar/commondl.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport sys\nimport types\nimport typing\n\n\nfrom edb.errors import EdgeQLSyntaxError\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom edb.common import parsing\nfrom edb.common import verutils\n\nfrom . import expressions\nfrom . import tokens\n\nfrom .precedence import *  # NOQA\nfrom .tokens import *  # NOQA\nfrom .expressions import *  # NOQA\n\n\nNonterm = expressions.Nonterm  # type: ignore[misc]\n\n\ndef _parse_language(node):\n    lang = node.val.upper()\n    if lang == 'EDGEQL':\n        return qlast.Language.EdgeQL\n    if lang == 'SQL':\n        return qlast.Language.SQL\n    raise EdgeQLSyntaxError(\n        f'{node.val} is not a valid language',\n        span=node.span) from None\n\n\ndef _validate_declarations(\n    declarations: typing.Sequence[\n        qlast.ModuleDeclaration | qlast.ObjectDDL]\n) -> None:\n    # Check that top-level declarations either use fully-qualified\n    # names or are module blocks.\n    for decl in declarations:\n        if (\n            not isinstance(\n                decl,\n                (qlast.ModuleDeclaration, qlast.ExtensionCommand,\n                 qlast.FutureCommand)\n            ) and decl.name.module is None\n        ):\n            raise EdgeQLSyntaxError(\n                \"only fully-qualified name is allowed in \"\n                \"top-level declaration\",\n                span=decl.name.span)\n\n\ndef extract_bases(bases, commands):\n    vbases = bases\n    vcommands = []\n    for command in commands:\n        if isinstance(command, qlast.AlterAddInherit):\n            if vbases:\n                raise EdgeQLSyntaxError(\n                    \"specifying EXTENDING twice is not allowed\",\n                    span=command.span)\n            vbases = command.bases\n        else:\n            vcommands.append(command)\n    return vbases, vcommands\n\n\nclass NewNontermHelper:\n    def __init__(self, modname):\n        self.name = modname\n\n    def _new_nonterm(\n        self, clsname, clsdict=None, clskwds=None, clsbases=(Nonterm,)\n    ):\n        if clsdict is None:\n            clsdict = {}\n        if clskwds is None:\n            clskwds = {}\n        mod = sys.modules[self.name]\n\n        def clsexec(ns):\n            ns['__module__'] = self.name\n            for k, v in clsdict.items():\n                ns[k] = v\n            return ns\n\n        cls = types.new_class(clsname, clsbases, clskwds, clsexec)\n        setattr(mod, clsname, cls)\n        return cls\n\n\nclass Semicolons(Nonterm):\n    # one or more semicolons\n    @parsing.inline(0)\n    def reduce_SEMICOLON(self, tok):\n        pass\n\n    @parsing.inline(0)\n    def reduce_Semicolons_SEMICOLON(self, semicolons, semicolon):\n        pass\n\n\nclass OptSemicolons(Nonterm):\n    @parsing.inline(0)\n    def reduce_Semicolons(self, semicolons):\n        pass\n\n    def reduce_empty(self):\n        self.val = None\n\n\nclass ExtendingSimple(Nonterm):\n    @parsing.inline(1)\n    def reduce_EXTENDING_SimpleTypeNameList(self, _, list):\n        pass\n\n\nclass OptExtendingSimple(Nonterm):\n    @parsing.inline(0)\n    def reduce_ExtendingSimple(self, extending):\n        pass\n\n    def reduce_empty(self):\n        self.val = []\n\n\nclass Extending(Nonterm):\n    @parsing.inline(1)\n    def reduce_EXTENDING_TypeNameList(self, _, list):\n        pass\n\n\nclass OptExtending(Nonterm):\n    @parsing.inline(0)\n    def reduce_Extending(self, extending):\n        pass\n\n    def reduce_empty(self):\n        self.val = []\n\n\nclass CreateSimpleExtending(Nonterm):\n    def reduce_EXTENDING_SimpleTypeNameList(self, *kids):\n        self.val = qlast.AlterAddInherit(bases=kids[1].val)\n\n\nclass OnExpr(Nonterm):\n    # NOTE: the reason why we need parentheses around the expression\n    # is to disambiguate whether the '{' following the expression is\n    # meant to be a shape or a nested DDL/SDL block.\n    @parsing.inline(1)\n    def reduce_ON_ParenExpr(self, _, expr):\n        pass\n\n\nclass OptOnExpr(Nonterm):\n    def reduce_empty(self):\n        self.val = None\n\n    @parsing.inline(0)\n    def reduce_OnExpr(self, expr):\n        pass\n\n\nclass OptDeferred(Nonterm):\n    def reduce_empty(self):\n        self.val = None\n\n    def reduce_DEFERRED(self, _):\n        self.val = True\n\n\nclass OptExceptExpr(Nonterm):\n    def reduce_empty(self):\n        self.val = None\n\n    @parsing.inline(1)\n    def reduce_EXCEPT_ParenExpr(self, _, expr):\n        pass\n\n\nclass OptConcreteConstraintArgList(Nonterm):\n    @parsing.inline(1)\n    def reduce_LPAREN_OptPosCallArgList_RPAREN(self, _lparen, list, _rparen):\n        pass\n\n    def reduce_empty(self):\n        self.val = []\n\n\nclass OptDefault(Nonterm):\n    def reduce_empty(self):\n        self.val = None\n\n    @parsing.inline(1)\n    def reduce_EQUALS_Expr(self, _, expr):\n        pass\n\n\nclass ParameterKind(Nonterm):\n    def reduce_VARIADIC(self, *kids):\n        self.val = qltypes.ParameterKind.VariadicParam\n\n    def reduce_NAMEDONLY(self, _):\n        self.val = qltypes.ParameterKind.NamedOnlyParam\n\n\nclass OptParameterKind(Nonterm):\n    def reduce_empty(self):\n        self.val = qltypes.ParameterKind.PositionalParam\n\n    @parsing.inline(0)\n    def reduce_ParameterKind(self, *kids):\n        pass\n\n\nclass FuncDeclArgName(Nonterm):\n    def reduce_Identifier(self, dp):\n        self.val = dp.val\n        self.span = dp.span\n\n    def reduce_PARAMETER(self, dp):\n        if dp.val[1].isdigit():\n            raise EdgeQLSyntaxError(\n                f'numeric parameters are not supported',\n                span=dp.span)\n        else:\n            raise EdgeQLSyntaxError(\n                f\"function parameters do not need a $ prefix, \"\n                f\"rewrite as '{dp.val[1:]}'\",\n                span=dp.span)\n\n\nclass FuncDeclArg(Nonterm):\n    def reduce_kwarg(self, kind, name, _, typemod, type, default):\n        r\"\"\"%reduce OptParameterKind FuncDeclArgName COLON \\\n                OptTypeQualifier FullTypeExpr OptDefault \\\n        \"\"\"\n        self.val = qlast.FuncParamDecl(\n            kind=kind.val,\n            name=name.val,\n            typemod=typemod.val,\n            type=type.val,\n            default=default.val\n        )\n\n    def reduce_OptParameterKind_FuncDeclArgName_OptDefault(\n        self, kind, name, default\n    ):\n        raise EdgeQLSyntaxError(\n            f'missing type declaration for the `{name.val}` parameter',\n            span=name.span\n        )\n\n\nclass FuncDeclArgList(parsing.ListNonterm, element=FuncDeclArg,\n                      separator=tokens.T_COMMA, allow_trailing_separator=True):\n    pass\n\n\nclass FuncDeclArgs(Nonterm):\n    @parsing.inline(0)\n    def reduce_FuncDeclArgList(self, list):\n        pass\n\n\nclass ProcessFunctionParamsMixin:\n    def _validate_params(self, params):\n        last_pos_default_arg = None\n        last_named_arg = None\n        variadic_arg = None\n        names = set()\n\n        for arg in params:\n            if isinstance(arg, tuple):\n                # A tuple here means that it's part of the \"param := val\"\n                raise EdgeQLSyntaxError(\n                    f\"Unexpected ':='\",\n                    span=arg[1])\n\n            if arg.name in names:\n                raise EdgeQLSyntaxError(\n                    f'duplicate parameter name `{arg.name}`',\n                    span=arg.span)\n            names.add(arg.name)\n\n            if arg.kind is qltypes.ParameterKind.VariadicParam:\n                if variadic_arg is not None:\n                    raise EdgeQLSyntaxError(\n                        'more than one variadic argument',\n                        span=arg.span)\n                elif last_named_arg is not None:\n                    raise EdgeQLSyntaxError(\n                        f'NAMED ONLY argument `{last_named_arg.name}` '\n                        f'before VARIADIC argument `{arg.name}`',\n                        span=last_named_arg.span)\n                else:\n                    variadic_arg = arg\n\n                if arg.default is not None:\n                    raise EdgeQLSyntaxError(\n                        f'VARIADIC argument `{arg.name}` '\n                        f'cannot have a default value',\n                        span=arg.span)\n\n            elif arg.kind is qltypes.ParameterKind.NamedOnlyParam:\n                last_named_arg = arg\n\n            else:\n                if last_named_arg is not None:\n                    raise EdgeQLSyntaxError(\n                        f'positional argument `{arg.name}` '\n                        f'follows NAMED ONLY argument `{last_named_arg.name}`',\n                        span=arg.span)\n\n                if variadic_arg is not None:\n                    raise EdgeQLSyntaxError(\n                        f'positional argument `{arg.name}` '\n                        f'follows VARIADIC argument `{variadic_arg.name}`',\n                        span=arg.span)\n\n            if arg.kind is qltypes.ParameterKind.PositionalParam:\n                if arg.default is None:\n                    if last_pos_default_arg is not None:\n                        raise EdgeQLSyntaxError(\n                            f'positional argument `{arg.name}` without '\n                            f'default follows positional argument '\n                            f'`{last_pos_default_arg.name}` with default',\n                            span=arg.span)\n                else:\n                    last_pos_default_arg = arg\n\n\nclass CreateFunctionArgs(Nonterm, ProcessFunctionParamsMixin):\n    def reduce_LPAREN_RPAREN(self, _lparen, _rparen):\n        self.val = []\n\n    def reduce_LPAREN_FuncDeclArgs_RPAREN(self, _lparen, args, _rparen):\n        args = args.val\n        self._validate_params(args)\n        self.val = args\n\n\nclass OptTypeQualifier(Nonterm):\n    def reduce_SET_OF(self, _s, _o):\n        self.val = qltypes.TypeModifier.SetOfType\n\n    def reduce_OPTIONAL(self, _):\n        self.val = qltypes.TypeModifier.OptionalType\n\n    def reduce_empty(self):\n        self.val = qltypes.TypeModifier.SingletonType\n\n\nclass FunctionType(Nonterm):\n    @parsing.inline(0)\n    def reduce_FullTypeExpr(self, expr):\n        pass\n\n\nclass FromFunction(Nonterm):\n    def reduce_USING_ParenExpr(self, _, expr):\n        lang = qlast.Language.EdgeQL\n        self.val = qlast.FunctionCode(\n            language=lang,\n            nativecode=expr.val)\n\n    def reduce_USING_Identifier_BaseStringConstant(self, _, ident, const):\n        lang = _parse_language(ident)\n        code = const.val.value\n        self.val = qlast.FunctionCode(language=lang, code=code)\n\n    def reduce_USING_Identifier_FUNCTION_BaseStringConstant(\n        self, _using, ident, _function, const\n    ):\n        lang = _parse_language(ident)\n        if lang != qlast.Language.SQL:\n            raise EdgeQLSyntaxError(\n                f'{lang} language is not supported in USING FUNCTION clause',\n                span=ident.span) from None\n\n        self.val = qlast.FunctionCode(\n            language=lang,\n            from_function=const.val.value\n        )\n\n    def reduce_USING_Identifier_EXPRESSION(self, _using, ident, _expression):\n        lang = _parse_language(ident)\n        if lang != qlast.Language.SQL:\n            raise EdgeQLSyntaxError(\n                f'{lang} language is not supported in USING clause',\n                span=ident.span) from None\n\n        self.val = qlast.FunctionCode(language=lang)\n\n\nclass ProcessFunctionBlockMixin:\n    span: parsing.Span\n\n    def _process_function_body(self, block, *, optional_using: bool=False):\n        props: dict[str, typing.Any] = {}\n\n        commands = []\n        code = None\n        nativecode = None\n        language = qlast.Language.EdgeQL\n        from_expr = False\n        from_function = None\n\n        for node in block.val:\n            if isinstance(node, qlast.FunctionCode):\n                if node.from_function:\n                    if from_function is not None:\n                        raise EdgeQLSyntaxError(\n                            'more than one USING FUNCTION clause',\n                            span=node.span)\n                    from_function = node.from_function\n                    language = qlast.Language.SQL\n\n                elif node.nativecode:\n                    if code is not None or nativecode is not None:\n                        raise EdgeQLSyntaxError(\n                            'more than one USING <code> clause',\n                            span=node.span)\n                    nativecode = node.nativecode\n                    language = node.language\n\n                elif node.code:\n                    if code is not None or nativecode is not None:\n                        raise EdgeQLSyntaxError(\n                            'more than one USING <code> clause',\n                            span=node.span)\n                    code = node.code\n                    language = node.language\n\n                else:\n                    # USING SQL EXPRESSION\n                    from_expr = True\n                    language = qlast.Language.SQL\n            else:\n                commands.append(node)\n\n        if (\n            nativecode is None and\n            code is None and\n            from_function is None and\n            not from_expr and\n            not optional_using\n        ):\n            raise EdgeQLSyntaxError(\n                'missing a USING clause',\n                span=block.span)\n\n        else:\n            if from_expr and (from_function or code):\n                raise EdgeQLSyntaxError(\n                    'USING SQL EXPRESSION is mutually exclusive with other '\n                    'USING variants',\n                    span=block.span)\n\n            props['code'] = qlast.FunctionCode(\n                language=language,\n                from_function=from_function,\n                from_expr=from_expr,\n                code=code,\n                span=self.span,\n            )\n\n            props['nativecode'] = nativecode\n\n        if commands:\n            props['commands'] = commands\n\n        return props\n\n\n#\n# CREATE TYPE ... { CREATE LINK ... { ON TARGET DELETE ...\n#\nclass OnTargetDeleteStmt(Nonterm):\n    def reduce_ON_TARGET_DELETE_RESTRICT(self, *_):\n        self.val = qlast.OnTargetDelete(\n            cascade=qltypes.LinkTargetDeleteAction.Restrict)\n\n    def reduce_ON_TARGET_DELETE_DELETE_SOURCE(self, *_):\n        self.val = qlast.OnTargetDelete(\n            cascade=qltypes.LinkTargetDeleteAction.DeleteSource)\n\n    def reduce_ON_TARGET_DELETE_ALLOW(self, *_):\n        self.val = qlast.OnTargetDelete(\n            cascade=qltypes.LinkTargetDeleteAction.Allow)\n\n    def reduce_ON_TARGET_DELETE_DEFERRED_RESTRICT(self, *_):\n        self.val = qlast.OnTargetDelete(\n            cascade=qltypes.LinkTargetDeleteAction.DeferredRestrict)\n\n\nclass OnSourceDeleteStmt(Nonterm):\n    def reduce_ON_SOURCE_DELETE_DELETE_TARGET(self, *_):\n        self.val = qlast.OnSourceDelete(\n            cascade=qltypes.LinkSourceDeleteAction.DeleteTarget)\n\n    def reduce_ON_SOURCE_DELETE_ALLOW(self, *_):\n        self.val = qlast.OnSourceDelete(\n            cascade=qltypes.LinkSourceDeleteAction.Allow)\n\n    def reduce_ON_SOURCE_DELETE_DELETE_TARGET_IF_ORPHAN(self, *_):\n        self.val = qlast.OnSourceDelete(\n            cascade=qltypes.LinkSourceDeleteAction.DeleteTargetIfOrphan)\n\n\nclass OptWhenBlock(Nonterm):\n    @parsing.inline(1)\n    def reduce_WHEN_ParenExpr(self, _, expr):\n        pass\n\n    def reduce_empty(self):\n        self.val = None\n\n\nclass OptUsingBlock(Nonterm):\n    @parsing.inline(1)\n    def reduce_USING_ParenExpr(self, _, expr):\n        pass\n\n    def reduce_empty(self):\n        self.val = None\n\n\nclass AccessKind(Nonterm):\n    val: list[qltypes.AccessKind]\n\n    def reduce_ALL(self, _):\n        self.val = list(qltypes.AccessKind)\n\n    def reduce_SELECT(self, _):\n        self.val = [qltypes.AccessKind.Select]\n\n    def reduce_UPDATE(self, _):\n        self.val = [\n            qltypes.AccessKind.UpdateRead, qltypes.AccessKind.UpdateWrite]\n\n    def reduce_UPDATE_READ(self, _u, _r):\n        self.val = [qltypes.AccessKind.UpdateRead]\n\n    def reduce_UPDATE_WRITE(self, _u, _w):\n        self.val = [qltypes.AccessKind.UpdateWrite]\n\n    def reduce_INSERT(self, _):\n        self.val = [qltypes.AccessKind.Insert]\n\n    def reduce_DELETE(self, _):\n        self.val = [qltypes.AccessKind.Delete]\n\n\nclass AccessKindList(parsing.ListNonterm, element=AccessKind,\n                     separator=tokens.T_COMMA):\n    val: list[list[qltypes.AccessKind]]\n\n\nclass AccessPolicyAction(Nonterm):\n\n    def reduce_ALLOW(self, _):\n        self.val = qltypes.AccessPolicyAction.Allow\n\n    def reduce_DENY(self, _):\n        self.val = qltypes.AccessPolicyAction.Deny\n\n\nclass TriggerTiming(Nonterm):\n    def reduce_AFTER(self, *kids):\n        self.val = qltypes.TriggerTiming.After\n\n    def reduce_AFTER_COMMIT_OF(self, *kids):\n        self.val = qltypes.TriggerTiming.AfterCommitOf\n\n\nclass TriggerKind(Nonterm):\n    def reduce_UPDATE(self, *kids):\n        self.val = qltypes.TriggerKind.Update\n\n    def reduce_INSERT(self, *kids):\n        self.val = qltypes.TriggerKind.Insert\n\n    def reduce_DELETE(self, *kids):\n        self.val = qltypes.TriggerKind.Delete\n\n\nclass TriggerKindList(parsing.ListNonterm, element=TriggerKind,\n                      separator=tokens.T_COMMA):\n    pass\n\n\nclass TriggerScope(Nonterm):\n    def reduce_EACH(self, *kids):\n        self.val = qltypes.TriggerScope.Each\n\n    def reduce_ALL(self, *kids):\n        self.val = qltypes.TriggerScope.All\n\n\nclass RewriteKind(Nonterm):\n    def reduce_UPDATE(self, *kids):\n        self.val = qltypes.RewriteKind.Update\n\n    def reduce_INSERT(self, *kids):\n        self.val = qltypes.RewriteKind.Insert\n\n\nclass RewriteKindList(parsing.ListNonterm, element=RewriteKind,\n                      separator=tokens.T_COMMA):\n    pass\n\n\nclass ExtensionVersion(Nonterm):\n\n    def reduce_VERSION_BaseStringConstant(self, _, const):\n        version = const.val\n\n        try:\n            verutils.parse_version(version.value)\n        except ValueError:\n            raise EdgeQLSyntaxError(\n                'invalid extension version format',\n                details='Expected a SemVer-compatible format.',\n                span=version.span,\n            ) from None\n\n        self.val = version\n\n\nclass OptExtensionVersion(Nonterm):\n\n    @parsing.inline(0)\n    def reduce_ExtensionVersion(self, version):\n        pass\n\n    def reduce_empty(self):\n        self.val = None\n\n\nclass IndexArg(Nonterm):\n    def reduce_kwarg_bad_definition(self, *kids):\n        r\"\"\"%reduce FuncDeclArgName COLON \\\n                OptTypeQualifier FullTypeExpr OptDefault \\\n        \"\"\"\n        raise EdgeQLSyntaxError(\n            f'index parameters have to be NAMED ONLY',\n            span=kids[0].span)\n\n    def reduce_kwarg_definition(self, kind, name, _, typemod, type, default):\n        r\"\"\"%reduce ParameterKind FuncDeclArgName COLON \\\n                OptTypeQualifier FullTypeExpr OptDefault \\\n        \"\"\"\n        if kind.val is not qltypes.ParameterKind.NamedOnlyParam:\n            raise EdgeQLSyntaxError(\n                f'index parameters have to be NAMED ONLY',\n                span=kind.span)\n\n        self.val = qlast.FuncParamDecl(\n            kind=kind.val,\n            name=name.val,\n            typemod=typemod.val,\n            type=type.val,\n            default=default.val\n        )\n\n    def reduce_AnyIdentifier_ASSIGN_Expr(self, ident, _, expr):\n        self.val = (\n            ident.val,\n            ident.span,\n            expr.val,\n        )\n\n    def reduce_FuncDeclArgName_OptDefault(self, name, default):\n        raise EdgeQLSyntaxError(\n            f'missing type declaration for the `{name.val}` parameter',\n            span=name.span)\n\n\nclass IndexArgList(parsing.ListNonterm, element=IndexArg,\n                   separator=tokens.T_COMMA, allow_trailing_separator=True):\n    pass\n\n\nclass OptIndexArgList(Nonterm):\n    @parsing.inline(0)\n    def reduce_IndexArgList(self, list):\n        pass\n\n    def reduce_empty(self):\n        self.val = []\n\n\nclass IndexExtArgList(Nonterm):\n\n    @parsing.inline(1)\n    def reduce_LPAREN_OptIndexArgList_RPAREN(self, *_):\n        pass\n\n\nclass OptIndexExtArgList(Nonterm):\n\n    @parsing.inline(0)\n    def reduce_IndexExtArgList(self, list):\n        pass\n\n    def reduce_empty(self):\n        self.val = []\n\n\nclass ProcessIndexMixin(ProcessFunctionParamsMixin):\n    def _process_arguments(self, arguments):\n        kwargs = {}\n        for argval in arguments:\n            if isinstance(argval, qlast.FuncParamDecl):\n                raise EdgeQLSyntaxError(\n                    f\"unexpected new parameter definition `{argval.name}`\",\n                    span=argval.span)\n\n            argname, argname_ctx, arg = argval\n            if argname in kwargs:\n                raise EdgeQLSyntaxError(\n                    f\"duplicate named argument `{argname}`\",\n                    span=argname_ctx)\n\n            kwargs[argname] = arg\n\n        return kwargs\n\n    def _process_params_or_kwargs(self, bases, arguments):\n        params = []\n        kwargs = dict()\n\n        # If the definition is extending another abstract index, then we\n        # cannot define new parameters, but can only supply some arguments.\n        if bases:\n            kwargs = self._process_arguments(arguments)\n        else:\n            params = arguments\n            self._validate_params(params)\n\n        return params, kwargs\n\n    def _process_sql_body(self, block, *, optional_using: bool=False):\n        props: dict[str, typing.Any] = {}\n\n        commands = []\n        code = None\n\n        for node in block.val:\n            if isinstance(node, qlast.IndexCode):\n                if code is not None:\n                    raise EdgeQLSyntaxError(\n                        'more than one USING <code> clause',\n                        span=node.span)\n                props['code'] = node\n            else:\n                commands.append(node)\n\n        if commands:\n            props['commands'] = commands\n\n        return props\n"
  },
  {
    "path": "edb/edgeql/parser/grammar/config.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nfrom edb import errors\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom .expressions import Nonterm\nfrom .tokens import *  # NOQA\nfrom .expressions import *  # NOQA\n\n\nclass ConfigScope(Nonterm):\n\n    def reduce_SESSION(self, _):\n        self.val = qltypes.ConfigScope.SESSION\n\n    def reduce_CURRENT_DATABASE(self, _c, _d):\n        self.val = qltypes.ConfigScope.DATABASE\n\n    def reduce_CURRENT_BRANCH(self, _c, _d):\n        self.val = qltypes.ConfigScope.DATABASE\n\n    def reduce_SYSTEM(self, _):\n        self.val = qltypes.ConfigScope.INSTANCE\n\n    def reduce_INSTANCE(self, _):\n        self.val = qltypes.ConfigScope.INSTANCE\n\n\nclass ConfigOp(Nonterm):\n    val: qlast.ConfigOp\n\n    def reduce_SET_NodeName_ASSIGN_Expr(self, _s, name, _a, expr):\n        self.val = qlast.ConfigSet(\n            name=name.val,\n            expr=expr.val,\n        )\n\n    def reduce_INSERT_NodeName_Shape(self, _, name, shape):\n        self.val = qlast.ConfigInsert(\n            name=name.val,\n            shape=shape.val,\n        )\n\n    def reduce_RESET_NodeName_OptFilterClause(self, _, name, where):\n        self.val = qlast.ConfigReset(\n            name=name.val,\n            where=where.val,\n        )\n\n\nclass ConfigStmt(Nonterm):\n\n    def reduce_CONFIGURE_DATABASE_ConfigOp(self, configure, database, _config):\n        raise errors.EdgeQLSyntaxError(\n            f\"'{configure.val} {database.val}' is invalid syntax. \"\n            f\"Did you mean '{configure.val} \"\n            f\"{'current' if database.val[0] == 'd' else 'CURRENT'} \"\n            f\"{database.val}'?\",\n            span=database.span)\n\n    def reduce_CONFIGURE_BRANCH_ConfigOp(self, configure, database, _config):\n        raise errors.EdgeQLSyntaxError(\n            f\"'{configure.val} {database.val}' is invalid syntax. \"\n            f\"Did you mean '{configure.val} \"\n            f\"{'current' if database.val[0] == 'd' else 'CURRENT'} \"\n            f\"{database.val}'?\",\n            span=database.span)\n\n    def reduce_CONFIGURE_ConfigScope_ConfigOp(self, _, scope, op):\n        self.val = op.val\n        self.val.scope = scope.val\n\n    def reduce_SET_GLOBAL_NodeName_ASSIGN_Expr(self, _s, _g, name, _a, expr):\n        self.val = qlast.ConfigSet(\n            name=name.val,\n            expr=expr.val,\n            scope=qltypes.ConfigScope.GLOBAL,\n        )\n\n    def reduce_RESET_GLOBAL_NodeName(self, _r, _g, name):\n        self.val = qlast.ConfigReset(\n            name=name.val,\n            scope=qltypes.ConfigScope.GLOBAL,\n        )\n"
  },
  {
    "path": "edb/edgeql/parser/grammar/ddl.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2015-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport collections\nimport re\nimport textwrap\nimport typing\n\nfrom edb import errors\nfrom edb.errors import EdgeQLSyntaxError\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom edb.common import span as edb_span\nfrom edb.common import parsing\n\nfrom . import expressions\nfrom . import commondl\nfrom . import tokens\n\nfrom .precedence import *  # NOQA\nfrom .tokens import *  # NOQA\nfrom .commondl import *  # NOQA\n\nfrom .sdl import *  # NOQA\n\n\nNonterm = expressions.Nonterm  # type: ignore[misc]\nSemicolons = commondl.Semicolons  # type: ignore[misc]\n\n\nsdl_nontem_helper = commondl.NewNontermHelper(__name__)\n_new_nonterm = sdl_nontem_helper._new_nonterm\n\n\nclass DDLStmt(Nonterm):\n    val: qlast.DDLCommand\n\n    @parsing.inline(0)\n    def reduce_DatabaseStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_BranchStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_RoleStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_ExtensionPackageStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_OptWithDDLStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_MigrationStmt(self, *_):\n        pass\n\n\nclass DDLWithBlock(Nonterm):\n    @parsing.inline(0)\n    def reduce_WithBlock(self, *_):\n        pass\n\n\nclass OptWithDDLStmt(Nonterm):\n    def reduce_DDLWithBlock_WithDDLStmt(self, *kids):\n        self.val = kids[1].val\n        self.val.aliases = kids[0].val.aliases\n\n    @parsing.inline(0)\n    def reduce_WithDDLStmt(self, *_):\n        pass\n\n\nclass WithDDLStmt(Nonterm):\n    @parsing.inline(0)\n    def reduce_InnerDDLStmt(self, *_):\n        pass\n\n\nclass InnerDDLStmt(Nonterm):\n\n    @parsing.inline(0)\n    def reduce_CreatePseudoTypeStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CreateScalarTypeStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterScalarTypeStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropScalarTypeStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CreateAnnotationStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterAnnotationStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropAnnotationStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CreateObjectTypeStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterObjectTypeStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropObjectTypeStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CreateAliasStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterAliasStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropAliasStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CreateConstraintStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterConstraintStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropConstraintStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CreateLinkStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterLinkStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropLinkStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CreatePropertyStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterPropertyStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropPropertyStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CreateModuleStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterModuleStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropModuleStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CreateFunctionStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterFunctionStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropFunctionStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CreateOperatorStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterOperatorStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropOperatorStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CreateCastStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterCastStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CreateGlobalStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterGlobalStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropGlobalStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CreatePermissionStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterPermissionStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropPermissionStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropCastStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_ExtensionStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_FutureStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CreateIndexStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterIndexStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropIndexStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CreateIndexMatchStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropIndexMatchStmt(self, *_):\n        pass\n\n\nclass PointerName(Nonterm):\n    @parsing.inline(0)\n    def reduce_PtrNodeName(self, *kids):\n        pass\n\n    def reduce_DUNDERTYPE(self, *kids):\n        self.val = qlast.ObjectRef(name=kids[0].val)\n\n\nclass UnqualifiedPointerName(Nonterm):\n    def reduce_PointerName(self, *kids):\n        if kids[0].val.module:\n            raise EdgeQLSyntaxError(\n                'unexpected fully-qualified name',\n                span=kids[0].val.span)\n        self.val = kids[0].val\n\n\nclass OptIfNotExists(Nonterm):\n    def reduce_IF_NOT_EXISTS(self, *kids):\n        self.val = True\n\n    def reduce_empty(self, *kids):\n        self.val = False\n\n\nclass ProductionTpl:\n    def _passthrough(self, cmd):\n        self.val = cmd.val\n\n    def _singleton_list(self, cmd):\n        self.val = [cmd.val]\n\n    def _empty(self, *kids):\n        self.val = []\n\n    def _block(self, lbrace, cmdlist, sc2, rbrace):\n        self.val = cmdlist.val\n\n    def _block2(self, lbrace, sc1, cmdlist, sc2, rbrace):\n        self.val = cmdlist.val\n\n\ndef commands_block(parent, *commands, opt=True, production_tpl=ProductionTpl):\n    if parent is None:\n        parent = ''\n\n    clsdict = collections.OrderedDict()\n\n    # Command := Command1 | Command2 ...\n    #\n    for command in commands:\n        clsdict['reduce_{}'.format(command.__name__)] = \\\n            production_tpl._passthrough\n\n    cmd = _new_nonterm(parent + 'Command', clsdict=clsdict)\n\n    # CommandsList := Command [; Command ...]\n    cmdlist = _new_nonterm(parent + 'CommandsList',\n                           clsbases=(parsing.ListNonterm,),\n                           clskwds=dict(element=cmd, separator=Semicolons))\n\n    # CommandsBlock :=\n    #\n    #   { [ ; ] CommandsList ; }\n    clsdict = collections.OrderedDict()\n    clsdict['reduce_LBRACE_' + cmdlist.__name__ + '_OptSemicolons_RBRACE'] = \\\n        production_tpl._block\n    clsdict['reduce_LBRACE_Semicolons_' + cmdlist.__name__ +\n            '_OptSemicolons_RBRACE'] = \\\n        production_tpl._block2\n    clsdict['reduce_LBRACE_OptSemicolons_RBRACE'] = \\\n        production_tpl._empty\n    if not opt:\n        #\n        #   | Command\n        clsdict['reduce_{}'.format(cmd.__name__)] = \\\n            production_tpl._singleton_list\n    cmdblock = _new_nonterm(\n        parent + 'CommandsBlock',\n        clsdict=clsdict,\n        clsbases=(Nonterm, production_tpl),\n    )\n\n    # OptCommandsBlock := CommandsBlock | <e>\n    clsdict = collections.OrderedDict()\n    clsdict['reduce_{}'.format(cmdblock.__name__)] = \\\n        production_tpl._passthrough\n    clsdict['reduce_empty'] = production_tpl._empty\n\n    if opt:\n        _new_nonterm(\n            'Opt' + parent + 'CommandsBlock',\n            clsdict=clsdict,\n            clsbases=(Nonterm, production_tpl),\n        )\n\n\nclass NestedQLBlockStmt(Nonterm):\n    val: qlast.DDLOperation\n\n    def reduce_Stmt(self, stmt):\n        if isinstance(stmt.val, qlast.Query):\n            self.val = qlast.DDLQuery(query=stmt.val)\n        else:\n            self.val = stmt.val\n\n    @parsing.inline(0)\n    def reduce_OptWithDDLStmt(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_SetFieldStmt(self, *kids):\n        pass\n\n\nclass NestedQLBlock(ProductionTpl):\n\n    @property\n    def allowed_fields(self) -> frozenset[str]:\n        raise NotImplementedError\n\n    @property\n    def result(self) -> typing.Any:\n        raise NotImplementedError\n\n    def _process_body(self, body):\n        fields = []\n        stmts = []\n        uniq_check = set()\n        for stmt in body:\n            if isinstance(stmt, qlast.SetField):\n                if stmt.name not in self.allowed_fields:\n                    raise errors.InvalidSyntaxError(\n                        f'unexpected field: {stmt.name!r}',\n                        span=stmt.span,\n                    )\n                if stmt.name in uniq_check:\n                    raise errors.InvalidSyntaxError(\n                        f'duplicate `SET {stmt.name} := ...`',\n                        span=stmt.span,\n                    )\n                uniq_check.add(stmt.name)\n                fields.append(stmt)\n            else:\n                stmts.append(stmt)\n\n        return fields, stmts\n\n    def _get_text(self, body):\n        # XXX: Workaround the rust lexer issue of returning\n        # byte token offsets instead of character offsets.\n        src_start = body.span.start\n        src_end = body.span.end\n        buffer = body.span.buffer.encode('utf-8')\n        text = buffer[src_start:src_end].decode('utf-8').strip().strip('}{\\n')\n        return textwrap.dedent(text).strip('\\n')\n\n    def _block(self, lbrace, cmdlist, sc2, rbrace):\n        # LBRACE NestedQLBlock OptSemicolons RBRACE\n        fields, stmts = self._process_body(cmdlist.val)\n        body = qlast.NestedQLBlock(commands=stmts)\n\n        kids = [lbrace, cmdlist, sc2, rbrace]\n        body.span = (\n            edb_span.merge_spans(k.span for k in kids if k.span)\n            or edb_span.Span.empty()\n        )\n\n        body.text = self._get_text(body)\n        self.val = self.result(body=body, fields=fields)\n\n    def _block2(self, lbrace, sc1, cmdlist, sc2, rbrace):\n        # LBRACE Semicolons NestedQLBlock OptSemicolons RBRACE\n        fields, stmts = self._process_body(cmdlist.val)\n        body = qlast.NestedQLBlock(commands=stmts)\n\n        kids = [lbrace, sc1, cmdlist, sc2, rbrace]\n        body.span = (\n            edb_span.merge_spans(k.span for k in kids if k.span)\n            or edb_span.Span.empty()\n        )\n\n        body.text = self._get_text(body)\n        self.val = self.result(body=body, fields=fields)\n\n    def _empty(self, *kids):\n        # LBRACE OptSemicolons RBRACE | <e>\n        self.val = []\n        body = qlast.NestedQLBlock(commands=[])\n        body.span = (\n            edb_span.merge_spans(k.span for k in kids if k.span)\n            or edb_span.Span.empty()\n        )\n        body.text = self._get_text(body)\n        self.val = self.result(body=body, fields=[])\n\n\nclass UsingStmt(Nonterm):\n\n    def reduce_USING_ParenExpr(self, *kids):\n        self.val = qlast.SetField(\n            name='expr',\n            value=kids[1].val,\n            special_syntax=True,\n        )\n\n    def reduce_RESET_EXPRESSION(self, *kids):\n        self.val = qlast.SetField(\n            name='expr',\n            value=None,\n            special_syntax=True,\n        )\n\n\nclass SetFieldStmt(Nonterm):\n    # field := <expr>\n    def reduce_SET_Identifier_ASSIGN_GenExpr(self, *kids):\n        self.val = qlast.SetField(\n            name=kids[1].val.lower(),\n            value=kids[3].val,\n        )\n\n\nclass ResetFieldStmt(Nonterm):\n    # RESET field\n    def reduce_RESET_IDENT(self, *kids):\n        self.val = qlast.SetField(\n            name=kids[1].val.lower(),\n            value=None,\n        )\n\n    def reduce_RESET_DEFAULT(self, *kids):\n        self.val = qlast.SetField(\n            name='default',\n            value=None,\n        )\n\n\nclass CreateAnnotationValueStmt(Nonterm):\n    def reduce_CREATE_ANNOTATION_NodeName_ASSIGN_GenExpr(self, *kids):\n        self.val = qlast.CreateAnnotationValue(\n            name=kids[2].val,\n            value=kids[4].val,\n        )\n\n\nclass AlterAnnotationValueStmt(Nonterm):\n    def reduce_ALTER_ANNOTATION_NodeName_ASSIGN_GenExpr(self, *kids):\n        self.val = qlast.AlterAnnotationValue(\n            name=kids[2].val,\n            value=kids[4].val,\n        )\n\n    def reduce_ALTER_ANNOTATION_NodeName_DROP_OWNED(self, *kids):\n        self.val = qlast.AlterAnnotationValue(\n            name=kids[2].val,\n        )\n        self.val.commands = [qlast.SetField(\n            name='owned',\n            value=qlast.Constant.boolean(False, span=self.span),\n            special_syntax=True,\n        )]\n\n\nclass DropAnnotationValueStmt(Nonterm):\n    def reduce_DROP_ANNOTATION_NodeName(self, *kids):\n        self.val = qlast.DropAnnotationValue(\n            name=kids[2].val,\n        )\n\n\nclass RenameStmt(Nonterm):\n    def reduce_RENAME_TO_NodeName(self, *kids):\n        self.val = qlast.Rename(new_name=kids[2].val)\n\n\ncommands_block(\n    'Create',\n    UsingStmt,\n    SetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n)\n\n\ncommands_block(\n    'Alter',\n    UsingStmt,\n    RenameStmt,\n    SetFieldStmt,\n    ResetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    opt=False)\n\n\nclass AlterAbstract(Nonterm):\n\n    def reduce_DROP_ABSTRACT(self, *kids):\n        # TODO: Raise a DeprecationWarning once we have facility for that.\n        self.val = qlast.SetField(\n            name='abstract',\n            value=qlast.Constant.boolean(False, span=self.span),\n            special_syntax=True,\n        )\n\n    def reduce_SET_NOT_ABSTRACT(self, *kids):\n        self.val = qlast.SetField(\n            name='abstract',\n            value=qlast.Constant.boolean(False, span=self.span),\n            special_syntax=True,\n        )\n\n    def reduce_SET_ABSTRACT(self, *kids):\n        self.val = qlast.SetField(\n            name='abstract',\n            value=qlast.Constant.boolean(True, span=self.span),\n            special_syntax=True,\n        )\n\n    def reduce_RESET_ABSTRACT(self, *kids):\n        self.val = qlast.SetField(\n            name='abstract',\n            value=None,\n            special_syntax=True,\n        )\n\n\nclass OptPosition(Nonterm):\n    def reduce_BEFORE_NodeName(self, *kids):\n        self.val = qlast.Position(ref=kids[1].val, position='BEFORE')\n\n    def reduce_AFTER_NodeName(self, *kids):\n        self.val = qlast.Position(ref=kids[1].val, position='AFTER')\n\n    def reduce_FIRST(self, *kids):\n        self.val = qlast.Position(position='FIRST')\n\n    def reduce_LAST(self, *kids):\n        self.val = qlast.Position(position='LAST')\n\n    def reduce_empty(self, *kids):\n        self.val = None\n\n\nclass AlterSimpleExtending(Nonterm):\n    def reduce_EXTENDING_SimpleTypeNameList_OptPosition(self, *kids):\n        self.val = qlast.AlterAddInherit(\n            bases=kids[1].val, position=kids[2].val\n        )\n\n    def reduce_DROP_EXTENDING_SimpleTypeNameList(self, *kids):\n        self.val = qlast.AlterDropInherit(bases=kids[2].val)\n\n    @parsing.inline(0)\n    def reduce_AlterAbstract(self, *kids):\n        pass\n\n\nclass AlterExtending(Nonterm):\n    def reduce_EXTENDING_TypeNameList_OptPosition(self, *kids):\n        self.val = qlast.AlterAddInherit(\n            bases=kids[1].val, position=kids[2].val\n        )\n\n    def reduce_DROP_EXTENDING_TypeNameList(self, *kids):\n        self.val = qlast.AlterDropInherit(bases=kids[2].val)\n\n    @parsing.inline(0)\n    def reduce_AlterAbstract(self, *kids):\n        pass\n\n\nclass AlterOwnedStmt(Nonterm):\n\n    def reduce_DROP_OWNED(self, *kids):\n        self.val = qlast.SetField(\n            name='owned',\n            value=qlast.Constant.boolean(False, span=self.span),\n            special_syntax=True,\n        )\n\n    def reduce_SET_OWNED(self, *kids):\n        self.val = qlast.SetField(\n            name='owned',\n            value=qlast.Constant.boolean(True, span=self.span),\n            special_syntax=True,\n        )\n\n\n#\n# DATABASE\n#\n\n\nclass DatabaseName(Nonterm):\n\n    def reduce_Identifier(self, kid):\n        self.val = qlast.ObjectRef(module=None, name=kid.val)\n\n    def reduce_ReservedKeyword(self, *kids):\n        name = kids[0].val\n        if (\n            name[:2] == '__' and name[-2:] == '__' and\n            name not in {'__edgedbsys__', '__edgedbtpl__'}\n        ):\n            # There are a few reserved keywords like __std__ and __subject__\n            # that can be used in paths but are prohibited to be used\n            # anywhere else. So just as the tokenizer prohibits using\n            # __names__ in general, we enforce the rule here for the\n            # few remaining reserved __keywords__.\n            raise EdgeQLSyntaxError(\n                \"identifiers surrounded by double underscores are forbidden\",\n                span=kids[0].span)\n\n        self.val = qlast.ObjectRef(\n            module=None,\n            name=name\n        )\n\n\nclass DatabaseStmt(Nonterm):\n\n    @parsing.inline(0)\n    def reduce_CreateDatabaseStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropDatabaseStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterDatabaseStmt(self, *kids):\n        pass\n\n\n#\n# CREATE DATABASE\n#\n\n\ncommands_block(\n    'CreateDatabase',\n    SetFieldStmt,\n)\n\n\nclass CreateDatabaseStmt(Nonterm):\n    def reduce_CREATE_DATABASE_regular(self, *kids):\n        \"\"\"%reduce CREATE DATABASE DatabaseName OptCreateDatabaseCommandsBlock\n        \"\"\"\n        self.val = qlast.CreateDatabase(\n            name=kids[2].val,\n            commands=kids[3].val,\n            branch_type=qlast.BranchType.EMPTY,\n            flavor='DATABASE',\n        )\n\n    # TODO: This one should probably not exist, and we'll get rid of\n    # it once we merge Victor's new testing.\n    def reduce_CREATE_DATABASE_from_template(self, *kids):\n        \"\"\"%reduce\n            CREATE DATABASE DatabaseName FROM AnyNodeName\n            OptCreateDatabaseCommandsBlock\n        \"\"\"\n        _, _, _name, _, _template, _commands = kids\n        self.val = qlast.CreateDatabase(\n            name=kids[2].val,\n            commands=kids[5].val,\n            branch_type=qlast.BranchType.DATA,\n            template=kids[4].val,\n            flavor='DATABASE',\n        )\n\n\n#\n# DROP DATABASE\n#\nclass DropDatabaseStmt(Nonterm):\n    def reduce_DROP_DATABASE_DatabaseName(self, *kids):\n        self.val = qlast.DropDatabase(\n            name=kids[2].val,\n            flavor='DATABASE',\n        )\n\n\n#\n# ALTER DATABASE\n#\n\n\ncommands_block(\n    'AlterDatabase',\n    RenameStmt,\n    opt=False\n)\n\n\nclass AlterDatabaseStmt(Nonterm):\n    def reduce_ALTER_DATABASE_DatabaseName_AlterDatabaseCommandsBlock(\n        self, *kids\n    ):\n        _, _, name, commands = kids\n        self.val = qlast.AlterDatabase(\n            name=name.val,\n            commands=commands.val,\n        )\n\n\n#\n# BRANCH\n#\n\n\nclass BranchStmt(Nonterm):\n\n    @parsing.inline(0)\n    def reduce_CreateBranchStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropBranchStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterBranchStmt(self, *kids):\n        pass\n\n#\n# CREATE BRANCH\n#\n\n\nclass CreateBranchStmt(Nonterm):\n    def reduce_CREATE_EMPTY_BRANCH_DatabaseName(self, *kids):\n        self.val = qlast.CreateDatabase(\n            name=kids[3].val,\n            branch_type=qlast.BranchType.EMPTY,\n        )\n\n    def reduce_create_schema_branch(self, *kids):\n        \"\"\"%reduce\n            CREATE SCHEMA BRANCH DatabaseName FROM DatabaseName\n        \"\"\"\n        self.val = qlast.CreateDatabase(\n            name=kids[3].val,\n            template=kids[5].val,\n            branch_type=qlast.BranchType.SCHEMA,\n        )\n\n    def reduce_create_data_branch(self, *kids):\n        \"\"\"%reduce\n            CREATE DATA BRANCH DatabaseName FROM DatabaseName\n        \"\"\"\n        self.val = qlast.CreateDatabase(\n            name=kids[3].val,\n            template=kids[5].val,\n            branch_type=qlast.BranchType.DATA,\n        )\n\n    def reduce_create_template_branch(self, *kids):\n        \"\"\"%reduce\n            CREATE TEMPLATE BRANCH DatabaseName FROM DatabaseName\n        \"\"\"\n        self.val = qlast.CreateDatabase(\n            name=kids[3].val,\n            template=kids[5].val,\n            branch_type=qlast.BranchType.TEMPLATE,\n        )\n\n\n#\n# DROP BRANCH\n#\n\nBranchOptionsSpec = collections.namedtuple(\n    'BranchOptionsSpec', ['force'])\n\n\nclass BranchOptions(Nonterm):\n    # This is generalizable, but we don't bother generalizing it yet.\n    def reduce_empty(self, *kids):\n        self.val = BranchOptionsSpec(force=False)\n\n    def reduce_FORCE(self, *kids):\n        self.val = BranchOptionsSpec(force=True)\n\n\nclass DropBranchStmt(Nonterm):\n    def reduce_DROP_BRANCH_DatabaseName_BranchOptions(self, *kids):\n        _, _, name, options = kids\n        self.val = qlast.DropDatabase(\n            name=name.val,\n            force=options.val.force,\n        )\n\n\n#\n# ALTER BRANCH\n#\n\n\ncommands_block(\n    'AlterBranch',\n    RenameStmt,\n    opt=False\n)\n\n\nclass AlterBranchStmt(Nonterm):\n    def reduce_alter_branch(self, *kids):\n        \"\"\"%reduce\n            ALTER BRANCH DatabaseName BranchOptions AlterBranchCommandsBlock\n        \"\"\"\n        _, _, name, options, commands = kids\n        self.val = qlast.AlterDatabase(\n            name=name.val,\n            commands=commands.val,\n            force=options.val.force,\n        )\n\n\n#\n# EXTENSION PACKAGE\n#\n\nclass ExtensionPackageStmt(Nonterm):\n\n    @parsing.inline(0)\n    def reduce_CreateExtensionPackageStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropExtensionPackageStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CreateExtensionPackageMigrationStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropExtensionPackageMigrationStmt(self, *kids):\n        pass\n\n\n#\n# CREATE EXTENSION PACKAGE\n#\nclass ExtensionPackageBody(typing.NamedTuple):\n\n    body: qlast.NestedQLBlock\n    fields: list[qlast.SetField]\n\n\nclass CreateExtensionPackageBodyBlock(NestedQLBlock):\n\n    @property\n    def allowed_fields(self) -> frozenset[str]:\n        return frozenset(\n            {'internal', 'ext_module', 'sql_extensions', 'dependencies',\n             'sql_setup_script', 'sql_teardown_script'}\n        )\n\n    @property\n    def result(self) -> typing.Any:\n        return ExtensionPackageBody\n\n\ncommands_block(\n    'CreateExtensionPackage',\n    NestedQLBlockStmt,\n    opt=True,\n    production_tpl=CreateExtensionPackageBodyBlock,\n)\n\n\nclass CreateExtensionPackageStmt(Nonterm):\n\n    def reduce_CreateExtensionPackageStmt(self, *kids):\n        r\"\"\"%reduce CREATE EXTENSIONPACKAGE ShortNodeName\n                    ExtensionVersion\n                    OptCreateExtensionPackageCommandsBlock\n        \"\"\"\n        self.val = qlast.CreateExtensionPackage(\n            name=kids[2].val,\n            version=kids[3].val,\n            body=kids[4].val.body,\n            commands=kids[4].val.fields,\n        )\n\n\n#\n# DROP EXTENSION PACKAGE\n#\nclass DropExtensionPackageStmt(Nonterm):\n\n    def reduce_DropExtensionPackageStmt(self, *kids):\n        r\"\"\"%reduce DROP EXTENSIONPACKAGE ShortNodeName ExtensionVersion\"\"\"\n        self.val = qlast.DropExtensionPackage(\n            name=kids[2].val,\n            version=kids[3].val,\n        )\n\n\n#\n# CREATE EXTENSION PACKAGE MIGRATION\n#\n\nclass CreateExtensionPackageMigrationBodyBlock(NestedQLBlock):\n\n    @property\n    def allowed_fields(self) -> frozenset[str]:\n        return frozenset(\n            {'early_sql_script', 'late_sql_script'}\n        )\n\n    @property\n    def result(self) -> typing.Any:\n        return ExtensionPackageBody\n\n\ncommands_block(\n    'CreateExtensionPackage',\n    NestedQLBlockStmt,\n    opt=True,\n    production_tpl=CreateExtensionPackageBodyBlock,\n)\n\n\nclass CreateExtensionPackageMigrationStmt(Nonterm):\n\n    def reduce_CreateExtensionPackageMigrationStmt(self, *kids):\n        r\"\"\"%reduce CREATE EXTENSIONPACKAGE ShortNodeName\n                    MIGRATION FROM\n                    ExtensionVersion TO\n                    ExtensionVersion\n                    OptCreateExtensionPackageCommandsBlock\n        \"\"\"\n        _, _, name, _, _, from_version, _, to_version, block = kids\n        self.val = qlast.CreateExtensionPackageMigration(\n            name=name.val,\n            from_version=from_version.val,\n            to_version=to_version.val,\n            body=block.val.body,\n            commands=block.val.fields,\n        )\n\n\n#\n# DROP EXTENSION PACKAGE MIGRATION\n#\nclass DropExtensionPackageMigrationStmt(Nonterm):\n\n    def reduce_DropExtensionPackageMigrationStmt(self, *kids):\n        r\"\"\"%reduce DROP EXTENSIONPACKAGE ShortNodeName\n                    MIGRATION FROM\n                    ExtensionVersion TO\n                    ExtensionVersion\n        \"\"\"\n        _, _, name, _, _, from_version, _, to_version = kids\n\n        self.val = qlast.DropExtensionPackageMigration(\n            name=name.val,\n            from_version=from_version.val,\n            to_version=to_version.val,\n        )\n\n\n#\n# EXTENSIONS\n#\n\n\nclass ExtensionStmt(Nonterm):\n\n    @parsing.inline(0)\n    def reduce_CreateExtensionStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterExtensionStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropExtensionStmt(self, *kids):\n        pass\n\n\n#\n# CREATE EXTENSION\n#\n\n\ncommands_block(\n    'CreateExtension',\n    SetFieldStmt,\n)\n\n\nclass CreateExtensionStmt(Nonterm):\n\n    def reduce_CreateExtensionStmt(self, *kids):\n        r\"\"\"%reduce CREATE EXTENSION ShortNodeName OptExtensionVersion\n                    OptCreateExtensionCommandsBlock\n        \"\"\"\n        self.val = qlast.CreateExtension(\n            name=kids[2].val,\n            version=kids[3].val,\n            commands=kids[4].val,\n        )\n\n#\n# ALTER EXTENSION\n#\n\n\nclass AlterExtensionStmt(Nonterm):\n\n    def reduce_AlterExtensionStmt(self, *kids):\n        r\"\"\"%reduce ALTER EXTENSION ShortNodeName\n                    TO ExtensionVersion\n        \"\"\"\n        _, _, name, _, ver = kids\n        self.val = qlast.AlterExtension(\n            name=name.val,\n            to_version=ver.val,\n        )\n\n\n#\n# DROP EXTENSION\n#\nclass DropExtensionStmt(Nonterm):\n\n    def reduce_DropExtensionPackageStmt(self, *kids):\n        r\"\"\"%reduce DROP EXTENSION ShortNodeName OptExtensionVersion\"\"\"\n        self.val = qlast.DropExtension(\n            name=kids[2].val,\n            version=kids[3].val,\n        )\n\n\n#\n# FUTURE\n#\n\n\nclass FutureStmt(Nonterm):\n\n    @parsing.inline(0)\n    def reduce_CreateFutureStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropFutureStmt(self, *kids):\n        pass\n\n\n#\n# CREATE FUTURE\n#\n\n\nclass CreateFutureStmt(Nonterm):\n\n    def reduce_CreateFutureStmt(self, *kids):\n        r\"\"\"%reduce CREATE FUTURE ShortNodeName\"\"\"\n        self.val = qlast.CreateFuture(\n            name=kids[2].val,\n        )\n\n\n#\n# DROP FUTURE\n#\nclass DropFutureStmt(Nonterm):\n\n    def reduce_DropFutureStmt(self, *kids):\n        r\"\"\"%reduce DROP FUTURE ShortNodeName\"\"\"\n        self.val = qlast.DropFuture(\n            name=kids[2].val,\n        )\n\n\n#\n# ROLE\n#\n\nclass RoleStmt(Nonterm):\n\n    @parsing.inline(0)\n    def reduce_CreateRoleStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterRoleStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropRoleStmt(self, *kids):\n        pass\n\n\nclass ShortTypeName(Nonterm):\n    def reduce_ShortNodeName(self, name):\n        self.val = qlast.TypeName(maintype=name.val)\n\n\nclass ShortTypeNameList(\n    parsing.ListNonterm, element=ShortTypeName, separator=tokens.T_COMMA\n):\n    pass\n\n\n#\n# CREATE ROLE\n#\nclass ShortExtending(Nonterm):\n    @parsing.inline(1)\n    def reduce_EXTENDING_ShortTypeNameList(self, *kids):\n        pass\n\n\nclass OptShortExtending(Nonterm):\n    @parsing.inline(0)\n    def reduce_ShortExtending(self, *kids):\n        pass\n\n    def reduce_empty(self, *kids):\n        self.val = []\n\n\ncommands_block(\n    'CreateRole',\n    SetFieldStmt,\n)\n\n\nclass OptSuperuser(Nonterm):\n\n    def reduce_SUPERUSER(self, *kids):\n        self.val = True\n\n    def reduce_empty(self, *kids):\n        self.val = False\n\n\nclass CreateRoleStmt(Nonterm):\n    def reduce_CreateRoleStmt(self, *kids):\n        r\"\"\"%reduce CREATE OptSuperuser ROLE ShortNodeName\n                    OptShortExtending OptIfNotExists OptCreateRoleCommandsBlock\n        \"\"\"\n        self.val = qlast.CreateRole(\n            name=kids[3].val,\n            bases=kids[4].val,\n            create_if_not_exists=kids[5].val,\n            commands=kids[6].val,\n            superuser=kids[1].val,\n        )\n\n\n#\n# ALTER ROLE\n#\nclass AlterRoleExtending(Nonterm):\n    def reduce_EXTENDING_ShortTypeNameList_OptPosition(self, *kids):\n        self.val = qlast.AlterAddInherit(\n            bases=kids[1].val,\n            position=kids[2].val\n        )\n\n    def reduce_DROP_EXTENDING_ShortTypeNameList(self, *kids):\n        self.val = qlast.AlterDropInherit(\n            bases=kids[2].val\n        )\n\n\ncommands_block(\n    'AlterRole',\n    RenameStmt,\n    SetFieldStmt,\n    ResetFieldStmt,\n    AlterRoleExtending,\n    opt=False\n)\n\n\nclass AlterRoleStmt(Nonterm):\n    def reduce_ALTER_ROLE_ShortNodeName_AlterRoleCommandsBlock(self, *kids):\n        self.val = qlast.AlterRole(\n            name=kids[2].val,\n            commands=kids[3].val,\n        )\n\n\n#\n# DROP ROLE\n#\nclass DropRoleStmt(Nonterm):\n    def reduce_DROP_ROLE_ShortNodeName(self, *kids):\n        self.val = qlast.DropRole(\n            name=kids[2].val,\n        )\n\n\n#\n# CREATE CONSTRAINT\n#\nclass CreateConstraintStmt(Nonterm):\n    def reduce_CreateConstraint(self, *kids):\n        r\"\"\"%reduce CREATE ABSTRACT CONSTRAINT NodeName OptOnExpr \\\n                    OptExtendingSimple OptCreateCommandsBlock\"\"\"\n        self.val = qlast.CreateConstraint(\n            name=kids[3].val,\n            subjectexpr=kids[4].val,\n            bases=kids[5].val,\n            commands=kids[6].val,\n        )\n\n    def reduce_CreateConstraint_CreateFunctionArgs(self, *kids):\n        r\"\"\"%reduce CREATE ABSTRACT CONSTRAINT NodeName CreateFunctionArgs \\\n                    OptOnExpr OptExtendingSimple OptCreateCommandsBlock\"\"\"\n        self.val = qlast.CreateConstraint(\n            name=kids[3].val,\n            params=kids[4].val,\n            subjectexpr=kids[5].val,\n            bases=kids[6].val,\n            commands=kids[7].val,\n        )\n\n\nclass AlterConstraintStmt(Nonterm):\n    def reduce_CreateConstraint(self, *kids):\n        r\"\"\"%reduce ALTER ABSTRACT CONSTRAINT NodeName \\\n                    AlterCommandsBlock\"\"\"\n        self.val = qlast.AlterConstraint(\n            name=kids[3].val,\n            commands=kids[4].val,\n        )\n\n\nclass DropConstraintStmt(Nonterm):\n    def reduce_CreateConstraint(self, *kids):\n        r\"\"\"%reduce DROP ABSTRACT CONSTRAINT NodeName\"\"\"\n        self.val = qlast.DropConstraint(\n            name=kids[3].val\n        )\n\n\nclass OptDelegated(Nonterm):\n    def reduce_DELEGATED(self, *kids):\n        self.val = True\n\n    def reduce_empty(self):\n        self.val = False\n\n\nclass CreateConcreteConstraintStmt(Nonterm):\n    def reduce_CreateConstraint(self, *kids):\n        r\"\"\"%reduce CREATE OptDelegated CONSTRAINT \\\n                    NodeName OptConcreteConstraintArgList OptOnExpr \\\n                    OptExceptExpr \\\n                    OptCreateCommandsBlock\"\"\"\n        self.val = qlast.CreateConcreteConstraint(\n            delegated=kids[1].val,\n            name=kids[3].val,\n            args=kids[4].val,\n            subjectexpr=kids[5].val,\n            except_expr=kids[6].val,\n            commands=kids[7].val,\n        )\n\n\nclass SetDelegatedStmt(Nonterm):\n\n    def reduce_SET_DELEGATED(self, *kids):\n        self.val = qlast.SetField(\n            name='delegated',\n            value=qlast.Constant.boolean(True, span=self.span),\n            special_syntax=True,\n        )\n\n    def reduce_SET_NOT_DELEGATED(self, *kids):\n        self.val = qlast.SetField(\n            name='delegated',\n            value=qlast.Constant.boolean(False, span=self.span),\n            special_syntax=True,\n        )\n\n    def reduce_RESET_DELEGATED(self, *kids):\n        self.val = qlast.SetField(\n            name='delegated',\n            value=None,\n            special_syntax=True,\n        )\n\n\ncommands_block(\n    'AlterConcreteConstraint',\n    SetFieldStmt,\n    ResetFieldStmt,\n    SetDelegatedStmt,\n    AlterOwnedStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    AlterAbstract,\n    opt=False\n)\n\n\nclass AlterConcreteConstraintStmt(Nonterm):\n    def reduce_CreateConstraint(self, *kids):\n        r\"\"\"%reduce ALTER CONSTRAINT NodeName\n                    OptConcreteConstraintArgList OptOnExpr OptExceptExpr\n                    AlterConcreteConstraintCommandsBlock\"\"\"\n        self.val = qlast.AlterConcreteConstraint(\n            name=kids[2].val,\n            args=kids[3].val,\n            subjectexpr=kids[4].val,\n            except_expr=kids[5].val,\n            commands=kids[6].val,\n        )\n\n\nclass DropConcreteConstraintStmt(Nonterm):\n    def reduce_DropConstraint(self, *kids):\n        r\"\"\"%reduce DROP CONSTRAINT NodeName\n                    OptConcreteConstraintArgList OptOnExpr OptExceptExpr\"\"\"\n        self.val = qlast.DropConcreteConstraint(\n            name=kids[2].val,\n            args=kids[3].val,\n            subjectexpr=kids[4].val,\n            except_expr=kids[5].val,\n        )\n\n\n#\n# CREATE PSEUDO TYPE\n#\n\ncommands_block(\n    'CreatePseudoType',\n    SetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n)\n\n\nclass CreatePseudoTypeStmt(Nonterm):\n\n    def reduce_CreatePseudoTypeStmt(self, *kids):\n        r\"\"\"%reduce\n            CREATE PSEUDO TYPE NodeName OptCreatePseudoTypeCommandsBlock\n        \"\"\"\n        self.val = qlast.CreatePseudoType(\n            name=kids[3].val,\n            commands=kids[4].val,\n        )\n\n\n#\n# CREATE SCALAR TYPE\n#\n\ncommands_block(\n    'CreateScalarType',\n    SetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    CreateConcreteConstraintStmt)\n\n\nclass CreateScalarTypeStmt(Nonterm):\n    def reduce_CreateAbstractScalarTypeStmt(self, *kids):\n        r\"\"\"%reduce \\\n            CREATE ABSTRACT SCALAR TYPE NodeName \\\n            OptExtending OptCreateScalarTypeCommandsBlock \\\n        \"\"\"\n        self.val = qlast.CreateScalarType(\n            name=kids[4].val,\n            abstract=True,\n            bases=kids[5].val,\n            commands=kids[6].val\n        )\n\n    def reduce_CreateFinalScalarTypeStmt(self, *kids):\n        r\"\"\"%reduce \\\n            CREATE FINAL SCALAR TYPE NodeName \\\n            OptExtending OptCreateScalarTypeCommandsBlock \\\n        \"\"\"\n        # Old dumps (1.0-beta.3 and earlier) specify FINAL for all\n        # scalar types, despite it not doing anything and being\n        # undocumented. So we need to support it in the syntax, and we\n        # reject later it when not reading an old dump.\n        self.val = qlast.CreateScalarType(\n            name=kids[4].val,\n            final=True,\n            bases=kids[5].val,\n            commands=kids[6].val\n        )\n\n    def reduce_CreateScalarTypeStmt(self, *kids):\n        r\"\"\"%reduce \\\n            CREATE SCALAR TYPE NodeName \\\n            OptExtending OptCreateScalarTypeCommandsBlock \\\n        \"\"\"\n        self.val = qlast.CreateScalarType(\n            name=kids[3].val,\n            bases=kids[4].val,\n            commands=kids[5].val\n        )\n\n\n#\n# ALTER SCALAR TYPE\n#\n\ncommands_block(\n    'AlterScalarType',\n    RenameStmt,\n    SetFieldStmt,\n    ResetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    AlterExtending,\n    CreateConcreteConstraintStmt,\n    AlterConcreteConstraintStmt,\n    DropConcreteConstraintStmt,\n    opt=False\n)\n\n\nclass AlterScalarTypeStmt(Nonterm):\n    def reduce_AlterScalarTypeStmt(self, *kids):\n        r\"\"\"%reduce \\\n            ALTER SCALAR TYPE NodeName \\\n            AlterScalarTypeCommandsBlock \\\n        \"\"\"\n        self.val = qlast.AlterScalarType(\n            name=kids[3].val,\n            commands=kids[4].val\n        )\n\n\nclass DropScalarTypeStmt(Nonterm):\n    def reduce_DROP_SCALAR_TYPE_NodeName(self, *kids):\n        self.val = qlast.DropScalarType(name=kids[3].val)\n\n\n#\n# CREATE ANNOTATION\n#\ncommands_block(\n    'CreateAnnotation',\n    CreateAnnotationValueStmt,\n)\n\n\nclass CreateAnnotationStmt(Nonterm):\n    def reduce_CreateAnnotation(self, *kids):\n        r\"\"\"%reduce CREATE ABSTRACT ANNOTATION NodeName \\\n                    OptCreateAnnotationCommandsBlock\"\"\"\n        self.val = qlast.CreateAnnotation(\n            name=kids[3].val,\n            commands=kids[4].val,\n            inheritable=False,\n        )\n\n    def reduce_CreateInheritableAnnotation(self, *kids):\n        r\"\"\"%reduce CREATE ABSTRACT INHERITABLE ANNOTATION\n                    NodeName OptCreateCommandsBlock\"\"\"\n        self.val = qlast.CreateAnnotation(\n            name=kids[4].val,\n            commands=kids[5].val,\n            inheritable=True,\n        )\n\n\n#\n# ALTER ANNOTATION\n#\ncommands_block(\n    'AlterAnnotation',\n    RenameStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    opt=False,\n)\n\n\nclass AlterAnnotationStmt(Nonterm):\n    def reduce_AlterAnnotation(self, *kids):\n        r\"\"\"%reduce ALTER ABSTRACT ANNOTATION NodeName \\\n                    AlterAnnotationCommandsBlock\"\"\"\n        self.val = qlast.AlterAnnotation(\n            name=kids[3].val,\n            commands=kids[4].val\n        )\n\n\n#\n# DROP ANNOTATION\n#\nclass DropAnnotationStmt(Nonterm):\n    def reduce_DropAnnotation(self, *kids):\n        r\"\"\"%reduce DROP ABSTRACT ANNOTATION NodeName\"\"\"\n        self.val = qlast.DropAnnotation(\n            name=kids[3].val,\n        )\n\n\n#\n# CREATE INDEX\n#\ncommands_block(\n    'CreateIndex',\n    UsingStmt,\n    SetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n)\n\n\ncommands_block(\n    'AlterIndex',\n    UsingStmt,\n    RenameStmt,\n    SetFieldStmt,\n    ResetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    opt=False)\n\n\nclass CreateIndexStmt(\n    Nonterm,\n    commondl.ProcessIndexMixin,\n):\n    def reduce_CreateIndex(self, *kids):\n        r\"\"\"%reduce CREATE ABSTRACT INDEX NodeName \\\n                    OptExtendingSimple OptCreateIndexCommandsBlock\"\"\"\n        self.val = qlast.CreateIndex(\n            name=kids[3].val,\n            bases=kids[4].val,\n            **self._process_sql_body(kids[5])\n        )\n\n    def reduce_CreateIndex_CreateFunctionArgs(self, *kids):\n        r\"\"\"%reduce CREATE ABSTRACT INDEX NodeName IndexExtArgList \\\n                    OptExtendingSimple OptCreateIndexCommandsBlock\"\"\"\n        bases = kids[5].val\n        params, kwargs = self._process_params_or_kwargs(bases, kids[4].val)\n\n        self.val = qlast.CreateIndex(\n            name=kids[3].val,\n            params=params,\n            kwargs=kwargs,\n            bases=bases,\n            **self._process_sql_body(kids[6])\n        )\n\n\n#\n# ALTER INDEX\n#\nclass AlterIndexStmt(Nonterm, commondl.ProcessIndexMixin):\n    def reduce_AlterIndex(self, *kids):\n        r\"\"\"%reduce ALTER ABSTRACT INDEX NodeName \\\n                    AlterIndexCommandsBlock\"\"\"\n        self.val = qlast.AlterIndex(\n            name=kids[3].val,\n            **self._process_sql_body(kids[4])\n        )\n\n\n#\n# DROP INDEX\n#\nclass DropIndexStmt(Nonterm):\n    def reduce_DropIndex(self, *kids):\n        r\"\"\"%reduce DROP ABSTRACT INDEX NodeName\"\"\"\n        self.val = qlast.DropIndex(\n            name=kids[3].val\n        )\n\n\n#\n# CREATE CONCRETE INDEX\n#\nclass CreateConcreteIndexStmt(Nonterm, commondl.ProcessIndexMixin):\n    def reduce_CreateConcreteDefaultIndex(self, *kids):\n        r\"\"\"%reduce CREATE OptDeferred INDEX OnExpr OptExceptExpr\n                    OptCreateCommandsBlock\n        \"\"\"\n        self.val = qlast.CreateConcreteIndex(\n            name=qlast.ObjectRef(module='__', name='idx', span=kids[2].span),\n            expr=kids[3].val,\n            except_expr=kids[4].val,\n            deferred=kids[1].val,\n            commands=kids[5].val,\n        )\n\n    def reduce_CreateConcreteIndex(self, *kids):\n        r\"\"\"%reduce CREATE OptDeferred INDEX NodeName\n                    OptIndexExtArgList OnExpr OptExceptExpr\n                    OptCreateCommandsBlock\n        \"\"\"\n        kwargs = self._process_arguments(kids[4].val)\n        self.val = qlast.CreateConcreteIndex(\n            name=kids[3].val,\n            kwargs=kwargs,\n            expr=kids[5].val,\n            except_expr=kids[6].val,\n            deferred=kids[1].val,\n            commands=kids[7].val,\n        )\n\n\n#\n# ALTER CONCRETE INDEX\n#\n\nclass AlterDeferredStmt(Nonterm):\n    def reduce_DROP_DEFERRED(self, *kids):\n        self.val = qlast.SetField(\n            name='deferred',\n            value=qlast.Constant.boolean(False, span=self.span),\n            special_syntax=True,\n        )\n\n    def reduce_SET_DEFERRED(self, *kids):\n        self.val = qlast.SetField(\n            name='deferred',\n            value=qlast.Constant.boolean(True, span=self.span),\n            special_syntax=True,\n        )\n\n\ncommands_block(\n    'AlterConcreteIndex',\n    SetFieldStmt,\n    ResetFieldStmt,\n    AlterOwnedStmt,\n    AlterDeferredStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    opt=False)\n\n\nclass AlterConcreteIndexStmt(Nonterm, commondl.ProcessIndexMixin):\n    def reduce_AlterConcreteIndex(self, *kids):\n        r\"\"\"%reduce ALTER INDEX OnExpr OptExceptExpr \\\n                    AlterConcreteIndexCommandsBlock \\\n        \"\"\"\n        self.val = qlast.AlterConcreteIndex(\n            name=qlast.ObjectRef(module='__', name='idx', span=kids[1].span),\n            expr=kids[2].val,\n            except_expr=kids[3].val,\n            commands=kids[4].val,\n        )\n\n    def reduce_AlterConcreteNamedIndex(self, *kids):\n        r\"\"\"%reduce ALTER INDEX NodeName OptIndexExtArgList OnExpr \\\n                    OptExceptExpr \\\n                    AlterConcreteIndexCommandsBlock \\\n        \"\"\"\n        kwargs = self._process_arguments(kids[3].val)\n        self.val = qlast.AlterConcreteIndex(\n            name=kids[2].val,\n            kwargs=kwargs,\n            expr=kids[4].val,\n            except_expr=kids[5].val,\n            commands=kids[6].val,\n        )\n\n\ncommands_block(\n    'DropConcreteIndex',\n    SetFieldStmt,\n    opt=True,\n)\n\n\n#\n# DROP CONCRETE INDEX\n#\nclass DropConcreteIndexStmt(Nonterm, commondl.ProcessIndexMixin):\n    def reduce_DropConcreteIndex(self, *kids):\n        r\"\"\"%reduce DROP INDEX OnExpr OptExceptExpr \\\n                    OptDropConcreteIndexCommandsBlock \\\n        \"\"\"\n        self.val = qlast.DropConcreteIndex(\n            name=qlast.ObjectRef(module='__', name='idx', span=kids[1].span),\n            expr=kids[2].val,\n            except_expr=kids[3].val,\n            commands=kids[4].val,\n        )\n\n    def reduce_DropConcreteNamedIndex(self, *kids):\n        r\"\"\"%reduce DROP INDEX NodeName OptIndexExtArgList OnExpr \\\n                    OptExceptExpr \\\n                    OptDropConcreteIndexCommandsBlock \\\n        \"\"\"\n        kwargs = self._process_arguments(kids[3].val)\n        self.val = qlast.DropConcreteIndex(\n            name=kids[2].val,\n            kwargs=kwargs,\n            expr=kids[4].val,\n            except_expr=kids[5].val,\n            commands=kids[6].val,\n        )\n\n\n#\n# CREATE INDEX MATCH\n#\ncommands_block(\n    'CreateIndexMatch',\n    CreateAnnotationValueStmt,\n)\n\n\nclass CreateIndexMatchStmt(Nonterm):\n    def reduce_CreateIndexMatch(self, *kids):\n        r\"\"\"%reduce CREATE INDEX MATCH FOR TypeName USING NodeName \\\n                    OptCreateIndexMatchCommandsBlock\"\"\"\n        self.val = qlast.CreateIndexMatch(\n            valid_type=kids[4].val,\n            name=kids[6].val,\n            commands=kids[7].val,\n        )\n\n\n#\n# DROP INDEX MATCH\n#\nclass DropIndexMatchStmt(Nonterm):\n    def reduce_DropIndexMatch(self, *kids):\n        r\"\"\"%reduce DROP INDEX MATCH FOR TypeName USING NodeName\"\"\"\n        self.val = qlast.DropIndexMatch(\n            valid_type=kids[4].val,\n            name=kids[6].val,\n        )\n\n\n#\n# CREATE REWRITE\n#\n\ncommands_block(\n    'CreateRewrite',\n    CreateAnnotationValueStmt,\n    SetFieldStmt,\n)\n\n\nclass CreateRewriteStmt(Nonterm):\n    def reduce_CreateRewrite(self, *kids):\n        \"\"\"%reduce\n            CREATE REWRITE RewriteKindList\n            USING ParenExpr\n            OptCreateRewriteCommandsBlock\n        \"\"\"\n        _, _, kinds, _, expr, commands = kids\n        self.val = qlast.CreateRewrite(\n            kinds=kinds.val,\n            expr=expr.val,\n            commands=commands.val,\n        )\n\n\ncommands_block(\n    'AlterRewrite',\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    SetFieldStmt,\n    ResetFieldStmt,\n    UsingStmt,\n    opt=False\n)\n\n\nclass AlterRewriteStmt(Nonterm):\n    def reduce_AlterRewrite(self, _a, _r, kinds, commands):\n        r\"\"\"%reduce \\\n            ALTER REWRITE RewriteKindList \\\n            AlterRewriteCommandsBlock \\\n        \"\"\"\n        self.val = qlast.AlterRewrite(\n            kinds=kinds.val,\n            commands=commands.val,\n        )\n\n\nclass DropRewriteStmt(Nonterm):\n    def reduce_DropRewrite(self, _d, _r, kinds):\n        r\"\"\"%reduce DROP REWRITE RewriteKindList\"\"\"\n        self.val = qlast.DropRewrite(\n            kinds=kinds.val\n        )\n\n\n#\n# CREATE PROPERTY\n#\n\ncommands_block(\n    'CreateProperty',\n    UsingStmt,\n    SetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    commondl.CreateSimpleExtending,\n)\n\n\nclass CreatePropertyStmt(Nonterm):\n    def reduce_CreateProperty(self, *kids):\n        r\"\"\"%reduce CREATE ABSTRACT PROPERTY PtrNodeName OptExtendingSimple \\\n                    OptCreatePropertyCommandsBlock \\\n        \"\"\"\n        vbases, vcommands = commondl.extract_bases(kids[4].val, kids[5].val)\n        self.val = qlast.CreateProperty(\n            name=kids[3].val,\n            bases=vbases,\n            commands=vcommands,\n            abstract=True,\n        )\n\n\n#\n# ALTER PROPERTY\n#\n\ncommands_block(\n    'AlterProperty',\n    RenameStmt,\n    SetFieldStmt,\n    ResetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    CreateRewriteStmt,\n    AlterRewriteStmt,\n    DropRewriteStmt,\n    opt=False\n)\n\n\nclass AlterPropertyStmt(Nonterm):\n    def reduce_AlterProperty(self, *kids):\n        r\"\"\"%reduce \\\n            ALTER ABSTRACT PROPERTY PtrNodeName \\\n            AlterPropertyCommandsBlock \\\n        \"\"\"\n        self.val = qlast.AlterProperty(\n            name=kids[3].val,\n            commands=kids[4].val\n        )\n\n\n#\n# DROP PROPERTY\n#\nclass DropPropertyStmt(Nonterm):\n    def reduce_DropProperty(self, *kids):\n        r\"\"\"%reduce DROP ABSTRACT PROPERTY PtrNodeName\"\"\"\n        self.val = qlast.DropProperty(\n            name=kids[3].val\n        )\n\n\n#\n# CREATE LINK ... { CREATE PROPERTY\n#\n\nclass SetRequiredInCreateStmt(Nonterm):\n\n    def reduce_SET_REQUIRED_OptAlterUsingClause(self, *kids):\n        self.val = qlast.SetPointerOptionality(\n            name='required',\n            value=qlast.Constant.boolean(True, span=self.span),\n            special_syntax=True,\n            fill_expr=kids[2].val,\n        )\n\n\ncommands_block(\n    'CreateConcreteProperty',\n    UsingStmt,\n    SetFieldStmt,\n    SetRequiredInCreateStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    CreateConcreteConstraintStmt,\n    CreateRewriteStmt,\n    commondl.CreateSimpleExtending,\n)\n\n\nclass CreateConcretePropertyStmt(Nonterm):\n    def reduce_CreateRegularProperty(self, *kids):\n        \"\"\"%reduce\n            CREATE OptPtrQuals PROPERTY UnqualifiedPointerName\n            OptExtendingSimple ARROW FullTypeExpr\n            OptCreateConcretePropertyCommandsBlock\n        \"\"\"\n        vbases, vcommands = commondl.extract_bases(kids[4].val, kids[7].val)\n        self.val = qlast.CreateConcreteProperty(\n            name=kids[3].val,\n            bases=vbases,\n            is_required=kids[1].val.required,\n            cardinality=kids[1].val.cardinality,\n            target=kids[6].val,\n            commands=vcommands,\n        )\n\n    def reduce_CreateRegularPropertyNew(self, *kids):\n        \"\"\"%reduce\n            CREATE OptPtrQuals PROPERTY UnqualifiedPointerName\n            OptExtendingSimple COLON FullTypeExpr\n            OptCreateConcretePropertyCommandsBlock\n        \"\"\"\n        vbases, vcommands = commondl.extract_bases(kids[4].val, kids[7].val)\n        self.val = qlast.CreateConcreteProperty(\n            name=kids[3].val,\n            bases=vbases,\n            is_required=kids[1].val.required,\n            cardinality=kids[1].val.cardinality,\n            target=kids[6].val,\n            commands=vcommands,\n        )\n\n    def reduce_CreateComputableProperty(self, *kids):\n        \"\"\"%reduce\n            CREATE OptPtrQuals PROPERTY UnqualifiedPointerName ASSIGN GenExpr\n        \"\"\"\n        self.val = qlast.CreateConcreteProperty(\n            name=kids[3].val,\n            is_required=kids[1].val.required,\n            cardinality=kids[1].val.cardinality,\n            target=kids[5].val,\n        )\n\n    def reduce_CreateComputablePropertyWithUsing(self, *kids):\n        \"\"\"%reduce\n            CREATE OptPtrQuals PROPERTY UnqualifiedPointerName\n            OptCreateConcretePropertyCommandsBlock\n        \"\"\"\n        cmds = kids[4].val\n        target = None\n\n        for cmd in cmds:\n            if isinstance(cmd, qlast.SetField) and cmd.name == 'expr':\n                if target is not None:\n                    raise EdgeQLSyntaxError(\n                        f'computed property with more than one expression',\n                        span=kids[3].span)\n                target = cmd.value\n            elif isinstance(cmd, qlast.AlterAddInherit):\n                raise EdgeQLSyntaxError(\n                    f'computed property cannot specify EXTENDING',\n                    span=kids[3].span)\n\n        if target is None:\n            raise EdgeQLSyntaxError(\n                f'computed property without expression',\n                span=kids[3].span)\n\n        self.val = qlast.CreateConcreteProperty(\n            name=kids[3].val,\n            is_required=kids[1].val.required,\n            cardinality=kids[1].val.cardinality,\n            target=target,\n            commands=cmds,\n        )\n\n\n#\n# ALTER LINK/PROPERTY\n#\n\n\nclass OptAlterUsingClause(Nonterm):\n    @parsing.inline(1)\n    def reduce_USING_ParenExpr(self, *kids):\n        pass\n\n    def reduce_empty(self):\n        self.val = None\n\n\nclass SetCardinalityStmt(Nonterm):\n\n    def reduce_SET_SINGLE_OptAlterUsingClause(self, *kids):\n        self.val = qlast.SetPointerCardinality(\n            name='cardinality',\n            value=qlast.Constant.string(\n                qltypes.SchemaCardinality.One,\n                span=kids[1].span,\n            ),\n            special_syntax=True,\n            conv_expr=kids[2].val,\n        )\n\n    def reduce_SET_MULTI(self, *kids):\n        self.val = qlast.SetPointerCardinality(\n            name='cardinality',\n            value=qlast.Constant.string(\n                qltypes.SchemaCardinality.Many,\n                span=kids[1].span,\n            ),\n            special_syntax=True,\n        )\n\n    def reduce_RESET_CARDINALITY_OptAlterUsingClause(self, *kids):\n        self.val = qlast.SetPointerCardinality(\n            name='cardinality',\n            value=None,\n            special_syntax=True,\n            conv_expr=kids[2].val,\n        )\n\n\nclass SetRequiredStmt(Nonterm):\n\n    def reduce_SET_REQUIRED_OptAlterUsingClause(self, *kids):\n        self.val = qlast.SetPointerOptionality(\n            name='required',\n            value=qlast.Constant.boolean(True, span=self.span),\n            special_syntax=True,\n            fill_expr=kids[2].val,\n        )\n\n    def reduce_SET_OPTIONAL(self, *kids):\n        self.val = qlast.SetPointerOptionality(\n            name='required',\n            value=qlast.Constant.boolean(False, span=self.span),\n            special_syntax=True,\n        )\n\n    def reduce_DROP_REQUIRED(self, *kids):\n        # TODO: Raise a DeprecationWarning once we have facility for that.\n        self.val = qlast.SetPointerOptionality(\n            name='required',\n            value=qlast.Constant.boolean(False, span=self.span),\n            special_syntax=True,\n        )\n\n    def reduce_RESET_OPTIONALITY(self, *kids):\n        self.val = qlast.SetPointerOptionality(\n            name='required',\n            value=None,\n            special_syntax=True,\n        )\n\n\nclass SetPointerTypeStmt(Nonterm):\n\n    def reduce_SETTYPE_FullTypeExpr_OptAlterUsingClause(self, *kids):\n        self.val = qlast.SetPointerType(\n            value=kids[1].val,\n            cast_expr=kids[2].val,\n        )\n\n    def reduce_RESET_TYPE(self, *kids):\n        self.val = qlast.SetPointerType(\n            value=None,\n        )\n\n\ncommands_block(\n    'AlterConcreteProperty',\n    UsingStmt,\n    RenameStmt,\n    SetFieldStmt,\n    ResetFieldStmt,\n    AlterOwnedStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    SetPointerTypeStmt,\n    SetCardinalityStmt,\n    SetRequiredStmt,\n    AlterSimpleExtending,\n    CreateConcreteConstraintStmt,\n    AlterConcreteConstraintStmt,\n    DropConcreteConstraintStmt,\n    CreateRewriteStmt,\n    AlterRewriteStmt,\n    DropRewriteStmt,\n    opt=False\n)\n\n\nclass AlterConcretePropertyStmt(Nonterm):\n    def reduce_AlterProperty(self, *kids):\n        r\"\"\"%reduce \\\n            ALTER PROPERTY UnqualifiedPointerName \\\n            AlterConcretePropertyCommandsBlock \\\n        \"\"\"\n        self.val = qlast.AlterConcreteProperty(\n            name=kids[2].val,\n            commands=kids[3].val\n        )\n\n\n#\n# ALTER LINK ... { DROP PROPERTY\n#\n\nclass DropConcretePropertyStmt(Nonterm):\n    def reduce_DropProperty(self, *kids):\n        r\"\"\"%reduce \\\n            DROP PROPERTY UnqualifiedPointerName \\\n        \"\"\"\n        self.val = qlast.DropConcreteProperty(\n            name=kids[2].val\n        )\n\n\n#\n# CREATE LINK\n#\n\ncommands_block(\n    'CreateLink',\n    SetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    CreateConcreteConstraintStmt,\n    CreateConcretePropertyStmt,\n    CreateConcreteIndexStmt,\n    CreateRewriteStmt,\n    commondl.CreateSimpleExtending,\n)\n\n\nclass CreateLinkStmt(Nonterm):\n    def reduce_CreateLink(self, *kids):\n        r\"\"\"%reduce \\\n            CREATE ABSTRACT LINK PtrNodeName OptExtendingSimple \\\n            OptCreateLinkCommandsBlock \\\n        \"\"\"\n        vbases, vcommands = commondl.extract_bases(\n            kids[4].val,\n            kids[5].val,\n        )\n        self.val = qlast.CreateLink(\n            name=kids[3].val,\n            bases=vbases,\n            commands=vcommands,\n            abstract=True,\n        )\n\n\n#\n# ALTER LINK\n#\n\ncommands_block(\n    'AlterLink',\n    RenameStmt,\n    SetFieldStmt,\n    ResetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    AlterSimpleExtending,\n    CreateConcreteConstraintStmt,\n    AlterConcreteConstraintStmt,\n    DropConcreteConstraintStmt,\n    CreateConcretePropertyStmt,\n    AlterConcretePropertyStmt,\n    DropConcretePropertyStmt,\n    CreateConcreteIndexStmt,\n    AlterConcreteIndexStmt,\n    DropConcreteIndexStmt,\n    CreateRewriteStmt,\n    AlterRewriteStmt,\n    DropRewriteStmt,\n    opt=False\n)\n\n\nclass AlterLinkStmt(Nonterm):\n    def reduce_AlterLink(self, *kids):\n        r\"\"\"%reduce \\\n            ALTER ABSTRACT LINK PtrNodeName \\\n            AlterLinkCommandsBlock \\\n        \"\"\"\n        self.val = qlast.AlterLink(\n            name=kids[3].val,\n            commands=kids[4].val\n        )\n\n\n#\n# DROP LINK\n#\n\ncommands_block(\n    'DropLink',\n    DropConcreteConstraintStmt,\n    DropConcreteConstraintStmt,\n    DropConcretePropertyStmt,\n    DropConcreteIndexStmt,\n)\n\n\nclass DropLinkStmt(Nonterm):\n    def reduce_DropLink(self, *kids):\n        r\"\"\"%reduce \\\n            DROP ABSTRACT LINK PtrNodeName \\\n            OptDropLinkCommandsBlock \\\n        \"\"\"\n        self.val = qlast.DropLink(\n            name=kids[3].val,\n            commands=kids[4].val\n        )\n\n\n#\n# CREATE TYPE ... { CREATE LINK\n#\n\ncommands_block(\n    'CreateConcreteLink',\n    UsingStmt,\n    SetFieldStmt,\n    SetRequiredInCreateStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    CreateConcreteConstraintStmt,\n    CreateConcretePropertyStmt,\n    CreateConcreteIndexStmt,\n    commondl.OnTargetDeleteStmt,\n    commondl.OnSourceDeleteStmt,\n    CreateRewriteStmt,\n    commondl.CreateSimpleExtending,\n)\n\n\nclass CreateConcreteLinkStmt(Nonterm):\n    def reduce_CreateRegularLink(self, *kids):\n        \"\"\"%reduce\n            CREATE OptPtrQuals LINK UnqualifiedPointerName OptExtendingSimple\n            ARROW FullTypeExpr OptCreateConcreteLinkCommandsBlock\n        \"\"\"\n        vbases, vcommands = commondl.extract_bases(kids[4].val, kids[7].val)\n        self.val = qlast.CreateConcreteLink(\n            name=kids[3].val,\n            bases=vbases,\n            is_required=kids[1].val.required,\n            cardinality=kids[1].val.cardinality,\n            target=kids[6].val,\n            commands=vcommands,\n        )\n\n    def reduce_CreateRegularLinkNew(self, *kids):\n        \"\"\"%reduce\n            CREATE OptPtrQuals LINK UnqualifiedPointerName OptExtendingSimple\n            COLON FullTypeExpr OptCreateConcreteLinkCommandsBlock\n        \"\"\"\n        vbases, vcommands = commondl.extract_bases(kids[4].val, kids[7].val)\n        self.val = qlast.CreateConcreteLink(\n            name=kids[3].val,\n            bases=vbases,\n            is_required=kids[1].val.required,\n            cardinality=kids[1].val.cardinality,\n            target=kids[6].val,\n            commands=vcommands\n        )\n\n    def reduce_CreateComputableLink(self, *kids):\n        \"\"\"%reduce\n            CREATE OptPtrQuals LINK UnqualifiedPointerName ASSIGN GenExpr\n        \"\"\"\n        self.val = qlast.CreateConcreteLink(\n            name=kids[3].val,\n            is_required=kids[1].val.required,\n            cardinality=kids[1].val.cardinality,\n            target=kids[5].val,\n        )\n\n    def reduce_CreateComputableLinkWithUsing(self, *kids):\n        \"\"\"%reduce\n            CREATE OptPtrQuals LINK UnqualifiedPointerName\n            OptCreateConcreteLinkCommandsBlock\n        \"\"\"\n        cmds = kids[4].val\n        target = None\n\n        for cmd in cmds:\n            if isinstance(cmd, qlast.SetField) and cmd.name == 'expr':\n                if target is not None:\n                    raise EdgeQLSyntaxError(\n                        f'computed link with more than one expression',\n                        span=kids[3].span)\n                target = cmd.value\n            elif isinstance(cmd, qlast.AlterAddInherit):\n                raise EdgeQLSyntaxError(\n                    f'computed link cannot specify EXTENDING',\n                    span=kids[3].span)\n\n        if target is None:\n            raise EdgeQLSyntaxError(\n                f'computed link without expression',\n                span=kids[3].span)\n\n        self.val = qlast.CreateConcreteLink(\n            name=kids[3].val,\n            is_required=kids[1].val.required,\n            cardinality=kids[1].val.cardinality,\n            target=target,\n            commands=cmds,\n        )\n\n\nclass OnTargetDeleteResetStmt(Nonterm):\n    def reduce_RESET_ON_TARGET_DELETE(self, *kids):\n        self.val = qlast.OnTargetDelete(cascade=None)\n\n\nclass OnSourceDeleteResetStmt(Nonterm):\n    def reduce_RESET_ON_SOURCE_DELETE(self, *kids):\n        self.val = qlast.OnSourceDelete(cascade=None)\n\n\ncommands_block(\n    'AlterConcreteLink',\n    UsingStmt,\n    RenameStmt,\n    SetFieldStmt,\n    ResetFieldStmt,\n    AlterOwnedStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    SetCardinalityStmt,\n    SetRequiredStmt,\n    SetPointerTypeStmt,\n    AlterSimpleExtending,\n    CreateConcreteConstraintStmt,\n    AlterConcreteConstraintStmt,\n    DropConcreteConstraintStmt,\n    CreateConcretePropertyStmt,\n    AlterConcretePropertyStmt,\n    DropConcretePropertyStmt,\n    CreateConcreteIndexStmt,\n    AlterConcreteIndexStmt,\n    DropConcreteIndexStmt,\n    commondl.OnTargetDeleteStmt,\n    commondl.OnSourceDeleteStmt,\n    OnTargetDeleteResetStmt,\n    OnSourceDeleteResetStmt,\n    CreateRewriteStmt,\n    AlterRewriteStmt,\n    DropRewriteStmt,\n    opt=False\n)\n\n\nclass AlterConcreteLinkStmt(Nonterm):\n    def reduce_AlterLink(self, *kids):\n        r\"\"\"%reduce \\\n            ALTER LINK UnqualifiedPointerName AlterConcreteLinkCommandsBlock \\\n        \"\"\"\n        self.val = qlast.AlterConcreteLink(\n            name=kids[2].val,\n            commands=kids[3].val\n        )\n\n\ncommands_block(\n    'DropConcreteLink',\n    DropConcreteConstraintStmt,\n    DropConcretePropertyStmt,\n    DropConcreteIndexStmt,\n)\n\n\nclass DropConcreteLinkStmt(Nonterm):\n    def reduce_DropLink(self, *kids):\n        r\"\"\"%reduce \\\n            DROP LINK UnqualifiedPointerName \\\n            OptDropConcreteLinkCommandsBlock \\\n        \"\"\"\n        self.val = qlast.DropConcreteLink(\n            name=kids[2].val,\n            commands=kids[3].val\n        )\n\n\n#\n# CREATE ACCESS POLICY\n#\n\ncommands_block(\n    'CreateAccessPolicy',\n    CreateAnnotationValueStmt,\n    SetFieldStmt,\n)\n\n\nclass CreateAccessPolicyStmt(Nonterm):\n    def reduce_CreateAccessPolicy(self, *kids):\n        \"\"\"%reduce\n            CREATE ACCESS POLICY UnqualifiedPointerName\n            OptWhenBlock AccessPolicyAction AccessKindList\n            OptUsingBlock\n            OptCreateAccessPolicyCommandsBlock\n        \"\"\"\n        self.val = qlast.CreateAccessPolicy(\n            name=kids[3].val,\n            condition=kids[4].val,\n            action=kids[5].val,\n            access_kinds=[y for x in kids[6].val for y in x],\n            expr=kids[7].val,\n            commands=kids[8].val,\n        )\n\n\nclass AccessPermStmt(Nonterm):\n    def reduce_AccessPolicyAction_AccessKindList(self, *kids):\n        self.val = qlast.SetAccessPerms(\n            action=kids[0].val,\n            access_kinds=[y for x in kids[1].val for y in x],\n        )\n\n\nclass AccessUsingStmt(Nonterm):\n    def reduce_USING_ParenExpr(self, *kids):\n        self.val = qlast.SetField(\n            name='expr',\n            value=kids[1].val,\n            special_syntax=True,\n        )\n\n    def reduce_RESET_EXPRESSION(self, *kids):\n        self.val = qlast.SetField(\n            name='expr',\n            value=None,\n            special_syntax=True,\n        )\n\n\nclass AccessWhenStmt(Nonterm):\n\n    def reduce_WHEN_ParenExpr(self, *kids):\n        self.val = qlast.SetField(\n            name='condition',\n            value=kids[1].val,\n            special_syntax=True,\n        )\n\n    def reduce_RESET_WHEN(self, *kids):\n        self.val = qlast.SetField(\n            name='condition',\n            value=None,\n            special_syntax=True,\n        )\n\n\ncommands_block(\n    'AlterAccessPolicy',\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    RenameStmt,\n    AccessPermStmt,\n    AccessUsingStmt,\n    AccessWhenStmt,\n    SetFieldStmt,\n    ResetFieldStmt,\n    opt=False\n)\n\n\nclass AlterAccessPolicyStmt(Nonterm):\n    def reduce_AlterAccessPolicy(self, *kids):\n        r\"\"\"%reduce \\\n            ALTER ACCESS POLICY UnqualifiedPointerName \\\n            AlterAccessPolicyCommandsBlock \\\n        \"\"\"\n        self.val = qlast.AlterAccessPolicy(\n            name=kids[3].val,\n            commands=kids[4].val,\n        )\n\n\nclass DropAccessPolicyStmt(Nonterm):\n    def reduce_DropAccessPolicy(self, *kids):\n        r\"\"\"%reduce DROP ACCESS POLICY UnqualifiedPointerName\"\"\"\n        self.val = qlast.DropAccessPolicy(\n            name=kids[3].val\n        )\n\n\n#\n# CREATE TRIGGER\n#\n\ncommands_block(\n    'CreateTrigger',\n    CreateAnnotationValueStmt,\n    SetFieldStmt,\n)\n\n\nclass CreateTriggerStmt(Nonterm):\n    def reduce_CreateTrigger(self, *kids):\n        \"\"\"%reduce\n            CREATE TRIGGER UnqualifiedPointerName\n            TriggerTiming TriggerKindList\n            FOR TriggerScope\n            OptWhenBlock\n            DO ParenExpr\n            OptCreateTriggerCommandsBlock\n        \"\"\"\n        _, _, name, timing, kinds, _, scope, when, _, expr, commands = kids\n        self.val = qlast.CreateTrigger(\n            name=name.val,\n            timing=timing.val,\n            kinds=kinds.val,\n            scope=scope.val,\n            expr=expr.val,\n            condition=when.val,\n            commands=commands.val,\n        )\n\n\n# TODO: commands to change timing/kind/scope?\ncommands_block(\n    'AlterTrigger',\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    RenameStmt,\n    UsingStmt,\n    AccessWhenStmt,\n    SetFieldStmt,\n    ResetFieldStmt,\n    opt=False\n)\n\n\nclass AlterTriggerStmt(Nonterm):\n    def reduce_AlterTrigger(self, *kids):\n        r\"\"\"%reduce \\\n            ALTER TRIGGER UnqualifiedPointerName \\\n            AlterTriggerCommandsBlock \\\n        \"\"\"\n        _, _, name, commands = kids\n        self.val = qlast.AlterTrigger(\n            name=name.val,\n            commands=commands.val,\n        )\n\n\nclass DropTriggerStmt(Nonterm):\n    def reduce_DropTrigger(self, *kids):\n        r\"\"\"%reduce DROP TRIGGER UnqualifiedPointerName\"\"\"\n        _, _, name = kids\n        self.val = qlast.DropTrigger(\n            name=name.val\n        )\n\n\n#\n# CREATE TYPE\n#\n\ncommands_block(\n    'CreateObjectType',\n    SetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    CreateConcretePropertyStmt,\n    AlterConcretePropertyStmt,\n    CreateConcreteLinkStmt,\n    AlterConcreteLinkStmt,\n    CreateConcreteConstraintStmt,\n    AlterConcreteConstraintStmt,\n    CreateConcreteIndexStmt,\n    AlterConcreteIndexStmt,\n    CreateAccessPolicyStmt,\n    AlterAccessPolicyStmt,\n    CreateTriggerStmt,\n    AlterTriggerStmt,\n)\n\n\nclass CreateObjectTypeStmt(Nonterm):\n    def reduce_CreateAbstractObjectTypeStmt(self, *kids):\n        r\"\"\"%reduce \\\n            CREATE ABSTRACT TYPE NodeName \\\n            OptExtendingSimple OptCreateObjectTypeCommandsBlock \\\n        \"\"\"\n        _, _, _, name, bases, commands = kids\n        self.val = qlast.CreateObjectType(\n            name=name.val,\n            bases=bases.val,\n            abstract=True,\n            commands=commands.val,\n        )\n\n    def reduce_CreateRegularObjectTypeStmt(self, *kids):\n        r\"\"\"%reduce \\\n            CREATE TYPE NodeName \\\n            OptExtendingSimple OptCreateObjectTypeCommandsBlock \\\n        \"\"\"\n        _, _, name, bases, commands = kids\n        self.val = qlast.CreateObjectType(\n            name=name.val,\n            bases=bases.val,\n            abstract=False,\n            commands=commands.val,\n        )\n\n\n#\n# ALTER TYPE\n#\n\ncommands_block(\n    'AlterObjectType',\n    RenameStmt,\n    SetFieldStmt,\n    ResetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    AlterSimpleExtending,\n    CreateConcretePropertyStmt,\n    AlterConcretePropertyStmt,\n    DropConcretePropertyStmt,\n    CreateConcreteLinkStmt,\n    AlterConcreteLinkStmt,\n    DropConcreteLinkStmt,\n    CreateConcreteConstraintStmt,\n    AlterConcreteConstraintStmt,\n    DropConcreteConstraintStmt,\n    CreateConcreteIndexStmt,\n    AlterConcreteIndexStmt,\n    DropConcreteIndexStmt,\n    CreateAccessPolicyStmt,\n    AlterAccessPolicyStmt,\n    DropAccessPolicyStmt,\n    CreateTriggerStmt,\n    AlterTriggerStmt,\n    DropTriggerStmt,\n    opt=False\n)\n\n\nclass AlterObjectTypeStmt(Nonterm):\n    def reduce_AlterObjectTypeStmt(self, *kids):\n        r\"\"\"%reduce \\\n            ALTER TYPE NodeName \\\n            AlterObjectTypeCommandsBlock \\\n        \"\"\"\n        self.val = qlast.AlterObjectType(\n            name=kids[2].val,\n            commands=kids[3].val\n        )\n\n\n#\n# DROP TYPE\n#\n\ncommands_block(\n    'DropObjectType',\n    DropConcretePropertyStmt,\n    DropConcreteLinkStmt,\n    DropConcreteConstraintStmt,\n    DropConcreteIndexStmt\n)\n\n\nclass DropObjectTypeStmt(Nonterm):\n    def reduce_DropObjectType(self, *kids):\n        r\"\"\"%reduce \\\n            DROP TYPE \\\n            NodeName OptDropObjectTypeCommandsBlock \\\n        \"\"\"\n        self.val = qlast.DropObjectType(\n            name=kids[2].val,\n            commands=kids[3].val\n        )\n\n\n#\n# CREATE ALIAS\n#\n\ncommands_block(\n    'CreateAlias',\n    UsingStmt,\n    SetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    opt=False\n)\n\n\nclass CreateAliasStmt(Nonterm):\n    def reduce_CreateAliasShortStmt(self, *kids):\n        r\"\"\"%reduce\n            CREATE ALIAS NodeName ASSIGN GenExpr\n        \"\"\"\n        self.val = qlast.CreateAlias(\n            name=kids[2].val,\n            commands=[\n                qlast.SetField(\n                    name='expr',\n                    value=kids[4].val,\n                    special_syntax=True,\n                    span=self.span,\n                )\n            ]\n        )\n\n    def reduce_CreateAliasRegularStmt(self, *kids):\n        r\"\"\"%reduce\n            CREATE ALIAS NodeName\n            CreateAliasCommandsBlock\n        \"\"\"\n        self.val = qlast.CreateAlias(\n            name=kids[2].val,\n            commands=kids[3].val,\n        )\n\n\n#\n# ALTER ALIAS\n#\n\ncommands_block(\n    'AlterAlias',\n    UsingStmt,\n    RenameStmt,\n    SetFieldStmt,\n    ResetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    opt=False\n)\n\n\nclass AlterAliasStmt(Nonterm):\n    def reduce_AlterAliasStmt(self, *kids):\n        r\"\"\"%reduce\n            ALTER ALIAS NodeName\n            AlterAliasCommandsBlock\n        \"\"\"\n        self.val = qlast.AlterAlias(\n            name=kids[2].val,\n            commands=kids[3].val\n        )\n\n\n#\n# DROP ALIAS\n#\n\nclass DropAliasStmt(Nonterm):\n    def reduce_DropAlias(self, *kids):\n        r\"\"\"%reduce\n            DROP ALIAS NodeName\n        \"\"\"\n        self.val = qlast.DropAlias(\n            name=kids[2].val,\n        )\n\n\n#\n# CREATE MODULE\n#\nclass CreateModuleStmt(Nonterm):\n    def reduce_CREATE_MODULE_ModuleName_OptIfNotExists_OptCreateCommandsBlock(\n        self, *kids\n    ):\n        self.val = qlast.CreateModule(\n            name=qlast.ObjectRef(\n                module=None, name='::'.join(kids[2].val), span=kids[2].span\n            ),\n            create_if_not_exists=kids[3].val,\n            commands=kids[4].val\n        )\n\n\n#\n# ALTER MODULE\n#\nclass AlterModuleStmt(Nonterm):\n    def reduce_ALTER_MODULE_ModuleName_AlterCommandsBlock(self, *kids):\n        self.val = qlast.AlterModule(\n            name=qlast.ObjectRef(\n                module=None, name='::'.join(kids[2].val), span=kids[2].span\n            ),\n            commands=kids[3].val\n        )\n\n\n#\n# DROP MODULE\n#\nclass DropModuleStmt(Nonterm):\n    def reduce_DROP_MODULE_ModuleName(self, *kids):\n        self.val = qlast.DropModule(\n            name=qlast.ObjectRef(\n                module=None, name='::'.join(kids[2].val), span=kids[2].span\n            )\n        )\n\n\n#\n# CREATE FUNCTION\n#\n\n\ncommands_block(\n    'CreateFunction',\n    commondl.FromFunction,\n    SetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    opt=False\n)\n\n\nclass CreateFunctionStmt(Nonterm, commondl.ProcessFunctionBlockMixin):\n    def reduce_CreateFunction(self, *kids):\n        r\"\"\"%reduce CREATE FUNCTION NodeName CreateFunctionArgs \\\n                FunctionResult CreateFunctionCommandsBlock\n        \"\"\"\n        self.val = qlast.CreateFunction(\n            name=kids[2].val,\n            params=kids[3].val,\n            returning=kids[4].val.result_type,\n            returning_typemod=kids[4].val.type_qualifier,\n            **self._process_function_body(kids[5])\n        )\n\n\nclass DropFunctionStmt(Nonterm):\n    def reduce_DropFunction(self, *kids):\n        r\"\"\"%reduce DROP FUNCTION NodeName CreateFunctionArgs\"\"\"\n        self.val = qlast.DropFunction(\n            name=kids[2].val,\n            params=kids[3].val)\n\n\n#\n# ALTER FUNCTION\n#\n\ncommands_block(\n    'AlterFunction',\n    commondl.FromFunction,\n    SetFieldStmt,\n    ResetFieldStmt,\n    RenameStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    opt=False\n)\n\n\nclass AlterFunctionStmt(Nonterm, commondl.ProcessFunctionBlockMixin):\n    def reduce_AlterFunctionStmt(self, *kids):\n        \"\"\"%reduce\n           ALTER FUNCTION NodeName CreateFunctionArgs\n           AlterFunctionCommandsBlock\n        \"\"\"\n        self.val = qlast.AlterFunction(\n            name=kids[2].val,\n            params=kids[3].val,\n            **self._process_function_body(kids[4], optional_using=True)\n        )\n\n\n#\n# CREATE OPERATOR\n#\n\nclass OperatorKind(Nonterm):\n\n    def reduce_INFIX(self, *kids):\n        self.val = qltypes.OperatorKind.Infix\n\n    def reduce_POSTFIX(self, *kids):\n        self.val = qltypes.OperatorKind.Postfix\n\n    def reduce_PREFIX(self, *kids):\n        self.val = qltypes.OperatorKind.Prefix\n\n    def reduce_TERNARY(self, *kids):\n        self.val = qltypes.OperatorKind.Ternary\n\n\nSQL_OP_RE = r\"([^(]+)(?:\\(([\\w\\.]*(?:,\\s*[\\w\\.]*)*)\\))?\"\n\n\nclass OperatorCode(Nonterm):\n\n    def reduce_USING_Identifier_OPERATOR_BaseStringConstant(self, *kids):\n        lang = commondl._parse_language(kids[1])\n        if lang != qlast.Language.SQL:\n            raise EdgeQLSyntaxError(\n                f'{lang} language is not supported in USING OPERATOR clause',\n                span=kids[1].span) from None\n\n        m = re.match(SQL_OP_RE, kids[3].val.value)\n        if not m:\n            raise EdgeQLSyntaxError(\n                f'invalid syntax for USING OPERATOR clause',\n                span=kids[3].span) from None\n\n        sql_operator = (m.group(1),)\n        if m.group(2):\n            sql_operator += tuple(op.strip() for op in m.group(2).split(\",\"))\n\n        self.val = qlast.OperatorCode(\n            language=lang, from_operator=sql_operator)\n\n    def reduce_USING_Identifier_FUNCTION_BaseStringConstant(self, *kids):\n        lang = commondl._parse_language(kids[1])\n        if lang != qlast.Language.SQL:\n            raise EdgeQLSyntaxError(\n                f'{lang} language is not supported in USING FUNCTION clause',\n                span=kids[1].span) from None\n\n        m = re.match(SQL_OP_RE, kids[3].val.value)\n        if not m:\n            raise EdgeQLSyntaxError(\n                f'invalid syntax for USING FUNCTION clause',\n                span=kids[3].span) from None\n\n        sql_function = (m.group(1),)\n        if m.group(2):\n            sql_function += tuple(op.strip() for op in m.group(2).split(','))\n\n        self.val = qlast.OperatorCode(\n            language=lang, from_function=sql_function)\n\n    def reduce_USING_Identifier_BaseStringConstant(self, *kids):\n        lang = commondl._parse_language(kids[1])\n        if lang != qlast.Language.SQL:\n            raise EdgeQLSyntaxError(\n                f'{lang} language is not supported in USING clause',\n                span=kids[1].span) from None\n\n        self.val = qlast.OperatorCode(language=lang,\n                                      code=kids[2].val.value)\n\n    def reduce_USING_Identifier_EXPRESSION(self, *kids):\n        lang = commondl._parse_language(kids[1])\n        if lang != qlast.Language.SQL:\n            raise EdgeQLSyntaxError(\n                f'{lang} language is not supported in USING clause',\n                span=kids[1].span) from None\n\n        self.val = qlast.OperatorCode(language=lang)\n\n\ncommands_block(\n    'CreateOperator',\n    SetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    OperatorCode,\n    opt=False\n)\n\n\nclass OptCreateOperatorCommandsBlock(Nonterm):\n\n    @parsing.inline(0)\n    def reduce_CreateOperatorCommandsBlock(self, *kids):\n        pass\n\n    def reduce_empty(self, *kids):\n        self.val = []\n\n\nclass CreateOperatorStmt(Nonterm):\n\n    def reduce_CreateOperatorStmt(self, *kids):\n        r\"\"\"%reduce\n            CREATE OperatorKind OPERATOR NodeName CreateFunctionArgs\n            FunctionResult CreateOperatorCommandsBlock\n        \"\"\"\n        self.val = qlast.CreateOperator(\n            kind=kids[1].val,\n            name=kids[3].val,\n            params=kids[4].val,\n            returning_typemod=kids[5].val.type_qualifier,\n            returning=kids[5].val.result_type,\n            **self._process_operator_body(kids[6])\n        )\n\n    def reduce_CreateAbstractOperatorStmt(self, *kids):\n        r\"\"\"%reduce\n            CREATE ABSTRACT OperatorKind OPERATOR NodeName CreateFunctionArgs\n            FunctionResult OptCreateOperatorCommandsBlock\n        \"\"\"\n        self.val = qlast.CreateOperator(\n            kind=kids[2].val,\n            name=kids[4].val,\n            params=kids[5].val,\n            returning_typemod=kids[6].val.type_qualifier,\n            returning=kids[6].val.result_type,\n            abstract=True,\n            **self._process_operator_body(kids[7], abstract=True)\n        )\n\n    def _process_operator_body(self, block, abstract: bool = False):\n        props: dict[str, typing.Any] = {}\n\n        commands = []\n        from_operator = None\n        from_function = None\n        from_expr = False\n        code = None\n\n        for node in block.val:\n            if isinstance(node, qlast.OperatorCode):\n                if abstract:\n                    raise errors.InvalidOperatorDefinitionError(\n                        'unexpected USING clause in abstract '\n                        'operator definition',\n                        span=node.span,\n                    )\n\n                if node.from_function:\n                    if from_function is not None:\n                        raise errors.InvalidOperatorDefinitionError(\n                            'more than one USING FUNCTION clause',\n                            span=node.span)\n                    from_function = node.from_function\n\n                elif node.from_operator:\n                    if from_operator is not None:\n                        raise errors.InvalidOperatorDefinitionError(\n                            'more than one USING OPERATOR clause',\n                            span=node.span)\n                    from_operator = node.from_operator\n\n                elif node.code:\n                    if code is not None:\n                        raise errors.InvalidOperatorDefinitionError(\n                            'more than one USING <code> clause',\n                            span=node.span)\n                    code = node.code\n\n                else:\n                    # USING SQL EXPRESSION\n                    from_expr = True\n            else:\n                commands.append(node)\n\n        if not abstract:\n            if (code is None and from_operator is None\n                    and from_function is None\n                    and not from_expr):\n                raise errors.InvalidOperatorDefinitionError(\n                    'CREATE OPERATOR requires at least one USING clause',\n                    span=block.span)\n\n            else:\n                if from_expr and (from_operator or from_function or code):\n                    raise errors.InvalidOperatorDefinitionError(\n                        'USING SQL EXPRESSION is mutually exclusive with '\n                        'other USING variants',\n                        span=block.span)\n\n                props['code'] = qlast.OperatorCode(\n                    language=qlast.Language.SQL,\n                    from_function=from_function,\n                    from_operator=from_operator,\n                    from_expr=from_expr,\n                    code=code,\n                    span=self.span,\n                )\n\n        if commands:\n            props['commands'] = commands\n\n        return props\n\n\n#\n# ALTER OPERATOR\n#\n\ncommands_block(\n    'AlterOperator',\n    SetFieldStmt,\n    ResetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    opt=False\n)\n\n\nclass AlterOperatorStmt(Nonterm):\n    def reduce_AlterOperatorStmt(self, *kids):\n        \"\"\"%reduce\n           ALTER OperatorKind OPERATOR NodeName CreateFunctionArgs\n           AlterOperatorCommandsBlock\n        \"\"\"\n        self.val = qlast.AlterOperator(\n            kind=kids[1].val,\n            name=kids[3].val,\n            params=kids[4].val,\n            commands=kids[5].val\n        )\n\n\n#\n# DROP OPERATOR\n#\n\nclass DropOperatorStmt(Nonterm):\n    def reduce_DropOperator(self, *kids):\n        \"\"\"%reduce\n           DROP OperatorKind OPERATOR NodeName CreateFunctionArgs\n        \"\"\"\n        self.val = qlast.DropOperator(\n            kind=kids[1].val,\n            name=kids[3].val,\n            params=kids[4].val,\n        )\n\n\n#\n# CREATE CAST\n#\n\n\nclass CastUseValue(typing.NamedTuple):\n\n    use: str\n\n\nclass CastAllowedUse(Nonterm):\n\n    def reduce_ALLOW_IMPLICIT(self, *kids):\n        self.val = CastUseValue(use=kids[1].val.upper())\n\n    def reduce_ALLOW_ASSIGNMENT(self, *kids):\n        self.val = CastUseValue(use=kids[1].val.upper())\n\n\nclass CastCode(Nonterm):\n\n    def reduce_USING_Identifier_FUNCTION_BaseStringConstant(self, *kids):\n        lang = commondl._parse_language(kids[1])\n        if lang not in {qlast.Language.SQL, qlast.Language.EdgeQL}:\n            raise EdgeQLSyntaxError(\n                f'{lang} language is not supported in USING FUNCTION clause',\n                span=kids[1].span) from None\n\n        self.val = qlast.CastCode(language=lang,\n                                  from_function=kids[3].val.value)\n\n    def reduce_USING_Identifier_BaseStringConstant(self, *kids):\n        lang = commondl._parse_language(kids[1])\n        if lang not in {qlast.Language.SQL, qlast.Language.EdgeQL}:\n            raise EdgeQLSyntaxError(\n                f'{lang} language is not supported in USING clause',\n                span=kids[1].span) from None\n\n        self.val = qlast.CastCode(language=lang,\n                                  code=kids[2].val.value)\n\n    def reduce_USING_Identifier_CAST(self, *kids):\n        lang = commondl._parse_language(kids[1])\n        if lang != qlast.Language.SQL:\n            raise EdgeQLSyntaxError(\n                f'{lang} language is not supported in USING CAST clause',\n                span=kids[1].span) from None\n\n        self.val = qlast.CastCode(language=lang, from_cast=True)\n\n    def reduce_USING_Identifier_EXPRESSION(self, *kids):\n        lang = commondl._parse_language(kids[1])\n        if lang != qlast.Language.SQL:\n            raise EdgeQLSyntaxError(\n                f'{lang} language is not supported in USING EXPRESSION clause',\n                span=kids[1].span) from None\n\n        self.val = qlast.CastCode(language=lang)\n\n\ncommands_block(\n    'CreateCast',\n    SetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    CastCode,\n    CastAllowedUse,\n    opt=False\n)\n\n\nclass CreateCastStmt(Nonterm):\n\n    def reduce_CreateCastStmt(self, *kids):\n        r\"\"\"%reduce\n            CREATE CAST FROM TypeName TO TypeName\n            CreateCastCommandsBlock\n        \"\"\"\n        self.val = qlast.CreateCast(\n            from_type=kids[3].val,\n            to_type=kids[5].val,\n            **self._process_cast_body(kids[6])\n        )\n\n    def _process_cast_body(self, block):\n        props = {}\n\n        commands = []\n        from_function = None\n        from_expr = False\n        from_cast = False\n        allow_implicit = False\n        allow_assignment = False\n        code = None\n\n        for node in block.val:\n            if isinstance(node, qlast.CastCode):\n                if node.from_function:\n                    if from_function is not None:\n                        raise EdgeQLSyntaxError(\n                            'more than one USING FUNCTION clause',\n                            span=node.span)\n                    from_function = node.from_function\n\n                elif node.code:\n                    if code is not None:\n                        raise EdgeQLSyntaxError(\n                            'more than one USING <code> clause',\n                            span=node.span)\n                    code = node.code\n\n                elif node.from_cast:\n                    # USING SQL CAST\n\n                    if from_cast:\n                        raise EdgeQLSyntaxError(\n                            'more than one USING CAST clause',\n                            span=node.span)\n\n                    from_cast = True\n\n                else:\n                    # USING SQL EXPRESSION\n\n                    if from_expr:\n                        raise EdgeQLSyntaxError(\n                            'more than one USING EXPRESSION clause',\n                            span=node.span)\n\n                    from_expr = True\n\n            elif isinstance(node, CastUseValue):\n\n                if node.use == 'IMPLICIT':\n                    allow_implicit = True\n                elif node.use == 'ASSIGNMENT':\n                    allow_assignment = True\n                else:\n                    raise EdgeQLSyntaxError(\n                        'unexpected ALLOW clause',\n                        span=node.span)\n\n            else:\n                commands.append(node)\n\n        if (code is None and from_function is None\n                and not from_expr and not from_cast):\n            raise EdgeQLSyntaxError(\n                'CREATE CAST requires at least one USING clause',\n                span=block.span)\n\n        else:\n            if from_expr and (from_function or code or from_cast):\n                raise EdgeQLSyntaxError(\n                    'USING SQL EXPRESSION is mutually exclusive with other '\n                    'USING variants',\n                    span=block.span)\n\n            if from_cast and (from_function or code or from_expr):\n                raise EdgeQLSyntaxError(\n                    'USING SQL CAST is mutually exclusive with other '\n                    'USING variants',\n                    span=block.span)\n\n            props['code'] = qlast.CastCode(\n                language=qlast.Language.SQL,\n                from_function=from_function,\n                from_expr=from_expr,\n                from_cast=from_cast,\n                code=code,\n                span=self.span,\n            )\n\n            props['allow_implicit'] = allow_implicit\n            props['allow_assignment'] = allow_assignment\n\n        if commands:\n            props['commands'] = commands\n\n        return props\n\n\n#\n# ALTER CAST\n#\n\ncommands_block(\n    'AlterCast',\n    SetFieldStmt,\n    ResetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    opt=False\n)\n\n\nclass AlterCastStmt(Nonterm):\n    def reduce_AlterCastStmt(self, *kids):\n        \"\"\"%reduce\n           ALTER CAST FROM TypeName TO TypeName\n           AlterCastCommandsBlock\n        \"\"\"\n        self.val = qlast.AlterCast(\n            from_type=kids[3].val,\n            to_type=kids[5].val,\n            commands=kids[6].val,\n        )\n\n\n#\n# DROP CAST\n#\n\nclass DropCastStmt(Nonterm):\n    def reduce_DropCastStmt(self, *kids):\n        \"\"\"%reduce\n           DROP CAST FROM TypeName TO TypeName\n        \"\"\"\n        self.val = qlast.DropCast(\n            from_type=kids[3].val,\n            to_type=kids[5].val,\n        )\n\n#\n# CREATE GLOBAL\n#\n\n\ncommands_block(\n    'CreateGlobal',\n    UsingStmt,\n    SetFieldStmt,\n    CreateAnnotationValueStmt,\n)\n\n\nclass CreateGlobalStmt(Nonterm):\n    def reduce_CreateRegularGlobal(self, *kids):\n        \"\"\"%reduce\n            CREATE OptPtrQuals GLOBAL NodeName\n            ARROW FullTypeExpr\n            OptCreateGlobalCommandsBlock\n        \"\"\"\n        self.val = qlast.CreateGlobal(\n            name=kids[3].val,\n            is_required=kids[1].val.required,\n            cardinality=kids[1].val.cardinality,\n            target=kids[5].val,\n            commands=kids[6].val,\n        )\n\n    def reduce_CreateRegularGlobalNew(self, *kids):\n        \"\"\"%reduce\n            CREATE OptPtrQuals GLOBAL NodeName\n            COLON FullTypeExpr\n            OptCreateGlobalCommandsBlock\n        \"\"\"\n        self.val = qlast.CreateGlobal(\n            name=kids[3].val,\n            is_required=kids[1].val.required,\n            cardinality=kids[1].val.cardinality,\n            target=kids[5].val,\n            commands=kids[6].val,\n        )\n\n    def reduce_CreateComputableGlobal(self, *kids):\n        \"\"\"%reduce\n            CREATE OptPtrQuals GLOBAL NodeName ASSIGN GenExpr\n        \"\"\"\n        self.val = qlast.CreateGlobal(\n            name=kids[3].val,\n            is_required=kids[1].val.required,\n            cardinality=kids[1].val.cardinality,\n            target=kids[5].val,\n        )\n\n    def reduce_CreateComputableGlobalWithUsing(self, *kids):\n        \"\"\"%reduce\n            CREATE OptPtrQuals GLOBAL NodeName\n            OptCreateConcretePropertyCommandsBlock\n        \"\"\"\n        cmds = kids[4].val\n        target = None\n\n        for cmd in cmds:\n            if isinstance(cmd, qlast.SetField) and cmd.name == 'expr':\n                if target is not None:\n                    raise EdgeQLSyntaxError(\n                        f'computed global with more than one expression',\n                        span=kids[3].span)\n                target = cmd.value\n\n        if target is None:\n            raise EdgeQLSyntaxError(\n                f'computed global without expression',\n                span=kids[3].span)\n\n        self.val = qlast.CreateGlobal(\n            name=kids[3].val,\n            is_required=kids[1].val.required,\n            cardinality=kids[1].val.cardinality,\n            target=target,\n            commands=cmds,\n        )\n\n\nclass SetGlobalTypeStmt(Nonterm):\n\n    def reduce_SETTYPE_FullTypeExpr_OptAlterUsingClause(self, *kids):\n        self.val = qlast.SetGlobalType(\n            value=kids[1].val,\n            cast_expr=kids[2].val,\n        )\n\n    def reduce_SETTYPE_FullTypeExpr_RESET_TO_DEFAULT(self, *kids):\n        self.val = qlast.SetGlobalType(\n            value=kids[1].val,\n            reset_value=True,\n        )\n\n    def reduce_RESET_TYPE(self, *kids):\n        self.val = qlast.SetGlobalType(\n            value=None,\n        )\n\n\ncommands_block(\n    'AlterGlobal',\n    UsingStmt,\n    RenameStmt,\n    SetFieldStmt,\n    ResetFieldStmt,\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    SetGlobalTypeStmt,\n    SetCardinalityStmt,\n    SetRequiredStmt,\n    opt=False\n)\n\n\nclass AlterGlobalStmt(Nonterm):\n    def reduce_AlterGlobal(self, *kids):\n        r\"\"\"%reduce \\\n            ALTER GLOBAL NodeName \\\n            AlterGlobalCommandsBlock \\\n        \"\"\"\n        self.val = qlast.AlterGlobal(\n            name=kids[2].val,\n            commands=kids[3].val\n        )\n\n\nclass DropGlobalStmt(Nonterm):\n    def reduce_DropGlobal(self, *kids):\n        r\"\"\"%reduce DROP GLOBAL NodeName\"\"\"\n        self.val = qlast.DropGlobal(\n            name=kids[2].val\n        )\n\n\n#\n# CREATE PERMISSION\n#\ncommands_block(\n    'CreatePermission',\n    CreateAnnotationValueStmt,\n)\n\n\nclass CreatePermissionStmt(Nonterm):\n    def reduce_CreatePermission(self, *kids):\n        \"\"\"%reduce\n            CREATE PERMISSION NodeName\n            OptCreatePermissionCommandsBlock\n        \"\"\"\n        _, _, name, commands = kids\n        self.val = qlast.CreatePermission(\n            name=name.val,\n            commands=commands.val,\n        )\n\n\n#\n# ALTER PERMISSION\n#\ncommands_block(\n    'AlterPermission',\n    CreateAnnotationValueStmt,\n    AlterAnnotationValueStmt,\n    DropAnnotationValueStmt,\n    RenameStmt,\n    opt=False\n)\n\n\nclass AlterPermissionStmt(Nonterm):\n    def reduce_AlterPermission(self, *kids):\n        r\"\"\"%reduce \\\n            ALTER PERMISSION NodeName \\\n            AlterPermissionCommandsBlock \\\n        \"\"\"\n        _, _, name, commands = kids\n        self.val = qlast.AlterPermission(\n            name=name.val,\n            commands=commands.val,\n        )\n\n\n#\n# DROP PERMISSION\n#\nclass DropPermissionStmt(Nonterm):\n    def reduce_DropPermission(self, *kids):\n        r\"\"\"%reduce DROP PERMISSION NodeName\"\"\"\n        _, _, name = kids\n        self.val = qlast.DropPermission(\n            name=name.val\n        )\n\n\n#\n# MIGRATIONS\n#\n\n\nclass MigrationStmt(Nonterm):\n\n    @parsing.inline(0)\n    def reduce_CreateMigrationStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterMigrationStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AlterCurrentMigrationStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_StartMigrationStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AbortMigrationStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_PopulateMigrationStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CommitMigrationStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DropMigrationStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_ResetSchemaStmt(self, *kids):\n        pass\n\n\nclass MigrationBody(typing.NamedTuple):\n\n    body: qlast.NestedQLBlock\n    fields: list[qlast.SetField]\n\n\nclass CreateMigrationBodyBlock(NestedQLBlock):\n\n    @property\n    def allowed_fields(self) -> frozenset[str]:\n        return frozenset({'message', 'generated_by'})\n\n    @property\n    def result(self) -> typing.Any:\n        return MigrationBody\n\n\ncommands_block(\n    'CreateMigration',\n    NestedQLBlockStmt,\n    opt=True,\n    production_tpl=CreateMigrationBodyBlock,\n)\n\n\nclass MigrationNameAndParent(typing.NamedTuple):\n\n    name: typing.Optional[qlast.ObjectRef]\n    parent: typing.Optional[qlast.ObjectRef]\n\n\nclass OptMigrationNameParentName(Nonterm):\n\n    def reduce_ShortNodeName_ONTO_ShortNodeName(self, *kids):\n        self.val = MigrationNameAndParent(\n            name=kids[0].val,\n            parent=kids[2].val,\n        )\n\n    def reduce_ShortNodeName(self, *kids):\n        self.val = MigrationNameAndParent(\n            name=kids[0].val,\n            parent=None,\n        )\n\n    def reduce_empty(self):\n        self.val = MigrationNameAndParent(\n            name=None,\n            parent=None,\n        )\n\n\nclass CreateMigrationStmt(Nonterm):\n\n    def reduce_CreateMigration(self, *kids):\n        r\"\"\"%reduce\n            CREATE MIGRATION OptMigrationNameParentName\n            OptCreateMigrationCommandsBlock\n        \"\"\"\n        self.val = qlast.CreateMigration(\n            name=kids[2].val.name,\n            parent=kids[2].val.parent,\n            body=kids[3].val.body,\n            commands=kids[3].val.fields,\n        )\n\n    def reduce_CreateAppliedMigration(self, *kids):\n        r\"\"\"%reduce\n            CREATE APPLIED MIGRATION OptMigrationNameParentName\n            OptCreateMigrationCommandsBlock\n        \"\"\"\n        self.val = qlast.CreateMigration(\n            name=kids[3].val.name,\n            parent=kids[3].val.parent,\n            body=kids[4].val.body,\n            metadata_only=True,\n            commands=kids[4].val.fields,\n        )\n\n\nclass StartMigrationStmt(Nonterm):\n\n    def reduce_StartMigration(self, *kids):\n        r\"\"\"%reduce START MIGRATION TO SDLCommandBlock\"\"\"\n\n        declarations = kids[3].val\n        commondl._validate_declarations(declarations)\n        self.val = qlast.StartMigration(\n            target=qlast.Schema(\n                declarations=declarations,\n                span=kids[3].span,\n            ),\n        )\n\n    def reduce_StartMigrationToCommitted(self, *kids):\n        r\"\"\"%reduce START MIGRATION TO COMMITTED SCHEMA\"\"\"\n        self.val = qlast.StartMigration(\n            target=qlast.CommittedSchema(span=self.span)\n        )\n\n    def reduce_StartMigrationRewrite(self, *kids):\n        r\"\"\"%reduce START MIGRATION REWRITE\"\"\"\n        self.val = qlast.StartMigrationRewrite()\n\n\nclass PopulateMigrationStmt(Nonterm):\n\n    def reduce_POPULATE_MIGRATION(self, *kids):\n        self.val = qlast.PopulateMigration()\n\n\nclass AlterCurrentMigrationStmt(Nonterm):\n\n    def reduce_ALTER_CURRENT_MIGRATION_REJECT_PROPOSED(self, *kids):\n        self.val = qlast.AlterCurrentMigrationRejectProposed()\n\n\nclass AbortMigrationStmt(Nonterm):\n\n    def reduce_ABORT_MIGRATION(self, *kids):\n        self.val = qlast.AbortMigration()\n\n    def reduce_ABORT_MIGRATION_REWRITE(self, *kids):\n        self.val = qlast.AbortMigrationRewrite()\n\n\nclass CommitMigrationStmt(Nonterm):\n\n    def reduce_COMMIT_MIGRATION(self, *kids):\n        self.val = qlast.CommitMigration()\n\n    def reduce_COMMIT_MIGRATION_REWRITE(self, *kids):\n        self.val = qlast.CommitMigrationRewrite()\n\n\ncommands_block(\n    'AlterMigration',\n    SetFieldStmt,\n    ResetFieldStmt,\n    opt=False,\n)\n\n\nclass AlterMigrationStmt(Nonterm):\n    def reduce_AlterMigration(self, *kids):\n        r\"\"\"%reduce ALTER MIGRATION NodeName \\\n                    AlterMigrationCommandsBlock \\\n        \"\"\"\n        self.val = qlast.AlterMigration(\n            name=kids[2].val,\n            commands=kids[3].val\n        )\n\n\nclass DropMigrationStmt(Nonterm):\n    def reduce_DROP_MIGRATION_NodeName(self, *kids):\n        self.val = qlast.DropMigration(\n            name=kids[2].val,\n        )\n\n\nclass ResetSchemaStmt(Nonterm):\n    def reduce_ResetSchemaTo(self, *kids):\n        r\"\"\"%reduce RESET SCHEMA TO NodeName\"\"\"\n        self.val = qlast.ResetSchema(\n            target=kids[3].val,\n        )\n"
  },
  {
    "path": "edb/edgeql/parser/grammar/expressions.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport collections\nimport typing\n\nfrom edb.common import parsing, span\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom edb import errors\n\nfrom . import keywords\nfrom . import precedence\nfrom . import tokens\n\nfrom .precedence import *  # NOQA\nfrom .tokens import *  # NOQA\n\n\nclass Nonterm(parsing.Nonterm, is_internal=True):\n    pass\n\n\ndef merge_spans(nodes: typing.Iterable[Nonterm]) -> span.Span:\n    return assert_non_null(span.merge_spans(n.span for n in nodes if n.span))\n\n\ndef assert_non_null(span):\n    assert span\n    return span\n\n\nclass ListNonterm(parsing.ListNonterm, element=None, is_internal=True):\n    pass\n\n\n# We have an annoying split between \"simple\" ExprStmt and \"annoying\"\n# ExprStmt. The heart of the issue is we want to allow unparenthesized\n# statements in places like function arguments, but the trailing\n# parenthesis allowed in the BY clause of GROUP conflicts with the\n# commas there.\n#\n# So instead we allow unparenthesized expressions as long as they\n# aren't GROUP (or a FOR <x> IN <whatever> GROUP ...).\nclass ExprStmt(Nonterm):\n    val: qlast.Query\n\n    @parsing.inline(0)\n    def reduce_ExprStmtSimple(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_ExprStmtAnnoying(self, *kids):\n        pass\n\n\nclass ExprStmtSimple(Nonterm):\n    val: qlast.Query\n\n    def reduce_WithBlock_ExprStmtSimpleCore(self, *kids):\n        self.val = kids[1].val\n        self.val.aliases = kids[0].val.aliases\n\n    @parsing.inline(0)\n    def reduce_ExprStmtSimpleCore(self, *kids):\n        pass\n\n\nclass ExprStmtAnnoying(Nonterm):\n    val: qlast.Query\n\n    def reduce_WithBlock_ExprStmtAnnoyingCore(self, *kids):\n        self.val = kids[1].val\n        self.val.aliases = kids[0].val.aliases\n\n    @parsing.inline(0)\n    def reduce_ExprStmtAnnoyingCore(self, *kids):\n        pass\n\n\nclass ExprStmtSimpleCore(Nonterm):\n    val: qlast.Query\n\n    def reduce_Select(self, *kids):\n        r\"%reduce SELECT OptionallyAliasedExpr \\\n                  OptFilterClause OptSortClause OptSelectLimit\"\n\n        offset, limit = kids[4].val\n\n        if offset is not None or limit is not None:\n            subj = qlast.SelectQuery(\n                result=kids[1].val.expr,\n                result_alias=kids[1].val.alias,\n                where=kids[2].val,\n                orderby=kids[3].val,\n                implicit=True,\n                span=merge_spans((kids[0], kids[3]))\n            )\n\n            self.val = qlast.SelectQuery(\n                result=subj,\n                offset=offset,\n                limit=limit,\n            )\n        else:\n            self.val = qlast.SelectQuery(\n                result=kids[1].val.expr,\n                result_alias=kids[1].val.alias,\n                where=kids[2].val,\n                orderby=kids[3].val,\n            )\n\n    def reduce_Insert(self, *kids):\n        r'%reduce INSERT Expr OptUnlessConflictClause'\n\n        subj = kids[1].val\n        unless_conflict = kids[2].val\n\n        if isinstance(subj, qlast.Shape):\n            if not subj.expr:\n                raise errors.EdgeQLSyntaxError(\n                    \"insert shape expressions must have a type name\",\n                    span=subj.span\n                )\n            subj_path = subj.expr\n            shape = subj.elements\n        else:\n            subj_path = subj\n            shape = []\n\n        if isinstance(subj_path, qlast.Path) and \\\n                len(subj_path.steps) == 1 and \\\n                isinstance(subj_path.steps[0], qlast.ObjectRef):\n            objtype = subj_path.steps[0]\n        elif isinstance(subj_path, qlast.IfElse):\n            # Insert attempted on something that looks like a conditional\n            # expression. Aside from it being an error, it also seems that\n            # the intent was to insert something conditionally.\n            raise errors.EdgeQLSyntaxError(\n                f\"INSERT only works with object types, not conditional \"\n                f\"expressions\",\n                hint=(\n                    f\"To resolve this try surrounding the INSERT branch of \"\n                    f\"the conditional expression with parentheses. This way \"\n                    f\"the INSERT will be triggered conditionally in one of \"\n                    f\"the branches.\"\n                ),\n                span=subj_path.span)\n        else:\n            raise errors.EdgeQLSyntaxError(\n                f\"INSERT only works with object types, not arbitrary \"\n                f\"expressions\",\n                hint=(\n                    f\"To resolve this try to surround the entire INSERT \"\n                    f\"statement with parentheses in order to separate it \"\n                    f\"from the rest of the expression.\"\n                ),\n                span=subj_path.span)\n\n        self.val = qlast.InsertQuery(\n            subject=objtype,\n            shape=shape,\n            unless_conflict=unless_conflict,\n        )\n\n    def reduce_Update(self, *kids):\n        \"%reduce UPDATE Expr OptFilterClause SET Shape\"\n        self.val = qlast.UpdateQuery(\n            subject=kids[1].val,\n            where=kids[2].val,\n            shape=kids[4].val,\n        )\n\n    def reduce_Delete(self, *kids):\n        r\"%reduce DELETE Expr \\\n                  OptFilterClause OptSortClause OptSelectLimit\"\n        self.val = qlast.DeleteQuery(\n            subject=kids[1].val,\n            where=kids[2].val,\n            orderby=kids[3].val,\n            offset=kids[4].val[0],\n            limit=kids[4].val[1],\n        )\n\n    def reduce_ForIn(self, *kids):\n        r\"%reduce FOR OptionalOptional Identifier IN AtomicExpr UNION Expr\"\n        _, optional, iterator_alias, _, iterator, _, body = kids\n        self.val = qlast.ForQuery(\n            optional=optional.val,\n            iterator_alias=iterator_alias.val,\n            iterator=iterator.val,\n            result=body.val,\n        )\n\n    def reduce_ForInStmt(self, *kids):\n        r\"%reduce FOR OptionalOptional Identifier IN AtomicExpr ExprStmtSimple\"\n        _, optional, iterator_alias, _, iterator, body = kids\n        self.val = qlast.ForQuery(\n            has_union=False,\n            optional=optional.val,\n            iterator_alias=iterator_alias.val,\n            iterator=iterator.val,\n            result=body.val,\n        )\n\n    def reduce_InternalGroup(self, *kids):\n        r\"%reduce FOR GROUP OptionallyAliasedExpr \\\n                  UsingClause \\\n                  ByClause \\\n                  IN Identifier OptGroupingAlias \\\n                  UNION OptionallyAliasedExpr \\\n                  OptFilterClause OptSortClause \\\n        \"\n        self.val = qlast.InternalGroupQuery(\n            subject=kids[2].val.expr,\n            subject_alias=kids[2].val.alias,\n            using=kids[3].val,\n            by=kids[4].val,\n            group_alias=kids[6].val,\n            grouping_alias=kids[7].val,\n            result_alias=kids[9].val.alias,\n            result=kids[9].val.expr,\n            where=kids[10].val,\n            orderby=kids[11].val,\n        )\n\n\nclass ExprStmtAnnoyingCore(Nonterm):\n    @parsing.inline(0)\n    def reduce_AnnoyingFor(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_SimpleGroup(self, *kids):\n        pass\n\n\n# A \"generalized expression\" that can be either an expression or\n# *most* unparenthesized statements.\n#\n# (Note that a number of places that are *approximately* using this\n# instead need to spell it out more explicitly because it doesn't\n# exactly fit.)\nclass GenExpr(Nonterm):\n    val: qlast.Expr\n\n    @parsing.inline(0)\n    def reduce_Expr(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_ExprStmtSimpleCore(self, *kids):\n        pass\n\n\nclass AliasedExpr(Nonterm):\n    val: qlast.AliasedExpr\n\n    def reduce_Identifier_ASSIGN_Expr(self, *kids):\n        self.val = qlast.AliasedExpr(alias=kids[0].val, expr=kids[2].val)\n\n\n# NOTE: This is intentionally not an AST node, since this structure never\n# makes it to the actual AST and exists solely for parser convenience.\nAliasedExprSpec = collections.namedtuple(\n    'AliasedExprSpec', ['alias', 'expr'], module=__name__)\n\n\nclass OptionallyAliasedExpr(Nonterm):\n    val: AliasedExprSpec\n\n    def reduce_AliasedExpr(self, *kids):\n        val = kids[0].val\n        self.val = AliasedExprSpec(alias=val.alias, expr=val.expr)\n\n    def reduce_Expr(self, *kids):\n        self.val = AliasedExprSpec(alias=None, expr=kids[0].val)\n\n\nclass AliasedExprList(ListNonterm, element=AliasedExpr,\n                      separator=tokens.T_COMMA, allow_trailing_separator=True):\n    val: list[qlast.AliasedExpr]\n\n\nclass GroupingIdent(Nonterm):\n    val: qlast.GroupingAtom\n\n    def reduce_Identifier(self, *kids):\n        self.val = qlast.ObjectRef(name=kids[0].val)\n\n    def reduce_DOT_Identifier(self, *kids):\n        self.val = qlast.Path(\n            partial=True,\n            steps=[\n                qlast.Ptr(\n                    name=kids[1].val,\n                    span=kids[1].span,\n                )\n            ],\n        )\n\n    def reduce_AT_Identifier(self, *kids):\n        self.val = qlast.Path(\n            partial=True,\n            steps=[\n                qlast.Ptr(\n                    name=kids[1].val,\n                    type='property',\n                    span=kids[1].span,\n                )\n            ]\n        )\n\n\nclass GroupingIdentList(ListNonterm, element=GroupingIdent,\n                        separator=tokens.T_COMMA):\n    val: list[qlast.GroupingAtom]\n\n\nclass GroupingAtom(Nonterm):\n    val: qlast.GroupingAtom\n\n    @parsing.inline(0)\n    def reduce_GroupingIdent(self, *kids):\n        pass\n\n    def reduce_LPAREN_GroupingIdentList_RPAREN(self, *kids):\n        self.val = qlast.GroupingIdentList(elements=kids[1].val)\n\n\nclass GroupingAtomList(\n        ListNonterm, element=GroupingAtom, separator=tokens.T_COMMA,\n        allow_trailing_separator=True):\n    val: list[qlast.GroupingAtom]\n\n\nclass GroupingElement(Nonterm):\n    val: qlast.GroupingElement\n\n    def reduce_GroupingAtom(self, *kids):\n        self.val = qlast.GroupingSimple(element=kids[0].val)\n\n    def reduce_LBRACE_GroupingElementList_RBRACE(self, *kids):\n        self.val = qlast.GroupingSets(sets=kids[1].val)\n\n    def reduce_ROLLUP_LPAREN_GroupingAtomList_RPAREN(self, *kids):\n        self.val = qlast.GroupingOperation(oper='rollup', elements=kids[2].val)\n\n    def reduce_CUBE_LPAREN_GroupingAtomList_RPAREN(self, *kids):\n        self.val = qlast.GroupingOperation(oper='cube', elements=kids[2].val)\n\n\nclass GroupingElementList(\n        ListNonterm, element=GroupingElement, separator=tokens.T_COMMA,\n        allow_trailing_separator=True):\n    val: list[qlast.GroupingElement]\n\n\nclass OptionalOptional(Nonterm):\n    val: bool\n\n    def reduce_OPTIONAL(self, *kids):\n        self.val = True\n\n    def reduce_empty(self, *kids):\n        self.val = False\n\n\nclass AnnoyingFor(Nonterm):\n    val: qlast.ForQuery\n\n    def reduce_ForInStmt(self, *kids):\n        r\"%reduce FOR OptionalOptional Identifier IN AtomicExpr \\\n                  ExprStmtAnnoying\"\n        _, optional, iterator_alias, _, iterator, body = kids\n        self.val = qlast.ForQuery(\n            has_union=False,\n            optional=optional.val,\n            iterator_alias=iterator_alias.val,\n            iterator=iterator.val,\n            result=body.val,\n        )\n\n\nclass ByClause(Nonterm):\n    val: list[qlast.GroupingElement]\n\n    @parsing.inline(1)\n    def reduce_BY_GroupingElementList(self, *kids):\n        pass\n\n\nclass UsingClause(Nonterm):\n    val: list[qlast.AliasedExpr]\n\n    @parsing.inline(1)\n    def reduce_USING_AliasedExprList(self, *kids):\n        pass\n\n\nclass OptUsingClause(Nonterm):\n    val: list[qlast.AliasedExpr]\n\n    @parsing.inline(0)\n    def reduce_UsingClause(self, *kids):\n        pass\n\n    def reduce_empty(self, *kids):\n        self.val = None\n\n\nclass SimpleGroup(Nonterm):\n    val: qlast.GroupQuery\n\n    def reduce_Group(self, *kids):\n        r\"%reduce GROUP OptionallyAliasedExpr \\\n                  OptUsingClause \\\n                  ByClause\"\n        self.val = qlast.GroupQuery(\n            subject=kids[1].val.expr,\n            subject_alias=kids[1].val.alias,\n            using=kids[2].val,\n            by=kids[3].val,\n        )\n\n\nclass OptGroupingAlias(Nonterm):\n    val: typing.Optional[qlast.GroupQuery]\n\n    @parsing.inline(1)\n    def reduce_COMMA_Identifier(self, *kids):\n        pass\n\n    def reduce_empty(self, *kids):\n        self.val = None\n\n\nFunctionResultData = collections.namedtuple(\n    'FunctionResultData',\n    ['type_qualifier', 'result_type'],\n    module=__name__\n)\n\n\nclass FunctionResult(Nonterm):\n    def reduce_ARROW_OptTypeQualifier_FunctionType(\n        self, _, type_qualifier, result_type\n    ):\n        self.val = FunctionResultData(\n            type_qualifier=type_qualifier.val,\n            result_type=result_type.val,\n        )\n\n\nWithBlockData = collections.namedtuple(\n    'WithBlockData', ['aliases'], module=__name__)\n\n\nclass WithBlock(Nonterm):\n    def reduce_WITH_WithDeclList(self, *kids):\n        aliases = []\n        for w in kids[1].val:\n            aliases.append(w)\n        self.val = WithBlockData(aliases=aliases)\n\n\nclass AliasDecl(Nonterm):\n    def reduce_MODULE_ModuleName(self, *kids):\n        self.val = qlast.ModuleAliasDecl(\n            module='::'.join(kids[1].val)\n        )\n\n    def reduce_Identifier_AS_MODULE_ModuleName(self, *kids):\n        self.val = qlast.ModuleAliasDecl(\n            alias=kids[0].val,\n            module='::'.join(kids[3].val)\n        )\n\n    @parsing.inline(0)\n    def reduce_AliasedExpr(self, *kids):\n        pass\n\n    def reduce_Identifier_ASSIGN_ExprStmtSimple(self, *kids):\n        self.val = qlast.AliasedExpr(alias=kids[0].val, expr=kids[2].val)\n\n\nclass WithDecl(Nonterm):\n    @parsing.inline(0)\n    def reduce_AliasDecl(self, *kids):\n        pass\n\n\nclass WithDeclList(ListNonterm, element=WithDecl,\n                   separator=tokens.T_COMMA, allow_trailing_separator=True):\n    pass\n\n\nclass Shape(Nonterm):\n    def reduce_LBRACE_RBRACE(self, *kids):\n        self.val = []\n\n    @parsing.inline(1)\n    def reduce_LBRACE_ShapeElementList_RBRACE(self, *kids):\n        pass\n\n\nclass FreeShape(Nonterm):\n    def reduce_LBRACE_FreeComputableShapePointerList_RBRACE(self, *kids):\n        self.val = qlast.Shape(elements=kids[1].val)\n\n\nclass OptAnySubShape(Nonterm):\n    @parsing.inline(1)\n    def reduce_COLON_Shape(self, *_):\n        pass\n\n    def reduce_empty(self, *kids):\n        self.val = []\n\n\nclass ShapeElement(Nonterm):\n    def reduce_ShapeElementWithSubShape(self, *kids):\n        r\"\"\"%reduce ShapePointer \\\n             OptAnySubShape OptFilterClause OptSortClause OptSelectLimit \\\n        \"\"\"\n        self.val = kids[0].val\n        self.val.elements = kids[1].val\n        self.val.where = kids[2].val\n        self.val.orderby = kids[3].val\n        self.val.offset = kids[4].val[0]\n        self.val.limit = kids[4].val[1]\n\n    @parsing.inline(0)\n    def reduce_ComputableShapePointer(self, *kids):\n        pass\n\n\nclass ShapeElementList(ListNonterm, element=ShapeElement,\n                       separator=tokens.T_COMMA, allow_trailing_separator=True):\n    pass\n\n\nclass SimpleShapePath(Nonterm):\n\n    def reduce_PathStepName(self, *kids):\n        from edb.schema import pointers as s_pointers\n\n        steps = [\n            qlast.Ptr(\n                name=kids[0].val.name,\n                direction=s_pointers.PointerDirection.Outbound,\n                span=kids[0].span,\n            ),\n        ]\n\n        self.val = qlast.Path(steps=steps)\n\n    def reduce_AT_PathNodeName(self, *kids):\n        self.val = qlast.Path(\n            steps=[\n                qlast.Ptr(\n                    name=kids[1].val.name,\n                    type='property',\n                    span=kids[1].span,\n                )\n            ]\n        )\n\n\nclass SimpleShapePointer(Nonterm):\n\n    def reduce_SimpleShapePath(self, *kids):\n        self.val = qlast.ShapeElement(\n            expr=kids[0].val\n        )\n\n\n# Shape pointers in free shapes are not allowed to be link\n# properties. This is because we need to be able to distinguish\n# free shapes from set literals with only one token of lookahead\n# (since this is an LL(1) parser) and seeing the := after @ident would\n# require two tokens of lookahead.\nclass FreeSimpleShapePointer(Nonterm):\n\n    def reduce_FreeStepName(self, *kids):\n        from edb.schema import pointers as s_pointers\n\n        steps = [\n            qlast.Ptr(\n                name=kids[0].val.name,\n                direction=s_pointers.PointerDirection.Outbound,\n                span=kids[0].span,\n            ),\n        ]\n\n        self.val = qlast.ShapeElement(\n            expr=qlast.Path(steps=steps, span=self.span)\n        )\n\n\nclass ShapePath(Nonterm):\n    # A form of Path appearing as an element in shapes.\n    #\n    # one-of:\n    #   __type__\n    #   link\n    #   @prop\n    #   [IS ObjectType].link\n    #   [IS Link]@prop - currently not supported\n    #   <splat> (see Splat production for possible syntaxes)\n\n    def reduce_PathStepName_OptTypeIntersection(self, *kids):\n        from edb.schema import pointers as s_pointers\n\n        steps = [\n            qlast.Ptr(\n                name=kids[0].val.name,\n                direction=s_pointers.PointerDirection.Outbound,\n                span=kids[0].span,\n            ),\n        ]\n\n        if kids[1].val is not None:\n            steps.append(kids[1].val)\n\n        self.val = qlast.Path(steps=steps)\n\n    @parsing.inline(0)\n    def reduce_Splat(self, *kids):\n        pass\n\n    def reduce_AT_PathNodeName(self, *kids):\n        self.val = qlast.Path(\n            steps=[\n                qlast.Ptr(\n                    name=kids[1].val.name,\n                    type='property',\n                    span=kids[1].span,\n                )\n            ]\n        )\n\n    def reduce_TypeIntersection_DOT_PathStepName_OptTypeIntersection(\n            self, *kids):\n        from edb.schema import pointers as s_pointers\n\n        steps = [\n            kids[0].val,\n            qlast.Ptr(\n                name=kids[2].val.name,\n                direction=s_pointers.PointerDirection.Outbound,\n                span=kids[2].span,\n            ),\n        ]\n\n        if kids[3].val is not None:\n            steps.append(kids[3].val)\n\n        self.val = qlast.Path(steps=steps)\n\n\n# N.B. the production verbosity below is necessary due to conflicts,\n#      as is the use of PathStepName in place of SimpleTypeName.\nclass Splat(Nonterm):\n    def reduce_STAR(self, *kids):\n        self.val = qlast.Path(steps=[\n            qlast.Splat(depth=1, span=kids[0].span),\n        ])\n\n    def reduce_DOUBLESTAR(self, *kids):\n        self.val = qlast.Path(steps=[\n            qlast.Splat(depth=2, span=kids[0].span),\n        ])\n\n    # Type.*\n    def reduce_PathStepName_DOT_STAR(self, *kids):\n        self.val = qlast.Path(steps=[\n            qlast.Splat(\n                depth=1,\n                type=qlast.TypeName(\n                    maintype=kids[0].val, span=kids[0].span\n                ),\n                span=merge_spans(kids),\n            ),\n        ])\n\n    # Type.**\n    def reduce_PathStepName_DOT_DOUBLESTAR(self, *kids):\n        self.val = qlast.Path(steps=[\n            qlast.Splat(\n                depth=2,\n                type=qlast.TypeName(\n                    maintype=kids[0].val, span=kids[0].span\n                ),\n                span=merge_spans(kids),\n            ),\n        ])\n\n    # [is Foo].*\n    def reduce_TypeIntersection_DOT_STAR(self, *kids):\n        self.val = qlast.Path(steps=[\n            qlast.Splat(\n                depth=1,\n                intersection=kids[0].val,\n                span=merge_spans(kids),\n            ),\n        ])\n\n    # [is Foo].**\n    def reduce_TypeIntersection_DOT_DOUBLESTAR(self, *kids):\n        self.val = qlast.Path(steps=[\n            qlast.Splat(\n                depth=2,\n                intersection=kids[0].val,\n                span=merge_spans(kids),\n            ),\n        ])\n\n    # Type[is Foo].*\n    def reduce_PathStepName_TypeIntersection_DOT_STAR(self, *kids):\n        self.val = qlast.Path(steps=[\n            qlast.Splat(\n                depth=1,\n                type=qlast.TypeName(\n                    maintype=kids[0].val, span=kids[0].span\n                ),\n                intersection=kids[1].val,\n                span=merge_spans(kids),\n            ),\n        ])\n\n    # Type[is Foo].**\n    def reduce_PathStepName_TypeIntersection_DOT_DOUBLESTAR(self, *kids):\n        self.val = qlast.Path(steps=[\n            qlast.Splat(\n                depth=2,\n                type=qlast.TypeName(\n                    maintype=kids[0].val, span=kids[0].span\n                ),\n                intersection=kids[1].val,\n                span=merge_spans(kids),\n            ),\n        ])\n\n    # module::Type.*\n    def reduce_PtrQualifiedNodeName_DOT_STAR(self, *kids):\n        self.val = qlast.Path(steps=[\n            qlast.Splat(\n                type=qlast.TypeName(\n                    maintype=kids[0].val, span=kids[0].span\n                ),\n                depth=1,\n                span=merge_spans(kids),\n            ),\n        ])\n\n    # module::Type.**\n    def reduce_PtrQualifiedNodeName_DOT_DOUBLESTAR(self, *kids):\n        self.val = qlast.Path(steps=[\n            qlast.Splat(\n                type=qlast.TypeName(\n                    maintype=kids[0].val, span=kids[0].span\n                ),\n                depth=2,\n                span=merge_spans(kids),\n            ),\n        ])\n\n    # module::Type[is <type-expr>].*\n    def reduce_PtrQualifiedNodeName_TypeIntersection_DOT_STAR(self, *kids):\n        self.val = qlast.Path(steps=[\n            qlast.Splat(\n                depth=1,\n                type=qlast.TypeName(\n                    maintype=kids[0].val, span=kids[0].span\n                ),\n                intersection=kids[1].val,\n                span=merge_spans(kids),\n            ),\n        ])\n\n    # module::Type[is <type-expr>].**\n    def reduce_PtrQualifiedNodeName_TypeIntersection_DOT_DOUBLESTAR(\n        self,\n        *kids,\n    ):\n        self.val = qlast.Path(steps=[\n            qlast.Splat(\n                depth=2,\n                type=qlast.TypeName(\n                    maintype=kids[0].val, span=kids[0].span\n                ),\n                intersection=kids[1].val,\n                span=merge_spans(kids),\n            ),\n        ])\n\n    # (<type-expr>).*\n    def reduce_ParenTypeExpr_DOT_STAR(self, *kids):\n        self.val = qlast.Path(steps=[\n            qlast.Splat(\n                depth=1,\n                type=kids[0].val,\n                span=merge_spans(kids),\n            ),\n        ])\n\n    # (<type-expr>).**\n    def reduce_ParenTypeExpr_TypeIntersection_DOT_STAR(self, *kids):\n        self.val = qlast.Path(steps=[\n            qlast.Splat(\n                depth=1,\n                type=kids[0].val,\n                intersection=kids[1].val,\n                span=merge_spans(kids),\n            ),\n        ])\n\n    # (<type-expr>)[is <type-expr>].*\n    def reduce_ParenTypeExpr_DOT_DOUBLESTAR(self, *kids):\n        self.val = qlast.Path(steps=[\n            qlast.Splat(\n                depth=2,\n                type=kids[0].val,\n                span=merge_spans(kids),\n            ),\n        ])\n\n    # (<type-expr>)[is <type-expr>].**\n    def reduce_ParenTypeExpr_TypeIntersection_DOT_DOUBLESTAR(self, *kids):\n        self.val = qlast.Path(steps=[\n            qlast.Splat(\n                depth=2,\n                type=kids[0].val,\n                intersection=kids[1].val,\n                span=merge_spans(kids),\n            ),\n        ])\n\n\nclass ShapePointer(Nonterm):\n    def reduce_ShapePath(self, *kids):\n        self.val = qlast.ShapeElement(\n            expr=kids[0].val\n        )\n\n\nclass PtrQualsSpec(typing.NamedTuple):\n    required: typing.Optional[bool] = None\n    cardinality: typing.Optional[qltypes.SchemaCardinality] = None\n\n\nclass PtrQuals(Nonterm):\n    def reduce_OPTIONAL(self, *kids):\n        self.val = PtrQualsSpec(required=False)\n\n    def reduce_REQUIRED(self, *kids):\n        self.val = PtrQualsSpec(required=True)\n\n    def reduce_SINGLE(self, *kids):\n        self.val = PtrQualsSpec(cardinality=qltypes.SchemaCardinality.One)\n\n    def reduce_MULTI(self, *kids):\n        self.val = PtrQualsSpec(cardinality=qltypes.SchemaCardinality.Many)\n\n    def reduce_OPTIONAL_SINGLE(self, *kids):\n        self.val = PtrQualsSpec(\n            required=False, cardinality=qltypes.SchemaCardinality.One)\n\n    def reduce_OPTIONAL_MULTI(self, *kids):\n        self.val = PtrQualsSpec(\n            required=False, cardinality=qltypes.SchemaCardinality.Many)\n\n    def reduce_REQUIRED_SINGLE(self, *kids):\n        self.val = PtrQualsSpec(\n            required=True, cardinality=qltypes.SchemaCardinality.One)\n\n    def reduce_REQUIRED_MULTI(self, *kids):\n        self.val = PtrQualsSpec(\n            required=True, cardinality=qltypes.SchemaCardinality.Many)\n\n\nclass OptPtrQuals(Nonterm):\n\n    def reduce_empty(self, *kids):\n        self.val = PtrQualsSpec()\n\n    @parsing.inline(0)\n    def reduce_PtrQuals(self, *kids):\n        pass\n\n\n# We have to inline the OptPtrQuals here because the parser generator\n# fails to cope with a shift/reduce on a REQUIRED token, since PtrQuals\n# are followed by an ident in this case (unlike in DDL, where it is followed\n# by a keyword).\nclass ComputableShapePointer(Nonterm):\n\n    def reduce_OPTIONAL_SimpleShapePointer_ASSIGN_GenExpr(self, *kids):\n        self.val = kids[1].val\n        self.val.compexpr = kids[3].val\n        self.val.required = False\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[2].span),\n        )\n\n    def reduce_REQUIRED_SimpleShapePointer_ASSIGN_GenExpr(self, *kids):\n        self.val = kids[1].val\n        self.val.compexpr = kids[3].val\n        self.val.required = True\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[2].span),\n        )\n\n    def reduce_MULTI_SimpleShapePointer_ASSIGN_GenExpr(self, *kids):\n        self.val = kids[1].val\n        self.val.compexpr = kids[3].val\n        self.val.cardinality = qltypes.SchemaCardinality.Many\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[2].span),\n        )\n\n    def reduce_SINGLE_SimpleShapePointer_ASSIGN_GenExpr(self, *kids):\n        self.val = kids[1].val\n        self.val.compexpr = kids[3].val\n        self.val.cardinality = qltypes.SchemaCardinality.One\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[2].span),\n        )\n\n    def reduce_OPTIONAL_MULTI_SimpleShapePointer_ASSIGN_GenExpr(self, *kids):\n        self.val = kids[2].val\n        self.val.compexpr = kids[4].val\n        self.val.required = False\n        self.val.cardinality = qltypes.SchemaCardinality.Many\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[3].span),\n        )\n\n    def reduce_OPTIONAL_SINGLE_SimpleShapePointer_ASSIGN_GenExpr(self, *kids):\n        self.val = kids[2].val\n        self.val.compexpr = kids[4].val\n        self.val.required = False\n        self.val.cardinality = qltypes.SchemaCardinality.One\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[3].span),\n        )\n\n    def reduce_REQUIRED_MULTI_SimpleShapePointer_ASSIGN_GenExpr(self, *kids):\n        self.val = kids[2].val\n        self.val.compexpr = kids[4].val\n        self.val.required = True\n        self.val.cardinality = qltypes.SchemaCardinality.Many\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[3].span),\n        )\n\n    def reduce_REQUIRED_SINGLE_SimpleShapePointer_ASSIGN_GenExpr(self, *kids):\n        self.val = kids[2].val\n        self.val.compexpr = kids[4].val\n        self.val.required = True\n        self.val.cardinality = qltypes.SchemaCardinality.One\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[3].span),\n        )\n\n    def reduce_SimpleShapePointer_ASSIGN_GenExpr(self, *kids):\n        self.val = kids[0].val\n        self.val.compexpr = kids[2].val\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[1].span),\n        )\n\n    def reduce_SimpleShapePointer_ADDASSIGN_GenExpr(self, *kids):\n        self.val = kids[0].val\n        self.val.compexpr = kids[2].val\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.APPEND,\n            span=assert_non_null(kids[1].span),\n        )\n\n    def reduce_SimpleShapePointer_REMASSIGN_GenExpr(self, *kids):\n        self.val = kids[0].val\n        self.val.compexpr = kids[2].val\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.SUBTRACT,\n            span=assert_non_null(kids[1].span),\n        )\n\n\n# This is the same as the above ComputableShapePointer, except using\n# FreeSimpleShapePointer and not allowing +=/-=.\nclass FreeComputableShapePointer(Nonterm):\n    def reduce_OPTIONAL_FreeSimpleShapePointer_ASSIGN_GenExpr(self, *kids):\n        self.val = kids[1].val\n        self.val.compexpr = kids[3].val\n        self.val.required = False\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[2].span),\n        )\n\n    def reduce_REQUIRED_FreeSimpleShapePointer_ASSIGN_GenExpr(self, *kids):\n        self.val = kids[1].val\n        self.val.compexpr = kids[3].val\n        self.val.required = True\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[2].span),\n        )\n\n    def reduce_MULTI_FreeSimpleShapePointer_ASSIGN_GenExpr(self, *kids):\n        self.val = kids[1].val\n        self.val.compexpr = kids[3].val\n        self.val.cardinality = qltypes.SchemaCardinality.Many\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[2].span),\n        )\n\n    def reduce_SINGLE_FreeSimpleShapePointer_ASSIGN_GenExpr(self, *kids):\n        self.val = kids[1].val\n        self.val.compexpr = kids[3].val\n        self.val.cardinality = qltypes.SchemaCardinality.One\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[2].span),\n        )\n\n    def reduce_OPTIONAL_MULTI_FreeSimpleShapePointer_ASSIGN_GenExpr(\n        self, *kids\n    ):\n        self.val = kids[2].val\n        self.val.compexpr = kids[4].val\n        self.val.required = False\n        self.val.cardinality = qltypes.SchemaCardinality.Many\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[3].span),\n        )\n\n    def reduce_OPTIONAL_SINGLE_FreeSimpleShapePointer_ASSIGN_GenExpr(\n        self, *kids\n    ):\n        self.val = kids[2].val\n        self.val.compexpr = kids[4].val\n        self.val.required = False\n        self.val.cardinality = qltypes.SchemaCardinality.One\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[3].span),\n        )\n\n    def reduce_REQUIRED_MULTI_FreeSimpleShapePointer_ASSIGN_GenExpr(\n        self, *kids\n    ):\n        self.val = kids[2].val\n        self.val.compexpr = kids[4].val\n        self.val.required = True\n        self.val.cardinality = qltypes.SchemaCardinality.Many\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[3].span),\n        )\n\n    def reduce_REQUIRED_SINGLE_FreeSimpleShapePointer_ASSIGN_GenExpr(\n        self, *kids\n    ):\n        self.val = kids[2].val\n        self.val.compexpr = kids[4].val\n        self.val.required = True\n        self.val.cardinality = qltypes.SchemaCardinality.One\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[3].span),\n        )\n\n    def reduce_FreeSimpleShapePointer_ASSIGN_GenExpr(self, *kids):\n        self.val = kids[0].val\n        self.val.compexpr = kids[2].val\n        self.val.operation = qlast.ShapeOperation(\n            op=qlast.ShapeOp.ASSIGN,\n            span=assert_non_null(kids[1].span),\n        )\n\n\nclass FreeComputableShapePointerList(ListNonterm,\n                                     element=FreeComputableShapePointer,\n                                     separator=tokens.T_COMMA,\n                                     allow_trailing_separator=True):\n    pass\n\n\nclass UnlessConflictSpecifier(Nonterm):\n    def reduce_ON_Expr_ELSE_Expr(self, *kids):\n        self.val = (kids[1].val, kids[3].val)\n\n    def reduce_ON_Expr(self, *kids):\n        self.val = (kids[1].val, None)\n\n    def reduce_empty(self, *kids):\n        self.val = (None, None)\n\n\nclass UnlessConflictCause(Nonterm):\n    @parsing.inline(2)\n    def reduce_UNLESS_CONFLICT_UnlessConflictSpecifier(self, *kids):\n        pass\n\n\nclass OptUnlessConflictClause(Nonterm):\n    @parsing.inline(0)\n    def reduce_UnlessConflictCause(self, *kids):\n        pass\n\n    def reduce_empty(self, *kids):\n        self.val = None\n\n\nclass FilterClause(Nonterm):\n    val: qlast.Expr\n\n    @parsing.inline(1)\n    def reduce_FILTER_Expr(self, *kids):\n        pass\n\n\nclass OptFilterClause(Nonterm):\n    val: typing.Optional[qlast.Expr]\n\n    @parsing.inline(0)\n    def reduce_FilterClause(self, *kids):\n        pass\n\n    def reduce_empty(self, *kids):\n        self.val = None\n\n\nclass SortClause(Nonterm):\n    val: list[qlast.SortExpr]\n\n    @parsing.inline(1)\n    def reduce_ORDERBY_OrderbyList(self, *kids):\n        pass\n\n\nclass OptSortClause(Nonterm):\n    val: list[qlast.SortExpr]\n\n    @parsing.inline(0)\n    def reduce_SortClause(self, *kids):\n        pass\n\n    def reduce_empty(self, *kids):\n        self.val = []\n\n\nclass OrderbyExpr(Nonterm):\n    val: qlast.SortExpr\n\n    def reduce_Expr_OptDirection_OptNonesOrder(self, *kids):\n        self.val = qlast.SortExpr(path=kids[0].val,\n                                  direction=kids[1].val,\n                                  nones_order=kids[2].val)\n\n\nclass OrderbyList(ListNonterm, element=OrderbyExpr,\n                  separator=tokens.T_THEN):\n    val: list[qlast.SortExpr]\n\n\nclass OptSelectLimit(Nonterm):\n    val: tuple[typing.Optional[qlast.Expr], typing.Optional[qlast.Expr]]\n\n    @parsing.inline(0)\n    def reduce_SelectLimit(self, *kids):\n        pass\n\n    def reduce_empty(self, *kids):\n        self.val = (None, None)\n\n\nclass SelectLimit(Nonterm):\n    val: tuple[typing.Optional[qlast.Expr], typing.Optional[qlast.Expr]]\n\n    def reduce_OffsetClause_LimitClause(self, *kids):\n        self.val = (kids[0].val, kids[1].val)\n\n    def reduce_OffsetClause(self, *kids):\n        self.val = (kids[0].val, None)\n\n    def reduce_LimitClause(self, *kids):\n        self.val = (None, kids[0].val)\n\n\nclass OffsetClause(Nonterm):\n    val: qlast.Expr\n\n    @parsing.inline(1)\n    def reduce_OFFSET_Expr(self, *kids):\n        pass\n\n\nclass LimitClause(Nonterm):\n    val: qlast.Expr\n\n    @parsing.inline(1)\n    def reduce_LIMIT_Expr(self, *kids):\n        pass\n\n\nclass OptDirection(Nonterm):\n    def reduce_ASC(self, *kids):\n        self.val = qlast.SortAsc\n\n    def reduce_DESC(self, *kids):\n        self.val = qlast.SortDesc\n\n    def reduce_empty(self, *kids):\n        self.val = qlast.SortDefault\n\n\nclass OptNonesOrder(Nonterm):\n    def reduce_EMPTY_FIRST(self, *kids):\n        self.val = qlast.NonesFirst\n\n    def reduce_EMPTY_LAST(self, *kids):\n        self.val = qlast.NonesLast\n\n    def reduce_empty(self, *kids):\n        self.val = None\n\n\nclass IndirectionEl(Nonterm):\n    def reduce_LBRACKET_Expr_RBRACKET(self, *kids):\n        self.val = qlast.Index(index=kids[1].val)\n\n    def reduce_LBRACKET_Expr_COLON_Expr_RBRACKET(self, *kids):\n        self.val = qlast.Slice(start=kids[1].val, stop=kids[3].val)\n\n    def reduce_LBRACKET_Expr_COLON_RBRACKET(self, *kids):\n        self.val = qlast.Slice(start=kids[1].val, stop=None)\n\n    def reduce_LBRACKET_COLON_Expr_RBRACKET(self, *kids):\n        self.val = qlast.Slice(start=None, stop=kids[2].val)\n\n\nclass ParenExpr(Nonterm):\n    @parsing.inline(1)\n    def reduce_LPAREN_Expr_RPAREN(self, *kids):\n        pass\n\n    @parsing.inline(1)\n    def reduce_LPAREN_ExprStmt_RPAREN(self, *kids):\n        pass\n\n\nclass BaseAtomicExpr(Nonterm):\n    val: qlast.Expr\n    # { ... } | Constant | '(' Expr ')' | FuncExpr\n    # | Tuple | NamedTuple | Collection | Set\n    # | '__source__' | '__subject__'\n    # | '__new__' | '__old__' | '__specified__' | '__default__'\n    # | NodeName | PathStep\n\n    @parsing.inline(0)\n    def reduce_FreeShape(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_Constant(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_StringInterpolation(self, *kids):\n        pass\n\n    def reduce_DUNDERSOURCE(self, kw):\n        self.val = qlast.Path(\n            steps=[\n                qlast.SpecialAnchor(name='__source__', span=kw.span)\n            ]\n        )\n\n    def reduce_DUNDERSUBJECT(self, kw):\n        self.val = qlast.Path(\n            steps=[\n                qlast.SpecialAnchor(name='__subject__', span=kw.span)\n            ]\n        )\n\n    def reduce_DUNDERNEW(self, kw):\n        self.val = qlast.Path(\n            steps=[\n                qlast.SpecialAnchor(name='__new__', span=kw.span)\n            ]\n        )\n\n    def reduce_DUNDEROLD(self, kw):\n        self.val = qlast.Path(\n            steps=[\n                qlast.SpecialAnchor(name='__old__', span=kw.span)\n            ]\n        )\n\n    def reduce_DUNDERSPECIFIED(self, kw):\n        self.val = qlast.Path(\n            steps=[\n                qlast.SpecialAnchor(name='__specified__', span=kw.span)\n            ]\n        )\n\n    def reduce_DUNDERDEFAULT(self, kw):\n        self.val = qlast.Path(\n            steps=[\n                qlast.SpecialAnchor(name='__default__', span=kw.span)\n            ]\n        )\n\n    @parsing.precedence(precedence.P_UMINUS)\n    @parsing.inline(0)\n    def reduce_ParenExpr(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_FuncExpr(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_Tuple(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_Collection(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_Set(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_NamedTuple(self, *kids):\n        pass\n\n    @parsing.precedence(precedence.P_DOT)\n    def reduce_NodeName(self, *kids):\n        self.val = qlast.Path(\n            steps=[\n                qlast.ObjectRef(\n                    name=kids[0].val.name,\n                    module=kids[0].val.module,\n                    span=kids[0].span,\n                )\n            ]\n        )\n\n    @parsing.precedence(precedence.P_DOT)\n    def reduce_PathStep(self, *kids):\n        self.val = qlast.Path(steps=[kids[0].val], partial=True)\n\n\nclass Expr(Nonterm):\n    val: qlast.Expr\n    # BaseAtomicExpr\n    # Path | Expr { ... }\n\n    # | Expr '[' Expr ']'\n    # | Expr '[' Expr ':' Expr ']'\n    # | Expr '[' ':' Expr ']'\n    # | Expr '[' Expr ':' ']'\n    # | Expr '[' IS NodeName ']'\n\n    # | '+' Expr | '-' Expr | Expr '+' Expr | Expr '-' Expr\n    # | Expr '*' Expr | Expr '/' Expr | Expr '%' Expr\n    # | Expr '**' Expr | Expr '<' Expr | Expr '>' Expr\n    # | Expr '=' Expr\n    # | Expr AND Expr | Expr OR Expr | NOT Expr\n    # | Expr LIKE Expr | Expr NOT LIKE Expr\n    # | Expr ILIKE Expr | Expr NOT ILIKE Expr\n    # | Expr IS TypeExpr | Expr IS NOT TypeExpr\n    # | INTROSPECT TypeExpr\n    # | Expr IN Expr | Expr NOT IN Expr\n    # | '<' TypeName '>' Expr\n    # | Expr IF Expr ELSE Expr\n    # | Expr ?? Expr\n    # | Expr UNION Expr | Expr UNION Expr\n    # | DISTINCT Expr\n    # | DETACHED Expr\n    # | GLOBAL Name\n    # | EXISTS Expr\n\n    @parsing.inline(0)\n    def reduce_BaseAtomicExpr(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_Path(self, *kids):\n        pass\n\n    def reduce_Expr_Shape(self, *kids):\n        self.val = qlast.Shape(expr=kids[0].val, elements=kids[1].val)\n\n    def reduce_EXISTS_Expr(self, *kids):\n        self.val = qlast.UnaryOp(op='EXISTS', operand=kids[1].val)\n\n    def reduce_DISTINCT_Expr(self, *kids):\n        self.val = qlast.UnaryOp(op='DISTINCT', operand=kids[1].val)\n\n    def reduce_DETACHED_Expr(self, *kids):\n        self.val = qlast.DetachedExpr(expr=kids[1].val)\n\n    def reduce_GLOBAL_NodeName(self, *kids):\n        self.val = qlast.GlobalExpr(name=kids[1].val)\n\n    def reduce_Expr_IndirectionEl(self, *kids):\n        expr = kids[0].val\n        if isinstance(expr, qlast.Indirection):\n            self.val = expr\n            expr.indirection.append(kids[1].val)\n        else:\n            self.val = qlast.Indirection(arg=expr,\n                                         indirection=[kids[1].val])\n\n    @parsing.precedence(precedence.P_UMINUS)\n    def reduce_PLUS_Expr(self, *kids):\n        self.val = qlast.UnaryOp(op=kids[0].val, operand=kids[1].val)\n\n    @parsing.precedence(precedence.P_UMINUS)\n    def reduce_MINUS_Expr(self, *kids):\n        arg = kids[1].val\n        if isinstance(arg, qlast.Constant) and arg.kind in {\n            qlast.ConstantKind.INTEGER,\n            qlast.ConstantKind.FLOAT,\n            qlast.ConstantKind.BIGINT,\n            qlast.ConstantKind.DECIMAL,\n        }:\n            self.val = type(arg)(value=f'-{arg.value}', kind=arg.kind)\n        else:\n            self.val = qlast.UnaryOp(op=kids[0].val, operand=arg)\n\n    def reduce_Expr_PLUS_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op=kids[1].val,\n                               right=kids[2].val)\n\n    def reduce_Expr_DOUBLEPLUS_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op=kids[1].val,\n                               right=kids[2].val)\n\n    def reduce_Expr_MINUS_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op=kids[1].val,\n                               right=kids[2].val)\n\n    def reduce_Expr_STAR_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op=kids[1].val,\n                               right=kids[2].val)\n\n    def reduce_Expr_SLASH_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op=kids[1].val,\n                               right=kids[2].val)\n\n    def reduce_Expr_DOUBLESLASH_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op=kids[1].val,\n                               right=kids[2].val)\n\n    def reduce_Expr_PERCENT_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op=kids[1].val,\n                               right=kids[2].val)\n\n    def reduce_Expr_CIRCUMFLEX_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op=kids[1].val,\n                               right=kids[2].val)\n\n    @parsing.precedence(precedence.P_DOUBLEQMARK_OP)\n    def reduce_Expr_DOUBLEQMARK_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op=kids[1].val,\n                               right=kids[2].val)\n\n    @parsing.precedence(precedence.P_COMPARE_OP)\n    def reduce_Expr_CompareOp_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op=kids[1].val,\n                               right=kids[2].val)\n\n    def reduce_Expr_AND_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op=kids[1].val.upper(),\n                               right=kids[2].val)\n\n    def reduce_Expr_OR_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op=kids[1].val.upper(),\n                               right=kids[2].val)\n\n    def reduce_NOT_Expr(self, *kids):\n        self.val = qlast.UnaryOp(op=kids[0].val.upper(), operand=kids[1].val)\n\n    def reduce_Expr_LIKE_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op='LIKE',\n                               right=kids[2].val)\n\n    def reduce_Expr_NOT_LIKE_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op='NOT LIKE',\n                               right=kids[3].val)\n\n    def reduce_Expr_ILIKE_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op='ILIKE',\n                               right=kids[2].val)\n\n    def reduce_Expr_NOT_ILIKE_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op='NOT ILIKE',\n                               right=kids[3].val)\n\n    def reduce_Expr_IS_TypeExpr(self, *kids):\n        self.val = qlast.IsOp(left=kids[0].val, op='IS',\n                              right=kids[2].val)\n\n    @parsing.precedence(precedence.P_IS)\n    def reduce_Expr_IS_NOT_TypeExpr(self, *kids):\n        self.val = qlast.IsOp(left=kids[0].val, op='IS NOT',\n                              right=kids[3].val)\n\n    def reduce_INTROSPECT_TypeExpr(self, *kids):\n        self.val = qlast.Introspect(type=kids[1].val)\n\n    def reduce_Expr_IN_Expr(self, *kids):\n        inexpr = kids[2].val\n        self.val = qlast.BinOp(left=kids[0].val, op='IN',\n                               right=inexpr)\n\n    @parsing.precedence(precedence.P_IN)\n    def reduce_Expr_NOT_IN_Expr(self, *kids):\n        inexpr = kids[3].val\n        self.val = qlast.BinOp(left=kids[0].val, op='NOT IN',\n                               right=inexpr)\n\n    @parsing.precedence(precedence.P_TYPECAST)\n    def reduce_LANGBRACKET_FullTypeExpr_RANGBRACKET_Expr(\n            self, *kids):\n        self.val = qlast.TypeCast(\n            expr=kids[3].val,\n            type=kids[1].val,\n            cardinality_mod=None,\n        )\n\n    @parsing.precedence(precedence.P_TYPECAST)\n    def reduce_LANGBRACKET_OPTIONAL_FullTypeExpr_RANGBRACKET_Expr(\n            self, *kids):\n        self.val = qlast.TypeCast(\n            expr=kids[4].val,\n            type=kids[2].val,\n            cardinality_mod=qlast.CardinalityModifier.Optional,\n        )\n\n    @parsing.precedence(precedence.P_TYPECAST)\n    def reduce_LANGBRACKET_REQUIRED_FullTypeExpr_RANGBRACKET_Expr(\n            self, *kids):\n        self.val = qlast.TypeCast(\n            expr=kids[4].val,\n            type=kids[2].val,\n            cardinality_mod=qlast.CardinalityModifier.Required,\n        )\n\n    def reduce_Expr_IF_Expr_ELSE_Expr(self, *kids):\n        if_expr, _, condition, _, else_expr = kids\n        self.val = qlast.IfElse(\n            if_expr=if_expr.val,\n            condition=condition.val,\n            else_expr=else_expr.val,\n            python_style=True,\n        )\n\n    @parsing.inline(0)\n    def reduce_IfThenElseExpr(self, _):\n        pass\n\n    def reduce_Expr_UNION_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op='UNION',\n                               right=kids[2].val)\n\n    def reduce_Expr_EXCEPT_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op='EXCEPT',\n                               right=kids[2].val)\n\n    def reduce_Expr_INTERSECT_Expr(self, *kids):\n        self.val = qlast.BinOp(left=kids[0].val, op='INTERSECT',\n                               right=kids[2].val)\n\n\nclass IfThenElseExpr(Nonterm):\n    def reduce_IF_Expr_THEN_Expr_ELSE_Expr(self, *kids):\n        _, condition, _, if_expr, _, else_expr = kids\n        self.val = qlast.IfElse(\n            condition=condition.val,\n            if_expr=if_expr.val,\n            else_expr=else_expr.val,\n        )\n\n\nclass CompareOp(Nonterm):\n    @parsing.inline(0)\n    @parsing.precedence(precedence.P_COMPARE_OP)\n    def reduce_DISTINCTFROM(self, *_):\n        pass\n\n    @parsing.inline(0)\n    @parsing.precedence(precedence.P_COMPARE_OP)\n    def reduce_GREATEREQ(self, *_):\n        pass\n\n    @parsing.inline(0)\n    @parsing.precedence(precedence.P_COMPARE_OP)\n    def reduce_LESSEQ(self, *_):\n        pass\n\n    @parsing.inline(0)\n    @parsing.precedence(precedence.P_COMPARE_OP)\n    def reduce_NOTDISTINCTFROM(self, *_):\n        pass\n\n    @parsing.inline(0)\n    @parsing.precedence(precedence.P_COMPARE_OP)\n    def reduce_NOTEQ(self, *_):\n        pass\n\n    @parsing.inline(0)\n    @parsing.precedence(precedence.P_COMPARE_OP)\n    def reduce_LANGBRACKET(self, *_):\n        pass\n\n    @parsing.inline(0)\n    @parsing.precedence(precedence.P_COMPARE_OP)\n    def reduce_RANGBRACKET(self, *_):\n        pass\n\n    @parsing.inline(0)\n    @parsing.precedence(precedence.P_COMPARE_OP)\n    def reduce_EQUALS(self, *_):\n        pass\n\n\nclass Tuple(Nonterm):\n    def reduce_LPAREN_GenExpr_COMMA_OptExprList_RPAREN(self, *kids):\n        self.val = qlast.Tuple(elements=[kids[1].val] + kids[3].val)\n\n    def reduce_LPAREN_RPAREN(self, *kids):\n        self.val = qlast.Tuple(elements=[])\n\n\nclass NamedTuple(Nonterm):\n    def reduce_LPAREN_NamedTupleElementList_RPAREN(self, *kids):\n        self.val = qlast.NamedTuple(elements=kids[1].val)\n\n\nclass NamedTupleElement(Nonterm):\n    def reduce_ShortNodeName_ASSIGN_GenExpr(self, *kids):\n        self.val = qlast.TupleElement(\n            name=qlast.Ptr(name=kids[0].val.name, span=kids[0].span),\n            val=kids[2].val\n        )\n\n\nclass NamedTupleElementList(ListNonterm, element=NamedTupleElement,\n                            separator=tokens.T_COMMA,\n                            allow_trailing_separator=True):\n    pass\n\n\nclass Set(Nonterm):\n    def reduce_LBRACE_OptExprList_RBRACE(self, *kids):\n        self.val = qlast.Set(elements=kids[1].val)\n\n\nclass Collection(Nonterm):\n    def reduce_LBRACKET_OptExprList_RBRACKET(self, *kids):\n        elements = kids[1].val\n        self.val = qlast.Array(elements=elements)\n\n\nclass OptExprList(Nonterm):\n    @parsing.inline(0)\n    def reduce_ExprList(self, *kids):\n        pass\n\n    def reduce_empty(self, *kids):\n        self.val = []\n\n\nclass ExprList(ListNonterm, element=GenExpr, separator=tokens.T_COMMA,\n               allow_trailing_separator=True):\n    val: list[qlast.Expr]\n\n\nclass Constant(Nonterm):\n    val: qlast.Expr\n\n    # PARAMETER\n    # | BaseNumberConstant\n    # | BaseStringConstant\n    # | BaseBooleanConstant\n    # | BaseBytesConstant\n\n    def reduce_PARAMETER(self, param):\n        self.val = qlast.QueryParameter(name=param.val[1:])\n\n    def reduce_PARAMETERANDTYPE(self, param):\n        assert param.val.startswith('<lit ')\n        type_name, param_name = param.val.removeprefix('<lit ').split('>$')\n        self.val = qlast.TypeCast(\n            type=qlast.TypeName(\n                maintype=qlast.ObjectRef(\n                    name=type_name,\n                    module='__std__'\n                ),\n                span=param.span,\n            ),\n            expr=qlast.QueryParameter(\n                name=param_name,\n                span=param.span,\n            ),\n        )\n\n    @parsing.inline(0)\n    def reduce_BaseNumberConstant(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_BaseStringConstant(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_BaseBooleanConstant(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_BaseBytesConstant(self, *kids):\n        pass\n\n\nclass StringInterpolationTail(Nonterm):\n    def reduce_Expr_STRINTERPEND(self, *kids):\n        expr, lit = kids\n        self.val = qlast.StrInterp(\n            prefix='',\n            interpolations=[\n                qlast.StrInterpFragment(\n                    expr=expr.val, suffix=lit.clean_value, span=self.span\n                ),\n            ]\n        )\n\n    def reduce_Expr_STRINTERPCONT_StringInterpolationTail(self, *kids):\n        expr, lit, tail = kids\n        self.val = tail.val\n        self.val.interpolations.append(\n            qlast.StrInterpFragment(\n                expr=expr.val, suffix=lit.clean_value, span=self.span\n            )\n        )\n\n\nclass StringInterpolation(Nonterm):\n    def reduce_STRINTERPSTART_StringInterpolationTail(self, *kids):\n        # We produce somewhat malformed StrInterp values out of\n        # StringInterpolationTail, for convenience and efficiency, and\n        # fix them up here.\n        # (In particular, we put the interpolations in backward.)\n        lit, tail = kids\n        self.val = tail.val\n        self.val.prefix = lit.clean_value\n        self.val.interpolations.reverse()\n\n\nclass BaseNumberConstant(Nonterm):\n    val: qlast.Constant\n\n    def reduce_ICONST(self, *kids):\n        self.val = qlast.Constant(\n            value=kids[0].val, kind=qlast.ConstantKind.INTEGER\n        )\n\n    def reduce_FCONST(self, *kids):\n        self.val = qlast.Constant(\n            value=kids[0].val, kind=qlast.ConstantKind.FLOAT\n        )\n\n    def reduce_NICONST(self, *kids):\n        self.val = qlast.Constant(\n            value=kids[0].val, kind=qlast.ConstantKind.BIGINT\n        )\n\n    def reduce_NFCONST(self, *kids):\n        self.val = qlast.Constant(\n            value=kids[0].val, kind=qlast.ConstantKind.DECIMAL\n        )\n\n\nclass BaseStringConstant(Nonterm):\n    val: qlast.Constant\n\n    def reduce_SCONST(self, token):\n        self.val = qlast.Constant.string(value=token.clean_value)\n\n\nclass BaseBytesConstant(Nonterm):\n    val: qlast.BaseConstant\n\n    def reduce_BCONST(self, bytes_tok):\n        self.val = qlast.BytesConstant(value=bytes_tok.clean_value)\n\n\nclass BaseBooleanConstant(Nonterm):\n    val: qlast.Constant\n\n    def reduce_TRUE(self, *kids):\n        self.val = qlast.Constant.boolean(True)\n\n    def reduce_FALSE(self, *kids):\n        self.val = qlast.Constant.boolean(False)\n\n\ndef ensure_path(expr):\n    if not isinstance(expr, qlast.Path):\n        expr = qlast.Path(steps=[expr])\n    return expr\n\n\nclass Path(Nonterm):\n    @parsing.precedence(precedence.P_DOT)\n    def reduce_Expr_PathStep(self, *kids):\n        path = ensure_path(kids[0].val)\n        path.steps.append(kids[1].val)\n        self.val = path\n\n\nclass AtomicExpr(Nonterm):\n    @parsing.inline(0)\n    def reduce_BaseAtomicExpr(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AtomicPath(self, *kids):\n        pass\n\n    @parsing.precedence(precedence.P_TYPECAST)\n    def reduce_LANGBRACKET_FullTypeExpr_RANGBRACKET_AtomicExpr(\n            self, *kids):\n        self.val = qlast.TypeCast(\n            expr=kids[3].val,\n            type=kids[1].val,\n            cardinality_mod=None,\n        )\n\n\n# Duplication of Path above, but with BasicExpr at the root\nclass AtomicPath(Nonterm):\n    @parsing.precedence(precedence.P_DOT)\n    def reduce_AtomicExpr_PathStep(self, *kids):\n        path = ensure_path(kids[0].val)\n        path.steps.append(kids[1].val)\n        self.val = path\n\n\nclass PathStep(Nonterm):\n    def reduce_DOT_PathStepName(self, *kids):\n        from edb.schema import pointers as s_pointers\n\n        self.val = qlast.Ptr(\n            name=kids[1].val.name,\n            direction=s_pointers.PointerDirection.Outbound\n        )\n\n    def reduce_DOT_ICONST(self, *kids):\n        # this is a valid link-like syntax for accessing unnamed tuples\n        from edb.schema import pointers as s_pointers\n\n        self.val = qlast.Ptr(\n            name=kids[1].val,\n            direction=s_pointers.PointerDirection.Outbound\n        )\n\n    def reduce_DOTBW_PathStepName(self, *kids):\n        from edb.schema import pointers as s_pointers\n\n        self.val = qlast.Ptr(\n            name=kids[1].val.name,\n            direction=s_pointers.PointerDirection.Inbound\n        )\n\n    def reduce_DOTQ_PathStepName(self, *kids):\n        from edb.schema import pointers as s_pointers\n\n        self.val = qlast.Ptr(\n            name=kids[1].val.name,\n            direction=s_pointers.PointerDirection.Outbound,\n            type='optional',\n        )\n\n    def reduce_AT_PathNodeName(self, *kids):\n        from edb.schema import pointers as s_pointers\n\n        self.val = qlast.Ptr(\n            name=kids[1].val.name,\n            direction=s_pointers.PointerDirection.Outbound,\n            type='property'\n        )\n\n    @parsing.inline(0)\n    def reduce_TypeIntersection(self, *kids):\n        pass\n\n\nclass TypeIntersection(Nonterm):\n    def reduce_LBRACKET_IS_FullTypeExpr_RBRACKET(self, *kids):\n        self.val = qlast.TypeIntersection(\n            type=kids[2].val,\n        )\n\n\nclass OptTypeIntersection(Nonterm):\n    @parsing.inline(0)\n    def reduce_TypeIntersection(self, *kids):\n        pass\n\n    def reduce_empty(self):\n        self.val = None\n\n\n# Used in free shapes\nclass FreeStepName(Nonterm):\n    @parsing.inline(0)\n    def reduce_ShortNodeName(self, *kids):\n        pass\n\n    def reduce_DUNDERTYPE(self, *kids):\n        self.val = qlast.ObjectRef(name=kids[0].val)\n\n\n# Used in shapes, paths and in PROPERTY/LINK definitions.\nclass PathStepName(Nonterm):\n    @parsing.inline(0)\n    def reduce_PathNodeName(self, *kids):\n        pass\n\n    def reduce_DUNDERTYPE(self, *kids):\n        self.val = qlast.ObjectRef(name=kids[0].val)\n\n\nclass FuncApplication(Nonterm):\n    def reduce_NodeName_LPAREN_OptFuncArgList_RPAREN(self, *kids):\n        module = kids[0].val.module\n        func_name = kids[0].val.name\n        name = func_name if not module else (module, func_name)\n\n        last_named_seen = None\n        args = []\n        kwargs = {}\n        for argname, argname_ctx, arg in kids[2].val:\n            if argname is not None:\n                if argname in kwargs:\n                    raise errors.EdgeQLSyntaxError(\n                        f\"duplicate named argument `{argname}`\",\n                        span=argname_ctx)\n\n                last_named_seen = argname\n                kwargs[argname] = arg\n\n            else:\n                if last_named_seen is not None:\n                    raise errors.EdgeQLSyntaxError(\n                        f\"positional argument after named \"\n                        f\"argument `{last_named_seen}`\",\n                        span=arg.span)\n                args.append(arg)\n\n        self.val = qlast.FunctionCall(func=name, args=args, kwargs=kwargs)\n\n\nclass FuncExpr(Nonterm):\n    @parsing.inline(0)\n    def reduce_FuncApplication(self, *kids):\n        pass\n\n\nclass FuncCallArgExpr(Nonterm):\n    def reduce_Expr(self, *kids):\n        self.val = (\n            None,\n            None,\n            kids[0].val,\n        )\n\n    def reduce_AnyIdentifier_ASSIGN_Expr(self, *kids):\n        self.val = (\n            kids[0].val,\n            kids[0].span,\n            kids[2].val,\n        )\n\n    def reduce_PARAMETER_ASSIGN_Expr(self, *kids):\n        if kids[0].val[1].isdigit():\n            raise errors.EdgeQLSyntaxError(\n                f\"numeric named parameters are not supported\",\n                span=kids[0].span)\n        else:\n            raise errors.EdgeQLSyntaxError(\n                f\"named parameters do not need a '$' prefix, \"\n                f\"rewrite as '{kids[0].val[1:]} := ...'\",\n                span=kids[0].span)\n\n\nclass FuncCallArg(Nonterm):\n    def reduce_FuncCallArgExpr_OptFilterClause_OptSortClause(self, *kids):\n        self.val = kids[0].val\n\n        if kids[1].val or kids[2].val:\n            qry = qlast.SelectQuery(\n                result=self.val[2],\n                where=kids[1].val,\n                orderby=kids[2].val,\n                implicit=True,\n                span=merge_spans(kids),\n            )\n            self.val = (self.val[0], self.val[1], qry)\n\n    def reduce_ExprStmtSimple(self, *kids):\n        self.val = (\n            None,\n            None,\n            kids[0].val,\n        )\n\n    def reduce_AnyIdentifier_ASSIGN_ExprStmtSimple(self, *kids):\n        self.val = (\n            kids[0].val,\n            kids[0].span,\n            kids[2].val,\n        )\n\n\nclass FuncArgList(ListNonterm, element=FuncCallArg, separator=tokens.T_COMMA,\n                  allow_trailing_separator=True):\n    pass\n\n\nclass OptFuncArgList(Nonterm):\n    @parsing.inline(0)\n    def reduce_FuncArgList(self, *kids):\n        pass\n\n    def reduce_empty(self, *kids):\n        self.val = []\n\n\nclass PosCallArg(Nonterm):\n    def reduce_Expr_OptFilterClause_OptSortClause(self, *kids):\n        self.val = kids[0].val\n        if kids[1].val or kids[2].val:\n            self.val = qlast.SelectQuery(\n                result=self.val,\n                where=kids[1].val,\n                orderby=kids[2].val,\n                implicit=True,\n            )\n\n\nclass PosCallArgList(ListNonterm, element=PosCallArg,\n                     separator=tokens.T_COMMA):\n    pass\n\n\nclass OptPosCallArgList(Nonterm):\n    @parsing.inline(0)\n    def reduce_PosCallArgList(self, *kids):\n        pass\n\n    def reduce_empty(self, *kids):\n        self.val = []\n\n\nclass Identifier(Nonterm):\n    val: str  # == Token.value\n\n    def reduce_IDENT(self, ident):\n        self.val = ident.clean_value\n\n    @parsing.inline(0)\n    def reduce_UnreservedKeyword(self, *_):\n        pass\n\n\nclass PtrIdentifier(Nonterm):\n    @parsing.inline(0)\n    def reduce_Identifier(self, *_):\n        pass\n\n    @parsing.inline(0)\n    def reduce_PartialReservedKeyword(self, *_):\n        pass\n\n\nclass AnyIdentifier(Nonterm):\n    @parsing.inline(0)\n    def reduce_PtrIdentifier(self, *kids):\n        pass\n\n    def reduce_ReservedKeyword(self, *kids):\n        name = kids[0].val\n        if name[:2] == '__' and name[-2:] == '__':\n            # There are a few reserved keywords like __std__ and __subject__\n            # that can be used in paths but are prohibited to be used\n            # anywhere else. So just as the tokenizer prohibits using\n            # __names__ in general, we enforce the rule here for the\n            # few remaining reserved __keywords__.\n            raise errors.EdgeQLSyntaxError(\n                \"identifiers surrounded by double underscores are forbidden\",\n                span=kids[0].span)\n\n        self.val = name\n\n\nclass DottedIdents(\n        ListNonterm, element=AnyIdentifier, separator=tokens.T_DOT):\n    pass\n\n\nclass DotName(Nonterm):\n    val: str\n\n    def reduce_DottedIdents(self, *kids):\n        self.val = '.'.join(part for part in kids[0].val)\n\n\nclass ModuleName(ListNonterm, element=DotName, separator=tokens.T_DOUBLECOLON):\n    val: list[str]\n\n\nclass ColonedIdents(\n        ListNonterm, element=AnyIdentifier, separator=tokens.T_DOUBLECOLON):\n    pass\n\n\nclass QualifiedName(Nonterm):\n    def reduce_Identifier_DOUBLECOLON_ColonedIdents(self, ident, _, idents):\n        assert ident.val\n        assert idents.val\n        self.val = [ident.val, *idents.val]\n\n    def reduce_DUNDERSTD_DOUBLECOLON_ColonedIdents(self, _s, _c, idents):\n        assert idents.val\n        self.val = ['__std__', *idents.val]\n\n\n# this can appear anywhere\nclass BaseName(Nonterm):\n    def reduce_Identifier(self, *kids):\n        self.val = [kids[0].val]\n\n    @parsing.inline(0)\n    def reduce_QualifiedName(self, *kids):\n        pass\n\n\n# this can appear in link/property definitions\nclass PtrName(Nonterm):\n    def reduce_PtrIdentifier(self, ptr_identifier):\n        assert ptr_identifier.val\n        self.val = [ptr_identifier.val]\n\n    @parsing.inline(0)\n    def reduce_QualifiedName(self, *_):\n        pass\n\n\n# Non-collection type.\nclass SimpleTypeName(Nonterm):\n    def reduce_PtrNodeName(self, *kids):\n        self.val = qlast.TypeName(maintype=kids[0].val)\n\n    def reduce_ANYTYPE(self, *kids):\n        self.val = qlast.TypeName(\n            maintype=qlast.PseudoObjectRef(name='anytype', span=self.span)\n        )\n\n    def reduce_ANYTUPLE(self, *kids):\n        self.val = qlast.TypeName(\n            maintype=qlast.PseudoObjectRef(name='anytuple', span=self.span)\n        )\n\n    def reduce_ANYOBJECT(self, *kids):\n        self.val = qlast.TypeName(\n            maintype=qlast.PseudoObjectRef(name='anyobject', span=self.span)\n        )\n\n\nclass SimpleTypeNameList(ListNonterm, element=SimpleTypeName,\n                         separator=tokens.T_COMMA):\n    pass\n\n\nclass CollectionTypeName(Nonterm):\n\n    def validate_subtype_list(self, lst):\n        has_nonstrval = has_strval = has_items = False\n        for el in lst.val:\n            if isinstance(el, qlast.TypeExprLiteral):\n                has_strval = True\n            elif isinstance(el, qlast.TypeName):\n                if el.name:\n                    has_items = True\n                else:\n                    has_nonstrval = True\n\n        if (has_nonstrval or has_items) and has_strval:\n            # Prohibit cases like `tuple<a: int64, 'aaaa'>` and\n            # `enum<bbbb, 'aaaa'>`\n            raise errors.EdgeQLSyntaxError(\n                \"mixing string type literals and type names is not supported\",\n                span=lst.span)\n\n        if has_items and has_nonstrval:\n            # Prohibit cases like `tuple<a: int64, int32>`\n            raise errors.EdgeQLSyntaxError(\n                \"mixing named and unnamed subtype declarations \"\n                \"is not supported\",\n                span=lst.span)\n\n    def reduce_NodeName_LANGBRACKET_RANGBRACKET(self, *kids):\n        # Constructs like `enum<>` or `array<>` aren't legal.\n        raise errors.EdgeQLSyntaxError(\n            'parametrized type must have at least one argument',\n            span=kids[1].span,\n        )\n\n    def reduce_NodeName_LANGBRACKET_SubtypeList_RANGBRACKET(self, *kids):\n        self.validate_subtype_list(kids[2])\n        self.val = qlast.TypeName(\n            maintype=kids[0].val,\n            subtypes=kids[2].val,\n        )\n\n\nclass TypeName(Nonterm):\n    @parsing.inline(0)\n    def reduce_SimpleTypeName(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_CollectionTypeName(self, *kids):\n        pass\n\n\nclass TypeNameList(ListNonterm, element=TypeName,\n                   separator=tokens.T_COMMA):\n    pass\n\n\n# A type expression that is not a simple type.\nclass NontrivialTypeExpr(Nonterm):\n    def reduce_TYPEOF_Expr(self, *kids):\n        self.val = qlast.TypeOf(expr=kids[1].val)\n\n    @parsing.inline(1)\n    def reduce_LPAREN_FullTypeExpr_RPAREN(self, *kids):\n        pass\n\n    def reduce_TypeExpr_PIPE_TypeExpr(self, *kids):\n        self.val = qlast.TypeOp(\n            left=kids[0].val,\n            op=qlast.TypeOpName.OR,\n            right=kids[2].val,\n        )\n\n    def reduce_TypeExpr_AMPER_TypeExpr(self, *kids):\n        self.val = qlast.TypeOp(\n            left=kids[0].val,\n            op=qlast.TypeOpName.AND,\n            right=kids[2].val,\n        )\n\n\n# This is a type expression without angle brackets, so it\n# can be used without parentheses in a context where the\n# angle bracket has a different meaning.\nclass TypeExpr(Nonterm):\n    @parsing.inline(0)\n    def reduce_SimpleTypeName(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_NontrivialTypeExpr(self, *kids):\n        pass\n\n\n# A type expression enclosed in parentheses\nclass ParenTypeExpr(Nonterm):\n    @parsing.inline(1)\n    def reduce_LPAREN_FullTypeExpr_RPAREN(self, *kids):\n        pass\n\n\n# This is a type expression which includes collection types,\n# so it can only be directly used in a context where the\n# angle bracket is unambiguous.\nclass FullTypeExpr(Nonterm):\n    @parsing.inline(0)\n    def reduce_TypeName(self, *kids):\n        pass\n\n    def reduce_TYPEOF_Expr(self, *kids):\n        self.val = qlast.TypeOf(expr=kids[1].val)\n\n    @parsing.inline(1)\n    def reduce_LPAREN_FullTypeExpr_RPAREN(self, *kids):\n        pass\n\n    def reduce_FullTypeExpr_PIPE_FullTypeExpr(self, *kids):\n        self.val = qlast.TypeOp(\n            left=kids[0].val,\n            op=qlast.TypeOpName.OR,\n            right=kids[2].val,\n        )\n\n    def reduce_FullTypeExpr_AMPER_FullTypeExpr(self, *kids):\n        self.val = qlast.TypeOp(\n            left=kids[0].val,\n            op=qlast.TypeOpName.AND,\n            right=kids[2].val,\n        )\n\n\nclass Subtype(Nonterm):\n    @parsing.inline(0)\n    def reduce_FullTypeExpr(self, *kids):\n        pass\n\n    def reduce_Identifier_COLON_FullTypeExpr(self, *kids):\n        self.val = kids[2].val\n        self.val.name = kids[0].val\n\n    def reduce_BaseStringConstant(self, *kids):\n        # TODO: Raise a DeprecationWarning once we have facility for that.\n        self.val = qlast.TypeExprLiteral(\n            val=kids[0].val,\n        )\n\n    def reduce_BaseNumberConstant(self, *kids):\n        self.val = qlast.TypeExprLiteral(\n            val=kids[0].val,\n        )\n\n\nclass SubtypeList(ListNonterm, element=Subtype, separator=tokens.T_COMMA,\n                  allow_trailing_separator=True):\n    pass\n\n\nclass NodeName(Nonterm):\n    # NOTE: Generic short of fully-qualified name.\n    #\n    # This name is safe to be used anywhere as it starts with IDENT only.\n\n    def reduce_BaseName(self, base_name):\n        self.val = qlast.ObjectRef(\n            module='::'.join(base_name.val[:-1]) or None,\n            name=base_name.val[-1])\n\n\nclass PtrNodeName(Nonterm):\n    # NOTE: Generic short of fully-qualified name.\n    #\n    # This name is safe to be used in most DDL and SDL definitions.\n\n    def reduce_PtrName(self, ptr_name):\n        self.val = qlast.ObjectRef(\n            module='::'.join(ptr_name.val[:-1]) or None,\n            name=ptr_name.val[-1])\n\n\nclass PtrQualifiedNodeName(Nonterm):\n    def reduce_QualifiedName(self, *kids):\n        self.val = qlast.ObjectRef(\n            module='::'.join(kids[0].val[:-1]),\n            name=kids[0].val[-1])\n\n\nclass ShortNodeName(Nonterm):\n    # NOTE: A non-qualified name that can be an identifier or\n    # UNRESERVED_KEYWORD.\n    #\n    # This name is used as part of paths after the DOT. It can be an\n    # identifier including UNRESERVED_KEYWORD and does not need to be\n    # quoted or parenthesized.\n\n    def reduce_Identifier(self, *kids):\n        self.val = qlast.ObjectRef(\n            module=None,\n            name=kids[0].val)\n\n\nclass PathNodeName(Nonterm):\n    # NOTE: A non-qualified name that can be an identifier or\n    # PARTIAL_RESERVED_KEYWORD.\n    #\n    # This name is used as part of paths after the DOT as well as in\n    # definitions after LINK/POINTER. It can be an identifier including\n    # PARTIAL_RESERVED_KEYWORD and does not need to be quoted or\n    # parenthesized.\n\n    def reduce_PtrIdentifier(self, *kids):\n        self.val = qlast.ObjectRef(\n            module=None,\n            name=kids[0].val)\n\n\nclass AnyNodeName(Nonterm):\n    # NOTE: A non-qualified name that can be ANY identifier.\n    #\n    # This name is used as part of paths after the DOT. It can be any\n    # identifier including RESERVED_KEYWORD and UNRESERVED_KEYWORD and\n    # does not need to be quoted or parenthesized.\n    #\n    # This is mainly used in DDL statements that have another keyword\n    # completely disambiguating that what comes next is a name. It\n    # CANNOT be used in Expr productions because it will cause\n    # ambiguity with NodeName, etc.\n\n    def reduce_AnyIdentifier(self, *kids):\n        self.val = qlast.ObjectRef(\n            module=None,\n            name=kids[0].val)\n\n\nclass Keyword(parsing.Nonterm):\n    \"\"\"Base class for the different classes of keywords.\n\n    Not a real nonterm on its own.\n    \"\"\"\n    def __init_subclass__(\n            cls, *, type, is_internal=False, **kwargs):\n        super().__init_subclass__(is_internal=is_internal, **kwargs)\n\n        if is_internal:\n            return\n\n        assert type in keywords.keyword_types\n\n        for token in keywords.by_type[type].values():\n            def method(inst, *kids):\n                inst.val = kids[0].val\n            method.__doc__ = \"%%reduce %s\" % token\n            method.__name__ = 'reduce_%s' % token\n            setattr(cls, method.__name__, method)\n\n\nclass UnreservedKeyword(Keyword,\n                        type=keywords.UNRESERVED_KEYWORD):\n    pass\n\n\nclass PartialReservedKeyword(Keyword,\n                             type=keywords.PARTIAL_RESERVED_KEYWORD):\n    pass\n\n\nclass ReservedKeyword(Keyword,\n                      type=keywords.RESERVED_KEYWORD):\n    pass\n\n\nclass SchemaObjectClassValue(typing.NamedTuple):\n\n    itemclass: qltypes.SchemaObjectClass\n\n\nclass SchemaObjectClass(Nonterm):\n\n    def reduce_ALIAS(self, *kids):\n        self.val = SchemaObjectClassValue(\n            itemclass=qltypes.SchemaObjectClass.ALIAS)\n\n    def reduce_ANNOTATION(self, *kids):\n        self.val = SchemaObjectClassValue(\n            itemclass=qltypes.SchemaObjectClass.ANNOTATION)\n\n    def reduce_CAST(self, *kids):\n        self.val = SchemaObjectClassValue(\n            itemclass=qltypes.SchemaObjectClass.CAST)\n\n    def reduce_CONSTRAINT(self, *kids):\n        self.val = SchemaObjectClassValue(\n            itemclass=qltypes.SchemaObjectClass.CONSTRAINT)\n\n    def reduce_FUNCTION(self, *kids):\n        self.val = SchemaObjectClassValue(\n            itemclass=qltypes.SchemaObjectClass.FUNCTION)\n\n    def reduce_LINK(self, *kids):\n        self.val = SchemaObjectClassValue(\n            itemclass=qltypes.SchemaObjectClass.LINK)\n\n    def reduce_MODULE(self, *kids):\n        self.val = SchemaObjectClassValue(\n            itemclass=qltypes.SchemaObjectClass.MODULE)\n\n    def reduce_OPERATOR(self, *kids):\n        self.val = SchemaObjectClassValue(\n            itemclass=qltypes.SchemaObjectClass.OPERATOR)\n\n    def reduce_PROPERTY(self, *kids):\n        self.val = SchemaObjectClassValue(\n            itemclass=qltypes.SchemaObjectClass.PROPERTY)\n\n    def reduce_SCALAR_TYPE(self, *kids):\n        self.val = SchemaObjectClassValue(\n            itemclass=qltypes.SchemaObjectClass.SCALAR_TYPE)\n\n    def reduce_TYPE(self, *kids):\n        self.val = SchemaObjectClassValue(\n            itemclass=qltypes.SchemaObjectClass.TYPE)\n\n\nclass SchemaItem(Nonterm):\n\n    def reduce_SchemaObjectClass_NodeName(self, *kids):\n        ref = kids[1].val\n        ref.itemclass = kids[0].val.itemclass\n        self.val = ref\n"
  },
  {
    "path": "edb/edgeql/parser/grammar/keywords.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2010-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport re\n\nimport edb._edgeql_parser as ql_parser\n\n\nkeyword_types = range(1, 5)\n(UNRESERVED_KEYWORD, RESERVED_KEYWORD, TYPE_FUNC_NAME_KEYWORD,\n PARTIAL_RESERVED_KEYWORD) = keyword_types\n\nunreserved_keywords = ql_parser.unreserved_keywords\nfuture_reserved_keywords = ql_parser.future_reserved_keywords\nreserved_keywords = (\n    future_reserved_keywords | ql_parser.current_reserved_keywords\n)\n# These keywords can be used in pretty much all the places where they are\n# preceeded by a reserved keyword or some other disambiguating token like `.`,\n# `.<`, or `@`.\n#\n# In practice we mainly relax their usage as link/property names.\npartial_reserved_keywords = ql_parser.partial_reserved_keywords\n\n\ndef _check_keywords():\n    duplicate_keywords = reserved_keywords & unreserved_keywords\n    if duplicate_keywords:\n        raise ValueError(\n            f'The following EdgeQL keywords are defined as *both* '\n            f'reserved and unreserved: {duplicate_keywords!r}')\n\n\n_check_keywords()\n\n\n_dunder_re = re.compile(r'(?i)^__[a-z]+__$')\n\n\ndef tok_name(keyword):\n    '''Convert a literal keyword into a token name.'''\n    if _dunder_re.match(keyword):\n        return f'DUNDER{keyword[2:-2].upper()}'\n    else:\n        return keyword.upper()\n\n\nedgeql_keywords = {k: (tok_name(k), UNRESERVED_KEYWORD)\n                   for k in unreserved_keywords}\nedgeql_keywords.update({k: (tok_name(k), RESERVED_KEYWORD)\n                        for k in reserved_keywords})\nedgeql_keywords.update({k: (tok_name(k), PARTIAL_RESERVED_KEYWORD)\n                        for k in partial_reserved_keywords})\n\n\nby_type: dict[int, dict] = {typ: {} for typ in keyword_types}\n\nfor val, spec in edgeql_keywords.items():\n    by_type[spec[1]][val] = spec[0]\n"
  },
  {
    "path": "edb/edgeql/parser/grammar/precedence.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom edb.common import parsing\n\n\nclass Precedence(parsing.Precedence, assoc='fail', is_internal=True):\n    pass\n\n\nclass P_UNION(Precedence, assoc='left', tokens=('UNION', 'EXCEPT',)):\n    pass\n\n\nclass P_INTERSECT(Precedence, assoc='left', tokens=('INTERSECT',)):\n    pass\n\n\nclass P_IFELSE(Precedence, assoc='right', tokens=('IF', 'ELSE')):\n    pass\n\n\nclass P_OR(Precedence, assoc='left', tokens=('OR',)):\n    pass\n\n\nclass P_AND(Precedence, assoc='left', tokens=('AND',)):\n    pass\n\n\nclass P_NOT(Precedence, assoc='right', tokens=('NOT',)):\n    pass\n\n\nclass P_LIKE_ILIKE(Precedence, assoc='nonassoc', tokens=('LIKE', 'ILIKE')):\n    pass\n\n\nclass P_IN(Precedence, assoc='nonassoc', tokens=('IN',)):\n    pass\n\n\nclass P_IDENT(Precedence, assoc='nonassoc', tokens=('IDENT', 'PARTITION')):\n    pass\n\n\nclass P_COMPARE_OP(\n    Precedence,\n    assoc='nonassoc',\n    tokens=(\n        'DISTINCTFROM',\n        'GREATEREQ',\n        'LESSEQ',\n        'NOTDISTINCTFROM',\n        'NOTEQ',\n        'LANGBRACKET',\n        'RANGBRACKET',\n        'EQUALS',\n    )\n):\n    pass\n\n\nclass P_IS(Precedence, assoc='nonassoc', tokens=('IS',)):\n    pass\n\n\nclass P_ADD_OP(Precedence, assoc='left',\n               tokens=('PLUS', 'MINUS', 'DOUBLEPLUS')):\n    pass\n\n\nclass P_MUL_OP(Precedence, assoc='left',\n               tokens=('STAR', 'SLASH', 'DOUBLESLASH', 'PERCENT')):\n    pass\n\n\nclass P_DOUBLEQMARK_OP(Precedence, assoc='right', tokens=('DOUBLEQMARK',)):\n    pass\n\n\nclass P_TYPEOF(Precedence, assoc='nonassoc', tokens=('TYPEOF',)):\n    pass\n\n\nclass P_INTROSPECT(Precedence, assoc='nonassoc', tokens=('INTROSPECT',)):\n    pass\n\n\nclass P_TYPEOR(Precedence, assoc='left', tokens=('PIPE',)):\n    pass\n\n\nclass P_TYPEAND(Precedence, assoc='left', tokens=('AMPER',)):\n    pass\n\n\nclass P_UMINUS(Precedence, assoc='right'):\n    pass\n\n\nclass P_EXISTS(Precedence, assoc='right', tokens=('EXISTS',),\n               rel_to_last='='):\n    pass\n\n\nclass P_DISTINCT(Precedence, assoc='right', tokens=('DISTINCT',),\n                 rel_to_last='='):\n    pass\n\n\nclass P_POW_OP(Precedence, assoc='right', tokens=('CIRCUMFLEX',)):\n    pass\n\n\nclass P_TYPECAST(Precedence, assoc='right'):\n    pass\n\n\nclass P_BRACE(Precedence, assoc='left', tokens=('LBRACE', 'RBRACE')):\n    pass\n\n\nclass P_BRACKET(Precedence, assoc='left', tokens=('LBRACKET', 'RBRACKET')):\n    pass\n\n\nclass P_PAREN(Precedence, assoc='left', tokens=('LPAREN', 'RPAREN')):\n    pass\n\n\nclass P_DOT(Precedence, assoc='left', tokens=('DOT', 'DOTBW', 'DOTQ')):\n    pass\n\n\nclass P_DETACHED(Precedence, assoc='right', tokens=('DETACHED',)):\n    pass\n\n\nclass P_GLOBAL(Precedence, assoc='right', tokens=('GLOBAL',)):\n    pass\n\n\nclass P_DOUBLECOLON(Precedence, assoc='left', tokens=('DOUBLECOLON',)):\n    pass\n\n\nclass P_AT(Precedence, assoc='left', tokens=('AT',)):\n    pass\n\n\n# XXX: I don't remember why this helps.\n\nclass P_REQUIRED(Precedence, assoc='right', tokens=('REQUIRED',)):\n    pass\n\n\nclass P_MULTI(Precedence, assoc='right', tokens=('MULTI',),\n              rel_to_last='='):\n    pass\n\n\nclass P_OPTIONAL(Precedence, assoc='right', tokens=('OPTIONAL',),\n                 rel_to_last='='):\n    pass\n\n\nclass P_SINGLE(Precedence, assoc='right', tokens=('SINGLE',),\n               rel_to_last='='):\n    pass\n"
  },
  {
    "path": "edb/edgeql/parser/grammar/sdl.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom edb.edgeql import ast as qlast\n\nfrom edb.common import parsing\nfrom edb import errors\n\nfrom . import expressions\nfrom . import commondl\n\nfrom .precedence import *  # NOQA\nfrom .tokens import *  # NOQA\nfrom .commondl import *  # NOQA\n\n\nNonterm = expressions.Nonterm  # type: ignore[misc]\nOptSemicolons = commondl.OptSemicolons  # type: ignore[misc]\n\n\nsdl_nontem_helper = commondl.NewNontermHelper(__name__)\n_new_nonterm = sdl_nontem_helper._new_nonterm\n\n\n# top-level SDL statements\nclass SDLStatement(Nonterm):\n    @parsing.inline(0)\n    def reduce_SDLBlockStatement(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_SDLShortStatement_SEMICOLON(self, *kids):\n        pass\n\n\n# a list of SDL statements with optional semicolon separators\nclass SDLStatements(parsing.ListNonterm, element=SDLStatement,\n                    separator=OptSemicolons):\n    pass\n\n\n# These statements have a block\nclass SDLBlockStatement(Nonterm):\n    @parsing.inline(0)\n    def reduce_ModuleDeclaration(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_ScalarTypeDeclaration(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AnnotationDeclaration(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_ObjectTypeDeclaration(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AliasDeclaration(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_ConstraintDeclaration(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_LinkDeclaration(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_PropertyDeclaration(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_FunctionDeclaration(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_GlobalDeclaration(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_IndexDeclaration(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_PermissionDeclaration(self, *kids):\n        pass\n\n\n# these statements have no {} block\nclass SDLShortStatement(Nonterm):\n\n    @parsing.inline(0)\n    def reduce_ExtensionRequirementDeclaration(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_FutureRequirementDeclaration(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_ScalarTypeDeclarationShort(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AnnotationDeclarationShort(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_ObjectTypeDeclarationShort(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_AliasDeclarationShort(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_ConstraintDeclarationShort(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_LinkDeclarationShort(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_PropertyDeclarationShort(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_FunctionDeclarationShort(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_GlobalDeclarationShort(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_IndexDeclarationShort(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_PermissionDeclarationShort(self, *kids):\n        pass\n\n\n# A rule for an SDL block, either as part of `module` declaration or\n# as top-level schema used in MIGRATION DDL.\nclass SDLCommandBlock(Nonterm):\n    # this command block can be empty\n    def reduce_LBRACE_OptSemicolons_RBRACE(self, *kids):\n        self.val = []\n\n    def reduce_statement_without_semicolons(self, _0, _1, stmt, _2):\n        r\"\"\"%reduce LBRACE \\\n                OptSemicolons SDLShortStatement \\\n            RBRACE\n        \"\"\"\n        self.val = [stmt.val]\n\n    def reduce_statements_without_optional_trailing_semicolons(self, *kids):\n        r\"\"\"%reduce LBRACE \\\n                OptSemicolons SDLStatements \\\n                OptSemicolons SDLShortStatement \\\n            RBRACE\n        \"\"\"\n        _, _, stmts, _, stmt, _ = kids\n        self.val = stmts.val + [stmt.val]\n\n    @parsing.inline(2)\n    def reduce_LBRACE_OptSemicolons_SDLStatements_RBRACE(self, *kids):\n        pass\n\n    @parsing.inline(2)\n    def reduce_statements_without_optional_trailing_semicolons2(self, *kids):\n        r\"\"\"%reduce LBRACE \\\n                OptSemicolons SDLStatements \\\n                Semicolons \\\n            RBRACE\n        \"\"\"\n\n\nclass SDLProductionHelper:\n    def _passthrough(self, *cmds):\n        self.val = cmds[0].val\n\n    def _singleton_list(self, cmd):\n        self.val = [cmd.val]\n\n    def _empty(self, *kids):\n        self.val = []\n\n    def _block(self, lbrace, sc1, cmdl, rbrace):\n        self.val = [cmdl.val]\n\n    def _block2(self, lbrace, sc1, cmdlist, sc2, rbrace):\n        self.val = cmdlist.val\n\n    def _block3(self, lbrace, sc1, cmdlist, sc2, cmd, rbrace):\n        self.val = cmdlist.val + [cmd.val]\n\n\ndef sdl_commands_block(parent, *commands, opt=True):\n    if parent is None:\n        parent = ''\n\n    # SDLCommand := SDLCommand1 | SDLCommand2 ...\n    #\n    # All the \"short\" commands, ones that need a \";\" are gathered as\n    # SDLCommandShort.\n    #\n    # All the \"block\" commands, ones that have a \"{...}\" and don't\n    # need a \";\" are gathered as SDLCommandBlock.\n    clsdict_b = {}\n    clsdict_s = {}\n\n    for command in commands:\n        if command.__name__.endswith('Block'):\n            clsdict_b[f'reduce_{command.__name__}'] = \\\n                SDLProductionHelper._passthrough\n        else:\n            clsdict_s[f'reduce_{command.__name__}'] = \\\n                SDLProductionHelper._passthrough\n\n    cmd_s = _new_nonterm(f'{parent}SDLCommandShort', clsdict=clsdict_s)\n    cmd_b = _new_nonterm(f'{parent}SDLCommandBlock', clsdict=clsdict_b)\n\n    # Merged command which has minimal \";\"\n    #\n    # SDLCommandFull := SDLCommandShort ; | SDLCommandBlock\n    clsdict = {}\n    clsdict[f'reduce_{cmd_s.__name__}_SEMICOLON'] = \\\n        SDLProductionHelper._passthrough\n    clsdict[f'reduce_{cmd_b.__name__}'] = \\\n        SDLProductionHelper._passthrough\n    cmd = _new_nonterm(f'{parent}SDLCommandFull', clsdict=clsdict)\n\n    # SDLCommandsList := SDLCommandFull [; SDLCommandFull ...]\n    cmdlist = _new_nonterm(f'{parent}SDLCommandsList',\n                           clsbases=(parsing.ListNonterm,),\n                           clskwds=dict(element=cmd, separator=OptSemicolons))\n\n    # Command block is tricky, but the inner commands must terminate\n    # without a \";\", is possible.\n    #\n    # SDLCommandsBlock :=\n    #\n    #   { [ ; ] SDLCommandFull }\n    #   { [ ; ] SDLCommandsList [ ; ]} |\n    #   { [ ; ] SDLCommandsList [ ; ] SDLCommandFull }\n    clsdict = {}\n    clsdict[f'reduce_LBRACE_OptSemicolons_{cmd_s.__name__}_RBRACE'] = \\\n        SDLProductionHelper._block\n    clsdict[f'reduce_LBRACE_OptSemicolons_{cmdlist.__name__}_' +\n            f'OptSemicolons_RBRACE'] = \\\n        SDLProductionHelper._block2\n    clsdict[f'reduce_LBRACE_OptSemicolons_{cmdlist.__name__}_OptSemicolons_' +\n            f'{cmd_s.__name__}_RBRACE'] = \\\n        SDLProductionHelper._block3\n    clsdict[f'reduce_LBRACE_OptSemicolons_RBRACE'] = \\\n        SDLProductionHelper._empty\n    _new_nonterm(f'{parent}SDLCommandsBlock', clsdict=clsdict)\n\n    if opt is False:\n        #   | Command\n        clsdict = {}\n        clsdict[f'reduce_{cmd_s.__name__}'] = \\\n            SDLProductionHelper._singleton_list\n        clsdict[f'reduce_{cmd_b.__name__}'] = \\\n            SDLProductionHelper._singleton_list\n        _new_nonterm(parent + 'SingleSDLCommandBlock', clsdict=clsdict)\n\n\nclass Using(Nonterm):\n    def reduce_USING_ParenExpr(self, *kids):\n        _, paren_expr = kids\n        self.val = qlast.SetField(\n            name='expr',\n            value=paren_expr.val,\n            special_syntax=True,\n        )\n\n\nclass SetField(Nonterm):\n    # field := <expr>\n    def reduce_Identifier_ASSIGN_GenExpr(self, *kids):\n        identifier, _, expr = kids\n        self.val = qlast.SetField(name=identifier.val, value=expr.val)\n\n\nclass SetAnnotation(Nonterm):\n    def reduce_ANNOTATION_NodeName_ASSIGN_GenExpr(self, *kids):\n        _, name, _, expr = kids\n        self.val = qlast.CreateAnnotationValue(name=name.val, value=expr.val)\n\n\nsdl_commands_block(\n    'Create',\n    Using,\n    SetField,\n    SetAnnotation)\n\n\nclass ExtensionRequirementDeclaration(Nonterm):\n\n    def reduce_USING_EXTENSION_ShortNodeName_OptExtensionVersion(self, *kids):\n        _, _, name, version = kids\n        self.val = qlast.CreateExtension(\n            name=name.val,\n            version=version.val,\n        )\n\n\nclass FutureRequirementDeclaration(Nonterm):\n\n    def reduce_USING_FUTURE_ShortNodeName(self, *kids):\n        _, _, name = kids\n        self.val = qlast.CreateFuture(\n            name=name.val,\n        )\n\n\nclass ModuleDeclaration(Nonterm):\n    def reduce_MODULE_ModuleName_SDLCommandBlock(self, _, name, block):\n\n        # Check that top-level declarations DO NOT use fully-qualified\n        # names and aren't nested module blocks.\n        declarations = block.val\n        for decl in declarations:\n            if isinstance(decl, qlast.ExtensionCommand):\n                raise errors.EdgeQLSyntaxError(\n                    \"'using extension' cannot be used inside a module block\",\n                    span=decl.span)\n            elif isinstance(decl, qlast.FutureCommand):\n                raise errors.EdgeQLSyntaxError(\n                    \"'using future' cannot be used inside a module block\",\n                    span=decl.span)\n            elif decl.name.module is not None:\n                raise errors.EdgeQLSyntaxError(\n                    \"fully-qualified name is not allowed in \"\n                    \"a module declaration\",\n                    span=decl.name.span)\n\n        self.val = qlast.ModuleDeclaration(\n            # mirror what we do in CREATE MODULE\n            name=qlast.ObjectRef(\n                module=None, name='::'.join(name.val), span=name.span\n            ),\n            declarations=declarations,\n        )\n\n\n#\n# Constraints\n#\nclass ConstraintDeclaration(Nonterm):\n    def reduce_CreateConstraint(self, *kids):\n        r\"\"\"%reduce ABSTRACT CONSTRAINT NodeName OptOnExpr \\\n                    OptExtendingSimple CreateSDLCommandsBlock\"\"\"\n        _, _, name, on_expr, extending, commands = kids\n        self.val = qlast.CreateConstraint(\n            name=name.val,\n            subjectexpr=on_expr.val,\n            bases=extending.val,\n            commands=commands.val,\n        )\n\n    def reduce_CreateConstraint_CreateFunctionArgs(self, *kids):\n        r\"\"\"%reduce ABSTRACT CONSTRAINT NodeName CreateFunctionArgs \\\n                    OptOnExpr OptExtendingSimple CreateSDLCommandsBlock\"\"\"\n        _, _, name, args, on_expr, extending, commands = kids\n        self.val = qlast.CreateConstraint(\n            name=name.val,\n            params=args.val,\n            subjectexpr=on_expr.val,\n            bases=extending.val,\n            commands=commands.val,\n        )\n\n\nclass ConstraintDeclarationShort(Nonterm):\n    def reduce_CreateConstraint(self, *kids):\n        r\"\"\"%reduce ABSTRACT CONSTRAINT NodeName OptOnExpr \\\n                    OptExtendingSimple\"\"\"\n        _, _, name, on_expr, extending = kids\n        self.val = qlast.CreateConstraint(\n            name=name.val,\n            subject=on_expr.val,\n            bases=extending.val,\n        )\n\n    def reduce_CreateConstraint_CreateFunctionArgs(self, *kids):\n        r\"\"\"%reduce ABSTRACT CONSTRAINT NodeName CreateFunctionArgs \\\n                    OptOnExpr OptExtendingSimple\"\"\"\n        _, _, name, args, on_expr, extending = kids\n        self.val = qlast.CreateConstraint(\n            name=name.val,\n            params=args.val,\n            subject=on_expr.val,\n            bases=extending.val,\n        )\n\n\nclass ConcreteConstraintBlock(Nonterm):\n    def reduce_CreateConstraint(self, *kids):\n        r\"\"\"%reduce CONSTRAINT \\\n                    NodeName OptConcreteConstraintArgList OptOnExpr \\\n                    OptExceptExpr \\\n                    CreateSDLCommandsBlock\"\"\"\n        _, name, arg_list, on_expr, except_expr, commands = kids\n        self.val = qlast.CreateConcreteConstraint(\n            name=name.val,\n            args=arg_list.val,\n            subjectexpr=on_expr.val,\n            except_expr=except_expr.val,\n            commands=commands.val,\n        )\n\n    def reduce_CreateDelegatedConstraint(self, *kids):\n        r\"\"\"%reduce DELEGATED CONSTRAINT \\\n                    NodeName OptConcreteConstraintArgList OptOnExpr \\\n                    OptExceptExpr \\\n                    CreateSDLCommandsBlock\"\"\"\n        _, _, name, arg_list, on_expr, except_expr, commands = kids\n        self.val = qlast.CreateConcreteConstraint(\n            delegated=True,\n            name=name.val,\n            args=arg_list.val,\n            subjectexpr=on_expr.val,\n            except_expr=except_expr.val,\n            commands=commands.val,\n        )\n\n\nclass ConcreteConstraintShort(Nonterm):\n    def reduce_CreateConstraint(self, *kids):\n        r\"\"\"%reduce CONSTRAINT \\\n                    NodeName OptConcreteConstraintArgList OptOnExpr \\\n                    OptExceptExpr\"\"\"\n        _, name, arg_list, on_expr, except_expr = kids\n        self.val = qlast.CreateConcreteConstraint(\n            name=name.val,\n            args=arg_list.val,\n            subjectexpr=on_expr.val,\n            except_expr=except_expr.val,\n        )\n\n    def reduce_CreateDelegatedConstraint(self, *kids):\n        r\"\"\"%reduce DELEGATED CONSTRAINT \\\n                    NodeName OptConcreteConstraintArgList OptOnExpr \\\n                    OptExceptExpr\"\"\"\n        _, _, name, arg_list, on_expr, except_expr = kids\n        self.val = qlast.CreateConcreteConstraint(\n            delegated=True,\n            name=name.val,\n            args=arg_list.val,\n            subjectexpr=on_expr.val,\n            except_expr=except_expr.val,\n        )\n\n\n#\n# Scalar Types\n#\n\nsdl_commands_block(\n    'CreateScalarType',\n    SetField,\n    SetAnnotation,\n    ConcreteConstraintBlock,\n    ConcreteConstraintShort,\n)\n\n\nclass ScalarTypeDeclaration(Nonterm):\n    def reduce_CreateAbstractScalarTypeStmt(self, *kids):\n        r\"\"\"%reduce \\\n            ABSTRACT SCALAR TYPE NodeName \\\n            OptExtending CreateScalarTypeSDLCommandsBlock \\\n        \"\"\"\n        _, _, _, name, extending, commands = kids\n        self.val = qlast.CreateScalarType(\n            abstract=True,\n            name=name.val,\n            bases=extending.val,\n            commands=commands.val,\n        )\n\n    def reduce_ScalarTypeDeclaration(self, *kids):\n        r\"\"\"%reduce \\\n            SCALAR TYPE NodeName \\\n            OptExtending CreateScalarTypeSDLCommandsBlock \\\n        \"\"\"\n        _, _, name, extending, commands = kids\n        self.val = qlast.CreateScalarType(\n            name=name.val,\n            bases=extending.val,\n            commands=commands.val,\n        )\n\n\nclass ScalarTypeDeclarationShort(Nonterm):\n    def reduce_CreateAbstractScalarTypeStmt(self, *kids):\n        r\"\"\"%reduce \\\n            ABSTRACT SCALAR TYPE NodeName \\\n            OptExtending \\\n        \"\"\"\n        _, _, _, name, extending = kids\n        self.val = qlast.CreateScalarType(\n            abstract=True,\n            name=name.val,\n            bases=extending.val,\n        )\n\n    def reduce_ScalarTypeDeclaration(self, *kids):\n        r\"\"\"%reduce \\\n            SCALAR TYPE NodeName \\\n            OptExtending \\\n        \"\"\"\n        _, _, name, extending = kids\n        self.val = qlast.CreateScalarType(\n            name=name.val,\n            bases=extending.val,\n        )\n\n\n#\n# Annotations\n#\nclass AnnotationDeclaration(Nonterm):\n    def reduce_CreateAnnotation(self, *kids):\n        r\"\"\"%reduce ABSTRACT ANNOTATION NodeName OptExtendingSimple \\\n                    CreateSDLCommandsBlock\"\"\"\n        _, _, name, extending, commands = kids\n        self.val = qlast.CreateAnnotation(\n            abstract=True,\n            name=name.val,\n            bases=extending.val,\n            inheritable=False,\n            commands=commands.val,\n        )\n\n    def reduce_CreateInheritableAnnotation(self, *kids):\n        r\"\"\"%reduce ABSTRACT INHERITABLE ANNOTATION\n                    NodeName OptExtendingSimple CreateSDLCommandsBlock\"\"\"\n        _, _, _, name, extending, commands = kids\n        self.val = qlast.CreateAnnotation(\n            abstract=True,\n            name=name.val,\n            bases=extending.val,\n            inheritable=True,\n            commands=commands.val,\n        )\n\n\nclass AnnotationDeclarationShort(Nonterm):\n    def reduce_CreateAnnotation(self, *kids):\n        r\"\"\"%reduce ABSTRACT ANNOTATION NodeName OptExtendingSimple\"\"\"\n        _, _, name, extending = kids\n        self.val = qlast.CreateAnnotation(\n            abstract=True,\n            name=name.val,\n            bases=extending.val,\n            inheritable=False,\n        )\n\n    def reduce_CreateInheritableAnnotation(self, *kids):\n        r\"\"\"%reduce ABSTRACT INHERITABLE ANNOTATION\n                    NodeName OptExtendingSimple\"\"\"\n        _, _, _, name, extending = kids\n        self.val = qlast.CreateAnnotation(\n            abstract=True,\n            name=name.val,\n            bases=extending.val,\n            inheritable=True,\n        )\n\n\n#\n# Indexes\n#\nsdl_commands_block(\n    'CreateIndex',\n    Using,\n    SetField,\n    SetAnnotation,\n)\n\n\nclass IndexDeclaration(\n    Nonterm,\n    commondl.ProcessIndexMixin,\n):\n    def reduce_CreateIndex(self, *kids):\n        r\"\"\"%reduce ABSTRACT INDEX NodeName \\\n                    OptExtendingSimple CreateIndexSDLCommandsBlock\"\"\"\n        _, _, name, bases, commands = kids\n        self.val = qlast.CreateIndex(\n            name=name.val,\n            bases=bases.val,\n            commands=commands.val,\n        )\n\n    def reduce_CreateIndex_CreateFunctionArgs(self, *kids):\n        r\"\"\"%reduce ABSTRACT INDEX NodeName IndexExtArgList \\\n                    OptExtendingSimple CreateIndexSDLCommandsBlock\"\"\"\n        _, _, name, arg_list, bases, commands = kids\n        params, kwargs = self._process_params_or_kwargs(\n            bases.val, arg_list.val)\n        self.val = qlast.CreateIndex(\n            name=name.val,\n            params=params,\n            kwargs=kwargs,\n            bases=bases.val,\n            commands=commands.val,\n        )\n\n\nclass IndexDeclarationShort(\n    Nonterm,\n    commondl.ProcessIndexMixin,\n):\n    def reduce_CreateIndex(self, *kids):\n        r\"\"\"%reduce ABSTRACT INDEX NodeName OptExtendingSimple\"\"\"\n        _, _, name, bases = kids\n        self.val = qlast.CreateIndex(\n            name=name.val,\n            bases=bases.val,\n        )\n\n    def reduce_CreateIndex_CreateFunctionArgs(self, *kids):\n        r\"\"\"%reduce ABSTRACT INDEX NodeName IndexExtArgList \\\n                    OptExtendingSimple\"\"\"\n        _, _, name, arg_list, bases = kids\n        params, kwargs = self._process_params_or_kwargs(\n            bases.val, arg_list.val)\n        self.val = qlast.CreateIndex(\n            name=name.val,\n            params=params,\n            kwargs=kwargs,\n            bases=bases.val,\n        )\n\n\nsdl_commands_block(\n    'CreateConcreteIndex',\n    SetField,\n    SetAnnotation)\n\n\nclass ConcreteIndexDeclarationBlock(Nonterm, commondl.ProcessIndexMixin):\n    def reduce_CreateConcreteAnonymousIndex(self, *kids):\n        r\"\"\"%reduce INDEX OnExpr OptExceptExpr\n                    CreateConcreteIndexSDLCommandsBlock\n        \"\"\"\n        _, on_expr, except_expr, commands = kids\n        self.val = qlast.CreateConcreteIndex(\n            name=qlast.ObjectRef(module='__', name='idx', span=kids[0].span),\n            expr=on_expr.val,\n            except_expr=except_expr.val,\n            commands=commands.val,\n        )\n\n    def reduce_CreateConcreteAnonymousDeferredIndex(self, *kids):\n        r\"\"\"%reduce DEFERRED INDEX OnExpr OptExceptExpr\n                    CreateConcreteIndexSDLCommandsBlock\n        \"\"\"\n        _, _, on_expr, except_expr, commands = kids\n        self.val = qlast.CreateConcreteIndex(\n            name=qlast.ObjectRef(module='__', name='idx', span=kids[0].span),\n            expr=on_expr.val,\n            except_expr=except_expr.val,\n            deferred=True,\n            commands=commands.val,\n        )\n\n    def reduce_CreateConcreteIndex(self, *kids):\n        r\"\"\"%reduce INDEX NodeName \\\n                    OnExpr OptExceptExpr \\\n                    CreateConcreteIndexSDLCommandsBlock \\\n        \"\"\"\n        _, name, on_expr, except_expr, commands = kids\n        self.val = qlast.CreateConcreteIndex(\n            name=name.val,\n            expr=on_expr.val,\n            except_expr=except_expr.val,\n            commands=commands.val,\n        )\n\n    def reduce_CreateConcreteDeferredIndex(self, *kids):\n        r\"\"\"%reduce DEFERRED INDEX NodeName \\\n                    OnExpr OptExceptExpr \\\n                    CreateConcreteIndexSDLCommandsBlock \\\n        \"\"\"\n        _, _, name, on_expr, except_expr, commands = kids\n        self.val = qlast.CreateConcreteIndex(\n            name=name.val,\n            expr=on_expr.val,\n            except_expr=except_expr.val,\n            deferred=True,\n            commands=commands.val,\n        )\n\n    def reduce_CreateConcreteIndexWithArgs(self, *kids):\n        r\"\"\"%reduce INDEX NodeName IndexExtArgList \\\n                    OnExpr OptExceptExpr \\\n                    CreateConcreteIndexSDLCommandsBlock \\\n        \"\"\"\n        _, name, arg_list, on_expr, except_expr, commands = kids\n        kwargs = self._process_arguments(arg_list.val)\n        self.val = qlast.CreateConcreteIndex(\n            name=name.val,\n            kwargs=kwargs,\n            expr=on_expr.val,\n            except_expr=except_expr.val,\n            commands=commands.val,\n        )\n\n    def reduce_CreateConcreteDeferredIndexWithArgs(self, *kids):\n        r\"\"\"%reduce DEFERRED INDEX NodeName IndexExtArgList \\\n                    OnExpr OptExceptExpr \\\n                    CreateConcreteIndexSDLCommandsBlock \\\n        \"\"\"\n        _, _, name, arg_list, on_expr, except_expr, commands = kids\n        kwargs = self._process_arguments(arg_list.val)\n        self.val = qlast.CreateConcreteIndex(\n            name=name.val,\n            kwargs=kwargs,\n            expr=on_expr.val,\n            except_expr=except_expr.val,\n            deferred=True,\n            commands=commands.val,\n        )\n\n\nclass ConcreteIndexDeclarationShort(Nonterm, commondl.ProcessIndexMixin):\n    def reduce_INDEX_OnExpr_OptExceptExpr(self, *kids):\n        _, on_expr, except_expr = kids\n        self.val = qlast.CreateConcreteIndex(\n            name=qlast.ObjectRef(module='__', name='idx', span=kids[0].span),\n            expr=on_expr.val,\n            except_expr=except_expr.val,\n        )\n\n    def reduce_DEFERRED_INDEX_OnExpr_OptExceptExpr(self, *kids):\n        _, _, on_expr, except_expr = kids\n        self.val = qlast.CreateConcreteIndex(\n            name=qlast.ObjectRef(module='__', name='idx', span=kids[0].span),\n            expr=on_expr.val,\n            except_expr=except_expr.val,\n            deferred=True,\n        )\n\n    def reduce_CreateConcreteIndex(self, *kids):\n        r\"\"\"%reduce INDEX NodeName OnExpr OptExceptExpr\n        \"\"\"\n        _, name, on_expr, except_expr = kids\n        self.val = qlast.CreateConcreteIndex(\n            name=name.val,\n            expr=on_expr.val,\n            except_expr=except_expr.val,\n        )\n\n    def reduce_CreateConcreteDeferredIndex(self, *kids):\n        r\"\"\"%reduce DEFERRED INDEX NodeName OnExpr OptExceptExpr\n        \"\"\"\n        _, _, name, on_expr, except_expr = kids\n        self.val = qlast.CreateConcreteIndex(\n            name=name.val,\n            expr=on_expr.val,\n            except_expr=except_expr.val,\n            deferred=True,\n        )\n\n    def reduce_CreateConcreteIndexWithArgs(self, *kids):\n        r\"\"\"%reduce INDEX NodeName IndexExtArgList \\\n                    OnExpr OptExceptExpr \\\n        \"\"\"\n        _, name, arg_list, on_expr, except_expr = kids\n        kwargs = self._process_arguments(arg_list.val)\n        self.val = qlast.CreateConcreteIndex(\n            name=name.val,\n            kwargs=kwargs,\n            expr=on_expr.val,\n            except_expr=except_expr.val,\n        )\n\n    def reduce_CreateConcreteDeferredIndexWithArgs(self, *kids):\n        r\"\"\"%reduce DEFERRED INDEX NodeName IndexExtArgList\n                    OnExpr OptExceptExpr\n        \"\"\"\n        _, _, name, arg_list, on_expr, except_expr = kids\n        kwargs = self._process_arguments(arg_list.val)\n        self.val = qlast.CreateConcreteIndex(\n            name=name.val,\n            kwargs=kwargs,\n            expr=on_expr.val,\n            except_expr=except_expr.val,\n            deferred=True,\n        )\n\n\n#\n# Mutation rewrites\n#\nsdl_commands_block(\n    'CreateRewrite',\n    SetField,\n    SetAnnotation\n)\n\n\nclass RewriteDeclarationBlock(Nonterm):\n    def reduce_CreateRewrite(self, _r, kinds, _u, expr, commands):\n        \"\"\"%reduce\n            REWRITE RewriteKindList\n            USING ParenExpr\n            CreateRewriteSDLCommandsBlock\n        \"\"\"\n        # The name isn't important (it gets replaced) but we need to\n        # have one.\n        name = '/'.join(str(kind) for kind in kinds.val)\n        self.val = qlast.CreateRewrite(\n            name=qlast.ObjectRef(name=name, span=kinds.span),\n            kinds=kinds.val,\n            expr=expr.val,\n            commands=commands.val,\n        )\n\n\nclass RewriteDeclarationShort(Nonterm):\n    def reduce_CreateRewrite(self, _r, kinds, _u, expr):\n        \"\"\"%reduce\n            REWRITE RewriteKindList\n            USING ParenExpr\n        \"\"\"\n        # The name isn't important (it gets replaced) but we need to\n        # have one.\n        name = '/'.join(str(kind) for kind in kinds.val)\n        self.val = qlast.CreateRewrite(\n            name=qlast.ObjectRef(name=name, span=kinds.span),\n            kinds=kinds.val,\n            expr=expr.val,\n        )\n\n\n#\n# Unknown kind pointers (could be link or property)\n#\n\nclass PtrTarget(Nonterm):\n\n    def reduce_ARROW_FullTypeExpr(self, *kids):\n        _arrow, type_expr = kids\n\n        self.val = type_expr.val\n        self.span = type_expr.val.span\n\n    def reduce_COLON_FullTypeExpr(self, *kids):\n        _, type_expr = kids\n        self.val = type_expr.val\n        self.span = type_expr.val.span\n\n\nclass OptPtrTarget(Nonterm):\n\n    def reduce_empty(self, *kids):\n        self.val = None\n\n    @parsing.inline(0)\n    def reduce_PtrTarget(self, *kids):\n        pass\n\n\nclass ConcreteUnknownPointerBlock(Nonterm):\n    def _validate(self):\n        on_target_delete = None\n        for cmd in self.val.commands:\n            if isinstance(cmd, qlast.OnTargetDelete):\n                if on_target_delete:\n                    raise errors.EdgeQLSyntaxError(\n                        f\"more than one 'on target delete' specification\",\n                        span=cmd.span)\n                else:\n                    on_target_delete = cmd\n\n    def _extract_target(self, target, cmds, span, *, overloaded=False):\n        if target:\n            return target, cmds\n\n        for cmd in cmds:\n            if isinstance(cmd, qlast.SetField) and cmd.name == 'expr':\n                if target is not None:\n                    raise errors.EdgeQLSyntaxError(\n                        f'computed link with more than one expression',\n                        span=span)\n                target = cmd.value\n\n        if not overloaded and target is None:\n            raise errors.EdgeQLSyntaxError(\n                f'computed link without expression',\n                span=span)\n\n        return target, cmds\n\n    def reduce_CreateRegularPointer(self, *kids):\n        \"\"\"%reduce\n            PathNodeName OptExtendingSimple\n            OptPtrTarget CreateConcreteLinkSDLCommandsBlock\n        \"\"\"\n        name, opt_bases, opt_target, block = kids\n        target, cmds = self._extract_target(\n            opt_target.val, block.val, name.span)\n        vbases, vcmds = commondl.extract_bases(opt_bases.val, cmds)\n        self.val = qlast.CreateConcreteUnknownPointer(\n            name=name.val,\n            bases=vbases,\n            target=target,\n            commands=vcmds,\n        )\n        self._validate()\n\n    def reduce_CreateRegularQualifiedPointer(self, *kids):\n        \"\"\"%reduce\n            PtrQuals PathNodeName OptExtendingSimple\n            OptPtrTarget CreateConcreteLinkSDLCommandsBlock\n        \"\"\"\n        quals, name, opt_bases, opt_target, block = kids\n        target, cmds = self._extract_target(\n            opt_target.val, block.val, name.span)\n        vbases, vcmds = commondl.extract_bases(opt_bases.val, cmds)\n        self.val = qlast.CreateConcreteUnknownPointer(\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n            name=name.val,\n            bases=vbases,\n            target=target,\n            commands=vcmds,\n        )\n        self._validate()\n\n    def reduce_CreateOverloadedPointer(self, *kids):\n        \"\"\"%reduce\n            OVERLOADED PathNodeName OptExtendingSimple\n            OptPtrTarget CreateConcreteLinkSDLCommandsBlock\n        \"\"\"\n        _, name, opt_bases, opt_target, block = kids\n        target, cmds = self._extract_target(\n            opt_target.val, block.val, name.span, overloaded=True)\n        vbases, vcmds = commondl.extract_bases(opt_bases.val, cmds)\n        self.val = qlast.CreateConcreteUnknownPointer(\n            name=name.val,\n            bases=vbases,\n            declared_overloaded=True,\n            is_required=None,\n            cardinality=None,\n            target=target,\n            commands=vcmds,\n        )\n        self._validate()\n\n    def reduce_CreateOverloadedQualifiedPointer(self, *kids):\n        \"\"\"%reduce\n            OVERLOADED PtrQuals PathNodeName OptExtendingSimple\n            OptPtrTarget CreateConcreteLinkSDLCommandsBlock\n        \"\"\"\n        _, quals, name, opt_bases, opt_target, block = kids\n        target, cmds = self._extract_target(\n            opt_target.val, block.val, name.span, overloaded=True)\n        vbases, vcmds = commondl.extract_bases(opt_bases.val, cmds)\n        self.val = qlast.CreateConcreteUnknownPointer(\n            name=name.val,\n            bases=vbases,\n            declared_overloaded=True,\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n            target=target,\n            commands=vcmds,\n        )\n        self._validate()\n\n\nclass ConcreteUnknownPointerShort(Nonterm):\n\n    def reduce_CreateRegularPointer(self, *kids):\n        \"\"\"%reduce\n            PathNodeName OptExtendingSimple\n            PtrTarget\n        \"\"\"\n        name, opt_bases, target = kids\n        self.val = qlast.CreateConcreteUnknownPointer(\n            name=name.val,\n            bases=opt_bases.val,\n            target=target.val,\n        )\n\n    def reduce_CreateRegularQualifiedPointer(self, *kids):\n        \"\"\"%reduce\n            PtrQuals PathNodeName OptExtendingSimple\n            PtrTarget\n        \"\"\"\n        quals, name, opt_bases, target = kids\n        self.val = qlast.CreateConcreteUnknownPointer(\n            name=name.val,\n            bases=opt_bases.val,\n            target=target.val,\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n        )\n\n    def reduce_CreateOverloadedPointer(self, *kids):\n        \"\"\"%reduce\n            OVERLOADED PathNodeName OptExtendingSimple\n            OptPtrTarget\n        \"\"\"\n        _, name, opt_bases, opt_target = kids\n        self.val = qlast.CreateConcreteUnknownPointer(\n            name=name.val,\n            bases=opt_bases.val,\n            declared_overloaded=True,\n            is_required=None,\n            cardinality=None,\n            target=opt_target.val,\n        )\n\n    def reduce_CreateOverloadedQualifiedPointer(self, *kids):\n        \"\"\"%reduce\n            OVERLOADED PtrQuals PathNodeName OptExtendingSimple\n            OptPtrTarget\n        \"\"\"\n        _, quals, name, opt_bases, opt_target = kids\n        self.val = qlast.CreateConcreteUnknownPointer(\n            name=name.val,\n            bases=opt_bases.val,\n            declared_overloaded=True,\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n            target=opt_target.val,\n        )\n\n\n# Unknown simple computed pointers can only go on objects, since they\n# conflict with SetField on links.\nclass ConcreteUnknownPointerObjectShort(Nonterm):\n    def reduce_CreateComputableUnknownPointer(self, *kids):\n        \"\"\"%reduce\n            PathNodeName ASSIGN GenExpr\n        \"\"\"\n        name, _, expr = kids\n        self.val = qlast.CreateConcreteUnknownPointer(\n            name=name.val,\n            target=expr.val,\n        )\n\n    def reduce_CreateQualifiedComputableUnknownPointer(self, *kids):\n        \"\"\"%reduce\n            PtrQuals PathNodeName ASSIGN GenExpr\n        \"\"\"\n        quals, name, _, expr = kids\n        self.val = qlast.CreateConcreteUnknownPointer(\n            name=name.val,\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n            target=expr.val,\n        )\n\n\n#\n# Properties\n#\nsdl_commands_block(\n    'CreateProperty',\n    Using,\n    SetField,\n    SetAnnotation,\n    commondl.CreateSimpleExtending,\n)\n\n\nclass PropertyDeclaration(Nonterm):\n    def reduce_CreateProperty(self, *kids):\n        r\"\"\"%reduce ABSTRACT PROPERTY PtrNodeName OptExtendingSimple \\\n                    CreatePropertySDLCommandsBlock \\\n        \"\"\"\n        _, _, name, extending, commands_block = kids\n\n        vbases, vcommands = commondl.extract_bases(\n            extending.val,\n            commands_block.val\n        )\n        self.val = qlast.CreateProperty(\n            name=name.val,\n            bases=vbases,\n            commands=vcommands,\n            abstract=True,\n        )\n\n\nclass PropertyDeclarationShort(Nonterm):\n    def reduce_CreateProperty(self, *kids):\n        r\"\"\"%reduce ABSTRACT PROPERTY PtrNodeName OptExtendingSimple\"\"\"\n        _, _, name, extending = kids\n        self.val = qlast.CreateProperty(\n            name=name.val,\n            bases=extending.val,\n            abstract=True,\n        )\n\n\nsdl_commands_block(\n    'CreateConcreteProperty',\n    Using,\n    SetField,\n    SetAnnotation,\n    ConcreteConstraintBlock,\n    ConcreteConstraintShort,\n    RewriteDeclarationBlock,\n    RewriteDeclarationShort,\n    commondl.CreateSimpleExtending,\n)\n\n\nclass ConcretePropertyBlock(Nonterm):\n    def _extract_target(self, target, cmds, span, *, overloaded=False):\n        if target:\n            return target, cmds\n\n        for cmd in cmds:\n            if isinstance(cmd, qlast.SetField) and cmd.name == 'expr':\n                if target is not None:\n                    raise errors.EdgeQLSyntaxError(\n                        f'computed property with more than one expression',\n                        span=span)\n                target = cmd.value\n\n        if not overloaded and target is None:\n            raise errors.EdgeQLSyntaxError(\n                f'computed property without expression',\n                span=span)\n\n        return target, cmds\n\n    def reduce_CreateRegularProperty(self, *kids):\n        \"\"\"%reduce\n            PROPERTY PathNodeName OptExtendingSimple\n            OptPtrTarget CreateConcretePropertySDLCommandsBlock\n        \"\"\"\n        _, name, extending, target, commands_block = kids\n\n        target, cmds = self._extract_target(\n            target.val, commands_block.val, name.span\n        )\n        vbases, vcmds = commondl.extract_bases(extending.val, cmds)\n        self.val = qlast.CreateConcreteProperty(\n            name=name.val,\n            bases=vbases,\n            target=target,\n            commands=vcmds,\n        )\n\n    def reduce_CreateRegularQualifiedProperty(self, *kids):\n        \"\"\"%reduce\n            PtrQuals PROPERTY PathNodeName OptExtendingSimple\n            OptPtrTarget CreateConcretePropertySDLCommandsBlock\n        \"\"\"\n        (quals, property, name, extending, target, commands) = kids\n\n        target, cmds = self._extract_target(\n            target.val, commands.val, property.span\n        )\n        vbases, vcmds = commondl.extract_bases(extending.val, cmds)\n        self.val = qlast.CreateConcreteProperty(\n            name=name.val,\n            bases=vbases,\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n            target=target,\n            commands=vcmds,\n        )\n\n    def reduce_CreateOverloadedProperty(self, *kids):\n        \"\"\"%reduce\n            OVERLOADED PROPERTY PathNodeName OptExtendingSimple\n            OptPtrTarget CreateConcretePropertySDLCommandsBlock\n        \"\"\"\n        _, _, name, opt_bases, opt_target, block = kids\n        target, cmds = self._extract_target(\n            opt_target.val, block.val, name.span, overloaded=True)\n        vbases, vcmds = commondl.extract_bases(opt_bases.val, cmds)\n        self.val = qlast.CreateConcreteProperty(\n            name=name.val,\n            bases=vbases,\n            declared_overloaded=True,\n            is_required=None,\n            cardinality=None,\n            target=target,\n            commands=vcmds,\n        )\n\n    def reduce_CreateOverloadedQualifiedProperty(self, *kids):\n        \"\"\"%reduce\n            OVERLOADED PtrQuals PROPERTY PathNodeName OptExtendingSimple\n            OptPtrTarget CreateConcretePropertySDLCommandsBlock\n        \"\"\"\n        _, quals, _, name, opt_bases, opt_target, block = kids\n        target, cmds = self._extract_target(\n            opt_target.val, block.val, name.span, overloaded=True)\n        vbases, vcmds = commondl.extract_bases(opt_bases.val, cmds)\n        self.val = qlast.CreateConcreteProperty(\n            name=name.val,\n            bases=vbases,\n            declared_overloaded=True,\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n            target=target,\n            commands=vcmds,\n        )\n\n\nclass ConcretePropertyShort(Nonterm):\n    def reduce_CreateRegularProperty(self, *kids):\n        \"\"\"%reduce\n            PROPERTY PathNodeName OptExtendingSimple PtrTarget\n        \"\"\"\n        _, name, extending, target = kids\n        self.val = qlast.CreateConcreteProperty(\n            name=name.val,\n            bases=extending.val,\n            target=target.val,\n        )\n\n    def reduce_CreateRegularQualifiedProperty(self, *kids):\n        \"\"\"%reduce\n            PtrQuals PROPERTY PathNodeName OptExtendingSimple PtrTarget\n        \"\"\"\n        quals, _, name, extending, target = kids\n        self.val = qlast.CreateConcreteProperty(\n            name=name.val,\n            bases=extending.val,\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n            target=target.val,\n        )\n\n    def reduce_CreateOverloadedProperty(self, *kids):\n        \"\"\"%reduce\n            OVERLOADED PROPERTY PathNodeName OptExtendingSimple\n            OptPtrTarget\n        \"\"\"\n        _, _, name, opt_bases, opt_target = kids\n        self.val = qlast.CreateConcreteProperty(\n            name=name.val,\n            bases=opt_bases.val,\n            declared_overloaded=True,\n            is_required=None,\n            cardinality=None,\n            target=opt_target.val,\n        )\n\n    def reduce_CreateOverloadedQualifiedProperty(self, *kids):\n        \"\"\"%reduce\n            OVERLOADED PtrQuals PROPERTY PathNodeName OptExtendingSimple\n            OptPtrTarget\n        \"\"\"\n        _, quals, _, name, opt_bases, opt_target = kids\n        self.val = qlast.CreateConcreteProperty(\n            name=name.val,\n            bases=opt_bases.val,\n            declared_overloaded=True,\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n            target=opt_target.val,\n        )\n\n    def reduce_CreateComputableProperty(self, *kids):\n        \"\"\"%reduce\n            PROPERTY PathNodeName ASSIGN GenExpr\n        \"\"\"\n        _, name, _, expr = kids\n        self.val = qlast.CreateConcreteProperty(\n            name=name.val,\n            target=expr.val,\n        )\n\n    def reduce_CreateQualifiedComputableProperty(self, *kids):\n        \"\"\"%reduce\n            PtrQuals PROPERTY PathNodeName ASSIGN GenExpr\n        \"\"\"\n        quals, _, name, _, expr = kids\n        self.val = qlast.CreateConcreteProperty(\n            name=name.val,\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n            target=expr.val,\n        )\n\n\n#\n# Links\n#\n\nsdl_commands_block(\n    'CreateLink',\n    SetField,\n    SetAnnotation,\n    ConcreteConstraintBlock,\n    ConcreteConstraintShort,\n    ConcretePropertyBlock,\n    ConcretePropertyShort,\n    ConcreteUnknownPointerBlock,\n    ConcreteUnknownPointerShort,\n    ConcreteIndexDeclarationBlock,\n    ConcreteIndexDeclarationShort,\n    RewriteDeclarationShort,\n    RewriteDeclarationBlock,\n    commondl.CreateSimpleExtending,\n)\n\n\nclass LinkDeclaration(Nonterm):\n    def reduce_CreateLink(self, *kids):\n        r\"\"\"%reduce \\\n            ABSTRACT LINK PtrNodeName OptExtendingSimple \\\n            CreateLinkSDLCommandsBlock \\\n        \"\"\"\n        _, _, name, extending, commands = kids\n        vbases, vcommands = commondl.extract_bases(extending.val, commands.val)\n        self.val = qlast.CreateLink(\n            name=name.val,\n            bases=vbases,\n            commands=vcommands,\n            abstract=True,\n        )\n\n\nclass LinkDeclarationShort(Nonterm):\n    def reduce_CreateLink(self, *kids):\n        r\"\"\"%reduce \\\n            ABSTRACT LINK PtrNodeName OptExtendingSimple\"\"\"\n        _, _, name, extending = kids\n        self.val = qlast.CreateLink(\n            name=name.val,\n            bases=extending.val,\n            abstract=True,\n        )\n\n\nsdl_commands_block(\n    'CreateConcreteLink',\n    Using,\n    SetField,\n    SetAnnotation,\n    ConcreteConstraintBlock,\n    ConcreteConstraintShort,\n    ConcretePropertyBlock,\n    ConcretePropertyShort,\n    ConcreteUnknownPointerBlock,\n    ConcreteUnknownPointerShort,\n    ConcreteIndexDeclarationBlock,\n    ConcreteIndexDeclarationShort,\n    commondl.OnTargetDeleteStmt,\n    commondl.OnSourceDeleteStmt,\n    RewriteDeclarationShort,\n    RewriteDeclarationBlock,\n    commondl.CreateSimpleExtending,\n)\n\n\nclass ConcreteLinkBlock(Nonterm):\n    def _validate(self):\n        on_target_delete = None\n        for cmd in self.val.commands:\n            if isinstance(cmd, qlast.OnTargetDelete):\n                if on_target_delete:\n                    raise errors.EdgeQLSyntaxError(\n                        f\"more than one 'on target delete' specification\",\n                        span=cmd.span)\n                else:\n                    on_target_delete = cmd\n\n    def _extract_target(self, target, cmds, span, *, overloaded=False):\n        if target:\n            return target, cmds\n\n        for cmd in cmds:\n            if isinstance(cmd, qlast.SetField) and cmd.name == 'expr':\n                if target is not None:\n                    raise errors.EdgeQLSyntaxError(\n                        f'computed link with more than one expression',\n                        span=span)\n                target = cmd.value\n\n        if not overloaded and target is None:\n            raise errors.EdgeQLSyntaxError(\n                f'computed link without expression',\n                span=span)\n\n        return target, cmds\n\n    def reduce_CreateRegularLink(self, *kids):\n        \"\"\"%reduce\n            LINK PathNodeName OptExtendingSimple\n            OptPtrTarget CreateConcreteLinkSDLCommandsBlock\n        \"\"\"\n        _, name, extending, target, commands = kids\n        target, cmds = self._extract_target(\n            target.val, commands.val, name.span\n        )\n        vbases, vcmds = commondl.extract_bases(extending.val, cmds)\n        self.val = qlast.CreateConcreteLink(\n            name=name.val,\n            bases=vbases,\n            target=target,\n            commands=vcmds,\n        )\n        self._validate()\n\n    def reduce_CreateRegularQualifiedLink(self, *kids):\n        \"\"\"%reduce\n            PtrQuals LINK PathNodeName OptExtendingSimple\n            OptPtrTarget CreateConcreteLinkSDLCommandsBlock\n        \"\"\"\n        quals, _, name, extending, target, commands = kids\n        target, cmds = self._extract_target(\n            target.val, commands.val, name.span\n        )\n        vbases, vcmds = commondl.extract_bases(extending.val, cmds)\n        self.val = qlast.CreateConcreteLink(\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n            name=name.val,\n            bases=vbases,\n            target=target,\n            commands=vcmds,\n        )\n        self._validate()\n\n    def reduce_CreateOverloadedLink(self, *kids):\n        \"\"\"%reduce\n            OVERLOADED LINK PathNodeName OptExtendingSimple\n            OptPtrTarget CreateConcreteLinkSDLCommandsBlock\n        \"\"\"\n        _, _, name, opt_bases, opt_target, block = kids\n        target, cmds = self._extract_target(\n            opt_target.val, block.val, name.span, overloaded=True)\n        vbases, vcmds = commondl.extract_bases(opt_bases.val, cmds)\n        self.val = qlast.CreateConcreteLink(\n            name=name.val,\n            bases=vbases,\n            declared_overloaded=True,\n            is_required=None,\n            cardinality=None,\n            target=target,\n            commands=vcmds,\n        )\n        self._validate()\n\n    def reduce_CreateOverloadedQualifiedLink(self, *kids):\n        \"\"\"%reduce\n            OVERLOADED PtrQuals LINK PathNodeName OptExtendingSimple\n            OptPtrTarget CreateConcreteLinkSDLCommandsBlock\n        \"\"\"\n        _, quals, _, name, opt_bases, opt_target, block = kids\n        target, cmds = self._extract_target(\n            opt_target.val, block.val, name.span, overloaded=True)\n        vbases, vcmds = commondl.extract_bases(opt_bases.val, cmds)\n        self.val = qlast.CreateConcreteLink(\n            name=name.val,\n            bases=vbases,\n            declared_overloaded=True,\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n            target=target,\n            commands=vcmds,\n        )\n        self._validate()\n\n\nclass ConcreteLinkShort(Nonterm):\n\n    def reduce_CreateRegularLink(self, *kids):\n        \"\"\"%reduce\n            LINK PathNodeName OptExtendingSimple\n            PtrTarget\n        \"\"\"\n        _, name, opt_bases, target = kids\n        self.val = qlast.CreateConcreteLink(\n            name=name.val,\n            bases=opt_bases.val,\n            target=target.val,\n        )\n\n    def reduce_CreateRegularQualifiedLink(self, *kids):\n        \"\"\"%reduce\n            PtrQuals LINK PathNodeName OptExtendingSimple\n            PtrTarget\n        \"\"\"\n        quals, _, name, opt_bases, target = kids\n        self.val = qlast.CreateConcreteLink(\n            name=name.val,\n            bases=opt_bases.val,\n            target=target.val,\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n        )\n\n    def reduce_CreateOverloadedLink(self, *kids):\n        \"\"\"%reduce\n            OVERLOADED LINK PathNodeName OptExtendingSimple\n            OptPtrTarget\n        \"\"\"\n        _, _, name, opt_bases, opt_target = kids\n        self.val = qlast.CreateConcreteLink(\n            name=name.val,\n            bases=opt_bases.val,\n            declared_overloaded=True,\n            is_required=None,\n            cardinality=None,\n            target=opt_target.val,\n        )\n\n    def reduce_CreateOverloadedQualifiedLink(self, *kids):\n        \"\"\"%reduce\n            OVERLOADED PtrQuals LINK PathNodeName OptExtendingSimple\n            OptPtrTarget\n        \"\"\"\n        _, quals, _, name, opt_bases, opt_target = kids\n        self.val = qlast.CreateConcreteLink(\n            name=name.val,\n            bases=opt_bases.val,\n            declared_overloaded=True,\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n            target=opt_target.val,\n        )\n\n    def reduce_CreateComputableLink(self, *kids):\n        \"\"\"%reduce\n            LINK PathNodeName ASSIGN GenExpr\n        \"\"\"\n        _, name, _, expr = kids\n        self.val = qlast.CreateConcreteLink(\n            name=name.val,\n            target=expr.val,\n        )\n\n    def reduce_CreateQualifiedComputableLink(self, *kids):\n        \"\"\"%reduce\n            PtrQuals LINK PathNodeName ASSIGN GenExpr\n        \"\"\"\n        quals, _, name, _, expr = kids\n        self.val = qlast.CreateConcreteLink(\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n            name=name.val,\n            target=expr.val,\n        )\n\n\n#\n# Access Policies\n#\nsdl_commands_block(\n    'CreateAccessPolicy',\n    SetField,\n    SetAnnotation\n)\n\n\nclass AccessPolicyDeclarationBlock(Nonterm):\n    def reduce_CreateAccessPolicy(self, *kids):\n        \"\"\"%reduce\n            ACCESS POLICY ShortNodeName\n            OptWhenBlock AccessPolicyAction AccessKindList\n            OptUsingBlock\n            CreateAccessPolicySDLCommandsBlock\n        \"\"\"\n        _, _, name, when, action, access_kinds, using, commands = kids\n        self.val = qlast.CreateAccessPolicy(\n            name=name.val,\n            condition=when.val,\n            action=action.val,\n            access_kinds=[y for x in access_kinds.val for y in x],\n            expr=using.val,\n            commands=commands.val,\n        )\n\n\nclass AccessPolicyDeclarationShort(Nonterm):\n    def reduce_CreateAccessPolicy(self, *kids):\n        \"\"\"%reduce\n            ACCESS POLICY ShortNodeName\n            OptWhenBlock AccessPolicyAction AccessKindList\n            OptUsingBlock\n        \"\"\"\n        _, _, name, when, action, access_kinds, using = kids\n        self.val = qlast.CreateAccessPolicy(\n            name=name.val,\n            condition=when.val,\n            action=action.val,\n            access_kinds=[y for x in access_kinds.val for y in x],\n            expr=using.val,\n        )\n\n\n#\n# Triggers\n#\nsdl_commands_block(\n    'CreateTrigger',\n    SetField,\n    SetAnnotation\n)\n\n\nclass TriggerDeclarationBlock(Nonterm):\n    def reduce_CreateTrigger(self, *kids):\n        \"\"\"%reduce\n            TRIGGER NodeName\n            TriggerTiming TriggerKindList\n            FOR TriggerScope\n            OptWhenBlock\n            DO ParenExpr\n            CreateTriggerSDLCommandsBlock\n        \"\"\"\n        _, name, timing, kinds, _, scope, when, _, expr, commands = kids\n        self.val = qlast.CreateTrigger(\n            name=name.val,\n            timing=timing.val,\n            kinds=kinds.val,\n            scope=scope.val,\n            expr=expr.val,\n            condition=when.val,\n            commands=commands.val,\n        )\n\n\nclass TriggerDeclarationShort(Nonterm):\n    def reduce_CreateTrigger(self, *kids):\n        \"\"\"%reduce\n            TRIGGER NodeName\n            TriggerTiming TriggerKindList\n            FOR TriggerScope\n            OptWhenBlock\n            DO ParenExpr\n        \"\"\"\n        _, name, timing, kinds, _, scope, when, _, expr = kids\n        self.val = qlast.CreateTrigger(\n            name=name.val,\n            timing=timing.val,\n            kinds=kinds.val,\n            scope=scope.val,\n            expr=expr.val,\n            condition=when.val,\n        )\n\n\n#\n# Object Types\n#\n\nsdl_commands_block(\n    'CreateObjectType',\n    SetAnnotation,\n    ConcretePropertyBlock,\n    ConcretePropertyShort,\n    ConcreteLinkBlock,\n    ConcreteLinkShort,\n    ConcreteUnknownPointerBlock,\n    ConcreteUnknownPointerShort,\n    ConcreteUnknownPointerObjectShort,\n    ConcreteConstraintBlock,\n    ConcreteConstraintShort,\n    ConcreteIndexDeclarationBlock,\n    ConcreteIndexDeclarationShort,\n    AccessPolicyDeclarationBlock,\n    AccessPolicyDeclarationShort,\n    TriggerDeclarationBlock,\n    TriggerDeclarationShort,\n)\n\n\nclass ObjectTypeDeclaration(Nonterm):\n    def reduce_CreateAbstractObjectTypeStmt(self, *kids):\n        r\"\"\"%reduce \\\n            ABSTRACT TYPE NodeName OptExtendingSimple \\\n            CreateObjectTypeSDLCommandsBlock \\\n        \"\"\"\n        _, _, name, extending, commands = kids\n        self.val = qlast.CreateObjectType(\n            abstract=True,\n            name=name.val,\n            bases=extending.val,\n            commands=commands.val,\n        )\n\n    def reduce_CreateRegularObjectTypeStmt(self, *kids):\n        r\"\"\"%reduce \\\n            TYPE NodeName OptExtendingSimple \\\n            CreateObjectTypeSDLCommandsBlock \\\n        \"\"\"\n        _, name, extending, commands = kids\n        self.val = qlast.CreateObjectType(\n            name=name.val,\n            bases=extending.val,\n            commands=commands.val,\n        )\n\n\nclass ObjectTypeDeclarationShort(Nonterm):\n    def reduce_CreateAbstractObjectTypeStmt(self, *kids):\n        r\"\"\"%reduce \\\n            ABSTRACT TYPE NodeName OptExtendingSimple\"\"\"\n        _, _, name, extending = kids\n        self.val = qlast.CreateObjectType(\n            abstract=True,\n            name=name.val,\n            bases=extending.val,\n        )\n\n    def reduce_CreateRegularObjectTypeStmt(self, *kids):\n        r\"\"\"%reduce \\\n            TYPE NodeName OptExtendingSimple\"\"\"\n        _, name, extending = kids\n        self.val = qlast.CreateObjectType(\n            name=name.val,\n            bases=extending.val,\n        )\n\n\n#\n# Aliases\n#\n\nsdl_commands_block(\n    'CreateAlias',\n    Using,\n    SetField,\n    SetAnnotation,\n    opt=False\n)\n\n\nclass AliasDeclaration(Nonterm):\n    def reduce_CreateAliasRegularStmt(self, *kids):\n        r\"\"\"%reduce\n            ALIAS NodeName CreateAliasSDLCommandsBlock\n        \"\"\"\n        _, name, commands = kids\n        self.val = qlast.CreateAlias(\n            name=name.val,\n            commands=commands.val,\n        )\n\n\nclass AliasDeclarationShort(Nonterm):\n    def reduce_CreateAliasShortStmt(self, *kids):\n        r\"\"\"%reduce\n            ALIAS NodeName ASSIGN GenExpr\n        \"\"\"\n        _, name, _, expr = kids\n        self.val = qlast.CreateAlias(\n            name=name.val,\n            commands=[\n                qlast.SetField(\n                    name='expr',\n                    value=expr.val,\n                    special_syntax=True,\n                    span=self.span,\n                )\n            ]\n        )\n\n    def reduce_CreateAliasRegularStmt(self, *kids):\n        r\"\"\"%reduce\n            ALIAS NodeName CreateAliasSingleSDLCommandBlock\n        \"\"\"\n        _, name, commands = kids\n        self.val = qlast.CreateAlias(\n            name=name.val,\n            commands=commands.val,\n        )\n\n\n#\n# Functions\n#\n\n\nsdl_commands_block(\n    'CreateFunction',\n    commondl.FromFunction,\n    SetField,\n    SetAnnotation,\n    opt=False\n)\n\n\nclass FunctionDeclaration(Nonterm, commondl.ProcessFunctionBlockMixin):\n    def reduce_CreateFunction(self, *kids):\n        r\"\"\"%reduce FUNCTION NodeName CreateFunctionArgs \\\n                FunctionResult CreateFunctionSDLCommandsBlock\n        \"\"\"\n        _, name, args, result, body = kids\n        self.val = qlast.CreateFunction(\n            name=name.val,\n            params=args.val,\n            returning=result.val.result_type,\n            returning_typemod=result.val.type_qualifier,\n            **self._process_function_body(body),\n        )\n\n\nclass FunctionDeclarationShort(Nonterm, commondl.ProcessFunctionBlockMixin):\n    def reduce_CreateFunction(self, *kids):\n        r\"\"\"%reduce FUNCTION NodeName CreateFunctionArgs \\\n                FunctionResult CreateFunctionSingleSDLCommandBlock\n        \"\"\"\n        _, name, args, result, body = kids\n        self.val = qlast.CreateFunction(\n            name=name.val,\n            params=args.val,\n            returning=result.val.result_type,\n            returning_typemod=result.val.type_qualifier,\n            **self._process_function_body(body),\n        )\n\n\n#\n# Globals\n#\n\nsdl_commands_block(\n    'CreateGlobal',\n    Using,\n    SetField,\n    SetAnnotation,\n)\n\n\nclass GlobalDeclaration(Nonterm):\n    def _extract_target(self, target, cmds, span, *, overloaded=False):\n        if target:\n            return target, cmds\n\n        for cmd in cmds:\n            if isinstance(cmd, qlast.SetField) and cmd.name == 'expr':\n                if target is not None:\n                    raise errors.EdgeQLSyntaxError(\n                        f'computed global with more than one expression',\n                        span=span)\n                target = cmd.value\n\n        if not overloaded and target is None:\n            raise errors.EdgeQLSyntaxError(\n                f'computed property without expression',\n                span=span)\n\n        return target, cmds\n\n    def reduce_CreateGlobalQuals(self, *kids):\n        \"\"\"%reduce\n            PtrQuals GLOBAL NodeName\n            OptPtrTarget CreateGlobalSDLCommandsBlock\n        \"\"\"\n        quals, glob, name, target, commands = kids\n        target, cmds = self._extract_target(\n            target.val, commands.val, glob.span\n        )\n        self.val = qlast.CreateGlobal(\n            name=name.val,\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n            target=target,\n            commands=cmds,\n        )\n\n    def reduce_CreateGlobal(self, *kids):\n        \"\"\"%reduce\n            GLOBAL NodeName\n            OptPtrTarget CreateGlobalSDLCommandsBlock\n        \"\"\"\n        glob, name, target, commands = kids\n        target, cmds = self._extract_target(\n            target.val, commands.val, glob.span\n        )\n        self.val = qlast.CreateGlobal(\n            name=name.val,\n            target=target,\n            commands=cmds,\n        )\n\n\nclass GlobalDeclarationShort(Nonterm):\n    def reduce_CreateRegularGlobalShortQuals(self, *kids):\n        \"\"\"%reduce\n            PtrQuals GLOBAL NodeName PtrTarget\n        \"\"\"\n        quals, _, name, target = kids\n        self.val = qlast.CreateGlobal(\n            name=name.val,\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n            target=target.val,\n        )\n\n    def reduce_CreateRegularGlobalShort(self, *kids):\n        \"\"\"%reduce\n            GLOBAL NodeName PtrTarget\n        \"\"\"\n        _, name, target = kids\n        self.val = qlast.CreateGlobal(\n            name=name.val,\n            target=target.val,\n        )\n\n    def reduce_CreateComputedGlobalShortQuals(self, *kids):\n        \"\"\"%reduce\n            PtrQuals GLOBAL NodeName ASSIGN GenExpr\n        \"\"\"\n        quals, _, name, _, expr = kids\n        self.val = qlast.CreateGlobal(\n            name=name.val,\n            is_required=quals.val.required,\n            cardinality=quals.val.cardinality,\n            target=expr.val,\n        )\n\n    def reduce_CreateComputedGlobalShort(self, *kids):\n        \"\"\"%reduce\n            GLOBAL NodeName ASSIGN GenExpr\n        \"\"\"\n        _, name, _, expr = kids\n        self.val = qlast.CreateGlobal(\n            name=name.val,\n            target=expr.val,\n        )\n\n\n#\n# Permissions\n#\n\n\nsdl_commands_block(\n    'CreatePermission',\n    SetAnnotation,\n)\n\n\nclass PermissionDeclaration(Nonterm):\n    def reduce_CreatePermission(self, *kids):\n        \"\"\"%reduce\n            PERMISSION NodeName\n            CreatePermissionSDLCommandsBlock\n        \"\"\"\n        _, name, commands = kids\n        self.val = qlast.CreatePermission(\n            name=name.val,\n            commands=commands.val,\n        )\n\n\nclass PermissionDeclarationShort(Nonterm):\n    def reduce_CreatePermission(self, *kids):\n        \"\"\"%reduce\n            PERMISSION NodeName\n        \"\"\"\n        _, name = kids\n        self.val = qlast.CreatePermission(\n            name=name.val,\n        )\n"
  },
  {
    "path": "edb/edgeql/parser/grammar/session.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nfrom edb.edgeql import ast as qlast\n\nfrom .expressions import Nonterm\nfrom .tokens import *  # NOQA\nfrom .expressions import *  # NOQA\n\n\nclass SetStmt(Nonterm):\n    val: qlast.SessionSetAliasDecl\n\n    def reduce_SET_ALIAS_Identifier_AS_MODULE_ModuleName(self, *kids):\n        _, _, alias, _, _, module = kids\n        self.val = qlast.SessionSetAliasDecl(\n            decl=qlast.ModuleAliasDecl(\n                module='::'.join(module.val), alias=alias.val, span=self.span\n            )\n        )\n\n    def reduce_SET_MODULE_ModuleName(self, *kids):\n        _, _, module = kids\n        self.val = qlast.SessionSetAliasDecl(\n            decl=qlast.ModuleAliasDecl(\n                module='::'.join(module.val), span=self.span\n            )\n        )\n\n\nclass ResetStmt(Nonterm):\n    val: qlast.SessionResetAliasDecl\n\n    def reduce_RESET_ALIAS_Identifier(self, *kids):\n        self.val = qlast.SessionResetAliasDecl(\n            alias=kids[2].val)\n\n    def reduce_RESET_MODULE(self, *kids):\n        self.val = qlast.SessionResetModule()\n\n    def reduce_RESET_ALIAS_STAR(self, *kids):\n        self.val = qlast.SessionResetAllAliases()\n"
  },
  {
    "path": "edb/edgeql/parser/grammar/start.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom edb.common import parsing\nfrom edb.edgeql import ast as qlast\n\nfrom . import commondl\nfrom .expressions import Nonterm\nfrom .precedence import *  # NOQA\nfrom .tokens import *  # NOQA\nfrom .statements import *  # NOQA\nfrom .ddl import *  # NOQA\nfrom .session import *  # NOQA\nfrom .config import *  # NOQA\n\n\n# The main EdgeQL grammar, all of whose productions should start with a\n# GrammarToken, that determines the \"subgrammar\" to use.\n#\n# To add a new \"subgrammar\":\n# - add a new GrammarToken in tokens.py,\n# - add a new production here,\n# - add a new token kind in tokenizer.rs,\n# - add a mapping from the Python token name into the Rust token kind\n#   in parser.rs `fn get_token_kind`\nclass EdgeQLGrammar(Nonterm):\n    \"%start\"\n\n    val: qlast.GrammarEntryPoint\n\n    def reduce_STARTBLOCK_EdgeQLBlock_EOI(self, *kids):\n        self.val = kids[1].val\n\n    def reduce_STARTEXTENSION_CreateExtensionPackageCommandsBlock_EOI(self, *k):\n        self.val = k[1].val\n\n    def reduce_STARTMIGRATION_CreateMigrationCommandsBlock_EOI(self, *kids):\n        self.val = kids[1].val\n\n    def reduce_STARTFRAGMENT_ExprStmt_EOI(self, *kids):\n        self.val = kids[1].val\n\n    def reduce_STARTFRAGMENT_Expr_EOI(self, *kids):\n        self.val = kids[1].val\n\n    def reduce_STARTSDLDOCUMENT_SDLDocument_EOI(self, *kids):\n        self.val = kids[1].val\n\n\nclass EdgeQLBlock(Nonterm):\n    val: qlast.Commands\n\n    def reduce_StmtList_OptSemicolons(self, s, _semicolon):\n        self.val = qlast.Commands(commands=s.val)\n\n    def reduce_OptSemicolons(self, _semicolon):\n        self.val = qlast.Commands(commands=[])\n\n\nclass SingleStmt(Nonterm):\n    val: qlast.Command\n\n    @parsing.inline(0)\n    def reduce_Stmt(self, stmt):\n        pass\n\n    def reduce_IfThenElseExpr(self, *kids):\n        # TODO: this should not be here, but in ExprStmtSimpleCore instead\n        self.val = qlast.SelectQuery(result=kids[0].val, implicit=True)\n\n    @parsing.inline(0)\n    def reduce_DDLStmt(self, _):\n        # Data definition commands\n        pass\n\n    @parsing.inline(0)\n    def reduce_SetStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_ResetStmt(self, *kids):\n        pass\n\n    @parsing.inline(0)\n    def reduce_ConfigStmt(self, _):\n        # Configuration commands\n        pass\n\n\nclass StmtList(\n    parsing.ListNonterm, element=SingleStmt, separator=commondl.Semicolons\n):\n    val: list[qlast.Command]\n\n\nclass SDLDocument(Nonterm):\n    def reduce_OptSemicolons(self, *kids):\n        self.val = qlast.Schema(declarations=[])\n\n    def reduce_statement_without_semicolons(self, *kids):\n        r\"\"\"%reduce \\\n            OptSemicolons SDLShortStatement\n        \"\"\"\n        declarations = [kids[1].val]\n        commondl._validate_declarations(declarations)\n        self.val = qlast.Schema(declarations=declarations)\n\n    def reduce_statements_without_optional_trailing_semicolons(self, *kids):\n        r\"\"\"%reduce \\\n            OptSemicolons SDLStatements \\\n            OptSemicolons SDLShortStatement\n        \"\"\"\n        declarations = kids[1].val + [kids[3].val]\n        commondl._validate_declarations(declarations)\n        self.val = qlast.Schema(declarations=declarations)\n\n    def reduce_OptSemicolons_SDLStatements(self, *kids):\n        declarations = kids[1].val\n        commondl._validate_declarations(declarations)\n        self.val = qlast.Schema(declarations=declarations)\n\n    def reduce_OptSemicolons_SDLStatements_Semicolons(self, *kids):\n        declarations = kids[1].val\n        commondl._validate_declarations(declarations)\n        self.val = qlast.Schema(declarations=declarations)\n"
  },
  {
    "path": "edb/edgeql/parser/grammar/statements.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nimport typing\n\nfrom edb import errors\nfrom edb.common import parsing\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom .expressions import Nonterm, ListNonterm\nfrom .precedence import *  # NOQA\nfrom .tokens import *  # NOQA\nfrom .expressions import *  # NOQA\n\nfrom . import tokens\n\n\nclass Stmt(Nonterm):\n    val: qlast.Command\n\n    @parsing.inline(0)\n    def reduce_TransactionStmt(self, stmt):\n        pass\n\n    @parsing.inline(0)\n    def reduce_DescribeStmt(self, stmt):\n        # DESCRIBE\n        pass\n\n    @parsing.inline(0)\n    def reduce_AnalyzeStmt(self, stmt):\n        # ANALYZE\n        pass\n\n    @parsing.inline(0)\n    def reduce_AdministerStmt(self, stmt):\n        pass\n\n    @parsing.inline(0)\n    def reduce_ExprStmt(self, stmt):\n        pass\n\n\nclass TransactionMode(Nonterm):\n\n    def reduce_ISOLATION_SERIALIZABLE(self, *kids):\n        self.val = (qltypes.TransactionIsolationLevel.SERIALIZABLE,\n                    kids[0].span)\n\n    def reduce_ISOLATION_REPEATABLE_READ(self, *kids):\n        self.val = (qltypes.TransactionIsolationLevel.REPEATABLE_READ,\n                    kids[0].span)\n\n    def reduce_READ_WRITE(self, *kids):\n        self.val = (qltypes.TransactionAccessMode.READ_WRITE,\n                    kids[0].span)\n\n    def reduce_READ_ONLY(self, *kids):\n        self.val = (qltypes.TransactionAccessMode.READ_ONLY,\n                    kids[0].span)\n\n    def reduce_DEFERRABLE(self, *kids):\n        self.val = (qltypes.TransactionDeferMode.DEFERRABLE,\n                    kids[0].span)\n\n    def reduce_NOT_DEFERRABLE(self, *kids):\n        self.val = (qltypes.TransactionDeferMode.NOT_DEFERRABLE,\n                    kids[0].span)\n\n\nclass TransactionModeList(ListNonterm, element=TransactionMode,\n                          separator=tokens.T_COMMA):\n    pass\n\n\nclass OptTransactionModeList(Nonterm):\n\n    @parsing.inline(0)\n    def reduce_TransactionModeList(self, *kids):\n        pass\n\n    def reduce_empty(self, *kids):\n        self.val = []\n\n\nclass TransactionStmt(Nonterm):\n\n    def reduce_START_TRANSACTION_OptTransactionModeList(self, *kids):\n        modes = kids[2].val\n\n        isolation = None\n        access = None\n        deferrable = None\n\n        for mode, mode_ctx in modes:\n            if isinstance(mode, qltypes.TransactionIsolationLevel):\n                if isolation is not None:\n                    raise errors.EdgeQLSyntaxError(\n                        f\"only one isolation level can be specified\",\n                        span=mode_ctx)\n                isolation = mode\n\n            elif isinstance(mode, qltypes.TransactionAccessMode):\n                if access is not None:\n                    raise errors.EdgeQLSyntaxError(\n                        f\"only one access mode can be specified\",\n                        span=mode_ctx)\n                access = mode\n\n            else:\n                assert isinstance(mode, qltypes.TransactionDeferMode)\n                if deferrable is not None:\n                    raise errors.EdgeQLSyntaxError(\n                        f\"deferrable mode can only be specified once\",\n                        span=mode_ctx)\n                deferrable = mode\n\n        self.val = qlast.StartTransaction(\n            isolation=isolation, access=access, deferrable=deferrable)\n\n    def reduce_COMMIT(self, *kids):\n        self.val = qlast.CommitTransaction()\n\n    def reduce_ROLLBACK(self, *kids):\n        self.val = qlast.RollbackTransaction()\n\n    def reduce_DECLARE_SAVEPOINT_Identifier(self, *kids):\n        self.val = qlast.DeclareSavepoint(name=kids[2].val)\n\n    def reduce_ROLLBACK_TO_SAVEPOINT_Identifier(self, *kids):\n        self.val = qlast.RollbackToSavepoint(name=kids[3].val)\n\n    def reduce_RELEASE_SAVEPOINT_Identifier(self, *kids):\n        self.val = qlast.ReleaseSavepoint(name=kids[2].val)\n\n\nclass DescribeFmt(typing.NamedTuple):\n\n    language: typing.Optional[qltypes.DescribeLanguage] = None\n    options: typing.Optional[qlast.Options] = None\n\n\nclass DescribeFormat(Nonterm):\n    val: DescribeFmt\n\n    def reduce_empty(self, *kids):\n        self.val = DescribeFmt(\n            language=qltypes.DescribeLanguage.DDL,\n            options=qlast.Options(),\n        )\n\n    def reduce_AS_DDL(self, *kids):\n        self.val = DescribeFmt(\n            language=qltypes.DescribeLanguage.DDL,\n            options=qlast.Options(),\n        )\n\n    def reduce_AS_SDL(self, *kids):\n        self.val = DescribeFmt(\n            language=qltypes.DescribeLanguage.SDL,\n            options=qlast.Options(),\n        )\n\n    def reduce_AS_JSON(self, *kids):\n        self.val = DescribeFmt(\n            language=qltypes.DescribeLanguage.JSON,\n            options=qlast.Options(),\n        )\n\n    def reduce_AS_TEXT(self, *kids):\n        self.val = DescribeFmt(\n            language=qltypes.DescribeLanguage.TEXT,\n            options=qlast.Options(),\n        )\n\n    def reduce_AS_TEXT_VERBOSE(self, *kids):\n        self.val = DescribeFmt(\n            language=qltypes.DescribeLanguage.TEXT,\n            options=qlast.Options(\n                options={'VERBOSE': qlast.OptionFlag(\n                    name='VERBOSE', val=True, span=kids[2].span)}\n            ),\n        )\n\n\nclass DescribeStmt(Nonterm):\n    val: qlast.DescribeStmt\n\n    def reduce_DESCRIBE_SCHEMA(self, *kids):\n        \"\"\"%reduce DESCRIBE SCHEMA DescribeFormat\"\"\"\n        self.val = qlast.DescribeStmt(\n            object=qlast.DescribeGlobal.Schema,\n            language=kids[2].val.language,\n            options=kids[2].val.options,\n        )\n\n    def reduce_DESCRIBE_CURRENT_DATABASE_CONFIG(self, *kids):\n        \"\"\"%reduce DESCRIBE CURRENT DATABASE CONFIG DescribeFormat\"\"\"\n        self.val = qlast.DescribeStmt(\n            object=qlast.DescribeGlobal.DatabaseConfig,\n            language=kids[4].val.language,\n            options=kids[4].val.options,\n        )\n\n    def reduce_DESCRIBE_CURRENT_BRANCH_CONFIG(self, *kids):\n        \"\"\"%reduce DESCRIBE CURRENT BRANCH CONFIG DescribeFormat\"\"\"\n        self.val = qlast.DescribeStmt(\n            object=qlast.DescribeGlobal.DatabaseConfig,\n            language=kids[4].val.language,\n            options=kids[4].val.options,\n        )\n\n    def reduce_DESCRIBE_INSTANCE_CONFIG(self, *kids):\n        \"\"\"%reduce DESCRIBE INSTANCE CONFIG DescribeFormat\"\"\"\n        self.val = qlast.DescribeStmt(\n            object=qlast.DescribeGlobal.InstanceConfig,\n            language=kids[3].val.language,\n            options=kids[3].val.options,\n        )\n\n    def reduce_DESCRIBE_SYSTEM_CONFIG(self, *kids):\n        \"\"\"%reduce DESCRIBE SYSTEM CONFIG DescribeFormat\"\"\"\n        return self.reduce_DESCRIBE_INSTANCE_CONFIG(*kids)\n\n    def reduce_DESCRIBE_ROLES(self, *kids):\n        \"\"\"%reduce DESCRIBE ROLES DescribeFormat\"\"\"\n        self.val = qlast.DescribeStmt(\n            object=qlast.DescribeGlobal.Roles,\n            language=kids[2].val.language,\n            options=kids[2].val.options,\n        )\n\n    def reduce_DESCRIBE_SchemaItem(self, *kids):\n        \"\"\"%reduce DESCRIBE SchemaItem DescribeFormat\"\"\"\n        self.val = qlast.DescribeStmt(\n            object=kids[1].val,\n            language=kids[2].val.language,\n            options=kids[2].val.options,\n        )\n\n    def reduce_DESCRIBE_OBJECT(self, *kids):\n        \"\"\"%reduce DESCRIBE OBJECT NodeName DescribeFormat\"\"\"\n        self.val = qlast.DescribeStmt(\n            object=kids[2].val,\n            language=kids[3].val.language,\n            options=kids[3].val.options,\n        )\n\n    def reduce_DESCRIBE_CURRENT_MIGRATION(self, *kids):\n        \"\"\"%reduce DESCRIBE CURRENT MIGRATION DescribeFormat\"\"\"\n        lang = kids[3].val.language\n        if (\n            lang is not qltypes.DescribeLanguage.DDL\n            and lang is not qltypes.DescribeLanguage.JSON\n        ):\n            raise errors.InvalidSyntaxError(\n                f'unexpected DESCRIBE format: {lang!r}',\n                span=kids[3].span,\n            )\n        if kids[3].val.options:\n            raise errors.InvalidSyntaxError(\n                f'DESCRIBE CURRENT MIGRATION does not support options',\n                span=kids[3].span,\n            )\n\n        self.val = qlast.DescribeCurrentMigration(\n            language=lang,\n        )\n\n\nclass AnalyzeStmt(Nonterm):\n    val: qlast.ExplainStmt\n\n    def reduce_ANALYZE_NamedTuple_ExprStmt(self, *kids):\n        _, args, stmt = kids\n        self.val = qlast.ExplainStmt(\n            args=args.val,\n            query=stmt.val,\n        )\n\n    def reduce_ANALYZE_ExprStmt(self, *kids):\n        _, stmt = kids\n        self.val = qlast.ExplainStmt(\n            query=stmt.val,\n        )\n\n\nclass AdministerStmt(Nonterm):\n    val: qlast.AdministerStmt\n\n    def reduce_ADMINISTER_FuncExpr(self, *kids):\n        _, expr = kids\n        self.val = qlast.AdministerStmt(expr=expr.val)\n"
  },
  {
    "path": "edb/edgeql/parser/grammar/tokens.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport re\nimport sys\nimport types\nimport typing\n\nfrom edb.common import parsing\n\nfrom . import keywords\n\n\nclean_string = re.compile(r\"'(?:\\s|\\n)+'\")\nstring_quote = re.compile(r'\\$(?:[A-Za-z_][A-Za-z_0-9]*)?\\$')\n\n\nclass Token(parsing.Token, is_internal=True):\n    pass\n\n\nclass GrammarToken(Token, is_internal=True):\n    \"\"\"\n    Instead of having different grammars, we prefix each query with a special\n    grammar token which directs the parser to appropriate grammar.\n\n    This greatly reduces the combined size of grammar specifications, since the\n    overlap between grammars is substantial.\n    \"\"\"\n\n\nclass T_STARTBLOCK(GrammarToken):\n    pass\n\n\nclass T_STARTEXTENSION(GrammarToken):\n    pass\n\n\nclass T_STARTFRAGMENT(GrammarToken):\n    pass\n\n\nclass T_STARTMIGRATION(GrammarToken):\n    pass\n\n\nclass T_STARTSDLDOCUMENT(GrammarToken):\n    pass\n\n\nclass T_STRINTERPSTART(GrammarToken):\n    pass\n\n\nclass T_STRINTERPCONT(GrammarToken):\n    pass\n\n\nclass T_STRINTERPEND(GrammarToken):\n    pass\n\n\nclass T_DOT(Token, lextoken='.'):\n    pass\n\n\nclass T_DOTBW(Token, lextoken='.<'):\n    pass\n\n\nclass T_DOTQ(Token, lextoken='.?>'):\n    pass\n\n\nclass T_LBRACKET(Token, lextoken='['):\n    pass\n\n\nclass T_RBRACKET(Token, lextoken=']'):\n    pass\n\n\nclass T_LPAREN(Token, lextoken='('):\n    pass\n\n\nclass T_RPAREN(Token, lextoken=')'):\n    pass\n\n\nclass T_LBRACE(Token, lextoken='{'):\n    pass\n\n\nclass T_RBRACE(Token, lextoken='}'):\n    pass\n\n\nclass T_DOUBLECOLON(Token, lextoken='::'):\n    pass\n\n\nclass T_DOUBLESTAR(Token, lextoken='**'):\n    pass\n\n\nclass T_DOUBLEQMARK(Token, lextoken='??'):\n    pass\n\n\nclass T_COLON(Token, lextoken=':'):\n    pass\n\n\nclass T_SEMICOLON(Token, lextoken=';'):\n    pass\n\n\nclass T_COMMA(Token, lextoken=','):\n    pass\n\n\nclass T_PLUS(Token, lextoken='+'):\n    pass\n\n\nclass T_DOUBLEPLUS(Token, lextoken='++'):\n    pass\n\n\nclass T_MINUS(Token, lextoken='-'):\n    pass\n\n\nclass T_STAR(Token, lextoken='*'):\n    pass\n\n\nclass T_SLASH(Token, lextoken='/'):\n    pass\n\n\nclass T_DOUBLESLASH(Token, lextoken='//'):\n    pass\n\n\nclass T_PERCENT(Token, lextoken='%'):\n    pass\n\n\nclass T_CIRCUMFLEX(Token, lextoken='^'):\n    pass\n\n\nclass T_AT(Token, lextoken='@'):\n    pass\n\n\nclass T_PARAMETER(Token):\n    pass\n\n\nclass T_PARAMETERANDTYPE(Token):\n    # A special token produced by normalization\n    pass\n\n\nclass T_ASSIGN(Token, lextoken=':='):\n    pass\n\n\nclass T_ADDASSIGN(Token, lextoken='+='):\n    pass\n\n\nclass T_REMASSIGN(Token, lextoken='-='):\n    pass\n\n\nclass T_ARROW(Token, lextoken='->'):\n    pass\n\n\nclass T_LANGBRACKET(Token, lextoken='<'):\n    pass\n\n\nclass T_RANGBRACKET(Token, lextoken='>'):\n    pass\n\n\nclass T_EQUALS(Token, lextoken='='):\n    pass\n\n\nclass T_AMPER(Token, lextoken='&'):\n    pass\n\n\nclass T_PIPE(Token, lextoken='|'):\n    pass\n\n\nclass T_NAMEDONLY(Token, lextoken='named only'):\n    pass\n\n\nclass T_SETTYPE(Token, lextoken='set type'):\n    pass\n\n\nclass T_EXTENSIONPACKAGE(Token, lextoken='extension package'):\n    pass\n\n\nclass T_ORDERBY(Token, lextoken='order by'):\n    pass\n\n\nclass T_ICONST(Token):\n    pass\n\n\nclass T_NICONST(Token):\n    pass\n\n\nclass T_FCONST(Token):\n    pass\n\n\nclass T_NFCONST(Token):\n    pass\n\n\nclass T_BCONST(Token):\n    pass\n\n\nclass T_SCONST(Token):\n    pass\n\n\nclass T_DISTINCTFROM(Token, lextoken=\"?!=\"):\n    pass\n\n\nclass T_GREATEREQ(Token, lextoken=\">=\"):\n    pass\n\n\nclass T_LESSEQ(Token, lextoken=\"<=\"):\n    pass\n\n\nclass T_NOTDISTINCTFROM(Token, lextoken=\"?=\"):\n    pass\n\n\nclass T_NOTEQ(Token, lextoken=\"!=\"):\n    pass\n\n\nclass T_IDENT(Token):\n    pass\n\n\nclass T_EOI(Token):\n    pass\n\n\n# explicitly define tokens which are referenced elsewhere\nT_THEN: typing.Optional[Token] = None\n\n\ndef _gen_keyword_tokens():\n    # Define keyword tokens\n\n    mod = sys.modules[__name__]\n\n    def clsexec(ns):\n        ns['__module__'] = __name__\n        return ns\n\n    for token, _ in keywords.edgeql_keywords.values():\n        clsname = 'T_{}'.format(token)\n        clskwds = dict(token=token)\n        cls = types.new_class(clsname, (Token,), clskwds, clsexec)\n        setattr(mod, clsname, cls)\n\n\n_gen_keyword_tokens()\n"
  },
  {
    "path": "edb/edgeql/qltypes.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import TYPE_CHECKING\n\nimport enum\n\nfrom edb.common import enum as s_enum\n\n\nif TYPE_CHECKING:\n    from edb.schema import types as s_types\n\n\nclass ParameterKind(s_enum.StrEnum):\n    VariadicParam = 'VariadicParam'\n    NamedOnlyParam = 'NamedOnlyParam'\n    PositionalParam = 'PositionalParam'\n\n    def to_edgeql(self) -> str:\n        if self is ParameterKind.VariadicParam:\n            return 'VARIADIC'\n        elif self is ParameterKind.NamedOnlyParam:\n            return 'NAMED ONLY'\n        else:\n            return ''\n\n\nclass TypeModifier(s_enum.StrEnum):\n    SetOfType = 'SetOfType'\n    OptionalType = 'OptionalType'\n    SingletonType = 'SingletonType'\n\n    def to_edgeql(self) -> str:\n        if self is TypeModifier.SetOfType:\n            return 'SET OF'\n        elif self is TypeModifier.OptionalType:\n            return 'OPTIONAL'\n        else:\n            return ''\n\n\nclass Polymorphism(s_enum.StrEnum):\n    NotUsed = 'NotUsed'\n    Simple = 'Simple'\n    Array = 'Array'\n    Collection = 'Collection'\n\n    @staticmethod\n    def from_schema_type(type: s_types.Type) -> Polymorphism:\n        return (\n            Polymorphism.Simple\n            if not type.is_collection() else\n            Polymorphism.Array\n            if type.is_array() else\n            Polymorphism.Collection\n        )\n\n\nclass OperatorKind(s_enum.StrEnum):\n    Infix = 'Infix'\n    Postfix = 'Postfix'\n    Prefix = 'Prefix'\n    Ternary = 'Ternary'\n\n\nclass TransactionIsolationLevel(s_enum.StrEnum):\n    REPEATABLE_READ = 'REPEATABLE READ'\n    SERIALIZABLE = 'SERIALIZABLE'\n\n\nclass TransactionAccessMode(s_enum.StrEnum):\n    READ_WRITE = 'READ WRITE'\n    READ_ONLY = 'READ ONLY'\n\n\nclass TransactionDeferMode(s_enum.StrEnum):\n    DEFERRABLE = 'DEFERRABLE'\n    NOT_DEFERRABLE = 'NOT DEFERRABLE'\n\n\nclass SchemaCardinality(s_enum.OrderedEnumMixin, s_enum.StrEnum):\n    '''This enum is used to store cardinality in the schema.'''\n    One = 'One'\n    Many = 'Many'\n    Unknown = 'Unknown'\n\n    def is_multi(self) -> bool:\n        if self is SchemaCardinality.One:\n            return False\n        elif self is SchemaCardinality.Many:\n            return True\n        else:\n            raise ValueError('cardinality is unknown')\n\n    def is_single(self) -> bool:\n        return not self.is_multi()\n\n    def is_known(self) -> bool:\n        return self is not SchemaCardinality.Unknown\n\n    def as_ptr_qual(self) -> str:\n        if self is SchemaCardinality.One:\n            return 'single'\n        elif self is SchemaCardinality.Many:\n            return 'multi'\n        else:\n            raise ValueError('cardinality is unknown')\n\n    def to_edgeql(self) -> str:\n        return self.as_ptr_qual().upper()\n\n\nclass Cardinality(s_enum.StrEnum):\n    '''This enum is used in cardinality inference internally.'''\n    # [0, 1]\n    AT_MOST_ONE = 'AT_MOST_ONE'\n    # [1, 1]\n    ONE = 'ONE'\n    # [0, inf)\n    MANY = 'MANY'\n    # [1, inf)\n    AT_LEAST_ONE = 'AT_LEAST_ONE'\n    # Sentinel\n    UNKNOWN = 'UNKNOWN'\n\n    def is_single(self) -> bool:\n        return self in {Cardinality.AT_MOST_ONE, Cardinality.ONE}\n\n    def is_multi(self) -> bool:\n        return not self.is_single()\n\n    def can_be_zero(self) -> bool:\n        return self not in {Cardinality.ONE, Cardinality.AT_LEAST_ONE}\n\n    def to_schema_value(self) -> tuple[bool, SchemaCardinality]:\n        return _CARD_TO_TUPLE[self]\n\n    @classmethod\n    def from_schema_value(\n        cls, required: bool, card: SchemaCardinality\n    ) -> Cardinality:\n        return _TUPLE_TO_CARD[(required, card)]\n\n\n_CARD_TO_TUPLE = {\n    Cardinality.AT_MOST_ONE: (False, SchemaCardinality.One),\n    Cardinality.ONE: (True, SchemaCardinality.One),\n    Cardinality.MANY: (False, SchemaCardinality.Many),\n    Cardinality.AT_LEAST_ONE: (True, SchemaCardinality.Many),\n}\n_TUPLE_TO_CARD = {\n    (False, SchemaCardinality.One): Cardinality.AT_MOST_ONE,\n    (True, SchemaCardinality.One): Cardinality.ONE,\n    (False, SchemaCardinality.Many): Cardinality.MANY,\n    (True, SchemaCardinality.Many): Cardinality.AT_LEAST_ONE,\n}\n\n\nclass Volatility(s_enum.OrderedEnumMixin, s_enum.StrEnum):\n    # Make sure that the values appear from least volatile to most volatile.\n    Immutable = 'Immutable'\n    Stable = 'Stable'\n    Volatile = 'Volatile'\n    Modifying = 'Modifying'\n\n    def is_volatile(self) -> bool:\n        return self in (Volatility.Volatile, Volatility.Modifying)\n\n    @classmethod\n    def _missing_(cls, name):\n        # We want both `volatility := 'immutable'` in SDL and\n        # `SET volatility := 'IMMUTABLE`` in DDL to work.\n        return cls(name.title())\n\n\nclass Multiplicity(s_enum.OrderedEnumMixin, s_enum.StrEnum):\n    # Make sure that the values appear in ascending order.\n    EMPTY = 'EMPTY'\n    UNIQUE = 'UNIQUE'\n    DUPLICATE = 'DUPLICATE'\n    UNKNOWN = 'UNKNOWN'\n\n    def is_empty(self) -> bool:\n        return self is Multiplicity.EMPTY\n\n    def is_unique(self) -> bool:\n        return self is Multiplicity.UNIQUE\n\n    def is_duplicate(self) -> bool:\n        return self is Multiplicity.DUPLICATE\n\n\nclass IndexDeferrability(s_enum.OrderedEnumMixin, s_enum.StrEnum):\n    Prohibited = 'Prohibited'\n    Permitted = 'Permitted'\n    Required = 'Required'\n\n    def is_deferrable(self) -> bool:\n        return (\n            self is IndexDeferrability.Required\n            or self is IndexDeferrability.Permitted\n        )\n\n\nclass AccessPolicyAction(s_enum.StrEnum):\n    Allow = 'Allow'\n    Deny = 'Deny'\n\n\nclass AccessKind(s_enum.StrEnum):\n    Select = 'Select'\n    UpdateRead = 'UpdateRead'\n    UpdateWrite = 'UpdateWrite'\n    Delete = 'Delete'\n    Insert = 'Insert'\n\n    def is_data_check(self) -> bool:\n        return self is AccessKind.UpdateWrite or self is AccessKind.Insert\n\n\nclass TriggerTiming(s_enum.StrEnum):\n    After = 'After'\n    AfterCommitOf = 'After Commit Of'\n\n\nclass TriggerKind(s_enum.StrEnum):\n    Update = 'Update'\n    Delete = 'Delete'\n    Insert = 'Insert'\n\n\nclass TriggerScope(s_enum.StrEnum):\n    Each = 'Each'\n    All = 'All'\n\n\nclass RewriteKind(s_enum.StrEnum):\n    Update = 'Update'\n    Insert = 'Insert'\n\n\nclass SplatStrategy(s_enum.StrEnum):\n    Default = 'Default'\n    Explicit = 'Explicit'\n    Implicit = 'Implicit'\n\n\nclass DescribeLanguage(s_enum.StrEnum):\n    DDL = 'DDL'\n    SDL = 'SDL'\n    TEXT = 'TEXT'\n    JSON = 'JSON'\n\n\nclass SchemaObjectClass(s_enum.StrEnum):\n\n    ACCESS_POLICY = 'ACCESS_POLICY'\n    ALIAS = 'ALIAS'\n    ANNOTATION = 'ANNOTATION'\n    ARRAY_TYPE = 'ARRAY TYPE'\n    BRANCH = 'BRANCH'\n    CAST = 'CAST'\n    CONSTRAINT = 'CONSTRAINT'\n    DATABASE = 'DATABASE'\n    EXTENSION = 'EXTENSION'\n    EXTENSION_PACKAGE = 'EXTENSION PACKAGE'\n    EXTENSION_PACKAGE_MIGRATION = 'EXTENSION PACKAGE MIGRATION'\n    FUTURE = 'FUTURE'\n    FUNCTION = 'FUNCTION'\n    GLOBAL = 'GLOBAL'\n    INDEX = 'INDEX'\n    INDEX_MATCH = 'INDEX MATCH'\n    LINK = 'LINK'\n    MIGRATION = 'MIGRATION'\n    MODULE = 'MODULE'\n    MULTIRANGE_TYPE = 'MULTIRANGE_TYPE'\n    OPERATOR = 'OPERATOR'\n    PARAMETER = 'PARAMETER'\n    PERMISSION = 'PERMISSION'\n    PROPERTY = 'PROPERTY'\n    PSEUDO_TYPE = 'PSEUDO TYPE'\n    RANGE_TYPE = 'RANGE TYPE'\n    REWRITE = 'REWRITE'\n    ROLE = 'ROLE'\n    SCALAR_TYPE = 'SCALAR TYPE'\n    TRIGGER = 'TRIGGER'\n    TUPLE_TYPE = 'TUPLE TYPE'\n    TYPE = 'TYPE'\n\n\nclass LinkTargetDeleteAction(s_enum.StrEnum):\n    Restrict = 'Restrict'\n    DeleteSource = 'DeleteSource'\n    Allow = 'Allow'\n    DeferredRestrict = 'DeferredRestrict'\n\n    def to_edgeql(self) -> str:\n        if self is LinkTargetDeleteAction.DeleteSource:\n            return 'DELETE SOURCE'\n        elif self is LinkTargetDeleteAction.DeferredRestrict:\n            return 'DEFERRED RESTRICT'\n        elif self is LinkTargetDeleteAction.Restrict:\n            return 'RESTRICT'\n        elif self is LinkTargetDeleteAction.Allow:\n            return 'ALLOW'\n        else:\n            raise ValueError(f'unsupported enum value {self!r}')\n\n\nclass LinkSourceDeleteAction(s_enum.StrEnum):\n    DeleteTarget = 'DeleteTarget'\n    Allow = 'Allow'\n    DeleteTargetIfOrphan = 'DeleteTargetIfOrphan'\n\n    def to_edgeql(self) -> str:\n        if self is LinkSourceDeleteAction.DeleteTarget:\n            return 'DELETE TARGET'\n        elif self is LinkSourceDeleteAction.Allow:\n            return 'ALLOW'\n        elif self is LinkSourceDeleteAction.DeleteTargetIfOrphan:\n            return 'DELETE TARGET IF ORPHAN'\n        else:\n            raise ValueError(f'unsupported enum value {self!r}')\n\n\nclass ConfigScope(s_enum.StrEnum):\n\n    INSTANCE = 'INSTANCE'\n    DATABASE = 'DATABASE'\n    SESSION = 'SESSION'\n    GLOBAL = 'GLOBAL'\n\n    def to_edgeql(self) -> str:\n        if self is ConfigScope.DATABASE:\n            return 'CURRENT BRANCH'\n        else:\n            return str(self)\n\n\nclass TypeTag(enum.IntEnum):\n    SCALAR = 0\n    TUPLE = 1\n    ARRAY = 2\n"
  },
  {
    "path": "edb/edgeql/quote.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2013-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport re\n\nfrom .parser.grammar import keywords\n\n\n_re_ident = re.compile(r'''(?x)\n    [^\\W\\d]\\w*  # alphanumeric identifier\n''')\n\n_re_ident_or_num = re.compile(r'''(?x)\n    [^\\W\\d]\\w*  # alphanumeric identifier\n    |\n    ([1-9]\\d* | 0)  # purely integer identifier\n''')\n\n\ndef escape_string(s: str) -> str:\n    # characters escaped according to\n    # https://www.edgedb.com/docs/reference/edgeql/lexical#strings\n    result = s\n\n    # escape backslash first\n    result = result.replace('\\\\', '\\\\\\\\')\n\n    result = result.replace('\\'', '\\\\\\'')\n    result = result.replace('\\b', '\\\\b')\n    result = result.replace('\\f', '\\\\f')\n    result = result.replace('\\n', '\\\\n')\n    result = result.replace('\\r', '\\\\r')\n    result = result.replace('\\t', '\\\\t')\n\n    return result\n\n\ndef quote_literal(string: str) -> str:\n    return \"'\" + escape_string(string) + \"'\"\n\n\ndef dollar_quote_literal(text: str) -> str:\n    quote = '$$'\n    qq = 0\n\n    while quote in text:\n        if qq % 16 < 10:\n            qq += 10 - qq % 16\n\n        quote = '${:x}$'.format(qq)[::-1]\n        qq += 1\n\n    return quote + text + quote\n\n\ndef needs_quoting(string: str, allow_reserved: bool, allow_num: bool) -> bool:\n    if not string or string.startswith('@') or '::' in string:\n        # some strings are illegal as identifiers and as such don't\n        # require quoting\n        return False\n\n    r = _re_ident_or_num if allow_num else _re_ident\n    isalnum = r.fullmatch(string)\n\n    string = string.lower()\n\n    is_reserved = (\n        string not in {'__type__', '__std__'}\n        and string in keywords.by_type[keywords.RESERVED_KEYWORD]\n    )\n\n    return (\n        not isalnum\n        or (not allow_reserved and is_reserved)\n    )\n\n\ndef _quote_ident(string: str) -> str:\n    return '`' + string.replace('`', '``') + '`'\n\n\ndef quote_ident(\n    string: str,\n    *,\n    force: bool = False,\n    allow_reserved: bool = False,\n    allow_num: bool = False,\n) -> str:\n    if force or needs_quoting(string, allow_reserved, allow_num):\n        return _quote_ident(string)\n    else:\n        return string\n"
  },
  {
    "path": "edb/edgeql/tokenizer.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, Sequence\n\nimport re\nimport hashlib\n\nimport edb._edgeql_parser as ql_parser\n\nfrom edb import errors\n\n\nTRAILING_WS_IN_CONTINUATION = re.compile(r'\\\\ \\s+\\n')\n\n\ndef deserialize(serialized: bytes, text: str) -> Source:\n    match serialized[0]:\n        case 0:\n            tokens = ql_parser.unpack(serialized)\n            assert isinstance(tokens, list)\n            return Source(text, tokens, serialized)\n        case 1:\n            entry = ql_parser.unpack(serialized)\n            assert isinstance(entry, ql_parser.Entry)\n            return NormalizedSource(entry, text, serialized)\n\n    raise ValueError(f\"Invalid type/version byte: {serialized[0]}\")\n\n\nclass Source:\n    def __init__(\n        self,\n        text: str,\n        tokens: list[ql_parser.OpaqueToken],\n        serialized: bytes,\n    ) -> None:\n        self._cache_key = hashlib.blake2b(serialized).digest()\n        self._text = text\n        self._tokens = tokens\n        self._serialized = serialized\n\n    def text(self) -> str:\n        return self._text\n\n    def cache_key(self) -> bytes:\n        return self._cache_key\n\n    def variables(self) -> dict[str, Any]:\n        return {}\n\n    def tokens(self) -> list[ql_parser.OpaqueToken]:\n        return self._tokens\n\n    def first_extra(self) -> Optional[int]:\n        return None\n\n    def extra_counts(self) -> Sequence[int]:\n        return ()\n\n    def extra_blobs(self) -> list[bytes]:\n        return []\n\n    def extra_formatted_as_text(self) -> bool:\n        return False\n\n    def extra_type_oids(self) -> Sequence[int]:\n        return ()\n\n    def serialize(self) -> bytes:\n        return self._serialized\n\n    @staticmethod\n    def from_string(text: str) -> Source:\n        result = _tokenize(text)\n        assert isinstance(result.out, list)\n        return Source(text=text, tokens=result.out, serialized=result.pack())\n\n    def __repr__(self):\n        return f'<edgeql.Source text={self._text!r}>'\n\n    def denormalized(self) -> Source:\n        return self\n\n\nclass NormalizedSource(Source):\n    def __init__(\n        self,\n        normalized: ql_parser.Entry,\n        text: str,\n        serialized: bytes,\n    ) -> None:\n        self._text = text\n        self._cache_key = normalized.key\n        self._tokens = normalized.tokens\n        self._variables = normalized.get_variables()\n        self._first_extra = normalized.first_extra\n        self._extra_counts = normalized.extra_counts\n        self._extra_blobs = normalized.extra_blobs\n        self._serialized = serialized\n\n    def text(self) -> str:\n        return self._text\n\n    def cache_key(self) -> bytes:\n        return self._cache_key\n\n    def variables(self) -> dict[str, Any]:\n        return self._variables\n\n    def tokens(self) -> list[ql_parser.OpaqueToken]:\n        return self._tokens\n\n    def first_extra(self) -> Optional[int]:\n        return self._first_extra\n\n    def extra_counts(self) -> Sequence[int]:\n        return self._extra_counts\n\n    def extra_blobs(self) -> list[bytes]:\n        return self._extra_blobs\n\n    @staticmethod\n    def from_string(text: str) -> NormalizedSource:\n        normalized = _normalize(text)\n        return NormalizedSource(normalized, text, normalized.pack())\n\n    def denormalized(self) -> Source:\n        return Source.from_string(self._text)\n\n\ndef inflate_span(\n    source: str, span: tuple[int, Optional[int]]\n) -> tuple[ql_parser.SourcePoint, Optional[ql_parser.SourcePoint]]:\n    (start, end) = span\n    source_bytes = source.encode('utf-8')\n\n    points = [start]\n    if end is not None:\n        points.append(end)\n\n    points_sp = ql_parser.SourcePoint.from_offsets(source_bytes, points)\n\n    start_sp = points_sp[0]\n    if end is not None:\n        end_sp = points_sp[1]\n    else:\n        end_sp = None\n    return (start_sp, end_sp)\n\n\ndef inflate_position(\n    source: str, span: tuple[int, Optional[int]]\n) -> tuple[int, int, int, Optional[int]]:\n    (start, end) = inflate_span(source, span)\n    return (\n        start.column,\n        start.line,\n        start.offset,\n        end.offset if end else None,\n    )\n\n\ndef line_col_to_source_point(\n    source: str,\n    line: int,  # zero-based\n    col: int,  # zero-based, in utf16 code points\n) -> ql_parser.SourcePoint:\n    points = ql_parser.SourcePoint.from_lines_cols(\n        source.encode('utf-8'), [(line, col)]\n    )\n    return points[0]\n\n\ndef _tokenize(eql: str) -> ql_parser.ParserResult:\n    result = ql_parser.tokenize(eql)\n\n    if len(result.errors) > 0:\n        # TODO: emit multiple errors\n        error = result.errors[0]\n\n        message, span, hint, details = error\n        position = inflate_position(eql, span)\n\n        hint = _derive_hint(eql, message, position) or hint\n        raise errors.EdgeQLSyntaxError(\n            message, position=position, hint=hint, details=details\n        )\n\n    return result\n\n\ndef _normalize(eql: str) -> ql_parser.Entry:\n    try:\n        return ql_parser.normalize(eql)\n    except ql_parser.SyntaxError as e:\n        message, span, hint, details = e.args\n        position = inflate_position(eql, span)\n\n        hint = _derive_hint(eql, message, position) or hint\n        raise errors.EdgeQLSyntaxError(\n            message, position=position, hint=hint, details=details\n        ) from e\n\n\ndef _derive_hint(\n    input: str,\n    message: str,\n    position: tuple[int, int, int, Optional[int]],\n) -> Optional[str]:\n    _, _, off, _ = position\n\n    if message.endswith(\n        r\"invalid string literal: invalid escape sequence '\\ '\"\n    ):\n        if TRAILING_WS_IN_CONTINUATION.search(input[off:]):\n            return \"consider removing trailing whitespace\"\n    return None\n"
  },
  {
    "path": "edb/edgeql/tracer.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2015-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n# Import specific things to avoid name clashes\nfrom typing import (Generator, Mapping, Optional,\n                    Iterable, TypeVar, Sequence,\n                    AbstractSet)\n\nimport functools\n\nfrom contextlib import contextmanager\nfrom edb import errors\nfrom edb.common import parsing\nfrom edb.schema import links as s_links\nfrom edb.schema import name as sn\nfrom edb.schema import objects as so\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import schema as s_schema\nfrom edb.schema import sources as s_sources\nfrom edb.schema import types as s_types\n\nfrom edb.edgeql import ast as qlast\n\n\nNamedObject_T = TypeVar(\"NamedObject_T\", bound=\"NamedObject\")\n\n\nclass NamedObject:\n    '''Generic tracing object with an explicit name.'''\n\n    def __init__(self, name: sn.QualName) -> None:\n        self.name = name\n\n    def get_name(self, schema: s_schema.Schema) -> sn.QualName:\n        return self.name\n\n    @classmethod\n    def get_schema_class_displayname(cls) -> str:\n        return cls.__name__.lower()\n\n\nSentinelObject = NamedObject(\n    name=sn.QualName(module='__unknown__', name='__unknown__'),\n)\n\n\nObjectLike = NamedObject | so.Object\n\n\nclass Function(NamedObject):\n    pass\n\n\nclass Constraint(NamedObject):\n    pass\n\n\nclass ConcreteConstraint(NamedObject):\n    pass\n\n\nclass Annotation(NamedObject):\n    pass\n\n\nclass AnnotationValue(NamedObject):\n    pass\n\n\nclass Global(NamedObject):\n    pass\n\n\nclass Permission(NamedObject):\n    pass\n\n\nclass Index(NamedObject):\n    pass\n\n\nclass ConcreteIndex(NamedObject):\n    pass\n\n\nclass Field(NamedObject):\n    pass\n\n\nclass Type(NamedObject):\n    def is_scalar(self) -> bool:\n        return False\n\n    def is_object_type(self) -> bool:\n        return False\n\n\nclass ScalarType(Type):\n    def is_scalar(self) -> bool:\n        return True\n\n\nTypeLike = Type | s_types.Type\n\n\nT = TypeVar('T')\n\n\nclass UnqualObjectIndex[T]:\n\n    def __init__(self, items: Mapping[sn.UnqualName, T]) -> None:\n        self._items = items\n\n    def items(\n        self,\n        schema: s_schema.Schema,\n    ) -> Iterable[tuple[sn.UnqualName, T]]:\n        return self._items.items()\n\n\nclass Source(NamedObject):\n\n    pointers: dict[sn.UnqualName, s_pointers.Pointer | Pointer]\n\n    '''Abstract type that mocks the s_sources.Source for tracing purposes.'''\n\n    def __init__(self, name: sn.QualName) -> None:\n        super().__init__(name)\n        self.pointers = {}\n\n    def maybe_get_ptr(\n        self,\n        schema: s_schema.Schema,\n        name: sn.UnqualName,\n    ) -> Optional[s_pointers.Pointer | Pointer]:\n        return self.pointers.get(name)\n\n    def getptr(\n        self,\n        schema: s_schema.Schema,\n        name: sn.UnqualName,\n    ) -> s_pointers.Pointer | Pointer:\n        ptr = self.maybe_get_ptr(schema, name)\n        if ptr is None:\n            raise AssertionError(f'{self.name} has no link or property {name}')\n        return ptr\n\n    def get_pointers(\n        self,\n        schema: s_schema.Schema,\n    ) -> UnqualObjectIndex[s_pointers.Pointer | Pointer]:\n        return UnqualObjectIndex(self.pointers)\n\n\nSource_T = TypeVar(\"Source_T\", bound=\"Source\")\nSourceLike = Source | s_sources.Source\nSourceLike_T = TypeVar(\"SourceLike_T\", bound=\"SourceLike\")\n\n\nclass ObjectType(Type, Source):\n\n    def is_pointer(self) -> bool:\n        return False\n\n    def is_scalar(self) -> bool:\n        return False\n\n    def is_object_type(self) -> bool:\n        return True\n\n\nclass Alias(ObjectType):\n    pass\n\n\nclass CompositeType(Type):\n\n    types: list[Type | CompositeType | s_types.Type]\n\n    def __init__(\n        self,\n        types: list[Type | CompositeType | s_types.Type],\n    ) -> None:\n        self.types = types\n\n\nclass UnionType(CompositeType):\n\n    def __init__(\n        self,\n        types: list[Type | CompositeType | s_types.Type],\n    ) -> None:\n        super().__init__(types)\n\n    def get_name(self, schema: s_schema.Schema) -> sn.QualName:\n        component_ids = sorted(str(t.get_name(schema)) for t in self.types)\n        nqname = f\"({' | '.join(component_ids)})\"\n        return sn.QualName(name=nqname, module='__derived__')\n\n    def is_object_type(self) -> bool:\n        return True\n\n\nclass IntersectionType(CompositeType):\n\n    def __init__(\n        self,\n        types: list[Type | CompositeType | s_types.Type],\n    ) -> None:\n        super().__init__(types)\n\n    def get_name(self, schema: s_schema.Schema) -> sn.QualName:\n        component_ids = sorted(str(t.get_name(schema)) for t in self.types)\n        nqname = f\"({' & '.join(component_ids)})\"\n        return sn.QualName(name=nqname, module='__derived__')\n\n    def is_object_type(self) -> bool:\n        return True\n\n\nclass Pointer(Source):\n\n    def __init__(\n        self,\n        name: sn.QualName,\n        *,\n        source: Optional[SourceLike] = None,\n        target: Optional[TypeLike] = None,\n        target_expr: Optional[qlast.Expr] = None,\n    ) -> None:\n        super().__init__(name)\n        self.source = source\n        self.target = target\n        self.target_expr = target_expr\n\n    def is_pointer(self) -> bool:\n        return True\n\n    def is_property(\n        self,\n        schema: s_schema.Schema,\n    ) -> bool:\n        raise NotImplementedError\n\n    def maybe_get_ptr(\n        self,\n        schema: s_schema.Schema,\n        name: sn.UnqualName,\n    ) -> Optional[s_pointers.Pointer | Pointer]:\n        if (not (res := super().maybe_get_ptr(schema, name))\n                and isinstance(self.target, (Source, s_sources.Source))):\n            res = self.target.maybe_get_ptr(schema, name)\n        return res\n\n    def get_target(\n        self,\n        schema: s_schema.Schema,\n    ) -> Optional[TypeLike]:\n        return self.target\n\n    def get_source(\n        self,\n        schema: s_schema.Schema,\n    ) -> Optional[SourceLike]:\n        return self.source\n\n\nclass Property(Pointer):\n    def is_property(\n        self,\n        schema: s_schema.Schema,\n    ) -> bool:\n        return True\n\n\nclass Link(Pointer):\n    def is_property(\n        self,\n        schema: s_schema.Schema,\n    ) -> bool:\n        return False\n\n\nclass UnknownPointer(Pointer):\n    def is_property(\n        self,\n        schema: s_schema.Schema,\n    ) -> bool:\n        return False\n\n    @classmethod\n    def get_schema_class_displayname(cls) -> str:\n        return 'link or property'\n\n\nclass AccessPolicy(NamedObject):\n\n    def __init__(\n        self,\n        name: sn.QualName,\n        *,\n        source: Optional[SourceLike] = None,\n    ) -> None:\n        super().__init__(name)\n        self.source = source\n\n    def get_source(\n        self,\n        schema: s_schema.Schema,\n    ) -> Optional[SourceLike]:\n        return self.source\n\n\nclass Trigger(NamedObject):\n\n    def __init__(\n        self,\n        name: sn.QualName,\n        *,\n        source: Optional[SourceLike] = None,\n    ) -> None:\n        super().__init__(name)\n        self.source = source\n\n    def get_source(\n        self,\n        schema: s_schema.Schema,\n    ) -> Optional[SourceLike]:\n        return self.source\n\n\nclass Rewrite(NamedObject):\n\n    def __init__(\n        self,\n        name: sn.QualName,\n        *,\n        source: Optional[SourceLike] = None,\n    ) -> None:\n        super().__init__(name)\n        self.source = source\n\n    def get_source(\n        self,\n        schema: s_schema.Schema,\n    ) -> Optional[SourceLike]:\n        return self.source\n\n\ndef qualify_name(name: sn.QualName, qual: str) -> sn.QualName:\n    return sn.QualName(name.module, f'{name.name}@{qual}')\n\n\ndef trace_refs(\n    qltree: qlast.Base,\n    *,\n    schema: s_schema.Schema,\n    anchors: Optional[Mapping[str, sn.QualName]] = None,\n    path_prefix: Optional[sn.QualName] = None,\n    module: str,\n    objects: dict[sn.QualName, Optional[ObjectLike]],\n    pointers: Mapping[sn.UnqualName, set[sn.QualName]],\n    params: Mapping[str, qlast.TypeExpr],\n    local_modules: AbstractSet[str]\n) -> tuple[frozenset[sn.QualName], frozenset[sn.QualName]]:\n\n    \"\"\"Return a list of schema item names used in an expression.\n\n    First set is strong deps, second is weak.\n    \"\"\"\n\n    ctx = TracerContext(\n        schema=schema,\n        module=module,\n        objects=objects,\n        pointers=pointers,\n        anchors=anchors or {},\n        path_prefix=path_prefix,\n        modaliases={},\n        params=params,\n        visited=set(),\n        local_modules=local_modules,\n    )\n    trace(qltree, ctx=ctx)\n    return frozenset(ctx.refs), frozenset(ctx.weak_refs)\n\n\ndef resolve_name(\n    ref: qlast.ObjectRef,\n    *,\n    current_module: str,\n    schema: s_schema.Schema,\n    objects: dict[sn.QualName, Optional[ObjectLike]],\n    modaliases: Optional[dict[Optional[str], str]],\n    local_modules: AbstractSet[str],\n    declaration: bool=False,\n) -> sn.QualName:\n    \"\"\"Resolve a name into a fully-qualified one.\n\n    This takes into account the current module and modaliases.\n\n    This function mostly mirrors schema.lookup\n    except:\n    - If no module and no default module was set, try the current module\n    - When searching in std, ensure module is not a local module\n    - If no result found, return a name with the best modname available\n    \"\"\"\n\n    def exists(name: sn.QualName) -> bool:\n        return (\n            objects.get(name) is not None\n            or schema.get(name, default=None, type=so.Object) is not None\n        )\n\n    module = ref.module\n    orig_module = module\n\n    # Apply module aliases\n    module = s_schema.apply_module_aliases(module, modaliases)\n    no_std = declaration\n\n    # Check if something matches the name\n    if module is not None:\n        fqname = sn.QualName(module=module, name=ref.name)\n        if exists(fqname):\n            return fqname\n\n    elif orig_module is None:\n        # Look for name in current module\n        fqname = sn.QualName(module=current_module, name=ref.name)\n        if exists(fqname):\n            return fqname\n\n    # Try something in std\n    if not no_std:\n        # If module == None, look in std\n        if orig_module is None:\n            mod_name = 'std'\n            fqname = sn.QualName(mod_name, ref.name)\n            if exists(fqname):\n                return fqname\n\n        # Ensure module is not a local module.\n        # Then try the module as part of std.\n        if module and module not in local_modules:\n            mod_name = f'std::{module}'\n            fqname = sn.QualName(mod_name, ref.name)\n            if exists(fqname):\n                return fqname\n\n    # Just pick the best module name available\n    return sn.QualName(\n        module=module or orig_module or current_module,\n        name=ref.name,\n    )\n\n\nclass TracerContext:\n    def __init__(\n        self,\n        *,\n        schema: s_schema.Schema,\n        module: str,\n        objects: dict[sn.QualName, Optional[ObjectLike]],\n        pointers: Mapping[sn.UnqualName, set[sn.QualName]],\n        anchors: Mapping[str, sn.QualName],\n        path_prefix: Optional[sn.QualName],\n        modaliases: dict[Optional[str], str],\n        params: Mapping[str, qlast.TypeExpr],\n        visited: set[s_pointers.Pointer | Pointer],\n        local_modules: AbstractSet[str],\n    ) -> None:\n        self.schema = schema\n        self.refs: set[sn.QualName] = set()\n        self.weak_refs: set[sn.QualName] = set()\n        self.module = module\n        self.objects = objects\n        self.pointers = pointers\n        self.anchors = anchors\n        self.path_prefix = path_prefix\n        self.modaliases = modaliases\n        self.params = params\n        self.local_modules = local_modules\n        self.visited = visited\n\n    def get_ref_name(self, ref: qlast.BaseObjectRef) -> sn.QualName:\n        # We don't actually expect to handle anything other than\n        # ObjectRef here.\n        assert isinstance(ref, qlast.ObjectRef)\n\n        return resolve_name(\n            ref,\n            current_module=self.module,\n            schema=self.schema,\n            objects=self.objects,\n            modaliases=self.modaliases,\n            local_modules=self.local_modules,\n        )\n\n    def get_ref_name_startswith(self, ref: qlast.ObjectRef) -> set[sn.QualName]:\n        refs = set()\n        prefixes = set()\n\n        if ref.module:\n            # replace the module alias with the real name\n            module = self.modaliases.get(ref.module, ref.module)\n            prefixes.add(f'{module}::{ref.name}')\n            prefixes.add(f'std::{module}::{ref.name}')\n        else:\n            prefixes.add(f'{self.module}::{ref.name}')\n            prefixes.add(f'std::{ref.name}')\n\n        for objname in self.objects.keys():\n            short_name = str(objname).split('@@', 1)[0]\n            if short_name in prefixes:\n                refs.add(objname)\n\n        return refs\n\n\ndef _fork_context(ctx: TracerContext) -> TracerContext:\n    nctx = TracerContext(\n        schema=ctx.schema,\n        module=ctx.module,\n        objects=dict(ctx.objects),\n        pointers=ctx.pointers,\n        anchors=ctx.anchors,\n        path_prefix=ctx.path_prefix,\n        modaliases=dict(ctx.modaliases),\n        params=ctx.params,\n        visited=ctx.visited,\n        local_modules=ctx.local_modules,\n    )\n    nctx.refs = ctx.refs\n    nctx.weak_refs = ctx.weak_refs\n\n    return nctx\n\n\n@contextmanager\ndef alias_context(\n    ctx: TracerContext,\n    aliases: Optional[\n        Sequence[qlast.Alias]],\n) -> Generator[TracerContext, None, None]:\n    ctx = _fork_context(ctx)\n\n    for alias in (aliases or ()):\n        # module and modalias in ctx needs to be amended\n        if isinstance(alias, qlast.ModuleAliasDecl):\n            if alias.alias:\n                ctx.modaliases[alias.alias] = alias.module\n            else:\n                # default module\n                ctx.module = alias.module\n\n        elif isinstance(alias, qlast.AliasedExpr):\n            obj = trace(alias.expr, ctx=ctx)\n            # Regardless of whether tracing the expression produces an\n            # object, record the alias.\n            ctx.objects[sn.QualName('__alias__', alias.alias)] = obj\n\n    try:\n        yield ctx\n    finally:\n        # refs are already updated\n        pass\n\n\n@contextmanager\ndef result_alias_context(\n    ctx: TracerContext,\n    node: qlast.ReturningQuery | qlast.SubjectQuery,\n    obj: Optional[ObjectLike],\n) -> Generator[TracerContext, None, None]:\n\n    alias: Optional[str] = None\n    if isinstance(node, qlast.SelectQuery):\n        alias = node.result_alias\n    elif isinstance(node, qlast.GroupQuery):\n        alias = node.subject_alias\n\n    # potentially SELECT uses an alias for the main result\n    if obj is not None and alias:\n        nctx = TracerContext(\n            schema=ctx.schema,\n            module=ctx.module,\n            objects=dict(ctx.objects),\n            pointers=ctx.pointers,\n            anchors=ctx.anchors,\n            path_prefix=ctx.path_prefix,\n            modaliases=ctx.modaliases,\n            params=ctx.params,\n            visited=ctx.visited,\n            local_modules=ctx.local_modules,\n        )\n        # use the same refs set\n        nctx.refs = ctx.refs\n        nctx.objects[sn.QualName('__alias__', alias)] = obj\n    else:\n        nctx = ctx\n\n    try:\n        yield nctx\n    finally:\n        # refs are already updated\n        pass\n\n\n@functools.singledispatch\ndef trace(\n    node: Optional[qlast.Base],\n    *,\n    ctx: TracerContext,\n) -> Optional[ObjectLike]:\n    raise NotImplementedError(f\"do not know how to trace {node!r}\")\n\n\n@trace.register\ndef trace_none(node: None, *, ctx: TracerContext) -> None:\n    pass\n\n\n@trace.register\ndef trace_Constant(node: qlast.BaseConstant, *, ctx: TracerContext) -> None:\n    pass\n\n\n@trace.register\ndef trace_QueryParameter(\n    node: qlast.QueryParameter, *, ctx: TracerContext\n) -> None:\n    raise errors.SchemaError(\n        'query parameters are not allowed in schemas',\n        span=node.span,\n    )\n\n\n@trace.register\ndef trace_FunctionParameter(\n    node: qlast.FunctionParameter, *, ctx: TracerContext\n) -> None:\n    raise AssertionError(\n        'function parameters are expected to be substituted for paths '\n        'in schemas',\n    )\n\n\n@trace.register\ndef trace_Array(node: qlast.Array, *, ctx: TracerContext) -> None:\n    for el in node.elements:\n        trace(el, ctx=ctx)\n\n\n@trace.register\ndef trace_StrInterpFragment(\n    node: qlast.StrInterpFragment, *, ctx: TracerContext\n) -> None:\n    trace(node.expr, ctx=ctx)\n\n\n@trace.register\ndef trace_StrInterp(node: qlast.StrInterp, *, ctx: TracerContext) -> None:\n    for el in node.interpolations:\n        trace(el, ctx=ctx)\n\n\n@trace.register\ndef trace_Set(node: qlast.Set, *, ctx: TracerContext) -> None:\n    for el in node.elements:\n        trace(el, ctx=ctx)\n\n\n@trace.register\ndef trace_Tuple(node: qlast.Tuple, *, ctx: TracerContext) -> None:\n    for el in node.elements:\n        trace(el, ctx=ctx)\n\n\n@trace.register\ndef trace_NamedTuple(node: qlast.NamedTuple, *, ctx: TracerContext) -> None:\n    for el in node.elements:\n        trace(el.val, ctx=ctx)\n\n\n@trace.register\ndef trace_BinOp(node: qlast.BinOp, *, ctx: TracerContext) -> None:\n    trace(node.left, ctx=ctx)\n    trace(node.right, ctx=ctx)\n\n\n@trace.register\ndef trace_UnaryOp(node: qlast.UnaryOp, *, ctx: TracerContext) -> None:\n    trace(node.operand, ctx=ctx)\n\n\n@trace.register\ndef trace_Detached(\n    node: qlast.DetachedExpr, *, ctx: TracerContext\n) -> Optional[ObjectLike]:\n    # DETACHED works with partial paths same as its inner expression.\n    return trace(node.expr, ctx=ctx)\n\n\n@trace.register\ndef trace_Global(\n    node: qlast.GlobalExpr, *, ctx: TracerContext\n) -> Optional[ObjectLike]:\n    refname = ctx.get_ref_name(node.name)\n    if refname in ctx.objects:\n        ctx.refs.add(refname)\n        tip = ctx.objects[refname]\n    else:\n        tip = ctx.schema.get(refname, span=node.span)\n    return tip\n\n\ndef check_type_exists(\n    typename: sn.QualName,\n    ctx: TracerContext,\n    span: Optional[parsing.Span],\n    *,\n    hint: Optional[str] = None,\n) -> None:\n    if typename in ctx.objects:\n        return\n\n    try:\n        # Check if the typename is already in the schema\n        ctx.schema.get(typename, type=s_types.Type, span=span)\n    except errors.InvalidReferenceError as e:\n        if hint and not e.hint:\n            e.set_hint_and_details(hint, e.details)\n        raise e\n\n\n@trace.register\ndef trace_TypeCast(node: qlast.TypeCast, *, ctx: TracerContext) -> None:\n    trace(node.expr, ctx=ctx)\n    if isinstance(node.type, qlast.TypeName):\n        if not node.type.subtypes:\n            typename: sn.QualName = ctx.get_ref_name(node.type.maintype)\n            check_type_exists(typename, ctx, node.type.span)\n            ctx.refs.add(typename)\n\n\n@trace.register\ndef trace_IsOp(node: qlast.IsOp, *, ctx: TracerContext) -> None:\n    trace(node.left, ctx=ctx)\n    if isinstance(node.right, qlast.TypeName):\n        if not node.right.subtypes:\n            typename: sn.QualName = ctx.get_ref_name(node.right.maintype)\n\n            hint: Optional[str] = None\n            if typename.name.lower() in ['null', 'none']:\n                hint = (\n                    'Did you mean to use `exists` to check if a set is empty?'\n                )\n            check_type_exists(typename, ctx, node.right.span, hint=hint)\n\n            ctx.refs.add(typename)\n\n\n@trace.register\ndef trace_Introspect(node: qlast.Introspect, *, ctx: TracerContext) -> None:\n    if isinstance(node.type, qlast.TypeName):\n        if not node.type.subtypes:\n            typename: sn.QualName = ctx.get_ref_name(node.type.maintype)\n            check_type_exists(typename, ctx, node.type.span)\n            ctx.refs.add(typename)\n\n\n@trace.register\ndef trace_FunctionCall(node: qlast.FunctionCall, *, ctx: TracerContext) -> None:\n\n    if isinstance(node.func, tuple):\n        fname = qlast.ObjectRef(module=node.func[0], name=node.func[1])\n    else:\n        fname = qlast.ObjectRef(name=node.func)\n    # The function call is dependent on the function actually being\n    # present, so we add all variations of that function name to the\n    # dependency list.\n\n    names = ctx.get_ref_name_startswith(fname)\n    ctx.refs.update(names)\n\n    for arg in node.args:\n        trace(arg, ctx=ctx)\n    for arg in node.kwargs.values():\n        trace(arg, ctx=ctx)\n\n\n@trace.register\ndef trace_Indirection(node: qlast.Indirection, *, ctx: TracerContext) -> None:\n    for indirection in node.indirection:\n        trace(indirection, ctx=ctx)\n    trace(node.arg, ctx=ctx)\n\n\n@trace.register\ndef trace_Index(node: qlast.Index, *, ctx: TracerContext) -> None:\n    trace(node.index, ctx=ctx)\n\n\n@trace.register\ndef trace_Slice(node: qlast.Slice, *, ctx: TracerContext) -> None:\n    trace(node.start, ctx=ctx)\n    trace(node.stop, ctx=ctx)\n\n\n@trace.register\ndef trace_Path(\n    node: qlast.Path,\n    *,\n    ctx: TracerContext,\n) -> Optional[ObjectLike]:\n    tip: Optional[ObjectLike] = None\n    ptr: Optional[Pointer | s_pointers.Pointer] = None\n    plen = len(node.steps)\n\n    # HACK: This isn't very smart, and can't properly track types\n    # through arbitrary expressions. To try to mitigate the damage\n    # from this, when we have a pointer step but don't know the type,\n    # we track *weak* references to all pointers with that name.\n    # This won't always work (if there is a tangle of cyclic weak deps),\n    # but it works pretty well.\n\n    for i, step in enumerate(node.steps):\n        if isinstance(step, qlast.ObjectRef):\n            # the ObjectRef without a module may be referring to an\n            # aliased expression\n            aname = sn.QualName('__alias__', step.name)\n            if not step.module and aname in ctx.objects:\n                tip = ctx.objects[aname]\n\n            elif not step.module and step.name in ctx.params:\n                param_type = ctx.params[step.name]\n                if (\n                    isinstance(param_type, qlast.TypeName)\n                    and isinstance(param_type.maintype, qlast.PseudoObjectRef)\n                ):\n                    # Pretend pseudotypes (eg. `anytype`) have a fully\n                    # qualified name.\n                    refname = sn.QualName('std', param_type.maintype.name)\n                    ctx.refs.add(refname)\n\n                    tip = ctx.objects[refname]\n\n                else:\n                    tip = _resolve_type_expr(param_type, ctx=ctx)\n\n            else:\n                refname = ctx.get_ref_name(step)\n                if refname in ctx.objects:\n                    ctx.refs.add(refname)\n                    tip = ctx.objects[refname]\n                else:\n                    tip = ctx.schema.get(refname, span=step.span)\n\n        elif isinstance(step, qlast.Ptr):\n            pname = sn.UnqualName(step.name)\n\n            if i == 0:\n                # Abbreviated path.\n                if ctx.path_prefix in ctx.objects:\n                    tip = ctx.objects[ctx.path_prefix]\n                    if isinstance(tip, Pointer):\n                        ptr = tip\n                else:\n                    # We can't reason about this path.\n                    # Do a weak dependency on anything with the same name.\n                    ctx.weak_refs.update(ctx.pointers.get(pname, ()))\n\n            if step.type == 'property':\n                if ptr is None:\n                    # This is either a computable def  or unknown link, bail.\n                    # Do a weak dependency on anything with the same name.\n                    ctx.weak_refs.update(ctx.pointers.get(pname, ()))\n                    tip = None\n\n                elif isinstance(ptr, (s_links.Link, Pointer)):\n                    lprop = ptr.maybe_get_ptr(\n                        ctx.schema,\n                        pname,\n                    )\n                    if lprop is None:\n                        # Invalid link property reference, bail.\n                        return None\n\n                    if (isinstance(lprop, Pointer) and\n                            lprop.source is not None):\n                        src = lprop.source\n                        src_name = src.get_name(ctx.schema)\n                        if (isinstance(src, Pointer) and\n                                src.source is not None):\n                            src_src_name = src.source.get_name(ctx.schema)\n                            source_name = qualify_name(\n                                src_src_name, src_name.name)\n                        else:\n                            source_name = src_name\n                        ctx.refs.add(qualify_name(source_name, step.name))\n            else:\n                if step.direction == '<':\n                    if plen > i + 1 and isinstance(node.steps[i + 1],\n                                                   qlast.TypeIntersection):\n                        # A reverse link traversal with a type intersection,\n                        # process it on the next step.\n                        pass\n                    else:\n                        # No type intersection, so the only type that\n                        # it can be is \"Object\", which is trivial.\n                        # However, we need to make it dependent on\n                        # every link of the same name now.\n                        for fqname in ctx.pointers.get(pname, ()):\n                            obj = ctx.objects.get(fqname)\n\n                            # Ignore what appears to not be a link\n                            # with the right name.\n                            if (isinstance(obj, (s_pointers.Pointer,\n                                                 Pointer)) and\n                                fqname.name.split('@', 1)[1] ==\n                                    step.name):\n\n                                target = obj.get_target(ctx.schema)\n                                # Ignore scalars, but include other\n                                # computables to produce better error\n                                # messages.\n                                if (target is None or\n                                        not target.is_scalar()):\n                                    # Record link with matching short\n                                    # name.\n                                    ctx.refs.add(fqname)\n\n                        tip = ptr = None\n                else:\n                    if isinstance(tip, (Source, s_sources.Source)):\n                        ptr = tip.maybe_get_ptr(\n                            ctx.schema, sn.UnqualName(step.name)\n                        )\n                        if ptr is None:\n                            # Invalid pointer reference, bail.\n                            return None\n                        else:\n                            ptr_source = ptr.get_source(ctx.schema)\n\n                        if ptr_source is not None:\n                            sname = ptr_source.get_name(ctx.schema)\n                            assert isinstance(sname, sn.QualName)\n                            ctx.refs.add(qualify_name(sname, step.name))\n                            tip = ptr.get_target(ctx.schema)\n\n                            if tip is None:\n                                if ptr in ctx.visited:\n                                    # Possibly recursive definition, bail out.\n                                    return None\n\n                                # This can only be Pointer that didn't\n                                # infer the target type yet.\n                                assert isinstance(ptr, Pointer)\n                                # We haven't computed the target yet,\n                                # so try computing it now.\n                                ctx.visited.add(ptr)\n\n                                target_ctx = _fork_context(ctx)\n                                target_ctx.path_prefix = sname\n                                ptr_target = trace(\n                                    ptr.target_expr, ctx=target_ctx\n                                )\n\n                                if isinstance(ptr_target, (Type,\n                                                           s_types.Type)):\n                                    tip = ptr.target = ptr_target\n\n                        else:\n                            # Can't figure out the new tip, so we bail.\n                            return None\n\n                    else:\n                        # We can't reason about this path.\n                        # Do a weak dependency on anything with the same name.\n                        ctx.weak_refs.update(ctx.pointers.get(pname, ()))\n                        tip = ptr = None\n\n        elif isinstance(step, qlast.TypeIntersection):\n            # This tip is determined from the type in the type\n            # intersection, which is valid in the general case, but\n            # there's a special case that needs to be potentially\n            # handled for backward links.\n            tip = _resolve_type_expr(step.type, ctx=ctx)\n            prev_step = node.steps[i - 1]\n            if isinstance(prev_step, qlast.Ptr):\n                if prev_step.direction == '<':\n                    if isinstance(tip, (s_sources.Source, ObjectType)):\n                        ptr = tip.maybe_get_ptr(\n                            ctx.schema, sn.UnqualName(prev_step.name)\n                        )\n                        if ptr is None:\n                            # Invalid pointer reference, bail.\n                            return None\n\n                        if isinstance(tip, Type):\n                            tip_name = tip.get_name(ctx.schema)\n                            ctx.refs.add(qualify_name(tip_name, prev_step.name))\n\n        elif isinstance(step, qlast.Splat):\n            if step.type is not None:\n                _resolve_type_expr(step.type, ctx=ctx)\n            if step.intersection is not None:\n                _resolve_type_expr(step.intersection.type, ctx=ctx)\n\n        else:\n            tr = trace(step, ctx=ctx)\n            tip = ptr = None\n            if tr is not None:\n                tip = tr\n                if isinstance(tip, Pointer):\n                    ptr = tip\n\n    return tip\n\n\n@trace.register\ndef trace_Anchor(\n    node: qlast.Anchor, *, ctx: TracerContext\n) -> Optional[ObjectLike]:\n    if name := ctx.anchors.get(node.name):\n        return ctx.objects[name]\n    return None\n\n\ndef _resolve_type_expr(\n    texpr: qlast.TypeExpr,\n    *,\n    ctx: TracerContext,\n) -> TypeLike:\n\n    if isinstance(texpr, qlast.TypeName):\n        if texpr.subtypes and isinstance(texpr.maintype, qlast.ObjectRef):\n            return Type(\n                name=sn.QualName(\n                    module='__coll__',\n                    name=texpr.maintype.name,\n                ),\n            )\n        else:\n            refname = ctx.get_ref_name(texpr.maintype)\n            local_obj = ctx.objects.get(refname)\n            obj: TypeLike\n            if local_obj is None:\n                obj = ctx.schema.get(\n                    refname, type=s_types.Type, span=texpr.span)\n            else:\n                assert isinstance(local_obj, Type)\n                obj = local_obj\n                ctx.refs.add(refname)\n\n            return obj\n\n    elif isinstance(texpr, qlast.TypeOp):\n\n        left = _resolve_type_expr(texpr.left, ctx=ctx)\n        right = _resolve_type_expr(texpr.right, ctx=ctx)\n\n        ThisCompositeType: type[CompositeType] = (\n            UnionType\n            if texpr.op == qlast.TypeOpName.OR else\n            IntersectionType\n        )\n\n        if isinstance(left, ThisCompositeType):\n            if isinstance(right, ThisCompositeType):\n                return ThisCompositeType(left.types + right.types)\n            else:\n                return ThisCompositeType(left.types + [right])\n        else:\n            if isinstance(right, ThisCompositeType):\n                return ThisCompositeType([left] + right.types)\n            else:\n                return ThisCompositeType([left, right])\n\n    else:\n        raise NotImplementedError(\n            f'unsupported type expression: {texpr!r}'\n        )\n\n\n@trace.register\ndef trace_TypeIntersection(\n    node: qlast.TypeIntersection, *, ctx: TracerContext\n) -> None:\n    trace(node.type, ctx=ctx)\n\n\n@trace.register\ndef trace_TypeOf(node: qlast.TypeOf, *, ctx: TracerContext) -> None:\n    trace(node.expr, ctx=ctx)\n\n\n@trace.register\ndef trace_TypeName(node: qlast.TypeName, *, ctx: TracerContext) -> None:\n    if node.subtypes:\n        for st in node.subtypes:\n            trace(st, ctx=ctx)\n    elif isinstance(node.maintype, qlast.ObjectRef):\n        tref = node.maintype\n        if tref.module:\n            fq_name = sn.QualName(module=tref.module, name=tref.name)\n        else:\n            fq_name = sn.QualName(module=ctx.module, name=tref.name)\n            if fq_name not in ctx.objects:\n                std_name = sn.QualName(module=\"std\", name=tref.name)\n                if ctx.schema.get(std_name, default=None) is not None:\n                    fq_name = std_name\n        ctx.refs.add(fq_name)\n\n\n@trace.register\ndef trace_TypeOp(node: qlast.TypeOp, *, ctx: TracerContext) -> None:\n    trace(node.left, ctx=ctx)\n    trace(node.right, ctx=ctx)\n\n\n@trace.register\ndef trace_IfElse(node: qlast.IfElse, *, ctx: TracerContext) -> None:\n    trace(node.if_expr, ctx=ctx)\n    trace(node.else_expr, ctx=ctx)\n    trace(node.condition, ctx=ctx)\n\n\n@trace.register\ndef trace_Shape(\n    node: qlast.Shape, *, ctx: TracerContext\n) -> Optional[ObjectLike]:\n    tip = trace(node.expr, ctx=ctx)\n    if isinstance(node.expr, qlast.Path):\n        orig_prefix = ctx.path_prefix\n        if tip is not None:\n            tip_name = tip.get_name(ctx.schema)\n            assert isinstance(tip_name, sn.QualName)\n            ctx.path_prefix = tip_name\n        else:\n            ctx.path_prefix = None\n\n    for element in node.elements:\n        trace(element, ctx=ctx)\n\n    if isinstance(node.expr, qlast.Path):\n        ctx.path_prefix = orig_prefix\n\n    return tip\n\n\n@trace.register\ndef trace_ShapeElement(node: qlast.ShapeElement, *, ctx: TracerContext) -> None:\n    trace(node.expr, ctx=ctx)\n    if node.elements:\n        for element in node.elements:\n            trace(element, ctx=ctx)\n    trace(node.where, ctx=ctx)\n    if node.orderby:\n        for sortexpr in node.orderby:\n            trace(sortexpr, ctx=ctx)\n    trace(node.offset, ctx=ctx)\n    trace(node.limit, ctx=ctx)\n    trace(node.compexpr, ctx=ctx)\n\n\ndef _update_path_prefix(tip: Optional[ObjectLike], ctx: TracerContext) -> None:\n    if tip is not None:\n        tip_name = tip.get_name(ctx.schema)\n        assert isinstance(tip_name, sn.QualName)\n        ctx.path_prefix = tip_name\n    else:\n        ctx.path_prefix = None\n\n\n@trace.register\ndef trace_Select(\n    node: qlast.SelectQuery, *, ctx: TracerContext\n) -> Optional[ObjectLike]:\n    with alias_context(ctx, node.aliases) as ctx:\n        tip = trace(node.result, ctx=ctx)\n        _update_path_prefix(tip, ctx=ctx)\n\n        # potentially SELECT uses an alias for the main result\n        with result_alias_context(ctx, node, tip) as nctx:\n            if node.where is not None:\n                trace(node.where, ctx=nctx)\n            if node.orderby:\n                for expr in node.orderby:\n                    trace(expr, ctx=nctx)\n            if node.offset is not None:\n                trace(node.offset, ctx=nctx)\n            if node.limit is not None:\n                trace(node.limit, ctx=nctx)\n\n        return tip\n\n\ndef trace_GroupingAtom(node: qlast.GroupingAtom, *, ctx: TracerContext) -> None:\n    if isinstance(node, qlast.ObjectRef):\n        trace(qlast.Path(steps=[node]), ctx=ctx)\n    elif isinstance(node, qlast.Path):\n        trace(node, ctx=ctx)\n    else:\n        assert isinstance(node, qlast.GroupingIdentList)\n        for el in node.elements:\n            trace_GroupingAtom(el, ctx=ctx)\n\n\n@trace.register\ndef trace_GroupingSimple(\n    node: qlast.GroupingSimple, *, ctx: TracerContext\n) -> None:\n    trace_GroupingAtom(node.element, ctx=ctx)\n\n\n@trace.register\ndef trace_GroupingSets(node: qlast.GroupingSets, *, ctx: TracerContext) -> None:\n    for s in node.sets:\n        trace(s, ctx=ctx)\n\n\n@trace.register\ndef trace_GroupingOperation(\n    node: qlast.GroupingOperation, *, ctx: TracerContext\n) -> None:\n    for s in node.elements:\n        trace(s, ctx=ctx)\n\n\n@trace.register\ndef trace_Group(\n    node: qlast.GroupQuery, *, ctx: TracerContext\n) -> Optional[ObjectLike]:\n    return _trace_GroupQuery(node, ctx=ctx)\n\n\n@trace.register\ndef trace_InternalGroupQuery(\n    node: qlast.InternalGroupQuery, *, ctx: TracerContext\n) -> Optional[ObjectLike]:\n    return _trace_GroupQuery(node, ctx=ctx)\n\n\ndef _trace_GroupQuery(\n    node: qlast.GroupQuery | qlast.InternalGroupQuery, *, ctx: TracerContext\n) -> Optional[ObjectLike]:\n    with alias_context(ctx, node.aliases) as ctx:\n        tip = trace(node.subject, ctx=ctx)\n        if tip is not None:\n            tip_name = tip.get_name(ctx.schema)\n            assert isinstance(tip_name, sn.QualName)\n            ctx.path_prefix = tip_name\n\n        # potentially GROUP uses an alias for the main result\n        with result_alias_context(ctx, node, tip) as nctx:\n            with alias_context(nctx, node.using) as byctx:\n                for by_el in node.by:\n                    trace(by_el, ctx=byctx)\n\n        if isinstance(node, qlast.InternalGroupQuery):\n            with alias_context(nctx, node.using) as byctx:\n                ctx.objects[sn.QualName('__alias__', node.group_alias)] = (\n                    SentinelObject)\n                if node.grouping_alias:\n                    ctx.objects[\n                        sn.QualName('__alias__', node.grouping_alias)] = (\n                            SentinelObject)\n                trace(node.result, ctx=byctx)\n\n        return tip\n\n\n@trace.register\ndef trace_SortExpr(node: qlast.SortExpr, *, ctx: TracerContext) -> None:\n    trace(node.path, ctx=ctx)\n\n\n@trace.register\ndef trace_InsertQuery(node: qlast.InsertQuery, *, ctx: TracerContext) -> None:\n    with alias_context(ctx, node.aliases) as ctx:\n        if node.unless_conflict:\n            trace(node.unless_conflict[0], ctx=ctx)\n            trace(node.unless_conflict[1], ctx=ctx)\n\n        tip = trace(qlast.Path(steps=[node.subject]), ctx=ctx)\n        _update_path_prefix(tip, ctx=ctx)\n\n        for element in node.shape:\n            trace(element, ctx=ctx)\n\n\n@trace.register\ndef trace_UpdateQuery(\n    node: qlast.UpdateQuery, *, ctx: TracerContext\n) -> Optional[ObjectLike]:\n    with alias_context(ctx, node.aliases) as ctx:\n        tip = trace(node.subject, ctx=ctx)\n        _update_path_prefix(tip, ctx=ctx)\n\n        # potentially UPDATE uses an alias for the main result\n        with result_alias_context(ctx, node, tip) as nctx:\n            for element in node.shape:\n                trace(element, ctx=nctx)\n\n            trace(node.where, ctx=nctx)\n\n        return tip\n\n\n@trace.register\ndef trace_DeleteQuery(\n    node: qlast.DeleteQuery, *, ctx: TracerContext\n) -> Optional[ObjectLike]:\n    with alias_context(ctx, node.aliases) as ctx:\n        tip = trace(node.subject, ctx=ctx)\n        _update_path_prefix(tip, ctx=ctx)\n\n        # potentially DELETE uses an alias for the main result\n        with result_alias_context(ctx, node, tip) as nctx:\n            if node.where is not None:\n                trace(node.where, ctx=nctx)\n            if node.orderby:\n                for expr in node.orderby:\n                    trace(expr, ctx=nctx)\n            if node.offset is not None:\n                trace(node.offset, ctx=nctx)\n            if node.limit is not None:\n                trace(node.limit, ctx=nctx)\n\n        return tip\n\n\n@trace.register\ndef trace_For(\n    node: qlast.ForQuery, *, ctx: TracerContext\n) -> Optional[ObjectLike]:\n    with alias_context(ctx, node.aliases) as ctx:\n        obj = trace(node.iterator, ctx=ctx)\n        if obj is None:\n            obj = SentinelObject\n        ctx.objects[sn.QualName('__alias__', node.iterator_alias)] = obj\n        tip = trace(node.result, ctx=ctx)\n\n        return tip\n\n\n@trace.register\ndef trace_DescribeStmt(\n    node: qlast.DescribeStmt,\n    *,\n    ctx: TracerContext,\n) -> None:\n\n    if isinstance(node.object, qlast.ObjectRef):\n        fq_name = ctx.get_ref_name(node.object)\n        ctx.refs.add(fq_name)\n\n\n@trace.register\ndef trace_ExplainStmt(\n    node: qlast.ExplainStmt,\n    *,\n    ctx: TracerContext,\n) -> None:\n    pass\n\n\n@trace.register\ndef trace_AdministerStmt(\n    node: qlast.AdministerStmt,\n    *,\n    ctx: TracerContext,\n) -> None:\n    pass\n\n\n@trace.register\ndef trace_Placeholder(\n    node: qlast.Placeholder,\n    *,\n    ctx: TracerContext,\n) -> None:\n    pass\n"
  },
  {
    "path": "edb/edgeql/utils.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2015-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport copy\nimport itertools\nfrom typing import Any, Optional, Mapping\n\nfrom edb import errors\nfrom edb.common import ast\nfrom edb.schema import schema as s_schema\nfrom edb.schema import functions as s_func\n\nfrom . import ast as qlast\n\n\nFREE_SHAPE_EXPR = qlast.DetachedExpr(\n    expr=qlast.Path(\n        steps=[qlast.ObjectRef(module='std', name='FreeObject')],\n        allow_factoring=True,\n    ),\n)\n\n\nclass ParameterInliner(ast.NodeTransformer):\n\n    def __init__(self, args_map: Mapping[str, qlast.Base]) -> None:\n        super().__init__()\n        self.args_map = args_map\n\n    def visit_Path(self, node: qlast.Path) -> qlast.Base:\n        if len(node.steps) != 1 or not isinstance(\n            node.steps[0], qlast.ObjectRef\n        ):\n            self.visit(node.steps[0])\n            return node\n\n        ref: qlast.ObjectRef = node.steps[0]\n        try:\n            arg = self.args_map[ref.name]\n        except KeyError:\n            return node\n\n        arg = copy.deepcopy(arg)\n        return arg\n\n\ndef inline_parameters(\n    ql_expr: qlast.Base, args: Mapping[str, qlast.Base]\n) -> None:\n\n    inliner = ParameterInliner(args)\n    inliner.visit(ql_expr)\n\n\ndef index_parameters(\n    ql_args: list[qlast.Base],\n    *,\n    parameters: s_func.ParameterLikeList,\n    schema: s_schema.Schema\n) -> dict[str, qlast.Base]:\n\n    result: dict[str, qlast.Base] = {}\n    varargs: Optional[list[qlast.Expr]] = None\n    variadic = parameters.find_variadic(schema)\n    variadic_num = variadic.get_num(schema) if variadic else -1  # type: ignore\n\n    params = parameters.objects(schema)\n\n    if not variadic and len(ql_args) > len(params):\n        # In error message we discount the implicit __subject__ param.\n        raise errors.SchemaDefinitionError(\n            f'Expected {len(params) - 1} arguments, but found '\n            f'{len(ql_args) - 1}',\n            span=ql_args[-1].span,\n            details='Did you mean to use ON (...) for specifying the subject?',\n        )\n\n    e: qlast.Expr\n    p: s_func.ParameterLike\n    for iter in itertools.zip_longest(\n        enumerate(ql_args), params, fillvalue=None\n    ):\n        (i, e), p = iter  # type: ignore\n        if isinstance(e, qlast.SelectQuery):\n            e = e.result\n\n        if variadic and variadic_num == i:\n            assert varargs is None\n            varargs = []\n            result[p.get_parameter_name(schema)] = qlast.Array(\n                elements=varargs\n            )\n\n        if varargs is not None:\n            varargs.append(e)\n        else:\n            result[p.get_parameter_name(schema)] = e\n\n    return result\n\n\nclass AnchorInliner(ast.NodeTransformer):\n\n    def __init__(self, anchors: Mapping[str, qlast.Base]) -> None:\n        super().__init__()\n        self.anchors = anchors\n\n    def visit_Path(self, node: qlast.Path) -> qlast.Path:\n        if not node.steps:\n            return node\n\n        step0 = node.steps[0]\n\n        if isinstance(step0, qlast.Anchor):\n            node.steps[0] = self.anchors[step0.name]  # type: ignore\n        elif isinstance(step0, qlast.ObjectRef) and step0.name in self.anchors:\n            node.steps[0] = self.anchors[step0.name]  # type: ignore\n\n        return node\n\n\ndef inline_anchors(\n    ql_expr: qlast.Base, anchors: Mapping[Any, qlast.Base]\n) -> None:\n\n    inliner = AnchorInliner(anchors)\n    inliner.visit(ql_expr)\n\n\ndef find_paths(ql: qlast.Base) -> list[qlast.Path]:\n    return ast.find_children(ql, qlast.Path)\n\n\ndef find_subject_ptrs(ast: qlast.Base) -> set[str]:\n    ptrs = set()\n    for path in find_paths(ast):\n        if path.partial:\n            p = path.steps[0]\n        elif is_anchor(path.steps[0], '__subject__') and len(path.steps) > 1:\n            p = path.steps[1]\n        else:\n            continue\n\n        if isinstance(p, qlast.Ptr):\n            ptrs.add(p.name)\n    return ptrs\n\n\ndef is_anchor(expr: qlast.PathElement, name: str) -> bool:\n    return isinstance(expr, qlast.Anchor) and expr.name == name\n\n\ndef subject_paths_substitute(\n    ast: qlast.Base_T,\n    subject_ptrs: dict[str, qlast.Expr],\n) -> qlast.Base_T:\n    ast = copy.deepcopy(ast)\n    for path in find_paths(ast):\n        if path.partial and isinstance(path.steps[0], qlast.Ptr):\n            path.steps[0] = subject_paths_substitute(\n                subject_ptrs[path.steps[0].name],\n                subject_ptrs,\n            )\n        elif (\n            is_anchor(path.steps[0], '__subject__')\n            and len(path.steps)\n            and isinstance(path.steps[1], qlast.Ptr)\n        ):\n            path.steps[0:2] = [subject_paths_substitute(\n                subject_ptrs[path.steps[1].name],\n                subject_ptrs,\n            )]\n    return ast\n\n\ndef subject_substitute(\n    ast: qlast.Base_T, new_subject: qlast.Expr\n) -> qlast.Base_T:\n    ast = copy.deepcopy(ast)\n    # If the subject is a path (usually will be), graft the path\n    # elements directly to avoid an extra SelectStmt/Set in the IR,\n    # which can result in worse codegen (unnecessary semijoins, for\n    # example).\n    # TODO: Unify other substitution functions.\n    if isinstance(new_subject, qlast.Path):\n        new_partial = new_subject.partial\n        new_head = new_subject.steps\n    else:\n        new_partial = False\n        new_head = [new_subject]\n\n    for path in find_paths(ast):\n        if is_anchor(path.steps[0], '__subject__'):\n            path.steps[0:1] = new_head\n            path.partial = new_partial\n        elif path.partial:\n            path.steps[0:0] = new_head\n            path.partial = new_partial\n    return ast\n\n\ndef is_enum(type_name: qlast.TypeName):\n    return (\n        isinstance(type_name.maintype, (qlast.TypeName, qlast.ObjectRef))\n        and type_name.maintype.name == \"enum\"\n        and type_name.subtypes\n    )\n"
  },
  {
    "path": "edb/edgeql-parser/Cargo.toml",
    "content": "[package]\nname = \"edgeql-parser\"\nversion = \"0.1.0\"\nlicense = \"MIT/Apache-2.0\"\nauthors = [\"MagicStack Inc. <hello@magic.io>\"]\nedition = \"2021\"\n\n[lints]\nworkspace = true\n\n[dependencies]\npyo3 = { workspace = true, optional = true }\n\nbase32 = \"0.5.1\"\nbigdecimal = { version = \"0.4.5\", features = [\"serde\"] }\nnum-bigint = { version = \"0.4.6\", features = [\"serde\"] }\nsha2 = \"0.10.2\"\nsnafu = \"0.8.1\"\nmemchr = \"2.5.0\"\nserde = { version = \"1.0.106\", features = [\"derive\"], optional = true }\nthiserror = \"2\"\nunicode-width = \"0.1.8\"\nedgeql-parser-derive = { path = \"edgeql-parser-derive\", optional = true }\nindexmap = \"2.4.0\"\nserde_json = { version = \"1.0\", features = [\"preserve_order\"] }\nbumpalo = { version = \"3.13.0\", features = [\"collections\"] }\nphf = { version = \"0.11.1\", features = [\"macros\"] }\nappend-only-vec = \"0.1.2\"\n\n[features]\ndefault = []\npython = [\"pyo3\", \"serde\", \"edgeql-parser-derive\"]\n\n[lib]\n"
  },
  {
    "path": "edb/edgeql-parser/edgeql-parser-derive/Cargo.toml",
    "content": "[package]\nname = \"edgeql-parser-derive\"\ndescription = \"Derive macros for IntoPython trait for AST\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lints]\nworkspace = true\n\n[lib]\nproc-macro = true\n\n[dependencies]\nsyn = { version = \"2.0.76\" }\nquote = \"1.0.37\"\nproc-macro2 = \"1.0\"\n"
  },
  {
    "path": "edb/edgeql-parser/edgeql-parser-derive/src/lib.rs",
    "content": "use proc_macro::TokenStream;\n\nuse syn::{parse_macro_input, Attribute, Type, TypePath};\n\nuse quote::quote;\nuse syn::{self, Fields, Ident};\n\n#[proc_macro_derive(IntoPython, attributes(py_child, py_enum, py_union))]\npub fn into_python(input: TokenStream) -> TokenStream {\n    use syn::Item;\n    let mut item = parse_macro_input!(input as Item);\n    match &mut item {\n        Item::Enum(enum_) => impl_enum_into_python(enum_),\n        Item::Struct(struct_) => impl_struct_into_python(struct_),\n        unsupported => {\n            syn::Error::new_spanned(unsupported, \"IntoPython only supports structs and enums\")\n                .into_compile_error()\n                .into()\n        }\n    }\n}\n\nfn impl_enum_into_python(enum_: &mut syn::ItemEnum) -> TokenStream {\n    let variants = infer_variants(enum_);\n\n    let name = &enum_.ident;\n    let mut cases = Vec::new();\n\n    if let Some(py_enum) = find_attr(&enum_.attrs, \"py_enum\") {\n        let class_path = py_enum.meta.path();\n\n        for Variant { name } in variants {\n            cases.push(quote! {\n                Self::#name => py.eval(#class_path.#name, None, None),\n            });\n        }\n    } else if find_attr(&enum_.attrs, \"py_child\").is_some() {\n        for Variant { name } in variants {\n            cases.push(quote! {\n                Self::#name(value) => value.into_python(py, parent),\n            });\n        }\n    } else if find_attr(&enum_.attrs, \"py_union\").is_some() {\n        for Variant { name } in variants {\n            cases.push(quote! {\n                Self::#name(value) => value.into_python(py, None),\n            });\n        }\n    } else {\n        panic!(\"enum is missing one of #[py_enum], #[py_child] or #[py_union]\")\n    }\n\n    quote! {\n        impl crate::into_python::IntoPython for #name {\n            fn into_python(\n                self,\n                py: cpython::Python,\n                parent: Option<cpython::PyDict>,\n            ) -> cpython::PyResult<cpython::PyObject> {\n                use crate::into_python::IntoPython;\n\n                match self { #(#cases)* }\n            }\n        }\n    }\n    .into()\n}\n\nfn infer_variants(enum_: &syn::ItemEnum) -> Vec<Variant> {\n    let mut variants = Vec::new();\n\n    for variant in &enum_.variants {\n        let name = variant.ident.clone();\n\n        match &variant.fields {\n            Fields::Named(_) => panic!(\"IntoPython does not support named enum variant fields\"),\n            Fields::Unnamed(fields) => {\n                if fields.unnamed.len() != 1 {\n                    panic!(\"IntoPython supports only enum variant fields with zero or one fields\")\n                }\n\n                variants.push(Variant { name });\n            }\n            Fields::Unit => {\n                variants.push(Variant { name });\n            }\n        }\n    }\n\n    variants\n}\n\n/// Information about the struct annotated with IntoPython\nstruct Variant {\n    name: Ident,\n}\n\nfn impl_struct_into_python(struct_: &mut syn::ItemStruct) -> TokenStream {\n    let (properties, py_child_field) = infer_fields(struct_);\n\n    let name = &struct_.ident;\n\n    let mut property_assigns = Vec::new();\n    for property in properties {\n        property_assigns.push(quote! {\n            kw_args.set_item(\n                py,\n                stringify!(#property),\n                self.#property.into_python(py, None)?\n            )?;\n        });\n    }\n\n    let init = if let Some(py_child_field) = py_child_field {\n        let field = py_child_field.ident;\n        if py_child_field.is_option {\n            quote! {\n                match self.#field {\n                    Some(kind) => kind.into_python(py, Some(kw_args)),\n                    None => crate::into_python::init_ast_class(py, stringify!(#name), kw_args)\n                }\n            }\n        } else {\n            quote! {\n                self.#field.into_python(py, Some(kw_args))\n            }\n        }\n    } else {\n        quote! {\n            crate::into_python::init_ast_class(py, stringify!(#name), kw_args)\n        }\n    };\n\n    quote! {\n        impl crate::into_python::IntoPython for #name {\n            fn into_python(\n                self,\n                py: cpython::Python,\n                parent_kw_args: Option<cpython::PyDict>,\n            ) -> cpython::PyResult<cpython::PyObject> {\n                use crate::into_python::IntoPython;\n\n                let kw_args = parent_kw_args.unwrap_or_else(|| cPython::PyDict::new_bound(py));\n                #(#property_assigns)*\n\n                #init\n            }\n        }\n    }\n    .into()\n}\n\nstruct PyChildField {\n    ident: Ident,\n    is_option: bool,\n}\n\nfn infer_fields(r#struct: &mut syn::ItemStruct) -> (Vec<Ident>, Option<PyChildField>) {\n    let mut properties = Vec::new();\n    let mut py_child = None;\n\n    for field in &mut r#struct.fields {\n        let ident = field\n            .ident\n            .clone()\n            .expect(\"py_inherit supports only named fields\");\n\n        if find_attr(&field.attrs, \"py_child\").is_some() {\n            let is_option = is_option(&field.ty);\n\n            py_child = Some(PyChildField { ident, is_option });\n            continue;\n        }\n\n        properties.push(ident);\n    }\n\n    (properties, py_child)\n}\n\nfn find_attr<'a>(attrs: &'a [Attribute], name: &'static str) -> Option<&'a Attribute> {\n    attrs.iter().find(|a| {\n        let Some(ident) = a.path().get_ident() else {\n            return false;\n        };\n        *ident == name\n    })\n}\n\nfn is_option(ty: &Type) -> bool {\n    let Type::Path(TypePath { path, .. }) = ty else {\n        return false;\n    };\n    let Some(segment) = path.segments.first() else {\n        return false;\n    };\n    segment.ident == \"Option\"\n}\n"
  },
  {
    "path": "edb/edgeql-parser/edgeql-parser-python/Cargo.toml",
    "content": "[package]\nname = \"edgeql-parser-python\"\nlicense = \"MIT/Apache-2.0\"\nversion = \"0.1.0\"\nauthors = [\"MagicStack Inc. <hello@magic.io>\"]\nedition = \"2021\"\n\n[lints]\nworkspace = true\n\n[features]\npython_extension = [\"pyo3/extension-module\"]\ndefault = [\"python_extension\"]\n\n[dependencies]\npyo3 = { workspace = true, optional = true }\n\nedgeql-parser = { path = \"..\", features = [\"serde\"] }\nbytes = \"1.0.1\"\nnum-bigint = \"0.4.3\"\nbigdecimal = { version = \"0.4.5\", features = [\"string-only\"] }\nblake2 = \"0.10.4\"\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0\"\nindexmap = \"2.4.0\"\nonce_cell = \"1.18.0\"\nbincode = { version = \"1.3.3\" }\ngel-protocol = { workspace = true, features = [\"with-num-bigint\", \"with-bigdecimal\"] }\n\n[lib]\ncrate-type = [\"lib\", \"cdylib\"]\nname = \"edgeql_rust\"\npath = \"src/lib.rs\"\n"
  },
  {
    "path": "edb/edgeql-parser/edgeql-parser-python/src/errors.rs",
    "content": "use edgeql_parser::tokenizer::Error;\nuse pyo3::exceptions::PyValueError;\nuse pyo3::prelude::*;\nuse pyo3::types::{PyBytes, PyList};\nuse pyo3::{create_exception, exceptions};\n\nuse crate::tokenizer::OpaqueToken;\n\ncreate_exception!(_edgeql_parser, SyntaxError, exceptions::PyException);\n\n#[pyclass]\npub struct ParserResult {\n    #[pyo3(get)]\n    pub out: Py<PyAny>,\n\n    #[pyo3(get)]\n    pub errors: Py<PyAny>,\n}\n\n#[pymethods]\nimpl ParserResult {\n    fn pack(&self, py: Python) -> PyResult<Py<PyAny>> {\n        let tokens = self.out.downcast_bound::<PyList>(py)?;\n        let mut rv = Vec::with_capacity(tokens.len());\n        for token in tokens {\n            let token: &Bound<OpaqueToken> = token.downcast()?;\n            rv.push(token.borrow().inner.clone());\n        }\n        let mut buf = vec![0u8]; // type and version\n        bincode::serialize_into(&mut buf, &rv)\n            .map_err(|e| PyValueError::new_err(format!(\"Failed to pack: {e}\")))?;\n        Ok(PyBytes::new(py, buf.as_slice()).into())\n    }\n}\n\npub fn parser_error_into_tuple(\n    error: &Error,\n) -> (&str, (u64, u64), Option<&String>, Option<&String>) {\n    (\n        &error.message,\n        (error.span.start, error.span.end),\n        error.hint.as_ref(),\n        error.details.as_ref(),\n    )\n}\n"
  },
  {
    "path": "edb/edgeql-parser/edgeql-parser-python/src/hash.rs",
    "content": "use std::sync::RwLock;\n\nuse edgeql_parser::hash;\nuse pyo3::{exceptions::PyRuntimeError, prelude::*, types::PyString};\n\nuse crate::errors::SyntaxError;\n\n#[pyclass]\npub struct Hasher {\n    _hasher: RwLock<Option<hash::Hasher>>,\n}\n\n#[pymethods]\nimpl Hasher {\n    #[staticmethod]\n    fn start_migration(parent_id: &Bound<PyString>) -> PyResult<Hasher> {\n        let hasher = hash::Hasher::start_migration(parent_id.to_str()?);\n        Ok(Hasher {\n            _hasher: RwLock::new(Some(hasher)),\n        })\n    }\n\n    fn add_source(&self, py: Python, data: &Bound<PyString>) -> PyResult<Py<PyAny>> {\n        let text = data.to_str()?;\n        let mut cell = self._hasher.write().unwrap();\n        let hasher = cell\n            .as_mut()\n            .ok_or_else(|| PyRuntimeError::new_err((\"cannot add source after finish\",)))?;\n\n        hasher.add_source(text).map_err(|e| match e {\n            hash::Error::Tokenizer(msg, pos) => {\n                SyntaxError::new_err((msg, (pos.offset, py.None()), py.None(), py.None()))\n            }\n        })?;\n        Ok(py.None())\n    }\n\n    fn make_migration_id(&self) -> PyResult<String> {\n        let mut cell = self._hasher.write().unwrap();\n        let hasher = cell\n            .take()\n            .ok_or_else(|| PyRuntimeError::new_err((\"cannot do migration id twice\",)))?;\n        Ok(hasher.make_migration_id())\n    }\n}\n"
  },
  {
    "path": "edb/edgeql-parser/edgeql-parser-python/src/keywords.rs",
    "content": "use pyo3::{prelude::*, types::PyFrozenSet};\n\nuse edgeql_parser::keywords;\n\npub struct AllKeywords {\n    pub current: Py<PyFrozenSet>,\n    pub future: Py<PyFrozenSet>,\n    pub unreserved: Py<PyFrozenSet>,\n    pub partial: Py<PyFrozenSet>,\n}\n\npub fn get_keywords(py: Python) -> PyResult<AllKeywords> {\n    let intern = py.import(\"sys\")?.getattr(\"intern\")?;\n\n    Ok(AllKeywords {\n        current: prepare_keywords(py, &keywords::CURRENT_RESERVED_KEYWORDS, &intern)?,\n        unreserved: prepare_keywords(py, &keywords::UNRESERVED_KEYWORDS, &intern)?,\n        future: prepare_keywords(py, &keywords::FUTURE_RESERVED_KEYWORDS, &intern)?,\n        partial: prepare_keywords(py, &keywords::PARTIAL_RESERVED_KEYWORDS, &intern)?,\n    })\n}\n\nfn prepare_keywords<'a, 'py, I: IntoIterator<Item = &'a &'static str>>(\n    py: Python<'py>,\n    keyword_set: I,\n    intern: &Bound<'py, PyAny>,\n) -> PyResult<Py<PyFrozenSet>> {\n    PyFrozenSet::new(\n        py,\n        keyword_set\n            .into_iter()\n            .map(|s| intern.call((&s,), None).unwrap()),\n    )\n    .map(|o| o.unbind())\n}\n"
  },
  {
    "path": "edb/edgeql-parser/edgeql-parser-python/src/lib.rs",
    "content": "#![cfg(feature = \"python_extension\")]\nmod errors;\nmod hash;\nmod keywords;\npub mod normalize;\nmod parser;\nmod position;\nmod pynormalize;\nmod tokenizer;\nmod unpack;\n\nuse pyo3::prelude::*;\n\n/// Rust bindings to the edgeql-parser crate\n#[pymodule]\nfn _edgeql_parser(py: Python, m: &Bound<PyModule>) -> PyResult<()> {\n    m.add(\"SyntaxError\", py.get_type::<errors::SyntaxError>())?;\n    m.add(\"ParserResult\", py.get_type::<errors::ParserResult>())?;\n\n    m.add_class::<hash::Hasher>()?;\n\n    let keywords = keywords::get_keywords(py)?;\n    m.add(\"unreserved_keywords\", keywords.unreserved)?;\n    m.add(\"partial_reserved_keywords\", keywords.partial)?;\n    m.add(\"future_reserved_keywords\", keywords.future)?;\n    m.add(\"current_reserved_keywords\", keywords.current)?;\n\n    m.add_class::<pynormalize::Entry>()?;\n    m.add_function(wrap_pyfunction!(pynormalize::normalize, m)?)?;\n\n    m.add_function(wrap_pyfunction!(parser::parse, m)?)?;\n    m.add_function(wrap_pyfunction!(parser::suggest_next_keywords, m)?)?;\n    m.add_function(wrap_pyfunction!(parser::preload_spec, m)?)?;\n    m.add_function(wrap_pyfunction!(parser::save_spec, m)?)?;\n    m.add_class::<parser::CSTNode>()?;\n    m.add_class::<parser::Production>()?;\n    m.add_class::<parser::Terminal>()?;\n\n    m.add_function(wrap_pyfunction!(position::offset_of_line, m)?)?;\n    m.add(\"SourcePoint\", py.get_type::<position::SourcePoint>())?;\n\n    m.add_class::<tokenizer::OpaqueToken>()?;\n    m.add_function(wrap_pyfunction!(tokenizer::tokenize, m)?)?;\n    m.add_function(wrap_pyfunction!(tokenizer::unpickle_token, m)?)?;\n\n    m.add_function(wrap_pyfunction!(unpack::unpack, m)?)?;\n\n    tokenizer::fini_module(m);\n\n    Ok(())\n}\n"
  },
  {
    "path": "edb/edgeql-parser/edgeql-parser-python/src/normalize.rs",
    "content": "use std::collections::BTreeSet;\n\nuse edgeql_parser::keywords::Keyword;\nuse edgeql_parser::position::{Pos, Span};\nuse edgeql_parser::tokenizer::{Kind, Token, Tokenizer, Value};\n\nuse blake2::{Blake2b512, Digest};\n\n#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct Variable {\n    pub value: Value,\n}\n\npub struct Entry {\n    pub processed_source: String,\n    pub hash: [u8; 64],\n    pub tokens: Vec<Token<'static>>,\n    pub variables: Vec<Vec<Variable>>,\n    pub named_args: bool,\n    pub first_arg: Option<usize>,\n}\n\n/// PackedEntry is a compact Entry for serialization purposes\n#[derive(serde::Serialize, serde::Deserialize)]\npub struct PackedEntry {\n    pub tokens: Vec<Token<'static>>,\n    pub variables: Vec<Vec<Variable>>,\n    pub named_args: bool,\n    pub first_arg: Option<usize>,\n}\n\nimpl From<Entry> for PackedEntry {\n    fn from(val: Entry) -> Self {\n        PackedEntry {\n            tokens: val.tokens,\n            variables: val.variables,\n            named_args: val.named_args,\n            first_arg: val.first_arg,\n        }\n    }\n}\n\nimpl From<PackedEntry> for Entry {\n    fn from(val: PackedEntry) -> Self {\n        let processed_source = serialize_tokens(&val.tokens[..]);\n        Entry {\n            hash: hash(&processed_source),\n            processed_source,\n            tokens: val.tokens,\n            variables: val.variables,\n            named_args: val.named_args,\n            first_arg: val.first_arg,\n        }\n    }\n}\n\n#[derive(Debug)]\npub enum Error {\n    Tokenizer(String, u64),\n    Assertion(String, Pos),\n}\n\npub fn normalize(text: &str) -> Result<Entry, Error> {\n    let tokens = Tokenizer::new(text)\n        .validated_values()\n        .with_eof()\n        .map(|x| x.map(|t| t.cloned()))\n        .collect::<Result<Vec<_>, _>>()\n        .map_err(|e| Error::Tokenizer(e.message, e.span.start))?;\n\n    let (named_args, var_idx) = match scan_vars(&tokens) {\n        Some(pair) => pair,\n        None => {\n            // don't extract from invalid query, let python code do its work\n            let processed_source = serialize_tokens(&tokens);\n            return Ok(Entry {\n                hash: hash(&processed_source),\n                processed_source,\n                tokens,\n                variables: Vec::new(),\n                named_args: false,\n                first_arg: None,\n            });\n        }\n    };\n    let mut rewritten_tokens = Vec::with_capacity(tokens.len());\n    let mut all_variables = Vec::new();\n    let mut variables = Vec::new();\n    let mut counter = var_idx;\n    let mut next_var = || {\n        let n = counter;\n        counter += 1;\n        if named_args {\n            format!(\"$__edb_arg_{n}\")\n        } else {\n            format!(\"${n}\")\n        }\n    };\n    let mut last_was_set = false;\n    for tok in &tokens {\n        let mut is_set = false;\n        match tok.kind {\n            Kind::IntConst\n            // Don't replace `.12` because this is a tuple access\n            if !matches!(rewritten_tokens.last(),\n                Some(Token { kind: Kind::Dot, .. }))\n            // Don't replace 'LIMIT 1' as a special case\n            && (tok.text != \"1\"\n                || !matches!(rewritten_tokens.last(),\n                    Some(Token { kind: Kind::Keyword(Keyword(\"limit\")), .. })))\n            && tok.text != \"9223372036854775808\"\n            => {\n                rewritten_tokens.push(arg_type_cast(\n                    \"int64\", next_var(), tok.span\n                ));\n                variables.push(Variable {\n                    value: tok.value.clone().unwrap(),\n                });\n                continue;\n            }\n            Kind::FloatConst => {\n                rewritten_tokens.push(arg_type_cast(\n                    \"float64\", next_var(), tok.span\n                ));\n                variables.push(Variable {\n                    value: tok.value.clone().unwrap(),\n                });\n                continue;\n            }\n            Kind::BigIntConst => {\n                rewritten_tokens.push(arg_type_cast(\n                    \"bigint\", next_var(), tok.span\n                ));\n                variables.push(Variable {\n                    value: tok.value.clone().unwrap(),\n                });\n                continue;\n            }\n            Kind::DecimalConst => {\n                rewritten_tokens.push(arg_type_cast(\n                    \"decimal\", next_var(), tok.span\n                ));\n                variables.push(Variable {\n                    value: tok.value.clone().unwrap(),\n                });\n                continue;\n            }\n            Kind::Str => {\n                rewritten_tokens.push(arg_type_cast(\n                    \"str\", next_var(), tok.span\n                ));\n                variables.push(Variable {\n                    value: tok.value.clone().unwrap(),\n                });\n                continue;\n            }\n            Kind::Keyword(Keyword(kw))\n            if (\n                matches!(kw, \"administer\"|\"configure\"|\"create\"|\"alter\"|\"drop\"|\"start\"|\"analyze\")\n                || (last_was_set && kw == \"global\")\n            ) => {\n                let processed_source = serialize_tokens(&tokens);\n                return Ok(Entry {\n                    hash: hash(&processed_source),\n                    processed_source,\n                    tokens,\n                    variables: Vec::new(),\n                    named_args: false,\n                    first_arg: None,\n                });\n            }\n            // Split on semicolons.\n            // N.B: This naive statement splitting on semicolons works\n            // because the only statements with internal semis are DDL\n            // statements, which we don't support anyway.\n            Kind::Semicolon => {\n                all_variables.push(variables);\n                variables = Vec::new();\n                rewritten_tokens.push(tok.clone());\n            }\n            Kind::Keyword(Keyword(\"set\")) => {\n                is_set = true;\n                rewritten_tokens.push(tok.clone());\n            }\n            _ => rewritten_tokens.push(tok.clone()),\n        }\n        last_was_set = is_set;\n    }\n\n    all_variables.push(variables);\n    // N.B: We always serialize the tokens to produce\n    // processed_source, even when no changes have been made. This is\n    // because when Source gets serialized, it always uses a\n    // PackedEntry, which will result in it being normalized *there*,\n    // and so if we don't do it *here*, then we won't be able to hit\n    // the persistent cache in cases where we didn't reserialize the\n    // tokens.\n    // TODO: Rework the caching to avoid needing to do this.\n    let processed_source = serialize_tokens(&rewritten_tokens[..]);\n    Ok(Entry {\n        hash: hash(&processed_source),\n        processed_source,\n        named_args,\n        first_arg: if counter <= var_idx {\n            None\n        } else {\n            Some(var_idx)\n        },\n        tokens: rewritten_tokens,\n        variables: all_variables,\n    })\n}\n\nfn is_operator(token: &Token) -> bool {\n    use edgeql_parser::tokenizer::Kind::*;\n    match token.kind {\n        Assign | SubAssign | AddAssign | Arrow | Coalesce | Namespace | DoubleSplat\n        | BackwardLink | OptionalLink | FloorDiv | Concat | GreaterEq | LessEq | NotEq\n        | NotDistinctFrom | DistinctFrom | Comma | OpenParen | CloseParen | OpenBracket\n        | CloseBracket | OpenBrace | CloseBrace | Dot | Semicolon | Colon | Add | Sub | Mul\n        | Div | Modulo | Pow | Less | Greater | Eq | Ampersand | Pipe | At => true,\n        DecimalConst | FloatConst | IntConst | BigIntConst | BinStr | Parameter\n        | ParameterAndType | Str | BacktickName | Keyword(_) | Ident | Substitution | EOI\n        | Epsilon | StartBlock | StartExtension | StartFragment | StartMigration\n        | StartSDLDocument | StrInterpStart | StrInterpCont | StrInterpEnd => false,\n    }\n}\n\nfn serialize_tokens(tokens: &[Token]) -> String {\n    use edgeql_parser::tokenizer::Kind::Parameter;\n\n    let mut buf = String::new();\n    let mut needs_space = false;\n    for token in tokens {\n        if matches!(token.kind, Kind::EOI) {\n            break;\n        }\n\n        if needs_space && !is_operator(token) && token.kind != Parameter {\n            buf.push(' ');\n        }\n        buf.push_str(&token.text);\n        needs_space = !is_operator(token);\n    }\n    buf\n}\n\nfn scan_vars<'x, 'y: 'x, I>(tokens: I) -> Option<(bool, usize)>\nwhere\n    I: IntoIterator<Item = &'x Token<'x>>,\n{\n    let mut max_visited = None::<usize>;\n    let mut names = BTreeSet::new();\n    for t in tokens {\n        if t.kind == Kind::Parameter {\n            if let Ok(v) = t.text[1..].parse() {\n                if max_visited.map(|old| v > old).unwrap_or(true) {\n                    max_visited = Some(v);\n                }\n            } else {\n                names.insert(&t.text[..]);\n            }\n        }\n    }\n    if names.is_empty() {\n        let next = max_visited.map(|x| x.checked_add(1)).unwrap_or(Some(0))?;\n        Some((false, next))\n    } else if max_visited.is_some() {\n        return None; // mixed arguments\n    } else {\n        Some((true, names.len()))\n    }\n}\n\nfn hash(text: &str) -> [u8; 64] {\n    let mut result = [0u8; 64];\n    result.copy_from_slice(&Blake2b512::new_with_prefix(text.as_bytes()).finalize());\n    result\n}\n\n/// Produces tokens corresponding to (<lit typ>$var)\nfn arg_type_cast(typ: &'static str, var: String, span: Span) -> Token<'static> {\n    // the `lit` is required so these tokens have different text than an actual\n    // type cast and parameter, so their hashes don't clash.\n    Token {\n        kind: Kind::ParameterAndType,\n        text: format!(\"<lit {typ}>{var}\").into(),\n        value: None,\n        span,\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::scan_vars;\n    use edgeql_parser::tokenizer::{Token, Tokenizer};\n\n    fn tokenize(s: &str) -> Vec<Token> {\n        let mut r = Vec::new();\n        let mut s = Tokenizer::new(s);\n        loop {\n            match s.next() {\n                Some(Ok(x)) => r.push(x),\n                None => break,\n                Some(Err(e)) => panic!(\"Parse error at {}: {}\", s.current_pos(), e.message),\n            }\n        }\n        r\n    }\n\n    #[test]\n    fn none() {\n        assert_eq!(scan_vars(&tokenize(\"SELECT 1+1\")).unwrap(), (false, 0));\n    }\n\n    #[test]\n    fn numeric() {\n        assert_eq!(scan_vars(&tokenize(\"$0 $1 $2\")).unwrap(), (false, 3));\n        assert_eq!(scan_vars(&tokenize(\"$2 $3 $2\")).unwrap(), (false, 4));\n        assert_eq!(scan_vars(&tokenize(\"$0 $0 $0\")).unwrap(), (false, 1));\n        assert_eq!(scan_vars(&tokenize(\"$10 $100\")).unwrap(), (false, 101));\n    }\n\n    #[test]\n    fn named() {\n        assert_eq!(scan_vars(&tokenize(\"$a\")).unwrap(), (true, 1));\n        assert_eq!(scan_vars(&tokenize(\"$b $c $d\")).unwrap(), (true, 3));\n        assert_eq!(scan_vars(&tokenize(\"$b $c $b\")).unwrap(), (true, 2));\n        assert_eq!(\n            scan_vars(&tokenize(\"$a $b $b $a $c $xx\")).unwrap(),\n            (true, 4)\n        );\n    }\n\n    #[test]\n    fn mixed() {\n        assert_eq!(scan_vars(&tokenize(\"$a $0\")), None);\n        assert_eq!(scan_vars(&tokenize(\"$0 $a\")), None);\n        assert_eq!(scan_vars(&tokenize(\"$b $c $100\")), None);\n        assert_eq!(scan_vars(&tokenize(\"$10 $xx $yy\")), None);\n    }\n}\n"
  },
  {
    "path": "edb/edgeql-parser/edgeql-parser-python/src/parser.rs",
    "content": "use once_cell::sync::OnceCell;\n\nuse edgeql_parser::parser;\nuse pyo3::exceptions::{PyAssertionError, PyValueError};\nuse pyo3::prelude::*;\nuse pyo3::types::{PyList, PyString};\n\nuse crate::errors::{parser_error_into_tuple, ParserResult};\nuse crate::pynormalize::TokenizerValue;\nuse crate::tokenizer::OpaqueToken;\n\n#[pyfunction]\npub fn parse(\n    py: Python,\n    start_token_name: &Bound<PyString>,\n    tokens: Py<PyAny>,\n) -> PyResult<(ParserResult, &'static Py<PyAny>)> {\n    let start_token_name = start_token_name.to_string();\n\n    let (spec, productions) = get_spec()?;\n\n    let tokens = downcast_tokens(py, &start_token_name, tokens)?;\n\n    let context = parser::Context::new(spec);\n    let (cst, errors) = parser::parse(&tokens, &context);\n\n    let errors = PyList::new(py, errors.iter().map(|e| parser_error_into_tuple(e)))?;\n\n    let res = ParserResult {\n        out: cst.as_ref().map(ParserCSTNode).into_pyobject(py)?.unbind(),\n        errors: errors.into(),\n    };\n\n    Ok((res, productions))\n}\n\n#[pyfunction]\npub fn suggest_next_keywords(\n    py: Python,\n    start_token_name: &Bound<PyString>,\n    tokens: Py<PyAny>,\n) -> PyResult<(Py<PyList>, bool)> {\n    let start_token_name = start_token_name.to_string();\n\n    let (spec, _) = get_spec()?;\n\n    let tokens = downcast_tokens(py, &start_token_name, tokens)?;\n\n    let context = parser::Context::new(spec);\n    let (suggestions, can_be_ident) = parser::suggest_next_keyword(&tokens, &context);\n\n    let suggestions_py = suggestions.iter().map(|k| PyString::new(py, k.0));\n    let suggestions_py = PyList::new(py, suggestions_py)?.into();\n\n    Ok((suggestions_py, can_be_ident))\n}\n\n#[pyclass]\npub struct CSTNode {\n    #[pyo3(get)]\n    production: Option<Py<Production>>,\n    #[pyo3(get)]\n    terminal: Option<Py<Terminal>>,\n}\n\n#[pyclass]\npub struct Production {\n    #[pyo3(get)]\n    id: usize,\n    #[pyo3(get)]\n    args: Py<PyAny>,\n    #[pyo3(get)]\n    start: Option<u64>,\n    #[pyo3(get)]\n    end: Option<u64>,\n}\n\n#[pyclass]\npub struct Terminal {\n    #[pyo3(get)]\n    text: String,\n    #[pyo3(get)]\n    value: Py<PyAny>,\n    #[pyo3(get)]\n    start: u64,\n    #[pyo3(get)]\n    end: u64,\n}\n\nstatic PARSER_SPECS: OnceCell<(parser::Spec, Py<PyAny>)> = OnceCell::new();\n\nfn downcast_tokens(\n    py: Python,\n    start_token_name: &str,\n    token_list: Py<PyAny>,\n) -> PyResult<Vec<parser::Terminal>> {\n    let tokens = token_list.downcast_bound::<PyList>(py)?;\n\n    let mut buf = Vec::with_capacity(tokens.len() + 1);\n    buf.push(parser::Terminal::from_start_name(start_token_name));\n    for token in tokens.iter() {\n        let token: &Bound<OpaqueToken> = token.downcast()?;\n        let token = token.borrow().inner.clone();\n\n        buf.push(parser::Terminal::from_token(token));\n    }\n\n    Ok(buf)\n}\n\nfn get_spec() -> PyResult<&'static (parser::Spec, Py<PyAny>)> {\n    if let Some(x) = PARSER_SPECS.get() {\n        Ok(x)\n    } else {\n        Err(PyAssertionError::new_err((\"grammar spec not loaded\",)))\n    }\n}\n\n/// Loads the grammar specification from file and caches it in memory.\n#[pyfunction]\npub fn preload_spec(py: Python, spec_filepath: &Bound<PyString>) -> PyResult<()> {\n    if PARSER_SPECS.get().is_some() {\n        return Ok(());\n    }\n\n    let spec_filepath = spec_filepath.to_string();\n    let bytes = std::fs::read(&spec_filepath)\n        .unwrap_or_else(|e| panic!(\"Cannot read grammar spec from {spec_filepath} ({e})\"));\n\n    let spec: parser::Spec = bincode::deserialize::<parser::SpecSerializable>(&bytes)\n        .map_err(|e| PyValueError::new_err(format!(\"Bad spec: {e}\")))?\n        .into();\n    let productions = load_productions(py, &spec)?;\n\n    let _ = PARSER_SPECS.set((spec, productions));\n    Ok(())\n}\n\n/// Serialize the grammar specification and write it to a file.\n///\n/// Called from setup.py.\n#[pyfunction]\npub fn save_spec(spec_json: &Bound<PyString>, dst: &Bound<PyString>) -> PyResult<()> {\n    let spec_json = spec_json.to_string();\n    let spec: parser::SpecSerializable = serde_json::from_str(&spec_json)\n        .map_err(|e| PyValueError::new_err(format!(\"Invalid JSON: {e}\")))?;\n    let spec_bitcode = bincode::serialize(&spec)\n        .map_err(|e| PyValueError::new_err(format!(\"Failed to pack spec: {e}\")))?;\n\n    let dst = dst.to_string();\n\n    std::fs::write(dst, spec_bitcode).ok().unwrap();\n    Ok(())\n}\n\nfn load_productions(py: Python<'_>, spec: &parser::Spec) -> PyResult<Py<PyAny>> {\n    let grammar_name = \"edb.edgeql.parser.grammar.start\";\n    let grammar_mod = py.import(grammar_name)?;\n    let load_productions = py\n        .import(\"edb.common.parsing\")?\n        .getattr(\"load_spec_productions\")?;\n\n    let productions = load_productions.call((&spec.production_names, grammar_mod), None)?;\n    Ok(productions.into())\n}\n\n/// Newtype required to define a trait for a foreign type.\nstruct ParserCSTNode<'a>(&'a parser::CSTNode<'a>);\n\nimpl<'py> IntoPyObject<'py> for ParserCSTNode<'_> {\n    type Target = CSTNode;\n    type Output = Bound<'py, Self::Target>;\n    type Error = PyErr;\n\n    fn into_pyobject(self, py: Python<'py>) -> PyResult<Self::Output> {\n        let res = match self.0 {\n            parser::CSTNode::Empty => CSTNode {\n                production: None,\n                terminal: None,\n            },\n            parser::CSTNode::Terminal(token) => CSTNode {\n                production: None,\n                terminal: Some(Py::new(\n                    py,\n                    Terminal {\n                        text: token.text.clone(),\n                        value: (token.value.as_ref())\n                            .map(TokenizerValue)\n                            .into_pyobject(py)?\n                            .unbind(),\n                        start: token.span.start,\n                        end: token.span.end,\n                    },\n                )?),\n            },\n            parser::CSTNode::Production(prod) => CSTNode {\n                production: Some(Py::new(\n                    py,\n                    Production {\n                        id: prod.id,\n                        args: PyList::new(py, prod.args.iter().map(ParserCSTNode))?.into(),\n                        start: prod.span.map(|s| s.start),\n                        end: prod.span.map(|s| s.end),\n                    },\n                )?),\n                terminal: None,\n            },\n        };\n        Ok(Py::new(py, res)?.bind(py).clone())\n    }\n}\n"
  },
  {
    "path": "edb/edgeql-parser/edgeql-parser-python/src/position.rs",
    "content": "use pyo3::{\n    exceptions::{PyIndexError, PyRuntimeError},\n    prelude::*,\n    types::{PyBytes, PyList},\n};\n\nuse edgeql_parser::position::InflatedPos;\n\n#[pyclass]\npub struct SourcePoint {\n    _position: InflatedPos,\n}\n\n#[pymethods]\nimpl SourcePoint {\n    #[staticmethod]\n    fn from_offsets(py: Python, data: &Bound<PyBytes>, offsets: Py<PyAny>) -> PyResult<Py<PyList>> {\n        let mut list: Vec<usize> = offsets.extract(py)?;\n        let data: &[u8] = data.as_bytes();\n        list.sort();\n        let result = InflatedPos::from_offsets(data, &list)\n            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;\n\n        PyList::new(\n            py,\n            result\n                .into_iter()\n                .map(|_position| SourcePoint { _position }),\n        )\n        .map(|v| v.into())\n    }\n\n    #[staticmethod]\n    fn from_lines_cols(\n        py: Python,\n        data: &Bound<PyBytes>,\n        lines_cols: Py<PyAny>,\n    ) -> PyResult<Py<PyList>> {\n        let mut list: Vec<(u64, u64)> = lines_cols.extract(py)?;\n        let data: &[u8] = data.as_bytes();\n        list.sort();\n        let result = InflatedPos::from_lines_cols(data, &list)\n            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;\n\n        PyList::new(\n            py,\n            result\n                .into_iter()\n                .map(|_position| SourcePoint { _position }),\n        )\n        .map(|v| v.into())\n    }\n\n    #[getter]\n    fn line(&self) -> u64 {\n        self._position.line + 1\n    }\n    #[getter]\n    fn zero_based_line(&self) -> u64 {\n        self._position.line\n    }\n    #[getter]\n    fn column(&self) -> u64 {\n        self._position.column + 1\n    }\n    #[getter]\n    fn utf16column(&self) -> u64 {\n        self._position.utf16column\n    }\n    #[getter]\n    fn offset(&self) -> u64 {\n        self._position.offset\n    }\n    #[getter]\n    fn char_offset(&self) -> u64 {\n        self._position.char_offset\n    }\n}\n\nfn _offset_of_line(text: &str, target: usize) -> Option<usize> {\n    let mut was_lf = false;\n    let mut line = 0; // this assumes line found by rfind\n    for (idx, &byte) in text.as_bytes().iter().enumerate() {\n        if line >= target {\n            return Some(idx);\n        }\n        match byte {\n            b'\\n' => {\n                line += 1;\n                was_lf = false;\n            }\n            _ if was_lf => {\n                line += 1;\n                if line >= target {\n                    return Some(idx);\n                }\n                was_lf = byte == b'\\r';\n            }\n            b'\\r' => {\n                was_lf = true;\n            }\n            _ => {}\n        }\n    }\n    if was_lf {\n        line += 1;\n    }\n    if target > line {\n        return None;\n    }\n    Some(text.len())\n}\n\n#[pyfunction]\npub fn offset_of_line(text: &str, target: usize) -> PyResult<usize> {\n    match _offset_of_line(text, target) {\n        Some(offset) => Ok(offset),\n        None => Err(PyIndexError::new_err(\"line number is too large\")),\n    }\n}\n\n#[test]\nfn line_offsets() {\n    assert_eq!(_offset_of_line(\"line1\\nline2\\nline3\", 0), Some(0));\n    assert_eq!(_offset_of_line(\"line1\\nline2\\nline3\", 1), Some(6));\n    assert_eq!(_offset_of_line(\"line1\\nline2\\nline3\", 2), Some(12));\n    assert_eq!(_offset_of_line(\"line1\\nline2\\nline3\", 3), None);\n    assert_eq!(_offset_of_line(\"line1\\rline2\\rline3\", 0), Some(0));\n    assert_eq!(_offset_of_line(\"line1\\rline2\\rline3\", 1), Some(6));\n    assert_eq!(_offset_of_line(\"line1\\rline2\\rline3\", 2), Some(12));\n    assert_eq!(_offset_of_line(\"line1\\rline2\\rline3\", 3), None);\n    assert_eq!(_offset_of_line(\"line1\\r\\nline2\\r\\nline3\", 0), Some(0));\n    assert_eq!(_offset_of_line(\"line1\\r\\nline2\\r\\nline3\", 1), Some(7));\n    assert_eq!(_offset_of_line(\"line1\\r\\nline2\\r\\nline3\", 2), Some(14));\n    assert_eq!(_offset_of_line(\"line1\\r\\nline2\\r\\nline3\", 3), None);\n    assert_eq!(_offset_of_line(\"line1\\rline2\\r\\nline3\\n\", 0), Some(0));\n    assert_eq!(_offset_of_line(\"line1\\rline2\\r\\nline3\\n\", 1), Some(6));\n    assert_eq!(_offset_of_line(\"line1\\rline2\\r\\nline3\\n\", 2), Some(13));\n    assert_eq!(_offset_of_line(\"line1\\rline2\\r\\nline3\\n\", 3), Some(19));\n    assert_eq!(_offset_of_line(\"line1\\rline2\\r\\nline3\\n\", 4), None);\n    assert_eq!(_offset_of_line(\"line1\\nline2\\rline3\\r\\n\", 0), Some(0));\n    assert_eq!(_offset_of_line(\"line1\\nline2\\rline3\\r\\n\", 1), Some(6));\n    assert_eq!(_offset_of_line(\"line1\\nline2\\rline3\\r\\n\", 2), Some(12));\n    assert_eq!(_offset_of_line(\"line1\\nline2\\rline3\\r\\n\", 3), Some(19));\n    assert_eq!(_offset_of_line(\"line1\\nline2\\rline3\\r\\n\", 4), None);\n    assert_eq!(_offset_of_line(\"line1\\n\\rline2\\r\\rline3\\r\", 0), Some(0));\n    assert_eq!(_offset_of_line(\"line1\\n\\rline2\\r\\rline3\\r\", 1), Some(6));\n    assert_eq!(_offset_of_line(\"line1\\n\\rline2\\r\\rline3\\r\", 2), Some(7));\n    assert_eq!(_offset_of_line(\"line1\\n\\rline2\\r\\rline3\\r\", 3), Some(13));\n    assert_eq!(_offset_of_line(\"line1\\n\\rline2\\r\\rline3\\r\", 4), Some(14));\n    assert_eq!(_offset_of_line(\"line1\\n\\rline2\\r\\rline3\\r\", 5), Some(20));\n    assert_eq!(_offset_of_line(\"line1\\n\\rline2\\r\\rline3\\r\", 6), None);\n}\n"
  },
  {
    "path": "edb/edgeql-parser/edgeql-parser-python/src/pynormalize.rs",
    "content": "use std::convert::TryFrom;\n\nuse bigdecimal::Num;\n\nuse bytes::{BufMut, Bytes, BytesMut};\nuse edgeql_parser::tokenizer::Value;\nuse gel_protocol::codec;\nuse gel_protocol::model::{BigInt, Decimal};\nuse pyo3::exceptions::{PyAssertionError, PyValueError};\nuse pyo3::prelude::*;\nuse pyo3::types::{PyBytes, PyDict, PyFloat, PyInt, PyList, PyString};\n\nuse crate::errors::SyntaxError;\nuse crate::normalize::{normalize as _normalize, Error, PackedEntry, Variable};\nuse crate::tokenizer::tokens_to_py;\n\n#[pyfunction]\npub fn normalize(py: Python<'_>, text: &Bound<PyString>) -> PyResult<Entry> {\n    let text = text.to_string();\n    match _normalize(&text) {\n        Ok(entry) => Entry::new(py, entry),\n        Err(Error::Tokenizer(msg, pos)) => Err(SyntaxError::new_err((\n            msg,\n            (pos, py.None()),\n            py.None(),\n            py.None(),\n        ))),\n        Err(Error::Assertion(msg, pos)) => Err(PyAssertionError::new_err(format!(\"{pos}: {msg}\"))),\n    }\n}\n\n#[pyclass]\npub struct Entry {\n    #[pyo3(get)]\n    key: Py<PyAny>,\n\n    #[pyo3(get)]\n    tokens: Py<PyAny>,\n\n    #[pyo3(get)]\n    extra_blobs: Py<PyAny>,\n\n    extra_named: bool,\n\n    #[pyo3(get)]\n    first_extra: Option<usize>,\n\n    #[pyo3(get)]\n    extra_counts: Py<PyAny>,\n\n    entry_pack: PackedEntry,\n}\n\nimpl Entry {\n    pub fn new(py: Python, entry: crate::normalize::Entry) -> PyResult<Self> {\n        let blobs = serialize_all(py, &entry.variables)?;\n        let counts = entry.variables.iter().map(|x| x.len());\n\n        Ok(Entry {\n            key: PyBytes::new(py, &entry.hash[..]).into(),\n            tokens: tokens_to_py(py, entry.tokens.clone())?.into_any(),\n            extra_blobs: blobs.into(),\n            extra_named: entry.named_args,\n            first_extra: entry.first_arg,\n            extra_counts: PyList::new(py, counts)?.into(),\n            entry_pack: entry.into(),\n        })\n    }\n}\n\n#[pymethods]\nimpl Entry {\n    fn get_variables(&self, py: Python) -> PyResult<Py<PyAny>> {\n        let vars = PyDict::new(py);\n        let first = match self.first_extra {\n            Some(first) => first,\n            None => return Ok(vars.into()),\n        };\n        for (idx, var) in self.entry_pack.variables.iter().flatten().enumerate() {\n            let s = if self.extra_named {\n                format!(\"__edb_arg_{}\", first + idx)\n            } else {\n                (first + idx).to_string()\n            };\n            vars.set_item(s, TokenizerValue(&var.value))?;\n        }\n\n        Ok(vars.into())\n    }\n\n    fn pack(&self, py: Python) -> PyResult<Py<PyAny>> {\n        let mut buf = vec![1u8]; // type and version\n        bincode::serialize_into(&mut buf, &self.entry_pack)\n            .map_err(|e| PyValueError::new_err(format!(\"Failed to pack: {e}\")))?;\n        Ok(PyBytes::new(py, buf.as_slice()).into())\n    }\n}\n\npub fn serialize_extra(variables: &[Variable]) -> Result<Bytes, String> {\n    use gel_protocol::codec::Codec;\n    use gel_protocol::value::Value as P;\n\n    let mut buf = BytesMut::new();\n    buf.reserve(4 * variables.len());\n    for var in variables {\n        buf.reserve(4);\n        let pos = buf.len();\n        buf.put_u32(0); // replaced after serializing a value\n        match var.value {\n            Value::Int(v) => {\n                codec::Int64\n                    .encode(&mut buf, &P::Int64(v))\n                    .map_err(|e| format!(\"int cannot be encoded: {e}\"))?;\n            }\n            Value::String(ref v) => {\n                codec::Str\n                    .encode(&mut buf, &P::Str(v.clone()))\n                    .map_err(|e| format!(\"str cannot be encoded: {e}\"))?;\n            }\n            Value::Float(ref v) => {\n                codec::Float64\n                    .encode(&mut buf, &P::Float64(*v))\n                    .map_err(|e| format!(\"float cannot be encoded: {e}\"))?;\n            }\n            Value::BigInt(ref v) => {\n                // We have two different versions of BigInt implementations here.\n                // We have to use bigdecimal::num_bigint::BigInt because it can parse with radix 16.\n\n                let val = bigdecimal::num_bigint::BigInt::from_str_radix(v, 16)\n                    .map_err(|e| format!(\"bigint cannot be encoded: {e}\"))\n                    .and_then(|x| {\n                        BigInt::try_from(x).map_err(|e| format!(\"bigint cannot be encoded: {e}\"))\n                    })?;\n\n                codec::BigInt\n                    .encode(&mut buf, &P::BigInt(val))\n                    .map_err(|e| format!(\"bigint cannot be encoded: {e}\"))?;\n            }\n            Value::Decimal(ref v) => {\n                let val = Decimal::try_from(v.clone())\n                    .map_err(|e| format!(\"decimal cannot be encoded: {e}\"))?;\n                codec::Decimal\n                    .encode(&mut buf, &P::Decimal(val))\n                    .map_err(|e| format!(\"decimal cannot be encoded: {e}\"))?;\n            }\n            Value::Bytes(_) => {\n                // bytes literals should not be extracted during normalization\n                unreachable!()\n            }\n        }\n        let len = buf.len() - pos - 4;\n        buf[pos..pos + 4].copy_from_slice(\n            &u32::try_from(len)\n                .map_err(|_| \"element isn't too long\".to_owned())?\n                .to_be_bytes(),\n        );\n    }\n    Ok(buf.freeze())\n}\n\npub fn serialize_all<'a>(\n    py: Python<'a>,\n    variables: &[Vec<Variable>],\n) -> PyResult<Bound<'a, PyList>> {\n    let mut buf = Vec::with_capacity(variables.len());\n    for vars in variables {\n        let bytes = serialize_extra(vars).map_err(PyAssertionError::new_err)?;\n        buf.push(PyBytes::new(py, &bytes));\n    }\n    PyList::new(py, &buf)\n}\n\n/// Newtype required to define a trait for a foreign type.\npub struct TokenizerValue<'a>(pub &'a Value);\n\nimpl<'py> IntoPyObject<'py> for TokenizerValue<'py> {\n    type Target = PyAny;\n    type Output = Bound<'py, Self::Target>;\n    type Error = PyErr;\n\n    fn into_pyobject(self, py: Python<'py>) -> PyResult<Self::Output> {\n        let res = match self.0 {\n            Value::Int(v) => v.into_pyobject(py)?.into_any(),\n            Value::String(v) => v.into_pyobject(py)?.into_any(),\n            Value::Float(v) => v.into_pyobject(py)?.into_any(),\n            Value::BigInt(v) => py.get_type::<PyInt>().call((v, 16), None)?,\n            Value::Decimal(v) => py\n                .get_type::<PyFloat>()\n                .call((v.to_string(),), None)?\n                .into_any(),\n            Value::Bytes(v) => PyBytes::new(py, v).into_any(),\n        };\n        Ok(res)\n    }\n}\n"
  },
  {
    "path": "edb/edgeql-parser/edgeql-parser-python/src/tokenizer.rs",
    "content": "use edgeql_parser::tokenizer::{Kind, Token, Tokenizer};\nuse once_cell::sync::OnceCell;\nuse pyo3::exceptions::PyValueError;\nuse pyo3::prelude::*;\nuse pyo3::types::{PyBytes, PyList, PyString};\n\nuse crate::errors::{parser_error_into_tuple, ParserResult};\n\n#[pyfunction]\npub fn tokenize(py: Python, s: &Bound<PyString>) -> PyResult<ParserResult> {\n    let data = s.to_string();\n\n    let token_stream = Tokenizer::new(&data[..]).validated_values().with_eof();\n\n    let mut tokens = vec![];\n    let mut errors = vec![];\n\n    for res in token_stream.into_iter() {\n        match res {\n            Ok(token) => tokens.push(token),\n            Err(e) => {\n                errors.push(parser_error_into_tuple(&e).into_pyobject(py)?);\n\n                // TODO: fix tokenizer to skip bad tokens and continue\n                break;\n            }\n        }\n    }\n\n    let out = tokens_to_py(py, tokens)?.into_pyobject(py)?.into();\n    let errors = PyList::new(py, errors)?.into();\n\n    Ok(ParserResult { out, errors })\n}\n\n// An opaque wrapper around [edgeql_parser::tokenizer::Token].\n// Supports Python pickle serialization.\n#[pyclass]\npub struct OpaqueToken {\n    pub inner: Token<'static>,\n}\n\n#[pymethods]\nimpl OpaqueToken {\n    fn __repr__(&self) -> PyResult<String> {\n        Ok(self.inner.to_string())\n    }\n    fn __reduce__(&self, py: Python) -> PyResult<(Py<PyAny>, (Py<PyAny>,))> {\n        let data = bincode::serialize(&self.inner)\n            .map_err(|e| PyValueError::new_err(format!(\"Failed to reduce: {e}\")))?;\n\n        let tok = get_unpickle_token_fn(py);\n        Ok((tok, (PyBytes::new(py, &data).into(),)))\n    }\n\n    fn span_start(&self) -> u64 {\n        self.inner.span.start\n    }\n\n    fn span_end(&self) -> u64 {\n        self.inner.span.end\n    }\n\n    fn is_ident(&self) -> bool {\n        matches!(self.inner.kind, Kind::Ident)\n    }\n}\n\npub fn tokens_to_py(py: Python<'_>, rust_tokens: Vec<Token>) -> PyResult<Py<PyList>> {\n    Ok(PyList::new(\n        py,\n        rust_tokens.into_iter().map(|tok| OpaqueToken {\n            inner: tok.cloned(),\n        }),\n    )?\n    .unbind())\n}\n\n/// To support pickle serialization of OpaqueTokens, we need to provide a\n/// deserialization function in __reduce__ methods.\n/// This function must not be inlined and must be globally accessible.\n/// To achieve this, we expose it a part of the module definition\n/// (`unpickle_token`) and save reference to is in the `FN_UNPICKLE_TOKEN`.\n///\n/// A bit hackly, but it works.\nstatic FN_UNPICKLE_TOKEN: OnceCell<Py<PyAny>> = OnceCell::new();\n\npub fn fini_module(m: &Bound<PyModule>) {\n    let _unpickle_token = m.getattr(\"unpickle_token\").unwrap();\n    FN_UNPICKLE_TOKEN\n        .set(_unpickle_token.unbind())\n        .expect(\"module is already initialized\");\n}\n\n#[pyfunction]\npub fn unpickle_token(bytes: &Bound<PyBytes>) -> PyResult<OpaqueToken> {\n    let token = bincode::deserialize(bytes.as_bytes())\n        .map_err(|e| PyValueError::new_err(format!(\"Failed to read token: {e}\")))?;\n    Ok(OpaqueToken { inner: token })\n}\n\nfn get_unpickle_token_fn(py: Python) -> Py<PyAny> {\n    let py_function = FN_UNPICKLE_TOKEN.get().expect(\"module uninitialized\");\n    py_function.clone_ref(py)\n}\n"
  },
  {
    "path": "edb/edgeql-parser/edgeql-parser-python/src/unpack.rs",
    "content": "use edgeql_parser::tokenizer::Token;\nuse pyo3::exceptions::PyValueError;\nuse pyo3::prelude::*;\nuse pyo3::types::PyBytes;\n\nuse crate::normalize::PackedEntry;\nuse crate::pynormalize::Entry;\nuse crate::tokenizer::tokens_to_py;\n\n#[pyfunction]\npub fn unpack(py: Python<'_>, serialized: &Bound<PyBytes>) -> PyResult<Py<PyAny>> {\n    let buf = serialized.as_bytes();\n    match buf[0] {\n        0u8 => {\n            let tokens: Vec<Token> = bincode::deserialize(&buf[1..])\n                .map_err(|e| PyValueError::new_err(format!(\"{e}\")))?;\n            Ok(tokens_to_py(py, tokens)?.into_any())\n        }\n        1u8 => {\n            let pack: PackedEntry = bincode::deserialize(&buf[1..])\n                .map_err(|e| PyValueError::new_err(format!(\"Failed to unpack: {e}\")))?;\n            let entry = Entry::new(py, pack.into())?;\n            entry.into_pyobject(py).map(|e| e.unbind().into_any())\n        }\n        _ => Err(PyValueError::new_err(format!(\n            \"Invalid type/version byte: {}\",\n            buf[0]\n        ))),\n    }\n}\n"
  },
  {
    "path": "edb/edgeql-parser/edgeql-parser-python/tests/normalize.rs",
    "content": "#![cfg(feature = \"python_extension\")]\nuse edgeql_parser::tokenizer::Value;\nuse edgeql_rust::normalize::{normalize, Variable};\nuse num_bigint::BigInt;\n\n#[test]\nfn test_verbatim() {\n    let entry = normalize(\n        r###\"\n        SELECT $1 + $2\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(entry.processed_source, \"SELECT$1+$2\");\n    assert_eq!(entry.variables, vec![vec![]]);\n}\n\n#[test]\nfn test_configure() {\n    let entry = normalize(\n        r###\"\n        CONFIGURE INSTANCE SET some_setting := 7\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.processed_source,\n        \"CONFIGURE INSTANCE SET some_setting:=7\"\n    );\n    assert_eq!(entry.variables, vec![] as Vec<Vec<Variable>>);\n}\n\n#[test]\nfn test_int() {\n    let entry = normalize(\n        r###\"\n        SELECT 1 + 2\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(entry.processed_source, \"SELECT <lit int64>$0+<lit int64>$1\");\n    assert_eq!(\n        entry.variables,\n        vec![vec![\n            Variable {\n                value: Value::Int(1),\n            },\n            Variable {\n                value: Value::Int(2),\n            }\n        ]]\n    );\n}\n\n#[test]\nfn test_str() {\n    let entry = normalize(\n        r#\"\n        SELECT \"x\" + \"yy\"\n    \"#,\n    )\n    .unwrap();\n    assert_eq!(entry.processed_source, \"SELECT <lit str>$0+<lit str>$1\");\n    assert_eq!(\n        entry.variables,\n        vec![vec![\n            Variable {\n                value: Value::String(\"x\".into()),\n            },\n            Variable {\n                value: Value::String(\"yy\".into()),\n            }\n        ]]\n    );\n}\n\n#[test]\nfn test_float() {\n    let entry = normalize(\n        r###\"\n        SELECT 1.5 + 23.25\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.processed_source,\n        \"SELECT <lit float64>$0+<lit float64>$1\"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![vec![\n            Variable {\n                value: Value::Float(1.5),\n            },\n            Variable {\n                value: Value::Float(23.25),\n            }\n        ]]\n    );\n}\n\n#[test]\nfn test_bigint() {\n    let entry = normalize(\n        r###\"\n        SELECT 1n + 23n\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.processed_source,\n        \"SELECT <lit bigint>$0+<lit bigint>$1\"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![vec![\n            Variable {\n                value: Value::BigInt(\"1\".into()),\n            },\n            Variable {\n                value: Value::BigInt(BigInt::from(23).to_str_radix(16)),\n            }\n        ]]\n    );\n}\n\n#[test]\nfn test_bigint_exponent() {\n    let entry = normalize(\n        r###\"\n        SELECT 1e10n + 23e13n\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.processed_source,\n        \"SELECT <lit bigint>$0+<lit bigint>$1\"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![vec![\n            Variable {\n                value: Value::BigInt(BigInt::from(10000000000u64).to_str_radix(16)),\n            },\n            Variable {\n                value: Value::BigInt(BigInt::from(230000000000000u64).to_str_radix(16)),\n            }\n        ]]\n    );\n}\n\n#[test]\nfn test_decimal() {\n    let entry = normalize(\n        r###\"\n        SELECT 1.33n + 23.77n\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.processed_source,\n        \"SELECT <lit decimal>$0+<lit decimal>$1\"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![vec![\n            Variable {\n                value: Value::Decimal(\"1.33\".parse().unwrap()),\n            },\n            Variable {\n                value: Value::Decimal(\"23.77\".parse().unwrap()),\n            }\n        ]]\n    );\n}\n\n#[test]\nfn test_positional() {\n    let entry = normalize(\n        r###\"\n        SELECT <int64>$0 + 2\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(entry.processed_source, \"SELECT<int64>$0+<lit int64>$1\");\n    assert_eq!(\n        entry.variables,\n        vec![vec![Variable {\n            value: Value::Int(2),\n        }]]\n    );\n}\n\n#[test]\nfn test_named() {\n    let entry = normalize(\n        r###\"\n        SELECT <int64>$test_var + 2\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.processed_source,\n        \"SELECT<int64>$test_var+<lit int64>$__edb_arg_1\"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![vec![Variable {\n            value: Value::Int(2),\n        }]]\n    );\n}\n\n#[test]\nfn test_limit_1() {\n    let entry = normalize(\n        r###\"\n        SELECT User { one := 1 } LIMIT 1\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.processed_source,\n        \"SELECT User{one:=<lit int64>$0}LIMIT 1\"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![vec![Variable {\n            value: Value::Int(1),\n        },]]\n    );\n}\n\n#[test]\nfn test_tuple_access() {\n    let entry = normalize(\n        r###\"\n        SELECT User { one := 2, two := .field.2, three := .field  . 3 }\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.processed_source,\n        \"SELECT User{one:=<lit int64>$0,\\\n                     two:=.field.2,three:=.field.3}\"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![vec![Variable {\n            value: Value::Int(2),\n        },]]\n    );\n}\n\n#[test]\nfn test_script() {\n    let entry = normalize(\n        r###\"\n        SELECT 1 + 2;\n        SELECT 2;\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.processed_source,\n        \"SELECT <lit int64>$0+<lit int64>$1;\\\n        SELECT <lit int64>$2;\",\n    );\n    assert_eq!(\n        entry.variables,\n        vec![\n            vec![\n                Variable {\n                    value: Value::Int(1),\n                },\n                Variable {\n                    value: Value::Int(2),\n                }\n            ],\n            vec![Variable {\n                value: Value::Int(2),\n            }],\n            vec![]\n        ]\n    );\n}\n\n#[test]\nfn test_script_with_args() {\n    let entry = normalize(\n        r###\"\n        SELECT 2 + $1;\n        SELECT $1 + 2;\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.processed_source,\n        \"SELECT <lit int64>$2+$1;SELECT$1+<lit int64>$3;\",\n    );\n    assert_eq!(\n        entry.variables,\n        vec![\n            vec![Variable {\n                value: Value::Int(2),\n            }],\n            vec![Variable {\n                value: Value::Int(2),\n            }],\n            vec![]\n        ]\n    );\n}\n"
  },
  {
    "path": "edb/edgeql-parser/src/ast.rs",
    "content": "// DO NOT EDIT. This file was generated with:\n//\n// $ edb gen-rust-ast\n\n//! Abstract Syntax Tree for EdgeQL\n#![allow(non_camel_case_types)]\n#![cfg(never)] // TODO: migrate cpython-rust to pyo3\n\nuse indexmap::IndexMap;\n\n#[cfg(feature = \"python\")]\nuse edgeql_parser_derive::IntoPython;\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct OptionValue {\n    pub name: String,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: Option<OptionValueKind>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum OptionValueKind {\n    OptionFlag(OptionFlag),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct OptionFlag {\n    pub val: bool,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Options {\n    pub options: IndexMap<String, OptionValue>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Expr {\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: ExprKind,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum ExprKind {\n    Placeholder(Placeholder),\n    Anchor(Anchor),\n    DetachedExpr(DetachedExpr),\n    GlobalExpr(GlobalExpr),\n    Indirection(Indirection),\n    BinOp(BinOp),\n    FunctionCall(FunctionCall),\n    BaseConstant(BaseConstant),\n    Parameter(Parameter),\n    UnaryOp(UnaryOp),\n    IsOp(IsOp),\n    Path(Path),\n    TypeCast(TypeCast),\n    Introspect(Introspect),\n    IfElse(IfElse),\n    NamedTuple(NamedTuple),\n    Tuple(Tuple),\n    Array(Array),\n    Set(Set),\n    ShapeElement(ShapeElement),\n    Shape(Shape),\n    Query(Query),\n    ConfigOp(ConfigOp),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Placeholder {\n    pub name: String,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct SortExpr {\n    pub path: Box<Expr>,\n    pub direction: Option<SortOrder>,\n    pub nones_order: Option<NonesOrder>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AliasedExpr {\n    pub alias: String,\n    pub expr: Box<Expr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct ModuleAliasDecl {\n    pub module: String,\n    pub alias: Option<String>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct BaseObjectRef {\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: BaseObjectRefKind,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum BaseObjectRefKind {\n    ObjectRef(ObjectRef),\n    PseudoObjectRef(PseudoObjectRef),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct ObjectRef {\n    pub name: String,\n    pub module: Option<String>,\n    pub itemclass: Option<SchemaObjectClass>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct PseudoObjectRef {\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: PseudoObjectRefKind,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum PseudoObjectRefKind {\n    AnyType(AnyType),\n    AnyTuple(AnyTuple),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AnyType {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AnyTuple {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Anchor {\n    pub name: String,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: AnchorKind,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum AnchorKind {\n    SpecialAnchor(SpecialAnchor),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct SpecialAnchor {\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: Option<SpecialAnchorKind>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum SpecialAnchorKind {\n    Source(Source),\n    Subject(Subject),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Source {\n    pub name: String,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Subject {\n    pub name: String,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DetachedExpr {\n    pub expr: Box<Expr>,\n    pub preserve_path_prefix: bool,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct GlobalExpr {\n    pub name: ObjectRef,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Index {\n    pub index: Box<Expr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Slice {\n    pub start: Option<Box<Expr>>,\n    pub stop: Option<Box<Expr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Indirection {\n    pub arg: Box<Expr>,\n    pub indirection: Vec<IndirectionIndirection>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_union)]\npub enum IndirectionIndirection {\n    Index(Index),\n    Slice(Slice),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct BinOp {\n    pub left: Box<Expr>,\n    pub op: String,\n    pub right: Box<Expr>,\n    pub rebalanced: bool,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: Option<BinOpKind>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum BinOpKind {\n    SetConstructorOp(SetConstructorOp),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct SetConstructorOp {\n    pub op: String,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct WindowSpec {\n    pub orderby: Vec<SortExpr>,\n    pub partition: Vec<Box<Expr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct FunctionCall {\n    pub func: FunctionCallFunc,\n    pub args: Vec<Box<Expr>>,\n    pub kwargs: IndexMap<String, Box<Expr>>,\n    pub window: Option<WindowSpec>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_union)]\npub enum FunctionCallFunc {\n    Tuple((String, String)),\n    str(String),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct BaseConstant {\n    pub value: String,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: BaseConstantKind,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum BaseConstantKind {\n    StringConstant(StringConstant),\n    BaseRealConstant(BaseRealConstant),\n    BooleanConstant(BooleanConstant),\n    BytesConstant(BytesConstant),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct StringConstant {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct BaseRealConstant {\n    pub is_negative: bool,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: BaseRealConstantKind,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum BaseRealConstantKind {\n    IntegerConstant(IntegerConstant),\n    FloatConstant(FloatConstant),\n    BigintConstant(BigintConstant),\n    DecimalConstant(DecimalConstant),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct IntegerConstant {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct FloatConstant {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct BigintConstant {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DecimalConstant {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct BooleanConstant {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct BytesConstant {\n    pub value: Vec<u8>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Parameter {\n    pub name: String,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct UnaryOp {\n    pub op: String,\n    pub operand: Box<Expr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct TypeExpr {\n    pub name: Option<String>,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: Option<TypeExprKind>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum TypeExprKind {\n    TypeOf(TypeOf),\n    TypeExprLiteral(TypeExprLiteral),\n    TypeName(TypeName),\n    TypeOp(TypeOp),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct TypeOf {\n    pub expr: Box<Expr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct TypeExprLiteral {\n    pub val: BaseConstant,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct TypeName {\n    pub maintype: BaseObjectRef,\n    pub subtypes: Option<Vec<TypeExpr>>,\n    pub dimensions: Option<Vec<i64>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct TypeOp {\n    pub left: Box<TypeExpr>,\n    pub op: String,\n    pub right: Box<TypeExpr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct FuncParam {\n    pub name: String,\n    pub r#type: TypeExpr,\n    pub typemod: TypeModifier,\n    pub kind: ParameterKind,\n    pub default: Option<Box<Expr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct IsOp {\n    pub left: Box<Expr>,\n    pub op: String,\n    pub right: TypeExpr,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct TypeIntersection {\n    pub r#type: TypeExpr,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Ptr {\n    pub ptr: ObjectRef,\n    pub direction: Option<String>,\n    pub r#type: Option<String>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Splat {\n    pub depth: i64,\n    pub r#type: Option<TypeExpr>,\n    pub intersection: Option<TypeIntersection>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Path {\n    pub steps: Vec<PathSteps>,\n    pub partial: bool,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_union)]\npub enum PathSteps {\n    Expr(Box<Expr>),\n    Ptr(Ptr),\n    TypeIntersection(TypeIntersection),\n    ObjectRef(ObjectRef),\n    Splat(Splat),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct TypeCast {\n    pub expr: Box<Expr>,\n    pub r#type: TypeExpr,\n    pub cardinality_mod: Option<CardinalityModifier>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Introspect {\n    pub r#type: TypeExpr,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct IfElse {\n    pub condition: Box<Expr>,\n    pub if_expr: Box<Expr>,\n    pub else_expr: Box<Expr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct TupleElement {\n    pub name: ObjectRef,\n    pub val: Box<Expr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct NamedTuple {\n    pub elements: Vec<TupleElement>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Tuple {\n    pub elements: Vec<Box<Expr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Array {\n    pub elements: Vec<Box<Expr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Set {\n    pub elements: Vec<Box<Expr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Command {\n    pub aliases: Option<Vec<CommandAliases>>,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: CommandKind,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_union)]\npub enum CommandAliases {\n    AliasedExpr(AliasedExpr),\n    ModuleAliasDecl(ModuleAliasDecl),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum CommandKind {\n    SessionSetAliasDecl(SessionSetAliasDecl),\n    SessionResetAliasDecl(SessionResetAliasDecl),\n    SessionResetModule(SessionResetModule),\n    SessionResetAllAliases(SessionResetAllAliases),\n    DDLCommand(DDLCommand),\n    DescribeStmt(DescribeStmt),\n    ExplainStmt(ExplainStmt),\n    AdministerStmt(AdministerStmt),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct SessionSetAliasDecl {\n    pub decl: ModuleAliasDecl,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct SessionResetAliasDecl {\n    pub alias: String,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct SessionResetModule {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct SessionResetAllAliases {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct ShapeOperation {\n    pub op: ShapeOp,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct ShapeElement {\n    pub expr: Path,\n    pub elements: Option<Vec<ShapeElement>>,\n    pub compexpr: Option<Box<Expr>>,\n    pub cardinality: Option<SchemaCardinality>,\n    pub required: Option<bool>,\n    pub operation: ShapeOperation,\n    pub origin: ShapeOrigin,\n    pub r#where: Option<Box<Expr>>,\n    pub orderby: Option<Vec<SortExpr>>,\n    pub offset: Option<Box<Expr>>,\n    pub limit: Option<Box<Expr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Shape {\n    pub expr: Option<Box<Expr>>,\n    pub elements: Vec<ShapeElement>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Query {\n    pub aliases: Option<Vec<QueryAliases>>,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: QueryKind,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_union)]\npub enum QueryAliases {\n    AliasedExpr(AliasedExpr),\n    ModuleAliasDecl(ModuleAliasDecl),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum QueryKind {\n    PipelinedQuery(PipelinedQuery),\n    GroupQuery(GroupQuery),\n    InsertQuery(InsertQuery),\n    UpdateQuery(UpdateQuery),\n    ForQuery(ForQuery),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct PipelinedQuery {\n    pub implicit: bool,\n    pub r#where: Option<Box<Expr>>,\n    pub orderby: Option<Vec<SortExpr>>,\n    pub offset: Option<Box<Expr>>,\n    pub limit: Option<Box<Expr>>,\n    pub rptr_passthrough: bool,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: PipelinedQueryKind,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum PipelinedQueryKind {\n    SelectQuery(SelectQuery),\n    DeleteQuery(DeleteQuery),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct SelectQuery {\n    pub result_alias: Option<String>,\n    pub result: Box<Expr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct GroupingIdentList {\n    pub elements: Vec<GroupingIdentListElements>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_union)]\npub enum GroupingIdentListElements {\n    ObjectRef(ObjectRef),\n    Path(Path),\n    GroupingIdentList(GroupingIdentList),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct GroupingElement {\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: GroupingElementKind,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum GroupingElementKind {\n    GroupingSimple(GroupingSimple),\n    GroupingSets(GroupingSets),\n    GroupingOperation(GroupingOperation),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct GroupingSimple {\n    pub element: GroupingSimpleElement,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_union)]\npub enum GroupingSimpleElement {\n    ObjectRef(ObjectRef),\n    Path(Path),\n    GroupingIdentList(GroupingIdentList),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct GroupingSets {\n    pub sets: Vec<GroupingElement>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct GroupingOperation {\n    pub oper: String,\n    pub elements: Vec<GroupingOperationElements>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_union)]\npub enum GroupingOperationElements {\n    ObjectRef(ObjectRef),\n    Path(Path),\n    GroupingIdentList(GroupingIdentList),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct GroupQuery {\n    pub subject_alias: Option<String>,\n    pub using: Option<Vec<AliasedExpr>>,\n    pub by: Vec<GroupingElement>,\n    pub subject: Box<Expr>,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: Option<GroupQueryKind>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum GroupQueryKind {\n    InternalGroupQuery(InternalGroupQuery),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct InternalGroupQuery {\n    pub group_alias: String,\n    pub grouping_alias: Option<String>,\n    pub from_desugaring: bool,\n    pub result_alias: Option<String>,\n    pub result: Box<Expr>,\n    pub r#where: Option<Box<Expr>>,\n    pub orderby: Option<Vec<SortExpr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct InsertQuery {\n    pub subject: ObjectRef,\n    pub shape: Vec<ShapeElement>,\n    pub unless_conflict: Option<(Option<Box<Expr>>, Option<Box<Expr>>)>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct UpdateQuery {\n    pub shape: Vec<ShapeElement>,\n    pub subject: Box<Expr>,\n    pub r#where: Option<Box<Expr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DeleteQuery {\n    pub subject: Box<Expr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct ForQuery {\n    pub iterator: Box<Expr>,\n    pub iterator_alias: String,\n    pub result_alias: Option<String>,\n    pub result: Box<Expr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Transaction {\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: TransactionKind,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum TransactionKind {\n    StartTransaction(StartTransaction),\n    CommitTransaction(CommitTransaction),\n    RollbackTransaction(RollbackTransaction),\n    DeclareSavepoint(DeclareSavepoint),\n    RollbackToSavepoint(RollbackToSavepoint),\n    ReleaseSavepoint(ReleaseSavepoint),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct StartTransaction {\n    pub isolation: Option<TransactionIsolationLevel>,\n    pub access: Option<TransactionAccessMode>,\n    pub deferrable: Option<TransactionDeferMode>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CommitTransaction {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct RollbackTransaction {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DeclareSavepoint {\n    pub name: String,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct RollbackToSavepoint {\n    pub name: String,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct ReleaseSavepoint {\n    pub name: String,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Position {\n    pub r#ref: Option<ObjectRef>,\n    pub position: String,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DDLOperation {\n    pub commands: Vec<DDLOperation>,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: DDLOperationKind,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum DDLOperationKind {\n    DDLCommand(DDLCommand),\n    AlterAddInherit(AlterAddInherit),\n    AlterDropInherit(AlterDropInherit),\n    OnTargetDelete(OnTargetDelete),\n    OnSourceDelete(OnSourceDelete),\n    SetField(SetField),\n    SetAccessPerms(SetAccessPerms),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DDLCommand {\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: DDLCommandKind,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum DDLCommandKind {\n    NamedDDL(NamedDDL),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterAddInherit {\n    pub position: Option<Position>,\n    pub bases: Vec<TypeExpr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterDropInherit {\n    pub bases: Vec<TypeExpr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct OnTargetDelete {\n    pub cascade: Option<LinkTargetDeleteAction>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct OnSourceDelete {\n    pub cascade: Option<LinkSourceDeleteAction>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct SetField {\n    pub name: String,\n    pub value: SetFieldValue,\n    pub special_syntax: bool,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: Option<SetFieldKind>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_union)]\npub enum SetFieldValue {\n    Expr(Box<Expr>),\n    TypeExpr(TypeExpr),\n    NoneType(()),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum SetFieldKind {\n    SetPointerType(SetPointerType),\n    SetPointerCardinality(SetPointerCardinality),\n    SetPointerOptionality(SetPointerOptionality),\n    SetGlobalType(SetGlobalType),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct SetPointerType {\n    pub name: String,\n    pub value: Option<TypeExpr>,\n    pub special_syntax: bool,\n    pub cast_expr: Option<Box<Expr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct SetPointerCardinality {\n    pub name: String,\n    pub special_syntax: bool,\n    pub conv_expr: Option<Box<Expr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct SetPointerOptionality {\n    pub name: String,\n    pub special_syntax: bool,\n    pub fill_expr: Option<Box<Expr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct NamedDDL {\n    pub name: ObjectRef,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: NamedDDLKind,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum NamedDDLKind {\n    ObjectDDL(ObjectDDL),\n    Rename(Rename),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct ObjectDDL {\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: ObjectDDLKind,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum ObjectDDLKind {\n    CreateObject(CreateObject),\n    AlterObject(AlterObject),\n    DropObject(DropObject),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateObject {\n    pub r#abstract: bool,\n    pub sdl_alter_if_exists: bool,\n    pub create_if_not_exists: bool,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: Option<CreateObjectKind>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum CreateObjectKind {\n    CreateExtendingObject(CreateExtendingObject),\n    CreateMigration(CreateMigration),\n    CreateDatabase(CreateDatabase),\n    CreateExtensionPackage(CreateExtensionPackage),\n    CreateExtension(CreateExtension),\n    CreateFuture(CreateFuture),\n    CreateModule(CreateModule),\n    CreateRole(CreateRole),\n    CreatePseudoType(CreatePseudoType),\n    CreateConcretePointer(CreateConcretePointer),\n    CreateAlias(CreateAlias),\n    CreateGlobal(CreateGlobal),\n    CreatePermission(CreatePermission),\n    CreateConcreteConstraint(CreateConcreteConstraint),\n    CreateConcreteIndex(CreateConcreteIndex),\n    CreateAnnotationValue(CreateAnnotationValue),\n    CreateAccessPolicy(CreateAccessPolicy),\n    CreateTrigger(CreateTrigger),\n    CreateRewrite(CreateRewrite),\n    CreateFunction(CreateFunction),\n    CreateOperator(CreateOperator),\n    CreateCast(CreateCast),\n    CreateIndexMatch(CreateIndexMatch),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterObject {\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: Option<AlterObjectKind>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum AlterObjectKind {\n    AlterMigration(AlterMigration),\n    AlterDatabase(AlterDatabase),\n    AlterModule(AlterModule),\n    AlterRole(AlterRole),\n    AlterAnnotation(AlterAnnotation),\n    AlterScalarType(AlterScalarType),\n    AlterProperty(AlterProperty),\n    AlterConcreteProperty(AlterConcreteProperty),\n    AlterObjectType(AlterObjectType),\n    AlterAlias(AlterAlias),\n    AlterGlobal(AlterGlobal),\n    AlterPermission(AlterPermission),\n    AlterLink(AlterLink),\n    AlterConcreteLink(AlterConcreteLink),\n    AlterConstraint(AlterConstraint),\n    AlterConcreteConstraint(AlterConcreteConstraint),\n    AlterIndex(AlterIndex),\n    AlterConcreteIndex(AlterConcreteIndex),\n    AlterAnnotationValue(AlterAnnotationValue),\n    AlterAccessPolicy(AlterAccessPolicy),\n    AlterTrigger(AlterTrigger),\n    AlterRewrite(AlterRewrite),\n    AlterFunction(AlterFunction),\n    AlterOperator(AlterOperator),\n    AlterCast(AlterCast),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropObject {\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: Option<DropObjectKind>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum DropObjectKind {\n    DropMigration(DropMigration),\n    DropDatabase(DropDatabase),\n    DropExtensionPackage(DropExtensionPackage),\n    DropExtension(DropExtension),\n    DropFuture(DropFuture),\n    DropModule(DropModule),\n    DropRole(DropRole),\n    DropAnnotation(DropAnnotation),\n    DropScalarType(DropScalarType),\n    DropProperty(DropProperty),\n    DropConcreteProperty(DropConcreteProperty),\n    DropObjectType(DropObjectType),\n    DropAlias(DropAlias),\n    DropGlobal(DropGlobal),\n    DropPermission(DropPermission),\n    DropLink(DropLink),\n    DropConcreteLink(DropConcreteLink),\n    DropConstraint(DropConstraint),\n    DropConcreteConstraint(DropConcreteConstraint),\n    DropIndex(DropIndex),\n    DropConcreteIndex(DropConcreteIndex),\n    DropAnnotationValue(DropAnnotationValue),\n    DropAccessPolicy(DropAccessPolicy),\n    DropTrigger(DropTrigger),\n    DropRewrite(DropRewrite),\n    DropFunction(DropFunction),\n    DropOperator(DropOperator),\n    DropCast(DropCast),\n    DropIndexMatch(DropIndexMatch),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateExtendingObject {\n    pub r#final: bool,\n    pub bases: Vec<TypeExpr>,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: Option<CreateExtendingObjectKind>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum CreateExtendingObjectKind {\n    CreateAnnotation(CreateAnnotation),\n    CreateScalarType(CreateScalarType),\n    CreateProperty(CreateProperty),\n    CreateObjectType(CreateObjectType),\n    CreateLink(CreateLink),\n    CreateConcreteLink(CreateConcreteLink),\n    CreateConstraint(CreateConstraint),\n    CreateIndex(CreateIndex),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Rename {\n    pub new_name: ObjectRef,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct NestedQLBlock {\n    pub commands: Vec<DDLOperation>,\n    pub text: Option<String>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateMigration {\n    pub body: NestedQLBlock,\n    pub parent: Option<ObjectRef>,\n    pub metadata_only: bool,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CommittedSchema {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct StartMigration {\n    pub target: StartMigrationTarget,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_union)]\npub enum StartMigrationTarget {\n    Schema(Schema),\n    CommittedSchema(CommittedSchema),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AbortMigration {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct PopulateMigration {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterCurrentMigrationRejectProposed {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DescribeCurrentMigration {\n    pub language: DescribeLanguage,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CommitMigration {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterMigration {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropMigration {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct ResetSchema {\n    pub target: ObjectRef,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct StartMigrationRewrite {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AbortMigrationRewrite {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CommitMigrationRewrite {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateDatabase {\n    pub template: Option<ObjectRef>,\n    pub branch_type: BranchType,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterDatabase {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropDatabase {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateExtensionPackage {\n    pub body: NestedQLBlock,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropExtensionPackage {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateExtension {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropExtension {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateFuture {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropFuture {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateModule {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterModule {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropModule {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateRole {\n    pub superuser: bool,\n    pub bases: Vec<TypeExpr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterRole {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropRole {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateAnnotation {\n    pub r#type: Option<TypeExpr>,\n    pub inheritable: bool,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterAnnotation {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropAnnotation {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreatePseudoType {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateScalarType {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterScalarType {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropScalarType {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateProperty {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterProperty {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropProperty {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateConcretePointer {\n    pub is_required: Option<bool>,\n    pub declared_overloaded: bool,\n    pub target: CreateConcretePointerTarget,\n    pub cardinality: SchemaCardinality,\n    pub bases: Vec<TypeExpr>,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: Option<CreateConcretePointerKind>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_union)]\npub enum CreateConcretePointerTarget {\n    Expr(Box<Expr>),\n    TypeExpr(TypeExpr),\n    NoneType(()),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum CreateConcretePointerKind {\n    CreateConcreteUnknownPointer(CreateConcreteUnknownPointer),\n    CreateConcreteProperty(CreateConcreteProperty),\n    CreateConcreteLink(CreateConcreteLink),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateConcreteUnknownPointer {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateConcreteProperty {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterConcreteProperty {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropConcreteProperty {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateObjectType {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterObjectType {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropObjectType {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateAlias {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterAlias {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropAlias {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateGlobal {\n    pub is_required: Option<bool>,\n    pub target: CreateGlobalTarget,\n    pub cardinality: Option<SchemaCardinality>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_union)]\npub enum CreateGlobalTarget {\n    Expr(Box<Expr>),\n    TypeExpr(TypeExpr),\n    NoneType(()),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterGlobal {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropGlobal {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct SetGlobalType {\n    pub name: String,\n    pub value: Option<TypeExpr>,\n    pub special_syntax: bool,\n    pub cast_expr: Option<Box<Expr>>,\n    pub reset_value: bool,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreatePermission {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterPermission {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropPermission {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateLink {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterLink {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropLink {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateConcreteLink {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterConcreteLink {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropConcreteLink {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateConstraint {\n    pub r#abstract: bool,\n    pub subjectexpr: Option<Box<Expr>>,\n    pub params: Vec<FuncParam>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterConstraint {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropConstraint {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateConcreteConstraint {\n    pub delegated: bool,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterConcreteConstraint {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropConcreteConstraint {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct IndexType {\n    pub name: ObjectRef,\n    pub args: Vec<Box<Expr>>,\n    pub kwargs: IndexMap<String, Box<Expr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct IndexCode {\n    pub language: Language,\n    pub code: String,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateIndex {\n    pub kwargs: IndexMap<String, Box<Expr>>,\n    pub index_types: Vec<IndexType>,\n    pub code: Option<IndexCode>,\n    pub params: Vec<FuncParam>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterIndex {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropIndex {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateConcreteIndex {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterConcreteIndex {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropConcreteIndex {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateIndexMatch {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropIndexMatch {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateAnnotationValue {\n    pub value: Box<Expr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterAnnotationValue {\n    pub value: Option<Box<Expr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropAnnotationValue {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateAccessPolicy {\n    pub condition: Option<Box<Expr>>,\n    pub action: AccessPolicyAction,\n    pub access_kinds: Vec<AccessKind>,\n    pub expr: Option<Box<Expr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct SetAccessPerms {\n    pub access_kinds: Vec<AccessKind>,\n    pub action: AccessPolicyAction,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterAccessPolicy {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropAccessPolicy {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateTrigger {\n    pub timing: TriggerTiming,\n    pub kinds: Vec<TriggerKind>,\n    pub scope: TriggerScope,\n    pub expr: Box<Expr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterTrigger {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropTrigger {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateRewrite {\n    pub expr: Box<Expr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterRewrite {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropRewrite {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct FunctionCode {\n    pub language: Language,\n    pub code: Option<String>,\n    pub nativecode: Option<Box<Expr>>,\n    pub from_function: Option<String>,\n    pub from_expr: bool,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateFunction {\n    pub returning: TypeExpr,\n    pub code: FunctionCode,\n    pub nativecode: Option<Box<Expr>>,\n    pub returning_typemod: TypeModifier,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterFunction {\n    pub code: FunctionCode,\n    pub nativecode: Option<Box<Expr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropFunction {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct OperatorCode {\n    pub language: Language,\n    pub from_operator: Option<Vec<String>>,\n    pub from_function: Option<Vec<String>>,\n    pub from_expr: bool,\n    pub code: Option<String>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateOperator {\n    pub returning: TypeExpr,\n    pub returning_typemod: TypeModifier,\n    pub code: OperatorCode,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterOperator {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropOperator {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CastCode {\n    pub language: Language,\n    pub from_function: String,\n    pub from_expr: bool,\n    pub from_cast: bool,\n    pub code: String,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct CreateCast {\n    pub code: CastCode,\n    pub allow_implicit: bool,\n    pub allow_assignment: bool,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AlterCast {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DropCast {}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct ConfigOp {\n    pub name: ObjectRef,\n    pub scope: ConfigScope,\n    #[cfg_attr(feature = \"python\", py_child)]\n    pub kind: ConfigOpKind,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_child)]\npub enum ConfigOpKind {\n    ConfigSet(ConfigSet),\n    ConfigInsert(ConfigInsert),\n    ConfigReset(ConfigReset),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct ConfigSet {\n    pub expr: Box<Expr>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct ConfigInsert {\n    pub shape: Vec<ShapeElement>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct ConfigReset {\n    pub r#where: Option<Box<Expr>>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct DescribeStmt {\n    pub language: DescribeLanguage,\n    pub object: DescribeStmtObject,\n    pub options: Options,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_union)]\npub enum DescribeStmtObject {\n    ObjectRef(ObjectRef),\n    DescribeGlobal(DescribeGlobal),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct ExplainStmt {\n    pub args: Option<NamedTuple>,\n    pub query: Query,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct AdministerStmt {\n    pub expr: FunctionCall,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct ModuleDeclaration {\n    pub name: ObjectRef,\n    pub declarations: Vec<ModuleDeclarationDeclarations>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_union)]\npub enum ModuleDeclarationDeclarations {\n    NamedDDL(DDLOperation),\n    ModuleDeclaration(ModuleDeclaration),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\npub struct Schema {\n    pub declarations: Vec<SchemaDeclarations>,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_union)]\npub enum SchemaDeclarations {\n    NamedDDL(DDLOperation),\n    ModuleDeclaration(ModuleDeclaration),\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qlast.SortOrder))]\npub enum SortOrder {\n    Asc,\n    Desc,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qlast.NonesOrder))]\npub enum NonesOrder {\n    First,\n    Last,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qlast.CardinalityModifier))]\npub enum CardinalityModifier {\n    Optional,\n    Required,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qlast.DescribeGlobal))]\npub enum DescribeGlobal {\n    Schema,\n    DatabaseConfig,\n    InstanceConfig,\n    Roles,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qlast.ShapeOp))]\npub enum ShapeOp {\n    APPEND,\n    SUBTRACT,\n    ASSIGN,\n    MATERIALIZE,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qlast.ShapeOrigin))]\npub enum ShapeOrigin {\n    EXPLICIT,\n    DEFAULT,\n    SPLAT_EXPANSION,\n    MATERIALIZATION,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qlast.Language))]\npub enum Language {\n    SQL,\n    EdgeQL,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.ParameterKind))]\npub enum ParameterKind {\n    VariadicParam,\n    NamedOnlyParam,\n    PositionalParam,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.TypeModifier))]\npub enum TypeModifier {\n    SetOfType,\n    OptionalType,\n    SingletonType,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.OperatorKind))]\npub enum OperatorKind {\n    Infix,\n    Postfix,\n    Prefix,\n    Ternary,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.TransactionIsolationLevel))]\npub enum TransactionIsolationLevel {\n    REPEATABLE_READ,\n    SERIALIZABLE,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.TransactionAccessMode))]\npub enum TransactionAccessMode {\n    READ_WRITE,\n    READ_ONLY,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.TransactionDeferMode))]\npub enum TransactionDeferMode {\n    DEFERRABLE,\n    NOT_DEFERRABLE,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.SchemaCardinality))]\npub enum SchemaCardinality {\n    One,\n    Many,\n    Unknown,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.Cardinality))]\npub enum Cardinality {\n    AT_MOST_ONE,\n    ONE,\n    MANY,\n    AT_LEAST_ONE,\n    UNKNOWN,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.Volatility))]\npub enum Volatility {\n    Immutable,\n    Stable,\n    Volatile,\n    Modifying,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.Multiplicity))]\npub enum Multiplicity {\n    EMPTY,\n    UNIQUE,\n    DUPLICATE,\n    UNKNOWN,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.AccessPolicyAction))]\npub enum AccessPolicyAction {\n    Allow,\n    Deny,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.AccessKind))]\npub enum AccessKind {\n    Select,\n    UpdateRead,\n    UpdateWrite,\n    Delete,\n    Insert,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.TriggerTiming))]\npub enum TriggerTiming {\n    After,\n    AfterCommitOf,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.TriggerKind))]\npub enum TriggerKind {\n    Update,\n    Delete,\n    Insert,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.TriggerScope))]\npub enum TriggerScope {\n    Each,\n    All,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.RewriteKind))]\npub enum RewriteKind {\n    Update,\n    Insert,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.DescribeLanguage))]\npub enum DescribeLanguage {\n    DDL,\n    SDL,\n    TEXT,\n    JSON,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.SchemaObjectClass))]\npub enum SchemaObjectClass {\n    ACCESS_POLICY,\n    ALIAS,\n    ANNOTATION,\n    ARRAY_TYPE,\n    CAST,\n    CONSTRAINT,\n    DATABASE,\n    EXTENSION,\n    EXTENSION_PACKAGE,\n    FUTURE,\n    FUNCTION,\n    GLOBAL,\n    INDEX,\n    LINK,\n    MIGRATION,\n    MODULE,\n    OPERATOR,\n    PARAMETER,\n    PERMISSION,\n    PROPERTY,\n    PSEUDO_TYPE,\n    RANGE_TYPE,\n    REWRITE,\n    ROLE,\n    SCALAR_TYPE,\n    TRIGGER,\n    TUPLE_TYPE,\n    TYPE,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.LinkTargetDeleteAction))]\npub enum LinkTargetDeleteAction {\n    Restrict,\n    DeleteSource,\n    Allow,\n    DeferredRestrict,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.LinkSourceDeleteAction))]\npub enum LinkSourceDeleteAction {\n    DeleteTarget,\n    Allow,\n    DeleteTargetIfOrphan,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qltypes.ConfigScope))]\npub enum ConfigScope {\n    INSTANCE,\n    DATABASE,\n    SESSION,\n    GLOBAL,\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"python\", derive(IntoPython))]\n#[cfg_attr(feature = \"python\", py_enum(qlast.BranchType))]\npub enum BranchType {\n    EMPTY,\n    SCHEMA,\n    DATA,\n}\n"
  },
  {
    "path": "edb/edgeql-parser/src/expr.rs",
    "content": "use crate::position::{InflatedPos, Pos};\nuse crate::tokenizer::{self, Kind};\n\n/// Error of expression checking\n///\n/// See [check][].\n#[derive(Debug, thiserror::Error)]\npub enum Error {\n    #[error(\"{}: tokenizer error: {}\", _1, _0)]\n    Tokenizer(String, Pos),\n    #[error(\n        \"{}: closing bracket mismatch, opened {:?} at {}, encountered {:?}\",\n        closing_pos,\n        opened,\n        opened_pos,\n        encountered\n    )]\n    BracketMismatch {\n        opened: &'static str,\n        encountered: &'static str,\n        opened_pos: Pos,\n        closing_pos: Pos,\n    },\n    #[error(\"{}: extra closing bracket {:?}\", _1, _0)]\n    ExtraBracket(&'static str, Pos),\n    #[error(\"{}: bracket {:?} has never been closed\", _1, _0)]\n    MissingBracket(&'static str, Pos),\n    #[error(\n        \"{}: token {:?} is not allowed in expression \\\n             (try parenthesize the expression)\",\n        _1,\n        _0\n    )]\n    UnexpectedToken(String, Pos),\n    #[error(\"expression is empty\")]\n    Empty,\n}\n\nfn bracket_str(tok: Kind) -> &'static str {\n    use crate::tokenizer::Kind::*;\n\n    match tok {\n        OpenBracket => \"[\",\n        CloseBracket => \"]\",\n        OpenBrace => \"{\",\n        CloseBrace => \"}\",\n        OpenParen => \"(\",\n        CloseParen => \")\",\n        _ => unreachable!(\"token is not a bracket\"),\n    }\n}\n\nfn matching_bracket(tok: Kind) -> Kind {\n    use crate::tokenizer::Kind::*;\n\n    match tok {\n        OpenBracket => CloseBracket,\n        OpenBrace => CloseBrace,\n        OpenParen => CloseParen,\n        _ => unreachable!(\"token is not a bracket\"),\n    }\n}\n\n/// Minimal validation of expression\n///\n/// This is used for substitutions in migrations. This check merely ensures\n/// that overall structure of the statement is not ruined. Mostly checks for\n/// matching brackets and quotes closed.\n///\n/// More specificaly current implementation checks that expression is not\n/// empty, checks for valid tokens, matching braces and disallows comma `,`and\n/// semicolon `;` outside of brackets.\n///\n/// This is NOT a security measure.\npub fn check(text: &str) -> Result<(), Error> {\n    use crate::tokenizer::Kind::*;\n    use Error::*;\n\n    let mut brackets = Vec::new();\n    let mut parser = &mut tokenizer::Tokenizer::new(text);\n    let mut empty = true;\n    for token in &mut parser {\n        let token = match token {\n            Ok(t) => t,\n            Err(crate::tokenizer::Error { message, .. }) => {\n                return Err(Tokenizer(message, parser.current_pos()));\n            }\n        };\n        let pos = token.span.start;\n        let pos = InflatedPos::from_offset(text.as_bytes(), pos)\n            .unwrap()\n            .deflate();\n\n        empty = false;\n        match token.kind {\n            Comma | Semicolon if brackets.is_empty() => {\n                return Err(UnexpectedToken(token.text.into(), pos));\n            }\n            OpenParen | OpenBracket | OpenBrace => {\n                brackets.push((token.kind, pos));\n            }\n            CloseParen | CloseBracket | CloseBrace => match brackets.pop() {\n                Some((opened, opened_pos)) => {\n                    if matching_bracket(opened) != token.kind {\n                        return Err(BracketMismatch {\n                            opened: bracket_str(opened),\n                            opened_pos,\n                            encountered: bracket_str(token.kind),\n                            closing_pos: pos,\n                        });\n                    }\n                }\n                None => {\n                    return Err(ExtraBracket(bracket_str(token.kind), pos));\n                }\n            },\n            _ => {}\n        }\n    }\n    if let Some((bracket, pos)) = brackets.pop() {\n        return Err(MissingBracket(bracket_str(bracket), pos));\n    }\n    if empty {\n        return Err(Empty);\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "edb/edgeql-parser/src/hash.rs",
    "content": "use sha2::digest::Digest;\n\nuse crate::position::Pos;\nuse crate::tokenizer::Tokenizer;\n\n#[derive(Debug, Clone)]\npub struct Hasher {\n    hasher: sha2::Sha256,\n}\n\n#[derive(Debug)]\npub enum Error {\n    // TODO: use [crate::Error] instead\n    Tokenizer(String, Pos),\n}\n\nimpl Hasher {\n    pub fn start_migration(parent_id: &str) -> Hasher {\n        let mut me = Hasher {\n            hasher: sha2::Sha256::new(),\n        };\n        me.hasher.update(b\"CREATE\\0MIGRATION\\0ONTO\\0\");\n        me.hasher.update(parent_id.as_bytes());\n        me.hasher.update(b\"\\0{\\0\");\n        me\n    }\n    pub fn add_source(&mut self, data: &str) -> Result<&mut Self, Error> {\n        let mut parser = &mut Tokenizer::new(data);\n        for token in &mut parser {\n            let token = match token {\n                Ok(t) => t,\n                Err(crate::tokenizer::Error { message, .. }) => {\n                    return Err(Error::Tokenizer(message, parser.current_pos()));\n                }\n            };\n            self.hasher.update(token.text.as_bytes());\n            self.hasher.update(b\"\\0\");\n        }\n        Ok(self)\n    }\n    pub fn make_migration_id(mut self) -> String {\n        self.hasher.update(b\"}\\0\");\n        let hash = base32::encode(\n            base32::Alphabet::Rfc4648 { padding: false },\n            &self.hasher.finalize(),\n        );\n        format!(\"m1{}\", hash.to_ascii_lowercase())\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::Hasher;\n\n    fn hash(initial: &str, text: &str) -> String {\n        let mut hasher = Hasher::start_migration(initial);\n        hasher.add_source(text).unwrap();\n        hasher.make_migration_id()\n    }\n\n    #[test]\n    fn empty() {\n        assert_eq!(\n            hash(\"initial\", \"    \\n   \"),\n            \"m1tjyzfl33vvzwjd5izo5nyp4zdsekyvxpdm7zhtt5ufmqjzczopdq\"\n        );\n    }\n\n    #[test]\n    fn hash_1() {\n        assert_eq!(\n            hash(\n                \"m1g3qzqdr57pp3w2mdwdkq4g7dq4oefawqdavzgeiov7fiwntpb3lq\",\n                r###\"\n                CREATE TYPE Type1;\n            \"###\n            ),\n            \"m1fvpcra5cxntkss3k2to2yfu7pit3t3owesvdw2nysqvvpihdiszq\"\n        );\n    }\n\n    #[test]\n    fn tokens_arent_normalized() {\n        assert_eq!(\n            hash(\n                \"m1g3qzqdr57pp3w2mdwdkq4g7dq4oefawqdavzgeiov7fiwntpb3lq\",\n                r###\"\n                CREATE type Type1;\n            \"###\n            ),\n            \"m1ddghtidugdk3mazwfzpfblqzuoqvsxpivgy2fbq4vywykab7z5rq\"\n        );\n\n        assert_eq!(\n            hash(\n                \"m1g3qzqdr57pp3w2mdwdkq4g7dq4oefawqdavzgeiov7fiwntpb3lq\",\n                r###\"\n                creATE TyPe Type1;\n            \"###\n            ),\n            \"m1oc32ytxeqlvxeyps3ozqiqazy2duuz5bcqog7nkhubmkbsjgf4vq\"\n        );\n    }\n\n    #[test]\n    fn hash_parent() {\n        assert_eq!(\n            hash(\n                \"initial\",\n                r###\"\n                CREATE TYPE Type1;\n            \"###\n            ),\n            \"m1q3jjfe7zjl74v3n2vxjwzneousdas6vvd4qwrfd6j6xmhmktyada\"\n        );\n    }\n}\n"
  },
  {
    "path": "edb/edgeql-parser/src/helpers/bytes.rs",
    "content": "pub fn unquote_bytes(value: &str) -> Result<Vec<u8>, String> {\n    let idx = value\n        .find(['\\'', '\"'])\n        .ok_or_else(|| \"invalid bytes literal: missing quotes\".to_string())?;\n    let prefix = &value[..idx];\n    match prefix {\n        \"br\" | \"rb\" => Ok(value.as_bytes()[3..value.len() - 1].to_vec()),\n        \"b\" => Ok(unquote_bytes_inner(&value[2..value.len() - 1])?),\n        _ => Err(\n            format_args!(\"prefix {prefix:?} is not allowed for bytes, allowed: `b`, `rb`\",)\n                .to_string(),\n        ),\n    }\n}\n\nfn unquote_bytes_inner(s: &str) -> Result<Vec<u8>, String> {\n    let mut res = Vec::with_capacity(s.len());\n    let mut bytes = s.as_bytes().iter();\n    while let Some(&c) = bytes.next() {\n        match c {\n            b'\\\\' => {\n                match *bytes.next().expect(\"slash cant be at the end\") {\n                    c @ b'\"' | c @ b'\\\\' | c @ b'/' | c @ b'\\'' => res.push(c),\n                    b'b' => res.push(b'\\x08'),\n                    b'f' => res.push(b'\\x0C'),\n                    b'n' => res.push(b'\\n'),\n                    b'r' => res.push(b'\\r'),\n                    b't' => res.push(b'\\t'),\n                    b'x' => {\n                        let tail = &s[s.len() - bytes.as_slice().len()..];\n                        let hex = tail.get(0..2);\n                        let code = hex\n                            .and_then(|s| u8::from_str_radix(s, 16).ok())\n                            .ok_or_else(|| {\n                                format!(\n                                    \"invalid bytes literal: \\\n                                invalid escape sequence '\\\\x{}'\",\n                                    hex.unwrap_or(tail).escape_debug()\n                                )\n                            })?;\n                        res.push(code);\n                        bytes.nth(1);\n                    }\n                    b'\\r' | b'\\n' => {\n                        let nskip = bytes\n                            .as_slice()\n                            .iter()\n                            .take_while(|&&x| x.is_ascii_whitespace())\n                            .count();\n                        if nskip > 0 {\n                            bytes.nth(nskip - 1);\n                        }\n                    }\n                    c => {\n                        let ch = if c < 0x7f {\n                            c as char\n                        } else {\n                            // recover the unicode byte\n                            s[s.len() - bytes.as_slice().len() - 1..]\n                                .chars()\n                                .next()\n                                .unwrap()\n                        };\n                        return Err(format!(\n                            \"invalid bytes literal: \\\n                            invalid escape sequence '\\\\{}'\",\n                            ch.escape_debug()\n                        ));\n                    }\n                }\n            }\n            c => res.push(c),\n        }\n    }\n\n    Ok(res)\n}\n\n#[test]\nfn simple_bytes() {\n    assert_eq!(unquote_bytes_inner(r\"\\x09\").unwrap(), b\"\\x09\");\n    assert_eq!(unquote_bytes_inner(r\"\\x0A\").unwrap(), b\"\\x0A\");\n    assert_eq!(unquote_bytes_inner(r\"\\x0D\").unwrap(), b\"\\x0D\");\n    assert_eq!(unquote_bytes_inner(r\"\\x20\").unwrap(), b\"\\x20\");\n    assert_eq!(unquote_bytes(r\"b'\\x09'\").unwrap(), b\"\\x09\");\n    assert_eq!(unquote_bytes(r\"b'\\x0A'\").unwrap(), b\"\\x0A\");\n    assert_eq!(unquote_bytes(r\"b'\\x0D'\").unwrap(), b\"\\x0D\");\n    assert_eq!(unquote_bytes(r\"b'\\x20'\").unwrap(), b\"\\x20\");\n    assert_eq!(unquote_bytes(r\"br'\\x09'\").unwrap(), b\"\\\\x09\");\n    assert_eq!(unquote_bytes(r\"br'\\x0A'\").unwrap(), b\"\\\\x0A\");\n    assert_eq!(unquote_bytes(r\"br'\\x0D'\").unwrap(), b\"\\\\x0D\");\n    assert_eq!(unquote_bytes(r\"br'\\x20'\").unwrap(), b\"\\\\x20\");\n}\n\n#[test]\nfn newline_escaping_bytes() {\n    assert_eq!(\n        unquote_bytes_inner(\n            r\"hello \\\n                                world\"\n        )\n        .unwrap(),\n        b\"hello world\"\n    );\n    assert_eq!(\n        unquote_bytes(\n            r\"br'hello \\\n                                world'\"\n        )\n        .unwrap(),\n        b\"hello \\\\\\n                                world\"\n    );\n\n    assert_eq!(\n        unquote_bytes_inner(\n            r\"bb\\\naa \\\n            bb\"\n        )\n        .unwrap(),\n        b\"bbaa bb\"\n    );\n    assert_eq!(\n        unquote_bytes(\n            r\"rb'bb\\\naa \\\n            bb'\"\n        )\n        .unwrap(),\n        b\"bb\\\\\\naa \\\\\\n            bb\"\n    );\n    assert_eq!(\n        unquote_bytes_inner(\n            r\"bb\\\n\n        aa\"\n        )\n        .unwrap(),\n        b\"bbaa\"\n    );\n    assert_eq!(\n        unquote_bytes(\n            r\"br'bb\\\n\n        aa'\"\n        )\n        .unwrap(),\n        b\"bb\\\\\\n\\n        aa\"\n    );\n    assert_eq!(\n        unquote_bytes_inner(\n            r\"bb\\\n        \\\n        aa\"\n        )\n        .unwrap(),\n        b\"bbaa\"\n    );\n    assert_eq!(\n        unquote_bytes(\n            r\"rb'bb\\\n        \\\n        aa'\"\n        )\n        .unwrap(),\n        b\"bb\\\\\\n        \\\\\\n        aa\"\n    );\n    assert_eq!(unquote_bytes_inner(\"bb\\\\\\r   aa\").unwrap(), b\"bbaa\");\n    assert_eq!(unquote_bytes(\"br'bb\\\\\\r   aa'\").unwrap(), b\"bb\\\\\\r   aa\");\n    assert_eq!(unquote_bytes_inner(\"bb\\\\\\r\\n   aa\").unwrap(), b\"bbaa\");\n    assert_eq!(\n        unquote_bytes(\"rb'bb\\\\\\r\\n   aa'\").unwrap(),\n        b\"bb\\\\\\r\\n   aa\"\n    );\n}\n\n#[test]\nfn complex_bytes() {\n    assert_eq!(\n        unquote_bytes_inner(r\"\\x09 hello \\x0A there\").unwrap(),\n        b\"\\x09 hello \\x0A there\"\n    );\n    assert_eq!(\n        unquote_bytes(r\"br'\\x09 hello \\x0A there'\").unwrap(),\n        b\"\\\\x09 hello \\\\x0A there\"\n    );\n}\n"
  },
  {
    "path": "edb/edgeql-parser/src/helpers/mod.rs",
    "content": "mod bytes;\nmod strings;\n\npub use bytes::*;\npub use strings::*;\n"
  },
  {
    "path": "edb/edgeql-parser/src/helpers/strings.rs",
    "content": "use std::borrow::Cow;\nuse std::char;\nuse std::error::Error;\nuse std::fmt::{self, Write};\n\nuse crate::keywords;\n\n/// Error returned from `unquote_string` function\n///\n/// Opaque for now\n#[derive(Debug)]\npub struct UnquoteError(String);\n\n/// Converts the string into edgeql-compatible name (of a column or a property)\n///\n/// # Examples\n/// ```\n/// use edgeql_parser::helpers::quote_name;\n/// assert_eq!(quote_name(\"col1\"), \"col1\");\n/// assert_eq!(quote_name(\"another name\"), \"`another name`\");\n/// assert_eq!(quote_name(\"with `quotes`\"), \"`with ``quotes```\");\n/// ```\npub fn quote_name(s: &str) -> Cow<str> {\n    if s.chars().all(|c| c.is_alphanumeric() || c == '_') {\n        let lower = s.to_ascii_lowercase();\n        if keywords::lookup(&lower).is_none() {\n            return s.into();\n        }\n    }\n    let escaped = s.replace('`', \"``\");\n    let mut s = String::with_capacity(escaped.len() + 2);\n    s.push('`');\n    s.push_str(&escaped);\n    s.push('`');\n    s.into()\n}\n\npub fn quote_string(s: &str) -> String {\n    let mut buf = String::with_capacity(s.len() + 2);\n    buf.push('\"');\n    for c in s.chars() {\n        match c {\n            '\"' => {\n                buf.push('\\\\');\n                buf.push('\"');\n            }\n            '\\\\' => {\n                buf.push('\\\\');\n                buf.push('\\\\');\n            }\n            '\\x00'..='\\x08'\n            | '\\x0B'\n            | '\\x0C'\n            | '\\x0E'..='\\x1F'\n            | '\\u{007F}'\n            | '\\u{0080}'..='\\u{009F}' => {\n                write!(buf, \"\\\\x{:02x}\", c as u32).unwrap();\n            }\n            c => buf.push(c),\n        }\n    }\n    buf.push('\"');\n    buf\n}\n\npub fn unquote_string(value: &str) -> Result<Cow<str>, UnquoteError> {\n    if value.starts_with('r') {\n        Ok(value[2..value.len() - 1].into())\n    } else if let Some(stripped) = value.strip_prefix('$') {\n        let msize = 2 + stripped\n            .find('$')\n            .ok_or_else(|| \"invalid dollar-quoted string\".to_string())\n            .map_err(UnquoteError)?;\n        Ok(value[msize..value.len() - msize].into())\n    } else {\n        let end_trim = if value.ends_with(\"\\\\(\") { 2 } else { 1 };\n\n        Ok(_unquote_string(&value[1..value.len() - end_trim])\n            .map_err(UnquoteError)?\n            .into())\n    }\n}\n\nfn _unquote_string(s: &str) -> Result<String, String> {\n    let mut res = String::with_capacity(s.len());\n    let mut chars = s.chars();\n    while let Some(c) = chars.next() {\n        match c {\n            '\\\\' => {\n                let c = chars\n                    .next()\n                    .ok_or_else(|| \"quoted string cannot end in slash\".to_string())?;\n                match c {\n                    c @ '\"' | c @ '\\\\' | c @ '/' | c @ '\\'' => res.push(c),\n                    'b' => res.push('\\u{0008}'),\n                    'f' => res.push('\\u{000C}'),\n                    'n' => res.push('\\n'),\n                    'r' => res.push('\\r'),\n                    't' => res.push('\\t'),\n                    'x' => {\n                        let hex = chars.as_str().get(0..2);\n                        let code = hex\n                            .and_then(|s| u8::from_str_radix(s, 16).ok())\n                            .ok_or_else(|| {\n                                format!(\n                                    \"invalid string literal: \\\n                                invalid escape sequence '\\\\x{}'\",\n                                    hex.unwrap_or(chars.as_str()).escape_debug()\n                                )\n                            })?;\n                        if code > 0x7f || code == 0 {\n                            return Err(format!(\n                                \"invalid string literal: \\\n                                 invalid escape sequence '\\\\x{code:x}' \\\n                                 (only non-null ascii allowed)\"\n                            ));\n                        }\n                        res.push(code as char);\n                        chars.nth(1);\n                    }\n                    'u' => {\n                        let hex = chars.as_str().get(0..4);\n                        let ch = hex\n                            .and_then(|s| u32::from_str_radix(s, 16).ok())\n                            .and_then(char::from_u32)\n                            .and_then(|c| if c == '\\0' { None } else { Some(c) })\n                            .ok_or_else(|| {\n                                format!(\n                                    \"invalid string literal: \\\n                                    invalid escape sequence '\\\\u{}'\",\n                                    hex.unwrap_or(chars.as_str()).escape_debug()\n                                )\n                            })?;\n                        res.push(ch);\n                        chars.nth(3);\n                    }\n                    'U' => {\n                        let hex = chars.as_str().get(0..8);\n                        let ch = hex\n                            .and_then(|s| u32::from_str_radix(s, 16).ok())\n                            .and_then(char::from_u32)\n                            .and_then(|c| if c == '\\0' { None } else { Some(c) })\n                            .ok_or_else(|| {\n                                format!(\n                                    \"invalid string literal: \\\n                                    invalid escape sequence '\\\\U{}'\",\n                                    hex.unwrap_or(chars.as_str()).escape_debug()\n                                )\n                            })?;\n                        res.push(ch);\n                        chars.nth(7);\n                    }\n                    '\\r' | '\\n' => {\n                        let nleft = chars.as_str().trim_start().len();\n                        let nskip = chars.as_str().len() - nleft;\n                        if nskip > 0 {\n                            chars.nth(nskip - 1);\n                        }\n                    }\n                    c => {\n                        return Err(format!(\n                            \"invalid string literal: \\\n                             invalid escape sequence '\\\\{}'\",\n                            c.escape_debug()\n                        ));\n                    }\n                }\n            }\n            c => res.push(c),\n        }\n    }\n\n    Ok(res)\n}\n\n#[test]\nfn unquote_unicode_string() {\n    assert_eq!(_unquote_string(r\"\\x09\").unwrap(), \"\\u{09}\");\n    assert_eq!(_unquote_string(r\"\\u000A\").unwrap(), \"\\u{000A}\");\n    assert_eq!(_unquote_string(r\"\\u000D\").unwrap(), \"\\u{000D}\");\n    assert_eq!(_unquote_string(r\"\\u0020\").unwrap(), \"\\u{0020}\");\n    assert_eq!(_unquote_string(r\"\\uFFFF\").unwrap(), \"\\u{FFFF}\");\n}\n\n#[test]\nfn unquote_string_error() {\n    assert_eq!(\n        _unquote_string(r\"\\x00\").unwrap_err(),\n        \"invalid string literal: \\\n             invalid escape sequence '\\\\x0' (only non-null ascii allowed)\"\n    );\n    assert_eq!(\n        _unquote_string(r\"\\u0000\").unwrap_err(),\n        \"invalid string literal: invalid escape sequence '\\\\u0000'\"\n    );\n    assert_eq!(\n        _unquote_string(r\"\\U00000000\").unwrap_err(),\n        \"invalid string literal: invalid escape sequence '\\\\U00000000'\"\n    );\n}\n\n#[test]\nfn newline_escaping_str() {\n    assert_eq!(\n        _unquote_string(\n            r\"hello \\\n                                world\"\n        )\n        .unwrap(),\n        \"hello world\"\n    );\n\n    assert_eq!(\n        _unquote_string(\n            r\"bb\\\naa \\\n            bb\"\n        )\n        .unwrap(),\n        \"bbaa bb\"\n    );\n    assert_eq!(\n        _unquote_string(\n            r\"bb\\\n\n        aa\"\n        )\n        .unwrap(),\n        \"bbaa\"\n    );\n    assert_eq!(\n        _unquote_string(\n            r\"bb\\\n        \\\n        aa\"\n        )\n        .unwrap(),\n        \"bbaa\"\n    );\n    assert_eq!(_unquote_string(\"bb\\\\\\r   aa\").unwrap(), \"bbaa\");\n    assert_eq!(_unquote_string(\"bb\\\\\\r\\n   aa\").unwrap(), \"bbaa\");\n}\n\n#[test]\nfn test_quote_string() {\n    assert_eq!(quote_string(r\"\\n\"), r#\"\"\\\\n\"\"#);\n    assert_eq!(unquote_string(&quote_string(r\"\\n\")).unwrap(), r\"\\n\");\n}\n\n#[test]\nfn complex_strings() {\n    assert_eq!(\n        _unquote_string(r\"\\u0009 hello \\u000A there\").unwrap(),\n        \"\\u{0009} hello \\u{000A} there\"\n    );\n\n    assert_eq!(\n        _unquote_string(r\"\\x62:\\u2665:\\U000025C6\").unwrap(),\n        \"\\u{62}:\\u{2665}:\\u{25C6}\"\n    );\n}\n\nimpl fmt::Display for UnquoteError {\n    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n        self.0.fmt(f)\n    }\n}\nimpl Error for UnquoteError {}\n"
  },
  {
    "path": "edb/edgeql-parser/src/keywords.rs",
    "content": "use phf::phf_set;\n\npub const UNRESERVED_KEYWORDS: phf::Set<&str> = phf_set!(\n    \"abort\",\n    \"abstract\",\n    \"access\",\n    \"after\",\n    \"alias\",\n    \"allow\",\n    \"all\",\n    \"annotation\",\n    \"applied\",\n    \"as\",\n    \"asc\",\n    \"assignment\",\n    \"before\",\n    \"blobal\",\n    \"branch\",\n    \"cardinality\",\n    \"cast\",\n    \"committed\",\n    \"config\",\n    \"conflict\",\n    \"constraint\",\n    \"cube\",\n    \"current\",\n    \"data\",\n    \"database\",\n    \"ddl\",\n    \"declare\",\n    \"default\",\n    \"deferrable\",\n    \"deferred\",\n    \"delegated\",\n    \"desc\",\n    \"deny\",\n    \"each\",\n    \"empty\",\n    \"expression\",\n    \"extension\",\n    \"final\",\n    \"first\",\n    \"force\",\n    \"from\",\n    \"function\",\n    \"future\",\n    \"implicit\",\n    \"index\",\n    \"infix\",\n    \"inheritable\",\n    \"instance\",\n    \"into\",\n    \"isolation\",\n    \"json\",\n    \"last\",\n    \"link\",\n    \"migration\",\n    \"multi\",\n    \"named\",\n    \"object\",\n    \"of\",\n    \"only\",\n    \"onto\",\n    \"operator\",\n    \"optionality\",\n    \"order\",\n    \"orphan\",\n    \"overloaded\",\n    \"owned\",\n    \"package\",\n    \"permission\",\n    \"policy\",\n    \"populate\",\n    \"postfix\",\n    \"prefix\",\n    \"property\",\n    \"proposed\",\n    \"pseudo\",\n    \"read\",\n    \"reject\",\n    \"release\",\n    \"rename\",\n    \"repeatable\",\n    \"required\",\n    \"reset\",\n    \"restrict\",\n    \"rewrite\",\n    \"role\",\n    \"roles\",\n    \"rollup\",\n    \"savepoint\",\n    \"scalar\",\n    \"schema\",\n    \"sdl\",\n    \"serializable\",\n    \"session\",\n    \"source\",\n    \"superuser\",\n    \"system\",\n    \"target\",\n    \"template\",\n    \"ternary\",\n    \"text\",\n    \"then\",\n    \"to\",\n    \"transaction\",\n    \"trigger\",\n    \"type\",\n    \"unless\",\n    \"using\",\n    \"verbose\",\n    \"version\",\n    \"view\",\n    \"write\",\n);\n\npub const PARTIAL_RESERVED_KEYWORDS: phf::Set<&str> = phf_set!(\"except\", \"intersect\", \"union\",);\n\npub const FUTURE_RESERVED_KEYWORDS: phf::Set<&str> = phf_set!(\n    \"anyarray\",\n    \"begin\",\n    \"case\",\n    \"check\",\n    \"deallocate\",\n    \"discard\",\n    \"end\",\n    \"explain\",\n    \"fetch\",\n    \"get\",\n    \"global\",\n    \"grant\",\n    \"import\",\n    \"listen\",\n    \"load\",\n    \"lock\",\n    \"match\",\n    \"move\",\n    \"notify\",\n    \"on\",\n    \"over\",\n    \"prepare\",\n    \"partition\",\n    \"raise\",\n    \"refresh\",\n    \"revoke\",\n    \"single\",\n    \"when\",\n    \"window\",\n    \"never\",\n);\n\npub const CURRENT_RESERVED_KEYWORDS: phf::Set<&str> = phf_set!(\n    \"__source__\",\n    \"__subject__\",\n    \"__type__\",\n    \"__std__\",\n    \"__edgedbsys__\",\n    \"__edgedbtpl__\",\n    \"__new__\",\n    \"__old__\",\n    \"__specified__\",\n    \"__default__\",\n    \"administer\",\n    \"alter\",\n    \"analyze\",\n    \"and\",\n    \"anytuple\",\n    \"anytype\",\n    \"anyobject\",\n    \"by\",\n    \"commit\",\n    \"configure\",\n    \"create\",\n    \"delete\",\n    \"describe\",\n    \"detached\",\n    \"distinct\",\n    \"do\",\n    \"drop\",\n    \"else\",\n    \"exists\",\n    \"extending\",\n    \"false\",\n    \"filter\",\n    \"for\",\n    \"group\",\n    \"if\",\n    \"ilike\",\n    \"in\",\n    \"insert\",\n    \"introspect\",\n    \"is\",\n    \"like\",\n    \"limit\",\n    \"module\",\n    \"not\",\n    \"offset\",\n    \"optional\",\n    \"or\",\n    \"rollback\",\n    \"select\",\n    \"set\",\n    \"start\",\n    \"true\",\n    \"typeof\",\n    \"update\",\n    \"variadic\",\n    \"with\",\n);\n\npub const COMBINED_KEYWORDS: phf::Set<&str> = phf_set!(\n    \"named only\",\n    \"set annotation\",\n    \"set type\",\n    \"extension package\",\n    \"order by\",\n);\n\npub fn lookup(s: &str) -> Option<Keyword> {\n    None.or_else(|| PARTIAL_RESERVED_KEYWORDS.get_key(s))\n        .or_else(|| FUTURE_RESERVED_KEYWORDS.get_key(s))\n        .or_else(|| CURRENT_RESERVED_KEYWORDS.get_key(s))\n        .map(|x| Keyword(x))\n}\n\npub fn lookup_all(s: &str) -> Option<Keyword> {\n    lookup(s).or_else(|| {\n        None.or_else(|| COMBINED_KEYWORDS.get_key(s))\n            .or_else(|| UNRESERVED_KEYWORDS.get_key(s))\n            .map(|x| Keyword(x))\n    })\n}\n\n/// This is required for serde deserializer for Token to work correctly.\n#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]\n#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]\npub struct Keyword(pub &'static str);\n\nimpl Keyword {\n    pub fn is_reserved(&self) -> bool {\n        FUTURE_RESERVED_KEYWORDS.contains(self.0) || CURRENT_RESERVED_KEYWORDS.contains(self.0)\n    }\n    pub fn is_unreserved(&self) -> bool {\n        UNRESERVED_KEYWORDS.contains(self.0) || PARTIAL_RESERVED_KEYWORDS.contains(self.0)\n    }\n    pub fn is_dunder(&self) -> bool {\n        self.0.starts_with(\"__\") && self.0.ends_with(\"__\")\n    }\n    pub fn is_bool(&self) -> bool {\n        self.0 == \"true\" || self.0 == \"false\"\n    }\n}\n\nimpl From<Keyword> for &'static str {\n    fn from(value: Keyword) -> Self {\n        value.0\n    }\n}\n"
  },
  {
    "path": "edb/edgeql-parser/src/lib.rs",
    "content": "pub mod ast;\npub mod expr;\npub mod hash;\npub mod helpers;\npub mod keywords;\npub mod parser;\npub mod position;\npub mod preparser;\npub mod schema_file;\npub mod tokenizer;\npub mod validation;\n"
  },
  {
    "path": "edb/edgeql-parser/src/parser/cst.rs",
    "content": "use crate::helpers::quote_name;\nuse crate::keywords::Keyword;\nuse crate::position::Span;\nuse crate::tokenizer::{Kind, Token, Value};\n\n/// A node of the CST tree.\n///\n/// Warning: allocated in the bumpalo arena, which does not Drop.\n/// Any types that do allocation with global allocator (such as String or Vec),\n/// must manually drop. This is why Terminal has a special vec arena that does\n/// Drop.\n#[derive(Debug, Clone, Copy, Default)]\npub enum CSTNode<'a> {\n    #[default]\n    Empty,\n    Terminal(&'a Terminal),\n    Production(Production<'a>),\n}\n#[derive(Clone, Debug)]\npub struct Terminal {\n    pub kind: Kind,\n    pub text: String,\n    pub value: Option<Value>,\n    pub span: Span,\n    pub(super) is_placeholder: bool,\n}\n\n#[derive(Debug, Clone, Copy)]\npub struct Production<'a> {\n    pub id: usize,\n    pub args: &'a [CSTNode<'a>],\n    pub span: Option<Span>,\n\n    /// When a production is inlined, its id is saved into the new production\n    /// This is needed when matching CST nodes by production id.\n    pub inlined_ids: Option<&'a [usize]>,\n}\n\nimpl std::fmt::Display for Terminal {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        if (self.is_placeholder && self.kind == Kind::Ident) || self.text.is_empty() {\n            if let Some(user_friendly) = self.kind.user_friendly_text() {\n                return write!(f, \"{user_friendly}\");\n            }\n        }\n\n        match self.kind {\n            Kind::Ident => write!(f, \"'{}'\", &quote_name(&self.text)),\n            Kind::Keyword(Keyword(kw)) => write!(f, \"keyword '{}'\", kw.to_ascii_uppercase()),\n            _ => write!(f, \"'{}'\", self.text),\n        }\n    }\n}\n\nimpl Terminal {\n    pub fn from_token(token: Token) -> Self {\n        Terminal {\n            kind: token.kind,\n            text: token.text.into(),\n            value: token.value,\n            span: token.span,\n            is_placeholder: false,\n        }\n    }\n\n    #[cfg(feature = \"serde\")]\n    pub fn from_start_name(start_name: &str) -> Self {\n        use super::spec;\n\n        Terminal {\n            kind: spec::get_token_kind(start_name),\n            text: \"\".to_string(),\n            value: None,\n            span: Default::default(),\n            is_placeholder: false,\n        }\n    }\n}\n"
  },
  {
    "path": "edb/edgeql-parser/src/parser/custom_errors.rs",
    "content": "use crate::tokenizer::Kind;\nuse crate::{keywords::Keyword, position::Span};\n\nuse super::{CSTNode, Context, Error, Parser, StackNode, Terminal};\n\nimpl Parser<'_> {\n    pub(super) fn custom_error(&self, ctx: &Context, token: &Terminal) -> Option<Error> {\n        let ltok = self.get_from_top(0).unwrap();\n\n        if let Some(value) = self.custom_error_from_rule(token, ctx) {\n            return Some(value);\n        }\n\n        if matches!(token.kind, Kind::Keyword(Keyword(\"explain\"))) {\n            return Some({\n                Error {\n                    message: format!(\"Unexpected keyword '{}'\", token.text.to_uppercase()),\n                    span: Span::default(),\n                    hint: Some(\"Use `analyze` to show query performance details\".to_string()),\n                    details: None,\n                }\n            });\n        }\n\n        if let Kind::Keyword(kw) = token.kind {\n            if kw.is_reserved() && !Cond::Production(\"Expr\").check(ltok, ctx) {\n                // Another token followed by a reserved keyword:\n                // likely an attempt to use keyword as identifier\n                return Some(unexpected_reserved_keyword(&token.text, token.span));\n            }\n        };\n\n        None\n    }\n\n    fn custom_error_from_rule(&self, token: &Terminal, ctx: &Context) -> Option<Error> {\n        let last = self.get_from_top(0).unwrap();\n\n        let (i, rule) = self.get_rule(ctx)?;\n        // Look at the parsing stack and use tokens and\n        // non-terminals to infer the parser rule when the\n        // error occurred.\n\n        match rule {\n            ParserRule::ListOfArguments\n                // The stack is like <NodeName> LPAREN <AnyIdentifier>\n                if i == 1\n                    && Cond::AnyOf(vec![\n                        Cond::Production(\"AnyIdentifier\"),\n                        Cond::keyword(\"with\"),\n                        Cond::keyword(\"select\"),\n                        Cond::keyword(\"for\"),\n                        Cond::keyword(\"insert\"),\n                        Cond::keyword(\"update\"),\n                        Cond::keyword(\"delete\"),\n                    ])\n                    .check(last, ctx)\n            => {\n                return Some(Error {\n                    message: \"Missing parentheses around statement used as an expression\"\n                        .to_string(),\n                    span: super::get_span_of_nodes(&[last.value]).unwrap_or_default(),\n                    hint: None,\n                    details: None,\n                });\n            }\n\n            ParserRule::ArraySlice\n                if matches!(token.kind, Kind::Ident | Kind::IntConst)\n                && !Cond::Terminal(Kind::Colon).check(last, ctx)\n            => {\n                // The offending token was something that could\n                // make an expression\n                return Some(Error::new(format!(\n                    \"It appears that a ':' is missing in {rule} before {}\",\n                    token.text\n                )));\n            },\n\n            ParserRule::Definition if token.kind == Kind::Ident => {\n                // Something went wrong in a definition, so check\n                // if the last successful token is a keyword.\n                if Cond::Production(\"Identifier\").check(last, ctx)\n                // TODO: && ltok.value.upper() == \"INDEX\"\n                {\n                    return Some(Error::new(format!(\n                        \"Expected 'ON', but got '{}' instead\",\n                        token.text\n                    )));\n                }\n            },\n\n            ParserRule::ForIterator => {\n                let span = if i >= 4 {\n                    let span_start = self.get_from_top(i - 4).unwrap();\n                    let span = super::get_span_of_nodes(&[span_start.value]).unwrap_or_default();\n                    span.combine(token.span)\n                } else {\n                    token.span\n                };\n                return Some(Error {\n                    message: \"Missing parentheses around complex expression in \\\n                              a FOR iterator clause\".to_string(),\n                    span,\n                    hint: None,\n                    details: None,\n                });\n            },\n\n            ParserRule::Create => {\n                if matches!(token.kind, Kind::Keyword(Keyword(\"branch\"))) {\n                    return Some(Error {\n                        message: \"Missing one of keywords 'EMPTY', 'SCHEMA' or 'DATA'\".to_string(),\n                        span: Span { start: token.span.start - 1, end: token.span.start },\n                        hint: None,\n                        details: None,\n                    })\n                }\n            }\n\n            _ => {}\n        }\n        None\n    }\n\n    /// Look at the parsing stack and use tokens and non-terminals\n    /// to infer the parser rule when the error occurred.\n    fn get_rule(&self, ctx: &Context) -> Option<(usize, ParserRule)> {\n        // If the last valid token was a closing brace/parent/bracket,\n        // so we need to find a match for it before deciding what rule\n        // context we're in.\n        let mut need_match = self.compare_stack(\n            &[Cond::AnyOf(vec![\n                Cond::Terminal(Kind::CloseBrace),\n                Cond::Terminal(Kind::CloseParen),\n                Cond::Terminal(Kind::CloseBracket),\n            ])],\n            0,\n            ctx,\n        );\n        let mut found_union = false;\n\n        let ltok = self.get_from_top(0).unwrap();\n\n        let mut nextel = None;\n        let mut curr_el = Some(self.stack_top);\n        let mut i = 0;\n        while let Some(el) = curr_el {\n            // We'll need the element right before \"{\", \"[\", or \"(\".\n            let prevel = el.parent;\n\n            match el.value {\n                CSTNode::Terminal(Terminal {\n                    kind: Kind::OpenBrace,\n                    ..\n                }) => {\n                    if need_match && Cond::Terminal(Kind::CloseBrace).check(ltok, ctx) {\n                        // This is matched, while we're looking\n                        // for unmatched braces.\n                        need_match = false;\n                    } else if Cond::Production(\"OptExtending\").check_opt(prevel, ctx) {\n                        // This is some SDL/DDL\n                        return Some((i, ParserRule::Definition));\n                    } else if prevel.is_some_and(|prevel| {\n                        Cond::Production(\"Expr\").check(prevel, ctx)\n                            || (Cond::Terminal(Kind::Colon).check(prevel, ctx)\n                                && Cond::Production(\"ShapePointer\").check_opt(prevel.parent, ctx))\n                    }) {\n                        // This is some kind of shape.\n                        return Some((i, ParserRule::Shape));\n                    } else {\n                        return None;\n                    }\n                }\n\n                CSTNode::Terminal(Terminal {\n                    kind: Kind::OpenParen,\n                    ..\n                }) => {\n                    if need_match && Cond::Terminal(Kind::CloseParen).check(ltok, ctx) {\n                        // This is matched, while we're looking\n                        // for unmatched parentheses.\n                        need_match = false\n                    } else if Cond::Production(\"NodeName\").check_opt(prevel, ctx) {\n                        return Some((i, ParserRule::ListOfArguments));\n                    } else if Cond::AnyOf(vec![\n                        Cond::keyword(\"for\"),\n                        Cond::keyword(\"select\"),\n                        Cond::keyword(\"update\"),\n                        Cond::keyword(\"delete\"),\n                        Cond::keyword(\"insert\"),\n                        Cond::keyword(\"for\"),\n                    ])\n                    .check_opt(nextel, ctx)\n                    {\n                        // A parenthesized subquery expression,\n                        // we should leave the error as is.\n                        return None;\n                    } else {\n                        return Some((i, ParserRule::Tuple));\n                    }\n                }\n\n                CSTNode::Terminal(Terminal {\n                    kind: Kind::OpenBracket,\n                    ..\n                }) => {\n                    // This is either an array literal or\n                    // array index.\n\n                    if need_match && Cond::Terminal(Kind::CloseBracket).check(ltok, ctx) {\n                        // This is matched, while we're looking\n                        // for unmatched brackets.\n                        need_match = false\n                    } else if Cond::Production(\"Expr\").check_opt(prevel, ctx) {\n                        return Some((i, ParserRule::ArraySlice));\n                    } else {\n                        return Some((i, ParserRule::Array));\n                    }\n                }\n\n                CSTNode::Terminal(Terminal {\n                    kind: Kind::Keyword(Keyword(\"create\")),\n                    ..\n                }) => return Some((i, ParserRule::Create)),\n\n                _ => {}\n            }\n\n            // Check if we're in the `FOR x IN bad_tokens` situation\n            if self.compare_stack(&[Cond::keyword(\"union\")], i, ctx) {\n                found_union = true;\n            }\n            if !found_union\n                && self.compare_stack(\n                    &[\n                        Cond::keyword(\"for\"),\n                        Cond::Production(\"OptionalOptional\"),\n                        Cond::Production(\"Identifier\"),\n                        Cond::keyword(\"in\"),\n                    ],\n                    i,\n                    ctx,\n                )\n            {\n                return Some((i + 3, ParserRule::ForIterator));\n            }\n\n            // Also keep track of the element right after current.\n            nextel = Some(el);\n            curr_el = el.parent;\n            i += 1;\n        }\n\n        None\n    }\n\n    /// Looks at the stack and compares it with the expected nodes.\n    /// Does not compare [top_offset] number of nodes from the top of the start.\n    ///\n    /// Example of matching with top_offset=1, expected=[X, Y, Z]\n    /// ```plain\n    /// stack top -> A     (offset 1)\n    ///              B - Z\n    ///              C - Y\n    ///              D - X\n    ///              E\n    /// ```\n    fn compare_stack(&self, expected: &[Cond], top_offset: usize, ctx: &Context) -> bool {\n        let mut current = self.get_from_top(top_offset);\n\n        for validator in expected.iter().rev() {\n            let Some(cur) = current else {\n                return false;\n            };\n            if !validator.check(cur, ctx) {\n                return false;\n            }\n\n            current = cur.parent;\n        }\n        true\n    }\n}\n\nfn unexpected_reserved_keyword(text: &str, span: Span) -> Error {\n    let text_upper = text.to_uppercase();\n    Error {\n        message: format!(\"Unexpected keyword '{text_upper}'\"),\n        span,\n        details: Some(\n            \"This name is a reserved keyword and cannot be \\\n            used as an identifier\"\n                .to_string(),\n        ),\n        hint: Some(format!(\n            \"Use a different identifier or quote the name \\\n            with backticks: `{text}`\"\n        )),\n    }\n}\n\n/// Condition for a stack node. An easier way to match stack node kinds.\nenum Cond {\n    Terminal(Kind),\n    Production(&'static str),\n    AnyOf(Vec<Cond>),\n}\n\nimpl Cond {\n    fn keyword(kw: &'static str) -> Self {\n        Cond::Terminal(Kind::Keyword(Keyword(kw)))\n    }\n\n    fn check(&self, node: &StackNode, ctx: &Context) -> bool {\n        match self {\n            Cond::Terminal(kind) => matches!(\n                node.value,\n                CSTNode::Terminal(Terminal { kind: k, .. }) if k == kind\n            ),\n            Cond::Production(non_term) => match node.value {\n                CSTNode::Production(prod) => {\n                    let (pn, _) = &ctx.spec.production_names[prod.id];\n                    if non_term == pn {\n                        return true;\n                    }\n\n                    // When looking for a production, it might have happened\n                    // that it was inlined and superseded by one of its\n                    // arguments. That's why we save the id of the parent into\n                    // child's `inlined_ids` and check all of them here.\n                    if let Some(inlined_ids) = prod.inlined_ids {\n                        for prod_id in inlined_ids {\n                            let (pn, _) = &ctx.spec.production_names[*prod_id];\n                            if non_term == pn {\n                                return true;\n                            }\n                        }\n                    }\n                    false\n                }\n                _ => false,\n            },\n            Cond::AnyOf(options) => options.iter().any(|v| v.check(node, ctx)),\n        }\n    }\n\n    fn check_opt(&self, node: Option<&StackNode>, ctx: &Context) -> bool {\n        node.is_some_and(|x| self.check(x, ctx))\n    }\n}\n\n#[derive(Debug)]\nenum ParserRule {\n    ForIterator,\n    Definition,\n    Shape,\n    ArraySlice,\n    Array,\n    Tuple,\n    ListOfArguments,\n    Create,\n}\n\nimpl std::fmt::Display for ParserRule {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            ParserRule::ForIterator => f.write_str(\"for iterator\"),\n            ParserRule::Definition => f.write_str(\"definition\"),\n            ParserRule::Shape => f.write_str(\"shape\"),\n            ParserRule::ArraySlice => f.write_str(\"array slice\"),\n            ParserRule::Array => f.write_str(\"array\"),\n            ParserRule::Tuple => f.write_str(\"tuple\"),\n            ParserRule::ListOfArguments => f.write_str(\"list of arguments\"),\n            ParserRule::Create => f.write_str(\"create\"),\n        }\n    }\n}\n\npub fn post_process(errors: Vec<Error>) -> Vec<Error> {\n    let mut new_errors: Vec<Error> = Vec::with_capacity(errors.len());\n    for error in errors {\n        // Enrich combination of 'Unexpected keyword' + 'Missing identifier'\n        if error.message == \"Missing identifier\" {\n            if let Some(last) = new_errors.last() {\n                if last.message.starts_with(\"Unexpected keyword '\")\n                    && last.span.end == error.span.start\n                {\n                    let last = new_errors.pop().unwrap();\n                    let text = last.message.strip_prefix(\"Unexpected keyword '\").unwrap();\n                    let text = text.strip_suffix('\\'').unwrap();\n\n                    new_errors.push(unexpected_reserved_keyword(text, last.span));\n                    continue;\n                }\n            }\n        }\n\n        new_errors.push(error);\n    }\n\n    new_errors\n}\n"
  },
  {
    "path": "edb/edgeql-parser/src/parser/mod.rs",
    "content": "mod cst;\nmod custom_errors;\nmod spec;\n\npub use cst::{CSTNode, Production, Terminal};\npub use spec::{Action, Reduce, Spec, SpecSerializable};\n\nuse append_only_vec::AppendOnlyVec;\n\nuse crate::keywords::{self, Keyword};\nuse crate::position::Span;\nuse crate::tokenizer::{Error, Kind, Value};\n\npub struct Context<'s> {\n    spec: &'s Spec,\n    arena: bumpalo::Bump,\n    terminal_arena: AppendOnlyVec<Terminal>,\n}\n\nimpl<'s> Context<'s> {\n    pub fn new(spec: &'s Spec) -> Self {\n        Context {\n            spec,\n            arena: bumpalo::Bump::new(),\n            terminal_arena: AppendOnlyVec::new(),\n        }\n    }\n}\n\n/// This is a const just so we remember to update it everywhere\n/// when changing.\nconst UNEXPECTED: &str = \"Unexpected\";\n\npub fn parse<'a>(input: &'a [Terminal], ctx: &'a Context) -> (Option<CSTNode<'a>>, Vec<Error>) {\n    let stack_top = ctx.arena.alloc(StackNode {\n        parent: None,\n        state: 0,\n        value: CSTNode::Empty,\n    });\n    let initial_track = Parser {\n        stack_top,\n        error_cost: 0,\n        node_count: 0,\n        can_recover: true,\n        errors: Vec::new(),\n        has_custom_error: false,\n    };\n\n    // Append EIO token.\n    // We have a weird setup that requires two EOI tokens:\n    // - one is consumed by the grammar generator and does not contribute to\n    //   span of the nodes.\n    // - second is consumed by explicit EOF tokens in EdgeQLGrammar NonTerm.\n    //   Since these are children of productions, they do contribute to the\n    //   spans of the top-level nodes.\n    // First EOI is produced by tokenizer (with correct offset) and second one\n    // is injected here.\n    let end = input.last().map(|t| t.span.end).unwrap_or_default();\n    let eoi = ctx.alloc_terminal(Terminal {\n        kind: Kind::EOI,\n        span: Span { start: end, end },\n        text: \"\".to_string(),\n        value: None,\n        is_placeholder: false,\n    });\n    let input = input.iter().chain([eoi]);\n\n    let mut parsers = vec![initial_track];\n    let mut prev_span: Option<Span> = None;\n    let mut new_parsers = Vec::with_capacity(parsers.len() + 5);\n\n    for token in input {\n        // println!(\"token {:?}\", token);\n\n        while let Some(mut parser) = parsers.pop() {\n            let res = parser.act(ctx, token);\n\n            if res.is_ok() {\n                // base case: ok\n                parser.node_successful();\n                new_parsers.push(parser);\n            } else {\n                // error: try to recover\n\n                let gap_span = {\n                    let prev_end = prev_span.map(|p| p.end).unwrap_or(token.span.start);\n\n                    Span {\n                        start: prev_end,\n                        end: token.span.start,\n                    }\n                };\n\n                // option 1: inject a token\n                if parser.error_cost <= ERROR_COST_INJECT_MAX && !parser.has_custom_error {\n                    let possible_actions = &ctx.spec.actions[parser.stack_top.state];\n                    for token_kind in possible_actions.keys() {\n                        if parser.can_act(ctx, token_kind).is_none() {\n                            continue;\n                        }\n\n                        let mut inject = parser.clone();\n\n                        let injection =\n                            new_token_for_injection(*token_kind, &prev_span, token.span, ctx);\n\n                        let cost = injection_cost(token_kind);\n                        let error = Error::new(format!(\"Missing {injection}\")).with_span(gap_span);\n                        inject.push_error(error, cost);\n\n                        if inject.error_cost <= ERROR_COST_INJECT_MAX\n                            && inject.act(ctx, injection).is_ok()\n                        {\n                            // println!(\"   --> [inject {injection}]\");\n\n                            // insert into parsers, to retry the original token\n                            parsers.push(inject);\n                        }\n                    }\n                }\n\n                // option 2: check for a custom error and skip token\n                //   Due to performance reasons, this is done only on first\n                //   error, not during all the steps of recovery.\n                if parser.error_cost == 0 {\n                    if let Some(error) = parser.custom_error(ctx, token) {\n                        parser\n                            .push_error(error.default_span_to(token.span), ERROR_COST_CUSTOM_ERROR);\n                        parser.has_custom_error = true;\n\n                        // println!(\"   --> [custom error]\");\n                        new_parsers.push(parser);\n                        continue;\n                    }\n                } else if parser.has_custom_error {\n                    // when there is a custom error, just skip the tokens until\n                    // the parser recovers\n                    // println!(\"   --> [skip because of custom error]\");\n                    new_parsers.push(parser);\n                    continue;\n                }\n\n                // option 3: skip the token\n                let mut skip = parser;\n                let error = Error::new(format!(\"{UNEXPECTED} {token}\")).with_span(token.span);\n                skip.push_error(error, ERROR_COST_SKIP);\n                if token.kind == Kind::EOI || token.kind == Kind::Semicolon {\n                    // extra penalty\n                    skip.error_cost += ERROR_COST_INJECT_MAX;\n                    skip.can_recover = false;\n                }\n\n                // insert into new_parsers, so the token is skipped\n                // println!(\"   --> [skip] {}\", skip.error_cost);\n                new_parsers.push(skip);\n            }\n        }\n\n        // has any parser recovered?\n        if new_parsers.len() > 1 {\n            new_parsers.sort_by_key(Parser::adjusted_cost);\n\n            if new_parsers[0].has_custom_error {\n                // if we have a custom error, just keep that\n\n                new_parsers.drain(1..);\n            } else if new_parsers[0].has_recovered() {\n                // recover parsers whose \"adjusted error cost\" reached 0 and discard the rest\n\n                new_parsers.retain(|p| p.has_recovered());\n                for p in &mut new_parsers {\n                    p.error_cost = 0;\n                }\n            } else if new_parsers[0].error_cost > ERROR_COST_INJECT_MAX {\n                // prune: pick only 1 best parsers that has cost > ERROR_COST_INJECT_MAX\n\n                new_parsers.drain(1..);\n            } else if new_parsers.len() > PARSER_COUNT_MAX {\n                // prune: pick only X best parsers\n\n                new_parsers.drain(PARSER_COUNT_MAX..);\n            }\n        }\n\n        assert!(parsers.is_empty());\n        std::mem::swap(&mut parsers, &mut new_parsers);\n        prev_span = Some(token.span);\n\n        // for (index, parser) in parsers.iter().enumerate() {\n        //     print!(\n        //         \"p{index} {:06} {:5}:\",\n        //         parser.error_cost, parser.can_recover\n        //     );\n        //     for e in &parser.errors {\n        //         print!(\" {}\", e.message);\n        //     }\n        //     println!(\"\");\n        // }\n        // println!(\"\");\n    }\n\n    // there will always be a parser left,\n    // since we always allow a token to be skipped\n    let parser = parsers\n        .into_iter()\n        .min_by(|a, b| {\n            Ord::cmp(&a.error_cost, &b.error_cost).then_with(|| {\n                Ord::cmp(\n                    &starts_with_unexpected_error(a),\n                    &starts_with_unexpected_error(b),\n                )\n                .reverse()\n            })\n        })\n        .unwrap();\n\n    let node = parser.finish(ctx);\n    let errors = custom_errors::post_process(parser.errors);\n    (node, errors)\n}\n\n/// Parses tokens and then inspects the state of the parser to suggest possible\n/// next keywords and a boolean indicating if next token can be an identifier.\n/// This is done by looking at available actions in current state.\n/// An important detail is that not all of these actions are valid.\n/// They might trigger a chain of reductions that ends in a state that\n/// does not accept the suggested token.\npub fn suggest_next_keyword<'a>(input: &'a [Terminal], ctx: &'a Context) -> (Vec<Keyword>, bool) {\n    // init\n    let stack_top = ctx.arena.alloc(StackNode {\n        parent: None,\n        state: 0,\n        value: CSTNode::Empty,\n    });\n    let mut parser = Parser {\n        stack_top,\n        error_cost: 0,\n        node_count: 0,\n        can_recover: true,\n        errors: Vec::new(),\n        has_custom_error: false,\n    };\n\n    // parse tokens\n    for token in input.iter() {\n        if matches!(token.kind, Kind::EOI) {\n            break;\n        }\n\n        let res = parser.act(ctx, token);\n\n        if res.is_err() {\n            return (vec![], false);\n        }\n    }\n\n    // extract possible next actions\n    let actions = &ctx.spec.actions[parser.stack_top.state];\n\n    let can_be_ident =\n        actions.contains_key(&Kind::Ident) && parser.can_act(ctx, &Kind::Ident).is_some();\n\n    let keywords = actions\n        .keys()\n        // suggest only keywords\n        .filter_map(|kind| {\n            if let Kind::Keyword(keyword) = kind {\n                Some(*keyword)\n            } else {\n                None\n            }\n        })\n        // never suggest dunder or bools, they should be suggested semantically\n        .filter(|k| !k.is_dunder() && !k.is_bool())\n        // if next token can be ident, hide all unreserved keywords\n        .filter(|k| !(can_be_ident && k.is_unreserved()))\n        // filter only valid actions\n        .filter(|k| parser.can_act(ctx, &Kind::Keyword(*k)).is_some())\n        .collect();\n\n    (keywords, can_be_ident)\n}\n\nfn starts_with_unexpected_error(a: &Parser) -> bool {\n    a.errors\n        .first()\n        .is_none_or(|x| x.message.starts_with(UNEXPECTED))\n}\n\nimpl Context<'_> {\n    fn alloc_terminal(&self, t: Terminal) -> &'_ Terminal {\n        let idx = self.terminal_arena.push(t);\n        &self.terminal_arena[idx]\n    }\n\n    fn alloc_slice_and_push(&self, slice: &Option<&[usize]>, element: usize) -> &[usize] {\n        let curr_len = slice.map_or(0, |x| x.len());\n        let mut new = Vec::with_capacity(curr_len + 1);\n        if let Some(inlined_ids) = slice {\n            new.extend(*inlined_ids);\n        }\n        new.push(element);\n        self.arena.alloc_slice_clone(new.as_slice())\n    }\n}\n\nfn new_token_for_injection<'a>(\n    kind: Kind,\n    prev_span: &Option<Span>,\n    next_span: Span,\n    ctx: &'a Context,\n) -> &'a Terminal {\n    let (text, value) = match kind {\n        Kind::Keyword(Keyword(kw)) => (kind.text(), Some(Value::String(kw.to_string()))),\n        Kind::Ident => {\n            let ident = \"ident_placeholder\";\n            (Some(ident), Some(Value::String(ident.into())))\n        }\n        _ => (kind.text(), None),\n    };\n\n    ctx.alloc_terminal(Terminal {\n        kind,\n        text: text.unwrap_or_default().to_string(),\n        value,\n        span: Span {\n            start: prev_span.map_or(0, |x| x.end),\n            end: next_span.start,\n        },\n        is_placeholder: true,\n    })\n}\n\nstruct StackNode<'p> {\n    parent: Option<&'p StackNode<'p>>,\n\n    state: usize,\n    value: CSTNode<'p>,\n}\n\n#[derive(Clone)]\nstruct Parser<'s> {\n    stack_top: &'s StackNode<'s>,\n\n    /// sum of cost of every error recovery action\n    error_cost: u16,\n\n    /// number of nodes pushed to stack since last error\n    node_count: u16,\n\n    /// prevent parser from recovering, for cases when EOF was skipped\n    can_recover: bool,\n\n    errors: Vec<Error>,\n\n    /// A flag that is used to make the parser prefer custom errors over other\n    /// recovery paths\n    has_custom_error: bool,\n}\n\nimpl<'s> Parser<'s> {\n    fn act(&mut self, ctx: &'s Context, token: &'s Terminal) -> Result<(), ()> {\n        // self.print_stack();\n        // println!(\"INPUT: {}\", token.text);\n\n        loop {\n            // find next action\n            let Some(action) = ctx.spec.actions[self.stack_top.state].get(&token.kind) else {\n                return Err(());\n            };\n\n            match action {\n                Action::Shift(next) => {\n                    // println!(\"   --> [shift {next}]\");\n\n                    // push on stack\n                    self.push_on_stack(ctx, *next, CSTNode::Terminal(token));\n                    return Ok(());\n                }\n                Action::Reduce(reduce) => {\n                    self.reduce(ctx, reduce);\n                }\n            }\n        }\n    }\n\n    fn reduce(&mut self, ctx: &'s Context, reduce: &'s Reduce) {\n        let args = ctx.arena.alloc_slice_fill_with(reduce.cnt, |_| {\n            let v = self.stack_top.value;\n            self.stack_top = self.stack_top.parent.unwrap();\n            v\n        });\n        args.reverse();\n\n        let value = CSTNode::Production(Production {\n            id: reduce.production_id,\n            span: get_span_of_nodes(args),\n            args,\n            inlined_ids: None,\n        });\n\n        let nstate = self.stack_top.state;\n\n        let next = *ctx.spec.goto[nstate].get(&reduce.non_term).unwrap();\n\n        // inline (if there is an inlining rule)\n        let mut value = value;\n        if let CSTNode::Production(production) = value {\n            if let Some(inline_position) = ctx.spec.inlines.get(&production.id) {\n                let inlined_id = production.id;\n                // inline rule found\n                let args = production.args;\n\n                value = args[*inline_position as usize];\n\n                // save inlined id\n                if let CSTNode::Production(new_prod) = &mut value {\n                    new_prod.inlined_ids =\n                        Some(ctx.alloc_slice_and_push(&new_prod.inlined_ids, inlined_id));\n                }\n            } else {\n                // place back\n                value = CSTNode::Production(production);\n            }\n        }\n\n        self.push_on_stack(ctx, next, value);\n\n        // println!(\n        //     \"   --> [reduce {} ::= ({} popped) at {}/{}]\",\n        //     production, cnt, state, nstate\n        // );\n        // self.print_stack();\n    }\n\n    pub fn push_on_stack(&mut self, ctx: &'s Context, state: usize, value: CSTNode<'s>) {\n        let node = StackNode {\n            parent: Some(self.stack_top),\n            state,\n            value,\n        };\n        self.stack_top = ctx.arena.alloc(node);\n    }\n\n    pub fn finish(&self, _ctx: &'s Context) -> Option<CSTNode<'s>> {\n        if !self.can_recover || self.has_custom_error {\n            return None;\n        }\n\n        // pop the EOI from the top of the stack\n        assert!(\n            matches!(\n                &self.stack_top.value,\n                CSTNode::Terminal(Terminal {\n                    kind: Kind::EOI,\n                    ..\n                })\n            ),\n            \"expected EOI CST node, got {:?}\",\n            self.stack_top.value\n        );\n\n        let final_node = self.stack_top.parent.unwrap();\n\n        // self.print_stack(_ctx);\n        // println!(\"   --> accept\");\n\n        let first = final_node.parent.unwrap();\n        assert!(\n            matches!(&first.value, CSTNode::Empty),\n            \"expected empty CST node, found {:?}\",\n            first.value\n        );\n\n        Some(final_node.value)\n    }\n\n    /// Lightweight version of act that checks if a token *could* be applied.\n    /// Returns next state.\n    fn can_act(&self, ctx: &'s Context, token: &Kind) -> Option<usize> {\n        let mut state = self.stack_top.state;\n\n        let mut node = &self.stack_top;\n\n        // count of \"ghost\" stack nodes, which should have been pushed to the stack,\n        // but haven't because we don't actually need them there, only need to know\n        // how many of them there are\n        let mut ghosts = 0;\n\n        loop {\n            // find next action\n            let action = ctx.spec.actions[state].get(token)?;\n\n            match action {\n                Action::Shift(next) => {\n                    return Some(*next);\n                }\n                Action::Reduce(reduce) => {\n                    // simulate reduce stack pops\n                    // (cancel out any ghost nodes if there is any)\n                    let cancel_out = usize::min(ghosts, reduce.cnt);\n                    ghosts -= cancel_out;\n                    for _ in 0..(reduce.cnt - cancel_out) {\n                        node = node.parent.as_ref().unwrap();\n                    }\n\n                    // get state of current stack top\n                    // Stack top is node.state, unless we have ghosts. In that case, the\n                    // state of node we would have pushed is stored in `state`.\n                    let stack_state = if ghosts > 0 { state } else { node.state };\n\n                    state = *ctx.spec.goto[stack_state].get(&reduce.non_term)?;\n\n                    ghosts += 1;\n                }\n            }\n        }\n    }\n\n    #[cfg(never)]\n    fn print_stack(&self, ctx: &'s Context) {\n        let prefix = \"STACK: \";\n\n        let mut stack = Vec::new();\n        let mut node = Some(self.stack_top);\n        while let Some(n) = node {\n            stack.push(n);\n            node = n.parent.clone();\n        }\n        stack.reverse();\n\n        let names = stack\n            .iter()\n            .map(|s| match s.value {\n                CSTNode::Empty => format!(\"Empty\"),\n                CSTNode::Terminal(term) => format!(\"{term}\"),\n                CSTNode::Production(prod) => {\n                    let prod_name = &ctx.spec.production_names[prod.id];\n                    format!(\"{}.{}\", prod_name.0, prod_name.1)\n                }\n            })\n            .collect::<Vec<_>>();\n\n        let mut states = format!(\"{:5}\", ' ');\n        for (index, node) in stack.iter().enumerate() {\n            let name_width = names[index].chars().count();\n            states += &format!(\"  {:<width$}\", node.state, width = name_width);\n        }\n\n        println!(\"{}{}\", prefix, names.join(\"  \"));\n        println!(\"{}\", states);\n    }\n\n    fn push_error(&mut self, error: Error, cost: u16) {\n        let mut suppress = false;\n        if error.message.starts_with(UNEXPECTED) {\n            if let Some(last) = self.errors.last() {\n                if last.message.starts_with(UNEXPECTED) {\n                    // don't repeat \"Unexpected\" errors\n\n                    // This is especially useful when we encounter an\n                    // unrecoverable error which will make all tokens until EOF\n                    // as \"Unexpected\".\n                    suppress = true;\n                }\n            }\n        }\n        if !suppress {\n            self.errors.push(error);\n        }\n\n        self.error_cost += cost;\n        self.node_count = 0;\n    }\n\n    fn node_successful(&mut self) {\n        self.node_count += 1;\n    }\n\n    /// Error cost, subtracted by a function of successfully parsed nodes.\n    fn adjusted_cost(&self) -> u16 {\n        let x = self.node_count.saturating_sub(3);\n        self.error_cost.saturating_sub(x.saturating_mul(x))\n    }\n\n    fn has_recovered(&self) -> bool {\n        self.can_recover && self.adjusted_cost() == 0\n    }\n\n    fn get_from_top(&self, steps: usize) -> Option<&StackNode<'s>> {\n        self.stack_top.step_up(steps)\n    }\n}\n\nimpl<'a> StackNode<'a> {\n    fn step_up(&self, steps: usize) -> Option<&StackNode<'a>> {\n        let mut node = Some(self);\n        for _ in 0..steps {\n            match node {\n                None => return None,\n                Some(n) => node = n.parent,\n            }\n        }\n        node\n    }\n}\n\n/// Returns the span of syntactically ordered nodes. Panic on empty nodes.\nfn get_span_of_nodes(nodes: &[CSTNode]) -> Option<Span> {\n    let start = nodes.iter().find_map(|x| match x {\n        CSTNode::Terminal(t) => Some(t.span.start),\n        CSTNode::Production(p) => Some(p.span?.start),\n        CSTNode::Empty => panic!(),\n    })?;\n    let end = nodes.iter().rev().find_map(|x| match x {\n        CSTNode::Terminal(t) => Some(t.span.end),\n        CSTNode::Production(p) => Some(p.span?.end),\n        CSTNode::Empty => panic!(),\n    })?;\n    Some(Span { start, end })\n}\n\nconst PARSER_COUNT_MAX: usize = 10;\n\nconst ERROR_COST_INJECT_MAX: u16 = 15;\nconst ERROR_COST_SKIP: u16 = 3;\nconst ERROR_COST_CUSTOM_ERROR: u16 = 3;\n\nfn injection_cost(kind: &Kind) -> u16 {\n    use Kind::*;\n\n    match kind {\n        Ident => 10,\n        Substitution => 8,\n\n        // Manual keyword tweaks to encourage some error messages and discourage others.\n        Keyword(keywords::Keyword(\n            \"delete\" | \"update\" | \"migration\" | \"role\" | \"global\" | \"administer\" | \"future\"\n            | \"database\" | \"serializable\" | \"REPEATABLE\" | \"NOT\", //  | \"if\" | \"group\",\n        )) => 100,\n        Keyword(keywords::Keyword(\"insert\" | \"module\" | \"extension\" | \"branch\")) => 20,\n        Keyword(keywords::Keyword(\"select\" | \"property\" | \"type\")) => 10,\n        Keyword(_) => 15,\n\n        Dot => 5,\n        OpenBrace | OpenBracket => 5,\n        OpenParen => 4,\n\n        CloseBrace | CloseBracket | CloseParen => 1,\n\n        Namespace => 10,\n        Comma | Colon | Semicolon => 2,\n        Eq => 5,\n\n        At => 6,\n        IntConst => 8,\n\n        Assign | Arrow => 5,\n\n        _ => 100, // forbidden\n    }\n}\n"
  },
  {
    "path": "edb/edgeql-parser/src/parser/spec.rs",
    "content": "use indexmap::IndexMap;\n\nuse crate::tokenizer::Kind;\n\npub struct Spec {\n    pub actions: Vec<IndexMap<Kind, Action>>,\n    pub goto: Vec<IndexMap<String, usize>>,\n    pub inlines: IndexMap<usize, u8>,\n    pub production_names: Vec<(String, String)>,\n}\n\n#[derive(Debug)]\n#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]\npub enum Action {\n    Shift(usize),\n    Reduce(Reduce),\n}\n\n#[derive(Debug)]\n#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]\npub struct Reduce {\n    /// Index of the production in the associated production array\n    pub production_id: usize,\n\n    pub non_term: String,\n\n    /// Number of arguments\n    pub cnt: usize,\n}\n\n#[cfg(feature = \"serde\")]\n#[derive(Debug, serde::Serialize, serde::Deserialize)]\npub struct SpecSerializable {\n    pub actions: Vec<Vec<(String, Action)>>,\n    pub goto: Vec<Vec<(String, usize)>>,\n    pub inlines: Vec<(usize, u8)>,\n    pub production_names: Vec<(String, String)>,\n}\n\n#[cfg(feature = \"serde\")]\nimpl From<SpecSerializable> for Spec {\n    fn from(v: SpecSerializable) -> Spec {\n        let actions = v\n            .actions\n            .into_iter()\n            .map(|x| x.into_iter().map(|(k, a)| (get_token_kind(&k), a)))\n            .map(IndexMap::from_iter)\n            .collect();\n        let goto = v.goto.into_iter().map(IndexMap::from_iter).collect();\n        let inlines = IndexMap::from_iter(v.inlines);\n\n        Spec {\n            actions,\n            goto,\n            inlines,\n            production_names: v.production_names,\n        }\n    }\n}\n\n#[cfg(feature = \"serde\")]\npub(super) fn get_token_kind(token_name: &str) -> Kind {\n    use Kind::*;\n\n    match token_name {\n        \"+\" => Add,\n        \"&\" => Ampersand,\n        \"@\" => At,\n        \".<\" => BackwardLink,\n        \".?>\" => OptionalLink,\n        \"}\" => CloseBrace,\n        \"]\" => CloseBracket,\n        \")\" => CloseParen,\n        \"??\" => Coalesce,\n        \":\" => Colon,\n        \",\" => Comma,\n        \"++\" => Concat,\n        \"/\" => Div,\n        \".\" => Dot,\n        \"**\" => DoubleSplat,\n        \"=\" => Eq,\n        \"//\" => FloorDiv,\n        \"%\" => Modulo,\n        \"*\" => Mul,\n        \"::\" => Namespace,\n        \"{\" => OpenBrace,\n        \"[\" => OpenBracket,\n        \"(\" => OpenParen,\n        \"|\" => Pipe,\n        \"^\" => Pow,\n        \";\" => Semicolon,\n        \"-\" => Sub,\n\n        \"?!=\" => DistinctFrom,\n        \">=\" => GreaterEq,\n        \"<=\" => LessEq,\n        \"?=\" => NotDistinctFrom,\n        \"!=\" => NotEq,\n        \"<\" => Less,\n        \">\" => Greater,\n\n        \"IDENT\" => Ident,\n        \"EOI\" | \"<$>\" => EOI,\n        \"<e>\" => Epsilon,\n\n        \"BCONST\" => BinStr,\n        \"FCONST\" => FloatConst,\n        \"ICONST\" => IntConst,\n        \"NFCONST\" => DecimalConst,\n        \"NICONST\" => BigIntConst,\n        \"SCONST\" => Str,\n\n        \"STARTBLOCK\" => StartBlock,\n        \"STARTEXTENSION\" => StartExtension,\n        \"STARTFRAGMENT\" => StartFragment,\n        \"STARTMIGRATION\" => StartMigration,\n        \"STARTSDLDOCUMENT\" => StartSDLDocument,\n\n        \"+=\" => AddAssign,\n        \"->\" => Arrow,\n        \":=\" => Assign,\n        \"-=\" => SubAssign,\n\n        \"PARAMETER\" => Parameter,\n        \"PARAMETERANDTYPE\" => ParameterAndType,\n        \"SUBSTITUTION\" => Substitution,\n\n        \"STRINTERPSTART\" => StrInterpStart,\n        \"STRINTERPCONT\" => StrInterpCont,\n        \"STRINTERPEND\" => StrInterpEnd,\n\n        _ => {\n            let mut token_name = token_name.to_lowercase();\n\n            if let Some(rem) = token_name.strip_prefix(\"dunder\") {\n                token_name = format!(\"__{rem}__\");\n            }\n\n            let kw = crate::keywords::lookup_all(&token_name)\n                .unwrap_or_else(|| panic!(\"unknown keyword {token_name}\"));\n            Keyword(kw)\n        }\n    }\n}\n"
  },
  {
    "path": "edb/edgeql-parser/src/position.rs",
    "content": "use std::fmt;\nuse std::str::{from_utf8, Utf8Error};\n\nuse unicode_width::{UnicodeWidthChar, UnicodeWidthStr};\n\n/// Span of an element in source code\n#[derive(Debug, Clone, Copy, Default, PartialEq)]\n#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]\npub struct Span {\n    /// Byte offset in the original file\n    ///\n    /// Technically you can read > 4Gb file on 32bit machine so it may\n    /// not fit in usize\n    pub start: u64,\n\n    /// Byte offset in the original file\n    ///\n    /// Technically you can read > 4Gb file on 32bit machine so it may\n    /// not fit in usize\n    pub end: u64,\n}\n/// Original position of element in source code\n#[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Default, Hash)]\n#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]\npub struct Pos {\n    /// One-based line number\n    pub line: usize,\n    /// One-based column number\n    pub column: usize,\n    /// Byte offset in the original file\n    ///\n    /// Technically you can read > 4Gb file on 32bit machine so it may\n    /// not fit in usize\n    pub offset: u64,\n}\n\n/// This contains position in all forms that EdgeDB needs\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub struct InflatedPos {\n    /// Zero-based line number\n    pub line: u64,\n    /// Zero-based column number\n    pub column: u64,\n    /// Zero-based Utf16 column offset\n    ///\n    /// (this is required by language server protocol, LSP)\n    pub utf16column: u64,\n    /// Bytes offset in the orignal (utf-8 encoded) byte buffer\n    pub offset: u64,\n    /// Character offset in the whole string\n    pub char_offset: u64,\n}\n\n/// Error calculating InflatedPos\n#[derive(Debug, thiserror::Error)]\npub enum InflatingError {\n    #[error(transparent)]\n    Utf8(Utf8Error),\n    #[error(\"offset out of range\")]\n    OutOfRange,\n}\n\nimpl fmt::Debug for Pos {\n    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n        write!(f, \"Pos({}:{})\", self.line, self.column)\n    }\n}\n\nimpl fmt::Display for Pos {\n    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n        write!(f, \"{}:{}\", self.line, self.column)\n    }\n}\n\nimpl Span {\n    pub fn combine(self, right: Span) -> Span {\n        Span {\n            start: self.start,\n            end: right.end,\n        }\n    }\n\n    pub fn extend(self, other: &Span) -> Span {\n        Span {\n            start: u64::min(self.start, other.start),\n            end: u64::max(self.end, other.end),\n        }\n    }\n}\n\nfn new_lines_in_fragment(data: &[u8]) -> u64 {\n    let mut was_lf = false;\n    let mut lines = 0;\n    for byte in data {\n        match byte {\n            b'\\n' if was_lf => {\n                was_lf = false;\n            }\n            b'\\n' => {\n                lines += 1;\n            }\n            b'\\r' => {\n                lines += 1;\n                was_lf = true;\n            }\n            _ => {\n                was_lf = false;\n            }\n        }\n    }\n    lines\n}\n\nimpl InflatedPos {\n    pub fn from_offset(data: &[u8], offset: u64) -> Result<InflatedPos, InflatingError> {\n        let res = Self::from_offsets(data, &[offset as usize])?;\n        Ok(res.into_iter().next().unwrap())\n    }\n\n    pub fn from_offsets(\n        data: &[u8],\n        offsets: &[usize],\n    ) -> Result<Vec<InflatedPos>, InflatingError> {\n        let mut result = Vec::with_capacity(offsets.len());\n        // TODO(tailhook) optimize calculation if offsets are growing\n        for &offset in offsets {\n            if offset > data.len() {\n                return Err(InflatingError::OutOfRange);\n            }\n            let prefix = &data[..offset];\n            let prefix_s = from_utf8(prefix).map_err(InflatingError::Utf8)?;\n            let line_offset;\n            let line;\n            if let Some(loff) = prefix_s.rfind(['\\r', '\\n']) {\n                line_offset = loff + 1;\n                let mut lines = &prefix[..loff];\n                if data[loff] == b'\\n' && loff > 0 && data[loff - 1] == b'\\r' {\n                    lines = &lines[..lines.len() - 1];\n                }\n                line = new_lines_in_fragment(lines) + 1;\n            } else {\n                line = 0;\n                line_offset = 0;\n            };\n            let col_s = &prefix_s[line_offset..offset];\n            result.push(InflatedPos {\n                line,\n                column: UnicodeWidthStr::width(col_s) as u64,\n                utf16column: col_s.chars().map(|c| c.len_utf16() as u64).sum(),\n                offset: offset as u64,\n                char_offset: prefix_s.chars().count() as u64,\n            });\n        }\n        Ok(result)\n    }\n\n    pub fn from_lines_cols(\n        data: &[u8],\n        lines_cols: &[(u64, u64)],\n    ) -> Result<Vec<InflatedPos>, InflatingError> {\n        let mut result = Vec::with_capacity(lines_cols.len());\n\n        let text = from_utf8(data).map_err(InflatingError::Utf8)?;\n        let mut text_iter = text.chars().peekable();\n\n        let mut lines_cols = lines_cols.iter().peekable();\n\n        let mut offset: u64 = 0;\n        let mut char_offset: u64 = 0;\n        let mut lines = 0..;\n\n        for line in &mut lines {\n            let mut utf16column = 0;\n            let mut column = 0;\n            'line: loop {\n                // emit all matching points (there will typically be only one)\n                loop {\n                    if let Some((l, c)) = lines_cols.peek() {\n                        if line == *l && *c <= utf16column {\n                            result.push(InflatedPos {\n                                line,\n                                column,\n                                utf16column,\n                                offset,\n                                char_offset,\n                            });\n                            lines_cols.next();\n                        } else {\n                            break;\n                        }\n                    } else {\n                        break 'line;\n                    }\n                }\n\n                // stop if end of line\n                let eol = text_iter.peek().is_none_or(|c| *c == '\\n' || *c == '\\r');\n                if eol {\n                    break;\n                }\n\n                // advance a char\n                let char = text_iter.next().unwrap();\n\n                offset += char.len_utf8() as u64;\n                utf16column += char.len_utf16() as u64;\n                char_offset += 1;\n                column += UnicodeWidthChar::width(char).unwrap_or(0) as u64;\n            }\n\n            // emit all point that had column out of line\n            while let Some((l, _)) = lines_cols.peek() {\n                if line == *l {\n                    result.push(InflatedPos {\n                        line,\n                        column,\n                        utf16column,\n                        offset,\n                        char_offset,\n                    });\n                    lines_cols.next();\n                } else {\n                    break;\n                }\n            }\n\n            if text_iter.peek().is_none() || lines_cols.peek().is_none() {\n                break;\n            }\n\n            // consume \\n or \\r\\n\n            if text_iter.peek().is_some_and(|c| *c == '\\r') {\n                text_iter.next();\n                offset += 1;\n                char_offset += 1;\n            }\n            if text_iter.peek().is_some_and(|c| *c == '\\n') {\n                text_iter.next();\n                offset += 1;\n                char_offset += 1;\n            }\n        }\n\n        // emit all lines out of buffer\n        let last_line = lines.next().unwrap().saturating_sub(1);\n        for _ in lines_cols {\n            result.push(InflatedPos {\n                line: last_line,\n                column: 0,\n                utf16column: 0,\n                offset,\n                char_offset,\n            });\n        }\n\n        Ok(result)\n    }\n\n    pub fn deflate(self) -> Pos {\n        Pos {\n            line: self.line as usize + 1,\n            column: self.column as usize + 1,\n            offset: self.offset,\n        }\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::{new_lines_in_fragment, InflatedPos};\n\n    fn mkpos(s: &str, off: usize) -> InflatedPos {\n        InflatedPos::from_offsets(s.as_bytes(), &[off]).unwrap()[0]\n    }\n\n    fn mkpos2(s: &str, line: u64, col: u64) -> InflatedPos {\n        InflatedPos::from_lines_cols(s.as_bytes(), &[(line, col)]).unwrap()[0]\n    }\n\n    #[track_caller]\n    fn mkpos_both(s: &str, off: usize) -> InflatedPos {\n        let pos = mkpos(s, off);\n        let pos2 = mkpos2(s, pos.line, pos.utf16column);\n        assert_eq!(pos, pos2);\n        pos\n    }\n\n    #[test]\n    fn ascii_line() {\n        let text = \"Lorem ipsum dolor sit amet, consectetur adipiscing elit,\";\n        for off in 0..text.len() {\n            let pos = mkpos(text, off);\n            let off = off as u64;\n            assert_eq!(pos.line, 0);\n            assert_eq!(pos.column, off);\n            assert_eq!(pos.utf16column, off);\n            assert_eq!(pos.offset, off);\n            assert_eq!(pos.char_offset, off);\n\n            let pos2 = mkpos2(text, pos.line, pos.utf16column);\n            assert_eq!(pos.line, pos2.line);\n            assert_eq!(pos.column, pos2.column);\n            assert_eq!(pos.utf16column, pos2.utf16column);\n            assert_eq!(pos.offset, pos2.offset);\n            assert_eq!(pos.char_offset, pos2.char_offset);\n        }\n    }\n\n    #[test]\n    fn ascii_multi_line() {\n        let text = \"line1\\nline2\";\n        for off in 6..text.len() {\n            let pos = mkpos(text, off);\n            let off = off as u64;\n            assert_eq!(pos.line, 1);\n            assert_eq!(pos.column, off - 6);\n            assert_eq!(pos.utf16column, off - 6);\n            assert_eq!(pos.offset, off);\n            assert_eq!(pos.char_offset, off);\n\n            let pos2 = mkpos2(text, pos.line, pos.utf16column);\n            assert_eq!(pos.line, pos2.line);\n            assert_eq!(pos.column, pos2.column);\n            assert_eq!(pos.utf16column, pos2.utf16column);\n            assert_eq!(pos.offset, pos2.offset);\n            assert_eq!(pos.char_offset, pos2.char_offset);\n        }\n    }\n\n    #[test]\n    fn line_endings() {\n        fn count(s: &str) -> u64 {\n            new_lines_in_fragment(s.as_bytes())\n        }\n        assert_eq!(count(\"line1\\nline2\\nline3\"), 2);\n        assert_eq!(count(\"line1\\rline2\\rline3\"), 2);\n        assert_eq!(count(\"line1\\r\\nline2\\r\\nline3\"), 2);\n        assert_eq!(count(\"line1\\rline2\\r\\nline3\\n\"), 3);\n        assert_eq!(count(\"line1\\nline2\\rline3\\r\\n\"), 3);\n        assert_eq!(count(\"line1\\n\\rline2\\r\\rline3\\r\"), 5);\n    }\n\n    #[test]\n    fn char_offsets_00() {\n        let pos = mkpos_both(\"bomb = 'b'\", 9);\n        assert_eq!(pos.line, 0);\n        assert_eq!(pos.column, 9);\n        assert_eq!(pos.utf16column, 9);\n        assert_eq!(pos.offset, 9);\n        assert_eq!(pos.char_offset, 9);\n    }\n\n    #[test]\n    fn char_offsets_01() {\n        assert!('💣'.len_utf16() == 2);\n\n        // bomb takes 4 bytes when encoded as utf8\n        let pos = mkpos(\"bomb = '💣'\", 12);\n        assert_eq!(pos.line, 0);\n        assert_eq!(pos.column, 10); // bomb takes two columns\n        assert_eq!(pos.utf16column, 10); // and also two 2 utf16 code points\n        assert_eq!(pos.offset, 12);\n        assert_eq!(pos.char_offset, 9);\n    }\n\n    #[test]\n    fn char_offsets_02() {\n        let pos = mkpos_both(\"line1\\nbomb = '💣'\", 18);\n        assert_eq!(pos.line, 1);\n        assert_eq!(pos.column, 10);\n        assert_eq!(pos.utf16column, 10);\n        assert_eq!(pos.offset, 18);\n        assert_eq!(pos.char_offset, 15);\n    }\n    #[test]\n    fn char_offsets_03() {\n        let pos = mkpos_both(\"bomb = '💣'\\nline1\", 18);\n        assert_eq!(pos.line, 1);\n        assert_eq!(pos.column, 4);\n        assert_eq!(pos.utf16column, 4);\n        assert_eq!(pos.offset, 18);\n        assert_eq!(pos.char_offset, 15);\n    }\n    #[test]\n    fn char_offsets_04() {\n        let pos = mkpos_both(\"letter = 'Ф'\", 12);\n        assert_eq!(pos.line, 0);\n        assert_eq!(pos.column, 11);\n        assert_eq!(pos.utf16column, 11);\n        assert_eq!(pos.offset, 12);\n        assert_eq!(pos.char_offset, 11);\n    }\n    #[test]\n    fn char_offsets_05() {\n        let pos = mkpos_both(\"line1\\nletter = 'Ф'\", 18);\n        assert_eq!(pos.line, 1);\n        assert_eq!(pos.column, 11);\n        assert_eq!(pos.utf16column, 11);\n        assert_eq!(pos.offset, 18);\n        assert_eq!(pos.char_offset, 17);\n    }\n    #[test]\n    fn char_offsets_06() {\n        let pos = mkpos_both(\"letter = 'Ф'\\nline1\", 18);\n        assert_eq!(pos.line, 1);\n        assert_eq!(pos.column, 4);\n        assert_eq!(pos.utf16column, 4);\n        assert_eq!(pos.offset, 18);\n        assert_eq!(pos.char_offset, 17);\n    }\n    #[test]\n    fn char_offsets_07() {\n        let pos = mkpos_both(\"letter = 'Ｈ'\", 13);\n        assert_eq!(pos.line, 0);\n        assert_eq!(pos.column, 12);\n        assert_eq!(pos.utf16column, 11);\n        assert_eq!(pos.offset, 13);\n        assert_eq!(pos.char_offset, 11);\n    }\n    #[test]\n    fn char_offsets_08() {\n        let pos = mkpos_both(\"line1\\nletter = 'Ｈ'\", 19);\n        assert_eq!(pos.line, 1);\n        assert_eq!(pos.column, 12);\n        assert_eq!(pos.utf16column, 11);\n        assert_eq!(pos.offset, 19);\n        assert_eq!(pos.char_offset, 17);\n    }\n    #[test]\n    fn char_offsets_09() {\n        let pos = mkpos_both(\"letter = 'Ｈ'\\nline1\", 19);\n        assert_eq!(pos.line, 1);\n        assert_eq!(pos.column, 4);\n        assert_eq!(pos.utf16column, 4);\n        assert_eq!(pos.offset, 19);\n        assert_eq!(pos.char_offset, 17);\n    }\n    #[test]\n    fn char_offsets_10() {\n        let pos = mkpos_both(\"hello\\r\\nworld\", 9);\n        assert_eq!(pos.line, 1);\n        assert_eq!(pos.column, 2);\n        assert_eq!(pos.utf16column, 2);\n        assert_eq!(pos.offset, 9);\n        assert_eq!(pos.char_offset, 9);\n    }\n    #[test]\n    fn char_offsets_11() {\n        let pos = mkpos2(\"hello\\r\\nworld\", 0, 10);\n        assert_eq!(pos.line, 0);\n        assert_eq!(pos.column, 5);\n        assert_eq!(pos.utf16column, 5);\n        assert_eq!(pos.offset, 5);\n        assert_eq!(pos.char_offset, 5);\n    }\n}\n"
  },
  {
    "path": "edb/edgeql-parser/src/preparser.rs",
    "content": "use memchr::memmem::find;\n\n#[derive(Debug, PartialEq)]\npub struct Continuation {\n    position: usize,\n    braces: Vec<u8>,\n}\n\n/// Returns index of semicolon, or position where to continue search on new\n/// data\npub fn full_statement(\n    data: &[u8],\n    continuation: Option<Continuation>,\n) -> Result<usize, Continuation> {\n    let mut iter = data.iter().enumerate().peekable();\n    if let Some(cont) = continuation.as_ref() {\n        if cont.position > 0 {\n            iter.nth(cont.position - 1);\n        }\n    }\n    let mut braces_buf = continuation\n        .map(|cont| cont.braces)\n        .unwrap_or_else(|| Vec::with_capacity(8));\n    'outer: while let Some((idx, b)) = iter.next() {\n        match b {\n            b'\"' => {\n                while let Some((_, b)) = iter.next() {\n                    match b {\n                        b'\\\\' => {\n                            // skip any next char, even quote\n                            iter.next();\n                        }\n                        b'\"' => continue 'outer,\n                        _ => continue,\n                    }\n                }\n                return Err(Continuation {\n                    position: idx,\n                    braces: braces_buf,\n                });\n            }\n            b'\\'' => {\n                while let Some((_, b)) = iter.next() {\n                    match b {\n                        b'\\\\' => {\n                            // skip any next char, even quote\n                            iter.next();\n                        }\n                        b'\\'' => continue 'outer,\n                        _ => continue,\n                    }\n                }\n                return Err(Continuation {\n                    position: idx,\n                    braces: braces_buf,\n                });\n            }\n            b'r' => {\n                if matches!(iter.peek(), Some((_, b'b'))) {\n                    // rb'something' -- skip `b` but match on quote\n                    iter.next();\n                };\n                match iter.peek() {\n                    None => {\n                        return Err(Continuation {\n                            position: idx,\n                            braces: braces_buf,\n                        });\n                    }\n                    Some((_, start @ (b'\\'' | b'\"'))) => {\n                        let end = *start;\n                        iter.next();\n                        for (_, b) in iter.by_ref() {\n                            if b == end {\n                                continue 'outer;\n                            }\n                        }\n                        return Err(Continuation {\n                            position: idx,\n                            braces: braces_buf,\n                        });\n                    }\n                    Some((_, _)) => continue,\n                }\n            }\n            b'`' => {\n                for (_, b) in iter.by_ref() {\n                    match b {\n                        b'`' => continue 'outer,\n                        _ => continue,\n                    }\n                }\n                return Err(Continuation {\n                    position: idx,\n                    braces: braces_buf,\n                });\n            }\n            b'#' => {\n                for (_, &b) in iter.by_ref() {\n                    if b == b'\\n' {\n                        continue 'outer;\n                    }\n                }\n                return Err(Continuation {\n                    position: idx,\n                    braces: braces_buf,\n                });\n            }\n            b'$' => {\n                match iter.next() {\n                    Some((end_idx, b'$')) => {\n                        let end = find(&data[end_idx + 1..], b\"$$\");\n                        if let Some(end) = end {\n                            iter.nth(end + end_idx - idx);\n                            continue 'outer;\n                        }\n                        return Err(Continuation {\n                            position: idx,\n                            braces: braces_buf,\n                        });\n                    }\n                    Some((_, b'A'..=b'Z')) | Some((_, b'a'..=b'z')) | Some((_, b'_')) => {}\n                    // Not a dollar-quote\n                    Some((_, _)) => continue 'outer,\n                    None => {\n                        return Err(Continuation {\n                            position: idx,\n                            braces: braces_buf,\n                        })\n                    }\n                }\n                loop {\n                    let (c_idx, c) = if let Some(pair) = iter.peek() {\n                        *pair\n                    } else {\n                        return Err(Continuation {\n                            position: idx,\n                            braces: braces_buf,\n                        });\n                    };\n                    match c {\n                        b'$' => {\n                            let end_idx = c_idx + 1;\n                            let marker_size = end_idx - idx;\n                            if let Some(end) = find(&data[end_idx..], &data[idx..end_idx]) {\n                                iter.nth(1 + end + marker_size - 1);\n                                continue 'outer;\n                            }\n                            return Err(Continuation {\n                                position: idx,\n                                braces: braces_buf,\n                            });\n                        }\n                        b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_' => {}\n                        // Not a dollar-quote\n                        _ => continue 'outer,\n                    }\n                    iter.next();\n                }\n            }\n            b'{' => braces_buf.push(b'}'),\n            b'(' => braces_buf.push(b')'),\n            b'[' => braces_buf.push(b']'),\n            b'}' | b')' | b']' if braces_buf.last() == Some(b) => {\n                braces_buf.pop();\n            }\n            b';' if braces_buf.is_empty() => return Ok(idx + 1),\n            _ => continue,\n        }\n    }\n    Err(Continuation {\n        position: data.len(),\n        braces: braces_buf,\n    })\n}\n\n/// Returns true if the text has no partial statements\n///\n/// This equivalent to `text.trim().is_empty()` except it also ignores\n/// EdgeQL comments.\n///\n/// This is useful to find out whether last part of text split by\n/// `full_statement` contains anything relevant. Before this function we\n/// couldn't add a comment at the end of EdgeQL file.\npub fn is_empty(text: &str) -> bool {\n    let mut iter = text.chars();\n    loop {\n        let cur_char = match iter.next() {\n            Some(c) => c,\n            None => return true,\n        };\n        match cur_char {\n            '\\u{feff}' | '\\r' | '\\t' | '\\n' | ' ' | ';' => continue,\n            // Comment\n            '#' => {\n                for c in iter.by_ref() {\n                    if c == '\\r' || c == '\\n' {\n                        break;\n                    }\n                }\n                continue;\n            }\n            _ => return false,\n        }\n    }\n}\n"
  },
  {
    "path": "edb/edgeql-parser/src/schema_file.rs",
    "content": "use crate::position::Pos;\nuse crate::tokenizer;\nuse crate::tokenizer::Tokenizer;\n\n#[derive(Debug, thiserror::Error)]\n#[non_exhaustive]\npub enum SchemaFileError {\n    #[error(\"{}: bracket `{}` has never been closed\", pos, kind)]\n    MissingBracket { pos: Pos, kind: char },\n    #[error(\n        \"{}: closing bracket mismatch, opened `{}` at {}, encountered `{}`\",\n        closing_pos,\n        opened,\n        opened_pos,\n        encountered\n    )]\n    BracketMismatch {\n        opened: char,\n        opened_pos: Pos,\n        closing_pos: Pos,\n        encountered: char,\n    },\n    #[error(\"{}: extra closing bracket `{}`\", pos, kind)]\n    ExtraBracket { pos: Pos, kind: char },\n    #[error(\"{}: tokenizer error: {}\", pos, error)]\n    TokenizerError { pos: Pos, error: String },\n}\n\nfn match_bracket(\n    open: char,\n    encountered: char,\n    pos: Pos,\n    brackets: &mut Vec<(char, char, Pos)>,\n) -> Result<(), SchemaFileError> {\n    use SchemaFileError::*;\n\n    match brackets.pop() {\n        Some((_, exp, _)) if exp == encountered => Ok(()),\n        Some((opened, _, opened_pos)) => Err(BracketMismatch {\n            opened,\n            opened_pos,\n            closing_pos: pos,\n            encountered,\n        }),\n        None => Err(ExtraBracket { pos, kind: open }),\n    }\n}\n\npub fn validate(text: &str) -> Result<(), SchemaFileError> {\n    use tokenizer::Kind::*;\n    use SchemaFileError::*;\n\n    let mut token_stream = Tokenizer::new(text);\n    let mut brackets = Vec::new();\n    loop {\n        let pos = token_stream.current_pos();\n        match token_stream.next() {\n            Some(Ok(tok)) => match tok.kind {\n                OpenParen => brackets.push(('(', ')', pos)),\n                OpenBrace => brackets.push(('{', '}', pos)),\n                OpenBracket => brackets.push(('[', ']', pos)),\n                CloseParen => match_bracket('(', ')', pos, &mut brackets)?,\n                CloseBrace => match_bracket('{', '}', pos, &mut brackets)?,\n                CloseBracket => match_bracket('[', ']', pos, &mut brackets)?,\n                _ => {}\n            },\n            None => break,\n            Some(Err(e)) => {\n                return Err(TokenizerError {\n                    pos: token_stream.current_pos(),\n                    error: e.message,\n                });\n            }\n        }\n    }\n    if let Some((kind, _, pos)) = brackets.pop() {\n        return Err(MissingBracket { kind, pos });\n    }\n    Ok(())\n}\n\n#[cfg(test)]\nmod test {\n    use super::validate;\n\n    fn check(s: &str) -> String {\n        validate(s)\n            .map(|_| String::new())\n            .map_err(|e| {\n                let s = e.to_string();\n                assert!(!s.is_empty());\n                s\n            })\n            .unwrap_or_else(|e| e)\n    }\n\n    #[test]\n    fn test_normal() {\n        assert_eq!(check(\"alias X := (SELECT 1)\"), \"\");\n    }\n\n    #[test]\n    fn test_braces() {\n        assert_eq!(\n            check(\"type X { property y := '}';\"),\n            \"1:8: bracket `{` has never been closed\"\n        );\n\n        assert_eq!(\n            check(\"type X { property y -> z; )\"),\n            \"1:27: closing bracket mismatch, \\\n            opened `{` at 1:8, encountered `)`\"\n        );\n\n        assert_eq!(\n            check(\"type X\\nproperty y; }\"),\n            \"2:13: extra closing bracket `{`\"\n        );\n\n        assert_eq!(check(\"type X { property y := (select 1)}\"), \"\");\n\n        assert_eq!(\n            check(\"type X { property y := (select 1})\"),\n            \"1:33: closing bracket mismatch, \\\n            opened `(` at 1:24, encountered `}`\"\n        );\n\n        assert_eq!(\n            check(\"type X { property y := (select 1\"),\n            \"1:24: bracket `(` has never been closed\"\n        );\n\n        assert_eq!(\n            check(\"type X { property y := (select 1)}}\"),\n            \"1:35: extra closing bracket `{`\"\n        );\n\n        assert_eq!(check(\"type X { property y := .z[1]}\"), \"\");\n    }\n\n    #[test]\n    fn test_str() {\n        assert_eq!(\n            check(\"create type X { \\\"} \"),\n            \"1:17: tokenizer error: \\\n                unterminated string, quoted by `\\\"`\"\n        );\n    }\n}\n"
  },
  {
    "path": "edb/edgeql-parser/src/tokenizer.rs",
    "content": "use std::borrow::Cow;\nuse std::fmt;\nuse std::str::CharIndices;\n\nuse bigdecimal::BigDecimal;\nuse memchr::memmem::find;\n\nuse crate::keywords::{self, Keyword};\nuse crate::position::{Pos, Span};\nuse crate::validation::Validator;\n\n// Current max keyword length is 10, but we're reserving some space\npub const MAX_KEYWORD_LENGTH: usize = 16;\n\n#[derive(Debug, Clone)]\n#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]\npub struct Token<'a> {\n    pub kind: Kind,\n    pub text: Cow<'a, str>,\n\n    /// Parsed during validation.\n    pub value: Option<Value>,\n\n    pub span: Span,\n}\n\n#[derive(Debug, Clone, PartialEq)]\n#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]\npub enum Value {\n    String(String),\n    Int(i64),\n    Float(f64),\n    Bytes(Vec<u8>),\n\n    /// Radix 16\n    BigInt(String),\n    Decimal(BigDecimal),\n}\n\n#[derive(Debug, Clone)]\npub struct Error {\n    pub message: String,\n    pub span: Span,\n    pub hint: Option<String>,\n    pub details: Option<String>,\n}\n\nimpl Error {\n    pub fn new<S: ToString>(message: S) -> Self {\n        Error {\n            message: message.to_string(),\n            span: Span::default(),\n            hint: None,\n            details: None,\n        }\n    }\n\n    pub fn with_span(mut self, span: Span) -> Self {\n        self.span = span;\n        self\n    }\n\n    pub fn default_span_to(mut self, span: Span) -> Self {\n        if self.span == Span::default() {\n            self.span = span;\n        }\n        self\n    }\n}\n\n#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]\n#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]\npub enum Kind {\n    Assign,           // :=\n    SubAssign,        // -=\n    AddAssign,        // +=\n    Arrow,            // ->\n    Coalesce,         // ??\n    Namespace,        // ::\n    BackwardLink,     // .<\n    OptionalLink,     // .?>\n    FloorDiv,         // //\n    Concat,           // ++\n    GreaterEq,        // >=\n    LessEq,           // <=\n    NotEq,            // !=\n    NotDistinctFrom,  // ?=\n    DistinctFrom,     // ?!=\n    Comma,            // ,\n    OpenParen,        // (\n    CloseParen,       // )\n    OpenBracket,      // [\n    CloseBracket,     // ]\n    OpenBrace,        // {\n    CloseBrace,       // }\n    Dot,              // .\n    Semicolon,        // ;\n    Colon,            // :\n    Add,              // +\n    Sub,              // -\n    DoubleSplat,      // **\n    Mul,              // *\n    Div,              // /\n    Modulo,           // %\n    Pow,              // ^\n    Less,             // <\n    Greater,          // >\n    Eq,               // =\n    Ampersand,        // &\n    Pipe,             // |\n    At,               // @\n    Parameter,        // $something, $`something`\n    ParameterAndType, // <lit int>$something\n    DecimalConst,\n    FloatConst,\n    IntConst,\n    BigIntConst,\n    BinStr, // b\"xx\", b'xx'\n    Str,    // \"xx\", 'xx', r\"xx\", r'xx', $$xx$$\n\n    StrInterpStart, // \"xx\\(, 'xx\\(\n    StrInterpCont,  // )xx\\(\n    StrInterpEnd,   // )xx\", )xx'\n\n    BacktickName, // `xx`\n    Substitution, // \\(name)\n\n    #[cfg_attr(feature = \"serde\", serde(deserialize_with = \"deserialize_keyword\"))]\n    Keyword(Keyword),\n\n    Ident,\n    EOI,     // end of input\n    Epsilon, // <e> (needed for LR parser)\n\n    StartBlock,\n    StartExtension,\n    StartFragment,\n    StartMigration,\n    StartSDLDocument,\n}\n\n#[derive(Debug, PartialEq, Eq, Clone, Copy)]\nstruct TokenStub<'a> {\n    pub kind: Kind,\n    pub text: &'a str,\n}\n\n#[derive(Debug, PartialEq)]\npub struct Tokenizer<'a> {\n    buf: &'a str,\n    position: Pos,\n    off: usize,\n    dot: bool,\n    next_state: Option<(usize, TokenStub<'a>, usize, Pos, Pos)>,\n    keyword_buf: String,\n    // We maintain a stack of the starting string characters and\n    // parentheses nesting level for all our open string\n    // interpolations, since we need to match the correct one when\n    // closing them.\n    str_interp_stack: Vec<(String, usize)>,\n    // The number of currently open parentheses. If we see a close\n    // paren when there are no open parens *and* we are inside a\n    // string inerpolation, we close it.\n    open_parens: usize,\n}\n\n#[derive(Clone, Debug, PartialEq)]\npub struct Checkpoint {\n    position: Pos,\n    off: usize,\n    dot: bool,\n}\n\nimpl<'a> Iterator for Tokenizer<'a> {\n    type Item = Result<Token<'a>, Error>;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        let start = self.current_pos().offset;\n\n        Some(\n            self.read_token()?\n                .map(|(token, end)| {\n                    let end = end.offset;\n                    Token {\n                        kind: token.kind,\n                        text: token.text.into(),\n                        value: None,\n                        span: Span { start, end },\n                    }\n                })\n                .map_err(|e| {\n                    let end = self.position.offset;\n                    e.with_span(Span { start, end })\n                }),\n        )\n    }\n}\n\nimpl<'a> Tokenizer<'a> {\n    pub fn new(s: &str) -> Tokenizer {\n        let mut me = Tokenizer {\n            buf: s,\n            position: Pos {\n                line: 1,\n                column: 1,\n                offset: 0,\n            },\n            off: 0,\n            dot: false,\n            next_state: None,\n            // Current max keyword length is 10, but we're reserving some\n            // space\n            keyword_buf: String::with_capacity(MAX_KEYWORD_LENGTH),\n            str_interp_stack: Vec::new(),\n            open_parens: 0,\n        };\n        me.skip_whitespace();\n        me\n    }\n\n    /// Start stream a with a modified position\n    ///\n    /// Note: we assume that the current position is at the start of slice `s`\n    pub fn new_at(s: &str, position: Pos) -> Tokenizer {\n        let mut me = Tokenizer {\n            buf: s,\n            position,\n            off: 0,\n            dot: false,\n            next_state: None,\n            keyword_buf: String::with_capacity(MAX_KEYWORD_LENGTH),\n            // XXX: If we are in the middle of an interpolated string we will have trouble\n            str_interp_stack: Vec::new(),\n            open_parens: 0,\n        };\n        me.skip_whitespace();\n        me\n    }\n\n    pub fn validated_values(self) -> Validator<'a> {\n        Validator::new(self)\n    }\n\n    pub fn checkpoint(&self) -> Checkpoint {\n        Checkpoint {\n            position: self.position,\n            off: self.off,\n            dot: self.dot,\n        }\n    }\n\n    pub fn reset(&mut self, checkpoint: Checkpoint) {\n        self.position = checkpoint.position;\n        self.off = checkpoint.off;\n        self.dot = checkpoint.dot;\n    }\n\n    pub fn current_pos(&self) -> Pos {\n        self.position\n    }\n\n    fn read_token(&mut self) -> Option<Result<(TokenStub<'a>, Pos), Error>> {\n        use self::Kind::*;\n\n        // This quickly resets the stream one token back\n        // (the most common reset that used quite often)\n        if let Some((at, tok, off, end, next)) = self.next_state {\n            if at == self.off {\n                self.off = off;\n                self.position = next;\n                return Some(Ok((tok, end)));\n            }\n        }\n\n        let old_pos = self.off;\n        let (kind, len) = match self.peek_token()? {\n            Ok(x) => x,\n            Err(e) => return Some(Err(e)),\n        };\n\n        match kind {\n            StrInterpStart => {\n                let start = self.buf[self.off..].chars().next()?;\n                self.str_interp_stack.push((start.into(), self.open_parens));\n            }\n            StrInterpEnd => {\n                self.str_interp_stack.pop();\n            }\n            OpenParen => {\n                self.open_parens += 1;\n            }\n            CloseParen => {\n                if self.open_parens > 0 {\n                    self.open_parens -= 1;\n                }\n            }\n            _ => {}\n        }\n\n        // note we may want to get rid of \"update_position\" here as it's\n        // faster to update 'as you go', but this is easier to get right first\n        self.update_position(len);\n        self.dot = matches!(kind, Kind::Dot);\n        let value = &self.buf[self.off - len..self.off];\n        let end = self.position;\n\n        self.skip_whitespace();\n        let token = TokenStub { kind, text: value };\n        // This is for quick reset on token back\n        self.next_state = Some((old_pos, token, self.off, end, self.position));\n        Some(Ok((token, end)))\n    }\n\n    fn peek_token(&mut self) -> Option<Result<(Kind, usize), Error>> {\n        let tail = &self.buf[self.off..];\n        let mut iter = tail.char_indices();\n\n        let (_, cur_char) = iter.next()?;\n        Some(self.peek_token_inner(cur_char, tail, &mut iter))\n    }\n\n    fn peek_token_inner(\n        &mut self,\n        cur_char: char,\n        tail: &str,\n        iter: &mut CharIndices<'_>,\n    ) -> Result<(Kind, usize), Error> {\n        use self::Kind::*;\n\n        match cur_char {\n            ':' => match iter.next() {\n                Some((_, '=')) => Ok((Assign, 2)),\n                Some((_, ':')) => Ok((Namespace, 2)),\n                _ => Ok((Colon, 1)),\n            },\n            '-' => match iter.next() {\n                Some((_, '>')) => Ok((Arrow, 2)),\n                Some((_, '=')) => Ok((SubAssign, 2)),\n                _ => Ok((Sub, 1)),\n            },\n            '>' => match iter.next() {\n                Some((_, '=')) => Ok((GreaterEq, 2)),\n                _ => Ok((Greater, 1)),\n            },\n            '<' => match iter.next() {\n                Some((_, '=')) => Ok((LessEq, 2)),\n                _ => Ok((Less, 1)),\n            },\n            '+' => match iter.next() {\n                Some((_, '=')) => Ok((AddAssign, 2)),\n                Some((_, '+')) => Ok((Concat, 2)),\n                _ => Ok((Add, 1)),\n            },\n            '/' => match iter.next() {\n                Some((_, '/')) => Ok((FloorDiv, 2)),\n                _ => Ok((Div, 1)),\n            },\n            '.' => match iter.next() {\n                Some((_, '<')) => Ok((BackwardLink, 2)),\n                Some((_, '?')) => {\n                    if let Some((_, '>')) = iter.next() {\n                        Ok((OptionalLink, 3))\n                    } else {\n                        Err(Error::new(\n                            \"`.?` is not an operator, \\\n                                did you mean `.?>` ?\",\n                        ))\n                    }\n                }\n                _ => Ok((Dot, 1)),\n            },\n            '?' => match iter.next() {\n                Some((_, '?')) => Ok((Coalesce, 2)),\n                Some((_, '=')) => Ok((NotDistinctFrom, 2)),\n                Some((_, '!')) => {\n                    if let Some((_, '=')) = iter.next() {\n                        Ok((DistinctFrom, 3))\n                    } else {\n                        Err(Error::new(\n                            \"`?!` is not an operator, \\\n                                did you mean `?!=` ?\",\n                        ))\n                    }\n                }\n                _ => Err(Error::new(\n                    \"Bare `?` is not an operator, \\\n                            did you mean `?=` or `??` ?\",\n                )),\n            },\n            '!' => match iter.next() {\n                Some((_, '=')) => Ok((NotEq, 2)),\n                _ => Err(Error::new(\n                    \"Bare `!` is not an operator, \\\n                            did you mean `!=`?\",\n                )),\n            },\n            '\"' | '\\'' => self.parse_string(0, false, false),\n            '`' => {\n                while let Some((idx, c)) = iter.next() {\n                    if c == '`' {\n                        if let Some((_, '`')) = iter.next() {\n                            continue;\n                        }\n                        let val = &tail[..idx + 1];\n                        if val.starts_with(\"`@\") {\n                            return Err(Error::new(\n                                \"backtick-quoted name cannot \\\n                                    start with char `@`\",\n                            ));\n                        }\n                        if val.starts_with(\"`$\") {\n                            return Err(Error::new(\n                                \"backtick-quoted name cannot \\\n                                    start with char `$`\",\n                            ));\n                        }\n                        if val.contains(\"::\") {\n                            return Err(Error::new(\n                                \"backtick-quoted name cannot \\\n                                    contain `::`\",\n                            ));\n                        }\n                        if val.starts_with(\"`__\") && val.ends_with(\"__`\") {\n                            return Err(Error::new(\n                                \"backtick-quoted names surrounded by double \\\n                                    underscores are forbidden\",\n                            ));\n                        }\n                        if idx == 1 {\n                            return Err(Error::new(\"backtick quotes cannot be empty\"));\n                        }\n                        return Ok((BacktickName, idx + 1));\n                    }\n                    check_prohibited(c, false)?;\n                }\n                Err(Error::new(\"unterminated backtick name\"))\n            }\n            '=' => Ok((Eq, 1)),\n            ',' => Ok((Comma, 1)),\n            '(' => Ok((OpenParen, 1)),\n            ')' => match self.str_interp_stack.last() {\n                Some((delim, paren_count)) if *paren_count == self.open_parens => {\n                    self.parse_string_interp_cont(delim)\n                }\n                _ => Ok((CloseParen, 1)),\n            },\n            '[' => Ok((OpenBracket, 1)),\n            ']' => Ok((CloseBracket, 1)),\n            '{' => Ok((OpenBrace, 1)),\n            '}' => Ok((CloseBrace, 1)),\n            ';' => Ok((Semicolon, 1)),\n            '*' => match iter.next() {\n                Some((_, '*')) => Ok((DoubleSplat, 2)),\n                _ => Ok((Mul, 1)),\n            },\n            '%' => Ok((Modulo, 1)),\n            '^' => Ok((Pow, 1)),\n            '&' => Ok((Ampersand, 1)),\n            '|' => Ok((Pipe, 1)),\n            '@' => Ok((At, 1)),\n            c if c == '_' || c.is_alphabetic() => {\n                let end_idx = loop {\n                    match iter.next() {\n                        Some((idx, '\"')) | Some((idx, '\\'')) => {\n                            let prefix = &tail[..idx];\n                            let (raw, binary) = match prefix {\n                                \"r\" => (true, false),\n                                \"b\" => (false, true),\n                                \"rb\" => (true, true),\n                                \"br\" => (true, true),\n                                _ => {\n                                    return Err(Error::new(format_args!(\n                                        \"prefix {prefix:?} \\\n                                    is not allowed for strings, \\\n                                    allowed: `b`, `r`\"\n                                    )))\n                                }\n                            };\n                            return self.parse_string(idx, raw, binary);\n                        }\n                        Some((idx, '`')) => {\n                            let prefix = &tail[..idx];\n                            return Err(Error::new(format_args!(\n                                \"prefix {prefix:?} is not \\\n                                allowed for field names, perhaps missing \\\n                                comma or dot?\"\n                            )));\n                        }\n                        Some((_, c)) if c == '_' || c.is_alphanumeric() => continue,\n                        Some((idx, _)) => break idx,\n                        None => break self.buf.len() - self.off,\n                    }\n                };\n                let val = &tail[..end_idx];\n                if let Some(keyword) = self.as_keyword(val) {\n                    Ok((Keyword(keyword), end_idx))\n                } else if val.starts_with(\"__\") && val.ends_with(\"__\") {\n                    return Err(Error::new(\n                        \"identifiers surrounded by double \\\n                            underscores are forbidden\",\n                    ));\n                } else {\n                    return Ok((Ident, end_idx));\n                }\n            }\n            '0'..='9' => {\n                if self.dot {\n                    let len = loop {\n                        match iter.next() {\n                            Some((_, '0'..='9')) => continue,\n                            Some((_, c)) if c.is_alphabetic() => {\n                                return Err(Error::new(format_args!(\n                                    \"unexpected char {c:?}, \\\n                                        only integers are allowed after dot \\\n                                        (for tuple access)\"\n                                )));\n                            }\n                            Some((idx, _)) => break idx,\n                            None => break self.buf.len() - self.off,\n                        }\n                    };\n                    if cur_char == '0' && len > 1 {\n                        return Err(Error::new(\"leading zeros are not allowed in numbers\"));\n                    }\n                    Ok((IntConst, len))\n                } else {\n                    self.parse_number()\n                }\n            }\n            '$' => {\n                let mut has_letter = false;\n                if let Some((_, c)) = iter.next() {\n                    match c {\n                        '$' => {\n                            let suffix = &self.buf[self.off + 2..];\n                            let end = find(suffix.as_bytes(), b\"$$\");\n                            if let Some(end) = end {\n                                for c in self.buf[self.off + 2..][..end].chars() {\n                                    check_prohibited(c, false)?;\n                                }\n                                return Ok((Str, 2 + end + 2));\n                            } else {\n                                return Err(Error::new(\"unterminated string started with $$\"));\n                            }\n                        }\n                        '`' => {\n                            while let Some((idx, c)) = iter.next() {\n                                if c == '`' {\n                                    if let Some((_, '`')) = iter.next() {\n                                        continue;\n                                    }\n                                    let var = &tail[..idx + 1];\n                                    if var.starts_with(\"$`@\") {\n                                        return Err(Error::new(\n                                            \"backtick-quoted argument \\\n                                                cannot start with char `@`\",\n                                        ));\n                                    }\n                                    if var.contains(\"::\") {\n                                        return Err(Error::new(\n                                            \"backtick-quoted argument \\\n                                                cannot contain `::`\",\n                                        ));\n                                    }\n                                    if var.starts_with(\"$`__\") && var.ends_with(\"__`\") {\n                                        return Err(Error::new(\n                                            \"backtick-quoted arguments \\\n                                                surrounded by double \\\n                                                underscores are forbidden\",\n                                        ));\n                                    }\n                                    if idx == 2 {\n                                        return Err(Error::new(\n                                            \"backtick-quoted argument cannot be empty\",\n                                        ));\n                                    }\n                                    return Ok((Parameter, idx + 1));\n                                }\n                                check_prohibited(c, false)?;\n                            }\n                            return Err(Error::new(\"unterminated backtick argument\"));\n                        }\n                        '0'..='9' => {}\n                        c if c.is_alphabetic() || c == '_' => {\n                            has_letter = true;\n                        }\n                        _ => return Err(Error::new(\"bare $ is not allowed\")),\n                    }\n                } else {\n                    return Err(Error::new(\"bare $ is not allowed\"));\n                }\n                let end_idx = loop {\n                    match iter.next() {\n                        Some((end_idx, '$')) => {\n                            let msize = end_idx + 1;\n                            let marker = &self.buf[self.off..][..msize];\n                            if let Some('0'..='9') = marker[1..].chars().next() {\n                                return Err(Error::new(\"dollar quote must not start with a digit\"));\n                            }\n                            if !marker.is_ascii() {\n                                return Err(Error::new(\"dollar quote supports only ascii chars\"));\n                            }\n                            if let Some(end) =\n                                find(&self.buf.as_bytes()[self.off + msize..], marker.as_bytes())\n                            {\n                                let data = &self.buf[self.off + msize..][..end];\n                                for c in data.chars() {\n                                    check_prohibited(c, false)?;\n                                }\n                                return Ok((Str, msize + end + msize));\n                            } else {\n                                return Err(Error::new(format_args!(\n                                    \"unterminated string started with {marker:?}\"\n                                )));\n                            }\n                        }\n                        Some((_, '0'..='9')) => continue,\n                        Some((_, c)) if c.is_alphabetic() || c == '_' => {\n                            has_letter = true;\n                            continue;\n                        }\n                        Some((end_idx, _)) => break end_idx,\n                        None => break self.buf.len() - self.off,\n                    }\n                };\n                if has_letter {\n                    let name = &tail[1..];\n                    if let Some('0'..='9') = name.chars().next() {\n                        return Err(Error::new(format_args!(\n                            \"the {:?} is not a valid \\\n                            argument, either name starting with letter \\\n                            or only digits are expected\",\n                            &tail[..end_idx]\n                        )));\n                    }\n                }\n                Ok((Parameter, end_idx))\n            }\n            '\\\\' => match iter.next() {\n                Some((_, '(')) => {\n                    let len = loop {\n                        match iter.next() {\n                            Some((_, '_')) => continue,\n                            Some((_, c)) if c.is_alphanumeric() => continue,\n                            Some((idx, ')')) => break idx,\n                            Some((_, _)) => {\n                                return Err(Error::new(\n                                    \"only alphanumerics are allowed in \\\n                                     \\\\(name) token\",\n                                ));\n                            }\n                            None => {\n                                return Err(Error::new(\"unclosed \\\\(name) token\"));\n                            }\n                        }\n                    };\n                    Ok((Substitution, len + 1))\n                }\n                _ => Err(Error::new(format_args!(\n                    \"unexpected character {cur_char:?}\",\n                ))),\n            },\n            _ => Err(Error::new(format_args!(\n                \"unexpected character {cur_char:?}\",\n            ))),\n        }\n    }\n\n    fn parse_string(\n        &self,\n        quote_off: usize,\n        raw: bool,\n        binary: bool,\n    ) -> Result<(Kind, usize), Error> {\n        let mut iter = self.buf[self.off + quote_off..].char_indices();\n        let open_quote = iter.next().unwrap().1;\n        if binary {\n            while let Some((idx, c)) = iter.next() {\n                match c {\n                    '\\\\' if !raw => match iter.next() {\n                        // skip any next char, even quote\n                        Some((_, _)) => continue,\n                        None => break,\n                    },\n                    c if c as u32 > 0x7f => {\n                        return Err(Error::new(format_args!(\n                            \"invalid bytes literal: character \\\n                                {c:?} is unexpected, only ascii chars are \\\n                                allowed in bytes literals\"\n                        )));\n                    }\n                    c if c == open_quote => return Ok((Kind::BinStr, quote_off + idx + 1)),\n                    _ => {}\n                }\n            }\n        } else {\n            while let Some((idx, c)) = iter.next() {\n                match c {\n                    '\\\\' if !raw => match iter.next() {\n                        Some((idx, '(')) => return Ok((Kind::StrInterpStart, quote_off + idx + 1)),\n                        // skip any next char, even quote\n                        Some((_, _)) => continue,\n                        None => break,\n                    },\n                    c if c == open_quote => return Ok((Kind::Str, quote_off + idx + 1)),\n                    _ => check_prohibited(c, true)?,\n                }\n            }\n        }\n        Err(Error::new(format_args!(\n            \"unterminated string, quoted by `{open_quote}`\"\n        )))\n    }\n\n    fn parse_string_interp_cont(&self, end: &str) -> Result<(Kind, usize), Error> {\n        let quote_off = 1;\n        let mut iter = self.buf[self.off + quote_off..].char_indices();\n\n        while let Some((idx, c)) = iter.next() {\n            match c {\n                '\\\\' => match iter.next() {\n                    Some((idx, '(')) => return Ok((Kind::StrInterpCont, quote_off + idx + 1)),\n                    // skip any next char, even quote\n                    Some((_, _)) => continue,\n                    None => break,\n                },\n                _ if self.buf[self.off + quote_off + idx..].starts_with(end) => {\n                    return Ok((Kind::StrInterpEnd, quote_off + idx + end.len()))\n                }\n                _ => check_prohibited(c, true)?,\n            }\n        }\n        Err(Error::new(format_args!(\n            \"unterminated string with interpolations, quoted by `{end}`\",\n        )))\n    }\n\n    fn parse_number(&mut self) -> Result<(Kind, usize), Error> {\n        #[derive(PartialEq, PartialOrd)]\n        enum Break {\n            Dot,\n            Exponent,\n            Letter,\n            End,\n        }\n        use self::Kind::*;\n        let mut iter = self.buf[self.off + 1..].char_indices();\n        let mut suffix = None;\n        let mut decimal = false;\n        // decimal part\n        let (mut bstate, dec_len) = loop {\n            match iter.next() {\n                Some((_, '0'..='9')) => continue,\n                Some((_, '_')) => continue,\n                Some((idx, 'e')) => break (Break::Exponent, idx + 1),\n                Some((idx, '.')) => break (Break::Dot, idx + 1),\n                Some((idx, c)) if c.is_alphabetic() => {\n                    suffix = Some(idx + 1);\n                    break (Break::Letter, idx + 1);\n                }\n                Some((idx, _)) => break (Break::End, idx + 1),\n                None => break (Break::End, self.buf.len() - self.off),\n            }\n        };\n        if self.buf.as_bytes()[self.off] == b'0' && dec_len > 1 {\n            return Err(Error::new(\n                \"unexpected leading zeros are not allowed in numbers\",\n            ));\n        }\n        if bstate == Break::End {\n            return Ok((IntConst, dec_len));\n        }\n        if bstate == Break::Dot {\n            decimal = true;\n            bstate = loop {\n                if let Some((idx, c)) = iter.next() {\n                    match c {\n                        '0'..='9' => continue,\n                        '_' => {\n                            if idx + 1 == dec_len + 1 {\n                                return Err(Error::new(\n                                    \"expected digit after dot, \\\n                                    found underscore\",\n                                ));\n                            }\n                            continue;\n                        }\n                        'e' => {\n                            if idx + 1 == dec_len + 1 {\n                                return Err(Error::new(\n                                    \"expected digit after dot, \\\n                                    found exponent\",\n                                ));\n                            }\n                            break Break::Exponent;\n                        }\n                        '.' => return Err(Error::new(\"unexpected extra decimal dot in number\")),\n                        c if c.is_alphabetic() => {\n                            if idx == dec_len {\n                                return Err(Error::new(\"expected digit after dot, found suffix\"));\n                            }\n                            suffix = Some(idx + 1);\n                            break Break::Letter;\n                        }\n                        _ => {\n                            if idx + 1 == dec_len + 1 {\n                                return Err(Error::new(\n                                    \"expected digit after dot, \\\n                                    found end of decimal\",\n                                ));\n                            }\n                            return Ok((FloatConst, idx + 1));\n                        }\n                    }\n                } else {\n                    if self.buf.len() - self.off == dec_len + 1 {\n                        return Err(Error::new(\"expected digit after dot, found end of decimal\"));\n                    }\n                    return Ok((FloatConst, self.buf.len() - self.off));\n                }\n            }\n        }\n        if bstate == Break::Exponent {\n            match iter.next() {\n                Some((_, '0'..='9')) => {}\n                Some((_, c @ '+')) | Some((_, c @ '-')) => {\n                    if c == '-' {\n                        decimal = true;\n                    }\n                    match iter.next() {\n                        Some((_, '0'..='9')) => {}\n                        Some((_, '.')) => {\n                            return Err(Error::new(\"unexpected extra decimal dot in number\"))\n                        }\n                        _ => {\n                            return Err(Error::new(\n                                \"unexpected optional `+` or `-` followed by digits must \\\n                                follow `e` in float const\",\n                            ))\n                        }\n                    }\n                }\n                _ => {\n                    return Err(Error::new(\n                        \"unexpected optional `+` or `-` followed by digits must \\\n                        follow `e` in float const\",\n                    ))\n                }\n            }\n            loop {\n                match iter.next() {\n                    Some((_, '0'..='9')) => continue,\n                    Some((_, '_')) => continue,\n                    Some((_, '.')) => {\n                        return Err(Error::new(\"unexpected extra decimal dot in number\"))\n                    }\n                    Some((idx, c)) if c.is_alphabetic() => {\n                        suffix = Some(idx + 1);\n                        break;\n                    }\n                    Some((idx, _)) => return Ok((FloatConst, idx + 1)),\n                    None => return Ok((FloatConst, self.buf.len() - self.off)),\n                }\n            }\n        }\n        let soff = suffix.expect(\"tokenizer integrity error\");\n        let end = loop {\n            if let Some((idx, c)) = iter.next() {\n                if c != '_' && !c.is_alphanumeric() {\n                    break idx + 1;\n                }\n            } else {\n                break self.buf.len() - self.off;\n            }\n        };\n        let suffix = &self.buf[self.off + soff..self.off + end];\n        if suffix == \"n\" {\n            if decimal {\n                Ok((DecimalConst, end))\n            } else {\n                Ok((BigIntConst, end))\n            }\n        } else {\n            let suffix = if suffix.len() > 8 {\n                Cow::Owned(format!(\"{}...\", &suffix[..8]))\n            } else {\n                Cow::Borrowed(suffix)\n            };\n            let val = if soff < 20 {\n                &self.buf[self.off..][..soff]\n            } else {\n                \"123\"\n            };\n            if suffix.starts_with('O') {\n                Err(Error::new(format_args!(\n                    \"suffix {suffix:?} is invalid for \\\n                        numbers, perhaps mixed up letter `O` \\\n                        with zero `0`?\"\n                )))\n            } else if decimal {\n                return Err(Error::new(format_args!(\n                    \"suffix {suffix:?} is invalid for \\\n                        numbers, perhaps you wanted `{val}n` (decimal)?\"\n                )));\n            } else {\n                return Err(Error::new(format_args!(\n                    \"suffix {suffix:?} is invalid for \\\n                        numbers, perhaps you wanted `{val}n` (bigint)?\"\n                )));\n            }\n        }\n    }\n\n    fn skip_whitespace(&mut self) {\n        let mut iter = self.buf[self.off..].char_indices();\n        let idx = 'outer: loop {\n            let (idx, cur_char) = match iter.next() {\n                Some(pair) => pair,\n                None => break self.buf.len() - self.off,\n            };\n            match cur_char {\n                '\\u{feff}' | '\\r' => continue,\n                '\\t' => self.position.column += 8,\n                '\\n' => {\n                    self.position.column = 1;\n                    self.position.line += 1;\n                }\n                // comma is also entirely ignored in spec\n                ' ' => {\n                    self.position.column += 1;\n                    continue;\n                }\n                //comment\n                '#' => {\n                    for (idx, cur_char) in iter.by_ref() {\n                        if check_prohibited(cur_char, false).is_err() {\n                            // can't return error from skip_whitespace\n                            // but we return up to this char, so the tokenizer\n                            // chokes on it next time is invoked\n                            break 'outer idx;\n                        }\n                        if cur_char == '\\r' || cur_char == '\\n' {\n                            self.position.column = 1;\n                            self.position.line += 1;\n                            break;\n                        }\n                    }\n                    continue;\n                }\n                _ => break idx,\n            }\n        };\n        self.off += idx;\n        self.position.offset += idx as u64;\n    }\n\n    fn update_position(&mut self, len: usize) {\n        let val = &self.buf[self.off..][..len];\n        self.off += len;\n        let lines = val.as_bytes().iter().filter(|&&x| x == b'\\n').count();\n        self.position.line += lines;\n        if lines > 0 {\n            let line_offset = val.rfind('\\n').unwrap() + 1;\n            let num = val[line_offset..].chars().count();\n            self.position.column = num + 1;\n        } else {\n            let num = val.chars().count();\n            self.position.column += num;\n        }\n        self.position.offset += len as u64;\n    }\n\n    fn as_keyword(&mut self, s: &str) -> Option<Keyword> {\n        if s.len() > MAX_KEYWORD_LENGTH {\n            return None;\n        }\n        self.keyword_buf.clear();\n        self.keyword_buf.push_str(s);\n        self.keyword_buf.make_ascii_lowercase();\n        keywords::lookup_all(&self.keyword_buf)\n    }\n}\n\nimpl fmt::Display for TokenStub<'_> {\n    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n        write!(f, \"{}[{:?}]\", self.text, self.kind)\n    }\n}\n\nimpl fmt::Display for Token<'_> {\n    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n        write!(f, \"{}[{:?}]\", self.text, self.kind)\n    }\n}\n\nimpl Token<'_> {\n    pub fn cloned(self) -> Token<'static> {\n        Token {\n            kind: self.kind,\n            text: Cow::<'static, str>::Owned(self.text.to_string()),\n            value: self.value,\n            span: self.span,\n        }\n    }\n}\n\nfn check_prohibited(c: char, escape: bool) -> Result<(), Error> {\n    match c {\n        '\\0' if escape => Err(Error::new(\"character U+0000 is not allowed\")),\n        '\\0' | '\\u{202A}' | '\\u{202B}' | '\\u{202C}' | '\\u{202D}' | '\\u{202E}' | '\\u{2066}'\n        | '\\u{2067}' | '\\u{2068}' | '\\u{2069}' => {\n            if escape {\n                Err(Error::new(format!(\n                    \"character U+{0:04X} is not allowed, \\\n                     use escaped form \\\\u{0:04x}\",\n                    c as u32\n                )))\n            } else {\n                Err(Error::new(format!(\n                    \"character U+{:04X} is not allowed\",\n                    c as u32\n                )))\n            }\n        }\n        _ => Ok(()),\n    }\n}\n\nimpl std::cmp::PartialEq for Token<'_> {\n    fn eq(&self, other: &Self) -> bool {\n        self.kind == other.kind && self.text == other.text && self.value == other.value\n    }\n}\n\nimpl std::fmt::Display for Error {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        f.write_str(&self.message)\n    }\n}\n\n#[cfg(feature = \"serde\")]\nfn deserialize_keyword<'de, D>(deserializer: D) -> Result<Keyword, D::Error>\nwhere\n    D: serde::Deserializer<'de>,\n{\n    struct Visitor;\n    use serde::de;\n\n    impl de::Visitor<'_> for Visitor {\n        type Value = Keyword;\n\n        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {\n            formatter.write_str(\"EdgeQL keyword\")\n        }\n\n        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>\n        where\n            E: de::Error,\n        {\n            keywords::lookup_all(v)\n                .ok_or_else(|| de::Error::invalid_value(de::Unexpected::Str(v), &\"keyword\"))\n        }\n    }\n\n    deserializer.deserialize_str(Visitor)\n}\n\nimpl Kind {\n    pub fn text(&self) -> Option<&'static str> {\n        use Kind::*;\n\n        Some(match self {\n            Add => \"+\",\n            Ampersand => \"&\",\n            At => \"@\",\n            BackwardLink => \".<\",\n            OptionalLink => \".?>\",\n            CloseBrace => \"}\",\n            CloseBracket => \"]\",\n            CloseParen => \")\",\n            Coalesce => \"??\",\n            Colon => \":\",\n            Comma => \",\",\n            Concat => \"++\",\n            Div => \"/\",\n            Dot => \".\",\n            DoubleSplat => \"**\",\n            Eq => \"=\",\n            FloorDiv => \"//\",\n            Modulo => \"%\",\n            Mul => \"*\",\n            Namespace => \"::\",\n            OpenBrace => \"{\",\n            OpenBracket => \"[\",\n            OpenParen => \"(\",\n            Pipe => \"|\",\n            Pow => \"^\",\n            Semicolon => \";\",\n            Sub => \"-\",\n\n            DistinctFrom => \"?!=\",\n            GreaterEq => \">=\",\n            LessEq => \"<=\",\n            NotDistinctFrom => \"?=\",\n            NotEq => \"!=\",\n            Less => \"<\",\n            Greater => \">\",\n\n            AddAssign => \"+=\",\n            Arrow => \"->\",\n            Assign => \":=\",\n            SubAssign => \"-=\",\n\n            Keyword(keywords::Keyword(kw)) => kw,\n\n            _ => return None,\n        })\n    }\n\n    pub fn user_friendly_text(&self) -> Option<&'static str> {\n        use Kind::*;\n        Some(match self {\n            Ident => \"identifier\",\n            EOI => \"end of input\",\n\n            BinStr => \"binary constant\",\n            FloatConst => \"float constant\",\n            IntConst => \"int constant\",\n            DecimalConst => \"decimal constant\",\n            BigIntConst => \"big int constant\",\n            Str => \"string constant\",\n\n            _ => return None,\n        })\n    }\n}\n"
  },
  {
    "path": "edb/edgeql-parser/src/validation.rs",
    "content": "use std::str::FromStr;\n\nuse bigdecimal::num_bigint::ToBigInt;\nuse bigdecimal::BigDecimal;\n\nuse crate::helpers::{unquote_bytes, unquote_string};\nuse crate::keywords::Keyword;\nuse crate::position::{Pos, Span};\nuse crate::tokenizer::{Error, Kind, Token, Tokenizer, Value, MAX_KEYWORD_LENGTH};\n\n/// Applies additional validation to the tokens.\n/// Combines multi-word keywords into single tokens.\n/// Remaps a few token kinds.\npub struct Validator<'a> {\n    pub inner: Tokenizer<'a>,\n\n    pub(super) peeked: Option<Option<Result<Token<'a>, Error>>>,\n    pub(super) keyword_buf: String,\n}\n\nimpl<'a> Iterator for Validator<'a> {\n    type Item = Result<Token<'a>, Error>;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        let mut token = match self.next_inner()? {\n            Ok(t) => t,\n            Err(e) => return Some(Err(e)),\n        };\n\n        token.value = match parse_value(&token) {\n            Ok(x) => x,\n            Err(e) => return Some(Err(Error::new(e).with_span(token.span))),\n        };\n\n        if let Some(keyword) = self.combine_multi_word_keywords(&token) {\n            token.text = keyword.into();\n            token.kind = Kind::Keyword(Keyword(keyword));\n            self.peeked = None;\n        }\n\n        token.kind = remap_kind(token.kind);\n\n        Some(Ok(token))\n    }\n}\n\nimpl<'a> Validator<'a> {\n    pub(super) fn new(inner: Tokenizer<'a>) -> Self {\n        Validator {\n            inner,\n            peeked: None,\n            keyword_buf: String::with_capacity(MAX_KEYWORD_LENGTH),\n        }\n    }\n\n    pub fn with_eof(self) -> WithEof<'a> {\n        WithEof {\n            inner: self,\n            emitted: false,\n        }\n    }\n\n    /// Mimics behavior of [std::iter::Peekable]. We could use that, but it\n    /// hides access to underlying iterator.\n    fn next_inner(&mut self) -> Option<Result<Token<'a>, Error>> {\n        if let Some(peeked) = self.peeked.take() {\n            peeked\n        } else {\n            self.inner.next()\n        }\n    }\n\n    /// Mimics behavior of [std::iter::Peekable]. We could use that, but it\n    /// hides access to underlying iterator.\n    fn peek(&mut self) -> &Option<Result<Token, Error>> {\n        if self.peeked.is_none() {\n            self.peeked = Some(self.inner.next());\n        }\n\n        self.peeked.as_ref().unwrap()\n    }\n\n    pub fn current_pos(&self) -> Pos {\n        self.inner.current_pos()\n    }\n\n    fn combine_multi_word_keywords(&mut self, token: &Token<'a>) -> Option<&'static str> {\n        if !matches!(token.kind, Kind::Ident | Kind::Keyword(_)) {\n            return None;\n        }\n        let text = &token.text;\n\n        if text.len() > MAX_KEYWORD_LENGTH {\n            return None;\n        }\n\n        self.keyword_buf.clear();\n        self.keyword_buf.push_str(text);\n        self.keyword_buf.make_ascii_lowercase();\n        match &self.keyword_buf[..] {\n            \"named\" => {\n                if self.peek_keyword(\"only\") {\n                    return Some(\"named only\");\n                }\n            }\n            \"set\" => {\n                if self.peek_keyword(\"annotation\") {\n                    return Some(\"set annotation\");\n                }\n                if self.peek_keyword(\"type\") {\n                    return Some(\"set type\");\n                }\n            }\n            \"extension\" => {\n                if self.peek_keyword(\"package\") {\n                    return Some(\"extension package\");\n                }\n            }\n            \"order\" => {\n                if self.peek_keyword(\"by\") {\n                    return Some(\"order by\");\n                }\n            }\n            _ => {}\n        }\n        None\n    }\n\n    fn peek_keyword(&mut self, kw: &'static str) -> bool {\n        self.peek()\n            .as_ref()\n            .and_then(|res| res.as_ref().ok())\n            .map(|t| {\n                t.kind == Kind::Keyword(Keyword(kw))\n                    || (t.kind == Kind::Ident && t.text.eq_ignore_ascii_case(kw))\n            })\n            .unwrap_or(false)\n    }\n}\n\npub fn parse_value(token: &Token) -> Result<Option<Value>, String> {\n    use Kind::*;\n    let text = &token.text;\n    let string_value = match token.kind {\n        Parameter => {\n            if text[1..].starts_with('`') {\n                text[2..text.len() - 1].replace(\"``\", \"`\")\n            } else {\n                text[1..].to_string()\n            }\n        }\n        DecimalConst => {\n            return text[..text.len() - 1]\n                .replace('_', \"\")\n                .parse()\n                .map(Value::Decimal)\n                .map(Some)\n                .map_err(|e| format!(\"can't parse decimal: {e}\"))\n        }\n        FloatConst => {\n            return text\n                .replace('_', \"\")\n                .parse::<f64>()\n                .map_err(|e| format!(\"can't parse std::float64: {e}\"))\n                .and_then(|num| {\n                    if num.is_infinite() {\n                        return Err(\"number is out of range for std::float64\".to_string());\n                    }\n                    if num == 0.0 {\n                        let mend = text.find(['e', 'E']).unwrap_or(text.len());\n                        let mantissa = &text[..mend];\n                        if mantissa.chars().any(|c| c != '0' && c != '.') {\n                            return Err(\"number is out of range for std::float64\".to_string());\n                        }\n                    }\n                    Ok(num)\n                })\n                .map(Value::Float)\n                .map(Some);\n        }\n        IntConst => {\n            // We read unsigned here, because unary minus will only\n            // be identified on the parser stage. And there is a number\n            // -9223372036854775808 which can't be represented in\n            // i64 as absolute (positive) value.\n            // Python has no problem of representing such a positive\n            // value, though.\n            return u64::from_str(&text.replace('_', \"\"))\n                .map(|x| Some(Value::Int(x as i64)))\n                .map_err(|e| format!(\"error reading int: {e}\"));\n        }\n        BigIntConst => {\n            return text[..text.len() - 1]\n                .replace('_', \"\")\n                .parse::<BigDecimal>()\n                .map_err(|e| format!(\"error reading bigint: {e}\"))\n                // this conversion to decimal and back to string\n                // fixes thing like `1e2n` which we support for bigints\n                .and_then(|x| {\n                    x.to_bigint()\n                        .ok_or_else(|| \"number is not integer\".to_string())\n                })\n                .map(|x| Some(Value::BigInt(x.to_str_radix(16))));\n        }\n        BinStr => {\n            return unquote_bytes(text).map(Value::Bytes).map(Some);\n        }\n\n        Str | StrInterpStart | StrInterpEnd | StrInterpCont => {\n            unquote_string(text).map_err(|s| s.to_string())?.to_string()\n        }\n        BacktickName => text[1..text.len() - 1].replace(\"``\", \"`\"),\n        Ident | Keyword(_) => text.to_string(),\n        Substitution => text[2..text.len() - 1].to_string(),\n        _ => return Ok(None),\n    };\n    Ok(Some(Value::String(string_value)))\n}\n\nfn remap_kind(kind: Kind) -> Kind {\n    match kind {\n        Kind::BacktickName => Kind::Ident,\n        kind => kind,\n    }\n}\n\npub struct WithEof<'a> {\n    inner: Validator<'a>,\n\n    emitted: bool,\n}\n\nimpl<'a> Iterator for WithEof<'a> {\n    type Item = Result<Token<'a>, Error>;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        if let Some(next) = self.inner.next() {\n            Some(next)\n        } else if !self.emitted {\n            self.emitted = true;\n            let pos = self.inner.current_pos().offset;\n\n            Some(Ok(Token {\n                kind: Kind::EOI,\n                text: \"\".into(),\n                value: None,\n                span: Span {\n                    start: pos,\n                    end: pos,\n                },\n            }))\n        } else {\n            None\n        }\n    }\n}\n"
  },
  {
    "path": "edb/edgeql-parser/tests/expr.rs",
    "content": "use edgeql_parser::expr::check;\n\n#[test]\nfn test_valid() {\n    check(\"1\").unwrap();\n    check(\" 42    \").unwrap();\n    check(\"42 # )\").unwrap();\n    check(\"33 ++ 44\").unwrap();\n    check(\"33 ++ '44'\").unwrap();\n    check(\"(1, 2) # tuple\").unwrap();\n    check(\"# next line\\n 2+2\").unwrap();\n    check(\"{}\").unwrap();\n    check(\"()\").unwrap();\n    check(\".user.name\").unwrap();\n    check(\"call(me.maybe)\").unwrap();\n    check(\"bad +/- grammar **** but --- allowed\").unwrap();\n}\n\nfn check_err(s: &str) -> String {\n    check(s).unwrap_err().to_string()\n}\n\n#[test]\nfn test_empty() {\n    assert_eq!(check_err(\"\"), \"expression is empty\");\n    assert_eq!(check_err(\"   \"), \"expression is empty\");\n    assert_eq!(check_err(\"# xxx + yyy\"), \"expression is empty\");\n}\n\n#[test]\nfn bad_token() {\n    assert_eq!(\n        check_err(\"'quote\"),\n        \"1:1: tokenizer error: unterminated string, quoted by `'`\"\n    );\n    assert_eq!(\n        check_err(\"\\\\(quote\"),\n        \"1:1: tokenizer error: unclosed \\\\(name) token\"\n    );\n}\n\n#[test]\nfn bracket_mismatch() {\n    assert_eq!(\n        check_err(\"(a[12)]\"),\n        \"1:6: closing bracket mismatch, \\\n            opened \\\"[\\\" at 1:3, encountered \\\")\\\"\"\n    );\n    assert_eq!(\n        check_err(\"(a12]\"),\n        \"1:5: closing bracket mismatch, \\\n            opened \\\"(\\\" at 1:1, encountered \\\"]\\\"\"\n    );\n    assert_eq!(\n        check_err(\"{'}']\"),\n        \"1:5: closing bracket mismatch, \\\n            opened \\\"{\\\" at 1:1, encountered \\\"]\\\"\"\n    );\n}\n\n#[test]\nfn extra_brackets() {\n    assert_eq!(check_err(\"func())\"), \"1:7: extra closing bracket \\\")\\\"\");\n    assert_eq!(check_err(\"{} + x]\"), \"1:7: extra closing bracket \\\"]\\\"\");\n    assert_eq!(\n        check_err(\"{'xxx(yyy'})\"),\n        \"1:12: extra closing bracket \\\")\\\"\"\n    );\n}\n\n#[test]\nfn missing_brackets() {\n    assert_eq!(\n        check_err(\"func((1, 2)\"),\n        \"1:5: bracket \\\"(\\\" has never been closed\"\n    );\n    assert_eq!(\n        check_err(\"{(1, 2), (3, '}')\"),\n        \"1:1: bracket \\\"{\\\" has never been closed\"\n    );\n    assert_eq!(\n        check_err(\"{((())[[()\"),\n        \"1:8: bracket \\\"[\\\" has never been closed\"\n    );\n}\n\n#[test]\nfn delimiter() {\n    assert_eq!(\n        check_err(\"1, 2\"),\n        \"1:2: token \\\",\\\" is not allowed in expression \\\n         (try parenthesize the expression)\"\n    );\n    check(\"(1, 2)\").unwrap();\n\n    assert_eq!(\n        check_err(\"create type Type1;\"),\n        \"1:18: token \\\";\\\" is not allowed in expression \\\n         (try parenthesize the expression)\"\n    );\n    // this doesn't work, but is fun to see\n    check(\"{create if not exists type Type1; SELECT Type1}\").unwrap();\n}\n"
  },
  {
    "path": "edb/edgeql-parser/tests/preparser.rs",
    "content": "use edgeql_parser::preparser::{full_statement, is_empty};\n\nfn test_statement(data: &[u8], len: usize) {\n    for i in 0..len - 1 {\n        let c = full_statement(&data[..i], None).unwrap_err();\n        let parsed_len = full_statement(data, Some(c)).unwrap();\n        assert_eq!(len, parsed_len, \"at {i}\");\n    }\n    for i in len..data.len() {\n        let parsed_len = full_statement(&data[..i], None).unwrap();\n        assert_eq!(len, parsed_len);\n    }\n}\n\n#[test]\nfn test_simple() {\n    test_statement(b\"select 1+1; some trailer\", 11);\n}\n\n#[test]\nfn test_quotes() {\n    test_statement(b\"select \\\"x\\\"; some trailer\", 11);\n}\n\n#[test]\nfn test_quoted_semicolon() {\n    test_statement(b\"select \\\"a;\\\"; some trailer\", 12);\n}\n\n#[test]\nfn test_raw_string() {\n    test_statement(br#\"select r\"\\\"; some trailer\"#, 12);\n}\n\n#[test]\nfn test_raw_byte_string() {\n    test_statement(br#\"select rb\"\\\"; some trailer\"#, 13);\n    test_statement(br\"select br'hello\\'; some trailer\", 18);\n}\n\n#[test]\nfn test_single_quoted_semicolon() {\n    test_statement(b\"select 'a;'; some trailer\", 12);\n}\n\n#[test]\nfn test_backtick_quoted_semicolon() {\n    test_statement(b\"select `a;`; some trailer\", 12);\n}\n\n#[test]\nfn test_commented_semicolon() {\n    test_statement(b\"select # test;\\n1+1;\", 19);\n}\n\n#[test]\nfn test_continuation() {\n    test_statement(b\"select 'a;'; '\", 12);\n}\n\n#[test]\nfn test_quoted_continuation() {\n    test_statement(b\"select \\\"a; \\\";\", 13);\n}\n\n#[test]\nfn test_single_quoted_continuation() {\n    test_statement(b\"select 'a; ' ;\", 14);\n}\n\n#[test]\nfn test_backtick_quoted_continuation() {\n    test_statement(b\"select `a;test`+1;\", 18);\n}\n\n#[test]\nfn test_dollar_semicolon() {\n    test_statement(b\"select $$ ; $$ test;\", 20);\n    test_statement(b\"select $$$$;\", 12);\n    test_statement(b\"select $$$ ; $$;\", 16);\n    test_statement(b\"select $some_L0ng_name$ ; $some_L0ng_name$;\", 43);\n}\n\n#[test]\nfn test_nested_dollar() {\n    test_statement(b\"select $a$ ; $b$ ; $b$ ; $a$; x\", 29);\n    test_statement(b\"select $a$ ; $b$ ; $a$; x\", 23);\n}\n\n#[test]\nfn test_dollar_continuation() {\n    test_statement(b\"select $$ ; $ab$ test; $$ ;\", 27);\n    test_statement(b\"select $a$ ; $$ test; $a$ ;\", 27);\n    test_statement(b\"select $a$ ; test; $a$ ;\", 24);\n    test_statement(b\"select $a$a$ ; $$ test; $a$;\", 28);\n    test_statement(b\"select $a$ ; $b$ ; $c$ ; $b$ test; $a$;\", 39);\n}\n\n#[test]\nfn test_dollar_var() {\n    test_statement(b\"select $a+b; $ test; $a+b; $ ;\", 12);\n    test_statement(b\"select $a b; $ test; $a b; $ ;\", 12);\n}\n\n#[test]\nfn test_after_variable() {\n    test_statement(b\"select $$ $$; extra;\", 13);\n    test_statement(b\"select $a$ $a$; extra;\", 15);\n    test_statement(b\"select $a;\", 10);\n    test_statement(b\"select $a{ x; };\", 16);\n}\n\n#[test]\nfn test_schema() {\n    test_statement(\n        br###\"\n        START MIGRATION TO {\n            module default {\n                type Movie {\n                    required property title -> str;\n                    # the year of release\n                    property year -> int64;\n                    required link director -> Person;\n                    multi link actors -> Person;\n                }\n                type Person {\n                    required property first_name -> str;\n                    required property last_name -> str;\n                }\n            }\n        };\n        \"###,\n        532,\n    );\n}\n\n#[test]\nfn test_function() {\n    test_statement(b\"drop function foo(s: str); \", 26);\n}\n\n#[test]\nfn empty() {\n    assert!(is_empty(\"\"));\n    assert!(is_empty(\" \"));\n    assert!(is_empty(\"\\n\"));\n    assert!(is_empty(\"#xx\"));\n    assert!(is_empty(\"#xx\\n\"));\n    assert!(is_empty(\"# xx\\n# yy\"));\n    assert!(is_empty(\" #xx\\n  #yy\"));\n    assert!(is_empty(\";\"));\n    assert!(is_empty(\";;\"));\n    assert!(is_empty(\"    ;\\n#cd\"));\n    assert!(!is_empty(\"a\"));\n    assert!(!is_empty(\"ab cd\"));\n    assert!(!is_empty(\",\"));\n    assert!(!is_empty(\";ab;\"));\n    assert!(!is_empty(\"ab;;de\"));\n    assert!(!is_empty(\"    xy\"));\n    assert!(!is_empty(\"    xy #c\"));\n    assert!(!is_empty(\"    '#c\"));\n    assert!(!is_empty(\"ab\\n#cd\"));\n}\n"
  },
  {
    "path": "edb/edgeql-parser/tests/tokenizer.rs",
    "content": "use edgeql_parser::tokenizer::Kind::*;\nuse edgeql_parser::tokenizer::{Kind, Tokenizer};\n\nfn tok_str(s: &str) -> Vec<String> {\n    let mut r = Vec::new();\n    let mut s = Tokenizer::new(s).validated_values();\n    loop {\n        match s.next() {\n            Some(Ok(x)) => r.push(x.text.to_string()),\n            None => break,\n            Some(Err(e)) => panic!(\"Parse error at {}: {}\", e.span.start, e.message),\n        }\n    }\n    r\n}\n\nfn tok_typ(s: &str) -> Vec<Kind> {\n    let mut r = Vec::new();\n    let mut s = Tokenizer::new(s).validated_values();\n    loop {\n        match s.next() {\n            Some(Ok(x)) => r.push(x.kind),\n            None => break,\n            Some(Err(e)) => panic!(\"Parse error at {}: {}\", e.span.start, e.message),\n        }\n    }\n    r\n}\n\nfn tok_err(s: &str) -> String {\n    let mut s = Tokenizer::new(s).validated_values();\n    loop {\n        match s.next() {\n            Some(Ok(_)) => {}\n            None => break,\n            Some(Err(e)) => return e.message.to_string(),\n        }\n    }\n    panic!(\"No error, where error expected\");\n}\n\nfn keyword(kw: &'static str) -> Kind {\n    Keyword(edgeql_parser::keywords::Keyword(kw))\n}\n\n#[test]\nfn whitespace_and_comments() {\n    assert_eq!(tok_str(\"# hello { world }\"), &[] as &[&str]);\n    assert_eq!(tok_str(\"# x\\n  \"), &[] as &[&str]);\n    assert_eq!(tok_str(\"  # x\"), &[] as &[&str]);\n    assert_eq!(\n        tok_err(\"  # xxx \\u{202A} yyy\"),\n        \"unexpected character '\\\\u{202a}'\"\n    );\n}\n\n#[test]\nfn idents() {\n    assert_eq!(tok_str(\"a bc d127\"), [\"a\", \"bc\", \"d127\"]);\n    assert_eq!(tok_typ(\"a bc d127\"), [Ident, Ident, Ident]);\n    assert_eq!(\n        tok_str(\"тест тест_abc abc_тест\"),\n        [\"тест\", \"тест_abc\", \"abc_тест\"]\n    );\n    assert_eq!(tok_typ(\"тест тест_abc abc_тест\"), [Ident, Ident, Ident]);\n    assert_eq!(\n        tok_err(\" + __test__\"),\n        \"identifiers surrounded by double underscores are forbidden\"\n    );\n    assert_eq!(tok_str(\"_1024\"), [\"_1024\"]);\n    assert_eq!(tok_typ(\"_1024\"), [Ident]);\n}\n\n#[test]\nfn keywords() {\n    assert_eq!(tok_str(\"SELECT a\"), [\"SELECT\", \"a\"]);\n    assert_eq!(tok_typ(\"SELECT a\"), [keyword(\"select\"), Ident]);\n    assert_eq!(tok_str(\"with Select\"), [\"with\", \"Select\"]);\n    assert_eq!(tok_typ(\"with Select\"), [keyword(\"with\"), keyword(\"select\")]);\n}\n\n#[test]\nfn colon_tokens() {\n    assert_eq!(tok_str(\"a :=b\"), [\"a\", \":=\", \"b\"]);\n    assert_eq!(tok_typ(\"a :=b\"), [Ident, Assign, Ident]);\n    assert_eq!(tok_str(\"a : = b\"), [\"a\", \":\", \"=\", \"b\"]);\n    assert_eq!(tok_typ(\"a : = b\"), [Ident, Colon, Eq, Ident]);\n    assert_eq!(tok_str(\"a ::= b\"), [\"a\", \"::\", \"=\", \"b\"]);\n    assert_eq!(tok_typ(\"a ::= b\"), [Ident, Namespace, Eq, Ident]);\n}\n\n#[test]\nfn dash_tokens() {\n    assert_eq!(tok_str(\"a-b -> c\"), [\"a\", \"-\", \"b\", \"->\", \"c\"]);\n    assert_eq!(tok_typ(\"a-b -> c\"), [Ident, Sub, Ident, Arrow, Ident]);\n    assert_eq!(tok_str(\"a - > b\"), [\"a\", \"-\", \">\", \"b\"]);\n    assert_eq!(tok_typ(\"a - > b\"), [Ident, Sub, Greater, Ident]);\n    assert_eq!(tok_str(\"a --> b\"), [\"a\", \"-\", \"->\", \"b\"]);\n    assert_eq!(tok_typ(\"a --> b\"), [Ident, Sub, Arrow, Ident]);\n}\n\n#[test]\nfn greater_tokens() {\n    assert_eq!(tok_str(\"a >= c\"), [\"a\", \">=\", \"c\"]);\n    assert_eq!(tok_typ(\"a >= c\"), [Ident, GreaterEq, Ident]);\n    assert_eq!(tok_str(\"a > = b\"), [\"a\", \">\", \"=\", \"b\"]);\n    assert_eq!(tok_typ(\"a > = b\"), [Ident, Greater, Eq, Ident]);\n    assert_eq!(tok_str(\"a>b\"), [\"a\", \">\", \"b\"]);\n    assert_eq!(tok_typ(\"a>b\"), [Ident, Greater, Ident]);\n}\n\n#[test]\nfn less_tokens() {\n    assert_eq!(tok_str(\"a <= c\"), [\"a\", \"<=\", \"c\"]);\n    assert_eq!(tok_typ(\"a <= c\"), [Ident, LessEq, Ident]);\n    assert_eq!(tok_str(\"a < = b\"), [\"a\", \"<\", \"=\", \"b\"]);\n    assert_eq!(tok_typ(\"a < = b\"), [Ident, Less, Eq, Ident]);\n    assert_eq!(tok_str(\"a<b\"), [\"a\", \"<\", \"b\"]);\n    assert_eq!(tok_typ(\"a<b\"), [Ident, Less, Ident]);\n}\n\n#[test]\nfn plus_tokens() {\n    assert_eq!(tok_str(\"a+b += c\"), [\"a\", \"+\", \"b\", \"+=\", \"c\"]);\n    assert_eq!(tok_typ(\"a+b += c\"), [Ident, Add, Ident, AddAssign, Ident]);\n    assert_eq!(tok_str(\"a + = b\"), [\"a\", \"+\", \"=\", \"b\"]);\n    assert_eq!(tok_typ(\"a + = b\"), [Ident, Add, Eq, Ident]);\n    assert_eq!(tok_str(\"a ++= b\"), [\"a\", \"++\", \"=\", \"b\"]);\n    assert_eq!(tok_typ(\"a ++= b\"), [Ident, Concat, Eq, Ident]);\n    assert_eq!(tok_str(\"1+1\"), [\"1\", \"+\", \"1\"]);\n    assert_eq!(tok_typ(\"1+1\"), [IntConst, Add, IntConst]);\n}\n\n#[test]\nfn not_equals_tokens() {\n    assert_eq!(tok_str(\"a != c\"), [\"a\", \"!=\", \"c\"]);\n    assert_eq!(tok_typ(\"a != c\"), [Ident, NotEq, Ident]);\n    assert_eq!(tok_str(\"a!=b\"), [\"a\", \"!=\", \"b\"]);\n    assert_eq!(tok_typ(\"a!=b\"), [Ident, NotEq, Ident]);\n    assert_eq!(\n        tok_err(\"a ! = b\"),\n        \"Bare `!` is not an operator, \\\n         did you mean `!=`?\"\n    );\n}\n\n#[test]\nfn question_tokens() {\n    assert_eq!(tok_str(\"a??b ?= c\"), [\"a\", \"??\", \"b\", \"?=\", \"c\"]);\n    assert_eq!(\n        tok_typ(\"a??b ?= c\"),\n        [Ident, Coalesce, Ident, NotDistinctFrom, Ident]\n    );\n    assert_eq!(tok_str(\"a ?!= b\"), [\"a\", \"?!=\", \"b\"]);\n    assert_eq!(tok_typ(\"a ?!= b\"), [Ident, DistinctFrom, Ident]);\n    assert_eq!(\n        tok_err(\"a ? b\"),\n        \"Bare `?` is not an operator, \\\n         did you mean `?=` or `??` ?\"\n    );\n\n    assert_eq!(\n        tok_err(\"something ?!\"),\n        \"`?!` is not an operator, \\\n         did you mean `?!=` ?\"\n    );\n}\n\n#[test]\nfn dot_tokens() {\n    assert_eq!(tok_str(\"a.b .> c\"), [\"a\", \".\", \"b\", \".\", \">\", \"c\"]);\n    assert_eq!(\n        tok_typ(\"a.b .> c\"),\n        [Ident, Dot, Ident, Dot, Greater, Ident]\n    );\n    assert_eq!(tok_str(\"a . > b\"), [\"a\", \".\", \">\", \"b\"]);\n    assert_eq!(tok_typ(\"a . > b\"), [Ident, Dot, Greater, Ident]);\n    assert_eq!(tok_str(\"a .>> b\"), [\"a\", \".\", \">\", \">\", \"b\"]);\n    assert_eq!(tok_typ(\"a .>> b\"), [Ident, Dot, Greater, Greater, Ident]);\n    assert_eq!(tok_str(\"a ..> b\"), [\"a\", \".\", \".\", \">\", \"b\"]);\n    assert_eq!(tok_typ(\"a ..> b\"), [Ident, Dot, Dot, Greater, Ident]);\n\n    assert_eq!(tok_str(\"a.b .< c\"), [\"a\", \".\", \"b\", \".<\", \"c\"]);\n    assert_eq!(\n        tok_typ(\"a.b .< c\"),\n        [Ident, Dot, Ident, BackwardLink, Ident]\n    );\n    assert_eq!(tok_str(\"a . < b\"), [\"a\", \".\", \"<\", \"b\"]);\n    assert_eq!(tok_typ(\"a . < b\"), [Ident, Dot, Less, Ident]);\n    assert_eq!(tok_str(\"a .<< b\"), [\"a\", \".<\", \"<\", \"b\"]);\n    assert_eq!(tok_typ(\"a .<< b\"), [Ident, BackwardLink, Less, Ident]);\n    assert_eq!(tok_str(\"a ..< b\"), [\"a\", \".\", \".<\", \"b\"]);\n    assert_eq!(tok_typ(\"a ..< b\"), [Ident, Dot, BackwardLink, Ident]);\n}\n\n#[test]\nfn tuple_dot_vs_float() {\n    assert_eq!(tok_str(\"tuple.1.<\"), [\"tuple\", \".\", \"1\", \".<\"]);\n    assert_eq!(tok_typ(\"tuple.1.<\"), [Ident, Dot, IntConst, BackwardLink]);\n    assert_eq!(tok_str(\"tuple.1.e123\"), [\"tuple\", \".\", \"1\", \".\", \"e123\"]);\n    assert_eq!(tok_typ(\"tuple.1.e123\"), [Ident, Dot, IntConst, Dot, Ident]);\n}\n\n#[test]\nfn div_tokens() {\n    assert_eq!(tok_str(\"a // c\"), [\"a\", \"//\", \"c\"]);\n    assert_eq!(tok_typ(\"a // c\"), [Ident, FloorDiv, Ident]);\n    assert_eq!(tok_str(\"a / / b\"), [\"a\", \"/\", \"/\", \"b\"]);\n    assert_eq!(tok_typ(\"a / / b\"), [Ident, Div, Div, Ident]);\n    assert_eq!(tok_str(\"a/b\"), [\"a\", \"/\", \"b\"]);\n    assert_eq!(tok_typ(\"a/b\"), [Ident, Div, Ident]);\n}\n\n#[test]\nfn single_char_tokens() {\n    assert_eq!(tok_str(\".;:+-*\"), [\".\", \";\", \":\", \"+\", \"-\", \"*\"]);\n    assert_eq!(tok_typ(\".;:+-*\"), [Dot, Semicolon, Colon, Add, Sub, Mul]);\n    assert_eq!(tok_str(\"/%^<>\"), [\"/\", \"%\", \"^\", \"<\", \">\"]);\n    assert_eq!(tok_typ(\"/%^<>\"), [Div, Modulo, Pow, Less, Greater]);\n    assert_eq!(tok_str(\"=&|@\"), [\"=\", \"&\", \"|\", \"@\"]);\n    assert_eq!(tok_typ(\"=&|@\"), [Eq, Ampersand, Pipe, At]);\n\n    assert_eq!(tok_str(\". ; : + - *\"), [\".\", \";\", \":\", \"+\", \"-\", \"*\"]);\n    assert_eq!(\n        tok_typ(\". ; : + - *\"),\n        [Dot, Semicolon, Colon, Add, Sub, Mul]\n    );\n    assert_eq!(tok_str(\"/ % ^ < >\"), [\"/\", \"%\", \"^\", \"<\", \">\"]);\n    assert_eq!(tok_typ(\"/ % ^ < >\"), [Div, Modulo, Pow, Less, Greater]);\n    assert_eq!(tok_str(\"= & | @\"), [\"=\", \"&\", \"|\", \"@\"]);\n    assert_eq!(tok_typ(\"= & | @\"), [Eq, Ampersand, Pipe, At]);\n}\n\n#[test]\nfn splats() {\n    assert_eq!(tok_str(\"*\"), [\"*\"]);\n    assert_eq!(tok_typ(\"*\"), [Mul]);\n    assert_eq!(tok_str(\"**\"), [\"**\"]);\n    assert_eq!(tok_typ(\"**\"), [DoubleSplat]);\n    assert_eq!(tok_str(\"* *\"), [\"*\", \"*\"]);\n    assert_eq!(tok_typ(\"* *\"), [Mul, Mul]);\n    assert_eq!(tok_str(\"User.*,\"), [\"User\", \".\", \"*\", \",\"]);\n    assert_eq!(tok_typ(\"User.*,\"), [Ident, Dot, Mul, Comma]);\n    assert_eq!(tok_str(\"User.**,\"), [\"User\", \".\", \"**\", \",\"]);\n    assert_eq!(tok_typ(\"User.**,\"), [Ident, Dot, DoubleSplat, Comma]);\n    assert_eq!(tok_str(\"User {*}\"), [\"User\", \"{\", \"*\", \"}\"]);\n    assert_eq!(tok_typ(\"User {*}\"), [Ident, OpenBrace, Mul, CloseBrace]);\n    assert_eq!(tok_str(\"User {**}\"), [\"User\", \"{\", \"**\", \"}\"]);\n    assert_eq!(\n        tok_typ(\"User {**}\"),\n        [Ident, OpenBrace, DoubleSplat, CloseBrace]\n    );\n}\n\n#[test]\nfn integer() {\n    assert_eq!(tok_str(\"0\"), [\"0\"]);\n    assert_eq!(tok_typ(\"0\"), [IntConst]);\n    assert_eq!(tok_str(\"*0\"), [\"*\", \"0\"]);\n    assert_eq!(tok_typ(\"*0\"), [Mul, IntConst]);\n    assert_eq!(tok_str(\"123\"), [\"123\"]);\n    assert_eq!(tok_typ(\"123\"), [IntConst]);\n    assert_eq!(tok_str(\"123_\"), [\"123_\"]);\n    assert_eq!(tok_typ(\"123_\"), [IntConst]);\n    assert_eq!(tok_str(\"123_456\"), [\"123_456\"]);\n    assert_eq!(tok_typ(\"123_456\"), [IntConst]);\n\n    assert_eq!(tok_str(\"0 \"), [\"0\"]);\n    assert_eq!(tok_typ(\"0 \"), [IntConst]);\n    assert_eq!(tok_str(\"123 \"), [\"123\"]);\n    assert_eq!(tok_typ(\"123 \"), [IntConst]);\n    assert_eq!(tok_str(\"123_ \"), [\"123_\"]);\n    assert_eq!(tok_typ(\"123_ \"), [IntConst]);\n    assert_eq!(tok_str(\"123_456 \"), [\"123_456\"]);\n    assert_eq!(tok_typ(\"123_456 \"), [IntConst]);\n}\n\n#[test]\nfn bigint() {\n    assert_eq!(tok_str(\"0n\"), [\"0n\"]);\n    assert_eq!(tok_typ(\"0n\"), [BigIntConst]);\n    assert_eq!(tok_str(\"*0n\"), [\"*\", \"0n\"]);\n    assert_eq!(tok_typ(\"*0n\"), [Mul, BigIntConst]);\n    assert_eq!(tok_str(\"123n\"), [\"123n\"]);\n    assert_eq!(tok_typ(\"123n\"), [BigIntConst]);\n    assert_eq!(tok_str(\"123e3n\"), [\"123e3n\"]);\n    assert_eq!(tok_typ(\"123e3n\"), [BigIntConst]);\n    assert_eq!(tok_str(\"123e+99n\"), [\"123e+99n\"]);\n    assert_eq!(tok_typ(\"123e+99n\"), [BigIntConst]);\n    assert_eq!(tok_str(\"123_n\"), [\"123_n\"]);\n    assert_eq!(tok_typ(\"123_n\"), [BigIntConst]);\n    assert_eq!(tok_str(\"123_456n\"), [\"123_456n\"]);\n    assert_eq!(tok_typ(\"123_456n\"), [BigIntConst]);\n\n    assert_eq!(tok_str(\"0n \"), [\"0n\"]);\n    assert_eq!(tok_typ(\"0n \"), [BigIntConst]);\n    assert_eq!(tok_str(\"123n \"), [\"123n\"]);\n    assert_eq!(tok_typ(\"123n \"), [BigIntConst]);\n    assert_eq!(tok_str(\"123e3n \"), [\"123e3n\"]);\n    assert_eq!(tok_typ(\"123e3n \"), [BigIntConst]);\n    assert_eq!(tok_str(\"123e+99n \"), [\"123e+99n\"]);\n    assert_eq!(tok_typ(\"123e+99n \"), [BigIntConst]);\n    assert_eq!(tok_str(\"123_n \"), [\"123_n\"]);\n    assert_eq!(tok_typ(\"123_n \"), [BigIntConst]);\n    assert_eq!(tok_str(\"123_456n \"), [\"123_456n\"]);\n    assert_eq!(tok_typ(\"123_456n \"), [BigIntConst]);\n}\n\n#[test]\nfn float() {\n    assert_eq!(tok_str(\"     0.0\"), [\"0.0\"]);\n    assert_eq!(tok_typ(\"     0.0\"), [FloatConst]);\n    assert_eq!(tok_str(\"123.999\"), [\"123.999\"]);\n    assert_eq!(tok_typ(\"123.999\"), [FloatConst]);\n    assert_eq!(tok_str(\"123.999e3\"), [\"123.999e3\"]);\n    assert_eq!(tok_typ(\"123.999e3\"), [FloatConst]);\n    assert_eq!(tok_str(\"123.999e+99\"), [\"123.999e+99\"]);\n    assert_eq!(tok_typ(\"123.999e+99\"), [FloatConst]);\n    assert_eq!(tok_str(\"2345.567e-7\"), [\"2345.567e-7\"]);\n    assert_eq!(tok_typ(\"2345.567e-7\"), [FloatConst]);\n    assert_eq!(tok_str(\"123e3\"), [\"123e3\"]);\n    assert_eq!(tok_typ(\"123e3\"), [FloatConst]);\n    assert_eq!(tok_str(\"123e+99\"), [\"123e+99\"]);\n    assert_eq!(tok_typ(\"123e+99\"), [FloatConst]);\n    assert_eq!(tok_str(\"123e+99_\"), [\"123e+99_\"]);\n    assert_eq!(tok_typ(\"123e+99_\"), [FloatConst]);\n    assert_eq!(tok_str(\"123e+9_9\"), [\"123e+9_9\"]);\n    assert_eq!(tok_typ(\"123e+9_9\"), [FloatConst]);\n    assert_eq!(tok_str(\"2345e-7\"), [\"2345e-7\"]);\n    assert_eq!(tok_typ(\"2345e-7\"), [FloatConst]);\n    assert_eq!(tok_str(\"2_345e-7\"), [\"2_345e-7\"]);\n    assert_eq!(tok_typ(\"2_345e-7\"), [FloatConst]);\n    assert_eq!(tok_str(\"1_023.9_099\"), [\"1_023.9_099\"]);\n    assert_eq!(tok_typ(\"1_023.9_099\"), [FloatConst]);\n    assert_eq!(tok_str(\"1_023_.9_099_\"), [\"1_023_.9_099_\"]);\n    assert_eq!(tok_typ(\"1_023_.9_099_\"), [FloatConst]);\n\n    assert_eq!(tok_str(\"     0.0 \"), [\"0.0\"]);\n    assert_eq!(tok_typ(\"     0.0 \"), [FloatConst]);\n    assert_eq!(tok_str(\"123.999 \"), [\"123.999\"]);\n    assert_eq!(tok_typ(\"123.999 \"), [FloatConst]);\n    assert_eq!(tok_str(\"123.999e3 \"), [\"123.999e3\"]);\n    assert_eq!(tok_typ(\"123.999e3 \"), [FloatConst]);\n    assert_eq!(tok_str(\"123.999e+99 \"), [\"123.999e+99\"]);\n    assert_eq!(tok_typ(\"123.999e+99 \"), [FloatConst]);\n    assert_eq!(tok_str(\"2345.567e-7 \"), [\"2345.567e-7\"]);\n    assert_eq!(tok_typ(\"2345.567e-7 \"), [FloatConst]);\n    assert_eq!(tok_str(\"123e3 \"), [\"123e3\"]);\n    assert_eq!(tok_typ(\"123e3 \"), [FloatConst]);\n    assert_eq!(tok_str(\"123e+99 \"), [\"123e+99\"]);\n    assert_eq!(tok_typ(\"123e+99 \"), [FloatConst]);\n    assert_eq!(tok_str(\"123e+99_ \"), [\"123e+99_\"]);\n    assert_eq!(tok_typ(\"123e+99_ \"), [FloatConst]);\n    assert_eq!(tok_str(\"2345e-7 \"), [\"2345e-7\"]);\n    assert_eq!(tok_typ(\"2345e-7 \"), [FloatConst]);\n    assert_eq!(tok_str(\"1_023_.9_099_ \"), [\"1_023_.9_099_\"]);\n    assert_eq!(tok_typ(\"1_023_.9_099_ \"), [FloatConst]);\n\n    assert_eq!(\n        tok_err(\"01.2\"),\n        \"unexpected leading zeros are not allowed in numbers\"\n    );\n}\n\n#[test]\nfn decimal() {\n    assert_eq!(tok_str(\"     0.0n\"), [\"0.0n\"]);\n    assert_eq!(tok_typ(\"     0.0n\"), [DecimalConst]);\n    assert_eq!(tok_str(\"123.999n\"), [\"123.999n\"]);\n    assert_eq!(tok_typ(\"123.999n\"), [DecimalConst]);\n    assert_eq!(tok_str(\"123.999e3n\"), [\"123.999e3n\"]);\n    assert_eq!(tok_typ(\"123.999e3n\"), [DecimalConst]);\n    assert_eq!(tok_str(\"123.999e+99n\"), [\"123.999e+99n\"]);\n    assert_eq!(tok_typ(\"123.999e+99n\"), [DecimalConst]);\n    assert_eq!(tok_str(\"2345.567e-7n\"), [\"2345.567e-7n\"]);\n    assert_eq!(tok_typ(\"2345.567e-7n\"), [DecimalConst]);\n    assert_eq!(tok_str(\"2345e-7n\"), [\"2345e-7n\"]);\n    assert_eq!(tok_typ(\"2345e-7n\"), [DecimalConst]);\n    assert_eq!(tok_str(\"2_345e-7n\"), [\"2_345e-7n\"]);\n    assert_eq!(tok_typ(\"2_345e-7n\"), [DecimalConst]);\n    assert_eq!(tok_str(\"1_023.9_099n\"), [\"1_023.9_099n\"]);\n    assert_eq!(tok_typ(\"1_023.9_099n\"), [DecimalConst]);\n    assert_eq!(tok_str(\"1_023_.9_099_n\"), [\"1_023_.9_099_n\"]);\n    assert_eq!(tok_typ(\"1_023_.9_099_n\"), [DecimalConst]);\n    assert_eq!(tok_str(\"2_345e-7n\"), [\"2_345e-7n\"]);\n    assert_eq!(tok_typ(\"2_345e-7n\"), [DecimalConst]);\n    assert_eq!(tok_str(\"2_345e-7_7n\"), [\"2_345e-7_7n\"]);\n    assert_eq!(tok_typ(\"2_345e-7_7n\"), [DecimalConst]);\n\n    assert_eq!(tok_str(\"     0.0n \"), [\"0.0n\"]);\n    assert_eq!(tok_typ(\"     0.0n \"), [DecimalConst]);\n    assert_eq!(tok_str(\"123.999n \"), [\"123.999n\"]);\n    assert_eq!(tok_typ(\"123.999n \"), [DecimalConst]);\n    assert_eq!(tok_str(\"123.999e3n \"), [\"123.999e3n\"]);\n    assert_eq!(tok_typ(\"123.999e3n \"), [DecimalConst]);\n    assert_eq!(tok_str(\"123.999e+99n \"), [\"123.999e+99n\"]);\n    assert_eq!(tok_typ(\"123.999e+99n \"), [DecimalConst]);\n    assert_eq!(tok_str(\"2345.567e-7n \"), [\"2345.567e-7n\"]);\n    assert_eq!(tok_typ(\"2345.567e-7n \"), [DecimalConst]);\n    assert_eq!(tok_str(\"2345e-7n \"), [\"2345e-7n\"]);\n    assert_eq!(tok_typ(\"2345e-7n \"), [DecimalConst]);\n\n    assert_eq!(\n        tok_err(\"01.0n\"),\n        \"unexpected leading zeros are not allowed in numbers\"\n    );\n}\n\n#[test]\nfn numbers_from_py() {\n    assert_eq!(tok_str(\"SELECT 3.5432;\"), [\"SELECT\", \"3.5432\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT 3.5432;\"),\n        [keyword(\"select\"), FloatConst, Semicolon]\n    );\n    assert_eq!(tok_str(\"SELECT +3.5432;\"), [\"SELECT\", \"+\", \"3.5432\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT +3.5432;\"),\n        [keyword(\"select\"), Add, FloatConst, Semicolon]\n    );\n    assert_eq!(tok_str(\"SELECT -3.5432;\"), [\"SELECT\", \"-\", \"3.5432\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT -3.5432;\"),\n        [keyword(\"select\"), Sub, FloatConst, Semicolon]\n    );\n    assert_eq!(tok_str(\"SELECT 354.32;\"), [\"SELECT\", \"354.32\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT 354.32;\"),\n        [keyword(\"select\"), FloatConst, Semicolon]\n    );\n    assert_eq!(\n        tok_str(\"SELECT 35400000000000.32;\"),\n        [\"SELECT\", \"35400000000000.32\", \";\"]\n    );\n    assert_eq!(\n        tok_typ(\"SELECT 35400000000000.32;\"),\n        [keyword(\"select\"), FloatConst, Semicolon]\n    );\n    assert_eq!(\n        tok_str(\"SELECT 35400000000000000000.32;\"),\n        [\"SELECT\", \"35400000000000000000.32\", \";\"]\n    );\n    assert_eq!(\n        tok_typ(\"SELECT 35400000000000000000.32;\"),\n        [keyword(\"select\"), FloatConst, Semicolon]\n    );\n    assert_eq!(tok_str(\"SELECT 3.5432e20;\"), [\"SELECT\", \"3.5432e20\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT 3.5432e20;\"),\n        [keyword(\"select\"), FloatConst, Semicolon]\n    );\n    assert_eq!(tok_str(\"SELECT 3.5432e+20;\"), [\"SELECT\", \"3.5432e+20\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT 3.5432e+20;\"),\n        [keyword(\"select\"), FloatConst, Semicolon]\n    );\n    assert_eq!(tok_str(\"SELECT 3.5432e-20;\"), [\"SELECT\", \"3.5432e-20\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT 3.5432e-20;\"),\n        [keyword(\"select\"), FloatConst, Semicolon]\n    );\n    assert_eq!(tok_str(\"SELECT 354.32e-20;\"), [\"SELECT\", \"354.32e-20\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT 354.32e-20;\"),\n        [keyword(\"select\"), FloatConst, Semicolon]\n    );\n    assert_eq!(tok_str(\"SELECT -0n;\"), [\"SELECT\", \"-\", \"0n\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT -0n;\"),\n        [keyword(\"select\"), Sub, BigIntConst, Semicolon]\n    );\n    assert_eq!(tok_str(\"SELECT 0n;\"), [\"SELECT\", \"0n\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT 0n;\"),\n        [keyword(\"select\"), BigIntConst, Semicolon]\n    );\n    assert_eq!(tok_str(\"SELECT 1n;\"), [\"SELECT\", \"1n\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT 1n;\"),\n        [keyword(\"select\"), BigIntConst, Semicolon]\n    );\n    assert_eq!(tok_str(\"SELECT -1n;\"), [\"SELECT\", \"-\", \"1n\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT -1n;\"),\n        [keyword(\"select\"), Sub, BigIntConst, Semicolon]\n    );\n    assert_eq!(tok_str(\"SELECT 100000n;\"), [\"SELECT\", \"100000n\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT 100000n;\"),\n        [keyword(\"select\"), BigIntConst, Semicolon]\n    );\n    assert_eq!(tok_str(\"SELECT -100000n;\"), [\"SELECT\", \"-\", \"100000n\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT -100000n;\"),\n        [keyword(\"select\"), Sub, BigIntConst, Semicolon]\n    );\n    assert_eq!(tok_str(\"SELECT -354.32n;\"), [\"SELECT\", \"-\", \"354.32n\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT -354.32n;\"),\n        [keyword(\"select\"), Sub, DecimalConst, Semicolon]\n    );\n    assert_eq!(\n        tok_str(\"SELECT 35400000000000.32n;\"),\n        [\"SELECT\", \"35400000000000.32n\", \";\"]\n    );\n    assert_eq!(\n        tok_typ(\"SELECT 35400000000000.32n;\"),\n        [keyword(\"select\"), DecimalConst, Semicolon]\n    );\n    assert_eq!(\n        tok_str(\"SELECT -35400000000000000000.32n;\"),\n        [\"SELECT\", \"-\", \"35400000000000000000.32n\", \";\"]\n    );\n    assert_eq!(\n        tok_typ(\"SELECT -35400000000000000000.32n;\"),\n        [keyword(\"select\"), Sub, DecimalConst, Semicolon]\n    );\n    assert_eq!(tok_str(\"SELECT 3.5432e20n;\"), [\"SELECT\", \"3.5432e20n\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT 3.5432e20n;\"),\n        [keyword(\"select\"), DecimalConst, Semicolon]\n    );\n    assert_eq!(\n        tok_str(\"SELECT -3.5432e+20n;\"),\n        [\"SELECT\", \"-\", \"3.5432e+20n\", \";\"]\n    );\n    assert_eq!(\n        tok_typ(\"SELECT -3.5432e+20n;\"),\n        [keyword(\"select\"), Sub, DecimalConst, Semicolon]\n    );\n    assert_eq!(\n        tok_str(\"SELECT 3.5432e-20n;\"),\n        [\"SELECT\", \"3.5432e-20n\", \";\"]\n    );\n    assert_eq!(\n        tok_typ(\"SELECT 3.5432e-20n;\"),\n        [keyword(\"select\"), DecimalConst, Semicolon]\n    );\n    assert_eq!(\n        tok_str(\"SELECT 354.32e-20n;\"),\n        [\"SELECT\", \"354.32e-20n\", \";\"]\n    );\n    assert_eq!(\n        tok_typ(\"SELECT 354.32e-20n;\"),\n        [keyword(\"select\"), DecimalConst, Semicolon]\n    );\n}\n\n#[test]\nfn num_errors() {\n    assert_eq!(\n        tok_err(\"0. \"),\n        \"expected digit after dot, found end of decimal\"\n    );\n    assert_eq!(\n        tok_err(\"1.<\"),\n        \"expected digit after dot, found end of decimal\"\n    );\n    assert_eq!(tok_err(\"0.n\"), \"expected digit after dot, found suffix\");\n    assert_eq!(tok_err(\"0.e1\"), \"expected digit after dot, found exponent\");\n    assert_eq!(tok_err(\"0.e1n\"), \"expected digit after dot, found exponent\");\n    assert_eq!(\n        tok_err(\"0.\"),\n        \"expected digit after dot, found end of decimal\"\n    );\n    assert_eq!(tok_err(\"1.0.x\"), \"unexpected extra decimal dot in number\");\n    assert_eq!(tok_err(\"1.0e1.\"), \"unexpected extra decimal dot in number\");\n    assert_eq!(\n        tok_err(\"1.0e.\"),\n        \"unexpected optional `+` or `-` \\\n        followed by digits must follow `e` in float const\"\n    );\n    assert_eq!(\n        tok_err(\"1.0e\"),\n        \"unexpected optional `+` or `-` \\\n        followed by digits must follow `e` in float const\"\n    );\n    assert_eq!(\n        tok_err(\"1.0ex\"),\n        \"unexpected optional `+` or `-` \\\n        followed by digits must follow `e` in float const\"\n    );\n    assert_eq!(\n        tok_err(\"1.0en\"),\n        \"unexpected optional `+` or `-` \\\n        followed by digits must follow `e` in float const\"\n    );\n    assert_eq!(\n        tok_err(\"1.0e \"),\n        \"unexpected optional `+` or `-` \\\n        followed by digits must follow `e` in float const\"\n    );\n    assert_eq!(\n        tok_err(\"1.0e_\"),\n        \"unexpected optional `+` or `-` \\\n        followed by digits must follow `e` in float const\"\n    );\n    assert_eq!(\n        tok_err(\"1.0e_ \"),\n        \"unexpected optional `+` or `-` \\\n        followed by digits must follow `e` in float const\"\n    );\n    assert_eq!(\n        tok_err(\"1.0e_1\"),\n        \"unexpected optional `+` or `-` \\\n        followed by digits must follow `e` in float const\"\n    );\n    assert_eq!(\n        tok_err(\"1.0e+\"),\n        \"unexpected optional `+` or `-` \\\n        followed by digits must follow `e` in float const\"\n    );\n    assert_eq!(\n        tok_err(\"1.0e+ \"),\n        \"unexpected optional `+` or `-` \\\n        followed by digits must follow `e` in float const\"\n    );\n    assert_eq!(\n        tok_err(\"1.0e+x\"),\n        \"unexpected optional `+` or `-` \\\n        followed by digits must follow `e` in float const\"\n    );\n    assert_eq!(\n        tok_err(\"1.0e+n\"),\n        \"unexpected optional `+` or `-` \\\n        followed by digits must follow `e` in float const\"\n    );\n    assert_eq!(\n        tok_err(\"1234numeric\"),\n        \"suffix \\\"numeric\\\" \\\n        is invalid for numbers, perhaps you wanted `1234n` (bigint)?\"\n    );\n    assert_eq!(\n        tok_err(\"1234some_l0ng_trash\"),\n        \"suffix \\\"some_l0n...\\\" \\\n        is invalid for numbers, perhaps you wanted `1234n` (bigint)?\"\n    );\n    assert_eq!(\n        tok_err(\"100O00\"),\n        \"suffix \\\"O00\\\" is invalid for numbers, \\\n        perhaps mixed up letter `O` with zero `0`?\"\n    );\n    assert_eq!(\n        tok_err(\"01\"),\n        \"unexpected leading zeros are not allowed in numbers\"\n    );\n    assert_eq!(\n        tok_err(\"01n\"),\n        \"unexpected leading zeros are not allowed in numbers\"\n    );\n    assert_eq!(\n        tok_err(\"01_n\"),\n        \"unexpected leading zeros are not allowed in numbers\"\n    );\n    assert_eq!(\n        tok_err(\"0_1_n\"),\n        \"unexpected leading zeros are not allowed in numbers\"\n    );\n    assert_eq!(\n        tok_err(\"0_1n\"),\n        \"unexpected leading zeros are not allowed in numbers\"\n    );\n}\n\n#[test]\nfn tuple_paths() {\n    assert_eq!(\n        tok_str(\"tup.1.2.3.4.5\"),\n        [\"tup\", \".\", \"1\", \".\", \"2\", \".\", \"3\", \".\", \"4\", \".\", \"5\"]\n    );\n    assert_eq!(\n        tok_typ(\"tup.1.2.3.4.5\"),\n        [Ident, Dot, IntConst, Dot, IntConst, Dot, IntConst, Dot, IntConst, Dot, IntConst]\n    );\n    assert_eq!(\n        tok_err(\"tup.1.2.>3.4.>5\"),\n        \"unexpected extra decimal dot in number\"\n    );\n    assert_eq!(\n        tok_str(\"$0.1.2.3.4.5\"),\n        [\"$0\", \".\", \"1\", \".\", \"2\", \".\", \"3\", \".\", \"4\", \".\", \"5\"]\n    );\n    assert_eq!(\n        tok_typ(\"$0.1.2.3.4.5\"),\n        [Parameter, Dot, IntConst, Dot, IntConst, Dot, IntConst, Dot, IntConst, Dot, IntConst]\n    );\n    assert_eq!(\n        tok_err(\"tup.1n\"),\n        \"unexpected char \\'n\\', only integers \\\n        are allowed after dot (for tuple access)\"\n    );\n\n    assert_eq!(\n        tok_err(\"tup.01\"),\n        \"leading zeros are not allowed in numbers\"\n    );\n}\n\n#[test]\nfn strings() {\n    assert_eq!(tok_str(r#\" \"\"  \"#), [r#\"\"\"\"#]);\n    assert_eq!(tok_typ(r#\" \"\"  \"#), [Str]);\n    assert_eq!(tok_str(r#\" ''  \"#), [r#\"''\"#]);\n    assert_eq!(tok_typ(r#\" ''  \"#), [Str]);\n    assert_eq!(tok_str(r#\" r\"\"  \"#), [r#\"r\"\"\"#]);\n    assert_eq!(tok_typ(r#\" r\"\"  \"#), [Str]);\n    assert_eq!(tok_str(r#\" r''  \"#), [r#\"r''\"#]);\n    assert_eq!(tok_typ(r#\" r''  \"#), [Str]);\n    assert_eq!(tok_str(r#\" b\"\"  \"#), [r#\"b\"\"\"#]);\n    assert_eq!(tok_typ(r#\" b\"\"  \"#), [BinStr]);\n    assert_eq!(tok_str(r#\" b''  \"#), [r#\"b''\"#]);\n    assert_eq!(tok_typ(r#\" b''  \"#), [BinStr]);\n    assert_eq!(tok_str(r#\" br\"\"  \"#), [r#\"br\"\"\"#]);\n    assert_eq!(tok_typ(r#\" br\"\"  \"#), [BinStr]);\n    assert_eq!(tok_str(r#\" br''  \"#), [r#\"br''\"#]);\n    assert_eq!(tok_typ(r#\" br''  \"#), [BinStr]);\n    assert_eq!(tok_err(r#\" ``  \"#), \"backtick quotes cannot be empty\");\n\n    assert_eq!(tok_str(r#\" \"hello\"  \"#), [r#\"\"hello\"\"#]);\n    assert_eq!(tok_typ(r#\" \"hello\"  \"#), [Str]);\n    assert_eq!(tok_str(r#\" 'hello'  \"#), [r#\"'hello'\"#]);\n    assert_eq!(tok_typ(r#\" 'hello'  \"#), [Str]);\n    assert_eq!(tok_str(r#\" r\"hello\"  \"#), [r#\"r\"hello\"\"#]);\n    assert_eq!(tok_typ(r#\" r\"hello\"  \"#), [Str]);\n    assert_eq!(tok_str(r#\" r'hello'  \"#), [r#\"r'hello'\"#]);\n    assert_eq!(tok_typ(r#\" r'hello'  \"#), [Str]);\n    assert_eq!(tok_str(r#\" b\"hello\"  \"#), [r#\"b\"hello\"\"#]);\n    assert_eq!(tok_typ(r#\" b\"hello\"  \"#), [BinStr]);\n    assert_eq!(tok_str(r#\" b'hello'  \"#), [r#\"b'hello'\"#]);\n    assert_eq!(tok_typ(r#\" b'hello'  \"#), [BinStr]);\n    assert_eq!(tok_str(r#\" rb\"hello\"  \"#), [r#\"rb\"hello\"\"#]);\n    assert_eq!(tok_typ(r#\" rb\"hello\"  \"#), [BinStr]);\n    assert_eq!(tok_str(r#\" rb'hello'  \"#), [r#\"rb'hello'\"#]);\n    assert_eq!(tok_typ(r#\" rb'hello'  \"#), [BinStr]);\n    assert_eq!(tok_str(r#\" `hello`  \"#), [r#\"`hello`\"#]);\n    assert_eq!(tok_typ(r#\" `hello`  \"#), [Ident]);\n\n    assert_eq!(tok_str(r#\" \"hello\"\"#), [r#\"\"hello\"\"#]);\n    assert_eq!(tok_typ(r#\" \"hello\"\"#), [Str]);\n    assert_eq!(tok_str(r#\" 'hello'\"#), [r#\"'hello'\"#]);\n    assert_eq!(tok_typ(r#\" 'hello'\"#), [Str]);\n    assert_eq!(tok_str(r#\" r\"hello\"\"#), [r#\"r\"hello\"\"#]);\n    assert_eq!(tok_typ(r#\" r\"hello\"\"#), [Str]);\n    assert_eq!(tok_str(r#\" r'hello'\"#), [r#\"r'hello'\"#]);\n    assert_eq!(tok_typ(r#\" r'hello'\"#), [Str]);\n    assert_eq!(tok_str(r#\" b\"hello\"\"#), [r#\"b\"hello\"\"#]);\n    assert_eq!(tok_typ(r#\" b\"hello\"\"#), [BinStr]);\n    assert_eq!(tok_str(r#\" b'hello'\"#), [r#\"b'hello'\"#]);\n    assert_eq!(tok_typ(r#\" b'hello'\"#), [BinStr]);\n    assert_eq!(tok_str(r#\" rb\"hello\"\"#), [r#\"rb\"hello\"\"#]);\n    assert_eq!(tok_typ(r#\" rb\"hello\"\"#), [BinStr]);\n    assert_eq!(tok_str(r#\" rb'hello'\"#), [r#\"rb'hello'\"#]);\n    assert_eq!(tok_typ(r#\" rb'hello'\"#), [BinStr]);\n    assert_eq!(tok_str(r#\" `hello`\"#), [r#\"`hello`\"#]);\n    assert_eq!(tok_typ(r#\" `hello`\"#), [Ident]);\n\n    assert_eq!(tok_str(r#\" \"h\\\"ello\" \"#), [r#\"\"h\\\"ello\"\"#]);\n    assert_eq!(tok_typ(r#\" \"h\\\"ello\" \"#), [Str]);\n    assert_eq!(tok_str(r\" 'h\\'ello' \"), [r\"'h\\'ello'\"]);\n    assert_eq!(tok_typ(r\" 'h\\'ello' \"), [Str]);\n    assert_eq!(tok_str(r#\" r\"hello\\\" \"#), [r#\"r\"hello\\\"\"#]);\n    assert_eq!(tok_typ(r#\" r\"hello\\\" \"#), [Str]);\n    assert_eq!(tok_str(r\" r'hello\\' \"), [r\"r'hello\\'\"]);\n    assert_eq!(tok_typ(r\" r'hello\\' \"), [Str]);\n    assert_eq!(tok_str(r#\" b\"h\\\"ello\" \"#), [r#\"b\"h\\\"ello\"\"#]);\n    assert_eq!(tok_typ(r#\" b\"h\\\"ello\" \"#), [BinStr]);\n    assert_eq!(tok_str(r\" b'h\\'ello' \"), [r\"b'h\\'ello'\"]);\n    assert_eq!(tok_typ(r\" b'h\\'ello' \"), [BinStr]);\n    assert_eq!(tok_str(r#\" rb\"hello\\\" \"#), [r#\"rb\"hello\\\"\"#]);\n    assert_eq!(tok_typ(r#\" rb\"hello\\\" \"#), [BinStr]);\n    assert_eq!(tok_str(r\" rb'hello\\' \"), [r\"rb'hello\\'\"]);\n    assert_eq!(tok_typ(r\" rb'hello\\' \"), [BinStr]);\n    assert_eq!(tok_str(r\" `hello\\` \"), [r\"`hello\\`\"]);\n    assert_eq!(tok_typ(r\" `hello\\` \"), [Ident]);\n    assert_eq!(tok_str(r#\" `hel``lo` \"#), [r#\"`hel``lo`\"#]);\n    assert_eq!(tok_typ(r#\" `hel``lo` \"#), [Ident]);\n\n    assert_eq!(tok_str(r#\" \"h'el`lo\" \"#), [r#\"\"h'el`lo\"\"#]);\n    assert_eq!(tok_typ(r#\" \"h'el`lo\" \"#), [Str]);\n    assert_eq!(tok_str(r#\" 'h\"el`lo' \"#), [r#\"'h\"el`lo'\"#]);\n    assert_eq!(tok_typ(r#\" 'h\"el`lo' \"#), [Str]);\n    assert_eq!(tok_str(r#\" r\"h'el`lo\" \"#), [r#\"r\"h'el`lo\"\"#]);\n    assert_eq!(tok_typ(r#\" r\"h'el`lo\" \"#), [Str]);\n    assert_eq!(tok_str(r#\" r'h\"el`lo' \"#), [r#\"r'h\"el`lo'\"#]);\n    assert_eq!(tok_typ(r#\" r'h\"el`lo' \"#), [Str]);\n    assert_eq!(tok_str(r#\" b\"h'el`lo\" \"#), [r#\"b\"h'el`lo\"\"#]);\n    assert_eq!(tok_typ(r#\" b\"h'el`lo\" \"#), [BinStr]);\n    assert_eq!(tok_str(r#\" b'h\"el`lo' \"#), [r#\"b'h\"el`lo'\"#]);\n    assert_eq!(tok_typ(r#\" b'h\"el`lo' \"#), [BinStr]);\n    assert_eq!(tok_str(r#\" rb\"h'el`lo\" \"#), [r#\"rb\"h'el`lo\"\"#]);\n    assert_eq!(tok_typ(r#\" rb\"h'el`lo\" \"#), [BinStr]);\n    assert_eq!(tok_str(r#\" rb'h\"el`lo' \"#), [r#\"rb'h\"el`lo'\"#]);\n    assert_eq!(tok_typ(r#\" rb'h\"el`lo' \"#), [BinStr]);\n    assert_eq!(tok_str(r#\" `h'el\"lo` \"#), [r#\"`h'el\"lo`\"#]);\n    assert_eq!(tok_typ(r#\" `h'el\"lo\\` \"#), [Ident]);\n\n    assert_eq!(tok_str(\" \\\"hel\\nlo\\\" \"), [\"\\\"hel\\nlo\\\"\"]);\n    assert_eq!(tok_typ(\" \\\"hel\\nlo\\\" \"), [Str]);\n    assert_eq!(tok_str(\" 'hel\\nlo' \"), [\"'hel\\nlo'\"]);\n    assert_eq!(tok_typ(\" 'hel\\nlo' \"), [Str]);\n    assert_eq!(tok_str(\" r\\\"hel\\nlo\\\" \"), [\"r\\\"hel\\nlo\\\"\"]);\n    assert_eq!(tok_typ(\" r\\\"hel\\nlo\\\" \"), [Str]);\n    assert_eq!(tok_str(\" r'hel\\nlo' \"), [\"r'hel\\nlo'\"]);\n    assert_eq!(tok_typ(\" r'hel\\nlo' \"), [Str]);\n    assert_eq!(tok_str(\" b\\\"hel\\nlo\\\" \"), [\"b\\\"hel\\nlo\\\"\"]);\n    assert_eq!(tok_typ(\" b\\\"hel\\nlo\\\" \"), [BinStr]);\n    assert_eq!(tok_str(\" b'hel\\nlo' \"), [\"b'hel\\nlo'\"]);\n    assert_eq!(tok_typ(\" b'hel\\nlo' \"), [BinStr]);\n    assert_eq!(tok_typ(\" rb'hel\\nlo' \"), [BinStr]);\n    assert_eq!(tok_typ(\" br'hel\\nlo' \"), [BinStr]);\n    assert_eq!(tok_str(\" rb'hel\\nlo' \"), [\"rb'hel\\nlo'\"]);\n    assert_eq!(tok_str(\" br'hel\\nlo' \"), [\"br'hel\\nlo'\"]);\n    assert_eq!(tok_str(\" `hel\\nlo` \"), [\"`hel\\nlo`\"]);\n    assert_eq!(tok_typ(\" `hel\\nlo` \"), [Ident]);\n\n    assert_eq!(tok_err(r#\"\"hello\"#), \"unterminated string, quoted by `\\\"`\");\n    assert_eq!(tok_err(r#\"'hello\"#), \"unterminated string, quoted by `'`\");\n    assert_eq!(tok_err(r#\"r\"hello\"#), \"unterminated string, quoted by `\\\"`\");\n    assert_eq!(tok_err(r#\"r'hello\"#), \"unterminated string, quoted by `'`\");\n    assert_eq!(tok_err(r#\"b\"hello\"#), \"unterminated string, quoted by `\\\"`\");\n    assert_eq!(tok_err(r#\"b'hello\"#), \"unterminated string, quoted by `'`\");\n    assert_eq!(tok_err(r#\"`hello\"#), \"unterminated backtick name\");\n\n    assert_eq!(\n        tok_err(r#\"name`type`\"#),\n        \"prefix \\\"name\\\" is not allowed for field names, \\\n        perhaps missing comma or dot?\"\n    );\n    assert_eq!(\n        tok_err(r#\"User`type`\"#),\n        \"prefix \\\"User\\\" is not allowed for field names, \\\n        perhaps missing comma or dot?\"\n    );\n    assert_eq!(\n        tok_err(r#\"r`hello\"#),\n        \"prefix \\\"r\\\" is not allowed for field names, \\\n        perhaps missing comma or dot?\"\n    );\n    assert_eq!(\n        tok_err(r#\"b`hello\"#),\n        \"prefix \\\"b\\\" is not allowed for field names, \\\n        perhaps missing comma or dot?\"\n    );\n    assert_eq!(\n        tok_err(r#\"test\"hello\"\"#),\n        \"prefix \\\"test\\\" is not allowed for strings, \\\n        allowed: `b`, `r`\"\n    );\n    assert_eq!(\n        tok_err(r#\"test'hello'\"#),\n        \"prefix \\\"test\\\" is not allowed for strings, \\\n        allowed: `b`, `r`\"\n    );\n    assert_eq!(\n        tok_err(r#\"`@x`\"#),\n        \"backtick-quoted name cannot start with char `@`\"\n    );\n    assert_eq!(\n        tok_err(r#\"`$x`\"#),\n        \"backtick-quoted name cannot start with char `$`\"\n    );\n    assert_eq!(\n        tok_err(r#\"`a::b`\"#),\n        \"backtick-quoted name cannot contain `::`\"\n    );\n    assert_eq!(\n        tok_err(r#\"`__x__`\"#),\n        \"backtick-quoted names surrounded by double \\\n                    underscores are forbidden\"\n    );\n}\n\n#[test]\nfn string_prohibited_chars() {\n    assert_eq!(\n        tok_err(\"'xxx \\u{202A}'\"),\n        \"character U+202A is not allowed, use escaped form \\\\u202a\"\n    );\n    assert_eq!(\n        tok_err(\"\\\"\\u{202A} yyy\\\"\"),\n        \"character U+202A is not allowed, use escaped form \\\\u202a\"\n    );\n    assert_eq!(\n        tok_err(\"r\\\"\\u{202A}\\\"\"),\n        \"character U+202A is not allowed, use escaped form \\\\u202a\"\n    );\n    assert_eq!(\n        tok_err(\"r'\\u{202A}'\"),\n        \"character U+202A is not allowed, use escaped form \\\\u202a\"\n    );\n    assert_eq!(\n        tok_err(\"b'\\u{202A}'\"),\n        \"invalid bytes literal: character '\\\\u{202a}' \\\n         is unexpected, only ascii chars are allowed in bytes literals\"\n    );\n    assert_eq!(\n        tok_err(\"b\\\"\\u{202A}\\\"\"),\n        \"invalid bytes literal: character '\\\\u{202a}' \\\n         is unexpected, only ascii chars are allowed in bytes literals\"\n    );\n    assert_eq!(tok_err(\"`\\u{202A}`\"), \"character U+202A is not allowed\");\n    assert_eq!(tok_err(\"$`\\u{202A}`\"), \"character U+202A is not allowed\");\n    assert_eq!(\n        tok_err(\"$x\\u{202A}$ inner $x\\u{202A}$\"),\n        \"unexpected character '\\\\u{202a}'\"\n    );\n    assert_eq!(tok_err(\"$$ \\u{202A} $$\"), \"character U+202A is not allowed\");\n    assert_eq!(\n        tok_err(\"$hello$ \\u{202A} $hello$\"),\n        \"character U+202A is not allowed\"\n    );\n    assert_eq!(tok_err(\"'xxx \\0'\"), \"character U+0000 is not allowed\");\n    assert_eq!(tok_err(\"xxx \\0\"), \"unexpected character '\\\\0'\");\n    assert_eq!(tok_err(\"xxx $x$\\0$x$\"), \"character U+0000 is not allowed\");\n}\n\n#[test]\nfn test_dollar() {\n    assert_eq!(\n        tok_str(\"select $$ something $$; x\"),\n        [\"select\", \"$$ something $$\", \";\", \"x\"]\n    );\n    assert_eq!(\n        tok_typ(\"select $$ something $$; x\"),\n        [keyword(\"select\"), Str, Semicolon, Ident]\n    );\n    assert_eq!(\n        tok_str(\"select $a$ ; $b$ ; $b$ ; $a$; x\"),\n        [\"select\", \"$a$ ; $b$ ; $b$ ; $a$\", \";\", \"x\"]\n    );\n    assert_eq!(\n        tok_typ(\"select $a$ ; $b$ ; $b$ ; $a$; x\"),\n        [keyword(\"select\"), Str, Semicolon, Ident]\n    );\n    assert_eq!(\n        tok_str(\"select $a$ ; $b$ ; $a$; x\"),\n        [\"select\", \"$a$ ; $b$ ; $a$\", \";\", \"x\"]\n    );\n    assert_eq!(\n        tok_typ(\"select $a$ ; $b$ ; $a$; x\"),\n        [keyword(\"select\"), Str, Semicolon, Ident]\n    );\n    assert_eq!(\n        tok_err(\"select $$ ; $ab$ test;\"),\n        \"unterminated string started with $$\"\n    );\n    assert_eq!(\n        tok_err(\"select $a$ ; $$ test;\"),\n        \"unterminated string started with \\\"$a$\\\"\"\n    );\n    assert_eq!(\n        tok_err(\"select $0$\"),\n        \"dollar quote must not start with a digit\"\n    );\n    assert_eq!(\n        tok_err(\"select $фыва$\"),\n        \"dollar quote supports only ascii chars\"\n    );\n    assert_eq!(\n        tok_str(\"select $a$a$ ; $a$ test;\"),\n        [\"select\", \"$a$a$ ; $a$\", \"test\", \";\"]\n    );\n    assert_eq!(\n        tok_typ(\"select $a$a$ ; $a$ test;\"),\n        [keyword(\"select\"), Str, Ident, Semicolon]\n    );\n    assert_eq!(\n        tok_str(\"select $a+b; $b test; $a+b; $b ;\"),\n        [\"select\", \"$a\", \"+\", \"b\", \";\", \"$b\", \"test\", \";\", \"$a\", \"+\", \"b\", \";\", \"$b\", \";\"]\n    );\n    assert_eq!(\n        tok_typ(\"select $a+b; $b test; $a+b; $b ;\"),\n        [\n            keyword(\"select\"),\n            Parameter,\n            Add,\n            Ident,\n            Semicolon,\n            Parameter,\n            Ident,\n            Semicolon,\n            Parameter,\n            Add,\n            Ident,\n            Semicolon,\n            Parameter,\n            Semicolon\n        ]\n    );\n    assert_eq!(\n        tok_str(\"select $def x$y test; $def x$y\"),\n        [\"select\", \"$def\", \"x\", \"$y\", \"test\", \";\", \"$def\", \"x\", \"$y\"]\n    );\n    assert_eq!(\n        tok_typ(\"select $def x$y test; $def x$y\"),\n        [\n            keyword(\"select\"),\n            Parameter,\n            Ident,\n            Parameter,\n            Ident,\n            Semicolon,\n            Parameter,\n            Ident,\n            Parameter\n        ]\n    );\n    assert_eq!(\n        tok_str(\"select $`x``y` + $0 + $`zz` + $1.2 + $фыва\"),\n        [\n            \"select\",\n            \"$`x``y`\",\n            \"+\",\n            \"$0\",\n            \"+\",\n            \"$`zz`\",\n            \"+\",\n            \"$1\",\n            \".\",\n            \"2\",\n            \"+\",\n            \"$фыва\"\n        ]\n    );\n    assert_eq!(\n        tok_typ(\"select $`x``y` + $0 + $`zz` + $1.2 + $фыва\"),\n        [\n            keyword(\"select\"),\n            Parameter,\n            Add,\n            Parameter,\n            Add,\n            Parameter,\n            Add,\n            Parameter,\n            Dot,\n            IntConst,\n            Add,\n            Parameter\n        ]\n    );\n    assert_eq!(tok_err(r#\"$-\"#), \"bare $ is not allowed\");\n    assert_eq!(\n        tok_err(r#\"$0abc\"#),\n        \"the \\\"$0abc\\\" is not a valid argument, \\\n         either name starting with letter or only digits are expected\"\n    );\n    assert_eq!(tok_err(r#\"-$\"#), \"bare $ is not allowed\");\n    assert_eq!(\n        tok_err(r#\" $``  \"#),\n        \"backtick-quoted argument cannot be empty\"\n    );\n    assert_eq!(\n        tok_err(r#\"$`@x`\"#),\n        \"backtick-quoted argument cannot \\\n        start with char `@`\"\n    );\n    assert_eq!(\n        tok_err(r#\"$`a::b`\"#),\n        \"backtick-quoted argument cannot contain `::`\"\n    );\n    assert_eq!(\n        tok_err(r#\"$`__x__`\"#),\n        \"backtick-quoted arguments surrounded by double \\\n                    underscores are forbidden\"\n    );\n}\n\n#[test]\nfn invalid_suffix() {\n    assert_eq!(\n        tok_err(\"SELECT 1d;\"),\n        \"suffix \\\"d\\\" \\\n        is invalid for numbers, perhaps you wanted `1n` (bigint)?\"\n    );\n}\n\n#[test]\nfn test_substitution() {\n    assert_eq!(tok_str(\"SELECT \\\\(expr);\"), [\"SELECT\", \"\\\\(expr)\", \";\"]);\n    assert_eq!(\n        tok_typ(\"SELECT \\\\(expr);\"),\n        [keyword(\"select\"), Substitution, Semicolon]\n    );\n    assert_eq!(\n        tok_str(\"SELECT \\\\(other_Name1);\"),\n        [\"SELECT\", \"\\\\(other_Name1)\", \";\"]\n    );\n    assert_eq!(\n        tok_typ(\"SELECT \\\\(other_Name1);\"),\n        [keyword(\"select\"), Substitution, Semicolon]\n    );\n    assert_eq!(\n        tok_err(\"SELECT \\\\(some-name);\"),\n        \"only alphanumerics are allowed in \\\\(name) token\"\n    );\n    assert_eq!(tok_err(\"SELECT \\\\(some_name\"), \"unclosed \\\\(name) token\");\n}\n"
  },
  {
    "path": "edb/errors/__init__.py",
    "content": "# AUTOGENERATED FROM \"edb/api/errors.txt\" WITH\n#    $ edb gen-errors\n\n\n# flake8: noqa\n\n\nfrom edb.errors.base import *\n\n\n__all__ = base.__all__ + (  # type: ignore\n    'InternalServerError',\n    'UnsupportedFeatureError',\n    'ProtocolError',\n    'BinaryProtocolError',\n    'UnsupportedProtocolVersionError',\n    'TypeSpecNotFoundError',\n    'UnexpectedMessageError',\n    'InputDataError',\n    'ParameterTypeMismatchError',\n    'StateMismatchError',\n    'ResultCardinalityMismatchError',\n    'CapabilityError',\n    'UnsupportedCapabilityError',\n    'DisabledCapabilityError',\n    'UnsafeIsolationLevelError',\n    'QueryError',\n    'InvalidSyntaxError',\n    'EdgeQLSyntaxError',\n    'SchemaSyntaxError',\n    'GraphQLSyntaxError',\n    'InvalidTypeError',\n    'InvalidTargetError',\n    'InvalidLinkTargetError',\n    'InvalidPropertyTargetError',\n    'InvalidReferenceError',\n    'UnknownModuleError',\n    'UnknownLinkError',\n    'UnknownPropertyError',\n    'UnknownUserError',\n    'UnknownDatabaseError',\n    'UnknownParameterError',\n    'DeprecatedScopingError',\n    'SchemaError',\n    'SchemaDefinitionError',\n    'InvalidDefinitionError',\n    'InvalidModuleDefinitionError',\n    'InvalidLinkDefinitionError',\n    'InvalidPropertyDefinitionError',\n    'InvalidUserDefinitionError',\n    'InvalidDatabaseDefinitionError',\n    'InvalidOperatorDefinitionError',\n    'InvalidAliasDefinitionError',\n    'InvalidFunctionDefinitionError',\n    'InvalidConstraintDefinitionError',\n    'InvalidCastDefinitionError',\n    'DuplicateDefinitionError',\n    'DuplicateModuleDefinitionError',\n    'DuplicateLinkDefinitionError',\n    'DuplicatePropertyDefinitionError',\n    'DuplicateUserDefinitionError',\n    'DuplicateDatabaseDefinitionError',\n    'DuplicateOperatorDefinitionError',\n    'DuplicateViewDefinitionError',\n    'DuplicateFunctionDefinitionError',\n    'DuplicateConstraintDefinitionError',\n    'DuplicateCastDefinitionError',\n    'DuplicateMigrationError',\n    'SessionTimeoutError',\n    'IdleSessionTimeoutError',\n    'QueryTimeoutError',\n    'TransactionTimeoutError',\n    'IdleTransactionTimeoutError',\n    'ExecutionError',\n    'InvalidValueError',\n    'DivisionByZeroError',\n    'NumericOutOfRangeError',\n    'AccessPolicyError',\n    'QueryAssertionError',\n    'IntegrityError',\n    'ConstraintViolationError',\n    'CardinalityViolationError',\n    'MissingRequiredError',\n    'TransactionError',\n    'TransactionConflictError',\n    'TransactionSerializationError',\n    'TransactionDeadlockError',\n    'QueryCacheInvalidationError',\n    'WatchError',\n    'ConfigurationError',\n    'AccessError',\n    'AuthenticationError',\n    'AvailabilityError',\n    'BackendUnavailableError',\n    'ServerOfflineError',\n    'UnknownTenantError',\n    'ServerBlockedError',\n    'BackendError',\n    'UnsupportedBackendFeatureError',\n    'LogMessage',\n    'WarningMessage',\n    'StatusMessage',\n    'MigrationStatusMessage',\n)\n\n\nclass InternalServerError(EdgeDBError):\n    _code = 0x_01_00_00_00\n\n\nclass UnsupportedFeatureError(EdgeDBError):\n    _code = 0x_02_00_00_00\n\n\nclass ProtocolError(EdgeDBError):\n    _code = 0x_03_00_00_00\n\n\nclass BinaryProtocolError(ProtocolError):\n    _code = 0x_03_01_00_00\n\n\nclass UnsupportedProtocolVersionError(BinaryProtocolError):\n    _code = 0x_03_01_00_01\n\n\nclass TypeSpecNotFoundError(BinaryProtocolError):\n    _code = 0x_03_01_00_02\n\n\nclass UnexpectedMessageError(BinaryProtocolError):\n    _code = 0x_03_01_00_03\n\n\nclass InputDataError(ProtocolError):\n    _code = 0x_03_02_00_00\n\n\nclass ParameterTypeMismatchError(InputDataError):\n    _code = 0x_03_02_01_00\n\n\nclass StateMismatchError(InputDataError):\n    _code = 0x_03_02_02_00\n\n\nclass ResultCardinalityMismatchError(ProtocolError):\n    _code = 0x_03_03_00_00\n\n\nclass CapabilityError(ProtocolError):\n    _code = 0x_03_04_00_00\n\n\nclass UnsupportedCapabilityError(CapabilityError):\n    _code = 0x_03_04_01_00\n\n\nclass DisabledCapabilityError(CapabilityError):\n    _code = 0x_03_04_02_00\n\n\nclass UnsafeIsolationLevelError(CapabilityError):\n    _code = 0x_03_04_03_00\n\n\nclass QueryError(EdgeDBError):\n    _code = 0x_04_00_00_00\n\n\nclass InvalidSyntaxError(QueryError):\n    _code = 0x_04_01_00_00\n\n\nclass EdgeQLSyntaxError(InvalidSyntaxError):\n    _code = 0x_04_01_01_00\n\n\nclass SchemaSyntaxError(InvalidSyntaxError):\n    _code = 0x_04_01_02_00\n\n\nclass GraphQLSyntaxError(InvalidSyntaxError):\n    _code = 0x_04_01_03_00\n\n\nclass InvalidTypeError(QueryError):\n    _code = 0x_04_02_00_00\n\n\nclass InvalidTargetError(InvalidTypeError):\n    _code = 0x_04_02_01_00\n\n\nclass InvalidLinkTargetError(InvalidTargetError):\n    _code = 0x_04_02_01_01\n\n\nclass InvalidPropertyTargetError(InvalidTargetError):\n    _code = 0x_04_02_01_02\n\n\nclass InvalidReferenceError(QueryError):\n    _code = 0x_04_03_00_00\n\n\nclass UnknownModuleError(InvalidReferenceError):\n    _code = 0x_04_03_00_01\n\n\nclass UnknownLinkError(InvalidReferenceError):\n    _code = 0x_04_03_00_02\n\n\nclass UnknownPropertyError(InvalidReferenceError):\n    _code = 0x_04_03_00_03\n\n\nclass UnknownUserError(InvalidReferenceError):\n    _code = 0x_04_03_00_04\n\n\nclass UnknownDatabaseError(InvalidReferenceError):\n    _code = 0x_04_03_00_05\n\n\nclass UnknownParameterError(InvalidReferenceError):\n    _code = 0x_04_03_00_06\n\n\nclass DeprecatedScopingError(InvalidReferenceError):\n    _code = 0x_04_03_00_07\n\n\nclass SchemaError(QueryError):\n    _code = 0x_04_04_00_00\n\n\nclass SchemaDefinitionError(QueryError):\n    _code = 0x_04_05_00_00\n\n\nclass InvalidDefinitionError(SchemaDefinitionError):\n    _code = 0x_04_05_01_00\n\n\nclass InvalidModuleDefinitionError(InvalidDefinitionError):\n    _code = 0x_04_05_01_01\n\n\nclass InvalidLinkDefinitionError(InvalidDefinitionError):\n    _code = 0x_04_05_01_02\n\n\nclass InvalidPropertyDefinitionError(InvalidDefinitionError):\n    _code = 0x_04_05_01_03\n\n\nclass InvalidUserDefinitionError(InvalidDefinitionError):\n    _code = 0x_04_05_01_04\n\n\nclass InvalidDatabaseDefinitionError(InvalidDefinitionError):\n    _code = 0x_04_05_01_05\n\n\nclass InvalidOperatorDefinitionError(InvalidDefinitionError):\n    _code = 0x_04_05_01_06\n\n\nclass InvalidAliasDefinitionError(InvalidDefinitionError):\n    _code = 0x_04_05_01_07\n\n\nclass InvalidFunctionDefinitionError(InvalidDefinitionError):\n    _code = 0x_04_05_01_08\n\n\nclass InvalidConstraintDefinitionError(InvalidDefinitionError):\n    _code = 0x_04_05_01_09\n\n\nclass InvalidCastDefinitionError(InvalidDefinitionError):\n    _code = 0x_04_05_01_0A\n\n\nclass DuplicateDefinitionError(SchemaDefinitionError):\n    _code = 0x_04_05_02_00\n\n\nclass DuplicateModuleDefinitionError(DuplicateDefinitionError):\n    _code = 0x_04_05_02_01\n\n\nclass DuplicateLinkDefinitionError(DuplicateDefinitionError):\n    _code = 0x_04_05_02_02\n\n\nclass DuplicatePropertyDefinitionError(DuplicateDefinitionError):\n    _code = 0x_04_05_02_03\n\n\nclass DuplicateUserDefinitionError(DuplicateDefinitionError):\n    _code = 0x_04_05_02_04\n\n\nclass DuplicateDatabaseDefinitionError(DuplicateDefinitionError):\n    _code = 0x_04_05_02_05\n\n\nclass DuplicateOperatorDefinitionError(DuplicateDefinitionError):\n    _code = 0x_04_05_02_06\n\n\nclass DuplicateViewDefinitionError(DuplicateDefinitionError):\n    _code = 0x_04_05_02_07\n\n\nclass DuplicateFunctionDefinitionError(DuplicateDefinitionError):\n    _code = 0x_04_05_02_08\n\n\nclass DuplicateConstraintDefinitionError(DuplicateDefinitionError):\n    _code = 0x_04_05_02_09\n\n\nclass DuplicateCastDefinitionError(DuplicateDefinitionError):\n    _code = 0x_04_05_02_0A\n\n\nclass DuplicateMigrationError(DuplicateDefinitionError):\n    _code = 0x_04_05_02_0B\n\n\nclass SessionTimeoutError(QueryError):\n    _code = 0x_04_06_00_00\n\n\nclass IdleSessionTimeoutError(SessionTimeoutError):\n    _code = 0x_04_06_01_00\n\n\nclass QueryTimeoutError(SessionTimeoutError):\n    _code = 0x_04_06_02_00\n\n\nclass TransactionTimeoutError(SessionTimeoutError):\n    _code = 0x_04_06_0A_00\n\n\nclass IdleTransactionTimeoutError(TransactionTimeoutError):\n    _code = 0x_04_06_0A_01\n\n\nclass ExecutionError(EdgeDBError):\n    _code = 0x_05_00_00_00\n\n\nclass InvalidValueError(ExecutionError):\n    _code = 0x_05_01_00_00\n\n\nclass DivisionByZeroError(InvalidValueError):\n    _code = 0x_05_01_00_01\n\n\nclass NumericOutOfRangeError(InvalidValueError):\n    _code = 0x_05_01_00_02\n\n\nclass AccessPolicyError(InvalidValueError):\n    _code = 0x_05_01_00_03\n\n\nclass QueryAssertionError(InvalidValueError):\n    _code = 0x_05_01_00_04\n\n\nclass IntegrityError(ExecutionError):\n    _code = 0x_05_02_00_00\n\n\nclass ConstraintViolationError(IntegrityError):\n    _code = 0x_05_02_00_01\n\n\nclass CardinalityViolationError(IntegrityError):\n    _code = 0x_05_02_00_02\n\n\nclass MissingRequiredError(IntegrityError):\n    _code = 0x_05_02_00_03\n\n\nclass TransactionError(ExecutionError):\n    _code = 0x_05_03_00_00\n\n\nclass TransactionConflictError(TransactionError):\n    _code = 0x_05_03_01_00\n\n\nclass TransactionSerializationError(TransactionConflictError):\n    _code = 0x_05_03_01_01\n\n\nclass TransactionDeadlockError(TransactionConflictError):\n    _code = 0x_05_03_01_02\n\n\nclass QueryCacheInvalidationError(TransactionConflictError):\n    _code = 0x_05_03_01_03\n\n\nclass WatchError(ExecutionError):\n    _code = 0x_05_04_00_00\n\n\nclass ConfigurationError(EdgeDBError):\n    _code = 0x_06_00_00_00\n\n\nclass AccessError(EdgeDBError):\n    _code = 0x_07_00_00_00\n\n\nclass AuthenticationError(AccessError):\n    _code = 0x_07_01_00_00\n\n\nclass AvailabilityError(EdgeDBError):\n    _code = 0x_08_00_00_00\n\n\nclass BackendUnavailableError(AvailabilityError):\n    _code = 0x_08_00_00_01\n\n\nclass ServerOfflineError(AvailabilityError):\n    _code = 0x_08_00_00_02\n\n\nclass UnknownTenantError(AvailabilityError):\n    _code = 0x_08_00_00_03\n\n\nclass ServerBlockedError(AvailabilityError):\n    _code = 0x_08_00_00_04\n\n\nclass BackendError(EdgeDBError):\n    _code = 0x_09_00_00_00\n\n\nclass UnsupportedBackendFeatureError(BackendError):\n    _code = 0x_09_00_01_00\n\n\nclass LogMessage(EdgeDBMessage):\n    _code = 0x_F0_00_00_00\n\n\nclass WarningMessage(LogMessage):\n    _code = 0x_F0_01_00_00\n\n\nclass StatusMessage(LogMessage):\n    _code = 0x_F0_02_00_00\n\n\nclass MigrationStatusMessage(StatusMessage):\n    _code = 0x_F0_02_00_01\n"
  },
  {
    "path": "edb/errors/base.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Optional, Iterator\n\nfrom edb.common import span as edb_span\nfrom edb.common import exceptions as ex\n\nimport contextlib\n\n\n__all__ = (\n    'EdgeDBError', 'EdgeDBMessage', 'ensure_span',\n)\n\n\nclass EdgeDBErrorMeta(type):\n    _error_map: dict[int, type[EdgeDBError]] = {}\n    _name_map: dict[str, type[EdgeDBError]] = {}\n\n    def __new__(mcls, name, bases, dct):\n        cls = super().__new__(mcls, name, bases, dct)\n\n        assert name not in mcls._name_map\n        mcls._name_map[name] = cls\n\n        code = dct.get('_code')\n        if code is not None:\n            mcls._error_map[code] = cls\n\n        return cls\n\n    def __init__(cls, name, bases, dct):\n        if cls._code is None and cls.__module__ != __name__:\n            # We don't want any EdgeDBError subclasses to not\n            # have a code.\n            raise RuntimeError(\n                'direct subclassing of EdgeDBError is prohibited; '\n                'subclass one of its subclasses in edb.errors')\n\n    @classmethod\n    def get_error_class_from_code(mcls, code: int) -> type[EdgeDBError]:\n        return mcls._error_map[code]\n\n    @classmethod\n    def get_error_class_from_name(mcls, name: str) -> type[EdgeDBError]:\n        return mcls._name_map[name]\n\n\nclass EdgeDBMessage(Warning):\n\n    _code: Optional[int] = None\n\n    @classmethod\n    def get_code(cls):\n        if cls._code is None:\n            raise RuntimeError(\n                f'EdgeDB message code is not set (type: {cls.__name__})')\n        return cls._code\n\n\nclass EdgeDBError(Exception, metaclass=EdgeDBErrorMeta):\n\n    _code: Optional[int] = None\n    _attrs: dict[int, str]\n    _pgext_code: Optional[str] = None\n\n    def __init__(\n        self,\n        msg: Optional[str] = None,\n        *,\n        hint: Optional[str] = None,\n        details: Optional[str] = None,\n        span: Optional[edb_span.Span] = None,\n        position: Optional[tuple[int, int, int, int | None]] = None,\n        filename: Optional[str] = None,\n        pgext_code: Optional[str] = None,\n    ):\n        if type(self) is EdgeDBError:\n            raise RuntimeError(\n                'EdgeDBError is not supposed to be instantiated directly')\n\n        self._attrs = {}\n        self._pgext_code = pgext_code\n\n        if span:\n            self.set_span(span)\n        elif position:\n            self.set_linecol(position[1], position[0])\n            self.set_position(position[2], position[3])\n\n        if filename is not None:\n            self.set_filename(filename)\n\n        self.set_hint_and_details(hint, details)\n\n        super().__init__(msg)\n\n    @classmethod\n    def get_code(cls):\n        if cls._code is None:\n            raise RuntimeError(\n                f'Gel message code is not set (type: {cls.__name__})')\n        return cls._code\n\n    def to_json(self):\n        err_dct = {\n            'message': str(self),\n            'type': str(type(self).__name__),\n            'code': self.get_code(),\n        }\n        for name, field in _JSON_FIELDS.items():\n            if field in self._attrs:\n                val = self._attrs[field]\n                if field in _INT_FIELDS:\n                    val = int(val)\n                err_dct[name] = val\n\n        return err_dct\n\n    def set_filename(self, filename):\n        self._attrs[FIELD_FILENAME] = filename\n\n    def set_linecol(\n        self,\n        line: Optional[int],  # one-based\n        col: Optional[int],  # one-based\n    ):\n        if line is not None:\n            self._attrs[FIELD_LINE_START] = str(line)\n        if col is not None:\n            self._attrs[FIELD_COLUMN_START] = str(col)\n\n    def compute_line_col(self, source: str):\n        from edb.edgeql import tokenizer\n\n        start: int = self.position\n        end: int | None = self.position_end\n        if end and end < 0:\n            end = None\n\n        start_s, end_s = tokenizer.inflate_span(source, (start, end))\n\n        self._attrs[FIELD_LINE_START] = str(start_s.line)\n        self._attrs[FIELD_COLUMN_START] = str(start_s.column)\n        if end_s is not None:\n            self._attrs[FIELD_LINE_END] = str(end_s.line)\n            self._attrs[FIELD_COLUMN_END] = str(end_s.column)\n\n    def set_hint_and_details(self, hint, details=None):\n        ex.replace_context(\n            self, ex.DefaultExceptionContext(hint=hint, details=details))\n\n        if hint is not None:\n            self._attrs[FIELD_HINT] = hint\n        if details is not None:\n            self._attrs[FIELD_DETAILS] = details\n\n    def has_span(self):\n        return FIELD_POSITION_START in self._attrs\n\n    def get_span(self) -> tuple[int, int | None] | None:\n        if FIELD_POSITION_START not in self._attrs:\n            return None\n        return (\n            int(self._attrs[FIELD_POSITION_START]),\n            (\n                int(self._attrs[FIELD_POSITION_END])\n                if FIELD_POSITION_END in self._attrs\n                else None\n            ),\n        )\n\n    def set_span(self, span: Optional[edb_span.Span]):\n        if not span:\n            return\n\n        start = span.start_point\n        end = span.end_point\n        ex.replace_context(self, span)\n\n        self._attrs[FIELD_POSITION_START] = str(start.offset)\n        self._attrs[FIELD_POSITION_END] = str(end.offset)\n        self._attrs[FIELD_CHARACTER_START] = str(start.char_offset)\n        self._attrs[FIELD_CHARACTER_END] = str(end.char_offset)\n        self._attrs[FIELD_LINE_START] = str(start.line)\n        self._attrs[FIELD_COLUMN_START] = str(start.column)\n        self._attrs[FIELD_UTF16_COLUMN_START] = str(start.utf16column)\n        self._attrs[FIELD_LINE_END] = str(end.line)\n        self._attrs[FIELD_COLUMN_END] = str(end.column)\n        self._attrs[FIELD_UTF16_COLUMN_END] = str(end.utf16column)\n        if span.filename and span.filename != '<string>':\n            self._attrs[FIELD_FILENAME] = span.filename\n\n    def get_position(self) -> tuple[int, int, int, int | None] | None:\n        if FIELD_COLUMN_START not in self._attrs:\n            return None\n        return (\n            int(self._attrs[FIELD_COLUMN_START]),\n            int(self._attrs[FIELD_LINE_START]),\n            int(self._attrs[FIELD_POSITION_START]),\n            int(self._attrs[FIELD_POSITION_END])\n            if FIELD_POSITION_END in self._attrs\n            else None,\n        )\n\n    def set_position(\n        self,\n        start: int,  # zero-based\n        end: Optional[int],  # zero-based\n    ):\n        self._attrs[FIELD_POSITION_START] = str(start)\n        self._attrs[FIELD_POSITION_END] = str(end or start)\n\n    @property\n    def line(self):\n        return int(self._attrs.get(FIELD_LINE_START, -1))\n\n    @property\n    def col(self):\n        return int(self._attrs.get(FIELD_COLUMN_START, -1))\n\n    @property\n    def line_end(self):\n        return int(self._attrs.get(FIELD_LINE_END, -1))\n\n    @property\n    def col_end(self):\n        return int(self._attrs.get(FIELD_COLUMN_END, -1))\n\n    @property\n    def position(self):\n        return int(self._attrs.get(FIELD_POSITION_START, -1))\n\n    @property\n    def position_end(self):\n        return int(self._attrs.get(FIELD_POSITION_END, -1))\n\n    @property\n    def hint(self):\n        return self._attrs.get(FIELD_HINT)\n\n    @property\n    def details(self):\n        return self._attrs.get(FIELD_DETAILS)\n\n    @property\n    def pgext_code(self):\n        return self._pgext_code\n\n    @property\n    def filename(self):\n        return self._attrs.get(FIELD_FILENAME, None)\n\n\n@contextlib.contextmanager\ndef ensure_span(span: Optional[edb_span.Span]) -> Iterator[None]:\n    try:\n        yield\n    except EdgeDBError as e:\n        if span and not e.has_span():\n            e.set_span(span)\n        raise\n\n\nFIELD_HINT = 0x_00_01\nFIELD_DETAILS = 0x_00_02\nFIELD_SERVER_TRACEBACK = 0x_01_01\n\n# XXX: Subject to be changed/deprecated.\nFIELD_POSITION_START = 0x_FF_F1\nFIELD_POSITION_END = 0x_FF_F2\nFIELD_LINE_START = 0x_FF_F3\nFIELD_COLUMN_START = 0x_FF_F4\nFIELD_UTF16_COLUMN_START = 0x_FF_F5\nFIELD_LINE_END = 0x_FF_F6\nFIELD_COLUMN_END = 0x_FF_F7\nFIELD_UTF16_COLUMN_END = 0x_FF_F8\nFIELD_CHARACTER_START = 0x_FF_F9\nFIELD_CHARACTER_END = 0x_FF_FA\nFIELD_FILENAME = 0x_FF_FB\n\n_INT_FIELDS = {\n    FIELD_POSITION_START,\n    FIELD_POSITION_END,\n    FIELD_LINE_START,\n    FIELD_COLUMN_START,\n    FIELD_UTF16_COLUMN_START,\n    FIELD_LINE_END,\n    FIELD_COLUMN_END,\n    FIELD_UTF16_COLUMN_END,\n    FIELD_CHARACTER_START,\n    FIELD_CHARACTER_END,\n}\n\n# Fields to include in the json dump of the type\n_JSON_FIELDS = {\n    'filename': FIELD_FILENAME,\n    'hint': FIELD_HINT,\n    'details': FIELD_DETAILS,\n    'start': FIELD_CHARACTER_START,\n    'end': FIELD_CHARACTER_END,\n    'line': FIELD_LINE_START,\n    'col': FIELD_COLUMN_START,\n}\n"
  },
  {
    "path": "edb/graphql/.gitignore",
    "content": "extension.c\n"
  },
  {
    "path": "edb/graphql/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom .compiler import compile_graphql\nfrom .translator import translate_ast, parse_text, parse_tokens\nfrom .translator import TranspiledOperation\nfrom .tokenizer import Source, NormalizedSource\nfrom .types import GQLCoreSchema\n\nfrom . import _patch_core\n_patch_core.patch_graphql_core()\n\n\n__all__ = (\n    'translate_ast', 'parse_text', 'parse_tokens', 'GQLCoreSchema',\n    'compile_graphql', 'TranspiledOperation', 'Source', 'NormalizedSource'\n)\n"
  },
  {
    "path": "edb/graphql/_patch_core.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n\ndef patch_graphql_core():\n    import graphql\n    import graphql.utilities.type_comparators as type_comparators\n\n    old_is_type_sub_type_of = type_comparators.is_type_sub_type_of\n\n    def is_type_sub_type_of(schema, maybe_subtype, super_type):\n        # allow coercing ints to floats\n        if super_type is graphql.GraphQLFloat:\n            if maybe_subtype is graphql.GraphQLInt:\n                return True\n        return old_is_type_sub_type_of(schema, maybe_subtype, super_type)\n\n    type_comparators.is_type_sub_type_of = is_type_sub_type_of\n"
  },
  {
    "path": "edb/graphql/codegen.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport json\nfrom edb.common.ast import codegen\n\n\nclass GraphQLSourceGenerator(codegen.SourceGenerator):\n    def generic_visit(self, node):\n        raise RuntimeError(\n            'No method to generate code for %s' % node.__class__.__name__)\n\n    def _visit_list(self, items, separator=None):\n        for item in items:\n            self.visit(item)\n            if item is not items[-1] and separator:\n                self.write(separator)\n\n    def _visit_arguments(self, node):\n        if node.arguments:\n            self.write('(')\n            self._visit_list(node.arguments, separator=', ')\n            self.write(')')\n\n    def _visit_directives(self, node):\n        if node.directives:\n            self.write(' ')\n            self._visit_list(node.directives, separator=', ')\n\n    def _visit_type_condition(self, node):\n        if node.type_condition:\n            self.write(' on ')\n            self.visit(node.type_condition)\n\n    def visit_NameNode(self, node):\n        self.write(node.value)\n\n    def visit_DocumentNode(self, node):\n        self._visit_list(node.definitions)\n\n    def visit_OperationDefinitionNode(self, node):\n        if node.operation:\n            self.write(node.operation)\n            if node.name:\n                self.write(' ')\n                self.visit(node.name)\n            if node.variable_definitions:\n                self.write('(')\n                self._visit_list(node.variable_definitions, separator=', ')\n                self.write(')')\n            self._visit_directives(node)\n\n        self.visit(node.selection_set)\n\n    def visit_FragmentDefinitionNode(self, node):\n        self.write('fragment ')\n        self.visit(node.name)\n        self._visit_type_condition(node)\n        self._visit_directives(node)\n        self.visit(node.selection_set)\n\n    def visit_SelectionSetNode(self, node):\n        self.write('{')\n        self.new_lines = 1\n        self.indentation += 1\n        self._visit_list(node.selections)\n        self.indentation -= 1\n        self.write('}')\n        self.new_lines = 2\n\n    def visit_FieldNode(self, node):\n        if node.alias:\n            self.visit(node.alias)\n            self.write(': ')\n        self.visit(node.name)\n        self._visit_arguments(node)\n        self._visit_directives(node)\n        if node.selection_set:\n            self.visit(node.selection_set)\n        else:\n            self.new_lines = 1\n\n    def visit_FragmentSpreadNode(self, node):\n        self.write('...')\n        self.visit(node.name)\n        self._visit_directives(node)\n        self.new_lines = 1\n\n    def visit_InlineFragmentNode(self, node):\n        self.write('...')\n        self._visit_type_condition(node)\n        self._visit_directives(node)\n        self.visit(node.selection_set)\n\n    def visit_ArgumentNode(self, node):\n        self.visit(node.name)\n        self.write(': ')\n        self.visit(node.value)\n\n    def visit_ObjectFieldNode(self, node):\n        self.visit_Argument(node)\n        self.new_lines = 1\n\n    def visit_VariableDefinitionNode(self, node):\n        self.visit(node.variable)\n        self.write(': ')\n        self.visit(node.type)\n        if node.default_value:\n            self.write(' = ')\n            self.visit(node.default_value)\n\n    def visit_DirectiveNode(self, node):\n        self.write('@')\n        self.visit(node.name)\n        self._visit_arguments(node)\n\n    def visit_StringValueNode(self, node):\n        # the GQL string works same as JSON string\n        self.write(json.dumps(node.value))\n\n    def visit_IntValueNode(self, node):\n        self.write(node.value)\n\n    def visit_FloatValueNode(self, node):\n        self.write(node.value)\n\n    def visit_BooleanValueNode(self, node):\n        if node.value:\n            self.write('true')\n        else:\n            self.write('false')\n\n    def visit_ListValueNode(self, node):\n        self.write('[')\n        self._visit_list(node.values, separator=', ')\n        self.write(']')\n\n    def visit_ObjectValueNode(self, node):\n        if node.fields:\n            self.write('{')\n            self.new_lines = 1\n            self.indentation += 1\n            self._visit_list(node.fields)\n            self.indentation -= 1\n            self.write('}')\n        else:\n            self.write('{}')\n\n    def visit_EnumValueNode(self, node):\n        self.write(node.value)\n\n    def visit_NullValueNode(self, node):\n        self.write('null')\n\n    def visit_VariableNode(self, node):\n        self.write('$')\n        self.visit(node.name)\n\n    def visit_NamedTypeNode(self, node):\n        self.visit(node.name)\n\n    def visit_ListTypeNode(self, node):\n        self.write('[')\n        self.visit(node.type)\n        self.write(']')\n\n    def visit_NonNullTypeNode(self, node):\n        self.visit(node.type)\n        self.write('!')\n\n\ngenerate_source = GraphQLSourceGenerator.to_source\n"
  },
  {
    "path": "edb/graphql/compiler.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, Mapping\n\n\nfrom edb import graphql\n\nfrom edb.schema import schema as s_schema\n\nfrom graphql.language import lexer as gql_lexer\n\n\ndef _get_gqlcore(\n    std_schema: s_schema.Schema,\n    user_schema: s_schema.Schema,\n    global_schema: s_schema.Schema,\n) -> graphql.GQLCoreSchema:\n    return graphql.GQLCoreSchema(\n        s_schema.ChainedSchema(\n            std_schema,\n            user_schema,\n            global_schema\n        )\n    )\n\n\ndef compile_graphql(\n    std_schema: s_schema.Schema,\n    user_schema: s_schema.Schema,\n    global_schema: s_schema.Schema,\n    database_config: Mapping[str, Any],\n    system_config: Mapping[str, Any],\n    gql: str,\n    tokens: Optional[\n        list[tuple[gql_lexer.TokenKind, int, int, int, int, str]]],\n    substitutions: Optional[dict[str, tuple[str, int, int]]],\n    operation_name: Optional[str] = None,\n    variables: Optional[Mapping[str, object]] = None,\n    native_input: bool = False,\n    extracted_variables: Optional[Mapping[str, object]] = None,\n) -> graphql.TranspiledOperation:\n    if tokens is None:\n        ast = graphql.parse_text(gql)\n    else:\n        ast = graphql.parse_tokens(gql, tokens)\n\n    gqlcore = _get_gqlcore(std_schema, user_schema, global_schema)\n\n    return graphql.translate_ast(\n        gqlcore,\n        ast,\n        variables=variables,\n        extracted_variables=extracted_variables,\n        substitutions=substitutions,\n        operation_name=operation_name,\n        native_input=native_input,\n    )\n"
  },
  {
    "path": "edb/graphql/errors.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nfrom typing import Optional\n\nfrom edb import errors\n\n\nclass GraphQLError(errors.QueryError):\n\n    def __init__(self, msg, *, loc: Optional[tuple[int, int]] = None):\n\n        super().__init__(msg)\n\n        if loc:\n            # XXX Will be fixes when we have proper LSP SourceLocation\n            # abstraction.\n            self.set_linecol(loc[0], loc[1])\n\n\nclass GraphQLTranslationError(GraphQLError):\n    pass\n\n\nclass GraphQLValidationError(GraphQLTranslationError):\n    pass\n\n\nclass GraphQLCoreError(GraphQLError):\n    pass\n"
  },
  {
    "path": "edb/graphql/explore.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport base64\n\n\n_react_ver = '16.8.3'\n_graphiql_ver = '0.12.0'\n\n\n_edgedb_logo = base64.b64encode(br'''\n<svg viewBox=\"0 0 110 90\" xmlns=\"http://www.w3.org/2000/svg\">\n  <path d=\"M93.17 44.76c0 7.59-3.043 8.95-6.445 8.95h-7.233V35.808h7.233c3.402 0 6.446 1.361 6.446 8.952zm-3.688 0c0-5.3-1.61-5.551-3.938-5.551h-2.256v11.1h2.256c2.327 0 3.938-.25 3.938-5.55zM51.17 53.71V35.808h11.386v3.402h-7.59v3.652h5.728v3.366h-5.729v4.082h7.591v3.402H51.17zm17.76 35.808h3.796V0H68.93v89.518zm31.833-43.756v4.547h3.15c1.97 0 2.471-1.289 2.471-2.256 0-.752-.358-2.291-3.043-2.291h-2.578zm0-6.553v3.402h2.578c1.468 0 2.327-.645 2.327-1.72 0-1.073-.86-1.682-2.327-1.682h-2.578zm-3.796-3.402h7.305c3.831 0 4.977 2.686 4.977 4.62 0 1.79-1.146 3.079-1.934 3.437 2.292 1.11 2.686 3.366 2.686 4.512 0 1.504-.752 5.335-5.73 5.335h-7.304V35.807zM29.362 44.76c0 7.591-3.044 8.952-6.445 8.952h-7.233V35.807h7.233c3.401 0 6.445 1.361 6.445 8.952zm11.172 5.693c1.933 0 2.936-.644 3.294-1.074v-1.97h-3.08V44.33h6.124v7.126c-.537.824-3.474 2.435-6.16 2.435-4.403 0-8.127-1.719-8.127-9.31s3.76-8.952 7.161-8.952c5.335 0 6.66 2.793 7.09 5.264l-3.151.716c-.18-1.146-1.182-2.578-3.473-2.578-2.328 0-3.94.25-3.94 5.55s1.684 5.872 4.262 5.872zm-14.86-5.693c0-5.3-1.611-5.55-3.939-5.55h-2.256v11.1h2.256c2.328 0 3.939-.25 3.939-5.55zM0 53.711V35.807h11.387v3.402H3.796v3.652h5.729v3.366h-5.73v4.082h7.592v3.402H0z\" fill=\"#4A4A4A\" fill-rule=\"evenodd\"/>\n</svg>''').decode()  # NoQA\n\n\nEXPLORE_HTML = (r'''\n<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"data:;base64,=\" />\n\n    <style>\n      body {\n        height: 100%;\n        margin: 0;\n        width: 100%;\n        overflow: hidden;\n      }\n      #graphiql {\n        height: 100vh;\n        width: 100wh;\n      }\n    </style>\n\n    <script src=\"//cdnjs.cloudflare.com/ajax/libs/react/''' +\n        _react_ver + r'''/umd/react.production.min.js\"></script>\n    <script src=\"//cdnjs.cloudflare.com/ajax/libs/react-dom/''' +\n        _react_ver + r'''/umd/react-dom.production.min.js\"></script>\n    <script src=\"//cdnjs.cloudflare.com/ajax/libs/graphiql/''' +\n        _graphiql_ver + r'''/graphiql.min.js\"></script>\n    <link href=\"//cdnjs.cloudflare.com/ajax/libs/graphiql/''' +\n        _graphiql_ver + r'''/graphiql.min.css\" rel=\"stylesheet\" />\n  </head>\n  <body>\n    <div id=\"graphiql\">Loading...</div>\n    <script><!--\n      function graphQLFetcher(graphQLParams) {\n        const root = window.location.toString().replace(/\\/explore[\\/]*$/, '');\n        return fetch(root, {\n          method: 'post',\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify(graphQLParams),\n        }).then(function(response) { return response.json() });\n      }\n\n      ReactDOM.render(\n        React.createElement(\n          GraphiQL,\n          {\n            fetcher: graphQLFetcher,\n          },\n          React.createElement(\n            GraphiQL.Logo,\n            {},\n            React.createElement(\n              'div',\n              {\n                style: {\n                  backgroundImage: 'url(\"data:image/svg+xml;base64,''' +\n                    _edgedb_logo + r'''\")',\n                  backgroundSize: 'cover',\n                  backgroundRepeat: 'no-repeat',\n                  backgroundPosition: 'center center',\n                  height: 40,\n                  width: 50,\n                }\n              }\n            )\n          )\n        ),\n        document.getElementById('graphiql')\n      );\n    //-->\n    </script>\n  </body>\n</html>\n''').encode()\n"
  },
  {
    "path": "edb/graphql/extension.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import (\n    Any,\n    Dict,\n    Tuple,\n    List,\n    Optional,\n    Union,\n)\n\nimport cython\nimport http\nimport json\nimport logging\nimport time\nimport urllib.parse\n\nfrom graphql.language import lexer as gql_lexer\n\nfrom edb import _graphql_rewrite\nfrom edb import errors\nfrom edb.graphql import errors as gql_errors\nfrom edb.server.dbview cimport dbview\nfrom edb.server import compiler, metrics\nfrom edb.server import defines as edbdef\nfrom edb.server.pgcon import errors as pgerrors\nfrom edb.server.protocol import execute\nfrom edb.server.compiler import errormech\n\nfrom edb.schema import schema as s_schema\n\nfrom edb.common import debug\nfrom edb.common import markup\n\nfrom . import explore\nfrom . import translator\n\n\nlogger = logging.getLogger(__name__)\n_USER_ERRORS = (\n    _graphql_rewrite.LexingError,\n    _graphql_rewrite.SyntaxError,\n    _graphql_rewrite.NotFoundError,\n)\n\n# key_vars tracks which variables are actually needed for evaluation,\n# since the compiler depends on some\n\n# redirect accounts for the fact that we actually don't always know\n# which the relevant variables are until after compiling\n\n@cython.final\ncdef class CacheRedirect:\n    cdef public list key_vars  # List[str],  must be sorted\n\n    def __init__(self, key_vars: List[str]):\n        self.key_vars = key_vars\n\n\nCacheEntry = Union[\n    CacheRedirect,\n    Tuple[compiler.QueryUnitGroup, translator.TranspiledOperation],\n]\n\n\nasync def handle_request(\n    object request,\n    object response,\n    object db,\n    str role_name,\n    list args,\n    object tenant,\n):\n    if args == ['explore'] and request.method == b'GET':\n        response.body = explore.EXPLORE_HTML\n        response.content_type = b'text/html'\n        return\n\n    if args != []:\n        response.body = b'Unknown path'\n        response.status = http.HTTPStatus.NOT_FOUND\n        response.close_connection = True\n        return\n\n    operation_name = None\n    variables = None\n    globals = None\n    config = None\n    deprecated_globals = None\n    query = None\n    query_bytes_len = 0\n\n    try:\n        if request.method == b'POST':\n            if request.content_type and b'json' in request.content_type:\n                body = json.loads(request.body)\n                if not isinstance(body, dict):\n                    raise TypeError(\n                        'the body of the request must be a JSON object')\n                query = body.get('query')\n                query_bytes_len = len(query.encode('utf-8'))\n                operation_name = body.get('operationName')\n                variables = body.get('variables')\n                deprecated_globals = body.get('globals')\n            elif request.content_type == 'application/graphql':\n                query_bytes_len = len(request.body)\n                query = request.body.decode('utf-8')\n            else:\n                raise TypeError(\n                    'unable to interpret GraphQL POST request')\n\n        elif request.method == b'GET':\n            if request.url.query:\n                url_query = request.url.query.decode('ascii')\n                qs = urllib.parse.parse_qs(url_query)\n\n                query = qs.get('query')\n                if query is not None:\n                    query = query[0]\n                    query_bytes_len = len(query.encode('utf-8'))\n\n                operation_name = qs.get('operationName')\n                if operation_name is not None:\n                    operation_name = operation_name[0]\n\n                variables = qs.get('variables')\n                if variables is not None:\n                    try:\n                        variables = json.loads(variables[0])\n                    except Exception:\n                        raise TypeError(\n                            '\"variables\" must be a JSON object')\n\n                deprecated_globals = qs.get('globals')\n                if deprecated_globals is not None:\n                    try:\n                        deprecated_globals = json.loads(deprecated_globals[0])\n                    except Exception:\n                        raise TypeError(\n                            '\"globals\" must be a JSON object')\n\n        else:\n            raise TypeError('expected a GET or a POST request')\n\n        if not query:\n            raise TypeError('invalid GraphQL request: query is missing')\n        metrics.query_size.observe(\n            query_bytes_len, tenant.get_instance_name(), 'graphql'\n        )\n\n        if (operation_name is not None and\n                not isinstance(operation_name, str)):\n            raise TypeError('operationName must be a string')\n\n        if variables is not None and not isinstance(variables, dict):\n            raise TypeError('\"variables\" must be a JSON object')\n\n        # There are 2 ways of sending globals:\n        # 1) as 'globals' field (deprecated)\n        # 2) as part of 'variables' in the '__globals__' element\n        #\n        # If both ways are present they must match.\n        if variables is not None:\n            globals = variables.get('__globals__')\n\n        if variables is not None:\n            config = variables.get('__config__')\n\n        if config is not None and not isinstance(config, dict):\n            raise TypeError('\"__config__\" must be a JSON object')\n\n        if globals is not None and not isinstance(globals, dict):\n            raise TypeError('\"__globals__\" must be a JSON object')\n        if (\n            deprecated_globals is not None and\n            not isinstance(deprecated_globals, dict)\n        ):\n            raise TypeError('\"globals\" must be a JSON object')\n\n        # Globals are dicts if they are present, make sure they are the same.\n        if (\n            globals is not None and deprecated_globals is not None and\n            globals != deprecated_globals\n        ):\n            raise ValueError('invalid \"__globals__\" and \"globals\": '\n                             'values must match when both are present')\n\n        globals = globals or deprecated_globals\n\n    except Exception as ex:\n        if debug.flags.server:\n            markup.dump(ex)\n\n        response.body = str(ex).encode()\n        response.status = http.HTTPStatus.BAD_REQUEST\n        response.close_connection = True\n        return\n\n    response.status = http.HTTPStatus.OK\n    response.content_type = b'application/json'\n    try:\n        result = await _execute(\n            db, role_name, tenant, query, operation_name, variables, globals,\n            config,\n        )\n    except Exception as ex:\n        if debug.flags.server:\n            markup.dump(ex)\n\n        if isinstance(ex, gql_errors.GraphQLError):\n            # XXX Fix this when LSP \"location\" objects are implemented\n            ex_type = errors.QueryError\n        else:\n            ex = await execute.interpret_error(\n                ex, db, from_graphql=True\n            )\n            ex_type = type(ex)\n\n        err_dct = {\n            'message': f'{ex_type.__name__}: {ex}',\n        }\n\n        if (isinstance(ex, errors.EdgeDBError) and\n                hasattr(ex, 'line') and\n                hasattr(ex, 'col')):\n            err_dct['locations'] = [{'line': ex.line, 'column': ex.col}]\n\n        response.body = json.dumps({'errors': [err_dct]}).encode()\n    else:\n        response.body = b'{\"data\":' + result + b'}'\n\n\nasync def compile(\n    dbview.DatabaseConnectionView dbv,\n    tenant,\n    query: str,\n    tokens: Optional[List[Tuple[int, int, int, str]]],\n    substitutions: Optional[Dict[str, Tuple[str, int, int]]],\n    operation_name: Optional[str],\n    variables: Dict[str, Any],\n):\n    db = dbv._db\n    server = tenant.server\n    compiler_pool = server.get_compiler_pool()\n    started_at = time.monotonic()\n    try:\n        return await compiler_pool.compile_graphql(\n            db.name,\n            db.user_schema_pickle,\n            tenant.get_global_schema_pickle(),\n            db.reflection_cache,\n            dbv.get_database_config(),\n            dbv.get_compilation_system_config(),\n            dbv.get_session_config(),\n            query,\n            tokens,\n            substitutions,\n            operation_name,\n            variables,\n            client_id=tenant.client_id,\n            client_name=tenant.get_instance_name(),\n        )\n    finally:\n        metrics.query_compilation_duration.observe(\n            time.monotonic() - started_at,\n            tenant.get_instance_name(),\n            \"graphql\",\n        )\n\nasync def _execute(\n    db, role_name, tenant, query, operation_name, variables, globals, config\n):\n    dbver = db.dbver\n    query_cache = tenant.server._http_query_cache\n\n    if variables:\n        for var_name in variables:\n            if var_name.startswith('__edb_arg_'):\n                raise errors.QueryError(\n                    f\"Variables starting with '__edb_arg_' are prohibited\")\n\n    query_cache_enabled = not debug.flags.disable_qcache\n\n    if debug.flags.graphql_compile:\n        debug.header('Input graphql')\n        print(query)\n        print(f'variables: {variables}')\n\n    try:\n        rewritten = _graphql_rewrite.rewrite(operation_name, query)\n    except _graphql_rewrite.QueryError as e:\n        raise errors.QueryError(e.args[0])\n    except Exception as e:\n        if isinstance(e, _USER_ERRORS):\n            logger.info(\"Error rewriting graphql query: %r\", e)\n        else:\n            logger.warning(\"Error rewriting graphql query: %r\", e)\n        rewritten = None\n        rewrite_error = e\n        prepared_query = query\n        vars = variables.copy() if variables else {}\n    else:\n        prepared_query = rewritten.key\n        vars = rewritten.variables.copy()\n        if variables:\n            vars.update(variables)\n\n        if debug.flags.graphql_compile:\n            debug.header('GraphQL optimized query')\n            print(rewritten)\n            print(f'variables: {vars}')\n\n    await db.introspection()\n\n    dbv: dbview.DatabaseConnectionView = await tenant.new_dbview(\n        dbname=db.name,\n        query_cache=False,\n        protocol_version=edbdef.CURRENT_PROTOCOL,\n        role_name=role_name,\n    )\n    dbv.is_transient = True\n    dbv.decode_json_session_config(config)\n\n    # Put the compilation-affecting session config into the cache key.\n    # N.B: We skip putting system/database config in here, since dbver\n    # gets bumped whenever those change.\n    config_key = db.server.compilation_config_serializer.encode_configs(\n        dbv.get_session_config()\n    )\n\n    cache_key = (\n        'graphql', prepared_query, (), operation_name, dbver, config_key\n    )\n    use_prep_stmt = False\n\n    entry: CacheEntry = None\n    if query_cache_enabled:\n        entry = query_cache.get(cache_key, None)\n\n    if isinstance(entry, CacheRedirect):\n        if debug.flags.graphql_compile:\n            print(\"REDIRECT\", entry.key_vars)\n\n        key_vars2 = tuple(vars[k] for k in entry.key_vars)\n        cache_key2 = (\n            prepared_query, key_vars2, operation_name, dbver, config_key\n        )\n        entry = query_cache.get(cache_key2, None)\n\n    if entry is None:\n        if rewritten is not None:\n            qug, gql_op = await compile(\n                dbv,\n                tenant,\n                query,\n                rewritten.tokens(gql_lexer.TokenKind),\n                rewritten.substitutions,\n                operation_name,\n                vars,\n            )\n        else:\n            qug, gql_op = await compile(\n                dbv,\n                tenant,\n                query,\n                None,\n                None,\n                operation_name,\n                vars,\n            )\n\n        if gql_op.cache_deps_vars and gql_op.cache_deps_vars:\n            key_var_set = set(gql_op.cache_deps_vars)\n            key_var_names = sorted(key_var_set)\n            redir = CacheRedirect(key_vars=key_var_names)\n            query_cache[cache_key] = redir\n            key_vars2 = tuple(vars[k] for k in key_var_names)\n            cache_key2 = (\n                'graphql', prepared_query, key_vars2, operation_name, dbver\n            )\n            query_cache[cache_key2] = qug, gql_op\n        else:\n            query_cache[cache_key] = qug, gql_op\n        metrics.graphql_query_compilations.inc(\n            1.0, tenant.get_instance_name(), 'compiler'\n        )\n    else:\n        qug, gql_op = entry\n        # This is at least the second time this query is used\n        # and it's safe to cache.\n        use_prep_stmt = True\n        metrics.graphql_query_compilations.inc(\n            1.0, tenant.get_instance_name(), 'cache'\n        )\n\n    compiled = dbview.CompiledQuery(query_unit_group=qug)\n\n    async with tenant.with_pgcon(db.name) as pgcon:\n        try:\n            return await execute.execute_json(\n                pgcon,\n                dbv,\n                compiled,\n                variables={**gql_op.variables_desc, **vars},\n                globals_=globals or {},\n                fe_conn=None,\n                use_prep_stmt=use_prep_stmt,\n            )\n        finally:\n            tenant.remove_dbview(dbv)\n"
  },
  {
    "path": "edb/graphql/tokenizer.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, Sequence\n\nimport hashlib\nimport struct\n\nfrom graphql.language import lexer as gql_lexer\n\nfrom edb import _graphql_rewrite as graphql_rewrite  # type: ignore\n\n\ndef deserialize(\n    serialized: bytes,\n    text: str,\n) -> Source:\n    match serialized[0]:\n        case 0:\n            text = serialized[1:].decode('utf-8')\n            return Source(text, serialized)\n        case 1:\n            entry = graphql_rewrite.unpack(serialized)\n            assert isinstance(entry, graphql_rewrite.Entry)\n            return NormalizedSource(entry, text, serialized)\n\n    raise ValueError(f\"Invalid type/version byte: {serialized[0]}\")\n\n\nclass Source:\n    def __init__(\n        self,\n        text: str,\n        serialized: bytes,\n    ) -> None:\n        self._cache_key = hashlib.blake2b(serialized).digest()\n        self._text = text\n        self._serialized = serialized\n\n    def text(self) -> str:\n        return self._text\n\n    def cache_key(self) -> bytes:\n        return self._cache_key\n\n    def variables(self) -> dict[str, Any]:\n        return {}\n\n    def substitutions(self) -> dict[str, Any]:\n        return {}\n\n    def tokens(self) -> Optional[list[Any]]:\n        return None\n\n    def first_extra(self) -> Optional[int]:\n        return None\n\n    def extra_counts(self) -> Sequence[int]:\n        return ()\n\n    def extra_blobs(self) -> list[bytes]:\n        return []\n\n    def extra_formatted_as_text(self) -> bool:\n        return False\n\n    def extra_type_oids(self) -> Sequence[int]:\n        return ()\n\n    def serialize(self) -> bytes:\n        return self._serialized\n\n    @staticmethod\n    def from_string(text: str, operation_name: Optional[str]=None) -> Source:\n        return Source(text=text, serialized=b'\\x00' + text.encode('utf-8'))\n\n    def __repr__(self):\n        return f'<edgeql.Source text={self._text!r}>'\n\n\nclass NormalizedSource(Source):\n    def __init__(\n        self,\n        # TODO: type it?\n        normalized: Any,\n        text: str,\n        serialized: bytes,\n    ) -> None:\n        self._text = text\n        self._cache_key = normalized.key.encode('utf-8')  # or hash?\n        self._tokens = normalized.tokens(gql_lexer.TokenKind)\n        self._variables = normalized.variables\n        self._substitutions = normalized.substitutions\n\n        self._first_extra = (\n            normalized.num_variables if normalized.substitutions else None\n        )\n        self._extra_counts = (len(normalized.substitutions),)\n        self._serialized = serialized\n\n    def text(self) -> str:\n        return self._text\n\n    def cache_key(self) -> bytes:\n        return self._cache_key\n\n    def variables(self) -> dict[str, Any]:\n        return self._variables\n\n    def substitutions(self) -> dict[str, Any]:\n        return self._substitutions\n\n    def tokens(self) -> Optional[list[Any]]:\n        return self._tokens\n\n    def first_extra(self) -> Optional[int]:\n        return self._first_extra\n\n    def extra_counts(self) -> Sequence[int]:\n        return self._extra_counts\n\n    def extra_blobs(self) -> list[bytes]:\n        out = b''\n        # Q: Or should we use `variables` instead and reencode it?\n        # (We'd need to use a DecimalEncoder.)\n        # I think the token encodings in substitutions are legit json?\n        # N.B: This relies on the substitutions being in the right order.\n        for v, _, _ in self._substitutions.values():\n            ev = b'\\x01' + v.encode('utf-8')\n            out += struct.pack('!I', len(ev))\n            out += ev\n\n        return [out]\n\n    @staticmethod\n    def from_string(text: str, operation_name: Optional[str]=None) -> Source:\n        rewritten = graphql_rewrite.rewrite(operation_name, text)\n        return NormalizedSource(rewritten, text, rewritten.pack())\n"
  },
  {
    "path": "edb/graphql/translator.py",
    "content": "# mypy: ignore-errors\n\n#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport contextlib\nimport decimal\nimport json\nimport re\nfrom typing import (\n    Any,\n    Optional,\n    Mapping,\n    NamedTuple,\n)\n\nimport graphql\nfrom graphql.language import ast as gql_ast\nfrom graphql.language import lexer as gql_lexer\nfrom graphql import error as gql_error\nfrom graphql import language as gql_lang\n\nfrom edb import errors\n\nfrom edb.common import debug\nfrom edb.common import typeutils\nfrom edb.common.ast import visitor\n\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import codegen as ql_codegen\nfrom edb.edgeql import qltypes\nfrom edb.edgeql import quote as eql_quote\nfrom edb.schema import utils as s_utils\n\nfrom . import types as gt\nfrom . import errors as g_errors\n\n\nARG_TYPES = {\n    'Int': gql_ast.IntValueNode,\n    'String': gql_ast.StringValueNode,\n}\n\nREWRITE_TYPE_ERROR = re.compile(\n    r\"Variable '\\$(?P<var_name>__edb_arg_\\d+)' of type\"\n    r\" '(?P<used>\\w+)!'\"\n    r\" used in position expecting type '(?P<expected>[^']+)'\"\n)\n_STR_TYPES = frozenset((\"ID\", \"ID!\"))\n_INT_TYPES = frozenset((\"Int64\", \"Int64!\", \"Bigint\", \"Decimal\"))\n_INT64_TYPES = frozenset((\"Bigint\", \"Decimal\"))\n_IMPLICIT_CONVERSIONS = {\n    # Used, Expected\n    (\"String\", \"ID\"),\n    (\"String\", \"ID!\"),\n    (\"Int\", \"Int64\"),\n    (\"Int\", \"Int64!\"),\n    (\"Int\", \"Bigint\"),\n    (\"Int\", \"Bigint!\"),\n    (\"Int\", \"Decimal\"),\n    (\"Int\", \"Decimal!\"),\n    (\"Int64\", \"Bigint\"),\n    (\"Int64\", \"Bigint!\"),\n    (\"Int64\", \"Decimal\"),\n    (\"Int64\", \"Decimal!\"),\n    (\"Decimal\", \"Float\"),\n    (\"Decimal\", \"Float!\"),\n}\nINT_FLOAT_ERROR = re.compile(\n    r\"Variable '\\$[^']+' of type 'Int!?'\"\n    r\" used in position expecting type 'Float!?'\"\n)\n\n\nclass GraphQLTranslatorContext:\n    def __init__(\n        self,\n        *,\n        gqlcore: gt.GQLCoreSchema,\n        variables,\n        query,\n        document_ast,\n        operation_name,\n        native_input,\n        parse_only_mode,\n    ):\n        self.variables = variables\n        self.fragments = {}\n        self.validated_fragments = {}\n        self.vars = {}\n        self.fields = []\n        self.path = []\n        self.filter = None\n        self.include_base = []\n        self.gqlcore = gqlcore\n        self.query = query\n        self.document_ast = document_ast\n        self.operation_name = operation_name\n        self.native_input = native_input\n        self.parse_only_mode = parse_only_mode\n\n        # only used inside ObjectFieldNode\n        self.base_expr = None\n        self.right_cast = None\n\n        # auto-incrementing counter\n        self._counter = 0\n\n    @property\n    def counter(self):\n        val = self._counter\n        self._counter += 1\n        return val\n\n\nclass Step(NamedTuple):\n    name: Any\n    type: Any\n    eql_alias: str\n\n\nclass Field(NamedTuple):\n    name: Any\n    value: Any\n\n\nclass Var(NamedTuple):\n    val: Any\n    defn: gql_ast.VariableDefinitionNode\n    critical: bool\n\n\nclass Operation(NamedTuple):\n    name: Any\n    stmt: Any\n    critvars: dict[str, Any]\n    vars: dict[str, Any]\n\n\nclass TranspiledOperation(NamedTuple):\n\n    edgeql_ast: qlast.Base\n    cache_deps_vars: Optional[frozenset[str]]\n    variables_desc: dict\n\n\nclass Ordering(NamedTuple):\n\n    names: list[str]\n    direction: qlast.SortOrder\n    nulls: qlast.NonesOrder\n\n\nclass BookkeepDict(dict):\n\n    def __init__(self, values):\n        self.update(values)\n        self.touched = set()\n\n    def __bool__(self):\n        # HACK! And kind of wrong!\n        # But this keeps this from getting replaced by code in graphql-core\n        # that does \"raw_variable_values or {}\"...\n        return True\n\n    def __getitem__(self, key):\n        self.touched.add(key)\n        return super().__getitem__(key)\n\n    def __contains__(self, key):\n        self.touched.add(key)\n        return super().__contains__(key)\n\n    def keys(self):\n        raise NotImplementedError()\n\n    def values(self):\n        raise NotImplementedError()\n\n    def items(self):\n        raise NotImplementedError()\n\n\nclass GraphQLTranslator:\n\n    def __init__(self, *, context=None):\n        self._context = context\n\n    def node_visit(self, node):\n        for cls in node.__class__.__mro__:\n            method = 'visit_' + cls.__name__\n            visitor = getattr(self, method, None)\n            if visitor is not None:\n                break\n        if visitor is None:\n            raise AssertionError(f\"Unexpected node {node.__class__}\")\n        result = visitor(node)\n        return result\n\n    def visit(self, node):\n        if typeutils.is_container(node):\n            return [self.node_visit(n) for n in node]\n        else:\n            return self.node_visit(node)\n\n    def get_loc(self, node):\n        if node.loc:\n            token = node.loc.start_token\n            return token.line, token.column\n        else:\n            return None\n\n    def get_type(self, name):\n        # the type may be from the EdgeDB schema or some special\n        # GraphQL type/adapter\n        assert isinstance(name, str)\n        return self._context.gqlcore.get(name)\n\n    def is_list_type(self, node):\n        return isinstance(node, gql_ast.ListTypeNode) or (\n            isinstance(node, gql_ast.NonNullTypeNode)\n            and self.is_list_type(node.type)\n        )\n\n    def get_field_type(self, base, name, *, args=None):\n        return base.get_field_type(name)\n\n    def get_optname(self, node):\n        if node.name:\n            return node.name.value\n        else:\n            return None\n\n    def visit_DocumentNode(self, node):\n        # we need to index all of the fragments before we process operations\n        if node.definitions:\n            self._context.fragments = {\n                f.name.value: f for f in node.definitions\n                if isinstance(f, gql_ast.FragmentDefinitionNode)\n            }\n        else:\n            self._context.fragments = {}\n\n        operation_name = self._context.operation_name\n        if operation_name is None:\n            opnames = []\n            for opnode in node.definitions:\n                if not isinstance(opnode, gql_ast.OperationDefinitionNode):\n                    continue\n                opname = None\n                if opnode.name:\n                    opname = opnode.name.value\n                opnames.append(opname)\n            if len(opnames) > 1:\n                raise errors.QueryError(\n                    'must provide operation name if query contains '\n                    'multiple operations')\n            operation_name = self._context.operation_name = opnames[0]\n\n        if node.definitions:\n            translated = {d.name: d\n                          for d in self.visit(node.definitions)\n                          if d is not None}\n        else:\n            translated = {}\n\n        if operation_name not in translated:\n            if operation_name:\n                raise errors.QueryError(\n                    f'unknown operation named \"{operation_name}\"')\n\n        operation = translated[operation_name]\n        for el in operation.stmt.result.elements:\n            # swap in the json bits\n            if (isinstance(el.compexpr, qlast.FunctionCall) and\n                    el.compexpr.func == 'to_json'):\n\n                # An introspection query; let graphql evaluate it for us.\n\n                vars = BookkeepDict(self._context.variables)\n                result = graphql.execute(\n                    self._context.gqlcore.graphql_schema,\n                    self._context.document_ast,\n                    operation_name=operation_name,\n                    variable_values=vars)\n                for var_name in vars.touched:\n                    var = self._context.vars.get(var_name)\n                    # TODO: Why do we track this twice?\n                    self._context.vars[var_name] = var._replace(critical=True)\n                    operation.critvars[var_name] = (\n                        self._context.vars[var_name].val\n                    )\n\n                if result.errors:\n                    err = result.errors[0]\n\n                    if (\n                        self._context.parse_only_mode\n                        and all(\n                            'was not provided' in str(e) for e in result.errors\n                        )\n                    ):\n                        # Don't worry about it.\n                        # And explain why!\n                        assert vars.touched\n                    elif isinstance(err, graphql.GraphQLError):\n                        err_loc = (err.locations[0].line,\n                                   err.locations[0].column)\n                        raise g_errors.GraphQLCoreError(\n                            err.message,\n                            loc=err_loc)\n                    else:\n                        raise err\n\n                    expr = qlast.FunctionCall(\n                        func='assert_exists',\n                        args=[\n                            qlast.TypeCast(\n                                type=qlast.TypeName(\n                                    maintype=qlast.ObjectRef(name='str')\n                                ),\n                                expr=qlast.Set(elements=[]),\n                            ),\n                        ],\n                        kwargs=dict(message=qlast.Constant.string(\n                            \"SERVER BUG: error with graphql introspection\"\n                        )),\n                    )\n\n                else:\n                    name = el.expr.steps[0].name\n                    expr = qlast.Constant.string(json.dumps(result.data[name]))\n\n                el.compexpr.args[0] = expr\n\n        return translated\n\n    def visit_FragmentDefinitionNode(self, node):\n        # fragments are already processed, no need to do anything here\n        return None\n\n    def visit_OperationDefinitionNode(self, node):\n        # create a dict of variables that will be marked as\n        # critical or not\n        self._context.vars = {\n            name: Var(val=val, defn=None, critical=False)\n            for name, val in self._context.variables.items()}\n        self._context.include_base.append(False)\n\n        opname = None\n        if node.name:\n            opname = node.name.value\n\n        if opname != self._context.operation_name:\n            self._context.include_base.pop()\n            return None\n\n        if (node.operation is None or\n                node.operation == graphql.OperationType.QUERY):\n            stmt = self._visit_query(node)\n        elif (node.operation is None or\n                node.operation == graphql.OperationType.MUTATION):\n            stmt = self._visit_mutation(node)\n        else:\n            raise ValueError(f'unsupported operation: {node.operation!r}')\n\n        # produce the list of variables critical to the shape\n        # of the query\n        critvars = {name: var.val for name, var\n                    in self._context.vars.items() if var.critical}\n        # variables that were defined in this operation\n        defvars = {name: var.val for name, var in self._context.vars.items()\n                   if var.defn is not None}\n\n        self._context.include_base.pop()\n\n        return Operation(\n            name=opname,\n            stmt=stmt,\n            critvars=critvars,\n            vars=defvars,\n        )\n\n    def _visit_query(self, node):\n        # populate input variables with defaults, where applicable\n        if node.variable_definitions:\n            self.visit(node.variable_definitions)\n\n        # base Query needs to be configured specially\n        base = self._context.gqlcore.get('__graphql__::Query')\n\n        # special treatment of the selection_set, different from inner\n        # recursion\n        query = qlast.SelectQuery(\n            result=qlast.Shape(\n                elements=[]\n            )\n        )\n\n        self._context.fields.append({})\n        self._context.path.append([Step(None, base, None)])\n        query.result.elements = self.visit(node.selection_set)\n        self._context.fields.pop()\n        self._context.path.pop()\n\n        return query\n\n    def _visit_mutation(self, node):\n        # populate input variables with defaults, where applicable\n        if node.variable_definitions:\n            self.visit(node.variable_definitions)\n\n        # base Mutation needs to be configured specially\n        base = self._context.gqlcore.get('__graphql__::Mutation')\n\n        # special treatment of the selection_set, different from inner\n        # recursion\n        query = qlast.SelectQuery(\n            result=qlast.Shape(\n                elements=[]\n            )\n        )\n\n        self._context.fields.append({})\n        self._context.path.append([Step(None, base, None)])\n        query.result.elements = self.visit(node.selection_set)\n        self._context.fields.pop()\n        self._context.path.pop()\n\n        return query\n\n    def _should_include(self, directives):\n        # First mark *everything* as critical\n        for directive in directives:\n            if directive.name.value in ('include', 'skip'):\n                cond = [a.value for a in directive.arguments\n                        if a.name.value == 'if'][0]\n\n                if isinstance(cond, gql_ast.VariableNode):\n                    varname = cond.name.value\n                    var = self._context.vars[varname]\n                    self._context.vars[varname] = var._replace(critical=True)\n\n        # In parse_only_mode we are done\n        if self._context.parse_only_mode:\n            return True\n\n        # Otherwise actually evaluate it\n        for directive in directives:\n            if directive.name.value in ('include', 'skip'):\n                cond = [a.value for a in directive.arguments\n                        if a.name.value == 'if'][0]\n\n                if isinstance(cond, gql_ast.VariableNode):\n                    varname = cond.name.value\n                    var = self._context.vars[varname]\n                    value = var.val\n\n                    if value is None:\n                        raise g_errors.GraphQLValidationError(\n                            f\"no value for the {varname!r} variable\",\n                            loc=self.get_loc(directive.name))\n                elif isinstance(cond, gql_ast.BooleanValueNode):\n                    value = cond.value\n\n                if not isinstance(value, bool):\n                    raise g_errors.GraphQLValidationError(\n                        f\"'if' argument of {directive.name.value} \" +\n                        \"directive must be a Boolean\",\n                        loc=self.get_loc(directive.name))\n\n                if directive.name.value == 'include' and not value:\n                    return False\n                elif directive.name.value == 'skip' and value:\n                    return False\n\n        return True\n\n    def visit_VariableDefinitionNode(self, node):\n        varname = node.variable.name.value\n        variables = self._context.vars\n        var = variables.get(varname)\n        if not var:\n            if node.default_value is None:\n                variables[varname] = Var(\n                    val=None, defn=node, critical=False)\n            else:\n                val = convert_default(node.default_value, varname)\n                variables[varname] = Var(val=val, defn=node, critical=False)\n\n                # In HTTP mode, we rely on merging the dict of defaults with\n                # the dict of actual arguments.\n                #\n                # FIXME: This is actually a little dodgy, since the gel proto\n                # will require passing an explicit None, but in graphql that\n                # should result in null being passed and the default not\n                # used... But in our implementation, that fails, because\n                # the rewriter (I think?) is marking it required?\n                #\n                # Alright, actually for now we are rejecting it.\n                if self._context.native_input:\n                    raise errors.UnsupportedFeatureError(\n                        'Default variables are not supported on the '\n                        'gel protocol'\n                    )\n\n        else:\n            # we have the variable, but we still need to update the defn field\n            variables[varname] = Var(\n                val=var.val, defn=node, critical=var.critical)\n\n    def visit_SelectionSetNode(self, node):\n        elements = []\n\n        for sel in node.selections:\n            spec = self.visit(sel)\n            if not self._should_include(sel.directives):\n                continue\n\n            if spec is not None:\n                elements.append(spec)\n\n        elements = self.combine_field_results(elements)\n\n        return elements\n\n    def _is_duplicate_field(self, node):\n        # if this field is a duplicate, that is not identical to the\n        # original, throw an exception\n        name = (node.alias or node.name).value\n        dup = self._context.fields[-1].get(name)\n        if dup:\n            return True\n        else:\n            self._context.fields[-1][name] = node\n\n        return False\n\n    # XXX: this might need to be trimmed\n    def _is_top_level_field(self, node, fail=None):\n        top = False\n\n        path = self._context.path[-1]\n        # there is different handling of top-level, built-in and inner\n        # fields\n        top = (len(self._context.path) == 1 and\n               len(path) == 1 and\n               path[0].name is None)\n\n        prevt = path[-1].type\n        target = self.get_field_type(\n            prevt, node.name.value)\n        path.append(Step(name=node.name.value, type=target, eql_alias=None))\n\n        if not top and fail:\n            raise g_errors.GraphQLValidationError(\n                f\"field {node.name.value!r} can only appear \"\n                f\"at the top-level Query\",\n                loc=self.get_loc(node))\n\n        return top\n\n    def _maybe_get_current_type(self):\n        if self._context.path:\n            path = self._context.path[-1]\n            return path[-1].type\n        else:\n            return None\n\n    def _get_parent_and_current_type(self):\n        path = self._context.path[-1]\n        cur = path[-1].type\n        if len(path) > 1:\n            par = path[-2].type\n        else:\n            par = self._context.path[-2][-1].type\n\n        return par, cur\n\n    def _prepare_field(self, node):\n        path = self._context.path[-1]\n        include_base = self._context.include_base[-1]\n\n        is_top = self._is_top_level_field(node)\n\n        spath = self._context.path[-1]\n        prevt, target = self._get_parent_and_current_type()\n\n        # insert normal or specialized link\n        steps = []\n        if include_base:\n            base = spath[0].type\n            steps.append(qlast.TypeIntersection(\n                type=qlast.TypeName(\n                    maintype=base.edb_base_name_ast\n                )\n            ))\n        steps.append(qlast.Ptr(name=node.name.value))\n\n        return is_top, path, prevt, target, steps\n\n    def visit_FieldNode(self, node):\n        if self._is_duplicate_field(node):\n            return\n\n        _is_top, _path, prevt, target, steps = \\\n            self._prepare_field(node)\n\n        json_mode = False\n        is_shadowed = prevt.is_field_shadowed(node.name.value)\n\n        # determine if there needs to be extra subqueries\n        if not prevt.dummy and target.dummy:\n            json_mode = True\n\n            # this is a special introspection type\n            eql, shape, filterable = target.get_template()\n\n            spec = qlast.ShapeElement(\n                expr=qlast.Path(\n                    steps=[qlast.Ptr(\n                        name=(node.alias or node.name).value,\n                    )]\n                ),\n                compexpr=eql,\n            )\n\n        elif is_shadowed and not node.alias:\n            # shadowed field that doesn't need an alias\n            spec = filterable = shape = qlast.ShapeElement(\n                expr=qlast.Path(steps=steps),\n            )\n\n        elif not node.selection_set or is_shadowed and node.alias:\n            # this is either an unshadowed terminal field or an aliased\n            # shadowed field\n            prefix = qlast.Path(steps=self.get_path_prefix(-1))\n            eql, shape, filterable = prevt.get_field_template(\n                node.name.value,\n                parent=prefix,\n                has_shape=bool(node.selection_set)\n            )\n            spec = qlast.ShapeElement(\n                expr=qlast.Path(\n                    steps=[qlast.Ptr(\n                        # this is already a sub-query\n                        name=(node.alias or node.name).value\n                    )]\n                ),\n                compexpr=eql,\n                # preserve the original cardinality of the computable\n                # aliased fields\n                cardinality=prevt.get_field_cardinality(node.name.value),\n            )\n\n        else:\n            # if the parent is NOT a shadowed type, we need an explicit SELECT\n            eql, shape, filterable = target.get_template()\n            spec = qlast.ShapeElement(\n                expr=qlast.Path(\n                    steps=[qlast.Ptr(\n                        # this is already a sub-query\n                        name=(node.alias or node.name).value\n                    )]\n                ),\n                compexpr=eql,\n                # preserve the original cardinality of the computable,\n                # which is basically one of the top-level query\n                # fields, all of which are returning lists\n                cardinality=qltypes.SchemaCardinality.Many,\n            )\n\n        self._context.include_base.append(False)\n        # INSERT mutations have different arguments from queries\n        if not is_shadowed and node.name.value.startswith('insert_'):\n            # a single recursion target, so we can process\n            # selection set now\n            with self._update_path_for_eql_alias():\n                alias = self._context.path[-1][-1].eql_alias\n                self._context.fields.append({})\n                shape.elements = self.visit(node.selection_set)\n                insert_shapes = self._visit_insert_arguments(node.arguments)\n                if not insert_shapes:\n                    # No insert arguments, nmeaning that a single object must\n                    # be inserted without any shape.\n                    insert_shapes = [None]\n                self._context.fields.pop()\n\n            filterable.aliases = [\n                qlast.AliasedExpr(\n                    alias=alias,\n                    expr=qlast.Set(elements=[\n                        qlast.InsertQuery(\n                            subject=shape.expr,\n                            shape=sh,\n                        ) for sh in insert_shapes\n                    ])\n                )\n            ]\n            filterable.result.expr = qlast.Path(\n                steps=[qlast.ObjectRef(name=alias)])\n\n        elif node.selection_set is not None:\n            delete_mode = (not is_shadowed and\n                           node.name.value.startswith('delete_'))\n            update_mode = (not is_shadowed and\n                           node.name.value.startswith('update_'))\n\n            if not json_mode:\n                # a single recursion target, so we can process\n                # selection set now\n                with self._update_path_for_eql_alias(\n                        delete_mode or update_mode):\n                    # set up a unique alias for the deleted object\n                    alias = self._context.path[-1][-1].eql_alias\n                    self._context.fields.append({})\n                    vals = self.visit(node.selection_set)\n                    self._context.fields.pop()\n\n                if shape:\n                    shape.elements = vals\n                if filterable:\n                    where, orderby, offset, limit = \\\n                        self._visit_query_arguments(node.arguments)\n\n                    filterable.where = where\n                    filterable.orderby = orderby\n                    filterable.offset = offset\n                    filterable.limit = limit\n\n            if delete_mode:\n                # this should be a DELETE operation, so we'll rearrange the\n                # components of the SelectQuery\n                filterable.aliases = [\n                    qlast.AliasedExpr(\n                        alias=alias,\n                        expr=qlast.DeleteQuery(\n                            subject=filterable.result.expr,\n                            where=filterable.where,\n                        )\n                    )\n                ]\n                filterable.where = None\n                filterable.result.expr = qlast.Path(\n                    steps=[qlast.ObjectRef(name=alias)])\n            elif update_mode:\n                update_shape = self._visit_update_arguments(node.arguments)\n                # this should be an UPDATE operation, so we'll rearrange the\n                # components of the SelectQuery and add data operations\n                filterable.aliases = [\n                    qlast.AliasedExpr(\n                        alias=alias,\n                        expr=qlast.UpdateQuery(\n                            subject=filterable.result.expr,\n                            where=filterable.where,\n                            shape=update_shape,\n                        )\n                    )\n                ]\n                filterable.where = None\n                filterable.result.expr = qlast.Path(\n                    steps=[qlast.ObjectRef(name=alias)])\n\n        # Remove the processed path.\n        self._context.path[-1].pop()\n        if len(self._context.path[-1]) == 0:\n            # If this was the last shape field, remove the now empty\n            # shell for the shape paths.\n            self._context.path.pop()\n\n        self._context.include_base.pop()\n        return spec\n\n    def visit_InlineFragmentNode(self, node):\n        self._validate_fragment_type(node, node)\n        result = self.visit(node.selection_set)\n        if node.type_condition is not None:\n            self._context.path.pop()\n            self._context.include_base.pop()\n\n        return result\n\n    def visit_FragmentSpreadNode(self, node):\n        frag = self._context.fragments[node.name.value]\n        self._validate_fragment_type(frag, node)\n        # in case of secondary type, recurse into a copy to avoid\n        # memoized results\n        selection_set = frag.selection_set\n\n        result = self.visit(selection_set)\n        self._context.path.pop()\n\n        if frag.type_condition is not None:\n            self._context.include_base.pop()\n\n        return result\n\n    def _validate_fragment_type(self, frag, spread):\n        is_specialized = False\n        base_type = None\n\n        # validate the fragment type w.r.t. the base\n        if frag.type_condition is None:\n            return\n\n        # validate the base if it's nested\n        if len(self._context.path) > 0:\n            path = self._context.path[-1]\n            base_type = path[-1].type\n            frag_type = self.get_type(frag.type_condition.name.value)\n\n            if base_type.issubclass(frag_type):\n                # legal hierarchy, no change\n                pass\n            elif frag_type.issubclass(base_type):\n                # specialized link, but still legal\n                is_specialized = True\n            else:\n                raise g_errors.GraphQLValidationError(\n                    f\"{base_type.short_name} and {frag_type.short_name} \" +\n                    \"are not related\",\n                    loc=self.get_loc(frag))\n\n        self._context.path.append([\n            Step(name=frag.type_condition, type=frag_type, eql_alias=None)])\n        self._context.include_base.append(is_specialized)\n\n    def _visit_query_arguments(self, arguments):\n        where = None\n        orderby = []\n        first = last = before = after = None\n\n        for arg in arguments:\n            if arg.name.value == 'filter':\n                where = self.visit(arg.value)\n            elif arg.name.value == 'order':\n                orderby = self.visit_order(arg.value)\n            elif arg.name.value == 'first':\n                first = self._visit_pagination_arg(\n                    arg, 'Int',\n                    expected='an int')\n            elif arg.name.value == 'last':\n                last = self._visit_pagination_arg(\n                    arg, 'Int',\n                    expected='an int')\n            elif arg.name.value == 'before':\n                before = self._visit_pagination_arg(\n                    arg, 'String',\n                    expected='a string castable to an int')\n            elif arg.name.value == 'after':\n                after = self._visit_pagination_arg(\n                    arg, 'String',\n                    expected='a string castable to an int')\n\n        # convert before, after, first and last into offset and limit\n        offset, limit = self.get_offset_limit(after, before, first, last)\n        # FIXME: it may be a good idea to create special scalar\n        # (positive integer) so that the values used for offset and\n        # limit can be cast into it and appropriate errors will be\n        # produced.\n\n        return where, orderby, offset, limit\n\n    def _visit_pagination_arg(self, node, argtype, expected):\n        if isinstance(node.value, gql_ast.VariableNode):\n            # variables will be type-checked by this point, so assume\n            # the type is valid\n            return self.visit(node.value)\n\n        elif not isinstance(node.value, ARG_TYPES[argtype]):\n            raise g_errors.GraphQLValidationError(\n                f\"invalid value for {node.name.value!r}: \"\n                f\"expected {expected}\",\n                loc=self.get_loc(node.value)) from None\n\n        try:\n            return int(node.value.value)\n        except (TypeError, ValueError):\n            raise g_errors.GraphQLValidationError(\n                f\"invalid value for {node.name.value!r}: \"\n                f\"expected {expected}, \"\n                f\"got {node.value.value!r}\",\n                loc=self.get_loc(node.value)) from None\n\n    def get_offset_limit(self, after, before, first, last):\n        # if all the parameters here are constants we can compute and\n        # compile shorter and simpler OFFSET/LIMIT values\n        if any(isinstance(x, qlast.Base)\n               for x in [after, before, first, last] if x is not None):\n            return self._get_general_offset_limit(after, before, first, last)\n        else:\n            return self._get_static_offset_limit(after, before, first, last)\n\n    def _get_static_offset_limit(self, after, before, first, last):\n        if after is not None:\n            # The +1 is to make 'after' into an appropriate index.\n            #\n            # 0--a--1--b--2--c--3-- ... we call element at\n            # index 0 (or \"element 0\" for short), the element\n            # immediately after the mark 0. So after \"element\n            # 0\" really means after \"index 1\".\n            after += 1\n\n        offset = limit = None\n        # convert before, after, first and last into offset and limit\n        if after is not None:\n            offset = after\n        if before is not None:\n            limit = before - (after or 0)\n        if first is not None:\n            if limit is None:\n                limit = first\n            else:\n                limit = min(first, limit)\n        if last is not None:\n            if limit is not None:\n                if last < limit:\n                    offset = (offset or 0) + limit - last\n                    limit = last\n            else:\n                # FIXME: there wasn't any limit, so we can define last\n                # in terms of offset alone without negative OFFSET\n                # implementation\n                raise g_errors.GraphQLTranslationError(\n                    f'last={last} translates to a negative OFFSET in '\n                    f'EdgeQL which is currently unsupported')\n\n        # convert integers into qlast literals\n        if offset is not None and not isinstance(offset, qlast.Base):\n            offset = qlast.Constant.integer(max(0, offset))\n        if limit is not None:\n            limit = qlast.Constant.integer(max(0, limit))\n\n        return offset, limit\n\n    def _get_int64_slice_value(self, value):\n        if value is None:\n            return None\n        if isinstance(value, qlast.Base):\n            return qlast.TypeCast(\n                type=qlast.TypeName(\n                    maintype=qlast.ObjectRef(name='int64')),\n                expr=value\n            )\n        else:\n            return qlast.Constant.integer(value)\n\n    def _get_general_offset_limit(self, after, before, first, last):\n        # Convert any static values to corresponding qlast and\n        # normalize them as int64.\n        after = self._get_int64_slice_value(after)\n        before = self._get_int64_slice_value(before)\n        first = self._get_int64_slice_value(first)\n        last = self._get_int64_slice_value(last)\n\n        offset = limit = None\n        # convert before, after, first and last into offset and limit\n        if after is not None:\n            # The +1 is to make 'after' into an appropriate index.\n            #\n            # 0--a--1--b--2--c--3-- ... we call element at\n            # index 0 (or \"element 0\" for short), the element\n            # immediately after the mark 0. So after \"element\n            # 0\" really means after \"index 1\".\n            offset = qlast.BinOp(\n                left=after,\n                op='+',\n                right=qlast.Constant.integer('1')\n            )\n\n        if before is not None:\n            # limit = before - (after or 0)\n            if after:\n                limit = qlast.BinOp(\n                    left=before,\n                    op='-',\n                    right=offset,\n                )\n            else:\n                limit = before\n\n        if first is not None:\n            if limit is None:\n                limit = first\n            else:\n                limit = qlast.IfElse(\n                    if_expr=first,\n                    condition=qlast.BinOp(\n                        left=first,\n                        op='<',\n                        right=limit\n                    ),\n                    else_expr=limit\n                )\n\n        if last is not None:\n            if limit is not None:\n                if offset:\n                    offset = qlast.BinOp(\n                        left=offset,\n                        op='+',\n                        right=qlast.BinOp(\n                            left=limit,\n                            op='-',\n                            right=last\n                        )\n                    )\n                else:\n                    offset = qlast.BinOp(\n                        left=limit,\n                        op='-',\n                        right=last\n                    )\n\n                limit = qlast.IfElse(\n                    if_expr=last,\n                    condition=qlast.BinOp(\n                        left=last,\n                        op='<',\n                        right=limit\n                    ),\n                    else_expr=limit\n                )\n\n            else:\n                # FIXME: there wasn't any limit, so we can define last\n                # in terms of offset alone without negative OFFSET\n                # implementation\n                raise g_errors.GraphQLTranslationError(\n                    f'last translates to a negative OFFSET in '\n                    f'EdgeQL which is currently unsupported')\n\n        return offset, limit\n\n    @contextlib.contextmanager\n    def _update_path_for_eql_alias(self, alias_needed=True):\n        if alias_needed:\n            # we need to update the path of the delete field to keep track\n            # of the delete alias\n            alias = f'x{self._context.counter}'\n\n            # just replace the last path element with the same\n            # element, but aliased\n            step = self._context.path[-1].pop()\n            self._context.path.append([\n                Step(name=step.name, type=step.type, eql_alias=alias)])\n        yield\n        # replace it back\n        if alias_needed:\n            self._context.path[-1].pop()\n            self._context.path[-1].append(step)\n\n    def _visit_update_arguments(self, arguments):\n        result = []\n\n        for arg in arguments:\n            if arg.name.value == 'data':\n                # the node is an ObjectNode with the update spec\n                for field in arg.value.fields:\n                    fname = field.name.value\n                    # capture the full path to the field being updated\n                    eqlpath = self.get_path_prefix()\n                    eqlpath.append(qlast.Ptr(name=fname))\n                    eqlpath = qlast.Path(steps=eqlpath)\n\n                    # set-up the current path to point to the thing\n                    # being updated (so that SELECT can be applied if needed)\n                    with self._update_path_for_insert_field(field):\n                        _, target = self._get_parent_and_current_type()\n                        res = self._visit_update_op(\n                            field.value, eqlpath, target)\n                        if res is None:\n                            continue\n                        shapeop, value = res\n\n                        result.append(\n                            qlast.ShapeElement(\n                                expr=qlast.Path(\n                                    steps=[qlast.Ptr(name=field.name.value)]\n                                ),\n                                operation=qlast.ShapeOperation(op=shapeop),\n                                compexpr=value,\n                            )\n                        )\n\n        return result\n\n    def _visit_update_op(self, node, eqlpath, ftype):\n        # The node is an ObjectNode with the update spec. The fields represent\n        # different oprations that can be performend. Although the spec lists\n        # multiple options exactly one of the options should be present.\n\n        if not node.fields:\n            raise g_errors.GraphQLValidationError(\n                \"No update operation was specified.\",\n                loc=self.get_loc(node))\n\n        if len(node.fields) > 1:\n            raise g_errors.GraphQLValidationError(\n                \"Too many update operations were specified.\",\n                loc=self.get_loc(node))\n\n        field = node.fields[0]\n        fname = field.name.value\n        # by default we expect an assign\n        shapeop = qlast.ShapeOp.ASSIGN\n        ptrname = eqlpath.steps[-1].name\n\n        # NOTE: there will be more operations in the future\n        if fname == 'set':\n            value = self._get_input_expr_for_pointer_mutation(field, ptrname)\n            return shapeop, value\n\n        elif fname == 'clear':\n            cond = field.value\n            if isinstance(cond, gql_ast.VariableNode):\n                var_name = cond.name.value\n                var = self._context.vars[var_name]\n                if not var.critical:\n                    self._context.vars[var_name] = \\\n                        var._replace(critical=True)\n                value = var.val\n            elif isinstance(cond, gql_ast.BooleanValueNode):\n                value = cond.value\n            elif isinstance(cond, gql_ast.NullValueNode):\n                value = None\n            else:\n                # We assume that schema was validated,\n                # so variable is of correct type\n                raise AssertionError(f\"Unexpected node {cond!r}\")\n\n            if value:\n                # empty set to clear the value\n                return shapeop, qlast.Set(elements=[])\n\n        elif fname == 'increment':\n            value = qlast.BinOp(\n                left=eqlpath,\n                op='+',\n                right=self._visit_insert_value(field.value)\n            )\n            return shapeop, value\n\n        elif fname == 'decrement':\n            value = qlast.BinOp(\n                left=eqlpath,\n                op='-',\n                right=self._visit_insert_value(field.value)\n            )\n            return shapeop, value\n\n        elif fname == 'prepend':\n            value = qlast.BinOp(\n                left=self._visit_insert_value(field.value),\n                op='++',\n                right=eqlpath\n            )\n            return shapeop, value\n\n        elif fname == 'append':\n            value = qlast.BinOp(\n                left=eqlpath,\n                op='++',\n                right=self._visit_insert_value(field.value)\n            )\n            return shapeop, value\n\n        elif fname == 'slice':\n            args = field.value.values\n            num_args = len(args)\n            if num_args == 1:\n                start = self.visit(args[0])\n                stop = None\n            elif num_args == 2:\n                start = self.visit(args[0])\n                stop = self.visit(args[1])\n            else:\n                raise g_errors.GraphQLTranslationError(\n                    f'\"slice\" must be a list of 1 or 2 integers')\n\n            value = qlast.Indirection(\n                arg=eqlpath,\n                indirection=[qlast.Slice(\n                    start=start,\n                    stop=stop\n                )]\n            )\n            return shapeop, value\n\n        elif fname == 'add':\n            # This is a set, so no reason to validate cardinality.\n            value = self._get_input_expr_for_pointer_mutation(\n                field, ptrname, validate_cardinality=False)\n            shapeop = qlast.ShapeOp.APPEND\n            return shapeop, value\n        elif fname == 'remove':\n            # This is a set, so no reason to validate cardinality.\n            value = self._get_input_expr_for_pointer_mutation(\n                field, ptrname, validate_cardinality=False)\n            shapeop = qlast.ShapeOp.SUBTRACT\n            return shapeop, value\n\n    def _visit_insert_arguments(self, arguments):\n        input_data = []\n\n        for arg in arguments:\n            if arg.name.value == 'data':\n                # normalize the value to a list\n                if isinstance(arg.value, gql_ast.ListValueNode):\n                    input_data = arg.value.values\n                else:\n                    input_data = [arg.value]\n\n        return [self._get_shape_from_input_data(node) for node in input_data]\n\n    def _get_shape_from_input_data(self, node):\n        # the node is an ObjectNode with the input spec\n        result = []\n        for field in node.fields:\n            # set-up the current path to point to the thing being inserted\n            with self._update_path_for_insert_field(field):\n                compexpr = self._get_input_expr_for_pointer_mutation(\n                    field, field.name.value)\n                result.append(\n                    qlast.ShapeElement(\n                        expr=qlast.Path(\n                            steps=[qlast.Ptr(name=field.name.value)]\n                        ),\n                        compexpr=compexpr,\n                    )\n                )\n\n        return result\n\n    def _get_input_expr_for_pointer_mutation(\n        self,\n        field,\n        fname,\n        validate_cardinality=True,\n    ):\n        compexpr = self._visit_insert_value(field.value)\n\n        # get the type of the value being inserted\n        ptype, target = self._get_parent_and_current_type()\n\n        # Object types in mutations potentially need some extra assertions\n        # to validate them.\n        if target.is_object_type:\n            if validate_cardinality:\n                card = ptype.get_field_cardinality(fname)\n                if card is qltypes.SchemaCardinality.Many:\n                    # Need to wrap the set into an \"assert_distinct()\".\n                    msg = f'objects provided for {fname!r} are not distinct'\n                    compexpr = qlast.FunctionCall(\n                        func='assert_distinct',\n                        args=[compexpr],\n                        kwargs={\n                            'message': qlast.Constant.string(msg)\n                        }\n                    )\n                else:\n                    # Singleton object values need to be verified.\n                    msg = f'more than one object provided for {fname!r}'\n                    compexpr = qlast.FunctionCall(\n                        func='assert_single',\n                        args=[compexpr],\n                        kwargs={\n                            'message': qlast.Constant.string(msg)\n                        }\n                    )\n\n            # Object types need to be wrapped in a DETACHED in\n            # mutations to avoid referencing the root object.\n            compexpr = qlast.DetachedExpr(expr=compexpr)\n\n        return compexpr\n\n    @contextlib.contextmanager\n    def _update_path_for_insert_field(self, node):\n        # we need to update the path of the insert field to keep track\n        # of the insert types\n        path = self._context.path[-1]\n\n        prevt = path[-1].type\n        target = self.get_field_type(\n            prevt, node.name.value)\n\n        self._context.path.append([\n            Step(name=None, type=target, eql_alias=None)])\n\n        yield\n        self._context.path.pop()\n\n    def _visit_range_spec(self, node, target):\n        assert isinstance(node, gql_ast.ObjectValueNode)\n        assert target.is_range or target.is_multirange\n\n        # This is a range spec\n        subtype = target.edb_base.get_subtypes(target.edb_schema)[0]\n        st_name = subtype.get_name(target.edb_schema)\n        kwargs = {\n            rf.name.value: self.visit(rf.value) for rf in node.fields\n            if not isinstance(rf.value, gql_ast.NullValueNode)\n        }\n        # move some kwargs into args\n        args = [\n            qlast.TypeCast(\n                expr=kwargs.pop('lower', qlast.Set(elements=[])),\n                type=qlast.TypeName(\n                    maintype=qlast.ObjectRef(name=str(st_name)),\n                ),\n            ),\n            qlast.TypeCast(\n                expr=kwargs.pop('upper', qlast.Set(elements=[])),\n                type=qlast.TypeName(\n                    maintype=qlast.ObjectRef(name=str(st_name)),\n                ),\n            ),\n        ]\n\n        return qlast.FunctionCall(\n            func='range',\n            args=args,\n            kwargs=kwargs,\n        )\n\n    def _visit_insert_value(self, node):\n        # get the type of the value being inserted\n        _, target = self._get_parent_and_current_type()\n        if isinstance(node, gql_ast.ObjectValueNode):\n            if target.is_range or target.is_multirange:\n                # This is a range spec\n                return self._visit_range_spec(node, target)\n\n            # get a template AST\n            eql, shape, filterable = target.get_template()\n\n            if node.fields[0].name.value == 'data':\n                # this may be a new object spec\n                data_node = node.fields[0].value\n\n                return qlast.InsertQuery(\n                    subject=shape.expr,\n                    shape=self._get_shape_from_input_data(data_node),\n                )\n            else:\n                eql.result = shape.expr\n                # this is a filter spec\n                where, orderby, offset, limit = \\\n                    self._visit_query_arguments(node.fields)\n                filterable.where = where\n                filterable.orderby = orderby\n                filterable.offset = offset\n                filterable.limit = limit\n\n                return eql\n\n        elif isinstance(node, gql_ast.ListValueNode) and target.is_multirange:\n            # Multiranges are composed of a list of ranges. So we just need to\n            # wrap the literal array into a range function call.\n            return qlast.FunctionCall(\n                func='multirange',\n                args=[\n                    qlast.Array(\n                        elements=[\n                            self._visit_insert_value(el) for el in node.values\n                        ]\n                    ),\n                ],\n            )\n\n        elif isinstance(node, gql_ast.ListValueNode) and not target.is_array:\n            # not an actual array or multirange, but a set represented as a\n            # list\n            return qlast.Set(elements=[\n                self._visit_insert_value(el) for el in node.values])\n\n        else:\n            # some scalar value\n            val = self.visit(node)\n\n            if target.is_json:\n                # JSON can only come as a variable and will already be\n                # converted appropriately.\n                return val\n            elif target.edb_base_name != 'std::str':\n\n                # bigint data would require a bigint input, so\n                # check if the expression is using a parameter\n                if (target.edb_base_name == 'std::bigint'\n                        and isinstance(node, gql_ast.VariableNode)\n                        and val.type.maintype.name == 'int64'):\n\n                    res = val\n                    res.type.maintype.name = target.edb_base_name\n\n                else:\n                    res = qlast.TypeCast(\n                        expr=val,\n                        type=qlast.TypeName(\n                            maintype=target.edb_base_name_ast\n                        )\n                    )\n\n                if target.is_array:\n                    res = qlast.TypeCast(\n                        expr=val,\n                        type=qlast.TypeName(\n                            maintype=qlast.ObjectRef(name='array'),\n                            subtypes=[res.type],\n                        )\n                    )\n\n                elif target.is_range:\n                    # Range inputs come in two varieties: as a variable or as\n                    # a literal. Variables are already in JSON format and only\n                    # need to be cast into the appropriate range. Literals are\n                    # processed earlier as ObjectValueNode.\n                    res = qlast.TypeCast(\n                        expr=val,\n                        type=qlast.TypeName(\n                            maintype=qlast.ObjectRef(name='range'),\n                            subtypes=[res.type],\n                        )\n                    )\n\n                elif target.is_multirange:\n                    # Multiranges are composed of a list of ranges. List\n                    # literal is processed earlier, so we just need to cast\n                    # JSON into an array of ranges if it came from a\n                    # varaible.\n                    rtype = qlast.TypeName(\n                        maintype=qlast.ObjectRef(name='range'),\n                        subtypes=[res.type],\n                    )\n                    res = qlast.FunctionCall(\n                        func='multirange',\n                        args=[\n                            qlast.TypeCast(\n                                expr=val,\n                                type=qlast.TypeName(\n                                    maintype=qlast.ObjectRef(name='array'),\n                                    subtypes=[rtype],\n                                )\n                            )\n                        ],\n                    )\n\n                return res\n            else:\n                return val\n\n    def get_path_prefix(self, end_trim=None):\n        # flatten the path\n        path = [step\n                for psteps in self._context.path\n                for step in psteps]\n\n        # find the first shadowed root\n        prev_step = None\n        base_step = None\n        partial = False\n        base_i = 0\n        for i, step in enumerate(path):\n            cur = step.type\n\n            # if the field is specifically shadowed, then this is\n            # appropriate shadow base\n            if base_step is None and not partial:\n                if (prev_step is not None and\n                        prev_step.type.is_field_shadowed(step.name)):\n                    base_step = prev_step\n                    base_i = i\n                    break\n\n                # otherwise the base must be shadowing an entire type\n                elif isinstance(cur, gt.GQLShadowType):\n                    base_step = step\n                    base_i = i\n\n            # we have a base, but we might find out that we need to\n            # override it with a partial path\n            elif step.name is None and isinstance(cur, gt.GQLShadowType):\n                partial = True\n                base_step = None\n                base_i = i\n\n            # this is where the actual partial path steps start\n            elif partial and step.name is not None:\n                break\n\n            prev_step = step\n        else:\n            # we got to the end of the list without hitting other\n            # conditions, so that's the base\n            if base_step is None:\n                base_step = step\n                base_i = i\n\n        # trim the rest of the path\n        path = path[base_i + 1:end_trim]\n        if base_step is None:\n            # if the base_step is of the form (None, GQLShadowType), then\n            # we don't want any prefix, because we'll use partial paths\n            prefix = []\n        elif base_step.eql_alias:\n            # the root may be aliased\n            prefix = [qlast.ObjectRef(name=base_step.eql_alias)]\n        else:\n            prefix = [base_step.type.edb_base_name_ast]\n\n        for step in path:\n            if isinstance(step.name, gql_ast.NamedTypeNode):\n                # This is coming from a fragment, so we need to add a\n                # type intersection.\n                base = step.type\n                prefix.append(\n                    qlast.TypeIntersection(\n                        type=qlast.TypeName(\n                            maintype=base.edb_base_name_ast\n                        )\n                    )\n                )\n            else:\n                prefix.append(qlast.Ptr(name=step.name))\n\n        return prefix\n\n    def visit_ListValueNode(self, node):\n        return qlast.Array(elements=self.visit(node.values))\n\n    def visit_ObjectValueNode(self, node):\n        # This represents some expression to be used in filter. In\n        # case of multiple expressions they are implicitly combined\n        # using AND.\n        return self._visit_list_generalized_bool_op(node.fields, 'AND')\n\n    def visit_ObjectFieldNode(self, node):\n        fname = node.name.value\n\n        # handle boolean ops\n        if fname == 'and':\n            # Conform to Postgres AND, which treats False AND NULL = False.\n            return self._visit_list_of_inputs(node.value, 'AND')\n        elif fname == 'or':\n            # Conform to Postgres OR, which treats True OR NULL = True\n            return self._visit_list_of_inputs(node.value, 'OR')\n        elif fname == 'not':\n            return qlast.UnaryOp(op='NOT', operand=self.visit(node.value))\n\n        # handle various scalar ops\n        op = gt.GQL_TO_OPS_MAP.get(fname)\n\n        if op:\n            value = self.visit(node.value)\n            left = self._context.base_expr\n\n            # 'exists' filter gets converted to:\n            # EXISTS (<expr>) = <value>\n            # where the <value> is either true or false. This is so\n            # that there's a one-to-one correspondence between the\n            # potential input variables and the EdgeQL variables.\n            #\n            # If different EdgeQL code were generated instead, then\n            # the assumption that it's safe to re-run the same EdgeQL\n            # query with different input variables would not hold.\n            if op == 'EXISTS':\n                left = qlast.UnaryOp(op='EXISTS', operand=left)\n                # The binary operator that we need here is \"=\"\n                op = '='\n\n            elif op == 'IN':\n                # Instead of wrapping the values in an array, wrap\n                # them in a set\n                value = qlast.FunctionCall(\n                    func='array_unpack',\n                    args=[value],\n                )\n\n            elif self._context.right_cast is not None:\n                # We don't need to cast the RHS for the EXISTS, only\n                # for other operations.\n                value = qlast.TypeCast(\n                    expr=value,\n                    type=self._context.right_cast,\n                )\n\n            return qlast.BinOp(\n                left=left, op=op, right=value)\n\n        # we're at the beginning of a scalar op\n        _, target = self._get_parent_and_current_type()\n\n        name = self.get_path_prefix()\n        name.append(qlast.Ptr(name=fname))\n        name = qlast.Path(\n            steps=name,\n            # paths that start with a Ptr are partial\n            partial=isinstance(name[0], qlast.Ptr),\n        )\n\n        ftype = target.get_field_type(fname)\n        typename = ftype.edb_base_name\n        if typename not in {'std::str', 'std::uuid'}:\n            gql_type = gt.EDB_TO_GQL_SCALARS_MAP.get(typename)\n            if gql_type == graphql.GraphQLString:\n                # potentially need to cast the 'name' side into a\n                # <str>, so as to be compatible with the 'value'\n                name = qlast.TypeCast(\n                    expr=name,\n                    type=qlast.TypeName(maintype=qlast.ObjectRef(name='str')),\n                )\n\n        # ### Set up context for the nested visitor ###\n        self._context.base_expr = name\n        # potentially the right-hand-side needs to be cast into a float\n        if ftype.is_float:\n            self._context.right_cast = qlast.TypeName(\n                maintype=ftype.edb_base_name_ast)\n        elif typename == 'std::uuid':\n            self._context.right_cast = qlast.TypeName(\n                maintype=qlast.ObjectRef(name='uuid'))\n\n        path = self._context.path[-1]\n        path.append(Step(name=fname, type=ftype, eql_alias=None))\n        try:\n            value = self.visit(node.value)\n        finally:\n            path.pop()\n            self._context.right_cast = None\n            self._context.base_expr = None\n\n        # we need to cast a target string into <uuid> or enum\n        if (typename == 'std::uuid'\n                and not (\n                    # EXISTS side does not need a <uuid> cast\n                    isinstance(value.left, qlast.UnaryOp) and\n                    value.left.op == 'EXISTS'\n                )\n                and not isinstance(value.right, qlast.TypeCast)):\n\n            value.right = qlast.TypeCast(\n                expr=value.right,\n                type=qlast.TypeName(maintype=ftype.edb_base_name_ast),\n            )\n        elif ftype.is_enum:\n            value.right = qlast.TypeCast(\n                expr=value.right,\n                type=qlast.TypeName(maintype=ftype.edb_base_name_ast),\n            )\n\n        return value\n\n    def visit_order(self, node):\n        if not isinstance(node, gql_ast.ObjectValueNode):\n            raise g_errors.GraphQLTranslationError(\n                f'an object is expected for \"order\"')\n\n        # if there is no specific ordering, then order by id\n        if not node.fields:\n            return [qlast.SortExpr(\n                path=qlast.Path(\n                    steps=[qlast.Ptr(name='id')],\n                    partial=True,\n                ),\n                direction=qlast.SortAsc,\n            )]\n\n        # Ordering is handled by specifying a list of special Ordering objects.\n        # Validation is already handled by this point.\n        orderby = []\n        for ordering in self._visit_order_item(node):\n            orderby.append(qlast.SortExpr(\n                path=qlast.Path(\n                    steps=[\n                        qlast.Ptr(name=name) for name in ordering.names\n                    ],\n                    partial=True,\n                ),\n                direction=ordering.direction,\n                nones_order=ordering.nulls,\n            ))\n\n        return orderby\n\n    def _visit_order_item(self, node):\n        if not isinstance(node, gql_ast.ObjectValueNode):\n            raise g_errors.GraphQLTranslationError(\n                f'an object is expected for \"order\"')\n\n        orderings = []\n        direction = nulls = None\n\n        for part in node.fields:\n            # Check if there's a longer nested path here. If there is,\n            # validate that there's only one option chosen at this\n            # level.\n            if isinstance(part.value, gql_ast.ObjectValueNode):\n                for subordering in self._visit_order_item(part.value):\n                    orderings.append(\n                        Ordering(\n                            names=[part.name.value] + subordering.names,\n                            direction=subordering.direction,\n                            nulls=subordering.nulls\n                        )\n                    )\n\n            elif part.name.value == 'dir':\n                direction = part.value.value\n            elif part.name.value == 'nulls':\n                nulls = part.value.value\n\n        if orderings:\n            # We have compiled some ordering paths, so we don't have\n            # any direction or nulls on this level.\n            return orderings\n\n        # direction is a required field, so we can rely on it having\n        # one of two values\n        if direction == 'ASC':\n            direction = qlast.SortAsc\n            # nulls are optional, but are 'SMALLEST' by default\n            if nulls == 'BIGGEST':\n                nulls = qlast.NonesLast\n            else:\n                nulls = qlast.NonesFirst\n\n        else:  # DESC\n            direction = qlast.SortDesc\n            # nulls are optional, but are 'SMALLEST' by default\n            if nulls == 'BIGGEST':\n                nulls = qlast.NonesFirst\n            else:\n                nulls = qlast.NonesLast\n\n        return [Ordering(names=[], direction=direction, nulls=nulls)]\n\n    def visit_VariableNode(self, node):\n        return self._get_variable(node.name.value)\n\n    def _get_variable(self, varname):\n        var = self._context.vars[varname]\n        err_msg = (f\"Only scalar input variables are allowed. \"\n                   f\"Variable {varname!r} has non-scalar value.\")\n\n        vartype = var.defn.type\n        optional = True\n        # get the type of the value being inserted\n        target = self._maybe_get_current_type()\n\n        if isinstance(vartype, gql_ast.NonNullTypeNode):\n            vartype = vartype.type\n            optional = False\n\n        if self.is_list_type(vartype):\n            if target and target.is_multirange:\n                castname = qlast.ObjectRef(name='json')\n\n            else:\n                # So far the only list allowed is a multirange\n                # representation.\n                raise errors.QueryError(err_msg)\n\n        elif vartype.name.value in gt.GQL_TO_EDB_SCALARS_MAP:\n            castname = qlast.ObjectRef(\n                name=gt.GQL_TO_EDB_SCALARS_MAP[vartype.name.value])\n        elif (\n            name := gt.GQL_TO_EDB_RANGES_MAP.get(vartype.name.value)\n        ) is not None:\n            castname = qlast.ObjectRef(name=name)\n        else:\n            try:\n                vtype = self.get_type(\n                    self._context.gqlcore.gql_to_edb_name(vartype.name.value))\n            except AssertionError:\n                raise errors.QueryError(err_msg)\n\n            if vtype.is_enum:\n                castname = vtype.edb_base_name_ast\n            else:\n                raise errors.QueryError(err_msg)\n\n        casttype = qlast.TypeName(maintype=castname)\n\n        casts = [casttype]\n        # Currently, whe using the native protocol we pass in\n        # extracted arguments as JSON instead of native encodings.\n        # We probably should be able to do better, since we do this right\n        # on the edgeql extraction side, but I didn't want to bother\n        # with integrating the extractors to share the code.\n        if self._context.native_input and varname.startswith('__edb_arg_'):\n            casts.append(\n                qlast.TypeName(maintype=qlast.ObjectRef(name='json'))\n            )\n\n        val = qlast.QueryParameter(name=varname)\n        for ct in reversed(casts):\n            val = qlast.TypeCast(\n                type=ct,\n                expr=val,\n                cardinality_mod=(\n                    qlast.CardinalityModifier.Optional if optional else None\n                ),\n            )\n\n        return val\n\n    def visit_StringValueNode(self, node):\n        return qlast.Constant.string(node.value)\n\n    def visit_IntValueNode(self, node):\n        # produces an int64 or bigint\n        val = int(node.value)\n        if s_utils.MIN_INT64 <= val <= s_utils.MAX_INT64:\n            return qlast.Constant.integer(val)\n        else:\n            return qlast.Constant(\n                value=f'{val}n', kind=qlast.ConstantKind.BIGINT\n            )\n\n    def visit_FloatValueNode(self, node):\n        # Treat all Float as Decimal by default and downcast as necessary\n        return qlast.Constant(\n            value=f'{node.value}n', kind=qlast.ConstantKind.DECIMAL\n        )\n\n    def visit_BooleanValueNode(self, node):\n        value = 'true' if node.value else 'false'\n        return qlast.Constant.boolean(value)\n\n    def visit_EnumValueNode(self, node):\n        return qlast.Constant.string(node.value)\n\n    def _visit_list_of_inputs(self, inputlist, op):\n        if not isinstance(inputlist, gql_ast.ListValueNode):\n            raise g_errors.GraphQLTranslationError(\n                f'a list was expected')\n\n        return self._visit_list_generalized_bool_op(\n            [node for node in inputlist.values], op)\n\n    def _visit_list_generalized_bool_op(self, nodes, op):\n        # Generalization of a boolean operation AND or OR as it is\n        # applied to a list of expressions. This comes up in filters\n        # either explicitly by using 'and' or 'or' or by supplying a\n        # list of expressions where 'and' is implied.\n        #\n        # In this limited context it is appropriate to use Postres'\n        # truth table for AND and OR, short-circuiting \"False AND\n        # anything\" or \"True OR anything\" respectively to \"False\" and\n        # \"True\" instead of the stricter EdgeQL rules that would\n        # produce empty sets if any of the inputs are empty.\n\n        if not nodes:\n            return None\n        elif len(nodes) == 1:\n            return self.visit(nodes[0])\n\n        # The short-circuiting value is True for OR and False for AND.\n        opname = ('sys', f'__pg_{op.lower()}')\n        exprs = [self.visit(node) for node in nodes]\n\n        result = qlast.FunctionCall(\n            func=opname,\n            args=exprs[0:2],\n        )\n        for expr in exprs[2:]:\n            result = qlast.FunctionCall(\n                func=opname,\n                args=[result, expr],\n            )\n\n        return result\n\n    def combine_field_results(self, results, *, flatten=True):\n        if flatten:\n            flattened = []\n            for res in results:\n                if isinstance(res, Field):\n                    flattened.append(res)\n                elif isinstance(res, dict):\n                    flattened.extend(res.values())\n                elif typeutils.is_container(res):\n                    flattened.extend(res)\n                else:\n                    flattened.append(res)\n            return flattened\n        else:\n            return results\n\n\ndef value_node_from_pyvalue(val: Any):\n    if val is None:\n        return None\n    elif isinstance(val, str):\n        val = val.replace('\\\\', '\\\\\\\\')\n        value = eql_quote.quote_literal(val)\n        return gql_ast.StringValueNode(value=value[1:-1])\n    elif isinstance(val, bool):\n        return gql_ast.BooleanValueNode(value=bool(val))\n    elif isinstance(val, int):\n        return gql_ast.IntValueNode(value=str(val))\n    elif isinstance(val, (float, decimal.Decimal)):\n        return gql_ast.FloatValueNode(value=str(val))\n    elif isinstance(val, list):\n        return gql_ast.ListValueNode(\n            values=[value_node_from_pyvalue(v) for v in val])\n    elif isinstance(val, dict):\n        return gql_ast.ObjectValueNode(\n            fields=[\n                gql_ast.ObjectFieldNode(\n                    name=n,\n                    value=value_node_from_pyvalue(v)\n                )\n                for n, v in val.items()\n            ])\n    else:\n        raise ValueError(f'unexpected constant type: {type(val)!r}')\n\n\ndef parse_text(query: str) -> graphql.Document:\n    try:\n        return graphql.parse(query)\n    except graphql.GraphQLError as err:\n        err_loc = (err.locations[0].line,\n                   err.locations[0].column)\n        raise g_errors.GraphQLCoreError(err.message, loc=err_loc) from None\n\n\nclass TokenLexer(graphql.language.lexer.Lexer):\n\n    def __init__(self, source, tokens, eof_pos):\n        self.__tokens = tokens\n        self.__index = 0\n        self.__eof_pos = eof_pos\n        self.source = source\n        kind, start, end, line, col, body = self.__tokens[0]\n        self.token = gql_lexer.Token(kind, start, end, line, col, None, body)\n\n    def advance(self) -> gql_lexer.Token:\n        self.last_token = self.token\n        token = self.token = self.lookahead()\n        self.__index += 1\n        return token\n\n    def lookahead(self) -> gql_lexer.Token:\n        token = self.token\n        if token.kind != gql_lexer.TokenKind.EOF:\n            if token.next:\n                return self.token.next\n            kind, start, end, line, col, body = self.__tokens[self.__index + 1]\n            token.next = gql_lexer.Token(\n                kind, start, end, line, col, token, body)\n            return token.next\n        else:\n            return token\n\n\ndef parse_tokens(\n    text: str, tokens: list[tuple[gql_lexer.TokenKind, int, int, int, int, str]]\n) -> graphql.Document:\n    try:\n        src = graphql.Source(text)\n        parser = graphql.language.parser.Parser(src)\n        parser._lexer = TokenLexer(src, tokens, len(text))\n        return parser.parse_document()\n    except graphql.GraphQLError as err:\n        err_loc = (err.locations[0].line,\n                   err.locations[0].column)\n        raise g_errors.GraphQLCoreError(err.message, loc=err_loc) from None\n\n\ndef convert_errors(\n    errs: list[gql_error.GraphQLError],\n    *,\n    substitutions: Optional[dict[str, tuple[str, int, int]]],\n) -> list[gql_error.GraphQLErrors]:\n    result = []\n    for err in errs:\n        m = REWRITE_TYPE_ERROR.match(err.message)\n        if not m:\n            # we allow conversion from Int to Float, and that is allowed by\n            # graphql spec. It's unclear why graphql-core chokes on this\n            if INT_FLOAT_ERROR.match(err.message):\n                continue\n\n            result.append(err)\n            continue\n        elif (m.group(\"used\"), m.group(\"expected\")) in _IMPLICIT_CONVERSIONS:\n            # skip the error, we avoid it in the execution code\n            continue\n        value, line, col = substitutions[m.group(\"var_name\")]\n        err = gql_error.GraphQLError(\n            f\"Expected type {m.group('expected')}, found {value}.\")\n        err.locations = [gql_lang.SourceLocation(line, col)]\n        result.append(err)\n    return result\n\n\ndef translate_ast(\n    gqlcore: gt.GQLCoreSchema,\n    document_ast: graphql.Document,\n    *,\n    operation_name: Optional[str]=None,\n    variables: Optional[Mapping[str, Any]]=None,\n    substitutions: Optional[dict[str, tuple[str, int, int]]],\n    extracted_variables: Optional[Mapping[str, Any]],\n    native_input: bool = False,\n) -> TranspiledOperation:\n\n    # If no variables have been provided, and we are in native\n    # protocol mode, that means we need to be tolerant of critical\n    # variables not having values. We'll report out which ones\n    # existed, and then recompile with the variables present.\n    parse_only_mode = native_input and variables is None\n\n    if variables is None:\n        variables = {}\n\n    # The normalizer tries to handle default variables on its own,\n    # which we still don't support in native mode.\n    # Detect what it does and reject it.\n    if (\n        native_input\n        and len(extracted_variables or ()) > len(substitutions or ())\n    ):\n        raise errors.UnsupportedFeatureError(\n            'Default variables are not supported on the gel protocol'\n        )\n\n    validation_errors = convert_errors(\n        graphql.validate(gqlcore.graphql_schema, document_ast),\n        substitutions=substitutions)\n    if validation_errors:\n        err = validation_errors[0]\n        if isinstance(err, graphql.GraphQLError):\n\n            # possibly add additional information and/or hints to the\n            # error message\n            msg = augment_error_message(gqlcore, err.message)\n\n            err_loc = (err.locations[0].line, err.locations[0].column)\n            raise g_errors.GraphQLCoreError(msg, loc=err_loc)\n        else:\n            raise err\n\n    context = GraphQLTranslatorContext(\n        gqlcore=gqlcore,\n        query=None,\n        variables=variables,\n        document_ast=document_ast,\n        operation_name=operation_name,\n        native_input=native_input,\n        parse_only_mode=parse_only_mode,\n    )\n\n    translator = GraphQLTranslator(context=context)\n    edge_forest_map = translator.visit(document_ast)\n\n    if debug.flags.graphql_compile:\n        for opname, op in sorted(edge_forest_map.items()):\n            print(f'== operationName: {opname!r} =============')\n            print(ql_codegen.generate_source(op.stmt))\n\n    op = next(iter(edge_forest_map.values()))\n\n    if native_input:\n        used_vars = {\n            p.name\n            for p in visitor.find_children(op.stmt, qlast.QueryParameter)\n        }\n        unused_vars = op.vars.keys() - used_vars\n\n        if unused_vars:\n            op.stmt.orderby = [\n                qlast.Tuple(\n                    elements=[\n                        translator._get_variable(vn)\n                        for vn in sorted(unused_vars)\n                    ]\n                )\n            ]\n\n    # generate the specific result\n    return TranspiledOperation(\n        edgeql_ast=op.stmt,\n        cache_deps_vars=frozenset(op.critvars) if op.critvars else None,\n        variables_desc=op.vars,\n    )\n\n\ndef augment_error_message(gqlcore: gt.GQLCoreSchema, message: str):\n    # If the error is about wrong Query field, we can add more details\n    # about what seems to have gone wrong. The type is missing,\n    # possibly because this connection is to the wrong DB. However,\n    # this is only relevant if the message doesn't contain a hint already.\n    if (re.match(r\"^Cannot query field '(.+?)' on type 'Query'\\.$\", message)):\n        field = message.split(\"'\", 2)[1]\n        name = gqlcore.gql_to_edb_name(field)\n\n        message += (\n            f' There\\'s no corresponding type or alias \"{name}\" exposed in '\n            'Gel. Please check the configuration settings for this port '\n            'to make sure that you\\'re connecting to the right database.'\n        )\n\n    return message\n\n\ndef convert_default(\n    node: gql_ast.ValueNode, varname: str\n) -> str | float | int | bool:\n    if isinstance(node, (gql_ast.StringValueNode,\n                         gql_ast.BooleanValueNode,\n                         gql_ast.EnumValueNode)):\n        return node.value\n    elif isinstance(node, gql_ast.IntValueNode):\n        return int(node.value)\n    elif isinstance(node, gql_ast.FloatValueNode):\n        return float(node.value)\n    else:\n        raise errors.QueryError(\n            f\"Only scalar defaults are allowed. \"\n            f\"Variable {varname!r} has non-scalar default value.\")\n"
  },
  {
    "path": "edb/graphql/types.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    ClassVar,\n    Optional,\n    cast,\n)\n\nfrom functools import partial\nfrom graphql import (\n    GraphQLAbstractType,\n    GraphQLSchema,\n    GraphQLInputType,\n    GraphQLNamedType,\n    GraphQLOutputType,\n    GraphQLObjectType,\n    GraphQLWrappingType,\n    GraphQLInterfaceType,\n    GraphQLInputObjectType,\n    GraphQLResolveInfo,\n    GraphQLField,\n    GraphQLInputField,\n    GraphQLArgument,\n    GraphQLList,\n    GraphQLNonNull,\n    GraphQLString,\n    GraphQLInt,\n    GraphQLFloat,\n    GraphQLBoolean,\n    GraphQLID,\n    GraphQLEnumType,\n)\nfrom graphql.type import GraphQLEnumValue, GraphQLScalarType\nfrom graphql.language import ast as gql_ast\nimport itertools\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\nfrom edb.edgeql import codegen\nfrom edb.edgeql.parser import parse_fragment\n\nfrom edb.schema import modules as s_mod\nfrom edb.schema import name as s_name\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import schema as s_schema\nfrom edb.schema import types as s_types\nfrom edb.schema import utils as s_utils\n\nfrom . import errors as g_errors\n\n\n'''\nThis module is responsible for mapping Gel types onto the GraphQL\ntypes. However, this is an imperfect mapping because not all the types\nor relationships between them can be expressed.\n\n# Aliased Types\n\nAliased types present a particular problem. Basically, they break\ninheritance and GraphQL fragments become useless for them.\n\nConsider a link friends in this alias:\n```\n    type User {... multi link friends -> User}\n    type SpecialUser extending User {property special-> str}\n    alias UserAlias := User {friends: {some_new_prop := 'foo'}}\n```\n\nIn GraphQL we will have type UserType implementing interfaces User,\nObject and SpecialUserType implementing SpecialUser, User, Object. The\ntrouble starts with our implicit aliased type for the aliased friends\nlink that targets __UserAlias__friends. This type gets reflected into\na GraphQL _edb__UserAlias__friends that implements... what? We have 2\noptions:\n\n    1) Implement interfaces mirroring the Gel types: User, Object.\n\n    2) Implement it's own interface (or just omit interfaces here,\n       since if the interface is unique it's not adding anything).\n\nCase 2) preserves all the fields defined in the alias to be accessible\nand filterable, etc., but loses inheritance information.\n\nCase 1) leads to the following additional choices:\n\n    a) The friends target is still interface User, then the field\n       some_new_prop will not appear in the nesting, but will require\n       a specialized fragment:\n\n        query {\n            UserAlias {\n                friends {\n                    ... on _edb__UserAlias_friends {\n                        some_new_prop\n                    }\n                }\n            }\n        }\n\n    b) We can make the target of friends of UserAlias to be the actual\n       type _edb__UserAlias__friends (similar to case 2), but then we\n       cannot use the typed fragment `... on SpecialUser` construct\n       inside it because the SpecialUser is a sibling of our aliased\n       type and will cause a GraphQL validation error.\n\n    c) The field target can be a union type, but it will still only\n       have fields that are common to all union members and will\n       require awkward inlined typed fragments to work with just like\n       the first bullet point.\n\nIn the end I think that rather than preserving the inheritance and\nthen essentially forcing the use of `... on _edb__UserAlias_friends`\njust to access the very field for which the alias was created in the\nfirst place it's better to bite the bullet accept that in GraphQL\naliased types reflection removes all inheritance info, but at least\nprovide all the fields as per normal. The reasoning being that the\nfields and data are probably much more important for practical\npurposes than inheritance purity. To allow the SpecialUser\npolymorphism I'd rather suggest to the user to bake it into the alias\nlike so:\n```\n    alias UserAlias := User {\n        friends: {\n            some_new_prop := 'foo',\n            [IS SpecialUser].special,\n        }\n    }\n```\n\nAlso, note that for the same reasons as outlined above that make\naliased types into a sibling branch in GraphQL, we can't give an\naccurate __typename for them, like we can in EdgeQL (__type__.name)\nbecause the \"correct\" types would violate the declared GraphQL type\nhierarchy. So aliased types are necessarily opaque in GraphQL in all\nthese ways. Unlike in EdgeQL.\n'''\n\n\ndef coerce_int64(value: Any) -> int:\n    if isinstance(value, int):\n        num = value\n    else:\n        num = int(value)\n    if s_utils.MIN_INT64 <= num <= s_utils.MAX_INT64:\n        return num\n\n    raise Exception(\n        f\"Int64 cannot represent non 64-bit signed integer value: {value}\")\n\n\ndef coerce_bigint(value: Any) -> int:\n    if isinstance(value, int):\n        num = value\n    else:\n        num = int(value)\n    return num\n\n\ndef parse_int_literal(\n    ast: gql_ast.Node,\n    _variables: Optional[dict[str, Any]] = None,\n) -> Optional[int]:\n    if isinstance(ast, gql_ast.IntValueNode):\n        return int(ast.value)\n    else:\n        return None\n\n\nGraphQLInt64 = GraphQLScalarType(\n    name=\"Int64\",\n    description=\"The `Int64` scalar type represents non-fractional signed \"\n                \"whole numeric values. Int can represent values between \"\n                \"-2^63 and 2^63 - 1.\",\n    serialize=coerce_int64,\n    parse_value=coerce_int64,\n    parse_literal=parse_int_literal,\n)\n\n\nGraphQLBigint = GraphQLScalarType(\n    name=\"Bigint\",\n    description=\"The `Bigint` scalar type represents non-fractional signed \"\n                \"whole numeric values.\",\n    serialize=coerce_bigint,\n    parse_value=coerce_bigint,\n    parse_literal=parse_int_literal,\n)\n\n\nGraphQLJSON = GraphQLScalarType(\n    name=\"JSON\",\n    description=\"The `JSON` scalar type represents arbitrary JSON values.\",\n)\n\n\ndef parse_decimal_literal(\n    ast: gql_ast.Node,\n    _variables: Optional[dict[str, Any]] = None,\n) -> Optional[float]:\n    if isinstance(ast, (gql_ast.FloatValueNode, gql_ast.IntValueNode)):\n        return float(ast.value)\n    else:\n        return None\n\n\nGraphQLDecimal = GraphQLScalarType(\n    name=\"Decimal\",\n    description=\"The `Decimal` scalar type represents signed \"\n                \"unlimited-precision fractional values.\",\n    serialize=GraphQLFloat.serialize,\n    parse_value=GraphQLFloat.parse_value,\n    parse_literal=parse_decimal_literal,\n)\n\n\nEDB_TO_GQL_SCALARS_MAP = {\n    # For compatibility with GraphQL we cast json into a String, since\n    # GraphQL doesn't have an equivalent type with arbitrary fields.\n    'std::json': GraphQLJSON,\n    'std::str': GraphQLString,\n    'std::anyint': GraphQLInt,\n    'std::int16': GraphQLInt,\n    'std::int32': GraphQLInt,\n    'std::int64': GraphQLInt64,\n    'std::bigint': GraphQLBigint,\n    'std::anyfloat': GraphQLFloat,\n    'std::float32': GraphQLFloat,\n    'std::float64': GraphQLFloat,\n    'std::anyreal': GraphQLFloat,\n    'std::decimal': GraphQLDecimal,\n    'std::bool': GraphQLBoolean,\n    'std::uuid': GraphQLID,\n    'std::datetime': GraphQLString,\n    'std::duration': GraphQLString,\n    'std::bytes': None,\n\n    'std::cal::local_datetime': GraphQLString,\n    'std::cal::local_date': GraphQLString,\n    'std::cal::local_time': GraphQLString,\n    'std::cal::relative_duration': GraphQLString,\n    'std::cal::date_duration': GraphQLString,\n}\n\n\n# used for casting input values from GraphQL to EdgeQL\nGQL_TO_EDB_SCALARS_MAP = {\n    'String': 'str',\n    'Int': 'int32',\n    'Int64': 'int64',\n    'Bigint': 'bigint',\n    'Float': 'float64',\n    'Decimal': 'decimal',\n    'Boolean': 'bool',\n    'ID': 'uuid',\n    'JSON': 'json',\n}\n\n\nGQL_TO_EDB_RANGES_MAP = {\n    'RangeOfString': 'json',\n    'RangeOfInt': 'json',\n    'RangeOfInt64': 'json',\n    'RangeOfFloat': 'json',\n    'RangeOfDecimal': 'json',\n}\n\n\nGQL_TO_OPS_MAP = {\n    'exists': 'EXISTS',\n    'in': 'IN',\n    'eq': '=',\n    'neq': '!=',\n    'gt': '>',\n    'gte': '>=',\n    'lt': '<',\n    'lte': '<=',\n    'like': 'LIKE',\n    'ilike': 'ILIKE',\n}\n\n\nHIDDEN_MODULES = set(s_schema.STD_MODULES) - {s_name.UnqualName('std')}\n# The following are placeholders.\nTOP_LEVEL_TYPES = {\n    s_name.QualName(module='__graphql__', name='Query'),\n    s_name.QualName(module='__graphql__', name='Mutation'),\n}\n# The following types should not be exposed as all.\nHIDDEN_TYPES = {\n    s_name.QualName(module='std', name='FreeObject'),\n}\n\n\nclass GQLCoreSchema:\n\n    _gql_interfaces: dict[\n        s_name.QualName,\n        GraphQLInterfaceType,\n    ]\n\n    _gql_objtypes_from_alias: dict[\n        s_name.QualName,\n        GraphQLObjectType,\n    ]\n\n    _gql_objtypes: dict[\n        s_name.QualName,\n        GraphQLObjectType,\n    ]\n\n    _gql_inobjtypes: dict[\n        str,\n        GraphQLInputObjectType | GraphQLEnumType | GraphQLScalarType\n    ]\n\n    _gql_ordertypes: dict[str, GraphQLInputType]\n\n    _gql_enums: dict[str, GraphQLEnumType]\n\n    _type_map: dict[tuple[str, bool], GQLBaseType]\n\n    def __init__(self, edb_schema: s_schema.Schema) -> None:\n        '''Create a graphql schema based on edgedb schema.'''\n\n        self.edb_schema = edb_schema\n        # extract and sort modules to have a consistent type ordering\n        self.modules = list(sorted({\n            m.get_name(self.edb_schema)\n            for m in self.edb_schema.get_objects(type=s_mod.Module)\n        } - HIDDEN_MODULES))\n\n        self._gql_interfaces = {}\n        self._gql_uniontypes: set[s_name.QualName] = set()\n        self._gql_objtypes_from_alias = {}\n        self._gql_objtypes = {}\n        self._gql_inobjtypes = {}\n        self._gql_ordertypes = {}\n        self._gql_enums = {}\n\n        self._define_types()\n\n        # Use a fake name as a placeholder.\n        Query = s_name.QualName(module='__graphql__', name='Query')\n        query = self._gql_objtypes[Query] = GraphQLObjectType(\n            name='Query',\n            fields=self.get_fields(Query),\n        )\n\n        # If a database only has abstract types and scalars, no\n        # mutations will be possible (such as in a blank database),\n        # but we would still want the reflection to work without\n        # error, even if all that can be discovered through GraphQL\n        # then is the schema.\n        Mutation = s_name.QualName(module='__graphql__', name='Mutation')\n        fields = self.get_fields(Mutation)\n        if not fields:\n            mutation = None\n        else:\n            mutation = self._gql_objtypes[Mutation] = GraphQLObjectType(\n                name='Mutation',\n                fields=fields,\n            )\n\n        # get a sorted list of types relevant for the Schema\n        types = [\n            objt for name, objt in\n            itertools.chain(self._gql_objtypes.items(),\n                            self._gql_inobjtypes.items())\n            # the Query is included separately\n            if name not in TOP_LEVEL_TYPES\n        ]\n        types = sorted(types, key=lambda x: x.name)\n        self._gql_schema = GraphQLSchema(\n            query=query, mutation=mutation, types=types)\n\n        # this map is used for GQL -> EQL translator needs\n        self._type_map = {}\n\n    @property\n    def edgedb_schema(self) -> s_schema.Schema:\n        return self.edb_schema\n\n    @property\n    def graphql_schema(self) -> GraphQLSchema:\n        return self._gql_schema\n\n    def _get_type_gql_name(self, type: s_types.Type) -> str:\n        if type.get_from_global(self.edb_schema):\n            # The names of global types are mangled so use shortname instead\n            typename = type.get_shortname(self.edb_schema)\n        else:\n            typename = type.get_name(self.edb_schema)\n        assert isinstance(typename, s_name.QualName)\n        return self.get_gql_name(typename)\n\n    @classmethod\n    def get_gql_name(cls, name: s_name.QualName) -> str:\n        module, shortname = name.module, name.name\n\n        # Adjust the shortname.\n        if shortname.startswith('__'):\n            # Use '_edb' prefix to mark derived and otherwise\n            # internal types. We opt out of '__edb' because we\n            # still rely on the first occurrence of '__' in\n            # GraphQL names to separate the module from the rest\n            # of the name in some code.\n            shortname = f'_edb{shortname}'\n        elif shortname.startswith('('):\n            # Looks like a union type, so we'll need to process individual\n            # parts of the name.\n            names = []\n            for part in shortname[1:-1].split(' | '):\n                names.append(\n                    cls.get_gql_name(s_name.QualName(*part.split(':', 1))))\n            shortname = '_OR_'.join(names)\n\n        if module in {'default', 'std'}:\n            return shortname\n        else:\n            assert module != '', f'get_gl_name {name=}'\n            return str(name).replace(\"::\", \"__\")\n\n    def get_input_name(self, inputtype: str, name: str) -> str:\n        if '__' in name:\n            module, shortname = name.rsplit('__', 1)\n            assert module != '', f'get_input_name {name=}'\n            return f'{module}__{inputtype}{shortname}'\n        else:\n            return f'{inputtype}{name}'\n\n    def gql_to_edb_name(self, name: str) -> str:\n        '''Convert the GraphQL field name into a Gel type/view name.'''\n        if '__' in name:\n            return name.replace('__', '::')\n        else:\n            return name\n\n    def _get_description(self, edb_type: s_types.Type) -> Optional[str]:\n        description_anno = edb_type.get_annotations(self.edb_schema).get(\n            self.edb_schema, s_name.QualName('std', 'description'), None)\n        if description_anno is not None:\n            return description_anno.get_value(self.edb_schema)\n\n        return None\n\n    def _convert_edb_type(\n        self,\n        edb_target: s_types.Type,\n    ) -> Optional[GraphQLOutputType]:\n        target: Optional[GraphQLOutputType] = None\n\n        if isinstance(edb_target, s_types.Array):\n            subtype = edb_target.get_subtypes(self.edb_schema)[0]\n            el_type = self._convert_edb_type(subtype)\n            if el_type is None:\n                # we can't expose an array of unexposable type\n                return el_type\n            else:\n                target = GraphQLList(GraphQLNonNull(el_type))\n\n        elif isinstance(edb_target, (s_types.Range, s_types.MultiRange)):\n            # Represent ranges and multiranges as JSON. Same as reason as for\n            # tuples: the values are atomic and cannot be fragmented via\n            # GraphQL specification, so we cannot use objects with fields to\n            # represent them.\n            target = EDB_TO_GQL_SCALARS_MAP['std::json']\n\n        elif edb_target.is_view(self.edb_schema):\n            tname = edb_target.get_name(self.edb_schema)\n            assert isinstance(tname, s_name.QualName)\n            target = self._gql_objtypes.get(tname)\n\n        elif isinstance(edb_target, s_objtypes.ObjectType):\n            target = self._gql_interfaces.get(\n                edb_target.get_name(self.edb_schema),\n                self._gql_objtypes.get(edb_target.get_name(self.edb_schema))\n            )\n\n        elif (\n            isinstance(edb_target, s_scalars.ScalarType)\n            and edb_target.is_enum(self.edb_schema)\n        ):\n            name = self._get_type_gql_name(edb_target)\n\n            if name in self._gql_enums:\n                target = self._gql_enums.get(name)\n\n        elif edb_target.is_tuple(self.edb_schema):\n            # Represent tuples as JSON.\n            target = EDB_TO_GQL_SCALARS_MAP['std::json']\n\n        elif isinstance(edb_target, s_types.InheritingType):\n            base_target = edb_target.get_topmost_concrete_base(self.edb_schema)\n            bt_name = base_target.get_name(self.edb_schema)\n            try:\n                target = EDB_TO_GQL_SCALARS_MAP[str(bt_name)]\n            except KeyError:\n                # this is the scalar base case, where all potentially\n                # unrecognized scalars should end up\n                edb_typename = edb_target.get_verbosename(self.edb_schema)\n                raise g_errors.GraphQLCoreError(\n                    f\"could not convert {edb_typename!r} type to\"\n                    f\" a GraphQL type\")\n        else:\n            raise AssertionError(f'unexpected schema object: {edb_target!r}')\n\n        return target\n\n    def _get_target(\n        self,\n        ptr: s_pointers.Pointer,\n    ) -> Optional[GraphQLOutputType]:\n        edb_target = ptr.get_target(self.edb_schema)\n        if edb_target is None:\n            raise AssertionError(f'unexpected abstract pointer: {ptr!r}')\n        target = self._convert_edb_type(edb_target)\n\n        if target is not None:\n            # figure out any additional wrappers due to cardinality\n            # and required flags\n            target = self._wrap_output_type(ptr, target)\n\n        return target\n\n    def _wrap_output_type(\n        self,\n        ptr: s_pointers.Pointer,\n        target: GraphQLOutputType,\n        *,\n        ignore_required: bool = False,\n    ) -> GraphQLOutputType:\n        # figure out any additional wrappers due to cardinality\n        # and required flags\n        if not ptr.singular(self.edb_schema):\n            target = GraphQLList(GraphQLNonNull(target))\n\n        if not ignore_required:\n            # for input values having a default cancels out being required\n            if ptr.get_required(self.edb_schema):\n                target = GraphQLNonNull(target)\n\n        return target\n\n    def _wrap_input_type(\n        self,\n        ptr: s_pointers.Pointer,\n        target: GraphQLInputType,\n        *,\n        ignore_required: bool = False,\n    ) -> GraphQLInputType:\n        # figure out any additional wrappers due to cardinality\n        # and required flags\n        if not ptr.singular(self.edb_schema):\n            target = GraphQLList(GraphQLNonNull(target))\n\n        if not ignore_required:\n            if (\n                ptr.get_required(self.edb_schema)\n                and ptr.get_default(self.edb_schema) is None\n            ):\n                target = GraphQLNonNull(target)\n\n        return target\n\n    def _get_query_args(\n        self,\n        typename: s_name.QualName,\n    ) -> dict[str, GraphQLArgument]:\n        return {\n            'filter': GraphQLArgument(self._gql_inobjtypes[str(typename)]),\n            'order': GraphQLArgument(self._gql_ordertypes[str(typename)]),\n            'first': GraphQLArgument(GraphQLInt),\n            'last': GraphQLArgument(GraphQLInt),\n            # before and after are supposed to be opaque values\n            # serialized to string\n            'before': GraphQLArgument(GraphQLString),\n            'after': GraphQLArgument(GraphQLString),\n        }\n\n    def _get_insert_args(\n        self,\n        typename: s_name.QualName,\n    ) -> dict[str, GraphQLArgument]:\n        # The data can only be a specific non-interface type, if no\n        # such type exists, skip it as we cannot accept unambiguous\n        # data input. It's still possible to just select some existing\n        # data.\n        intype = self._gql_inobjtypes.get(f'Insert{typename}')\n        if intype is None:\n            return {}\n\n        return {\n            'data': GraphQLArgument(\n                GraphQLNonNull(GraphQLList(GraphQLNonNull(intype)))),\n        }\n\n    def _get_update_args(\n        self,\n        typename: s_name.QualName,\n    ) -> dict[str, GraphQLArgument]:\n        # some types have no updates\n        uptype = self._gql_inobjtypes.get(f'Update{typename}')\n        if uptype is None:\n            return {}\n\n        # the update args are same as for query + data\n        args = self._get_query_args(typename)\n        args['data'] = GraphQLArgument(GraphQLNonNull(uptype))\n        return args\n\n    def get_fields(\n        self,\n        typename: s_name.QualName,\n    ) -> dict[str, GraphQLField]:\n        fields = {}\n\n        if str(typename) == '__graphql__::Query':\n            # The fields here will come from abstract types and aliases.\n            queryable: list[tuple[s_name.QualName, GraphQLNamedType]] = []\n            queryable.extend(self._gql_interfaces.items())\n            queryable.extend(self._gql_objtypes_from_alias.items())\n            queryable.sort(key=lambda x: x[1].name)\n            for name, gqliface in queryable:\n                # '_edb' prefix indicates an internally generated type\n                # (e.g. nested aliased type), which should not be\n                # exposed as a top-level query option.\n                if name in TOP_LEVEL_TYPES or gqliface.name.startswith('_edb'):\n                    continue\n                # Check that the underlying type is not a union type.\n                if name in self._gql_uniontypes:\n                    continue\n                fields[gqliface.name] = GraphQLField(\n                    GraphQLList(GraphQLNonNull(gqliface)),\n                    args=self._get_query_args(name),\n                )\n        elif str(typename) == '__graphql__::Mutation':\n            # Get a list of alias names, so that we don't generate inserts for\n            # them.\n            aliases = {t.name for t in self._gql_objtypes_from_alias.values()}\n            for name, gqltype in sorted(self._gql_objtypes.items(),\n                                        key=lambda x: x[1].name):\n                # '_edb' prefix indicates an internally generated type\n                # (e.g. nested aliased type), which should not be\n                # exposed as a top-level mutation option.\n                if name in TOP_LEVEL_TYPES or gqltype.name.startswith('_edb'):\n                    continue\n                edb_type = self.edb_schema.get(name, type=s_types.Type)\n                gname = self._get_type_gql_name(edb_type)\n                fields[f'delete_{gname}'] = GraphQLField(\n                    GraphQLList(GraphQLNonNull(gqltype)),\n                    args=self._get_query_args(name),\n                )\n                if gname in aliases:\n                    # Aliases can only have delete mutations\n                    continue\n\n                args = self._get_insert_args(name)\n                fields[f'insert_{gname}'] = GraphQLField(\n                    GraphQLList(GraphQLNonNull(gqltype)),\n                    args=args,\n                )\n\n            for name, gqliface in sorted(self._gql_interfaces.items(),\n                                         key=lambda x: x[1].name):\n                if (name in TOP_LEVEL_TYPES or\n                    gqliface.name.startswith('_edb') or\n                        f'Update{name}' not in self._gql_inobjtypes):\n                    continue\n                edb_type = self.edb_schema.get(name, type=s_types.Type)\n                gname = self._get_type_gql_name(edb_type)\n                args = self._get_update_args(name)\n                if args:\n                    # If there are no args, there's nothing to update.\n                    fields[f'update_{gname}'] = GraphQLField(\n                        GraphQLList(GraphQLNonNull(gqliface)),\n                        args=args,\n                    )\n        else:\n            edb_type = self.edb_schema.get(\n                typename,\n                type=s_objtypes.ObjectType,\n            )\n            pointers = edb_type.get_pointers(self.edb_schema)\n\n            for unqual_pn, ptr in sorted(pointers.items(self.edb_schema)):\n                pn = str(unqual_pn)\n                if pn == '__type__':\n                    continue\n                assert isinstance(ptr, s_pointers.Pointer)\n\n                tgt = ptr.get_target(self.edb_schema)\n                assert tgt is not None\n                # Aliased types ignore their ancestors in order to\n                # allow all their fields appear properly in the\n                # filters.\n                #\n                # If the target is not a view, but this is computed,\n                # so we cannot later override it, thus we can use the\n                # type as is.\n                if (\n                    not tgt.is_view(self.edb_schema) and\n                    not ptr.is_pure_computable(self.edb_schema)\n                ):\n                    # We want to look at the pointer lineage because that\n                    # will be reflected into GraphQL interface that is\n                    # being extended and the type cannot be changed.\n                    ancestors: tuple[s_pointers.Pointer, ...]\n                    ancestors = ptr.get_ancestors(\n                        self.edb_schema).objects(self.edb_schema)\n\n                    # We want the first non-generic ancestor of this\n                    # pointer as its target type will dictate the target\n                    # types of all its derived pointers.\n                    #\n                    # NOTE: We're guaranteed to have a non-generic one\n                    # since we're inspecting the lineage of a pointer\n                    # belonging to an actual type.\n                    for ancestor in reversed((ptr,) + ancestors):\n                        if not ancestor.is_non_concrete(self.edb_schema):\n                            ptr = ancestor\n                            break\n\n                target = self._get_target(ptr)\n\n                if target is not None:\n                    ptgt = ptr.get_target(self.edb_schema)\n                    if not isinstance(ptgt, s_objtypes.ObjectType):\n                        objargs = None\n                    else:\n                        objargs = self._get_query_args(\n                            ptgt.get_name(self.edb_schema))\n\n                    fields[pn] = GraphQLField(target, args=objargs)\n\n        return fields\n\n    def get_filter_fields(\n        self,\n        typename: s_name.QualName,\n        nested: bool = False,\n    ) -> dict[str, GraphQLInputField]:\n        selftype = self._gql_inobjtypes[str(typename)]\n        fields = {}\n\n        if not nested:\n            fields['and'] = GraphQLInputField(\n                GraphQLList(GraphQLNonNull(selftype)))\n            fields['or'] = GraphQLInputField(\n                GraphQLList(GraphQLNonNull(selftype)))\n            fields['not'] = GraphQLInputField(selftype)\n        else:\n            # Always include the 'exists' operation\n            fields['exists'] = GraphQLInputField(GraphQLBoolean)\n\n        edb_type = self.edb_schema.get(typename, type=s_objtypes.ObjectType)\n        pointers = edb_type.get_pointers(self.edb_schema)\n        names = sorted(pointers.keys(self.edb_schema))\n        for unqual_name in names:\n            name = str(unqual_name)\n            if name == '__type__':\n                continue\n            if name in fields:\n                raise g_errors.GraphQLCoreError(\n                    f\"{name!r} of {typename} clashes with special \"\n                    \"reserved fields required for GraphQL conversion\"\n                )\n\n            ptr = edb_type.getptr(self.edb_schema, unqual_name)\n            edb_target = ptr.get_target(self.edb_schema)\n            assert edb_target is not None\n\n            if isinstance(edb_target, s_objtypes.ObjectType):\n                t_name = edb_target.get_name(self.edb_schema)\n                gql_name = self.get_input_name(\n                    'NestedFilter', self._get_type_gql_name(edb_target))\n\n                intype = self._gql_inobjtypes.get(gql_name)\n                if intype is None:\n                    # construct a nested insert type\n                    intype = GraphQLInputObjectType(\n                        name=gql_name,\n                        fields=partial(self.get_filter_fields, t_name, True),\n                    )\n                    self._gql_inobjtypes[gql_name] = intype\n\n            elif not edb_target.is_scalar():\n                continue\n\n            else:\n                target = self._convert_edb_type(edb_target)\n                if target is None:\n                    # don't expose this\n                    continue\n\n                if isinstance(target, GraphQLNamedType):\n                    intype = self._gql_inobjtypes.get(f'Filter{target.name}')\n                else:\n                    raise AssertionError(\n                        f'unexpected GraphQL type: {target!r}'\n                    )\n\n            if intype:\n                fields[name] = GraphQLInputField(intype)\n\n        return fields\n\n    def get_insert_fields(\n        self,\n        typename: s_name.QualName,\n    ) -> dict[str, GraphQLInputField]:\n        fields = {}\n\n        edb_type = self.edb_schema.get(typename, type=s_objtypes.ObjectType)\n        pointers = edb_type.get_pointers(self.edb_schema)\n        names = sorted(pointers.keys(self.edb_schema))\n        for unqual_name in names:\n            name = str(unqual_name)\n            if name in {'__type__', 'id'}:\n                continue\n\n            ptr = edb_type.getptr(self.edb_schema, unqual_name)\n            edb_target = ptr.get_target(self.edb_schema)\n            intype: GraphQLInputType\n\n            if ptr.is_pure_computable(self.edb_schema):\n                # skip computed pointer\n                continue\n\n            elif isinstance(edb_target, s_objtypes.ObjectType):\n                typename = edb_target.get_name(self.edb_schema)\n\n                inobjtype = self._gql_inobjtypes.get(f'NestedInsert{typename}')\n                if inobjtype is not None:\n                    intype = inobjtype\n                else:\n                    # construct a nested insert type\n                    intype = self._make_generic_nested_insert_type(edb_target)\n\n                intype = self._wrap_input_type(ptr, intype)\n                fields[name] = GraphQLInputField(intype)\n\n            elif (\n                edb_target and\n                edb_target.contains_array_of_tuples(self.edb_schema)\n            ):\n                # Can't insert array<tuple<...>>\n                continue\n\n            elif (\n                isinstance(edb_target, s_scalars.ScalarType)\n                or isinstance(edb_target, s_types.Array)\n            ):\n                target = self._convert_edb_type(edb_target)\n                if target is None:\n                    # don't expose this\n                    continue\n\n                if isinstance(target, GraphQLList):\n                    # Check whether the edb_target is an array of enums,\n                    # because enums need slightly different handling.\n                    assert isinstance(edb_target, s_types.Array)\n                    el = edb_target.get_element_type(self.edb_schema)\n                    if el.is_enum(self.edb_schema):\n                        tname = el.get_name(self.edb_schema)\n                        assert isinstance(tname, s_name.QualName)\n                    else:\n                        tname = target.of_type.of_type.name\n\n                    inobjtype = self._gql_inobjtypes.get(f'Insert{tname}')\n                    assert inobjtype is not None\n                    intype = GraphQLList(GraphQLNonNull(inobjtype))\n\n                elif edb_target.is_enum(self.edb_schema):\n                    enum_name = edb_target.get_name(self.edb_schema)\n                    assert isinstance(enum_name, s_name.QualName)\n                    inobjtype = self._gql_inobjtypes.get(f'Insert{enum_name}')\n                    assert inobjtype is not None\n                    intype = inobjtype\n\n                elif isinstance(target, GraphQLNamedType):\n                    inobjtype = self._gql_inobjtypes.get(\n                        f'Insert{target.name}')\n                    assert inobjtype is not None\n                    intype = inobjtype\n\n                else:\n                    raise AssertionError(\n                        f'unexpected GraphQL type\" {target!r}'\n                    )\n\n                intype = self._wrap_input_type(ptr, intype)\n\n                if intype:\n                    fields[name] = GraphQLInputField(intype)\n\n            elif isinstance(edb_target, s_types.Range):\n                subtype = edb_target.get_subtypes(self.edb_schema)[0]\n                intype = self.get_input_range_type(subtype)\n                intype = self._wrap_input_type(ptr, intype)\n                fields[name] = GraphQLInputField(intype)\n\n            elif isinstance(edb_target, s_types.MultiRange):\n                subtype = edb_target.get_subtypes(self.edb_schema)[0]\n                intype = GraphQLList(GraphQLNonNull(\n                    self.get_input_range_type(subtype)))\n                intype = self._wrap_input_type(ptr, intype)\n                fields[name] = GraphQLInputField(intype)\n\n            else:\n                continue\n\n        return fields\n\n    def get_update_fields(\n        self,\n        typename: s_name.QualName,\n    ) -> dict[str, GraphQLInputField]:\n        fields = {}\n\n        edb_type = self.edb_schema.get(typename, type=s_objtypes.ObjectType)\n        pointers = edb_type.get_pointers(self.edb_schema)\n        names = sorted(pointers.keys(self.edb_schema))\n        # This is just a heavily re-used type variable\n        target: GraphQLInputType | Optional[GraphQLOutputType]\n        for unqual_name in names:\n            name = str(unqual_name)\n            if name == '__type__':\n                continue\n\n            ptr = edb_type.getptr(self.edb_schema, unqual_name)\n            edb_target = ptr.get_target(self.edb_schema)\n\n            if ptr.is_pure_computable(self.edb_schema):\n                # skip computed pointer\n                continue\n\n            elif isinstance(edb_target, s_objtypes.ObjectType):\n                intype = self._gql_inobjtypes.get(\n                    f'UpdateOp{typename}__{name}')\n                if intype is None:\n                    # the links can only be updated by selecting some\n                    # objects, meaning that the basis is the same as for\n                    # query of whatever is the link type\n                    intype = self._gql_inobjtypes.get(\n                        f'NestedUpdate{edb_target.get_name(self.edb_schema)}')\n                    if intype is None:\n                        # construct a nested insert type\n                        intype = self._make_generic_nested_update_type(\n                            edb_target)\n\n                    # depending on whether this is a multilink or not wrap\n                    # it in a List\n                    intype = cast(\n                        GraphQLInputObjectType,\n                        self._wrap_input_type(\n                            ptr, intype, ignore_required=True),\n                    )\n                    # wrap into additional layer representing update ops\n                    intype = self._make_generic_update_op_type(\n                        ptr, name, edb_type, intype)\n\n                fields[name] = GraphQLInputField(intype)\n\n            elif (\n                edb_target and\n                edb_target.contains_array_of_tuples(self.edb_schema)\n            ):\n                # Can't update array<tuple<...>>\n                continue\n\n            elif isinstance(\n                edb_target,\n                (\n                    s_scalars.ScalarType,\n                    s_types.Array,\n                )\n            ):\n                target = self._convert_edb_type(edb_target)\n                if target is None or ptr.get_readonly(self.edb_schema):\n                    # don't expose this\n                    continue\n\n                intype = self._gql_inobjtypes.get(\n                    f'UpdateOp{typename}__{name}')\n                if intype is None:\n                    # construct a nested insert type\n                    assert isinstance(\n                        target,\n                        (\n                            GraphQLScalarType,\n                            GraphQLEnumType,\n                            GraphQLInputObjectType,\n                            GraphQLWrappingType,\n                        ),\n                    ), f'got {target!r}, expected GraphQLInputType'\n                    intype = self._make_generic_update_op_type(\n                        ptr,\n                        fname=name,\n                        edb_base=edb_type,\n                        target=self._wrap_input_type(\n                            ptr,\n                            target,\n                            ignore_required=True,\n                        ),\n                    )\n\n                if intype:\n                    fields[name] = GraphQLInputField(intype)\n\n            elif isinstance(\n                edb_target,\n                (\n                    s_types.Range,\n                    s_types.MultiRange,\n                )\n            ):\n                subtype = edb_target.get_subtypes(self.edb_schema)[0]\n                target = self.get_input_range_type(subtype)\n                if isinstance(edb_target, s_types.MultiRange):\n                    target = GraphQLList(GraphQLNonNull(target))\n\n                intype = self._gql_inobjtypes.get(\n                    f'UpdateOp{typename}__{name}')\n                if intype is None:\n                    # construct a nested insert type\n                    intype = self._make_generic_update_op_type(\n                        ptr,\n                        fname=name,\n                        edb_base=edb_type,\n                        target=self._wrap_input_type(\n                            ptr,\n                            target,\n                            ignore_required=True,\n                        ),\n                    )\n\n                if intype:\n                    fields[name] = GraphQLInputField(intype)\n\n            else:\n                continue\n\n        return fields\n\n    def _make_generic_update_op_type(\n        self,\n        ptr: s_pointers.Pointer,\n        fname: str,\n        edb_base: s_types.Type,\n        target: GraphQLInputType,\n    ) -> GraphQLInputObjectType:\n        typename = edb_base.get_name(self.edb_schema)\n        assert isinstance(typename, s_name.QualName)\n        name = f'UpdateOp{typename}__{fname}'\n        edb_target = ptr.get_target(self.edb_schema)\n\n        fields = {\n            'set': GraphQLInputField(target)\n        }\n\n        # get additional commands based on the pointer type\n        if not ptr.get_required(self.edb_schema):\n            fields['clear'] = GraphQLInputField(GraphQLBoolean)\n\n        bt_name: Optional[s_name.QualName]\n        if isinstance(edb_target, s_scalars.ScalarType):\n            base_target = edb_target.get_topmost_concrete_base(self.edb_schema)\n            bt_name = base_target.get_name(self.edb_schema)\n        else:\n            bt_name = None\n\n        # first check for this being a multi-link\n        if not ptr.singular(self.edb_schema):\n            fields['add'] = GraphQLInputField(target)\n            fields['remove'] = GraphQLInputField(target)\n        elif target in {GraphQLInt, GraphQLInt64, GraphQLBigint,\n                        GraphQLFloat, GraphQLDecimal}:\n            # anything that maps onto the numeric types is a fair game\n            fields['increment'] = GraphQLInputField(target)\n            fields['decrement'] = GraphQLInputField(target)\n        elif (\n            bt_name == s_name.QualName(module='std', name='str')\n            or isinstance(edb_target, s_types.Array)\n        ):\n            # only actual strings and arrays have append, prepend and\n            # slice ops\n            fields['prepend'] = GraphQLInputField(target)\n            fields['append'] = GraphQLInputField(target)\n            # slice [from, to]\n            fields['slice'] = GraphQLInputField(\n                GraphQLList(GraphQLNonNull(GraphQLInt))\n            )\n\n        nitype = GraphQLInputObjectType(\n            name=self.get_input_name(\n                f'UpdateOp_{fname}_',\n                self._get_type_gql_name(edb_base),\n            ),\n            fields=fields,\n        )\n        self._gql_inobjtypes[name] = nitype\n\n        return nitype\n\n    def _make_generic_nested_update_type(\n        self,\n        edb_base: s_objtypes.ObjectType,\n    ) -> GraphQLInputObjectType:\n        typename = edb_base.get_name(self.edb_schema)\n        name = f'NestedUpdate{typename}'\n        nitype = GraphQLInputObjectType(\n            name=self.get_input_name(\n                'NestedUpdate', self._get_type_gql_name(edb_base)),\n            fields={\n                'filter': GraphQLInputField(\n                    self._gql_inobjtypes[str(typename)]),\n                'order': GraphQLInputField(\n                    self._gql_ordertypes[str(typename)]),\n                'first': GraphQLInputField(GraphQLInt),\n                'last': GraphQLInputField(GraphQLInt),\n                # before and after are supposed to be opaque values\n                # serialized to string\n                'before': GraphQLInputField(GraphQLString),\n                'after': GraphQLInputField(GraphQLString),\n            },\n        )\n\n        self._gql_inobjtypes[name] = nitype\n\n        return nitype\n\n    def _make_generic_nested_insert_type(\n        self,\n        edb_base: s_objtypes.ObjectType,\n    ) -> GraphQLInputObjectType:\n        typename = edb_base.get_name(self.edb_schema)\n        name = f'NestedInsert{typename}'\n        fields = {\n            'filter': GraphQLInputField(\n                self._gql_inobjtypes[str(typename)]),\n            'order': GraphQLInputField(\n                self._gql_ordertypes[str(typename)]),\n            'first': GraphQLInputField(GraphQLInt),\n            'last': GraphQLInputField(GraphQLInt),\n            # before and after are supposed to be opaque values\n            # serialized to string\n            'before': GraphQLInputField(GraphQLString),\n            'after': GraphQLInputField(GraphQLString),\n        }\n\n        # The data can only be a specific non-interface type, if no\n        # such type exists, skip it as we cannot accept unambiguous\n        # data input. It's still possible to just select some existing\n        # data.\n        data_t = self._gql_inobjtypes.get(f'Insert{typename}')\n        if data_t:\n            fields['data'] = GraphQLInputField(data_t)\n\n        nitype = GraphQLInputObjectType(\n            name=self.get_input_name(\n                'NestedInsert', self._get_type_gql_name(edb_base)),\n            fields=fields,\n        )\n\n        self._gql_inobjtypes[name] = nitype\n\n        return nitype\n\n    def define_enums(self) -> None:\n        self._gql_enums['directionEnum'] = GraphQLEnumType(\n            'directionEnum',\n            values=dict(\n                ASC=GraphQLEnumValue(),\n                DESC=GraphQLEnumValue()\n            ),\n            description='Enum value used to specify ordering direction.',\n        )\n        self._gql_enums['nullsOrderingEnum'] = GraphQLEnumType(\n            'nullsOrderingEnum',\n            values=dict(\n                SMALLEST=GraphQLEnumValue(),\n                BIGGEST=GraphQLEnumValue(),\n            ),\n            description='Enum value used to specify how nulls are ordered.',\n        )\n\n        scalar_types = list(\n            self.edb_schema.get_objects(\n                included_modules=self.modules,\n                type=s_scalars.ScalarType\n            ),\n        )\n        for st in scalar_types:\n            enum_values = st.get_enum_values(self.edb_schema)\n            if enum_values is not None:\n                t_name = st.get_name(self.edb_schema)\n                gql_name = self._get_type_gql_name(st)\n                enum_type = GraphQLEnumType(\n                    gql_name,\n                    values={key: GraphQLEnumValue() for key in enum_values},\n                    description=self._get_description(st),\n                )\n\n                self._gql_enums[gql_name] = enum_type\n                self._gql_inobjtypes[f'Insert{t_name}'] = enum_type\n\n    def define_generic_filter_types(self) -> None:\n        eq = ['eq', 'neq']\n        comp = eq + ['gte', 'gt', 'lte', 'lt']\n        string = comp + ['like', 'ilike']\n\n        self._make_generic_filter_type(GraphQLBoolean, eq)\n        self._make_generic_filter_type(GraphQLID, eq)\n        self._make_generic_filter_type(GraphQLInt, comp)\n        self._make_generic_filter_type(GraphQLInt64, comp)\n        self._make_generic_filter_type(GraphQLBigint, comp)\n        self._make_generic_filter_type(GraphQLFloat, comp)\n        self._make_generic_filter_type(GraphQLDecimal, comp)\n        self._make_generic_filter_type(GraphQLString, string)\n        self._make_generic_filter_type(GraphQLJSON, comp)\n\n        for name, etype in self._gql_enums.items():\n            if name not in {'directionEnum', 'nullsOrderingEnum'}:\n                self._make_generic_filter_type(etype, comp)\n\n    def _make_generic_filter_type(\n        self,\n        base: GraphQLScalarType | GraphQLEnumType,\n        ops: list[str],\n    ) -> None:\n        name = f'Filter{base.name}'\n        fields = {}\n\n        # Always include the 'exists' operation\n        fields['exists'] = GraphQLInputField(GraphQLBoolean)\n        # Always include the 'in' operation\n        fields['in'] = GraphQLInputField(GraphQLList(GraphQLNonNull(base)))\n        for op in ops:\n            fields[op] = GraphQLInputField(base)\n\n        self._gql_inobjtypes[name] = GraphQLInputObjectType(\n            name=name,\n            fields=fields,\n        )\n\n    def define_generic_insert_types(self) -> None:\n        for itype in [\n            GraphQLBoolean,\n            GraphQLID,\n            GraphQLInt,\n            GraphQLInt64,\n            GraphQLBigint,\n            GraphQLFloat,\n            GraphQLDecimal,\n            GraphQLString,\n            GraphQLJSON,\n        ]:\n            self._gql_inobjtypes[f'Insert{itype.name}'] = itype\n\n    def define_generic_order_types(self) -> None:\n        self._gql_ordertypes['directionEnum'] = self._gql_enums['directionEnum']\n        self._gql_ordertypes['nullsOrderingEnum'] = self._gql_enums[\n            'nullsOrderingEnum'\n        ]\n        self._gql_ordertypes['Ordering'] = GraphQLInputObjectType(\n            'Ordering',\n            fields=dict(\n                dir=GraphQLInputField(\n                    GraphQLNonNull(self._gql_enums['directionEnum']),\n                ),\n                nulls=GraphQLInputField(\n                    self._gql_enums['nullsOrderingEnum'],\n                    default_value='SMALLEST',\n                ),\n            )\n        )\n\n    def get_order_fields(\n        self,\n        typename: s_name.QualName,\n    ) -> dict[str, GraphQLInputField]:\n        fields: dict[str, GraphQLInputField] = {}\n\n        edb_type = self.edb_schema.get(typename, type=s_objtypes.ObjectType)\n        pointers = edb_type.get_pointers(self.edb_schema)\n        names = sorted(pointers.keys(self.edb_schema))\n\n        for unqual_name in names:\n            name = str(unqual_name)\n            if name == '__type__':\n                continue\n\n            ptr = edb_type.getptr(self.edb_schema, unqual_name)\n\n            if not ptr.singular(self.edb_schema):\n                continue\n\n            t = ptr.get_target(self.edb_schema)\n            assert t is not None\n\n            target = self._convert_edb_type(t)\n            if target is None:\n                # Don't expose this\n                continue\n\n            if isinstance(t, s_scalars.ScalarType):\n                assert isinstance(target, GraphQLNamedType)\n                # This makes sure that we can only order by properties\n                # that can be reflected into GraphQL\n                intype = self._gql_inobjtypes.get(f'Filter{target.name}')\n\n                if intype:\n                    fields[name] = GraphQLInputField(\n                        self._gql_ordertypes['Ordering']\n                    )\n            elif isinstance(t, s_objtypes.ObjectType):\n                # It's a link so we need the link's type order input\n                t_name = t.get_name(self.edb_schema)\n                fields[name] = GraphQLInputField(\n                    self._gql_ordertypes[str(t_name)]\n                )\n            else:\n                # We ignore pointers that aren't scalars or objects.\n                pass\n\n        return fields\n\n    def get_input_range_type(\n        self, subtype: s_types.Type\n    ) -> GraphQLInputObjectType:\n        sub_gqltype = self._convert_edb_type(subtype)\n        assert isinstance(sub_gqltype, GraphQLScalarType)\n        r_name = f'RangeOf{sub_gqltype.name}'\n        # Check the type cache...\n        if (res := self._gql_inobjtypes.get(r_name)) is not None:\n            assert isinstance(res, GraphQLInputObjectType)\n            return res\n\n        gqltype = GraphQLInputObjectType(\n            name=r_name,\n            fields=dict(\n                lower=GraphQLInputField(sub_gqltype),\n                inc_lower=GraphQLInputField(GraphQLBoolean),\n                upper=GraphQLInputField(sub_gqltype),\n                inc_upper=GraphQLInputField(GraphQLBoolean),\n                empty=GraphQLInputField(GraphQLBoolean),\n            ),\n            description=f'Range of {sub_gqltype.name} values',\n        )\n        self._gql_inobjtypes[r_name] = gqltype\n\n        return gqltype\n\n    def _define_types(self) -> None:\n        interface_types = []\n        obj_types = []\n        from_union = {}\n\n        self.define_enums()\n        self.define_generic_filter_types()\n        self.define_generic_order_types()\n        self.define_generic_insert_types()\n\n        # Every ObjectType is reflected as an interface.\n        interface_types = list(\n            self.edb_schema.get_objects(included_modules=self.modules,\n                                        type=s_objtypes.ObjectType))\n\n        # concrete types are also reflected as Type (with a '_Type' postfix)\n        obj_types += [t for t in interface_types\n                      if not t.get_abstract(self.edb_schema)]\n\n        # interfaces\n        for t in interface_types:\n            t_name = t.get_name(self.edb_schema)\n            gql_name = self._get_type_gql_name(t)\n\n            if t_name in HIDDEN_TYPES:\n                continue\n\n            if t.is_view(self.edb_schema):\n                # The aliased types actually only reflect as an object\n                # type, but the rest of the processing is identical to\n                # interfaces.\n                self._gql_objtypes_from_alias[t_name] = GraphQLObjectType(\n                    name=gql_name,\n                    fields=partial(self.get_fields, t_name),\n                    description=self._get_description(t),\n                )\n            else:\n\n                def _type_resolver(\n                    obj: GraphQLObjectType,\n                    info: GraphQLResolveInfo,\n                    _t: GraphQLAbstractType,\n                ) -> GraphQLObjectType:\n                    return obj\n                self._gql_interfaces[t_name] = GraphQLInterfaceType(\n                    name=gql_name,\n                    fields=partial(self.get_fields, t_name),\n                    resolve_type=_type_resolver,\n                    description=self._get_description(t),\n                )\n\n            if t.is_union_type(self.edb_schema):\n                # NOTE: EdgeDB union types and GraphQL union types are\n                # different in some important ways. In EdgeDB a union object\n                # type will have all the common links and properties that are\n                # shared among the members of the union. In GraphQL a union\n                # type has *no fields* at all and must be accessed via typed\n                # fragments. Effectively, EdgeDB union types behave exactly\n                # like GraphQL interfaces, though, which is why they will be\n                # reflected more naturally as interfaces.\n                #\n                # We still need to internally keep track of which interfaces\n                # are actually union types so that we don't create any\n                # top-level Query or Mutation entires for union types, but\n                # stick to only use them in the nested structures they\n                # actually appear in.\n\n                self._gql_uniontypes.add(t_name)\n                for member in t.get_union_of(self.edb_schema) \\\n                               .names(self.edb_schema):\n                    # Union types must be interfaces for each of\n                    # the individual components so we need to record that.\n                    from_union[member] = t_name\n\n            # input object types corresponding to this interface\n            gqlfiltertype = GraphQLInputObjectType(\n                name=self.get_input_name('Filter', gql_name),\n                fields=partial(self.get_filter_fields, t_name),\n            )\n            self._gql_inobjtypes[str(t_name)] = gqlfiltertype\n\n            # ordering input type\n            gqlordertype = GraphQLInputObjectType(\n                name=self.get_input_name('Order', gql_name),\n                fields=partial(self.get_order_fields, t_name),\n            )\n            self._gql_ordertypes[str(t_name)] = gqlordertype\n\n            # update object types corresponding to this object (all types\n            # except views and union types can appear as update types)\n            if not (t.is_view(self.edb_schema)\n                    or t.is_union_type(self.edb_schema)):\n\n                # only objects that have at least one non-readonly\n                # link/property are eligible\n                pointers = t.get_pointers(self.edb_schema)\n                if any(not p.get_readonly(self.edb_schema) and\n                       not p.is_pure_computable(self.edb_schema)\n                       for _, p in pointers.items(self.edb_schema)):\n                    gqlupdatetype = GraphQLInputObjectType(\n                        name=self.get_input_name('Update', gql_name),\n                        fields=partial(self.get_update_fields, t_name),\n                    )\n                    self._gql_inobjtypes[f'Update{t_name}'] = gqlupdatetype\n\n        # object types\n        for t in obj_types:\n            interfaces = []\n            t_name = t.get_name(self.edb_schema)\n            gql_name = self._get_type_gql_name(t)\n\n            if t_name in HIDDEN_TYPES:\n                continue\n\n            if t.is_view(self.edb_schema):\n                # Just copy previously computed type.\n                self._gql_objtypes[t_name] = \\\n                    self._gql_objtypes_from_alias[t_name]\n                continue\n\n            if t.is_union_type(self.edb_schema):\n                continue\n\n            if t_name in self._gql_interfaces:\n                interfaces.append(self._gql_interfaces[t_name])\n\n            if t_name in from_union:\n                interfaces.append(self._gql_interfaces[from_union[t_name]])\n\n            ancestors = t.get_ancestors(self.edb_schema)\n            for st in ancestors.objects(self.edb_schema):\n                if (st.is_object_type() and\n                    st.get_name(self.edb_schema) in\n                        self._gql_interfaces):\n                    interfaces.append(\n                        self._gql_interfaces[st.get_name(self.edb_schema)])\n\n            gqltype = GraphQLObjectType(\n                name=f'{gql_name}_Type',\n                fields=partial(self.get_fields, t_name),\n                interfaces=interfaces,\n                description=self._get_description(t),\n            )\n            self._gql_objtypes[t_name] = gqltype\n\n            # only objects that have at least one non-computed\n            # link/property are eligible to be input objects\n            pointers = t.get_pointers(self.edb_schema)\n            if any(not p.is_pure_computable(self.edb_schema)\n                   for pname, p in pointers.items(self.edb_schema)\n                   if str(pname) not in {'__type__', 'id'}):\n\n                # input object types corresponding to this object (only\n                # real objects can appear as input objects)\n                gqlinserttype = GraphQLInputObjectType(\n                    name=self.get_input_name('Insert', gql_name),\n                    fields=partial(self.get_insert_fields, t_name),\n                )\n                self._gql_inobjtypes[f'Insert{t_name}'] = gqlinserttype\n\n    def get(self, name: str, *, dummy: bool = False) -> GQLBaseType:\n        '''Get a special GQL type either by name or based on Gel type.'''\n        # normalize name and possibly add 'edb_base' to kwargs\n        edb_base = None\n        kwargs: dict[str, Any] = {'dummy': dummy}\n\n        if not name.startswith('__graphql__::'):\n            # The name may potentially contain the suffix \"_Type\",\n            # which in 99% cases indicates that it's a GraphQL\n            # internal type generated from the EdgeDB base type, but\n            # we technically need to check both.\n            if name.endswith('_Type'):\n                names = [name[:-len('_Type')], name]\n            else:\n                names = [name]\n\n            for tname in names:\n                if edb_base is None:\n                    module: s_name.Name | str\n\n                    if '::' in tname:\n                        edb_base = self.edb_schema.get(\n                            tname,\n                            type=s_types.Type,\n                        )\n                    elif '__' in tname:\n                        # Looks like it's coming from a specific module\n                        edb_base = self.edb_schema.get(\n                            f\"{tname.replace('__', '::')}\",\n                            type=s_types.Type,\n                        )\n                    else:\n                        for module in self.modules:\n                            edb_base = self.edb_schema.get(\n                                f'{module}::{tname}',\n                                type=s_types.Type,\n                                default=None,\n                            )\n                            if edb_base:\n                                break\n\n                        # XXX: find a better way to do this\n                        for stype in [s_types.Array, s_types.Tuple,\n                                      s_types.Range, s_types.MultiRange]:\n                            if edb_base is None:\n                                edb_base = self.edb_schema.get_global(\n                                    stype, tname, default=None\n                                )\n                            else:\n                                break\n\n            if edb_base is None:\n                raise AssertionError(\n                    f'unresolved type: {name}')\n\n            kwargs['edb_base'] = edb_base\n\n        # check if the type already exists\n        fkey = (name, dummy)\n        gqltype = self._type_map.get(fkey)\n\n        if not gqltype:\n            _type = GQLTypeMeta.edb_map.get(name, GQLShadowType)\n            gqltype = _type(schema=self, **kwargs)\n            self._type_map[fkey] = gqltype\n\n        return gqltype\n\n\nclass GQLTypeMeta(type):\n    edb_map: dict[str, type[GQLBaseType]] = {}\n\n    def __new__(\n        mcls,\n        name: str,\n        bases: tuple[type, ...],\n        dct: dict[str, Any],\n    ) -> GQLTypeMeta:\n        cls = super().__new__(mcls, name, bases, dct)\n\n        edb_type = dct.get('edb_type')\n        if edb_type:\n            mcls.edb_map[str(edb_type)] = cls  # type: ignore\n\n        return cls\n\n\nclass GQLBaseType(metaclass=GQLTypeMeta):\n\n    edb_type: ClassVar[Optional[s_name.QualName]] = None\n    _edb_base: Optional[s_types.Type]\n    _module: Optional[str]\n    _fields: dict[tuple[str, bool], GQLBaseType]\n    _shadow_fields: tuple[str, ...]\n\n    def __init__(\n        self,\n        schema: GQLCoreSchema,\n        *,\n        name: Optional[str] = None,\n        edb_base: Optional[s_types.Type] = None,\n        dummy: bool = False,\n    ) -> None:\n        self._shadow_fields = ()\n\n        if edb_base is None:\n            if self.edb_type:\n                if self.edb_type.module == '__graphql__':\n                    edb_base_name = str(self.edb_type)\n                else:\n                    edb_base = schema.edb_schema.get(\n                        self.edb_type,\n                        type=s_objtypes.ObjectType,\n                    )\n                    edb_base_name = str(edb_base.get_name(schema.edb_schema))\n            else:\n                raise AssertionError(\n                    f'neither the constructor, nor the class attribute '\n                    f'define a required edb_base for {type(self)!r}',\n                )\n        else:\n            edb_base_name = str(edb_base.get_name(schema.edb_schema))\n\n        # __typename\n        if name is None:\n            self._name = edb_base_name\n        else:\n            self._name = name\n\n        # determine module from name if not already specified\n        if '::' in self._name:\n            self._module = self._name.rsplit('::', 1)[0]\n        else:\n            self._module = None\n\n        # what EdgeDB entity will be the root for queries, if any\n        self._edb_base = edb_base\n        self._schema = schema\n        self._fields = {}\n\n        # XXX clean up needed, but otherwise it means that the type is\n        # used to validate the fields/types/args/etc., but is not\n        # expected to generate non-empty results, so messy EQL is not\n        # needed.\n        self.dummy = dummy\n        # JSON and bool need some special treatment so we want to know if\n        # we're dealing with it\n        if isinstance(edb_base, s_scalars.ScalarType):\n            bt = edb_base.get_topmost_concrete_base(self.edb_schema)\n            bt_name = str(bt.get_name(self.edb_schema))\n            self._is_json = bt_name == 'std::json'\n            self._is_bool = bt_name == 'std::bool'\n            self._is_float = edb_base.issubclass(\n                self.edb_schema,\n                self.edb_schema.get(\n                    'std::anyfloat',\n                    type=s_scalars.ScalarType,\n                ),\n            )\n        else:\n            self._is_json = self._is_bool = self._is_float = False\n\n    @property\n    def is_json(self) -> bool:\n        return self._is_json\n\n    @property\n    def is_enum(self) -> bool:\n        return False\n\n    @property\n    def is_bool(self) -> bool:\n        return self._is_bool\n\n    @property\n    def is_float(self) -> bool:\n        return self._is_float\n\n    @property\n    def is_array(self) -> bool:\n        if self.edb_base is None:\n            return False\n        else:\n            return self.edb_base.is_array()\n\n    @property\n    def is_range(self) -> bool:\n        if self.edb_base is None:\n            return False\n        else:\n            return self.edb_base.is_range()\n\n    @property\n    def is_multirange(self) -> bool:\n        if self.edb_base is None:\n            return False\n        else:\n            return self.edb_base.is_multirange()\n\n    @property\n    def is_object_type(self) -> bool:\n        if self.edb_base is None:\n            return False\n        else:\n            return self.edb_base.is_object_type()\n\n    @property\n    def name(self) -> str:\n        return self._name\n\n    @property\n    def short_name(self) -> str:\n        return self._name.split('::')[-1]\n\n    @property\n    def module(self) -> Optional[str]:\n        return self._module\n\n    @property\n    def edb_base(self) -> Optional[s_types.Type]:\n        return self._edb_base\n\n    @property\n    def edb_base_name_ast(self) -> Optional[qlast.ObjectRef]:\n        if self.edb_base is None:\n            return None\n        if isinstance(self.edb_base, (s_types.Array,\n                                      s_types.Range,\n                                      s_types.MultiRange)):\n            el = self.edb_base.get_element_type(self.edb_schema)\n            base_name = el.get_name(self.edb_schema)\n            assert isinstance(base_name, s_name.QualName)\n            return qlast.ObjectRef(\n                module=base_name.module,\n                name=base_name.name,\n            )\n        else:\n            base_name = self.edb_base.get_name(self.edb_schema)\n            assert isinstance(base_name, s_name.QualName)\n            return qlast.ObjectRef(\n                module=base_name.module,\n                name=base_name.name,\n            )\n\n    @property\n    def edb_base_name(self) -> str:\n        ast = self.edb_base_name_ast\n        if ast is None:\n            return ''\n        else:\n            return codegen.generate_source(ast)\n\n    @property\n    def gql_typename(self) -> str:\n        name = self.name\n        module, shortname = name.rsplit('::', 1)\n\n        if self.edb_base is None:\n            # We expect that this is one of the fake objects, that\n            # only have an edb_type.\n            assert self.edb_type is not None\n            return self.edb_type.name\n        elif self.edb_base.is_view(self.edb_schema):\n            suffix = ''\n        else:\n            suffix = '_Type'\n\n        if module in {'default', 'std'}:\n            return f'{shortname}{suffix}'\n        else:\n            assert module != '', 'gql_typename ' + module\n            return f'{name.replace(\"::\", \"__\")}{suffix}'\n\n    @property\n    def schema(self) -> GQLCoreSchema:\n        return self._schema\n\n    @property\n    def edb_schema(self) -> s_schema.Schema:\n        return self._schema.edb_schema\n\n    @edb_schema.setter\n    def edb_schema(self, schema: s_schema.Schema) -> None:\n        self._schema.edb_schema = schema\n\n    def convert_edb_to_gql_type(\n        self,\n        base: s_types.Type | s_pointers.Pointer,\n        **kwargs: Any,\n    ) -> GQLBaseType:\n        if isinstance(base, s_pointers.Pointer):\n            tgt = base.get_target(self.edb_schema)\n            assert tgt is not None\n            base = tgt\n\n        if self.dummy:\n            kwargs['dummy'] = True\n\n        return self.schema.get(str(base.get_name(self.edb_schema)), **kwargs)\n\n    def is_field_shadowed(self, name: str) -> bool:\n        return name in self._shadow_fields\n\n    def get_field_type(self, name: str) -> Optional[GQLBaseType]:\n        if self.dummy:\n            return None\n\n        # this is just shadowing a real EdgeDB type\n        fkey = (name, self.dummy)\n        target = self._fields.get(fkey)\n\n        if target is None:\n            # special handling of '__typename'\n            if name == '__typename':\n                target = self.convert_edb_to_gql_type(\n                    self.edb_schema.get(\n                        s_name.QualName(\n                            module='std',\n                            name='str',\n                        ),\n                        type=s_scalars.ScalarType,\n                    ),\n                )\n\n            elif isinstance(self.edb_base, s_objtypes.ObjectType):\n                ptr = self.edb_base.maybe_get_ptr(\n                    self.edb_schema,\n                    s_name.UnqualName(name),\n                )\n                if ptr is not None:\n                    target = self.convert_edb_to_gql_type(ptr)\n\n            if target is not None:\n                self._fields[fkey] = target\n\n        return target\n\n    def has_native_field(self, name: str) -> bool:\n        if isinstance(self.edb_base, s_objtypes.ObjectType):\n            ptr = self.edb_base.maybe_get_ptr(\n                self.edb_schema, s_name.UnqualName(name))\n            return ptr is not None\n        else:\n            return False\n\n    def issubclass(self, other: Any) -> bool:\n        if (\n            self.edb_base is not None\n            and other.edb_base is not None\n            and isinstance(other, GQLShadowType)\n        ):\n            return self.edb_base.issubclass(\n                self._schema.edb_schema, other.edb_base\n            )\n        else:\n            return False\n\n    def get_template(\n        self,\n    ) -> tuple[qlast.Base, Optional[qlast.Expr], Optional[qlast.SelectQuery]]:\n        '''Provide an EQL AST template to be filled.\n\n        Return the overall ast, a reference to where the shape element\n        with placeholder is, and a reference to the element which may\n        be filtered.\n        '''\n\n        if self.dummy:\n            return parse_fragment(f'''to_json(\"xxx\")'''), None, None\n\n        eql = parse_fragment(f'''\n            SELECT {self.edb_base_name} {{\n                xxx\n            }}\n        ''')\n\n        filterable = eql\n        assert isinstance(filterable, qlast.SelectQuery)\n        shape = filterable.result\n\n        return eql, shape, filterable\n\n    def get_field_template(\n        self,\n        name: str,\n        *,\n        parent: qlast.Base,\n        has_shape: bool = False,\n    ) -> tuple[\n        Optional[qlast.Base],\n        Optional[qlast.Expr],\n        Optional[qlast.SelectQuery],\n    ]:\n        eql = shape = filterable = None\n        if self.dummy:\n            return eql, shape, filterable\n\n        if name == '__typename' and not self.is_field_shadowed(name):\n            if self.edb_base is None:\n                # We expect that this is one of the fake objects, that\n                # only have an edb_type.\n                assert self.edb_type is not None\n                eql = parse_fragment(f'{self.edb_type.name!r}')\n            elif self.edb_base.is_view(self.edb_schema):\n                eql = parse_fragment(f'{self.gql_typename!r}')\n            else:\n                # Construct the GraphQL type name from the actual type name.\n                eql = parse_fragment(fr'''\n                    WITH name := {codegen.generate_source(parent)}\n                        .__type__.name\n                    SELECT (\n                        name[5:] IF name LIKE 'std::%' ELSE\n                        name[9:] IF name LIKE 'default::%' ELSE\n                        str_replace(name, '::', '__')\n                    ) ++ '_Type'\n                ''')\n\n        elif has_shape:\n            eql = parse_fragment(\n                f'''SELECT {codegen.generate_source(parent)}.\n                        {codegen.generate_source(qlast.ObjectRef(name=name))}\n                        {{ xxx }}\n                ''')\n            assert isinstance(eql, qlast.SelectQuery)\n            filterable = eql\n            shape = filterable.result\n\n        else:\n            eql = parse_fragment(\n                f'''SELECT {codegen.generate_source(parent)}.\n                        {codegen.generate_source(qlast.ObjectRef(name=name))}\n                ''')\n            assert isinstance(eql, qlast.SelectQuery)\n            filterable = eql\n\n        return eql, shape, filterable\n\n    def get_field_cardinality(\n        self,\n        name: str,\n    ) -> Optional[qltypes.SchemaCardinality]:\n        if not self.is_field_shadowed(name):\n            return None\n\n        elif isinstance(self.edb_base, s_objtypes.ObjectType):\n            ptr = self.edb_base.getptr(\n                self.edb_schema,\n                s_name.UnqualName(name),\n            )\n            if not ptr.singular(self.edb_schema):\n                return qltypes.SchemaCardinality.Many\n\n        return None\n\n\nclass GQLShadowType(GQLBaseType):\n\n    def is_field_shadowed(self, name: str) -> bool:\n        if name == '__typename':\n            return False\n\n        ftype = self.get_field_type(name)\n        # JSON fields are not shadowed\n        if ftype is None:\n            return False\n\n        return True\n\n    @property\n    def is_enum(self) -> bool:\n        if self.edb_base is None:\n            return False\n        else:\n            return self.edb_base.is_enum(self.edb_schema)\n\n\nclass GQLBaseQuery(GQLBaseType):\n\n    def __init__(\n        self,\n        schema: GQLCoreSchema,\n        *,\n        name: Optional[str] = None,\n        edb_base: Optional[s_types.Type] = None,\n        dummy: bool = False,\n    ) -> None:\n        self.modules = schema.modules\n        super().__init__(schema, name=name, edb_base=edb_base, dummy=dummy)\n        # Record names of std built-in object types\n        self._std_obj_names = [\n            t.get_name(self.edb_schema).name for t in\n            self.edb_schema.get_objects(\n                included_modules=[s_name.UnqualName('std')],\n                type=s_objtypes.ObjectType,\n            )\n        ]\n\n    def get_module_and_name(self, name: str) -> tuple[str, ...]:\n        if name in self._std_obj_names:\n            return ('std', name)\n        elif '__' in name:\n            module, name = name.rsplit('__', 1)\n            return (module.replace('__', '::'), name)\n        else:\n            return ('default', name)\n\n\nclass GQLQuery(GQLBaseQuery):\n    edb_type = s_name.QualName(module='__graphql__', name='Query')\n\n    def get_field_type(self, name: str) -> Optional[GQLBaseType]:\n        fkey = (name, self.dummy)\n        target = None\n\n        if name in {'__type', '__schema'}:\n            if fkey in self._fields:\n                return self._fields[fkey]\n\n            target = self.schema.get(str(self.edb_type), dummy=True)\n\n        else:\n            target = super().get_field_type(name)\n\n            if target is None:\n                module, edb_name = self.get_module_and_name(name)\n                edb_qname = s_name.QualName(module=module, name=edb_name)\n                edb_type = self.edb_schema.get(\n                    edb_qname,\n                    default=None,\n                    type=s_types.Type,\n                )\n                if edb_type is not None:\n                    target = self.convert_edb_to_gql_type(edb_type)\n\n        if target is not None:\n            self._fields[fkey] = target\n\n        return target\n\n\nclass GQLMutation(GQLBaseQuery):\n    edb_type = s_name.QualName(module='__graphql__', name='Mutation')\n\n    def get_field_type(self, name: str) -> Optional[GQLBaseType]:\n        fkey = (name, self.dummy)\n        target = None\n\n        if name == '__typename':\n            # It's a valid field that doesn't start with a command\n            target = super().get_field_type(name)\n        else:\n            op, name = name.split('_', 1)\n            if op in {'delete', 'insert', 'update'}:\n                target = super().get_field_type(name)\n\n                if target is None:\n                    module, edb_name = self.get_module_and_name(name)\n                    edb_qname = s_name.QualName(module=module, name=edb_name)\n                    edb_type = self.edb_schema.get(\n                        edb_qname,\n                        default=None,\n                        type=s_types.Type,\n                    )\n                    if edb_type is not None:\n                        target = self.convert_edb_to_gql_type(edb_type)\n\n        if target is not None:\n            self._fields[fkey] = target\n\n        return target\n"
  },
  {
    "path": "edb/graphql-rewrite/Cargo.toml",
    "content": "[package]\nname = \"graphql-rewrite\"\nversion = \"0.1.0\"\nlicense = \"MIT/Apache-2.0\"\nauthors = [\"MagicStack Inc. <hello@magic.io>\"]\nedition = \"2021\"\n\n[lints]\nworkspace = true\n\n[features]\npython_extension = [\"pyo3/extension-module\"]\ndefault = [\"python_extension\"]\n\n[dependencies]\npyo3 = { workspace = true, optional = true }\n\ncombine = \"3.8\"\nthiserror = \"2\"\nnum-bigint = \"0.4.3\"\nnum-traits = \"0.2.11\"\nedb-graphql-parser = { git=\"https://github.com/edgedb/graphql-parser\", features = [\"serde\"] }\nserde = { version = \"1.0.106\", features = [\"derive\"] }\nbincode = { version = \"1.3.3\" }\n\n[dev-dependencies]\npretty_assertions = \"1.2.0\"\n\n[lib]\ncrate-type = [\"lib\", \"cdylib\"]\nname = \"graphql_rewrite\"\npath = \"src/lib.rs\"\n"
  },
  {
    "path": "edb/graphql-rewrite/_graphql_rewrite.pyi",
    "content": "from typing import Any, Optional\n\nclass Entry:\n    key: str\n    key_vars: list[str]\n    variables: dict[str, Any]\n    substitutions: dict[str, tuple[str, int, int]]\n\n    def tokens(self) -> list[tuple[Any, int, int, int, int, Any]]: ...\n\ndef rewrite(operation: Optional[str], text: str) -> Entry: ...\n"
  },
  {
    "path": "edb/graphql-rewrite/src/lib.rs",
    "content": "#![cfg(feature = \"python_extension\")]\nmod py_entry;\nmod py_exception;\nmod py_token;\nmod rewrite;\nmod token_vec;\n\npub use py_token::{PyToken, PyTokenKind};\npub use rewrite::{rewrite, Value, Variable};\n\nuse py_exception::{AssertionError, LexingError, NotFoundError, QueryError, SyntaxError};\nuse pyo3::{prelude::*, types::PyString};\n\n/// Rust optimizer for graphql queries\n#[pymodule]\nfn _graphql_rewrite(py: Python, m: &Bound<PyModule>) -> PyResult<()> {\n    m.add_function(wrap_pyfunction!(py_rewrite, m)?)?;\n    m.add_function(wrap_pyfunction!(py_entry::unpack, m)?)?;\n    m.add_class::<py_entry::Entry>()?;\n    m.add(\"LexingError\", py.get_type::<LexingError>())?;\n    m.add(\"SyntaxError\", py.get_type::<SyntaxError>())?;\n    m.add(\"NotFoundError\", py.get_type::<NotFoundError>())?;\n    m.add(\"AssertionError\", py.get_type::<AssertionError>())?;\n    m.add(\"QueryError\", py.get_type::<QueryError>())?;\n    Ok(())\n}\n\n#[pyo3::pyfunction(name = \"rewrite\")]\n#[pyo3(signature = (operation, text))]\nfn py_rewrite(\n    py: Python<'_>,\n    operation: Option<&Bound<PyString>>,\n    text: &Bound<PyString>,\n) -> PyResult<py_entry::Entry> {\n    // convert args\n    let operation = operation.map(|x| x.to_string());\n    let text = text.to_string();\n\n    match rewrite::rewrite(operation.as_ref().map(|x| &x[..]), &text) {\n        Ok(entry) => py_entry::convert_entry(py, entry),\n        Err(e) => Err(py_exception::convert_error(e)),\n    }\n}\n"
  },
  {
    "path": "edb/graphql-rewrite/src/py_entry.rs",
    "content": "use pyo3::exceptions::PyValueError;\nuse pyo3::prelude::*;\nuse pyo3::types::{PyBytes, PyDict, PyInt, PyString, PyType};\n\nuse edb_graphql_parser::position::Pos;\n\nuse crate::py_token::{self, PyToken};\nuse crate::rewrite::{self, Value};\n\n#[pyclass]\npub struct Entry {\n    #[pyo3(get)]\n    key: Py<PyAny>,\n    #[pyo3(get)]\n    variables: Py<PyAny>,\n    #[pyo3(get)]\n    substitutions: Py<PyAny>,\n    _tokens: Vec<PyToken>,\n    _end_pos: Pos,\n    #[pyo3(get)]\n    num_variables: usize,\n\n    orig_entry: rewrite::Entry,\n}\n\n#[pymethods]\nimpl Entry {\n    fn tokens<'py>(&self, py: Python<'py>, kinds: Py<PyAny>) -> PyResult<impl IntoPyObject<'py>> {\n        py_token::convert_tokens(py, &self._tokens, &self._end_pos, kinds)\n    }\n\n    fn pack(&self, py: Python) -> PyResult<Py<PyAny>> {\n        let mut buf = vec![1u8]; // type and version\n        bincode::serialize_into(&mut buf, &self.orig_entry)\n            .map_err(|e| PyValueError::new_err(format!(\"Failed to pack: {e}\")))?;\n        Ok(PyBytes::new(py, buf.as_slice()).into())\n    }\n}\n\n#[pyfunction]\npub fn unpack(py: Python<'_>, serialized: &Bound<PyBytes>) -> PyResult<Py<PyAny>> {\n    let buf = serialized.as_bytes();\n    match buf[0] {\n        1u8 => {\n            let pack: rewrite::Entry = bincode::deserialize(&buf[1..])\n                .map_err(|e| PyValueError::new_err(format!(\"Failed to unpack: {e}\")))?;\n            let entry = convert_entry(py, pack)?;\n            entry.into_pyobject(py).map(|e| e.unbind().into_any())\n        }\n        _ => Err(PyValueError::new_err(format!(\n            \"Invalid type/version byte: {}\",\n            buf[0]\n        ))),\n    }\n}\n\npub fn convert_entry(py: Python<'_>, entry: rewrite::Entry) -> PyResult<Entry> {\n    // import decimal\n    let decimal_cls = PyModule::import(py, \"decimal\")?.getattr(\"Decimal\")?;\n\n    let vars = PyDict::new(py);\n    let substitutions = PyDict::new(py);\n    for (idx, var) in entry.variables.iter().enumerate() {\n        let s = format!(\"__edb_arg_{idx}\").into_pyobject(py)?;\n\n        vars.set_item(&s, value_to_py(py, &var.value, &decimal_cls)?)?;\n        substitutions.set_item(\n            s,\n            (\n                &var.token.value,\n                var.token.position.map(|x| x.line),\n                var.token.position.map(|x| x.column),\n            ),\n        )?;\n    }\n    for (name, var) in &entry.defaults {\n        vars.set_item(name, value_to_py(py, &var.value, &decimal_cls)?)?\n    }\n    let orig_entry = entry.clone();\n    Ok(Entry {\n        key: PyString::new(py, &entry.key).into(),\n        variables: vars.into_pyobject(py)?.into(),\n        substitutions: substitutions.into(),\n        _tokens: entry.tokens,\n        _end_pos: entry.end_pos,\n        num_variables: entry.num_variables,\n        orig_entry,\n    })\n}\n\nfn value_to_py(py: Python, value: &Value, decimal_cls: &Bound<PyAny>) -> PyResult<Py<PyAny>> {\n    let v = match value {\n        Value::Str(ref v) => PyString::new(py, v).into_any(),\n        Value::Int32(v) => v.into_pyobject(py)?.into_any(),\n        Value::Int64(v) => v.into_pyobject(py)?.into_any(),\n        Value::Decimal(v) => decimal_cls.call((v.as_str(),), None)?.into_any(),\n        Value::BigInt(ref v) => PyType::new::<PyInt>(py)\n            .call((v.as_str(),), None)?\n            .into_any(),\n        Value::Boolean(b) => b.into_pyobject(py)?.to_owned().into_any(),\n    };\n    Ok(v.into())\n}\n"
  },
  {
    "path": "edb/graphql-rewrite/src/py_exception.rs",
    "content": "use pyo3::{create_exception, exceptions::PyException, PyErr};\n\nuse crate::rewrite::Error;\n\ncreate_exception!(_graphql_rewrite, LexingError, PyException);\n\ncreate_exception!(_graphql_rewrite, SyntaxError, PyException);\n\ncreate_exception!(_graphql_rewrite, NotFoundError, PyException);\n\ncreate_exception!(_graphql_rewrite, AssertionError, PyException);\n\ncreate_exception!(_graphql_rewrite, QueryError, PyException);\n\npub fn convert_error(error: Error) -> PyErr {\n    match error {\n        Error::Lexing(e) => LexingError::new_err(e),\n        Error::Syntax(e) => SyntaxError::new_err(e.to_string()),\n        Error::NotFound(e) => NotFoundError::new_err(e),\n        Error::Query(e) => QueryError::new_err(e),\n        Error::Assertion(e) => AssertionError::new_err(e),\n    }\n}\n"
  },
  {
    "path": "edb/graphql-rewrite/src/py_token.rs",
    "content": "use edb_graphql_parser::common::{unquote_block_string, unquote_string};\nuse edb_graphql_parser::position::Pos;\nuse edb_graphql_parser::tokenizer::Token;\nuse pyo3::prelude::*;\nuse pyo3::types::{PyList, PyString, PyTuple};\nuse std::borrow::Cow;\n\nuse crate::py_exception::LexingError;\nuse crate::rewrite::Error;\n\n#[derive(Debug, PartialEq, Copy, Clone, serde::Serialize, serde::Deserialize)]\npub enum PyTokenKind {\n    Sof,\n    Eof,\n    Bang,\n    Dollar,\n    ParenL,\n    ParenR,\n    Spread,\n    Colon,\n    Equals,\n    At,\n    BracketL,\n    BracketR,\n    BraceL,\n    Pipe,\n    BraceR,\n    Name,\n    Int,\n    Float,\n    String,\n    BlockString,\n}\n\n#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]\npub struct PyToken {\n    pub kind: PyTokenKind,\n    pub value: Cow<'static, str>,\n    pub position: Option<Pos>,\n}\n\nimpl PyToken {\n    pub fn new((token, position): &(Token<'_>, Pos)) -> Result<PyToken, Error> {\n        use edb_graphql_parser::tokenizer::Kind::*;\n        use PyTokenKind as T;\n\n        let (kind, value) = match (token.kind, token.value) {\n            (IntValue, val) => (T::Int, Cow::Owned(val.into())),\n            (FloatValue, val) => (T::Float, Cow::Owned(val.into())),\n            (StringValue, val) => (T::String, Cow::Owned(val.into())),\n            (BlockString, val) => (T::BlockString, Cow::Owned(val.into())),\n            (Name, val) => (T::Name, Cow::Owned(val.into())),\n            (Punctuator, \"!\") => (T::Bang, \"!\".into()),\n            (Punctuator, \"$\") => (T::Dollar, \"$\".into()),\n            (Punctuator, \"(\") => (T::ParenL, \"(\".into()),\n            (Punctuator, \")\") => (T::ParenR, \")\".into()),\n            (Punctuator, \"...\") => (T::Spread, \"...\".into()),\n            (Punctuator, \":\") => (T::Colon, \":\".into()),\n            (Punctuator, \"=\") => (T::Equals, \"=\".into()),\n            (Punctuator, \"@\") => (T::At, \"@\".into()),\n            (Punctuator, \"[\") => (T::BracketL, \"[\".into()),\n            (Punctuator, \"]\") => (T::BracketR, \"]\".into()),\n            (Punctuator, \"{\") => (T::BraceL, \"{\".into()),\n            (Punctuator, \"}\") => (T::BraceR, \"}\".into()),\n            (Punctuator, \"|\") => (T::Pipe, \"|\".into()),\n            (Punctuator, _) => Err(Error::Assertion(\"unsupported punctuator\".into()))?,\n        };\n        Ok(PyToken {\n            kind,\n            value,\n            position: Some(*position),\n        })\n    }\n}\n\npub fn convert_tokens<'py>(\n    py: Python<'py>,\n    tokens: &[PyToken],\n    end_pos: &Pos,\n    kinds: Py<PyAny>,\n) -> PyResult<impl IntoPyObject<'py>> {\n    use PyTokenKind as K;\n\n    let sof = kinds.getattr(py, \"SOF\")?;\n    let eof = kinds.getattr(py, \"EOF\")?;\n    let bang = kinds.getattr(py, \"BANG\")?;\n    let bang_v = \"!\".into_pyobject(py)?;\n    let dollar = kinds.getattr(py, \"DOLLAR\")?;\n    let dollar_v = \"$\".into_pyobject(py)?;\n    let paren_l = kinds.getattr(py, \"PAREN_L\")?;\n    let paren_l_v = \"(\".into_pyobject(py)?;\n    let paren_r = kinds.getattr(py, \"PAREN_R\")?;\n    let paren_r_v = \")\".into_pyobject(py)?;\n    let spread = kinds.getattr(py, \"SPREAD\")?;\n    let spread_v = \"...\".into_pyobject(py)?;\n    let colon = kinds.getattr(py, \"COLON\")?;\n    let colon_v = \":\".into_pyobject(py)?;\n    let equals = kinds.getattr(py, \"EQUALS\")?;\n    let equals_v = \"=\".into_pyobject(py)?;\n    let at = kinds.getattr(py, \"AT\")?;\n    let at_v = \"@\".into_pyobject(py)?;\n    let bracket_l = kinds.getattr(py, \"BRACKET_L\")?;\n    let bracket_l_v = \"[\".into_pyobject(py)?;\n    let bracket_r = kinds.getattr(py, \"BRACKET_R\")?;\n    let bracket_r_v = \"]\".into_pyobject(py)?;\n    let brace_l = kinds.getattr(py, \"BRACE_L\")?;\n    let brace_l_v = \"{\".into_pyobject(py)?;\n    let pipe = kinds.getattr(py, \"PIPE\")?;\n    let pipe_v = \"|\".into_pyobject(py)?;\n    let brace_r = kinds.getattr(py, \"BRACE_R\")?;\n    let brace_r_v = \"}\".into_pyobject(py)?;\n    let name = kinds.getattr(py, \"NAME\")?;\n    let int = kinds.getattr(py, \"INT\")?;\n    let float = kinds.getattr(py, \"FLOAT\")?;\n    let string = kinds.getattr(py, \"STRING\")?;\n    let block_string = kinds.getattr(py, \"BLOCK_STRING\")?;\n\n    let mut elems: Vec<Py<PyAny>> = Vec::with_capacity(tokens.len());\n\n    let zero = 0u32.into_pyobject(py).unwrap();\n    let start_of_file = [\n        sof.clone_ref(py),\n        zero.clone().into(),\n        zero.clone().into(),\n        zero.clone().into(),\n        zero.clone().into(),\n        py.None(),\n    ];\n    elems.push(PyTuple::new(py, &start_of_file)?.into());\n\n    for token in tokens {\n        let (kind, value) = match token.kind {\n            K::Sof => (sof.clone_ref(py), py.None()),\n            K::Eof => (eof.clone_ref(py), py.None()),\n            K::Bang => (bang.clone_ref(py), bang_v.to_owned().into()),\n            K::Dollar => (dollar.clone_ref(py), dollar_v.to_owned().into()),\n            K::ParenL => (paren_l.clone_ref(py), paren_l_v.to_owned().into()),\n            K::ParenR => (paren_r.clone_ref(py), paren_r_v.to_owned().into()),\n            K::Spread => (spread.clone_ref(py), spread_v.to_owned().into()),\n            K::Colon => (colon.clone_ref(py), colon_v.to_owned().into()),\n            K::Equals => (equals.clone_ref(py), equals_v.to_owned().into()),\n            K::At => (at.clone_ref(py), at_v.to_owned().into()),\n            K::BracketL => (bracket_l.clone_ref(py), bracket_l_v.to_owned().into()),\n            K::BracketR => (bracket_r.clone_ref(py), bracket_r_v.to_owned().into()),\n            K::BraceL => (brace_l.clone_ref(py), brace_l_v.to_owned().into()),\n            K::Pipe => (pipe.clone_ref(py), pipe_v.to_owned().into()),\n            K::BraceR => (brace_r.clone_ref(py), brace_r_v.to_owned().into()),\n            K::Name => (name.clone_ref(py), PyString::new(py, &token.value).into()),\n            K::Int => (int.clone_ref(py), PyString::new(py, &token.value).into()),\n            K::Float => (float.clone_ref(py), PyString::new(py, &token.value).into()),\n            K::String => {\n                // graphql-core 3 receives unescaped strings from the lexer\n                let v = unquote_string(&token.value)\n                    .map_err(|e| LexingError::new_err(e.to_string()))?\n                    .into_pyobject(py)?;\n                (string.clone_ref(py), v.to_owned().into())\n            }\n            K::BlockString => {\n                // graphql-core 3 receives unescaped strings from the lexer\n                let v = unquote_block_string(&token.value)\n                    .map_err(|e| LexingError::new_err(e.to_string()))?\n                    .into_pyobject(py)?;\n                (block_string.clone_ref(py), v.to_owned().into())\n            }\n        };\n        let token_tuple = (\n            kind,\n            token.position.map(|x| x.character),\n            token\n                .position\n                .map(|x| x.character + token.value.chars().count()),\n            token.position.map(|x| x.line),\n            token.position.map(|x| x.column),\n            value,\n        )\n            .into_pyobject(py)?;\n        elems.push(token_tuple.into());\n    }\n    elems.push(\n        (\n            eof,\n            end_pos.character,\n            end_pos.line,\n            end_pos.column,\n            end_pos.character,\n            py.None(),\n        )\n            .into_pyobject(py)?\n            .into(),\n    );\n    PyList::new(py, elems)\n}\n"
  },
  {
    "path": "edb/graphql-rewrite/src/rewrite.rs",
    "content": "use std::collections::{BTreeMap, HashSet};\n\nuse combine::stream::{Positioned, StreamOnce};\n\nuse edb_graphql_parser::common::{unquote_string, Type, Value as GqlValue};\nuse edb_graphql_parser::position::Pos;\nuse edb_graphql_parser::query::{parse_query, Document, ParseError};\nuse edb_graphql_parser::query::{Definition, Directive};\nuse edb_graphql_parser::query::{InsertVars, InsertVarsKind, Operation};\nuse edb_graphql_parser::tokenizer::Kind::{BlockString, StringValue};\nuse edb_graphql_parser::tokenizer::Kind::{FloatValue, IntValue};\nuse edb_graphql_parser::tokenizer::Kind::{Name, Punctuator};\nuse edb_graphql_parser::tokenizer::{Token, TokenStream};\nuse edb_graphql_parser::visitor::Visit;\n\nuse crate::py_token::{PyToken, PyTokenKind};\nuse crate::token_vec::TokenVec;\n\n#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]\npub enum Value {\n    Str(String),\n    Int32(i32),\n    Int64(i64),\n    BigInt(String),\n    Decimal(String),\n    Boolean(bool),\n}\n\n#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]\npub struct Variable {\n    pub value: Value,\n    pub token: PyToken,\n}\n\n#[derive(Debug)]\npub enum Error {\n    Lexing(String),\n    Syntax(ParseError),\n    NotFound(String),\n    Assertion(String),\n    Query(String),\n}\n\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct Entry {\n    pub key: String,\n    pub variables: Vec<Variable>,\n    pub defaults: BTreeMap<String, Variable>,\n    pub tokens: Vec<PyToken>,\n    pub end_pos: Pos,\n    pub num_variables: usize,\n}\n\npub fn rewrite(operation: Option<&str>, s: &str) -> Result<Entry, Error> {\n    use crate::py_token::PyTokenKind as P;\n    use edb_graphql_parser::query::Value as G;\n    use Value::*;\n\n    let document: Document<'_, &str> = parse_query(s).map_err(Error::Syntax)?;\n    let oper = if let Some(oper_name) = operation {\n        find_operation(&document, oper_name)\n            .ok_or_else(|| Error::NotFound(format!(\"no operation {operation:?} found\")))?\n    } else {\n        let mut oper = None;\n        for def in &document.definitions {\n            match def {\n                Definition::Operation(ref op) => {\n                    if oper.is_some() {\n                        Err(Error::NotFound(\n                            \"Multiple operations \\\n                            found. Please specify operation name\"\n                                .into(),\n                        ))?;\n                    } else {\n                        oper = Some(op);\n                    }\n                }\n                _ => continue,\n            };\n        }\n        oper.ok_or_else(|| Error::NotFound(\"no operation found\".into()))?\n    };\n    let (all_src_tokens, end_pos) = token_array(s)?;\n    let mut src_tokens = TokenVec::new(&all_src_tokens);\n    let mut tokens = Vec::with_capacity(src_tokens.len());\n\n    let mut variables = Vec::new();\n    let mut defaults = BTreeMap::new();\n    let mut value_positions = HashSet::new();\n\n    visit_directives(&mut value_positions, oper);\n\n    for var in &oper.variable_definitions {\n        if var.name.starts_with(\"__edb_arg_\") {\n            return Err(Error::Query(\n                \"Variables starting with '__edb_arg_' are prohibited\".into(),\n            ));\n        }\n        if let Some(ref dvalue) = var.default_value {\n            let value = match (&dvalue.value, type_name(&var.var_type)) {\n                (G::String(ref s), Some(\"String\")) => Str(s.clone()),\n                (G::Int(ref s), Some(\"Int\")) | (G::Int(ref s), Some(\"Int32\")) => {\n                    let value = match s.as_i64() {\n                        Some(v) if v <= i32::MAX as i64 && v >= i32::MIN as i64 => v,\n                        // Ignore bad values. Let graphql solver handle that\n                        _ => continue,\n                    };\n                    Int32(value as i32)\n                }\n                (G::Int(ref s), Some(\"Int64\")) => {\n                    let value = match s.as_i64() {\n                        Some(v) => v,\n                        // Ignore bad values. Let graphql solver handle that\n                        _ => continue,\n                    };\n                    Int64(value)\n                }\n                (G::Int(ref s), Some(\"Bigint\")) => BigInt(s.as_bigint().to_string()),\n                (G::Float(s), Some(\"Float\")) => Decimal(s.clone()),\n                (G::Float(s), Some(\"Decimal\")) => Decimal(s.clone()),\n                (G::Boolean(s), Some(\"Boolean\")) => Boolean(*s),\n                // other types are unsupported\n                _ => continue,\n            };\n            for tok in src_tokens.drain_to(dvalue.span.0.token) {\n                tokens.push(PyToken::new(tok)?);\n            }\n            if !matches!(var.var_type, Type::NonNullType(..)) {\n                tokens.push(PyToken {\n                    kind: P::Bang,\n                    value: \"!\".into(),\n                    position: None,\n                });\n            }\n            // first token is needed for errors, others are discarded\n            let pair = src_tokens\n                .drain_to(dvalue.span.1.token)\n                .next()\n                .expect(\"at least one token of default value\");\n            defaults.insert(\n                var.name.to_owned(),\n                Variable {\n                    value,\n                    token: PyToken::new(pair)?,\n                },\n            );\n        }\n    }\n    for tok in src_tokens.drain_to(oper.insert_variables.position.token) {\n        tokens.push(PyToken::new(tok)?);\n    }\n    let mut args = Vec::new();\n    let mut tmp = Vec::with_capacity(oper.selection_set.span.1.token - tokens.len());\n    for tok in src_tokens.drain_to(oper.selection_set.span.0.token) {\n        tmp.push(PyToken::new(tok)?);\n    }\n    for (token, pos) in src_tokens.drain_to(oper.selection_set.span.1.token) {\n        match token.kind {\n            StringValue | BlockString => {\n                let var_name = format!(\"__edb_arg_{}\", variables.len());\n                tmp.push(PyToken {\n                    kind: P::Dollar,\n                    value: \"$\".into(),\n                    position: None,\n                });\n                tmp.push(PyToken {\n                    kind: P::Name,\n                    value: var_name.clone().into(),\n                    position: None,\n                });\n                variables.push(Variable {\n                    token: PyToken::new(&(*token, *pos))?,\n                    value: Str(unquote_string(token.value)?),\n                });\n                push_var_definition(&mut args, &var_name, \"String\");\n                continue;\n            }\n            IntValue => {\n                if token.value == \"1\"\n                    && pos.token > 2\n                    && all_src_tokens[pos.token - 1].0.kind == Punctuator\n                    && all_src_tokens[pos.token - 1].0.value == \":\"\n                    && all_src_tokens[pos.token - 2].0.kind == Name\n                    && all_src_tokens[pos.token - 2].0.value == \"first\"\n                {\n                    // skip `first: 1` as this is used to fetch singleton\n                    // properties from queries where literal `LIMIT 1`\n                    // should be present\n                    tmp.push(PyToken::new(&(*token, *pos))?);\n                    continue;\n                }\n                let var_name = format!(\"__edb_arg_{}\", variables.len());\n                tmp.push(PyToken {\n                    kind: P::Dollar,\n                    value: \"$\".into(),\n                    position: None,\n                });\n                tmp.push(PyToken {\n                    kind: P::Name,\n                    value: var_name.clone().into(),\n                    position: None,\n                });\n                let (value, typ) = if let Ok(val) = token.value.parse::<i64>() {\n                    if val <= i32::MAX as i64 && val >= i32::MIN as i64 {\n                        (Value::Int32(val as i32), \"Int\")\n                    } else {\n                        (Value::Int64(val), \"Int64\")\n                    }\n                } else {\n                    (Value::BigInt(token.value.into()), \"Bigint\")\n                };\n                variables.push(Variable {\n                    token: PyToken::new(&(*token, *pos))?,\n                    value,\n                });\n                push_var_definition(&mut args, &var_name, typ);\n                continue;\n            }\n            FloatValue => {\n                let var_name = format!(\"__edb_arg_{}\", variables.len());\n                tmp.push(PyToken {\n                    kind: P::Dollar,\n                    value: \"$\".into(),\n                    position: None,\n                });\n                tmp.push(PyToken {\n                    kind: P::Name,\n                    value: var_name.clone().into(),\n                    position: None,\n                });\n                variables.push(Variable {\n                    token: PyToken::new(&(*token, *pos))?,\n                    value: Value::Decimal(token.value.to_string()),\n                });\n                push_var_definition(&mut args, &var_name, \"Decimal\");\n                continue;\n            }\n            Name if token.value == \"true\" || token.value == \"false\" => {\n                let var_name = format!(\"__edb_arg_{}\", variables.len());\n                tmp.push(PyToken {\n                    kind: P::Dollar,\n                    value: \"$\".into(),\n                    position: None,\n                });\n                tmp.push(PyToken {\n                    kind: P::Name,\n                    value: var_name.clone().into(),\n                    position: None,\n                });\n                variables.push(Variable {\n                    token: PyToken::new(&(*token, *pos))?,\n                    value: Value::Boolean(token.value == \"true\"),\n                });\n                push_var_definition(&mut args, &var_name, \"Boolean\");\n                continue;\n            }\n            _ => {}\n        }\n        tmp.push(PyToken::new(&(*token, *pos))?);\n    }\n    insert_args(&mut tokens, &oper.insert_variables, args);\n    tokens.extend(tmp);\n\n    for tok in src_tokens.drain(src_tokens.len()) {\n        tokens.push(PyToken::new(tok)?);\n    }\n\n    Ok(Entry {\n        key: join_tokens(&tokens),\n        variables,\n        defaults,\n        tokens,\n        end_pos,\n        num_variables: oper.variable_definitions.len(),\n    })\n}\n\nimpl From<ParseError> for Error {\n    fn from(v: ParseError) -> Error {\n        Error::Syntax(v)\n    }\n}\n\nimpl<'a> From<combine::easy::Error<Token<'a>, Token<'a>>> for Error {\n    fn from(v: combine::easy::Error<Token<'a>, Token<'a>>) -> Error {\n        Error::Lexing(v.to_string())\n    }\n}\n\nfn token_array(s: &str) -> Result<(Vec<(Token, Pos)>, Pos), Error> {\n    let mut lexer = TokenStream::new(s);\n    let mut tokens = Vec::new();\n    let mut pos = lexer.position();\n    loop {\n        match lexer.uncons() {\n            Ok(token) => {\n                tokens.push((token, pos));\n                pos = lexer.position();\n            }\n            Err(ref e) if e == &combine::easy::Error::end_of_input() => break,\n            Err(e) => panic!(\"Parse error at {}: {}\", lexer.position(), e),\n        }\n    }\n    Ok((tokens, lexer.position()))\n}\n\nfn find_operation<'a>(\n    document: &'a Document<'a, &'a str>,\n    operation: &str,\n) -> Option<&'a Operation<'a, &'a str>> {\n    for def in &document.definitions {\n        let res = match def {\n            Definition::Operation(ref op) if op.name == Some(operation) => op,\n            _ => continue,\n        };\n        return Some(res);\n    }\n    None\n}\n\nfn insert_args(dest: &mut Vec<PyToken>, ins: &InsertVars, args: Vec<PyToken>) {\n    use crate::py_token::PyTokenKind as P;\n\n    if args.is_empty() {\n        return;\n    }\n    if ins.kind == InsertVarsKind::Query {\n        dest.push(PyToken {\n            kind: P::Name,\n            value: \"query\".into(),\n            position: None,\n        });\n    }\n    if ins.kind != InsertVarsKind::Normal {\n        dest.push(PyToken {\n            kind: P::ParenL,\n            value: \"(\".into(),\n            position: None,\n        });\n    }\n    dest.extend(args);\n    if ins.kind != InsertVarsKind::Normal {\n        dest.push(PyToken {\n            kind: P::ParenR,\n            value: \")\".into(),\n            position: None,\n        });\n    }\n}\n\nfn type_name<'x>(var_type: &'x Type<'x, &'x str>) -> Option<&'x str> {\n    match var_type {\n        Type::NamedType(t) => Some(t),\n        Type::NonNullType(b) => type_name(b),\n        _ => None,\n    }\n}\n\nfn push_var_definition(args: &mut Vec<PyToken>, var_name: &str, var_type: &'static str) {\n    use crate::py_token::PyTokenKind as P;\n\n    args.push(PyToken {\n        kind: P::Dollar,\n        value: \"$\".into(),\n        position: None,\n    });\n    args.push(PyToken {\n        kind: P::Name,\n        value: var_name.to_owned().into(),\n        position: None,\n    });\n    args.push(PyToken {\n        kind: P::Colon,\n        value: \":\".into(),\n        position: None,\n    });\n    args.push(PyToken {\n        kind: P::Name,\n        value: var_type.into(),\n        position: None,\n    });\n    args.push(PyToken {\n        kind: P::Bang,\n        value: \"!\".into(),\n        position: None,\n    });\n}\n\nfn visit_directives<'x>(value_positions: &mut HashSet<usize>, oper: &'x Operation<'x, &'x str>) {\n    for dir in oper.selection_set.visit::<Directive<_>>() {\n        if dir.name == \"include\" || dir.name == \"skip\" {\n            for arg in &dir.arguments {\n                if let GqlValue::Boolean(_) = arg.value {\n                    value_positions.insert(arg.value_position.token);\n                }\n            }\n        }\n    }\n}\n\nfn join_tokens<'a, I: IntoIterator<Item = &'a PyToken>>(tokens: I) -> String {\n    let mut buf = String::new();\n    let mut needs_whitespace = false;\n    for token in tokens {\n        match (token.kind, needs_whitespace) {\n            // space before puncutators is optional\n            (PyTokenKind::ParenL, true) => {}\n            (PyTokenKind::ParenR, true) => {}\n            (PyTokenKind::Spread, true) => {}\n            (PyTokenKind::Colon, true) => {}\n            (PyTokenKind::Equals, true) => {}\n            (PyTokenKind::At, true) => {}\n            (PyTokenKind::BracketL, true) => {}\n            (PyTokenKind::BracketR, true) => {}\n            (PyTokenKind::BraceL, true) => {}\n            (PyTokenKind::BraceR, true) => {}\n            (PyTokenKind::Pipe, true) => {}\n            (PyTokenKind::Bang, true) => {}\n            (_, true) => buf.push(' '),\n            (_, false) => {}\n        }\n        buf.push_str(&token.value);\n        needs_whitespace = match token.kind {\n            PyTokenKind::Dollar => false,\n            PyTokenKind::Bang => false,\n            PyTokenKind::ParenL => false,\n            PyTokenKind::ParenR => false,\n            PyTokenKind::Spread => false,\n            PyTokenKind::Colon => false,\n            PyTokenKind::Equals => false,\n            PyTokenKind::At => false,\n            PyTokenKind::BracketL => false,\n            PyTokenKind::BracketR => false,\n            PyTokenKind::BraceL => false,\n            PyTokenKind::BraceR => false,\n            PyTokenKind::Pipe => false,\n            PyTokenKind::Int => true,\n            PyTokenKind::Float => true,\n            PyTokenKind::String => true,\n            PyTokenKind::BlockString => true,\n            PyTokenKind::Name => true,\n            PyTokenKind::Eof => unreachable!(),\n            PyTokenKind::Sof => unreachable!(),\n        };\n    }\n    buf\n}\n"
  },
  {
    "path": "edb/graphql-rewrite/src/token_vec.rs",
    "content": "use edb_graphql_parser::position::Pos;\nuse edb_graphql_parser::tokenizer::Token;\n\npub struct TokenVec<'a> {\n    tokens: &'a Vec<(Token<'a>, Pos)>,\n    consumed: usize,\n}\n\nimpl<'a> TokenVec<'a> {\n    pub fn new(tokens: &'a Vec<(Token<'a>, Pos)>) -> TokenVec<'a> {\n        TokenVec {\n            tokens,\n            consumed: 0,\n        }\n    }\n    pub fn drain(&mut self, n: usize) -> impl Iterator<Item = &(Token, Pos)> {\n        let pos = self.consumed;\n        self.consumed += n;\n        assert!(n <= self.tokens.len(), \"attempt to more tokens than exist\");\n        self.tokens[pos..][..n].iter()\n    }\n    pub fn drain_to(&mut self, end: usize) -> impl Iterator<Item = &(Token, Pos)> {\n        let n = end\n            .checked_sub(self.consumed)\n            .expect(\"drain_to with index smaller than current\");\n        self.drain(n)\n    }\n    pub fn len(&self) -> usize {\n        self.tokens\n            .len()\n            .checked_sub(self.consumed)\n            .expect(\"consumed more tokens than exists\")\n    }\n}\n"
  },
  {
    "path": "edb/graphql-rewrite/tests/rewrite.rs",
    "content": "#![cfg(feature = \"python_extension\")]\nuse std::collections::BTreeMap;\n\nuse edb_graphql_parser::Pos;\n\nuse graphql_rewrite::{rewrite, Value, Variable};\nuse graphql_rewrite::{PyToken, PyTokenKind};\n\n#[test]\nfn test_no_args() {\n    let entry = rewrite(\n        None,\n        r#\"\n        query {\n            object(filter: {field: {eq: \"test\"}}) {\n                field\n            }\n        }\n    \"#,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query($__edb_arg_0:String!){\\\n            object(filter:{field:{eq:$__edb_arg_0}}){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![Variable {\n            token: PyToken {\n                kind: PyTokenKind::String,\n                value: r#\"\"test\"\"#.into(),\n                position: Some(Pos {\n                    line: 3,\n                    column: 41,\n                    character: 57,\n                    token: 12\n                }),\n            },\n            value: Value::Str(\"test\".into()),\n        }]\n    );\n}\n\n#[test]\nfn test_no_query() {\n    let entry = rewrite(\n        None,\n        r#\"\n        {\n            object(filter: {field: {eq: \"test\"}}) {\n                field\n            }\n        }\n    \"#,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query($__edb_arg_0:String!){\\\n            object(filter:{field:{eq:$__edb_arg_0}}){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![Variable {\n            token: PyToken {\n                kind: PyTokenKind::String,\n                value: r#\"\"test\"\"#.into(),\n                position: Some(Pos {\n                    line: 3,\n                    column: 41,\n                    character: 51,\n                    token: 11\n                }),\n            },\n            value: Value::Str(\"test\".into()),\n        }]\n    );\n}\n\n#[test]\nfn test_no_name() {\n    let entry = rewrite(\n        None,\n        r#\"\n        query($x: String) {\n            object(filter: {field: {eq: \"test\"}}, y: $x) {\n                field\n            }\n        }\n    \"#,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query($x:String $__edb_arg_0:String!){\\\n            object(filter:{field:{eq:$__edb_arg_0}}y:$x){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![Variable {\n            token: PyToken {\n                kind: PyTokenKind::String,\n                value: r#\"\"test\"\"#.into(),\n                position: Some(Pos {\n                    line: 3,\n                    column: 41,\n                    character: 69,\n                    token: 18\n                }),\n            },\n            value: Value::Str(\"test\".into()),\n        }]\n    );\n}\n\n#[test]\nfn test_name_args() {\n    let entry = rewrite(\n        Some(\"Hello\"),\n        r#\"\n        query Hello($x: String, $y: String!) {\n            object(filter: {field: {eq: \"test\"}}, x: $x, y: $y) {\n                field\n            }\n        }\n    \"#,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query Hello($x:String $y:String!$__edb_arg_0:String!){\\\n            object(filter:{field:{eq:$__edb_arg_0}}x:$x y:$y){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![Variable {\n            token: PyToken {\n                kind: PyTokenKind::String,\n                value: r#\"\"test\"\"#.into(),\n                position: Some(Pos {\n                    line: 3,\n                    column: 41,\n                    character: 88,\n                    token: 24\n                }),\n            },\n            value: Value::Str(\"test\".into()),\n        }]\n    );\n}\n\n#[test]\nfn test_name() {\n    let entry = rewrite(\n        Some(\"Hello\"),\n        r#\"\n        query Hello {\n            object(filter: {field: {eq: \"test\"}}) {\n                field\n            }\n        }\n    \"#,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query Hello($__edb_arg_0:String!){\\\n            object(filter:{field:{eq:$__edb_arg_0}}){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![Variable {\n            token: PyToken {\n                kind: PyTokenKind::String,\n                value: r#\"\"test\"\"#.into(),\n                position: Some(Pos {\n                    line: 3,\n                    column: 41,\n                    character: 63,\n                    token: 13\n                }),\n            },\n            value: Value::Str(\"test\".into()),\n        }]\n    );\n}\n\n#[test]\nfn test_default_name() {\n    let entry = rewrite(\n        None,\n        r#\"\n        query Hello {\n            object(filter: {field: {eq: \"test\"}}) {\n                field\n            }\n        }\n    \"#,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query Hello($__edb_arg_0:String!){\\\n            object(filter:{field:{eq:$__edb_arg_0}}){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![Variable {\n            token: PyToken {\n                kind: PyTokenKind::String,\n                value: r#\"\"test\"\"#.into(),\n                position: Some(Pos {\n                    line: 3,\n                    column: 41,\n                    character: 63,\n                    token: 13\n                }),\n            },\n            value: Value::Str(\"test\".into()),\n        }]\n    );\n}\n\n#[test]\nfn test_other() {\n    let entry = rewrite(\n        Some(\"Hello\"),\n        r#\"\n        query Other {\n            object(filter: {field: {eq: \"test1\"}}) {\n                field\n            }\n        }\n        query Hello {\n            object(filter: {field: {eq: \"test2\"}}) {\n                field\n            }\n        }\n    \"#,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query Other{\\\n            object(filter:{field:{eq:\\\"test1\\\"}}){\\\n                field\\\n            }\\\n        }\\\n        query Hello($__edb_arg_0:String!){\\\n            object(filter:{field:{eq:$__edb_arg_0}}){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![Variable {\n            token: PyToken {\n                kind: PyTokenKind::String,\n                value: r#\"\"test2\"\"#.into(),\n                position: Some(Pos {\n                    line: 8,\n                    column: 41,\n                    character: 184,\n                    token: 34\n                }),\n            },\n            value: Value::Str(\"test2\".into()),\n        }]\n    );\n}\n\n#[test]\nfn test_defaults() {\n    let entry = rewrite(\n        Some(\"Hello\"),\n        r#\"\n        query Hello($x: String = \"xxx\", $y: String! = \"yyy\") {\n            object(filter: {field: {eq: \"test\"}}, x: $x, y: $y) {\n                field\n            }\n        }\n    \"#,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query Hello($x:String!$y:String!$__edb_arg_0:String!){\\\n            object(filter:{field:{eq:$__edb_arg_0}}x:$x y:$y){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    let mut defaults = BTreeMap::new();\n    defaults.insert(\n        \"x\".to_owned(),\n        Variable {\n            value: Value::Str(\"xxx\".into()),\n            token: PyToken {\n                kind: PyTokenKind::Equals,\n                value: \"=\".into(),\n                position: Some(Pos {\n                    line: 2,\n                    column: 32,\n                    character: 32,\n                    token: 7,\n                }),\n            },\n        },\n    );\n    defaults.insert(\n        \"y\".to_owned(),\n        Variable {\n            value: Value::Str(\"yyy\".into()),\n            token: PyToken {\n                kind: PyTokenKind::Equals,\n                value: \"=\".into(),\n                position: Some(Pos {\n                    line: 2,\n                    column: 53,\n                    character: 53,\n                    token: 14,\n                }),\n            },\n        },\n    );\n    assert_eq!(entry.defaults, defaults);\n}\n\n#[test]\nfn test_int32() {\n    let entry = rewrite(\n        None,\n        r###\"\n        query {\n            object(filter: {field: {eq: 17}}) {\n                field\n            }\n        }\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query($__edb_arg_0:Int!){\\\n            object(filter:{field:{eq:$__edb_arg_0}}){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![Variable {\n            token: PyToken {\n                kind: PyTokenKind::Int,\n                value: r#\"17\"#.into(),\n                position: Some(Pos {\n                    line: 3,\n                    column: 41,\n                    character: 57,\n                    token: 12\n                }),\n            },\n            value: Value::Int32(17),\n        }]\n    );\n}\n\n#[test]\nfn test_int64() {\n    let entry = rewrite(\n        None,\n        r###\"\n        query {\n            object(filter: {field: {eq: 17123456790}}) {\n                field\n            }\n        }\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query($__edb_arg_0:Int64!){\\\n            object(filter:{field:{eq:$__edb_arg_0}}){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![Variable {\n            token: PyToken {\n                kind: PyTokenKind::Int,\n                value: r#\"17123456790\"#.into(),\n                position: Some(Pos {\n                    line: 3,\n                    column: 41,\n                    character: 57,\n                    token: 12\n                }),\n            },\n            value: Value::Int64(17123456790),\n        }]\n    );\n}\n\n#[test]\nfn test_bigint() {\n    let entry = rewrite(\n        None,\n        r###\"\n        query {\n            object(filter: {field: {eq: 171234567901234567890}}) {\n                field\n            }\n        }\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query($__edb_arg_0:Bigint!){\\\n            object(filter:{field:{eq:$__edb_arg_0}}){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![Variable {\n            token: PyToken {\n                kind: PyTokenKind::Int,\n                value: r#\"171234567901234567890\"#.into(),\n                position: Some(Pos {\n                    line: 3,\n                    column: 41,\n                    character: 57,\n                    token: 12\n                }),\n            },\n            value: Value::BigInt(\"171234567901234567890\".into()),\n        }]\n    );\n}\n\n#[test]\nfn test_first_1() {\n    let entry = rewrite(\n        None,\n        r###\"\n        query {\n            object(filter: {field: {eq: 1}}, first: 1) {\n                field\n            }\n        }\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query($__edb_arg_0:Int!){\\\n            object(filter:{field:{eq:$__edb_arg_0}}first:1){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![Variable {\n            token: PyToken {\n                kind: PyTokenKind::Int,\n                value: r#\"1\"#.into(),\n                position: Some(Pos {\n                    line: 3,\n                    column: 41,\n                    character: 57,\n                    token: 12\n                }),\n            },\n            value: Value::Int32(1),\n        }]\n    );\n}\n\n#[test]\nfn test_first_2() {\n    let entry = rewrite(\n        None,\n        r###\"\n        query {\n            object(filter: {field: {eq: 1}}, first: 2) {\n                field\n            }\n        }\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query($__edb_arg_0:Int!$__edb_arg_1:Int!){\\\n            object(filter:{field:{eq:$__edb_arg_0}}first:$__edb_arg_1){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![\n            Variable {\n                token: PyToken {\n                    kind: PyTokenKind::Int,\n                    value: r#\"1\"#.into(),\n                    position: Some(Pos {\n                        line: 3,\n                        column: 41,\n                        character: 57,\n                        token: 12\n                    }),\n                },\n                value: Value::Int32(1),\n            },\n            Variable {\n                token: PyToken {\n                    kind: PyTokenKind::Int,\n                    value: r#\"2\"#.into(),\n                    position: Some(Pos {\n                        line: 3,\n                        column: 53,\n                        character: 69,\n                        token: 17\n                    }),\n                },\n                value: Value::Int32(2),\n            },\n        ]\n    );\n}\n\n#[test]\nfn test_defaults_int() {\n    let entry = rewrite(\n        Some(\"Hello\"),\n        r###\"\n        query Hello($x: Int = 123, $y: Int! = 1234) {\n            object(x: $x, y: $y) {\n                field\n            }\n        }\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query Hello($x:Int!$y:Int!){\\\n            object(x:$x y:$y){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    let mut defaults = BTreeMap::new();\n    defaults.insert(\n        \"x\".to_owned(),\n        Variable {\n            value: Value::Int32(123),\n            token: PyToken {\n                kind: PyTokenKind::Equals,\n                value: \"=\".into(),\n                position: Some(Pos {\n                    line: 2,\n                    column: 29,\n                    character: 29,\n                    token: 7,\n                }),\n            },\n        },\n    );\n    defaults.insert(\n        \"y\".to_owned(),\n        Variable {\n            value: Value::Int32(1234),\n            token: PyToken {\n                kind: PyTokenKind::Equals,\n                value: \"=\".into(),\n                position: Some(Pos {\n                    line: 2,\n                    column: 45,\n                    character: 45,\n                    token: 14,\n                }),\n            },\n        },\n    );\n    assert_eq!(entry.defaults, defaults);\n}\n\n#[test]\nfn test_float() {\n    let entry = rewrite(\n        None,\n        r###\"\n        query {\n            object(filter: {field: {eq: 17.25}}) {\n                field\n            }\n        }\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query($__edb_arg_0:Decimal!){\\\n            object(filter:{field:{eq:$__edb_arg_0}}){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    assert_eq!(\n        entry.variables,\n        vec![Variable {\n            token: PyToken {\n                kind: PyTokenKind::Float,\n                value: r#\"17.25\"#.into(),\n                position: Some(Pos {\n                    line: 3,\n                    column: 41,\n                    character: 57,\n                    token: 12\n                }),\n            },\n            value: Value::Decimal(\"17.25\".into()),\n        }]\n    );\n}\n\n#[test]\nfn test_defaults_float() {\n    let entry = rewrite(\n        Some(\"Hello\"),\n        r###\"\n        query Hello($x: Float = 123.25, $y: Float! = 1234.75) {\n            object(x: $x, y: $y) {\n                field\n            }\n        }\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query Hello($x:Float!$y:Float!){\\\n            object(x:$x y:$y){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    let mut defaults = BTreeMap::new();\n    defaults.insert(\n        \"x\".to_owned(),\n        Variable {\n            value: Value::Decimal(\"123.25\".into()),\n            token: PyToken {\n                kind: PyTokenKind::Equals,\n                value: \"=\".into(),\n                position: Some(Pos {\n                    line: 2,\n                    column: 31,\n                    character: 31,\n                    token: 7,\n                }),\n            },\n        },\n    );\n    defaults.insert(\n        \"y\".to_owned(),\n        Variable {\n            value: Value::Decimal(\"1234.75\".into()),\n            token: PyToken {\n                kind: PyTokenKind::Equals,\n                value: \"=\".into(),\n                position: Some(Pos {\n                    line: 2,\n                    column: 52,\n                    character: 52,\n                    token: 14,\n                }),\n            },\n        },\n    );\n    assert_eq!(entry.defaults, defaults);\n}\n\n#[test]\nfn test_defaults_bool() {\n    let entry = rewrite(\n        Some(\"Hello\"),\n        r###\"\n        query Hello($x: Boolean = true, $y: Boolean! = false) {\n            object(x: $x, y: $y) {\n                field\n            }\n        }\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query Hello($x:Boolean!$y:Boolean!){\\\n            object(x:$x y:$y){\\\n                field\\\n            }\\\n        }\\\n    \"\n    );\n    let mut defaults = BTreeMap::new();\n    defaults.insert(\n        \"x\".to_owned(),\n        Variable {\n            value: Value::Boolean(true),\n            token: PyToken {\n                kind: PyTokenKind::Equals,\n                value: \"=\".into(),\n                position: Some(Pos {\n                    line: 2,\n                    column: 33,\n                    character: 33,\n                    token: 7,\n                }),\n            },\n        },\n    );\n    defaults.insert(\n        \"y\".to_owned(),\n        Variable {\n            value: Value::Boolean(false),\n            token: PyToken {\n                kind: PyTokenKind::Equals,\n                value: \"=\".into(),\n                position: Some(Pos {\n                    line: 2,\n                    column: 54,\n                    character: 54,\n                    token: 14,\n                }),\n            },\n        },\n    );\n    assert_eq!(entry.defaults, defaults);\n}\n\n#[test]\nfn test_include_skip() {\n    let entry = rewrite(\n        Some(\"Hello\"),\n        r###\"\n        query Hello($x: Boolean = true) {\n            object {\n                hello @include(if: $x)\n                world @skip(if: true)\n            }\n        }\n    \"###,\n    )\n    .unwrap();\n    assert_eq!(\n        entry.key,\n        \"\\\n        query Hello($x:Boolean!$__edb_arg_0:Boolean!){\\\n            object{\\\n                hello@include(if:$x)\\\n                world@skip(if:$__edb_arg_0)\\\n            }\\\n        }\\\n    \"\n    );\n    let mut defaults = BTreeMap::new();\n    defaults.insert(\n        \"x\".to_owned(),\n        Variable {\n            value: Value::Boolean(true),\n            token: PyToken {\n                kind: PyTokenKind::Equals,\n                value: \"=\".into(),\n                position: Some(Pos {\n                    line: 2,\n                    column: 33,\n                    character: 33,\n                    token: 7,\n                }),\n            },\n        },\n    );\n    assert_eq!(entry.defaults, defaults);\n    assert_eq!(\n        entry.variables,\n        vec![Variable {\n            token: PyToken {\n                kind: PyTokenKind::Name,\n                value: r#\"true\"#.into(),\n                position: Some(Pos {\n                    line: 5,\n                    column: 33,\n                    character: 135,\n                    token: 28\n                }),\n            },\n            value: Value::Boolean(true),\n        }]\n    );\n}\n"
  },
  {
    "path": "edb/ir/__init__.py",
    "content": "##\n# Copyright (c) 2008-present MagicStack Inc.\n# All rights reserved.\n#\n# See LICENSE for details.\n##\n\nfrom __future__ import annotations\n"
  },
  {
    "path": "edb/ir/ast.py",
    "content": "# mypy: implicit-reexport\n\n#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"IR expression tree node definitions.\n\nThe IR expression tree is produced by the EdgeQL compiler\n(see :mod:`edgeql.compiler`).  It is a self-contained representation\nof an EdgeQL expression, which, together with the accompanying scope tree\n(:mod:`ir.scopetree`) is sufficient to produce a backend query (e.g. SQL)\nwithout any other input or context.\n\nThe most common part of the IR expression tree is the :class:`~Set` class.\nEvery expression is encoded as a ``Set`` instance that contains all common\nmetadata, such as the expression type, its symbolic identity (PathId) and\nother useful bits.  The ``Set.expr`` field contains the specific node for\nthe expression.  The expression nodes usually refer to ``Set`` nodes\nrather than other nodes directly.\n\nFor example, the EdgeQL expression ``SELECT str_lower('ABC') ++ 'd'``\nyields the following IR (roughly):\n\nSet (\n  expr = SelectStmt (\n    result = Set (\n      expr = OperatorCall (\n        args = [\n          CallArg (\n            expr = Set (\n              expr = FunctionCall (\n                args = [\n                  CallArg (\n                    expr = Set ( expr = StringConstant ( value = 'ABC' ) ),\n                  ),\n                  CallArg (\n                    expr = Set ( expr = StringConstant ( value = 'd' ) ),\n                  )\n                ]\n              )\n            )\n          )\n        ]\n      )\n    )\n  )\n)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport abc\nimport dataclasses\nimport typing\nimport uuid\n\nfrom edb.common import ast, compiler, span, markup, enum as s_enum\n\nfrom edb import errors\n\nfrom edb.schema import modules as s_mod\nfrom edb.schema import name as sn\nfrom edb.schema import objects as so\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import permissions as s_permissions\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import schema as s_schema\nfrom edb.schema import types as s_types\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom .pathid import PathId, Namespace  # noqa\nfrom .scopetree import ScopeTreeNode  # noqa\n\n\nSpan = span.Span\n\n\ndef new_scope_tree() -> ScopeTreeNode:\n    return ScopeTreeNode(fenced=True)\n\n\nclass Base(ast.AST):\n    __abstract_node__ = True\n    __ast_hidden__ = {'span'}\n\n    span: typing.Optional[Span] = None\n\n    def __repr__(self) -> str:\n        return (\n            f'<ir.{self.__class__.__name__} at 0x{id(self):x}>'\n        )\n\n\nclass ImmutableBase(ast.ImmutableASTMixin, Base):\n    __abstract_node__ = True\n\n\nclass ViewShapeMetadata(Base):\n\n    has_implicit_id: bool = False\n\n\nclass TypeRef(ImmutableBase):\n    # Hide ancestors and children from debug spew because they are\n    # incredibly noisy.\n    __ast_hidden__ = {'ancestors', 'children'}\n\n    # The id of the referenced type\n    id: uuid.UUID\n    # Full name of the type, not necessarily schema-addressable,\n    # used for annotations only.\n    name_hint: sn.Name\n    # Name hint of the real underlying type, if the type ref was created\n    # with an explicitly specified typename.\n    orig_name_hint: typing.Optional[sn.Name] = None\n    # The ref of the underlying material type, if this is a view type,\n    # else None.\n    material_type: typing.Optional[TypeRef] = None\n    # If this is a scalar type, base_type would be the highest\n    # non-abstract base type.\n    base_type: typing.Optional[TypeRef] = None\n    # A set of type children descriptors, if necessary for\n    # this type description.\n    children: typing.Optional[frozenset[TypeRef]] = None\n    # A set of type ancestor descriptors, if necessary for\n    # this type description.\n    ancestors: typing.Optional[frozenset[TypeRef]] = None\n    # If this is a compound type, this is a non-overlapping set of\n    # constituent types.\n    union: typing.Optional[frozenset[TypeRef]] = None\n    # Whether the union is specified by an exhaustive list of\n    # types, and type inheritance should not be considered.\n    union_is_exhaustive: bool = False\n    # If this is a complex type, record the expression used to generate the\n    # type. This is used later to get the correct rvar in `get_path_var`.\n    expr_intersection: typing.Optional[frozenset[TypeRef]] = None\n    expr_union: typing.Optional[frozenset[TypeRef]] = None\n    # If this node is an element of a collection, and the\n    # collection elements are named, this would be then\n    # name of the element.\n    element_name: typing.Optional[str] = None\n    # The kind of the collection type if this is a collection\n    collection: typing.Optional[str] = None\n    # Collection subtypes if this is a collection\n    subtypes: tuple[TypeRef, ...] = ()\n    # True, if this describes a scalar type\n    is_scalar: bool = False\n    # True, if this describes a view\n    is_view: bool = False\n    # True, if this describes a cfg view\n    is_cfg_view: bool = False\n    # True, if this describes an abstract type\n    is_abstract: bool = False\n    # True, if the collection type is persisted in the schema\n    in_schema: bool = False\n    # True, if this describes an opaque union type\n    is_opaque_union: bool = False\n    # Does this need to call a custom json cast function\n    needs_custom_json_cast: bool = False\n    # If this has a schema-configured backend type, what is it\n    sql_type: typing.Optional[str] = None\n    # If this has a schema-configured custom sql serialization, what is it\n    custom_sql_serialization: typing.Optional[str] = None\n\n    def __repr__(self) -> str:\n        return f'<ir.TypeRef \\'{self.name_hint}\\' at 0x{id(self):x}>'\n\n    @property\n    def real_material_type(self) -> TypeRef:\n        return self.material_type or self\n\n    @property\n    def real_base_type(self) -> TypeRef:\n        return self.base_type or self\n\n    def __eq__(self, other: object) -> bool:\n        if not isinstance(other, self.__class__):\n            return False\n\n        return self.id == other.id\n\n    def __hash__(self) -> int:\n        return hash(self.id)\n\n\nclass AnyTypeRef(TypeRef):\n    pass\n\n\nclass AnyTupleRef(TypeRef):\n    pass\n\n\nclass AnyObjectRef(TypeRef):\n    pass\n\n\nclass BasePointerRef(ImmutableBase):\n    __abstract_node__ = True\n\n    # Hide children to reduce noise\n    __ast_hidden__ = {'children'}\n\n    # cardinality fields need to be mutable for lazy cardinality inference.\n    # and children because we update pointers with newly derived children\n    __ast_mutable_fields__ = frozenset(\n        ('in_cardinality', 'out_cardinality', 'children',\n         'is_computable')\n    )\n\n    # The defaults set here are mostly to try to reduce debug spew output.\n    name: sn.QualName\n    shortname: sn.QualName\n    std_parent_name: typing.Optional[sn.QualName] = None\n    out_source: TypeRef\n    out_target: TypeRef\n    source_ptr: typing.Optional[PointerRef] = None\n    base_ptr: typing.Optional[BasePointerRef] = None\n    material_ptr: typing.Optional[BasePointerRef] = None\n    children: frozenset[BasePointerRef] = frozenset()\n    union_components: typing.Optional[set[BasePointerRef]] = None\n    intersection_components: typing.Optional[set[BasePointerRef]] = None\n    union_is_exhaustive: bool = False\n    has_properties: bool = False\n    is_derived: bool = False\n    is_computable: bool = False\n    # Outbound cardinality of the pointer.\n    out_cardinality: qltypes.Cardinality\n    # Inbound cardinality of the pointer.\n    in_cardinality: qltypes.Cardinality = qltypes.Cardinality.MANY\n    defined_here: bool = False\n    computed_link_alias: typing.Optional[BasePointerRef] = None\n    computed_link_alias_is_backward: typing.Optional[bool] = None\n\n    def dir_target(self, direction: s_pointers.PointerDirection) -> TypeRef:\n        if direction is s_pointers.PointerDirection.Outbound:\n            return self.out_target\n        else:\n            return self.out_source\n\n    def dir_source(self, direction: s_pointers.PointerDirection) -> TypeRef:\n        if direction is s_pointers.PointerDirection.Outbound:\n            return self.out_source\n        else:\n            return self.out_target\n\n    def dir_cardinality(\n        self, direction: s_pointers.PointerDirection\n    ) -> qltypes.Cardinality:\n        if direction is s_pointers.PointerDirection.Outbound:\n            return self.out_cardinality\n        else:\n            return self.in_cardinality\n\n    @property\n    def required(self) -> bool:\n        return self.out_cardinality.to_schema_value()[0]\n\n    def descendants(self) -> set[BasePointerRef]:\n        res = set(self.children)\n        for child in self.children:\n            res.update(child.descendants())\n        return res\n\n    @property\n    def real_material_ptr(self) -> BasePointerRef:\n        return self.material_ptr or self\n\n    @property\n    def real_base_ptr(self) -> BasePointerRef:\n        return self.base_ptr or self\n\n    def __repr__(self) -> str:\n        return f'<ir.{type(self).__name__} \\'{self.name}\\' at 0x{id(self):x}>'\n\n\nclass PointerRef(BasePointerRef):\n    id: uuid.UUID\n\n\nclass ConstraintRef(ImmutableBase):\n    # The id of the constraint\n    id: uuid.UUID\n\n\nclass TupleIndirectionLink(s_pointers.PseudoPointer):\n    \"\"\"A Link-alike that can be used in tuple indirection path ids.\"\"\"\n\n    def __init__(\n        self,\n        source: so.Object,\n        target: s_types.Type,\n        *,\n        element_name: str,\n    ) -> None:\n        self._source = source\n        self._target = target\n        self._name = sn.QualName(\n            module='__tuple__', name=str(element_name))\n\n    def __hash__(self) -> int:\n        return hash((self.__class__, self._source, self._name))\n\n    def __eq__(self, other: typing.Any) -> bool:\n        if not isinstance(other, self.__class__):\n            return False\n\n        return self._source == other._source and self._name == other._name\n\n    def get_name(self, schema: s_schema.Schema) -> sn.QualName:\n        return self._name\n\n    def get_cardinality(\n        self, schema: s_schema.Schema\n    ) -> qltypes.SchemaCardinality:\n        return qltypes.SchemaCardinality.One\n\n    def singular(\n        self,\n        schema: s_schema.Schema,\n        direction: s_pointers.PointerDirection =\n            s_pointers.PointerDirection.Outbound\n    ) -> bool:\n        return True\n\n    def scalar(self) -> bool:\n        return self._target.is_scalar()\n\n    def get_source(self, schema: s_schema.Schema) -> so.Object:\n        return self._source\n\n    def get_target(self, schema: s_schema.Schema) -> s_types.Type:\n        return self._target\n\n    def is_tuple_indirection(self) -> bool:\n        return True\n\n    def get_computable(self, schema: s_schema.Schema) -> bool:\n        return False\n\n\nclass TupleIndirectionPointerRef(BasePointerRef):\n    pass\n\n\nclass SpecialPointerRef(BasePointerRef):\n    \"\"\"Pointer ref used for internal columns, such as __fts_document__\"\"\"\n    pass\n\n\nclass TypeIntersectionLink(s_pointers.PseudoPointer):\n    \"\"\"A Link-alike that can be used in type intersection path ids.\"\"\"\n\n    def __init__(\n        self,\n        source: so.Object,\n        target: s_types.Type,\n        *,\n        optional: bool,\n        is_empty: bool,\n        is_subtype: bool,\n        rptr_specialization: typing.Iterable[PointerRef] = (),\n        cardinality: qltypes.SchemaCardinality,\n    ) -> None:\n        name = 'optindirection' if optional else 'indirection'\n        self._name = sn.QualName(module='__type__', name=name)\n        self._source = source\n        self._target = target\n        self._cardinality = cardinality\n        self._optional = optional\n        self._is_empty = is_empty\n        self._is_subtype = is_subtype\n        self._rptr_specialization = frozenset(rptr_specialization)\n\n    def get_name(self, schema: s_schema.Schema) -> sn.QualName:\n        return self._name\n\n    def get_cardinality(\n        self, schema: s_schema.Schema\n    ) -> qltypes.SchemaCardinality:\n        return self._cardinality\n\n    def get_computable(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_type_intersection(self) -> bool:\n        return True\n\n    def is_optional(self) -> bool:\n        return self._optional\n\n    def is_empty(self) -> bool:\n        return self._is_empty\n\n    def is_subtype(self) -> bool:\n        return self._is_subtype\n\n    def get_rptr_specialization(self) -> frozenset[PointerRef]:\n        return self._rptr_specialization\n\n    def get_source(self, schema: s_schema.Schema) -> so.Object:\n        return self._source\n\n    def get_target(self, schema: s_schema.Schema) -> s_types.Type:\n        return self._target\n\n    def singular(\n        self,\n        schema: s_schema.Schema,\n        direction: s_pointers.PointerDirection =\n            s_pointers.PointerDirection.Outbound\n    ) -> bool:\n        if direction is s_pointers.PointerDirection.Outbound:\n            return (self.get_cardinality(schema) is\n                    qltypes.SchemaCardinality.One)\n        else:\n            return True\n\n    def scalar(self) -> bool:\n        return self._target.is_scalar()\n\n\nclass TypeIntersectionPointerRef(BasePointerRef):\n\n    optional: bool\n    is_empty: bool\n    is_subtype: bool\n    rptr_specialization: frozenset[PointerRef]\n\n\nclass Expr(Base):\n    __abstract_node__ = True\n\n    if typing.TYPE_CHECKING:\n        @property\n        @abc.abstractmethod\n        def typeref(self) -> TypeRef:\n            raise NotImplementedError\n\n    # Sets to materialize at this point, keyed by the type/ptr id.\n    materialized_sets: typing.Optional[\n        dict[uuid.UUID, MaterializedSet]] = None\n\n\nclass Pointer(Expr):\n\n    source: Set\n    ptrref: BasePointerRef\n    direction: s_pointers.PointerDirection\n    # Whether to make this an optional deref (written '.?>') that\n    # suppresses any error due to looking at a required link hidden by\n    # a policy .\n    optional_deref: bool = False\n    # Whether to *always* use a link table when this pointer is\n    # accessed.  This is needed (for example) when a (possibly single)\n    # link property is being referenced in a FOR iterator, and we\n    # aren't going to have access to the Pointer when we access\n    # the iterator variable.\n    force_link_table: bool = False\n\n    # If the pointer is a computed pointer (or a computed pointer\n    # definition), the expression.\n    expr: typing.Optional[Expr] = None\n\n    is_definition: bool\n    # Set when we have placed an rptr to help route link properties\n    # but it is not a genuine pointer use.\n    is_phony: bool = False\n    anchor: typing.Optional[str] = None\n    show_as_anchor: typing.Optional[str] = None\n\n    is_mutation: bool = False\n\n    @property\n    def is_inbound(self) -> bool:\n        return self.direction == s_pointers.PointerDirection.Inbound\n\n    @property\n    def dir_cardinality(self) -> qltypes.Cardinality:\n        return self.ptrref.dir_cardinality(self.direction)\n\n    @property\n    def typeref(self) -> TypeRef:\n        return self.ptrref.dir_target(self.direction)\n\n\nclass TypeIntersectionPointer(Pointer):\n\n    optional: bool\n    ptrref: TypeIntersectionPointerRef\n    is_definition: bool = False\n\n\nclass TupleIndirectionPointer(Pointer):\n\n    ptrref: TupleIndirectionPointerRef\n    is_definition: bool = False\n\n\nclass ImmutableExpr(Expr, ImmutableBase):\n    __abstract_node__ = True\n\n\nclass BindingKind(s_enum.StrEnum):\n    With = 'With'\n    For = 'For'\n    Select = 'Select'\n    Schema = 'Schema'\n\n\nclass TypeRoot(Expr):\n    # This will be replicated in the enclosing set.\n    typeref: TypeRef\n\n    # Whether this is a reference to a global that is cached in a\n    # materialized CTE in the query.\n    is_cached_global: bool = False\n\n    # Whether to force this to not select subtypes\n    skip_subtypes: bool = False\n\n\nclass RefExpr(Expr):\n    '''Different expressions sorts that refer to some kind of binding.'''\n    __abstract_node__ = True\n    typeref: TypeRef\n\n\nclass MaterializedExpr(RefExpr):\n    pass\n\n\nclass VisibleBindingExpr(RefExpr):\n    pass\n\n\nclass InlinedParameterExpr(RefExpr):\n    required: bool\n    is_global: bool\n\n\nT_expr_co = typing.TypeVar('T_expr_co', covariant=True, bound=Expr)\n\n\n# SetE is the base 'Set' type, and it is parameterized over what kind\n# of expression it holds. Most code uses the Set alias below, which\n# instantiates it with Expr.\n# irutils.is_set_instance can be used to refine the type.\nclass SetE(Base, typing.Generic[T_expr_co]):  # noqa: UP046\n    '''A somewhat overloaded metadata container for expressions.\n\n    Its primary purpose is to be the holder for expression metadata\n    such as path_id.\n\n    It *also* contains shape applications.\n    '''\n\n    __ast_frozen_fields__ = frozenset({'typeref'})\n\n    # N.B: Make sure to add new fields to setgen.new_set_from_set!\n\n    path_id: PathId\n    path_scope_id: typing.Optional[int] = None\n    typeref: TypeRef\n    expr: T_expr_co\n    shape: tuple[tuple[SetE[Pointer], qlast.ShapeOp], ...] = ()\n\n    anchor: typing.Optional[str] = None\n    show_as_anchor: typing.Optional[str] = None\n    # A pointer to a set nested within this one has a shape and the same\n    # typeref, if such a set exists.\n    shape_source: typing.Optional[Set] = None\n    is_binding: typing.Optional[BindingKind] = None\n    is_schema_alias: bool = False\n\n    is_materialized_ref: bool = False\n    # A ref to a visible binding (like a for iterator variable) should\n    # never need to be compiled--it should always be found. We set a\n    # flag instead of clearing expr because clearing expr can mess up\n    # card/multi inference.\n    is_visible_binding_ref: bool = False\n\n    # Whether to force this to ignore rewrites. Very dangerous!\n    # Currently for preventing duplicate explicit .id\n    # insertions to BaseObject and for ignoring other access policies\n    # inside access policy expressions.\n    #\n    # N.B: This is defined on Set and not on TypeRoot because we use the Set\n    # to join against target types on links, and to ensure rvars.\n    ignore_rewrites: bool = False\n\n    # Is this Set a dummy introduced by simple_scoping to protect a\n    # path from factoring? We track this because we try to collapse\n    # these extra scopes away when they are not needed, at the end of\n    # compilation.\n    is_factoring_protected: bool = False\n\n    def __repr__(self) -> str:\n        return f'<ir.Set \\'{self.path_id}\\' at 0x{id(self):x}>'\n\n# We set its name to Set because that's what we want visitors to use.\n\n\nSetE.__name__ = 'Set'\n\nif typing.TYPE_CHECKING:\n    Set = SetE[Expr]\nelse:\n    Set = SetE\n\n\nDUMMY_SET = Set()  # type: ignore[call-arg]\n\n\nclass Command(Base):\n    __abstract_node__ = True\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass Param:\n    \"\"\"Query parameter with its schema type and IR type\"\"\"\n\n    name: str\n    \"\"\"Parameter name\"\"\"\n\n    required: bool\n    \"\"\"Whether parameter is OPTIONAL or REQUIRED\"\"\"\n\n    schema_type: s_types.Type\n    \"\"\"Schema type\"\"\"\n\n    ir_type: TypeRef\n    \"\"\"IR type reference\"\"\"\n\n    sub_params: SubParams | None = None\n    \"\"\"Sub-parameters containing tuple components.\n\n    If the param needs to be split into multiple real postgres params\n    in order to implement tuples, this collects those parameters and\n    the decoder expression.\n    \"\"\"\n\n    @property\n    def is_sub_param(self) -> bool:\n        return (\n            self.name.startswith('__edb_decoded_') and self.name.endswith('__')\n        )\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass SubParams:\n    \"\"\"Information about sub-parameters needed for tuple components.\n\n    If the param needs to be split into multiple real postgres params\n    in order to implement tuples, this collects those parameters and\n    the decoder expression.\n    \"\"\"\n    trans_type: ParamTransType\n    decoder_edgeql: qlast.Expr\n    params: tuple[Param, ...]\n    decoder_ir: Set | None = None\n\n\n@dataclasses.dataclass(eq=False)\nclass ParamTransType:\n    \"\"\"Representation of how a tuple-containing parameter type is broken down.\n\n    The key thing here is that each node contains the index corresponding\n    to which sub-parameter that node in the argument type corresponds with.\n    See edgeql.compiler.tuple_args for details.\n\n    The reason we track this in a separate data structure (instead of just\n    having an dict from TypeRefs to indexes, say) is that TypeRefs will often\n    be shared among identical types, but we need to track different indexes\n    for different components of a type.\n    (For example, if we have an param type `tuple<str, str>`, this gets\n    decomposed into two `str` params, with indexes 0 and 1.\n    \"\"\"\n    typeref: TypeRef\n    idx: int\n\n    def flatten(self) -> tuple[typing.Any, ...]:\n        \"\"\"Flatten out the trans type into a tuple representation.\n\n        The idea here is to produce something that our inner loop in cython\n        can consume efficiently.\n        \"\"\"\n        raise NotImplementedError\n\n\n@dataclasses.dataclass(eq=False)\nclass ParamScalar(ParamTransType):\n    cast_to: typing.Optional[TypeRef] = None\n\n    def flatten(self) -> tuple[typing.Any, ...]:\n        return (int(qltypes.TypeTag.SCALAR), self.idx)\n\n\n@dataclasses.dataclass(eq=False)\nclass ParamTuple(ParamTransType):\n    typs: tuple[tuple[typing.Optional[str], ParamTransType], ...]\n\n    def flatten(self) -> tuple[typing.Any, ...]:\n        return (\n            (int(qltypes.TypeTag.TUPLE), self.idx)\n            + tuple(x.flatten() for _, x in self.typs)\n        )\n\n\n@dataclasses.dataclass(eq=False)\nclass ParamArray(ParamTransType):\n    typ: ParamTransType\n\n    def flatten(self) -> tuple[typing.Any, ...]:\n        return (int(qltypes.TypeTag.ARRAY), self.idx, self.typ.flatten())\n\n\n@dataclasses.dataclass(frozen=True)\nclass Global(Param):\n    global_name: sn.QualName\n    \"\"\"The name of the global\"\"\"\n\n    has_present_arg: bool\n    \"\"\"Whether this global needs a companion parameter indicating whether\n    the global is present.\n\n    This is needed when a global has a default but also is optional,\n    and so we need to distinguish \"unset\" and \"set to {}\".\n    \"\"\"\n\n    is_permission: bool\n    \"\"\"Whether this global comes from a Permission.\n\n    Permissions are injected directly by the server based on the connection\n    role.\n    \"\"\"\n\n\n@dataclasses.dataclass(frozen=True)\nclass ScriptInfo:\n    \"\"\"Result of preprocessing a script of multiple statements\"\"\"\n\n    params: dict[str, Param]\n    \"\"\"All parameters in all statements in the script\"\"\"\n\n    schema: s_schema.Schema\n    \"\"\"The schema after preprocessing. (Collections may have been created.)\"\"\"\n\n\nclass MaterializeVolatile(Base):\n    pass\n\n\nclass MaterializeVisible(Base):\n    __ast_hidden__ = {'sets'}\n    sets: set[tuple[PathId, Set]]\n    path_scope_id: int\n\n\n@markup.serializer.serializer.register(MaterializeVisible)\ndef _serialize_to_markup_mat_vis(\n    ir: MaterializeVisible, *, ctx: typing.Any\n) -> typing.Any:\n    # We want to show the path_ids but *not* to show the full sets\n    node = ast.serialize_to_markup(ir, ctx=ctx)\n    fixed = {(x, y.path_id) for x, y in ir.sets}\n    node.add_child(label='uses', node=markup.serialize(fixed, ctx=ctx))\n    return node\n\n\nMaterializeReason = MaterializeVolatile | MaterializeVisible\n\n\nclass ComputableInfo(typing.NamedTuple):\n\n    qlexpr: qlast.Expr\n    irexpr: typing.Optional[Set | Expr]\n    context: compiler.ContextLevel\n    path_id: PathId\n    path_id_ns: typing.Optional[Namespace]\n    shape_op: qlast.ShapeOp\n    should_materialize: typing.Sequence[MaterializeReason]\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass ServerParamConversion:\n    param_name: str\n    conversion_name: str\n    additional_info: tuple[str, ...]\n\n    # If the parameter is a query parameter, track its script params index.\n    script_param_index: typing.Optional[int] = None\n\n    # If the parameter is a constant value, pass to directly to the server.\n    constant_value: typing.Optional[typing.Any] = None\n\n\nclass Statement(Command):\n\n    expr: Set\n    views: dict[sn.Name, s_types.Type]\n    params: list[Param]\n    globals: list[Global]\n    required_permissions: set[s_permissions.Permission]\n    server_param_conversions: list[ServerParamConversion]\n    server_param_conversion_params: list[Param]\n    cardinality: qltypes.Cardinality\n    volatility: qltypes.Volatility\n    multiplicity: qltypes.Multiplicity\n    stype: s_types.Type\n    view_shapes: dict[so.Object, list[s_pointers.Pointer]]\n    view_shapes_metadata: dict[s_types.Type, ViewShapeMetadata]\n    schema: s_schema.Schema\n    schema_refs: frozenset[so.Object]\n    schema_ref_exprs: typing.Optional[\n        dict[so.Object, set[qlast.Base]]]\n    scope_tree: ScopeTreeNode\n    dml_exprs: list[qlast.Base]\n    type_rewrites: dict[tuple[uuid.UUID, bool], Set]\n    singletons: list[PathId]\n    triggers: tuple[tuple[Trigger, ...], ...]\n    warnings: tuple[errors.EdgeDBError, ...]\n    unsafe_isolation_dangers: tuple[errors.UnsafeIsolationLevelError, ...]\n\n\nclass TypeIntrospection(ImmutableExpr):\n\n    # The type value to return\n    output_typeref: TypeRef\n    # The type value *of the output*\n    typeref: TypeRef\n\n\nclass ConstExpr(Expr):\n    __abstract_node__ = True\n    typeref: TypeRef\n\n\nclass EmptySet(ConstExpr):\n    pass\n\n\nclass BaseConstant(ConstExpr, ImmutableExpr):\n    __abstract_node__ = True\n    value: typing.Any\n\n    def __init__(\n        self,\n        *args: typing.Any,\n        typeref: TypeRef,\n        **kwargs: typing.Any,\n    ) -> None:\n        super().__init__(*args, typeref=typeref, **kwargs)\n        if self.typeref is None:\n            raise ValueError('cannot create irast.Constant without a type')\n        if self.value is None:\n            raise ValueError('cannot create irast.Constant without a value')\n\n    def _init_copy(self) -> BaseConstant:\n        return self.__class__(typeref=self.typeref, value=self.value)\n\n\nclass BaseStrConstant(BaseConstant):\n    __abstract_node__ = True\n    value: str\n\n\nclass StringConstant(BaseStrConstant):\n    pass\n\n\nclass IntegerConstant(BaseStrConstant):\n    pass\n\n\nclass FloatConstant(BaseStrConstant):\n    pass\n\n\nclass DecimalConstant(BaseStrConstant):\n    pass\n\n\nclass BigintConstant(BaseStrConstant):\n    pass\n\n\nclass BooleanConstant(BaseStrConstant):\n    pass\n\n\nclass BytesConstant(BaseConstant):\n\n    value: bytes\n\n\nclass ConstantSet(ConstExpr, ImmutableExpr):\n\n    elements: tuple[BaseConstant | BaseParameter, ...]\n\n\nclass BaseParameter(ImmutableExpr):\n    __abstract_node__ = True\n\n    name: str\n    required: bool\n    typeref: TypeRef\n    # None means not a global. Otherwise, whether this is an implicitly\n    # created global for a function call.\n    is_implicit_global: typing.Optional[bool] = None\n\n    @property\n    def is_global(self) -> bool:\n        return self.is_implicit_global is not None\n\n\nclass QueryParameter(BaseParameter):\n    pass\n\n\nclass FunctionParameter(BaseParameter):\n    pass\n\n\nclass TupleElement(ImmutableBase):\n\n    name: str\n    val: Set\n    path_id: typing.Optional[PathId] = None\n\n\nclass Tuple(ImmutableExpr):\n\n    named: bool = False\n    elements: list[TupleElement]\n    typeref: TypeRef\n\n\nclass Array(ImmutableExpr):\n\n    elements: typing.Sequence[Set]\n    typeref: TypeRef\n\n\nclass TypeCheckOp(ImmutableExpr):\n\n    left: Set\n    right: TypeRef\n    op: str\n    result: typing.Optional[bool] = None\n    typeref: TypeRef\n\n\nclass SortExpr(Base):\n\n    expr: Set\n    direction: typing.Optional[qlast.SortOrder]\n    nones_order: typing.Optional[qlast.NonesOrder]\n\n\nclass CallArg(ImmutableBase):\n    \"\"\"Call argument.\"\"\"\n\n    # cardinality fields need to be mutable for lazy cardinality inference.\n    __ast_mutable_fields__ = frozenset(('cardinality', 'multiplicity'))\n\n    expr: Set\n    \"\"\"PathId for the __type__ link of object type arguments.\"\"\"\n    expr_type_path_id: typing.Optional[PathId] = None\n    cardinality: qltypes.Cardinality = qltypes.Cardinality.UNKNOWN\n    multiplicity: qltypes.Multiplicity = qltypes.Multiplicity.UNKNOWN\n    is_default: bool = False\n    param_typemod: qltypes.TypeModifier\n    polymorphism: qltypes.Polymorphism = qltypes.Polymorphism.NotUsed\n\n\nclass Call(ImmutableExpr):\n    \"\"\"Operator or a function call.\"\"\"\n    __abstract_node__ = True\n\n    # Bound callable has polymorphic parameters and\n    # a polymorphic return type.\n    func_polymorphic: bool\n\n    # Bound callable's name.\n    func_shortname: sn.QualName\n\n    # Whether the bound callable is a \"USING SQL EXPRESSION\" callable.\n    func_sql_expr: bool = False\n\n    # Whether the return value of the function should be\n    # explicitly cast into the declared function return type.\n    force_return_cast: bool\n\n    # Bound arguments.\n    # Named arguments are indexed by argument name.\n    # Positional arguments are indexed by argument position.\n    args: dict[int | str, CallArg]\n\n    # Return type and typemod.  In bodies of polymorphic functions\n    # the return type can be polymorphic; in queries the return\n    # type will be a concrete schema type.\n    typeref: TypeRef\n    typemod: qltypes.TypeModifier\n\n    # If the return type is a tuple, this will contain a list\n    # of tuple element path ids relative to the call set.\n    tuple_path_ids: list[PathId]\n\n    # Volatility of the function or operator.\n    volatility: qltypes.Volatility\n\n    # Whether the underlying implementation is strict in all its required\n    # arguments (NULL inputs lead to NULL results). If not, we need to\n    # filter at the call site.\n    impl_is_strict: bool = False\n\n    # Kind of a hack: indicates that when possible we should pass arguments\n    # to this function as a subquery-as-an-expression.\n    # See comment in schema/functions.py for more discussion.\n    prefer_subquery_args: bool = False\n\n    # If this is a set of call but is allowed in singleton expressions.\n    is_singleton_set_of: typing.Optional[bool] = None\n\n    # The polymorphism of the return type\n    # This is used to identify cases where polymorphism needs to be handled in\n    # a specialized way (eg. arrays of arrays).\n    return_polymorphism: qltypes.Polymorphism = qltypes.Polymorphism.NotUsed\n\n\nclass FunctionCall(Call):\n\n    __ast_mutable_fields__ = frozenset((\n        'extras', 'body'\n    ))\n\n    # If the bound callable is a \"USING SQL\" callable, this\n    # attribute will be set to the name of the SQL function.\n    func_sql_function: typing.Optional[str]\n\n    # initial value needed for aggregate function calls to correctly\n    # handle empty set\n    func_initial_value: typing.Optional[Set] = None\n\n    # True if the bound function has a variadic parameter and\n    # there are no arguments that are bound to it.\n    has_empty_variadic: bool = False\n\n    # The underlying SQL function has OUT parameters.\n    sql_func_has_out_params: bool = False\n\n    # backend_name for the underlying function\n    backend_name: typing.Optional[uuid.UUID] = None\n\n    # Error to raise if the underlying SQL function returns NULL.\n    error_on_null_result: typing.Optional[str] = None\n\n    # Whether the generic function preserves optionality of the generic\n    # argument(s).\n    preserves_optionality: bool = False\n\n    # Whether the generic function preserves upper cardinality of the generic\n    # argument(s).\n    preserves_upper_cardinality: bool = False\n\n    # Set to the type of the variadic parameter of the bound function\n    # (or None, if the function has no variadic parameters.)\n    variadic_param_type: typing.Optional[TypeRef] = None\n\n    # Additional arguments representing global variables\n    global_args: typing.Optional[list[Set]] = None\n\n    # Any extra information useful for compilation of special-case callables.\n    extras: typing.Optional[dict[str, typing.Any]] = None\n\n    # Inline body of the callable.\n    body: typing.Optional[Set] = None\n\n\nclass OperatorCall(Call):\n\n    # The kind of the bound operator (INFIX, PREFIX, etc.).\n    operator_kind: qltypes.OperatorKind\n\n    # If the bound callable is a \"USING SQL FUNCTION\" callable, this\n    # attribute will be set to the name of the SQL function.\n    sql_function: typing.Optional[tuple[str, ...]] = None\n\n    # If this operator maps directly onto an SQL operator, this\n    # will contain the operator name, and, optionally, backend\n    # operand types.\n    sql_operator: typing.Optional[tuple[str, ...]] = None\n\n    # The name of the origin operator if this is a derivative operator.\n    origin_name: typing.Optional[sn.QualName] = None\n\n    # The module id of the origin operator if this is a derivative operator.\n    origin_module_id: typing.Optional[uuid.UUID] = None\n\n\nclass IndexIndirection(ImmutableExpr):\n\n    expr: Base\n    index: Base\n    typeref: TypeRef\n\n\nclass SliceIndirection(ImmutableExpr):\n\n    expr: Set\n    start: typing.Optional[Base]\n    stop: typing.Optional[Base]\n    typeref: TypeRef\n\n\nclass TypeCast(ImmutableExpr):\n    \"\"\"<Type>ImmutableExpr\"\"\"\n\n    expr: Set\n    cast_name: typing.Optional[sn.QualName] = None\n    from_type: TypeRef\n    to_type: TypeRef\n    cardinality_mod: typing.Optional[qlast.CardinalityModifier] = None\n    sql_function: typing.Optional[str] = None\n    sql_cast: bool\n    sql_expr: bool\n    error_message_context: typing.Optional[str] = None\n\n    @property\n    def typeref(self) -> TypeRef:\n        return self.to_type\n\n\nclass MaterializedSet(Base):\n    # Hide uses to reduce spew; we produce our own simpler uses\n    __ast_hidden__ = {'use_sets'}\n    materialized: Set\n    reason: typing.Sequence[MaterializeReason]\n\n    # We really only want the *paths* of all the places it is used,\n    # but we need to store the sets to take advantage of weak\n    # namespace rewriting.\n    use_sets: list[Set]\n    cardinality: qltypes.Cardinality = qltypes.Cardinality.UNKNOWN\n\n    # Whether this has been \"finalized\" by stmtctx; just for supporting some\n    # assertions\n    finalized: bool = False\n\n    @property\n    def uses(self) -> list[PathId]:\n        return [x.path_id for x in self.use_sets]\n\n\n@markup.serializer.serializer.register(MaterializedSet)\ndef _serialize_to_markup_mat_set(\n    ir: MaterializedSet, *, ctx: typing.Any\n) -> typing.Any:\n    # We want to show the path_ids but *not* to show the full uses\n    node = ast.serialize_to_markup(ir, ctx=ctx)\n    node.add_child(label='uses', node=markup.serialize(ir.uses, ctx=ctx))\n    return node\n\n\nclass Stmt(Expr):\n    __abstract_node__ = True\n    # Hide parent_stmt to reduce debug spew and to hide it from find_children\n    __ast_hidden__ = {'parent_stmt'}\n\n    name: typing.Optional[str] = None\n    # Parts of the edgeql->IR compiler need to create statements and fill in\n    # the result later, but making it Optional would cause lots of errors,\n    # so we stick a dummy set set in.\n    result: Set = DUMMY_SET\n    parent_stmt: typing.Optional[Stmt] = None\n    iterator_stmt: typing.Optional[Set] = None\n    bindings: typing.Optional[list[tuple[Set, qltypes.Volatility]]] = None\n\n    @property\n    def typeref(self) -> TypeRef:\n        return self.result.typeref\n\n\nclass FilteredStmt(Stmt):\n    __abstract_node__ = True\n    where: typing.Optional[Set] = None\n    where_card: qltypes.Cardinality = qltypes.Cardinality.UNKNOWN\n\n\nclass SelectStmt(FilteredStmt):\n\n    orderby: typing.Optional[list[SortExpr]] = None\n    offset: typing.Optional[Set] = None\n    limit: typing.Optional[Set] = None\n    implicit_wrapper: bool = False\n\n    # An expression to use instead of this one for the purpose of\n    # cardinality/multiplicity inference. This is used for when something\n    # is desugared in a way that doesn't preserve cardinality, but we\n    # need to anyway.\n    card_inference_override: typing.Optional[Set] = None\n\n\nclass GroupStmt(FilteredStmt):\n    subject: Set = DUMMY_SET\n    using: dict[str, tuple[Set, qltypes.Cardinality]] = (\n        ast.field(factory=dict))\n    by: list[qlast.GroupingElement]\n    result: Set = DUMMY_SET\n    group_binding: Set = DUMMY_SET\n    grouping_binding: typing.Optional[Set] = None\n    orderby: typing.Optional[list[SortExpr]] = None\n    # Optimization information\n    group_aggregate_sets: dict[\n        typing.Optional[Set], frozenset[PathId]\n    ] = ast.field(factory=dict)\n\n\nclass MutatingLikeStmt(Expr):\n    \"\"\"Represents statements that are \"like\" mutations for certain purposes.\n\n    In particular, it includes both MutatingStmt, representing actual\n    mutations, and TriggerAnchor, which is a way to signal that\n    something should (or should not) see certain mutation overlays in\n    the backend without being an actual mutation.\n    \"\"\"\n    __abstract_node__ = True\n\n\nclass TriggerAnchor(MutatingLikeStmt):\n\n    \"\"\"A placeholder to be put in trigger __old__ nodes.\n\n    The idea here is that in the backend, it will be treated as if it\n    was a MutatingStmt for the purposes of determining whether to use\n    overlays.\n    \"\"\"\n    typeref: TypeRef\n\n\nclass MutatingStmt(Stmt, MutatingLikeStmt):\n    __abstract_node__ = True\n    # Parts of the edgeql->IR compiler need to create statements and fill in\n    # the subject later, but making it Optional would cause lots of errors,\n    # so we stick a dummy set in.\n    subject: Set = DUMMY_SET\n    # Conflict checks that we should manually raise constraint violations\n    # for.\n    conflict_checks: typing.Optional[list[OnConflictClause]] = None\n    # Access policy checks that we should raise errors on\n    write_policies: dict[uuid.UUID, WritePolicies] = ast.field(\n        factory=dict\n    )\n    # Access policy checks that we should filter on\n    read_policies: dict[uuid.UUID, ReadPolicyExpr] = ast.field(\n        factory=dict\n    )\n\n    # Rewrites of the subject shape\n    rewrites: typing.Optional[Rewrites] = None\n\n    @property\n    def material_type(self) -> TypeRef:\n        \"\"\"The proper material type being operated on.\n\n        This should have all views stripped out.\n        \"\"\"\n        raise NotImplementedError\n\n\nclass ReadPolicyExpr(Base):\n    expr: Set\n    cardinality: qltypes.Cardinality = qltypes.Cardinality.UNKNOWN\n\n\nclass WritePolicies(Base):\n    policies: list[WritePolicy]\n\n\nclass WritePolicy(Base):\n    expr: Set\n    action: qltypes.AccessPolicyAction\n    name: str\n    error_msg: typing.Optional[str]\n\n    cardinality: qltypes.Cardinality = qltypes.Cardinality.UNKNOWN\n\n\nclass Trigger(Base):\n    expr: Set\n    # All the relevant dml\n    affected: set[tuple[TypeRef, MutatingStmt]]\n    all_affected_types: set[TypeRef]\n    source_type: TypeRef\n    kinds: set[qltypes.TriggerKind]\n    scope: qltypes.TriggerScope\n\n    # N.B: Semantically and in the external language, delete triggers\n    # don't have a __new__ set, but we give it one in the\n    # implementation (identical to the old set), to help make the\n    # implementation more uniform.\n    new_set: Set\n    old_set: typing.Optional[Set]\n\n\nclass OnConflictClause(Base):\n    constraint: typing.Optional[ConstraintRef]\n    select_ir: Set\n    always_check: bool\n    else_ir: typing.Optional[Set]\n    check_anchor: typing.Optional[PathId] = None\n    else_fail: typing.Optional[MutatingStmt] = None\n\n\nclass InsertStmt(MutatingStmt):\n    on_conflict: typing.Optional[OnConflictClause] = None\n    final_typeref: typing.Optional[TypeRef] = None\n\n    @property\n    def material_type(self) -> TypeRef:\n        return self.subject.typeref.real_material_type\n\n    @property\n    def typeref(self) -> TypeRef:\n        return self.final_typeref or self.result.typeref\n\n\n# N.B: The PointerRef corresponds to the *definition* point of the rewrite.\nRewritesOfType = dict[str, tuple[SetE[Pointer], BasePointerRef]]\n\n\n@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)\nclass Rewrites:\n    old_path_id: typing.Optional[PathId]\n\n    by_type: dict[TypeRef, RewritesOfType]\n\n\nclass UpdateStmt(MutatingStmt, FilteredStmt):\n    _material_type: TypeRef | None = None\n\n    @property\n    def material_type(self) -> TypeRef:\n        assert self._material_type\n        return self._material_type\n\n\nclass DeleteStmt(MutatingStmt, FilteredStmt):\n    _material_type: TypeRef | None = None\n\n    links_to_delete: dict[\n        uuid.UUID,\n        tuple[PointerRef, ...]\n    ] = ast.field(factory=dict)\n\n    @property\n    def material_type(self) -> TypeRef:\n        assert self._material_type\n        return self._material_type\n\n\nclass SessionStateCmd(Command):\n\n    modaliases: dict[typing.Optional[str], s_mod.Module]\n    testmode: bool\n\n\nclass ConfigCommand(Command, Expr):\n    __abstract_node__ = True\n    name: str\n    scope: qltypes.ConfigScope\n    cardinality: qltypes.SchemaCardinality\n    requires_restart: bool\n    backend_setting: typing.Optional[str]\n    is_system_config: bool\n    type_rewrites: typing.Optional[dict[tuple[uuid.UUID, bool], Set]] = None\n    globals: typing.Optional[list[Global]] = None\n    scope_tree: typing.Optional[ScopeTreeNode] = None\n\n    params: list[Param] = ast.field(factory=list)\n    schema: typing.Optional[s_schema.Schema] = None\n\n\nclass ConfigSet(ConfigCommand):\n\n    expr: Set\n    required: bool\n    backend_expr: typing.Optional[Set] = None\n\n    @property\n    def typeref(self) -> TypeRef:\n        return self.expr.typeref\n\n\nclass ConfigReset(ConfigCommand):\n\n    selector: typing.Optional[Set] = None\n\n    @property\n    def typeref(self) -> TypeRef:\n        return TypeRef(\n            id=so.get_known_type_id('anytype'),\n            name_hint=sn.UnqualName('anytype'),\n        )\n\n\nclass ConfigInsert(ConfigCommand):\n\n    expr: Set\n\n    @property\n    def typeref(self) -> TypeRef:\n        return self.expr.typeref\n\n\nclass FTSDocument(ImmutableExpr):\n    \"\"\"\n    Text and information on how to search through it.\n\n    Constructed with `std::fts::with_options`.\n    \"\"\"\n\n    text: Set\n\n    language: Set\n\n    language_domain: set[str]\n\n    weight: typing.Optional[str]\n\n    typeref: TypeRef\n\n\n# StaticIntrospection is only used in static evaluation (staeval.py),\n# but unfortunately the IR AST node can only be defined here.\nclass StaticIntrospection(Tuple):\n\n    ir: TypeIntrospection\n    schema: s_schema.Schema\n\n    @property\n    def meta_type(self) -> s_objtypes.ObjectType:\n        return self.schema.get_by_id(\n            self.ir.typeref.id, type=s_objtypes.ObjectType\n        )\n\n    @property\n    def output_type(self) -> s_types.Type:\n        return self.schema.get_by_id(\n            self.ir.output_typeref.id, type=s_types.Type\n        )\n\n    @property\n    def elements(self) -> list[TupleElement]:\n        from . import staeval\n\n        rv = []\n        schema = self.schema\n        output_type = self.output_type\n        for ptr in self.meta_type.get_pointers(schema).objects(schema):\n            field_sn = ptr.get_shortname(schema)\n            field_name = field_sn.name\n            field_type = ptr.get_target(schema)\n            assert field_type is not None\n            try:\n                field_value = output_type.get_field_value(schema, field_name)\n            except LookupError:\n                continue\n            try:\n                val = staeval.coerce_py_const(field_type.id, field_value)\n            except staeval.UnsupportedExpressionError:\n                continue\n            ref = TypeRef(id=field_type.id, name_hint=field_sn)\n            vset = Set(expr=val, typeref=ref, path_id=PathId.from_typeref(ref))\n            rv.append(TupleElement(name=field_name, val=vset))\n        return rv\n\n    @elements.setter\n    def elements(self, elements: list[TupleElement]) -> None:\n        pass\n\n    def get_field_value(self, name: sn.QualName) -> ConstExpr | TypeCast:\n        from . import staeval\n\n        ptr = self.meta_type.getptr(self.schema, name.get_local_name())\n        rv_type = ptr.get_target(self.schema)\n        assert rv_type is not None\n        rv_value = self.output_type.get_field_value(self.schema, name.name)\n        return staeval.coerce_py_const(rv_type.id, rv_value)\n"
  },
  {
    "path": "edb/ir/astexpr.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2013-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import Optional\n\nfrom edb.schema import name as sn\n\nfrom . import ast as irast\n\n\ndef get_constraint_references(tree: irast.Base) -> Optional[list[irast.Base]]:\n    return is_constraint_expr(tree)\n\n\ndef is_constraint_expr(tree: irast.Base) -> Optional[list[irast.Base]]:\n    return (\n        is_distinct_expr(tree) or\n        is_set_expr(tree) or\n        is_binop(tree)\n    )\n\n\ndef is_distinct_expr(tree: irast.Base) -> Optional[list[irast.Base]]:\n    return (\n        is_pure_distinct_expr(tree) or\n        is_possibly_wrapped_distinct_expr(tree)\n    )\n\n\ndef is_pure_distinct_expr(tree: irast.Base) -> Optional[list[irast.Base]]:\n    if not isinstance(tree, irast.FunctionCall):\n        return None\n    if tree.func_shortname != sn.QualName('std', '_is_exclusive'):\n        return None\n    if len(tree.args) != 1:\n        return None\n    if 0 not in tree.args:\n        return None\n    if not isinstance(tree.args[0], irast.CallArg):\n        return None\n\n    return [tree.args[0].expr]\n\n\ndef is_possibly_wrapped_distinct_expr(\n    tree: irast.Base\n) -> Optional[list[irast.Base]]:\n    if not isinstance(tree, irast.SelectStmt):\n        return None\n\n    return is_set_expr(tree.result)\n\n\ndef is_set_expr(tree: irast.Base) -> Optional[list[irast.Base]]:\n    if not isinstance(tree, irast.Set):\n        return None\n\n    return (\n        is_distinct_expr(tree.expr) or\n        is_binop(tree.expr)\n    )\n\n\ndef is_binop(tree: irast.Base) -> Optional[list[irast.Base]]:\n    if not isinstance(tree, irast.OperatorCall):\n        return None\n    if not tree.func_shortname != sn.QualName('std', 'AND'):\n        return None\n    if len(tree.args) != 2:\n        return None\n\n    refs = []\n\n    for arg in tree.args:\n        if not isinstance(arg, irast.CallArg):\n            return None\n        ref = is_constraint_expr(arg.expr)\n        if not ref:\n            return None\n\n        refs.extend(ref)\n\n    return refs\n"
  },
  {
    "path": "edb/ir/pathid.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    AbstractSet,\n    Iterator,\n    cast,\n    TYPE_CHECKING,\n)\n\nfrom . import typeutils\n\nfrom edb.common import uuidgen\n\nfrom edb.schema import name as s_name\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import types as s_types\n\nfrom edb.ir import ast as irast\n\nif TYPE_CHECKING:\n    import uuid\n\n    from edb.schema import schema as s_schema\n    from edb.edgeql.compiler import context as qlcompiler_ctx\n\n\nNamespace = str\n\n\nclass PathId:\n    \"\"\"A descriptor of a *variable* in an expression.\n\n    ``PathId`` instances are used to identify and describe expressions\n    in EdgeQL.  They are immutable, hashable and comparable.  Instances\n    of ``PathId`` describing the same expression variable are equal.\n    Another important aspect (and the reason for the class name) is that\n    ``PathId`` instances describe *paths* in a structured way that allows\n    walking the path to its root.\n\n    ``PathId`` instances are normally directly created for a path root,\n    and then PathIds representing the steps of a path are derived by\n    calling ``extend()`` on the previous step.\n\n    For example, for the expression ``Movie.reviews.author``\n    the following would return a corresponding ``PathId`` (in pseudo-code):\n\n        path_id = PathId.from_type(Movie).extend('reviews').extend('author')\n    \"\"\"\n\n    __slots__ = ('_path', '_norm_path', '_namespace', '_prefix',\n                 '_is_ptr', '_is_linkprop', '_hash')\n\n    #: Actual path information.\n    _path: tuple[\n        irast.TypeRef\n        | tuple[irast.BasePointerRef, s_pointers.PointerDirection],\n        ...\n    ]\n\n    #: Normalized path data, used for PathId hashing and comparisons.\n    _norm_path: tuple[\n        uuid.UUID\n        | s_name.Name\n        | tuple[s_name.QualName, s_pointers.PointerDirection, bool],\n        ...\n    ]\n\n    #: A set of namespace identifiers which this PathId belongs to.\n    _namespace: frozenset[str]\n\n    #: If this PathId has a prefix from another namespace, this will\n    #: contain said prefix.\n    _prefix: Optional[PathId]\n\n    #: True if this PathId represents the link portion of a link property path.\n    _is_ptr: bool\n\n    #: True if this PathId represents a link property path.\n    _is_linkprop: bool\n\n    def __init__(\n        self,\n        initializer: Optional[PathId] = None,\n        *,\n        namespace: AbstractSet[str] = frozenset(),\n        typename: Optional[str] = None,\n    ) -> None:\n        if isinstance(initializer, PathId):\n            self._path = initializer._path\n            self._norm_path = initializer._norm_path\n            if namespace:\n                self._namespace = frozenset(namespace)\n            else:\n                self._namespace = initializer._namespace\n            self._is_ptr = initializer._is_ptr\n            self._is_linkprop = initializer._is_linkprop\n            self._prefix = initializer._prefix\n        elif initializer is not None:\n            raise TypeError('use PathId.from_type')\n        else:\n            self._path = ()\n            self._norm_path = ()\n            self._namespace = frozenset(namespace)\n            self._prefix = None\n            self._is_ptr = False\n            self._is_linkprop = False\n\n        self._hash = -1\n\n    def __getstate__(self) -> Any:\n        # We need to omit the cached _hash when we pickle because it won't\n        # be correct in a different process.\n        return tuple([\n            getattr(self, k) if k != '_hash' else -1\n            for k in PathId.__slots__\n        ])\n\n    def __setstate__(self, state: Any) -> None:\n        for k, v in zip(PathId.__slots__, state):\n            setattr(self, k, v)\n\n    @classmethod\n    def from_type(\n        cls,\n        schema: s_schema.Schema,\n        t: s_types.Type,\n        *,\n        env: Optional[qlcompiler_ctx.Environment],\n        namespace: AbstractSet[Namespace] = frozenset(),\n        typename: Optional[s_name.QualName] = None,\n    ) -> PathId:\n        \"\"\"Return a ``PathId`` instance for a given :class:`schema.types.Type`\n\n        The returned ``PathId`` instance describes a set variable of type *t*.\n        The name of the passed type is used as the name for the variable,\n        unless *typename* is specified, in which case it is used instead.\n\n        Args:\n            schema:\n                A schema instance where the type *t* is defined.\n            t:\n                The type of the variable being defined.\n            env:\n                Optional EdgeQL compiler environment, used for caching.\n            namespace:\n                Optional namespace in which the variable is defined.\n            typename:\n                If specified, used as the name for the variable instead\n                of the name of the type *t*.\n\n        Returns:\n            A ``PathId`` instance of type *t*.\n        \"\"\"\n        if not isinstance(t, s_types.Type):\n            raise ValueError(\n                f'invalid PathId: bad source: {t!r}')\n\n        cache = env.type_ref_cache if env is not None else None\n        typeref = typeutils.type_to_typeref(\n            schema, t, cache=cache, typename=typename\n        )\n        return cls.from_typeref(typeref, namespace=namespace,\n                                typename=typename)\n\n    @classmethod\n    def from_pointer(\n        cls,\n        schema: s_schema.Schema,\n        pointer: s_pointers.Pointer,\n        *,\n        namespace: AbstractSet[Namespace] = frozenset(),\n        env: Optional[qlcompiler_ctx.Environment],\n    ) -> PathId:\n        \"\"\"Return a ``PathId`` instance for a given link or property.\n\n        The specified *pointer* argument must be a concrete link or property.\n        The returned ``PathId`` instance describes a set variable of all\n        objects represented by the pointer (i.e, for a link, a set of all\n        link targets).\n\n        Args:\n            schema:\n                A schema instance where the type *t* is defined.\n            pointer:\n                An instance of a concrete link or property.\n            namespace:\n                Optional namespace in which the variable is defined.\n\n        Returns:\n            A ``PathId`` instance.\n        \"\"\"\n        if pointer.is_non_concrete(schema):\n            raise ValueError(f'invalid PathId: {pointer} is not concrete')\n\n        source = pointer.get_source(schema)\n        if isinstance(source, s_pointers.Pointer):\n            prefix = cls.from_pointer(\n                schema, source, namespace=namespace, env=env\n            )\n            prefix = prefix.ptr_path()\n        elif isinstance(source, s_types.Type):\n            prefix = cls.from_type(schema, source, namespace=namespace, env=env)\n        else:\n            raise AssertionError(f'unexpected pointer source: {source!r}')\n\n        typeref_cache = env.type_ref_cache if env is not None else None\n        ptrref_cache = env.ptr_ref_cache if env is not None else None\n\n        ptrref = typeutils.ptrref_from_ptrcls(\n            schema=schema, ptrcls=pointer,\n            cache=ptrref_cache, typeref_cache=typeref_cache,\n        )\n        return prefix.extend(ptrref=ptrref)\n\n    @classmethod\n    def from_typeref(\n        cls,\n        typeref: irast.TypeRef,\n        *,\n        namespace: AbstractSet[Namespace] = frozenset(),\n        typename: Optional[s_name.Name | uuid.UUID] = None,\n    ) -> PathId:\n        \"\"\"Return a ``PathId`` instance for a given :class:`ir.ast.TypeRef`\n\n        The returned ``PathId`` instance describes a set variable of type\n        described by *typeref*.  The name of the passed type is used as\n        the name for the variable, unless *typename* is specified, in\n        which case it is used instead.\n\n        Args:\n            typeref:\n                The descriptor of a type of the variable being defined.\n            namespace:\n                Optional namespace in which the variable is defined.\n            typename:\n                If specified, used as the name for the variable instead\n                of the name of the type *t*.\n\n        Returns:\n            A ``PathId`` instance of type described by *typeref*.\n        \"\"\"\n        pid = cls()\n        pid._path = (typeref,)\n        if typename is None:\n            typename = typeref.id\n        pid._norm_path = (typename,)\n        pid._namespace = frozenset(namespace)\n        return pid\n\n    @classmethod\n    def from_ptrref(\n        cls,\n        ptrref: irast.PointerRef,\n        *,\n        namespace: AbstractSet[Namespace] = frozenset(),\n    ) -> PathId:\n        \"\"\"Return a ``PathId`` instance for a given :class:`ir.ast.PointerRef`\n\n        Args:\n            ptrref:\n                The descriptor of a ptr of the variable being defined.\n            namespace:\n                Optional namespace in which the variable is defined.\n\n        Returns:\n            A ``PathId`` instance of type described by *ptrref*.\n        \"\"\"\n        pid = cls.from_typeref(ptrref.out_source, namespace=namespace)\n        pid = pid.extend(ptrref=ptrref)\n        return pid\n\n    @classmethod\n    def new_dummy(cls, name: str) -> PathId:\n        name_hint = s_name.QualName(module='__derived__', name=name)\n        typeref = irast.TypeRef(id=uuidgen.uuid1mc(), name_hint=name_hint)\n        return irast.PathId.from_typeref(typeref=typeref)\n\n    def __hash__(self) -> int:\n        if self._hash == -1:\n            self._hash = hash((\n                self.__class__,\n                self._norm_path,\n                self._namespace,\n                self._prefix,\n                self._is_ptr,\n            ))\n        return self._hash\n\n    def __eq__(self, other: Any) -> bool:\n        if not isinstance(other, PathId):\n            return NotImplemented\n\n        return (\n            self._norm_path == other._norm_path and\n            self._namespace == other._namespace and\n            self._prefix == other._prefix and\n            self._is_ptr == other._is_ptr\n        )\n\n    def __len__(self) -> int:\n        return len(self._path)\n\n    def __str__(self) -> str:\n        return self.pformat_internal(debug=False)\n\n    __repr__ = __str__\n\n    def extend(\n        self,\n        *,\n        ptrref: irast.BasePointerRef,\n        direction: s_pointers.PointerDirection = (\n            s_pointers.PointerDirection.Outbound),\n        ns: AbstractSet[Namespace] = frozenset(),\n    ) -> PathId:\n        \"\"\"Return a new ``PathId`` that is a *path step* from this ``PathId``.\n\n        For example, if you have a ``PathId`` that describes a variable ``A``,\n        and you want to obtain a ``PathId`` for ``A.b``, you should call\n        ``path_id_for_A.extend(ptrcls=pointer_object_b, schema=schema)``.\n\n        Args:\n            ptrref:\n                A ``ir.ast.BasePointerRef`` instance that corresponds\n                to the path step.  This may be a regular link or property\n                object, or a pseudo-pointer, like a tuple or type intersection\n                step.\n            direction:\n                The direction of the *ptrcls* pointer.  This makes sense\n                only for reverse link traversal, all other path steps are\n                always forward.\n            namespace:\n                Optional namespace in which the path extension is defined.\n                If not specified, the namespace of the current PathId is\n                used.\n            schema:\n                A schema instance.\n\n        Returns:\n            A new ``PathId`` instance representing a step extension of\n            this ``PathId``.\n        \"\"\"\n        if not self:\n            raise ValueError('cannot extend empty PathId')\n\n        if direction is s_pointers.PointerDirection.Outbound:\n            target_ref = ptrref.out_target\n        else:\n            target_ref = ptrref.out_source\n\n        is_linkprop = ptrref.source_ptr is not None\n        if is_linkprop and not self._is_ptr:\n            raise ValueError(\n                'link property path extension on a non-link path')\n\n        result = self.__class__()\n        result._path = self._path + ((ptrref, direction), target_ref)\n        link_name = ptrref.name\n        lnk = (link_name, direction, is_linkprop)\n        result._is_linkprop = is_linkprop\n\n        if target_ref.material_type is not None:\n            material_type = target_ref.material_type\n        else:\n            material_type = target_ref\n\n        result._norm_path = (self._norm_path + (lnk, material_type.id))\n\n        if ns:\n            if self._namespace:\n                result._namespace = self._namespace | frozenset(ns)\n            else:\n                result._namespace = frozenset(ns)\n        else:\n            result._namespace = self._namespace\n\n        if self._namespace != result._namespace:\n            result._prefix = self\n        else:\n            result._prefix = self._prefix\n\n        return result\n\n    def replace_namespace(\n        self,\n        namespace: AbstractSet[Namespace],\n    ) -> PathId:\n        \"\"\"Return a copy of this ``PathId`` with namespace set to *namespace*.\n        \"\"\"\n        result = self.__class__(self)\n        result._namespace = frozenset(namespace)\n\n        if result._prefix is not None:\n            result._prefix = result._get_minimal_prefix(\n                result._prefix.replace_namespace(namespace))\n\n        return result\n\n    def merge_namespace(\n        self,\n        namespace: AbstractSet[Namespace],\n        *,\n        deep: bool=False,\n    ) -> PathId:\n        \"\"\"Return a copy of this ``PathId`` that has *namespace* added to its\n           namespace.\n        \"\"\"\n        new_namespace = self._namespace | frozenset(namespace)\n\n        if new_namespace != self._namespace or deep:\n            result = self.__class__(self)\n            result._namespace = new_namespace\n            if deep and result._prefix is not None:\n                result._prefix = result._prefix.merge_namespace(new_namespace)\n            if result._prefix is not None:\n                result._prefix = result._get_minimal_prefix(result._prefix)\n\n            return result\n\n        else:\n            return self\n\n    def strip_namespace(self, namespace: AbstractSet[Namespace]) -> PathId:\n        \"\"\"Return a copy of this ``PathId`` with a given portion of the\n           namespace id removed.\"\"\"\n        if self._namespace and namespace:\n            stripped_ns = self._namespace - set(namespace)\n            result = self.replace_namespace(stripped_ns)\n\n            if result._prefix is not None:\n                result._prefix = result._get_minimal_prefix(\n                    result._prefix.strip_namespace(namespace))\n\n            return result\n        else:\n            return self\n\n    def pformat_internal(self, debug: bool = False) -> str:\n        \"\"\"Verbose format for debugging purposes.\"\"\"\n        result = ''\n\n        if not self._path:\n            return ''\n\n        if self._namespace:\n            result += f'{\"@\".join(sorted(self._namespace))}@@'\n\n        path = self._path\n\n        result += f'({path[0].name_hint})'  # type: ignore\n\n        for i in range(1, len(path) - 1, 2):\n            ptrspec = cast(\n                tuple[irast.BasePointerRef, s_pointers.PointerDirection],\n                path[i],\n            )\n\n            tgtspec = cast(\n                irast.TypeRef,\n                path[i + 1],\n            )\n\n            if debug:\n                link_name = str(ptrspec[0].name)\n                ptr = f'({link_name})'\n            else:\n                ptr = ptrspec[0].shortname.name\n            ptrdir = ptrspec[1]\n            is_lprop = ptrspec[0].source_ptr is not None\n\n            if tgtspec.material_type is not None:\n                mat_tgt = tgtspec.material_type\n            else:\n                mat_tgt = tgtspec\n            tgt = mat_tgt.name_hint\n\n            if tgt:\n                lexpr = f'{ptr}[IS {tgt}]'\n            else:\n                lexpr = f'{ptr}'\n\n            if is_lprop:\n                step = '@'\n            else:\n                step = f'.{ptrdir}'\n\n            result += f'{step}{lexpr}'\n\n        if self._is_ptr:\n            result += '@'\n\n        return result\n\n    def pformat(self) -> str:\n        \"\"\"Pretty PathId format for user-visible messages.\"\"\"\n        result = ''\n\n        if not self._path:\n            return ''\n\n        path = self._path\n\n        start_name = s_name.shortname_from_fullname(\n            path[0].name_hint)  # type: ignore\n        result += f'{start_name.name}'\n\n        for i in range(1, len(path) - 1, 2):\n            ptrspec = cast(\n                tuple[irast.BasePointerRef, s_pointers.PointerDirection],\n                path[i],\n            )\n\n            ptr_name = ptrspec[0].shortname\n            ptrdir = ptrspec[1]\n            is_lprop = ptrspec[0].source_ptr is not None\n\n            if is_lprop:\n                step = '@'\n            else:\n                step = '.'\n                if ptrdir == s_pointers.PointerDirection.Inbound:\n                    step += ptrdir\n\n            result += f'{step}{ptr_name.name}'\n\n        if self._is_ptr:\n            result += '@'\n\n        return result\n\n    def rptr(self) -> Optional[irast.BasePointerRef]:\n        \"\"\"Return the descriptor of a pointer for the last path step, if any.\n\n           If this PathId represents a non-path expression, ``rptr()``\n           will return ``None``.\n        \"\"\"\n        if len(self._path) > 1:\n            return self._path[-2][0]  # type: ignore\n        else:\n            return None\n\n    def rptr_dir(self) -> Optional[s_pointers.PointerDirection]:\n        \"\"\"Return the direction of a pointer for the last path step, if any.\n\n           If this PathId represents a non-path expression, ``rptr_dir()``\n           will return ``None``.\n        \"\"\"\n        if len(self._path) > 1:\n            return self._path[-2][1]  # type: ignore\n        else:\n            return None\n\n    def rptr_name(self) -> Optional[s_name.QualName]:\n        \"\"\"Return the name of a pointer for the last path step, if any.\n\n           If this PathId represents a non-path expression, ``rptr_name()``\n           will return ``None``.\n        \"\"\"\n        rptr = self.rptr()\n        if rptr is not None:\n            return rptr.shortname\n        else:\n            return None\n\n    def src_path(self) -> Optional[PathId]:\n        \"\"\"Return a ``PathId`` instance representing an immediate path prefix\n           of this ``PathId``, i.e\n           ``PathId('Foo.bar.baz').src_path() == PathId('Foo.bar')``.\n\n           If this PathId represents a non-path expression, ``src_path()``\n           will return ``None``.\n        \"\"\"\n        if len(self._path) > 1:\n            return self._get_prefix(-2)\n        else:\n            return None\n\n    def ptr_path(self) -> PathId:\n        \"\"\"Return a new ``PathId`` instance that is a \"pointer prefix\" of this\n           ``PathId``.\n\n           A pointer prefix is the common path prefix shared by paths to\n           link properties of the same link, i.e\n\n               common_path_id(Foo.bar@prop1, Foo.bar@prop2)\n                   == PathId(Foo.bar).ptr_path()\n        \"\"\"\n        if self._is_ptr:\n            return self\n        else:\n            result = self.__class__(self)\n            result._is_ptr = True\n            return result\n\n    def tgt_path(self) -> PathId:\n        \"\"\"If this is a pointer prefix, return the ``PathId`` representing\n           the path to the target of the pointer.\n\n           This is the inverse of :meth:`~PathId.ptr_path`.\n        \"\"\"\n        if not self._is_ptr:\n            return self\n        else:\n            result = self.__class__(self)\n            result._is_ptr = False\n            return result\n\n    def iter_prefixes(self, include_ptr: bool = False) -> Iterator[PathId]:\n        \"\"\"Return an iterator over all prefixes of this ``PathId``.\n\n           The order of prefixes is from longest to shortest, i.e\n           ``PathId(A.b.c.d).iter_prefixes()`` will yield\n           [PathId(A.b.c.d), PathId(A.b.c), PathId(A.b), PathId(A)].\n\n           If *include_ptr* is ``True``, then pointer prefixes for each\n           step are also included.\n        \"\"\"\n        if self._prefix is not None:\n            yield from self._prefix.iter_prefixes(include_ptr=include_ptr)\n            start = len(self._prefix)\n        else:\n            yield self._get_prefix(1)\n            start = 1\n\n        for i in range(start, len(self._path) - 1, 2):\n            path_id = self._get_prefix(i + 2)\n            if path_id.is_ptr_path():\n                yield path_id.tgt_path()\n                if include_ptr:\n                    yield path_id\n            else:\n                yield path_id\n\n    def startswith(\n        self, path_id: PathId, permissive_ptr_path: bool = False\n    ) -> bool:\n        \"\"\"Return true if this ``PathId`` has *path_id* as a prefix.\"\"\"\n        base = self._get_prefix(len(path_id))\n        return base == path_id or (\n            permissive_ptr_path and base.tgt_path() == path_id)\n\n    @property\n    def target(self) -> irast.TypeRef:\n        \"\"\"Return the type descriptor for this PathId.\"\"\"\n        return self._path[-1]  # type: ignore\n\n    @property\n    def target_name_hint(self) -> s_name.Name:\n        \"\"\"Return the name of the type for this PathId.\"\"\"\n        if self.target.material_type is not None:\n            material_type = self.target.material_type\n        else:\n            material_type = self.target\n        return material_type.name_hint\n\n    def is_objtype_path(self) -> bool:\n        \"\"\"Return True if this PathId represents an expression of object\n           type.\n        \"\"\"\n        return not self.is_ptr_path() and typeutils.is_object(self.target)\n\n    def is_scalar_path(self) -> bool:\n        \"\"\"Return True if this PathId represents an expression of scalar\n           type.\n        \"\"\"\n        return not self.is_ptr_path() and typeutils.is_scalar(self.target)\n\n    def is_view_path(self) -> bool:\n        \"\"\"Return True if this PathId represents an expression that is a view.\n        \"\"\"\n        return not self.is_ptr_path() and typeutils.is_view(self.target)\n\n    def is_tuple_path(self) -> bool:\n        \"\"\"Return True if this PathId represents an expression of an tuple\n           type.\n        \"\"\"\n        return not self.is_ptr_path() and typeutils.is_tuple(self.target)\n\n    def is_tuple_indirection_path(self) -> bool:\n        \"\"\"Return True if this PathId represents a tuple element indirection\n           expression.\n        \"\"\"\n        src_path = self.src_path()\n        return src_path is not None and src_path.is_tuple_path()\n\n    def is_array_path(self) -> bool:\n        \"\"\"Return True if this PathId represents an expression of an array\n           type.\n        \"\"\"\n        return not self.is_ptr_path() and typeutils.is_array(self.target)\n\n    def is_range_path(self) -> bool:\n        \"\"\"Return True if this PathId represents an expression of a range\n           type.\n        \"\"\"\n        return not self.is_ptr_path() and typeutils.is_range(self.target)\n\n    def is_collection_path(self) -> bool:\n        \"\"\"Return True if this PathId represents an expression of a collection\n           type.\n        \"\"\"\n        return not self.is_ptr_path() and typeutils.is_collection(self.target)\n\n    def is_ptr_path(self) -> bool:\n        \"\"\"Return True if this PathId represents a link prefix of the path.\n\n        Immediate prefix of a link property ``PathId`` will return True here.\n        \"\"\"\n        return self._is_ptr\n\n    def is_linkprop_path(self) -> bool:\n        \"\"\"Return True if this PathId represents a link property path\n           expression, i.e ``Foo.bar@prop``.\"\"\"\n        return self._is_linkprop\n\n    def is_type_intersection_path(self) -> bool:\n        \"\"\"Return True if this PathId represents a type intersection\n           expression, i.e ``Foo[IS Bar]``.\"\"\"\n        rptr_name = self.rptr_name()\n        if rptr_name is None:\n            return False\n        else:\n            return str(rptr_name) in (\n                '__type__::indirection',\n                '__type__::optindirection',\n            )\n\n    @property\n    def namespace(self) -> frozenset[str]:\n        \"\"\"The namespace of this ``PathId``\"\"\"\n        return self._namespace\n\n    def _get_prefix(self, size: int) -> PathId:\n        if size < 0:\n            size = len(self._path) + size\n\n        if size == len(self._path):\n            return self\n\n        if self._prefix is not None:\n            prefix_len = len(self._prefix)\n            if prefix_len == size:\n                return self._prefix\n            elif prefix_len > size:\n                return self._prefix._get_prefix(size)\n\n        result = self.__class__()\n        result._path = self._path[0:size]\n        result._norm_path = self._norm_path[0:size]\n        result._prefix = self._prefix\n        result._namespace = self._namespace\n        if rptr := result.rptr():\n            result._is_linkprop = rptr.source_ptr is not None\n\n        if size < len(self._path) and self._norm_path[size][2]:  # type: ignore\n            # A link property ref has been chopped off.\n            result._is_ptr = True\n\n        return result\n\n    def _get_minimal_prefix(\n        self,\n        prefix: Optional[PathId],\n    ) -> Optional[PathId]:\n        while prefix is not None:\n            if prefix._namespace == self._namespace:\n                prefix = prefix._prefix\n            else:\n                break\n\n        return prefix\n"
  },
  {
    "path": "edb/ir/scopetree.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Query scope tree implementation.\"\"\"\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    AbstractSet,\n    Iterator,\n    Mapping,\n    Collection,\n    NamedTuple,\n    Protocol,\n    cast,\n    TYPE_CHECKING,\n)\n\nif TYPE_CHECKING:\n    from typing_extensions import TypeGuard\n\nimport sys\nimport textwrap\nimport weakref\n\nfrom edb import errors\nfrom edb.common import ordered\nfrom edb.common import span\nfrom edb.common import term\n\nfrom . import pathid\nfrom . import ast as irast\n\n\nclass WarningContext(Protocol):\n    def log_warning(self, warning: errors.EdgeDBError) -> None:\n        ...\n\n\nclass FenceInfo(NamedTuple):\n    unnest_fence: bool\n    factoring_fence: bool\n\n    def __or__(self, other: FenceInfo) -> FenceInfo:\n        return FenceInfo(\n            unnest_fence=self.unnest_fence or other.unnest_fence,\n            factoring_fence=self.factoring_fence or other.factoring_fence,\n        )\n\n\ndef has_path_id(nobe: ScopeTreeNode) -> TypeGuard[ScopeTreeNodeWithPathId]:\n    return nobe.path_id is not None\n\n\nclass ScopeTreeNode:\n    unique_id: Optional[int]\n    \"\"\"A unique identifier used to map scopes on sets.\"\"\"\n\n    path_id: Optional[pathid.PathId]\n    \"\"\"Node path id, or None for branch nodes.\"\"\"\n\n    fenced: bool\n    \"\"\"Whether the subtree represents a SET OF argument.\"\"\"\n\n    is_group: bool\n    \"\"\"Whether the node reprents a GROUP binding (and so *is* multi...).\"\"\"\n\n    unnest_fence: bool\n    \"\"\"Prevent unnesting in parents.\"\"\"\n\n    factoring_fence: bool\n    \"\"\"Prevent prefix factoring across this node.\"\"\"\n\n    factoring_allowlist: set[pathid.PathId]\n    \"\"\"A list of prefixes that are always allowed to be factored.\"\"\"\n\n    optional: bool\n    \"\"\"Whether this node represents an optional path.\"\"\"\n\n    children: list[ScopeTreeNode]\n    \"\"\"A set of child nodes.\"\"\"\n\n    namespaces: set[pathid.Namespace]\n    \"\"\"A set of namespaces used by paths in this branch.\n\n    When a path node is pulled up from this branch,\n    and its namespace matches anything in `namespaces`,\n    the namespace will be stripped.  This is used to\n    implement \"semi-detached\" semantics used by\n    aliases declared in a WITH block.\"\"\"\n\n    def __init__(\n        self,\n        *,\n        path_id: Optional[pathid.PathId]=None,\n        fenced: bool=False,\n        unique_id: Optional[int]=None,\n        optional: bool=False,\n    ) -> None:\n        self.unique_id = unique_id\n        self.path_id = path_id\n        self.fenced = fenced\n        self.unnest_fence = False\n        self.factoring_fence = False\n        self.factoring_allowlist = set()\n        self.optional = optional\n        self.children = []\n        self.namespaces = set()\n        self.is_group = False\n        self._parent: Optional[weakref.ReferenceType[ScopeTreeNode]] = None\n\n    FIELDS = (\n        'unique_id', 'path_id', 'fenced', 'unnest_fence', 'factoring_fence',\n        'factoring_allowlist', 'optional', 'children', 'namespaces',\n        'is_group',\n    )\n\n    def __getstate__(self) -> Any:\n        res = self.__dict__.copy()\n        del res['_parent']\n        return res\n\n    def __setstate__(self, state: Any) -> None:\n        for f, val in state.items():\n            setattr(self, f, val)\n        self._parent = None\n        for child in self.children:\n            child._parent = weakref.ref(self)\n\n    def __repr__(self) -> str:\n        name = 'ScopeFenceNode' if self.fenced else 'ScopeTreeNode'\n        return (f'<{name} {self.path_id!r} at {id(self):0x}>')\n\n    def find_dupe_unique_ids(self) -> set[int]:\n        seen = set()\n        dupes = set()\n        for node in self.root.descendants:\n            if node.unique_id is not None:\n                if node.unique_id in seen:\n                    dupes.add(node.unique_id)\n                seen.add(node.unique_id)\n        return dupes\n\n    def validate_unique_ids(self) -> None:\n        dupes = self.find_dupe_unique_ids()\n        assert not dupes, f'Duplicate \"unique\" ids seen {dupes}'\n\n    @property\n    def name(self) -> str:\n        return self._name(debug=False)\n\n    def _name(self, debug: bool) -> str:\n        if self.path_id is None:\n            name = (\n                ('FENCE' if self.fenced else 'BRANCH')\n            )\n        else:\n            name = self.path_id.pformat_internal(debug=debug)\n        return f'{name}{\" [OPT]\" if self.optional else \"\"}'\n\n    def debugname(self, fuller: bool = False) -> str:\n        parts = [f'{self._name(debug=fuller)}']\n        if self.unique_id:\n            parts.append(f'uid:{self.unique_id}')\n        if self.namespaces:\n            parts.append(','.join(self.namespaces))\n        if self.unnest_fence:\n            parts.append('no-unnest')\n        if self.factoring_fence:\n            parts.append('no-factor')\n        if self.is_group:\n            parts.append('group')\n        return ' '.join(parts)\n\n    @property\n    def fence_info(self) -> FenceInfo:\n        return FenceInfo(\n            unnest_fence=self.unnest_fence,\n            factoring_fence=self.factoring_fence,\n        )\n\n    def fence_info_ex(\n        self, path_id: pathid.PathId, namespaces: AbstractSet[str]\n    ) -> FenceInfo:\n        finfo = self.fence_info\n        if any(\n            _paths_equal(path_id, wl, namespaces)\n            for wl in self.factoring_allowlist\n        ):\n            finfo = finfo._replace(factoring_fence=False)\n        return finfo\n\n    @property\n    def ancestors(self) -> Iterator[ScopeTreeNode]:\n        \"\"\"An iterator of node's ancestors, including self.\"\"\"\n        node: Optional[ScopeTreeNode] = self\n        while node is not None:\n            yield node\n            node = node.parent\n\n    @property\n    def strict_ancestors(self) -> Iterator[ScopeTreeNode]:\n        \"\"\"An iterator of node's ancestors, not including self.\"\"\"\n        node: Optional[ScopeTreeNode] = self.parent\n        while node is not None:\n            yield node\n            node = node.parent\n\n    @property\n    def ancestors_and_namespaces(\n        self,\n    ) -> Iterator[tuple[ScopeTreeNode, frozenset[pathid.Namespace]]]:\n        \"\"\"An iterator of node's ancestors and namespaces, including self.\"\"\"\n        namespaces: frozenset[str] = frozenset()\n        node: Optional[ScopeTreeNode] = self\n        while node is not None:\n            namespaces |= node.namespaces\n            yield node, namespaces\n            node = node.parent\n\n    @property\n    def path_children(self) -> Iterator[ScopeTreeNodeWithPathId]:\n        \"\"\"An iterator of node's children that have path ids.\"\"\"\n        return (\n            p for p in self.children\n            if has_path_id(p)\n        )\n\n    @property\n    def path_descendants(self) -> Iterator[ScopeTreeNodeWithPathId]:\n        \"\"\"An iterator of node's descendants that have path ids.\"\"\"\n        return (\n            p for p in self.descendants\n            if has_path_id(p)\n        )\n\n    def get_all_paths(self) -> AbstractSet[pathid.PathId]:\n        return ordered.OrderedSet(pd.path_id for pd in self.path_descendants)\n\n    @property\n    def descendants(self) -> Iterator[ScopeTreeNode]:\n        \"\"\"An iterator of node's descendants including self top-first.\"\"\"\n        yield self\n        yield from self.strict_descendants\n\n    @property\n    def strict_descendants(self) -> Iterator[ScopeTreeNode]:\n        \"\"\"An iterator of node's descendants not including self top-first.\"\"\"\n        for child in tuple(self.children):\n            yield child\n            if child.parent is self:\n                yield from child.strict_descendants\n\n    def descendants_and_namespaces_ex(\n        self,\n        *,\n        unfenced_only: bool=False,\n        strict: bool=False,\n        skip: Optional[ScopeTreeNode]=None,\n    ) -> Iterator[\n        tuple[\n            ScopeTreeNode,\n            AbstractSet[pathid.Namespace],\n            FenceInfo\n        ]\n    ]:\n        \"\"\"An iterator of node's descendants and namespaces.\n\n        Args:\n            unfenced_only:\n                Whether to skip traversing through fenced nodes\n            strict:\n                Whether to skip the node itself\n            skip:\n                An optional child to skip during the traversal. This\n                is useful for avoiding performance pathologies when\n                repeatedly searching descendants while climbing the\n                tree (see find_factorable_nodes).\n\n        Top-first.\n        \"\"\"\n        if not strict:\n            yield self, frozenset(), FenceInfo(\n                unnest_fence=False, factoring_fence=False)\n        for child in tuple(self.children):\n            if unfenced_only and child.fenced:\n                continue\n            if child is skip:\n                continue\n            finfo = child.fence_info\n            yield child, child.namespaces, finfo\n            if child.parent is not self:\n                continue\n            desc_ns = child.descendants_and_namespaces_ex(\n                unfenced_only=unfenced_only, strict=True)\n            for desc, desc_namespaces, desc_finfo in desc_ns:\n                yield (\n                    desc,\n                    child.namespaces | desc_namespaces,\n                    finfo | desc_finfo,\n                )\n\n    @property\n    def strict_descendants_and_namespaces(\n        self,\n    ) -> Iterator[\n        tuple[\n            ScopeTreeNode,\n            AbstractSet[pathid.Namespace],\n            FenceInfo\n        ]\n    ]:\n        \"\"\"An iterator of node's descendants and namespaces.\n\n        Does not include self. Top-first.\n        \"\"\"\n        return self.descendants_and_namespaces_ex(strict=True)\n\n    @property\n    def descendant_namespaces(self) -> set[pathid.Namespace]:\n        \"\"\"An set of namespaces declared by descendants.\"\"\"\n        namespaces = set()\n        for child in self.descendants:\n            namespaces.update(child.namespaces)\n\n        return namespaces\n\n    @property\n    def fence(self) -> ScopeTreeNode:\n        \"\"\"The nearest ancestor fence (or self, if fence).\"\"\"\n        if self.fenced:\n            return self\n        else:\n            return cast(ScopeTreeNode, self.parent_fence)\n\n    @property\n    def parent(self) -> Optional[ScopeTreeNode]:\n        \"\"\"The parent node.\"\"\"\n        if self._parent is None:\n            return None\n        else:\n            return self._parent()\n\n    @property\n    def path_ancestor(self) -> Optional[ScopeTreeNodeWithPathId]:\n        for ancestor in self.strict_ancestors:\n            if has_path_id(ancestor):\n                return ancestor\n\n        return None\n\n    @property\n    def parent_fence(self) -> Optional[ScopeTreeNode]:\n        \"\"\"The nearest strict ancestor fence.\"\"\"\n        for ancestor in self.strict_ancestors:\n            if ancestor.fenced:\n                return ancestor\n\n        return None\n\n    @property\n    def parent_branch(self) -> Optional[ScopeTreeNode]:\n        \"\"\"The nearest strict ancestor branch or fence.\"\"\"\n        for ancestor in self.strict_ancestors:\n            if ancestor.path_id is None:\n                return ancestor\n\n        return None\n\n    @property\n    def root(self) -> ScopeTreeNode:\n        \"\"\"The root of this tree.\"\"\"\n        node = self\n        while node.parent is not None:\n            node = node.parent\n        return node\n\n    def strip_path_namespace(self, ns: AbstractSet[str]) -> None:\n        if not ns:\n            return\n        for pd in self.path_descendants:\n            pd.path_id = pd.path_id.strip_namespace(ns)\n\n    def attach_child(\n        self, node: ScopeTreeNode, span: Optional[span.Span] = None\n    ) -> None:\n        \"\"\"Attach a child node to this node.\n\n        This is a low-level operation, no tree validation is\n        performed.  For safe tree modification, use attach_subtree()\"\"\n        \"\"\"\n        if node.path_id is not None:\n            for child in self.children:\n                if child.path_id == node.path_id:\n                    raise errors.InvalidReferenceError(\n                        f'{node.path_id} is already present in {self!r}',\n                        span=span,\n                    )\n\n        if node.unique_id is not None:\n            for child in self.children:\n                if child.unique_id == node.unique_id:\n                    return\n\n        node._set_parent(self)\n\n    def attach_fence(self) -> ScopeTreeNode:\n        \"\"\"Create and attach an empty fenced node.\"\"\"\n        fence = ScopeTreeNode(fenced=True)\n        self.attach_child(fence)\n        return fence\n\n    def attach_branch(self) -> ScopeTreeNode:\n        \"\"\"Create and attach an empty branch node.\"\"\"\n        fence = ScopeTreeNode()\n        self.attach_child(fence)\n        return fence\n\n    def attach_path(\n        self,\n        path_id: pathid.PathId,\n        *,\n        optional: bool=False,\n        span: Optional[span.Span],\n        ctx: WarningContext,\n    ) -> None:\n        \"\"\"Attach a scope subtree representing *path_id*.\"\"\"\n\n        subtree = parent = ScopeTreeNode(fenced=True)\n        is_lprop = False\n        lprop_base = None\n        for prefix in reversed(list(path_id.iter_prefixes())):\n            new_child = ScopeTreeNode(path_id=prefix,\n                                      optional=optional and parent is subtree)\n\n            # Normally the prefix is nested, except that tuple\n            # indirection prefixes and the *object* prefixes of link\n            # properties are are at the same level.\n            #\n            # For example, Foo.bar.baz, where Foo is an object type,\n            # forms this scope shape:\n            #   Foo.bar.baz\n            #    |-Foo.bar\n            #       |-Foo\n            #\n            # Whereas, <tuple>.bar.baz results in this:\n            #   <tuple>\n            #   <tuple>.bar\n            #   <tuple>.bar.baz\n            #\n            # And Foo.bar[is Typ]@baz results in:\n            #   Foo.bar[is Typ]@baz\n            #    |-Foo.bar[is Typ]\n            #       |-Foo.bar\n            #   Foo\n            #\n            # For tuples, this is permissable because their fields are always\n            # singletons.\n            # FIXME: I think that it should not be *necessary* for tuples,\n            # but test_edgeql_volatility_select_tuples_* fail if it is changed,\n            # I think for incidental reasons.\n            #\n            # For link properties, this is necessary because referring\n            # to a link property at the end of a path suppresses\n            # deduplication of the link, which is realized by forcing\n            # the link source to be visible. We avoid making the rest of\n            # the path visible, to preserve prefix visibility information\n            # for certain optimizations. (Foo.bar[is Typ] can be compiled\n            # such that it joins directly on Typ (instead of on Bar first),\n            # but *only* if Foo.bar isn't visible without the type intersection.\n            if prefix.is_linkprop_path():\n                assert lprop_base is None\n                # If we just saw a linkprop, track where, since we'll\n                # need to come back to this level in the tree once we\n                # reach the \"object prefix\" of it.\n                lprop_base = parent\n                is_lprop = True\n            elif is_lprop:\n                # Skip through type intersections (i.e [IS Foo]) until\n                # we actually get to the link.\n                if not prefix.is_type_intersection_path():\n                    is_lprop = False\n            else:\n                # If we've reached the \"object prefix\" of a path\n                # referencing a linkprop, pop back up to the level the\n                # linkprop was attached to.\n                if lprop_base is not None:\n                    parent = lprop_base\n                    lprop_base = None\n\n            parent.attach_child(new_child)\n            if not prefix.is_tuple_indirection_path():\n                parent = new_child\n\n        self.attach_subtree(subtree, span=span, ctx=ctx)\n\n    def attach_subtree(\n        self,\n        node: ScopeTreeNode,\n        was_fenced: bool = False,\n        span: Optional[span.Span] = None,\n        fusing: bool = False,\n        *,\n        ctx: WarningContext,\n    ) -> None:\n        \"\"\"Attach a subtree to this node.\n\n        *node* is expected to be a balanced scope tree and may be modified\n        by this function.\n\n        If *node* is not a path node (path_id is None), it is discarded,\n        and it's descendants are attached directly.  The tree balance is\n        maintained.\n        \"\"\"\n        if node.path_id is not None:\n            # Wrap path node\n            wrapper_node = ScopeTreeNode(fenced=True)\n            wrapper_node.attach_child(node)\n            node = wrapper_node\n\n        for descendant, dns, _ in node.descendants_and_namespaces_ex():\n            if not has_path_id(descendant):\n                continue\n\n            path_id = descendant.path_id.strip_namespace(dns)\n            if descendant.parent_fence is node:\n                # Unfenced path.\n\n                # Search for occurences elsewhere in the tree that\n                # can be factored with this one.\n                # If found, attach that node directly to the factoring point\n                # and fuse our node onto it.\n                # If there are multiple factorable occurences, we do\n                # this iteratively, from closest to furthest away.\n                factorable_nodes = self.find_factorable_nodes(path_id)\n\n                current = descendant\n                if factorable_nodes:\n                    descendant.strip_path_namespace(dns)\n                    desc_optional = (\n                        descendant.is_optional_upto(node.parent)\n                        # Check if there is an optional branch between here\n                        # and the *highest* factoring point.\n                        or self.is_optional_upto(factorable_nodes[-1][1])\n                    )\n                    if desc_optional:\n                        descendant.mark_as_optional()\n\n                for factorable in factorable_nodes:\n                    (\n                        existing,\n                        factor_point,\n                        current_ns,\n                        existing_ns,\n                        existing_finfo,\n                        unnest_fence,\n                        node_fenced,\n                    ) = factorable\n\n                    self._check_factoring_errors(\n                        path_id, descendant, factor_point, existing,\n                        unnest_fence, existing_finfo, span,\n                    )\n\n                    existing_fenced = existing.parent_fence is not None and (\n                        factor_point in existing.parent_fence.strict_ancestors\n                    )\n                    if existing.is_optional_upto(factor_point):\n                        existing.mark_as_optional()\n\n                    # Strip the namespaces of everything in the lifted nodes\n                    # based on what they have been lifted through.\n                    existing.strip_path_namespace(existing_ns)\n                    current.strip_path_namespace(current_ns)\n\n                    current.remove()\n                    if (\n                        factor_point is not existing.parent\n                        and factor_point is not existing\n                    ):\n                        existing.remove()\n                        factor_point.attach_child(existing)\n\n                    # Discard the node from the subtree being attached.\n                    existing.fuse_subtree(\n                        current,\n                        self_fenced=existing_fenced,\n                        node_fenced=node_fenced,\n                        span=span,\n                        ctx=ctx,\n                    )\n\n                    current = existing\n\n                    # HACK: If we are being called from fuse_subtree,\n                    # skip all but the first. This is because we don't\n                    # want to merge any children before the parent\n                    # fully finishes all of its factoring.\n                    if fusing:\n                        break\n\n        for child in tuple(node.children):\n            # Attach whatever is remaining in the subtree.\n            for pd in child.path_descendants:\n                if pd.path_id.namespace:\n                    to_strip = set(pd.path_id.namespace) & node.namespaces\n                    pd.path_id = pd.path_id.strip_namespace(to_strip)\n\n            self.attach_child(child)\n\n    def _check_factoring_errors(\n        self,\n        path_id: pathid.PathId,\n        descendant: ScopeTreeNodeWithPathId,\n        factor_point: ScopeTreeNode,\n        existing: ScopeTreeNodeWithPathId,\n        unnest_fence: bool,\n        existing_finfo: FenceInfo,\n        span: Optional[span.Span],\n    ) -> None:\n        if existing_finfo.factoring_fence:\n            # This node is already present in the surrounding\n            # scope and cannot be factored out, such as\n            # a reference to a correlated set inside a DML\n            # statement.\n            raise errors.InvalidReferenceError(\n                f'cannot reference correlated set '\n                f'{path_id.pformat()!r} here',\n                span=span,\n            )\n\n        if (\n            unnest_fence\n            and (\n                factor_point.find_child(\n                    path_id,\n                    in_branches=True,\n                    pfx_with_invariant_card=True,\n                ) is None\n            )\n            and (\n                not (src_path := path_id.src_path())\n                or not self.is_visible(src_path)\n            )\n            and not existing._node_paths_are_not_links()\n        ):\n            path_ancestor = descendant.path_ancestor\n            if path_ancestor is not None:\n                offending_node = path_ancestor\n            else:\n                offending_node = descendant\n\n            assert offending_node.path_id is not None\n\n            imp = ''\n            offending_id = f'{offending_node.path_id.pformat()!r}'\n            existing_id = f'{existing.path_id.pformat()!r}'\n            # If the id is generated, don't leak meaningless info\n            # and try to explain that the reference is implicit.\n            if '~' in offending_id:\n                imp = 'implicit '\n                offending_id = 'an object'\n                existing_id = 'it'\n\n            raise errors.InvalidReferenceError(\n                f'{imp}reference to {offending_id} '\n                f'changes the interpretation of {existing_id} '\n                f'elsewhere in the query',\n                span=span,\n            )\n\n    def _node_paths_are_not_links(self) -> bool:\n        \"\"\"\n        Check if all the pointers a path might be hoisted past are not links\n\n        If the node is a path_id node, return true if the rptrs on\n        all of the chain of parent nodes with path_ids are not links.\n\n        This is in support of allowing queries like\n          select Card.element filter Card.name = 'Imp'\n\n        No real change in interpretation happens here, since element\n        is a property and so doesn't get deduplicated.\n        \"\"\"\n\n        node: ScopeTreeNode | None = self\n        while node and node.path_id:\n            if (\n                isinstance(node.path_id.rptr(), irast.PointerRef)\n                and node.path_id.is_objtype_path()\n            ):\n                return False\n            node = node.parent\n        return True\n\n    def fuse_subtree(\n        self,\n        node: ScopeTreeNode,\n        self_fenced: bool=False,\n        node_fenced: bool=False,\n        span: Optional[span.Span]=None,\n        *,\n        ctx: WarningContext,\n    ) -> None:\n        node.remove()\n\n        if not node.optional and not node_fenced:\n            self.optional = False\n        if node.optional and self_fenced:\n            self.optional = True\n\n        if node.path_id is not None:\n            subtree = ScopeTreeNode(fenced=True)\n            subtree.optional = node.optional\n            for child in tuple(node.children):\n                subtree.attach_child(child)\n        else:\n            subtree = node\n\n        self.attach_subtree(\n            subtree, was_fenced=self_fenced, span=span, fusing=True, ctx=ctx\n        )\n\n    def remove_subtree(self, node: ScopeTreeNode) -> None:\n        \"\"\"Remove the given subtree from this node.\"\"\"\n        if node not in self.children:\n            raise KeyError(f'{node} is not a child of {self}')\n\n        node._set_parent(None)\n\n    def remove_descendants(\n        self, path_id: pathid.PathId, new: ScopeTreeNode\n    ) -> None:\n        \"\"\"Remove all descendant nodes matching *path_id*.\"\"\"\n\n        matching = set()\n\n        for node in self.descendants:\n            if (node.path_id is not None\n                    and _paths_equal(node.path_id, path_id, set())):\n                matching.add(node)\n\n        for node in matching:\n            node.remove()\n\n    def mark_as_optional(self) -> None:\n        \"\"\"Indicate that this scope is used as an OPTIONAL argument.\"\"\"\n        self.optional = True\n\n    def is_optional(self, path_id: pathid.PathId) -> bool:\n        node = self.find_visible(path_id)\n        if node is not None:\n            return node.optional\n        else:\n            return False\n\n    def add_namespaces(\n        self,\n        namespaces: AbstractSet[pathid.Namespace],\n    ) -> None:\n        # Make sure we don't add namespaces that already appear\n        # in on of the ancestors.\n        namespaces = frozenset(namespaces) - self.get_effective_namespaces()\n        self.namespaces.update(namespaces)\n\n    def get_effective_namespaces(self) -> AbstractSet[pathid.Namespace]:\n        namespaces: set[pathid.Namespace] = set()\n\n        for _node, ans in self.ancestors_and_namespaces:\n            namespaces |= ans\n\n        return namespaces\n\n    def remove(self) -> None:\n        \"\"\"Remove this node from the tree (subtree becomes independent).\"\"\"\n        parent = self.parent\n        if parent is not None:\n            parent.remove_subtree(self)\n\n    def is_empty(self) -> bool:\n        if self.path_id is not None:\n            return False\n        else:\n            return (\n                not self.children or\n                all(c.is_empty() for c in self.children)\n            )\n\n    def get_all_visible(self) -> set[pathid.PathId]:\n        paths = set()\n\n        for node in self.ancestors:\n            if node.path_id:\n                paths.add(node.path_id)\n            else:\n                for c in node.children:\n                    if c.path_id:\n                        paths.add(c.path_id)\n\n        return paths\n\n    def find_visible_ex(\n        self,\n        path_id: pathid.PathId,\n        *,\n        allow_group: bool=False,\n    ) -> tuple[\n        Optional[ScopeTreeNode],\n        FenceInfo,\n        AbstractSet[pathid.Namespace],\n    ]:\n        \"\"\"Find the visible node with the given *path_id*.\"\"\"\n        namespaces: set[pathid.Namespace] = set()\n        found = None\n        nodes: list[ScopeTreeNode] = []\n        for node, ans in self.ancestors_and_namespaces:\n            if (node.path_id is not None\n                    and _paths_equal(node.path_id, path_id, namespaces)):\n                found = node\n                break\n\n            for child in node.children:\n                if (child.path_id is not None\n                        and _paths_equal(child.path_id, path_id, namespaces)):\n                    found = child\n                    break\n\n            if found is not None:\n                break\n\n            namespaces |= ans\n\n            if node is not self:\n                nodes.append(node)\n\n        finfo = FenceInfo(False, False)\n        for node in nodes:\n            finfo |= node.fence_info_ex(path_id, namespaces)\n\n        if found and found.is_group and not allow_group:\n            found = None\n        return found, finfo, namespaces\n\n    def find_visible(\n        self, path_id: pathid.PathId, *, allow_group: bool = False\n    ) -> Optional[ScopeTreeNode]:\n        node, _, _ = self.find_visible_ex(path_id, allow_group=allow_group)\n        return node\n\n    def is_visible(\n        self, path_id: pathid.PathId, *, allow_group: bool = False\n    ) -> bool:\n        return self.find_visible(path_id, allow_group=allow_group) is not None\n\n    def is_any_prefix_visible(self, path_id: pathid.PathId) -> bool:\n        for prefix in reversed(list(path_id.iter_prefixes())):\n            if self.find_visible(prefix) is not None:\n                return True\n\n        return False\n\n    def find_child(\n        self,\n        path_id: pathid.PathId,\n        *,\n        in_branches: bool = False,\n        pfx_with_invariant_card: bool = False,\n    ) -> Optional[ScopeTreeNode]:\n        for child in self.children:\n            if child.path_id == path_id:\n                return child\n            if (\n                (\n                    in_branches\n                    and child.path_id is None\n                    and not child.fenced\n                ) or (\n                    pfx_with_invariant_card\n                    and child.path_id is not None\n                    # Type intersections have invariant cardinality\n                    # regardless of prefix visiblity.\n                    and child.path_id.is_type_intersection_path()\n                )\n            ):\n                desc = child.find_child(\n                    path_id,\n                    in_branches=True,\n                    pfx_with_invariant_card=pfx_with_invariant_card,\n                )\n                if desc is not None:\n                    return desc\n\n        return None\n\n    def find_descendant(\n        self,\n        path_id: pathid.PathId,\n    ) -> Optional[ScopeTreeNode]:\n        for descendant, dns, _ in self.strict_descendants_and_namespaces:\n            if (descendant.path_id is not None\n                    and _paths_equal(descendant.path_id, path_id, dns)):\n                return descendant\n\n        return None\n\n    def find_descendants(\n        self,\n        path_id: pathid.PathId,\n    ) -> list[ScopeTreeNodeWithPathId]:\n        matched = []\n        for descendant, dns, _ in self.strict_descendants_and_namespaces:\n            if (has_path_id(descendant)\n                    and _paths_equal(descendant.path_id, path_id, dns)):\n                matched.append(descendant)\n\n        return matched\n\n    def find_descendant_and_ns(self, path_id: pathid.PathId) -> tuple[\n        Optional[ScopeTreeNode],\n        AbstractSet[pathid.Namespace],\n        Optional[FenceInfo],\n    ]:\n        for descendant, dns, finfo in self.strict_descendants_and_namespaces:\n            if (descendant.path_id is not None\n                    and _paths_equal(descendant.path_id, path_id, dns)):\n                return descendant, dns, finfo\n\n        return None, frozenset(), None\n\n    def is_optional_upto(self, ancestor: Optional[ScopeTreeNode]) -> bool:\n        node: Optional[ScopeTreeNode] = self\n        while node and node is not ancestor:\n            if node.optional:\n                return True\n            node = node.parent\n        return False\n\n    def find_factorable_nodes(\n        self,\n        path_id: pathid.PathId,\n        *,\n        child_to_skip: Optional[ScopeTreeNode] = None,\n    ) -> list[\n        tuple[\n            ScopeTreeNodeWithPathId,\n            ScopeTreeNode,\n            AbstractSet[pathid.Namespace],\n            AbstractSet[pathid.Namespace],\n            FenceInfo,\n            bool,\n            bool,\n        ]\n    ]:\n        \"\"\"Find nodes factorable with path_id (if attaching path_id to self)\n\n        This is done by searching up the tree looking for an ancestor\n        node that has path_id as a descendant such that *at most one*\n        of self and the path_id descendant are fenced.\n\n        That descendant, then, is a factorable node, and the ancestor\n        is its factoring point.\n\n        We do this by tracking whether we have passed a fence on our\n        way up the tree, and only looking for unfenced descendants if\n        so.\n\n        We find all such factorable nodes and return them sorted by\n        factoring point, from closest to furthest up.\n        \"\"\"\n        namespaces: AbstractSet[str] = frozenset()\n        unnest_fence_seen = False\n        fence_seen = False\n        points = []\n        up_finfo = FenceInfo(False, False)\n\n        # Track the last seen node so that we can skip it while looking\n        # for descendants, to avoid performance pathologies, but also\n        # to avoid rediscovering the same nodes when searching higher\n        # in the tree.\n        last = child_to_skip\n\n        # Search up the tree\n        for node, ans in self.ancestors_and_namespaces:\n\n            # For each ancestor, search its descendants for path_id.\n            # If we have passed a fence on the way up, only look for\n            # unfenced descendants.\n            for descendant, dns, finfo in (\n                node.descendants_and_namespaces_ex(\n                    unfenced_only=fence_seen, skip=last)\n            ):\n                cns = namespaces | dns\n                if (has_path_id(descendant)\n                        and not descendant.is_group\n                        and _paths_equal(descendant.path_id, path_id, cns)):\n                    points.append((\n                        descendant, node, namespaces, dns, finfo | up_finfo,\n                        unnest_fence_seen, fence_seen,\n                    ))\n\n            namespaces |= ans\n            unnest_fence_seen |= node.unnest_fence\n            fence_seen |= node.fenced\n\n            if node is not self:\n                up_finfo |= node.fence_info_ex(path_id, namespaces)\n\n            last = node\n\n        return points\n\n    def pformat(self) -> str:\n        if self.children:\n            child_formats = []\n            for c in self.children:\n                cf = c.pformat()\n                if cf:\n                    child_formats.append(cf)\n\n            if child_formats:\n                children = textwrap.indent(',\\n'.join(child_formats), '    ')\n                return f'\"{self.name}\": {{\\n{children}\\n}}'\n\n        if self.path_id is not None:\n            return f'\"{self.name}\"'\n        else:\n            return ''\n\n    def pdebugformat(\n        self,\n        fuller: bool=False,\n        styles: Optional[Mapping[ScopeTreeNode, term.AbstractStyle]]=None,\n    ) -> str:\n        name = f'\"{self.debugname(fuller=fuller)}\"'\n        if styles and self in styles:\n            name = styles[self].apply(name)\n\n        if self.children:\n            child_formats = []\n            for c in self.children:\n                cf = c.pdebugformat(fuller=fuller, styles=styles)\n                if cf:\n                    child_formats.append(cf)\n\n            children = textwrap.indent(',\\n'.join(child_formats), '    ')\n            return f'{name}: {{\\n{children}\\n}}'\n        else:\n            return name\n\n    def dump(self) -> None:\n        print(self.pdebugformat())\n\n    def dump_full(self, others: Collection[ScopeTreeNode] = ()) -> None:\n        \"\"\"Do a debug dump of the root but hilight the current node.\"\"\"\n        styles = {}\n        if term.supports_colors(sys.stdout.fileno()):\n            styles[self] = term.Style16(color='magenta', bold=True)\n            for other in others:\n                styles[other] = term.Style16(color='blue', bold=True)\n        print(self.root.pdebugformat(styles=styles))\n\n    def _set_parent(self, parent: Optional[ScopeTreeNode]) -> None:\n        assert self is not parent\n        current_parent = self.parent\n        if parent is current_parent:\n            return\n\n        if current_parent is not None:\n            # Make sure no other node refers to us.\n            current_parent.children.remove(self)\n\n        if parent is not None:\n            self._parent = weakref.ref(parent)\n            parent.children.append(self)\n        else:\n            self._parent = None\n\n\nclass ScopeTreeNodeWithPathId(ScopeTreeNode):\n\n    path_id: pathid.PathId\n\n\ndef _paths_equal(\n    path_id_1: pathid.PathId,\n    path_id_2: pathid.PathId,\n    namespaces: AbstractSet[str],\n) -> bool:\n    if namespaces:\n        path_id_1 = path_id_1.strip_namespace(namespaces)\n        path_id_2 = path_id_2.strip_namespace(namespaces)\n\n    return path_id_1 == path_id_2\n"
  },
  {
    "path": "edb/ir/staeval.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Static evaluation of EdgeQL IR.\"\"\"\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    TypeVar,\n)\n\nimport decimal\nimport functools\nimport uuid\n\nimport immutables\n\n\nfrom edb import errors\n\nfrom edb.common import typeutils\nfrom edb.common import parsing\nfrom edb.common import value_dispatch\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.edgeql import qltypes\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtyputils\nfrom edb.ir import statypes as statypes\nfrom edb.ir import utils as irutils\n\nfrom edb.schema import name as sn\nfrom edb.schema import objects as s_obj\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import types as s_types\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import schema as s_schema\nfrom edb.schema import constraints as s_constr\nfrom edb.schema import pointers as s_pointers\n\nfrom edb.server import config\n\n\nclass StaticEvaluationError(errors.QueryError):\n    pass\n\n\nclass UnsupportedExpressionError(errors.QueryError):\n    pass\n\n\nEvaluationResult = irast.TypeCast | irast.ConstExpr | irast.Array | irast.Tuple\n\n\ndef evaluate_to_python_val(\n    ir: irast.Base,\n    schema: s_schema.Schema,\n) -> Any:\n    const = evaluate(ir, schema=schema)\n    return const_to_python(const, schema=schema)\n\n\n@functools.singledispatch\ndef evaluate(ir: irast.Base, schema: s_schema.Schema) -> EvaluationResult:\n    raise UnsupportedExpressionError(\n        f'no static IR evaluation handler for {ir.__class__}')\n\n\n@evaluate.register(irast.SelectStmt)\ndef evaluate_SelectStmt(\n    ir_stmt: irast.SelectStmt, schema: s_schema.Schema\n) -> EvaluationResult:\n\n    if irutils.is_trivial_select(ir_stmt) and not ir_stmt.result.is_binding:\n        return evaluate(ir_stmt.result, schema)\n    else:\n        raise UnsupportedExpressionError(\n            'expression is not constant', span=ir_stmt.span)\n\n\n@evaluate.register(irast.InsertStmt)\ndef evaluate_InsertStmt(\n    ir: irast.InsertStmt, schema: s_schema.Schema\n) -> EvaluationResult:\n    # InsertStmt should NOT be statically evaluated in general;\n    # This is a special case for inserting nested cfg::ConfigObject\n    # when it's evaluated into a named tuple and then squashed into\n    # a Python dict to be used in compile_structured_config().\n    tmp_schema, subject_type = irtyputils.ir_typeref_to_type(\n        schema, ir.subject.expr.typeref\n    )\n    config_obj = schema.get(\"cfg::ConfigObject\")\n    assert isinstance(config_obj, s_obj.SubclassableObject)\n    if subject_type.issubclass(tmp_schema, config_obj):\n        return irast.Tuple(\n            named=True,\n            typeref=ir.subject.typeref,\n            elements=[\n                irast.TupleElement(\n                    name=ptr_set.expr.ptrref.shortname.name,\n                    val=irast.Set(\n                        expr=evaluate(ptr_set.expr.expr, schema),\n                        typeref=ptr_set.typeref,\n                        path_id=ptr_set.path_id,\n                    ),\n                )\n                for ptr_set, _ in ir.subject.shape\n                if ptr_set.expr.ptrref.shortname.name != \"id\"\n                and ptr_set.expr.expr is not None\n            ],\n        )\n\n    raise UnsupportedExpressionError(\n        f'no static IR evaluation handler for general {ir.__class__}'\n    )\n\n\n@evaluate.register(irast.TypeIntrospection)\ndef evaluate_TypeIntrospection(\n    ir: irast.TypeIntrospection, schema: s_schema.Schema\n) -> EvaluationResult:\n    return irast.StaticIntrospection(\n        named=True, ir=ir, schema=schema, elements=[], typeref=ir.typeref\n    )\n\n\n@evaluate.register(irast.TypeCast)\ndef evaluate_TypeCast(\n    ir_cast: irast.TypeCast, schema: s_schema.Schema\n) -> EvaluationResult:\n\n    schema, from_type = irtyputils.ir_typeref_to_type(\n        schema, ir_cast.from_type)\n    schema, to_type = irtyputils.ir_typeref_to_type(\n        schema, ir_cast.to_type)\n\n    if (\n        not isinstance(from_type, s_scalars.ScalarType)\n        or not isinstance(to_type, s_scalars.ScalarType)\n    ):\n        raise UnsupportedExpressionError('object cast not supported')\n    scalar_type_to_python_type(from_type, schema)\n    scalar_type_to_python_type(to_type, schema)\n    evaluate(ir_cast.expr, schema)\n    return ir_cast\n\n\n@evaluate.register(irast.EmptySet)\ndef evaluate_EmptySet(\n    ir_set: irast.EmptySet, schema: s_schema.Schema\n) -> EvaluationResult:\n    return ir_set\n\n\n@evaluate.register(irast.Set)\ndef evaluate_Set(\n        ir_set: irast.Set,\n        schema: s_schema.Schema) -> EvaluationResult:\n    return evaluate(ir_set.expr, schema=schema)\n\n\n@evaluate.register\ndef evaluate_Pointer(\n    ptr: irast.Pointer, schema: s_schema.Schema\n) -> EvaluationResult:\n    if ptr.expr is not None:\n        return evaluate(ptr.expr, schema=schema)\n\n    elif (\n        ptr.direction == s_pointers.PointerDirection.Outbound\n        and isinstance(ptr.ptrref, irast.PointerRef)\n        and ptr.ptrref.out_cardinality.is_single()\n        and ptr.ptrref.out_target.is_scalar\n    ):\n        return evaluate_pointer_ref(\n            evaluate(ptr.source.expr, schema=schema), ptr.ptrref\n        )\n\n    else:\n        raise UnsupportedExpressionError(\n            'expression is not constant', span=ptr.span)\n\n\n@functools.singledispatch\ndef evaluate_pointer_ref(\n    evaluated_source: EvaluationResult, ptrref: irast.PointerRef\n) -> EvaluationResult:\n    raise UnsupportedExpressionError(\n        f'unsupported PointerRef on source {evaluated_source}',\n        span=ptrref.span,\n    )\n\n\n@evaluate_pointer_ref.register(irast.StaticIntrospection)\ndef evaluate_pointer_ref_StaticIntrospection(\n    source: irast.StaticIntrospection, ptrref: irast.PointerRef\n) -> EvaluationResult:\n    return source.get_field_value(ptrref.shortname)\n\n\n@evaluate.register(irast.ConstExpr)\ndef evaluate_BaseConstant(\n    ir_const: irast.ConstExpr, schema: s_schema.Schema\n) -> EvaluationResult:\n    return ir_const\n\n\n@evaluate.register(irast.Array)\ndef evaluate_Array(\n    ir: irast.Array, schema: s_schema.Schema\n) -> EvaluationResult:\n    return irast.Array(\n        elements=tuple(\n            x.replace(expr=evaluate(x, schema)) for x in ir.elements\n        ),\n        typeref=ir.typeref,\n    )\n\n\n@evaluate.register(irast.Tuple)\ndef evaluate_Tuple(\n    ir: irast.Tuple, schema: s_schema.Schema\n) -> EvaluationResult:\n    return irast.Tuple(\n        named=ir.named,\n        elements=[\n            x.replace(\n                val=x.val.replace(\n                    expr=evaluate(x.val, schema)\n                ),\n            ) for x in ir.elements\n        ],\n        typeref=ir.typeref,\n    )\n\n\ndef _process_op_result(\n    value: object,\n    typeref: irast.TypeRef,\n    schema: s_schema.Schema,\n    *,\n    span: Optional[parsing.Span]=None,\n) -> irast.ConstExpr:\n    qlconst: qlast.BaseConstant\n    if isinstance(value, str):\n        qlconst = qlast.Constant.string(value)\n    elif isinstance(value, bool):\n        qlconst = qlast.Constant.boolean(value)\n    else:\n        raise UnsupportedExpressionError(\n            f\"unsupported result type: {type(value)}\", span=span\n        )\n\n    result = qlcompiler.compile_constant_tree_to_ir(\n        qlconst, styperef=typeref, schema=schema)\n\n    assert isinstance(result, irast.ConstExpr), 'expected ConstExpr'\n    return result\n\n\nop_table = {\n    # Concatenation\n    ('Infix', 'std::++'): lambda a, b: a + b,\n    ('Infix', 'std::>='): lambda a, b: a >= b,\n    ('Infix', 'std::>'): lambda a, b: a > b,\n    ('Infix', 'std::<='): lambda a, b: a <= b,\n    ('Infix', 'std::<'): lambda a, b: a < b,\n    ('Infix', 'std::='): lambda a, b: a == b,\n    ('Infix', 'std::!='): lambda a, b: a != b,\n}\n\n\n@evaluate.register(irast.OperatorCall)\ndef evaluate_OperatorCall(\n    opcall: irast.OperatorCall, schema: s_schema.Schema\n) -> irast.ConstExpr:\n\n    if irutils.is_union_expr(opcall):\n        return _evaluate_union(opcall, schema)\n\n    eval_func = op_table.get(\n        (opcall.operator_kind, str(opcall.func_shortname)),\n    )\n    if eval_func is None:\n        raise UnsupportedExpressionError(\n            f'unsupported operator: {opcall.func_shortname}',\n            span=opcall.span)\n\n    args: dict[int, irast.CallArg] = {}\n    for key, arg in opcall.args.items():\n        arg_val = evaluate_to_python_val(arg.expr, schema=schema)\n        if isinstance(arg_val, tuple):\n            raise UnsupportedExpressionError(\n                f'non-singleton operations are not supported',\n                span=opcall.span)\n        if arg_val is None:\n            raise UnsupportedExpressionError(\n                f'empty operations are not supported',\n                span=opcall.span)\n        if isinstance(key, str):\n            raise UnsupportedExpressionError(\n                f'named arguments are not allowed for operators',\n                span=opcall.span)\n\n        args[key] = arg_val\n\n    args_list: list[irast.CallArg] = []\n    for key in range(len(args)):\n        if key not in args:\n            raise UnsupportedExpressionError(\n                f'missing positional argument {key}',\n                span=opcall.span)\n\n        args_list.append(args[key])\n\n    value = eval_func(*args_list)\n    return _process_op_result(\n        value, opcall.typeref, schema, span=opcall.span)\n\n\n@evaluate.register(irast.SliceIndirection)\ndef evaluate_SliceIndirection(\n    slice: irast.SliceIndirection, schema: s_schema.Schema\n) -> irast.ConstExpr:\n\n    args = [slice.expr, slice.start, slice.stop]\n    vals = [\n        evaluate_to_python_val(arg, schema=schema) if arg else None\n        for arg in args\n    ]\n\n    for arg, arg_val in zip(args, vals):\n        if arg is None:\n            continue\n        if isinstance(arg_val, tuple):\n            raise UnsupportedExpressionError(\n                f'non-singleton operations are not supported',\n                span=slice.span)\n        if arg_val is None:\n            raise UnsupportedExpressionError(\n                f'empty operations are not supported',\n                span=slice.span)\n\n    base, start, stop = vals\n\n    value = base[start:stop]  # type: ignore[index]\n    return _process_op_result(\n        value, slice.expr.typeref, schema, span=slice.span)\n\n\ndef _evaluate_union(\n    opcall: irast.OperatorCall, schema: s_schema.Schema\n) -> irast.ConstExpr:\n\n    elements: list[irast.BaseConstant] = []\n    for arg in opcall.args.values():\n        val = evaluate(arg.expr, schema=schema)\n        if isinstance(val, irast.TypeCast):\n            val = evaluate(val.expr, schema=schema)\n        if isinstance(val, irast.ConstantSet):\n            for el in val.elements:\n                if isinstance(el, irast.BaseParameter):\n                    raise UnsupportedExpressionError(\n                        f'{el!r} not supported in UNION',\n                        span=opcall.span)\n                elements.append(el)\n        elif isinstance(val, irast.EmptySet):\n            empty_set = val\n        elif isinstance(val, irast.BaseConstant):\n            elements.append(val)\n        else:\n            raise UnsupportedExpressionError(\n                f'{val!r} not supported in UNION',\n                span=opcall.span)\n\n    if elements:\n        return irast.ConstantSet(\n            elements=tuple(elements),\n            typeref=next(iter(elements)).typeref,\n        )\n    else:\n        # We get an empty set if the UNION was exclusivly empty set\n        # literals. If that happens, grab one of the empty sets\n        # that we saw and return it.\n        return empty_set\n\n\n@functools.singledispatch\ndef const_to_python(ir: irast.Expr | None, schema: s_schema.Schema) -> Any:\n    raise UnsupportedExpressionError(f'cannot convert {ir!r} to Python value')\n\n\n@const_to_python.register(irast.EmptySet)\ndef empty_set_to_python(\n    ir: irast.EmptySet,\n    schema: s_schema.Schema,\n) -> None:\n    return None\n\n\n@const_to_python.register(irast.ConstantSet)\ndef const_set_to_python(\n    ir: irast.ConstantSet, schema: s_schema.Schema\n) -> tuple[Any, ...]:\n    return tuple(const_to_python(v, schema) for v in ir.elements)\n\n\n@const_to_python.register(irast.Array)\ndef array_const_to_python(ir: irast.Array, schema: s_schema.Schema) -> Any:\n    return [const_to_python(x.expr, schema) for x in ir.elements]\n\n\n@const_to_python.register(irast.Tuple)\ndef tuple_const_to_python(ir: irast.Tuple, schema: s_schema.Schema) -> Any:\n    if ir.named:\n        return {\n            x.name: const_to_python(x.val.expr, schema) for x in ir.elements\n        }\n    else:\n        return tuple(\n            const_to_python(x.val.expr, schema) for x in ir.elements\n        )\n\n\n@const_to_python.register(irast.IntegerConstant)\ndef int_const_to_python(\n    ir: irast.IntegerConstant, schema: s_schema.Schema\n) -> Any:\n\n    stype = schema.get_by_id(ir.typeref.id)\n    assert isinstance(stype, s_types.Type)\n    bigint = schema.get('std::bigint', type=s_obj.SubclassableObject)\n    if stype.issubclass(schema, bigint):\n        return decimal.Decimal(ir.value)\n    else:\n        return int(ir.value)\n\n\n@const_to_python.register(irast.FloatConstant)\ndef float_const_to_python(\n    ir: irast.FloatConstant, schema: s_schema.Schema\n) -> Any:\n\n    stype = schema.get_by_id(ir.typeref.id)\n    assert isinstance(stype, s_types.Type)\n    bigint = schema.get('std::bigint', type=s_obj.SubclassableObject)\n    if stype.issubclass(schema, bigint):\n        return decimal.Decimal(ir.value)\n    else:\n        return float(ir.value)\n\n\n@const_to_python.register(irast.StringConstant)\ndef str_const_to_python(\n    ir: irast.StringConstant, schema: s_schema.Schema\n) -> Any:\n\n    return ir.value\n\n\n@const_to_python.register(irast.BooleanConstant)\ndef bool_const_to_python(\n    ir: irast.BooleanConstant, schema: s_schema.Schema\n) -> Any:\n\n    return ir.value == 'true'\n\n\n@const_to_python.register(irast.TypeCast)\ndef cast_const_to_python(ir: irast.TypeCast, schema: s_schema.Schema) -> Any:\n\n    schema, stype = irtyputils.ir_typeref_to_type(schema, ir.to_type)\n    if not isinstance(stype, s_scalars.ScalarType):\n        raise UnsupportedExpressionError(\n            \"non-scalar casts are not supported in Python eval\")\n    pytype = scalar_type_to_python_type(stype, schema)\n    sval = evaluate_to_python_val(ir.expr, schema=schema)\n    return python_cast(sval, pytype)\n\n\n@functools.singledispatch\ndef python_cast(sval: Any, pytype: type) -> Any:\n    return pytype(sval)\n\n\n@python_cast.register(type(None))\ndef python_cast_none(sval: None, pytype: type) -> None:\n    return None\n\n\n@python_cast.register(tuple)\ndef python_cast_tuple(sval: tuple[Any, ...], pytype: type) -> Any:\n    return tuple(python_cast(elem, pytype) for elem in sval)\n\n\n@python_cast.register(str)\ndef python_cast_str(sval: str, pytype: type) -> Any:\n    if pytype is bool:\n        if sval.lower() == 'true':\n            return True\n        elif sval.lower() == 'false':\n            return False\n        else:\n            raise errors.InvalidValueError(\n                f\"invalid input syntax for type bool: {sval!r}\",\n                hint=\"bool value can only be one of: true, false\"\n            )\n    else:\n        return pytype(sval)\n\n\ndef schema_type_to_python_type(\n    stype: s_types.Type, schema: s_schema.Schema\n) -> type | statypes.CompositeTypeSpec:\n    if isinstance(stype, s_scalars.ScalarType):\n        return scalar_type_to_python_type(stype, schema)\n    elif isinstance(stype, s_objtypes.ObjectType):\n        return object_type_to_spec(\n            stype, schema, spec_class=statypes.CompositeTypeSpec)\n    else:\n        raise UnsupportedExpressionError(\n            f'{stype.get_displayname(schema)} is not representable in Python')\n\n\ndef scalar_type_to_python_type(\n    stype: s_scalars.ScalarType,\n    schema: s_schema.Schema,\n) -> type:\n    typname = stype.get_name(schema)\n    pytype = statypes.maybe_get_python_type_for_scalar_type_name(str(typname))\n    if pytype is None:\n        for ancestor in stype.get_ancestors(schema).objects(schema):\n            typname = ancestor.get_name(schema)\n            pytype = statypes.maybe_get_python_type_for_scalar_type_name(\n                str(typname))\n            if pytype is not None:\n                break\n\n    if pytype is not None:\n        return pytype\n    elif stype.is_enum(schema):\n        return str\n\n    raise UnsupportedExpressionError(\n        f'{stype.get_displayname(schema)} is not representable in Python')\n\n\nT_spec = TypeVar('T_spec', bound=statypes.CompositeTypeSpec)\n\n\nclass _Missing:\n    pass\n\n\ndef object_type_to_spec(\n    objtype: s_objtypes.ObjectType,\n    schema: s_schema.Schema,\n    *,\n    # We pass a spec_class so that users like the config system can ask for\n    # their own subtyped versions of a spec.\n    spec_class: type[T_spec],\n    parent: Optional[T_spec] = None,\n    _memo: Optional[dict[s_types.Type, T_spec | type]] = None,\n) -> T_spec:\n    if _memo is None:\n        _memo = {}\n    # Prevent infinite recursion\n    _memo[objtype] = _Missing\n\n    default: Any\n    fields = {}\n\n    for pn, p in objtype.get_pointers(schema).items(schema):\n        assert isinstance(p, s_pointers.Pointer)\n        str_pn = str(pn)\n        if str_pn in ('id', '__type__'):\n            continue\n\n        ptype = p.get_target(schema)\n        assert ptype is not None\n\n        if isinstance(ptype, s_objtypes.ObjectType):\n            pytype = _memo.get(ptype)\n            if pytype is _Missing:\n                raise UnsupportedExpressionError()\n            if pytype is None:\n                pytype = object_type_to_spec(\n                    ptype, schema, spec_class=spec_class,\n                    parent=parent, _memo=_memo)\n                _memo[ptype] = pytype\n        elif isinstance(ptype, s_scalars.ScalarType):\n            pytype = scalar_type_to_python_type(ptype, schema)\n        else:\n            raise UnsupportedExpressionError(f\"unsupported cast type: {ptype}\")\n\n        ptr_card: qltypes.SchemaCardinality = p.get_cardinality(schema)\n        if ptr_card.is_known():\n            is_multi = ptr_card.is_multi()\n        else:\n            raise UnsupportedExpressionError()\n\n        if is_multi:\n            pytype = frozenset[pytype]  # type: ignore\n\n        default = p.get_default(schema)\n        if default is None:\n            if p.get_required(schema):\n                default = statypes.MISSING\n        else:\n            default = qlcompiler.evaluate_to_python_val(\n                default.text, schema=schema)\n            if is_multi and not isinstance(default, frozenset):\n                default = frozenset((default,))\n\n        constraints = p.get_constraints(schema).objects(schema)\n        exclusive = schema.get('std::exclusive', type=s_constr.Constraint)\n        unique = (\n            not ptype.is_object_type()\n            and any(\n                c.issubclass(schema, exclusive) and not c.get_delegated(schema)\n                for c in constraints\n            )\n        )\n        fields[str_pn] = statypes.CompositeTypeSpecField(\n            name=str_pn,\n            type=pytype,\n            unique=unique,\n            default=default,\n            secret=p.get_secret(schema),\n            protected=p.get_protected(schema),\n        )\n\n    spec = spec_class(\n        name=str(objtype.get_name(schema)),\n        fields=immutables.Map(fields),\n        parent=parent,\n    )\n\n    for subtype in objtype.children(schema):\n        spec.children.append(\n            object_type_to_spec(\n                subtype, schema,\n                spec_class=spec_class,\n                parent=spec, _memo=_memo))\n\n    return spec\n\n\n@functools.singledispatch\ndef evaluate_to_config_op(\n    ir: irast.Base, schema: s_schema.Schema\n) -> config.Operation:\n    raise UnsupportedExpressionError(\n        f'no config op evaluation handler for {ir.__class__}')\n\n\n@evaluate_to_config_op.register(irast.ConfigSet)\ndef evaluate_config_set(\n    ir: irast.ConfigSet, schema: s_schema.Schema\n) -> config.Operation:\n\n    if ir.scope == qltypes.ConfigScope.GLOBAL:\n        raise UnsupportedExpressionError(\n            'SET GLOBAL is not supported by static eval'\n        )\n\n    value = evaluate_to_python_val(ir.expr, schema)\n    if ir.cardinality is qltypes.SchemaCardinality.Many:\n        if value is None:\n            value = []\n        elif not typeutils.is_container(value):\n            value = [value]\n\n    return config.Operation(\n        opcode=config.OpCode.CONFIG_SET,\n        scope=ir.scope,\n        setting_name=ir.name,\n        value=value,\n    )\n\n\n@evaluate_to_config_op.register(irast.ConfigReset)\ndef evaluate_config_reset(\n    ir: irast.ConfigReset, schema: s_schema.Schema\n) -> config.Operation:\n\n    if ir.selector is not None:\n        raise UnsupportedExpressionError(\n            'filtered CONFIGURE RESET is not supported by static eval'\n        )\n\n    return config.Operation(\n        opcode=config.OpCode.CONFIG_RESET,\n        scope=ir.scope,\n        setting_name=ir.name,\n        value=None,\n    )\n\n\n@evaluate_to_config_op.register(irast.ConfigInsert)\ndef evaluate_config_insert(\n    ir: irast.ConfigInsert, schema: s_schema.Schema\n) -> config.Operation:\n    return config.Operation(\n        opcode=config.OpCode.CONFIG_ADD,\n        scope=ir.scope,\n        setting_name=ir.name,\n        value=evaluate_to_python_val(\n            irast.InsertStmt(subject=ir.expr), schema=schema\n        ),\n    )\n\n\n@value_dispatch.value_dispatch\ndef coerce_py_const(\n    type_id: uuid.UUID, val: Any\n) -> irast.ConstExpr | irast.TypeCast:\n    raise UnsupportedExpressionError(f\"unimplemented coerce type: {type_id}\")\n\n\n@coerce_py_const.register(s_obj.get_known_type_id(\"std::str\"))\ndef evaluate_std_str(\n    type_id: uuid.UUID, val: Any\n) -> irast.ConstExpr | irast.TypeCast:\n    return irast.StringConstant(\n        typeref=irast.TypeRef(\n            id=type_id, name_hint=sn.name_from_string(\"std::str\")\n        ),\n        value=str(val),\n    )\n\n\n@coerce_py_const.register(s_obj.get_known_type_id(\"std::bool\"))\ndef evaluate_std_bool(\n    type_id: uuid.UUID, val: Any\n) -> irast.ConstExpr | irast.TypeCast:\n    return irast.BooleanConstant(\n        typeref=irast.TypeRef(\n            id=type_id, name_hint=sn.name_from_string(\"std::bool\")\n        ),\n        value=str(bool(val)).lower(),\n    )\n\n\n@coerce_py_const.register(s_obj.get_known_type_id(\"std::uuid\"))\ndef evaluate_std_uuid(\n    type_id: uuid.UUID, val: Any\n) -> irast.ConstExpr | irast.TypeCast:\n    str_type_id = s_obj.get_known_type_id(\"std::str\")\n    str_typeref = irast.TypeRef(\n        id=str_type_id, name_hint=sn.name_from_string(\"std::str\")\n    )\n    return irast.TypeCast(\n        from_type=str_typeref,\n        to_type=irast.TypeRef(\n            id=type_id, name_hint=sn.name_from_string(\"std::uuid\")\n        ),\n        expr=irast.Set(\n            expr=irast.StringConstant(typeref=str_typeref, value=str(val)),\n            typeref=str_typeref,\n            path_id=irast.PathId.from_typeref(str_typeref),\n        ),\n        sql_cast=True,\n        sql_expr=False,\n    )\n"
  },
  {
    "path": "edb/ir/statypes.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    ClassVar,\n    Mapping,\n    Optional,\n    Self,\n    TYPE_CHECKING,\n)\n\nimport dataclasses\nimport datetime\nimport decimal\nimport enum\nimport functools\nimport re\nimport struct\nimport uuid\n\nimport immutables\n\nfrom edb import errors\nfrom edb.common import parametric\nfrom edb.common import uuidgen\n\nfrom edb.schema import name as s_name\nfrom edb.schema import objects as s_obj\n\nif TYPE_CHECKING:\n    from edb.edgeql import qltypes\n\nMISSING: Any = object()\n\n\n@dataclasses.dataclass(frozen=True)\nclass CompositeTypeSpecField:\n    name: str\n    type: type | CompositeTypeSpec\n    _: dataclasses.KW_ONLY\n    unique: bool = False\n    default: Any = MISSING\n    secret: bool = False\n    protected: bool = False\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass CompositeTypeSpec:\n    name: str\n    fields: immutables.Map[str, CompositeTypeSpecField]\n    parent: Optional[CompositeTypeSpec] = None\n    children: list[CompositeTypeSpec] = dataclasses.field(\n        default_factory=list, hash=False, compare=False\n    )\n    has_secret: bool = False\n\n    def __post_init__(self) -> None:\n        has_secret = any(\n            field.secret\n            or (\n                isinstance(field, CompositeTypeSpec)\n                # We look at children of pointer targets, and not\n                # children of the object itself, on the idea that for\n                # config objects, omitting individual top level\n                # objects with secrets should be fine.\n                and (\n                    field.has_secret\n                    or any(child.has_secret for child in field.children)\n                )\n            )\n            for field in self.fields.values()\n        )\n        object.__setattr__(self, 'has_secret', has_secret)\n\n    @property\n    def __name__(self) -> str:\n        return self.name\n\n    def get_field_unique_site(self, name: str) -> Optional[CompositeTypeSpec]:\n        typ: Optional[CompositeTypeSpec] = self\n        site: Optional[CompositeTypeSpec] = None\n        while typ:\n            if name in typ.fields and typ.fields[name].unique:\n                site = typ\n            typ = typ.parent\n        return site\n\n\nclass CompositeType:\n    _tspec: CompositeTypeSpec\n\n    def to_json_value(self, redacted: bool = False) -> dict[str, Any]:\n        raise NotImplementedError\n\n\nclass ScalarType:\n    def __init__(self, val: str, /) -> None:\n        raise NotImplementedError\n\n    def to_backend_str(self) -> str:\n        raise NotImplementedError\n\n    @classmethod\n    def to_backend_expr(cls, expr: str) -> str:\n        raise NotImplementedError(\"{cls}.to_backend_expr()\")\n\n    @classmethod\n    def to_frontend_expr(cls, expr: str) -> Optional[str]:\n        raise NotImplementedError(\"{cls}.to_frontend_expr()\")\n\n    def to_json(self) -> str:\n        raise NotImplementedError\n\n    def encode(self) -> bytes:\n        raise NotImplementedError\n\n    @classmethod\n    def decode(cls, data: bytes) -> ScalarType:\n        raise NotImplementedError\n\n\n@functools.total_ordering\nclass Duration(ScalarType):\n\n    _pg_simple_parser = re.compile(r'''\n        ^\n        \\s*\n        (\n            (?P<sign>(\\+|\\-)?)\n        )\n        (\n            (?P<hours>\\d+)\n        )\n        :\n        (\n            (?P<minutes>\\d+)?\n            (\n                :(?P<seconds>\\d+)\n                (\n                    \\.(?P<milliseconds>\\d{0,3})\n                    (?P<microseconds>\\d{0,3})\n                    (?P<submicro>\\d*)\n                )?\n            )?\n        )?\n        \\s*\n        $\n    ''', re.X)\n\n    _pg_parser = re.compile(r'''\n        (\n            (\n                \\s*\n                (?P<hours>(\\+|\\-)?\\d+)\n                \\s*\n                (h|hr|hrs|hour|hours)\n                \\s*\n            )\n            |\n            (\n                \\s*\n                (?P<minutes>(\\+|\\-)?\\d+)\n                \\s*\n                (m|min|mins|minute|minutes)\n                \\s*\n            )\n            |\n            (\n                \\s*\n                (?P<milliseconds>(\\+|\\-)?\\d+)\n                \\s*\n                (ms | (millisecon(s|d|ds)?))  # '12 millisecon' is valid\n                \\s*\n            )\n            |\n            (\n                \\s*\n                (?P<microseconds>(\\+|\\-)?\\d+)\n                \\s*\n                (us | (microsecond(s)?))\n                \\s*\n            )\n            |\n            (\n                \\s*\n                (?P<seconds>(\\+|\\-)?\\d+)\n                (\n                    (\\s* $) | (\\s* (s|sec|secs|second|seconds))\n                )\n                \\s*\n            )\n        )(?=$ | \\d | \\s)\n        |\n        (\n            \\s*\n            (?P<error>.+)\n        )\n    ''', re.X | re.I)\n\n    _iso_parser = re.compile(r'''\n        ^\n        PT\n        (\n            (?P<hours>(\\+|\\-)?\\d+) H\n        )?\n        (\n            (?P<minutes>(\\+|\\-)?\\d+) M\n        )?\n        (\n            (\n                (?P<secsign>\\+|\\-)?\n                (?P<seconds>\\d+)\n                (\n                    \\.\n                    (?P<microseconds>\\d+)\n                )?\n            ) S\n        )?\n        $\n    ''', re.X)\n\n    _codec = struct.Struct('!QLL')\n\n    _value: int  # microseconds\n\n    def __init__(\n        self, pg_text: str = '', /, *, microseconds: Optional[int] = None\n    ) -> None:\n        if pg_text == '' and microseconds is not None:\n            self._value = microseconds\n        else:\n            self._value = self._us_from_pg_text(pg_text)\n\n    def _us_from_pg_text(self, input: str, /) -> int:\n        try:\n            seconds = int(input)\n        except ValueError:\n            pass\n        else:\n            return seconds * 1000 * 1000\n\n        m = self._pg_simple_parser.match(input)\n        if m is not None:\n            value = 0\n            parsed = m.groupdict()\n            if parsed['hours']:\n                hours = int(parsed['hours'])\n                if 0 <= hours <= 2147483647:\n                    value += hours * 3600_000_000\n                else:\n                    raise errors.NumericOutOfRangeError(\n                        'interval field value out of range')\n            if parsed['minutes']:\n                mins = int(parsed['minutes'])\n                if 0 <= mins <= 59:\n                    value += mins * 60_000_000\n                else:\n                    raise errors.NumericOutOfRangeError(\n                        'interval field value out of range')\n            if parsed['seconds']:\n                secs = int(parsed['seconds'])\n                if 0 <= secs <= 59:\n                    value += secs * 1_000_000\n                else:\n                    raise errors.NumericOutOfRangeError(\n                        'interval field value out of range')\n            if parsed['milliseconds']:\n                value += int(parsed['milliseconds'].ljust(3, '0')) * 1_000\n            if parsed['microseconds']:\n                value += int(parsed['microseconds'].ljust(3, '0'))\n            if parsed['submicro'] and int(parsed['submicro'][:1]) >= 5:\n                value += 1\n            if parsed['sign'] == '-':\n                value = -value\n\n            return value\n\n        if (parsed_iso := self._parse_iso8601(input)) is not None:\n            return parsed_iso\n\n        value = 0\n        seen: set[str] = set()\n        for m in self._pg_parser.finditer(input):\n            filtered = {\n                k: v for k, v in m.groupdict().items()\n                if v is not None\n            }\n            if len(filtered) != 1:\n                raise errors.InvalidValueError(\n                    'invalid input syntax for type std::duration')\n\n            kind, val = next(iter(filtered.items()))\n            if kind == 'error':\n                raise errors.InvalidValueError(\n                    f'invalid input syntax for type std::duration: '\n                    f'unable to parse {val!r}')\n            if kind in seen:\n                raise errors.InvalidValueError(\n                    f'invalid input syntax for type std::duration: '\n                    f'the {kind!r} component has been specified '\n                    f'more than once')\n            seen.add(kind)\n\n            intval = int(val)\n            if kind == 'hours':\n                value += intval * 3600_000_000\n            elif kind == 'minutes':\n                value += intval * 60_000_000\n            elif kind == 'seconds':\n                value += intval * 1_000_000\n            elif kind == 'milliseconds':\n                value += intval * 1_000\n            elif kind == 'microseconds':\n                value += intval\n\n        return value\n\n    @classmethod\n    def _parse_iso8601(cls, input: str, /) -> Optional[int]:\n        m = cls._iso_parser.match(input)\n        if not m:\n            return None\n\n        value = 0\n        if m['hours']:\n            value += int(m['hours']) * 3600_000_000\n        if m['minutes']:\n            value += int(m['minutes']) * 60_000_000\n\n        secsign = -1 if m['secsign'] == '-' else +1\n        if m['seconds']:\n            value += int(m['seconds']) * 1_000_000 * secsign\n        if m['microseconds']:\n            ms = m['microseconds'][:6]\n            ms = ms.ljust(6, '0')\n            value += int(ms) * secsign\n\n        return value\n\n    @classmethod\n    def from_iso8601(cls, input: str, /) -> Duration:\n        val = cls._parse_iso8601(input)\n        if val is None:\n            raise errors.InvalidValueError(\n                f'invalid input syntax for type std::duration: '\n                f'cannot parse {input!r} as ISO 8601')\n        return cls(microseconds=val)\n\n    @classmethod\n    def from_microseconds(cls, input: int, /) -> Duration:\n        return cls(microseconds=input)\n\n    def to_microseconds(self) -> int:\n        return self._value\n\n    def __lt__(self, other: Duration) -> bool:\n        return self._value < other._value\n\n    def to_iso8601(self) -> str:\n        neg = '-' if self._value < 0 else ''\n        seconds, usecs = divmod(abs(self._value), 1_000_000)\n        minutes, seconds = divmod(seconds, 60)\n        hours, minutes = divmod(minutes, 60)\n        ret = ['PT']\n        if hours:\n            ret.append(f'{neg}{hours}H')\n        if minutes:\n            ret.append(f'{neg}{minutes}M')\n        if seconds or usecs:\n            if usecs:\n                ret.append(f\"{neg}{seconds}.\")\n                ret.append(f\"{str(usecs).rjust(6, '0')}\"[:6].rstrip('0'))\n            else:\n                ret.append(f'{neg}{seconds}')\n            ret.append('S')\n        if ret == ['PT']:\n            ret.append('0S')\n        return ''.join(ret)\n\n    def to_timedelta(self) -> datetime.timedelta:\n        return datetime.timedelta(microseconds=self.to_microseconds())\n\n    def to_backend_str(self) -> str:\n        return f'{self.to_microseconds()}us'\n\n    @classmethod\n    def to_backend_expr(cls, expr: str) -> str:\n        return f\"edgedb_VER._interval_to_ms(({expr})::interval)::text || 'ms'\"\n\n    @classmethod\n    def to_frontend_expr(cls, expr: str) -> Optional[str]:\n        return None\n\n    def to_json(self) -> str:\n        return self.to_iso8601()\n\n    def __repr__(self) -> str:\n        return f'<statypes.Duration {self.to_iso8601()!r}>'\n\n    def encode(self) -> bytes:\n        return self._codec.pack(self._value, 0, 0)\n\n    @classmethod\n    def decode(cls, data: bytes) -> Duration:\n        return cls(microseconds=cls._codec.unpack(data)[0])\n\n    def __hash__(self) -> int:\n        return hash(self._value)\n\n    def __eq__(self, other: object) -> bool:\n        if isinstance(other, Duration):\n            return self._value == other._value\n        else:\n            return False\n\n\n@functools.total_ordering\nclass ConfigMemory(ScalarType):\n\n    PiB = 1024 * 1024 * 1024 * 1024 * 1024\n    TiB = 1024 * 1024 * 1024 * 1024\n    GiB = 1024 * 1024 * 1024\n    MiB = 1024 * 1024\n    KiB = 1024\n\n    _parser = re.compile(r'''\n        ^\n        (?P<num>\\d+)\n        (?P<unit>B|KiB|MiB|GiB|TiB|PiB)\n        $\n    ''', re.X)\n\n    _value: int\n\n    def __init__(\n        self,\n        val: str | int,\n        /,\n    ) -> None:\n        if isinstance(val, int):\n            self._value = val\n        elif isinstance(val, str):\n            text = val\n            if text == '0':\n                self._value = 0\n                return\n\n            m = self._parser.match(text)\n            if m is None:\n                raise errors.InvalidValueError(\n                    f'unable to parse memory size: {text!r}')\n\n            num = int(m.group('num'))\n            unit = m.group('unit')\n\n            if unit == 'B':\n                self._value = num\n            elif unit == 'KiB':\n                self._value = num * self.KiB\n            elif unit == 'MiB':\n                self._value = num * self.MiB\n            elif unit == 'GiB':\n                self._value = num * self.GiB\n            elif unit == 'TiB':\n                self._value = num * self.TiB\n            elif unit == 'PiB':\n                self._value = num * self.PiB\n            else:\n                raise AssertionError('unexpected unit')\n        else:\n            raise ValueError(\n                f\"invalid ConfigMemory value: {type(val)}, expected int | str\")\n\n    def __lt__(self, other: ConfigMemory) -> bool:\n        return self._value < other._value\n\n    def to_nbytes(self) -> int:\n        return self._value\n\n    def to_str(self) -> str:\n        if self._value >= self.PiB and self._value % self.PiB == 0:\n            return f'{self._value // self.PiB}PiB'\n\n        if self._value >= self.TiB and self._value % self.TiB == 0:\n            return f'{self._value // self.TiB}TiB'\n\n        if self._value >= self.GiB and self._value % self.GiB == 0:\n            return f'{self._value // self.GiB}GiB'\n\n        if self._value >= self.MiB and self._value % self.MiB == 0:\n            return f'{self._value // self.MiB}MiB'\n\n        if self._value >= self.KiB and self._value % self.KiB == 0:\n            return f'{self._value // self.KiB}KiB'\n\n        return f'{self._value}B'\n\n    def to_backend_str(self) -> str:\n        if self._value >= self.TiB and self._value % self.TiB == 0:\n            return f'{self._value // self.TiB}TB'\n\n        if self._value >= self.GiB and self._value % self.GiB == 0:\n            return f'{self._value // self.GiB}GB'\n\n        if self._value >= self.MiB and self._value % self.MiB == 0:\n            return f'{self._value // self.MiB}MB'\n\n        if self._value >= self.KiB and self._value % self.KiB == 0:\n            return f'{self._value // self.KiB}kB'\n\n        return f'{self._value}B'\n\n    @classmethod\n    def to_backend_expr(cls, expr: str) -> str:\n        return f\"edgedb_VER.cfg_memory_to_str({expr})\"\n\n    @classmethod\n    def to_frontend_expr(cls, expr: str) -> Optional[str]:\n        return f\"(edgedb_VER.str_to_cfg_memory({expr})::text || 'B')\"\n\n    def to_json(self) -> str:\n        return self.to_str()\n\n    def __repr__(self) -> str:\n        return f'<statypes.ConfigMemory {self.to_str()!r}>'\n\n    def __hash__(self) -> int:\n        return hash(self._value)\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, ConfigMemory):\n            return self._value == other._value\n        else:\n            return False\n\n\ntypemap = {\n    'std::str': str,\n    'std::anyint': int,\n    'std::anyfloat': float,\n    'std::decimal': decimal.Decimal,\n    'std::bigint': decimal.Decimal,\n    'std::bool': bool,\n    'std::json': str,\n    'std::uuid': uuidgen.UUID,\n    'std::duration': Duration,\n    'cfg::memory': ConfigMemory,\n}\n\n\ndef maybe_get_python_type_for_scalar_type_name(name: str) -> Optional[type]:\n    return typemap.get(name)\n\n\nclass EnumScalarType[E: enum.StrEnum](\n    ScalarType,\n    parametric.SingleParametricType[E],\n):\n    \"\"\"Configuration value represented by a custom string enum type that\n    supports arbitrary value mapping to backend (Postgres) configuration\n    values, e.g mapping \"Enabled\"/\"Disabled\" enum to a bool value, etc.\n\n    We use SingleParametricType to obtain runtime access to the Generic\n    type arg to avoid having to copy-paste the constructors.\n    \"\"\"\n\n    _val: E\n    _eql_type: ClassVar[Optional[s_name.QualName]]\n\n    def __init_subclass__(\n        cls,\n        *,\n        edgeql_type: Optional[str] = None,\n        **kwargs: Any,\n    ) -> None:\n        global typemap\n        super().__init_subclass__(**kwargs)\n        if edgeql_type is not None:\n            if edgeql_type in typemap:\n                raise TypeError(\n                    f\"{edgeql_type} is already a registered EnumScalarType\")\n            typemap[edgeql_type] = cls\n            cls._eql_type = s_name.QualName.from_string(edgeql_type)\n\n    def __init__(\n        self,\n        val: E | str,\n    ) -> None:\n        if isinstance(val, self.type):\n            self._val = val\n        elif isinstance(val, str):\n            try:\n                self._val = self.type(val)\n            except ValueError:\n                raise errors.InvalidValueError(\n                    f'unexpected backend value for '\n                    f'{self.__class__.__name__}: {val!r}'\n                ) from None\n\n    def to_str(self) -> str:\n        return str(self._val)\n\n    def to_json(self) -> str:\n        return self._val\n\n    def encode(self) -> bytes:\n        return self._val.encode(\"utf8\")\n\n    @classmethod\n    def get_translation_map(cls) -> Mapping[E, str]:\n        raise NotImplementedError\n\n    @classmethod\n    def decode(cls, data: bytes) -> Self:\n        return cls(val=cls.type(data.decode(\"utf8\")))\n\n    def __repr__(self) -> str:\n        return f\"<statypes.{self.__class__.__name__} '{self._val}'>\"\n\n    def __hash__(self) -> int:\n        return hash(self._val)\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, type(self)):\n            return self._val == other._val\n        else:\n            return NotImplemented\n\n    def __reduce__(self) -> tuple[\n        Callable[..., EnumScalarType[Any]],\n        tuple[\n            Optional[tuple[type, ...] | type],\n            E,\n        ],\n    ]:\n        assert type(self).is_fully_resolved(), \\\n            f'{type(self)} parameters are not resolved'\n\n        cls: type[EnumScalarType[E]] = self.__class__\n        types: Optional[tuple[type, ...]] = self.orig_args\n        if types is None or not cls.is_anon_parametrized():\n            typeargs = None\n        else:\n            typeargs = types[0] if len(types) == 1 else types\n        return (cls.__restore__, (typeargs, self._val))\n\n    @classmethod\n    def __restore__(\n        cls,\n        typeargs: Optional[tuple[type, ...] | type],\n        val: E,\n    ) -> Self:\n        if typeargs is None or cls.is_anon_parametrized():\n            obj = cls(val)\n        else:\n            obj = cls[typeargs](val)  # type: ignore[index]\n\n        return obj\n\n    @classmethod\n    def get_edgeql_typeid(cls) -> uuid.UUID:\n        return s_obj.get_known_type_id('std::str')\n\n    @classmethod\n    def get_edgeql_type(cls) -> s_name.QualName:\n        \"\"\"Return fully-qualified name of the scalar type for this setting.\"\"\"\n        assert cls._eql_type is not None\n        return cls._eql_type\n\n    def to_backend_str(self) -> str:\n        \"\"\"Convert static frontend config value to backend config value.\"\"\"\n        return self.get_translation_map()[self._val]\n\n    @classmethod\n    def to_backend_expr(cls, expr: str) -> str:\n        \"\"\"Convert dynamic backend config value to frontend config value.\"\"\"\n        cases_list = []\n        for fe_val, be_val in cls.get_translation_map().items():\n            cases_list.append(f\"WHEN lower('{fe_val}') THEN '{be_val}'\")\n        cases = \"\\n\".join(cases_list)\n        errmsg = f\"unexpected frontend value for {cls.__name__}: %s\"\n        err = f\"edgedb_VER.raise(NULL::text, msg => format('{errmsg}', v))\"\n        return (\n            f\"(SELECT CASE v\\n{cases}\\nELSE\\n{err}\\nEND \"\n            f\"FROM lower(({expr})) AS f(v))\"\n        )\n\n    @classmethod\n    def to_frontend_expr(cls, expr: str) -> Optional[str]:\n        \"\"\"Convert dynamic frontend config value to backend config value.\"\"\"\n        cases_list = []\n        for fe_val, be_val in cls.get_translation_map().items():\n            cases_list.append(f\"WHEN lower('{be_val}') THEN '{fe_val}'\")\n        cases = \"\\n\".join(cases_list)\n        errmsg = f\"unexpected backend value for {cls.__name__}: %s\"\n        err = f\"edgedb_VER.raise(NULL::text, msg => format('{errmsg}', v))\"\n        return (\n            f\"(SELECT CASE v\\n{cases}\\nELSE\\n{err}\\nEND \"\n            f\"FROM lower(({expr})) AS f(v))\"\n        )\n\n\nclass EnabledDisabledEnum(enum.StrEnum):\n    Enabled = \"Enabled\"\n    Disabled = \"Disabled\"\n\n\nclass EnabledDisabledType(\n    EnumScalarType[EnabledDisabledEnum],\n    edgeql_type=\"cfg::TestEnabledDisabledEnum\",\n):\n    @classmethod\n    def get_translation_map(cls) -> Mapping[EnabledDisabledEnum, str]:\n        return {\n            EnabledDisabledEnum.Enabled: \"true\",\n            EnabledDisabledEnum.Disabled: \"false\",\n        }\n\n\nclass TransactionAccessModeEnum(enum.StrEnum):\n    ReadOnly = \"ReadOnly\"\n    ReadWrite = \"ReadWrite\"\n\n\nclass TransactionAccessMode(\n    EnumScalarType[TransactionAccessModeEnum],\n    edgeql_type=\"sys::TransactionAccessMode\",\n):\n    @classmethod\n    def get_translation_map(cls) -> Mapping[TransactionAccessModeEnum, str]:\n        return {\n            TransactionAccessModeEnum.ReadOnly: \"true\",\n            TransactionAccessModeEnum.ReadWrite: \"false\",\n        }\n\n    def to_qltypes(self) -> qltypes.TransactionAccessMode:\n        from edb.edgeql import qltypes\n        match self._val:\n            case TransactionAccessModeEnum.ReadOnly:\n                return qltypes.TransactionAccessMode.READ_ONLY\n            case TransactionAccessModeEnum.ReadWrite:\n                return qltypes.TransactionAccessMode.READ_WRITE\n            case _:\n                raise AssertionError(f\"unexpected value: {self._val!r}\")\n\n\nclass TransactionDeferrabilityEnum(enum.StrEnum):\n    Deferrable = \"Deferrable\"\n    NotDeferrable = \"NotDeferrable\"\n\n\nclass TransactionDeferrability(\n    EnumScalarType[TransactionDeferrabilityEnum],\n    edgeql_type=\"sys::TransactionDeferrability\",\n):\n    @classmethod\n    def get_translation_map(cls) -> Mapping[TransactionDeferrabilityEnum, str]:\n        return {\n            TransactionDeferrabilityEnum.Deferrable: \"true\",\n            TransactionDeferrabilityEnum.NotDeferrable: \"false\",\n        }\n\n\nclass TransactionIsolationEnum(enum.StrEnum):\n    Serializable = \"Serializable\"\n    RepeatableRead = \"RepeatableRead\"\n\n\nclass TransactionIsolation(\n    EnumScalarType[TransactionIsolationEnum],\n    edgeql_type=\"sys::TransactionIsolation\",\n):\n    @classmethod\n    def get_translation_map(cls) -> Mapping[TransactionIsolationEnum, str]:\n        return {\n            TransactionIsolationEnum.Serializable: \"serializable\",\n            TransactionIsolationEnum.RepeatableRead: \"repeatable read\",\n        }\n\n    def to_qltypes(self) -> qltypes.TransactionIsolationLevel:\n        from edb.edgeql import qltypes\n        match self._val:\n            case TransactionIsolationEnum.Serializable:\n                return qltypes.TransactionIsolationLevel.SERIALIZABLE\n            case TransactionIsolationEnum.RepeatableRead:\n                return qltypes.TransactionIsolationLevel.REPEATABLE_READ\n            case _:\n                raise AssertionError(f\"unexpected value: {self._val!r}\")\n"
  },
  {
    "path": "edb/ir/typeutils.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2015-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Utilities for IR type descriptors.\"\"\"\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    Iterable,\n    Optional,\n    TYPE_CHECKING,\n    overload,\n)\n\nimport uuid\n\nfrom edb.edgeql import qltypes\n\nfrom edb.schema import casts as s_casts\nfrom edb.schema import links as s_links\nfrom edb.schema import name as s_name\nfrom edb.schema import properties as s_props\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import types as s_types\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import objects as s_obj\nfrom edb.schema import utils as s_utils\n\nfrom . import ast as irast\n\nif TYPE_CHECKING:\n\n    from edb.schema import schema as s_schema\n\n\nTypeRefCacheKey = tuple[uuid.UUID, bool, bool]\nPtrRefCacheKey = s_pointers.PointerLike\n\nPtrRefCache = dict[PtrRefCacheKey, 'irast.BasePointerRef']\nTypeRefCache = dict[TypeRefCacheKey, 'irast.TypeRef']\n\n\n# Modules where all the \"types\" in them are really just custom views\n# provided by metaschema.\nVIEW_MODULES = ('sys', 'cfg')\n\n\ndef is_cfg_view(\n    obj: s_obj.Object,\n    schema: s_schema.Schema,\n) -> bool:\n    return (\n        isinstance(obj, (s_objtypes.ObjectType, s_pointers.Pointer))\n        and (\n            obj.get_name(schema).module in VIEW_MODULES\n            or bool(\n                (cfg_object := schema.get(\n                    'cfg::ConfigObject',\n                    type=s_objtypes.ObjectType, default=None\n                ))\n                and (\n                    nobj := (\n                        obj if isinstance(obj, s_objtypes.ObjectType)\n                        else obj.get_source(schema)\n                    )\n                )\n                and nobj.issubclass(schema, cfg_object)\n            )\n        )\n    )\n\n\ndef is_excluded_cfg_view(\n    child: s_obj.Object,\n    *,\n    ancestor: s_obj.Object,\n    schema: s_schema.Schema,\n) -> bool:\n    \"\"\"Used to exclude sys/cfg tables from non sys/cfg views for performance.\n\n    Also used by access policies to prevent including cfg views in\n    when expanding a non-cfg type's descendants (#8865).\n    \"\"\"\n    return is_cfg_view(child, schema) and not is_cfg_view(ancestor, schema)\n\n\ndef is_scalar(typeref: irast.TypeRef) -> bool:\n    \"\"\"Return True if *typeref* describes a scalar type.\"\"\"\n    return typeref.is_scalar\n\n\ndef is_object(typeref: irast.TypeRef) -> bool:\n    \"\"\"Return True if *typeref* describes an object type.\"\"\"\n    return (\n        not is_scalar(typeref)\n        and not is_collection(typeref)\n        and not is_generic(typeref)\n    )\n\n\ndef is_view(typeref: irast.TypeRef) -> bool:\n    \"\"\"Return True if *typeref* describes a view.\"\"\"\n    return typeref.is_view\n\n\ndef is_collection(typeref: irast.TypeRef) -> bool:\n    \"\"\"Return True if *typeref* describes an collection type.\"\"\"\n    return bool(typeref.collection)\n\n\ndef is_array(typeref: irast.TypeRef) -> bool:\n    \"\"\"Return True if *typeref* describes an array type.\"\"\"\n    return typeref.collection == s_types.Array.get_schema_name()\n\n\ndef is_tuple(typeref: irast.TypeRef) -> bool:\n    \"\"\"Return True if *typeref* describes a tuple type.\"\"\"\n    return typeref.collection == s_types.Tuple.get_schema_name()\n\n\ndef is_range(typeref: irast.TypeRef) -> bool:\n    \"\"\"Return True if *typeref* describes a range type.\"\"\"\n    return typeref.collection == s_types.Range.get_schema_name()\n\n\ndef is_multirange(typeref: irast.TypeRef) -> bool:\n    \"\"\"Return True if *typeref* describes a multirange type.\"\"\"\n    return typeref.collection == s_types.MultiRange.get_schema_name()\n\n\ndef is_any(typeref: irast.TypeRef) -> bool:\n    \"\"\"Return True if *typeref* describes the ``anytype`` generic type.\"\"\"\n    return isinstance(typeref, irast.AnyTypeRef)\n\n\ndef is_anytuple(typeref: irast.TypeRef) -> bool:\n    \"\"\"Return True if *typeref* describes the ``anytuple`` generic type.\"\"\"\n    return isinstance(typeref, irast.AnyTupleRef)\n\n\ndef is_anyobject(typeref: irast.TypeRef) -> bool:\n    \"\"\"Return True if *typeref* describes the ``anyobject`` generic type.\"\"\"\n    return isinstance(typeref, irast.AnyObjectRef)\n\n\ndef is_generic(typeref: irast.TypeRef) -> bool:\n    \"\"\"Return True if *typeref* describes a generic type.\"\"\"\n    if is_collection(typeref):\n        return any(is_generic(st) for st in typeref.subtypes)\n    else:\n        return is_any(typeref) or is_anytuple(typeref) or is_anyobject(typeref)\n\n\ndef is_abstract(typeref: irast.TypeRef) -> bool:\n    \"\"\"Return True if *typeref* describes an abstract type.\"\"\"\n    return typeref.is_abstract\n\n\ndef is_json(typeref: irast.TypeRef) -> bool:\n    \"\"\"Return True if *typeref* describes the json type.\"\"\"\n    return typeref.real_base_type.id == s_obj.get_known_type_id('std::json')\n\n\ndef is_bytes(typeref: irast.TypeRef) -> bool:\n    \"\"\"Return True if *typeref* describes the bytes type.\"\"\"\n    return typeref.real_base_type.id == s_obj.get_known_type_id('std::bytes')\n\n\ndef is_exactly_free_object(typeref: irast.TypeRef) -> bool:\n    return typeref.name_hint == s_name.QualName('std', 'FreeObject')\n\n\ndef is_free_object(typeref: irast.TypeRef) -> bool:\n    if typeref.material_type:\n        typeref = typeref.material_type\n    return is_exactly_free_object(typeref)\n\n\ndef is_persistent_tuple(typeref: irast.TypeRef) -> bool:\n    if is_tuple(typeref):\n        if typeref.material_type is not None:\n            material = typeref.material_type\n        else:\n            material = typeref\n\n        return material.in_schema\n    else:\n        return False\n\n\ndef is_empty_typeref(typeref: irast.TypeRef) -> bool:\n    return typeref.union is not None and len(typeref.union) == 0\n\n\ndef needs_custom_serialization(typeref: irast.TypeRef) -> bool:\n    # True if any component needs custom serialization\n    return contains_predicate(\n        typeref,\n        lambda typeref:\n            typeref.real_base_type.custom_sql_serialization is not None\n    )\n\n\ndef contains_predicate(\n    typeref: irast.TypeRef,\n    pred: Callable[[irast.TypeRef], bool],\n) -> bool:\n    if pred(typeref):\n        return True\n\n    elif typeref.union:\n        return any(\n            contains_predicate(sub, pred) for sub in typeref.union\n        )\n    return any(\n        contains_predicate(sub, pred) for sub in typeref.subtypes\n    )\n\n\ndef contains_object(typeref: irast.TypeRef) -> bool:\n    return contains_predicate(typeref, is_object)\n\n\ndef type_to_typeref(\n    schema: s_schema.Schema,\n    t: s_types.Type,\n    *,\n    cache: Optional[dict[TypeRefCacheKey, irast.TypeRef]],\n    typename: Optional[s_name.QualName] = None,\n    include_children: bool = False,\n    include_ancestors: bool = False,\n    _name: Optional[str] = None,\n) -> irast.TypeRef:\n    \"\"\"Return an instance of :class:`ir.ast.TypeRef` for a given type.\n\n    An IR TypeRef is an object that fully describes a schema type for\n    the purposes of query compilation.\n\n    Args:\n        schema:\n            A schema instance, in which the type *t* is defined.\n        t:\n            A schema type instance.\n        cache:\n            Optional mapping from (type UUID, typename) to cached IR TypeRefs.\n        typename:\n            Optional name hint to use for the type in the returned\n            TypeRef.  If ``None``, the type name is used.\n        include_children:\n            Whether to include the description of all material type children\n            of *t*.\n        include_ancestors:\n            Whether to include the description of all material type ancestors\n            of *t*.\n        _name:\n            Optional subtype element name if this type is a collection within\n            a Tuple,\n\n    Returns:\n        A ``TypeRef`` instance corresponding to the given schema type.\n    \"\"\"\n\n    if cache is not None and typename is None:\n        key = (t.id, include_children, include_ancestors)\n\n        cached_result = cache.get(key)\n        if cached_result is not None:\n            # If the schema changed due to an ongoing compilation, the name\n            # hint might be outdated.\n            if cached_result.name_hint == t.get_name(schema):\n                return cached_result\n\n    # We separate the uncached version into another function because\n    # it makes it easy to tell in a profiler when the cache isn't\n    # operating, and because if the cache *is* operating it is no\n    # great loss.\n    return _type_to_typeref(\n        schema,\n        t,\n        cache=cache,\n        typename=typename,\n        include_children=include_children,\n        include_ancestors=include_ancestors,\n        _name=_name,\n    )\n\n\ndef _type_to_typeref(\n    schema: s_schema.Schema,\n    t: s_types.Type,\n    *,\n    cache: Optional[dict[TypeRefCacheKey, irast.TypeRef]] = None,\n    typename: Optional[s_name.QualName] = None,\n    include_children: bool = False,\n    include_ancestors: bool = False,\n    _name: Optional[str] = None,\n) -> irast.TypeRef:\n\n    def _typeref(\n        t: s_types.Type,\n        *,\n        include_children: bool = include_children,\n        include_ancestors: bool = include_ancestors,\n    ) -> irast.TypeRef:\n        return type_to_typeref(\n            schema,\n            t,\n            include_children=include_children,\n            include_ancestors=include_ancestors,\n            cache=cache,\n        )\n\n    result: irast.TypeRef\n    material_type: s_types.Type\n\n    name_hint = typename or t.get_name(schema)\n    orig_name_hint = None if not typename else t.get_name(schema)\n\n    if t.is_anytuple(schema):\n        result = irast.AnyTupleRef(\n            id=t.id,\n            name_hint=name_hint,\n            orig_name_hint=orig_name_hint,\n        )\n    elif t.is_anyobject(schema):\n        result = irast.AnyObjectRef(\n            id=t.id,\n            name_hint=name_hint,\n            orig_name_hint=orig_name_hint,\n        )\n    elif t.is_any(schema):\n        result = irast.AnyTypeRef(\n            id=t.id,\n            name_hint=name_hint,\n            orig_name_hint=orig_name_hint,\n        )\n    elif not isinstance(t, s_types.Collection):\n        assert isinstance(t, s_types.InheritingType)\n\n        union: Optional[frozenset[irast.TypeRef]] = None\n        union_is_exhaustive: bool = False\n        expr_intersection: Optional[frozenset[irast.TypeRef]] = None\n        expr_union: Optional[frozenset[irast.TypeRef]] = None\n        if t.is_union_type(schema) or t.is_intersection_type(schema):\n            union_types, union_is_exhaustive = (\n                s_utils.get_type_expr_non_overlapping_union(t, schema)\n            )\n\n            union = frozenset(\n                _typeref(c) for c in union_types\n            )\n\n            # Keep track of type expression structure.\n            # This is necessary to determine the correct rvar when doing\n            # type intersections or polymorphic queries.\n            if expr_intersection_types := t.get_intersection_of(schema):\n                expr_intersection = frozenset(\n                    _typeref(c)\n                    for c in expr_intersection_types.objects(schema)\n                )\n\n            if expr_union_types := t.get_union_of(schema):\n                expr_union = frozenset(\n                    _typeref(c)\n                    for c in expr_union_types.objects(schema)\n                )\n\n        schema, material_type = t.material_type(schema)\n\n        material_typeref: Optional[irast.TypeRef]\n        if material_type != t:\n            material_typeref = _typeref(material_type)\n        else:\n            material_typeref = None\n\n        if (isinstance(material_type, s_scalars.ScalarType)\n                and not material_type.get_abstract(schema)):\n            base_type = material_type.get_topmost_concrete_base(schema)\n            if base_type == material_type:\n                base_typeref = None\n            else:\n                assert isinstance(base_type, s_types.Type)\n                base_typeref = _typeref(base_type, include_children=False)\n        else:\n            base_typeref = None\n\n        children: Optional[frozenset[irast.TypeRef]] = None\n        if (\n            material_typeref is None\n            and include_children\n            and children is None\n        ):\n            children = frozenset(\n                _typeref(child, include_children=True)\n                for child in t.children(schema)\n                if not child.get_is_derived(schema)\n                and not child.is_compound_type(schema)\n            )\n\n        ancestors: Optional[frozenset[irast.TypeRef]] = None\n        if (\n            material_typeref is None\n            and include_ancestors\n            and ancestors is None\n        ):\n            ancestors = frozenset(\n                _typeref(ancestor, include_ancestors=False)\n                for ancestor in t.get_ancestors(schema).objects(schema)\n            )\n\n        sql_type = None\n        needs_custom_json_cast = False\n        custom_sql_serialization = None\n        if isinstance(t, s_scalars.ScalarType):\n            sql_type = t.resolve_sql_type(schema)\n            if material_typeref is None:\n                cast_name = s_casts.get_cast_fullname_from_names(\n                    orig_name_hint or name_hint,\n                    s_name.QualName('std', 'json'))\n                jcast = schema.get(cast_name, type=s_casts.Cast, default=None)\n                if jcast:\n                    needs_custom_json_cast = bool(jcast.get_code(schema))\n\n            custom_sql_serialization = t.get_custom_sql_serialization(schema)\n\n        result = irast.TypeRef(\n            id=t.id,\n            name_hint=name_hint,\n            orig_name_hint=orig_name_hint,\n            material_type=material_typeref,\n            base_type=base_typeref,\n            children=children,\n            ancestors=ancestors,\n            union=union,\n            union_is_exhaustive=union_is_exhaustive,\n            expr_intersection=expr_intersection,\n            expr_union=expr_union,\n            element_name=_name,\n            is_scalar=t.is_scalar(),\n            is_abstract=t.get_abstract(schema),\n            is_view=t.is_view(schema),\n            is_cfg_view=is_cfg_view(t, schema),\n            is_opaque_union=t.get_is_opaque_union(schema),\n            needs_custom_json_cast=needs_custom_json_cast,\n            sql_type=sql_type,\n            custom_sql_serialization=custom_sql_serialization,\n        )\n    elif isinstance(t, s_types.Tuple) and t.is_named(schema):\n        schema, material_type = t.material_type(schema)\n\n        if material_type != t:\n            material_typeref = _typeref(material_type)\n        else:\n            material_typeref = None\n\n        result = irast.TypeRef(\n            id=t.id,\n            name_hint=name_hint,\n            orig_name_hint=orig_name_hint,\n            material_type=material_typeref,\n            element_name=_name,\n            collection=t.get_schema_name(),\n            in_schema=t.get_is_persistent(schema),\n            subtypes=tuple(\n                # ??? no cache\n                type_to_typeref(schema, st, _name=sn, cache=None)\n                for sn, st in t.iter_subtypes(schema)\n            )\n        )\n    else:\n        schema, material_type = t.material_type(schema)\n\n        if material_type != t:\n            material_typeref = type_to_typeref(\n                schema, material_type, cache=cache\n            )\n        else:\n            material_typeref = None\n\n        result = irast.TypeRef(\n            id=t.id,\n            name_hint=name_hint,\n            orig_name_hint=orig_name_hint,\n            material_type=material_typeref,\n            element_name=_name,\n            collection=t.get_schema_name(),\n            in_schema=t.get_is_persistent(schema),\n            subtypes=tuple(\n                _typeref(st) for st in t.get_subtypes(schema)\n            )\n        )\n\n    if cache is not None and typename is None and _name is None:\n        key = (t.id, include_children, include_ancestors)\n\n        # Note: there is no cache for `_name` variants since they are only used\n        # for Tuple subtypes and thus they will be cached on the outer level\n        # anyway.\n        # There's also no variant for types with custom typenames since they\n        # proved to have a very low hit rate.\n        # This way we save on the size of the key tuple.\n        cache[key] = result\n    return result\n\n\ndef ir_typeref_to_type(\n    schema: s_schema.Schema,\n    typeref: irast.TypeRef,\n) -> tuple[s_schema.Schema, s_types.Type]:\n    \"\"\"Return a schema type for a given IR TypeRef.\n\n    This is the reverse of :func:`~type_to_typeref`.\n\n    Args:\n        schema:\n            A schema instance. The result type must exist in it.\n        typeref:\n            A :class:`ir.ast.TypeRef` instance for which to return\n            the corresponding schema type.\n\n    Returns:\n        A tuple containing the possibly modified schema and\n        a :class:`schema.types.Type` instance corresponding to the\n        given *typeref*.\n    \"\"\"\n    # Optimistically try to lookup the type by id. Sometimes for\n    # arrays and tuples this will fail, and we'll need to create it.\n    t = schema.get_by_id(typeref.id, default=None, type=s_types.Type)\n    if t:\n        return schema, t\n\n    elif is_tuple(typeref):\n        named = False\n        tuple_subtypes = {}\n        for si, st in enumerate(typeref.subtypes):\n            if st.element_name:\n                named = True\n                type_name = st.element_name\n            else:\n                type_name = str(si)\n\n            schema, st_t = ir_typeref_to_type(schema, st)\n            tuple_subtypes[type_name] = st_t\n\n        return s_types.Tuple.from_subtypes(\n            schema, tuple_subtypes, {'named': named})\n\n    elif is_array(typeref):\n        array_subtypes = []\n        for st in typeref.subtypes:\n            schema, st_t = ir_typeref_to_type(schema, st)\n            array_subtypes.append(st_t)\n\n        return s_types.Array.from_subtypes(schema, array_subtypes)\n\n    else:\n        raise AssertionError(\"couldn't find type from typeref\")\n\n\n@overload\ndef ptrref_from_ptrcls(\n    *,\n    schema: s_schema.Schema,\n    ptrcls: s_pointers.Pointer,\n    cache: Optional[PtrRefCache],\n    typeref_cache: Optional[TypeRefCache],\n) -> irast.PointerRef:\n    ...\n\n\n@overload\ndef ptrref_from_ptrcls(\n    *,\n    schema: s_schema.Schema,\n    ptrcls: s_pointers.PointerLike,\n    cache: Optional[PtrRefCache],\n    typeref_cache: Optional[TypeRefCache],\n) -> irast.BasePointerRef:\n    ...\n\n\ndef ptrref_from_ptrcls(\n    *,\n    schema: s_schema.Schema,\n    ptrcls: s_pointers.PointerLike,\n    cache: Optional[PtrRefCache],\n    typeref_cache: Optional[TypeRefCache],\n) -> irast.BasePointerRef:\n    \"\"\"Return an IR pointer descriptor for a given schema pointer.\n\n    An IR PointerRef is an object that fully describes a schema pointer for\n    the purposes of query compilation.\n\n    Args:\n        schema:\n            A schema instance, in which the type *t* is defined.\n        ptrcls:\n            A :class:`schema.pointers.Pointer` instance for which to\n            return the PointerRef.\n        direction:\n            The direction of the pointer in the path expression.\n\n    Returns:\n        An instance of a subclass of :class:`ir.ast.BasePointerRef`\n        corresponding to the given schema pointer.\n    \"\"\"\n\n    if cache is not None:\n        cached = cache.get(ptrcls)\n        if cached is not None:\n            return cached\n\n    kwargs: dict[str, Any] = {}\n\n    ircls: type[irast.BasePointerRef]\n\n    source_ref: Optional[irast.TypeRef]\n    target_ref: Optional[irast.TypeRef]\n    out_source: Optional[irast.TypeRef]\n\n    if isinstance(ptrcls, irast.TupleIndirectionLink):\n        ircls = irast.TupleIndirectionPointerRef\n    elif isinstance(ptrcls, irast.TypeIntersectionLink):\n        ircls = irast.TypeIntersectionPointerRef\n        kwargs['optional'] = ptrcls.is_optional()\n        kwargs['is_empty'] = ptrcls.is_empty()\n        kwargs['is_subtype'] = ptrcls.is_subtype()\n        kwargs['rptr_specialization'] = ptrcls.get_rptr_specialization()\n    elif isinstance(ptrcls, s_pointers.Pointer):\n        ircls = irast.PointerRef\n        kwargs['id'] = ptrcls.id\n        kwargs['defined_here'] = ptrcls.get_defined_here(schema)\n        if backlink := ptrcls.get_computed_link_alias(schema):\n            assert isinstance(backlink, s_pointers.Pointer)\n            kwargs['computed_link_alias'] = ptrref_from_ptrcls(\n                ptrcls=backlink, schema=schema,\n                cache=cache, typeref_cache=typeref_cache,\n            )\n            kwargs['computed_link_alias_is_backward'] = (\n                ptrcls.get_computed_link_alias_is_backward(schema))\n\n    else:\n        raise AssertionError(f'unexpected pointer class: {ptrcls}')\n\n    target = ptrcls.get_target(schema)\n    if target is not None and not isinstance(target, irast.TypeRef):\n        assert isinstance(target, s_types.Type)\n        target_ref = type_to_typeref(\n            schema, target, include_children=True, cache=typeref_cache)\n    else:\n        target_ref = target\n\n    source = ptrcls.get_source(schema)\n\n    source_ptr: Optional[irast.BasePointerRef]\n    if (isinstance(ptrcls, s_props.Property)\n            and isinstance(source, s_links.Link)):\n        source_ptr = ptrref_from_ptrcls(\n            ptrcls=source,\n            schema=schema,\n            cache=cache,\n            typeref_cache=typeref_cache,\n        )\n        source_ref = None\n    else:\n        if source is not None and not isinstance(source, irast.TypeRef):\n            assert isinstance(source, s_types.Type)\n            source_ref = type_to_typeref(schema,\n                                         source,\n                                         include_ancestors=True,\n                                         cache=typeref_cache)\n        else:\n            source_ref = source\n        source_ptr = None\n\n    out_source = source_ref\n    out_target = target_ref\n\n    out_cardinality, in_cardinality = cardinality_from_ptrcls(\n        schema, ptrcls)\n\n    schema, material_ptrcls = ptrcls.material_type(schema)\n    material_ptr: Optional[irast.BasePointerRef]\n    if material_ptrcls is not None and material_ptrcls != ptrcls:\n        material_ptr = ptrref_from_ptrcls(\n            ptrcls=material_ptrcls,\n            schema=schema,\n            cache=cache,\n            typeref_cache=typeref_cache,\n        )\n    else:\n        material_ptr = None\n\n    union_components: Optional[set[irast.BasePointerRef]] = None\n    union_of = ptrcls.get_union_of(schema)\n    union_is_exhaustive = False\n    if union_of:\n        union_ptrs = set()\n\n        for component in union_of.objects(schema):\n            assert isinstance(component, s_pointers.Pointer)\n            schema, material_comp = component.material_type(schema)\n            union_ptrs.add(material_comp)\n\n        non_overlapping, union_is_exhaustive = (\n            s_utils.get_non_overlapping_union(\n                schema,\n                union_ptrs,\n            )\n        )\n\n        union_components = {\n            ptrref_from_ptrcls(\n                ptrcls=p,\n                schema=schema,\n                cache=cache,\n                typeref_cache=typeref_cache,\n            ) for p in non_overlapping\n        }\n\n    intersection_components: Optional[set[irast.BasePointerRef]] = None\n    intersection_of = ptrcls.get_intersection_of(schema)\n    if intersection_of:\n        intersection_ptrs = set()\n\n        for component in intersection_of.objects(schema):\n            assert isinstance(component, s_pointers.Pointer)\n            schema, material_comp = component.material_type(schema)\n            intersection_ptrs.add(material_comp)\n\n        intersection_components = {\n            ptrref_from_ptrcls(\n                ptrcls=p,\n                schema=schema,\n                cache=cache,\n                typeref_cache=typeref_cache,\n            ) for p in intersection_ptrs\n        }\n\n    std_parent_name = None\n    for ancestor in ptrcls.get_ancestors(schema).objects(schema):\n        ancestor_name = ancestor.get_name(schema)\n        if ancestor_name.module == 'std' and ancestor.is_non_concrete(schema):\n            std_parent_name = ancestor_name\n            break\n\n    is_derived = ptrcls.get_is_derived(schema)\n    base_ptr: Optional[irast.BasePointerRef]\n    if is_derived:\n        base_ptrcls = ptrcls.get_bases(schema).first(schema)\n        top_ptr_name = type(base_ptrcls).get_default_base_name()\n        if base_ptrcls.get_name(schema) != top_ptr_name:\n            base_ptr = ptrref_from_ptrcls(\n                ptrcls=base_ptrcls,\n                schema=schema,\n                cache=cache,\n                typeref_cache=typeref_cache,\n            )\n        else:\n            base_ptr = None\n    else:\n        base_ptr = None\n\n    if (\n        material_ptr is None\n        and isinstance(ptrcls, s_pointers.Pointer)\n    ):\n        children = frozenset(\n            ptrref_from_ptrcls(\n                ptrcls=child,\n                schema=schema,\n                cache=cache,\n                typeref_cache=typeref_cache,\n            )\n            for child in ptrcls.children(schema)\n            if not child.get_is_derived(schema)\n        )\n    else:\n        children = frozenset()\n\n    kwargs.update(\n        dict(\n            out_source=out_source,\n            out_target=out_target,\n            name=ptrcls.get_name(schema),\n            shortname=ptrcls.get_shortname(schema),\n            std_parent_name=std_parent_name,\n            source_ptr=source_ptr,\n            base_ptr=base_ptr,\n            material_ptr=material_ptr,\n            children=children,\n            is_derived=ptrcls.get_is_derived(schema),\n            is_computable=ptrcls.get_computable(schema),\n            union_components=union_components,\n            intersection_components=intersection_components,\n            union_is_exhaustive=union_is_exhaustive,\n            has_properties=ptrcls.has_user_defined_properties(schema),\n            in_cardinality=in_cardinality,\n            out_cardinality=out_cardinality,\n        )\n    )\n\n    ptrref = ircls(**kwargs)\n\n    if cache is not None:\n        cache[ptrcls] = ptrref\n\n        # This is kind of unfortunate, but if we are caching, update the\n        # base_ptr with this child\n        if base_ptr and not material_ptr and ptrref not in base_ptr.children:\n            base_ptr.children = base_ptr.children | frozenset([ptrref])\n\n    return ptrref\n\n\n@overload\ndef ptrcls_from_ptrref(\n    ptrref: irast.PointerRef,\n    *,\n    schema: s_schema.Schema,\n) -> tuple[s_schema.Schema, s_pointers.Pointer]:\n    ...\n\n\n@overload\ndef ptrcls_from_ptrref(\n    ptrref: irast.BasePointerRef,\n    *,\n    schema: s_schema.Schema,\n) -> tuple[s_schema.Schema, s_pointers.PointerLike]:\n    ...\n\n\ndef ptrcls_from_ptrref(\n    ptrref: irast.BasePointerRef,\n    *,\n    schema: s_schema.Schema,\n) -> tuple[s_schema.Schema, s_pointers.PointerLike]:\n    \"\"\"Return a schema pointer for a given IR PointerRef.\n\n    This is the reverse of :func:`~type_to_typeref`.\n\n    Args:\n        schema:\n            A schema instance. The result type must exist in it.\n        ptrref:\n            A :class:`ir.ast.BasePointerRef` instance for which to return\n            the corresponding schema pointer.\n\n    Returns:\n        A tuple containing the possibly modifed schema and\n        a :class:`schema.pointers.PointerLike` instance corresponding to the\n        given *ptrref*.\n    \"\"\"\n\n    ptrcls: s_pointers.PointerLike\n\n    if isinstance(ptrref, irast.TupleIndirectionPointerRef):\n        schema, src_t = ir_typeref_to_type(schema, ptrref.out_source)\n        schema, tgt_t = ir_typeref_to_type(schema, ptrref.out_target)\n        ptrcls = irast.TupleIndirectionLink(\n            source=src_t,\n            target=tgt_t,\n            element_name=ptrref.name.name,\n        )\n    elif isinstance(ptrref, irast.TypeIntersectionPointerRef):\n        target = schema.get_by_id(ptrref.out_target.id)\n        assert isinstance(target, s_types.Type)\n        ptrcls = irast.TypeIntersectionLink(\n            source=schema.get_by_id(ptrref.out_source.id),\n            target=target,\n            optional=ptrref.optional,\n            is_empty=ptrref.is_empty,\n            is_subtype=ptrref.is_subtype,\n            cardinality=ptrref.out_cardinality.to_schema_value()[1],\n        )\n    elif isinstance(ptrref, irast.PointerRef):\n        ptr = schema.get_by_id(ptrref.id)\n        assert isinstance(ptr, s_pointers.Pointer)\n        ptrcls = ptr\n    else:\n        raise TypeError(f'unexpected pointer ref type: {ptrref!r}')\n\n    return schema, ptrcls\n\n\ndef cardinality_from_ptrcls(\n    schema: s_schema.Schema,\n    ptrcls: s_pointers.PointerLike,\n) -> tuple[Optional[qltypes.Cardinality], Optional[qltypes.Cardinality]]:\n\n    out_card = ptrcls.get_cardinality(schema)\n    required = ptrcls.get_required(schema)\n    if out_card is None or not out_card.is_known():\n        # The cardinality is not yet known.\n        out_cardinality = None\n        in_cardinality = None\n    else:\n        assert isinstance(out_card, qltypes.SchemaCardinality)\n        out_cardinality = qltypes.Cardinality.from_schema_value(\n            required, out_card)\n        # Backward link cannot be required, but exclusivity\n        # controls upper bound on cardinality.\n        if not ptrcls.is_non_concrete(schema) and ptrcls.is_exclusive(schema):\n            in_cardinality = qltypes.Cardinality.AT_MOST_ONE\n        else:\n            in_cardinality = qltypes.Cardinality.MANY\n\n    return out_cardinality, in_cardinality\n\n\ndef is_id_ptrref(ptrref: irast.BasePointerRef) -> bool:\n    \"\"\"Return True if *ptrref* describes the id property.\"\"\"\n    return (\n        str(ptrref.std_parent_name) == 'std::id'\n    ) and not ptrref.source_ptr\n\n\ndef is_computable_ptrref(ptrref: irast.BasePointerRef) -> bool:\n    \"\"\"Return True if pointer described by *ptrref* is computed.\"\"\"\n    return ptrref.is_computable\n\n\ndef get_tuple_element_index(ptrref: irast.TupleIndirectionPointerRef) -> int:\n    name = ptrref.name.name\n    if name.isdecimal() and name.isascii():\n        return int(name)\n    else:\n        for i, st in enumerate(ptrref.out_source.subtypes):\n            if st.element_name == name:\n                return i\n\n        raise AssertionError(f\"element {name} is not found in tuple type\")\n\n\ndef type_contains(\n    parent: irast.TypeRef,\n    child: irast.TypeRef,\n) -> bool:\n    \"\"\"Check if *parent* typeref contains the given *child* typeref.\n\n    Both *parent* and *child* can be type expressions.\n    \"\"\"\n    if parent == child:\n        return True\n\n    # Calculate the minterms of both *parent* and *child*.\n    parent_minterms = _disjunctive_normal_form(parent)\n    child_minterms = _disjunctive_normal_form(child)\n\n    # The *parent* contains *child* if each child minterm is contained\n    # by a parent minterm.\n    #\n    # Examples\n    # - [A] contains [AB]\n    # - [A,B] contains [A]\n    # - [AB] does not contain [A]\n    # - [A] does not contain [A,B]\n    # - [AB,CD] does not contain [BD]\n    return all(\n        any(\n            c.issuperset(p)\n            for p in parent_minterms\n        )\n        for c in child_minterms\n    )\n\n\ndef _disjunctive_normal_form(\n    typeref: irast.TypeRef\n) -> list[set[uuid.UUID]]:\n    \"\"\"Convert any typeref into a minimal disjunctive normal form.\n\n    In the result:\n    - The outer list represents unions.\n    - The inner sets represent intersections of simple types (ie. minterms).\n\n    Duplicate and superset minterms are removed as redundant.\n    \"\"\"\n\n    def simplify(\n        expr: Iterable[set[uuid.UUID]]\n    ) -> list[set[uuid.UUID]]:\n        # Remove any minterms which imply others\n        # eg. [A, AB, BC] -> [A, BC]\n        minterms_by_length = sorted(\n            expr,\n            key=lambda i: len(i)\n        )\n\n        result: list[set[uuid.UUID]] = []\n        for minterm in minterms_by_length:\n            if not any(\n                minterm.issuperset(r)\n                for r in result\n            ):\n                result.append(minterm)\n\n        return result\n\n    if typeref.expr_union:\n        return simplify(\n            minterm\n            for t in typeref.expr_union\n            for minterm in _disjunctive_normal_form(t)\n        )\n\n    elif typeref.expr_intersection:\n        components = [\n            _disjunctive_normal_form(t)\n            for t in typeref.expr_intersection\n        ]\n\n        result = components[0]\n        for other in components[1:]:\n            result = [\n                set.union(r, o)\n                for r in result\n                for o in other\n            ]\n\n        return simplify(result)\n\n    else:\n        return [{typeref.id}]\n\n\ndef find_actual_ptrref(\n    source_typeref: irast.TypeRef,\n    parent_ptrref: irast.BasePointerRef,\n    *,\n    dir: s_pointers.PointerDirection = s_pointers.PointerDirection.Outbound,\n    material: bool=True,\n) -> irast.BasePointerRef:\n    if material and source_typeref.material_type:\n        source_typeref = source_typeref.material_type\n\n    if material and parent_ptrref.material_ptr:\n        parent_ptrref = parent_ptrref.material_ptr\n\n    ptrref = parent_ptrref\n\n    if ptrref.source_ptr is not None:\n        # Link property ref\n        link_ptr: irast.BasePointerRef = ptrref.source_ptr\n        if link_ptr.material_ptr:\n            link_ptr = link_ptr.material_ptr\n        if link_ptr.dir_source(dir).id == source_typeref.id:\n            return ptrref\n    elif ptrref.dir_source(dir).id == source_typeref.id:\n        return ptrref\n\n    # We are updating a subtype, find the\n    # correct descendant ptrref.\n    for dp in (\n        (ptrref.union_components or set())\n        | (ptrref.intersection_components or set())\n    ):\n        candidate = maybe_find_actual_ptrref(\n            source_typeref, dp, material=material, dir=dir)\n        if candidate is not None:\n            return candidate\n\n    for dp in ptrref.children:\n        if dp.dir_source(dir) and dp.dir_source(dir).id == source_typeref.id:\n            return dp\n        else:\n            candidate = maybe_find_actual_ptrref(\n                source_typeref, dp, material=material, dir=dir)\n            if candidate is not None:\n                return candidate\n\n    raise LookupError(\n        f'cannot find ptrref matching typeref {source_typeref.id}')\n\n\ndef maybe_find_actual_ptrref(\n    source_typeref: irast.TypeRef,\n    parent_ptrref: irast.BasePointerRef,\n    *,\n    material: bool=True,\n    dir: s_pointers.PointerDirection = s_pointers.PointerDirection.Outbound,\n) -> Optional[irast.BasePointerRef]:\n    try:\n        return find_actual_ptrref(\n            source_typeref, parent_ptrref, material=material, dir=dir)\n    except LookupError:\n        return None\n\n\ndef get_typeref_descendants(typeref: irast.TypeRef) -> set[irast.TypeRef]:\n    result = set()\n    if typeref.children:\n        for child in typeref.children:\n            result.add(child)\n            result.update(get_typeref_descendants(child))\n\n    return result\n\n\ndef maybe_lookup_obj_pointer(\n    schema: s_schema.Schema,\n    name: s_name.QualName,\n    ptr_name: s_name.UnqualName,\n) -> Optional[s_pointers.Pointer]:\n    base_object = schema.get(name, type=s_objtypes.ObjectType, default=None)\n    if not base_object:\n        return None\n    ptr = base_object.maybe_get_ptr(schema, ptr_name)\n    return ptr\n\n\ndef lookup_obj_ptrref(\n    schema: s_schema.Schema,\n    name: s_name.QualName,\n    ptr_name: s_name.UnqualName,\n    cache: Optional[dict[PtrRefCacheKey, irast.BasePointerRef]] = None,\n    typeref_cache: Optional[dict[TypeRefCacheKey, irast.TypeRef]] = None,\n) -> irast.PointerRef:\n    ptr = maybe_lookup_obj_pointer(schema, name, ptr_name)\n    assert ptr\n    return ptrref_from_ptrcls(\n        ptrcls=ptr, schema=schema, cache=cache, typeref_cache=typeref_cache,\n    )\n\n\ndef replace_pathid_prefix(\n    path_id: irast.PathId,\n    prefix: irast.PathId,\n    replacement: irast.PathId,\n    permissive_ptr_path: bool=False,\n) -> irast.PathId:\n    \"\"\"Return a copy of *path_id* with *prefix* replaced by\n       *replacement*.\n\n       Example:\n\n           replace_pathid_prefix(A.b.c, A.b, X.y) == PathId(X.y.c)\n    \"\"\"\n    if not path_id.startswith(prefix, permissive_ptr_path=permissive_ptr_path):\n        return path_id\n\n    # TODO: iter_prefixes is kind of expensive; can we do this in a\n    # way that peeks into the internals more?\n\n    result = replacement\n    prefixes = list(path_id.iter_prefixes(include_ptr=prefix.is_ptr_path()))\n    lastns = prefix.namespace\n\n    try:\n        start = prefixes.index(prefix)\n    except ValueError:\n        if permissive_ptr_path:\n            start = prefixes.index(prefix.ptr_path())\n        else:\n            raise\n\n    for part in prefixes[start + 1:]:\n        if part.is_ptr_path():\n            continue\n\n        ptrref = part.rptr()\n        if not ptrref:\n            continue\n        dir = part.rptr_dir()\n        assert dir\n\n        if (\n            isinstance(ptrref, irast.TupleIndirectionPointerRef)\n            and result.target.collection == 'tuple'\n        ):\n            # For tuple indirections, we want to update the target\n            # type when we get mapped to a subtype.\n            idx = get_tuple_element_index(ptrref)\n            target = result.target\n            if target.id != target.subtypes[idx].id:\n                ptrref = ptrref.replace(\n                    out_source=target,\n                    out_target=target.subtypes[idx],\n                )\n\n        if ptrref.source_ptr:\n            result = result.ptr_path()\n        result = result.extend(\n            ptrref=ptrref, direction=dir, ns=part.namespace - lastns)\n        lastns = part.namespace\n\n    if path_id.is_ptr_path():\n        result = result.ptr_path()\n\n    return result\n"
  },
  {
    "path": "edb/ir/utils.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2015-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Miscellaneous utilities for the IR.\"\"\"\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    AbstractSet,\n    Mapping,\n    Sequence,\n    Iterable,\n    cast,\n    TYPE_CHECKING,\n)\n\nif TYPE_CHECKING:\n    from typing_extensions import TypeGuard\n\nimport json\nimport uuid\n\nfrom edb import errors\n\nfrom edb.common import ast\nfrom edb.common import ordered\n\nfrom edb.edgeql import qltypes as ft\n\nfrom . import ast as irast\nfrom . import typeutils\n\n\ndef get_longest_paths(ir: irast.Base) -> set[irast.Set]:\n    \"\"\"Return a distinct set of longest paths found in an expression.\n\n    For example in SELECT (A.B.C, D.E.F, A.B, D.E) the result would\n    be {A.B.C, D.E.F}.\n    \"\"\"\n    result = set()\n    parents = set()\n\n    ir_sets = ast.find_children(\n        ir,\n        irast.Set,\n        lambda n: sub_expr(n) is None or isinstance(n.expr, irast.TypeRoot),\n    )\n    for ir_set in ir_sets:\n        result.add(ir_set)\n        if isinstance(ir_set.expr, irast.Pointer):\n            parents.add(ir_set.expr.source)\n\n    return result - parents\n\n\ndef get_parameters(ir: irast.Base) -> set[irast.QueryParameter]:\n    \"\"\"Return all parameters found in *ir*.\"\"\"\n    return set(ast.find_children(ir, irast.QueryParameter))\n\n\ndef is_const(ir: irast.Base) -> bool:\n    \"\"\"Return True if the given *ir* expression is constant.\"\"\"\n    roots = ast.find_children(ir, irast.TypeRoot)\n    variables = get_parameters(ir)\n    return not roots and not variables\n\n\ndef is_union_expr(ir: irast.Base) -> bool:\n    \"\"\"Return True if the given *ir* expression is a UNION expression.\"\"\"\n    return (\n        isinstance(ir, irast.OperatorCall) and\n        ir.operator_kind is ft.OperatorKind.Infix and\n        str(ir.func_shortname) == 'std::UNION'\n    )\n\n\ndef is_empty_array_expr(ir: Optional[irast.Base]) -> TypeGuard[irast.Array]:\n    \"\"\"Return True if the given *ir* expression is an empty array expression.\n    \"\"\"\n    return (\n        isinstance(ir, irast.Array)\n        and not ir.elements\n    )\n\n\ndef is_untyped_empty_array_expr(\n    ir: Optional[irast.Base],\n) -> TypeGuard[irast.Array]:\n    \"\"\"Return True if the given *ir* expression is an empty\n       array expression of an uknown type.\n    \"\"\"\n    return (\n        is_empty_array_expr(ir)\n        and (ir.typeref is None\n             or typeutils.is_generic(ir.typeref))\n    )\n\n\ndef is_empty(ir: irast.Base) -> bool:\n    \"\"\"Return True if the given *ir* expression is an empty set\n    or an empty array.\n    \"\"\"\n    return (\n        isinstance(ir, irast.EmptySet) or\n        (isinstance(ir, irast.Array) and not ir.elements) or\n        (\n            isinstance(ir, irast.Set)\n            and is_empty(ir.expr)\n        )\n    )\n\n\ndef is_subquery_set(ir_expr: irast.Base) -> bool:\n    \"\"\"Return True if the given *ir_expr* expression is a subquery.\"\"\"\n    return (\n        isinstance(ir_expr, irast.Set)\n        and (\n            isinstance(ir_expr.expr, irast.Stmt)\n            or (\n                isinstance(ir_expr.expr, irast.Pointer)\n                and ir_expr.expr.expr is not None\n            )\n        )\n    )\n\n\ndef is_implicit_wrapper(\n    ir_expr: Optional[irast.Base],\n) -> TypeGuard[irast.SelectStmt]:\n    \"\"\"Return True if the given *ir_expr* expression is an implicit\n       SELECT wrapper.\n    \"\"\"\n    return (\n        isinstance(ir_expr, irast.SelectStmt) and\n        ir_expr.implicit_wrapper\n    )\n\n\ndef is_trivial_select(ir_expr: irast.Base) -> TypeGuard[irast.SelectStmt]:\n    \"\"\"Return True if the given *ir_expr* expression is a trivial\n       SELECT expression, i.e `SELECT <expr>`.\n    \"\"\"\n    if not isinstance(ir_expr, irast.SelectStmt):\n        return False\n\n    return (\n        not ir_expr.orderby\n        and ir_expr.iterator_stmt is None\n        and ir_expr.where is None\n        and ir_expr.limit is None\n        and ir_expr.offset is None\n        and ir_expr.card_inference_override is None\n    )\n\n\ndef unwrap_set(ir_set: irast.Set) -> irast.Set:\n    \"\"\"If the given *ir_set* is an implicit SELECT wrapper, return the\n       wrapped set.\n    \"\"\"\n    if is_implicit_wrapper(ir_set.expr):\n        return ir_set.expr.result\n    else:\n        return ir_set\n\n\ndef get_path_root(ir_set: irast.Set) -> irast.Set:\n    result = ir_set\n    while isinstance(result.expr, irast.Pointer):\n        result = result.expr.source\n    return result\n\n\ndef get_span_as_json(\n    expr: irast.Base,\n    exctype: type[errors.EdgeDBError] = errors.InternalServerError,\n) -> str:\n    if expr.span:\n        details = json.dumps({\n            # TODO(tailhook) should we add offset, utf16column here?\n            'line': expr.span.start_point.line,\n            'column': expr.span.start_point.column,\n            'name': expr.span.filename,\n            'code': exctype.get_code(),\n        })\n\n    else:\n        details = json.dumps({\n            'code': exctype.get_code(),\n        })\n\n    return details\n\n\ndef is_type_intersection_reference(ir_expr: irast.Base) -> bool:\n    \"\"\"Return True if the given *ir_expr* is a type intersection, i.e\n       ``Foo[IS Type]``.\n    \"\"\"\n    if not isinstance(ir_expr, irast.Set):\n        return False\n    if not isinstance(ir_expr.expr, irast.Pointer):\n        return False\n    rptr = ir_expr.expr\n\n    ir_source = rptr.source\n\n    if ir_source.path_id.is_type_intersection_path():\n        source_is_type_intersection = True\n    else:\n        source_is_type_intersection = False\n\n    return source_is_type_intersection\n\n\ndef is_trivial_free_object(ir: irast.Set) -> bool:\n    ir = unwrap_set(ir)\n    return (\n        isinstance(ir.expr, irast.TypeRoot)\n        and typeutils.is_exactly_free_object(ir.typeref)\n    )\n\n\ndef collapse_type_intersection(\n    ir_set: irast.Set,\n) -> tuple[irast.Set, list[irast.TypeIntersectionPointer]]:\n\n    result: list[irast.TypeIntersectionPointer] = []\n\n    source = ir_set\n    while True:\n        rptr = source.expr\n        if not isinstance(rptr, irast.TypeIntersectionPointer):\n            break\n        result.append(rptr)\n        source = rptr.source\n\n    return source, result\n\n\nclass CollectDMLSourceVisitor(ast.NodeVisitor):\n    skip_hidden = True\n\n    def __init__(\n        self,\n        binding_dml: Mapping[irast.PathId, Sequence[irast.MutatingLikeStmt]],\n    ) -> None:\n        super().__init__()\n        self.binding_dml = binding_dml\n        self.dml: list[irast.MutatingLikeStmt] = []\n\n    def visit_MutatingLikeStmt(self, stmt: irast.MutatingLikeStmt) -> None:\n        # Only INSERTs and UPDATEs produce meaningful overlays.\n        if not isinstance(stmt, irast.DeleteStmt):\n            self.dml.append(stmt)\n\n    def visit_Set(self, node: irast.Set) -> None:\n        # Visit sub-trees\n        if node.expr:\n            self.visit(node.expr)\n        if node.is_binding:\n            self.dml.extend(self.binding_dml.get(node.path_id, ()))\n\n    def visit_Pointer(self, node: irast.Pointer) -> None:\n        if node.expr:\n            self.visit(node.expr)\n        else:\n            self.visit(node.source)\n\n\ndef get_dml_sources(\n    ir_set: irast.Set,\n    binding_dml: Mapping[irast.PathId, Sequence[irast.MutatingLikeStmt]],\n) -> Sequence[irast.MutatingLikeStmt]:\n    \"\"\"Find the DML expressions that can contribute to the value of a set\n\n    This is used to compute which overlays to use during SQL compilation.\n    \"\"\"\n    # TODO: Make this caching.\n    visitor = CollectDMLSourceVisitor(binding_dml)\n    visitor.visit(ir_set)\n    # Deduplicate, but preserve order. It shouldn't matter for\n    # *correctness* but it helps keep the nondeterminism in the output\n    # SQL down.\n    return tuple(ordered.OrderedSet(visitor.dml))\n\n\nclass ContainsDMLVisitor(ast.NodeVisitor):\n    skip_hidden = True\n\n    def __init__(self, *, skip_bindings: bool) -> None:\n        super().__init__()\n        self.skip_bindings = skip_bindings\n\n    def combine_field_results(self, xs: Iterable[Optional[bool]]) -> bool:\n        return any(\n            x is True\n            or (isinstance(x, (list, tuple)) and self.combine_field_results(x))\n            or (isinstance(x, dict) and self.combine_field_results(x.values()))\n            for x in xs\n        )\n\n    def visit_MutatingStmt(self, stmt: irast.MutatingStmt) -> bool:\n        return True\n\n    def visit_Set(self, node: irast.Set) -> bool:\n        if self.skip_bindings and node.is_binding:\n            return False\n\n        # Visit sub-trees\n        return bool(self.generic_visit(node))\n\n\ndef contains_dml(\n    stmt: irast.Base,\n    *,\n    skip_bindings: bool = False,\n    skip_nodes: Iterable[irast.Base] = (),\n) -> bool:\n    \"\"\"Check whether a statement contains any DML in a subtree.\"\"\"\n    # TODO: Make this caching.\n    visitor = ContainsDMLVisitor(skip_bindings=skip_bindings)\n    for node in skip_nodes:\n        visitor._memo[node] = False\n    res = visitor.visit(stmt) is True\n    return res\n\n\nclass FindPathScopes(ast.NodeVisitor):\n    \"\"\"Visitor to find the enclosing path scope id of sub expressions.\n\n    Sets inherit an effective scope id from enclosing expressions,\n    and this visitor computes those.\n\n    This is set up so that another visitor could inherit from it,\n    override process_set, and also collect the scope tree info.\n    \"\"\"\n\n    def __init__(self, init_scope: Optional[int] = None) -> None:\n        super().__init__()\n        self.path_scope_ids: list[Optional[int]] = [init_scope]\n        self.use_scopes: dict[irast.Set, Optional[int]] = {}\n        self.scopes: dict[irast.Set, Optional[int]] = {}\n\n    def visit_Stmt(self, stmt: irast.Stmt) -> Any:\n        # Sometimes there is sharing, so we want the official scope\n        # for a node to be based on its appearance in the result,\n        # not in a subquery.\n        # I think it might not actually matter, though.\n        self.visit(stmt.bindings)\n        if stmt.iterator_stmt:\n            self.visit(stmt.iterator_stmt)\n        if isinstance(stmt, (irast.MutatingStmt, irast.GroupStmt)):\n            self.visit(stmt.subject)\n        if isinstance(stmt, irast.GroupStmt):\n            for v in stmt.using.values():\n                self.visit(v)\n        self.visit(stmt.result)\n\n        return self.generic_visit(stmt)\n\n    def visit_Set(self, node: irast.Set) -> Any:\n        val = self.path_scope_ids[-1]\n        self.use_scopes[node] = val\n        if node.path_scope_id:\n            self.path_scope_ids.append(node.path_scope_id)\n        if not node.is_binding:\n            val = self.path_scope_ids[-1]\n\n        # Visit sub-trees\n        self.scopes[node] = val\n        res = self.process_set(node)\n\n        if node.path_scope_id:\n            self.path_scope_ids.pop()\n\n        return res\n\n    def process_set(self, node: irast.Set) -> Any:\n        self.generic_visit(node)\n        return None\n\n\ndef find_path_scopes(\n    stmt: irast.Base | Sequence[irast.Base],\n) -> dict[irast.Set, Optional[int]]:\n    visitor = FindPathScopes()\n    visitor.visit(stmt)\n    return visitor.scopes\n\n\nclass FindPotentiallyVisibleVisitor(FindPathScopes):\n    skip_hidden = True\n    extra_skips = frozenset(['materialized_sets'])\n\n    def __init__(\n        self,\n        to_skip: AbstractSet[irast.PathId],\n        scope: irast.ScopeTreeNode,\n        scope_tree_nodes: Mapping[int, irast.ScopeTreeNode],\n    ) -> None:\n        super().__init__(init_scope=scope.unique_id)\n        self.to_skip = to_skip\n        self.orig_scope = scope\n        self.scope_tree_nodes = scope_tree_nodes\n\n    def combine_field_results(self, xs: Any) -> set[irast.Set]:\n        out = set()\n        for x in xs:\n            if isinstance(x, (list, tuple)):\n                x = self.combine_field_results(x)\n            if isinstance(x, dict):\n                x = self.combine_field_results(x.values())\n            if x:\n                if isinstance(x, set):\n                    out.update(x)\n        return out\n\n    def visit_Pointer(self, node: irast.Pointer) -> set[irast.Set]:\n        res: set[irast.Set] = self.visit(node.source)\n        return res\n\n    def process_set(self, node: irast.Set) -> set[irast.Set]:\n        if node.path_id in self.to_skip:\n            # We only skip nodes in to_skip if their use site is\n            # underneath our original binding site. This prevents us\n            # from skipping references to them embedded in outside\n            # WITH bindings.\n            if (\n                (psid := self.use_scopes[node]) is not None\n                and (\n                    self.orig_scope in\n                    self.scope_tree_nodes[psid].ancestors\n                )\n            ):\n                return set()\n\n        results = [{node}]\n        if isinstance(node.expr, irast.Pointer):\n            results.append(self.visit(node.expr))\n            results.append(self.visit(node.shape))\n        else:\n            results.append(self.visit(node.shape))\n            results.append(self.visit(node.expr))\n\n        # Bound variables are always potentially visible as are object\n        # references.\n        if (\n            node.is_binding\n            or isinstance(node.expr, irast.TypeRoot)\n        ):\n            results.append({node})\n\n        # Visit sub-trees\n        return self.combine_field_results(results)\n\n\ndef find_potentially_visible(\n    stmt: irast.Base,\n    scope: irast.ScopeTreeNode,\n    scope_tree_nodes: Mapping[int, irast.ScopeTreeNode],\n    to_skip: AbstractSet[irast.PathId]=frozenset()\n) -> set[tuple[irast.PathId, irast.Set]]:\n    \"\"\"Find all \"potentially visible\" sets referenced.\"\"\"\n    # TODO: Make this caching.\n    visitor = FindPotentiallyVisibleVisitor(\n        to_skip=to_skip, scope=scope, scope_tree_nodes=scope_tree_nodes)\n    visible_sets = cast(set[irast.Set], visitor.visit(stmt))\n\n    visible_paths = set()\n    for ir in visible_sets:\n        path_id = ir.path_id\n        # Collect any namespaces between where the set is referred to\n        # and the binding point we are looking from, and strip those off.\n        # We need to do this because visibility *from the binding point*\n        # needs to not include namespaces defined below it.\n        # (See test_edgeql_scope_ref_side_02 for an example where this\n        # matters.)\n        if (set_scope_id := visitor.scopes.get(ir)) is not None:\n            set_scope = scope_tree_nodes[set_scope_id]\n            for anc, ns in set_scope.ancestors_and_namespaces:\n                if anc is scope:\n                    path_id = path_id.strip_namespace(ns)\n                    break\n\n        visible_paths.add((path_id, ir))\n\n    return visible_paths\n\n\ndef is_singleton_set_of_call(\n    call: irast.Call\n) -> bool:\n    # Some set functions and operators are allowed in singleton mode\n    # as long as their inputs are singletons\n\n    return bool(call.is_singleton_set_of)\n\n\ndef has_set_of_param(\n    call: irast.Call,\n) -> bool:\n    return any(\n        arg.param_typemod == ft.TypeModifier.SetOfType\n        for arg in call.args.values()\n    )\n\n\ndef returns_set_of(\n    call: irast.Call,\n) -> bool:\n    return call.typemod == ft.TypeModifier.SetOfType\n\n\ndef find_set_of_op(\n    ir: irast.Base,\n    has_multi_param: bool,\n) -> Optional[irast.Call]:\n    def flt(n: irast.Call) -> bool:\n        return (\n            (has_multi_param or not is_singleton_set_of_call(n))\n            and (has_set_of_param(n) or returns_set_of(n))\n        )\n    calls = ast.find_children(ir, irast.Call, flt, terminate_early=True)\n    return next(iter(calls or []), None)\n\n\ndef is_set_instance[ExprT: irast.Expr](\n    ir: irast.Set,\n    typ: type[ExprT],\n) -> TypeGuard[irast.SetE[ExprT]]:\n    return isinstance(ir.expr, typ)\n\n\ndef ref_contains_multi(ref: irast.Set, singleton_id: uuid.UUID) -> bool:\n    while isinstance(ref.expr, irast.Pointer):\n        pointer: irast.Pointer = ref.expr\n        if pointer.dir_cardinality.is_multi():\n            return True\n\n        # We don't need to look further than the object that we know is a\n        # singleton.\n        if (\n            singleton_id\n            and isinstance(pointer.ptrref, irast.PointerRef)\n            and pointer.ptrref.id == singleton_id\n        ):\n            break\n        ref = pointer.source\n    return False\n\n\ndef sub_expr(ir: irast.Set) -> Optional[irast.Expr]:\n    \"\"\"Fetch the \"sub-expression\" of a set.\n\n    For a non-pointer Set, it's just the expr, but for a Pointer\n    it is the optional computed expression.\n    \"\"\"\n    if isinstance(ir.expr, irast.Pointer):\n        return ir.expr.expr\n    else:\n        return ir.expr\n\n\nclass CollectSchemaTypesVisitor(ast.NodeVisitor):\n    types: set[uuid.UUID]\n\n    def __init__(self) -> None:\n        super().__init__()\n        self.types = set()\n\n    def visit_Set(self, node: irast.Set) -> None:\n        self.types.add(node.typeref.id)\n        self.generic_visit(node)\n\n\ndef collect_schema_types(stmt: irast.Base) -> set[uuid.UUID]:\n    \"\"\"Collect ids of all types referenced in the statement.\"\"\"\n\n    visitor = CollectSchemaTypesVisitor()\n    visitor.visit(stmt)\n    return visitor.types\n\n\ndef is_linkful(ir: irast.Base) -> bool:\n    def flt(p: irast.Pointer) -> bool:\n        return typeutils.is_object(p.typeref)\n\n    return bool(ast.find_children(ir, irast.Pointer, flt, terminate_early=True))\n"
  },
  {
    "path": "edb/language_server/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport dataclasses\nfrom typing import Optional, Any\n\n\n@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)\nclass Result[T, E]:\n    ok: Optional[T] = None\n    err: Optional[E] = None\n\n\ndef is_schema_file(path: str) -> bool:\n    return path.endswith(('.esdl', '.gel'))\n\n\ndef is_edgeql_file(path: str) -> bool:\n    return path.endswith('.edgeql')\n\n\ndef dump_to_str(node: Any) -> str:\n    import io\n    from edb.common import markup\n\n    buf = io.StringIO()\n    markup.dump(node, file=buf)\n    return buf.getvalue()\n\n\ndef dump_to_local_file(path: str, node: Any):\n    import pathlib\n    from edb.common import markup\n\n    with ('.' / pathlib.Path(path)).open('w') as file:\n        markup.dump(node, file=file)\n"
  },
  {
    "path": "edb/language_server/completion.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import Any\n\nfrom lsprotocol import types as lsp_types\nimport pygls\n\nfrom edb.common import ast\nfrom edb.common import span as edb_span\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import tokenizer as qltokenizer\nfrom edb.edgeql import compiler as qlcompiler\n\nfrom edb.schema import name as sn\nfrom edb.schema import modules as s_modules\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import types as s_types\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import schema as s_schema\nfrom edb.schema import objects as s_objects\n\nfrom . import parsing as ls_parsing\nfrom . import server as ls_server\nfrom . import schema as ls_schema\n\n\ndef get_completion(\n    ls: ls_server.GelLanguageServer, params: lsp_types.CompletionParams\n) -> lsp_types.CompletionList:\n    document = ls.workspace.get_text_document(params.text_document.uri)\n\n    target = qltokenizer.line_col_to_source_point(\n        document.source, params.position.line, params.position.character\n    )\n    ls.show_message_log(f'get_completion at position {target.offset}')\n\n    # get syntactic suggestions\n    items, can_be_ident = ls_parsing.get_completion(document, target.offset, ls)\n\n    ls.show_message_log(f'can_be_ident = {can_be_ident}')\n\n    if can_be_ident:\n        ql_ast = ls_parsing.parse_and_recover(document)\n        ls.show_message_log(f'ql_ast = {ql_ast}')\n        if isinstance(ql_ast, qlast.Commands):\n            items = (\n                _get_completion_in_ql(ls, document, ql_ast, target.offset)\n            ) + items\n        elif isinstance(ql_ast, qlast.Schema):\n            items = (\n                _get_completion_in_schema(ls, document, ql_ast, target.offset)\n            ) + items\n\n    return lsp_types.CompletionList(is_incomplete=False, items=items)\n\n\ndef _get_completion_in_ql(\n    ls: ls_server.GelLanguageServer,\n    document: pygls.workspace.TextDocument,\n    ql_stmts: qlast.Commands,\n    target: int,\n) -> list[lsp_types.CompletionItem]:\n    # replace the expr under the cursor with qlast.Cursor\n    if not ql_stmts.commands:\n        return []\n    for ql_stmt in ql_stmts.commands:\n        replaced = replace_by_source_position(ql_stmt, qlast.Cursor(), target)\n        if replaced:\n            break\n    if not replaced:\n        ls.show_message_log(f'Cannot inject qlast.Cursor')\n        return []\n\n    # compile the stmt that now contains the qlast.Cursor,\n    # which should halt compilation, when it gets to the cursor\n    try:\n        diagnostics, _ir_stmts = ls_server.compile_ql(ls, document, [ql_stmt])\n    except qlcompiler.expr.IdentCompletionException as e:\n        return [\n            lsp_types.CompletionItem(\n                label=s, kind=lsp_types.CompletionItemKind.Variable\n            )\n            for s in e.suggestions\n        ]\n\n    for diags in diagnostics.by_doc.values():\n        for d in diags:\n            ls.show_message_log(f'Cannot provide completion: {d.message}')\n            return []\n\n    raise AssertionError('qlast.Cursor did not raise IdentCompletionException')\n\n\ndef _get_completion_in_schema(\n    ls: ls_server.GelLanguageServer,\n    document: pygls.workspace.TextDocument,\n    ql_schema: qlast.Schema,\n    target: int,\n) -> list[lsp_types.CompletionItem]:\n    node_path = edb_span.find_by_source_position(ql_schema, target)\n    ls.show_message_log(f\"node_path = {node_path}\")\n    if not node_path:\n        return []\n\n    schema = ls.state.schema\n    if not schema:\n        return []\n\n    items: list[lsp_types.CompletionItem] = []\n\n    # when in a module, suggest objects from that module\n    module = ls_schema.get_module_context(node_path[1:])\n    ls.show_message_log(f\"module = {module}\")\n    if module:\n        objects: s_schema.SchemaIterator[s_objects.Object] = schema.get_objects(\n            included_modules=(sn.UnqualName(module),),\n        )\n        for obj in objects:\n            if isinstance(obj, s_types.Type) and obj.get_from_alias(schema):\n                continue\n\n            kind: lsp_types.CompletionItemKind\n            if isinstance(obj, s_objtypes.ObjectType):\n                kind = lsp_types.CompletionItemKind.Struct\n            elif isinstance(obj, s_scalars.ScalarType):\n                kind = lsp_types.CompletionItemKind.Value\n            else:\n                continue\n\n            label = obj.get_name(schema).name\n\n            items.append(\n                lsp_types.CompletionItem(\n                    label=label,\n                    kind=kind,\n                    # detail=str(obj),\n                )\n            )\n\n    # always suggest modules\n    objects = schema.get_objects(type=s_modules.Module)\n    for obj in objects:\n        name = obj.get_displayname(schema)\n        items.append(\n            lsp_types.CompletionItem(\n                label=name,\n                insert_text=name + '::',\n                kind=lsp_types.CompletionItemKind.Module,\n            )\n        )\n\n    return items\n\n\n# Replaces an expr node in AST that has a certain position within the source.\n# It matches the first Expr whose span contains the target offset in a\n# post-order traversal of the AST.\ndef replace_by_source_position(\n    tree: qlast.Base, replacement: qlast.Expr, target_offset: int\n) -> bool:\n    replacer = SpanReplacer(target_offset, replacement)\n    replacer.visit(tree)\n    return replacer.found\n\n\nclass SpanReplacer(ast.NodeTransformer):\n    target_offset: int\n    replacement: qlast.Expr\n    found: bool\n\n    def __init__(self, target_offset: int, replacement: qlast.Expr):\n        super().__init__()\n        self.target_offset = target_offset\n        self.replacement = replacement\n        self.found = False\n\n    def generic_visit(self, node, *, combine_results=None) -> Any:\n        if self.found:\n            return node\n\n        has_span = False\n        if node_span := getattr(node, 'span', None):\n            has_span = True\n            if not edb_span.span_contains(node_span, self.target_offset):\n                return node\n\n        r = super().generic_visit(node)\n\n        if not self.found and has_span and isinstance(node, qlast.Expr):\n            self.found = True\n            return self.replacement\n\n        return r\n"
  },
  {
    "path": "edb/language_server/definition.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import Optional\n\nfrom lsprotocol import types as lsp_types\nimport pygls\nimport pygls.workspace\n\nfrom edb.common import span as edb_span\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import tokenizer as qltokenizer\nfrom edb.ir import ast as irast\n\nfrom edb.schema import objects as s_objects\nfrom edb.schema import name as s_name\nfrom edb.schema import schema as s_schema\nfrom edb.schema import types as s_types\n\nfrom . import parsing as ls_parsing\nfrom . import utils as ls_utils\nfrom . import server as ls_server\nfrom . import schema as ls_schema\nfrom . import is_schema_file, is_edgeql_file\n\n\ndef get_definition(\n    ls: ls_server.GelLanguageServer, params: lsp_types.DefinitionParams\n) -> lsp_types.Location | list[lsp_types.Location]:\n    doc_uri = params.text_document.uri\n    document = ls.workspace.get_text_document(doc_uri)\n\n    position: int = qltokenizer.line_col_to_source_point(\n        document.source, params.position.line, params.position.character\n    ).offset\n    ls.show_message_log(f'get_definition at position = {position}')\n\n    try:\n        if is_schema_file(doc_uri):\n            return _get_definition_in_schema(ls, document, position) or []\n\n        elif is_edgeql_file(doc_uri):\n            ql_ast_res = ls_parsing.parse(document)\n            if not ql_ast_res.ok:\n                return []\n            ql_ast = ql_ast_res.ok\n\n            if isinstance(ql_ast, qlast.Commands):\n                return (\n                    _get_definition_in_ql(ls, document, ql_ast, position) or []\n                )\n            else:\n                # SDL in query files?\n                pass\n        else:\n            ls.show_message_log(f'Unknown file type: {doc_uri}')\n\n    except BaseException as e:\n        ls_server.send_internal_error(ls, e)\n\n    return []\n\n\ndef _get_definition_in_ql(\n    ls: ls_server.GelLanguageServer,\n    document: pygls.workspace.TextDocument,\n    ql_ast: qlast.Commands,\n    position: int,\n) -> lsp_types.Location | None:\n    # compile the whole doc\n    # TODO: search ql ast before compiling all stmts\n    _, ir_stmts = ls_server.compile_ql(ls, document, ql_ast.commands)\n\n    # find the ir node at the position\n    node_path = None\n    for ir_stmt in ir_stmts:\n        node_path = edb_span.find_by_source_position(ir_stmt, position)\n        if node_path:\n            break\n\n    if not node_path:\n        ls.show_message_log(f\"cannot find span in {len(ir_stmts)} stmts\")\n        return None\n    node = node_path[0]\n    assert isinstance(node, irast.Base), node\n    ls.show_message_log(f\"node: {node}\")\n\n    schema = ir_stmt.schema\n    assert schema\n\n    # lookup schema objects depending on which ir node we are over\n    target = _determine_ir_target(node, schema)\n    if not target:\n        ls.show_message_log(f\"don't know how to lookup schema by {node}\")\n        return None\n\n    return _schema_obj_to_doc_location(ls, target, schema, document)\n\n\ndef _determine_ir_target(\n    node: irast.Base, schema: s_schema.Schema\n) -> Optional[s_objects.Object]:\n    # special handling: references to WITH bindings\n    if (\n        isinstance(node, irast.SetE)\n        and node.is_binding == irast.BindingKind.With\n    ):\n        target = schema.get_by_id(\n            node.typeref.id, type=s_objects.InheritingObject\n        )\n        assert target\n        while (\n            target.get_span(schema) is None\n            and isinstance(target, s_types.Type)\n            and target.get_from_alias(schema)\n        ):\n            target = target.get_bases(schema).objects(schema)[0]\n        return target\n\n    # unwrap a set\n    if isinstance(node, irast.SetE):\n        node = node.expr\n\n    # unwrap select stmts\n    while isinstance(node, irast.SelectStmt):\n        node = node.result.expr\n\n    # references to object types\n    if isinstance(node, irast.TypeRoot):\n        return schema.get_by_id(node.typeref.id)\n\n    # references to pointers\n    if isinstance(node, irast.Pointer) and isinstance(\n        node.ptrref, irast.PointerRef\n    ):\n        return schema.get_by_id(node.ptrref.id)\n\n    return None\n\n\n# Finds definition of names in schema files.\n#\n# Parses the file and finds the ObjectRef at the given position. Then, it\n# computes \"module context\", by looking at names of encapsulating modules so\n# it can convert ObjectRef into a qualified name. Then it just looks up that\n# name in the schema.\n#\n# This impl might be lacking, since it does not use the code we use for name\n# resolution in the main compiler (tracing.py), and might report some\n# definitions incorrectly (i.e. within expressions).\ndef _get_definition_in_schema(\n    ls: ls_server.GelLanguageServer,\n    document: pygls.workspace.TextDocument,\n    position: int,\n) -> lsp_types.Location | None:\n    res = ls_schema._ensure_schema_docs_loaded(ls)\n    if res.err:\n        return None\n\n    # parse current doc, return on errors\n    _ = ls_schema._parse_schema(ls)\n    assert ls.state.schema_sdl\n\n    # find the span in ql ast\n    node_path = edb_span.find_by_source_position(ls.state.schema_sdl, position)\n    if not node_path:\n        return None\n\n    # ls.show_message_log(f\"found node: {dump_to_str(node_path[0])}\")\n\n    # only resolve ObjectRefs\n    if not isinstance(node_path[0], qlast.ObjectRef):\n        return None\n\n    # convert qlast.ObjectRef into a sn.QualName\n    name: str = node_path[0].name\n    module: Optional[str] = node_path[0].module\n    if not module:\n        module = ls_schema.get_module_context(node_path[1:])\n    if not module:\n        return None\n    q_name = s_name.QualName(module, name)\n    ls.show_message_log(f\"name: {q_name}\")\n\n    # lookup the name in latest compiled schema\n    schema = ls.state.schema\n    if not schema:\n        return None\n    obj = schema.get(q_name, default=None)\n    if not obj:\n        ls.show_message_log(f\"object with this name not found\")\n        return None\n\n    return _schema_obj_to_doc_location(ls, obj, schema, document)\n\n\ndef _schema_obj_to_doc_location(\n    ls: ls_server.GelLanguageServer,\n    obj: s_objects.Object,\n    schema: s_schema.Schema,\n    curr_doc: pygls.workspace.TextDocument,\n) -> lsp_types.Location | None:\n    name = obj.get_name(schema)\n    ls.show_message_log(f\"find schema object: {name}\")\n\n    span: edb_span.Span | None = obj.get_span(schema)\n    if not span:\n        ls.show_message_log(f\"no span for schema object\")\n        return None\n\n    # find originating document\n    doc: Optional[pygls.workspace.TextDocument] = None\n\n    # is doc the current document?\n    if span.filename == curr_doc.filename:\n        doc = curr_doc\n\n    # find schema docs with this filename\n    if not doc:\n        docs = ls.state.schema_docs\n        doc = next((d for d in docs if d.filename == span.filename), None)\n\n    if not doc:\n        ls.show_message_log(f\"Cannot find doc: {span.filename}\")\n        return None\n\n    return lsp_types.Location(\n        uri=doc.uri,\n        range=ls_utils.span_to_lsp(doc.source, (span.start, span.end)),\n    )\n"
  },
  {
    "path": "edb/language_server/main.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport sys\nimport json\n\n\nfrom lsprotocol import types as lsp_types\nimport click\n\nfrom edb import buildmeta\nfrom edb.edgeql import parser as qlparser\n\nfrom . import server as ls_server\nfrom . import definition as ls_definition\nfrom . import completion as ls_completion\n\n\n@click.command()\n@click.option('--version', is_flag=True, help=\"Show the version and exit.\")\n@click.option(\n    '--stdio',\n    is_flag=True,\n    help=\"Use stdio for LSP. This is currently the only transport.\",\n)\n@click.argument(\"options\", type=str, default='{}')\ndef main(options: str | None, *, version: bool, stdio: bool):\n    if version:\n        print(f\"gel-ls, version {buildmeta.get_version()}\")\n        sys.exit(0)\n\n    ls = init(options)\n\n    if stdio:\n        ls.start_io()\n    else:\n        print(\"Error: no LSP transport enabled. Use --stdio.\")\n\n\ndef init(options_json: str | None) -> ls_server.GelLanguageServer:\n    # load config\n    options_dict = json.loads(options_json or '{}')\n    project_dir = '.'\n    if 'project_dir' in options_dict:\n        project_dir = options_dict['project_dir']\n    config = ls_server.Config(project_dir=project_dir)\n\n    # construct server\n    ls = ls_server.GelLanguageServer(config)\n    debug_init(ls)\n\n    # register hooks\n    @ls.feature(\n        lsp_types.INITIALIZE,\n    )\n    def init(_params: lsp_types.InitializeParams):\n        qlparser.preload_spec()\n        ls.show_message_log('gel-ls ready for requests')\n\n    @ls.feature(lsp_types.TEXT_DOCUMENT_DID_OPEN)\n    def text_document_did_open(params: lsp_types.DidOpenTextDocumentParams):\n        ls_server.document_updated(ls, params.text_document.uri, compile=True)\n\n    @ls.feature(lsp_types.TEXT_DOCUMENT_DID_CHANGE)\n    def text_document_did_change(params: lsp_types.DidChangeTextDocumentParams):\n        ls_server.document_updated(ls, params.text_document.uri, compile=False)\n\n    @ls.feature(lsp_types.TEXT_DOCUMENT_DID_SAVE)\n    def text_document_did_save(params: lsp_types.DidChangeTextDocumentParams):\n        ls_server.document_updated(ls, params.text_document.uri, compile=True)\n\n    @ls.feature(lsp_types.TEXT_DOCUMENT_DEFINITION)\n    def text_document_definition(\n        params: lsp_types.DefinitionParams,\n    ) -> lsp_types.Definition:\n        return ls_definition.get_definition(ls, params)\n\n    @ls.feature(\n        lsp_types.TEXT_DOCUMENT_COMPLETION,\n        lsp_types.CompletionOptions(trigger_characters=[',']),\n    )\n    def completion(params: lsp_types.CompletionParams):\n        return ls_completion.get_completion(ls, params)\n\n    return ls\n\n\n# Last gel-ls instance initialed. Use ONLY for debugging purposes.\n__gel_ls: ls_server.GelLanguageServer | None = None\n\n\ndef debug_init(ls: ls_server.GelLanguageServer):\n    global __gel_ls\n    __gel_ls = ls\n\n\ndef send_log_message(message: str):\n    global __gel_ls\n    assert __gel_ls, 'GelLanguageServer has not be started yet'\n    __gel_ls.show_message_log(message)\n"
  },
  {
    "path": "edb/language_server/parsing.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import Optional\n\nfrom pygls.server import LanguageServer\nfrom pygls.workspace import TextDocument\nfrom lsprotocol import types as lsp_types\n\nfrom edb import errors\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import tokenizer\nfrom edb.edgeql import parser as qlparser\nfrom edb.edgeql.parser.grammar import tokens as qltokens\nimport edb._edgeql_parser as rust_parser\n\nfrom . import Result, is_schema_file\nfrom . import utils as ls_utils\n\n\ndef parse(\n    doc: TextDocument,\n) -> Result[qlast.Commands | qlast.Schema, list[lsp_types.Diagnostic]]:\n    sdl = is_schema_file(doc.filename) if doc.filename else False\n\n    start_t = qltokens.T_STARTSDLDOCUMENT if sdl else qltokens.T_STARTBLOCK\n    start_t_name = start_t.__name__[2:]\n\n    source_res = _tokenize(doc.source)\n    if diagnostics := source_res.err:\n        return Result(err=diagnostics)\n    source = source_res.ok\n    assert source\n\n    result, productions = rust_parser.parse(start_t_name, source.tokens())\n\n    if result.errors:\n        diagnostics = []\n        for error in result.errors:\n            message, span, hint, details = error\n\n            if details:\n                message += f\"\\n{details}\"\n            if hint:\n                message += f\"\\nHint: {hint}\"\n\n            diagnostics.append(\n                lsp_types.Diagnostic(\n                    range=ls_utils.span_to_lsp(source.text(), span),\n                    severity=lsp_types.DiagnosticSeverity.Error,\n                    message=message,\n                )\n            )\n\n        return Result(err=diagnostics)\n\n    # parsing successful\n    assert isinstance(result.out, rust_parser.CSTNode)\n\n    try:\n        ast = qlparser._cst_to_ast(\n            result.out, productions, source, doc.filename\n        ).val\n    except errors.EdgeDBError as e:\n        return Result(err=[ls_utils.error_to_lsp(e)])\n    if sdl:\n        assert isinstance(ast, qlast.Schema), ast\n    else:\n        assert isinstance(ast, qlast.Commands), ast\n    return Result(ok=ast)\n\n\ndef parse_and_recover(\n    doc: TextDocument,\n) -> Optional[qlast.Commands | qlast.Schema]:\n    sdl = is_schema_file(doc.filename) if doc.filename else False\n\n    start_t = qltokens.T_STARTSDLDOCUMENT if sdl else qltokens.T_STARTBLOCK\n    start_t_name = start_t.__name__[2:]\n\n    source_res = _tokenize(doc.source)\n    if not source_res.ok:\n        return None\n    source = source_res.ok\n\n    result, productions = rust_parser.parse(start_t_name, source.tokens())\n\n    if not isinstance(result.out, rust_parser.CSTNode):\n        return None\n\n    try:\n        ast = qlparser._cst_to_ast(\n            result.out, productions, source, doc.filename\n        ).val\n    except errors.EdgeDBError:\n        return None\n\n    if sdl:\n        assert isinstance(ast, qlast.Schema), ast\n    else:\n        assert isinstance(ast, qlast.Commands), ast\n\n    return ast\n\n\ndef get_completion(\n    doc: TextDocument, target: int, ls: LanguageServer\n) -> tuple[list[lsp_types.CompletionItem], bool]:\n    sdl = is_schema_file(doc.path)\n\n    start_t = qltokens.T_STARTSDLDOCUMENT if sdl else qltokens.T_STARTBLOCK\n    start_t_name = start_t.__name__[2:]\n\n    # tokenize\n    source_res = _tokenize(doc.source)\n    if not source_res.ok:\n        return [], False\n    source: tokenizer.Source = source_res.ok\n\n    # limit tokens to things preceding cursor position\n    cut_index = len(source.tokens())\n    for index, tok in enumerate(source.tokens()):\n        if not tok.span_end() <= target:\n            cut_index = index\n            break\n    tokens = source.tokens()[0:cut_index]\n\n    # special case: cursor is *on* the last ident\n    if tokens[-1].is_ident() and tokens[-1].span_end() == target:\n        return [], True\n\n    # run parser and suggest next possible keywords\n    suggestions, can_be_ident = rust_parser.suggest_next_keywords(\n        start_t_name, tokens\n    )\n\n    # convert to CompletionItem\n    return [\n        lsp_types.CompletionItem(\n            label=keyword,\n            kind=lsp_types.CompletionItemKind.Keyword,\n        )\n        for keyword in suggestions\n    ], can_be_ident\n\n\ndef _tokenize(\n    source: str,\n) -> Result[tokenizer.Source, list[lsp_types.Diagnostic]]:\n    try:\n        return Result(ok=tokenizer.Source.from_string(source))\n    except errors.EdgeQLSyntaxError as e:\n        return Result(\n            err=[\n                lsp_types.Diagnostic(\n                    range=ls_utils.span_to_lsp(source, e.get_span()),\n                    severity=lsp_types.DiagnosticSeverity.Error,\n                    message=e.args[0],\n                )\n            ]\n        )\n"
  },
  {
    "path": "edb/language_server/project.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import cast, Any\nfrom pathlib import Path\nimport dataclasses\nimport tomllib\n\n\n@dataclasses.dataclass(kw_only=True, frozen=True)\nclass Instance:\n    server_version: str\n\n\n@dataclasses.dataclass(kw_only=True, frozen=True)\nclass Project:\n    schema_dir: Path\n\n\n@dataclasses.dataclass(kw_only=True, frozen=True)\nclass Manifest:\n    instance: Instance | None\n    project: Project | None\n    # hooks: Option<Hooks>,\n    # watch: Vec<WatchScript>,\n\n\ndef read_manifest(project_dir: Path) -> tuple[Manifest, Path]:\n    try:\n        path = project_dir / 'gel.toml'\n        with open(path, 'rb') as f:\n            manifest_dict = tomllib.load(f)\n    except FileNotFoundError as e:\n        path = project_dir / 'edgedb.toml'\n        try:\n            with open(path, 'rb') as f:\n                manifest_dict = tomllib.load(f)\n        except FileNotFoundError:\n            raise e\n\n    return (_load_manifest(manifest_dict), path)\n\n\ndef _load_manifest(manifest_dict: Any) -> Manifest:\n    instance = None\n    if 'instance' in manifest_dict:\n        instance = _load_instance(manifest_dict['instance'])\n    elif 'edgedb' in manifest_dict:\n        instance = _load_instance(manifest_dict['edgedb'])\n\n    project = None\n    if 'project' in manifest_dict:\n        project = _load_project(manifest_dict['project'])\n\n    return Manifest(\n        instance=instance,\n        project=project,\n    )\n\n\ndef _load_instance(instance_dict: Any) -> Instance | None:\n    server_version = None\n    if 'server-version' in instance_dict:\n        server_version = cast(str, instance_dict['server-version'])\n    else:\n        return None\n\n    return Instance(server_version=server_version)\n\n\ndef _load_project(project_dict: Any) -> Project | None:\n    schema_dir = None\n    if 'schema-dir' in project_dict:\n        schema_dir = Path(project_dict['schema-dir'])\n    else:\n        return None\n\n    return Project(schema_dir=schema_dir)\n"
  },
  {
    "path": "edb/language_server/schema.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import Optional, cast, Iterable\nimport pathlib\nimport os\n\nfrom pygls import uris as pygls_uris\nimport pygls\nfrom lsprotocol import types as lsp_types\n\n\nfrom edb import errors\n\nfrom edb.edgeql import ast as qlast\n\nfrom edb.schema import schema as s_schema\nfrom edb.schema import std as s_std\nfrom edb.schema import ddl as s_ddl\nimport pygls.workspace\n\nfrom . import parsing as ls_parsing\nfrom . import is_schema_file\nfrom . import project\nfrom . import utils as ls_utils\nfrom . import server as ls_server\nfrom edb.language_server import Result\n\n\ndef get_schema(\n    ls: ls_server.GelLanguageServer,\n) -> tuple[s_schema.Schema | None, ls_utils.DiagnosticsSet, str | None]:\n    if ls.state.schema:\n        return (ls.state.schema, ls_utils.DiagnosticsSet(), None)\n\n    err_msg: str | None = None\n    if len(ls.state.schema_docs) == 0:\n        schema_dir, err_msg = _determine_schema_dir(ls)\n        if not schema_dir:\n            return (None, ls_utils.DiagnosticsSet(), err_msg)\n        err_msg = _load_schema_docs(ls, schema_dir)\n\n    if len(ls.state.schema_docs) == 0:\n        return (None, ls_utils.DiagnosticsSet(), err_msg)\n\n    schema, diagnostics = _compile_schema(ls)\n    return schema, diagnostics, None\n\n\ndef store_schema_doc(\n    ls: ls_server.GelLanguageServer, doc: pygls.workspace.TextDocument\n) -> list[lsp_types.Diagnostic]:\n    res = _ensure_schema_docs_loaded(ls)\n    if e := res.err:\n        return [ls_utils.new_diagnostic_at_the_top(e)]\n    schema_dir = cast(pathlib.Path, res.ok)\n\n    # dont update if doc is not in schema_dir\n    if schema_dir not in pathlib.Path(doc.path).parents:\n        return [\n            ls_utils.new_diagnostic_at_the_top(\n                f\"this schema file is not in schema-dir ({schema_dir})\"\n            )\n        ]\n\n    existing = next(\n        (i for i, d in enumerate(ls.state.schema_docs) if d.path == doc.path),\n        None,\n    )\n    if existing is not None:\n        # update\n        ls.state.schema_docs[existing] = doc\n    else:\n        # insert\n        ls.show_message_log(\"new schema file added: \" + doc.path)\n        ls.show_message_log(\"existing files: \")\n        for d in ls.state.schema_docs:\n            ls.show_message_log(\"- \" + d.path)\n\n        ls.state.schema_docs.append(doc)\n\n    # clear AST cache\n    ls.state.schema_sdl = None\n\n    return []\n\n\ndef _ensure_schema_docs_loaded(\n    ls: ls_server.GelLanguageServer,\n) -> Result[pathlib.Path, str]:\n    schema_dir, err_msg = _determine_schema_dir(ls)\n    if not schema_dir:\n        return Result(err=err_msg or \"cannot find schema-dir\")\n\n    if len(ls.state.schema_docs) == 0:\n        if err_mgs := _load_schema_docs(ls, schema_dir):\n            return Result(err=err_mgs)\n    return Result(ok=schema_dir)\n\n\ndef _get_workspace_path(\n    ls: ls_server.GelLanguageServer,\n) -> tuple[pathlib.Path | None, str | None]:\n    if len(ls.workspace.folders) > 1:\n        return None, \"Workspaces with multiple root folders are not supported\"\n    if len(ls.workspace.folders) == 0:\n        return None, \"No workspace open, cannot load schema\"\n\n    workspace: lsp_types.WorkspaceFolder = next(\n        iter(ls.workspace.folders.values())\n    )\n    return pathlib.Path(pygls_uris.to_fs_path(workspace.uri)), None\n\n\n# Looks as the file system and loads schema documents into ls.state\n# Returns error message.\ndef _load_schema_docs(\n    ls: ls_server.GelLanguageServer, schema_dir: pathlib.Path\n) -> Optional[str]:\n    # discard all existing docs\n    ls.state.schema_docs.clear()\n\n    try:\n        entries = os.listdir(schema_dir)\n    except FileNotFoundError:\n        return f\"Cannot list directory: {schema_dir}\"\n\n    # read .esdl files\n    for entry in entries:\n        if not is_schema_file(entry):\n            continue\n        doc = ls.workspace.get_text_document(f\"file://{schema_dir / entry}\")\n        ls.state.schema_docs.append(doc)\n\n    # clear AST cache\n    ls.state.schema_sdl = None\n\n    return None\n\n\ndef _determine_schema_dir(\n    ls: ls_server.GelLanguageServer,\n) -> tuple[pathlib.Path | None, str | None]:\n    workspace_path, err_msg = _get_workspace_path(ls)\n    if not workspace_path:\n        return None, err_msg or \"Cannot determine schema dir\"\n\n    project_dir = workspace_path / pathlib.Path(ls.config.project_dir)\n\n    manifest, err_msg = _load_manifest(ls, project_dir)\n    if not manifest:\n        # no manifest: don't infer any schema dir\n        return None, err_msg\n\n    if manifest.project and manifest.project.schema_dir:\n        schema_dir = project_dir / manifest.project.schema_dir\n    else:\n        schema_dir = project_dir / \"dbschema\"\n\n    if schema_dir.is_dir():\n        return schema_dir, None\n    return None, f\"Missing schema dir at {schema_dir}\"\n\n\ndef _load_manifest(\n    ls: ls_server.GelLanguageServer,\n    project_dir: pathlib.Path,\n) -> tuple[project.Manifest | None, str | None]:\n    if ls.state.manifest:\n        return ls.state.manifest[0], None\n\n    try:\n        ls.state.manifest = project.read_manifest(project_dir)\n        return ls.state.manifest[0], None\n    except BaseException as e:\n        return None, str(e)\n\n\ndef _parse_schema(\n    ls: ls_server.GelLanguageServer,\n) -> ls_utils.DiagnosticsSet:\n    diagnostics = ls_utils.DiagnosticsSet()\n\n    if ls.state.schema_sdl:\n        return ls_utils.DiagnosticsSet()\n\n    sdl = qlast.Schema(declarations=[])\n    for doc in ls.state.schema_docs:\n        res = ls_parsing.parse(doc)\n        if d := res.err:\n            diagnostics.by_doc[doc] = d\n        else:\n            diagnostics.by_doc[doc] = []\n            if isinstance(res.ok, qlast.Schema):\n                sdl.declarations.extend(res.ok.declarations)\n            else:\n                # TODO: complain that .gel contains non-SDL syntax\n                pass\n    ls.state.schema_sdl = sdl\n    return diagnostics\n\n\ndef _compile_schema(\n    ls: ls_server.GelLanguageServer,\n) -> tuple[s_schema.Schema | None, ls_utils.DiagnosticsSet]:\n    diagnostics = _parse_schema(ls)\n    assert ls.state.schema_sdl\n\n    std_schema = _load_std_schema(ls.state)\n\n    # apply SDL to std schema\n    ls.show_message_log(\"compiling schema ..\")\n    try:\n        schema, _warnings = s_ddl.apply_sdl(\n            ls.state.schema_sdl, base_schema=std_schema\n        )\n        ls.state.schema = schema\n        ls.show_message_log(\".. done\")\n    except errors.EdgeDBError as error:\n        ls.show_message_log(\".. error\")\n        schema = None\n\n        # find doc\n        do = next(\n            (d for d in ls.state.schema_docs if error.filename == d.filename),\n            None,\n        )\n        if do is None:\n            ls.show_message_log(\n                f\"cannot find original doc of the error ({error.filename}), \"\n                \"using first schema file\"\n            )\n            do = ls.state.schema_docs[0]\n\n        # convert error\n        diagnostics.append(do, ls_utils.error_to_lsp(error))\n\n    ls.state.schema_diagnostics = diagnostics\n    return (schema, diagnostics)\n\n\ndef _load_std_schema(state: ls_server.State) -> s_schema.Schema:\n    if state.std_schema is not None:\n        return state.std_schema\n\n    schema: s_schema.Schema = s_schema.EMPTY_SCHEMA\n    for modname in s_schema.STD_SOURCES:\n        schema = s_std.load_std_module(schema, modname)\n\n    state.std_schema = schema\n    return state.std_schema\n\n\n# Given a path from a node to qlast.Schema root, collects the names of\n# encapsulating modules.\ndef get_module_context(path: Iterable[qlast.Base]) -> str | None:\n    mod_names = []\n    for node in path:\n        if isinstance(node, qlast.ModuleDeclaration):\n            mod_names.append(node.name.name)\n    if not mod_names:\n        return None\n    mod_names.reverse()\n    return '::'.join(mod_names)\n"
  },
  {
    "path": "edb/language_server/server.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import Mapping\nimport dataclasses\nimport pathlib\nfrom pygls.server import LanguageServer\nimport pygls\nfrom lsprotocol import types as lsp_types\n\nfrom edb import errors\nfrom edb.common import traceback as edb_traceback\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import compiler as qlcompiler\n\nfrom edb.ir import ast as irast\n\nfrom edb.schema import schema as s_schema\nfrom edb.schema import ddl as s_ddl\n\nfrom . import project as ls_project\nfrom . import utils as ls_utils\nfrom . import parsing as ls_parsing\nfrom . import is_edgeql_file, is_schema_file\n\n\n@dataclasses.dataclass(kw_only=True)\nclass State:\n    manifest: tuple[ls_project.Manifest, pathlib.Path] | None = None\n\n    schema_docs: list[pygls.workspace.TextDocument] = dataclasses.field(\n        default_factory=lambda: []\n    )\n\n    schema_sdl: qlast.Schema | None = None\n\n    schema: s_schema.Schema | None = None\n\n    schema_diagnostics: ls_utils.DiagnosticsSet | None = None\n\n    std_schema: s_schema.Schema | None = None\n\n\n@dataclasses.dataclass(kw_only=True)\nclass Config:\n    project_dir: str\n\n\nclass GelLanguageServer(LanguageServer):\n    state: State\n    config: Config\n\n    def __init__(self, config: Config):\n        super().__init__(\"Gel Language Server\", \"v0.1\")\n        self.state = State()\n        self.config = config\n\n\ndef send_internal_error(ls: GelLanguageServer, e: BaseException):\n    text = edb_traceback.format_exception(e)\n    ls.show_message_log(f'Internal error: {text}')\n\n\ndef document_updated(ls: GelLanguageServer, doc_uri: str, *, compile: bool):\n    # each call to this function should yield in exactly one publish_diagnostics\n    # for this document\n\n    from . import schema as ls_schema\n\n    document = ls.workspace.get_text_document(doc_uri)\n\n    diagnostic_set = ls_utils.DiagnosticsSet()\n    diagnostic_set.extend(document, [])  # make sure we publish for document\n\n    try:\n        if is_schema_file(doc_uri):\n            # schema file\n\n            # parse\n            diags = ls_schema.store_schema_doc(ls, document)\n            diagnostic_set.extend(document, diags)\n            diagnostic_set.merge(ls_schema._parse_schema(ls))\n\n            # compile\n            if compile and not diagnostic_set.has_any():\n                _, _ = ls_schema._compile_schema(ls)\n\n            # add schema diagnostics from last compilation\n            if ls.state.schema_diagnostics:\n                diagnostic_set.merge(ls.state.schema_diagnostics)\n\n        elif is_edgeql_file(doc_uri):\n            # query file\n\n            # parse\n            ast_res = ls_parsing.parse(document)\n            if ast_res.err:\n                diagnostic_set.extend(document, ast_res.err)\n\n            # compile\n            if compile and isinstance(ast_res.ok, qlast.Commands):\n                diag, _ = compile_ql(ls, document, ast_res.ok.commands)\n                diagnostic_set.merge(diag)\n        else:\n            ls.show_message_log(f'Unknown file type: {doc_uri}')\n\n        for doc, diags in diagnostic_set.by_doc.items():\n            ls.publish_diagnostics(doc.uri, diags, doc.version)\n    except BaseException as e:\n        send_internal_error(ls, e)\n        ls.publish_diagnostics(document.uri, [], document.version)\n\n\ndef compile_ql(\n    ls: GelLanguageServer,\n    doc: pygls.workspace.TextDocument,\n    stmts: list[qlast.Command],\n) -> tuple[ls_utils.DiagnosticsSet, list[irast.Statement]]:\n    from . import schema as ls_schema\n\n    if not stmts:\n        return (ls_utils.DiagnosticsSet(by_doc={doc: []}), [])\n\n    schema, diagnostics_set, err_msg = ls_schema.get_schema(ls)\n    if not schema:\n        if len(ls.state.schema_docs) == 0:\n            diagnostics_set.append(\n                doc,\n                ls_utils.new_diagnostic_at_the_top(\n                    err_msg or \"Cannot find schema files\"\n                ),\n            )\n        return (diagnostics_set, [])\n\n    diagnostics: list[lsp_types.Diagnostic] = []\n    ir_stmts: list[irast.Statement] = []\n    modaliases: Mapping[str | None, str] = {None: \"default\"}\n    for ql_stmt in stmts:\n        try:\n            if isinstance(ql_stmt, qlast.DDLCommand):\n                schema, _delta = s_ddl.delta_and_schema_from_ddl(\n                    ql_stmt, schema=schema, modaliases=modaliases\n                )\n\n            elif isinstance(ql_stmt, (qlast.Command, qlast.Expr)):\n                options = qlcompiler.CompilerOptions(modaliases=modaliases)\n                ir_res = qlcompiler.compile_ast_to_ir(\n                    ql_stmt, schema, options=options\n                )\n                if isinstance(ir_res, irast.Statement):\n                    ir_stmts.append(ir_res)\n            else:\n                ls.show_message_log(f\"skip compile of {ql_stmt}\")\n        except errors.EdgeDBError as error:\n            diagnostics.append(ls_utils.error_to_lsp(error))\n\n    diagnostics_set.extend(doc, diagnostics)\n    return (diagnostics_set, ir_stmts)\n"
  },
  {
    "path": "edb/language_server/utils.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport dataclasses\nfrom typing import Iterable\n\nfrom lsprotocol import types as lsp_types\nimport pygls\n\nfrom edb import errors\nfrom edb.edgeql import tokenizer\n\n\n@dataclasses.dataclass(kw_only=True)\nclass DiagnosticsSet:\n    by_doc: dict[pygls.workspace.TextDocument, list[lsp_types.Diagnostic]] = (\n        dataclasses.field(default_factory=lambda: {})\n    )\n\n    def append(\n        self,\n        doc: pygls.workspace.TextDocument,\n        diagnostic: lsp_types.Diagnostic,\n    ):\n        if doc not in self.by_doc:\n            self.by_doc[doc] = []\n        self.by_doc[doc].append(diagnostic)\n\n    def extend(\n        self,\n        doc: pygls.workspace.TextDocument,\n        diagnostics: Iterable[lsp_types.Diagnostic],\n    ):\n        if doc not in self.by_doc:\n            self.by_doc[doc] = []\n        self.by_doc[doc].extend(diagnostics)\n\n    def merge(self, other: 'DiagnosticsSet'):\n        for doc, diags in other.by_doc.items():\n            self.extend(doc, diags)\n\n    def has_any(self) -> bool:\n        for diags in self.by_doc.values():\n            if len(diags) != 0:\n                return True\n        return False\n\n\n# Convert a Span to LSP Range\ndef span_to_lsp(\n    source: str, span: tuple[int, int | None] | None\n) -> lsp_types.Range:\n    if span:\n        (start, end) = tokenizer.inflate_span(source, span)\n    else:\n        (start, end) = (None, None)\n    assert end\n\n    return lsp_types.Range(\n        start=(\n            lsp_types.Position(\n                line=start.line - 1,\n                character=start.column - 1,\n            )\n            if start\n            else lsp_types.Position(line=0, character=0)\n        ),\n        end=(\n            lsp_types.Position(\n                line=end.line - 1,\n                character=end.column - 1,\n            )\n            if end\n            else lsp_types.Position(line=0, character=0)\n        ),\n    )\n\n\n# Convert EdgeDBError into an LSP Diagnostic\ndef error_to_lsp(error: errors.EdgeDBError) -> lsp_types.Diagnostic:\n    message: str = error.args[0]\n\n    if hint := error.hint:\n        message += f\"\\nHint: {hint}\"\n\n    return lsp_types.Diagnostic(\n        range=(\n            lsp_types.Range(\n                start=lsp_types.Position(\n                    line=error.line - 1,\n                    character=error.col - 1,\n                ),\n                end=lsp_types.Position(\n                    line=error.line_end - 1,\n                    character=error.col_end - 1,\n                ),\n            )\n            if error.line >= 0\n            else lsp_types.Range(\n                start=lsp_types.Position(line=0, character=0),\n                end=lsp_types.Position(line=0, character=0),\n            )\n        ),\n        severity=lsp_types.DiagnosticSeverity.Error,\n        message=message,\n    )\n\n\n# Constructs a new diagnostic in the first line of the document\ndef new_diagnostic_at_the_top(message: str) -> lsp_types.Diagnostic:\n    return lsp_types.Diagnostic(\n        range=lsp_types.Range(\n            start=lsp_types.Position(line=0, character=0),\n            end=lsp_types.Position(line=1, character=0),\n        ),\n        severity=lsp_types.DiagnosticSeverity.Error,\n        message=message,\n        related_information=[],\n    )\n"
  },
  {
    "path": "edb/lib/__init__.py",
    "content": "from __future__ import annotations\n"
  },
  {
    "path": "edb/lib/_testmode.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# Bits used for testing of the std-only functionality.\n# These definitions are picked up if the EdgeDB instance is bootstrapped\n# with --testmode.\n\nCREATE TYPE cfg::TestSessionConfig EXTENDING cfg::ConfigObject {\n    CREATE REQUIRED PROPERTY name -> std::str {\n        CREATE CONSTRAINT std::exclusive;\n    }\n};\n\n\nCREATE ABSTRACT TYPE cfg::Base EXTENDING cfg::ConfigObject {\n    CREATE REQUIRED PROPERTY name -> std::str\n};\n\n\nCREATE TYPE cfg::Subclass1 EXTENDING cfg::Base {\n    CREATE REQUIRED PROPERTY sub1 -> std::str;\n};\n\n\nCREATE TYPE cfg::Subclass2 EXTENDING cfg::Base {\n    CREATE REQUIRED PROPERTY sub2 -> std::str;\n};\n\n\nCREATE TYPE cfg::TestInstanceConfig EXTENDING cfg::ConfigObject {\n    CREATE REQUIRED PROPERTY name -> std::str {\n        CREATE CONSTRAINT std::exclusive;\n    };\n\n    CREATE LINK obj -> cfg::Base;\n};\n\nCREATE TYPE cfg::TestInstanceConfigStatTypes EXTENDING cfg::TestInstanceConfig {\n    CREATE PROPERTY memprop -> cfg::memory;\n    CREATE PROPERTY durprop -> std::duration;\n};\n\n\nCREATE SCALAR TYPE cfg::TestEnum EXTENDING enum<One, Two, Three>;\nCREATE SCALAR TYPE cfg::TestEnabledDisabledEnum\n    EXTENDING enum<Enabled, Disabled>;\n\n\nALTER TYPE cfg::AbstractConfig {\n    CREATE MULTI LINK sessobj -> cfg::TestSessionConfig {\n        CREATE ANNOTATION cfg::internal := 'true';\n    };\n    CREATE MULTI LINK sysobj -> cfg::TestInstanceConfig {\n        CREATE ANNOTATION cfg::internal := 'true';\n    };\n\n    CREATE PROPERTY __internal_testvalue -> std::int64 {\n        CREATE ANNOTATION cfg::internal := 'true';\n        CREATE ANNOTATION cfg::system := 'true';\n        SET default := 0;\n    };\n\n    CREATE PROPERTY __internal_sess_testvalue -> std::int64 {\n        CREATE ANNOTATION cfg::internal := 'true';\n        SET default := 0;\n    };\n\n    CREATE PROPERTY __internal_testmode -> std::bool {\n        CREATE ANNOTATION cfg::internal := 'true';\n        CREATE ANNOTATION cfg::affects_compilation := 'true';\n        SET default := false;\n    };\n\n    # Fully suppress apply_query_rewrites, like is done for internal\n    # reflection queries.\n    CREATE PROPERTY __internal_no_apply_query_rewrites -> std::bool {\n        CREATE ANNOTATION cfg::internal := 'true';\n        CREATE ANNOTATION cfg::affects_compilation := 'true';\n        SET default := false;\n    };\n\n    # Use the \"reflection schema\" as the base schema instead of the\n    # normal std schema. This allows looking at all the schema fields\n    # that are hidden in the public introspection schema.\n    CREATE PROPERTY __internal_query_reflschema -> std::bool {\n        CREATE ANNOTATION cfg::internal := 'true';\n        CREATE ANNOTATION cfg::affects_compilation := 'true';\n        SET default := false;\n    };\n\n    CREATE PROPERTY __internal_restart -> std::bool {\n        CREATE ANNOTATION cfg::internal := 'true';\n        CREATE ANNOTATION cfg::system := 'true';\n        CREATE ANNOTATION cfg::requires_restart := 'true';\n        SET default := false;\n    };\n\n    CREATE MULTI PROPERTY multiprop -> std::str {\n        CREATE ANNOTATION cfg::internal := 'true';\n    };\n\n    CREATE PROPERTY singleprop -> std::str {\n        CREATE ANNOTATION cfg::internal := 'true';\n        SET default := '';\n    };\n\n    CREATE PROPERTY memprop -> cfg::memory {\n        CREATE ANNOTATION cfg::internal := 'true';\n        SET default := <cfg::memory>'0';\n    };\n\n    CREATE PROPERTY durprop -> std::duration {\n        CREATE ANNOTATION cfg::internal := 'true';\n        SET default := <std::duration>'0 seconds';\n    };\n\n    CREATE PROPERTY enumprop -> cfg::TestEnum {\n        CREATE ANNOTATION cfg::internal := 'true';\n        SET default := cfg::TestEnum.One;\n    };\n\n    CREATE PROPERTY boolprop -> std::bool {\n        CREATE ANNOTATION cfg::internal := 'true';\n        SET default := true;\n    };\n\n    CREATE PROPERTY __pg_max_connections -> std::int64 {\n        CREATE ANNOTATION cfg::internal := 'true';\n        CREATE ANNOTATION cfg::backend_setting := '\"max_connections\"';\n    };\n\n    CREATE PROPERTY __check_function_bodies -> cfg::TestEnabledDisabledEnum {\n        CREATE ANNOTATION cfg::internal := 'true';\n        CREATE ANNOTATION cfg::backend_setting := '\"check_function_bodies\"';\n        SET default := cfg::TestEnabledDisabledEnum.Enabled;\n    };\n};\n\n\n# For testing configs defined in extensions\ncreate extension package _conf VERSION '1.0' {\n    set ext_module := \"ext::_conf\";\n    set sql_extensions := [];\n    create module ext::_conf;\n\n    create type ext::_conf::SingleObj extending cfg::ConfigObject {\n        create required property name -> std::str {\n            set readonly := true;\n        };\n        create required property value -> std::str {\n            set readonly := true;\n        };\n        create required property fixed -> std::str {\n            set default := \"fixed!\";\n            set readonly := true;\n            set protected := true;\n        };\n    };\n    create type ext::_conf::Obj extending cfg::ConfigObject {\n        create required property name -> std::str {\n            set readonly := true;\n            create constraint std::exclusive;\n        };\n        create required property value -> std::str {\n            set readonly := true;\n            create delegated constraint std::exclusive;\n            create constraint expression on (__subject__[:5] != 'asdf_');\n        };\n        create property opt_value -> std::str {\n            set readonly := true;\n        };\n    };\n    create type ext::_conf::SubObj extending ext::_conf::Obj {\n        create required property extra -> int64 {\n            set readonly := true;\n        };\n        create required property duration_config: std::duration {\n            set default := <std::duration>'10 minutes';\n        };\n    };\n    create type ext::_conf::SecretObj extending ext::_conf::Obj {\n        create property secret -> std::str {\n            set readonly := true;\n            set secret := true;\n        };\n    };\n\n    create type ext::_conf::Obj2 extending cfg::ConfigObject {\n        create required property name -> std::str {\n            set readonly := true;\n            create constraint std::exclusive;\n        };\n    };\n\n    create type ext::_conf::Config extending cfg::ExtensionConfig {\n        create multi link objs -> ext::_conf::Obj;\n        create link obj -> ext::_conf::SingleObj;\n        create multi link objs2 -> ext::_conf::Obj2;\n\n        create property config_name -> std::str {\n            set default := \"\";\n        };\n        create property opt_value -> std::str;\n        create property secret -> std::str {\n            set secret := true;\n        };\n    };\n\n    create function ext::_conf::get_secret(c: ext::_conf::SecretObj)\n        -> optional std::str using (c.secret);\n    create function ext::_conf::get_top_secret()\n        -> set of std::str using (\n          cfg::Config.extensions[is ext::_conf::Config].secret);\n    create alias ext::_conf::OK := (\n        cfg::Config.extensions[is ext::_conf::Config].secret ?= 'foobaz');\n};\n\n# std::_gen_series\n\nCREATE FUNCTION\nstd::_gen_series(\n    `start`: std::int64,\n    stop: std::int64\n) -> SET OF std::int64\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'generate_series';\n};\n\nCREATE FUNCTION\nstd::_gen_series(\n    `start`: std::int64,\n    stop: std::int64,\n    step: std::int64\n) -> SET OF std::int64\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'generate_series';\n};\n\nCREATE FUNCTION\nstd::_gen_series(\n    `start`: std::bigint,\n    stop: std::bigint\n) -> SET OF std::bigint\n{\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    USING SQL FUNCTION 'generate_series';\n};\n\nCREATE FUNCTION\nstd::_gen_series(\n    `start`: std::bigint,\n    stop: std::bigint,\n    step: std::bigint\n) -> SET OF std::bigint\n{\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    USING SQL FUNCTION 'generate_series';\n};\n\n\nCREATE FUNCTION\nsys::_sleep(duration: std::float64) -> std::bool\n{\n    CREATE ANNOTATION std::description :=\n        'Make the current session sleep for *duration* seconds.';\n    # This function has side-effect.\n    SET volatility := 'Volatile';\n    USING SQL $$\n    SELECT pg_sleep(\"duration\") IS NOT NULL;\n    $$;\n};\n\nCREATE FUNCTION\nsys::_sleep(duration: std::duration) -> std::bool\n{\n    CREATE ANNOTATION std::description :=\n        'Make the current session sleep for *duration* time.';\n    # This function has side-effect.\n    SET volatility := 'Volatile';\n    USING SQL $$\n    SELECT pg_sleep_for(\"duration\") IS NOT NULL;\n    $$;\n};\n\n\nCREATE FUNCTION\nsys::_postgres_version() -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Get the postgres version string';\n    USING SQL $$\n    SELECT version()\n    $$;\n};\n\n\nCREATE FUNCTION\nsys::_advisory_lock(key: std::int64) -> std::bool\n{\n    CREATE ANNOTATION std::description :=\n        'Obtain an exclusive session-level advisory lock.';\n    # This function has side-effect.\n    SET volatility := 'Volatile';\n    USING SQL $$\n    SELECT CASE WHEN \"key\" < 0 THEN\n        edgedb_VER.raise(NULL::bool, msg => 'lock key cannot be negative')\n    ELSE\n        pg_advisory_lock(\"key\") IS NOT NULL\n    END;\n    $$;\n};\n\n\nCREATE FUNCTION\nsys::_advisory_unlock(key: std::int64) -> std::bool\n{\n    CREATE ANNOTATION std::description :=\n        'Release an exclusive session-level advisory lock.';\n    # This function has side-effect.\n    SET volatility := 'Volatile';\n    USING SQL $$\n    SELECT CASE WHEN \"key\" < 0 THEN\n        edgedb_VER.raise(NULL::bool, msg => 'lock key cannot be negative')\n    ELSE\n        pg_advisory_unlock(\"key\")\n    END;\n    $$;\n};\n\n\nCREATE FUNCTION\nsys::_advisory_unlock_all() -> std::bool\n{\n    CREATE ANNOTATION std::description :=\n        'Release all session-level advisory locks held by the current session.';\n    # This function has side-effect.\n    SET volatility := 'Volatile';\n    USING SQL $$\n    SELECT pg_advisory_unlock_all() IS NOT NULL;\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::_datetime_range_buckets(\n    low: std::datetime,\n    high: std::datetime,\n    granularity: str,\n) -> SET OF tuple<std::datetime, std::datetime>\n{\n    CREATE ANNOTATION std::description :=\n        'Generate a set of datetime buckets for a given time period '\n        ++ 'and a given granularity';\n    # date_trunc of timestamptz is STABLE in PostgreSQL\n    SET volatility := 'Stable';\n    USING SQL $$\n    SELECT\n        lo::edgedbt.timestamptz_t,\n        hi::edgedbt.timestamptz_t\n    FROM\n        (SELECT\n            series AS lo,\n            lead(series) OVER () AS hi\n        FROM\n            generate_series(\n                \"low\",\n                \"high\",\n                \"granularity\"::interval\n            ) AS series) AS q\n    WHERE\n        hi IS NOT NULL\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::_current_setting(sqlname: str) -> OPTIONAL std::str {\n    USING SQL $$\n      SELECT current_setting(sqlname, true)\n    $$;\n};\n\n\ncreate function std::_set_config(sqlname: std::str, val: std::str) -> std::str {\n    using sql $$\n      select set_config(sqlname, val, true)\n    $$;\n};\n\ncreate function std::_warn_on_call() -> std::int64 {\n    using (0)\n};\n\n\nCREATE MODULE std::_test;\n\n\nCREATE FUNCTION\nstd::_test::abs(x: std::anyreal) -> std::anyreal\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'abs';\n};\n"
  },
  {
    "path": "edb/lib/cal.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nCREATE MODULE std::cal;\n\nCREATE SCALAR TYPE std::cal::local_datetime\n    EXTENDING std::anycontiguous;\n\nCREATE SCALAR TYPE std::cal::local_date\n    EXTENDING std::anydiscrete;\n\nCREATE SCALAR TYPE std::cal::local_time EXTENDING std::anyscalar;\n\nCREATE SCALAR TYPE std::cal::relative_duration EXTENDING std::anyscalar;\n\nCREATE SCALAR TYPE std::cal::date_duration EXTENDING std::anyscalar;\n\n\n## Functions\n## ---------\n\nCREATE FUNCTION\nstd::cal::to_local_datetime(s: std::str, fmt: OPTIONAL str={})\n    -> std::cal::local_datetime\n{\n    CREATE ANNOTATION std::description :=\n        'Create a `std::cal::local_datetime` value.';\n    # Helper function to_local_datetime is VOLATILE.\n    SET volatility := 'Volatile';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            edgedb_VER.local_datetime_in(\"s\")\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::edgedbt.timestamp_t,\n                'invalid_parameter_value',\n                msg => (\n                    'to_local_datetime(): '\n                    || '\"fmt\" argument must be a non-empty string'\n                )\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                edgedb_VER.to_local_datetime(\"s\", \"fmt\"),\n                'invalid_parameter_value',\n                msg => (\n                    'to_local_datetime(): '\n                    || 'format ''' || \"fmt\" || ''' is invalid'\n                )\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::cal::to_local_datetime(year: std::int64, month: std::int64, day: std::int64,\n                       hour: std::int64, min: std::int64, sec: std::float64)\n    -> std::cal::local_datetime\n{\n    CREATE ANNOTATION std::description :=\n        'Create a `std::cal::local_datetime` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT make_timestamp(\n        \"year\"::int, \"month\"::int, \"day\"::int,\n        \"hour\"::int, \"min\"::int, \"sec\"\n    )::edgedbt.timestamp_t\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::cal::to_local_datetime(dt: std::datetime, zone: std::str)\n    -> std::cal::local_datetime\n{\n    CREATE ANNOTATION std::description :=\n        'Create a `std::cal::local_datetime` value.';\n    # The version of timezone with these arguments is IMMUTABLE.\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT timezone(\"zone\", \"dt\")::edgedbt.timestamp_t;\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::cal::to_local_date(s: std::str, fmt: OPTIONAL str={}) -> std::cal::local_date\n{\n    CREATE ANNOTATION std::description := 'Create a `std::cal::local_date` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            edgedb_VER.local_date_in(\"s\")\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::edgedbt.date_t,\n                'invalid_parameter_value',\n                msg => (\n                    'to_local_date(): '\n                    || '\"fmt\" argument must be a non-empty string'\n                )\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                edgedb_VER.to_local_datetime(\"s\", \"fmt\")::edgedbt.date_t,\n                'invalid_parameter_value',\n                msg => (\n                    'to_local_date(): format ''' || \"fmt\" || ''' is invalid'\n                )\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::cal::to_local_date(dt: std::datetime, zone: std::str)\n    -> std::cal::local_date\n{\n    CREATE ANNOTATION std::description := 'Create a `std::cal::local_date` value.';\n    # The version of timezone with these arguments is IMMUTABLE.\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT timezone(\"zone\", \"dt\")::edgedbt.date_t;\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::cal::to_local_date(year: std::int64, month: std::int64, day: std::int64)\n    -> std::cal::local_date\n{\n    CREATE ANNOTATION std::description := 'Create a `std::cal::local_date` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT make_date(\"year\"::int, \"month\"::int, \"day\"::int)::edgedbt.date_t\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::cal::to_local_time(s: std::str, fmt: OPTIONAL str={}) -> std::cal::local_time\n{\n    CREATE ANNOTATION std::description := 'Create a `std::cal::local_time` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            edgedb_VER.local_time_in(\"s\")\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::time,\n                'invalid_parameter_value',\n                msg => (\n                    'to_local_time(): '\n                    || '\"fmt\" argument must be a non-empty string'\n                )\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                edgedb_VER.to_local_datetime(\"s\", \"fmt\")::time,\n                'invalid_parameter_value',\n                msg => (\n                    'to_local_time(): '\n                    || 'format ''' || \"fmt\" || ''' is invalid'\n                )\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::cal::to_local_time(dt: std::datetime, zone: std::str)\n    -> std::cal::local_time\n{\n    CREATE ANNOTATION std::description := 'Create a `std::cal::local_time` value.';\n    # The version of timezone with these arguments is IMMUTABLE and so\n    # is the cast.\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT timezone(\"zone\", \"dt\")::time;\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::cal::to_local_time(hour: std::int64, min: std::int64, sec: std::float64)\n    -> std::cal::local_time\n{\n    CREATE ANNOTATION std::description := 'Create a `std::cal::local_time` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT\n        CASE WHEN date_part('hour', x.t) = 24\n        THEN\n            edgedb_VER.raise(\n                NULL::time,\n                'invalid_datetime_format',\n                msg => (\n                    'std::cal::local_time field value out of range: '\n                    || quote_literal(x.t::text)\n                )\n            )\n        ELSE\n            x.t\n        END\n    FROM (\n        SELECT make_time(\"hour\"::int, \"min\"::int, \"sec\") as t\n    ) as x\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::cal::to_relative_duration(\n        NAMED ONLY years: std::int64=0,\n        NAMED ONLY months: std::int64=0,\n        NAMED ONLY days: std::int64=0,\n        NAMED ONLY hours: std::int64=0,\n        NAMED ONLY minutes: std::int64=0,\n        NAMED ONLY seconds: std::float64=0,\n        NAMED ONLY microseconds: std::int64=0\n    ) -> std::cal::relative_duration\n{\n    CREATE ANNOTATION std::description := 'Create a `std::cal::relative_duration` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        make_interval(\n            \"years\"::int,\n            \"months\"::int,\n            0,\n            \"days\"::int,\n            \"hours\"::int,\n            \"minutes\"::int,\n            \"seconds\"\n        ) +\n        (microseconds::text || ' microseconds')::interval\n    )::edgedbt.relative_duration_t\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::cal::to_date_duration(\n        NAMED ONLY years: std::int64=0,\n        NAMED ONLY months: std::int64=0,\n        NAMED ONLY days: std::int64=0\n    ) -> std::cal::date_duration\n{\n    CREATE ANNOTATION std::description := 'Create a `std::cal::date_duration` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT make_interval(\n        \"years\"::int,\n        \"months\"::int,\n        0,\n        \"days\"::int\n    )::edgedbt.date_duration_t\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::cal::time_get(dt: std::cal::local_time, el: std::str) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Extract a specific element of input time by name.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT CASE WHEN \"el\" IN ('hour', 'microseconds', 'milliseconds',\n            'minutes', 'seconds')\n        THEN date_part(\"el\", \"dt\")\n        WHEN \"el\" = 'midnightseconds'\n        THEN date_part('epoch', \"dt\")\n        ELSE\n            edgedb_VER.raise(\n                NULL::float,\n                'invalid_datetime_format',\n                msg => (\n                    'invalid unit for std::time_get: ' || quote_literal(\"el\")\n                ),\n                detail => (\n                    '{\"hint\":\"Supported units: hour, microseconds, ' ||\n                    'midnightseconds, milliseconds, minutes, seconds.\"}'\n                )\n            )\n        END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::cal::date_get(dt: std::cal::local_date, el: std::str) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Extract a specific element of input date by name.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT CASE WHEN \"el\" IN (\n            'century', 'day', 'decade', 'dow', 'doy',\n            'isodow', 'isoyear', 'millennium',\n            'month', 'quarter', 'week', 'year')\n        THEN date_part(\"el\", \"dt\")\n        ELSE\n            edgedb_VER.raise(\n                NULL::float,\n                'invalid_datetime_format',\n                msg => (\n                    'invalid unit for std::date_get: ' || quote_literal(\"el\")\n                ),\n                detail => (\n                    '{\"hint\":\"Supported units: century, day, ' ||\n                    'decade, dow, doy, isodow, isoyear, ' ||\n                    'millennium, month, quarter, seconds, week, year.\"}'\n                )\n            )\n        END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::cal::duration_normalize_hours(dur: std::cal::relative_duration)\n  -> std::cal::relative_duration\n{\n    CREATE ANNOTATION std::description :=\n        'Convert 24-hour chunks into days.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    USING SQL FUNCTION 'justify_hours';\n};\n\n\nCREATE FUNCTION\nstd::cal::duration_normalize_days(dur: std::cal::relative_duration)\n  -> std::cal::relative_duration\n{\n    CREATE ANNOTATION std::description :=\n        'Convert 30-day chunks into months.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    USING SQL FUNCTION 'justify_days';\n};\n\n\nCREATE FUNCTION\nstd::cal::duration_normalize_days(dur: std::cal::date_duration)\n  -> std::cal::date_duration\n{\n    CREATE ANNOTATION std::description :=\n        'Convert 30-day chunks into months.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    USING SQL FUNCTION 'justify_days';\n};\n\n\n\n## Operators on std::datetime\n## --------------------------\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::datetime, r: std::cal::relative_duration) -> std::datetime {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    # Immutable because datetime is guaranteed to be in UTC and no DST issues\n    # should affect this.\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL $$\n        SELECT (\"l\" + \"r\")::edgedbt.timestamptz_t\n    $$\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::cal::relative_duration, r: std::datetime) -> std::datetime {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    # Immutable because datetime is guaranteed to be in UTC and no DST issues\n    # should affect this.\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL $$\n        SELECT (\"l\" + \"r\")::edgedbt.timestamptz_t\n    $$\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::datetime, r: std::cal::relative_duration) -> std::datetime {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time subtraction.';\n    # Immutable because datetime is guaranteed to be in UTC and no DST issues\n    # should affect this.\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT (\"l\" - \"r\")::edgedbt.timestamptz_t\n    $$\n};\n\n\n## Operators on std::cal::local_datetime\n## --------------------------------\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::cal::local_datetime, r: std::cal::local_datetime) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=(timestamp,timestamp)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::cal::local_datetime,\n           r: OPTIONAL std::cal::local_datetime) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::cal::local_datetime, r: std::cal::local_datetime) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>(timestamp,timestamp)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::cal::local_datetime,\n            r: OPTIONAL std::cal::local_datetime) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::cal::local_datetime, r: std::cal::local_datetime) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>(timestamp,timestamp)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::cal::local_datetime, r: std::cal::local_datetime) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=(timestamp,timestamp)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::cal::local_datetime, r: std::cal::local_datetime) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<(timestamp,timestamp)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::cal::local_datetime, r: std::cal::local_datetime) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=(timestamp,timestamp)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::cal::local_datetime, r: std::duration) -> std::cal::local_datetime {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL $$\n        SELECT (\"l\" + \"r\")::edgedbt.timestamp_t\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::duration, r: std::cal::local_datetime) -> std::cal::local_datetime {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL $$\n        SELECT (\"l\" + \"r\")::edgedbt.timestamp_t\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::cal::local_datetime, r: std::duration) -> std::cal::local_datetime {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT (\"l\" - \"r\")::edgedbt.timestamp_t\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::cal::local_datetime, r: std::cal::relative_duration) -> std::cal::local_datetime {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL $$\n        SELECT (\"l\" + \"r\")::edgedbt.timestamp_t\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::cal::relative_duration, r: std::cal::local_datetime) -> std::cal::local_datetime {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL $$\n        SELECT (\"l\" + \"r\")::edgedbt.timestamp_t\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::cal::local_datetime, r: std::cal::relative_duration) -> std::cal::local_datetime {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT (\"l\" - \"r\")::edgedbt.timestamp_t\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::cal::local_datetime, r: std::cal::local_datetime) -> std::cal::relative_duration {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Date/time subtraction.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    USING SQL OPERATOR r'-(timestamp, timestamp)';\n};\n\n\n## Operators on std::cal::local_date\n## ----------------------------\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::cal::local_date, r: std::cal::local_date) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=(date,date)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::cal::local_date,\n           r: OPTIONAL std::cal::local_date) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::cal::local_date, r: std::cal::local_date) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>(date,date)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::cal::local_date,\n            r: OPTIONAL std::cal::local_date) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::cal::local_date, r: std::cal::local_date) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>(date,date)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::cal::local_date, r: std::cal::local_date) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=(date,date)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::cal::local_date, r: std::cal::local_date) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<(date,date)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::cal::local_date, r: std::cal::local_date) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=(date,date)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::cal::local_date, r: std::duration) -> std::cal::local_datetime\n{\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    SET force_return_cast := true;\n    USING SQL $$\n        SELECT (\"l\" + \"r\")::edgedbt.timestamp_t\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::duration, r: std::cal::local_date) -> std::cal::local_datetime\n{\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    SET force_return_cast := true;\n    USING SQL $$\n        SELECT (\"l\" + \"r\")::edgedbt.timestamp_t\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::cal::local_date, r: std::duration) -> std::cal::local_datetime\n{\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time subtraction.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    USING SQL $$\n        SELECT (\"l\" - \"r\")::edgedbt.timestamp_t\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::cal::local_date, r: std::cal::relative_duration) -> std::cal::local_datetime\n{\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    SET force_return_cast := true;\n    USING SQL $$\n        SELECT (\"l\" + \"r\")::edgedbt.timestamp_t\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::cal::relative_duration, r: std::cal::local_date) -> std::cal::local_datetime\n{\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    SET force_return_cast := true;\n    USING SQL $$\n        SELECT (\"l\" + \"r\")::edgedbt.timestamp_t\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::cal::local_date, r: std::cal::relative_duration) -> std::cal::local_datetime\n{\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time subtraction.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    USING SQL $$\n        SELECT (\"l\" - \"r\")::edgedbt.timestamp_t\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::cal::local_date, r: std::cal::date_duration) -> std::cal::local_date\n{\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    SET force_return_cast := true;\n    USING SQL $$\n        SELECT (\"l\" + \"r\")::edgedbt.date_t\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::cal::date_duration, r: std::cal::local_date) -> std::cal::local_date\n{\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    SET force_return_cast := true;\n    USING SQL $$\n        SELECT (\"l\" + \"r\")::edgedbt.date_t\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::cal::local_date, r: std::cal::date_duration) -> std::cal::local_date\n{\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time subtraction.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    USING SQL $$\n        SELECT (\"l\" - \"r\")::edgedbt.date_t\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::cal::local_date, r: std::cal::local_date) -> std::cal::date_duration\n{\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Date subtraction.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    USING SQL $$\n        SELECT make_interval(0, 0, 0, \"l\" - \"r\")::edgedbt.date_duration_t\n    $$;\n};\n\n\n## Operators on std::cal::local_time\n## ----------------------------\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::cal::local_time, r: std::cal::local_time) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::cal::local_time,\n           r: OPTIONAL std::cal::local_time) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::cal::local_time, r: std::cal::local_time) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::cal::local_time,\n            r: OPTIONAL std::cal::local_time) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::cal::local_time, r: std::cal::local_time) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::cal::local_time, r: std::cal::local_time) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::cal::local_time, r: std::cal::local_time) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::cal::local_time, r: std::cal::local_time) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::cal::local_time, r: std::duration) -> std::cal::local_time {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL OPERATOR r'+(time, interval)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::duration, r: std::cal::local_time) -> std::cal::local_time {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL OPERATOR r'+(interval, time)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::cal::local_time, r: std::duration) -> std::cal::local_time {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'-(time, interval)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::cal::local_time, r: std::cal::relative_duration) -> std::cal::local_time {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL OPERATOR r'+(time, interval)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::cal::relative_duration, r: std::cal::local_time) -> std::cal::local_time {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL OPERATOR r'+(interval, time)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::cal::local_time, r: std::cal::relative_duration) -> std::cal::local_time {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'-(time, interval)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::cal::local_time, r: std::cal::local_time) -> std::cal::relative_duration {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Time subtraction.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    USING SQL OPERATOR r'-(time, time)';\n};\n\n\n## Operators on std::cal::relative_duration\n## ----------------------------\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::cal::relative_duration, r: std::cal::relative_duration) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=(interval,interval)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::cal::relative_duration,\n           r: OPTIONAL std::cal::relative_duration) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::cal::relative_duration, r: std::cal::relative_duration) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>(interval,interval)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (\n        l: OPTIONAL std::cal::relative_duration,\n        r: OPTIONAL std::cal::relative_duration\n) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::cal::relative_duration, r: std::cal::relative_duration) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>(interval,interval)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::cal::relative_duration, r: std::cal::relative_duration) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=(interval,interval)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::cal::relative_duration, r: std::cal::relative_duration) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<(interval,interval)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::cal::relative_duration, r: std::cal::relative_duration) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=(interval,interval)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::cal::relative_duration, r: std::cal::relative_duration) -> std::cal::relative_duration {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL $$\n    SELECT (\"l\"::interval + \"r\"::interval)::edgedbt.relative_duration_t;\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::cal::relative_duration, r: std::cal::relative_duration) -> std::cal::relative_duration {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description :=\n        'Time interval subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\"l\"::interval - \"r\"::interval)::edgedbt.relative_duration_t;\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::cal::date_duration, r: std::cal::date_duration) -> std::cal::date_duration {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL $$\n    SELECT (\"l\" + \"r\")::edgedbt.date_duration_t;\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::cal::date_duration, r: std::cal::date_duration) -> std::cal::date_duration {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description :=\n        'Time interval subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\"l\" - \"r\")::edgedbt.date_duration_t;\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::duration, r: std::cal::relative_duration) -> std::cal::relative_duration {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL $$\n    SELECT (\"l\"::interval + \"r\"::interval)::edgedbt.relative_duration_t;\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::cal::relative_duration, r: std::duration) -> std::cal::relative_duration {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL $$\n    SELECT (\"l\"::interval + \"r\"::interval)::edgedbt.relative_duration_t;\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::duration, r: std::cal::relative_duration) -> std::cal::relative_duration {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description :=\n        'Time interval subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\"l\"::interval - \"r\"::interval)::edgedbt.relative_duration_t;\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::cal::relative_duration, r: std::duration) -> std::cal::relative_duration {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description :=\n        'Time interval subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\"l\"::interval - \"r\"::interval)::edgedbt.relative_duration_t;\n    $$;\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`-` (v: std::cal::relative_duration) -> std::cal::relative_duration {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description :=\n        'Time interval negation.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (-\"v\"::interval)::edgedbt.relative_duration_t;\n    $$;\n};\n\n\n## Date/time casts\n## ---------------\n\nCREATE CAST FROM std::cal::local_datetime TO std::cal::local_date {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::cal::local_datetime TO std::cal::local_time {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::cal::local_date TO std::cal::local_datetime {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n    # Analogous to implicit cast from int64 to float64.\n    ALLOW IMPLICIT;\n};\n\n\nCREATE CAST FROM std::str TO std::cal::local_datetime {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'edgedb.local_datetime_in';\n};\n\n\nCREATE CAST FROM std::str TO std::cal::local_date {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'edgedb.local_date_in';\n};\n\n\nCREATE CAST FROM std::str TO std::cal::local_time {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'edgedb.local_time_in';\n};\n\n\nCREATE CAST FROM std::str TO std::cal::relative_duration {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT val::edgedbt.relative_duration_t;\n    $$;\n};\n\n\nCREATE CAST FROM std::str TO std::cal::date_duration {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'edgedb.date_duration_in';\n};\n\n\nCREATE CAST FROM std::cal::local_datetime TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT trim(to_json(val)::text, '\"');\n    $$;\n};\n\nCREATE CAST FROM std::cal::local_date TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::cal::local_time TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::cal::relative_duration TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::cal::date_duration TO std::str {\n    SET volatility := 'Immutable';\n    # We want the 0 date_duration canonically represented be in lowest\n    # date_duration units, i.e. in days.\n    USING SQL $$\n    SELECT CASE WHEN (val::text = 'PT0S')\n        THEN 'P0D'\n        ELSE val::text\n    END\n    $$;\n};\n\n\nCREATE CAST FROM std::cal::local_datetime TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'to_jsonb';\n};\n\n\nCREATE CAST FROM std::cal::local_date TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'to_jsonb';\n};\n\n\nCREATE CAST FROM std::cal::local_time TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'to_jsonb';\n};\n\n\nCREATE CAST FROM std::cal::relative_duration TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'to_jsonb';\n};\n\n\nCREATE CAST FROM std::cal::date_duration TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'to_jsonb';\n    # We want the 0 date_duration canonically represented be in lowest\n    # date_duration units, i.e. in days.\n    USING SQL $$\n    SELECT CASE WHEN (val::text = 'PT0S')\n        THEN to_jsonb('P0D'::text)\n        ELSE to_jsonb(val)\n    END\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::cal::local_datetime {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT edgedb_VER.local_datetime_in(\n        edgedb_VER.jsonb_extract_scalar(val, 'string', detail => detail)\n    );\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::cal::local_date {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT edgedb_VER.local_date_in(\n        edgedb_VER.jsonb_extract_scalar(val, 'string', detail => detail)\n    );\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::cal::local_time {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT edgedb_VER.local_time_in(\n        edgedb_VER.jsonb_extract_scalar(val, 'string', detail => detail)\n    );\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::cal::relative_duration {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT edgedb_VER.jsonb_extract_scalar(\n        val, 'string', detail => detail\n    )::interval::edgedbt.relative_duration_t;\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::cal::date_duration {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT edgedb_VER.date_duration_in(\n        edgedb_VER.jsonb_extract_scalar(val, 'string', detail => detail)\n    );\n    $$;\n};\n\n\nCREATE CAST FROM std::duration TO std::cal::relative_duration {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::cal::relative_duration TO std::duration {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::cal::date_duration TO std::cal::relative_duration {\n    # Same underlying types that don't require any DST calculations to convert\n    # into eachother.\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n    # Analogous to implicit cast from int64 to float64.\n    ALLOW IMPLICIT;\n};\n\n\nCREATE CAST FROM std::cal::relative_duration TO std::cal::date_duration {\n    # Same underlying types that don't require any DST calculations to convert\n    # into eachother.\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\n## Modified functions\n## ------------------\n\nCREATE FUNCTION\nstd::datetime_get(dt: std::cal::local_datetime, el: std::str) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Extract a specific element of input datetime by name.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT CASE WHEN \"el\" IN (\n            'century', 'day', 'decade', 'dow', 'doy', 'hour',\n            'isodow', 'isoyear', 'microseconds', 'millennium',\n            'milliseconds', 'minutes', 'month', 'quarter',\n            'seconds', 'week', 'year')\n        THEN date_part(\"el\", \"dt\")\n        WHEN \"el\" = 'epochseconds'\n        THEN date_part('epoch', \"dt\")\n        ELSE\n            edgedb_VER.raise(\n                NULL::float,\n                'invalid_datetime_format',\n                msg => (\n                    'invalid unit for std::datetime_get: '\n                    || quote_literal(\"el\")\n                ),\n                detail => (\n                    '{\"hint\":\"Supported units: epochseconds, century, '\n                    || 'day, decade, dow, doy, hour, isodow, isoyear, '\n                    || 'microseconds, millennium, milliseconds, minutes, '\n                    || 'month, quarter, seconds, week, year.\"}'\n                )\n            )\n        END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::duration_get(dt: std::cal::date_duration, el: std::str) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Extract a specific element of input duration by name.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT CASE WHEN \"el\" IN (\n            'millennium', 'century', 'decade', 'year', 'quarter', 'month',\n            'day')\n        THEN date_part(\"el\", \"dt\")\n        WHEN \"el\" = 'totalseconds'\n        THEN date_part('epoch', \"dt\")\n        ELSE\n            edgedb_VER.raise(\n                NULL::float,\n                'invalid_datetime_format',\n                msg => (\n                    'invalid unit for std::duration_get: '\n                    || quote_literal(\"el\")\n                ),\n                detail => (\n                    '{\"hint\":\"Supported units: '\n                    || 'millennium, century, decade, year, quarter, month, day, '\n                    || 'hour, and totalseconds.\"}'\n                )\n            )\n        END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::duration_get(dt: std::cal::relative_duration, el: std::str) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Extract a specific element of input duration by name.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT CASE WHEN \"el\" IN (\n            'millennium', 'century', 'decade', 'year', 'quarter', 'month',\n            'day', 'hour', 'minutes', 'seconds', 'milliseconds',\n            'microseconds')\n        THEN date_part(\"el\", \"dt\")\n        WHEN \"el\" = 'totalseconds'\n        THEN date_part('epoch', \"dt\")\n        ELSE\n            edgedb_VER.raise(\n                NULL::float,\n                'invalid_datetime_format',\n                msg => (\n                    'invalid unit for std::duration_get: '\n                    || quote_literal(\"el\")\n                ),\n                detail => (\n                    '{\"hint\":\"Supported units: '\n                    || 'millennium, century, decade, year, quarter, month, day, '\n                    || 'hour, minutes, seconds, milliseconds, microseconds, '\n                    || 'and totalseconds.\"}'\n                )\n            )\n        END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::duration_truncate(\n    dt: std::cal::date_duration,\n    unit: std::str\n) -> std::cal::date_duration\n{\n    CREATE ANNOTATION std::description :=\n        'Truncate the input duration to a particular precision.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT CASE WHEN \"unit\" IN (\n            'days', 'weeks', 'months', 'years', 'decades', 'centuries')\n        THEN date_trunc(\"unit\", \"dt\")::edgedbt.relative_duration_t\n        WHEN \"unit\" = 'quarters'\n        THEN date_trunc('quarter', \"dt\")::edgedbt.relative_duration_t\n        ELSE\n            edgedb_VER.raise(\n                NULL::edgedbt.relative_duration_t,\n                'invalid_datetime_format',\n                msg => (\n                    'invalid unit for std::duration_truncate: '\n                    || quote_literal(\"unit\")\n                ),\n                detail => (\n                    '{\"hint\":\"Supported units: days, weeks, months, '\n                    || 'quarters, years, decades, centuries.\"}'\n                )\n            )\n        END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::duration_truncate(\n    dt: std::cal::relative_duration,\n    unit: std::str\n) -> std::cal::relative_duration\n{\n    CREATE ANNOTATION std::description :=\n        'Truncate the input duration to a particular precision.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT CASE WHEN \"unit\" IN (\n            'microseconds', 'milliseconds', 'seconds',\n            'minutes', 'hours', 'days', 'weeks', 'months',\n            'years', 'decades', 'centuries')\n        THEN date_trunc(\"unit\", \"dt\")::edgedbt.relative_duration_t\n        WHEN \"unit\" = 'quarters'\n        THEN date_trunc('quarter', \"dt\")::edgedbt.relative_duration_t\n        ELSE\n            edgedb_VER.raise(\n                NULL::edgedbt.relative_duration_t,\n                'invalid_datetime_format',\n                msg => (\n                    'invalid unit for std::duration_truncate: '\n                    || quote_literal(\"unit\")\n                ),\n                detail => (\n                    '{\"hint\":\"Supported units: microseconds, milliseconds, '\n                    || 'seconds, minutes, hours, days, weeks, months, '\n                    || 'quarters, years, decades, centuries.\"}'\n                )\n            )\n        END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_str(dt: std::cal::local_datetime, fmt: OPTIONAL str={}) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return string representation of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            trim(to_json(\"dt\")::text, '\"')\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::text,\n                'invalid_parameter_value',\n                msg => 'to_str(): \"fmt\" argument must be a non-empty string'\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_char(\"dt\", \"fmt\"),\n                'invalid_parameter_value',\n                msg => 'to_str(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_str(d: std::cal::local_date, fmt: OPTIONAL str={}) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return string representation of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            \"d\"::text\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::text,\n                'invalid_parameter_value',\n                msg => 'to_str(): \"fmt\" argument must be a non-empty string'\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_char(\"d\", \"fmt\"),\n                'invalid_parameter_value',\n                msg => 'to_str(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\n# Currently local time is formatted by composing it with the local\n# current local date. This at least guarantees that the time\n# formatting is accessible and consistent with full datetime\n# formatting, but it exposes current date as well if it is included in\n# the format.\n# FIXME: date formatting should not have any special effect.\nCREATE FUNCTION\nstd::to_str(nt: std::cal::local_time, fmt: OPTIONAL str={}) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return string representation of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            \"nt\"::text\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::text,\n                'invalid_parameter_value',\n                msg => 'to_str(): \"fmt\" argument must be a non-empty string'\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_char(date_trunc('day', localtimestamp) + \"nt\", \"fmt\"),\n                'invalid_parameter_value',\n                msg => 'to_str(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_str(rd: std::cal::relative_duration, fmt: OPTIONAL str={}) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return string representation of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            \"rd\"::text\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::text,\n                'invalid_parameter_value',\n                msg => 'to_str(): \"fmt\" argument must be a non-empty string'\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_char(\"rd\", \"fmt\"),\n                'invalid_parameter_value',\n                msg => 'to_str(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_datetime(local: std::cal::local_datetime, zone: std::str)\n    -> std::datetime\n{\n    CREATE ANNOTATION std::description := 'Create a `datetime` value.';\n    # The version of timezone with these arguments is IMMUTABLE.\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT timezone(\"zone\", \"local\")::edgedbt.timestamptz_t;\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::min(vals: SET OF std::cal::local_datetime) -> OPTIONAL std::cal::local_datetime\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'min';\n};\n\n\nCREATE FUNCTION\nstd::min(vals: SET OF std::cal::local_date) -> OPTIONAL std::cal::local_date\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'min';\n};\n\n\nCREATE FUNCTION\nstd::min(vals: SET OF std::cal::local_time) -> OPTIONAL std::cal::local_time\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'min';\n};\n\n\nCREATE FUNCTION\nstd::min(vals: SET OF std::cal::relative_duration) -> OPTIONAL std::cal::relative_duration\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'min';\n};\n\n\nCREATE FUNCTION\nstd::min(vals: SET OF std::cal::date_duration) -> OPTIONAL std::cal::date_duration\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'min';\n};\n\n\nCREATE FUNCTION\nstd::min(vals: SET OF array<std::cal::local_datetime>) -> OPTIONAL array<std::cal::local_datetime>\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'min';\n};\n\n\nCREATE FUNCTION\nstd::min(vals: SET OF array<std::cal::local_date>) -> OPTIONAL array<std::cal::local_date>\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'min';\n};\n\n\nCREATE FUNCTION\nstd::min(vals: SET OF array<std::cal::local_time>) -> OPTIONAL array<std::cal::local_time>\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'min';\n};\n\n\nCREATE FUNCTION\nstd::min(vals: SET OF array<std::cal::relative_duration>) -> OPTIONAL array<std::cal::relative_duration>\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'min';\n};\n\n\nCREATE FUNCTION\nstd::min(vals: SET OF array<std::cal::date_duration>) -> OPTIONAL array<std::cal::date_duration>\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'min';\n};\n\n\nCREATE FUNCTION\nstd::max(vals: SET OF std::cal::local_datetime) -> OPTIONAL std::cal::local_datetime\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'max';\n};\n\n\nCREATE FUNCTION\nstd::max(vals: SET OF std::cal::local_date) -> OPTIONAL std::cal::local_date\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'max';\n};\n\n\nCREATE FUNCTION\nstd::max(vals: SET OF std::cal::local_time) -> OPTIONAL std::cal::local_time\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'max';\n};\n\n\nCREATE FUNCTION\nstd::max(vals: SET OF std::cal::relative_duration) -> OPTIONAL std::cal::relative_duration\n{\n    CREATE ANNOTATION std::description :=\n        'Return the greatest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'max';\n};\n\n\nCREATE FUNCTION\nstd::max(vals: SET OF std::cal::date_duration) -> OPTIONAL std::cal::date_duration\n{\n    CREATE ANNOTATION std::description :=\n        'Return the greatest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'max';\n};\n\n\nCREATE FUNCTION\nstd::max(vals: SET OF array<std::cal::local_datetime>) -> OPTIONAL array<std::cal::local_datetime>\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'max';\n};\n\n\nCREATE FUNCTION\nstd::max(vals: SET OF array<std::cal::local_date>) -> OPTIONAL array<std::cal::local_date>\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'max';\n};\n\n\nCREATE FUNCTION\nstd::max(vals: SET OF array<std::cal::local_time>) -> OPTIONAL array<std::cal::local_time>\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'max';\n};\n\n\nCREATE FUNCTION\nstd::max(vals: SET OF array<std::cal::relative_duration>) -> OPTIONAL array<std::cal::relative_duration>\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'max';\n};\n\n\nCREATE FUNCTION\nstd::max(vals: SET OF array<std::cal::date_duration>) -> OPTIONAL array<std::cal::date_duration>\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'max';\n};\n\n\nCREATE FUNCTION\nstd::sum(s: SET OF std::cal::relative_duration) -> std::cal::relative_duration\n{\n    CREATE ANNOTATION std::description :=\n        'Return the arithmetic sum of values in a set.';\n    SET volatility := 'Immutable';\n    SET initial_value := <std::cal::relative_duration>\"PT0S\";\n    SET force_return_cast := true;\n    USING SQL FUNCTION 'sum';\n};\n\n\nCREATE FUNCTION\nstd::sum(s: SET OF std::cal::date_duration) -> std::cal::date_duration\n{\n    CREATE ANNOTATION std::description :=\n        'Return the arithmetic sum of values in a set.';\n    SET volatility := 'Immutable';\n    SET initial_value := <std::cal::date_duration>\"PT0S\";\n    SET force_return_cast := true;\n    USING SQL FUNCTION 'sum';\n};\n\n\n\n## Range functions\n\n\n# FIXME: These functions introduce the concrete multirange types into the\n# schema. That's why they exist for each concrete type explicitly and aren't\n# defined generically for anytype.\nCREATE FUNCTION std::multirange_unpack(\n    val: multirange<std::cal::local_datetime>,\n) -> set of range<std::cal::local_datetime>\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'unnest';\n};\n\n\nCREATE FUNCTION std::multirange_unpack(\n    val: multirange<std::cal::local_date>,\n) -> set of range<std::cal::local_date>\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'unnest';\n};\n\n\nCREATE FUNCTION\nstd::range_unpack(\n    val: range<std::cal::local_datetime>,\n    step: std::cal::relative_duration\n) -> set of std::cal::local_datetime\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT d::edgedbt.timestamp_t\n        FROM\n            generate_series(\n                (\n                    edgedb_VER.range_lower_validate(val) + (\n                        CASE WHEN lower_inc(val)\n                            THEN '0'::interval\n                            ELSE step\n                        END\n                    )\n                )::timestamptz,\n                (\n                    edgedb_VER.range_upper_validate(val)\n                )::timestamptz,\n                step::interval\n            ) AS d\n        WHERE\n            upper_inc(val) OR d::edgedbt.timestamp_t < upper(val)\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::range_unpack(\n    val: range<std::cal::local_date>\n) -> set of std::cal::local_date\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT\n            generate_series(\n                (\n                    edgedb_VER.range_lower_validate(val) + (\n                        CASE WHEN lower_inc(val)\n                            THEN '0'::interval\n                            ELSE 'P1D'::interval\n                        END\n                    )\n                )::timestamp,\n                (\n                    edgedb_VER.range_upper_validate(val) - (\n                        CASE WHEN upper_inc(val)\n                            THEN '0'::interval\n                            ELSE 'P1D'::interval\n                        END\n                    )\n                )::timestamp,\n                'P1D'::interval\n            )::edgedbt.date_t\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::range_unpack(\n    val: range<std::cal::local_date>,\n    step: std::cal::date_duration\n) -> set of std::cal::local_date\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT\n            generate_series(\n                (\n                    edgedb_VER.range_lower_validate(val) + (\n                        CASE WHEN lower_inc(val)\n                            THEN '0'::interval\n                            ELSE 'P1D'::interval\n                        END\n                    )\n                )::timestamp,\n                (\n                    edgedb_VER.range_upper_validate(val) - (\n                        CASE WHEN upper_inc(val)\n                            THEN '0'::interval\n                            ELSE 'P1D'::interval\n                        END\n                    )\n                )::timestamp,\n                step::interval\n            )::edgedbt.date_t\n    $$;\n};\n\n# Need to cast edgedbt.date_t to date in order for the @> operator to work.\nCREATE FUNCTION std::contains(\n    haystack: range<std::cal::local_date>,\n    needle: std::cal::local_date\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"haystack\" @> (\"needle\"::date)\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\nCREATE FUNCTION std::contains(\n    haystack: multirange<std::cal::local_date>,\n    needle: std::cal::local_date\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"haystack\" @> (\"needle\"::date)\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};"
  },
  {
    "path": "edb/lib/cfg.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nCREATE MODULE cfg;\n\nCREATE MODULE cfg::perm;\nCREATE PERMISSION cfg::perm::configure_timeout;\nCREATE PERMISSION cfg::perm::configure_apply_access_policies;\nCREATE PERMISSION cfg::perm::configure_allow_user_specified_id;\n\nCREATE ABSTRACT INHERITABLE ANNOTATION cfg::backend_setting;\n\n# If report is set to 'true', that *system* config will be included\n# in the `system_config` ParameterStatus on each connection.\n# Non-system config cannot be reported.\nCREATE ABSTRACT INHERITABLE ANNOTATION cfg::report;\n\nCREATE ABSTRACT INHERITABLE ANNOTATION cfg::internal;\nCREATE ABSTRACT INHERITABLE ANNOTATION cfg::requires_restart;\n\n# System config means that config value can only be modified using\n# CONFIGURE INSTANCE command. System config is therefore *not* included\n# in the binary protocol state.\nCREATE ABSTRACT INHERITABLE ANNOTATION cfg::system;\n\nCREATE ABSTRACT INHERITABLE ANNOTATION cfg::affects_compilation;\n\n# Value is json. \"*\" means always allowed, other strings mean a permission\n# that must be held.\nCREATE ABSTRACT INHERITABLE ANNOTATION cfg::session_cfg_permissions;\n\n\nCREATE SCALAR TYPE cfg::memory EXTENDING std::anyscalar;\nCREATE SCALAR TYPE cfg::AllowBareDDL EXTENDING enum<AlwaysAllow, NeverAllow>;\nCREATE SCALAR TYPE cfg::StoreMigrationSDL EXTENDING enum<\n    AlwaysStore, NeverStore,\n>;\nCREATE SCALAR TYPE cfg::ConnectionTransport EXTENDING enum<\n    TCP, TCP_PG, HTTP, SIMPLE_HTTP, HTTP_METRICS, HTTP_HEALTH>;\nCREATE SCALAR TYPE cfg::QueryCacheMode EXTENDING enum<\n    InMemory, RegInline, PgFunc, Default>;\nCREATE SCALAR TYPE cfg::QueryStatsOption EXTENDING enum<None, All>;\n\nCREATE ABSTRACT TYPE cfg::ConfigObject EXTENDING std::BaseObject;\n\nCREATE ABSTRACT TYPE cfg::AuthMethod EXTENDING cfg::ConfigObject {\n    # Connection transports applicable to this auth entry.\n    # An empty set means \"apply to all transports\".\n    CREATE MULTI PROPERTY transports -> cfg::ConnectionTransport {\n        SET readonly := true;\n    };\n};\n\nCREATE TYPE cfg::Trust EXTENDING cfg::AuthMethod;\nCREATE TYPE cfg::SCRAM EXTENDING cfg::AuthMethod {\n    ALTER PROPERTY transports {\n        SET default := { cfg::ConnectionTransport.TCP };\n    };\n};\nCREATE TYPE cfg::JWT EXTENDING cfg::AuthMethod {\n    ALTER PROPERTY transports {\n        SET default := { cfg::ConnectionTransport.HTTP };\n    };\n};\nCREATE TYPE cfg::Password EXTENDING cfg::AuthMethod {\n    ALTER PROPERTY transports {\n        SET default := { cfg::ConnectionTransport.SIMPLE_HTTP };\n    };\n};\nCREATE TYPE cfg::mTLS EXTENDING cfg::AuthMethod {\n    ALTER PROPERTY transports {\n        SET default := {\n            cfg::ConnectionTransport.HTTP_METRICS,\n            cfg::ConnectionTransport.HTTP_HEALTH,\n        };\n    };\n};\n\nCREATE TYPE cfg::Auth EXTENDING cfg::ConfigObject {\n    CREATE REQUIRED PROPERTY priority -> std::int64 {\n        CREATE CONSTRAINT std::exclusive;\n        SET readonly := true;\n    };\n\n    CREATE MULTI PROPERTY user -> std::str {\n        SET readonly := true;\n        SET default := {'*'};\n    };\n\n    CREATE SINGLE LINK method -> cfg::AuthMethod {\n        CREATE CONSTRAINT std::exclusive;\n        SET readonly := true;\n    };\n\n    CREATE PROPERTY comment -> std::str {\n        SET readonly := true;\n    };\n};\n\nCREATE SCALAR TYPE cfg::SMTPSecurity EXTENDING enum<\n    PlainText,\n    TLS,\n    STARTTLS,\n    STARTTLSOrPlainText,\n>;\n\nCREATE ABSTRACT TYPE cfg::EmailProviderConfig EXTENDING cfg::ConfigObject {\n    CREATE REQUIRED PROPERTY name -> std::str {\n        CREATE CONSTRAINT std::exclusive;\n        CREATE ANNOTATION std::description :=\n            \"The name of the email provider.\";\n    };\n};\n\nCREATE TYPE cfg::SMTPProviderConfig EXTENDING cfg::EmailProviderConfig {\n    CREATE PROPERTY sender -> std::str {\n        CREATE ANNOTATION std::description :=\n            \"\\\"From\\\" address of system emails sent for e.g. \\\n            password reset, etc.\";\n    };\n    CREATE PROPERTY host -> std::str {\n        CREATE ANNOTATION std::description :=\n            \"Host of SMTP server to use for sending emails. \\\n            If not set, \\\"localhost\\\" will be used.\";\n    };\n    CREATE PROPERTY port -> std::int32 {\n        CREATE ANNOTATION std::description :=\n            \"Port of SMTP server to use for sending emails. \\\n            If not set, common defaults will be used depending on security: \\\n            465 for TLS, 587 for STARTTLS, 25 otherwise.\";\n    };\n    CREATE PROPERTY username -> std::str {\n        CREATE ANNOTATION std::description :=\n            \"Username to login as after connected to SMTP server.\";\n    };\n    CREATE PROPERTY password -> std::str {\n        SET secret := true;\n        CREATE ANNOTATION std::description :=\n            \"Password for login after connected to SMTP server.\";\n    };\n    CREATE REQUIRED PROPERTY security -> cfg::SMTPSecurity {\n        SET default := cfg::SMTPSecurity.STARTTLSOrPlainText;\n        CREATE ANNOTATION std::description :=\n            \"Security mode of the connection to SMTP server. \\\n            By default, initiate a STARTTLS upgrade if supported by the \\\n            server, or fallback to PlainText.\";\n    };\n    CREATE REQUIRED PROPERTY validate_certs -> std::bool {\n        SET default := true;\n        CREATE ANNOTATION std::description :=\n            \"Determines if SMTP server certificates are validated.\";\n    };\n    CREATE REQUIRED PROPERTY timeout_per_email -> std::duration {\n        SET default := <std::duration>'60 seconds';\n        CREATE ANNOTATION std::description :=\n            \"Maximum time to send an email, including retry attempts.\";\n    };\n    CREATE REQUIRED PROPERTY timeout_per_attempt -> std::duration {\n        SET default := <std::duration>'15 seconds';\n        CREATE ANNOTATION std::description :=\n            \"Maximum time for each SMTP request.\";\n    };\n};\n\nCREATE ABSTRACT TYPE cfg::AbstractConfig extending cfg::ConfigObject;\n\nCREATE ABSTRACT TYPE cfg::ExtensionConfig EXTENDING cfg::ConfigObject {\n    CREATE REQUIRED SINGLE LINK cfg -> cfg::AbstractConfig {\n        CREATE DELEGATED CONSTRAINT std::exclusive;\n    };\n};\n\nALTER TYPE cfg::AbstractConfig {\n    CREATE MULTI LINK extensions := .<cfg[IS cfg::ExtensionConfig];\n\n    CREATE REQUIRED PROPERTY session_idle_timeout -> std::duration {\n        CREATE ANNOTATION cfg::system := 'true';\n        CREATE ANNOTATION cfg::report := 'true';\n        CREATE ANNOTATION std::description :=\n            'How long client connections can stay inactive before being \\\n            closed by the server.';\n        SET default := <std::duration>'60 seconds';\n    };\n\n    CREATE REQUIRED PROPERTY default_transaction_isolation\n        -> sys::TransactionIsolation\n    {\n        CREATE ANNOTATION cfg::affects_compilation := 'true';\n        CREATE ANNOTATION cfg::backend_setting :=\n            '\"default_transaction_isolation\"';\n        CREATE ANNOTATION cfg::session_cfg_permissions := '\"*\"';\n        CREATE ANNOTATION std::description :=\n            'Controls the default isolation level of each new transaction, \\\n            including implicit transactions. Defaults to `Serializable`. \\\n            Note that changing this to a lower isolation level implies \\\n            that the transactions are also read-only by default regardless \\\n            of the value of the `default_transaction_access_mode` setting.';\n        SET default := sys::TransactionIsolation.Serializable;\n    };\n\n    CREATE REQUIRED PROPERTY default_transaction_access_mode\n        -> sys::TransactionAccessMode\n    {\n        CREATE ANNOTATION cfg::affects_compilation := 'true';\n        CREATE ANNOTATION cfg::session_cfg_permissions := '\"*\"';\n        CREATE ANNOTATION std::description :=\n            'Controls the default read-only status of each new transaction, \\\n            including implicit transactions. Defaults to `ReadWrite`. \\\n            Note that if `default_transaction_isolation` is set to any value \\\n            other than Serializable this parameter is implied to be \\\n            `ReadOnly` regardless of the actual value.';\n        SET default := sys::TransactionAccessMode.ReadWrite;\n    };\n\n    CREATE REQUIRED PROPERTY default_transaction_deferrable\n        -> sys::TransactionDeferrability\n    {\n        CREATE ANNOTATION cfg::backend_setting :=\n            '\"default_transaction_deferrable\"';\n        CREATE ANNOTATION cfg::session_cfg_permissions := '\"*\"';\n        CREATE ANNOTATION std::description :=\n            'Controls the default deferrable status of each new transaction. \\\n            It currently has no effect on read-write transactions or those \\\n            operating at isolation levels lower than `Serializable`. \\\n            The default is `NotDeferrable`.';\n        SET default := sys::TransactionDeferrability.NotDeferrable;\n    };\n\n    CREATE REQUIRED PROPERTY session_idle_transaction_timeout -> std::duration {\n        CREATE ANNOTATION cfg::backend_setting :=\n            '\"idle_in_transaction_session_timeout\"';\n        CREATE ANNOTATION cfg::session_cfg_permissions :=\n            '\"cfg::perm::configure_timeout\"';\n        CREATE ANNOTATION std::description :=\n            'How long client connections can stay inactive while in a \\\n            transaction.';\n        SET default := <std::duration>'10 seconds';\n    };\n\n    CREATE REQUIRED PROPERTY query_execution_timeout -> std::duration {\n        CREATE ANNOTATION cfg::backend_setting := '\"statement_timeout\"';\n        CREATE ANNOTATION cfg::session_cfg_permissions :=\n            '\"cfg::perm::configure_timeout\"';\n        CREATE ANNOTATION std::description :=\n            'How long an individual query can run before being aborted.';\n    };\n\n    CREATE REQUIRED PROPERTY listen_port -> std::int32 {\n        CREATE ANNOTATION cfg::system := 'true';\n        CREATE ANNOTATION std::description :=\n            'The TCP port the server listens on.';\n        SET default := 5656;\n        # Really we want a uint16, but oh well\n        CREATE CONSTRAINT std::min_value(0);\n        CREATE CONSTRAINT std::max_value(65535);\n    };\n\n    CREATE MULTI PROPERTY listen_addresses -> std::str {\n        CREATE ANNOTATION cfg::system := 'true';\n        CREATE ANNOTATION std::description :=\n            'The TCP/IP address(es) on which the server is to listen for \\\n            connections from client applications.';\n    };\n\n    CREATE MULTI LINK auth -> cfg::Auth {\n        CREATE ANNOTATION cfg::system := 'true';\n    };\n\n    CREATE MULTI LINK email_providers -> cfg::EmailProviderConfig {\n        CREATE ANNOTATION std::description :=\n            'The list of email providers that can be used to send emails.';\n    };\n\n    CREATE PROPERTY current_email_provider_name -> std::str {\n        CREATE ANNOTATION std::description :=\n            'The name of the current email provider.';\n    };\n\n    CREATE PROPERTY allow_dml_in_functions -> std::bool {\n        SET default := false;\n        CREATE ANNOTATION cfg::affects_compilation := 'true';\n        CREATE ANNOTATION cfg::internal := 'true';\n    };\n\n    CREATE PROPERTY allow_bare_ddl -> cfg::AllowBareDDL {\n        SET default := cfg::AllowBareDDL.AlwaysAllow;\n        CREATE ANNOTATION cfg::affects_compilation := 'true';\n        CREATE ANNOTATION std::description :=\n            'Whether DDL is allowed to be executed outside a migration.';\n    };\n\n    CREATE PROPERTY store_migration_sdl -> cfg::StoreMigrationSDL {\n        SET default := cfg::StoreMigrationSDL.NeverStore;\n        CREATE ANNOTATION cfg::affects_compilation := 'true';\n        CREATE ANNOTATION std::description :=\n            'When to store resulting SDL of a Migration. This may be slow.';\n    };\n\n    CREATE PROPERTY apply_access_policies -> std::bool {\n        SET default := true;\n        CREATE ANNOTATION cfg::affects_compilation := 'true';\n        CREATE ANNOTATION cfg::session_cfg_permissions :=\n            '\"cfg::perm::configure_apply_access_policies\"';\n        CREATE ANNOTATION std::description :=\n            'Whether access policies will be applied when running queries.';\n    };\n\n    CREATE PROPERTY apply_access_policies_pg -> std::bool {\n        SET default := true;\n        CREATE ANNOTATION cfg::affects_compilation := 'false';\n        CREATE ANNOTATION cfg::session_cfg_permissions :=\n            '\"cfg::perm::configure_apply_access_policies\"';\n        CREATE ANNOTATION std::description :=\n            'Whether access policies will be applied when running queries over \\\n            SQL adapter.';\n    };\n\n    CREATE PROPERTY allow_user_specified_id -> std::bool {\n        SET default := false;\n        CREATE ANNOTATION cfg::affects_compilation := 'true';\n        CREATE ANNOTATION cfg::session_cfg_permissions :=\n            '\"cfg::perm::configure_allow_user_specified_id\"';\n        CREATE ANNOTATION std::description :=\n            'Whether inserts are allowed to set the \\'id\\' property.';\n    };\n\n    CREATE PROPERTY simple_scoping -> std::bool {\n        CREATE ANNOTATION cfg::affects_compilation := 'true';\n        CREATE ANNOTATION cfg::session_cfg_permissions := '\"*\"';\n        CREATE ANNOTATION std::description :=\n            'Whether to use the new simple scoping behavior \\\n            (disable path factoring)';\n    };\n\n    CREATE PROPERTY warn_old_scoping -> std::bool {\n        CREATE ANNOTATION cfg::affects_compilation := 'true';\n        CREATE ANNOTATION cfg::session_cfg_permissions := '\"*\"';\n        CREATE ANNOTATION std::description :=\n            'Whether to warn when depending on old scoping behavior.';\n    };\n\n    CREATE MULTI PROPERTY cors_allow_origins -> std::str {\n        CREATE ANNOTATION std::description :=\n            'List of origins that can be returned in the \\\n            Access-Control-Allow-Origin HTTP header';\n    };\n\n    CREATE PROPERTY auto_rebuild_query_cache -> std::bool {\n        SET default := true;\n        CREATE ANNOTATION std::description :=\n            'Recompile all cached queries on DDL if enabled.';\n    };\n\n    CREATE PROPERTY auto_rebuild_query_cache_timeout -> std::duration {\n        CREATE ANNOTATION std::description :=\n            'Maximum time to spend recompiling cached queries on DDL.';\n        SET default := <std::duration>'60 seconds';\n    };\n\n    CREATE PROPERTY query_cache_mode -> cfg::QueryCacheMode {\n        SET default := cfg::QueryCacheMode.Default;\n        CREATE ANNOTATION cfg::affects_compilation := 'true';\n        CREATE ANNOTATION std::description :=\n            'Where the query cache is finally stored';\n    };\n\n    CREATE PROPERTY query_cache_size -> std::int32 {\n        SET default := 1000;\n        CREATE ANNOTATION cfg::system := 'true';\n        CREATE ANNOTATION cfg::requires_restart := 'true';\n        CREATE ANNOTATION std::description :=\n            'Maximum number of queries to cache in the query cache';\n    };\n\n    # HTTP Worker Configuration\n    CREATE PROPERTY http_max_connections -> std::int64 {\n        SET default := 10;\n        CREATE ANNOTATION std::description :=\n            'The maximum number of concurrent HTTP connections.';\n        CREATE ANNOTATION cfg::system := 'true';\n    };\n\n    # Exposed backend settings follow.\n    # When exposing a new setting, remember to modify\n    # the _read_sys_config function to select the value\n    # from pg_settings in the config_backend CTE.\n    CREATE PROPERTY shared_buffers -> cfg::memory {\n        CREATE ANNOTATION cfg::system := 'true';\n        CREATE ANNOTATION cfg::backend_setting := '\"shared_buffers\"';\n        CREATE ANNOTATION cfg::requires_restart := 'true';\n        CREATE ANNOTATION std::description :=\n            'The amount of memory used for shared memory buffers.';\n    };\n\n    CREATE PROPERTY query_work_mem -> cfg::memory {\n        CREATE ANNOTATION cfg::system := 'true';\n        CREATE ANNOTATION cfg::backend_setting := '\"work_mem\"';\n        CREATE ANNOTATION std::description :=\n            'The amount of memory used by internal query operations such as \\\n            sorting.';\n    };\n\n    CREATE PROPERTY maintenance_work_mem -> cfg::memory {\n        CREATE ANNOTATION cfg::system := 'true';\n        CREATE ANNOTATION cfg::backend_setting := '\"maintenance_work_mem\"';\n        CREATE ANNOTATION std::description :=\n            'The amount of memory used by operations such as \\\n            CREATE INDEX.';\n    };\n\n    CREATE PROPERTY effective_cache_size -> cfg::memory {\n        CREATE ANNOTATION cfg::system := 'true';\n        CREATE ANNOTATION cfg::backend_setting := '\"effective_cache_size\"';\n        CREATE ANNOTATION std::description :=\n            'An estimate of the effective size of the disk cache available \\\n            to a single query.';\n    };\n\n    CREATE PROPERTY effective_io_concurrency -> std::int64 {\n        CREATE ANNOTATION cfg::system := 'true';\n        CREATE ANNOTATION cfg::backend_setting := '\"effective_io_concurrency\"';\n        CREATE ANNOTATION std::description :=\n            'The number of concurrent disk I/O operations that can be \\\n            executed simultaneously.';\n    };\n\n    CREATE PROPERTY default_statistics_target -> std::int64 {\n        CREATE ANNOTATION cfg::system := 'true';\n        CREATE ANNOTATION cfg::backend_setting := '\"default_statistics_target\"';\n        CREATE ANNOTATION std::description :=\n            'The default data statistics target for the planner.';\n    };\n\n    CREATE PROPERTY force_database_error -> std::str {\n        SET default := 'false';\n        CREATE ANNOTATION cfg::affects_compilation := 'true';\n        CREATE ANNOTATION cfg::session_cfg_permissions := '\"*\"';\n        CREATE ANNOTATION std::description :=\n            'A hook to force all queries to produce an error.';\n    };\n\n    CREATE REQUIRED PROPERTY _pg_prepared_statement_cache_size -> std::int16 {\n        CREATE ANNOTATION cfg::system := 'true';\n        CREATE ANNOTATION std::description :=\n            'The maximum number of prepared statements each backend \\\n            connection could hold at the same time.';\n        CREATE CONSTRAINT std::min_value(1);\n        SET default := 100;\n    };\n\n    CREATE PROPERTY track_query_stats -> cfg::QueryStatsOption {\n        CREATE ANNOTATION cfg::backend_setting := '\"edb_stat_statements.track\"';\n        CREATE ANNOTATION std::description :=\n            'Select what queries are tracked in sys::QueryStats';\n    };\n};\n\n\nCREATE TYPE cfg::Config EXTENDING cfg::AbstractConfig;\nCREATE TYPE cfg::InstanceConfig EXTENDING cfg::AbstractConfig;\nCREATE TYPE cfg::DatabaseConfig EXTENDING cfg::AbstractConfig;\nCREATE ALIAS cfg::BranchConfig := cfg::DatabaseConfig;\n\n\nCREATE FUNCTION\ncfg::get_config_json(\n    NAMED ONLY sources: OPTIONAL array<std::str> = {},\n    NAMED ONLY max_source: OPTIONAL std::str = {}\n) -> std::json\n{\n    USING SQL $$\n    SELECT\n        coalesce(\n            jsonb_object_agg(\n                cfg.name,\n                -- Redact config values from extension configs, since\n                -- they might contain secrets, and it isn't worth the\n                -- trouble right now to care about which ones actually do.\n                (CASE WHEN\n                     cfg.name LIKE '%::%'\n                     AND cfg.value != 'null'::jsonb\n                 THEN\n                     jsonb_set(to_jsonb(cfg), '{value}',\n                               '{\"redacted\": true}'::jsonb)\n                 ELSE\n                     to_jsonb(cfg)\n                 END)\n            ),\n            '{}'::jsonb\n        )\n    FROM\n        edgedb_VER._read_sys_config(\n            sources::edgedb._sys_config_source_t[],\n            max_source::edgedb._sys_config_source_t\n        ) AS cfg\n    $$;\n};\n\nCREATE FUNCTION\ncfg::_quote(text: std::str) -> std::str\n{\n    SET volatility := 'Immutable';\n    SET internal := true;\n    USING SQL $$\n        SELECT replace(quote_literal(text), '''''', '\\\\''')\n    $$\n};\n\nCREATE CAST FROM std::int64 TO cfg::memory {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM cfg::memory TO std::int64 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::str TO cfg::memory {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'edgedb.str_to_cfg_memory';\n};\n\n\nCREATE CAST FROM cfg::memory TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'edgedb.cfg_memory_to_str';\n};\n\n\nCREATE CAST FROM std::json TO cfg::memory {\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT edgedb_VER.str_to_cfg_memory(\n            edgedb_VER.jsonb_extract_scalar(val, 'string', detail => detail)\n        )\n    $$;\n};\n\n\nCREATE CAST FROM cfg::memory TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT to_jsonb(edgedb_VER.cfg_memory_to_str(val))\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: cfg::memory, r: cfg::memory) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=(int8,int8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL cfg::memory, r: OPTIONAL cfg::memory) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: cfg::memory, r: cfg::memory) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>(int8,int8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL cfg::memory, r: OPTIONAL cfg::memory) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: cfg::memory, r: cfg::memory) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>(int8,int8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: cfg::memory, r: cfg::memory) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=(int8,int8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: cfg::memory, r: cfg::memory) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<(int8,int8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: cfg::memory, r: cfg::memory) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=(int8,int8)';\n};\n"
  },
  {
    "path": "edb/lib/enc.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright EdgeDB Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nCREATE MODULE std::enc;\n\n\nCREATE SCALAR TYPE\nstd::enc::Base64Alphabet EXTENDING enum<standard, urlsafe>;\n\n\nCREATE FUNCTION\nstd::enc::base64_encode(\n    data: std::bytes,\n    NAMED ONLY alphabet: std::enc::Base64Alphabet =\n        std::enc::Base64Alphabet.standard,\n    NAMED ONLY padding: std::bool = true,\n) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Encode given data as a base64 string';\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT\n            CASE\n            WHEN \"alphabet\" = 'standard' AND \"padding\" THEN\n                pg_catalog.translate(\n                    pg_catalog.encode(\"data\", 'base64'),\n                    E'\\n',\n                    ''\n                )\n            WHEN \"alphabet\" = 'standard' AND NOT \"padding\" THEN\n                pg_catalog.translate(\n                    pg_catalog.rtrim(\n                        pg_catalog.encode(\"data\", 'base64'),\n                        '='\n                    ),\n                    E'\\n',\n                    ''\n                )\n            WHEN \"alphabet\" = 'urlsafe' AND \"padding\" THEN\n                pg_catalog.translate(\n                    pg_catalog.encode(\"data\", 'base64'),\n                    E'+/\\n',\n                    '-_'\n                )\n            WHEN \"alphabet\" = 'urlsafe' AND NOT \"padding\" THEN\n                pg_catalog.translate(\n                    pg_catalog.rtrim(\n                        pg_catalog.encode(\"data\", 'base64'),\n                        '='\n                    ),\n                    E'+/\\n',\n                    '-_'\n                )\n            ELSE\n                edgedb_VER.raise(\n                    NULL::text,\n                    'invalid_parameter_value',\n                    msg => (\n                        'invalid alphabet for std::enc::base64_encode: '\n                        || pg_catalog.quote_literal(\"alphabet\")\n                    ),\n                    detail => (\n                        '{\"hint\":\"Supported alphabets: standard, urlsafe.\"}'\n                    )\n                )\n            END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::enc::base64_decode(\n    data: std::str,\n    NAMED ONLY alphabet: std::enc::Base64Alphabet =\n        std::enc::Base64Alphabet.standard,\n    NAMED ONLY padding: std::bool = true,\n) -> std::bytes\n{\n    CREATE ANNOTATION std::description :=\n        'Decode the byte64-encoded byte string and return decoded bytes.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT\n            CASE\n            WHEN \"alphabet\" = 'standard' AND \"padding\" THEN\n                pg_catalog.decode(\"data\", 'base64')\n            WHEN \"alphabet\" = 'standard' AND NOT \"padding\" THEN\n                pg_catalog.decode(\n                    edgedb_VER.pad_base64_string(\"data\"),\n                    'base64'\n                )\n            WHEN \"alphabet\" = 'urlsafe' AND \"padding\" THEN\n                pg_catalog.decode(\n                    pg_catalog.translate(\"data\", '-_', '+/'),\n                    'base64'\n                )\n            WHEN \"alphabet\" = 'urlsafe' AND NOT \"padding\" THEN\n                pg_catalog.decode(\n                    edgedb_VER.pad_base64_string(\n                        pg_catalog.translate(\"data\", '-_', '+/')\n                    ),\n                    'base64'\n                )\n            ELSE\n                edgedb_VER.raise(\n                    NULL::bytea,\n                    'invalid_parameter_value',\n                    msg => (\n                        'invalid alphabet for std::enc::base64_decode: '\n                        || pg_catalog.quote_literal(\"alphabet\")\n                    ),\n                    detail => (\n                        '{\"hint\":\"Supported alphabets: standard, urlsafe.\"}'\n                    )\n                )\n            END\n    $$;\n};\n"
  },
  {
    "path": "edb/lib/ext/ai.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nCREATE EXTENSION PACKAGE ai VERSION '1.0' {\n    set ext_module := \"ext::ai\";\n    set dependencies := [\"pgvector>=0.7\"];\n\n    create module ext::ai;\n\n    create module ext::ai::perm;\n    create permission ext::ai::perm::provider_call;\n    create permission ext::ai::perm::chat_prompt_read;\n    create permission ext::ai::perm::chat_prompt_write;\n\n    create scalar type ext::ai::ProviderAPIStyle\n        extending enum<OpenAI, Anthropic, Ollama>;\n\n    create abstract type ext::ai::ProviderConfig extending cfg::ConfigObject {\n        create required property name: std::str {\n            set readonly := true;\n            create constraint exclusive;\n            create annotation std::description :=\n                \"Unique provider name.\";\n        };\n\n        create required property display_name: std::str {\n            set readonly := true;\n            create annotation std::description :=\n                \"Human-friendly provider name.\";\n        };\n\n        create required property api_url: std::str {\n            set readonly := true;\n            create annotation std::description := \"Provider API URL.\";\n        };\n\n        create property client_id: std::str {\n            set readonly := true;\n            create annotation std::description :=\n                \"ID for client provided by model API vendor.\";\n        };\n\n        create required property secret: std::str {\n            set readonly := true;\n            set secret := true;\n            create annotation std::description :=\n                \"Secret provided by model API vendor.\";\n        };\n\n        create required property api_style: ext::ai::ProviderAPIStyle {\n            create annotation std::description :=\n                \"The API style exposed by this provider.\";\n        };\n    };\n\n    create type ext::ai::CustomProviderConfig extending ext::ai::ProviderConfig {\n        alter property display_name {\n            set default := 'Custom';\n        };\n\n        alter property api_style {\n            set default := ext::ai::ProviderAPIStyle.OpenAI;\n        };\n    };\n\n    create type ext::ai::OpenAIProviderConfig extending ext::ai::ProviderConfig {\n        alter property name {\n            set protected := true;\n            set default := 'builtin::openai';\n        };\n\n        alter property display_name {\n            set protected := true;\n            set default := 'OpenAI';\n        };\n\n        alter property api_url {\n            set default := 'https://api.openai.com/v1'\n        };\n\n        alter property api_style {\n            set protected := true;\n            set default := ext::ai::ProviderAPIStyle.OpenAI;\n        };\n    };\n\n    create type ext::ai::MistralProviderConfig extending ext::ai::ProviderConfig {\n        alter property name {\n            set protected := true;\n            set default := 'builtin::mistral';\n        };\n\n        alter property display_name {\n            set protected := true;\n            set default := 'Mistral';\n        };\n\n        alter property api_url {\n            set default := 'https://api.mistral.ai/v1'\n        };\n\n        alter property api_style {\n            set protected := true;\n            set default := ext::ai::ProviderAPIStyle.OpenAI;\n        };\n    };\n\n    create type ext::ai::AnthropicProviderConfig extending ext::ai::ProviderConfig {\n        alter property name {\n            set protected := true;\n            set default := 'builtin::anthropic';\n        };\n\n        alter property display_name {\n            set protected := true;\n            set default := 'Anthropic';\n        };\n\n        alter property api_url {\n            set default := 'https://api.anthropic.com/v1'\n        };\n\n        alter property api_style {\n            set protected := true;\n            set default := ext::ai::ProviderAPIStyle.Anthropic;\n        };\n    };\n\n    create type ext::ai::OllamaProviderConfig extending ext::ai::ProviderConfig {\n        alter property name {\n            set protected := true;\n            set default := 'builtin::ollama';\n        };\n\n        alter property display_name {\n            set protected := true;\n            set default := 'Ollama';\n        };\n\n        alter property api_url {\n            set default := 'http://localhost:11434/api'\n        };\n\n        alter property secret {\n            set default := ''\n        };\n\n        alter property api_style {\n            set protected := true;\n            set default := ext::ai::ProviderAPIStyle.Ollama;\n        };\n    };\n\n    create type ext::ai::Config extending cfg::ExtensionConfig {\n        create required property indexer_naptime: std::duration {\n            set default := <std::duration>'10s';\n            create annotation std::description := '\n                Specifies the minimum delay between runs of the\n                deferred ext::ai::index indexer on any given branch.\n            ';\n        };\n\n        create multi link providers: ext::ai::ProviderConfig {\n            create annotation std::description :=\n                \"AI model provider configurations.\";\n        };\n    };\n\n    create abstract inheritable annotation\n        ext::ai::model_name;\n    create abstract inheritable annotation\n        ext::ai::model_provider;\n\n    create abstract type ext::ai::Model extending std::BaseObject {\n        create annotation ext::ai::model_name := \"<must override>\";\n        create annotation ext::ai::model_provider := \"<must override>\";\n    };\n\n    create abstract inheritable annotation\n        ext::ai::embedding_model_max_input_tokens;\n\n    create abstract inheritable annotation\n        ext::ai::embedding_model_max_batch_tokens;\n\n    create abstract inheritable annotation\n        ext::ai::embedding_model_max_batch_size;\n\n    create abstract inheritable annotation\n        ext::ai::embedding_model_max_output_dimensions;\n\n    create abstract inheritable annotation\n        ext::ai::embedding_model_supports_shortening;\n\n    create abstract type ext::ai::EmbeddingModel\n        extending ext::ai::Model\n    {\n        create annotation\n            ext::ai::embedding_model_max_input_tokens := \"<must override>\";\n        # for now, use the openai batch limit as the default.\n        create annotation\n            ext::ai::embedding_model_max_batch_tokens := \"8191\";\n        create annotation\n            ext::ai::embedding_model_max_batch_size := \"<optional>\";\n        create annotation\n            ext::ai::embedding_model_max_output_dimensions := \"<must override>\";\n        create annotation\n            ext::ai::embedding_model_supports_shortening := \"false\";\n    };\n\n    create abstract inheritable annotation\n        ext::ai::text_gen_model_context_window;\n\n    create abstract type ext::ai::TextGenerationModel\n        extending ext::ai::Model\n    {\n        create annotation\n            ext::ai::text_gen_model_context_window := \"<must override>\";\n    };\n\n    # OpenAI models\n    create abstract type ext::ai::OpenAITextEmbedding3SmallModel\n        extending ext::ai::EmbeddingModel\n    {\n        alter annotation\n            ext::ai::model_name := \"text-embedding-3-small\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::openai\";\n        alter annotation\n            ext::ai::embedding_model_max_input_tokens := \"8191\";\n        alter annotation\n            ext::ai::embedding_model_max_batch_tokens := \"8191\";\n        alter annotation\n            ext::ai::embedding_model_max_output_dimensions := \"1536\";\n        alter annotation\n            ext::ai::embedding_model_supports_shortening := \"true\";\n    };\n\n    create abstract type ext::ai::OpenAITextEmbedding3LargeModel\n        extending ext::ai::EmbeddingModel\n    {\n        alter annotation\n            ext::ai::model_name := \"text-embedding-3-large\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::openai\";\n        alter annotation\n            ext::ai::embedding_model_max_input_tokens := \"8191\";\n        alter annotation\n            ext::ai::embedding_model_max_batch_tokens := \"8191\";\n        # Note: ext::pgvector is currently limited to 2000 dimensions,\n        # so returned embeddings will be automatically truncated if\n        # pgvector is used as the index implementation.\n        alter annotation\n            ext::ai::embedding_model_max_output_dimensions := \"3072\";\n        alter annotation\n            ext::ai::embedding_model_supports_shortening := \"true\";\n    };\n\n    create abstract type ext::ai::OpenAITextEmbeddingAda002Model\n        extending ext::ai::EmbeddingModel\n    {\n        alter annotation\n            ext::ai::model_name := \"text-embedding-ada-002\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::openai\";\n        alter annotation\n            ext::ai::embedding_model_max_input_tokens := \"8191\";\n        alter annotation\n            ext::ai::embedding_model_max_batch_tokens := \"8191\";\n        alter annotation\n            ext::ai::embedding_model_max_output_dimensions := \"1536\";\n    };\n\n    create abstract type ext::ai::OpenAIGPT_3_5_TurboModel\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"gpt-3.5-turbo\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::openai\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"16385\";\n    };\n\n    create abstract type ext::ai::OpenAIGPT_4_TurboPreviewModel\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"gpt-4-turbo-preview\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::openai\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"128000\";\n    };\n\n    create abstract type ext::ai::OpenAIGPT_4_TurboModel\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"gpt-4-turbo\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::openai\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"128000\";\n    };\n\n    create abstract type ext::ai::OpenAIGPT_4o_Model\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"gpt-4o\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::openai\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"128000\";\n    };\n\n    create abstract type ext::ai::OpenAIGPT_4o_MiniModel\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"gpt-4o-mini\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::openai\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"128000\";\n    };\n\n    create abstract type ext::ai::OpenAIGPT_4_Model\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"gpt-4\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::openai\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"128000\";\n    };\n\n    create abstract type ext::ai::OpenAI_O1_PreviewModel\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"o1-preview\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::openai\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"128000\";\n    };\n\n    create abstract type ext::ai::OpenAI_O1_MiniModel\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"o1-mini\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::openai\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"128000\";\n    };\n\n    # Mistral models\n    create abstract type ext::ai::MistralEmbedModel\n        extending ext::ai::EmbeddingModel\n    {\n        alter annotation\n            ext::ai::model_name := \"mistral-embed\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::mistral\";\n        alter annotation\n            ext::ai::embedding_model_max_input_tokens := \"8192\";\n        alter annotation\n            ext::ai::embedding_model_max_batch_tokens := \"16384\";\n        alter annotation\n            ext::ai::embedding_model_max_output_dimensions := \"1024\";\n    };\n\n    create abstract type ext::ai::MistralSmallModel\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"mistral-small-latest\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::mistral\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"32000\";\n    };\n\n    # Mistral legacy model\n    create abstract type ext::ai::MistralMediumModel\n        extending ext::ai::TextGenerationModel\n    {\n        create annotation std::deprecated :=\n        \"This model is noted as a legacy model in the Mistral docs.\";\n        alter annotation\n            ext::ai::model_name := \"mistral-medium-latest\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::mistral\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"32000\";\n    };\n\n    create abstract type ext::ai::MistralLargeModel\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"mistral-large-latest\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::mistral\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"128000\";\n    };\n\n    create abstract type ext::ai::PixtralLargeModel\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"pixtral-large-latest\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::mistral\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"128000\";\n    };\n\n    create abstract type ext::ai::Ministral_3B_Model\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"ministral-3b-latest\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::mistral\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"128000\";\n    };\n\n    create abstract type ext::ai::Ministral_8B_Model\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"ministral-8b-latest\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::mistral\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"128000\";\n    };\n\n    create abstract type ext::ai::CodestralModel\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"codestral-latest\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::mistral\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"32000\";\n    };\n\n    # Mistral free models\n    create abstract type ext::ai::PixtralModel\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"pixtral-12b-2409\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::mistral\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"128000\";\n    };\n\n    create abstract type ext::ai::MistralNemo\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"open-mistral-nemo\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::mistral\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"128000\";\n    };\n\n    create abstract type ext::ai::CodestralMamba\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"open-codestral-mamba\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::mistral\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"256000\";\n    };\n\n    # Anthropic models\n    # Anthropic most intelligent model\n    create abstract type ext::ai::AnthropicClaude_3_5_SonnetModel\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"claude-3-5-sonnet-latest\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::anthropic\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"200000\";\n    };\n\n    # Anthropic fastest model\n    create abstract type ext::ai::AnthropicClaude_3_5_HaikuModel\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"claude-3-5-haiku-latest\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::anthropic\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"200000\";\n    };\n\n    create abstract type ext::ai::AnthropicClaude3HaikuModel\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"claude-3-haiku-20240307\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::anthropic\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"200000\";\n    };\n\n    create abstract type ext::ai::AnthropicClaude3SonnetModel\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"claude-3-sonnet-20240229\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::anthropic\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"200000\";\n    };\n\n    create abstract type ext::ai::AnthropicClaude3OpusModel\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"claude-3-opus-latest\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::anthropic\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"200000\";\n    };\n\n    # Ollama embedding models\n    create abstract type ext::ai::OllamaLlama_3_2_Model\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"llama3.2\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::ollama\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"131072\";\n    };\n\n    create abstract type ext::ai::OllamaLlama_3_3_Model\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"llama3.3\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::ollama\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"131072\";\n    };\n\n    create abstract type ext::ai::OllamaNomicEmbedTextModel\n        extending ext::ai::EmbeddingModel\n    {\n        alter annotation\n            ext::ai::model_name := \"nomic-embed-text\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::ollama\";\n        alter annotation\n            ext::ai::embedding_model_max_input_tokens := \"2048\";\n        alter annotation\n            ext::ai::embedding_model_max_batch_tokens := \"2048\";\n        alter annotation\n            ext::ai::embedding_model_max_output_dimensions := \"768\";\n    };\n\n    create abstract type ext::ai::OllamaBgeM3Model\n        extending ext::ai::EmbeddingModel\n    {\n        alter annotation\n            ext::ai::model_name := \"bge-m3\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::ollama\";\n        alter annotation\n            ext::ai::embedding_model_max_input_tokens := \"8192\";\n        alter annotation\n            ext::ai::embedding_model_max_batch_tokens := \"8192\";\n        alter annotation\n            ext::ai::embedding_model_max_output_dimensions := \"1024\";\n    };\n\n    create abstract type ext::ai::OllamaSnowflakeArcticEmbed2Model\n        extending ext::ai::EmbeddingModel\n    {\n        alter annotation\n            ext::ai::model_name := \"snowflake-arctic-embed2\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::ollama\";\n        alter annotation\n            ext::ai::embedding_model_max_input_tokens := \"8192\";\n        alter annotation\n            ext::ai::embedding_model_max_batch_tokens := \"8192\";\n        alter annotation\n            ext::ai::embedding_model_max_output_dimensions := \"1024\";\n    };\n\n    create scalar type ext::ai::DistanceFunction\n        extending enum<Cosine, InnerProduct, L2>;\n\n    create scalar type ext::ai::IndexType\n        extending enum<HNSW>;\n\n    create abstract inheritable annotation\n        ext::ai::embedding_dimensions;\n\n    create abstract index ext::ai::index (\n        named only embedding_model: str,\n        named only dimensions: optional int64 = {},\n        named only distance_function: ext::ai::DistanceFunction\n            = ext::ai::DistanceFunction.Cosine,\n        named only index_type: ext::ai::IndexType\n            = ext::ai::IndexType.HNSW,\n        named only index_parameters: tuple<m: int64, ef_construction: int64>\n            = (m := 32, ef_construction := 100),\n        named only truncate_to_max: bool = False,\n    ) {\n        create annotation std::description :=\n            \"Semantic similarity index.\";\n        create annotation ext::ai::embedding_dimensions := \"\";\n        set deferrability := 'Required';\n    };\n\n    create function ext::ai::to_context(\n        object: anyobject,\n    ) -> std::str\n    {\n        create annotation std::description :=\n            \"Evaluate the expression of an ai::index defined on the passed \"\n            ++ \"object type and return it.\";\n        set volatility := 'Stable';\n        using sql expression;\n    };\n\n    create function ext::ai::search(\n        object: anyobject,\n        query: array<std::float32>,\n    ) -> optional tuple<object: anyobject, distance: float64>\n    {\n        create annotation std::description := '\n            Search an object using its ext::ai::index index.\n            Returns objects that match the specified semantic query and the\n            similarity score.\n        ';\n        set volatility := 'Stable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        using sql expression;\n    };\n\n    create function ext::ai::search(\n        object: anyobject,\n        query: str,\n    ) -> optional tuple<object: anyobject, distance: float64>\n    {\n        create annotation std::description := '\n            Search an object using its ext::ai::index index.\n            Gets an embedding for the query from the ai provider then\n            returns objects that match the specified semantic query and the\n            similarity score.\n        ';\n        set volatility := 'Volatile';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        set server_param_conversions := '{\"query\": [\"ai_text_embedding\", \"object\"]}';\n        set required_permissions := { ext::ai::perm::provider_call };\n        using sql expression;\n    };\n\n    create scalar type ext::ai::ChatParticipantRole\n        extending enum<System, User, Assistant, Tool>;\n\n    create type ext::ai::ChatPromptMessage extending std::BaseObject {\n        create required property participant_role:\n            ext::ai::ChatParticipantRole\n        {\n            create annotation std::description :=\n                'The role of the messages author.'\n        };\n\n        create property participant_name: str {\n            create annotation std::description :=\n                'Optional name for the participant.'\n        };\n\n        create required property content: str {\n            create annotation std::description :=\n                'Prompt message contenxt.'\n        };\n\n        create access policy ap_read allow select using (\n            global ext::ai::perm::chat_prompt_read\n        );\n        create access policy ap_write allow insert, update, delete using (\n            global ext::ai::perm::chat_prompt_write\n        );\n    };\n\n    create type ext::ai::ChatPrompt extending std::BaseObject {\n        create required property name: str {\n            create constraint exclusive;\n            create annotation std::description :=\n                'Unique name for the prompt configuration';\n        };\n\n        create required multi link messages: ext::ai::ChatPromptMessage {\n            create constraint exclusive;\n            create annotation std::description :=\n                'Messages in this prompt configuration';\n        };\n\n        create access policy ap_read allow select using (\n            global ext::ai::perm::chat_prompt_read\n        );\n        create access policy ap_write allow insert, update, delete using (\n            global ext::ai::perm::chat_prompt_write\n        );\n    };\n\n    insert ext::ai::ChatPrompt {\n        name := 'builtin::rag-default',\n        messages := {\n            (insert ext::ai::ChatPromptMessage {\n                participant_role := ext::ai::ChatParticipantRole.System,\n                content := (\n                    \"You are an expert Q&A system.\\n\" ++\n                    \"Always answer questions based on the provided \\\n                     context information. Never use prior knowledge.\\n\" ++\n                    \"Follow these additional rules:\\n\\\n                     1. Never directly reference the given context in your \\\n                        answer.\\n\\\n                     2. Never include phrases like 'Based on the context, ...' \\\n                        or any similar phrases in your responses.\\n\\\n                     3. When the context does not provide information about \\\n                        the question, answer with \\\n                        'No information available.'.\\n\\\n                     Context information is below:\\n{context}\\n\\\n                     Given the context information above and not prior \\\n                     knowledge, answer the user query.\"\n                ),\n            }),\n            (insert ext::ai::ChatPromptMessage {\n                participant_role := ext::ai::ChatParticipantRole.User,\n                content := (\n                    \"Query: {query}\\n\\\n                     Answer: \"\n                ),\n            })\n        }\n    };\n\n    create index match for std::str using ext::ai::index;\n};\n"
  },
  {
    "path": "edb/lib/ext/auth.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nCREATE EXTENSION PACKAGE auth VERSION '1.0' {\n    set ext_module := \"ext::auth\";\n    set dependencies := [\"pgcrypto>=1.3\"];\n\n    create module ext::auth;\n\n    create module ext::auth::perm;\n    create permission ext::auth::perm::auth_read;\n    create permission ext::auth::perm::auth_write;\n    create permission ext::auth::perm::auth_read_user;\n\n    create abstract type ext::auth::Auditable extending std::BaseObject {\n        create required property created_at: std::datetime {\n            set default := std::datetime_current();\n            set readonly := true;\n        };\n        create required property modified_at: std::datetime {\n            create rewrite insert, update using (\n                std::datetime_current()\n            );\n        };\n\n        create access policy ap_read allow select using (\n            global ext::auth::perm::auth_read\n        );\n        create access policy ap_write allow insert, update, delete using (\n            global ext::auth::perm::auth_write\n        );\n    };\n\n    create type ext::auth::Identity extending ext::auth::Auditable {\n        create required property issuer: std::str;\n        create required property subject: std::str;\n\n        create constraint exclusive on ((.issuer, .subject));\n    };\n\n    create type ext::auth::LocalIdentity extending ext::auth::Identity {\n        alter property subject {\n            create rewrite insert using (<str>.id);\n        };\n    };\n\n    create abstract type ext::auth::Factor extending ext::auth::Auditable {\n        create required link identity: ext::auth::LocalIdentity {\n            create constraint exclusive;\n            on target delete delete source;\n        };\n    };\n\n    create type ext::auth::EmailFactor extending ext::auth::Factor {\n        create required property email: str;\n        create property verified_at: std::datetime;\n    };\n\n    create type ext::auth::EmailPasswordFactor\n        extending ext::auth::EmailFactor {\n        alter property email {\n            create constraint exclusive;\n        };\n        create required property password_hash: std::str;\n    };\n\n    create type ext::auth::MagicLinkFactor extending ext::auth::EmailFactor {\n        alter property email {\n            create constraint exclusive;\n        };\n    };\n\n    create type ext::auth::WebAuthnFactor extending ext::auth::EmailFactor {\n        create required property user_handle: std::bytes;\n        create required property credential_id: std::bytes {\n            create constraint exclusive;\n        };\n        create required property public_key: std::bytes {\n            create constraint exclusive;\n        };\n\n        create trigger email_shares_user_handle after insert for each do (\n            std::assert(\n                __new__.user_handle = (\n                    select detached ext::auth::WebAuthnFactor\n                    filter .email = __new__.email\n                    and not .id = __new__.id\n                ).user_handle,\n                message := \"user_handle must be the same for a given email\"\n            )\n        );\n        create constraint exclusive on ((.email, .credential_id));\n    };\n\n    create type ext::auth::WebAuthnRegistrationChallenge\n        extending ext::auth::Auditable {\n        create required property challenge: std::bytes {\n            create constraint exclusive;\n        };\n        create required property email: std::str;\n        create required property user_handle: std::bytes;\n\n        create constraint exclusive on ((.user_handle, .email, .challenge));\n    };\n\n    create type ext::auth::WebAuthnAuthenticationChallenge\n        extending ext::auth::Auditable {\n        create required property challenge: std::bytes {\n            create constraint exclusive;\n        };\n        create required multi link factors: ext::auth::WebAuthnFactor {\n            create constraint exclusive;\n            on target delete delete source;\n        };\n    };\n\n    create type ext::auth::PKCEChallenge extending ext::auth::Auditable {\n        create required property challenge: std::str {\n            create constraint exclusive;\n        };\n        create property auth_token: std::str {\n            create annotation std::description :=\n                \"Identity provider's auth token.\";\n        };\n        create property refresh_token: std::str {\n            create annotation std::description :=\n                \"Identity provider's refresh token.\";\n        };\n        create property id_token: std::str {\n            create annotation std::description :=\n                \"Identity provider's OpenID Connect id_token.\";\n        };\n        create link identity: ext::auth::Identity {\n            on target delete delete source;\n        };\n    };\n\n    create type ext::auth::OneTimeCode extending ext::auth::Auditable {\n        create required property code_hash: std::bytes {\n            create constraint exclusive;\n            create annotation std::description :=\n                \"The securely hashed one-time code.\";\n        };\n        create required property expires_at: std::datetime {\n            create annotation std::description :=\n                \"The date and time when the code expires.\";\n        };\n        create index on (.expires_at);\n\n        create required link factor: ext::auth::Factor {\n            on target delete delete source;\n        };\n    };\n\n    create scalar type ext::auth::AuthenticationAttemptType extending std::enum<\n        SignIn,\n        EmailVerification,\n        PasswordReset,\n        MagicLink,\n        OneTimeCode\n    >;\n\n    create type ext::auth::AuthenticationAttempt extending ext::auth::Auditable {\n        create required link factor: ext::auth::Factor {\n            on target delete delete source;\n        };\n        create required property attempt_type: ext::auth::AuthenticationAttemptType {\n            create annotation std::description :=\n                \"The type of authentication attempt being made.\";\n        };\n        create required property successful: std::bool {\n            create annotation std::description :=\n                \"Whether this authentication attempt was successful.\";\n        };\n    };\n\n    create scalar type ext::auth::VerificationMethod extending std::enum<Link, Code>;\n\n    create abstract type ext::auth::ProviderConfig\n        extending cfg::ConfigObject {\n        create required property name: std::str {\n            set readonly := true;\n            create constraint exclusive;\n        }\n    };\n\n    create abstract type ext::auth::OAuthProviderConfig\n        extending ext::auth::ProviderConfig {\n        alter property name {\n            set protected := true;\n        };\n\n        create required property secret: std::str {\n            set readonly := true;\n            set secret := true;\n            create annotation std::description :=\n                \"Secret provided by auth provider.\";\n        };\n\n        create required property client_id: std::str {\n            set readonly := true;\n            create annotation std::description :=\n                \"ID for client provided by auth provider.\";\n        };\n\n        create required property display_name: std::str {\n            set readonly := true;\n            set protected := true;\n            create annotation std::description :=\n                \"Provider name to be displayed in login UI.\";\n        };\n\n        create property additional_scope: std::str {\n            set readonly := true;\n            create annotation std::description :=\n                \"Space-separated list of scopes to be included in the \\\n                authorize request to the OAuth provider.\";\n        };\n    };\n\n    create type ext::auth::OpenIDConnectProvider\n        extending ext::auth::OAuthProviderConfig {\n        alter property name {\n            set protected := false;\n        };\n\n        alter property display_name {\n            set protected := false;\n        };\n\n        create required property issuer_url: std::str {\n            create annotation std::description :=\n                \"The issuer URL of the provider.\";\n        };\n\n        create property logo_url: std::str {\n            create annotation std::description :=\n                \"A url to an image of the provider's logo.\";\n        };\n\n        create constraint exclusive on ((.issuer_url, .client_id));\n    };\n\n    create type ext::auth::AppleOAuthProvider\n        extending ext::auth::OAuthProviderConfig {\n        alter property name {\n            set default := 'builtin::oauth_apple';\n        };\n\n        alter property display_name {\n            set default := 'Apple';\n        };\n    };\n\n    create type ext::auth::AzureOAuthProvider\n        extending ext::auth::OAuthProviderConfig {\n        alter property name {\n            set default := 'builtin::oauth_azure';\n        };\n\n        alter property display_name {\n            set default := 'Azure';\n        };\n    };\n\n    create type ext::auth::DiscordOAuthProvider\n        extending ext::auth::OAuthProviderConfig {\n        alter property name {\n            set default := 'builtin::oauth_discord';\n        };\n\n        alter property display_name {\n            set default := 'Discord';\n        };\n\n        create required property prompt: std::str {\n            create annotation std::description :=\n                \"Controls how the authorization flow handles existing authorizations. \\\n                If a user has previously authorized your application with the \\\n                requested scopes and prompt is set to consent, it will request them \\\n                to reapprove their authorization. If set to none, it will skip the \\\n                authorization screen and redirect them back to your redirect URI \\\n                without requesting their authorization. For passthrough scopes, like \\\n                bot and webhook.incoming, authorization is always required.\";\n            set default := 'consent';\n        };\n    };\n\n    create type ext::auth::SlackOAuthProvider\n        extending ext::auth::OAuthProviderConfig {\n        alter property name {\n            set default := 'builtin::oauth_slack';\n        };\n\n        alter property display_name {\n            set default := 'Slack';\n        };\n    };\n\n    create type ext::auth::GitHubOAuthProvider\n        extending ext::auth::OAuthProviderConfig {\n        alter property name {\n            set default := 'builtin::oauth_github';\n        };\n\n        alter property display_name {\n            set default := 'GitHub';\n        };\n    };\n\n    create type ext::auth::GoogleOAuthProvider\n        extending ext::auth::OAuthProviderConfig {\n        alter property name {\n            set default := 'builtin::oauth_google';\n        };\n\n        alter property display_name {\n            set default := 'Google';\n        };\n    };\n\n    create type ext::auth::EmailPasswordProviderConfig\n        extending ext::auth::ProviderConfig {\n        alter property name {\n            set default := 'builtin::local_emailpassword';\n            set protected := true;\n        };\n\n        create required property require_verification: std::bool {\n            set default := true;\n        };\n\n        create required property verification_method: ext::auth::VerificationMethod {\n            set default := ext::auth::VerificationMethod.Link;\n        };\n    };\n\n    create type ext::auth::WebAuthnProviderConfig\n        extending ext::auth::ProviderConfig {\n        alter property name {\n            set default := 'builtin::local_webauthn';\n            set protected := true;\n        };\n\n        create required property relying_party_origin: std::str {\n            create annotation std::description :=\n                \"The full origin of the sign-in page including protocol and \\\n                port of the application. If using the built-in UI, this \\\n                should be the origin of the EdgeDB server.\";\n        };\n\n        create required property require_verification: std::bool {\n            set default := true;\n        };\n\n        create required property verification_method: ext::auth::VerificationMethod {\n            set default := ext::auth::VerificationMethod.Link;\n        };\n    };\n\n    create type ext::auth::MagicLinkProviderConfig\n        extending ext::auth::ProviderConfig {\n        alter property name {\n            set default := 'builtin::local_magic_link';\n            set protected := true;\n        };\n\n        create required property token_time_to_live: std::duration {\n            set default := <std::duration>'10 minutes';\n            create annotation std::description :=\n                \"The time after which a magic link token expires.\";\n        };\n\n        create required property verification_method: ext::auth::VerificationMethod {\n            set default := ext::auth::VerificationMethod.Link;\n        };\n\n        create required property auto_signup: std::bool {\n            set default := false;\n        };\n    };\n\n    create scalar type ext::auth::FlowType extending std::enum<PKCE, Implicit>;\n\n    create type ext::auth::UIConfig extending cfg::ConfigObject {\n        create required property redirect_to: std::str {\n            create annotation std::description :=\n                \"The url to redirect to after successful sign in.\";\n        };\n\n        create property redirect_to_on_signup: std::str {\n            create annotation std::description :=\n                \"The url to redirect to after a new user signs up. \\\n                If not set, 'redirect_to' will be used instead.\";\n        };\n\n        create required property flow_type: ext::auth::FlowType {\n            create annotation std::description :=\n                \"The flow used when requesting authentication.\";\n            set default := ext::auth::FlowType.PKCE;\n        };\n\n        create property app_name: std::str {\n            create annotation std::description :=\n                \"The name of your application to be shown on the login \\\n                screen.\";\n            create annotation std::deprecated :=\n                \"Use the app_name property in ext::auth::AuthConfig instead.\";\n        };\n\n        create property logo_url: std::str {\n            create annotation std::description :=\n                \"A url to an image of your application's logo.\";\n            create annotation std::deprecated :=\n                \"Use the logo_url property in ext::auth::AuthConfig instead.\";\n        };\n\n        create property dark_logo_url: std::str {\n            create annotation std::description :=\n                \"A url to an image of your application's logo to be used \\\n                with the dark theme.\";\n            create annotation std::deprecated :=\n                \"Use the dark_logo_url property in ext::auth::AuthConfig \\\n                instead.\";\n        };\n\n        create property brand_color: std::str {\n            create annotation std::description :=\n                \"The brand color of your application as a hex string.\";\n            create annotation std::deprecated :=\n                \"Use the brand_color property in ext::auth::AuthConfig \\\n                instead.\";\n        };\n    };\n\n    create scalar type ext::auth::WebhookEvent extending std::enum<\n        IdentityCreated,\n        IdentityAuthenticated,\n        EmailFactorCreated,\n        EmailVerified,\n        EmailVerificationRequested,\n        PasswordResetRequested,\n        MagicLinkRequested,\n        OneTimeCodeRequested,\n        OneTimeCodeVerified,\n    >;\n\n    create type ext::auth::WebhookConfig extending cfg::ConfigObject {\n        create required property url: std::str {\n            create annotation std::description :=\n                \"The url to send webhooks to.\";\n\n            create constraint exclusive;\n        };\n\n        create required multi property events: ext::auth::WebhookEvent {\n            create annotation std::description :=\n                \"The events to send webhooks for.\";\n        };\n\n        create property signing_secret_key: std::str {\n            set secret := true;\n\n            create annotation std::description :=\n                \"The secret key used to sign webhook requests.\";\n        };\n    };\n\n    create function ext::auth::webhook_signing_key_exists(\n        webhook_config: ext::auth::WebhookConfig\n    ) -> std::bool {\n        using (\n            select exists webhook_config.signing_secret_key\n        );\n        SET required_permissions := ext::auth::perm::auth_read;\n    };\n\n    create type ext::auth::AuthConfig extending cfg::ExtensionConfig {\n        create multi link providers: ext::auth::ProviderConfig {\n            create annotation std::description :=\n                \"Configuration for auth provider clients.\";\n        };\n\n        create link ui: ext::auth::UIConfig {\n            create annotation std::description :=\n                \"Configuration for builtin auth UI. If not set the builtin \\\n                UI is disabled.\";\n        };\n\n        create multi link webhooks: ext::auth::WebhookConfig {\n            create annotation std::description :=\n                \"Configuration for webhooks.\";\n        };\n\n        create property app_name: std::str {\n            create annotation std::description :=\n                \"The name of your application.\";\n        };\n\n        create property logo_url: std::str {\n            create annotation std::description :=\n                \"A url to an image of your application's logo.\";\n        };\n\n        create property dark_logo_url: std::str {\n            create annotation std::description :=\n                \"A url to an image of your application's logo to be used \\\n                with the dark theme.\";\n        };\n\n        create property brand_color: std::str {\n            create annotation std::description :=\n                \"The brand color of your application as a hex string.\";\n        };\n\n        create property auth_signing_key: std::str {\n            set secret := true;\n            create annotation std::description :=\n                \"The signing key used for auth extension. Must be at \\\n                least 32 characters long.\";\n        };\n\n        create property token_time_to_live: std::duration {\n            create annotation std::description :=\n                \"The time after which an auth token expires. A value of 0 \\\n                indicates that the token should never expire.\";\n            set default := <std::duration>'336 hours';\n        };\n\n        create multi property allowed_redirect_urls: std::str {\n            create annotation std::description :=\n                \"When redirecting the user in various flows, the URL will be \\\n                checked against this list to ensure they are going \\\n                to a trusted domain controlled by the application. URLs are \\\n                matched based on checking if the candidate redirect URL is \\\n                a match or a subdirectory of any of these allowed URLs\";\n        };\n    };\n\n    create function ext::auth::signing_key_exists() -> std::bool {\n        using (\n            select exists cfg::Config.extensions[is ext::auth::AuthConfig]\n                .auth_signing_key\n        );\n        SET required_permissions := ext::auth::perm::auth_read;\n    };\n\n    create scalar type ext::auth::JWTAlgo extending enum<RS256, HS256>;\n\n    create function ext::auth::_jwt_check_signature(\n        jwt: tuple<header: std::str, payload: std::str, signature: std::str>,\n        key: std::str,\n        algo: ext::auth::JWTAlgo = ext::auth::JWTAlgo.HS256,\n    ) -> std::json\n    {\n        set volatility := 'Stable';\n        using (\n            with\n                module ext::auth,\n                msg := jwt.header ++ \".\" ++ jwt.payload,\n                hash := (\n                    \"sha256\" if algo = JWTAlgo.RS256 or algo = JWTAlgo.HS256\n                    else <str>std::assert(\n                        false, message := \"unsupported JWT algo\")\n                ),\n            select\n                std::to_json(\n                    std::to_str(\n                        std::enc::base64_decode(\n                            jwt.payload,\n                            padding := false,\n                            alphabet := std::enc::Base64Alphabet.urlsafe,\n                        ),\n                    ),\n                )\n            order by\n                assert(\n                    std::enc::base64_encode(\n                        ext::pgcrypto::hmac(msg, key, hash),\n                        padding := false,\n                        alphabet := std::enc::Base64Alphabet.urlsafe,\n                    ) = jwt.signature,\n                    message := \"JWT signature mismatch\",\n                )\n        );\n    };\n\n    create function ext::auth::_jwt_parse(\n        token: std::str,\n    ) -> tuple<header: std::str, payload: std::str, signature: std::str>\n    {\n        set volatility := 'Stable';\n        using (\n            for parts in std::str_split(token, \".\")\n            select\n                (\n                    header := parts[0],\n                    payload := parts[1],\n                    signature := parts[2],\n                )\n            order by\n                assert(len(parts) = 3, message := \"JWT is malformed\")\n        );\n    };\n\n    create function ext::auth::_jwt_verify(\n        token: std::str,\n        key: std::str,\n        algo: ext::auth::JWTAlgo = ext::auth::JWTAlgo.HS256,\n    ) -> std::json\n    {\n        set volatility := 'Stable';\n        using (\n            for jwt in (\n                ext::auth::_jwt_check_signature(\n                    ext::auth::_jwt_parse(token),\n                    key,\n                    algo,\n                )\n            )\n            with\n                validity_range := std::range(\n                    std::to_datetime(<float64>json_get(jwt, \"nbf\")),\n                    std::to_datetime(<float64>json_get(jwt, \"exp\")),\n                ),\n            select\n                jwt\n            order by\n                assert(\n                    std::contains(\n                        validity_range,\n                        std::datetime_of_transaction(),\n                    ),\n                    message := \"JWT is expired or is not yet valid\",\n                )\n        );\n    };\n\n    create global ext::auth::client_token: std::str;\n\n    create single global ext::auth::_client_token_id := (\n        for conf_key in (\n            (\n                select cfg::Config.extensions[is ext::auth::AuthConfig]\n                limit 1\n            ).auth_signing_key\n        )\n        for jwt_claims in (\n            ext::auth::_jwt_verify(\n                global ext::auth::client_token,\n                conf_key,\n            )\n        )\n        select <uuid>json_get(jwt_claims, \"sub\")\n    );\n    alter type ext::auth::Identity {\n        create access policy read_current allow select using (\n            not global ext::auth::perm::auth_read\n            and global ext::auth::perm::auth_read_user\n            and .id ?= global ext::auth::_client_token_id\n        );\n    };\n\n    create single global ext::auth::ClientTokenIdentity := (\n        select\n            ext::auth::Identity\n        filter\n            .id = global ext::auth::_client_token_id\n    );\n};\n"
  },
  {
    "path": "edb/lib/ext/edgeqlhttp.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nCREATE EXTENSION PACKAGE edgeql_http VERSION '1.0';\n"
  },
  {
    "path": "edb/lib/ext/graphql.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nCREATE EXTENSION PACKAGE graphql VERSION '1.0';\n"
  },
  {
    "path": "edb/lib/ext/notebook.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nCREATE EXTENSION PACKAGE notebook VERSION '1.0' {\n    SET internal := true;\n};\n"
  },
  {
    "path": "edb/lib/ext/pg_trgm.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ncreate extension package pg_trgm version '1.6' {\n    set ext_module := \"ext::pg_trgm\";\n    set sql_extensions := [\"pg_trgm >=1.6\"];\n\n    create module ext::pg_trgm;\n\n    create type ext::pg_trgm::Config extending cfg::ExtensionConfig {\n        create required property similarity_threshold: std::float32 {\n            create annotation cfg::backend_setting :=\n                '\"pg_trgm.similarity_threshold\"';\n            create annotation cfg::session_cfg_permissions := '\"*\"';\n            create annotation std::description :=\n                \"The current similarity threshold that is used by the \"\n                ++ \"pg_trgm::similar() function, the pg_trgm::gin and \"\n                ++ \"the pg_trgm::gist indexes.  The threshold must be \"\n                ++ \"between 0 and 1 (default is 0.3).\";\n            set default := 0.3;\n            create constraint std::min_value(0.0);\n            create constraint std::max_value(1.0);\n        };\n        create required property word_similarity_threshold: std::float32 {\n            create annotation cfg::backend_setting :=\n                '\"pg_trgm.word_similarity_threshold\"';\n            create annotation cfg::session_cfg_permissions := '\"*\"';\n            create annotation std::description :=\n                \"The current word similarity threshold that is used by the \"\n                ++ \"pg_trgrm::word_similar() function. The threshold must be \"\n                ++ \"between 0 and 1 (default is 0.6).\";\n            set default := 0.6;\n            create constraint std::min_value(0.0);\n            create constraint std::max_value(1.0);\n        };\n        create required property strict_word_similarity_threshold: std::float32\n        {\n            create annotation cfg::backend_setting :=\n                '\"pg_trgm.strict_word_similarity_threshold\"';\n            create annotation cfg::session_cfg_permissions := '\"*\"';\n            create annotation std::description :=\n                \"The current strict word similarity threshold that is used by \"\n                ++ \"the pg_trgrm::strict_word_similar() function. The \"\n                ++ \"threshold must be between 0 and 1 (default is 0.5).\";\n            set default := 0.5;\n            create constraint std::min_value(0.0);\n            create constraint std::max_value(1.0);\n        };\n    };\n\n    create function ext::pg_trgm::similarity(\n        a: std::str,\n        b: std::str,\n    ) -> std::float32 {\n        set volatility := 'Immutable';\n        using sql 'select 1.0::real - (a <-> b)';\n    };\n\n    create function ext::pg_trgm::similar(\n        a: std::str,\n        b: std::str,\n    ) -> std::bool {\n        set volatility := 'Stable';  # Depends on config.\n        using sql 'select a % b';\n    };\n\n    create function ext::pg_trgm::similarity_dist(\n        a: std::str,\n        b: std::str,\n    ) -> std::float32 {\n        set volatility := 'Immutable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        using sql 'select a <-> b';\n    };\n\n    create function ext::pg_trgm::word_similarity(\n        a: std::str,\n        b: std::str,\n    ) -> std::float32 {\n        set volatility := 'Immutable';\n        using sql 'select 1.0::real - (a <<-> b)';\n    };\n\n    create function ext::pg_trgm::word_similar(\n        a: std::str,\n        b: std::str,\n    ) -> std::bool {\n        set volatility := 'Stable';  # Depends on config.\n        using sql 'select a <% b';\n    };\n\n    create function ext::pg_trgm::word_similarity_dist(\n        a: std::str,\n        b: std::str,\n    ) -> std::float32 {\n        set volatility := 'Immutable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        using sql 'select a <<-> b';\n    };\n\n    create function ext::pg_trgm::strict_word_similarity(\n        a: std::str,\n        b: std::str,\n    ) -> std::float32 {\n        set volatility := 'Immutable';\n        using sql 'select 1.0::real - (a <<<-> b)';\n    };\n\n    create function ext::pg_trgm::strict_word_similar(\n        a: std::str,\n        b: std::str,\n    ) -> std::bool {\n        set volatility := 'Stable';  # Depends on config.\n        using sql 'select a <<% b';\n    };\n\n    create function ext::pg_trgm::strict_word_similarity_dist(\n        a: std::str,\n        b: std::str,\n    ) -> std::float32 {\n        set volatility := 'Immutable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        using sql 'select a <<<-> b';\n    };\n\n    create abstract index ext::pg_trgm::gin() {\n        create annotation std::description :=\n            'pg_trgm GIN index.';\n        set code :=\n            'GIN (__col__ gin_trgm_ops)';\n    };\n\n    create abstract index ext::pg_trgm::gist(\n        named only siglen: int64 = 12\n    ) {\n        create annotation std::description :=\n            'pg_trgm GIST index.';\n        set code :=\n            'GIST (__col__ gist_trgm_ops(siglen = __kw_siglen__))';\n    };\n\n    create index match for std::str using ext::pg_trgm::gin;\n    create index match for std::str using ext::pg_trgm::gist;\n};\n"
  },
  {
    "path": "edb/lib/ext/pg_unaccent.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ncreate extension package pg_unaccent version '1.1' {\n    set ext_module := \"ext::pg_unaccent\";\n    set sql_extensions := [\"unaccent >=1.1\"];\n\n    create module ext::pg_unaccent;\n\n    create function ext::pg_unaccent::unaccent(\n        text: std::str,\n    ) -> std::str {\n        set volatility := 'Immutable';\n        using sql 'select edgedb.unaccent(text)';\n    };\n};\n"
  },
  {
    "path": "edb/lib/ext/pgcrypto.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ncreate extension package pgcrypto version '1.3' {\n    set ext_module := \"ext::pgcrypto\";\n    set sql_extensions := [\"pgcrypto >=1.3\"];\n\n    create module ext::pgcrypto;\n\n    create function ext::pgcrypto::digest(\n        data: std::str,\n        type: std::str,\n    ) -> std::bytes {\n        set volatility := 'Immutable';\n        using sql function 'edgedb.digest';\n    };\n\n    create function ext::pgcrypto::digest(\n        data: std::bytes,\n        type: std::str,\n    ) -> std::bytes {\n        set volatility := 'Immutable';\n        using sql function 'edgedb.digest';\n    };\n\n    create function ext::pgcrypto::hmac(\n        data: std::str,\n        key: std::str,\n        type: std::str,\n    ) -> std::bytes {\n        set volatility := 'Immutable';\n        using sql function 'edgedb.hmac';\n    };\n\n    create function ext::pgcrypto::hmac(\n        data: std::bytes,\n        key: std::bytes,\n        type: std::str,\n    ) -> std::bytes {\n        set volatility := 'Immutable';\n        using sql function 'edgedb.hmac';\n    };\n\n    create function ext::pgcrypto::gen_salt(\n    ) -> std::str {\n        set volatility := 'Volatile';\n        using sql \"SELECT edgedb.gen_salt('bf')\";\n    };\n\n    create function ext::pgcrypto::gen_salt(\n        type: std::str,\n    ) -> std::str {\n        set volatility := 'Volatile';\n        using sql 'SELECT edgedb.gen_salt(\"type\")';\n    };\n\n    create function ext::pgcrypto::gen_salt(\n        type: std::str,\n        iter_count: std::int64,\n    ) -> std::str {\n        set volatility := 'Volatile';\n        using sql 'SELECT edgedb.gen_salt(\"type\", \"iter_count\"::integer)';\n    };\n\n    create function ext::pgcrypto::crypt(\n        password: std::str,\n        salt: std::str,\n    ) -> std::str {\n        set volatility := 'Immutable';\n        using sql function 'edgedb.crypt';\n    };\n};\n"
  },
  {
    "path": "edb/lib/ext/pgvector.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ncreate extension package pgvector version '0.7.4' {\n    set ext_module := \"ext::pgvector\";\n    set sql_extensions := [\"vector >=0.7.4,<0.9.0\"];\n\n    set sql_setup_script := $script$\n        -- Rename the vector_norm to be consistent with l2_norm\n        ALTER FUNCTION edgedb.vector_norm(edgedb.vector) RENAME TO l2_norm;\n\n        -- Add some helpers\n\n        -- about 5-6 times slower than the C cast, but retains 0-based index\n        CREATE FUNCTION sparsevec_to_text(val sparsevec) RETURNS text AS $$\n        DECLARE\n            vectxt text := val::text;\n            mid text[];\n            kv text[];\n            i int8;\n            res text := '{';\n        BEGIN\n            mid := string_to_array(substr(split_part(vectxt, '}', 1), 2), ',');\n            FOR i IN 1..cardinality(mid)\n            LOOP\n                kv := string_to_array(mid[i], ':');\n                kv[1] := (kv[1]::int8 - 1)::text;\n                res := res || kv[1] || ':' || kv[2] || ',';\n            END LOOP;\n\n            RETURN left(res, -1) || '}' || split_part(vectxt, '}', 2);\n        END;\n        $$ LANGUAGE plpgsql\n        IMMUTABLE STRICT;\n\n        -- about 10 times slower than a cast\n        CREATE FUNCTION text_to_sparsevec(val text) RETURNS sparsevec AS $$\n        DECLARE\n            mid text[];\n            kv text[];\n            i int8;\n            res text := '{';\n        BEGIN\n            IF val ~ '^\\s*{\\s*(\\d+\\s*:.+?,\\s*)*\\d+\\s*:.+}\\s*/\\s*\\d+\\s*$'\n            THEN\n                mid := string_to_array(split_part(split_part(val, '}', 1), '{', 2), ',');\n                FOR i IN 1..cardinality(mid)\n                LOOP\n                    kv := string_to_array(mid[i], ':');\n                    kv[1] := (trim(kv[1])::int8 + 1)::text;\n                    res := res || kv[1] || ':' || kv[2] || ',';\n                END LOOP;\n                RETURN (left(res, -1) || '}' || split_part(val, '}', 2))::sparsevec;\n            ELSE\n                RETURN val::sparsevec;\n            END IF;\n        END;\n        $$ LANGUAGE plpgsql\n        IMMUTABLE STRICT;\n\n        CREATE FUNCTION sparsevec_to_jsonb(val sparsevec) RETURNS jsonb AS $$\n        DECLARE\n            vectxt text := val::text;\n            mid text[];\n            kv text[];\n            i int8;\n            dim text := split_part(vectxt, '/', 2);\n            res text := '{';\n        BEGIN\n            mid := string_to_array(substr(split_part(vectxt, '}', 1), 2), ',');\n            FOR i IN 1..cardinality(mid)\n            LOOP\n                kv := string_to_array(mid[i], ':');\n                kv[1] := (kv[1]::int8 - 1)::text;\n                res := res || '\"' || kv[1] || '\":' || kv[2] || ',';\n            END LOOP;\n\n            RETURN (res || '\"dim\":' || dim || '}')::jsonb;\n        END;\n        $$ LANGUAGE plpgsql\n        IMMUTABLE STRICT;\n\n        CREATE FUNCTION jsonb_to_sparsevec(val jsonb) RETURNS sparsevec AS $$\n        DECLARE\n            mid text[];\n            kv text[];\n            r record;\n            i int8;\n            dim text := NULL;\n            res text := '{';\n            msg text;\n        BEGIN\n            IF jsonb_typeof(val) = 'object'\n            THEN\n                msg := 'missing \"dim\"';\n                FOR r IN SELECT * FROM jsonb_each(val)\n                LOOP\n                    CASE\n                        WHEN r.key = 'dim' THEN\n                            dim := r.value::text;\n                        WHEN r.key ~ $r$\\d+$r$ THEN\n                            res := res || (r.key::int8 + 1)::text || ':'\n                                       || r.value::text || ',';\n                        ELSE\n                            msg := 'unexpected key in JSON object: ' || r.key;\n                            EXIT;\n                    END CASE;\n                END LOOP;\n\n                IF dim IS NOT NULL\n                THEN\n                    RETURN (left(res, -1) || '}/' || dim)::sparsevec;\n                END IF;\n            ELSE\n                msg := 'JSON object expected, got ' || jsonb_typeof(val) || ' instead';\n            END IF;\n\n            RAISE EXCEPTION USING\n                ERRCODE = 22000,\n                MESSAGE = msg;\n        END;\n        $$ LANGUAGE plpgsql\n        IMMUTABLE STRICT;\n    $script$;\n    set sql_teardown_script := $$\n        ALTER FUNCTION edgedb.l2_norm(edgedb.vector) RENAME TO vector_norm;\n        -- remove helpers\n        DROP FUNCTION edgedb.sparsevec_to_jsonb;\n        DROP FUNCTION edgedb.jsonb_to_sparsevec;\n        DROP FUNCTION edgedb.sparsevec_to_text;\n        DROP FUNCTION edgedb.text_to_sparsevec;\n    $$;\n\n    create module ext::pgvector;\n\n    create type ext::pgvector::Config extending cfg::ExtensionConfig {\n        create required property probes: std::int64 {\n            create annotation cfg::backend_setting :=\n                '\"ivfflat.probes\"';\n            create annotation cfg::session_cfg_permissions := '\"*\"';\n            create annotation std::description :=\n                \"The number of probes (1 by default) used by IVFFlat \"\n                ++ \"index. A higher value provides better recall at the \"\n                ++ \"cost of speed, and it can be set to the number of \"\n                ++ \"lists for exact nearest neighbor search (at which point \"\n                ++ \"the planner won’t use the index)\";\n            set default := 1;\n            create constraint std::min_value(1);\n        };\n        create required property ef_search: std::int64 {\n            create annotation cfg::backend_setting :=\n                '\"hnsw.ef_search\"';\n            create annotation cfg::session_cfg_permissions := '\"*\"';\n            create annotation std::description :=\n                \"The size of the dynamic candidate list for search (40 \"\n                ++ \"by default) used by HNSW index. A higher value \"\n                ++ \"provides better recall at the  cost of speed.\";\n            set default := 40;\n            create constraint std::min_value(1);\n        };\n    };\n\n    create scalar type ext::pgvector::vector extending std::anyscalar {\n        set id := <uuid>\"9565dd88-04f5-11ee-a691-0b6ebe179825\";\n        set sql_type := \"vector\";\n        set sql_type_scheme := \"vector({__arg_0__})\";\n        set num_params := 1;\n    };\n\n    create scalar type ext::pgvector::halfvec extending std::anyscalar {\n        set id := <uuid>\"4ba84534-188e-43b4-a7ce-cea2af0f405b\";\n        set sql_type := \"halfvec\";\n        set sql_type_scheme := \"halfvec({__arg_0__})\";\n        set num_params := 1;\n    };\n\n    create scalar type ext::pgvector::sparsevec extending std::anyscalar {\n        set id := <uuid>\"003e434d-cac2-430a-b238-fb39d73447d2\";\n        set sql_type := \"sparsevec\";\n        set sql_type_scheme := \"sparsevec({__arg_0__})\";\n        set num_params := 1;\n    };\n\n    create cast from ext::pgvector::vector to std::json {\n        set volatility := 'Immutable';\n        using sql 'SELECT val::text::jsonb';\n    };\n\n    create cast from std::json to ext::pgvector::vector {\n        set volatility := 'Immutable';\n        using sql $$\n        SELECT (\n            nullif(val, 'null'::jsonb)::text::vector\n        )\n        $$;\n    };\n\n    create cast from ext::pgvector::vector to std::str {\n        set volatility := 'Immutable';\n        using sql cast;\n    };\n\n    create cast from std::str to ext::pgvector::vector {\n        set volatility := 'Immutable';\n        using sql cast;\n    };\n\n    create cast from ext::pgvector::vector to std::bytes {\n        set volatility := 'Immutable';\n        using sql 'SELECT vector_send(val)';\n    };\n\n    create cast from ext::pgvector::halfvec to std::json {\n        set volatility := 'Immutable';\n        using sql 'SELECT val::text::jsonb';\n    };\n\n    create cast from std::json to ext::pgvector::halfvec {\n        set volatility := 'Immutable';\n        using sql $$\n        SELECT (\n            nullif(val, 'null'::jsonb)::text::halfvec\n        )\n        $$;\n    };\n\n    create cast from ext::pgvector::halfvec to std::str {\n        set volatility := 'Immutable';\n        using sql cast;\n    };\n\n    create cast from std::str to ext::pgvector::halfvec {\n        set volatility := 'Immutable';\n        using sql cast;\n    };\n\n    create cast from ext::pgvector::halfvec to std::bytes {\n        set volatility := 'Immutable';\n        using sql 'SELECT halfvec_send(val)';\n    };\n\n    create cast from ext::pgvector::sparsevec to std::str {\n        set volatility := 'Immutable';\n        using sql 'SELECT sparsevec_to_text(val)';\n    };\n\n    create cast from std::str to ext::pgvector::sparsevec {\n        set volatility := 'Immutable';\n        using sql 'SELECT text_to_sparsevec(val)';\n    };\n\n    create cast from ext::pgvector::sparsevec to std::bytes {\n        set volatility := 'Immutable';\n        using sql 'SELECT sparsevec_send(val)';\n    };\n\n    create cast from ext::pgvector::sparsevec to std::json {\n        set volatility := 'Immutable';\n        using sql 'SELECT sparsevec_to_jsonb(val)';\n    };\n\n    create cast from std::json to ext::pgvector::sparsevec {\n        set volatility := 'Immutable';\n        using sql 'SELECT jsonb_to_sparsevec(val)';\n    };\n\n    # All casts from numerical arrays should allow assignment casts.\n    create cast from array<std::float32> to ext::pgvector::vector {\n        set volatility := 'Immutable';\n        using sql cast;\n        allow assignment;\n    };\n\n    create cast from array<std::float32> to ext::pgvector::halfvec {\n        set volatility := 'Immutable';\n        using sql cast;\n        allow assignment;\n    };\n\n    create cast from array<std::float64> to ext::pgvector::vector {\n        set volatility := 'Immutable';\n        using sql cast;\n        allow assignment;\n    };\n\n    create cast from array<std::float64> to ext::pgvector::halfvec {\n        set volatility := 'Immutable';\n        using sql cast;\n        allow assignment;\n    };\n\n    create cast from array<std::int16> to ext::pgvector::vector {\n        set volatility := 'Immutable';\n        using sql $$\n        SELECT val::float4[]::vector\n        $$;\n        allow assignment;\n    };\n\n    create cast from array<std::int16> to ext::pgvector::halfvec {\n        set volatility := 'Immutable';\n        using sql $$\n        SELECT val::float4[]::halfvec\n        $$;\n        allow assignment;\n    };\n\n    create cast from array<std::int32> to ext::pgvector::vector {\n        set volatility := 'Immutable';\n        using sql cast;\n        allow assignment;\n    };\n\n    create cast from array<std::int32> to ext::pgvector::halfvec {\n        set volatility := 'Immutable';\n        using sql cast;\n        allow assignment;\n    };\n\n    create cast from array<std::int64> to ext::pgvector::vector {\n        set volatility := 'Immutable';\n        using sql $$\n        SELECT val::float4[]::vector\n        $$;\n        allow assignment;\n    };\n\n    create cast from array<std::int64> to ext::pgvector::halfvec {\n        set volatility := 'Immutable';\n        using sql $$\n        SELECT val::float4[]::halfvec\n        $$;\n        allow assignment;\n    };\n\n    create cast from ext::pgvector::vector to array<std::float32> {\n        set volatility := 'Immutable';\n        using sql cast;\n    };\n\n    create cast from ext::pgvector::halfvec to array<std::float32> {\n        set volatility := 'Immutable';\n        using sql cast;\n    };\n\n    create cast from ext::pgvector::vector to ext::pgvector::halfvec {\n        set volatility := 'Immutable';\n        using sql cast;\n        allow assignment;\n    };\n\n    create cast from ext::pgvector::vector to ext::pgvector::sparsevec {\n        set volatility := 'Immutable';\n        using sql cast;\n        allow assignment;\n    };\n\n    create cast from ext::pgvector::halfvec to ext::pgvector::vector {\n        set volatility := 'Immutable';\n        using sql cast;\n        allow implicit;\n    };\n\n    create cast from ext::pgvector::halfvec to ext::pgvector::sparsevec {\n        set volatility := 'Immutable';\n        using sql cast;\n        allow assignment;\n    };\n\n    create cast from ext::pgvector::sparsevec to ext::pgvector::vector {\n        set volatility := 'Immutable';\n        using sql cast;\n        allow assignment;\n    };\n\n    create cast from ext::pgvector::sparsevec to ext::pgvector::halfvec {\n        set volatility := 'Immutable';\n        using sql cast;\n        allow assignment;\n    };\n\n    create function ext::pgvector::euclidean_distance(\n        a: ext::pgvector::vector,\n        b: ext::pgvector::vector,\n    ) -> std::float64 {\n        set volatility := 'Immutable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        using sql 'SELECT a <-> b';\n    };\n\n    create function ext::pgvector::euclidean_distance(\n        a: ext::pgvector::halfvec,\n        b: ext::pgvector::halfvec,\n    ) -> std::float64 {\n        set volatility := 'Immutable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        using sql 'SELECT a <-> b';\n    };\n\n    create function ext::pgvector::euclidean_distance(\n        a: ext::pgvector::sparsevec,\n        b: ext::pgvector::sparsevec,\n    ) -> std::float64 {\n        set volatility := 'Immutable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        using sql 'SELECT a <-> b';\n    };\n\n    create function ext::pgvector::neg_inner_product(\n        a: ext::pgvector::vector,\n        b: ext::pgvector::vector,\n    ) -> std::float64 {\n        set volatility := 'Immutable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        using sql 'SELECT (a <#> b)';\n    };\n\n    create function ext::pgvector::neg_inner_product(\n        a: ext::pgvector::halfvec,\n        b: ext::pgvector::halfvec,\n    ) -> std::float64 {\n        set volatility := 'Immutable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        using sql 'SELECT (a <#> b)';\n    };\n\n    create function ext::pgvector::neg_inner_product(\n        a: ext::pgvector::sparsevec,\n        b: ext::pgvector::sparsevec,\n    ) -> std::float64 {\n        set volatility := 'Immutable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        using sql 'SELECT (a <#> b)';\n    };\n\n    create function ext::pgvector::cosine_distance(\n        a: ext::pgvector::vector,\n        b: ext::pgvector::vector,\n    ) -> std::float64 {\n        set volatility := 'Immutable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        using sql 'SELECT a <=> b';\n    };\n\n    create function ext::pgvector::cosine_distance(\n        a: ext::pgvector::halfvec,\n        b: ext::pgvector::halfvec,\n    ) -> std::float64 {\n        set volatility := 'Immutable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        using sql 'SELECT a <=> b';\n    };\n\n    create function ext::pgvector::cosine_distance(\n        a: ext::pgvector::sparsevec,\n        b: ext::pgvector::sparsevec,\n    ) -> std::float64 {\n        set volatility := 'Immutable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        using sql 'SELECT a <=> b';\n    };\n\n    create function ext::pgvector::taxicab_distance(\n        a: ext::pgvector::vector,\n        b: ext::pgvector::vector,\n    ) -> std::float64 {\n        set volatility := 'Immutable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        using sql 'SELECT a <+> b';\n    };\n\n    create function ext::pgvector::taxicab_distance(\n        a: ext::pgvector::halfvec,\n        b: ext::pgvector::halfvec,\n    ) -> std::float64 {\n        set volatility := 'Immutable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        using sql 'SELECT a <+> b';\n    };\n\n    create function ext::pgvector::taxicab_distance(\n        a: ext::pgvector::sparsevec,\n        b: ext::pgvector::sparsevec,\n    ) -> std::float64 {\n        set volatility := 'Immutable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        using sql 'SELECT a <+> b';\n    };\n\n    create function ext::pgvector::euclidean_norm(\n        a: ext::pgvector::vector\n    ) -> std::float64 {\n        using sql function 'l2_norm';\n        set volatility := 'Immutable';\n        set force_return_cast := true;\n    };\n\n    create function ext::pgvector::euclidean_norm(\n        a: ext::pgvector::halfvec\n    ) -> std::float64 {\n        using sql function 'l2_norm';\n        set volatility := 'Immutable';\n        set force_return_cast := true;\n    };\n\n    create function ext::pgvector::euclidean_norm(\n        a: ext::pgvector::sparsevec\n    ) -> std::float64 {\n        using sql function 'l2_norm';\n        set volatility := 'Immutable';\n        set force_return_cast := true;\n    };\n\n    create function ext::pgvector::l2_normalize(\n        a: ext::pgvector::vector\n    ) -> ext::pgvector::vector {\n        using sql function 'l2_normalize';\n        set volatility := 'Immutable';\n        set force_return_cast := true;\n    };\n\n    create function ext::pgvector::l2_normalize(\n        a: ext::pgvector::halfvec\n    ) -> ext::pgvector::halfvec {\n        using sql function 'l2_normalize';\n        set volatility := 'Immutable';\n        set force_return_cast := true;\n    };\n\n    create function ext::pgvector::l2_normalize(\n        a: ext::pgvector::sparsevec\n    ) -> ext::pgvector::sparsevec {\n        using sql function 'l2_normalize';\n        set volatility := 'Immutable';\n        set force_return_cast := true;\n    };\n\n    create function ext::pgvector::subvector(\n        a: ext::pgvector::vector,\n        i: std::int64,\n        len: std::int64,\n    ) -> ext::pgvector::vector {\n        set volatility := 'Immutable';\n        using sql 'SELECT subvector(a, (i+1)::int, len::int)';\n    };\n\n    create function ext::pgvector::subvector(\n        a: ext::pgvector::halfvec,\n        i: std::int64,\n        len: std::int64,\n    ) -> ext::pgvector::halfvec {\n        set volatility := 'Immutable';\n        using sql 'SELECT subvector(a, (i+1)::int, len::int)';\n    };\n\n    create function ext::pgvector::set_probes(num: std::int64) -> std::int64 {\n        using sql $$\n            select num from (\n                select set_config('ivfflat.probes', num::text, true)\n            ) as dummy;\n        $$;\n        CREATE ANNOTATION std::deprecated :=\n            'This function is deprecated. ' ++\n            'Configure ext::pgvector::Config::probes instead';\n    };\n\n    create abstract index ext::pgvector::ivfflat_euclidean(\n        named only lists: int64\n    ) {\n        create annotation std::description :=\n            'IVFFlat index for euclidean distance.';\n        set code :=\n            'ivfflat (__col__ vector_l2_ops) WITH (lists = __kw_lists__)';\n    };\n\n    create abstract index ext::pgvector::ivfflat_ip(\n        named only lists: int64\n    ) {\n        create annotation std::description :=\n            'IVFFlat index for inner product.';\n        set code :=\n            'ivfflat (__col__ vector_ip_ops) WITH (lists = __kw_lists__)';\n    };\n\n    create abstract index ext::pgvector::ivfflat_cosine(\n        named only lists: int64\n    ) {\n        create annotation std::description :=\n            'IVFFlat index for cosine distance.';\n        set code :=\n            'ivfflat (__col__ vector_cosine_ops) WITH (lists = __kw_lists__)';\n    };\n\n    create abstract index ext::pgvector::hnsw_euclidean(\n        named only m: int64 = 16,\n        named only ef_construction: int64 = 64,\n    ) {\n        create annotation std::description :=\n            'HNSW index for euclidean distance.';\n        set code := $$\n            hnsw (__col__ vector_l2_ops)\n            WITH (m = __kw_m__, ef_construction = __kw_ef_construction__)\n        $$;\n    };\n\n    create abstract index ext::pgvector::hnsw_ip(\n        named only m: int64 = 16,\n        named only ef_construction: int64 = 64,\n    ) {\n        create annotation std::description :=\n            'HNSW index for inner product.';\n        set code := $$\n            hnsw (__col__ vector_ip_ops)\n            WITH (m = __kw_m__, ef_construction = __kw_ef_construction__)\n        $$;\n    };\n\n    create abstract index ext::pgvector::hnsw_cosine(\n        named only m: int64 = 16,\n        named only ef_construction: int64 = 64,\n    ) {\n        create annotation std::description :=\n            'HNSW index for cosine distance.';\n        set code := $$\n            hnsw (__col__ vector_cosine_ops)\n            WITH (m = __kw_m__, ef_construction = __kw_ef_construction__)\n        $$;\n    };\n\n    create abstract index ext::pgvector::hnsw_taxicab(\n        named only m: int64 = 16,\n        named only ef_construction: int64 = 64,\n    ) {\n        create annotation std::description :=\n            'HNSW index for taxicab (L1) distance.';\n        set code := $$\n            hnsw (__col__ vector_l1_ops)\n            WITH (m = __kw_m__, ef_construction = __kw_ef_construction__)\n        $$;\n    };\n\n    create index match for ext::pgvector::vector using ext::pgvector::ivfflat_euclidean;\n    create index match for ext::pgvector::vector using ext::pgvector::ivfflat_ip;\n    create index match for ext::pgvector::vector using ext::pgvector::ivfflat_cosine;\n    create index match for ext::pgvector::vector using ext::pgvector::hnsw_euclidean;\n    create index match for ext::pgvector::vector using ext::pgvector::hnsw_ip;\n    create index match for ext::pgvector::vector using ext::pgvector::hnsw_cosine;\n    create index match for ext::pgvector::vector using ext::pgvector::hnsw_taxicab;\n\n    create abstract index ext::pgvector::ivfflat_hv_euclidean(\n        named only lists: int64\n    ) {\n        create annotation std::description :=\n            'IVFFlat index for euclidean distance.';\n        set code :=\n            'ivfflat (__col__ halfvec_l2_ops) WITH (lists = __kw_lists__)';\n    };\n\n    create abstract index ext::pgvector::ivfflat_hv_ip(\n        named only lists: int64\n    ) {\n        create annotation std::description :=\n            'IVFFlat index for inner product.';\n        set code :=\n            'ivfflat (__col__ halfvec_ip_ops) WITH (lists = __kw_lists__)';\n    };\n\n    create abstract index ext::pgvector::ivfflat_hv_cosine(\n        named only lists: int64\n    ) {\n        create annotation std::description :=\n            'IVFFlat index for cosine distance.';\n        set code :=\n            'ivfflat (__col__ halfvec_cosine_ops) WITH (lists = __kw_lists__)';\n    };\n\n    create abstract index ext::pgvector::hnsw_hv_euclidean(\n        named only m: int64 = 16,\n        named only ef_construction: int64 = 64,\n    ) {\n        create annotation std::description :=\n            'HNSW index for euclidean distance.';\n        set code := $$\n            hnsw (__col__ halfvec_l2_ops)\n            WITH (m = __kw_m__, ef_construction = __kw_ef_construction__)\n        $$;\n    };\n\n    create abstract index ext::pgvector::hnsw_hv_ip(\n        named only m: int64 = 16,\n        named only ef_construction: int64 = 64,\n    ) {\n        create annotation std::description :=\n            'HNSW index for inner product.';\n        set code := $$\n            hnsw (__col__ halfvec_ip_ops)\n            WITH (m = __kw_m__, ef_construction = __kw_ef_construction__)\n        $$;\n    };\n\n    create abstract index ext::pgvector::hnsw_hv_cosine(\n        named only m: int64 = 16,\n        named only ef_construction: int64 = 64,\n    ) {\n        create annotation std::description :=\n            'HNSW index for cosine distance.';\n        set code := $$\n            hnsw (__col__ halfvec_cosine_ops)\n            WITH (m = __kw_m__, ef_construction = __kw_ef_construction__)\n        $$;\n    };\n\n    create abstract index ext::pgvector::hnsw_hv_taxicab(\n        named only m: int64 = 16,\n        named only ef_construction: int64 = 64,\n    ) {\n        create annotation std::description :=\n            'HNSW index for taxicab (L1) distance.';\n        set code := $$\n            hnsw (__col__ halfvec_l1_ops)\n            WITH (m = __kw_m__, ef_construction = __kw_ef_construction__)\n        $$;\n    };\n\n    create index match for ext::pgvector::halfvec using ext::pgvector::ivfflat_hv_euclidean;\n    create index match for ext::pgvector::halfvec using ext::pgvector::ivfflat_hv_ip;\n    create index match for ext::pgvector::halfvec using ext::pgvector::ivfflat_hv_cosine;\n    create index match for ext::pgvector::halfvec using ext::pgvector::hnsw_hv_euclidean;\n    create index match for ext::pgvector::halfvec using ext::pgvector::hnsw_hv_ip;\n    create index match for ext::pgvector::halfvec using ext::pgvector::hnsw_hv_cosine;\n    create index match for ext::pgvector::halfvec using ext::pgvector::hnsw_hv_taxicab;\n\n    create abstract index ext::pgvector::hnsw_sv_euclidean(\n        named only m: int64 = 16,\n        named only ef_construction: int64 = 64,\n    ) {\n        create annotation std::description :=\n            'HNSW index for euclidean distance.';\n        set code := $$\n            hnsw (__col__ sparsevec_l2_ops)\n            WITH (m = __kw_m__, ef_construction = __kw_ef_construction__)\n        $$;\n    };\n\n    create abstract index ext::pgvector::hnsw_sv_ip(\n        named only m: int64 = 16,\n        named only ef_construction: int64 = 64,\n    ) {\n        create annotation std::description :=\n            'HNSW index for inner product.';\n        set code := $$\n            hnsw (__col__ sparsevec_ip_ops)\n            WITH (m = __kw_m__, ef_construction = __kw_ef_construction__)\n        $$;\n    };\n\n    create abstract index ext::pgvector::hnsw_sv_cosine(\n        named only m: int64 = 16,\n        named only ef_construction: int64 = 64,\n    ) {\n        create annotation std::description :=\n            'HNSW index for cosine distance.';\n        set code := $$\n            hnsw (__col__ sparsevec_cosine_ops)\n            WITH (m = __kw_m__, ef_construction = __kw_ef_construction__)\n        $$;\n    };\n\n    create abstract index ext::pgvector::hnsw_sv_taxicab(\n        named only m: int64 = 16,\n        named only ef_construction: int64 = 64,\n    ) {\n        create annotation std::description :=\n            'HNSW index for taxicab (L1) distance.';\n        set code := $$\n            hnsw (__col__ sparsevec_l1_ops)\n            WITH (m = __kw_m__, ef_construction = __kw_ef_construction__)\n        $$;\n    };\n\n    create index match for ext::pgvector::sparsevec using ext::pgvector::hnsw_sv_euclidean;\n    create index match for ext::pgvector::sparsevec using ext::pgvector::hnsw_sv_ip;\n    create index match for ext::pgvector::sparsevec using ext::pgvector::hnsw_sv_cosine;\n    create index match for ext::pgvector::sparsevec using ext::pgvector::hnsw_sv_taxicab;\n};\n"
  },
  {
    "path": "edb/lib/fts.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nCREATE MODULE std::fts;\n\nCREATE SCALAR TYPE std::fts::Language\n    EXTENDING enum<\n        ara,\n        hye,\n        eus,\n        cat,\n        dan,\n        nld,\n        eng,\n        fin,\n        fra,\n        deu,\n        ell,\n        hin,\n        hun,\n        ind,\n        gle,\n        ita,\n        nor,\n        por,\n        ron,\n        rus,\n        spa,\n        swe,\n        tur,\n    > {\n    CREATE ANNOTATION std::description := '\n        Languages supported by PostgreSQL FTS, ElasticSearch and Apache Lucene.\n        Names are ISO 639-3 language identifiers.\n    ';\n};\n\nCREATE SCALAR TYPE std::fts::Weight EXTENDING enum<A, B, C, D> {\n    CREATE ANNOTATION std::description := \"\n        Weight category.\n        Weight values for each category can be provided in std::fts::search.\n    \";\n};\n\nCREATE ABSTRACT INDEX std::fts::index {\n    CREATE ANNOTATION std::description :=\n        \"Full-text search index based on the Postgres's GIN index.\";\n    SET code := ''; # overridden by a special case\n};\n\nCREATE SCALAR TYPE std::fts::document {\n    SET transient := true;\n};\n\ncreate index match for std::fts::document using std::fts::index;\n\nCREATE FUNCTION std::fts::with_options(\n    text: std::str,\n    NAMED ONLY language: anyenum,\n    NAMED ONLY weight_category: optional std::fts::Weight = std::fts::Weight.A,\n) -> std::fts::document {\n    CREATE ANNOTATION std::description := '\n        Adds language and weight category information to a string,\n        so it be indexed with std::fts::index.\n    ';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\nCREATE FUNCTION std::fts::search(\n    object: anyobject,\n    query: std::str,\n    named only language: std::str = <std::str>std::fts::Language.eng,\n    named only weights: optional array<float64> = {},\n) -> optional tuple<object: anyobject, score: float32>\n{\n    CREATE ANNOTATION std::description := '\n        Search an object using its std::fts::index index.\n        Returns objects that match the specified query and the matching score.\n    ';\n    SET volatility := 'Stable';\n    USING SQL EXPRESSION;\n};\n\nCREATE SCALAR TYPE std::fts::PGLanguage\n    EXTENDING enum<\n        xxx_simple,\n        ara,\n        hye,\n        eus,\n        cat,\n        dan,\n        nld,\n        eng,\n        fin,\n        fra,\n        deu,\n        ell,\n        hin,\n        hun,\n        ind,\n        gle,\n        ita,\n        lit,\n        npi,\n        nor,\n        por,\n        ron,\n        rus,\n        srp,\n        spa,\n        swe,\n        tam,\n        tur,\n        yid,\n    > {\n    CREATE ANNOTATION std::description :='\n        Languages supported by PostgreSQL FTS.\n        Names are ISO 639-3 language identifiers or Postgres regconfig names\n        prefixed with `xxx_`.\n    ';\n};\n\nCREATE SCALAR TYPE std::fts::ElasticLanguage\n    EXTENDING enum<\n        ara,\n        bul,\n        cat,\n        ces,\n        ckb,\n        dan,\n        deu,\n        ell,\n        eng,\n        eus,\n        fas,\n        fin,\n        fra,\n        gle,\n        glg,\n        hin,\n        hun,\n        hye,\n        ind,\n        ita,\n        lav,\n        nld,\n        nor,\n        por,\n        ron,\n        rus,\n        spa,\n        swe,\n        tha,\n        tur,\n        zho,\n        edb_Brazilian,\n        edb_ChineseJapaneseKorean,\n    > {\n    CREATE ANNOTATION std::description := '\n        Languages supported by ElasticSearch.\n        Names are ISO 639-3 language identifiers or EdgeDB language identifers.\n    ';\n};\n\nCREATE SCALAR TYPE std::fts::LuceneLanguage\n    EXTENDING enum<\n        ara,\n        ben,\n        bul,\n        cat,\n        ces,\n        ckb,\n        dan,\n        deu,\n        ell,\n        eng,\n        est,\n        eus,\n        fas,\n        fin,\n        fra,\n        gle,\n        glg,\n        hin,\n        hun,\n        hye,\n        ind,\n        ita,\n        lav,\n        lit,\n        nld,\n        nor,\n        por,\n        ron,\n        rus,\n        spa,\n        srp,\n        swe,\n        tha,\n        tur,\n        edb_Brazilian,\n        edb_ChineseJapaneseKorean,\n        edb_Indian,\n    > {\n    CREATE ANNOTATION std::description := '\n        Languages supported by Apache Lucene.\n        Names are ISO 639-3 language identifiers or EdgeDB language identifers.\n    ';\n};\n"
  },
  {
    "path": "edb/lib/math.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nCREATE MODULE std::math;\n\n\nCREATE FUNCTION\nstd::math::abs(x: std::anyreal) -> std::anyreal\n{\n    CREATE ANNOTATION std::description :=\n        'Return the absolute value of the input *x*.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'abs';\n};\n\n\nCREATE FUNCTION\nstd::math::ceil(x: std::int64) -> std::int64\n{\n    CREATE ANNOTATION std::description := 'Round up to the nearest integer.';\n    SET volatility := 'Immutable';\n    USING SQL 'SELECT \"x\";';\n};\n\n\nCREATE FUNCTION\nstd::math::ceil(x: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description := 'Round up to the nearest integer.';\n    SET volatility := 'Immutable';\n    USING SQL 'SELECT ceil(\"x\");'\n};\n\n\nCREATE FUNCTION\nstd::math::ceil(x: std::bigint) -> std::bigint\n{\n    CREATE ANNOTATION std::description := 'Round up to the nearest integer.';\n    SET volatility := 'Immutable';\n    USING SQL 'SELECT \"x\";'\n};\n\n\nCREATE FUNCTION\nstd::math::ceil(x: std::decimal) -> std::decimal\n{\n    CREATE ANNOTATION std::description := 'Round up to the nearest integer.';\n    SET volatility := 'Immutable';\n    USING SQL 'SELECT ceil(\"x\");'\n};\n\n\nCREATE FUNCTION\nstd::math::floor(x: std::int64) -> std::int64\n{\n    CREATE ANNOTATION std::description := 'Round down to the nearest integer.';\n    SET volatility := 'Immutable';\n    USING SQL 'SELECT \"x\";';\n};\n\n\nCREATE FUNCTION\nstd::math::floor(x: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description := 'Round down to the nearest integer.';\n    SET volatility := 'Immutable';\n    USING SQL 'SELECT floor(\"x\");';\n};\n\n\nCREATE FUNCTION\nstd::math::floor(x: std::bigint) -> std::bigint\n{\n    CREATE ANNOTATION std::description := 'Round down to the nearest integer.';\n    SET volatility := 'Immutable';\n    USING SQL 'SELECT \"x\";'\n};\n\n\nCREATE FUNCTION\nstd::math::floor(x: std::decimal) -> std::decimal\n{\n    CREATE ANNOTATION std::description := 'Round down to the nearest integer.';\n    SET volatility := 'Immutable';\n    USING SQL 'SELECT floor(\"x\");';\n};\n\nCREATE FUNCTION\nstd::math::exp(x: std::int64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the exponential of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'exp';\n};\n\n\nCREATE FUNCTION\nstd::math::exp(x: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the exponential of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'exp';\n};\n\n\nCREATE FUNCTION\nstd::math::exp(x: std::decimal) -> std::decimal\n{\n    CREATE ANNOTATION std::description :=\n        'Return the exponential of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'exp';\n};\n\n\nCREATE FUNCTION\nstd::math::ln(x: std::int64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the natural logarithm of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'ln';\n};\n\n\nCREATE FUNCTION\nstd::math::ln(x: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the natural logarithm of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'ln';\n};\n\n\nCREATE FUNCTION\nstd::math::ln(x: std::decimal) -> std::decimal\n{\n    CREATE ANNOTATION std::description :=\n        'Return the natural logarithm of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'ln';\n};\n\n\nCREATE FUNCTION\nstd::math::lg(x: std::int64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the base 10 logarithm of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'log';\n};\n\n\nCREATE FUNCTION\nstd::math::lg(x: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the base 10 logarithm of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'log';\n};\n\n\nCREATE FUNCTION\nstd::math::lg(x: std::decimal) -> std::decimal\n{\n    CREATE ANNOTATION std::description :=\n        'Return the base 10 logarithm of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'log';\n};\n\n\nCREATE FUNCTION\nstd::math::log(x: std::decimal, NAMED ONLY base: std::decimal) -> std::decimal\n{\n    CREATE ANNOTATION std::description :=\n        'Return the logarithm of the input value in the specified *base*.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT log(\"base\", \"x\")\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::math::sqrt(x: std::int64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the square root of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'sqrt';\n};\n\n\nCREATE FUNCTION\nstd::math::sqrt(x: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the square root of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'sqrt';\n};\n\n\nCREATE FUNCTION\nstd::math::sqrt(x: std::decimal) -> std::decimal\n{\n    CREATE ANNOTATION std::description :=\n        'Return the square root of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'sqrt';\n};\n\n# std::math::mean\n# -----------\n# The mean function returns an empty set if the input is empty set. On\n# all other inputs it returns the mean for that input set.\nCREATE FUNCTION\nstd::math::mean(vals: SET OF std::decimal) -> std::decimal\n{\n    CREATE ANNOTATION std::description :=\n        'Return the arithmetic mean of the input set.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'avg';\n    SET error_on_null_result := 'invalid input to mean(): not ' ++\n                                'enough elements in input set';\n};\n\n\nCREATE FUNCTION\nstd::math::mean(vals: SET OF std::int64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the arithmetic mean of the input set.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'avg';\n    # SQL 'avg' returns numeric on integer inputs.\n    SET force_return_cast := true;\n    SET error_on_null_result := 'invalid input to mean(): not ' ++\n                                'enough elements in input set';\n};\n\n\nCREATE FUNCTION\nstd::math::mean(vals: SET OF std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the arithmetic mean of the input set.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'avg';\n    SET error_on_null_result := 'invalid input to mean(): not ' ++\n                                'enough elements in input set';\n};\n\n\n# std::math::stddev\n# ------------\nCREATE FUNCTION\nstd::math::stddev(vals: SET OF std::decimal) -> std::decimal\n{\n    CREATE ANNOTATION std::description :=\n        'Return the sample standard deviation of the input set.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'stddev';\n    SET error_on_null_result := 'invalid input to stddev(): not ' ++\n                                'enough elements in input set';\n};\n\n\nCREATE FUNCTION\nstd::math::stddev(vals: SET OF std::int64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the sample standard deviation of the input set.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'stddev';\n    # SQL 'stddev' returns numeric on integer inputs.\n    SET force_return_cast := true;\n    SET error_on_null_result := 'invalid input to stddev(): not ' ++\n                                'enough elements in input set';\n};\n\n\nCREATE FUNCTION\nstd::math::stddev(vals: SET OF std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the sample standard deviation of the input set.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'stddev';\n    SET error_on_null_result := 'invalid input to stddev(): not ' ++\n                                'enough elements in input set';\n};\n\n\n# std::math::stddev_pop\n# ----------------\nCREATE FUNCTION\nstd::math::stddev_pop(vals: SET OF std::decimal) -> std::decimal\n{\n    CREATE ANNOTATION std::description :=\n        'Return the population standard deviation of the input set.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'stddev_pop';\n    SET error_on_null_result := 'invalid input to stddev_pop(): not ' ++\n                                'enough elements in input set';\n};\n\n\nCREATE FUNCTION\nstd::math::stddev_pop(vals: SET OF std::int64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the population standard deviation of the input set.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'stddev_pop';\n    # SQL 'stddev_pop' returns numeric on integer inputs.\n    SET force_return_cast := true;\n    SET error_on_null_result := 'invalid input to stddev_pop(): not ' ++\n                                'enough elements in input set';\n};\n\n\nCREATE FUNCTION\nstd::math::stddev_pop(vals: SET OF std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the population standard deviation of the input set.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'stddev_pop';\n    SET error_on_null_result := 'invalid input to stddev_pop(): not ' ++\n                                'enough elements in input set';\n};\n\n\n# std::math::var\n# --------------\nCREATE FUNCTION\nstd::math::var(vals: SET OF std::decimal) -> OPTIONAL std::decimal\n{\n    CREATE ANNOTATION std::description :=\n        'Return the sample variance of the input set.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'variance';\n    SET error_on_null_result := 'invalid input to var(): not ' ++\n                                'enough elements in input set';\n};\n\n\nCREATE FUNCTION\nstd::math::var(vals: SET OF std::int64) -> OPTIONAL std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the sample variance of the input set.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'variance';\n    # SQL 'var' returns numeric on integer inputs.\n    SET force_return_cast := true;\n    SET error_on_null_result := 'invalid input to var(): not ' ++\n                                'enough elements in input set';\n};\n\n\nCREATE FUNCTION\nstd::math::var(vals: SET OF std::float64) -> OPTIONAL std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the sample variance of the input set.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'variance';\n    SET error_on_null_result := 'invalid input to var(): not ' ++\n                                'enough elements in input set';\n};\n\n\n# std::math::var_pop\n# -------------\nCREATE FUNCTION\nstd::math::var_pop(vals: SET OF std::decimal) -> OPTIONAL std::decimal\n{\n    CREATE ANNOTATION std::description :=\n        'Return the population variance of the input set.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'var_pop';\n    SET error_on_null_result := 'invalid input to var_pop(): not ' ++\n                                'enough elements in input set';\n};\n\n\nCREATE FUNCTION\nstd::math::var_pop(vals: SET OF std::int64) -> OPTIONAL std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the population variance of the input set.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'var_pop';\n    # SQL 'var_pop' returns numeric on integer inputs.\n    SET force_return_cast := true;\n    SET error_on_null_result := 'invalid input to var_pop(): not ' ++\n                                'enough elements in input set';\n};\n\n\nCREATE FUNCTION\nstd::math::var_pop(vals: SET OF std::float64) -> OPTIONAL std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the population variance of the input set.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'var_pop';\n    SET error_on_null_result := 'invalid input to var_pop(): not ' ++\n                                'enough elements in input set';\n};\n\n\nCREATE FUNCTION\nstd::math::pi() -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the constant value of pi.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'pi';\n};\n\nCREATE FUNCTION\nstd::math::e() -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the constant value of e.';\n    SET volatility := 'Immutable';\n    USING SQL 'SELECT exp(1);'\n};\n\nCREATE FUNCTION\nstd::math::acos(x: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the inverse cosine of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'acos';\n};\n\n\nCREATE FUNCTION\nstd::math::asin(x: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the inverse sine of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'asin';\n};\n\n\nCREATE FUNCTION\nstd::math::atan(x: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the inverse tangent of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'atan';\n};\n\n\nCREATE FUNCTION\nstd::math::atan2(y: std::float64, x: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the inverse tangent of y/x of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'atan2';\n};\n\n\nCREATE FUNCTION\nstd::math::cos(x: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the cosine of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'cos';\n};\n\n\nCREATE FUNCTION\nstd::math::cot(x: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the cotangent of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'cot';\n};\n\n\nCREATE FUNCTION\nstd::math::sin(x: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the sine of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'sin';\n};\n\n\nCREATE FUNCTION\nstd::math::tan(x: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the tangent of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'tan';\n};\n"
  },
  {
    "path": "edb/lib/net.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nCREATE MODULE std::net;\n\nCREATE MODULE std::net::perm;\nCREATE PERMISSION std::net::perm::http_read;\nCREATE PERMISSION std::net::perm::http_write;\n\nCREATE SCALAR TYPE std::net::RequestState EXTENDING std::enum<\n    Pending,\n    InProgress,\n    Completed,\n    Failed\n>;\n\nCREATE SCALAR TYPE std::net::RequestFailureKind EXTENDING std::enum<\n    NetworkError,\n    Timeout\n>;\n\nCREATE MODULE std::net::http;\n\nCREATE SCALAR TYPE std::net::http::Method EXTENDING std::enum<\n    `GET`,\n    POST,\n    PUT,\n    `DELETE`,\n    HEAD,\n    OPTIONS,\n    PATCH\n>;\n\nCREATE TYPE std::net::http::Response EXTENDING std::BaseObject {\n    CREATE REQUIRED PROPERTY created_at: std::datetime;\n    CREATE PROPERTY status: std::int16;\n    CREATE PROPERTY headers: std::array<std::tuple<name: std::str, value: std::str>>;\n    CREATE PROPERTY body: std::bytes;\n\n    CREATE ACCESS POLICY ap_read allow select using (\n        global std::net::perm::http_read\n    );\n    CREATE ACCESS POLICY ap_write allow insert, update, delete using (\n        global std::net::perm::http_write\n    );\n};\n\nCREATE TYPE std::net::http::ScheduledRequest extending std::BaseObject {\n    CREATE REQUIRED PROPERTY state: std::net::RequestState;\n    CREATE REQUIRED PROPERTY created_at: std::datetime;\n    CREATE REQUIRED PROPERTY updated_at: std::datetime;\n    CREATE PROPERTY failure: tuple<kind: std::net::RequestFailureKind, message: str>;\n\n    CREATE REQUIRED PROPERTY url: std::str;\n    CREATE REQUIRED PROPERTY method: std::net::http::Method;\n    CREATE PROPERTY headers: std::array<std::tuple<name: std::str, value: std::str>>;\n    CREATE PROPERTY body: std::bytes;\n\n    CREATE LINK response: std::net::http::Response {\n        CREATE CONSTRAINT exclusive;\n        ON SOURCE DELETE DELETE TARGET;\n    };\n\n    CREATE INDEX ON ((.state, .updated_at));\n\n    CREATE ACCESS POLICY ap_read allow select using (\n        global std::net::perm::http_read\n    );\n    CREATE ACCESS POLICY ap_write allow insert, update, delete using (\n        global std::net::perm::http_write\n    );\n};\n\nALTER TYPE std::net::http::Response {\n    CREATE LINK request := .<response[is std::net::http::ScheduledRequest];\n};\n\nCREATE FUNCTION\nstd::net::http::schedule_request(\n    url: str,\n    named only body: optional std::bytes = {},\n    named only headers: optional std::array<\n        std::tuple<\n            name: std::str,\n            value: std::str\n        >\n    > = {},\n    named only method: std::net::http::Method = std::net::http::Method.`GET`,\n) -> std::net::http::ScheduledRequest\n{\n    SET is_inlined := true;\n    set required_permissions := { std::net::perm::http_write };\n    USING ((\n        INSERT std::net::http::ScheduledRequest {\n            url := url,\n            method := method,\n            headers := headers,\n            body := body,\n\n            created_at := std::datetime_of_statement(),\n            updated_at := std::datetime_of_statement(),\n            state := std::net::RequestState.Pending,\n        }\n    ));\n};\n\nCREATE FUNCTION\nstd::net::http::schedule_request(\n    url: str,\n    named only body: std::json,\n    named only headers: optional std::array<\n        std::tuple<\n            name: std::str,\n            value: std::str\n        >\n    > = {},\n    named only method: std::net::http::Method = std::net::http::Method.`GET`,\n) -> std::net::http::ScheduledRequest\n{\n    SET is_inlined := true;\n    USING ((\n        WITH \n            has_content_type := any(\n                std::str_lower(std::array_unpack(headers).0) = 'content-type'\n            ),\n            actual_headers := (\n                IF has_content_type\n                THEN headers\n                ELSE headers ++ [\n                    (name := \"Content-Type\", value := \"application/json\")\n                ]\n            )\n        select std::net::http::schedule_request(\n            url,\n            body := to_bytes(body),\n            headers := actual_headers,\n            method := method,\n        )\n    ));\n};\n"
  },
  {
    "path": "edb/lib/pg.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nCREATE MODULE std::pg;\n\nCREATE ABSTRACT INDEX std::pg::hash {\n    CREATE ANNOTATION std::description :=\n        'Index based on a 32-bit hash derived from the indexed value.';\n    SET code := 'hash ((__col__))';\n};\n\ncreate index match for anytype using std::pg::hash;\n\nCREATE ABSTRACT INDEX std::pg::btree {\n    CREATE ANNOTATION std::description :=\n        'B-tree index can be used to retrieve data in sorted order.';\n    SET code := 'btree ((__col__) NULLS FIRST)';\n};\n\ncreate index match for anytype using std::pg::btree;\n\nCREATE ABSTRACT INDEX std::pg::gin {\n    CREATE ANNOTATION std::description :=\n        'GIN is an \"inverted index\" appropriate for data values that \\\n        contain multiple elements, such as arrays and JSON.';\n    SET code := 'gin ((__col__))';\n};\n\ncreate index match for array<anytype> using std::pg::gin;\ncreate index match for std::json using std::pg::gin;\n\nCREATE ABSTRACT INDEX std::pg::gist {\n    CREATE ANNOTATION std::description :=\n        'GIST index can be used to optimize searches involving ranges.';\n    SET code := 'gist ((__col__))';\n};\n\ncreate index match for array<anytype> using std::pg::gist;\ncreate index match for range<std::anypoint> using std::pg::gist;\ncreate index match for multirange<std::anypoint> using std::pg::gist;\n\nCREATE ABSTRACT INDEX std::pg::spgist {\n    CREATE ANNOTATION std::description :=\n        'SP-GIST index can be used to optimize searches involving ranges \\\n        and strings.';\n    SET code := 'spgist ((__col__))';\n};\n\ncreate index match for range<std::anypoint> using std::pg::spgist;\ncreate index match for std::str using std::pg::spgist;\n\nCREATE ABSTRACT INDEX std::pg::brin {\n    CREATE ANNOTATION std::description :=\n        'BRIN (Block Range INdex) index works with summaries about the values \\\n        stored in consecutive physical block ranges in the database.';\n    SET code := 'brin ((__col__))';\n};\n\ncreate index match for range<std::anypoint> using std::pg::brin;\ncreate index match for std::anyreal using std::pg::brin;\ncreate index match for std::bytes using std::pg::brin;\ncreate index match for std::str using std::pg::brin;\ncreate index match for std::uuid using std::pg::brin;\ncreate index match for std::datetime using std::pg::brin;\ncreate index match for std::duration using std::pg::brin;\ncreate index match for std::cal::local_datetime using std::pg::brin;\ncreate index match for std::cal::local_date using std::pg::brin;\ncreate index match for std::cal::local_time using std::pg::brin;\ncreate index match for std::cal::relative_duration using std::pg::brin;\ncreate index match for std::cal::date_duration using std::pg::brin;\n\ncreate scalar type std::pg::json extending std::anyscalar;\ncreate scalar type std::pg::timestamptz extending std::anycontiguous;\ncreate scalar type std::pg::timestamp extending std::anycontiguous;\ncreate scalar type std::pg::date extending std::anydiscrete;\ncreate scalar type std::pg::interval extending std::anycontiguous;\n"
  },
  {
    "path": "edb/lib/schema.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## INTROSPECTION SCHEMA\n\n\nCREATE MODULE schema;\n\nCREATE SCALAR TYPE schema::Cardinality\n    EXTENDING enum<One, Many>;\n\nCREATE SCALAR TYPE schema::TargetDeleteAction\n    EXTENDING enum<Restrict, DeleteSource, Allow, DeferredRestrict>;\n\nCREATE SCALAR TYPE schema::SourceDeleteAction\n    EXTENDING enum<DeleteTarget, Allow, DeleteTargetIfOrphan>;\n\nCREATE SCALAR TYPE schema::OperatorKind\n    EXTENDING enum<Infix, Postfix, Prefix, Ternary>;\n\nCREATE SCALAR TYPE schema::Volatility\n    EXTENDING enum<Immutable, Stable, Volatile, Modifying>;\n\nCREATE SCALAR TYPE schema::ParameterKind\n    EXTENDING enum<VariadicParam, NamedOnlyParam, PositionalParam>;\n\nCREATE SCALAR TYPE schema::TypeModifier\n    EXTENDING enum<SetOfType, OptionalType, SingletonType>;\n\nCREATE SCALAR TYPE schema::AccessPolicyAction\n    EXTENDING enum<Allow, Deny>;\n\nCREATE SCALAR TYPE schema::AccessKind\n    EXTENDING enum<`Select`, UpdateRead, UpdateWrite, `Delete`, `Insert`>;\n\nCREATE SCALAR TYPE schema::TriggerTiming\n    EXTENDING enum<After, AfterCommitOf>;\n\nCREATE SCALAR TYPE schema::TriggerKind\n    EXTENDING enum<`Update`, `Delete`, `Insert`>;\n\nCREATE SCALAR TYPE schema::TriggerScope\n    EXTENDING enum<All, Each>;\n\nCREATE SCALAR TYPE schema::RewriteKind\n    EXTENDING enum<`Update`, `Insert`>;\n\nCREATE SCALAR TYPE schema::MigrationGeneratedBy\n    EXTENDING enum<DevMode, DDLStatement>;\n\nCREATE SCALAR TYPE schema::IndexDeferrability\n    EXTENDING enum<Prohibited, Permitted, `Required`>;\n\nCREATE SCALAR TYPE schema::SplatStrategy\n    EXTENDING enum<Default, Explicit, Implicit>;\n\n# Base type for all schema entities.\nCREATE ABSTRACT TYPE schema::Object EXTENDING std::BaseObject {\n    CREATE REQUIRED PROPERTY name -> std::str;\n    CREATE REQUIRED PROPERTY internal -> std::bool {\n        SET default := false;\n    };\n    CREATE REQUIRED PROPERTY builtin -> std::bool {\n        SET default := false;\n    };\n    CREATE PROPERTY computed_fields -> array<std::str>;\n\n    CREATE ACCESS POLICY not_internal\n        ALLOW SELECT USING (not .internal);\n};\n\n\nCREATE ABSTRACT TYPE schema::SubclassableObject EXTENDING schema::Object {\n    CREATE PROPERTY abstract -> std::bool {\n        SET default := false;\n    };\n\n    # Backwards compatibility.\n    CREATE PROPERTY is_abstract := .abstract;\n\n    # Backwards compatibility. (But will maybe become a real property one day.)\n    CREATE PROPERTY final := false;\n\n    # Backwards compatibility.\n    CREATE PROPERTY is_final := .final;\n};\n\n\n# Base type for all *types*.\nCREATE ABSTRACT TYPE schema::Type EXTENDING schema::SubclassableObject;\nCREATE TYPE schema::PseudoType EXTENDING schema::Type;\n\nALTER TYPE schema::Type {\n    CREATE PROPERTY expr -> std::str;\n    CREATE PROPERTY from_alias -> bool;\n    # Backwards compatibility.\n    CREATE PROPERTY is_from_alias := .from_alias;\n};\n\n\nCREATE ABSTRACT LINK schema::reference {\n    CREATE PROPERTY owned -> std::bool;\n    # Backwards compatibility.\n    CREATE PROPERTY is_owned := @owned;\n};\n\n\nCREATE ABSTRACT LINK schema::ordered {\n    CREATE PROPERTY index -> std::int64;\n};\n\n\nCREATE TYPE schema::Module EXTENDING schema::Object;\n\n\nCREATE ABSTRACT TYPE schema::PrimitiveType EXTENDING schema::Type;\n\n\nCREATE ABSTRACT TYPE schema::CollectionType EXTENDING schema::PrimitiveType;\n\n\nCREATE TYPE schema::Array EXTENDING schema::CollectionType {\n    CREATE REQUIRED LINK element_type -> schema::Type;\n    CREATE PROPERTY dimensions -> array<std::int16>;\n};\n\n\nCREATE TYPE schema::ArrayExprAlias EXTENDING schema::Array;\n\n\nCREATE TYPE schema::TupleElement EXTENDING std::BaseObject {\n    CREATE REQUIRED LINK type -> schema::Type;\n    CREATE PROPERTY name -> std::str;\n};\n\n\nCREATE TYPE schema::Tuple EXTENDING schema::CollectionType {\n    CREATE REQUIRED PROPERTY named -> bool;\n    CREATE MULTI LINK element_types EXTENDING schema::ordered\n    -> schema::TupleElement {\n        CREATE CONSTRAINT std::exclusive;\n    }\n};\n\n\nCREATE TYPE schema::TupleExprAlias EXTENDING schema::Tuple;\n\n\nCREATE TYPE schema::Range EXTENDING schema::CollectionType {\n    CREATE REQUIRED LINK element_type -> schema::Type;\n};\n\n\nCREATE TYPE schema::RangeExprAlias EXTENDING schema::Range;\n\n\nCREATE TYPE schema::MultiRange EXTENDING schema::CollectionType {\n    CREATE REQUIRED LINK element_type -> schema::Type;\n};\n\n\nCREATE TYPE schema::MultiRangeExprAlias EXTENDING schema::MultiRange;\n\n\nCREATE TYPE schema::Delta EXTENDING schema::Object {\n    CREATE MULTI LINK parents -> schema::Delta;\n};\n\n\nCREATE ABSTRACT TYPE schema::AnnotationSubject EXTENDING schema::Object;\n\nCREATE TYPE schema::Annotation EXTENDING schema::AnnotationSubject {\n    CREATE PROPERTY inheritable -> std::bool;\n};\n\nALTER TYPE schema::AnnotationSubject {\n    CREATE MULTI LINK annotations EXTENDING schema::reference\n    -> schema::Annotation {\n        CREATE PROPERTY value -> std::str;\n        ON TARGET DELETE ALLOW;\n    };\n};\n\n\nCREATE ABSTRACT TYPE schema::InheritingObject\nEXTENDING schema::SubclassableObject {\n    CREATE MULTI LINK bases EXTENDING schema::ordered\n        -> schema::InheritingObject;\n    CREATE MULTI LINK ancestors EXTENDING schema::ordered\n        -> schema::InheritingObject;\n    CREATE PROPERTY inherited_fields -> array<std::str>;\n};\n\n\nCREATE TYPE schema::Parameter EXTENDING schema::Object {\n    CREATE REQUIRED LINK type -> schema::Type;\n    CREATE REQUIRED PROPERTY typemod -> schema::TypeModifier;\n    CREATE REQUIRED PROPERTY kind -> schema::ParameterKind;\n    CREATE REQUIRED PROPERTY num -> std::int64;\n    CREATE PROPERTY default -> std::str;\n};\n\n\nCREATE ABSTRACT TYPE schema::CallableObject\n    EXTENDING schema::AnnotationSubject\n{\n    CREATE MULTI LINK params EXTENDING schema::ordered -> schema::Parameter {\n        ON TARGET DELETE ALLOW;\n    };\n\n    CREATE LINK return_type -> schema::Type;\n    CREATE PROPERTY return_typemod -> schema::TypeModifier;\n};\n\n\nCREATE ABSTRACT TYPE schema::VolatilitySubject EXTENDING schema::Object {\n    CREATE PROPERTY volatility -> schema::Volatility {\n        # NOTE: this default indicates the default value in the python\n        # implementation, but is not itself a source of truth\n        SET default := 'Volatile';\n    };\n};\n\n\nCREATE TYPE schema::Constraint\n    EXTENDING schema::CallableObject, schema::InheritingObject\n{\n    ALTER LINK params {\n        CREATE PROPERTY value -> std::str;\n    };\n    CREATE PROPERTY expr -> std::str;\n    CREATE PROPERTY subjectexpr -> std::str;\n    CREATE PROPERTY finalexpr -> std::str;\n    CREATE PROPERTY errmessage -> std::str;\n    CREATE PROPERTY delegated -> std::bool;\n    CREATE PROPERTY except_expr -> std::str;\n};\n\n\nCREATE ABSTRACT TYPE schema::ConsistencySubject\n      EXTENDING schema::InheritingObject {\n    CREATE MULTI LINK constraints EXTENDING schema::reference\n    -> schema::Constraint {\n        CREATE CONSTRAINT std::exclusive;\n        ON TARGET DELETE ALLOW;\n    };\n};\n\n\nALTER TYPE schema::Constraint {\n    CREATE LINK subject -> schema::ConsistencySubject;\n};\n\n\nCREATE TYPE schema::Index\n    EXTENDING schema::InheritingObject, schema::AnnotationSubject\n{\n    CREATE PROPERTY expr -> std::str;\n    CREATE PROPERTY except_expr -> std::str;\n    CREATE PROPERTY deferrability -> schema::IndexDeferrability;\n    CREATE PROPERTY deferred -> std::bool;\n    CREATE PROPERTY active -> std::bool;\n    CREATE PROPERTY build_concurrently -> std::bool;\n    CREATE MULTI LINK params EXTENDING schema::ordered -> schema::Parameter {\n        ON TARGET DELETE ALLOW;\n    };\n    CREATE PROPERTY kwargs -> array<tuple<name: str, expr: str>>;\n};\n\n\nCREATE ABSTRACT TYPE schema::Source EXTENDING schema::Object {\n    CREATE MULTI LINK indexes EXTENDING schema::reference -> schema::Index {\n        CREATE CONSTRAINT std::exclusive;\n        ON TARGET DELETE ALLOW;\n    };\n};\n\n\nCREATE ABSTRACT TYPE schema::Pointer\n    EXTENDING\n        schema::ConsistencySubject,\n        schema::AnnotationSubject\n{\n    CREATE PROPERTY cardinality -> schema::Cardinality;\n    CREATE PROPERTY required -> std::bool;\n    CREATE PROPERTY readonly -> std::bool;\n    CREATE PROPERTY default -> std::str;\n    CREATE PROPERTY expr -> std::str;\n    CREATE PROPERTY secret -> std::bool;\n    CREATE PROPERTY splat_strategy -> schema::SplatStrategy;\n    CREATE PROPERTY linkful -> std::bool;\n    CREATE PROPERTY protected -> std::bool;\n};\n\n\nCREATE TYPE schema::AccessPolicy\n    EXTENDING\n        schema::InheritingObject, schema::AnnotationSubject;\n\n\nCREATE TYPE schema::Trigger\n    EXTENDING\n        schema::InheritingObject, schema::AnnotationSubject;\n\n\nCREATE TYPE schema::Rewrite\n    EXTENDING\n        schema::InheritingObject, schema::AnnotationSubject;\n\n\nALTER TYPE schema::Source {\n    CREATE MULTI LINK pointers EXTENDING schema::reference -> schema::Pointer {\n        CREATE CONSTRAINT std::exclusive;\n        ON TARGET DELETE ALLOW;\n    };\n};\n\n\nCREATE TYPE schema::Alias EXTENDING schema::AnnotationSubject\n{\n    CREATE REQUIRED PROPERTY expr -> std::str;\n    # This link is DEFINITELY not optional. This works around\n    # compiler weirdness that forces the DEFERRED RESTRICT\n    # behavior, which prohibits required-ness.\n    CREATE OPTIONAL LINK type -> schema::Type {\n        ON TARGET DELETE DEFERRED RESTRICT;\n    };\n};\n\n\nCREATE TYPE schema::ScalarType\n    EXTENDING\n        schema::PrimitiveType,\n        schema::ConsistencySubject,\n        schema::AnnotationSubject\n{\n    CREATE PROPERTY default -> std::str;\n    CREATE PROPERTY enum_values -> array<std::str>;\n    CREATE PROPERTY arg_values -> array<std::str>;\n};\n\n\nCREATE FUNCTION std::sequence_reset(\n    seq: schema::ScalarType,\n    value: std::int64,\n) -> std::int64\n{\n    SET volatility := 'Volatile';\n    USING SQL $$\n        SELECT\n            pg_catalog.setval(\n                pg_catalog.quote_ident(sn.schema)\n                    || '.' || pg_catalog.quote_ident(sn.name),\n                \"value\",\n                true\n            )\n        FROM\n            ROWS FROM (edgedb_VER.get_user_sequence_backend_name(\"seq\"))\n                AS sn(schema text, name text)\n    $$;\n};\n\n\nCREATE FUNCTION std::sequence_reset(\n    seq: schema::ScalarType,\n) -> std::int64\n{\n    SET volatility := 'Volatile';\n    USING SQL $$\n        SELECT\n            pg_catalog.setval(\n                pg_catalog.quote_ident(sn.schema)\n                    || '.' || pg_catalog.quote_ident(sn.name),\n                s.start_value,\n                false\n            )\n        FROM\n            ROWS FROM (edgedb_VER.get_user_sequence_backend_name(\"seq\"))\n                AS sn(schema text, name text),\n            LATERAL (\n                SELECT start_value\n                FROM pg_catalog.pg_sequences\n                WHERE schemaname = sn.schema AND sequencename = sn.name\n            ) AS s\n    $$;\n};\n\n\nCREATE FUNCTION std::sequence_next(\n    seq: schema::ScalarType,\n) -> std::int64\n{\n    SET volatility := 'Volatile';\n    USING SQL $$\n        SELECT\n            pg_catalog.nextval(\n                pg_catalog.quote_ident(sn.schema)\n                    || '.' || pg_catalog.quote_ident(sn.name)\n            )\n        FROM\n            ROWS FROM (edgedb_VER.get_user_sequence_backend_name(\"seq\"))\n                AS sn(schema text, name text)\n    $$;\n};\n\n\nCREATE TYPE schema::ObjectType\n    EXTENDING\n        schema::Source,\n        schema::ConsistencySubject,\n        schema::InheritingObject,\n        schema::Type,\n        schema::AnnotationSubject;\n\n\nALTER TYPE std::BaseObject {\n    # N.B: Since __type__ is uniquely determined by the type of the\n    # source object, as a special-case optimization we do not actually\n    # store it in the database. Instead, we inject it into the views\n    # we use to implement inheritance and inject it in the compiler\n    # when operating on tables directly.\n    CREATE REQUIRED LINK __type__ -> schema::ObjectType {\n        SET readonly := True;\n        SET protected := True;\n    };\n};\n\n\nALTER TYPE schema::ObjectType {\n    CREATE MULTI LINK union_of -> schema::ObjectType;\n    CREATE MULTI LINK intersection_of -> schema::ObjectType;\n    CREATE MULTI LINK access_policies\n            EXTENDING schema::reference -> schema::AccessPolicy {\n        CREATE CONSTRAINT std::exclusive;\n        ON TARGET DELETE ALLOW;\n    };\n    CREATE MULTI LINK triggers\n            EXTENDING schema::reference -> schema::Trigger {\n        CREATE CONSTRAINT std::exclusive;\n        ON TARGET DELETE ALLOW;\n    };\n    CREATE PROPERTY compound_type := (\n        EXISTS .union_of OR EXISTS .intersection_of\n    );\n    # Backwards compatibility.\n    CREATE PROPERTY is_compound_type := .compound_type;\n};\n\n\nALTER TYPE schema::AccessPolicy {\n  CREATE REQUIRED LINK subject -> schema::ObjectType;\n  CREATE MULTI PROPERTY access_kinds -> schema::AccessKind;\n  CREATE PROPERTY condition -> std::str;\n  CREATE REQUIRED PROPERTY action -> schema::AccessPolicyAction;\n  CREATE PROPERTY expr -> std::str;\n  CREATE PROPERTY errmessage -> std::str;\n};\n\n\nALTER TYPE schema::Trigger {\n  CREATE REQUIRED LINK subject -> schema::ObjectType;\n  CREATE REQUIRED PROPERTY timing -> schema::TriggerTiming;\n  CREATE MULTI PROPERTY kinds -> schema::TriggerKind;\n  CREATE REQUIRED PROPERTY scope -> schema::TriggerScope;\n  CREATE PROPERTY expr -> std::str;\n  CREATE PROPERTY condition -> std::str;\n};\n\nALTER TYPE schema::Rewrite {\n  CREATE REQUIRED LINK subject -> schema::Pointer;\n  CREATE REQUIRED PROPERTY kind -> schema::TriggerKind;\n  CREATE REQUIRED PROPERTY expr -> std::str;\n};\n\nCREATE TYPE schema::Link EXTENDING schema::Pointer, schema::Source;\n\n\nCREATE TYPE schema::Property EXTENDING schema::Pointer;\n\n\nALTER TYPE schema::Pointer {\n    CREATE LINK source -> schema::Source;\n    CREATE LINK target -> schema::Type;\n    CREATE MULTI LINK rewrites\n            EXTENDING schema::reference -> schema::Rewrite {\n        CREATE CONSTRAINT std::exclusive;\n        ON TARGET DELETE ALLOW;\n    };\n};\n\n\nALTER TYPE schema::Link {\n    ALTER LINK target\n        SET TYPE schema::ObjectType\n        USING (.target[IS schema::ObjectType]);\n    CREATE MULTI LINK properties := .pointers[IS schema::Property];\n    CREATE PROPERTY on_target_delete -> schema::TargetDeleteAction;\n    CREATE PROPERTY on_source_delete -> schema::SourceDeleteAction;\n};\n\n\nALTER TYPE schema::ObjectType {\n    CREATE MULTI LINK links := .pointers[IS schema::Link];\n    CREATE MULTI LINK properties := .pointers[IS schema::Property];\n};\n\n\nCREATE TYPE schema::Global EXTENDING schema::AnnotationSubject {\n    # This is most definitely NOT optional. It works around some\n    # compiler weirdness which requires the on target delete deferred restrict\n    CREATE OPTIONAL LINK target -> schema::Type {\n        ON TARGET DELETE DEFERRED RESTRICT;\n    };\n    CREATE PROPERTY required -> std::bool;\n    CREATE PROPERTY cardinality -> schema::Cardinality;\n    CREATE PROPERTY expr -> std::str;\n    CREATE PROPERTY default -> std::str;\n};\n\n\nCREATE TYPE schema::Permission\n    EXTENDING\n        schema::AnnotationSubject;\n\n\nCREATE TYPE schema::Function\n    EXTENDING schema::CallableObject, schema::VolatilitySubject\n{\n    CREATE PROPERTY preserves_optionality -> std::bool {\n        SET default := false;\n    };\n\n    CREATE PROPERTY body -> str;\n    CREATE REQUIRED PROPERTY language -> str;\n\n    CREATE MULTI LINK used_globals EXTENDING schema::ordered -> schema::Global;\n    CREATE MULTI LINK used_permissions EXTENDING schema::ordered\n        -> schema::Permission;\n\n    CREATE MULTI LINK required_permissions EXTENDING schema::ordered\n        -> schema::Permission;\n\n};\n\n\nCREATE TYPE schema::Operator\n    EXTENDING schema::CallableObject, schema::VolatilitySubject\n{\n    CREATE PROPERTY operator_kind -> schema::OperatorKind;\n    CREATE PROPERTY abstract -> std::bool {\n        SET default := false;\n    };\n    # Backwards compatibility.\n    CREATE PROPERTY is_abstract := .abstract;\n};\n\n\nCREATE TYPE schema::Cast\n    EXTENDING schema::AnnotationSubject, schema::VolatilitySubject\n{\n    CREATE LINK from_type -> schema::Type;\n    CREATE LINK to_type -> schema::Type;\n    CREATE PROPERTY allow_implicit -> std::bool;\n    CREATE PROPERTY allow_assignment -> std::bool;\n};\n\nCREATE TYPE schema::Migration\n    EXTENDING schema::AnnotationSubject\n{\n    CREATE MULTI LINK parents -> schema::Migration;\n    CREATE REQUIRED PROPERTY script -> str;\n    CREATE PROPERTY sdl -> str;\n    CREATE PROPERTY message -> str;\n    CREATE PROPERTY generated_by -> schema::MigrationGeneratedBy;\n};\n\n\n# The package link is added in sys.edgeql\nCREATE TYPE schema::Extension EXTENDING schema::AnnotationSubject;\n\nCREATE TYPE schema::FutureBehavior EXTENDING schema::Object;\n"
  },
  {
    "path": "edb/lib/std/00-prelude.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nCREATE MODULE std;\nCREATE MODULE ext;\n"
  },
  {
    "path": "edb/lib/std/10-scalars.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nCREATE PSEUDO TYPE `anytype`;\n\nCREATE PSEUDO TYPE `anytuple`;\n\nCREATE PSEUDO TYPE `anyobject`;\n\nCREATE ABSTRACT SCALAR TYPE std::anyscalar;\n\nCREATE ABSTRACT SCALAR TYPE std::anypoint EXTENDING std::anyscalar;\n\nCREATE ABSTRACT SCALAR TYPE std::anydiscrete EXTENDING std::anypoint;\n\nCREATE ABSTRACT SCALAR TYPE std::anycontiguous EXTENDING std::anypoint;\n\nCREATE SCALAR TYPE std::bool EXTENDING std::anyscalar;\n\nCREATE SCALAR TYPE std::bytes EXTENDING std::anyscalar;\n\nCREATE SCALAR TYPE std::uuid EXTENDING std::anyscalar;\n\nCREATE SCALAR TYPE std::str EXTENDING std::anyscalar;\n\nCREATE SCALAR TYPE std::json EXTENDING std::anyscalar;\n\nCREATE SCALAR TYPE std::datetime EXTENDING std::anycontiguous;\n\nCREATE SCALAR TYPE std::duration EXTENDING std::anycontiguous;\n\nCREATE ABSTRACT SCALAR TYPE std::anyreal EXTENDING std::anyscalar;\n\nCREATE ABSTRACT SCALAR TYPE std::anyint EXTENDING std::anyreal;\n\nCREATE SCALAR TYPE std::int16 EXTENDING std::anyint;\n\nCREATE SCALAR TYPE std::int32 EXTENDING std::anyint, std::anydiscrete;\n\nCREATE SCALAR TYPE std::int64 EXTENDING std::anyint, std::anydiscrete;\n\nCREATE ABSTRACT SCALAR TYPE std::anyfloat\n    EXTENDING std::anyreal, std::anycontiguous;\n\nCREATE SCALAR TYPE std::float32 EXTENDING std::anyfloat;\n\nCREATE SCALAR TYPE std::float64 EXTENDING std::anyfloat;\n\nCREATE ABSTRACT SCALAR TYPE std::anynumeric EXTENDING std::anyreal;\n\nCREATE SCALAR TYPE std::decimal EXTENDING std::anynumeric, std::anycontiguous;\n\nCREATE SCALAR TYPE std::bigint EXTENDING std::anynumeric, std::anyint;\n\nCREATE ABSTRACT SCALAR TYPE std::sequence EXTENDING std::int64;\n\nCREATE ABSTRACT SCALAR TYPE std::anyenum EXTENDING std::anyscalar;\n"
  },
  {
    "path": "edb/lib/std/15-attrs.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n## Generic annotations.\n\nCREATE ABSTRACT ANNOTATION std::description;\nALTER ABSTRACT ANNOTATION std::description {\n    CREATE ANNOTATION std::description := 'A short documentation string.';\n};\nCREATE ABSTRACT ANNOTATION std::title {\n    CREATE ANNOTATION std::description := 'A human-readable name.';\n};\nCREATE ABSTRACT ANNOTATION std::deprecated {\n    CREATE ANNOTATION std::description :=\n        'A marker that an item is deprecated.';\n};\nCREATE ABSTRACT ANNOTATION std::identifier;\n\nCREATE MODULE std::lang;\n\nCREATE MODULE std::lang::go;\nCREATE ABSTRACT ANNOTATION std::lang::go::type;\n\nCREATE MODULE std::lang::js;\nCREATE ABSTRACT ANNOTATION std::lang::js::type;\n\nCREATE MODULE std::lang::py;\nCREATE ABSTRACT ANNOTATION std::lang::py::type;\n\nCREATE MODULE std::lang::rs;\nCREATE ABSTRACT ANNOTATION std::lang::rs::type;\n"
  },
  {
    "path": "edb/lib/std/17-abstractops.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n# All EdgeDB types support ordering and comparison.\n# The below definitions of abstract operators declare this fact\n# for the benefit of generic expressions (e.g. in abstract constraints).\n\nCREATE ABSTRACT INFIX OPERATOR\nstd::`>=` (l: anytype, r: anytype) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'ge';\n};\n\nCREATE ABSTRACT INFIX OPERATOR\nstd::`>` (l: anytype, r: anytype) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n};\n\nCREATE ABSTRACT INFIX OPERATOR\nstd::`<=` (l: anytype, r: anytype) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'le';\n};\n\nCREATE ABSTRACT INFIX OPERATOR\nstd::`<` (l: anytype, r: anytype) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n};\n\nCREATE ABSTRACT INFIX OPERATOR\nstd::`=` (l: anytype, r: anytype) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n};\n\nCREATE ABSTRACT INFIX OPERATOR\nstd::`?=` (l: OPTIONAL anytype, r: OPTIONAL anytype) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n};\n\nCREATE ABSTRACT INFIX OPERATOR\nstd::`!=` (l: anytype, r: anytype) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'ne';\n};\n\nCREATE ABSTRACT INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL anytype, r: OPTIONAL anytype) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n};\n"
  },
  {
    "path": "edb/lib/std/20-genericfuncs.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n## Fundamental polymorphic functions\n\n\n# std::assert_single -- runtime cardinality assertion (upper bound)\n# -----------------------------------------------------------------\n\nCREATE FUNCTION\nstd::assert_single(\n    input: SET OF anytype,\n    NAMED ONLY message: OPTIONAL str = <str>{},\n) -> OPTIONAL anytype\n{\n    CREATE ANNOTATION std::description :=\n        \"Check that the input set contains at most one element, raise\n         CardinalityViolationError otherwise.\";\n    SET volatility := 'Immutable';\n    SET preserves_optionality := true;\n    USING SQL EXPRESSION;\n};\n\n\n# std::assert_exists -- runtime cardinality assertion (lower bound)\n# -----------------------------------------------------------------\n\nCREATE FUNCTION\nstd::assert_exists(\n    input: SET OF anytype,\n    NAMED ONLY message: OPTIONAL str = <str>{},\n) -> SET OF anytype\n{\n    CREATE ANNOTATION std::description :=\n        \"Check that the input set contains at least one element, raise\n         CardinalityViolationError otherwise.\";\n    SET volatility := 'Immutable';\n    SET preserves_upper_cardinality := true;\n    USING SQL EXPRESSION;\n};\n\n\n# std::assert_distinct -- runtime multiplicity assertion\n# ------------------------------------------------------\n\nCREATE FUNCTION\nstd::assert_distinct(\n    input: SET OF anytype,\n    NAMED ONLY message: OPTIONAL str = <str>{},\n) -> SET OF anytype\n{\n    CREATE ANNOTATION std::description :=\n        \"Check that the input set is a proper set, i.e. all elements\n         are unique\";\n    SET volatility := 'Immutable';\n    SET preserves_optionality := true;\n    SET preserves_upper_cardinality := true;\n    USING SQL EXPRESSION;\n};\n\n# std::assert -- boolean assertion\n# --------------------------------\nCREATE FUNCTION\nstd::assert(\n    input: bool,\n    NAMED ONLY message: OPTIONAL str = <str>{},\n) -> bool\n{\n    CREATE ANNOTATION std::description :=\n        \"Assert that a boolean value is true.\";\n    SET volatility := 'Stable';\n    USING SQL $$\n    SELECT (\n        edgedb_VER.raise_on_null(\n            nullif(\"input\", false),\n            'cardinality_violation',\n            \"constraint\" => 'std::assert',\n            msg => coalesce(\"message\", 'assertion failed')\n        )\n    )\n    $$;\n};\n\n# std::materialized_exists -- force materialization of a set\n# ----------------------------------------------------------\n\nCREATE FUNCTION\nstd::materialized(\n    input: anytype,\n) -> anytype\n{\n    CREATE ANNOTATION std::description :=\n        \"Force materialization of a set.\";\n    SET volatility := 'Volatile';\n    USING SQL EXPRESSION;\n};\n\n\n# std::len\n# --------\n\nCREATE FUNCTION\nstd::len(str: std::str) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'A polymorphic function to calculate a \"length\" of its first argument.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT char_length(\"str\")::bigint\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::len(bytes: std::bytes) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'A polymorphic function to calculate a \"length\" of its first argument.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT length(\"bytes\")::bigint\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::len(array: array<anytype>) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'A polymorphic function to calculate a \"length\" of its first argument.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT cardinality(\"array\")::bigint\n    $$;\n};\n\n\n# std::sum\n# --------\n\nCREATE FUNCTION\nstd::sum(s: SET OF std::bigint) -> std::bigint\n{\n    CREATE ANNOTATION std::description :=\n        'Return the arithmetic sum of values in a set.';\n    SET volatility := 'Immutable';\n    SET initial_value := 0;\n    SET force_return_cast := true;\n    USING SQL FUNCTION 'sum';\n};\n\n\nCREATE FUNCTION\nstd::sum(s: SET OF std::decimal) -> std::decimal\n{\n    CREATE ANNOTATION std::description :=\n        'Return the arithmetic sum of values in a set.';\n    SET volatility := 'Immutable';\n    SET initial_value := 0;\n    USING SQL FUNCTION 'sum';\n};\n\n\nCREATE FUNCTION\nstd::sum(s: SET OF std::int32) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the arithmetic sum of values in a set.';\n    SET volatility := 'Immutable';\n    SET initial_value := 0;\n    SET force_return_cast := true;\n    USING SQL FUNCTION 'sum';\n};\n\n\nCREATE FUNCTION\nstd::sum(s: SET OF std::int64) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the arithmetic sum of values in a set.';\n    SET volatility := 'Immutable';\n    SET initial_value := 0;\n    SET force_return_cast := true;\n    USING SQL FUNCTION 'sum';\n};\n\n\nCREATE FUNCTION\nstd::sum(s: SET OF std::float32) -> std::float32\n{\n    CREATE ANNOTATION std::description :=\n        'Return the arithmetic sum of values in a set.';\n    SET volatility := 'Immutable';\n    SET initial_value := 0;\n    USING SQL FUNCTION 'sum';\n};\n\n\nCREATE FUNCTION\nstd::sum(s: SET OF std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the arithmetic sum of values in a set.';\n    SET volatility := 'Immutable';\n    SET initial_value := 0;\n    USING SQL FUNCTION 'sum';\n};\n\n\n# std::count\n# ----------\n\nCREATE FUNCTION\nstd::count(s: SET OF anytype) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'Return the number of elements in a set.';\n    SET volatility := 'Immutable';\n    SET initial_value := 0;\n    USING SQL FUNCTION 'count';\n};\n\n\n# std::random\n# -----------\n\nCREATE FUNCTION\nstd::random() -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Return a pseudo-random number in the range `0.0 <= x < 1.0`';\n    SET volatility := 'Volatile';\n    USING SQL FUNCTION 'random';\n};\n\n\n# std::min\n# --------\n\nCREATE FUNCTION\nstd::min(vals: SET OF anytype) -> OPTIONAL anytype\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET fallback := true;\n    SET preserves_optionality := true;\n    USING SQL EXPRESSION;\n};\n\n\n# Postgres only implements min and max for specific scalars and their\n# respective arrays, but in EdgeDB every type is orderable and so\n# minimum and maximum value can be determined for all types. The\n# general catch-all using `anytype` above is valid for all types, but\n# it is somewhat slower than the specialized natively implemented min\n# and max aggregates. So for the types that Postgres supports, we want\n# to use the more specialized implementation.\n#\n# Turns out that the min/max implementation for arrays is not\n# noticeably faster than the fallback we use, so there's no\n# specialized version of it in the polymorphic implementations.\nCREATE FUNCTION\nstd::min(vals: SET OF anyreal) -> OPTIONAL anyreal\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'min';\n};\n\n\nCREATE FUNCTION\nstd::min(vals: SET OF anyenum) -> OPTIONAL anyenum\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'min';\n};\n\n\nCREATE FUNCTION\nstd::min(vals: SET OF str) -> OPTIONAL str\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'min';\n};\n\n\nCREATE FUNCTION\nstd::min(vals: SET OF datetime) -> OPTIONAL datetime\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'min';\n};\n\n\nCREATE FUNCTION\nstd::min(vals: SET OF duration) -> OPTIONAL duration\n{\n    CREATE ANNOTATION std::description :=\n        'Return the smallest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'min';\n};\n\n\n# std::max\n# --------\n\nCREATE FUNCTION\nstd::max(vals: SET OF anytype) -> OPTIONAL anytype\n{\n    CREATE ANNOTATION std::description :=\n        'Return the greatest value of the input set.';\n    SET volatility := 'Immutable';\n    SET fallback := true;\n    SET preserves_optionality := true;\n    USING SQL EXPRESSION;\n};\n\n\n# Postgres only implements min and max for specific scalars and their\n# respective arrays, but in EdgeDB every type is orderable and so\n# minimum and maximum value can be determined for all types. The\n# general catch-all using `anytype` above is valid for all types, but\n# it is somewhat slower than the specialized natively implemented min\n# and max aggregates. So for the types that Postgres supports, we want\n# to use the more specialized implementation.\n#\n# Turns out that the min/max implementation for arrays is not\n# noticeably faster than the fallback we use, so there's no\n# specialized version of it in the polymorphic implementations.\nCREATE FUNCTION\nstd::max(vals: SET OF anyreal) -> OPTIONAL anyreal\n{\n    CREATE ANNOTATION std::description :=\n        'Return the greatest value of the input set.';\n    SET volatility := 'Immutable';\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'max';\n};\n\n\nCREATE FUNCTION\nstd::max(vals: SET OF anyenum) -> OPTIONAL anyenum\n{\n    CREATE ANNOTATION std::description :=\n        'Return the greatest value of the input set.';\n    SET volatility := 'Immutable';\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'max';\n};\n\n\nCREATE FUNCTION\nstd::max(vals: SET OF str) -> OPTIONAL str\n{\n    CREATE ANNOTATION std::description :=\n        'Return the greatest value of the input set.';\n    SET volatility := 'Immutable';\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'max';\n};\n\n\nCREATE FUNCTION\nstd::max(vals: SET OF datetime) -> OPTIONAL datetime\n{\n    CREATE ANNOTATION std::description :=\n        'Return the greatest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'max';\n};\n\n\nCREATE FUNCTION\nstd::max(vals: SET OF duration) -> OPTIONAL duration\n{\n    CREATE ANNOTATION std::description :=\n        'Return the greatest value of the input set.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    SET preserves_optionality := true;\n    USING SQL FUNCTION 'max';\n};\n\n\n# std::all\n# --------\n\nCREATE FUNCTION\nstd::all(vals: SET OF std::bool) -> std::bool\n{\n    CREATE ANNOTATION std::description :=\n        'Generalized boolean `AND` applied to the set of *values*.';\n    SET volatility := 'Immutable';\n    SET initial_value := True;\n    USING SQL FUNCTION 'bool_and';\n};\n\n\n# std::any\n# --------\n\nCREATE FUNCTION\nstd::any(vals: SET OF std::bool) -> std::bool\n{\n    CREATE ANNOTATION std::description :=\n        'Generalized boolean `OR` applied to the set of *values*.';\n    SET volatility := 'Immutable';\n    SET initial_value := False;\n    USING SQL FUNCTION 'bool_or';\n};\n\n\n# std::enumerate\n# --------------\n\nCREATE FUNCTION\nstd::enumerate(\n    vals: SET OF anytype\n) -> SET OF tuple<std::int64, anytype>\n{\n    CREATE ANNOTATION std::description :=\n        'Return a set of tuples of the form `(index, element)`.';\n    SET volatility := 'Immutable';\n    SET preserves_optionality := true;\n    SET preserves_upper_cardinality := true;\n    USING SQL EXPRESSION;\n};\n\n\n# std::round\n# ----------\n\nCREATE FUNCTION\nstd::round(val: std::int64) -> std::int64\n{\n    CREATE ANNOTATION std::description := 'Round to the nearest value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT \"val\"\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::round(val: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::description := 'Round to the nearest value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT round(\"val\")\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::round(val: std::bigint) -> std::bigint\n{\n    CREATE ANNOTATION std::description := 'Round to the nearest value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT \"val\";\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::round(val: std::decimal) -> std::decimal\n{\n    CREATE ANNOTATION std::description := 'Round to the nearest value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT round(\"val\");\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::round(val: std::decimal, d: std::int64) -> std::decimal\n{\n    CREATE ANNOTATION std::description := 'Round to the nearest value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT round(\"val\", \"d\"::int4)\n    $$;\n};\n\n\n# std::contains\n# ---------\n\nCREATE FUNCTION\nstd::contains(haystack: std::str, needle: std::str) -> std::bool\n{\n    CREATE ANNOTATION std::description :=\n        'A polymorphic function to test if a sequence contains a certain element.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        -- There was a regression in 12.0 (fixed in 12.1): strpos\n        -- started to report 0 for empty search strings:\n        -- https://postgr.es/m/CADT4RqAz7oN4vkPir86Kg1_mQBmBxCp-L_=9vRpgSNPJf0KRkw@mail.gmail.com\n        --\n        -- This CASE..WHEN fixes this edge case.\n        CASE\n            WHEN \"needle\" = '' THEN 1\n            ELSE strpos(\"haystack\", \"needle\")\n        END\n    ) != 0\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::contains(haystack: std::bytes, needle: std::bytes) -> std::bool\n{\n    CREATE ANNOTATION std::description :=\n        'A polymorphic function to test if a sequence contains a certain element.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT position(\"needle\" in \"haystack\") != 0\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::contains(haystack: array<anytype>, needle: anytype) -> std::bool\n{\n    CREATE ANNOTATION std::description :=\n        'A polymorphic function to test if a sequence contains a certain element.';\n    SET volatility := 'Immutable';\n    # Postgres only manages to inline this function if it isn't marked strict,\n    # and we want it to be inlined so that std::pg::gin indexes work with it.\n    SET impl_is_strict := false;\n    USING SQL $$\n    SELECT \"haystack\" @> ARRAY[\"needle\"]\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::contains(haystack: json, needle: json) -> std::bool\n{\n    CREATE ANNOTATION std::description :=\n        'A polymorphic function to test if one JSON value contains another JSON value.';\n    SET volatility := 'Immutable';\n    # Postgres only manages to inline this function if it isn't marked strict,\n    # and we want it to be inlined so that std::pg::gin indexes work with it.\n    SET impl_is_strict := false;\n    USING SQL $$\n    SELECT \"haystack\" @> \"needle\"\n    $$;\n};\n\n\n# std::find\n# ---------\n\nCREATE FUNCTION\nstd::find(haystack: std::str, needle: std::str) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'A polymorphic function to find index of an element in a sequence.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        -- There was a regression in 12.0 (fixed in 12.1): strpos\n        -- started to report 0 for empty search strings:\n        -- https://postgr.es/m/CADT4RqAz7oN4vkPir86Kg1_mQBmBxCp-L_=9vRpgSNPJf0KRkw@mail.gmail.com\n        --\n        -- This CASE..WHEN fixes this edge case.\n        CASE\n            WHEN \"needle\" = '' THEN 0\n            ELSE strpos(\"haystack\", \"needle\") - 1\n        END\n    )::int8\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::find(haystack: std::bytes, needle: std::bytes) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'A polymorphic function to find index of an element in a sequence.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (position(\"needle\" in \"haystack\") - 1)::int8\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::find(haystack: array<anytype>, needle: anytype,\n          from_pos: std::int64=0) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'A polymorphic function to find index of an element in a sequence.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT COALESCE(\n        array_position(\"haystack\", \"needle\", (\"from_pos\"::int4 + 1)::int4) - 1,\n        -1)::int8\n    $$;\n};\n\n\n# Generic comparison operators\n# ----------------------------\n\nCREATE INFIX OPERATOR\nstd::`=` (l: anyscalar, r: anyscalar) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL anyscalar, r: OPTIONAL anyscalar) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: anyscalar, r: anyscalar) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL anyscalar, r: OPTIONAL anyscalar) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: anyscalar, r: anyscalar) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR '>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: anyscalar, r: anyscalar) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR '>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: anyscalar, r: anyscalar) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR '<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: anyscalar, r: anyscalar) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR '<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: anytuple, r: anytuple) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR '=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL anytuple, r: OPTIONAL anytuple) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n    SET recursive := true;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: anytuple, r: anytuple) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR '<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL anytuple, r: OPTIONAL anytuple) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n    SET recursive := true;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: anytuple, r: anytuple) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR '>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: anytuple, r: anytuple) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR '>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: anytuple, r: anytuple) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR '<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: anytuple, r: anytuple) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR '<';\n};\n"
  },
  {
    "path": "edb/lib/std/25-booloperators.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## Standard boolean operators\n## --------------------------\n\n\nCREATE INFIX OPERATOR\nstd::`OR` (a: std::bool, b: std::bool) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'or';\n    CREATE ANNOTATION std::description := 'Logical disjunction.';\n    SET volatility := 'Immutable';\n    SET impl_is_strict := false;\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`AND` (a: std::bool, b: std::bool) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'and';\n    CREATE ANNOTATION std::description := 'Logical conjunction.';\n    SET volatility := 'Immutable';\n    SET impl_is_strict := false;\n    USING SQL EXPRESSION;\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`NOT` (v: std::bool) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'not';\n    CREATE ANNOTATION std::description := 'Logical negation.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::bool, r: std::bool) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::bool, r: OPTIONAL std::bool) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::bool, r: std::bool) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::bool, r: OPTIONAL std::bool) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::bool, r: std::bool) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR '>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::bool, r: std::bool) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR '>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::bool, r: std::bool) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR '<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::bool, r: std::bool) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR '<';\n};\n\n\n## Boolean casts\n## -------------\n\nCREATE CAST FROM std::str TO std::bool {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'edgedb.str_to_bool';\n};\n\n\nCREATE CAST FROM std::bool TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n"
  },
  {
    "path": "edb/lib/std/25-enumoperators.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## Standard enum operators\n## -----------------------\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::anyenum, r: std::anyenum) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::anyenum, r: OPTIONAL std::anyenum) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::anyenum, r: std::anyenum) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::anyenum, r: OPTIONAL std::anyenum) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::anyenum, r: std::anyenum) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR '>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::anyenum, r: std::anyenum) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR '>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::anyenum, r: std::anyenum) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR '<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::anyenum, r: std::anyenum) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR '<';\n};\n\n\n## Enum casts\n## ----------\n\n# The only way to create an enum is to cast a str into it, so it makes\n# sense to create an implicit assignment cast.\nCREATE CAST FROM std::str TO std::anyenum {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n    ALLOW ASSIGNMENT;\n};\n\n\nCREATE CAST FROM std::anyenum TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\nCREATE CAST FROM std::anyenum TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL \"SELECT to_jsonb(val::text)\"\n};\n\n# Handled in compile_cast\nCREATE CAST FROM std::json TO std::anyenum {\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n"
  },
  {
    "path": "edb/lib/std/25-numoperators.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## Standard numeric operators\n## --------------------------\n\n# NOTE: we follow PostgreSQL in creating an explicit operator\n# for each permutation of common integer and floating-point\n# operand types to avoid casting overhead, as these operations\n# are very common.\n#\n# Our implicit casts do not coincide with PostgreSQL. In particular we\n# do not implicitly cast between decimals and floats. The philosophy\n# behind that is that using decimal arithmetic should be opt-in. On\n# the other hand, if decimals are used they should not be accidentally\n# switched to floating point arithmetic. One of the consequences of\n# this is that we need to explicitly define arithmetic operators for\n# every legal combination of floats and decimals as unlike PostgreSQL\n# we cannot rely on implicit casts between decimals and other numeric\n# types.\n#\n# Floating point numbers are inherently imprecise. This means that\n# casting a given float into another representation and back may yield\n# a different value. This is especially important with float and\n# decimal casts as both directions can lose precision. Discussion\n# about precision loss of float to numeric casts can be found here:\n# https://www.postgresql.org/message-id/5A937D7E.60305%40anastigmatix.net\n\n# EQUALITY\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::int16, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::int16, r: OPTIONAL std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::int16, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::int16, r: OPTIONAL std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::int16, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::int16, r: OPTIONAL std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::int32, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::int32, r: OPTIONAL std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::int32, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::int32, r: OPTIONAL std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::int32, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::int32, r: OPTIONAL std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::int64, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::int64, r: OPTIONAL std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::int64, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::int64, r: OPTIONAL std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::int64, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::int64, r: OPTIONAL std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::float32, r: std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::float32, r: OPTIONAL std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::float32, r: std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::float32, r: OPTIONAL std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::float64, r: std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::float64, r: OPTIONAL std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::float64, r: std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::float64, r: OPTIONAL std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::bigint, r: std::bigint) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=(numeric,numeric)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::decimal, r: std::decimal) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::decimal, r: std::anyint) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::bigint, r: OPTIONAL std::bigint) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::decimal, r: OPTIONAL std::decimal) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::decimal, r: OPTIONAL std::anyint) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::anyint, r: std::decimal) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::anyint, r: OPTIONAL std::decimal) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\n# INEQUALITY\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::int16, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::int16, r: OPTIONAL std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::int16, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::int16, r: OPTIONAL std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::int16, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::int16, r: OPTIONAL std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::int32, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::int32, r: OPTIONAL std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::int32, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::int32, r: OPTIONAL std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::int32, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::int32, r: OPTIONAL std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::int64, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::int64, r: OPTIONAL std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::int64, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::int64, r: OPTIONAL std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::int64, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::int64, r: OPTIONAL std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::float32, r: std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::float32, r: OPTIONAL std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::float32, r: std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::float32, r: OPTIONAL std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::float64, r: std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::float64, r: OPTIONAL std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::float64, r: std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::float64, r: OPTIONAL std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::bigint, r: std::bigint) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>(numeric,numeric)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::decimal, r: std::decimal) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::decimal, r: std::anyint) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::bigint, r: OPTIONAL std::bigint) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::decimal, r: OPTIONAL std::decimal) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::decimal, r: OPTIONAL std::anyint) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::anyint, r: std::decimal) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::anyint, r: OPTIONAL std::decimal) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\n\n# GREATER THAN\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::int16, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::int16, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::int16, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::int32, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::int32, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::int32, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::int32, r: std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR '>(float8,float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::int64, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::int64, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::int64, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::int64, r: std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>(float8,float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::float32, r: std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::float32, r: std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::float32, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR '>(float8,float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::float64, r: std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::float64, r: std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::float64, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>(float8,float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::bigint, r: std::bigint) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>(numeric,numeric)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::decimal, r: std::decimal) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::anyint, r: std::decimal) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::decimal, r: std::anyint) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\n# GREATER OR EQUAL\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::int16, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::int16, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::int16, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::int32, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::int32, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::int32, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::int32, r: std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR '>=(float8,float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::int64, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::int64, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::int64, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::int64, r: std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=(float8,float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::float32, r: std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::float32, r: std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::float32, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR '>=(float8,float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::float64, r: std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::float64, r: std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::float64, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=(float8,float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::bigint, r: std::bigint) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=(numeric,numeric)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::decimal, r: std::decimal) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::anyint, r: std::decimal) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::decimal, r: std::anyint) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\n\n# LESS THAN\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::int16, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::int16, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::int16, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::int32, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::int32, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::int32, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::int32, r: std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR '<(float8,float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::int64, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::int64, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::int64, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::int64, r: std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<(float8,float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::float32, r: std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::float32, r: std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::float32, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR '<(float8,float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::float64, r: std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::float64, r: std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::float64, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<(float8,float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::bigint, r: std::bigint) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<(numeric,numeric)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::decimal, r: std::decimal) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::anyint, r: std::decimal) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::decimal, r: std::anyint) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\n# LESS THAN OR EQUAL\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::int16, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::int16, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::int16, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::int32, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::int32, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::int32, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::int32, r: std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR '<=(float8,float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::int64, r: std::int16) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::int64, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::int64, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::int64, r: std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=(float8,float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::float32, r: std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::float32, r: std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::float32, r: std::int32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR '<=(float8,float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::float64, r: std::float32) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::float64, r: std::float64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::float64, r: std::int64) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=(float8,float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::bigint, r: std::bigint) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=(numeric,numeric)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::decimal, r: std::decimal) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::anyint, r: std::decimal) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::decimal, r: std::anyint) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\n# INFIX PLUS\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::int16, r: std::int16) -> std::int16 {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description := 'Arithmetic addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL OPERATOR r'+';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::int32, r: std::int32) -> std::int32 {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description := 'Arithmetic addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL OPERATOR r'+';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::int64, r: std::int64) -> std::int64 {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description := 'Arithmetic addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL OPERATOR r'+';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::float32, r: std::float32) -> std::float32 {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description := 'Arithmetic addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL OPERATOR r'+';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::float64, r: std::float64) -> std::float64 {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description := 'Arithmetic addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL OPERATOR r'+';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::bigint, r: std::bigint) -> std::bigint {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description := 'Arithmetic addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    SET force_return_cast := true;\n    USING SQL OPERATOR r'+(numeric,numeric)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::decimal, r: std::decimal) -> std::decimal {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description := 'Arithmetic addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL OPERATOR r'+';\n};\n\n\n# PREFIX PLUS\n\nCREATE PREFIX OPERATOR\nstd::`+` (l: std::int16) -> std::int16 {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description := 'Arithmetic addition.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'+';\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`+` (l: std::int32) -> std::int32 {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description := 'Arithmetic addition.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'+';\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`+` (l: std::int64) -> std::int64 {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description := 'Arithmetic addition.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'+';\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`+` (l: std::float32) -> std::float32 {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description := 'Arithmetic addition.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'+';\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`+` (l: std::float64) -> std::float64 {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description := 'Arithmetic addition.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'+';\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`+` (l: std::bigint) -> std::bigint {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description := 'Arithmetic addition.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    USING SQL OPERATOR r'+(,numeric)';\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`+` (l: std::decimal) -> std::decimal {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description := 'Arithmetic addition.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'+';\n};\n\n\n\n# INFIX MINUS\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::int16, r: std::int16) -> std::int16 {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Arithmetic subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'-';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::int32, r: std::int32) -> std::int32 {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Arithmetic subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'-';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::int64, r: std::int64) -> std::int64 {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Arithmetic subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'-';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::float32, r: std::float32) -> std::float32 {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Arithmetic subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'-';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::float64, r: std::float64) -> std::float64 {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Arithmetic subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'-';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::bigint, r: std::bigint) -> std::bigint {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Arithmetic subtraction.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    USING SQL OPERATOR r'-(numeric,numeric)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::decimal, r: std::decimal) -> std::decimal {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Arithmetic subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'-';\n};\n\n\n# PREFIX MINUS\n\nCREATE PREFIX OPERATOR\nstd::`-` (l: std::int16) -> std::int16 {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Arithmetic subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'-';\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`-` (l: std::int32) -> std::int32 {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Arithmetic subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'-';\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`-` (l: std::int64) -> std::int64 {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Arithmetic subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'-';\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`-` (l: std::float32) -> std::float32 {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Arithmetic subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'-';\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`-` (l: std::float64) -> std::float64 {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Arithmetic subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'-';\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`-` (l: std::bigint) -> std::bigint {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Arithmetic subtraction.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    USING SQL OPERATOR r'-(,numeric)';\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`-` (l: std::decimal) -> std::decimal {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Arithmetic subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'-';\n};\n\n\n# MUL\n\nCREATE INFIX OPERATOR\nstd::`*` (l: std::int16, r: std::int16) -> std::int16 {\n    CREATE ANNOTATION std::identifier := 'mult';\n    CREATE ANNOTATION std::description := 'Arithmetic multiplication.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::*';\n    USING SQL OPERATOR r'*';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`*` (l: std::int32, r: std::int32) -> std::int32 {\n    CREATE ANNOTATION std::identifier := 'mult';\n    CREATE ANNOTATION std::description := 'Arithmetic multiplication.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::*';\n    USING SQL OPERATOR r'*';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`*` (l: std::int64, r: std::int64) -> std::int64 {\n    CREATE ANNOTATION std::identifier := 'mult';\n    CREATE ANNOTATION std::description := 'Arithmetic multiplication.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::*';\n    USING SQL OPERATOR r'*';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`*` (l: std::float32, r: std::float32) -> std::float32 {\n    CREATE ANNOTATION std::identifier := 'mult';\n    CREATE ANNOTATION std::description := 'Arithmetic multiplication.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::*';\n    USING SQL OPERATOR r'*';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`*` (l: std::float64, r: std::float64) -> std::float64 {\n    CREATE ANNOTATION std::identifier := 'mult';\n    CREATE ANNOTATION std::description := 'Arithmetic multiplication.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::*';\n    USING SQL OPERATOR r'*';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`*` (l: std::bigint, r: std::bigint) -> std::bigint {\n    CREATE ANNOTATION std::identifier := 'mult';\n    CREATE ANNOTATION std::description := 'Arithmetic multiplication.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::*';\n    SET force_return_cast := true;\n    USING SQL OPERATOR r'*(numeric,numeric)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`*` (l: std::decimal, r: std::decimal) -> std::decimal {\n    CREATE ANNOTATION std::identifier := 'mult';\n    CREATE ANNOTATION std::description := 'Arithmetic multiplication.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::*';\n    USING SQL OPERATOR r'*';\n};\n\n\n# DIV\n\nCREATE INFIX OPERATOR\nstd::`/` (l: std::int64, r: std::int64) -> std::float64 {\n    CREATE ANNOTATION std::identifier := 'div';\n    CREATE ANNOTATION std::description := 'Arithmetic division.';\n    SET volatility := 'Immutable';\n    # We need both USING SQL OPERATOR and USING SQL to copy\n    # the common attributes of the SQL division operator while\n    # overriding the main operator function.\n    USING SQL OPERATOR r'/';\n    USING SQL 'SELECT \"l\" / (\"r\"::float8)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`/` (l: std::float32, r: std::float32) -> std::float32 {\n    CREATE ANNOTATION std::identifier := 'div';\n    CREATE ANNOTATION std::description := 'Arithmetic division.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'/';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`/` (l: std::float64, r: std::float64) -> std::float64 {\n    CREATE ANNOTATION std::identifier := 'div';\n    CREATE ANNOTATION std::description := 'Arithmetic division.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'/';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`/` (l: std::decimal, r: std::decimal) -> std::decimal {\n    CREATE ANNOTATION std::identifier := 'div';\n    CREATE ANNOTATION std::description := 'Arithmetic division.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'/';\n};\n\n\n# FLOORDIV\n\n# PostgreSQL uses truncation division, so the -12 % 5 is -2, because -12 // 5\n# is -2, but EdgeQL uses floor division, so -12 // 5 is -3, and so -12 % 5\n# must be 3. The correct divmod behavior is implemented via the floor\n# function working specifically with numeric type. The numeric value needs to\n# be forced into using arbitrary precision by getting multiplied by\n# 1.0::numeric.\n\nCREATE INFIX OPERATOR\nstd::`//` (n: std::int16, d: std::int16) -> std::int16\n{\n    CREATE ANNOTATION std::identifier := 'floordiv';\n    CREATE ANNOTATION std::description :=\n        'Floor division. Result is rounded down to the nearest integer';\n    SET volatility := 'Immutable';\n    # We need both USING SQL OPERATOR and USING SQL FUNCTION to copy\n    # the common attributes of the SQL division operator while\n    # overriding the main operator function.\n    USING SQL OPERATOR r'/';\n    USING SQL\n        'SELECT floor(1.0::numeric * \"n\"::numeric / \"d\"::numeric)::int2';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`//` (n: std::int32, d: std::int32) -> std::int32\n{\n    CREATE ANNOTATION std::identifier := 'floordiv';\n    CREATE ANNOTATION std::description :=\n        'Floor division. Result is rounded down to the nearest integer';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'/';\n    USING SQL\n        'SELECT floor(1.0::numeric * \"n\"::numeric / \"d\"::numeric)::int4';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`//` (n: std::int64, d: std::int64) -> std::int64\n{\n    CREATE ANNOTATION std::identifier := 'floordiv';\n    CREATE ANNOTATION std::description :=\n        'Floor division. Result is rounded down to the nearest integer';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'/';\n    USING SQL\n        'SELECT floor(1.0::numeric * \"n\"::numeric / \"d\"::numeric)::int8';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`//` (n: std::float32, d: std::float32) -> std::float32\n{\n    CREATE ANNOTATION std::identifier := 'floordiv';\n    CREATE ANNOTATION std::description :=\n        'Floor division. Result is rounded down to the nearest integer';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'/';\n    USING SQL 'SELECT floor(\"n\" / \"d\")::float4';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`//` (n: std::float64, d: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::identifier := 'floordiv';\n    CREATE ANNOTATION std::description :=\n        'Floor division. Result is rounded down to the nearest integer';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'/';\n    USING SQL 'SELECT floor(\"n\" / \"d\")';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`//` (n: std::bigint, d: std::bigint) -> std::bigint\n{\n    CREATE ANNOTATION std::identifier := 'floordiv';\n    CREATE ANNOTATION std::description :=\n        'Floor division. Result is rounded down to the nearest integer';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'/(numeric,numeric)';\n    USING SQL $$\n        SELECT floor(\n            1.0::numeric * \"n\"::numeric / \"d\"::numeric\n        )::edgedbt.bigint_t;\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`//` (n: std::decimal, d: std::decimal) -> std::decimal\n{\n    CREATE ANNOTATION std::identifier := 'floordiv';\n    CREATE ANNOTATION std::description :=\n        'Floor division. Result is rounded down to the nearest integer';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'/';\n    USING SQL 'SELECT floor(\"n\" / \"d\");'\n};\n\n\n# MODULO\n\n# We have 2 issues to deal with:\n# 1) Postgres will produce a negative remainder for a posisitve divisor,\n#    whereas generally it's a bit more intuitive to have the remainder in the\n#    range [0, divisor).\n# 2) When implementing the modulo operator we need to make sure that addition\n#    or subtraction doesn't cause an overflow.\n#\n# The easiest way to avoid overflow errors is to upcast values to a larger\n# integer type. However, upcasting int64 to bigint and back is very slow\n# (5x-6x slower), so we need a different approach here.\n#\n# The breakdown is like this:\n# - We only want to add `d` if `n` and `d` have opposite signs.\n# - XOR helps to isolate the sign bit if it is different.\n# - Right arithmetic shift by 63 bits produces an \"all 1\" bitmask for\n#   negative integers and 0 otherwise.\n# - Performing AND using the above bitmask makes `d` go away if\n#   `sign(n) = sign(d)` and keeps it as is otherwise.\n# - Finally we want to perform another MOD `d` operation to address the corner\n#   case of 10 % -5 = -5 instead of 0 (which is equivalent, but does not\n#   conform to making 0 inclusive and `d` itself exclusive).\n#\n# According to our microbenchmarks this kind of bit magic is no worse and\n# maybe slightly better than upcasting for int16 and int32 cases.\n\nCREATE INFIX OPERATOR\nstd::`%` (n: std::int16, d: std::int16) -> std::int16\n{\n    CREATE ANNOTATION std::identifier := 'mod';\n    CREATE ANNOTATION std::description := 'Remainder from division (modulo).';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'%';\n    USING SQL $$\n        SELECT (\n            (n % d)\n            +\n            (d & ((n # d)>>15::int4))\n        ) % d\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`%` (n: std::int32, d: std::int32) -> std::int32\n{\n    CREATE ANNOTATION std::identifier := 'mod';\n    CREATE ANNOTATION std::description := 'Remainder from division (modulo).';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'%';\n    USING SQL $$\n        SELECT (\n            (n % d)\n            +\n            (d & ((n # d)>>31::int4))\n        ) % d\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`%` (n: std::int64, d: std::int64) -> std::int64\n{\n    CREATE ANNOTATION std::identifier := 'mod';\n    CREATE ANNOTATION std::description := 'Remainder from division (modulo).';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'%';\n    USING SQL $$\n        SELECT (\n            (n % d)\n            +\n            (d & ((n # d)>>63::int4))\n        ) % d\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`%` (n: std::float32, d: std::float32) -> std::float32\n{\n    CREATE ANNOTATION std::identifier := 'mod';\n    CREATE ANNOTATION std::description := 'Remainder from division (modulo).';\n    SET volatility := 'Immutable';\n    # We cheat here a bit by copying most of SQL operator metadata\n    # from the `/` operator, since there is no float % in Postgres.\n    USING SQL OPERATOR r'/';\n    USING SQL $$\n        SELECT n - floor(n / d)::float4 * d;\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`%` (n: std::float64, d: std::float64) -> std::float64\n{\n    CREATE ANNOTATION std::identifier := 'mod';\n    CREATE ANNOTATION std::description := 'Remainder from division (modulo).';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'/';\n    USING SQL $$\n        SELECT n - floor(n / d) * d;\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`%` (n: std::bigint, d: std::bigint) -> std::bigint\n{\n    CREATE ANNOTATION std::identifier := 'mod';\n    CREATE ANNOTATION std::description := 'Remainder from division (modulo).';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'%(numeric,numeric)';\n    USING SQL $$\n        SELECT (((n % d) + d) % d)::edgedbt.bigint_t;\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`%` (n: std::decimal, d: std::decimal) -> std::decimal\n{\n    CREATE ANNOTATION std::identifier := 'mod';\n    CREATE ANNOTATION std::description := 'Remainder from division (modulo).';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'%';\n    USING SQL $$\n        SELECT ((n % d) + d) % d;\n    $$;\n};\n\n\n# need an explicit operator for int64 in order to guarantee the result\n# is float64 and not decimal\nCREATE INFIX OPERATOR\nstd::`^` (n: std::int64, p: std::int64) -> std::float64\n{\n    CREATE ANNOTATION std::identifier := 'pow';\n    CREATE ANNOTATION std::description := 'Power operation.';\n    SET volatility := 'Immutable';\n    # We cheat here a bit by copying most of SQL operator metadata\n    # from the `/` operator, since there is no int ^ in Postgres. The\n    # power operator can behave like a division (negative power),\n    # therefore it should have the same basic properties w.r.t. types,\n    # etc. We don't use an explicit cast of the result because\n    # Postgres will treat this as float8 already.\n    USING SQL OPERATOR r'/';\n    USING SQL 'SELECT (\"n\" ^ \"p\")';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`^` (n: std::float32, p: std::float32) -> std::float32\n{\n    CREATE ANNOTATION std::identifier := 'pow';\n    CREATE ANNOTATION std::description := 'Power operation.';\n    SET volatility := 'Immutable';\n    # We cheat here a bit by copying most of SQL operator metadata\n    # from the `/` operator, since there is no float4 ^ in Postgres.\n    # The power operator can behave like a division (negative power),\n    # therefore it should have the same basic properties w.r.t. types,\n    # etc.\n    USING SQL OPERATOR '/';\n    USING SQL 'SELECT (\"n\" ^ \"p\")::float4';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`^` (n: std::float64, p: std::float64) -> std::float64 {\n    CREATE ANNOTATION std::identifier := 'pow';\n    CREATE ANNOTATION std::description := 'Power operation.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR '^';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`^` (n: std::bigint, p: std::bigint) -> std::decimal {\n    CREATE ANNOTATION std::identifier := 'pow';\n    CREATE ANNOTATION std::description := 'Power operation.';\n    SET volatility := 'Immutable';\n    SET force_return_cast := true;\n    USING SQL OPERATOR '^(numeric,numeric)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`^` (n: std::decimal, p: std::decimal) -> std::decimal {\n    CREATE ANNOTATION std::identifier := 'pow';\n    CREATE ANNOTATION std::description := 'Power operation.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR '^';\n};\n\n\n## Standard numeric casts\n## ----------------------\n\n\n## Implicit casts between numerics.\n\nCREATE CAST FROM std::int16 TO std::int32 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n    ALLOW IMPLICIT;\n};\n\n\nCREATE CAST FROM std::int32 TO std::int64 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n    ALLOW IMPLICIT;\n};\n\n\nCREATE CAST FROM std::int16 TO std::float32 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n    ALLOW IMPLICIT;\n};\n\n\nCREATE CAST FROM std::int64 TO std::float64 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n    ALLOW IMPLICIT;\n};\n\n\nCREATE CAST FROM std::int64 TO std::bigint {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n    ALLOW IMPLICIT;\n};\n\n\nCREATE CAST FROM std::int64 TO std::decimal {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n    ALLOW IMPLICIT;\n};\n\n\nCREATE CAST FROM std::bigint TO std::decimal {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n    ALLOW IMPLICIT;\n};\n\n\nCREATE CAST FROM std::float32 TO std::float64 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n    ALLOW IMPLICIT;\n};\n\n\n## Explicit and assignment casts.\n\nCREATE CAST FROM std::int32 TO std::int16 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::int64 TO std::int32 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n    ALLOW ASSIGNMENT;\n};\n\n\nCREATE CAST FROM std::int64 TO std::int16 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n    ALLOW ASSIGNMENT;\n};\n\n\nCREATE CAST FROM std::int64 TO std::float32 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n    ALLOW ASSIGNMENT;\n};\n\n\nCREATE CAST FROM std::float64 TO std::float32 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n    ALLOW ASSIGNMENT;\n};\n\n\nCREATE CAST FROM std::decimal TO std::int16 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::decimal TO std::int32 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::decimal TO std::int64 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::decimal TO std::float64 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::decimal TO std::float32 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::decimal TO std::bigint {\n    SET volatility := 'Immutable';\n    USING SQL 'SELECT round($1)::edgedbt.bigint_t';\n};\n\n\nCREATE CAST FROM std::float32 TO std::int16 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::float32 TO std::int32 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::float32 TO std::int64 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::float32 TO std::bigint {\n    SET volatility := 'Immutable';\n    USING SQL 'SELECT round($1)::edgedbt.bigint_t';\n};\n\n\nCREATE CAST FROM std::float32 TO std::decimal {\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT\n            (CASE WHEN val != 'NaN'\n                       AND val != 'Infinity'\n                       AND val != '-Infinity'\n            THEN\n                val::numeric\n            WHEN val IS NULL\n            THEN\n                NULL::numeric\n            ELSE\n                edgedb_VER.raise(\n                    NULL::numeric,\n                    'invalid_text_representation',\n                    msg => 'invalid value for numeric: ' || quote_literal(val)\n                )\n            END)\n        ;\n    $$;\n};\n\n\nCREATE CAST FROM std::float64 TO std::int16 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::float64 TO std::int32 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::float64 TO std::int64 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::float64 TO std::bigint {\n    SET volatility := 'Immutable';\n    USING SQL 'SELECT round($1)::edgedbt.bigint_t';\n};\n\n\nCREATE CAST FROM std::float64 TO std::decimal {\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT\n            (CASE WHEN val != 'NaN'\n                       AND val != 'Infinity'\n                       AND val != '-Infinity'\n            THEN\n                val::numeric\n            WHEN val IS NULL\n            THEN\n                NULL::numeric\n            ELSE\n                edgedb_VER.raise(\n                    NULL::numeric,\n                    'invalid_text_representation',\n                    msg => 'invalid value for numeric: ' || quote_literal(val)\n                )\n            END)\n        ;\n    $$;\n};\n\n\n## String casts.\n\nCREATE CAST FROM std::str TO std::int16 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::str TO std::int32 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::str TO std::int64 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::str TO std::float32 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::str TO std::float64 {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::str TO std::bigint {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'edgedb.str_to_bigint';\n};\n\n\nCREATE CAST FROM std::str TO std::decimal {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'edgedb.str_to_decimal';\n};\n\n\nCREATE CAST FROM std::int16 TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::int32 TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::int64 TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::float32 TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::float64 TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::decimal TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n"
  },
  {
    "path": "edb/lib/std/25-setoperators.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## Standard set operators\n## --------------------------\n\n\n# The set membership operators (IN, NOT IN) are defined\n# in terms of the corresponding equality operator.\n\nCREATE INFIX OPERATOR\nstd::`IN` (e: anytype, s: SET OF anytype) -> std::bool\n{\n    CREATE ANNOTATION std::identifier := 'in';\n    CREATE ANNOTATION std::description :=\n        'Test the membership of an element in a set.';\n    USING SQL EXPRESSION;\n    SET volatility := 'Immutable';\n    SET derivative_of := 'std::=';\n    SET is_singleton_set_of := true;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`NOT IN` (e: anytype, s: SET OF anytype) -> std::bool\n{\n    CREATE ANNOTATION std::identifier := 'not_in';\n    CREATE ANNOTATION std::description :=\n        'Test the membership of an element in a set.';\n    USING SQL EXPRESSION;\n    SET volatility := 'Immutable';\n    SET derivative_of := 'std::!=';\n    SET is_singleton_set_of := true;\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`EXISTS` (s: SET OF anytype) -> bool {\n    CREATE ANNOTATION std::identifier := 'exists';\n    CREATE ANNOTATION std::description := 'Test whether a set is not empty.';\n    SET volatility := 'Immutable';\n    SET is_singleton_set_of := true;\n    USING SQL EXPRESSION;\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`DISTINCT` (s: SET OF anytype) -> SET OF anytype {\n    CREATE ANNOTATION std::identifier := 'distinct';\n    CREATE ANNOTATION std::description :=\n        'Return a set without repeating any elements.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`UNION` (s1: SET OF anytype, s2: SET OF anytype) -> SET OF anytype {\n    CREATE ANNOTATION std::identifier := 'union';\n    CREATE ANNOTATION std::description := 'Merge two sets.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`EXCEPT` (s1: SET OF anytype, s2: SET OF anytype) -> SET OF anytype {\n    CREATE ANNOTATION std::identifier := 'except';\n    CREATE ANNOTATION std::description := 'Multiset difference.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`INTERSECT` (s1: SET OF anytype, s2: SET OF anytype) -> SET OF anytype {\n    CREATE ANNOTATION std::identifier := 'intersect';\n    CREATE ANNOTATION std::description := 'Multiset intersection.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`??` (l: OPTIONAL anytype, r: SET OF anytype) -> SET OF anytype {\n    CREATE ANNOTATION std::identifier := 'coalesce';\n    CREATE ANNOTATION std::description := 'Coalesce.';\n    SET volatility := 'Immutable';\n    SET is_singleton_set_of := true;\n    USING SQL EXPRESSION;\n};\n\n\nCREATE TERNARY OPERATOR\nstd::`IF` (if_true: SET OF anytype, condition: bool,\n           if_false: SET OF anytype) -> SET OF anytype {\n    CREATE ANNOTATION std::identifier := 'if_else';\n    CREATE ANNOTATION std::description :=\n        'Conditionally provide one or the other result.';\n    SET volatility := 'Immutable';\n    SET is_singleton_set_of := true;\n    USING SQL EXPRESSION;\n};\n"
  },
  {
    "path": "edb/lib/std/26-bitwisefuncs.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## Bitwise numeric functions\n## -------------------------\n\n\nCREATE FUNCTION\nstd::bit_and(l: std::int16, r: std::int16) -> std::int16\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise AND operator for 16-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT l & r\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::bit_and(l: std::int32, r: std::int32) -> std::int32\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise AND operator for 32-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT l & r\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::bit_and(l: std::int64, r: std::int64) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise AND operator for 64-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT l & r\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::bit_or(l: std::int16, r: std::int16) -> std::int16\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise OR operator for 16-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT l | r\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::bit_or(l: std::int32, r: std::int32) -> std::int32\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise OR operator for 32-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT l | r\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::bit_or(l: std::int64, r: std::int64) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise OR operator for 64-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT l | r\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::bit_xor(l: std::int16, r: std::int16) -> std::int16\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise exclusive OR operator for 16-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT l # r\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::bit_xor(l: std::int32, r: std::int32) -> std::int32\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise exclusive OR operator for 32-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT l # r\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::bit_xor(l: std::int64, r: std::int64) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise exclusive OR operator for 64-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT l # r\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::bit_not(r: std::int16) -> std::int16\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise NOT operator for 16-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT ~r\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::bit_not(r: std::int32) -> std::int32\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise NOT operator for 32-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT ~r\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::bit_not(r: std::int64) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise NOT operator for 64-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT ~r\n    $$;\n};\n\n\n# In Postgres bitwise shift operators accept a 32-bit integer as the number of\n# bit positions that need to be shifted. However, in EdgeDB the default\n# integer literal is int64, so we should accept that and cast it down inside\n# the function body.\n#\n# In Postgres the number of bits shifted gets truncated using a positive mod\n# 32 (or mod 64 for int8). We do not want such truncation in EdgeDB. Shifting by 20 bits 2 times\n# should bethe same as shifting by 40 bits once.\nCREATE FUNCTION\nstd::bit_rshift(val: std::int16, n: std::int64) -> std::int16\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise right-shift operator for 16-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE\n            WHEN n < 0 THEN\n                edgedb_VER.raise(\n                    NULL::int8,\n                    'invalid_parameter_value',\n                    msg => (\n                        'bit_rshift(): cannot shift by negative amount'\n                    )\n                )\n            WHEN n > 31 THEN\n                CASE\n                    WHEN val < 0 THEN -1\n                    ELSE 0\n                END\n            ELSE val >> n::int4\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::bit_rshift(val: std::int32, n: std::int64) -> std::int32\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise right-shift operator for 32-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE\n            WHEN n < 0 THEN\n                edgedb_VER.raise(\n                    NULL::int8,\n                    'invalid_parameter_value',\n                    msg => (\n                        'bit_rshift(): cannot shift by negative amount'\n                    )\n                )\n            WHEN n > 31 THEN\n                CASE\n                    WHEN val < 0 THEN -1\n                    ELSE 0\n                END\n            ELSE val >> n::int4\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::bit_rshift(val: std::int64, n: std::int64) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise right-shift operator for 64-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE\n            WHEN n < 0 THEN\n                edgedb_VER.raise(\n                    NULL::int8,\n                    'invalid_parameter_value',\n                    msg => (\n                        'bit_rshift(): cannot shift by negative amount'\n                    )\n                )\n            WHEN n > 63 THEN\n                CASE\n                    WHEN val < 0 THEN -1\n                    ELSE 0\n                END\n            ELSE val >> n::int4\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::bit_lshift(val: std::int16, n: std::int64) -> std::int16\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise left-shift operator for 16-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE\n            WHEN n < 0 THEN\n                edgedb_VER.raise(\n                    NULL::int8,\n                    'invalid_parameter_value',\n                    msg => (\n                        'bit_lshift(): cannot shift by negative amount'\n                    )\n                )\n            WHEN n > 31 THEN 0\n            ELSE val << n::int4\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::bit_lshift(val: std::int32, n: std::int64) -> std::int32\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise left-shift operator for 32-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE\n            WHEN n < 0 THEN\n                edgedb_VER.raise(\n                    NULL::int8,\n                    'invalid_parameter_value',\n                    msg => (\n                        'bit_lshift(): cannot shift by negative amount'\n                    )\n                )\n            WHEN n > 31 THEN 0\n            ELSE val << n::int4\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::bit_lshift(val: std::int64, n: std::int64) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'Bitwise left-shift operator for 64-bit integers.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE\n            WHEN n < 0 THEN\n                edgedb_VER.raise(\n                    NULL::int8,\n                    'invalid_parameter_value',\n                    msg => (\n                        'bit_lshift(): cannot shift by negative amount'\n                    )\n                )\n            WHEN n > 63 THEN 0\n            ELSE val << n::int4\n        END\n    )\n    $$;\n};\n\nCREATE FUNCTION\nstd::bit_count(val: std::int16) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'Count the number of set bits in a 16-bit integer.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT bit_count(val::int4::bit(16))\n    $$;\n};\n\nCREATE FUNCTION\nstd::bit_count(val: std::int32) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'Count the number of set bits in a 32-bit integer.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT bit_count(val::bit(32))\n    $$;\n};\n\nCREATE FUNCTION\nstd::bit_count(val: std::int64) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'Count the number of set bits in a 64-bit integer.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT bit_count(val::bit(64))\n    $$;\n};\n"
  },
  {
    "path": "edb/lib/std/30-arrayfuncs.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## Array functions\n\n\nCREATE FUNCTION\nstd::array_agg(s: SET OF anytype) -> array<anytype>\n{\n    CREATE ANNOTATION std::description :=\n        'Return the array made from all of the input set elements.';\n    SET volatility := 'Immutable';\n    SET initial_value := [];\n    SET impl_is_strict := false;\n    USING SQL FUNCTION 'array_agg';\n};\n\n\nCREATE FUNCTION\nstd::array_unpack(array: array<anytype>) -> SET OF anytype\n{\n    CREATE ANNOTATION std::description := 'Return array elements as a set.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'unnest';\n};\n\n\nCREATE FUNCTION\nstd::array_fill(val: anytype, n: std::int64) -> array<anytype>\n{\n    CREATE ANNOTATION std::description :=\n        'Return an array filled with the given value repeated \\\n         as many times as specified.';\n    SET volatility := 'Immutable';\n    # Postgres uses integer (int4) as the second argument. There is a maximum\n    # array size, however. So when we get an `n` value greater than maximum\n    # int4, we just truncate it to the maximum and let Postgres produce its\n    # error.\n    USING SQL $$\n    SELECT array_fill(\n        val,\n        ARRAY[(CASE WHEN n > 2147483647 THEN 2147483647 ELSE n END)::int4]\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::array_replace(\n    array: array<anytype>,\n    old: anytype,\n    new: anytype\n) -> array<anytype>\n{\n    CREATE ANNOTATION std::description :=\n        'Replace each array element equal to the second argument \\\n         with the third argument.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'array_replace';\n};\n\n\nCREATE FUNCTION\nstd::array_get(\n    array: array<anytype>,\n    idx: std::int64,\n    NAMED ONLY default: OPTIONAL anytype={}\n) -> OPTIONAL anytype\n{\n    CREATE ANNOTATION std::description :=\n        'Return the element of *array* at the specified *index*.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT COALESCE(\n        \"array\"[\n            edgedb_VER._normalize_array_index(\n                \"idx\"::int, array_upper(\"array\", 1))\n        ],\n        \"default\"\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::array_set(\n    array: array<anytype>,\n    idx: std::int64,\n    val: anytype\n) -> array<anytype>\n{\n    CREATE ANNOTATION std::description :=\n        'Set the element of *array* at the specified *index*.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT CASE\n        WHEN cardinality(\"array\") = 0 THEN\n            edgedb.raise(\n                \"array\",\n                'invalid_parameter_value',\n                msg => 'array index ' || idx::text || ' is out of bounds'\n            )\n        WHEN edgedb._normalize_array_index(\n            \"idx\"::int, array_upper(\"array\", 1)\n        ) NOT BETWEEN 1 and array_upper(\"array\", 1) THEN\n            edgedb.raise(\n                \"array\",\n                'invalid_parameter_value',\n                msg => 'array index ' || idx::text || ' is out of bounds'\n            )\n        WHEN edgedb._normalize_array_index(\n            \"idx\"::int, array_upper(\"array\", 1)\n        ) = 1 THEN\n            ARRAY[val] || \"array\"[2 :]\n        WHEN edgedb._normalize_array_index(\n            \"idx\"::int, array_upper(\"array\", 1)\n        ) = array_upper(\"array\", 1) THEN\n            \"array\"[: array_upper(\"array\", 1) - 1] || ARRAY[val]\n        ELSE\n            \"array\"[\n                : edgedb._normalize_array_index(\n                    \"idx\"::int,\n                    array_upper(\"array\", 1)\n                ) - 1\n            ]\n            || ARRAY[val]\n            || \"array\"[\n                edgedb._normalize_array_index(\n                    \"idx\"::int,\n                    array_upper(\"array\", 1)\n                ) + 1 :\n            ]\n    END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::array_insert(\n    array: array<anytype>,\n    idx: std::int64,\n    val: anytype\n) -> array<anytype>\n{\n    CREATE ANNOTATION std::description :=\n        'Insert *val* at the specified *index* of the *array*.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT CASE\n        WHEN cardinality(\"array\") = 0 AND \"idx\"::int != 0 THEN\n            edgedb.raise(\n                \"array\",\n                'invalid_parameter_value',\n                msg => 'array index ' || idx::text || ' is out of bounds'\n            )\n        WHEN cardinality(\"array\") = 0 AND \"idx\"::int = 0 THEN\n            ARRAY[val]\n\n        WHEN edgedb._normalize_array_index(\n            \"idx\"::int, array_upper(\"array\", 1)\n        ) NOT BETWEEN 1 and array_upper(\"array\", 1) + 1 THEN\n            edgedb.raise(\n                \"array\",\n                'invalid_parameter_value',\n                msg => 'array index ' || idx::text || ' is out of bounds'\n            )\n        WHEN edgedb._normalize_array_index(\n            \"idx\"::int, array_upper(\"array\", 1)\n        ) = 1 THEN\n            ARRAY[val] || \"array\"\n        WHEN edgedb._normalize_array_index(\n            \"idx\"::int, array_upper(\"array\", 1)\n        ) = array_upper(\"array\", 1) + 1 THEN\n            \"array\" || ARRAY[val]\n        ELSE\n            \"array\"[\n                : edgedb._normalize_array_index(\n                    \"idx\"::int,\n                    array_upper(\"array\", 1)\n                ) - 1\n            ]\n            || ARRAY[val]\n            || \"array\"[\n                edgedb._normalize_array_index(\n                    \"idx\"::int,\n                    array_upper(\"array\", 1)\n                ) :\n            ]\n    END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::array_join(array: array<std::str>, delimiter: std::str) -> std::str\n{\n    CREATE ANNOTATION std::description := 'Render an array to a string.';\n    # The Postgres function array_to_string works for any array type, but we\n    # use it specifically for string arrays. For string arrays it should be\n    # \"immutable\".\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT array_to_string(\"array\", \"delimiter\");\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::array_join(array: array<std::bytes>, delimiter: std::bytes) -> std::bytes\n{\n    CREATE ANNOTATION std::description := 'Render an array to a byte-string.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT\n        COALESCE (string_agg(el, \"delimiter\"), '\\x')\n    FROM\n        (SELECT unnest(\"array\") AS el) AS t\n    $$;\n};\n\n\n## Array operators\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: array<anytype>, r: array<anytype>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR '=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL array<anytype>,\n           r: OPTIONAL array<anytype>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n    SET recursive := true;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: array<anytype>, r: array<anytype>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR '<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL array<anytype>,\n            r: OPTIONAL array<anytype>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n    SET recursive := true;\n};\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: array<anytype>, r: array<anytype>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR '>=';\n};\n\nCREATE INFIX OPERATOR\nstd::`>` (l: array<anytype>, r: array<anytype>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR '>';\n};\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: array<anytype>, r: array<anytype>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR '<=';\n};\n\nCREATE INFIX OPERATOR\nstd::`<` (l: array<anytype>, r: array<anytype>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR '<';\n};\n\n# Concatenation\nCREATE INFIX OPERATOR\nstd::`++` (l: array<anytype>, r: array<anytype>) -> array<anytype> {\n    CREATE ANNOTATION std::identifier := 'concat';\n    CREATE ANNOTATION std::description := 'Array concatenation.';\n    SET volatility := 'Immutable';\n    SET impl_is_strict := false;\n    USING SQL FUNCTION 'array_cat';\n};\n\nCREATE INFIX OPERATOR\nstd::`[]` (l: array<anytype>, r: std::int64) -> anytype {\n    CREATE ANNOTATION std::identifier := 'index';\n    CREATE ANNOTATION std::description := 'Array indexing.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\nCREATE INFIX OPERATOR\nstd::`[]` (l: array<anytype>, r: tuple<std::int64, std::int64>) -> array<anytype> {\n    CREATE ANNOTATION std::identifier := 'slice';\n    CREATE ANNOTATION std::description := 'Array slicing.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n"
  },
  {
    "path": "edb/lib/std/30-bytesfuncs.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## Byte string functions\n## ---------------------\n\nCREATE FUNCTION\nstd::bytes_get_bit(bytes: std::bytes, num: int64) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'Get the *nth* bit of the *bytes* value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT get_bit(\"bytes\", \"num\"::int)::bigint\n    $$;\n};\n\nCREATE FUNCTION\nstd::bit_count(bytes: std::bytes) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'Count the number of set bits the bytes value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT bit_count(bytes)\n    $$;\n};\n\n\n\n## Byte string operators\n## ---------------------\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::bytes, r: std::bytes) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::bytes, r: OPTIONAL std::bytes) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::bytes, r: std::bytes) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::bytes, r: OPTIONAL std::bytes) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`++` (l: std::bytes, r: std::bytes) -> std::bytes {\n    CREATE ANNOTATION std::identifier := 'concat';\n    CREATE ANNOTATION std::description := 'Bytes concatenation.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR r'||';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::bytes, r: std::bytes) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR '>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::bytes, r: std::bytes) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR '>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::bytes, r: std::bytes) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR '<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::bytes, r: std::bytes) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR '<';\n};\n\nCREATE INFIX OPERATOR\nstd::`[]` (l: std::bytes, r: std::int64) -> std::bytes {\n    CREATE ANNOTATION std::identifier := 'index';\n    CREATE ANNOTATION std::description := 'Bytes indexing.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\nCREATE INFIX OPERATOR\nstd::`[]` (l: std::bytes, r: tuple<std::int64, std::int64>) -> std::bytes {\n    CREATE ANNOTATION std::identifier := 'slice';\n    CREATE ANNOTATION std::description := 'Bytes slicing.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n"
  },
  {
    "path": "edb/lib/std/30-datetimefuncs.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## Date/time functions\n## -------------------\n\nCREATE FUNCTION\nstd::datetime_current() -> std::datetime\n{\n    CREATE ANNOTATION std::description :=\n        'Return the current server date and time.';\n    SET volatility := 'Volatile';\n    SET force_return_cast := true;\n    USING SQL FUNCTION 'clock_timestamp';\n};\n\n\nCREATE FUNCTION\nstd::datetime_of_transaction() -> std::datetime\n{\n    CREATE ANNOTATION std::description :=\n        'Return the date and time of the start of the current transaction.';\n    SET volatility := 'Stable';\n    SET force_return_cast := true;\n    USING SQL FUNCTION 'transaction_timestamp';\n};\n\n\nCREATE FUNCTION\nstd::datetime_of_statement() -> std::datetime\n{\n    CREATE ANNOTATION std::description :=\n        'Return the date and time of the start of the current statement.';\n    SET volatility := 'Stable';\n    SET force_return_cast := true;\n    USING SQL FUNCTION 'statement_timestamp';\n};\n\n\nCREATE FUNCTION\nstd::datetime_get(dt: std::datetime, el: std::str) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Extract a specific element of input datetime by name.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT CASE WHEN \"el\" IN (\n            'century', 'day', 'decade', 'dow', 'doy', 'hour',\n            'isodow', 'isoyear', 'microseconds', 'millennium',\n            'milliseconds', 'minutes', 'month', 'quarter',\n            'seconds', 'week', 'year')\n        THEN date_part(\"el\", \"dt\")\n        WHEN \"el\" = 'epochseconds'\n        THEN date_part('epoch', \"dt\")\n        ELSE\n            edgedb_VER.raise(\n                NULL::float,\n                'invalid_datetime_format',\n                msg => (\n                    'invalid unit for std::datetime_get: '\n                    || quote_literal(\"el\")\n                ),\n                detail => (\n                    '{\"hint\":\"Supported units: epochseconds, century, day, '\n                    || 'decade, dow, doy, hour, isodow, isoyear, '\n                    || 'microseconds, millennium, milliseconds, minutes, '\n                    || 'month, quarter, seconds, week, year.\"}'\n                )\n            )\n        END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::datetime_truncate(dt: std::datetime, unit: std::str) -> std::datetime\n{\n    CREATE ANNOTATION std::description :=\n        'Truncate the input datetime to a particular precision.';\n    # date_trunc of timestamptz is STABLE in PostgreSQL\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT CASE WHEN \"unit\" IN (\n            'microseconds', 'milliseconds', 'seconds',\n            'minutes', 'hours', 'days', 'weeks', 'months',\n            'years', 'decades', 'centuries')\n        THEN date_trunc(\"unit\", \"dt\")::edgedbt.timestamptz_t\n        WHEN \"unit\" = 'quarters'\n        THEN date_trunc('quarter', \"dt\")::edgedbt.timestamptz_t\n        ELSE\n            edgedb_VER.raise(\n                NULL::edgedbt.timestamptz_t,\n                'invalid_datetime_format',\n                msg => (\n                    'invalid unit for std::datetime_truncate: '\n                    || quote_literal(\"unit\")\n                ),\n                detail => (\n                    '{\"hint\":\"Supported units: microseconds, milliseconds, '\n                    || 'seconds, minutes, hours, days, weeks, months, '\n                    || 'quarters, years, decades, centuries.\"}'\n                )\n            )\n        END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::duration_get(dt: std::duration, el: std::str) -> std::float64\n{\n    CREATE ANNOTATION std::description :=\n        'Extract a specific element of input duration by name.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT CASE WHEN \"el\" IN (\n            'hour', 'minutes', 'seconds', 'milliseconds', 'microseconds')\n        THEN date_part(\"el\", \"dt\")\n        WHEN \"el\" = 'totalseconds'\n        THEN date_part('epoch', \"dt\")\n        ELSE\n            edgedb_VER.raise(\n                NULL::float,\n                'invalid_datetime_format',\n                msg => (\n                    'invalid unit for std::duration_get: '\n                    || quote_literal(\"el\")\n                ),\n                detail => (\n                    '{\"hint\":\"Supported units: '\n                    || 'hour, minutes, seconds, milliseconds, microseconds, '\n                    || 'and totalseconds.\"}'\n                )\n            )\n        END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::duration_truncate(dt: std::duration, unit: std::str) -> std::duration\n{\n    CREATE ANNOTATION std::description :=\n        'Truncate the input duration to a particular precision.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT CASE WHEN \"unit\" in ('microseconds', 'milliseconds',\n                                'seconds', 'minutes', 'hours')\n        THEN date_trunc(\"unit\", \"dt\")::edgedbt.duration_t\n        ELSE\n            edgedb_VER.raise(\n                NULL::edgedbt.duration_t,\n                'invalid_datetime_format',\n                msg => (\n                    'invalid unit for std::duration_truncate: '\n                    || quote_literal(\"unit\")\n                ),\n                detail => (\n                    '{\"hint\":\"Supported units: microseconds, milliseconds, '\n                    || 'seconds, minutes, hours.\"}'\n                )\n            )\n        END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::duration_to_seconds(dur: std::duration) -> std::decimal\n{\n    CREATE ANNOTATION std::description :=\n        'Return duration as total number of seconds in interval.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT EXTRACT(epoch FROM date_trunc('minute', dur))::bigint::decimal +\n           '0.000001'::decimal*EXTRACT(microsecond FROM dur)::decimal\n    $$;\n};\n\n\n## Date/time operators\n## -------------------\n\n# std::datetime\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::datetime, r: std::datetime) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=(timestamptz,timestamptz)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::datetime, r: OPTIONAL std::datetime) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::datetime, r: std::datetime) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>(timestamptz,timestamptz)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::datetime, r: OPTIONAL std::datetime) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::datetime, r: std::datetime) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>(timestamptz,timestamptz)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::datetime, r: std::datetime) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=(timestamptz,timestamptz)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::datetime, r: std::datetime) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<(timestamptz,timestamptz)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::datetime, r: std::datetime) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=(timestamptz,timestamptz)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::datetime, r: std::duration) -> std::datetime {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    # Immutable because datetime is guaranteed to be in UTC and no DST issues\n    # should affect this.\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL $$\n        SELECT (\"l\" + \"r\")::edgedbt.timestamptz_t\n    $$\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::duration, r: std::datetime) -> std::datetime {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time addition.';\n    # Immutable because datetime is guaranteed to be in UTC and no DST issues\n    # should affect this.\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL $$\n        SELECT (\"l\" + \"r\")::edgedbt.timestamptz_t\n    $$\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::datetime, r: std::duration) -> std::datetime {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description :=\n        'Time interval and date/time subtraction.';\n    # Immutable because datetime is guaranteed to be in UTC and no DST issues\n    # should affect this.\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT (\"l\" - \"r\")::edgedbt.timestamptz_t\n    $$\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::datetime, r: std::datetime) -> std::duration {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Date/time subtraction.';\n    # Immutable because datetime is guaranteed to be in UTC and no DST issues\n    # should affect this.\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT EXTRACT(epoch FROM \"l\" - \"r\")::text::edgedbt.duration_t\n    $$\n};\n\n\n# std::duration\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::duration, r: std::duration) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=(interval,interval)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::duration, r: OPTIONAL std::duration) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::duration, r: std::duration) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>(interval,interval)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (\n        l: OPTIONAL std::duration,\n        r: OPTIONAL std::duration\n) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::duration, r: std::duration) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>(interval,interval)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::duration, r: std::duration) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=(interval,interval)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::duration, r: std::duration) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<(interval,interval)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::duration, r: std::duration) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=(interval,interval)';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: std::duration, r: std::duration) -> std::duration {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description :=\n        'Time interval addition.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::+';\n    USING SQL $$\n    SELECT (\"l\"::interval + \"r\"::interval)::edgedbt.duration_t;\n    $$;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: std::duration, r: std::duration) -> std::duration {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description :=\n        'Time interval subtraction.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\"l\"::interval - \"r\"::interval)::edgedbt.duration_t;\n    $$;\n};\n\n\nCREATE PREFIX OPERATOR\nstd::`-` (v: std::duration) -> std::duration {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description :=\n        'Time interval negation.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (-\"v\"::interval)::edgedbt.duration_t;\n    $$;\n};\n\n\n## String casts\n\nCREATE CAST FROM std::str TO std::datetime {\n    # Stable because the input string can contain an explicit time-zone. Time\n    # zones are externally defined things that can change suddenly and\n    # arbitrarily by human laws, thus potentially changing the interpretatio\n    # of the input string.\n    SET volatility := 'Stable';\n    USING SQL FUNCTION 'edgedb.datetime_in';\n};\n\n\nCREATE CAST FROM std::str TO std::duration {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'edgedb.duration_in';\n};\n\n\n# Normalize [local] datetime to text conversion to have\n# the same format as one would get by serializing to JSON.\n# Otherwise Postgres doesn't follow the ISO8601 standard\n# and uses ' ' instead of 'T' as a separator between date\n# and time.\nCREATE CAST FROM std::datetime TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT trim(to_json(val)::text, '\"');\n    $$;\n};\n\n\nCREATE CAST FROM std::duration TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT regexp_replace(val::text, '[[:<:]]mon(?=s?[[:>:]])', 'month');\n    $$;\n};\n\n\n# std::sum\n\nCREATE FUNCTION\nstd::sum(s: SET OF std::duration) -> std::duration\n{\n    CREATE ANNOTATION std::description :=\n        'Return the arithmetic sum of values in a set.';\n    SET volatility := 'Immutable';\n    SET initial_value := <std::duration>\"PT0S\";\n    SET force_return_cast := true;\n    USING SQL FUNCTION 'sum';\n};\n"
  },
  {
    "path": "edb/lib/std/30-jsonfuncs.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## JSON functions and operators.\n\nCREATE SCALAR TYPE std::JsonEmpty EXTENDING enum<ReturnEmpty, ReturnTarget, Error, UseNull, DeleteKey>;\n\nCREATE FUNCTION\nstd::json_typeof(json: std::json) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return the type of the outermost JSON value as a string.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'jsonb_typeof';\n};\n\n\nCREATE FUNCTION\nstd::json_array_unpack(array: std::json) -> SET OF std::json\n{\n    CREATE ANNOTATION std::description :=\n        'Return elements of JSON array as a set of `json`.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'jsonb_array_elements';\n};\n\n\nCREATE FUNCTION\nstd::json_object_unpack(obj: std::json) -> SET OF tuple<std::str, std::json>\n{\n    CREATE ANNOTATION std::description :=\n        'Return set of key/value tuples that make up the JSON object.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'jsonb_each';\n    # jsonb_each is defined as (jsonb, OUT key text, OUT value jsonb),\n    # and, quite perprexingly, would reject a column definition list\n    # with `a column definition list is only allowed for functions\n    # returning \"record\"`, even though it _is_ returning \"record\".\n    # Hence, we need this flag to tell the compiler to avoid generating\n    # a coldeflist for this function.\n    SET sql_func_has_out_params := True;\n};\n\n\nCREATE FUNCTION\nstd::json_object_pack(pairs: SET OF tuple<str, json>) -> std::json\n{\n    CREATE ANNOTATION std::description :=\n        'Return a JSON object with set key/value pairs.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE FUNCTION\nstd::json_get(\n    json: std::json,\n    VARIADIC path: std::str,\n    NAMED ONLY default: OPTIONAL std::json={}) -> OPTIONAL std::json\n{\n    CREATE ANNOTATION std::description :=\n        'Return the JSON value at the end of the specified path or an empty set.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT COALESCE(\n        jsonb_extract_path(\"json\", VARIADIC \"path\"),\n        \"default\"\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::__json_get_not_null(\n    json: std::json,\n    VARIADIC path: std::str,\n    NAMED ONLY detail: std::str='') -> OPTIONAL std::json\n{\n    SET volatility := 'Immutable';\n    SET internal := true;\n    USING SQL $$\n    SELECT\n        CASE\n        WHEN \"json\" = 'null'::jsonb THEN\n            NULL\n        ELSE\n            edgedb_VER.raise_on_null(\n                jsonb_extract_path(\"json\", VARIADIC \"path\"),\n                'invalid_parameter_value',\n                'missing value in JSON object',\n                detail => detail\n            )\n        END\n    $$;\n};\n\nCREATE FUNCTION\nstd::json_set(\n    target: std::json,\n    VARIADIC path: std::str,\n    NAMED ONLY value: OPTIONAL std::json,\n    NAMED ONLY create_if_missing: std::bool = true,\n    NAMED ONLY empty_treatment: std::JsonEmpty = std::JsonEmpty.ReturnEmpty,\n) -> OPTIONAL std::json\n{\n    CREATE ANNOTATION std::description :=\n        'Return an updated JSON target with a new value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE\n        WHEN \"value\" IS NULL AND \"empty_treatment\" = 'ReturnEmpty' THEN\n            NULL\n        WHEN \"value\" IS NULL AND \"empty_treatment\" = 'ReturnTarget' THEN\n            \"target\"\n        WHEN \"value\" IS NULL AND \"empty_treatment\" = 'Error' THEN\n            edgedb_VER.raise(\n                NULL::jsonb,\n                'invalid_parameter_value',\n                msg => 'invalid empty JSON value'\n            )\n        WHEN \"value\" IS NULL AND \"empty_treatment\" = 'UseNull' THEN\n            jsonb_set(\"target\", \"path\", 'null'::jsonb, \"create_if_missing\")\n        WHEN \"value\" IS NULL AND \"empty_treatment\" = 'DeleteKey' THEN\n            \"target\" #- \"path\"\n        ELSE\n            jsonb_set(\"target\", \"path\", \"value\", \"create_if_missing\")\n        END\n    )\n    $$;\n};\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::json, r: std::json) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::json, r: OPTIONAL std::json) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::json, r: std::json) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::json, r: OPTIONAL std::json) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::json, r: std::json) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR '>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::json, r: std::json) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR '>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::json, r: std::json) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR '<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::json, r: std::json) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR '<';\n};\n\nCREATE INFIX OPERATOR\nstd::`[]` (l: std::json, r: std::int64) -> std::json {\n    CREATE ANNOTATION std::identifier := 'index';\n    CREATE ANNOTATION std::description := 'JSON array/string indexing.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\nCREATE INFIX OPERATOR\nstd::`[]` (l: std::json, r: tuple<std::int64, std::int64>) -> std::json {\n    CREATE ANNOTATION std::identifier := 'slice';\n    CREATE ANNOTATION std::description := 'JSON array/string slicing.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\nCREATE INFIX OPERATOR\nstd::`[]` (l: std::json, r: std::str) -> std::json {\n    CREATE ANNOTATION std::identifier := 'destructure';\n    CREATE ANNOTATION std::description := 'JSON object property access.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\nCREATE INFIX OPERATOR\nstd::`++` (l: std::json, r: std::json) -> std::json {\n    CREATE ANNOTATION std::identifier := 'concatenate';\n    CREATE ANNOTATION std::description := 'Concatenate two JSON values into a new JSON value.';\n    SET volatility := 'Stable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN jsonb_typeof(\"l\") = 'array' AND jsonb_typeof(\"r\") = 'array' THEN\n            \"l\" || \"r\"\n        WHEN jsonb_typeof(\"l\") = 'object' AND jsonb_typeof(\"r\") = 'object' THEN\n            \"l\" || \"r\"\n        WHEN jsonb_typeof(\"l\") = 'string' AND jsonb_typeof(\"r\") = 'string' THEN\n            to_jsonb((\"l\"#>>'{}') || (\"r\"#>>'{}'))\n        ELSE\n            edgedb_VER.raise(\n                NULL::jsonb,\n                'invalid_parameter_value',\n                msg => (\n                    'invalid JSON values for ++ operator'\n                ),\n                detail => (\n                    '{\"hint\":\"Supported JSON types for concatenation: '\n                    || 'array ++ array, object ++ object, string ++ string.\"}'\n                )\n            )\n        END\n    )\n    $$;\n};\n\n## CASTS\n\n# This is only a container cast, and subject to element type cast\n# availability.\nCREATE CAST FROM array<anytype> TO std::json {\n    SET volatility := 'Stable';\n    USING SQL FUNCTION 'to_jsonb';\n};\n\n\n# This is only a container cast, and subject to element type cast\n# availability.\nCREATE CAST FROM anytuple TO std::json {\n    SET volatility := 'Stable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE CAST FROM std::json TO anytuple {\n    SET volatility := 'Stable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE FUNCTION\nstd::__tuple_validate_json(\n    v: std::json,\n    allow_null: std::bool,\n    detail: std::str=''\n    ) -> OPTIONAL std::json\n{\n    SET volatility := 'Immutable';\n    SET internal := true;\n    USING SQL $$\n    SELECT\n        CASE\n        WHEN v = 'null'::jsonb AND NOT allow_null THEN\n            edgedb_VER.raise(\n                NULL::jsonb,\n                'wrong_object_type',\n                msg => 'invalid null value in cast',\n                detail => detail\n            )\n        ELSE\n            edgedb_VER.jsonb_assert_type(\n                v,\n                ARRAY['array','object','null'],\n                detail => detail\n            )\n        END;\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO array<json> {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN nullif(val, 'null'::jsonb) IS NULL THEN NULL\n        ELSE\n            (SELECT COALESCE(array_agg(j), ARRAY[]::jsonb[])\n            FROM jsonb_array_elements(\n                edgedb_VER.jsonb_assert_type(val, ARRAY['array'], detail => detail)\n            ) as j)\n        END\n    )\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO array<anytype> {\n    SET volatility := 'Stable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE FUNCTION\nstd::__range_validate_json(val: std::json, detail: std::str='') -> OPTIONAL std::json\n{\n    SET volatility := 'Immutable';\n    SET internal := true;\n    USING SQL $$\n    SELECT (\n        SELECT\n            CASE\n            WHEN v = 'null'::jsonb THEN\n                NULL\n            WHEN\n                empty\n                AND (lower IS DISTINCT FROM upper\n                    OR lower IS NOT NULL AND inc_upper AND inc_lower)\n            THEN\n                edgedb_VER.raise(\n                    NULL::jsonb,\n                    'invalid_parameter_value',\n                    msg => 'conflicting arguments in range constructor:'\n                            || ' ''empty'' is `true` while the specified'\n                            || ' bounds suggest otherwise',\n                    detail => detail\n                )\n\n            WHEN\n                NOT empty\n                AND inc_lower IS NULL\n            THEN\n                edgedb_VER.raise(\n                    NULL::jsonb,\n                    'invalid_parameter_value',\n                    msg => 'JSON object representing a range must include an'\n                            || ' ''inc_lower'' boolean property',\n                    detail => detail\n                )\n\n            WHEN\n                NOT empty\n                AND inc_upper IS NULL\n            THEN\n                edgedb_VER.raise(\n                    NULL::jsonb,\n                    'invalid_parameter_value',\n                    msg => 'JSON object representing a range must include an'\n                            || ' ''inc_upper'' boolean property',\n                    detail => detail\n                )\n\n            WHEN\n                EXISTS (\n                    SELECT jsonb_object_keys(v)\n                    EXCEPT\n                    VALUES\n                        ('lower'),\n                        ('upper'),\n                        ('inc_lower'),\n                        ('inc_upper'),\n                        ('empty')\n                )\n            THEN\n                (SELECT edgedb_VER.raise(\n                    NULL::jsonb,\n                    'invalid_parameter_value',\n                    msg => 'JSON object representing a range contains unexpected'\n                            || ' keys: ' || string_agg(k.k, ', ' ORDER BY k.k),\n                    detail => detail\n                )\n                FROM\n                    (SELECT jsonb_object_keys(v)\n                    EXCEPT\n                    VALUES\n                        ('lower'),\n                        ('upper'),\n                        ('inc_lower'),\n                        ('inc_upper'),\n                        ('empty')\n                    ) AS k(k)\n                )\n            ELSE\n                v\n            END\n        FROM\n            (SELECT\n                (v ->> 'lower') AS lower,\n                (v ->> 'upper') AS upper,\n                (v ->> 'inc_lower')::bool AS inc_lower,\n                (v ->> 'inc_upper')::bool AS inc_upper,\n                coalesce((v ->> 'empty')::bool, false) AS empty\n            ) j\n    )\n    FROM (\n        SELECT edgedb_VER.jsonb_assert_type(\n            val,\n            ARRAY['object', 'null'],\n            detail => detail\n        ) AS v\n    ) AS x\n    $$;\n};\n\n\nCREATE CAST FROM range<std::anypoint> TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'edgedb.range_to_jsonb';\n};\n\n\nCREATE CAST FROM multirange<std::anypoint> TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'edgedb.multirange_to_jsonb';\n};\n\n\nCREATE CAST FROM std::json TO range<std::anypoint> {\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE CAST FROM std::json TO multirange<std::anypoint> {\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\n# The function to_jsonb is STABLE in PostgreSQL, but this function is\n# generic and STABLE volatility may be an overestimation in many cases.\nCREATE CAST FROM std::bool TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'to_jsonb';\n};\n\n\nCREATE CAST FROM std::bytes TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT to_jsonb(encode(val, 'base64'));\n    $$;\n};\n\n\nCREATE CAST FROM std::uuid TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'to_jsonb';\n};\n\n\nCREATE CAST FROM std::str TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'to_jsonb';\n};\n\n\nCREATE CAST FROM std::datetime TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'to_jsonb';\n};\n\n\nCREATE CAST FROM std::duration TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'to_jsonb';\n};\n\n\nCREATE CAST FROM std::int16 TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'to_jsonb';\n};\n\n\nCREATE CAST FROM std::int32 TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'to_jsonb';\n};\n\n\nCREATE CAST FROM std::int64 TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'to_jsonb';\n};\n\n\nCREATE CAST FROM std::float32 TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'to_jsonb';\n};\n\n\nCREATE CAST FROM std::float64 TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'to_jsonb';\n};\n\n\nCREATE CAST FROM std::decimal TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'to_jsonb';\n};\n\n\nCREATE CAST FROM std::json TO std::bool  {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT edgedb_VER.jsonb_extract_scalar(val, 'boolean', detail => detail)::bool;\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::uuid {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT edgedb_VER.jsonb_extract_scalar(val, 'string', detail => detail)::uuid;\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::bytes {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT decode(\n        edgedb_VER.jsonb_extract_scalar(val, 'string', detail => detail),\n        'base64'\n    )::bytea;\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT edgedb_VER.jsonb_extract_scalar(val, 'string', detail => detail);\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::datetime {\n    # Stable because the input string can contain an explicit time-zone. Time\n    # zones are externally defined things that can change suddenly and\n    # arbitrarily by human laws, thus potentially changing the interpretatio\n    # of the input string.\n    SET volatility := 'Stable';\n    USING SQL $$\n    SELECT edgedb_VER.datetime_in(\n        edgedb_VER.jsonb_extract_scalar(val, 'string', detail => detail)\n    );\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::duration {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT edgedb_VER.duration_in(\n        edgedb_VER.jsonb_extract_scalar(val, 'string', detail => detail)\n    );\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::int16 {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT edgedb_VER.jsonb_extract_scalar(val, 'number', detail => detail)::int2;\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::int32 {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT edgedb_VER.jsonb_extract_scalar(val, 'number', detail => detail)::int4;\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::int64 {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT edgedb_VER.jsonb_extract_scalar(val, 'number', detail => detail)::int8;\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::float32 {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT edgedb_VER.jsonb_extract_scalar(val, 'number', detail => detail)::float4;\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::float64 {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT edgedb_VER.jsonb_extract_scalar(val, 'number', detail => detail)::float8;\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::decimal {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT edgedb_VER.str_to_decimal(\n        edgedb_VER.jsonb_extract_scalar(val, 'number', detail => detail)\n    );\n    $$;\n};\n\n\nCREATE CAST FROM std::json TO std::bigint {\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT edgedb_VER.str_to_bigint(\n        edgedb_VER.jsonb_extract_scalar(val, 'number', detail => detail)\n    );\n    $$;\n};\n"
  },
  {
    "path": "edb/lib/std/30-regexpfuncs.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## Regular expression functions.\n\n\nCREATE FUNCTION\nstd::re_match(pattern: std::str, str: std::str) -> array<std::str>\n{\n    CREATE ANNOTATION std::description :=\n        'Find the first regular expression match in a string.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT array_replace(regexp_matches(\"str\", \"pattern\"), NULL, '');\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::re_match_all(pattern: std::str, str: std::str) -> SET OF array<std::str>\n{\n    CREATE ANNOTATION std::description :=\n        'Find all regular expression matches in a string.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT array_replace(regexp_matches(\"str\", \"pattern\", 'g'), NULL, '');\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::re_test(pattern: std::str, str: std::str) -> std::bool\n{\n    CREATE ANNOTATION std::description :=\n        'Test if a regular expression has a match in a string.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT \"str\" ~ \"pattern\";\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::re_replace(\n    pattern: std::str,\n    sub: std::str,\n    str: std::str,\n    NAMED ONLY flags: std::str = '') -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Replace matching substrings in a given string.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT regexp_replace(\"str\", \"pattern\", \"sub\", \"flags\");\n    $$;\n};\n"
  },
  {
    "path": "edb/lib/std/30-sequencefuncs.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## std::sequence functions and operators.\n\n# See schema.edgeql for definitions of sequence_next() and friends.\n"
  },
  {
    "path": "edb/lib/std/30-strfuncs.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## String operators\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::str, r: std::str) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::str, r: OPTIONAL std::str) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::str, r: std::str) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::str, r: OPTIONAL std::str) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\n# Concatenation.\nCREATE INFIX OPERATOR\nstd::`++` (l: std::str, r: std::str) -> std::str {\n    CREATE ANNOTATION std::identifier := 'concat';\n    CREATE ANNOTATION std::description := 'String concatenation.';\n    SET volatility := 'Immutable';\n    USING SQL OPERATOR '||';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`LIKE` (string: std::str, pattern: std::str) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'like';\n    CREATE ANNOTATION std::description :=\n        'Case-sensitive simple string matching.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`ILIKE` (string: std::str, pattern: std::str) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'ilike';\n    CREATE ANNOTATION std::description :=\n        'Case-insensitive simple string matching.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`NOT LIKE` (string: std::str, pattern: std::str) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'not_like';\n    CREATE ANNOTATION std::description :=\n        'Case-sensitive simple string matching.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`NOT ILIKE` (string: std::str, pattern: std::str) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'not_ilike';\n    CREATE ANNOTATION std::description :=\n        'Case-insensitive simple string matching.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::str, r: std::str) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR r'<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::str, r: std::str) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR r'<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::str, r: std::str) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR r'>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::str, r: std::str) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR r'>=';\n};\n\nCREATE INFIX OPERATOR\nstd::`[]` (l: std::str, r: std::int64) -> std::str {\n    CREATE ANNOTATION std::identifier := 'index';\n    CREATE ANNOTATION std::description := 'String indexing.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\nCREATE INFIX OPERATOR\nstd::`[]` (l: std::str, r: tuple<std::int64, std::int64>) -> std::str {\n    CREATE ANNOTATION std::identifier := 'slice';\n    CREATE ANNOTATION std::description := 'String slicing.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\n## String functions\n\n\nCREATE FUNCTION\nstd::str_repeat(s: std::str, n: std::int64) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Repeat the input *string* *n* times.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT repeat(\"s\", \"n\"::int4)\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::str_lower(s: std::str) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return a lowercase copy of the input *string*.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'lower';\n};\n\n\nCREATE FUNCTION\nstd::str_upper(s: std::str) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return an uppercase copy of the input *string*.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'upper';\n};\n\n\nCREATE FUNCTION\nstd::str_title(s: std::str) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return a titlecase copy of the input *string*.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'initcap';\n};\n\n\nCREATE FUNCTION\nstd::str_pad_start(s: std::str, n: std::int64, fill: std::str=' ') -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return the input string padded at the start to the length *n*.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT lpad(\"s\", \"n\"::int4, \"fill\")\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::str_lpad(s: std::str, n: std::int64, fill: std::str=' ') -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return the input string left-padded to the length *n*.';\n    CREATE ANNOTATION std::deprecated :=\n        'This function is deprecated and is scheduled \\\n         to be removed before 1.0.\\n\\\n         Use std::str_pad_start() instead.';\n    SET volatility := 'Immutable';\n    USING (std::str_pad_start(s, n, fill));\n};\n\n\nCREATE FUNCTION\nstd::str_pad_end(s: std::str, n: std::int64, fill: std::str=' ') -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return the input string padded at the end to the length *n*.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT rpad(\"s\", \"n\"::int4, \"fill\")\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::str_rpad(s: std::str, n: std::int64, fill: std::str=' ') -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return the input string right-padded to the length *n*.';\n    CREATE ANNOTATION std::deprecated :=\n        'This function is deprecated and is scheduled \\\n         to be removed before 1.0.\\n\\\n         Use std::str_pad_end() instead.';\n    SET volatility := 'Immutable';\n    USING (std::str_pad_end(s, n, fill));\n};\n\n\nCREATE FUNCTION\nstd::str_trim_start(s: std::str, tr: std::str=' ') -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return the input string with all *trim* characters removed from \\\n         its start.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'ltrim';\n};\n\n\nCREATE FUNCTION\nstd::str_ltrim(s: std::str, tr: std::str=' ') -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return the input string with all leftmost *trim* characters removed.';\n    CREATE ANNOTATION std::deprecated :=\n        'This function is deprecated and is scheduled \\\n         to be removed before 1.0.\\n\\\n         Use std::str_trim_start() instead.';\n    SET volatility := 'Immutable';\n    USING (std::str_trim_start(s, tr));\n};\n\n\nCREATE FUNCTION\nstd::str_trim_end(s: std::str, tr: std::str=' ') -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return the input string with all *trim* characters removed from \\\n         its end.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'rtrim';\n};\n\n\nCREATE FUNCTION\nstd::str_rtrim(s: std::str, tr: std::str=' ') -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return the input string with all rightmost *trim* characters removed.';\n    CREATE ANNOTATION std::deprecated :=\n        'This function is deprecated and is scheduled \\\n         to be removed before 1.0.\\n\\\n         Use std::str_trim_end() instead.';\n    SET volatility := 'Immutable';\n    USING (std::str_trim_end(s, tr));\n};\n\n\nCREATE FUNCTION\nstd::str_trim(s: std::str, tr: std::str=' ') -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return the input string with *trim* characters removed from \\\n         both ends.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'btrim';\n};\n\n\nCREATE FUNCTION\nstd::str_split(s: std::str, delimiter: std::str) -> array<std::str>\n{\n    CREATE ANNOTATION std::description :=\n        'Split string into array elements using the supplied delimiter.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT (\n            CASE WHEN \"delimiter\" != ''\n            THEN string_to_array(\"s\", \"delimiter\")\n            ELSE regexp_split_to_array(\"s\", '')\n            END\n        );\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::str_replace(s: std::str, old: std::str, new: std::str) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Given a string, find a matching substring and replace all its \\\n        occurrences with a new substring.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'replace';\n};\n\n\nCREATE FUNCTION\nstd::str_reverse(s: std::str) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Reverse the order of the characters in the string.';\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'reverse';\n};"
  },
  {
    "path": "edb/lib/std/30-uuidfuncs.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## UUID functions and operators.\n\n\nCREATE FUNCTION\nstd::uuid_generate_v1mc() -> std::uuid {\n    CREATE ANNOTATION std::description := 'Return a version 1 UUID.';\n    SET volatility := 'Volatile';\n    USING SQL FUNCTION 'edgedb.uuid_generate_v1mc';\n};\n\n\nCREATE FUNCTION\nstd::uuid_generate_v4() -> std::uuid {\n    CREATE ANNOTATION std::description := 'Return a version 4 UUID.';\n    SET volatility := 'Volatile';\n    USING SQL FUNCTION 'edgedb.uuid_generate_v4';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: std::uuid, r: std::uuid) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR r'=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL std::uuid, r: OPTIONAL std::uuid) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::uuid, r: std::uuid) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR r'<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL std::uuid, r: OPTIONAL std::uuid) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::uuid, r: std::uuid) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR '>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::uuid, r: std::uuid) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR '>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::uuid, r: std::uuid) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR '<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::uuid, r: std::uuid) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR '<';\n};\n\n\n## String casts.\n\nCREATE CAST FROM std::str TO std::uuid {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n\n\nCREATE CAST FROM std::uuid TO std::str {\n    SET volatility := 'Immutable';\n    USING SQL CAST;\n};\n"
  },
  {
    "path": "edb/lib/std/31-rangefuncs.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## Range/multirange functions\n\n\nCREATE FUNCTION\nstd::range(\n    lower: optional std::anypoint = {},\n    upper: optional std::anypoint = {},\n    named only inc_lower: bool = true,\n    named only inc_upper: bool = false,\n    named only empty: bool = false,\n) -> range<std::anypoint>\n{\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\n# TODO: maybe also add a constructor taking a set?\nCREATE FUNCTION\nstd::multirange(\n    ranges: array<range<std::anypoint>>,\n) -> multirange<std::anypoint>\n{\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE FUNCTION\nstd::range_is_empty(\n    val: range<anypoint>\n) -> bool\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'isempty';\n};\n\n\nCREATE FUNCTION\nstd::range_is_empty(\n    val: multirange<anypoint>\n) -> bool\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'isempty';\n};\n\n\nCREATE FUNCTION\nstd::range_unpack(\n    val: range<int32>\n) -> set of int32\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT\n            generate_series(\n                (\n                    edgedb_VER.range_lower_validate(val) +\n                    (CASE WHEN lower_inc(val) THEN 0 ELSE 1 END)\n                )::int8,\n                (\n                    edgedb_VER.range_upper_validate(val) -\n                    (CASE WHEN upper_inc(val) THEN 0 ELSE 1 END)\n                )::int8\n            )::int4\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::range_unpack(\n    val: range<int32>,\n    step: int32\n) -> set of int32\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT\n            generate_series(\n                (\n                    edgedb_VER.range_lower_validate(val) +\n                    (CASE WHEN lower_inc(val) THEN 0 ELSE 1 END)\n                )::int8,\n                (\n                    edgedb_VER.range_upper_validate(val) -\n                    (CASE WHEN upper_inc(val) THEN 0 ELSE 1 END)\n                )::int8,\n                step::int8\n            )::int4\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::range_unpack(\n    val: range<int64>\n) -> set of int64\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT\n            generate_series(\n                (\n                    edgedb_VER.range_lower_validate(val) +\n                    (CASE WHEN lower_inc(val) THEN 0 ELSE 1 END)\n                )::int8,\n                (\n                    edgedb_VER.range_upper_validate(val) -\n                    (CASE WHEN upper_inc(val) THEN 0 ELSE 1 END)\n                )::int8\n            )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::range_unpack(\n    val: range<int64>,\n    step: int64\n) -> set of int64\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT\n            generate_series(\n                (\n                    edgedb_VER.range_lower_validate(val) +\n                    (CASE WHEN lower_inc(val) THEN 0 ELSE 1 END)\n                )::int8,\n                (\n                    edgedb_VER.range_upper_validate(val) -\n                    (CASE WHEN upper_inc(val) THEN 0 ELSE 1 END)\n                )::int8,\n                step\n            )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::range_unpack(\n    val: range<float32>,\n    step: float32\n) -> set of float32\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT num::float4\n        FROM\n            generate_series(\n                (\n                    edgedb_VER.range_lower_validate(val) +\n                    (CASE WHEN lower_inc(val) THEN 0 ELSE step END)\n                )::numeric,\n                (\n                    edgedb_VER.range_upper_validate(val)\n                )::numeric,\n                step::numeric\n            ) AS num\n        WHERE\n            upper_inc(val) OR num::float4 < upper(val)\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::range_unpack(\n    val: range<float64>,\n    step: float64\n) -> set of float64\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT num::float8\n        FROM\n            generate_series(\n                (\n                    edgedb_VER.range_lower_validate(val) +\n                    (CASE WHEN lower_inc(val) THEN 0 ELSE step END)\n                )::numeric,\n                (\n                    edgedb_VER.range_upper_validate(val)\n                )::numeric,\n                step::numeric\n            ) AS num\n        WHERE\n            upper_inc(val) OR num::float8 < upper(val)\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::range_unpack(\n    val: range<decimal>,\n    step: decimal\n) -> set of decimal\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT num\n        FROM\n            generate_series(\n                edgedb_VER.range_lower_validate(val) +\n                    (CASE WHEN lower_inc(val) THEN 0 ELSE step END),\n                edgedb_VER.range_upper_validate(val),\n                step\n            ) AS num\n        WHERE\n            upper_inc(val) OR num < upper(val)\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::range_unpack(\n    val: range<datetime>,\n    step: duration\n) -> set of datetime\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT d::edgedbt.timestamptz_t\n        FROM\n            generate_series(\n                (\n                    edgedb_VER.range_lower_validate(val) + (\n                        CASE WHEN lower_inc(val)\n                            THEN '0'::interval\n                            ELSE step\n                        END\n                    )\n                )::timestamptz,\n                (\n                    edgedb_VER.range_upper_validate(val)\n                )::timestamptz,\n                step::interval\n            ) AS d\n        WHERE\n            upper_inc(val) OR d::edgedbt.timestamptz_t < upper(val)\n    $$;\n};\n\n\nCREATE FUNCTION std::range_get_upper(r: range<anypoint>) -> optional anypoint\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'upper';\n    SET force_return_cast := true;\n};\n\n\nCREATE FUNCTION std::range_get_lower(r: range<anypoint>) -> optional anypoint\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'lower';\n    SET force_return_cast := true;\n};\n\n\nCREATE FUNCTION std::range_is_inclusive_upper(r: range<anypoint>) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'upper_inc';\n};\n\n\nCREATE FUNCTION std::range_is_inclusive_lower(r: range<anypoint>) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'lower_inc';\n};\n\n\nCREATE FUNCTION std::range_get_upper(\n    r: multirange<anypoint>\n) -> optional anypoint\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'upper';\n    SET force_return_cast := true;\n};\n\n\nCREATE FUNCTION std::range_get_lower(\n    r: multirange<anypoint>\n) -> optional anypoint\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'lower';\n    SET force_return_cast := true;\n};\n\n\nCREATE FUNCTION std::range_is_inclusive_upper(\n    r: multirange<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'upper_inc';\n};\n\n\nCREATE FUNCTION std::range_is_inclusive_lower(\n    r: multirange<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'lower_inc';\n};\n\n\nCREATE FUNCTION std::contains(\n    haystack: range<anypoint>,\n    needle: range<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"haystack\" @> \"needle\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    # Postgres only manages to inline this function if it isn't marked strict,\n    # and we want it to be inlined so that std::pg::gin indexes work with it.\n    set impl_is_strict := false;\n};\n\n\nCREATE FUNCTION std::contains(\n    haystack: range<anypoint>,\n    needle: anypoint\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"haystack\" @> \"needle\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\nCREATE FUNCTION std::contains(\n    haystack: multirange<anypoint>,\n    needle: multirange<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"haystack\" @> \"needle\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\nCREATE FUNCTION std::contains(\n    haystack: multirange<anypoint>,\n    needle: range<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"haystack\" @> \"needle\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\nCREATE FUNCTION std::contains(\n    haystack: multirange<anypoint>,\n    needle: anypoint\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"haystack\" @> \"needle\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\nCREATE FUNCTION std::overlaps(\n    l: range<anypoint>,\n    r: range<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"l\" && \"r\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\nCREATE FUNCTION std::overlaps(\n    l: multirange<anypoint>,\n    r: multirange<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"l\" && \"r\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\n# FIXME: These functions introduce the concrete multirange types into the\n# schema. That's why they exist for each concrete type explicitly and aren't\n# defined generically for anytype.\nCREATE FUNCTION std::multirange_unpack(\n    val: multirange<std::int32>,\n) -> set of range<std::int32>\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'unnest';\n};\n\n\nCREATE FUNCTION std::multirange_unpack(\n    val: multirange<std::int64>,\n) -> set of range<std::int64>\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'unnest';\n};\n\n\nCREATE FUNCTION std::multirange_unpack(\n    val: multirange<std::float32>,\n) -> set of range<std::float32>\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'unnest';\n};\n\n\nCREATE FUNCTION std::multirange_unpack(\n    val: multirange<std::float64>,\n) -> set of range<std::float64>\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'unnest';\n};\n\n\nCREATE FUNCTION std::multirange_unpack(\n    val: multirange<std::decimal>,\n) -> set of range<std::decimal>\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'unnest';\n};\n\n\nCREATE FUNCTION std::multirange_unpack(\n    val: multirange<std::datetime>,\n) -> set of range<std::datetime>\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'unnest';\n};\n\n\nCREATE FUNCTION std::strictly_below(\n    l: range<anypoint>,\n    r: range<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"l\" << \"r\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\nCREATE FUNCTION std::strictly_below(\n    l: multirange<anypoint>,\n    r: multirange<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"l\" << \"r\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\nCREATE FUNCTION std::strictly_above(\n    l: range<anypoint>,\n    r: range<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"l\" >> \"r\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\nCREATE FUNCTION std::strictly_above(\n    l: multirange<anypoint>,\n    r: multirange<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"l\" >> \"r\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\nCREATE FUNCTION std::bounded_above(\n    l: range<anypoint>,\n    r: range<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"l\" &< \"r\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\nCREATE FUNCTION std::bounded_above(\n    l: multirange<anypoint>,\n    r: multirange<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"l\" &< \"r\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\nCREATE FUNCTION std::bounded_below(\n    l: range<anypoint>,\n    r: range<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"l\" &> \"r\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\nCREATE FUNCTION std::bounded_below(\n    l: multirange<anypoint>,\n    r: multirange<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"l\" &> \"r\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\nCREATE FUNCTION std::adjacent(\n    l: range<anypoint>,\n    r: range<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"l\" -|- \"r\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\nCREATE FUNCTION std::adjacent(\n    l: multirange<anypoint>,\n    r: multirange<anypoint>\n) -> std::bool\n{\n    SET volatility := 'Immutable';\n    USING SQL $$\n       SELECT \"l\" -|- \"r\"\n    $$;\n    # Needed to pick up the indexes when used in FILTER.\n    set prefer_subquery_args := true;\n    set impl_is_strict := false;\n};\n\n\n## Range operators\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: range<anypoint>, r: range<anypoint>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR '=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL range<anypoint>,\n           r: OPTIONAL range<anypoint>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n    SET recursive := true;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: range<anypoint>, r: range<anypoint>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR '<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL range<anypoint>,\n            r: OPTIONAL range<anypoint>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n    SET recursive := true;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: range<anypoint>, r: range<anypoint>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR '>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: range<anypoint>, r: range<anypoint>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR '>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: range<anypoint>, r: range<anypoint>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR '<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: range<anypoint>, r: range<anypoint>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR '<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: range<anypoint>, r: range<anypoint>) -> range<anypoint> {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description := 'Range union.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::+';\n    USING SQL OPERATOR r'+';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: range<anypoint>, r: range<anypoint>) -> range<anypoint> {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Range difference.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    USING SQL OPERATOR r'-';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`*` (l: range<anypoint>, r: range<anypoint>) -> range<anypoint> {\n    CREATE ANNOTATION std::identifier := 'mult';\n    CREATE ANNOTATION std::description := 'Range intersection.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::*';\n    USING SQL OPERATOR r'*';\n};\n\n\n## MultiRange operators\n\n\nCREATE INFIX OPERATOR\nstd::`=` (l: multirange<anypoint>, r: multirange<anypoint>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::=';\n    SET negator := 'std::!=';\n    USING SQL OPERATOR '=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (l: OPTIONAL multirange<anypoint>,\n           r: OPTIONAL multirange<anypoint>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n    SET recursive := true;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: multirange<anypoint>, r: multirange<anypoint>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::!=';\n    SET negator := 'std::=';\n    USING SQL OPERATOR '<>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (l: OPTIONAL multirange<anypoint>,\n            r: OPTIONAL multirange<anypoint>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n    SET recursive := true;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: multirange<anypoint>, r: multirange<anypoint>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::<=';\n    SET negator := 'std::<';\n    USING SQL OPERATOR '>=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: multirange<anypoint>, r: multirange<anypoint>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::<';\n    SET negator := 'std::<=';\n    USING SQL OPERATOR '>';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: multirange<anypoint>, r: multirange<anypoint>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::>=';\n    SET negator := 'std::>';\n    USING SQL OPERATOR '<=';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: multirange<anypoint>, r: multirange<anypoint>) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::>';\n    SET negator := 'std::>=';\n    USING SQL OPERATOR '<';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`+` (l: multirange<anypoint>, r: multirange<anypoint>) -> multirange<anypoint> {\n    CREATE ANNOTATION std::identifier := 'plus';\n    CREATE ANNOTATION std::description := 'Range union.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::+';\n    USING SQL OPERATOR r'+';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`-` (l: multirange<anypoint>, r: multirange<anypoint>) -> multirange<anypoint> {\n    CREATE ANNOTATION std::identifier := 'minus';\n    CREATE ANNOTATION std::description := 'Range difference.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    USING SQL OPERATOR r'-';\n};\n\n\nCREATE INFIX OPERATOR\nstd::`*` (l: multirange<anypoint>, r: multirange<anypoint>) -> multirange<anypoint> {\n    CREATE ANNOTATION std::identifier := 'mult';\n    CREATE ANNOTATION std::description := 'Range intersection.';\n    SET volatility := 'Immutable';\n    SET recursive := true;\n    SET commutator := 'std::*';\n    USING SQL OPERATOR r'*';\n};\n\n\n## Range/multirange casts\n\nCREATE CAST FROM range<anypoint> TO multirange<anypoint> {\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n    # Any range can be implicitly cast into a multirange.\n    ALLOW IMPLICIT;\n};\n\n\n## For annoying performance reasons, we want to be able to internally\n## directly call generate_series.\n## Hopefully I'll fix this better later.\n\nCREATE FUNCTION\nstd::__pg_generate_series(\n    `start`: std::int64,\n    stop: std::int64\n) -> SET OF std::int64\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'generate_series';\n};\n"
  },
  {
    "path": "edb/lib/std/50-constraints.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## Standard constraints.\n\n\nCREATE FUNCTION\nstd::_is_exclusive(s: SET OF anytype) -> std::bool\n{\n    SET volatility := 'Immutable';\n    SET initial_value := True;\n    SET internal := true;\n    USING SQL EXPRESSION;\n};\n\n\nCREATE ABSTRACT CONSTRAINT\nstd::constraint\n{\n    SET errmessage := 'invalid {__subject__}';\n};\n\n\nCREATE ABSTRACT CONSTRAINT\nstd::expression EXTENDING std::constraint\n{\n    CREATE ANNOTATION std::description := 'Arbitrary constraint expression.';\n    USING (__subject__);\n};\n\n\nCREATE ABSTRACT CONSTRAINT\nstd::exclusive EXTENDING std::constraint\n{\n    CREATE ANNOTATION std::description :=\n        'Specifies that the link or property value must be exclusive (unique).';\n    SET is_aggregate := true;\n    SET errmessage := '{__subject__} violates exclusivity constraint';\n    USING (std::_is_exclusive(__subject__));\n};\n\n\nCREATE ABSTRACT CONSTRAINT\nstd::one_of(VARIADIC vals: anytype) EXTENDING std::constraint\n{\n    CREATE ANNOTATION std::description :=\n        'Specifies the list of allowed values directly.';\n    SET errmessage := '{__subject__} must be one of: {vals}.';\n    USING (contains(vals, __subject__));\n};\n\n\nCREATE ABSTRACT CONSTRAINT\nstd::len_value ON (len(<std::str>__subject__)) EXTENDING std::constraint\n{\n    SET errmessage := 'invalid {__subject__}';\n};\n\n\nCREATE ABSTRACT CONSTRAINT\nstd::max_value(max: anytype) EXTENDING std::constraint\n{\n    CREATE ANNOTATION std::description :=\n        'Specifies the maximum value for the subject.';\n    SET errmessage := 'Maximum allowed value for {__subject__} is {max}.';\n    USING (__subject__ <= max);\n};\n\n\nCREATE ABSTRACT CONSTRAINT\nstd::min_value(min: anytype) EXTENDING std::constraint\n{\n    CREATE ANNOTATION std::description :=\n        'Specifies the minimum value for the subject.';\n    SET errmessage := 'Minimum allowed value for {__subject__} is {min}.';\n    USING (__subject__ >= min);\n};\n\n\nCREATE ABSTRACT CONSTRAINT\nstd::max_ex_value(max: anytype) EXTENDING std::max_value\n{\n    CREATE ANNOTATION std::description :=\n        'Specifies the maximum value (as an open interval) for the subject.';\n    SET errmessage := '{__subject__} must be less than {max}.';\n    USING (__subject__ < max);\n};\n\n\nCREATE ABSTRACT CONSTRAINT\nstd::min_ex_value(min: anytype) EXTENDING std::min_value\n{\n    CREATE ANNOTATION std::description :=\n        'Specifies the minimum value (as an open interval) for the subject.';\n    SET errmessage := '{__subject__} must be greater than {min}.';\n    USING (__subject__ > min);\n};\n\n\nCREATE ABSTRACT CONSTRAINT\nstd::regexp(pattern: std::str) EXTENDING std::constraint\n{\n    CREATE ANNOTATION std::description :=\n        'Specifies that the string representation of the subject must match a regexp.';\n    SET errmessage := 'invalid {__subject__}';\n    USING (re_test(pattern, __subject__));\n};\n\n\nCREATE ABSTRACT CONSTRAINT\nstd::max_len_value(max: std::int64) EXTENDING std::max_value, std::len_value\n{\n    CREATE ANNOTATION std::description :=\n        'Specifies the maximum length of subject string representation.';\n    SET errmessage := '{__subject__} must be no longer than {max} characters.';\n};\n\n\nCREATE ABSTRACT CONSTRAINT\nstd::min_len_value(min: std::int64) EXTENDING std::min_value, std::len_value\n{\n    CREATE ANNOTATION std::description :=\n        'Specifies the minimum length of subject string representation.';\n    SET errmessage :=\n        '{__subject__} must be no shorter than {min} characters.';\n};\n"
  },
  {
    "path": "edb/lib/std/60-baseobject.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n## Base object type, link and property definitions.\n\n\nCREATE ABSTRACT PROPERTY std::property;\n\nCREATE ABSTRACT PROPERTY std::id;\n\nCREATE ABSTRACT PROPERTY std::source;\n\nCREATE ABSTRACT PROPERTY std::target;\n\nCREATE ABSTRACT LINK std::link;\n\nCREATE ABSTRACT TYPE std::BaseObject {\n    CREATE REQUIRED PROPERTY id EXTENDING std::id -> std::uuid {\n        SET default := std::uuid_generate_v1mc();\n        SET readonly := True;\n        CREATE CONSTRAINT std::exclusive;\n    };\n    CREATE ANNOTATION std::description := 'Root object type.'\n};\n\nCREATE ABSTRACT TYPE std::Object EXTENDING std::BaseObject {\n    CREATE ANNOTATION std::description :=\n        'Root object type for user-defined types';\n};\n\n# N.B: This does *not* derive from std::BaseObject!\nCREATE TYPE std::FreeObject {\n    CREATE ANNOTATION std::description :=\n        'Object type for free shapes';\n};\n\n# 'USING SQL EXPRESSION' creates an EdgeDB Operator for purposes of\n# introspection and validation by the EdgeQL compiler. However, no\n# object is created in Postgres and the EdgeQL->SQL compiler is expected\n# to produce some expression that will be valid.\n#\n# 'USING SQL OPERATOR' does all of the above and it also creates an\n# actual Postgres operator. It is expected that the EdgeQL->SQL compiler\n# will specifically use that operator.\n\n# HACK: We use 'USING SQL EXPRESSION' instead of 'USING SQL OPERATOR'\n# here because in actuality Objects will be resolved as their uuids\n# and in the end it's the uuid operators that will be called in SQL.\n# On the other hand, if we use \"USING SQL OPERATOR\", we will end up\n# clashing with the operators for uuid in Postgres.\nCREATE INFIX OPERATOR\nstd::`=` (l: std::BaseObject, r: std::BaseObject) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'eq';\n    CREATE ANNOTATION std::description := 'Compare two values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?=` (\n    l: OPTIONAL std::BaseObject,\n    r: OPTIONAL std::BaseObject\n) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_eq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for equality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`!=` (l: std::BaseObject, r: std::BaseObject) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'neq';\n    CREATE ANNOTATION std::description := 'Compare two values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`?!=` (\n    l: OPTIONAL std::BaseObject,\n    r: OPTIONAL std::BaseObject\n) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'coal_neq';\n    CREATE ANNOTATION std::description :=\n        'Compare two (potentially empty) values for inequality.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>=` (l: std::BaseObject, r: std::BaseObject) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gte';\n    CREATE ANNOTATION std::description := 'Greater than or equal.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`>` (l: std::BaseObject, r: std::BaseObject) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'gt';\n    CREATE ANNOTATION std::description := 'Greater than.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<=` (l: std::BaseObject, r: std::BaseObject) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lte';\n    CREATE ANNOTATION std::description := 'Less than or equal.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\nCREATE INFIX OPERATOR\nstd::`<` (l: std::BaseObject, r: std::BaseObject) -> std::bool {\n    CREATE ANNOTATION std::identifier := 'lt';\n    CREATE ANNOTATION std::description := 'Less than.';\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n\n\n# The only possible Object cast is into json.\nCREATE CAST FROM std::BaseObject TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\nCREATE CAST FROM std::FreeObject TO std::json {\n    SET volatility := 'Immutable';\n    USING SQL EXPRESSION;\n};\n"
  },
  {
    "path": "edb/lib/std/70-converters.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n## Function that construct various scalars from strings or other types.\n\n\n# std::to_str\n# --------\n\n# Normalize [local] datetime to text conversion to have\n# the same format as one would get by serializing to JSON.\n# Otherwise Postgres doesn't follow the ISO8601 standard\n# and uses ' ' instead of 'T' as a separator between date\n# and time.\n#\n# EdgeQL: <text><datetime>'2010-10-10';\n# To SQL: trim(to_json('2010-01-01'::timestamptz)::text, '\"')\nCREATE FUNCTION\nstd::to_str(dt: std::datetime, fmt: OPTIONAL str={}) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return string representation of the input value.';\n    # Helper functions raising exceptions are STABLE.\n    SET volatility := 'Stable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            trim(to_json(\"dt\")::text, '\"')\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::text,\n                'invalid_parameter_value',\n                msg => 'to_str(): \"fmt\" argument must be a non-empty string'\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_char(\"dt\", \"fmt\"),\n                'invalid_parameter_value',\n                msg => 'to_str(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_str(td: std::duration, fmt: OPTIONAL str={}) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return string representation of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            trim(to_json(\"td\")::text, '\"')\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::text,\n                'invalid_parameter_value',\n                msg => 'to_str(): \"fmt\" argument must be a non-empty string'\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_char(\"td\", \"fmt\"),\n                'invalid_parameter_value',\n                msg => 'to_str(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\n# FIXME: There's no good safe default for all possible durations and some\n# durations cannot be formatted without non-trivial conversions (e.g.\n# 7,000 days).\n\n\nCREATE FUNCTION\nstd::to_str(i: std::int64, fmt: OPTIONAL str={}) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return string representation of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            \"i\"::text\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::text,\n                'invalid_parameter_value',\n                msg => 'to_str(): \"fmt\" argument must be a non-empty string'\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_char(\"i\", \"fmt\"),\n                'invalid_parameter_value',\n                msg => 'to_str(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_str(f: std::float64, fmt: OPTIONAL str={}) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return string representation of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            \"f\"::text\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::text,\n                'invalid_parameter_value',\n                msg => 'to_str(): \"fmt\" argument must be a non-empty string'\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_char(\"f\", \"fmt\"),\n                'invalid_parameter_value',\n                msg => 'to_str(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_str(d: std::bigint, fmt: OPTIONAL str={}) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return string representation of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            \"d\"::text\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::text,\n                'invalid_parameter_value',\n                msg => 'to_str(): \"fmt\" argument must be a non-empty string'\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_char(\"d\", \"fmt\"),\n                'invalid_parameter_value',\n                'to_str(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_str(d: std::decimal, fmt: OPTIONAL str={}) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return string representation of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            \"d\"::text\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::text,\n                'invalid_parameter_value',\n                msg => 'to_str(): \"fmt\" argument must be a non-empty string'\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_char(\"d\", \"fmt\"),\n                'invalid_parameter_value',\n                msg => 'to_str(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_str(array: array<std::str>, delimiter: std::str) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return string representation of the input value.';\n    CREATE ANNOTATION std::deprecated :=\n        'This converter function is deprecated and \\\n         is scheduled to be removed before 1.0.\\n\\\n         Use std::array_join() instead.';\n    SET volatility := 'Immutable';\n    USING (\n        SELECT std::array_join(array, delimiter)\n    );\n};\n\n\n# JSON can be prettified by specifying 'pretty' as the format, any\n# other value will result in an exception.\nCREATE FUNCTION\nstd::to_str(json: std::json, fmt: OPTIONAL str={}) -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return string representation of the input value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            \"json\"::text\n        WHEN \"fmt\" = 'pretty' THEN\n            jsonb_pretty(\"json\")\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::text,\n                'invalid_parameter_value',\n                msg => 'to_str(): \"fmt\" argument must be a non-empty string'\n            )\n        ELSE\n            edgedb_VER.raise(\n                NULL::text,\n                'invalid_parameter_value',\n                msg => 'to_str(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_str(b: std::bytes) -> std::str {\n    CREATE ANNOTATION std::description :=\n        'Convert a binary UTF-8 string to a text value.';\n    SET volatility := 'Immutable';\n    USING SQL $$ SELECT pg_catalog.convert_from(\"b\", 'UTF8') $$;\n};\n\n\nCREATE FUNCTION\nstd::to_bytes(s: std::str) -> std::bytes {\n    CREATE ANNOTATION std::description :=\n        'Convert a text string to a binary UTF-8 string.';\n    SET volatility := 'Immutable';\n    USING SQL $$ SELECT pg_catalog.convert_to(\"s\", 'UTF8') $$;\n};\n\n\nCREATE FUNCTION\nstd::to_bytes(j: std::json) -> std::bytes {\n    CREATE ANNOTATION std::description :=\n        'Convert a json value to a binary UTF-8 string.';\n    SET volatility := 'Immutable';\n    USING (to_bytes(to_str(j)));\n};\n\n\nCREATE SCALAR TYPE\nstd::Endian EXTENDING enum<Little, Big>;\n\n\nCREATE FUNCTION\nstd::to_bytes(val: std::int16, endian: std::Endian) -> std::bytes\n{\n    CREATE ANNOTATION std::description :=\n        'Convert an int16 using specified endian binary format.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT\n            CASE WHEN (endian = 'Little') THEN\n                substring(bin, 2, 1)\n                || substring(bin, 1, 1)\n            ELSE\n                bin\n            END\n        FROM (\n            SELECT int2send(val) AS bin\n        ) AS t;\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_bytes(val: std::int32, endian: std::Endian) -> std::bytes\n{\n    CREATE ANNOTATION std::description :=\n        'Convert an int32 using specified endian binary format.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT\n            CASE WHEN (endian = 'Little') THEN\n                substring(bin, 4, 1)\n                || substring(bin, 3, 1)\n                || substring(bin, 2, 1)\n                || substring(bin, 1, 1)\n            ELSE\n                bin\n            END\n        FROM (\n            SELECT int4send(val) AS bin\n        ) AS t;\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_bytes(val: std::int64, endian: std::Endian) -> std::bytes\n{\n    CREATE ANNOTATION std::description :=\n        'Convert an int64 using specified endian binary format.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT\n            CASE WHEN (endian = 'Little') THEN\n                substring(bin, 8, 1)\n                || substring(bin, 7, 1)\n                || substring(bin, 6, 1)\n                || substring(bin, 5, 1)\n                || substring(bin, 4, 1)\n                || substring(bin, 3, 1)\n                || substring(bin, 2, 1)\n                || substring(bin, 1, 1)\n            ELSE\n                bin\n            END\n        FROM (\n            SELECT int8send(val) AS bin\n        ) AS t;\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_bytes(val: std::uuid) -> std::bytes\n{\n    CREATE ANNOTATION std::description :=\n        'Convert an UUID to binary format.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT uuid_send(val);\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_json(str: std::str) -> std::json\n{\n    CREATE ANNOTATION std::description :=\n        'Return JSON value represented by the input *string*.';\n    # Casting of jsonb to and from text in PostgreSQL is IMMUTABLE.\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT \"str\"::jsonb\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_datetime(s: std::str, fmt: OPTIONAL str={}) -> std::datetime\n{\n    CREATE ANNOTATION std::description := 'Create a `datetime` value.';\n    # Helper function to_datetime is VOLATILE.\n    SET volatility := 'Volatile';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            edgedb_VER.datetime_in(\"s\")\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::edgedbt.timestamptz_t,\n                'invalid_parameter_value',\n                msg => (\n                    'to_datetime(): \"fmt\" argument must be a non-empty string'\n                )\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                edgedb_VER.to_datetime(\"s\", \"fmt\"),\n                'invalid_parameter_value',\n                msg => 'to_datetime(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\nCREATE FUNCTION\nstd::to_datetime(year: std::int64, month: std::int64, day: std::int64,\n                 hour: std::int64, min: std::int64, sec: std::float64,\n                 timezone: std::str)\n    -> std::datetime\n{\n    CREATE ANNOTATION std::description := 'Create a `datetime` value.';\n    # make_timestamptz is STABLE\n    SET volatility := 'Stable';\n    USING SQL $$\n    SELECT make_timestamptz(\n        \"year\"::int, \"month\"::int, \"day\"::int,\n        \"hour\"::int, \"min\"::int, \"sec\", \"timezone\"\n    )::edgedbt.timestamptz_t\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_datetime(epochseconds: std::float64) -> std::datetime\n{\n    CREATE ANNOTATION std::description := 'Create a `datetime` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT to_timestamp(\"epochseconds\")::edgedbt.timestamptz_t\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_datetime(epochseconds: std::int64) -> std::datetime\n{\n    CREATE ANNOTATION std::description := 'Create a `datetime` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT to_timestamp(\"epochseconds\")::edgedbt.timestamptz_t\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_datetime(epochseconds: std::decimal) -> std::datetime\n{\n    CREATE ANNOTATION std::description := 'Create a `datetime` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT to_timestamp(\"epochseconds\")::edgedbt.timestamptz_t\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_duration(\n        NAMED ONLY hours: std::int64=0,\n        NAMED ONLY minutes: std::int64=0,\n        NAMED ONLY seconds: std::float64=0,\n        NAMED ONLY microseconds: std::int64=0\n    ) -> std::duration\n{\n    CREATE ANNOTATION std::description := 'Create a `duration` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        make_interval(\n            0,\n            0,\n            0,\n            0,\n            \"hours\"::int,\n            \"minutes\"::int,\n            \"seconds\"\n        ) +\n        (microseconds::text || ' microseconds')::interval\n    )::edgedbt.duration_t\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_bigint(s: std::str, fmt: OPTIONAL str={}) -> std::bigint\n{\n    CREATE ANNOTATION std::description := 'Create a `bigint` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            edgedb_VER.str_to_bigint(\"s\")\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::edgedbt.bigint_t,\n                'invalid_parameter_value',\n                msg => (\n                    'to_bigint(): \"fmt\" argument must be a non-empty string'\n                )\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_number(\"s\", \"fmt\")::edgedbt.bigint_t,\n                'invalid_parameter_value',\n                msg => 'to_bigint(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_decimal(s: std::str, fmt: OPTIONAL str={}) -> std::decimal\n{\n    CREATE ANNOTATION std::description := 'Create a `decimal` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            edgedb_VER.str_to_decimal(\"s\")\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::numeric,\n                'invalid_parameter_value',\n                msg => (\n                    'to_decimal(): \"fmt\" argument must be a non-empty string'\n                )\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_number(\"s\", \"fmt\")::numeric,\n                'invalid_parameter_value',\n                msg => 'to_decimal(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_int64(s: std::str, fmt: OPTIONAL str={}) -> std::int64\n{\n    CREATE ANNOTATION std::description := 'Create a `int64` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            -- Must use the noninline version to prevent\n            -- the overeager function inliner from crashing\n            edgedb_VER.str_to_int64_noinline(\"s\")\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::bigint,\n                'invalid_parameter_value',\n                msg => 'to_int64(): \"fmt\" argument must be a non-empty string'\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_number(\"s\", \"fmt\")::bigint,\n                'invalid_parameter_value',\n                msg => 'to_int64(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_int64(val: std::bytes, endian: std::Endian) -> std::int64\n{\n    CREATE ANNOTATION std::description :=\n        'Convert bytes into `int64` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT\n        CASE WHEN (length(val) = 8) THEN\n            (\n                'x'\n                || right(\n                    (\n                        CASE WHEN (endian = 'Little') THEN\n                            substring(val, 8, 1)\n                            || substring(val, 7, 1)\n                            || substring(val, 6, 1)\n                            || substring(val, 5, 1)\n                            || substring(val, 4, 1)\n                            || substring(val, 3, 1)\n                            || substring(val, 2, 1)\n                            || substring(val, 1, 1)\n                        ELSE\n                            val\n                        END\n                    )::text, 16\n                )\n            )::bit(64)::int8\n        ELSE\n            edgedb_VER.raise(\n                0::int8,\n                'invalid_parameter_value',\n                msg => (\n                    'to_int64(): the argument must be exactly 8 bytes long'\n                )\n            )\n        END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_int32(s: std::str, fmt: OPTIONAL str={}) -> std::int32\n{\n    CREATE ANNOTATION std::description := 'Create a `int32` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            -- Must use the noninline version to prevent\n            -- the overeager function inliner from crashing\n            edgedb_VER.str_to_int32_noinline(\"s\")\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::int,\n                'invalid_parameter_value',\n                msg => 'to_int32(): \"fmt\" argument must be a non-empty string'\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_number(\"s\", \"fmt\")::int,\n                'invalid_parameter_value',\n                msg => 'to_int32(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_int32(val: std::bytes, endian: std::Endian) -> std::int32\n{\n    CREATE ANNOTATION std::description :=\n        'Convert bytes into `int32` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT\n        CASE WHEN (length(val) = 4) THEN\n            (\n                'x'\n                || right(\n                    (\n                        CASE WHEN (endian = 'Little') THEN\n                            substring(val, 4, 1)\n                            || substring(val, 3, 1)\n                            || substring(val, 2, 1)\n                            || substring(val, 1, 1)\n                        ELSE\n                            val\n                        END\n                    )::text, 8\n                )\n            )::bit(32)::int4\n        ELSE\n            edgedb_VER.raise(\n                0::int4,\n                'invalid_parameter_value',\n                msg => (\n                    'to_int32(): the argument must be exactly 4 bytes long'\n                )\n            )\n        END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_int16(s: std::str, fmt: OPTIONAL str={}) -> std::int16\n{\n    CREATE ANNOTATION std::description := 'Create a `int16` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            -- Must use the noninline version to prevent\n            -- the overeager function inliner from crashing\n            edgedb_VER.str_to_int16_noinline(\"s\")\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::smallint,\n                'invalid_parameter_value',\n                msg => 'to_int16(): \"fmt\" argument must be a non-empty string'\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_number(\"s\", \"fmt\")::smallint,\n                'invalid_parameter_value',\n                msg => 'to_int16(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_int16(val: std::bytes, endian: std::Endian) -> std::int16\n{\n    CREATE ANNOTATION std::description :=\n        'Convert bytes into `int16` value.';\n    SET volatility := 'Immutable';\n    # There is no direct cast from bits to int2 in Postgres, so we need to use\n    # the bit(32)::int4 as an intermediary value. However, the first bit is\n    # the sign bit and must be preserved as such, otherwise we will have\n    # overflow when casting from int4 to int2. So we pad the bytes with 0 on\n    # the right (which happens by default when casting 2 bytes from text to\n    # bit(32)) and then right-shift preserving the sign bit. This results in\n    # the int4 value in the lower two bytes being fully compatible with int2\n    # value.\n    USING SQL $$\n    SELECT\n        CASE WHEN (length(val) = 2) THEN\n            (\n                (\n                    (\n                        'x'\n                        || right(\n                            (\n                                CASE WHEN (endian = 'Little') THEN\n                                    substring(val, 2, 1)\n                                    || substring(val, 1, 1)\n                                ELSE\n                                    val\n                                END\n                            )::text, 4\n                        )\n                    )::bit(32)::int4\n                )>>16\n            )::int2\n        ELSE\n            edgedb_VER.raise(\n                0::int2,\n                'invalid_parameter_value',\n                msg => (\n                    'to_int16(): the argument must be exactly 2 bytes long'\n                )\n            )\n        END\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_float64(s: std::str, fmt: OPTIONAL str={}) -> std::float64\n{\n    CREATE ANNOTATION std::description := 'Create a `float64` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            edgedb_VER.str_to_float64_noinline(\"s\")\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::float8,\n                'invalid_parameter_value',\n                msg => (\n                    'to_float64(): \"fmt\" argument must be a non-empty string'\n                )\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_number(\"s\", \"fmt\")::float8,\n                'invalid_parameter_value',\n                msg => 'to_float64(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_float32(s: std::str, fmt: OPTIONAL str={}) -> std::float32\n{\n    CREATE ANNOTATION std::description := 'Create a `float32` value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT (\n        CASE WHEN \"fmt\" IS NULL THEN\n            edgedb_VER.str_to_float32_noinline(\"s\")\n        WHEN \"fmt\" = '' THEN\n            edgedb_VER.raise(\n                NULL::float4,\n                'invalid_parameter_value',\n                msg => (\n                    'to_float32(): \"fmt\" argument must be a non-empty string'\n                )\n            )\n        ELSE\n            edgedb_VER.raise_on_null(\n                to_number(\"s\", \"fmt\")::float4,\n                'invalid_parameter_value',\n                msg => 'to_float32(): format ''' || \"fmt\" || ''' is invalid'\n            )\n        END\n    )\n    $$;\n};\n\n\nCREATE FUNCTION\nstd::to_uuid(val: std::bytes) -> std::uuid\n{\n    CREATE ANNOTATION std::description :=\n        'Convert binary representation into UUID value.';\n    SET volatility := 'Immutable';\n    USING SQL $$\n    SELECT\n        CASE WHEN (length(val) = 16) THEN\n            ENCODE(val, 'hex')::uuid\n        ELSE\n            edgedb_VER.raise(\n                NULL::uuid,\n                'invalid_parameter_value',\n                msg => (\n                    'to_uuid(): the argument must be exactly 16 bytes long'\n                )\n            )\n        END\n    $$;\n};\n"
  },
  {
    "path": "edb/lib/sys.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nCREATE MODULE sys;\n\n\nCREATE MODULE sys::perm;\nCREATE PERMISSION sys::perm::superuser;\nCREATE PERMISSION sys::perm::data_modification;\nCREATE PERMISSION sys::perm::ddl;\nCREATE PERMISSION sys::perm::branch_config;\nCREATE PERMISSION sys::perm::sql_session_config;\nCREATE PERMISSION sys::perm::analyze;\n\nCREATE PERMISSION sys::perm::query_stats_read;\nCREATE PERMISSION sys::perm::approximate_count;\n\n\nCREATE SCALAR TYPE sys::TransactionIsolation\n    EXTENDING enum<RepeatableRead, Serializable>;\n\n\nCREATE SCALAR TYPE sys::TransactionAccessMode\n    EXTENDING enum<ReadOnly, ReadWrite>;\n\n\nCREATE SCALAR TYPE sys::TransactionDeferrability\n    EXTENDING enum<Deferrable, NotDeferrable>;\n\n\nCREATE SCALAR TYPE sys::VersionStage\n    EXTENDING enum<dev, alpha, beta, rc, final>;\n\n\nCREATE SCALAR TYPE sys::QueryType\n    EXTENDING enum<EdgeQL, SQL>;\n\n\nCREATE SCALAR TYPE sys::OutputFormat\n    EXTENDING enum<BINARY, JSON, JSON_ELEMENTS, NONE>;\n\n\nCREATE ABSTRACT TYPE sys::SystemObject EXTENDING schema::Object;\n\nCREATE ABSTRACT TYPE sys::ExternalObject EXTENDING sys::SystemObject;\n\n\nCREATE TYPE sys::Branch EXTENDING\n        sys::ExternalObject,\n        schema::AnnotationSubject {\n    ALTER PROPERTY name {\n        CREATE CONSTRAINT std::exclusive;\n    };\n    CREATE PROPERTY last_migration-> std::str;\n};\n\nCREATE ALIAS sys::Database := sys::Branch;\n\n\nCREATE TYPE sys::ExtensionPackage EXTENDING\n        sys::SystemObject,\n        schema::AnnotationSubject {\n    CREATE REQUIRED PROPERTY script -> str;\n    CREATE REQUIRED PROPERTY version ->\n        tuple<\n             major: std::int64,\n             minor: std::int64,\n             stage: sys::VersionStage,\n             stage_no: std::int64,\n             local: array<std::str>,\n         >;\n};\n\nCREATE TYPE sys::ExtensionPackageMigration EXTENDING\n        sys::SystemObject,\n        schema::AnnotationSubject {\n    CREATE REQUIRED PROPERTY script -> str;\n    CREATE REQUIRED PROPERTY from_version ->\n        tuple<\n             major: std::int64,\n             minor: std::int64,\n             stage: sys::VersionStage,\n             stage_no: std::int64,\n             local: array<std::str>,\n         >;\n    CREATE REQUIRED PROPERTY to_version ->\n        tuple<\n             major: std::int64,\n             minor: std::int64,\n             stage: sys::VersionStage,\n             stage_no: std::int64,\n             local: array<std::str>,\n         >;\n};\n\n\nALTER TYPE schema::Extension {\n    CREATE REQUIRED LINK package -> sys::ExtensionPackage {\n        CREATE CONSTRAINT std::exclusive;\n    }\n};\n\n\nCREATE TYPE sys::Role EXTENDING\n        sys::SystemObject,\n        schema::InheritingObject,\n        schema::AnnotationSubject {\n    ALTER PROPERTY name {\n        CREATE CONSTRAINT std::exclusive;\n    };\n\n    CREATE REQUIRED PROPERTY superuser -> std::bool;\n    # Backwards compatibility.\n    CREATE PROPERTY is_superuser := .superuser;\n    CREATE PROPERTY password -> std::str;\n    CREATE MULTI PROPERTY permissions -> std::str;\n    CREATE MULTI PROPERTY branches -> std::str;\n    CREATE PROPERTY apply_access_policies_pg_default -> std::bool;\n\n    CREATE ACCESS POLICY ap_read deny select using (\n        not global sys::perm::superuser\n    );\n};\n\n\nALTER TYPE sys::Role {\n    CREATE MULTI LINK member_of -> sys::Role;\n};\n\n\nCREATE TYPE sys::QueryStats EXTENDING sys::ExternalObject {\n    CREATE LINK branch -> sys::Branch {\n        CREATE ANNOTATION std::description :=\n            \"The branch this statistics entry was collected in.\";\n    };\n    CREATE PROPERTY query -> std::str {\n        CREATE ANNOTATION std::description :=\n            \"Text string of a representative query.\";\n    };\n    CREATE PROPERTY query_type -> sys::QueryType {\n        CREATE ANNOTATION std::description :=\n            \"Type of the query.\";\n    };\n    CREATE PROPERTY tag -> std::str {\n        CREATE ANNOTATION std::description :=\n            \"Query tag, commonly specifies the origin of the query, e.g 'gel/cli' for queries originating from the CLI.  Clients can specify a tag for easier query identification.\";\n    };\n\n    CREATE PROPERTY compilation_config -> std::json;\n    CREATE PROPERTY protocol_version -> tuple<major: std::int16,\n                                              minor: std::int16>;\n    CREATE PROPERTY default_namespace -> std::str;\n    CREATE OPTIONAL PROPERTY namespace_aliases -> std::json;\n    CREATE OPTIONAL PROPERTY output_format -> sys::OutputFormat;\n    CREATE OPTIONAL PROPERTY expect_one -> std::bool;\n    CREATE OPTIONAL PROPERTY implicit_limit -> std::int64;\n    CREATE OPTIONAL PROPERTY inline_typeids -> std::bool;\n    CREATE OPTIONAL PROPERTY inline_typenames -> std::bool;\n    CREATE OPTIONAL PROPERTY inline_objectids -> std::bool;\n\n    CREATE PROPERTY plans -> std::int64 {\n        CREATE ANNOTATION std::description :=\n            \"Number of times the query was planned in the backend.\";\n    };\n    CREATE PROPERTY total_plan_time -> std::duration {\n        CREATE ANNOTATION std::description :=\n            \"Total time spent planning the query in the backend.\";\n    };\n    CREATE PROPERTY min_plan_time -> std::duration {\n        CREATE ANNOTATION std::description :=\n            \"Minimum time spent planning the query in the backend. \"\n            ++ \"This field will be zero if the counter has been reset \"\n            ++ \"using the `sys::reset_query_stats` function \"\n            ++ \"with the `minmax_only` parameter set to `true` \"\n            ++ \"and never been planned since.\";\n    };\n    CREATE PROPERTY max_plan_time -> std::duration {\n        CREATE ANNOTATION std::description :=\n            \"Maximum time spent planning the query in the backend. \"\n            ++ \"This field will be zero if the counter has been reset \"\n            ++ \"using the `sys::reset_query_stats` function \"\n            ++ \"with the `minmax_only` parameter set to `true` \"\n            ++ \"and never been planned since.\";\n    };\n    CREATE PROPERTY mean_plan_time -> std::duration {\n        CREATE ANNOTATION std::description :=\n            \"Mean time spent planning the query in the backend.\";\n    };\n    CREATE PROPERTY stddev_plan_time -> std::duration {\n        CREATE ANNOTATION std::description :=\n            \"Population standard deviation of time spent \"\n            ++ \"planning the query in the backend.\";\n    };\n\n    CREATE PROPERTY calls -> std::int64 {\n        CREATE ANNOTATION std::description :=\n            \"Number of times the query was executed.\";\n    };\n    CREATE PROPERTY total_exec_time -> std::duration {\n        CREATE ANNOTATION std::description :=\n            \"Total time spent executing the query in the backend.\";\n    };\n    CREATE PROPERTY min_exec_time -> std::duration {\n        CREATE ANNOTATION std::description :=\n            \"Minimum time spent executing the query in the backend, \"\n            ++ \"this field will be zero until this query is executed \"\n            ++ \"first time after reset performed by the \"\n            ++ \"`sys::reset_query_stats` function with the \"\n            ++ \"`minmax_only` parameter set to `true`\";\n    };\n    CREATE PROPERTY max_exec_time -> std::duration {\n        CREATE ANNOTATION std::description :=\n            \"Maximum time spent executing the query in the backend, \"\n            ++ \"this field will be zero until this query is executed \"\n            ++ \"first time after reset performed by the \"\n            ++ \"`sys::reset_query_stats` function with the \"\n            ++ \"`minmax_only` parameter set to `true`\";\n    };\n    CREATE PROPERTY mean_exec_time -> std::duration {\n        CREATE ANNOTATION std::description :=\n            \"Mean time spent executing the query in the backend.\";\n    };\n    CREATE PROPERTY stddev_exec_time -> std::duration {\n        CREATE ANNOTATION std::description :=\n            \"Population standard deviation of time spent \"\n            ++ \"executing the query in the backend.\";\n    };\n\n    CREATE PROPERTY rows -> std::int64 {\n        CREATE ANNOTATION std::description :=\n            \"Total number of rows retrieved or affected by the query.\";\n    };\n    CREATE PROPERTY stats_since -> std::datetime {\n        CREATE ANNOTATION std::description :=\n            \"Time at which statistics gathering started for this query.\";\n    };\n    CREATE PROPERTY minmax_stats_since -> std::datetime {\n        CREATE ANNOTATION std::description :=\n            \"Time at which min/max statistics gathering started \"\n            ++ \"for this query (fields `min_plan_time`, `max_plan_time`, \"\n            ++ \"`min_exec_time` and `max_exec_time`).\";\n    };\n\n    CREATE ACCESS POLICY ap_read allow select using (\n        global sys::perm::query_stats_read\n    );\n};\n\n\nCREATE FUNCTION\nsys::reset_query_stats(\n    named only branch_name: OPTIONAL std::str = {},\n    named only id: OPTIONAL std::uuid = {},\n    named only minmax_only: OPTIONAL std::bool = false,\n) -> OPTIONAL std::datetime {\n    CREATE ANNOTATION std::description :=\n        'Discard query statistics gathered so far corresponding to the '\n        ++ 'specified `branch_name` and `id`. If either of the '\n        ++ 'parameters is not specified, the statistics that match with the '\n        ++ 'other parameter will be reset. If no parameter is specified, '\n        ++ 'it will discard all statistics. When `minmax_only` is `true`, '\n        ++ 'only the values of minimum and maximum planning and execution '\n        ++ 'time will be reset (i.e. `min_plan_time`, `max_plan_time`, '\n        ++ '`min_exec_time` and `max_exec_time` fields). The default value '\n        ++ 'for `minmax_only` parameter is `false`. This function returns '\n        ++ 'the time of a reset. This time is saved to `stats_reset` or '\n        ++ '`minmax_stats_since` field of `sys::QueryStats` if the '\n        ++ 'corresponding reset was actually performed.';\n    SET volatility := 'Volatile';\n    USING SQL FUNCTION 'edgedb.reset_query_stats';\n    set required_permissions := { sys::perm::superuser };\n};\n\n\n# An intermediate function is needed because we can't\n# cast JSON to tuples yet.  DO NOT use directly, it'll go away.\nCREATE FUNCTION\nsys::__version_internal() -> tuple<major: std::int64,\n                                   minor: std::int64,\n                                   stage: std::str,\n                                   stage_no: std::int64,\n                                   local: array<std::str>>\n{\n    # This function reads from a table.\n    SET volatility := 'Stable';\n    SET internal := true;\n    USING SQL $$\n    SELECT\n        (v ->> 'major')::int8,\n        (v ->> 'minor')::int8,\n        (v ->> 'stage')::text,\n        (v ->> 'stage_no')::int8,\n        (SELECT coalesce(array_agg(el), ARRAY[]::text[])\n         FROM jsonb_array_elements_text(v -> 'local') AS el)\n    FROM\n        (SELECT\n            pg_catalog.current_setting('edgedb.server_version')::jsonb AS v\n        ) AS q\n    $$;\n};\n\n\nCREATE FUNCTION\nsys::get_version() -> tuple<major: std::int64,\n                            minor: std::int64,\n                            stage: sys::VersionStage,\n                            stage_no: std::int64,\n                            local: array<std::str>>\n{\n    CREATE ANNOTATION std::description :=\n        'Return the server version as a tuple.';\n    SET volatility := 'Stable';\n    USING (\n        SELECT <tuple<major: std::int64,\n                    minor: std::int64,\n                    stage: sys::VersionStage,\n                    stage_no: std::int64,\n                    local: array<std::str>>>sys::__version_internal()\n    );\n};\n\n\nCREATE FUNCTION\nsys::get_version_as_str() -> std::str\n{\n    CREATE ANNOTATION std::description :=\n        'Return the server version as a string.';\n    SET volatility := 'Stable';\n    USING (\n        WITH v := sys::get_version()\n        SELECT\n            <str>v.major\n            ++ '.' ++ <str>v.minor\n            ++ (('-' ++ <str>v.stage ++ '.' ++ <str>v.stage_no)\n                IF v.stage != <sys::VersionStage>'final' ELSE '')\n            ++ (('+' ++ std::array_join(v.local, '.')) IF len(v.local) > 0\n                ELSE '')\n    );\n};\n\n\nCREATE FUNCTION sys::get_instance_name() -> std::str{\n    CREATE ANNOTATION std::description :=\n        'Return the server instance name.';\n    SET volatility := 'Stable';\n    USING SQL $$\n        SELECT pg_catalog.current_setting('edgedb.instance_name');\n    $$;\n};\n\n\nCREATE FUNCTION\nsys::get_transaction_isolation() -> sys::TransactionIsolation\n{\n    CREATE ANNOTATION std::description :=\n        'Return the isolation level of the current transaction.';\n    # This function only reads from a table.\n    SET volatility := 'Stable';\n    SET force_return_cast := true;\n    USING SQL FUNCTION 'edgedb._get_transaction_isolation';\n};\n\n\nCREATE FUNCTION\nsys::get_current_database() -> str\n{\n    CREATE ANNOTATION std::description :=\n        'Return the name of the current database branch as a string.';\n    # The results won't change within a single statement.\n    SET volatility := 'Stable';\n    USING SQL FUNCTION 'edgedb.get_current_database';\n};\n\n\nCREATE FUNCTION\nsys::get_current_branch() -> str\n{\n    CREATE ANNOTATION std::description :=\n        'Return the name of the current database branch as a string.';\n    # The results won't change within a single statement.\n    SET volatility := 'Stable';\n    USING SQL FUNCTION 'edgedb.get_current_database';\n};\n\n\nCREATE FUNCTION\nsys::_describe_roles_as_ddl() -> str\n{\n    # The results won't change within a single statement.\n    SET volatility := 'Stable';\n    SET internal := true;\n    USING SQL FUNCTION 'edgedb._describe_roles_as_ddl';\n    set required_permissions := { sys::perm::superuser };\n};\n\n\nCREATE FUNCTION\nsys::_get_all_role_memberships(r: uuid) -> array<uuid>\n{\n    # The results won't change within a single statement.\n    SET volatility := 'Stable';\n    SET internal := true;\n    USING SQL FUNCTION 'edgedb._all_role_memberships';\n    set impl_is_strict := false;\n    set required_permissions := { sys::perm::superuser };\n};\n\n\nALTER TYPE sys::Role {\n    CREATE MULTI PROPERTY all_permissions := distinct({\n        .permissions,\n        (\n            with self_id := .id\n            select detached sys::Role\n            filter .id in array_unpack(\n                sys::_get_all_role_memberships(self_id)\n            )\n        ).permissions,\n    });\n};\n\n\nCREATE FUNCTION\nsys::__pg_and(a: OPTIONAL std::bool, b: OPTIONAL std::bool) -> std::bool\n{\n    SET volatility := 'Immutable';\n    SET internal := true;\n    USING SQL $$\n        SELECT a AND b;\n    $$;\n};\n\n\nCREATE FUNCTION\nsys::__pg_or(a: OPTIONAL std::bool, b: OPTIONAL std::bool) -> std::bool\n{\n    SET volatility := 'Immutable';\n    SET internal := true;\n    USING SQL $$\n        SELECT a OR b;\n    $$;\n};\n\n\nCREATE FUNCTION\nsys::approximate_count(\n    type: schema::ObjectType,\n    NAMED ONLY ignore_subtypes: std::bool=false,\n) -> int64\n{\n    SET volatility := 'Stable';\n    USING SQL FUNCTION 'edgedb.approximate_count';\n    set impl_is_strict := false;\n    set required_permissions := { sys::perm::approximate_count };\n};\n\nCREATE REQUIRED GLOBAL sys::current_role -> str {\n    SET default := '';\n};\n\nCREATE REQUIRED GLOBAL sys::current_permissions -> array<str> {\n    SET default := <array<str>>[];\n};\n\n# Add permissions to schema and std.\n\n# These modules are populated before sys permissions so we need to\n# add these restrictions here.\n\nALTER TYPE schema::Migration {\n    CREATE ACCESS POLICY ap_read allow select using (\n        global sys::perm::ddl\n    );\n};\n\nALTER FUNCTION std::sequence_reset(\n    seq: schema::ScalarType,\n    value: std::int64,\n) {\n    SET required_permissions := sys::perm::ddl;\n};\nALTER FUNCTION std::sequence_reset(\n    seq: schema::ScalarType,\n) {\n    SET required_permissions := sys::perm::ddl;\n};\n"
  },
  {
    "path": "edb/load_ext/main.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Command to load an extension into an edgedb installation.\n\nIt is a command distributed with the server, but it is designed so\nthat it has no dependencies and does not import any server code\nif it is *only* installing the postgres part of an extension with\na specified pg_config, so it *can* be pulled out and used standalone.\n(It requires Python 3.11 for tomllib.)\n\"\"\"\n\nfrom __future__ import annotations\n\n\nimport argparse\nimport json\nimport os\nimport pathlib\nimport shutil\nimport subprocess\nimport sys\nimport tempfile\nimport tomllib\nimport zipfile\n\n# Directories that we map to config values in pg_config.\nCONFIG_PATHS = {\n    'share': 'sharedir',\n    'lib': 'pkglibdir',\n    'include': 'pkgincludedir-server',\n}\n\n\ndef install_pg_extension(\n    pkg: pathlib.Path,\n    pg_config: dict[str, str],\n    manifest_target: pathlib.Path | None,\n) -> None:\n\n    to_install = []\n    with zipfile.ZipFile(pkg) as z:\n        base = get_dir(z)\n\n        # Compute what files to install\n        for entry in z.infolist():\n            fpath = pathlib.Path(entry.filename)\n\n            if entry.is_dir():\n                continue\n            if fpath.parts[0] != str(base):\n                continue\n            # If the path is too short or isn't one of the\n            # directories we know about, skip it.\n            if (\n                len(fpath.parts) < 3\n                or not (config_field := CONFIG_PATHS.get(fpath.parts[1]))\n                or fpath.parts[2] != 'postgresql'\n            ):\n                # print(\"Skipping\", fpath)\n                continue\n\n            fpath = fpath.relative_to(\n                pathlib.Path(fpath.parts[0])\n                / fpath.parts[1]\n                / 'postgresql'\n            )\n            to_install.append((entry.filename, config_field, fpath))\n\n        # Write a manifest out of all the files installed into the\n        # postgres installation.\n        if manifest_target:\n            manifest_contents = [\n                {'postgres_dir': config_field, 'path': str(fpath)}\n                for _, config_field, fpath in to_install\n            ]\n            with open(manifest_target, \"w\") as f:\n                json.dump(manifest_contents, f)\n\n        # Install them\n        for zip_name, config_field, fpath in to_install:\n            config_dir = pg_config[config_field]\n            target_file = config_dir / fpath\n\n            os.makedirs(target_file.parent, exist_ok=True)\n            with z.open(zip_name) as src:\n                with open(target_file, \"wb\") as dst:\n                    print(\"Installing\", target_file)\n                    shutil.copyfileobj(src, dst)\n\n\ndef uninstall_pg_extension(\n    pg_manifest: list[dict[str, str]],\n    pg_config: dict[str, str],\n) -> None:\n    for entry in pg_manifest:\n        config_field = entry['postgres_dir']\n        fpath = entry['path']\n\n        full_path = pathlib.Path(pg_config[config_field]) / fpath\n        print(\"Removing\", full_path)\n        try:\n            os.remove(full_path)\n        except FileNotFoundError:\n            print(\"Could not remove missing\", full_path)\n\n\ndef get_pg_config(pg_config_path: pathlib.Path) -> dict[str, str]:\n    output = subprocess.run(\n        pg_config_path,\n        capture_output=True,\n        text=True,\n        check=True,\n    )\n    stdout_lines = output.stdout.split('\\n')\n\n    config = {}\n    for line in stdout_lines:\n        k, eq, v = line.partition('=')\n        if eq:\n            config[k.strip().lower()] = v.strip()\n\n    return config\n\n\ndef get_dir(z: zipfile.ZipFile) -> pathlib.Path:\n    files = z.infolist()\n    if not (files and files[0].is_dir()):\n        print('ERROR: Extension package must contain one top-level dir')\n        sys.exit(1)\n    dirname = pathlib.Path(files[0].filename)\n\n    return dirname\n\n\ndef install_edgedb_extension(\n    pkg: pathlib.Path,\n    ext_dir: pathlib.Path,\n) -> pathlib.Path:\n    with tempfile.TemporaryDirectory() as tdir, \\\n         zipfile.ZipFile(pkg) as z:\n\n        dirname = get_dir(z)\n\n        target = ext_dir / dirname\n        if target.exists():\n            print(\n                f'ERROR: Extension {dirname} is already installed at {target}'\n            )\n            sys.exit(1)\n\n        print(\"Installing\", target)\n\n        ttarget = pathlib.Path(tdir) / dirname\n        os.mkdir(ttarget)\n\n        with z.open(str(dirname / 'MANIFEST.toml')) as m:\n            manifest = tomllib.load(m)\n\n        files = ['MANIFEST.toml'] + manifest['files']\n\n        for f in files:\n            target_file = target / f\n            ttarget_file = ttarget / f\n\n            with z.open(str(dirname / f)) as src:\n                with open(ttarget_file, \"wb\") as dst:\n                    print(\"Installing\", target_file)\n                    shutil.copyfileobj(src, dst)\n\n        os.makedirs(ext_dir, exist_ok=True)\n        # If there was a race and the file was created between the\n        # earlier check and now, we'll produce a worse error\n        # message. Oh well.\n        shutil.move(ttarget, ext_dir)\n\n    return target\n\n\ndef load_ext_install(\n    package: pathlib.Path,\n    skip_edgedb: bool,\n    skip_gel: bool,\n    skip_postgres: bool,\n    with_pg_config: pathlib.Path | None,\n) -> None:\n    target_dir = None\n    if not skip_edgedb and not skip_gel:\n        from edb import buildmeta\n\n        ext_dir = buildmeta.get_extension_dir_path()\n        target_dir = install_edgedb_extension(package, ext_dir)\n\n    if not skip_postgres:\n        if with_pg_config is None:\n            from edb import buildmeta\n            with_pg_config = buildmeta.get_pg_config_path()\n\n        pg_config = get_pg_config(with_pg_config)\n        pg_manifest = target_dir / \"PG_MANIFEST.json\" if target_dir else None\n        install_pg_extension(package, pg_config, pg_manifest)\n\n\ndef load_ext_uninstall(\n    package: pathlib.Path,\n    skip_edgedb: bool,\n    skip_gel: bool,\n    skip_postgres: bool,\n    with_pg_config: pathlib.Path | None,\n) -> None:\n    from edb import buildmeta\n    target_dir = None\n    if len(package.parts) != 1:\n        print(\n            f'ERROR: {package} is not a valid extension name'\n        )\n        sys.exit(1)\n\n    ext_dir = buildmeta.get_extension_dir_path()\n    target_dir = ext_dir / package\n\n    if not target_dir.exists():\n        print(\n            f'ERROR: Extension {package} is not currently '\n            f'installed at {target_dir}'\n        )\n        sys.exit(1)\n\n    if not skip_postgres:\n        try:\n            with open(target_dir / \"PG_MANIFEST.json\") as f:\n                pg_manifest = json.load(f)\n        except FileNotFoundError:\n            pg_manifest = []\n\n        if with_pg_config is None:\n            with_pg_config = buildmeta.get_pg_config_path()\n\n        pg_config = get_pg_config(with_pg_config)\n        uninstall_pg_extension(pg_manifest, pg_config)\n\n    if not skip_edgedb and not skip_gel:\n        print(\"Removing\", target_dir)\n        shutil.rmtree(target_dir)\n\n\ndef load_ext_list_packages() -> None:\n    from edb import buildmeta\n\n    ext_dir = buildmeta.get_extension_dir_path()\n\n    exts = []\n    try:\n        with os.scandir(ext_dir) as it:\n            for entry in it:\n                entry_path = pathlib.Path(entry)\n                manifest_path = entry_path / 'MANIFEST.toml'\n                if (\n                    entry.is_dir()\n                    and manifest_path.exists()\n                ):\n                    with open(manifest_path, 'rb') as m:\n                        manifest = tomllib.load(m)\n\n                    info = dict(\n                        key=entry_path.name,\n                        extension_name=manifest['name'],\n                        extension_version=manifest['version'],\n                        path=str(entry_path.absolute()),\n                    )\n\n                    exts.append(info)\n    except FileNotFoundError:\n        pass\n\n    print(json.dumps(exts, indent=4))\n\n\ndef load_ext_main(\n    *,\n    package: pathlib.Path | None,\n    uninstall: pathlib.Path | None,\n    list_packages: bool,\n    **kwargs,\n) -> None:\n    if uninstall:\n        load_ext_uninstall(uninstall, **kwargs)\n    elif package:\n        load_ext_install(package, **kwargs)\n    elif list_packages:\n        load_ext_list_packages()\n    else:\n        raise AssertionError('No command specified?')\n\n\nparser = argparse.ArgumentParser(description='Install an extension package')\nparser.add_argument(\n    '--skip-gel', action='store_true',\n    help=\"Skip installing the extension package into the Gel \"\n          \"installation\",\n)\nparser.add_argument(\n    '--skip-edgedb', action='store_true', help=argparse.SUPPRESS,\n)\nparser.add_argument(\n    '--skip-postgres', action='store_true',\n    help=\"Skip installing the extension package into the \"\n         \"Postgres installation\",\n)\nparser.add_argument(\n    '--with-pg-config', metavar='PATH',\n    help=\"Use the specified pg_config binary to find the Postgres \"\n         \"to install into (instead of using the bundled one)\"\n)\ngroup = parser.add_mutually_exclusive_group(required=True)\ngroup.add_argument(\n    '--list-packages', action='store_true',\n    help=\"List the extension packages that are installed (in JSON)\"\n)\ngroup.add_argument(\n    '--uninstall', metavar='NAME',\n    type=pathlib.Path,\n    help=\"Uninstall a package (by package directory name) instead of \"\n         \"installing it\"\n)\ngroup.add_argument('package', nargs='?', type=pathlib.Path)\n\n\ndef main(argv: tuple[str, ...] | None = None):\n    argv = argv if argv is not None else tuple(sys.argv[1:])\n    args = parser.parse_args(argv)\n    load_ext_main(**vars(args))\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "edb/pgsql/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n"
  },
  {
    "path": "edb/pgsql/ast.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport enum\nimport dataclasses\nimport typing\nimport uuid\n\nfrom edb.common import ast, span\nfrom edb.common import typeutils\nfrom edb.edgeql import ast as qlast\nfrom edb.ir import ast as irast\n\nif typing.TYPE_CHECKING:\n    # PathAspect is imported without qualifiers here because otherwise in\n    # base.AST._collect_direct_fields, typing.get_type_hints will not correctly\n    # locate the type.\n    from .compiler.enums import PathAspect\n\n\n# The structure of the nodes mostly follows that of Postgres'\n# parsenodes.h and primnodes.h, but only with fields that are\n# relevant to parsing and code generation.\n#\n# Certain nodes have EdgeDB-specific fields used by the\n# compiler.\n\n\nSpan = span.Span\n\n\nclass Base(ast.AST):\n    __ast_hidden__ = {'span'}\n\n    span: typing.Optional[Span] = None\n\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n\n    def __repr__(self):\n        return f'<pg.{self.__class__.__name__} at 0x{id(self):x}>'\n\n    def dump_sql(self) -> None:\n        from edb.common.debug import dump_sql\n        dump_sql(self, reordered=True, pretty=True)\n\n\nclass ImmutableBase(ast.ImmutableASTMixin, Base):\n    __ast_mutable_fields__ = frozenset(['span'])\n\n\nclass Alias(ImmutableBase):\n    \"\"\"Alias for a range variable.\"\"\"\n\n    # aliased relation name\n    aliasname: str\n    # optional list of column aliases\n    colnames: typing.Optional[list[str]] = None\n\n\nclass Keyword(ImmutableBase):\n    \"\"\"An SQL keyword that must be output without quoting.\"\"\"\n\n    name: str                   # Keyword name\n\n\nclass Star(Base):\n    \"\"\"'*' representing all columns of a table or compound field.\"\"\"\n\n\nclass BaseExpr(Base):\n    \"\"\"Any non-statement expression node that returns a value.\"\"\"\n\n    __ast_meta__ = {'nullable'}\n\n    nullable: typing.Optional[bool] = None  # Whether the result can be NULL.\n    ser_safe: bool = False  # Whether the expr is serialization-safe.\n\n    def __init__(\n        self, *, nullable: typing.Optional[bool] = None, **kwargs\n    ) -> None:\n        nullable = self._is_nullable(kwargs, nullable)\n        super().__init__(nullable=nullable, **kwargs)\n\n    def _is_nullable(\n        self, kwargs: dict[str, object], nullable: typing.Optional[bool]\n    ) -> bool:\n        if nullable is None:\n            default = type(self).get_field('nullable').default\n            if default is not None:\n                nullable = default\n            else:\n                nullable = self._infer_nullability(kwargs)\n        return nullable\n\n    def _infer_nullability(self, kwargs: dict[str, object]) -> bool:\n        nullable = False\n        for v in kwargs.values():\n            if typeutils.is_container(v):\n                items = typing.cast(typing.Iterable, v)\n                nullable = any(getattr(vv, 'nullable', False) for vv in items)\n\n            elif getattr(v, 'nullable', None):\n                nullable = True\n\n            if nullable:\n                break\n\n        return nullable\n\n\nclass ImmutableBaseExpr(BaseExpr, ImmutableBase):\n    pass\n\n\nclass OutputVar(ImmutableBaseExpr):\n    \"\"\"A base class representing expression output address.\"\"\"\n\n    # Whether this represents a packed array of data\n    is_packed_multi: bool = False\n\n\nclass ExprOutputVar(OutputVar):\n    \"\"\"A \"fake\" output var representing a wrapped BaseExpr.\n\n    In some obscure cases (specifically, returning __type__ from a\n    non-view base relation that doesn't actually contain it), we need\n    to return a non output var value from something expecting\n    OutputVar.\n\n    Instead of fully blowing away the type discipline of OutputVar\n    and making everything operate on BaseExpr, we require such expressions\n    to be explicitly wrapped.\n    \"\"\"\n\n    expr: BaseExpr\n\n\nclass EdgeQLPathInfo(Base):\n    \"\"\"A general mixin providing EdgeQL-specific metadata on certain nodes.\"\"\"\n\n    # Ignore the below fields in AST visitor/transformer.\n    __ast_meta__ = {\n        'path_id', 'path_bonds', 'path_outputs', 'is_distinct',\n        'path_id_mask', 'path_namespace',\n        'packed_path_outputs', 'packed_path_namespace',\n    }\n\n    # The path id represented by the node.\n    path_id: typing.Optional[irast.PathId] = None\n\n    # Whether the node represents a distinct set.\n    is_distinct: bool = True\n\n    # A subset of paths necessary to perform joining.\n    path_bonds: set[tuple[irast.PathId, bool]] = ast.field(factory=set)\n\n    # Whether to ignore namespaces when looking at path outputs.\n    # TODO: Maybe instead, Relation should have a way of specifying\n    # output by PointerRef instead.\n    strip_output_namespaces: bool = False\n\n    # Map of res target names corresponding to paths.\n    path_outputs: dict[\n        tuple[irast.PathId, PathAspect], OutputVar\n    ] = ast.field(factory=dict)\n\n    # Map of res target names corresponding to materialized paths.\n    packed_path_outputs: typing.Optional[dict[\n        tuple[irast.PathId, PathAspect],\n        OutputVar,\n    ]] = None\n\n    def get_path_outputs(\n        self, flavor: str\n    ) -> dict[tuple[irast.PathId, PathAspect], OutputVar]:\n        if flavor == 'packed':\n            if self.packed_path_outputs is None:\n                self.packed_path_outputs = {}\n            return self.packed_path_outputs\n        elif flavor == 'normal':\n            return self.path_outputs\n        else:\n            raise AssertionError(f'unexpected flavor \"{flavor}\"')\n\n    path_id_mask: set[irast.PathId] = ast.field(factory=set)\n\n    # Map of col refs corresponding to paths.\n    path_namespace: dict[\n        tuple[irast.PathId, PathAspect],\n        BaseExpr,\n    ] = ast.field(factory=dict)\n\n    # Same, but for packed.\n    packed_path_namespace: typing.Optional[dict[\n        tuple[irast.PathId, PathAspect],\n        BaseExpr,\n    ]] = None\n\n\nclass BaseRangeVar(ImmutableBaseExpr):\n    \"\"\"\n    Range variable, used in FROM clauses.\n\n    This can be though as a specific instance of a table within a query.\n    \"\"\"\n\n    __ast_meta__ = {'schema_object_id', 'tag', 'ir_origins'}\n    __ast_mutable_fields__ = frozenset(['ir_origins', 'span'])\n\n    # This is a hack, since there is some code that relies on not\n    # having an alias on a range var (to refer to a CTE directly, for\n    # example, while other code depends on reading the alias name out\n    # of range vars. This is mostly disjoint code, so we hack around it\n    # with an empty aliasname.\n    alias: Alias = Alias(aliasname='')\n\n    #: The id of the schema object this rvar represents\n    schema_object_id: typing.Optional[uuid.UUID] = None\n\n    #: Optional identification piece to describe what's inside the rvar\n    tag: typing.Optional[str] = None\n\n    #: Optional reference to the sets that this refers to\n    #: Only used for helping recover information during explain.\n    #: The type is a list of objects to help prevent any thought\n    #: of using this field computationally during compilation.\n    ir_origins: typing.Optional[list[object]] = None\n\n    def __repr__(self) -> str:\n        return (\n            f'<pg.{self.__class__.__name__} '\n            f'alias={self.alias.aliasname} '\n            f'at {id(self):#x}>'\n        )\n\n\nclass BaseRelation(EdgeQLPathInfo, BaseExpr):\n    \"\"\"\n    A relation-valued (table-valued) expression.\n    \"\"\"\n\n    name: typing.Optional[str] = None\n    nullable: typing.Optional[bool] = None  # Whether the result can be NULL.\n\n\nclass Relation(BaseRelation):\n    \"\"\"A reference to a table or a view.\"\"\"\n\n    # The type or pointer this represents.\n    # Should be non-None for any relation arising from a type or\n    # pointer during compilation.\n    type_or_ptr_ref: typing.Optional[irast.TypeRef | irast.PointerRef] = None\n\n    catalogname: typing.Optional[str] = None\n    schemaname: typing.Optional[str] = None\n    is_temporary: typing.Optional[bool] = None\n\n\nclass CommonTableExpr(Base):\n\n    # Query name (unqualified)\n    name: str\n    # Whether the result can be NULL.\n    nullable: typing.Optional[bool] = None\n    # Optional list of column names\n    aliascolnames: typing.Optional[list[str]] = None\n    # The CTE query\n    query: Query\n    # True if this CTE is recursive\n    recursive: bool = False\n    # If specified, determines if CTE is [NOT] MATERIALIZED\n    materialized: typing.Optional[bool] = None\n\n    # the dml stmt that this CTE was generated for\n    for_dml_stmt: typing.Optional[irast.MutatingLikeStmt] = None\n\n    # marks the CTE that contains the output of a DML operation\n    # (so it can be used in RETURNING and CommandComplete tag)\n    output_of_dml: typing.Optional[irast.MutatingLikeStmt] = None\n\n    def __repr__(self):\n        return (\n            f'<pg.{self.__class__.__name__} '\n            f'name={self.name!r} at 0x{id(self):x}>'\n        )\n\n\nclass PathRangeVar(BaseRangeVar):\n\n    #: The IR TypeRef this rvar represents (if any).\n    typeref: typing.Optional[irast.TypeRef] = None\n\n    @property\n    def query(self) -> BaseRelation:\n        raise NotImplementedError\n\n\nclass RelRangeVar(PathRangeVar):\n    \"\"\"Relation range variable, used in FROM clauses.\"\"\"\n\n    relation: BaseRelation | CommonTableExpr\n    include_inherited: bool = True\n\n    @property\n    def query(self) -> BaseRelation:\n        if isinstance(self.relation, CommonTableExpr):\n            return self.relation.query\n        else:\n            return self.relation\n\n    def __repr__(self) -> str:\n        return (\n            f'<pg.{self.__class__.__name__} '\n            f'name={self.relation.name!r} alias={self.alias.aliasname} '\n            f'at {id(self):#x}>'\n        )\n\n\nclass IntersectionRangeVar(PathRangeVar):\n\n    component_rvars: list[PathRangeVar]\n\n\nclass DynamicRangeVarFunc(typing.Protocol):\n    \"\"\"A 'dynamic' range var that provides a callback hook\n    for finding path_ids in range var.\n\n    Used to sneak more complex search logic in.\n    I am 100% going to regret this.\n\n    Update: Sully says that he hasn't regretted it yet.\n    \"\"\"\n\n    # Lookup function for a DynamicRangeVar. If it returns a\n    # PathRangeVar, keep looking in that rvar. If it returns\n    # another expression, that's the output.\n    def __call__(\n        self,\n        rel: Query,\n        path_id: irast.PathId,\n        *,\n        flavor: str,\n        aspect: str,\n        env: typing.Any,\n    ) -> typing.Optional[BaseExpr | PathRangeVar]:\n        pass\n\n\nclass DynamicRangeVar(PathRangeVar):\n\n    dynamic_get_path: DynamicRangeVarFunc\n\n    @property\n    def query(self) -> BaseRelation:\n        raise AssertionError('cannot retrieve query from a dynamic range var')\n\n    # pickling is broken here, oh well\n    def __getstate__(self) -> typing.Any:\n        return ()\n\n    def __setstate__(self, state: typing.Any) -> None:\n        self.dynamic_get_path = None  # type: ignore\n\n\nclass TypeName(ImmutableBase):\n    \"\"\"Type in definitions and casts.\"\"\"\n\n    name: tuple[str, ...]                # Type name\n    setof: bool = False                         # SET OF?\n    typmods: typing.Optional[list] = None       # Type modifiers\n    array_bounds: typing.Optional[list[int]] = None\n\n\nclass ColumnRef(OutputVar):\n    \"\"\"Specifies a reference to a column.\"\"\"\n\n    # Column name list.\n    name: typing.Sequence[str | Star]\n    # Whether the col is an optional path bond (i.e accepted when NULL)\n    optional: typing.Optional[bool] = None\n\n    def __repr__(self):\n        if hasattr(self, 'name'):\n            return (\n                f'<pg.{self.__class__.__name__} '\n                f'name={\".\".join(self.name)!r} at 0x{id(self):x}>'\n            )\n        else:\n            return super().__repr__()\n\n\nclass TupleElementBase(ImmutableBase):\n\n    path_id: irast.PathId\n    name: typing.Optional[OutputVar | str]\n\n    def __init__(\n        self,\n        path_id: irast.PathId,\n        name: typing.Optional[OutputVar | str] = None,\n    ):\n        self.path_id = path_id\n        self.name = name\n\n    def __repr__(self):\n        return (\n            f'<{self.__class__.__name__} '\n            f'name={self.name} path_id={self.path_id}>'\n        )\n\n\nclass TupleElement(TupleElementBase):\n\n    val: BaseExpr\n\n    def __init__(\n        self,\n        path_id: irast.PathId,\n        val: BaseExpr,\n        *,\n        name: typing.Optional[OutputVar | str] = None,\n    ):\n        super().__init__(path_id, name)\n        self.val = val\n\n    def __repr__(self):\n        return (\n            f'<{self.__class__.__name__} '\n            f'name={self.name} val={self.val} path_id={self.path_id}>'\n        )\n\n\nclass TupleVarBase(OutputVar):\n\n    elements: typing.Sequence[TupleElementBase]\n    named: bool\n    nullable: bool\n    typeref: typing.Optional[irast.TypeRef]\n\n    def __init__(\n        self,\n        elements: list[TupleElementBase],\n        *,\n        named: bool = False,\n        nullable: bool = False,\n        is_packed_multi: bool = False,\n        typeref: typing.Optional[irast.TypeRef] = None,\n    ):\n        self.elements = elements\n        self.named = named\n        self.nullable = nullable\n        self.is_packed_multi = is_packed_multi\n        self.typeref = typeref\n\n    def __repr__(self):\n        return f'<{self.__class__.__name__} [{self.elements!r}]'\n\n\nclass TupleVar(TupleVarBase):\n\n    elements: typing.Sequence[TupleElement]\n\n    def __init__(\n        self,\n        elements: list[TupleElement],\n        *,\n        named: bool = False,\n        nullable: bool = False,\n        is_packed_multi: bool = False,\n        typeref: typing.Optional[irast.TypeRef] = None,\n    ):\n        self.elements = elements\n        self.named = named\n        self.nullable = nullable\n        self.is_packed_multi = is_packed_multi\n        self.typeref = typeref\n\n\nclass ParamRef(ImmutableBaseExpr):\n    \"\"\"Query parameter ($0..$n).\"\"\"\n\n    __ast_mutable_fields__ = (\n        ImmutableBaseExpr.__ast_mutable_fields__ | frozenset(['number']))\n\n    # Number of the parameter.\n    number: int\n\n\nclass ResTarget(ImmutableBaseExpr):\n    \"\"\"Query result target.\"\"\"\n\n    # Column name (optional)\n    name: typing.Optional[str] = None\n    # value expression to compute\n    val: BaseExpr\n\n\nclass InsertTarget(ImmutableBaseExpr):\n    \"\"\"Column reference in INSERT.\"\"\"\n\n    # Column name\n    name: str\n\n\nclass UpdateTarget(ImmutableBaseExpr):\n    \"\"\"Query update target.\"\"\"\n\n    # column names\n    name: str\n    # value expression to assign\n    val: BaseExpr\n    # subscripts, field names and '*'\n    indirection: typing.Optional[list[IndirectionOp]] = None\n\n\nclass OnConflictTarget(ImmutableBaseExpr):\n    # IndexElems to infer unique index\n    index_elems: typing.Optional[list[IndexElem]] = None\n    # Partial-index predicate\n    index_where: typing.Optional[BaseExpr] = None\n\n    # Constraint name\n    constraint_name: typing.Optional[str] = None\n\n\nclass IndexElem(ImmutableBaseExpr):\n    expr: BaseExpr\n    ordering: typing.Optional[qlast.SortOrder] = None\n    nulls_ordering: typing.Optional[qlast.NonesOrder] = None\n\n\nclass OnConflictAction(enum.StrEnum):\n    DO_NOTHING = \"DO_NOTHING\"\n    DO_UPDATE = \"DO_UPDATE\"\n\n\nclass OnConflictClause(ImmutableBaseExpr):\n\n    action: OnConflictAction\n    target: typing.Optional[OnConflictTarget] = None\n\n    update_list: typing.Optional[list[UpdateTarget | MultiAssignRef]] = None\n    update_where: typing.Optional[BaseExpr] = None\n\n\nclass ReturningQuery(BaseRelation):\n\n    target_list: list[ResTarget] = ast.field(factory=list)\n\n\nclass NullRelation(ReturningQuery):\n    \"\"\"Special relation that produces nulls for all its attributes.\"\"\"\n\n    type_or_ptr_ref: typing.Optional[irast.TypeRef | irast.PointerRef] = None\n\n    where_clause: typing.Optional[BaseExpr] = None\n\n\n@dataclasses.dataclass\nclass Param:\n    #: postgres' variable index\n    index: int\n\n    #: whether parameter is required\n    required: bool\n\n    #: index in the \"logical\" arg map\n    logical_index: int\n\n\nclass Query(ReturningQuery):\n    \"\"\"Generic superclass representing a query.\"\"\"\n\n    # Ignore the below fields in AST visitor/transformer.\n    __ast_meta__ = {'path_rvar_map', 'path_packed_rvar_map',\n                    'view_path_id_map', 'argnames', 'nullable'}\n\n    view_path_id_map: dict[\n        irast.PathId, irast.PathId\n    ] = ast.field(factory=dict)\n    # Map of RangeVars corresponding to paths.\n    path_rvar_map: dict[\n        tuple[irast.PathId, PathAspect], PathRangeVar\n    ] = ast.field(factory=dict)\n    # Map of materialized RangeVars corresponding to paths.\n    path_packed_rvar_map: typing.Optional[dict[\n        tuple[irast.PathId, PathAspect],\n        PathRangeVar,\n    ]] = None\n\n    argnames: typing.Optional[dict[str, Param]] = None\n\n    ctes: typing.Optional[list[CommonTableExpr]] = None\n\n    def get_rvar_map(\n        self, flavor: str\n    ) -> dict[tuple[irast.PathId, PathAspect], PathRangeVar]:\n        if flavor == 'packed':\n            if self.path_packed_rvar_map is None:\n                self.path_packed_rvar_map = {}\n            return self.path_packed_rvar_map\n        elif flavor == 'normal':\n            return self.path_rvar_map\n        else:\n            raise AssertionError(f'unexpected flavor \"{flavor}\"')\n\n    def maybe_get_rvar_map(\n        self, flavor: str\n    ) -> typing.Optional[\n        dict[tuple[irast.PathId, PathAspect], PathRangeVar]\n    ]:\n        if flavor == 'packed':\n            return self.path_packed_rvar_map\n        elif flavor == 'normal':\n            return self.path_rvar_map\n        else:\n            raise AssertionError(f'unexpected flavor \"{flavor}\"')\n\n    @property\n    def ser_safe(self):\n        if not self.target_list:\n            return False\n        return all(t.ser_safe for t in self.target_list)\n\n    def append_cte(self, cte: CommonTableExpr) -> None:\n        if self.ctes is None:\n            self.ctes = []\n        self.ctes.append(cte)\n\n\nclass DMLQuery(Query):\n    \"\"\"Generic superclass for INSERT/UPDATE/DELETE statements.\"\"\"\n    __abstract_node__ = True\n\n    # Target relation to perform the operation on.\n    relation: RelRangeVar\n    # List of expressions returned\n    returning_list: list[ResTarget] = ast.field(factory=list)\n\n    @property\n    def target_list(self):\n        return self.returning_list\n\n\nclass InsertStmt(DMLQuery):\n\n    # (optional) list of target column names\n    cols: typing.Optional[list[InsertTarget]] = None\n    # source SELECT/VALUES or None\n    select_stmt: typing.Optional[Query] = None\n    # ON CONFLICT clause\n    on_conflict: typing.Optional[OnConflictClause] = None\n\n\nclass UpdateStmt(DMLQuery):\n\n    # The UPDATE target list\n    targets: list[UpdateTarget | MultiAssignRef] = ast.field(\n        factory=list\n    )\n    # WHERE clause\n    where_clause: typing.Optional[BaseExpr] = None\n    # optional FROM clause\n    from_clause: list[BaseRangeVar] = ast.field(factory=list)\n\n\nclass DeleteStmt(DMLQuery):\n    # WHERE clause\n    where_clause: typing.Optional[BaseExpr] = None\n    # optional USING clause\n    using_clause: list[BaseRangeVar] = ast.field(factory=list)\n\n\nclass SelectStmt(Query):\n\n    # List of DISTINCT ON expressions, empty list for DISTINCT ALL\n    distinct_clause: typing.Optional[typing.Sequence[OutputVar | Star]] = None\n    # The FROM clause\n    from_clause: list[BaseRangeVar] = ast.field(factory=list)\n    # The WHERE clause\n    where_clause: typing.Optional[BaseExpr] = None\n    # GROUP BY clauses\n    group_clause: typing.Optional[list[Base]] = None\n    # HAVING expression\n    having_clause: typing.Optional[BaseExpr] = None\n    # WINDOW window_name AS(...),\n    window_clause: typing.Optional[list[Base]] = None\n    # List of ImplicitRow's in a VALUES query\n    values: typing.Optional[list[Base]] = None\n    # ORDER BY clause\n    sort_clause: typing.Optional[list[SortBy]] = None\n    # OFFSET expression\n    limit_offset: typing.Optional[BaseExpr] = None\n    # LIMIT expression\n    limit_count: typing.Optional[BaseExpr] = None\n    # FOR UPDATE clause\n    locking_clause: typing.Optional[list[LockingClause]] = None\n\n    # Set operation type\n    op: typing.Optional[str] = None\n    # ALL modifier\n    all: bool = False\n    # Left operand of set op\n    larg: typing.Optional[Query] = None\n    # Right operand of set op,\n    rarg: typing.Optional[Query] = None\n\n    # When used as a sub-query, it is generally nullable.\n    nullable: bool = True\n\n\nclass Expr(ImmutableBaseExpr):\n    \"\"\"Infix, prefix, and postfix expressions.\"\"\"\n\n    # Possibly-qualified name of operator\n    name: str\n    # Left argument, if any\n    lexpr: typing.Optional[BaseExpr] = None\n    # Right argument, if any\n    rexpr: typing.Optional[BaseExpr] = None\n\n\nclass BaseConstant(ImmutableBaseExpr):\n    pass\n\n\nclass StringConstant(BaseConstant):\n    \"\"\"A literal string constant.\"\"\"\n\n    # Constant value\n    val: str\n\n\nclass NullConstant(BaseConstant):\n    \"\"\"A NULL constant.\"\"\"\n\n    nullable: bool = True\n\n\nclass BitStringConstant(BaseConstant):\n    \"\"\"A bit string constant.\"\"\"\n\n    # x or b\n    kind: str\n\n    val: str\n\n\nclass ByteaConstant(BaseConstant):\n    \"\"\"A bytea string.\"\"\"\n\n    val: bytes\n\n\nclass NumericConstant(BaseConstant):\n    val: str\n\n\nclass BooleanConstant(BaseConstant):\n    val: bool\n\n\nclass LiteralExpr(ImmutableBaseExpr):\n    \"\"\"A literal expression.\"\"\"\n\n    # Expression text\n    expr: str\n\n\nclass TypeCast(ImmutableBaseExpr):\n    \"\"\"A CAST expression.\"\"\"\n\n    # Expression being casted.\n    arg: BaseExpr\n    # Target type.\n    type_name: TypeName\n\n\nclass CollateClause(ImmutableBaseExpr):\n    \"\"\"A COLLATE expression.\"\"\"\n\n    # Input expression\n    arg: BaseExpr\n    # Possibly-qualified collation name\n    collname: typing.Sequence[str]\n\n\nclass VariadicArgument(ImmutableBaseExpr):\n\n    expr: BaseExpr\n    nullable: bool = False\n\n\nclass TableElement(ImmutableBase):\n    pass\n\n\nclass ColumnDef(TableElement):\n\n    # name of column\n    name: str\n    # type of column\n    typename: TypeName\n    # default value, if any\n    default_expr: typing.Optional[BaseExpr] = None\n    # COLLATE clause, if any\n    coll_clause: typing.Optional[BaseExpr] = None\n\n    # NOT NULL\n    is_not_null: bool = False\n\n\nclass FuncCall(ImmutableBaseExpr):\n\n    # Function name\n    name: tuple[str, ...]\n    # List of arguments\n    args: list[BaseExpr]\n    # ORDER BY\n    agg_order: typing.Optional[list[SortBy]]\n    # FILTER clause\n    agg_filter: typing.Optional[BaseExpr]\n    # Argument list is '*'\n    agg_star: bool\n    # Arguments were labeled DISTINCT\n    agg_distinct: bool\n    # arg_order is in WITHIN GROUP (...)\n    agg_within_group: bool = False\n    # OVER clause, if any\n    over: typing.Optional[WindowDef]\n    # WITH ORDINALITY\n    with_ordinality: bool = False\n    # list of Columndef  nodes to describe result of\n    # the function returning RECORD.\n    coldeflist: list[ColumnDef]\n\n    def __init__(\n        self,\n        *,\n        nullable: typing.Optional[bool] = None,\n        null_safe: bool = False,\n        **kwargs,\n    ) -> None:\n        \"\"\"Function call node.\n\n        @param null_safe:\n            Specifies whether this function is guaranteed\n            to never return NULL on non-NULL input.\n        \"\"\"\n        if nullable is None and not null_safe:\n            nullable = True\n        super().__init__(nullable=nullable, **kwargs)\n\n\nclass NamedFuncArg(ImmutableBaseExpr):\n\n    name: str\n    val: BaseExpr\n\n\n# N.B: Index and Slice aren't *really* Exprs but we mark them as such\n# so that nullability inference gets done on them.\nclass Index(ImmutableBaseExpr):\n    \"\"\"Array subscript.\"\"\"\n    idx: BaseExpr\n\n\nclass Slice(ImmutableBaseExpr):\n    \"\"\"Array slice bounds.\"\"\"\n    # Lower bound, if any\n    lidx: typing.Optional[BaseExpr]\n    # Upper bound if any\n    ridx: typing.Optional[BaseExpr]\n\n\nclass RecordIndirectionOp(ImmutableBase):\n    name: str\n\n\nIndirectionOp = Slice | Index | Star | RecordIndirectionOp\n\n\nclass Indirection(ImmutableBaseExpr):\n    \"\"\"Field and/or array element indirection.\"\"\"\n\n    # Indirection subject\n    arg: BaseExpr\n    # Subscripts and/or field names and/or '*'\n    indirection: list[IndirectionOp]\n\n\nclass ArrayExpr(ImmutableBaseExpr):\n    \"\"\"ARRAY[] construct.\"\"\"\n\n    # array element expressions\n    elements: list[BaseExpr]\n\n\nclass ArrayDimension(ImmutableBaseExpr):\n    \"\"\"An array dimension\"\"\"\n    elements: list[BaseExpr]\n\n\nclass MultiAssignRef(ImmutableBase):\n    \"\"\"UPDATE (a, b, c) = row-valued-expr.\"\"\"\n\n    # row-valued expression\n    source: BaseExpr\n    # list of columns to assign to\n    columns: list[str]\n\n\nclass SortBy(ImmutableBase):\n    \"\"\"ORDER BY clause element.\"\"\"\n\n    # expression to sort on\n    node: BaseExpr\n    # ASC/DESC/USING/default\n    dir: typing.Optional[qlast.SortOrder] = None\n    # NULLS FIRST/LAST\n    nulls: typing.Optional[qlast.NonesOrder] = None\n\n\nclass LockClauseStrength(enum.StrEnum):\n    UPDATE = \"UPDATE\"\n    NO_KEY_UPDATE = \"NO KEY UPDATE\"\n    SHARE = \"SHARE\"\n    KEY_SHARE = \"KEY SHARE\"\n\n\nclass LockWaitPolicy(enum.StrEnum):\n    LockWaitBlock = \"\"\n    LockWaitSkip = \"SKIP LOCKED\"\n    LockWaitError = \"NOWAIT\"\n\n\nclass LockingClause(ImmutableBase):\n    \"\"\"Locking clause element (FOR ... )\"\"\"\n\n    strength: LockClauseStrength\n    \"lock strength\"\n\n    locked_rels: typing.Optional[list[RelRangeVar]] = None\n    \"locked relations\"\n\n    wait_policy: typing.Optional[LockWaitPolicy] = None\n    \"lock wait policy\"\n\n\nclass WindowDef(ImmutableBase):\n    \"\"\"WINDOW and OVER clauses.\"\"\"\n\n    # window name\n    name: typing.Optional[str] = None\n    # referenced window name, if any\n    refname: typing.Optional[str] = None\n    # PARTITION BY expr list\n    partition_clause: typing.Optional[list[BaseExpr]] = None\n    # ORDER BY\n    order_clause: typing.Optional[list[SortBy]] = None\n    # Window frame options\n    frame_options: typing.Optional[list] = None\n    # expression for starting bound, if any\n    start_offset: typing.Optional[BaseExpr] = None\n    # expression for ending ound, if any\n    end_offset: typing.Optional[BaseExpr] = None\n\n\nclass RangeSubselect(PathRangeVar):\n    \"\"\"Subquery appearing in FROM clauses.\"\"\"\n\n    # Before postgres 16, an alias is always required on selects from\n    # a subquery. Try to catch that with the typechecker by getting\n    # rid of the default value.\n    alias: Alias\n\n    lateral: bool = False\n    subquery: Query\n\n    @property\n    def query(self) -> Query:\n        return self.subquery\n\n\nclass RangeFunction(BaseRangeVar):\n\n    lateral: bool = False\n    # WITH ORDINALITY\n    with_ordinality: bool = False\n    # ROWS FROM form\n    is_rowsfrom: bool = False\n    functions: list[BaseExpr]\n\n\nclass JoinClause(BaseRangeVar):\n    # Type of join\n    type: str\n    # Right subtree\n    rarg: BaseRangeVar\n    # USING clause, if any\n    using_clause: typing.Optional[list[ColumnRef]] = None\n    # Qualifiers on join, if any\n    quals: typing.Optional[BaseExpr] = None\n\n\nclass JoinExpr(BaseRangeVar):\n    # Left subtree\n    larg: BaseRangeVar\n    # Join clauses\n    # We represent joins as being N-ary to avoid recursing too deeply\n    joins: list[JoinClause]\n\n    @classmethod\n    def make_inplace(\n        cls,\n        *,\n        larg: BaseRangeVar,\n        type: str,\n        rarg: BaseRangeVar,\n        using_clause: typing.Optional[list[ColumnRef]] = None,\n        quals: typing.Optional[BaseExpr] = None,\n    ) -> JoinExpr:\n        clause = JoinClause(\n            type=type, rarg=rarg, using_clause=using_clause, quals=quals\n        )\n        if isinstance(larg, JoinExpr):\n            larg.joins.append(clause)\n            return larg\n        else:\n            return JoinExpr(larg=larg, joins=[clause])\n\n\nclass SubLink(ImmutableBaseExpr):\n    \"\"\"Subselect appearing in an expression.\"\"\"\n\n    # Sublink expression\n    test_expr: typing.Optional[BaseExpr] = None\n    # EXISTS, NOT_EXISTS, ALL, ANY\n    operator: typing.Optional[str]\n    # Sublink expression\n    expr: BaseExpr\n    # Sublink is never NULL\n    nullable: bool = False\n\n\nclass RowExpr(ImmutableBaseExpr):\n    \"\"\"A ROW() expression.\"\"\"\n\n    # The fields.\n    args: list[BaseExpr]\n    # Row expressions, while may contain NULLs, are not NULL themselves.\n    nullable: bool = False\n\n\nclass ImplicitRowExpr(ImmutableBaseExpr):\n    \"\"\"A (a, b, c) expression.\"\"\"\n\n    # The fields.\n    args: typing.Sequence[BaseExpr]\n    # Row expressions, while may contain NULLs, are not NULL themselves.\n    nullable: bool = False\n\n\nclass CoalesceExpr(ImmutableBaseExpr):\n    \"\"\"A COALESCE() expression.\"\"\"\n\n    # The arguments.\n    args: list[Base]\n\n    def _infer_nullability(self, kwargs: dict[str, typing.Any]) -> bool:\n        # nullability of COALESCE is the nullability of the RHS\n        if 'args' in kwargs:\n            return kwargs['args'][1].nullable\n        else:\n            return True\n\n\nclass NullTest(ImmutableBaseExpr):\n    \"\"\"IS [NOT] NULL.\"\"\"\n\n    # Input expression,\n    arg: BaseExpr\n    # NOT NULL?\n    negated: bool = False\n    # NullTest is never NULL\n    nullable: bool = False\n\n\nclass BooleanTest(ImmutableBaseExpr):\n    \"\"\"IS [NOT] {TRUE,FALSE}\"\"\"\n\n    # Input expression,\n    arg: BaseExpr\n    negated: bool = False\n    is_true: bool = False\n    # NullTest is never NULL\n    nullable: bool = False\n\n\nclass CaseWhen(ImmutableBase):\n\n    # Condition expression\n    expr: BaseExpr\n    # subsitution result\n    result: BaseExpr\n\n\nclass CaseExpr(ImmutableBaseExpr):\n\n    # Equality comparison argument\n    arg: typing.Optional[BaseExpr] = None\n    # List of WHEN clauses\n    args: list[CaseWhen]\n    # ELSE clause\n    defresult: typing.Optional[BaseExpr] = None\n\n\nclass GroupingOperation(Base):\n    operation: typing.Optional[str] = None\n    args: list[Base]\n\n\nSortAsc = qlast.SortAsc\nSortDesc = qlast.SortDesc\nSortDefault = qlast.SortDefault\n\nNullsFirst = qlast.NonesFirst\nNullsLast = qlast.NonesLast\n\n\nclass AlterSystem(ImmutableBaseExpr):\n\n    name: str\n    value: typing.Optional[BaseExpr]\n\n\nclass Set(ImmutableBaseExpr):\n\n    name: str\n    value: BaseExpr\n\n\nclass ConfigureDatabase(ImmutableBase):\n\n    database_name: str\n    parameter_name: str\n    value: BaseExpr\n\n\nclass IteratorCTE(ImmutableBase):\n    path_id: irast.PathId\n    cte: CommonTableExpr\n    parent: typing.Optional[IteratorCTE]\n\n    # A list of other paths to *also* register the iterator rvar as\n    # providing when it is merged into a statement.\n    other_paths: tuple[tuple[irast.PathId, PathAspect], ...] = ()\n    iterator_bond: bool = False\n\n    @property\n    def aspect(self) -> PathAspect:\n        from .compiler import enums as pgce\n        return (\n            pgce.PathAspect.ITERATOR\n            if self.iterator_bond else\n            pgce.PathAspect.IDENTITY\n        )\n\n\nclass Statement(Base):\n    \"\"\"A statement that does not return a relation\"\"\"\n    pass\n\n\nclass VariableSetStmt(Statement):\n    name: str\n    args: ArgsList\n    scope: OptionsScope\n\n\nclass ArgsList(Base):\n    args: list[BaseExpr]\n\n\nclass VariableResetStmt(Statement):\n    name: typing.Optional[str]\n    scope: OptionsScope\n\n\nclass SetTransactionStmt(Statement):\n    \"\"\"A special case of VariableSetStmt\"\"\"\n\n    options: TransactionOptions\n    scope: OptionsScope\n\n\nclass VariableShowStmt(Statement):\n    name: str\n\n\nclass TransactionStmt(Statement):\n    pass\n\n\nclass OptionsScope(enum.IntEnum):\n    TRANSACTION = enum.auto()\n    SESSION = enum.auto()\n\n\nclass BeginStmt(TransactionStmt):\n    options: typing.Optional[TransactionOptions]\n\n\nclass StartStmt(TransactionStmt):\n    options: typing.Optional[TransactionOptions]\n\n\nclass CommitStmt(TransactionStmt):\n    chain: typing.Optional[bool]\n\n\nclass RollbackStmt(TransactionStmt):\n    chain: typing.Optional[bool]\n\n\nclass SavepointStmt(TransactionStmt):\n    savepoint_name: str\n\n\nclass ReleaseStmt(TransactionStmt):\n    savepoint_name: str\n\n\nclass RollbackToStmt(TransactionStmt):\n    savepoint_name: str\n\n\nclass TwoPhaseTransactionStmt(TransactionStmt):\n    gid: str\n\n\nclass PrepareTransaction(TwoPhaseTransactionStmt):\n    pass\n\n\nclass CommitPreparedStmt(TwoPhaseTransactionStmt):\n    pass\n\n\nclass RollbackPreparedStmt(TwoPhaseTransactionStmt):\n    pass\n\n\nclass TransactionOptions(Base):\n    options: dict[str, BaseExpr]\n\n\nclass PrepareStmt(Statement):\n    name: str\n    argtypes: typing.Optional[list[Base]]\n    query: BaseRelation\n\n\nclass ExecuteStmt(Statement):\n    name: str\n    params: typing.Optional[list[Base]]\n\n\nclass DeallocateStmt(Statement):\n    name: str\n\n\nclass SQLValueFunctionOP(enum.IntEnum):\n    CURRENT_DATE = enum.auto()\n    CURRENT_TIME = enum.auto()\n    CURRENT_TIME_N = enum.auto()\n    CURRENT_TIMESTAMP = enum.auto()\n    CURRENT_TIMESTAMP_N = enum.auto()\n    LOCALTIME = enum.auto()\n    LOCALTIME_N = enum.auto()\n    LOCALTIMESTAMP = enum.auto()\n    LOCALTIMESTAMP_N = enum.auto()\n    CURRENT_ROLE = enum.auto()\n    CURRENT_USER = enum.auto()\n    USER = enum.auto()\n    SESSION_USER = enum.auto()\n    CURRENT_CATALOG = enum.auto()\n    CURRENT_SCHEMA = enum.auto()\n\n\nclass SQLValueFunction(BaseExpr):\n    op: SQLValueFunctionOP\n    arg: typing.Optional[BaseExpr] = None\n\n\nclass CreateStmt(Statement):\n    relation: Relation\n\n    table_elements: list[TableElement]\n\n    on_commit: typing.Optional[str]\n\n\nclass CreateTableAsStmt(Statement):\n    into: CreateStmt\n    query: Query\n\n    with_no_data: bool\n\n\nclass MinMaxExpr(BaseExpr):\n    # GREATEST / LEAST expression\n    # Very similar to FuncCall, except that the name is not escaped\n\n    op: str\n    args: list[BaseExpr]\n\n\nclass LockStmt(Statement):\n    relations: list[BaseRangeVar]\n    mode: str\n    no_wait: bool = False\n\n\nclass CopyFormat(enum.IntEnum):\n    TEXT = enum.auto()\n    CSV = enum.auto()\n    BINARY = enum.auto()\n\n\nclass CopyOptions(Base):\n    # Options for the copy command\n    format: typing.Optional[CopyFormat] = None\n    freeze: typing.Optional[bool] = None\n    delimiter: typing.Optional[str] = None\n    null: typing.Optional[str] = None\n    header: typing.Optional[bool] = None\n    quote: typing.Optional[str] = None\n    escape: typing.Optional[str] = None\n    force_quote: list[str] = []\n    force_not_null: list[str] = []\n    force_null: list[str] = []\n    encoding: typing.Optional[str] = None\n\n\nclass CopyStmt(Statement):\n    relation: typing.Optional[Relation]\n    colnames: typing.Optional[list[str]]\n    query: typing.Optional[Query]\n\n    is_from: bool = False\n    is_program: bool = False\n    filename: typing.Optional[str]\n\n    options: CopyOptions\n\n    where_clause: typing.Optional[BaseExpr] = None\n\n\nclass FTSDocument(BaseExpr):\n    \"\"\"\n    Text and information on how to search through it.\n\n    Constructed with `std::fts::with_options`.\n    \"\"\"\n\n    text: BaseExpr\n\n    language: BaseExpr\n    language_domain: set[str]\n\n    weight: typing.Optional[str]\n"
  },
  {
    "path": "edb/pgsql/codegen.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Any, Optional, Sequence\n\nimport abc\nimport collections\nimport dataclasses\n\nfrom edb import errors\n\nfrom edb.pgsql import common\nfrom edb.pgsql import ast as pgast\nfrom edb.common.ast import codegen\nfrom edb.common import exceptions\nfrom edb.common import markup\n\n\ndef generate(\n    node: pgast.Base,\n    *,\n    indent_with: str = ' ' * 4,\n    add_line_information: bool = False,\n    pretty: bool = True,\n    reordered: bool = False,\n    with_source_map: bool = False,\n) -> SQLSource:\n    # Main entrypoint\n\n    generator = SQLSourceGenerator(\n        opts=codegen.Options(\n            indent_with=indent_with,\n            add_line_information=add_line_information,\n            pretty=pretty,\n        ),\n        reordered=reordered,\n        with_source_map=with_source_map,\n    )\n\n    try:\n        generator.visit(node)\n    except RecursionError:\n        # Don't try to wrap and add context to a recursion error,\n        # since the context might easily be too deeply recursive to\n        # process further down the pipe.\n        raise\n    except GeneratorError as error:\n        ctx = GeneratorContext(node, generator.result)\n        exceptions.add_context(error, ctx)\n        raise\n    except Exception as error:\n        ctx = GeneratorContext(node, generator.result)\n        err = GeneratorError('error while generating SQL source')\n        exceptions.add_context(err, ctx)\n        raise err from error\n\n    if with_source_map:\n        assert generator.source_map\n\n    return SQLSource(\n        text=generator.finish(),\n        source_map=generator.source_map,\n        param_index=generator.param_index,\n    )\n\n\ndef generate_source(\n    node: pgast.Base,\n    *,\n    indent_with: str = ' ' * 4,\n    add_line_information: bool = False,\n    pretty: bool = False,\n    reordered: bool = False,\n) -> str:\n    # Simplified entrypoint\n\n    source = generate(\n        node,\n        indent_with=indent_with,\n        add_line_information=add_line_information,\n        pretty=pretty,\n        reordered=reordered,\n    )\n    return source.text\n\n\ndef generate_ctes_source(\n    ctes: list[pgast.CommonTableExpr],\n) -> str:\n    # Alternative simplified entrypoint generating 'WITH a AS (...)' only.\n\n    generator = SQLSourceGenerator(opts=codegen.Options())\n    generator.gen_ctes(ctes)\n\n    return generator.finish()\n\n\nclass SourceMap:\n    @abc.abstractmethod\n    def translate(self, pos: int) -> int:\n        ...\n\n\n@dataclasses.dataclass(kw_only=True)\nclass BaseSourceMap(SourceMap):\n    source_start: int\n    output_start: int\n    output_end: int | None = None\n    children: list[BaseSourceMap] = (\n        dataclasses.field(default_factory=list))\n\n    def translate(self, pos: int) -> int:\n        bu = None\n        for u in self.children:\n            if u.output_start >= pos:\n                break\n            bu = u\n        if bu and (bu.output_end is None or bu.output_end > pos):\n            return bu.translate(pos)\n        return self.source_start\n\n\n@dataclasses.dataclass\nclass ChainedSourceMap(SourceMap):\n    parts: list[SourceMap] = (\n        dataclasses.field(default_factory=list))\n\n    def translate(self, pos: int) -> int:\n        for part in self.parts:\n            pos = part.translate(pos)\n        return pos\n\n\n@dataclasses.dataclass(frozen=True)\nclass SQLSource:\n    text: str\n    param_index: dict[int, list[int]]\n    source_map: Optional[SourceMap] = None\n\n\nclass SQLSourceGenerator(codegen.SourceGenerator):\n    def __init__(\n        self,\n        opts: codegen.Options,\n        *,\n        with_source_map: bool = False,\n        reordered: bool = False,\n    ):\n        super().__init__(\n            indent_with=opts.indent_with,\n            add_line_information=opts.add_line_information,\n            pretty=opts.pretty,\n        )\n        self.is_toplevel = True\n        # params\n        self.with_source_map: bool = with_source_map\n        self.reordered = reordered\n\n        # state\n        self.param_index: collections.defaultdict[int, list[int]] = (\n            collections.defaultdict(list))\n        self.write_index: int = 0\n        self.source_map: Optional[BaseSourceMap] = None\n\n    def write(\n        self,\n        *x: str,\n        delimiter: Optional[str] = None,\n    ) -> None:\n        self.is_toplevel = False\n        start = len(self.result)\n        super().write(*x, delimiter=delimiter)\n        for new in range(start, len(self.result)):\n            self.write_index += len(self.result[new])\n\n    def visit(self, node):  # type: ignore\n        if self.with_source_map:\n            source_map = BaseSourceMap(\n                source_start=node.span.start if node.span else 0,\n                output_start=self.write_index,\n            )\n            old_top = self.source_map\n            self.source_map = source_map\n        super().visit(node)\n        if self.with_source_map:\n            assert self.source_map == source_map\n            self.source_map.output_end = self.write_index\n            if old_top:\n                old_top.children.append(self.source_map)\n                self.source_map = old_top\n\n    def generic_visit(self, node):  # type: ignore\n        raise GeneratorError(\n            'No method to generate code for %s' % node.__class__.__name__\n        )\n\n    def gen_ctes(self, ctes: list[pgast.CommonTableExpr]) -> None:\n        count = len(ctes)\n        for i, cte in enumerate(ctes):\n            self.new_lines = 1\n            if i == 0 and getattr(cte, 'recursive', None):\n                self.write('RECURSIVE ')\n            self.write(common.quote_ident(cte.name))\n\n            if cte.aliascolnames:\n                self.write('(')\n                for (index, col_name) in enumerate(cte.aliascolnames):\n                    self.write(common.qname(col_name, column=True))\n                    if index + 1 < len(cte.aliascolnames):\n                        self.write(',')\n                self.write(')')\n\n            self.write(' AS ')\n            if cte.materialized is not None:\n                if cte.materialized:\n                    self.write('MATERIALIZED ')\n                else:\n                    self.write('NOT MATERIALIZED ')\n            self.indentation += 1\n            self.new_lines = 1\n            self.write('(')\n            self.visit(cte.query)\n            self.write(')')\n            if i != count - 1:\n                self.write(',')\n            self.indentation -= 1\n\n        self.new_lines = 1\n\n    def visit__Ref(self, node):  # type: ignore\n        self.visit(node.node)\n\n    def visit_Relation(self, node: pgast.Relation) -> None:\n        assert node.name\n        if node.schemaname is None:\n            self.write(common.qname(node.name))\n        else:\n            self.write(common.qname(node.schemaname, node.name))\n\n    def visit_NullRelation(self, node: pgast.NullRelation) -> None:\n        self.write('(SELECT ')\n        if node.target_list:\n            self.visit_list(node.target_list)\n        if node.where_clause:\n            self.indentation += 1\n            self.new_lines = 1\n            self.write('WHERE')\n            self.new_lines = 1\n            self.indentation += 1\n            self.visit(node.where_clause)\n            self.indentation -= 2\n        self.write(')')\n\n    def visit_SelectStmt(self, node: pgast.SelectStmt) -> None:\n        parenthesize = not self.is_toplevel\n\n        if parenthesize:\n            if not self.reordered and self.result:\n                self.new_lines = 1\n            self.write('(')\n            if self.reordered:\n                self.new_lines = 1\n                if not node.op:\n                    self.indentation += 1\n\n        if node.ctes:\n            self.write('WITH ')\n            self.gen_ctes(node.ctes)\n\n        if node.values:\n            self.write('VALUES')\n            self.new_lines = 1\n            self.visit_list(node.values)\n            if parenthesize:\n                self.new_lines = 1\n                if self.reordered and not node.op:\n                    self.indentation -= 1\n                self.write(')')\n            return\n\n        # If reordered is True, we try to put the FROM clause *before* SELECT,\n        # like it *ought* to be. We do various hokey things to try to make\n        # that look good.\n        # Otherwise we emit real SQL.\n        def _select() -> None:\n            self.write('SELECT')\n            if node.distinct_clause:\n                self.write(' DISTINCT')\n                if len(node.distinct_clause) > 1 or not isinstance(\n                    node.distinct_clause[0], pgast.Star\n                ):\n                    self.write(' ON (')\n                    self.visit_list(node.distinct_clause, newlines=False)\n                    self.write(')')\n            if self.pretty:\n                self.write('/*', repr(node), '*/')\n            self.new_lines = 1\n\n        if node.op:\n            # Upper level set operation node (UNION/INTERSECT)\n\n            # HACK: The LHS of a set operation is *not* top-level, and\n            # shouldn't be treated as such. Since we (also hackily)\n            # use whether anything has been written do determine\n            # whether we are at the top level, write out an empty\n            # string to force parenthesization.\n            self.is_toplevel = False\n            self.visit(node.larg)\n            self.write(' ' + node.op + ' ')\n            if node.all:\n                self.write('ALL ')\n            self.visit(node.rarg)\n        else:\n            if not self.reordered:\n                _select()\n                self.indentation += 2\n\n        if not self.reordered:\n            if node.target_list:\n                self.visit_list(node.target_list)\n\n            if not node.op:\n                self.indentation -= 2\n\n        if node.from_clause:\n            if not self.reordered:\n                self.indentation += 1\n                self.new_lines = 1\n            self.write('FROM ')\n            if not self.reordered:\n                self.new_lines = 1\n                self.indentation += 1\n            self.visit_list(node.from_clause)\n            if self.reordered:\n                self.new_lines = 1\n            else:\n                self.indentation -= 2\n\n        if self.reordered and not node.op:\n            _select()\n\n            self.indentation += 1\n\n            if node.target_list:\n                self.visit_list(node.target_list)\n\n            # In reordered mode, we don't want to indent the clauses,\n            # so we overreduce the indentation at this point and fix\n            # it up at the end\n            self.indentation -= 2\n\n        if node.where_clause:\n            self.indentation += 1\n            self.new_lines = 1\n            self.write('WHERE')\n            self.new_lines = 1\n            self.indentation += 1\n            self.visit(node.where_clause)\n            self.indentation -= 2\n\n        if node.group_clause:\n            self.indentation += 1\n            self.new_lines = 1\n            self.write('GROUP BY')\n            self.new_lines = 1\n            self.indentation += 1\n            self.visit_list(node.group_clause)\n            self.indentation -= 2\n\n        if node.having_clause:\n            self.indentation += 1\n            self.new_lines = 1\n            self.write('HAVING')\n            self.new_lines = 1\n            self.indentation += 1\n            self.visit(node.having_clause)\n            self.indentation -= 2\n\n        if node.sort_clause:\n            self.indentation += 1\n            self.new_lines = 1\n            self.write('ORDER BY')\n            self.new_lines = 1\n            self.indentation += 1\n            self.visit_list(node.sort_clause)\n            self.indentation -= 2\n\n        if node.limit_offset:\n            self.indentation += 1\n            self.new_lines = 1\n            self.write('OFFSET ')\n            self.visit(node.limit_offset)\n            self.indentation -= 1\n\n        if node.limit_count:\n            self.indentation += 1\n            self.new_lines = 1\n            self.write('LIMIT ')\n            self.visit(node.limit_count)\n            self.indentation -= 1\n\n        if node.locking_clause:\n            self.indentation += 1\n            self.new_lines = 1\n            self.visit_list(node.locking_clause, separator=\" \")\n            self.indentation -= 1\n\n        if self.reordered and not node.op:\n            self.indentation += 1\n\n        if parenthesize:\n            self.new_lines = 1\n            if self.reordered and not node.op:\n                self.indentation -= 1\n            self.write(')')\n\n    def visit_InsertStmt(self, node: pgast.InsertStmt) -> None:\n        if node.ctes:\n            self.write('WITH ')\n            self.gen_ctes(node.ctes)\n\n        self.write('INSERT INTO ')\n        self.visit(node.relation)\n        if node.cols:\n            self.new_lines = 1\n            self.indentation += 1\n            self.write('(')\n            self.visit_list(node.cols, newlines=False)\n            self.write(')')\n            self.indentation -= 1\n\n        self.indentation += 1\n        self.new_lines = 1\n\n        if node.select_stmt:\n            if (\n                isinstance(node.select_stmt, pgast.SelectStmt)\n                and node.select_stmt.values\n            ):\n                self.write('VALUES ')\n                self.new_lines = 1\n                self.indentation += 1\n                self.visit_list(node.select_stmt.values)\n                self.indentation -= 1\n            else:\n                self.write('(')\n                self.visit(node.select_stmt)\n                self.write(')')\n        else:\n            self.write('DEFAULT VALUES')\n\n        if node.on_conflict:\n            self.new_lines = 1\n            self.write('ON CONFLICT')\n\n            if node.on_conflict.target:\n                self.visit(node.on_conflict.target)\n\n            self.write(' DO ')\n            self.write(node.on_conflict.action[3:])\n\n            if node.on_conflict.update_list:\n                self.write(' SET')\n                self.new_lines = 1\n                self.indentation += 1\n                self.visit_list(node.on_conflict.update_list)\n                self.indentation -= 1\n\n            if node.on_conflict.update_where:\n                self.write(' WHERE ')\n                self.indentation += 1\n                self.visit(node.on_conflict.update_where)\n                self.indentation -= 1\n\n        if node.returning_list:\n            self.new_lines = 1\n            self.write('RETURNING')\n            self.new_lines = 1\n            self.indentation += 1\n            self.visit_list(node.returning_list)\n            self.indentation -= 1\n\n        self.indentation -= 1\n\n    def visit_UpdateStmt(self, node: pgast.UpdateStmt) -> None:\n        if node.ctes:\n            self.write('WITH ')\n            self.gen_ctes(node.ctes)\n\n        self.write('UPDATE ')\n        self.new_lines = 1\n        self.indentation += 1\n        self.visit(node.relation)\n        self.indentation -= 1\n        self.new_lines = 1\n        self.write('SET')\n\n        self.new_lines = 1\n        self.indentation += 1\n        self.visit_list(node.targets)\n        self.indentation -= 1\n\n        if node.from_clause:\n            self.new_lines = 1\n            self.write('FROM')\n            self.new_lines = 1\n            self.indentation += 1\n            self.visit_list(node.from_clause)\n            self.indentation -= 1\n\n        if node.where_clause:\n            self.new_lines = 1\n            self.write('WHERE')\n            self.new_lines = 1\n            self.indentation += 1\n            self.visit(node.where_clause)\n            self.new_lines = 1\n            self.indentation -= 1\n\n        if node.returning_list:\n            self.new_lines = 1\n            self.write('RETURNING')\n            self.new_lines = 1\n            self.indentation += 1\n            self.visit_list(node.returning_list)\n            self.indentation -= 1\n\n    def visit_DeleteStmt(self, node: pgast.DeleteStmt) -> None:\n        if node.ctes:\n            self.write('WITH ')\n            self.gen_ctes(node.ctes)\n\n        self.write('DELETE FROM ')\n        self.new_lines = 1\n        self.indentation += 1\n        self.visit(node.relation)\n        self.indentation -= 1\n\n        if node.using_clause:\n            self.new_lines = 1\n            self.write('USING')\n            self.new_lines = 1\n            self.indentation += 1\n            self.visit_list(node.using_clause)\n            self.indentation -= 1\n\n        if node.where_clause:\n            self.new_lines = 1\n            self.write('WHERE')\n            self.new_lines = 1\n            self.indentation += 1\n            self.visit(node.where_clause)\n            self.new_lines = 1\n            self.indentation -= 1\n\n        if node.returning_list:\n            self.new_lines = 1\n            self.write('RETURNING')\n            self.new_lines = 1\n            self.indentation += 1\n            self.visit_list(node.returning_list)\n            self.indentation -= 1\n\n    def visit_OnConflictTarget(self, node: pgast.OnConflictTarget) -> None:\n        assert not node.constraint_name or not node.index_elems\n        if node.constraint_name:\n            self.write(' ON CONSTRAINT ')\n            self.write(node.constraint_name)\n        if node.index_elems:\n            self.write(' (')\n            self.visit_list(node.index_elems)\n            self.write(')')\n            if node.index_where:\n                self.write(' WHERE ')\n                self.visit(node.index_where)\n\n    def visit_IndexElem(self, node: pgast.IndexElem) -> None:\n        self.visit(node.expr)\n\n        if node.ordering == pgast.SortAsc:\n            self.write(' ASC')\n        elif node.ordering == pgast.SortDesc:\n            self.write(' DESC')\n\n        if node.nulls_ordering == pgast.NullsFirst:\n            self.write(' NULLS FIRST')\n        elif node.nulls_ordering == pgast.NullsLast:\n            self.write(' NULLS LAST')\n\n    def visit_MultiAssignRef(self, node: pgast.MultiAssignRef) -> None:\n        self.write('(')\n        for index, col in enumerate(node.columns):\n            if index > 0:\n                self.write(', ')\n            self.write(common.quote_col(col))\n        self.write(') = ')\n        self.visit(node.source)\n\n    def visit_LiteralExpr(self, node: pgast.LiteralExpr) -> None:\n        self.write(node.expr)\n\n    def visit_ResTarget(self, node: pgast.ResTarget) -> None:\n        self.visit(node.val)\n        if node.name:\n            self.write(' AS ' + common.quote_col(node.name))\n\n    def visit_InsertTarget(self, node: pgast.InsertTarget) -> None:\n        self.write(common.quote_col(node.name))\n\n    def visit_UpdateTarget(self, node: pgast.UpdateTarget) -> None:\n        if isinstance(node.name, list):\n            self.write('(')\n            self.write(', '.join(common.quote_col(n) for n in node.name))\n            self.write(')')\n        else:\n            self.write(common.quote_col(node.name))\n        if node.indirection:\n            self._visit_indirection_ops(node.indirection)\n        self.write(' = ')\n        self.visit(node.val)\n\n    def visit_Alias(self, node: pgast.Alias) -> None:\n        self.write(common.quote_ident(node.aliasname))\n        if node.colnames:\n            self.write('(')\n            self.write(', '.join(common.quote_col(n) for n in node.colnames))\n            self.write(')')\n\n    def visit_Keyword(self, node: pgast.Keyword) -> None:\n        self.write(node.name)\n\n    def visit_RelRangeVar(self, node: pgast.RelRangeVar) -> None:\n        rel = node.relation\n\n        if not node.include_inherited:\n            self.write(' ONLY (')\n\n        if isinstance(rel, (pgast.Relation, pgast.NullRelation)):\n            self.visit(rel)\n        elif isinstance(rel, pgast.CommonTableExpr):\n            self.write(common.quote_ident(rel.name))\n        else:\n            raise GeneratorError(\n                'unexpected relation in RelRangeVar: {!r}'.format(rel)\n            )\n\n        if not node.include_inherited:\n            self.write(')')\n\n        if node.alias and node.alias.aliasname:\n            self.write(' AS ')\n            self.visit(node.alias)\n\n    def visit_RangeSubselect(self, node: pgast.RangeSubselect) -> None:\n        if node.lateral:\n            self.write('LATERAL ')\n\n        self.visit(node.subquery)\n\n        if node.alias and node.alias.aliasname:\n            self.write(' AS ')\n            self.visit(node.alias)\n\n    def visit_RangeFunction(self, node: pgast.RangeFunction) -> None:\n        if node.lateral:\n            self.write('LATERAL ')\n\n        if node.is_rowsfrom:\n            self.write('ROWS FROM (')\n\n        self.visit_list(node.functions)\n\n        if node.is_rowsfrom:\n            self.write(')')\n\n        if node.with_ordinality:\n            self.write(' WITH ORDINALITY ')\n\n        if node.alias and node.alias.aliasname:\n            self.write(' AS ')\n            self.visit(node.alias)\n\n    def visit_ColumnRef(self, node: pgast.ColumnRef) -> None:\n        names = node.name\n        if isinstance(names[-1], pgast.Star):\n            self.write(common.qname(*names))\n        else:\n            if names == ['VALUE']:\n                self.write('VALUE')\n            elif names[0] in {'OLD', 'NEW'}:\n                assert isinstance(names[0], str)\n                self.write(names[0])\n                if len(names) > 1:\n                    self.write('.')\n                    self.write(common.qname(*names[1:], column=True))\n            else:\n                self.write(common.qname(*names, column=True))\n\n    def visit_ExprOutputVar(self, node: pgast.ExprOutputVar) -> None:\n        self.visit(node.expr)\n\n    def visit_ColumnDef(self, node: pgast.ColumnDef) -> None:\n        self.write(common.quote_col(node.name))\n        if node.typename:\n            self.write(' ')\n            self.visit(node.typename)\n\n        if node.is_not_null:\n            self.write(' NOT NULL')\n        if node.default_expr:\n            self.write(' DEFAULT ')\n            self.visit(node.default_expr)\n\n    def visit_GroupingOperation(self, node: pgast.GroupingOperation) -> None:\n        if node.operation:\n            self.write(node.operation)\n            self.write(' ')\n        self.write('(')\n        self.visit_list(node.args, newlines=False)\n        self.write(')')\n\n    def visit_JoinExpr(self, node: pgast.JoinExpr) -> None:\n        self.visit(node.larg)\n        for join in node.joins:\n            self.new_lines = 1\n            if not join.quals and not join.using_clause:\n                join_type = 'CROSS'\n            else:\n                join_type = join.type.upper()\n            if join_type == 'INNER':\n                self.write('JOIN ')\n            else:\n                self.write(join_type + ' JOIN ')\n            nested_join = (\n                isinstance(join.rarg, pgast.JoinExpr)\n                and join.rarg.joins\n            )\n            if nested_join:\n                self.write('(')\n                self.new_lines = 1\n                self.indentation += 1\n            self.visit(join.rarg)\n            if nested_join:\n                self.indentation -= 1\n                self.new_lines = 1\n                self.write(')')\n            if join.quals is not None:\n                if not nested_join:\n                    self.indentation += 1\n                    self.new_lines = 1\n                    self.write('ON ')\n                else:\n                    self.write(' ON ')\n                self.visit(join.quals)\n                if not nested_join:\n                    self.indentation -= 1\n            elif join.using_clause:\n                self.write(\" USING (\")\n                self.visit_list(join.using_clause)\n                self.write(\")\")\n\n    def visit_Expr(self, node: pgast.Expr) -> None:\n        self.write('(')\n        if node.lexpr is not None:\n            self.visit(node.lexpr)\n            self.write(' ')\n        op = str(node.name)\n        if '.' not in op:\n            op = op.upper()\n        self.write(op)\n        if node.rexpr is not None:\n            self.write(\" \")\n            self.visit_indented(node.rexpr, indent=op in {\"OR\", \"AND\"})\n        self.write(\")\")\n\n    def visit_NullConstant(self, _node: pgast.NullConstant) -> None:\n        self.write('NULL')\n\n    def visit_NumericConstant(self, node: pgast.NumericConstant) -> None:\n        self.write(node.val)\n\n    def visit_BooleanConstant(self, node: pgast.BooleanConstant) -> None:\n        self.write('TRUE' if node.val else 'FALSE')\n\n    def visit_StringConstant(self, node: pgast.StringConstant) -> None:\n        self.write(common.quote_literal(node.val))\n\n    def visit_BitStringConstant(self, node: pgast.BitStringConstant) -> None:\n        self.write(f\"{node.kind}'{node.val}'\")\n\n    def visit_ByteaConstant(self, node: pgast.ByteaConstant) -> None:\n        self.write(common.quote_bytea_literal(node.val))\n\n    def visit_ParamRef(self, node: pgast.ParamRef) -> None:\n        self.write(f'${node.number}')\n        self.param_index[node.number].append(len(self.result) - 1)\n\n    def visit_RowExpr(self, node: pgast.RowExpr) -> None:\n        self.write('ROW(')\n        self.visit_list(node.args, newlines=False)\n        self.write(')')\n\n    def visit_ImplicitRowExpr(self, node: pgast.ImplicitRowExpr) -> None:\n        self.write('(')\n        self.visit_list(node.args, newlines=False)\n        self.write(')')\n\n    def visit_ArrayExpr(self, node: pgast.ArrayExpr) -> None:\n        self.write('ARRAY[')\n        self.visit_list(node.elements, newlines=False)\n        self.write(']')\n\n    def visit_ArrayDimension(self, node: pgast.ArrayDimension) -> None:\n        self.write('[')\n        self.visit_list(node.elements, newlines=False)\n        self.write(']')\n\n    def visit_VariadicArgument(self, node: pgast.VariadicArgument) -> None:\n        self.write('VARIADIC ')\n        self.visit(node.expr)\n\n    def visit_FuncCall(self, node: pgast.FuncCall) -> None:\n        self.write(common.qname(*node.name))\n\n        self.write('(')\n        if node.agg_star:\n            self.write(\"*\")\n        elif node.agg_distinct:\n            self.write('DISTINCT ')\n        self.visit_list(node.args, newlines=False)\n\n        if node.agg_order and not node.agg_within_group:\n            self.write(' ORDER BY ')\n            self.visit_list(node.agg_order, newlines=False)\n\n        self.write(')')\n\n        if node.agg_order and node.agg_within_group:\n            self.write(' WITHIN GROUP (ORDER BY ')\n            self.visit_list(node.agg_order, newlines=False)\n            self.write(')')\n\n        if node.agg_filter:\n            self.write(' FILTER (WHERE ')\n            self.visit(node.agg_filter)\n            self.write(')')\n\n        if node.over:\n            self.write(' OVER (')\n            if node.over.partition_clause:\n                self.write('PARTITION BY ')\n                self.visit_list(node.over.partition_clause, newlines=False)\n\n            if node.over.order_clause:\n                self.write(' ORDER BY ')\n                self.visit_list(node.over.order_clause, newlines=False)\n\n            # XXX: add support for frame definition\n\n            self.write(')')\n\n        if node.with_ordinality:\n            self.write(' WITH ORDINALITY')\n\n        if node.coldeflist:\n            self.write(' AS (')\n            self.visit_list(node.coldeflist, newlines=False)\n            self.write(')')\n\n    def visit_NamedFuncArg(self, node: pgast.NamedFuncArg) -> None:\n        self.write(common.quote_ident(node.name), ' => ')\n        self.visit(node.val)\n\n    def visit_SubLink(self, node: pgast.SubLink) -> None:\n        if node.test_expr:\n            self.visit(node.test_expr)\n\n        if node.operator:\n            self.write(\" \" + node.operator + \" \")\n        self.visit_indented(node.expr, indent=True, nest=True)\n\n    def visit_SortBy(self, node: pgast.SortBy) -> None:\n        self.visit(node.node)\n        if node.dir:\n            direction = 'ASC' if node.dir == pgast.SortAsc else 'DESC'\n            self.write(' ' + direction)\n\n            if node.nulls is None:\n                if node.dir == pgast.SortDesc:\n                    self.write(' NULLS LAST')\n                else:\n                    self.write(' NULLS FIRST')\n            elif node.nulls == pgast.NullsFirst:\n                self.write(' NULLS FIRST')\n            elif node.nulls == pgast.NullsLast:\n                self.write(' NULLS LAST')\n            else:\n                raise GeneratorError(\n                    'unexpected NULLS order: {}'.format(node.nulls)\n                )\n\n    def visit_LockingClause(self, node: pgast.LockingClause) -> None:\n        self.write(\"FOR \", str(node.strength))\n        if node.locked_rels:\n            self.write(\" OF \")\n            self.visit_list(node.locked_rels)\n        if node.wait_policy is not None:\n            if kw := str(node.wait_policy):\n                self.write(f\" {kw}\")\n\n    def visit_TypeCast(self, node: pgast.TypeCast) -> None:\n        # '::' has very high precedence, so parenthesize the expression.\n        self.write('(')\n        self.visit(node.arg)\n        self.write(')')\n        self.write('::')\n        self.visit(node.type_name)\n\n    def visit_TypeName(self, node: pgast.TypeName) -> None:\n        self.write(common.quote_type(node.name))\n        if node.array_bounds:\n            for array_bound in node.array_bounds:\n                self.write('[')\n                if array_bound >= 0:\n                    self.write(str(array_bound))\n                self.write(']')\n\n    def visit_Star(self, _: pgast.Star) -> None:\n        self.write('*')\n\n    def visit_CaseExpr(self, node: pgast.CaseExpr) -> None:\n        self.write('(CASE ')\n        if node.arg:\n            self.visit(node.arg)\n            self.write(' ')\n        for arg in node.args:\n            self.visit(arg)\n            self.new_lines = 1\n        if node.defresult:\n            self.write('ELSE ')\n            self.visit(node.defresult)\n            self.new_lines = 1\n        self.write('END)')\n\n    def visit_CaseWhen(self, node: pgast.CaseWhen) -> None:\n        self.write('WHEN ')\n        self.visit(node.expr)\n        self.write(' THEN ')\n        self.visit(node.result)\n\n    def visit_NullTest(self, node: pgast.NullTest) -> None:\n        self.write('(')\n        self.visit(node.arg)\n        if node.negated:\n            self.write(' IS NOT NULL')\n        else:\n            self.write(' IS NULL')\n        self.write(')')\n\n    def visit_BooleanTest(self, node: pgast.BooleanTest) -> None:\n        self.write(\"(\")\n        self.visit(node.arg)\n        op = \" IS\"\n        if node.negated:\n            op += \" NOT\"\n        if node.is_true:\n            op += \" TRUE\"\n        else:\n            op += \" FALSE\"\n        self.write(op)\n        self.write(\")\")\n\n    def visit_Indirection(self, node: pgast.Indirection) -> None:\n        self.write('(')\n        self.visit(node.arg)\n        self.write(')')\n        self._visit_indirection_ops(node.indirection)\n\n    def visit_RecordIndirectionOp(\n        self, node: pgast.RecordIndirectionOp\n    ) -> None:\n        self.write('.')\n        self.write(common.qname(node.name))\n\n    def _visit_indirection_ops(\n        self, ops: Sequence[pgast.IndirectionOp]\n    ) -> None:\n        for op in ops:\n            if isinstance(op, pgast.Star):\n                self.write('.')\n            self.visit(op)\n\n    def visit_Index(self, node: pgast.Index) -> None:\n        self.write('[')\n        self.visit(node.idx)\n        self.write(']')\n\n    def visit_Slice(self, node: pgast.Slice) -> None:\n        self.write('[')\n        if node.lidx is not None:\n            self.visit(node.lidx)\n        self.write(':')\n        if node.ridx is not None:\n            self.visit(node.ridx)\n        self.write(']')\n\n    def visit_CollateClause(self, node: pgast.CollateClause) -> None:\n        self.visit(node.arg)\n        self.write(f' COLLATE {common.qname(*node.collname)}')\n\n    def visit_CoalesceExpr(self, node: pgast.CoalesceExpr) -> None:\n        self.write('COALESCE(')\n        self.visit_list(node.args, newlines=False)\n        self.write(')')\n\n    def visit_AlterSystem(self, node: pgast.AlterSystem) -> None:\n        self.write('ALTER SYSTEM ')\n        if node.value is not None:\n            self.write('SET ')\n            self.write(common.quote_ident(node.name))\n            self.write(' = ')\n            self.visit(node.value)\n        else:\n            self.write('RESET ')\n            self.write(common.quote_ident(node.name))\n\n    def visit_Set(self, node: pgast.Set) -> None:\n        if node.value is not None:\n            self.write('SET ')\n            self.write(common.quote_ident(node.name))\n            self.write(' = ')\n            self.visit(node.value)\n        else:\n            self.write('RESET ')\n            self.write(common.quote_ident(node.name))\n\n    def visit_VariableSetStmt(self, node: pgast.VariableSetStmt) -> None:\n        self.write(\"SET \")\n        if node.scope == pgast.OptionsScope.TRANSACTION:\n            self.write(\"LOCAL \")\n        self.write(common.qname(node.name))\n        self.write(\" TO \")\n        self.visit(node.args)\n\n    def visit_ArgsList(self, node: pgast.ArgsList) -> None:\n        self.visit_list(node.args)\n\n    def visit_VariableResetStmt(self, node: pgast.VariableResetStmt) -> None:\n        if node.name is None:\n            assert node.scope == pgast.OptionsScope.SESSION\n            self.write(\"RESET ALL\")\n        else:\n            self.write(\"SET \")\n            if node.scope == pgast.OptionsScope.TRANSACTION:\n                self.write(\"LOCAL \")\n            self.write(common.qname(node.name))\n            self.write(\" TO DEFAULT\")\n\n    def visit_SetTransactionStmt(self, node: pgast.SetTransactionStmt) -> None:\n        self.write(\"SET \")\n        if node.scope == pgast.OptionsScope.TRANSACTION:\n            self.write(\"TRANSACTION \")\n        else:\n            self.write(\"SESSION CHARACTERISTICS AS TRANSACTION \")\n        self.visit(node.options)\n\n    def visit_VariableShowStmt(self, node: pgast.VariableShowStmt) -> None:\n        self.write(\"SHOW \")\n        self.write(common.qname(node.name))\n\n    def visit_BeginStmt(self, node: pgast.BeginStmt) -> None:\n        self.write(\"BEGIN\")\n        if node.options:\n            self.visit(node.options)\n\n    def visit_StartStmt(self, node: pgast.StartStmt) -> None:\n        self.write(\"START TRANSACTION\")\n        if node.options:\n            self.visit(node.options)\n\n    def visit_CommitStmt(self, node: pgast.CommitStmt) -> None:\n        self.write(\"COMMIT\")\n        if node.chain:\n            self.write(\" AND CHAIN\")\n\n    def visit_RollbackStmt(self, node: pgast.RollbackStmt) -> None:\n        self.write(\"ROLLBACK\")\n        if node.chain:\n            self.write(\" AND CHAIN\")\n\n    def visit_SavepointStmt(self, node: pgast.SavepointStmt) -> None:\n        self.write(f\"SAVEPOINT {node.savepoint_name}\")\n\n    def visit_ReleaseStmt(self, node: pgast.ReleaseStmt) -> None:\n        self.write(f\"RELEASE {node.savepoint_name}\")\n\n    def visit_RollbackToStmt(self, node: pgast.RollbackToStmt) -> None:\n        self.write(f\"ROLLBACK TO SAVEPOINT {node.savepoint_name}\")\n\n    def visit_PrepareTransaction(self, node: pgast.PrepareTransaction) -> None:\n        self.write(f\"PREPARE TRANSACTION '{node.gid}'\")\n\n    def visit_CommitPreparedStmt(self, node: pgast.CommitPreparedStmt) -> None:\n        self.write(f\"COMMIT PREPARED '{node.gid}'\")\n\n    def visit_RollbackPreparedStmt(\n        self, node: pgast.RollbackPreparedStmt\n    ) -> None:\n        self.write(f\"ROLLBACK PREPARED '{node.gid}'\")\n\n    def visit_TransactionOptions(self, node: pgast.TransactionOptions) -> None:\n        for def_name, arg in node.options.items():\n            if def_name == \"transaction_isolation\":\n                self.write(\" ISOLATION LEVEL \")\n                if isinstance(arg, pgast.StringConstant):\n                    self.write(arg.val.upper())\n            elif def_name == \"transaction_read_only\":\n                if isinstance(arg, pgast.NumericConstant):\n                    if arg.val == \"1\":\n                        self.write(\" READ ONLY\")\n                    else:\n                        self.write(\" READ WRITE\")\n            elif def_name == \"transaction_deferrable\":\n                if isinstance(arg, pgast.NumericConstant):\n                    if arg.val != \"1\":\n                        self.write(\" NOT\")\n                    self.write(\" DEFERRABLE\")\n\n    def visit_PrepareStmt(self, node: pgast.PrepareStmt) -> None:\n        self.write(f\"PREPARE {common.quote_ident(node.name)}\")\n        if node.argtypes:\n            self.write(f\"(\")\n            self.visit_list(node.argtypes, newlines=False)\n            self.write(f\")\")\n        self.write(f\" AS \")\n        self.visit(node.query)\n\n    def visit_ExecuteStmt(self, node: pgast.ExecuteStmt) -> None:\n        self.write(f\"EXECUTE {common.quote_ident(node.name)}\")\n        if node.params:\n            self.write(f\"(\")\n            self.visit_list(node.params, newlines=False)\n            self.write(f\")\")\n\n    def visit_DeallocateStmt(self, node: pgast.DeallocateStmt) -> None:\n        self.write(f\"DEALLOCATE {common.quote_ident(node.name)}\")\n\n    def visit_SQLValueFunction(self, node: pgast.SQLValueFunction) -> None:\n        self.write(common.get_sql_value_function_op(node.op))\n        if node.arg:\n            self.write(\"(\")\n            self.visit(node.arg)\n            self.write(\")\")\n\n    def visit_CreateStmt(self, node: pgast.CreateStmt) -> None:\n        self.write('CREATE ')\n        if node.relation.is_temporary:\n            self.write('TEMPORARY ')\n        self.write('TABLE ')\n        self.visit_Relation(node.relation)\n\n        if node.table_elements:\n            self.write(' (')\n            self.visit_list(node.table_elements)\n            self.write(')')\n\n        if node.on_commit:\n            self.write(' ON COMMIT ')\n            self.write(node.on_commit)\n\n    def visit_CreateTableAsStmt(self, node: pgast.CreateTableAsStmt) -> None:\n        self.visit(node.into)\n        self.write(' AS ')\n        self.visit(node.query)\n\n        if node.with_no_data:\n            self.write(' WITH NO DATA')\n\n    def visit_MinMaxExpr(self, node: pgast.MinMaxExpr) -> None:\n        self.write(node.op)\n        self.write('(')\n        self.visit_list(node.args)\n        self.write(')')\n\n    def visit_LockStmt(self, node: pgast.LockStmt) -> None:\n        self.write('LOCK TABLE ')\n        self.visit_list(node.relations)\n        self.write(' IN ')\n        self.write(node.mode)\n        self.write(' MODE')\n        if node.no_wait:\n            self.write(' NOWAIT')\n\n    def visit_CopyStmt(self, node: pgast.CopyStmt) -> None:\n        self.write('COPY ')\n        if node.query:\n            self.write('(')\n            self.indentation += 1\n            self.new_lines = 1\n            self.visit(node.query)\n            self.indentation -= 1\n            self.write(')')\n        elif node.relation:\n            self.visit_Relation(node.relation)\n            if node.colnames:\n                self.write(' (')\n                self.write(\n                    ', '.join(common.quote_ident(n) for n in node.colnames))\n                self.write(')')\n\n        if node.is_from:\n            self.write(' FROM ')\n        else:\n            self.write(' TO ')\n\n        if node.is_program:\n            self.write('PROGRAM ')\n        if node.filename:\n            self.write(common.quote_literal(node.filename))\n        else:\n            if node.is_from:\n                self.write('STDIN')\n            else:\n                self.write('STDOUT')\n\n        self.visit_CopyOptions(node.options)\n\n        if node.where_clause:\n            self.indentation += 1\n            self.new_lines = 1\n            self.write('WHERE')\n            self.new_lines = 1\n            self.indentation += 1\n            self.visit(node.where_clause)\n            self.indentation -= 2\n\n    def visit_CopyOptions(self, node: pgast.CopyOptions) -> None:\n        ql = common.quote_literal\n        qi = common.quote_ident\n\n        opts = []\n\n        if node.format:\n            opts.append('FORMAT ' + node.format._name_)\n        if node.freeze is not None:\n            opts.append('FREEZE' + ('' if node.freeze else ' FALSE'))\n        if node.delimiter:\n            opts.append('DELIMITER ' + ql(node.delimiter))\n        if node.null:\n            opts.append('NULL ' + ql(node.null))\n        if node.header is not None:\n            opts.append('HEADER' + ('' if node.header else ' FALSE'))\n        if node.quote:\n            opts.append('QUOTE ' + ql(node.quote))\n        if node.escape:\n            opts.append('ESCAPE ' + ql(node.escape))\n        if node.force_quote:\n            opts.append(\n                'FORCE_QUOTE (' + ', '.join(map(qi, node.force_quote)) + ')'\n            )\n        if node.force_not_null:\n            opts.append(\n                'FORCE_NOT_NULL ('\n                + ', '.join(map(qi, node.force_not_null))\n                + ')'\n            )\n        if node.force_null:\n            opts.append(\n                'FORCE_NULL (' + ', '.join(map(qi, node.force_null)) + ')'\n            )\n        if node.encoding:\n            opts.append('ENCODING ' + ql(node.encoding))\n\n        if opts:\n            self.write(' (' + ', '.join(opts), ')')\n\n\nclass GeneratorContext(markup.MarkupExceptionContext):\n    title = 'SQL Source Generator Context'\n\n    def __init__(\n        self,\n        node: pgast.Base,\n        chunks_generated: Optional[Sequence[str]] = None,\n    ):\n        self.node = node\n        self.chunks_generated = chunks_generated\n\n    @classmethod\n    def as_markup(cls: Any, self: Any, *, ctx: Any):  # type: ignore\n        me = markup.elements\n\n        body = [\n            me.doc.Section(\n                title='SQL Tree',\n                body=[markup.serialize(self.node, ctx=ctx)],  # type: ignore\n            )\n        ]\n\n        if self.chunks_generated:\n            code = markup.serializer.serialize_code(\n                ''.join(self.chunks_generated), lexer='sql'\n            )\n            body.append(\n                me.doc.Section(\n                    title='SQL generated so far', body=[code]  # type: ignore\n                )\n            )\n\n        return me.lang.ExceptionContext(\n            title=self.title, body=body  # type: ignore\n        )\n\n\nclass GeneratorError(errors.InternalServerError):\n    def __init__(\n        self,\n        msg: str,\n        *,\n        node: Optional[pgast.Base] = None,\n        details: Optional[str] = None,\n        hint: Optional[str] = None,\n    ) -> None:\n        super().__init__(msg, details=details, hint=hint)\n        if node is not None:\n            ctx = GeneratorContext(node)\n            exceptions.add_context(self, ctx)\n"
  },
  {
    "path": "edb/pgsql/common.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport binascii\nimport functools\nimport hashlib\nimport base64\nimport re\nfrom typing import Literal, Optional, overload\nimport uuid\n\nfrom edb import buildmeta\nfrom edb.common import typeutils\nfrom edb.common import uuidgen\nfrom edb.schema import casts as s_casts\nfrom edb.schema import constraints as s_constr\nfrom edb.schema import defines as s_def\nfrom edb.schema import functions as s_func\nfrom edb.schema import indexes as s_indexes\nfrom edb.schema import name as s_name\nfrom edb.schema import objects as so\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import operators as s_opers\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import types as s_types\nfrom edb.schema import schema as s_schema\n\nfrom edb.pgsql import ast as pgast\n\nfrom . import keywords as pg_keywords\n\n\n# This is a postgres limitation.\n# Note that this can be overridden in custom builds.\n# https://www.postgresql.org/docs/current/datatype-enum.html\nMAX_ENUM_LABEL_LENGTH = 63\n\n\ndef quote_e_literal(string: str) -> str:\n    def escape_sq(s):\n        split = re.split(r\"(\\n|\\\\\\\\|\\\\')\", s)\n\n        if len(split) == 1:\n            return s.replace(r\"'\", r\"\\'\")\n\n        return ''.join((r if i % 2 else r.replace(r\"'\", r\"\\'\"))\n                       for i, r in enumerate(split))\n\n    return \"E'\" + escape_sq(string) + \"'\"\n\n\ndef quote_literal(string: str) -> str:\n    return \"'\" + string.replace(\"'\", \"''\") + \"'\"\n\n\ndef _quote_ident(string: str) -> str:\n    return '\"' + string.replace('\"', '\"\"') + '\"'\n\n\ndef quote_ident(ident: str | pgast.Star, *, force=False, column=False) -> str:\n    if isinstance(ident, pgast.Star):\n        return \"*\"\n    return (\n        _quote_ident(ident)\n        if needs_quoting(ident, column=column) or force else ident\n    )\n\n\ndef quote_col(ident: str | pgast.Star) -> str:\n    return quote_ident(ident, column=True)\n\n\ndef quote_bytea_literal(data: bytes) -> str:\n    \"\"\"Return valid SQL representation of a bytes value.\"\"\"\n\n    if data:\n        b = binascii.b2a_hex(data).decode('ascii')\n        return f\"'\\\\x{b}'::bytea\"\n    else:\n        return \"''::bytea\"\n\n\ndef needs_quoting(string: str, column: bool = False) -> bool:\n    isalnum = (\n        string\n        and not string[0].isdecimal()\n        and string.replace('_', 'a').isalnum()\n    )\n    return (\n        not isalnum or\n        string.lower() in pg_keywords.by_type[\n            pg_keywords.RESERVED_KEYWORD] or\n        string.lower() in pg_keywords.by_type[\n            pg_keywords.TYPE_FUNC_NAME_KEYWORD] or\n        (column and string.lower() in pg_keywords.by_type[\n            pg_keywords.COL_NAME_KEYWORD]) or\n        string.lower() != string\n    )\n\n\ndef qname(*parts: str | pgast.Star, column: bool = False) -> str:\n    assert len(parts) <= 3, parts\n    return '.'.join([quote_ident(q, column=column) for q in parts])\n\n\ndef quote_type(type_: tuple[str, ...] | str) -> str:\n    if isinstance(type_, tuple):\n        first = qname(*type_[:-1]) + '.' if len(type_) > 1 else ''\n        last = type_[-1]\n    else:\n        first = ''\n        last = type_\n\n    is_rowtype = last.endswith('%ROWTYPE')\n    if is_rowtype:\n        last = last[:-8]\n\n    is_array = last.endswith('[]')\n    if is_array:\n        last = last[:-2]\n\n    param = None\n    if '(' in last:\n        last, param = last.split('(', 1)\n        param = '(' + param\n\n    last = quote_ident(last)\n\n    if is_rowtype:\n        last += '%ROWTYPE'\n\n    if param:\n        last += param\n\n    if is_array:\n        last += '[]'\n\n    return first + last\n\n\ndef get_module_backend_name(module: s_name.Name) -> str:\n    # standard modules go into \"edgedbstd\", user ones into \"edgedbpub\"\n    return \"edgedbstd\" if module in s_schema.STD_MODULES else \"edgedbpub\"\n\n\ndef get_unique_random_name() -> str:\n    return base64.b64encode(uuidgen.uuid1mc().bytes).rstrip(b'=').decode()\n\n\nVERSIONED_SCHEMAS = ('edgedb', 'edgedbstd', 'edgedbsql', 'edgedbinstdata')\n\n\nSCHEMA_SUFFIX: str | None = None\n\n\ndef versioned_schema(s: str) -> str:\n    global SCHEMA_SUFFIX\n    if SCHEMA_SUFFIX is None:\n        version = buildmeta.get_version_dict()['major']\n        SCHEMA_SUFFIX = f'_v{version}_{buildmeta.EDGEDB_CATALOG_VERSION:x}'\n\n    # N.B: We don't bother quoting the schema name, so make sure it is\n    # lower case and doesn't have weird characters.\n    return f'{s}{SCHEMA_SUFFIX}'\n\n\ndef maybe_versioned_schema(\n    s: str,\n    versioned: bool=True,\n) -> str:\n    return (\n        versioned_schema(s)\n        if versioned and s in VERSIONED_SCHEMAS\n        else s\n    )\n\n\ndef versioned_name(\n    s: tuple[str, ...],\n) -> tuple[str, ...]:\n    if len(s) > 1:\n        return (maybe_versioned_schema(s[0]), *s[1:])\n    else:\n        return s\n\n\ndef maybe_versioned_name(\n    s: tuple[str, ...], *, versioned: bool,\n) -> tuple[str, ...]:\n    return versioned_name(s) if versioned else s\n\n\n@functools.lru_cache()\ndef _edgedb_name_to_pg_name(name: str, prefix_length: int = 0) -> str:\n    # Note: PostgreSQL doesn't have a sha1 implementation as a\n    # built-in function available in all versions, hence we use md5.\n    #\n    # Although sha1 would be slightly better as it's marginally faster than\n    # md5 (and it doesn't matter which function is better cryptographically\n    # in this case.)\n    hashed = base64.b64encode(\n        hashlib.md5(name.encode(), usedforsecurity=False).digest()\n    ).decode().rstrip('=')\n\n    return (\n        name[:prefix_length] +\n        hashed +\n        ':' +\n        name[-(s_def.MAX_NAME_LENGTH - prefix_length - 1 - len(hashed)):]\n    )\n\n\ndef edgedb_name_to_pg_name(name: str, prefix_length: int = 0) -> str:\n    \"\"\"Convert Gel name to a valid PostgresSQL column name.\n\n    PostgreSQL has a limit of 63 characters for column names.\n\n    @param name: Gel name to convert\n    @return: PostgreSQL column name\n    \"\"\"\n    if not (0 <= prefix_length < s_def.MAX_NAME_LENGTH):\n        raise ValueError('supplied name is too long '\n                         'to be kept in original form')\n\n    name = str(name)\n    if len(name) <= s_def.MAX_NAME_LENGTH - prefix_length:\n        return name\n\n    return _edgedb_name_to_pg_name(name, prefix_length)\n\n\ndef convert_name(\n    name: s_name.QualName, suffix='', catenate=True,\n    *,\n    versioned=True,\n):\n    schema = get_module_backend_name(name.get_module_name())\n    if suffix:\n        sname = f'{name.name}_{suffix}'\n    else:\n        sname = name.name\n\n    dbname = edgedb_name_to_pg_name(sname)\n\n    if versioned:\n        schema = maybe_versioned_schema(schema)\n\n    if catenate:\n        return qname(schema, dbname)\n    else:\n        return schema, dbname\n\n\ndef get_database_backend_name(db_name: str, *, tenant_id: str) -> str:\n    return f'{tenant_id}_{db_name}'\n\n\ndef get_role_backend_name(role_name: str, *, tenant_id: str) -> str:\n    return f'{tenant_id}_{role_name}'\n\n\ndef update_aspect(name, aspect):\n    \"\"\"Update the aspect on a non catenated name.\n\n    It also needs to be from an object that uses ids for names\"\"\"\n    suffix = get_aspect_suffix(aspect)\n    stripped = name[1].rsplit(\"_\", 1)[0]\n    if suffix:\n        return (name[0], f'{stripped}_{suffix}')\n    else:\n        return (name[0], stripped)\n\n\ndef get_scalar_backend_name(\n    id, module_name, catenate=True, *, versioned=True, aspect=None\n):\n    if aspect is None:\n        aspect = 'domain'\n    if aspect not in (\n        \"domain\",\n        \"sequence\",\n        \"enum\",\n        \"enum-cast-into-str\",\n        \"enum-cast-from-str\",\n        \"source-del-imm-otl-f\",\n        \"source-del-imm-otl-t\",\n    ):\n        raise ValueError(\n            f'unexpected aspect for scalar backend name: {aspect!r}')\n    name = s_name.QualName(module=module_name, name=str(id))\n\n    # XXX: TRAMPOLINE: VERSIONING???\n    if aspect.startswith(\"enum-cast-\"):\n        suffix = \"_into_str\" if aspect == \"enum-cast-into-str\" else \"_from_str\"\n        name = s_name.QualName(name.module, name.name + suffix)\n        return get_cast_backend_name(\n            name, catenate, versioned=versioned, aspect=\"function\")\n\n    return convert_name(name, aspect, catenate, versioned=False)\n\n\ndef get_aspect_suffix(aspect):\n    if aspect == 'table':\n        return ''\n    elif aspect == 'inhview':\n        return 't'\n    else:\n        return aspect\n\n\ndef is_inhview_name(name: str) -> bool:\n    return name.endswith('_t')\n\n\ndef get_objtype_backend_name(\n    id: uuid.UUID,\n    module_name: str,\n    *,\n    catenate: bool = True,\n    versioned: bool = False,\n    aspect: Optional[str] = None,\n):\n    if aspect is None:\n        aspect = 'table'\n    if (\n        aspect not in {'table', 'inhview', 'dummy'}\n        and not re.match(\n            r'(source|target)-del-(def|imm)-(inl|otl)-(f|t)', aspect)\n        and not aspect.startswith(\"ext\")\n    ):\n        raise ValueError(\n            f'unexpected aspect for object type backend name: {aspect!r}')\n\n    name = s_name.QualName(module=module_name, name=str(id))\n\n    suffix = get_aspect_suffix(aspect)\n    return convert_name(\n        name, suffix=suffix, catenate=catenate, versioned=versioned)\n\n\ndef get_pointer_backend_name(\n    id, module_name, *, catenate=False, aspect=None, versioned=True\n):\n    if aspect is None:\n        aspect = 'table'\n\n    if aspect not in ('table', 'index', 'inhview', 'dummy'):\n        raise ValueError(\n            f'unexpected aspect for pointer backend name: {aspect!r}')\n\n    name = s_name.QualName(module=module_name, name=str(id))\n\n    suffix = get_aspect_suffix(aspect)\n    return convert_name(\n        name, suffix=suffix, catenate=catenate, versioned=versioned\n    )\n\n\noperator_map = {\n    s_name.name_from_string('std::AND'): 'AND',\n    s_name.name_from_string('std::OR'): 'OR',\n    s_name.name_from_string('std::NOT'): 'NOT',\n    s_name.name_from_string('std::?='): 'IS NOT DISTINCT FROM',\n    s_name.name_from_string('std::?!='): 'IS DISTINCT FROM',\n    s_name.name_from_string('std::LIKE'): 'LIKE',\n    s_name.name_from_string('std::ILIKE'): 'ILIKE',\n    s_name.name_from_string('std::NOT LIKE'): 'NOT LIKE',\n    s_name.name_from_string('std::NOT ILIKE'): 'NOT ILIKE',\n}\n\n\ndef get_operator_backend_name(\n    name, catenate=False, *, versioned=True, aspect=None\n):\n    if aspect is None:\n        aspect = 'operator'\n\n    if aspect == 'function':\n        return convert_name(name, 'f', catenate=catenate, versioned=versioned)\n    elif aspect != 'operator':\n        raise ValueError(\n            f'unexpected aspect for operator backend name: {aspect!r}')\n\n    oper_name = operator_map.get(name)\n    if oper_name is None:\n        oper_name = name.name\n        if re.search(r'[a-zA-Z]', oper_name):\n            raise ValueError(\n                f'cannot represent operator {oper_name} in Postgres')\n\n        oper_name = f'`{oper_name}`'\n        schema = 'edgedb'\n    else:\n        schema = ''\n\n    if catenate:\n        return qname(schema, oper_name)\n    else:\n        return schema, oper_name\n\n\ndef get_cast_backend_name(\n    fullname: s_name.QualName, catenate=False, *, versioned=True, aspect=None\n):\n    if aspect == \"function\":\n        return convert_name(\n            fullname, \"f\", catenate=catenate, versioned=versioned)\n    else:\n        raise ValueError(\n            f'unexpected aspect for cast backend name: {aspect!r}')\n\n\ndef get_function_backend_name(\n    name, backend_name, catenate=False, versioned=True,\n):\n    real_name = backend_name or name.name\n\n    fullname = s_name.QualName(module=name.module, name=real_name)\n    schema, func_name = convert_name(\n        fullname, catenate=False, versioned=versioned)\n    if catenate:\n        return qname(schema, func_name)\n    else:\n        return schema, func_name\n\n\ndef get_constraint_backend_name(id, module_name, catenate=True, *, aspect=None):\n    if aspect not in ('trigproc', 'index'):\n        raise ValueError(\n            f'unexpected aspect for constraint backend name: {aspect!r}')\n\n    sname = str(id)\n    if aspect == 'index':\n        aspect = None\n        sname = get_constraint_raw_name(id)\n    name = s_name.QualName(module=module_name, name=sname)\n    return convert_name(name, aspect, catenate)\n\n\ndef get_constraint_raw_name(id):\n    return f'{id};schemaconstr'\n\n\ndef get_index_backend_name(id, module_name, catenate=True, *, aspect=None):\n    if aspect is None:\n        aspect = 'index'\n    name = s_name.QualName(module=module_name, name=str(id))\n    return convert_name(name, aspect, catenate)\n\n\ndef get_index_table_backend_name(\n    index: s_indexes.Index,\n    schema: s_schema.Schema,\n    *,\n    aspect: Optional[str] = None,\n) -> tuple[str, str]:\n    subject = index.get_subject(schema)\n    assert isinstance(subject, s_types.Type)\n    return get_backend_name(schema, subject, aspect=aspect, catenate=False)\n\n\ndef get_tuple_backend_name(\n    id, catenate=True, *, aspect=None\n) -> str | tuple[str, str]:\n\n    name = s_name.QualName(module='edgedb', name=f'{id}_t')\n    return convert_name(name, aspect, catenate)\n\n\n@overload\ndef get_backend_name(\n    schema: s_schema.Schema,\n    obj: so.Object,\n    catenate: Literal[True]=True,\n    *,\n    versioned: bool=True,\n    aspect: Optional[str]=None\n) -> str:\n    ...\n\n\n@overload\ndef get_backend_name(\n    schema: s_schema.Schema,\n    obj: so.Object,\n    catenate: Literal[False],\n    *,\n    versioned: bool=True,\n    aspect: Optional[str]=None\n) -> tuple[str, str]:\n    ...\n\n\ndef get_backend_name(\n    schema: s_schema.Schema,\n    obj: so.Object,\n    catenate: bool=True,\n    *,\n    aspect: Optional[str]=None,\n    versioned: bool=True,\n) -> str | tuple[str, str]:\n    name: s_name.QualName | s_name.Name\n    if isinstance(obj, s_objtypes.ObjectType):\n        name = obj.get_name(schema)\n        return get_objtype_backend_name(\n            obj.id, name.module, catenate=catenate,\n            aspect=aspect, versioned=versioned,\n        )\n\n    elif isinstance(obj, s_pointers.Pointer):\n        name = obj.get_name(schema)\n        return get_pointer_backend_name(obj.id, name.module, catenate=catenate,\n                                        versioned=versioned,\n                                        aspect=aspect)\n\n    elif isinstance(obj, s_scalars.ScalarType):\n        name = obj.get_name(schema)\n        return get_scalar_backend_name(obj.id, name.module, catenate=catenate,\n                                       versioned=versioned,\n                                       aspect=aspect)\n\n    elif isinstance(obj, s_opers.Operator):\n        name = obj.get_shortname(schema)\n        return get_operator_backend_name(\n            name, catenate, versioned=versioned, aspect=aspect)\n\n    elif isinstance(obj, s_casts.Cast):\n        name = obj.get_name(schema)\n        return get_cast_backend_name(\n            name, catenate, versioned=versioned, aspect=aspect)\n\n    elif isinstance(obj, s_func.Function):\n        name = obj.get_shortname(schema)\n        backend_name = obj.get_backend_name(schema)\n        return get_function_backend_name(\n            name, backend_name, catenate, versioned=versioned)\n\n    elif isinstance(obj, s_constr.Constraint):\n        name = obj.get_name(schema)\n        return get_constraint_backend_name(\n            obj.id, name.module, catenate, aspect=aspect)\n\n    elif isinstance(obj, s_indexes.Index):\n        name = obj.get_name(schema)\n        return get_index_backend_name(\n            obj.id, name.module, catenate, aspect=aspect)\n\n    elif isinstance(obj, s_types.Tuple):\n        # XXX: TRAMPOLINE: VERSIONED?\n        return get_tuple_backend_name(\n            obj.id, catenate, aspect=aspect)\n\n    else:\n        raise ValueError(f'cannot determine backend name for {obj!r}')\n\n\ndef get_object_from_backend_name(schema, metaclass, name, *, aspect=None):\n\n    if issubclass(metaclass, s_objtypes.ObjectType):\n        table_name = name[1]\n        obj_id = uuidgen.UUID(table_name)\n        return schema.get_by_id(obj_id)\n\n    elif issubclass(metaclass, s_pointers.Pointer):\n        obj_id = uuidgen.UUID(name)\n        return schema.get_by_id(obj_id)\n\n    else:\n        raise ValueError(\n            f'cannot determine object from backend name for {metaclass!r}')\n\n\ndef get_sql_value_function_op(op: pgast.SQLValueFunctionOP) -> str:\n    from edb.pgsql.ast import SQLValueFunctionOP as OP\n\n    NAMES = {\n        OP.CURRENT_DATE: \"current_date\",\n        OP.CURRENT_TIME: \"current_time\",\n        OP.CURRENT_TIME_N: \"current_time\",\n        OP.CURRENT_TIMESTAMP: \"current_timestamp\",\n        OP.CURRENT_TIMESTAMP_N: \"current_timestamp\",\n        OP.LOCALTIME: \"localtime\",\n        OP.LOCALTIME_N: \"localtime\",\n        OP.LOCALTIMESTAMP: \"localtimestamp\",\n        OP.LOCALTIMESTAMP_N: \"localtimestamp\",\n        OP.CURRENT_ROLE: \"current_role\",\n        OP.CURRENT_USER: \"current_user\",\n        OP.USER: \"user\",\n        OP.SESSION_USER: \"session_user\",\n        OP.CURRENT_CATALOG: \"current_catalog\",\n        OP.CURRENT_SCHEMA: \"current_schema\",\n    }\n    return NAMES[op]\n\n\n# Settings that are enums or bools and should not be quoted.\n# Can be retrieved from PostgreSQL with:\n#   SELECt name FROM pg_catalog.pg_settings WHERE vartype IN ('enum', 'bool');\nENUM_SETTINGS = {\n    'allow_alter_system',\n    'allow_in_place_tablespaces',\n    'allow_system_table_mods',\n    'archive_mode',\n    'array_nulls',\n    'autovacuum',\n    'backslash_quote',\n    'bytea_output',\n    'check_function_bodies',\n    'client_min_messages',\n    'compute_query_id',\n    'constraint_exclusion',\n    'data_checksums',\n    'data_sync_retry',\n    'debug_assertions',\n    'debug_logical_replication_streaming',\n    'debug_parallel_query',\n    'debug_pretty_print',\n    'debug_print_parse',\n    'debug_print_plan',\n    'debug_print_rewritten',\n    'default_toast_compression',\n    'default_transaction_deferrable',\n    'default_transaction_isolation',\n    'default_transaction_read_only',\n    'dynamic_shared_memory_type',\n    'edb_stat_statements.save',\n    'edb_stat_statements.track',\n    'edb_stat_statements.track_planning',\n    'edb_stat_statements.track_utility',\n    'enable_async_append',\n    'enable_bitmapscan',\n    'enable_gathermerge',\n    'enable_group_by_reordering',\n    'enable_hashagg',\n    'enable_hashjoin',\n    'enable_incremental_sort',\n    'enable_indexonlyscan',\n    'enable_indexscan',\n    'enable_material',\n    'enable_memoize',\n    'enable_mergejoin',\n    'enable_nestloop',\n    'enable_parallel_append',\n    'enable_parallel_hash',\n    'enable_partition_pruning',\n    'enable_partitionwise_aggregate',\n    'enable_partitionwise_join',\n    'enable_presorted_aggregate',\n    'enable_seqscan',\n    'enable_sort',\n    'enable_tidscan',\n    'escape_string_warning',\n    'event_triggers',\n    'exit_on_error',\n    'fsync',\n    'full_page_writes',\n    'geqo',\n    'gss_accept_delegation',\n    'hot_standby',\n    'hot_standby_feedback',\n    'huge_pages',\n    'huge_pages_status',\n    'icu_validation_level',\n    'ignore_checksum_failure',\n    'ignore_invalid_pages',\n    'ignore_system_indexes',\n    'in_hot_standby',\n    'integer_datetimes',\n    'intervalstyle',\n    'jit',\n    'jit_debugging_support',\n    'jit_dump_bitcode',\n    'jit_expressions',\n    'jit_profiling_support',\n    'jit_tuple_deforming',\n    'krb_caseins_users',\n    'lo_compat_privileges',\n    'log_checkpoints',\n    'log_connections',\n    'log_disconnections',\n    'log_duration',\n    'log_error_verbosity',\n    'log_executor_stats',\n    'log_hostname',\n    'log_lock_waits',\n    'log_min_error_statement',\n    'log_min_messages',\n    'log_parser_stats',\n    'log_planner_stats',\n    'log_recovery_conflict_waits',\n    'log_replication_commands',\n    'log_statement',\n    'log_statement_stats',\n    'log_truncate_on_rotation',\n    'logging_collector',\n    'parallel_leader_participation',\n    'password_encryption',\n    'plan_cache_mode',\n    'quote_all_identifiers',\n    'recovery_init_sync_method',\n    'recovery_prefetch',\n    'recovery_target_action',\n    'recovery_target_inclusive',\n    'remove_temp_files_after_crash',\n    'restart_after_crash',\n    'row_security',\n    'send_abort_for_crash',\n    'send_abort_for_kill',\n    'session_replication_role',\n    'shared_memory_type',\n    'ssl',\n    'ssl_max_protocol_version',\n    'ssl_min_protocol_version',\n    'ssl_passphrase_command_supports_reload',\n    'ssl_prefer_server_ciphers',\n    'standard_conforming_strings',\n    'stats_fetch_consistency',\n    'summarize_wal',\n    'sync_replication_slots',\n    'synchronize_seqscans',\n    'synchronous_commit',\n    'syslog_facility',\n    'syslog_sequence_numbers',\n    'syslog_split_messages',\n    'trace_connection_negotiation',\n    'trace_notify',\n    'trace_sort',\n    'track_activities',\n    'track_commit_timestamp',\n    'track_counts',\n    'track_functions',\n    'track_io_timing',\n    'track_wal_io_timing',\n    'transaction_deferrable',\n    'transaction_isolation',\n    'transaction_read_only',\n    'transform_null_equals',\n    'update_process_title',\n    'wal_compression',\n    'wal_init_zero',\n    'wal_level',\n    'wal_log_hints',\n    'wal_receiver_create_temp_slot',\n    'wal_recycle',\n    'wal_sync_method',\n    'xmlbinary',\n    'xmloption',\n    'zero_damaged_pages',\n\n    # additionally, there are some settings that also should not be quoted\n    'work_mem',\n}\n\n\ndef setting_to_sql(name, setting):\n    is_enum = name.lower() in ENUM_SETTINGS\n\n    assert typeutils.is_container(setting)\n    return ', '.join(setting_val_to_sql(v, is_enum) for v in setting)\n\n\ndef setting_val_to_sql(val: str | int | float, is_enum: bool):\n    if isinstance(val, str):\n        if is_enum:\n            # special case: no quoting\n            return val\n        # quote as identifier\n        return quote_ident(val)\n    if isinstance(val, int):\n        return str(val)\n    if isinstance(val, float):\n        return str(val)\n    raise NotImplementedError('cannot convert setting to SQL: ', val)\n"
  },
  {
    "path": "edb/pgsql/compiler/ARCHITECTURE.md",
    "content": "# Architecture of PG compiler\n\nRelVar = Relation variable. Basically an instance of a relation within a query.\n\nPathVar = Reference to a column, as seen from within the declaring query.\n\nOutputVar = Reference to a column, that can be used from outside of declaring\nquery.\n\n## Recursive column injection\n\nWhen as IR set is compiled, it may not be known which properties of that object\nwill be needed downstream. To avoid fetching, computing and possibly\nmaterializing too much data, sets are compiled in two steps:\n\n1. Compile general structure of the query. In this process every IR set\n   will be bound to some SQL select statement.\n\n2. Inject columns into this tree. This is mainly done in\n   `pathctx.get_path_var`, which:\n   - finds which RelVar provides source aspect for this path\n     (see `pathctx._find_rel_rvar`)\n   - determines what is the OutputVar of this path within the RelVar\n     (see `pathctx.get_path_output`). This recursively calls `get_path_var`.\n   - when an actual table is encountered, a plain ColRef to it's columns is\n     returned.\n\n## Overlays\n\nPostgres has a limitation where effects of any DML are not visible in the same\nquery.\n\nFor example:\n\n```\nWITH insert_result AS (INSERT INTO my_table(a) VALUES (1) RETURNING a)\nSELECT a FROM my_table, insert_result\n```\n\nIn this query, `my_table` will not contain the inserted value. Obvious\nsolution is use `insert_result` only and not rely on `my_table` anymore.\n\nThis is the gist of what overlays accomplish. They define a new relation that\nshould be used instead of the base table when the compiler wants to pull data\nfor some path_id.\n\nOverlay also allows specifying operation that needs to be applied when\nconstructing the rel var: union, exclude, replace.\nFor example, union is used after INSERTing, exclude when DELETING.\n\nOverlays are also used for access policies and rewrites.\n\n## Misc\n\nMost references to database objects are prepared by `common.get_backend_name`.\n"
  },
  {
    "path": "edb/pgsql/compiler/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Optional, Mapping, TYPE_CHECKING\nfrom dataclasses import dataclass\nimport itertools\nimport uuid\n\nfrom edb import errors\n\nfrom edb.ir import ast as irast\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import params as pgparams\nfrom edb.pgsql import types as pgtypes\n\nfrom . import config as _config_compiler  # NOQA\nfrom . import expr as _expr_compiler  # NOQA\nfrom . import stmt as _stmt_compiler  # NOQA\n\nfrom . import clauses\nfrom . import context\nfrom . import dispatch\nfrom . import dml\nfrom . import pathctx\nfrom . import aliases\n\nfrom .context import OutputFormat as OutputFormat # NOQA\n\nif TYPE_CHECKING:\n    import enums as pgce\n\n\n@dataclass(kw_only=True, slots=True, repr=False, eq=False, frozen=True)\nclass CompileResult:\n    ast: pgast.Base\n\n    env: context.Environment\n\n    argmap: dict[str, pgast.Param]\n\n    cached_params: Optional[list[tuple[str, ...]]] = None\n\n\ndef compile_ir_to_sql_tree(\n    ir_expr: irast.Base,\n    *,\n    output_format: Optional[OutputFormat] = None,\n    ignore_shapes: bool = False,\n    explicit_top_cast: Optional[irast.TypeRef] = None,\n    singleton_mode: bool = False,\n    named_param_prefix: Optional[tuple[str, ...]] = None,\n    expected_cardinality_one: bool = False,\n    is_explain: bool = False,\n    external_rvars: Optional[\n        Mapping[tuple[irast.PathId, pgce.PathAspect], pgast.PathRangeVar]\n    ] = None,\n    external_rels: Optional[\n        Mapping[\n            irast.PathId,\n            tuple[\n                pgast.BaseRelation | pgast.CommonTableExpr,\n                tuple[pgce.PathAspect, ...]\n            ],\n        ]\n    ] = None,\n    json_parameters: bool = False,\n    backend_runtime_params: Optional[pgparams.BackendRuntimeParams]=None,\n    cache_as_function: bool = False,\n    alias_generator: Optional[aliases.AliasGenerator] = None,\n    versioned_stdlib: bool = True,\n    # HACK?\n    versioned_singleton: bool = False,\n    sql_dml_mode: bool = False,\n) -> CompileResult:\n    if singleton_mode and not versioned_singleton:\n        versioned_stdlib = False\n\n    try:\n        # Transform to sql tree\n        query_params: list[irast.Param] = []\n        query_globals: list[irast.Global] = []\n        server_param_conversion_params: list[irast.Param] = []\n        type_rewrites: dict[tuple[uuid.UUID, bool], irast.Set] = {}\n        triggers: tuple[tuple[irast.Trigger, ...], ...] = ()\n\n        singletons = []\n        if isinstance(ir_expr, irast.Statement):\n            scope_tree = ir_expr.scope_tree\n            query_params = list(ir_expr.params)\n            query_globals = list(ir_expr.globals)\n            server_param_conversion_params = (\n                ir_expr.server_param_conversion_params\n            )\n            type_rewrites = ir_expr.type_rewrites\n            singletons = ir_expr.singletons\n            triggers = ir_expr.triggers\n            ir_expr = ir_expr.expr\n        elif isinstance(ir_expr, irast.ConfigCommand):\n            assert ir_expr.scope_tree\n            scope_tree = ir_expr.scope_tree\n            query_params = list(ir_expr.params)\n            if ir_expr.globals:\n                query_globals = list(ir_expr.globals)\n            if ir_expr.type_rewrites:\n                type_rewrites = ir_expr.type_rewrites\n        else:\n            scope_tree = irast.new_scope_tree()\n\n        # In JSON parameters mode, keep only the synthetic globals\n        if json_parameters:\n            query_globals = [\n                g for g in query_globals if g.global_name.module == '__'\n            ]\n        # Ensure permissions are after globals, since they are injected\n        # after other globals.\n        query_globals.sort(key=lambda g: g.is_permission)\n\n        scope_tree_nodes = {\n            node.unique_id: node for node in scope_tree.descendants\n            if node.unique_id is not None\n        }\n\n        if backend_runtime_params is None:\n            backend_runtime_params = pgparams.get_default_runtime_params()\n\n        env = context.Environment(\n            alias_generator=alias_generator,\n            output_format=output_format,\n            expected_cardinality_one=expected_cardinality_one,\n            named_param_prefix=named_param_prefix,\n            query_params=list(tuple(query_params) + tuple(query_globals)),\n            type_rewrites=type_rewrites,\n            ignore_object_shapes=ignore_shapes,\n            explicit_top_cast=explicit_top_cast,\n            is_explain=is_explain,\n            singleton_mode=singleton_mode,\n            scope_tree_nodes=scope_tree_nodes,\n            external_rvars=external_rvars,\n            backend_runtime_params=backend_runtime_params,\n            versioned_stdlib=versioned_stdlib,\n            sql_dml_mode=sql_dml_mode,\n        )\n\n        ctx = context.CompilerContextLevel(\n            None,\n            context.ContextSwitchMode.TRANSPARENT,\n            env=env,\n            scope_tree=scope_tree,\n        )\n        ctx.rel = pgast.SelectStmt()\n\n        _ = context.CompilerContext(initial=ctx)\n\n        ctx.singleton_mode = singleton_mode\n        ctx.expr_exposed = True\n        for sing in singletons:\n            ctx.path_scope[sing] = ctx.rel\n        if external_rels:\n            ctx.external_rels = external_rels\n        clauses.populate_argmap(\n            query_params,\n            query_globals,\n            server_param_conversion_params,\n            ctx=ctx,\n        )\n\n        qtree = dispatch.compile(ir_expr, ctx=ctx)\n        dml.compile_triggers(triggers, qtree, ctx=ctx)\n\n        if not singleton_mode:\n            if isinstance(ir_expr, irast.Set):\n                assert isinstance(qtree, pgast.Query)\n                clauses.fini_toplevel(qtree, ctx)\n\n            elif isinstance(qtree, pgast.Query):\n                # Other types of expressions may compile to queries which may\n                # use inheritance CTEs. Ensure they are added here.\n                clauses.insert_ctes(qtree, ctx)\n\n        if cache_as_function:\n            cached_params_idx = {\n                ctx.argmap[param.name].index: (\n                    pgtypes.pg_type_from_ir_typeref(\n                        param.ir_type.base_type or param.ir_type,\n                        # Needs serialized=True so types without their own\n                        # binary encodings (like postgis::box2d) get mapped\n                        # to the real underlying type.\n                        serialized=True,\n                    )\n                )\n                for param in itertools.chain(\n                    ctx.env.query_params,\n                    server_param_conversion_params,\n                )\n                if not param.sub_params\n            }\n        else:\n            cached_params_idx = {}\n        cached_params = [p for _, p in sorted(cached_params_idx.items())]\n\n    except errors.EdgeDBError:\n        # Don't wrap propertly typed EdgeDB errors into\n        # InternalServerError; raise them as is.\n        raise\n\n    except Exception as e:  # pragma: no cover\n        try:\n            args = [e.args[0]]\n        except (AttributeError, IndexError):\n            args = []\n        raise errors.InternalServerError(*args) from e\n\n    return CompileResult(\n        ast=qtree, env=env, argmap=ctx.argmap, cached_params=cached_params\n    )\n\n\ndef new_external_rvar(\n    *,\n    rel_name: tuple[str, ...],\n    path_id: irast.PathId,\n    outputs: Mapping[tuple[irast.PathId, tuple[pgce.PathAspect, ...]], str],\n) -> pgast.RelRangeVar:\n    \"\"\"Construct a ``RangeVar`` instance given a relation name and a path id.\n\n    Given an optionally-qualified relation name *rel_name* and a *path_id*,\n    return a ``RangeVar`` instance over the specified relation that is\n    then assumed to represent the *path_id* binding.\n\n    This is useful in situations where it is necessary to \"prime\" the compiler\n    with a list of external relations that exist in a larger SQL expression\n    that _this_ expression is being embedded into.\n\n    The *outputs* mapping optionally specifies a set of outputs in the\n    resulting range var as a ``(path_id, tuple-of-aspects): attribute name``\n    mapping.\n    \"\"\"\n    rel = new_external_rel(rel_name=rel_name, path_id=path_id)\n    assert rel.name\n\n    alias = pgast.Alias(aliasname=rel.name)\n\n    if not path_id.is_ptr_path():\n        rvar = pgast.RelRangeVar(\n            relation=rel, typeref=path_id.target, alias=alias)\n    else:\n        rvar = pgast.RelRangeVar(\n            relation=rel, alias=alias)\n\n    for (output_pid, output_aspects), colname in outputs.items():\n        var = pgast.ColumnRef(name=[colname])\n        for aspect in output_aspects:\n            rel.path_outputs[output_pid, aspect] = var\n\n    return rvar\n\n\ndef new_external_rvar_as_subquery(\n    *,\n    rel_name: tuple[str, ...],\n    path_id: irast.PathId,\n    aspects: tuple[pgce.PathAspect, ...],\n) -> pgast.SelectStmt:\n    rvar = new_external_rvar(\n        rel_name=rel_name,\n        path_id=path_id,\n        outputs={},\n    )\n    qry = pgast.SelectStmt(\n        from_clause=[rvar],\n    )\n    for aspect in aspects:\n        pathctx.put_path_rvar(qry, path_id, rvar, aspect=aspect)\n    return qry\n\n\ndef new_external_rel(\n    *,\n    rel_name: tuple[str, ...],\n    path_id: irast.PathId,\n) -> pgast.Relation:\n    if len(rel_name) == 1:\n        table_name = rel_name[0]\n        schema_name = None\n    elif len(rel_name) == 2:\n        schema_name, table_name = rel_name\n    else:\n        raise AssertionError(f'unexpected rvar name: {rel_name}')\n\n    return pgast.Relation(\n        name=table_name,\n        schemaname=schema_name,\n        path_id=path_id,\n    )\n"
  },
  {
    "path": "edb/pgsql/compiler/aliases.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom edb.common import compiler\nfrom edb.pgsql import common\n\n\nclass AliasGenerator(compiler.AliasGenerator):\n    def get(self, hint: str = '') -> str:\n        alias = super().get(hint)\n        return common.edgedb_name_to_pg_name(alias)\n"
  },
  {
    "path": "edb/pgsql/compiler/astutils.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Context-agnostic SQL AST utilities.\"\"\"\n\n\nfrom __future__ import annotations\n\nfrom typing import Optional, Iterator, Sequence, TYPE_CHECKING\n\nfrom edb.ir import typeutils as irtyputils\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import common\nfrom edb.pgsql import types as pg_types\n\nif TYPE_CHECKING:\n    from typing_extensions import TypeGuard\n    from edb.ir import ast as irast\n    from . import context\n\n\ndef tuple_element_for_shape_el(\n    shape_el: irast.Set,\n    value: Optional[pgast.BaseExpr]=None,\n    *,\n    ctx: context.CompilerContextLevel\n) -> pgast.TupleElementBase:\n    from edb.ir import ast as irast\n\n    if shape_el.path_id.is_type_intersection_path():\n        assert isinstance(shape_el.expr, irast.Pointer)\n        rptr = shape_el.expr.source.expr\n    else:\n        rptr = shape_el.expr\n    assert isinstance(rptr, irast.Pointer)\n    ptrref = rptr.ptrref\n    ptrname = ptrref.shortname\n\n    if value is not None:\n        return pgast.TupleElement(\n            path_id=shape_el.path_id,\n            name=ptrname.name,\n            val=value,\n        )\n    else:\n        return pgast.TupleElementBase(\n            path_id=shape_el.path_id,\n            name=ptrname.name,\n        )\n\n\ndef tuple_getattr(\n    tuple_val: pgast.BaseExpr,\n    tuple_typeref: irast.TypeRef,\n    attr: str,\n) -> pgast.BaseExpr:\n\n    ttypes = []\n    pgtypes = []\n    for i, st in enumerate(tuple_typeref.subtypes):\n        pgtype = pg_types.pg_type_from_ir_typeref(st)\n        pgtypes.append(pgtype)\n\n        if st.element_name:\n            ttypes.append(st.element_name)\n        else:\n            ttypes.append(str(i))\n\n    index = ttypes.index(attr)\n\n    set_expr: pgast.BaseExpr\n\n    if irtyputils.is_persistent_tuple(tuple_typeref):\n        set_expr = pgast.Indirection(\n            arg=tuple_val,\n            indirection=[pgast.RecordIndirectionOp(name=attr)],\n        )\n    else:\n        set_expr = pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    val=pgast.ColumnRef(\n                        name=[str(index)],\n                    ),\n                ),\n            ],\n            from_clause=[\n                pgast.RangeFunction(\n                    functions=[\n                        pgast.FuncCall(\n                            name=('unnest',),\n                            args=[\n                                pgast.ArrayExpr(\n                                    elements=[tuple_val],\n                                )\n                            ],\n                            coldeflist=[\n                                pgast.ColumnDef(\n                                    name=str(i),\n                                    typename=pgast.TypeName(\n                                        name=t\n                                    )\n                                )\n                                for i, t in enumerate(pgtypes)\n                            ]\n                        )\n                    ]\n                )\n            ]\n        )\n\n    return set_expr\n\n\ndef array_get_inner_array(\n    wrapped_array: pgast.BaseExpr,\n    array_typeref: irast.TypeRef,\n) -> pgast.BaseExpr:\n    \"\"\"Unwrap and get the inner array of a formerly nested array.\n\n    Since array<array<...>> is implemented as array<tuple<array<...>>>, when\n    an element is accessed, it needs to be unwrapped.\n\n    Essentially, this function takes tuple<array<...>> and returns array<...>\n\n    Postgres does not support arbitrarily accessing fields out of unnamed\n    composites and so we need to do an extra unnest(array[]) to be able to\n    specify the name and type our resulting array.\n\n    For example, the query: `select [[1]][0];` will produce the following SQL:\n\n    SELECT\n            \"expr-6~2\".\"array_value~4\" AS \"array_serialized~1\"\n        FROM\n            LATERAL\n            (SELECT\n                    \"expr-5~2\".\"array_value~3\" AS \"array_value~4\"\n                FROM\n                    LATERAL\n                    (SELECT\n                            (SELECT\n                                    \"0\"\n                                FROM\n                                    -- EXTRA unnest(array[])\n                                    unnest(ARRAY[\n                                        -- INDEX INDIRECTION\n                                        edgedb_v7_2f26206480._index(\n                                            \"expr-3~2\".\"array_value~2\",\n                                            ($2)::int8,\n                                            'ERROR MESSAGE'\n                                        )\n                                    ]) AS (\"0\" int8[])\n                            ) AS \"array_value~3\"\n                        FROM\n                            LATERAL\n                            -- INITAL ARRAY [[1]]\n                            (SELECT\n                                    ARRAY[ROW(\"expr-2~2\".\"array_value~1\")]\n                                    AS \"array_value~2\"\n                                FROM\n                                    LATERAL\n                                    (SELECT\n                                            ARRAY[($1)::int8]\n                                            AS \"array_value~1\"\n                                    ) AS \"expr-2~2\"\n                            ) AS \"expr-3~2\"\n                    ) AS \"expr-5~2\"\n            ) AS \"expr-6~2\"\n        WHERE\n            (\"expr-6~2\".\"array_value~4\" IS NOT NULL)\n        LIMIT\n        (SELECT\n                (101)::int8 AS \"expr~7_value~1\"\n        )\n    \"\"\"\n    return pgast.SelectStmt(\n        target_list=[\n            pgast.ResTarget(val=pgast.ColumnRef(name=['0'])),\n        ],\n        from_clause=[\n            pgast.RangeFunction(\n                functions=[\n                    pgast.FuncCall(\n                        name=('unnest',),\n                        args=[\n                            pgast.ArrayExpr(\n                                elements=[wrapped_array],\n                            )\n                        ],\n                        coldeflist=[\n                            pgast.ColumnDef(\n                                name='0',\n                                typename=pgast.TypeName(\n                                    name=pg_types.pg_type_from_ir_typeref(array_typeref)\n                                )\n                            )\n                        ]\n                    )\n                ]\n            )\n        ]\n    )\n\n\ndef is_null_const(expr: pgast.BaseExpr) -> bool:\n    if isinstance(expr, pgast.TypeCast):\n        expr = expr.arg\n    return isinstance(expr, pgast.NullConstant)\n\n\ndef is_set_op_query(query: pgast.BaseExpr) -> TypeGuard[pgast.SelectStmt]:\n    return (\n        isinstance(query, pgast.SelectStmt)\n        and query.op is not None\n    )\n\n\ndef get_leftmost_query(query: pgast.Query) -> pgast.Query:\n    result = query\n    while is_set_op_query(result):\n        assert result.larg\n        result = result.larg\n    return result\n\n\ndef each_query_in_set(qry: pgast.Query) -> Iterator[pgast.Query]:\n    # We do this iteratively instead of recursively (with yield from)\n    # to avoid being pointlessly quadratic.\n    stack = [qry]\n    while stack:\n        qry = stack.pop()\n        if is_set_op_query(qry):\n            assert qry.larg and qry.rarg\n            stack.append(qry.rarg)\n            stack.append(qry.larg)\n        else:\n            yield qry\n\n\ndef each_base_rvar(rvar: pgast.BaseRangeVar) -> Iterator[pgast.BaseRangeVar]:\n    # We do this iteratively instead of recursively (with yield from)\n    # to avoid being pointlessly quadratic.\n    stack = [rvar]\n    while stack:\n        rvar = stack.pop()\n        if isinstance(rvar, pgast.JoinExpr):\n            for clause in reversed(rvar.joins):\n                stack.append(clause.rarg)\n            stack.append(rvar.larg)\n        else:\n            yield rvar\n\n\ndef new_binop(\n    lexpr: pgast.BaseExpr,\n    rexpr: pgast.BaseExpr,\n    op: str,\n) -> pgast.Expr:\n    return pgast.Expr(\n        name=op,\n        lexpr=lexpr,\n        rexpr=rexpr\n    )\n\n\ndef extend_binop(\n    binop: Optional[pgast.BaseExpr],\n    *exprs: pgast.BaseExpr,\n    op: str = 'AND',\n) -> pgast.BaseExpr:\n    exprlist = list(exprs)\n    result: pgast.BaseExpr\n\n    if binop is None:\n        result = exprlist.pop(0)\n    else:\n        result = binop\n\n    for expr in exprlist:\n        if expr is not None and expr is not result:\n            result = new_binop(lexpr=result, op=op, rexpr=expr)\n\n    return result\n\n\ndef extend_concat(\n    expr: str | pgast.BaseExpr, *exprs: str | pgast.BaseExpr\n) -> pgast.BaseExpr:\n    return extend_binop(\n        pgast.StringConstant(val=expr) if isinstance(expr, str) else expr,\n        *[\n            pgast.StringConstant(val=e) if isinstance(e, str) else e\n            for e in exprs\n        ],\n        op='||',\n    )\n\n\ndef new_coalesce(\n    expr: pgast.BaseExpr, fallback: pgast.BaseExpr\n) -> pgast.BaseExpr:\n    return pgast.FuncCall(name=('coalesce',), args=[expr, fallback])\n\n\ndef extend_select_op(\n    stmt: Optional[pgast.SelectStmt],\n    *stmts: pgast.SelectStmt,\n    op: str = 'UNION',\n) -> Optional[pgast.SelectStmt]:\n    stmt_list = list(stmts)\n    result: pgast.SelectStmt\n\n    if stmt is None:\n        if len(stmt_list) == 0:\n            return None\n        result = stmt_list.pop(0)\n    else:\n        result = stmt\n\n    for s in stmt_list:\n        if s is not None and s is not result:\n            result = pgast.SelectStmt(larg=result, op=op, rarg=s)\n\n    return result\n\n\ndef new_unop(op: str, expr: pgast.BaseExpr) -> pgast.Expr:\n    return pgast.Expr(name=op, rexpr=expr)\n\n\ndef join_condition(\n    lref: pgast.ColumnRef,\n    rref: pgast.ColumnRef,\n) -> pgast.BaseExpr:\n    path_cond: pgast.BaseExpr = new_binop(lref, rref, op='=')\n\n    if lref.optional:\n        opt_cond = pgast.NullTest(arg=lref)\n        path_cond = extend_binop(path_cond, opt_cond, op='OR')\n\n    if rref.optional:\n        opt_cond = pgast.NullTest(arg=rref)\n        path_cond = extend_binop(path_cond, opt_cond, op='OR')\n\n    return path_cond\n\n\ndef safe_array_expr(\n    elements: list[pgast.BaseExpr],\n    *,\n    ser_safe: bool = False,\n    ctx: context.CompilerContextLevel,\n) -> pgast.BaseExpr:\n    result: pgast.BaseExpr = pgast.ArrayExpr(\n        elements=elements,\n        ser_safe=ser_safe,\n    )\n    if any(el.nullable for el in elements):\n        result = pgast.FuncCall(\n            name=edgedb_func('_nullif_array_nulls', ctx=ctx),\n            args=[result],\n            ser_safe=ser_safe,\n        )\n    return result\n\n\ndef find_column_in_subselect_rvar(\n    rvar: pgast.RangeSubselect,\n    name: str,\n) -> int:\n    # Range over a subquery, we can inspect the output list\n    # of the subquery.  If the subquery is a UNION (or EXCEPT),\n    # we take the leftmost non-setop query.\n    subquery = get_leftmost_query(rvar.subquery)\n    for i, rt in enumerate(subquery.target_list):\n        if rt.name == name:\n            return i\n\n    raise RuntimeError(f'cannot find {name!r} in {rvar} output')\n\n\ndef get_column(\n    rvar: pgast.BaseRangeVar,\n    colspec: str | pgast.ColumnRef,\n    *,\n    is_packed_multi: bool = True,\n    nullable: Optional[bool] = None,\n) -> pgast.ColumnRef:\n\n    if isinstance(colspec, pgast.ColumnRef):\n        colname = colspec.name[-1]\n    else:\n        colname = colspec\n\n    assert isinstance(colname, str)\n\n    ser_safe = False\n\n    if nullable is None:\n        if isinstance(rvar, pgast.RelRangeVar):\n            # Range over a relation, we cannot infer nullability in\n            # this context, so assume it's true, unless we are looking\n            # at a colspec that says it is false\n            if isinstance(colspec, pgast.ColumnRef):\n                nullable = colspec.nullable\n            else:\n                nullable = True\n\n        elif isinstance(rvar, pgast.RangeSubselect):\n            col_idx = find_column_in_subselect_rvar(rvar, colname)\n            if is_set_op_query(rvar.subquery):\n                nullables = []\n                ser_safes = []\n\n                for q in each_query_in_set(rvar.subquery):\n                    nullables.append(q.target_list[col_idx].nullable)\n                    ser_safes.append(q.target_list[col_idx].ser_safe)\n\n                nullable = any(nullables)\n                ser_safe = all(ser_safes)\n            else:\n                rt = rvar.subquery.target_list[col_idx]\n                nullable = rt.nullable\n                ser_safe = rt.ser_safe\n\n        elif isinstance(rvar, pgast.RangeFunction):\n            # Range over a function.\n            # TODO: look into the possibility of inspecting coldeflist.\n            nullable = True\n\n        elif isinstance(rvar, pgast.JoinExpr):\n            raise RuntimeError(\n                f'cannot find {colname!r} in unexpected {rvar!r} range var')\n\n    name = [rvar.alias.aliasname, colname]\n\n    return pgast.ColumnRef(\n        name=name, nullable=nullable, ser_safe=ser_safe,\n        is_packed_multi=is_packed_multi)\n\n\ndef get_rvar_var(\n    rvar: pgast.BaseRangeVar, var: pgast.OutputVar\n) -> pgast.OutputVar:\n\n    fieldref: pgast.OutputVar\n\n    if isinstance(var, pgast.TupleVarBase):\n        elements = []\n\n        for el in var.elements:\n            assert isinstance(el.name, pgast.OutputVar)\n            val = get_rvar_var(rvar, el.name)\n            elements.append(\n                pgast.TupleElement(\n                    path_id=el.path_id, name=el.name, val=val))\n\n        fieldref = pgast.TupleVar(\n            elements,\n            named=var.named,\n            typeref=var.typeref,\n            is_packed_multi=var.is_packed_multi,\n        )\n\n    elif isinstance(var, pgast.ColumnRef):\n        fieldref = get_column(rvar, var, is_packed_multi=var.is_packed_multi)\n\n    elif isinstance(var, pgast.ExprOutputVar):\n        fieldref = var\n\n    else:\n        raise AssertionError(f'unexpected OutputVar subclass: {var!r}')\n\n    return fieldref\n\n\ndef strip_output_var(\n    var: pgast.OutputVar,\n    *,\n    optional: Optional[bool] = None,\n    nullable: Optional[bool] = None,\n) -> pgast.OutputVar:\n\n    result: pgast.OutputVar\n\n    if isinstance(var, pgast.TupleVarBase):\n        elements = []\n\n        for el in var.elements:\n            val: pgast.OutputVar\n            el_name = el.name\n\n            if isinstance(el_name, str):\n                val = pgast.ColumnRef(name=[el_name])\n            elif isinstance(el_name, pgast.OutputVar):\n                val = strip_output_var(el_name)\n            else:\n                raise AssertionError(\n                    f'unexpected tuple element class: {el_name!r}')\n\n            elements.append(\n                pgast.TupleElement(\n                    path_id=el.path_id, name=el_name, val=val))\n\n        result = pgast.TupleVar(\n            elements,\n            named=var.named,\n            typeref=var.typeref,\n        )\n\n    elif isinstance(var, pgast.ColumnRef):\n        result = pgast.ColumnRef(\n            name=[var.name[-1]],\n            optional=optional if optional is not None else var.optional,\n            nullable=nullable if nullable is not None else var.nullable,\n        )\n\n    else:\n        raise AssertionError(f'unexpected OutputVar subclass: {var!r}')\n\n    return result\n\n\ndef select_is_simple(stmt: pgast.SelectStmt) -> bool:\n    return (\n        not stmt.distinct_clause\n        and not stmt.where_clause\n        and not stmt.group_clause\n        and not stmt.having_clause\n        and not stmt.window_clause\n        and not stmt.values\n        and not stmt.sort_clause\n        and not stmt.limit_offset\n        and not stmt.limit_count\n        and not stmt.locking_clause\n        and not stmt.op\n    )\n\n\ndef is_row_expr(expr: pgast.BaseExpr) -> bool:\n    while True:\n        if isinstance(expr, (pgast.RowExpr, pgast.ImplicitRowExpr)):\n            return True\n        elif isinstance(expr, pgast.TypeCast):\n            expr = expr.arg\n        else:\n            return False\n\n\ndef _get_target_from_range(\n    target: pgast.BaseExpr, rvar: pgast.BaseRangeVar\n) -> Optional[pgast.BaseExpr]:\n    \"\"\"Try to read a target out of a very simple rvar.\n\n    The goal here is to allow collapsing trivial pass-through subqueries.\n    In particular, given a target `foo.bar` and an rvar\n    `(SELECT <expr> as \"bar\") AS \"foo\"`, we produce <expr>.\n\n    We can also recursively handle the nested case.\n    \"\"\"\n    if (\n        not isinstance(rvar, pgast.RangeSubselect)\n\n        # Check that the relation name matches the rvar\n        or not isinstance(target, pgast.ColumnRef)\n        or not target.name\n        or target.name[0] != rvar.alias.aliasname\n\n        # And that the rvar is a simple subquery with one target\n        # and at most one from clause\n        or not (subq := rvar.subquery)\n        or len(subq.target_list) != 1\n        or not isinstance(subq, pgast.SelectStmt)\n        or not select_is_simple(subq)\n        or len(subq.from_clause) > 1\n\n        # And that the one target matches\n        or not (inner_tgt := rvar.subquery.target_list[0])\n        or inner_tgt.name != target.name[1]\n    ):\n        return None\n\n    if subq.from_clause:\n        return _get_target_from_range(inner_tgt.val, subq.from_clause[0])\n    else:\n        return inner_tgt.val\n\n\ndef collapse_query(query: pgast.Query) -> pgast.BaseExpr:\n    \"\"\"Try to collapse trivial queries into simple expressions.\n\n    In particular, we want to transform\n    `(SELECT foo.bar FROM LATERAL (SELECT <expr> as \"bar\") AS \"foo\")`\n    into simply `<expr>`.\n    \"\"\"\n    if not isinstance(query, pgast.SelectStmt):\n        return query\n\n    if (\n        isinstance(query, pgast.SelectStmt)\n        and len(query.target_list) == 1\n        and len(query.from_clause) == 0\n        and select_is_simple(query)\n    ):\n        return query.target_list[0].val\n\n    if (\n        not isinstance(query, pgast.SelectStmt)\n        or len(query.target_list) != 1\n        or len(query.from_clause) != 1\n    ):\n        return query\n\n    val = _get_target_from_range(\n        query.target_list[0].val, query.from_clause[0])\n    if val:\n        return val\n    else:\n        return query\n\n\ndef compile_typeref(expr: irast.TypeRef) -> pgast.BaseExpr:\n    if expr.collection:\n        raise NotImplementedError()\n    else:\n        result = pgast.TypeCast(\n            arg=pgast.StringConstant(val=str(expr.id)),\n            type_name=pgast.TypeName(\n                name=('uuid',)\n            )\n        )\n\n    return result\n\n\ndef maybe_unpack_row(expr: pgast.Base) -> Sequence[pgast.BaseExpr]:\n    assert isinstance(expr, pgast.BaseExpr)\n    match expr:\n        case pgast.ImplicitRowExpr():\n            return expr.args\n        case pgast.RowExpr():\n            return expr.args\n    return (expr,)\n\n\ndef edgedb_func(\n    name: str,\n    *,\n    ctx: context.CompilerContextLevel\n) -> tuple[str, ...]:\n    return common.maybe_versioned_name(\n        ('edgedb', name),\n        versioned=ctx.env.versioned_stdlib,\n    )\n"
  },
  {
    "path": "edb/pgsql/compiler/clauses.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Optional, Sequence\n\nimport random\n\nfrom edb.common import ast as ast_visitor\n\nfrom edb.edgeql import qltypes\nfrom edb.ir import ast as irast\nfrom edb.ir import utils as irutils\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import types as pg_types\n\nfrom . import astutils\nfrom . import context\nfrom . import dispatch\nfrom . import dml\nfrom . import enums as pgce\nfrom . import output\nfrom . import pathctx\nfrom . import relctx\nfrom . import relgen\n\n\ndef get_volatility_ref(\n        path_id: irast.PathId,\n        stmt: pgast.SelectStmt,\n        *,\n        ctx: context.CompilerContextLevel) -> Optional[pgast.BaseExpr]:\n    \"\"\"Produce an appropriate volatility_ref from a path_id.\"\"\"\n\n    ref: Optional[pgast.BaseExpr] = relctx.maybe_get_path_var(\n        stmt, path_id, aspect=pgce.PathAspect.ITERATOR, ctx=ctx)\n    if not ref:\n        ref = relctx.maybe_get_path_var(\n            stmt, path_id, aspect=pgce.PathAspect.IDENTITY, ctx=ctx)\n    if not ref:\n        rvar = relctx.maybe_get_path_rvar(\n            stmt, path_id, aspect=pgce.PathAspect.VALUE, ctx=ctx)\n        if (\n            rvar\n            and isinstance(rvar.query, pgast.ReturningQuery)\n            # Expanded inhviews might be unions, which can't naively have\n            # a row_number stuck on; they should be safe to just grab\n            # the path_id value from, though\n            and rvar.tag != 'expanded-inhview'\n        ):\n            # If we are selecting from a nontrivial subquery, manually\n            # add a volatility ref based on row_number. We do it\n            # manually because the row number isn't /really/ the\n            # identity of the set.\n            name = ctx.env.aliases.get('key')\n            rvar.query.target_list.append(\n                pgast.ResTarget(\n                    name=name,\n                    val=pgast.FuncCall(name=('row_number',), args=[],\n                                       over=pgast.WindowDef())\n                )\n            )\n            ref = pgast.ColumnRef(name=[rvar.alias.aliasname, name])\n        else:\n            ref = relctx.maybe_get_path_var(\n                stmt, path_id, aspect=pgce.PathAspect.VALUE, ctx=ctx)\n\n    return ref\n\n\ndef setup_iterator_volatility(\n        iterator: Optional[irast.Set | pgast.IteratorCTE], *,\n        ctx: context.CompilerContextLevel) -> None:\n    if iterator is None:\n        return\n\n    path_id = iterator.path_id\n\n    # We use a callback scheme here to avoid inserting volatility ref\n    # columns unless there is actually a volatile operation that\n    # requires it.\n    ctx.volatility_ref += (\n        lambda stmt, xctx: get_volatility_ref(path_id, stmt, ctx=xctx),)\n\n\ndef compile_materialized_exprs(\n        query: pgast.SelectStmt, stmt: irast.Stmt, *,\n        ctx: context.CompilerContextLevel) -> None:\n    if not stmt.materialized_sets:\n        return\n\n    if stmt in ctx.materializing:\n        return\n\n    with context.output_format(ctx, context.OutputFormat.NATIVE), (\n            ctx.new()) as matctx:\n        matctx.materializing |= {stmt}\n        matctx.expr_exposed = True\n\n        # HACK: Sort longer paths before shorter ones\n        # We want foo->bar to appear before foo\n        mat_sets = sorted(\n            (stmt.materialized_sets.values()),\n            key=lambda m: -len(m.materialized.path_id),\n        )\n\n        for mat_set in mat_sets:\n            if len(mat_set.uses) <= 1:\n                continue\n            assert mat_set.finalized, \"materialized set was not finalized!\"\n            if relctx.find_rvar(\n                    query, flavor='packed',\n                    path_id=mat_set.materialized.path_id, ctx=matctx):\n                continue\n\n            _compile_materialized_expr(query, mat_set, ctx=matctx)\n\n\ndef _compile_materialized_expr(\n    query: pgast.SelectStmt,\n    mat_set: irast.MaterializedSet,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> None:\n    mat_ids = set(mat_set.uses)\n\n    # We pack optional things into arrays also, since it works.\n    # TODO: use NULL?\n    card = mat_set.cardinality\n    assert card != qltypes.Cardinality.UNKNOWN\n    is_singleton = card.is_single() and not card.can_be_zero()\n\n    old_scope = ctx.path_scope\n    ctx.path_scope = old_scope.new_child()\n    for mat_id in mat_ids:\n        for k in old_scope:\n            if k.startswith(mat_id):\n                ctx.path_scope[k] = None\n    mat_qry = relgen.set_as_subquery(\n        mat_set.materialized, as_value=True, ctx=ctx\n    )\n\n    if not is_singleton:\n        mat_qry = relctx.set_to_array(\n            path_id=mat_set.materialized.path_id,\n            query=mat_qry,\n            ctx=ctx)\n\n    if not mat_qry.target_list[0].name:\n        mat_qry.target_list[0].name = ctx.env.aliases.get('v')\n\n    ref = pgast.ColumnRef(\n        name=[mat_qry.target_list[0].name],\n        is_packed_multi=not is_singleton,\n    )\n    for mat_id in mat_ids:\n        pathctx.put_path_packed_output(mat_qry, mat_id, ref)\n\n    mat_rvar = relctx.rvar_for_rel(mat_qry, lateral=True, ctx=ctx)\n    for mat_id in mat_ids:\n        relctx.include_rvar(\n            query, mat_rvar, path_id=mat_id,\n            flavor='packed', update_mask=False, pull_namespace=False,\n            ctx=ctx,\n        )\n\n\ndef compile_iterator_expr(\n        query: pgast.SelectStmt, iterator_expr: irast.Set, *,\n        is_dml: bool,\n        ctx: context.CompilerContextLevel) \\\n        -> pgast.PathRangeVar:\n\n    assert isinstance(iterator_expr.expr, (irast.GroupStmt, irast.SelectStmt))\n\n    ctx.env.binding_dml[iterator_expr.path_id] = irutils.get_dml_sources(\n        iterator_expr, ctx.env.binding_dml\n    )\n\n    with ctx.new() as subctx:\n        subctx.expr_exposed = False\n        subctx.rel = query\n\n        dispatch.visit(iterator_expr, ctx=subctx)\n        iterator_rvar = relctx.get_path_rvar(\n            query,\n            iterator_expr.path_id,\n            aspect=pgce.PathAspect.VALUE,\n            ctx=ctx,\n        )\n        iterator_query = iterator_rvar.query\n\n        # If the iterator value is nullable, add a null test. This\n        # makes sure that we don't spuriously produce output when\n        # iterating over optional pointers.\n        is_optional = ctx.scope_tree.is_optional(iterator_expr.path_id)\n        if isinstance(iterator_query, pgast.SelectStmt):\n            iterator_var = pathctx.get_path_value_var(\n                iterator_query, path_id=iterator_expr.path_id, env=ctx.env)\n        if not is_optional:\n            if isinstance(iterator_query, pgast.SelectStmt):\n                iterator_var = pathctx.get_path_value_var(\n                    iterator_query, path_id=iterator_expr.path_id, env=ctx.env)\n                if iterator_var.nullable:\n                    iterator_query.where_clause = astutils.extend_binop(\n                        iterator_query.where_clause,\n                        pgast.NullTest(arg=iterator_var, negated=True))\n            elif isinstance(iterator_query, pgast.Relation):\n                # will never be null\n                pass\n            else:\n                raise NotImplementedError()\n\n        # For DML-containing FOR, regardless of result type, iterators need\n        # their own transient identity for path identity of the\n        # iterator expression in order maintain correct correlation\n        # for the state of iteration in DML statements, even when\n        # there are duplicates in the iterator.  This gets tracked as\n        # a special ITERATOR aspect in order to distinguish it from\n        # actual object identity.\n        #\n        # We also do this for optional iterators, since object\n        # identity isn't safe to use as a volatility ref if the object\n        # might be NULL.\n        if is_dml or is_optional:\n            relctx.create_iterator_identity_for_path(\n                iterator_expr.path_id, iterator_query,\n                apply_volatility=is_dml,\n                ctx=subctx)\n\n            pathctx.put_path_rvar(\n                query,\n                iterator_expr.path_id,\n                iterator_rvar,\n                aspect=pgce.PathAspect.ITERATOR,\n            )\n\n    return iterator_rvar\n\n\ndef compile_output(\n        ir_set: irast.Set, *,\n        ctx: context.CompilerContextLevel) -> pgast.OutputVar:\n    with ctx.new() as newctx:\n        dispatch.visit(ir_set, ctx=newctx)\n\n        path_id = ir_set.path_id\n\n        if (output.in_serialization_ctx(ctx) and\n                newctx.stmt is newctx.toplevel_stmt):\n            val = pathctx.get_path_serialized_output(\n                ctx.rel, path_id, env=ctx.env)\n        else:\n            val = pathctx.get_path_value_output(\n                ctx.rel, path_id, env=ctx.env)\n\n    return val\n\n\ndef compile_volatile_bindings(\n    stmt: irast.Stmt,\n    *,\n    ctx: context.CompilerContextLevel\n) -> None:\n    for binding, volatility in (stmt.bindings or ()):\n        # If something we are WITH binding contains DML, we want to\n        # compile it *now*, in the context of its initial appearance\n        # and not where the variable is used.\n        #\n        # Similarly, if something we are WITH binding is volatile and the stmt\n        # contains dml, we similarly want to compile it *now*.\n\n        # If the binding is a with binding for a DML stmt, manually construct\n        # the CTEs.\n        #\n        # Note: This condition is checked first, because if the binding\n        # *references* DML then contains_dml is true. If the binding is compiled\n        # normally, since the referenced DML was already compiled, the rvar will\n        # be retrieved, and no CTEs will be set up.\n        if volatility.is_volatile() and irutils.contains_dml(stmt):\n            _compile_volatile_binding_for_dml(stmt, binding, ctx=ctx)\n\n        # For typical DML, just compile it. This will populate dml_stmts with\n        # the CTEs, which will be picked up when the variable is referenced.\n        elif irutils.contains_dml(binding):\n            with ctx.substmt() as bctx:\n                dispatch.compile(binding, ctx=bctx)\n\n\ndef _compile_volatile_binding_for_dml(\n    stmt: irast.Stmt,\n    binding: irast.Set,\n    *,\n    ctx: context.CompilerContextLevel\n) -> None:\n    materialized_set = None\n    if (\n        stmt.materialized_sets\n        and binding.typeref.id in stmt.materialized_sets\n    ):\n        materialized_set = stmt.materialized_sets[binding.typeref.id]\n    assert materialized_set is not None\n\n    last_iterator = ctx.enclosing_cte_iterator\n\n    with (\n        context.output_format(ctx, context.OutputFormat.NATIVE),\n        ctx.newrel() as matctx\n    ):\n        matctx.materializing |= {stmt}\n        matctx.expr_exposed = True\n\n        dml.merge_iterator(last_iterator, matctx.rel, ctx=matctx)\n        setup_iterator_volatility(last_iterator, ctx=matctx)\n\n        _compile_materialized_expr(\n            matctx.rel, materialized_set, ctx=matctx\n        )\n\n        # Add iterator identity\n        bind_pathid = (\n            irast.PathId.new_dummy(ctx.env.aliases.get('bind_path'))\n        )\n        with matctx.subrel() as bind_pathid_ctx:\n            relctx.create_iterator_identity_for_path(\n                bind_pathid, bind_pathid_ctx.rel, ctx=bind_pathid_ctx\n            )\n        bind_id_rvar = relctx.rvar_for_rel(\n            bind_pathid_ctx.rel, lateral=True, ctx=matctx\n        )\n        relctx.include_rvar(\n            matctx.rel, bind_id_rvar, path_id=bind_pathid, ctx=matctx\n        )\n\n    bind_cte = pgast.CommonTableExpr(\n        name=ctx.env.aliases.get('bind'),\n        query=matctx.rel,\n        materialized=False,\n    )\n\n    bind_iterator = pgast.IteratorCTE(\n        path_id=bind_pathid,\n        cte=bind_cte,\n        parent=last_iterator,\n        iterator_bond=True,\n    )\n    ctx.toplevel_stmt.append_cte(bind_cte)\n\n    # Merge the new iterator\n    ctx.path_scope = ctx.path_scope.new_child()\n    dml.merge_iterator(bind_iterator, ctx.rel, ctx=ctx)\n    setup_iterator_volatility(bind_iterator, ctx=ctx)\n\n    ctx.enclosing_cte_iterator = bind_iterator\n\n\ndef compile_filter_clause(\n        ir_set: irast.Set,\n        cardinality: qltypes.Cardinality, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n    where_clause: pgast.BaseExpr\n\n    with ctx.new() as ctx1:\n        ctx1.expr_exposed = False\n\n        assert cardinality != qltypes.Cardinality.UNKNOWN\n        if cardinality.is_single():\n            where_clause = dispatch.compile(ir_set, ctx=ctx1)\n        else:\n            # In WHERE we compile ir.Set as a boolean disjunction:\n            #    EXISTS(SELECT FROM SetRel WHERE SetRel.value)\n            with ctx1.subrel() as subctx:\n                dispatch.visit(ir_set, ctx=subctx)\n                wrapper = subctx.rel\n                wrapper.where_clause = pathctx.get_path_value_var(\n                    wrapper, ir_set.path_id, env=subctx.env)\n\n            where_clause = pgast.SubLink(operator=\"EXISTS\", expr=wrapper)\n\n    return where_clause\n\n\ndef compile_orderby_clause(\n        ir_exprs: Sequence[irast.SortExpr], *,\n        ctx: context.CompilerContextLevel) -> list[pgast.SortBy]:\n\n    sort_clause = []\n\n    for expr in ir_exprs:\n        with ctx.new() as orderctx:\n            orderctx.expr_exposed = False\n\n            # In ORDER BY we compile ir.Set as a subquery:\n            #    SELECT SetRel.value FROM SetRel)\n            subq = relgen.set_as_subquery(\n                expr.expr, as_value=True, ctx=orderctx)\n            # pg apparently can't use indexes for ordering if the body\n            # of an ORDER BY is a subquery, so try to collapse the query\n            # into a simple expression.\n            value = astutils.collapse_query(subq)\n\n            sortexpr = pgast.SortBy(\n                node=value,\n                dir=expr.direction,\n                nulls=expr.nones_order)\n            sort_clause.append(sortexpr)\n\n    return sort_clause\n\n\ndef compile_limit_offset_clause(\n        ir_set: Optional[irast.Set], *,\n        ctx: context.CompilerContextLevel) -> Optional[pgast.BaseExpr]:\n    if ir_set is None:\n        return None\n\n    with ctx.new() as ctx1:\n        ctx1.expr_exposed = False\n\n        # In OFFSET/LIMIT we compile ir.Set as a subquery:\n        #    SELECT SetRel.value FROM SetRel)\n        limit_offset_clause = relgen.set_as_subquery(ir_set, ctx=ctx1)\n\n    return limit_offset_clause\n\n\ndef make_check_scan(\n    check_cte: pgast.CommonTableExpr,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.BaseExpr:\n    return pgast.SelectStmt(\n        target_list=[\n            pgast.ResTarget(\n                val=pgast.FuncCall(name=('count',), args=[pgast.Star()]),\n            )\n        ],\n        from_clause=[\n            relctx.rvar_for_rel(check_cte, ctx=ctx),\n        ],\n    )\n\n\ndef scan_check_ctes(\n    stmt: pgast.Query,\n    check_ctes: list[pgast.CommonTableExpr],\n    *,\n    ctx: context.CompilerContextLevel,\n) -> None:\n    if not check_ctes:\n        return\n\n    # Scan all of the check CTEs to enforce constraints that are\n    # checked as explicit queries and not Postgres constraints or\n    # triggers.\n\n    # To make sure that Postgres can't optimize the checks away, we\n    # reference them in the where clause of an UPDATE to a dummy\n    # table.\n\n    # Add a big random number, so that different queries should try to\n    # access different \"rows\" of the table, in case that matters.\n    base_int = random.randint(0, (1 << 60) - 1)\n    val: pgast.BaseExpr = pgast.NumericConstant(val=str(base_int))\n\n    for check_cte in check_ctes:\n        # We want the CTE to be MATERIALIZED, because otherwise\n        # Postgres might not fully evaluate all its columns when\n        # scanning it.\n        check_cte.materialized = True\n        check = make_check_scan(check_cte, ctx=ctx)\n        val = pgast.Expr(name=\"+\", lexpr=val, rexpr=check)\n\n    update_query = pgast.UpdateStmt(\n        targets=[pgast.UpdateTarget(\n            name='flag', val=pgast.BooleanConstant(val=True)\n        )],\n        relation=pgast.RelRangeVar(relation=pgast.Relation(\n            name='_dml_dummy')),\n        where_clause=pgast.Expr(\n            name=\"=\",\n            lexpr=pgast.ColumnRef(name=[\"id\"]),\n            rexpr=val,\n        )\n    )\n    stmt.append_cte(pgast.CommonTableExpr(\n        query=update_query,\n        name=ctx.env.aliases.get(hint='check_scan')\n    ))\n\n\ndef insert_ctes(\n    stmt: pgast.Query, ctx: context.CompilerContextLevel\n) -> None:\n    if stmt.ctes is None:\n        stmt.ctes = []\n    stmt.ctes[:0] = [\n        *ctx.param_ctes.values(),\n        *ctx.ptr_inheritance_ctes.values(),\n        *ctx.ordered_type_ctes,\n    ]\n\n\ndef fini_toplevel(\n        stmt: pgast.Query, ctx: context.CompilerContextLevel) -> None:\n\n    scan_check_ctes(stmt, ctx.env.check_ctes, ctx=ctx)\n\n    # Type rewrites and inheritance CTEs go first.\n    insert_ctes(stmt, ctx)\n\n    if ctx.env.named_param_prefix is None:\n        # Adding unused parameters into a CTE\n\n        # Find the used parameters by searching the query, so we don't\n        # get confused if something has been compiled but then omitted\n        # from the output for some reason.\n        param_refs = ast_visitor.find_children(stmt, pgast.ParamRef)\n\n        used = {param_ref.number for param_ref in param_refs}\n\n        targets = []\n        for param in ctx.env.query_params:\n            pgparam = ctx.argmap[param.name]\n            if pgparam.index in used or param.sub_params:\n                continue\n            targets.append(pgast.ResTarget(val=pgast.TypeCast(\n                arg=pgast.ParamRef(number=pgparam.index),\n                type_name=pgast.TypeName(\n                    name=pg_types.pg_type_from_ir_typeref(param.ir_type)\n                )\n            )))\n            if isinstance(param, irast.Global) and param.has_present_arg:\n                targets.append(pgast.ResTarget(val=pgast.TypeCast(\n                    arg=pgast.ParamRef(number=pgparam.index + 1),\n                    type_name=pgast.TypeName(name=('bool',)),\n                )))\n\n        if targets:\n            stmt.append_cte(\n                pgast.CommonTableExpr(\n                    name=\"__unused_vars\",\n                    query=pgast.SelectStmt(target_list=targets)\n                )\n            )\n\n\ndef populate_argmap(\n    params: list[irast.Param],\n    globals: list[irast.Global],\n    server_param_conversion_params: list[irast.Param],\n    *,\n    ctx: context.CompilerContextLevel,\n) -> None:\n    physical_index = 1\n    logical_index = 1\n    for map_extra in (False, True):\n        for param in params:\n            if (\n                ctx.env.named_param_prefix is not None\n                and not param.name.isdecimal()\n            ):\n                continue\n            if param.name.startswith('__edb_arg_') != map_extra:\n                continue\n\n            ctx.argmap[param.name] = pgast.Param(\n                index=physical_index,\n                logical_index=logical_index,\n                required=param.required,\n            )\n            if not param.sub_params:\n                physical_index += 1\n            if not param.is_sub_param:\n                logical_index += 1\n    for param in globals:\n        ctx.argmap[param.name] = pgast.Param(\n            index=physical_index,\n            required=param.required,\n            logical_index=-1,\n        )\n        physical_index += 1\n        if param.has_present_arg:\n            ctx.argmap[param.name + \"present__\"] = pgast.Param(\n                index=physical_index,\n                required=True,\n                logical_index=-1,\n            )\n            physical_index += 1\n    for param in server_param_conversion_params:\n        ctx.argmap[param.name] = pgast.Param(\n            index=physical_index,\n            logical_index=logical_index,\n            required=param.required,\n        )\n        if not param.sub_params:\n            physical_index += 1\n        if not param.is_sub_param:\n            logical_index += 1\n"
  },
  {
    "path": "edb/pgsql/compiler/config.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom edb import errors\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtyputils\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom edb.schema import casts as s_casts\nfrom edb.schema import name as sn\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import common\n\nfrom . import astutils\nfrom . import context\nfrom . import dispatch\nfrom . import pathctx\nfrom . import relctx\nfrom . import output\n\n\n@dispatch.compile.register\ndef compile_ConfigSet(\n    op: irast.ConfigSet,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.BaseExpr:\n\n    val = _compile_config_value(op, ctx=ctx)\n    result: pgast.BaseExpr\n\n    if op.scope is qltypes.ConfigScope.INSTANCE and op.backend_setting:\n        if not ctx.env.backend_runtime_params.has_configfile_access:\n            raise errors.UnsupportedBackendFeatureError(\n                \"configuring backend parameters via CONFIGURE INSTANCE\"\n                \" is not supported by the current backend\"\n            )\n        result = pgast.AlterSystem(\n            name=op.backend_setting,\n            value=val,\n        )\n\n    elif op.scope is qltypes.ConfigScope.DATABASE and op.backend_setting:\n        if not isinstance(val, pgast.StringConstant):\n            val = pgast.TypeCast(\n                arg=val,\n                type_name=pgast.TypeName(name=('text',)),\n            )\n\n        fcall = pgast.FuncCall(\n            name=astutils.edgedb_func('_alter_current_database_set', ctx=ctx),\n            args=[pgast.StringConstant(val=op.backend_setting), val],\n        )\n\n        result = output.wrap_script_stmt(\n            pgast.SelectStmt(target_list=[pgast.ResTarget(val=fcall)]),\n            suppress_all_output=True,\n            env=ctx.env,\n        )\n\n    elif op.scope is qltypes.ConfigScope.SESSION and op.backend_setting:\n        if not isinstance(val, pgast.StringConstant):\n            val = pgast.TypeCast(\n                arg=val,\n                type_name=pgast.TypeName(name=('text',)),\n            )\n\n        fcall = pgast.FuncCall(\n            name=('pg_catalog', 'set_config'),\n            args=[\n                pgast.StringConstant(val=op.backend_setting),\n                val,\n                pgast.BooleanConstant(val=False),\n            ],\n        )\n\n        result = output.wrap_script_stmt(\n            pgast.SelectStmt(target_list=[pgast.ResTarget(val=fcall)]),\n            suppress_all_output=True,\n            env=ctx.env,\n        )\n\n    elif op.scope is qltypes.ConfigScope.INSTANCE:\n        result_row = pgast.RowExpr(\n            args=[\n                pgast.StringConstant(val='SET'),\n                pgast.StringConstant(val=str(op.scope)),\n                pgast.StringConstant(val=op.name),\n                val,\n            ]\n        )\n\n        result = pgast.FuncCall(\n            name=('jsonb_build_array',),\n            args=result_row.args,\n            null_safe=True,\n            ser_safe=True,\n        )\n\n        result = pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    val=result,\n                ),\n            ],\n        )\n    elif op.scope is qltypes.ConfigScope.SESSION:\n        result = pgast.InsertStmt(\n            relation=pgast.RelRangeVar(\n                relation=pgast.Relation(\n                    name='_edgecon_state',\n                ),\n            ),\n            select_stmt=pgast.SelectStmt(\n                values=[\n                    pgast.ImplicitRowExpr(\n                        args=[\n                            pgast.StringConstant(\n                                val=op.name,\n                            ),\n                            val,\n                            pgast.StringConstant(\n                                val='C',\n                            ),\n                        ]\n                    )\n                ]\n            ),\n            cols=[\n                pgast.InsertTarget(name='name'),\n                pgast.InsertTarget(name='value'),\n                pgast.InsertTarget(name='type'),\n            ],\n            on_conflict=pgast.OnConflictClause(\n                action=pgast.OnConflictAction.DO_UPDATE,\n                target=pgast.OnConflictTarget(\n                    index_elems=[\n                        pgast.IndexElem(expr=pgast.ColumnRef(name=['name'])),\n                        pgast.IndexElem(expr=pgast.ColumnRef(name=['type'])),\n                    ],\n                ),\n                update_list=[\n                    pgast.MultiAssignRef(\n                        columns=['value'],\n                        source=pgast.RowExpr(\n                            args=[\n                                val,\n                            ],\n                        ),\n                    ),\n                ],\n            ),\n        )\n\n    elif op.scope is qltypes.ConfigScope.GLOBAL:\n        result_row = pgast.RowExpr(\n            args=[\n                pgast.StringConstant(val='SET'),\n                pgast.StringConstant(val=str(op.scope)),\n                pgast.StringConstant(val=op.name),\n                val,\n            ]\n        )\n\n        build_array = pgast.FuncCall(\n            name=('jsonb_build_array',),\n            args=result_row.args,\n            null_safe=True,\n            ser_safe=True,\n        )\n\n        result = pgast.SelectStmt(\n            target_list=[pgast.ResTarget(val=build_array)],\n        )\n\n    elif op.scope is qltypes.ConfigScope.DATABASE:\n        result = pgast.InsertStmt(\n            relation=pgast.RelRangeVar(\n                relation=pgast.Relation(\n                    name='_db_config',\n                    schemaname='edgedb',\n                ),\n            ),\n            select_stmt=pgast.SelectStmt(\n                values=[\n                    pgast.ImplicitRowExpr(\n                        args=[\n                            pgast.StringConstant(\n                                val=op.name,\n                            ),\n                            val,\n                        ]\n                    )\n                ]\n            ),\n            cols=[\n                pgast.InsertTarget(name='name'),\n                pgast.InsertTarget(name='value'),\n            ],\n            on_conflict=pgast.OnConflictClause(\n                action=pgast.OnConflictAction.DO_UPDATE,\n                target=pgast.OnConflictTarget(\n                    index_elems=[\n                        pgast.IndexElem(expr=pgast.ColumnRef(name=['name'])),\n                    ],\n                ),\n                update_list=[\n                    pgast.MultiAssignRef(\n                        columns=['value'],\n                        source=pgast.RowExpr(\n                            args=[\n                                val,\n                            ],\n                        ),\n                    ),\n                ],\n            ),\n        )\n    else:\n        raise AssertionError(f'unexpected configuration scope: {op.scope}')\n\n    return result\n\n\n@dispatch.compile.register\ndef compile_ConfigReset(\n    op: irast.ConfigReset,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.BaseExpr:\n\n    stmt: pgast.BaseExpr\n\n    if op.scope is qltypes.ConfigScope.INSTANCE and op.backend_setting:\n        stmt = pgast.AlterSystem(\n            name=op.backend_setting,\n            value=None,\n        )\n\n    elif op.scope is qltypes.ConfigScope.DATABASE and op.backend_setting:\n        fcall = pgast.FuncCall(\n            name=astutils.edgedb_func('_alter_current_database_set', ctx=ctx),\n            args=[\n                pgast.StringConstant(val=op.backend_setting),\n                pgast.NullConstant(),\n            ],\n        )\n\n        stmt = output.wrap_script_stmt(\n            pgast.SelectStmt(target_list=[pgast.ResTarget(val=fcall)]),\n            suppress_all_output=True,\n            env=ctx.env,\n        )\n\n    elif op.scope is qltypes.ConfigScope.SESSION and op.backend_setting:\n        fcall = pgast.FuncCall(\n            name=('pg_catalog', 'set_config'),\n            args=[\n                pgast.StringConstant(val=op.backend_setting),\n                pgast.NullConstant(),\n                pgast.BooleanConstant(val=False),\n            ],\n        )\n\n        stmt = output.wrap_script_stmt(\n            pgast.SelectStmt(target_list=[pgast.ResTarget(val=fcall)]),\n            suppress_all_output=True,\n            env=ctx.env,\n        )\n\n    elif op.scope is qltypes.ConfigScope.INSTANCE:\n\n        if op.selector is None:\n            # Scalar reset\n            result_row = pgast.RowExpr(\n                args=[\n                    pgast.StringConstant(val='RESET'),\n                    pgast.StringConstant(val=str(op.scope)),\n                    pgast.StringConstant(val=op.name),\n                    pgast.NullConstant(),\n                ]\n            )\n\n            rvar = None\n        else:\n            with context.output_format(ctx, context.OutputFormat.JSONB):\n                selector = dispatch.compile(op.selector, ctx=ctx)\n\n            assert isinstance(selector, pgast.SelectStmt), \\\n                \"expected ast.SelectStmt\"\n            target = selector.target_list[0]\n            if not target.name:\n                target = selector.target_list[0] = pgast.ResTarget(\n                    name=ctx.env.aliases.get('res'),\n                    val=target.val,\n                )\n                assert target.name is not None\n\n            rvar = relctx.rvar_for_rel(selector, ctx=ctx)\n\n            result_row = pgast.RowExpr(\n                args=[\n                    pgast.StringConstant(val='REM'),\n                    pgast.StringConstant(val=str(op.scope)),\n                    pgast.StringConstant(val=op.name),\n                    astutils.get_column(rvar, target.name),\n                ]\n            )\n\n        result = pgast.FuncCall(\n            name=('jsonb_build_array',),\n            args=result_row.args,\n            null_safe=True,\n            ser_safe=True,\n        )\n\n        stmt = pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    val=result,\n                ),\n            ],\n        )\n\n        if rvar is not None:\n            stmt.from_clause = [rvar]\n\n    elif op.scope is qltypes.ConfigScope.DATABASE and op.selector is None:\n        stmt = pgast.DeleteStmt(\n            relation=pgast.RelRangeVar(\n                relation=pgast.Relation(\n                    name='_db_config',\n                    schemaname='edgedb',\n                ),\n            ),\n\n            where_clause=astutils.new_binop(\n                lexpr=pgast.ColumnRef(name=['name']),\n                rexpr=pgast.StringConstant(val=op.name),\n                op='=',\n            ),\n        )\n\n    elif op.scope is qltypes.ConfigScope.DATABASE and op.selector is not None:\n        # For FILTERed RESET on the database, we have to do a decent\n        # amount of work to actually delete the RESET objects from the\n        # json config blogs.\n        #\n        # This is because the server isn't set up to write back just\n        # the changed parts of the config based on interpreting the output,\n        # so instead we do all the work here.\n        with context.output_format(ctx, context.OutputFormat.JSONB):\n            selector = dispatch.compile(op.selector, ctx=ctx)\n\n        assert isinstance(selector, pgast.SelectStmt), \\\n            \"expected ast.SelectStmt\"\n        target = selector.target_list[0]\n        if not target.name:\n            target = selector.target_list[0] = pgast.ResTarget(\n                name=ctx.env.aliases.get('res'),\n                val=target.val,\n            )\n            assert target.name is not None\n\n        rvar = relctx.rvar_for_rel(selector, ctx=ctx)\n\n        sel_expr = op.selector.expr\n        assert isinstance(sel_expr, irast.SelectStmt)\n        sel_expr = sel_expr.result.expr\n        assert isinstance(sel_expr, irast.SelectStmt)\n\n        # Grab all the non-link properties of the object as keys. We\n        # could just do the exclusive ones, but this works too and we\n        # have the information at hand.\n        # XXX: Do we need to consider _tname also?\n        keys = [\n            el.expr.ptrref.shortname.name\n            for el, op in sel_expr.result.shape\n            if op == qlast.ShapeOp.ASSIGN\n            and not irtyputils.is_object(el.expr.ptrref.out_target)\n        ]\n\n        newval = pgast.SelectStmt(\n            target_list=[pgast.ResTarget(\n                val=pgast.FuncCall(\n                    name=('jsonb_agg',),\n                    args=[pgast.ColumnRef(name=['ov', 'value'])],\n                ),\n            )],\n            from_clause=[\n                pgast.RangeFunction(\n                    lateral=True,\n                    alias=pgast.Alias(aliasname='ov'),\n                    functions=[pgast.FuncCall(\n                        name=('jsonb_array_elements',),\n                        args=[pgast.ColumnRef(name=['value'])],\n                    )],\n                ),\n            ],\n            where_clause=(\n                pgast.SubLink(\n                    operator=\"NOT EXISTS\",\n                    expr=pgast.SelectStmt(\n                        from_clause=[rvar],\n                        where_clause=astutils.extend_binop(\n                            None,\n                            *[\n                                pgast.Expr(\n                                    name='=',\n                                    lexpr=pgast.Expr(\n                                        name='->',\n                                        lexpr=pgast.ColumnRef(name=[\n                                            rvar.alias.aliasname,\n                                            target.name,\n                                        ]),\n                                        rexpr=pgast.StringConstant(val=key),\n                                    ),\n                                    rexpr=pgast.CoalesceExpr(\n                                        args=[\n                                            pgast.Expr(\n                                                name='->',\n                                                lexpr=pgast.ColumnRef(name=[\n                                                    'ov', 'value'\n                                                ]),\n                                                rexpr=pgast.StringConstant(\n                                                    val=key\n                                                ),\n                                            ),\n                                            pgast.TypeCast(\n                                                arg=pgast.StringConstant(\n                                                    val='null'),\n                                                type_name=pgast.TypeName(\n                                                    name=('jsonb',),\n                                                ),\n                                            ),\n                                        ]\n                                    )\n                                )\n                                for key in keys\n                            ],\n                        )\n                    )\n                )\n            ),\n        )\n\n        stmt = pgast.UpdateStmt(\n            targets=[pgast.UpdateTarget(\n                name='value',\n                val=newval,\n            )],\n            relation=pgast.RelRangeVar(\n                relation=pgast.Relation(\n                    name='_db_config',\n                    schemaname='edgedb',\n                ),\n            ),\n            where_clause=astutils.new_binop(\n                lexpr=pgast.ColumnRef(name=['name']),\n                rexpr=pgast.StringConstant(val=op.name),\n                op='=',\n            ),\n            returning_list=[pgast.ResTarget(\n                val=pgast.CaseExpr(\n                    args=[\n                        pgast.CaseWhen(\n                            expr=pgast.NullTest(\n                                arg=pgast.ColumnRef(name=['value'])\n                            ),\n                            result=pgast.FuncCall(\n                                name=('jsonb_build_array',),\n                                args=[\n                                    pgast.StringConstant(val='RESET'),\n                                    pgast.StringConstant(val=str(op.scope)),\n                                    pgast.StringConstant(val=op.name),\n                                    pgast.NullConstant(),\n                                ],\n                            )\n                        ),\n                    ],\n                    defresult=pgast.FuncCall(\n                        name=('jsonb_build_array',),\n                        args=[\n                            pgast.StringConstant(val='SET'),\n                            pgast.StringConstant(val=str(op.scope)),\n                            pgast.StringConstant(val=op.name),\n                            pgast.ColumnRef(name=['value']),\n                        ],\n                    )\n                )\n            )],\n        )\n\n    elif op.scope is qltypes.ConfigScope.SESSION:\n        stmt = pgast.DeleteStmt(\n            relation=pgast.RelRangeVar(\n                relation=pgast.Relation(\n                    name='_edgecon_state',\n                ),\n            ),\n\n            where_clause=astutils.new_binop(\n                lexpr=astutils.new_binop(\n                    lexpr=pgast.ColumnRef(name=['name']),\n                    rexpr=pgast.StringConstant(val=op.name),\n                    op='=',\n                ),\n                rexpr=astutils.new_binop(\n                    lexpr=pgast.ColumnRef(name=['type']),\n                    rexpr=pgast.StringConstant(val='C'),\n                    op='=',\n                ),\n                op='AND',\n            )\n        )\n    elif op.scope is qltypes.ConfigScope.GLOBAL:\n        stmt = pgast.SelectStmt(\n            where_clause=pgast.BooleanConstant(val=False)\n        )\n    else:\n        raise AssertionError(f'unexpected configuration scope: {op.scope}')\n\n    return stmt\n\n\n@dispatch.compile.register\ndef compile_ConfigInsert(\n    stmt: irast.ConfigInsert, *, ctx: context.CompilerContextLevel\n) -> pgast.BaseExpr:\n\n    with ctx.new() as subctx:\n        with context.output_format(ctx, context.OutputFormat.JSONB):\n            subctx.expr_exposed = True\n            rewritten = _rewrite_config_insert(stmt.expr, ctx=subctx)\n            dispatch.compile(rewritten, ctx=subctx)\n\n            return pathctx.get_path_serialized_output(\n                ctx.rel, stmt.expr.path_id, env=ctx.env)\n\n\ndef _rewrite_config_insert(\n    ir_set: irast.Set, *, ctx: context.CompilerContextLevel\n) -> irast.Set:\n\n    overwrite_query = pgast.SelectStmt()\n    id_expr = pgast.FuncCall(\n        name=astutils.edgedb_func('uuid_generate_v1mc', ctx=ctx),\n        args=[],\n    )\n    pathctx.put_path_identity_var(\n        overwrite_query, ir_set.path_id, id_expr, force=True\n    )\n    pathctx.put_path_value_var(\n        overwrite_query, ir_set.path_id, id_expr, force=True\n    )\n    pathctx.put_path_source_rvar(\n        overwrite_query,\n        ir_set.path_id,\n        relctx.rvar_for_rel(pgast.NullRelation(), ctx=ctx),\n    )\n\n    relctx.add_type_rel_overlay(\n        ir_set.typeref,\n        context.OverlayOp.REPLACE,\n        overwrite_query,\n        path_id=ir_set.path_id,\n        ctx=ctx,\n    )\n\n    # Config objects have derived computed ids,\n    # so the autogenerated id must not be returned.\n    ir_set.shape = tuple(filter(\n        lambda el: (\n            el[0].expr.ptrref.shortname.name != 'id'\n        ),\n        ir_set.shape,\n    ))\n\n    for el, _ in ir_set.shape:\n        if isinstance(el.expr.expr, irast.InsertStmt):\n            el.shape = tuple(filter(\n                lambda e: (\n                    e[0].expr.ptrref.shortname.name != 'id'\n                ),\n                el.shape,\n            ))\n\n            result = _rewrite_config_insert(el.expr.expr.subject, ctx=ctx)\n            el.expr.expr = irast.SelectStmt(\n                result=result,\n                parent_stmt=el.expr.expr.parent_stmt,\n            )\n\n    return ir_set\n\n\ndef _compile_config_value(\n    op: irast.ConfigSet,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.BaseExpr:\n    val: pgast.BaseExpr\n\n    expr = op.backend_expr or op.expr\n\n    with ctx.new() as subctx:\n        if op.backend_setting or op.scope == qltypes.ConfigScope.GLOBAL:\n            output_format = context.OutputFormat.NATIVE\n        else:\n            output_format = context.OutputFormat.JSONB\n\n        with context.output_format(ctx, output_format):\n            if isinstance(expr.expr, irast.EmptySet):\n                # Special handling for empty sets, because we want a\n                # singleton representation of the value and not an empty rel\n                # in this context.\n                if op.cardinality is qltypes.SchemaCardinality.One:\n                    val = pgast.NullConstant()\n                elif subctx.env.output_format is context.OutputFormat.JSONB:\n                    val = pgast.TypeCast(\n                        arg=pgast.StringConstant(val='[]'),\n                        type_name=pgast.TypeName(\n                            name=('jsonb',),\n                        ),\n                    )\n                else:\n                    val = pgast.TypeCast(\n                        arg=pgast.ArrayExpr(elements=[]),\n                        type_name=pgast.TypeName(\n                            name=('text[]',),\n                        ),\n                    )\n            else:\n                val = dispatch.compile(expr, ctx=subctx)\n                assert isinstance(val, pgast.SelectStmt), \"expected SelectStmt\"\n\n                pathctx.get_path_serialized_output(\n                    val, expr.path_id, env=ctx.env)\n\n                if op.cardinality is qltypes.SchemaCardinality.Many:\n                    val = output.aggregate_json_output(\n                        val, expr, env=ctx.env)\n\n    # For globals, we need to output the binary encoding so that we\n    # can just hand it back to the server. We abuse `record_send` to\n    # act as a generic `_send` function\n    if op.scope is qltypes.ConfigScope.GLOBAL:\n        val = pgast.FuncCall(\n            name=('substring',),\n            args=[\n                pgast.FuncCall(\n                    name=('record_send',),\n                    args=[pgast.RowExpr(args=[val])],\n                ),\n                # The first 8 bytes are header, then 4 bytes are the length\n                # of our element, then the encoding of our actual element.\n                # We include the length so we can distinguish NULL (len=-1)\n                # from empty strings and the like (len=0).\n                pgast.NumericConstant(val=\"9\"),\n            ],\n        )\n        cast_name = s_casts.get_cast_fullname_from_names(\n            sn.QualName('std', 'bytes'), sn.QualName('std', 'json'))\n        val = pgast.FuncCall(\n            name=common.get_cast_backend_name(\n                cast_name,\n                aspect='function',\n                versioned=ctx.env.versioned_stdlib,\n            ),\n            args=[val],\n        )\n\n    if op.backend_setting and op.scope is qltypes.ConfigScope.INSTANCE:\n        assert isinstance(val, pgast.SelectStmt) and len(val.target_list) == 1\n        val = val.target_list[0].val\n        if isinstance(val, pgast.TypeCast):\n            val = val.arg\n        if not isinstance(val, pgast.BaseConstant):\n            raise AssertionError('value is not a constant in ConfigSet')\n\n    return val\n\n\ndef top_output_as_config_op(\n    ir_set: irast.Set, stmt: pgast.SelectStmt, *, env: context.Environment\n) -> pgast.Query:\n\n    assert isinstance(ir_set.expr, irast.ConfigCommand)\n    op = ir_set.expr\n\n    alias = env.aliases.get('cfg')\n    cte = pgast.CommonTableExpr(query=stmt, name=alias)\n    ctes = [cte]\n\n    subrvar = relctx.rvar_for_rel(cte, env=env)\n\n    stmt_res = stmt.target_list[0]\n\n    if stmt_res.name is None:\n        stmt_res = stmt.target_list[0] = pgast.ResTarget(\n            name=env.aliases.get('v'),\n            val=stmt_res.val,\n        )\n        assert stmt_res.name is not None\n    val = pgast.ColumnRef(name=[stmt_res.name])\n\n    # FIXME: Can the duplication with other db cases be reduced?\n    if op.scope is qltypes.ConfigScope.DATABASE:\n        sval = pgast.SelectStmt(\n            target_list=[pgast.ResTarget(val=val)], from_clause=[subrvar])\n        ins_val = pgast.FuncCall(\n            name=('jsonb_build_array',),\n            args=[sval],\n            null_safe=True,\n            ser_safe=True,\n        )\n\n        old_val = pgast.CoalesceExpr(\n            args=[\n                pgast.ColumnRef(name=['edgedb', '_db_config', 'value']),\n                pgast.TypeCast(\n                    arg=pgast.StringConstant(val='[]'),\n                    type_name=pgast.TypeName(\n                        name=('jsonb',),\n                    ),\n                ),\n            ],\n        )\n        upd_val = pgast.Expr(\n            name='||',\n            lexpr=old_val,\n            rexpr=ins_val,\n        )\n\n        ins = pgast.InsertStmt(\n            relation=pgast.RelRangeVar(\n                relation=pgast.Relation(\n                    name='_db_config',\n                    schemaname='edgedb',\n                ),\n            ),\n            select_stmt=pgast.SelectStmt(\n                values=[\n                    pgast.ImplicitRowExpr(\n                        args=[\n                            pgast.StringConstant(\n                                val=op.name,\n                            ),\n                            ins_val,\n                        ]\n                    )\n                ],\n            ),\n            cols=[\n                pgast.InsertTarget(name='name'),\n                pgast.InsertTarget(name='value'),\n            ],\n            on_conflict=pgast.OnConflictClause(\n                action=pgast.OnConflictAction.DO_UPDATE,\n                target=pgast.OnConflictTarget(\n                    index_elems=[\n                        pgast.IndexElem(expr=pgast.ColumnRef(name=['name'])),\n                    ],\n                ),\n                update_list=[\n                    pgast.MultiAssignRef(\n                        columns=['value'],\n                        source=pgast.RowExpr(\n                            args=[\n                                upd_val,\n                            ],\n                        ),\n                    ),\n                ],\n            ),\n            returning_list=[\n                pgast.ResTarget(\n                    val=pgast.ColumnRef(name=[pgast.Star()])\n                )\n            ],\n        )\n        ctes.append(\n            pgast.CommonTableExpr(query=ins, name=env.aliases.get('ins'))\n        )\n\n        subrvar = relctx.rvar_for_rel(ctes[-1], env=env)\n        val = pgast.ColumnRef(name=['value'])\n\n    if ir_set.expr.scope in (\n        qltypes.ConfigScope.INSTANCE, qltypes.ConfigScope.DATABASE\n    ):\n        # For database config, we do SET, and we return the entire new\n        # value, in order to avoid race conditions in duplicate\n        # checking.\n        command = (\n            'SET' if ir_set.expr.scope is qltypes.ConfigScope.DATABASE\n            else 'ADD'\n        )\n        result_row = pgast.RowExpr(\n            args=[\n                pgast.StringConstant(val=command),\n                pgast.StringConstant(val=str(ir_set.expr.scope)),\n                pgast.StringConstant(val=ir_set.expr.name),\n                val,\n            ]\n        )\n\n        array = pgast.FuncCall(\n            name=('jsonb_build_array',),\n            args=result_row.args,\n            null_safe=True,\n            ser_safe=True,\n        )\n\n        result = pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    val=array,\n                ),\n            ],\n            from_clause=[\n                subrvar,\n            ],\n            ctes=ctes + (stmt.ctes or []),\n        )\n\n        stmt.ctes = []\n\n        return result\n    else:\n        raise errors.InternalServerError(\n            f'CONFIGURE {ir_set.expr.scope} INSERT is not supported')\n"
  },
  {
    "path": "edb/pgsql/compiler/context.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"IR compiler context.\"\"\"\n\nfrom __future__ import annotations\nfrom typing import (\n    Callable,\n    Optional,\n    Mapping,\n    ChainMap,\n    Generator,\n    Sequence,\n    TYPE_CHECKING,\n)\n\nimport collections\nimport contextlib\nimport dataclasses\nimport enum\nimport uuid\n\nimport immutables as immu\n\nfrom edb.common import compiler\nfrom edb.common import enum as s_enum\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import params as pgparams\n\nfrom . import aliases as pg_aliases\n\nif TYPE_CHECKING:\n    from edb.ir import ast as irast\n    from . import enums as pgce\n\n\nclass ContextSwitchMode(enum.Enum):\n    TRANSPARENT = enum.auto()\n    SUBREL = enum.auto()\n    NEWREL = enum.auto()\n    SUBSTMT = enum.auto()\n    NEWSCOPE = enum.auto()\n\n\nclass ShapeFormat(enum.Enum):\n    SERIALIZED = enum.auto()\n    FLAT = enum.auto()\n\n\nclass OutputFormat(enum.Enum):\n    #: Result data output in PostgreSQL format.\n    NATIVE = enum.auto()\n    #: Result data output as a single JSON string.\n    JSON = enum.auto()\n    #: Result data output as a single PostgreSQL JSONB type value.\n    JSONB = enum.auto()\n    #: Result data output as a JSON string for each element in returned set.\n    JSON_ELEMENTS = enum.auto()\n    #: None mode: query result not returned, cardinality of result set\n    #: is returned instead.\n    NONE = enum.auto()\n    #: Like NATIVE, but objects without an explicit shape are serialized\n    #: as UUIDs.\n    NATIVE_INTERNAL = enum.auto()\n\n\nNO_STMT = pgast.SelectStmt()\n\n\nclass OverlayOp(s_enum.StrEnum):\n    UNION = 'union'\n    REPLACE = 'replace'\n    FILTER = 'filter'\n    EXCEPT = 'except'\n\n\nOverlayEntry = tuple[\n    OverlayOp,\n    pgast.BaseRelation | pgast.CommonTableExpr,\n    'irast.PathId',\n]\n\n\n@dataclasses.dataclass(kw_only=True)\nclass RelOverlays:\n    \"\"\"Container for relation overlays.\n\n    These track \"overlays\" that can be registered for different types,\n    in the context of DML.\n\n    Consider the query:\n      with X := (\n        insert Person {\n          name := \"Sully\",\n          notes := assert_distinct({\n            (insert Note {name := \"1\"}),\n            (select Note filter .name = \"2\"),\n          }),\n        }),\n      select X { name, notes: {name} };\n\n    When we go to select X, we find the source of that set without any\n    trouble (it's the result of the actual insert statement, more or\n    less; in any case, it's in a CTE that we then include).\n\n    Handling the notes are trickier, though:\n      * The links aren't in the link table yet, but only in a CTE.\n        (In similar update cases, with things like +=, they might be mixed\n        between both.)\n      * Some of the actual Note objects aren't in the table yet, just an insert\n        CTE. But some *are*, so we need to union them.\n\n    We solve these problems using overlays:\n      * Whenever we do DML (or reference WITH-bound DML),\n        we register overlays describing the changes done\n        to *all of the enclosing DML*. So here, the Note insert's overlays\n        get registered both for the Note insert and for the Person insert.\n      * When we try to compile a root set or pointer, we see if it is connected\n        to a DML statement, and if so we apply the overlays.\n\n    The overlay itself is simply a sequence of operations on relations\n    and CTEs that mix in the new data. In the obvious insert cases,\n    these consist of unioning the new data in.\n\n    This system works decently well but is also a little broken: I\n    think that both the \"all of the enclosing DML\" and the \"see if it\n    is connected to a DML statement\" have dangers; see Issue #3030.\n\n    In relctx, see range_for_material_objtype, range_for_ptrref, and\n    range_from_queryset (which those two call) for details on how\n    overlays are applied.\n    Overlays are added to with relctx.add_type_rel_overlay\n    and relctx.add_ptr_rel_overlay.\n\n\n    ===== NOTE ON MUTABILITY:\n    In typical use, the overlays are mutable: nested DML adds overlays\n    that are then consumed by code in enclosing contexts.\n\n    In some places, however, we need to temporarily customize the\n    overlay environment (during policy and trigger compilation, for\n    example).\n\n    The original version of overlays were implemented as a dict of\n    dicts of lists. Doing temporary customizations required doing\n    at least some copying. Doing a full deep copy always felt excessive\n    but doing anything short of that left me constantly terrified.\n\n    So instead we represent the overlays as a mutable object that\n    contains immutable maps. When we add overlays, we update the maps\n    and then reassign their values.\n\n    When we want to do a temporary adjustment, we can cheaply make a\n    fresh RelOverlays object and then modify that without touching the\n    original.\n    \"\"\"\n\n    #: Relations used to \"overlay\" the main table for\n    #: the type.  Mostly used with DML statements.\n    type: immu.Map[\n        Optional[irast.MutatingLikeStmt],\n        immu.Map[\n            uuid.UUID,\n            tuple[OverlayEntry, ...],\n        ],\n    ] = immu.Map()\n\n    #: Relations used to \"overlay\" the main table for\n    #: the pointer.  Mostly used with DML statements.\n    ptr: immu.Map[\n        Optional[irast.MutatingLikeStmt],\n        immu.Map[\n            tuple[uuid.UUID, str],\n            tuple[\n                tuple[\n                    OverlayOp,\n                    pgast.BaseRelation | pgast.CommonTableExpr,\n                    irast.PathId,\n                ], ...\n            ],\n        ],\n    ] = immu.Map()\n\n    def copy(self) -> RelOverlays:\n        return RelOverlays(type=self.type, ptr=self.ptr)\n\n\nclass CompilerContextLevel(compiler.ContextLevel):\n    #: static compilation environment\n    env: Environment\n\n    #: mapping of named args to position\n    argmap: dict[str, pgast.Param]\n\n    #: whether compiling in singleton expression mode\n    singleton_mode: bool\n\n    #: whether compiling a trigger\n    trigger_mode: bool\n\n    #: the top-level SQL statement\n    toplevel_stmt: pgast.Query\n\n    #: Record of DML CTEs generated for the corresponding IR DML.\n    #: CTEs generated for DML-containing FOR statements are keyed\n    #: by their iterator set.\n    dml_stmts: dict[irast.MutatingStmt | irast.Set, pgast.CommonTableExpr]\n\n    #: Inline DML functions may require additional CTEs.\n    #: Record such CTEs as well as the path used by their iterators.\n    #: This ensures CTEs are created only once, and that the correct\n    #: iterator bonds are applied.\n    inline_dml_ctes: dict[\n        irast.PathId,\n        tuple[irast.PathId, pgast.CommonTableExpr],\n    ]\n\n    #: SQL statement corresponding to the IR statement\n    #: currently being compiled.\n    stmt: pgast.SelectStmt\n\n    #: Current SQL subquery\n    rel: pgast.SelectStmt\n\n    #: SQL query hierarchy\n    rel_hierarchy: dict[pgast.Query, pgast.Query]\n\n    #: CTEs representing decoded parameters\n    param_ctes: dict[str, pgast.CommonTableExpr]\n\n    #: CTEs representing pointers and their inherited pointers\n    ptr_inheritance_ctes: dict[uuid.UUID, pgast.CommonTableExpr]\n\n    #: CTEs representing types, when rewritten based on access policy\n    type_rewrite_ctes: dict[FullRewriteKey, pgast.CommonTableExpr]\n\n    #: A set of type CTEs currently being generated\n    pending_type_rewrite_ctes: set[RewriteKey]\n\n    #: CTEs representing types and their inherited types\n    type_inheritance_ctes: dict[uuid.UUID, pgast.CommonTableExpr]\n\n    # Type and type inheriance CTEs in creation order. This ensures type CTEs\n    # referring to other CTEs are in the correct order.\n    ordered_type_ctes: list[pgast.CommonTableExpr]\n\n    #: The logical parent of the current query in the\n    #: query hierarchy\n    parent_rel: Optional[pgast.Query]\n\n    #: Query to become current in the next SUBSTMT switch.\n    pending_query: Optional[pgast.SelectStmt]\n\n    #: Sets currently being materialized\n    materializing: frozenset[irast.Stmt]\n\n    #: Whether the expression currently being processed is\n    #: directly exposed to the output of the statement.\n    expr_exposed: Optional[bool]\n\n    #: A hack that indicates a tuple element that should be treated as\n    #: exposed. This enables us to treat 'bar' in (foo, bar).1 as exposed,\n    #: which eta-expansion and some casts rely on.\n    expr_exposed_tuple_cheat: Optional[irast.TupleElement]\n\n    #: Expression to use to force SQL expression volatility in this context\n    #: (Delayed with a lambda to avoid inserting it when not used.)\n    volatility_ref: tuple[\n        Callable[[pgast.SelectStmt, CompilerContextLevel],\n                 Optional[pgast.BaseExpr]], ...]\n\n    # Current path_id we are INSERTing, so that we can avoid creating\n    # a bogus volatility ref to it...\n    current_insert_path_id: Optional[irast.PathId]\n\n    #: Paths, for which semi-join is banned in this context.\n    disable_semi_join: frozenset[irast.PathId]\n\n    #: Paths, which need to be explicitly wrapped into SQL\n    #: optionality scaffolding.\n    force_optional: frozenset[irast.PathId]\n\n    #: Paths that can be ignored when they appear as the source of a\n    # computable. This is key to optimizing away free object sources in\n    # group by aggregates.\n    skippable_sources: frozenset[irast.PathId]\n\n    #: Specifies that references to a specific Set must be narrowed\n    #: by only selecting instances of type specified by the mapping value.\n    intersection_narrowing: dict[irast.Set, irast.Set]\n\n    #: Which SQL query holds the SQL scope for the given PathId\n    path_scope: ChainMap[irast.PathId, Optional[pgast.SelectStmt]]\n\n    #: Relevant IR scope for this context.\n    scope_tree: irast.ScopeTreeNode\n\n    #: A stack of dml statements currently being compiled. Used for\n    #: figuring out what to record in type_rel_overlays.\n    dml_stmt_stack: list[irast.MutatingLikeStmt]\n\n    #: Relations used to \"overlay\" the main table for\n    #: the type.  Mostly used with DML statements.\n    rel_overlays: RelOverlays\n\n    #: Mapping from path ids to \"external\" rels given by a particular relation\n    external_rels: Mapping[\n        irast.PathId,\n        tuple[\n            pgast.BaseRelation | pgast.CommonTableExpr,\n            tuple[pgce.PathAspect, ...]\n        ]\n    ]\n\n    #: The CTE and some metadata of any enclosing iterator-like\n    #: construct (which includes iterators, insert/update, and INSERT\n    #: ELSE select clauses) currently being compiled.\n    enclosing_cte_iterator: Optional[pgast.IteratorCTE]\n\n    #: Sets to force shape compilation on, because the values are\n    #: needed by DML.\n    shapes_needed_by_dml: set[irast.Set]\n\n    def __init__(\n        self,\n        prevlevel: Optional[CompilerContextLevel],\n        mode: ContextSwitchMode,\n        *,\n        env: Optional[Environment] = None,\n        scope_tree: Optional[irast.ScopeTreeNode] = None,\n    ) -> None:\n        if prevlevel is None:\n            assert env is not None\n            assert scope_tree is not None\n\n            self.env = env\n            self.argmap = collections.OrderedDict()\n\n            self.singleton_mode = False\n\n            self.toplevel_stmt = NO_STMT\n            self.stmt = NO_STMT\n            self.rel = NO_STMT\n            self.rel_hierarchy = {}\n            self.param_ctes = {}\n            self.ptr_inheritance_ctes = {}\n            self.type_rewrite_ctes = {}\n            self.pending_type_rewrite_ctes = set()\n            self.type_inheritance_ctes = {}\n            self.ordered_type_ctes = []\n            self.dml_stmts = {}\n            self.inline_dml_ctes = {}\n            self.parent_rel = None\n            self.pending_query = None\n            self.materializing = frozenset()\n\n            self.expr_exposed = None\n            self.expr_exposed_tuple_cheat = None\n            self.volatility_ref = ()\n            self.current_insert_path_id = None\n\n            self.disable_semi_join = frozenset()\n            self.force_optional = frozenset()\n            self.skippable_sources = frozenset()\n            self.intersection_narrowing = {}\n\n            self.path_scope = collections.ChainMap()\n            self.scope_tree = scope_tree\n            self.dml_stmt_stack = []\n            self.rel_overlays = RelOverlays()\n\n            self.external_rels = {}\n            self.enclosing_cte_iterator = None\n            self.shapes_needed_by_dml = set()\n\n            self.trigger_mode = False\n\n        else:\n            self.env = prevlevel.env\n            self.argmap = prevlevel.argmap\n\n            self.singleton_mode = prevlevel.singleton_mode\n\n            self.toplevel_stmt = prevlevel.toplevel_stmt\n            self.stmt = prevlevel.stmt\n            self.rel = prevlevel.rel\n            self.rel_hierarchy = prevlevel.rel_hierarchy\n            self.param_ctes = prevlevel.param_ctes\n            self.ptr_inheritance_ctes = prevlevel.ptr_inheritance_ctes\n            self.type_rewrite_ctes = prevlevel.type_rewrite_ctes\n            self.pending_type_rewrite_ctes = prevlevel.pending_type_rewrite_ctes\n            self.type_inheritance_ctes = prevlevel.type_inheritance_ctes\n            self.ordered_type_ctes = prevlevel.ordered_type_ctes\n            self.dml_stmts = prevlevel.dml_stmts\n            self.inline_dml_ctes = prevlevel.inline_dml_ctes\n            self.parent_rel = prevlevel.parent_rel\n            self.pending_query = prevlevel.pending_query\n            self.materializing = prevlevel.materializing\n\n            self.expr_exposed = prevlevel.expr_exposed\n            self.expr_exposed_tuple_cheat = prevlevel.expr_exposed_tuple_cheat\n            self.volatility_ref = prevlevel.volatility_ref\n            self.current_insert_path_id = prevlevel.current_insert_path_id\n\n            self.disable_semi_join = prevlevel.disable_semi_join\n            self.force_optional = prevlevel.force_optional\n            self.skippable_sources = prevlevel.skippable_sources\n            self.intersection_narrowing = prevlevel.intersection_narrowing\n\n            self.path_scope = prevlevel.path_scope\n            self.scope_tree = prevlevel.scope_tree\n            self.dml_stmt_stack = prevlevel.dml_stmt_stack\n            self.rel_overlays = prevlevel.rel_overlays\n            self.enclosing_cte_iterator = prevlevel.enclosing_cte_iterator\n            self.shapes_needed_by_dml = prevlevel.shapes_needed_by_dml\n            self.external_rels = prevlevel.external_rels\n\n            self.trigger_mode = prevlevel.trigger_mode\n\n            if mode is ContextSwitchMode.SUBSTMT:\n                if self.pending_query is not None:\n                    self.rel = self.pending_query\n                else:\n                    self.rel = pgast.SelectStmt()\n                    if prevlevel.parent_rel is not None:\n                        parent_rel = prevlevel.parent_rel\n                    else:\n                        parent_rel = prevlevel.rel\n                    self.rel_hierarchy[self.rel] = parent_rel\n\n                self.stmt = self.rel\n                self.pending_query = None\n                self.parent_rel = None\n\n            elif mode is ContextSwitchMode.SUBREL:\n                self.rel = pgast.SelectStmt()\n                if prevlevel.parent_rel is not None:\n                    parent_rel = prevlevel.parent_rel\n                else:\n                    parent_rel = prevlevel.rel\n                self.rel_hierarchy[self.rel] = parent_rel\n                self.pending_query = None\n                self.parent_rel = None\n\n            elif mode is ContextSwitchMode.NEWREL:\n                self.rel = pgast.SelectStmt()\n                self.pending_query = None\n                self.parent_rel = None\n                self.path_scope = collections.ChainMap()\n                self.rel_hierarchy = {}\n                self.scope_tree = prevlevel.scope_tree.root\n                self.volatility_ref = ()\n\n                self.disable_semi_join = frozenset()\n                self.force_optional = frozenset()\n                self.intersection_narrowing = {}\n                self.pending_type_rewrite_ctes = set(\n                    prevlevel.pending_type_rewrite_ctes\n                )\n\n            elif mode == ContextSwitchMode.NEWSCOPE:\n                self.path_scope = prevlevel.path_scope.new_child()\n\n    def get_current_dml_stmt(self) -> Optional[irast.MutatingLikeStmt]:\n        if len(self.dml_stmt_stack) == 0:\n            return None\n        return self.dml_stmt_stack[-1]\n\n    def subrel(\n        self,\n    ) -> compiler.CompilerContextManager[CompilerContextLevel]:\n        return self.new(ContextSwitchMode.SUBREL)\n\n    def newrel(\n        self,\n    ) -> compiler.CompilerContextManager[CompilerContextLevel]:\n        return self.new(ContextSwitchMode.NEWREL)\n\n    def substmt(\n        self,\n    ) -> compiler.CompilerContextManager[CompilerContextLevel]:\n        return self.new(ContextSwitchMode.SUBSTMT)\n\n    def newscope(\n        self,\n    ) -> compiler.CompilerContextManager[CompilerContextLevel]:\n        return self.new(ContextSwitchMode.NEWSCOPE)\n\n    def up_hierarchy(\n        self,\n        n: int, q: Optional[pgast.Query]=None\n    ) -> Optional[pgast.Query]:\n        # mostly intended as a debugging helper\n        q = q or self.rel\n        for _ in range(n):\n            if q:\n                q = self.rel_hierarchy.get(q)\n        return q\n\n\nclass CompilerContext(compiler.CompilerContext[CompilerContextLevel]):\n    ContextLevelClass = CompilerContextLevel\n    default_mode = ContextSwitchMode.TRANSPARENT\n\n\nRewriteKey = tuple[uuid.UUID, bool]\nFullRewriteKey = tuple[\n    uuid.UUID, bool, Optional[frozenset['irast.MutatingLikeStmt']]]\n\n\nclass Environment:\n    \"\"\"Static compilation environment.\"\"\"\n\n    aliases: pg_aliases.AliasGenerator\n    output_format: Optional[OutputFormat]\n    named_param_prefix: Optional[tuple[str, ...]]\n    ptrref_source_visibility: dict[irast.BasePointerRef, bool]\n    expected_cardinality_one: bool\n    ignore_object_shapes: bool\n    explicit_top_cast: Optional[irast.TypeRef]\n    singleton_mode: bool\n    query_params: list[irast.Param]\n    type_rewrites: dict[RewriteKey, irast.Set]\n    scope_tree_nodes: dict[int, irast.ScopeTreeNode]\n    external_rvars: Mapping[\n        tuple[irast.PathId, pgce.PathAspect], pgast.PathRangeVar\n    ]\n    materialized_views: dict[uuid.UUID, irast.Set]\n    backend_runtime_params: pgparams.BackendRuntimeParams\n    versioned_stdlib: bool\n    sql_dml_mode: bool\n\n    #: A list of CTEs that implement constraint validation at the\n    #: query level.\n    check_ctes: list[pgast.CommonTableExpr]\n\n    #: Map of binding path ids to DML used in the binding. I hope and\n    #: suspect that this will grow towards becoming a more general and\n    #: traditional symbol table as I rip out path factoring? Who knows\n    #: though.\n    binding_dml: dict[irast.PathId, Sequence[irast.MutatingLikeStmt]]\n\n    def __init__(\n        self,\n        *,\n        alias_generator: Optional[pg_aliases.AliasGenerator] = None,\n        output_format: Optional[OutputFormat],\n        named_param_prefix: Optional[tuple[str, ...]],\n        expected_cardinality_one: bool,\n        ignore_object_shapes: bool,\n        singleton_mode: bool,\n        is_explain: bool,\n        explicit_top_cast: Optional[irast.TypeRef],\n        query_params: list[irast.Param],\n        type_rewrites: dict[RewriteKey, irast.Set],\n        scope_tree_nodes: dict[int, irast.ScopeTreeNode],\n        external_rvars: Optional[\n            Mapping[tuple[irast.PathId, pgce.PathAspect], pgast.PathRangeVar]\n        ] = None,\n        backend_runtime_params: pgparams.BackendRuntimeParams,\n        # XXX: TRAMPOLINE: THIS IS WRONG\n        versioned_stdlib: bool = True,\n        sql_dml_mode: bool = False,\n    ) -> None:\n        self.aliases = alias_generator or pg_aliases.AliasGenerator()\n        self.output_format = output_format\n        self.named_param_prefix = named_param_prefix\n        self.ptrref_source_visibility = {}\n        self.expected_cardinality_one = expected_cardinality_one\n        self.ignore_object_shapes = ignore_object_shapes\n        self.singleton_mode = singleton_mode\n        self.is_explain = is_explain\n        self.explicit_top_cast = explicit_top_cast\n        self.query_params = query_params\n        self.type_rewrites = type_rewrites\n        self.scope_tree_nodes = scope_tree_nodes\n        self.external_rvars = external_rvars or {}\n        self.materialized_views = {}\n        self.check_ctes = []\n        self.backend_runtime_params = backend_runtime_params\n        self.versioned_stdlib = versioned_stdlib\n        self.sql_dml_mode = sql_dml_mode\n        self.binding_dml = {}\n\n\n# XXX: this context hack is necessary until pathctx is converted\n#      to use context levels instead of using env directly.\n@contextlib.contextmanager\ndef output_format(\n    ctx: CompilerContextLevel,\n    output_format: OutputFormat,\n) -> Generator[None, None, None]:\n    original_output_format = ctx.env.output_format\n    original_ignore_object_shapes = ctx.env.ignore_object_shapes\n    ctx.env.output_format = output_format\n    ctx.env.ignore_object_shapes = False\n    try:\n        yield\n    finally:\n        ctx.env.output_format = original_output_format\n        ctx.env.ignore_object_shapes = original_ignore_object_shapes\n"
  },
  {
    "path": "edb/pgsql/compiler/dispatch.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport functools\n\nfrom edb.ir import ast as irast\n\nfrom edb.pgsql import ast as pgast\n\nfrom . import context\n\n\n@functools.singledispatch\ndef compile(\n    ir: irast.Base, *, ctx: context.CompilerContextLevel\n) -> pgast.BaseExpr:\n    raise NotImplementedError(f'no IR compiler handler for {ir.__class__}')\n\n\n@functools.singledispatch\ndef visit(ir: irast.Base, *, ctx: context.CompilerContextLevel) -> None:\n    \"\"\"A compilation version that does not pull the value eagerly.\"\"\"\n    compile(ir, ctx=ctx)\n"
  },
  {
    "path": "edb/pgsql/compiler/dml.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"IR compiler support for INSERT/UPDATE/DELETE statements.\"\"\"\n\n#\n# The processing of the DML statement is done in two parts.\n#\n# 1. The statement's *range* query is built: the relation representing\n#    the statement's target Object with any WHERE quals taken into account.\n#\n# 2. The statement body is processed to generate a series of\n#    SQL substatements to modify all relations touched by the statement\n#    depending on the link layout.\n#\n\nfrom __future__ import annotations\n\nfrom typing import (\n    Optional,\n    Mapping,\n    Sequence,\n    Collection,\n    NamedTuple,\n)\n\nimport immutables as immu\n\nfrom edb.common.typeutils import downcast, not_none\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom edb.schema import name as sn\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtyputils\nfrom edb.ir import utils as irutils\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import types as pg_types\nfrom edb.pgsql import common\n\nfrom . import astutils\nfrom . import clauses\nfrom . import context\nfrom . import dispatch\nfrom . import enums as pgce\nfrom . import output\nfrom . import pathctx\nfrom . import relctx\nfrom . import relgen\n\n\nclass DMLParts(NamedTuple):\n\n    dml_ctes: Mapping[\n        irast.TypeRef,\n        tuple[pgast.CommonTableExpr, pgast.PathRangeVar],\n    ]\n\n    else_cte: Optional[tuple[pgast.CommonTableExpr, pgast.PathRangeVar]]\n\n    range_cte: Optional[pgast.CommonTableExpr]\n\n\ndef init_dml_stmt(\n    ir_stmt: irast.MutatingStmt,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> DMLParts:\n    \"\"\"Prepare the common structure of the query representing a DML stmt.\n\n    Args:\n        ir_stmt:\n            IR of the DML statement.\n\n    Returns:\n        A ``DMLParts`` tuple containing a map of DML CTEs as well as the\n        common range CTE for UPDATE/DELETE statements.\n    \"\"\"\n    range_cte: Optional[pgast.CommonTableExpr]\n    range_rvar: Optional[pgast.RelRangeVar]\n\n    clauses.compile_volatile_bindings(ir_stmt, ctx=ctx)\n\n    if isinstance(ir_stmt, (irast.UpdateStmt, irast.DeleteStmt)):\n        # UPDATE and DELETE operate over a range, so generate\n        # the corresponding CTE and connect it to the DML statements.\n        range_cte = get_dml_range(ir_stmt, ctx=ctx)\n        range_rvar = pgast.RelRangeVar(\n            relation=range_cte,\n            alias=pgast.Alias(\n                aliasname=ctx.env.aliases.get(hint='range')\n            )\n        )\n    else:\n        range_cte = None\n        range_rvar = None\n\n    top_typeref = ir_stmt.material_type\n\n    typerefs = [top_typeref]\n\n    if isinstance(ir_stmt, (irast.UpdateStmt, irast.DeleteStmt)):\n        if top_typeref.union:\n            for component in top_typeref.union:\n                if component.material_type:\n                    component = component.material_type\n\n                typerefs.append(component)\n                typerefs.extend(irtyputils.get_typeref_descendants(component))\n\n        typerefs.extend(irtyputils.get_typeref_descendants(top_typeref))\n\n        # Only update/delete concrete types. (Except in the degenerate\n        # corner case where there are none, in which case keep using\n        # everything so as to avoid needing a more complex special case.)\n        concrete_typerefs = [t for t in typerefs if not t.is_abstract]\n        if concrete_typerefs:\n            typerefs = concrete_typerefs\n\n    dml_map = {}\n\n    for typeref in typerefs:\n        if typeref.union:\n            continue\n        if (\n            isinstance(typeref.name_hint, sn.QualName)\n            and typeref.name_hint.module in ('sys', 'cfg')\n        ):\n            continue\n        dml_cte, dml_rvar = gen_dml_cte(\n            ir_stmt,\n            range_rvar=range_rvar,\n            typeref=typeref,\n            ctx=ctx,\n        )\n\n        dml_map[typeref] = (dml_cte, dml_rvar)\n\n    else_cte = None\n    if (\n        isinstance(ir_stmt, irast.InsertStmt)\n        and ir_stmt.on_conflict and ir_stmt.on_conflict.else_ir is not None\n    ):\n        dml_cte = pgast.CommonTableExpr(\n            query=pgast.SelectStmt(),\n            name=ctx.env.aliases.get(hint='melse'),\n            for_dml_stmt=ir_stmt,\n        )\n        dml_rvar = relctx.rvar_for_rel(dml_cte, ctx=ctx)\n        else_cte = (dml_cte, dml_rvar)\n\n    put_iterator_bond(ctx.enclosing_cte_iterator, ctx.rel)\n\n    ctx.dml_stmt_stack.append(ir_stmt)\n\n    return DMLParts(\n        dml_ctes=dml_map,\n        range_cte=range_cte,\n        else_cte=else_cte,\n    )\n\n\ndef gen_dml_union(\n    ir_stmt: irast.MutatingStmt,\n    parts: DMLParts,\n    *,\n    ctx: context.CompilerContextLevel\n) -> tuple[pgast.CommonTableExpr, pgast.PathRangeVar]:\n    dml_entries = list(parts.dml_ctes.values())\n    if parts.else_cte:\n        dml_entries.append(parts.else_cte)\n\n    if len(dml_entries) == 1:\n        union_cte, union_rvar = dml_entries[0]\n    else:\n        union_components = []\n        for _, dml_rvar in dml_entries:\n            union_component = pgast.SelectStmt()\n            relctx.include_rvar(\n                union_component,\n                dml_rvar,\n                ir_stmt.subject.path_id,\n                ctx=ctx,\n            )\n            union_components.append(union_component)\n\n        qry = pgast.SelectStmt(\n            all=True,\n            larg=union_components[0],\n        )\n\n        for union_component in union_components[1:]:\n            qry.op = 'UNION'\n            qry.rarg = union_component\n            qry = pgast.SelectStmt(\n                all=True,\n                larg=qry,\n            )\n\n        assert qry.larg\n        put_iterator_bond(ctx.enclosing_cte_iterator, qry.larg)\n\n        union_cte = pgast.CommonTableExpr(\n            query=qry.larg,\n            name=ctx.env.aliases.get(hint='dml_union'),\n            for_dml_stmt=ctx.get_current_dml_stmt(),\n        )\n\n        union_rvar = relctx.rvar_for_rel(\n            union_cte,\n            typeref=ir_stmt.subject.typeref,\n            ctx=ctx,\n        )\n\n    ctx.dml_stmts[ir_stmt] = union_cte\n    union_cte.output_of_dml = ir_stmt\n\n    return union_cte, union_rvar\n\n\ndef gen_dml_cte(\n    ir_stmt: irast.MutatingStmt,\n    *,\n    range_rvar: Optional[pgast.RelRangeVar],\n    typeref: irast.TypeRef,\n    ctx: context.CompilerContextLevel,\n) -> tuple[pgast.CommonTableExpr, pgast.PathRangeVar]:\n\n    subject_ir_set = ir_stmt.subject\n    subject_path_id = subject_ir_set.path_id\n\n    dml_stmt: pgast.InsertStmt | pgast.SelectStmt | pgast.DeleteStmt\n    subject_rvar: pgast.BaseRangeVar\n    if isinstance(ir_stmt, irast.InsertStmt):\n        relation = relctx.range_for_typeref(\n            typeref,\n            subject_path_id,\n            for_mutation=True,\n            ctx=ctx,\n        )\n        assert isinstance(relation, pgast.RelRangeVar), (\n            \"spurious overlay on DML target\"\n        )\n\n        dml_stmt = pgast.InsertStmt(relation=relation)\n        subject_rvar = relation\n\n    elif isinstance(ir_stmt, irast.UpdateStmt):\n        # We generate a Select as the initial statement for an update,\n        # since the contents select is the query that needs to join\n        # the range and include policy filters and because we\n        # sometimes end up not needing an UPDATE anyway (if it only\n        # touches link tables).\n        dml_stmt = pgast.SelectStmt()\n\n        if ctx.env.sql_dml_mode:\n            # We join with the concrete table for this type, but also include\n            # overlays produced by previous DML stmts. This is needed for SQL\n            # DML, which needs to update a link table of a newly inserted object\n            subject_rel_overlayed = relctx.range_for_typeref(\n                typeref,\n                subject_path_id,\n                for_mutation=False,\n                include_descendants=False,\n                dml_source=[\n                    k for k in ctx.dml_stmts.keys()\n                    if isinstance(k, irast.MutatingLikeStmt)\n                ],\n                ctx=ctx,\n            )\n            dml_stmt.from_clause.append(subject_rel_overlayed)\n            subject_rvar = subject_rel_overlayed\n        else:\n            # We join with the concrete table for this type\n            relation = relctx.range_for_typeref(\n                typeref,\n                subject_path_id,\n                for_mutation=True,\n                ctx=ctx,\n            )\n            dml_stmt.from_clause.append(relation)\n            subject_rvar = relation\n\n    elif isinstance(ir_stmt, irast.DeleteStmt):\n        relation = relctx.range_for_typeref(\n            typeref,\n            subject_path_id,\n            for_mutation=True,\n            ctx=ctx,\n        )\n        assert isinstance(relation, pgast.RelRangeVar), (\n            \"spurious overlay on DML target\"\n        )\n        dml_stmt = pgast.DeleteStmt(relation=relation)\n        subject_rvar = relation\n    else:\n        raise AssertionError(f'unexpected DML IR: {ir_stmt!r}')\n\n    pathctx.put_path_value_rvar(dml_stmt, subject_path_id, subject_rvar)\n    pathctx.put_path_source_rvar(dml_stmt, subject_path_id, subject_rvar)\n    # Skip the path bond for inserts, since it doesn't help and\n    # interferes when inserting in an UNLESS CONFLICT ELSE\n    if not isinstance(ir_stmt, irast.InsertStmt):\n        pathctx.put_path_bond(dml_stmt, subject_path_id)\n\n    dml_cte = pgast.CommonTableExpr(\n        query=dml_stmt,\n        name=ctx.env.aliases.get(hint='dml'),\n        for_dml_stmt=ir_stmt,\n    )\n\n    # Due to the fact that DML statements are structured\n    # as a flat list of CTEs instead of nested range vars,\n    # the top level path scope must be empty.  The necessary\n    # range vars will be injected explicitly in all rels that\n    # need them.\n    ctx.path_scope.maps.clear()\n\n    if range_rvar is not None:\n        relctx.pull_path_namespace(\n            target=dml_stmt, source=range_rvar, ctx=ctx)\n\n        # Auxiliary relations are always joined via the WHERE\n        # clause due to the structure of the UPDATE/DELETE SQL statements.\n        assert isinstance(dml_stmt, (pgast.SelectStmt, pgast.DeleteStmt))\n        dml_stmt.where_clause = astutils.new_binop(\n            lexpr=pathctx.get_rvar_path_identity_var(\n                subject_rvar, subject_path_id, env=ctx.env\n            ),\n            op='=',\n            rexpr=pathctx.get_rvar_path_identity_var(\n                range_rvar, subject_path_id, env=ctx.env\n            )\n        )\n\n        # Do any read-side filtering\n        if pol_expr := ir_stmt.read_policies.get(typeref.id):\n            with ctx.newrel() as sctx:\n                pathctx.put_path_value_rvar(\n                    sctx.rel, subject_path_id, subject_rvar\n                )\n                pathctx.put_path_source_rvar(\n                    sctx.rel, subject_path_id, subject_rvar\n                )\n\n                val = clauses.compile_filter_clause(\n                    pol_expr.expr, pol_expr.cardinality, ctx=sctx\n                )\n            sctx.rel.target_list.append(pgast.ResTarget(val=val))\n\n            dml_stmt.where_clause = astutils.extend_binop(\n                dml_stmt.where_clause, sctx.rel\n            )\n\n        # SELECT has \"FROM\", while DELETE has \"USING\".\n        if isinstance(dml_stmt, pgast.SelectStmt):\n            dml_stmt.from_clause.append(range_rvar)\n        elif isinstance(dml_stmt, pgast.DeleteStmt):\n            dml_stmt.using_clause.append(range_rvar)\n\n    dml_rvar = relctx.rvar_for_rel(dml_cte, typeref=typeref, ctx=ctx)\n\n    return dml_cte, dml_rvar\n\n\ndef wrap_dml_cte(\n    ir_stmt: irast.MutatingStmt,\n    dml_cte: pgast.CommonTableExpr,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n\n    wrapper = ctx.rel\n    dml_rvar = relctx.rvar_for_rel(\n        dml_cte,\n        typeref=ir_stmt.subject.typeref,\n        ctx=ctx,\n    )\n    relctx.include_rvar(wrapper, dml_rvar, ir_stmt.subject.path_id, ctx=ctx)\n    pathctx.put_path_bond(wrapper, ir_stmt.subject.path_id)\n\n    if ctx.dml_stmt_stack:\n        relctx.reuse_type_rel_overlays(\n            dml_source=ir_stmt, dml_stmts=ctx.dml_stmt_stack, ctx=ctx)\n\n    return dml_rvar\n\n\ndef put_iterator_bond(\n    iterator: Optional[pgast.IteratorCTE],\n    select: pgast.Query,\n) -> None:\n    if iterator:\n        pathctx.put_path_bond(\n            select, iterator.path_id, iterator=iterator.iterator_bond)\n\n\ndef merge_iterator_scope(\n    iterator: Optional[pgast.IteratorCTE],\n    select: pgast.SelectStmt,\n    *,\n    ctx: context.CompilerContextLevel\n) -> None:\n    while iterator:\n        ctx.path_scope[iterator.path_id] = select\n        iterator = iterator.parent\n\n\ndef merge_iterator(\n    iterator: Optional[pgast.IteratorCTE],\n    select: pgast.SelectStmt,\n    *,\n    ctx: context.CompilerContextLevel\n) -> Optional[pgast.PathRangeVar]:\n    merge_iterator_scope(iterator, select, ctx=ctx)\n\n    if iterator:\n        iterator_rvar = relctx.rvar_for_rel(iterator.cte, ctx=ctx)\n\n        put_iterator_bond(iterator, select)\n        relctx.include_rvar(\n            select, iterator_rvar,\n            aspects=(pgce.PathAspect.VALUE, iterator.aspect) + (\n                (pgce.PathAspect.SOURCE,)\n                if iterator.path_id.is_objtype_path() else\n                ()\n            ),\n            path_id=iterator.path_id,\n            overwrite_path_rvar=True,\n            ctx=ctx)\n        # We need nested iterators to re-export their enclosing\n        # iterators in some cases that the path_id_mask blocks\n        # otherwise.\n        select.path_id_mask.discard(iterator.path_id)\n\n        # HACK: This is a hack for triggers, to stick __old__ in\n        # as a reference to __new__'s identity for updates/deletes\n        for other_path, aspect in iterator.other_paths:\n            pathctx.put_path_rvar(\n                select, other_path, iterator_rvar, aspect=aspect\n            )\n\n        return iterator_rvar\n\n    else:\n        return None\n\n\ndef fini_dml_stmt(\n    ir_stmt: irast.MutatingStmt,\n    parts: DMLParts,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> None:\n\n    union_cte, union_rvar = gen_dml_union(ir_stmt, parts, ctx=ctx)\n\n    if len(parts.dml_ctes) > 1 or parts.else_cte:\n        ctx.toplevel_stmt.append_cte(union_cte)\n\n    relctx.include_rvar(ctx.rel, union_rvar, ir_stmt.subject.path_id, ctx=ctx)\n\n    # Record the effect of this insertion in the relation overlay\n    # context to ensure that the RETURNING clause potentially\n    # referencing this class yields the expected results.\n    dml_stack = ctx.dml_stmt_stack\n    if isinstance(ir_stmt, irast.InsertStmt):\n        # The union CTE might have a SELECT from an ELSE clause, which\n        # we don't actually want to include.\n        assert len(parts.dml_ctes) == 1\n        cte = next(iter(parts.dml_ctes.values()))[0]\n        relctx.add_type_rel_overlay(\n            ir_stmt.subject.typeref, context.OverlayOp.UNION, cte,\n            dml_stmts=dml_stack, path_id=ir_stmt.subject.path_id, ctx=ctx)\n    elif isinstance(ir_stmt, irast.UpdateStmt):\n        base_typeref = ir_stmt.subject.typeref.real_material_type\n\n        for typeref, (cte, _) in parts.dml_ctes.items():\n            # Because we have a nice union_cte for the base type,\n            # we don't need to propagate the children overlays to\n            # that type or its ancestors, hence the stop_ref argument.\n            if typeref.id == base_typeref.id:\n                cte = union_cte\n                stop_ref = None\n            else:\n                stop_ref = base_typeref\n\n            # When the base type is abstract, there will be no CTE for it,\n            # so the overlays of children types have to apply to the whole\n            # ancestry tree.\n            if base_typeref.is_abstract:\n                stop_ref = None\n\n            # The overlay for update is in two parts:\n            # First, filter out objects that have been updated, then union them\n            # back in. (If we just did union, we'd see the old values also.)\n            relctx.add_type_rel_overlay(\n                typeref, context.OverlayOp.FILTER, cte,\n                stop_ref=stop_ref,\n                dml_stmts=dml_stack, path_id=ir_stmt.subject.path_id, ctx=ctx)\n            relctx.add_type_rel_overlay(\n                typeref, context.OverlayOp.UNION, cte,\n                stop_ref=stop_ref,\n                dml_stmts=dml_stack, path_id=ir_stmt.subject.path_id, ctx=ctx)\n\n        process_extra_conflicts(ir_stmt=ir_stmt, dml_parts=parts, ctx=ctx)\n    elif isinstance(ir_stmt, irast.DeleteStmt):\n        base_typeref = ir_stmt.subject.typeref.real_material_type\n\n        for typeref, (cte, _) in parts.dml_ctes.items():\n            # see above, re: stop_ref\n            if typeref.id == base_typeref.id:\n                cte = union_cte\n                stop_ref = None\n            else:\n                stop_ref = base_typeref\n\n            relctx.add_type_rel_overlay(\n                typeref, context.OverlayOp.EXCEPT, cte,\n                stop_ref=stop_ref,\n                dml_stmts=dml_stack, path_id=ir_stmt.subject.path_id, ctx=ctx)\n\n    clauses.compile_output(ir_stmt.result, ctx=ctx)\n\n    ctx.dml_stmt_stack.pop()\n\n\ndef get_dml_range(\n    ir_stmt: irast.UpdateStmt | irast.DeleteStmt,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.CommonTableExpr:\n    \"\"\"Create a range CTE for the given DML statement.\n\n    Args:\n        ir_stmt:\n            IR of the DML statement.\n\n    Returns:\n        A CommonTableExpr node representing the range affected\n        by the DML statement.\n    \"\"\"\n    target_ir_set = ir_stmt.subject\n    ir_qual_expr = ir_stmt.where\n    ir_qual_card = ir_stmt.where_card\n\n    with ctx.newrel() as subctx:\n        subctx.expr_exposed = False\n        range_stmt = subctx.rel\n\n        merge_iterator(ctx.enclosing_cte_iterator, range_stmt, ctx=subctx)\n\n        dispatch.visit(target_ir_set, ctx=subctx)\n        relgen.ensure_source_rvar(target_ir_set, range_stmt, ctx=subctx)\n\n        pathctx.get_path_identity_output(\n            range_stmt, target_ir_set.path_id, env=subctx.env)\n\n        if ir_qual_expr is not None:\n            with subctx.new() as wctx:\n                clauses.setup_iterator_volatility(target_ir_set, ctx=wctx)\n                range_stmt.where_clause = astutils.extend_binop(\n                    range_stmt.where_clause,\n                    clauses.compile_filter_clause(\n                        ir_qual_expr, ir_qual_card, ctx=wctx))\n\n        range_stmt.path_id_mask.discard(target_ir_set.path_id)\n        pathctx.put_path_bond(range_stmt, target_ir_set.path_id)\n\n        range_cte = pgast.CommonTableExpr(\n            query=range_stmt,\n            name=ctx.env.aliases.get('range'),\n            for_dml_stmt=ctx.get_current_dml_stmt(),\n        )\n\n        return range_cte\n\n\ndef compile_iterator_cte(\n    iterator_set: irast.Set,\n    *,\n    ctx: context.CompilerContextLevel\n) -> Optional[pgast.IteratorCTE]:\n\n    last_iterator = ctx.enclosing_cte_iterator\n\n    # If this iterator has already been compiled to a CTE, use\n    # that CTE instead of recompiling. (This will happen when\n    # a DML-containing FOR loop is WITH bound, for example.)\n    if iterator_set in ctx.dml_stmts:\n        iterator_cte = ctx.dml_stmts[iterator_set]\n        return pgast.IteratorCTE(\n            path_id=iterator_set.path_id, cte=iterator_cte,\n            parent=last_iterator, iterator_bond=True)\n\n    with ctx.newrel() as ictx:\n        ictx.scope_tree = ctx.scope_tree\n        ictx.path_scope[iterator_set.path_id] = ictx.rel\n\n        # Correlate with enclosing iterators\n        merge_iterator(last_iterator, ictx.rel, ctx=ictx)\n        clauses.setup_iterator_volatility(last_iterator, ctx=ictx)\n\n        clauses.compile_iterator_expr(\n            ictx.rel, iterator_set, is_dml=True, ctx=ictx)\n        if iterator_set.path_id.is_objtype_path():\n            relgen.ensure_source_rvar(iterator_set, ictx.rel, ctx=ictx)\n        ictx.rel.path_id = iterator_set.path_id\n        pathctx.put_path_bond(ictx.rel, iterator_set.path_id, iterator=True)\n        iterator_cte = pgast.CommonTableExpr(\n            query=ictx.rel,\n            name=ctx.env.aliases.get('iter'),\n            for_dml_stmt=ctx.get_current_dml_stmt(),\n        )\n        ictx.toplevel_stmt.append_cte(iterator_cte)\n\n    ctx.dml_stmts[iterator_set] = iterator_cte\n\n    return pgast.IteratorCTE(\n        path_id=iterator_set.path_id,\n        cte=iterator_cte,\n        parent=last_iterator,\n        iterator_bond=True,\n    )\n\n\ndef _mk_dynamic_get_path(\n    ptr_map: dict[sn.Name, pgast.BaseExpr],\n    typeref: irast.TypeRef,\n    fallback_rvar: Optional[pgast.PathRangeVar] = None,\n) -> pgast.DynamicRangeVarFunc:\n    \"\"\"A dynamic rvar function for insert/update.\n\n    It returns values out of a select purely based on material rptr,\n    as if it was a base relation. This is to make it easy for access\n    policies to operate on the results.\n    \"\"\"\n\n    def dynamic_get_path(\n        rel: pgast.Query, path_id: irast.PathId, *,\n        flavor: str,\n        aspect: str, env: context.Environment\n    ) -> Optional[pgast.BaseExpr | pgast.PathRangeVar]:\n        if (\n            flavor != 'normal'\n            or aspect not in (\n                pgce.PathAspect.VALUE, pgce.PathAspect.IDENTITY\n            )\n        ):\n            return None\n        if rptr := path_id.rptr():\n            if ret := ptr_map.get(rptr.real_material_ptr.name):\n                return ret\n            if rptr.real_material_ptr.shortname.name == '__type__':\n                return astutils.compile_typeref(typeref)\n        # If a fallback rvar is specified, defer to that.\n        # This is used in rewrites to go back to the original\n        if fallback_rvar:\n            return fallback_rvar\n        if not rptr:\n            raise LookupError('only pointers appear in insert fallback')\n        # Properties that aren't specified are {}\n        return pgast.NullConstant()\n    return dynamic_get_path\n\n\ndef process_insert_body(\n    *,\n    ir_stmt: irast.InsertStmt,\n    insert_cte: pgast.CommonTableExpr,\n    dml_parts: DMLParts,\n    ctx: context.CompilerContextLevel,\n) -> None:\n    \"\"\"Generate SQL DML CTEs from an InsertStmt IR.\n\n    Args:\n        ir_stmt:\n            IR of the DML statement.\n        insert_cte:\n            A CommonTableExpr node representing the SQL INSERT into\n            the main relation of the DML subject.\n        else_cte_rvar:\n            If present, a tuple containing a CommonTableExpr and\n            a RangeVar for it, which represent the body of an\n            ELSE clause in an UNLESS CONFLICT construct.\n        dml_parts:\n            A DMLParts tuple returned by init_dml_stmt().\n    \"\"\"\n\n    # We build the tuples to insert in a select we put into a CTE\n    select = pgast.SelectStmt(target_list=[])\n\n    # The main INSERT query of this statement will always be\n    # present to insert at least the `id` property.\n    insert_stmt = insert_cte.query\n    assert isinstance(insert_stmt, pgast.InsertStmt)\n\n    typeref = ir_stmt.subject.typeref.real_material_type\n\n    # Handle an UNLESS CONFLICT if we need it\n\n    # If there is an UNLESS CONFLICT, we need to know that there is a\n    # conflict *before* we execute DML for fields stored in the object\n    # itself, so we can prevent that execution from happening. If that\n    # is necessary, compile_insert_else_body will generate an iterator\n    # CTE with a row for each non-conflicting insert we want to do. We\n    # then use that as the iterator for any DML in inline fields.\n    #\n    # (For DML in the definition of pointers stored in link tables, we\n    # don't need to worry about this, because we can run that DML\n    # after the enclosing INSERT, using the enclosing INSERT as the\n    # iterator.)\n    on_conflict_fake_iterator = None\n    if ir_stmt.on_conflict:\n        assert not insert_stmt.on_conflict\n\n        on_conflict_fake_iterator = compile_insert_else_body(\n            insert_stmt,\n            ir_stmt,\n            ir_stmt.on_conflict,\n            ctx.enclosing_cte_iterator,\n            dml_parts.else_cte,\n            ctx=ctx,\n        )\n\n    iterator = ctx.enclosing_cte_iterator\n    inner_iterator = on_conflict_fake_iterator or iterator\n\n    # ptr_map needs to be set up in advance of compiling the shape\n    # because defaults might reference earlier pointers.\n    ptr_map: dict[sn.Name, pgast.BaseExpr] = {}\n\n    # Use a dynamic rvar to return values out of the select purely\n    # based on material rptr, as if it was a base relation.\n    # This is to make it easy for access policies to operate on the result\n    # of the INSERT.\n    fallback_rvar = pgast.DynamicRangeVar(\n        dynamic_get_path=_mk_dynamic_get_path(ptr_map, typeref))\n    pathctx.put_path_source_rvar(\n        select, ir_stmt.subject.path_id, fallback_rvar\n    )\n    pathctx.put_path_value_rvar(select, ir_stmt.subject.path_id, fallback_rvar)\n\n    # compile contents CTE\n    elements: list[tuple[irast.SetE[irast.Pointer], irast.BasePointerRef]] = []\n    for shape_el, shape_op in ir_stmt.subject.shape:\n        assert shape_op is qlast.ShapeOp.ASSIGN\n\n        # If the shape element is a linkprop, we do nothing.\n        # It will be picked up by the enclosing DML.\n        if shape_el.path_id.is_linkprop_path():\n            continue\n\n        ptrref = shape_el.expr.ptrref\n        if ptrref.material_ptr is not None:\n            ptrref = ptrref.material_ptr\n        assert shape_el.expr.expr\n        elements.append((shape_el, ptrref))\n\n    external_inserts = process_insert_shape(\n        ir_stmt, select, ptr_map, elements, iterator, inner_iterator, ctx\n    )\n    single_external = [\n        ir for ir in external_inserts\n        if ir.expr.dir_cardinality.is_single()\n    ]\n\n    # Put the select that builds the tuples to insert into its own CTE.\n    # We do this for two reasons:\n    # 1. Generating the object ids outside of the actual SQL insert allows\n    #    us to join any enclosing iterators into any nested external inserts.\n    # 2. We can use the contents CTE to evaluate insert access policies\n    #    before we actually try the insert. This is important because\n    #    otherwise an exclusive constraint could be raised first,\n    #    which leaks information.\n    pathctx.put_path_bond(select, ir_stmt.subject.path_id)\n    contents_cte = pgast.CommonTableExpr(\n        query=select,\n        name=ctx.env.aliases.get('ins_contents'),\n        for_dml_stmt=ctx.get_current_dml_stmt(),\n    )\n    ctx.toplevel_stmt.append_cte(contents_cte)\n    contents_rvar = relctx.rvar_for_rel(contents_cte, ctx=ctx)\n\n    rewrites = ir_stmt.rewrites and ir_stmt.rewrites.by_type.get(typeref)\n\n    pol_expr = ir_stmt.write_policies.get(typeref.id)\n    pol_ctx = None\n    if pol_expr or rewrites or single_external:\n        # Create a context for handling policies/rewrites that we will\n        # use later. We do this in advance so that the link update code\n        # can populate overlay fields in it.\n        with ctx.new() as pol_ctx:\n            pol_ctx.rel_overlays = context.RelOverlays()\n\n    needs_insert_on_conflict = bool(\n        ir_stmt.on_conflict and not on_conflict_fake_iterator)\n\n    # The first serious bit of trickiness: if there are rewrites, the link\n    # table updates need to be done *before* we compute the rewrites, since\n    # the rewrites might refer to them.\n    #\n    # However, we can't unconditionally do it like this, because we\n    # want to be able to use ON CONFLICT to implement UNLESS CONFLICT\n    # ON when possible, and in that case the link table operations\n    # need to be done after the *actual insert*, because it is the actual\n    # insert that filters out conflicting rows. (This also means that we\n    # can't use ON CONFLICT if there are rewrites.)\n    #\n    # Similar issues obtain with access policies: we can't use ON\n    # CONFLICT if there are access policies, since we can't \"see\" all\n    # possible conflicting objects.\n    #\n    # We *also* need link tables to go first if there are any single links\n    # with link properties. We do the actual computation for those in a link\n    # table and then join it in to the main table, where it is duplicated.\n    link_ctes = []\n\n    def _update_link_tables(inp_cte: pgast.CommonTableExpr) -> None:\n        # Process necessary updates to the link tables.\n        for shape_el in external_inserts:\n            link_cte, check_cte = process_link_update(\n                ir_stmt=ir_stmt,\n                ir_set=shape_el,\n                dml_cte=inp_cte,\n                source_typeref=typeref,\n                iterator=iterator,\n                policy_ctx=pol_ctx,\n                ctx=ctx,\n            )\n            if link_cte:\n                link_ctes.append(link_cte)\n            if check_cte is not None:\n                ctx.env.check_ctes.append(check_cte)\n\n    if not needs_insert_on_conflict:\n        _update_link_tables(contents_cte)\n\n    # compile rewrites CTE\n    if rewrites or single_external:\n        rewrites = rewrites or {}\n        assert not needs_insert_on_conflict\n\n        assert pol_ctx\n        # Now that all the compilation for the INSERT has been done,\n        # apply the tweaked policy overlays.\n        pol_ctx.rel_overlays = update_overlay(\n            ctx.rel_overlays, pol_ctx.rel_overlays\n        )\n        with pol_ctx.reenter(), pol_ctx.newrel() as rctx:\n            # Pull in ptr rel overlays, so we can see the pointers\n            merge_overlays_globally((ir_stmt,), ctx=rctx)\n\n            contents_cte, contents_rvar = process_insert_rewrites(\n                ir_stmt,\n                contents_cte=contents_cte,\n                iterator=iterator,\n                inner_iterator=inner_iterator,\n                rewrites=rewrites,\n                single_external=single_external,\n                elements=elements,\n                ctx=rctx,\n            )\n\n    # Populate the real insert statement based on the select we generated\n    insert_stmt.cols = [\n        pgast.InsertTarget(name=name)\n        for value in contents_cte.query.target_list\n        # Filter out generated columns; only keep concrete ones\n        if '~' not in (name := not_none(value.name))\n    ]\n    insert_stmt.select_stmt = pgast.SelectStmt(\n        target_list=[\n            pgast.ResTarget(val=col) for col in insert_stmt.cols\n        ],\n        from_clause=[contents_rvar],\n    )\n    pathctx.put_path_bond(insert_stmt, ir_stmt.subject.path_id)\n\n    real_insert_cte = pgast.CommonTableExpr(\n        query=insert_stmt,\n        name=ctx.env.aliases.get('ins'),\n        for_dml_stmt=ctx.get_current_dml_stmt(),\n    )\n\n    # Create the final CTE for the insert that joins the insert\n    # and the select together.\n    with ctx.newrel() as ictx:\n        merge_iterator(iterator, ictx.rel, ctx=ictx)\n        insert_rvar = relctx.rvar_for_rel(real_insert_cte, ctx=ctx)\n        relctx.include_rvar(\n            ictx.rel, insert_rvar, ir_stmt.subject.path_id, ctx=ictx)\n        relctx.include_rvar(\n            ictx.rel, contents_rvar, ir_stmt.subject.path_id, ctx=ictx)\n    # TODO: set up dml_parts with a SelectStmt for inserts always?\n    insert_cte.query = ictx.rel\n\n    # If there is an ON CONFLICT clause, insert the CTEs now so that the\n    # link inserts can depend on it. Otherwise we have the link updates\n    # depend on the contents cte so that policies can operate before\n    # doing any actual INSERTs.\n    if needs_insert_on_conflict:\n        ctx.toplevel_stmt.append_cte(real_insert_cte)\n        ctx.toplevel_stmt.append_cte(insert_cte)\n        link_op_cte = insert_cte\n    else:\n        link_op_cte = contents_cte\n\n    if needs_insert_on_conflict:\n        _update_link_tables(link_op_cte)\n\n    if pol_expr:\n        assert pol_ctx\n        assert not needs_insert_on_conflict\n        with pol_ctx.reenter():\n            policy_cte = compile_policy_check(\n                contents_cte, ir_stmt, pol_expr, typeref=typeref, ctx=pol_ctx\n            )\n        force_policy_checks(\n            policy_cte,\n            (insert_stmt,) + tuple(cte.query for cte in link_ctes),\n            ctx=ctx)\n\n    for link_cte in link_ctes:\n        ctx.toplevel_stmt.append_cte(link_cte)\n\n    if not needs_insert_on_conflict:\n        ctx.toplevel_stmt.append_cte(real_insert_cte)\n        ctx.toplevel_stmt.append_cte(insert_cte)\n\n    # XXX: do we need to pass in inner_iterator here?\n    process_extra_conflicts(ir_stmt=ir_stmt, dml_parts=dml_parts, ctx=ctx)\n\n\ndef process_insert_rewrites(\n    ir_stmt: irast.InsertStmt,\n    *,\n    contents_cte: pgast.CommonTableExpr,\n    iterator: Optional[pgast.IteratorCTE],\n    inner_iterator: Optional[pgast.IteratorCTE],\n    rewrites: irast.RewritesOfType,\n    single_external: list[irast.SetE[irast.Pointer]],\n    elements: Sequence[tuple[irast.SetE[irast.Pointer], irast.BasePointerRef]],\n    ctx: context.CompilerContextLevel,\n) -> tuple[pgast.CommonTableExpr, pgast.PathRangeVar]:\n    typeref = ir_stmt.subject.typeref.real_material_type\n\n    subject_path_id = ir_stmt.subject.path_id\n    rew_stmt = ctx.rel\n\n    # Use the original contents as the iterator.\n    inner_iterator = pgast.IteratorCTE(\n        path_id=subject_path_id,\n        cte=contents_cte,\n        parent=inner_iterator,\n        other_paths=(\n            (subject_path_id, pgce.PathAspect.IDENTITY),\n            (subject_path_id, pgce.PathAspect.VALUE),\n            (subject_path_id, pgce.PathAspect.SOURCE),\n        ),\n    )\n\n    # compile rewrite shape\n    rewrite_elements = list(rewrites.values())\n    nptr_map: dict[sn.Name, pgast.BaseExpr] = {}\n    process_insert_shape(\n        ir_stmt,\n        rew_stmt,\n        nptr_map,\n        rewrite_elements,\n        iterator,\n        inner_iterator,\n        ctx,\n        force_optional=True,\n    )\n\n    iterator_rvar = pathctx.get_path_rvar(\n        rew_stmt, path_id=subject_path_id, aspect=pgce.PathAspect.VALUE\n    )\n    fallback_rvar = pgast.DynamicRangeVar(\n        dynamic_get_path=_mk_dynamic_get_path(nptr_map, typeref, iterator_rvar)\n    )\n    pathctx.put_path_source_rvar(rew_stmt, subject_path_id, fallback_rvar)\n    pathctx.put_path_value_rvar(rew_stmt, subject_path_id, fallback_rvar)\n\n    # If there are any single links that were compiled externally,\n    # populate the field from the link overlays.\n    handled = set(rewrites)\n    for ext_ir in single_external:\n        handled.add(ext_ir.expr.ptrref.shortname.name)\n        with ctx.subrel() as ectx:\n            ext_rvar = relctx.new_pointer_rvar(\n                ext_ir, link_bias=True, src_rvar=iterator_rvar, ctx=ectx)\n            relctx.include_rvar(ectx.rel, ext_rvar, ext_ir.path_id, ctx=ectx)\n            # Make the subquery output the target\n            pathctx.get_path_value_output(\n                ectx.rel, ext_ir.path_id, env=ctx.env)\n\n        ptr_info = pg_types.get_ptrref_storage_info(\n            ext_ir.expr.ptrref, resolve_type=True, link_bias=False)\n        rew_stmt.target_list.append(pgast.ResTarget(\n            name=ptr_info.column_name, val=ectx.rel))\n        nptr_map[ext_ir.expr.ptrref.real_material_ptr.name] = ectx.rel\n\n    # Pull in pointers that were not rewritten\n    not_rewritten = {\n        (e, ptrref) for e, ptrref in elements\n        if ptrref.shortname.name not in handled\n    }\n    for e, ptrref in not_rewritten:\n        # FIXME: Duplicates some with process_insert_shape\n        ptr_info = pg_types.get_ptrref_storage_info(\n            ptrref, resolve_type=True, link_bias=False)\n        if ptr_info.table_type == 'ObjectType':\n            val = pathctx.get_path_var(\n                rew_stmt,\n                e.path_id,\n                aspect=pgce.PathAspect.VALUE,\n                env=ctx.env,\n            )\n            val = output.output_as_value(val, env=ctx.env)\n            rew_stmt.target_list.append(pgast.ResTarget(\n                name=ptr_info.column_name, val=val))\n\n    # construct the CTE\n    pathctx.put_path_bond(rew_stmt, ir_stmt.subject.path_id)\n    rewrites_cte = pgast.CommonTableExpr(\n        query=rew_stmt,\n        name=ctx.env.aliases.get('ins_rewrites'),\n        for_dml_stmt=ctx.get_current_dml_stmt(),\n    )\n    ctx.toplevel_stmt.append_cte(rewrites_cte)\n    rewrites_rvar = relctx.rvar_for_rel(rewrites_cte, ctx=ctx)\n\n    return rewrites_cte, rewrites_rvar\n\n\ndef process_insert_shape(\n    ir_stmt: irast.InsertStmt,\n    select: pgast.SelectStmt,\n    ptr_map: dict[sn.Name, pgast.BaseExpr],\n    elements: Sequence[tuple[irast.SetE[irast.Pointer], irast.BasePointerRef]],\n    iterator: Optional[pgast.IteratorCTE],\n    inner_iterator: Optional[pgast.IteratorCTE],\n    ctx: context.CompilerContextLevel,\n    force_optional: bool=False,\n) -> list[irast.SetE[irast.Pointer]]:\n    # Compile the shape\n    external_inserts = []\n\n    with ctx.newrel() as subctx:\n        subctx.enclosing_cte_iterator = inner_iterator\n\n        subctx.rel = select\n        subctx.expr_exposed = False\n\n        inner_iterator_id = None\n        if inner_iterator is not None:\n            subctx.path_scope = ctx.path_scope.new_child()\n            merge_iterator(inner_iterator, select, ctx=subctx)\n            inner_iterator_id = relctx.get_path_var(\n                select, inner_iterator.path_id,\n                aspect=inner_iterator.aspect,\n                ctx=ctx)\n\n        # Process the Insert IR and separate links that go\n        # into the main table from links that are inserted into\n        # a separate link table.\n        for element, ptrref in elements:\n\n            ptr_info = pg_types.get_ptrref_storage_info(\n                ptrref, resolve_type=True, link_bias=False)\n            link_ptr_info = pg_types.get_ptrref_storage_info(\n                ptrref, resolve_type=False, link_bias=True)\n\n            # First, process all local link inserts. Single link with\n            # link properties are not processed here; we compile those\n            # in link tables and then select those back into the main\n            # table as a rewrite.\n            if not link_ptr_info and ptr_info.table_type == 'ObjectType':\n                compile_insert_shape_element(\n                    element,\n                    ir_stmt=ir_stmt,\n                    iterator_id=inner_iterator_id,\n                    force_optional=force_optional,\n                    ctx=subctx,\n                )\n\n                insvalue = pathctx.get_path_value_var(\n                    subctx.rel, element.path_id, env=ctx.env)\n\n                if irtyputils.is_tuple(element.typeref):\n                    # Tuples require an explicit cast.\n                    insvalue = pgast.TypeCast(\n                        arg=output.output_as_value(insvalue, env=ctx.env),\n                        type_name=pgast.TypeName(\n                            name=ptr_info.column_type,\n                        ),\n                    )\n\n                ptr_map[ptrref.name] = insvalue\n                select.target_list.append(pgast.ResTarget(\n                    name=ptr_info.column_name, val=insvalue))\n\n            if link_ptr_info and link_ptr_info.table_type == 'link':\n                external_inserts.append(element)\n\n        put_iterator_bond(iterator, select)\n\n    for aspect in (pgce.PathAspect.VALUE, pgce.PathAspect.IDENTITY):\n        pathctx._put_path_output_var(\n            select, ir_stmt.subject.path_id, aspect=aspect,\n            var=pgast.ColumnRef(name=['id']),\n        )\n\n    return external_inserts\n\n\ndef compile_insert_shape_element(\n    shape_el: irast.SetE[irast.Pointer],\n    *,\n    ir_stmt: irast.MutatingStmt,\n    iterator_id: Optional[pgast.BaseExpr],\n    force_optional: bool,\n    ctx: context.CompilerContextLevel,\n) -> None:\n\n    with ctx.new() as insvalctx:\n        if iterator_id is not None:\n            id = iterator_id\n            insvalctx.volatility_ref = (lambda _stmt, _ctx: id,)\n        else:\n            # Single inserts have no need for forced\n            # computable volatility, and, furthermore,\n            # we do not have a valid identity reference\n            # anyway.\n            insvalctx.volatility_ref = ()\n\n        insvalctx.current_insert_path_id = ir_stmt.subject.path_id\n\n        if shape_el.expr.dir_cardinality.can_be_zero() or force_optional:\n            # If the element can be empty, compile it in a subquery to force it\n            # to be NULL.\n            value = relgen.set_as_subquery(\n                shape_el, as_value=True, ctx=insvalctx)\n            pathctx.put_path_value_var(insvalctx.rel, shape_el.path_id, value)\n        else:\n            dispatch.visit(shape_el, ctx=insvalctx)\n\n\ndef merge_overlays_globally(\n    ir_stmts: Collection[irast.MutatingLikeStmt | None],\n    *,\n    ctx: context.CompilerContextLevel,\n) -> None:\n    ctx.rel_overlays = ctx.rel_overlays.copy()\n\n    type_overlay = ctx.rel_overlays.type.get(None, immu.Map())\n    ptr_overlay = ctx.rel_overlays.ptr.get(None, immu.Map())\n\n    for ir_stmt in ir_stmts:\n        if not ir_stmt:\n            continue\n        for k, v in ctx.rel_overlays.type.get(ir_stmt, immu.Map()).items():\n            els = set(type_overlay.get(k, ()))\n            n_els = (\n                type_overlay.get(k, ()) + tuple(e for e in v if e not in els)\n            )\n            type_overlay = type_overlay.set(k, n_els)\n        for k2, v2 in ctx.rel_overlays.ptr.get(ir_stmt, immu.Map()).items():\n            els = set(ptr_overlay.get(k2, ()))\n            n_els = (\n                ptr_overlay.get(k2, ()) + tuple(e for e in v2 if e not in els)\n            )\n            ptr_overlay = ptr_overlay.set(k2, n_els)\n\n    ctx.rel_overlays.type = ctx.rel_overlays.type.set(None, type_overlay)\n    ctx.rel_overlays.ptr = ctx.rel_overlays.ptr.set(None, ptr_overlay)\n\n\ndef update_overlay(\n    left: context.RelOverlays,\n    right: context.RelOverlays,\n) -> context.RelOverlays:\n    '''Make an updated copy of the left overlay using right.'''\n    left = left.copy()\n\n    for ir_stmt, tm in right.type.items():\n        ltm = left.type.get(ir_stmt, immu.Map())\n        ltm = ltm.update(tm)\n        left.type = left.type.set(ir_stmt, ltm)\n    for ir_stmt, pm in right.ptr.items():\n        lpm = left.ptr.get(ir_stmt, immu.Map())\n        lpm = lpm.update(pm)\n        left.ptr = left.ptr.set(ir_stmt, lpm)\n\n    return left\n\n\ndef compile_policy_check(\n    dml_cte: pgast.CommonTableExpr,\n    ir_stmt: irast.MutatingStmt,\n    access_policies: irast.WritePolicies,\n    typeref: irast.TypeRef,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.CommonTableExpr:\n    subject_id = ir_stmt.subject.path_id\n\n    with ctx.newrel() as ictx:\n        # Pull in ptr rel overlays, so we can see the pointers\n        merge_overlays_globally((ir_stmt,), ctx=ictx)\n\n        dml_rvar = relctx.rvar_for_rel(dml_cte, ctx=ctx)\n        relctx.include_rvar(ictx.rel, dml_rvar, path_id=subject_id, ctx=ictx)\n\n        # split and compile\n        allow, deny = [], []\n        for policy in access_policies.policies:\n            cond_ref = clauses.compile_filter_clause(\n                policy.expr, policy.cardinality, ctx=ictx\n            )\n\n            if policy.action == qltypes.AccessPolicyAction.Allow:\n                allow.append((policy, cond_ref))\n            else:\n                deny.append((policy, cond_ref))\n\n        def raise_if(a: pgast.BaseExpr, msg: pgast.BaseExpr) -> pgast.BaseExpr:\n            return pgast.FuncCall(\n                name=astutils.edgedb_func('raise_on_null', ctx=ctx),\n                args=[\n                    pgast.FuncCall(\n                        name=('nullif',),\n                        args=[a, pgast.BooleanConstant(val=True)],\n                    ),\n                    pgast.StringConstant(val='insufficient_privilege'),\n                    pgast.NamedFuncArg(\n                        name='msg',\n                        val=msg,\n                    ),\n                    pgast.NamedFuncArg(\n                        name='table',\n                        val=pgast.StringConstant(val=str(typeref.id)),\n                    ),\n                ],\n            )\n\n        # allow\n        if allow:\n            allow_conds = (cond for _, cond in allow)\n            no_allow_expr: pgast.BaseExpr = astutils.new_unop(\n                'NOT', astutils.extend_binop(None, *allow_conds, op='OR')\n            )\n        else:\n            no_allow_expr = pgast.BooleanConstant(val=True)\n\n        # deny\n        deny_exprs = (cond for _, cond in deny)\n\n        # message\n        if isinstance(ir_stmt, irast.InsertStmt):\n            op = 'insert'\n        else:\n            op = 'update'\n        msg = f'access policy violation on {op} of {typeref.name_hint}'\n\n        allow_hints = (pol.error_msg for pol, _ in allow if pol.error_msg)\n        allow_hint = '; '.join(allow_hints)\n\n        hints = [(allow_hint, no_allow_expr)] + [\n            (pol.error_msg, cond) for pol, cond in deny if pol.error_msg\n        ]\n\n        hint = _conditional_string_agg(hints)\n        if hint:\n            hint = astutils.new_coalesce(\n                astutils.extend_concat(' (', hint, ')'),\n                pgast.StringConstant(val=''),\n            )\n            message = astutils.extend_concat(msg, hint)\n        else:\n            message = astutils.extend_concat(msg)\n\n        ictx.rel.target_list.append(\n            pgast.ResTarget(\n                name=f'error',\n                val=raise_if(\n                    astutils.extend_binop(no_allow_expr, *deny_exprs, op='OR'),\n                    msg=message,\n                ),\n            )\n        )\n\n        policy_cte = pgast.CommonTableExpr(\n            query=ictx.rel,\n            name=ctx.env.aliases.get('policy'),\n            materialized=True,\n            for_dml_stmt=ctx.get_current_dml_stmt(),\n        )\n        ictx.toplevel_stmt.append_cte(policy_cte)\n        return policy_cte\n\n\ndef _conditional_string_agg(\n    pairs: Sequence[tuple[Optional[str], pgast.BaseExpr]],\n) -> Optional[pgast.BaseExpr]:\n\n    selects = [\n        pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    val=pgast.StringConstant(val=str)\n                    if str\n                    else pgast.NullConstant()\n                )\n            ],\n            where_clause=cond,\n        )\n        for str, cond in pairs\n    ]\n    union = astutils.extend_select_op(None, *selects)\n\n    if not union:\n        return None\n\n    return pgast.SelectStmt(\n        target_list=[\n            pgast.ResTarget(\n                val=pgast.FuncCall(\n                    name=('string_agg',),\n                    args=[\n                        pgast.ColumnRef(name=('error_msg',)),\n                        pgast.StringConstant(val='; '),\n                    ],\n                ),\n            )\n        ],\n        from_clause=[\n            pgast.RangeSubselect(\n                subquery=union,\n                alias=pgast.Alias(aliasname='t', colnames=['error_msg']),\n            )\n        ],\n    )\n\n\ndef force_policy_checks(\n        policy_cte: pgast.CommonTableExpr,\n        queries: Sequence[pgast.Query],\n        *,\n        ctx: context.CompilerContextLevel) -> None:\n    # The actual DML statements need to be made dependent on the\n    # policy CTE, to ensure that it is evaluated before any\n    # modifications are done.\n\n    scan = pgast.Expr(\n        name=\">\",\n        lexpr=clauses.make_check_scan(policy_cte, ctx=ctx),\n        rexpr=pgast.NumericConstant(val=\"-1\"),\n    )\n    stmt: Optional[pgast.Query]\n    for stmt in queries:\n        if isinstance(stmt, pgast.InsertStmt):\n            stmt = stmt.select_stmt\n        if isinstance(stmt, (pgast.SelectStmt, pgast.UpdateStmt)):\n            stmt.where_clause = astutils.extend_binop(\n                stmt.where_clause, scan\n            )\n\n    # If there aren't any update/insert queries to put it into\n    # (because it is just an update with a -=, probably), make it a\n    # normal check CTE.\n    if not queries:\n        ctx.env.check_ctes.append(policy_cte)\n\n\ndef insert_needs_conflict_cte(\n    ir_stmt: irast.MutatingStmt,\n    on_conflict: irast.OnConflictClause,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> bool:\n    # We need to generate a conflict CTE if it is possible that\n    # the query might generate two conflicting objects.\n    if on_conflict.else_fail:\n        return False\n\n    if on_conflict.always_check or ir_stmt.conflict_checks:\n        return True\n\n    # We can't use ON CONFLICT if there are access policies\n    # on the type, since UNLESS CONFLICT only should avoid\n    # conflicts with objects that are visible.\n    type_id = ir_stmt.subject.typeref.real_material_type.id\n    if (\n        (type_id, True) in ctx.env.type_rewrites\n        or (type_id, False) in ctx.env.type_rewrites\n    ):\n        return True\n\n    # We can't use ON CONFLICT if there are rewrites on the type\n    # because rewrites might reference multi pointers, which means\n    # we need to execute link operations before the final INSERT.\n    if ir_stmt.rewrites and ir_stmt.rewrites.by_type:\n        return True\n\n    for shape_el, _ in ir_stmt.subject.shape:\n        ptrref = shape_el.expr.ptrref\n        ptr_info = pg_types.get_ptrref_storage_info(\n            ptrref, resolve_type=True, link_bias=False)\n\n        # We need to generate a conflict CTE if we have a DML containing\n        # pointer stored in the object itself\n        if (\n            ptr_info.table_type == 'ObjectType'\n            and shape_el.expr.expr\n            and irutils.contains_dml(\n                shape_el.expr.expr,\n                skip_bindings=True,\n                skip_nodes=(ir_stmt.subject,),\n            )\n        ):\n            return True\n\n        # If there are any single links with link properties, we need\n        # a conflict CTE, since the link tables have to go before the\n        # insert.\n        if ptr_info.table_type == 'ObjectType':\n            link_ptr_info = pg_types.get_ptrref_storage_info(\n                ptrref, resolve_type=True, link_bias=True)\n            if link_ptr_info:\n                return True\n\n    return False\n\n\ndef compile_insert_else_body(\n        stmt: Optional[pgast.InsertStmt],\n        ir_stmt: irast.MutatingStmt,\n        on_conflict: irast.OnConflictClause,\n        enclosing_cte_iterator: Optional[pgast.IteratorCTE],\n        else_cte_rvar: Optional[\n            tuple[pgast.CommonTableExpr, pgast.PathRangeVar]],\n        *,\n        ctx: context.CompilerContextLevel) -> Optional[pgast.IteratorCTE]:\n\n    else_select = on_conflict.select_ir\n    else_branch = on_conflict.else_ir\n    else_fail = on_conflict.else_fail\n\n    # We need to generate a \"conflict CTE\" that filters out\n    # objects-to-insert that would conflict with existing objects in\n    # three scenarios:\n    #  1) When there is a nested DML operation as part of the value\n    #     of a pointer that is stored inline with the object.\n    #     This is because we need to prevent that DML from executing\n    #     before we have a chance to see what ON CONFLICT does.\n    #  2) When there could be a conflict with an object INSERT/UPDATEd\n    #     in this same query. (Either because of FOR or other DML statements.)\n    #     This is because we need that to raise a ConstraintError,\n    #     which means we can't use ON CONFLICT, and so we need to prevent\n    #     the insertion of objects that conflict with existing ones ourselves.\n    #  3) When the type to insert has rewrite rules on it that could\n    #     prevent seeing the existing objects, we use conflict ctes\n    #     instead of setting ON CONFLICT so that we raise ConstraintError\n    #     instead of succeeding. This is partially for compatibility with\n    #     cases that have access rules and fall into case 1, where we\n    #     must do this, and partly because we would not be able to return\n    #     the objects in the ELSE anyway.\n    #\n    # When we need a conflict CTE, we don't use SQL ON CONFLICT. In\n    # cases 2 & 3, that is the whole point, while in case 1 it would\n    # just be superfluous to do so.\n    #\n    # When none of these cases obtain, we use ON CONFLICT because it\n    # ought to be more performant.\n    needs_conflict_cte = insert_needs_conflict_cte(\n        ir_stmt, on_conflict, ctx=ctx)\n    if not needs_conflict_cte and not else_fail:\n        target = None\n        if on_conflict.constraint:\n            constraint_name = common.get_constraint_raw_name(\n                on_conflict.constraint.id)\n            target = pgast.OnConflictTarget(\n                constraint_name=f'\"{constraint_name}\"'\n            )\n\n        assert isinstance(stmt, pgast.InsertStmt)\n        stmt.on_conflict = pgast.OnConflictClause(\n            action=pgast.OnConflictAction.DO_NOTHING,\n            target=target,\n        )\n\n    if not else_branch and not needs_conflict_cte and not else_fail:\n        return None\n\n    subject_id = ir_stmt.subject.path_id\n\n    # Compile the query CTE that selects out the existing rows\n    # that we would conflict with\n    with ctx.newrel() as ictx:\n        ictx.expr_exposed = False\n        ictx.path_scope[subject_id] = ictx.rel\n\n        compile_insert_else_body_failure_check(ir_stmt, on_conflict, ctx=ictx)\n\n        merge_iterator(enclosing_cte_iterator, ictx.rel, ctx=ictx)\n        clauses.setup_iterator_volatility(enclosing_cte_iterator, ctx=ictx)\n\n        dispatch.compile(else_select, ctx=ictx)\n        pathctx.put_path_id_map(ictx.rel, subject_id, else_select.path_id)\n        # Discard else_branch from the path_id_mask to prevent subject_id\n        # from being masked.\n        ictx.rel.path_id_mask.discard(else_select.path_id)\n\n        else_select_cte = pgast.CommonTableExpr(\n            query=ictx.rel,\n            name=ctx.env.aliases.get('else'),\n            for_dml_stmt=ctx.get_current_dml_stmt(),\n        )\n        if else_fail:\n            ctx.env.check_ctes.append(else_select_cte)\n\n        ictx.toplevel_stmt.append_cte(else_select_cte)\n\n    else_select_rvar = relctx.rvar_for_rel(else_select_cte, ctx=ctx)\n\n    if else_branch:\n        # Compile the body of the ELSE query\n        with ctx.newrel() as ictx:\n            ictx.path_scope[subject_id] = ictx.rel\n\n            relctx.include_rvar(ictx.rel, else_select_rvar,\n                                path_id=else_select.path_id, ctx=ictx)\n\n            ictx.enclosing_cte_iterator = pgast.IteratorCTE(\n                path_id=else_select.path_id, cte=else_select_cte,\n                parent=enclosing_cte_iterator)\n            dispatch.compile(else_branch, ctx=ictx)\n            pathctx.put_path_id_map(ictx.rel, subject_id, else_branch.path_id)\n            # Discard else_branch from the path_id_mask to prevent subject_id\n            # from being masked.\n            ictx.rel.path_id_mask.discard(else_branch.path_id)\n\n            assert else_cte_rvar\n            else_branch_cte = else_cte_rvar[0]\n            else_branch_cte.query = ictx.rel\n            ictx.toplevel_stmt.append_cte(else_branch_cte)\n\n    anti_cte_iterator = None\n    if needs_conflict_cte:\n        # Compile a CTE that matches rows that didn't appear in the\n        # ELSE query of conflicting rows.\n        with ctx.newrel() as ictx:\n            merge_iterator(enclosing_cte_iterator, ictx.rel, ctx=ictx)\n            clauses.setup_iterator_volatility(enclosing_cte_iterator, ctx=ictx)\n\n            # Set up a dummy path to represent all of the rows\n            # that *aren't* being filtered out\n            dummy_pathid = irast.PathId.new_dummy(ctx.env.aliases.get('dummy'))\n            with ictx.subrel() as dctx:\n                dummy_q = dctx.rel\n                relctx.create_iterator_identity_for_path(\n                    dummy_pathid, dummy_q, ctx=dctx)\n            dummy_rvar = relctx.rvar_for_rel(\n                dummy_q, lateral=True, ctx=ictx)\n            relctx.include_rvar(ictx.rel, dummy_rvar,\n                                path_id=dummy_pathid, ctx=ictx)\n\n            with ictx.subrel() as subrelctx:\n                subrel = subrelctx.rel\n                relctx.include_rvar(subrel, else_select_rvar,\n                                    path_id=subject_id, ctx=ictx)\n\n            # Do the anti-join\n            iter_path_id = (\n                enclosing_cte_iterator.path_id if\n                enclosing_cte_iterator else None)\n            aspect = (\n                enclosing_cte_iterator.aspect if enclosing_cte_iterator\n                else pgce.PathAspect.IDENTITY\n            )\n            relctx.anti_join(ictx.rel, subrel, iter_path_id,\n                             aspect=aspect, ctx=ctx)\n\n            # Package it up as a CTE\n            anti_cte = pgast.CommonTableExpr(\n                query=ictx.rel,\n                name=ctx.env.aliases.get('non_conflict'),\n                for_dml_stmt=ctx.get_current_dml_stmt(),\n            )\n            ictx.toplevel_stmt.append_cte(anti_cte)\n            anti_cte_iterator = pgast.IteratorCTE(\n                path_id=dummy_pathid, cte=anti_cte,\n                parent=ictx.enclosing_cte_iterator,\n                iterator_bond=True\n            )\n\n    return anti_cte_iterator\n\n\ndef compile_insert_else_body_failure_check(\n        ir_stmt: irast.MutatingStmt,\n        on_conflict: irast.OnConflictClause,\n        *,\n        ctx: context.CompilerContextLevel) -> None:\n    else_fail = on_conflict.else_fail\n    if not else_fail:\n        return\n\n    # Copy the type rels from the possibly conflicting earlier DML\n    # into the None overlays so it gets picked up.\n    # Also copy our own overlays, which we care about just for\n    # the pointer overlays.\n    merge_overlays_globally((ir_stmt, else_fail,), ctx=ctx)\n\n    # Do some work so that we aren't looking at the existing on-disk\n    # data, just newly data created data.\n    overlays_map = ctx.rel_overlays.type.get(None, immu.Map())\n    for k, overlays in overlays_map.items():\n        # Strip out filters, which we don't care about in this context\n        overlays = tuple([\n            (k, r, p)\n            for k, r, p in overlays\n            if k != context.OverlayOp.FILTER\n        ])\n        # Drop the initial set\n        if overlays and overlays[0][0] == context.OverlayOp.UNION:\n            overlays = (\n                (context.OverlayOp.REPLACE, *overlays[0][1:]),\n                *overlays[1:]\n            )\n        overlays_map = overlays_map.set(k, overlays)\n\n    ctx.rel_overlays.type = ctx.rel_overlays.type.set(None, overlays_map)\n\n    assert on_conflict.constraint\n    cid = common.get_constraint_raw_name(on_conflict.constraint.id)\n    maybe_raise = pgast.FuncCall(\n        name=astutils.edgedb_func('raise', ctx=ctx),\n        args=[\n            pgast.TypeCast(\n                arg=pgast.NullConstant(),\n                type_name=pgast.TypeName(name=('text',))),\n            pgast.StringConstant(val='exclusion_violation'),\n            pgast.NamedFuncArg(\n                name='msg',\n                val=pgast.StringConstant(\n                    val=(\n                        f'duplicate key value violates unique '\n                        f'constraint \"{cid}\"'\n                    )\n                ),\n            ),\n            pgast.NamedFuncArg(\n                name='constraint',\n                val=pgast.StringConstant(val=f\"{cid}\")\n            ),\n        ],\n    )\n    ctx.rel.target_list.append(\n        pgast.ResTarget(name='error', val=maybe_raise)\n    )\n\n\ndef process_update_body(\n    *,\n    ir_stmt: irast.UpdateStmt,\n    update_cte: pgast.CommonTableExpr,\n    dml_parts: DMLParts,\n    typeref: irast.TypeRef,\n    ctx: context.CompilerContextLevel,\n) -> None:\n    \"\"\"Generate SQL DML CTEs from an UpdateStmt IR.\n\n    Args:\n        ir_stmt:\n            IR of the DML statement.\n        update_cte:\n            CTE representing the SQL UPDATE to the main relation of the UPDATE\n            subject.\n        dml_parts:\n            A DMLParts tuple returned by init_dml_stmt().\n        typeref:\n            A TypeRef corresponding the the type of a subject being updated\n            by the update_cte.\n    \"\"\"\n    assert isinstance(update_cte.query, pgast.SelectStmt)\n    contents_select = update_cte.query\n    toplevel = ctx.toplevel_stmt\n\n    put_iterator_bond(ctx.enclosing_cte_iterator, contents_select)\n\n    assert dml_parts.range_cte\n    iterator = pgast.IteratorCTE(\n        path_id=ir_stmt.subject.path_id,\n        cte=dml_parts.range_cte,\n        parent=ctx.enclosing_cte_iterator,\n    )\n\n    with ctx.newscope() as subctx:\n        # It is necessary to process the expressions in\n        # the UpdateStmt shape body in the context of the\n        # UPDATE statement so that references to the current\n        # values of the updated object are resolved correctly.\n        subctx.parent_rel = contents_select\n        subctx.expr_exposed = False\n        subctx.enclosing_cte_iterator = iterator\n\n        clauses.setup_iterator_volatility(iterator, ctx=subctx)\n\n        # compile contents CTE\n        elements = [\n            (shape_el, shape_el.expr.ptrref, shape_op)\n            for shape_el, shape_op in ir_stmt.subject.shape\n            if shape_op != qlast.ShapeOp.MATERIALIZE\n        ]\n\n        values, external_updates, ptr_map = process_update_shape(\n            ir_stmt, contents_select, elements, typeref, subctx\n        )\n\n        relation = contents_select.from_clause[0]\n        assert isinstance(relation, pgast.PathRangeVar)\n\n        # Use a dynamic rvar to return values out of the select purely\n        # based on material rptr, as if it was a base relation (and to\n        # fall back to the base relation if the value wasn't updated.)\n        fallback_rvar = pgast.DynamicRangeVar(\n            dynamic_get_path=_mk_dynamic_get_path(ptr_map, typeref, relation),\n        )\n        pathctx.put_path_source_rvar(\n            contents_select,\n            ir_stmt.subject.path_id,\n            fallback_rvar,\n        )\n        pathctx.put_path_value_rvar(\n            contents_select,\n            ir_stmt.subject.path_id,\n            fallback_rvar,\n        )\n\n    update_stmt = None\n\n    single_external = [\n        ir for ir, _ in external_updates\n        if ir.expr.dir_cardinality.is_single()\n    ]\n\n    rewrites = ir_stmt.rewrites and ir_stmt.rewrites.by_type.get(typeref)\n\n    pol_expr = ir_stmt.write_policies.get(typeref.id)\n    pol_ctx = None\n    if pol_expr or rewrites or single_external:\n        # Create a context for handling policies/rewrites that we will\n        # use later. We do this in advance so that the link update code\n        # can populate overlay fields in it.\n        with ctx.new() as pol_ctx:\n            pol_ctx.rel_overlays = context.RelOverlays()\n\n    no_update = not values and not rewrites and not single_external\n    if no_update:\n        # No updates directly to the set target table,\n        # so convert the UPDATE statement into a SELECT.\n        update_cte.query = contents_select\n        contents_cte = update_cte\n    else:\n        contents_cte = pgast.CommonTableExpr(\n            query=contents_select,\n            name=ctx.env.aliases.get(\"upd_contents\"),\n            for_dml_stmt=ctx.get_current_dml_stmt(),\n        )\n    toplevel.append_cte(contents_cte)\n\n    # Process necessary updates to the link tables.\n    # We do link tables before we do the main update so that\n    link_ctes = []\n    for expr, shape_op in external_updates:\n        link_cte, check_cte = process_link_update(\n            ir_stmt=ir_stmt,\n            ir_set=expr,\n            dml_cte=contents_cte,\n            iterator=iterator,\n            shape_op=shape_op,\n            source_typeref=typeref,\n            ctx=ctx,\n            policy_ctx=pol_ctx,\n        )\n        if link_cte:\n            link_ctes.append(link_cte)\n\n        if check_cte is not None:\n            ctx.env.check_ctes.append(check_cte)\n\n    if not no_update:\n        table_relation = relctx.range_for_typeref(\n            typeref,\n            ir_stmt.subject.path_id,\n            for_mutation=True,\n            ctx=ctx,\n        )\n        assert isinstance(table_relation, pgast.RelRangeVar)\n        range_relation = contents_select.from_clause[1]\n        assert isinstance(range_relation, pgast.PathRangeVar)\n\n        contents_rvar = relctx.rvar_for_rel(contents_cte, ctx=ctx)\n        subject_path_id = ir_stmt.subject.path_id\n\n        # Compile rewrites CTE\n        if rewrites or single_external:\n            rewrites = rewrites or {}\n            assert pol_ctx\n            # Now that all the compilation for the UPDATE has been done,\n            # apply the tweaked policy overlays.\n            pol_ctx.rel_overlays = update_overlay(\n                ctx.rel_overlays, pol_ctx.rel_overlays\n            )\n            with pol_ctx.reenter(), pol_ctx.new() as rctx:\n                merge_overlays_globally((ir_stmt,), ctx=rctx)\n                contents_cte, contents_rvar, values = process_update_rewrites(\n                    ir_stmt,\n                    typeref=typeref,\n                    contents_cte=contents_cte,\n                    contents_rvar=contents_rvar,\n                    iterator=iterator,\n                    contents_select=contents_select,\n                    table_relation=table_relation,\n                    range_relation=range_relation,\n                    single_external=single_external,\n                    rewrites=rewrites,\n                    elements=elements,\n                    ctx=rctx,\n                )\n\n        update_stmt = pgast.UpdateStmt(\n            relation=table_relation,\n            where_clause=astutils.new_binop(\n                lexpr=pgast.ColumnRef(\n                    name=[table_relation.alias.aliasname, \"id\"]\n                ),\n                op=\"=\",\n                rexpr=pathctx.get_rvar_path_identity_var(\n                    contents_rvar, subject_path_id, env=ctx.env\n                ),\n            ),\n            from_clause=[contents_rvar],\n            targets=[\n                pgast.MultiAssignRef(\n                    columns=[not_none(value.name) for value, _ in values],\n                    source=pgast.SelectStmt(\n                        target_list=[\n                            pgast.ResTarget(\n                                val=pgast.ColumnRef(\n                                    name=[\n                                        contents_rvar.alias.aliasname,\n                                        not_none(value.name),\n                                    ]\n                                )\n                            )\n                            for value, _ in values\n                        ],\n                    ),\n                )\n            ],\n        )\n        relctx.pull_path_namespace(\n            target=update_stmt, source=contents_rvar, ctx=ctx\n        )\n        pathctx.put_path_value_rvar(\n            update_stmt, subject_path_id, table_relation\n        )\n        pathctx.put_path_source_rvar(\n            update_stmt, subject_path_id, table_relation\n        )\n        put_iterator_bond(ctx.enclosing_cte_iterator, update_stmt)\n\n        update_cte.query = update_stmt\n\n    if pol_expr:\n        assert pol_ctx\n        with pol_ctx.reenter():\n            policy_cte = compile_policy_check(\n                contents_cte, ir_stmt, pol_expr, typeref=typeref, ctx=pol_ctx\n            )\n        force_policy_checks(\n            policy_cte,\n            ((update_stmt,) if update_stmt else ())\n            + tuple(cte.query for cte in link_ctes),\n            ctx=ctx,\n        )\n\n    if values:\n        toplevel.append_cte(update_cte)\n\n    for link_cte in link_ctes:\n        toplevel.append_cte(link_cte)\n\n\ndef process_update_rewrites(\n    ir_stmt: irast.UpdateStmt,\n    *,\n    typeref: irast.TypeRef,\n    contents_cte: pgast.CommonTableExpr,\n    contents_rvar: pgast.PathRangeVar,\n    iterator: Optional[pgast.IteratorCTE],\n    contents_select: pgast.SelectStmt,\n    table_relation: pgast.RelRangeVar,\n    range_relation: pgast.PathRangeVar,\n    single_external: list[irast.SetE[irast.Pointer]],\n    rewrites: irast.RewritesOfType,\n    elements: Sequence[\n        tuple[irast.SetE[irast.Pointer], irast.BasePointerRef, qlast.ShapeOp]],\n    ctx: context.CompilerContextLevel,\n) -> tuple[\n    pgast.CommonTableExpr,\n    pgast.PathRangeVar,\n    list[tuple[pgast.ResTarget, irast.PathId]],\n]:\n    # assert ir_stmt.rewrites\n    subject_path_id = ir_stmt.subject.path_id\n    if ir_stmt.rewrites:\n        old_path_id = ir_stmt.rewrites.old_path_id\n    else:\n        # Need values for the single external link case\n        old_path_id = subject_path_id\n    assert old_path_id\n\n    table_rel = table_relation.relation\n    assert isinstance(table_rel, pgast.Relation)\n\n    # Need to set up an iterator for any internal DML.\n    iterator = pgast.IteratorCTE(\n        path_id=subject_path_id,\n        cte=contents_cte,\n        parent=iterator,\n        # __old__\n        other_paths=(\n            ((old_path_id, pgce.PathAspect.IDENTITY),)\n        ),\n    )\n\n    with ctx.newrel() as rctx:\n        rewrites_stmt = rctx.rel\n        clauses.setup_iterator_volatility(iterator, ctx=rctx)\n        rctx.enclosing_cte_iterator = iterator\n\n        # pruned down version of gen_dml_cte\n        rewrites_stmt.from_clause.append(range_relation)\n\n        # pull in contents_select for __subject__\n        relctx.include_rvar(\n            rewrites_stmt,\n            contents_rvar,\n            subject_path_id,\n            # We don't want to update the mask... in case the subject\n            # is an iterator that needs to be reexported.\n            update_mask=False,\n            ctx=ctx,\n        )\n        rewrites_stmt.where_clause = astutils.new_binop(\n            lexpr=pathctx.get_rvar_path_identity_var(\n                contents_rvar, subject_path_id, env=ctx.env\n            ),\n            op=\"=\",\n            rexpr=pathctx.get_rvar_path_identity_var(\n                range_relation, subject_path_id, env=ctx.env\n            ),\n        )\n\n        # pull in table_relation for __old__\n        table_rel.path_outputs[\n            (old_path_id, pgce.PathAspect.VALUE)\n        ] = pathctx.get_path_value_output(\n            table_rel, subject_path_id, env=ctx.env\n        )\n        relctx.include_rvar(\n            rewrites_stmt, table_relation, old_path_id, ctx=ctx\n        )\n        rewrites_stmt.where_clause = astutils.extend_binop(\n            rewrites_stmt.where_clause,\n            astutils.new_binop(\n                lexpr=pgast.ColumnRef(\n                    name=[table_relation.alias.aliasname, \"id\"]\n                ),\n                op=\"=\",\n                rexpr=pathctx.get_rvar_path_identity_var(\n                    range_relation, subject_path_id, env=ctx.env\n                ),\n            ),\n        )\n\n        relctx.pull_path_namespace(\n            target=rewrites_stmt, source=table_relation, ctx=ctx\n        )\n\n        rewrite_elements = [\n            (el, ptrref, qlast.ShapeOp.ASSIGN)\n            for el, ptrref in rewrites.values()\n        ]\n        values, _, nptr_map = process_update_shape(\n            ir_stmt, rewrites_stmt, rewrite_elements, typeref, rctx,\n        )\n\n        # If there are any single links that were compiled externally,\n        # populate the field from the link overlays.\n        handled = set(rewrites)\n        for ext_ir in single_external:\n            handled.add(ext_ir.expr.ptrref.shortname.name)\n            actual_ptrref = irtyputils.find_actual_ptrref(\n                typeref, ext_ir.expr.ptrref)\n            with rctx.subrel() as ectx:\n                ext_rvar = relctx.new_pointer_rvar(\n                    ext_ir, link_bias=True, src_rvar=contents_rvar,\n                    ctx=ectx)\n                relctx.include_rvar(\n                    ectx.rel, ext_rvar, ext_ir.path_id, ctx=ectx)\n                # Make the subquery output the target\n                pathctx.get_path_value_output(\n                    ectx.rel, ext_ir.path_id, env=ctx.env)\n\n            ptr_info = pg_types.get_ptrref_storage_info(\n                actual_ptrref, resolve_type=True, link_bias=False)\n            updval = pgast.ResTarget(\n                name=ptr_info.column_name, val=ectx.rel)\n            rewrites_stmt.target_list.append(updval)\n            values.append((updval, ext_ir.path_id))\n            nptr_map[actual_ptrref.name] = ectx.rel\n\n        # Pull in pointers that were not rewritten\n        not_rewritten = {\n            (e, ptrref) for e, ptrref, _ in elements\n            if ptrref.shortname.name not in handled\n        }\n        for e, ptrref in not_rewritten:\n            # FIXME: Duplicates some with process_update_shape\n            actual_ptrref = irtyputils.find_actual_ptrref(typeref, ptrref)\n            ptr_info = pg_types.get_ptrref_storage_info(\n                actual_ptrref, resolve_type=True, link_bias=False)\n            if ptr_info.table_type == 'ObjectType':\n                val = pathctx.get_path_var(\n                    rewrites_stmt,\n                    e.path_id,\n                    aspect=pgce.PathAspect.VALUE,\n                    env=ctx.env,\n                )\n                updval = pgast.ResTarget(\n                    name=ptr_info.column_name, val=val)\n                values.append((updval, e.path_id))\n                rewrites_stmt.target_list.append(updval)\n\n        fallback_rvar = pgast.DynamicRangeVar(\n            dynamic_get_path=_mk_dynamic_get_path(\n                nptr_map, typeref, contents_rvar),\n        )\n        pathctx.put_path_source_rvar(rctx.rel, subject_path_id, fallback_rvar)\n        pathctx.put_path_value_rvar(rctx.rel, subject_path_id, fallback_rvar)\n\n        rewrites_cte = pgast.CommonTableExpr(\n            query=rctx.rel,\n            name=ctx.env.aliases.get(\"upd_rewrites\"),\n            for_dml_stmt=ctx.get_current_dml_stmt(),\n        )\n        ctx.toplevel_stmt.append_cte(rewrites_cte)\n        rewrites_rvar = relctx.rvar_for_rel(rewrites_cte, ctx=ctx)\n\n    return rewrites_cte, rewrites_rvar, values\n\n\ndef process_update_shape(\n    ir_stmt: irast.UpdateStmt,\n    rel: pgast.SelectStmt,\n    elements: Sequence[\n        tuple[irast.SetE[irast.Pointer], irast.BasePointerRef, qlast.ShapeOp]],\n    typeref: irast.TypeRef,\n    ctx: context.CompilerContextLevel,\n) -> tuple[\n    list[tuple[pgast.ResTarget, irast.PathId]],\n    list[tuple[irast.SetE[irast.Pointer], qlast.ShapeOp]],\n    dict[sn.Name, pgast.BaseExpr],\n]:\n    values: list[tuple[pgast.ResTarget, irast.PathId]] = []\n    external_updates: list[tuple[irast.SetE[irast.Pointer], qlast.ShapeOp]] = []\n    ptr_map: dict[sn.Name, pgast.BaseExpr] = {}\n\n    for element, shape_ptrref, shape_op in elements:\n        actual_ptrref = irtyputils.find_actual_ptrref(typeref, shape_ptrref)\n        ptr_info = pg_types.get_ptrref_storage_info(\n            actual_ptrref, resolve_type=True, link_bias=False\n        )\n        link_ptr_info = pg_types.get_ptrref_storage_info(\n            actual_ptrref, resolve_type=False, link_bias=True\n        )\n        # XXX: Slightly nervous about this.\n        assert isinstance(element.expr, irast.Pointer)\n        updvalue = element.expr.expr\n\n        if (\n            ptr_info.table_type == \"ObjectType\"\n            and not link_ptr_info\n            and updvalue is not None\n        ):\n            with ctx.newscope() as scopectx:\n                scopectx.expr_exposed = False\n                val: pgast.BaseExpr\n\n                if irtyputils.is_tuple(element.typeref):\n                    # When target is a tuple type, make sure\n                    # the expression is compiled into a subquery\n                    # returning a single column that is explicitly\n                    # cast into the appropriate composite type.\n                    val = relgen.set_as_subquery(\n                        element,\n                        as_value=True,\n                        explicit_cast=ptr_info.column_type,\n                        ctx=scopectx,\n                    )\n                else:\n                    if (\n                        isinstance(updvalue, irast.MutatingStmt)\n                        and updvalue in ctx.dml_stmts\n                    ):\n                        with scopectx.substmt() as srelctx:\n                            dml_cte = ctx.dml_stmts[updvalue]\n                            wrap_dml_cte(updvalue, dml_cte, ctx=srelctx)\n                            pathctx.get_path_identity_output(\n                                srelctx.rel,\n                                updvalue.subject.path_id,\n                                env=srelctx.env,\n                            )\n                            val = srelctx.rel\n                    else:\n                        # base case\n                        val = dispatch.compile(updvalue, ctx=scopectx)\n\n                    assert isinstance(updvalue, irast.Stmt)\n\n                    val = check_update_type(\n                        val,\n                        val,\n                        is_subquery=True,\n                        ir_stmt=ir_stmt,\n                        ir_set=updvalue.result,\n                        shape_ptrref=shape_ptrref,\n                        actual_ptrref=actual_ptrref,\n                        ctx=scopectx,\n                    )\n\n                    val = pgast.TypeCast(\n                        arg=val,\n                        type_name=pgast.TypeName(name=ptr_info.column_type),\n                    )\n\n                if shape_op is qlast.ShapeOp.SUBTRACT:\n                    val = pgast.FuncCall(\n                        name=(\"nullif\",),\n                        args=[\n                            pgast.ColumnRef(name=[ptr_info.column_name]),\n                            val,\n                        ],\n                    )\n\n                ptr_map[actual_ptrref.name] = val\n                updtarget = pgast.ResTarget(\n                    name=ptr_info.column_name,\n                    val=val,\n                )\n                values.append((updtarget, element.path_id))\n\n                # Register the output as both a var and an output\n                # so that if it is referenced in a policy or\n                # rewrite, the find_path_output optimization fires\n                # and we reuse the output instead of duplicating\n                # it.\n                # XXX: Maybe this suggests a rework of the\n                # DynamicRangeVar mechanism would be a good idea.\n                pathctx.put_path_var(\n                    rel,\n                    element.path_id,\n                    aspect=pgce.PathAspect.VALUE,\n                    var=val,\n                )\n                pathctx._put_path_output_var(\n                    rel,\n                    element.path_id,\n                    aspect=pgce.PathAspect.VALUE,\n                    var=pgast.ColumnRef(name=[ptr_info.column_name]),\n                )\n\n        if link_ptr_info and link_ptr_info.table_type == \"link\":\n            external_updates.append((element, shape_op))\n\n    rel.target_list.extend(v for v, _ in values)\n\n    return (values, external_updates, ptr_map)\n\n\ndef process_extra_conflicts(\n    *,\n    ir_stmt: irast.MutatingStmt,\n    dml_parts: DMLParts,\n    ctx: context.CompilerContextLevel,\n) -> None:\n    if not ir_stmt.conflict_checks:\n        return\n\n    for extra_conflict in ir_stmt.conflict_checks:\n        q_path = extra_conflict.check_anchor\n        assert q_path\n        typeref = q_path.target.real_material_type\n        cte, _ = dml_parts.dml_ctes[typeref]\n\n        pathctx.put_path_id_map(\n            cte.query, q_path, ir_stmt.subject.path_id)\n\n        conflict_iterator = pgast.IteratorCTE(\n            path_id=q_path, cte=cte,\n            parent=ctx.enclosing_cte_iterator)\n\n        compile_insert_else_body(\n            None,\n            ir_stmt,\n            extra_conflict,\n            conflict_iterator,\n            None,\n            ctx=ctx,\n        )\n\n\ndef check_update_type(\n    val: pgast.BaseExpr,\n    rel_or_rvar: pgast.BaseExpr | pgast.PathRangeVar,\n    *,\n    is_subquery: bool,\n    ir_stmt: irast.UpdateStmt,\n    ir_set: irast.Set,\n    shape_ptrref: irast.BasePointerRef,\n    actual_ptrref: irast.BasePointerRef,\n    ctx: context.CompilerContextLevel,\n) -> pgast.BaseExpr:\n    \"\"\"Possibly insert a type check on an UPDATE to a link\n\n    Because edgedb allows subtypes to covariantly override the target\n    types of links, we need to insert runtime type checks when\n    the target in a base type being UPDATEd does not match the\n    target type for this concrete subtype being handled.\n    \"\"\"\n\n    assert isinstance(actual_ptrref, irast.PointerRef)\n    base_ptrref = shape_ptrref.real_material_ptr\n\n    # We skip the check if either the base type matches exactly\n    # or the shape type matches exactly. FIXME: *Really* we want to do\n    # a subtype check, here, though, since this could do a needless\n    # check if we have multiple levels of overloading, but we don't\n    # have the infrastructure here.\n    if (\n        not irtyputils.is_object(ir_set.typeref)\n        or base_ptrref.out_target.id == actual_ptrref.out_target.id\n        or shape_ptrref.out_target.id == actual_ptrref.out_target.id\n    ):\n        return val\n\n    if isinstance(rel_or_rvar, pgast.PathRangeVar):\n        rvar = rel_or_rvar\n    else:\n        assert isinstance(rel_or_rvar, pgast.BaseRelation)\n        rvar = relctx.rvar_for_rel(rel_or_rvar, ctx=ctx)\n\n    # Make up a ptrref for the __type__ link on our actual target type\n    # and make up a new path_id to access it. Relies on __type__ always\n    # being named __type__, but that's fine.\n    # (Arranging to actually get a legit pointer ref is pointlessly expensive.)\n    el_name = sn.QualName('__', '__type__')\n    actual_type_ptrref = irast.SpecialPointerRef(\n        name=el_name,\n        shortname=el_name,\n        out_source=actual_ptrref.out_target,\n        # HACK: This is obviously not the right target type, but we don't\n        # need it for anything and the pathid never escapes this function.\n        out_target=actual_ptrref.out_target,\n        out_cardinality=qltypes.Cardinality.AT_MOST_ONE,\n    )\n    type_pathid = ir_set.path_id.extend(ptrref=actual_type_ptrref)\n\n    # Grab the actual value we have inserted and pull the __type__ out\n    rval = pathctx.get_rvar_path_identity_var(\n        rvar, ir_set.path_id, env=ctx.env)\n    typ = pathctx.get_rvar_path_identity_var(rvar, type_pathid, env=ctx.env)\n\n    typeref_val = dispatch.compile(actual_ptrref.out_target, ctx=ctx)\n\n    # Do the check! Include the ptrref for this concrete class and\n    # also the (dynamic) type of the argument, so that we can produce\n    # a good error message.\n    check_result = pgast.FuncCall(\n        name=astutils.edgedb_func('issubclass', ctx=ctx),\n        args=[typ, typeref_val],\n    )\n    maybe_null = pgast.CaseExpr(\n        args=[pgast.CaseWhen(expr=check_result, result=rval)])\n    maybe_raise = pgast.FuncCall(\n        name=astutils.edgedb_func('raise_on_null', ctx=ctx),\n        args=[\n            maybe_null,\n            pgast.StringConstant(val='wrong_object_type'),\n            pgast.NamedFuncArg(\n                name='msg',\n                val=pgast.StringConstant(val='covariance error')\n            ),\n            pgast.NamedFuncArg(\n                name='column',\n                val=pgast.StringConstant(val=str(actual_ptrref.id)),\n            ),\n            pgast.NamedFuncArg(\n                name='table',\n                val=pgast.TypeCast(\n                    arg=typ, type_name=pgast.TypeName(name=('text',))\n                ),\n            ),\n        ],\n    )\n\n    if is_subquery:\n        # If this is supposed to be a subquery (because it is an\n        # update of a single link), wrap the result query in a new one,\n        # since we need to access two outputs from it and produce just one\n        # from this query\n        return pgast.SelectStmt(\n            from_clause=[rvar],\n            target_list=[pgast.ResTarget(val=maybe_raise)],\n        )\n    else:\n        return maybe_raise\n\n\ndef process_link_update(\n    *,\n    ir_stmt: irast.MutatingStmt,\n    ir_set: irast.SetE[irast.Pointer],\n    shape_op: qlast.ShapeOp = qlast.ShapeOp.ASSIGN,\n    source_typeref: irast.TypeRef,\n    dml_cte: pgast.CommonTableExpr,\n    iterator: Optional[pgast.IteratorCTE] = None,\n    ctx: context.CompilerContextLevel,\n    policy_ctx: Optional[context.CompilerContextLevel],\n) -> tuple[Optional[pgast.CommonTableExpr], Optional[pgast.CommonTableExpr]]:\n    \"\"\"Perform updates to a link relation as part of a DML statement.\n\n    Args:\n        ir_stmt:\n            IR of the DML statement.\n        ir_set:\n            IR of the INSERT/UPDATE body element.\n        shape_op:\n            The operation of the UPDATE body element (:=, +=, -=).  For\n            INSERT this should always be :=.\n        source_typeref:\n            An ir.TypeRef instance representing the specific type of an object\n            being updated.\n        dml_cte:\n            CTE representing the SQL INSERT or UPDATE to the main\n            relation of the DML subject.\n        iterator:\n            IR and CTE representing the iterator range in the FOR clause\n            of the EdgeQL DML statement (if present).\n        policy_ctx:\n            Optionally, a context in which to populate overlays that\n            use the select CTE for overlays instead of the\n            actual insert CTE. This is needed if an access policy is to\n            be applied, and requires disabling a potential optimization.\n\n            We need separate overlay contexts because default values for\n            link properties don't currently get populated in our IR, so we\n            need to do actual SQL DML to get their values. (And so we disallow\n            their use in policies.)\n    \"\"\"\n    toplevel = ctx.toplevel_stmt\n    is_insert = isinstance(ir_stmt, irast.InsertStmt)\n\n    rptr = ir_set.expr\n    ptrref = rptr.ptrref\n    assert isinstance(ptrref, irast.PointerRef)\n    target_is_scalar = not irtyputils.is_object(ir_set.typeref)\n    path_id = ir_set.path_id\n\n    # The links in the dml class shape have been derived,\n    # but we must use the correct specialized link class for the\n    # base material type.\n    mptrref = irtyputils.find_actual_ptrref(source_typeref, ptrref)\n    assert isinstance(mptrref, irast.PointerRef)\n\n    target_rvar = relctx.range_for_ptrref(\n        mptrref, for_mutation=True, only_self=True, ctx=ctx)\n    assert isinstance(target_rvar, pgast.RelRangeVar)\n    assert isinstance(target_rvar.relation, pgast.Relation)\n    target_alias = target_rvar.alias.aliasname\n\n    dml_cte_rvar = pgast.RelRangeVar(\n        relation=dml_cte,\n        alias=pgast.Alias(\n            aliasname=ctx.env.aliases.get('m')\n        )\n    )\n\n    # Turn the IR of the expression on the right side of :=\n    # into a subquery returning records for the link table.\n    data_cte, specified_cols = process_link_values(\n        ir_stmt=ir_stmt,\n        ir_expr=ir_set,\n        dml_rvar=dml_cte_rvar,\n        source_typeref=source_typeref,\n        target_is_scalar=target_is_scalar,\n        enforce_cardinality=(shape_op is qlast.ShapeOp.ASSIGN),\n        dml_cte=dml_cte,\n        iterator=iterator,\n        ctx=ctx,\n    )\n\n    toplevel.append_cte(data_cte)\n\n    delqry: Optional[pgast.DeleteStmt]\n\n    data_select = pgast.SelectStmt(\n        target_list=[\n            pgast.ResTarget(\n                val=pgast.ColumnRef(\n                    name=[data_cte.name, pgast.Star()]\n                ),\n            ),\n        ],\n        from_clause=[\n            pgast.RelRangeVar(relation=data_cte),\n        ],\n    )\n\n    if not is_insert and shape_op is not qlast.ShapeOp.APPEND:\n\n        source_ref = pathctx.get_rvar_path_identity_var(\n            dml_cte_rvar,\n            ir_stmt.subject.path_id,\n            env=ctx.env,\n        )\n\n        if shape_op is qlast.ShapeOp.SUBTRACT:\n            data_rvar = relctx.rvar_for_rel(data_select, ctx=ctx)\n\n            if target_is_scalar:\n                # MULTI properties are not distinct, and since `-=` must\n                # be a proper inverse of `+=` we cannot simply DELETE\n                # all property values matching the `-=` expression, and\n                # instead have to resort to careful deletion of no more\n                # than the number of tuples returned by the expression.\n                # Here, we rely on the \"ctid\" system column to refer to\n                # specific tuples.\n                #\n                # DELETE\n                #   FROM <link-tab>\n                # WHERE\n                #   ctid IN (\n                #     SELECT\n                #       shortlist.ctid\n                #     FROM\n                #       (SELECT\n                #         source,\n                #         target,\n                #         count(target) AS cnt\n                #        FROM\n                #         <data-expr>\n                #        GROUP BY source, target\n                #       ) AS counts,\n                #       LATERAL (\n                #         SELECT\n                #           candidates.ctid\n                #         FROM\n                #           (SELECT\n                #             ctid,\n                #             row_number() OVER (\n                #               PARTITION BY data\n                #               ORDER BY data\n                #             ) AS rn\n                #           FROM\n                #             <link-tab>\n                #           WHERE\n                #             source = counts.source\n                #             AND target = counts.target\n                #           ) AS candidates\n                #         WHERE\n                #           candidates.rn <= counts.cnt\n                #       ) AS shortlist\n                #   );\n\n                val_src_ref = pgast.ColumnRef(\n                    name=[data_rvar.alias.aliasname, 'source'],\n                )\n                val_tgt_ref = pgast.ColumnRef(\n                    name=[data_rvar.alias.aliasname, 'target'],\n                )\n                counts_select = pgast.SelectStmt(\n                    target_list=[\n                        pgast.ResTarget(name='source', val=val_src_ref),\n                        pgast.ResTarget(name='target', val=val_tgt_ref),\n                        pgast.ResTarget(\n                            name='cnt',\n                            val=pgast.FuncCall(\n                                name=('count',),\n                                args=[val_tgt_ref],\n                            ),\n                        ),\n                    ],\n                    from_clause=[data_rvar],\n                    group_clause=[val_src_ref, val_tgt_ref],\n                )\n\n                counts_rvar = relctx.rvar_for_rel(counts_select, ctx=ctx)\n                counts_alias = counts_rvar.alias.aliasname\n\n                target_ref = pgast.ColumnRef(name=[target_alias, 'target'])\n\n                candidates_select = pgast.SelectStmt(\n                    target_list=[\n                        pgast.ResTarget(\n                            name='ctid',\n                            val=pgast.ColumnRef(\n                                name=[target_alias, 'ctid'],\n                            ),\n                        ),\n                        pgast.ResTarget(\n                            name='rn',\n                            val=pgast.FuncCall(\n                                name=('row_number',),\n                                args=[],\n                                over=pgast.WindowDef(\n                                    partition_clause=[target_ref],\n                                    order_clause=[\n                                        pgast.SortBy(node=target_ref),\n                                    ],\n                                ),\n                            ),\n                        ),\n                    ],\n                    from_clause=[target_rvar],\n                    where_clause=astutils.new_binop(\n                        lexpr=astutils.new_binop(\n                            lexpr=pgast.ColumnRef(\n                                name=[counts_alias, 'source'],\n                            ),\n                            op='=',\n                            rexpr=pgast.ColumnRef(\n                                name=[target_alias, 'source'],\n                            ),\n                        ),\n                        op='AND',\n                        rexpr=astutils.new_binop(\n                            lexpr=target_ref,\n                            op='=',\n                            rexpr=pgast.ColumnRef(\n                                name=[counts_alias, 'target']),\n                        ),\n                    ),\n                )\n\n                candidates_rvar = relctx.rvar_for_rel(\n                    candidates_select, ctx=ctx)\n\n                candidates_alias = candidates_rvar.alias.aliasname\n\n                shortlist_select = pgast.SelectStmt(\n                    target_list=[\n                        pgast.ResTarget(\n                            name='ctid',\n                            val=pgast.ColumnRef(\n                                name=[candidates_alias, 'ctid'],\n                            ),\n                        ),\n                    ],\n                    from_clause=[candidates_rvar],\n                    where_clause=astutils.new_binop(\n                        lexpr=pgast.ColumnRef(name=[candidates_alias, 'rn']),\n                        op='<=',\n                        rexpr=pgast.ColumnRef(name=[counts_alias, 'cnt']),\n                    ),\n                )\n\n                shortlist_rvar = relctx.rvar_for_rel(\n                    shortlist_select, lateral=True, ctx=ctx)\n                shortlist_alias = shortlist_rvar.alias.aliasname\n\n                ctid_select = pgast.SelectStmt(\n                    target_list=[\n                        pgast.ResTarget(\n                            name='ctid',\n                            val=pgast.ColumnRef(name=[shortlist_alias, 'ctid'])\n                        ),\n                    ],\n                    from_clause=[\n                        counts_rvar,\n                        shortlist_rvar,\n                    ],\n                )\n\n                delqry = pgast.DeleteStmt(\n                    relation=target_rvar,\n                    where_clause=astutils.new_binop(\n                        lexpr=pgast.ColumnRef(\n                            name=[target_alias, 'ctid'],\n                        ),\n                        op='=',\n                        rexpr=pgast.SubLink(\n                            operator=\"ANY\",\n                            expr=ctid_select,\n                        ),\n                    ),\n                    returning_list=[\n                        pgast.ResTarget(\n                            val=pgast.ColumnRef(\n                                name=[target_alias, pgast.Star()],\n                            ),\n                        )\n                    ]\n                )\n            else:\n                # Links are always distinct, so we can simply\n                # DELETE the tuples matching the `-=` expression.\n                delqry = pgast.DeleteStmt(\n                    relation=target_rvar,\n                    where_clause=astutils.new_binop(\n                        lexpr=astutils.new_binop(\n                            lexpr=source_ref,\n                            op='=',\n                            rexpr=pgast.ColumnRef(\n                                name=[target_alias, 'source'],\n                            ),\n                        ),\n                        op='AND',\n                        rexpr=astutils.new_binop(\n                            lexpr=pgast.ColumnRef(\n                                name=[target_alias, 'target'],\n                            ),\n                            op='=',\n                            rexpr=pgast.ColumnRef(\n                                name=[data_rvar.alias.aliasname, 'target'],\n                            ),\n                        ),\n                    ),\n                    using_clause=[\n                        dml_cte_rvar,\n                        data_rvar,\n                    ],\n                    returning_list=[\n                        pgast.ResTarget(\n                            val=pgast.ColumnRef(\n                                name=[target_alias, pgast.Star()],\n                            ),\n                        )\n                    ]\n                )\n        else:\n            # Drop all previous link records for this source.\n            delqry = pgast.DeleteStmt(\n                relation=target_rvar,\n                where_clause=astutils.new_binop(\n                    lexpr=source_ref,\n                    op='=',\n                    rexpr=pgast.ColumnRef(\n                        name=[target_alias, 'source'],\n                    ),\n                ),\n                using_clause=[dml_cte_rvar],\n                returning_list=[\n                    pgast.ResTarget(\n                        val=pgast.ColumnRef(\n                            name=[target_alias, pgast.Star()],\n                        ),\n                    )\n                ]\n            )\n\n        delcte = pgast.CommonTableExpr(\n            name=ctx.env.aliases.get(hint='link_upd_del'),\n            query=delqry,\n            for_dml_stmt=ctx.get_current_dml_stmt(),\n        )\n\n        if shape_op is not qlast.ShapeOp.SUBTRACT:\n            # Correlate the deletion with INSERT to make sure\n            # link properties get erased properly and we aren't\n            # just ON CONFLICT UPDATE-ing the link rows.\n            # This basically just tacks on a\n            #    WHERE (SELECT count(*) FROM delcte) IS NOT NULL)\n            del_select = pgast.SelectStmt(\n                target_list=[\n                    pgast.ResTarget(\n                        val=pgast.FuncCall(\n                            name=['count'],\n                            args=[pgast.ColumnRef(name=[pgast.Star()])],\n                        ),\n                    ),\n                ],\n                from_clause=[\n                    pgast.RelRangeVar(relation=delcte),\n                ],\n            )\n\n            data_select.where_clause = astutils.extend_binop(\n                data_select.where_clause,\n                pgast.NullTest(arg=del_select, negated=True),\n            )\n\n        pathctx.put_path_value_rvar(\n            delcte.query, path_id.ptr_path(), target_rvar\n        )\n\n        # Record the effect of this removal in the relation overlay\n        # context to ensure that references to the link in the result\n        # of this DML statement yield the expected results.\n        except_overlay = (lambda octx:\n            relctx.add_ptr_rel_overlay(\n                mptrref,\n                context.OverlayOp.EXCEPT,\n                delcte,\n                path_id=path_id.ptr_path(),\n                dml_stmts=ctx.dml_stmt_stack,\n                ctx=octx,\n            )\n        )\n        except_overlay(ctx)\n        if policy_ctx:\n            except_overlay(policy_ctx)\n\n        toplevel.append_cte(delcte)\n    else:\n        delqry = None\n\n    if shape_op is qlast.ShapeOp.SUBTRACT:\n        if mptrref.dir_cardinality(rptr.direction).can_be_zero():\n            # The pointer is OPTIONAL, no checks or further processing\n            # is needed.\n            return None, None\n        else:\n            # The pointer is REQUIRED, so we must take the result of\n            # the subtraction produced by the \"delcte\" above, apply it\n            # as a subtracting overlay, and re-compute the pointer relation\n            # to see if there are any newly created empty sets.\n            #\n            # The actual work is done via raise_on_null injection performed\n            # by \"process_link_values()\" below (hence \"enforce_cardinality\").\n            #\n            # The other part of this enforcement is in doing it when a\n            # target is deleted and the link policy is ALLOW. This is\n            # handled in _get_outline_link_trigger_proc_text in\n            # pgsql/delta.py.\n\n            # Turn `foo := <expr>` into just `foo`.\n            ptr_ref_set = irast.Set(\n                path_id=ir_set.path_id,\n                path_scope_id=ir_set.path_scope_id,\n                typeref=ir_set.typeref,\n                expr=ir_set.expr.replace(expr=None),\n            )\n            assert irutils.is_set_instance(ptr_ref_set, irast.Pointer)\n\n            with ctx.new() as subctx:\n                # TODO: Do we really need a copy here? things /seem/\n                # to work without it\n                subctx.rel_overlays = subctx.rel_overlays.copy()\n                relctx.add_ptr_rel_overlay(\n                    ptrref,\n                    context.OverlayOp.EXCEPT,\n                    delcte,\n                    path_id=path_id.ptr_path(),\n                    ctx=subctx\n                )\n\n                check_cte, _ = process_link_values(\n                    ir_stmt=ir_stmt,\n                    ir_expr=ptr_ref_set,\n                    dml_rvar=dml_cte_rvar,\n                    source_typeref=source_typeref,\n                    target_is_scalar=target_is_scalar,\n                    enforce_cardinality=True,\n                    dml_cte=dml_cte,\n                    iterator=iterator,\n                    ctx=subctx,\n                )\n\n                toplevel.append_cte(check_cte)\n\n            return None, check_cte\n\n    cols = [pgast.ColumnRef(name=[col]) for col in specified_cols]\n    conflict_cols = ['source', 'target']\n\n    if is_insert or target_is_scalar:\n        conflict_clause = None\n    elif (\n        len(cols) == len(conflict_cols)\n        and delqry is not None\n        and not policy_ctx\n    ):\n        # There are no link properties, so we can optimize the\n        # link replacement operation by omitting the overlapping\n        # link rows from deletion.\n        filter_select = pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    val=pgast.ColumnRef(name=['source']),\n                ),\n                pgast.ResTarget(\n                    val=pgast.ColumnRef(name=['target']),\n                ),\n            ],\n            from_clause=[pgast.RelRangeVar(relation=data_cte)],\n        )\n\n        delqry.where_clause = astutils.extend_binop(\n            delqry.where_clause,\n            astutils.new_binop(\n                lexpr=pgast.ImplicitRowExpr(\n                    args=[\n                        pgast.ColumnRef(name=['source']),\n                        pgast.ColumnRef(name=['target']),\n                    ],\n                ),\n                rexpr=pgast.SubLink(\n                    operator=\"ALL\",\n                    expr=filter_select,\n                ),\n                op='!=',\n            )\n        )\n\n        conflict_clause = pgast.OnConflictClause(\n            action=pgast.OnConflictAction.DO_NOTHING,\n            target=pgast.OnConflictTarget(\n                index_elems=[\n                    pgast.IndexElem(expr=pgast.ColumnRef(name=[col]))\n                    for col in conflict_cols\n                ]\n            ),\n        )\n    else:\n        # Inserting rows into the link table may produce cardinality\n        # constraint violations, since the INSERT into the link table\n        # is executed in the snapshot where the above DELETE from\n        # the link table is not visible.  Hence, we need to use\n        # the ON CONFLICT clause to resolve this.\n        conflict_inference = [\n            pgast.IndexElem(expr=pgast.ColumnRef(name=[col]))\n            for col in conflict_cols\n        ]\n\n        target_cols = [\n            col.name[0]\n            for col in cols\n            if isinstance(col.name[0], str) and col.name[0] not in conflict_cols\n        ]\n\n        if len(target_cols) == 0:\n            conflict_clause = pgast.OnConflictClause(\n                action=pgast.OnConflictAction.DO_NOTHING,\n                target=pgast.OnConflictTarget(\n                    index_elems=conflict_inference\n                )\n            )\n        else:\n            conflict_data = pgast.RowExpr(\n                args=[\n                    pgast.ColumnRef(name=['excluded', col])\n                    for col in target_cols\n                ],\n            )\n            conflict_clause = pgast.OnConflictClause(\n                action=pgast.OnConflictAction.DO_UPDATE,\n                target=pgast.OnConflictTarget(\n                    index_elems=conflict_inference\n                ),\n                update_list=[\n                    pgast.MultiAssignRef(\n                        columns=target_cols,\n                        source=conflict_data\n                    )\n                ]\n            )\n\n    update = pgast.CommonTableExpr(\n        name=ctx.env.aliases.get(hint='link_upd_ins'),\n        query=pgast.InsertStmt(\n            relation=target_rvar,\n            select_stmt=data_select,\n            cols=[\n                pgast.InsertTarget(name=downcast(str, col.name[0]))\n                for col in cols\n            ],\n            on_conflict=conflict_clause,\n            returning_list=[\n                pgast.ResTarget(\n                    val=pgast.ColumnRef(name=[pgast.Star()])\n                )\n            ]\n        ),\n        for_dml_stmt=ctx.get_current_dml_stmt(),\n    )\n\n    pathctx.put_path_value_rvar(update.query, path_id.ptr_path(), target_rvar)\n\n    def register_overlays(\n        overlay_cte: pgast.CommonTableExpr, octx: context.CompilerContextLevel\n    ) -> None:\n        assert isinstance(mptrref, irast.PointerRef)\n        # Record the effect of this insertion in the relation overlay\n        # context to ensure that references to the link in the result\n        # of this DML statement yield the expected results.\n        if shape_op is qlast.ShapeOp.APPEND and not target_is_scalar:\n            # When doing an UPDATE with +=, we need to do an anti-join\n            # based filter to filter out links that were already present\n            # and have been re-added.\n            relctx.add_ptr_rel_overlay(\n                mptrref,\n                context.OverlayOp.FILTER,\n                overlay_cte,\n                dml_stmts=ctx.dml_stmt_stack,\n                path_id=path_id.ptr_path(),\n                ctx=octx\n            )\n\n        relctx.add_ptr_rel_overlay(\n            mptrref,\n            context.OverlayOp.UNION,\n            overlay_cte,\n            dml_stmts=ctx.dml_stmt_stack,\n            path_id=path_id.ptr_path(),\n            ctx=octx\n        )\n\n    if policy_ctx:\n        register_overlays(data_cte, policy_ctx)\n\n    register_overlays(update, ctx)\n\n    return update, None\n\n\ndef process_link_values(\n    *,\n    ir_stmt: irast.MutatingStmt,\n    ir_expr: irast.SetE[irast.Pointer],\n    dml_rvar: pgast.PathRangeVar,\n    dml_cte: pgast.CommonTableExpr,\n    source_typeref: irast.TypeRef,\n    target_is_scalar: bool,\n    enforce_cardinality: bool,\n    iterator: Optional[pgast.IteratorCTE],\n    ctx: context.CompilerContextLevel,\n) -> tuple[pgast.CommonTableExpr, list[str]]:\n    \"\"\"Produce a pointer relation for a given body element of an INSERT/UPDATE.\n\n    Given an INSERT/UPDATE body shape element that mutates a MULTI pointer,\n    produce a (source, target [, link properties]) relation as a CTE and\n    return it along with a list of relation attribute names.\n\n    Args:\n        ir_stmt:\n            IR of the DML statement.\n        ir_set:\n            IR of the INSERT/UPDATE body element.\n        dml_rvar:\n            The RangeVar over the SQL INSERT/UPDATE of the main relation\n            of the object being updated.\n        dml_cte:\n            CTE representing the SQL INSERT or UPDATE to the main\n            relation of the DML subject.\n        source_typeref:\n            An ir.TypeRef instance representing the specific type of an object\n            being updated.\n        target_is_scalar:\n            True, if mutating a property, False if a link.\n        enforce_cardinality:\n            Whether an explicit empty set check should be generated.\n            Used for REQUIRED pointers.\n        iterator:\n            IR and CTE representing the iterator range in the FOR clause\n            of the EdgeQL DML statement (if present).\n\n    Returns:\n        A tuple containing the pointer relation CTE and a list of attribute\n        names in it.\n    \"\"\"\n    old_dml_count = len(ctx.dml_stmts)\n    with ctx.newrel() as subrelctx:\n        # For inserts, we need to use the main DML statement as the\n        # iterator, while for updates, we need to use the DML range\n        # CTE as the iterator (and so arrange for it to be passed in).\n        # This is because, for updates, we need to execute any nested\n        # DML once for each row in the range over all types, while\n        # dml_cte contains just one subtype.\n        if isinstance(ir_stmt, irast.InsertStmt):\n            subrelctx.enclosing_cte_iterator = pgast.IteratorCTE(\n                path_id=ir_stmt.subject.path_id, cte=dml_cte,\n                parent=iterator)\n        else:\n            subrelctx.enclosing_cte_iterator = iterator\n        row_query = subrelctx.rel\n\n        merge_iterator(iterator, row_query, ctx=subrelctx)\n\n        relctx.include_rvar(row_query, dml_rvar, pull_namespace=False,\n                            path_id=ir_stmt.subject.path_id, ctx=subrelctx)\n        subrelctx.path_scope[ir_stmt.subject.path_id] = row_query\n\n        ir_rptr = ir_expr.expr\n        ptrref = ir_rptr.ptrref\n        if ptrref.material_ptr is not None:\n            ptrref = ptrref.material_ptr\n        assert isinstance(ptrref, irast.PointerRef)\n        ptr_is_multi_required = (\n            ptrref.out_cardinality == qltypes.Cardinality.AT_LEAST_ONE\n        )\n\n        with subrelctx.newscope() as sctx, sctx.subrel() as input_rel_ctx:\n            input_rel = input_rel_ctx.rel\n            input_rel_ctx.expr_exposed = False\n            input_rel_ctx.volatility_ref = (\n                lambda _stmt, _ctx: pathctx.get_path_identity_var(\n                    row_query, ir_stmt.subject.path_id,\n                    env=input_rel_ctx.env),)\n\n            # Check if some nested Set provides a shape that is\n            # visible here.\n            shape_expr = ir_expr.shape_source or ir_expr\n            # Register that this shape needs to be compiled for use by DML,\n            # so that the values will be there for us to grab later.\n            input_rel_ctx.shapes_needed_by_dml.add(shape_expr)\n\n            if ptr_is_multi_required and enforce_cardinality:\n                input_rel_ctx.force_optional |= {ir_expr.path_id}\n\n            dispatch.visit(ir_expr, ctx=input_rel_ctx)\n\n    input_stmt: pgast.Query = input_rel\n\n    input_rvar = pgast.RangeSubselect(\n        subquery=input_rel,\n        lateral=True,\n        alias=pgast.Alias(\n            aliasname=ctx.env.aliases.get('val')\n        )\n    )\n\n    if len(ctx.dml_stmts) > old_dml_count:\n        # If there were any nested inserts, we need to join them in.\n        pathctx.put_rvar_path_bond(input_rvar, ir_stmt.subject.path_id)\n    relctx.include_rvar(row_query, input_rvar,\n                        path_id=ir_stmt.subject.path_id,\n                        ctx=ctx)\n\n    source_data: dict[str, tuple[irast.PathId, pgast.BaseExpr]] = {}\n\n    if isinstance(input_stmt, pgast.SelectStmt) and input_stmt.op is not None:\n        # UNION\n        assert input_stmt.rarg\n        input_stmt = input_stmt.rarg\n\n    path_id = ir_expr.path_id\n\n    target_ref: pgast.BaseExpr\n    if shape_expr.shape:\n        for element, _ in shape_expr.shape:\n            if not element.path_id.is_linkprop_path():\n                continue\n            val = pathctx.get_rvar_path_value_var(\n                input_rvar, element.path_id, env=ctx.env)\n            rptr = element.path_id.rptr()\n            assert isinstance(rptr, irast.PointerRef)\n            actual_rptr = irtyputils.find_actual_ptrref(source_typeref, rptr)\n            ptr_info = pg_types.get_ptrref_storage_info(actual_rptr)\n            real_path_id = path_id.ptr_path().extend(ptrref=actual_rptr)\n            source_data.setdefault(\n                ptr_info.column_name, (real_path_id, val))\n\n        if not target_is_scalar and 'target' not in source_data:\n            target_ref = pathctx.get_rvar_path_identity_var(\n                input_rvar, path_id, env=ctx.env)\n\n    else:\n        if target_is_scalar:\n            target_ref = pathctx.get_rvar_path_value_var(\n                input_rvar, path_id, env=ctx.env)\n            target_ref = output.output_as_value(target_ref, env=ctx.env)\n        else:\n            target_ref = pathctx.get_rvar_path_identity_var(\n                input_rvar, path_id, env=ctx.env)\n\n    if isinstance(ir_stmt, irast.UpdateStmt) and not target_is_scalar:\n        actual_ptrref = irtyputils.find_actual_ptrref(source_typeref, ptrref)\n        target_ref = check_update_type(\n            target_ref,\n            input_rvar,\n            is_subquery=False,\n            ir_stmt=ir_stmt,\n            ir_set=ir_expr,\n            shape_ptrref=ptrref,\n            actual_ptrref=actual_ptrref,\n            ctx=ctx,\n        )\n\n    if ptr_is_multi_required and enforce_cardinality:\n        target_ref = pgast.FuncCall(\n            name=astutils.edgedb_func('raise_on_null', ctx=ctx),\n            args=[\n                target_ref,\n                pgast.StringConstant(val='not_null_violation'),\n                pgast.NamedFuncArg(\n                    name='msg',\n                    val=pgast.StringConstant(val='missing value'),\n                ),\n                pgast.NamedFuncArg(\n                    name='column',\n                    val=pgast.StringConstant(val=str(ptrref.id)),\n                ),\n            ],\n        )\n\n    source_data['target'] = (path_id, target_ref)\n\n    row_query.target_list.append(\n        pgast.ResTarget(\n            name='source',\n            val=pathctx.get_rvar_path_identity_var(\n                dml_rvar,\n                ir_stmt.subject.path_id,\n                env=ctx.env,\n            ),\n        ),\n    )\n\n    specified_cols = ['source']\n    for col, (col_path_id, expr) in source_data.items():\n        row_query.target_list.append(\n            pgast.ResTarget(\n                val=expr,\n                name=col,\n            ),\n        )\n        specified_cols.append(col)\n        # XXX: This is dodgy. Do we need to do the dynamic rvar thing?\n        # XXX: And can we make defaults work?\n        pathctx._put_path_output_var(\n            row_query, col_path_id, aspect=pgce.PathAspect.VALUE,\n            var=pgast.ColumnRef(name=[col]),\n        )\n\n    link_rows = pgast.CommonTableExpr(\n        query=row_query,\n        name=ctx.env.aliases.get(hint='r'),\n        for_dml_stmt=ctx.get_current_dml_stmt(),\n    )\n\n    return link_rows, specified_cols\n\n\ndef process_delete_body(\n    *,\n    ir_stmt: irast.DeleteStmt,\n    delete_cte: pgast.CommonTableExpr,\n    typeref: irast.TypeRef,\n    ctx: context.CompilerContextLevel,\n) -> None:\n    \"\"\"Finalize DELETE on an object.\n\n    The actual DELETE was generated in gen_dml_cte, so we only\n    have work to do here if there are link tables to clean up.\n    \"\"\"\n    ctx.toplevel_stmt.append_cte(delete_cte)\n\n    put_iterator_bond(ctx.enclosing_cte_iterator, delete_cte.query)\n\n    pointers = ir_stmt.links_to_delete[typeref.id]\n\n    for ptrref in pointers:\n        target_rvar = relctx.range_for_ptrref(\n            ptrref, for_mutation=True, only_self=True, ctx=ctx)\n        assert isinstance(target_rvar, pgast.RelRangeVar)\n\n        range_rvar = pgast.RelRangeVar(\n            relation=delete_cte,\n            alias=pgast.Alias(\n                aliasname=ctx.env.aliases.get(hint='range')\n            )\n        )\n\n        where_clause = astutils.new_binop(\n            lexpr=pgast.ColumnRef(name=[\n                target_rvar.alias.aliasname, 'source'\n            ]),\n            op='=',\n            rexpr=pathctx.get_rvar_path_identity_var(\n                range_rvar, ir_stmt.result.path_id, env=ctx.env)\n        )\n        del_query = pgast.DeleteStmt(\n            relation=target_rvar,\n            where_clause=where_clause,\n            using_clause=[range_rvar],\n        )\n        ctx.toplevel_stmt.append_cte(pgast.CommonTableExpr(\n            query=del_query,\n            name=ctx.env.aliases.get(hint='mlink'),\n            for_dml_stmt=ctx.get_current_dml_stmt(),\n        ))\n\n\n# Trigger compilation\ndef compile_triggers(\n    triggers: tuple[tuple[irast.Trigger, ...], ...],\n    stmt: pgast.Base,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> None:\n    if not triggers:\n        return\n    assert isinstance(stmt, pgast.Query)\n\n    if stmt.ctes is None:\n        stmt.ctes = []\n    start_ctes = len(stmt.ctes)\n\n    with ctx.new() as ictx:\n        # Clear out type_ctes so that we will recompile them all with\n        # our overlays baked in (trigger_mode = True causes the\n        # overlays to be included), so that access policies still\n        # apply to our \"new view\" of the database.\n        # FIXME: I think we actually need to keep the old type_ctes\n        # available for pointers off of __old__ to use.\n        ictx.trigger_mode = True\n        ictx.type_rewrite_ctes = {}\n        ictx.type_inheritance_ctes = {}\n        ictx.ordered_type_ctes = []\n        ictx.toplevel_stmt = stmt\n\n        for stage in triggers:\n            new_overlays = []\n            for trigger in stage:\n                ictx.path_scope = ctx.path_scope.new_child()\n                new_overlays.append(compile_trigger(trigger, ctx=ictx))\n\n            for overlay in new_overlays:\n                ictx.rel_overlays.type = (\n                    ictx.rel_overlays.type.update(overlay.type))\n                ictx.rel_overlays.ptr = (\n                    ictx.rel_overlays.ptr.update(overlay.ptr))\n\n    # Install any newly created type CTEs before the CTEs created from\n    # this trigger compilation but after anything compiled before.\n    stmt.ctes[start_ctes:start_ctes] = ictx.ordered_type_ctes\n\n\ndef compile_trigger(\n    trigger: irast.Trigger,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> context.RelOverlays:\n    # N.B: The *base type* overlays have the whole union, while subtypes\n    # just have subtype things.\n    # The things we produce for `affected` take this into account.\n\n    new_path = trigger.new_set.path_id\n    old_path = trigger.old_set.path_id if trigger.old_set else None\n\n    # We use overlays to drive the trigger, since with a bit of\n    # tweaking, they contain all the relevant information.\n    overlays: list[context.OverlayEntry] = []\n    for typeref, dml in trigger.affected:\n        toverlays = ctx.rel_overlays.type[dml]\n        if ov := toverlays.get(typeref.id):\n            overlays.extend(ov)\n\n    # Handle deletions by turning except into union\n    # Drop FILTER, which is included by update but doesn't help us here\n    overlays = [\n        (\n            (context.OverlayOp.UNION, *x[1:])\n            if x[0] == context.OverlayOp.EXCEPT\n            else x\n        )\n        for x in overlays\n        if x[0] != context.OverlayOp.FILTER\n    ]\n    # Replace an initial union with REPLACE, since we *don't* want whatever\n    # already existed\n    assert overlays and overlays[0][0] == context.OverlayOp.UNION\n    overlays[0] = (context.OverlayOp.REPLACE, *overlays[0][1:])\n\n    # Produce a CTE containing all of the affected objects for this trigger\n    with ctx.newrel() as ictx:\n        ictx.rel_overlays = context.RelOverlays()\n        ictx.rel_overlays.type = immu.Map({\n            None: immu.Map({trigger.source_type.id: tuple(overlays)})\n        })\n\n        # The range produced here will be driven just by the overlays\n        rvar = relctx.range_for_material_objtype(\n            trigger.source_type,\n            new_path,\n            include_overlays=True,\n            ignore_rewrites=True,\n            ctx=ictx,\n        )\n        relctx.include_rvar(\n            ictx.rel, rvar, path_id=new_path, ctx=ictx\n        )\n\n        # If __old__ is available, we register its identity/value,\n        # but *not* its source.\n        if old_path:\n            new_ident = pathctx.get_path_identity_var(\n                ictx.rel, new_path, env=ctx.env\n            )\n            pathctx.put_path_identity_var(ictx.rel, old_path, new_ident)\n            pathctx.put_path_value_var(ictx.rel, old_path, new_ident)\n\n        contents_cte = pgast.CommonTableExpr(\n            query=ictx.rel,\n            name=ctx.env.aliases.get('trig_contents'),\n            materialized=True,  # XXX: or not?\n            for_dml_stmt=ctx.get_current_dml_stmt(),\n        )\n        ictx.toplevel_stmt.append_cte(contents_cte)\n\n    # Actually compile the trigger\n    with ctx.newrel() as tctx:\n        # With FOR EACH, we use the iterator machinery to iterate over\n        # all of the objects\n        if trigger.scope == qltypes.TriggerScope.Each:\n            tctx.enclosing_cte_iterator = pgast.IteratorCTE(\n                path_id=new_path,\n                cte=contents_cte,\n                parent=None,\n                # old_path gets registered as also appearing in the\n                # iterator cte, and so will get included whenever\n                # merged\n                other_paths=(\n                    ((old_path, pgce.PathAspect.IDENTITY),)\n                    if old_path else\n                    ()\n                ),\n            )\n            merge_iterator(tctx.enclosing_cte_iterator, tctx.rel, ctx=ctx)\n\n        # While with FOR ALL, we register the sets as external rels\n        else:\n            tctx.external_rels = dict(tctx.external_rels)\n            # new_path is just the contents_cte\n            tctx.external_rels[new_path] = (\n                contents_cte,\n                (pgce.PathAspect.VALUE, pgce.PathAspect.SOURCE)\n            )\n            if old_path:\n                # old_path is *also* the contents_cte, but without a source\n                # aspect, so we need to include the real database back in.\n                tctx.external_rels[old_path] = (\n                    contents_cte,\n                    (pgce.PathAspect.VALUE, pgce.PathAspect.IDENTITY,)\n                )\n\n        # This is somewhat subtle: we merge *every* DML into\n        # the \"None\" overlay, so that all the new database state shows\n        # up everywhere...  but __old__ has a TriggerAnchor set up in\n        # it, which acts like a dml statement, and *diverts* __old__\n        # away from the new data!\n\n        # We grab the list of DML out of dml_stmts instead of just\n        # from the overlays for determinism reasons; it effects the\n        # order overlays appear in\n        all_dml = [\n            x for x in ctx.dml_stmts if isinstance(x, irast.MutatingStmt)]\n        merge_overlays_globally(all_dml, ctx=tctx)\n\n        # Strip out everything but None. This tidies things up and makes\n        # it easy to detect new additions.\n        tctx.rel_overlays.type = immu.Map({None: tctx.rel_overlays.type[None]})\n        tctx.rel_overlays.ptr = immu.Map({None: tctx.rel_overlays.ptr[None]})\n\n        # Copy over the global overlay to __new__, since it should see\n        # the new data also.\n        # TODO: We should consider building a dedicated __new__overlay\n        # in order to reduce overlay sizes in common cases\n        assert isinstance(trigger.new_set.expr, irast.TriggerAnchor)\n        tctx.rel_overlays.type = tctx.rel_overlays.type.set(\n            trigger.new_set.expr, tctx.rel_overlays.type[None])\n        tctx.rel_overlays.ptr = tctx.rel_overlays.ptr.set(\n            trigger.new_set.expr, tctx.rel_overlays.ptr[None])\n\n        # N.B: Any DML in the trigger will have the \"global\" overlay (None)\n        # as its starting point.\n        dispatch.compile(trigger.expr, ctx=tctx)\n        # Force the value to get output so that if it might error\n        # it will be forced up by check_ctes\n        pathctx.get_path_value_output(\n            tctx.rel, trigger.expr.path_id, env=ctx.env)\n        pathctx.get_path_serialized_output(\n            tctx.rel, trigger.expr.path_id, env=ctx.env)\n\n        # If the expression is *just* DML, as an optimization, skip\n        # generating a CTE for the expression and forcing its evaluation\n        # with check_ctes. The actual work is all in a DML CTE so we\n        # don't need to worry about anything more.\n        if (\n            not isinstance(trigger.expr.expr, irast.MutatingStmt)\n            and not trigger.expr.shape\n        ):\n            trigger_cte = pgast.CommonTableExpr(\n                query=tctx.rel,\n                name=ctx.env.aliases.get('trig_body'),\n                materialized=True,  # XXX: or not?\n                for_dml_stmt=ctx.get_current_dml_stmt(),\n            )\n            tctx.toplevel_stmt.append_cte(trigger_cte)\n            tctx.env.check_ctes.append(trigger_cte)\n\n    saved_overlays = tctx.rel_overlays.copy()\n    saved_overlays.type = saved_overlays.type.delete(trigger.new_set.expr)\n    saved_overlays.ptr = saved_overlays.ptr.delete(trigger.new_set.expr)\n    return saved_overlays\n"
  },
  {
    "path": "edb/pgsql/compiler/enums.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom edb.common import enum as s_enum\n\n\nclass PathAspect(s_enum.StrEnum):\n    IDENTITY = 'identity'\n    VALUE = 'value'\n    SOURCE = 'source'\n    SERIALIZED = 'serialized'\n    ITERATOR = 'iterator'\n"
  },
  {
    "path": "edb/pgsql/compiler/expr.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Compilation handlers for non-statement expressions.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Optional, Sequence\n\nfrom edb import errors\n\nfrom edb.edgeql import qltypes as ql_ft\nfrom edb.edgeql import ast as qlast\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtyputils\nfrom edb.ir import utils as irutils\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import common\nfrom edb.pgsql import types as pg_types\n\nfrom . import astutils\nfrom . import config\nfrom . import context\nfrom . import dispatch\nfrom . import enums as pgce\nfrom . import expr as expr_compiler  # NOQA\nfrom . import output\nfrom . import pathctx\nfrom . import relgen\nfrom . import shapecomp\n\n\n@dispatch.compile.register(irast.Set)\ndef compile_Set(\n        ir_set: irast.Set, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n\n    if ctx.singleton_mode:\n        return dispatch.compile(ir_set.expr, ctx=ctx)\n\n    is_toplevel = ctx.toplevel_stmt is context.NO_STMT\n\n    _compile_set_impl(ir_set, ctx=ctx)\n\n    if is_toplevel:\n        if isinstance(ir_set.expr, irast.ConfigCommand):\n            return config.top_output_as_config_op(\n                ir_set, ctx.rel, env=ctx.env)\n        else:\n            pathctx.get_path_serialized_output(\n                ctx.rel, ir_set.path_id, env=ctx.env)\n            return output.top_output_as_value(ctx.rel, ir_set, env=ctx.env)\n    else:\n        value = pathctx.get_path_value_var(\n            ctx.rel, ir_set.path_id, env=ctx.env)\n\n        return output.output_as_value(value, env=ctx.env)\n\n\n@dispatch.visit.register(irast.Set)\ndef visit_Set(\n        ir_set: irast.Set, *,\n        ctx: context.CompilerContextLevel) -> None:\n\n    if ctx.singleton_mode:\n        dispatch.compile(ir_set.expr, ctx=ctx)\n\n    _compile_set_impl(ir_set, ctx=ctx)\n\n\ndef _compile_set_impl(\n        ir_set: irast.Set, *,\n        ctx: context.CompilerContextLevel) -> None:\n\n    is_toplevel = ctx.toplevel_stmt is context.NO_STMT\n\n    if isinstance(ir_set.expr, (irast.BaseConstant, irast.BaseParameter)):\n        # Avoid creating needlessly complicated constructs for\n        # constant expressions.  Besides being an optimization,\n        # this helps in GROUP BY queries.\n        value = dispatch.compile(ir_set.expr, ctx=ctx)\n        if is_toplevel:\n            ctx.rel = ctx.toplevel_stmt = pgast.SelectStmt()\n        pathctx.put_path_value_var_if_not_exists(\n            ctx.rel, ir_set.path_id, value)\n        if (output.in_serialization_ctx(ctx) and ir_set.shape\n                and not ctx.env.ignore_object_shapes):\n            _compile_shape(ir_set, ir_set.shape, ctx=ctx)\n\n    elif ir_set.path_scope_id is not None and not is_toplevel:\n        # This Set is behind a scope fence, so compute it\n        # in a fenced context.\n        with ctx.newscope() as scopectx:\n            _compile_set(ir_set, ctx=scopectx)\n\n    else:\n        # All other sets.\n        _compile_set(ir_set, ctx=ctx)\n\n\n@dispatch.compile.register(irast.QueryParameter)\ndef compile_QueryParameter(\n    expr: irast.QueryParameter,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.BaseExpr:\n\n    result: pgast.BaseExpr\n\n    params = [p for p in ctx.env.query_params if p.name == expr.name]\n    param = params[0] if params else None\n\n    if param and param.sub_params:\n        return relgen.process_encoded_param(param, ctx=ctx)\n    else:\n        index = ctx.argmap[expr.name].index\n        result = pgast.ParamRef(number=index, nullable=not expr.required)\n\n    if irtyputils.needs_custom_serialization(expr.typeref):\n        if irtyputils.is_array(expr.typeref):\n            subt = expr.typeref.subtypes[0]\n            el_sql_type = subt.real_base_type.custom_sql_serialization\n            # Arrays of text encoded types need to come in as the custom type\n            result = pgast.TypeCast(\n                arg=result,\n                type_name=pgast.TypeName(name=(f'{el_sql_type}[]',)),\n            )\n        else:\n            el_sql_type = expr.typeref.real_base_type.custom_sql_serialization\n            assert el_sql_type is not None\n            result = pgast.TypeCast(\n                arg=result,\n                type_name=pgast.TypeName(name=(el_sql_type,)),\n            )\n\n    return pgast.TypeCast(\n        arg=result,\n        type_name=pgast.TypeName(\n            name=pg_types.pg_type_from_ir_typeref(expr.typeref)\n        )\n    )\n\n\n@dispatch.compile.register(irast.FunctionParameter)\ndef compile_FunctionParameter(\n    expr: irast.FunctionParameter,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.BaseExpr:\n\n    result: pgast.BaseExpr\n\n    if ctx.env.named_param_prefix is not None:\n        # When compiling functions\n        result = pgast.ColumnRef(\n            name=ctx.env.named_param_prefix + (expr.name,),\n            nullable=not expr.required,\n        )\n    else:\n        # Other things such as constraints\n        index = ctx.argmap[expr.name].index\n        result = pgast.ParamRef(number=index, nullable=not expr.required)\n\n    return pgast.TypeCast(\n        arg=result,\n        type_name=pgast.TypeName(\n            name=pg_types.pg_type_from_ir_typeref(expr.typeref)\n        )\n    )\n\n\n@dispatch.compile.register(irast.StringConstant)\ndef compile_StringConstant(\n        expr: irast.StringConstant, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n\n    return pgast.TypeCast(\n        arg=pgast.StringConstant(val=expr.value),\n        type_name=pgast.TypeName(\n            name=pg_types.pg_type_from_ir_typeref(expr.typeref)\n        )\n    )\n\n\n@dispatch.compile.register(irast.BytesConstant)\ndef compile_BytesConstant(\n    expr: irast.BytesConstant, *, ctx: context.CompilerContextLevel\n) -> pgast.BaseExpr:\n\n    return pgast.ByteaConstant(val=expr.value)\n\n\n@dispatch.compile.register(irast.FloatConstant)\n@dispatch.compile.register(irast.DecimalConstant)\n@dispatch.compile.register(irast.BigintConstant)\n@dispatch.compile.register(irast.IntegerConstant)\ndef compile_FloatConstant(\n        expr: irast.BaseConstant, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n\n    return pgast.TypeCast(\n        arg=pgast.NumericConstant(val=expr.value),\n        type_name=pgast.TypeName(\n            name=pg_types.pg_type_from_ir_typeref(expr.typeref)\n        )\n    )\n\n\n@dispatch.compile.register(irast.BooleanConstant)\ndef compile_BooleanConstant(\n        expr: irast.BooleanConstant, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n\n    return pgast.TypeCast(\n        arg=pgast.BooleanConstant(val=expr.value.lower() == 'true'),\n        type_name=pgast.TypeName(\n            name=pg_types.pg_type_from_ir_typeref(expr.typeref)\n        )\n    )\n\n\n@dispatch.compile.register(irast.TypeCast)\ndef compile_TypeCast(\n        expr: irast.TypeCast, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n\n    pg_expr = dispatch.compile(expr.expr, ctx=ctx)\n\n    detail: Optional[pgast.StringConstant] = None\n    if expr.error_message_context is not None:\n        detail = pgast.StringConstant(\n            val=(\n                '{\"error_message_context\": \"'\n                + expr.error_message_context\n                + '\"}'\n            )\n        )\n\n    if expr.sql_cast:\n        # Use explicit SQL cast.\n\n        pg_type = pg_types.pg_type_from_ir_typeref(expr.to_type)\n        res: pgast.BaseExpr = pgast.TypeCast(\n            arg=pg_expr,\n            type_name=pgast.TypeName(\n                name=pg_type\n            )\n        )\n\n    elif expr.sql_expr:\n        # Cast implemented as a function.\n        assert expr.cast_name\n\n        func_name = common.get_cast_backend_name(\n            expr.cast_name, aspect=\"function\",\n            versioned=ctx.env.versioned_stdlib,\n        )\n\n        args = [pg_expr]\n        if detail is not None:\n            args.append(detail)\n        res = pgast.FuncCall(\n            name=func_name,\n            args=args,\n        )\n\n    elif expr.sql_function:\n        res = pgast.FuncCall(\n            name=tuple(expr.sql_function.split(\".\")),\n            args=[pg_expr],\n        )\n\n    else:\n        raise errors.UnsupportedFeatureError('cast not supported')\n\n    if expr.cardinality_mod is qlast.CardinalityModifier.Required:\n        args = [\n            res,\n            pgast.StringConstant(\n                val='invalid_parameter_value',\n            ),\n            pgast.StringConstant(\n                val='invalid null value in cast',\n            ),\n        ]\n        if detail is not None:\n            args.append(detail)\n        res = pgast.FuncCall(\n            name=astutils.edgedb_func('raise_on_null', ctx=ctx),\n            args=args\n        )\n\n    return res\n\n\n@dispatch.compile.register(irast.IndexIndirection)\ndef compile_IndexIndirection(\n        expr: irast.IndexIndirection, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n    # Handle Expr[Index], where Expr may be std::str, array<T> or\n    # std::json. For strings we translate this into substr calls.\n    # Arrays use the native index access. JSON is handled by using the\n    # `->` accessor. Additionally, in all of the above cases a\n    # boundary-check is performed on the index and an exception is\n    # potentially raised.\n\n    # line, column and filename are captured here to be used with the\n    # error message\n    span = pgast.StringConstant(\n        val=irutils.get_span_as_json(\n            expr.index, errors.InvalidValueError\n        )\n    )\n\n    with ctx.new() as subctx:\n        subctx.expr_exposed = False\n        subj = dispatch.compile(expr.expr, ctx=subctx)\n        index = dispatch.compile(expr.index, ctx=subctx)\n\n    result: pgast.BaseExpr = pgast.FuncCall(\n        name=astutils.edgedb_func('_index', ctx=ctx),\n        args=[subj, index, span]\n    )\n\n    if irtyputils.is_array(expr.typeref):\n        # Unwrap the nested array from its tuple\n        result = astutils.array_get_inner_array(result, expr.typeref)\n\n    return result\n\n\n@dispatch.compile.register(irast.SliceIndirection)\ndef compile_SliceIndirection(\n    expr: irast.SliceIndirection, *, ctx: context.CompilerContextLevel\n) -> pgast.BaseExpr:\n    # Handle Expr[Index], where Expr may be std::str, array<T> or\n    # std::json. For strings we translate this into substr calls.\n    # Arrays use the native slice syntax. JSON is handled by a\n    # combination of unnesting aggregation and array slicing.\n    with ctx.new() as subctx:\n        subctx.expr_exposed = False\n        subj = dispatch.compile(expr.expr, ctx=subctx)\n\n        if expr.start is None:\n            start: pgast.BaseExpr = pgast.LiteralExpr(expr=\"0\")\n        else:\n            start = dispatch.compile(expr.start, ctx=subctx)\n\n        if expr.stop is None:\n            stop: pgast.BaseExpr = pgast.LiteralExpr(expr=str(2**31 - 1))\n        else:\n            stop = dispatch.compile(expr.stop, ctx=subctx)\n\n        typ = expr.expr.typeref\n        inline_array_slicing = irtyputils.is_array(typ) and any(\n            irtyputils.is_tuple(st) or irtyputils.is_array(st)\n            for st in typ.subtypes\n        )\n\n        if inline_array_slicing:\n            return _inline_array_slicing(subj, start, stop, ctx=ctx)\n        else:\n            return pgast.FuncCall(\n                name=astutils.edgedb_func('_slice', ctx=ctx),\n                args=[subj, start, stop]\n            )\n\n\ndef _inline_array_slicing(\n    subj: pgast.BaseExpr, start: pgast.BaseExpr, stop: pgast.BaseExpr,\n    ctx: context.CompilerContextLevel\n) -> pgast.BaseExpr:\n    return pgast.Indirection(\n        arg=subj,\n        indirection=[\n            pgast.Slice(\n                lidx=pgast.FuncCall(\n                    name=astutils.edgedb_func(\n                        '_normalize_array_slice_index', ctx=ctx),\n                    args=[\n                        start,\n                        pgast.FuncCall(\n                            name=(\"cardinality\",), args=[subj]\n                        ),\n                    ],\n                ),\n                ridx=astutils.new_binop(\n                    lexpr=pgast.FuncCall(\n                        name=astutils.edgedb_func(\n                            '_normalize_array_slice_index', ctx=ctx),\n                        args=[\n                            stop,\n                            pgast.FuncCall(\n                                name=(\"cardinality\",), args=[subj]\n                            ),\n                        ],\n                    ),\n                    op=\"-\",\n                    rexpr=pgast.LiteralExpr(expr=\"1\"),\n                ),\n            )\n        ],\n    )\n\n\ndef _compile_call_args(\n    expr: irast.Call, *,\n    ctx: context.CompilerContextLevel\n) -> tuple[list[pgast.BaseExpr], list[pgast.BaseExpr]]:\n    args = []\n    maybe_null = []\n    if isinstance(expr, irast.FunctionCall) and expr.global_args:\n        args += [dispatch.compile(arg, ctx=ctx) for arg in expr.global_args]\n    for ir_arg in expr.args.values():\n        ref = dispatch.compile(ir_arg.expr, ctx=ctx)\n        args.append(ref)\n        if (\n            not expr.impl_is_strict\n            and ir_arg.cardinality.can_be_zero()\n            and ref.nullable\n            and ir_arg.param_typemod == ql_ft.TypeModifier.SingletonType\n        ):\n            maybe_null.append(ref)\n    return args, maybe_null\n\n\ndef _wrap_call(\n    expr: pgast.BaseExpr, maybe_nulls: list[pgast.BaseExpr], *,\n    ctx: context.CompilerContextLevel\n) -> pgast.BaseExpr:\n    # If necessary, use CASE to filter out NULLs while calling a\n    # non-strict function.\n    if maybe_nulls:\n        tests = [pgast.NullTest(arg=arg, negated=True) for arg in maybe_nulls]\n        expr = pgast.CaseExpr(\n            args=[pgast.CaseWhen(\n                expr=astutils.extend_binop(None, *tests, op='AND'),\n                result=expr,\n            )]\n        )\n    return expr\n\n\n@dispatch.compile.register(irast.OperatorCall)\ndef compile_OperatorCall(\n        expr: irast.OperatorCall, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n\n    if (str(expr.func_shortname) == 'std::IF'\n            and expr.args[0].cardinality.is_single()\n            and expr.args[2].cardinality.is_single()):\n        if_expr, condition, else_expr = (a.expr for a in expr.args.values())\n        return pgast.CaseExpr(\n            args=[\n                pgast.CaseWhen(\n                    expr=dispatch.compile(condition, ctx=ctx),\n                    result=dispatch.compile(if_expr, ctx=ctx))\n            ],\n            defresult=dispatch.compile(else_expr, ctx=ctx))\n    elif (str(expr.func_shortname) == 'std::??'\n            and expr.args[0].cardinality.is_single()\n            and expr.args[1].cardinality.is_single()):\n        l_expr, r_expr = (a.expr for a in expr.args.values())\n        return pgast.CoalesceExpr(\n            args=[\n                dispatch.compile(l_expr, ctx=ctx),\n                dispatch.compile(r_expr, ctx=ctx),\n            ],\n        )\n    elif irutils.is_singleton_set_of_call(expr):\n        pass\n    elif irutils.returns_set_of(expr):\n        raise errors.UnsupportedFeatureError(\n            f\"set returning operator '{expr.func_shortname}' is not supported \"\n            f\"in singleton expressions\")\n    elif irutils.has_set_of_param(expr):\n        raise errors.UnsupportedFeatureError(\n            f\"aggregate operator '{expr.func_shortname}' is not supported \"\n            f\"in singleton expressions\")\n\n    args, maybe_null = _compile_call_args(expr, ctx=ctx)\n    return _wrap_call(\n        compile_operator(expr, args, ctx=ctx), maybe_null, ctx=ctx)\n\n\ndef compile_operator(\n        expr: irast.OperatorCall,\n        args: Sequence[pgast.BaseExpr], *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n    lexpr = rexpr = None\n    result: Optional[pgast.BaseExpr] = None\n\n    if expr.operator_kind is ql_ft.OperatorKind.Infix:\n        lexpr, rexpr = args\n    elif expr.operator_kind is ql_ft.OperatorKind.Prefix:\n        rexpr = args[0]\n    elif expr.operator_kind is ql_ft.OperatorKind.Postfix:\n        lexpr = args[0]\n    else:\n        raise RuntimeError(f'unexpected operator kind: {expr.operator_kind!r}')\n\n    str_func_name = str(expr.func_shortname)\n    if ((str_func_name in {'std::=', 'std::!='}\n            or str(expr.origin_name) in {'std::=', 'std::!='})\n            and expr.args[0].expr.typeref is not None\n            and irtyputils.is_object(expr.args[0].expr.typeref)\n            and expr.args[1].expr.typeref is not None\n            and irtyputils.is_object(expr.args[1].expr.typeref)):\n        if str_func_name == 'std::=' or str(expr.origin_name) == 'std::=':\n            sql_oper = '='\n        else:\n            sql_oper = '!='\n\n    elif str_func_name == 'std::EXISTS':\n        assert rexpr\n        result = pgast.NullTest(arg=rexpr, negated=True)\n\n    elif expr.func_shortname in common.operator_map:\n        sql_oper = common.operator_map[expr.func_shortname]\n\n    elif expr.sql_operator:\n        sql_oper = expr.sql_operator[0]\n        if len(expr.sql_operator) > 1:\n            # Explicit operand types given in FROM SQL OPERATOR\n            lexpr, rexpr = _cast_operands(lexpr, rexpr, expr.sql_operator[1:])\n\n    elif expr.origin_name is not None:\n        sql_oper = common.get_operator_backend_name(\n            expr.origin_name)[1]\n\n    else:\n        if expr.sql_function:\n            sql_func, *cast_types = expr.sql_function\n\n            func_name = common.maybe_versioned_name(\n                tuple(sql_func.split('.', 1)),\n                versioned=(\n                    ctx.env.versioned_stdlib\n                    and expr.func_shortname.get_root_module_name().name != 'ext'\n                ),\n            )\n\n            if cast_types:\n                # Explicit operand types given in FROM SQL FUNCTION\n                lexpr, rexpr = _cast_operands(lexpr, rexpr, cast_types)\n        else:\n            func_name = common.get_operator_backend_name(\n                expr.func_shortname, aspect='function',\n                versioned=ctx.env.versioned_stdlib)\n\n        args = []\n        if lexpr is not None:\n            args.append(lexpr)\n        if rexpr is not None:\n            args.append(rexpr)\n\n        result = pgast.FuncCall(name=func_name, args=args)\n\n    # If result was not already computed, it's going to be a generic Expr.\n    if result is None:\n        result = pgast.Expr(\n            name=sql_oper,\n            lexpr=lexpr,\n            rexpr=rexpr,\n        )\n\n    if expr.force_return_cast:\n        # The underlying operator has a return value type\n        # different from that of the EdgeQL operator declaration,\n        # so we need to make an explicit cast here.\n        result = pgast.TypeCast(\n            arg=result,\n            type_name=pgast.TypeName(\n                name=pg_types.pg_type_from_ir_typeref(expr.typeref)\n            )\n        )\n\n    return result\n\n\ndef _cast_operands(\n    lexpr: Optional[pgast.BaseExpr],\n    rexpr: Optional[pgast.BaseExpr],\n    sql_types: Sequence[str],\n) -> tuple[Optional[pgast.BaseExpr], Optional[pgast.BaseExpr]]:\n\n    if lexpr is not None:\n        lexpr = pgast.TypeCast(\n            arg=lexpr,\n            type_name=pgast.TypeName(\n                name=(sql_types[0],)\n            )\n        )\n\n    if rexpr is not None:\n        rexpr_qry = None\n\n        if (isinstance(rexpr, pgast.SubLink)\n                and isinstance(rexpr.expr, pgast.SelectStmt)):\n            rexpr_qry = rexpr.expr\n        elif isinstance(rexpr, pgast.SelectStmt):\n            rexpr_qry = rexpr\n\n        if rexpr_qry is not None:\n            # Handle cases like foo <op> ANY (SELECT) and\n            # foo <OP> (SELECT).\n            rexpr_qry.target_list[0] = pgast.ResTarget(\n                name=rexpr_qry.target_list[0].name,\n                val=pgast.TypeCast(\n                    arg=rexpr_qry.target_list[0].val,\n                    type_name=pgast.TypeName(\n                        name=(sql_types[1],)\n                    )\n                )\n            )\n        else:\n            rexpr = pgast.TypeCast(\n                arg=rexpr,\n                type_name=pgast.TypeName(\n                    name=(sql_types[1],)\n                )\n            )\n\n    return lexpr, rexpr\n\n\ndef get_func_call_backend_name(\n    expr: irast.FunctionCall, *,\n    ctx: context.CompilerContextLevel\n) -> tuple[str, ...]:\n    if expr.func_sql_function:\n        # The name might contain a \".\" if it's one of our\n        # metaschema helpers.\n        func_name = common.maybe_versioned_name(\n            tuple(expr.func_sql_function.split('.', 1)),\n            versioned=(\n                ctx.env.versioned_stdlib\n                and expr.func_shortname.get_root_module_name().name != 'ext'\n            ),\n        )\n    else:\n        func_name = common.get_function_backend_name(\n            expr.func_shortname, expr.backend_name,\n            versioned=ctx.env.versioned_stdlib)\n    return func_name\n\n\n@dispatch.compile.register(irast.TypeCheckOp)\ndef compile_TypeCheckOp(\n        expr: irast.TypeCheckOp, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n\n    with ctx.new() as newctx:\n        newctx.expr_exposed = False\n        left = dispatch.compile(expr.left, ctx=newctx)\n        negated = expr.op == 'IS NOT'\n\n        result: pgast.BaseExpr\n\n        if expr.result is not None:\n            result = pgast.BooleanConstant(\n                val=(expr.result and not negated)\n            )\n        else:\n            right: pgast.BaseExpr\n\n            if expr.right.union:\n                right = pgast.ArrayExpr(\n                    elements=[\n                        dispatch.compile(c, ctx=newctx)\n                        for c in expr.right.union\n                    ]\n                )\n            else:\n                right = dispatch.compile(expr.right, ctx=newctx)\n\n            result = pgast.FuncCall(\n                name=astutils.edgedb_func('issubclass', ctx=ctx),\n                args=[left, right])\n\n            if negated:\n                result = astutils.new_unop('NOT', result)\n\n    return result\n\n\n@dispatch.compile.register(irast.ConstantSet)\ndef compile_ConstantSet(\n        expr: irast.ConstantSet, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n    raise errors.UnsupportedFeatureError(\n        \"Constant sets not allowed in singleton mode\",\n        hint=\"Are you passing a set into a variadic function?\")\n\n\n@dispatch.compile.register(irast.Array)\ndef compile_Array(\n        expr: irast.Array, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n    elements = [dispatch.compile(e, ctx=ctx) for e in expr.elements]\n    return relgen.build_array_expr(expr, elements, ctx=ctx)\n\n\n@dispatch.compile.register(irast.Tuple)\ndef compile_Tuple(\n        expr: irast.Tuple, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n    ttype = expr.typeref\n    ttypes = {}\n    for i, st in enumerate(ttype.subtypes):\n        if st.element_name:\n            ttypes[st.element_name] = st\n        else:\n            ttypes[str(i)] = st\n    telems = list(ttypes)\n\n    elements = []\n\n    for i, e in enumerate(expr.elements):\n        telem = telems[i]\n        ttype = ttypes[telem]\n        val = dispatch.compile(e.val, ctx=ctx)\n        assert e.path_id\n        elements.append(pgast.TupleElement(path_id=e.path_id, val=val))\n\n    result = pgast.TupleVar(elements=elements, typeref=ttype)\n\n    return output.output_as_value(result, env=ctx.env)\n\n\n@dispatch.compile.register(irast.TypeRef)\ndef compile_TypeRef(\n        expr: irast.TypeRef, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n    return astutils.compile_typeref(expr)\n\n\n@dispatch.compile.register(irast.TypeIntrospection)\ndef compile_TypeIntrospection(\n        expr: irast.TypeIntrospection, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n    return astutils.compile_typeref(expr.output_typeref)\n\n\n@dispatch.compile.register(irast.FunctionCall)\ndef compile_FunctionCall(\n        expr: irast.FunctionCall, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n\n    fname = str(expr.func_shortname)\n    if sfunc := relgen._SIMPLE_SPECIAL_FUNCTIONS.get(fname):\n        return sfunc(expr, ctx=ctx)\n\n    if expr.func_sql_expr:\n        raise errors.UnsupportedFeatureError(\n            f'unimplemented function for singleton mode: {fname}'\n        )\n\n    if irutils.is_singleton_set_of_call(expr):\n        pass\n    elif irutils.returns_set_of(expr):\n        raise errors.UnsupportedFeatureError(\n            'set returning functions are not supported in simple expressions')\n    elif irutils.has_set_of_param(expr):\n        raise errors.UnsupportedFeatureError(\n            f\"aggregate function '{expr.func_shortname}' is not supported \"\n            f\"in singleton expressions\")\n\n    args, maybe_null = _compile_call_args(expr, ctx=ctx)\n\n    if expr.has_empty_variadic and expr.variadic_param_type is not None:\n        var = pgast.TypeCast(\n            arg=pgast.ArrayExpr(elements=[]),\n            type_name=pgast.TypeName(\n                name=pg_types.pg_type_from_ir_typeref(expr.variadic_param_type)\n            )\n        )\n\n        args.append(pgast.VariadicArgument(expr=var))\n\n    name = get_func_call_backend_name(expr, ctx=ctx)\n\n    result: pgast.BaseExpr = pgast.FuncCall(name=name, args=args)\n\n    result = _wrap_call(result, maybe_null, ctx=ctx)\n\n    if expr.force_return_cast:\n        # The underlying function has a return value type\n        # different from that of the EdgeQL function declaration,\n        # so we need to make an explicit cast here.\n        result = pgast.TypeCast(\n            arg=result,\n            type_name=pgast.TypeName(\n                name=pg_types.pg_type_from_ir_typeref(expr.typeref)\n            )\n        )\n\n    return result\n\n\ndef _tuple_to_row_expr(\n        tuple_set: irast.Set, *,\n        ctx: context.CompilerContextLevel,\n) -> pgast.ImplicitRowExpr | pgast.RowExpr:\n    tuple_val = dispatch.compile(tuple_set, ctx=ctx)\n    if not isinstance(tuple_val, (pgast.RowExpr, pgast.ImplicitRowExpr)):\n        raise RuntimeError('tuple compilation unexpectedly did '\n                           'not return a RowExpr or ImplicitRowExpr')\n    return tuple_val\n\n\ndef _compile_set(\n        ir_set: irast.Set, *,\n        ctx: context.CompilerContextLevel) -> None:\n\n    relgen.get_set_rvar(ir_set, ctx=ctx)\n\n    if (output.in_serialization_ctx(ctx) and ir_set.shape\n            and not ctx.env.ignore_object_shapes):\n        _compile_shape(ir_set, ir_set.shape, ctx=ctx)\n    elif ir_set.shape and ir_set in ctx.shapes_needed_by_dml:\n        # If this shape is needed for DML purposes (because it is\n        # populating link properties), compile it and populate its\n        # elements as *values*, so that process_link_values can pick\n        # them up and insert them.\n        shape_tuple = shapecomp.compile_shape(ir_set, ir_set.shape, ctx=ctx)\n        for element in shape_tuple.elements:\n            pathctx.put_path_var_if_not_exists(\n                ctx.rel,\n                element.path_id,\n                element.val,\n                aspect=pgce.PathAspect.VALUE,\n            )\n\n\ndef _compile_shape(\n        ir_set: irast.Set,\n        shape: Sequence[tuple[irast.SetE[irast.Pointer], qlast.ShapeOp]],\n        *,\n        ctx: context.CompilerContextLevel) -> pgast.TupleVar:\n\n    result = shapecomp.compile_shape(ir_set, shape, ctx=ctx)\n\n    for element in result.elements:\n        # We want to force, because the path id might already exist\n        # serialized with a different shape, and we need ours to be\n        # visible. (Anything needing the old one needs to have pulled\n        # it already: see the \"unfortunate hack\" in\n        # process_set_as_tuple.)\n        pathctx.put_path_serialized_var(\n            ctx.rel, element.path_id, element.val, force=True\n        )\n\n    # When we compile a shape during materialization, stash the\n    # set away so we can consume it in unpack_rvar.\n    if (\n        ctx.materializing\n        and ir_set.typeref.id not in ctx.env.materialized_views\n    ):\n        ctx.env.materialized_views[ir_set.typeref.id] = ir_set\n\n    ser_elements = []\n    for el in result.elements:\n        ser_val = pathctx.get_path_serialized_or_value_var(\n            ctx.rel, el.path_id, env=ctx.env)\n        ser_elements.append(pgast.TupleElement(\n            path_id=el.path_id,\n            name=el.name,\n            val=ser_val\n        ))\n        # Don't let the elements themselves leak out, since they may\n        # be wrapped in arrays.\n        pathctx.put_path_id_mask(ctx.rel, el.path_id)\n\n    ser_result = pgast.TupleVar(elements=ser_elements, named=True)\n    sval = output.serialize_expr(\n        ser_result, path_id=ir_set.path_id, env=ctx.env\n    )\n    pathctx.put_path_serialized_var(ctx.rel, ir_set.path_id, sval, force=True)\n\n    return result\n\n\n@dispatch.compile.register\ndef compile_EmptySet(\n    expr: irast.EmptySet, *, ctx: context.CompilerContextLevel\n) -> pgast.BaseExpr:\n    return pgast.NullConstant()\n\n\n@dispatch.compile.register\ndef compile_TypeRoot(\n    expr: irast.TypeRoot, *, ctx: context.CompilerContextLevel\n) -> pgast.BaseExpr:\n    name = [common.edgedb_name_to_pg_name(str(expr.typeref.id))]\n    if irtyputils.is_object(expr.typeref):\n        name.append('id')\n\n    return pgast.ColumnRef(name=name)\n\n\n@dispatch.compile.register\ndef compile_Pointer(\n    rptr: irast.Pointer, *, ctx: context.CompilerContextLevel\n) -> pgast.BaseExpr:\n    assert ctx.singleton_mode\n\n    if rptr.expr:\n        return dispatch.compile(rptr.expr, ctx=ctx)\n\n    ptrref = rptr.ptrref\n    source = rptr.source\n\n    if ptrref.source_ptr is None and isinstance(source.expr, irast.Pointer):\n        raise errors.UnsupportedFeatureError(\n            'unexpectedly long path in simple expr')\n\n    # In most cases, we don't need to reference the rvar (since there\n    # will be only one in scope), but sometimes we do (for example NEW\n    # in trigger functions).\n    rvar_name = []\n    if src := ctx.env.external_rvars.get(\n        (source.path_id, pgce.PathAspect.SOURCE)\n    ):\n        rvar_name = [src.alias.aliasname]\n\n    # compile column name\n    ptr_stor_info = pg_types.get_ptrref_storage_info(\n        ptrref, resolve_type=False)\n\n    colref = pgast.ColumnRef(\n        name=rvar_name + [ptr_stor_info.column_name],\n        nullable=rptr.dir_cardinality.can_be_zero())\n\n    return colref\n\n\n@dispatch.compile.register\ndef compile_TupleIndirectionPointer(\n    rptr: irast.TupleIndirectionPointer, *, ctx: context.CompilerContextLevel\n) -> pgast.BaseExpr:\n    tuple_val = dispatch.compile(rptr.source, ctx=ctx)\n    set_expr = astutils.tuple_getattr(\n        tuple_val,\n        rptr.source.typeref,\n        rptr.ptrref.shortname.name,\n    )\n    return set_expr\n\n\n@dispatch.compile.register(irast.FTSDocument)\ndef compile_FTSDocument(\n    expr: irast.FTSDocument, *, ctx: context.CompilerContextLevel\n) -> pgast.BaseExpr:\n    return pgast.FTSDocument(\n        text=dispatch.compile(expr.text, ctx=ctx),\n        language=dispatch.compile(expr.language, ctx=ctx),\n        language_domain=expr.language_domain,\n        weight=expr.weight,\n    )\n"
  },
  {
    "path": "edb/pgsql/compiler/group.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Optional, AbstractSet\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import desugar_group\nfrom edb.ir import ast as irast\nfrom edb.ir import utils as irutils\nfrom edb.pgsql import ast as pgast\n\nfrom . import astutils\nfrom . import clauses\nfrom . import context\nfrom . import dispatch\nfrom . import enums as pgce\nfrom . import output\nfrom . import pathctx\nfrom . import relctx\nfrom . import relgen\n\n\ndef compile_grouping_atom(\n    el: qlast.GroupingAtom,\n    stmt: irast.GroupStmt, *, ctx: context.CompilerContextLevel\n) -> pgast.Base:\n    '''Compile a GroupingAtom into sql grouping sets'''\n    if isinstance(el, qlast.GroupingIdentList):\n        return pgast.GroupingOperation(\n            args=[\n                compile_grouping_atom(at, stmt, ctx=ctx) for at in el.elements\n            ],\n        )\n\n    assert isinstance(el, qlast.ObjectRef)\n    alias_set, _ = stmt.using[el.name]\n    return pathctx.get_path_value_var(\n        ctx.rel, alias_set.path_id, env=ctx.env)\n\n\ndef compile_grouping_el(\n    el: qlast.GroupingElement,\n    stmt: irast.GroupStmt, *, ctx: context.CompilerContextLevel\n) -> pgast.Base:\n    '''Compile a GroupingElement into sql grouping sets'''\n    if isinstance(el, qlast.GroupingSets):\n        return pgast.GroupingOperation(\n            operation='GROUPING SETS',\n            args=[compile_grouping_el(sub, stmt, ctx=ctx) for sub in el.sets],\n        )\n    elif isinstance(el, qlast.GroupingOperation):\n        return pgast.GroupingOperation(\n            operation=el.oper,\n            args=[\n                compile_grouping_atom(at, stmt, ctx=ctx) for at in el.elements\n            ],\n        )\n    elif isinstance(el, qlast.GroupingSimple):\n        return compile_grouping_atom(el.element, stmt, ctx=ctx)\n    raise AssertionError('Unknown GroupingElement')\n\n\ndef _compile_grouping_value(\n        stmt: irast.GroupStmt, used_args: AbstractSet[str], *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n    '''Produce the value for the grouping binding saying what is grouped on'''\n    assert stmt.grouping_binding\n    grouprel = ctx.rel\n\n    # If there is only one grouping set, hardcode the output\n    if all(isinstance(b, qlast.GroupingSimple) for b in stmt.by):\n        return pgast.ArrayExpr(\n            elements=[\n                pgast.StringConstant(val=desugar_group.key_name(arg))\n                for arg in used_args\n            ],\n        )\n\n    using = {k: stmt.using[k] for k in used_args}\n\n    args = [\n        pathctx.get_path_var(\n            grouprel,\n            alias_set.path_id,\n            aspect=pgce.PathAspect.VALUE,\n            env=ctx.env,\n        )\n        for alias_set, _ in using.values()\n    ]\n\n    # Call grouping on each element we group on to produce a bitmask\n    grouping_alias = ctx.env.aliases.get('g')\n    grouping_call = pgast.FuncCall(name=('grouping',), args=args)\n    subq = pgast.SelectStmt(\n        target_list=[\n            pgast.ResTarget(name=grouping_alias, val=grouping_call),\n        ]\n    )\n    q = pgast.SelectStmt(\n        from_clause=[pgast.RangeSubselect(\n            subquery=subq,\n            alias=pgast.Alias(aliasname=ctx.env.aliases.get())\n        )]\n    )\n\n    grouping_ref = pgast.ColumnRef(name=(grouping_alias,))\n\n    # Generate a call to ARRAY[...] with a case for each grouping\n    # element, then array_remove out the NULLs.\n    els: list[pgast.BaseExpr] = []\n    for i, name in enumerate(using):\n        name = desugar_group.key_name(name)\n        mask = 1 << (len(using) - i - 1)\n        # (CASE (e & <mask>) WHEN 0 THEN '<name>' ELSE NULL END)\n\n        els.append(pgast.CaseExpr(\n            arg=pgast.Expr(\n                name='&',\n                lexpr=grouping_ref,\n                rexpr=pgast.LiteralExpr(expr=str(mask))\n            ),\n            args=[\n                pgast.CaseWhen(\n                    expr=pgast.LiteralExpr(expr='0'),\n                    result=pgast.StringConstant(val=name)\n                )\n            ],\n            defresult=pgast.NullConstant()\n        ))\n\n    val = pgast.FuncCall(\n        name=('array_remove',),\n        args=[pgast.ArrayExpr(elements=els), pgast.NullConstant()]\n    )\n\n    q.target_list.append(pgast.ResTarget(val=val))\n\n    return q\n\n\ndef _compile_grouping_binding(\n        stmt: irast.GroupStmt, *, used_args: AbstractSet[str],\n        ctx: context.CompilerContextLevel) -> None:\n    assert stmt.grouping_binding\n    pathctx.put_path_var(\n        ctx.rel,\n        stmt.grouping_binding.path_id,\n        _compile_grouping_value(stmt, used_args=used_args, ctx=ctx),\n        aspect=pgce.PathAspect.VALUE,\n    )\n\n\ndef _compile_group(\n        stmt: irast.GroupStmt, *,\n        ctx: context.CompilerContextLevel,\n        parent_ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n\n    clauses.compile_volatile_bindings(stmt, ctx=ctx)\n\n    query = ctx.stmt\n\n    # Compile a GROUP BY into a subquery, along with all the aggregations\n    with ctx.subrel() as groupctx:\n        grouprel = groupctx.rel\n\n        # First compile the actual subject\n        # subrel *solely* for path id map reasons\n        with groupctx.subrel() as subjctx:\n            subjctx.expr_exposed = False\n\n            dispatch.visit(stmt.subject, ctx=subjctx)\n            if stmt.subject.path_id.is_objtype_path():\n                # This shouldn't technically be needed but we generate\n                # better code with it.\n                relgen.ensure_source_rvar(\n                    stmt.subject, subjctx.rel, ctx=subjctx)\n\n        subj_rvar = relctx.rvar_for_rel(\n            subjctx.rel, ctx=groupctx, lateral=True)\n        aspects = pathctx.list_path_aspects(subjctx.rel, stmt.subject.path_id)\n\n        pathctx.put_path_id_map(\n            subjctx.rel,\n            stmt.group_binding.path_id, stmt.subject.path_id)\n\n        # update_mask=False because we are doing this solely to remap\n        # elements individually and don't want to affect the mask.\n        relctx.include_rvar(\n            grouprel, subj_rvar, stmt.group_binding.path_id,\n            aspects=aspects,\n            update_mask=False, ctx=groupctx)\n        relctx.include_rvar(\n            grouprel, subj_rvar, stmt.subject.path_id,\n            aspects=aspects,\n            update_mask=False, ctx=groupctx)\n\n        # Now we compile the bindings\n        groupctx.path_scope = subjctx.path_scope.new_child()\n        groupctx.path_scope[stmt.group_binding.path_id] = None\n        if stmt.grouping_binding:\n            groupctx.path_scope[stmt.grouping_binding.path_id] = None\n\n        # Compile all the 'using' items\n        for _alias, (value, using_card) in stmt.using.items():\n            # If the using bit is nullable, we need to compile it\n            # as optional, or we'll get in trouble.\n            # TODO: Can we do better here and not do this\n            # in obvious cases like directly referencing an optional\n            # property.\n            if using_card.can_be_zero():\n                groupctx.force_optional = ctx.force_optional | {value.path_id}\n            groupctx.path_scope[value.path_id] = None\n\n            dispatch.visit(value, ctx=groupctx)\n            groupctx.force_optional = ctx.force_optional\n\n        # Compile all of the aggregate calls that we found. This lets us\n        # compute things like sum and count without needing to materialize\n        # the result.\n        for group_use, skippable in stmt.group_aggregate_sets.items():\n            if not group_use:\n                continue\n            with groupctx.subrel() as hoistctx:\n                hoistctx.skippable_sources |= skippable\n\n                assert irutils.is_set_instance(group_use, irast.FunctionCall)\n                relgen.process_set_as_agg_expr_inner(\n                    group_use,\n                    aspect=pgce.PathAspect.VALUE,\n                    wrapper=None,\n                    for_group_by=True,\n                    ctx=hoistctx,\n                )\n                pathctx.get_path_value_output(\n                    rel=hoistctx.rel, path_id=group_use.path_id, env=ctx.env)\n                pathctx.put_path_value_var(\n                    grouprel, group_use.path_id, hoistctx.rel\n                )\n\n        packed = False\n        # Materializing the actual grouping sets\n        if None in stmt.group_aggregate_sets:\n            packed = True\n            # TODO: Be able to directly output the final serialized version\n            # if it is consumed directly.\n            with context.output_format(ctx, context.OutputFormat.NATIVE), (\n                    groupctx.new()) as matctx:\n                matctx.materializing |= {stmt}\n                matctx.expr_exposed = True\n\n                mat_qry = relgen.set_as_subquery(\n                    stmt.group_binding, as_value=True, ctx=matctx)\n                mat_qry = relctx.set_to_array(\n                    path_id=stmt.group_binding.path_id,\n                    for_group_by=True,\n                    query=mat_qry,\n                    ctx=matctx)\n                if not mat_qry.target_list[0].name:\n                    mat_qry.target_list[0].name = ctx.env.aliases.get('v')\n\n                ref = pgast.ColumnRef(\n                    name=[mat_qry.target_list[0].name],\n                    is_packed_multi=True,\n                )\n                pathctx.put_path_packed_output(\n                    mat_qry, stmt.group_binding.path_id, ref)\n\n                pathctx.put_path_var(\n                    grouprel,\n                    stmt.group_binding.path_id,\n                    mat_qry,\n                    aspect=pgce.PathAspect.VALUE,\n                    flavor='packed',\n                )\n\n        used_args = desugar_group.collect_grouping_atoms(stmt.by)\n\n        if stmt.grouping_binding:\n            _compile_grouping_binding(stmt, used_args=used_args, ctx=groupctx)\n\n        # We want to make sure that every grouping key is associated\n        # with exactly one output from the query. The means that\n        # tuples must be packed up and keys must not have an extra\n        # serialized output.\n        #\n        # We do this by manually packing up any TupleVarBases and\n        # copying value aspects to serialized.\n        # of the grouping keys get an extra serialized output from\n        # grouprel, so we just copy all their value aspects to their\n        # serialized aspects.\n        using = {k: stmt.using[k] for k in used_args}\n        for using_val, _ in using.values():\n            uvar = pathctx.get_path_var(\n                grouprel,\n                using_val.path_id,\n                aspect=pgce.PathAspect.VALUE,\n                env=ctx.env,\n            )\n            if isinstance(uvar, pgast.TupleVarBase):\n                uvar = output.output_as_value(uvar, env=ctx.env)\n                pathctx.put_path_var(\n                    grouprel,\n                    using_val.path_id,\n                    uvar,\n                    aspect=pgce.PathAspect.VALUE,\n                    force=True,\n                )\n\n            uout = pathctx.get_path_output(\n                grouprel,\n                using_val.path_id,\n                aspect=pgce.PathAspect.VALUE,\n                env=ctx.env,\n            )\n            pathctx._put_path_output_var(\n                grouprel,\n                using_val.path_id,\n                pgce.PathAspect.SERIALIZED,\n                uout,\n            )\n\n        grouprel.group_clause = [\n            compile_grouping_el(el, stmt, ctx=groupctx) for el in stmt.by\n        ]\n\n    group_rvar = relctx.rvar_for_rel(grouprel, ctx=ctx, lateral=True)\n    if packed:\n        relctx.include_rvar(\n            query, group_rvar, path_id=stmt.group_binding.path_id,\n            flavor='packed', update_mask=False, pull_namespace=False,\n            aspects=(pgce.PathAspect.VALUE,),\n            ctx=ctx)\n    else:\n        # Not include_rvar because we don't actually provide the path id!\n        relctx.rel_join(query, group_rvar, ctx=ctx)\n\n    # Set up the hoisted aggregates and bindings to be found\n    # in the group subquery.\n    for group_use in [\n        *stmt.group_aggregate_sets,\n        *[x for x, _ in stmt.using.values()],\n        stmt.grouping_binding,\n    ]:\n        if group_use:\n            pathctx.put_path_rvar(\n                query,\n                group_use.path_id,\n                group_rvar,\n                aspect=pgce.PathAspect.VALUE,\n            )\n\n    vol_ref = None\n\n    def _get_volatility_ref() -> Optional[pgast.BaseExpr]:\n        nonlocal vol_ref\n        if vol_ref:\n            return vol_ref\n\n        name = ctx.env.aliases.get('key')\n        grouprel.target_list.append(\n            pgast.ResTarget(\n                name=name,\n                val=pgast.FuncCall(name=('row_number',), args=[],\n                                   over=pgast.WindowDef())\n            )\n        )\n        vol_ref = pgast.ColumnRef(name=[group_rvar.alias.aliasname, name])\n        return vol_ref\n\n    with ctx.new() as outctx:\n        # Inherit the path_scope we made earlier (with the GROUP bindings\n        # removed), so that we'll always look for those in the right place.\n        outctx.path_scope = groupctx.path_scope\n\n        outctx.volatility_ref += (lambda stmt, xctx: _get_volatility_ref(),)\n\n        # Process materialized sets\n        clauses.compile_materialized_exprs(query, stmt, ctx=outctx)\n\n        clauses.compile_output(stmt.result, ctx=outctx)\n\n    with ctx.new() as ictx:\n        ictx.path_scope = groupctx.path_scope\n\n        # FILTER and ORDER BY need to have the base result as a\n        # volatility ref.\n        clauses.setup_iterator_volatility(stmt.result, ctx=ictx)\n\n        # The FILTER clause.\n        if stmt.where is not None:\n            query.where_clause = astutils.extend_binop(\n                query.where_clause,\n                clauses.compile_filter_clause(\n                    stmt.where, stmt.where_card, ctx=ictx))\n\n        # The ORDER BY clause\n        if stmt.orderby is not None:\n            with ictx.new() as octx:\n                query.sort_clause = clauses.compile_orderby_clause(\n                    stmt.orderby, ctx=octx)\n\n    return query\n\n\ndef compile_group(\n        stmt: irast.GroupStmt, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n    with ctx.substmt() as sctx:\n        return _compile_group(stmt, ctx=sctx, parent_ctx=ctx)\n"
  },
  {
    "path": "edb/pgsql/compiler/output.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Compilation helpers for output formatting and serialization.\"\"\"\n\nfrom __future__ import annotations\nfrom typing import Optional, Sequence\n\nimport itertools\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtyputils\n\nfrom edb.schema import casts as s_casts\nfrom edb.schema import defines as s_defs\nfrom edb.schema import name as sn\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import common\nfrom edb.pgsql import types as pgtypes\n\nfrom . import astutils\nfrom . import context\n\n\n_JSON_FORMATS = {context.OutputFormat.JSON, context.OutputFormat.JSON_ELEMENTS}\n\n\ndef _get_json_func(\n    name: str,\n    *,\n    output_format: Optional[context.OutputFormat] = None,\n    env: context.Environment,\n) -> tuple[str, ...]:\n\n    if output_format is None:\n        output_format = env.output_format\n\n    if output_format in _JSON_FORMATS:\n        prefix_suffix = 'json'\n    else:\n        prefix_suffix = 'jsonb'\n\n    if name == 'to':\n        return (f'{name}_{prefix_suffix}',)\n    else:\n        return (f'{prefix_suffix}_{name}',)\n\n\ndef _build_json(\n    name: str,\n    args: Sequence[pgast.BaseExpr],\n    *,\n    null_safe: bool = False,\n    ser_safe: bool = False,\n    nullable: Optional[bool] = None,\n    env: context.Environment,\n) -> pgast.BaseExpr:\n    # PostgreSQL has a limit on the maximum number of arguments\n    # passed to a function call, so we must chop input into chunks\n    # if the argument count is greater then the limit.\n\n    if len(args) > s_defs.MAX_FUNC_ARG_COUNT:\n        json_func = _get_json_func(\n            name,\n            output_format=context.OutputFormat.JSONB,\n            env=env,\n        )\n\n        chunk_iters = [iter(args)] * s_defs.MAX_FUNC_ARG_COUNT\n        chunks = list(itertools.zip_longest(*chunk_iters, fillvalue=None))\n        if len(args) != len(chunks) * s_defs.MAX_FUNC_ARG_COUNT:\n            chunks[-1] = tuple(filter(None, chunks[-1]))\n\n        result: pgast.BaseExpr = pgast.FuncCall(\n            name=json_func,\n            args=list(chunks[0]),\n            null_safe=null_safe,\n            ser_safe=ser_safe,\n            nullable=nullable,\n        )\n\n        for chunk in chunks[1:]:\n            fc = pgast.FuncCall(\n                name=json_func,\n                args=list(chunk),\n                null_safe=null_safe,\n                ser_safe=ser_safe,\n                nullable=nullable,\n            )\n\n            result = astutils.new_binop(\n                lexpr=result,\n                rexpr=fc,\n                op='||',\n            )\n\n        if env.output_format in _JSON_FORMATS:\n            result = pgast.TypeCast(\n                arg=result,\n                type_name=pgast.TypeName(\n                    name=('json',)\n                )\n            )\n\n        return result\n\n    else:\n        json_func = _get_json_func(name, env=env)\n\n        return pgast.FuncCall(\n            name=json_func,\n            args=args,\n            null_safe=null_safe,\n            ser_safe=ser_safe,\n            nullable=nullable,\n        )\n\n\ndef coll_as_json_object(\n    expr: pgast.BaseExpr,\n    *,\n    styperef: irast.TypeRef,\n    env: context.Environment,\n) -> pgast.BaseExpr:\n    if irtyputils.is_tuple(styperef):\n        return tuple_as_json_object(expr, styperef=styperef, env=env)\n    elif irtyputils.is_array(styperef):\n        return array_as_json_object(expr, styperef=styperef, env=env)\n    else:\n        raise RuntimeError(f'{styperef!r} is not a collection')\n\n\ndef array_as_json_object(\n    expr: pgast.BaseExpr,\n    *,\n    styperef: irast.TypeRef,\n    env: context.Environment,\n) -> pgast.BaseExpr:\n    el_type = styperef.subtypes[0]\n\n    is_tuple = irtyputils.is_tuple(el_type)\n    # Tuples/ranges/scalars with custom casts need underlying casts to be done\n    if (\n        is_tuple\n        or irtyputils.is_range(el_type)\n        or irtyputils.is_multirange(el_type)\n        or el_type.real_base_type.needs_custom_json_cast\n    ):\n        coldeflist = []\n\n        out_alias = env.aliases.get('q')\n\n        val: pgast.BaseExpr\n        if is_tuple:\n            json_args: list[pgast.BaseExpr] = []\n            is_named = any(st.element_name for st in el_type.subtypes)\n            for i, st in enumerate(el_type.subtypes):\n                if is_named:\n                    colname = st.element_name\n                    assert colname\n                    json_args.append(pgast.StringConstant(val=colname))\n                else:\n                    colname = str(i)\n\n                val = pgast.ColumnRef(name=[colname])\n                val = serialize_expr_to_json(\n                    val, styperef=st, nested=True, env=env)\n\n                json_args.append(val)\n\n                if not irtyputils.is_persistent_tuple(el_type):\n                    # Column definition list is only allowed for functions\n                    # returning \"record\", i.e. an anonymous tuple, which\n                    # would not be the case for schema-persistent tuple types.\n                    coldeflist.append(\n                        pgast.ColumnDef(\n                            name=colname,\n                            typename=pgast.TypeName(\n                                name=pgtypes.pg_type_from_ir_typeref(st)\n                            )\n                        )\n                    )\n\n            json_func = 'build_object' if is_named else 'build_array'\n            agg_arg = _build_json(json_func, json_args, env=env)\n\n            needs_unnest = bool(el_type.subtypes)\n        else:\n            val = pgast.ColumnRef(name=[out_alias])\n            agg_arg = serialize_expr_to_json(\n                val, styperef=el_type, nested=True, env=env)\n            needs_unnest = True\n\n        return pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    val=pgast.CoalesceExpr(\n                        args=[\n                            pgast.FuncCall(\n                                name=_get_json_func('agg', env=env),\n                                args=[agg_arg],\n                            ),\n                            pgast.StringConstant(val='[]'),\n                        ]\n                    ),\n                    ser_safe=True,\n                )\n            ],\n            from_clause=[\n                pgast.RangeFunction(\n                    alias=pgast.Alias(aliasname=out_alias),\n                    is_rowsfrom=True,\n                    functions=[\n                        pgast.FuncCall(\n                            name=('unnest',),\n                            args=[expr],\n                            coldeflist=coldeflist,\n                        )\n                    ]\n                )\n            ] if needs_unnest else [],\n        )\n    elif irtyputils.is_array(el_type):\n        # array<array<...>> implemented as array<tuple<array<...>>>\n        #\n        # If we serialize without any special handling, the tuple will be\n        # included, with the key 'f1'\n        #\n        # eg. [[1, 2, 3], [4, 5]] -> [{'f1': [1,2,3]}, {'f1': [4,5]}]\n        #\n        # To prevent this, we need to explicitly serialize the inner arrays then\n        # aggregate them.\n        el_name = 'f1'\n        coldeflist = [\n            pgast.ColumnDef(\n                name=str(el_name),\n                typename=pgast.TypeName(\n                    name=pgtypes.pg_type_from_ir_typeref(el_type),\n                ),\n            )\n        ]\n        unwrapped_inner_array = pgast.RangeFunction(\n            functions=[\n                pgast.FuncCall(\n                    name=('unnest',),\n                    args=[expr],\n                    coldeflist=coldeflist,\n                )\n            ]\n        )\n        serialized_inner_array = serialize_expr_to_json(\n            pgast.ColumnRef(name=[str(el_name)]),\n            styperef=el_type,\n            nested=True,\n            env=env,\n        )\n\n        return pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    val=pgast.CoalesceExpr(\n                        args=[\n                            pgast.FuncCall(\n                                name=_get_json_func('agg', env=env),\n                                args=[serialized_inner_array],\n                            ),\n                            pgast.StringConstant(val='[]'),\n                        ]\n                    ),\n                    ser_safe=True,\n                )\n            ],\n            from_clause=[unwrapped_inner_array]\n        )\n    else:\n        return pgast.FuncCall(\n            name=_get_json_func('to', env=env), args=[expr],\n            null_safe=True, ser_safe=True)\n\n\ndef tuple_as_json_object(\n    expr: pgast.BaseExpr,\n    *,\n    styperef: irast.TypeRef,\n    env: context.Environment,\n) -> pgast.BaseExpr:\n    if any(st.element_name for st in styperef.subtypes):\n        return named_tuple_as_json_object(expr, styperef=styperef, env=env)\n    else:\n        return unnamed_tuple_as_json_object(expr, styperef=styperef, env=env)\n\n\ndef unnamed_tuple_as_json_object(\n    expr: pgast.BaseExpr,\n    *,\n    styperef: irast.TypeRef,\n    env: context.Environment,\n) -> pgast.BaseExpr:\n    vals: list[pgast.BaseExpr] = []\n\n    if irtyputils.is_persistent_tuple(styperef):\n        for el_idx, el_type in enumerate(styperef.subtypes):\n            val: pgast.BaseExpr = pgast.Indirection(\n                arg=expr,\n                indirection=[pgast.RecordIndirectionOp(name=str(el_idx))],\n            )\n            val = serialize_expr_to_json(\n                val, styperef=el_type, nested=True, env=env)\n            vals.append(val)\n\n        obj = _build_json(\n            'build_array',\n            args=vals,\n            null_safe=True,\n            ser_safe=True,\n            nullable=expr.nullable,\n            env=env,\n        )\n\n    else:\n        coldeflist = []\n\n        for el_idx, el_type in enumerate(styperef.subtypes):\n\n            coldeflist.append(pgast.ColumnDef(\n                name=str(el_idx),\n                typename=pgast.TypeName(\n                    name=pgtypes.pg_type_from_ir_typeref(el_type),\n                ),\n            ))\n\n            val = pgast.ColumnRef(name=[str(el_idx)])\n\n            val = serialize_expr_to_json(\n                val, styperef=el_type, nested=True, env=env)\n\n            vals.append(val)\n\n        obj = _build_json(\n            'build_array',\n            args=vals,\n            null_safe=True,\n            ser_safe=True,\n            nullable=expr.nullable,\n            env=env,\n        )\n\n        obj = pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    val=obj,\n                ),\n            ],\n            from_clause=[\n                pgast.RangeFunction(\n                    functions=[\n                        pgast.FuncCall(\n                            name=('unnest',),\n                            args=[\n                                pgast.ArrayExpr(\n                                    elements=[expr],\n                                )\n                            ],\n                            coldeflist=coldeflist,\n                        )\n                    ]\n                )\n            ] if styperef.subtypes else []\n        )\n\n    if expr.nullable:\n        obj = pgast.SelectStmt(\n            target_list=[pgast.ResTarget(val=obj)],\n            where_clause=pgast.NullTest(arg=expr, negated=True)\n        )\n    return obj\n\n\ndef named_tuple_as_json_object(\n    expr: pgast.BaseExpr,\n    *,\n    styperef: irast.TypeRef,\n    env: context.Environment,\n) -> pgast.BaseExpr:\n    keyvals: list[pgast.BaseExpr] = []\n\n    if irtyputils.is_persistent_tuple(styperef):\n        for el_type in styperef.subtypes:\n            assert el_type.element_name\n            keyvals.append(pgast.StringConstant(val=el_type.element_name))\n            val: pgast.BaseExpr = pgast.Indirection(\n                arg=expr,\n                indirection=[\n                    pgast.RecordIndirectionOp(\n                        name=el_type.element_name\n                    )\n                ]\n            )\n            val = serialize_expr_to_json(\n                val, styperef=el_type, nested=True, env=env)\n            keyvals.append(val)\n\n        obj = _build_json(\n            'build_object',\n            args=keyvals,\n            null_safe=True,\n            ser_safe=True,\n            nullable=expr.nullable,\n            env=env,\n        )\n\n    else:\n        coldeflist = []\n\n        for el_type in styperef.subtypes:\n            assert el_type.element_name\n            keyvals.append(pgast.StringConstant(val=el_type.element_name))\n\n            coldeflist.append(pgast.ColumnDef(\n                name=el_type.element_name,\n                typename=pgast.TypeName(\n                    name=pgtypes.pg_type_from_ir_typeref(el_type),\n                ),\n            ))\n\n            val = pgast.ColumnRef(name=[el_type.element_name])\n\n            val = serialize_expr_to_json(\n                val, styperef=el_type, nested=True, env=env)\n\n            keyvals.append(val)\n\n        obj = _build_json(\n            'build_object',\n            args=keyvals,\n            null_safe=True,\n            ser_safe=True,\n            nullable=expr.nullable,\n            env=env,\n        )\n\n        obj = pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    val=obj,\n                ),\n            ],\n            from_clause=[\n                pgast.RangeFunction(\n                    functions=[\n                        pgast.FuncCall(\n                            name=('unnest',),\n                            args=[\n                                pgast.ArrayExpr(\n                                    elements=[expr],\n                                )\n                            ],\n                            coldeflist=coldeflist,\n                        )\n                    ]\n                )\n            ] if styperef.subtypes else []\n        )\n\n    if expr.nullable:\n        obj = pgast.SelectStmt(\n            target_list=[pgast.ResTarget(val=obj)],\n            where_clause=pgast.NullTest(arg=expr, negated=True)\n        )\n    return obj\n\n\ndef tuple_var_as_json_object(\n    tvar: pgast.TupleVar,\n    *,\n    styperef: irast.TypeRef,\n    env: context.Environment,\n) -> pgast.BaseExpr:\n\n    if not tvar.named:\n        vals = [\n            serialize_expr(t.val, path_id=t.path_id, nested=True, env=env)\n            for t in tvar.elements\n        ]\n\n        return _build_json(\n            'build_array',\n            args=vals,\n            null_safe=True,\n            ser_safe=True,\n            nullable=tvar.nullable,\n            env=env,\n        )\n    else:\n        keyvals: list[pgast.BaseExpr] = []\n\n        for element in tvar.elements:\n            rptr = element.path_id.rptr()\n            assert rptr is not None\n            name = rptr.shortname.name\n            if rptr.source_ptr is not None:\n                name = '@' + name\n            keyvals.append(pgast.StringConstant(val=name))\n            val = serialize_expr(\n                element.val, path_id=element.path_id, nested=True, env=env)\n            keyvals.append(val)\n\n        return _build_json(\n            'build_object',\n            args=keyvals,\n            null_safe=True,\n            ser_safe=True,\n            nullable=tvar.nullable,\n            env=env,\n        )\n\n\ndef in_serialization_ctx(ctx: context.CompilerContextLevel) -> bool:\n    return ctx.expr_exposed is None or ctx.expr_exposed\n\n\ndef serialize_custom_tuple(\n    expr: pgast.BaseExpr,\n    *,\n    styperef: irast.TypeRef,\n    env: context.Environment,\n) -> pgast.BaseExpr:\n    \"\"\"Serialize a tuple that needs custom serialization for a component\"\"\"\n    vals: list[pgast.BaseExpr] = []\n\n    obj: pgast.BaseExpr\n\n    if irtyputils.is_persistent_tuple(styperef):\n        for el_idx, el_type in enumerate(styperef.subtypes):\n            val: pgast.BaseExpr = pgast.Indirection(\n                arg=expr,\n                indirection=[\n                    pgast.RecordIndirectionOp(name=str(el_idx)),\n                ],\n            )\n            val = output_as_value(\n                val, ser_typeref=el_type, env=env)\n            vals.append(val)\n\n        obj = _row(vals)\n\n    else:\n        coldeflist = []\n\n        for el_idx, el_type in enumerate(styperef.subtypes):\n\n            coldeflist.append(pgast.ColumnDef(\n                name=str(el_idx),\n                typename=pgast.TypeName(\n                    name=pgtypes.pg_type_from_ir_typeref(el_type),\n                ),\n            ))\n\n            val = pgast.ColumnRef(name=[str(el_idx)])\n\n            val = output_as_value(\n                val, ser_typeref=el_type, env=env)\n\n            vals.append(val)\n\n        obj = _row(vals)\n\n        obj = pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    val=obj,\n                ),\n            ],\n            from_clause=[\n                pgast.RangeFunction(\n                    functions=[\n                        pgast.FuncCall(\n                            name=('unnest',),\n                            args=[\n                                pgast.ArrayExpr(\n                                    elements=[expr],\n                                )\n                            ],\n                            coldeflist=coldeflist,\n                        )\n                    ]\n                )\n            ] if styperef.subtypes else []\n        )\n\n    if expr.nullable:\n        obj = pgast.SelectStmt(\n            target_list=[pgast.ResTarget(val=obj)],\n            where_clause=pgast.NullTest(arg=expr, negated=True)\n        )\n    return obj\n\n\ndef serialize_custom_array(\n    expr: pgast.BaseExpr,\n    *,\n    styperef: irast.TypeRef,\n    env: context.Environment,\n) -> pgast.BaseExpr:\n    \"\"\"Serialize an array that needs custom serialization for a component\"\"\"\n    el_type = styperef.subtypes[0]\n    is_tuple = irtyputils.is_tuple(el_type)\n\n    if is_tuple:\n        coldeflist = []\n\n        out_alias = env.aliases.get('q')\n\n        val: pgast.BaseExpr\n        args: list[pgast.BaseExpr] = []\n        is_named = any(st.element_name for st in el_type.subtypes)\n        for i, st in enumerate(el_type.subtypes):\n            if is_named:\n                colname = st.element_name\n                assert colname\n                args.append(pgast.StringConstant(val=colname))\n            else:\n                colname = str(i)\n\n            val = pgast.ColumnRef(name=[colname])\n            val = output_as_value(val, ser_typeref=st, env=env)\n\n            args.append(val)\n\n            if not irtyputils.is_persistent_tuple(el_type):\n                # Column definition list is only allowed for functions\n                # returning \"record\", i.e. an anonymous tuple, which\n                # would not be the case for schema-persistent tuple types.\n                coldeflist.append(\n                    pgast.ColumnDef(\n                        name=colname,\n                        typename=pgast.TypeName(\n                            name=pgtypes.pg_type_from_ir_typeref(st)\n                        )\n                    )\n                )\n\n        agg_arg: pgast.BaseExpr = _row(args)\n\n        return pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    val=pgast.CoalesceExpr(\n                        args=[\n                            pgast.FuncCall(\n                                name=('array_agg',),\n                                args=[agg_arg],\n                            ),\n                            pgast.TypeCast(\n                                arg=pgast.ArrayExpr(elements=[]),\n                                type_name=pgast.TypeName(name=('record[]',)),\n                            ),\n                        ]\n                    ),\n                    ser_safe=True,\n                )\n            ],\n            from_clause=[\n                pgast.RangeFunction(\n                    alias=pgast.Alias(aliasname=out_alias),\n                    is_rowsfrom=True,\n                    functions=[\n                        pgast.FuncCall(\n                            name=('unnest',),\n                            args=[expr],\n                            coldeflist=coldeflist,\n                        )\n                    ]\n                )\n            ]\n        )\n    else:\n        el_sql_type = el_type.real_base_type.custom_sql_serialization\n        return pgast.TypeCast(\n            arg=expr,\n            type_name=pgast.TypeName(name=(f'{el_sql_type}[]',)),\n        )\n\n\ndef _row(\n    args: list[pgast.BaseExpr]\n) -> pgast.ImplicitRowExpr | pgast.RowExpr:\n    if len(args) > 1:\n        return pgast.ImplicitRowExpr(args=args)\n    else:\n        return pgast.RowExpr(args=args)\n\n\ndef output_as_value(\n        expr: pgast.BaseExpr, *,\n        ser_typeref: Optional[irast.TypeRef] = None,\n        env: context.Environment) -> pgast.BaseExpr:\n    \"\"\"Format an expression as a proper value.\n\n    Normally this just means packing TupleVars into real expressions,\n    but if ser_typeref is provided, we also will do binary serialization.\n\n    In particular, certain types actually need to be serialized as text or\n    or some other format, and we handle that here.\n    \"\"\"\n\n    needs_custom_serialization = ser_typeref and (\n        irtyputils.needs_custom_serialization(ser_typeref))\n\n    val = expr\n\n    if isinstance(expr, pgast.TupleVar):\n        if (\n            env.output_format is context.OutputFormat.NATIVE_INTERNAL\n            and len(expr.elements) == 1\n            and (path_id := (el0 := expr.elements[0]).path_id) is not None\n            and (rptr_name := path_id.rptr_name()) is not None\n            and (rptr_name.name == 'id')\n        ):\n            # This is is a special mode whereby bare refs to objects\n            # are serialized to UUID values.\n            return output_as_value(el0.val, env=env)\n\n        ser_typerefs = [\n            ser_typeref.subtypes[i]\n            if ser_typeref and ser_typeref.subtypes else None\n            for i in range(len(expr.elements))\n        ]\n        val = _row([\n            output_as_value(e.val, ser_typeref=ser_typerefs[i], env=env)\n            for i, e in enumerate(expr.elements)\n        ])\n\n        if (expr.typeref is not None\n                and not needs_custom_serialization\n                and not env.singleton_mode\n                and irtyputils.is_persistent_tuple(expr.typeref)):\n            pg_type = pgtypes.pg_type_from_ir_typeref(expr.typeref)\n            val = pgast.TypeCast(\n                arg=val,\n                type_name=pgast.TypeName(\n                    name=pg_type,\n                ),\n            )\n\n    elif (needs_custom_serialization and not expr.ser_safe):\n        assert ser_typeref is not None\n        if irtyputils.is_array(ser_typeref):\n            return serialize_custom_array(expr, styperef=ser_typeref, env=env)\n        elif irtyputils.is_tuple(ser_typeref):\n            return serialize_custom_tuple(expr, styperef=ser_typeref, env=env)\n        else:\n            el_sql_type = ser_typeref.real_base_type.custom_sql_serialization\n            assert el_sql_type is not None\n            val = pgast.TypeCast(\n                arg=val,\n                type_name=pgast.TypeName(name=(el_sql_type,)),\n            )\n\n    return val\n\n\ndef add_null_test(expr: pgast.BaseExpr, query: pgast.SelectStmt) -> None:\n    if not expr.nullable:\n        return\n\n    while isinstance(expr, pgast.TupleVar) and expr.elements:\n        expr = expr.elements[0].val\n\n    query.where_clause = astutils.extend_binop(\n        query.where_clause,\n        pgast.NullTest(arg=expr, negated=True)\n    )\n\n\ndef serialize_expr_if_needed(\n        expr: pgast.BaseExpr, *,\n        path_id: irast.PathId,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n    if in_serialization_ctx(ctx):\n        val = serialize_expr(expr, path_id=path_id, env=ctx.env)\n    else:\n        val = expr\n\n    return val\n\n\ndef serialize_expr_to_json(\n        expr: pgast.BaseExpr, *,\n        styperef: irast.TypeRef,\n        nested: bool=False,\n        env: context.Environment) -> pgast.BaseExpr:\n\n    val: pgast.BaseExpr\n\n    if isinstance(expr, pgast.TupleVar):\n        val = tuple_var_as_json_object(expr, styperef=styperef, env=env)\n\n    elif isinstance(expr, (pgast.RowExpr, pgast.ImplicitRowExpr)):\n        val = _build_json(\n            'build_array',\n            args=expr.args,\n            null_safe=True,\n            ser_safe=True,\n            env=env,\n        )\n\n    elif irtyputils.is_range(styperef) and not expr.ser_safe:\n        val = pgast.FuncCall(\n            # Use the actual generic helper for converting anyrange to jsonb\n            name=common.maybe_versioned_name(\n                ('edgedb', 'range_to_jsonb'),\n                versioned=env.versioned_stdlib,\n            ),\n            args=[expr], null_safe=True, ser_safe=True)\n        if env.output_format in _JSON_FORMATS:\n            val = pgast.TypeCast(\n                arg=val,\n                type_name=pgast.TypeName(name=('json',))\n            )\n\n    elif irtyputils.is_multirange(styperef) and not expr.ser_safe:\n        val = pgast.FuncCall(\n            # Use the actual generic helper for converting anymultirange to\n            # jsonb\n            name=common.maybe_versioned_name(\n                ('edgedb', 'multirange_to_jsonb'),\n                versioned=env.versioned_stdlib,\n            ),\n            args=[expr], null_safe=True, ser_safe=True)\n        if env.output_format in _JSON_FORMATS:\n            val = pgast.TypeCast(\n                arg=val,\n                type_name=pgast.TypeName(name=('json',))\n            )\n\n    elif irtyputils.is_collection(styperef) and not expr.ser_safe:\n        val = coll_as_json_object(expr, styperef=styperef, env=env)\n\n    elif (\n        styperef.real_base_type.needs_custom_json_cast\n        and not expr.ser_safe\n    ):\n        base = styperef.real_base_type\n        cast_name = s_casts.get_cast_fullname_from_names(\n            base.orig_name_hint or base.name_hint,\n            sn.QualName('std', 'json'),\n        )\n        val = pgast.FuncCall(\n            name=common.get_cast_backend_name(\n                cast_name, aspect='function', versioned=env.versioned_stdlib\n            ),\n            args=[expr], null_safe=True, ser_safe=True)\n        if env.output_format in _JSON_FORMATS:\n            val = pgast.TypeCast(\n                arg=val,\n                type_name=pgast.TypeName(name=('json',))\n            )\n\n    elif not nested:\n        val = pgast.FuncCall(\n            name=_get_json_func('to', env=env),\n            args=[expr], null_safe=True, ser_safe=True)\n\n    else:\n        val = expr\n\n    return val\n\n\ndef serialize_expr(\n        expr: pgast.BaseExpr, *,\n        path_id: irast.PathId,\n        nested: bool=False,\n        env: context.Environment) -> pgast.BaseExpr:\n\n    if env.output_format in (context.OutputFormat.JSON,\n                             context.OutputFormat.JSON_ELEMENTS,\n                             context.OutputFormat.JSONB):\n        val = serialize_expr_to_json(\n            expr, styperef=path_id.target, nested=nested, env=env)\n\n    elif env.output_format in (context.OutputFormat.NATIVE,\n                               context.OutputFormat.NATIVE_INTERNAL,\n                               context.OutputFormat.NONE):\n        val = output_as_value(expr, ser_typeref=path_id.target, env=env)\n\n    else:\n        raise RuntimeError(f'unexpected output format: {env.output_format!r}')\n\n    return val\n\n\ndef get_pg_type(\n        typeref: irast.TypeRef, *,\n        ctx: context.CompilerContextLevel) -> tuple[str, ...]:\n\n    if in_serialization_ctx(ctx):\n        if ctx.env.output_format is context.OutputFormat.JSONB:\n            return ('jsonb',)\n        elif ctx.env.output_format in _JSON_FORMATS:\n            return ('json',)\n        elif irtyputils.is_object(typeref):\n            return ('record',)\n        else:\n            return pgtypes.pg_type_from_ir_typeref(typeref)\n\n    else:\n        return pgtypes.pg_type_from_ir_typeref(typeref)\n\n\ndef aggregate_json_output(\n        stmt: pgast.SelectStmt,\n        ir_set: irast.Set, *,\n        env: context.Environment) -> pgast.SelectStmt:\n\n    subrvar = pgast.RangeSubselect(\n        subquery=stmt,\n        alias=pgast.Alias(\n            aliasname=env.aliases.get('aggw')\n        )\n    )\n\n    stmt_res = stmt.target_list[0]\n\n    if stmt_res.name is None:\n        stmt_res = stmt.target_list[0] = pgast.ResTarget(\n            name=env.aliases.get('v'),\n            val=stmt_res.val,\n        )\n        assert stmt_res.name is not None\n\n    new_val = pgast.CoalesceExpr(\n        args=[\n            pgast.FuncCall(\n                name=_get_json_func('agg', env=env),\n                args=[pgast.ColumnRef(name=[stmt_res.name])]\n            ),\n            pgast.StringConstant(val='[]')\n        ]\n    )\n\n    result = pgast.SelectStmt(\n        target_list=[\n            pgast.ResTarget(\n                val=new_val\n            )\n        ],\n\n        from_clause=[\n            subrvar\n        ]\n    )\n\n    result.ctes = stmt.ctes\n    stmt.ctes = []\n\n    return result\n\n\ndef wrap_script_stmt(\n    stmt: pgast.SelectStmt,\n    *,\n    suppress_all_output: bool = False,\n    env: context.Environment,\n) -> pgast.SelectStmt:\n\n    subrvar = pgast.RangeSubselect(\n        subquery=stmt,\n        alias=pgast.Alias(\n            aliasname=env.aliases.get('aggw')\n        )\n    )\n\n    stmt_res = stmt.target_list[0]\n\n    if stmt_res.name is None:\n        stmt_res = stmt.target_list[0] = pgast.ResTarget(\n            name=env.aliases.get('v'),\n            val=stmt_res.val,\n        )\n        assert stmt_res.name is not None\n\n    count_val = pgast.FuncCall(\n        name=('count',),\n        args=[pgast.ColumnRef(name=[stmt_res.name])]\n    )\n\n    result = pgast.SelectStmt(\n        target_list=[\n            pgast.ResTarget(\n                val=count_val,\n                name=stmt_res.name,\n            ),\n        ],\n\n        from_clause=[\n            subrvar,\n        ]\n    )\n\n    if suppress_all_output:\n        subrvar = pgast.RangeSubselect(\n            subquery=result,\n            alias=pgast.Alias(\n                aliasname=env.aliases.get('q')\n            )\n        )\n\n        result = pgast.SelectStmt(\n            target_list=[],\n            from_clause=[\n                subrvar,\n            ],\n            where_clause=pgast.NullTest(\n                arg=pgast.ColumnRef(\n                    name=[subrvar.alias.aliasname, stmt_res.name],\n                ),\n            ),\n        )\n\n    result.ctes = stmt.ctes\n    stmt.ctes = []\n\n    return result\n\n\ndef top_output_as_value(\n        stmt: pgast.SelectStmt,\n        ir_set: irast.Set, *,\n        env: context.Environment) -> pgast.SelectStmt:\n    \"\"\"Finalize output serialization on the top level.\"\"\"\n\n    if (env.output_format is context.OutputFormat.JSON and\n            not env.expected_cardinality_one):\n        # For JSON we just want to aggregate the whole thing\n        # into a JSON array.\n        return aggregate_json_output(stmt, ir_set, env=env)\n\n    elif (\n        env.explicit_top_cast is not None\n        and (\n            env.output_format is context.OutputFormat.NATIVE\n            or env.output_format is context.OutputFormat.NATIVE_INTERNAL\n        )\n    ):\n        typecast = pgast.TypeCast(\n            arg=stmt.target_list[0].val,\n            type_name=pgast.TypeName(\n                name=pgtypes.pg_type_from_ir_typeref(\n                    env.explicit_top_cast,\n                    persistent_tuples=True,\n                ),\n            ),\n        )\n\n        stmt.target_list[0] = pgast.ResTarget(\n            name=env.aliases.get('v'),\n            val=typecast,\n        )\n\n        return stmt\n\n    elif env.output_format is context.OutputFormat.NONE:\n        return wrap_script_stmt(stmt, env=env)\n\n    else:\n        # JSON_ELEMENTS and BINARY don't require any wrapping\n        return stmt\n"
  },
  {
    "path": "edb/pgsql/compiler/pathctx.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Helpers to manage statement path contexts.\"\"\"\n\n\nfrom __future__ import annotations\n\nfrom typing import Optional, Sequence, TypeGuard\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtyputils\n\nfrom edb.schema import pointers as s_pointers\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import common\nfrom edb.pgsql import types as pg_types\n\nfrom . import astutils\nfrom . import context\nfrom . import enums as pgce\nfrom . import output\n\n\n# A mapping of more specific aspect -> less specific aspect for objects\nOBJECT_ASPECT_SPECIFICITY_MAP = {\n    pgce.PathAspect.IDENTITY: pgce.PathAspect.VALUE,\n    pgce.PathAspect.VALUE: pgce.PathAspect.SOURCE,\n    pgce.PathAspect.SERIALIZED: pgce.PathAspect.SOURCE,\n}\n\n# A mapping of more specific aspect -> less specific aspect for primitives\nPRIMITIVE_ASPECT_SPECIFICITY_MAP = {\n    pgce.PathAspect.SERIALIZED: pgce.PathAspect.VALUE,\n}\n\n\ndef get_less_specific_aspect(\n    path_id: irast.PathId,\n    aspect: pgce.PathAspect,\n) -> Optional[pgce.PathAspect]:\n    if path_id.is_objtype_path():\n        mapping = OBJECT_ASPECT_SPECIFICITY_MAP\n    else:\n        mapping = PRIMITIVE_ASPECT_SPECIFICITY_MAP\n\n    less_specific_aspect = mapping.get(pgce.PathAspect(aspect))\n    if less_specific_aspect is not None:\n        return less_specific_aspect\n    else:\n        return None\n\n\ndef map_path_id(\n        path_id: irast.PathId,\n        path_id_map: dict[irast.PathId, irast.PathId]) -> irast.PathId:\n\n    sorted_map = sorted(\n        path_id_map.items(), key=lambda kv: len(kv[0]), reverse=True)\n\n    for outer_id, inner_id in sorted_map:\n        new_path_id = irtyputils.replace_pathid_prefix(\n            path_id, outer_id, inner_id, permissive_ptr_path=True)\n        if new_path_id != path_id:\n            path_id = new_path_id\n            break\n\n    return path_id\n\n\ndef reverse_map_path_id(\n        path_id: irast.PathId,\n        path_id_map: dict[irast.PathId, irast.PathId]) -> irast.PathId:\n    for outer_id, inner_id in path_id_map.items():\n        new_path_id = irtyputils.replace_pathid_prefix(\n            path_id, inner_id, outer_id)\n        if new_path_id != path_id:\n            path_id = new_path_id\n            break\n\n    return path_id\n\n\ndef put_path_id_mask(\n    stmt: pgast.EdgeQLPathInfo, path_id: irast.PathId\n) -> None:\n    stmt.path_id_mask.add(path_id)\n\n\ndef put_path_id_map(\n    rel: pgast.Query,\n    outer_path_id: irast.PathId,\n    inner_path_id: irast.PathId,\n) -> None:\n    inner_path_id = map_path_id(inner_path_id, rel.view_path_id_map)\n    rel.view_path_id_map[outer_path_id] = inner_path_id\n\n\ndef get_path_var(\n    rel: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    flavor: str='normal',\n    aspect: pgce.PathAspect,\n    env: context.Environment,\n) -> pgast.BaseExpr:\n    \"\"\"\n    Return a value expression for a given *path_id* in a given *rel*.\n\n    This function is a part of \"recursive column injection\" algorithm,\n    described in [./ARCHITECTURE.md].\n    \"\"\"\n    if isinstance(rel, pgast.CommonTableExpr):\n        rel = rel.query\n\n    if flavor == 'normal':\n        if rel.view_path_id_map:\n            path_id = map_path_id(path_id, rel.view_path_id_map)\n\n        if (path_id, aspect) in rel.path_namespace:\n            return rel.path_namespace[path_id, aspect]\n    elif flavor == 'packed':\n        if (\n            rel.packed_path_namespace\n            and (path_id, aspect) in rel.packed_path_namespace\n        ):\n            return rel.packed_path_namespace[path_id, aspect]\n\n    if astutils.is_set_op_query(rel):\n        return _get_path_var_in_setop(\n            rel, path_id, aspect=aspect, flavor=flavor, env=env)\n\n    ptrref = path_id.rptr()\n    ptrref_dir = path_id.rptr_dir()\n    is_type_intersection = path_id.is_type_intersection_path()\n\n    src_path_id: Optional[irast.PathId] = None\n    if ptrref is not None and not is_type_intersection:\n        ptr_info = pg_types.get_ptrref_storage_info(\n            ptrref, resolve_type=False, link_bias=False, allow_missing=True)\n        ptr_dir = path_id.rptr_dir()\n        is_inbound = ptr_dir == s_pointers.PointerDirection.Inbound\n        if is_inbound:\n            src_path_id = path_id\n        else:\n            src_path_id = path_id.src_path()\n            assert src_path_id is not None\n            src_rptr = src_path_id.rptr()\n            if (\n                irtyputils.is_id_ptrref(ptrref)\n                and (\n                    src_rptr is None\n                    or ptrref_dir is not s_pointers.PointerDirection.Inbound\n                )\n            ):\n                # When there is a reference to the id property of\n                # an object which is linked to by a link stored\n                # inline, we want to route the reference to the\n                # inline attribute.  For example,\n                # Foo.__type__.id gets resolved to the Foo.__type__\n                # column.  This can only be done if Foo is visible\n                # in scope, and Foo.__type__ is not a computable.\n                pid = src_path_id\n                while pid.is_type_intersection_path():\n                    # Skip type intersection step(s).\n                    src_pid = pid.src_path()\n                    if src_pid is not None:\n                        src_rptr = src_pid.rptr()\n                        pid = src_pid\n                    else:\n                        break\n\n                if (src_rptr is not None\n                        and not irtyputils.is_computable_ptrref(src_rptr)\n                        and env.ptrref_source_visibility.get(src_rptr)):\n                    src_ptr_info = pg_types.get_ptrref_storage_info(\n                        src_rptr, resolve_type=False, link_bias=False,\n                        allow_missing=True)\n                    if (src_ptr_info\n                            and src_ptr_info.table_type == 'ObjectType'):\n                        src_path_id = src_path_id.src_path()\n                        ptr_info = src_ptr_info\n\n    else:\n        ptr_info = None\n        ptr_dir = None\n\n    var: Optional[pgast.BaseExpr]\n\n    if ptrref is None:\n        if len(path_id) == 1:\n            # This is an scalar set derived from an expression.\n            src_path_id = path_id\n\n    elif ptrref.source_ptr is not None:\n        if ptr_info and ptr_info.table_type != 'link' and not is_inbound:\n            # This is a link prop that is stored in source rel,\n            # step back to link source rvar.\n            _prefix_pid = path_id.src_path()\n            assert _prefix_pid is not None\n            src_path_id = _prefix_pid.src_path()\n\n    elif is_type_intersection:\n        src_path_id = path_id\n\n    assert src_path_id is not None\n\n    # Find which rvar will have path_id as an output\n    src_aspect, rel_rvar, found_path_var = _find_rel_rvar(\n        rel, path_id, src_path_id, aspect=aspect, flavor=flavor\n    )\n\n    if found_path_var:\n        return found_path_var\n\n    # Slight hack: Inject the __type__ field of a FreeObject when necessary\n    if (\n        rel_rvar is None\n        and ptrref\n        and ptrref.shortname.name == '__type__'\n        and irtyputils.is_free_object(src_path_id.target)\n    ):\n        return astutils.compile_typeref(src_path_id.target.real_material_type)\n\n    if isinstance(rel_rvar, pgast.DynamicRangeVar):\n        var = rel_rvar.dynamic_get_path(\n            rel, path_id, flavor=flavor, aspect=aspect, env=env)\n        if isinstance(var, pgast.PathRangeVar):\n            rel_rvar = var\n        elif var:\n            put_path_var(rel, path_id, var, aspect=aspect, flavor=flavor)\n            return var\n        else:\n            rel_rvar = None\n\n    if rel_rvar is None:\n        raise LookupError(\n            f'there is no range var for '\n            f'{src_path_id} {src_aspect} in {rel}')\n\n    if isinstance(rel_rvar, pgast.IntersectionRangeVar):\n        if (\n            (path_id.is_objtype_path() and src_path_id == path_id)\n            or (ptrref is not None and irtyputils.is_id_ptrref(ptrref))\n        ):\n            rel_rvar = rel_rvar.component_rvars[-1]\n        else:\n            # Intersection rvars are basically JOINs of the relevant\n            # parts of the type intersection, and so we need to make\n            # sure we pick the correct component relation of that JOIN.\n            rel_rvar = _find_rvar_in_intersection_by_typeref(\n                path_id,\n                rel_rvar.component_rvars,\n            )\n\n    source_rel = rel_rvar.query\n\n    outvar = get_path_output(\n        source_rel, path_id, aspect=aspect, flavor=flavor, env=env)\n\n    var = astutils.get_rvar_var(rel_rvar, outvar)\n    put_path_var(rel, path_id, var, aspect=aspect, flavor=flavor)\n\n    if isinstance(var, pgast.TupleVar):\n        for element in var.elements:\n            put_path_var_if_not_exists(\n                rel, element.path_id, element.val, flavor=flavor, aspect=aspect\n            )\n\n    return var\n\n\ndef _find_rel_rvar(\n    rel: pgast.Query,\n    path_id: irast.PathId,\n    src_path_id: irast.PathId,\n    *,\n    aspect: pgce.PathAspect,\n    flavor: str,\n) -> tuple[str, Optional[pgast.PathRangeVar], Optional[pgast.BaseExpr]]:\n    \"\"\"Rummage around rel looking for an appropriate rvar for path_id.\n\n    Somewhat unfortunately, some checks to find the actual path var\n    (in a particular tuple case) need to occur in the middle of the\n    rvar rel search, so we can also find the actual path var in passing.\n    \"\"\"\n    src_aspect = aspect\n    rel_rvar = maybe_get_path_rvar(rel, path_id, aspect=aspect, flavor=flavor)\n\n    if rel_rvar is None:\n        alt_aspect = get_less_specific_aspect(path_id, aspect)\n        if alt_aspect is not None:\n            rel_rvar = maybe_get_path_rvar(rel, path_id, aspect=alt_aspect)\n    else:\n        alt_aspect = None\n\n    if rel_rvar is None:\n        if flavor == 'packed':\n            src_aspect = aspect\n        elif src_path_id.is_objtype_path():\n            src_aspect = pgce.PathAspect.SOURCE\n        else:\n            src_aspect = aspect\n\n        if src_path_id.is_tuple_path():\n            if src_aspect == pgce.PathAspect.IDENTITY:\n                src_aspect = pgce.PathAspect.VALUE\n\n            if var := _find_in_output_tuple(rel, path_id, src_aspect):\n                return src_aspect, None, var\n\n            rel_rvar = maybe_get_path_rvar(rel, src_path_id, aspect=src_aspect)\n\n            if rel_rvar is None:\n                _src_path_id_prefix = src_path_id.src_path()\n                if _src_path_id_prefix is not None:\n                    rel_rvar = maybe_get_path_rvar(\n                        rel, _src_path_id_prefix, aspect=src_aspect\n                    )\n        else:\n            rel_rvar = maybe_get_path_rvar(rel, src_path_id, aspect=src_aspect)\n\n        if (\n            rel_rvar is None\n            and src_aspect != pgce.PathAspect.SOURCE\n            and path_id != src_path_id\n        ):\n            rel_rvar = maybe_get_path_rvar(\n                rel,\n                src_path_id,\n                aspect=pgce.PathAspect.SOURCE\n            )\n\n    if rel_rvar is None and alt_aspect is not None and flavor == 'normal':\n        # There is no source range var for the requested aspect,\n        # check if there is a cached var with less specificity.\n        var = rel.path_namespace.get((path_id, alt_aspect))\n        if var is not None:\n            put_path_var(rel, path_id, var, aspect=aspect, flavor=flavor)\n            return src_aspect, None, var\n\n    return src_aspect, rel_rvar, None\n\n\ndef _get_path_var_in_setop(\n    rel: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    aspect: pgce.PathAspect,\n    flavor: str,\n    env: context.Environment,\n) -> pgast.BaseExpr:\n    test_vals = []\n    if aspect in (pgce.PathAspect.VALUE, pgce.PathAspect.SERIALIZED):\n        test_vals = [\n            maybe_get_path_var(q, env=env, path_id=path_id, aspect=aspect)\n            for q in astutils.each_query_in_set(rel)\n        ]\n\n    # In order to ensure output balance, we only want to output\n    # a TupleVar if *every* subquery outputs a TupleVar.\n    # If some but not all output TupleVars, we need to fix up\n    # the output TupleVars by outputting them as a real tuple.\n    # This is needed for cases like `(Foo.bar UNION (1,2))`.\n    if (\n        any(isinstance(x, pgast.TupleVarBase) for x in test_vals)\n        and not all(isinstance(x, pgast.TupleVarBase) for x in test_vals)\n    ):\n        for subrel in astutils.each_query_in_set(rel):\n            cur = get_path_var(\n                subrel, env=env, path_id=path_id, aspect=aspect)\n            assert flavor == 'normal'\n            if isinstance(cur, pgast.TupleVarBase):\n                new = output.output_as_value(cur, env=env)\n                new_path_id = map_path_id(path_id, subrel.view_path_id_map)\n                put_path_var(\n                    subrel, new_path_id, new, force=True, aspect=aspect\n                )\n\n    # We disable the find_path_output optimization when doing\n    # UNIONs to avoid situations where they have different numbers\n    # of columns.\n    outputs = [\n        get_path_output_or_null(\n            q,\n            env=env,\n            disable_output_fusion=True,\n            path_id=path_id,\n            aspect=aspect,\n            flavor=flavor\n        ) for q in astutils.each_query_in_set(rel)\n    ]\n\n    counts = [len(x.target_list) for x in astutils.each_query_in_set(rel)]\n    assert counts == [counts[0]] * len(counts)\n\n    first: Optional[pgast.OutputVar] = None\n    optional = False\n    all_null = True\n    nullable = False\n\n    for colref, is_null in outputs:\n        if colref.nullable:\n            nullable = True\n        if first is None:\n            first = colref\n        if is_null:\n            optional = True\n        else:\n            all_null = False\n\n    # Fail if no subquery had the path or, for scalar identity paths,\n    # if any did not have it.\n    #\n    # We need to do this for scalar identity because scalar identity\n    # is only used for volatility refs, and it is OK if looking it up\n    # fails, because we create a backup volatility ref---but it is\n    # *not* OK for it to succeed and produce NULL in some cases.\n    if all_null or (\n        aspect == pgce.PathAspect.IDENTITY\n        and optional and not path_id.is_objtype_path()\n    ):\n        # If *none* of the subqueries had it, we have to remove them all\n        # before erroring, lest a future call see them and decide\n        # they really exist.\n        for subrel in astutils.each_query_in_set(rel):\n            assert flavor == 'normal'\n            new_path_id = map_path_id(path_id, subrel.view_path_id_map)\n            del subrel.path_outputs[new_path_id, aspect]\n            subrel.target_list.pop()\n\n        raise LookupError(\n            f'cannot find refs for '\n            f'path {path_id} {aspect} in {rel}')\n\n    if first is None:\n        raise AssertionError(\n            f'union did not produce any outputs')\n\n    # Path vars produced by UNION expressions can be \"optional\",\n    # i.e the record is accepted as-is when such var is NULL.\n    # This is necessary to correctly join heterogeneous UNIONs.\n    var = astutils.strip_output_var(\n        first, optional=optional, nullable=optional or nullable)\n    put_path_var(rel, path_id, var, aspect=aspect, flavor=flavor)\n    return var\n\n\ndef _find_rvar_in_intersection_by_typeref(\n    path_id: irast.PathId,\n    component_rvars: Sequence[pgast.PathRangeVar],\n) -> pgast.PathRangeVar:\n\n    assert component_rvars\n\n    if src_path := path_id.src_path():\n        tref = src_path.target\n    else:\n        tref = path_id.target\n\n    for component_rvar in component_rvars:\n        if (\n            component_rvar.typeref is not None\n            and irtyputils.type_contains(tref, component_rvar.typeref)\n        ):\n            rel_rvar = component_rvar\n            break\n    else:\n        raise AssertionError(\n            f'no rvar in intersection matches path id {path_id}'\n        )\n\n    return rel_rvar\n\n\ndef _find_in_output_tuple(\n    rel: pgast.Query, path_id: irast.PathId, aspect: pgce.PathAspect\n) -> Optional[pgast.BaseExpr]:\n    \"\"\"Try indirecting a source tuple already present as an output.\n\n    Normally tuple indirections are handled by\n    process_set_as_tuple_indirection, but UNIONing an explicit tuple with a\n    tuple coming from a base relation (like `(Foo.bar UNION (1,2)).0`)\n    can lead to us looking for a tuple path in relations that only have\n    the actual full tuple.\n    (See test_edgeql_coalesce_tuple_{08,09}).\n\n    We handle this by checking whether some prefix of the tuple path\n    is present in the path_outputs.\n\n    This is sufficient because the relevant cases are all caused by\n    set ops, and the \"fixup\" done in set op cases ensures that the\n    tuple will be already present.\n    \"\"\"\n\n    steps = []\n    src_path_id = path_id.src_path()\n    ptrref = path_id.rptr()\n    while (\n        src_path_id\n        and src_path_id.is_tuple_path()\n        and isinstance(ptrref, irast.TupleIndirectionPointerRef)\n    ):\n        steps.append((ptrref.shortname.name, src_path_id))\n\n        if (\n            (var := rel.path_namespace.get((src_path_id, aspect)))\n            and not isinstance(var, pgast.TupleVarBase)\n        ):\n            for name, src in reversed(steps):\n                var = astutils.tuple_getattr(var, src.target, name)\n            put_path_var(rel, path_id, var, aspect=aspect)\n            return var\n\n        ptrref = src_path_id.rptr()\n        src_path_id = src_path_id.src_path()\n\n    return None\n\n\ndef get_path_identity_var(\n    rel: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    env: context.Environment,\n) -> pgast.BaseExpr:\n    return get_path_var(rel, path_id, aspect=pgce.PathAspect.IDENTITY, env=env)\n\n\ndef get_path_value_var(\n    rel: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    env: context.Environment,\n) -> pgast.BaseExpr:\n    return get_path_var(rel, path_id, aspect=pgce.PathAspect.VALUE, env=env)\n\n\ndef is_relation_rvar(\n    rvar: pgast.BaseRangeVar,\n) -> bool:\n    return (\n        isinstance(rvar, pgast.RelRangeVar) and\n        is_terminal_relation(rvar.query)\n    )\n\n\ndef is_terminal_relation(\n    rel: pgast.BaseRelation\n) -> TypeGuard[pgast.Relation | pgast.NullRelation]:\n    return isinstance(rel, (pgast.Relation, pgast.NullRelation))\n\n\ndef is_values_relation(\n    rel: pgast.BaseRelation,\n) -> bool:\n    return bool(getattr(rel, 'values', None))\n\n\ndef maybe_get_path_var(\n    rel: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    aspect: pgce.PathAspect,\n    flavor: str='normal',\n    env: context.Environment,\n) -> Optional[pgast.BaseExpr]:\n    try:\n        return get_path_var(\n            rel, path_id, aspect=aspect, flavor=flavor, env=env)\n    except LookupError:\n        return None\n\n\ndef maybe_get_path_identity_var(\n    rel: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    env: context.Environment,\n) -> Optional[pgast.BaseExpr]:\n    try:\n        return get_path_var(\n            rel, path_id, aspect=pgce.PathAspect.IDENTITY, env=env\n        )\n    except LookupError:\n        return None\n\n\ndef maybe_get_path_value_var(\n    rel: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    env: context.Environment,\n) -> Optional[pgast.BaseExpr]:\n    try:\n        return get_path_var(\n            rel, path_id, aspect=pgce.PathAspect.VALUE, env=env\n        )\n    except LookupError:\n        return None\n\n\ndef maybe_get_path_serialized_var(\n    rel: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    env: context.Environment,\n) -> Optional[pgast.BaseExpr]:\n    try:\n        return get_path_var(\n            rel, path_id, aspect=pgce.PathAspect.SERIALIZED, env=env\n        )\n    except LookupError:\n        return None\n\n\ndef put_path_var(\n    rel: pgast.BaseRelation,\n    path_id: irast.PathId,\n    var: pgast.BaseExpr,\n    *,\n    aspect: pgce.PathAspect,\n    flavor: str = 'normal',\n    force: bool = False,\n) -> None:\n    if flavor == 'packed':\n        if rel.packed_path_namespace is None:\n            rel.packed_path_namespace = {}\n        path_namespace = rel.packed_path_namespace\n    else:\n        path_namespace = rel.path_namespace\n\n    if (path_id, aspect) in path_namespace and not force:\n        raise KeyError(\n            f'{aspect} of {path_id} is already present in {rel}')\n    path_namespace[path_id, aspect] = var\n\n\ndef put_path_var_if_not_exists(\n    rel: pgast.Query,\n    path_id: irast.PathId,\n    var: pgast.BaseExpr,\n    *,\n    flavor: str = 'normal',\n    aspect: pgce.PathAspect,\n) -> None:\n    try:\n        put_path_var(rel, path_id, var, aspect=aspect, flavor=flavor)\n    except KeyError:\n        pass\n\n\ndef put_path_identity_var(\n    rel: pgast.BaseRelation,\n    path_id: irast.PathId,\n    var: pgast.BaseExpr,\n    *,\n    force: bool = False,\n) -> None:\n    put_path_var(\n        rel, path_id, var, aspect=pgce.PathAspect.IDENTITY, force=force\n    )\n\n\ndef put_path_value_var(\n    rel: pgast.BaseRelation,\n    path_id: irast.PathId,\n    var: pgast.BaseExpr,\n    *,\n    force: bool = False,\n) -> None:\n    put_path_var(\n        rel, path_id, var, aspect=pgce.PathAspect.VALUE, force=force\n    )\n\n\ndef put_path_serialized_var(\n    rel: pgast.BaseRelation,\n    path_id: irast.PathId,\n    var: pgast.BaseExpr,\n    *,\n    force: bool = False,\n) -> None:\n    put_path_var(\n        rel, path_id, var, aspect=pgce.PathAspect.SERIALIZED, force=force\n    )\n\n\ndef put_path_value_var_if_not_exists(\n    rel: pgast.BaseRelation,\n    path_id: irast.PathId,\n    var: pgast.BaseExpr,\n    *,\n    force: bool = False,\n) -> None:\n    try:\n        put_path_var(\n            rel, path_id, var, aspect=pgce.PathAspect.VALUE, force=force\n        )\n    except KeyError:\n        pass\n\n\ndef put_path_serialized_var_if_not_exists(\n    rel: pgast.BaseRelation,\n    path_id: irast.PathId,\n    var: pgast.BaseExpr,\n    *,\n    force: bool = False,\n) -> None:\n    try:\n        put_path_var(\n            rel,\n            path_id,\n            var,\n            aspect=pgce.PathAspect.SERIALIZED,\n            force=force,\n        )\n    except KeyError:\n        pass\n\n\ndef put_path_bond(\n    stmt: pgast.BaseRelation, path_id: irast.PathId, iterator: bool=False\n) -> None:\n    '''Register a path id that should be joined on when joining stmt\n\n    iterator indicates whether the identity or iterator aspect should\n    be used.\n    '''\n    stmt.path_bonds.add((path_id, iterator))\n\n\ndef put_rvar_path_bond(\n        rvar: pgast.PathRangeVar, path_id: irast.PathId) -> None:\n    put_path_bond(rvar.query, path_id)\n\n\ndef get_path_output_alias(\n    path_id: irast.PathId,\n    aspect: pgce.PathAspect,\n    *,\n    env: context.Environment,\n) -> str:\n    rptr = path_id.rptr()\n    if rptr is not None:\n        alias_base = rptr.shortname.name\n    elif path_id.is_collection_path():\n        assert path_id.target.collection is not None\n        alias_base = path_id.target.collection\n    else:\n        alias_base = path_id.target_name_hint.name\n\n    return env.aliases.get(f'{alias_base}_{aspect}')\n\n\ndef get_rvar_path_var(\n    rvar: pgast.PathRangeVar,\n    path_id: irast.PathId,\n    aspect: pgce.PathAspect,\n    *,\n    flavor: str='normal',\n    env: context.Environment,\n) -> pgast.OutputVar:\n    \"\"\"Return ColumnRef for a given *path_id* in a given *range var*.\"\"\"\n    outvar = get_path_output(\n        rvar.query, path_id, aspect=aspect, flavor=flavor, env=env)\n    return astutils.get_rvar_var(rvar, outvar)\n\n\ndef put_rvar_path_output(\n    rvar: pgast.PathRangeVar,\n    path_id: irast.PathId,\n    aspect: pgce.PathAspect,\n    var: pgast.OutputVar,\n) -> None:\n    _put_path_output_var(rvar.query, path_id, aspect, var)\n\n\ndef maybe_get_rvar_path_var(\n    rvar: pgast.PathRangeVar,\n    path_id: irast.PathId,\n    *,\n    aspect: pgce.PathAspect,\n    flavor: str='normal',\n    env: context.Environment,\n) -> Optional[pgast.OutputVar]:\n    try:\n        return get_rvar_path_var(\n            rvar, path_id, aspect=aspect, flavor=flavor, env=env)\n    except LookupError:\n        return None\n\n\ndef get_rvar_path_identity_var(\n    rvar: pgast.PathRangeVar,\n    path_id: irast.PathId,\n    *,\n    env: context.Environment,\n) -> pgast.OutputVar:\n    return get_rvar_path_var(\n        rvar, path_id, aspect=pgce.PathAspect.IDENTITY, env=env\n    )\n\n\ndef get_rvar_path_value_var(\n    rvar: pgast.PathRangeVar,\n    path_id: irast.PathId,\n    *,\n    env: context.Environment,\n) -> pgast.OutputVar:\n    return get_rvar_path_var(\n        rvar, path_id, aspect=pgce.PathAspect.VALUE, env=env\n    )\n\n\ndef get_rvar_output_var_as_col_list(\n    rvar: pgast.PathRangeVar,\n    outvar: pgast.OutputVar,\n    aspect: pgce.PathAspect,\n    *,\n    env: context.Environment,\n) -> list[pgast.OutputVar]:\n\n    cols: list[pgast.OutputVar]\n\n    if isinstance(outvar, pgast.TupleVarBase):\n        cols = []\n        for el in outvar.elements:\n            col = get_rvar_path_var(rvar, el.path_id, aspect=aspect, env=env)\n            cols.extend(get_rvar_output_var_as_col_list(\n                rvar, col, aspect=aspect, env=env))\n    else:\n        cols = [outvar]\n\n    return cols\n\n\ndef put_path_rvar(\n    stmt: pgast.Query,\n    path_id: irast.PathId,\n    rvar: pgast.PathRangeVar,\n    *,\n    flavor: str = 'normal',\n    aspect: pgce.PathAspect,\n) -> None:\n    assert isinstance(path_id, irast.PathId)\n    stmt.get_rvar_map(flavor)[path_id, aspect] = rvar\n\n\ndef put_path_value_rvar(\n    stmt: pgast.Query,\n    path_id: irast.PathId,\n    rvar: pgast.PathRangeVar,\n    *,\n    flavor: str = 'normal',\n) -> None:\n    put_path_rvar(\n        stmt, path_id, rvar, aspect=pgce.PathAspect.VALUE, flavor=flavor\n    )\n\n\ndef put_path_source_rvar(\n    stmt: pgast.Query,\n    path_id: irast.PathId,\n    rvar: pgast.PathRangeVar,\n    *,\n    flavor: str = 'normal',\n) -> None:\n    put_path_rvar(\n        stmt, path_id, rvar, aspect=pgce.PathAspect.SOURCE, flavor=flavor\n    )\n\n\ndef has_rvar(stmt: pgast.Query, rvar: pgast.PathRangeVar) -> bool:\n    return any(\n        rvar in set(stmt.get_rvar_map(flavor).values())\n        for flavor in ('normal', 'packed')\n    )\n\n\ndef put_path_rvar_if_not_exists(\n    stmt: pgast.Query,\n    path_id: irast.PathId,\n    rvar: pgast.PathRangeVar,\n    *,\n    flavor: str = 'normal',\n    aspect: pgce.PathAspect,\n) -> None:\n    if (path_id, aspect) not in stmt.get_rvar_map(flavor):\n        put_path_rvar(stmt, path_id, rvar, aspect=aspect, flavor=flavor)\n\n\ndef get_path_rvar(\n    stmt: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    flavor: str = 'normal',\n    aspect: pgce.PathAspect,\n) -> pgast.PathRangeVar:\n    rvar = maybe_get_path_rvar(stmt, path_id, aspect=aspect, flavor=flavor)\n    if rvar is None:\n        raise LookupError(\n            f'there is no range var for {path_id} {aspect} in {stmt}')\n    return rvar\n\n\ndef maybe_get_path_rvar(\n    stmt: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    aspect: pgce.PathAspect,\n    flavor: str = 'normal',\n) -> Optional[pgast.PathRangeVar]:\n    rvar = None\n    path_rvar_map = stmt.maybe_get_rvar_map(flavor)\n    if path_rvar_map is not None:\n        if path_rvar_map:\n            rvar = path_rvar_map.get((path_id, aspect))\n        if rvar is None and aspect == pgce.PathAspect.IDENTITY:\n            rvar = path_rvar_map.get((path_id, pgce.PathAspect.VALUE))\n    return rvar\n\n\ndef _has_path_aspect(\n    stmt: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    aspect: pgce.PathAspect,\n) -> bool:\n    key = path_id, aspect\n    return (\n        key in stmt.path_rvar_map\n        or key in stmt.path_namespace\n        or key in stmt.path_outputs\n    )\n\n\ndef has_path_aspect(\n    stmt: pgast.Query, path_id: irast.PathId, *, aspect: pgce.PathAspect\n) -> bool:\n    path_id = map_path_id(path_id, stmt.view_path_id_map)\n    return _has_path_aspect(stmt, path_id, aspect=aspect)\n\n\ndef list_path_aspects(\n    stmt: pgast.Query, path_id: irast.PathId\n) -> set[pgce.PathAspect]:\n    path_aspects = (\n        pgce.PathAspect.VALUE,\n        pgce.PathAspect.IDENTITY,\n        pgce.PathAspect.SOURCE,\n        pgce.PathAspect.SERIALIZED,\n    )\n\n    path_id = map_path_id(path_id, stmt.view_path_id_map)\n    return {\n        aspect for aspect in path_aspects\n        if _has_path_aspect(stmt, path_id, aspect=aspect)\n    }\n\n\ndef maybe_get_path_value_rvar(\n    stmt: pgast.Query, path_id: irast.PathId\n) -> Optional[pgast.BaseRangeVar]:\n    return maybe_get_path_rvar(stmt, path_id, aspect=pgce.PathAspect.VALUE)\n\n\ndef _same_expr(expr1: pgast.BaseExpr, expr2: pgast.BaseExpr) -> bool:\n    if (isinstance(expr1, pgast.ColumnRef) and\n            isinstance(expr2, pgast.ColumnRef)):\n        return expr1.name == expr2.name\n    else:\n        return expr1 == expr2\n\n\ndef put_path_packed_output(\n    rel: pgast.EdgeQLPathInfo,\n    path_id: irast.PathId,\n    val: pgast.OutputVar,\n    aspect: pgce.PathAspect=pgce.PathAspect.VALUE,\n) -> None:\n    if rel.packed_path_outputs is None:\n        rel.packed_path_outputs = {}\n    rel.packed_path_outputs[path_id, aspect] = val\n\n\ndef _put_path_output_var(\n    rel: pgast.BaseRelation,\n    path_id: irast.PathId,\n    aspect: pgce.PathAspect,\n    var: pgast.OutputVar,\n    *,\n    flavor: str = 'normal',\n) -> None:\n    if flavor == 'packed':\n        put_path_packed_output(rel, path_id, var, aspect)\n    else:\n        rel.path_outputs[path_id, aspect] = var\n\n\ndef _get_rel_object_id_output(\n    rel: pgast.BaseRelation,\n    path_id: irast.PathId,\n    *,\n    aspect: pgce.PathAspect,\n    env: context.Environment,\n) -> pgast.OutputVar:\n\n    var = rel.path_outputs.get((path_id, aspect))\n    if var is not None:\n        return var\n\n    if isinstance(rel, pgast.NullRelation):\n        name = env.aliases.get('id')\n\n        val = pgast.TypeCast(\n            arg=pgast.NullConstant(),\n            type_name=pgast.TypeName(\n                name=('uuid',),\n            )\n        )\n\n        rel.target_list.append(pgast.ResTarget(name=name, val=val))\n        result = pgast.ColumnRef(name=[name], nullable=True)\n\n    else:\n        result = pgast.ColumnRef(name=['id'], nullable=False)\n\n    _put_path_output_var(rel, path_id, aspect, result)\n\n    return result\n\n\ndef _get_rel_path_output(\n    rel: pgast.Relation | pgast.NullRelation,\n    path_id: irast.PathId,\n    *,\n    aspect: pgce.PathAspect,\n    flavor: str,\n    env: context.Environment,\n) -> pgast.OutputVar:\n\n    if path_id.is_objtype_path():\n        if aspect == pgce.PathAspect.IDENTITY:\n            aspect = pgce.PathAspect.VALUE\n\n        if aspect != pgce.PathAspect.VALUE:\n            raise LookupError(\n                f'invalid request for non-scalar path {path_id} {aspect}')\n\n        if (path_id == rel.path_id or\n                (rel.path_id and\n                 rel.path_id.is_type_intersection_path() and\n                 path_id == rel.path_id.src_path())):\n\n            return _get_rel_object_id_output(\n                rel, path_id, aspect=aspect, env=env)\n    else:\n        if aspect == pgce.PathAspect.IDENTITY:\n            raise LookupError(\n                f'invalid request for scalar path {path_id} {aspect}')\n\n        elif aspect == pgce.PathAspect.SERIALIZED:\n            aspect = pgce.PathAspect.VALUE\n\n    var = rel.path_outputs.get((path_id, aspect))\n    if var is not None:\n        return var\n\n    # The ptrref from the path id may be from a super type of the\n    # actual type this relation corresponds to. We know the relation's\n    # type, so find the real ptrref that corresponds to the current\n    # type (since the column names will be different in the parent\n    # and child tables).\n    rptr_dir = path_id.rptr_dir()\n    ptrref = path_id.rptr()\n    if isinstance(ptrref, irast.PointerRef) and rel.type_or_ptr_ref:\n        typeref = rel.type_or_ptr_ref\n        if isinstance(typeref, irast.PointerRef):\n            typeref = typeref.out_source\n        assert rptr_dir\n        actual_ptrref = irtyputils.maybe_find_actual_ptrref(\n            typeref, ptrref, dir=rptr_dir)\n        if actual_ptrref:\n            ptrref = actual_ptrref\n\n    ptr_info = None\n    if ptrref and not isinstance(ptrref, irast.TypeIntersectionPointerRef):\n        ptr_info = pg_types.get_ptrref_storage_info(\n            ptrref,\n            resolve_type=False,\n            link_bias=bool(rel.path_id and rel.path_id.is_ptr_path()),\n        )\n\n    if (rptr_dir is not None and\n            rptr_dir != s_pointers.PointerDirection.Outbound):\n        raise LookupError(\n            f'{path_id} is an inbound pointer and cannot be resolved '\n            f'on a base relation')\n\n    result: pgast.OutputVar\n    if isinstance(rel, pgast.NullRelation):\n        if ptrref is not None:\n            target = ptrref.out_target\n        else:\n            target = path_id.target\n\n        pg_type = pg_types.pg_type_from_ir_typeref(target)\n\n        if ptr_info is not None:\n            name = env.aliases.get(ptr_info.column_name)\n        else:\n            name = env.aliases.get('v')\n\n        val = pgast.TypeCast(\n            arg=pgast.NullConstant(),\n            type_name=pgast.TypeName(\n                name=pg_type,\n            )\n        )\n\n        rel.target_list.append(pgast.ResTarget(name=name, val=val))\n        result = pgast.ColumnRef(name=[name], nullable=True)\n    else:\n        if ptrref is None or ptr_info is None:\n            raise LookupError(\n                f'could not resolve trailing pointer class for {path_id}')\n\n        if ptrref.is_computable:\n            raise LookupError(\"can't lookup computable ptrref\")\n\n        # Refuse to try to access a link table when we are actually\n        # looking at an object rel. This check is needed because\n        # relgen._lookup_set_rvar_in_source sometimes does some pretty\n        # wild maybe_get_path_value_var calls.\n        if (\n            ptr_info.table_type == 'link'\n            and isinstance(rel.type_or_ptr_ref, irast.TypeRef)\n        ):\n            raise LookupError(\"can't access link table on object rel\")\n\n        if (\n            ptrref.shortname.name == '__type__'\n            and rel.name\n            and not common.is_inhview_name(rel.name)\n        ):\n            assert isinstance(rel.type_or_ptr_ref, irast.TypeRef)\n            result = pgast.ExprOutputVar(\n                expr=astutils.compile_typeref(rel.type_or_ptr_ref))\n        else:\n            result = pgast.ColumnRef(\n                name=[ptr_info.column_name],\n                nullable=not ptrref.required)\n\n    _put_path_output_var(rel, path_id, aspect, result, flavor=flavor)\n    return result\n\n\ndef has_type_rewrite(\n        typeref: irast.TypeRef, *, env: context.Environment) -> bool:\n    return any(\n        (typeref.real_material_type.id, b) in env.type_rewrites\n        for b in (True, False)\n    )\n\n\ndef link_needs_type_rewrite(\n        typeref: irast.TypeRef, *, env: context.Environment) -> bool:\n    return (\n        has_type_rewrite(typeref, env=env)\n        # Typically we need to apply rewrites when looking at a link\n        # target that has a policy on it, but we suppress this for\n        # schema::ObjectType. None of the hidden objects should be\n        # user visible anyway, and this allows us to do type id\n        # injection without a join.\n        and str(typeref.real_material_type.name_hint) != 'schema::ObjectType'\n    )\n\n\ndef find_path_output(\n    rel: pgast.BaseRelation, ref: pgast.BaseExpr\n) -> Optional[pgast.OutputVar]:\n    if isinstance(ref, pgast.TupleVarBase):\n        return None\n\n    for key, other_ref in rel.path_namespace.items():\n        if _same_expr(other_ref, ref) and key in rel.path_outputs:\n            return rel.path_outputs.get(key)\n    else:\n        return None\n\n\ndef get_path_output(\n    rel: pgast.BaseRelation,\n    path_id: irast.PathId,\n    *,\n    aspect: pgce.PathAspect,\n    allow_nullable: bool=True,\n    disable_output_fusion: bool=False,\n    flavor: str='normal',\n    env: context.Environment\n) -> pgast.OutputVar:\n\n    if isinstance(rel, pgast.Query) and flavor == 'normal':\n        path_id = map_path_id(path_id, rel.view_path_id_map)\n\n    # XXX: This is a haaaaack.\n    if rel.strip_output_namespaces:\n        path_id = path_id.strip_namespace(path_id.namespace)\n\n    return _get_path_output(rel, path_id=path_id, aspect=aspect,\n                            disable_output_fusion=disable_output_fusion,\n                            allow_nullable=allow_nullable,\n                            flavor=flavor,\n                            env=env)\n\n\ndef _get_path_output(\n    rel: pgast.BaseRelation,\n    path_id: irast.PathId,\n    *,\n    aspect: pgce.PathAspect,\n    allow_nullable: bool=True,\n    disable_output_fusion: bool=False,\n    flavor: str,\n    env: context.Environment,\n) -> pgast.OutputVar:\n\n    if flavor == 'packed':\n        result = (rel.packed_path_outputs.get((path_id, aspect))\n                  if rel.packed_path_outputs else None)\n    else:\n        result = rel.path_outputs.get((path_id, aspect))\n    if result is not None:\n        return result\n\n    ref: pgast.BaseExpr\n    alias = None\n    rptr = path_id.rptr()\n    if (\n        rptr is not None\n        and irtyputils.is_id_ptrref(rptr)\n        and (src_path_id := path_id.src_path())\n        and not disable_output_fusion\n        and not (\n            (src_rptr := src_path_id.rptr())\n            and src_rptr.real_material_ptr.out_cardinality.is_multi()\n            and not irtyputils.is_free_object(src_path_id.target)\n        )\n        and not link_needs_type_rewrite(src_path_id.target, env=env)\n    ):\n        # A value reference to Object.id is the same as a value\n        # reference to the Object itself. (Though we want to only\n        # apply this in the cases that process_set_as_path does this\n        # optimization, which means not for multi props. We also always\n        # allow it for free objects.)\n        id_output = maybe_get_path_output(\n            rel,\n            src_path_id,\n            aspect=pgce.PathAspect.VALUE,\n            allow_nullable=allow_nullable,\n            env=env\n        )\n        if id_output is not None:\n            _put_path_output_var(rel, path_id, aspect, id_output)\n            return id_output\n\n    if is_terminal_relation(rel):\n        return _get_rel_path_output(\n            rel, path_id, aspect=aspect, flavor=flavor, env=env)\n\n    assert isinstance(rel, pgast.Query)\n    if is_values_relation(rel) and aspect != pgce.PathAspect.IDENTITY:\n        # The VALUES() construct seems to always expose its\n        # value as \"column1\".\n        alias = 'column1'\n        ref = pgast.ColumnRef(name=[alias], nullable=rel.nullable)\n    else:\n        ref = get_path_var(rel, path_id, aspect=aspect, flavor=flavor, env=env)\n\n    # As an optimization, look to see if the same expression is being\n    # output on a different aspect. This can save us needing to do the\n    # work twice in the query.\n    other_output = find_path_output(rel, ref)\n    if other_output is not None and not disable_output_fusion:\n        _put_path_output_var(rel, path_id, aspect, other_output, flavor=flavor)\n        return other_output\n\n    if isinstance(ref, pgast.TupleVarBase):\n        elements = []\n        for el in ref.elements:\n            element = _get_path_output(\n                rel, el.path_id, aspect=aspect,\n                disable_output_fusion=disable_output_fusion,\n                flavor=flavor,\n                allow_nullable=allow_nullable, env=env)\n\n            # We need to reverse the mapping for the element path in\n            # the output TupleVar, since it will be used *outside*\n            # this rel, and so without the map applied.\n            el_path_id = reverse_map_path_id(el.path_id, rel.view_path_id_map)\n            elements.append(pgast.TupleElement(\n                path_id=el_path_id, val=element, name=element))\n\n        result = pgast.TupleVar(\n            elements=elements,\n            named=ref.named,\n            typeref=ref.typeref,\n            is_packed_multi=ref.is_packed_multi,\n        )\n\n    else:\n        if astutils.is_set_op_query(rel):\n            assert isinstance(ref, pgast.OutputVar)\n            result = astutils.strip_output_var(ref)\n        else:\n            assert isinstance(rel, pgast.ReturningQuery), \\\n                \"expected ReturningQuery\"\n\n            if alias is None:\n                alias = get_path_output_alias(path_id, aspect, env=env)\n\n            if isinstance(ref, pgast.NullConstant):\n                pg_type = pg_types.pg_type_from_ir_typeref(path_id.target)\n\n                ref = pgast.TypeCast(\n                    arg=ref, type_name=pgast.TypeName(name=pg_type)\n                )\n\n            restarget = pgast.ResTarget(\n                name=alias, val=ref, ser_safe=getattr(ref, 'ser_safe', False))\n            rel.target_list.append(restarget)\n\n            nullable = is_nullable(ref, env=env)\n\n            optional = None\n            is_packed_multi = False\n            if isinstance(ref, pgast.ColumnRef):\n                optional = ref.optional\n                is_packed_multi = ref.is_packed_multi\n\n            # group by will register a *subquery* as a path var\n            # for a packed group, and if we want to avoid losing\n            # track of whether is is multi, we need to figure that out.\n            if (\n                isinstance(ref, pgast.SelectStmt)\n                and flavor == 'packed'\n                and ref.packed_path_outputs\n                and (path_id, aspect) in ref.packed_path_outputs\n            ):\n                is_packed_multi = ref.packed_path_outputs[\n                    path_id, aspect].is_packed_multi\n\n            if nullable and not allow_nullable:\n                assert isinstance(rel, pgast.SelectStmt), \\\n                    \"expected SelectStmt\"\n                var = get_path_var(rel, path_id, aspect=aspect, env=env)\n                rel.where_clause = astutils.extend_binop(\n                    rel.where_clause,\n                    pgast.NullTest(arg=var, negated=True)\n                )\n                nullable = False\n\n            result = pgast.ColumnRef(\n                name=[alias], nullable=nullable, optional=optional,\n                is_packed_multi=is_packed_multi)\n\n    _put_path_output_var(rel, path_id, aspect, result, flavor=flavor)\n    if (path_id.is_objtype_path()\n            and not isinstance(result, pgast.TupleVarBase)):\n        equiv_aspect = None\n        if aspect == pgce.PathAspect.IDENTITY:\n            equiv_aspect = pgce.PathAspect.VALUE\n        elif aspect == pgce.PathAspect.VALUE:\n            equiv_aspect = pgce.PathAspect.IDENTITY\n\n        if (equiv_aspect is not None\n                and (path_id, equiv_aspect) not in rel.path_outputs):\n            _put_path_output_var(\n                rel, path_id, equiv_aspect, result, flavor=flavor\n            )\n\n    return result\n\n\ndef maybe_get_path_output(\n    rel: pgast.BaseRelation,\n    path_id: irast.PathId,\n    *,\n    aspect: pgce.PathAspect,\n    allow_nullable: bool=True,\n    disable_output_fusion: bool=False,\n    flavor: str='normal',\n    env: context.Environment,\n) -> Optional[pgast.OutputVar]:\n    try:\n        return get_path_output(rel, path_id=path_id, aspect=aspect,\n                               allow_nullable=allow_nullable,\n                               disable_output_fusion=disable_output_fusion,\n                               flavor=flavor, env=env)\n    except LookupError:\n        return None\n\n\ndef get_path_identity_output(\n    rel: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    env: context.Environment,\n) -> pgast.OutputVar:\n    return get_path_output(\n        rel, path_id, aspect=pgce.PathAspect.IDENTITY, env=env\n    )\n\n\ndef get_path_value_output(\n    rel: pgast.BaseRelation,\n    path_id: irast.PathId,\n    *,\n    env: context.Environment,\n) -> pgast.OutputVar:\n    return get_path_output(\n        rel, path_id, aspect=pgce.PathAspect.VALUE, env=env\n    )\n\n\ndef get_path_serialized_or_value_var(\n        rel: pgast.Query, path_id: irast.PathId, *,\n        env: context.Environment) -> pgast.BaseExpr:\n\n    ref = maybe_get_path_serialized_var(rel, path_id, env=env)\n    if ref is None:\n        ref = get_path_value_var(rel, path_id, env=env)\n    return ref\n\n\ndef fix_tuple(\n    rel: pgast.Query,\n    ref: pgast.BaseExpr,\n    *,\n    aspect: pgce.PathAspect,\n    env: context.Environment,\n) -> pgast.BaseExpr:\n\n    if (\n        isinstance(ref, pgast.TupleVarBase)\n        and not isinstance(ref, pgast.TupleVar)\n    ):\n        elements = []\n\n        for el in ref.elements:\n            assert el.path_id is not None\n            var = get_path_var(rel, el.path_id, aspect=aspect, env=env)\n            val = fix_tuple(rel, var, aspect=aspect, env=env)\n            elements.append(\n                pgast.TupleElement(\n                    path_id=el.path_id, name=el.name, val=val))\n\n        ref = pgast.TupleVar(\n            elements,\n            named=ref.named,\n            typeref=ref.typeref,\n        )\n\n    return ref\n\n\ndef get_path_serialized_output(\n    rel: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    env: context.Environment,\n) -> pgast.OutputVar:\n    # Serialized output is a special case, we don't\n    # want this behaviour to be recursive, so it\n    # must be kept outside of get_path_output() generic.\n    aspect = pgce.PathAspect.SERIALIZED\n\n    path_id = map_path_id(path_id, rel.view_path_id_map)\n    result = rel.path_outputs.get((path_id, aspect))\n    if result is not None:\n        return result\n\n    ref = get_path_serialized_or_value_var(rel, path_id, env=env)\n\n    if (\n        isinstance(ref, pgast.TupleVarBase)\n        and not isinstance(ref, pgast.TupleVar)\n    ):\n        elements = []\n\n        for el in ref.elements:\n            assert el.path_id is not None\n            val = get_path_serialized_or_value_var(rel, el.path_id, env=env)\n            elements.append(\n                pgast.TupleElement(\n                    path_id=el.path_id, name=el.name, val=val))\n\n        ref = pgast.TupleVar(\n            elements,\n            named=ref.named,\n            typeref=ref.typeref,\n        )\n\n    refexpr = output.serialize_expr(ref, path_id=path_id, env=env)\n    alias = get_path_output_alias(path_id, aspect, env=env)\n\n    restarget = pgast.ResTarget(name=alias, val=refexpr, ser_safe=True)\n    rel.target_list.append(restarget)\n\n    result = pgast.ColumnRef(\n        name=[alias], nullable=refexpr.nullable, ser_safe=True)\n\n    _put_path_output_var(rel, path_id, aspect, result)\n\n    return result\n\n\ndef get_path_output_or_null(\n    rel: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    disable_output_fusion: bool=False,\n    flavor: str='normal',\n    aspect: pgce.PathAspect,\n    env: context.Environment,\n) -> tuple[pgast.OutputVar, bool]:\n\n    path_id = map_path_id(path_id, rel.view_path_id_map)\n\n    ref = maybe_get_path_output(\n        rel, path_id,\n        disable_output_fusion=disable_output_fusion,\n        flavor=flavor,\n        aspect=aspect, env=env)\n    if ref is not None:\n        return ref, False\n\n    alt_aspect = get_less_specific_aspect(path_id, aspect)\n    if alt_aspect is not None and flavor == 'normal':\n        # If disable_output_fusion is true, we need to be careful\n        # to not reuse an existing column\n        if disable_output_fusion:\n            preexisting = rel.path_outputs.pop((path_id, alt_aspect), None)\n        ref = maybe_get_path_output(\n            rel, path_id,\n            disable_output_fusion=disable_output_fusion,\n            aspect=alt_aspect, env=env)\n        if disable_output_fusion:\n            # Put back the path_output to whatever it was before\n            if not preexisting:\n                rel.path_outputs.pop((path_id, alt_aspect), None)\n            else:\n                rel.path_outputs[(path_id, alt_aspect)] = preexisting\n\n        if ref is not None:\n            _put_path_output_var(rel, path_id, aspect, ref)\n            return ref, False\n\n    alias = env.aliases.get('null')\n    restarget = pgast.ResTarget(\n        name=alias,\n        val=pgast.NullConstant())\n\n    rel.target_list.append(restarget)\n\n    ref = pgast.ColumnRef(name=[alias], nullable=True)\n    _put_path_output_var(rel, path_id, aspect, ref, flavor=flavor)\n\n    return ref, True\n\n\ndef is_nullable(\n        expr: pgast.BaseExpr, *,\n        env: context.Environment) -> Optional[bool]:\n    try:\n        return expr.nullable\n    except AttributeError:\n        if isinstance(expr, pgast.ReturningQuery):\n            tl_len = len(expr.target_list)\n            if tl_len != 1:\n                raise RuntimeError(\n                    f'subquery used as a value returns {tl_len} columns')\n\n            return is_nullable(expr.target_list[0].val, env=env)\n        else:\n            raise\n"
  },
  {
    "path": "edb/pgsql/compiler/relctx.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Compiler routines managing relation ranges and scope.\"\"\"\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Callable,\n    Optional,\n    AbstractSet,\n    Iterable,\n    Sequence,\n    NamedTuple,\n    cast,\n)\n\nimport uuid\n\nimport immutables as immu\n\nfrom edb import errors\n\n\nfrom edb.edgeql import qltypes\nfrom edb.edgeql import ast as qlast\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtyputils\nfrom edb.ir import utils as irutils\n\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import name as sn\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import common\nfrom edb.pgsql import types as pg_types\n\nfrom . import astutils\nfrom . import context\nfrom . import dispatch\nfrom . import enums as pgce\nfrom . import output\nfrom . import pathctx\n\n\ndef init_toplevel_query(\n        ir_set: irast.Set, *,\n        ctx: context.CompilerContextLevel) -> None:\n\n    ctx.toplevel_stmt = ctx.stmt = ctx.rel\n    update_scope(ir_set, ctx.rel, ctx=ctx)\n    ctx.pending_query = ctx.rel\n\n\ndef _pull_path_namespace(\n        *, target: pgast.Query, source: pgast.PathRangeVar,\n        flavor: str='normal',\n        replace_bonds: bool=True, ctx: context.CompilerContextLevel) -> None:\n\n    squery = source.query\n    source_qs: list[pgast.BaseRelation]\n\n    if astutils.is_set_op_query(squery):\n        # Set op query\n        assert squery.larg and squery.rarg\n        source_qs = [squery, squery.larg, squery.rarg]\n    else:\n        source_qs = [squery]\n\n    for source_q in source_qs:\n        s_paths: set[tuple[irast.PathId, pgce.PathAspect]] = set()\n        if flavor == 'normal':\n            if hasattr(source_q, 'path_outputs'):\n                s_paths.update(source_q.path_outputs)\n            if hasattr(source_q, 'path_namespace'):\n                s_paths.update(source_q.path_namespace)\n            if isinstance(source_q, pgast.Query):\n                s_paths.update(source_q.path_rvar_map)\n        elif flavor == 'packed':\n            if hasattr(source_q, 'packed_path_outputs'):\n                if source_q.packed_path_outputs:\n                    s_paths.update(source_q.packed_path_outputs)\n            if isinstance(source_q, pgast.Query):\n                if source_q.path_packed_rvar_map:\n                    s_paths.update(source_q.path_packed_rvar_map)\n        else:\n            raise AssertionError(f'unexpected flavor \"{flavor}\"')\n\n        view_path_id_map = getattr(source_q, 'view_path_id_map', {})\n\n        for path_id, aspect in s_paths:\n            orig_path_id = path_id\n            if flavor != 'packed':\n                path_id = pathctx.reverse_map_path_id(\n                    path_id, view_path_id_map)\n\n            # Skip pulling paths that match the path_id_mask before or after\n            # doing path id mapping. We need to look at before as well\n            # to prevent paths leaking out under a different name.\n            if flavor != 'packed' and (\n                path_id in squery.path_id_mask\n                or orig_path_id in squery.path_id_mask\n            ):\n                continue\n\n            rvar = pathctx.maybe_get_path_rvar(\n                target, path_id, aspect=aspect, flavor=flavor\n            )\n            if rvar is None or flavor == 'packed':\n                pathctx.put_path_rvar(\n                    target, path_id, source, aspect=aspect, flavor=flavor\n                )\n\n\ndef pull_path_namespace(\n        *, target: pgast.Query, source: pgast.PathRangeVar,\n        replace_bonds: bool=True, ctx: context.CompilerContextLevel) -> None:\n    for flavor in ('normal', 'packed'):\n        _pull_path_namespace(target=target, source=source, flavor=flavor,\n                             replace_bonds=replace_bonds, ctx=ctx)\n\n\ndef find_rvar(\n        stmt: pgast.Query, *,\n        flavor: str='normal',\n        source_stmt: Optional[pgast.Query]=None,\n        path_id: irast.PathId,\n        ctx: context.CompilerContextLevel) -> \\\n        Optional[pgast.PathRangeVar]:\n    \"\"\"Find an existing range var for a given *path_id* in stmt hierarchy.\n\n    If a range var is visible in a given SQL scope denoted by *stmt*, or,\n    optionally, *source_stmt*, record it on *stmt* for future reference.\n\n    :param stmt:\n        The statement to ensure range var visibility in.\n\n    :param flavor:\n        Whether to look for normal rvars or packed rvars\n\n    :param source_stmt:\n        An optional statement object which is used as the starting SQL scope\n        for range var search.  If not specified, *stmt* is used as the\n        starting scope.\n\n    :param path_id:\n        The path ID of the range var being searched.\n\n    :param ctx:\n        Compiler context.\n\n    :return:\n        A range var instance if found, ``None`` otherwise.\n    \"\"\"\n\n    if source_stmt is None:\n        source_stmt = stmt\n\n    rvar = maybe_get_path_rvar(\n        source_stmt,\n        path_id=path_id,\n        aspect=pgce.PathAspect.VALUE,\n        flavor=flavor,\n        ctx=ctx,\n    )\n    if rvar is not None:\n        pathctx.put_path_rvar_if_not_exists(\n            stmt,\n            path_id,\n            rvar,\n            aspect=pgce.PathAspect.VALUE,\n            flavor=flavor,\n        )\n\n        src_rvar = maybe_get_path_rvar(\n            source_stmt,\n            path_id=path_id,\n            aspect=pgce.PathAspect.SOURCE,\n            flavor=flavor,\n            ctx=ctx\n        )\n\n        if src_rvar is not None:\n            pathctx.put_path_rvar_if_not_exists(\n                stmt,\n                path_id,\n                src_rvar,\n                aspect=pgce.PathAspect.SOURCE,\n                flavor=flavor,\n            )\n\n    return rvar\n\n\ndef include_rvar(\n    stmt: pgast.SelectStmt,\n    rvar: pgast.PathRangeVar,\n    path_id: irast.PathId, *,\n    overwrite_path_rvar: bool=False,\n    pull_namespace: bool=True,\n    update_mask: bool=True,\n    flavor: str='normal',\n    aspects: Optional[\n        tuple[pgce.PathAspect, ...]\n        | AbstractSet[pgce.PathAspect]\n    ]=None,\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n    \"\"\"Ensure that *rvar* is visible in *stmt* as a value/source aspect.\n\n    :param stmt:\n        The statement to include *rel* in.\n\n    :param rvar:\n        The range var node to join.\n\n    :param join_type:\n        JOIN type to use when including *rel*.\n\n    :param flavor:\n        Whether this is a normal or packed rvar\n\n    :param aspect:\n        The reference aspect of the range var.\n\n    :param ctx:\n        Compiler context.\n    \"\"\"\n    if aspects is None:\n        aspects = (pgce.PathAspect.VALUE,)\n\n        if path_id.is_objtype_path():\n            if isinstance(rvar, pgast.RangeSubselect):\n                if pathctx.has_path_aspect(\n                    rvar.query,\n                    path_id,\n                    aspect=pgce.PathAspect.SOURCE,\n                ):\n                    aspects += (pgce.PathAspect.SOURCE,)\n            else:\n                aspects += (pgce.PathAspect.SOURCE,)\n\n        elif path_id.is_tuple_path():\n            aspects += (pgce.PathAspect.SOURCE,)\n\n    return include_specific_rvar(\n        stmt, rvar=rvar, path_id=path_id,\n        overwrite_path_rvar=overwrite_path_rvar,\n        pull_namespace=pull_namespace,\n        update_mask=update_mask,\n        flavor=flavor,\n        aspects=aspects,\n        ctx=ctx)\n\n\ndef include_specific_rvar(\n    stmt: pgast.SelectStmt,\n    rvar: pgast.PathRangeVar,\n    path_id: irast.PathId, *,\n    overwrite_path_rvar: bool=False,\n    pull_namespace: bool=True,\n    update_mask: bool=True,\n    flavor: str='normal',\n    aspects: Iterable[pgce.PathAspect]=(pgce.PathAspect.VALUE,),\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n    \"\"\"Make the *aspect* of *path_id* visible in *stmt* as *rvar*.\n\n    :param stmt:\n        The statement to include *rel* in.\n\n    :param rvar:\n        The range var node to join.\n\n    :param join_type:\n        JOIN type to use when including *rel*.\n\n    :param flavor:\n        Whether this is a normal or packed rvar\n\n    :param aspect:\n        The reference aspect of the range var.\n\n    :param ctx:\n        Compiler context.\n    \"\"\"\n\n    if not has_rvar(stmt, rvar, ctx=ctx):\n        rel_join(stmt, rvar, ctx=ctx)\n        # Make sure that the path namespace of *rvar* is mapped\n        # onto the path namespace of *stmt*.\n        if pull_namespace:\n            pull_path_namespace(target=stmt, source=rvar, ctx=ctx)\n\n    for aspect in aspects:\n        if overwrite_path_rvar:\n            pathctx.put_path_rvar(\n                stmt, path_id, rvar, flavor=flavor, aspect=aspect\n            )\n        else:\n            pathctx.put_path_rvar_if_not_exists(\n                stmt, path_id, rvar, flavor=flavor, aspect=aspect\n            )\n\n    if update_mask:\n        scopes = [ctx.scope_tree]\n        parent_scope = ctx.scope_tree.parent\n        if parent_scope is not None:\n            scopes.append(parent_scope)\n\n        tpath_id = path_id.tgt_path()\n        if not any(scope.path_id == tpath_id or\n                   scope.find_child(tpath_id) for scope in scopes):\n            pathctx.put_path_id_mask(stmt, path_id)\n\n    return rvar\n\n\ndef has_rvar(\n        stmt: pgast.Query, rvar: pgast.PathRangeVar, *,\n        ctx: context.CompilerContextLevel) -> bool:\n\n    curstmt: Optional[pgast.Query] = stmt\n\n    if ctx.env.external_rvars and has_external_rvar(rvar, ctx=ctx):\n        return True\n\n    while curstmt is not None:\n        if pathctx.has_rvar(curstmt, rvar):\n            return True\n        curstmt = ctx.rel_hierarchy.get(curstmt)\n\n    return False\n\n\ndef has_external_rvar(\n    rvar: pgast.PathRangeVar,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> bool:\n    return rvar in ctx.env.external_rvars.values()\n\n\ndef _maybe_get_path_rvar(\n    stmt: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    flavor: str='normal',\n    aspect: pgce.PathAspect,\n    ctx: context.CompilerContextLevel,\n) -> Optional[tuple[pgast.PathRangeVar, irast.PathId]]:\n    rvar = ctx.env.external_rvars.get((path_id, aspect))\n    if rvar:\n        return rvar, path_id\n\n    qry: Optional[pgast.Query] = stmt\n    while qry is not None:\n        rvar = pathctx.maybe_get_path_rvar(\n            qry, path_id, aspect=aspect, flavor=flavor\n        )\n        if rvar is not None:\n            if qry is not stmt:\n                # Cache the rvar reference.\n                pathctx.put_path_rvar(\n                    stmt, path_id, rvar, flavor=flavor, aspect=aspect\n                )\n            return rvar, path_id\n        qry = ctx.rel_hierarchy.get(qry)\n\n    return None\n\n\ndef _get_path_rvar(\n    stmt: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    flavor: str='normal',\n    aspect: pgce.PathAspect,\n    ctx: context.CompilerContextLevel,\n) -> tuple[pgast.PathRangeVar, irast.PathId]:\n    result = _maybe_get_path_rvar(\n        stmt, path_id, flavor=flavor, aspect=aspect, ctx=ctx)\n    if result is None:\n        raise LookupError(f'there is no range var for {path_id} in {stmt}')\n    else:\n        return result\n\n\ndef get_path_rvar(\n    stmt: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    flavor: str='normal',\n    aspect: pgce.PathAspect,\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n    return _get_path_rvar(\n        stmt, path_id, flavor=flavor, aspect=aspect, ctx=ctx)[0]\n\n\ndef get_path_var(\n    stmt: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    aspect: pgce.PathAspect,\n    ctx: context.CompilerContextLevel,\n) -> pgast.BaseExpr:\n    var = pathctx.maybe_get_path_var(\n        stmt, path_id=path_id, aspect=aspect, env=ctx.env)\n    if var is not None:\n        return var\n    else:\n        rvar, path_id = _get_path_rvar(stmt, path_id, aspect=aspect, ctx=ctx)\n        return pathctx.get_rvar_path_var(\n            rvar, path_id, aspect=aspect, env=ctx.env)\n\n\ndef maybe_get_path_rvar(\n    stmt: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    flavor: str='normal',\n    aspect: pgce.PathAspect,\n    ctx: context.CompilerContextLevel,\n) -> Optional[pgast.PathRangeVar]:\n    result = _maybe_get_path_rvar(stmt, path_id,\n                                  aspect=aspect, flavor=flavor, ctx=ctx)\n    return result[0] if result is not None else None\n\n\ndef maybe_get_path_var(\n    stmt: pgast.Query,\n    path_id: irast.PathId,\n    *,\n    aspect: pgce.PathAspect,\n    ctx: context.CompilerContextLevel,\n) -> Optional[pgast.BaseExpr]:\n    result = _maybe_get_path_rvar(stmt, path_id, aspect=aspect, ctx=ctx)\n    if result is None:\n        return None\n    else:\n        try:\n            return pathctx.get_rvar_path_var(\n                result[0], result[1], aspect=aspect, env=ctx.env)\n        except LookupError:\n            return None\n\n\ndef new_empty_rvar(\n        ir_set: irast.SetE[irast.EmptySet], *,\n        ctx: context.CompilerContextLevel) -> pgast.PathRangeVar:\n    nullrel = pgast.NullRelation(\n        path_id=ir_set.path_id, type_or_ptr_ref=ir_set.typeref)\n    rvar = rvar_for_rel(nullrel, ctx=ctx)\n    return rvar\n\n\ndef new_free_object_rvar(\n    typeref: irast.TypeRef,\n    path_id: irast.PathId,\n    *,\n    lateral: bool=False,\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n    \"\"\"Create a fake source rel for a free object\n\n    Free objects don't *really* have ids, but the compiler needs all\n    objects to have ids, so we just inject the type id as if it was an\n    id. It shouldn't get used for anything but NULL testing, so no\n    problem.\n\n    The only thing other than ids that need to come from a free object\n    is __type__, which we inject in a special case way in\n    pathctx.get_path_var.\n\n    We also have a special case in relgen.ensure_source_rvar to reuse an\n    existing value rvar instead of creating a new root rvar.\n\n    (We inject __type__ in get_path_var instead of injecting it here because\n    we don't have the pathid for it available to us here and because it\n    allows ensure_source_rvar to simply reuse a value rvar.)\n\n    \"\"\"\n    with ctx.subrel() as subctx:\n        qry = subctx.rel\n\n        id_expr = astutils.compile_typeref(typeref.real_material_type)\n        pathctx.put_path_identity_var(qry, path_id, id_expr)\n        pathctx.put_path_value_var(qry, path_id, id_expr)\n\n    return rvar_for_rel(qry, typeref=typeref, lateral=lateral, ctx=ctx)\n\n\ndef deep_copy_primitive_rvar_path_var(\n    orig_id: irast.PathId, new_id: irast.PathId,\n    rvar: pgast.PathRangeVar, *,\n    env: context.Environment\n) -> None:\n    \"\"\"Copy one identity path to another in a primitive rvar.\n\n    The trickiness here is because primitive rvars might have an\n    overlay stack, which means if they are joined on, it might be\n    using _lateral_union_join, which requires every component of\n    the union to have all the path bonds.\n    \"\"\"\n\n    if isinstance(rvar, pgast.RangeSubselect):\n        for component in astutils.each_query_in_set(rvar.query):\n            rref = pathctx.get_path_var(\n                component, orig_id, aspect=pgce.PathAspect.IDENTITY, env=env\n            )\n            pathctx.put_path_var(\n                component,\n                new_id,\n                rref,\n                aspect=pgce.PathAspect.IDENTITY,\n            )\n    else:\n        rref = pathctx.get_path_output(\n            rvar.query, orig_id, aspect=pgce.PathAspect.IDENTITY, env=env\n        )\n        pathctx.put_rvar_path_output(\n            rvar,\n            new_id,\n            aspect=pgce.PathAspect.IDENTITY,\n            var=rref,\n        )\n\n\ndef new_primitive_rvar(\n    ir_set: irast.Set,\n    *,\n    path_id: irast.PathId,\n    lateral: bool,\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n    # XXX: is this needed?\n    expr = irutils.sub_expr(ir_set)\n    if isinstance(expr, irast.TypeRoot):\n        skip_subtypes = expr.skip_subtypes\n        is_global = expr.is_cached_global\n    else:\n        skip_subtypes = False\n        is_global = False\n\n    typeref = ir_set.typeref\n    dml_source = irutils.get_dml_sources(ir_set, ctx.env.binding_dml)\n    set_rvar = range_for_typeref(\n        typeref, path_id, lateral=lateral, dml_source=dml_source,\n        include_descendants=not skip_subtypes,\n        ignore_rewrites=ir_set.ignore_rewrites,\n        is_global=is_global,\n        ctx=ctx,\n    )\n    pathctx.put_rvar_path_bond(set_rvar, path_id)\n\n    # FIXME: This feels like it should all not be here.\n    if isinstance(ir_set.expr, irast.Pointer):\n        rptr = ir_set.expr\n        if (\n            isinstance(rptr.ptrref, irast.TypeIntersectionPointerRef)\n            and isinstance(rptr.source.expr, irast.Pointer)\n        ):\n            rptr = rptr.source.expr\n\n        # If the set comes from an backlink, and the link is stored inline,\n        # we want to output the source path.\n        if (\n            rptr.is_inbound\n            and (\n                rptrref := irtyputils.maybe_find_actual_ptrref(\n                    set_rvar.typeref, rptr.ptrref) or rptr.ptrref\n                if set_rvar.typeref else rptr.ptrref\n            ) and (\n                ptr_info := pg_types.get_ptrref_storage_info(\n                    rptrref, resolve_type=False, link_bias=False,\n                    allow_missing=True)\n            ) and ptr_info.table_type == 'ObjectType'\n        ):\n            # Inline link\n            prefix_path_id = path_id.src_path()\n            assert prefix_path_id is not None, 'expected a path'\n\n            flipped_id = path_id.extend(ptrref=rptrref)\n\n            # Unfortunately we can't necessarily just install the\n            # prefix path id path---the rvar from range_from_typeref\n            # might be a DML overlay, which means joins on it will try\n            # to use _lateral_union_join; this means that all of the\n            # path bonds need to be valid on each *subquery*, so we\n            # need to set them up in each subquery.\n            deep_copy_primitive_rvar_path_var(\n                flipped_id, prefix_path_id, set_rvar, env=ctx.env)\n            pathctx.put_rvar_path_bond(set_rvar, prefix_path_id)\n\n    return set_rvar\n\n\ndef new_root_rvar(\n    ir_set: irast.Set,\n    *,\n    lateral: bool = False,\n    path_id: Optional[irast.PathId] = None,\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n\n    if path_id is None:\n        path_id = ir_set.path_id\n\n    if irtyputils.is_free_object(path_id.target):\n        return new_free_object_rvar(\n            path_id.target, path_id, lateral=lateral, ctx=ctx)\n\n    narrowing = ctx.intersection_narrowing.get(ir_set)\n    if narrowing is not None:\n        ir_set = narrowing\n    return new_primitive_rvar(\n        ir_set, lateral=lateral, path_id=path_id, ctx=ctx)\n\n\ndef new_pointer_rvar(\n    ir_set: irast.SetE[irast.Pointer],\n    *,\n    link_bias: bool=False,\n    src_rvar: pgast.PathRangeVar,\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n    ir_ptr = ir_set.expr\n    ptrref = ir_ptr.ptrref\n\n    link_bias = link_bias or ir_ptr.force_link_table\n\n    ptr_info = pg_types.get_ptrref_storage_info(\n        ptrref, resolve_type=False, link_bias=link_bias, allow_missing=True,\n        versioned=ctx.env.versioned_stdlib,\n    )\n\n    if ptr_info and ptr_info.table_type == 'ObjectType':\n        # Inline link\n        return _new_inline_pointer_rvar(\n            ir_set, src_rvar=src_rvar, ctx=ctx\n        )\n    else:\n        return _new_mapped_pointer_rvar(ir_set, ctx=ctx)\n\n\ndef _new_inline_pointer_rvar(\n        ir_set: irast.SetE[irast.Pointer], *,\n        lateral: bool=True,\n        src_rvar: pgast.PathRangeVar,\n        ctx: context.CompilerContextLevel) -> pgast.PathRangeVar:\n    ir_ptr = ir_set.expr\n\n    ptr_rel = pgast.SelectStmt()\n    ptr_rvar = rvar_for_rel(ptr_rel, lateral=lateral, ctx=ctx)\n    ptr_rvar.query.path_id = ir_set.path_id.ptr_path()\n\n    is_inbound = ir_ptr.direction == s_pointers.PointerDirection.Inbound\n\n    if is_inbound:\n        far_pid = ir_ptr.source.path_id\n    else:\n        far_pid = ir_set.path_id\n\n    far_ref = pathctx.get_rvar_path_identity_var(\n        src_rvar, far_pid, env=ctx.env)\n\n    pathctx.put_rvar_path_bond(ptr_rvar, far_pid)\n    pathctx.put_path_identity_var(ptr_rel, far_pid, var=far_ref)\n\n    return ptr_rvar\n\n\ndef _new_mapped_pointer_rvar(\n        ir_set: irast.SetE[irast.Pointer], *,\n        ctx: context.CompilerContextLevel) -> pgast.PathRangeVar:\n    ir_ptr = ir_set.expr\n\n    ptrref = ir_ptr.ptrref\n    dml_source = irutils.get_dml_sources(ir_ptr.source, ctx.env.binding_dml)\n    ptr_rvar = range_for_pointer(ir_set, dml_source=dml_source, ctx=ctx)\n\n    src_col = 'source'\n    source_ref = pgast.ColumnRef(name=[src_col], nullable=False)\n\n    tgt_col = 'target'\n    target_ref = pgast.ColumnRef(\n        name=[tgt_col],\n        nullable=not ptrref.required)\n\n    if (\n        ir_ptr.direction == s_pointers.PointerDirection.Inbound\n        or ptrref.computed_link_alias_is_backward\n    ):\n        near_ref = target_ref\n        far_ref = source_ref\n    else:\n        near_ref = source_ref\n        far_ref = target_ref\n\n    src_pid = ir_ptr.source.path_id\n    tgt_pid = ir_set.path_id\n    ptr_pid = tgt_pid.ptr_path()\n\n    ptr_rvar.query.path_id = ptr_pid\n    pathctx.put_rvar_path_bond(ptr_rvar, src_pid)\n    pathctx.put_rvar_path_output(\n        ptr_rvar, src_pid, aspect=pgce.PathAspect.IDENTITY, var=near_ref\n    )\n    pathctx.put_rvar_path_output(\n        ptr_rvar, src_pid, aspect=pgce.PathAspect.VALUE, var=near_ref\n    )\n    pathctx.put_rvar_path_output(\n        ptr_rvar, tgt_pid, aspect=pgce.PathAspect.VALUE, var=far_ref\n    )\n\n    if tgt_pid.is_objtype_path():\n        pathctx.put_rvar_path_bond(ptr_rvar, tgt_pid)\n        pathctx.put_rvar_path_output(\n            ptr_rvar, tgt_pid, aspect=pgce.PathAspect.IDENTITY, var=far_ref\n        )\n\n    return ptr_rvar\n\n\ndef is_pointer_rvar(\n    rvar: pgast.PathRangeVar,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> bool:\n    return rvar.query.path_id is not None and rvar.query.path_id.is_ptr_path()\n\n\ndef new_rel_rvar(\n        ir_set: irast.Set, stmt: pgast.Query, *,\n        lateral: bool=True,\n        ctx: context.CompilerContextLevel) -> pgast.PathRangeVar:\n    return rvar_for_rel(stmt, typeref=ir_set.typeref, lateral=lateral, ctx=ctx)\n\n\ndef semi_join(\n        stmt: pgast.SelectStmt,\n        ir_set: irast.SetE[irast.Pointer], src_rvar: pgast.PathRangeVar, *,\n        ctx: context.CompilerContextLevel) -> pgast.PathRangeVar:\n    \"\"\"Join an IR Set using semi-join.\"\"\"\n    rptr = ir_set.expr\n\n    # Target set range.\n    set_rvar = new_root_rvar(ir_set, lateral=True, ctx=ctx)\n\n    ptrref = rptr.ptrref\n    ptr_info = pg_types.get_ptrref_storage_info(\n        ptrref, resolve_type=False, allow_missing=True)\n\n    if ptr_info and ptr_info.table_type == 'ObjectType':\n        if rptr.is_inbound:\n            far_pid = ir_set.path_id.src_path()\n            assert far_pid is not None\n        else:\n            far_pid = ir_set.path_id\n    else:\n        far_pid = ir_set.path_id\n        # Link range.\n        map_rvar = new_pointer_rvar(ir_set, src_rvar=src_rvar, ctx=ctx)\n        include_rvar(\n            ctx.rel, map_rvar,\n            path_id=ir_set.path_id.ptr_path(), ctx=ctx)\n\n    tgt_ref = pathctx.get_rvar_path_identity_var(\n        set_rvar, far_pid, env=ctx.env)\n\n    pathctx.get_path_identity_output(\n        ctx.rel, far_pid, env=ctx.env)\n\n    cond = astutils.new_binop(tgt_ref, ctx.rel, 'IN')\n    stmt.where_clause = astutils.extend_binop(\n        stmt.where_clause, cond)\n\n    return set_rvar\n\n\ndef apply_volatility_ref(\n        stmt: pgast.SelectStmt, *,\n        ctx: context.CompilerContextLevel) -> None:\n    for ref in ctx.volatility_ref:\n        # Apply the volatility reference.\n        # See the comment in process_set_as_subquery().\n        arg = ref(stmt, ctx)\n        if not arg:\n            continue\n        stmt.where_clause = astutils.extend_binop(\n            stmt.where_clause,\n            pgast.NullTest(\n                arg=arg,\n                negated=True,\n            )\n        )\n\n\ndef create_iterator_identity_for_path(\n    path_id: irast.PathId,\n    stmt: pgast.BaseRelation,\n    *,\n    ctx: context.CompilerContextLevel,\n    apply_volatility: bool=True,\n) -> None:\n\n    id_expr = pgast.FuncCall(\n        name=astutils.edgedb_func('uuid_generate_v4', ctx=ctx),\n        args=[],\n    )\n\n    if isinstance(stmt, pgast.SelectStmt):\n        path_id = pathctx.map_path_id(path_id, stmt.view_path_id_map)\n        if apply_volatility:\n            apply_volatility_ref(stmt, ctx=ctx)\n\n    pathctx.put_path_var(\n        stmt,\n        path_id,\n        id_expr,\n        force=True,\n        aspect=pgce.PathAspect.ITERATOR,\n    )\n    pathctx.put_path_bond(stmt, path_id, iterator=True)\n\n\ndef get_scope(\n    ir_set: irast.Set, *,\n    ctx: context.CompilerContextLevel,\n) -> Optional[irast.ScopeTreeNode]:\n\n    result: Optional[irast.ScopeTreeNode] = None\n\n    if ir_set.path_scope_id is not None:\n        result = ctx.env.scope_tree_nodes.get(ir_set.path_scope_id)\n\n    return result\n\n\ndef update_scope(\n        ir_set: irast.Set, stmt: pgast.SelectStmt, *,\n        ctx: context.CompilerContextLevel) -> None:\n    \"\"\"Update the scope of an ir set to be a pg stmt.\n\n    If ir_set has a scope node associated with it, update path_scope\n    so that any paths bound in that scope will be compiled in the\n    context of stmt.\n\n    This, combined with maybe_get_scope_stmt, is the mechanism by\n    which the scope tree influences the shape of the output query.\n    \"\"\"\n\n    scope_tree = get_scope(ir_set, ctx=ctx)\n    if scope_tree is None:\n        return\n\n    ctx.scope_tree = scope_tree\n    ctx.path_scope = ctx.path_scope.new_child()\n\n    # Register paths in the current scope to be compiled as a subrel\n    # of stmt.\n    for p in scope_tree.path_children:\n        assert p.path_id is not None\n        ctx.path_scope[p.path_id] = stmt\n\n\ndef update_scope_masks(\n        ir_set: irast.Set, rvar: pgast.PathRangeVar, *,\n        ctx: context.CompilerContextLevel) -> None:\n\n    if not isinstance(rvar, pgast.RangeSubselect):\n        return\n    stmt = rvar.subquery\n\n    # Mark any paths under the scope tree as masked, so that they\n    # won't get picked up by pull_path_namespace.\n    for child_path in ctx.scope_tree.get_all_paths():\n        pathctx.put_path_id_mask(stmt, child_path)\n\n    # If this is an optional scope node, we need to be certain that\n    # we don't leak out any paths that collide with a visible non-optional\n    # path.\n    # See test_edgeql_optional_leakage_01 for one case where this comes up.\n    #\n    # FIXME: I actually think we ought to be able to mask off visible\n    # paths in *most* cases, but when I tried it I ran into trouble\n    # with some DML linkprop cases (probably easy to fix) and a number\n    # of materialization cases (possibly hard to fix), so I'm going\n    # with a more conservative approach.\n    if ctx.scope_tree.is_optional(ir_set.path_id):\n        # Since compilation is done, anything visible to us *will* be\n        # up on the spine. Anything tucked away under a node must have\n        # been pulled up.\n        for anc in ctx.scope_tree.ancestors:\n            for direct_child in anc.path_children:\n                if not direct_child.optional:\n                    pathctx.put_path_id_mask(stmt, direct_child.path_id)\n\n\ndef maybe_get_scope_stmt(\n    path_id: irast.PathId,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> Optional[pgast.SelectStmt]:\n    stmt = ctx.path_scope.get(path_id)\n    if stmt is None and path_id.is_ptr_path():\n        stmt = ctx.path_scope.get(path_id.tgt_path())\n    return stmt\n\n\ndef set_to_array(\n        path_id: irast.PathId, query: pgast.Query, *,\n        for_group_by: bool=False,\n        ctx: context.CompilerContextLevel) -> pgast.Query:\n    \"\"\"Collapse a set into an array.\"\"\"\n    subrvar = pgast.RangeSubselect(\n        subquery=query,\n        alias=pgast.Alias(\n            aliasname=ctx.env.aliases.get('aggw')\n        )\n    )\n\n    result = pgast.SelectStmt()\n    aspects = pathctx.list_path_aspects(subrvar.query, path_id)\n    include_rvar(result, subrvar, path_id=path_id, aspects=aspects, ctx=ctx)\n\n    val: Optional[pgast.BaseExpr] = (\n        pathctx.maybe_get_path_serialized_var(\n            result, path_id, env=ctx.env)\n    )\n\n    if val is None:\n        value_var = pathctx.get_path_value_var(result, path_id, env=ctx.env)\n        val = output.serialize_expr(value_var, path_id=path_id, env=ctx.env)\n        pathctx.put_path_serialized_var(result, path_id, val, force=True)\n\n    if isinstance(val, pgast.TupleVarBase):\n        val = output.serialize_expr(\n            val, path_id=path_id, env=ctx.env)\n\n    pg_type = output.get_pg_type(path_id.target, ctx=ctx)\n\n    agg_filter_safe = True\n\n    if for_group_by:\n        # When doing this as part of a GROUP, the stuff being aggregated\n        # needs to actually appear *inside* of the aggregate call...\n        result.target_list = [pgast.ResTarget(val=val, ser_safe=val.ser_safe)]\n        val = result\n        try_collapse = astutils.collapse_query(val)\n        if isinstance(try_collapse, pgast.ColumnRef):\n            val = try_collapse\n        else:\n            agg_filter_safe = False\n\n        result = pgast.SelectStmt()\n\n    orig_val = val\n\n    if (path_id.is_array_path()\n            and ctx.env.output_format is context.OutputFormat.NATIVE):\n        # We cannot aggregate arrays straight away, as\n        # they be of different length, so we have to\n        # encase each element into a record.\n        val = pgast.RowExpr(args=[val], ser_safe=val.ser_safe)\n        pg_type = ('record',)\n\n    array_agg = pgast.FuncCall(\n        name=('array_agg',),\n        args=[val],\n        agg_filter=(\n            astutils.new_binop(orig_val, pgast.NullConstant(),\n                               'IS DISTINCT FROM')\n            if orig_val.nullable and agg_filter_safe else None\n        ),\n        ser_safe=val.ser_safe,\n    )\n\n    # If this is for a group by, and the body isn't just a column ref,\n    # then we need to remove NULLs after the fact.\n    if orig_val.nullable and not agg_filter_safe:\n        array_agg = pgast.FuncCall(\n            name=('array_remove',),\n            args=[array_agg, pgast.NullConstant()]\n        )\n\n    agg_expr = pgast.CoalesceExpr(\n        args=[\n            array_agg,\n            pgast.TypeCast(\n                arg=pgast.ArrayExpr(elements=[]),\n                type_name=pgast.TypeName(name=pg_type, array_bounds=[-1])\n            )\n        ],\n        ser_safe=array_agg.ser_safe,\n        nullable=False,\n    )\n\n    result.target_list = [\n        pgast.ResTarget(\n            name=ctx.env.aliases.get('v'),\n            val=agg_expr,\n            ser_safe=agg_expr.ser_safe,\n        )\n    ]\n\n    return result\n\n\nclass UnpackElement(NamedTuple):\n    path_id: irast.PathId\n    colname: str\n    packed: bool\n    multi: bool\n    ref: Optional[pgast.BaseExpr]\n\n\ndef unpack_rvar(\n        stmt: pgast.SelectStmt, path_id: irast.PathId, *,\n        packed_rvar: pgast.PathRangeVar,\n        ctx: context.CompilerContextLevel) -> pgast.PathRangeVar:\n    ref = pathctx.get_rvar_path_var(\n        packed_rvar,\n        path_id,\n        aspect=pgce.PathAspect.VALUE,\n        flavor='packed',\n        env=ctx.env,\n    )\n    return unpack_var(stmt, path_id, ref=ref, ctx=ctx)\n\n\ndef unpack_var(\n        stmt: pgast.SelectStmt, path_id: irast.PathId, *,\n        ref: pgast.OutputVar,\n        ctx: context.CompilerContextLevel) -> pgast.PathRangeVar:\n\n    qry = pgast.SelectStmt()\n\n    view_tvars: list[tuple[irast.PathId, pgast.TupleVarBase, bool]] = []\n    els = []\n    ctr = 0\n\n    def walk(ref: pgast.BaseExpr, path_id: irast.PathId, multi: bool) -> None:\n        nonlocal ctr\n\n        coldeflist = []\n        alias = ctx.env.aliases.get('unpack')\n        simple = False\n        if irtyputils.is_tuple(path_id.target):\n            els.append(UnpackElement(\n                path_id, alias, packed=False, multi=False, ref=None\n            ))\n\n            orig_view_count = len(view_tvars)\n            tuple_tvar_elements = []\n            for i, st in enumerate(path_id.target.subtypes):\n                colname = f'_t{ctr}'\n                ctr += 1\n\n                typ = pg_types.pg_type_from_ir_typeref(st)\n                if st.id in ctx.env.materialized_views:\n                    typ = ('record',)\n\n                # Construct a path_id for the element\n                el_name = sn.QualName('__tuple__', st.element_name or str(i))\n                el_ref = irast.TupleIndirectionPointerRef(\n                    name=el_name, shortname=el_name,\n                    out_source=path_id.target,\n                    out_target=st,\n                    out_cardinality=qltypes.Cardinality.ONE,\n                )\n                el_path_id = path_id.extend(ptrref=el_ref)\n\n                el_var = (\n                    astutils.tuple_getattr(\n                        pgast.ColumnRef(name=[alias]),\n                        path_id.target, el_name.name)\n                    if irtyputils.is_persistent_tuple(path_id.target)\n                    else pgast.ColumnRef(name=[colname])\n                )\n                walk(el_var, el_path_id, multi=False)\n\n                tuple_tvar_elements.append(\n                    pgast.TupleElementBase(\n                        path_id=el_path_id, name=el_name.name\n                    )\n                )\n\n                coldeflist.append(\n                    pgast.ColumnDef(\n                        name=colname,\n                        typename=pgast.TypeName(name=typ)\n                    )\n                )\n\n            if len(view_tvars) > orig_view_count:\n                tuple_tvar = pgast.TupleVarBase(\n                    elements=tuple_tvar_elements,\n                    typeref=path_id.target,\n                    named=any(\n                        st.element_name for st in path_id.target.subtypes),\n                )\n                view_tvars.append((path_id, tuple_tvar, True))\n\n            if irtyputils.is_persistent_tuple(path_id.target):\n                coldeflist = []\n\n        elif irtyputils.is_array(path_id.target) and multi:\n            # TODO: materialized arrays of tuples and arrays are really\n            # quite broken\n            coldeflist = [\n                pgast.ColumnDef(\n                    name='q',\n                    typename=pgast.TypeName(\n                        name=pg_types.pg_type_from_ir_typeref(\n                            path_id.target)\n                    )\n                )\n            ]\n\n            els.append(UnpackElement(\n                path_id, coldeflist[0].name,\n                packed=False, multi=False, ref=None\n            ))\n\n        elif path_id.target.id in ctx.env.materialized_views:\n            view_tuple = ctx.env.materialized_views[path_id.target.id]\n\n            vpath_ids = []\n            id_idx = None\n            for el, _ in view_tuple.shape:\n                src_path, el_ptrref = el.path_id.src_path(), el.path_id.rptr()\n                assert src_path and el_ptrref\n\n                el_id = path_id.ptr_path().extend(ptrref=el_ptrref)\n\n                card = el.expr.dir_cardinality\n                is_singleton = card.is_single() and not card.can_be_zero()\n                must_pack = not is_singleton\n\n                if (rptr_name := el_id.rptr_name()) and rptr_name.name == 'id':\n                    id_idx = len(els)\n\n                colname = f'_t{ctr}'\n                ctr += 1\n\n                typ = pg_types.pg_type_from_ir_typeref(el_id.target)\n                if el_id.target.id in ctx.env.materialized_views:\n                    typ = ('record',)\n                    must_pack = True\n\n                if not is_singleton:\n                    # Arrays get wrapped in a record before they can be put\n                    # in another array\n                    if el_id.is_array_path():\n                        typ = ('record',)\n                        must_pack = True\n                    typ = pg_types.pg_type_array(typ)\n\n                coldeflist.append(\n                    pgast.ColumnDef(\n                        name=colname,\n                        typename=pgast.TypeName(name=typ),\n                    )\n                )\n                els.append(UnpackElement(\n                    el_id, colname,\n                    packed=must_pack, multi=not is_singleton, ref=None\n                ))\n\n                vpath_ids.append(el_id)\n\n            if id_idx is not None:\n                els.append(UnpackElement(\n                    path_id, els[id_idx].colname,\n                    multi=False, packed=False, ref=None,\n                ))\n            else:\n                colname = f'_t{ctr}'\n                ctr += 1\n                coldeflist.append(\n                    pgast.ColumnDef(\n                        name=colname,\n                        typename=pgast.TypeName(name=('uuid',)),\n                    )\n                )\n                els.append(UnpackElement(\n                    path_id, colname, packed=False, multi=False, ref=None\n                ))\n\n            view_tvars.append((path_id, pgast.TupleVarBase(\n                elements=[\n                    pgast.TupleElementBase(\n                        path_id=pid,\n                        name=astutils.tuple_element_for_shape_el(\n                            el, ctx=ctx).name,\n                    )\n                    for (el, op), pid in zip(view_tuple.shape, vpath_ids)\n                    if op != qlast.ShapeOp.MATERIALIZE or ctx.materializing\n                ],\n                typeref=path_id.target,\n                named=True,\n            ), False))\n\n        else:\n            coldeflist = []\n            simple = not multi\n            els.append(UnpackElement(\n                path_id, alias, multi=False, packed=False,\n                ref=ref if simple else None,\n            ))\n\n        if not simple:\n            if not multi:\n                # Sigh, have to wrap in an array so we can unpack.\n                ref = pgast.ArrayExpr(elements=[ref])\n\n            qry.from_clause.insert(\n                0,\n                pgast.RangeFunction(\n                    alias=pgast.Alias(\n                        aliasname=alias,\n                    ),\n                    is_rowsfrom=True,\n                    functions=[\n                        pgast.FuncCall(\n                            name=('unnest',),\n                            args=[ref],\n                            coldeflist=coldeflist,\n                        )\n                    ]\n                )\n            )\n\n    ########################\n\n    walk(ref, path_id, ref.is_packed_multi)\n\n    rvar = rvar_for_rel(qry, lateral=True, ctx=ctx)\n    include_rvar(\n        stmt,\n        rvar,\n        path_id=path_id,\n        aspects=(pgce.PathAspect.VALUE,),\n        ctx=ctx,\n    )\n\n    for el in els:\n        el_id = el.path_id\n        cur_ref = el.ref or pgast.ColumnRef(name=[el.colname])\n\n        for aspect in (pgce.PathAspect.VALUE, pgce.PathAspect.SERIALIZED):\n            pathctx.put_path_var(qry, el_id, cur_ref, aspect=aspect)\n\n        if not el.packed:\n            pathctx.put_path_rvar(\n                stmt,\n                el_id,\n                rvar,\n                aspect=pgce.PathAspect.VALUE,\n            )\n\n            pathctx.put_path_rvar(\n                ctx.rel,\n                el_id,\n                rvar,\n                aspect=pgce.PathAspect.VALUE,\n            )\n        else:\n            cref = pathctx.get_path_output(\n                qry,\n                el_id,\n                aspect=pgce.PathAspect.VALUE,\n                env=ctx.env,\n            )\n            cref = cref.replace(is_packed_multi=el.multi)\n\n            pathctx.put_path_packed_output(qry, el_id, val=cref)\n\n            pathctx.put_path_rvar(\n                stmt,\n                el_id,\n                rvar,\n                flavor='packed',\n                aspect=pgce.PathAspect.VALUE,\n            )\n\n    # When we're producing an exposed shape, we need to rewrite the\n    # serialized shape.\n    # We also need to rewrite tuples that contain such shapes!\n    # What a pain!\n    #\n    # We *also* need to rewrite tuple values, so that we don't consider\n    # serialized materialized objects as part of the value of the tuple\n    for view_path_id, view_tvar, is_tuple in view_tvars:\n        if not view_tvar.elements:\n            continue\n\n        rewrite_aspects = []\n        if ctx.expr_exposed and not is_tuple:\n            rewrite_aspects.append(pgce.PathAspect.SERIALIZED)\n        if is_tuple:\n            rewrite_aspects.append(pgce.PathAspect.VALUE)\n\n        # Reserialize links if we are producing final output\n        if (\n            ctx.expr_exposed and not ctx.materializing and not is_tuple\n        ):\n            for tel in view_tvar.elements:\n                el = [x for x in els if x.path_id == tel.path_id][0]\n                if not el.packed:\n                    continue\n                reqry = reserialize_object(el, tel, ctx=ctx)\n                pathctx.put_path_var(\n                    qry,\n                    tel.path_id,\n                    reqry,\n                    aspect=pgce.PathAspect.SERIALIZED,\n                    force=True,\n                )\n\n        for aspect in rewrite_aspects:\n            tv = pathctx.fix_tuple(qry, view_tvar, aspect=aspect, env=ctx.env)\n            sval = (\n                output.output_as_value(tv, env=ctx.env)\n                if aspect == pgce.PathAspect.VALUE else\n                output.serialize_expr(tv, path_id=view_path_id, env=ctx.env)\n            )\n            pathctx.put_path_var(\n                qry, view_path_id, sval, aspect=aspect, force=True\n            )\n            pathctx.put_path_rvar(ctx.rel, view_path_id, rvar, aspect=aspect)\n\n    return rvar\n\n\ndef reserialize_object(\n        el: UnpackElement, tel: pgast.TupleElementBase,\n        *,\n        ctx: context.CompilerContextLevel) -> pgast.Query:\n    tref = pgast.ColumnRef(name=[el.colname], is_packed_multi=el.multi)\n\n    with ctx.subrel() as subctx:\n        sub_rvar = unpack_var(subctx.rel, tel.path_id, ref=tref, ctx=subctx)\n    reqry = sub_rvar.query\n    assert isinstance(reqry, pgast.Query)\n    rptr = tel.path_id.rptr()\n    pathctx.get_path_serialized_output(reqry, tel.path_id, env=ctx.env)\n    assert rptr\n    if rptr.out_cardinality.is_multi():\n        with ctx.subrel() as subctx:\n            reqry = set_to_array(\n                path_id=tel.path_id, query=reqry, ctx=subctx)\n    return reqry\n\n\ndef get_scope_stmt(\n    path_id: irast.PathId,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.SelectStmt:\n    stmt = maybe_get_scope_stmt(path_id, ctx=ctx)\n    if stmt is None:\n        raise LookupError(f'cannot find scope statement for {path_id}')\n    else:\n        return stmt\n\n\ndef rel_join(\n    query: pgast.SelectStmt,\n    right_rvar: pgast.PathRangeVar,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> None:\n\n    if (\n        isinstance(right_rvar, pgast.RangeSubselect)\n        and astutils.is_set_op_query(right_rvar.subquery)\n        and right_rvar.tag == \"overlay-stack\"\n        and all(isinstance(q, pgast.SelectStmt)\n                for q in astutils.each_query_in_set(right_rvar.subquery))\n        and not is_pointer_rvar(right_rvar, ctx=ctx)\n    ):\n        # Unfortunately Postgres sometimes produces a very bad plan\n        # when we join a UNION which is not a trivial Append, most notably\n        # those produced by DML overlays.  To work around this we push\n        # the JOIN condition into the WHERE clause of each UNION component.\n        # While this is likely not harmful (and possibly beneficial) for\n        # all kinds of UNIONs, we restrict this optimization to overlay\n        # UNIONs only to limit the possibility of breakage as not all\n        # UNIONs are guaranteed to have correct path namespace and\n        # translation maps set up.\n        _lateral_union_join(query, right_rvar, ctx=ctx)\n    else:\n        _plain_join(query, right_rvar, ctx=ctx)\n\n\ndef _plain_join(\n    query: pgast.SelectStmt,\n    right_rvar: pgast.PathRangeVar,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> None:\n    condition = None\n\n    for path_id, iterator_var in right_rvar.query.path_bonds:\n        lref = None\n        aspect = (\n            pgce.PathAspect.ITERATOR\n            if iterator_var else\n            pgce.PathAspect.IDENTITY\n        )\n\n        lref = maybe_get_path_var(\n            query, path_id, aspect=aspect, ctx=ctx)\n        if lref is None and not iterator_var:\n            lref = maybe_get_path_var(\n                query,\n                path_id,\n                aspect=pgce.PathAspect.VALUE,\n                ctx=ctx,\n            )\n        if lref is None:\n            continue\n\n        rref = pathctx.get_rvar_path_var(\n            right_rvar, path_id, aspect=aspect, env=ctx.env)\n\n        assert isinstance(lref, pgast.ColumnRef)\n        assert isinstance(rref, pgast.ColumnRef)\n        path_cond = astutils.join_condition(lref, rref)\n        condition = astutils.extend_binop(condition, path_cond)\n\n    if condition is None:\n        join_type = 'cross'\n    else:\n        join_type = 'inner'\n\n    if not query.from_clause:\n        query.from_clause.append(right_rvar)\n        if condition is not None:\n            query.where_clause = astutils.extend_binop(\n                query.where_clause, condition)\n    else:\n        larg = query.from_clause[0]\n        rarg = right_rvar\n\n        query.from_clause[0] = pgast.JoinExpr.make_inplace(\n            type=join_type, larg=larg, rarg=rarg, quals=condition)\n\n\ndef _lateral_union_join(\n    query: pgast.SelectStmt,\n    right_rvar: pgast.RangeSubselect,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> None:\n    # Inject the filter into every subquery\n    for component in astutils.each_query_in_set(right_rvar.subquery):\n        condition = None\n\n        for path_id, iterator_var in right_rvar.query.path_bonds:\n            aspect = (\n                pgce.PathAspect.ITERATOR\n                if iterator_var else\n                pgce.PathAspect.IDENTITY\n            )\n            lref = maybe_get_path_var(\n                query, path_id, aspect=aspect, ctx=ctx)\n            if lref is None and not iterator_var:\n                lref = maybe_get_path_var(\n                    query,\n                    path_id,\n                    aspect=pgce.PathAspect.VALUE,\n                    ctx=ctx,\n                )\n            if lref is None:\n                continue\n\n            rref = pathctx.get_path_var(\n                component, path_id, aspect=aspect, env=ctx.env)\n\n            assert isinstance(lref, pgast.ColumnRef)\n            assert isinstance(rref, pgast.ColumnRef)\n            path_cond = astutils.join_condition(lref, rref)\n            condition = astutils.extend_binop(condition, path_cond)\n\n        if condition is not None:\n            assert isinstance(component, pgast.SelectStmt)\n            component.where_clause = astutils.extend_binop(\n                component.where_clause, condition)\n\n    # Do the actual join\n    if not query.from_clause:\n        query.from_clause.append(right_rvar)\n    else:\n        larg = query.from_clause[0]\n        rarg = right_rvar\n\n        query.from_clause[0] = pgast.JoinExpr.make_inplace(\n            type='cross', larg=larg, rarg=rarg)\n\n\ndef _needs_cte(typeref: irast.TypeRef) -> bool:\n    \"\"\"Check whether a typeref needs to be forced into a materialized CTE.\n\n    The main use case here is for sys::SystemObjects which are stored as\n    views that populate their data by parsing JSON metadata embedded in\n    comments on the SQL system objects. The query plans when fetching multi\n    links from these objects wind up being pretty pathologically quadratic.\n\n    So instead we force the objects and links into materialized CTEs\n    so that they *can't* be shoved into nested loops.\n    \"\"\"\n    assert isinstance(typeref.name_hint, sn.QualName)\n    return typeref.name_hint.module == 'sys'\n\n\ndef range_for_material_objtype(\n    typeref: irast.TypeRef,\n    path_id: irast.PathId,\n    *,\n    for_mutation: bool=False,\n    lateral: bool=False,\n    include_overlays: bool=True,\n    include_descendants: bool=True,\n    ignore_rewrites: bool=False,\n    is_global: bool=False,\n    dml_source: Sequence[irast.MutatingLikeStmt]=(),\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n\n    if not is_global:\n        typeref = typeref.real_material_type\n\n    if not is_global and not path_id.is_objtype_path():\n        raise ValueError('cannot create root rvar for non-object path')\n\n    assert isinstance(typeref.name_hint, sn.QualName)\n\n    dml_source_key = (\n        frozenset(dml_source) if ctx.trigger_mode and dml_source else None\n    )\n    rw_key = (typeref.id, include_descendants)\n    key = rw_key + (dml_source_key,)\n    force_cte = _needs_cte(typeref)\n    if (\n        (not ignore_rewrites or is_global)\n        and (\n            (rewrite := ctx.env.type_rewrites.get(rw_key)) is not None\n            or force_cte\n        )\n        and rw_key not in ctx.pending_type_rewrite_ctes\n        and not for_mutation\n    ):\n        if not rewrite:\n            # If we are forcing CTE materialization but there is not a\n            # real rewrite, then create a trivial one.\n            rewrite = irast.Set(\n                path_id=irast.PathId.from_typeref(typeref, namespace={'rw'}),\n                typeref=typeref,\n                expr=irast.TypeRoot(typeref=typeref),\n            )\n\n        # Don't include overlays in the normal way in trigger mode\n        # when a type cte is used, because we bake the overlays into\n        # the cte instead (and so including them normally could union\n        # back in things that we have filtered out).\n        #\n        # We *don't* do this if we actually have DML sources; if it is\n        # __old__, because we don't want overlays at all and for\n        # __new__ and for actual DML because we want the overlays to\n        # apply after policies.\n        trigger_mode = ctx.trigger_mode and not dml_source\n        if trigger_mode:\n            include_overlays = False\n\n        type_rel: pgast.BaseRelation | pgast.CommonTableExpr\n        if (type_cte := ctx.type_rewrite_ctes.get(key)) is None:\n            with ctx.newrel() as sctx:\n                sctx.pending_type_rewrite_ctes.add(rw_key)\n                sctx.pending_query = sctx.rel\n                # Normally we want to compile type rewrites without\n                # polluting them with any sort of overlays, but when\n                # compiling triggers, we recompile all of the type\n                # rewrites *to include* overlays, so that we can't peek\n                # at all newly created objects that we can't see\n                if not trigger_mode:\n                    sctx.rel_overlays = context.RelOverlays()\n\n                dispatch.visit(rewrite, ctx=sctx)\n                # If we are explaining, we also expand type\n                # rewrites, so don't populate type_ctes. The normal\n                # case is to stick it in a CTE and cache that, though.\n                if ctx.env.is_explain and not is_global:\n                    type_rel = sctx.rel\n                else:\n                    type_cte = pgast.CommonTableExpr(\n                        name=ctx.env.aliases.get(f't_{typeref.name_hint}'),\n                        query=sctx.rel,\n                        materialized=is_global or force_cte,\n                    )\n                    ctx.type_rewrite_ctes[key] = type_cte\n                    ctx.ordered_type_ctes.append(type_cte)\n                    type_rel = type_cte\n        else:\n            type_rel = type_cte\n\n        with ctx.subrel() as sctx:\n            cte_rvar = rvar_for_rel(\n                type_rel,\n                typeref=typeref,\n                alias=ctx.env.aliases.get('t'),\n                ctx=ctx,\n            )\n            pathctx.put_path_id_map(sctx.rel, path_id, rewrite.path_id)\n            include_rvar(\n                sctx.rel, cte_rvar, rewrite.path_id, pull_namespace=False,\n                ctx=sctx,\n            )\n            rvar = rvar_for_rel(\n                sctx.rel, lateral=lateral, typeref=typeref, ctx=sctx)\n\n    else:\n        assert not typeref.is_view, \"attempting to generate range from view\"\n        typeref_descendants = _get_typeref_descendants(\n            typeref,\n            include_descendants=include_descendants,\n            for_mutation=for_mutation,\n        )\n        if (\n            # When we are compiling a query for EXPLAIN, expand out type\n            # references to an explicit union of all the types, rather than\n            # using a CTE. This allows postgres to actually give us back the\n            # alias names that we use for relations, which we use to track which\n            # parts of the query are being referred to.\n            ctx.env.is_explain\n\n            # Don't use CTEs if there is no inheritance. (ie. There is only a\n            # single material type)\n            or len(typeref_descendants) <= 1\n        ):\n            inheritance_selects = _selects_for_typeref_descendants(\n                typeref_descendants, path_id, ctx=ctx,\n            )\n            ops = [\n                (context.OverlayOp.UNION, select)\n                for select in inheritance_selects\n            ]\n\n            rvar = range_from_queryset(\n                ops,\n                typeref.name_hint,\n                lateral=lateral,\n                path_id=path_id,\n                typeref=typeref,\n                tag='expanded-inhview',\n                ctx=ctx,\n            )\n\n        else:\n            typeref_path: irast.PathId = irast.PathId.from_typeref(\n                typeref,\n                # If there are backlinks and the path revisits a type, a\n                # semi-join is produced. This ensures that the rvar produced\n                # does not have a duplicate path var.\n                # For example: (select A.b.<b[is A])\n                namespace=frozenset({'typeref'})\n            )\n\n            if typeref.id not in ctx.type_inheritance_ctes:\n                inheritance_selects = _selects_for_typeref_descendants(\n                    typeref_descendants, typeref_path, ctx=ctx,\n                )\n\n                type_qry: pgast.SelectStmt = inheritance_selects[0]\n                for rarg in inheritance_selects[1:]:\n                    type_qry = pgast.SelectStmt(\n                        op='union',\n                        all=True,\n                        larg=type_qry,\n                        rarg=rarg,\n                    )\n\n                type_cte = pgast.CommonTableExpr(\n                    name=ctx.env.aliases.get(f't_{typeref.name_hint}'),\n                    query=type_qry,\n                    materialized=False,\n                )\n                ctx.type_inheritance_ctes[typeref.id] = type_cte\n                ctx.ordered_type_ctes.append(type_cte)\n\n            else:\n                type_cte = ctx.type_inheritance_ctes[typeref.id]\n\n            with ctx.subrel() as sctx:\n                cte_rvar = rvar_for_rel(\n                    type_cte,\n                    typeref=typeref,\n                    ctx=ctx,\n                )\n                if path_id != typeref_path:\n                    pathctx.put_path_id_map(sctx.rel, path_id, typeref_path)\n                include_rvar(\n                    sctx.rel,\n                    cte_rvar,\n                    typeref_path,\n                    pull_namespace=False,\n                    ctx=sctx,\n                )\n                rvar = rvar_for_rel(\n                    sctx.rel, lateral=lateral, typeref=typeref, ctx=sctx)\n\n    overlays = get_type_rel_overlays(typeref, dml_source=dml_source, ctx=ctx)\n    external_rvar = ctx.env.external_rvars.get(\n        (path_id, pgce.PathAspect.SOURCE)\n    )\n    if external_rvar is not None:\n        if overlays:\n            raise AssertionError('cannot mix external and internal overlays')\n        return external_rvar\n\n    if overlays and include_overlays:\n        set_ops = []\n\n        qry = pgast.SelectStmt()\n        qry.from_clause.append(rvar)\n        pathctx.put_path_value_rvar(qry, path_id, rvar)\n        if path_id.is_objtype_path():\n            pathctx.put_path_source_rvar(qry, path_id, rvar)\n        pathctx.put_path_bond(qry, path_id)\n\n        set_ops.append((context.OverlayOp.UNION, qry))\n\n        for op, cte, cte_path_id in overlays:\n            rvar = rvar_for_rel(cte, typeref=typeref, ctx=ctx)\n\n            qry = pgast.SelectStmt(\n                from_clause=[rvar],\n            )\n\n            pathctx.put_path_value_rvar(qry, cte_path_id, rvar)\n            if path_id.is_objtype_path():\n                pathctx.put_path_source_rvar(qry, cte_path_id, rvar)\n            pathctx.put_path_bond(qry, cte_path_id)\n            pathctx.put_path_id_map(qry, path_id, cte_path_id)\n\n            qry_rvar = pgast.RangeSubselect(\n                subquery=qry,\n                alias=pgast.Alias(\n                    aliasname=ctx.env.aliases.get(hint=cte.name or '')\n                )\n            )\n\n            qry2 = pgast.SelectStmt(\n                from_clause=[qry_rvar]\n            )\n            pathctx.put_path_value_rvar(qry2, path_id, qry_rvar)\n            if path_id.is_objtype_path():\n                pathctx.put_path_source_rvar(qry2, path_id, qry_rvar)\n            pathctx.put_path_bond(qry2, path_id)\n\n            if op == context.OverlayOp.REPLACE:\n                op = context.OverlayOp.UNION\n                set_ops = []\n            set_ops.append((op, qry2))\n\n        rvar = range_from_queryset(\n            set_ops,\n            typeref.name_hint,\n            lateral=lateral,\n            path_id=path_id,\n            typeref=typeref,\n            tag='overlay-stack',\n            ctx=ctx,\n        )\n\n    return rvar\n\n\ndef _get_typeref_descendants(\n    typeref: irast.TypeRef,\n    *,\n    include_descendants: bool,\n    for_mutation: bool,\n) -> list[irast.TypeRef]:\n    if (\n        include_descendants\n        and not for_mutation\n    ):\n        descendants = [\n            typeref,\n            *(\n                descendant\n                for descendant in irtyputils.get_typeref_descendants(typeref)\n\n                # XXX: Exclude sys/cfg tables from non sys/cfg inheritance CTEs.\n                # This probably isn't *really* what we want to do, but until we\n                # figure that out, do *something* so that DDL isn't\n                # excruciatingly slow because of the cost of explicit id\n                # checks. See #5168.\n                if (\n                    not descendant.is_cfg_view\n                    or typeref.is_cfg_view\n                )\n            )\n        ]\n\n        # Try to only select from actual concrete types.\n        concrete_descendants = [\n            subref for subref in descendants if not subref.is_abstract\n        ]\n        # If there aren't any concrete types, we still need to\n        # generate *something*, so just do the initial one.\n        if concrete_descendants:\n            return concrete_descendants\n        else:\n            return [typeref]\n\n    else:\n        return [typeref]\n\n\ndef _selects_for_typeref_descendants(\n    typeref_descendants: Sequence[irast.TypeRef],\n    path_id: irast.PathId,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> list[pgast.SelectStmt]:\n    selects = []\n    for subref in typeref_descendants:\n        rvar = _table_from_typeref(\n            subref, path_id, ctx=ctx,\n        )\n        qry = pgast.SelectStmt(from_clause=[rvar])\n        sub_path_id = path_id\n        pathctx.put_path_value_rvar(qry, sub_path_id, rvar)\n        pathctx.put_path_source_rvar(qry, sub_path_id, rvar)\n\n        selects.append(qry)\n\n    return selects\n\n\ndef _table_from_typeref(\n    typeref: irast.TypeRef,\n    path_id: irast.PathId,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n    assert isinstance(typeref.name_hint, sn.QualName)\n\n    aspect = 'table'\n\n    table_schema_name, table_name = common.get_objtype_backend_name(\n        typeref.id,\n        typeref.name_hint.module,\n        aspect=aspect,\n        catenate=False,\n        versioned=ctx.env.versioned_stdlib,\n    )\n\n    relation = pgast.Relation(\n        schemaname=table_schema_name,\n        name=table_name,\n        path_id=path_id,\n        type_or_ptr_ref=typeref,\n    )\n\n    return pgast.RelRangeVar(\n        relation=relation,\n        typeref=typeref,\n        alias=pgast.Alias(\n            aliasname=ctx.env.aliases.get(typeref.name_hint.name)\n        )\n    )\n\n\ndef range_for_typeref(\n    typeref: irast.TypeRef,\n    path_id: irast.PathId,\n    *,\n    lateral: bool=False,\n    for_mutation: bool=False,\n    include_descendants: bool=True,\n    ignore_rewrites: bool=False,\n    is_global: bool=False,\n    dml_source: Sequence[irast.MutatingLikeStmt]=(),\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n\n    if typeref.union:\n        # Union object types are represented as a UNION of selects\n        # from their children, which is, for most purposes, equivalent\n        # to SELECTing from a parent table.\n        set_ops = []\n\n        # Concrete unions might have view type elements with duplicate\n        # material types, and we need to filter those out.\n        seen = set()\n        for child in typeref.union:\n            mat_child = child.material_type or child\n            if mat_child.id in seen:\n                assert typeref.union_is_exhaustive\n                continue\n            seen.add(mat_child.id)\n\n            c_rvar = range_for_typeref(\n                child,\n                path_id=path_id,\n                include_descendants=not typeref.union_is_exhaustive,\n                for_mutation=for_mutation,\n                dml_source=dml_source,\n                lateral=lateral,\n                ctx=ctx,\n            )\n\n            qry = pgast.SelectStmt(\n                from_clause=[c_rvar],\n            )\n\n            pathctx.put_path_value_rvar(qry, path_id, c_rvar)\n            if path_id.is_objtype_path():\n                pathctx.put_path_source_rvar(qry, path_id, c_rvar)\n\n            pathctx.put_path_bond(qry, path_id)\n\n            set_ops.append((context.OverlayOp.UNION, qry))\n\n        rvar = range_from_queryset(\n            set_ops,\n            typeref.name_hint,\n            lateral=lateral,\n            typeref=typeref,\n            ctx=ctx,\n        )\n\n    else:\n        rvar = range_for_material_objtype(\n            typeref,\n            path_id,\n            lateral=lateral,\n            include_descendants=include_descendants,\n            ignore_rewrites=ignore_rewrites,\n            include_overlays=not for_mutation,\n            for_mutation=for_mutation,\n            is_global=is_global,\n            dml_source=dml_source,\n            ctx=ctx,\n        )\n\n    rvar.query.path_id = path_id\n\n    return rvar\n\n\ndef wrap_set_op_query(\n    qry: pgast.SelectStmt, *,\n    ctx: context.CompilerContextLevel\n) -> pgast.SelectStmt:\n    if astutils.is_set_op_query(qry):\n        rvar = rvar_for_rel(qry, ctx=ctx)\n        nqry = pgast.SelectStmt(from_clause=[rvar])\n        nqry.target_list = [\n            pgast.ResTarget(\n                name=col.name,\n                val=pgast.ColumnRef(\n                    name=[rvar.alias.aliasname, col.name],\n                )\n            )\n            for col in astutils.get_leftmost_query(qry).target_list\n            if col.name\n        ]\n\n        pull_path_namespace(target=nqry, source=rvar, ctx=ctx)\n        qry = nqry\n    return qry\n\n\ndef anti_join(\n    lhs: pgast.SelectStmt, rhs: pgast.SelectStmt,\n    path_id: Optional[irast.PathId], *,\n    aspect: pgce.PathAspect=pgce.PathAspect.IDENTITY,\n    ctx: context.CompilerContextLevel,\n) -> None:\n    \"\"\"Filter elements out of the LHS that appear on the RHS\"\"\"\n\n    if path_id:\n        # grab the identity from the LHS and do an\n        # anti-join against the RHS.\n        src_ref = pathctx.get_path_var(\n            lhs, path_id=path_id, aspect=aspect, env=ctx.env)\n        pathctx.get_path_output(\n            rhs, path_id=path_id, aspect=aspect, env=ctx.env)\n        cond_expr: pgast.BaseExpr = astutils.new_binop(\n            src_ref, rhs, 'NOT IN')\n    else:\n        # No path we care about. Just check existance.\n        cond_expr = pgast.SubLink(operator=\"NOT EXISTS\", expr=rhs)\n    lhs.where_clause = astutils.extend_binop(lhs.where_clause, cond_expr)\n\n\ndef range_from_queryset(\n    set_ops: Sequence[tuple[context.OverlayOp, pgast.SelectStmt]],\n    objname: sn.Name,\n    *,\n    prep_filter: Callable[\n        [pgast.SelectStmt, pgast.SelectStmt], None]=lambda a, b: None,\n    path_id: Optional[irast.PathId]=None,\n    lateral: bool=False,\n    typeref: Optional[irast.TypeRef]=None,\n    tag: Optional[str]=None,\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n\n    rvar: pgast.PathRangeVar\n\n    if len(set_ops) > 1:\n        # More than one class table, generate a UNION/EXCEPT clause.\n        qry = set_ops[0][1]\n\n        for op, rarg in set_ops[1:]:\n            if op == context.OverlayOp.FILTER:\n                qry = wrap_set_op_query(qry, ctx=ctx)\n                prep_filter(qry, rarg)\n                anti_join(qry, rarg, path_id, ctx=ctx)\n            else:\n                qry = pgast.SelectStmt(\n                    op=op,\n                    all=True,\n                    larg=qry,\n                    rarg=rarg,\n                )\n\n        rvar = pgast.RangeSubselect(\n            subquery=qry,\n            lateral=lateral,\n            tag=tag,\n            alias=pgast.Alias(\n                aliasname=ctx.env.aliases.get(objname.name),\n            ),\n            typeref=typeref,\n        )\n\n    elif any(\n        (\n            target.name is not None\n            and isinstance(target.val, pgast.ColumnRef)\n            and target.name != target.val.name[-1]\n        )\n        for target in set_ops[0][1].target_list\n    ):\n        # A column name name is being changed\n        rvar = pgast.RangeSubselect(\n            subquery=set_ops[0][1],\n            lateral=lateral,\n            tag=tag,\n            alias=pgast.Alias(\n                aliasname=ctx.env.aliases.get(objname.name),\n            ),\n            typeref=typeref,\n        )\n\n    else:\n        # Just one class table, so return it directly\n        from_rvar = set_ops[0][1].from_clause[0]\n        assert isinstance(from_rvar, pgast.PathRangeVar)\n        from_rvar = from_rvar.replace(typeref=typeref)\n        rvar = from_rvar\n\n    return rvar\n\n\ndef range_for_ptrref(\n    ptrref: irast.BasePointerRef, *,\n    dml_source: Sequence[irast.MutatingLikeStmt]=(),\n    for_mutation: bool=False,\n    only_self: bool=False,\n    path_id: Optional[irast.PathId]=None,\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n    \"\"\"\"Return a Range subclass corresponding to a given ptr step.\n\n    The return value may potentially be a UNION of all tables\n    corresponding to a set of specialized links computed from the given\n    `ptrref` taking source inheritance into account.\n    \"\"\"\n\n    if ptrref.union_components:\n        component_refs = ptrref.union_components\n        if only_self and len(component_refs) > 1:\n            raise errors.InternalServerError(\n                'unexpected union link'\n            )\n    elif ptrref.intersection_components:\n        # This is a little funky, but in an intersection, the pointer\n        # needs to appear in *all* of the tables, so we just pick any\n        # one of them.\n        component_refs = {next(iter((ptrref.intersection_components)))}\n    elif ptrref.computed_link_alias:\n        component_refs = {ptrref.computed_link_alias}\n    else:\n        component_refs = {ptrref}\n\n    assert isinstance(ptrref.out_source.name_hint, sn.QualName)\n    include_descendants = not ptrref.union_is_exhaustive\n\n    output_cols = ('source', 'target')\n\n    set_ops = []\n\n    for component_ref in component_refs:\n        assert isinstance(component_ref, irast.PointerRef), \\\n            \"expected regular PointerRef\"\n\n        component_rvar = _range_for_component_ptrref(\n            component_ref,\n            output_cols,\n            dml_source=dml_source,\n            include_descendants=include_descendants,\n            for_mutation=for_mutation,\n            path_id=path_id,\n            ctx=ctx,\n        )\n\n        component_qry = pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    val=pgast.ColumnRef(\n                        name=[output_colname]\n                    ),\n                    name=output_colname\n                )\n                for output_colname in output_cols\n            ],\n            from_clause=[component_rvar]\n        )\n        if path_id:\n            target_ref = pgast.ColumnRef(\n                name=[component_rvar.alias.aliasname, output_cols[1]]\n            )\n            pathctx.put_path_identity_var(\n                component_qry, path_id, var=target_ref\n            )\n            pathctx.put_path_source_rvar(\n                component_qry, path_id, component_rvar\n            )\n\n        set_ops.append((context.OverlayOp.UNION, component_qry))\n\n    return range_from_queryset(\n        set_ops,\n        ptrref.shortname,\n        path_id=path_id,\n        ctx=ctx,\n    )\n\n\ndef _range_for_component_ptrref(\n    component_ptrref: irast.PointerRef,\n    output_cols: Sequence[str],\n    *,\n    dml_source: Sequence[irast.MutatingLikeStmt],\n    include_descendants: bool,\n    for_mutation: bool,\n    path_id: Optional[irast.PathId],\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n    ptrref_descendants = _get_ptrref_descendants(\n        component_ptrref,\n        include_descendants=include_descendants,\n        for_mutation=for_mutation,\n    )\n\n    if (\n        # When we are compiling a query for EXPLAIN, expand out pointer\n        # references in place. See range_for_material_objtype for more details.\n        ctx.env.is_explain\n\n        # Don't use CTEs if there is no inheritance. (ie. There is only a\n        # single ptrref)\n        or len(ptrref_descendants) <= 1\n    ):\n        descendant_selects = _selects_for_ptrref_descendants(\n            ptrref_descendants,\n            output_cols=output_cols,\n            path_id=path_id,\n            ctx=ctx,\n        )\n        descendant_ops = [\n            (context.OverlayOp.UNION, select)\n            for select in descendant_selects\n        ]\n        component_rvar = range_from_queryset(\n            descendant_ops,\n            component_ptrref.shortname,\n            path_id=path_id,\n            ctx=ctx,\n        )\n\n    else:\n        component_ptrref_path_id: irast.PathId = irast.PathId.from_ptrref(\n            component_ptrref,\n        ).ptr_path()\n\n        if component_ptrref.id not in ctx.ptr_inheritance_ctes:\n            descendant_selects = _selects_for_ptrref_descendants(\n                ptrref_descendants,\n                output_cols=output_cols,\n                path_id=component_ptrref_path_id,\n                ctx=ctx,\n            )\n\n            inheritance_qry: pgast.SelectStmt = descendant_selects[0]\n            for rarg in descendant_selects[1:]:\n                inheritance_qry = pgast.SelectStmt(\n                    op='union',\n                    all=True,\n                    larg=inheritance_qry,\n                    rarg=rarg,\n                )\n\n            # Add the path to the CTE's query. This allows for the proper\n            # path mapping to occur when processing link properties\n            inheritance_qry.path_id = component_ptrref_path_id\n\n            ptr_cte = pgast.CommonTableExpr(\n                name=ctx.env.aliases.get(f't_{component_ptrref.name}'),\n                query=inheritance_qry,\n                materialized=False,\n            )\n            ctx.ptr_inheritance_ctes[component_ptrref.id] = ptr_cte\n\n        else:\n            ptr_cte = ctx.ptr_inheritance_ctes[component_ptrref.id]\n\n        with ctx.subrel() as sctx:\n            cte_rvar = rvar_for_rel(\n                ptr_cte,\n                typeref=component_ptrref.out_target,\n                ctx=ctx,\n            )\n            if path_id is not None and path_id != component_ptrref_path_id:\n                pathctx.put_path_id_map(\n                    sctx.rel,\n                    path_id,\n                    component_ptrref_path_id,\n                )\n            include_rvar(\n                sctx.rel,\n                cte_rvar,\n                component_ptrref_path_id,\n                pull_namespace=False,\n                ctx=sctx,\n            )\n\n            # Ensure source and target columns are output\n            for output_colname in output_cols:\n                selexpr = pgast.ColumnRef(\n                    name=[cte_rvar.alias.aliasname, output_colname])\n                sctx.rel.target_list.append(\n                    pgast.ResTarget(val=selexpr, name=output_colname)\n                )\n\n            target_ref = sctx.rel.target_list[1].val\n            pathctx.put_path_identity_var(\n                sctx.rel, component_ptrref_path_id, var=target_ref\n            )\n            pathctx.put_path_source_rvar(\n                sctx.rel,\n                component_ptrref_path_id,\n                cte_rvar,\n            )\n\n            component_rvar = rvar_for_rel(\n                sctx.rel,\n                typeref=component_ptrref.out_target,\n                ctx=sctx\n            )\n\n    # Add overlays at the end of each expanded inheritance.\n    overlays = get_ptr_rel_overlays(\n        component_ptrref, dml_source=dml_source, ctx=ctx)\n    if overlays and not for_mutation:\n        set_ops = []\n\n        component_qry = pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    val=pgast.ColumnRef(\n                        name=[output_colname]\n                    ),\n                    name=output_colname\n                )\n                for output_colname in output_cols\n            ],\n            from_clause=[component_rvar]\n        )\n        if path_id:\n            target_ref = pgast.ColumnRef(\n                name=[component_rvar.alias.aliasname, output_cols[1]]\n            )\n            pathctx.put_path_identity_var(\n                component_qry, path_id, var=target_ref\n            )\n            pathctx.put_path_source_rvar(\n                component_qry, path_id, component_rvar\n            )\n\n        set_ops.append((context.OverlayOp.UNION, component_qry))\n\n        orig_ptr_info = _get_ptrref_storage_info(component_ptrref, ctx=ctx)\n        cols = _get_ptrref_column_names(orig_ptr_info)\n\n        for op, cte, cte_path_id in overlays:\n            rvar = rvar_for_rel(cte, ctx=ctx)\n\n            qry = pgast.SelectStmt(\n                target_list=[\n                    pgast.ResTarget(\n                        val=pgast.ColumnRef(\n                            name=[col]\n                        )\n                    )\n                    for col in cols\n                ],\n                from_clause=[rvar],\n            )\n            # Set up identity var, source rvar for reasons discussed above\n            if path_id:\n                target_ref = pgast.ColumnRef(\n                    name=[rvar.alias.aliasname, cols[1]])\n                pathctx.put_path_identity_var(\n                    qry, cte_path_id, var=target_ref\n                )\n                pathctx.put_path_source_rvar(qry, cte_path_id, rvar)\n                pathctx.put_path_id_map(qry, path_id, cte_path_id)\n\n            set_ops.append((op, qry))\n\n        component_rvar = range_from_queryset(\n            set_ops, component_ptrref.shortname,\n            prep_filter=_prep_filter, path_id=path_id, ctx=ctx)\n\n    return component_rvar\n\n\ndef _prep_filter(larg: pgast.SelectStmt, rarg: pgast.SelectStmt) -> None:\n    # Set up the proper join on the source field and clear the target list\n    # of the rhs of a filter overlay.\n\n    # If the names don't have table refs, make them refer to the table\n    # being joined.\n    lval = larg.target_list[0].val\n    assert isinstance(lval, pgast.ColumnRef)\n    if len(lval.name) == 1:\n        lval = astutils.get_column(larg.from_clause[0], lval)\n\n    rval = rarg.target_list[0].val\n    assert isinstance(rval, pgast.ColumnRef)\n    if len(rval.name) == 1:\n        rval = astutils.get_column(rarg.from_clause[0], rval)\n\n    rarg.where_clause = astutils.join_condition(lval, rval)\n    rarg.target_list.clear()\n\n\ndef _get_ptrref_descendants(\n    ptrref: irast.PointerRef,\n    *,\n    include_descendants: bool,\n    for_mutation: bool,\n) -> list[irast.PointerRef]:\n    # When doing EXPLAIN, don't use CTEs. See range_for_material_objtype for\n    # details.\n    if (\n        include_descendants\n        and not for_mutation\n    ):\n        include_descendants = False\n\n        descendants: list[irast.PointerRef] = []\n        descendants.extend(\n            cast(Iterable[irast.PointerRef], ptrref.descendants())\n        )\n        descendants.append(ptrref)\n        assert isinstance(ptrref, irast.PointerRef)\n\n        # Try to only select from actual concrete types.\n        concrete_descendants = [\n            ref for ref in descendants if not ref.out_source.is_abstract\n        ]\n        # If there aren't any concrete types, we still need to\n        # generate *something*, so just do the initial one.\n        if concrete_descendants:\n            return concrete_descendants\n        else:\n            return [ptrref]\n\n    else:\n        return [ptrref]\n\n\ndef _selects_for_ptrref_descendants(\n    ptrref_descendants: Sequence[irast.PointerRef],\n    output_cols: Iterable[str],\n    *,\n    path_id: Optional[irast.PathId],\n    ctx: context.CompilerContextLevel,\n) -> list[pgast.SelectStmt]:\n    selects = []\n\n    for ptrref_descendant in ptrref_descendants:\n        ptr_info = _get_ptrref_storage_info(ptrref_descendant, ctx=ctx)\n        cols = _get_ptrref_column_names(ptr_info)\n\n        table = _table_from_ptrref(\n            ptrref_descendant,\n            ptr_info,\n            ctx=ctx,\n        )\n        table.query.path_id = path_id\n\n        qry = pgast.SelectStmt()\n        qry.from_clause.append(table)\n\n        # Make sure all property references are pulled up properly\n        for colname, output_colname in zip(cols, output_cols):\n            selexpr = pgast.ColumnRef(\n                name=[table.alias.aliasname, colname])\n            qry.target_list.append(\n                pgast.ResTarget(val=selexpr, name=output_colname))\n\n        selects.append(qry)\n\n        # We need the identity var for semi_join to work and\n        # the source rvar so that linkprops can be found here.\n        if path_id:\n            target_ref = qry.target_list[1].val\n            pathctx.put_path_identity_var(qry, path_id, var=target_ref)\n            pathctx.put_path_source_rvar(qry, path_id, table)\n\n    return selects\n\n\ndef _table_from_ptrref(\n    ptrref: irast.PointerRef,\n    ptr_info: pg_types.PointerStorageInfo,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.RelRangeVar:\n    \"\"\"Return a Table corresponding to a given Link.\"\"\"\n\n    aspect = 'table'\n    table_schema_name, table_name = common.update_aspect(\n        ptr_info.table_name, aspect\n    )\n\n    typeref = ptrref.out_source if ptrref else None\n    relation = pgast.Relation(\n        schemaname=table_schema_name,\n        name=table_name,\n        type_or_ptr_ref=ptrref,\n    )\n\n    # Pseudo pointers (tuple and type intersection) have no schema id.\n    sobj_id = ptrref.id if isinstance(ptrref, irast.PointerRef) else None\n    rvar = pgast.RelRangeVar(\n        schema_object_id=sobj_id,\n        typeref=typeref,\n        relation=relation,\n        alias=pgast.Alias(\n            aliasname=ctx.env.aliases.get(ptrref.shortname.name)\n        )\n    )\n\n    return rvar\n\n\ndef _get_ptrref_storage_info(\n    ptrref: irast.PointerRef,\n    *,\n    ctx: context.CompilerContextLevel\n) -> pg_types.PointerStorageInfo:\n    # Most references to inline links are dispatched to a separate\n    # code path (_new_inline_pointer_rvar) by new_pointer_rvar,\n    # but when we have union pointers, some might be inline.  We\n    # always use the link table if it exists (because this range\n    # needs to contain any link properties, for one reason.)\n    ptr_info = pg_types.get_ptrref_storage_info(\n        ptrref, resolve_type=False, link_bias=True,\n        versioned=ctx.env.versioned_stdlib,\n    )\n    if not ptr_info:\n        ptr_info = pg_types.get_ptrref_storage_info(\n            ptrref, resolve_type=False, link_bias=False,\n            versioned=ctx.env.versioned_stdlib,\n        )\n    return ptr_info\n\n\ndef _get_ptrref_column_names(\n    ptr_info: pg_types.PointerStorageInfo\n) -> list[str]:\n    return [\n        'source' if ptr_info.table_type == 'link' else 'id',\n        ptr_info.column_name,\n    ]\n\n\ndef range_for_pointer(\n    ir_set: irast.SetE[irast.Pointer],\n    *,\n    dml_source: Sequence[irast.MutatingLikeStmt]=(),\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n    pointer = ir_set.expr\n\n    path_id = ir_set.path_id.ptr_path()\n    external_rvar = ctx.env.external_rvars.get(\n        (path_id, pgce.PathAspect.SOURCE)\n    )\n    if external_rvar is not None:\n        return external_rvar\n\n    ptrref = pointer.ptrref\n    if ptrref.material_ptr is not None:\n        ptrref = ptrref.material_ptr\n\n    return range_for_ptrref(\n        ptrref, dml_source=dml_source, path_id=path_id, ctx=ctx)\n\n\ndef rvar_for_rel(\n    rel: pgast.BaseRelation | pgast.CommonTableExpr,\n    *,\n    alias: Optional[str] = None,\n    typeref: Optional[irast.TypeRef] = None,\n    lateral: bool = False,\n    colnames: Optional[list[str]] = None,\n    ctx: Optional[context.CompilerContextLevel] = None,\n    env: Optional[context.Environment] = None,\n) -> pgast.PathRangeVar:\n    if ctx:\n        env = ctx.env\n    assert env\n\n    rvar: pgast.PathRangeVar\n\n    if colnames is None:\n        colnames = []\n\n    if isinstance(rel, pgast.Query):\n        alias = alias or env.aliases.get(rel.name or 'q')\n\n        rvar = pgast.RangeSubselect(\n            subquery=rel,\n            alias=pgast.Alias(aliasname=alias, colnames=colnames),\n            lateral=lateral,\n            typeref=typeref,\n        )\n    else:\n        alias = alias or env.aliases.get(rel.name or '')\n\n        rvar = pgast.RelRangeVar(\n            relation=rel,\n            alias=pgast.Alias(aliasname=alias, colnames=colnames),\n            typeref=typeref,\n        )\n\n    return rvar\n\n\ndef _add_type_rel_overlay(\n    typeid: uuid.UUID,\n    op: context.OverlayOp,\n    rel: pgast.BaseRelation | pgast.CommonTableExpr, *,\n    dml_stmts: Iterable[irast.MutatingLikeStmt] = (),\n    path_id: irast.PathId,\n    ctx: context.CompilerContextLevel\n) -> None:\n    entry = (op, rel, path_id)\n    dml_stmts2 = dml_stmts if dml_stmts else (None,)\n    # If there is a \"global\" overlay, and there is none for the\n    # current statements, use it as the base. This is important for\n    # not losing track of the global environment in triggers.\n    root = ctx.rel_overlays.type.get(None, immu.Map())\n    for dml_stmt in dml_stmts2:\n        ds_overlays = ctx.rel_overlays.type.get(dml_stmt, root)\n        overlays = ds_overlays.get(typeid, ())\n        if entry not in overlays:\n            ds_overlays = ds_overlays.set(typeid, overlays + (entry,))\n            ctx.rel_overlays.type = (\n                ctx.rel_overlays.type.set(dml_stmt, ds_overlays))\n\n\ndef add_type_rel_overlay(\n    typeref: irast.TypeRef,\n    op: context.OverlayOp,\n    rel: pgast.BaseRelation | pgast.CommonTableExpr, *,\n    stop_ref: Optional[irast.TypeRef]=None,\n    dml_stmts: Iterable[irast.MutatingLikeStmt] = (),\n    path_id: irast.PathId,\n    ctx: context.CompilerContextLevel\n) -> None:\n    typeref = typeref.real_material_type\n    objs = [typeref]\n    if typeref.ancestors:\n        objs.extend(typeref.ancestors)\n\n    for obj in objs:\n        if stop_ref and (\n            obj == stop_ref or\n            (stop_ref.ancestors and obj in stop_ref.ancestors)\n        ):\n            continue\n        _add_type_rel_overlay(\n            obj.id, op, rel,\n            dml_stmts=dml_stmts, path_id=path_id, ctx=ctx)\n\n\ndef get_type_rel_overlays(\n    typeref: irast.TypeRef,\n    *,\n    dml_source: Sequence[irast.MutatingLikeStmt]=(),\n    ctx: context.CompilerContextLevel,\n) -> tuple[context.OverlayEntry, ...]:\n    if typeref.material_type is not None:\n        typeref = typeref.material_type\n\n    xdml_source = dml_source or (None,)\n    return tuple(\n        entry\n        for src in xdml_source\n        if src in ctx.rel_overlays.type\n        for entry in ctx.rel_overlays.type[src].get(typeref.id, ())\n    )\n\n\ndef reuse_type_rel_overlays(\n    *,\n    dml_stmts: Iterable[irast.MutatingLikeStmt] = (),\n    dml_source: irast.MutatingLikeStmt,\n    ctx: context.CompilerContextLevel,\n) -> None:\n    \"\"\"Update type rel overlays when a DML statement is reused.\n\n    When a WITH bound DML is used, we need to add it (and all of its\n    nested overlays) as an overlay for all the enclosing DML\n    statements.\n    \"\"\"\n    ref_overlays = ctx.rel_overlays.type.get(dml_source, immu.Map())\n    for tid, overlays in ref_overlays.items():\n        for op, rel, path_id in overlays:\n            _add_type_rel_overlay(\n                tid, op, rel, dml_stmts=dml_stmts, path_id=path_id, ctx=ctx\n            )\n    ptr_overlays = ctx.rel_overlays.ptr.get(dml_source, immu.Map())\n    for (obj, ptr), poverlays in ptr_overlays.items():\n        for op, rel, path_id in poverlays:\n            _add_ptr_rel_overlay(\n                obj, ptr, op, rel, path_id=path_id, dml_stmts=dml_stmts,\n                ctx=ctx\n            )\n\n\ndef _add_ptr_rel_overlay(\n    typeid: uuid.UUID,\n    ptrref_name: str,\n    op: context.OverlayOp,\n    rel: pgast.BaseRelation | pgast.CommonTableExpr, *,\n    dml_stmts: Iterable[irast.MutatingLikeStmt] = (),\n    path_id: irast.PathId,\n    ctx: context.CompilerContextLevel\n) -> None:\n\n    entry = (op, rel, path_id)\n    dml_stmts2 = dml_stmts if dml_stmts else (None,)\n    key = typeid, ptrref_name\n    # If there is a \"global\" overlay, and there is none for the\n    # current statements, use it as the base. This is important for\n    # not losing track of the global environment in triggers.\n    root = ctx.rel_overlays.ptr.get(None, immu.Map())\n    for dml_stmt in dml_stmts2:\n        ds_overlays = ctx.rel_overlays.ptr.get(dml_stmt, root)\n        overlays = ds_overlays.get(key, ())\n        if entry not in overlays:\n            ds_overlays = ds_overlays.set(key, overlays + (entry,))\n            ctx.rel_overlays.ptr = (\n                ctx.rel_overlays.ptr.set(dml_stmt, ds_overlays))\n\n\ndef add_ptr_rel_overlay(\n    ptrref: irast.PointerRef,\n    op: context.OverlayOp,\n    rel: pgast.BaseRelation | pgast.CommonTableExpr, *,\n    dml_stmts: Iterable[irast.MutatingLikeStmt] = (),\n    path_id: irast.PathId,\n    ctx: context.CompilerContextLevel\n) -> None:\n\n    typeref = ptrref.out_source.real_material_type\n    objs = [typeref]\n    if typeref.ancestors:\n        objs.extend(typeref.ancestors)\n\n    for obj in objs:\n        _add_ptr_rel_overlay(\n            obj.id, ptrref.shortname.name, op, rel, path_id=path_id,\n            dml_stmts=dml_stmts,\n            ctx=ctx)\n\n\ndef get_ptr_rel_overlays(\n    ptrref: irast.PointerRef, *,\n    dml_source: Sequence[irast.MutatingLikeStmt]=(),\n    ctx: context.CompilerContextLevel,\n) -> tuple[context.OverlayEntry, ...]:\n    typeref = ptrref.out_source.real_material_type\n    key = typeref.id, ptrref.shortname.name\n    xdml_source = dml_source or (None,)\n    return tuple(\n        entry\n        for src in xdml_source\n        if src in ctx.rel_overlays.ptr\n        for entry in ctx.rel_overlays.ptr[src].get(key, ())\n    )\n"
  },
  {
    "path": "edb/pgsql/compiler/relgen.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Compiler functions to generate SQL relations for IR sets.\"\"\"\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Callable,\n    Optional,\n    Protocol,\n    Iterable,\n    Collection,\n    NamedTuple,\n    cast,\n)\n\nimport dataclasses\nimport contextlib\nimport functools\n\nfrom edb import errors\n\nfrom edb.edgeql import qltypes\n\nfrom edb.schema import objects as s_obj\nfrom edb.schema import name as sn\n\nfrom edb.edgeql import ast as qlast\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtyputils\nfrom edb.ir import utils as irutils\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import common\nfrom edb.pgsql import types as pg_types\n\nfrom edb.common.typeutils import not_none\n\nfrom . import astutils\nfrom . import clauses\nfrom . import context\nfrom . import dispatch\nfrom . import dml\nfrom . import enums as pgce\nfrom . import expr as exprcomp\nfrom . import output\nfrom . import pathctx\nfrom . import relctx\n\n\n@dataclasses.dataclass(repr=False, eq=False)\nclass SetRVar:\n    rvar: pgast.PathRangeVar\n    path_id: irast.PathId\n    aspects: Iterable[pgce.PathAspect] = dataclasses.field(\n        default=(pgce.PathAspect.VALUE,)\n    )\n\n\n@dataclasses.dataclass(kw_only=True, repr=False, eq=False)\nclass SetRVars:\n    main: SetRVar\n    new: list[SetRVar]\n\n\ndef new_simple_set_rvar(\n    ir_set: irast.Set,\n    rvar: pgast.PathRangeVar,\n    aspects: Iterable[pgce.PathAspect],\n) -> SetRVars:\n    srvar = SetRVar(rvar=rvar, path_id=ir_set.path_id, aspects=aspects)\n    return SetRVars(main=srvar, new=[srvar])\n\n\ndef new_source_set_rvar(\n    ir_set: irast.Set,\n    rvar: pgast.PathRangeVar,\n) -> SetRVars:\n    aspects = [pgce.PathAspect.VALUE]\n    if ir_set.path_id.is_objtype_path():\n        aspects.append(pgce.PathAspect.SOURCE)\n\n    return new_simple_set_rvar(ir_set, rvar, aspects)\n\n\ndef new_stmt_set_rvar(\n    ir_set: irast.Set,\n    stmt: pgast.Query,\n    *,\n    aspects: Optional[Iterable[pgce.PathAspect]]=None,\n    ctx: context.CompilerContextLevel,\n) -> SetRVars:\n    rvar = relctx.new_rel_rvar(ir_set, stmt, ctx=ctx)\n    if aspects is not None:\n        aspects = tuple(aspects)\n    else:\n        aspects = pathctx.list_path_aspects(stmt, ir_set.path_id)\n    return new_simple_set_rvar(ir_set, rvar, aspects=aspects)\n\n\nclass OptionalRel(NamedTuple):\n\n    scope_rel: pgast.SelectStmt\n    target_rel: pgast.SelectStmt\n    emptyrel: pgast.SelectStmt\n    unionrel: pgast.SelectStmt\n    wrapper: pgast.SelectStmt\n    container: pgast.SelectStmt\n    marker: str\n\n\ndef _lookup_set_rvar(\n        ir_set: irast.Set, *,\n        scope_stmt: Optional[pgast.SelectStmt]=None,\n        ctx: context.CompilerContextLevel) -> Optional[pgast.PathRangeVar]:\n    path_id = ir_set.path_id\n\n    rvar = relctx.find_rvar(ctx.rel, source_stmt=scope_stmt,\n                            path_id=path_id, ctx=ctx)\n\n    if rvar is not None:\n        return rvar\n\n    # We couldn't find a regular rvar, but maybe we can find a packed one?\n    packed_rvar = relctx.find_rvar(ctx.rel, flavor='packed',\n                                   source_stmt=scope_stmt,\n                                   path_id=path_id, ctx=ctx)\n\n    if packed_rvar is not None:\n        rvar = relctx.unpack_rvar(\n            scope_stmt or ctx.rel,\n            path_id, packed_rvar=packed_rvar, ctx=ctx)\n\n        return rvar\n\n    return None\n\n\ndef get_set_rvar(\n        ir_set: irast.Set, *,\n        ctx: context.CompilerContextLevel) -> pgast.PathRangeVar:\n    \"\"\"Return a PathRangeVar for a given IR Set.\n\n    Basically all of compilation comes through here for each set.\n\n    @param ir_set: IR Set node.\n    \"\"\"\n    path_id = ir_set.path_id\n\n    scope_stmt = relctx.maybe_get_scope_stmt(path_id, ctx=ctx)\n    if rvar := _lookup_set_rvar(ir_set, scope_stmt=scope_stmt, ctx=ctx):\n        return rvar\n\n    if ctx.toplevel_stmt is context.NO_STMT:\n        # Top level query\n        return _process_toplevel_query(ir_set, ctx=ctx)\n\n    with contextlib.ExitStack() as cstack:\n\n        # If there was a scope_stmt registered for our path, we compile\n        # as a subrel of that scope_stmt. Otherwise we use whatever the\n        # current rel was.\n        if scope_stmt is not None:\n            newctx = cstack.enter_context(ctx.new())\n            newctx.rel = scope_stmt\n        else:\n            newctx = ctx\n            scope_stmt = newctx.rel\n\n        subctx = cstack.enter_context(newctx.subrel())\n        # *stmt* here is a tentative container for the relation generated\n        # by processing the *ir_set*.  However, the actual compilation\n        # is free to return something else instead of a range var over\n        # stmt.\n        stmt = subctx.rel\n        stmt.name = ctx.env.aliases.get(get_set_rel_alias(ir_set, ctx=ctx))\n\n        # If ir.Set compilation needs to produce a subquery,\n        # make sure it uses the current subrel.  This makes it\n        # possible to set up the path scope here and don't worry\n        # about it later.\n        subctx.pending_query = stmt\n\n        is_empty_set = isinstance(ir_set.expr, irast.EmptySet)\n\n        path_scope = relctx.get_scope(ir_set, ctx=subctx)\n        new_scope = path_scope or subctx.scope_tree\n        is_optional = (\n            subctx.scope_tree.is_optional(path_id) or\n            new_scope.is_optional(path_id) or\n            path_id in subctx.force_optional\n        ) and not can_omit_optional_wrapper(ir_set, new_scope, ctx=ctx)\n\n        optional_wrapping = is_optional and not is_empty_set\n\n        if optional_wrapping:\n            stmt, optrel = prepare_optional_rel(\n                ir_set=ir_set, stmt=stmt, ctx=subctx)\n            subctx.pending_query = subctx.rel = stmt\n\n        # XXX: This is pretty dodgy, because it updates the path_scope\n        # *before* we call new_child() on it. Removing it only breaks two\n        # tests of lprops on backlinks.\n        if path_scope and path_scope.is_visible(path_id):\n            subctx.path_scope[path_id] = scope_stmt\n\n        # If this set has a scope in the scope tree associated with it,\n        # register paths in that scope to be compiled with this stmt\n        # as their scope_stmt.\n        if path_scope:\n            relctx.update_scope(ir_set, stmt, ctx=subctx)\n\n        # Actually compile the set\n        rvars = _get_expr_set_rvar(ir_set.expr, ir_set, ctx=subctx)\n        relctx.update_scope_masks(ir_set, rvars.main.rvar, ctx=subctx)\n\n        if ctx.env.is_explain:\n            for srvar in rvars.new:\n                if not srvar.rvar.ir_origins:\n                    srvar.rvar.ir_origins = []\n                srvar.rvar.ir_origins.append(ir_set)\n\n        if optional_wrapping:\n            rvars = finalize_optional_rel(ir_set, optrel=optrel,\n                                          rvars=rvars, ctx=subctx)\n            relctx.update_scope_masks(ir_set, rvars.main.rvar, ctx=subctx)\n        elif not is_optional and is_empty_set:\n            # In most cases it is totally fine for us to represent an\n            # empty set as an empty relation.\n            # (except when it needs to be fed to an optional argument)\n            null_query = rvars.main.rvar.query\n            assert isinstance(\n                null_query, (pgast.SelectStmt, pgast.NullRelation))\n            null_query.where_clause = pgast.BooleanConstant(val=False)\n\n        result_rvar = _include_rvars(rvars, scope_stmt=scope_stmt, ctx=subctx)\n        for aspect in rvars.main.aspects:\n            pathctx.put_path_rvar_if_not_exists(\n                ctx.rel,\n                path_id,\n                result_rvar,\n                aspect=aspect,\n            )\n\n    return result_rvar\n\n\ndef _include_rvars(\n    rvars: SetRVars,\n    *,\n    scope_stmt: pgast.SelectStmt,\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n    for set_rvar in rvars.new:\n        # overwrite_path_rvar is needed because we want\n        # the outermost Set with the given path_id to\n        # represent the path.  Nested Sets with the\n        # same path_id but different expression are\n        # possible when there is a computable pointer\n        # that refers to itself in its expression.\n        relctx.include_specific_rvar(\n            scope_stmt,\n            set_rvar.rvar,\n            path_id=set_rvar.path_id,\n            overwrite_path_rvar=True,\n            aspects=set_rvar.aspects,\n            ctx=ctx,\n        )\n\n    return rvars.main.rvar\n\n\ndef _process_toplevel_query(\n    ir_set: irast.Set,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n    # TODO: Can we get rid of the need for this special handling of\n    # the toplevel? What is it good for anyway?\n    # I think it might just be suppressing what would be one extra\n    # level of select wrapping?\n\n    relctx.init_toplevel_query(ir_set, ctx=ctx)\n    rvars = _get_expr_set_rvar(ir_set.expr, ir_set, ctx=ctx)\n    result_rvar = rvars.main.rvar\n    # Usually the result_rvar is wrapping ctx.rel, which is the final\n    # top-level query. (And thus the result_rvar is actually bogus and\n    # will never be used!) But if not, we need to include it, or we'll\n    # have an empty query.\n    if result_rvar.query is not ctx.rel:\n        _include_rvars(rvars, scope_stmt=ctx.rel, ctx=ctx)\n\n    return result_rvar\n\n\nclass _SpecialCaseFunc(Protocol):\n    def __call__(\n        self, ir_set: irast.SetE[irast.Call],\n        *,\n        ctx: context.CompilerContextLevel,\n    ) -> SetRVars:\n        pass\n\n\nclass _FunctionSpecialCase(NamedTuple):\n    func: _SpecialCaseFunc\n    only_as_fallback: bool\n\n\n_SPECIAL_FUNCTIONS: dict[str, _FunctionSpecialCase] = {}\n\n\ndef _special_case(name: str, only_as_fallback: bool = False) -> Callable[\n    [_SpecialCaseFunc], _SpecialCaseFunc\n]:\n    def func(f: _SpecialCaseFunc) -> _SpecialCaseFunc:\n        _SPECIAL_FUNCTIONS[name] = _FunctionSpecialCase(f, only_as_fallback)\n        return f\n\n    return func\n\n\nclass _SimpleSpecialCaseFunc(Protocol):\n    def __call__(\n        self, expr: irast.FunctionCall, *, ctx: context.CompilerContextLevel\n    ) -> pgast.BaseExpr:\n        pass\n\n\n_SIMPLE_SPECIAL_FUNCTIONS: dict[str, _SimpleSpecialCaseFunc] = {}\n\n\ndef simple_special_case(\n    name: str,\n) -> Callable[[_SimpleSpecialCaseFunc], _SimpleSpecialCaseFunc]:\n    def func(f: _SimpleSpecialCaseFunc) -> _SimpleSpecialCaseFunc:\n        _SIMPLE_SPECIAL_FUNCTIONS[name] = f\n        return f\n\n    return func\n\n\n# Dispatcher for _get_set_rvar implementations for different expressions.\n# The implementations just take a SetE[T] for some T, so register_get_rvar\n# needs to do some wrapping.\n@functools.singledispatch\ndef _get_expr_set_rvar(\n    expr: irast.Expr,\n    ir: irast.Set,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> SetRVars:\n    raise NotImplementedError(f'no relgen handler for {ir.__class__}')\n\n\nclass _GetExprRvarFunc[T_expr: irast.Expr](Protocol):  # noqa: UP046\n    def __call__(\n        self, __ir_set: irast.SetE[T_expr], *, ctx: context.CompilerContextLevel\n    ) -> SetRVars:\n        pass\n\n\ndef register_get_rvar[T_expr: irast.Expr](\n    typ: type[T_expr],\n) -> Callable[[_GetExprRvarFunc[T_expr]], _GetExprRvarFunc[T_expr]]:\n    def func(f: _GetExprRvarFunc[T_expr]) -> _GetExprRvarFunc[T_expr]:\n        _get_expr_set_rvar.register(typ)(\n            lambda _, ir, *, ctx: f(ir, ctx=ctx))\n        return f\n\n    return func\n\n\ndef _get_source_rvar(\n    ir_set: irast.Set,\n    scope_stmt: pgast.SelectStmt,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n    is_optional = (\n        ctx.scope_tree.is_optional(ir_set.path_id) or\n        ir_set.path_id in ctx.force_optional\n    )\n\n    if not is_optional:\n        rvar = relctx.new_root_rvar(ir_set, lateral=True, ctx=ctx)\n        relctx.include_rvar(\n            scope_stmt, rvar, path_id=ir_set.path_id, ctx=ctx\n        )\n    else:\n        # If the path is optional in the context we are in, then we\n        # need to put optional wrapping around the join with the base table.\n        with ctx.subrel() as subctx:\n            stmt, optrel = prepare_optional_rel(\n                ir_set=ir_set, stmt=subctx.rel, ctx=subctx)\n            subctx.pending_query = subctx.rel = stmt\n\n            rvar = relctx.new_root_rvar(ir_set, lateral=True, ctx=subctx)\n            rvars = new_source_set_rvar(ir_set, rvar)\n            rvars = finalize_optional_rel(\n                ir_set, optrel=optrel, rvars=rvars, ctx=subctx)\n\n            rvar = _include_rvars(rvars, scope_stmt=scope_stmt, ctx=ctx)\n\n    return rvar\n\n\ndef ensure_source_rvar(\n    ir_set: irast.Set,\n    stmt: pgast.Query,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.PathRangeVar:\n    \"\"\"Make sure that a source aspect is available for ir_set.\n\n    If no aspect is available, compile it. If value/identity is available\n    but source is not, select from the base relation and join it in.\n    \"\"\"\n\n    rvar = relctx.maybe_get_path_rvar(\n        stmt, ir_set.path_id, aspect=pgce.PathAspect.SOURCE, ctx=ctx)\n    if rvar is None:\n        get_set_rvar(ir_set, ctx=ctx)\n\n    rvar = relctx.maybe_get_path_rvar(\n        stmt, ir_set.path_id, aspect=pgce.PathAspect.SOURCE, ctx=ctx)\n    if rvar is None:\n        scope_stmt = relctx.maybe_get_scope_stmt(ir_set.path_id, ctx=ctx)\n        if scope_stmt is None:\n            scope_stmt = ctx.rel\n        rvar = relctx.maybe_get_path_rvar(\n            scope_stmt,\n            ir_set.path_id,\n            aspect=pgce.PathAspect.SOURCE,\n            ctx=ctx,\n        )\n        if rvar is None:\n            if irtyputils.is_free_object(ir_set.path_id.target):\n                # Free objects don't have a real source, and\n                # generating a new fake source doesn't work because\n                # the ids don't match, so instead we call the existing\n                # value rvar a source.\n                rvar = relctx.get_path_rvar(\n                    scope_stmt,\n                    ir_set.path_id,\n                    aspect=pgce.PathAspect.VALUE,\n                    ctx=ctx,\n                )\n            else:\n                rvar = _get_source_rvar(ir_set, scope_stmt, ctx=ctx)\n            pathctx.put_path_rvar(\n                stmt, ir_set.path_id, rvar, aspect=pgce.PathAspect.SOURCE,\n            )\n\n    return rvar\n\n\ndef set_as_subquery(\n        ir_set: irast.Set, *,\n        as_value: bool=False,\n        explicit_cast: Optional[tuple[str, ...]] = None,\n        ctx: context.CompilerContextLevel) -> pgast.Query:\n    # Compile *ir_set* into a subquery as follows:\n    #     (\n    #         SELECT <set_rel>.v\n    #         FROM <set_rel>\n    #     )\n    with ctx.subrel() as subctx:\n        wrapper = subctx.rel\n        wrapper.name = ctx.env.aliases.get('set_as_subquery')\n        dispatch.visit(ir_set, ctx=subctx)\n\n        if as_value:\n\n            if output.in_serialization_ctx(ctx):\n                pathctx.get_path_serialized_output(\n                    rel=wrapper, path_id=ir_set.path_id, env=ctx.env)\n            else:\n                pathctx.get_path_value_output(\n                    rel=wrapper, path_id=ir_set.path_id, env=ctx.env)\n\n                var = pathctx.get_path_value_var(\n                    rel=wrapper, path_id=ir_set.path_id, env=ctx.env)\n                value = output.output_as_value(var, env=ctx.env)\n\n                if explicit_cast is not None:\n                    value = pgast.TypeCast(\n                        arg=value,\n                        type_name=pgast.TypeName(name=explicit_cast),\n                    )\n\n                wrapper.target_list = [\n                    pgast.ResTarget(val=value)\n                ]\n        else:\n            pathctx.get_path_value_output(\n                rel=wrapper, path_id=ir_set.path_id, env=ctx.env)\n\n    return wrapper\n\n\ndef can_omit_optional_wrapper(\n        ir_set: irast.Set, new_scope: irast.ScopeTreeNode, *,\n        ctx: context.CompilerContextLevel) -> bool:\n    \"\"\"Determine whether it is safe to omit the optional wrapper.\n\n    Doing so is safe when the expression is guarenteed to result in\n    a NULL and not an empty set.\n\n    The main such case implemented is a path `foo.bar` where foo\n    is visible and bar is a single non-computed property, which we know\n    will be stored as NULL in the database.\n\n    We also handle trivial SELECTs wrapping such an expression.\n    \"\"\"\n    if ir_set.expr and irutils.is_trivial_select(ir_set.expr):\n        return can_omit_optional_wrapper(\n            ir_set.expr.result,\n            relctx.get_scope(ir_set.expr.result, ctx=ctx) or new_scope,\n            ctx=ctx,\n        )\n\n    if isinstance(ir_set.expr, irast.QueryParameter):\n        return True\n\n    # Our base json casts should all preserve nullity (instead of\n    # turning it into an empty set), so allow passing through those\n    # cases. This is mainly an optimization for passing globals to\n    # functions, where we need to convert a bunch of optional params\n    # to json, and for casting out of json there and in schema updates.\n    if (\n        isinstance(ir_set.expr, irast.TypeCast)\n        and ((\n            irtyputils.is_scalar(ir_set.expr.expr.typeref)\n            and irtyputils.is_json(ir_set.expr.to_type)\n        ) or (\n            irtyputils.is_json(ir_set.expr.expr.typeref)\n            and irtyputils.is_scalar(ir_set.expr.to_type)\n        ))\n    ):\n        return can_omit_optional_wrapper(\n            ir_set.expr.expr,\n            relctx.get_scope(ir_set.expr.expr, ctx=ctx) or new_scope,\n            ctx=ctx,\n        )\n\n    if isinstance(ir_set.expr, irast.TupleIndirectionPointer):\n        return can_omit_optional_wrapper(ir_set.expr.source, new_scope, ctx=ctx)\n\n    return bool(\n        isinstance(ir_set.expr, irast.Pointer)\n        and (rptr := ir_set.expr)\n        and rptr.expr is None\n        and not ir_set.path_id.is_objtype_path()\n        and not ir_set.path_id.is_type_intersection_path()\n        and new_scope.is_visible(rptr.source.path_id)\n        and not rptr.is_inbound\n        and rptr.ptrref.out_cardinality.is_single()\n        and not rptr.ptrref.is_computable\n    )\n\n\ndef prepare_optional_rel(\n        *, ir_set: irast.Set, stmt: pgast.SelectStmt,\n        ctx: context.CompilerContextLevel) \\\n        -> tuple[pgast.SelectStmt, OptionalRel]:\n\n    # For OPTIONAL sets we compute a UNION of both sides and annotate\n    # each side with a marker.  We then select only rows that match\n    # the marker of the first row:\n    #\n    #     SELECT\n    #         q.*\n    #     FROM\n    #         (SELECT\n    #             marker = first_value(marker) OVER () AS marker,\n    #             ...\n    #          FROM\n    #             (SELECT 1 AS marker, * FROM left\n    #              UNION ALL\n    #              SELECT 2 AS marker, * FROM right) AS u\n    #         ) AS q\n    #     WHERE marker\n\n    with ctx.new() as subctx:\n        subctx.rel = stmt\n\n        with subctx.subrel() as wrapctx:\n            wrapper = wrapctx.rel\n\n            with wrapctx.subrel() as unionctx:\n\n                with unionctx.subrel() as scopectx:\n                    scope_rel = scopectx.rel\n\n                    with scopectx.subrel() as targetctx:\n                        target_rel = targetctx.rel\n\n                with unionctx.subrel() as scopectx:\n                    emptyrel = scopectx.rel\n                    empty_ir = irast.Set(\n                        path_id=ir_set.path_id,\n                        typeref=ir_set.typeref,\n                        expr=irast.EmptySet(typeref=ir_set.typeref),\n                    )\n\n                    emptyrvar = relctx.new_empty_rvar(\n                        cast('irast.SetE[irast.EmptySet]', empty_ir),\n                        ctx=scopectx)\n\n                    relctx.include_rvar(\n                        emptyrel, emptyrvar, path_id=ir_set.path_id,\n                        ctx=scopectx)\n\n                marker = unionctx.env.aliases.get('m')\n\n                scope_rel.target_list.insert(\n                    0,\n                    pgast.ResTarget(val=pgast.NumericConstant(val='1'),\n                                    name=marker))\n                emptyrel.target_list.insert(\n                    0,\n                    pgast.ResTarget(val=pgast.NumericConstant(val='2'),\n                                    name=marker))\n\n                unionqry = unionctx.rel\n                unionqry.op = 'UNION'\n                unionqry.all = True\n                unionqry.larg = scope_rel\n                unionqry.rarg = emptyrel\n\n            lagged_marker = pgast.FuncCall(\n                name=('first_value',),\n                args=[pgast.ColumnRef(name=[marker])],\n                over=pgast.WindowDef()\n            )\n\n            marker_ok = astutils.new_binop(\n                pgast.ColumnRef(name=[marker]),\n                lagged_marker,\n                op='=',\n            )\n\n            wrapper.target_list.append(\n                pgast.ResTarget(\n                    name=marker,\n                    val=marker_ok\n                )\n            )\n\n    return (\n        target_rel,\n        OptionalRel(scope_rel=scope_rel, target_rel=target_rel,\n                    emptyrel=emptyrel, unionrel=unionqry,\n                    wrapper=wrapper, container=stmt, marker=marker)\n    )\n\n\ndef finalize_optional_rel(\n        ir_set: irast.Set, optrel: OptionalRel, rvars: SetRVars,\n        ctx: context.CompilerContextLevel) -> SetRVars:\n\n    with ctx.new() as subctx:\n        subctx.rel = setrel = optrel.scope_rel\n\n        for set_rvar in rvars.new:\n            relctx.include_specific_rvar(\n                setrel, set_rvar.rvar, path_id=set_rvar.path_id,\n                aspects=set_rvar.aspects, ctx=subctx)\n\n        for aspect in rvars.main.aspects:\n            pathctx.put_path_rvar_if_not_exists(\n                setrel, ir_set.path_id, rvars.main.rvar, aspect=aspect\n            )\n\n        lvar = pathctx.get_path_value_var(\n            setrel, path_id=ir_set.path_id, env=subctx.env)\n\n        if lvar.nullable:\n            # The left var is still nullable, which may be the\n            # case for non-required singleton properties.\n            # Filter out NULLs.\n            setrel.where_clause = astutils.extend_binop(\n                setrel.where_clause,\n                pgast.NullTest(\n                    arg=lvar, negated=True\n                )\n            )\n\n    unionrel = optrel.unionrel\n    union_rvar = relctx.rvar_for_rel(unionrel, lateral=True, ctx=ctx)\n\n    with ctx.new() as subctx:\n        subctx.rel = wrapper = optrel.wrapper\n        relctx.include_rvar(wrapper, union_rvar, ir_set.path_id, ctx=subctx)\n\n    with ctx.new() as subctx:\n        subctx.rel = stmt = optrel.container\n        wrapper_rvar = relctx.rvar_for_rel(wrapper, lateral=True, ctx=subctx)\n\n        relctx.include_rvar(stmt, wrapper_rvar, ir_set.path_id, ctx=subctx)\n\n        stmt.where_clause = astutils.extend_binop(\n            stmt.where_clause,\n            astutils.get_column(wrapper_rvar, optrel.marker, nullable=False))\n\n        stmt.nullable = True\n\n    sub_rvar = SetRVar(rvar=relctx.new_rel_rvar(ir_set, stmt, ctx=ctx),\n                       path_id=ir_set.path_id,\n                       aspects=rvars.main.aspects)\n\n    return SetRVars(main=sub_rvar, new=[sub_rvar])\n\n\ndef get_set_rel_alias(ir_set: irast.Set, *,\n                      ctx: context.CompilerContextLevel) -> str:\n    dname = ir_set.path_id.target_name_hint.name\n    if (\n        isinstance(ir_set.expr, irast.Pointer)\n        and ir_set.expr.source.typeref is not None\n    ):\n        alias_hint = '{}_{}'.format(\n            dname,\n            ir_set.expr.ptrref.shortname.name\n        )\n    else:\n        alias_hint = dname.replace('~', '-')\n\n    return alias_hint\n\n\n# N.B: registered for get_rvar for TypeRoot below\ndef process_set_as_root(\n    ir_set: irast.Set, *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n\n    # TODO(ir): Represent these as something other than TypeRoot?\n    if ir_set.path_id in ctx.external_rels:\n        return process_external_rel(ir_set, ctx=ctx)\n\n    assert not ir_set.is_visible_binding_ref, (\n        f\"Can't compile ref to visible binding root {ir_set.path_id}\"\n    )\n\n    rvar = relctx.new_root_rvar(ir_set, ctx=ctx)\n    return new_source_set_rvar(ir_set, rvar)\n\n\nregister_get_rvar(irast.TypeRoot)(process_set_as_root)\n\n\n@register_get_rvar(irast.VisibleBindingExpr)\ndef process_set_as_visible_binding(\n    ir_set: irast.SetE[irast.VisibleBindingExpr],\n    *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    raise AssertionError(\n        f\"Can't compile ref to visible binding {ir_set.path_id}\"\n    )\n\n\n@register_get_rvar(irast.InlinedParameterExpr)\ndef process_set_as_inlined_parameter(\n    ir_set: irast.SetE[irast.InlinedParameterExpr],\n    *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    raise AssertionError(\n        f\"Can't compile ref to inline parameter {ir_set.path_id}\"\n    )\n\n\n@register_get_rvar(irast.EmptySet)\ndef process_set_as_empty(\n    ir_set: irast.SetE[irast.EmptySet], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n\n    rvar = relctx.new_empty_rvar(ir_set, ctx=ctx)\n    return new_source_set_rvar(ir_set, rvar)\n\n\ndef process_external_rel(\n    ir_set: irast.Set, *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    rel, aspects = ctx.external_rels[ir_set.path_id]\n    for a in aspects:\n        assert isinstance(a, str)\n\n    rvar = relctx.rvar_for_rel(rel, ctx=ctx)\n    return new_simple_set_rvar(ir_set, rvar, aspects)\n\n\ndef process_set_as_link_property_ref(\n    ir_set: irast.SetE[irast.Pointer], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    rptr = ir_set.expr\n    ir_source = rptr.source\n    rvars = []\n\n    lpropref = rptr.ptrref\n    ptr_info = pg_types.get_ptrref_storage_info(\n        lpropref, resolve_type=False, link_bias=False)\n\n    if (ptr_info.table_type == 'ObjectType' or\n            str(lpropref.std_parent_name) == 'std::target'):\n        # This is a singleton link property stored in source rel,\n        # e.g. @target\n        src_rvar = get_set_rvar(ir_source, ctx=ctx)\n\n        val = pathctx.get_rvar_path_var(\n            src_rvar,\n            ir_source.path_id,\n            aspect=pgce.PathAspect.VALUE,\n            env=ctx.env,\n        )\n\n        pathctx.put_rvar_path_output(\n            src_rvar, ir_set.path_id, aspect=pgce.PathAspect.VALUE, var=val\n        )\n\n        return SetRVars(\n            main=SetRVar(rvar=src_rvar, path_id=ir_set.path_id), new=[])\n\n    with ctx.new() as newctx:\n        link_path_id = ir_set.path_id.src_path()\n        assert link_path_id is not None\n\n        rptr_specialization: Optional[set[irast.PointerRef]] = None\n\n        if link_path_id.is_type_intersection_path():\n            rptr_specialization = set()\n            link_prefix, ind_ptrs = (\n                irutils.collapse_type_intersection(ir_source))\n            for ind_ptr in ind_ptrs:\n                rptr_specialization.update(ind_ptr.ptrref.rptr_specialization)\n        else:\n            link_prefix = ir_source\n\n        source_scope_stmt = relctx.maybe_get_scope_stmt(\n            ir_source.path_id, ctx=ctx\n        ) or ctx.rel\n        link_rvar = pathctx.maybe_get_path_rvar(\n            source_scope_stmt, link_path_id, aspect=pgce.PathAspect.SOURCE\n        )\n\n        if link_rvar is None:\n            src_rvar = get_set_rvar(ir_source, ctx=newctx)\n            assert irutils.is_set_instance(link_prefix, irast.Pointer), (\n                f'projecting lprop on {link_prefix.expr}')\n            link_rvar = relctx.new_pointer_rvar(\n                link_prefix, src_rvar=src_rvar,\n                link_bias=True, ctx=newctx)\n            # Make sure the link rvar understands the path_id we are using.\n            # (FIXME: Would it be better to pass this in to new_pointer_rvar?)\n            pathctx.put_path_bond(link_rvar.query, link_path_id.tgt_path())\n            var = pathctx.get_rvar_path_identity_var(\n                link_rvar, link_prefix.path_id, env=ctx.env)\n            pathctx.put_rvar_path_output(\n                link_rvar,\n                link_path_id.tgt_path(),\n                pgce.PathAspect.IDENTITY,\n                var,\n            )\n\n        if astutils.is_set_op_query(link_rvar.query):\n            # If we have an rptr_specialization, then this is a link\n            # property reference to a link union narrowed by a type\n            # intersection.  We already know which union components\n            # match the indirection expression, and can route the link\n            # property references to correct UNION subqueries.\n            ptr_ids = (\n                {spec.id for spec in rptr_specialization}\n                if rptr_specialization is not None else None\n            )\n            if ptr_ids and rptr_specialization:\n                ptr_ids.update(\n                    x.id for spec in rptr_specialization\n                    for x in spec.descendants()\n                    if isinstance(x, irast.PointerRef)\n                )\n\n            for subquery in astutils.each_query_in_set(link_rvar.query):\n                if isinstance(subquery, pgast.SelectStmt):\n                    rvar = subquery.from_clause[0]\n                    assert isinstance(rvar, pgast.PathRangeVar)\n                    if ptr_ids is None or rvar.schema_object_id in ptr_ids:\n                        pathctx.put_path_source_rvar(\n                            subquery, link_path_id, rvar\n                        )\n                        continue\n                # Spare get_path_var() from attempting to rebalance\n                # the UNION by recording an explicit NULL as as the\n                # link property var.\n                pathctx.put_path_value_var(\n                    subquery,\n                    ir_set.path_id,\n                    pgast.TypeCast(\n                        arg=pgast.NullConstant(),\n                        type_name=pgast.TypeName(\n                            name=pg_types.pg_type_from_ir_typeref(\n                                ir_set.typeref),\n                        ),\n                    ),\n                )\n        elif isinstance(link_rvar.query, pgast.SelectStmt):\n            # When processing link properties into a CTE, map the current link\n            # path id into the form used by the CTE.\n\n            for from_rvar in link_rvar.query.from_clause:\n                if (\n                    isinstance(from_rvar, pgast.RelRangeVar)\n                    and isinstance(from_rvar.relation, pgast.CommonTableExpr)\n                    and from_rvar.relation.query.path_id is not None\n                ):\n                    pathctx.put_path_id_map(\n                        link_rvar.query,\n                        link_path_id,\n                        from_rvar.relation.query.path_id\n                    )\n\n        rvars.append(SetRVar(\n            link_rvar,\n            link_path_id,\n            aspects=[pgce.PathAspect.VALUE, pgce.PathAspect.SOURCE],\n        ))\n\n    return SetRVars(main=SetRVar(link_rvar, ir_set.path_id), new=rvars)\n\n\n@register_get_rvar(irast.TypeIntersectionPointer)\ndef process_set_as_path_type_intersection(\n    ir_set: irast.SetE[irast.TypeIntersectionPointer],\n    *,\n    ctx: context.CompilerContextLevel,\n) -> SetRVars:\n    rptr = ir_set.expr\n    ir_source = rptr.source\n    source_is_visible = ctx.scope_tree.is_visible(ir_source.path_id)\n    stmt = ctx.rel\n\n    assert not rptr.expr, 'type intersection pointer with expr??'\n\n    if irtyputils.is_empty_typeref(ir_set.typeref):\n        # If the typeref was a type expression which resolves to no actual\n        # types, just return an empty set.\n        empty_ir = irast.Set(\n            path_id=ir_set.path_id,\n            typeref=ir_set.typeref,\n            expr=irast.EmptySet(typeref=ir_set.typeref),\n        )\n        source_rvar = relctx.new_empty_rvar(\n            cast('irast.SetE[irast.EmptySet]', empty_ir),\n            ctx=ctx)\n        relctx.include_rvar(stmt, source_rvar, ir_set.path_id, ctx=ctx)\n\n    elif (not source_is_visible\n            and isinstance(ir_source.expr, irast.Pointer)\n            and not ir_source.path_id.is_type_intersection_path()\n            and not ir_source.expr.expr\n            and (\n                rptr.ptrref.is_subtype\n                or pg_types.get_ptrref_storage_info(\n                    ir_source.expr.ptrref).table_type != 'ObjectType'\n            )):\n        # Otherwise, if the source link path is not visible,\n        # and this is a subtype intersection, or the pointer is not inline,\n        # we have an opportunity to opmimize the target join by\n        # directly replacing the target type.\n        with ctx.new() as subctx:\n            subctx.intersection_narrowing = (\n                subctx.intersection_narrowing.copy())\n            subctx.intersection_narrowing[ir_source] = ir_set\n            source_rvar = get_set_rvar(ir_source, ctx=subctx)\n\n        pathctx.put_path_id_map(stmt, ir_set.path_id, ir_source.path_id)\n        relctx.include_rvar(stmt, source_rvar, ir_set.path_id, ctx=ctx)\n\n    else:\n        source_rvar = get_set_rvar(ir_source, ctx=ctx)\n\n        poly_rvar = relctx.range_for_typeref(\n            rptr.ptrref.out_target,\n            path_id=ir_set.path_id,\n            dml_source=irutils.get_dml_sources(ir_set, ctx.env.binding_dml),\n            lateral=True,\n            ctx=ctx,\n        )\n\n        prefix_path_id = ir_set.path_id.src_path()\n        assert prefix_path_id is not None, 'expected a path'\n\n        relctx.deep_copy_primitive_rvar_path_var(\n            ir_set.path_id, prefix_path_id, poly_rvar, env=ctx.env)\n        pathctx.put_rvar_path_bond(poly_rvar, prefix_path_id)\n        relctx.include_rvar(stmt, poly_rvar, ir_set.path_id, ctx=ctx)\n        int_rvar = pgast.IntersectionRangeVar(\n            component_rvars=[\n                source_rvar,\n                poly_rvar,\n            ]\n        )\n\n        if isinstance(source_rvar.query, pgast.Query):\n            pathctx.put_path_id_map(\n                source_rvar.query, ir_set.path_id, ir_source.path_id)\n\n        for aspect in (pgce.PathAspect.SOURCE, pgce.PathAspect.VALUE):\n            pathctx.put_path_rvar(\n                stmt,\n                ir_source.path_id,\n                source_rvar,\n                aspect=aspect,\n            )\n\n            pathctx.put_path_rvar(\n                stmt,\n                ir_set.path_id,\n                int_rvar,\n                aspect=aspect,\n            )\n\n    rvars = new_stmt_set_rvar(\n        ir_set,\n        stmt,\n        aspects=[pgce.PathAspect.VALUE, pgce.PathAspect.SOURCE],\n        ctx=ctx,\n    )\n    # If the inner set also exposes a pointer path source, we need to\n    # also expose a pointer path source. See tests like\n    # test_edgeql_for_lprop_02, where it is needed to to make FOR binding\n    # of backlinks work.\n    if pathctx.maybe_get_path_rvar(\n        stmt,\n        ir_source.path_id.ptr_path(),\n        aspect=pgce.PathAspect.SOURCE,\n    ):\n        rvars.new.append(\n            SetRVar(\n                rvars.main.rvar,\n                ir_set.path_id.ptr_path(),\n                aspects=(pgce.PathAspect.SOURCE,),\n            )\n        )\n\n    return rvars\n\n\ndef _source_path_needs_semi_join(\n        ir_source: irast.Set,\n        ctx: context.CompilerContextLevel) -> bool:\n    \"\"\"Check if the path might need a semi-join\n\n    It does not need one if it has a visible prefix followed by single\n    pointers. Otherwise it might.\n\n    This is an optimization that allows us to avoid doing a semi-join\n    when there is a chain of single links referenced (probably in a filter\n    or a computable).\n\n    \"\"\"\n    if ctx.scope_tree.is_visible(ir_source.path_id):\n        return False\n\n    while (\n        isinstance(ir_source.expr, irast.Pointer)\n        and ir_source.expr.dir_cardinality.is_single()\n        and not ir_source.expr.expr\n    ):\n        ir_source = ir_source.expr.source\n\n        if ctx.scope_tree.is_visible(ir_source.path_id):\n            return False\n\n    return True\n\n\n@register_get_rvar(irast.Pointer)\ndef process_set_as_path(\n    ir_set: irast.SetE[irast.Pointer],\n    *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    if ir_set.expr.expr:\n        return process_set_as_subquery(ir_set, ctx=ctx)\n\n    rptr = ir_set.expr\n    ptrref = rptr.ptrref\n    ir_source = rptr.source\n    stmt = ctx.rel\n\n    source_is_visible = ctx.scope_tree.is_visible(ir_source.path_id)\n    rvars = []\n\n    ptr_info = pg_types.get_ptrref_storage_info(\n        ptrref,\n        resolve_type=False,\n        link_bias=rptr.force_link_table,\n        allow_missing=True,\n    )\n\n    # Path is a link property.\n    is_linkprop = ptrref.source_ptr is not None\n    is_primitive_ref = not irtyputils.is_object(ptrref.out_target)\n    # Path is a reference to a relationship stored in the source table.\n    is_inline_ref = bool(ptr_info and ptr_info.table_type == 'ObjectType')\n    is_inline_primitive_ref = is_inline_ref and is_primitive_ref\n    is_id_ref_to_inline_source = False\n\n    semi_join = (\n        ir_set.path_id not in ctx.disable_semi_join and\n        not (is_linkprop or is_primitive_ref) and\n        _source_path_needs_semi_join(ir_source, ctx=ctx) and\n        # This is an optimization for when we are inside of a semi-join on\n        # a computable: process_set_as_subquery will have included an\n        # rvar for the computable source, and we want to join on it\n        # instead of semi-joining.\n        not relctx.find_rvar(stmt, path_id=ir_source.path_id, ctx=ctx)\n    )\n\n    if irtyputils.is_empty_typeref(ir_source.typeref):\n        # If the source is an empty type intersection, just produce an empty set\n\n        if is_primitive_ref:\n            aspects = [pgce.PathAspect.VALUE]\n        else:\n            aspects = [pgce.PathAspect.VALUE, pgce.PathAspect.SOURCE]\n\n        empty_ir = irast.Set(\n            path_id=ir_set.path_id,\n            typeref=ir_set.typeref,\n            expr=irast.EmptySet(typeref=ir_set.typeref),\n        )\n        empty_rvar = SetRVar(\n            relctx.new_empty_rvar(\n                cast('irast.SetE[irast.EmptySet]', empty_ir),\n                ctx=ctx\n            ),\n            path_id=ir_set.path_id,\n            aspects=aspects,\n        )\n        return SetRVars(main=empty_rvar, new=[empty_rvar])\n\n    main_rvar = None\n    source_rptr = (\n        ir_source.expr if isinstance(ir_source.expr, irast.Pointer) else None)\n    if (irtyputils.is_id_ptrref(ptrref) and source_rptr is not None\n            and isinstance(source_rptr.ptrref, irast.PointerRef)\n            and not source_rptr.is_inbound\n            and not irtyputils.is_computable_ptrref(source_rptr.ptrref)\n            and not irutils.is_type_intersection_reference(ir_set)\n            and not pathctx.link_needs_type_rewrite(\n                ir_source.typeref, env=ctx.env)):\n\n        src_src_is_visible = ctx.scope_tree.is_visible(\n            source_rptr.source.path_id)\n\n        # Record the ptrref visibility in a way that get_path_var\n        # can access, to properly apply the second part of this\n        # optimization.\n        ctx.env.ptrref_source_visibility[source_rptr.ptrref] = (\n            src_src_is_visible)\n\n        if src_src_is_visible:\n            # When there is a reference to the id property of\n            # an object which is linked to by a link stored\n            # inline, we want to route the reference to the\n            # inline attribute.  For example,\n            # Foo.__type__.id gets resolved to the Foo.__type__\n            # column.  However, this optimization must not be\n            # applied if the source is a type intersection, e.g\n            # __type__[IS Array].id, or if Foo is not visible in\n            # this scope.\n            source_ptr_info = pg_types.get_ptrref_storage_info(\n                source_rptr.ptrref, resolve_type=False, link_bias=False,\n                allow_missing=True)\n            is_id_ref_to_inline_source = bool(\n                source_ptr_info and source_ptr_info.table_type == 'ObjectType')\n\n    if semi_join:\n        with ctx.subrel() as srcctx:\n            srcctx.expr_exposed = False\n            src_rvar = get_set_rvar(ir_source, ctx=srcctx)\n            # semi_join needs a source rvar, so make sure we have one.\n            # (The returned one won't be a source rvar if it comes\n            # from a function, for example)\n            if not ir_source.path_id.is_type_intersection_path():\n                src_rvar = ensure_source_rvar(ir_source, stmt, ctx=srcctx)\n            set_rvar = relctx.semi_join(stmt, ir_set, src_rvar, ctx=srcctx)\n            rvars.append(SetRVar(\n                set_rvar,\n                ir_set.path_id,\n                [pgce.PathAspect.VALUE, pgce.PathAspect.SOURCE]\n            ))\n\n    elif is_id_ref_to_inline_source:\n        assert source_rptr is not None\n        ir_source = source_rptr.source\n        src_rvar = get_set_rvar(ir_source, ctx=ctx)\n\n    elif not source_is_visible:\n        with ctx.subrel() as srcctx:\n            srcctx.expr_exposed = False\n\n            get_set_rvar(ir_source, ctx=srcctx)\n\n            if is_inline_primitive_ref:\n                # Semi-join variant for inline scalar links,\n                # which is, essentially, just filtering out NULLs.\n                ensure_source_rvar(ir_source, srcctx.rel, ctx=srcctx)\n\n                var = pathctx.get_path_value_var(\n                    srcctx.rel, path_id=ir_set.path_id, env=ctx.env)\n                if var.nullable:\n                    srcctx.rel.where_clause = astutils.extend_binop(\n                        srcctx.rel.where_clause,\n                        pgast.NullTest(arg=var, negated=True))\n\n        srcrel = srcctx.rel\n        src_rvar = relctx.rvar_for_rel(srcrel, lateral=True, ctx=srcctx)\n        relctx.include_rvar(stmt, src_rvar, path_id=ir_source.path_id, ctx=ctx)\n        pathctx.put_path_id_mask(stmt, ir_source.path_id)\n\n    # Path is a reference to a link property.\n    if is_linkprop:\n        srvars = process_set_as_link_property_ref(ir_set, ctx=ctx)\n        main_rvar = srvars.main\n        rvars.extend(srvars.new)\n\n    elif is_id_ref_to_inline_source:\n        main_rvar = SetRVar(\n            ensure_source_rvar(ir_source, stmt, ctx=ctx),\n            path_id=ir_set.path_id,\n            aspects=[pgce.PathAspect.VALUE]\n        )\n\n    elif is_inline_primitive_ref:\n        # There is an opportunity to also expose the \"source\" aspect\n        # for tuple refs here, but that requires teaching pathctx about\n        # complex field indirections, so rely on tuple_getattr()\n        # fallback for tuple properties for now.\n        main_rvar = SetRVar(\n            ensure_source_rvar(ir_source, stmt, ctx=ctx),\n            path_id=ir_set.path_id,\n            aspects=[pgce.PathAspect.VALUE]\n        )\n        rvars = [main_rvar]\n\n    elif not semi_join:\n        # Link range.\n        if is_inline_ref:\n            aspects = [pgce.PathAspect.VALUE]\n            # If this is a link that is stored inline, make sure\n            # the source aspect is actually accessible (not just value).\n            src_rvar = ensure_source_rvar(ir_source, stmt, ctx=ctx)\n            # In case the source is visible (so the codepath below\n            # that uses the current statement doesn't trigger) but the\n            # source aspect wasn't available, make sure we include it\n            # in our return. This can come up with __old__ in triggers.\n            if source_is_visible:\n                rvars.append(SetRVar(\n                    src_rvar,\n                    path_id=ir_source.path_id,\n                    aspects=[pgce.PathAspect.SOURCE]\n                ))\n        else:\n            aspects = [pgce.PathAspect.VALUE, pgce.PathAspect.SOURCE]\n            src_rvar = get_set_rvar(ir_source, ctx=ctx)\n\n        map_rvar = SetRVar(\n            relctx.new_pointer_rvar(ir_set, src_rvar=src_rvar, ctx=ctx),\n            path_id=ir_set.path_id.ptr_path(),\n            aspects=aspects\n        )\n\n        rvars.append(map_rvar)\n\n        # Target set range.\n        if irtyputils.is_object(ir_set.typeref):\n            target_rvar = relctx.new_root_rvar(ir_set, lateral=True, ctx=ctx)\n\n            main_rvar = SetRVar(\n                target_rvar,\n                path_id=ir_set.path_id,\n                aspects=[pgce.PathAspect.VALUE, pgce.PathAspect.SOURCE]\n            )\n\n            rvars.append(main_rvar)\n        else:\n            main_rvar = SetRVar(\n                map_rvar.rvar,\n                path_id=ir_set.path_id,\n                aspects=[pgce.PathAspect.VALUE],\n            )\n            rvars.append(main_rvar)\n\n    if not source_is_visible:\n        # If the source path is not visible in the current scope,\n        # it means that there are no other paths sharing this path prefix\n        # in this scope.  In such cases the path is represented by a subquery\n        # rather than a simple set of ranges.\n        for srvar in rvars:\n            relctx.include_specific_rvar(\n                stmt, srvar.rvar, path_id=srvar.path_id,\n                aspects=srvar.aspects, ctx=ctx)\n\n        if is_primitive_ref:\n            aspects = [pgce.PathAspect.VALUE]\n        else:\n            aspects = [pgce.PathAspect.VALUE, pgce.PathAspect.SOURCE]\n\n        main_rvar = SetRVar(\n            relctx.new_rel_rvar(ir_set, stmt, ctx=ctx),\n            path_id=ir_set.path_id,\n            aspects=aspects,\n        )\n\n        rvars = [main_rvar]\n\n    assert main_rvar\n\n    return SetRVars(main=main_rvar, new=rvars)\n\n\ndef _new_subquery_stmt_set_rvar(\n    ir_set: irast.Set,\n    stmt: pgast.Query,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> SetRVars:\n    aspects = pathctx.list_path_aspects(stmt, ir_set.path_id)\n    if ir_set.path_id.is_tuple_path():\n        # If we are wrapping a tuple expression, make sure not to\n        # over-represent it in terms of the exposed aspects.\n        aspects -= {pgce.PathAspect.SERIALIZED}\n\n    return new_stmt_set_rvar(\n        ir_set, stmt, aspects=aspects, ctx=ctx)\n\n\ndef _lookup_set_rvar_in_source(\n        ir_set: irast.Set,\n        src_rvar: Optional[pgast.PathRangeVar], *,\n        ctx: context.CompilerContextLevel) -> Optional[pgast.PathRangeVar]:\n    if not (\n        ir_set.is_materialized_ref\n        and isinstance(src_rvar, pgast.RangeSubselect)\n    ):\n        return None\n\n    if pathctx.maybe_get_path_value_var(\n        src_rvar.subquery, ir_set.path_id, env=ctx.env\n    ):\n        return src_rvar\n\n    # When looking for an packed value in our source rvar, we need to\n    # account for the fact that unpack_rvar names all of its outputs\n    # based solely on the source--that is, if any of the pointer paths\n    # have extra namespaces on them, they won't appear. Rebuild the\n    # path_id without any namespaces that aren't on the src_path.\n    path_id = ir_set.path_id\n    path_id = not_none(path_id.src_path()).extend(\n        ptrref=not_none(path_id.rptr()),\n        direction=not_none(path_id.rptr_dir()),\n    )\n    if packed_ref := pathctx.maybe_get_rvar_path_var(\n        src_rvar,\n        pathctx.map_path_id(\n            path_id,\n            src_rvar.subquery.view_path_id_map,\n        ),\n        aspect=pgce.PathAspect.VALUE,\n        flavor='packed',\n        env=ctx.env,\n    ):\n        return relctx.unpack_var(\n            ctx.rel, ir_set.path_id, ref=packed_ref, ctx=ctx)\n    return None\n\n\n# N.B: registered for get_rvar for Stmt and MaterializedExpr below\n# Also, called explicitly for Pointer when expr is not None\n# TODO: This is a tangled mess that handles several cases.\n# Most of the code is for computed pointers.\ndef process_set_as_subquery(\n    ir_set: irast.Set, *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    is_objtype_path = ir_set.path_id.is_objtype_path()\n\n    stmt = ctx.rel\n    if isinstance(ir_set.expr, irast.Pointer):\n        rptr = ir_set.expr\n        expr = rptr.expr\n    else:\n        expr = ir_set.expr\n        rptr = None\n\n    ir_source: Optional[irast.Set]\n\n    source_set_rvar = None\n    if rptr is not None:\n        ir_source = rptr.source\n\n        if not is_objtype_path:\n            source_is_visible = True\n        else:\n            # Non-scalar computable pointer.  Check if path source is\n            # visible in the outer scope.\n            outer_fence = ctx.scope_tree.parent_branch\n            assert outer_fence is not None\n            source_is_visible = outer_fence.is_visible(ir_source.path_id)\n\n        if source_is_visible and (\n            ir_source.path_id not in ctx.skippable_sources\n        ):\n            with ctx.new() as sctx:\n                sctx.expr_exposed = False\n                source_set_rvar = get_set_rvar(ir_source, ctx=sctx)\n                # Force a source rvar so that trivial computed pointers\n                # on erroneous objects (like a bad array deref) fail.\n                # (Most sensible computables will end up requiring the\n                # source rvar anyway.)\n                ensure_source_rvar(ir_source, stmt, ctx=sctx)\n    else:\n        ir_source = None\n        source_is_visible = False\n\n    with ctx.new() as newctx:\n        # Suppress volatility refs while compiling schema\n        # aliases/globals.  While they might try to apply volatility\n        # refs due to FOR/free objects, it shouldn't be semantically\n        # necessary that they actually are attached to the enclosing\n        # location. This turns out to be an important optimization for\n        # ext::auth::ClientTokenIdentity.\n        if ir_set.is_schema_alias:\n            newctx.volatility_ref = ()\n\n        outer_id = ir_set.path_id\n        semi_join = False\n\n        if ir_source is not None:\n            if (\n                ir_source.path_id != ctx.current_insert_path_id\n                and not irutils.is_trivial_free_object(ir_source)\n            ):\n                # This is a computable pointer.  In order to ensure that\n                # the volatile functions in the pointer expression are called\n                # the necessary number of times, we must inject a\n                # \"volatility reference\" into function expressions.\n                # The volatility_ref is the identity of the pointer source.\n\n                # If the source is an insert that we are in the middle\n                # of doing, we don't have a volatility ref to add, so\n                # skip it based on the current_insert_path_id check.\n\n                # Note also that we skip this when the source is a\n                # trivial free object reference. A trivial free object\n                # reference is always executed exactly once (if there\n                # is an outer iterator of some kind, we'll pick up\n                # *that* volatility ref) and, unlike other shapes, may\n                # contain DML. We disable the volatility ref for\n                # trival free objects then both as a minor\n                # optimization and to avoid it interfering with DML in\n                # the object (since the volatility ref would not be\n                # visible in DML CTEs).\n                path_id = ir_source.path_id\n                newctx.volatility_ref += (\n                    lambda _stmt, xctx: relctx.maybe_get_path_var(\n                        stmt,\n                        path_id=path_id,\n                        aspect=pgce.PathAspect.IDENTITY,\n                        ctx=xctx,\n                    ),\n                )\n\n            if is_objtype_path and not source_is_visible:\n                # Non-scalar computable semi-join.\n\n                # TODO: The basic path case has a more sophisticated\n                # understanding of when to do semi-joins. Using that\n                # naively here doesn't work, but perhaps it could be\n                # adapted?\n                # Don't semi-join on free objects, since they are all unique\n                # (but don't *actually* have unique ids...)\n                semi_join = not irtyputils.is_free_object(ir_set.typeref)\n\n                # We need to compile the source and include it in,\n                # since we need to do the semi-join deduplication here\n                # on the outside, and not when the source is used in a\n                # path inside the computable.\n                # (See test_edgeql_scope_computables_09 for an example.)\n                with newctx.subrel() as _, _.newscope() as subctx:\n                    get_set_rvar(ir_source, ctx=subctx)\n                    subrvar = relctx.rvar_for_rel(subctx.rel, ctx=subctx)\n                    # Force a source rvar. See above.\n                    ensure_source_rvar(ir_source, subctx.rel, ctx=subctx)\n\n                relctx.include_rvar(\n                    stmt, subrvar, ir_source.path_id, ctx=newctx)\n\n        # If we are looking at a materialized computable, running\n        # get_set_rvar on the source above may have made it show\n        # up. So try to lookup the rvar again, and try to look it up\n        # in the source_rvar itself, and if we find it, skip compiling\n        # the computable.\n        if ir_source and (new_rvar := (\n            _lookup_set_rvar(ir_set, ctx=newctx)\n            or _lookup_set_rvar_in_source(ir_set, source_set_rvar, ctx=newctx)\n        )):\n            if semi_join:\n                # We need to use DISTINCT, instead of doing an actual\n                # semi-join, unfortunately: we need to extract data\n                # out from stmt, which we can't do with a semi-join.\n                value_var = pathctx.get_rvar_path_var(\n                    new_rvar,\n                    outer_id,\n                    aspect=pgce.PathAspect.VALUE,\n                    env=ctx.env,\n                )\n                stmt.distinct_clause = (\n                    pathctx.get_rvar_output_var_as_col_list(\n                        subrvar,\n                        value_var,\n                        aspect=pgce.PathAspect.VALUE,\n                        env=ctx.env,\n                    )\n                )\n\n            return _new_subquery_stmt_set_rvar(ir_set, stmt, ctx=newctx)\n\n        # materialized refs should always get picked up by now\n        assert not isinstance(expr, irast.MaterializedExpr), (\n            f\"Can't find materialized set {ir_set.path_id}\"\n        )\n        assert isinstance(expr, irast.Stmt)\n\n        inner_set = expr.result\n        inner_id = inner_set.path_id\n\n        if inner_id != outer_id:\n            pathctx.put_path_id_map(stmt, outer_id, inner_id)\n\n        if isinstance(expr, irast.MutatingStmt) and expr in ctx.dml_stmts:\n            # The DML table-routing logic may result in the same\n            # DML subquery to be visited twice, such as in the case\n            # of a nested INSERT declaring link properties, so guard\n            # against generating a duplicate DML CTE.\n            with newctx.substmt() as subrelctx:\n                dml_cte = ctx.dml_stmts[expr]\n                dml.wrap_dml_cte(expr, dml_cte, ctx=subrelctx)\n        else:\n            dispatch.visit(expr, ctx=newctx)\n\n        if semi_join:\n            set_rvar = relctx.new_root_rvar(ir_set, ctx=newctx)\n            tgt_ref = pathctx.get_rvar_path_identity_var(\n                set_rvar, ir_set.path_id, env=ctx.env)\n\n            pathctx.get_path_identity_output(\n                stmt, ir_set.path_id, env=ctx.env)\n            cond_expr = astutils.new_binop(tgt_ref, stmt, 'IN')\n\n            # Make a new stmt, join in the new root, and semi join on\n            # the original statement.\n            stmt = pgast.SelectStmt()\n            relctx.include_rvar(stmt, set_rvar, ir_set.path_id, ctx=newctx)\n            stmt.where_clause = astutils.extend_binop(\n                stmt.where_clause, cond_expr)\n\n    rvars = _new_subquery_stmt_set_rvar(ir_set, stmt, ctx=ctx)\n    # If the inner set also exposes a pointer path source, we need to\n    # also expose a pointer path source. See tests like\n    # test_edgeql_select_linkprop_rebind_01\n    if pathctx.maybe_get_path_rvar(\n        stmt,\n        inner_id.ptr_path(),\n        aspect=pgce.PathAspect.SOURCE,\n    ):\n        rvars.new.append(\n            SetRVar(\n                rvars.main.rvar,\n                outer_id.ptr_path(),\n                aspects=(pgce.PathAspect.SOURCE,),\n            )\n        )\n\n    return rvars\n\n\nregister_get_rvar(irast.Stmt)(process_set_as_subquery)\nregister_get_rvar(irast.MaterializedExpr)(process_set_as_subquery)\n\n\n@_special_case('std::IN')\n@_special_case('std::NOT IN')\ndef process_set_as_membership_expr(\n    ir_set: irast.SetE[irast.Call], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    expr = ir_set.expr\n    assert isinstance(expr, irast.OperatorCall)\n\n    with ctx.new() as newctx:\n        left, right = (a for a in expr.args.values())\n        left_arg, right_arg = left.expr, right.expr\n\n        newctx.expr_exposed = False\n        left_out = dispatch.compile(left_arg, ctx=newctx)\n\n        orig_right_arg = right_arg\n        unwrapped_right_arg = irutils.unwrap_set(right_arg)\n        # If the right operand of [NOT] IN is an array_unpack call,\n        # then use the ANY/ALL array comparison operator directly,\n        # since that has a higher chance of using the indexes.\n        right_expr = unwrapped_right_arg.expr\n        needs_coalesce = False\n\n        if (\n            isinstance(right_expr, irast.FunctionCall)\n            and str(right_expr.func_shortname) == 'std::array_unpack'\n            and not right_expr.args[0].cardinality.is_multi()\n            and (not expr.sql_operator or len(expr.sql_operator) <= 1)\n        ):\n            is_array_unpack = True\n            right_arg = right_expr.args[0].expr\n            needs_coalesce = right_expr.args[0].cardinality.can_be_zero()\n        else:\n            is_array_unpack = False\n\n        left_is_row_expr = astutils.is_row_expr(left_out)\n\n        with newctx.subrel() as _, _.newscope() as subctx:\n            if is_array_unpack:\n                relctx.update_scope(orig_right_arg, subctx.rel, ctx=subctx)\n                relctx.update_scope(\n                    unwrapped_right_arg, subctx.rel, ctx=subctx)\n\n            dispatch.compile(right_arg, ctx=subctx)\n            right_rel = subctx.rel\n            right_out = pathctx.get_path_value_var(\n                right_rel, right_arg.path_id, env=subctx.env)\n            right_out = output.output_as_value(right_out, env=ctx.env)\n\n            if (\n                left_is_row_expr\n                and right_arg.path_id.is_tuple_path()\n            ):\n                # When the RHS is an opaque tuple, we must unpack\n                # it using the (...).* indirection syntax, otherwise\n                # we get \"subquery has too few columns\".\n                right_out = pgast.Indirection(\n                    arg=right_out,\n                    indirection=[pgast.Star()],\n                )\n\n            right_rel.target_list = [pgast.ResTarget(val=right_out)]\n\n            if is_array_unpack:\n                right_rel = pgast.TypeCast(\n                    arg=right_rel,\n                    type_name=pgast.TypeName(\n                        name=pg_types.pg_type_from_ir_typeref(\n                            right_arg.typeref)\n                    )\n                )\n\n            negated = str(expr.func_shortname) == 'std::NOT IN'\n\n            set_expr = exprcomp.compile_operator(\n                expr,\n                [\n                    left_out,\n                    pgast.SubLink(\n                        operator=\"ALL\" if negated else \"ANY\",\n                        expr=right_rel,\n                    ),\n                ],\n                ctx=ctx,\n            )\n\n            # A NULL argument to the array variant will produce NULL, so we\n            # need to coalesce if that is possible.\n            if needs_coalesce:\n                empty_val = negated\n                set_expr = pgast.CoalesceExpr(args=[\n                    set_expr, pgast.BooleanConstant(val=empty_val)])\n\n            # Filter out situations where the LHS is a SQL NULL,\n            # since those will report false instead of {}.\n            if left.cardinality.can_be_zero() and left_out.nullable:\n                ctx.rel.where_clause = astutils.extend_binop(\n                    ctx.rel.where_clause,\n                    pgast.NullTest(arg=left_out, negated=True),\n                )\n\n            pathctx.put_path_value_var_if_not_exists(\n                ctx.rel, ir_set.path_id, set_expr\n            )\n\n    return new_stmt_set_rvar(ir_set, ctx.rel, ctx=ctx)\n\n\n@_special_case('std::UNION')\n@_special_case('std::EXCEPT')\n@_special_case('std::INTERSECT')\ndef process_set_as_setop(\n    ir_set: irast.SetE[irast.Call], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    expr = ir_set.expr\n\n    with ctx.new() as newctx:\n        newctx.expr_exposed = False\n\n        left, right = (a.expr for a in expr.args.values())\n\n        with newctx.subrel() as _, _.newscope() as scopectx:\n            larg = scopectx.rel\n            pathctx.put_path_id_map(larg, ir_set.path_id, left.path_id)\n            dispatch.visit(left, ctx=scopectx)\n\n        with newctx.subrel() as _, _.newscope() as scopectx:\n            rarg = scopectx.rel\n            pathctx.put_path_id_map(rarg, ir_set.path_id, right.path_id)\n            dispatch.visit(right, ctx=scopectx)\n\n    aspects = pathctx.list_path_aspects(\n        larg, left.path_id\n    ) & pathctx.list_path_aspects(rarg, right.path_id)\n\n    with ctx.subrel() as subctx:\n        subqry = subctx.rel\n        # There are three possible binary set operators coming from IR:\n        # UNION, EXCEPT, and INTERSECT\n        subqry.op = expr.func_shortname.name\n        subqry.all = True\n        subqry.larg = larg\n        subqry.rarg = rarg\n\n        setop_rvar = relctx.rvar_for_rel(subqry, lateral=True, ctx=subctx)\n        # No pull_namespace because we don't want the union arguments to\n        # escape, just the final result.\n        relctx.include_rvar(\n            ctx.rel,\n            setop_rvar,\n            ir_set.path_id,\n            aspects=aspects,\n            pull_namespace=False,\n            ctx=subctx,\n        )\n\n    return new_stmt_set_rvar(ir_set, ctx.rel, ctx=ctx)\n\n\n@_special_case('std::DISTINCT')\ndef process_set_as_distinct(\n    ir_set: irast.SetE[irast.Call], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    expr = ir_set.expr\n    stmt = ctx.rel\n\n    with ctx.subrel() as subctx:\n        subqry = subctx.rel\n        arg = expr.args[0].expr\n        pathctx.put_path_id_map(subqry, ir_set.path_id, arg.path_id)\n        dispatch.visit(arg, ctx=subctx)\n        subrvar = relctx.rvar_for_rel(\n            subqry, typeref=arg.typeref, lateral=True, ctx=subctx)\n\n    relctx.include_rvar(stmt, subrvar, ir_set.path_id, ctx=ctx)\n\n    value_var = pathctx.get_rvar_path_var(\n        subrvar,\n        ir_set.path_id,\n        aspect=pgce.PathAspect.VALUE,\n        env=ctx.env,\n    )\n\n    stmt.distinct_clause = pathctx.get_rvar_output_var_as_col_list(\n        subrvar,\n        value_var,\n        aspect=pgce.PathAspect.VALUE,\n        env=ctx.env,\n    )\n    # If there aren't any columns, we are doing DISTINCT on empty\n    # tuples. All empty tuples are equivalent, so we can just compile\n    # this by adding a LIMIT 1.\n    if not stmt.distinct_clause:\n        stmt.limit_count = pgast.NumericConstant(val=\"1\")\n\n    return new_stmt_set_rvar(ir_set, stmt, ctx=ctx)\n\n\n@_special_case('std::IF')\ndef process_set_as_ifelse(\n    ir_set: irast.SetE[irast.Call], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    # A IF Cond ELSE B is transformed into:\n    # SELECT A WHERE Cond UNION ALL SELECT B WHERE NOT Cond\n    expr = ir_set.expr\n    stmt = ctx.rel\n\n    if_expr, condition, else_expr = (a.expr for a in expr.args.values())\n    if_expr_card, _, else_expr_card = (\n        a.cardinality for a in expr.args.values()\n    )\n\n    with ctx.new() as newctx:\n        newctx.expr_exposed = False\n        dispatch.visit(condition, ctx=newctx)\n        condref = relctx.get_path_var(\n            stmt,\n            path_id=condition.path_id,\n            aspect=pgce.PathAspect.VALUE,\n            ctx=newctx,\n        )\n\n    if (if_expr_card.is_single() and else_expr_card.is_single()\n            and irtyputils.is_scalar(expr.typeref)):\n        # For a simple case of singleton scalars on both ends of IF,\n        # use a CASE WHEN construct, since it's normally faster than\n        # a UNION ALL with filters.  The reason why we limit this\n        # optimization to scalars is because CASE WHEN can only yield\n        # a single value, hence no other aspects can be supported\n        # by this rvar.\n        with ctx.new() as newctx:\n            newctx.expr_exposed = False\n            # Values still need to be encased in subqueries to guard\n            # against empty sets.\n            if_val = set_as_subquery(if_expr, as_value=True, ctx=newctx)\n            else_val = set_as_subquery(else_expr, as_value=True, ctx=newctx)\n\n        set_expr = pgast.CaseExpr(\n            args=[pgast.CaseWhen(expr=condref, result=if_val)],\n            defresult=else_val,\n        )\n\n        with ctx.subrel() as subctx:\n            pathctx.put_path_value_var_if_not_exists(\n                subctx.rel,\n                ir_set.path_id,\n                set_expr,\n            )\n            sub_rvar = relctx.rvar_for_rel(\n                subctx.rel,\n                lateral=True,\n                ctx=subctx,\n            )\n            relctx.include_rvar(stmt, sub_rvar, ir_set.path_id, ctx=subctx)\n\n        rvar = pathctx.get_path_value_var(\n            stmt, path_id=ir_set.path_id, env=ctx.env)\n        # We need to NULL filter both the result and the input condition\n        for var in [rvar, condref]:\n            stmt.where_clause = astutils.extend_binop(\n                stmt.where_clause,\n                pgast.NullTest(\n                    arg=var, negated=True\n                )\n            )\n\n    else:\n        with ctx.subrel() as _, _.newscope() as subctx:\n            subctx.expr_exposed = False\n            larg = subctx.rel\n            pathctx.put_path_id_map(larg, ir_set.path_id, if_expr.path_id)\n            dispatch.visit(if_expr, ctx=subctx)\n\n            larg.where_clause = astutils.extend_binop(\n                larg.where_clause,\n                condref\n            )\n\n        with ctx.subrel() as _, _.newscope() as subctx:\n            subctx.expr_exposed = False\n            rarg = subctx.rel\n            pathctx.put_path_id_map(rarg, ir_set.path_id, else_expr.path_id)\n            dispatch.visit(else_expr, ctx=subctx)\n\n            rarg.where_clause = astutils.extend_binop(\n                rarg.where_clause,\n                astutils.new_unop('NOT', condref)\n            )\n\n        aspects = pathctx.list_path_aspects(\n            larg, if_expr.path_id\n        ) & pathctx.list_path_aspects(rarg, else_expr.path_id)\n\n        with ctx.subrel() as subctx:\n            subqry = subctx.rel\n            subqry.op = 'UNION'\n            subqry.all = True\n            subqry.larg = larg\n            subqry.rarg = rarg\n\n            union_rvar = relctx.rvar_for_rel(subqry, lateral=True, ctx=subctx)\n            relctx.include_rvar(\n                stmt,\n                union_rvar,\n                ir_set.path_id,\n                pull_namespace=False,\n                aspects=aspects,\n                ctx=subctx,\n            )\n\n    return new_stmt_set_rvar(ir_set, stmt, ctx=ctx)\n\n\n@_special_case('std::??')\ndef process_set_as_coalesce(\n    ir_set: irast.SetE[irast.Call], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    expr = ir_set.expr\n\n    with ctx.new() as newctx:\n        newctx.expr_exposed = False\n        left_ir, right_ir = (a.expr for a in expr.args.values())\n        _left_card, right_card = (a.cardinality for a in expr.args.values())\n        is_object = (\n            ir_set.path_id.is_objtype_path()\n            or ir_set.path_id.is_tuple_path()\n        )\n\n        # The cardinality optimization below applies only to\n        # non-object/non-tuple expressions, because we don't want to\n        # have to deal with the complexity of resolving coalesced\n        # sources for potential link or property references.\n        if right_card.is_single() and not is_object:\n            left = dispatch.compile(left_ir, ctx=newctx)\n\n            # If the RHS is optional, we compile it in a subquery so that\n            # it becomes NULL instead of potentially joining in zero rows.\n            # If not, just compile it without any protection.\n            right = (\n                set_as_subquery(right_ir, ctx=newctx)\n                if right_card.can_be_zero()\n                else dispatch.compile(right_ir, ctx=newctx)\n            )\n\n            # Just use scalar COALESCE now\n            set_expr = pgast.CoalesceExpr(args=[left, right])\n            pathctx.put_path_value_var(ctx.rel, ir_set.path_id, set_expr)\n\n        else:\n            # Things become tricky in cases where the RHS is a non-singleton\n            # or where we need to worry about source aspects.\n            # We cannot use the regular scalar COALESCE over a JOIN,\n            # as that'll blow up the result cardinality. Instead, we do\n            # something like:\n            #\n            #     CROSS JOIN LATERAL\n            #       (<left>) as lhs\n            #     CROSS JOIN LATERAL (\n            #       SELECT * FROM lhs WHERE lhs.value IS NOT NULL\n            #       UNION ALL\n            #       SELECT * FROM <right> as rhs WHERE lhs.value IS NULL\n            #     ) as q\n            #\n            # Note that <left> will be compiled with optional wrapping,\n            # so it shouldn't ever produce zero rows.\n            lhs_rvar = get_set_rvar(left_ir, ctx=newctx)\n            lvar = pathctx.get_rvar_path_var(\n                lhs_rvar,\n                left_ir.path_id,\n                aspect=pgce.PathAspect.VALUE,\n                env=ctx.env,\n            )\n            lval = output.output_as_value(lvar, env=ctx.env)\n\n            with newctx.subrel() as lctx:\n                larg = lctx.rel\n                pathctx.put_path_id_map(larg, ir_set.path_id, left_ir.path_id)\n\n                relctx.include_rvar(\n                    larg,\n                    lhs_rvar,\n                    path_id=left_ir.path_id,\n                    ctx=lctx,\n                    # Only include the aspects that got included\n                    # into our rel; it's possible some were explicitly\n                    # left out, and we should respect that.\n                    aspects=pathctx.list_path_aspects(\n                        newctx.rel, left_ir.path_id\n                    ),\n                )\n\n                # Include the LHS when it is not NULL.\n                larg.where_clause = astutils.extend_binop(\n                    larg.where_clause,\n                    pgast.NullTest(\n                        arg=lval, negated=True\n                    )\n                )\n\n            with newctx.subrel() as rctx:\n                rarg = rctx.rel\n                pathctx.put_path_id_map(rarg, ir_set.path_id, right_ir.path_id)\n                rvar = dispatch.compile(right_ir, ctx=rctx)\n\n                # Include the RHS when the LHS is NULL.\n                # Note that an important precondition of this is that\n                # there are not \"stray\" NULLs in the LHS input set.\n                #\n                # HACK: We need to use IS NOT DISTINCT FROM\n                # because ROW() IS NULL is true, and that\n                # breaks some things.\n                rarg.where_clause = astutils.extend_binop(\n                    rarg.where_clause,\n                    astutils.new_binop(\n                        lval, pgast.NullConstant(),\n                        'IS NOT DISTINCT FROM',\n                    ),\n                )\n\n                if rvar.nullable:\n                    rarg.where_clause = astutils.extend_binop(\n                        rarg.where_clause,\n                        pgast.NullTest(arg=rvar, negated=True)\n                    )\n\n            union_rvar = relctx.rvar_for_rel(\n                pgast.SelectStmt(op='UNION', all=True, larg=larg, rarg=rarg),\n                lateral=True,\n                ctx=newctx,\n            )\n\n            aspects = (\n                pathctx.list_path_aspects(larg, left_ir.path_id)\n                & pathctx.list_path_aspects(rarg, right_ir.path_id)\n            )\n\n            # No pull_namespace because we don't want the coalesce arguments to\n            # escape, just the final result.\n            relctx.include_rvar(\n                ctx.rel,\n                union_rvar,\n                path_id=ir_set.path_id,\n                aspects=aspects,\n                pull_namespace=False,\n                ctx=newctx,\n            )\n\n    return new_stmt_set_rvar(ir_set, ctx.rel, ctx=ctx)\n\n\n@register_get_rvar(irast.Tuple)\ndef process_set_as_tuple(\n    ir_set: irast.SetE[irast.Tuple], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    expr = ir_set.expr\n    stmt = ctx.rel\n\n    with ctx.new() as subctx:\n        subctx.expr_exposed_tuple_cheat = None\n        elements = []\n\n        ttypes = {}\n        for i, st in enumerate(ir_set.typeref.subtypes):\n            if st.element_name:\n                ttypes[st.element_name] = st\n            else:\n                ttypes[str(i)] = st\n\n        for element in expr.elements:\n            assert element.path_id\n            path_id = element.path_id\n\n            # We compile in a subrel *solely* so that we can map\n            # each element individually. It would be nice to have\n            # a way to do this that doesn't actually affect the output!\n            with subctx.subrel() as newctx:\n                if element is ctx.expr_exposed_tuple_cheat:\n                    newctx.expr_exposed = True\n\n                if path_id != element.val.path_id:\n                    pathctx.put_path_id_map(\n                        newctx.rel, path_id, element.val.path_id)\n                dispatch.visit(element.val, ctx=newctx)\n\n            el_rvar = relctx.new_rel_rvar(ir_set, newctx.rel, ctx=ctx)\n            aspects = pathctx.list_path_aspects(\n                newctx.rel, element.val.path_id\n            )\n            # update_mask=False because we are doing this solely to remap\n            # elements individually and don't want to affect the mask.\n            relctx.include_rvar(\n                stmt,\n                el_rvar,\n                path_id,\n                update_mask=False,\n                aspects=aspects,\n                ctx=ctx,\n            )\n            tvar = pathctx.get_path_value_var(stmt, path_id, env=subctx.env)\n\n            elements.append(pgast.TupleElementBase(path_id=path_id))\n\n            # We need to filter out NULLs at tuple creation time, to\n            # prevent having tuples that are part-NULL.\n            if tvar.nullable:\n                stmt.where_clause = astutils.extend_binop(\n                    stmt.where_clause,\n                    pgast.NullTest(arg=tvar, negated=True)\n                )\n\n            var = pathctx.maybe_get_path_var(\n                stmt,\n                element.val.path_id,\n                aspect=pgce.PathAspect.SERIALIZED,\n                env=subctx.env,\n            )\n            if var is not None:\n                pathctx.put_path_var(\n                    stmt,\n                    path_id,\n                    var,\n                    aspect=pgce.PathAspect.SERIALIZED,\n                )\n\n        set_expr = pgast.TupleVarBase(\n            elements=elements,\n            named=expr.named,\n            typeref=ir_set.typeref,\n        )\n\n    pathctx.put_path_value_var(stmt, ir_set.path_id, set_expr)\n\n    # This is an unfortunate hack. If any of those types that we\n    # contain are an object, then force the computation of the\n    # serialized output now. This avoids issues where there may be\n    # references to tuple elements with the same path id but different\n    # shapes, and the delaying induced by a TupleBaseVar can cause the\n    # wrong one to be output. (See test_edgeql_scope_shape_03 for an example\n    # where this can come up.)\n    # (We only do it for objects as an optimization.)\n    if (\n        output.in_serialization_ctx(ctx)\n        and any(irtyputils.is_object(x) for x in ir_set.typeref.subtypes)\n    ):\n        pathctx.get_path_serialized_output(stmt, ir_set.path_id, env=ctx.env)\n\n    return new_stmt_set_rvar(\n        ir_set,\n        stmt,\n        aspects=[pgce.PathAspect.VALUE, pgce.PathAspect.SOURCE],\n        ctx=ctx,\n    )\n\n\n@register_get_rvar(irast.TupleIndirectionPointer)\ndef process_set_as_tuple_indirection(\n    ir_set: irast.SetE[irast.TupleIndirectionPointer],\n    *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    rptr = ir_set.expr\n    tuple_set = rptr.source\n    stmt = ctx.rel\n\n    assert not rptr.expr, 'tuple indirection pointer with expr??'\n\n    with ctx.new() as subctx:\n        # Usually the LHS is is not exposed, but when we are directly\n        # projecting from an explicit tuple, and the result is a\n        # collection, arrange to have the element we are projecting\n        # treated as exposed. This behavior is needed for our\n        # eta-expansion of arrays to work, since it generates that\n        # idiom in a place where it needs the output to be exposed.\n        subctx.expr_exposed = False\n        if (\n            ctx.expr_exposed\n            and not tuple_set.is_binding\n            and isinstance(tuple_set.expr, irast.Tuple)\n            and ir_set.path_id.is_collection_path()\n        ):\n            for el in tuple_set.expr.elements:\n                if el.name == rptr.ptrref.shortname.name:\n                    subctx.expr_exposed_tuple_cheat = el\n                    break\n        rvar = get_set_rvar(tuple_set, ctx=subctx)\n\n        source_rvar = relctx.maybe_get_path_rvar(\n            stmt,\n            tuple_set.path_id,\n            aspect=pgce.PathAspect.SOURCE,\n            ctx=subctx,\n        )\n\n        if source_rvar is None:\n            # Lack of visible tuple source means we are\n            # an indirection over an opaque tuple, e.g. in\n            # `SELECT [(1,)][0].0`.  This means we must\n            # use an explicit row attribute dereference.\n            tuple_val = pathctx.get_path_value_var(\n                stmt, path_id=tuple_set.path_id, env=subctx.env)\n\n            set_expr = astutils.tuple_getattr(\n                tuple_val,\n                tuple_set.typeref,\n                rptr.ptrref.shortname.name,\n            )\n\n            pathctx.put_path_var_if_not_exists(\n                stmt,\n                ir_set.path_id,\n                set_expr,\n                aspect=pgce.PathAspect.VALUE,\n            )\n\n            rvar = relctx.new_rel_rvar(ir_set, stmt, ctx=subctx)\n\n    return new_simple_set_rvar(\n        ir_set,\n        rvar,\n        aspects=(pgce.PathAspect.VALUE,),\n    )\n\n\n@register_get_rvar(irast.TypeCast)\ndef process_set_as_type_cast(\n    ir_set: irast.SetE[irast.TypeCast], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    expr = ir_set.expr\n    stmt = ctx.rel\n\n    inner_set = expr.expr\n    is_json_cast = expr.to_type.id == s_obj.get_known_type_id('std::json')\n\n    # Are we casting by compiling the innards in json mode?\n    implicit_cast = (\n        is_json_cast\n        and not irtyputils.is_range(inner_set.typeref)\n        and (irtyputils.is_collection(inner_set.typeref)\n             or irtyputils.is_object(inner_set.typeref))\n    )\n    fmt_ctx = (\n        context.output_format(ctx, context.OutputFormat.JSONB) if implicit_cast\n        else contextlib.nullcontext()\n    )\n\n    with fmt_ctx, ctx.new() as subctx:\n        pathctx.put_path_id_map(ctx.rel, ir_set.path_id, inner_set.path_id)\n\n        if implicit_cast:\n            subctx.expr_exposed = True\n\n            set_expr = dispatch.compile(inner_set, ctx=subctx)\n\n            serialized: Optional[pgast.BaseExpr] = (\n                pathctx.maybe_get_path_serialized_var(\n                    stmt, inner_set.path_id, env=subctx.env)\n            )\n\n            if serialized is not None:\n                if irtyputils.is_collection(inner_set.typeref):\n                    serialized = output.serialize_expr_to_json(\n                        serialized, styperef=inner_set.path_id.target,\n                        env=subctx.env)\n\n                pathctx.put_path_value_var(\n                    stmt, inner_set.path_id, serialized, force=True\n                )\n\n                pathctx.put_path_serialized_var(\n                    stmt, inner_set.path_id, serialized, force=True\n                )\n        else:\n            # Rely on the simple implementation of TypeCast\n            set_expr = dispatch.compile(expr, ctx=subctx)\n\n            # A proper path var mapping way would be to wrap\n            # the inner expression in a subquery, but that\n            # seems excessive for a type cast, so we cover\n            # our tracks here by removing the mapping and\n            # relying on the value and serialized vars\n            # populated above.\n            stmt.view_path_id_map.pop(ir_set.path_id)\n\n    pathctx.put_path_value_var_if_not_exists(stmt, ir_set.path_id, set_expr)\n\n    return new_stmt_set_rvar(ir_set, stmt, ctx=ctx)\n\n\n@register_get_rvar(irast.ConstantSet)\ndef process_set_as_const_set(\n    ir_set: irast.SetE[irast.ConstantSet], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    with ctx.subrel() as subctx:\n        vals = [dispatch.compile(v, ctx=subctx) for v in ir_set.expr.elements]\n        vals_rel = subctx.rel\n        vals_rel.values = [pgast.ImplicitRowExpr(args=[v]) for v in vals]\n        vals_rel.nullable = any(v.nullable for v in vals)\n\n    vals_rvar = relctx.new_rel_rvar(ir_set, vals_rel, ctx=ctx)\n    relctx.include_rvar(ctx.rel, vals_rvar, ir_set.path_id, ctx=ctx)\n\n    return new_stmt_set_rvar(ir_set, ctx.rel, ctx=ctx)\n\n\ndef process_set_as_oper_expr(\n    ir_set: irast.SetE[irast.OperatorCall], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    # XXX: do we need a subrel?\n    with ctx.new() as newctx:\n        newctx.expr_exposed = False\n        args = _compile_call_args(ir_set, ctx=newctx)\n        oper_expr = exprcomp.compile_operator(ir_set.expr, args, ctx=newctx)\n\n        if _should_unwrap_polymorphic_return_array(ir_set.expr):\n            oper_expr = astutils.array_get_inner_array(\n                oper_expr, ir_set.expr.typeref\n            )\n\n    pathctx.put_path_value_var_if_not_exists(\n        ctx.rel, ir_set.path_id, oper_expr\n    )\n\n    return new_stmt_set_rvar(ir_set, ctx.rel, ctx=ctx)\n\n\n@register_get_rvar(irast.TriggerAnchor)\ndef process_set_as_trigger_anchor(\n    ir_set: irast.Set, *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    # XXX: This will need to grow more things\n    if ir_set.path_id in ctx.external_rels:\n        return process_external_rel(ir_set, ctx=ctx)\n\n    return process_set_as_root(ir_set, ctx=ctx)\n\n\n@register_get_rvar(irast.Expr)\ndef process_set_as_expr(\n    ir_set: irast.SetE[irast.Expr], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    with ctx.new() as newctx:\n        newctx.expr_exposed = False\n        set_expr = dispatch.compile(ir_set.expr, ctx=newctx)\n\n    pathctx.put_path_value_var_if_not_exists(ctx.rel, ir_set.path_id, set_expr)\n\n    return new_stmt_set_rvar(ir_set, ctx.rel, ctx=ctx)\n\n\n@_special_case('std::assert_single')\ndef process_set_as_singleton_assertion(\n    ir_set: irast.SetE[irast.Call],\n    *,\n    ctx: context.CompilerContextLevel,\n) -> SetRVars:\n    expr = ir_set.expr\n    stmt = ctx.rel\n\n    msg_arg = expr.args['message']\n    ir_arg = expr.args[0]\n    ir_arg_set = ir_arg.expr\n\n    if (\n        ir_arg.cardinality.is_single()\n        and not msg_arg.cardinality.is_multi()\n    ):\n        # If the argument has been statically proven to be a singleton,\n        # elide the entire assertion.\n        arg_ref = dispatch.compile(ir_arg_set, ctx=ctx)\n        pathctx.put_path_value_var(stmt, ir_set.path_id, arg_ref)\n        pathctx.put_path_id_map(stmt, ir_set.path_id, ir_arg_set.path_id)\n        return new_stmt_set_rvar(ir_set, stmt, ctx=ctx)\n\n    with ctx.subrel() as newctx:\n        arg_ref = dispatch.compile(ir_arg_set, ctx=newctx)\n        arg_val = output.output_as_value(arg_ref, env=newctx.env)\n\n        msg = dispatch.compile(msg_arg.expr, ctx=newctx)\n\n        # Generate a singleton set assertion as the following SQL:\n        #\n        #    SELECT\n        #        <target_set>,\n        #        raise_on_null(NULLIF(row_number() OVER (), 2)) AS _sentinel\n        #    ORDER BY\n        #        _sentinel\n        #\n        # This effectively raises an error whenever the row counter reaches 2.\n        check_expr = pgast.FuncCall(\n            name=('nullif',),\n            args=[\n                pgast.FuncCall(\n                    name=('row_number',),\n                    args=[],\n                    over=pgast.WindowDef()\n                ),\n                pgast.NumericConstant(\n                    val='2',\n                ),\n            ],\n        )\n\n        maybe_raise = pgast.FuncCall(\n            name=astutils.edgedb_func('raise_on_null', ctx=ctx),\n            args=[\n                check_expr,\n                pgast.StringConstant(val='cardinality_violation'),\n                pgast.NamedFuncArg(\n                    name='msg',\n                    val=pgast.CoalesceExpr(\n                        args=[\n                            msg,\n                            pgast.StringConstant(\n                                val='assert_single violation: more than one '\n                                    'element returned by an expression',\n                            ),\n                        ],\n                    ),\n                ),\n                pgast.NamedFuncArg(\n                    name='constraint',\n                    val=pgast.StringConstant(val='std::assert_single'),\n                ),\n            ],\n        )\n\n        output.add_null_test(arg_ref, newctx.rel)\n\n        # Force Postgres to actually evaluate the result target\n        # by putting it into an ORDER BY.\n        newctx.rel.target_list.append(\n            pgast.ResTarget(\n                name=\"_sentinel\",\n                val=maybe_raise,\n            ),\n        )\n\n        if newctx.rel.sort_clause is None:\n            newctx.rel.sort_clause = []\n        newctx.rel.sort_clause.append(\n            pgast.SortBy(node=pgast.ColumnRef(name=[\"_sentinel\"])),\n        )\n\n        pathctx.put_path_var_if_not_exists(\n            newctx.rel, ir_set.path_id, arg_val, aspect=pgce.PathAspect.VALUE\n        )\n\n        pathctx.put_path_id_map(newctx.rel, ir_set.path_id, ir_arg_set.path_id)\n\n    aspects = pathctx.list_path_aspects(newctx.rel, ir_arg_set.path_id)\n    func_rvar = relctx.new_rel_rvar(ir_set, newctx.rel, ctx=ctx)\n    relctx.include_rvar(stmt, func_rvar, ir_set.path_id,\n                        aspects=aspects, ctx=ctx)\n\n    return new_stmt_set_rvar(ir_set, stmt, aspects=aspects, ctx=ctx)\n\n\n@_special_case('std::assert_exists')\ndef process_set_as_existence_assertion(\n    ir_set: irast.SetE[irast.Call],\n    *,\n    ctx: context.CompilerContextLevel,\n) -> SetRVars:\n    \"\"\"Implementation of std::assert_exists\"\"\"\n    expr = ir_set.expr\n    stmt = ctx.rel\n\n    msg_arg = expr.args['message']\n    ir_arg = expr.args[0]\n    ir_arg_set = ir_arg.expr\n\n    if (\n        not ir_arg.cardinality.can_be_zero()\n        and not msg_arg.cardinality.is_multi()\n    ):\n        # If the argument has been statically proven to be non empty,\n        # elide the entire assertion.\n        arg_ref = dispatch.compile(ir_arg_set, ctx=ctx)\n        pathctx.put_path_value_var(stmt, ir_set.path_id, arg_ref)\n        pathctx.put_path_id_map(stmt, ir_set.path_id, ir_arg_set.path_id)\n        return new_stmt_set_rvar(ir_set, stmt, ctx=ctx)\n\n    with ctx.subrel() as newctx:\n        # The solution to assert_exists() is as simple as\n        # calling raise_on_null().\n        newctx.expr_exposed = False\n        newctx.force_optional |= {ir_arg_set.path_id}\n        pathctx.put_path_id_map(newctx.rel, ir_set.path_id, ir_arg_set.path_id)\n        arg_ref = dispatch.compile(ir_arg_set, ctx=newctx)\n        arg_val = output.output_as_value(arg_ref, env=newctx.env)\n\n        msg = dispatch.compile(msg_arg.expr, ctx=newctx)\n\n        set_expr = pgast.FuncCall(\n            name=astutils.edgedb_func('raise_on_null', ctx=ctx),\n            args=[\n                arg_val,\n                pgast.StringConstant(val='cardinality_violation'),\n                pgast.NamedFuncArg(\n                    name='msg',\n                    val=pgast.CoalesceExpr(\n                        args=[\n                            msg,\n                            pgast.StringConstant(\n                                val='assert_exists violation: expression '\n                                    'returned an empty set',\n                            ),\n                        ]\n                    ),\n                ),\n                pgast.NamedFuncArg(\n                    name='constraint',\n                    val=pgast.StringConstant(val='std::assert_exists'),\n                ),\n            ],\n        )\n\n        pathctx.put_path_value_var(\n            newctx.rel,\n            ir_arg_set.path_id,\n            set_expr,\n            force=True,\n        )\n        other_aspect = (\n            pgce.PathAspect.IDENTITY\n            if ir_set.path_id.is_objtype_path() else\n            pgce.PathAspect.SERIALIZED\n        )\n        pathctx.put_path_var(\n            newctx.rel,\n            ir_arg_set.path_id,\n            set_expr,\n            force=True,\n            aspect=other_aspect,\n        )\n\n    # It is important that we do not provide source, which could allow\n    # fields on the object to be accessed without triggering the\n    # raise_on_null. Not providing source means another join is\n    # needed, which will trigger it.\n    func_rvar = relctx.new_rel_rvar(ir_set, newctx.rel, ctx=ctx)\n    relctx.include_rvar(\n        stmt,\n        func_rvar,\n        ir_set.path_id,\n        aspects=(pgce.PathAspect.VALUE,),\n        ctx=ctx,\n    )\n\n    return new_stmt_set_rvar(\n        ir_set,\n        stmt,\n        aspects=(pgce.PathAspect.VALUE,),\n        ctx=ctx,\n    )\n\n\n@_special_case('std::assert_distinct')\ndef process_set_as_multiplicity_assertion(\n    ir_set: irast.SetE[irast.Call],\n    *,\n    ctx: context.CompilerContextLevel,\n) -> SetRVars:\n    \"\"\"Implementation of std::assert_distinct\"\"\"\n    expr = ir_set.expr\n    msg_arg = expr.args['message']\n    ir_arg = expr.args[0]\n    ir_arg_set = ir_arg.expr\n\n    if (\n        not ir_arg.multiplicity.is_duplicate()\n        and not msg_arg.cardinality.is_multi()\n    ):\n        # If the argument has been statically proven to be distinct,\n        # elide the entire assertion.\n        arg_ref = dispatch.compile(ir_arg_set, ctx=ctx)\n        pathctx.put_path_value_var(ctx.rel, ir_set.path_id, arg_ref)\n        pathctx.put_path_id_map(ctx.rel, ir_set.path_id, ir_arg_set.path_id)\n        return new_stmt_set_rvar(ir_set, ctx.rel, ctx=ctx)\n\n    # Generate a distinct set assertion as the following SQL:\n    #\n    #    SELECT\n    #        <target_set>,\n    #        (CASE WHEN\n    #            <target_set>\n    #            IS DISTINCT FROM\n    #            lag(<target_set>) OVER (ORDER BY <target_set>)\n    #        THEN <target_set>\n    #        ELSE edgedb.raise(ConstraintViolationError)) AS check_expr\n    #    FROM\n    #        (SELECT <target_set>, row_number() OVER () AS i) AS q\n    #    ORDER BY\n    #        q.i, check_expr\n    #\n    # NOTE: sorting over original row_number() is necessary to preserve\n    #       order, as assert_distinct() must be completely transparent for\n    #       compliant sets.\n    with ctx.subrel() as newctx:\n        with newctx.subrel() as subctx:\n            dispatch.visit(ir_arg_set, ctx=subctx)\n            arg_ref = pathctx.get_path_output(\n                subctx.rel,\n                ir_arg_set.path_id,\n                aspect=pgce.PathAspect.VALUE,\n                env=subctx.env,\n            )\n            arg_val = output.output_as_value(arg_ref, env=newctx.env)\n            sub_rvar = relctx.new_rel_rvar(ir_arg_set, subctx.rel, ctx=subctx)\n\n            aspects = pathctx.list_path_aspects(subctx.rel, ir_arg_set.path_id)\n            relctx.include_rvar(\n                newctx.rel, sub_rvar, ir_arg_set.path_id,\n                aspects=aspects, ctx=subctx,\n            )\n            alias = ctx.env.aliases.get('i')\n            subctx.rel.target_list.append(\n                pgast.ResTarget(\n                    name=alias,\n                    val=pgast.FuncCall(\n                        name=('row_number',),\n                        args=[],\n                        over=pgast.WindowDef(),\n                    )\n                )\n            )\n\n        msg = dispatch.compile(msg_arg.expr, ctx=newctx)\n\n        do_raise = pgast.FuncCall(\n            name=astutils.edgedb_func('raise', ctx=ctx),\n            args=[\n                pgast.TypeCast(\n                    arg=pgast.NullConstant(),\n                    type_name=pgast.TypeName(\n                        name=pg_types.pg_type_from_ir_typeref(\n                            ir_arg_set.typeref),\n                    ),\n                ),\n                pgast.StringConstant(val='cardinality_violation'),\n                pgast.NamedFuncArg(\n                    name='msg',\n                    val=pgast.CoalesceExpr(\n                        args=[\n                            msg,\n                            pgast.StringConstant(\n                                val='assert_distinct violation: expression '\n                                    'returned a set with duplicate elements',\n                            ),\n                        ],\n                    ),\n                ),\n                pgast.NamedFuncArg(\n                    name='constraint',\n                    val=pgast.StringConstant(val='std::assert_distinct'),\n                ),\n            ],\n        )\n\n        check_expr = pgast.CaseExpr(\n            args=[\n                pgast.CaseWhen(\n                    expr=astutils.new_binop(\n                        lexpr=arg_val,\n                        op='IS DISTINCT FROM',\n                        rexpr=pgast.FuncCall(\n                            name=('lag',),\n                            args=[arg_val],\n                            over=pgast.WindowDef(\n                                order_clause=[pgast.SortBy(node=arg_val)],\n                            ),\n                        ),\n                    ),\n                    result=arg_val,\n                ),\n            ],\n            defresult=do_raise,\n        )\n\n        alias2 = ctx.env.aliases.get('v')\n        newctx.rel.target_list.append(\n            pgast.ResTarget(\n                val=check_expr,\n                name=alias2,\n            )\n        )\n\n        pathctx.put_path_var(\n            newctx.rel,\n            ir_set.path_id,\n            check_expr,\n            aspect=pgce.PathAspect.VALUE,\n        )\n\n        if newctx.rel.sort_clause is None:\n            newctx.rel.sort_clause = []\n        newctx.rel.sort_clause.extend([\n            pgast.SortBy(\n                node=pgast.ColumnRef(name=[sub_rvar.alias.aliasname, alias]),\n            ),\n            pgast.SortBy(\n                node=pgast.ColumnRef(name=[alias2]),\n            ),\n        ])\n\n        pathctx.put_path_id_map(newctx.rel, ir_set.path_id, ir_arg_set.path_id)\n\n    func_rvar = relctx.new_rel_rvar(ir_set, newctx.rel, ctx=ctx)\n    relctx.include_rvar(\n        ctx.rel, func_rvar, ir_set.path_id, aspects=aspects, ctx=ctx\n    )\n\n    return new_stmt_set_rvar(ir_set, ctx.rel, aspects=aspects, ctx=ctx)\n\n\n@_special_case('std::materialized')\ndef process_set_as_materialized_call(\n    ir_set: irast.SetE[irast.Call],\n    *,\n    ctx: context.CompilerContextLevel,\n) -> SetRVars:\n    # It's a pure pass-through. Just an identity function marked volatile.\n    stmt = ctx.rel\n\n    ir_arg_set = ir_set.expr.args[0].expr\n\n    arg_ref = dispatch.compile(ir_arg_set, ctx=ctx)\n    pathctx.put_path_value_var(stmt, ir_set.path_id, arg_ref)\n    pathctx.put_path_id_map(stmt, ir_set.path_id, ir_arg_set.path_id)\n    return new_stmt_set_rvar(ir_set, stmt, ctx=ctx)\n\n\ndef process_set_as_simple_enumerate(\n    ir_set: irast.Set, *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    expr = ir_set.expr\n    assert isinstance(expr, irast.FunctionCall)\n\n    with ctx.subrel() as newctx:\n        ir_call_arg = expr.args[0]\n        ir_arg = ir_call_arg.expr\n        arg_ref = dispatch.compile(ir_arg, ctx=newctx)\n        arg_val = output.output_as_value(arg_ref, env=newctx.env)\n\n        if arg_ref.nullable:\n            newctx.rel.where_clause = astutils.extend_binop(\n                newctx.rel.where_clause,\n                pgast.NullTest(arg=arg_ref, negated=True)\n            )\n\n        rtype = expr.typeref\n        named_tuple = any(st.element_name for st in rtype.subtypes)\n\n        num_expr = pgast.Expr(\n            name='-',\n            lexpr=pgast.FuncCall(\n                name=('row_number',),\n                args=[],\n                over=pgast.WindowDef()\n            ),\n            rexpr=pgast.NumericConstant(val='1'),\n            nullable=False,\n        )\n\n        set_expr = pgast.TupleVar(\n            elements=[\n                pgast.TupleElement(\n                    path_id=expr.tuple_path_ids[0],\n                    name=rtype.subtypes[0].element_name or '0',\n                    val=num_expr,\n                ),\n                pgast.TupleElement(\n                    path_id=expr.tuple_path_ids[1],\n                    name=rtype.subtypes[1].element_name or '1',\n                    val=arg_val,\n                ),\n            ],\n            named=named_tuple,\n            typeref=ir_set.typeref,\n        )\n\n        for element in set_expr.elements:\n            pathctx.put_path_value_var(\n                newctx.rel, element.path_id, element.val\n            )\n\n        var = pathctx.maybe_get_path_var(\n            newctx.rel,\n            ir_arg.path_id,\n            aspect=pgce.PathAspect.SERIALIZED,\n            env=newctx.env,\n        )\n        if var is not None:\n            pathctx.put_path_var(\n                newctx.rel,\n                set_expr.elements[1].path_id,\n                var,\n                aspect=pgce.PathAspect.SERIALIZED,\n            )\n\n        pathctx.put_path_var_if_not_exists(\n            newctx.rel,\n            ir_set.path_id,\n            set_expr,\n            aspect=pgce.PathAspect.VALUE,\n        )\n\n    aspects = pathctx.list_path_aspects(newctx.rel, ir_arg.path_id) | {\n        pgce.PathAspect.SOURCE\n    }\n\n    pathctx.put_path_id_map(newctx.rel, expr.tuple_path_ids[1], ir_arg.path_id)\n\n    func_rvar = relctx.new_rel_rvar(ir_set, newctx.rel, ctx=ctx)\n    relctx.include_rvar(\n        ctx.rel, func_rvar, ir_set.path_id, aspects=aspects, ctx=ctx\n    )\n\n    return new_stmt_set_rvar(ir_set, ctx.rel, aspects=aspects, ctx=ctx)\n\n\n@_special_case('std::enumerate')\ndef process_set_as_enumerate(\n    ir_set: irast.SetE[irast.Call], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    expr = ir_set.expr\n    arg_set = expr.args[0].expr\n    arg_expr = arg_set.expr\n    arg_subj = irutils.unwrap_set(arg_set).expr\n    if (\n        isinstance(arg_subj, irast.FunctionCall)\n        and not arg_subj.func_sql_expr\n        and not (\n            isinstance(arg_expr, irast.SelectStmt)\n            and (\n                arg_expr.where\n                or arg_expr.orderby\n                or arg_expr.limit\n                or arg_expr.offset\n            )\n        ) and not any(\n            f_arg.param_typemod == qltypes.TypeModifier.SetOfType\n            for _, f_arg in arg_subj.args.items()\n        )\n    ):\n        # Enumeration of a non-aggregate function\n        rvars = process_set_as_func_enumerate(ir_set, ctx=ctx)\n    else:\n        rvars = process_set_as_simple_enumerate(ir_set, ctx=ctx)\n\n    return rvars\n\n\n@_special_case('std::max', only_as_fallback=True)\n@_special_case('std::min', only_as_fallback=True)\ndef process_set_as_std_min_max(\n    ir_set: irast.SetE[irast.Call],\n    *,\n    ctx: context.CompilerContextLevel,\n) -> SetRVars:\n    # Postgres implements min/max aggregates for only a specific\n    # subset of scalars and their respective arrays. However, in\n    # EdgeDB every type is orderable (supports < and >) and so to\n    # accommodate that we must choose between the native Postgres\n    # aggregate and the generic fallback implementation (the native\n    # implementation being faster).\n    #\n    # Since the fallback implementation is not mapped onto the same\n    # polymorphic function in Postgres as the implementation for\n    # supported types, we cannot rely on Postgres to always correctly\n    # pick the polymorphic function to call, instead we use static\n    # type inference to determine whether we'll delegate this to\n    # Postgres (e.g. for anyreal) or we'll use the slower\n    # one-size-fits-all fallback which then gets compiled differently.\n    # In particular this means that when used inside a body of another\n    # polymorphic (anytype) function, the slower generic version of\n    # min/max will be used regardless of the actual concrete input\n    # type.\n\n    expr = ir_set.expr\n    with ctx.subrel() as newctx:\n        ir_arg = expr.args[0].expr\n        dispatch.visit(ir_arg, ctx=newctx)\n\n        arg_ref = pathctx.get_path_value_var(\n            newctx.rel, ir_arg.path_id, env=newctx.env)\n\n        arg_val = output.output_as_value(arg_ref, env=newctx.env)\n\n        if newctx.rel.sort_clause is None:\n            newctx.rel.sort_clause = []\n        newctx.rel.sort_clause.append(\n            pgast.SortBy(\n                node=arg_val,\n                dir=(\n                    pgast.SortAsc\n                    if str(expr.func_shortname) == 'std::min'\n                    else pgast.SortDesc\n                ),\n            ),\n        )\n        newctx.rel.limit_count = pgast.NumericConstant(val='1')\n\n        pathctx.put_path_id_map(newctx.rel, ir_set.path_id, ir_arg.path_id)\n\n    func_rvar = relctx.new_rel_rvar(ir_set, newctx.rel, ctx=ctx)\n    relctx.include_rvar(\n        ctx.rel, func_rvar, ir_set.path_id, pull_namespace=False, ctx=ctx\n    )\n\n    return new_stmt_set_rvar(ir_set, ctx.rel, ctx=ctx)\n\n\n@simple_special_case('std::range')\ndef process_set_as_std_range(\n    expr: irast.FunctionCall,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.BaseExpr:\n    # Generic range constructor implementation\n    #\n    #   std::range(\n    #     lower,\n    #     upper,\n    #     named only inc_lower,\n    #     named only inc_upper,\n    #     named only empty,\n    #   )\n    #\n    #     into\n    #\n    #   case when empty then\n    #     'empty'::<pg_range_type>\n    #   else\n    #     <pg_range_type>(\n    #       lower,\n    #       upper,\n    #       (array[['()', '(]'], ['[)', '[]']])\n    #         [inc_lower::int + 1][inc_upper::int + 1]\n    #     )\n    #   end\n\n    empty = dispatch.compile(expr.args['empty'].expr, ctx=ctx)\n    inc_lower = dispatch.compile(expr.args['inc_lower'].expr, ctx=ctx)\n    inc_upper = dispatch.compile(expr.args['inc_upper'].expr, ctx=ctx)\n    lower = dispatch.compile(expr.args[0].expr, ctx=ctx)\n    upper = dispatch.compile(expr.args[1].expr, ctx=ctx)\n\n    lb = pgast.Index(\n        idx=astutils.new_binop(\n            lexpr=pgast.TypeCast(\n                arg=inc_lower,\n                type_name=pgast.TypeName(name=('int4',)),\n            ),\n            op='+',\n            rexpr=pgast.NumericConstant(val='1'),\n        ),\n    )\n\n    rb = pgast.Index(\n        idx=astutils.new_binop(\n            lexpr=pgast.TypeCast(\n                arg=inc_upper,\n                type_name=pgast.TypeName(name=('int4',)),\n            ),\n            op='+',\n            rexpr=pgast.NumericConstant(val='1'),\n        ),\n    )\n\n    bounds_matrix = pgast.ArrayExpr(\n        elements=[\n            pgast.ArrayDimension(\n                elements=[\n                    pgast.StringConstant(val=\"()\"),\n                    pgast.StringConstant(val=\"(]\"),\n                ],\n            ),\n            pgast.ArrayDimension(\n                elements=[\n                    pgast.StringConstant(val=\"[)\"),\n                    pgast.StringConstant(val=\"[]\"),\n                ],\n            ),\n        ]\n    )\n\n    bounds = pgast.Indirection(arg=bounds_matrix, indirection=[lb, rb])\n    pg_type = pg_types.pg_type_from_ir_typeref(expr.typeref)\n    non_empty_range = pgast.FuncCall(name=pg_type, args=[lower, upper, bounds])\n    empty_range = pgast.TypeCast(\n        arg=pgast.StringConstant(val='empty'),\n        type_name=pgast.TypeName(name=pg_type),\n    )\n\n    # If any of the non-optional arguments are nullable, add an explicit\n    # null check for them.\n    null_checks = [\n        pgast.NullTest(arg=e) for e in [empty, inc_upper, inc_lower]\n        if e.nullable\n    ]\n    if null_checks:\n        null_case = [\n            pgast.CaseWhen(\n                expr=astutils.extend_binop(None, *null_checks, op='OR'),\n                result=pgast.NullConstant(),\n            )\n        ]\n    else:\n        null_case = []\n\n    set_expr = pgast.CaseExpr(\n        args=[\n            *null_case,\n            pgast.CaseWhen(\n                expr=pgast.FuncCall(\n                    name=astutils.edgedb_func('range_validate', ctx=ctx),\n                    args=[lower, upper, inc_lower, inc_upper, empty],\n                ),\n                result=empty_range,\n            ),\n        ],\n        defresult=non_empty_range,\n    )\n\n    return set_expr\n\n\n@simple_special_case('std::_is_exclusive')\ndef process_set_as_std_is_exclusive(\n    expr: irast.FunctionCall,\n    *,\n    ctx: context.CompilerContextLevel,\n) -> pgast.BaseExpr:\n    # `std::_is_exclusive` is a helper function used in the implementation of\n    # exclusive constraints. It is removed before (ir->sql) compilation and will\n    # never be executed by the server.\n    #\n    # However, during the (ql->ir) compilation, an additional (ir->sql)\n    # compilation takes place in order to catch any potential downstream errors,\n    # such as set returning functions.\n    #\n    # This simple special case is used to prevent unwanted errors during this\n    # additional (ir->sql) compilation.\n    return pgast.BooleanConstant(val=False)\n\n\n@_special_case('std::multirange', only_as_fallback=True)\ndef process_set_as_std_multirange(\n    ir_set: irast.SetE[irast.Call],\n    *,\n    ctx: context.CompilerContextLevel,\n) -> SetRVars:\n    # Generic multirange constructor implementation\n    #\n    #   std::multirange(\n    #     ranges: array<range<anypoint>>,\n    #   )\n    #\n    #     into\n    #\n    #   <pg_range_type>(variadic ranges)\n    expr = ir_set.expr\n\n    ranges = dispatch.compile(expr.args[0].expr, ctx=ctx)\n    pg_type = pg_types.pg_type_from_ir_typeref(expr.typeref)\n    set_expr = pgast.FuncCall(\n        name=pg_type,\n        args=[pgast.VariadicArgument(expr=ranges)]\n    )\n\n    pathctx.put_path_value_var(ctx.rel, ir_set.path_id, set_expr)\n\n    return new_stmt_set_rvar(ir_set, ctx.rel, ctx=ctx)\n\n\n@register_get_rvar(irast.Call)\ndef process_set_as_call(\n    ir_set: irast.SetE[irast.Call], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    fname = str(ir_set.expr.func_shortname)\n    if (func := _SPECIAL_FUNCTIONS.get(fname)) and (\n        not func.only_as_fallback or ir_set.expr.func_sql_expr\n    ):\n        return func.func(ir_set, ctx=ctx)\n\n    # Route simple special functions through expr compilation\n    if fname in _SIMPLE_SPECIAL_FUNCTIONS:\n        return process_set_as_expr(ir_set, ctx=ctx)\n\n    if irutils.is_set_instance(ir_set, irast.OperatorCall):\n        # Operator call\n        return process_set_as_oper_expr(ir_set, ctx=ctx)\n\n    assert irutils.is_set_instance(ir_set, irast.FunctionCall)\n\n    if any(\n        arg.param_typemod is qltypes.TypeModifier.SetOfType\n        for key, arg in ir_set.expr.args.items()\n    ):\n        # Call to an aggregate function.\n        return process_set_as_agg_expr(ir_set, ctx=ctx)\n\n    # Regular function call.\n    return process_set_as_func_expr(ir_set, ctx=ctx)\n\n\n@dataclasses.dataclass\nclass _FuncWithOrdinalityInfo:\n    rvar: pgast.BaseRangeVar\n    colnames: list[str]\n    inner_expr: pgast.OutputVar\n    arg_is_tuple: bool\n    nullable: bool\n\n\ndef _process_typical_set_func_with_ordinality(\n    ir_set: irast.Set,\n    *,\n    outer_func_set: irast.Set,\n    func_name: tuple[str, ...],\n    args: list[pgast.BaseExpr],\n    ctx: context.CompilerContextLevel\n) -> _FuncWithOrdinalityInfo:\n    expr = ir_set.expr\n    assert isinstance(expr, irast.FunctionCall)\n    rtype = outer_func_set.typeref\n    outer_func_expr = outer_func_set.expr\n    assert isinstance(outer_func_expr, irast.FunctionCall)\n\n    inner_rtype = ir_set.typeref\n\n    coldeflist = []\n    arg_is_tuple = irtyputils.is_tuple(inner_rtype)\n\n    if arg_is_tuple:\n        subtypes = {}\n        for i, st in enumerate(inner_rtype.subtypes):\n            colname = st.element_name or f'_t{i + 1}'\n            subtypes[colname] = st\n            coldeflist.append(\n                pgast.ColumnDef(\n                    name=colname,\n                    typename=pgast.TypeName(\n                        name=pg_types.pg_type_from_ir_typeref(st)\n                    )\n                )\n            )\n\n        colnames = list(subtypes)\n\n    else:\n        colnames = [ctx.env.aliases.get('v')]\n        coldeflist = []\n\n    if (expr.sql_func_has_out_params\n            or irtyputils.is_persistent_tuple(inner_rtype)):\n        # SQL functions declared with OUT params reject column definitions.\n        # Also persistent tuple types\n        coldeflist = []\n\n    fexpr = pgast.FuncCall(name=func_name, args=args, coldeflist=coldeflist)\n\n    colnames.append(\n        rtype.subtypes[0].element_name or '_i'\n    )\n\n    func_rvar = pgast.RangeFunction(\n        alias=pgast.Alias(\n            aliasname=ctx.env.aliases.get('f'),\n            colnames=colnames),\n        lateral=True,\n        is_rowsfrom=True,\n        with_ordinality=True,\n        functions=[fexpr])\n\n    ctx.rel.from_clause.append(func_rvar)\n\n    inner_expr: pgast.OutputVar\n\n    if arg_is_tuple:\n        inner_named_tuple = any(st.element_name for st in inner_rtype.subtypes)\n        inner_expr = pgast.TupleVar(\n            elements=[\n                pgast.TupleElement(\n                    path_id=outer_func_expr.tuple_path_ids[\n                        len(rtype.subtypes) + i],\n                    name=n,\n                    val=astutils.get_column(\n                        func_rvar, n, nullable=fexpr.nullable)\n                )\n                for i, n in enumerate(colnames[:-1])\n            ],\n            named=inner_named_tuple,\n            typeref=inner_rtype,\n        )\n    else:\n        inner_expr = astutils.get_column(\n            func_rvar, colnames[0], nullable=fexpr.nullable)\n\n    return _FuncWithOrdinalityInfo(\n        rvar=func_rvar,\n        colnames=colnames,\n        inner_expr=inner_expr,\n        arg_is_tuple=arg_is_tuple,\n        nullable=bool(fexpr.nullable),\n    )\n\n\ndef _process_nested_array_set_func_with_ordinality(\n    ir_set: irast.Set,\n    *,\n    outer_func_set: irast.Set,\n    args: list[pgast.BaseExpr],\n    ctx: context.CompilerContextLevel\n) -> _FuncWithOrdinalityInfo:\n    # array<array<...>> is implemented as array<tuple<array<...>>>\n    #\n    # If we are unnesting with ordinality, the ordinality is paired with\n    # the wrapping tuple. We need to unpack the tuple and re-pair the\n    # ordinality with the array.\n\n    expr = ir_set.expr\n    assert isinstance(expr, irast.FunctionCall)\n    outer_func_expr = outer_func_set.expr\n    assert isinstance(outer_func_expr, irast.FunctionCall)\n\n    inner_rtype = ir_set.typeref\n\n    colnames = [ctx.env.aliases.get('v'), '_i']\n    alias = pgast.Alias(\n        aliasname=ctx.env.aliases.get('f'),\n        colnames=colnames,\n    )\n\n    # Get (ordinal, tuple<array<...>>)\n    # - unnests the outer array with ordinal\n    # - select the resulting ordinal and tuple<array<...>>\n    ordinal_tuple_expr = pgast.SelectStmt(\n        target_list=[\n            pgast.ResTarget(\n                val=pgast.ColumnRef(name=[colname]),\n                name=colname\n            )\n            for colname in colnames\n        ],\n        from_clause=[\n            pgast.RangeFunction(\n                alias=alias,\n                functions=[\n                    pgast.FuncCall(\n                        name=('unnest',),\n                        args=[args[0]],\n                        coldeflist=[\n                            pgast.ColumnDef(\n                                name='0',\n                                typename=pgast.TypeName(\n                                    name=pg_types.pg_type_from_ir_typeref(\n                                        inner_rtype\n                                    )\n                                )\n                            )\n                        ]\n                    )\n                ],\n                lateral=True,\n                is_rowsfrom=True,\n                with_ordinality=True,\n            )\n        ]\n    )\n    ordinal_tuple_rvar = relctx.rvar_for_rel(\n        ordinal_tuple_expr,\n        lateral=True,\n        ctx=ctx,\n    )\n\n    # Get (ordinal, array<...>)\n    inner_array_expr = pgast.CoalesceExpr(\n        args=[\n            astutils.get_column(ordinal_tuple_rvar, colnames[0]),\n            pgast.TypeCast(\n                arg=pgast.ArrayExpr(elements=[]),\n                type_name=pgast.TypeName(\n                    name=pg_types.pg_type_from_ir_typeref(\n                        inner_rtype\n                    )\n                ),\n            ),\n        ]\n    )\n    ordinal_expr = astutils.get_column(ordinal_tuple_rvar, colnames[-1])\n    ordinal_array_expr = pgast.SelectStmt(\n        target_list=[\n            pgast.ResTarget(\n                val=inner_array_expr,\n                name=colnames[0],\n            ),\n            pgast.ResTarget(\n                val=ordinal_expr,\n                name=colnames[-1],\n            ),\n        ],\n        from_clause=[\n            ordinal_tuple_rvar,\n        ]\n    )\n\n    func_rvar = relctx.rvar_for_rel(\n        ordinal_array_expr,\n        lateral=True,\n        ctx=ctx,\n    )\n    ctx.rel.from_clause.append(func_rvar)\n\n    return _FuncWithOrdinalityInfo(\n        rvar=func_rvar,\n        colnames=colnames,\n        inner_expr=astutils.get_column(func_rvar, colnames[0]),\n        arg_is_tuple=False,\n        nullable=True,\n    )\n\n\ndef _process_set_func_with_ordinality(\n    ir_set: irast.Set,\n    *,\n    outer_func_set: irast.Set,\n    func_name: tuple[str, ...],\n    args: list[pgast.BaseExpr],\n    ctx: context.CompilerContextLevel\n) -> pgast.BaseExpr:\n    expr = ir_set.expr\n    assert isinstance(expr, irast.FunctionCall)\n    rtype = outer_func_set.typeref\n    outer_func_expr = outer_func_set.expr\n    assert isinstance(outer_func_expr, irast.FunctionCall)\n\n    func_info: _FuncWithOrdinalityInfo\n    if (\n        func_name == ('unnest',)\n        and irtyputils.is_array(ir_set.typeref)\n    ):\n        func_info = _process_nested_array_set_func_with_ordinality(\n            ir_set,\n            outer_func_set=outer_func_set,\n            args=args,\n            ctx=ctx,\n        )\n    else:\n        func_info = _process_typical_set_func_with_ordinality(\n            ir_set,\n            outer_func_set=outer_func_set,\n            func_name=func_name,\n            args=args,\n            ctx=ctx,\n        )\n\n    named_tuple = any(st.element_name for st in rtype.subtypes)\n\n    set_expr = pgast.TupleVar(\n        elements=[\n            pgast.TupleElement(\n                path_id=outer_func_expr.tuple_path_ids[0],\n                name=func_info.colnames[0],\n                val=pgast.Expr(\n                    name='-',\n                    lexpr=astutils.get_column(\n                        func_info.rvar,\n                        func_info.colnames[-1],\n                        nullable=func_info.nullable,\n                    ),\n                    rexpr=pgast.NumericConstant(val='1')\n                )\n            ),\n            pgast.TupleElement(\n                path_id=outer_func_expr.tuple_path_ids[1],\n                name=rtype.subtypes[1].element_name or '1',\n                val=func_info.inner_expr,\n            ),\n        ],\n        named=named_tuple,\n        typeref=outer_func_set.typeref,\n    )\n\n    for element in set_expr.elements:\n        pathctx.put_path_value_var(ctx.rel, element.path_id, element.val)\n\n    if func_info.arg_is_tuple:\n        arg_tuple = set_expr.elements[1].val\n        assert isinstance(arg_tuple, pgast.TupleVar)\n        for element in arg_tuple.elements:\n            pathctx.put_path_value_var(ctx.rel, element.path_id, element.val)\n\n    # If there is a shape specified on the argument to enumerate, we need\n    # to compile it here manually, since we are skipping the normal\n    # code path for it.\n    if (output.in_serialization_ctx(ctx) and ir_set.shape\n            and not ctx.env.ignore_object_shapes):\n        ensure_source_rvar(ir_set, ctx.rel, ctx=ctx)\n        exprcomp._compile_shape(ir_set, ir_set.shape, ctx=ctx)\n\n    var = pathctx.maybe_get_path_var(\n        ctx.rel,\n        ir_set.path_id,\n        aspect=pgce.PathAspect.SERIALIZED,\n        env=ctx.env,\n    )\n    if var is not None:\n        pathctx.put_path_var(\n            ctx.rel,\n            set_expr.elements[1].path_id,\n            var,\n            aspect=pgce.PathAspect.SERIALIZED,\n        )\n\n    return set_expr\n\n\ndef _process_set_func(\n    ir_set: irast.Set,\n    *,\n    func_name: tuple[str, ...],\n    args: list[pgast.BaseExpr],\n    ctx: context.CompilerContextLevel,\n) -> pgast.BaseExpr:\n    expr = ir_set.expr\n    assert isinstance(expr, irast.FunctionCall)\n\n    rtype = expr.typeref\n    named_tuple = any(st.element_name for st in rtype.subtypes)\n    coldeflist = []\n    is_tuple = irtyputils.is_tuple(rtype)\n\n    if is_tuple:\n        subtypes = {}\n        for i, st in enumerate(rtype.subtypes):\n            colname = st.element_name or str(i)\n            subtypes[colname] = st\n            coldeflist.append(\n                pgast.ColumnDef(\n                    name=colname,\n                    typename=pgast.TypeName(\n                        name=pg_types.pg_type_from_ir_typeref(st)\n                    )\n                )\n            )\n\n        colnames = list(subtypes)\n    else:\n        colnames = [ctx.env.aliases.get('v')]\n\n        if _should_unwrap_polymorphic_return_array(expr):\n            # If we are unwrapping a previously nested array, its pg type is\n            # record and so we need to provide a column definition list.\n            coldeflist = [\n                pgast.ColumnDef(\n                    name='v',\n                    typename=pgast.TypeName(\n                        name=pg_types.pg_type_from_ir_typeref(expr.typeref)\n                    )\n                )\n            ]\n\n    if (\n        # SQL functions declared with OUT params or returning\n        # named composite types reject column definitions.\n        irtyputils.is_persistent_tuple(rtype)\n        or expr.sql_func_has_out_params\n    ):\n        coldeflist = []\n\n    fexpr = pgast.FuncCall(name=func_name, args=args, coldeflist=coldeflist)\n\n    func_rvar = pgast.RangeFunction(\n        alias=pgast.Alias(\n            aliasname=ctx.env.aliases.get('f'),\n            colnames=colnames),\n        lateral=True,\n        is_rowsfrom=True,\n        functions=[fexpr])\n\n    ctx.rel.from_clause.append(func_rvar)\n\n    set_expr: pgast.BaseExpr\n\n    if not is_tuple:\n        set_expr = astutils.get_column(\n            func_rvar, colnames[0], nullable=fexpr.nullable)\n    else:\n        set_expr = pgast.TupleVar(\n            elements=[\n                pgast.TupleElement(\n                    path_id=expr.tuple_path_ids[i],\n                    name=n,\n                    val=astutils.get_column(\n                        func_rvar, n, nullable=fexpr.nullable)\n                )\n                for i, n in enumerate(colnames)\n            ],\n            named=named_tuple,\n            typeref=rtype,\n        )\n\n        for element in set_expr.elements:\n            pathctx.put_path_value_var_if_not_exists(\n                ctx.rel, element.path_id, element.val\n            )\n\n    return set_expr\n\n\ndef _compile_func_epilogue(\n        ir_set: irast.Set, *,\n        set_expr: pgast.BaseExpr,\n        func_rel: pgast.SelectStmt,\n        ctx: context.CompilerContextLevel) -> SetRVars:\n    expr = ir_set.expr\n    assert isinstance(expr, irast.FunctionCall)\n\n    if expr.volatility.is_volatile():\n        relctx.apply_volatility_ref(func_rel, ctx=ctx)\n\n    pathctx.put_path_var_if_not_exists(\n        func_rel,\n        ir_set.path_id,\n        set_expr,\n        aspect=pgce.PathAspect.VALUE,\n    )\n\n    aspects: tuple[pgce.PathAspect, ...]\n    if expr.body:\n        # For inlined functions, we want all of the aspects provided.\n        aspects = tuple(pathctx.list_path_aspects(func_rel, ir_set.path_id))\n    else:\n        # Otherwise we just know we have value.\n        aspects = (pgce.PathAspect.VALUE,)\n\n    func_rvar = relctx.new_rel_rvar(ir_set, func_rel, ctx=ctx)\n    relctx.include_rvar(\n        ctx.rel,\n        func_rvar,\n        ir_set.path_id,\n        pull_namespace=False,\n        aspects=aspects,\n        ctx=ctx,\n    )\n\n    if (ir_set.path_id.is_tuple_path()\n            and expr.typemod is qltypes.TypeModifier.SetOfType):\n        # Functions returning a set of tuples are compiled with an\n        # explicit coldeflist, so the result is represented as a\n        # TupleVar as opposed to an opaque record datum, so\n        # we can access the elements directly without using\n        # `tuple_getattr()`.\n        aspects += (pgce.PathAspect.SOURCE,)\n\n    return new_stmt_set_rvar(ir_set, ctx.rel, aspects=aspects, ctx=ctx)\n\n\ndef _needs_arg_null_check(\n    call_expr: irast.Call, ir_arg: irast.CallArg,\n    typemod: qltypes.TypeModifier, *,\n    ctx: context.CompilerContextLevel\n) -> bool:\n    return (\n        not call_expr.impl_is_strict\n        and not ir_arg.is_default\n        and (\n            (\n                typemod == qltypes.TypeModifier.SingletonType\n                and ir_arg.cardinality.can_be_zero()\n            ) or typemod == qltypes.TypeModifier.SetOfType\n        )\n    )\n\n\ndef _compile_arg_null_check(\n    call_expr: irast.Call, ir_arg: irast.CallArg, arg_ref: pgast.BaseExpr,\n    typemod: qltypes.TypeModifier, *,\n    ctx: context.CompilerContextLevel\n) -> None:\n    if (\n        _needs_arg_null_check(call_expr, ir_arg, typemod, ctx=ctx)\n        and arg_ref.nullable\n    ):\n        ctx.rel.where_clause = astutils.extend_binop(\n            ctx.rel.where_clause,\n            pgast.NullTest(arg=arg_ref, negated=True)\n        )\n\n\n# Polymorphic calls need special handling for nested arrays since\n# array<array<...>> is implemented as array<tuple<array<...>>>.\n#\n# Currently 2 cases are handled:\n#   A) At least 1 arg type is `anyarray`\n#   B) Return type is `anyarray`\n#\n# In both cases, any simple polymorphic types will be added into an array in\n# the result. If this parameter is also an array, then it needs to be wrapped\n# in a tuple.\n#\n# In case A and if the return type is anytype, the call is returning the\n# contents of the array. The result needs to be unwrapped to get the actual\n# array.\n\ndef _has_polymorphic_array_arg(\n    expr: irast.Call\n) -> bool:\n    return any(\n        ir_arg.polymorphism == qltypes.Polymorphism.Array\n        for ir_arg in expr.args.values()\n    )\n\n\ndef _should_wrap_polymorphic_array_args(\n    expr: irast.Call\n) -> bool:\n    return (\n        _has_polymorphic_array_arg(expr)\n        or expr.return_polymorphism == qltypes.Polymorphism.Array\n    )\n\n\ndef _is_array_arg_as_simple_polymorphic(\n    arg: irast.CallArg\n) -> bool:\n    return (\n        arg.polymorphism == qltypes.Polymorphism.Simple\n        and irtyputils.is_array(arg.expr.typeref)\n    )\n\n\ndef _should_unwrap_polymorphic_return_array(\n    expr: irast.Call\n) -> bool:\n    return (\n        _has_polymorphic_array_arg(expr)\n        and expr.return_polymorphism == qltypes.Polymorphism.Simple\n        and irtyputils.is_array(expr.typeref)\n    )\n\n\ndef _compile_call_args(\n    ir_set: irast.SetE[irast.Call],\n    *,\n    skip: Collection[int] = (),\n    no_subquery_args: bool = False,\n    ctx: context.CompilerContextLevel,\n) -> list[pgast.BaseExpr]:\n    \"\"\"\n    Compiles function call arguments, whose index is not in `skip`.\n    \"\"\"\n\n    expr = ir_set.expr\n\n    args = []\n\n    if isinstance(expr, irast.FunctionCall) and expr.global_args:\n        for glob_arg in expr.global_args:\n            arg_ref = dispatch.compile(glob_arg, ctx=ctx)\n            args.append(output.output_as_value(arg_ref, env=ctx.env))\n\n    for ir_key, ir_arg in expr.args.items():\n        if ir_key in skip:\n            continue\n        assert ir_arg.multiplicity != qltypes.Multiplicity.UNKNOWN\n\n        typemod = ir_arg.param_typemod\n\n        # Support a mode where we try to compile arguments as pure\n        # subqueries. This is occasionally valuable as it lets us\n        # \"push down\" the subqueries from the top level, which is\n        # important for things like hitting pgvector indexes in an\n        # ORDER BY.\n        arg_typeref = ir_arg.expr.typeref\n        make_subquery = (\n            expr.prefer_subquery_args\n            and typemod != qltypes.TypeModifier.SetOfType\n            and ir_arg.cardinality.is_single()\n            and (arg_typeref.is_scalar or arg_typeref.collection)\n            and not _needs_arg_null_check(expr, ir_arg, typemod, ctx=ctx)\n            and not no_subquery_args\n        )\n\n        if make_subquery:\n            arg_ref = set_as_subquery(ir_arg.expr, as_value=True, ctx=ctx)\n            arg_ref.nullable = ir_arg.cardinality.can_be_zero()\n            arg_ref = astutils.collapse_query(arg_ref)\n        else:\n            arg_ref = dispatch.compile(ir_arg.expr, ctx=ctx)\n            arg_ref = output.output_as_value(arg_ref, env=ctx.env)\n\n        if (\n            _should_wrap_polymorphic_array_args(expr)\n            and _is_array_arg_as_simple_polymorphic(ir_arg)\n        ):\n            arg_ref = pgast.RowExpr(args=[arg_ref])\n\n        args.append(arg_ref)\n        _compile_arg_null_check(expr, ir_arg, arg_ref, typemod, ctx=ctx)\n\n        if (\n            isinstance(expr, irast.FunctionCall)\n            and ir_arg.expr_type_path_id is not None\n        ):\n            # Object type arguments are represented by two\n            # SQL arguments: object id and object type id.\n            # The latter is needed for proper overload\n            # dispatch.\n            ensure_source_rvar(ir_arg.expr, ctx.rel, ctx=ctx)\n            type_ref = relctx.get_path_var(\n                ctx.rel,\n                ir_arg.expr_type_path_id,\n                aspect=pgce.PathAspect.IDENTITY,\n                ctx=ctx,\n            )\n            args.append(type_ref)\n\n    if (\n        isinstance(expr, irast.FunctionCall)\n        and expr.has_empty_variadic\n        and expr.variadic_param_type is not None\n    ):\n        var = pgast.TypeCast(\n            arg=pgast.ArrayExpr(elements=[]),\n            type_name=pgast.TypeName(\n                name=pg_types.pg_type_from_ir_typeref(\n                    expr.variadic_param_type)\n            )\n        )\n\n        args.append(pgast.VariadicArgument(expr=var))\n\n    return args\n\n\ndef _compile_inlined_call_args(\n    ir_set: irast.SetE[irast.FunctionCall], *, ctx: context.CompilerContextLevel\n) -> None:\n    expr = ir_set.expr\n    assert expr.body is not None\n\n    if irutils.contains_dml(expr.body):\n        last_iterator = ctx.enclosing_cte_iterator\n\n        # If this function call has already been compiled to a CTE, don't\n        # recompile the arguments.\n        # (This will happen when a DML-containing funcion in a FOR loop is\n        # WITH bound, for example.)\n        if ir_set.path_id in ctx.inline_dml_ctes:\n            args_pathid, arg_cte = ctx.inline_dml_ctes[ir_set.path_id]\n\n        else:\n            # Compile args into an iterator CTE\n            with ctx.newrel() as arg_ctx:\n                dml.merge_iterator(last_iterator, arg_ctx.rel, ctx=arg_ctx)\n                clauses.setup_iterator_volatility(last_iterator, ctx=arg_ctx)\n\n                _compile_call_args(ir_set, ctx=arg_ctx)\n\n                # Add iterator identity\n                args_pathid = irast.PathId.new_dummy(\n                    ctx.env.aliases.get('args')\n                )\n                with arg_ctx.subrel() as args_pathid_ctx:\n                    relctx.create_iterator_identity_for_path(\n                        args_pathid, args_pathid_ctx.rel, ctx=args_pathid_ctx\n                    )\n                args_id_rvar = relctx.rvar_for_rel(\n                    args_pathid_ctx.rel, lateral=True, ctx=arg_ctx\n                )\n                relctx.include_rvar(\n                    arg_ctx.rel, args_id_rvar, path_id=args_pathid, ctx=arg_ctx\n                )\n\n                for ir_arg in expr.args.values():\n                    arg_path_id = ir_arg.expr.path_id\n                    # Ensure args appear in arg CTE\n                    pathctx.get_path_output(\n                        arg_ctx.rel,\n                        arg_path_id,\n                        aspect=pgce.PathAspect.VALUE,\n                        env=arg_ctx.env,\n                    )\n                    pathctx.put_path_bond(\n                        arg_ctx.rel, arg_path_id, iterator=True\n                    )\n\n            arg_cte = pgast.CommonTableExpr(\n                name=ctx.env.aliases.get('args'),\n                query=arg_ctx.rel,\n                materialized=False,\n            )\n            ctx.toplevel_stmt.append_cte(arg_cte)\n\n            ctx.inline_dml_ctes[ir_set.path_id] = (args_pathid, arg_cte)\n\n        arg_iterator = pgast.IteratorCTE(\n            path_id=args_pathid,\n            cte=arg_cte,\n            parent=last_iterator,\n            other_paths=tuple(\n                (ir_arg.expr.path_id, pgce.PathAspect.VALUE)\n                for ir_arg in expr.args.values()\n            ),\n            iterator_bond=True,\n        )\n\n        # Merge the new iterator\n        ctx.path_scope = ctx.path_scope.new_child()\n        arg_rvar = not_none(\n            dml.merge_iterator(arg_iterator, ctx.rel, ctx=ctx)\n        )\n        clauses.setup_iterator_volatility(arg_iterator, ctx=ctx)\n\n        ctx.enclosing_cte_iterator = arg_iterator\n\n    else:\n        with ctx.subrel() as arg_ctx:\n            # Compile the call args but don't do anything with the resulting\n            # exprs. Their rvar will be found when compiling the inlined body.\n            _compile_call_args(ir_set, ctx=arg_ctx)\n\n            arg_rvar = relctx.rvar_for_rel(arg_ctx.rel, ctx=ctx)\n\n        for ir_arg in expr.args.values():\n            arg_path_id = ir_arg.expr.path_id\n            relctx.include_rvar(\n                ctx.rel,\n                arg_rvar,\n                arg_path_id,\n                ctx=ctx,\n            )\n\n    for ir_arg in expr.args.values():\n        arg_path_id = ir_arg.expr.path_id\n        if arg_scope_stmt := relctx.maybe_get_scope_stmt(\n            arg_path_id, ctx=ctx\n        ):\n            # The args are joined to ctx.rel, but other sets may\n            # look for it in the scope statement. Make sure it's\n            # available.\n            pathctx.put_path_value_rvar(\n                arg_scope_stmt,\n                arg_path_id,\n                arg_rvar,\n            )\n\n\ndef process_set_as_func_enumerate(\n    ir_set: irast.SetE[irast.Call], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    expr = ir_set.expr\n\n    inner_func_set = irutils.unwrap_set(expr.args[0].expr)\n    assert irutils.is_set_instance(inner_func_set, irast.FunctionCall)\n    inner_func = inner_func_set.expr\n\n    with ctx.subrel() as newctx:\n        with newctx.new() as newctx2:\n            newctx2.expr_exposed = False\n            args = _compile_call_args(inner_func_set, ctx=newctx2)\n        func_name = exprcomp.get_func_call_backend_name(inner_func, ctx=newctx)\n\n        set_expr = _process_set_func_with_ordinality(\n            ir_set=inner_func_set,\n            outer_func_set=ir_set,\n            func_name=func_name,\n            args=args,\n            ctx=newctx)\n\n        func_rel = newctx.rel\n\n    return _compile_func_epilogue(\n        ir_set, set_expr=set_expr, func_rel=func_rel, ctx=ctx\n    )\n\n\ndef process_set_as_func_expr(\n    ir_set: irast.SetE[irast.FunctionCall], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    expr = ir_set.expr\n\n    with ctx.subrel() as newctx:\n        newctx.expr_exposed = False\n\n        if expr.body is not None:\n            _compile_inlined_call_args(ir_set, ctx=newctx)\n\n            set_expr = dispatch.compile(expr.body, ctx=newctx)\n            # Map the path id so that we can extract source aspects\n            # from it, which we want so that we can directly select\n            # from an INSERT instead of using overlays.\n            pathctx.put_path_id_map(\n                newctx.rel, ir_set.path_id, expr.body.path_id\n            )\n\n            if _should_unwrap_polymorphic_return_array(expr):\n                set_expr = astutils.array_get_inner_array(\n                    set_expr, expr.typeref\n                )\n\n        else:\n            args = _compile_call_args(ir_set, ctx=newctx)\n\n            name = exprcomp.get_func_call_backend_name(expr, ctx=newctx)\n\n            if expr.typemod is qltypes.TypeModifier.SetOfType:\n                set_expr = _process_set_func(\n                    ir_set,\n                    func_name=name,\n                    args=args,\n                    ctx=newctx,\n                )\n\n            else:\n                set_expr = pgast.FuncCall(name=name, args=args)\n\n                if _should_unwrap_polymorphic_return_array(expr):\n                    set_expr = astutils.array_get_inner_array(\n                        set_expr, expr.typeref\n                    )\n\n        if expr.error_on_null_result:\n            set_expr = pgast.FuncCall(\n                name=astutils.edgedb_func('raise_on_null', ctx=ctx),\n                args=[\n                    set_expr,\n                    pgast.StringConstant(\n                        val='invalid_parameter_value',\n                    ),\n                    pgast.StringConstant(\n                        val=expr.error_on_null_result,\n                    ),\n                    pgast.StringConstant(\n                        val=irutils.get_span_as_json(\n                            expr, errors.InvalidValueError),\n                    ),\n                ]\n            )\n\n        if expr.force_return_cast:\n            # The underlying function has a return value type\n            # different from that of the EdgeQL function declaration,\n            # so we need to make an explicit cast here.\n            set_expr = pgast.TypeCast(\n                arg=set_expr,\n                type_name=pgast.TypeName(\n                    name=pg_types.pg_type_from_ir_typeref(expr.typeref)\n                )\n            )\n\n        func_rel = newctx.rel\n\n    return _compile_func_epilogue(\n        ir_set, set_expr=set_expr, func_rel=func_rel, ctx=ctx\n    )\n\n\ndef process_set_as_agg_expr_inner(\n    ir_set: irast.SetE[irast.FunctionCall],\n    *,\n    aspect: pgce.PathAspect,\n    wrapper: Optional[pgast.SelectStmt],\n    for_group_by: bool = False,\n    ctx: context.CompilerContextLevel,\n) -> SetRVars:\n    expr = ir_set.expr\n    assert isinstance(expr, irast.FunctionCall)\n    stmt = ctx.rel\n\n    set_expr: pgast.BaseExpr\n\n    with ctx.newscope() as newctx:\n        agg_filter = None\n        agg_sort = []\n\n        with newctx.new() as argctx:\n            # We want array_agg() (and similar) to do the right\n            # thing with respect to output format, so, barring\n            # the (unacceptable) hardcoding of function names,\n            # check if the aggregate accepts a single argument\n            # of \"any\" to determine serialized input safety.\n            serialization_safe = (\n                expr.func_polymorphic\n                and aspect == pgce.PathAspect.SERIALIZED\n            )\n\n            if not serialization_safe:\n                argctx.expr_exposed = False\n\n            args = []\n\n            for ir_call_key, ir_call_arg in expr.args.items():\n                ir_arg = ir_call_arg.expr\n\n                arg_ref: pgast.BaseExpr\n                if for_group_by:\n                    arg_ref = set_as_subquery(\n                        ir_arg, as_value=True, ctx=argctx)\n                    arg_ref.nullable = False\n                elif aspect == pgce.PathAspect.SERIALIZED:\n                    dispatch.visit(ir_arg, ctx=argctx)\n\n                    arg_ref = pathctx.get_path_serialized_or_value_var(\n                        argctx.rel, ir_arg.path_id, env=argctx.env)\n\n                    if isinstance(arg_ref, pgast.TupleVar):\n                        arg_ref = output.serialize_expr(\n                            arg_ref, path_id=ir_arg.path_id, env=argctx.env)\n                else:\n                    dispatch.visit(ir_arg, ctx=argctx)\n\n                    arg_ref = pathctx.get_path_value_var(\n                        argctx.rel, ir_arg.path_id, env=argctx.env)\n\n                    if isinstance(arg_ref, pgast.TupleVar):\n                        arg_ref = output.output_as_value(\n                            arg_ref, env=argctx.env)\n\n                _compile_arg_null_check(\n                    expr,\n                    ir_call_arg,\n                    arg_ref,\n                    ir_call_arg.param_typemod,\n                    ctx=argctx\n                )\n\n                path_scope = relctx.get_scope(ir_arg, ctx=argctx)\n                if path_scope is not None and path_scope.parent is not None:\n                    arg_is_visible = path_scope.parent.is_any_prefix_visible(\n                        ir_arg.path_id)\n                else:\n                    arg_is_visible = False\n\n                if arg_is_visible:\n                    # If the argument set is visible above us, we\n                    # are aggregating a singleton set, potentially on\n                    # the same query level, as the source set.\n                    # Postgres doesn't like aggregates on the same query\n                    # level, so wrap the arg ref into a VALUES range.\n                    wrapper = pgast.SelectStmt(\n                        values=[pgast.ImplicitRowExpr(args=[arg_ref])]\n                    )\n                    colname = argctx.env.aliases.get('a')\n                    wrapper_rvar = relctx.rvar_for_rel(\n                        wrapper, lateral=True, colnames=[colname], ctx=argctx)\n                    relctx.include_rvar(\n                        ctx.rel,\n                        wrapper_rvar,\n                        path_id=ir_arg.path_id,\n                        ctx=argctx,\n                    )\n                    arg_ref = astutils.get_column(wrapper_rvar, colname)\n\n                if ir_call_key == 0 and irutils.is_subquery_set(ir_arg):\n                    # If the first argument of the aggregate\n                    # is a SELECT or GROUP with an ORDER BY clause,\n                    # we move the ordering conditions to the aggregate\n                    # call to make sure the ordering is as expected.\n                    substmt = ir_arg.expr\n                    if isinstance(substmt, irast.GroupStmt):\n                        substmt = substmt.result.expr\n\n                    if (isinstance(substmt, irast.SelectStmt) and\n                            substmt.orderby):\n                        qrvar = pathctx.get_path_rvar(\n                            ctx.rel,\n                            ir_arg.path_id,\n                            aspect=pgce.PathAspect.VALUE,\n                        )\n                        query = qrvar.query\n                        assert isinstance(query, pgast.SelectStmt)\n\n                        for i, sortref in enumerate(query.sort_clause or ()):\n                            alias = argctx.env.aliases.get(f's{i}')\n\n                            query.target_list.append(\n                                pgast.ResTarget(\n                                    val=sortref.node,\n                                    name=alias\n                                )\n                            )\n\n                            agg_sort.append(\n                                pgast.SortBy(\n                                    node=astutils.get_column(qrvar, alias),\n                                    dir=sortref.dir,\n                                    nulls=sortref.nulls))\n\n                        query.sort_clause = []\n\n                if (\n                    _should_wrap_polymorphic_array_args(expr)\n                    and _is_array_arg_as_simple_polymorphic(ir_call_arg)\n                ):\n                    # Wrap aggregated arrays in a tuple\n                    arg_ref = pgast.RowExpr(args=[arg_ref])\n\n                    if aspect == pgce.PathAspect.SERIALIZED:\n                        arg_ref = output.serialize_expr(\n                            arg_ref, path_id=ir_arg.path_id, env=argctx.env)\n\n                args.append(arg_ref)\n\n        name = exprcomp.get_func_call_backend_name(expr, ctx=newctx)\n\n        set_expr = pgast.FuncCall(\n            name=name, args=args, agg_order=agg_sort, agg_filter=agg_filter,\n            ser_safe=serialization_safe and all(x.ser_safe for x in args))\n\n        if _should_unwrap_polymorphic_return_array(expr):\n            set_expr = astutils.array_get_inner_array(\n                set_expr, expr.typeref\n            )\n\n        if for_group_by and not expr.impl_is_strict:\n            # If we are doing this for a GROUP BY, and the function is not\n            # strict in its arguments, we are in trouble!\n\n            # The problem is that we don't have a way to filter the NULLs\n            # out in the subquery in general. The value could be\n            # computed *inside* the subquery, so we can't use an agg_filter,\n            # and we can't filter it inside the subquery because it gets\n            # executed separately for each row and collapses to NULL when\n            # it is empty!\n\n            # Fortunately I think that only array_agg has this property,\n            # so we can just handle that by popping the NULLs out.\n            # If other cases turn up, we could handle it by falling\n            # back to aggregate grouping.\n\n            # TODO: only do this when there might really be a null?\n            assert str(expr.func_shortname) == 'std::array_agg'\n            set_expr = pgast.FuncCall(\n                name=('array_remove',),\n                args=[set_expr, pgast.NullConstant()]\n            )\n\n        if expr.error_on_null_result:\n            set_expr = pgast.FuncCall(\n                name=astutils.edgedb_func('raise_on_null', ctx=ctx),\n                args=[\n                    set_expr,\n                    pgast.StringConstant(\n                        val='invalid_parameter_value',\n                    ),\n                    pgast.StringConstant(\n                        val=expr.error_on_null_result,\n                    ),\n                    pgast.StringConstant(\n                        val=irutils.get_span_as_json(\n                            expr, errors.InvalidValueError),\n                    ),\n                ]\n            )\n\n        if expr.force_return_cast:\n            # The underlying function has a return value type\n            # different from that of the EdgeQL function declaration,\n            # so we need to make an explicit cast here.\n            set_expr = pgast.TypeCast(\n                arg=set_expr,\n                type_name=pgast.TypeName(\n                    name=pg_types.pg_type_from_ir_typeref(expr.typeref)\n                )\n            )\n\n    if expr.func_initial_value is not None and wrapper:\n        iv_ir = expr.func_initial_value.expr\n        assert iv_ir is not None\n\n        if serialization_safe and aspect == pgce.PathAspect.SERIALIZED:\n            # Serialization has changed the output type.\n            with ctx.new() as ivctx:\n                iv = dispatch.compile(iv_ir, ctx=ivctx)\n\n                iv = output.serialize_expr_if_needed(\n                    iv, path_id=ir_set.path_id, ctx=ctx)\n                set_expr = output.serialize_expr_if_needed(\n                    set_expr, path_id=ir_set.path_id, ctx=ctx)\n        else:\n            with ctx.new() as ivctx:\n                iv = dispatch.compile(iv_ir, ctx=ivctx)\n\n        pathctx.put_path_var(stmt, ir_set.path_id, set_expr, aspect=aspect)\n        out = pathctx.get_path_output(\n            stmt, ir_set.path_id, aspect=aspect, env=ctx.env\n        )\n        assert isinstance(out, pgast.ColumnRef)\n\n        # HACK: We select join in the inner statement instead of just\n        # using it as a subquery to work around a postgres bug that\n        # occurs when something defined with a subquery is used as an\n        # argument to `grouping`. See #3844.\n        stmt_rvar = relctx.rvar_for_rel(stmt, ctx=ctx)\n        wrapper.from_clause.append(stmt_rvar)\n        val = astutils.get_column(stmt_rvar, out)\n\n        assert wrapper\n        set_expr = pgast.CoalesceExpr(\n            args=[val, iv], ser_safe=serialization_safe)\n\n        pathctx.put_path_var(wrapper, ir_set.path_id, set_expr, aspect=aspect)\n        stmt = wrapper\n\n    pathctx.put_path_var_if_not_exists(\n        stmt, ir_set.path_id, set_expr, aspect=aspect\n    )\n    # Cheat a little bit: as discussed above, pretend the serialized\n    # value is also really a value. Eta-expansion should ensure this\n    # only happens when we don't really need the value again.\n    if aspect == pgce.PathAspect.SERIALIZED:\n        pathctx.put_path_var_if_not_exists(\n            stmt, ir_set.path_id, set_expr, aspect=pgce.PathAspect.VALUE\n        )\n\n    return new_stmt_set_rvar(ir_set, stmt, ctx=ctx)\n\n\ndef process_set_as_agg_expr(\n    ir_set: irast.SetE[irast.FunctionCall], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    # If the func has an initial val, we need to do the interesting\n    # work in subrels and provide a wrapper to put the coalesces in\n    wrapper = None\n    if ir_set.expr.func_initial_value is not None:\n        wrapper = ctx.rel\n\n    # In a serialization context that produces something containing an object,\n    # we produce *only* a serialized value, and we claim it is the value too.\n    # For this to be correct, we need to only have serialized agg expr results\n    # in cases where value can't be used anymore. Our eta-expansion pass\n    # make sure this happens.\n    # (... the only such *function* currently is array_agg.)\n\n    # Though if the result type contains no objects, the value should be good\n    # enough, so don't generate a bunch of unnecessary code to produce\n    # a serialized value when we can use value.\n    serialized = (\n        output.in_serialization_ctx(ctx=ctx)\n        and irtyputils.contains_object(ir_set.typeref)\n    )\n\n    cctx = ctx.subrel() if wrapper else ctx.new()\n    with cctx as xctx:\n        xctx.expr_exposed = serialized\n        aspect = (\n            pgce.PathAspect.SERIALIZED\n            if serialized else\n            pgce.PathAspect.VALUE\n        )\n        process_set_as_agg_expr_inner(\n            ir_set, aspect=aspect, wrapper=wrapper, ctx=xctx\n        )\n\n    return new_stmt_set_rvar(ir_set, ctx.rel, ctx=ctx)\n\n\n@_special_case('std::EXISTS')\ndef process_set_as_exists_expr(\n    ir_set: irast.SetE[irast.Call], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    expr = ir_set.expr\n    with ctx.subrel() as subctx:\n        wrapper = subctx.rel\n        subctx.expr_exposed = False\n        ir_expr = expr.args[0].expr\n        set_ref = dispatch.compile(ir_expr, ctx=subctx)\n\n        pathctx.put_path_value_var(wrapper, ir_set.path_id, set_ref)\n        pathctx.get_path_value_output(wrapper, ir_set.path_id, env=ctx.env)\n\n        wrapper.where_clause = astutils.extend_binop(\n            wrapper.where_clause, pgast.NullTest(arg=set_ref, negated=True))\n\n        set_expr = pgast.SubLink(operator=\"EXISTS\", expr=wrapper)\n\n    pathctx.put_path_value_var(ctx.rel, ir_set.path_id, set_expr)\n    return new_stmt_set_rvar(ir_set, ctx.rel, ctx=ctx)\n\n\n@_special_case('std::json_object_pack')\ndef process_set_as_json_object_pack(\n    ir_set: irast.SetE[irast.Call], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    ir_arg = ir_set.expr.args[0].expr\n\n    # compile IR to pg AST\n    dispatch.visit(ir_arg, ctx=ctx)\n    arg_val = pathctx.get_path_value_var(ctx.rel, ir_arg.path_id, env=ctx.env)\n\n    # get first and the second fields of the tuple\n    if isinstance(arg_val, pgast.TupleVar):\n        keys = arg_val.elements[0].val\n        values = arg_val.elements[1].val\n    else:\n        keys = astutils.tuple_getattr(arg_val, ir_arg.typeref, \"0\")\n        values = astutils.tuple_getattr(arg_val, ir_arg.typeref, \"1\")\n\n    # construct the function call\n    set_expr = pgast.FuncCall(\n        name=(\"coalesce\",),\n        args=[\n            pgast.FuncCall(name=(\"jsonb_object_agg\",), args=[keys, values]),\n            pgast.TypeCast(\n                arg=pgast.StringConstant(val=\"{}\"),\n                type_name=pgast.TypeName(name=('jsonb',)),\n            ),\n        ],\n    )\n\n    # declare that the 'aspect=value' of ir_set (original set)\n    # can be found by in ctx.rel, by using set_expr\n    pathctx.put_path_value_var_if_not_exists(ctx.rel, ir_set.path_id, set_expr)\n\n    # return subquery as set_rvar\n    return new_stmt_set_rvar(ir_set, ctx.rel, ctx=ctx)\n\n\ndef build_array_expr(\n        ir_expr: irast.Base,\n        elements: list[pgast.BaseExpr], *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n\n    array = astutils.safe_array_expr(elements, ctx=ctx)\n\n    if irutils.is_empty_array_expr(ir_expr):\n        assert isinstance(ir_expr, irast.Array)\n        typeref = ir_expr.typeref\n\n        if irtyputils.is_any(typeref.subtypes[0]):\n            # The type of the input is not determined, which means that\n            # the result of this expression is passed as an argument\n            # to a generic function, e.g. `count(array_agg({}))`.  In this\n            # case, amend the array type to a concrete type,\n            # since Postgres balks at `[]::anyarray`.\n            pg_type: tuple[str, ...] = ('text[]',)\n        else:\n            serialized = output.in_serialization_ctx(ctx=ctx)\n            pg_type = pg_types.pg_type_from_ir_typeref(\n                typeref, serialized=serialized)\n\n        return pgast.TypeCast(\n            arg=array,\n            type_name=pgast.TypeName(\n                name=pg_type,\n            ),\n        )\n    else:\n        return array\n\n\n@register_get_rvar(irast.Array)\ndef process_set_as_array_expr(\n    ir_set: irast.SetE[irast.Array], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    expr = ir_set.expr\n\n    elements = []\n    s_elements = []\n    serializing = (\n        output.in_serialization_ctx(ctx=ctx)\n        and irtyputils.contains_object(ir_set.typeref)\n    )\n\n    for ir_element in expr.elements:\n        element = dispatch.compile(ir_element, ctx=ctx)\n        if irtyputils.is_array(ir_element.typeref):\n            # Wrap nested arrays in a tuple\n            element = pgast.RowExpr(args=[element])\n        elements.append(element)\n\n        if serializing:\n            s_var: Optional[pgast.BaseExpr]\n\n            s_var = pathctx.maybe_get_path_serialized_var(\n                ctx.rel, ir_element.path_id, env=ctx.env\n            )\n\n            if s_var is None:\n                v_var = pathctx.get_path_value_var(\n                    ctx.rel, ir_element.path_id, env=ctx.env\n                )\n                s_var = output.serialize_expr(\n                    v_var, path_id=ir_element.path_id, env=ctx.env)\n            elif isinstance(s_var, pgast.TupleVar):\n                s_var = output.serialize_expr(\n                    s_var, path_id=ir_element.path_id, env=ctx.env)\n\n            s_elements.append(s_var)\n\n    if serializing:\n        set_expr = astutils.safe_array_expr(\n            s_elements, ser_safe=all(x.ser_safe for x in s_elements), ctx=ctx)\n\n        if irutils.is_empty_array_expr(expr):\n            set_expr = pgast.TypeCast(\n                arg=set_expr,\n                type_name=pgast.TypeName(\n                    name=pg_types.pg_type_from_ir_typeref(expr.typeref)\n                )\n            )\n\n        pathctx.put_path_serialized_var(ctx.rel, ir_set.path_id, set_expr)\n    else:\n        set_expr = build_array_expr(expr, elements, ctx=ctx)\n\n    pathctx.put_path_value_var_if_not_exists(ctx.rel, ir_set.path_id, set_expr)\n\n    return new_stmt_set_rvar(ir_set, ctx.rel, ctx=ctx)\n\n\ndef process_encoded_param(\n        param: irast.Param, *,\n        ctx: context.CompilerContextLevel) -> pgast.BaseExpr:\n\n    assert param.sub_params\n    decoder = param.sub_params.decoder_ir\n    assert decoder\n\n    if (param_cte := ctx.param_ctes.get(param.name)) is None:\n        with ctx.newrel() as sctx:\n            sctx.pending_query = sctx.rel\n            sctx.rel_overlays = context.RelOverlays()\n            arg_ref = dispatch.compile(decoder, ctx=sctx)\n\n            # Force it into a real tuple so we can just always grab it\n            # from a subquery below.\n            arg_val = output.output_as_value(arg_ref, env=sctx.env)\n            pathctx.put_path_value_var(\n                sctx.rel, decoder.path_id, arg_val, force=True\n            )\n\n            param_cte = pgast.CommonTableExpr(\n                name=ctx.env.aliases.get('p'),\n                query=sctx.rel,\n                materialized=False,\n            )\n            ctx.param_ctes[param.name] = param_cte\n\n    with ctx.subrel() as sctx:\n        cte_rvar = pgast.RelRangeVar(\n            relation=param_cte,\n            typeref=decoder.typeref,\n            alias=pgast.Alias(aliasname=ctx.env.aliases.get('t'))\n        )\n        relctx.include_rvar(\n            sctx.rel, cte_rvar, decoder.path_id, pull_namespace=False,\n            aspects=(pgce.PathAspect.VALUE,), ctx=sctx,\n        )\n        pathctx.get_path_value_output(sctx.rel, decoder.path_id, env=ctx.env)\n        if not param.required:\n            sctx.rel.nullable = True\n\n    return sctx.rel\n\n\n_ObjectSearchInnerCallback = Callable[\n    [\n        irast.Call,\n        irast.PathId,\n        list[pgast.BaseExpr],\n        context.CompilerContextLevel,\n        context.CompilerContextLevel,\n        context.CompilerContextLevel,\n    ],\n    tuple[pgast.BaseExpr, Optional[pgast.BaseExpr]],\n]\n\n\n@_special_case('std::fts::search')\ndef process_set_as_fts_search(\n    ir_set: irast.SetE[irast.Call], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    from edb.common import debug\n\n    cb: _ObjectSearchInnerCallback\n    if debug.flags.zombodb:\n        cb = _fts_search_inner_zombo\n    else:\n        cb = _fts_search_inner_pg\n\n    return _process_set_as_object_search(\n        ir_set, inner_cb=cb, ctx=ctx)\n\n\n@_special_case('ext::ai::search')\ndef process_set_as_ext_ai_search(\n    ir_set: irast.SetE[irast.Call], *, ctx: context.CompilerContextLevel\n) -> SetRVars:\n    cb = _ext_ai_search_inner_pgvector\n    return _process_set_as_object_search(\n        ir_set, inner_cb=cb, ctx=ctx)\n\n\ndef _ext_ai_search_inner_pgvector(\n    call: irast.Call,\n    obj_id: irast.PathId,\n    args_pg: list[pgast.BaseExpr],\n    _ctx: context.CompilerContextLevel,\n    newctx: context.CompilerContextLevel,\n    _inner_ctx: context.CompilerContextLevel,\n) -> tuple[pgast.BaseExpr, Optional[pgast.BaseExpr]]:\n    assert isinstance(call, irast.FunctionCall)\n    if call.extras is None:\n        raise AssertionError(\n            \"missing expected index metadata in FunctionCall.extras\")\n    index_metadata = call.extras.get(\"index_metadata\")\n    if index_metadata is None:\n        raise AssertionError(\n            \"missing expected index metadata in FunctionCall.extras\")\n    tgt = obj_id.target\n    if tgt.material_type is not None:\n        tgt = tgt.material_type\n    target_index_metadata = index_metadata.get(tgt)\n    if target_index_metadata is None:\n        raise AssertionError(\n            \"missing expected index metadata in FunctionCall.extras\")\n    index_id = target_index_metadata.get(\"id\")\n    if index_id is None:\n        raise AssertionError(\n            \"missing expected index metadata in FunctionCall.extras\")\n    dimensions = target_index_metadata.get(\"dimensions\")\n    if dimensions is None:\n        raise AssertionError(\n            \"missing expected index metadata in FunctionCall.extras\")\n    df = target_index_metadata.get(\"distance_function\")\n    if index_id is None:\n        raise AssertionError(\n            \"missing expected index metadata in FunctionCall.extras\")\n\n    query, = args_pg\n    el_name = sn.QualName(\n        '__object__',\n        f'__ext_ai_{index_id}_embedding__',\n    )\n    embedding_ptrref = irast.SpecialPointerRef(\n        name=el_name,\n        shortname=el_name,\n        out_source=obj_id.target,\n        out_target=pg_types.pg_tsvector_typeref,\n        out_cardinality=qltypes.Cardinality.AT_MOST_ONE,\n    )\n    embedding_id = obj_id.extend(ptrref=embedding_ptrref)\n    embedding = relctx.get_path_var(\n        newctx.rel,\n        embedding_id,\n        aspect=pgce.PathAspect.VALUE,\n        ctx=newctx,\n    )\n\n    similarity = pgast.FuncCall(\n        name=common.get_function_backend_name(*df),\n        args=[\n            embedding,\n            pgast.TypeCast(\n                arg=query,\n                type_name=pgast.TypeName(\n                    name=('edgedb', f'vector({dimensions})'),\n                ),\n            ),\n        ],\n    )\n\n    # Install the filter directly in newctx.rel. We could return it\n    # and have it put in inner_ctx.rel, and that does seem to work,\n    # but seems weirder.\n    valid = pgast.NullTest(arg=embedding, negated=True)\n    newctx.rel.where_clause = astutils.extend_binop(\n        newctx.rel.where_clause, valid\n    )\n\n    # Do an integrated sort. This ensures we can hit the index, and is\n    # more ergonomic anyway. Having the ORDER BY operate directly on\n    # the function call is not the *only* way to have it work, but it\n    # is the most reliable.\n    sort_by = pgast.SortBy(\n        node=similarity,\n        dir=qlast.SortOrder.Asc,\n        nulls=qlast.NonesOrder.Last,\n    )\n    if newctx.rel.sort_clause is None:\n        newctx.rel.sort_clause = []\n    newctx.rel.sort_clause.append(sort_by)\n\n    return similarity, None\n\n\ndef _process_set_as_object_search(\n    ir_set: irast.SetE[irast.Call],\n    *,\n    inner_cb: _ObjectSearchInnerCallback,\n    ctx: context.CompilerContextLevel,\n) -> SetRVars:\n    func_call = ir_set.expr\n\n    # We skip the object, as it has to be compiled as rvar source.\n    #\n    # Also, disable subquery args. ai::search needs it for its\n    # scoping effects, but we don't need to use it here, since\n    # it can cause the ai search to duplicate arguments.\n    args_pg = _compile_call_args(\n        ir_set, skip={0}, no_subquery_args=True, ctx=ctx)\n\n    with ctx.subrel() as newctx:\n        newctx.expr_exposed = False\n\n        obj_ir = func_call.args[0].expr\n        obj_id = obj_ir.path_id\n        obj_rvar = ensure_source_rvar(obj_ir, newctx.rel, ctx=newctx)\n\n        out_obj_id, out_score_id = func_call.tuple_path_ids\n\n        with newctx.subrel() as inner_ctx:\n            # inner_ctx generates the `SELECT score WHERE test` relation\n            score_pg, where_clause = inner_cb(\n                func_call,\n                obj_id,\n                args_pg,\n                ctx,\n                newctx,\n                inner_ctx,\n            )\n\n            pathctx.put_path_var(\n                inner_ctx.rel,\n                out_score_id,\n                score_pg,\n                aspect=pgce.PathAspect.VALUE,\n            )\n\n            if where_clause is not None:\n                inner_ctx.rel.where_clause = astutils.extend_binop(\n                    inner_ctx.rel.where_clause, where_clause\n                )\n\n            in_rvar = relctx.new_rel_rvar(ir_set, inner_ctx.rel, ctx=newctx)\n            relctx.include_rvar(\n                newctx.rel,\n                in_rvar,\n                out_score_id,\n                aspects={pgce.PathAspect.VALUE},\n                ctx=newctx,\n            )\n\n        obj_id_pg_ref = pathctx.get_rvar_path_var(\n            obj_rvar,\n            obj_id,\n            aspect=pgce.PathAspect.VALUE,\n            env=newctx.env,\n        )\n        score_pg_ref = pathctx.get_path_var(\n            newctx.rel,\n            out_score_id,\n            aspect=pgce.PathAspect.VALUE,\n            env=newctx.env,\n        )\n\n        tuple_expr = pgast.TupleVar(\n            elements=[\n                pgast.TupleElement(\n                    path_id=out_obj_id,\n                    name='object',\n                    val=obj_id_pg_ref,\n                ),\n                pgast.TupleElement(\n                    path_id=out_score_id,\n                    name='score',\n                    val=score_pg_ref,\n                ),\n            ],\n            named=True,\n            typeref=ir_set.typeref,\n        )\n\n        pathctx.put_path_var(\n            newctx.rel,\n            ir_set.path_id,\n            tuple_expr,\n            aspect=pgce.PathAspect.VALUE,\n        )\n\n        var = pathctx.maybe_get_path_var(\n            newctx.rel,\n            obj_id,\n            aspect=pgce.PathAspect.SERIALIZED,\n            env=newctx.env,\n        )\n        if var is not None:\n            pathctx.put_path_var(\n                newctx.rel,\n                out_obj_id,\n                var,\n                aspect=pgce.PathAspect.SERIALIZED,\n            )\n\n    pathctx.put_path_id_map(newctx.rel, out_obj_id, obj_id)\n\n    aspects = {pgce.PathAspect.VALUE, pgce.PathAspect.SOURCE}\n\n    func_rvar = relctx.new_rel_rvar(ir_set, newctx.rel, ctx=ctx)\n    relctx.include_rvar(\n        ctx.rel, func_rvar, ir_set.path_id, aspects=aspects, ctx=ctx\n    )\n\n    pathctx.put_path_rvar(\n        ctx.rel,\n        out_obj_id,\n        func_rvar,\n        aspect=pgce.PathAspect.SOURCE,\n    )\n\n    return new_stmt_set_rvar(ir_set, ctx.rel, aspects=aspects, ctx=ctx)\n\n\ndef _fts_search_inner_pg(\n    _call: irast.Call,\n    obj_id: irast.PathId,\n    args_pg: list[pgast.BaseExpr],\n    ctx: context.CompilerContextLevel,\n    newctx: context.CompilerContextLevel,\n    inner_ctx: context.CompilerContextLevel,\n) -> tuple[pgast.BaseExpr, pgast.BaseExpr]:\n    lang, weights, query = args_pg\n    el_name = sn.QualName('__object__', '__fts_document__')\n    fts_document_ptrref = irast.SpecialPointerRef(\n        name=el_name,\n        shortname=el_name,\n        out_source=obj_id.target,\n        out_target=pg_types.pg_tsvector_typeref,\n        out_cardinality=qltypes.Cardinality.AT_MOST_ONE,\n    )\n    fts_document_id = obj_id.extend(ptrref=fts_document_ptrref)\n    fts_document = relctx.get_path_var(\n        newctx.rel,\n        fts_document_id,\n        aspect=pgce.PathAspect.VALUE,\n        ctx=newctx,\n    )\n\n    lang = pgast.FuncCall(\n        name=astutils.edgedb_func('fts_to_regconfig', ctx=ctx),\n        args=[lang],\n    )\n\n    parsed_query: pgast.BaseExpr = pgast.FuncCall(\n        name=astutils.edgedb_func('fts_parse_query', ctx=ctx),\n        args=[query, lang]\n    )\n    parsed_query_id = create_subrel_for_expr(parsed_query, ctx=inner_ctx)\n    parsed_query = pathctx.get_path_var(\n        inner_ctx.rel,\n        parsed_query_id,\n        aspect=pgce.PathAspect.VALUE,\n        env=ctx.env,\n    )\n\n    weights = _fts_prepare_weights(weights, ctx=inner_ctx)\n\n    score_pg = pgast.FuncCall(\n        name=('pg_catalog', 'ts_rank'),\n        args=[weights, fts_document, parsed_query],\n    )\n    where_clause = pgast.Expr(lexpr=fts_document, name='@@', rexpr=parsed_query)\n\n    return score_pg, where_clause\n\n\ndef _fts_prepare_weights(\n    weights: pgast.BaseExpr,\n    ctx: context.CompilerContextLevel,\n) -> pgast.BaseExpr:\n    # default value\n    default_weights = pgast.ArrayExpr(\n        elements=[\n            pgast.NumericConstant(val='1.0'),\n            pgast.NumericConstant(val='0.5'),\n            pgast.NumericConstant(val='0.25'),\n            pgast.NumericConstant(val='0.125'),\n        ]\n    )\n    weights = pgast.CoalesceExpr(args=[weights, default_weights])\n\n    # cast to reals\n    weights = pgast.TypeCast(\n        arg=weights, type_name=pgast.TypeName(name=('real',), array_bounds=[-1])\n    )\n\n    # pad with zeros\n    zero = pgast.NumericConstant(val='0.0')\n    padding_weights = pgast.ArrayExpr(elements=[zero, zero, zero, zero])\n    weights = pgast.Expr(\n        lexpr=weights,\n        name='||',\n        rexpr=padding_weights,\n    )\n\n    # put the whole expression into subrel,\n    # so it can be referenced mutiple times\n    weights_id = create_subrel_for_expr(weights, ctx=ctx)\n    weights = pathctx.get_path_var(\n        ctx.rel,\n        weights_id,\n        aspect=pgce.PathAspect.VALUE,\n        env=ctx.env,\n    )\n\n    # return array of first 4 values, reversed\n    return pgast.ArrayExpr(\n        elements=[\n            pgast.Indirection(\n                arg=weights,\n                indirection=[\n                    pgast.Index(idx=pgast.NumericConstant(val=str(i)))\n                ],\n            )\n            for i in range(4, 0, -1)\n        ]\n    )\n\n\ndef _fts_search_inner_zombo(\n    _call: irast.Call,\n    obj_id: irast.PathId,\n    args_pg: list[pgast.BaseExpr],\n    _ctx: context.CompilerContextLevel,\n    newctx: context.CompilerContextLevel,\n    _inner_ctx: context.CompilerContextLevel,\n) -> tuple[pgast.BaseExpr, pgast.BaseExpr]:\n    _, _, query = args_pg\n    el_name = sn.QualName('__object__', 'ctid')\n    ctid_ptrref = irast.SpecialPointerRef(\n        name=el_name,\n        shortname=el_name,\n        out_source=obj_id.target,\n        out_target=pg_types.pg_oid_typeref,\n        out_cardinality=qltypes.Cardinality.AT_MOST_ONE,\n    )\n    ctid_id = obj_id.extend(ptrref=ctid_ptrref)\n    ctid = relctx.get_path_var(\n        newctx.rel,\n        ctid_id,\n        aspect=pgce.PathAspect.VALUE,\n        ctx=newctx,\n    )\n\n    score_pg = pgast.FuncCall(name=('zdb', 'score'), args=[ctid])\n    where_clause = pgast.Expr(\n        lexpr=ctid,\n        name='==>',\n        rexpr=query,\n    )\n    return score_pg, where_clause\n\n\ndef create_subrel_for_expr(\n    expr: pgast.BaseExpr, *, ctx: context.CompilerContextLevel\n) -> irast.PathId:\n    \"\"\"\n    Creates a sub query relation that contains the given expression.\n    \"\"\"\n\n    # create a dummy path id for a dummy object\n    expr_id = irast.PathId.new_dummy(ctx.env.aliases.get('d'))\n\n    with ctx.subrel() as newctx:\n\n        # register the expression\n        pathctx.put_path_var(\n            newctx.rel,\n            expr_id,\n            expr,\n            aspect=pgce.PathAspect.VALUE,\n        )\n\n        # include the subrel in the parent\n        new_rvar = relctx.rvar_for_rel(newctx.rel, ctx=ctx)\n        relctx.include_rvar(\n            ctx.rel,\n            new_rvar,\n            expr_id,\n            aspects=(pgce.PathAspect.VALUE,),\n            ctx=ctx,\n        )\n\n    return expr_id\n"
  },
  {
    "path": "edb/pgsql/compiler/shapecomp.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Compilation helpers for shapes.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Sequence\n\nfrom edb.edgeql import ast as qlast\n\n\nfrom edb.ir import ast as irast\nfrom edb.ir import utils as irutils\n\nfrom edb.pgsql import ast as pgast\n\nfrom . import astutils\nfrom . import context\nfrom . import dispatch\nfrom . import expr as expr_compiler  # NOQA\nfrom . import relgen\nfrom . import relctx\nfrom . import pathctx\n\n\ndef compile_shape(\n        ir_set: irast.Set,\n        shape: Sequence[tuple[irast.SetE[irast.Pointer], qlast.ShapeOp]], *,\n        ctx: context.CompilerContextLevel) -> pgast.TupleVar:\n    elements = []\n\n    # If the object identity is potentially nullable, filter it out\n    # to prevent shapes with bogusly null insides.\n    var = pathctx.get_path_value_var(\n        ctx.rel, path_id=ir_set.path_id, env=ctx.env)\n    if var.nullable:\n        ctx.rel.where_clause = astutils.extend_binop(\n            ctx.rel.where_clause,\n            pgast.NullTest(arg=var, negated=True))\n\n    with ctx.newscope() as shapectx:\n        shapectx.disable_semi_join |= {ir_set.path_id}\n\n        if isinstance(ir_set.expr, irast.Stmt):\n            # The source set for this shape is a FOR statement,\n            # which is special in that besides set path_id it\n            # should also expose the path_id of the FOR iterator\n            # so that shape element expressions that might contain\n            # an iterator reference find it properly.\n            #\n            # So, for:\n            #    SELECT Bar {\n            #        foo := (FOR x := ... UNION Foo { spam := x })\n            #    }\n            #\n            # the path scope when processing the shape of Bar.foo\n            # should be {'Bar.foo', 'x'}.\n            iterator = ir_set.expr.iterator_stmt\n            if iterator:\n                shapectx.path_scope[iterator.path_id] = ctx.rel\n\n        has_id = False\n        for el, op in shape:\n            if op == qlast.ShapeOp.MATERIALIZE and not ctx.materializing:\n                continue\n\n            rptr = el.expr\n            ptrref = rptr.ptrref\n            has_id |= ptrref.shortname.name == 'id'\n            # As an implementation expedient, we currently represent\n            # AT_MOST_ONE materialized values with arrays\n            card = rptr.dir_cardinality\n            is_singleton = (\n                card.is_single() and (\n                    not ctx.materializing or not card.can_be_zero()\n                )\n            )\n            value: pgast.BaseExpr\n\n            if (irutils.is_subquery_set(el) or\n                    el.path_id.is_objtype_path() or\n                    not is_singleton or\n                    not ptrref.required):\n                wrapper = relgen.set_as_subquery(\n                    el, as_value=True, ctx=shapectx)\n                if not is_singleton:\n                    value = relctx.set_to_array(\n                        path_id=el.path_id, query=wrapper, ctx=shapectx)\n                else:\n                    value = wrapper\n            else:\n                value = dispatch.compile(el, ctx=shapectx)\n\n            tuple_el = astutils.tuple_element_for_shape_el(\n                el, value, ctx=shapectx)\n\n            assert isinstance(tuple_el, pgast.TupleElement)\n            elements.append(tuple_el)\n\n        # If there wasn't an id (because its a FreeObject), add a fake one.\n        if ctx.materializing and not has_id:\n            elements.append(pgast.TupleElement(\n                path_id=ir_set.path_id,\n                val=var,\n            ))\n\n    return pgast.TupleVar(elements=elements, named=True)\n"
  },
  {
    "path": "edb/pgsql/compiler/stmt.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Optional\n\nfrom edb import errors\n\nfrom edb.ir import ast as irast\nfrom edb.ir import utils as irutils\n\nfrom edb.pgsql import ast as pgast\n\nfrom . import astutils\nfrom . import clauses\nfrom . import context\nfrom . import dispatch\nfrom . import enums as pgce\nfrom . import group\nfrom . import dml\nfrom . import output\nfrom . import pathctx\n\n\n@dispatch.compile.register(irast.SelectStmt)\ndef compile_SelectStmt(\n    stmt: irast.SelectStmt, *, ctx: context.CompilerContextLevel\n) -> pgast.BaseExpr:\n\n    if ctx.singleton_mode:\n        if not irutils.is_trivial_select(stmt):\n            raise errors.UnsupportedFeatureError(\n                'Clause on SELECT statement in simple expression')\n\n        return dispatch.compile(stmt.result, ctx=ctx)\n\n    parent_ctx = ctx\n    with parent_ctx.substmt() as ctx:\n        # Common setup.\n        clauses.compile_volatile_bindings(stmt, ctx=ctx)\n\n        query = ctx.stmt\n\n        # Process materialized sets\n        clauses.compile_materialized_exprs(query, stmt, ctx=ctx)\n\n        iterator_set = stmt.iterator_stmt\n        last_iterator: Optional[irast.Set] = None\n        if iterator_set:\n            if irutils.contains_dml(stmt):\n                # If we have iterators and we contain nested DML\n                # statements, we need to hoist the iterators into CTEs and\n                # then explicitly join them back into the query.\n                iterator = dml.compile_iterator_cte(iterator_set, ctx=ctx)\n                ctx.path_scope = ctx.path_scope.new_child()\n                dml.merge_iterator(iterator, ctx.rel, ctx=ctx)\n\n                ctx.enclosing_cte_iterator = iterator\n                last_iterator = stmt.iterator_stmt\n\n            else:\n                # Process FOR clause.\n                with ctx.new() as ictx:\n                    clauses.setup_iterator_volatility(last_iterator, ctx=ictx)\n                    iterator_rvar = clauses.compile_iterator_expr(\n                        query, iterator_set, is_dml=False, ctx=ictx)\n                for aspect in {pgce.PathAspect.IDENTITY, pgce.PathAspect.VALUE}:\n                    pathctx.put_path_rvar(\n                        query,\n                        path_id=iterator_set.path_id,\n                        rvar=iterator_rvar,\n                        aspect=aspect,\n                    )\n                last_iterator = iterator_set\n\n        # Process the result expression.\n        with ctx.new() as ictx:\n            clauses.setup_iterator_volatility(last_iterator, ctx=ictx)\n            outvar = clauses.compile_output(stmt.result, ctx=ictx)\n\n        with ctx.new() as ictx:\n            # FILTER and ORDER BY need to have the base result as a\n            # volatility ref.\n            clauses.setup_iterator_volatility(stmt.result, ctx=ictx)\n\n            # The FILTER clause.\n            if stmt.where is not None:\n                query.where_clause = astutils.extend_binop(\n                    query.where_clause,\n                    clauses.compile_filter_clause(\n                        stmt.where, stmt.where_card, ctx=ictx))\n\n            # The ORDER BY clause\n            if stmt.orderby is not None:\n                with ictx.new() as octx:\n                    query.sort_clause = clauses.compile_orderby_clause(\n                        stmt.orderby, ctx=octx)\n\n        # Need to filter out NULLs in certain cases:\n        if outvar.nullable and (\n            # A nullable var has bubbled up to the top\n            query is ctx.toplevel_stmt\n            # The cardinality is being overridden, so we need to make\n            # sure there aren't extra NULLs in single set\n            or stmt.card_inference_override\n            # There is a LIMIT or OFFSET clause and NULLs would interfere\n            or stmt.limit\n            or stmt.offset\n        ):\n            valvar = pathctx.get_path_value_var(\n                query, stmt.result.path_id, env=ctx.env)\n            output.add_null_test(valvar, query)\n\n        # The OFFSET clause\n        query.limit_offset = clauses.compile_limit_offset_clause(\n            stmt.offset, ctx=ctx)\n\n        # The LIMIT clause\n        query.limit_count = clauses.compile_limit_offset_clause(\n            stmt.limit, ctx=ctx)\n\n    return query\n\n\n@dispatch.compile.register(irast.GroupStmt)\ndef compile_GroupStmt(\n    stmt: irast.GroupStmt, *, ctx: context.CompilerContextLevel\n) -> pgast.BaseExpr:\n    return group.compile_group(stmt, ctx=ctx)\n\n\n@dispatch.compile.register(irast.InsertStmt)\ndef compile_InsertStmt(\n    stmt: irast.InsertStmt, *, ctx: context.CompilerContextLevel\n) -> pgast.Query:\n\n    parent_ctx = ctx\n    with parent_ctx.substmt() as ctx:\n        # Common DML bootstrap.\n        parts = dml.init_dml_stmt(stmt, ctx=ctx)\n\n        top_typeref = stmt.subject.typeref\n        if top_typeref.material_type is not None:\n            top_typeref = top_typeref.material_type\n        insert_cte, _ = parts.dml_ctes[top_typeref]\n\n        # Process INSERT body.\n        dml.process_insert_body(\n            ir_stmt=stmt,\n            insert_cte=insert_cte,\n            dml_parts=parts,\n            ctx=ctx,\n        )\n\n        # Wrap up.\n        dml.fini_dml_stmt(stmt, parts, ctx=ctx)\n        return ctx.rel\n\n\n@dispatch.compile.register(irast.UpdateStmt)\ndef compile_UpdateStmt(\n    stmt: irast.UpdateStmt, *, ctx: context.CompilerContextLevel\n) -> pgast.Query:\n\n    parent_ctx = ctx\n    with parent_ctx.substmt() as ctx:\n        # Common DML bootstrap.\n        parts = dml.init_dml_stmt(stmt, ctx=ctx)\n        range_cte = parts.range_cte\n        assert range_cte is not None\n\n        toplevel = ctx.toplevel_stmt\n        toplevel.append_cte(range_cte)\n\n        for typeref, (update_cte, _) in parts.dml_ctes.items():\n            # Process UPDATE body.\n            dml.process_update_body(\n                ir_stmt=stmt,\n                update_cte=update_cte,\n                dml_parts=parts,\n                typeref=typeref,\n                ctx=ctx,\n            )\n\n        dml.fini_dml_stmt(stmt, parts, ctx=ctx)\n        return ctx.rel\n\n\n@dispatch.compile.register(irast.DeleteStmt)\ndef compile_DeleteStmt(\n    stmt: irast.DeleteStmt, *, ctx: context.CompilerContextLevel\n) -> pgast.Query:\n\n    parent_ctx = ctx\n    with parent_ctx.substmt() as ctx:\n        # Common DML bootstrap\n        parts = dml.init_dml_stmt(stmt, ctx=ctx)\n\n        range_cte = parts.range_cte\n        assert range_cte is not None\n        ctx.toplevel_stmt.append_cte(range_cte)\n\n        for typeref, (delete_cte, _) in parts.dml_ctes.items():\n            dml.process_delete_body(\n                ir_stmt=stmt,\n                delete_cte=delete_cte,\n                typeref=typeref,\n                ctx=ctx,\n            )\n\n        # Wrap up.\n        dml.fini_dml_stmt(stmt, parts, ctx=ctx)\n        return ctx.rel\n"
  },
  {
    "path": "edb/pgsql/dbops/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Abstractions for low-level database DDL and DML operations and data.\"\"\"\n\nfrom __future__ import annotations\n\nfrom .base import *  # NOQA\nfrom .config import *  # type: ignore  # NOQA\nfrom .ddl import *  # NOQA\nfrom .databases import *  # NOQA\nfrom .domains import *  # NOQA\nfrom .enums import *  # NOQA\nfrom .extensions import *  # NOQA\nfrom .functions import *  # NOQA\nfrom .indexes import *  # NOQA\nfrom .operators import *  # NOQA\nfrom .ranges import *  # NOQA\nfrom .roles import * # NOQA\nfrom .schemas import *  # NOQA\nfrom .sequences import *  # NOQA\nfrom .tables import *  # NOQA\nfrom .triggers import *  # NOQA\nfrom .types import *  # NOQA\nfrom .views import *  # NOQA\n"
  },
  {
    "path": "edb/pgsql/dbops/base.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Final,\n    Iterable,\n    Iterator,\n    Mapping,\n    Optional,\n    Sequence,\n)\nfrom collections.abc import MutableSequence\n\nimport collections\nimport enum\nimport numbers\nimport textwrap\n\nfrom edb.common import markup\nfrom edb.common import struct\nfrom edb.common import typeutils\n\nfrom ..common import quote_ident as qi\nfrom ..common import quote_literal as ql\nfrom ..common import quote_type as qt\nfrom ..common import qname as qn\n\n\nclass NotSpecifiedT(enum.Enum):\n    NotSpecified = 0\n\n\nNotSpecified: Final = NotSpecifiedT.NotSpecified\n\n\ndef encode_value(val: Any) -> str:\n    \"\"\"Encode value into an appropriate SQL expression.\"\"\"\n    if hasattr(val, 'to_sql_expr'):\n        val = val.to_sql_expr()\n    elif isinstance(val, tuple):\n        val_list = [encode_value(el) for el in val]\n        val = f'ROW({\", \".join(val_list)})'\n    elif isinstance(val, struct.Struct):\n        val_list = [encode_value(el) for el in val.as_tuple()]\n        val = f'ROW({\", \".join(val_list)})'\n    elif typeutils.is_container(val):\n        val_list = [encode_value(el) for el in val]\n        val = f'ARRAY[{\", \".join(val_list)}]'\n    elif val is None:\n        val = 'NULL'\n    elif not isinstance(val, numbers.Number):\n        val = ql(str(val))\n    elif isinstance(val, int):\n        val = str(int(val))\n    else:\n        val = str(val)\n\n    return val\n\n\nclass PLExpression(str):\n    pass\n\n\nclass SQLBlock:\n    commands: list[str | PLBlock]\n\n    def __init__(self) -> None:\n        self.commands = []\n        self._transactional = True\n\n    def add_block(self) -> PLBlock:\n        block = PLTopBlock()\n        self.add_command(block)\n        return block\n\n    def to_string(self) -> str:\n        if not self._transactional:\n            raise ValueError(\n                'block is non-transactional, please use .get_statements()'\n            )\n        stmts = self.get_statements()\n        body = '\\n\\n'.join(stmt + ';' if stmt[-1] != ';' else stmt\n                           for stmt in stmts if stmt).rstrip()\n        if body and body[-1] != ';':\n            body += ';'\n\n        return body\n\n    def get_statements(self) -> list[str]:\n        return [(cmd if isinstance(cmd, str) else cmd.to_string()).rstrip()\n                for cmd in self.commands]\n\n    def add_command(self, stmt: str | PLBlock) -> None:\n        self.commands.append(stmt)\n\n    def has_declarations(self) -> bool:\n        return False\n\n    def set_non_transactional(self) -> None:\n        self._transactional = False\n\n    def is_transactional(self) -> bool:\n        return self._transactional\n\n\nclass PLBlock(SQLBlock):\n\n    varcounter: dict[str, int]\n    shared_vars: set[str]\n    declarations: list[tuple[str, str | tuple[str, str]]]\n    conditions: Iterable[str | Condition]\n    neg_conditions: Iterable[str | Condition]\n\n    def __init__(self, top_block: Optional[PLTopBlock], level: int) -> None:\n        super().__init__()\n        self.top_block = top_block\n        self.varcounter = collections.defaultdict(int)\n        self.shared_vars = set()\n        self.declarations = []\n        self.level = level\n        self.conditions = set()\n        self.neg_conditions = set()\n\n    def has_declarations(self) -> bool:\n        return bool(self.declarations)\n\n    def has_statements(self) -> bool:\n        return bool(self.commands)\n\n    def get_top_block(self) -> PLTopBlock:\n        return typeutils.not_none(self.top_block)\n\n    def add_block(self) -> PLBlock:\n        block = PLBlock(top_block=self.top_block, level=self.level + 1)\n        self.add_command(block)\n        return block\n\n    def to_string(self) -> str:\n        if self.declarations:\n            vv = (f'    {qi(n)} {qt(t)};' for n, t in self.declarations)\n            decls = 'DECLARE\\n' + '\\n'.join(vv) + '\\n'\n        else:\n            decls = ''\n\n        body = super().to_string()\n\n        if self.conditions or self.neg_conditions:\n            exprs = []\n            if self.conditions:\n                for cond in self.conditions:\n                    if not isinstance(cond, str):\n                        cond_expr = f'EXISTS ({cond.code()})'\n                    else:\n                        cond_expr = cond\n                    exprs.append(cond_expr)\n\n            if self.neg_conditions:\n                for cond in self.neg_conditions:\n                    if not isinstance(cond, str):\n                        cond_expr = f'EXISTS ({cond.code()})'\n                    else:\n                        cond_expr = cond\n                    exprs.append(f'NOT {cond_expr}')\n\n            if_clause = '\\n    AND'.join(\n                f'({textwrap.indent(expr, \"    \").lstrip()})'\n                for expr in exprs\n            )\n\n            body = textwrap.indent(body, '    ').rstrip()\n            semicolon = ';' if body[-1] != ';' else ''\n            body = f'IF {if_clause}\\nTHEN\\n{body}{semicolon}\\nEND IF;'\n\n        if decls or not isinstance(self.top_block, PLBlock):\n            return textwrap.indent(\n                f'{decls}BEGIN\\n{body}\\nEND;',\n                ' ' * self.level * 4,\n            )\n        else:\n            return body\n\n    def add_command(\n        self,\n        cmd: str | PLBlock,\n        *,\n        conditions: Optional[Iterable[str | Condition]] = None,\n        neg_conditions: Optional[Iterable[str | Condition]] = None\n    ) -> None:\n        stmt: str | PLBlock\n        if conditions or neg_conditions:\n            exprs = []\n            if conditions:\n                for cond in conditions:\n                    if not isinstance(cond, str):\n                        cond_expr = f'EXISTS ({cond.code()})'\n                    else:\n                        cond_expr = cond\n                    exprs.append(cond_expr)\n\n            if neg_conditions:\n                for cond in neg_conditions:\n                    if not isinstance(cond, str):\n                        cond_expr = f'EXISTS ({cond.code()})'\n                    else:\n                        cond_expr = cond\n                    exprs.append(f'NOT {cond_expr}')\n\n            if_clause = '\\n    AND'.join(\n                f'({textwrap.indent(expr, \"    \").lstrip()})'\n                for expr in exprs\n            )\n\n            if isinstance(cmd, PLBlock):\n                cmd = cmd.to_string()\n\n            cmd = textwrap.indent(cmd, '    ').rstrip()\n            semicolon = ';' if cmd[-1] != ';' else ''\n            stmt = f'IF {if_clause}\\nTHEN\\n{cmd}{semicolon}\\nEND IF;'\n        else:\n            stmt = cmd\n\n        super().add_command(stmt)\n\n    def get_var_name(self, hint: Optional[str] = None) -> str:\n        if hint is None:\n            hint = 'v'\n        self.varcounter[hint] += 1\n        return f'{hint}_{self.varcounter[hint]}'\n\n    def declare_var(\n        self,\n        type_name: str | tuple[str, str],\n        *,\n        var_name: str='',\n        var_name_prefix: str='v',\n        shared: bool=False,\n    ) -> str:\n        if shared:\n            if not var_name:\n                var_name = var_name_prefix\n            if var_name not in self.shared_vars:\n                self.declarations.append((var_name, type_name))\n                self.shared_vars.add(var_name)\n        else:\n            if not var_name:\n                var_name = self.get_var_name(var_name_prefix)\n            self.declarations.append((var_name, type_name))\n\n        return var_name\n\n\nclass PLTopBlock(PLBlock):\n    def __init__(self) -> None:\n        super().__init__(top_block=None, level=0)\n        self.declare_var('text', var_name='_dummy_text', shared=True)\n\n    def add_block(self) -> PLBlock:\n        block = PLBlock(top_block=self, level=self.level + 1)\n        self.add_command(block)\n        return block\n\n    def to_string(self) -> str:\n        body = super().to_string()\n        return f'DO LANGUAGE plpgsql $__$\\n{body}\\n$__$;'\n\n    def get_top_block(self) -> PLTopBlock:\n        return self\n\n\nclass BaseCommand(markup.MarkupCapableMixin):\n    def generate(self, block: SQLBlock) -> None:\n        raise NotImplementedError\n\n    @classmethod\n    def as_markup(cls, self, *, ctx) -> markup.elements.lang.TreeNode:\n        return markup.elements.lang.TreeNode(name=repr(self))\n\n    def dump(self) -> str:\n        return str(self)\n\n\nclass Command(BaseCommand):\n\n    conditions: set[str | Condition]\n    neg_conditions: set[str | Condition]\n\n    def __init__(\n        self,\n        *,\n        conditions: Optional[Iterable[str | Condition]] = None,\n        neg_conditions: Optional[Iterable[str | Condition]] = None,\n    ) -> None:\n        self.opid = id(self)\n        self.conditions = set(conditions) if conditions else set()\n        self.neg_conditions = set(neg_conditions) if neg_conditions else set()\n\n    def generate(self, block: SQLBlock) -> None:\n        self_block = self.generate_self_block(block)\n        if self_block is None:\n            return\n\n        self.generate_extra(self_block)\n        self_block.conditions = self.conditions\n        self_block.neg_conditions = self.neg_conditions\n\n    def generate_self_block(self, block: SQLBlock) -> Optional[PLBlock]:\n        # Default implementation simply calls self.code_with_block()\n        self_block = block.add_block()\n        self_block.add_command(self.code_with_block(self_block))\n        return self_block\n\n    def generate_extra(self, block: PLBlock) -> None:\n        pass\n\n    def code(self) -> str:\n        raise NotImplementedError\n\n    def code_with_block(self, block: PLBlock) -> str:\n        return self.code()\n\n\nclass CommandGroup(Command):\n    commands: MutableSequence[Command]\n\n    def __init__(\n        self,\n        *,\n        conditions: Optional[Iterable[str | Condition]] = None,\n        neg_conditions: Optional[Iterable[str | Condition]] = None,\n    ) -> None:\n        super().__init__(conditions=conditions, neg_conditions=neg_conditions)\n        self.commands = []\n\n    def add_command(self, cmd: Command) -> None:\n        self.commands.append(cmd)\n\n    def add_commands(self, cmds: Sequence[Command]) -> None:\n        self.commands.extend(cmds)\n\n    def generate_self_block(self, block: SQLBlock) -> Optional[PLBlock]:\n        if not self.commands:\n            return None\n\n        self_block = block.add_block()\n\n        for cmd in self.commands:\n            cmd.generate(self_block)\n\n        return self_block\n\n    @classmethod\n    def as_markup(cls, self, *, ctx) -> markup.elements.lang.TreeNode:\n        node = markup.elements.lang.TreeNode(name=repr(self))\n\n        for op in self.commands:\n            node.add_child(node=markup.serialize(op, ctx=ctx))\n\n        return node\n\n    def __iter__(self) -> Iterator[Command]:\n        return iter(self.commands)\n\n    def __len__(self) -> int:\n        return len(self.commands)\n\n\nclass CompositeCommand(Command):\n\n    def generate_extra_composite(\n        self, block: PLBlock, group: CompositeCommandGroup\n    ) -> None:\n        pass\n\n\nclass CompositeCommandGroup(Command):\n    commands: MutableSequence[CompositeCommand]\n\n    def __init__(\n        self,\n        *,\n        conditions: Optional[Iterable[str | Condition]] = None,\n        neg_conditions: Optional[Iterable[str | Condition]] = None,\n    ) -> None:\n        super().__init__(conditions=conditions, neg_conditions=neg_conditions)\n        self.commands = []\n\n    def add_command(self, cmd: CompositeCommand) -> None:\n        self.commands.append(cmd)\n\n    def add_commands(self, cmds: Sequence[CompositeCommand]) -> None:\n        self.commands.extend(cmds)\n\n    def generate_self_block(self, block: SQLBlock) -> Optional[PLBlock]:\n        if not self.commands:\n            return None\n\n        self_block = block.add_block()\n        prefix_code = self.prefix_code()\n        actions = []\n        dynamic_actions = []\n\n        for cmd in self.commands:\n            if isinstance(cmd, tuple) and (cmd[1] or cmd[2]):\n                action = cmd[0].code_with_block(self_block)\n                if isinstance(action, PLExpression):\n                    subcommand = \\\n                        f\"EXECUTE {ql(prefix_code)} || ' ' || {action}\"\n                else:\n                    subcommand = prefix_code + ' ' + action\n                self_block.add_command(\n                    subcommand, conditions=cmd[1], neg_conditions=cmd[2])\n            else:\n                action = cmd.code_with_block(self_block)\n                if isinstance(action, PLExpression):\n                    subcommand = \\\n                        f\"EXECUTE {ql(prefix_code)} || ' ' || {action}\"\n                    dynamic_actions.append(subcommand)\n                else:\n                    actions.append(action)\n\n        if actions:\n            command = prefix_code + ' ' + ', '.join(actions)\n            self_block.add_command(command)\n\n        if dynamic_actions:\n            for action in dynamic_actions:\n                self_block.add_command(action)\n\n        extra_block = self_block.add_block()\n\n        for cmd in self.commands:\n            if isinstance(cmd, tuple) and (cmd[1] or cmd[2]):\n                cmd[0].generate_extra_composite(extra_block, self)\n            else:\n                cmd.generate_extra_composite(extra_block, self)\n\n        return self_block\n\n    def prefix_code(self) -> str:\n        raise NotImplementedError\n\n    def __iter__(self) -> Iterator[CompositeCommand]:\n        return iter(self.commands)\n\n    def __len__(self) -> int:\n        return len(self.commands)\n\n\nclass Condition(BaseCommand):\n\n    def code(self) -> str:\n        raise NotImplementedError()\n\n\nclass Query(Command):\n    def __init__(\n        self,\n        text: str,\n        *,\n        type: Optional[str | tuple[str, str]] = None,\n        trampoline_fixup: bool = True,\n    ) -> None:\n        from ..import trampoline\n\n        super().__init__()\n        if trampoline_fixup:\n            text = trampoline.fixup_query(text)\n        self.text = text\n        self.type = type\n\n    def to_sql_expr(self) -> str:\n        if self.type:\n            return f'({self.text})::{qn(*self.type)}'\n        else:\n            return self.text\n\n    def code(self) -> str:\n        return self.text\n\n    def __repr__(self) -> str:\n        return f'<Query {self.text!r}>'\n\n\nclass PLQuery(Query):\n    pass\n\n\nclass DefaultMeta(type):\n    def __bool__(cls):\n        return False\n\n    def __repr__(self):\n        return '<DEFAULT>'\n\n    __str__ = __repr__\n\n\nclass Default(metaclass=DefaultMeta):\n    pass\n\n\nclass DBObject:\n    def __init__(\n        self,\n        *,\n        metadata: Optional[Mapping[str, Any]] = None\n    ) -> None:\n        self.metadata = dict(metadata) if metadata else None\n\n    def add_metadata(self, key: str, value: Any) -> None:\n        if self.metadata is None:\n            self.metadata = {}\n\n        self.metadata[key] = value\n\n    def get_metadata(self, key: str) -> Any:\n        if self.metadata is None:\n            return None\n        else:\n            return self.metadata.get(key)\n\n    def is_shared(self) -> bool:\n        return False\n\n    def get_type(self) -> str:\n        raise NotImplementedError()\n\n    def get_id(self) -> str:\n        raise NotImplementedError()\n\n\nclass InheritableDBObject(DBObject):\n    def __init__(\n        self,\n        *,\n        inherit: bool = False,\n        metadata: Optional[Mapping[str, Any]] = None,\n    ) -> None:\n        super().__init__(metadata=metadata)\n        if inherit:\n            self.add_metadata('ddl:inherit', inherit)\n\n    @property\n    def inherit(self) -> bool:\n        return self.get_metadata('ddl:inherit') or False\n\n\nclass NoOpCommand(Command):\n    def generate_self_block(self, block: SQLBlock) -> Optional[PLBlock]:\n        return None\n"
  },
  {
    "path": "edb/pgsql/dbops/catalogs.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2013-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom . import tables\n\n\nclass PgDescriptionTable(tables.Table):\n    def __init__(self, name=None):\n        super().__init__(name=('pg_catalog', 'pg_description'))\n\n        self.add_columns([\n            tables.Column(name='objoid', type='oid', required=True),\n            tables.Column(name='classoid', type='oid', required=True),\n            tables.Column(name='objsubid', type='integer', required=True),\n            tables.Column(name='description', type='text')\n        ])\n"
  },
  {
    "path": "edb/pgsql/dbops/composites.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Iterable, Sequence\n\nfrom edb.common import ordered\n\nfrom .. import common\nfrom . import base\nfrom . import tables\n\n\nclass Record(type):\n    def __new__(mcls, name, fields, default=None):\n        dct = {'_fields___': fields, '_default___': default}\n        bases = (RecordBase, )\n        return super(Record, mcls).__new__(mcls, name, bases, dct)\n\n    def __init__(cls, name, fields, default):\n        pass\n\n    def has_field(cls, name):\n        return name in cls._fields___\n\n\nclass RecordBase:\n    def __init__(self, **kwargs):\n        for k, v in kwargs.items():\n            if k not in self.__class__._fields___:\n                msg = '__init__() got an unexpected keyword argument %s' % k\n                raise TypeError(msg)\n            setattr(self, k, v)\n\n        for k in set(self.__class__._fields___) - set(kwargs.keys()):\n            setattr(self, k, self.__class__._default___)\n\n    def __setattr__(self, name, value):\n        if name not in self.__class__._fields___:\n            msg = '%s has no attribute %s' % (self.__class__.__name__, name)\n            raise AttributeError(msg)\n        super().__setattr__(name, value)\n\n    def __eq__(self, tup):\n        if not isinstance(tup, tuple):\n            return NotImplemented\n\n        return tuple(self) == tup\n\n    def __getitem__(self, index):\n        return getattr(self, self.__class__._fields___[index])\n\n    def __iter__(self):\n        for name in self.__class__._fields___:\n            yield getattr(self, name)\n\n    def __len__(self):\n        return len(self.__class__._fields___)\n\n    def items(self):\n        for name in self.__class__._fields___:\n            yield name, getattr(self, name)\n\n    def keys(self):\n        return iter(self.__class__._fields___)\n\n    def __str__(self):\n        f = ', '.join(str(v) for v in self)\n        if len(self) == 1:\n            f += ','\n        return '(%s)' % f\n\n    __repr__ = __str__\n\n\nclass CompositeDBObject(base.DBObject):\n    def __init__(\n        self,\n        name: Sequence[str],\n        columns: Iterable[tables.Column] | None = None,\n    ):\n        super().__init__()\n        self.name = name\n        self._columns: ordered.OrderedSet[tables.Column] = ordered.OrderedSet()\n        self.add_columns(columns or [])\n\n    def add_columns(self, iterable: Iterable[tables.Column]):\n        self._columns.update(iterable)\n\n    @property\n    def record(self):\n        return Record(\n            self.__class__.__name__ + '_record',\n            [c.name for c in self._columns], default=base.Default)\n\n\nclass CompositeAttributeCommand:\n    def __init__(self, attribute):\n        self.attribute = attribute\n\n    def __repr__(self):\n        return '<%s.%s %r>' % (\n            self.__class__.__module__, self.__class__.__name__, self.attribute)\n\n\nclass AlterCompositeAddAttribute(CompositeAttributeCommand):\n    def code(self) -> str:\n        return (f'ADD {self.get_attribute_term()} '  # type: ignore\n                f'{self.attribute.code()}')\n\n    def generate_extra_composite(\n        self, block: base.PLBlock, alter: base.CompositeCommandGroup\n    ) -> None:\n        self.attribute.generate_extra_composite(block, alter)\n\n\nclass AlterCompositeDropAttribute(CompositeAttributeCommand):\n    def code(self) -> str:\n        attrname = common.qname(self.attribute.name)\n        return f'DROP {self.get_attribute_term()} {attrname}'  # type: ignore\n\n\nclass AlterCompositeAlterAttributeType:\n    def __init__(self, attribute_name, new_type, *, cast_expr=None):\n        self.attribute_name = attribute_name\n        self.new_type = new_type\n        self.cast_expr = cast_expr\n\n    def code(self) -> str:\n        attrterm = self.get_attribute_term()  # type: ignore\n        attrname = common.quote_ident(str(self.attribute_name))\n        code = f'ALTER {attrterm} {attrname} SET DATA TYPE {self.new_type}'\n        if self.cast_expr is not None:\n            code += f' USING ({self.cast_expr})'\n\n        return code\n\n    def __repr__(self):\n        cls = self.__class__\n        return f'<{cls.__name__} {self.attribute_name!r} to {self.new_type}>'\n"
  },
  {
    "path": "edb/pgsql/dbops/config.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom ..common import quote_ident as qi\nfrom ..common import quote_literal as ql\n\nfrom . import base\n\n\nclass Set(base.Command):\n\n    def __init__(self, key, val, **kwargs):\n        super().__init__(**kwargs)\n        self.key = key\n        self.val = val\n\n    def code(self) -> str:\n        return f'SET {qi(self.key)} = {ql(self.val)}'\n"
  },
  {
    "path": "edb/pgsql/dbops/constraints.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Optional, Sequence\n\nfrom .. import common\nfrom . import base\n\n\nclass Constraint(base.DBObject):\n    def __init__(\n        self,\n        subject_name: Sequence[str],\n        constraint_name: Optional[str] = None,\n    ):\n        self._subject_name = tuple(subject_name)\n        self._constraint_name = constraint_name\n\n    def get_type(self):\n        return 'CONSTRAINT'\n\n    def get_subject_type(self):\n        raise NotImplementedError\n\n    def generate_extra(self, block: base.PLBlock) -> None:\n        raise NotImplementedError\n\n    def get_subject_name(self, quote=True):\n        if quote:\n            return common.qname(*self._subject_name)\n        else:\n            return self._subject_name\n\n    def get_id(self):\n        return '{} ON {} {}'.format(\n            self.constraint_name(), self.get_subject_type(),\n            self.get_subject_name())\n\n    def constraint_name(self, quote=True) -> str:\n        if quote and self._constraint_name:\n            return common.quote_ident(self._constraint_name)\n        else:\n            return self._constraint_name or ''\n\n    def constraint_code(self, block: base.PLBlock) -> str | list[str]:\n        raise NotImplementedError\n"
  },
  {
    "path": "edb/pgsql/dbops/databases.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, Mapping\n\nimport textwrap\n\nfrom ..common import quote_ident as qi\nfrom ..common import quote_literal as ql\nfrom ..common import versioned_schema as V\n\nfrom . import base\nfrom . import ddl\n\n\nclass AbstractDatabase(base.DBObject):\n    def get_type(self):\n        return 'DATABASE'\n\n    def is_shared(self) -> bool:\n        return True\n\n    def _get_id_expr(self) -> str:\n        raise NotImplementedError()\n\n    def get_oid(self) -> base.Query:\n        qry = textwrap.dedent(f'''\\\n            SELECT\n                'pg_database'::regclass::oid AS classoid,\n                pg_database.oid AS objectoid,\n                0\n            FROM\n                pg_database\n            WHERE\n                datname = {self._get_id_expr()}\n        ''')\n\n        return base.Query(text=qry)\n\n\nclass Database(AbstractDatabase):\n    def __init__(\n        self,\n        name: str,\n        *,\n        owner: Optional[str] = None,\n        is_template: bool = False,\n        encoding: Optional[str] = None,\n        lc_collate: Optional[str] = None,\n        lc_ctype: Optional[str] = None,\n        metadata: Optional[Mapping[str, Any]] = None,\n    ) -> None:\n        super().__init__(metadata=metadata)\n        self.name = name\n        self.owner = owner\n        self.is_template = is_template\n        self.encoding = encoding\n        self.lc_collate = lc_collate\n        self.lc_ctype = lc_ctype\n\n    def get_id(self):\n        return qi(self.name)\n\n    def _get_id_expr(self) -> str:\n        return ql(self.name)\n\n\nclass DatabaseWithTenant(Database):\n    def __init__(\n        self,\n        name: str,\n    ) -> None:\n        super().__init__(name=name)\n\n    def get_id(self) -> str:\n        return f\"' || quote_ident({self._get_id_expr()}) || '\"\n\n    def _get_id_expr(self) -> str:\n        return f'{V(\"edgedb\")}.get_database_backend_name({ql(self.name)})'\n\n\nclass CurrentDatabase(AbstractDatabase):\n    def get_id(self) -> str:\n        return f\"' || quote_ident({self._get_id_expr()}) || '\"\n\n    def _get_id_expr(self) -> str:\n        return 'current_database()'\n\n\nclass DatabaseExists(base.Condition):\n    def __init__(self, name):\n        self.name = name\n\n    def code(self) -> str:\n        return textwrap.dedent(f'''\\\n            SELECT\n                typname\n            FROM\n                pg_catalog.pg_database AS db\n            WHERE\n                datname = {ql(self.name)}\n        ''')\n\n\nclass CreateDatabase(ddl.CreateObject, ddl.NonTransactionalDDLOperation):\n\n    def __init__(self, object, *, template: str | None, **kwargs):\n        super().__init__(object, **kwargs)\n        self.template = template\n\n    def code(self) -> str:\n        extra = ''\n\n        if self.object.owner:\n            extra += f' OWNER={ql(self.object.owner)}'\n        if self.object.is_template:\n            extra += f' IS_TEMPLATE = TRUE'\n        if self.template:\n            extra += f' TEMPLATE={ql(self.template)}'\n        if self.object.encoding:\n            extra += f' ENCODING={ql(self.object.encoding)}'\n        if self.object.lc_collate:\n            extra += f' LC_COLLATE={ql(self.object.lc_collate)}'\n        if self.object.lc_ctype:\n            extra += f' LC_CTYPE={ql(self.object.lc_ctype)}'\n\n        return (f'CREATE DATABASE {self.object.get_id()} {extra}')\n\n\nclass DropDatabase(ddl.SchemaObjectOperation,\n                   ddl.NonTransactionalDDLOperation):\n\n    def code(self) -> str:\n        return f'DROP DATABASE {qi(self.name)}'\n\n\nclass RenameDatabase(ddl.AlterObject,\n                     ddl.NonTransactionalDDLOperation):\n    def __init__(self, object, *, old_name: str, **kwargs):\n        super().__init__(object, **kwargs)\n        self.old_name = old_name\n\n    def code(self) -> str:\n        return (\n            f'ALTER DATABASE {qi(self.old_name)} '\n            f'RENAME TO {self.object.get_id()}'\n        )\n"
  },
  {
    "path": "edb/pgsql/dbops/ddl.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport json\nimport textwrap\n\nfrom edb.server import defines\n\nfrom ..common import quote_ident as qi\nfrom ..common import quote_literal as ql\n\nfrom . import base\n\n\nclass DDLOperation(base.Command):\n    pass\n\n\nclass NonTransactionalDDLOperation(DDLOperation):\n    def generate(\n        self,\n        block: base.SQLBlock,\n    ) -> None:\n        block.add_command(self.code())\n        block.set_non_transactional()\n        self_block = block.add_block()\n\n        self.generate_extra(self_block)\n        self_block.conditions = self.conditions\n        self_block.neg_conditions = self.neg_conditions\n\n\nclass SchemaObjectOperation(DDLOperation):\n    def __init__(self, name, *, conditions=None, neg_conditions=None):\n        super().__init__(conditions=conditions, neg_conditions=neg_conditions)\n        self.name = name\n        self.opid = name\n\n    def __repr__(self):\n        return '<edb.sync.%s %s>' % (self.__class__.__name__, self.name)\n\n\nclass Comment(DDLOperation):\n    def __init__(self, object, text, **kwargs):\n        super().__init__(**kwargs)\n\n        self.object = object\n        self.text = text\n\n    def code(self) -> str:\n        object_type = self.object.get_type()\n        object_id = self.object.get_id()\n\n        code = 'COMMENT ON {type} {id} IS {text}'.format(\n            type=object_type, id=object_id,\n            text=ql(self.text))\n\n        return code\n\n\nclass ReassignOwned(DDLOperation):\n    def __init__(self, old_role, new_role, **kwargs):\n        super().__init__(**kwargs)\n        self.old_role = old_role\n        self.new_role = new_role\n\n    def qi(self, ident: str) -> str:\n        if ident.upper() in ('CURRENT_USER', 'SESSION_USER'):\n            return ident\n        else:\n            return qi(ident)\n\n    def code(self) -> str:\n        return (\n            f'REASSIGN OWNED BY {self.qi(self.old_role)} '\n            f'TO {self.qi(self.new_role)}'\n        )\n\n\nclass GetMetadata(base.Command):\n    def __init__(self, object):\n        super().__init__()\n        self.object = object\n\n    def code_with_block(self, block: base.PLBlock) -> str:\n        from .. import trampoline\n\n        oid = self.object.get_oid()\n        is_shared = self.object.is_shared()\n        if isinstance(oid, base.Query):\n            qry = oid.text\n            classoid = block.declare_var('oid')\n            objoid = block.declare_var('oid')\n            objsubid = block.declare_var('oid')\n            block.add_command(\n                qry + f' INTO {classoid}, {objoid}, {objsubid}')\n        else:\n            objoid, classoid, objsubid = oid\n\n        if is_shared:\n            q = textwrap.dedent(f'''\\\n                SELECT\n                    edgedb_VER.shobj_metadata(\n                        {objoid},\n                        {classoid}::regclass::text\n                    )\n                ''')\n        elif objsubid:\n            q = textwrap.dedent(f'''\\\n                SELECT\n                    edgedb_VER.col_metadata(\n                        {objoid},\n                        {objsubid}\n                    )\n                ''')\n        else:\n            q = textwrap.dedent(f'''\\\n                SELECT\n                    edgedb_VER.obj_metadata(\n                        {objoid},\n                        {classoid}::regclass::text,\n                    )\n                ''')\n\n        return trampoline.fixup_query(q)\n\n\nclass GetSingleDBMetadata(base.Command):\n    def __init__(self, dbname, **kwargs):\n        super().__init__(**kwargs)\n        self.dbname = dbname\n\n    def code(self) -> str:\n        from .. import trampoline\n\n        key = f'{self.dbname}metadata'\n        return textwrap.dedent(trampoline.fixup_query(f'''\\\n            SELECT\n                json\n            FROM\n                edgedbinstdata_VER.instdata\n            WHERE\n                key = {ql(key)}\n        '''))\n\n\nclass PutMetadata(DDLOperation):\n    def __init__(self, object, metadata, **kwargs):\n        super().__init__(**kwargs)\n        self.object = object\n        self.metadata = metadata\n\n    def __repr__(self):\n        return \\\n            '<{mod}.{cls} {object!r} {metadata!r}>'.format(\n                mod=self.__class__.__module__,\n                cls=self.__class__.__name__,\n                object=self.object,\n                metadata=self.metadata)\n\n\nclass PutSingleDBMetadata(DDLOperation):\n    def __init__(self, dbname, metadata, **kwargs):\n        super().__init__(**kwargs)\n        self.dbname = dbname\n        self.metadata = metadata\n\n    @property\n    def key(self):\n        return f'{self.dbname}metadata'\n\n    def __repr__(self):\n        return \\\n            '<{mod}.{cls} Branch({dbname!r}) {metadata!r}>'.format(\n                mod=self.__class__.__module__,\n                cls=self.__class__.__name__,\n                dbname=self.dbname,\n                metadata=self.metadata)\n\n\nclass SetMetadata(PutMetadata):\n    def creation_code(self) -> str:\n        metadata = self.metadata\n\n        object_type = self.object.get_type()\n        object_id = self.object.get_id()\n\n        prefix = ql(defines.EDGEDB_VISIBLE_METADATA_PREFIX)\n        desc = ql(json.dumps(metadata))\n        comment = f'E{prefix} || {desc}'\n\n        return textwrap.dedent(f'''\\\n            'COMMENT ON {object_type} {object_id} IS '\n            || quote_literal({comment})\n        ''')\n\n    def code(self) -> str:\n        return 'EXECUTE ' + self.creation_code() + ';'\n\n\nclass SetSingleDBMetadata(PutSingleDBMetadata):\n    def code(self) -> str:\n        from .. import trampoline\n\n        metadata = ql(json.dumps(self.metadata))\n        return textwrap.dedent(trampoline.fixup_query(f'''\\\n            UPDATE\n                edgedbinstdata_VER.instdata\n            SET\n                json = {metadata}\n            WHERE\n                key = {ql(self.key)};\n        '''))\n\n\nclass UpdateMetadata(PutMetadata):\n    def code_with_block(self, block: base.PLBlock) -> str:\n        metadata_qry = GetMetadata(self.object).code_with_block(block)\n        prefix = ql(defines.EDGEDB_VISIBLE_METADATA_PREFIX)\n        json_v = block.declare_var('jsonb')\n        upd_v = block.declare_var('text')\n        meta_v = block.declare_var('jsonb')\n        block.add_command(f'{json_v} := ({metadata_qry});')\n        upd_metadata = ql(json.dumps(self.metadata))\n        block.add_command(f'{meta_v} := {upd_metadata}::jsonb')\n\n        block.add_command(textwrap.dedent(f'''\\\n            IF {json_v} IS NOT NULL THEN\n                {upd_v} := E{prefix} || ({json_v} || {meta_v})::text;\n            ELSE\n                {upd_v} := E{prefix} || {meta_v}::text;\n            END IF;\n        '''))\n\n        object_type = self.object.get_type()\n        object_id = self.object.get_id()\n\n        return textwrap.dedent(f'''\\\n            IF {upd_v} IS NOT NULL THEN\n                EXECUTE 'COMMENT ON {object_type} {object_id} IS ' ||\n                    quote_literal({upd_v});\n            END IF;\n        ''')\n\n\nclass UpdateSingleDBMetadata(PutSingleDBMetadata):\n    def code_with_block(self, block: base.PLBlock) -> str:\n        from .. import trampoline\n\n        metadata_qry = GetSingleDBMetadata(self.dbname).code_with_block(block)\n        json_v = block.declare_var('jsonb')\n        meta_v = block.declare_var('jsonb')\n        block.add_command(f'{json_v} := ({metadata_qry});')\n        upd_metadata = ql(json.dumps(self.metadata))\n        block.add_command(f'{meta_v} := {upd_metadata}::jsonb')\n\n        return textwrap.dedent(trampoline.fixup_query(f'''\\\n            UPDATE\n                edgedbinstdata_VER.instdata\n            SET\n                json = {json_v} || {meta_v}\n            WHERE\n                key = {ql(self.key)}\n        '''))\n\n\nclass UpdateMetadataSectionMixin:\n    def __init__(self, *args, section, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.section = section\n\n    def _metadata_query(self) -> base.Command:\n        raise NotImplementedError\n\n    def _merge(self, block):\n        metadata_qry = self._metadata_query().code_with_block(block)\n        json_v = block.declare_var('jsonb')\n        meta_v = block.declare_var('jsonb')\n        block.add_command(f'{json_v} := ({metadata_qry});')\n        upd_metadata = ql(json.dumps(self.metadata))\n        block.add_command(\n            f\"{meta_v} := jsonb_strip_nulls(jsonb_build_object(\\n\"\n            f\"    {ql(self.section)},\\n\"\n            f\"    COALESCE({json_v} -> {ql(self.section)}, '{{}}')\"\n            f\" || {upd_metadata}::jsonb\\n\"\n            f\"))\"\n        )\n        return json_v, meta_v\n\n\nclass UpdateMetadataSection(UpdateMetadataSectionMixin, PutMetadata):\n    def _metadata_query(self) -> base.Command:\n        return GetMetadata(self.object)\n\n    def code_with_block(self, block: base.PLBlock) -> str:\n        json_v, meta_v = self._merge(block)\n        upd_v = block.declare_var('text')\n        prefix = ql(defines.EDGEDB_VISIBLE_METADATA_PREFIX)\n        block.add_command(textwrap.dedent(f'''\\\n            IF {json_v} IS NOT NULL THEN\n                {upd_v} := E{prefix} || ({json_v} || {meta_v})::text;\n            ELSE\n                {upd_v} := E{prefix} || {meta_v}::text;\n            END IF;\n        '''))\n\n        object_type = self.object.get_type()\n        object_id = self.object.get_id()\n\n        return textwrap.dedent(f'''\\\n            IF {upd_v} IS NOT NULL THEN\n                EXECUTE 'COMMENT ON {object_type} {object_id} IS ' ||\n                    quote_literal({upd_v});\n            END IF;\n        ''')\n\n\nclass UpdateSingleDBMetadataSection(\n    UpdateMetadataSectionMixin, PutSingleDBMetadata\n):\n    def _metadata_query(self) -> base.Command:\n        return GetSingleDBMetadata(self.dbname)\n\n    def code_with_block(self, block: base.PLBlock) -> str:\n        from .. import trampoline\n\n        json_v, meta_v = self._merge(block)\n        return textwrap.dedent(trampoline.fixup_query(f'''\\\n            UPDATE\n                edgedbinstdata_VER.instdata\n            SET\n                json = {json_v} || {meta_v}\n            WHERE\n                key = {ql(self.key)}\n        '''))\n\n\nclass CreateObject(SchemaObjectOperation):\n\n    def __init__(self, object, **kwargs):\n        super().__init__(object.get_id(), **kwargs)\n        self.object = object\n\n    def generate_extra(self, block: base.PLBlock) -> None:\n        super().generate_extra(block)\n        if self.object.metadata:\n            mdata = SetMetadata(self.object, self.object.metadata)\n            block.add_command(mdata.code_with_block(block))\n\n\nclass RenameObject(SchemaObjectOperation):\n    def __init__(self, object, *, new_name, **kwargs):\n        super().__init__(name=object.name, **kwargs)\n        self.object = object\n        self.altered_object = object.copy()\n        self.altered_object.rename(new_name)\n        self.new_name = new_name\n\n    def generate_extra(self, block: base.PLBlock) -> None:\n        super().generate_extra(block)\n        # FIXME?: This used to be here, in the original code, but it\n        # doesn't work anymore and probably isn't important.\n        # if self.object.metadata:\n        #     mdata = UpdateMetadata(\n        #         self.altered_object, self.altered_object.metadata)\n        #     block.add_command(mdata.code_with_block(block))\n\n\nclass AlterObject(SchemaObjectOperation):\n    def __init__(self, object, **kwargs):\n        super().__init__(object.get_id(), **kwargs)\n        self.object = object\n\n    def generate_extra(self, block: base.PLBlock) -> None:\n        super().generate_extra(block)\n        if self.object.metadata:\n            mdata = SetMetadata(self.object, self.object.metadata)\n            block.add_command(mdata.code_with_block(block))\n\n\nclass DropObject(SchemaObjectOperation):\n\n    def __init__(self, object, **kwargs):\n        super().__init__(object.get_id(), **kwargs)\n        self.object = object\n"
  },
  {
    "path": "edb/pgsql/dbops/domains.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Iterable,\n    Mapping,\n    Optional,\n    Sequence,\n    TypeAlias,\n)\n\nimport textwrap\n\nfrom ..common import qname as qn\nfrom ..common import quote_literal as ql\nfrom ..common import quote_type as qt\n\nfrom . import base\nfrom . import constraints\nfrom . import ddl\n\n\nDomainName: TypeAlias = tuple[str, ...]\n\n\nclass DomainExists(base.Condition):\n    def __init__(self, name: DomainName):\n        self.name = name\n\n    def code(self) -> str:\n        return textwrap.dedent(f'''\\\n            SELECT\n                domain_name\n            FROM\n                information_schema.domains\n            WHERE\n                domain_schema = {ql(self.name[0])}\n                AND domain_name = {ql(self.name[1])}\n        ''')\n\n\nclass Domain(base.DBObject):\n\n    def __init__(\n        self,\n        name: DomainName,\n        *,\n        base: str | DomainName,\n        constraints: Sequence[DomainConstraint] = (),\n        metadata: Optional[Mapping[str, Any]] = None\n    ):\n        self.constraints = tuple(constraints)\n        self.base = base\n        self.name = name\n        super().__init__(metadata=metadata)\n\n\nclass CreateDomain(ddl.SchemaObjectOperation):\n    def __init__(\n        self,\n        domain: Domain,\n        *,\n        conditions: Optional[Iterable[str | base.Condition]] = None,\n        neg_conditions: Optional[Iterable[str | base.Condition]] = None,\n    ) -> None:\n        super().__init__(\n            domain.name, conditions=conditions, neg_conditions=neg_conditions\n        )\n        self.domain = domain\n\n    def code_with_block(self, block: base.PLBlock) -> str:\n        extra: list[str] = []\n        for constraint in self.domain.constraints:\n            extra.append(constraint.constraint_code(block))\n\n        return textwrap.dedent(f'''\\\n            CREATE DOMAIN {qn(*self.domain.name)}\n            AS {qt(self.domain.base)}\n            {' '.join(extra) if extra else ''}\n        ''').strip()\n\n\nclass AlterDomain(ddl.DDLOperation):\n    def __init__(\n        self,\n        name: DomainName,\n        *,\n        conditions: Optional[Iterable[str | base.Condition]] = None,\n        neg_conditions: Optional[Iterable[str | base.Condition]] = None,\n    ) -> None:\n        super().__init__(conditions=conditions, neg_conditions=neg_conditions)\n        self.name = name\n\n    def prefix_code(self) -> str:\n        return f'ALTER DOMAIN {qn(*self.name)}'\n\n    def __repr__(self) -> str:\n        return '<edb.sync.%s %s>' % (self.__class__.__name__, self.name)\n\n\nclass AlterDomainAlterDefault(AlterDomain):\n    def __init__(\n        self,\n        name: DomainName,\n        default: Optional[str]\n    ) -> None:\n        super().__init__(name)\n        self.default = default\n\n    def code(self) -> str:\n        code = self.prefix_code()\n        if self.default is None:\n            code += ' DROP DEFAULT '\n        else:\n            if self.default is not None:\n                value = ql(str(self.default))\n            else:\n                value = 'None'\n            code += f' SET DEFAULT {value}'\n        return code\n\n\nclass AlterDomainAlterNull(AlterDomain):\n    def __init__(self, name: DomainName, null: bool) -> None:\n        super().__init__(name)\n        self.null = null\n\n    def code(self) -> str:\n        code = self.prefix_code()\n        if self.null:\n            code += ' DROP NOT NULL '\n        else:\n            code += ' SET NOT NULL '\n        return code\n\n\nclass AlterDomainAlterConstraint(AlterDomain):\n    def __init__(\n        self,\n        name: DomainName,\n        constraint: DomainConstraint,\n        *,\n        conditions: Optional[Iterable[str | base.Condition]] = None,\n        neg_conditions: Optional[Iterable[str | base.Condition]] = None,\n    ) -> None:\n        super().__init__(\n            name, conditions=conditions, neg_conditions=neg_conditions)\n        self._constraint = constraint\n\n\nclass DomainConstraint(constraints.Constraint):\n    def get_subject_type(self) -> str:\n        return 'DOMAIN'\n\n    def constraint_code(self, block: base.PLBlock) -> str:\n        raise NotImplementedError()\n\n\nclass DomainCheckConstraint(DomainConstraint):\n\n    def __init__(\n        self,\n        domain_name: DomainName,\n        constraint_name: Optional[str] = None,\n        *,\n        expr: base.Query | str,\n    ) -> None:\n        super().__init__(domain_name, constraint_name=constraint_name)\n        self.expr = expr\n\n    def constraint_code(self, block: base.PLBlock) -> str:\n        if isinstance(self.expr, base.Query):\n            assert self.expr.type\n            var = block.declare_var(self.expr.type)\n            indent = len(var) + 5\n            expr_text = textwrap.indent(self.expr.text, ' ' * indent).strip()\n            block.add_command(f'{var} := ({expr_text})')\n\n            code = f\"'CHECK (' || {var} || ')'\"\n            code = base.PLExpression(code)\n\n        else:\n            code = f'CHECK ({self.expr})'\n\n        return code\n\n\nclass AlterDomainAddConstraint(AlterDomainAlterConstraint):\n    def code_with_block(self, block: base.PLBlock) -> str:\n        code = self.prefix_code()\n        constr_name = self._constraint.constraint_name()\n        constr_code = self._constraint.constraint_code(block)\n        if isinstance(constr_code, base.PLExpression):\n            code = (f\"EXECUTE {ql(code)} || ' ADD CONSTRAINT {constr_name} ' \"\n                    f\"|| {constr_code}\")\n        else:\n            code += f' ADD CONSTRAINT {constr_name} {constr_code}'\n        return code\n\n    def generate_extra(self, block: base.PLBlock) -> None:\n        return self._constraint.generate_extra(block)\n\n\nclass AlterDomainDropConstraint(AlterDomainAlterConstraint):\n    def code(self) -> str:\n        code = super().prefix_code()\n        code += f' DROP CONSTRAINT {self._constraint.constraint_name()}'\n        return code\n\n\nclass DropDomain(ddl.SchemaObjectOperation):\n    def code(self) -> str:\n        return f'DROP DOMAIN {qn(*self.name)}'\n"
  },
  {
    "path": "edb/pgsql/dbops/enums.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Iterable,\n    Mapping,\n    Optional,\n    Sequence,\n    TypeAlias,\n)\n\nimport textwrap\n\nfrom ..common import qname as qn\nfrom ..common import quote_literal as ql\n\nfrom . import base\nfrom . import ddl\n\n\nEnumName: TypeAlias = tuple[str, str]\n\n\nclass EnumExists(base.Condition):\n    def __init__(self, name: EnumName) -> None:\n        self.name = name\n\n    def code(self) -> str:\n        return textwrap.dedent(f'''\\\n            SELECT\n                t.typname\n            FROM\n                pg_catalog.pg_type t\n                INNER JOIN pg_namespace nsp\n                    ON (t.typnamespace = nsp.oid)\n            WHERE\n                nsp.nspname = {ql(self.name[0])}\n                AND t.typname = {ql(self.name[1])}\n                AND t.typtype = 'e'\n        ''')\n\n\nclass Enum(base.DBObject):\n    def __init__(\n        self,\n        name: EnumName,\n        values: Sequence[str],\n        *,\n        metadata: Optional[Mapping[str, Any]] = None,\n    ) -> None:\n        self.name = name\n        self.values = values\n        super().__init__(metadata=metadata)\n\n\nclass CreateEnum(ddl.SchemaObjectOperation):\n    def __init__(\n        self,\n        enum: Enum,\n        *,\n        conditions: Optional[Iterable[str | base.Condition]] = None,\n        neg_conditions: Optional[Iterable[str | base.Condition]] = None,\n    ) -> None:\n        super().__init__(\n            enum.name, conditions=conditions, neg_conditions=neg_conditions)\n        self.values = enum.values\n\n    def code(self) -> str:\n        vals = ', '.join(ql(v) for v in self.values)\n        return f'CREATE TYPE {qn(*self.name)} AS ENUM ({vals})'\n\n\nclass AlterEnum(ddl.DDLOperation):\n    def __init__(\n        self,\n        name: EnumName,\n        *,\n        conditions: Optional[Iterable[str | base.Condition]] = None,\n        neg_conditions: Optional[Iterable[str | base.Condition]] = None,\n    ) -> None:\n        super().__init__(conditions=conditions, neg_conditions=neg_conditions)\n        self.name = name\n\n    def prefix_code(self) -> str:\n        return f'ALTER TYPE {qn(*self.name)}'\n\n    def __repr__(self) -> str:\n        return '<edb.sync.%s %s>' % (self.__class__.__name__, self.name)\n\n\nclass AlterEnumAddValue(AlterEnum):\n    def __init__(\n        self,\n        name: EnumName,\n        value: str,\n        *,\n        before: Optional[str] = None,\n        after: Optional[str] = None,\n        conditional: bool = False,\n    ):\n        super().__init__(name)\n        self.value = value\n        self.before = before\n        self.after = after\n        self.conditional = conditional\n\n    def code(self) -> str:\n        code = self.prefix_code()\n        code += ' ADD VALUE'\n        if self.conditional:\n            code += ' IF NOT EXISTS'\n        code += f' {ql(self.value)}'\n        if self.before:\n            code += f' BEFORE {ql(self.before)}'\n        elif self.after:\n            code += f' AFTER {ql(self.after)}'\n\n        return code\n\n\nclass DropEnum(ddl.SchemaObjectOperation):\n    def code(self) -> str:\n        return f'DROP TYPE {qn(*self.name)}'\n"
  },
  {
    "path": "edb/pgsql/dbops/extensions.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom ..common import quote_ident as qi\n\nfrom . import ddl\n\n\nclass Extension:\n    def __init__(self, name, schema='edgedb'):\n        self.name = name\n        self.schema = schema\n\n    def get_extension_name(self):\n        return self.name\n\n    def code(self) -> str:\n        name = qi(self.get_extension_name())\n        schema = qi(self.schema)\n        return f'CREATE EXTENSION {name} WITH SCHEMA {schema}'\n\n\nclass CreateExtension(ddl.DDLOperation):\n    def __init__(\n        self,\n        extension,\n        *,\n        conditions=None,\n        neg_conditions=None,\n        conditional=False,\n    ):\n        super().__init__(conditions=conditions, neg_conditions=neg_conditions)\n        self.extension = extension\n        self.opid = extension.name\n        self.conditional = conditional\n\n    def code(self) -> str:\n        ext = self.extension\n        name = qi(ext.get_extension_name())\n        schema = qi(ext.schema)\n        condition = \"IF NOT EXISTS \" if self.conditional else ''\n        return f'CREATE EXTENSION {condition}{name} WITH SCHEMA {schema}'\n\n\nclass DropExtension(ddl.DDLOperation):\n    def __init__(self, extension, *, conditions=None, neg_conditions=None):\n        super().__init__(conditions=conditions, neg_conditions=neg_conditions)\n        self.extension = extension\n        self.opid = extension.name\n\n    def code(self) -> str:\n        ext = self.extension\n        name = qi(ext.get_extension_name())\n        return f'DROP EXTENSION {name}'\n"
  },
  {
    "path": "edb/pgsql/dbops/functions.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport textwrap\nfrom typing import Optional, Sequence, cast\n\nfrom ..common import qname as qn\nfrom ..common import quote_literal as ql\nfrom ..common import quote_type as qt\n\nfrom . import base\nfrom . import ddl\n\nFunctionArgType = str | tuple[str, ...]\nFunctionArgTyped = tuple[Optional[str], FunctionArgType]\nFunctionArgDefaulted = tuple[Optional[str], FunctionArgType, str]\nFunctionArg = FunctionArgTyped | FunctionArgDefaulted\n\nNormalizedFunctionArg = tuple[Optional[str], tuple[str, ...], Optional[str]]\n\n\nclass Function(base.DBObject):\n    def __init__(\n        self,\n        name: tuple[str, ...],\n        *,\n        args: Optional[Sequence[FunctionArg]] = None,\n        returns: str | tuple[str, ...],\n        text: str,\n        volatility: str = \"volatile\",\n        language: str = \"sql\",\n        has_variadic: Optional[bool] = None,\n        strict: bool = False,\n        parallel_safe: bool = False,\n        set_returning: bool = False,\n\n        # Unused for Function, used in VersionedFunction.\n        wrapper_volatility: Optional[str] = None,\n    ):\n        if volatility.lower() == 'modifying':\n            volatility = 'volatile'\n\n        self.name = name\n        self.args = args\n        self.returns = returns\n        self.text = text\n        self.volatility = volatility\n        self.language = language\n        self.has_variadic = has_variadic\n        self.strict = strict\n        self.set_returning = set_returning\n        self.parallel_safe = parallel_safe\n\n    def __repr__(self):\n        return '<{} {} at 0x{}>'.format(\n            self.__class__.__name__, self.name, id(self))\n\n\nclass FunctionExists(base.Condition):\n    def __init__(self, name, args=None):\n        self.name = name\n        self.args = FunctionOperation.normalize_args(args)\n\n    def code(self) -> str:\n        targs = [f\"{ql(qt(a))}::regtype::oid\" for _, a, _ in self.args]\n        args = f\"ARRAY[{','.join(targs)}]\"\n\n        return textwrap.dedent(f'''\\\n            SELECT\n                p.proname\n            FROM\n                pg_catalog.pg_proc p\n                INNER JOIN pg_catalog.pg_namespace ns\n                    ON (ns.oid = p.pronamespace)\n            WHERE\n                p.proname = {ql(self.name[1])}\n                AND ns.nspname = {ql(self.name[0])}\n                AND {args}::oid[] = ARRAY(\n                    SELECT\n                        t\n                    FROM\n                        unnest(p.proargtypes) AS t)\n        ''')\n\n\nclass FunctionOperation:\n    @staticmethod\n    def normalize_args(\n        args: Optional[Sequence[FunctionArg]]\n    ) -> Sequence[NormalizedFunctionArg]:\n        normed = []\n\n        for arg in args or ():\n            name = None\n            default = None\n            if isinstance(arg, tuple):\n                name = arg[0]\n                typ = arg[1]\n                if len(arg) > 2:\n                    arg_def = cast(FunctionArgDefaulted, arg)\n                    default = arg_def[2]\n\n            else:\n                typ = arg\n\n            ttyp = (typ,) if isinstance(typ, str) else typ\n\n            normed.append((name, ttyp, default))\n\n        return normed\n\n    @staticmethod\n    def format_args(\n        args: Optional[Sequence[FunctionArg]],\n        has_variadic: Optional[bool],\n        *,\n        include_defaults: bool = True,\n    ) -> str:\n        if not args:\n            return ''\n\n        args_buf = []\n        normed = FunctionOperation.normalize_args(args)\n        for argi, (arg_name, arg_typ, arg_def) in enumerate(normed, 1):\n            vararg = has_variadic and (len(args) == argi)\n            arg_expr = 'VARIADIC ' if vararg else ''\n            if arg_name is not None:\n                arg_expr += qn(arg_name, column=True)\n            arg_expr += ' ' + qt(arg_typ)\n            if include_defaults:\n                if arg_def:\n                    arg_expr += ' = ' + arg_def\n\n            args_buf.append(arg_expr)\n\n        return ', '.join(args_buf)\n\n\nclass CreateFunction(ddl.DDLOperation, FunctionOperation):\n    def __init__(\n        self, function: Function, *, or_replace: bool = False, **kwargs\n    ):\n        super().__init__(**kwargs)\n        self.function = function\n        self.or_replace = or_replace\n\n    def code(self) -> str:\n        args = self.format_args(self.function.args, self.function.has_variadic)\n\n        code = textwrap.dedent('''\n            CREATE {replace} FUNCTION {name}({args})\n            RETURNS {setof} {returns}\n            AS $____funcbody____$\n            {text}\n            $____funcbody____$\n            LANGUAGE {lang} {volatility} {strict} {parallel};\n        ''').format_map({\n            'replace': 'OR REPLACE' if self.or_replace else '',\n            'name': qn(*self.function.name),\n            'args': args,\n            'returns': qt(self.function.returns),\n            'lang': self.function.language,\n            'volatility': self.function.volatility.upper(),\n            'text': textwrap.dedent(self.function.text).strip(),\n            'strict': 'STRICT' if self.function.strict else '',\n            'setof': 'SETOF' if self.function.set_returning else '',\n            'parallel': (\n                'PARALLEL '\n                + ('SAFE' if self.function.parallel_safe else 'UNSAFE')\n            ),\n        })\n        return code.strip()\n\n\nclass DropFunction(ddl.DDLOperation, FunctionOperation):\n    def __init__(\n        self,\n        name: tuple[str, ...],\n        args: Sequence[FunctionArg],\n        *,\n        if_exists: bool = False,\n        has_variadic: bool = False,\n        conditions: Optional[list[str | base.Condition]] = None,\n        neg_conditions: Optional[list[str | base.Condition]] = None,\n    ):\n        self.conditional = if_exists\n        super().__init__(conditions=conditions, neg_conditions=neg_conditions)\n        self.name = name\n        self.args = args\n        self.has_variadic = has_variadic\n\n    def code(self) -> str:\n        ifexists = ' IF EXISTS' if self.conditional else ''\n        args = self.format_args(self.args, self.has_variadic,\n                                include_defaults=False)\n        return f'DROP FUNCTION{ifexists} {qn(*self.name)}({args})'\n"
  },
  {
    "path": "edb/pgsql/dbops/indexes.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport re\nimport textwrap\nfrom typing import Any, Iterable\n\nfrom edb.common import ordered\n\nfrom ..common import qname as qn\nfrom ..common import quote_ident as qi\nfrom ..common import quote_literal as ql\nfrom .. import ast as pgast\n\nfrom . import base\nfrom . import ddl\nfrom . import tables\n\n\nclass Index(tables.InheritableTableObject):\n    def __init__(\n        self,\n        name: str,\n        table_name: tuple[str, str],\n        unique: bool = True,\n        exprs: Iterable[str] | None = None,\n        with_clause: dict[str, str] | None = None,\n        predicate: str | None = None,\n        inherit: bool = False,\n        metadata: dict[str, Any] | None = None,\n        columns: Iterable[str | pgast.Star] | None = None,\n    ) -> None:\n        super().__init__(inherit=inherit, metadata=metadata)\n\n        assert table_name[1] != 'feature'\n\n        self.name = name\n        self.table_name = table_name\n        self._columns: ordered.OrderedSet[str] = ordered.OrderedSet()\n        if columns:\n            self.add_columns(columns)\n        self.with_clause = with_clause\n        self.predicate = predicate\n        self.unique = unique\n        self.exprs = exprs\n\n        self.add_metadata('fullname', self.name)\n\n    @property\n    def name_in_catalog(self) -> str:\n        return self.name\n\n    def add_columns(self, columns: Iterable[str | pgast.Star]) -> None:\n        for col in columns:\n            if isinstance(col, pgast.Star):\n                raise NotImplementedError()\n            self._columns.add(col)\n\n    def rename(self, new_name):\n        self.name = new_name\n\n    def creation_code(\n        self,\n        concurrently: bool = False,\n        conditional: bool = False,\n    ) -> str:\n        if self.exprs:\n            exprs = self.exprs\n        else:\n            exprs = [qi(c) for c in self.columns]\n\n        # Break down the code into the index name (if present) and the rest of\n        # the expression\n        m = re.match(r'(?P<using>\\w+)?\\s*(?P<expr>.+)',\n                     self.get_metadata('code').strip())\n        assert m is not None\n        using = m['using']\n        expr = m['expr']\n\n        code = 'CREATE'\n        if self.unique:\n            code += ' UNIQUE'\n        code += f' INDEX'\n        if concurrently:\n            code += ' CONCURRENTLY'\n        if conditional:\n            code += ' IF NOT EXISTS'\n\n        code += f' {qn(self.name_in_catalog)} ON {qn(*self.table_name)}'\n\n        if using:\n            code += f' USING {using}'\n\n        # expr is expected to be wrapped in parentheses, but in order to\n        # manipulate it better we strip the parentheses\n        expr = expr[1:-1].replace('__col__', '{col}')\n        expr = ', '.join(expr.format(col=e) for e in exprs)\n\n        kwargs = self.get_metadata('kwargs')\n        if kwargs is not None:\n            # Escape the expression first\n            expr = expr.replace('{', '{{').replace('}', '}}')\n            expr = re.sub(r'(__kw_(\\w+?)__)', r'{\\2}', expr)\n            expr = expr.format(**kwargs)\n\n        # Put the stripped parentheses back\n        code += f'({expr})'\n\n        if self.with_clause:\n            with_content = ','.join(\n                f'{k}={v}' for (k, v) in self.with_clause.items()\n            )\n            code += f' WITH ({with_content})'\n\n        if self.predicate:\n            code += f' WHERE {self.predicate}'\n\n        return code\n\n    @property\n    def columns(self) -> list[str]:\n        return list(self._columns)\n\n    def get_type(self) -> str:\n        return 'INDEX'\n\n    def get_id(self) -> str:\n        return qn(self.table_name[0], self.name_in_catalog)\n\n    def get_oid(self) -> base.Query:\n        qry = textwrap.dedent(f'''\\\n            SELECT\n                'pg_class'::regclass::oid AS classoid,\n                i.indexrelid AS objectoid,\n                0\n            FROM\n                pg_class AS c\n                INNER JOIN pg_namespace AS ns ON ns.oid = c.relnamespace\n                INNER JOIN pg_index AS i ON i.indrelid = c.oid\n                INNER JOIN pg_class AS ic ON i.indexrelid = ic.oid\n            WHERE\n                ic.relname = {ql(self.name_in_catalog)}\n                AND ns.nspname = {ql(self.table_name[0])}\n                AND c.relname = {ql(self.table_name[1])}\n        ''')\n\n        return base.Query(text=qry)\n\n    def copy(self) -> Index:\n        return self.__class__(\n            name=self.name,\n            table_name=self.table_name,\n            unique=self.unique,\n            exprs=self.exprs,\n            predicate=self.predicate,\n            columns=self.columns,\n            metadata=(\n                self.metadata.copy() if self.metadata is not None else None\n            )\n        )\n\n    def __repr__(self) -> str:\n        return \\\n            '<%(mod)s.%(cls)s table=%(table)s name=%(name)s ' \\\n            'cols=(%(cols)s) unique=%(uniq)s predicate=%(pred)s>' % \\\n            {'mod': self.__class__.__module__,\n             'cls': self.__class__.__name__,\n             'name': self.name,\n             'cols': ','.join('%r' % c for c in self.columns),\n             'uniq': self.unique,\n             'pred': self.predicate,\n             'table': '{}.{}'.format(*self.table_name)}\n\n\nclass IndexExists(base.Condition):\n    def __init__(self, index_name: tuple[str, str]):\n        self.index_name = index_name\n\n    def code(self) -> str:\n        return textwrap.dedent(f'''\\\n            SELECT\n                   i.indexrelid\n               FROM\n                   pg_catalog.pg_index i\n                   INNER JOIN pg_catalog.pg_class ic\n                        ON ic.oid = i.indexrelid\n                   INNER JOIN pg_catalog.pg_namespace icn\n                        ON icn.oid = ic.relnamespace\n               WHERE\n                   icn.nspname = {ql(self.index_name[0])}\n                   AND ic.relname = {ql(self.index_name[1])}\n        ''')\n\n\nclass CreateIndex(ddl.CreateObject):\n    def __init__(\n        self,\n        index: Index,\n        *,\n        conditional: bool = False,\n        builtin_conditional: bool = False,\n        concurrently: bool = False,\n        **kwargs: Any\n    ) -> None:\n        super().__init__(index, **kwargs)\n        self.index = index\n        self.concurrently = concurrently\n        self.builtin_conditional = builtin_conditional\n        if conditional:\n            self.neg_conditions.add(\n                IndexExists((index.table_name[0], index.name_in_catalog))\n            )\n\n    def code(self) -> str:\n        return self.index.creation_code(\n            concurrently=self.concurrently,\n            conditional=self.builtin_conditional,\n        )\n\n\nclass RenameIndex(ddl.RenameObject):\n    def __init__(self, index, *, new_name, conditional=False, **kwargs):\n        super().__init__(index, new_name=new_name, **kwargs)\n        if conditional:\n            self.conditions.add(\n                IndexExists((index.table_name[0], index.name_in_catalog)))\n\n    def code(self) -> str:\n        name = qn(self.object.table_name[0], self.object.name_in_catalog)\n        new_name = qi(self.altered_object.name_in_catalog)\n        return f'ALTER INDEX {name} RENAME TO {new_name}'\n\n    @classmethod\n    def pl_code(cls, index_desc_var: str, block: base.PLBlock) -> str:\n        index_name = (\n            f\"(quote_ident({index_desc_var}.table_name[0])\"\n            f\" || '.' || quote_ident({index_desc_var}.name))\"\n        )\n        new_name = (\n            f\"quote_ident({index_desc_var}.name)\"\n        )\n\n        return (\n            f\"EXECUTE 'ALTER INDEX ' || {index_name} \"\n            f\"|| ' RENAME TO ' || {new_name};\"\n        )\n\n\nclass DropIndex(ddl.DropObject):\n    def __init__(\n        self,\n        index: Index,\n        *,\n        conditional: bool = False,\n        **kwargs: Any\n    ):\n        super().__init__(index, **kwargs)\n        if conditional:\n            self.conditions.add(\n                IndexExists((index.table_name[0], index.name_in_catalog))\n            )\n\n    def code(self) -> str:\n        assert isinstance(self.object, Index)\n        name = qn(self.object.table_name[0], self.object.name_in_catalog)\n        return f'DROP INDEX {name}'\n"
  },
  {
    "path": "edb/pgsql/dbops/operators.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport textwrap\n\nfrom ..common import quote_ident as qi\nfrom ..common import quote_literal as ql\nfrom ..common import quote_type as qt\n\nfrom . import base\nfrom . import ddl\n\n\nclass CreateOperatorAlias(ddl.SchemaObjectOperation):\n    def __init__(\n        self,\n        *,\n        name,\n        args,\n        base_operator,\n        operator_args,\n        negator=None,\n        commutator=None,\n        procedure=None,\n        **kwargs,\n    ):\n        super().__init__(name=name, **kwargs)\n        self.args = args\n        self.operator = base_operator\n        self.operator_args = operator_args\n        self.procedure = procedure\n        self.commutator = commutator\n        self.negator = negator\n\n    def code_with_block(self, block: base.PLBlock) -> str:\n        oper_var = block.declare_var(('pg_catalog', 'pg_operator%ROWTYPE'))\n        oper_cond = []\n\n        oper_name = f'{qi(self.name[0])}.{self.name[1]}'\n\n        if self.args[0] is not None:\n            left_type_desc = qt(self.args[0])\n            left_type = f\"', LEFTARG = {left_type_desc}'\"\n            oper_cond.append(\n                f'o.oprleft = {ql(qt(self.operator_args[0]))}::regtype')\n        else:\n            left_type_desc = 'NONE'\n            left_type = \"''\"\n            oper_cond.append(f'o.oprleft = 0')\n\n        if self.args[1] is not None:\n            right_type_desc = qt(self.args[1])\n            right_type = f\"', RIGHTARG = {right_type_desc}'\"\n            oper_cond.append(\n                f'o.oprright = {ql(qt(self.operator_args[1]))}::regtype')\n        else:\n            right_type_desc = 'NONE'\n            right_type = \"''\"\n            oper_cond.append(f'o.oprright = 0')\n\n        oper_desc = (\n            f'{qi(self.operator[0])}.{self.operator[1]} ('\n            f'{left_type_desc}, {right_type_desc})'\n        ).strip()\n\n        if self.commutator:\n            commutator_name = f'{qi(self.commutator[0])}.{self.commutator[1]}'\n            commutator_decl = textwrap.indent(textwrap.dedent(f'''\\\n                ', COMMUTATOR = OPERATOR({commutator_name})'\n            '''), ' ' * 8).strip()\n            commutator_cond = 'TRUE'\n        else:\n            commutator_decl = textwrap.indent(textwrap.dedent(f'''\\\n                ', COMMUTATOR = ' || (\n                    SELECT edgedb.raise(\n                        NULL::text,\n                        'invalid_object_definition',\n                        msg => (\n                            'missing required commutator for operator '\n                            || {ql(oper_name)}\n                        )\n                    )\n                )\n            '''), ' ' * 8).strip()\n            commutator_cond = 'FALSE'\n\n        if self.negator:\n            negator_name = f'{qi(self.negator[0])}.{self.negator[1]}'\n            negator_decl = textwrap.indent(textwrap.dedent(f'''\\\n                ', NEGATOR = OPERATOR({negator_name})'\n            '''), ' ' * 8).strip()\n            negator_cond = 'TRUE'\n        else:\n            negator_decl = textwrap.indent(textwrap.dedent(f'''\\\n                ', NEGATOR = ' || (\n                    SELECT edgedb.raise(\n                        NULL::text,\n                        'invalid_object_definition',\n                        msg => (\n                            'missing required negator for operator '\n                            || {ql(oper_name)}\n                        )\n                    )\n                )\n            '''), ' ' * 8).strip()\n            negator_cond = 'FALSE'\n\n        def _get_op_field(field, oid):\n            return textwrap.indent(textwrap.dedent(f'''\\\n                (CASE WHEN {oid} != 0 THEN\n                 ', {field} = ' || (\n                    SELECT\n                        'OPERATOR('\n                        || quote_ident(nspname) || '.' || oprname\n                        || ')'\n                    FROM\n                        pg_operator o\n                        INNER JOIN pg_namespace ns\n                            ON (o.oprnamespace = ns.oid)\n                    WHERE o.oid = {oid}\n                 )\n                 ELSE ''\n                 END)\n            '''), ' ' * 8).strip()\n\n        code = textwrap.dedent('''\\\n            SELECT\n                o.*\n            INTO\n                {oper}\n            FROM\n                pg_operator o\n                INNER JOIN pg_namespace ns\n                    ON (o.oprnamespace = ns.oid)\n            WHERE\n                o.oprname = {oper_name}\n                AND ns.nspname = {oper_schema}\n                AND {oper_cond}\n            ;\n\n            IF NOT FOUND THEN\n                RAISE\n                    'SQL operator does not exist: %',\n                    {oper_desc}\n                    USING ERRCODE = 'undefined_function';\n            END IF;\n\n            EXECUTE\n                'CREATE OPERATOR {name} ('\n                || 'PROCEDURE = ' || {procedure}\n                || {left_type}\n                || {right_type}\n                || {commutator}\n                || {negator}\n                || {restrict}\n                || {join}\n                || {hashes}\n                || {merges}\n                || ')'\n                ;\n        ''').format_map({\n            'name': oper_name,\n            'oper': oper_var,\n            'procedure': (ql(self.procedure) if self.procedure\n                          else f'{oper_var}.oprcode::text'),\n            'oper_schema': ql(self.operator[0]),\n            'oper_name': ql(self.operator[1]),\n            'oper_cond': ' AND '.join(oper_cond),\n            'oper_desc': ql(oper_desc),\n            'left_type': left_type,\n            'right_type': right_type,\n            'commutator': (\n                f\"(CASE WHEN {oper_var}.oprcom != 0 OR {commutator_cond} THEN \"\n                f\"{commutator_decl} \"\n                f\"ELSE '' END)\"\n            ),\n            'negator': (\n                f\"(CASE WHEN {oper_var}.oprnegate != 0 OR {negator_cond} THEN \"\n                f\"{negator_decl} \"\n                f\"ELSE '' END)\"\n            ),\n            'restrict': (\n                f\"(CASE WHEN {oper_var}.oprrest != 0 THEN \"\n                f\"', RESTRICT = ' || {oper_var}.oprrest::text \"\n                f\"ELSE '' END)\"\n            ),\n            'join': (\n                f\"(CASE WHEN {oper_var}.oprjoin != 0 THEN \"\n                f\"', JOIN = ' || {oper_var}.oprjoin::text \"\n                f\"ELSE '' END)\"\n            ),\n            'hashes': (\n                f\"(CASE WHEN {oper_var}.oprcanhash THEN \"\n                f\"', HASHES ' \"\n                f\"ELSE '' END)\"\n            ),\n            'merges': (\n                f\"(CASE WHEN {oper_var}.oprcanmerge THEN \"\n                f\"', MERGES ' \"\n                f\"ELSE '' END)\"\n            ),\n        })\n        return code.strip()\n\n\nclass CreateOperator(ddl.SchemaObjectOperation):\n    def __init__(self, *, name, args, procedure, **kwargs):\n        super().__init__(name=name, **kwargs)\n        self.args = args\n        self.procedure = procedure\n\n    def code(self) -> str:\n        if self.args[0] is not None:\n            left_type_desc = qt(self.args[0])\n            left_type = f\", LEFTARG = {left_type_desc}\"\n        else:\n            left_type = \"\"\n\n        if self.args[1] is not None:\n            right_type_desc = qt(self.args[1])\n            right_type = f\", RIGHTARG = {right_type_desc}\"\n        else:\n            right_type = \"\"\n\n        return textwrap.dedent(f'''\\\n            CREATE OPERATOR {qi(self.name[0])}.{self.name[1]} (\n                PROCEDURE = {self.procedure}\n                {left_type}\n                {right_type}\n            );\n        ''')\n\n\nclass DropOperator(ddl.SchemaObjectOperation):\n    def __init__(self, *, name, args, **kwargs):\n        super().__init__(name=name, **kwargs)\n        self.args = args\n\n    def code(self) -> str:\n        if self.args[0] is not None:\n            left_type = qt(self.args[0])\n        else:\n            left_type = 'NONE'\n\n        if self.args[1] is not None:\n            right_type = qt(self.args[1])\n        else:\n            right_type = 'NONE'\n\n        return textwrap.dedent(f'''\\\n            DROP OPERATOR {qi(self.name[0])}.{self.name[1]} (\n                {left_type}, {right_type}\n            );\n        ''')\n"
  },
  {
    "path": "edb/pgsql/dbops/ranges.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport textwrap\n\nfrom ..common import qname as qn\nfrom ..common import quote_literal as ql\n\nfrom . import base\nfrom . import ddl\n\n\nclass RangeExists(base.Condition):\n    def __init__(self, name):\n        self.name = name\n\n    def code(self) -> str:\n        return textwrap.dedent(f'''\\\n            SELECT\n                t.typname\n            FROM\n                pg_catalog.pg_type t\n                INNER JOIN pg_namespace nsp\n                    ON (t.typnamespace = nsp.oid)\n            WHERE\n                nsp.nspname = {ql(self.name[0])}\n                AND t.typname = {ql(self.name[1])}\n                AND t.typtype = 'r'\n        ''')\n\n\nclass Range(base.DBObject):\n    def __init__(self, name, subtype, *, subtype_diff=None):\n        super().__init__()\n        self.name = name\n        self.subtype = subtype\n        self.subtype_diff = subtype_diff\n\n\nclass CreateRange(ddl.SchemaObjectOperation):\n    def __init__(self, range, *, conditions=None, neg_conditions=None):\n        super().__init__(\n            range.name, conditions=conditions, neg_conditions=neg_conditions)\n        self.range = range\n\n    def code(self) -> str:\n        subs = [f'subtype = {qn(*self.range.subtype)}']\n        if self.range.subtype_diff is not None:\n            subs.append(f'subtype_diff = {qn(*self.range.subtype_diff)}')\n        subcommands = ', '.join(subs)\n        return f'''\\\n            CREATE TYPE {qn(*self.name)} AS RANGE ({subcommands})\n        '''\n\n\nclass DropRange(ddl.SchemaObjectOperation):\n    def code(self) -> str:\n        return f'DROP TYPE {qn(*self.name)}'\n"
  },
  {
    "path": "edb/pgsql/dbops/roles.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Iterable,\n    Mapping,\n    Optional,\n    TypeAlias,\n)\n\nimport json\nimport textwrap\n\nfrom ..common import quote_ident as qi\nfrom ..common import quote_literal as ql\n\n\nfrom . import base\nfrom . import ddl\n\n\nRoleName: TypeAlias = str\n\n\nclass Role(base.DBObject):\n    def __init__(\n        self,\n        name: RoleName,\n        *,\n        allow_login: bool | base.NotSpecifiedT = base.NotSpecified,\n        allow_createdb: bool | base.NotSpecifiedT = base.NotSpecified,\n        allow_createrole: bool | base.NotSpecifiedT = base.NotSpecified,\n        password: None | str | base.NotSpecifiedT = base.NotSpecified,\n        superuser: bool | base.NotSpecifiedT = base.NotSpecified,\n        membership: Optional[Iterable[str]] = None,\n        members: Optional[Iterable[str]] = None,\n        metadata: Optional[Mapping[str, Any]] = None,\n    ) -> None:\n        super().__init__(metadata=metadata)\n        self.name = name\n        self.superuser = superuser\n        self.allow_login = allow_login\n        self.allow_createdb = allow_createdb\n        self.allow_createrole = allow_createrole\n        self.password = password\n        self.membership = membership\n        self.members = members\n\n    def get_type(self) -> str:\n        return 'ROLE'\n\n    def get_id(self) -> str:\n        return qi(self.name)\n\n\nclass SingleRole(Role):\n    def __init__(\n        self,\n        *,\n        password: None | str | base.NotSpecifiedT = base.NotSpecified,\n        metadata: Optional[Mapping[str, Any]] = None,\n    ) -> None:\n        super().__init__('current_user', password=password)\n        self.single_role_metadata = metadata\n\n    def get_id(self) -> str:\n        return self.name\n\n\nclass RoleExists(base.Condition):\n    def __init__(self, name: RoleName):\n        self.name = name\n\n    def code(self) -> str:\n        return textwrap.dedent(f'''\\\n            SELECT\n                rolname\n            FROM\n                pg_catalog.pg_roles\n            WHERE\n                rolname = {ql(self.name)}\n        ''')\n\n\nclass RoleCommand:\n\n    object: Role\n\n    def _role(self) -> str:\n        return f'ROLE {self.object.get_id()}'\n\n    def _attrs(self) -> str:\n        attrs = []\n\n        attrmap = {\n            'superuser': 'SUPERUSER',\n            'allow_login': 'LOGIN',\n            'allow_createdb': 'CREATEDB',\n            'allow_createrole': 'CREATEROLE',\n        }\n\n        for objattr, stmtattr in attrmap.items():\n            attr = getattr(self.object, objattr)\n            if attr is base.NotSpecified:\n                continue\n            elif attr:\n                attrs.append(stmtattr)\n            else:\n                attrs.append(f'NO{stmtattr}')\n\n        if self.object.password is None:\n            attrs.append('PASSWORD NULL')\n        elif self.object.password is not base.NotSpecified:\n            attrs.append(f'PASSWORD {ql(self.object.password)}')\n\n        return \" \".join(attrs)\n\n\nclass CreateRole(ddl.CreateObject, RoleCommand):\n\n    def code(self) -> str:\n        if self.object.membership:\n            roles = ', '.join(qi(str(m)) for m in self.object.membership)\n            membership = f'IN ROLE {roles}'\n        else:\n            membership = ''\n        if self.object.members:\n            roles = ', '.join(qi(str(m)) for m in self.object.members)\n            members = f'ROLE {roles}'\n        else:\n            members = ''\n        return f'CREATE {self._role()} {self._attrs()} {membership} {members}'\n\n\nclass AlterRole(ddl.AlterObject, RoleCommand):\n\n    def code(self) -> str:\n        attrs = self._attrs()\n        if attrs:\n            return f'ALTER {self._role()} {attrs}'\n        else:\n            return ''\n\n    def generate_extra(self, block: base.PLBlock) -> None:\n        from .. import trampoline\n\n        super().generate_extra(block)\n        if getattr(self.object, 'single_role_metadata', None):\n            value = json.dumps(self.object.single_role_metadata)\n            query = base.Query(trampoline.fixup_query(\n                f'''\n                UPDATE edgedbinstdata_VER.instdata\n                SET json = {ql(value)}::jsonb\n                WHERE key = 'single_role_metadata'\n                '''\n            ))\n            block.add_command(query.code_with_block(block))\n\n\nclass DropRole(ddl.SchemaObjectOperation):\n\n    def code(self) -> str:\n        return f'DROP ROLE {qi(self.name)}'\n\n\nclass AlterRoleAddMember(ddl.SchemaObjectOperation):\n\n    def __init__(\n        self,\n        name: RoleName,\n        member: str,\n        *,\n        conditions: Optional[Iterable[str | base.Condition]] = None,\n        neg_conditions: Optional[Iterable[str | base.Condition]] = None,\n    ):\n        super().__init__(\n            name, conditions=conditions, neg_conditions=neg_conditions\n        )\n        self.member = member\n\n    def code(self) -> str:\n        return f'GRANT {qi(self.name)} TO {qi(self.member)}'\n\n\nclass AlterRoleDropMember(ddl.SchemaObjectOperation):\n\n    def __init__(\n        self,\n        name: RoleName,\n        member: str,\n        *,\n        conditions: Optional[Iterable[str | base.Condition]] = None,\n        neg_conditions: Optional[Iterable[str | base.Condition]] = None,\n    ) -> None:\n        super().__init__(\n            name, conditions=conditions, neg_conditions=neg_conditions\n        )\n        self.member = member\n\n    def code(self) -> str:\n        return f'REVOKE {qi(self.name)} FROM {qi(self.member)}'\n\n\nclass AlterRoleAddMembership(ddl.SchemaObjectOperation):\n\n    def __init__(\n        self,\n        name: RoleName,\n        membership: Iterable[str],\n        *,\n        conditions: Optional[Iterable[str | base.Condition]] = None,\n        neg_conditions: Optional[Iterable[str | base.Condition]] = None,\n    ):\n        super().__init__(\n            name, conditions=conditions, neg_conditions=neg_conditions\n        )\n        self.membership = membership\n\n    def code(self) -> str:\n        roles = ', '.join(qi(m) for m in self.membership)\n        return f'GRANT {roles} TO {qi(self.name)}'\n"
  },
  {
    "path": "edb/pgsql/dbops/schemas.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport textwrap\n\nfrom ..common import quote_ident as qi\nfrom ..common import quote_literal as ql\n\nfrom . import base\nfrom . import ddl\n\n\nclass SchemaExists(base.Condition):\n    def __init__(self, name):\n        self.name = name\n\n    def code(self) -> str:\n        return textwrap.dedent(f'''\\\n            SELECT\n                oid\n            FROM\n                pg_catalog.pg_namespace\n            WHERE\n                nspname = {ql(self.name)}\n        ''')\n\n\nclass CreateSchema(ddl.DDLOperation):\n    def __init__(\n        self, name, *, conditions=None, neg_conditions=None, conditional=False\n    ):\n        super().__init__(conditions=conditions, neg_conditions=neg_conditions)\n        self.name = name\n        self.opid = name\n        self.conditional = conditional\n\n    def code(self) -> str:\n        condition = \"IF NOT EXISTS \" if self.conditional else ''\n        return f'CREATE SCHEMA {condition}{qi(self.name)}'\n\n    def __repr__(self):\n        return '<edb.sync.%s %s>' % (self.__class__.__name__, self.name)\n\n\nclass DropSchema(ddl.DDLOperation):\n    def __init__(self, name, *, conditions=None, neg_conditions=None):\n        super().__init__(conditions=conditions, neg_conditions=neg_conditions)\n        self.name = name\n\n    def code(self) -> str:\n        return f'DROP SCHEMA {qi(self.name)}'\n\n    def __repr__(self):\n        return '<edb.sync.%s %s>' % (self.__class__.__name__, self.name)\n"
  },
  {
    "path": "edb/pgsql/dbops/sequences.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom ..common import qname as qn\n\nfrom . import ddl\n\n\nclass CreateSequence(ddl.SchemaObjectOperation):\n    def __init__(self, name):\n        super().__init__(name)\n\n    def code(self) -> str:\n        return f'CREATE SEQUENCE {qn(*self.name)}'\n\n\nclass DropSequence(ddl.SchemaObjectOperation):\n    def __init__(self, name):\n        super().__init__(name)\n\n    def code(self) -> str:\n        return f'DROP SEQUENCE {qn(*self.name)}'\n"
  },
  {
    "path": "edb/pgsql/dbops/tables.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport collections\nimport textwrap\nfrom typing import (\n    Iterable,\n    Iterator,\n    Optional,\n    Sequence,\n    TypeAlias,\n)\n\nfrom edb.common import ordered\n\nfrom ..common import qname as qn\nfrom ..common import quote_ident as qi\nfrom ..common import quote_literal as ql\n\nfrom .. import ast as pgast\n\nfrom . import base\nfrom . import composites\nfrom . import constraints\nfrom . import ddl\n\n\nTableName: TypeAlias = tuple[str, ...]\nColumnName: TypeAlias = str\n\n\nclass Table(composites.CompositeDBObject):\n    bases: ordered.OrderedSet[Table]\n    constraints: ordered.OrderedSet[SingleTableConstraint]\n\n    # Columns from bases and self\n    all_columns: collections.OrderedDict[str, Column]\n\n    def __init__(\n        self,\n        name: TableName,\n        *,\n        columns: Optional[Iterable[Column]] = None,\n        bases: Optional[ordered.OrderedSet[Table]] = None,\n        constraints: Optional[ordered.OrderedSet[SingleTableConstraint]] = None\n    ) -> None:\n        self.bases = ordered.OrderedSet(bases or [])\n        self.constraints = ordered.OrderedSet(constraints or [])\n        super().__init__(name, columns=columns)\n\n        self.all_columns = collections.OrderedDict(\n            (c.name, c) for c in self.iter_columns()\n        )\n\n    def iter_columns(\n        self,\n        writable_only: bool = False,\n        only_self: bool = False,\n    ) -> Iterable[Column]:\n        cols: collections.OrderedDict[ColumnName, Column] = (\n            collections.OrderedDict()\n        )\n        cols.update(\n            (c.name, c)\n            for c in self._columns\n            if not writable_only or not c.readonly\n        )\n\n        if not only_self:\n            for base in reversed(self.bases):\n                cols.update(\n                    (name, bc)\n                    for name, bc in base.all_columns.items()\n                    if not writable_only or not bc.readonly\n                )\n\n        return ordered.OrderedSet(cols.values())\n\n    def __iter__(self) -> Iterator[Column]:\n        return iter(self._columns)\n\n    def add_bases(self, iterable: Iterable[Table]) -> None:\n        self.bases.update(iterable)\n        self.all_columns = collections.OrderedDict(\n            (c.name, c) for c in self.iter_columns()\n        )\n\n    def add_columns(self, iterable: Iterable[Column]) -> None:\n        super().add_columns(iterable)\n        self.all_columns = collections.OrderedDict(\n            (c.name, c) for c in self.iter_columns()\n        )\n\n    def add_constraint(self, const: SingleTableConstraint) -> None:\n        self.constraints.add(const)\n\n    def get_column(self, name: ColumnName) -> Optional[Column]:\n        return self.all_columns.get(name)\n\n    def get_type(self) -> str:\n        return 'TABLE'\n\n    def get_id(self) -> str:\n        return qn(*self.name)\n\n    @property\n    def record(self) -> composites.Record:\n        return composites.Record(\n            self.__class__.__name__ + '_record',\n            list(self.all_columns), default=base.Default)\n\n    @property\n    def system_catalog(self) -> str:\n        return 'pg_class'\n\n    @property\n    def oid_type(self) -> str:\n        return 'regclass'\n\n    def __repr__(self) -> str:\n        return f'<db.Table {self.name} at {id(self):0x}>'\n\n\nclass InheritableTableObject(base.InheritableDBObject):\n    name: str\n\n    @property\n    def name_in_catalog(self) -> str:\n        return self.name\n\n\nclass Column(base.DBObject):\n    def __init__(\n        self,\n        name: ColumnName,\n        type: str | tuple[str, str],\n        required: bool = False,\n        default: Optional[str] = None,\n        constraints: Sequence[ColumnConstraint] = (),\n        readonly: bool = False,\n        comment: Optional[str] = None,\n    ) -> None:\n        self.name = name\n        self.type = type\n        self.required = required\n        self.default = default\n        self.constraints = constraints\n        self.readonly = readonly\n        self.comment = comment\n\n    def add_constraint(self, constraint: ColumnConstraint) -> None:\n        self.constraints = list(self.constraints) + [constraint]\n\n    def code(self, short: bool = False) -> str:\n        code = f\"{qi(self.name)} {self.type}\"\n        if not short:\n            if self.required:\n                code += ' NOT NULL'\n\n            if self.default is not None:\n                code += f' DEFAULT {self.default}'\n\n            for c in self.constraints:\n                code += ' ' + c.code()\n        return code\n\n    def generate_extra_composite(\n        self, block: base.PLBlock, alter_table: base.CompositeCommandGroup\n    ) -> None:\n        if self.comment is not None:\n            assert isinstance(alter_table, AlterTable)\n            col = TableColumn(table_name=alter_table.name, column=self)\n            cmd = ddl.Comment(object=col, text=self.comment)\n            cmd.generate(block)\n\n    def __repr__(self) -> str:\n        return '<%s.%s \"%s\" %s>' % (\n            self.__class__.__module__, self.__class__.__name__, self.name,\n            self.type)\n\n\nclass TableColumn(base.DBObject):\n    def __init__(self, table_name: TableName, column: Column) -> None:\n        self.table_name = table_name\n        self.column = column\n\n    def get_type(self) -> str:\n        return 'COLUMN'\n\n    def get_id(self) -> str:\n        return qn(\n            self.table_name[0], self.table_name[1], self.column.name\n        )\n\n\nclass ColumnConstraint:\n    def __init__(self, constraint_name: str):\n        self.constraint_name = constraint_name\n\n    def code(self) -> str:\n        raise NotImplementedError()\n\n\nclass GeneratedConstraint(ColumnConstraint):\n    def __init__(self, constraint_name: str, expr: str) -> None:\n        super().__init__(constraint_name)\n        self.expr = expr\n\n    def code(self) -> str:\n        return (\n            f'CONSTRAINT {self.constraint_name} '\n            f'GENERATED ALWAYS AS ({self.expr}) STORED'\n        )\n\n\nclass TableConstraint(constraints.Constraint):\n    def generate_extra(self, block: base.PLBlock) -> None:\n        pass\n\n    def get_subject_type(self) -> str:\n        return ''  # For table constraints the accepted syntax is\n        # simply CONSTRAINT ON \"{tab_name}\", not\n        # CONSTRAINT ON TABLE, unlike constraints on\n        # other objects.\n\n\nclass SingleTableConstraint(TableConstraint):\n\n    def constraint_code(self, block: base.PLBlock) -> str:\n        raise NotImplementedError()\n\n\nclass PrimaryKey(SingleTableConstraint):\n    def __init__(\n        self, table_name: TableName, columns: Sequence[str | pgast.Star]\n    ) -> None:\n        super().__init__(table_name)\n        self.columns = columns\n\n    def constraint_code(self, block: base.PLBlock) -> str:\n        cols = ', '.join(qi(c) for c in self.columns)\n        return f'PRIMARY KEY ({cols})'\n\n\nclass UniqueConstraint(SingleTableConstraint):\n    def __init__(\n        self, table_name: TableName, columns: Sequence[str | pgast.Star]\n    ) -> None:\n        super().__init__(table_name)\n        self.columns = columns\n\n    def constraint_code(self, block: base.PLBlock) -> str:\n        cols = ', '.join(qi(c) for c in self.columns)\n        return f'UNIQUE ({cols})'\n\n\nclass CheckConstraint(SingleTableConstraint):\n    def __init__(\n        self,\n        table_name: TableName,\n        constraint_name: str,\n        expr: base.Query | str,\n        inherit: bool = True,\n    ) -> None:\n        super().__init__(table_name, constraint_name=constraint_name)\n        self.expr = expr\n        self.inherit = inherit\n\n    def constraint_code(self, block: base.PLBlock) -> str:\n        if isinstance(self.expr, base.Query):\n            assert self.expr.type\n            var = block.declare_var(self.expr.type)\n            indent = len(var) + 5\n            expr_text = textwrap.indent(self.expr.text, ' ' * indent).strip()\n            block.add_command(f'{var} := ({expr_text})')\n\n            code = f\"'CHECK (' || {var} || ')'\"\n            if not self.inherit:\n                code += \" || ' NO INHERIT'\"\n\n            code = base.PLExpression(code)\n\n        else:\n            code = f'CHECK ({self.expr})'\n            if not self.inherit:\n                code += ' NO INHERIT'\n\n        return code\n\n\nclass TableExists(base.Condition):\n    def __init__(self, name: TableName) -> None:\n        self.name = name\n\n    def code(self) -> str:\n        return textwrap.dedent(f'''\\\n            SELECT\n                tablename\n            FROM\n                pg_catalog.pg_tables\n            WHERE\n                schemaname = {ql(self.name[0])}\n                AND tablename = {ql(self.name[1])}\n        ''')\n\n\nclass TableInherits(base.Condition):\n    def __init__(\n        self,\n        name: TableName,\n        parent_name: TableName,\n    ) -> None:\n        self.name = name\n        self.parent_name = parent_name\n\n    def code(self) -> str:\n        return textwrap.dedent(f'''\\\n            SELECT\n                c.relname\n            FROM\n                pg_class c\n                INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace\n                INNER JOIN pg_inherits i ON i.inhrelid = c.oid\n                INNER JOIN pg_class pc ON i.inhparent = pc.oid\n                INNER JOIN pg_namespace pns ON pns.oid = pc.relnamespace\n            WHERE\n                ns.nspname = {ql(self.name[0])}\n                AND c.relname = {ql(self.name[1])}\n                AND pns.nspname = {ql(self.parent_name[0])}\n                AND pc.relname = {ql(self.parent_name[1])}\n        ''')\n\n\nclass ColumnExists(base.Condition):\n    def __init__(\n        self,\n        table_name: TableName,\n        column_name: ColumnName,\n    ) -> None:\n        self.table_name = table_name\n        self.column_name = column_name\n\n    def code(self) -> str:\n        return textwrap.dedent(f'''\\\n            SELECT\n                column_name\n            FROM\n                information_schema.columns\n            WHERE\n                table_schema = {ql(self.table_name[0])}\n                AND table_name = {ql(self.table_name[1])}\n                AND column_name = {ql(self.column_name)}\n        ''')\n\n\nclass ColumnIsInherited(base.Condition):\n    def __init__(\n        self,\n        table_name: TableName,\n        column_name: ColumnName,\n    ) -> None:\n        self.table_name = table_name\n        self.column_name = column_name\n\n    def code(self) -> str:\n        return textwrap.dedent(f'''\\\n            SELECT\n                True\n            FROM\n                pg_class c\n                INNER JOIN pg_namespace ns ON ns.oid = c.relnamespace\n                INNER JOIN pg_attribute a ON a.attrelid = c.oid\n            WHERE\n                ns.nspname = {ql(self.table_name[0])}\n                AND c.relname = {ql(self.table_name[1])}\n                AND a.attname = {ql(self.column_name)}\n                AND a.attinhcount > 0\n        ''')\n\n\nclass CreateTable(ddl.SchemaObjectOperation):\n    def __init__(\n        self,\n        table: Table,\n        temporary: bool = False,\n        *,\n        conditions: Optional[Iterable[str | base.Condition]] = None,\n        neg_conditions: Optional[Iterable[str | base.Condition]] = None,\n    ) -> None:\n        super().__init__(\n            table.name, conditions=conditions, neg_conditions=neg_conditions\n        )\n        self.table = table\n        self.temporary = temporary\n\n    def code_with_block(self, block: base.PLBlock) -> str:\n        elems = [\n            c.code() for c in self.table.iter_columns(only_self=True)\n        ]\n        for c in self.table.constraints:\n            elems.append(c.constraint_code(block))\n\n        name = qn(*self.table.name)\n        temp = 'TEMPORARY ' if self.temporary else ''\n        chunks = [f'CREATE {temp}TABLE {name} (', ')']\n        if self.table.bases:\n            bases = ','.join(qn(*b.name) for b in self.table.bases)\n            chunks.append(f' INHERITS ({bases})')\n\n        if any(isinstance(e, base.PLExpression) for e in elems):\n            # Dynamic declaration\n            elem_chunks: list[base.PLExpression | str] = []\n            for e in elems:\n                if isinstance(e, base.PLExpression):\n                    elem_chunks.append(e)\n                else:\n                    elem_chunks.append(ql(e))\n\n            chunks = [ql(c) for c in chunks]\n            chunks.insert(1, \" || ',' || \".join(elem_chunks))\n            code = 'EXECUTE ' + ' || '.join(chunks)\n\n        else:\n            # Static declaration\n            chunks.insert(1, ', '.join(elems))\n            code = ''.join(chunks)\n\n        return code\n\n\nclass AlterTableBaseMixin:\n\n    name: TableName\n    contained: bool\n\n    def __init__(\n        self, name: TableName, contained: bool = False\n    ) -> None:\n        self.name = name\n        self.contained = contained\n\n    def prefix_code(self) -> str:\n        return 'ALTER TABLE %s%s' % (\n            'ONLY ' if self.contained else '', qn(*self.name))\n\n    def __repr__(self) -> str:\n        return '<%s.%s %s>' % (\n            self.__class__.__module__, self.__class__.__name__, self.name)\n\n\nclass AlterTableBase(AlterTableBaseMixin, ddl.DDLOperation):\n    def __init__(\n        self,\n        name: TableName,\n        *,\n        contained: bool = False,\n        conditions: Optional[Iterable[str | base.Condition]] = None,\n        neg_conditions: Optional[Iterable[str | base.Condition]] = None,\n    ) -> None:\n        ddl.DDLOperation.__init__(\n            self, conditions=conditions, neg_conditions=neg_conditions)\n        AlterTableBaseMixin.__init__(self, name=name, contained=contained)\n\n    def get_attribute_term(self) -> str:\n        return 'COLUMN'\n\n\nclass AlterTableFragment(ddl.DDLOperation, base.CompositeCommand):\n    def get_attribute_term(self) -> str:\n        return 'COLUMN'\n\n    def generate_extra_composite(\n        self, block: base.PLBlock, group: base.CompositeCommandGroup\n    ) -> None:\n        pass\n\n\nclass AlterTable(\n    AlterTableBaseMixin, ddl.DDLOperation, base.CompositeCommandGroup\n):\n    def __init__(\n        self,\n        name: TableName,\n        *,\n        contained: bool = False,\n        conditions: Optional[Iterable[str | base.Condition]] = None,\n        neg_conditions: Optional[Iterable[str | base.Condition]] = None,\n    ):\n        base.CompositeCommandGroup.__init__(\n            self, conditions=conditions, neg_conditions=neg_conditions)\n        AlterTableBaseMixin.__init__(self, name=name, contained=contained)\n        self.ops = self.commands\n\n    add_operation = base.CompositeCommandGroup.add_command\n\n\nclass AlterTableAddParent(AlterTableFragment):\n    def __init__(self, parent_name: TableName, **kwargs) -> None:\n        super().__init__(**kwargs)\n        self.parent_name = parent_name\n\n    def code(self) -> str:\n        return f'INHERIT {qn(*self.parent_name)}'\n\n    def __repr__(self) -> str:\n        return '<%s.%s %s>' % (\n            self.__class__.__module__, self.__class__.__name__,\n            self.parent_name)\n\n\nclass AlterTableDropParent(AlterTableFragment):\n    def __init__(self, parent_name: TableName):\n        self.parent_name = parent_name\n\n    def code(self) -> str:\n        return f'NO INHERIT {qn(*self.parent_name)}'\n\n    def __repr__(self) -> str:\n        return '<%s.%s %s>' % (\n            self.__class__.__module__, self.__class__.__name__,\n            self.parent_name)\n\n\nclass AlterTableAddColumn(  # type: ignore\n        composites.AlterCompositeAddAttribute, AlterTableFragment):\n    pass\n\n\nclass AlterTableDropColumn(\n        composites.AlterCompositeDropAttribute, AlterTableFragment):\n    pass\n\n\nclass AlterTableAlterColumnType(\n        composites.AlterCompositeAlterAttributeType, AlterTableFragment):\n    pass\n\n\nclass AlterTableAlterColumnNull(AlterTableFragment):\n    def __init__(self, column_name: ColumnName, null) -> None:\n        self.column_name = column_name\n        self.null = null\n\n    def code(self) -> str:\n        action = 'DROP' if self.null else 'SET'\n        return f'ALTER COLUMN {qi(self.column_name)} {action} NOT NULL'\n\n    def __repr__(self) -> str:\n        return '<{}.{} \"{}\" {} NOT NULL>'.format(\n            self.__class__.__module__, self.__class__.__name__,\n            self.column_name, 'DROP' if self.null else 'SET')\n\n\nclass AlterTableAlterColumnDefault(AlterTableFragment):\n    def __init__(self, column_name: ColumnName, default: Optional[str]):\n        self.column_name = column_name\n        self.default = default\n\n    def code(self) -> str:\n        if self.default is None:\n            return f'ALTER COLUMN {qi(self.column_name)} DROP DEFAULT'\n        else:\n            return (f'ALTER COLUMN {qi(self.column_name)} '\n                    f'SET DEFAULT {self.default}')\n\n    def __repr__(self) -> str:\n        return '<{}.{} \"{}\" {} DEFAULT{}>'.format(\n            self.__class__.__module__, self.__class__.__name__,\n            self.column_name, 'DROP' if self.default is None else 'SET', ''\n            if self.default is None else ' {!r}'.format(self.default))\n\n\nclass TableConstraintCommand:\n    pass\n\n\nclass TableConstraintExists(base.Condition):\n    def __init__(self, table_name: TableName, constraint_name: str):\n        self.table_name = table_name\n        self.constraint_name = constraint_name\n\n    def code(self) -> str:\n        return textwrap.dedent(f'''\\\n            SELECT\n                True\n            FROM\n                pg_catalog.pg_constraint c\n                INNER JOIN pg_catalog.pg_class t\n                    ON c.conrelid = t.oid\n                INNER JOIN pg_catalog.pg_namespace ns\n                    ON t.relnamespace = ns.oid\n            WHERE\n                conname = {ql(self.constraint_name)}\n                AND nspname = {ql(self.table_name[0])}\n                AND relname = {ql(self.table_name[1])}\n        ''')\n\n\nclass AlterTableAddConstraint[TableConstraint_T: \"TableConstraint\"](\n    AlterTableFragment,\n    TableConstraintCommand,\n):\n    constraint: TableConstraint_T\n\n    def __init__(self, constraint: TableConstraint_T):\n        assert not isinstance(constraint, list)\n        self.constraint = constraint\n\n    def code_with_block(self, block: base.PLBlock) -> str:\n        code = 'ADD '\n        name = self.constraint.constraint_name()\n        if name:\n            code += f'CONSTRAINT {name} '\n\n        constr_code = self.constraint.constraint_code(block)\n        assert isinstance(constr_code, str)\n\n        if not isinstance(constr_code, base.PLExpression):\n            # Static declaration\n            return code + constr_code\n        else:\n            # Dynamic declaration\n            return base.PLExpression(f'{ql(code)} || {constr_code}')\n\n    def generate_extra_composite(\n        self, block: base.PLBlock, group: base.CompositeCommandGroup\n    ) -> None:\n        return self.constraint.generate_extra(block)\n\n    def __repr__(self) -> str:\n        return '<%s.%s %r>' % (\n            self.__class__.__module__, self.__class__.__name__,\n            self.constraint)\n\n\nclass AlterTableDropConstraint(AlterTableFragment, TableConstraintCommand):\n    def __init__(self, constraint) -> None:\n        self.constraint = constraint\n\n    def code(self) -> str:\n        return f'DROP CONSTRAINT {self.constraint.constraint_name()}'\n\n    def __repr__(self) -> str:\n        return '<%s.%s %r>' % (\n            self.__class__.__module__, self.__class__.__name__,\n            self.constraint)\n\n\nclass DropTable(ddl.SchemaObjectOperation):\n    def code(self) -> str:\n        return f'DROP TABLE {qn(*self.name)}'\n"
  },
  {
    "path": "edb/pgsql/dbops/triggers.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport textwrap\nfrom typing import (\n    Any,\n    Mapping,\n    Optional,\n    TypeAlias,\n)\n\nfrom ...common import enum as s_enum\nfrom ..common import qname as qn\nfrom ..common import quote_ident as qi\nfrom ..common import quote_literal as ql\n\nfrom . import base\nfrom . import ddl\nfrom . import tables\n\n\nTriggerName: TypeAlias = str\n\n\nclass TriggerTiming(s_enum.StrEnum):\n    Before = 'before'\n    After = 'after'\n\n\nclass TriggerGranularity(s_enum.StrEnum):\n    Row = 'row'\n\n\nclass TriggerExists(base.Condition):\n    def __init__(\n        self,\n        trigger_name: TriggerName,\n        table_name: tables.TableName\n    ) -> None:\n        self.trigger_name = trigger_name\n        self.table_name = table_name\n\n    def code(self) -> str:\n        return textwrap.dedent(\n            f'''\\\n            SELECT\n                tg.tgname\n            FROM\n                pg_catalog.pg_trigger tg\n                INNER JOIN pg_catalog.pg_class tab\n                    ON (tab.oid = tg.tgrelid)\n                INNER JOIN pg_catalog.pg_namespace ns\n                    ON (ns.oid = tab.relnamespace)\n            WHERE\n                tab.relname = {ql(self.table_name[1])}\n                AND ns.nspname = {ql(self.table_name[0])}\n                AND tg.tgname = {ql(self.trigger_name)}\n        '''\n        )\n\n\nclass Trigger(tables.InheritableTableObject):\n    def __init__(\n        self,\n        name: TriggerName,\n        *,\n        table_name: tables.TableName,\n        events: tuple[str, ...],\n        timing: TriggerTiming = TriggerTiming.After,\n        granularity: TriggerGranularity = TriggerGranularity.Row,\n        procedure,\n        condition=None,\n        is_constraint: bool = False,\n        deferred: bool = False,\n        inherit: bool = False,\n        metadata: Optional[Mapping[str, Any]] = None,\n    ) -> None:\n        super().__init__(inherit=inherit, metadata=metadata)\n\n        self.name = name\n        self.table_name = table_name\n        self.events = events\n        self.timing = timing\n        self.granularity = granularity\n        self.procedure = procedure\n        self.condition = condition\n        self.is_constraint = is_constraint\n        self.deferred = deferred\n\n        if is_constraint and granularity != TriggerGranularity.Row:\n            msg = 'invalid granularity for ' 'constraint trigger: {}'.format(\n                granularity\n            )\n            raise ValueError(msg)\n\n        if deferred and not is_constraint:\n            raise ValueError('only constraint triggers can be deferred')\n\n    def get_type(self) -> str:\n        return 'TRIGGER'\n\n    def get_id(self) -> str:\n        return f'{qi(self.name)} ON {qn(*self.table_name)}'\n\n    def get_oid(self) -> base.Query:\n        qry = textwrap.dedent(\n            f'''\\\n            SELECT\n                'pg_trigger'::regclass::oid AS classoid,\n                pg_trigger.oid AS objectoid,\n                0\n            FROM\n                pg_trigger\n                INNER JOIN pg_class ON tgrelid = pg_class.oid\n                INNER JOIN pg_namespace ON relnamespace = pg_namespace.oid\n            WHERE\n                tgname = {ql(self.name)}\n                AND nspname = {ql(self.table_name[0])}\n                AND relname = {ql(self.table_name[1])}\n        '''\n        )\n\n        return base.Query(text=qry)\n\n    def copy(self) -> Trigger:\n        return self.__class__(\n            name=self.name,\n            table_name=self.table_name,\n            events=self.events,\n            timing=self.timing,\n            granularity=self.granularity,\n            procedure=self.procedure,\n            condition=self.condition,\n            is_constraint=self.is_constraint,\n            deferred=self.deferred,\n            metadata=(\n                self.metadata.copy() if self.metadata is not None else None\n            ),\n        )\n\n    def __repr__(self) -> str:\n        return '<{mod}.{cls} {name} ON {table_name} {timing} {events}>'.format(\n            mod=self.__class__.__module__,\n            cls=self.__class__.__name__,\n            name=self.name,\n            table_name=qn(*self.table_name),\n            timing=self.timing,\n            events=' OR '.join(self.events),\n        )\n\n\nclass CreateTrigger(ddl.CreateObject):\n    def __init__(\n        self,\n        object: Trigger,\n        *,\n        conditional: bool = False,\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(object, **kwargs)\n        self.trigger = object\n        if conditional:\n            self.neg_conditions.add(\n                TriggerExists(self.trigger.name, self.trigger.table_name)\n            )\n\n    def code(self) -> str:\n        return textwrap.dedent(\n            '''\\\n            CREATE {constr}TRIGGER {trigger_name} {timing} {events}\n                   ON {table_name}\n                   {deferred}\n                   FOR EACH {granularity} {condition}\n                   EXECUTE PROCEDURE {procedure}\n        '''\n        ).format(\n            constr='CONSTRAINT ' if self.trigger.is_constraint else '',\n            trigger_name=qi(self.trigger.name),\n            timing=self.trigger.timing,\n            events=' OR '.join(self.trigger.events),\n            table_name=qn(*self.trigger.table_name),\n            deferred=(\n                'DEFERRABLE INITIALLY DEFERRED'\n                if self.trigger.deferred\n                else ''\n            ),\n            granularity=self.trigger.granularity,\n            condition=(\n                f'WHEN ({self.trigger.condition})'\n                if self.trigger.condition\n                else ''\n            ),\n            procedure=f'{qn(*self.trigger.procedure)}()',\n        )\n\n\nclass DropTrigger(ddl.DropObject):\n    def __init__(\n        self,\n        object: Trigger,\n        *,\n        conditional: bool = False,\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(object, **kwargs)\n        self.trigger = object\n        self.conditional = conditional\n        if conditional:\n            self.conditions.add(\n                TriggerExists(self.trigger.name, self.trigger.table_name)\n            )\n\n    def code(self) -> str:\n        ifexists = ' IF EXISTS' if self.conditional else ''\n        return (\n            f'DROP TRIGGER{ifexists} {qi(self.trigger.name)} '\n            f'ON {qn(*self.trigger.table_name)}'\n        )\n\n\nclass DisableTrigger(ddl.DDLOperation):\n    def __init__(\n        self,\n        trigger: Trigger,\n        *,\n        self_only: bool = False,\n        **kwargs: Any,\n    ):\n        super().__init__(**kwargs)\n        self.trigger = trigger\n        self.self_only = self_only\n\n    def code(self) -> str:\n        only = ' ONLY' if self.self_only else ''\n        return (\n            f'ALTER TABLE{only} {qn(*self.trigger.table_name)} '\n            f'DISABLE TRIGGER {qi(self.trigger.name)}'\n        )\n\n    def __repr__(self) -> str:\n        return '<{mod}.{cls} {trigger!r}>'.format(\n            mod=self.__class__.__module__,\n            cls=self.__class__.__name__,\n            trigger=self.trigger,\n        )\n\n\nclass EnableTrigger(ddl.DDLOperation):\n    def __init__(\n        self,\n        trigger: Trigger,\n        **kwargs: Any,\n    ):\n        super().__init__(**kwargs)\n        self.trigger = trigger\n\n    def code(self) -> str:\n        return (\n            f'ALTER TABLE {qn(*self.trigger.table_name)} '\n            f'ENABLE TRIGGER {qi(self.trigger.name)}'\n        )\n\n    def __repr__(self) -> str:\n        return '<{mod}.{cls} {trigger!r}>'.format(\n            mod=self.__class__.__module__,\n            cls=self.__class__.__name__,\n            trigger=self.trigger,\n        )\n"
  },
  {
    "path": "edb/pgsql/dbops/types.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import (\n    Any,\n    Collection,\n    Iterable,\n    Iterator,\n    Optional,\n    TypeAlias,\n)\n\nimport textwrap\n\nfrom edb.common import ordered\n\nfrom ..common import qname as qn\nfrom ..common import quote_literal as ql\n\nfrom . import base\nfrom . import composites\nfrom . import ddl\nfrom . import tables\n\n\nCompositeTypeName: TypeAlias = tuple[str, str]\n\n\nclass CompositeType(composites.CompositeDBObject):\n    def __init__(\n        self,\n        name: CompositeTypeName,\n        columns: Collection[tables.Column] = (),\n    ):\n        super().__init__(name)\n        self._columns = ordered.OrderedSet(columns)\n\n    def iter_columns(self) -> Iterator[tables.Column]:\n        return iter(self._columns)\n\n\nclass TypeExists(base.Condition):\n    def __init__(self, name: CompositeTypeName):\n        self.name = name\n\n    def code(self) -> str:\n        return textwrap.dedent(f'''\\\n            SELECT\n                typname\n            FROM\n                pg_catalog.pg_type typ\n                INNER JOIN pg_catalog.pg_namespace nsp\n                    ON nsp.oid = typ.typnamespace\n            WHERE\n                nsp.nspname = {ql(self.name[0])}\n                AND typ.typname = {ql(self.name[1])}\n        ''')\n\n\ndef type_oid(name: CompositeTypeName) -> base.Query:\n    if len(name) == 2:\n        typnamespace, typname = name\n    else:\n        typname = name[0]\n        typnamespace = 'pg_catalog'\n\n    qry = textwrap.dedent(f'''\\\n        SELECT\n            typ.oid\n        FROM\n            pg_catalog.pg_type typ\n            INNER JOIN pg_catalog.pg_namespace nsp\n                ON nsp.oid = typ.typnamespace\n        WHERE\n            typ.typname = {ql(typname)}\n            AND nsp.nspname = {ql(typnamespace)}\n    ''')\n\n    return base.Query(qry)\n\n\nCompositeTypeExists = TypeExists\n\n\nclass CompositeTypeAttributeExists(base.Condition):\n    def __init__(\n        self,\n        type_name: CompositeTypeName,\n        attribute_name: str,\n    ):\n        self.type_name = type_name\n        self.attribute_name = attribute_name\n\n    def code(self) -> str:\n        return textwrap.dedent(f'''\\\n            SELECT\n                attribute_name\n            FROM\n                information_schema.attributes\n            WHERE\n                udt_schema = {ql(self.type_name[0])}\n                AND udt_name = {ql(self.type_name[1])}\n                AND attribute_name = {ql(self.attribute_name)}\n        ''')\n\n\nclass CreateCompositeType(ddl.SchemaObjectOperation):\n    def __init__(\n        self,\n        type: CompositeType,\n        *,\n        conditions: Optional[Iterable[str | base.Condition]] = None,\n        neg_conditions: Optional[Iterable[str | base.Condition]] = None,\n    ) -> None:\n        super().__init__(\n            type.name, conditions=conditions, neg_conditions=neg_conditions\n        )\n        self.type = type\n\n    def code(self) -> str:\n        elems = [c.code(short=True) for c in self.type.iter_columns()]\n        name = qn(*self.type.name)\n        cols = ', '.join(c for c in elems)\n        return f'CREATE TYPE {name} AS ({cols})'\n\n\nclass AlterCompositeTypeBaseMixin:\n    def __init__(self, name: CompositeTypeName, **kwargs: Any):\n        self.name = name\n\n    def prefix_code(self) -> str:\n        return f'ALTER TYPE {qn(*self.name)}'\n\n    def __repr__(self) -> str:\n        return '<%s.%s %s>' % (\n            self.__class__.__module__, self.__class__.__name__, self.name)\n\n\nclass AlterCompositeTypeBase(AlterCompositeTypeBaseMixin, ddl.DDLOperation):\n    def __init__(\n        self,\n        name: CompositeTypeName,\n        *,\n        conditions: Optional[Iterable[str | base.Condition]] = None,\n        neg_conditions: Optional[Iterable[str | base.Condition]] = None,\n    ) -> None:\n        ddl.DDLOperation.__init__(\n            self, conditions=conditions, neg_conditions=neg_conditions)\n        AlterCompositeTypeBaseMixin.__init__(self, name=name)\n\n\nclass AlterCompositeTypeFragment(ddl.DDLOperation):\n    def get_attribute_term(self) -> str:\n        return 'ATTRIBUTE'\n\n\nclass AlterCompositeType(\n    AlterCompositeTypeBaseMixin, base.CompositeCommandGroup\n):\n    def __init__(\n        self,\n        name: CompositeTypeName,\n        *,\n        conditions: Optional[Iterable[str | base.Condition]] = None,\n        neg_conditions: Optional[Iterable[str | base.Condition]] = None,\n    ) -> None:\n        base.CompositeCommandGroup.__init__(\n            self, conditions=conditions, neg_conditions=neg_conditions)\n        AlterCompositeTypeBaseMixin.__init__(self, name=name)\n\n\nclass AlterCompositeTypeAddAttribute(  # type: ignore\n    composites.AlterCompositeAddAttribute, AlterCompositeTypeFragment\n):\n    def code(self) -> str:\n        return 'ADD {} {}'.format(\n            self.get_attribute_term(), self.attribute.code(short=True))\n\n\nclass AlterCompositeTypeDropAttribute(\n        composites.AlterCompositeDropAttribute, AlterCompositeTypeFragment):\n    pass\n\n\nclass AlterCompositeTypeAlterAttributeType(\n        composites.AlterCompositeAlterAttributeType,\n        AlterCompositeTypeFragment):\n    pass\n\n\nclass DropCompositeType(ddl.SchemaObjectOperation):\n    def __init__(\n        self,\n        name: CompositeTypeName,\n        *,\n        cascade: bool = False,\n        conditions: Optional[Iterable[str | base.Condition]] = None,\n        neg_conditions: Optional[Iterable[str | base.Condition]] = None,\n    ):\n        super().__init__(\n            name, conditions=conditions, neg_conditions=neg_conditions\n        )\n        self.cascade = cascade\n\n    def code(self) -> str:\n        cascade = ' CASCADE' if self.cascade else ''\n        return f'DROP TYPE {qn(*self.name)}{cascade}'\n"
  },
  {
    "path": "edb/pgsql/dbops/views.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport textwrap\n\nfrom ..common import qname as qn\nfrom ..common import quote_literal as ql\n\nfrom . import base\nfrom . import ddl\n\n\nclass View(base.DBObject):\n    def __init__(self, name, query, materialized=False):\n        super().__init__()\n        self.name = name\n        self.query = query\n        self.materialized = materialized\n\n    def get_type(self) -> str:\n        return \"VIEW\" if not self.materialized else \"MATERIALIZED VIEW\"\n\n    def get_id(self):\n        return qn(*self.name)\n\n\nclass CreateView(ddl.SchemaObjectOperation):\n    def __init__(\n        self,\n        view,\n        *,\n        conditions=None,\n        neg_conditions=None,\n        or_replace=False,\n    ):\n        super().__init__(view.name, conditions=conditions,\n                         neg_conditions=neg_conditions)\n        self.view = view\n        self.or_replace = or_replace\n\n    def code(self) -> str:\n        query = textwrap.indent(textwrap.dedent(self.view.query), '    ')\n        return (\n            f'CREATE {\"OR REPLACE\" if self.or_replace else \"\"}'\n            f' {self.view.get_type()} {qn(*self.view.name)} AS\\n{query}'\n        )\n\n\nclass DropView(ddl.SchemaObjectOperation):\n\n    def __init__(\n        self,\n        name,\n        *,\n        conditional=False,\n        conditions=None,\n        neg_conditions=None,\n        materialized=False,\n    ):\n        super().__init__(\n            name,\n            conditions=conditions,\n            neg_conditions=neg_conditions,\n        )\n        self.conditional = conditional\n        self.materialized = materialized\n\n    def code(self) -> str:\n        mat = 'MATERIALIZED ' if self.materialized else ''\n        if self.conditional:\n            return f'DROP {mat}VIEW IF EXISTS {qn(*self.name)}'\n        else:\n            return f'DROP {mat}VIEW {qn(*self.name)}'\n\n\nclass ViewExists(base.Condition):\n\n    def __init__(self, name, materialized=False):\n        self.name = name\n        self.materialized = materialized\n\n    def code(self) -> str:\n        mat = 'mat' if self.materialized else ''\n        return textwrap.dedent(f'''\\\n            SELECT\n                {mat}viewname\n            FROM\n                pg_catalog.pg_{mat}views\n            WHERE\n                schemaname = {ql(self.name[0])}\n                AND {mat}viewname = {ql(self.name[1])}\n        ''')\n"
  },
  {
    "path": "edb/pgsql/debug.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n\nimport uuid\n\nfrom edb.common import debug\nfrom edb.common import uuidgen\n\nfrom edb.schema import functions as s_funcs\nfrom edb.schema import objects as so\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import schema as s_schema\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import codegen as pgcodegen\nfrom edb.ir import ast as irast\n\n\ndef dump_ast_and_query(\n    pg_expr: pgast.Base,\n    ir_expr: irast.Base,\n) -> None:\n    if not (\n        debug.flags.edgeql_compile\n        or debug.flags.edgeql_compile_sql_ast\n        or debug.flags.edgeql_compile_sql_reordered_text\n        or debug.flags.edgeql_compile_sql_text\n    ):\n        return\n\n    if debug.flags.edgeql_compile or debug.flags.edgeql_compile_sql_ast:\n        debug.header('SQL Tree')\n        debug.dump(\n            pg_expr, _ast_include_meta=debug.flags.edgeql_compile_sql_ast_meta\n        )\n\n    if debug.flags.edgeql_compile or debug.flags.edgeql_compile_sql_text:\n        sql_text = pgcodegen.generate_source(pg_expr, pretty=True)\n        debug.header('SQL')\n        debug.dump_code(sql_text, lexer='sql')\n\n    if debug.flags.edgeql_compile_sql_reordered_text:\n        debug.header('Reordered SQL')\n        debug_sql_text = pgcodegen.generate_source(\n            pg_expr, pretty=True, reordered=True\n        )\n        if isinstance(ir_expr, irast.Statement):\n            debug_sql_text = _rewrite_names_in_sql(\n                debug_sql_text, ir_expr.schema\n            )\n        debug.dump_code(debug_sql_text, lexer='sql')\n\n\ndef _rewrite_names_in_sql(text: str, schema: s_schema.Schema) -> str:\n    \"\"\"Rewrite the SQL output of the compiler to include real object names.\n\n    Replace UUIDs with object names when possible. The output of this\n    won't be valid, but will probably be easier to read.\n    This is done by default when pretty printing our \"reordered\" output,\n    which isn't anything like valid SQL anyway.\n    \"\"\"\n    # Functions are actually named after their `backend_name` rather\n    # than their id, so that overloaded functions all have the same\n    # name. Build a map from `backend_name` to real names. (This dict\n    # comprehension might have collisions, but that's fine; the names\n    # we get out will be the same no matter which is picked.)\n    func_map = {\n        f.get_backend_name(schema): f\n        for f in schema.get_objects(type=s_funcs.Function)\n    }\n\n    # Find all the uuids and try to rewrite them.\n    for m in set(uuidgen.UUID_RE.findall(text)):\n        uid = uuid.UUID(m)\n        sobj = schema.get_by_id(uid, default=None)\n        if not sobj:\n            sobj = func_map.get(uid)\n        if sobj:\n            s = _obj_to_name(sobj, schema)\n            text = text.replace(m, s)\n\n    return text\n\n\ndef _obj_to_name(\n    sobj: so.Object,\n    schema: s_schema.Schema,\n) -> str:\n    if isinstance(sobj, s_pointers.Pointer):\n        s = str(sobj.get_shortname(schema).name)\n        if sobj.is_link_property(schema):\n            s = f'@{s}'\n        # If the pointer is multi, then it is probably a table name,\n        # so let's give a fully qualified version with the source.\n        if sobj.get_cardinality(schema).is_multi() and (\n            src := sobj.get_source(schema)\n        ):\n            src_name = src.get_name(schema)\n            s = f'{src_name}.{s}'\n    elif isinstance(sobj, s_funcs.Function):\n        return str(sobj.get_shortname(schema))\n    else:\n        s = str(sobj.get_name(schema))\n\n    return s\n"
  },
  {
    "path": "edb/pgsql/delta.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Callable,\n    Optional,\n    Iterable,\n    Mapping,\n    Sequence,\n    cast,\n    TYPE_CHECKING,\n)\nfrom copy import copy\n\nimport collections.abc\nimport itertools\nimport textwrap\nimport uuid\n\nfrom edb import errors\n\nfrom edb.edgeql import ast as ql_ast\nfrom edb.edgeql import qltypes as ql_ft\nfrom edb.edgeql import compiler as qlcompiler\n\nfrom edb.schema import annos as s_anno\nfrom edb.schema import casts as s_casts\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import constraints as s_constr\nfrom edb.schema import database as s_db\nfrom edb.schema import delta as sd\nfrom edb.schema import expr as s_expr\nfrom edb.schema import expraliases as s_aliases\nfrom edb.schema import extensions as s_exts\nfrom edb.schema import futures as s_futures\nfrom edb.schema import functions as s_funcs\nfrom edb.schema import globals as s_globals\nfrom edb.schema import indexes as s_indexes\nfrom edb.schema import links as s_links\nfrom edb.schema import permissions as s_permissions\nfrom edb.schema import policies as s_policies\nfrom edb.schema import properties as s_props\nfrom edb.schema import migrations as s_migrations\nfrom edb.schema import modules as s_mod\nfrom edb.schema import name as sn\nfrom edb.schema import objects as so\nfrom edb.schema import operators as s_opers\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import pseudo as s_pseudo\nfrom edb.schema import roles as s_roles\nfrom edb.schema import rewrites as s_rewrites\nfrom edb.schema import sources as s_sources\nfrom edb.schema import triggers as s_triggers\nfrom edb.schema import types as s_types\nfrom edb.schema import version as s_ver\nfrom edb.schema import utils as s_utils\n\nfrom edb.common import markup\nfrom edb.common import ordered\nfrom edb.common import uuidgen\nfrom edb.common import parsing\nfrom edb.common.typeutils import not_none\n\nfrom edb.ir import ast as irast\nfrom edb.ir import pathid as irpathid\nfrom edb.ir import typeutils as irtyputils\nfrom edb.ir import utils as irutils\n\nfrom edb.pgsql import common\nfrom edb.pgsql import debug as pg_debug\nfrom edb.pgsql import dbops\nfrom edb.pgsql import params\nfrom edb.pgsql import deltafts\nfrom edb.pgsql import delta_ext_ai\n\nfrom edb.server import defines as edbdef\nfrom edb.server import config\nfrom edb.server.config import ops as config_ops\nfrom edb.server.compiler import sertypes\n\nfrom . import ast as pgast\nfrom .common import qname as q\nfrom .common import quote_literal as ql\nfrom .common import quote_ident as qi\nfrom .common import quote_type as qt\nfrom .common import versioned_schema as V\nfrom .compiler import enums as pgce\nfrom . import compiler\nfrom . import codegen\nfrom . import schemamech\nfrom . import trampoline\nfrom . import types\n\nif TYPE_CHECKING:\n    from edb.schema import schema as s_schema\n\n\nDEFAULT_INDEX_CODE = ' ((__col__) NULLS FIRST)'\n\n\nclass CommandMeta(sd.CommandMeta):\n    pass\n\n\nclass MetaCommand(sd.Command, metaclass=CommandMeta):\n    pgops: ordered.OrderedSet[dbops.Command | sd.Command]\n\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        self.pgops = ordered.OrderedSet()\n\n    def apply_prerequisites(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply_prerequisites(schema, context)\n        for op in self.get_prerequisites():\n            if not isinstance(op, sd.AlterObjectProperty):\n                self.pgops.add(op)\n        return schema\n\n    def apply_subcommands(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply_subcommands(schema, context)\n        for op in self.get_subcommands(\n            include_prerequisites=False,\n            include_caused=False,\n        ):\n            if not isinstance(op, sd.AlterObjectProperty):\n                self.pgops.add(op)\n        return schema\n\n    def apply_caused(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply_caused(schema, context)\n        for op in self.get_caused():\n            if not isinstance(op, sd.AlterObjectProperty):\n                self.pgops.add(op)\n        return schema\n\n    def generate(self, block: dbops.PLBlock) -> None:\n        for op in self.pgops:\n            assert isinstance(op, (dbops.Command, MetaCommand))\n            op.generate(block)\n\n    @classmethod\n    def as_markup(cls, self, *, ctx):\n        node = markup.elements.lang.TreeNode(name=str(self))\n\n        for dd in self.ops:\n            if isinstance(dd, AlterObjectProperty):\n                diff = markup.elements.doc.ValueDiff(\n                    before=repr(dd.old_value), after=repr(dd.new_value))\n\n                if dd.new_inherited:\n                    diff.comment = 'inherited'\n                elif dd.new_computed:\n                    diff.comment = 'computed'\n\n                node.add_child(label=dd.property, node=diff)\n        for dd in self.pgops:\n            node.add_child(node=markup.serialize(dd, ctx=ctx))\n\n        return node\n\n    def _get_backend_params(\n        self,\n        context: sd.CommandContext,\n    ) -> params.BackendRuntimeParams:\n\n        ctx_backend_params = context.backend_runtime_params\n        if ctx_backend_params is not None:\n            backend_params = cast(\n                params.BackendRuntimeParams, ctx_backend_params)\n        else:\n            backend_params = params.get_default_runtime_params()\n\n        return backend_params\n\n    def _get_instance_params(\n        self,\n        context: sd.CommandContext,\n    ) -> params.BackendInstanceParams:\n        return self._get_backend_params(context).instance_params\n\n    def _get_tenant_id(self, context: sd.CommandContext) -> str:\n        return self._get_instance_params(context).tenant_id\n\n    def _get_topmost_command_op(\n        self,\n        context: sd.CommandContext,\n        ctxcls: type[sd.CommandContextToken[sd.Command]],\n    ) -> CompositeMetaCommand:\n        ctx = context.get_topmost_ancestor(ctxcls)\n        if ctx is None:\n            raise AssertionError(f\"there is no {ctxcls} in context stack\")\n        assert isinstance(ctx.op, CompositeMetaCommand)\n        return ctx.op\n\n    def schedule_constraint_trigger_update(\n        self,\n        constraint: s_constr.Constraint,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        ctxcls: type[sd.CommandContextToken[sd.Command]],\n    ) -> None:\n\n        if (\n            not isinstance(\n                constraint.get_subject(schema),\n                (s_objtypes.ObjectType, s_pointers.Pointer)\n            )\n            or not schemamech.table_constraint_requires_triggers(\n                constraint, schema, 'unique'\n            )\n        ):\n            return\n\n        op = self._get_topmost_command_op(context, ctxcls)\n        op.constraint_trigger_updates.add(constraint.id)\n\n    @staticmethod\n    def get_function_type(\n        name: tuple[str, str]\n    ) -> type[dbops.Function] | type[trampoline.VersionedFunction]:\n        return (\n            trampoline.VersionedFunction if name[0] == 'edgedbstd'\n            else dbops.Function\n        )\n\n    @classmethod\n    def maybe_trampoline(\n        cls,\n        f: Optional[dbops.Function],\n        context: sd.CommandContext,\n    ) -> None:\n        if isinstance(f, trampoline.VersionedFunction):\n            create = trampoline.make_trampoline(f)\n\n            ctx = not_none(context.get(sd.DeltaRootContext))\n            assert isinstance(ctx.op, DeltaRoot)\n            create_trampolines = ctx.op.create_trampolines\n            create_trampolines.trampolines.append(create)\n\n\nclass CommandGroupAdapted(MetaCommand, adapts=sd.CommandGroup):\n    pass\n\n\nclass Nop(MetaCommand, adapts=sd.Nop):\n    pass\n\n\nclass Query(MetaCommand, adapts=sd.Query):\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n\n        assert self.expr.irast\n        sql_res = compiler.compile_ir_to_sql_tree(\n            self.expr.irast,\n            output_format=compiler.OutputFormat.NATIVE_INTERNAL,\n            explicit_top_cast=irtyputils.type_to_typeref(\n                schema,\n                schema.get('std::str', type=s_types.Type),\n                cache=None,\n            ),\n            backend_runtime_params=context.backend_runtime_params,\n        )\n        sql_text = codegen.generate_source(sql_res.ast)\n\n        # The INTO _dummy_text bit is needed because PL/pgSQL _really_\n        # wants the result of a returning query to be stored in a variable,\n        # and the PERFORM hack does not work if the query has DML CTEs.\n        self.pgops.add(dbops.Query(\n            text=f'{sql_text} INTO _dummy_text',\n        ))\n\n        return schema\n\n\nclass AlterObjectProperty(MetaCommand, adapts=sd.AlterObjectProperty):\n    pass\n\n\nclass SchemaVersionCommand(MetaCommand):\n    pass\n\n\nclass CreateSchemaVersion(\n    SchemaVersionCommand,\n    adapts=s_ver.CreateSchemaVersion,\n):\n    pass\n\n\nclass AlterSchemaVersion(\n    SchemaVersionCommand,\n    adapts=s_ver.AlterSchemaVersion,\n):\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n        expected_ver = self.get_orig_attribute_value('version')\n        check = dbops.Query(\n            f'''\n                SELECT\n                    edgedb_VER.raise_on_not_null(\n                        (SELECT NULLIF(\n                            (SELECT\n                                version::text\n                            FROM\n                                {V('edgedb')}.\"_SchemaSchemaVersion\"\n                            FOR UPDATE),\n                            {ql(str(expected_ver))}\n                        )),\n                        'serialization_failure',\n                        msg => (\n                            'Cannot serialize DDL: '\n                            || (SELECT version::text FROM\n                                {V('edgedb')}.\"_SchemaSchemaVersion\")\n                        )\n                    )\n                INTO _dummy_text\n            '''\n        )\n        self.pgops.add(check)\n        return schema\n\n\nclass GlobalSchemaVersionCommand(MetaCommand):\n    pass\n\n\nclass CreateGlobalSchemaVersion(\n    MetaCommand,\n    adapts=s_ver.CreateGlobalSchemaVersion,\n):\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n        ver_id = str(self.scls.id)\n        ver_name = str(self.scls.get_name(schema))\n\n        ctx_backend_params = context.backend_runtime_params\n        if ctx_backend_params is not None:\n            backend_params = cast(\n                params.BackendRuntimeParams, ctx_backend_params)\n        else:\n            backend_params = params.get_default_runtime_params()\n\n        metadata = {\n            ver_id: {\n                'id': ver_id,\n                'name': ver_name,\n                'version': str(self.scls.get_version(schema)),\n                'builtin': self.scls.get_builtin(schema),\n                'internal': self.scls.get_internal(schema),\n            }\n        }\n        if backend_params.has_create_database:\n            self.pgops.add(\n                dbops.UpdateMetadataSection(\n                    dbops.DatabaseWithTenant(name=edbdef.EDGEDB_TEMPLATE_DB),\n                    section='GlobalSchemaVersion',\n                    metadata=metadata\n                )\n            )\n        else:\n            self.pgops.add(\n                dbops.UpdateSingleDBMetadataSection(\n                    edbdef.EDGEDB_TEMPLATE_DB,\n                    section='GlobalSchemaVersion',\n                    metadata=metadata\n                )\n            )\n\n        return schema\n\n\nclass AlterGlobalSchemaVersion(\n    MetaCommand,\n    adapts=s_ver.AlterGlobalSchemaVersion,\n):\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n        ver_id = str(self.scls.id)\n        ver_name = str(self.scls.get_name(schema))\n\n        ctx_backend_params = context.backend_runtime_params\n        if ctx_backend_params is not None:\n            backend_params = cast(\n                params.BackendRuntimeParams, ctx_backend_params)\n        else:\n            backend_params = params.get_default_runtime_params()\n\n        if not backend_params.has_create_database:\n            key = f'{edbdef.EDGEDB_TEMPLATE_DB}metadata'\n            lock = dbops.Query(\n                trampoline.fixup_query(f'''\n                SELECT\n                    json\n                FROM\n                    edgedbinstdata_VER.instdata\n                WHERE\n                    key = {ql(key)}\n                FOR UPDATE\n                INTO _dummy_text\n            '''\n            ))\n        elif backend_params.has_superuser_access:\n            # Only superusers are generally allowed to make an UPDATE\n            # lock on shared catalogs.\n            lock = dbops.Query(\n                f'''\n                    SELECT\n                        description\n                    FROM\n                        pg_catalog.pg_shdescription\n                    WHERE\n                        objoid = (\n                            SELECT oid\n                            FROM pg_database\n                            WHERE datname =\n                              {V('edgedb')}.get_database_backend_name(\n                                {ql(edbdef.EDGEDB_TEMPLATE_DB)})\n                        )\n                        AND classoid = 'pg_database'::regclass::oid\n                    FOR UPDATE\n                    INTO _dummy_text\n                '''\n            )\n        else:\n            # Without superuser access we have to resort to lock polling.\n            # This is racy, but is unfortunately the best we can do.\n            lock = dbops.Query(f'''\n                SELECT\n                    edgedb_VER.raise_on_not_null(\n                        (\n                            SELECT 'locked'\n                            FROM pg_catalog.pg_locks\n                            WHERE\n                                locktype = 'object'\n                                AND classid = 'pg_database'::regclass::oid\n                                AND objid = (\n                                    SELECT oid\n                                    FROM pg_database\n                                    WHERE datname =\n                                      {V('edgedb')}.get_database_backend_name(\n                                        {ql(edbdef.EDGEDB_TEMPLATE_DB)})\n                                )\n                                AND mode = 'ShareUpdateExclusiveLock'\n                                AND granted\n                                AND pid != pg_backend_pid()\n                        ),\n                        'serialization_failure',\n                        msg => (\n                            'Cannot serialize global DDL: '\n                            || (SELECT version::text FROM\n                                {V('edgedb')}.\"_SysGlobalSchemaVersion\")\n                        )\n                    )\n                INTO _dummy_text\n            ''')\n\n        self.pgops.add(lock)\n\n        expected_ver = self.get_orig_attribute_value('version')\n        check = dbops.Query(\n            f'''\n                SELECT\n                    edgedb_VER.raise_on_not_null(\n                        (SELECT NULLIF(\n                            (SELECT\n                                version::text\n                            FROM\n                                {V('edgedb')}.\"_SysGlobalSchemaVersion\"\n                            ),\n                            {ql(str(expected_ver))}\n                        )),\n                        'serialization_failure',\n                        msg => (\n                            'Cannot serialize global DDL: '\n                            || (SELECT version::text FROM\n                                {V('edgedb')}.\"_SysGlobalSchemaVersion\")\n                        )\n                    )\n                INTO _dummy_text\n            '''\n        )\n        self.pgops.add(check)\n\n        metadata = {\n            ver_id: {\n                'id': ver_id,\n                'name': ver_name,\n                'version': str(self.scls.get_version(schema)),\n                'builtin': self.scls.get_builtin(schema),\n                'internal': self.scls.get_internal(schema),\n            }\n        }\n        if backend_params.has_create_database:\n            self.pgops.add(\n                dbops.UpdateMetadataSection(\n                    dbops.DatabaseWithTenant(name=edbdef.EDGEDB_TEMPLATE_DB),\n                    section='GlobalSchemaVersion',\n                    metadata=metadata\n                )\n            )\n        else:\n            self.pgops.add(\n                dbops.UpdateSingleDBMetadataSection(\n                    edbdef.EDGEDB_TEMPLATE_DB,\n                    section='GlobalSchemaVersion',\n                    metadata=metadata\n                )\n            )\n\n        return schema\n\n\nclass PseudoTypeCommand(MetaCommand):\n    pass\n\n\nclass CreatePseudoType(\n    PseudoTypeCommand,\n    adapts=s_pseudo.CreatePseudoType,\n):\n    pass\n\n\nclass TupleCommand(MetaCommand):\n\n    pass\n\n\nclass CreateTuple(TupleCommand, adapts=s_types.CreateTuple):\n\n    @classmethod\n    def create_tuple(\n        cls,\n        tup: s_types.Tuple,\n        schema: s_schema.Schema,\n        conditional: bool=False,\n    ) -> dbops.Command:\n        elements = tup.get_element_types(schema).items(schema)\n\n        name = common.get_backend_name(schema, tup, catenate=False)\n        ctype = dbops.CompositeType(\n            name=name,\n            columns=[\n                dbops.Column(\n                    name=n,\n                    type=qt(types.pg_type_from_object(\n                        schema, t, persistent_tuples=True)),\n                )\n                for n, t in elements\n            ]\n        )\n\n        neg_conditions = []\n        if conditional:\n            neg_conditions.append(dbops.TypeExists(name=name))\n\n        return dbops.CreateCompositeType(\n            type=ctype, neg_conditions=neg_conditions)\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n\n        if self.scls.is_polymorphic(schema):\n            return schema\n\n        self.pgops.add(self.create_tuple(\n            self.scls,\n            schema,\n            # XXX: WHY\n            conditional=context.stdmode,\n        ))\n\n        return schema\n\n\nclass AlterTuple(TupleCommand, adapts=s_types.AlterTuple):\n    pass\n\n\nclass RenameTuple(TupleCommand, adapts=s_types.RenameTuple):\n    pass\n\n\nclass DeleteTuple(TupleCommand, adapts=s_types.DeleteTuple):\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        tup = schema.get_global(s_types.Tuple, self.classname)\n        if not tup.is_polymorphic(schema):\n            domain_name = common.get_backend_name(schema, tup, catenate=False)\n            assert isinstance(domain_name, tuple)\n            self.pgops.add(drop_dependant_func_cache(domain_name))\n            self.pgops.add(dbops.DropCompositeType(name=domain_name))\n\n        schema = super().apply(schema, context)\n\n        return schema\n\n\nclass ExprAliasCommand(MetaCommand):\n    pass\n\n\nclass CreateAlias(\n    ExprAliasCommand,\n    adapts=s_aliases.CreateAlias,\n):\n    pass\n\n\nclass RenameAlias(\n    ExprAliasCommand,\n    adapts=s_aliases.RenameAlias,\n):\n    pass\n\n\nclass AlterAlias(\n    ExprAliasCommand,\n    adapts=s_aliases.AlterAlias,\n):\n    pass\n\n\nclass DeleteAlias(\n    ExprAliasCommand,\n    adapts=s_aliases.DeleteAlias,\n):\n    pass\n\n\nclass GlobalCommand(MetaCommand):\n    pass\n\n\nclass CreateGlobal(\n    GlobalCommand,\n    adapts=s_globals.CreateGlobal,\n):\n    pass\n\n\nclass RenameGlobal(\n    GlobalCommand,\n    adapts=s_globals.RenameGlobal,\n):\n    pass\n\n\nclass AlterGlobal(\n    GlobalCommand,\n    adapts=s_globals.AlterGlobal,\n):\n    pass\n\n\nclass SetGlobalType(\n    GlobalCommand,  # ???\n    adapts=s_globals.SetGlobalType,\n):\n    def register_config_op(self, op, context):\n        ops = context.get(sd.DeltaRootContext).op.config_ops\n        ops.append(op)\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n\n        schema = super().apply(schema, context)\n        if self.reset_value:\n            op = config_ops.Operation(\n                opcode=config_ops.OpCode.CONFIG_RESET,\n                scope=ql_ft.ConfigScope.GLOBAL,\n                setting_name=str(self.scls.get_name(schema)),\n                value=None,\n            )\n            self.register_config_op(op, context)\n\n        return schema\n\n\nclass DeleteGlobal(\n    GlobalCommand,\n    adapts=s_globals.DeleteGlobal,\n):\n    pass\n\n\nclass PermissionCommand(MetaCommand):\n    pass\n\n\nclass CreatePermission(\n    PermissionCommand,\n    adapts=s_permissions.CreatePermission,\n):\n    pass\n\n\nclass AlterPermission(\n    PermissionCommand,\n    adapts=s_permissions.AlterPermission,\n):\n    pass\n\n\nclass DeletePermission(\n    PermissionCommand,\n    adapts=s_permissions.DeletePermission,\n):\n    pass\n\n\nclass RenamePermission(\n    PermissionCommand,\n    adapts=s_permissions.RenamePermission,\n):\n    pass\n\n\nclass AccessPolicyCommand(MetaCommand):\n    pass\n\n\nclass CreateAccessPolicy(\n    AccessPolicyCommand,\n    adapts=s_policies.CreateAccessPolicy,\n):\n    pass\n\n\nclass RenameAccessPolicy(\n    AccessPolicyCommand,\n    adapts=s_policies.RenameAccessPolicy,\n):\n    pass\n\n\nclass RebaseAccessPolicy(\n    AccessPolicyCommand,\n    adapts=s_policies.RebaseAccessPolicy,\n):\n    pass\n\n\nclass AlterAccessPolicy(\n    AccessPolicyCommand,\n    adapts=s_policies.AlterAccessPolicy,\n):\n    pass\n\n\nclass DeleteAccessPolicy(\n    AccessPolicyCommand,\n    adapts=s_policies.DeleteAccessPolicy,\n):\n    pass\n\n\nclass TriggerCommand(MetaCommand):\n    pass\n\n\nclass CreateTrigger(\n    TriggerCommand,\n    adapts=s_triggers.CreateTrigger,\n):\n    pass\n\n\nclass RenameTrigger(\n    TriggerCommand,\n    adapts=s_triggers.RenameTrigger,\n):\n    pass\n\n\nclass RebaseTrigger(\n    TriggerCommand,\n    adapts=s_triggers.RebaseTrigger,\n):\n    pass\n\n\nclass AlterTrigger(\n    TriggerCommand,\n    adapts=s_triggers.AlterTrigger,\n):\n    pass\n\n\nclass DeleteTrigger(\n    TriggerCommand,\n    adapts=s_triggers.DeleteTrigger,\n):\n    pass\n\n\nclass RewriteCommand(MetaCommand):\n    pass\n\n\nclass CreateRewrite(\n    RewriteCommand,\n    adapts=s_rewrites.CreateRewrite,\n):\n    pass\n\n\nclass RebaseRewrite(\n    RewriteCommand,\n    adapts=s_rewrites.RebaseRewrite,\n):\n    pass\n\n\nclass RenameRewrite(\n    RewriteCommand,\n    adapts=s_rewrites.RenameRewrite,\n):\n    pass\n\n\nclass AlterRewrite(\n    RewriteCommand,\n    adapts=s_rewrites.AlterRewrite,\n):\n    pass\n\n\nclass DeleteRewrite(\n    RewriteCommand,\n    adapts=s_rewrites.DeleteRewrite,\n):\n    pass\n\n\nclass TupleExprAliasCommand(MetaCommand):\n    pass\n\n\nclass CreateTupleExprAlias(\n    TupleExprAliasCommand,\n    adapts=s_types.CreateTupleExprAlias,\n):\n    pass\n\n\nclass RenameTupleExprAlias(\n    TupleExprAliasCommand,\n    adapts=s_types.RenameTupleExprAlias,\n):\n    pass\n\n\nclass AlterTupleExprAlias(\n    TupleExprAliasCommand,\n    adapts=s_types.AlterTupleExprAlias,\n):\n    pass\n\n\nclass DeleteTupleExprAlias(\n    TupleExprAliasCommand,\n    adapts=s_types.DeleteTupleExprAlias,\n):\n    pass\n\n\nclass ArrayCommand(MetaCommand):\n    pass\n\n\nclass CreateArray(ArrayCommand, adapts=s_types.CreateArray):\n    pass\n\n\nclass AlterArray(ArrayCommand, adapts=s_types.AlterArray):\n    pass\n\n\nclass RenameArray(ArrayCommand, adapts=s_types.RenameArray):\n    pass\n\n\nclass DeleteArray(ArrayCommand, adapts=s_types.DeleteArray):\n    pass\n\n\nclass ArrayExprAliasCommand(MetaCommand):\n    pass\n\n\nclass CreateArrayExprAlias(\n    ArrayExprAliasCommand,\n    adapts=s_types.CreateArrayExprAlias,\n):\n    pass\n\n\nclass RenameArrayExprAlias(\n    ArrayExprAliasCommand,\n    adapts=s_types.RenameArrayExprAlias,\n):\n    pass\n\n\nclass AlterArrayExprAlias(\n    ArrayExprAliasCommand,\n    adapts=s_types.AlterArrayExprAlias,\n):\n    pass\n\n\nclass DeleteArrayExprAlias(\n    ArrayExprAliasCommand,\n    adapts=s_types.DeleteArrayExprAlias,\n):\n    pass\n\n\nclass RangeCommand(MetaCommand):\n    pass\n\n\nclass CreateRange(RangeCommand, adapts=s_types.CreateRange):\n    pass\n\n\nclass AlterRange(RangeCommand, adapts=s_types.AlterRange):\n    pass\n\n\nclass RenameRange(RangeCommand, adapts=s_types.RenameRange):\n    pass\n\n\nclass DeleteRange(RangeCommand, adapts=s_types.DeleteRange):\n    pass\n\n\nclass RangeExprAliasCommand(MetaCommand):\n    pass\n\n\nclass CreateRangeExprAlias(\n    RangeExprAliasCommand,\n    adapts=s_types.CreateRangeExprAlias,\n):\n    pass\n\n\nclass RenameRangeExprAlias(\n    RangeExprAliasCommand,\n    adapts=s_types.RenameRangeExprAlias,\n):\n    pass\n\n\nclass AlterRangeExprAlias(\n    RangeExprAliasCommand,\n    adapts=s_types.AlterRangeExprAlias,\n):\n    pass\n\n\nclass DeleteRangeExprAlias(\n    RangeExprAliasCommand,\n    adapts=s_types.DeleteRangeExprAlias,\n):\n    pass\n\n\nclass MultiRangeCommand(MetaCommand):\n    pass\n\n\nclass CreateMultiRange(MultiRangeCommand, adapts=s_types.CreateMultiRange):\n    pass\n\n\nclass AlterMultiRange(MultiRangeCommand, adapts=s_types.AlterMultiRange):\n    pass\n\n\nclass RenameMultiRange(MultiRangeCommand, adapts=s_types.RenameMultiRange):\n    pass\n\n\nclass DeleteMultiRange(MultiRangeCommand, adapts=s_types.DeleteMultiRange):\n    pass\n\n\nclass ParameterCommand(MetaCommand):\n    pass\n\n\nclass CreateParameter(\n    ParameterCommand,\n    adapts=s_funcs.CreateParameter,\n):\n    pass\n\n\nclass DeleteParameter(\n    ParameterCommand,\n    adapts=s_funcs.DeleteParameter,\n):\n    pass\n\n\nclass RenameParameter(\n    ParameterCommand,\n    adapts=s_funcs.RenameParameter,\n):\n    pass\n\n\nclass AlterParameter(\n    ParameterCommand,\n    adapts=s_funcs.AlterParameter,\n):\n    pass\n\n\nclass FunctionCommand(MetaCommand):\n    def get_pgname(self, func: s_funcs.Function, schema, versioned: bool=False):\n        return common.get_backend_name(\n            schema, func, catenate=False, versioned=versioned)\n\n    def get_pgtype(self, func: s_funcs.CallableObject, obj, schema):\n        if obj.is_any(schema):\n            return ('anyelement',)\n\n        try:\n            return types.pg_type_from_object(\n                schema, obj, persistent_tuples=True)\n        except ValueError:\n            raise errors.QueryError(\n                f'could not compile parameter type {obj!r} '\n                f'of function {func.get_shortname(schema)}',\n                span=self.span) from None\n\n    def compile_default(\n        self, func: s_funcs.Function, default: s_expr.Expression, schema\n    ):\n        try:\n            comp = default.compiled(\n                schema=schema,\n                as_fragment=True,\n                context=None,\n            )\n\n            ir = comp.irast\n            if not irutils.is_const(ir.expr):\n                raise ValueError('expression not constant')\n\n            sql_res = compiler.compile_ir_to_sql_tree(\n                ir.expr, singleton_mode=True)\n            return codegen.generate_source(sql_res.ast)\n\n        except Exception as ex:\n            raise errors.QueryError(\n                f'could not compile default expression {default!r} '\n                f'of function {func.get_shortname(schema)}: {ex}',\n                span=self.span) from ex\n\n    def compile_args(self, func: s_funcs.Function, schema):\n        func_params = func.get_params(schema)\n        has_inlined_defaults = func.has_inlined_defaults(schema)\n\n        args = []\n\n        func_language = func.get_language(schema)\n        if func_language is ql_ast.Language.EdgeQL:\n            args.append(('__edb_json_globals__', ('jsonb',), None))\n\n        if has_inlined_defaults:\n            args.append(('__defaults_mask__', ('bytea',), None))\n\n        compile_defaults = not (\n            has_inlined_defaults or func_params.find_named_only(schema)\n        )\n\n        for param in func_params.get_in_canonical_order(schema):\n            param_type = param.get_type(schema)\n            param_default = param.get_default(schema)\n\n            pg_at = self.get_pgtype(func, param_type, schema)\n\n            default = None\n            if compile_defaults and param_default is not None:\n                default = self.compile_default(func, param_default, schema)\n\n            pn = param.get_parameter_name(schema)\n            args.append((pn, pg_at, default))\n\n            if param_type.is_object_type():\n                args.append((f'__{pn}__type', ('uuid',), None))\n\n        return args\n\n    def make_function(self, func: s_funcs.Function, code, schema):\n        func_return_typemod = func.get_return_typemod(schema)\n        func_params = func.get_params(schema)\n\n        name = self.get_pgname(func, schema, versioned=False)\n        return self.get_function_type(name)(\n            name=name,\n            args=self.compile_args(func, schema),\n            has_variadic=func_params.find_variadic(schema) is not None,\n            set_returning=func_return_typemod is ql_ft.TypeModifier.SetOfType,\n            volatility=func.get_volatility(schema),\n            strict=func.get_impl_is_strict(schema),\n            returns=self.get_pgtype(\n                func, func.get_return_type(schema), schema),\n            text=code,\n        )\n\n    def compile_sql_function(self, func: s_funcs.Function, schema):\n        return self.make_function(func, func.get_code(schema), schema)\n\n    def _compile_edgeql_function(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        func: s_funcs.Function,\n        body: s_expr.Expression,\n    ) -> s_expr.CompiledExpression:\n        if isinstance(body, s_expr.CompiledExpression):\n            return body\n\n        # HACK: When an object type selected by a function (via\n        # inheritance) is dropped, the function gets\n        # recompiled. Unfortunately, 'caused' subcommands run *before*\n        # the object is actually deleted, and so we would ordinarily\n        # still try to select from the deleted object. To avoid\n        # needing to add *another* type of subcommand, we work around\n        # this by temporarily stripping all objects that are about to\n        # be deleted from the schema.\n        for ctx in context.stack:\n            if isinstance(ctx.op, s_objtypes.DeleteObjectType):\n                # Also get the pointers, since we look at pointer descendents.\n                # This is really all a pretty bad hack.\n                for ptr in ctx.op.scls.get_pointers(schema).objects(schema):\n                    schema = schema.delete(ptr)\n                schema = schema.delete(ctx.op.scls)\n            elif isinstance(ctx.op, s_pointers.DeletePointer):\n                schema = schema.delete(ctx.op.scls)\n\n        return s_funcs.compile_function(\n            schema,\n            context,\n            body=body,\n            func_name=func.get_name(schema),\n            params=func.get_params(schema),\n            language=ql_ast.Language.EdgeQL,\n            return_type=func.get_return_type(schema),\n            return_typemod=func.get_return_typemod(schema),\n        )\n\n    def fix_return_type(\n        self,\n        func: s_funcs.Function,\n        nativecode: s_expr.CompiledExpression,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_expr.CompiledExpression:\n\n        return_type = func.get_return_type(schema)\n        ir = nativecode.irast\n\n        if not (\n            return_type.is_object_type()\n            or s_types.is_type_compatible(return_type, ir.stype,\n                                          schema=nativecode.schema)\n        ):\n            # Add a cast and recompile it\n            qlexpr = qlcompiler.astutils.ensure_ql_query(\n                ql_ast.TypeCast(\n                    type=s_utils.typeref_to_ast(schema, return_type),\n                    expr=nativecode.parse(),\n                )\n            )\n            nativecode = self._compile_edgeql_function(\n                schema,\n                context,\n                func,\n                type(nativecode).from_ast(qlexpr, schema),\n            )\n\n        return nativecode\n\n    def compile_edgeql_function_body(\n        self,\n        func: s_funcs.Function,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> str:\n        nativecode = func.get_nativecode(schema)\n        assert nativecode\n        nativecode = self._compile_edgeql_function(\n            schema,\n            context,\n            func,\n            nativecode,\n        )\n\n        nativecode = self.fix_return_type(func, nativecode, schema, context)\n\n        sql_res = compiler.compile_ir_to_sql_tree(\n            nativecode.irast,\n            ignore_shapes=True,\n            explicit_top_cast=irtyputils.type_to_typeref(  # note: no cache\n                schema, func.get_return_type(schema), cache=None),\n            output_format=compiler.OutputFormat.NATIVE,\n            named_param_prefix=self.get_pgname(func, schema)[-1:],\n            versioned_stdlib=context.stdmode,\n        )\n\n        return codegen.generate_source(sql_res.ast)\n\n    def compile_edgeql_overloaded_function_body(\n        self,\n        func: s_funcs.Function,\n        overloads: list[s_funcs.Function],\n        ov_param_idx: int,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> str:\n        func_return_typemod = func.get_return_typemod(schema)\n        set_returning = func_return_typemod is ql_ft.TypeModifier.SetOfType\n        my_params = func.get_params(schema).objects(schema)\n        param_name = my_params[ov_param_idx].get_parameter_name(schema)\n        type_param_name = f'__{param_name}__type'\n        cases = {}\n        all_overloads = list(overloads)\n        if not isinstance(self, DeleteFunction):\n            all_overloads.append(func)\n        for overload in all_overloads:\n            ov_p = tuple(overload.get_params(schema).objects(schema))\n            ov_p_t = ov_p[ov_param_idx].get_type(schema)\n            ov_body = self.compile_edgeql_function_body(\n                overload, schema, context)\n\n            if set_returning:\n                case = (\n                    f\"(SELECT * FROM ({ov_body}) AS q \"\n                    f\"WHERE ancestor = {ql(str(ov_p_t.id))})\"\n                )\n            else:\n                case = (\n                    f\"WHEN ancestor = {ql(str(ov_p_t.id))} \"\n                    f\"THEN \\n({ov_body})\"\n                )\n\n            cases[ov_p_t] = case\n\n        impl_ids = ', '.join(f'{ql(str(t.id))}::uuid' for t in cases)\n        branches = list(cases.values())\n\n        # N.B: edgedb_VER.raise and coalesce are used below instead of\n        #      raise_on_null, because the latter somehow results in a\n        #      significantly more complex query plan.\n        matching_impl = f\"\"\"\n            coalesce(\n                (\n                    SELECT\n                        ancestor\n                    FROM\n                        (SELECT\n                            {qi(type_param_name)} AS ancestor,\n                            -1 AS index\n                        UNION ALL\n                        SELECT\n                            target AS ancestor,\n                            index\n                        FROM\n                            edgedb._object_ancestors\n                            WHERE source = {qi(type_param_name)}\n                        ) a\n                    WHERE ancestor IN ({impl_ids})\n                    ORDER BY index\n                    LIMIT 1\n                ),\n\n                edgedb.raise(\n                    NULL::uuid,\n                    'assert_failure',\n                    msg => format(\n                        'unhandled object type %s in overloaded function',\n                        {qi(type_param_name)}\n                    )\n                )\n            ) AS impl(ancestor)\n        \"\"\"\n\n        if set_returning:\n            arms = \"\\nUNION ALL\\n\".join(branches)\n            return f\"\"\"\n                SELECT\n                    q.*\n                FROM\n                    {matching_impl},\n                    LATERAL (\n                        {arms}\n                    ) AS q\n            \"\"\"\n        else:\n            arms = \"\\n\".join(branches)\n            return f\"\"\"\n                SELECT\n                    (CASE {arms} END)\n                FROM\n                    {matching_impl}\n            \"\"\"\n\n    def compile_edgeql_function(\n        self,\n        func: s_funcs.Function,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> tuple[Optional[dbops.Function], bool]:\n        if func.get_volatility(schema) == ql_ft.Volatility.Modifying:\n            # Modifying functions cannot be compiled correctly and should be\n            # inlined at the call point.\n\n            if func.find_object_param_overloads(schema) is not None:\n                raise errors.SchemaDefinitionError(\n                    f\"cannot overload an existing function \"\n                    f\"with a modifying function: \"\n                    f\"'{func.get_shortname(schema)}'\",\n                    span=self.span,\n                )\n\n            return None, False\n\n        nativecode: s_expr.Expression = not_none(func.get_nativecode(schema))\n        compiled_expr = self._compile_edgeql_function(\n            schema, context, func, nativecode\n        )\n        compiled_expr = self.fix_return_type(\n            func, compiled_expr, schema, context\n        )\n\n        replace = False\n\n        obj_overload = func.find_object_param_overloads(schema)\n        if obj_overload is not None:\n            overloads, ov_param_idx = obj_overload\n            if any(\n                overload.get_volatility(schema) == ql_ft.Volatility.Modifying\n                for overload in overloads\n            ):\n                raise errors.SchemaDefinitionError(\n                    f\"cannot overload an existing modifying function: \"\n                    f\"'{func.get_shortname(schema)}'\",\n                    span=self.span,\n                )\n\n            body = self.compile_edgeql_overloaded_function_body(\n                func, overloads, ov_param_idx, schema, context\n            )\n            replace = True\n        else:\n            sql_res = compiler.compile_ir_to_sql_tree(\n                compiled_expr.irast,\n                ignore_shapes=True,\n                explicit_top_cast=irtyputils.type_to_typeref(  # note: no cache\n                    schema, func.get_return_type(schema), cache=None),\n                output_format=compiler.OutputFormat.NATIVE,\n                named_param_prefix=self.get_pgname(func, schema)[-1:],\n                backend_runtime_params=context.backend_runtime_params,\n                versioned_stdlib=context.stdmode,\n            )\n\n            pg_debug.dump_ast_and_query(sql_res.ast, compiled_expr.irast)\n\n            body = codegen.generate_source(sql_res.ast)\n\n        return self.make_function(func, body, schema), replace\n\n    def sql_rval_consistency_check(\n        self,\n        cobj: s_funcs.CallableObject,\n        expr: str,\n        schema: s_schema.Schema,\n    ) -> dbops.Command:\n        fname = cobj.get_verbosename(schema)\n        rtype = types.pg_type_from_object(\n            schema,\n            cobj.get_return_type(schema),\n            persistent_tuples=True,\n        )\n        rtype_desc = '.'.join(rtype)\n\n        # Determine the actual returned type of the SQL function.\n        # We can't easily do this by looking in system catalogs because\n        # of polymorphic dispatch, but, fortunately, there's pg_typeof().\n        # We only need to be sure to actually NOT call the target function,\n        # as we can't assume how it'll behave with dummy inputs. Hence, the\n        # weird looking query below, where we rely in Postgres executor to\n        # skip the call, because no rows satisfy the WHERE condition, but\n        # we then still generate a NULL row via a LEFT JOIN.\n        f_test = textwrap.dedent(f'''\\\n            (SELECT\n                pg_typeof(f.i)\n            FROM\n                (SELECT NULL::text) AS spreader\n                LEFT JOIN (SELECT {expr} WHERE False) AS f(i) ON (true))''')\n\n        check = dbops.Query(text=f'''\n            PERFORM\n                edgedb_VER.raise_on_not_null(\n                    NULLIF(\n                        pg_typeof(NULL::{qt(rtype)}),\n                        {f_test}\n                    ),\n                    'invalid_function_definition',\n                    msg => format(\n                        '%s is declared to return SQL type \"%s\", but '\n                        || 'the underlying SQL function returns \"%s\"',\n                        {ql(fname)},\n                        {ql(rtype_desc)},\n                        {f_test}::text\n                    ),\n                    hint => (\n                        'Declare the function with '\n                        || '`force_return_cast := true`, '\n                        || 'or add an explicit cast to its body.'\n                    )\n                );\n        ''')\n\n        return check\n\n    def sql_strict_consistency_check(\n        self,\n        cobj: s_funcs.CallableObject,\n        func: str,\n        schema: s_schema.Schema,\n    ) -> dbops.Command:\n        fname = cobj.get_verbosename(schema)\n\n        # impl_is_strict means that the function is strict in all\n        # singleton arguments, so we don't need to do the check if\n        # no such arguments exist.\n        if (\n            not cobj.get_impl_is_strict(schema)\n            or not cobj.get_params(schema).has_type_mod(\n                schema, ql_ft.TypeModifier.SingletonType\n            )\n        ):\n            return dbops.CommandGroup()\n\n        if '.' in func:\n            ns, func = func.split('.')\n        else:\n            ns = 'pg_catalog'\n\n        f_test = textwrap.dedent(f'''\\\n            COALESCE((\n                SELECT bool_and(proisstrict) FROM pg_proc\n                INNER JOIN pg_namespace ON pg_namespace.oid = pronamespace\n                WHERE proname = {ql(func)} AND nspname = {ql(ns)}\n            ), false)\n        ''')\n\n        check = dbops.Query(text=f'''\n            PERFORM\n                edgedb_VER.raise_on_null(\n                    NULLIF(\n                        false,\n                        {f_test}\n                    ),\n                    'invalid_function_definition',\n                    msg => format(\n                        '%s is declared to have a strict impl but does not',\n                        {ql(fname)}\n                    ),\n                    hint => (\n                        'Add `impl_is_strict := false` to the declaration.'\n                    )\n                );\n        ''')\n\n        return check\n\n    def get_dummy_func_call(\n        self,\n        cobj: s_funcs.CallableObject,\n        sql_func: Sequence[str],\n        schema: s_schema.Schema,\n    ) -> str:\n        name = common.maybe_versioned_name(\n            tuple(sql_func),\n            versioned=(\n                cobj.get_name(schema).get_root_module_name().name != 'ext'\n            ),\n        )\n\n        args = []\n        func_params = cobj.get_params(schema)\n        for param in func_params.get_in_canonical_order(schema):\n            param_type = param.get_type(schema)\n            pg_at = self.get_pgtype(cobj, param_type, schema)\n            args.append(f'NULL::{qt(pg_at)}')\n            if isinstance(param_type, s_objtypes.ObjectType):\n                args.append(f'NULL::uuid')\n\n        return f'{q(*name)}({\", \".join(args)})'\n\n    def make_op(\n        self,\n        func: s_funcs.Function,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        or_replace: bool=False,\n    ) -> Iterable[dbops.Command]:\n        if func.get_from_expr(schema):\n            # Intrinsic function, handled directly by the compiler.\n            return ()\n        elif sql_func := func.get_from_function(schema):\n            func_params = func.get_params(schema)\n\n            if (\n                func.get_force_return_cast(schema)\n                or func_params.has_polymorphic(schema)\n                or func.get_sql_func_has_out_params(schema)\n            ):\n                return ()\n            else:\n                # Function backed directly by an SQL function.\n                # Check the consistency of the return type.\n                dexpr = self.get_dummy_func_call(\n                    func, sql_func.split('.'), schema)\n                return (\n                    self.sql_rval_consistency_check(func, dexpr, schema),\n                    self.sql_strict_consistency_check(func, sql_func, schema),\n                )\n        else:\n            func_language = func.get_language(schema)\n\n            dbf: Optional[dbops.Function]\n            if func_language is ql_ast.Language.SQL:\n                dbf = self.compile_sql_function(func, schema)\n            elif func_language is ql_ast.Language.EdgeQL:\n                dbf, overload_replace = self.compile_edgeql_function(\n                    func, schema, context\n                )\n                if overload_replace:\n                    or_replace = True\n            else:\n                raise errors.QueryError(\n                    f'cannot compile function {func.get_shortname(schema)}: '\n                    f'unsupported language {func_language}',\n                    span=self.span)\n\n            ops: list[dbops.Command] = []\n\n            if dbf is not None:\n                ops.append(dbops.CreateFunction(dbf, or_replace=or_replace))\n                self.maybe_trampoline(dbf, context)\n            return ops\n\n\nclass CreateFunction(\n    FunctionCommand,\n    adapts=s_funcs.CreateFunction,\n):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n        ops = self.make_op(self.scls, schema, context)\n        self.pgops.update(ops)\n        return schema\n\n\nclass RenameFunction(FunctionCommand, adapts=s_funcs.RenameFunction):\n    pass\n\n\nclass AlterFunction(FunctionCommand, adapts=s_funcs.AlterFunction):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n\n        if self.metadata_only:\n            return schema\n\n        if (\n            self.get_attribute_value('volatility') is not None or\n            self.get_attribute_value('nativecode') is not None or\n            self.get_attribute_value('code') is not None\n        ):\n            self.pgops.update(\n                self.make_op(self.scls, schema, context, or_replace=True))\n\n        return schema\n\n\nclass DeleteFunction(FunctionCommand, adapts=s_funcs.DeleteFunction):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        func = self.get_object(schema, context)\n        nativecode = func.get_nativecode(schema)\n\n        if func.get_code(schema) or nativecode:\n            # An EdgeQL or a SQL function\n            # (not just an alias to a SQL function).\n\n            overload = False\n            if nativecode and func.find_object_param_overloads(schema):\n                dbf, overload_replace = self.compile_edgeql_function(\n                    func, schema, context\n                )\n                if dbf is not None and overload_replace:\n                    self.pgops.add(dbops.CreateFunction(dbf, or_replace=True))\n                    overload = True\n\n            if not overload:\n                variadic = func.get_params(schema).find_variadic(schema)\n                if func.get_volatility(schema) != ql_ft.Volatility.Modifying:\n                    # Modifying functions are not compiled.\n                    # See: compile_edgeql_function\n                    self.pgops.add(\n                        dbops.DropFunction(\n                            name=self.get_pgname(func, schema),\n                            args=self.compile_args(func, schema),\n                            has_variadic=variadic is not None,\n                        )\n                    )\n\n        return super().apply(schema, context)\n\n\nclass OperatorCommand(FunctionCommand):\n\n    def oper_name_to_pg_name(\n        self,\n        schema,\n        name: sn.QualName,\n    ) -> tuple[str, str]:\n        return common.get_operator_backend_name(\n            name, catenate=False)\n\n    def get_pg_operands(self, schema, oper: s_opers.Operator):\n        left_type = None\n        right_type = None\n        oper_params = list(oper.get_params(schema).objects(schema))\n        oper_kind = oper.get_operator_kind(schema)\n\n        if oper_kind is ql_ft.OperatorKind.Infix:\n            left_type = types.pg_type_from_object(\n                schema, oper_params[0].get_type(schema))\n\n            right_type = types.pg_type_from_object(\n                schema, oper_params[1].get_type(schema))\n\n        elif oper_kind is ql_ft.OperatorKind.Prefix:\n            right_type = types.pg_type_from_object(\n                schema, oper_params[0].get_type(schema))\n\n        elif oper_kind is ql_ft.OperatorKind.Postfix:\n            left_type = types.pg_type_from_object(\n                schema, oper_params[0].get_type(schema))\n\n        else:\n            raise RuntimeError(\n                f'unexpected operator type: {oper_kind!r}')\n\n        return left_type, right_type\n\n    # FIXME: We should make split FunctionCommand into CallableCommand\n    # and FunctionCommand and only inherit from CallableCommand\n    def compile_args(self, oper: s_opers.Operator, schema):  # type: ignore\n        args = []\n        oper_params = oper.get_params(schema)\n\n        for param in oper_params.get_in_canonical_order(schema):\n            pg_at = self.get_pgtype(oper, param.get_type(schema), schema)\n            args.append((param.get_parameter_name(schema), pg_at))\n\n        return args\n\n    def make_operator_function(self, oper: s_opers.Operator, schema):\n        name = common.get_backend_name(\n            schema, oper, catenate=False, versioned=False, aspect='function')\n        return self.get_function_type(name)(\n            name=name,\n            args=self.compile_args(oper, schema),\n            volatility=oper.get_volatility(schema),\n            returns=self.get_pgtype(\n                oper, oper.get_return_type(schema), schema),\n            text=not_none(oper.get_code(schema)),\n        )\n\n    def get_dummy_operator_call(\n        self,\n        oper: s_opers.Operator,\n        pgop: str,\n        from_args: Sequence[tuple[str, ...] | str],\n        schema: s_schema.Schema,\n    ) -> str:\n        # Need a proxy function with casts\n        oper_kind = oper.get_operator_kind(schema)\n\n        if oper_kind is ql_ft.OperatorKind.Infix:\n            op = f'NULL::{qt(from_args[0])} {pgop} NULL::{qt(from_args[1])}'\n        elif oper_kind is ql_ft.OperatorKind.Postfix:\n            op = f'NULL::{qt(from_args[0])} {pgop}'\n        elif oper_kind is ql_ft.OperatorKind.Prefix:\n            op = f'{pgop} NULL::{qt(from_args[1])}'\n        else:\n            raise RuntimeError(f'unexpected operator kind: {oper_kind!r}')\n\n        return op\n\n\nclass CreateOperator(OperatorCommand, adapts=s_opers.CreateOperator):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n        oper = self.scls\n        if oper.get_abstract(schema):\n            return schema\n\n        params = oper.get_params(schema)\n        oper_language = oper.get_language(schema)\n        oper_fromop = oper.get_from_operator(schema)\n        oper_fromfunc = oper.get_from_function(schema)\n        oper_code = oper.get_code(schema)\n\n        # We support having both fromop and one of the others for\n        # \"legacy\" purposes, but ignore it.\n        if oper_code or oper_fromfunc:\n            oper_fromop = None\n\n        if oper_language is ql_ast.Language.SQL and oper_fromop:\n            pg_oper_name = oper_fromop[0]\n            args = self.get_pg_operands(schema, oper)\n            if len(oper_fromop) > 1:\n                # Explicit operand types given in FROM SQL OPERATOR.\n                from_args = oper_fromop[1:]\n            else:\n                from_args = args\n\n            if (\n                pg_oper_name is not None\n                and not params.has_polymorphic(schema)\n                and not oper.get_force_return_cast(schema)\n            ):\n                cexpr = self.get_dummy_operator_call(\n                    oper, pg_oper_name, from_args, schema)\n\n                # We don't do a strictness consistency check for\n                # USING SQL OPERATOR because they are heavily\n                # overloaded, and so we'd need to take the types\n                # into account; this is doable, but doesn't seem\n                # worth doing since the only non-strict operator\n                # is || on arrays, and we use array_cat for that\n                # anyway!\n                check = self.sql_rval_consistency_check(\n                    oper, cexpr, schema)\n                self.pgops.add(check)\n\n        elif oper_language is ql_ast.Language.SQL and oper_code:\n            args = self.get_pg_operands(schema, oper)\n            oper_func = self.make_operator_function(oper, schema)\n            self.pgops.add(dbops.CreateFunction(oper_func))\n\n            self.maybe_trampoline(oper_func, context)\n\n            if not params.has_polymorphic(schema):\n                cexpr = self.get_dummy_func_call(\n                    oper, oper_func.name, schema)\n                check = self.sql_rval_consistency_check(oper, cexpr, schema)\n                self.pgops.add(check)\n\n        elif oper_language is ql_ast.Language.SQL and oper_fromfunc:\n            args = self.get_pg_operands(schema, oper)\n            oper_func_name = oper_fromfunc[0]\n            if len(oper_fromfunc) > 1:\n                args = oper_fromfunc[1:]\n\n            cargs = []\n            for t in args:\n                if t is not None:\n                    cargs.append(f'NULL::{qt(t)}')\n\n            if not params.has_polymorphic(schema):\n                cexpr = f\"{qi(oper_func_name)}({', '.join(cargs)})\"\n                check = self.sql_rval_consistency_check(oper, cexpr, schema)\n                self.pgops.add(check)\n            check2 = self.sql_strict_consistency_check(\n                oper, oper_func_name, schema)\n            self.pgops.add(check2)\n\n        elif oper.get_from_expr(schema):\n            # This operator is handled by the compiler and does not\n            # need explicit representation in the backend.\n            pass\n\n        else:\n            raise errors.QueryError(\n                f'cannot create operator {oper.get_shortname(schema)}: '\n                f'only \"FROM SQL\" and \"FROM SQL OPERATOR\" operators '\n                f'are currently supported',\n                span=self.span)\n\n        return schema\n\n\nclass RenameOperator(OperatorCommand, adapts=s_opers.RenameOperator):\n    pass\n\n\nclass AlterOperator(OperatorCommand, adapts=s_opers.AlterOperator):\n    pass\n\n\nclass DeleteOperator(OperatorCommand, adapts=s_opers.DeleteOperator):\n    pass\n\n\nclass CastCommand(MetaCommand):\n    def make_cast_function(self, cast: s_casts.Cast, schema):\n        name = common.get_backend_name(\n            schema, cast, catenate=False, versioned=False, aspect='function')\n\n        args: Sequence[dbops.FunctionArg] = [\n            (\n                'val',\n                types.pg_type_from_object(schema, cast.get_from_type(schema))\n            ),\n            ('detail', ('text',), \"''\"),\n        ]\n\n        returns = types.pg_type_from_object(schema, cast.get_to_type(schema))\n\n        # N.B: Semantically, strict *ought* to be true, since we want\n        # all of our casts to have strict behavior. Unfortunately,\n        # actually marking them as strict causes a huge performance\n        # regression when bootstrapping (and probably anything else that\n        # is heavy on json casts), so instead we just need to make sure\n        # to write cast code that is naturally strict (this is enforced\n        # by test_edgeql_casts_all_null).\n        return self.get_function_type(name)(\n            name=name,\n            args=args,\n            returns=returns,\n            strict=False,\n            wrapper_volatility=cast.get_volatility(schema),\n            text=not_none(cast.get_code(schema)),\n        )\n\n\nclass CreateCast(CastCommand, adapts=s_casts.CreateCast):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n        cast = self.scls\n        cast_language = cast.get_language(schema)\n        cast_code = cast.get_code(schema)\n        from_cast = cast.get_from_cast(schema)\n        from_expr = cast.get_from_expr(schema)\n\n        if cast_language is ql_ast.Language.SQL and cast_code:\n            cast_func = self.make_cast_function(cast, schema)\n            self.pgops.add(dbops.CreateFunction(cast_func))\n            self.maybe_trampoline(cast_func, context)\n\n        elif from_cast is not None or from_expr is not None:\n            # This operator is handled by the compiler and does not\n            # need explicit representation in the backend.\n            pass\n\n        else:\n            raise errors.QueryError(\n                f'cannot create cast: '\n                f'only \"FROM SQL\" and \"FROM SQL FUNCTION\" casts '\n                f'are currently supported',\n                span=self.span)\n\n        return schema\n\n\nclass RenameCast(CastCommand, adapts=s_casts.RenameCast):\n    pass\n\n\nclass AlterCast(CastCommand, adapts=s_casts.AlterCast):\n    pass\n\n\nclass DeleteCast(CastCommand, adapts=s_casts.DeleteCast):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        cast = schema.get(self.classname, type=s_casts.Cast)\n        cast_language = cast.get_language(schema)\n        cast_code = cast.get_code(schema)\n\n        schema = super().apply(schema, context)\n\n        if cast_language is ql_ast.Language.SQL and cast_code:\n            cast_func = self.make_cast_function(cast, orig_schema)\n            self.pgops.add(dbops.DropFunction(\n                cast_func.name, cast_func.args))\n\n        return schema\n\n\nclass AnnotationCommand(MetaCommand):\n    pass\n\n\nclass CreateAnnotation(AnnotationCommand, adapts=s_anno.CreateAnnotation):\n    pass\n\n\nclass RenameAnnotation(AnnotationCommand, adapts=s_anno.RenameAnnotation):\n    pass\n\n\nclass AlterAnnotation(AnnotationCommand, adapts=s_anno.AlterAnnotation):\n    pass\n\n\nclass DeleteAnnotation(AnnotationCommand, adapts=s_anno.DeleteAnnotation):\n    pass\n\n\nclass AnnotationValueCommand(MetaCommand):\n    pass\n\n\nclass CreateAnnotationValue(\n    AnnotationValueCommand,\n    adapts=s_anno.CreateAnnotationValue,\n):\n    pass\n\n\nclass AlterAnnotationValue(\n    AnnotationValueCommand,\n    adapts=s_anno.AlterAnnotationValue,\n):\n    pass\n\n\nclass AlterAnnotationValueOwned(\n    AnnotationValueCommand,\n    adapts=s_anno.AlterAnnotationValueOwned,\n):\n    pass\n\n\nclass RenameAnnotationValue(\n    AnnotationValueCommand,\n    adapts=s_anno.RenameAnnotationValue,\n):\n    pass\n\n\nclass RebaseAnnotationValue(\n    AnnotationValueCommand,\n    adapts=s_anno.RebaseAnnotationValue,\n):\n    pass\n\n\nclass DeleteAnnotationValue(\n    AnnotationValueCommand,\n    adapts=s_anno.DeleteAnnotationValue,\n):\n    pass\n\n\nclass ConstraintCommand(MetaCommand):\n    @classmethod\n    def constraint_is_effective(\n        cls, schema: s_schema.Schema, constraint: s_constr.Constraint\n    ) -> bool:\n        subject = constraint.get_subject(schema)\n        if subject is None:\n            return False\n\n        ancestors = [\n            a for a in constraint.get_ancestors(schema).objects(schema)\n            if not a.is_non_concrete(schema)\n        ]\n\n        if (\n            constraint.get_delegated(schema)\n            and all(ancestor.get_delegated(schema) for ancestor in ancestors)\n        ):\n            return False\n\n        if irtyputils.is_cfg_view(subject, schema):\n            return False\n\n        match subject:\n            case s_pointers.Pointer():\n                if subject.is_non_concrete(schema):\n                    return True\n                else:\n                    return types.has_table(subject.get_source(schema), schema)\n            case s_objtypes.ObjectType():\n                return types.has_table(subject, schema)\n            case s_scalars.ScalarType():\n                return not subject.get_abstract(schema)\n        raise NotImplementedError(subject)\n\n    def schedule_relatives_constraint_trigger_update(\n        self,\n        constraint: s_constr.Constraint,\n        orig_schema: s_schema.Schema,\n        curr_schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ):\n        # Find all origins whose relationship with the constraint has changed.\n        orig_origins: dict[uuid.UUID, s_constr.Constraint] = {}\n        if orig_schema.has_object(constraint.id):\n            for origin in constraint.get_constraint_origins(orig_schema):\n                orig_origins[origin.id] = origin\n        curr_origins: dict[uuid.UUID, s_constr.Constraint] = {}\n        if curr_schema.has_object(constraint.id):\n            for origin in constraint.get_constraint_origins(curr_schema):\n                curr_origins[origin.id] = origin\n\n        # Find all constraints whose inheritance relationship with the\n        # constraint has changed.\n        relative_ids: set[uuid.UUID] = set()\n        for origin_id in (orig_origins.keys() - curr_origins.keys()):\n            origin = orig_origins[origin_id]\n            for relative in (\n                [origin] + list(origin.descendants(orig_schema))\n            ):\n                if not curr_schema.has_object(relative.id):\n                    # The constraint was deleted, updating the triggers is\n                    # not needed.\n                    continue\n                relative_ids.add(relative.id)\n\n        for origin_id in (curr_origins.keys() - orig_origins.keys()):\n            origin = curr_origins[origin_id]\n            for relative in (\n                [origin] + list(origin.descendants(curr_schema))\n            ):\n                relative_ids.add(relative.id)\n\n        relatives: list[s_constr.Constraint] = [\n            curr_schema.get_by_id(relative_id, type=s_constr.Constraint)\n            for relative_id in relative_ids\n        ]\n\n        op = dbops.CommandGroup()\n\n        # Schedule constraint trigger updates for relatives.\n        for relative in relatives:\n            self.schedule_constraint_trigger_update(\n                relative,\n                curr_schema,\n                context,\n                s_sources.SourceCommandContext,\n            )\n\n        return op\n\n    @staticmethod\n    def create_constraint(\n        current_command: MetaCommand,\n        constraint: s_constr.Constraint,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        span: Optional[parsing.Span] = None,\n        *,\n        create_triggers_if_needed: bool = True,\n    ) -> dbops.Command:\n        op = dbops.CommandGroup()\n        if ConstraintCommand.constraint_is_effective(schema, constraint):\n            subject = constraint.get_subject(schema)\n\n            if subject is not None:\n                op.add_command(ConstraintCommand._get_create_ops(\n                    current_command,\n                    constraint,\n                    schema,\n                    context,\n                    span,\n                    create_triggers_if_needed=create_triggers_if_needed,\n                ))\n\n        return op\n\n    @staticmethod\n    def _get_create_ops(\n        current_command: MetaCommand,\n        constraint: s_constr.Constraint,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        span: Optional[parsing.Span] = None,\n        *,\n        create_triggers_if_needed: bool = True,\n    ) -> dbops.CommandGroup:\n        subject = constraint.get_subject(schema)\n        assert subject is not None\n        compiled_constraint = schemamech.compile_constraint(\n            subject,\n            constraint,\n            schema,\n            span,\n        )\n\n        op = compiled_constraint.create_ops()\n\n        if create_triggers_if_needed:\n            # Constraint triggers are created last to avoid repeated\n            # recompilation.\n            current_command.schedule_constraint_trigger_update(\n                constraint,\n                schema,\n                context,\n                s_sources.SourceCommandContext,\n            )\n\n        return op\n\n    @staticmethod\n    def _get_alter_ops(\n        current_command: MetaCommand,\n        constraint: s_constr.Constraint,\n        orig_schema: s_schema.Schema,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        span: Optional[parsing.Span] = None,\n    ) -> dbops.CommandGroup:\n        orig_subject = constraint.get_subject(orig_schema)\n        assert orig_subject is not None\n        orig_compiled_constraint = schemamech.compile_constraint(\n            orig_subject,\n            constraint,\n            orig_schema,\n            span,\n        )\n\n        subject = constraint.get_subject(schema)\n        assert subject is not None\n        compiled_constraint = schemamech.compile_constraint(\n            subject,\n            constraint,\n            schema,\n            span,\n        )\n\n        op = compiled_constraint.alter_ops(orig_compiled_constraint)\n\n        # Constraint triggers are created last to avoid repeated recompilation.\n        current_command.schedule_constraint_trigger_update(\n            constraint,\n            schema,\n            context,\n            s_sources.SourceCommandContext,\n        )\n\n        return op\n\n    @classmethod\n    def delete_constraint(\n        cls,\n        constraint: s_constr.Constraint,\n        schema: s_schema.Schema,\n        span: Optional[parsing.Span] = None,\n    ) -> dbops.Command:\n        op = dbops.CommandGroup()\n        if cls.constraint_is_effective(schema, constraint):\n            subject = constraint.get_subject(schema)\n\n            if subject is not None:\n                bconstr = schemamech.compile_constraint(\n                    subject, constraint, schema, span\n                )\n\n                op.add_command(bconstr.delete_ops())\n\n        return op\n\n    @classmethod\n    def enforce_constraint(\n        cls,\n        constraint: s_constr.Constraint,\n        schema: s_schema.Schema,\n        span: Optional[parsing.Span] = None,\n    ) -> dbops.Command:\n\n        if cls.constraint_is_effective(schema, constraint):\n            subject = constraint.get_subject(schema)\n\n            if subject is not None:\n                bconstr = schemamech.compile_constraint(\n                    subject, constraint, schema, span\n                )\n\n                return bconstr.enforce_ops()\n        else:\n            return dbops.CommandGroup()\n\n\nclass CreateConstraint(ConstraintCommand, adapts=s_constr.CreateConstraint):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        schema = super().apply(schema, context)\n        constraint: s_constr.Constraint = self.scls\n\n        self.pgops.add(ConstraintCommand.create_constraint(\n            self, constraint, schema, context, self.span\n        ))\n\n        self.pgops.add(self.schedule_relatives_constraint_trigger_update(\n            constraint, orig_schema, schema, context,\n        ))\n\n        # If the constraint is being added to existing data,\n        # we need to enforce it on the existing data. (This only\n        # matters when inheritance is in play and we use triggers\n        # to enforce exclusivity across tables.)\n        if (\n            (subject := constraint.get_subject(schema))\n            and isinstance(\n                subject, (s_objtypes.ObjectType, s_pointers.Pointer))\n            and not context.is_creating(subject)\n        ):\n            self.pgops.add(self.enforce_constraint(\n                constraint, schema, self.span\n            ))\n\n        return schema\n\n\nclass RenameConstraint(ConstraintCommand, adapts=s_constr.RenameConstraint):\n    pass\n\n\nclass AlterConstraintOwned(\n    ConstraintCommand,\n    adapts=s_constr.AlterConstraintOwned,\n):\n    pass\n\n\nclass AlterConstraint(\n    ConstraintCommand,\n    adapts=s_constr.AlterConstraint,\n):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        schema = super().apply(schema, context)\n        constraint: s_constr.Constraint = self.scls\n        if self.metadata_only:\n            return schema\n        if (\n            not self.constraint_is_effective(schema, constraint)\n            and not self.constraint_is_effective(orig_schema, constraint)\n        ):\n            return schema\n\n        subject = constraint.get_subject(schema)\n\n        subcommands = list(self.get_subcommands())\n        if (not subcommands or\n                isinstance(subcommands[0], s_constr.RenameConstraint)):\n            # This is a pure rename, so everything had been handled by\n            # RenameConstraint above.\n            return schema\n\n        if subject is not None:\n            if pcontext := context.get(s_pointers.PointerCommandContext):\n                orig_schema = pcontext.original_schema\n\n            op = dbops.CommandGroup()\n            if not self.constraint_is_effective(orig_schema, constraint):\n                op.add_command(ConstraintCommand._get_create_ops(\n                    self, constraint, schema, context, self.span\n                ))\n\n                # XXX: I don't think any of this logic is needed??\n                for child in constraint.children(schema):\n                    op.add_command(ConstraintCommand._get_alter_ops(\n                        self, child, orig_schema, schema, context, self.span\n                    ))\n            elif not self.constraint_is_effective(schema, constraint):\n                op.add_command(ConstraintCommand._get_alter_ops(\n                    self, constraint, orig_schema, schema, context, self.span\n                ))\n\n                for child in constraint.children(schema):\n                    op.add_command(ConstraintCommand._get_alter_ops(\n                        self, child, orig_schema, schema, context, self.span\n                    ))\n            else:\n                op.add_command(ConstraintCommand._get_alter_ops(\n                    self, constraint, orig_schema, schema, context, self.span\n                ))\n            self.pgops.add(op)\n\n            if (\n                (subject := constraint.get_subject(schema))\n                and isinstance(\n                    subject, (s_objtypes.ObjectType, s_pointers.Pointer))\n                and not context.is_creating(subject)\n                and not context.is_deleting(subject)\n            ):\n                self.pgops.add(self.enforce_constraint(\n                    constraint, schema, self.span\n                ))\n\n            self.pgops.add(self.schedule_relatives_constraint_trigger_update(\n                constraint, orig_schema, schema, context,\n            ))\n\n        return schema\n\n\nclass DeleteConstraint(ConstraintCommand, adapts=s_constr.DeleteConstraint):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        delta_root_ctx = context.top()\n        orig_schema = delta_root_ctx.original_schema\n        constraint: s_constr.Constraint = (\n            schema.get(self.classname, type=s_constr.Constraint)\n        )\n\n        schema = super().apply(schema, context)\n        op = self.delete_constraint(\n            constraint, orig_schema, self.span\n        )\n        self.pgops.add(op)\n\n        self.pgops.add(self.schedule_relatives_constraint_trigger_update(\n            constraint, orig_schema, schema, context,\n        ))\n\n        return schema\n\n\nclass RebaseConstraint(ConstraintCommand, adapts=s_constr.RebaseConstraint):\n    pass\n\n\nclass AliasCapableMetaCommand(MetaCommand):\n    pass\n\n\nclass ScalarTypeMetaCommand(AliasCapableMetaCommand):\n\n    @classmethod\n    def is_sequence(cls, schema, scalar):\n        seq = schema.get('std::sequence', default=None)\n        return seq is not None and scalar.issubclass(schema, seq)\n\n\nclass CreateScalarType(ScalarTypeMetaCommand,\n                       adapts=s_scalars.CreateScalarType):\n\n    @classmethod\n    def create_scalar(\n        cls,\n        scalar: s_scalars.ScalarType,\n        default: Optional[s_expr.Expression],\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> dbops.Command:\n\n        if scalar.is_concrete_enum(schema):\n            enum_values = scalar.get_enum_values(schema)\n            assert enum_values\n\n            return CreateScalarType.create_enum(\n                scalar, enum_values, schema, context)\n        else:\n            ops = dbops.CommandGroup()\n\n            if scalar.get_transient(schema):\n                return ops\n\n            base = types.get_scalar_base(schema, scalar)\n\n            new_domain_name = types.pg_type_from_scalar(schema, scalar)\n\n            if cls.is_sequence(schema, scalar):\n                seq_name = common.get_backend_name(\n                    schema, scalar, catenate=False, aspect='sequence')\n                ops.add_command(dbops.CreateSequence(name=seq_name))\n\n            domain = dbops.Domain(name=new_domain_name, base=base)\n            ops.add_command(dbops.CreateDomain(domain=domain))\n\n            if (default is not None\n                    and not isinstance(default, s_expr.Expression)):\n                # We only care to support literal defaults here. Supporting\n                # defaults based on queries has no sense on the database\n                # level since the database forbids queries for DEFAULT and\n                # pre- calculating the value does not make sense either\n                # since the whole point of query defaults is for them to be\n                # dynamic.\n                ops.add_command(\n                    dbops.AlterDomainAlterDefault(\n                        name=new_domain_name, default=default))\n\n            return ops\n\n    @classmethod\n    def create_enum(\n        cls,\n        scalar: s_scalars.ScalarType,\n        values: Sequence[str],\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> dbops.Command:\n        ops = dbops.CommandGroup()\n\n        new_enum_name = common.get_backend_name(schema, scalar, catenate=False)\n\n        neg_conditions = []\n        if context.stdmode:\n            neg_conditions.append(dbops.EnumExists(name=new_enum_name))\n\n        ops.add_command(\n            dbops.CreateEnum(\n                dbops.Enum(name=new_enum_name, values=values),\n                neg_conditions=neg_conditions,\n            )\n        )\n\n        fcls = cls.get_function_type(new_enum_name)\n\n        # Cast wrapper function is needed for immutable casts, which are\n        # needed for casting within indexes/constraints.\n        # (Postgres casts are only stable)\n        cast_func_name = common.get_backend_name(\n            schema, scalar, catenate=False, aspect=\"enum-cast-from-str\"\n        )\n        cast_func = fcls(\n            name=cast_func_name,\n            args=[(\"value\", (\"anyelement\",))],\n            volatility=\"immutable\",\n            returns=new_enum_name,\n            text=f\"SELECT value::{qt(new_enum_name)}\",\n        )\n        ops.add_command(dbops.CreateFunction(cast_func))\n        cls.maybe_trampoline(cast_func, context)\n\n        # Simialry, uncast from enum to str\n        uncast_func_name = common.get_backend_name(\n            schema, scalar, catenate=False, aspect=\"enum-cast-into-str\"\n        )\n        uncast_func = fcls(\n            name=uncast_func_name,\n            args=[(\"value\", (\"anyelement\",))],\n            volatility=\"immutable\",\n            returns=\"text\",\n            text=f\"SELECT value::text\",\n        )\n        ops.add_command(dbops.CreateFunction(uncast_func))\n        cls.maybe_trampoline(uncast_func, context)\n        return ops\n\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._create_begin(schema, context)\n        scalar = self.scls\n\n        if scalar.get_abstract(schema):\n            return schema\n\n        if types.is_builtin_scalar(schema, scalar):\n            return schema\n        # If this type exposes a SQL type or is a parameterized\n        # subtype of a SQL type, we don't create a real type here.\n        if scalar.resolve_sql_type_scheme(schema)[0]:\n            return schema\n\n        default = self.get_resolved_attribute_value(\n            'default',\n            schema=schema,\n            context=context,\n        )\n        self.pgops.add(self.create_scalar(scalar, default, schema, context))\n\n        return schema\n\n\nclass RenameScalarType(\n    ScalarTypeMetaCommand,\n    adapts=s_scalars.RenameScalarType,\n):\n    pass\n\n\nclass RebaseScalarType(\n    ScalarTypeMetaCommand,\n    adapts=s_scalars.RebaseScalarType,\n):\n    # Actual rebase is taken care of in AlterScalarType\n    pass\n\n\nclass AlterScalarType(ScalarTypeMetaCommand, adapts=s_scalars.AlterScalarType):\n\n    problematic_refs: Optional[tuple[\n        tuple[so.Object, ...],\n        dict[s_props.Property, s_types.TypeShell],\n    ]]\n\n    def _get_problematic_refs(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        composite_only: bool,\n    ) -> Optional[tuple[\n        tuple[so.Object, ...],\n        dict[s_props.Property, s_types.TypeShell],\n    ]]:\n        \"\"\"Find problematic references to this scalar type that need handled.\n\n        This is used to work around two irritating limitations of Postgres:\n          1. That elements of enum types may not be removed or reordered\n          2. That a constraint may not be added to a domain type if that\n             domain type appears in a *composite* type that is used in a\n             column somewhere.\n\n        We don't want to have these limitations, and we need to do a decent\n        amount of work to work around them.\n\n        1. Find all of the affected properties. For case 2, this is any\n           property whose type is a container type that contains this\n           scalar. (Possibly transitively.) For case 1, the container type\n           restriction is dropped.\n        2. Change the type of all offending properties to an equivalent type\n           that does not reference this scalar. This may require creating\n           new types. (See _undo_everything.)\n        3. Add the constraint.\n        4. Restore the type of all offending properties. If existing data\n           violates the new constraint, we will fail here. Delete any\n           temporarily created types. (See _redo_everything.)\n\n        Somewhat hackily, _undo_everything and _redo_everything\n        operate by creating new schema delta command objects, and\n        adapting and applying them. This is the most straightforward\n        way to perform the high-level operations needed here.\n\n        I've kept this code in pgsql/delta instead of trying to put in\n        schema/delta because it is pretty aggressively an irritating\n        pgsql implementation detail and because I didn't want it to\n        have to interact with ordering ever.\n\n        This function finds all of the relevant properties and returns\n        a list of them along with the appropriate replacement type.\n\n        In case 1, it also finds other referencing objects which need\n        to be deleted and then recreated.\n        \"\"\"\n\n        seen_props = set()\n        seen_other: set[so.Object] = set()\n\n        typ = self.scls\n        typs = [typ]\n        # Do a worklist driven search for properties that refer to this scalar\n        # through a collection type. We search backwards starting from\n        # referring collection types or from all refs, depending on\n        # composite_only.\n        scls_type = s_types.Collection if composite_only else None\n        wl = list(schema.get_referrers(typ, scls_type=scls_type))\n        while wl:\n            obj = wl.pop()\n            if isinstance(obj, s_props.Property):\n                seen_props.add(obj)\n            elif isinstance(obj, s_scalars.ScalarType) and not composite_only:\n                wl.extend(schema.get_referrers(obj))\n                seen_other.add(obj)\n                typs.append(obj)\n            elif isinstance(obj, s_types.Collection):\n                wl.extend(schema.get_referrers(obj))\n                seen_other.add(obj)\n            elif isinstance(obj, s_funcs.Parameter) and not composite_only:\n                wl.extend(schema.get_referrers(obj))\n                seen_other.add(obj)\n            elif isinstance(obj, s_funcs.Function) and not composite_only:\n                wl.extend(schema.get_referrers(obj))\n                seen_other.add(obj)\n            elif isinstance(obj, s_constr.Constraint) and not composite_only:\n                seen_other.add(obj)\n            elif isinstance(obj, s_indexes.Index) and not composite_only:\n                seen_other.add(obj)\n\n        if not seen_props and not seen_other:\n            return None\n\n        props = {}\n        if seen_props:\n            type_substs: dict[sn.Name, s_types.TypeShell[s_types.Type]] = {}\n            for typ in typs:\n                # Find a concrete ancestor to substitute in.\n                if typ.is_enum(schema):\n                    ancestor = schema.get(\n                        sn.QualName('std', 'str'), type=s_types.Type)\n                else:\n                    for ancestor in typ.get_ancestors(schema).objects(schema):\n                        if not ancestor.get_abstract(schema):\n                            break\n                    else:\n                        raise AssertionError(\n                            \"can't find concrete base for scalar\")\n                type_substs[typ.get_name(schema)] = ancestor.as_shell(schema)\n\n            props = {\n                prop:\n                s_utils.type_shell_multi_substitute(\n                    type_substs,\n                    not_none(prop.get_target(schema)).as_shell(schema),\n                    schema,\n                )\n                for prop in seen_props\n            }\n\n        objs = sd.sort_by_cross_refs(schema, seen_props | seen_other)\n\n        return objs, props\n\n    def _undo_everything(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        objs: tuple[so.Object, ...],\n        props: dict[s_props.Property, s_types.TypeShell],\n    ) -> s_schema.Schema:\n        \"\"\"Rewrite the type of everything that uses this scalar dangerously.\n\n        See _get_problematic_refs above for details.\n        \"\"\"\n\n        # First we need to strip out any default value that might reference\n        # one of the functions we are going to delete.\n        # We also create any new types, in this pass.\n        cmd = sd.DeltaRoot()\n        for prop, new_typ in props.items():\n            try:\n                cmd.add(new_typ.as_create_delta(schema))\n            except NotImplementedError as e:\n                if e.args == ('unsupported typeshell',):\n                    pass\n                else:\n                    raise\n\n            if prop.get_default(schema):\n                delta_alter, cmd_alter, _alter_context = prop.init_delta_branch(\n                    schema, context, cmdtype=sd.AlterObject)\n                cmd_alter.set_attribute_value('default', None)\n                cmd.add(delta_alter)\n\n        cmd.apply(schema, context)\n        acmd = CommandMeta.adapt(cmd)\n        schema = acmd.apply(schema, context)\n        self.pgops.update(acmd.get_subcommands())\n\n        # Now process all the objects in the appropriate order\n        for obj in objs:\n            if isinstance(obj, s_funcs.Function):\n                # Force function deletions at the SQL level without ever\n                # bothering to remove them from our schema.\n                fc = FunctionCommand()\n                variadic = obj.get_params(schema).find_variadic(schema)\n                self.pgops.add(\n                    dbops.DropFunction(\n                        name=fc.get_pgname(obj, schema),\n                        args=fc.compile_args(obj, schema),\n                        has_variadic=variadic is not None,\n                    )\n                )\n            elif isinstance(obj, s_constr.Constraint):\n                self.pgops.add(ConstraintCommand.delete_constraint(obj, schema))\n            elif isinstance(obj, s_indexes.Index):\n                self.pgops.add(DeleteIndex.delete_index(obj, schema, context))\n            elif isinstance(obj, s_types.Tuple):\n                self.pgops.add(dbops.DropCompositeType(\n                    name=common.get_backend_name(schema, obj, catenate=False),\n                ))\n            elif isinstance(obj, s_scalars.ScalarType):\n                self.pgops.add(DeleteScalarType.delete_scalar(obj, schema))\n            elif isinstance(obj, s_props.Property):\n                new_typ = props[obj]\n\n                delta_alter, cmd_alter, _alter_context = obj.init_delta_branch(\n                    schema, context, cmdtype=sd.AlterObject)\n                cmd_alter.set_attribute_value('target', new_typ)\n                cmd_alter.set_attribute_value('default', None)\n\n                delta_alter.apply(schema, context)\n                acmd2 = CommandMeta.adapt(delta_alter)\n                schema = acmd2.apply(schema, context)\n                self.pgops.add(acmd2)\n\n        return schema\n\n    def _redo_everything(\n        self,\n        schema: s_schema.Schema,\n        orig_schema: s_schema.Schema,\n        context: sd.CommandContext,\n        objs: tuple[so.Object, ...],\n        props: dict[s_props.Property, s_types.TypeShell],\n    ) -> s_schema.Schema:\n        \"\"\"Restore the type of everything that uses this scalar dangerously.\n\n        See _get_problematic_refs above for details.\n        \"\"\"\n\n        for obj in reversed(objs):\n            if isinstance(obj, s_funcs.Function):\n                # Super hackily recreate the functions\n                fc = CreateFunction(\n                    classname=obj.get_name(schema))  # type: ignore\n                for f in ('language', 'params', 'return_type'):\n                    fc.set_attribute_value(f, obj.get_field_value(schema, f))\n                self.pgops.update(fc.make_op(obj, schema, context))\n            elif isinstance(obj, s_constr.Constraint):\n                self.pgops.add(ConstraintCommand.create_constraint(\n                    self,\n                    obj,\n                    schema,\n                    context,\n                    create_triggers_if_needed=False,\n                ))\n            elif isinstance(obj, s_indexes.Index):\n                self.pgops.add(\n                    CreateIndex.create_index(obj, orig_schema, context))\n            elif isinstance(obj, s_types.Tuple):\n                self.pgops.add(CreateTuple.create_tuple(obj, orig_schema))\n            elif isinstance(obj, s_scalars.ScalarType):\n                self.pgops.add(\n                    CreateScalarType.create_scalar(\n                        obj, obj.get_default(schema), orig_schema, context\n                    )\n                )\n            elif isinstance(obj, s_props.Property):\n                new_typ = props[obj]\n\n                delta_alter, cmd_alter, _ = obj.init_delta_branch(\n                    schema, context, cmdtype=sd.AlterObject)\n                cmd_alter.set_attribute_value(\n                    'target', obj.get_target(orig_schema))\n\n                delta_alter.apply(schema, context)\n                acmd = CommandMeta.adapt(delta_alter)\n                schema = acmd.apply(schema, context)\n                self.pgops.add(acmd)\n\n        # Restore defaults and prune newly created types\n        cmd = sd.DeltaRoot()\n        for prop, new_typ in props.items():\n            rnew_typ = new_typ.resolve(schema)\n            if delete := rnew_typ.as_type_delete_if_unused(schema):\n                cmd.add_caused(delete)\n\n            delta_alter, cmd_alter, _ = prop.init_delta_branch(\n                schema, context, cmdtype=sd.AlterObject)\n            cmd_alter.set_attribute_value(\n                'default', prop.get_default(orig_schema))\n            cmd.add(delta_alter)\n\n        # do an apply of the schema-level command to force it to canonicalize,\n        # which prunes out duplicate deletions\n        #\n        # HACK: Clear out the context's stack so that\n        # context.canonical is false while doing this.\n        stack, context.stack = context.stack, []\n        cmd.apply(schema, context)\n        context.stack = stack\n\n        for sub in cmd.get_subcommands():\n            acmd2 = CommandMeta.adapt(sub)\n            schema = acmd2.apply(schema, context)\n            self.pgops.add(acmd2)\n\n        return schema\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        schema = super()._alter_begin(schema, context)\n        new_scalar = self.scls\n\n        has_create_constraint = bool(\n            list(self.get_subcommands(type=s_constr.CreateConstraint)))\n        has_rebase = bool(\n            list(self.get_subcommands(type=s_scalars.RebaseScalarType)))\n\n        old_enum_values: Sequence[str] = (\n            new_scalar.get_enum_values(orig_schema) or [])\n        new_enum_values: Sequence[str]\n\n        if has_rebase and old_enum_values:\n            # Ugly hack alert: we need to do this \"lookahead\" rebase\n            # apply to get the list of new enum values to decide\n            # whether special handling is needed _before_ the actual\n            # _alter_innards() takes place, because we are also handling\n            # domain constraints here.  TODO: a cleaner way to handle this\n            # would be to move this logic into actual subcomands\n            # (RebaseScalarType and CreateConstraint).\n            rebased_schema = super()._alter_innards(schema, context)\n            new_enum_values = new_scalar.get_enum_values(rebased_schema) or []\n        else:\n            new_enum_values = old_enum_values\n\n        # If values were deleted or reordered, we need to drop the enum\n        # and recreate it.\n        needs_recreate = (\n            old_enum_values != new_enum_values\n            and old_enum_values != new_enum_values[:len(old_enum_values)])\n\n        self.problematic_refs = None\n        if needs_recreate or has_create_constraint:\n            self.problematic_refs = self._get_problematic_refs(\n                schema, context, composite_only=not needs_recreate)\n            if self.problematic_refs:\n                objs, props = self.problematic_refs\n                schema = self._undo_everything(schema, context, objs, props)\n\n        if new_enum_values:\n            type_name = common.get_backend_name(\n                schema, new_scalar, catenate=False)\n\n            if needs_recreate:\n                self.pgops.add(\n                    DeleteScalarType.delete_scalar(new_scalar, orig_schema)\n                )\n                self.pgops.add(\n                    CreateScalarType.create_enum(\n                        new_scalar, new_enum_values, schema, context\n                    )\n                )\n\n            elif old_enum_values != new_enum_values:\n                old_idx = 0\n                old_enum_values = list(old_enum_values)\n                for v in new_enum_values:\n                    if old_idx >= len(old_enum_values):\n                        self.pgops.add(\n                            dbops.AlterEnumAddValue(\n                                type_name, v,\n                            )\n                        )\n                    elif v != old_enum_values[old_idx]:\n                        self.pgops.add(\n                            dbops.AlterEnumAddValue(\n                                type_name, v, before=old_enum_values[old_idx],\n                            )\n                        )\n                        old_enum_values.insert(old_idx, v)\n                    else:\n                        old_idx += 1\n\n        default_delta = self.get_resolved_attribute_value(\n            'default',\n            schema=schema,\n            context=context,\n        )\n        if default_delta:\n            if (default_delta is None or\n                    isinstance(default_delta, s_expr.Expression)):\n                new_default = None\n            else:\n                new_default = default_delta\n\n            domain_name = common.get_backend_name(\n                schema, new_scalar, catenate=False)\n            adad = dbops.AlterDomainAlterDefault(\n                name=domain_name, default=new_default)\n            self.pgops.add(adad)\n\n        return schema\n\n    def _alter_finalize(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._alter_finalize(schema, context)\n        if self.problematic_refs:\n            objs, props = self.problematic_refs\n            schema = self._redo_everything(\n                schema,\n                context.current().original_schema,\n                context,\n                objs,\n                props,\n            )\n\n        return schema\n\n\ndef drop_dependant_func_cache(pg_type: tuple[str, ...]) -> dbops.PLQuery:\n    if len(pg_type) == 1:\n        types_cte = f'''\n                    SELECT\n                        pt.oid AS oid\n                    FROM\n                        pg_type pt\n                    WHERE\n                        pt.typname = {ql(pg_type[0])}\n                        OR pt.typname = {ql('_' + pg_type[0])}\\\n        '''\n    else:\n        types_cte = f'''\n                    SELECT\n                        pt.oid AS oid\n                    FROM\n                        pg_type pt\n                        JOIN pg_namespace pn\n                            ON pt.typnamespace = pn.oid\n                    WHERE\n                        pn.nspname = {ql(pg_type[0])}\n                        AND (\n                            pt.typname = {ql(pg_type[1])}\n                            OR pt.typname = {ql('_' + pg_type[1])}\n                        )\\\n        '''\n    drop_func_cache_sql = textwrap.dedent(f'''\n        DECLARE\n            qc RECORD;\n        BEGIN\n            FOR qc IN\n                WITH\n                types AS ({types_cte}\n                ),\n                class AS (\n                    SELECT\n                        pc.oid AS oid\n                    FROM\n                        pg_class pc\n                        JOIN pg_namespace pn\n                            ON pc.relnamespace = pn.oid\n                    WHERE\n                        pn.nspname = 'pg_catalog'\n                        AND pc.relname = 'pg_type'\n                )\n                SELECT\n                    substring(p.proname FROM 6)::uuid AS key\n                FROM\n                    pg_proc p\n                    JOIN pg_depend d\n                        ON d.objid = p.oid\n                    JOIN types t\n                        ON d.refobjid = t.oid\n                    JOIN class c\n                        ON d.refclassid = c.oid\n                WHERE\n                    p.proname LIKE '__qh_%'\n            LOOP\n                PERFORM edgedb_VER.\"_evict_query_cache\"(qc.key);\n            END LOOP;\n        END;\n    ''')\n    return dbops.PLQuery(drop_func_cache_sql)\n\n\nclass DeleteScalarType(ScalarTypeMetaCommand,\n                       adapts=s_scalars.DeleteScalarType):\n    @classmethod\n    def delete_scalar(\n        cls, scalar: s_scalars.ScalarType, orig_schema: s_schema.Schema\n    ) -> dbops.Command:\n        ops = dbops.CommandGroup()\n\n        # The custom scalar types are sometimes included in the function\n        # signatures of query cache functions under QueryCacheMode.PgFunc.\n        # We need to find such functions through pg_depend and evict the cache\n        # before dropping the custom scalar type.\n        pg_type = types.pg_type_from_scalar(orig_schema, scalar)\n        ops.add_command(drop_dependant_func_cache(pg_type))\n\n        old_domain_name = common.get_backend_name(\n            orig_schema, scalar, catenate=False)\n        cond: dbops.Condition\n        if scalar.is_concrete_enum(orig_schema):\n            old_enum_name = old_domain_name\n            cond = dbops.EnumExists(old_enum_name)\n\n            cast_func_name = common.get_backend_name(\n                orig_schema, scalar, False, aspect=\"enum-cast-from-str\"\n            )\n            cast_func = dbops.DropFunction(\n                name=cast_func_name,\n                args=[(\"value\", (\"anyelement\",))],\n                conditions=[cond],\n            )\n            ops.add_command(cast_func)\n\n            uncast_func_name = common.get_backend_name(\n                orig_schema, scalar, False, aspect=\"enum-cast-into-str\"\n            )\n            uncast_func = dbops.DropFunction(\n                name=uncast_func_name,\n                args=[(\"value\", (\"anyelement\",))],\n                conditions=[cond],\n            )\n            ops.add_command(uncast_func)\n\n            enum = dbops.DropEnum(name=old_enum_name, conditions=[cond])\n            ops.add_command(enum)\n\n        else:\n            cond = dbops.DomainExists(old_domain_name)\n            domain = dbops.DropDomain(name=old_domain_name, conditions=[cond])\n            ops.add_command(domain)\n\n        return ops\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        schema = super().apply(schema, context)\n        scalar = self.scls\n\n        link = None\n        if context:\n            link = context.get(s_links.LinkCommandContext)\n\n        if link:\n            assert isinstance(link.op, MetaCommand)\n            ops = link.op.pgops\n        else:\n            ops = self.pgops\n\n        ops.add(self.delete_scalar(scalar, orig_schema))\n\n        if self.is_sequence(orig_schema, scalar):\n            seq_name = common.get_backend_name(\n                orig_schema, scalar, catenate=False, aspect='sequence')\n            self.pgops.add(dbops.DropSequence(name=seq_name))\n\n        return schema\n\n\nif TYPE_CHECKING:\n    # In pgsql/delta, a \"composite object\" is anything that can have a table.\n    # That is, an object type, a link, or a property.\n    # We represent it as Source | Pointer, since many call sites are generic\n    # over one of those things.\n    CompositeObject = s_sources.Source | s_pointers.Pointer\n\n    PostCommand = (\n        dbops.Command\n        | Callable[\n            [s_schema.Schema, sd.CommandContext],\n            Optional[dbops.Command]\n        ]\n    )\n\n\nclass CompositeMetaCommand(MetaCommand):\n\n    constraint_trigger_updates: set[uuid.UUID]\n\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        self.table_name = None\n        self._multicommands = {}\n        self.update_search_indexes = None\n        self.constraint_trigger_updates = set()\n\n    def schedule_trampoline(self, obj, schema, context):\n        delta = context.get(sd.DeltaRootContext).op\n        create_trampolines = delta.create_trampolines\n        create_trampolines.table_targets.append(obj)\n\n    def _get_multicommand(\n        self,\n        context,\n        cmdtype,\n        object_name,\n        *,\n        force_new=False,\n        manual=False,\n        cmdkwargs=None,\n    ):\n        if cmdkwargs is None:\n            cmdkwargs = {}\n        key = (object_name, frozenset(cmdkwargs.items()))\n\n        try:\n            typecommands = self._multicommands[cmdtype]\n        except KeyError:\n            typecommands = self._multicommands[cmdtype] = {}\n\n        commands = typecommands.get(key)\n\n        if commands is None or force_new or manual:\n            command = cmdtype(object_name, **cmdkwargs)\n\n            if not manual:\n                try:\n                    commands = typecommands[key]\n                except KeyError:\n                    commands = typecommands[key] = []\n\n                commands.append(command)\n        else:\n            command = commands[-1]\n\n        return command\n\n    def _attach_multicommand(self, context, cmdtype):\n        try:\n            typecommands = self._multicommands[cmdtype]\n        except KeyError:\n            return\n        else:\n            commands = list(\n                itertools.chain.from_iterable(typecommands.values()))\n\n            if commands:\n                self.pgops.update(commands)\n\n    def get_alter_table(\n        self,\n        schema,\n        context,\n        force_new=False,\n        contained=False,\n        manual=False,\n        table_name=None,\n    ):\n\n        tabname = table_name if table_name else self.table_name\n\n        # XXX: should this be arranged to always have been done?\n        if not tabname:\n            ctx = context.get(self.__class__)\n            assert ctx\n            tabname = self._get_table_name(ctx.scls, schema)\n            if table_name is None:\n                self.table_name = tabname\n\n        return self._get_multicommand(\n            context, dbops.AlterTable, tabname,\n            force_new=force_new, manual=manual,\n            cmdkwargs={'contained': contained})\n\n    def attach_alter_table(self, context):\n        self._attach_multicommand(context, dbops.AlterTable)\n\n    @staticmethod\n    def _get_table_name(obj, schema) -> tuple[str, str]:\n        is_internal_view = irtyputils.is_cfg_view(obj, schema)\n        aspect = 'dummy' if is_internal_view else None\n        return common.get_backend_name(\n            schema, obj, catenate=False, aspect=aspect)\n\n    @classmethod\n    def _refresh_fake_cfg_view_cmd(\n        cls,\n        obj: CompositeObject,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> dbops.Command:\n        if not types.has_table(obj, schema):\n            return dbops.CommandGroup()\n        # Objects in sys and cfg are actually implemented by views\n        # that are defined in metaschema. The metaschema scripts run\n        # *after* the schema is instantiated, though, and we need to\n        # populate something *now* that can go into inhviews.\n        #\n        # The way we do this is by creating an actual concrete table\n        # with the suffix \"_dummy\" and then creating a view with the\n        # expected table name that simply `select *`s from the dummy\n        # table. Pointer creation on the type gets routed to the dummy\n        # table, so it has the right columns. Since the view `select\n        # *`s from the table, it also has the right columns, and can\n        # go into all of the inheritance views without any trouble.\n        #\n        # We refresh the fake config view before creating/updating\n        # inhviews associated with the object, since that corresponds\n        # with when it actually needs to happen by.\n        #\n        # Then, when we run the metaschema script, it simply swaps out\n        # this hacky view for the real one and everything works out fine.\n        orig_name = common.get_backend_name(\n            schema, obj, catenate=False,\n        )\n        dummy_name = cls._get_table_name(obj, schema)\n        query = f'''\n            SELECT * FROM {q(*dummy_name)}\n        '''\n        view = dbops.View(name=orig_name, query=query)\n        return dbops.CreateView(view, or_replace=True)\n\n    def update_if_cfg_view(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        obj: CompositeObject,\n    ):\n        if irtyputils.is_cfg_view(obj, schema) and not context.in_deletion():\n            self.pgops.add(\n                self._refresh_fake_cfg_view_cmd(obj, schema, context))\n\n    def update_source_if_cfg_view(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        ptr: s_pointers.Pointer,\n    ) -> None:\n        if src := ptr.get_source(schema):\n            assert isinstance(src, s_sources.Source)\n            self.update_if_cfg_view(schema, context, src)\n\n    @classmethod\n    def get_source_and_pointer_ctx(cls, schema, context):\n        if context:\n            objtype = context.get(s_objtypes.ObjectTypeCommandContext)\n            link = context.get(s_links.LinkCommandContext)\n        else:\n            objtype = link = None\n\n        if objtype:\n            source, pointer = objtype, link\n        elif link:\n            property = context.get(s_props.PropertyCommandContext)\n            source, pointer = link, property\n        else:\n            source = pointer = None\n\n        return source, pointer\n\n    @classmethod\n    def create_type_trampoline(\n        cls,\n        schema: s_schema.Schema,\n        obj: CompositeObject,\n        aspect: str='table',\n    ) -> Optional[trampoline.TrampolineView]:\n        versioned_name = common.get_backend_name(\n            schema, obj, aspect=aspect, catenate=False\n        )\n        trampolined_name = common.get_backend_name(\n            schema, obj, aspect=aspect, catenate=False, versioned=False\n        )\n        if versioned_name != trampolined_name:\n            return trampoline.make_table_trampoline(versioned_name)\n        else:\n            return None\n\n    def apply_constraint_trigger_updates(\n        self,\n        schema: s_schema.Schema,\n    ) -> None:\n        for constraint_id in self.constraint_trigger_updates:\n            constraint = (\n                schema.get_by_id(constraint_id, type=s_constr.Constraint)\n                if schema.has_object(constraint_id) else\n                None\n            )\n            if not constraint:\n                continue\n\n            if not ConstraintCommand.constraint_is_effective(\n                schema, constraint\n            ):\n                continue\n\n            subject = constraint.get_subject(schema)\n            bconstr = schemamech.compile_constraint(\n                subject, constraint, schema, None\n            )\n\n            self.pgops.add(bconstr.update_trigger_ops())\n\n\nclass IndexCommand(MetaCommand):\n    pass\n\n\ndef get_index_compile_options(\n    index: s_indexes.Index,\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    schema_object_context: Optional[type[so.Object_T]],\n) -> qlcompiler.CompilerOptions:\n    subject = index.get_subject(schema)\n    assert isinstance(subject, (s_types.Type, s_pointers.Pointer))\n\n    return qlcompiler.CompilerOptions(\n        modaliases=modaliases,\n        schema_object_context=schema_object_context,\n        anchors={'__subject__': subject},\n        path_prefix_anchor='__subject__',\n        singletons=[subject],\n        apply_query_rewrites=False,\n    )\n\n\ndef get_reindex_sql(\n    obj: s_objtypes.ObjectType,\n    restore_desc: sertypes.ShapeDesc,\n    schema: s_schema.Schema,\n) -> Optional[str]:\n    \"\"\"Generate SQL statement that repopulates the index after a restore.\n\n    Currently this only applies to FTS indexes, and it only fires if\n    __fts_document__ is not in the dump (which it wasn't prior to 5.0).\n\n    AI index columns might also be missing if they were made with a\n    5.0rc1 dump, but the indexer will pick them up without our\n    intervention.\n    \"\"\"\n\n    (fts_index, _) = s_indexes.get_effective_object_index(\n        schema, obj, sn.QualName(\"std::fts\", \"index\")\n    )\n    if fts_index and '__fts_document__' not in restore_desc.fields:\n        options = get_index_compile_options(fts_index, schema, {}, None)\n        cmd = deltafts.update_fts_document(fts_index, options, schema)\n        return cmd.code()\n\n    return None\n\n\nclass CreateIndex(IndexCommand, adapts=s_indexes.CreateIndex):\n    @classmethod\n    def create_index(\n        cls,\n        index: s_indexes.Index,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> dbops.Command:\n        from .compiler import astutils\n\n        options = get_index_compile_options(\n            index, schema, context.modaliases, cls.get_schema_metaclass()\n        )\n\n        index_sexpr: Optional[s_expr.Expression] = index.get_expr(schema)\n        assert index_sexpr\n        index_expr = index_sexpr.ensure_compiled(\n            schema=schema,\n            options=options,\n            context=None,\n        )\n        ir = index_expr.irast\n\n        except_expr = index.get_except_expr(schema)\n        if except_expr:\n            except_expr = except_expr.ensure_compiled(\n                schema=schema,\n                options=options,\n                context=None,\n            )\n            assert except_expr.irast\n            except_res = compiler.compile_ir_to_sql_tree(\n                except_expr.irast.expr, singleton_mode=True)\n            except_src = codegen.generate_source(except_res.ast)\n            predicate_src = f'({except_src}) is not true'\n        else:\n            predicate_src = None\n\n        sql_kwarg_exprs = dict()\n        # Get the name of the root index that this index implements\n        orig_name: sn.Name = sn.shortname_from_fullname(index.get_name(schema))\n        root_name: sn.Name\n        root_code: str | None\n        if orig_name == s_indexes.DEFAULT_INDEX:\n            root_name = orig_name\n            root_code = DEFAULT_INDEX_CODE\n        else:\n            root = index.get_root(schema)\n            root_name = root.get_name(schema)\n            root_code = root.get_code(schema)\n\n            kwargs = index.get_concrete_kwargs(schema)\n            for name, expr in kwargs.items():\n                kw_ir = expr.assert_compiled().irast\n                kw_sql_res = compiler.compile_ir_to_sql_tree(\n                    kw_ir.expr, singleton_mode=True)\n                kw_sql_tree = kw_sql_res.ast\n                # HACK: the compiled SQL is expected to have some unnecessary\n                # casts, strip them as they mess with the requirement that\n                # index expressions are IMMUTABLE (also indexes expect the\n                # usage of literals and will do their own implicit casts).\n                if isinstance(kw_sql_tree, pgast.TypeCast):\n                    kw_sql_tree = kw_sql_tree.arg\n                sql = codegen.generate_source(kw_sql_tree)\n                sql_kwarg_exprs[name] = sql\n\n        # FTS\n        if root_name == sn.QualName('std::fts', 'index'):\n            return deltafts.create_fts_index(\n                index,\n                ir.expr,\n                predicate_src,\n                sql_kwarg_exprs,\n                options,\n                schema,\n                context,\n            )\n        elif root_name == sn.QualName('ext::ai', 'index'):\n            return delta_ext_ai.create_ext_ai_index(\n                index,\n                predicate_src,\n                sql_kwarg_exprs,\n                options,\n                schema,\n                context,\n            )\n\n        if root_code is None:\n            raise AssertionError(f'index {root_name} is missing the code')\n\n        sql_res = compiler.compile_ir_to_sql_tree(ir.expr, singleton_mode=True)\n        exprs = astutils.maybe_unpack_row(sql_res.ast)\n\n        if len(exprs) == 0:\n            raise errors.SchemaDefinitionError(\n                f'cannot index empty tuples using {root_name}'\n            )\n\n        subject = index.get_subject(schema)\n        assert subject\n        table_name = common.get_backend_name(schema, subject, catenate=False)\n\n        module_name = index.get_name(schema).module\n        index_name = common.get_index_backend_name(\n            index.id, module_name, catenate=False)\n\n        sql_exprs = [codegen.generate_source(e) for e in exprs]\n        pg_index = dbops.Index(\n            name=index_name[1],\n            table_name=table_name,  # type: ignore\n            exprs=sql_exprs,\n            unique=False,\n            inherit=True,\n            predicate=predicate_src,\n            metadata={\n                'schemaname': str(index.get_name(schema)),\n                'code': root_code,\n                'kwargs': sql_kwarg_exprs,\n            }\n        )\n        concurrently = index.get_build_concurrently(schema)\n        return dbops.CreateIndex(\n            pg_index,\n            concurrently=concurrently,\n            builtin_conditional=concurrently,\n        )\n\n    # N.B: This is in _create_finalize instead of _create_innards\n    # because when trying to do repair_schema() to repair issue #9033,\n    # there will be generated CreateIndexes where the annotation\n    # creation is in `caused`, not regular subcommands...\n    def _create_finalize(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._create_finalize(schema, context)\n        index = self.scls\n\n        if index.get_abstract(schema):\n            # Don't do anything for abstract indexes\n            return schema\n\n        if index.get_build_concurrently(schema):\n            return schema\n\n        with errors.ensure_span(self.span):\n            self.pgops.add(self.create_index(index, schema, context))\n\n        return schema\n\n\n# mypy claims that _cmd_from_ast in IndexCommand is incompatible with\n# that in RenameObject.\nclass RenameIndex(IndexCommand, adapts=s_indexes.RenameIndex):  # type: ignore\n    pass\n\n\nclass AlterIndexOwned(IndexCommand, adapts=s_indexes.AlterIndexOwned):\n    pass\n\n\nclass AlterIndex(IndexCommand, adapts=s_indexes.AlterIndex):\n    pass\n\n\nclass DeleteIndex(IndexCommand, adapts=s_indexes.DeleteIndex):\n    @classmethod\n    def delete_index(\n        cls,\n        index: s_indexes.Index,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ):\n        subject = index.get_subject(schema)\n        assert subject\n        table_name = common.get_backend_name(\n            schema, subject, catenate=False)\n        module_name = index.get_name(schema).module\n        orig_idx_name = common.get_index_backend_name(\n            index.id, module_name, catenate=False)\n        pg_index = dbops.Index(\n            name=orig_idx_name[1], table_name=table_name, inherit=True)\n        index_exists = dbops.IndexExists(\n            (table_name[0], pg_index.name_in_catalog)\n        )\n        return dbops.DropIndex(pg_index, conditions=(index_exists,))\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        schema = super().apply(schema, context)\n        index = self.scls\n\n        if index.get_abstract(orig_schema):\n            # Don't do anything for abstract indexes\n            return schema\n\n        source: Optional[\n            sd.CommandContextToken[s_sources.SourceCommand[s_sources.Source]]]\n        # XXX: I think to make these work, the type vars in the Commands\n        # would need to be covariant.\n        source = context.get(s_links.LinkCommandContext)  # type: ignore\n        if not source:\n            source = context.get(\n                s_objtypes.ObjectTypeCommandContext)   # type: ignore\n            assert source\n\n        if not isinstance(source.op, sd.DeleteObject):\n            # We should not drop indexes when the host is being dropped since\n            # the indexes are dropped automatically in this case.\n            drop_index = self.delete_index(index, orig_schema, context)\n        else:\n            drop_index = dbops.NoOpCommand()\n\n        # FTS\n        if s_indexes.is_fts_index(orig_schema, index):\n            # compile commands for index drop\n            options = get_index_compile_options(\n                index,\n                orig_schema,\n                context.modaliases,\n                self.get_schema_metaclass()\n            )\n            self.pgops.add(deltafts.delete_fts_index(\n                index, drop_index, options, schema, orig_schema, context\n            ))\n\n        # ext::ai::index\n        elif s_indexes.is_ext_ai_index(orig_schema, index):\n            # compile commands for index drop\n            options = get_index_compile_options(\n                index,\n                orig_schema,\n                context.modaliases,\n                self.get_schema_metaclass()\n            )\n            drop_support_ops, drop_col_ops = delta_ext_ai.delete_ext_ai_index(\n                index, drop_index, options, schema, orig_schema, context\n            )\n\n            # Even though the object type table is getting dropped, we have\n            # to drop the trigger and its function\n            self.pgops.add(drop_support_ops)\n            self.pgops.add(drop_col_ops)\n        else:\n            self.pgops.add(drop_index)\n\n        return schema\n\n\nclass RebaseIndex(IndexCommand, adapts=s_indexes.RebaseIndex):\n    pass\n\n\nclass IndexMatchCommand(MetaCommand):\n    pass\n\n\nclass CreateIndexMatch(IndexMatchCommand, adapts=s_indexes.CreateIndexMatch):\n    # Index match is handled by the compiler and does not need explicit\n    # representation in the backend.\n    pass\n\n\nclass DeleteIndexMatch(IndexMatchCommand, adapts=s_indexes.DeleteIndexMatch):\n    pass\n\n\nclass CreateUnionType(\n    MetaCommand,\n    adapts=s_types.CreateUnionType,\n    metaclass=CommandMeta,\n):\n    pass\n\n\nclass CreateIntersectionType(\n    MetaCommand,\n    adapts=s_types.CreateIntersectionType,\n    metaclass=CommandMeta,\n):\n    pass\n\n\nclass ObjectTypeMetaCommand(AliasCapableMetaCommand, CompositeMetaCommand):\n    def schedule_endpoint_delete_action_update(self, obj, schema, context):\n        endpoint_delete_actions = context.get(\n            sd.DeltaRootContext).op.update_endpoint_delete_actions\n        changed_targets = endpoint_delete_actions.changed_targets\n        changed_targets.add((self, obj))\n\n    def _fixup_configs(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        orig_schema = context.current().original_schema\n        eff_schema = (\n            orig_schema if isinstance(self, sd.DeleteObject) else schema)\n        scls: s_objtypes.ObjectType = self.scls  # type: ignore\n\n        # If we are updating a config object that is *not* in cfg::\n        # (that is, an extension config), we need to update the config\n        # views and specs. We *don't* do that for standard library\n        # configs, since those need to be created after the standard\n        # schema is in place.\n        if not (\n            irtyputils.is_cfg_view(scls, eff_schema)\n            and scls.get_name(eff_schema).module not in irtyputils.VIEW_MODULES\n        ):\n            return\n\n        from edb.pgsql import metaschema\n\n        new_local_spec = config.load_spec_from_schema(\n            schema,\n            only_exts=True,\n            # suppress validation because we might be in an intermediate state\n            validate=False,\n        )\n        spec_json = config.spec_to_json(new_local_spec)\n        self.pgops.add(dbops.Query(textwrap.dedent(trampoline.fixup_query(f'''\\\n            UPDATE\n                edgedbinstdata_VER.instdata\n            SET\n                json = {ql(spec_json)}\n            WHERE\n                key = 'configspec_ext';\n        '''))))\n\n        for sub in self.get_subcommands(type=s_pointers.DeletePointer):\n            if types.has_table(sub.scls, orig_schema):\n                self.pgops.add(dbops.DropView(common.get_backend_name(\n                    orig_schema, sub.scls, catenate=False)))\n\n        if isinstance(self, sd.DeleteObject):\n            self.pgops.add(dbops.DropView(common.get_backend_name(\n                eff_schema, scls, catenate=False)))\n        elif isinstance(self, sd.CreateObject):\n            views = metaschema.get_config_type_views(\n                eff_schema, scls, scope=None)\n            self.pgops.update(views)\n        # FIXME: ALTER doesn't work in meaningful ways. We'll maybe\n        # need to fix that when we have patching configs.\n\n\nclass CreateObjectType(\n    ObjectTypeMetaCommand, adapts=s_objtypes.CreateObjectType\n):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n\n        objtype = self.scls\n        if objtype.is_compound_type(schema) or objtype.get_is_derived(schema):\n            return schema\n\n        self.attach_alter_table(context)\n\n        if self.update_search_indexes:\n            schema = self.update_search_indexes.apply(schema, context)\n            self.pgops.add(self.update_search_indexes)\n\n        self.schedule_endpoint_delete_action_update(self.scls, schema, context)\n        self.schedule_trampoline(self.scls, schema, context)\n\n        self._fixup_configs(schema, context)\n\n        return schema\n\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._create_begin(schema, context)\n        objtype = self.scls\n        if objtype.is_compound_type(schema) or objtype.get_is_derived(schema):\n            return schema\n\n        new_table_name = self._get_table_name(self.scls, schema)\n\n        self.table_name = new_table_name\n        columns: list[dbops.Column] = []\n\n        objtype_table = dbops.Table(name=new_table_name, columns=columns)\n        self.pgops.add(dbops.CreateTable(table=objtype_table))\n        self.pgops.add(dbops.Comment(\n            object=objtype_table,\n            text=str(objtype.get_verbosename(schema)),\n        ))\n        # Don't update ancestors yet: no pointers have been added to\n        # the type yet, so this type won't actually be added to any\n        # ancestor views. We'll fix up the ancestors in\n        # _create_finalize.\n        self.update_if_cfg_view(schema, context, objtype)\n        return schema\n\n    def _create_finalize(self, schema, context):\n        schema = super()._create_finalize(schema, context)\n        self.apply_constraint_trigger_updates(schema)\n        return schema\n\n\nclass RenameObjectType(\n    ObjectTypeMetaCommand,\n    adapts=s_objtypes.RenameObjectType,\n):\n    pass\n\n\nclass RebaseObjectType(\n    ObjectTypeMetaCommand, adapts=s_objtypes.RebaseObjectType\n):\n    def _alter_innards(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        if types.has_table(self.scls, schema):\n            self.update_if_cfg_view(schema, context, self.scls)\n\n        schema = super()._alter_innards(schema, context)\n        self.schedule_endpoint_delete_action_update(self.scls, schema, context)\n\n        return schema\n\n\nclass AlterObjectType(ObjectTypeMetaCommand, adapts=s_objtypes.AlterObjectType):\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._alter_begin(schema, context)\n        # We want to set this name up early, so children operations see it\n        self.table_name = self._get_table_name(self.scls, schema)\n        return schema\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        schema = super().apply(schema, context=context)\n        objtype = self.scls\n\n        self.apply_constraint_trigger_updates(schema)\n\n        self._maybe_do_abstract_test(orig_schema, schema, context)\n\n        if types.has_table(objtype, schema):\n            self.attach_alter_table(context)\n\n            if self.update_search_indexes:\n                schema = self.update_search_indexes.apply(schema, context)\n                self.pgops.add(self.update_search_indexes)\n\n        self._fixup_configs(schema, context)\n\n        return schema\n\n    def _maybe_do_abstract_test(\n        self,\n        orig_schema: s_schema.Schema,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        orig_abstract = self.scls.get_abstract(orig_schema)\n        new_abstract = self.scls.get_abstract(schema)\n        if orig_abstract or not new_abstract:\n            return\n\n        table = q(*common.get_backend_name(\n            schema,\n            self.scls,\n            catenate=False,\n        ))\n\n        vn = self.scls.get_verbosename(schema)\n        check_qry = textwrap.dedent(f'''\\\n            SELECT\n                edgedb_VER.raise(\n                    NULL::text,\n                    'cardinality_violation',\n                    msg => {common.quote_literal(\n                            f\"may not make non-empty {vn} abstract\")},\n                    \"constraint\" => 'set abstract'\n                )\n            FROM {table}\n            INTO _dummy_text;\n        ''')\n\n        self.pgops.add(dbops.Query(check_qry))\n\n\nclass DeleteObjectType(\n    ObjectTypeMetaCommand, adapts=s_objtypes.DeleteObjectType\n):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        self.scls = objtype = schema.get(\n            self.classname, type=s_objtypes.ObjectType)\n\n        old_table_name = self._get_table_name(self.scls, schema)\n\n        orig_schema = schema\n        schema = super().apply(schema, context)\n\n        self.apply_constraint_trigger_updates(schema)\n\n        if types.has_table(objtype, orig_schema):\n            self.attach_alter_table(context)\n            self.pgops.add(dbops.DropTable(name=old_table_name))\n\n        self._fixup_configs(schema, context)\n\n        return schema\n\n\nclass SchedulePointerCardinalityUpdate(MetaCommand):\n    pass\n\n\nclass CancelPointerCardinalityUpdate(MetaCommand):\n    pass\n\n\nclass PointerMetaCommand[Pointer_T: s_pointers.Pointer](\n    CompositeMetaCommand,\n    s_pointers.PointerCommand[Pointer_T],\n):\n    def get_host(self, schema, context):\n        if context:\n            link = context.get(s_links.LinkCommandContext)\n            if link and isinstance(self, s_props.PropertyCommand):\n                return link\n            objtype = context.get(s_objtypes.ObjectTypeCommandContext)\n            if objtype:\n                return objtype\n\n    def is_sequence_ptr(self, ptr, schema):\n        return bool(\n            (tgt := ptr.get_target(schema))\n            and tgt.issubclass(schema, schema.get('std::sequence'))\n        )\n\n    def get_pointer_default(self, ptr, schema, context):\n        if ptr.is_pure_computable(schema):\n            return None\n\n        # Skip id, because it shouldn't ever matter for performance\n        # and because it wants to use the trampoline function, which\n        # might not exist yet.\n        if ptr.is_id_pointer(schema):\n            return None\n        if context.stdmode:\n            return None\n\n        # We only *need* to use postgres defaults for link properties\n        # and sequence values (since we always explicitly inject it in\n        # INSERTs anyway), but we *want* to use it whenever we can,\n        # since it is much faster than explicitly populating the\n        # column.\n        default = ptr.get_default(schema)\n        default_value = None\n\n        if default is not None:\n            default_value = schemamech.ptr_default_to_col_default(\n                schema, ptr, default)\n        elif self.is_sequence_ptr(ptr, schema):\n            # TODO: replace this with a generic scalar type default\n            #       using std::nextval().\n            seq_name = common.quote_literal(\n                common.get_backend_name(\n                    schema, ptr.get_target(schema), aspect='sequence'))\n            default_value = f'nextval({seq_name}::regclass)'\n\n        return default_value\n\n    @classmethod\n    def get_columns(\n        cls, pointer, schema, default=None, sets_required=False\n    ) -> list[dbops.Column]:\n        ptr_stor_info = types.get_pointer_storage_info(pointer, schema=schema)\n        col_type = common.quote_type(tuple(ptr_stor_info.column_type))\n\n        return [\n            dbops.Column(\n                name=ptr_stor_info.column_name,\n                type=col_type,\n                required=(\n                    (\n                        pointer.get_required(schema)\n                        and not pointer.is_pure_computable(schema)\n                        and not sets_required\n                        and not (pointer.get_default(schema) and not default)\n                    ) or (\n                        ptr_stor_info.table_type == 'link'\n                        and not pointer.is_link_property(schema)\n                    )\n                ),\n                default=default,\n                comment=str(pointer.get_shortname(schema)),\n            ),\n        ]\n\n    def create_table(self, ptr, schema, context):\n        if types.has_table(ptr, schema):\n            c = self._create_table(ptr, schema, context, conditional=True)\n            self.pgops.add(c)\n            self.update_if_cfg_view(schema, context, ptr)\n            return True\n        else:\n            return False\n\n    def _alter_pointer_cardinality(\n        self,\n        schema: s_schema.Schema,\n        orig_schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        assert isinstance(self, s_pointers.AlterPointerUpperCardinality)\n\n        ptr = self.scls\n        ptr_stor_info = types.get_pointer_storage_info(ptr, schema=schema)\n        old_ptr_stor_info = types.get_pointer_storage_info(\n            ptr, schema=orig_schema)\n        ptr_table = ptr_stor_info.table_type == 'link'\n        is_lprop = ptr.is_link_property(schema)\n        is_multi = ptr_table and not is_lprop\n        is_required = ptr.get_required(schema)\n        is_scalar = ptr.is_property()\n\n        ref_ctx = self.get_referrer_context_or_die(context)\n        ref_op = ref_ctx.op\n\n        source_op: sd.ObjectCommand\n        if is_multi:\n            if isinstance(self, sd.AlterObjectFragment):\n                source_op = self.get_parent_op(context)\n            else:\n                source_op = self\n        else:\n            source_op = ref_op\n        assert isinstance(source_op, CompositeMetaCommand)\n\n        # Ignore cardinality changes resulting from the creation of\n        # an overloaded pointer as there is no data yet.\n        if isinstance(source_op, sd.CreateObject):\n            return\n\n        # If the pointer has any constraints, drop them now. We'll\n        # create them again at the end.\n        # N.B: Since the pointer is either starting or ending as multi,\n        # it can't have any object constraints referencing it.\n        # TODO?: Maybe we should handle the constraint by generating\n        # an alter in the front-end and running _alter_innards in the\n        # middle of this function. (After creations, before deletions.)\n        for constr in ptr.get_constraints(schema).objects(schema):\n            self.pgops.add(ConstraintCommand.delete_constraint(\n                constr, orig_schema\n            ))\n\n        assert ptr_stor_info.table_name\n        tab = q(*ptr_stor_info.table_name)\n        target_col = ptr_stor_info.column_name\n        source = ptr.get_source(orig_schema)\n        assert source\n        src_tab = q(*common.get_backend_name(\n            orig_schema,\n            source,\n            catenate=False,\n        ))\n\n        # initial extern relvar (see docs of _compile_conversion_expr)\n        source_rel = textwrap.dedent(f'''\\\n            SELECT * FROM {src_tab}\n        ''')\n        source_rel_alias = f'source_{uuidgen.uuid1mc()}'\n\n        if self.conv_expr is not None:\n            (conv_expr_ctes, _, _) = self._compile_conversion_expr(\n                ptr,\n                self.conv_expr,\n                source_rel_alias,\n                schema=schema,\n                orig_schema=orig_schema,\n                context=context,\n                target_as_singleton=False,\n                check_non_null=is_required and not is_multi\n            )\n        else:\n            if not is_multi:\n                raise AssertionError(\n                    'explicit conversion expression was expected'\n                    ' for multi->single transition'\n                )\n\n            # single -> multi\n            conv_expr_ctes = textwrap.dedent(f'''\\\n                _conv_rel(val, id) AS (\n                    SELECT {qi(old_ptr_stor_info.column_name)}, id\n                    FROM {qi(source_rel_alias)}\n                )\n            ''')\n\n        if not is_multi:\n            # Moving from pointer table to source table.\n            cols = self.get_columns(ptr, schema)\n\n            # create columns\n            alter_table = source_op.get_alter_table(\n                schema, context, manual=True)\n            cols_required: list[dbops.Column] = []\n            for col in cols:\n                cond = dbops.ColumnExists(\n                    ptr_stor_info.table_name,\n                    column_name=col.name,\n                )\n                if col.required:\n                    cols_required.append(copy(col))\n\n                col.required = False\n                op = (dbops.AlterTableAddColumn(col), None, (cond, ))\n                alter_table.add_operation(op)\n\n            self.pgops.add(alter_table)\n\n            update_qry = textwrap.dedent(f'''\\\n                WITH\n                \"{source_rel_alias}\" AS ({source_rel}),\n                {conv_expr_ctes}\n                UPDATE {tab} AS _update\n                SET {qi(target_col)} = _conv_rel.val\n                FROM _conv_rel WHERE _update.id = _conv_rel.id\n            ''')\n            self.pgops.add(dbops.Query(update_qry))\n\n            # set NOT NULL\n            if cols_required:\n                alter_table = source_op.get_alter_table(\n                    schema, context, manual=True\n                )\n                for col in cols_required:\n                    op2 = dbops.AlterTableAlterColumnNull(col.name, False)\n                    alter_table.add_operation(op2)\n                self.pgops.add(alter_table)\n\n            # A link might still own a table if it has properties.\n            if not types.has_table(ptr, schema):\n                otabname = common.get_backend_name(\n                    orig_schema, ptr, catenate=False)\n                condition = dbops.TableExists(name=otabname)\n                dt = dbops.DropTable(name=otabname, conditions=[condition])\n                self.pgops.add(dt)\n\n            self.update_source_if_cfg_view(\n                schema, context, ptr,\n            )\n        else:\n            # Moving from source table to pointer table.\n            self.create_table(ptr, schema, context)\n            source = ptr.get_source(orig_schema)\n            assert source\n            src_tab = q(*common.get_backend_name(\n                orig_schema,\n                source,\n                catenate=False,\n            ))\n\n            update_qry = textwrap.dedent(f'''\\\n                WITH\n                \"{source_rel_alias}\" AS ({source_rel}),\n                {conv_expr_ctes}\n                INSERT INTO {tab} (source, target)\n                (SELECT id, val FROM _conv_rel WHERE _conv_rel.val IS NOT NULL)\n            ''')\n\n            if not is_scalar:\n                update_qry += 'ON CONFLICT (source, target) DO NOTHING'\n\n            self.pgops.add(dbops.Query(update_qry))\n\n            assert isinstance(ref_op.scls, s_sources.Source)\n            self.update_if_cfg_view(schema, context, ref_op.scls)\n\n            ref_op = self.get_referrer_context_or_die(context).op\n            assert isinstance(ref_op, CompositeMetaCommand)\n            alter_table = ref_op.get_alter_table(\n                schema, context, manual=True)\n            col = dbops.Column(\n                name=old_ptr_stor_info.column_name,\n                type=common.qname(*old_ptr_stor_info.column_type),\n            )\n            alter_table.add_operation(dbops.AlterTableDropColumn(col))\n            self.pgops.add(alter_table)\n\n        for constr in ptr.get_constraints(schema).objects(schema):\n            self.pgops.add(ConstraintCommand.create_constraint(\n                self, constr, schema, context\n            ))\n\n    def _alter_pointer_optionality(\n        self,\n        schema: s_schema.Schema,\n        orig_schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        fill_expr: Optional[s_expr.Expression],\n        is_default: bool=False,\n    ) -> None:\n        new_required = self.scls.get_required(schema)\n\n        ptr = self.scls\n        ptr_stor_info = types.get_pointer_storage_info(ptr, schema=schema)\n        ptr_table = ptr_stor_info.table_type == 'link'\n        is_lprop = ptr.is_link_property(schema)\n        is_multi = ptr_table and not is_lprop\n        is_required = ptr.get_required(schema)\n\n        source_ctx = self.get_referrer_context_or_die(context)\n        source_op = source_ctx.op\n        assert isinstance(source_op, CompositeMetaCommand)\n\n        alter_table = None\n        if not ptr_table or is_lprop:\n            alter_table = source_op.get_alter_table(\n                schema,\n                context,\n                manual=True,\n            )\n            alter_table.add_operation(\n                dbops.AlterTableAlterColumnNull(\n                    column_name=ptr_stor_info.column_name,\n                    null=not new_required,\n                )\n            )\n\n        # Ignore optionality changes resulting from the creation of\n        # an overloaded pointer as there is no data yet.\n        if isinstance(source_op, sd.CreateObject):\n            if alter_table:\n                self.pgops.add(alter_table)\n            return\n\n        ops = dbops.CommandGroup()\n\n        # For multi pointers, if there is no fill expression, we\n        # synthesize a bogus one so that an error will trip if there\n        # are any objects with empty values.\n        if fill_expr is None and is_multi and is_required:\n            if (\n                ptr.get_cardinality(schema).is_multi()\n                and fill_expr is None\n                and (target := ptr.get_target(schema))\n            ):\n                fill_ast = ql_ast.TypeCast(\n                    expr=ql_ast.Set(elements=[]),\n                    type=s_utils.typeref_to_ast(schema, target),\n                )\n                fill_expr = s_expr.Expression.from_ast(\n                    qltree=fill_ast, schema=schema\n                )\n\n        if fill_expr is not None:\n\n            assert ptr_stor_info.table_name\n            tab = q(*ptr_stor_info.table_name)\n            target_col = ptr_stor_info.column_name\n            source = ptr.get_source(orig_schema)\n            assert source\n            src_tab = q(*common.get_backend_name(\n                orig_schema,\n                source,\n                catenate=False,\n            ))\n\n            if not is_multi:\n                # For singleton pointers we simply update the\n                # requisite column of the host source in every\n                # row where it is NULL.\n                source_rel = textwrap.dedent(f'''\\\n                    SELECT * FROM {tab}\n                    WHERE {qi(target_col)} IS NULL\n                ''')\n            else:\n                # For multi pointers we have to INSERT the\n                # result of USING into the link table for\n                # every source object that has _no entries_\n                # in said link table.\n                source_rel = textwrap.dedent(f'''\\\n                    SELECT * FROM {src_tab}\n                    WHERE id NOT IN (SELECT source FROM {tab})\n                ''')\n\n            source_rel_alias = f'source_{uuidgen.uuid1mc()}'\n\n            (conv_expr_ctes, conv_expr, _) = self._compile_conversion_expr(\n                ptr,\n                fill_expr,\n                source_rel_alias,\n                schema=schema,\n                orig_schema=orig_schema,\n                context=context,\n                check_non_null=is_required and not is_multi,\n                allow_globals=is_default,\n                produce_ctes=not is_lprop,\n            )\n\n            if is_lprop:\n                # The produce_ctes=True flow really wants to key\n                # everything based on id, which doesn't work for link\n                # properties.  If produce_ctes=True was able to use\n                # ctid or (source, target) for keying, we could use\n                # that here, but for now we will just use the\n                # old-school version. See #5050, which would require\n                # that in order to support DML in cast expressions.\n                assert conv_expr\n                update_with = (\n                    f'WITH {conv_expr_ctes}' if conv_expr_ctes else ''\n                )\n                update_qry = f'''\n                    {update_with}\n                    UPDATE {tab} AS {qi(source_rel_alias)}\n                    SET {qi(target_col)} = ({conv_expr})\n                    WHERE {qi(target_col)} IS NULL\n                '''\n                self.pgops.add(dbops.Query(update_qry))\n\n            elif not is_multi:\n                update_qry = textwrap.dedent(f'''\\\n                    WITH\n                    \"{source_rel_alias}\" AS ({source_rel}),\n                    {conv_expr_ctes}\n                    UPDATE {tab} AS _update\n                    SET {qi(target_col)} = _conv_rel.val\n                    FROM _conv_rel WHERE _update.id = _conv_rel.id\n                ''')\n                ops.add_command(dbops.Query(update_qry))\n            else:\n                update_qry = textwrap.dedent(f'''\\\n                    WITH\n                    \"{source_rel_alias}\" AS ({source_rel}),\n                    {conv_expr_ctes}\n                    INSERT INTO {tab} (source, target)\n                    (SELECT id, val FROM _conv_rel WHERE val IS NOT NULL)\n                ''')\n\n                ops.add_command(dbops.Query(update_qry))\n\n                if is_required:\n                    check_qry = textwrap.dedent(f'''\\\n                        SELECT\n                            edgedb_VER.raise(\n                                NULL::text,\n                                'not_null_violation',\n                                msg => 'missing value for required property',\n                                detail => '{{\"object_id\": \"' || id || '\"}}',\n                                \"column\" => {ql(str(ptr.id))}\n                            )\n                        FROM {src_tab}\n                        WHERE id != ALL (SELECT source FROM {tab})\n                        LIMIT 1\n                        INTO _dummy_text;\n                    ''')\n                    ops.add_command(dbops.Query(check_qry))\n\n        if alter_table:\n            ops.add_command(alter_table)\n\n        self.pgops.add(ops)\n\n    def _drop_constraints(self, pointer, schema, context):\n        # We need to be able to drop all the constraints referencing a\n        # pointer before modifying its type, and then recreate them\n        # once the change is done.\n        # We look at all referrers to the pointer (and not just the\n        # constraints directly on the pointer) because we want to\n        # pick up object constraints that reference it as well.\n        for cnstr in schema.get_referrers(\n            pointer, scls_type=s_constr.Constraint\n        ):\n            self.pgops.add(ConstraintCommand.delete_constraint(cnstr, schema))\n\n    def _recreate_constraints(self, pointer, schema, context):\n        for cnstr in schema.get_referrers(\n            pointer, scls_type=s_constr.Constraint\n        ):\n            self.pgops.add(ConstraintCommand.create_constraint(\n                self, cnstr, schema, context,\n            ))\n\n    def _alter_pointer_type(self, pointer, schema, orig_schema, context):\n        old_ptr_stor_info = types.get_pointer_storage_info(\n            pointer, schema=orig_schema)\n        new_target = pointer.get_target(schema)\n\n        ptr_table = old_ptr_stor_info.table_type == 'link'\n        is_link = isinstance(pointer, s_links.Link)\n        is_lprop = pointer.is_link_property(schema)\n        is_multi = ptr_table and not is_lprop\n        is_required = pointer.get_required(schema)\n        changing_col_type = not is_link\n\n        source_ctx = self.get_referrer_context_or_die(context)\n        ptr_op = self.get_parent_op(context)\n        if is_multi:\n            source_op = ptr_op\n        else:\n            source_op = source_ctx.op\n\n        # Ignore type narrowing resulting from a creation of a subtype\n        # as there isn't any data in the link yet.\n        if is_link and isinstance(source_ctx.op, sd.CreateObject):\n            return\n\n        new_target = pointer.get_target(schema)\n        orig_target = pointer.get_target(orig_schema)\n        new_type = types.pg_type_from_object(\n            schema, new_target, persistent_tuples=True)\n\n        source = source_op.scls\n        cast_expr = self.cast_expr\n\n        # For links, when the new type is a supertype of the old, no\n        # SQL-level changes are necessary, unless an explicit conversion\n        # expression was specified.\n        if (\n            is_link\n            and cast_expr is None\n            and orig_target.issubclass(orig_schema, new_target)\n        ):\n            return\n\n        # We actually have work to do, so drop any constraints we have\n        self._drop_constraints(pointer, schema, context)\n\n        if cast_expr is None and not is_link:\n            # A lack of an explicit EdgeQL conversion expression means\n            # that the new type is assignment-castable from the old type\n            # in the EdgeDB schema.  BUT, it would not necessarily be\n            # assignment-castable in Postgres, especially if the types are\n            # compound.  Thus, generate an explicit cast expression.\n            pname = pointer.get_shortname(schema).name\n            cast_expr = s_expr.Expression.from_ast(\n                ql_ast.TypeCast(\n                    expr=ql_ast.Path(\n                        partial=True,\n                        steps=[\n                            ql_ast.Ptr(\n                                name=pname,\n                                type='property' if is_lprop else None,\n                            ),\n                        ],\n                    ),\n                    type=s_utils.typeref_to_ast(schema, new_target),\n                ),\n                schema=orig_schema,\n            )\n\n        tab = q(*old_ptr_stor_info.table_name)\n        target_col = old_ptr_stor_info.column_name\n        aux_ptr_table = None\n        aux_ptr_col = None\n\n        source_rel_alias = f'source_{uuidgen.uuid1mc()}'\n\n        # There are two major possibilities about the USING claus:\n        # 1) trivial case, where the USING clause refers only to the\n        # columns of the source table, in which case we simply compile that\n        # into an equivalent SQL USING clause, and 2) complex case, which\n        # supports arbitrary queries, but requires a temporary column,\n        # which is populated with the transition query and then used as the\n        # source for the SQL USING clause.\n        (cast_expr_ctes, cast_expr_sql, expr_is_nullable) = (\n            self._compile_conversion_expr(\n                pointer,\n                cast_expr,\n                source_rel_alias,\n                schema=schema,\n                orig_schema=orig_schema,\n                context=context,\n                check_non_null=is_required and not is_multi,\n                produce_ctes=False,\n            )\n        )\n        assert cast_expr_sql is not None\n        need_temp_col = (\n            (is_multi and expr_is_nullable) or changing_col_type\n        )\n\n        if is_link:\n            old_lb_ptr_stor_info = types.get_pointer_storage_info(\n                pointer, link_bias=True, schema=orig_schema)\n\n            if (\n                old_lb_ptr_stor_info is not None\n                and old_lb_ptr_stor_info.table_type == 'link'\n            ):\n                aux_ptr_table = old_lb_ptr_stor_info.table_name\n                aux_ptr_col = old_lb_ptr_stor_info.column_name\n\n        if need_temp_col:\n            alter_table = source_op.get_alter_table(\n                schema, context, force_new=True, manual=True)\n            temp_column = dbops.Column(\n                name=f'??{pointer.id}_{common.get_unique_random_name()}',\n                type=qt(new_type),\n            )\n            alter_table.add_operation(\n                dbops.AlterTableAddColumn(temp_column))\n            self.pgops.add(alter_table)\n            target_col = temp_column.name\n\n        update_with = f'WITH {cast_expr_ctes}' if cast_expr_ctes else ''\n        update_qry = f'''\n            {update_with}\n            UPDATE {tab} AS {qi(source_rel_alias)}\n            SET {qi(target_col)} = ({cast_expr_sql})\n        '''\n        self.pgops.add(dbops.Query(update_qry))\n        trivial_cast_expr = qi(target_col)\n\n        if changing_col_type or need_temp_col:\n            alter_table = source_op.get_alter_table(\n                schema, context, force_new=True, manual=True)\n\n        if is_multi:\n            # Remove all rows where the conversion expression produced NULLs.\n            col = qi(target_col)\n            if pointer.get_required(schema):\n                clean_nulls = dbops.Query(textwrap.dedent(f'''\\\n                    WITH d AS (\n                        DELETE FROM {tab} WHERE {col} IS NULL RETURNING source\n                    )\n                    SELECT\n                        edgedb_VER.raise(\n                            NULL::text,\n                            'not_null_violation',\n                            msg => 'missing value for required property',\n                            detail => '{{\"object_id\": \"' || l.source || '\"}}',\n                            \"column\" => {ql(str(pointer.id))}\n                        )\n                    FROM\n                        {tab} AS l\n                    WHERE\n                        l.source IN (SELECT source FROM d)\n                        AND True = ALL (\n                            SELECT {col} IS NULL\n                            FROM {tab} AS l2\n                            WHERE l2.source = l.source\n                        )\n                    LIMIT\n                        1\n                    INTO _dummy_text;\n                '''))\n            else:\n                clean_nulls = dbops.Query(textwrap.dedent(f'''\\\n                    DELETE FROM {tab} WHERE {col} IS NULL\n                '''))\n\n            self.pgops.add(clean_nulls)\n\n        elif aux_ptr_table is not None:\n            # SINGLE links with link properties are represented in\n            # _two_ tables (the host type table and a link table with\n            # properties), and we must update both.\n            actual_col = qi(old_ptr_stor_info.column_name)\n\n            if expr_is_nullable and not is_required:\n                cleanup_qry = textwrap.dedent(f'''\\\n                    DELETE FROM {q(*aux_ptr_table)} AS aux\n                    USING {tab} AS main\n                    WHERE\n                        main.id = aux.source\n                        AND {actual_col} IS NULL\n                ''')\n                self.pgops.add(dbops.Query(cleanup_qry))\n\n            update_qry = textwrap.dedent(f'''\\\n                UPDATE {q(*aux_ptr_table)} AS aux\n                SET {qi(aux_ptr_col)} = main.{actual_col}\n                FROM {tab} AS main\n                WHERE\n                    main.id = aux.source\n            ''')\n            self.pgops.add(dbops.Query(update_qry))\n\n        if changing_col_type:\n            # In case the column has a default, clear it out before\n            # changing the type\n            alter_table.add_operation(\n                dbops.AlterTableAlterColumnDefault(\n                    column_name=old_ptr_stor_info.column_name,\n                    default=None))\n\n            alter_type = dbops.AlterTableAlterColumnType(\n                old_ptr_stor_info.column_name,\n                common.quote_type(new_type),\n                cast_expr=trivial_cast_expr,\n            )\n\n            alter_table.add_operation(alter_type)\n        elif need_temp_col:\n            move_data = dbops.Query(textwrap.dedent(f'''\\\n                UPDATE {q(*old_ptr_stor_info.table_name)}\n                SET {qi(old_ptr_stor_info.column_name)} = ({qi(target_col)})\n            '''))\n            self.pgops.add(move_data)\n\n        if need_temp_col:\n            alter_table.add_operation(dbops.AlterTableDropColumn(temp_column))\n\n        if changing_col_type or need_temp_col:\n            self.pgops.add(alter_table)\n\n        self._recreate_constraints(pointer, schema, context)\n\n        if changing_col_type:\n            self.update_if_cfg_view(schema, context, source)\n\n    def _compile_conversion_expr(\n        self,\n        pointer: s_pointers.Pointer,\n        conv_expr: s_expr.Expression,\n        source_alias: str,\n        *,\n        schema: s_schema.Schema,\n        orig_schema: s_schema.Schema,\n        context: sd.CommandContext,\n        target_as_singleton: bool = True,\n        check_non_null: bool = False,\n        produce_ctes: bool = True,\n        allow_globals: bool=False,\n    ) -> tuple[\n        str,  # CTE SQL\n        Optional[str],  # Query SQL\n        bool,  # is_nullable\n    ]:\n        \"\"\"\n        Compile USING expression of an ALTER statement.\n\n        producing_ctes contract:\n        - Must be provided with alias of \"source\" rel - the relation that\n          contains a row for each of the evaluations for the USING expression.\n        - Source rel var must contain all columns of the __subject__\n          ObjectType.\n        - Result is SQL string that contains CTEs, last of which has following\n          signature: _conv_rel (id, val)\n\n        not producing_ctes contract:\n        - Alias of the source must refer to a relation var, not a relation.\n        - Result is SQL string that contain a single SELECT statement that\n          has a single value column.\n        \"\"\"\n        old_ptr_stor_info = types.get_pointer_storage_info(\n            pointer, schema=orig_schema)\n        ptr_table = old_ptr_stor_info.table_type == 'link'\n        is_link = isinstance(pointer, s_links.Link)\n        is_lprop = pointer.is_link_property(schema)\n\n        new_target = not_none(pointer.get_target(schema))\n\n        if conv_expr.irast is None:\n            conv_expr = self._compile_expr(\n                orig_schema,\n                context,\n                conv_expr,\n                target_as_singleton=target_as_singleton,\n                make_globals_empty=allow_globals,\n                no_query_rewrites=True,\n            )\n        ir = conv_expr.irast\n        assert ir\n\n        if ir.stype != new_target and not is_link:\n            # The result of an EdgeQL USING clause does not match\n            # the target type exactly, but is castable.  Like in the\n            # case of an empty USING clause, we still have to make\n            # ane explicit EdgeQL cast rather than rely on Postgres\n            # casting.\n            conv_expr = self._compile_expr(\n                orig_schema,\n                context,\n                s_expr.Expression.from_ast(\n                    ql_ast.TypeCast(\n                        expr=conv_expr.parse(),\n                        type=s_utils.typeref_to_ast(schema, new_target),\n                    ),\n                    schema=orig_schema,\n                ),\n                target_as_singleton=target_as_singleton,\n                make_globals_empty=allow_globals,\n                no_query_rewrites=True,\n            )\n\n            ir = conv_expr.irast\n\n        if params := irutils.get_parameters(ir):\n            param = list(params)[0]\n            if param.is_global:\n                if param.is_implicit_global:\n                    problem = 'functions that reference globals'\n                else:\n                    problem = 'globals'\n            else:\n                problem = 'parameters'\n            raise errors.UnsupportedFeatureError(\n                f'{problem} may not be used when converting/populating '\n                f'data in migrations',\n                span=self.span,\n            )\n\n        # Non-trivial conversion expression means that we\n        # are compiling a full-blown EdgeQL statement as\n        # opposed to compiling a scalar fragment in trivial\n        # expression mode.\n\n        if is_lprop:\n            # For linkprops we actually want the source path.\n            # To make it work for abstract links, get the source\n            # path out of the IR's output (to take advantage\n            # of the types we made up for it).\n            # FIXME: Maybe we shouldn't be compiling stuff\n            # for abstract links!\n            tgt_path_id = ir.singletons[0]\n        else:\n            tgt_path_id = irpathid.PathId.from_pointer(\n                orig_schema, pointer, env=None\n            )\n\n        refs = irutils.get_longest_paths(ir.expr)\n        ref_tables = schemamech.get_ref_storage_info(ir.schema, refs)\n        local_table_only = all(\n            t == old_ptr_stor_info.table_name\n            for t in ref_tables\n        )\n\n        ptr_path_id = tgt_path_id.ptr_path()\n        src_path_id = ptr_path_id.src_path()\n        assert src_path_id\n\n        external_rels = {}\n        external_rvars = {}\n\n        if produce_ctes:\n            external_rels[src_path_id] = compiler.new_external_rel(\n                rel_name=(source_alias,),\n                path_id=src_path_id,\n            ), (pgce.PathAspect.VALUE, pgce.PathAspect.SOURCE)\n        else:\n            if ptr_table:\n                rvar = compiler.new_external_rvar(\n                    rel_name=(source_alias,),\n                    path_id=ptr_path_id,\n                    outputs={\n                        (src_path_id, (pgce.PathAspect.IDENTITY,)): 'source',\n                    },\n                )\n                external_rvars[ptr_path_id, pgce.PathAspect.SOURCE] = rvar\n                external_rvars[ptr_path_id, pgce.PathAspect.VALUE] = rvar\n                external_rvars[src_path_id, pgce.PathAspect.IDENTITY] = rvar\n                external_rvars[tgt_path_id, pgce.PathAspect.IDENTITY] = rvar\n                if local_table_only and not is_lprop:\n                    external_rvars[src_path_id, pgce.PathAspect.SOURCE] = rvar\n                    external_rvars[src_path_id, pgce.PathAspect.VALUE] = rvar\n                elif is_lprop:\n                    external_rvars[tgt_path_id, pgce.PathAspect.VALUE] = rvar\n            else:\n                src_rvar = compiler.new_external_rvar(\n                    rel_name=(source_alias,),\n                    path_id=src_path_id,\n                    outputs={},\n                )\n                external_rvars[src_path_id, pgce.PathAspect.IDENTITY] = src_rvar\n                external_rvars[src_path_id, pgce.PathAspect.VALUE] = src_rvar\n                external_rvars[src_path_id, pgce.PathAspect.SOURCE] = src_rvar\n\n        # Wrap the expression into a select with iterator, so DML and\n        # volatile expressions are executed once for each object.\n        #\n        # The result is roughly equivalent to:\n        # for obj in Object union select <expr>\n\n        # generate a unique path id for the outer scope\n        typ = orig_schema.get(f'schema::ObjectType', type=s_types.Type)\n        outer_path = irast.PathId.from_type(\n            orig_schema, typ, typename=sn.QualName(\"std\", \"obj\"), env=None,\n        )\n\n        root_uid = -1\n        iter_uid = -2\n        body_uid = -3\n        # scope tree wrapping is roughly equivalent to:\n        # \"(std::obj) uid:-1\": {\n        #   \"BRANCH uid:-2\",\n        #   \"FENCE uid:-3\": { ... compiled scope children ... }\n        # }\n        scope_iter = irast.ScopeTreeNode(\n            unique_id=iter_uid,\n        )\n        scope_body = irast.ScopeTreeNode(\n            unique_id=body_uid,\n            fenced=True\n        )\n        # Need to make a copy of the children list because\n        # attach_child removes the node from the parent list.\n        for child in list(ir.scope_tree.children):\n            scope_body.attach_child(child)\n\n        scope_node = irast.ScopeTreeNode(\n            unique_id=root_uid,\n            path_id=outer_path,\n        )\n        scope_node.attach_child(scope_iter)\n        scope_node.attach_child(scope_body)\n\n        # The top-level node must be a fence.\n        scope_root = irast.ScopeTreeNode(fenced=True)\n        scope_root.attach_child(scope_node)\n        ir.scope_tree = scope_root\n\n        # IR ast wrapping\n        assert isinstance(ir.expr, irast.Set)\n        for_body = ir.expr\n        for_body.path_scope_id = body_uid\n        ir.expr = irast.Set(\n            path_id=outer_path,\n            typeref=outer_path.target,\n            path_scope_id=root_uid,\n            expr=irast.SelectStmt(\n                iterator_stmt=irast.Set(\n                    path_id=src_path_id,\n                    typeref=src_path_id.target,\n                    path_scope_id=iter_uid,\n                    expr=irast.SelectStmt(\n                        result=irast.Set(\n                            path_scope_id=iter_uid,\n                            path_id=src_path_id,\n                            typeref=src_path_id.target,\n                            expr=irast.TypeRoot(typeref=src_path_id.target),\n                        )\n                    )\n                ),\n\n                result=for_body,\n            )\n        )\n\n        # compile\n        sql_res = compiler.compile_ir_to_sql_tree(\n            ir,\n            output_format=compiler.OutputFormat.NATIVE_INTERNAL,\n            external_rels=external_rels,\n            external_rvars=external_rvars,\n            backend_runtime_params=context.backend_runtime_params,\n        )\n        sql_tree = sql_res.ast\n        assert isinstance(sql_tree, pgast.SelectStmt)\n\n        if produce_ctes:\n            # ensure the result contains the object id in the second column\n\n            from edb.pgsql.compiler import pathctx\n            pathctx.get_path_output(\n                sql_tree,\n                src_path_id,\n                aspect=pgce.PathAspect.IDENTITY,\n                env=sql_res.env,\n            )\n\n        ctes = list(sql_tree.ctes or [])\n        if sql_tree.ctes:\n            sql_tree.ctes.clear()\n\n        if check_non_null:\n            # wrap into raise_on_null\n            pointer_name = 'link' if is_link else 'property'\n            msg = pgast.StringConstant(\n                val=f\"missing value for required {pointer_name}\"\n            )\n            # Concat to string which is a JSON. Great. Equivalent to SQL:\n            # '{\"object_id\": \"' || {obj_id_ref} || '\"}'\n            # We report 'source' for linkprops. Seems OK, I guess.\n            id_col = 'source' if is_lprop else 'id'\n            detail = pgast.Expr(\n                name='||',\n                lexpr=pgast.StringConstant(val='{\"object_id\": \"'),\n                rexpr=pgast.Expr(\n                    name='||',\n                    lexpr=pgast.ColumnRef(name=(id_col,)),\n                    rexpr=pgast.StringConstant(val='\"}'),\n                )\n            )\n            column = pgast.StringConstant(val=str(pointer.id))\n\n            null_check = pgast.FuncCall(\n                name=(\"edgedb\", \"raise_on_null\"),\n                args=[\n                    pgast.ColumnRef(name=(\"val\",)),\n                    pgast.StringConstant(val=\"not_null_violation\"),\n                    pgast.NamedFuncArg(name=\"msg\", val=msg),\n                    pgast.NamedFuncArg(name=\"detail\", val=detail),\n                    pgast.NamedFuncArg(name=\"column\", val=column),\n                ],\n            )\n\n            inner_colnames = [\"val\"]\n            target_list = [pgast.ResTarget(val=null_check)]\n            if produce_ctes:\n                inner_colnames.append(\"id\")\n                target_list.append(\n                    pgast.ResTarget(val=pgast.ColumnRef(name=(\"id\",)))\n                )\n\n            sql_tree = pgast.SelectStmt(\n                target_list=target_list,\n                from_clause=[\n                    pgast.RangeSubselect(\n                        subquery=sql_tree,\n                        alias=pgast.Alias(\n                            aliasname=\"_inner\", colnames=inner_colnames\n                        )\n                    )\n                ]\n            )\n\n        nullable = conv_expr.cardinality.can_be_zero()\n\n        if produce_ctes:\n            # convert root query into last CTE\n            ctes.append(\n                pgast.CommonTableExpr(\n                    name=\"_conv_rel\",\n                    aliascolnames=[\"val\", \"id\"],\n                    query=sql_tree,\n                )\n            )\n            # compile to SQL\n            ctes_sql = codegen.generate_ctes_source(ctes)\n\n            return (ctes_sql, None, nullable)\n\n        else:\n            # keep CTEs and select separate\n            ctes_sql = codegen.generate_ctes_source(ctes)\n            select_sql = codegen.generate_source(sql_tree)\n\n            return (ctes_sql, select_sql, nullable)\n\n    def schedule_endpoint_delete_action_update(\n        self, link, orig_schema, schema, context\n    ):\n        endpoint_delete_actions = context.get(\n            sd.DeltaRootContext).op.update_endpoint_delete_actions\n        link_ops = endpoint_delete_actions.link_ops\n\n        if isinstance(self, sd.DeleteObject):\n            for i, (_, ex_link, _, _) in enumerate(link_ops):\n                if ex_link == link:\n                    link_ops.pop(i)\n                    break\n\n        link_ops.append((self, link, orig_schema, schema))\n\n\nclass LinkMetaCommand(PointerMetaCommand[s_links.Link]):\n\n    @classmethod\n    def _create_table(\n        cls,\n        link: s_links.Link,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        conditional: bool = False,\n        create_children: bool = True,\n    ):\n        new_table_name = cls._get_table_name(link, schema)\n\n        create_c = dbops.CommandGroup()\n\n        constraints = []\n        columns = []\n\n        src_col = 'source'\n        tgt_col = 'target'\n\n        columns.append(\n            dbops.Column(\n                name=src_col, type='uuid', required=True))\n        columns.append(\n            dbops.Column(\n                name=tgt_col, type='uuid', required=True))\n\n        constraints.append(\n            dbops.UniqueConstraint(\n                table_name=new_table_name,\n                columns=[src_col, tgt_col]))\n\n        if not link.is_non_concrete(schema) and link.is_property():\n            tgt_prop = link.getptr(schema, sn.UnqualName('target'))\n            tgt_ptr = types.get_pointer_storage_info(\n                tgt_prop, schema=schema)\n            columns.append(\n                dbops.Column(\n                    name=tgt_ptr.column_name,\n                    type=common.qname(*tgt_ptr.column_type)))\n\n        table = dbops.Table(name=new_table_name)\n        table.add_columns(columns)\n        table.constraints = ordered.OrderedSet(constraints)\n\n        ct = dbops.CreateTable(table=table)\n\n        index_name = common.edgedb_name_to_pg_name(\n            str(link.id) + '_target_key')\n        index = dbops.Index(\n            index_name,\n            new_table_name,\n            unique=False,\n            metadata={'code': DEFAULT_INDEX_CODE},\n        )\n        index.add_columns([tgt_col])\n        ci = dbops.CreateIndex(index)\n\n        if conditional:\n            c = dbops.CommandGroup(\n                neg_conditions=[dbops.TableExists(new_table_name)])\n        else:\n            c = dbops.CommandGroup()\n\n        c.add_command(ct)\n        c.add_command(ci)\n\n        c.add_command(\n            dbops.Comment(\n                table,\n                str(link.get_verbosename(schema, with_parent=True)),\n            ),\n        )\n\n        create_c.add_command(c)\n\n        if create_children:\n            for l_descendant in link.descendants(schema):\n                if types.has_table(l_descendant, schema):\n                    lc = LinkMetaCommand._create_table(\n                        l_descendant,\n                        schema,\n                        context,\n                        conditional=True,\n                        create_children=False,\n                    )\n                    create_c.add_command(lc)\n\n        return create_c\n\n    def _create_link(\n        self,\n        link: s_links.Link,\n        schema: s_schema.Schema,\n        orig_schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n\n        objtype = context.get(s_objtypes.ObjectTypeCommandContext)\n        source = link.get_source(schema)\n\n        if source is not None:\n            source_is_view = (\n                source.is_view(schema)\n                or source.is_compound_type(schema)\n                or source.get_is_derived(schema)\n            )\n        else:\n            source_is_view = None\n\n        if types.has_table(self.scls, schema):\n            self.create_table(self.scls, schema, context)\n\n        if (\n            source is not None\n            and not source_is_view\n            and not link.is_pure_computable(schema)\n        ):\n            # We optimize away __type__ and don't store it.\n            # Nothing to do except make sure the inhviews get updated.\n            if link.get_shortname(schema).name == '__type__':\n                self.update_source_if_cfg_view(\n                    schema, context, link\n                )\n                return\n\n            assert objtype\n            ptr_stor_info = types.get_pointer_storage_info(\n                link, resolve_type=False, schema=schema)\n\n            fills_required = any(\n                x.fill_expr for x in\n                self.get_subcommands(\n                    type=s_pointers.AlterPointerLowerCardinality))\n            sets_required = bool(\n                self.get_subcommands(\n                    type=s_pointers.AlterPointerLowerCardinality))\n\n            if ptr_stor_info.table_type == 'ObjectType':\n                cols = self.get_columns(\n                    link, schema, None, sets_required)\n                table_name = objtype.op.table_name  # type: ignore\n                assert isinstance(objtype.op, CompositeMetaCommand)\n                objtype_alter_table = objtype.op.get_alter_table(\n                    schema, context, manual=True)\n\n                for col in cols:\n                    cmd = dbops.AlterTableAddColumn(col)\n                    objtype_alter_table.add_operation(cmd)\n\n                self.pgops.add(objtype_alter_table)\n\n                index_name = common.get_backend_name(\n                    schema, link, catenate=False, aspect='index'\n                )[1]\n\n                pg_index = dbops.Index(\n                    name=index_name, table_name=table_name,\n                    unique=False, columns=[c.name for c in cols],\n                    inherit=True,\n                    metadata={\n                        'code': DEFAULT_INDEX_CODE,\n                    },\n                )\n\n                ci = dbops.CreateIndex(pg_index)\n                self.pgops.add(ci)\n\n                self.update_source_if_cfg_view(\n                    schema, context, link\n                )\n\n            if (\n                (default := link.get_default(schema))\n                and not link.is_pure_computable(schema)\n                and not fills_required\n            ):\n                self._alter_pointer_optionality(\n                    schema, schema, context,\n                    fill_expr=default, is_default=True)\n            # If we're creating a required multi pointer without a SET\n            # REQUIRED USING inside, run the alter_pointer_optionality\n            # path to produce an error if there is existing data.\n            elif (\n                link.get_cardinality(schema).is_multi()\n                and link.get_required(schema)\n                and not link.is_pure_computable(schema)\n                and not sets_required\n            ):\n                self._alter_pointer_optionality(\n                    schema, schema, context, fill_expr=None)\n\n            if not link.is_pure_computable(schema):\n                self.schedule_endpoint_delete_action_update(\n                    link, orig_schema, schema, context)\n\n    def _delete_link(\n        self,\n        link: s_links.Link,\n        schema: s_schema.Schema,\n        orig_schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n\n        # We optimize away __type__ and don't store it. Nothing to do.\n        if link.get_shortname(schema).name == '__type__':\n            return\n\n        old_table_name = self._get_table_name(link, schema)\n\n        if (\n            not link.is_non_concrete(orig_schema)\n            and types.has_table(link.get_source(orig_schema), orig_schema)\n            and not link.is_pure_computable(orig_schema)\n        ):\n            ptr_stor_info = types.get_pointer_storage_info(\n                link, schema=orig_schema)\n\n            objtype = context.get(s_objtypes.ObjectTypeCommandContext)\n            assert objtype\n\n            if (not isinstance(objtype.op, s_objtypes.DeleteObjectType)\n                    and ptr_stor_info.table_type == 'ObjectType'):\n                self.update_if_cfg_view(schema, context, objtype.scls)\n                assert isinstance(objtype.op, CompositeMetaCommand)\n                alter_table = objtype.op.get_alter_table(\n                    schema, context, manual=True)\n                col = dbops.Column(\n                    name=ptr_stor_info.column_name,\n                    type=common.qname(*ptr_stor_info.column_type))\n                colop = dbops.AlterTableDropColumn(col)\n                alter_table.add_operation(colop)\n                self.pgops.add(alter_table)\n\n            self.attach_alter_table(context)\n\n        if types.has_table(link, orig_schema):\n            condition = dbops.TableExists(name=old_table_name)\n            self.pgops.add(\n                dbops.DropTable(name=old_table_name, conditions=[condition]))\n\n        self.schedule_endpoint_delete_action_update(\n            link, orig_schema, schema, context)\n\n\nclass CreateLink(LinkMetaCommand, adapts=s_links.CreateLink):\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        schema = super()._create_begin(schema, context)\n\n        link = self.scls\n        self.table_name = self._get_table_name(self.scls, schema)\n\n        self._create_link(link, schema, orig_schema, context)\n\n        return schema\n\n    def _create_finalize(self, schema, context):\n        schema = super()._create_finalize(schema, context)\n        self.apply_constraint_trigger_updates(schema)\n        self.schedule_trampoline(self.scls, schema, context)\n        return schema\n\n\nclass RenameLink(LinkMetaCommand, adapts=s_links.RenameLink):\n    pass\n\n\nclass RebaseLink(LinkMetaCommand, adapts=s_links.RebaseLink):\n    def _alter_innards(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = context.current().original_schema\n        if types.has_table(self.scls, schema):\n            self.update_if_cfg_view(schema, context, self.scls)\n\n        schema = super()._alter_innards(schema, context)\n\n        self.schedule_endpoint_delete_action_update(\n            self.scls, orig_schema, schema, context)\n\n        return schema\n\n\nclass SetLinkType(LinkMetaCommand, adapts=s_links.SetLinkType):\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        schema = super()._alter_begin(schema, context)\n        orig_type = self.scls.get_target(orig_schema)\n        new_type = self.scls.get_target(schema)\n        if (\n            types.has_table(self.scls.get_source(schema), schema)\n            and not self.scls.is_pure_computable(schema)\n            and (orig_type != new_type or self.cast_expr is not None)\n        ):\n            self._alter_pointer_type(self.scls, schema, orig_schema, context)\n            self.schedule_endpoint_delete_action_update(\n                self.scls, orig_schema, schema, context)\n        return schema\n\n\nclass AlterLinkUpperCardinality(\n    LinkMetaCommand,\n    adapts=s_links.AlterLinkUpperCardinality,\n):\n    def _alter_innards(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = context.current().original_schema\n\n        # We need to run the parent change *before* the children,\n        # or else the view update in the child might fail if a\n        # link table isn't created in the parent yet.\n        if (\n            not self.scls.is_non_concrete(schema)\n            and not self.scls.is_pure_computable(schema)\n            and types.has_table(self.scls.get_source(schema), schema)\n        ):\n            orig_card = self.scls.get_cardinality(orig_schema)\n            new_card = self.scls.get_cardinality(schema)\n            if orig_card != new_card:\n                self._alter_pointer_cardinality(schema, orig_schema, context)\n\n        return super()._alter_innards(schema, context)\n\n\nclass AlterLinkLowerCardinality(\n    LinkMetaCommand,\n    adapts=s_links.AlterLinkLowerCardinality,\n):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        schema = super().apply(schema, context)\n\n        if not self.scls.is_non_concrete(schema):\n            orig_required = self.scls.get_required(orig_schema)\n            new_required = self.scls.get_required(schema)\n            if (\n                types.has_table(self.scls.get_source(schema), schema)\n                and not self.scls.is_endpoint_pointer(schema)\n                and not self.scls.is_pure_computable(schema)\n                and orig_required != new_required\n            ):\n                self._alter_pointer_optionality(\n                    schema, orig_schema, context, fill_expr=self.fill_expr)\n\n                # If the link has an Allow on target delete action, we\n                # need to refresh the triggers, since required and\n                # optional behave differently. (Required does a\n                # check.)\n                if (\n                    self.scls.get_on_target_delete(schema) ==\n                    s_links.LinkTargetDeleteAction.Allow\n                ):\n                    self.schedule_endpoint_delete_action_update(\n                        self.scls, orig_schema, schema, context)\n\n        return schema\n\n\nclass AlterLinkOwned(LinkMetaCommand, adapts=s_links.AlterLinkOwned):\n    pass\n\n\nclass AlterLink(LinkMetaCommand, adapts=s_links.AlterLink):\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._alter_begin(schema, context)\n        # We want to set this name up early, so children operations see it\n        self.table_name = self._get_table_name(self.scls, schema)\n        return schema\n\n    def _alter_innards(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = context.current().original_schema\n\n        link = self.scls\n        is_abs = link.is_non_concrete(schema)\n        is_comp = link.is_pure_computable(schema)\n        was_comp = link.is_pure_computable(orig_schema)\n\n        if not is_abs and (was_comp and not is_comp):\n            self._create_link(link, schema, orig_schema, context)\n\n        schema = super()._alter_innards(schema, context)\n\n        if not is_abs and (not was_comp and is_comp):\n            self._delete_link(link, schema, orig_schema, context)\n\n        # We check whether otd has changed, rather than whether\n        # it is an attribute on this alter, because it might\n        # live on a nested SetOwned, for example.\n        otd_changed = (\n            link.get_on_target_delete(orig_schema) !=\n            link.get_on_target_delete(schema)\n        )\n        osd_changed = (\n            link.get_on_source_delete(orig_schema) !=\n            link.get_on_source_delete(schema)\n        )\n        card_changed = (\n            link.get_cardinality(orig_schema) !=\n            link.get_cardinality(schema)\n        )\n        if (\n            (otd_changed or osd_changed or card_changed)\n            and not link.is_pure_computable(schema)\n        ):\n            self.schedule_endpoint_delete_action_update(\n                link, orig_schema, schema, context)\n\n        return schema\n\n    def _alter_finalize(self, schema, context):\n        schema = super()._alter_finalize(schema, context)\n        self.apply_constraint_trigger_updates(schema)\n        return schema\n\n\nclass DeleteLink(LinkMetaCommand, adapts=s_links.DeleteLink):\n    def _delete_innards(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = context.current().original_schema\n        link = schema.get(self.classname, type=s_links.Link)\n\n        schema = super()._delete_innards(schema, context)\n        self._delete_link(link, schema, orig_schema, context)\n\n        return schema\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n\n        self.apply_constraint_trigger_updates(schema)\n\n        return schema\n\n\nclass PropertyMetaCommand(PointerMetaCommand[s_props.Property]):\n\n    @classmethod\n    def _create_table(\n        cls,\n        prop: s_props.Property,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        conditional: bool = False,\n        create_children: bool = True,\n    ):\n        new_table_name = cls._get_table_name(prop, schema)\n\n        create_c = dbops.CommandGroup()\n\n        columns = []\n\n        src_col = common.edgedb_name_to_pg_name('source')\n\n        columns.append(\n            dbops.Column(\n                name=src_col, type='uuid', required=True))\n\n        id = sn.QualName(\n            module=prop.get_name(schema).module, name=str(prop.id))\n        index_name = common.convert_name(id, 'idx0', catenate=False)\n\n        pg_index = dbops.Index(\n            name=index_name[1], table_name=new_table_name,\n            unique=False, columns=[src_col],\n            metadata={'code': DEFAULT_INDEX_CODE},\n        )\n\n        ci = dbops.CreateIndex(pg_index)\n\n        if not prop.is_non_concrete(schema):\n            tgt_cols = cls.get_columns(prop, schema, None)\n            columns.extend(tgt_cols)\n\n        table = dbops.Table(name=new_table_name)\n        table.add_columns(columns)\n\n        ct = dbops.CreateTable(table=table)\n\n        if conditional:\n            c = dbops.CommandGroup(\n                neg_conditions=[dbops.TableExists(new_table_name)])\n        else:\n            c = dbops.CommandGroup()\n\n        c.add_command(ct)\n        c.add_command(ci)\n\n        c.add_command(\n            dbops.Comment(\n                table,\n                str(prop.get_verbosename(schema, with_parent=True)),\n            ),\n        )\n\n        create_c.add_command(c)\n\n        if create_children:\n            for p_descendant in prop.descendants(schema):\n                if types.has_table(p_descendant, schema):\n                    pc = PropertyMetaCommand._create_table(\n                        p_descendant,\n                        schema,\n                        context,\n                        conditional=True,\n                        create_children=False,\n                    )\n                    create_c.add_command(pc)\n\n        return create_c\n\n    def _create_property(\n        self,\n        prop: s_props.Property,\n        src: Optional[sd.ObjectCommandContext[s_sources.Source]],\n        schema: s_schema.Schema,\n        orig_schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        propname = prop.get_shortname(schema).name\n\n        if types.has_table(prop, schema):\n            self.create_table(prop, schema, context)\n\n        if (\n            src\n            and types.has_table(src.scls, schema)\n            and not prop.is_pure_computable(schema)\n        ):\n            if (\n                isinstance(src.scls, s_links.Link)\n                and not types.has_table(src.scls, orig_schema)\n            ):\n                ct = src.op._create_table(  # type: ignore\n                    src.scls, schema, context)\n                self.pgops.add(ct)\n\n            ptr_stor_info = types.get_pointer_storage_info(\n                prop, resolve_type=False, schema=schema)\n\n            fills_required = any(\n                x.fill_expr for x in\n                self.get_subcommands(\n                    type=s_pointers.AlterPointerLowerCardinality))\n            sets_required = bool(\n                self.get_subcommands(\n                    type=s_pointers.AlterPointerLowerCardinality))\n\n            if (\n                not isinstance(src.scls, s_objtypes.ObjectType)\n                or ptr_stor_info.table_type == 'ObjectType'\n            ):\n                if (\n                    not isinstance(src.scls, s_links.Link)\n                    or propname not in {'source', 'target'}\n                ):\n                    assert isinstance(src.op, CompositeMetaCommand)\n\n                    alter_table = src.op.get_alter_table(\n                        schema,\n                        context,\n                        force_new=True,\n                        manual=True,\n                    )\n\n                    default_value = self.get_pointer_default(\n                        prop, schema, context)\n\n                    if (\n                        isinstance(src.scls, s_links.Link)\n                        and not default_value\n                        and prop.get_default(schema)\n                    ):\n                        raise errors.UnsupportedFeatureError(\n                            f'default value for '\n                            f'{prop.get_verbosename(schema, with_parent=True)}'\n                            f' is too complicated; link property defaults '\n                            f'must not depend on database contents',\n                            span=self.span)\n\n                    cols = self.get_columns(\n                        prop, schema, default_value, sets_required)\n\n                    for col in cols:\n                        cmd = dbops.AlterTableAddColumn(col)\n                        alter_table.add_operation(cmd)\n\n                    self.pgops.add(alter_table)\n\n                self.update_source_if_cfg_view(\n                    schema, context, prop\n                )\n\n            if (\n                (default := prop.get_default(schema))\n                and not prop.is_pure_computable(schema)\n                and not fills_required\n                and not irtyputils.is_cfg_view(src.scls, schema)  # sigh\n                # link properties use SQL defaults and shouldn't need\n                # us to do it explicitly (which is good, since\n                # _alter_pointer_optionality doesn't currently work on\n                # linkprops)\n                and not prop.is_link_property(schema)\n            ):\n                self._alter_pointer_optionality(\n                    schema, schema, context,\n                    fill_expr=default, is_default=True)\n            # If we're creating a required multi pointer without a SET\n            # REQUIRED USING inside, run the alter_pointer_optionality\n            # path to produce an error if there is existing data.\n            elif (\n                prop.get_cardinality(schema).is_multi()\n                and prop.get_required(schema)\n                and not prop.is_pure_computable(schema)\n                and not sets_required\n            ):\n                self._alter_pointer_optionality(\n                    schema, schema, context, fill_expr=None)\n\n            if not prop.is_pure_computable(schema):\n                self.schedule_endpoint_delete_action_update(\n                    prop, orig_schema, schema, context)\n\n    def _delete_property(\n        self,\n        prop: s_props.Property,\n        source: s_sources.Source,\n        source_op,\n        schema: s_schema.Schema,\n        orig_schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        if types.has_table(source, schema):\n            ptr_stor_info = types.get_pointer_storage_info(\n                prop,\n                schema=schema,\n                link_bias=prop.is_link_property(schema),\n            )\n\n            if (\n                ptr_stor_info.table_type == 'ObjectType'\n                or prop.is_link_property(schema)\n            ):\n                alter_table = source_op.get_alter_table(\n                    schema, context, force_new=True, manual=True)\n\n                # source and target don't have a proper inheritence\n                # hierarchy, so we can't do the source trick for them\n                self.update_if_cfg_view(schema, context, source)\n\n                col = dbops.AlterTableDropColumn(\n                    dbops.Column(name=ptr_stor_info.column_name,\n                                 type=ptr_stor_info.column_type))\n\n                alter_table.add_operation(col)\n\n                self.pgops.add(alter_table)\n        elif (\n            prop.is_link_property(schema)\n            and types.has_table(source, orig_schema)\n        ):\n            old_table_name = self._get_table_name(source, orig_schema)\n            self.pgops.add(dbops.DropTable(name=old_table_name))\n\n        if types.has_table(prop, orig_schema):\n            old_table_name = self._get_table_name(prop, orig_schema)\n            self.pgops.add(dbops.DropTable(name=old_table_name))\n            self.schedule_endpoint_delete_action_update(\n                prop, orig_schema, schema, context)\n\n\nclass CreateProperty(PropertyMetaCommand, adapts=s_props.CreateProperty):\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        schema = super()._create_begin(schema, context)\n        prop = self.scls\n\n        src = context.get(s_sources.SourceCommandContext)\n\n        self.table_name = self._get_table_name(prop, schema)\n\n        self._create_property(prop, src, schema, orig_schema, context)\n        self.schedule_trampoline(self.scls, schema, context)\n\n        return schema\n\n\nclass RenameProperty(PropertyMetaCommand, adapts=s_props.RenameProperty):\n    pass\n\n\nclass RebaseProperty(PropertyMetaCommand, adapts=s_props.RebaseProperty):\n    def _alter_innards(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = context.current().original_schema\n        if types.has_table(self.scls, schema):\n            self.update_if_cfg_view(schema, context, self.scls)\n\n        schema = super()._alter_innards(schema, context)\n\n        if not self.scls.is_pure_computable(schema):\n            self.schedule_endpoint_delete_action_update(\n                self.scls, orig_schema, schema, context)\n\n        return schema\n\n\nclass SetPropertyType(PropertyMetaCommand, adapts=s_props.SetPropertyType):\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        schema = super()._alter_begin(schema, context)\n        orig_type = self.scls.get_target(orig_schema)\n        new_type = self.scls.get_target(schema)\n        if (\n            types.has_table(self.scls.get_source(schema), schema)\n            and not self.scls.is_pure_computable(schema)\n            and not self.scls.is_endpoint_pointer(schema)\n            and (orig_type != new_type or self.cast_expr is not None)\n        ):\n            self._alter_pointer_type(self.scls, schema, orig_schema, context)\n        return schema\n\n\nclass AlterPropertyUpperCardinality(\n    PropertyMetaCommand,\n    adapts=s_props.AlterPropertyUpperCardinality,\n):\n    def _alter_innards(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = context.current().original_schema\n\n        # We need to run the parent change *before* the children,\n        # or else the view update in the child might fail if a\n        # link table isn't created in the parent yet.\n        if (\n            not self.scls.is_non_concrete(schema)\n            and not self.scls.is_pure_computable(schema)\n            and not self.scls.is_endpoint_pointer(schema)\n            and types.has_table(self.scls.get_source(schema), schema)\n        ):\n            orig_card = self.scls.get_cardinality(orig_schema)\n            new_card = self.scls.get_cardinality(schema)\n            if orig_card != new_card:\n                self._alter_pointer_cardinality(schema, orig_schema, context)\n\n        return super()._alter_innards(schema, context)\n\n\nclass AlterPropertyLowerCardinality(\n    PropertyMetaCommand,\n    adapts=s_props.AlterPropertyLowerCardinality,\n):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        schema = super().apply(schema, context)\n\n        if not self.scls.is_non_concrete(schema):\n            orig_required = self.scls.get_required(orig_schema)\n            new_required = self.scls.get_required(schema)\n            if (\n                types.has_table(self.scls.get_source(schema), schema)\n                and not self.scls.is_endpoint_pointer(schema)\n                and not self.scls.is_pure_computable(schema)\n                and orig_required != new_required\n            ):\n                self._alter_pointer_optionality(\n                    schema, orig_schema, context, fill_expr=self.fill_expr)\n\n        return schema\n\n\nclass AlterPropertyOwned(\n    PropertyMetaCommand,\n    adapts=s_props.AlterPropertyOwned,\n):\n    pass\n\n\nclass AlterProperty(PropertyMetaCommand, adapts=s_props.AlterProperty):\n    def _alter_innards(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        prop = self.scls\n        orig_schema = context.current().original_schema\n\n        src = context.get(s_sources.SourceCommandContext)\n        is_comp = prop.is_pure_computable(schema)\n        was_comp = prop.is_pure_computable(orig_schema)\n\n        if src and (was_comp and not is_comp):\n            self._create_property(prop, src, schema, orig_schema, context)\n\n        schema = super()._alter_innards(schema, context)\n\n        if src and (not was_comp and is_comp):\n            self._delete_property(\n                prop, src.scls, src.op, schema, orig_schema, context)\n\n        if self.metadata_only:\n            return schema\n\n        if (\n            not is_comp\n            and (src and types.has_table(src.scls, schema))\n        ):\n            orig_def_val = self.get_pointer_default(prop, orig_schema, context)\n            def_val = self.get_pointer_default(prop, schema, context)\n\n            if orig_def_val != def_val:\n                if prop.get_cardinality(schema).is_multi():\n                    source_op: sd.Command = self\n                else:\n                    source_op = not_none(context.get_ancestor(\n                        s_sources.SourceCommandContext, self)).op\n\n                assert isinstance(source_op, CompositeMetaCommand)\n                alter_table = source_op.get_alter_table(\n                    schema, context, manual=True)\n\n                ptr_stor_info = types.get_pointer_storage_info(\n                    prop, schema=schema)\n                alter_table.add_operation(\n                    dbops.AlterTableAlterColumnDefault(\n                        column_name=ptr_stor_info.column_name,\n                        default=def_val))\n\n                self.pgops.add(alter_table)\n\n            card = self.get_resolved_attribute_value(\n                'cardinality',\n                schema=schema,\n                context=context,\n            )\n            if card:\n                self.schedule_endpoint_delete_action_update(\n                    prop, orig_schema, schema, context)\n\n        return schema\n\n\nclass DeleteProperty(PropertyMetaCommand, adapts=s_props.DeleteProperty):\n\n    def _delete_innards(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._delete_innards(schema, context)\n        prop = self.scls\n        orig_schema = context.current().original_schema\n\n        source_ctx = self.get_referrer_context(context)\n        if source_ctx is not None:\n            source = source_ctx.scls\n            source_op = source_ctx.op\n        else:\n            source = None\n            source_op = None\n\n        if source and not prop.is_pure_computable(schema):\n            assert isinstance(source, s_sources.Source)\n            self._delete_property(\n                prop, source, source_op, schema, orig_schema, context)\n\n        return schema\n\n\nclass CreateTrampolines(MetaCommand):\n    def __init__(self, **kwargs) -> None:\n        super().__init__(**kwargs)\n        self.trampolines: list[trampoline.Trampoline] = []\n        self.table_targets: list[s_objtypes.ObjectType | s_pointers.Pointer] = (\n            []\n        )\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        for obj in self.table_targets:\n            if not (schema.has_object(obj.id) and types.has_table(obj, schema)):\n                continue\n            if tramp := CompositeMetaCommand.create_type_trampoline(\n                schema, obj\n            ):\n                self.trampolines.append(tramp)\n\n        for t in self.trampolines:\n            self.pgops.add(t.make())\n\n        return schema\n\n\nclass UpdateEndpointDeleteActions(MetaCommand):\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        self.link_ops = []\n        self.changed_targets = set()\n\n    def _get_link_table_union(self, schema, links) -> str:\n        selects = []\n        for link in links:\n            selects.append(textwrap.dedent('''\\\n                (SELECT\n                    {id}::uuid AS __sobj_id__,\n                    {src} as source,\n                    {tgt} as target\n                FROM {table})\n            ''').format(\n                id=ql(str(link.id)),\n                src=common.quote_ident('source'),\n                tgt=common.quote_ident('target'),\n                table=common.get_backend_name(\n                    schema,\n                    link,\n                ),\n            ))\n\n        return '(' + '\\nUNION ALL\\n    '.join(selects) + ') as q'\n\n    def _get_inline_link_table_union(\n        self, schema, links\n    ) -> str:\n        selects = []\n        for link in links:\n            link_psi = types.get_pointer_storage_info(link, schema=schema)\n            link_col = link_psi.column_name\n            selects.append(textwrap.dedent('''\\\n                (SELECT\n                    {id}::uuid AS __sobj_id__,\n                    {src} as source,\n                    {tgt} as target\n                FROM {table})\n            ''').format(\n                id=ql(str(link.id)),\n                src=common.quote_ident('id'),\n                tgt=common.quote_ident(link_col),\n                table=common.get_backend_name(\n                    schema,\n                    link.get_source(schema),\n                ),\n            ))\n\n        return '(' + '\\nUNION ALL\\n    '.join(selects) + ') as q'\n\n    def get_target_objs(self, link, schema):\n        tgt = link.get_target(schema)\n        if union := tgt.get_union_of(schema).objects(schema):\n            objs = set(union)\n        else:\n            objs = {tgt}\n        objs |= {\n            x for obj in objs for x in obj.descendants(schema)}\n        return {\n            obj for obj in objs\n            if (\n                not obj.is_view(schema)\n                and not irtyputils.is_cfg_view(obj, schema)\n            )\n        }\n\n    def get_orphan_link_ancestors(self, link, schema):\n        val = s_links.LinkSourceDeleteAction.DeleteTargetIfOrphan\n        if link.get_on_source_delete(schema) != val:\n            return set()\n        ancestors = {\n            x\n            for base in link.get_bases(schema).objects(schema)\n            for x in self.get_orphan_link_ancestors(base, schema)\n        }\n        if ancestors:\n            return ancestors\n        else:\n            return {link}\n\n    def get_trigger_name(\n        self, schema, target, disposition, deferred=False, inline=False\n    ):\n        if disposition == 'target':\n            aspect = 'target-del'\n        else:\n            aspect = 'source-del'\n\n        if deferred:\n            aspect += '-def'\n        else:\n            aspect += '-imm'\n\n        if inline:\n            aspect += '-inl'\n        else:\n            aspect += '-otl'\n\n        aspect += '-t'\n\n        # Postgres applies triggers in alphabetical order, and\n        # the names are uuids, which are not useful here.\n        #\n        # All we want for now is for source triggers to apply first,\n        # though, so that a loop of objects with\n        # 'on source delete delete target' + 'on target delete restrict'\n        # succeeds.\n        #\n        # Fortunately S comes before T.\n        order_prefix = disposition[0]\n\n        name = common.get_backend_name(schema, target, catenate=False)\n        return f'{order_prefix}_{name[1]}_{aspect}'\n\n    def get_trigger_proc_name(\n        self, schema, target, disposition, deferred=False, inline=False\n    ):\n        if disposition == 'target':\n            aspect = 'target-del'\n        else:\n            aspect = 'source-del'\n\n        if deferred:\n            aspect += '-def'\n        else:\n            aspect += '-imm'\n\n        if inline:\n            aspect += '-inl'\n        else:\n            aspect += '-otl'\n\n        aspect += '-f'\n\n        name = common.get_backend_name(schema, target, catenate=False)\n        return (name[0], f'{name[1]}_{aspect}')\n\n    def get_trigger_proc_text(\n        self, target, links, *, disposition, inline, schema, context,\n    ):\n        if inline:\n            return self._get_inline_link_trigger_proc_text(\n                target, links, disposition=disposition,\n                schema=schema, context=context)\n        else:\n            return self._get_outline_link_trigger_proc_text(\n                target, links, disposition=disposition,\n                schema=schema, context=context)\n\n    def _get_outline_link_trigger_proc_text(\n        self, target, links, *, disposition, schema, context\n    ):\n\n        chunks = []\n\n        DA = s_links.LinkTargetDeleteAction\n\n        if disposition == 'target':\n            groups = itertools.groupby(\n                links, lambda l: l.get_on_target_delete(schema))\n            near_endpoint, far_endpoint = 'target', 'source'\n        else:\n            groups = itertools.groupby(\n                links, lambda l: (\n                    l.get_on_source_delete(schema)\n                    if isinstance(l, s_links.Link)\n                    else s_links.LinkSourceDeleteAction.Allow))\n            near_endpoint, far_endpoint = 'source', 'target'\n\n        for action, links in groups:\n            if action is DA.Restrict or action is DA.DeferredRestrict:\n                tables = self._get_link_table_union(schema, links)\n\n                # We want versioned for stdlib (since the trampolines\n                # don't exist yet) but trampolined for user code\n                prefix = 'edgedb_VER' if context.stdmode else 'edgedb'\n\n                text = textwrap.dedent(trampoline.fixup_query('''\\\n                    SELECT\n                        q.__sobj_id__, q.source, q.target\n                        INTO link_type_id, srcid, tgtid\n                    FROM\n                        {tables}\n                    WHERE\n                        q.{near_endpoint} = OLD.{id}\n                    LIMIT 1;\n\n                    IF FOUND THEN\n                        SELECT\n                            {prefix}.shortname_from_fullname(link.name),\n                            {prefix}._get_schema_object_name(\n                                link.{far_endpoint})\n                            INTO linkname, endname\n                        FROM\n                            {prefix}._schema_links AS link\n                        WHERE\n                            link.id = link_type_id;\n                        RAISE foreign_key_violation\n                            USING\n                                TABLE = TG_TABLE_NAME,\n                                SCHEMA = TG_TABLE_SCHEMA,\n                                MESSAGE = 'deletion of {tgtname} (' || tgtid\n                                    || ') is prohibited by link target policy',\n                                DETAIL = 'Object is still referenced in link '\n                                    || linkname || ' of ' || endname || ' ('\n                                    || srcid || ').';\n                    END IF;\n                '''.format(\n                    tables=tables,\n                    id='id',\n                    tgtname=target.get_displayname(schema),\n                    near_endpoint=near_endpoint,\n                    far_endpoint=far_endpoint,\n                    prefix=prefix,\n                )))\n\n                chunks.append(text)\n\n            elif (\n                action == s_links.LinkTargetDeleteAction.Allow\n                or action == s_links.LinkSourceDeleteAction.Allow\n            ):\n                for link in links:\n                    link_table = common.get_backend_name(\n                        schema, link)\n\n                    # Since enforcement of 'required' on multi links\n                    # is enforced manually on the query side and (not\n                    # through constraints/triggers of its own), we\n                    # also need to do manual enforcement of it when\n                    # deleting a required multi link.\n                    if link.get_required(schema) and disposition == 'target':\n                        required_text = textwrap.dedent('''\\\n                            SELECT q.source INTO srcid\n                            FROM {link_table} as q\n                                WHERE q.target = OLD.{id}\n                                AND NOT EXISTS (\n                                    SELECT FROM {link_table} as q2\n                                    WHERE q.source = q2.source\n                                          AND q2.target != OLD.{id}\n                                );\n\n                            IF FOUND THEN\n                                RAISE not_null_violation\n                                    USING\n                                        TABLE = TG_TABLE_NAME,\n                                        SCHEMA = TG_TABLE_SCHEMA,\n                                        MESSAGE = 'missing value',\n                                        COLUMN = '{link_id}';\n                            END IF;\n                        ''').format(\n                            link_table=link_table,\n                            link_id=str(link.id),\n                            id='id'\n                        )\n\n                        chunks.append(required_text)\n\n                    # Otherwise just delete it from the link table.\n                    text = textwrap.dedent('''\\\n                        DELETE FROM\n                            {link_table}\n                        WHERE\n                            {endpoint} = OLD.{id};\n                    ''').format(\n                        link_table=link_table,\n                        endpoint=common.quote_ident(near_endpoint),\n                        id='id'\n                    )\n\n                    chunks.append(text)\n\n            elif action == s_links.LinkTargetDeleteAction.DeleteSource:\n                sources = collections.defaultdict(list)\n                for link in links:\n                    sources[link.get_source(schema)].append(link)\n\n                for source, source_links in sources.items():\n                    tables = self._get_link_table_union(schema, source_links)\n\n                    text = textwrap.dedent('''\\\n                        DELETE FROM\n                            {source_table}\n                        WHERE\n                            {source_table}.{id} IN (\n                                SELECT source\n                                FROM {tables}\n                                WHERE target = OLD.{id}\n                            );\n                    ''').format(\n                        source_table=common.get_backend_name(schema, source),\n                        id='id',\n                        tables=tables,\n                    )\n\n                    chunks.append(text)\n\n            elif (\n                action == s_links.LinkSourceDeleteAction.DeleteTarget\n                or action ==\n                    s_links.LinkSourceDeleteAction.DeleteTargetIfOrphan\n            ):\n                for link in links:\n                    link_table = common.get_backend_name(schema, link)\n                    objs = self.get_target_objs(link, schema)\n\n                    # If the link is DELETE TARGET IF ORPHAN, build\n                    # filters to ignore any objects that aren't\n                    # orphans (wrt to this link).\n                    roots = {\n                        x\n                        for root in self.get_orphan_link_ancestors(link, schema)\n                        for x in [root, *root.descendants(schema)]\n                    }\n\n                    orphan_check = ''\n                    for orphan_check_root in roots:\n                        if not types.has_table(orphan_check_root, schema):\n                            continue\n                        check_table = common.get_backend_name(\n                            schema, orphan_check_root\n                        )\n                        orphan_check += f'''\\\n                            AND NOT EXISTS (\n                                SELECT FROM {check_table} as q2\n                                WHERE q.target = q2.target\n                                      AND q2.source != OLD.id\n                            )\n                        '''.strip()\n\n                    # We find all the objects to delete in a CTE, then\n                    # delete the link table entries, and then delete\n                    # the targets. We apply the non-orphan filter when\n                    # finding the objects.\n                    prefix = textwrap.dedent(f'''\\\n                        WITH range AS (\n                            SELECT target FROM {link_table} as q\n                            WHERE q.source = OLD.id\n                            {orphan_check}\n                        ),\n                        del AS (\n                            DELETE FROM\n                                {link_table}\n                            WHERE\n                                source = OLD.id\n                        )\n                    ''').strip()\n                    parts = [prefix]\n\n                    for i, obj in enumerate(objs):\n                        tgt_table = common.get_backend_name(schema, obj)\n                        text = textwrap.dedent(f'''\\\n                            d{i} AS (\n                                DELETE FROM\n                                    {tgt_table}\n                                WHERE\n                                    {tgt_table}.id IN (\n                                        SELECT target\n                                        FROM range\n                                    )\n                            )\n                        ''').strip()\n                        parts.append(text)\n\n                    full = ',\\n'.join(parts) + \"\\nSELECT '' INTO _dummy_text;\"\n                    chunks.append(full)\n\n        text = textwrap.dedent('''\\\n            DECLARE\n                link_type_id uuid;\n                srcid uuid;\n                tgtid uuid;\n                linkname text;\n                endname text;\n                _dummy_text text;\n            BEGIN\n                {chunks}\n                RETURN OLD;\n            END;\n        ''').format(chunks='\\n\\n'.join(chunks))\n\n        return text\n\n    def _get_inline_link_trigger_proc_text(\n        self, target, links, *, disposition, schema, context\n    ):\n\n        chunks = []\n\n        DA = s_links.LinkTargetDeleteAction\n\n        if disposition == 'target':\n            groups = itertools.groupby(\n                links, lambda l: l.get_on_target_delete(schema))\n        else:\n            groups = itertools.groupby(\n                links, lambda l: l.get_on_source_delete(schema))\n\n        near_endpoint, far_endpoint = 'target', 'source'\n\n        for action, links in groups:\n            if action is DA.Restrict or action is DA.DeferredRestrict:\n                tables = self._get_inline_link_table_union(schema, links)\n\n                # We want versioned for stdlib (since the trampolines\n                # don't exist yet) but trampolined for user code\n                prefix = 'edgedb_VER' if context.stdmode else 'edgedb'\n\n                text = textwrap.dedent(trampoline.fixup_query('''\\\n                    SELECT\n                        q.__sobj_id__, q.source, q.target\n                        INTO link_type_id, srcid, tgtid\n                    FROM\n                        {tables}\n                    WHERE\n                        q.{near_endpoint} = OLD.{id}\n                    LIMIT 1;\n\n                    IF FOUND THEN\n                        SELECT\n                            {prefix}.shortname_from_fullname(link.name),\n                            {prefix}._get_schema_object_name(\n                                link.{far_endpoint})\n                            INTO linkname, endname\n                        FROM\n                            {prefix}._schema_links AS link\n                        WHERE\n                            link.id = link_type_id;\n                        RAISE foreign_key_violation\n                            USING\n                                TABLE = TG_TABLE_NAME,\n                                SCHEMA = TG_TABLE_SCHEMA,\n                                MESSAGE = 'deletion of {tgtname} (' || tgtid\n                                    || ') is prohibited by link target policy',\n                                DETAIL = 'Object is still referenced in link '\n                                    || linkname || ' of ' || endname || ' ('\n                                    || srcid || ').';\n                    END IF;\n                '''.format(\n                    tables=tables,\n                    id='id',\n                    tgtname=target.get_displayname(schema),\n                    near_endpoint=near_endpoint,\n                    far_endpoint=far_endpoint,\n                    prefix=prefix,\n                )))\n\n                chunks.append(text)\n\n            elif action == s_links.LinkTargetDeleteAction.Allow:\n                for link in links:\n                    link_psi = types.get_pointer_storage_info(\n                        link, schema=schema)\n                    link_col = link_psi.column_name\n                    source_table = common.get_backend_name(\n                        schema, link.get_source(schema))\n\n                    text = textwrap.dedent(f'''\\\n                        UPDATE\n                            {source_table}\n                        SET\n                            {qi(link_col)} = NULL\n                        WHERE\n                            {qi(link_col)} = OLD.id;\n                    ''')\n\n                    chunks.append(text)\n\n            elif action == s_links.LinkTargetDeleteAction.DeleteSource:\n                sources = collections.defaultdict(list)\n                for link in links:\n                    sources[link.get_source(schema)].append(link)\n\n                for source, source_links in sources.items():\n                    tables = self._get_inline_link_table_union(\n                        schema, source_links)\n\n                    text = textwrap.dedent('''\\\n                        DELETE FROM\n                            {source_table}\n                        WHERE\n                            {source_table}.{id} IN (\n                                SELECT source\n                                FROM {tables}\n                                WHERE target = OLD.{id}\n                            );\n                    ''').format(\n                        source_table=common.get_backend_name(schema, source),\n                        id='id',\n                        tables=tables,\n                    )\n\n                    chunks.append(text)\n\n            elif (\n                action == s_links.LinkSourceDeleteAction.DeleteTarget\n                or action ==\n                    s_links.LinkSourceDeleteAction.DeleteTargetIfOrphan\n            ):\n                for link in links:\n                    objs = self.get_target_objs(link, schema)\n\n                    link_psi = types.get_pointer_storage_info(\n                        link, schema=schema)\n                    link_col = common.quote_ident(link_psi.column_name)\n\n                    # If the link is DELETE TARGET IF ORPHAN, filter out\n                    # any objects that aren't orphans (wrt to this link).\n                    roots = {\n                        x\n                        for root in self.get_orphan_link_ancestors(link, schema)\n                        for x in [root, *root.descendants(schema)]\n                    }\n\n                    orphan_check = ''\n                    for orphan_check_root in roots:\n                        check_source = orphan_check_root.get_source(schema)\n                        if not types.has_table(check_source, schema):\n                            continue\n                        check_table = common.get_backend_name(\n                            schema, check_source\n                        )\n\n                        check_link_psi = types.get_pointer_storage_info(\n                            orphan_check_root, schema=schema)\n                        check_link_col = common.quote_ident(\n                            check_link_psi.column_name)\n\n                        orphan_check += f'''\\\n                            AND NOT EXISTS (\n                                SELECT FROM {check_table} as q2\n                                WHERE q2.{check_link_col} = OLD.{link_col}\n                                      AND q2.id != OLD.id\n                            )\n                        '''.strip()\n\n                    # Do the orphan check (which trivially succeeds if\n                    # the link isn't IF ORPHAN)\n                    text = textwrap.dedent(f'''\\\n                        SELECT (\n                            SELECT true\n                            {orphan_check}\n                        ) INTO ok;\n                    ''').strip()\n\n                    chunks.append(text)\n                    for obj in objs:\n                        tgt_table = common.get_backend_name(schema, obj)\n                        text = textwrap.dedent(f'''\\\n                            IF ok THEN\n                                DELETE FROM\n                                    {tgt_table}\n                                WHERE\n                                    {tgt_table}.id = OLD.{link_col};\n                            END IF;\n                        ''')\n                        chunks.append(text)\n\n        text = textwrap.dedent('''\\\n            DECLARE\n                link_type_id uuid;\n                srcid uuid;\n                tgtid uuid;\n                linkname text;\n                endname text;\n                ok bool;\n                links text[];\n            BEGIN\n                {chunks}\n                RETURN OLD;\n            END;\n        ''').format(chunks='\\n\\n'.join(chunks))\n\n        return text\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        if not self.link_ops and not self.changed_targets:\n            return schema\n\n        DA = s_links.LinkTargetDeleteAction\n        DS = s_links.LinkSourceDeleteAction\n\n        affected_sources: set[s_sources.Source] = set()\n        affected_targets = {t for _, t in self.changed_targets}\n        modifications = any(\n            isinstance(op, RebaseObjectType) and op.removed_bases\n            for op, _ in self.changed_targets\n        )\n\n        for link_op, link, orig_schema, eff_schema in self.link_ops:\n            # Skip __type__ triggers, since __type__ isn't real and\n            # also would be a huge pain to update each time if it was.\n            if link.get_shortname(eff_schema).name == '__type__':\n                continue\n\n            if (\n                isinstance(link_op, (DeleteProperty, DeleteLink))\n                or (\n                    link.is_pure_computable(eff_schema)\n                    and not link.is_pure_computable(orig_schema)\n                )\n            ):\n                source = link.get_source(orig_schema)\n                if source:\n                    current_source = schema.get_by_id(source.id, None)\n                    if (current_source is not None\n                            and not current_source.is_view(schema)):\n                        modifications = True\n                        affected_sources.add(current_source)\n\n            if not eff_schema.has_object(link.id):\n                continue\n\n            target_is_affected = isinstance(link, s_links.Link)\n\n            if link.is_non_concrete(eff_schema) or (\n                link.is_pure_computable(eff_schema)\n                and link.is_pure_computable(orig_schema)\n            ):\n                continue\n\n            source = link.get_source(eff_schema)\n            target = link.get_target(eff_schema)\n\n            if (\n                not isinstance(source, s_objtypes.ObjectType)\n                or irtyputils.is_cfg_view(source, eff_schema)\n            ):\n                continue\n\n            if not isinstance(link_op, (CreateProperty, CreateLink)):\n                modifications = True\n\n            if isinstance(link_op, (DeleteProperty, DeleteLink)):\n                current_target = schema.get_by_id(target.id, None)\n                if target_is_affected and current_target is not None:\n                    affected_targets.add(current_target)\n            else:\n                if not source.is_material_object_type(eff_schema):\n                    continue\n\n                current_source = schema.get_by_id(source.id, None)\n                if current_source:\n                    affected_sources.add(current_source)\n\n                if target_is_affected:\n                    affected_targets.add(target)\n\n                if isinstance(link_op, (SetLinkType, SetPropertyType)):\n                    orig_target = link.get_target(orig_schema)\n                    if target != orig_target:\n                        current_orig_target = schema.get_by_id(\n                            orig_target.id, None)\n                        if current_orig_target is not None:\n                            affected_targets.add(current_orig_target)\n\n        # All descendants of affected targets also need to have their\n        # triggers updated, so track them down.\n        all_affected_targets = set()\n        for target in affected_targets:\n            union_of = target.get_union_of(schema)\n            if union_of:\n                objtypes = tuple(union_of.objects(schema))\n            else:\n                objtypes = (target,)\n\n            for objtype in objtypes:\n                all_affected_targets.add(objtype)\n                for descendant in objtype.descendants(schema):\n                    if types.has_table(descendant, schema):\n                        all_affected_targets.add(descendant)\n\n        delete_target_targets = set()\n\n        for target in all_affected_targets:\n            if irtyputils.is_cfg_view(target, schema):\n                continue\n\n            deferred_links = []\n            deferred_inline_links = []\n            links = []\n            inline_links = []\n\n            inbound_links = schema.get_referrers(\n                target, scls_type=s_links.Link, field_name='target')\n\n            # We need to look at all inbound links to all ancestors\n            for ancestor in itertools.chain(\n                target.get_ancestors(schema).objects(schema),\n                schema.get_referrers(\n                    target, scls_type=s_objtypes.ObjectType,\n                    field_name='union_of'\n                ),\n            ):\n                inbound_links |= schema.get_referrers(\n                    ancestor, scls_type=s_links.Link, field_name='target')\n\n            for link in inbound_links:\n                if link.is_pure_computable(schema):\n                    continue\n\n                # Skip __type__ triggers, since __type__ isn't real and\n                # also would be a huge pain to update each time if it was.\n                if link.get_shortname(schema).name == '__type__':\n                    continue\n\n                source = link.get_source(schema)\n                if (\n                    not source.is_material_object_type(schema)\n                    or irtyputils.is_cfg_view(source, schema)\n                ):\n                    continue\n\n                # We need to track what objects are targets that can be\n                # deleted on a source delete; it feeds into a decision we\n                # need to make when handling source triggers below\n                if link.get_on_source_delete(schema) != DS.Allow:\n                    delete_target_targets.add(target)\n                    affected_sources.add(target)\n\n                action = link.get_on_target_delete(schema)\n\n                ptr_stor_info = types.get_pointer_storage_info(\n                    link, schema=schema)\n                if ptr_stor_info.table_type != 'link':\n                    if action is DA.DeferredRestrict:\n                        deferred_inline_links.append(link)\n                    else:\n                        inline_links.append(link)\n                else:\n                    if action is DA.DeferredRestrict:\n                        deferred_links.append(link)\n                    else:\n                        links.append(link)\n\n            # The ordering that we process links matters: Restrict\n            # must be processed *after* Allow and DeleteSource,\n            # because Restrict is applied (via views) to all\n            # descendant links regardless of whether they have been\n            # overridden, and so Allow and DeleteSource must be\n            # handled first.\n            ordering = (DA.Restrict, DA.Allow, DA.DeleteSource)\n\n            links.sort(\n                key=lambda l: (ordering.index(l.get_on_target_delete(schema)),\n                               l.get_name(schema)))\n\n            inline_links.sort(\n                key=lambda l: (ordering.index(l.get_on_target_delete(schema)),\n                               l.get_name(schema)))\n\n            deferred_links.sort(\n                key=lambda l: l.get_name(schema))\n\n            deferred_inline_links.sort(\n                key=lambda l: l.get_name(schema))\n\n            if links or modifications:\n                self._update_action_triggers(\n                    schema, context, target, links, disposition='target')\n\n            if inline_links or modifications:\n                self._update_action_triggers(\n                    schema, context, target, inline_links,\n                    disposition='target', inline=True)\n\n            if deferred_links or modifications:\n                self._update_action_triggers(\n                    schema, context, target, deferred_links,\n                    disposition='target', deferred=True)\n\n            if deferred_inline_links or modifications:\n                self._update_action_triggers(\n                    schema, context, target, deferred_inline_links,\n                    disposition='target', deferred=True,\n                    inline=True)\n\n        # Now process source targets\n        for source in affected_sources:\n            links = []\n            inline_links = []\n\n            can_be_deleted_by_trigger = any(\n                link.get_on_target_delete(schema) == DA.DeleteSource\n                for link in source.get_pointers(schema).objects(schema)\n                if isinstance(link, s_links.Link)\n            ) or source in delete_target_targets\n\n            for link in source.get_pointers(schema).objects(schema):\n                if link.is_pure_computable(schema):\n                    continue\n                ptr_stor_info = types.get_pointer_storage_info(\n                    link, schema=schema)\n\n                delete_target = (\n                    isinstance(link, s_links.Link)\n                    and link.get_on_source_delete(schema) != DS.Allow\n                )\n\n                if ptr_stor_info.table_type == 'link' and (\n                    # When a query does a delete, link tables get\n                    # cleared out explicitly in our SQL, and so we\n                    # don't need to run a source trigger unless there\n                    # is an interesting source delete policy.\n                    #\n                    # However, if the object might be deleted by a\n                    # link policy, then we still use a trigger to\n                    # clean up the link table, since handling it\n                    # in the original policy triggers would require\n                    # lots of pretty nonlocal changes (adding a link\n                    # to type Bar might require changing the triggers for\n                    # type Foo that links to Bar).\n                    can_be_deleted_by_trigger\n                    or delete_target\n                ):\n                    links.append(link)\n                # Inline links only need source actions if they might\n                # delete the target\n                elif delete_target:\n                    inline_links.append(link)\n\n            links.sort(\n                key=lambda l: (\n                    (l.get_on_target_delete(schema),)\n                    if isinstance(l, s_links.Link) else (),\n                    l.get_name(schema)))\n\n            inline_links.sort(\n                key=lambda l: (\n                    (l.get_on_target_delete(schema),)\n                    if isinstance(l, s_links.Link) else (),\n                    l.get_name(schema)))\n\n            if links or modifications:\n                self._update_action_triggers(\n                    schema, context, source, links, disposition='source')\n\n            if inline_links or modifications:\n                self._update_action_triggers(\n                    schema, context, source, inline_links,\n                    inline=True, disposition='source')\n\n        return schema\n\n    def _update_action_triggers(\n        self,\n        schema,\n        context: sd.CommandContext,\n        objtype: s_objtypes.ObjectType,\n        links: list[s_links.Link],\n        *,\n        disposition: str,\n        deferred: bool = False,\n        inline: bool = False,\n    ) -> None:\n\n        table_name = common.get_backend_name(schema, objtype, catenate=False)\n\n        trigger_name = self.get_trigger_name(\n            schema, objtype, disposition=disposition,\n            deferred=deferred, inline=inline)\n\n        proc_name = self.get_trigger_proc_name(\n            schema, objtype, disposition=disposition,\n            deferred=deferred, inline=inline)\n\n        trigger = dbops.Trigger(\n            name=trigger_name, table_name=table_name,\n            events=('delete',), procedure=proc_name,\n            is_constraint=True, inherit=True, deferred=deferred)\n\n        if links:\n            proc_text = self.get_trigger_proc_text(\n                objtype, links, disposition=disposition,\n                inline=inline, schema=schema, context=context)\n\n            trig_func = dbops.Function(\n                name=proc_name, text=proc_text, volatility='volatile',\n                returns='trigger', language='plpgsql')\n\n            self.pgops.add(dbops.CreateFunction(trig_func, or_replace=True))\n\n            self.pgops.add(dbops.CreateTrigger(\n                trigger, neg_conditions=[dbops.TriggerExists(\n                    trigger_name=trigger_name, table_name=table_name\n                )]\n            ))\n        else:\n            self.pgops.add(\n                dbops.DropTrigger(\n                    trigger,\n                    conditions=[dbops.TriggerExists(\n                        trigger_name=trigger_name,\n                        table_name=table_name,\n                    )]\n                )\n            )\n\n            self.pgops.add(\n                dbops.DropFunction(\n                    name=proc_name,\n                    args=[],\n                    conditions=[dbops.FunctionExists(\n                        name=proc_name,\n                        args=[],\n                    )]\n                )\n            )\n\n\nclass ModuleMetaCommand(MetaCommand):\n    pass\n\n\nclass CreateModule(ModuleMetaCommand, adapts=s_mod.CreateModule):\n    pass\n\n\nclass AlterModule(ModuleMetaCommand, adapts=s_mod.AlterModule):\n    pass\n\n\nclass DeleteModule(ModuleMetaCommand, adapts=s_mod.DeleteModule):\n    pass\n\n\nclass DatabaseMixin:\n    def ensure_has_create_database(self, backend_params):\n        if not backend_params.has_create_database:\n            self.pgops.add(\n                dbops.Query(\n                    f'''\n                    SELECT\n                        edgedb_VER.raise(\n                            NULL::uuid,\n                            msg => 'operation is not supported by the backend',\n                            exc => 'feature_not_supported'\n                        )\n                    INTO _dummy_text\n                    '''\n                )\n            )\n\n\nclass CreateDatabase(MetaCommand, DatabaseMixin, adapts=s_db.CreateBranch):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        backend_params = self._get_backend_params(context)\n        self.ensure_has_create_database(backend_params)\n\n        schema = super().apply(schema, context)\n        db = self.scls\n        tenant_id = self._get_tenant_id(context)\n        db_name = common.get_database_backend_name(\n            str(self.classname), tenant_id=tenant_id)\n        # We use the base template for SCHEMA and DATA branches, since we\n        # implement branches ourselves using pg_dump in order to avoid\n        # connection restrictions.\n        # For internal-only TEMPLATE branches, we use the source as\n        # the template.\n        template = (\n            self.template\n            if self.template and self.branch_type == ql_ast.BranchType.TEMPLATE\n            else edbdef.EDGEDB_TEMPLATE_DB\n        )\n        tpl_name = common.get_database_backend_name(\n            template, tenant_id=tenant_id)\n        self.pgops.add(\n            dbops.CreateDatabase(\n                dbops.Database(\n                    db_name,\n                    metadata=dict(\n                        id=str(db.id),\n                        tenant_id=tenant_id,\n                        builtin=self.get_attribute_value('builtin'),\n                    ),\n                ),\n                template=tpl_name,\n            )\n        )\n        return schema\n\n\nclass DropDatabase(MetaCommand, DatabaseMixin, adapts=s_db.DropBranch):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        backend_params = self._get_backend_params(context)\n        self.ensure_has_create_database(backend_params)\n\n        schema = super().apply(schema, context)\n        tenant_id = self._get_tenant_id(context)\n        db_name = common.get_database_backend_name(\n            str(self.classname), tenant_id=tenant_id)\n        self.pgops.add(dbops.DropDatabase(db_name))\n        return schema\n\n\nclass AlterDatabase(MetaCommand, DatabaseMixin, adapts=s_db.AlterBranch):\n    pass\n\n\nclass RenameDatabase(MetaCommand, DatabaseMixin, adapts=s_db.RenameBranch):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        backend_params = self._get_backend_params(context)\n        self.ensure_has_create_database(backend_params)\n\n        schema = super().apply(schema, context)\n        tenant_id = self._get_tenant_id(context)\n        db_name = common.get_database_backend_name(\n            str(self.classname), tenant_id=tenant_id)\n        new_name = common.get_database_backend_name(\n            str(self.new_name), tenant_id=tenant_id)\n        self.pgops.add(\n            dbops.RenameDatabase(\n                dbops.Database(\n                    new_name,\n                ),\n                old_name=db_name,\n            )\n        )\n        return schema\n\n\nclass RoleMixin:\n    def ensure_has_create_role(self, backend_params):\n        if not backend_params.has_create_role:\n            self.pgops.add(\n                dbops.Query(\n                    f'''\n                    SELECT\n                        edgedb_VER.raise(\n                            NULL::uuid,\n                            msg => 'operation is not supported by the backend',\n                            exc => 'feature_not_supported'\n                        )\n                    INTO _dummy_text\n                    '''\n                )\n            )\n\n\nclass CreateRole(MetaCommand, RoleMixin, adapts=s_roles.CreateRole):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        backend_params = self._get_backend_params(context)\n        self.ensure_has_create_role(backend_params)\n\n        schema = super().apply(schema, context)\n        role = self.scls\n\n        membership = [str(x) for x in role.get_bases(schema).names(schema)]\n        passwd = role.get_password(schema)\n        superuser_flag = False\n\n        members = set()\n\n        role_name = str(role.get_name(schema))\n        permissions: list[str] = list(sorted(\n            role.get_permissions(schema) or ()\n        ))\n        branches: list[str] = list(sorted(\n            role.get_branches(schema)\n        ))\n        apply_access_policies_pg_default = (\n            role.get_apply_access_policies_pg_default(schema)\n        )\n\n        instance_params = backend_params.instance_params\n        tenant_id = instance_params.tenant_id\n\n        if role.get_superuser(schema):\n            membership.append(edbdef.EDGEDB_SUPERGROUP)\n\n            # If the cluster is not exposing an explicit superuser role,\n            # we will make the created Postgres role superuser if we can\n            if not instance_params.base_superuser:\n                superuser_flag = backend_params.has_superuser_access\n\n        if backend_params.session_authorization_role is not None:\n            # When we connect to the backend via a proxy role, we\n            # must ensure that role is a member of _every_ EdgeDB\n            # role so that `SET ROLE` can work properly.\n            members.add(backend_params.session_authorization_role)\n\n        db_role = dbops.Role(\n            name=common.get_role_backend_name(role_name, tenant_id=tenant_id),\n            allow_login=True,\n            superuser=superuser_flag,\n            password=passwd,\n            membership=[\n                common.get_role_backend_name(parent_role, tenant_id=tenant_id)\n                for parent_role in membership\n            ],\n            metadata=dict(\n                id=str(role.id),\n                name=role_name,\n                tenant_id=tenant_id,\n                password_hash=passwd,\n                builtin=role.get_builtin(schema),\n                permissions=permissions,\n                branches=branches,\n                apply_access_policies_pg_default=(\n                    apply_access_policies_pg_default\n                ),\n            ),\n        )\n        self.pgops.add(dbops.CreateRole(db_role))\n        return schema\n\n\nclass AlterRole(MetaCommand, RoleMixin, adapts=s_roles.AlterRole):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n        role = self.scls\n\n        backend_params = self._get_backend_params(context)\n        instance_params = backend_params.instance_params\n        tenant_id = instance_params.tenant_id\n        role_name = str(role.get_name(schema))\n\n        kwargs = {}\n        update_metadata = False\n        metadata = dict(\n            id=str(role.id),\n            name=role_name,\n            tenant_id=tenant_id,\n            builtin=role.get_builtin(schema),\n            permissions=list(sorted(role.get_permissions(schema) or ())),\n            branches=list(sorted(role.get_branches(schema) or ())),\n            apply_access_policies_pg_default=(\n                role.get_apply_access_policies_pg_default(schema)\n            ),\n        )\n\n        if self.has_attribute_value('password'):\n            passwd = self.get_attribute_value('password')\n            if backend_params.has_create_role:\n                # Only modify Postgres password of roles managed by EdgeDB\n                kwargs['password'] = passwd\n\n            update_metadata = True\n            metadata['password_hash'] = passwd\n        elif old_passwd := role.get_password(schema):\n            if backend_params.has_create_role:\n                # Only modify Postgres password of roles managed by EdgeDB\n                kwargs['password'] = old_passwd\n            metadata['password_hash'] = old_passwd\n\n        if (\n            self.has_attribute_value('permissions')\n            or self.has_attribute_value('branches')\n            or self.has_attribute_value('apply_access_policies_pg_default')\n        ):\n            update_metadata = True\n\n        pg_role_name = common.get_role_backend_name(\n            role_name, tenant_id=tenant_id)\n        if self.has_attribute_value('superuser'):\n            self.ensure_has_create_role(backend_params)\n            membership = [str(x) for x in role.get_bases(schema).names(schema)]\n            membership.append(edbdef.EDGEDB_SUPERGROUP)\n            self.pgops.add(\n                dbops.AlterRoleAddMembership(\n                    name=pg_role_name,\n                    membership=[\n                        common.get_role_backend_name(\n                            parent_role, tenant_id=tenant_id)\n                        for parent_role in membership\n                    ],\n                )\n            )\n\n            superuser_flag = False\n\n            # If the cluster is not exposing an explicit superuser role,\n            # we will make the modified Postgres role superuser if we can\n            if not instance_params.base_superuser:\n                superuser_flag = backend_params.has_superuser_access\n\n            kwargs['superuser'] = superuser_flag\n\n        if update_metadata:\n            kwargs['metadata'] = metadata\n\n        if backend_params.has_create_role:\n            dbrole = dbops.Role(name=pg_role_name, **kwargs)\n        else:\n            dbrole = dbops.SingleRole(**kwargs)\n        self.pgops.add(dbops.AlterRole(dbrole))\n\n        return schema\n\n\nclass RebaseRole(MetaCommand, RoleMixin, adapts=s_roles.RebaseRole):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        backend_params = self._get_backend_params(context)\n        self.ensure_has_create_role(backend_params)\n\n        schema = super().apply(schema, context)\n        role = self.scls\n\n        tenant_id = self._get_tenant_id(context)\n\n        for dropped in self.removed_bases:\n            self.pgops.add(dbops.AlterRoleDropMember(\n                name=common.get_role_backend_name(\n                    str(dropped.name), tenant_id=tenant_id),\n                member=common.get_role_backend_name(\n                    str(role.get_name(schema)), tenant_id=tenant_id),\n            ))\n\n        for bases, _pos in self.added_bases:\n            for added in bases:\n                self.pgops.add(dbops.AlterRoleAddMember(\n                    name=common.get_role_backend_name(\n                        str(added.name), tenant_id=tenant_id),\n                    member=common.get_role_backend_name(\n                        str(role.get_name(schema)), tenant_id=tenant_id),\n                ))\n\n        return schema\n\n\nclass DeleteRole(MetaCommand, RoleMixin, adapts=s_roles.DeleteRole):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        backend_params = self._get_backend_params(context)\n        self.ensure_has_create_role(backend_params)\n\n        schema = super().apply(schema, context)\n        tenant_id = self._get_tenant_id(context)\n        self.pgops.add(dbops.DropRole(\n            common.get_role_backend_name(\n                str(self.classname), tenant_id=tenant_id)))\n        return schema\n\n\nclass CreateExtensionPackage(\n    MetaCommand,\n    adapts=s_exts.CreateExtensionPackage,\n):\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n\n        ext_id = str(self.scls.id)\n        name__internal = str(self.scls.get_name(schema))\n        name = self.scls.get_displayname(schema)\n        version = self.scls.get_version(schema)._asdict()\n        version['stage'] = version['stage'].name.lower()\n\n        ext_module = self.scls.get_ext_module(schema)\n        metadata = {\n            ext_id: {\n                'id': ext_id,\n                'name': name,\n                'name__internal': name__internal,\n                'script': self.scls.get_script(schema),\n                'version': version,\n                'builtin': self.scls.get_builtin(schema),\n                'internal': self.scls.get_internal(schema),\n                'ext_module': ext_module and str(ext_module),\n                'sql_extensions': list(self.scls.get_sql_extensions(schema)),\n                'sql_setup_script': self.scls.get_sql_setup_script(schema),\n                'sql_teardown_script': (\n                    self.scls.get_sql_teardown_script(schema)\n                ),\n                'dependencies': list(self.scls.get_dependencies(schema)),\n            }\n        }\n\n        ctx_backend_params = context.backend_runtime_params\n        if ctx_backend_params is not None:\n            backend_params = cast(\n                params.BackendRuntimeParams, ctx_backend_params)\n        else:\n            backend_params = params.get_default_runtime_params()\n\n        if backend_params.has_create_database:\n            self.pgops.add(\n                dbops.UpdateMetadataSection(\n                    dbops.DatabaseWithTenant(name=edbdef.EDGEDB_TEMPLATE_DB),\n                    section='ExtensionPackage',\n                    metadata=metadata\n                )\n            )\n        else:\n            self.pgops.add(\n                dbops.UpdateSingleDBMetadataSection(\n                    edbdef.EDGEDB_TEMPLATE_DB,\n                    section='ExtensionPackage',\n                    metadata=metadata\n                )\n            )\n\n        return schema\n\n\nclass DeleteExtensionPackage(\n    MetaCommand,\n    adapts=s_exts.DeleteExtensionPackage,\n):\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n\n        ctx_backend_params = context.backend_runtime_params\n        if ctx_backend_params is not None:\n            backend_params = cast(\n                params.BackendRuntimeParams, ctx_backend_params)\n        else:\n            backend_params = params.get_default_runtime_params()\n\n        ext_id = str(self.scls.id)\n        metadata = {\n            ext_id: None\n        }\n\n        if backend_params.has_create_database:\n            self.pgops.add(\n                dbops.UpdateMetadataSection(\n                    dbops.DatabaseWithTenant(name=edbdef.EDGEDB_TEMPLATE_DB),\n                    section='ExtensionPackage',\n                    metadata=metadata\n                )\n            )\n        else:\n            self.pgops.add(\n                dbops.UpdateSingleDBMetadataSection(\n                    edbdef.EDGEDB_TEMPLATE_DB,\n                    section='ExtensionPackage',\n                    metadata=metadata\n                )\n            )\n\n        return schema\n\n\nclass CreateExtensionPackageMigration(\n    MetaCommand,\n    adapts=s_exts.CreateExtensionPackageMigration,\n):\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n\n        ext_id = str(self.scls.id)\n        name__internal = str(self.scls.get_name(schema))\n        name = self.scls.get_displayname(schema)\n        from_version = self.scls.get_from_version(schema)._asdict()\n        from_version['stage'] = from_version['stage'].name.lower()\n        to_version = self.scls.get_to_version(schema)._asdict()\n        to_version['stage'] = to_version['stage'].name.lower()\n\n        metadata = {\n            ext_id: {\n                'id': ext_id,\n                'name': name,\n                'name__internal': name__internal,\n                'script': self.scls.get_script(schema),\n                'from_version': from_version,\n                'to_version': to_version,\n                'builtin': self.scls.get_builtin(schema),\n                'internal': self.scls.get_internal(schema),\n                'sql_early_script': self.scls.get_sql_early_script(schema),\n                'sql_late_script': self.scls.get_sql_late_script(schema),\n            }\n        }\n\n        ctx_backend_params = context.backend_runtime_params\n        if ctx_backend_params is not None:\n            backend_params = cast(\n                params.BackendRuntimeParams, ctx_backend_params)\n        else:\n            backend_params = params.get_default_runtime_params()\n\n        if backend_params.has_create_database:\n            self.pgops.add(\n                dbops.UpdateMetadataSection(\n                    dbops.DatabaseWithTenant(name=edbdef.EDGEDB_TEMPLATE_DB),\n                    section='ExtensionPackageMigration',\n                    metadata=metadata\n                )\n            )\n        else:\n            self.pgops.add(\n                dbops.UpdateSingleDBMetadataSection(\n                    edbdef.EDGEDB_TEMPLATE_DB,\n                    section='ExtensionPackageMigration',\n                    metadata=metadata\n                )\n            )\n\n        return schema\n\n\nclass DeleteExtensionPackageMigration(\n    MetaCommand,\n    adapts=s_exts.DeleteExtensionPackageMigration,\n):\n    # XXX: 100% duplication with DeleteExtensionPackage\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n\n        ctx_backend_params = context.backend_runtime_params\n        if ctx_backend_params is not None:\n            backend_params = cast(\n                params.BackendRuntimeParams, ctx_backend_params)\n        else:\n            backend_params = params.get_default_runtime_params()\n\n        ext_id = str(self.scls.id)\n        metadata = {\n            ext_id: None\n        }\n\n        if backend_params.has_create_database:\n            self.pgops.add(\n                dbops.UpdateMetadataSection(\n                    dbops.DatabaseWithTenant(name=edbdef.EDGEDB_TEMPLATE_DB),\n                    section='ExtensionPackageMigration',\n                    metadata=metadata\n                )\n            )\n        else:\n            self.pgops.add(\n                dbops.UpdateSingleDBMetadataSection(\n                    edbdef.EDGEDB_TEMPLATE_DB,\n                    section='ExtensionPackageMigration',\n                    metadata=metadata\n                )\n            )\n\n        return schema\n\n\nclass ExtensionCommand(MetaCommand):\n    def _compute_version(self, ext_spec: str) -> None:\n        '''Emits a Query to compute the version.\n\n        Dumps it in _dummy_text.\n        '''\n        ext, vclauses = _parse_spec(ext_spec)\n\n        # Dynamically select the highest version extension that matches\n        # the provided version specification.\n        lclauses = []\n        for op, ver in vclauses:\n            pver = f\"string_to_array({ql(ver)}, '.')::int8[]\"\n            assert op in {'=', '>', '>=', '<', '<='}\n            lclauses.append(f'v.split {op} {pver}')\n        cond = ' and '.join(lclauses) if lclauses else 'true'\n\n        ver_regexp = r'^\\d+(\\.\\d+)+$'\n        qry = textwrap.dedent(f'''\\\n            with v as (\n               select name, version,\n               string_to_array(version, '.')::int8[] as split\n               from pg_available_extension_versions\n               where\n                 name = {ql(ext)}\n                 and\n                 version ~ '{ver_regexp}'\n            )\n            select edgedb_VER.raise_on_null(\n              (\n                 select v.version from v\n                 where {cond}\n                 order by split desc limit 1\n              ),\n              'feature_not_supported',\n              msg => (\n                'could not find extension satisfying ' || {ql(ext_spec)}\n                || ': ' ||\n                coalesce(\n                  'only found versions ' ||\n                    (select string_agg(v.version, ', ' order by v.split)\n                     from v),\n                  'extension not found'\n                )\n              )\n            )\n            into _dummy_text;\n        ''')\n        self.pgops.add(dbops.Query(qry))\n\n    def _create_extension(self, ext_spec: str) -> None:\n        ext = _get_ext_name(ext_spec)\n        self._compute_version(ext_spec)\n\n        # XXX: hardcode to put stuff into edgedb schema\n        # so that operations can be easily accessed.\n        # N.B: this won't work on heroku; is that fine?\n        target_schema = 'edgedb'\n\n        self.pgops.add(dbops.Query(textwrap.dedent(f\"\"\"\\\n            EXECUTE\n              'CREATE EXTENSION {ext} WITH SCHEMA {target_schema} VERSION '''\n              || _dummy_text || ''''\n        \"\"\")))\n\n\ndef _get_ext_name(spec: str) -> str:\n    return spec.split(' ')[0]\n\n\ndef _parse_spec(spec: str) -> tuple[str, list[tuple[str, str]]]:\n    if ' ' not in spec:\n        return (spec, [])\n\n    ext, versions = spec.split(' ', 1)\n    clauses = versions.split(',')\n    pclauses = []\n    for clause in clauses:\n        for i in range(len(clause)):\n            if clause[i].isnumeric():\n                break\n        pclauses.append((clause[:i], clause[i:]))\n\n    return ext, pclauses\n\n\nclass CreateExtension(ExtensionCommand, adapts=s_exts.CreateExtension):\n\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._create_begin(schema, context)\n        # backend_params = self._get_backend_params(context)\n        # ext_schema = backend_params.instance_params.ext_schema\n\n        package = self.scls.get_package(schema)\n\n        for ext_spec in package.get_sql_extensions(schema):\n            self._create_extension(ext_spec)\n\n        if script := package.get_sql_setup_script(schema):\n            self.pgops.add(dbops.Query(script))\n\n        return schema\n\n    def _create_innards(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._create_innards(schema, context)\n\n        if str(self.classname) == \"ai\":\n            self.pgops.add(\n                delta_ext_ai.pg_rebuild_all_pending_embeddings_views(\n                    schema, context\n                ),\n            )\n\n        return schema\n\n\nclass AlterExtension(ExtensionCommand, adapts=s_exts.AlterExtension):\n    def _upgrade_extension(self, ext_spec: str) -> None:\n        ext = _get_ext_name(ext_spec)\n        self._compute_version(ext_spec)\n\n        self.pgops.add(dbops.Query(textwrap.dedent(f\"\"\"\\\n            EXECUTE\n              'ALTER EXTENSION {ext} UPDATE TO '''\n              || _dummy_text || ''''\n        \"\"\")))\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        old_package = self.scls.get_package(schema)\n        schema = super()._alter_begin(schema, context)\n        if not self.migration:\n            return schema\n\n        new_package = self.scls.get_package(schema)\n\n        old_exts = {\n            _get_ext_name(spec)\n            for spec in old_package.get_sql_extensions(schema)\n        }\n\n        new_exts = set()\n        # XXX: be smarter!\n        for ext_spec in new_package.get_sql_extensions(schema):\n            ext = _get_ext_name(ext_spec)\n            new_exts.add(ext)\n            if ext in old_exts:\n                self._upgrade_extension(ext_spec)\n            else:\n                self._create_extension(ext_spec)\n\n        # # XXX??? should do this after\n        # for ext in old_exts:\n        #     if ext not in new_exts:\n        #         self.pgops.add(\n        #             dbops.DropExtension(\n        #                 dbops.Extension(\n        #                     name=ext,\n        #                     schema=ext,\n        #                 ),\n        #             )\n        #         )\n\n        # TODO: UPDATE the sql extension? Or should we do that in the\n        # script?\n        if script := self.migration.get_sql_early_script(schema):\n            self.pgops.add(dbops.Query(script))\n\n        return schema\n\n    def _alter_finalize(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._alter_finalize(schema, context)\n\n        if (\n            self.migration\n            and (script := self.migration.get_sql_late_script(schema))\n        ):\n            self.pgops.add(dbops.Query(script))\n\n        return schema\n\n\nclass DeleteExtension(ExtensionCommand, adapts=s_exts.DeleteExtension):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        extension = schema.get_global(s_exts.Extension, self.classname)\n        package = extension.get_package(schema)\n\n        if str(self.classname) == \"ai\":\n            self.pgops.add(\n                delta_ext_ai.pg_drop_all_pending_embeddings_views(schema),\n            )\n\n        schema = super().apply(schema, context)\n\n        if script := package.get_sql_teardown_script(schema):\n            self.pgops.add(dbops.Query(script))\n\n        for ext_spec in package.get_sql_extensions(schema):\n            ext = _get_ext_name(ext_spec)\n\n            self.pgops.add(\n                dbops.DropExtension(\n                    dbops.Extension(\n                        name=ext,\n                        schema=ext,\n                    ),\n                )\n            )\n\n        return schema\n\n\nclass FutureBehaviorCommand(MetaCommand, s_futures.FutureBehaviorCommand):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n        if self.future_cmd:\n            self.pgops.add(self.future_cmd)\n        return schema\n\n\nclass CreateFutureBehavior(\n        FutureBehaviorCommand, adapts=s_futures.CreateFutureBehavior):\n    pass\n\n\nclass DeleteFutureBehavior(\n        FutureBehaviorCommand, adapts=s_futures.DeleteFutureBehavior):\n    pass\n\n\nclass AlterFutureBehavior(\n        FutureBehaviorCommand, adapts=s_futures.AlterFutureBehavior):\n    pass\n\n\nclass DeltaRoot(MetaCommand, adapts=sd.DeltaRoot):\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        self.config_ops = []\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        self.update_endpoint_delete_actions = UpdateEndpointDeleteActions()\n        self.create_trampolines = CreateTrampolines()\n\n        schema = super().apply(schema, context)\n\n        self.update_endpoint_delete_actions.apply(schema, context)\n        self.pgops.add(self.update_endpoint_delete_actions)\n\n        self.create_trampolines.apply(schema, context)\n\n        return schema\n\n\nclass MigrationCommand(MetaCommand):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n        if last_mig := schema.get_last_migration():\n            last_mig_name = last_mig.get_name(schema).name\n        else:\n            last_mig_name = None\n        self.pgops.add(dbops.UpdateMetadata(\n            dbops.CurrentDatabase(),\n            {'last_migration': last_mig_name},\n        ))\n        return schema\n\n\nclass CreateMigration(\n    MigrationCommand,\n    adapts=s_migrations.CreateMigration,\n):\n    pass\n\n\nclass AlterMigration(\n    MigrationCommand,\n    adapts=s_migrations.AlterMigration,\n):\n    pass\n\n\nclass DeleteMigration(\n    MigrationCommand,\n    adapts=s_migrations.DeleteMigration,\n):\n    pass\n"
  },
  {
    "path": "edb/pgsql/delta_ext_ai.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#\n# Backend support for ext::ai::index\n#\n# The index adds the following hidden attribute to the object type relation\n#\n#    __ext_ai_{idx_id}_embedding__ vector(<index_embedding_dimensions>)\n#\n# The data in the attribute gets populated by an external indexing process,\n# hence the ext::ai::index is currently always deferred.  If a given object\n# record is yet unindexed, the attribute value would be NULL and the entry\n# will be picked up in the work queue view (see below).\n#\n# To invalidate embeddings on changes of data referenced in the index\n# expression changes, a simple trigger is also added, which resets the\n# value of the embedding attribute back to NULL.\n#\n# The index is currently always deferred, hence the unindexed data\n# needs to be exposed conveniently to an external indexer.  We do\n# this here by creating the following internal SQL views:\n#\n# Enumeration of embedding models currently used in ext::ai::index()\n# declarations in the current schema:\n#\n#   CREATE VIEW edgedbext.ai_active_embedding_models(\n#     id,       -- generated unique id as int64 (could be used for locking)\n#     name,     -- model name as specified in the ext::ai::model_name anno\n#     provider, -- provider name as specified in the ext::ai::provider_name\n#   )\n#\n# For each active model in the above view the following views are also\n# generated:\n#\n#   CREATE VIEW edgedbext.\"ai_pending_embeddings_{model_name}\"(\n#     id,          -- Object ID\n#     text,        -- Indexed text document (result of index expr eval)\n#     target_rel,  -- SQL relation containing the embedding data\n#     target_attr, -- Column in the above relation containing embedding data\n#     target_dims_shortening -- If the embedding model produces more dimensions\n#                            -- than the underlying index can handle, this\n#                            -- would be the maximum dimensions supported by\n#                            -- the index.  Embedding model must support\n#                            -- vector shortening (e.g OpenAI\n#                            -- embedding-text-3- models).\n#  )\n#\n# The above view is a UNION of SELECTs over object relations, where each\n# UNION element is roughly this:\n#\n#   SELECT (\n#     Object.id,\n#     eval(get_index_expr(Object, 'ext::ai::index')),\n#   )\n#   WHERE\n#     eval(get_index_except_expr(Object, 'ext::ai::index')) IS NOT TRUE\n#     AND Object.__ext_ai_{idx_id}_embedding__  IS NULL\n#\n\nfrom __future__ import annotations\nfrom typing import (\n    cast,\n    Optional,\n)\n\nimport collections\nimport dataclasses\nimport hashlib\nimport struct\nimport textwrap\n\nfrom edb.schema import expr as s_expr\nfrom edb.schema import indexes as s_indexes\nfrom edb.schema import types as s_types\nfrom edb.schema import schema as s_schema\nfrom edb.schema import delta as sd\nfrom edb.schema import name as sn\nfrom edb.schema import properties as s_props\n\nfrom edb.ir import ast as irast\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import compiler as qlcompiler\n\nfrom . import codegen\nfrom . import common\nfrom . import deltadbops\nfrom . import dbops\nfrom . import compiler\nfrom . import types\nfrom . import ast as pgast\n\nfrom .common import qname as q\nfrom .common import quote_literal as ql\nfrom .common import quote_ident as qi\nfrom .compiler import astutils\nfrom .compiler import enums as pgce\n\n\nai_index_base_name = sn.QualName(\"ext::ai\", \"index\")\n\n\ndef get_ext_ai_pre_restore_script(\n    schema: s_schema.Schema,\n) -> str:\n    # We helpfully populate ext::ai::ChatPrompt with a starter prompt\n    # in the extension setup script.\n    # Unfortunately, this means that before user data is restored, we need\n    # to delete those objects, or there will be a constraint error.\n    return '''\n        delete {ext::ai::ChatPrompt, ext::ai::ChatPromptMessage}\n    '''\n\n\ndef create_ext_ai_index(\n    index: s_indexes.Index,\n    predicate_src: Optional[str],\n    sql_kwarg_exprs: dict[str, str],\n    options: qlcompiler.CompilerOptions,\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n) -> dbops.Command:\n    subject = index.get_subject(schema)\n    assert isinstance(subject, s_indexes.IndexableSubject)\n\n    effective, has_overridden = s_indexes.get_effective_object_index(\n        schema, subject, ai_index_base_name\n    )\n\n    if index != effective:\n        return dbops.CommandGroup()\n\n    # When creating an index on a child that already has an ext::ai index\n    # inherited from the parent, we don't need to create the index, but just\n    # update the populating expressions.\n    if has_overridden:\n        return _refresh_ai_embeddings(\n            index,\n            has_overridden[0],\n            options,\n            schema,\n            context,\n        )\n    else:\n        return _create_ai_embeddings(\n            index,\n            predicate_src,\n            sql_kwarg_exprs,\n            options,\n            schema,\n            context,\n        )\n\n\ndef delete_ext_ai_index(\n    index: s_indexes.Index,\n    drop_index: dbops.Command,\n    options: qlcompiler.CompilerOptions,\n    schema: s_schema.Schema,\n    orig_schema: s_schema.Schema,\n    context: sd.CommandContext,\n) -> tuple[dbops.Command, dbops.Command]:\n    subject = index.get_subject(orig_schema)\n    assert isinstance(subject, s_indexes.IndexableSubject)\n\n    effective, _ = s_indexes.get_effective_object_index(\n        schema, subject, ai_index_base_name\n    )\n\n    if not effective:\n        return _delete_ai_embeddings(\n            index, drop_index, schema, orig_schema, context)\n    else:\n        # effective index remains: don't drop the embeddings\n        return (\n            dbops.CommandGroup(),\n            _refresh_ai_embeddings(\n                effective,\n                index,\n                options,\n                orig_schema,\n                context,\n            ),\n        )\n\n\ndef _compile_ai_embeddings_source_view_expr(\n    index: s_indexes.Index,\n    options: qlcompiler.CompilerOptions,\n    schema: s_schema.Schema,\n) -> pgast.SelectStmt:\n    # Compile a view returning a set of (id, text-to-embed) tuples\n    # roughly as the following pseudo-QL\n    #\n    # FOR obj in Object UNION (\n    #   SELECT (\n    #     obj.id,\n    #     eval(get_index_expr(obj, 'ext::ai::index')),\n    #   )\n    #   WHERE\n    #     eval(get_index_except_expr(obj, 'ext::ai::index')) IS NOT TRUE\n    #     AND obj.embedding_column IS NULL\n    # )\n    index_sexpr: Optional[s_expr.Expression] = index.get_expr(schema)\n    assert index_sexpr\n\n    ql_iter_alias = \"__ext_ai_index_iter__\"\n    ql_index_sexpr_name = \"__ext_ai_index_sexpr__\"\n    ql = qlast.ForQuery(\n        iterator_alias=ql_iter_alias,\n        iterator=qlast.Shape(\n            expr=qlast.Path(\n                steps=[qlast.IRAnchor(name='__subject__')],\n            ),\n            elements=[\n                qlast.ShapeElement(\n                    expr=qlast.Path(\n                        steps=[qlast.Ptr(name=\"id\")],\n                    ),\n                ),\n                qlast.ShapeElement(\n                    expr=qlast.Path(\n                        steps=[qlast.Ptr(name=ql_index_sexpr_name)],\n                    ),\n                    compexpr=index_sexpr.parse(),\n                ),\n            ]\n        ),\n        result=qlast.Tuple(\n            elements=[\n                qlast.Path(\n                    steps=[\n                        qlast.ObjectRef(name=ql_iter_alias),\n                        qlast.Ptr(name=\"id\"),\n                    ],\n                ),\n                qlast.Path(\n                    steps=[\n                        qlast.ObjectRef(name=ql_iter_alias),\n                        qlast.Ptr(name=ql_index_sexpr_name),\n                    ],\n                ),\n            ],\n        ),\n    )\n\n    my_options = dataclasses.replace(options, singletons=frozenset())\n    ir = qlcompiler.compile_ast_to_ir(\n        ql,\n        schema=schema,\n        options=my_options,\n    )\n    assert isinstance(ir, irast.Statement)\n\n    subject = index.get_subject(schema)\n    assert isinstance(subject, s_types.Type)\n    subject_id = irast.PathId.from_type(schema, subject, env=None)\n\n    idx_id = _get_index_root_id(schema, index)\n    table_name = common.get_index_table_backend_name(index, schema)\n    aspects = (\n        pgce.PathAspect.IDENTITY,\n        pgce.PathAspect.VALUE,\n        pgce.PathAspect.SOURCE\n    )\n    qry = compiler.new_external_rvar_as_subquery(\n        rel_name=table_name,\n        path_id=subject_id,\n        aspects=aspects,\n    )\n    qry.where_clause = astutils.extend_binop(\n        qry.where_clause,\n        pgast.NullTest(\n            arg=pgast.ColumnRef(\n                name=(f\"__ext_ai_{idx_id}_embedding__\",),\n            ),\n        )\n    )\n\n    except_expr = index.get_except_expr(schema)\n    if except_expr:\n        except_expr = except_expr.ensure_compiled(\n            schema=schema,\n            options=options,\n            context=None,\n        )\n        assert except_expr.irast\n        except_res = compiler.compile_ir_to_sql_tree(\n            except_expr.irast.expr, singleton_mode=True)\n        assert isinstance(except_res.ast, pgast.BaseExpr)\n        qry.where_clause = astutils.extend_binop(\n            qry.where_clause,\n            pgast.Expr(\n                lexpr=except_res.ast,\n                name=\"IS NOT\",\n                rexpr=pgast.BooleanConstant(val=True),\n            ),\n        )\n\n    sql_res = compiler.compile_ir_to_sql_tree(\n        ir,\n        output_format=compiler.OutputFormat.NATIVE_INTERNAL,\n        external_rels={\n            subject_id: (qry, aspects),\n        },\n    )\n    expr = sql_res.ast\n    assert isinstance(expr, pgast.SelectStmt)\n\n    return expr\n\n\ndef _create_ai_embeddings(\n    index: s_indexes.Index,\n    predicate_src: Optional[str],\n    sql_kwarg_exprs: dict[str, str],\n    options: qlcompiler.CompilerOptions,\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n) -> dbops.Command:\n    return _pg_create_ai_embeddings(\n        index,\n        options,\n        predicate_src,\n        sql_kwarg_exprs,\n        schema,\n        context,\n    )\n\n\ndef _refresh_ai_embeddings(\n    index: s_indexes.Index,\n    old_index: s_indexes.Index,\n    options: qlcompiler.CompilerOptions,\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n) -> dbops.Command:\n    ops = dbops.CommandGroup()\n    table_name = common.get_index_table_backend_name(index, schema)\n    ops.add_command(\n        _pg_drop_trigger(index, table_name, schema))\n\n    idx_id = _get_index_root_id(schema, index)\n    ops.add_command(dbops.Query(textwrap.dedent(f\"\"\"\\\n        UPDATE {common.qname(*table_name)}\n        SET __ext_ai_{idx_id}_embedding__ = NULL\n        WHERE __ext_ai_{idx_id}_embedding__ IS NOT NULL\n    \"\"\")))\n\n    ops.add_command(\n        _pg_create_trigger(index, table_name, schema))\n    ops.add_command(\n        _pg_create_ai_embeddings_source_view(\n            index, options, schema, context))\n\n    # Sigh, we need to rename the main index to match the new id,\n    # entirely for the purpose of having ANALYZE be able to pick it up\n    dimensions = index.must_get_json_annotation(\n        schema,\n        sn.QualName(\"ext::ai\", \"embedding_dimensions\"),\n        int,\n    )\n    ops.add_command(\n        deltadbops.rename_pg_index(\n            old_index=old_index,\n            new_index=index,\n            schema=schema,\n            aspect=f'{dimensions}_index',\n        )\n    )\n\n    return ops\n\n\ndef _delete_ai_embeddings(\n    index: s_indexes.Index,\n    drop_index: dbops.Command,\n    schema: s_schema.Schema,\n    orig_schema: s_schema.Schema,\n    context: sd.CommandContext,\n) -> tuple[dbops.Command, dbops.Command]:\n    return _pg_delete_ai_embeddings(\n        index, drop_index, schema, orig_schema, context\n    )\n\n\n# --- pgvector ---\n\n\ndef _pg_create_ai_embeddings(\n    index: s_indexes.Index,\n    options: qlcompiler.CompilerOptions,\n    predicate_src: Optional[str],\n    sql_kwarg_exprs: dict[str, str],\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n) -> dbops.Command:\n    # Create:\n    # * the \"__ext_ai_{idx_id}_embedding__\" vector attribute;\n    # * pgvector index on the above;\n    # * the embedding attribute invalidation trigger\n    # * a component view for the \"ai_pending_embeddings_{model_name}\" union\n    ops = dbops.CommandGroup()\n\n    table_name = common.get_index_table_backend_name(index, schema)\n\n    with_clause = {}\n    kwargs = index.get_concrete_kwargs(schema)\n    index_params_expr = kwargs.get(\"index_parameters\")\n    if index_params_expr is not None:\n        index_params = index_params_expr.assert_compiled().as_python_value()\n        with_clause[\"m\"] = index_params[\"m\"]\n        with_clause[\"ef_construction\"] = index_params[\"ef_construction\"]\n\n    dimensions = index.must_get_json_annotation(\n        schema,\n        sn.QualName(\"ext::ai\", \"embedding_dimensions\"),\n        int,\n    )\n\n    idx_id = _get_index_root_id(schema, index)\n\n    alter_table = dbops.AlterTable(table_name)\n\n    # The attribute\n    alter_table.add_operation(\n        dbops.AlterTableAddColumn(\n            dbops.Column(\n                name=f'__ext_ai_{idx_id}_embedding__',\n                type=f'edgedb.vector({dimensions})',\n                required=False,\n            )\n        )\n    )\n\n    ops.add_command(alter_table)\n\n    # Also create a constant partial index on outdated entries\n    # so that we use an index scan and not a seq scan when\n    # picking out pending embeddings.\n    outdated_idx_name = common.get_index_table_backend_name(\n        index, schema, aspect=\"extaiselidx\")\n\n    ops.add_command(\n        dbops.CreateIndex(\n            dbops.Index(\n                name=outdated_idx_name[1],\n                table_name=table_name,\n                exprs=[\"(1)\"],\n                predicate=(\n                    f'__ext_ai_{idx_id}_embedding__ IS NULL'),\n                unique=False,\n                metadata={\n                    'code': '(__col__)',\n                },\n            ),\n        ),\n    )\n\n    df_expr = kwargs.get(\"distance_function\")\n    if df_expr is not None:\n        df = df_expr.assert_compiled().as_python_value()\n    else:\n        df = \"Cosine\"\n\n    match df:\n        case \"Cosine\":\n            opclass = \"vector_cosine_ops\"\n        case \"InnerProduct\":\n            opclass = \"vector_ip_ops\"\n        case \"L2\":\n            opclass = \"vector_l2_ops\"\n        case _:\n            raise RuntimeError(f\"unsupported distance_function: {df}\")\n\n    # The main similarity (a.k.a distance) search index.\n    module_name = index.get_name(schema).module\n    index_name = common.get_index_backend_name(\n        index.id, module_name, catenate=False, aspect=f'{dimensions}_index'\n    )\n\n    pg_index = dbops.Index(\n        name=index_name[1],\n        table_name=table_name,\n        exprs=[f\"__ext_ai_{idx_id}_embedding__\"],\n        with_clause=with_clause,\n        unique=False,\n        predicate=predicate_src,\n        metadata={\n            'schemaname': str(index.get_name(schema)),\n            'kwargs': sql_kwarg_exprs,\n            'code': f'hnsw (__col__ {opclass})',\n            'dimensions': str(dimensions),\n            'distance_function': str(df),\n        },\n    )\n    ops.add_command(dbops.CreateIndex(pg_index))\n\n    # The invalidation trigger\n    ops.add_command(_pg_create_trigger(index, table_name, schema))\n\n    # The component view for the \"ai_pending_embeddings_{model_name}\" union\n    ops.add_command(\n        _pg_create_ai_embeddings_source_view(index, options, schema, context))\n\n    return ops\n\n\ndef _get_dep_cols(\n    index: s_indexes.Index,\n    schema: s_schema.Schema,\n) -> list[str]:\n    index_expr = index.get_expr(schema)\n    assert index_expr is not None\n    dep_cols = []\n    assert index_expr.refs is not None\n    for obj in index_expr.refs.objects(schema):\n        if (\n            isinstance(obj, s_props.Property)\n            # Exclude computed pointers, they don't actually have columns\n            and obj.get_expr(schema) is None\n        ):\n            ptrinfo = types.get_pointer_storage_info(obj, schema=schema)\n            dep_cols.append(ptrinfo.column_name)\n\n    return dep_cols\n\n\ndef _pg_delete_ai_embeddings(\n    index: s_indexes.Index,\n    drop_index: dbops.Command,\n    schema: s_schema.Schema,\n    orig_schema: s_schema.Schema,\n    context: sd.CommandContext,\n) -> tuple[dbops.Command, dbops.Command]:\n    table_name = common.get_index_table_backend_name(index, orig_schema)\n    idx_id = _get_index_root_id(orig_schema, index)\n\n    table_ops = dbops.CommandGroup()\n\n    ops = dbops.CommandGroup()\n    ops.add_command(drop_index)\n\n    # Drop the invalidation trigger\n    ops.add_command(_pg_drop_trigger(index, table_name, orig_schema))\n    # Drop component view for the \"ai_pending_embeddings_{model_name}\" union\n    ops.add_command(_pg_drop_ai_embeddings_source_view(\n        index, schema, orig_schema, context))\n\n    # When the ObjectType is being deleted, we don't drop the index,\n    # as it will get dropped with the parent table.\n    # The same goes for __ext_ai_{idx_id}_embedding__.\n    source_drop = isinstance(drop_index, dbops.NoOpCommand)\n    if not source_drop:\n        table_name = common.get_index_table_backend_name(index, orig_schema)\n\n        alter_table = dbops.AlterTable(table_name)\n\n        alter_table.add_operation(\n            dbops.AlterTableDropColumn(\n                dbops.Column(\n                    name=f'__ext_ai_{idx_id}_embedding__',\n                    # This isn't actually needed to do the drop, and\n                    # it saves us needing to get the dimensions.\n                    # (Which is good, because they are missing when we\n                    # try to schema_repair to fix #9033.)\n                    type='XXX UNUSED',\n                )\n            )\n        )\n\n        table_ops.add_command(alter_table)\n\n    return ops, table_ops\n\n\ndef _pg_create_trigger(\n    index: s_indexes.Index,\n    table_name: tuple[str, str],\n    schema: s_schema.Schema,\n) -> dbops.Command:\n    dep_cols = _get_dep_cols(index, schema)\n\n    # Create a trigger that resets the __ext_ai_{idx_id}_embedding__ to\n    # NULL whenever data referenced in the ext::ai::index expression gets\n    # modified (TODO: the selective approach could also be used on\n    # std::fts::index)\n    ops = dbops.CommandGroup()\n    idx_id = _get_index_root_id(schema, index)\n\n    # create update function\n    func_name = _pg_update_func_name(table_name, idx_id)\n    function = dbops.Function(\n        name=func_name,\n        text=f\"\"\"\n        BEGIN\n            NEW.\"__ext_ai_{idx_id}_embedding__\" := NULL;\n            RETURN NEW;\n        END;\n        \"\"\",\n        volatility='immutable',\n        returns='trigger',\n        language='plpgsql',\n    )\n    ops.add_command(dbops.CreateFunction(function))\n\n    conditions = []\n    for dep_col in dep_cols:\n        dep_col = qi(dep_col)\n        conditions.append(f'OLD.{dep_col} IS DISTINCT FROM NEW.{dep_col}')\n\n    trigger_name = _pg_trigger_name(table_name[1], idx_id)\n    trigger = dbops.Trigger(\n        name=trigger_name,\n        table_name=table_name,\n        events=('update',),\n        timing=dbops.TriggerTiming.Before,\n        procedure=func_name,\n        condition=' OR '.join(conditions),\n    )\n    ops.add_command(dbops.CreateTrigger(trigger))\n    return ops\n\n\ndef _pg_drop_trigger(\n    index: s_indexes.Index,\n    table_name: tuple[str, str],\n    schema: s_schema.Schema,\n    override_id: Optional[str] = None,\n) -> dbops.Command:\n    idx_id = override_id or _get_index_root_id(schema, index)\n    ops = dbops.CommandGroup()\n\n    ops.add_command(\n        dbops.DropTrigger(\n            dbops.Trigger(\n                _pg_trigger_name(table_name[1], idx_id),\n                table_name=table_name,\n                events=(),\n                procedure='',\n            )\n        )\n    )\n\n    ops.add_command(\n        dbops.DropFunction(\n            _pg_update_func_name(table_name, idx_id),\n            (),\n        )\n    )\n    return ops\n\n\ndef pg_rebuild_all_pending_embeddings_views(\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n) -> dbops.Command:\n    ops = dbops.CommandGroup()\n\n    def flt(schema: s_schema.Schema, index: s_indexes.Index) -> bool:\n        return (\n            index.get_subject(schema) is not None\n            and s_indexes.is_ext_ai_index(schema, index)\n        )\n\n    all_ai_indexes = schema.get_objects(\n        type=s_indexes.Index,\n        extra_filters=(flt,),\n    )\n\n    all_models = s_indexes.get_defined_ext_ai_embedding_models(schema)\n\n    used_models = collections.defaultdict(list)\n    for other_index in all_ai_indexes:\n        if context.is_deleting(other_index):\n            continue\n\n        tabname = common.get_index_table_backend_name(\n            other_index, schema, aspect=\"extaiview\")\n        model_name = other_index.must_get_annotation(\n            schema, sn.QualName(\"ext::ai\", \"model_name\"))\n        used_models[model_name].append(f\"SELECT * FROM {q(*tabname)}\")\n\n    model_providers = {}\n\n    for model_name, model_stype in all_models.items():\n        views = used_models.get(model_name)\n\n        if views:\n            query = \" UNION ALL \".join(views)\n        else:\n            query = textwrap.dedent(\"\"\"\\\n                SELECT\n                    NULL::uuid    AS \"id\",\n                    NULL::text    AS \"text\",\n                    NULL::text    AS \"target_rel\",\n                    NULL::text    AS \"target_attr\",\n                    NULL::int     AS \"target_dims_shortening\",\n                    NULL::boolean AS \"truncate_to_max\"\n                WHERE\n                    FALSE\n            \"\"\")\n\n        view = dbops.View(\n            name=(\n                \"edgedbext\",\n                common.edgedb_name_to_pg_name(\n                    f\"ai_pending_embeddings_{model_name}\"\n                ),\n            ),\n            query=query,\n        )\n        ops.add_command(dbops.CreateView(view, or_replace=True))\n\n        provider = model_stype.must_get_annotation(\n            schema, sn.QualName(\"ext::ai\", \"model_provider\"))\n        model_providers[model_name] = provider\n\n    if used_models:\n        bits = []\n        for model_name in used_models:\n            mnhash = hashlib.blake2b(model_name.encode(\"utf-8\"), digest_size=8)\n            model_id: int = struct.unpack(\"q\", mnhash.digest())[0]\n\n            provider = model_providers[model_name]\n            bits.append(textwrap.dedent(f\"\"\"\\\n                SELECT\n                    {model_id}::bigint AS id,\n                    {ql(model_name)} AS name,\n                    {ql(provider)} AS provider\n            \"\"\"))\n        used_sql = \" UNION ALL \".join(bits)\n    else:\n        used_sql = textwrap.dedent(\"\"\"\\\n            SELECT\n                NULL::bigint AS id,\n                NULL::text AS name,\n                NULL::text AS provider\n            WHERE\n                FALSE\n        \"\"\")\n\n    ops.add_command(dbops.CreateView(\n        view=dbops.View(\n            name=(\"edgedbext\", \"ai_active_embedding_models\"),\n            query=used_sql,\n        ),\n        or_replace=True,\n    ))\n\n    return ops\n\n\ndef pg_drop_all_pending_embeddings_views(\n    schema: s_schema.Schema,\n) -> dbops.Command:\n    ops = dbops.CommandGroup()\n\n    all_models = s_indexes.get_defined_ext_ai_embedding_models(schema)\n    for model_name in all_models:\n        view_name = (\n            \"edgedbext\",\n            common.edgedb_name_to_pg_name(\n                f\"ai_pending_embeddings_{model_name}\"\n            ),\n        )\n        ops.add_command(dbops.DropView(view_name, conditional=True))\n\n    ops.add_command(\n        dbops.DropView((\"edgedbext\", \"ai_active_embedding_models\")))\n\n    return ops\n\n\ndef _pg_create_ai_embeddings_source_view(\n    index: s_indexes.Index,\n    options: qlcompiler.CompilerOptions,\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    *,\n    rebuild_all: bool=True,\n) -> dbops.Command:\n    ops = dbops.CommandGroup()\n\n    expr = _compile_ai_embeddings_source_view_expr(index, options, schema)\n    view_name = common.get_index_table_backend_name(\n        index, schema, aspect=\"extaiview\")\n\n    idx_id = _get_index_root_id(schema, index)\n    target_col = f\"__ext_ai_{idx_id}_embedding__\"\n    index_dimensions = index.must_get_json_annotation(\n        schema,\n        sn.QualName(\"ext::ai\", \"embedding_dimensions\"),\n        int,\n    )\n    model_dimensions = index.must_get_json_annotation(\n        schema,\n        sn.QualName(\"ext::ai\", \"embedding_model_max_output_dimensions\"),\n        int,\n    )\n\n    if index_dimensions < model_dimensions:\n        target_dims_shortening = str(index_dimensions)\n    else:\n        target_dims_shortening = \"NULL\"\n\n    kwargs = index.get_concrete_kwargs(schema)\n    truncate_to_max_arg = kwargs.get(\"truncate_to_max\")\n    if truncate_to_max_arg is not None:\n        truncate_to_max = cast(\n            bool,\n            truncate_to_max_arg.assert_compiled().as_python_value()\n        )\n    else:\n        truncate_to_max = False\n\n    table_name = common.get_index_table_backend_name(index, schema)\n    expr_sql = codegen.generate_source(expr)\n    document_sql = textwrap.dedent(f\"\"\"\\\n        SELECT\n            (q.val).f1 AS \"id\",\n            (q.val).f2 AS \"text\",\n            {ql(q(*table_name))} AS \"target_rel\",\n            {ql(qi(target_col))} AS \"target_attr\",\n            {target_dims_shortening}::int AS \"target_dims_shortening\",\n            {truncate_to_max}::boolean AS \"truncate_to_max\"\n        FROM\n            ({expr_sql}) AS q(val)\n    \"\"\")\n\n    view = dbops.View(name=view_name, query=document_sql)\n    ops.add_command(dbops.CreateView(view, or_replace=True))\n    if rebuild_all:\n        ops.add_command(\n            pg_rebuild_all_pending_embeddings_views(schema, context))\n\n    return ops\n\n\ndef _pg_drop_ai_embeddings_source_view(\n    index: s_indexes.Index,\n    schema: s_schema.Schema,\n    orig_schema: s_schema.Schema,\n    context: sd.CommandContext,\n) -> dbops.Command:\n    ops = dbops.CommandGroup()\n\n    ops.add_command(pg_rebuild_all_pending_embeddings_views(\n        schema, context\n    ))\n\n    view_name = common.get_index_table_backend_name(\n        index, orig_schema, aspect=\"extaiview\")\n    ops.add_command(dbops.DropView(view_name))\n\n    return ops\n\n\ndef _pg_update_func_name(\n    tbl_name: tuple[str, str],\n    idx_id: str,\n) -> tuple[str, ...]:\n    return (\n        tbl_name[0],\n        common.edgedb_name_to_pg_name(tbl_name[1] + f'_extai_{idx_id}_upd'),\n    )\n\n\ndef _pg_trigger_name(\n    tbl_name: str,\n    idx_id: str,\n) -> str:\n    return common.edgedb_name_to_pg_name(tbl_name + f'_extai_{idx_id}_trg')\n\n\ndef _get_index_root_id(\n    schema: s_schema.Schema,\n    index: s_indexes.Index,\n) -> str:\n    return s_indexes.get_ai_index_id(schema, index)\n"
  },
  {
    "path": "edb/pgsql/deltadbops.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Abstractions for low-level database DDL and DML operations.\"\"\"\n\nfrom __future__ import annotations\nfrom typing import Optional\n\nimport itertools\n\nfrom edb.common import adapter\n\nfrom edb.schema import indexes as s_indexes\nfrom edb.schema import objects as s_obj\nfrom edb.schema import schema as s_schema\n\nfrom edb.pgsql import common\nfrom edb.pgsql import dbops\nfrom edb.pgsql import schemamech\n\n\nclass SchemaDBObjectMeta(adapter.Adapter):  # type: ignore\n    def __init__(cls, name, bases, dct, *, adapts=None):\n        adapter.Adapter.__init__(cls, name, bases, dct, adapts=adapts)\n        type(s_obj.Object).__init__(cls, name, bases, dct)\n\n\nclass SchemaDBObject(metaclass=SchemaDBObjectMeta):\n    @classmethod\n    def adapt(cls, obj):\n        return cls.copy(obj)\n\n\nclass ConstraintCommon:\n    def __init__(self, constraint, schema):\n        self._constr_id = constraint.id\n        self._schema_constr_name = constraint.get_name(schema)\n        self._schema_constr_is_delegated = constraint.get_delegated(schema)\n        self._schema = schema\n        self._constraint = constraint\n\n    def constraint_name(self, quote=True):\n        name = self.raw_constraint_name()\n        name = common.edgedb_name_to_pg_name(name)\n        return common.quote_ident(name) if quote else name\n\n    def schema_constraint_name(self):\n        return self._schema_constr_name\n\n    def raw_constraint_name(self):\n        return common.get_constraint_raw_name(self._constr_id)\n\n    def generate_extra(self, block: dbops.PLBlock) -> None:\n        text = self.raw_constraint_name()\n        cmd = dbops.Comment(object=self, text=text)\n        cmd.generate(block)\n\n    @property\n    def delegated(self):\n        return self._schema_constr_is_delegated\n\n\nclass SchemaConstraintDomainConstraint(\n    ConstraintCommon, dbops.DomainConstraint\n):\n    def __init__(self, domain_name, constraint, exprdata, schema):\n        ConstraintCommon.__init__(self, constraint, schema)\n        dbops.DomainConstraint.__init__(self, domain_name)\n        self._exprdata = exprdata\n\n    def constraint_code(self, block: dbops.PLBlock) -> str:\n        if len(self._exprdata) == 1:\n            expr = self._exprdata[0].exprdata.plain\n        else:\n            exprs = [e.plain for e in self._exprdata.exprdata]\n            expr = '(' + ') AND ('.join(exprs) + ')'\n\n        return f'CHECK ({expr})'\n\n    def __repr__(self):\n        return '<{}.{} {!r} {!r}>'.format(\n            self.__class__.__module__, self.__class__.__name__,\n            self.domain_name, self._constraint)\n\n\nclass SchemaConstraintTableConstraint(ConstraintCommon, dbops.TableConstraint):\n    def __init__(\n        self,\n        table_name,\n        *,\n        constraint,\n        exprdata: list[schemamech.ExprData],\n        relative_exprdata: list[schemamech.ExprData],\n        scope,\n        type,\n        table_type,\n        except_data,\n        schema,\n    ):\n        ConstraintCommon.__init__(self, constraint, schema)\n        dbops.TableConstraint.__init__(self, table_name, None)\n        self._exprdata = exprdata\n        self._relative_exprdata = relative_exprdata\n        self._scope = scope\n        self._type = type\n        self._table_type = table_type\n        self._except_data = except_data\n\n    def is_non_row_and_identical(\n        self, other: SchemaConstraintTableConstraint\n    ) -> bool:\n        \"\"\"Constraints which only contain references to columns and have no\n        expressions or except clause are considered trivial. Such constraints\n        can be checked for equivalence without actually generating their code.\n\n        This function should by updated if `constraint_code` changes.\n        \"\"\"\n        return (\n            # Constraint is on the same subject\n            self._subject_name == other._subject_name\n            # Row scope constraints treated differently, see `constraint_code`\n            and self._scope != 'row'\n            and other._scope != 'row'\n            # Expr data is identical\n            and len(self._exprdata) == len(other._exprdata)\n            and all(\n                exprdata.is_trivial == other_exprdata.is_trivial\n                and (\n                    list(exprdata.exprdata.plain_chunks)\n                    == list(other_exprdata.exprdata.plain_chunks)\n                )\n                and (\n                    list(exprdata.exprdata.plain_chunks)\n                    == list(other_exprdata.exprdata.plain_chunks)\n                )\n                for exprdata, other_exprdata in zip(\n                    self._exprdata, other._exprdata\n                )\n            )\n            # Except data is identical\n            and self._except_data == other._except_data\n        )\n\n    def constraint_code(self, block: dbops.PLBlock) -> str | list[str]:\n        if self._scope == 'row':\n            if len(self._exprdata) == 1:\n                expr = self._exprdata[0].exprdata.plain\n            else:\n                exprs = [e.exprdata.plain for e in self._exprdata]\n                expr = '(' + ') AND ('.join(exprs) + ')'\n\n            if self._except_data:\n                cond = self._except_data.plain\n                expr = f'({expr}) OR ({cond}) is true'\n\n            return f'CHECK ({expr})'\n\n        else:\n            if self._type != 'unique':\n                raise ValueError(\n                    'unexpected constraint type: {}'.format(self._type))\n\n            constr_exprs = []\n\n            for exprdata in self._exprdata:\n                if exprdata.is_trivial and not self._except_data:\n                    # A constraint that contains one or more\n                    # references to columns, and no expressions.\n                    #\n                    # Update `is_non_row_and_identical` if this ever changes!\n                    expr = ', '.join(exprdata.exprdata.plain_chunks)\n                    expr = 'UNIQUE ({})'.format(expr)\n                else:\n                    # Complex constraint with arbitrary expressions\n                    # needs to use EXCLUDE.\n                    #\n                    chunks = exprdata.exprdata.plain_chunks\n                    expr = ', '.join(\n                        \"{} WITH =\".format(chunk) for chunk in chunks)\n                    expr = f'EXCLUDE ({expr})'\n                    if self._except_data:\n                        cond = self._except_data.plain\n                        expr = f'{expr} WHERE (({cond}) is not true)'\n\n                constr_exprs.append(expr)\n\n            return constr_exprs\n\n    def numbered_constraint_name(self, i, quote=True):\n        raw_name = self.raw_constraint_name()\n        name = common.edgedb_name_to_pg_name('{}#{}'.format(raw_name, i))\n        return common.quote_ident(name) if quote else name\n\n    def get_trigger_procname(self):\n        return common.get_backend_name(\n            self._schema, self._constraint, catenate=False, aspect='trigproc')\n\n    def get_trigger_condition(self):\n        chunks = []\n\n        for expr in self._exprdata:\n            condition = '{old_expr} IS DISTINCT FROM {new_expr}'.format(\n                old_expr=expr.exprdata.old, new_expr=expr.exprdata.new\n            )\n            chunks.append(condition)\n\n        if len(chunks) == 1:\n            return chunks[0]\n        else:\n            return '(' + ') OR ('.join(chunks) + ')'\n\n    def get_trigger_proc_text(self):\n        chunks = []\n\n        constr_name = self.constraint_name()\n        raw_constr_name = self.constraint_name(quote=False)\n\n        errmsg = 'duplicate key value violates unique ' \\\n                 'constraint {constr}'.format(constr=constr_name)\n\n        for expr, relative_expr in zip(\n            itertools.cycle(self._exprdata), self._relative_exprdata\n        ):\n            exprdata = expr.exprdata\n            relative_exprdata = relative_expr.exprdata\n\n            except_data = self._except_data\n            relative_except_data = relative_expr.except_data\n\n            if self._except_data:\n                except_part = f'''\n                    AND ({relative_except_data.plain} is not true)\n                    AND ({except_data.new} is not true)\n                '''\n            else:\n                except_part = ''\n\n            # Link tables get updated by deleting and then reinserting\n            # rows, and so the trigger might fire even on rows that\n            # did not *really* change. Check `source` also to prevent\n            # spurious errors in those cases. (Anything with the same\n            # source must have the same type, so any genuine constraint\n            # errors this filters away will get caught by the *actual*\n            # constraint.)\n            # We *could* do a check for id on object tables, but it\n            # isn't needed and would take at least some time.\n            src_check = (\n                ' AND source != NEW.source'\n                if self._table_type == 'link' else ''\n            )\n\n            schemaname, tablename = relative_expr.subject_db_name\n            text = '''\n                PERFORM\n                    TRUE\n                  FROM\n                    {table}\n                  WHERE\n                    {plain_expr} = {new_expr}{except_part}{src_check};\n                IF FOUND THEN\n                  RAISE unique_violation\n                      USING\n                          TABLE = '{tablename}',\n                          SCHEMA = '{schemaname}',\n                          CONSTRAINT = '{constr}',\n                          MESSAGE = '{errmsg}',\n                          DETAIL = {detail};\n                END IF;\n            '''.format(\n                table=common.qname(schemaname, tablename),\n                plain_expr=relative_exprdata.plain,\n                new_expr=exprdata.new,\n                except_part=except_part,\n                src_check=src_check,\n                schemaname=schemaname,\n                tablename=tablename,\n                constr=raw_constr_name,\n                errmsg=errmsg,\n                detail=common.quote_literal(\n                    f\"Key ({relative_exprdata.plain}) already exists.\"\n                ),\n            )\n\n            chunks.append(text)\n\n        text = 'BEGIN\\n' + '\\n\\n'.join(chunks) + '\\nRETURN NEW;\\nEND;'\n\n        return text\n\n    def requires_triggers(self):\n        return schemamech.table_constraint_requires_triggers(\n            self._constraint,\n            self._schema,\n            self._type,\n        )\n\n    def can_disable_triggers(self):\n        return self._constraint.is_independent(self._schema)\n\n    def __repr__(self):\n        return '<{}.{} {!r} at 0x{:x}>'.format(\n            self.__class__.__module__, self.__class__.__name__,\n            self.schema_constraint_name(), id(self))\n\n\nclass MultiConstraintItem:\n    def __init__(\n        self,\n        constraint: SchemaConstraintTableConstraint,\n        index: int,\n    ):\n        self.constraint = constraint\n        self.index = index\n\n    def get_type(self):\n        return self.constraint.get_type()\n\n    def get_id(self):\n        # XXX\n        name = self.constraint.numbered_constraint_name(self.index)\n\n        return '{} ON {} {}'.format(\n            name, self.constraint.get_subject_type(),\n            self.constraint.get_subject_name())\n\n\nclass AlterTableAddMultiConstraint(\n    dbops.AlterTableAddConstraint[SchemaConstraintTableConstraint]\n):\n    def code_with_block(self, block: dbops.PLBlock) -> str:\n        exprs = self.constraint.constraint_code(block)\n\n        if isinstance(exprs, list) and len(exprs) > 1:\n            chunks = []\n\n            for i, expr in enumerate(exprs):\n                name = self.constraint.numbered_constraint_name(i)\n                chunk = f'ADD CONSTRAINT {name} {expr}'\n                chunks.append(chunk)\n\n            code = ', '.join(chunks)\n        else:\n            if isinstance(exprs, list):\n                exprs = exprs[0]\n\n            name = self.constraint.constraint_name()\n            code = f'ADD CONSTRAINT {name} {exprs}'\n\n        return code\n\n    def generate_extra_composite(\n        self, block: dbops.PLBlock, group: dbops.CompositeCommandGroup\n    ) -> None:\n        comments = []\n\n        exprs = self.constraint.constraint_code(block)\n\n        if isinstance(exprs, list) and len(exprs) > 1:\n            assert isinstance(self.constraint, SchemaConstraintTableConstraint)\n            for i, _expr in enumerate(exprs):\n                name = self.constraint.numbered_constraint_name(i)\n                constraint = MultiConstraintItem(self.constraint, i)\n\n                comment = dbops.Comment(constraint, name)\n                comments.append(comment)\n        else:\n            name = self.constraint.constraint_name()\n            comment = dbops.Comment(self.constraint, name)\n            comments.append(comment)\n\n        for comment in comments:\n            comment.generate(block)\n\n\nclass AlterTableDropMultiConstraint(dbops.AlterTableDropConstraint):\n    def code_with_block(self, block: dbops.PLBlock) -> str:\n        exprs = self.constraint.constraint_code(block)\n\n        if isinstance(exprs, list) and len(exprs) > 1:\n            chunks = []\n\n            for i, _expr in enumerate(exprs):\n                name = self.constraint.numbered_constraint_name(i)\n                chunk = f'DROP CONSTRAINT {name}'\n                chunks.append(chunk)\n\n            code = ', '.join(chunks)\n\n        else:\n            name = self.constraint.constraint_name()\n            code = f'DROP CONSTRAINT {name}'\n\n        return code\n\n\nclass AlterTableConstraintBase(dbops.AlterTableBaseMixin, dbops.CommandGroup):\n    def __init__(\n        self,\n        name: tuple[str, ...],\n        *,\n        constraint: SchemaConstraintTableConstraint,\n        contained: bool = False,\n        conditions: Optional[set[str | dbops.Condition]] = None,\n        neg_conditions: Optional[set[str | dbops.Condition]] = None,\n    ):\n        dbops.CommandGroup.__init__(\n            self, conditions=conditions, neg_conditions=neg_conditions\n        )\n\n        dbops.AlterTableBaseMixin.__init__(\n            self, name=name, contained=contained)\n\n        self._constraint = constraint\n\n    def _get_triggers(\n        self,\n        table_name: tuple[str, ...],\n        constraint: SchemaConstraintTableConstraint,\n        proc_name='null',\n    ) -> tuple[dbops.Trigger, ...]:\n        cname = constraint.raw_constraint_name()\n\n        ins_trigger_name = cname + '_instrigger'\n        ins_trigger = dbops.Trigger(\n            name=ins_trigger_name, table_name=table_name, events=('insert', ),\n            procedure=proc_name, is_constraint=True, inherit=True)\n\n        upd_trigger_name = cname + '_updtrigger'\n        condition = constraint.get_trigger_condition()\n\n        upd_trigger = dbops.Trigger(\n            name=upd_trigger_name, table_name=table_name, events=('update', ),\n            procedure=proc_name, condition=condition, is_constraint=True,\n            inherit=True)\n\n        return ins_trigger, upd_trigger\n\n    def create_constr_trigger(\n        self,\n        table_name: tuple[str, ...],\n        constraint: SchemaConstraintTableConstraint,\n        proc_name: str,\n    ) -> list[dbops.CreateTrigger]:\n        ins_trigger, upd_trigger = self._get_triggers(\n            table_name, constraint, proc_name\n        )\n\n        return [\n            dbops.CreateTrigger(ins_trigger),\n            dbops.CreateTrigger(upd_trigger),\n        ]\n\n    def drop_constr_trigger(\n        self,\n        table_name: tuple[str, ...],\n        constraint: SchemaConstraintTableConstraint,\n    ) -> list[dbops.DDLOperation]:\n        ins_trigger, upd_trigger = self._get_triggers(table_name, constraint)\n\n        return [\n            dbops.DropTrigger(ins_trigger, conditional=True),\n            dbops.DropTrigger(upd_trigger, conditional=True),\n        ]\n\n    def create_constr_trigger_function(\n        self, constraint: SchemaConstraintTableConstraint\n    ):\n        proc_name = constraint.get_trigger_procname()\n        proc_text = constraint.get_trigger_proc_text()\n\n        # Because of casting is not immutable in PG, this function may not be\n        # immutable, only stable. But because we check that casing in edgeql\n        # *is* immutable, we can (almost) safely assume that this function is\n        # also immutable.\n        func = dbops.Function(\n            name=proc_name,\n            text=proc_text,\n            volatility='immutable',\n            returns='trigger',\n            language='plpgsql',\n        )\n\n        return [dbops.CreateFunction(func, or_replace=True)]\n\n    def drop_constr_trigger_function(self, proc_name: tuple[str, ...]):\n        return [dbops.DropFunction(\n            name=proc_name,\n            args=(),\n            # Use a condition instead of if_exists ot reduce annoying\n            # debug spew from postgres.\n            conditions=[dbops.FunctionExists(name=proc_name, args=())],\n        )]\n\n    def create_constraint(self, constraint: SchemaConstraintTableConstraint):\n        # Add the constraint normally to our table\n        #\n        my_alter = dbops.AlterTable(self.name)\n        add_constr = AlterTableAddMultiConstraint(constraint=constraint)\n        my_alter.add_command(add_constr)\n\n        self.add_command(my_alter)\n\n    def create_constraint_trigger_and_fuction(\n        self, constraint: SchemaConstraintTableConstraint\n    ):\n        \"\"\"Create constraint trigger FUNCTION and TRIGGER.\n\n        Adds the new function to the trigger.\n        Disables the trigger if possible.\n        \"\"\"\n        if (\n            constraint.requires_triggers()\n            and not constraint.can_disable_triggers()\n        ):\n            # Create trigger function\n            self.add_commands(self.create_constr_trigger_function(constraint))\n\n            proc_name = constraint.get_trigger_procname()\n            cr_trigger = self.create_constr_trigger(\n                self.name, constraint, proc_name)\n            self.add_commands(cr_trigger)\n\n    def alter_constraint(\n        self,\n        old_constraint: SchemaConstraintTableConstraint,\n        new_constraint: SchemaConstraintTableConstraint,\n    ):\n        if old_constraint.delegated and not new_constraint.delegated:\n            # No longer delegated, create db structures\n            self.create_constraint(new_constraint)\n\n        elif not old_constraint.delegated and new_constraint.delegated:\n            # Becoming delegated, drop db structures\n            self.drop_constraint(old_constraint)\n\n        elif not new_constraint.delegated:\n            # Some other modification\n            if old_constraint.is_non_row_and_identical(new_constraint):\n                # If the constraint itself is unchanged, it is still necessary\n                # to drop any constraint triggers. This is to clear any\n                # postgres dependencies on constraint columns.\n                #\n                # For example, given:\n                #   type X { property n: int64 { constraint exclusive } };\n                #   type Y extending X;\n                #\n                # Altering the property with\n                #   alter type X alter property n using (1);\n                #\n                # will result in the column for X.n to be dropped. To ensure\n                # this operation succeeds, the constraint trigger must be\n                # deleted before dropping the column.\n                self.drop_constraint_trigger_and_fuction(old_constraint)\n\n            else:\n                # If the constraint is actually different, drop and create.\n                self.drop_constraint(old_constraint)\n                self.create_constraint(new_constraint)\n\n    def drop_constraint(self, constraint: SchemaConstraintTableConstraint):\n        self.drop_constraint_trigger_and_fuction(constraint)\n\n        # Drop the constraint normally from our table\n        #\n        my_alter = dbops.AlterTable(constraint._subject_name)\n\n        drop_constr = AlterTableDropMultiConstraint(constraint=constraint)\n        my_alter.add_command(drop_constr)\n\n        self.add_command(my_alter)\n\n    def drop_constraint_trigger_and_fuction(\n        self, constraint: SchemaConstraintTableConstraint\n    ):\n        \"\"\"Drop constraint trigger FUNCTION and TRIGGER.\"\"\"\n        if constraint.requires_triggers():\n            self.add_commands(self.drop_constr_trigger(\n                constraint._subject_name, constraint))\n            proc_name = constraint.get_trigger_procname()\n            self.add_commands(self.drop_constr_trigger_function(proc_name))\n\n\nclass AlterTableAddConstraint(AlterTableConstraintBase):\n    def __repr__(self):\n        return '<{}.{} {!r}>'.format(\n            self.__class__.__module__, self.__class__.__name__,\n            self._constraint)\n\n    def generate(self, block):\n        if not self._constraint.delegated:\n            self.create_constraint(self._constraint)\n        super().generate(block)\n\n\nclass AlterTableAlterConstraint(AlterTableConstraintBase):\n    def __init__(\n        self, name, *, constraint, new_constraint, **kwargs\n    ):\n        super().__init__(name, constraint=constraint, **kwargs)\n        self._new_constraint = new_constraint\n\n    def __repr__(self):\n        return '<{}.{} {!r}>'.format(\n            self.__class__.__module__, self.__class__.__name__,\n            self._constraint)\n\n    def generate(self, block):\n        self.alter_constraint(self._constraint, self._new_constraint)\n        super().generate(block)\n\n\nclass AlterTableDropConstraint(AlterTableConstraintBase):\n    def __repr__(self):\n        return '<{}.{} {!r}>'.format(\n            self.__class__.__module__, self.__class__.__name__,\n            self._constraint)\n\n    def generate(self, block):\n        if not self._constraint.delegated:\n            self.drop_constraint(self._constraint)\n        super().generate(block)\n\n\nclass AlterTableUpdateConstraintTrigger(AlterTableConstraintBase):\n    def __repr__(self):\n        return '<{}.{} {!r}>'.format(\n            self.__class__.__module__, self.__class__.__name__,\n            self._constraint)\n\n    def generate(self, block):\n        self.drop_constraint_trigger_and_fuction(self._constraint)\n        self.create_constraint_trigger_and_fuction(self._constraint)\n        super().generate(block)\n\n\nclass AlterTableUpdateConstraintTriggerFixup(AlterTableConstraintBase):\n    def __repr__(self):\n        return '<{}.{} {!r}>'.format(\n            self.__class__.__module__, self.__class__.__name__,\n            self._constraint)\n\n    def generate(self, block):\n        # Pre 6.8 versions of gel created needless disabled triggers\n        # in some cases. This path (invoked by administer\n        # remove_pointless_triggers()) deletes them.\n        if (\n            self._constraint.requires_triggers()\n            and self._constraint.can_disable_triggers()\n        ):\n            self.drop_constraint_trigger_and_fuction(self._constraint)\n        super().generate(block)\n\n\ndef rename_pg_index(\n    old_index: s_indexes.Index,\n    new_index: s_indexes.Index,\n    schema: s_schema.Schema,\n    aspect: str = 'index'\n) -> dbops.Command:\n    table_name = common.get_index_table_backend_name(new_index, schema)\n    module_name = new_index.get_name(schema).module\n    old_index_name = common.get_index_backend_name(\n        old_index.id, module_name, catenate=False, aspect=aspect\n    )\n    new_index_name = common.get_index_backend_name(\n        new_index.id, module_name, catenate=False, aspect=aspect\n    )\n    pg_index = dbops.Index(\n        name=old_index_name[1],\n        table_name=table_name,  # type: ignore\n    )\n    return dbops.RenameIndex(\n        pg_index,\n        new_name=new_index_name[1],\n        conditional=True,\n    )\n"
  },
  {
    "path": "edb/pgsql/deltafts.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import Optional, Iterable, Sequence, Collection\n\nfrom edb import errors\n\nfrom edb.schema import indexes as s_indexes\nfrom edb.schema import types as s_types\nfrom edb.schema import expr as s_expr\nfrom edb.schema import schema as s_schema\nfrom edb.schema import delta as sd\nfrom edb.schema import name as sn\n\nfrom edb.ir import ast as irast\n\nfrom edb.edgeql import compiler as qlcompiler\n\nfrom . import common\nfrom . import dbops\nfrom . import deltadbops\nfrom . import compiler\nfrom . import codegen\nfrom . import types\nfrom . import ast as pgast\n\nfrom .common import qname as q\nfrom .common import quote_literal as ql\nfrom .common import quote_ident as qi\nfrom .compiler import astutils\nfrom .compiler import enums as pgce\n\n\ndef create_fts_index(\n    index: s_indexes.Index,\n    index_expr: irast.Set,\n    predicate_src: Optional[str],\n    sql_kwarg_exprs: dict[str, str],\n    options: qlcompiler.CompilerOptions,\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n) -> dbops.Command:\n    subject = index.get_subject(schema)\n    assert isinstance(subject, s_indexes.IndexableSubject)\n\n    effective, has_overridden = s_indexes.get_effective_object_index(\n        schema, subject, sn.QualName(\"std::fts\", \"index\")\n    )\n\n    if index != effective:\n        return dbops.CommandGroup()\n\n    # When creating an index on a child that already has an fts index\n    # inherited from the parent, we don't need to create the index, but just\n    # update the populating expressions.\n    if has_overridden:\n        return _refresh_fts_document(\n            index, has_overridden[0], options, schema, context\n        )\n    else:\n        return _create_fts_document(\n            index,\n            index_expr,\n            predicate_src,\n            sql_kwarg_exprs,\n            schema,\n            context,\n        )\n\n\ndef delete_fts_index(\n    index: s_indexes.Index,\n    drop_index: dbops.Command,\n    options: qlcompiler.CompilerOptions,\n    schema: s_schema.Schema,\n    orig_schema: s_schema.Schema,\n    context: sd.CommandContext,\n) -> dbops.Command:\n    subject = index.get_subject(orig_schema)\n    assert isinstance(subject, s_indexes.IndexableSubject)\n\n    effective, _ = s_indexes.get_effective_object_index(\n        schema, subject, sn.QualName(\"std::fts\", \"index\")\n    )\n\n    if not effective:\n        return _delete_fts_document(index, drop_index, orig_schema, context)\n    else:\n        # effective index remains: don't drop the fts document\n\n        effective_subject = effective.get_subject(schema)\n        is_eff_on_direct_parent = effective_subject in subject.get_bases(\n            schema\n        ).objects(schema)\n\n        if is_eff_on_direct_parent:\n            return _refresh_fts_document(\n                index, effective, options, schema, context\n            )\n        else:\n            return dbops.CommandGroup()\n\n\ndef _compile_ir_index_exprs(\n    index: s_indexes.Index, index_expr: irast.Set, schema: s_schema.Schema\n):\n    subject = index.get_subject(schema)\n    assert isinstance(subject, s_types.Type)\n\n    subject_id = irast.PathId.from_type(schema, subject, env=None)\n    sql_res = compiler.compile_ir_to_sql_tree(\n        index_expr,\n        singleton_mode=True,\n        external_rvars={\n            (subject_id, pgce.PathAspect.SOURCE): pgast.RelRangeVar(\n                alias=pgast.Alias(aliasname='NEW'),\n                relation=pgast.Relation(name='NEW'),\n            )\n        },\n    )\n    return astutils.maybe_unpack_row(sql_res.ast)\n\n\ndef _create_fts_document(\n    index: s_indexes.Index,\n    index_expr: irast.Set,\n    predicate_src: Optional[str],\n    sql_kwarg_exprs: dict[str, str],\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n) -> dbops.Command:\n    exprs = _compile_ir_index_exprs(index, index_expr, schema)\n\n    from edb.common import debug\n\n    if debug.flags.zombodb:\n        return _zombo_create_fts_document(\n            index, exprs, predicate_src, sql_kwarg_exprs, schema\n        )\n    else:\n        return _pg_create_fts_document(\n            index, exprs, predicate_src, sql_kwarg_exprs, schema\n        )\n\n\ndef _delete_fts_document(\n    index: s_indexes.Index,\n    drop_index: dbops.Command,\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n) -> dbops.Command:\n    table_name = common.get_index_table_backend_name(index, schema)\n\n    ops = dbops.CommandGroup()\n    ops.add_command(drop_index)\n\n    from edb.common import debug\n\n    if debug.flags.zombodb:\n        zombo_func_name = _zombo_func_name(table_name)\n        ops.add_command(dbops.DropFunction(zombo_func_name, args=[table_name]))\n\n        zombo_type_name = _zombo_type_name(table_name)\n        ops.add_command(dbops.DropCompositeType(zombo_type_name))\n    else:\n        ops.add_command(_pg_drop_trigger(table_name))\n\n        # When the ObjectType is being deleted, we don't drop the index, as it\n        # will get dropped with parent table.\n        # The same goes for the __fts_document__ column.\n        source_drop = isinstance(drop_index, dbops.NoOpCommand)\n\n        if not source_drop:\n            fts_document = dbops.Column(\n                name=f'__fts_document__',\n                type=('pg_catalog', 'tsvector'),\n            )\n            alter_table = dbops.AlterTable(table_name)\n            alter_table.add_operation(dbops.AlterTableDropColumn(fts_document))\n            ops.add_command(alter_table)\n    return ops\n\n\ndef update_fts_document(\n    index: s_indexes.Index,\n    options: qlcompiler.CompilerOptions,\n    schema: s_schema.Schema,\n) -> dbops.Query:\n    table_name = common.get_index_table_backend_name(index, schema)\n\n    # compile the expression\n    index_sexpr: Optional[s_expr.Expression] = index.get_expr(schema)\n    assert index_sexpr\n    index_expr = index_sexpr.ensure_compiled(\n        schema=schema,\n        options=options,\n        context=None,\n    )\n    exprs = _compile_ir_index_exprs(index, index_expr.irast.expr, schema)\n\n    from edb.common import debug\n    if debug.flags.zombodb:\n        raise NotImplementedError('zombo refresh index not implemented')\n    else:\n        # to avoid code duplication, we call code for creating triggers and\n        # extract the first UPDATE command\n        create_trigger_ops = _pg_create_trigger(table_name, exprs)\n        update_fts_document_op = create_trigger_ops.commands[0]\n        assert isinstance(update_fts_document_op, dbops.Query)\n\n        return update_fts_document_op\n\n\ndef _refresh_fts_document(\n    index: s_indexes.Index,\n    old_index: s_indexes.Index,\n    options: qlcompiler.CompilerOptions,\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n) -> dbops.Command:\n    table_name = common.get_index_table_backend_name(index, schema)\n\n    # compile the expression\n    index_sexpr: Optional[s_expr.Expression] = index.get_expr(schema)\n    assert index_sexpr\n    index_expr = index_sexpr.ensure_compiled(\n        schema=schema,\n        options=options,\n        context=context,\n    )\n\n    exprs = _compile_ir_index_exprs(index, index_expr.irast.expr, schema)\n\n    ops = dbops.CommandGroup()\n\n    from edb.common import debug\n\n    if debug.flags.zombodb:\n        raise NotImplementedError('zombo refresh index not implemented')\n    else:\n        ops.add_command(_pg_drop_trigger(table_name))\n        ops.add_command(_pg_create_trigger(table_name, exprs))\n\n    # Sigh, we need to rename the main index to match the new id,\n    # entirely for the purpose of having ANALYZE be able to pick it up\n    ops.add_command(\n        deltadbops.rename_pg_index(\n            old_index=old_index,\n            new_index=index,\n            schema=schema,\n        )\n    )\n\n    return ops\n\n\ndef _raise_unsupported_language_error(\n    unsupported: Collection[str],\n) -> None:\n    unsupported = list(unsupported)\n    unsupported.sort()\n\n    msg = 'Full text search language'\n    if len(unsupported) > 1:\n        msg += 's'\n\n    msg += ' ' + ', '.join(f'`{l}`' for l in unsupported)\n    msg += ' not supported'\n\n    raise errors.UnsupportedFeatureError(msg)\n\n\n# --- pg fts ---\n\n\ndef _pg_create_fts_document(\n    index: s_indexes.Index,\n    exprs: Sequence[pgast.BaseExpr],\n    predicate_src: Optional[str],\n    sql_kwarg_exprs: dict[str, str],\n    schema: s_schema.Schema,\n) -> dbops.Command:\n    ops = dbops.CommandGroup()\n\n    # create column __fts_document__\n    table_name = common.get_index_table_backend_name(index, schema)\n\n    module_name = index.get_name(schema).module\n    index_name = common.get_index_backend_name(\n        index.id, module_name, catenate=False\n    )\n\n    fts_document = dbops.Column(\n        name=f'__fts_document__', type='pg_catalog.tsvector'\n    )\n    alter_table = dbops.AlterTable(table_name)\n    alter_table.add_operation(dbops.AlterTableAddColumn(fts_document))\n    ops.add_command(alter_table)\n\n    ops.add_command(_pg_create_trigger(table_name, exprs))\n\n    pg_index = dbops.Index(\n        name=index_name[1],\n        table_name=table_name,  # type: ignore\n        exprs=['__fts_document__'],\n        unique=False,\n        inherit=True,\n        predicate=predicate_src,\n        metadata={\n            'schemaname': str(index.get_name(schema)),\n            'kwargs': sql_kwarg_exprs,\n            # use a reference to the new column in the index instead\n            'code': 'gin (__col__)',\n        },\n    )\n    ops.add_command(dbops.CreateIndex(pg_index))\n    return ops\n\n\ndef _pg_create_trigger(\n    table_name: tuple[str, str],\n    exprs: Sequence[pgast.BaseExpr],\n) -> dbops.CommandGroup:\n    ops = dbops.CommandGroup()\n\n    # prepare the expression to update __fts_document__\n    document_exprs = []\n    for expr in exprs:\n        assert isinstance(expr, pgast.FTSDocument)\n\n        lang_domain: Iterable[str] = expr.language_domain\n        lang_domain = map(types.to_regconfig, lang_domain)\n        unsupported = set(lang_domain).difference(types.pg_langs)\n        if len(unsupported) > 0:\n            _raise_unsupported_language_error(unsupported)\n\n        text_sql = codegen.generate_source(expr.text)\n        language_sql = codegen.generate_source(expr.language)\n\n        document_expr = f'''\n            to_tsvector(\n                edgedb.fts_to_regconfig(({language_sql})::text),\n                COALESCE({text_sql}, '')\n            )\n        '''\n        if expr.weight:\n            document_expr = f'setweight({document_expr}, {ql(expr.weight)})'\n        document_exprs.append(document_expr)\n\n    document_sql = ' || '.join(document_exprs) if document_exprs else 'NULL'\n\n    # update existing rows\n    ops.add_command(\n        dbops.Query(\n            f\"\"\"\n        UPDATE {q(*table_name)} as NEW SET __fts_document__ = ({document_sql});\n        \"\"\"\n        )\n    )\n\n    # create update function\n    func_name = _pg_update_func_name(table_name)\n    function = dbops.Function(\n        name=func_name,\n        text=f'''\n            BEGIN\n                NEW.__fts_document__ := ({document_sql});\n                RETURN NEW;\n            END;\n        ''',\n        volatility='immutable',\n        returns='trigger',\n        language='plpgsql',\n    )\n    ops.add_command(dbops.CreateFunction(function))\n\n    # create trigger to update the __fts_document__\n    trigger_name = _pg_trigger_name(table_name[1])\n    trigger = dbops.Trigger(\n        name=trigger_name,\n        table_name=table_name,\n        events=('insert', 'update'),\n        timing=dbops.TriggerTiming.Before,\n        procedure=func_name,\n    )\n    ops.add_command(dbops.CreateTrigger(trigger))\n    return ops\n\n\ndef _pg_drop_trigger(\n    table_name: tuple[str, str],\n) -> dbops.Command:\n    ops = dbops.CommandGroup()\n\n    ops.add_command(\n        dbops.DropTrigger(\n            dbops.Trigger(\n                _pg_trigger_name(table_name[1]),\n                table_name=table_name,\n                events=(),\n                procedure='',\n            )\n        )\n    )\n\n    ops.add_command(\n        dbops.DropFunction(\n            _pg_update_func_name(table_name),\n            (),\n        )\n    )\n    return ops\n\n\ndef _pg_update_func_name(\n    tbl_name: tuple[str, str],\n) -> tuple[str, ...]:\n    return (\n        tbl_name[0],\n        common.edgedb_name_to_pg_name(tbl_name[1] + '_ftsupdate'),\n    )\n\n\ndef _pg_trigger_name(\n    tbl_name: str,\n) -> str:\n    return common.edgedb_name_to_pg_name(tbl_name + '_ftstrigger')\n\n\n# --- zombo ---\n\n\ndef _zombo_create_fts_document(\n    index: s_indexes.Index,\n    exprs: Sequence[pgast.BaseExpr],\n    predicate_src: Optional[str],\n    sql_kwarg_exprs: dict[str, str],\n    schema: s_schema.Schema,\n) -> dbops.Command:\n    ops = dbops.CommandGroup()\n\n    table_name = common.get_index_table_backend_name(index, schema)\n\n    module_name = index.get_name(schema).module\n    index_name = common.get_index_backend_name(\n        index.id, module_name, catenate=False\n    )\n\n    zombo_type_name = _zombo_type_name(table_name)\n    ops.add_command(\n        dbops.CreateCompositeType(\n            dbops.CompositeType(\n                name=zombo_type_name,\n                columns=[\n                    dbops.Column(\n                        name=f'field{idx}',\n                        type='text',\n                    )\n                    for idx, _ in enumerate(exprs)\n                ],\n            )\n        )\n    )\n\n    type_mappings: list[tuple[str, str]] = []\n    document_exprs = []\n    for idx, expr in enumerate(exprs):\n        assert isinstance(expr, pgast.FTSDocument)\n\n        text_sql = codegen.generate_source(expr.text)\n\n        if len(expr.language_domain) != 1:\n            raise errors.UnsupportedFeatureError(\n                'zombo fts indexes support only exactly one language'\n            )\n        language = next(iter(expr.language_domain))\n\n        document_exprs.append(text_sql)\n        type_mappings.append((f'field{idx}', language))\n\n    zombo_func_name = _zombo_func_name(table_name)\n    ops.add_command(\n        dbops.CreateFunction(\n            dbops.Function(\n                name=zombo_func_name,\n                args=[('new', table_name)],\n                returns=zombo_type_name,\n                text=f'''\n                SELECT\n                    ROW({','.join(document_exprs)})::{q(*zombo_type_name)};\n                ''',\n            )\n        )\n    )\n\n    for col_name, language in type_mappings:\n        mapping = f'{{\"type\": \"text\", \"analyzer\": \"{language}\"}}'\n\n        ops.add_command(\n            dbops.Query(\n                f\"\"\"PERFORM zdb.define_field_mapping(\n                    {ql(q(*table_name))}::regclass,\n                    {ql(col_name)}::text,\n                    {ql(mapping)}::json\n                )\"\"\"\n            )\n        )\n\n    index_exprs = [f'{q(*zombo_func_name)}({qi(table_name[1])}.*)']\n\n    pg_index = dbops.Index(\n        name=index_name[1],\n        table_name=table_name,  # type: ignore\n        exprs=index_exprs,\n        unique=False,\n        inherit=True,\n        with_clause={'url': ql('http://localhost:9200/')},\n        predicate=predicate_src,\n        metadata={\n            'schemaname': str(index.get_name(schema)),\n            'code': 'zombodb ((__col__))',\n            'kwargs': sql_kwarg_exprs,\n        },\n    )\n    ops.add_command(dbops.CreateIndex(pg_index))\n    return ops\n\n\ndef _zombo_type_name(\n    tbl_name: tuple[str, str],\n) -> tuple[str, str]:\n    return (\n        tbl_name[0],\n        common.edgedb_name_to_pg_name(tbl_name[1] + '_zombo_type'),\n    )\n\n\ndef _zombo_func_name(\n    tbl_name: tuple[str, str],\n) -> tuple[str, ...]:\n    return (\n        tbl_name[0],\n        common.edgedb_name_to_pg_name(tbl_name[1] + '_zombo_func'),\n    )\n"
  },
  {
    "path": "edb/pgsql/inheritance.py",
    "content": "from __future__ import annotations\nfrom typing import Optional, AbstractSet, Iterator\n\nfrom edb.schema import links as s_links\nfrom edb.schema import name as sn\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import sources as s_sources\nfrom edb.schema import schema as s_schema\n\nfrom edb.ir import typeutils as irtyputils\n\nfrom . import ast as pgast\nfrom . import types\nfrom . import common\n\n\ndef get_inheritance_view(\n    schema: s_schema.Schema,\n    obj: s_sources.Source | s_pointers.Pointer,\n    exclude_children: AbstractSet[\n        s_sources.Source | s_pointers.Pointer\n    ] = frozenset(),\n    exclude_ptrs: AbstractSet[s_pointers.Pointer] = frozenset(),\n) -> pgast.SelectStmt:\n    ptrs: dict[sn.UnqualName, tuple[list[str], tuple[str, ...]]] = {}\n\n    if isinstance(obj, s_sources.Source):\n        pointers = list(obj.get_pointers(schema).items(schema))\n        # Sort by UUID timestamp for stable VIEW column order.\n        pointers.sort(key=lambda p: p[1].id.time)\n\n        for ptrname, ptr in pointers:\n            if ptr in exclude_ptrs:\n                continue\n            if ptr.is_pure_computable(schema):\n                continue\n            ptr_stor_info = types.get_pointer_storage_info(\n                ptr,\n                link_bias=isinstance(obj, s_links.Link),\n                schema=schema,\n            )\n            if (\n                isinstance(obj, s_links.Link)\n                or ptr_stor_info.table_type == 'ObjectType'\n            ):\n                ptrs[ptrname] = (\n                    [ptr_stor_info.column_name],\n                    ptr_stor_info.column_type,\n                )\n\n                shortname = ptr.get_shortname(schema).name\n                if shortname != ptr_stor_info.column_name:\n                    ptrs[ptrname][0].append(common.quote_ident(shortname))\n\n        for name, alias, type in obj.get_addon_columns(schema):\n            ptrs[sn.UnqualName(name)] = ([alias], type)\n\n    else:\n        # MULTI PROPERTY\n        ptrs[sn.UnqualName('source')] = (['source'], ('uuid',))\n        lp_info = types.get_pointer_storage_info(\n            obj,\n            link_bias=True,\n            schema=schema,\n        )\n        ptrs[sn.UnqualName('target')] = (['target'], lp_info.column_type)\n\n    descendants = [\n        child\n        for child in obj.descendants(schema)\n        if types.has_table(child, schema) and child not in exclude_children\n        # XXX: Exclude sys/cfg tables from non sys/cfg views. This\n        # probably isn't *really* what we want to do, but until we\n        # figure that out, do *something* so that DDL isn't\n        # excruciatingly slow because of the cost of explicit id\n        # checks. See #5168.\n        and not irtyputils.is_excluded_cfg_view(\n            child, ancestor=obj, schema=schema\n        )\n    ]\n\n    # Hackily force 'source' to appear in abstract links. We need\n    # source present in the code we generate to enforce newly\n    # created exclusive constraints across types.\n    if (\n        ptrs\n        and isinstance(obj, s_links.Link)\n        and sn.UnqualName('source') not in ptrs\n        and obj.is_non_concrete(schema)\n    ):\n        ptrs[sn.UnqualName('source')] = (['source'], ('uuid',))\n\n    components = []\n    components.append(_get_select_from(schema, obj, ptrs))\n    components.extend(\n        _get_select_from(schema, child, ptrs) for child in descendants\n    )\n\n    return _union_all(filter(None, components))\n\n\ndef _union_all(components: Iterator[pgast.SelectStmt]) -> pgast.SelectStmt:\n    query = next(components)\n    for component in components:\n        query = pgast.SelectStmt(\n            larg=query,\n            op='UNION',\n            all=True,\n            rarg=component,\n        )\n    return query\n\n\ndef _get_select_from(\n    schema: s_schema.Schema,\n    obj: s_sources.Source | s_pointers.Pointer,\n    ptr_names: dict[sn.UnqualName, tuple[list[str], tuple[str, ...]]],\n) -> Optional[pgast.SelectStmt]:\n    schema_name, table_name = common.get_backend_name(\n        schema,\n        obj,\n        catenate=False,\n        aspect='table',\n    )\n    # the name of the rel var of the object table within the select query\n    table_rvar_name = table_name\n\n    target_list: list[pgast.ResTarget] = []\n\n    system_cols = ['tableoid', 'xmin', 'cmin', 'xmax', 'cmax', 'ctid']\n    for sys_col_name in system_cols:\n        val: pgast.BaseExpr\n        if not irtyputils.is_cfg_view(obj, schema):\n            val = pgast.ColumnRef(name=(table_rvar_name, sys_col_name))\n        else:\n            val = pgast.NullConstant()\n        target_list.append(pgast.ResTarget(name=sys_col_name, val=val))\n\n    if isinstance(obj, s_sources.Source):\n        ptrs = dict(obj.get_pointers(schema).items(schema))\n\n        for ptr_name, (aliases, pg_type) in ptr_names.items():\n            ptr = ptrs.get(ptr_name)\n\n            if ptr_name == sn.UnqualName('__type__'):\n                # __type__ is special cased: since it is uniquely\n                # determined by the type, we directly insert it\n                # into the views instead of storing it (to save space)\n                val = pgast.TypeCast(\n                    arg=pgast.StringConstant(val=str(obj.id)),\n                    type_name=pgast.TypeName(name=('uuid',)),\n                )\n\n            elif ptr is not None:\n                ptr_stor_info = types.get_pointer_storage_info(\n                    ptr,\n                    link_bias=isinstance(obj, s_links.Link),\n                    schema=schema,\n                )\n                if ptr_stor_info.column_type != pg_type:\n                    return None\n                val = pgast.ColumnRef(\n                    name=(table_rvar_name, ptr_stor_info.column_name)\n                )\n\n            elif ptr_name == sn.UnqualName('source'):\n                val = pgast.TypeCast(\n                    arg=pgast.NullConstant(),\n                    type_name=pgast.TypeName(name=('uuid',)),\n                )\n\n            elif ptr_name == sn.UnqualName('__fts_document__') or (\n                ptr_name.name.startswith('__ext_ai_')\n                and ptr_name.name.endswith('__')\n            ):\n                # an addon column\n                val = pgast.ColumnRef(name=(table_rvar_name, ptr_name.name))\n\n            else:\n                return None\n\n            for alias in aliases:\n                target_list.append(pgast.ResTarget(name=alias, val=val))\n\n    else:\n        for ptr_name, (aliases, _) in ptr_names.items():\n            for alias in aliases:\n                target_list.append(\n                    pgast.ResTarget(\n                        name=alias,\n                        val=pgast.ColumnRef(\n                            name=(table_rvar_name, str(ptr_name)),\n                        ),\n                    )\n                )\n\n    return pgast.SelectStmt(\n        from_clause=[\n            pgast.RelRangeVar(\n                alias=pgast.Alias(aliasname=table_rvar_name),\n                relation=pgast.Relation(\n                    schemaname=schema_name,\n                    name=table_name,\n                ),\n            )\n        ],\n        target_list=target_list,\n    )\n"
  },
  {
    "path": "edb/pgsql/keywords.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2010-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n\nkeyword_types = range(1, 5)\n(UNRESERVED_KEYWORD, RESERVED_KEYWORD,\n TYPE_FUNC_NAME_KEYWORD, COL_NAME_KEYWORD) = keyword_types\n\npg_keywords = {\n    \"abort\": (\"ABORT_P\", UNRESERVED_KEYWORD),\n    \"absolute\": (\"ABSOLUTE_P\", UNRESERVED_KEYWORD),\n    \"access\": (\"ACCESS\", UNRESERVED_KEYWORD),\n    \"action\": (\"ACTION\", UNRESERVED_KEYWORD),\n    \"add\": (\"ADD_P\", UNRESERVED_KEYWORD),\n    \"admin\": (\"ADMIN\", UNRESERVED_KEYWORD),\n    \"after\": (\"AFTER\", UNRESERVED_KEYWORD),\n    \"aggregate\": (\"AGGREGATE\", UNRESERVED_KEYWORD),\n    \"all\": (\"ALL\", RESERVED_KEYWORD),\n    \"also\": (\"ALSO\", UNRESERVED_KEYWORD),\n    \"alter\": (\"ALTER\", UNRESERVED_KEYWORD),\n    \"always\": (\"ALWAYS\", UNRESERVED_KEYWORD),\n    \"analyse\": (\"ANALYSE\", RESERVED_KEYWORD),\n    \"analyze\": (\"ANALYZE\", RESERVED_KEYWORD),\n    \"and\": (\"AND\", RESERVED_KEYWORD),\n    \"any\": (\"ANY\", RESERVED_KEYWORD),\n    \"array\": (\"ARRAY\", RESERVED_KEYWORD),\n    \"as\": (\"AS\", RESERVED_KEYWORD),\n    \"asc\": (\"ASC\", RESERVED_KEYWORD),\n    \"assertion\": (\"ASSERTION\", UNRESERVED_KEYWORD),\n    \"assignment\": (\"ASSIGNMENT\", UNRESERVED_KEYWORD),\n    \"asymmetric\": (\"ASYMMETRIC\", RESERVED_KEYWORD),\n    \"at\": (\"AT\", UNRESERVED_KEYWORD),\n    \"authorization\": (\"AUTHORIZATION\", TYPE_FUNC_NAME_KEYWORD),\n    \"backward\": (\"BACKWARD\", UNRESERVED_KEYWORD),\n    \"before\": (\"BEFORE\", UNRESERVED_KEYWORD),\n    \"begin\": (\"BEGIN_P\", UNRESERVED_KEYWORD),\n    \"between\": (\"BETWEEN\", COL_NAME_KEYWORD),\n    \"bigint\": (\"BIGINT\", COL_NAME_KEYWORD),\n    \"binary\": (\"BINARY\", TYPE_FUNC_NAME_KEYWORD),\n    \"bit\": (\"BIT\", COL_NAME_KEYWORD),\n    \"boolean\": (\"BOOLEAN_P\", COL_NAME_KEYWORD),\n    \"both\": (\"BOTH\", RESERVED_KEYWORD),\n    \"by\": (\"BY\", UNRESERVED_KEYWORD),\n    \"cache\": (\"CACHE\", UNRESERVED_KEYWORD),\n    \"called\": (\"CALLED\", UNRESERVED_KEYWORD),\n    \"cascade\": (\"CASCADE\", UNRESERVED_KEYWORD),\n    \"cascaded\": (\"CASCADED\", UNRESERVED_KEYWORD),\n    \"case\": (\"CASE\", RESERVED_KEYWORD),\n    \"cast\": (\"CAST\", RESERVED_KEYWORD),\n    \"catalog\": (\"CATALOG_P\", UNRESERVED_KEYWORD),\n    \"chain\": (\"CHAIN\", UNRESERVED_KEYWORD),\n    \"char\": (\"CHAR_P\", COL_NAME_KEYWORD),\n    \"character\": (\"CHARACTER\", COL_NAME_KEYWORD),\n    \"characteristics\": (\"CHARACTERISTICS\", UNRESERVED_KEYWORD),\n    \"check\": (\"CHECK\", RESERVED_KEYWORD),\n    \"checkpoint\": (\"CHECKPOINT\", UNRESERVED_KEYWORD),\n    \"class\": (\"CLASS\", UNRESERVED_KEYWORD),\n    \"close\": (\"CLOSE\", UNRESERVED_KEYWORD),\n    \"cluster\": (\"CLUSTER\", UNRESERVED_KEYWORD),\n    \"coalesce\": (\"COALESCE\", COL_NAME_KEYWORD),\n    \"collate\": (\"COLLATE\", RESERVED_KEYWORD),\n    \"column\": (\"COLUMN\", RESERVED_KEYWORD),\n    \"comment\": (\"COMMENT\", UNRESERVED_KEYWORD),\n    \"comments\": (\"COMMENTS\", UNRESERVED_KEYWORD),\n    \"commit\": (\"COMMIT\", UNRESERVED_KEYWORD),\n    \"committed\": (\"COMMITTED\", UNRESERVED_KEYWORD),\n    \"concurrently\": (\"CONCURRENTLY\", TYPE_FUNC_NAME_KEYWORD),\n    \"configuration\": (\"CONFIGURATION\", UNRESERVED_KEYWORD),\n    \"connection\": (\"CONNECTION\", UNRESERVED_KEYWORD),\n    \"constraint\": (\"CONSTRAINT\", RESERVED_KEYWORD),\n    \"constraints\": (\"CONSTRAINTS\", UNRESERVED_KEYWORD),\n    \"content\": (\"CONTENT_P\", UNRESERVED_KEYWORD),\n    \"continue\": (\"CONTINUE_P\", UNRESERVED_KEYWORD),\n    \"conversion\": (\"CONVERSION_P\", UNRESERVED_KEYWORD),\n    \"copy\": (\"COPY\", UNRESERVED_KEYWORD),\n    \"cost\": (\"COST\", UNRESERVED_KEYWORD),\n    \"create\": (\"CREATE\", RESERVED_KEYWORD),\n    \"createdb\": (\"CREATEDB\", UNRESERVED_KEYWORD),\n    \"createrole\": (\"CREATEROLE\", UNRESERVED_KEYWORD),\n    \"createuser\": (\"CREATEUSER\", UNRESERVED_KEYWORD),\n    \"cross\": (\"CROSS\", TYPE_FUNC_NAME_KEYWORD),\n    \"csv\": (\"CSV\", UNRESERVED_KEYWORD),\n    \"current\": (\"CURRENT_P\", UNRESERVED_KEYWORD),\n    \"current_catalog\": (\"CURRENT_CATALOG\", RESERVED_KEYWORD),\n    \"current_date\": (\"CURRENT_DATE\", RESERVED_KEYWORD),\n    \"current_role\": (\"CURRENT_ROLE\", RESERVED_KEYWORD),\n    \"current_schema\": (\"CURRENT_SCHEMA\", TYPE_FUNC_NAME_KEYWORD),\n    \"current_time\": (\"CURRENT_TIME\", RESERVED_KEYWORD),\n    \"current_timestamp\": (\"CURRENT_TIMESTAMP\", RESERVED_KEYWORD),\n    \"current_user\": (\"CURRENT_USER\", RESERVED_KEYWORD),\n    \"cursor\": (\"CURSOR\", UNRESERVED_KEYWORD),\n    \"cycle\": (\"CYCLE\", UNRESERVED_KEYWORD),\n    \"data\": (\"DATA_P\", UNRESERVED_KEYWORD),\n    \"database\": (\"DATABASE\", UNRESERVED_KEYWORD),\n    \"day\": (\"DAY_P\", UNRESERVED_KEYWORD),\n    \"deallocate\": (\"DEALLOCATE\", UNRESERVED_KEYWORD),\n    \"dec\": (\"DEC\", COL_NAME_KEYWORD),\n    \"decimal\": (\"DECIMAL_P\", COL_NAME_KEYWORD),\n    \"declare\": (\"DECLARE\", UNRESERVED_KEYWORD),\n    \"default\": (\"DEFAULT\", RESERVED_KEYWORD),\n    \"defaults\": (\"DEFAULTS\", UNRESERVED_KEYWORD),\n    \"deferrable\": (\"DEFERRABLE\", RESERVED_KEYWORD),\n    \"deferred\": (\"DEFERRED\", UNRESERVED_KEYWORD),\n    \"definer\": (\"DEFINER\", UNRESERVED_KEYWORD),\n    \"delete\": (\"DELETE_P\", UNRESERVED_KEYWORD),\n    \"delimiter\": (\"DELIMITER\", UNRESERVED_KEYWORD),\n    \"delimiters\": (\"DELIMITERS\", UNRESERVED_KEYWORD),\n    \"desc\": (\"DESC\", RESERVED_KEYWORD),\n    \"dictionary\": (\"DICTIONARY\", UNRESERVED_KEYWORD),\n    \"disable\": (\"DISABLE_P\", UNRESERVED_KEYWORD),\n    \"discard\": (\"DISCARD\", UNRESERVED_KEYWORD),\n    \"distinct\": (\"DISTINCT\", RESERVED_KEYWORD),\n    \"do\": (\"DO\", RESERVED_KEYWORD),\n    \"document\": (\"DOCUMENT_P\", UNRESERVED_KEYWORD),\n    \"domain\": (\"DOMAIN_P\", UNRESERVED_KEYWORD),\n    \"double\": (\"DOUBLE_P\", UNRESERVED_KEYWORD),\n    \"drop\": (\"DROP\", UNRESERVED_KEYWORD),\n    \"each\": (\"EACH\", UNRESERVED_KEYWORD),\n    \"else\": (\"ELSE\", RESERVED_KEYWORD),\n    \"enable\": (\"ENABLE_P\", UNRESERVED_KEYWORD),\n    \"encoding\": (\"ENCODING\", UNRESERVED_KEYWORD),\n    \"encrypted\": (\"ENCRYPTED\", UNRESERVED_KEYWORD),\n    \"end\": (\"END_P\", RESERVED_KEYWORD),\n    \"enum\": (\"ENUM_P\", UNRESERVED_KEYWORD),\n    \"escape\": (\"ESCAPE\", UNRESERVED_KEYWORD),\n    \"except\": (\"EXCEPT\", RESERVED_KEYWORD),\n    \"exclude\": (\"EXCLUDE\", UNRESERVED_KEYWORD),\n    \"excluding\": (\"EXCLUDING\", UNRESERVED_KEYWORD),\n    \"exclusive\": (\"EXCLUSIVE\", UNRESERVED_KEYWORD),\n    \"execute\": (\"EXECUTE\", UNRESERVED_KEYWORD),\n    \"exists\": (\"EXISTS\", COL_NAME_KEYWORD),\n    \"explain\": (\"EXPLAIN\", UNRESERVED_KEYWORD),\n    \"external\": (\"EXTERNAL\", UNRESERVED_KEYWORD),\n    \"extract\": (\"EXTRACT\", COL_NAME_KEYWORD),\n    \"false\": (\"FALSE_P\", RESERVED_KEYWORD),\n    \"family\": (\"FAMILY\", UNRESERVED_KEYWORD),\n    \"fetch\": (\"FETCH\", RESERVED_KEYWORD),\n    \"first\": (\"FIRST_P\", UNRESERVED_KEYWORD),\n    \"float\": (\"FLOAT_P\", COL_NAME_KEYWORD),\n    \"following\": (\"FOLLOWING\", UNRESERVED_KEYWORD),\n    \"for\": (\"FOR\", RESERVED_KEYWORD),\n    \"force\": (\"FORCE\", UNRESERVED_KEYWORD),\n    \"foreign\": (\"FOREIGN\", RESERVED_KEYWORD),\n    \"forward\": (\"FORWARD\", UNRESERVED_KEYWORD),\n    \"freeze\": (\"FREEZE\", TYPE_FUNC_NAME_KEYWORD),\n    \"from\": (\"FROM\", RESERVED_KEYWORD),\n    \"full\": (\"FULL\", TYPE_FUNC_NAME_KEYWORD),\n    \"function\": (\"FUNCTION\", UNRESERVED_KEYWORD),\n    \"functions\": (\"FUNCTIONS\", UNRESERVED_KEYWORD),\n    \"global\": (\"GLOBAL\", UNRESERVED_KEYWORD),\n    \"grant\": (\"GRANT\", RESERVED_KEYWORD),\n    \"granted\": (\"GRANTED\", UNRESERVED_KEYWORD),\n    \"greatest\": (\"GREATEST\", COL_NAME_KEYWORD),\n    \"group\": (\"GROUP_P\", RESERVED_KEYWORD),\n    \"handler\": (\"HANDLER\", UNRESERVED_KEYWORD),\n    \"having\": (\"HAVING\", RESERVED_KEYWORD),\n    \"header\": (\"HEADER_P\", UNRESERVED_KEYWORD),\n    \"hold\": (\"HOLD\", UNRESERVED_KEYWORD),\n    \"hour\": (\"HOUR_P\", UNRESERVED_KEYWORD),\n    \"identity\": (\"IDENTITY_P\", UNRESERVED_KEYWORD),\n    \"if\": (\"IF_P\", UNRESERVED_KEYWORD),\n    \"ilike\": (\"ILIKE\", TYPE_FUNC_NAME_KEYWORD),\n    \"immediate\": (\"IMMEDIATE\", UNRESERVED_KEYWORD),\n    \"immutable\": (\"IMMUTABLE\", UNRESERVED_KEYWORD),\n    \"implicit\": (\"IMPLICIT_P\", UNRESERVED_KEYWORD),\n    \"in\": (\"IN_P\", RESERVED_KEYWORD),\n    \"including\": (\"INCLUDING\", UNRESERVED_KEYWORD),\n    \"increment\": (\"INCREMENT\", UNRESERVED_KEYWORD),\n    \"index\": (\"INDEX\", UNRESERVED_KEYWORD),\n    \"indexes\": (\"INDEXES\", UNRESERVED_KEYWORD),\n    \"inherit\": (\"INHERIT\", UNRESERVED_KEYWORD),\n    \"inherits\": (\"INHERITS\", UNRESERVED_KEYWORD),\n    \"initially\": (\"INITIALLY\", RESERVED_KEYWORD),\n    \"inline\": (\"INLINE_P\", UNRESERVED_KEYWORD),\n    \"inner\": (\"INNER_P\", TYPE_FUNC_NAME_KEYWORD),\n    \"inout\": (\"INOUT\", COL_NAME_KEYWORD),\n    \"input\": (\"INPUT_P\", UNRESERVED_KEYWORD),\n    \"insensitive\": (\"INSENSITIVE\", UNRESERVED_KEYWORD),\n    \"insert\": (\"INSERT\", UNRESERVED_KEYWORD),\n    \"instead\": (\"INSTEAD\", UNRESERVED_KEYWORD),\n    \"int\": (\"INT_P\", COL_NAME_KEYWORD),\n    \"integer\": (\"INTEGER\", COL_NAME_KEYWORD),\n    \"intersect\": (\"INTERSECT\", RESERVED_KEYWORD),\n    \"interval\": (\"INTERVAL\", COL_NAME_KEYWORD),\n    \"into\": (\"INTO\", RESERVED_KEYWORD),\n    \"invoker\": (\"INVOKER\", UNRESERVED_KEYWORD),\n    \"is\": (\"IS\", TYPE_FUNC_NAME_KEYWORD),\n    \"isnull\": (\"ISNULL\", TYPE_FUNC_NAME_KEYWORD),\n    \"isolation\": (\"ISOLATION\", UNRESERVED_KEYWORD),\n    \"join\": (\"JOIN\", TYPE_FUNC_NAME_KEYWORD),\n    \"json\": (\"JSON\", TYPE_FUNC_NAME_KEYWORD),\n    \"json_array\": (\"JSON_ARRAY\", TYPE_FUNC_NAME_KEYWORD),\n    \"json_arrayagg\": (\"JSON_ARRAYAGG\", TYPE_FUNC_NAME_KEYWORD),\n    \"json_exists\": (\"JSON_EXISTS\", TYPE_FUNC_NAME_KEYWORD),\n    \"json_object\": (\"JSON_OBJECT\", TYPE_FUNC_NAME_KEYWORD),\n    \"json_objectagg\": (\"JSON_OBJECTAGG\", TYPE_FUNC_NAME_KEYWORD),\n    \"json_query\": (\"JSON_QUERY\", TYPE_FUNC_NAME_KEYWORD),\n    \"json_scalar\": (\"JSON_SCALAR\", TYPE_FUNC_NAME_KEYWORD),\n    \"json_serialize\": (\"JSON_SERIALIZE\", TYPE_FUNC_NAME_KEYWORD),\n    \"json_table\": (\"JSON_TABLE\", TYPE_FUNC_NAME_KEYWORD),\n    \"json_table_primitive\": (\"JSON_TABLE_PRIMITIVE\", TYPE_FUNC_NAME_KEYWORD),\n    \"json_value\": (\"JSON_VALUE\", TYPE_FUNC_NAME_KEYWORD),\n    \"key\": (\"KEY\", UNRESERVED_KEYWORD),\n    \"language\": (\"LANGUAGE\", UNRESERVED_KEYWORD),\n    \"large\": (\"LARGE_P\", UNRESERVED_KEYWORD),\n    \"last\": (\"LAST_P\", UNRESERVED_KEYWORD),\n    \"lc_collate\": (\"LC_COLLATE_P\", UNRESERVED_KEYWORD),\n    \"lc_ctype\": (\"LC_CTYPE_P\", UNRESERVED_KEYWORD),\n    \"leading\": (\"LEADING\", RESERVED_KEYWORD),\n    \"least\": (\"LEAST\", COL_NAME_KEYWORD),\n    \"left\": (\"LEFT\", TYPE_FUNC_NAME_KEYWORD),\n    \"level\": (\"LEVEL\", UNRESERVED_KEYWORD),\n    \"like\": (\"LIKE\", TYPE_FUNC_NAME_KEYWORD),\n    \"limit\": (\"LIMIT\", RESERVED_KEYWORD),\n    \"listen\": (\"LISTEN\", UNRESERVED_KEYWORD),\n    \"load\": (\"LOAD\", UNRESERVED_KEYWORD),\n    \"local\": (\"LOCAL\", UNRESERVED_KEYWORD),\n    \"localtime\": (\"LOCALTIME\", RESERVED_KEYWORD),\n    \"localtimestamp\": (\"LOCALTIMESTAMP\", RESERVED_KEYWORD),\n    \"location\": (\"LOCATION\", UNRESERVED_KEYWORD),\n    \"lock\": (\"LOCK_P\", UNRESERVED_KEYWORD),\n    \"login\": (\"LOGIN_P\", UNRESERVED_KEYWORD),\n    \"mapping\": (\"MAPPING\", UNRESERVED_KEYWORD),\n    \"match\": (\"MATCH\", UNRESERVED_KEYWORD),\n    \"maxvalue\": (\"MAXVALUE\", UNRESERVED_KEYWORD),\n    \"minute\": (\"MINUTE_P\", UNRESERVED_KEYWORD),\n    \"minvalue\": (\"MINVALUE\", UNRESERVED_KEYWORD),\n    \"mode\": (\"MODE\", UNRESERVED_KEYWORD),\n    \"month\": (\"MONTH_P\", UNRESERVED_KEYWORD),\n    \"move\": (\"MOVE\", UNRESERVED_KEYWORD),\n    \"name\": (\"NAME_P\", UNRESERVED_KEYWORD),\n    \"names\": (\"NAMES\", UNRESERVED_KEYWORD),\n    \"national\": (\"NATIONAL\", COL_NAME_KEYWORD),\n    \"natural\": (\"NATURAL\", TYPE_FUNC_NAME_KEYWORD),\n    \"nchar\": (\"NCHAR\", COL_NAME_KEYWORD),\n    \"next\": (\"NEXT\", UNRESERVED_KEYWORD),\n    \"no\": (\"NO\", UNRESERVED_KEYWORD),\n    \"nocreatedb\": (\"NOCREATEDB\", UNRESERVED_KEYWORD),\n    \"nocreaterole\": (\"NOCREATEROLE\", UNRESERVED_KEYWORD),\n    \"nocreateuser\": (\"NOCREATEUSER\", UNRESERVED_KEYWORD),\n    \"noinherit\": (\"NOINHERIT\", UNRESERVED_KEYWORD),\n    \"nologin\": (\"NOLOGIN_P\", UNRESERVED_KEYWORD),\n    \"none\": (\"NONE\", COL_NAME_KEYWORD),\n    \"nosuperuser\": (\"NOSUPERUSER\", UNRESERVED_KEYWORD),\n    \"not\": (\"NOT\", RESERVED_KEYWORD),\n    \"nothing\": (\"NOTHING\", UNRESERVED_KEYWORD),\n    \"notify\": (\"NOTIFY\", UNRESERVED_KEYWORD),\n    \"notnull\": (\"NOTNULL\", TYPE_FUNC_NAME_KEYWORD),\n    \"nowait\": (\"NOWAIT\", UNRESERVED_KEYWORD),\n    \"null\": (\"NULL_P\", RESERVED_KEYWORD),\n    \"nullif\": (\"NULLIF\", COL_NAME_KEYWORD),\n    \"nulls\": (\"NULLS_P\", UNRESERVED_KEYWORD),\n    \"numeric\": (\"NUMERIC\", COL_NAME_KEYWORD),\n    \"object\": (\"OBJECT_P\", UNRESERVED_KEYWORD),\n    \"of\": (\"OF\", UNRESERVED_KEYWORD),\n    \"off\": (\"OFF\", RESERVED_KEYWORD),\n    \"offset\": (\"OFFSET\", RESERVED_KEYWORD),\n    \"oids\": (\"OIDS\", UNRESERVED_KEYWORD),\n    \"on\": (\"ON\", RESERVED_KEYWORD),\n    \"only\": (\"ONLY\", RESERVED_KEYWORD),\n    \"operator\": (\"OPERATOR\", UNRESERVED_KEYWORD),\n    \"option\": (\"OPTION\", UNRESERVED_KEYWORD),\n    \"options\": (\"OPTIONS\", UNRESERVED_KEYWORD),\n    \"or\": (\"OR\", RESERVED_KEYWORD),\n    \"order\": (\"ORDER\", RESERVED_KEYWORD),\n    \"out\": (\"OUT_P\", COL_NAME_KEYWORD),\n    \"outer\": (\"OUTER_P\", TYPE_FUNC_NAME_KEYWORD),\n    \"over\": (\"OVER\", TYPE_FUNC_NAME_KEYWORD),\n    \"overlaps\": (\"OVERLAPS\", TYPE_FUNC_NAME_KEYWORD),\n    \"overlay\": (\"OVERLAY\", COL_NAME_KEYWORD),\n    \"owned\": (\"OWNED\", UNRESERVED_KEYWORD),\n    \"owner\": (\"OWNER\", UNRESERVED_KEYWORD),\n    \"parser\": (\"PARSER\", UNRESERVED_KEYWORD),\n    \"partial\": (\"PARTIAL\", UNRESERVED_KEYWORD),\n    \"partition\": (\"PARTITION\", UNRESERVED_KEYWORD),\n    \"password\": (\"PASSWORD\", UNRESERVED_KEYWORD),\n    \"placing\": (\"PLACING\", RESERVED_KEYWORD),\n    \"plans\": (\"PLANS\", UNRESERVED_KEYWORD),\n    \"position\": (\"POSITION\", COL_NAME_KEYWORD),\n    \"preceding\": (\"PRECEDING\", UNRESERVED_KEYWORD),\n    \"precision\": (\"PRECISION\", COL_NAME_KEYWORD),\n    \"prepare\": (\"PREPARE\", UNRESERVED_KEYWORD),\n    \"prepared\": (\"PREPARED\", UNRESERVED_KEYWORD),\n    \"preserve\": (\"PRESERVE\", UNRESERVED_KEYWORD),\n    \"primary\": (\"PRIMARY\", RESERVED_KEYWORD),\n    \"prior\": (\"PRIOR\", UNRESERVED_KEYWORD),\n    \"privileges\": (\"PRIVILEGES\", UNRESERVED_KEYWORD),\n    \"procedural\": (\"PROCEDURAL\", UNRESERVED_KEYWORD),\n    \"procedure\": (\"PROCEDURE\", UNRESERVED_KEYWORD),\n    \"quote\": (\"QUOTE\", UNRESERVED_KEYWORD),\n    \"range\": (\"RANGE\", UNRESERVED_KEYWORD),\n    \"read\": (\"READ\", UNRESERVED_KEYWORD),\n    \"real\": (\"REAL\", COL_NAME_KEYWORD),\n    \"reassign\": (\"REASSIGN\", UNRESERVED_KEYWORD),\n    \"recheck\": (\"RECHECK\", UNRESERVED_KEYWORD),\n    \"recursive\": (\"RECURSIVE\", UNRESERVED_KEYWORD),\n    \"references\": (\"REFERENCES\", RESERVED_KEYWORD),\n    \"reindex\": (\"REINDEX\", UNRESERVED_KEYWORD),\n    \"relative\": (\"RELATIVE_P\", UNRESERVED_KEYWORD),\n    \"release\": (\"RELEASE\", UNRESERVED_KEYWORD),\n    \"rename\": (\"RENAME\", UNRESERVED_KEYWORD),\n    \"repeatable\": (\"REPEATABLE\", UNRESERVED_KEYWORD),\n    \"replace\": (\"REPLACE\", UNRESERVED_KEYWORD),\n    \"replica\": (\"REPLICA\", UNRESERVED_KEYWORD),\n    \"reset\": (\"RESET\", UNRESERVED_KEYWORD),\n    \"restart\": (\"RESTART\", UNRESERVED_KEYWORD),\n    \"restrict\": (\"RESTRICT\", UNRESERVED_KEYWORD),\n    \"returning\": (\"RETURNING\", RESERVED_KEYWORD),\n    \"returns\": (\"RETURNS\", UNRESERVED_KEYWORD),\n    \"revoke\": (\"REVOKE\", UNRESERVED_KEYWORD),\n    \"right\": (\"RIGHT\", TYPE_FUNC_NAME_KEYWORD),\n    \"role\": (\"ROLE\", UNRESERVED_KEYWORD),\n    \"rollback\": (\"ROLLBACK\", UNRESERVED_KEYWORD),\n    \"row\": (\"ROW\", COL_NAME_KEYWORD),\n    \"rows\": (\"ROWS\", UNRESERVED_KEYWORD),\n    \"rule\": (\"RULE\", UNRESERVED_KEYWORD),\n    \"savepoint\": (\"SAVEPOINT\", UNRESERVED_KEYWORD),\n    \"schema\": (\"SCHEMA\", UNRESERVED_KEYWORD),\n    \"scroll\": (\"SCROLL\", UNRESERVED_KEYWORD),\n    \"search\": (\"SEARCH\", UNRESERVED_KEYWORD),\n    \"second\": (\"SECOND_P\", UNRESERVED_KEYWORD),\n    \"security\": (\"SECURITY\", UNRESERVED_KEYWORD),\n    \"select\": (\"SELECT\", RESERVED_KEYWORD),\n    \"sequence\": (\"SEQUENCE\", UNRESERVED_KEYWORD),\n    \"sequences\": (\"SEQUENCES\", UNRESERVED_KEYWORD),\n    \"serializable\": (\"SERIALIZABLE\", UNRESERVED_KEYWORD),\n    \"server\": (\"SERVER\", UNRESERVED_KEYWORD),\n    \"session\": (\"SESSION\", UNRESERVED_KEYWORD),\n    \"session_user\": (\"SESSION_USER\", RESERVED_KEYWORD),\n    \"set\": (\"SET\", UNRESERVED_KEYWORD),\n    \"setof\": (\"SETOF\", COL_NAME_KEYWORD),\n    \"share\": (\"SHARE\", UNRESERVED_KEYWORD),\n    \"show\": (\"SHOW\", UNRESERVED_KEYWORD),\n    \"similar\": (\"SIMILAR\", TYPE_FUNC_NAME_KEYWORD),\n    \"simple\": (\"SIMPLE\", UNRESERVED_KEYWORD),\n    \"smallint\": (\"SMALLINT\", COL_NAME_KEYWORD),\n    \"some\": (\"SOME\", RESERVED_KEYWORD),\n    \"stable\": (\"STABLE\", UNRESERVED_KEYWORD),\n    \"standalone\": (\"STANDALONE_P\", UNRESERVED_KEYWORD),\n    \"start\": (\"START\", UNRESERVED_KEYWORD),\n    \"statement\": (\"STATEMENT\", UNRESERVED_KEYWORD),\n    \"statistics\": (\"STATISTICS\", UNRESERVED_KEYWORD),\n    \"stdin\": (\"STDIN\", UNRESERVED_KEYWORD),\n    \"stdout\": (\"STDOUT\", UNRESERVED_KEYWORD),\n    \"storage\": (\"STORAGE\", UNRESERVED_KEYWORD),\n    \"strict\": (\"STRICT_P\", UNRESERVED_KEYWORD),\n    \"strip\": (\"STRIP_P\", UNRESERVED_KEYWORD),\n    \"substring\": (\"SUBSTRING\", COL_NAME_KEYWORD),\n    \"superuser\": (\"SUPERUSER_P\", UNRESERVED_KEYWORD),\n    \"symmetric\": (\"SYMMETRIC\", RESERVED_KEYWORD),\n    \"sysid\": (\"SYSID\", UNRESERVED_KEYWORD),\n    \"system\": (\"SYSTEM_P\", UNRESERVED_KEYWORD),\n    \"table\": (\"TABLE\", RESERVED_KEYWORD),\n    \"tables\": (\"TABLES\", UNRESERVED_KEYWORD),\n    \"tablespace\": (\"TABLESPACE\", UNRESERVED_KEYWORD),\n    \"temp\": (\"TEMP\", UNRESERVED_KEYWORD),\n    \"template\": (\"TEMPLATE\", UNRESERVED_KEYWORD),\n    \"temporary\": (\"TEMPORARY\", UNRESERVED_KEYWORD),\n    \"text\": (\"TEXT_P\", UNRESERVED_KEYWORD),\n    \"then\": (\"THEN\", RESERVED_KEYWORD),\n    \"time\": (\"TIME\", COL_NAME_KEYWORD),\n    \"timestamp\": (\"TIMESTAMP\", COL_NAME_KEYWORD),\n    \"to\": (\"TO\", RESERVED_KEYWORD),\n    \"trailing\": (\"TRAILING\", RESERVED_KEYWORD),\n    \"transaction\": (\"TRANSACTION\", UNRESERVED_KEYWORD),\n    \"treat\": (\"TREAT\", COL_NAME_KEYWORD),\n    \"trigger\": (\"TRIGGER\", UNRESERVED_KEYWORD),\n    \"trim\": (\"TRIM\", COL_NAME_KEYWORD),\n    \"true\": (\"TRUE_P\", RESERVED_KEYWORD),\n    \"truncate\": (\"TRUNCATE\", UNRESERVED_KEYWORD),\n    \"trusted\": (\"TRUSTED\", UNRESERVED_KEYWORD),\n    \"type\": (\"TYPE_P\", UNRESERVED_KEYWORD),\n    \"unbounded\": (\"UNBOUNDED\", UNRESERVED_KEYWORD),\n    \"uncommitted\": (\"UNCOMMITTED\", UNRESERVED_KEYWORD),\n    \"unencrypted\": (\"UNENCRYPTED\", UNRESERVED_KEYWORD),\n    \"union\": (\"UNION\", RESERVED_KEYWORD),\n    \"unique\": (\"UNIQUE\", RESERVED_KEYWORD),\n    \"unknown\": (\"UNKNOWN\", UNRESERVED_KEYWORD),\n    \"unlisten\": (\"UNLISTEN\", UNRESERVED_KEYWORD),\n    \"until\": (\"UNTIL\", UNRESERVED_KEYWORD),\n    \"update\": (\"UPDATE\", UNRESERVED_KEYWORD),\n    \"user\": (\"USER\", RESERVED_KEYWORD),\n    \"using\": (\"USING\", RESERVED_KEYWORD),\n    \"vacuum\": (\"VACUUM\", UNRESERVED_KEYWORD),\n    \"valid\": (\"VALID\", UNRESERVED_KEYWORD),\n    \"validator\": (\"VALIDATOR\", UNRESERVED_KEYWORD),\n    \"value\": (\"VALUE_P\", UNRESERVED_KEYWORD),\n    \"values\": (\"VALUES\", COL_NAME_KEYWORD),\n    \"varchar\": (\"VARCHAR\", COL_NAME_KEYWORD),\n    \"variadic\": (\"VARIADIC\", RESERVED_KEYWORD),\n    \"varying\": (\"VARYING\", UNRESERVED_KEYWORD),\n    \"verbose\": (\"VERBOSE\", TYPE_FUNC_NAME_KEYWORD),\n    \"version\": (\"VERSION_P\", UNRESERVED_KEYWORD),\n    \"view\": (\"VIEW\", UNRESERVED_KEYWORD),\n    \"volatile\": (\"VOLATILE\", UNRESERVED_KEYWORD),\n    \"when\": (\"WHEN\", RESERVED_KEYWORD),\n    \"where\": (\"WHERE\", RESERVED_KEYWORD),\n    \"whitespace\": (\"WHITESPACE_P\", UNRESERVED_KEYWORD),\n    \"window\": (\"WINDOW\", RESERVED_KEYWORD),\n    \"with\": (\"WITH\", RESERVED_KEYWORD),\n    \"without\": (\"WITHOUT\", UNRESERVED_KEYWORD),\n    \"work\": (\"WORK\", UNRESERVED_KEYWORD),\n    \"wrapper\": (\"WRAPPER\", UNRESERVED_KEYWORD),\n    \"write\": (\"WRITE\", UNRESERVED_KEYWORD),\n    \"xml\": (\"XML_P\", UNRESERVED_KEYWORD),\n    \"xmlattributes\": (\"XMLATTRIBUTES\", COL_NAME_KEYWORD),\n    \"xmlconcat\": (\"XMLCONCAT\", COL_NAME_KEYWORD),\n    \"xmlelement\": (\"XMLELEMENT\", COL_NAME_KEYWORD),\n    \"xmlforest\": (\"XMLFOREST\", COL_NAME_KEYWORD),\n    \"xmlparse\": (\"XMLPARSE\", COL_NAME_KEYWORD),\n    \"xmlpi\": (\"XMLPI\", COL_NAME_KEYWORD),\n    \"xmlroot\": (\"XMLROOT\", COL_NAME_KEYWORD),\n    \"xmlserialize\": (\"XMLSERIALIZE\", COL_NAME_KEYWORD),\n    \"year\": (\"YEAR_P\", UNRESERVED_KEYWORD),\n    \"yes\": (\"YES_P\", UNRESERVED_KEYWORD),\n    \"zone\": (\"ZONE\", UNRESERVED_KEYWORD),\n}\n\nby_type: dict[int, dict[str, str]] = {typ: {} for typ in keyword_types}\n\nfor val, spec in pg_keywords.items():\n    by_type[spec[1]][val] = spec[0]\n"
  },
  {
    "path": "edb/pgsql/metaschema.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Database structure and objects supporting Gel metadata.\"\"\"\n\nfrom __future__ import annotations\nfrom typing import (\n    Callable,\n    Optional,\n    Protocol,\n    Iterable,\n    Sequence,\n    cast,\n    Any\n)\n\nimport functools\nimport json\nimport re\n\nimport edb._edgeql_parser as ql_parser\n\nfrom edb.common import debug\nfrom edb.common import exceptions\nfrom edb.common import ordered\nfrom edb.common import uuidgen\nfrom edb.common import xdedent\nfrom edb.common.typeutils import not_none\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\nfrom edb.edgeql import quote as qlquote\nfrom edb.edgeql import compiler as qlcompiler\n\nfrom edb.ir import statypes\n\nfrom edb.schema import constraints as s_constr\nfrom edb.schema import links as s_links\nfrom edb.schema import name as s_name\nfrom edb.schema import objects as s_obj\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import properties as s_props\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import schema as s_schema\nfrom edb.schema import sources as s_sources\nfrom edb.schema import types as s_types\nfrom edb.schema import utils as s_utils\n\nfrom edb.server import defines\nfrom edb.server import compiler as edbcompiler\nfrom edb.server import config as edbconfig\nfrom edb.server import pgcon  # HM.\n\nfrom .resolver import sql_introspection\n\nfrom . import codegen\nfrom . import common\nfrom . import compiler\nfrom . import dbops\nfrom . import inheritance\nfrom . import params\nfrom . import trampoline\nfrom . import types\n\nq = common.qname\nqi = common.quote_ident\nql = common.quote_literal\nqt = common.quote_type\nV = common.versioned_schema\n\n\nDATABASE_ID_NAMESPACE = uuidgen.UUID('0e6fed66-204b-11e9-8666-cffd58a5240b')\nCONFIG_ID_NAMESPACE = uuidgen.UUID('a48b38fa-349b-11e9-a6be-4f337f82f5ad')\nCONFIG_ID = {\n    None: uuidgen.UUID('172097a4-39f4-11e9-b189-9321eb2f4b97'),\n    qltypes.ConfigScope.INSTANCE: uuidgen.UUID(\n        '172097a4-39f4-11e9-b189-9321eb2f4b98'),\n    qltypes.ConfigScope.DATABASE: uuidgen.UUID(\n        '172097a4-39f4-11e9-b189-9321eb2f4b99'),\n}\n\n\ndef qtl(t: tuple[str, ...]) -> str:\n    \"\"\"Quote type literal\"\"\"\n    return ql(f'{t[0]}.{t[1]}') if len(t) == 2 else ql(f'pg_catalog.{t[0]}')\n\n\nclass PGConnection(Protocol):\n\n    async def sql_execute(\n        self,\n        sql: bytes,\n    ) -> None:\n        ...\n\n    async def sql_fetch(\n        self,\n        sql: bytes,\n        *,\n        args: tuple[bytes, ...] | list[bytes] = (),\n    ) -> list[tuple[bytes, ...]]:\n        ...\n\n    async def sql_fetch_val(\n        self,\n        sql: bytes,\n        *,\n        args: tuple[bytes, ...] | list[bytes] = (),\n    ) -> bytes:\n        ...\n\n    async def sql_fetch_col(\n        self,\n        sql: bytes,\n        *,\n        args: tuple[bytes, ...] | list[bytes] = (),\n    ) -> list[bytes]:\n        ...\n\n\nclass DBConfigTable(dbops.Table):\n    def __init__(self) -> None:\n        super().__init__(name=('edgedb', '_db_config'))\n\n        self.add_columns([\n            dbops.Column(name='name', type='text'),\n            dbops.Column(name='value', type='jsonb'),\n        ])\n\n        self.add_constraint(\n            dbops.UniqueConstraint(\n                table_name=('edgedb', '_db_config'),\n                columns=['name'],\n            ),\n        )\n\n\nclass InstDataTable(dbops.Table):\n    def __init__(self) -> None:\n        sname = V('edgedbinstdata')\n        super().__init__(\n            name=(sname, 'instdata'),\n            columns=[\n                dbops.Column(\n                    name='key',\n                    type='text',\n                ),\n                dbops.Column(\n                    name='bin',\n                    type='bytea',\n                ),\n                dbops.Column(\n                    name='text',\n                    type='text',\n                ),\n                dbops.Column(\n                    name='json',\n                    type='jsonb',\n                ),\n            ],\n            constraints=ordered.OrderedSet([\n                dbops.PrimaryKey(\n                    table_name=(sname, 'instdata'),\n                    columns=['key'],\n                ),\n            ]),\n        )\n\n\nclass QueryCacheTable(dbops.Table):\n    def __init__(self) -> None:\n        super().__init__(name=('edgedb', '_query_cache'))\n\n        self.add_columns([\n            dbops.Column(name='key', type='uuid', required=True),\n            dbops.Column(name='schema_version', type='uuid', required=True),\n            dbops.Column(name='input', type='bytea', required=True),\n            dbops.Column(name='output', type='bytea', required=True),\n            dbops.Column(name='evict', type='text', required=True),\n            dbops.Column(\n                name='creation_time',\n                type='timestamp with time zone',\n                required=True,\n                default='current_timestamp',\n            ),\n        ])\n\n        self.add_constraint(\n            dbops.PrimaryKey(\n                table_name=('edgedb', '_query_cache'),\n                columns=['key'],\n            ),\n        )\n\n\nclass EvictQueryCacheFunction(trampoline.VersionedFunction):\n\n    text = f'''\n    DECLARE\n        evict_sql text;\n    BEGIN\n        DELETE FROM \"edgedb\".\"_query_cache\"\n            WHERE \"key\" = cache_key\n            RETURNING \"evict\" INTO evict_sql;\n        IF evict_sql IS NOT NULL THEN\n            EXECUTE evict_sql;\n        END IF;\n    END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_evict_query_cache'),\n            args=[(\"cache_key\", (\"uuid\",))],\n            returns=(\"void\",),\n            language='plpgsql',\n            volatility='volatile',\n            text=self.text,\n        )\n\n\nclass ClearQueryCacheFunction(trampoline.VersionedFunction):\n\n    # TODO(fantix): this may consume a lot of memory in Postgres\n    text = f'''\n    DECLARE\n        row record;\n    BEGIN\n        FOR row IN\n            DELETE FROM \"edgedb\".\"_query_cache\"\n            RETURNING \"input\", \"evict\"\n        LOOP\n            EXECUTE row.\"evict\";\n            RETURN NEXT row.\"input\";\n        END LOOP;\n    END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_clear_query_cache'),\n            args=[],\n            returns=('bytea',),\n            set_returning=True,\n            language='plpgsql',\n            volatility='volatile',\n            text=self.text,\n        )\n\n\nclass CreateTrampolineViewFunction(trampoline.VersionedFunction):\n    text = f'''\n        DECLARE\n            cols text;\n            tgt text;\n            dummy text;\n        BEGIN\n            tgt := quote_ident(tgt_schema) || '.' || quote_ident(tgt_name);\n\n            -- Check if the view already exists.\n            select viewname into dummy\n            from pg_catalog.pg_views\n            where schemaname = tgt_schema\n            and viewname = tgt_name;\n\n            IF FOUND THEN\n                -- If the view already existed, we need to generate a column\n                -- list that maintains the order of anything that was present in\n                -- the old view, and that doesn't remove any columns that were\n                -- dropped.\n                select\n                  string_agg(\n                    COALESCE(\n                      quote_ident(tname),\n                      'NULL::' || vtypname || ' AS ' || quote_ident(vname)\n                    ),\n                    ','\n                  )\n                from (\n                  select\n                    a1.attname as tname,\n                    a2.attname as vname,\n                    pg_catalog.format_type(a2.atttypid, NULL) as vtypname\n                  from (\n                    select * from pg_catalog.pg_attribute\n                    where attrelid = src::regclass::oid\n                    and attnum >= 0\n                  ) a1\n                  full outer join (\n                    select * from pg_catalog.pg_attribute\n                    where attrelid = tgt::regclass::oid\n                  ) a2\n                  on a1.attname = a2.attname\n                  order by a2.attnum, a1.attnum\n                ) t\n                INTO cols;\n\n            END IF;\n\n            -- If it doesn't exist or has no columns, create it with SELECT *\n            cols := COALESCE(cols, '*');\n\n            EXECUTE 'CREATE OR REPLACE VIEW ' || tgt || ' AS ' ||\n              'SELECT ' || cols || ' FROM ' || src;\n\n        END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_create_trampoline_view'),\n            args=[\n                ('src', ('text',)),\n                ('tgt_schema', ('text',)),\n                ('tgt_name', ('text',)),\n            ],\n            returns=('void',),\n            language='plpgsql',\n            volatility='volatile',\n            text=self.text,\n        )\n\n\nclass BigintDomain(dbops.Domain):\n    \"\"\"Bigint: a variant of numeric that enforces zero digits after the dot.\n\n    We're using an explicit scale check as opposed to simply specifying\n    the numeric bounds, because using bounds severly restricts the range\n    of the numeric type (1000 vs 131072 digits).\n    \"\"\"\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedbt', 'bigint_t'),\n            base='numeric',\n            constraints=(\n                dbops.DomainCheckConstraint(\n                    domain_name=('edgedbt', 'bigint_t'),\n                    expr=(\"scale(VALUE) = 0 AND VALUE != 'NaN'\"),\n                ),\n            ),\n        )\n\n\nclass ConfigMemoryDomain(dbops.Domain):\n    \"\"\"Represents the cfg::memory type. Stores number of bytes.\n\n    Defined just as edgedbt.bigint_t:\n\n    * numeric is used to ensure we can comfortably represent huge amounts\n      of data beyond petabytes;\n    * enforces zero digits after the dot.\n    \"\"\"\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedbt', 'memory_t'),\n            base='int8',\n            constraints=(\n                dbops.DomainCheckConstraint(\n                    domain_name=('edgedbt', 'memory_t'),\n                    expr=(\"VALUE >= 0\"),\n                ),\n            ),\n        )\n\n\nclass TimestampTzDomain(dbops.Domain):\n    \"\"\"Timestamptz clamped to years 0001-9999.\n\n    The default timestamp range of (4713 BC - 294276 AD) has problems:\n    Postgres isn't ISO compliant with years out of the 1-9999 range and\n    language compatibility is questionable.\n    \"\"\"\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedbt', 'timestamptz_t'),\n            base='timestamptz',\n            constraints=(\n                dbops.DomainCheckConstraint(\n                    domain_name=('edgedbt', 'timestamptz_t'),\n                    expr=(\"EXTRACT(years from VALUE) BETWEEN 1 AND 9999\"),\n                ),\n            ),\n        )\n\n\nclass TimestampDomain(dbops.Domain):\n    \"\"\"Timestamp clamped to years 0001-9999.\n\n    The default timestamp range of (4713 BC - 294276 AD) has problems:\n    Postgres isn't ISO compliant with years out of the 1-9999 range and\n    language compatibility is questionable.\n    \"\"\"\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedbt', 'timestamp_t'),\n            base='timestamp',\n            constraints=(\n                dbops.DomainCheckConstraint(\n                    domain_name=('edgedbt', 'timestamp_t'),\n                    expr=(\"EXTRACT(years from VALUE) BETWEEN 1 AND 9999\"),\n                ),\n            ),\n        )\n\n\nclass DateDomain(dbops.Domain):\n    \"\"\"Date clamped to years 0001-9999.\n\n    The default timestamp range of (4713 BC - 294276 AD) has problems:\n    Postgres isn't ISO compliant with years out of the 1-9999 range and\n    language compatibility is questionable.\n    \"\"\"\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedbt', 'date_t'),\n            base='date',\n            constraints=(\n                dbops.DomainCheckConstraint(\n                    domain_name=('edgedbt', 'date_t'),\n                    expr=(\"EXTRACT(years from VALUE) BETWEEN 1 AND 9999\"),\n                ),\n            ),\n        )\n\n\nclass DurationDomain(dbops.Domain):\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedbt', 'duration_t'),\n            base='interval',\n            constraints=(\n                dbops.DomainCheckConstraint(\n                    domain_name=('edgedbt', 'duration_t'),\n                    expr=r'''\n                        EXTRACT(months from VALUE) = 0 AND\n                        EXTRACT(years from VALUE) = 0 AND\n                        EXTRACT(days from VALUE) = 0\n                    ''',\n                ),\n            ),\n        )\n\n\nclass RelativeDurationDomain(dbops.Domain):\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedbt', 'relative_duration_t'),\n            base='interval',\n            constraints=(\n                dbops.DomainCheckConstraint(\n                    domain_name=('edgedbt', 'relative_duration_t'),\n                    expr=\"true\",\n                ),\n            ),\n        )\n\n\nclass DateDurationDomain(dbops.Domain):\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedbt', 'date_duration_t'),\n            base='interval',\n            constraints=(\n                dbops.DomainCheckConstraint(\n                    domain_name=('edgedbt', 'date_duration_t'),\n                    expr=r'''\n                        EXTRACT(hour from VALUE) = 0 AND\n                        EXTRACT(minute from VALUE) = 0 AND\n                        EXTRACT(second from VALUE) = 0\n                    ''',\n                ),\n            ),\n        )\n\n\nclass Float32Range(dbops.Range):\n    def __init__(self) -> None:\n        super().__init__(\n            name=types.type_to_range_name_map[('float4',)],\n            subtype=('float4',),\n        )\n\n\nclass Float64Range(dbops.Range):\n    def __init__(self) -> None:\n        super().__init__(\n            name=types.type_to_range_name_map[('float8',)],\n            subtype=('float8',),\n            subtype_diff=('float8mi',)\n        )\n\n\nclass DatetimeRange(dbops.Range):\n    def __init__(self) -> None:\n        super().__init__(\n            name=types.type_to_range_name_map[('edgedbt', 'timestamptz_t')],\n            subtype=('edgedbt', 'timestamptz_t'),\n        )\n\n\nclass LocalDatetimeRange(dbops.Range):\n    def __init__(self) -> None:\n        super().__init__(\n            name=types.type_to_range_name_map[('edgedbt', 'timestamp_t')],\n            subtype=('edgedbt', 'timestamp_t'),\n        )\n\n\nclass RangeToJsonFunction(trampoline.VersionedFunction):\n    \"\"\"Convert anyrange to a jsonb object.\"\"\"\n    text = r'''\n        SELECT\n            CASE\n            WHEN val IS NULL THEN\n                NULL\n            WHEN isempty(val) THEN\n                jsonb_build_object('empty', true)\n            ELSE\n                to_jsonb(o)\n            END\n        FROM\n            (SELECT\n                lower(val) as lower,\n                lower_inc(val) as inc_lower,\n                upper(val) as upper,\n                upper_inc(val) as inc_upper\n            ) AS o\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'range_to_jsonb'),\n            args=[\n                ('val', ('anyrange',)),\n            ],\n            returns=('jsonb',),\n            volatility='immutable',\n            language='sql',\n            text=self.text,\n        )\n\n\nclass MultiRangeToJsonFunction(trampoline.VersionedFunction):\n    \"\"\"Convert anymultirange to a jsonb object.\"\"\"\n    text = r'''\n        SELECT\n            CASE\n            WHEN val IS NULL THEN\n                NULL\n            WHEN isempty(val) THEN\n                jsonb_build_array()\n            ELSE\n                (\n                    SELECT\n                        jsonb_agg(edgedb_VER.range_to_jsonb(m.el))\n                    FROM\n                        (SELECT\n                            unnest(val) AS el\n                        ) AS m\n                )\n            END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'multirange_to_jsonb'),\n            args=[\n                ('val', ('anymultirange',)),\n            ],\n            returns=('jsonb',),\n            volatility='immutable',\n            language='sql',\n            text=self.text,\n        )\n\n\nclass RangeValidateFunction(trampoline.VersionedFunction):\n    \"\"\"Range constructor validation function.\"\"\"\n    text = r'''\n        SELECT\n            CASE\n            WHEN\n                empty\n                AND (lower IS DISTINCT FROM upper\n                     OR lower IS NOT NULL AND inc_upper AND inc_lower)\n            THEN\n                edgedb_VER.raise(\n                    NULL::bool,\n                    'invalid_parameter_value',\n                    msg => 'conflicting arguments in range constructor:'\n                           || ' \"empty\" is `true` while the specified'\n                           || ' bounds suggest otherwise'\n                )\n            ELSE\n                empty\n            END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'range_validate'),\n            args=[\n                ('lower', ('anyelement',)),\n                ('upper', ('anyelement',)),\n                ('inc_lower', ('bool',)),\n                ('inc_upper', ('bool',)),\n                ('empty', ('bool',)),\n            ],\n            returns=('bool',),\n            volatility='immutable',\n            language='sql',\n            text=self.text,\n        )\n\n\nclass RangeUnpackLowerValidateFunction(trampoline.VersionedFunction):\n    \"\"\"Range unpack validation function.\"\"\"\n    text = r'''\n        SELECT\n            CASE WHEN\n                NOT isempty(range)\n            THEN\n                edgedb_VER.raise_on_null(\n                    lower(range),\n                    'invalid_parameter_value',\n                    msg => 'cannot unpack an unbounded range'\n                )\n            ELSE\n                lower(range)\n            END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'range_lower_validate'),\n            args=[\n                ('range', ('anyrange',)),\n            ],\n            returns=('anyelement',),\n            volatility='immutable',\n            language='sql',\n            text=self.text,\n        )\n\n\nclass RangeUnpackUpperValidateFunction(trampoline.VersionedFunction):\n    \"\"\"Range unpack validation function.\"\"\"\n    text = r'''\n        SELECT\n            CASE WHEN\n                NOT isempty(range)\n            THEN\n                edgedb_VER.raise_on_null(\n                    upper(range),\n                    'invalid_parameter_value',\n                    msg => 'cannot unpack an unbounded range'\n                )\n            ELSE\n                upper(range)\n            END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'range_upper_validate'),\n            args=[\n                ('range', ('anyrange',)),\n            ],\n            returns=('anyelement',),\n            volatility='immutable',\n            language='sql',\n            text=self.text,\n        )\n\n\nclass StrToConfigMemoryFunction(trampoline.VersionedFunction):\n    \"\"\"An implementation of std::str to cfg::memory cast.\"\"\"\n    text = r'''\n        SELECT\n            (CASE\n                WHEN m.v[1] IS NOT NULL AND m.v[2] IS NOT NULL\n                THEN (\n                    CASE\n                        WHEN m.v[2] = 'B'\n                        THEN m.v[1]::int8\n\n                        WHEN m.v[2] = 'KiB'\n                        THEN m.v[1]::int8 * 1024\n\n                        WHEN m.v[2] = 'MiB'\n                        THEN m.v[1]::int8 * 1024 * 1024\n\n                        WHEN m.v[2] = 'GiB'\n                        THEN m.v[1]::int8 * 1024 * 1024 * 1024\n\n                        WHEN m.v[2] = 'TiB'\n                        THEN m.v[1]::int8 * 1024 * 1024 * 1024 * 1024\n\n                        WHEN m.v[2] = 'PiB'\n                        THEN m.v[1]::int8 * 1024 * 1024 * 1024 * 1024 * 1024\n\n                        ELSE\n                            -- Won't happen but we still have a guard for\n                            -- completeness.\n                            edgedb_VER.raise(\n                                NULL::int8,\n                                'invalid_parameter_value',\n                                msg => (\n                                    'unsupported memory size unit \"' ||\n                                    m.v[2] || '\"'\n                                )\n                            )\n                    END\n                )\n                ELSE\n                    CASE\n                        WHEN \"val\" = '0'\n                        THEN 0::int8\n                        ELSE\n                            edgedb_VER.raise(\n                                NULL::int8,\n                                'invalid_parameter_value',\n                                msg => (\n                                    'unable to parse memory size \"' ||\n                                    \"val\" || '\"'\n                                )\n                            )\n                    END\n            END)::edgedbt.memory_t\n        FROM LATERAL (\n            SELECT regexp_match(\n                \"val\", '^(\\d+)([[:alpha:]]+)$') AS v\n        ) AS m\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'str_to_cfg_memory'),\n            args=[\n                ('val', ('text',)),\n            ],\n            returns=('edgedbt', 'memory_t'),\n            strict=True,\n            volatility='immutable',\n            language='sql',\n            text=self.text,\n        )\n\n\nclass ConfigMemoryToStrFunction(trampoline.VersionedFunction):\n    \"\"\"An implementation of cfg::memory to std::str cast.\"\"\"\n    text = r'''\n        SELECT\n            CASE\n                WHEN\n                    \"val\" >= (1024::int8 * 1024 * 1024 * 1024 * 1024) AND\n                    \"val\" % (1024::int8 * 1024 * 1024 * 1024 * 1024) = 0\n                THEN\n                    (\n                        \"val\" / (1024::int8 * 1024 * 1024 * 1024 * 1024)\n                    )::text || 'PiB'\n\n                WHEN\n                    \"val\" >= (1024::int8 * 1024 * 1024 * 1024) AND\n                    \"val\" % (1024::int8 * 1024 * 1024 * 1024) = 0\n                THEN\n                    (\n                        \"val\" / (1024::int8 * 1024 * 1024 * 1024)\n                    )::text || 'TiB'\n\n                WHEN\n                    \"val\" >= (1024::int8 * 1024 * 1024) AND\n                    \"val\" % (1024::int8 * 1024 * 1024) = 0\n                THEN (\"val\" / (1024::int8 * 1024 * 1024))::text || 'GiB'\n\n                WHEN \"val\" >= 1024::int8 * 1024 AND\n                     \"val\" % (1024::int8 * 1024) = 0\n                THEN (\"val\" / (1024::int8 * 1024))::text || 'MiB'\n\n                WHEN \"val\" >= 1024 AND \"val\" % 1024 = 0\n                THEN (\"val\" / 1024::int8)::text || 'KiB'\n\n                ELSE \"val\"::text || 'B'\n            END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'cfg_memory_to_str'),\n            args=[\n                ('val', ('edgedbt', 'memory_t')),\n            ],\n            returns=('text',),\n            volatility='immutable',\n            language='sql',\n            text=self.text,\n        )\n\n\nclass AlterCurrentDatabaseSetString(trampoline.VersionedFunction):\n    \"\"\"Alter a PostgreSQL configuration parameter of the current database.\"\"\"\n    text = '''\n    BEGIN\n        EXECUTE 'ALTER DATABASE ' || quote_ident(current_database())\n        || ' SET ' || quote_ident(parameter) || ' = '\n        || coalesce(quote_literal(value), 'DEFAULT');\n        RETURN value;\n    END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_alter_current_database_set'),\n            args=[('parameter', ('text',)), ('value', ('text',))],\n            returns=('text',),\n            volatility='volatile',\n            language='plpgsql',\n            text=self.text,\n        )\n\n\nclass AlterCurrentDatabaseSetStringArray(trampoline.VersionedFunction):\n    \"\"\"Alter a PostgreSQL configuration parameter of the current database.\"\"\"\n    text = '''\n    BEGIN\n        EXECUTE 'ALTER DATABASE ' || quote_ident(current_database())\n        || ' SET ' || quote_ident(parameter) || ' = '\n        || coalesce(\n            (SELECT\n                array_to_string(array_agg(quote_literal(q.v)), ',')\n             FROM\n                unnest(value) AS q(v)\n            ),\n            'DEFAULT'\n        );\n        RETURN value;\n    END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_alter_current_database_set'),\n            args=[\n                ('parameter', ('text',)),\n                ('value', ('text[]',)),\n            ],\n            returns=('text[]',),\n            volatility='volatile',\n            language='plpgsql',\n            text=self.text,\n        )\n\n\nclass AlterCurrentDatabaseSetNonArray(trampoline.VersionedFunction):\n    \"\"\"Alter a PostgreSQL configuration parameter of the current database.\"\"\"\n    text = '''\n    BEGIN\n        EXECUTE 'ALTER DATABASE ' || quote_ident(current_database())\n        || ' SET ' || quote_ident(parameter) || ' = '\n        || coalesce(value::text, 'DEFAULT');\n        RETURN value;\n    END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_alter_current_database_set'),\n            args=[\n                ('parameter', ('text',)),\n                ('value', ('anynonarray',)),\n            ],\n            returns=('anynonarray',),\n            volatility='volatile',\n            language='plpgsql',\n            text=self.text,\n        )\n\n\nclass AlterCurrentDatabaseSetArray(trampoline.VersionedFunction):\n    \"\"\"Alter a PostgreSQL configuration parameter of the current database.\"\"\"\n    text = '''\n    BEGIN\n        EXECUTE 'ALTER DATABASE ' || quote_ident(current_database())\n        || ' SET ' || quote_ident(parameter) || ' = '\n        || coalesce(\n            (SELECT\n                array_to_string(array_agg(q.v::text), ',')\n             FROM\n                unnest(value) AS q(v)\n            ),\n            'DEFAULT'\n        );\n        RETURN value;\n    END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_alter_current_database_set'),\n            args=[\n                ('parameter', ('text',)),\n                ('value', ('anyarray',)),\n            ],\n            returns=('anyarray',),\n            volatility='volatile',\n            language='plpgsql',\n            text=self.text,\n        )\n\n\nclass CopyDatabaseConfigs(trampoline.VersionedFunction):\n    \"\"\"Copy database configs from one database to the current one\"\"\"\n    text = '''\n        SELECT edgedb_VER._alter_current_database_set(\n            nameval.name, nameval.value)\n        FROM\n            pg_db_role_setting AS cfg,\n            LATERAL unnest(cfg.setconfig) as cfg_set(s),\n            LATERAL (\n                SELECT\n                    split_part(cfg_set.s, '=', 1) AS name,\n                    split_part(cfg_set.s, '=', 2) AS value\n            ) AS nameval\n        WHERE\n            setdatabase = (\n                SELECT oid\n                FROM pg_database\n                WHERE datname = source_db\n            )\n            AND setrole = 0;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_copy_database_configs'),\n            args=[('source_db', ('text',))],\n            returns=('text',),\n            volatility='volatile',\n            text=self.text,\n        )\n\n\nclass StrToBigint(trampoline.VersionedFunction):\n    \"\"\"Parse bigint from text.\"\"\"\n\n    # The plpgsql execption handling nonsense is actually just so that\n    # we can produce an exception that mentions edgedbt.bigint_t\n    # instead of numeric, and thus produce the right user-facing\n    # exception. As a nice side effect it is like twice as fast\n    # as the previous code too.\n    text = r'''\n        DECLARE\n            v numeric;\n        BEGIN\n            BEGIN\n              v := val::numeric;\n            EXCEPTION\n              WHEN OTHERS THEN\n                 v := NULL;\n            END;\n\n            IF scale(v) = 0 THEN\n                RETURN v::edgedbt.bigint_t;\n            ELSE\n                EXECUTE edgedb_VER.raise(\n                    NULL::numeric,\n                    'invalid_text_representation',\n                    msg => (\n                        'invalid input syntax for type edgedbt.bigint_t: '\n                        || quote_literal(val)\n                    )\n                );\n            END IF;\n        END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'str_to_bigint'),\n            args=[('val', ('text',))],\n            returns=('edgedbt', 'bigint_t'),\n            language='plpgsql',\n            volatility='immutable',\n            strict=True,\n            text=self.text)\n\n\nclass StrToDecimal(trampoline.VersionedFunction):\n    \"\"\"Parse decimal from text.\"\"\"\n    text = r'''\n        SELECT\n            (CASE WHEN v.column1 != 'NaN' THEN\n                v.column1\n            ELSE\n                edgedb_VER.raise(\n                    NULL::numeric,\n                    'invalid_text_representation',\n                    msg => (\n                        'invalid input syntax for type numeric: '\n                        || quote_literal(val)\n                    )\n                )\n            END)\n        FROM\n            (VALUES (\n                val::numeric\n            )) AS v\n        ;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'str_to_decimal'),\n            args=[('val', ('text',))],\n            returns=('numeric',),\n            volatility='immutable',\n            strict=True,\n            text=self.text,\n        )\n\n\nclass StrToInt64NoInline(trampoline.VersionedFunction):\n    \"\"\"String-to-int64 cast with noinline guard.\n\n    Adding a LIMIT clause to the function statement makes it\n    uninlinable due to the Postgres inlining heuristic looking\n    for simple SELECT expressions only (i.e. no clauses.)\n\n    This might need to change in the future if the heuristic\n    changes.\n    \"\"\"\n    text = r'''\n        SELECT\n            \"val\"::bigint\n        LIMIT\n            1\n        ;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'str_to_int64_noinline'),\n            args=[('val', ('text',))],\n            returns=('bigint',),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass StrToInt32NoInline(trampoline.VersionedFunction):\n    \"\"\"String-to-int32 cast with noinline guard.\"\"\"\n    text = r'''\n        SELECT\n            \"val\"::int\n        LIMIT\n            1\n        ;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'str_to_int32_noinline'),\n            args=[('val', ('text',))],\n            returns=('int',),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass StrToInt16NoInline(trampoline.VersionedFunction):\n    \"\"\"String-to-int16 cast with noinline guard.\"\"\"\n    text = r'''\n        SELECT\n            \"val\"::smallint\n        LIMIT\n            1\n        ;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'str_to_int16_noinline'),\n            args=[('val', ('text',))],\n            returns=('smallint',),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass StrToFloat64NoInline(trampoline.VersionedFunction):\n    \"\"\"String-to-float64 cast with noinline guard.\"\"\"\n    text = r'''\n        SELECT\n            \"val\"::float8\n        LIMIT\n            1\n        ;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'str_to_float64_noinline'),\n            args=[('val', ('text',))],\n            returns=('float8',),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass StrToFloat32NoInline(trampoline.VersionedFunction):\n    \"\"\"String-to-float32 cast with noinline guard.\"\"\"\n    text = r'''\n        SELECT\n            \"val\"::float4\n        LIMIT\n            1\n        ;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'str_to_float32_noinline'),\n            args=[('val', ('text',))],\n            returns=('float4',),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass GetBackendCapabilitiesFunction(trampoline.VersionedFunction):\n\n    text = f'''\n        SELECT\n            (json ->> 'capabilities')::bigint\n        FROM\n            edgedbinstdata_VER.instdata\n        WHERE\n            key = 'backend_instance_params'\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'get_backend_capabilities'),\n            args=[],\n            returns=('bigint',),\n            language='sql',\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass GetBackendTenantIDFunction(trampoline.VersionedFunction):\n\n    text = f'''\n        SELECT\n            (json ->> 'tenant_id')::text\n        FROM\n            edgedbinstdata_VER.instdata\n        WHERE\n            key = 'backend_instance_params'\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'get_backend_tenant_id'),\n            args=[],\n            returns=('text',),\n            language='sql',\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass GetDatabaseBackendNameFunction(trampoline.VersionedFunction):\n\n    text = f'''\n    SELECT\n        CASE\n        WHEN\n            (edgedb_VER.get_backend_capabilities()\n             & {int(params.BackendCapabilities.CREATE_DATABASE)}) != 0\n        THEN\n            edgedb_VER.get_backend_tenant_id() || '_' || \"db_name\"\n        ELSE\n            current_database()::text\n        END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'get_database_backend_name'),\n            args=[('db_name', ('text',))],\n            returns=('text',),\n            language='sql',\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass GetDatabaseFrontendNameFunction(trampoline.VersionedFunction):\n\n    text = f'''\n    SELECT\n        CASE\n        WHEN\n            (edgedb_VER.get_backend_capabilities()\n             & {int(params.BackendCapabilities.CREATE_DATABASE)}) != 0\n        THEN\n            substring(db_name, position('_' in db_name) + 1)\n        ELSE\n            'main'\n        END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'get_database_frontend_name'),\n            args=[('db_name', ('text',))],\n            returns=('text',),\n            language='sql',\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass GetRoleBackendNameFunction(trampoline.VersionedFunction):\n\n    text = f'''\n    SELECT\n        CASE\n        WHEN\n            (edgedb_VER.get_backend_capabilities()\n             & {int(params.BackendCapabilities.CREATE_ROLE)}) != 0\n        THEN\n            edgedb_VER.get_backend_tenant_id() || '_' || \"role_name\"\n        ELSE\n            current_user::text\n        END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'get_role_backend_name'),\n            args=[('role_name', ('text',))],\n            returns=('text',),\n            language='sql',\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass GetUserSequenceBackendNameFunction(trampoline.VersionedFunction):\n\n    text = f\"\"\"\n        SELECT\n            'edgedbpub',\n            \"sequence_type_id\"::text || '_sequence'\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'get_user_sequence_backend_name'),\n            args=[('sequence_type_id', ('uuid',))],\n            returns=('record',),\n            language='sql',\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass GetSequenceBackendNameFunction(trampoline.VersionedFunction):\n\n    text = f'''\n        SELECT\n            (CASE\n                WHEN edgedb_VER.get_name_module(st.name)\n                     = any(edgedb_VER.get_std_modules())\n                THEN 'edgedbstd'\n                ELSE 'edgedbpub'\n             END),\n            \"sequence_type_id\"::text || '_sequence'\n        FROM\n            edgedb_VER.\"_SchemaScalarType\" AS st\n        WHERE\n            st.id = \"sequence_type_id\"\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'get_sequence_backend_name'),\n            args=[('sequence_type_id', ('uuid',))],\n            returns=('record',),\n            language='sql',\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass GetStdModulesFunction(trampoline.VersionedFunction):\n\n    text = f'''\n        SELECT ARRAY[{\",\".join(ql(str(m)) for m in s_schema.STD_MODULES)}]\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'get_std_modules'),\n            args=[],\n            returns=('text[]',),\n            language='sql',\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass GetObjectMetadata(trampoline.VersionedFunction):\n    \"\"\"Return Gel metadata associated with a backend object.\"\"\"\n    text = '''\n        SELECT\n            CASE WHEN substr(d, 1, char_length({prefix})) = {prefix}\n            THEN substr(d, char_length({prefix}) + 1)::jsonb\n            ELSE '{{}}'::jsonb\n            END\n        FROM\n            obj_description(\"objoid\", \"objclass\") AS d\n    '''.format(\n        prefix=f'E{ql(defines.EDGEDB_VISIBLE_METADATA_PREFIX)}',\n    )\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'obj_metadata'),\n            args=[('objoid', ('oid',)), ('objclass', ('text',))],\n            returns=('jsonb',),\n            volatility='stable',\n            text=self.text)\n\n\nclass GetColumnMetadata(trampoline.VersionedFunction):\n    \"\"\"Return Gel metadata associated with a backend object.\"\"\"\n    text = '''\n        SELECT\n            CASE WHEN substr(d, 1, char_length({prefix})) = {prefix}\n            THEN substr(d, char_length({prefix}) + 1)::jsonb\n            ELSE '{{}}'::jsonb\n            END\n        FROM\n            col_description(\"tableoid\", \"column\") AS d\n    '''.format(\n        prefix=f'E{ql(defines.EDGEDB_VISIBLE_METADATA_PREFIX)}',\n    )\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'col_metadata'),\n            args=[('tableoid', ('oid',)), ('column', ('integer',))],\n            returns=('jsonb',),\n            volatility='stable',\n            text=self.text)\n\n\nclass GetSharedObjectMetadata(trampoline.VersionedFunction):\n    \"\"\"Return Gel metadata associated with a backend object.\"\"\"\n    text = '''\n        SELECT\n            CASE WHEN substr(d, 1, char_length({prefix})) = {prefix}\n            THEN substr(d, char_length({prefix}) + 1)::jsonb\n            ELSE '{{}}'::jsonb\n            END\n        FROM\n            shobj_description(\"objoid\", \"objclass\") AS d\n    '''.format(\n        prefix=f'E{ql(defines.EDGEDB_VISIBLE_METADATA_PREFIX)}',\n    )\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'shobj_metadata'),\n            args=[('objoid', ('oid',)), ('objclass', ('text',))],\n            returns=('jsonb',),\n            volatility='stable',\n            text=self.text)\n\n\nclass GetDatabaseMetadataFunction(trampoline.VersionedFunction):\n    \"\"\"Return Gel metadata associated with a given database.\"\"\"\n    text = f'''\n        SELECT\n            CASE\n            WHEN\n                \"dbname\" = {ql(defines.EDGEDB_SUPERUSER_DB)}\n                OR (edgedb_VER.get_backend_capabilities()\n                    & {int(params.BackendCapabilities.CREATE_DATABASE)}) != 0\n            THEN\n                edgedb_VER.shobj_metadata(\n                    (SELECT\n                        oid\n                     FROM\n                        pg_database\n                     WHERE\n                        datname = edgedb_VER.get_database_backend_name(\"dbname\")\n                    ),\n                    'pg_database'\n                )\n            ELSE\n                COALESCE(\n                    (SELECT\n                        json\n                     FROM\n                        edgedbinstdata_VER.instdata\n                     WHERE\n                        key = \"dbname\" || 'metadata'\n                    ),\n                    '{{}}'::jsonb\n                )\n            END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'get_database_metadata'),\n            args=[('dbname', ('text',))],\n            returns=('jsonb',),\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass GetCurrentDatabaseFunction(trampoline.VersionedFunction):\n\n    text = f'''\n        SELECT\n            CASE\n            WHEN\n                (edgedb_VER.get_backend_capabilities()\n                 & {int(params.BackendCapabilities.CREATE_DATABASE)}) != 0\n            THEN\n                substr(\n                    current_database(),\n                    char_length(edgedb_VER.get_backend_tenant_id()) + 2\n                )\n            ELSE\n                {ql(defines.EDGEDB_SUPERUSER_DB)}\n            END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'get_current_database'),\n            args=[],\n            returns=('text',),\n            language='sql',\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass RaiseNoticeFunction(trampoline.VersionedFunction):\n    text = '''\n    BEGIN\n        RAISE NOTICE USING\n            MESSAGE = \"msg\",\n            DETAIL = COALESCE(\"detail\", ''),\n            HINT = COALESCE(\"hint\", ''),\n            COLUMN = COALESCE(\"column\", ''),\n            CONSTRAINT = COALESCE(\"constraint\", ''),\n            DATATYPE = COALESCE(\"datatype\", ''),\n            TABLE = COALESCE(\"table\", ''),\n            SCHEMA = COALESCE(\"schema\", '');\n        RETURN \"rtype\";\n    END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'notice'),\n            args=[\n                ('rtype', ('anyelement',)),\n                ('msg', ('text',), \"''\"),\n                ('detail', ('text',), \"''\"),\n                ('hint', ('text',), \"''\"),\n                ('column', ('text',), \"''\"),\n                ('constraint', ('text',), \"''\"),\n                ('datatype', ('text',), \"''\"),\n                ('table', ('text',), \"''\"),\n                ('schema', ('text',), \"''\"),\n            ],\n            returns=('anyelement',),\n            # NOTE: The main reason why we don't want this function to be\n            # immutable is that immutable functions can be\n            # pre-evaluated by the query planner once if they have\n            # constant arguments. This means that using this function\n            # as the second argument in a COALESCE will raise a\n            # notice regardless of whether the first argument is\n            # NULL or not.\n            volatility='stable',\n            language='plpgsql',\n            text=self.text,\n        )\n\n\n# edgedb.indirect_return() to be used to return values from\n# anonymous code blocks or other contexts that have no return\n# data channel.\nclass IndirectReturnFunction(trampoline.VersionedFunction):\n    text = \"\"\"\n    SELECT\n        edgedb_VER.notice(\n            NULL::text,\n            msg => 'edb:notice:indirect_return',\n            detail => \"value\"\n        )\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'indirect_return'),\n            args=[\n                ('value', ('text',)),\n            ],\n            returns=('text',),\n            # NOTE: The main reason why we don't want this function to be\n            # immutable is that immutable functions can be\n            # pre-evaluated by the query planner once if they have\n            # constant arguments. This means that using this function\n            # as the second argument in a COALESCE will raise a\n            # notice regardless of whether the first argument is\n            # NULL or not.\n            volatility='stable',\n            language='sql',\n            text=self.text,\n        )\n\n\nclass RaiseExceptionFunction(trampoline.VersionedFunction):\n    text = '''\n    BEGIN\n        RAISE EXCEPTION USING\n            ERRCODE = \"exc\",\n            MESSAGE = \"msg\",\n            DETAIL = COALESCE(\"detail\", ''),\n            HINT = COALESCE(\"hint\", ''),\n            COLUMN = COALESCE(\"column\", ''),\n            CONSTRAINT = COALESCE(\"constraint\", ''),\n            DATATYPE = COALESCE(\"datatype\", ''),\n            TABLE = COALESCE(\"table\", ''),\n            SCHEMA = COALESCE(\"schema\", '');\n        RETURN \"rtype\";\n    END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'raise'),\n            args=[\n                ('rtype', ('anyelement',)),\n                ('exc', ('text',), \"'raise_exception'\"),\n                ('msg', ('text',), \"''\"),\n                ('detail', ('text',), \"''\"),\n                ('hint', ('text',), \"''\"),\n                ('column', ('text',), \"''\"),\n                ('constraint', ('text',), \"''\"),\n                ('datatype', ('text',), \"''\"),\n                ('table', ('text',), \"''\"),\n                ('schema', ('text',), \"''\"),\n            ],\n            returns=('anyelement',),\n            # NOTE: The main reason why we don't want this function to be\n            # immutable is that immutable functions can be\n            # pre-evaluated by the query planner once if they have\n            # constant arguments. This means that using this function\n            # as the second argument in a COALESCE will raise an\n            # exception regardless of whether the first argument is\n            # NULL or not.\n            volatility='stable',\n            language='plpgsql',\n            text=self.text,\n        )\n\n\nclass RaiseExceptionOnNullFunction(trampoline.VersionedFunction):\n    \"\"\"Return the passed value or raise an exception if it's NULL.\"\"\"\n    text = '''\n        SELECT coalesce(\n            val,\n            edgedb_VER.raise(\n                val,\n                exc,\n                msg => msg,\n                detail => detail,\n                hint => hint,\n                \"column\" => \"column\",\n                \"constraint\" => \"constraint\",\n                \"datatype\" => \"datatype\",\n                \"table\" => \"table\",\n                \"schema\" => \"schema\"\n            )\n        )\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'raise_on_null'),\n            args=[\n                ('val', ('anyelement',)),\n                ('exc', ('text',)),\n                ('msg', ('text',)),\n                ('detail', ('text',), \"''\"),\n                ('hint', ('text',), \"''\"),\n                ('column', ('text',), \"''\"),\n                ('constraint', ('text',), \"''\"),\n                ('datatype', ('text',), \"''\"),\n                ('table', ('text',), \"''\"),\n                ('schema', ('text',), \"''\"),\n            ],\n            returns=('anyelement',),\n            # Same volatility as raise()\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass RaiseExceptionOnNotNullFunction(trampoline.VersionedFunction):\n    \"\"\"Return the passed value or raise an exception if it's NOT NULL.\"\"\"\n    text = '''\n        SELECT\n            CASE\n            WHEN val IS NULL THEN\n                val\n            ELSE\n                edgedb_VER.raise(\n                    val,\n                    exc,\n                    msg => msg,\n                    detail => detail,\n                    hint => hint,\n                    \"column\" => \"column\",\n                    \"constraint\" => \"constraint\",\n                    \"datatype\" => \"datatype\",\n                    \"table\" => \"table\",\n                    \"schema\" => \"schema\"\n                )\n            END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'raise_on_not_null'),\n            args=[\n                ('val', ('anyelement',)),\n                ('exc', ('text',)),\n                ('msg', ('text',)),\n                ('detail', ('text',), \"''\"),\n                ('hint', ('text',), \"''\"),\n                ('column', ('text',), \"''\"),\n                ('constraint', ('text',), \"''\"),\n                ('datatype', ('text',), \"''\"),\n                ('table', ('text',), \"''\"),\n                ('schema', ('text',), \"''\"),\n            ],\n            returns=('anyelement',),\n            # Same volatility as raise()\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass RaiseExceptionOnEmptyStringFunction(trampoline.VersionedFunction):\n    \"\"\"Return the passed string or raise an exception if it's empty.\"\"\"\n    text = '''\n        SELECT\n            CASE WHEN edgedb_VER._length(val) = 0 THEN\n                edgedb_VER.raise(val, exc, msg => msg, detail => detail)\n            ELSE\n                val\n            END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'raise_on_empty'),\n            args=[\n                ('val', ('anyelement',)),\n                ('exc', ('text',)),\n                ('msg', ('text',)),\n                ('detail', ('text',), \"''\"),\n            ],\n            returns=('anyelement',),\n            # Same volatility as raise()\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass AssertJSONTypeFunction(trampoline.VersionedFunction):\n    \"\"\"Assert that the JSON type matches what is expected.\"\"\"\n    text = '''\n        SELECT\n            CASE WHEN array_position(typenames, jsonb_typeof(val)) IS NULL THEN\n                edgedb_VER.raise(\n                    NULL::jsonb,\n                    'wrong_object_type',\n                    msg => coalesce(\n                        msg,\n                        format(\n                            'expected JSON %s; got JSON %s: %s',\n                            array_to_string(typenames, ' or '),\n                            coalesce(jsonb_typeof(val), 'UNKNOWN'),\n                            val::text\n                        )\n                    ),\n                    detail => detail\n                )\n            ELSE\n                val\n            END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'jsonb_assert_type'),\n            args=[\n                ('val', ('jsonb',)),\n                ('typenames', ('text[]',)),\n                ('msg', ('text',), 'NULL'),\n                ('detail', ('text',), \"''\"),\n            ],\n            returns=('jsonb',),\n            # Max volatility of raise() and array_to_string() (stable)\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass ExtractJSONScalarFunction(trampoline.VersionedFunction):\n    \"\"\"Convert a given JSON scalar value into a text value.\"\"\"\n    text = '''\n        SELECT\n            (to_jsonb(ARRAY[\n                edgedb_VER.jsonb_assert_type(\n                    coalesce(val, 'null'::jsonb),\n                    ARRAY[json_typename, 'null'],\n                    msg => msg,\n                    detail => detail\n                )\n            ])->>0)\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'jsonb_extract_scalar'),\n            args=[\n                ('val', ('jsonb',)),\n                ('json_typename', ('text',)),\n                ('msg', ('text',), 'NULL'),\n                ('detail', ('text',), \"''\"),\n            ],\n            returns=('text',),\n            volatility='stable',\n            wrapper_volatility='immutable',\n            text=self.text,\n        )\n\n\nclass GetSchemaObjectNameFunction(trampoline.VersionedFunction):\n    text = '''\n        SELECT coalesce(\n            (SELECT name FROM edgedb_VER.\"_SchemaObject\"\n             WHERE id = type::uuid),\n            edgedb_VER.raise(\n                NULL::text,\n                msg => 'resolve_type_name: unknown type: \"' || type || '\"'\n            )\n        )\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_get_schema_object_name'),\n            args=[('type', ('uuid',))],\n            returns=('text',),\n            # Max volatility of raise() and a SELECT from a\n            # table (stable).\n            volatility='stable',\n            text=self.text,\n            strict=True,\n        )\n\n\n# We create this version first (since it is used by the stdlib), and\n# then replace it with the real version later.\nclass ApproximateCountDummy(trampoline.VersionedFunction):\n    text = '''\n        SELECT 0\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'approximate_count'),\n            args=[\n                ('ignore_subtypes', ('bool',)),\n                ('type', ('uuid',)),\n                ('type_type', ('uuid',), \"NULL\"),\n            ],\n            returns=('bigint',),\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass ApproximateCount(trampoline.VersionedFunction):\n    text = '''\n        SELECT coalesce(sum(reltuples::bigint), 0) AS estimate\n        FROM pg_class pc\n        WHERE pc.relname IN (\n          SELECT oa.source::text\n          FROM edgedb_VER.\"_SchemaObjectType__ancestors\" oa\n          WHERE oa.target = type AND not ignore_subtypes\n          UNION\n          select type::text\n        ) AND pc.reltuples >= 0;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'approximate_count'),\n            args=[\n                ('ignore_subtypes', ('bool',)),\n                ('type', ('uuid',)),\n                ('type_type', ('uuid',), \"NULL\"),\n            ],\n            returns=('bigint',),\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass IssubclassFunction(trampoline.VersionedFunction):\n    text = '''\n        SELECT\n            clsid = any(classes) OR (\n                SELECT classes && q.ancestors\n                FROM\n                    (SELECT\n                        array_agg(o.target) AS ancestors\n                        FROM edgedb_VER.\"_SchemaInheritingObject__ancestors\" o\n                        WHERE o.source = clsid\n                    ) AS q\n            );\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'issubclass'),\n            args=[('clsid', 'uuid'), ('classes', 'uuid[]')],\n            returns='bool',\n            volatility='stable',\n            text=self.__class__.text)\n\n\nclass IssubclassFunction2(trampoline.VersionedFunction):\n    text = '''\n        SELECT\n            clsid = pclsid OR (\n                SELECT\n                    pclsid IN (\n                        SELECT\n                            o.target\n                        FROM edgedb_VER.\"_SchemaInheritingObject__ancestors\" o\n                            WHERE o.source = clsid\n                    )\n            );\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'issubclass'),\n            args=[('clsid', 'uuid'), ('pclsid', 'uuid')],\n            returns='bool',\n            volatility='stable',\n            text=self.__class__.text)\n\n\nclass NormalizeNameFunction(trampoline.VersionedFunction):\n    text = '''\n        SELECT\n            CASE WHEN strpos(name, '@') = 0 THEN\n                name\n            ELSE\n                CASE WHEN strpos(name, '::') = 0 THEN\n                    replace(split_part(name, '@', 1), '|', '::')\n                ELSE\n                    replace(\n                        split_part(\n                            -- \"reverse\" calls are to emulate \"rsplit\"\n                            reverse(split_part(reverse(name), '::', 1)),\n                            '@', 1),\n                        '|', '::')\n                END\n            END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'shortname_from_fullname'),\n            args=[('name', 'text')],\n            returns='text',\n            volatility='immutable',\n            language='sql',\n            text=self.__class__.text)\n\n\nclass GetNameModuleFunction(trampoline.VersionedFunction):\n    text = '''\n        SELECT reverse(split_part(reverse(\"name\"), '::', 1))\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'get_name_module'),\n            args=[('name', 'text')],\n            returns='text',\n            volatility='immutable',\n            language='sql',\n            text=self.__class__.text)\n\n\nclass NullIfArrayNullsFunction(trampoline.VersionedFunction):\n    \"\"\"Check if array contains NULLs and if so, return NULL.\"\"\"\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_nullif_array_nulls'),\n            args=[('a', 'anyarray')],\n            returns='anyarray',\n            volatility='stable',\n            language='sql',\n            text='''\n                SELECT CASE WHEN array_position(a, NULL) IS NULL\n                THEN a ELSE NULL END\n            ''')\n\n\nclass NormalizeArrayIndexFunction(trampoline.VersionedFunction):\n    \"\"\"Convert an EdgeQL index to SQL index.\"\"\"\n\n    text = '''\n        SELECT\n            CASE WHEN index > (2147483647-1) OR index < -2147483648 THEN\n                NULL\n            WHEN index < 0 THEN\n                length + index::int + 1\n            ELSE\n                index::int + 1\n            END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_normalize_array_index'),\n            args=[('index', ('bigint',)), ('length', ('int',))],\n            returns=('int',),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass NormalizeArraySliceIndexFunction(trampoline.VersionedFunction):\n    \"\"\"Convert an EdgeQL index to SQL index (for slices)\"\"\"\n\n    text = '''\n        SELECT\n            GREATEST(0, LEAST(2147483647,\n                CASE WHEN index < 0 THEN\n                    length::bigint + index + 1\n                ELSE\n                    index + 1\n                END\n            ))\n        WHERE index IS NOT NULL\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_normalize_array_slice_index'),\n            args=[('index', ('bigint',)), ('length', ('int',))],\n            returns=('int',),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass IntOrNullFunction(trampoline.VersionedFunction):\n    \"\"\"\n    Convert bigint to int. If it does not fit, return NULL.\n    \"\"\"\n\n    text = \"\"\"\n        SELECT\n            CASE WHEN val <= 2147483647 AND val >= -2147483648 THEN\n                val\n            ELSE\n                NULL\n            END\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=(\"edgedb\", \"_int_or_null\"),\n            args=[(\"val\", (\"bigint\",))],\n            returns=(\"int\",),\n            volatility=\"immutable\",\n            strict=True,\n            text=self.text,\n        )\n\n\nclass ArrayIndexWithBoundsFunction(trampoline.VersionedFunction):\n    \"\"\"Get an array element or raise an out-of-bounds exception.\"\"\"\n\n    text = '''\n        SELECT CASE WHEN val IS NULL THEN\n            NULL\n        ELSE\n            edgedb_VER.raise_on_null(\n                val[edgedb_VER._normalize_array_index(\n                    index, array_upper(val, 1))],\n                'array_subscript_error',\n                msg => 'array index ' || index::text || ' is out of bounds',\n                detail => detail\n            )\n        END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_index'),\n            args=[('val', ('anyarray',)), ('index', ('bigint',)),\n                  ('detail', ('text',))],\n            returns=('anyelement',),\n            # Min volatility of exception helpers and pg_typeof is 'stable',\n            # but for all practical purposes, we can assume 'immutable'\n            volatility='stable',\n            wrapper_volatility='immutable',\n            text=self.text,\n        )\n\n\nclass ArraySliceFunction(trampoline.VersionedFunction):\n    \"\"\"Get an array slice.\"\"\"\n\n    # This function is also inlined in expr.py#_inline_array_slicing.\n\n    # Known bug: if array has 2G elements and both bounds are overflowing,\n    # this will return last element instead of an empty array.\n    text = \"\"\"\n        SELECT val[\n            edgedb_VER._normalize_array_slice_index(start, cardinality(val))\n            :\n            edgedb_VER._normalize_array_slice_index(stop, cardinality(val)) - 1\n        ]\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=(\"edgedb\", \"_slice\"),\n            args=[\n                (\"val\", (\"anyarray\",)),\n                (\"start\", (\"bigint\",)),\n                (\"stop\", (\"bigint\",)),\n            ],\n            returns=(\"anyarray\",),\n            volatility=\"stable\",\n            wrapper_volatility='immutable',\n            text=self.text,\n        )\n\n\nclass StringIndexWithBoundsFunction(trampoline.VersionedFunction):\n    \"\"\"Get a string character or raise an out-of-bounds exception.\"\"\"\n\n    text = '''\n        SELECT edgedb_VER.raise_on_empty(\n            CASE WHEN pg_index IS NULL THEN\n                ''\n            ELSE\n                substr(\"val\", pg_index, 1)\n            END,\n            'invalid_parameter_value',\n            \"typename\" || ' index ' || \"index\"::text || ' is out of bounds',\n            \"detail\"\n        )\n        FROM (\n            SELECT (\n                edgedb_VER._normalize_array_index(\"index\", char_length(\"val\"))\n            ) as pg_index\n        ) t\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_index'),\n            args=[\n                ('val', ('text',)),\n                ('index', ('bigint',)),\n                ('detail', ('text',)),\n                ('typename', ('text',), \"'string'\"),\n            ],\n            returns=('text',),\n            # Min volatility of exception helpers and pg_typeof is 'stable',\n            # but for all practical purposes, we can assume 'immutable'\n            volatility='stable',\n            wrapper_volatility='immutable',\n            text=self.text,\n        )\n\n\nclass BytesIndexWithBoundsFunction(trampoline.VersionedFunction):\n    \"\"\"Get a bytes character or raise an out-of-bounds exception.\"\"\"\n\n    text = '''\n        SELECT edgedb_VER.raise_on_empty(\n            CASE WHEN pg_index IS NULL THEN\n                ''::bytea\n            ELSE\n                substr(\"val\", pg_index, 1)\n            END,\n            'invalid_parameter_value',\n            'byte string index ' || \"index\"::text || ' is out of bounds',\n            \"detail\"\n        )\n        FROM (\n            SELECT (\n                edgedb_VER._normalize_array_index(\"index\", length(\"val\"))\n            ) as pg_index\n        ) t\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_index'),\n            args=[\n                ('val', ('bytea',)),\n                ('index', ('bigint',)),\n                ('detail', ('text',)),\n            ],\n            returns=('bytea',),\n            # Min volatility of exception helpers and pg_typeof is 'stable',\n            # but for all practical purposes, we can assume 'immutable'\n            volatility='stable',\n            wrapper_volatility='immutable',\n            text=self.text,\n        )\n\n\nclass SubstrProxyFunction(trampoline.VersionedFunction):\n    \"\"\"Same as substr, but interpret negative length as 0 instead.\"\"\"\n\n    text = r\"\"\"\n        SELECT\n            CASE\n                WHEN length < 0 THEN ''\n                ELSE substr(val, start::int, length)\n            END\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=(\"edgedb\", \"_substr\"),\n            args=[\n                (\"val\", (\"anyelement\",)),\n                (\"start\", (\"int\",)),\n                (\"length\", (\"int\",)),\n            ],\n            returns=(\"anyelement\",),\n            volatility=\"immutable\",\n            strict=True,\n            text=self.text,\n        )\n\n\nclass LengthStringProxyFunction(trampoline.VersionedFunction):\n    \"\"\"Same as substr, but interpret negative length as 0 instead.\"\"\"\n    text = r'''\n        SELECT char_length(val)\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_length'),\n            args=[('val', ('text',))],\n            returns=('int',),\n            volatility='immutable',\n            strict=True,\n            text=self.text)\n\n\nclass LengthBytesProxyFunction(trampoline.VersionedFunction):\n    \"\"\"Same as substr, but interpret negative length as 0 instead.\"\"\"\n    text = r'''\n        SELECT length(val)\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_length'),\n            args=[('val', ('bytea',))],\n            returns=('int',),\n            volatility='immutable',\n            strict=True,\n            text=self.text)\n\n\nclass StringSliceImplFunction(trampoline.VersionedFunction):\n    \"\"\"Get a string slice.\"\"\"\n\n    text = r\"\"\"\n        SELECT\n            edgedb_VER._substr(\n                val,\n                pg_start,\n                pg_end - pg_start\n            )\n        FROM (SELECT\n            edgedb_VER._normalize_array_slice_index(\n                start, edgedb_VER._length(val)\n            ) as pg_start,\n            edgedb_VER._normalize_array_slice_index(\n                stop, edgedb_VER._length(val)\n            ) as pg_end\n        ) t\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=(\"edgedb\", \"_str_slice\"),\n            args=[\n                (\"val\", (\"anyelement\",)),\n                (\"start\", (\"bigint\",)),\n                (\"stop\", (\"bigint\",)),\n            ],\n            returns=(\"anyelement\",),\n            volatility=\"immutable\",\n            text=self.text,\n        )\n\n\nclass StringSliceFunction(trampoline.VersionedFunction):\n    \"\"\"Get a string slice.\"\"\"\n    text = r'''\n        SELECT edgedb_VER._str_slice(val, start, stop)\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_slice'),\n            args=[\n                ('val', ('text',)),\n                ('start', ('bigint',)),\n                ('stop', ('bigint',)),\n            ],\n            returns=('text',),\n            volatility='stable',\n            text=self.text)\n\n\nclass BytesSliceFunction(trampoline.VersionedFunction):\n    \"\"\"Get a string slice.\"\"\"\n    text = r'''\n        SELECT edgedb_VER._str_slice(val, start, stop)\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_slice'),\n            args=[\n                ('val', ('bytea',)),\n                ('start', ('bigint',)),\n                ('stop', ('bigint',)),\n            ],\n            returns=('bytea',),\n            volatility='stable',\n            text=self.text)\n\n\nclass JSONIndexByTextFunction(trampoline.VersionedFunction):\n    \"\"\"Get a JSON element by text index or raise an exception.\"\"\"\n    text = r'''\n        SELECT\n            CASE jsonb_typeof(val)\n            WHEN 'object' THEN (\n                edgedb_VER.raise_on_null(\n                    val -> index,\n                    'invalid_parameter_value',\n                    msg => (\n                        'JSON index ' || quote_literal(index)\n                        || ' is out of bounds'\n                    ),\n                    detail => detail\n                )\n            )\n            WHEN 'array' THEN (\n                edgedb_VER.raise(\n                    NULL::jsonb,\n                    'wrong_object_type',\n                    msg => (\n                        'cannot index JSON ' || jsonb_typeof(val)\n                        || ' by ' || pg_typeof(index)::text\n                    ),\n                    detail => detail\n                )\n            )\n            ELSE\n                edgedb_VER.raise(\n                    NULL::jsonb,\n                    'wrong_object_type',\n                    msg => (\n                        'cannot index JSON '\n                        || coalesce(jsonb_typeof(val), 'UNKNOWN')\n                    ),\n                    detail => (\n                        '{\"hint\":\"Retrieving an element by a string index '\n                        || 'is only available for JSON objects.\"}'\n                    )\n                )\n            END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_index'),\n            args=[\n                ('val', ('jsonb',)),\n                ('index', ('text',)),\n                ('detail', ('text',), \"''\"),\n            ],\n            returns=('jsonb',),\n            # Min volatility of exception helpers 'stable',\n            # but for all practical purposes, we can assume 'immutable'\n            volatility='stable',\n            wrapper_volatility='immutable',\n            strict=True,\n            text=self.text,\n        )\n\n\nclass JSONIndexByIntFunction(trampoline.VersionedFunction):\n    \"\"\"Get a JSON element by int index or raise an exception.\"\"\"\n\n    text = r'''\n        SELECT\n            CASE jsonb_typeof(val)\n            WHEN 'object' THEN (\n                edgedb_VER.raise(\n                    NULL::jsonb,\n                    'wrong_object_type',\n                    msg => (\n                        'cannot index JSON ' || jsonb_typeof(val)\n                        || ' by ' || pg_typeof(index)::text\n                    ),\n                    detail => detail\n                )\n            )\n            WHEN 'array' THEN (\n                edgedb_VER.raise_on_null(\n                    val -> edgedb_VER._int_or_null(index),\n                    'invalid_parameter_value',\n                    msg => 'JSON index ' || index::text || ' is out of bounds',\n                    detail => detail\n                )\n            )\n            WHEN 'string' THEN (\n                to_jsonb(edgedb_VER._index(\n                    val#>>'{}',\n                    index,\n                    detail,\n                    'JSON'\n                ))\n            )\n            ELSE\n                edgedb_VER.raise(\n                    NULL::jsonb,\n                    'wrong_object_type',\n                    msg => (\n                        'cannot index JSON '\n                        || coalesce(jsonb_typeof(val), 'UNKNOWN')\n                    ),\n                    detail => (\n                        '{\"hint\":\"Retrieving an element by an integer index '\n                        || 'is only available for JSON arrays and strings.\"}'\n                    )\n                )\n            END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_index'),\n            args=[\n                ('val', ('jsonb',)),\n                ('index', ('bigint',)),\n                ('detail', ('text',), \"''\"),\n            ],\n            returns=('jsonb',),\n            # Min volatility of exception helpers and pg_typeof is 'stable',\n            # but for all practical purposes, we can assume 'immutable'\n            volatility='stable',\n            wrapper_volatility='immutable',\n            strict=True,\n            text=self.text,\n        )\n\n\nclass JSONSliceFunction(trampoline.VersionedFunction):\n    \"\"\"Get a JSON array slice.\"\"\"\n\n    text = r\"\"\"\n        SELECT\n            CASE\n            WHEN val IS NULL THEN NULL\n            WHEN jsonb_typeof(val) = 'array' THEN (\n                to_jsonb(edgedb_VER._slice(\n                    (\n                        SELECT coalesce(array_agg(value), '{}'::jsonb[])\n                        FROM jsonb_array_elements(val)\n                    ),\n                    start, stop\n                ))\n            )\n            WHEN jsonb_typeof(val) = 'string' THEN (\n                to_jsonb(edgedb_VER._slice(val#>>'{}', start, stop))\n            )\n            ELSE\n                edgedb_VER.raise(\n                    NULL::jsonb,\n                    'wrong_object_type',\n                    msg => (\n                        'cannot slice JSON '\n                        || coalesce(jsonb_typeof(val), 'UNKNOWN')\n                    ),\n                    detail => (\n                        '{\"hint\":\"Slicing is only available for JSON arrays'\n                        || ' and strings.\"}'\n                    )\n                )\n            END\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=(\"edgedb\", \"_slice\"),\n            args=[\n                (\"val\", (\"jsonb\",)),\n                (\"start\", (\"bigint\",)),\n                (\"stop\", (\"bigint\",)),\n            ],\n            returns=(\"jsonb\",),\n            # Min volatility of to_jsonb is 'stable',\n            # but for all practical purposes, we can assume 'immutable'\n            volatility=\"stable\",\n            wrapper_volatility='immutable',\n            text=self.text,\n        )\n\n\n# We need custom casting functions for various datetime scalars in\n# order to enforce correctness w.r.t. local vs time-zone-aware\n# datetime. Postgres does a lot of magic and guessing for time zones\n# and generally will accept text with or without time zone for any\n# particular flavor of timestamp. In order to guarantee that we can\n# detect time-zones we restrict the inputs to ISO8601 format.\n#\n# See issue #740.\nclass DatetimeInFunction(trampoline.VersionedFunction):\n    \"\"\"Cast text into timestamptz using ISO8601 spec.\"\"\"\n    text = r'''\n        SELECT\n            CASE WHEN val !~ (\n                    '^\\s*(' ||\n                        '(\\d{4}-\\d{2}-\\d{2}|\\d{8})' ||\n                        '[ tT]' ||\n                        '(\\d{2}(:\\d{2}(:\\d{2}(\\.\\d+)?)?)?|\\d{2,6}(\\.\\d+)?)' ||\n                        '([zZ]|[-+](\\d{2,4}|\\d{2}:\\d{2}))' ||\n                    ')\\s*$'\n                )\n            THEN\n                edgedb_VER.raise(\n                    NULL::edgedbt.timestamptz_t,\n                    'invalid_datetime_format',\n                    msg => (\n                        'invalid input syntax for type timestamptz: '\n                        || quote_literal(val)\n                    ),\n                    detail => (\n                        '{\"hint\":\"Please use ISO8601 format. Example: '\n                        || '2010-12-27T23:59:59-07:00. Alternatively '\n                        || '\\\"to_datetime\\\" function provides custom '\n                        || 'formatting options.\"}'\n                    )\n                )\n            ELSE\n                val::edgedbt.timestamptz_t\n            END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'datetime_in'),\n            args=[('val', ('text',))],\n            returns=('edgedbt', 'timestamptz_t'),\n            # Same volatility as raise() (stable)\n            volatility='stable',\n            text=self.text)\n\n\nclass DurationInFunction(trampoline.VersionedFunction):\n    \"\"\"Cast text into duration, ensuring there is no days or months units\"\"\"\n    text = r'''\n        SELECT\n            CASE WHEN\n                EXTRACT(MONTH FROM v.column1) != 0 OR\n                EXTRACT(YEAR FROM v.column1) != 0 OR\n                EXTRACT(DAY FROM v.column1) != 0\n            THEN\n                edgedb_VER.raise(\n                    NULL::edgedbt.duration_t,\n                    'invalid_datetime_format',\n                    msg => (\n                        'invalid input syntax for type std::duration: '\n                        || quote_literal(val)\n                    ),\n                    detail => (\n                        '{\"hint\":\"Day, month and year units cannot be used '\n                        || 'for std::duration.\"}'\n                    )\n                )\n            ELSE v.column1::edgedbt.duration_t\n            END\n        FROM\n            (VALUES (\n                val::interval\n            )) AS v\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'duration_in'),\n            args=[('val', ('text',))],\n            returns=('edgedbt', 'duration_t'),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass DateDurationInFunction(trampoline.VersionedFunction):\n    \"\"\"\n    Cast text into date_duration, ensuring there is no unit smaller\n    than days.\n    \"\"\"\n    text = r'''\n        SELECT\n            CASE WHEN\n                EXTRACT(HOUR FROM v.column1) != 0 OR\n                EXTRACT(MINUTE FROM v.column1) != 0 OR\n                EXTRACT(SECOND FROM v.column1) != 0\n            THEN\n                edgedb_VER.raise(\n                    NULL::edgedbt.date_duration_t,\n                    'invalid_datetime_format',\n                    msg => (\n                        'invalid input syntax for type '\n                        || 'std::cal::date_duration: '\n                        || quote_literal(val)\n                    ),\n                    detail => (\n                        '{\"hint\":\"Units smaller than days cannot be used '\n                        || 'for std::cal::date_duration.\"}'\n                    )\n                )\n            ELSE v.column1::edgedbt.date_duration_t\n            END\n        FROM\n            (VALUES (\n                val::interval\n            )) AS v\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'date_duration_in'),\n            args=[('val', ('text',))],\n            returns=('edgedbt', 'date_duration_t'),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass LocalDatetimeInFunction(trampoline.VersionedFunction):\n    \"\"\"Cast text into timestamp using ISO8601 spec.\"\"\"\n    text = r'''\n        SELECT\n            CASE WHEN\n                val !~ (\n                    '^\\s*(' ||\n                        '(\\d{4}-\\d{2}-\\d{2}|\\d{8})' ||\n                        '[ tT]' ||\n                        '(\\d{2}(:\\d{2}(:\\d{2}(\\.\\d+)?)?)?|\\d{2,6}(\\.\\d+)?)' ||\n                    ')\\s*$'\n                )\n            THEN\n                edgedb_VER.raise(\n                    NULL::edgedbt.timestamp_t,\n                    'invalid_datetime_format',\n                    msg => (\n                        'invalid input syntax for type timestamp: '\n                        || quote_literal(val)\n                    ),\n                    detail => (\n                        '{\"hint\":\"Please use ISO8601 format. Example '\n                        || '2010-04-18T09:27:00 Alternatively '\n                        || '\\\"to_local_datetime\\\" function provides custom '\n                        || 'formatting options.\"}'\n                    )\n                )\n            ELSE\n                val::edgedbt.timestamp_t\n            END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'local_datetime_in'),\n            args=[('val', ('text',))],\n            returns=('edgedbt', 'timestamp_t'),\n            volatility='immutable',\n            text=self.text)\n\n\nclass LocalDateInFunction(trampoline.VersionedFunction):\n    \"\"\"Cast text into date using ISO8601 spec.\"\"\"\n    text = r'''\n        SELECT\n            CASE WHEN\n                val !~ (\n                    '^\\s*(' ||\n                        '(\\d{4}-\\d{2}-\\d{2}|\\d{8})' ||\n                    ')\\s*$'\n                )\n            THEN\n                edgedb_VER.raise(\n                    NULL::edgedbt.date_t,\n                    'invalid_datetime_format',\n                    msg => (\n                        'invalid input syntax for type date: '\n                        || quote_literal(val)\n                    ),\n                    detail => (\n                        '{\"hint\":\"Please use ISO8601 format. Example '\n                        || '2010-04-18 Alternatively '\n                        || '\\\"to_local_date\\\" function provides custom '\n                        || 'formatting options.\"}'\n                    )\n                )\n            ELSE\n                val::edgedbt.date_t\n            END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'local_date_in'),\n            args=[('val', ('text',))],\n            returns=('edgedbt', 'date_t'),\n            volatility='immutable',\n            text=self.text)\n\n\nclass LocalTimeInFunction(trampoline.VersionedFunction):\n    \"\"\"Cast text into time using ISO8601 spec.\"\"\"\n    text = r'''\n        SELECT\n            CASE WHEN date_part('hour', x.t) = 24\n            THEN\n                edgedb_VER.raise(\n                    NULL::time,\n                    'invalid_datetime_format',\n                    msg => (\n                        'std::cal::local_time field value out of range: '\n                        || quote_literal(val)\n                    )\n                )\n            ELSE\n                x.t\n            END\n        FROM (\n            SELECT\n                CASE WHEN val !~ ('^\\s*(' ||\n                        '(\\d{2}(:\\d{2}(:\\d{2}(\\.\\d+)?)?)?|\\d{2,6}(\\.\\d+)?)' ||\n                    ')\\s*$')\n                THEN\n                    edgedb_VER.raise(\n                        NULL::time,\n                        'invalid_datetime_format',\n                        msg => (\n                            'invalid input syntax for type time: '\n                            || quote_literal(val)\n                        ),\n                        detail => (\n                            '{\"hint\":\"Please use ISO8601 format. Examples: '\n                            || '18:43:27 or 18:43 Alternatively '\n                            || '\\\"to_local_time\\\" function provides custom '\n                            || 'formatting options.\"}'\n                        )\n                    )\n                ELSE\n                    val::time\n                END as t\n        ) as x;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'local_time_in'),\n            args=[('val', ('text',))],\n            returns=('time',),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass ToTimestampTZCheck(trampoline.VersionedFunction):\n    \"\"\"Checks if the original text has time zone or not.\"\"\"\n    # What are we trying to mitigate?\n    # We're trying to detect that when we're casting to datetime the\n    # time zone is in fact present in the input. It is a problem if\n    # it's not since then one gets assigned implicitly based on the\n    # server settings.\n    #\n    # It is insufficient to rely on the presence of TZH in the format\n    # string, since `to_timestamp` will happily ignore the missing\n    # time-zone in the input anyway. So in order to tell whether the\n    # input string contained a time zone that was in fact parsed we\n    # employ the following trick:\n    #\n    # If the time zone is in the input then it is unambiguous and the\n    # parsed value will not depend on the current server time zone.\n    # However, if the time zone was omitted, then the parsed value\n    # will default to the server time zone. This implies that if\n    # changing the server time zone for the same input string affects\n    # the parsed value, the input string itself didn't contain a time\n    # zone.\n    text = r'''\n        DECLARE\n            result timestamptz;\n            chk timestamptz;\n            msg text;\n        BEGIN\n            result := to_timestamp(val, fmt);\n            PERFORM set_config('TimeZone', 'America/Toronto', true);\n            chk := to_timestamp(val, fmt);\n            -- We're deliberately not doing any save/restore because\n            -- the server MUST be in UTC. In fact, this check relies\n            -- on it.\n            PERFORM set_config('TimeZone', 'UTC', true);\n\n            IF hastz THEN\n                msg := 'missing required';\n            ELSE\n                msg := 'unexpected';\n            END IF;\n\n            IF (result = chk) != hastz THEN\n                RAISE EXCEPTION USING\n                    ERRCODE = 'invalid_datetime_format',\n                    MESSAGE = msg || ' time zone in input ' ||\n                        quote_literal(val),\n                    DETAIL = '';\n            END IF;\n\n            RETURN result::edgedbt.timestamptz_t;\n        END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_to_timestamptz_check'),\n            args=[('val', ('text',)), ('fmt', ('text',)),\n                  ('hastz', ('bool',))],\n            returns=('edgedbt', 'timestamptz_t'),\n            # We're relying on changing settings, so it's volatile.\n            volatility='volatile',\n            language='plpgsql',\n            text=self.text)\n\n\nclass ToDatetimeFunction(trampoline.VersionedFunction):\n    \"\"\"Convert text into timestamptz using a formatting spec.\"\"\"\n    # NOTE that if only the TZM (minutes) are mentioned it is not\n    # enough for a valid time zone definition\n    text = r'''\n        SELECT\n            CASE WHEN fmt !~ (\n                    '^(' ||\n                        '(\"([^\"\\\\]|\\\\.)*\")|' ||\n                        '([^\"]+)' ||\n                    ')*(TZH).*$'\n                )\n            THEN\n                edgedb_VER.raise(\n                    NULL::edgedbt.timestamptz_t,\n                    'invalid_datetime_format',\n                    msg => (\n                        'missing required time zone in format: '\n                        || quote_literal(fmt)\n                    ),\n                    detail => (\n                        $h${\"hint\":\"Use one or both of the following: $h$\n                        || $h$'TZH', 'TZM'\"}$h$\n                    )\n                )\n            ELSE\n                edgedb_VER._to_timestamptz_check(val, fmt, true)\n            END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'to_datetime'),\n            args=[('val', ('text',)), ('fmt', ('text',))],\n            returns=('edgedbt', 'timestamptz_t'),\n            # Same as _to_timestamptz_check.\n            volatility='volatile',\n            text=self.text)\n\n\nclass ToLocalDatetimeFunction(trampoline.VersionedFunction):\n    \"\"\"Convert text into timestamp using a formatting spec.\"\"\"\n    # NOTE time zone should not be mentioned at all.\n    text = r'''\n        SELECT\n            CASE WHEN fmt ~ (\n                    '^(' ||\n                        '(\"([^\"\\\\]|\\\\.)*\")|' ||\n                        '([^\"]+)' ||\n                    ')*(TZH|TZM).*$'\n                )\n            THEN\n                edgedb_VER.raise(\n                    NULL::edgedbt.timestamp_t,\n                    'invalid_datetime_format',\n                    msg => (\n                        'unexpected time zone in format: '\n                        || quote_literal(fmt)\n                    )\n                )\n            ELSE\n                edgedb_VER._to_timestamptz_check(val, fmt, false)\n                    ::edgedbt.timestamp_t\n            END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'to_local_datetime'),\n            args=[('val', ('text',)), ('fmt', ('text',))],\n            returns=('edgedbt', 'timestamp_t'),\n            # Same as _to_timestamptz_check.\n            volatility='volatile',\n            text=self.text)\n\n\nclass StrToBool(trampoline.VersionedFunction):\n    \"\"\"Parse bool from text.\"\"\"\n    # We first try to match case-insensitive \"true|false\" at all. On\n    # null, we raise an exception. But otherwise we know that we have\n    # an array of matches. The first element matching \"true\" and\n    # second - \"false\". So the boolean value is then \"true\" if the\n    # second array element is NULL and false otherwise.\n    text = r'''\n        SELECT (\n            coalesce(\n                regexp_match(val, '^\\s*(?:(true)|(false))\\s*$', 'i')::text[],\n                edgedb_VER.raise(\n                    NULL::text[],\n                    'invalid_text_representation',\n                    msg => 'invalid input syntax for type bool: '\n                           || quote_literal(val)\n                )\n            )\n        )[2] IS NULL;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'str_to_bool'),\n            args=[('val', ('text',))],\n            returns=('bool',),\n            strict=True,\n            # Stable because it's raising exceptions.\n            volatility='stable',\n            text=self.text)\n\n\nclass QuoteLiteralFunction(trampoline.VersionedFunction):\n    \"\"\"Encode string as edgeql literal quoted string\"\"\"\n    text = r'''\n        SELECT concat('\\'',\n            replace(\n                replace(val, '\\\\', '\\\\\\\\'),\n                '\\'', '\\\\\\''),\n            '\\'')\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'quote_literal'),\n            args=[('val', ('text',))],\n            returns=('str',),\n            volatility='immutable',\n            text=self.text)\n\n\nclass QuoteIdentFunction(trampoline.VersionedFunction):\n    \"\"\"Quote ident function.\"\"\"\n    # TODO do not quote valid identifiers unless they are reserved\n    text = r'''\n        SELECT concat('`', replace(val, '`', '``'), '`')\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'quote_ident'),\n            args=[('val', ('text',))],\n            returns=('text',),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass QuoteNameFunction(trampoline.VersionedFunction):\n\n    text = r\"\"\"\n        SELECT\n            string_agg(edgedb_VER.quote_ident(np), '::')\n        FROM\n            unnest(string_to_array(\"name\", '::')) AS np\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'quote_name'),\n            args=[('name', ('text',))],\n            returns=('text',),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass DescribeRolesAsDDLFunctionForwardDecl(trampoline.VersionedFunction):\n    \"\"\"Forward declaration for _describe_roles_as_ddl\"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_describe_roles_as_ddl'),\n            args=[],\n            returns=('text'),\n            # Stable because it's raising exceptions.\n            volatility='stable',\n            text='SELECT NULL::text',\n        )\n\n\nclass DescribeRolesAsDDLFunction(trampoline.VersionedFunction):\n    \"\"\"Describe roles as DDL\"\"\"\n\n    def __init__(self, schema: s_schema.Schema) -> None:\n        role_obj = schema.get(\"sys::Role\", type=s_objtypes.ObjectType)\n        roles = _schema_alias_view_name(schema, role_obj)\n        roles = (common.maybe_versioned_schema(roles[0]), roles[1])\n\n        member_of = role_obj.getptr(schema, s_name.UnqualName('member_of'))\n        members = _schema_alias_view_name(schema, member_of)\n        members = (common.maybe_versioned_schema(members[0]), members[1])\n\n        permissions_ptr = role_obj.getptr(\n            schema, s_name.UnqualName('permissions'), type=s_props.Property\n        )\n        permissions = _schema_alias_view_name(schema, permissions_ptr)\n        permissions = (\n            common.maybe_versioned_schema(permissions[0]), permissions[1]\n        )\n\n        branches_ptr = role_obj.getptr(\n            schema, s_name.UnqualName('branches'), type=s_props.Property\n        )\n        branches = _schema_alias_view_name(schema, branches_ptr)\n        branches = (\n            common.maybe_versioned_schema(branches[0]), branches[1]\n        )\n\n        super_col = ptr_col_name(schema, role_obj, 'superuser')\n        name_col = ptr_col_name(schema, role_obj, 'name')\n        pass_col = ptr_col_name(schema, role_obj, 'password')\n        pg_pol_col = ptr_col_name(\n            schema,\n            role_obj,\n            'apply_access_policies_pg_default',\n        )\n        qi_superuser = qlquote.quote_ident(defines.EDGEDB_SUPERUSER)\n        text = f\"\"\"\n            WITH RECURSIVE\n            dependencies AS (\n                SELECT r.id AS id, m.target AS parent\n                    FROM {q(*roles)} r\n                        LEFT OUTER JOIN {q(*members)} m ON r.id = m.source\n            ),\n            roles_with_depths(id, depth) AS (\n                SELECT id, 0 FROM dependencies WHERE parent IS NULL\n                UNION ALL\n                SELECT dependencies.id, roles_with_depths.depth + 1\n                FROM dependencies\n                INNER JOIN roles_with_depths\n                    ON dependencies.parent = roles_with_depths.id\n            ),\n            ordered_roles AS (\n                SELECT id, max(depth) FROM roles_with_depths\n                GROUP BY id\n                ORDER BY max(depth) ASC\n            )\n            SELECT\n            coalesce(string_agg(\n                CASE WHEN\n                    role.{qi(name_col)} = {ql(defines.EDGEDB_SUPERUSER)} THEN\n                    NULLIF(\n                        concat(\n                            'ALTER ROLE {qi_superuser} {{ ',\n                            NULLIF(\n                                (SELECT\n                                    concat(\n                                        'EXTENDING ',\n                                        string_agg(\n                                            edgedb_VER.quote_ident(\n                                                parent.{qi(name_col)}\n                                            ),\n                                            ', '\n                                        ),\n                                        '; '\n                                    )\n                                    FROM {q(*members)} member\n                                        INNER JOIN {q(*roles)} parent\n                                        ON parent.id = member.target\n                                    WHERE member.source = role.id\n                                ),\n                                'EXTENDING ; '\n                            ),\n                            (CASE\n                                WHEN role.{qi(pass_col)} IS NOT NULL THEN\n                                    concat(\n                                        'SET password_hash := ',\n                                        quote_literal(role.{qi(pass_col)}),\n                                        '; '\n                                    )\n                                ELSE NULL END\n                            ),\n                            (CASE\n                                WHEN role.{qi(pg_pol_col)} IS NOT NULL THEN\n                                    concat(\n                                        'SET apply_access_policies_pg_default ',\n                                        ':= ',\n                                        role.{qi(pg_pol_col)}::text,\n                                        '; '\n                                    )\n                                ELSE NULL END\n                            ),\n                            NULLIF (\n                                concat(\n                                    'SET permissions := {{ ',\n                                    (\n                                        SELECT\n                                            string_agg(\n                                                permissions.target,\n                                                ', '\n                                            )\n                                        FROM {q(*permissions)} permissions\n                                        WHERE permissions.source = role.id\n                                    ),\n                                    ' }}; '\n                                ),\n                                'SET permissions := {{  }}; '\n                            ),\n                            NULLIF (\n                                concat(\n                                    'SET branches := {{ ',\n                                    (\n                                        SELECT\n                                            string_agg(\n                                                quote_literal(branches.target),\n                                                ', '\n                                            )\n                                        FROM {q(*branches)} branches\n                                        WHERE branches.source = role.id\n                                    ),\n                                    ' }}; '\n                                ),\n                                'SET branches := {{ ''*'' }}; '\n                            ),\n\n                            '}};'\n                        ),\n                        'ALTER ROLE {qi_superuser} {{ }};'\n                    )\n                ELSE\n                    concat(\n                        'CREATE ',\n                        (CASE\n                            WHEN role.{qi(super_col)} THEN\n                                'SUPERUSER '\n                            ELSE NULL END\n                        ),\n                        'ROLE ',\n                        edgedb_VER.quote_ident(role.{qi(name_col)}),\n                        NULLIF(\n                            (\n                                SELECT\n                                    concat(\n                                        ' EXTENDING ',\n                                        string_agg(\n                                            edgedb_VER.quote_ident(\n                                                parent.{qi(name_col)}\n                                            ),\n                                            ', '\n                                        )\n                                    )\n                                FROM {q(*members)} member\n                                    INNER JOIN {q(*roles)} parent\n                                    ON parent.id = member.target\n                                WHERE member.source = role.id\n                            ),\n                            ' EXTENDING '\n                        ),\n                        NULLIF(\n                            concat(\n                                ' {{ ',\n                                (CASE\n                                    WHEN role.{qi(pass_col)} IS NOT NULL THEN\n                                        concat(\n                                            'SET password_hash := ',\n                                            quote_literal(role.{qi(pass_col)}),\n                                            '; '\n                                        )\n                                    ELSE NULL END\n                                ),\n                                (CASE\n                                    WHEN role.{qi(pg_pol_col)} IS NOT NULL THEN\n                                        concat(\n                                            'SET ',\n                                            'apply_access_policies_pg_default ',\n                                            ':= ',\n                                            role.{qi(pg_pol_col)}::text,\n                                            '; '\n                                        )\n                                    ELSE NULL END\n                                ),\n                                NULLIF (\n                                    concat(\n                                        'SET permissions := {{ ',\n                                        (\n                                            SELECT\n                                                string_agg(\n                                                    permissions.target,\n                                                    ', '\n                                                )\n                                            FROM {q(*permissions)} permissions\n                                            WHERE permissions.source = role.id\n                                        ),\n                                        ' }}; '\n                                    ),\n                                    'SET permissions := {{  }}; '\n                                ),\n                                NULLIF (\n                                    concat(\n                                        'SET branches := {{ ',\n                                        (\n                                            SELECT\n                                                string_agg(\n                                                    quote_literal(\n                                                        branches.target),\n                                                    ', '\n                                                )\n                                            FROM {q(*branches)} branches\n                                            WHERE branches.source = role.id\n                                        ),\n                                        ' }}; '\n                                    ),\n                                    'SET branches := {{ ''*'' }}; '\n                                ),\n\n                                '}}'\n                            ),\n                            ' {{ }}'\n                        ),\n                        ';'\n                    )\n                END,\n                '\\n'\n            ), '') str\n            FROM ordered_roles\n                JOIN {q(*roles)} role\n                ON role.id = ordered_roles.id\n        \"\"\"\n\n        super().__init__(\n            name=('edgedb', '_describe_roles_as_ddl'),\n            args=[],\n            returns=('text'),\n            # Stable because it's raising exceptions.\n            volatility='stable',\n            text=text)\n\n\nclass AllRoleMembershipsFunctionForwardDecl(trampoline.VersionedFunction):\n    \"\"\"Forward declaration for _all_role_memberships\"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_all_role_memberships'),\n            args=[('role_id', ('uuid',))],\n            returns=('uuid[]'),\n            volatility='stable',\n            text='SELECT NULL::uuid[]',\n        )\n\n\nclass AllRoleMembershipsFunction(trampoline.VersionedFunction):\n    \"\"\"Get all memberships for a given role\"\"\"\n\n    def __init__(self, schema: s_schema.Schema) -> None:\n        role_obj = schema.get(\"sys::Role\", type=s_objtypes.ObjectType)\n        roles = _schema_alias_view_name(schema, role_obj)\n        roles = (common.maybe_versioned_schema(roles[0]), roles[1])\n\n        member_of = role_obj.getptr(schema, s_name.UnqualName('member_of'))\n        members = _schema_alias_view_name(schema, member_of)\n        members = (common.maybe_versioned_schema(members[0]), members[1])\n\n        text = f\"\"\"\n            WITH RECURSIVE memberships (id, member_of)\n            AS (\n                (\n                    SELECT\n                        r.id as id,\n                        m.target as member_of\n                    FROM {q(*roles)} r\n                    INNER JOIN {q(*members)} m\n                    ON\n                        r.id = m.source\n                    WHERE\n                        r.id = \"role_id\"\n                )\n                UNION\n                (\n                    SELECT\n                        r.id as id,\n                        m.target as member_of\n                    FROM {q(*roles)} r\n                    INNER JOIN {q(*members)} m\n                    ON\n                        r.id = m.source\n                    INNER JOIN memberships ms\n                    ON\n                        ms.member_of = r.id\n                )\n            )\n            SELECT\n                array_agg(member_of)\n            FROM\n                memberships\n        \"\"\"\n\n        super().__init__(\n            name=('edgedb', '_all_role_memberships'),\n            args=[('role_id', ('uuid',))],\n            returns=('uuid[]'),\n            volatility='stable',\n            text=text,\n        )\n\n\nclass DumpSequencesFunction(trampoline.VersionedFunction):\n\n    text = r\"\"\"\n        SELECT\n            string_agg(\n                'SELECT std::sequence_reset('\n                || 'INTROSPECT ' || edgedb_VER.quote_name(seq.name)\n                || (CASE WHEN seq_st.is_called\n                    THEN ', ' || seq_st.last_value::text\n                    ELSE '' END)\n                || ');',\n                E'\\n'\n            )\n        FROM\n            (SELECT\n                id,\n                name\n             FROM\n                edgedb_VER.\"_SchemaScalarType\"\n             WHERE\n                id = any(\"seqs\")\n            ) AS seq,\n            LATERAL (\n                SELECT\n                    COALESCE(last_value, start_value)::text AS last_value,\n                    last_value IS NOT NULL AS is_called\n                FROM\n                    pg_sequences,\n                    LATERAL ROWS FROM (\n                        edgedb_VER.get_sequence_backend_name(seq.id)\n                    ) AS seq_name(schema text, name text)\n                WHERE\n                    (pg_sequences.schemaname, pg_sequences.sequencename)\n                    = (seq_name.schema, seq_name.name)\n            ) AS seq_st\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_dump_sequences'),\n            args=[('seqs', ('uuid[]',))],\n            returns=('text',),\n            # Volatile because sequence state is volatile\n            volatility='volatile',\n            text=self.text,\n        )\n\n\nclass SysConfigSourceType(dbops.Enum):\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_sys_config_source_t'),\n            values=[\n                'default',\n                'postgres default',\n                'postgres environment variable',\n                'postgres configuration file',\n                'environment variable',\n                'command line',\n                'postgres command line',\n                'postgres global',\n                'postgres client',\n                'system override',\n                'database',\n                'postgres override',\n                'postgres interactive',\n                'postgres test',\n                'session',\n            ]\n        )\n\n\nclass SysConfigScopeType(dbops.Enum):\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_sys_config_scope_t'),\n            values=[\n                'INSTANCE',\n                'DATABASE',\n                'SESSION',\n            ]\n        )\n\n\nclass SysConfigValueType(dbops.CompositeType):\n    \"\"\"Type of values returned by _read_sys_config.\"\"\"\n    def __init__(self) -> None:\n        super().__init__(name=('edgedb', '_sys_config_val_t'))\n\n        self.add_columns([\n            dbops.Column(name='name', type='text'),\n            dbops.Column(name='value', type='jsonb'),\n            dbops.Column(name='source', type='edgedb._sys_config_source_t'),\n            dbops.Column(name='scope', type='edgedb._sys_config_scope_t'),\n        ])\n\n\nclass SysConfigEntryType(dbops.CompositeType):\n    \"\"\"Type of values returned by _read_sys_config_full.\"\"\"\n    def __init__(self) -> None:\n        super().__init__(name=('edgedb', '_sys_config_entry_t'))\n\n        self.add_columns([\n            dbops.Column(name='max_source', type='edgedb._sys_config_source_t'),\n            dbops.Column(name='value', type='edgedb._sys_config_val_t'),\n        ])\n\n\nclass IntervalToMillisecondsFunction(trampoline.VersionedFunction):\n    \"\"\"Cast an interval into milliseconds.\"\"\"\n\n    text = r'''\n        SELECT\n            trunc(extract(hours from \"val\"))::numeric * 3600000 +\n            trunc(extract(minutes from \"val\"))::numeric * 60000 +\n            trunc(extract(milliseconds from \"val\"))::numeric\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_interval_to_ms'),\n            args=[('val', ('interval',))],\n            returns=('numeric',),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass SafeIntervalCastFunction(trampoline.VersionedFunction):\n    \"\"\"A safer text to interval casting implementaion.\n\n    Casting large-unit durations (like '4032000000us') results in an error.\n    Huge durations like this can be returned when introspecting current\n    database config. Fix that by parsing the argument and using multiplication.\n    \"\"\"\n\n    text = r'''\n        SELECT\n            CASE\n\n                WHEN m.v[1] IS NOT NULL AND m.v[2] IS NOT NULL\n                THEN\n                    m.v[1]::numeric * ('1' || m.v[2])::interval\n\n                ELSE\n                    \"val\"::interval\n            END\n        FROM LATERAL (\n            SELECT regexp_match(\n                \"val\", '^(\\d+)\\s*(us|ms|s|min|h)$') AS v\n        ) AS m\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_interval_safe_cast'),\n            args=[('val', ('text',))],\n            returns=('interval',),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass ConvertPostgresConfigUnitsFunction(trampoline.VersionedFunction):\n    \"\"\"Convert duration/memory values to milliseconds/kilobytes.\n\n    See https://www.postgresql.org/docs/12/config-setting.html\n    for information about the units Postgres config system has.\n    \"\"\"\n\n    text = r\"\"\"\n    SELECT (\n        CASE\n            WHEN \"unit\" = any(ARRAY['us', 'ms', 's', 'min', 'h'])\n            THEN to_jsonb(\n                edgedb_VER._interval_safe_cast(\n                    (\"value\" * \"multiplier\")::text || \"unit\"\n                )\n            )\n\n            WHEN \"unit\" = 'B'\n            THEN to_jsonb(\n                (\"value\" * \"multiplier\")::text || 'B'\n            )\n\n            WHEN \"unit\" = 'kB'\n            THEN to_jsonb(\n                (\"value\" * \"multiplier\")::text || 'KiB'\n            )\n\n            WHEN \"unit\" = 'MB'\n            THEN to_jsonb(\n                (\"value\" * \"multiplier\")::text || 'MiB'\n            )\n\n            WHEN \"unit\" = 'GB'\n            THEN to_jsonb(\n                (\"value\" * \"multiplier\")::text || 'GiB'\n            )\n\n            WHEN \"unit\" = 'TB'\n            THEN to_jsonb(\n                (\"value\" * \"multiplier\")::text || 'TiB'\n            )\n\n            WHEN \"unit\" = ''\n            THEN (\"value\" * \"multiplier\")::text::jsonb\n\n            ELSE edgedb_VER.raise(\n                NULL::jsonb,\n                msg => (\n                    'unknown configuration unit \"' ||\n                    COALESCE(\"unit\", '<NULL>') ||\n                    '\"'\n                )\n            )\n        END\n    )\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_convert_postgres_config_units'),\n            args=[\n                ('value', ('numeric',)),\n                ('multiplier', ('numeric',)),\n                ('unit', ('text',))\n            ],\n            returns=('jsonb',),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass TypeIDToConfigType(trampoline.VersionedFunction):\n    \"\"\"Get a postgres config type from a type id.\n\n    (We typically try to read extension configs straight from the\n    config tables, but for extension configs those aren't present.)\n    \"\"\"\n\n    config_types = {\n        'bool': ['std::bool'],\n        'string': ['std::str'],\n        'integer': ['std::int16', 'std::int32', 'std::int64'],\n        'real': ['std::float32', 'std::float64'],\n    }\n    cases = [\n        f'''\n        WHEN \"typeid\" = '{s_obj.get_known_type_id(t)}' THEN '{ct}'\n        '''\n        for ct, types in config_types.items()\n        for t in types\n    ]\n    scases = '\\n'.join(cases)\n\n    text = f\"\"\"\n    SELECT (\n        CASE\n            {scases}\n            ELSE edgedb_VER.raise(\n                NULL::text,\n                msg => (\n                    'unknown configuration type \"' || \"typeid\" || '\"'\n                )\n            )\n        END\n    )\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_type_id_to_config_type'),\n            args=[\n                ('typeid', ('uuid',)),\n            ],\n            returns=('text',),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass NormalizedPgSettingsView(trampoline.VersionedView):\n    \"\"\"Just like `pg_settings` but with the parsed 'unit' column.\"\"\"\n\n    query = r'''\n        SELECT\n            s.name AS name,\n            s.setting AS setting,\n            s.vartype AS vartype,\n            s.source AS source,\n            unit.multiplier AS multiplier,\n            unit.unit AS unit\n\n        FROM pg_settings AS s,\n\n        LATERAL (\n            SELECT regexp_match(\n                s.unit, '^(\\d*)\\s*([a-zA-Z]{1,3})$') AS v\n        ) AS _unit,\n\n        LATERAL (\n            SELECT\n                COALESCE(\n                    CASE\n                        WHEN _unit.v[1] = '' THEN 1\n                        ELSE _unit.v[1]::int\n                    END,\n                    1\n                ) AS multiplier,\n                COALESCE(_unit.v[2], '') AS unit\n        ) AS unit\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_normalized_pg_settings'),\n            query=self.query,\n        )\n\n\nclass InterpretConfigValueToJsonFunction(trampoline.VersionedFunction):\n    \"\"\"Convert a Postgres config value to jsonb.\n\n    This function:\n\n    * converts booleans to JSON true/false;\n    * converts enums and strings to JSON strings;\n    * converts real/integers to JSON numbers:\n      - for durations: we always convert to milliseconds;\n      - for memory size: we always convert to kilobytes;\n      - already unitless numbers are left as is.\n\n    See https://www.postgresql.org/docs/current/config-setting.html\n    for information about the units Postgres config system has.\n    \"\"\"\n\n    text = r\"\"\"\n    SELECT (\n        CASE\n            WHEN \"type\" = 'bool'\n            THEN (\n                CASE\n                WHEN lower(\"value\") = any(ARRAY['on', 'true', 'yes', '1'])\n                THEN 'true'\n                ELSE 'false'\n                END\n            )::jsonb\n\n            WHEN \"type\" = 'enum' OR \"type\" = 'string'\n            THEN to_jsonb(\"value\")\n\n            WHEN \"type\" = 'integer' OR \"type\" = 'real'\n            THEN edgedb_VER._convert_postgres_config_units(\n                    \"value\"::numeric, \"multiplier\"::numeric, \"unit\"\n                 )\n\n            ELSE\n                edgedb_VER.raise(\n                    NULL::jsonb,\n                    msg => (\n                        'unknown configuration type \"' ||\n                        COALESCE(\"type\", '<NULL>') ||\n                        '\"'\n                    )\n                )\n        END\n    )\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_interpret_config_value_to_json'),\n            args=[\n                ('value', ('text',)),\n                ('type', ('text',)),\n                ('multiplier', ('int',)),\n                ('unit', ('text',))\n            ],\n            returns=('jsonb',),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass PostgresJsonConfigValueToFrontendConfigValueFunction(\n    trampoline.VersionedFunction,\n):\n    \"\"\"Convert a Postgres config value to frontend config value.\n\n    Most values are retained as-is, but some need translation, which\n    is implemented as a to_frontend_expr() on the corresponding\n    setting ScalarType.\n    \"\"\"\n\n    def __init__(self, config_spec: edbconfig.Spec) -> None:\n        variants_list = []\n        for setting in config_spec.values():\n            if (\n                setting.backend_setting\n                and isinstance(setting.type, type)\n                and issubclass(setting.type, statypes.ScalarType)\n            ):\n                conv_expr = setting.type.to_frontend_expr('\"value\"->>0')\n                if conv_expr is not None:\n                    variants_list.append(f\"\"\"\n                        WHEN {ql(setting.backend_setting)}\n                        THEN to_jsonb({conv_expr})\n                    \"\"\")\n\n        variants = \"\\n\".join(variants_list)\n        text = f\"\"\"\n        SELECT (\n            CASE \"setting_name\"\n                {variants}\n                ELSE \"value\"\n            END\n        )\n        \"\"\"\n\n        super().__init__(\n            name=('edgedb', '_postgres_json_config_value_to_fe_config_value'),\n            args=[\n                ('setting_name', ('text',)),\n                ('value', ('jsonb',))\n            ],\n            returns=('jsonb',),\n            volatility='immutable',\n            text=text,\n        )\n\n\nclass PostgresConfigValueToJsonFunction(trampoline.VersionedFunction):\n    \"\"\"Convert a Postgres setting to JSON value.\n\n    Steps:\n\n    * Lookup the `setting_name` in pg_settings to determine its\n      type and unit.\n\n    * Parse `setting_value` to see if it starts with numbers and ends\n      with what looks like a unit.\n\n    * Fetch the unit/multiplier pg_settings (well, from our view over it).\n\n    * If `setting_value` has a unit, pass it to\n      `_interpret_config_value_to_json`\n\n    * If `setting_value` doesn't have a unit, pass it to\n      `_interpret_config_value_to_json` along with the base unit/multiplier\n      from pg_settings.\n\n    * Then, the `_interpret_config_value_to_json` is capable of casting the\n      value correctly based on the pg_settings type and the supplied\n      unit/multiplier.\n    \"\"\"\n\n    text = r\"\"\"\n        SELECT\n            edgedb_VER._postgres_json_config_value_to_fe_config_value(\n                \"setting_name\",\n                backend_json_value.value\n            )\n        FROM\n            LATERAL (\n                SELECT regexp_match(\n                    \"setting_value\", '^(\\d+)\\s*([a-zA-Z]{0,3})$') AS v\n            ) AS _unit,\n\n            LATERAL (\n                SELECT\n                    COALESCE(_unit.v[1], \"setting_value\") AS val,\n                    COALESCE(_unit.v[2], '') AS unit\n            ) AS parsed_value\n        LEFT OUTER JOIN\n            (\n                SELECT\n                    epg_settings.vartype AS vartype,\n                    epg_settings.multiplier AS multiplier,\n                    epg_settings.unit AS unit\n                FROM\n                    edgedb_VER._normalized_pg_settings AS epg_settings\n                WHERE\n                    epg_settings.name = \"setting_name\"\n            ) AS settings_in ON true\n        CROSS JOIN LATERAL\n            (\n                SELECT\n                    COALESCE(settings_in.vartype,\n                             edgedb_VER._type_id_to_config_type(\"setting_typeid\"))\n                    as vartype,\n                    COALESCE(settings_in.multiplier, '1') as multiplier,\n                    COALESCE(settings_in.unit, '') as unit\n            ) AS settings\n        CROSS JOIN LATERAL\n            (SELECT\n                (CASE\n                    WHEN parsed_value.unit != ''\n                    THEN\n                        edgedb_VER._interpret_config_value_to_json(\n                            parsed_value.val,\n                            settings.vartype,\n                            1,\n                            parsed_value.unit\n                        )\n\n                    ELSE\n                        edgedb_VER._interpret_config_value_to_json(\n                            \"setting_value\",\n                            settings.vartype,\n                            settings.multiplier,\n                            settings.unit\n                        )\n\n                END) AS value\n            ) AS backend_json_value\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_postgres_config_value_to_json'),\n            args=[\n                ('setting_name', ('text',)),\n                ('setting_typeid', ('uuid',)),\n                ('setting_value', ('text',)),\n            ],\n            returns=('jsonb',),\n            volatility='volatile',\n            text=self.text,\n        )\n\n\nclass SysConfigFullFunction(trampoline.VersionedFunction):\n\n    # This is a function because \"_edgecon_state\" is a temporary table\n    # and therefore cannot be used in a view.\n\n    text = f'''\n    DECLARE\n        query text;\n    BEGIN\n\n    query := $$\n        WITH\n\n        config_spec AS (\n            SELECT\n                s.key AS name,\n                s.value->'default' AS default,\n                (s.value->>'internal')::bool AS internal,\n                (s.value->>'system')::bool AS system,\n                (s.value->>'typeid')::uuid AS typeid,\n                (s.value->>'typemod') AS typemod,\n                (s.value->>'backend_setting') AS backend_setting\n            FROM\n                edgedbinstdata_VER.instdata as id,\n            LATERAL jsonb_each(id.json) AS s\n            WHERE id.key LIKE 'configspec%'\n        ),\n\n        config_defaults AS (\n            SELECT\n                s.name AS name,\n                s.default AS value,\n                'default' AS source,\n                s.backend_setting IS NOT NULL AS is_backend\n            FROM\n                config_spec s\n        ),\n        config_extension_defaults AS (\n            SELECT * FROM config_defaults WHERE name like '%::%'\n        ),\n\n        config_static AS (\n            SELECT\n                s.name AS name,\n                s.value AS value,\n                (CASE\n                    WHEN s.type = 'A' THEN 'command line'\n                    -- Due to inplace upgrade limits, without adding a new\n                    -- layer, configuration file values are manually squashed\n                    -- into the `environment variables` layer, see below.\n                    ELSE 'environment variable'\n                END) AS source,\n                config_spec.backend_setting IS NOT NULL AS is_backend\n            FROM\n                _edgecon_state s\n                INNER JOIN config_spec ON (config_spec.name = s.name)\n            WHERE\n                -- Give precedence to configuration file values over\n                -- environment variables manually.\n                s.type = 'A' OR s.type = 'F' OR (\n                    s.type = 'E' AND NOT EXISTS (\n                        SELECT 1 FROM _edgecon_state ss\n                        WHERE ss.name = s.name AND ss.type = 'F'\n                    )\n                )\n        ),\n\n        config_sys AS (\n            SELECT\n                s.key AS name,\n                s.value AS value,\n                'system override' AS source,\n                config_spec.backend_setting IS NOT NULL AS is_backend\n            FROM\n                jsonb_each(\n                    edgedb_VER.get_database_metadata(\n                        {ql(defines.EDGEDB_SYSTEM_DB)}\n                    ) -> 'sysconfig'\n                ) AS s\n                INNER JOIN config_spec ON (config_spec.name = s.key)\n        ),\n\n        config_db AS (\n            SELECT\n                s.name AS name,\n                s.value AS value,\n                'database' AS source,\n                config_spec.backend_setting IS NOT NULL AS is_backend\n            FROM\n                edgedb._db_config s\n                INNER JOIN config_spec ON (config_spec.name = s.name)\n        ),\n\n        config_sess AS (\n            SELECT\n                s.name AS name,\n                s.value AS value,\n                'session' AS source,\n                FALSE AS is_backend  -- only 'B' is for backend settings\n            FROM\n                _edgecon_state s\n            WHERE\n                s.type = 'C'\n        ),\n\n        pg_db_setting AS (\n            SELECT\n                spec.name,\n                edgedb_VER._postgres_config_value_to_json(\n                    spec.backend_setting, spec.typeid, nameval.value\n                ) AS value,\n                'database' AS source,\n                TRUE AS is_backend\n            FROM\n                (SELECT\n                    setconfig\n                FROM\n                    pg_db_role_setting\n                WHERE\n                    setdatabase = (\n                        SELECT oid\n                        FROM pg_database\n                        WHERE datname = current_database()\n                    )\n                    AND setrole = 0\n                ) AS cfg_array,\n                LATERAL unnest(cfg_array.setconfig) AS cfg_set(s),\n                LATERAL (\n                    SELECT\n                        split_part(cfg_set.s, '=', 1) AS name,\n                        split_part(cfg_set.s, '=', 2) AS value\n                ) AS nameval,\n                LATERAL (\n                    SELECT\n                        config_spec.name,\n                        config_spec.backend_setting,\n                        config_spec.typeid\n                    FROM\n                        config_spec\n                    WHERE\n                        nameval.name = config_spec.backend_setting\n                ) AS spec\n        ),\n    $$;\n\n    IF fs_access THEN\n        query := query || $$\n            pg_conf_settings AS (\n                SELECT\n                    spec.name,\n                    edgedb_VER._postgres_config_value_to_json(\n                        spec.backend_setting, spec.typeid, setting\n                    ) AS value,\n                    'postgres configuration file' AS source,\n                    TRUE AS is_backend\n                FROM\n                    pg_file_settings,\n                    LATERAL (\n                        SELECT\n                            config_spec.name,\n                            config_spec.backend_setting,\n                            config_spec.typeid\n                        FROM\n                            config_spec\n                        WHERE\n                            pg_file_settings.name = config_spec.backend_setting\n                    ) AS spec\n                WHERE\n                    sourcefile != ((\n                        SELECT setting\n                        FROM pg_settings WHERE name = 'data_directory'\n                    ) || '/postgresql.auto.conf')\n                    AND applied\n            ),\n\n            pg_auto_conf_settings AS (\n                SELECT\n                    spec.name,\n                    edgedb_VER._postgres_config_value_to_json(\n                        spec.backend_setting, spec.typeid, setting\n                    ) AS value,\n                    'system override' AS source,\n                    TRUE AS is_backend\n                FROM\n                    pg_file_settings,\n                    LATERAL (\n                        SELECT\n                            config_spec.name,\n                            config_spec.backend_setting,\n                            config_spec.typeid\n                        FROM\n                            config_spec\n                        WHERE\n                            pg_file_settings.name = config_spec.backend_setting\n                    ) AS spec\n                WHERE\n                    sourcefile = ((\n                        SELECT setting\n                        FROM pg_settings WHERE name = 'data_directory'\n                    ) || '/postgresql.auto.conf')\n                    AND applied\n            ),\n        $$;\n    END IF;\n\n    query := query || $$\n        pg_config AS (\n            SELECT\n                spec.name,\n                edgedb_VER._postgres_json_config_value_to_fe_config_value(\n                    settings.name,\n                    edgedb_VER._interpret_config_value_to_json(\n                        settings.setting,\n                        settings.vartype,\n                        settings.multiplier,\n                        settings.unit\n                    )\n                ) AS value,\n                source AS source,\n                TRUE AS is_backend\n            FROM\n                (\n                    SELECT\n                        epg_settings.name AS name,\n                        epg_settings.unit AS unit,\n                        epg_settings.multiplier AS multiplier,\n                        epg_settings.vartype AS vartype,\n                        epg_settings.setting AS setting,\n                        (CASE\n                            WHEN epg_settings.source = 'session' THEN\n                                epg_settings.source\n                            ELSE\n                                'postgres ' || epg_settings.source\n                        END) AS source\n                    FROM\n                        edgedb_VER._normalized_pg_settings AS epg_settings\n                    WHERE\n                        epg_settings.source != 'database'\n                ) AS settings,\n\n                LATERAL (\n                    SELECT\n                        config_spec.name\n                    FROM\n                        config_spec\n                    WHERE\n                        settings.name = config_spec.backend_setting\n                ) AS spec\n            ),\n\n        -- extension session configs don't show up in any system view, so we\n        -- check _edgecon_state to see when they are present.\n        pg_extension_config AS (\n            SELECT\n                config_spec.name,\n                -- XXX: Or would it be better to just use the json directly?\n                edgedb_VER._postgres_config_value_to_json(\n                    config_spec.backend_setting,\n                    config_spec.typeid,\n                    current_setting(config_spec.backend_setting, true)\n                ) AS value,\n                'session' AS source,\n                TRUE AS is_backend\n            FROM _edgecon_state s\n            INNER JOIN config_spec\n            ON s.name = config_spec.name\n            WHERE s.type = 'B' AND s.name LIKE '%::%'\n        ),\n\n        edge_all_settings AS MATERIALIZED (\n            SELECT\n                q.*\n            FROM\n                (\n                    SELECT * FROM config_defaults UNION ALL\n                    SELECT * FROM config_static UNION ALL\n                    SELECT * FROM config_sys UNION ALL\n                    SELECT * FROM config_db UNION ALL\n                    SELECT * FROM config_sess\n                ) AS q\n            WHERE\n                NOT q.is_backend\n        ),\n\n    $$;\n\n    IF fs_access THEN\n        query := query || $$\n            pg_all_settings AS MATERIALIZED (\n                SELECT\n                    q.*\n                FROM\n                    (\n                        -- extension defaults aren't in any system views\n                        SELECT * FROM config_extension_defaults UNION ALL\n                        SELECT * FROM pg_db_setting UNION ALL\n                        SELECT * FROM pg_conf_settings UNION ALL\n                        SELECT * FROM pg_auto_conf_settings UNION ALL\n                        SELECT * FROM pg_config UNION ALL\n                        SELECT * FROM pg_extension_config\n                    ) AS q\n                WHERE\n                    q.is_backend\n            )\n        $$;\n    ELSE\n        query := query || $$\n            pg_all_settings AS MATERIALIZED (\n                SELECT\n                    q.*\n                FROM\n                    (\n                        -- extension defaults aren't in any system views\n                        SELECT * FROM config_extension_defaults UNION ALL\n                        -- config_sys is here, because there\n                        -- is no other way to read instance-level\n                        -- configuration overrides.\n                        SELECT * FROM config_sys UNION ALL\n                        SELECT * FROM pg_db_setting UNION ALL\n                        SELECT * FROM pg_config UNION ALL\n                        SELECT * FROM pg_extension_config\n                    ) AS q\n                WHERE\n                    q.is_backend\n            )\n        $$;\n    END IF;\n\n    query := query || $$\n        SELECT\n            max_source AS max_source,\n            (q.name,\n            q.value,\n            q.source,\n            (CASE\n                WHEN q.source < 'database'::edgedb._sys_config_source_t THEN\n                    'INSTANCE'\n                WHEN q.source = 'database'::edgedb._sys_config_source_t THEN\n                    'DATABASE'\n                ELSE\n                    'SESSION'\n            END)::edgedb._sys_config_scope_t\n            )::edgedb._sys_config_val_t as value\n        FROM\n            unnest($2) as max_source,\n            LATERAL (SELECT\n                u.name,\n                u.value,\n                u.source::edgedb._sys_config_source_t,\n                row_number() OVER (\n                    PARTITION BY u.name\n                    ORDER BY u.source::edgedb._sys_config_source_t DESC\n                ) AS n\n            FROM\n                (SELECT\n                    *\n                FROM\n                    (\n                        SELECT * FROM edge_all_settings UNION ALL\n                        SELECT * FROM pg_all_settings\n                    ) AS q\n                WHERE\n                    q.value IS NOT NULL\n                    AND ($1 IS NULL OR\n                        q.source::edgedb._sys_config_source_t = any($1)\n                    )\n                    AND (max_source IS NULL OR\n                        q.source::edgedb._sys_config_source_t <= max_source\n                    )\n                ) AS u\n            ) AS q\n        WHERE\n            q.n = 1;\n    $$;\n\n    RETURN QUERY EXECUTE query USING source_filter, max_sources;\n    END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_read_sys_config_full'),\n            args=[\n                (\n                    'source_filter',\n                    ('edgedb', '_sys_config_source_t[]',),\n                    'NULL',\n                ),\n                (\n                    'max_sources',\n                    ('edgedb', '_sys_config_source_t[]'),\n                    'NULL',\n                ),\n                (\n                    'fs_access',\n                    ('bool',),\n                    'TRUE',\n                )\n            ],\n            returns=('edgedb', '_sys_config_entry_t'),\n            set_returning=True,\n            language='plpgsql',\n            volatility='volatile',\n            text=self.text,\n        )\n\n\nclass SysConfigUncachedFunction(trampoline.VersionedFunction):\n\n    text = f'''\n    DECLARE\n        backend_caps bigint;\n    BEGIN\n\n    backend_caps := edgedb_VER.get_backend_capabilities();\n    IF (backend_caps\n        & {int(params.BackendCapabilities.CONFIGFILE_ACCESS)}) != 0\n    THEN\n        RETURN QUERY\n        SELECT *\n        FROM edgedb_VER._read_sys_config_full(\n            source_filter, max_sources, TRUE);\n    ELSE\n        RETURN QUERY\n        SELECT *\n        FROM edgedb_VER._read_sys_config_full(\n            source_filter, max_sources, FALSE);\n    END IF;\n\n    END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_read_sys_config_uncached'),\n            args=[\n                (\n                    'source_filter',\n                    ('edgedb', '_sys_config_source_t[]',),\n                    'NULL',\n                ),\n                (\n                    'max_sources',\n                    ('edgedb', '_sys_config_source_t[]'),\n                    'NULL',\n                ),\n            ],\n            returns=('edgedb', '_sys_config_entry_t'),\n            set_returning=True,\n            language='plpgsql',\n            volatility='volatile',\n            text=self.text,\n        )\n\n\nclass SysConfigFunction(trampoline.VersionedFunction):\n\n    text = f'''\n    DECLARE\n    BEGIN\n\n    -- Only bother caching the source_filter IS NULL case, since that\n    -- is what drives the config views. source_filter is used in\n    -- DESCRIBE CONFIG\n    IF source_filter IS NOT NULL OR array_position(\n     ARRAY[NULL, 'database', 'system override']::edgedb._sys_config_source_t[],\n      max_source) IS NULL\n     THEN\n        RETURN QUERY\n        SELECT\n          (c.value).name, (c.value).value, (c.value).source, (c.value).scope\n        FROM edgedb_VER._read_sys_config_uncached(\n          source_filter, ARRAY[max_source]) AS c;\n        RETURN;\n    END IF;\n\n    IF count(*) = 0 FROM \"_config_cache\" c\n       WHERE source IS NOT DISTINCT FROM max_source\n    THEN\n        INSERT INTO \"_config_cache\"\n        SELECT (s.max_source), (s.value)\n        FROM edgedb_VER._read_sys_config_uncached(\n          source_filter, ARRAY[\n            NULL, 'database', 'system override']::edgedb._sys_config_source_t[])\n             AS s;\n    END IF;\n\n    RETURN QUERY\n    SELECT (c.value).name, (c.value).value, (c.value).source, (c.value).scope\n    FROM \"_config_cache\" c WHERE source IS NOT DISTINCT FROM max_source;\n\n    END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_read_sys_config'),\n            args=[\n                (\n                    'source_filter',\n                    ('edgedb', '_sys_config_source_t[]',),\n                    'NULL',\n                ),\n                (\n                    'max_source',\n                    ('edgedb', '_sys_config_source_t'),\n                    'NULL',\n                ),\n            ],\n            returns=('edgedb', '_sys_config_val_t'),\n            set_returning=True,\n            language='plpgsql',\n            volatility='volatile',\n            text=self.text,\n        )\n\n\nclass SysClearConfigCacheFunction(trampoline.VersionedFunction):\n\n    text = f'''\n    DECLARE\n    BEGIN\n\n    DELETE FROM \"_config_cache\" c;\n    RETURN true;\n\n    END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_clear_sys_config_cache'),\n            args=[],\n            returns=(\"boolean\"),\n            set_returning=False,\n            language='plpgsql',\n            volatility='volatile',\n            text=self.text,\n        )\n\n\nclass ResetSessionConfigFunction(trampoline.VersionedFunction):\n\n    text = f'''\n        RESET ALL\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_reset_session_config'),\n            args=[],\n            returns=('void',),\n            language='sql',\n            volatility='volatile',\n            text=self.text,\n        )\n\n\nclass ApplySessionConfigFunction(trampoline.VersionedFunction):\n    \"\"\"Apply a Gel config setting to the backend, if possible.\n\n    The function accepts any Gel config name/value pair. If this\n    specific config setting happens to be implemented via a backend\n    setting, it would be applied to the current PostgreSQL session.\n    If the config setting doesn't reflect into a backend setting the\n    function is a no-op.\n\n    The function always returns the passed config name, unmodified\n    (this simplifies using the function in queries.)\n    \"\"\"\n\n    def __init__(self, config_spec: edbconfig.Spec) -> None:\n\n        backend_settings = {}\n        for setting_name in config_spec:\n            setting = config_spec[setting_name]\n\n            if setting.backend_setting and not setting.system:\n                backend_settings[setting_name] = setting.backend_setting\n\n        variants_list = []\n        for setting_name, backend_setting_name in backend_settings.items():\n            setting = config_spec[setting_name]\n\n            valql = '\"value\"->>0'\n            if (\n                isinstance(setting.type, type)\n                and issubclass(setting.type, statypes.ScalarType)\n            ):\n                valql = setting.type.to_backend_expr(valql)\n\n            variants_list.append(f'''\n                WHEN \"name\" = {ql(setting_name)}\n                THEN\n                    pg_catalog.set_config(\n                        {ql(backend_setting_name)}::text,\n                        {valql},\n                        false\n                    )\n            ''')\n\n        ext_config = '''\n            SELECT pg_catalog.set_config(\n                (s.val->>'backend_setting')::text,\n                \"value\"->>0,\n                false\n            )\n            FROM\n                edgedbinstdata_VER.instdata as id,\n            LATERAL jsonb_each(id.json) AS s(key, val)\n            WHERE id.key = 'configspec_ext' AND s.key = \"name\"\n        '''\n\n        variants = \"\\n\".join(variants_list)\n        text = f'''\n        SELECT (\n            CASE\n                WHEN \"name\" = any(\n                    ARRAY[{\",\".join(ql(str(bs)) for bs in backend_settings)}]\n                )\n                THEN (\n                    CASE\n                        WHEN\n                            (CASE\n                                {variants}\n                            END) IS NULL\n                        THEN \"name\"\n                        ELSE \"name\"\n                    END\n                )\n\n                WHEN \"name\" LIKE '%::%'\n                THEN\n                    CASE WHEN ({ext_config}) IS NULL\n                    THEN \"name\"\n                    ELSE \"name\"\n                END\n\n                ELSE \"name\"\n            END\n        )\n        '''\n\n        super().__init__(\n            name=('edgedb', '_apply_session_config'),\n            args=[\n                ('name', ('text',)),\n                ('value', ('jsonb',)),\n            ],\n            returns=('text',),\n            language='sql',\n            volatility='volatile',\n            text=text,\n        )\n\n\nclass SysGetTransactionIsolation(trampoline.VersionedFunction):\n    \"Get transaction isolation value as text compatible with Gel's enum.\"\n    text = r'''\n        SELECT\n            CASE setting\n                WHEN 'repeatable read' THEN 'RepeatableRead'\n                WHEN 'serializable' THEN 'Serializable'\n                ELSE (\n                    SELECT edgedb_VER.raise(\n                        NULL::text,\n                        msg => (\n                            'unknown transaction isolation level \"'\n                            || setting || '\"'\n                        )\n                    )\n                )\n            END\n        FROM pg_settings\n        WHERE name = 'transaction_isolation'\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_get_transaction_isolation'),\n            args=[],\n            returns=('text',),\n            # This function only reads from a table.\n            volatility='stable',\n            text=self.text)\n\n\nclass GetCachedReflection(trampoline.VersionedFunction):\n    \"Return a list of existing schema reflection helpers.\"\n    text = '''\n        SELECT\n            substring(proname, '__rh_#\"%#\"', '#') AS eql_hash,\n            proargnames AS argnames\n        FROM\n            pg_proc\n            INNER JOIN pg_namespace ON (pronamespace = pg_namespace.oid)\n        WHERE\n            proname LIKE '\\\\_\\\\_rh\\\\_%'\n            AND nspname = 'edgedb_VER'\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_get_cached_reflection'),\n            args=[],\n            returns=('record',),\n            set_returning=True,\n            # This function only reads from a table.\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass GetBaseScalarTypeMap(trampoline.VersionedFunction):\n    \"\"\"Return a map of base Gel scalar type ids to Postgres type names.\"\"\"\n\n    text = \"VALUES\" + \", \".join(\n        f\"({ql(str(k))}::uuid, {qtl(v)})\"\n        for k, v in types.base_type_name_map.items()\n    )\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_get_base_scalar_type_map'),\n            args=[],\n            returns=('record',),\n            set_returning=True,\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass GetTypeToRangeNameMap(trampoline.VersionedFunction):\n    \"\"\"Return a map of type names to the name of the associated range type\"\"\"\n\n    text = f\"VALUES\" + \", \".join(\n        f\"({qtl(k)}, {qtl(v)})\"\n        for k, v in types.type_to_range_name_map.items()\n    )\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_get_type_to_range_type_map'),\n            args=[],\n            returns=('record',),\n            set_returning=True,\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass GetTypeToMultiRangeNameMap(trampoline.VersionedFunction):\n    \"Return a map of type names to the name of the associated multirange type\"\n\n    text = f\"VALUES\" + \", \".join(\n        f\"({qtl(k)}, {qtl(v)})\"\n        for k, v in types.type_to_multirange_name_map.items()\n    )\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_get_type_to_multirange_type_map'),\n            args=[],\n            returns=('record',),\n            set_returning=True,\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass GetPgTypeForEdgeDBTypeFunction(trampoline.VersionedFunction):\n    \"\"\"Return Postgres OID representing a given Gel type.\"\"\"\n\n    text = f'''\n        SELECT\n            coalesce(\n                sql_type::regtype::oid,\n                (\n                    SELECT\n                        tn::regtype::oid\n                    FROM\n                        edgedb_VER._get_base_scalar_type_map()\n                            AS m(tid uuid, tn text)\n                    WHERE\n                        m.tid = \"typeid\"\n                ),\n                (\n                    SELECT\n                        typ.oid\n                    FROM\n                        pg_catalog.pg_type typ\n                    WHERE\n                        typ.typname = \"typeid\"::text || '_domain'\n                        OR typ.typname = \"typeid\"::text || '_t'\n                ),\n                (\n                    SELECT\n                        typ.typarray\n                    FROM\n                        pg_catalog.pg_type typ\n                    WHERE\n                        \"kind\" = 'schema::Array'\n                         AND (\n                            typ.typname = \"elemid\"::text || '_domain'\n                            OR typ.typname = \"elemid\"::text || '_t'\n                            OR typ.oid = (\n                                SELECT\n                                    tn::regtype::oid\n                                FROM\n                                    edgedb_VER._get_base_scalar_type_map()\n                                        AS m(tid uuid, tn text)\n                                WHERE\n                                    tid = \"elemid\"\n                            )\n                        )\n                ),\n                (\n                    SELECT\n                        rng.rngtypid\n                    FROM\n                        pg_catalog.pg_range rng\n                    WHERE\n                        \"kind\" = 'schema::Range'\n                        -- For ranges, we need to do the lookup based on\n                        -- our internal map of elem names to range names,\n                        -- because we use the builtin daterange as the range\n                        -- for edgedbt.date_t.\n                        AND rng.rngtypid = (\n                            SELECT\n                                rn::regtype::oid\n                            FROM\n                                edgedb_VER._get_base_scalar_type_map()\n                                    AS m(tid uuid, tn text)\n                            INNER JOIN\n                                edgedb_VER._get_type_to_range_type_map()\n                                    AS m2(tn2 text, rn text)\n                                ON tn = tn2\n                            WHERE\n                                tid = \"elemid\"\n                        )\n                ),\n                (\n                    SELECT\n                        rng.rngmultitypid\n                    FROM\n                        pg_catalog.pg_range rng\n                    WHERE\n                        \"kind\" = 'schema::MultiRange'\n                        -- For multiranges, we need to do the lookup based on\n                        -- our internal map of elem names to range names,\n                        -- because we use the builtin daterange as the range\n                        -- for edgedbt.date_t.\n                        AND rng.rngmultitypid = (\n                            SELECT\n                                rn::regtype::oid\n                            FROM\n                                edgedb_VER._get_base_scalar_type_map()\n                                    AS m(tid uuid, tn text)\n                            INNER JOIN\n                                edgedb_VER._get_type_to_multirange_type_map()\n                                    AS m2(tn2 text, rn text)\n                                ON tn = tn2\n                            WHERE\n                                tid = \"elemid\"\n                        )\n                ),\n                edgedb_VER.raise(\n                    NULL::bigint,\n                    'invalid_parameter_value',\n                    msg => (\n                        format(\n                            'cannot determine OID of Gel type %L',\n                            \"typeid\"::text\n                        )\n                    )\n                )\n            )::bigint\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'get_pg_type_for_edgedb_type'),\n            args=[\n                ('typeid', ('uuid',)),\n                ('kind', ('text',)),\n                ('elemid', ('uuid',)),\n                ('sql_type', ('text',)),\n            ],\n            returns=('bigint',),\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass GetPgTypeForEdgeDBTypeFunction2(trampoline.VersionedFunction):\n    \"\"\"Return Postgres OID representing a given Gel type.\n\n    This is an updated version that should replace the original. It takes\n    advantage of the schema views to correctly identify non-trivial array\n    types.\n    \"\"\"\n\n    text = f'''\n        SELECT\n            coalesce(\n                sql_type::regtype::oid,\n                (\n                    SELECT\n                        tn::regtype::oid\n                    FROM\n                        edgedb_VER._get_base_scalar_type_map()\n                            AS m(tid uuid, tn text)\n                    WHERE\n                        m.tid = \"typeid\"\n                ),\n                (\n                    SELECT\n                        typ.oid\n                    FROM\n                        pg_catalog.pg_type typ\n                    WHERE\n                        typ.typname = \"typeid\"::text || '_domain'\n                        OR typ.typname = \"typeid\"::text || '_t'\n                ),\n                (\n                    SELECT\n                        typ.typarray\n                    FROM\n                        pg_catalog.pg_type typ\n                    WHERE\n                        \"kind\" = 'schema::Array'\n                         AND (\n                            typ.typname = \"elemid\"::text || '_domain'\n                            OR typ.typname = \"elemid\"::text || '_t'\n                            OR typ.oid = (\n                                SELECT\n                                    tn::regtype::oid\n                                FROM\n                                    edgedb_VER._get_base_scalar_type_map()\n                                        AS m(tid uuid, tn text)\n                                WHERE\n                                    tid = \"elemid\"\n                            )\n                        )\n                ),\n                (\n                    SELECT\n                        typ.typarray\n                    FROM\n                        pg_catalog.pg_type typ\n                    WHERE\n                        \"kind\" = 'schema::Array'\n                         AND (\n                            typ.typname = \"elemid\"::text || '_domain'\n                            OR typ.typname = \"elemid\"::text\n                            OR typ.oid = (\n                                SELECT\n                                    st.backend_id\n                                FROM\n                                    edgedb_VER.\"_SchemaType\" AS st\n                                WHERE\n                                    st.id = \"elemid\"\n                            )\n                        )\n                ),\n                (\n                    SELECT\n                        rng.rngtypid\n                    FROM\n                        pg_catalog.pg_range rng\n                    WHERE\n                        \"kind\" = 'schema::Range'\n                        -- For ranges, we need to do the lookup based on\n                        -- our internal map of elem names to range names,\n                        -- because we use the builtin daterange as the range\n                        -- for edgedbt.date_t.\n                        AND rng.rngtypid = (\n                            SELECT\n                                rn::regtype::oid\n                            FROM\n                                edgedb_VER._get_base_scalar_type_map()\n                                    AS m(tid uuid, tn text)\n                            INNER JOIN\n                                edgedb_VER._get_type_to_range_type_map()\n                                    AS m2(tn2 text, rn text)\n                                ON tn = tn2\n                            WHERE\n                                tid = \"elemid\"\n                        )\n                ),\n                (\n                    SELECT\n                        rng.rngmultitypid\n                    FROM\n                        pg_catalog.pg_range rng\n                    WHERE\n                        \"kind\" = 'schema::MultiRange'\n                        -- For multiranges, we need to do the lookup based on\n                        -- our internal map of elem names to range names,\n                        -- because we use the builtin daterange as the range\n                        -- for edgedbt.date_t.\n                        AND rng.rngmultitypid = (\n                            SELECT\n                                rn::regtype::oid\n                            FROM\n                                edgedb_VER._get_base_scalar_type_map()\n                                    AS m(tid uuid, tn text)\n                            INNER JOIN\n                                edgedb_VER._get_type_to_multirange_type_map()\n                                    AS m2(tn2 text, rn text)\n                                ON tn = tn2\n                            WHERE\n                                tid = \"elemid\"\n                        )\n                ),\n                edgedb_VER.raise(\n                    NULL::bigint,\n                    'invalid_parameter_value',\n                    msg => (\n                        format(\n                            'cannot determine Postgres OID of Gel %s(%L)%s',\n                            \"kind\",\n                            \"typeid\"::text,\n                            (case when \"elemid\" is not null\n                             then ' with element type ' || \"elemid\"::text\n                             else ''\n                             end)\n                        )\n                    )\n                )\n            )::bigint\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'get_pg_type_for_edgedb_type'),\n            args=[\n                ('typeid', ('uuid',)),\n                ('kind', ('text',)),\n                ('elemid', ('uuid',)),\n                ('sql_type', ('text',)),\n            ],\n            returns=('bigint',),\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass FTSParseQueryFunction(trampoline.VersionedFunction):\n    \"\"\"Return tsquery representing the given FTS input query.\"\"\"\n\n    text = r'''\n    DECLARE\n        parts text[];\n        exclude text;\n        term text;\n        rest text;\n        cur_op text := NULL;\n        default_op text;\n        tsq tsquery;\n        el tsquery;\n        result tsquery := ''::tsquery;\n\n    BEGIN\n        IF q IS NULL OR q = '' THEN\n            RETURN result;\n        END IF;\n\n        -- Break up the query string into the current term, optional next\n        -- operator and the rest.\n        parts := regexp_match(\n            q, $$^(-)?((?:\"[^\"]*\")|(?:\\S+))\\s*(OR|AND)?\\s*(.*)$$\n        );\n        exclude := parts[1];\n        term := parts[2];\n        cur_op := parts[3];\n        rest := parts[4];\n\n        IF starts_with(term, '\"') THEN\n            -- match as a phrase\n            tsq := phraseto_tsquery(language, trim(both '\"' from term));\n        ELSE\n            tsq := to_tsquery(language, term);\n        END IF;\n\n        IF exclude IS NOT NULL THEN\n            tsq := !!tsq;\n        END IF;\n\n        -- figure out the operator between the current term and the next one\n        IF rest = '' THEN\n            -- base case, one one term left, so we ignore the cur_op even if\n            -- present\n            IF prev_op = 'OR' THEN\n                -- explicit 'OR' terms are \"should\"\n                should := array_append(should, tsq);\n            ELSIF starts_with(term, '\"')\n               OR exclude IS NOT NULL\n               OR prev_op = 'AND' THEN\n                -- phrases, exclusions and 'AND' terms are \"must\"\n                must := array_append(must, tsq);\n            ELSE\n                -- regular terms are \"should\" by default\n                should := array_append(should, tsq);\n            END IF;\n        ELSE\n            -- recursion\n\n            IF prev_op = 'OR' OR cur_op = 'OR' THEN\n                -- if at least one of the suprrounding operators is 'OR',\n                -- then the phrase is put into \"should\" category\n                should := array_append(should, tsq);\n            ELSIF prev_op = 'AND' OR cur_op = 'AND' THEN\n                -- if at least one of the suprrounding operators is 'AND',\n                -- then the phrase is put into \"must\" category\n                must := array_append(must, tsq);\n            ELSIF starts_with(term, '\"') OR exclude IS NOT NULL THEN\n                -- phrases and exclusions are \"must\"\n                must := array_append(must, tsq);\n            ELSE\n                -- regular terms are \"should\" by default\n                should := array_append(should, tsq);\n            END IF;\n\n            RETURN edgedb_VER.fts_parse_query(\n                rest, language, must, should, cur_op);\n        END IF;\n\n        FOREACH el IN ARRAY should\n        LOOP\n            result := result || el;\n        END LOOP;\n\n        FOREACH el IN ARRAY must\n        LOOP\n            result := result && el;\n        END LOOP;\n\n        RETURN result;\n\n    END;\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'fts_parse_query'),\n            args=[\n                ('q', ('text',)),\n                ('language', ('regconfig',), \"'english'\"),\n                ('must', ('tsquery[]',), 'array[]::tsquery[]'),\n                ('should', ('tsquery[]',), 'array[]::tsquery[]'),\n                ('prev_op', ('text',), 'NULL'),\n            ],\n            returns=('tsquery',),\n            volatility='immutable',\n            language='plpgsql',\n            text=self.text,\n        )\n\n\nclass FTSNormalizeWeightFunction(trampoline.VersionedFunction):\n    \"\"\"Normalize an array of weights to be a 4-value weight array.\"\"\"\n\n    text = r'''\n    SELECT\n        CASE COALESCE(array_length(weights, 1), 0)\n            WHEN 0 THEN array[1, 1, 1, 1]::float4[]\n            WHEN 1 THEN array[0, 0, 0, weights[1]]::float4[]\n            WHEN 2 THEN array[0, 0, weights[2], weights[1]]::float4[]\n            WHEN 3 THEN array[0, weights[3], weights[2], weights[1]]::float4[]\n            ELSE (\n                WITH raw as (\n                    SELECT w\n                    FROM UNNEST(weights) AS w\n                    ORDER BY w DESC\n                )\n                SELECT array_prepend(rest.w, first.arrw)::float4[]\n                FROM\n                (\n                    SELECT array_agg(rw1.w) as arrw\n                    FROM (\n                        SELECT w\n                        FROM (SELECT w FROM raw LIMIT 3) as rw0\n                        ORDER BY w ASC\n                    ) as rw1\n                ) AS first,\n                (\n                    SELECT avg(rw2.w) as w\n                    FROM (SELECT w FROM raw OFFSET 3) as rw2\n                ) AS rest\n            )\n        END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'fts_normalize_weights'),\n            args=[\n                ('weights', ('float8[]',)),\n            ],\n            returns=('float4[]',),\n            volatility='immutable',\n            text=self.text,\n        )\n\n\nclass FTSNormalizeDocFunction(trampoline.VersionedFunction):\n    \"\"\"Normalize a document based on an array of weights.\"\"\"\n\n    text = r'''\n    SELECT\n        CASE COALESCE(array_length(doc, 1), 0)\n            WHEN 0 THEN ''::tsvector\n            WHEN 1 THEN setweight(to_tsvector(language, doc[1]), 'A')\n            WHEN 2 THEN (\n                setweight(to_tsvector(language, doc[1]), 'A') ||\n                setweight(to_tsvector(language, doc[2]), 'B')\n            )\n            WHEN 3 THEN (\n                setweight(to_tsvector(language, doc[1]), 'A') ||\n                setweight(to_tsvector(language, doc[2]), 'B') ||\n                setweight(to_tsvector(language, doc[3]), 'C')\n            )\n            ELSE (\n                WITH raw as (\n                    SELECT d.v as t\n                    FROM UNNEST(doc) WITH ORDINALITY AS d(v, n)\n                    LEFT JOIN UNNEST(weights) WITH ORDINALITY AS w(v, n)\n                    ON d.n = w.n\n                    ORDER BY w.v DESC\n                )\n                SELECT\n                    setweight(to_tsvector(language, d.arr[1]), 'A') ||\n                    setweight(to_tsvector(language, d.arr[2]), 'B') ||\n                    setweight(to_tsvector(language, d.arr[3]), 'C') ||\n                    setweight(to_tsvector(language,\n                                          array_to_string(d.arr[4:], ' ')),\n                              'D')\n                FROM\n                (\n                    SELECT array_agg(raw.t) as arr\n                    FROM raw\n                ) AS d\n            )\n        END\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'fts_normalize_doc'),\n            args=[\n                ('doc', ('text[]',)),\n                ('weights', ('float8[]',)),\n                ('language', ('regconfig',)),\n            ],\n            returns=('tsvector',),\n            volatility='stable',\n            text=self.text,\n        )\n\n\nclass FTSToRegconfig(trampoline.VersionedFunction):\n    \"\"\"\n    Converts ISO 639-3 language identifiers into a regconfig.\n    Defaults to english.\n    Identifiers prefixed with 'xxx_' have the prefix stripped and the remainder\n    used as regconfg identifier.\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'fts_to_regconfig'),\n            args=[\n                ('language', ('text',)),\n            ],\n            returns=('regconfig',),\n            volatility='immutable',\n            text='''\n            SELECT CASE\n                WHEN language ILIKE 'xxx_%' THEN SUBSTR(language, 4)\n                ELSE (CASE LOWER(language)\n                    WHEN 'ara' THEN 'arabic'\n                    WHEN 'hye' THEN 'armenian'\n                    WHEN 'eus' THEN 'basque'\n                    WHEN 'cat' THEN 'catalan'\n                    WHEN 'dan' THEN 'danish'\n                    WHEN 'nld' THEN 'dutch'\n                    WHEN 'eng' THEN 'english'\n                    WHEN 'fin' THEN 'finnish'\n                    WHEN 'fra' THEN 'french'\n                    WHEN 'deu' THEN 'german'\n                    WHEN 'ell' THEN 'greek'\n                    WHEN 'hin' THEN 'hindi'\n                    WHEN 'hun' THEN 'hungarian'\n                    WHEN 'ind' THEN 'indonesian'\n                    WHEN 'gle' THEN 'irish'\n                    WHEN 'ita' THEN 'italian'\n                    WHEN 'lit' THEN 'lithuanian'\n                    WHEN 'npi' THEN 'nepali'\n                    WHEN 'nor' THEN 'norwegian'\n                    WHEN 'por' THEN 'portuguese'\n                    WHEN 'ron' THEN 'romanian'\n                    WHEN 'rus' THEN 'russian'\n                    WHEN 'srp' THEN 'serbian'\n                    WHEN 'spa' THEN 'spanish'\n                    WHEN 'swe' THEN 'swedish'\n                    WHEN 'tam' THEN 'tamil'\n                    WHEN 'tur' THEN 'turkish'\n                    WHEN 'yid' THEN 'yiddish'\n                    ELSE 'english' END\n                )\n            END::pg_catalog.regconfig;\n            ''',\n        )\n\n\nclass UuidGenerateV1mcFunction(trampoline.VersionedFunction):\n    def __init__(self, ext_schema: str) -> None:\n        super().__init__(\n            name=('edgedb', 'uuid_generate_v1mc'),\n            args=[],\n            returns=('uuid',),\n            volatility='volatile',\n            language='sql',\n            strict=True,\n            parallel_safe=True,\n            text=f'SELECT \"{ext_schema}\".uuid_generate_v1mc();'\n        )\n\n\nclass UuidGenerateV4Function(trampoline.VersionedFunction):\n    def __init__(self, ext_schema: str) -> None:\n        super().__init__(\n            name=('edgedb', 'uuid_generate_v4'),\n            args=[],\n            returns=('uuid',),\n            volatility='volatile',\n            language='sql',\n            strict=True,\n            parallel_safe=True,\n            text=f'SELECT \"{ext_schema}\".uuid_generate_v4();'\n        )\n\n\nclass UuidGenerateV5Function(trampoline.VersionedFunction):\n    def __init__(self, ext_schema: str) -> None:\n        super().__init__(\n            name=('edgedb', 'uuid_generate_v5'),\n            args=[\n                ('namespace', ('uuid',)),\n                ('name', ('text',)),\n            ],\n            returns=('uuid',),\n            volatility='immutable',\n            language='sql',\n            strict=True,\n            parallel_safe=True,\n            text=f'SELECT \"{ext_schema}\".uuid_generate_v5(namespace, name);'\n        )\n\n\nclass PadBase64StringFunction(trampoline.VersionedFunction):\n    text = r\"\"\"\n        WITH\n            l AS (SELECT pg_catalog.length(\"s\") % 4 AS r),\n            p AS (\n                SELECT\n                    (CASE WHEN l.r > 0 THEN repeat('=', (4 - l.r))\n                    ELSE '' END) AS p\n                FROM\n                    l\n            )\n        SELECT\n            \"s\" || p.p\n        FROM\n            p\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', 'pad_base64_string'),\n            args=[\n                ('s', ('text',)),\n            ],\n            returns=('text',),\n            volatility='immutable',\n            language='sql',\n            strict=True,\n            parallel_safe=True,\n            text=self.text,\n        )\n\n\nclass ResetQueryStatsFunction(trampoline.VersionedFunction):\n    text = r\"\"\"\n    DECLARE\n        tenant_id TEXT;\n        other_tenant_exists BOOLEAN;\n        db_oid OID;\n        queryid bigint;\n    BEGIN\n        tenant_id := edgedb_VER.get_backend_tenant_id();\n        IF id IS NULL THEN\n            queryid := 0;\n        ELSE\n            queryid := edgedbext.edb_stat_queryid(id);\n        END IF;\n\n        SELECT EXISTS (\n            SELECT 1\n            FROM\n                pg_database dat\n                CROSS JOIN LATERAL (\n                    SELECT\n                        edgedb_VER.shobj_metadata(dat.oid, 'pg_database')\n                            AS description\n                ) AS d\n            WHERE\n                (d.description)->>'id' IS NOT NULL\n                AND (d.description)->>'tenant_id' != tenant_id\n        ) INTO other_tenant_exists;\n\n        IF branch_name IS NULL THEN\n            IF other_tenant_exists THEN\n                RETURN edgedbext.edb_stat_statements_reset(\n                    0,  -- userid\n                    ARRAY(\n                        SELECT\n                            dat.oid\n                        FROM\n                            pg_database dat\n                            CROSS JOIN LATERAL (\n                                SELECT\n                                    edgedb_VER.shobj_metadata(dat.oid,\n                                                              'pg_database')\n                                        AS description\n                            ) AS d\n                        WHERE\n                            (d.description)->>'id' IS NOT NULL\n                            AND (d.description)->>'tenant_id' = tenant_id\n                    ),\n                    queryid,\n                    COALESCE(minmax_only, false)\n                );\n            ELSE\n                RETURN edgedbext.edb_stat_statements_reset(\n                    0,  -- userid\n                    '{}',  -- database oid\n                    queryid,\n                    COALESCE(minmax_only, false)\n                );\n            END IF;\n        ELSE\n            SELECT\n                dat.oid INTO db_oid\n            FROM\n                pg_database dat\n                CROSS JOIN LATERAL (\n                    SELECT\n                        edgedb_VER.shobj_metadata(dat.oid, 'pg_database')\n                            AS description\n                ) AS d\n            WHERE\n                (d.description)->>'id' IS NOT NULL\n                AND (d.description)->>'tenant_id' = tenant_id\n                AND edgedb_VER.get_database_frontend_name(dat.datname) =\n                    branch_name;\n\n            IF db_oid IS NULL THEN\n                RETURN NULL::edgedbt.timestamptz_t;\n            END IF;\n\n            RETURN edgedbext.edb_stat_statements_reset(\n                0,  -- userid\n                ARRAY[db_oid],\n                queryid,\n                COALESCE(minmax_only, false)\n            );\n        END IF;\n\n        RETURN now()::edgedbt.timestamptz_t;\n    END;\n    \"\"\"\n\n    noop_text = r\"\"\"\n        BEGIN\n        RETURN NULL::edgedbt.timestamptz_t;\n        END;\n    \"\"\"\n\n    def __init__(self, enable_stats: bool) -> None:\n        super().__init__(\n            name=('edgedb', 'reset_query_stats'),\n            args=[\n                ('branch_name', ('text',)),\n                ('id', ('uuid',)),\n                ('minmax_only', ('bool',)),\n            ],\n            returns=('edgedbt', 'timestamptz_t'),\n            volatility='volatile',\n            language='plpgsql',\n            text=self.text if enable_stats else self.noop_text,\n        )\n\n\n# N.B: This is a VersionedFunction but it can not be trampolined, since\n# the trampoline wrapper can't be a trigger.\nclass ClearFELocalSQLSettingsFunction(trampoline.VersionedFunction):\n    text = r\"\"\"\n    BEGIN\n        DELETE FROM _edgecon_state WHERE type = 'L' AND name = NEW.name;\n        RETURN NEW;\n    END;\n    \"\"\"\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_clear_fe_local_sql_settings'),\n            args=[],\n            returns='trigger',\n            language='plpgsql',\n            volatility='volatile',\n            text=self.text,\n        )\n\n\ndef _maybe_trampoline(\n    cmd: dbops.Command, out: list[trampoline.Trampoline]\n) -> None:\n    namespace = V('')\n    if (\n        isinstance(cmd, dbops.CreateFunction)\n        and cmd.function.name[0].endswith(namespace)\n    ):\n        out.append(trampoline.make_trampoline(cmd.function))\n    elif (\n        isinstance(cmd, dbops.CreateView)\n        and cmd.view.name[0].endswith(namespace)\n    ):\n        out.append(trampoline.make_view_trampoline(cmd.view))\n    elif (\n        isinstance(cmd, dbops.CreateTable)\n        and cmd.table.name[0].endswith(namespace)\n    ):\n        f, n = cmd.table.name\n        out.append(trampoline.make_table_trampoline((f, n)))\n\n\ndef trampoline_functions(\n    cmds: Sequence[dbops.Command]\n) -> list[trampoline.Trampoline]:\n    ncmds: list[trampoline.Trampoline] = []\n    for cmd in cmds:\n        _maybe_trampoline(cmd, ncmds)\n    return ncmds\n\n\ndef trampoline_command(cmd: dbops.Command) -> list[trampoline.Trampoline]:\n    ncmds: list[trampoline.Trampoline] = []\n\n    def go(cmd: dbops.Command) -> None:\n        if isinstance(cmd, dbops.CommandGroup):\n            for subcmd in cmd.commands:\n                go(subcmd)\n        else:\n            _maybe_trampoline(cmd, ncmds)\n\n    go(cmd)\n\n    return ncmds\n\n\ndef get_fixed_bootstrap_commands() -> dbops.CommandGroup:\n    \"\"\"Create metaschema objects that are truly global\"\"\"\n\n    cmds = [\n        dbops.CreateSchema(name='edgedb'),\n        dbops.CreateSchema(name='edgedbt'),\n        dbops.CreateSchema(name='edgedbpub'),\n        dbops.CreateSchema(name='edgedbstd'),\n        dbops.CreateSchema(name='edgedbinstdata'),\n\n        dbops.CreateTable(\n            DBConfigTable(),\n        ),\n        # TODO: SHOULD THIS BE VERSIONED?\n        dbops.CreateTable(QueryCacheTable()),\n\n        dbops.CreateDomain(BigintDomain()),\n        dbops.CreateDomain(ConfigMemoryDomain()),\n        dbops.CreateDomain(TimestampTzDomain()),\n        dbops.CreateDomain(TimestampDomain()),\n        dbops.CreateDomain(DateDomain()),\n        dbops.CreateDomain(DurationDomain()),\n        dbops.CreateDomain(RelativeDurationDomain()),\n        dbops.CreateDomain(DateDurationDomain()),\n\n        dbops.CreateEnum(SysConfigSourceType()),\n        dbops.CreateEnum(SysConfigScopeType()),\n\n        dbops.CreateCompositeType(SysConfigValueType()),\n        dbops.CreateCompositeType(SysConfigEntryType()),\n        dbops.CreateRange(Float32Range()),\n        dbops.CreateRange(Float64Range()),\n        dbops.CreateRange(DatetimeRange()),\n        dbops.CreateRange(LocalDatetimeRange()),\n    ]\n\n    commands = dbops.CommandGroup()\n    commands.add_commands(cmds)\n    return commands\n\n\ndef get_instdata_commands(\n) -> tuple[dbops.CommandGroup, list[trampoline.Trampoline]]:\n    cmds = [\n        dbops.CreateSchema(name=V('edgedbinstdata')),\n        dbops.CreateTable(InstDataTable()),\n    ]\n\n    commands = dbops.CommandGroup()\n    commands.add_commands(cmds)\n\n    return commands, trampoline_functions(cmds)\n\n\nasync def generate_instdata_table(\n    conn: PGConnection,\n) -> list[trampoline.Trampoline]:\n    commands, trampolines = get_instdata_commands()\n    block = dbops.PLTopBlock()\n    commands.generate(block)\n    await _execute_block(conn, block)\n    return trampolines\n\n\ndef get_bootstrap_commands(\n    config_spec: edbconfig.Spec,\n) -> tuple[dbops.CommandGroup, list[trampoline.Trampoline]]:\n    trampolined = [\n        dbops.CreateSchema(name=V('edgedb')),\n        dbops.CreateSchema(name=V('edgedbpub')),\n        dbops.CreateSchema(name=V('edgedbstd')),\n        dbops.CreateSchema(name=V('edgedbsql')),\n\n        dbops.CreateView(NormalizedPgSettingsView()),\n        dbops.CreateFunction(EvictQueryCacheFunction()),\n        dbops.CreateFunction(ClearQueryCacheFunction()),\n        dbops.CreateFunction(CreateTrampolineViewFunction()),\n        dbops.CreateFunction(UuidGenerateV1mcFunction('edgedbext')),\n        dbops.CreateFunction(UuidGenerateV4Function('edgedbext')),\n        dbops.CreateFunction(UuidGenerateV5Function('edgedbext')),\n        dbops.CreateFunction(IntervalToMillisecondsFunction()),\n        dbops.CreateFunction(SafeIntervalCastFunction()),\n        dbops.CreateFunction(QuoteIdentFunction()),\n        dbops.CreateFunction(QuoteNameFunction()),\n        dbops.CreateFunction(AlterCurrentDatabaseSetString()),\n        dbops.CreateFunction(AlterCurrentDatabaseSetStringArray()),\n        dbops.CreateFunction(AlterCurrentDatabaseSetNonArray()),\n        dbops.CreateFunction(AlterCurrentDatabaseSetArray()),\n        dbops.CreateFunction(CopyDatabaseConfigs()),\n        dbops.CreateFunction(GetBackendCapabilitiesFunction()),\n        dbops.CreateFunction(GetBackendTenantIDFunction()),\n        dbops.CreateFunction(GetDatabaseBackendNameFunction()),\n        dbops.CreateFunction(GetDatabaseFrontendNameFunction()),\n        dbops.CreateFunction(GetRoleBackendNameFunction()),\n        dbops.CreateFunction(GetUserSequenceBackendNameFunction()),\n        dbops.CreateFunction(GetStdModulesFunction()),\n        dbops.CreateFunction(GetObjectMetadata()),\n        dbops.CreateFunction(GetColumnMetadata()),\n        dbops.CreateFunction(GetSharedObjectMetadata()),\n        dbops.CreateFunction(GetDatabaseMetadataFunction()),\n        dbops.CreateFunction(GetCurrentDatabaseFunction()),\n        dbops.CreateFunction(RaiseNoticeFunction()),\n        dbops.CreateFunction(IndirectReturnFunction()),\n        dbops.CreateFunction(RaiseExceptionFunction()),\n        dbops.CreateFunction(RaiseExceptionOnNullFunction()),\n        dbops.CreateFunction(RaiseExceptionOnNotNullFunction()),\n        dbops.CreateFunction(RaiseExceptionOnEmptyStringFunction()),\n        dbops.CreateFunction(AssertJSONTypeFunction()),\n        dbops.CreateFunction(ExtractJSONScalarFunction()),\n        dbops.CreateFunction(NormalizeNameFunction()),\n        dbops.CreateFunction(GetNameModuleFunction()),\n        dbops.CreateFunction(NullIfArrayNullsFunction()),\n        dbops.CreateFunction(StrToConfigMemoryFunction()),\n        dbops.CreateFunction(ConfigMemoryToStrFunction()),\n        dbops.CreateFunction(StrToBigint()),\n        dbops.CreateFunction(StrToDecimal()),\n        dbops.CreateFunction(StrToInt64NoInline()),\n        dbops.CreateFunction(StrToInt32NoInline()),\n        dbops.CreateFunction(StrToInt16NoInline()),\n        dbops.CreateFunction(StrToFloat64NoInline()),\n        dbops.CreateFunction(StrToFloat32NoInline()),\n        dbops.CreateFunction(NormalizeArrayIndexFunction()),\n        dbops.CreateFunction(NormalizeArraySliceIndexFunction()),\n        dbops.CreateFunction(IntOrNullFunction()),\n        dbops.CreateFunction(ArrayIndexWithBoundsFunction()),\n        dbops.CreateFunction(ArraySliceFunction()),\n        dbops.CreateFunction(StringIndexWithBoundsFunction()),\n        dbops.CreateFunction(LengthStringProxyFunction()),\n        dbops.CreateFunction(LengthBytesProxyFunction()),\n        dbops.CreateFunction(SubstrProxyFunction()),\n        dbops.CreateFunction(StringSliceImplFunction()),\n        dbops.CreateFunction(StringSliceFunction()),\n        dbops.CreateFunction(BytesSliceFunction()),\n        dbops.CreateFunction(JSONIndexByTextFunction()),\n        dbops.CreateFunction(JSONIndexByIntFunction()),\n        dbops.CreateFunction(JSONSliceFunction()),\n        dbops.CreateFunction(DatetimeInFunction()),\n        dbops.CreateFunction(DurationInFunction()),\n        dbops.CreateFunction(DateDurationInFunction()),\n        dbops.CreateFunction(LocalDatetimeInFunction()),\n        dbops.CreateFunction(LocalDateInFunction()),\n        dbops.CreateFunction(LocalTimeInFunction()),\n        dbops.CreateFunction(ToTimestampTZCheck()),\n        dbops.CreateFunction(ToDatetimeFunction()),\n        dbops.CreateFunction(ToLocalDatetimeFunction()),\n        dbops.CreateFunction(StrToBool()),\n        dbops.CreateFunction(BytesIndexWithBoundsFunction()),\n        dbops.CreateFunction(TypeIDToConfigType()),\n        dbops.CreateFunction(ConvertPostgresConfigUnitsFunction()),\n        dbops.CreateFunction(InterpretConfigValueToJsonFunction()),\n        dbops.CreateFunction(\n            PostgresJsonConfigValueToFrontendConfigValueFunction(config_spec)),\n        dbops.CreateFunction(PostgresConfigValueToJsonFunction()),\n        dbops.CreateFunction(SysConfigFullFunction()),\n        dbops.CreateFunction(SysConfigUncachedFunction()),\n        dbops.Query(pgcon.SETUP_CONFIG_CACHE_SCRIPT),\n        dbops.CreateFunction(SysConfigFunction()),\n        dbops.CreateFunction(SysClearConfigCacheFunction()),\n        dbops.CreateFunction(ResetSessionConfigFunction()),\n        dbops.CreateFunction(ApplySessionConfigFunction(config_spec)),\n        dbops.CreateFunction(SysGetTransactionIsolation()),\n        dbops.CreateFunction(GetCachedReflection()),\n        dbops.CreateFunction(GetBaseScalarTypeMap()),\n        dbops.CreateFunction(GetTypeToRangeNameMap()),\n        dbops.CreateFunction(GetTypeToMultiRangeNameMap()),\n        dbops.CreateFunction(GetPgTypeForEdgeDBTypeFunction()),\n        dbops.CreateFunction(DescribeRolesAsDDLFunctionForwardDecl()),\n        dbops.CreateFunction(AllRoleMembershipsFunctionForwardDecl()),\n        dbops.CreateFunction(RangeToJsonFunction()),\n        dbops.CreateFunction(MultiRangeToJsonFunction()),\n        dbops.CreateFunction(RangeValidateFunction()),\n        dbops.CreateFunction(RangeUnpackLowerValidateFunction()),\n        dbops.CreateFunction(RangeUnpackUpperValidateFunction()),\n        dbops.CreateFunction(FTSParseQueryFunction()),\n        dbops.CreateFunction(FTSNormalizeWeightFunction()),\n        dbops.CreateFunction(FTSNormalizeDocFunction()),\n        dbops.CreateFunction(FTSToRegconfig()),\n        dbops.CreateFunction(PadBase64StringFunction()),\n        dbops.CreateFunction(ResetQueryStatsFunction(False)),\n        dbops.CreateFunction(ApproximateCountDummy()),\n    ]\n\n    non_trampolined = [\n        dbops.CreateFunction(ClearFELocalSQLSettingsFunction()),\n    ]\n\n    commands = dbops.CommandGroup()\n    commands.add_commands(trampolined)\n    commands.add_commands(non_trampolined)\n\n    return commands, trampoline_functions(trampolined)\n\n\nasync def create_pg_extensions(\n    conn: PGConnection,\n    backend_params: params.BackendRuntimeParams,\n) -> None:\n    inst_params = backend_params.instance_params\n    ext_schema = inst_params.ext_schema\n    # Both the extension schema, and the desired extension\n    # might already exist in a single database backend,\n    # attempt to create things conditionally.\n    commands = dbops.CommandGroup()\n    commands.add_command(\n        dbops.CreateSchema(name=ext_schema, conditional=True),\n    )\n    extensions = [\"uuid-ossp\"]\n    if backend_params.has_stat_statements:\n        extensions.append(\"edb_stat_statements\")\n    for ext in extensions:\n        if (\n            inst_params.existing_exts is None\n            or inst_params.existing_exts.get(ext) is None\n        ):\n            commands.add_commands([\n                dbops.CreateExtension(\n                    dbops.Extension(name=ext, schema=ext_schema),\n                ),\n            ])\n    block = dbops.PLTopBlock()\n    commands.generate(block)\n    await _execute_block(conn, block)\n\n\nasync def patch_pg_extensions(\n    conn: PGConnection,\n    backend_params: params.BackendRuntimeParams,\n) -> None:\n    # A single database backend might restrict creation of extensions\n    # to a specific schema, or restrict creation of extensions altogether\n    # and provide a way to register them using a different method\n    # (e.g. a hosting panel UI).\n    inst_params = backend_params.instance_params\n    if inst_params.existing_exts is not None:\n        uuid_ext_schema = inst_params.existing_exts.get(\"uuid-ossp\")\n        if uuid_ext_schema is None:\n            uuid_ext_schema = inst_params.ext_schema\n    else:\n        uuid_ext_schema = inst_params.ext_schema\n\n    commands = dbops.CommandGroup()\n\n    if uuid_ext_schema != \"edgedbext\":\n        commands.add_commands([\n            dbops.CreateFunction(\n                UuidGenerateV1mcFunction(uuid_ext_schema), or_replace=True),\n            dbops.CreateFunction(\n                UuidGenerateV4Function(uuid_ext_schema), or_replace=True),\n            dbops.CreateFunction(\n                UuidGenerateV5Function(uuid_ext_schema), or_replace=True),\n        ])\n\n    if len(commands) > 0:\n        block = dbops.PLTopBlock()\n        commands.generate(block)\n        await _execute_block(conn, block)\n\n\nclassref_attr_aliases = {\n    'links': 'pointers',\n    'link_properties': 'pointers'\n}\n\n\ndef tabname(\n    schema: s_schema.Schema, obj: s_obj.Object\n) -> tuple[str, str]:\n    return common.get_backend_name(\n        schema,\n        obj,\n        aspect='table',\n        catenate=False,\n        versioned=True,\n    )\n\n\ndef ptr_col_name(\n    schema: s_schema.Schema,\n    obj: s_sources.Source,\n    propname: str,\n) -> str:\n    prop = obj.getptr(schema, s_name.UnqualName(propname))\n    psi = types.get_pointer_storage_info(prop, schema=schema)\n    return psi.column_name\n\n\ndef format_fields(\n    schema: s_schema.Schema,\n    obj: s_sources.Source,\n    fields: dict[str, str],\n) -> str:\n    \"\"\"Format a dictionary of column mappings for database views\n\n    The reason we do it this way is because, since these views are\n    overwriting existing temporary views, we need to put all the\n    columns in the same order as the original view.\n    \"\"\"\n    ptrs = [obj.getptr(schema, s_name.UnqualName(s)) for s in fields]\n\n    # Sort by the order the pointers were added to the source.\n    # N.B: This only works because we are using the original in-memory\n    # schema. If it was loaded from reflection it probably wouldn't\n    # work.\n    ptr_indexes = {\n        v: i for i, v in enumerate(obj.get_pointers(schema).objects(schema))\n    }\n    ptrs.sort(key=(\n        lambda p: (not p.is_link_source_property(schema), ptr_indexes[p])\n    ))\n\n    cols = []\n    for ptr in ptrs:\n        name = ptr.get_shortname(schema).name\n        val = fields[name]\n        sname = qi(ptr_col_name(schema, obj, name))\n        cols.append(f'            {val} AS {sname}')\n\n    return ',\\n'.join(cols)\n\n\ndef _generate_branch_views(schema: s_schema.Schema) -> list[dbops.View]:\n    Branch = schema.get('sys::Branch', type=s_objtypes.ObjectType)\n    annos = Branch.getptr(\n        schema, s_name.UnqualName('annotations'), type=s_links.Link)\n    int_annos = Branch.getptr(\n        schema, s_name.UnqualName('annotations__internal'), type=s_links.Link)\n\n    view_fields = {\n        'id': \"((d.description)->>'id')::uuid\",\n        'internal': f\"\"\"(CASE WHEN\n                (edgedb_VER.get_backend_capabilities()\n                 & {int(params.BackendCapabilities.CREATE_DATABASE)}) != 0\n             THEN\n                datname IN (\n                    edgedb_VER.get_database_backend_name(\n                        {ql(defines.EDGEDB_TEMPLATE_DB)}),\n                    edgedb_VER.get_database_backend_name(\n                        {ql(defines.EDGEDB_SYSTEM_DB)})\n                )\n             ELSE False END\n        )\"\"\",\n        'name': (\n            'edgedb_VER.get_database_frontend_name(datname) COLLATE \"default\"'\n        ),\n        'name__internal': (\n            'edgedb_VER.get_database_frontend_name(datname) COLLATE \"default\"'\n        ),\n        'computed_fields': 'ARRAY[]::text[]',\n        'builtin': \"((d.description)->>'builtin')::bool\",\n        'last_migration': \"(d.description)->>'last_migration'\",\n    }\n\n    view_query = f'''\n        SELECT\n            {format_fields(schema, Branch, view_fields)}\n        FROM\n            pg_database dat\n            CROSS JOIN LATERAL (\n                SELECT\n                    edgedb_VER.shobj_metadata(dat.oid, 'pg_database')\n                        AS description\n            ) AS d\n        WHERE\n            (d.description)->>'id' IS NOT NULL\n            AND (d.description)->>'tenant_id'\n                = edgedb_VER.get_backend_tenant_id()\n    '''\n\n    annos_link_fields = {\n        'source': \"((d.description)->>'id')::uuid\",\n        'target': \"(annotations->>'id')::uuid\",\n        'value': \"(annotations->>'value')::text\",\n        'owned': \"(annotations->>'owned')::bool\",\n    }\n\n    annos_link_query = f'''\n        SELECT\n            {format_fields(schema, annos, annos_link_fields)}\n        FROM\n            pg_database dat\n            CROSS JOIN LATERAL (\n                SELECT\n                    edgedb_VER.shobj_metadata(dat.oid, 'pg_database')\n                        AS description\n            ) AS d\n            CROSS JOIN LATERAL\n                ROWS FROM (\n                    jsonb_array_elements((d.description)->'annotations')\n                ) AS annotations\n    '''\n\n    int_annos_link_fields = {\n        'source': \"((d.description)->>'id')::uuid\",\n        'target': \"(annotations->>'id')::uuid\",\n        'owned': \"(annotations->>'owned')::bool\",\n    }\n\n    int_annos_link_query = f'''\n        SELECT\n            {format_fields(schema, int_annos, int_annos_link_fields)}\n        FROM\n            pg_database dat\n            CROSS JOIN LATERAL (\n                SELECT\n                    edgedb_VER.shobj_metadata(dat.oid, 'pg_database')\n                        AS description\n            ) AS d\n            CROSS JOIN LATERAL\n                ROWS FROM (\n                    jsonb_array_elements(\n                        (d.description)->'annotations__internal'\n                    )\n                ) AS annotations\n    '''\n\n    objects = {\n        Branch: view_query,\n        annos: annos_link_query,\n        int_annos: int_annos_link_query,\n    }\n\n    views: list[dbops.View] = []\n    for obj, query in objects.items():\n        tabview = trampoline.VersionedView(\n            name=tabname(schema, obj), query=query)\n        views.append(tabview)\n\n    return views\n\n\ndef _generate_extension_views(schema: s_schema.Schema) -> list[dbops.View]:\n    ExtPkg = schema.get('sys::ExtensionPackage', type=s_objtypes.ObjectType)\n    annos = ExtPkg.getptr(\n        schema, s_name.UnqualName('annotations'), type=s_links.Link)\n    int_annos = ExtPkg.getptr(\n        schema, s_name.UnqualName('annotations__internal'), type=s_links.Link)\n    ver = ExtPkg.getptr(\n        schema, s_name.UnqualName('version'), type=s_props.Property)\n    ver_t = common.get_backend_name(\n        schema,\n        not_none(ver.get_target(schema)),\n        catenate=False,\n    )\n\n    view_query_fields = {\n        'id': \"(e.value->>'id')::uuid\",\n        'name': \"(e.value->>'name')\",\n        'name__internal': \"(e.value->>'name__internal')\",\n        'script': \"(e.value->>'script')\",\n        'sql_extensions': '''\n            COALESCE(\n                (SELECT\n                    array_agg(edgedb_VER.jsonb_extract_scalar(q.v, 'string'))\n                FROM jsonb_array_elements(\n                    e.value->'sql_extensions'\n                ) AS q(v)),\n                ARRAY[]::text[]\n            )\n        ''',\n        'dependencies': '''\n            COALESCE(\n                (SELECT\n                    array_agg(edgedb_VER.jsonb_extract_scalar(q.v, 'string'))\n                FROM jsonb_array_elements(\n                    e.value->'dependencies'\n                ) AS q(v)),\n                ARRAY[]::text[]\n            )\n        ''',\n        'ext_module': \"(e.value->>'ext_module')\",\n        'sql_setup_script': \"(e.value->>'sql_setup_script')\",\n        'sql_teardown_script': \"(e.value->>'sql_teardown_script')\",\n        'computed_fields': 'ARRAY[]::text[]',\n        'builtin': \"(e.value->>'builtin')::bool\",\n        'internal': \"(e.value->>'internal')::bool\",\n        'version': f'''\n            (\n                (e.value->'version'->>'major')::int,\n                (e.value->'version'->>'minor')::int,\n                (e.value->'version'->>'stage')::text,\n                (e.value->'version'->>'stage_no')::int,\n                COALESCE(\n                    (SELECT array_agg(q.v::text)\n                    FROM jsonb_array_elements(\n                        e.value->'version'->'local'\n                    ) AS q(v)),\n                    ARRAY[]::text[]\n                )\n            )::{qt(ver_t)}\n        ''',\n    }\n\n    view_query = f'''\n        SELECT\n            {format_fields(schema, ExtPkg, view_query_fields)}\n        FROM\n            jsonb_each(\n                edgedb_VER.get_database_metadata(\n                    {ql(defines.EDGEDB_TEMPLATE_DB)}\n                ) -> 'ExtensionPackage'\n            ) AS e\n    '''\n\n    annos_link_fields = {\n        'source': \"(e.value->>'id')::uuid\",\n        'target': \"(annotations->>'id')::uuid\",\n        'value': \"(annotations->>'value')::text\",\n        'owned': \"(annotations->>'owned')::bool\",\n    }\n\n    int_annos_link_fields = {\n        'source': \"(e.value->>'id')::uuid\",\n        'target': \"(annotations->>'id')::uuid\",\n        'owned': \"(annotations->>'owned')::bool\",\n    }\n\n    annos_link_query = f'''\n        SELECT\n            {format_fields(schema, annos, annos_link_fields)}\n        FROM\n            jsonb_each(\n                edgedb_VER.get_database_metadata(\n                    {ql(defines.EDGEDB_TEMPLATE_DB)}\n                ) -> 'ExtensionPackage'\n            ) AS e\n            CROSS JOIN LATERAL\n                ROWS FROM (\n                    jsonb_array_elements(e.value->'annotations')\n                ) AS annotations\n    '''\n\n    int_annos_link_query = f'''\n        SELECT\n            {format_fields(schema, int_annos, int_annos_link_fields)}\n        FROM\n            jsonb_each(\n                edgedb_VER.get_database_metadata(\n                    {ql(defines.EDGEDB_TEMPLATE_DB)}\n                ) -> 'ExtensionPackage'\n            ) AS e\n            CROSS JOIN LATERAL\n                ROWS FROM (\n                    jsonb_array_elements(e.value->'annotations__internal')\n                ) AS annotations\n    '''\n\n    objects = {\n        ExtPkg: view_query,\n        annos: annos_link_query,\n        int_annos: int_annos_link_query,\n    }\n\n    views: list[dbops.View] = []\n    for obj, query in objects.items():\n        tabview = trampoline.VersionedView(\n            name=tabname(schema, obj), query=query)\n        views.append(tabview)\n\n    return views\n\n\ndef _generate_extension_migration_views(\n    schema: s_schema.Schema\n) -> list[dbops.View]:\n    ExtPkgMigration = schema.get(\n        'sys::ExtensionPackageMigration', type=s_objtypes.ObjectType)\n    annos = ExtPkgMigration.getptr(\n        schema, s_name.UnqualName('annotations'), type=s_links.Link)\n    int_annos = ExtPkgMigration.getptr(\n        schema, s_name.UnqualName('annotations__internal'), type=s_links.Link)\n    from_ver = ExtPkgMigration.getptr(\n        schema, s_name.UnqualName('from_version'), type=s_props.Property)\n    ver_t = common.get_backend_name(\n        schema,\n        not_none(from_ver.get_target(schema)),\n        catenate=False,\n    )\n\n    view_query_fields = {\n        'id': \"(e.value->>'id')::uuid\",\n        'name': \"(e.value->>'name')\",\n        'name__internal': \"(e.value->>'name__internal')\",\n        'script': \"(e.value->>'script')\",\n        'sql_early_script': \"(e.value->>'sql_early_script')\",\n        'sql_late_script': \"(e.value->>'sql_late_script')\",\n        'computed_fields': 'ARRAY[]::text[]',\n        'builtin': \"(e.value->>'builtin')::bool\",\n        'internal': \"(e.value->>'internal')::bool\",\n        # XXX: code duplication here\n        'from_version': f'''\n            (\n                (e.value->'from_version'->>'major')::int,\n                (e.value->'from_version'->>'minor')::int,\n                (e.value->'from_version'->>'stage')::text,\n                (e.value->'from_version'->>'stage_no')::int,\n                COALESCE(\n                    (SELECT array_agg(q.v::text)\n                    FROM jsonb_array_elements(\n                        e.value->'from_version'->'local'\n                    ) AS q(v)),\n                    ARRAY[]::text[]\n                )\n            )::{qt(ver_t)}\n        ''',\n        'to_version': f'''\n            (\n                (e.value->'to_version'->>'major')::int,\n                (e.value->'to_version'->>'minor')::int,\n                (e.value->'to_version'->>'stage')::text,\n                (e.value->'to_version'->>'stage_no')::int,\n                COALESCE(\n                    (SELECT array_agg(q.v::text)\n                    FROM jsonb_array_elements(\n                        e.value->'to_version'->'local'\n                    ) AS q(v)),\n                    ARRAY[]::text[]\n                )\n            )::{qt(ver_t)}\n        ''',\n    }\n\n    view_query = f'''\n        SELECT\n            {format_fields(schema, ExtPkgMigration, view_query_fields)}\n        FROM\n            jsonb_each(\n                edgedb_VER.get_database_metadata(\n                    {ql(defines.EDGEDB_TEMPLATE_DB)}\n                ) -> 'ExtensionPackageMigration'\n            ) AS e\n    '''\n\n    annos_link_fields = {\n        'source': \"(e.value->>'id')::uuid\",\n        'target': \"(annotations->>'id')::uuid\",\n        'value': \"(annotations->>'value')::text\",\n        'owned': \"(annotations->>'owned')::bool\",\n    }\n\n    int_annos_link_fields = {\n        'source': \"(e.value->>'id')::uuid\",\n        'target': \"(annotations->>'id')::uuid\",\n        'owned': \"(annotations->>'owned')::bool\",\n    }\n\n    annos_link_query = f'''\n        SELECT\n            {format_fields(schema, annos, annos_link_fields)}\n        FROM\n            jsonb_each(\n                edgedb_VER.get_database_metadata(\n                    {ql(defines.EDGEDB_TEMPLATE_DB)}\n                ) -> 'ExtensionPackageMigration'\n            ) AS e\n            CROSS JOIN LATERAL\n                ROWS FROM (\n                    jsonb_array_elements(e.value->'annotations')\n                ) AS annotations\n    '''\n\n    int_annos_link_query = f'''\n        SELECT\n            {format_fields(schema, int_annos, int_annos_link_fields)}\n        FROM\n            jsonb_each(\n                edgedb_VER.get_database_metadata(\n                    {ql(defines.EDGEDB_TEMPLATE_DB)}\n                ) -> 'ExtensionPackageMigration'\n            ) AS e\n            CROSS JOIN LATERAL\n                ROWS FROM (\n                    jsonb_array_elements(e.value->'annotations__internal')\n                ) AS annotations\n    '''\n\n    objects = {\n        ExtPkgMigration: view_query,\n        annos: annos_link_query,\n        int_annos: int_annos_link_query,\n    }\n\n    views: list[dbops.View] = []\n    for obj, query in objects.items():\n        tabview = trampoline.VersionedView(\n            name=tabname(schema, obj), query=query)\n        views.append(tabview)\n\n    return views\n\n\ndef _generate_role_views(schema: s_schema.Schema) -> list[dbops.View]:\n    Role = schema.get('sys::Role', type=s_objtypes.ObjectType)\n    member_of = Role.getptr(\n        schema, s_name.UnqualName('member_of'), type=s_links.Link\n    )\n    bases = Role.getptr(\n        schema, s_name.UnqualName('bases'), type=s_links.Link\n    )\n    ancestors = Role.getptr(\n        schema, s_name.UnqualName('ancestors'), type=s_links.Link\n    )\n    annos = Role.getptr(\n        schema, s_name.UnqualName('annotations'), type=s_links.Link\n    )\n    int_annos = Role.getptr(\n        schema, s_name.UnqualName('annotations__internal'), type=s_links.Link\n    )\n    permissions = Role.getptr(\n        schema, s_name.UnqualName('permissions'), type=s_props.Property\n    )\n    branches = Role.getptr(\n        schema, s_name.UnqualName('branches'), type=s_props.Property\n    )\n\n    superuser = f'''\n        a.rolsuper OR EXISTS (\n            SELECT\n            FROM\n                pg_auth_members m\n                INNER JOIN pg_catalog.pg_roles g\n                    ON (m.roleid = g.oid)\n            WHERE\n                m.member = a.oid\n                AND g.rolname = edgedb_VER.get_role_backend_name(\n                    {ql(defines.EDGEDB_SUPERGROUP)}\n                )\n        )\n    '''\n\n    view_query_fields = {\n        'id': \"((d.description)->>'id')::uuid\",\n        'name': \"(d.description)->>'name'\",\n        'name__internal': \"(d.description)->>'name'\",\n        'superuser': f'{superuser}',\n        'abstract': 'False',\n        'is_derived': 'False',\n        'inherited_fields': 'ARRAY[]::text[]',\n        'computed_fields': 'ARRAY[]::text[]',\n        'builtin': \"((d.description)->>'builtin')::bool\",\n        'internal': 'False',\n        'password': \"(d.description)->>'password_hash'\",\n        'apply_access_policies_pg_default': (\n            \"((d.description)->>'apply_access_policies_pg_default')::bool\"\n        ),\n    }\n\n    view_query = f'''\n        SELECT\n            {format_fields(schema, Role, view_query_fields)}\n        FROM\n            pg_catalog.pg_roles AS a\n            CROSS JOIN LATERAL (\n                SELECT\n                    edgedb_VER.shobj_metadata(a.oid, 'pg_authid')\n                        AS description\n            ) AS d\n        WHERE\n            (d.description)->>'id' IS NOT NULL\n            AND\n              (d.description)->>'tenant_id' = edgedb_VER.get_backend_tenant_id()\n    '''\n\n    member_of_link_query_fields = {\n        'source': \"((d.description)->>'id')::uuid\",\n        'target': \"((md.description)->>'id')::uuid\",\n    }\n\n    member_of_link_query = f'''\n        SELECT\n            {format_fields(schema, member_of, member_of_link_query_fields)}\n        FROM\n            pg_catalog.pg_roles AS a\n            CROSS JOIN LATERAL (\n                SELECT\n                    edgedb_VER.shobj_metadata(a.oid, 'pg_authid')\n                        AS description\n            ) AS d\n            INNER JOIN pg_auth_members m ON m.member = a.oid\n            CROSS JOIN LATERAL (\n                SELECT\n                    edgedb_VER.shobj_metadata(m.roleid, 'pg_authid')\n                        AS description\n            ) AS md\n    '''\n\n    bases_link_query_fields = {\n        'source': \"((d.description)->>'id')::uuid\",\n        'target': \"((md.description)->>'id')::uuid\",\n        'index': 'row_number() OVER (PARTITION BY a.oid ORDER BY m.roleid)',\n    }\n\n    bases_link_query = f'''\n        SELECT\n            {format_fields(schema, bases, bases_link_query_fields)}\n        FROM\n            pg_catalog.pg_roles AS a\n            CROSS JOIN LATERAL (\n                SELECT\n                    edgedb_VER.shobj_metadata(a.oid, 'pg_authid')\n                        AS description\n            ) AS d\n            INNER JOIN pg_auth_members m ON m.member = a.oid\n            CROSS JOIN LATERAL (\n                SELECT\n                    edgedb_VER.shobj_metadata(m.roleid, 'pg_authid')\n                        AS description\n            ) AS md\n    '''\n\n    ancestors_link_query = f'''\n        SELECT\n            {format_fields(schema, ancestors, bases_link_query_fields)}\n        FROM\n            pg_catalog.pg_roles AS a\n            CROSS JOIN LATERAL (\n                SELECT\n                    edgedb_VER.shobj_metadata(a.oid, 'pg_authid')\n                        AS description\n            ) AS d\n            INNER JOIN pg_auth_members m ON m.member = a.oid\n            CROSS JOIN LATERAL (\n                SELECT\n                    edgedb_VER.shobj_metadata(m.roleid, 'pg_authid')\n                        AS description\n            ) AS md\n    '''\n\n    annos_link_fields = {\n        'source': \"((d.description)->>'id')::uuid\",\n        'target': \"(annotations->>'id')::uuid\",\n        'value': \"(annotations->>'value')::text\",\n        'owned': \"(annotations->>'owned')::bool\",\n    }\n\n    annos_link_query = f'''\n        SELECT\n            {format_fields(schema, annos, annos_link_fields)}\n        FROM\n            pg_catalog.pg_roles AS a\n            CROSS JOIN LATERAL (\n                SELECT\n                    edgedb_VER.shobj_metadata(a.oid, 'pg_authid')\n                        AS description\n            ) AS d\n            CROSS JOIN LATERAL\n                ROWS FROM (\n                    jsonb_array_elements(\n                        (d.description)->'annotations'\n                    )\n                ) AS annotations\n    '''\n\n    int_annos_link_fields = {\n        'source': \"((d.description)->>'id')::uuid\",\n        'target': \"(annotations->>'id')::uuid\",\n        'owned': \"(annotations->>'owned')::bool\",\n    }\n\n    int_annos_link_query = f'''\n        SELECT\n            {format_fields(schema, int_annos, int_annos_link_fields)}\n        FROM\n            pg_catalog.pg_roles AS a\n            CROSS JOIN LATERAL (\n                SELECT\n                    edgedb_VER.shobj_metadata(a.oid, 'pg_authid')\n                        AS description\n            ) AS d\n            CROSS JOIN LATERAL\n                ROWS FROM (\n                    jsonb_array_elements(\n                        (d.description)->'annotations__internal'\n                    )\n                ) AS annotations\n    '''\n\n    permissions_query = f'''\n        SELECT\n            ((d.description)->>'id')::uuid AS source,\n            jsonb_array_elements_text(\n                (d.description)->'permissions'\n            )::text as target\n        FROM\n            pg_catalog.pg_roles AS a\n            CROSS JOIN LATERAL (\n                SELECT\n                    edgedb_VER.shobj_metadata(a.oid, 'pg_authid')\n                        AS description\n            ) AS d\n        WHERE\n            (d.description)->>'id' IS NOT NULL\n            AND\n              (d.description)->>'tenant_id' = edgedb_VER.get_backend_tenant_id()\n    '''\n    branches_query = f'''\n        SELECT\n            ((d.description)->>'id')::uuid AS source,\n            jsonb_array_elements_text(\n                -- The coalesce is to handle inplace upgrades from versions\n                -- before the field was added. If it is lacking from the dict,\n                -- make it ['*'].\n                coalesce((d.description)->'branches', '[\"*\"]'::jsonb)\n            )::text as target\n        FROM\n            pg_catalog.pg_roles AS a\n            CROSS JOIN LATERAL (\n                SELECT\n                    edgedb_VER.shobj_metadata(a.oid, 'pg_authid')\n                        AS description\n            ) AS d\n        WHERE\n            (d.description)->>'id' IS NOT NULL\n            AND\n              (d.description)->>'tenant_id' = edgedb_VER.get_backend_tenant_id()\n    '''\n\n    objects = {\n        Role: view_query,\n        member_of: member_of_link_query,\n        bases: bases_link_query,\n        ancestors: ancestors_link_query,\n        annos: annos_link_query,\n        int_annos: int_annos_link_query,\n        permissions: permissions_query,\n        branches: branches_query,\n    }\n\n    views: list[dbops.View] = []\n    for obj, query in objects.items():\n        tabview = trampoline.VersionedView(\n            name=tabname(schema, obj), query=query)\n        views.append(tabview)\n\n    return views\n\n\ndef _generate_single_role_views(schema: s_schema.Schema) -> list[dbops.View]:\n    Role = schema.get('sys::Role', type=s_objtypes.ObjectType)\n    member_of = Role.getptr(\n        schema, s_name.UnqualName('member_of'), type=s_links.Link\n    )\n    bases = Role.getptr(\n        schema, s_name.UnqualName('bases'), type=s_links.Link\n    )\n    ancestors = Role.getptr(\n        schema, s_name.UnqualName('ancestors'), type=s_links.Link\n    )\n    annos = Role.getptr(\n        schema, s_name.UnqualName('annotations'), type=s_links.Link\n    )\n    int_annos = Role.getptr(\n        schema, s_name.UnqualName('annotations__internal'), type=s_links.Link\n    )\n    permissions = Role.getptr(\n        schema, s_name.UnqualName('permissions'), type=s_props.Property\n    )\n    branches = Role.getptr(\n        schema, s_name.UnqualName('branches'), type=s_props.Property\n    )\n\n    view_query_fields = {\n        'id': \"(json->>'id')::uuid\",\n        'name': \"json->>'name'\",\n        'name__internal': \"json->>'name'\",\n        'superuser': 'True',\n        'abstract': 'False',\n        'is_derived': 'False',\n        'inherited_fields': 'ARRAY[]::text[]',\n        'computed_fields': 'ARRAY[]::text[]',\n        'builtin': 'True',\n        'internal': 'False',\n        'password': \"json->>'password_hash'\",\n        'apply_access_policies_pg_default': (\n            \"(json->>'pg_apply_access_policies_default')::bool\"\n        ),\n    }\n\n    view_query = f'''\n        SELECT\n            {format_fields(schema, Role, view_query_fields)}\n        FROM\n            edgedbinstdata_VER.instdata\n        WHERE\n            key = 'single_role_metadata'\n            AND json->>'tenant_id' = edgedb_VER.get_backend_tenant_id()\n    '''\n\n    member_of_link_query_fields = {\n        'source': \"'00000000-0000-0000-0000-000000000000'::uuid\",\n        'target': \"'00000000-0000-0000-0000-000000000000'::uuid\",\n    }\n\n    member_of_link_query = f'''\n        SELECT\n            {format_fields(schema, member_of, member_of_link_query_fields)}\n        LIMIT 0\n    '''\n\n    bases_link_query_fields = {\n        'source': \"'00000000-0000-0000-0000-000000000000'::uuid\",\n        'target': \"'00000000-0000-0000-0000-000000000000'::uuid\",\n        'index': \"0::bigint\",\n    }\n\n    bases_link_query = f'''\n        SELECT\n            {format_fields(schema, bases, bases_link_query_fields)}\n        LIMIT 0\n    '''\n\n    ancestors_link_query = f'''\n        SELECT\n            {format_fields(schema, ancestors, bases_link_query_fields)}\n        LIMIT 0\n    '''\n\n    annos_link_fields = {\n        'source': \"(json->>'id')::uuid\",\n        'target': \"(annotations->>'id')::uuid\",\n        'value': \"(annotations->>'value')::text\",\n        'owned': \"(annotations->>'owned')::bool\",\n    }\n\n    annos_link_query = f'''\n        SELECT\n            {format_fields(schema, annos, annos_link_fields)}\n        FROM\n            edgedbinstdata_VER.instdata\n            CROSS JOIN LATERAL\n                ROWS FROM (\n                    jsonb_array_elements(json->'annotations')\n                ) AS annotations\n        WHERE\n            key = 'single_role_metadata'\n            AND json->>'tenant_id' = edgedb_VER.get_backend_tenant_id()\n    '''\n\n    int_annos_link_fields = {\n        'source': \"(json->>'id')::uuid\",\n        'target': \"(annotations->>'id')::uuid\",\n        'owned': \"(annotations->>'owned')::bool\",\n    }\n\n    int_annos_link_query = f'''\n        SELECT\n            {format_fields(schema, int_annos, int_annos_link_fields)}\n        FROM\n            edgedbinstdata_VER.instdata\n            CROSS JOIN LATERAL\n                ROWS FROM (\n                    jsonb_array_elements(json->'annotations__internal')\n                ) AS annotations\n        WHERE\n            key = 'single_role_metadata'\n            AND json->>'tenant_id' = edgedb_VER.get_backend_tenant_id()\n    '''\n\n    # The single superuser role already has all permissions.\n    # For completeness, create a permissions multi-prop table with dummy\n    # values. It will return no rows since its WHERE clause is always false.\n    permissions_query = f'''\n        SELECT\n            '00000000-0000-0000-0000-000000000000'::uuid AS source,\n            ''::text AS target\n        WHERE 1 = 0\n    '''\n\n    branches_query = f'''\n        SELECT\n            (json->>'id')::uuid AS source,\n            '*'::text as target\n        FROM\n            edgedbinstdata_VER.instdata\n        WHERE\n            key = 'single_role_metadata'\n            AND json->>'tenant_id' = edgedb_VER.get_backend_tenant_id()\n    '''\n\n    objects = {\n        Role: view_query,\n        member_of: member_of_link_query,\n        bases: bases_link_query,\n        ancestors: ancestors_link_query,\n        annos: annos_link_query,\n        int_annos: int_annos_link_query,\n        permissions: permissions_query,\n        branches: branches_query,\n    }\n\n    views: list[dbops.View] = []\n    for obj, query in objects.items():\n        tabview = trampoline.VersionedView(\n            name=tabname(schema, obj), query=query)\n        views.append(tabview)\n\n    return views\n\n\ndef _generate_schema_ver_views(schema: s_schema.Schema) -> list[dbops.View]:\n    Ver = schema.get(\n        'sys::GlobalSchemaVersion',\n        type=s_objtypes.ObjectType,\n    )\n\n    view_fields = {\n        'id': \"(v.value->>'id')::uuid\",\n        'name': \"(v.value->>'name')\",\n        'name__internal': \"(v.value->>'name')\",\n        'version': \"(v.value->>'version')::uuid\",\n        'builtin': \"(v.value->>'builtin')::bool\",\n        'internal': \"(v.value->>'internal')::bool\",\n        'computed_fields': 'ARRAY[]::text[]',\n    }\n\n    view_query = f'''\n        SELECT\n            {format_fields(schema, Ver, view_fields)}\n        FROM\n            jsonb_each(\n                edgedb_VER.get_database_metadata(\n                    {ql(defines.EDGEDB_TEMPLATE_DB)}\n                ) -> 'GlobalSchemaVersion'\n            ) AS v\n    '''\n\n    objects = {\n        Ver: view_query\n    }\n\n    views: list[dbops.View] = []\n    for obj, query in objects.items():\n        tabview = trampoline.VersionedView(\n            name=tabname(schema, obj), query=query)\n        views.append(tabview)\n\n    return views\n\n\ndef _generate_stats_views(schema: s_schema.Schema) -> list[dbops.View]:\n    QueryStats = schema.get(\n        'sys::QueryStats',\n        type=s_objtypes.ObjectType,\n    )\n    pvd = common.get_backend_name(\n        schema,\n        QueryStats\n            .getptr(schema, s_name.UnqualName(\"protocol_version\"))\n            .get_target(schema)  # type: ignore\n    )\n    QueryType = schema.get(\n        'sys::QueryType',\n        type=s_scalars.ScalarType,\n    )\n    query_type_domain = common.get_backend_name(schema, QueryType)\n    type_mapping = {\n        str(v): k for k, v in defines.QueryType.__members__.items()\n    }\n    output_format_domain = common.get_backend_name(\n        schema, schema.get('sys::OutputFormat', type=s_scalars.ScalarType)\n    )\n\n    def float64_to_duration_t(val: str) -> str:\n        return f\"({val} * interval '1ms')::edgedbt.duration_t\"\n\n    query_stats_fields = {\n        'id': \"s.id\",\n        'name': \"s.id::text\",\n        'name__internal': \"s.queryid::text\",\n        'builtin': \"false\",\n        'internal': \"false\",\n        'computed_fields': 'ARRAY[]::text[]',\n\n        'compilation_config': \"s.extras->'cc'\",\n        'protocol_version': f\"ROW(s.extras->'pv'->0, s.extras->'pv'->1)::{pvd}\",\n        'default_namespace': \"s.extras->>'dn'\",\n        'namespace_aliases': \"s.extras->'na'\",\n        'output_format': f\"(s.extras->>'of')::{output_format_domain}\",\n        'expect_one': \"(s.extras->'e1')::boolean\",\n        'implicit_limit': \"(s.extras->'il')::bigint\",\n        'inline_typeids': \"(s.extras->'ii')::boolean\",\n        'inline_typenames': \"(s.extras->'in')::boolean\",\n        'inline_objectids': \"(s.extras->'io')::boolean\",\n\n        'branch': \"((d.description)->>'id')::uuid\",\n        'query': \"s.query\",\n        'query_type': f\"(t.mapping->>s.stmt_type::text)::{query_type_domain}\",\n        'tag': \"s.tag\",\n\n        'plans': 's.plans',\n        'total_plan_time': float64_to_duration_t('s.total_plan_time'),\n        'min_plan_time': float64_to_duration_t('s.min_plan_time'),\n        'max_plan_time': float64_to_duration_t('s.max_plan_time'),\n        'mean_plan_time': float64_to_duration_t('s.mean_plan_time'),\n        'stddev_plan_time': float64_to_duration_t('s.stddev_plan_time'),\n\n        'calls': 's.calls',\n        'total_exec_time': float64_to_duration_t('s.total_exec_time'),\n        'min_exec_time': float64_to_duration_t('s.min_exec_time'),\n        'max_exec_time': float64_to_duration_t('s.max_exec_time'),\n        'mean_exec_time': float64_to_duration_t('s.mean_exec_time'),\n        'stddev_exec_time': float64_to_duration_t('s.stddev_exec_time'),\n\n        'rows': 's.rows',\n        'stats_since': 's.stats_since::edgedbt.timestamptz_t',\n        'minmax_stats_since': 's.minmax_stats_since::edgedbt.timestamptz_t',\n    }\n\n    query_stats_query = fr'''\n        SELECT\n            {format_fields(schema, QueryStats, query_stats_fields)}\n        FROM\n            edgedbext.edb_stat_statements AS s\n            INNER JOIN pg_database dat ON s.dbid = dat.oid\n            CROSS JOIN LATERAL (\n                SELECT\n                    edgedb_VER.shobj_metadata(dat.oid, 'pg_database')\n                        AS description\n            ) AS d\n            CROSS JOIN LATERAL (\n                SELECT {ql(json.dumps(type_mapping))}::jsonb AS mapping\n            ) AS t\n        WHERE\n            s.id IS NOT NULL\n            AND (d.description)->>'id' IS NOT NULL\n            AND (d.description)->>'tenant_id'\n                = edgedb_VER.get_backend_tenant_id()\n            AND t.mapping ? s.stmt_type::text\n    '''\n\n    objects = {\n        QueryStats: query_stats_query,\n    }\n\n    views: list[dbops.View] = []\n    for obj, query in objects.items():\n        tabview = trampoline.VersionedView(\n            name=tabname(schema, obj), query=query)\n        views.append(tabview)\n\n    return views\n\n\ndef _make_json_caster(\n    schema: s_schema.Schema,\n    stype: s_types.Type,\n    versioned: bool,\n) -> Callable[[str], str]:\n    cast_expr = qlast.TypeCast(\n        expr=qlast.TypeCast(\n            expr=qlast.FunctionParameter(name=\"__replaceme__\"),\n            type=s_utils.typeref_to_ast(schema, schema.get('std::json')),\n        ),\n        type=s_utils.typeref_to_ast(schema, stype),\n    )\n\n    cast_ir = qlcompiler.compile_ast_fragment_to_ir(\n        cast_expr,\n        schema,\n    )\n\n    cast_sql_res = compiler.compile_ir_to_sql_tree(\n        cast_ir,\n        named_param_prefix=(),\n        singleton_mode=True,\n        versioned_singleton=versioned,\n    )\n    cast_sql = codegen.generate_source(cast_sql_res.ast)\n\n    return lambda val: cast_sql.replace('__replaceme__', val)\n\n\ndef _generate_schema_alias_views(\n    schema: s_schema.Schema,\n    module: s_name.UnqualName,\n) -> list[dbops.View]:\n    views = []\n\n    schema_objs = schema.get_objects(\n        type=s_objtypes.ObjectType,\n        included_modules=(module,),\n    )\n\n    for schema_obj in schema_objs:\n        if not schema_obj.get_from_alias(schema):\n            views.append(_generate_schema_alias_view(schema, schema_obj))\n\n    return views\n\n\ndef _generate_schema_alias_view(\n    schema: s_schema.Schema,\n    obj: s_sources.Source | s_pointers.Pointer,\n) -> dbops.View:\n\n    name = _schema_alias_view_name(schema, obj)\n    select = inheritance.get_inheritance_view(schema, obj)\n\n    return trampoline.VersionedView(\n        name=name,\n        query=codegen.generate_source(select),\n    )\n\n\ndef _schema_alias_view_name(\n    schema: s_schema.Schema,\n    obj: s_sources.Source | s_pointers.Pointer,\n) -> tuple[str, str]:\n    module = obj.get_name(schema).module\n    prefix = module.capitalize()\n\n    if isinstance(obj, s_links.Link):\n        objtype = obj.get_source(schema)\n        assert objtype is not None\n        objname = objtype.get_name(schema).name\n        lname = obj.get_shortname(schema).name\n        name = f'_{prefix}{objname}__{lname}'\n    else:\n        name = f'_{prefix}{obj.get_name(schema).name}'\n\n    return ('edgedb', name)\n\n\ndef _generate_sql_information_schema(\n    backend_version: params.BackendVersion\n) -> list[dbops.Command]:\n\n    # Helper to create wrappers around materialized views.  For\n    # performance, we use MATERIALIZED VIEW for some of our SQL\n    # emulation tables. Unfortunately we can't use those directly,\n    # since we need tableoid to match the real pg_catalog table.\n    def make_wrapper_view(name: str) -> trampoline.VersionedView:\n        return trampoline.VersionedView(\n            name=(\"edgedbsql\", name),\n            query=f\"\"\"\n            SELECT *,\n            'pg_catalog.{name}'::regclass::oid as tableoid,\n            xmin, cmin, xmax, cmax, ctid\n            FROM edgedbsql_VER.{name}_\n            \"\"\",\n        )\n\n    # A helper view that contains all data tables we expose over SQL, excluding\n    # introspection tables.\n    # It contains table & schema names and associated module id.\n    virtual_tables = trampoline.VersionedView(\n        name=('edgedbsql', 'virtual_tables'),\n        materialized=True,\n        query='''\n        WITH obj_ty_pre AS (\n            SELECT\n                id,\n                REGEXP_REPLACE(name, '::[^:]*$', '') AS module_name,\n                REGEXP_REPLACE(name, '^.*::', '') as table_name\n            FROM edgedb_VER.\"_SchemaObjectType\"\n            WHERE internal IS NOT TRUE\n        ),\n        obj_ty AS (\n            SELECT\n                id,\n                REGEXP_REPLACE(module_name, '^default(?=::|$)', 'public')\n                    AS schema_name,\n                module_name,\n                table_name\n            FROM obj_ty_pre\n        ),\n        all_tables (id, schema_name, module_name, table_name) AS ((\n            SELECT * FROM obj_ty\n        ) UNION ALL (\n            WITH qualified_links AS (\n                -- multi links and links with at least one property\n                -- (besides source and target)\n                SELECT link.id\n                FROM edgedb_VER.\"_SchemaLink\" link\n                JOIN edgedb_VER.\"_SchemaProperty\" AS prop\n                  ON link.id = prop.source\n                WHERE prop.computable IS NOT TRUE AND prop.internal IS NOT TRUE\n                GROUP BY link.id, link.cardinality\n                HAVING link.cardinality = 'Many' OR COUNT(*) > 2\n            )\n            SELECT link.id, obj_ty.schema_name, obj_ty.module_name,\n                CONCAT(obj_ty.table_name, '.', link.name) AS table_name\n            FROM edgedb_VER.\"_SchemaLink\" link\n            JOIN obj_ty ON obj_ty.id = link.source\n            WHERE link.id IN (SELECT * FROM qualified_links)\n        ) UNION ALL (\n            -- multi properties\n            SELECT prop.id, obj_ty.schema_name, obj_ty.module_name,\n                CONCAT(obj_ty.table_name, '.', prop.name) AS table_name\n            FROM edgedb_VER.\"_SchemaProperty\" AS prop\n            JOIN obj_ty ON obj_ty.id = prop.source\n            WHERE prop.computable IS NOT TRUE\n            AND prop.internal IS NOT TRUE\n            AND prop.cardinality = 'Many'\n        ))\n        SELECT\n            at.id,\n            schema_name,\n            table_name,\n            sm.id AS module_id,\n            pt.oid AS pg_type_id\n        FROM all_tables at\n        JOIN edgedb_VER.\"_SchemaModule\" sm ON sm.name = at.module_name\n        LEFT JOIN pg_type pt ON pt.typname = at.id::text\n        WHERE schema_name not in (\n            'cfg',\n            'sys',\n            'schema',\n            'std',\n            'std::net',\n            'std::net::http',\n            'std::net::perm'\n        )\n        '''\n    )\n    # A few tables in here were causing problems, so let's hide them as an\n    # implementation detail.\n    # To be more specific:\n    # - following tables were missing from information_schema:\n    #   Link.properties, ObjectType.links, ObjectType.properties\n    # - even though introspection worked, I wasn't able to select from some\n    #   tables in cfg and sys\n\n    # For making up oids of schemas that represent modules\n    uuid_to_oid = trampoline.VersionedFunction(\n        name=('edgedbsql', 'uuid_to_oid'),\n        args=(\n            ('id', 'uuid'),\n            # extra is two extra bits to throw into the oid, for now\n            ('extra', 'int4', '0'),\n        ),\n        returns=('oid',),\n        volatility='immutable',\n        text=\"\"\"\n            SELECT (\n                ('x' || substring(id::text, 2, 7))::bit(28)::bigint*4 + extra\n                 + 40000)::oid;\n        \"\"\"\n    )\n    long_name = trampoline.VersionedFunction(\n        name=('edgedbsql', '_long_name'),\n        args=[\n            ('origname', ('text',)),\n            ('longname', ('text',)),\n        ],\n        returns=('text',),\n        volatility='stable',\n        text=r'''\n            SELECT CASE WHEN length(longname) > 63\n                THEN left(longname, 55) || left(origname, 8)\n                ELSE longname\n                END\n        '''\n    )\n    type_rename = trampoline.VersionedFunction(\n        name=('edgedbsql', '_pg_type_rename'),\n        args=[\n            ('typeoid', ('oid',)),\n            ('typename', ('name',)),\n        ],\n        returns=('name',),\n        volatility='stable',\n        text=r'''\n            SELECT COALESCE (\n                -- is the name in virtual_tables?\n                (\n                    SELECT vt.table_name::name\n                    FROM edgedbsql_VER.virtual_tables vt\n                    WHERE vt.pg_type_id = typeoid\n                ),\n                -- is this a scalar or tuple?\n                (\n                    SELECT name::name\n                    FROM (\n                        -- get the built-in scalars\n                        SELECT\n                            split_part(name, '::', 2) AS name,\n                            backend_id\n                        FROM edgedb_VER.\"_SchemaScalarType\"\n                        WHERE NOT builtin AND arg_values IS NULL\n                        UNION ALL\n                        -- get the tuples\n                        SELECT\n                            edgedbsql_VER._long_name(typename, name),\n                            backend_id\n                        FROM edgedb_VER.\"_SchemaTuple\"\n                    ) x\n                    WHERE x.backend_id = typeoid\n                ),\n                typename\n            )\n        '''\n    )\n    namespace_rename = trampoline.VersionedFunction(\n        name=('edgedbsql', '_pg_namespace_rename'),\n        args=[\n            ('typeoid', ('oid',)),\n            ('typens', ('oid',)),\n        ],\n        returns=('oid',),\n        volatility='stable',\n        text=r'''\n            WITH\n                nspub AS (\n                    SELECT oid FROM pg_namespace WHERE nspname = 'edgedbpub'\n                ),\n                nsdef AS (\n                    SELECT edgedbsql_VER.uuid_to_oid(id) AS oid\n                    FROM edgedb_VER.\"_SchemaModule\"\n                    WHERE name = 'default'\n                )\n            SELECT COALESCE (\n                (\n                    SELECT edgedbsql_VER.uuid_to_oid(vt.module_id)\n                    FROM edgedbsql_VER.virtual_tables vt\n                    WHERE vt.pg_type_id = typeoid\n                ),\n                -- just replace \"edgedbpub\" with \"public\"\n                (SELECT nsdef.oid WHERE typens = nspub.oid),\n                typens\n            )\n            FROM\n                nspub,\n                nsdef\n        '''\n    )\n    # pg_settings is a function because \"_edgecon_state\" is a temporary table\n    # and therefore cannot be used in a view.\n    fe_pg_settings = trampoline.VersionedFunction(\n        name=('edgedbsql', 'pg_show_all_settings'),\n        args=[],\n        returns=('pg_catalog', 'pg_settings'),\n        set_returning=True,\n        volatility='volatile',\n        text='''\n            SELECT\n                p.name,\n                COALESCE(\n                    COALESCE(l.value, s.value, d.value) #>> '{}',\n                    p.setting\n                ) AS setting,\n                unit,\n                category,\n                short_desc,\n                extra_desc,\n                context,\n                vartype,\n                CASE\n                    WHEN l.value IS NOT NULL THEN 'session'\n                    WHEN s.value IS NOT NULL THEN 'session'\n                    WHEN d.value IS NOT NULL THEN 'default'\n                    ELSE p.source\n                END AS source,\n                min_val,\n                max_val,\n                enumvals,\n                boot_val,\n                CASE\n                    WHEN d.value IS NOT NULL THEN d.value #>> '{}'\n                    ELSE p.reset_val\n                END AS reset_val,\n                sourcefile,\n                sourceline,\n                pending_restart\n            FROM\n                pg_settings p\n                LEFT JOIN _edgecon_state l ON p.name = l.name AND l.type = 'L'\n                LEFT JOIN _edgecon_state s ON p.name = s.name AND s.type = 'S'\n                LEFT JOIN (\n                    SELECT\n                        (j->>'name') AS name,\n                        (j->'value') AS value\n                    FROM\n                        edgedbinstdata_VER.instdata\n                        CROSS JOIN LATERAL\n                            jsonb_array_elements(instdata.json) AS j\n                    WHERE\n                        key = 'sql_default_fe_settings'\n                ) d ON p.name = d.name\n        '''\n    )\n\n    sql_ident = 'information_schema.sql_identifier'\n    sql_str = 'information_schema.character_data'\n    sql_bool = 'information_schema.yes_or_no'\n    sql_card = 'information_schema.cardinal_number'\n    tables_and_columns = [\n        trampoline.VersionedView(\n            name=('edgedbsql', 'tables'),\n            query=(\n                f'''\n        SELECT\n            edgedb_VER.get_current_database()::{sql_ident} AS table_catalog,\n            vt.schema_name::{sql_ident} AS table_schema,\n            vt.table_name::{sql_ident} AS table_name,\n            ist.table_type,\n            ist.self_referencing_column_name,\n            ist.reference_generation,\n            ist.user_defined_type_catalog,\n            ist.user_defined_type_schema,\n            ist.user_defined_type_name,\n            ist.is_insertable_into,\n            ist.is_typed,\n            ist.commit_action\n        FROM information_schema.tables ist\n        JOIN edgedbsql_VER.virtual_tables vt ON vt.id::text = ist.table_name\n            '''\n            ),\n        ),\n        trampoline.VersionedView(\n            name=('edgedbsql', 'columns'),\n            query=(\n                f'''\n        SELECT\n            edgedb_VER.get_current_database()::{sql_ident} AS table_catalog,\n            vt_table_schema::{sql_ident} AS table_schema,\n            vt_table_name::{sql_ident} AS table_name,\n            v_column_name::{sql_ident} as column_name,\n            cast(ROW_NUMBER() OVER (\n                PARTITION BY vt_table_schema, vt_table_name\n                ORDER BY position, v_column_name\n            ) AS INT) AS ordinal_position,\n            column_default,\n            is_nullable,\n            data_type,\n            NULL::{sql_card} AS character_maximum_length,\n            NULL::{sql_card} AS character_octet_length,\n            NULL::{sql_card} AS numeric_precision,\n            NULL::{sql_card} AS numeric_precision_radix,\n            NULL::{sql_card} AS numeric_scale,\n            NULL::{sql_card} AS datetime_precision,\n            NULL::{sql_str} AS interval_type,\n            NULL::{sql_card} AS interval_precision,\n            NULL::{sql_ident} AS character_set_catalog,\n            NULL::{sql_ident} AS character_set_schema,\n            NULL::{sql_ident} AS character_set_name,\n            NULL::{sql_ident} AS collation_catalog,\n            NULL::{sql_ident} AS collation_schema,\n            NULL::{sql_ident} AS collation_name,\n            NULL::{sql_ident} AS domain_catalog,\n            NULL::{sql_ident} AS domain_schema,\n            NULL::{sql_ident} AS domain_name,\n            edgedb_VER.get_current_database()::{sql_ident} AS udt_catalog,\n            'pg_catalog'::{sql_ident} AS udt_schema,\n            NULL::{sql_ident} AS udt_name,\n            NULL::{sql_ident} AS scope_catalog,\n            NULL::{sql_ident} AS scope_schema,\n            NULL::{sql_ident} AS scope_name,\n            NULL::{sql_card} AS maximum_cardinality,\n            0::{sql_ident} AS dtd_identifier,\n            'NO'::{sql_bool} AS is_self_referencing,\n            'NO'::{sql_bool} AS is_identity,\n            NULL::{sql_str} AS identity_generation,\n            NULL::{sql_str} AS identity_start,\n            NULL::{sql_str} AS identity_increment,\n            NULL::{sql_str} AS identity_maximum,\n            NULL::{sql_str} AS identity_minimum,\n            'NO' ::{sql_bool} AS identity_cycle,\n            'NEVER'::{sql_str} AS is_generated,\n            NULL::{sql_str} AS generation_expression,\n            'YES'::{sql_bool} AS is_updatable\n        FROM (\n        SELECT\n            vt.schema_name AS vt_table_schema,\n            vt.table_name AS vt_table_name,\n            COALESCE(\n                -- this happends for id and __type__\n                spec.name,\n\n                -- fallback to pointer name, with suffix '_id' for links\n                sp.name || case when sl.id is not null then '_id' else '' end\n            ) AS v_column_name,\n            COALESCE(spec.position, 2) AS position,\n            (sp.expr IS NOT NULL) AS is_computed,\n            isc.column_default,\n            CASE WHEN sp.required OR spec.k IS NOT NULL\n                THEN 'NO' ELSE 'YES' END AS is_nullable,\n\n            -- HACK: computeds don't have backing rows in isc,\n            -- so we just default to 'text'. This is wrong.\n            COALESCE(isc.data_type, 'text') AS data_type\n        FROM edgedb_VER.\"_SchemaPointer\" sp\n        LEFT JOIN information_schema.columns isc ON (\n            isc.table_name = sp.source::TEXT AND CASE\n                WHEN length(isc.column_name) = 36 -- if column name is uuid\n                THEN isc.column_name = sp.id::text -- compare uuids\n                ELSE isc.column_name = sp.name -- for id, source, target\n            END\n        )\n\n        -- needed for attaching `_id`\n        LEFT JOIN edgedb_VER.\"_SchemaLink\" sl ON sl.id = sp.id\n\n        -- needed for determining table name\n        JOIN edgedbsql_VER.virtual_tables vt ON vt.id = sp.source\n\n        -- positions for special pointers\n        -- duplicate id get both id and __type__ columns out of it\n        LEFT JOIN (\n            VALUES  ('id', 'id', 0),\n                    ('id', '__type__', 1),\n                    ('source', 'source', 0),\n                    ('target', 'target', 1)\n        ) spec(k, name, position) ON (spec.k = isc.column_name)\n\n        WHERE isc.column_name IS NOT NULL -- normal pointers\n           OR sp.expr IS NOT NULL AND sp.cardinality <> 'Many' -- computeds\n\n        UNION ALL\n\n        -- special case: multi properties source and target\n        -- (this is needed, because schema does not create pointers for\n        -- these two columns)\n        SELECT\n            vt.schema_name AS vt_table_schema,\n            vt.table_name AS vt_table_name,\n            isc.column_name AS v_column_name,\n            spec.position as position,\n            FALSE as is_computed,\n            isc.column_default,\n            'NO' as is_nullable,\n            isc.data_type as data_type\n        FROM edgedb_VER.\"_SchemaPointer\" sp\n        JOIN information_schema.columns isc ON isc.table_name = sp.id::TEXT\n\n        -- needed for filtering out links\n        LEFT JOIN edgedb_VER.\"_SchemaLink\" sl ON sl.id = sp.id\n\n        -- needed for determining table name\n        JOIN edgedbsql_VER.virtual_tables vt ON vt.id = sp.id\n\n        -- positions for special pointers\n        JOIN (\n            VALUES  ('source', 'source', 0),\n                    ('target', 'target', 1)\n        ) spec(k, name, position) ON (spec.k = isc.column_name)\n\n        WHERE\n            sl.id IS NULL -- property (non-link)\n            AND sp.cardinality = 'Many' -- multi\n            AND sp.expr IS NULL -- non-computed\n        ) t\n            '''\n            ),\n        ),\n    ]\n\n    pg_catalog_views = [\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_namespace_\"),\n            materialized=True,\n            query=\"\"\"\n        -- system schemas\n        SELECT\n            oid,\n            nspname,\n            nspowner,\n            nspacl\n        FROM pg_namespace\n        WHERE nspname IN ('pg_catalog', 'pg_toast', 'information_schema',\n                          'edgedb', 'edgedbstd', 'edgedbt',\n                          'edgedb_VER', 'edgedbstd_VER')\n        UNION ALL\n\n        -- virtual schemas\n        SELECT\n            edgedbsql_VER.uuid_to_oid(t.module_id)  AS oid,\n            t.schema_name                       AS nspname,\n            (SELECT oid\n             FROM pg_roles\n             WHERE rolname = CURRENT_USER\n             LIMIT 1)                           AS nspowner,\n            NULL AS nspacl\n        FROM (\n            SELECT schema_name, module_id\n            FROM edgedbsql_VER.virtual_tables\n\n            UNION\n\n            -- always include the default module,\n            -- because it is needed for tuple types\n            SELECT 'public' AS schema_name, id AS module_id\n            FROM edgedb_VER.\"_SchemaModule\"\n            WHERE name = 'default'\n        ) t\n        \"\"\",\n        ),\n        make_wrapper_view(\"pg_namespace\"),\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_type_\"),\n            materialized=True,\n            query=\"\"\"\n        SELECT\n            pt.oid,\n            edgedbsql_VER._pg_type_rename(pt.oid, pt.typname)\n                AS typname,\n            edgedbsql_VER._pg_namespace_rename(pt.oid, pt.typnamespace)\n                AS typnamespace,\n            {0}\n        FROM pg_type pt\n        JOIN pg_namespace pn ON pt.typnamespace = pn.oid\n        WHERE\n            nspname IN ('pg_catalog', 'pg_toast', 'information_schema',\n                        'edgedb', 'edgedbstd', 'edgedb_VER', 'edgedbstd_VER',\n                        'edgedbpub', 'edgedbt')\n        \"\"\".format(\n                \",\".join(\n                    f\"pt.{col}\"\n                    for col, _, _ in sql_introspection.PG_CATALOG[\"pg_type\"][3:]\n                )\n            ),\n        ),\n        make_wrapper_view(\"pg_type\"),\n        # pg_class that contains classes only for tables\n        # This is needed so we can use it to filter pg_index to indexes only on\n        # visible tables.\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_class_tables\"),\n            materialized=True,\n            query=\"\"\"\n        -- Postgres tables\n        SELECT pc.*\n        FROM pg_class pc\n        JOIN pg_namespace pn ON pc.relnamespace = pn.oid\n        WHERE nspname IN ('pg_catalog', 'pg_toast', 'information_schema')\n\n        UNION ALL\n\n        -- user-defined tables\n        SELECT\n            oid,\n            vt.table_name as relname,\n            edgedbsql_VER.uuid_to_oid(vt.module_id) as relnamespace,\n            reltype,\n            reloftype,\n            relowner,\n            relam,\n            relfilenode,\n            reltablespace,\n            relpages,\n            reltuples,\n            relallvisible,\n            reltoastrelid,\n            relhasindex,\n            relisshared,\n            relpersistence,\n            relkind,\n            relnatts,\n            0 as relchecks, -- don't care about CHECK constraints\n            relhasrules,\n            relhastriggers,\n            relhassubclass,\n            relrowsecurity,\n            relforcerowsecurity,\n            relispopulated,\n            relreplident,\n            relispartition,\n            relrewrite,\n            relfrozenxid,\n            relminmxid,\n            relacl,\n            reloptions,\n            relpartbound\n        FROM pg_class pc\n        JOIN edgedbsql_VER.virtual_tables vt ON vt.pg_type_id = pc.reltype\n        \"\"\",\n        ),\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_index_\"),\n            materialized=True,\n            query=f\"\"\"\n        SELECT\n            pi.indexrelid,\n            pi.indrelid,\n            pi.indnatts,\n            pi.indnkeyatts,\n            CASE\n                WHEN COALESCE(is_id.t, FALSE) THEN TRUE\n                ELSE pi.indisprimary\n            END AS indisunique,\n            {'pi.indnullsnotdistinct,' if backend_version.major >= 15 else ''}\n            CASE\n                WHEN COALESCE(is_id.t, FALSE) THEN TRUE\n                ELSE pi.indisprimary\n            END AS indisprimary,\n            pi.indisexclusion,\n            pi.indimmediate,\n            pi.indisclustered,\n            pi.indisvalid,\n            pi.indcheckxmin,\n            CASE\n                WHEN COALESCE(is_id.t, FALSE) THEN TRUE\n                ELSE FALSE -- override so pg_dump won't try to recreate them\n            END AS indisready,\n            pi.indislive,\n            pi.indisreplident,\n            CASE\n                WHEN COALESCE(is_id.t, FALSE) THEN ARRAY[1]::int2vector -- id: 1\n                ELSE pi.indkey\n            END AS indkey,\n            pi.indcollation,\n            pi.indclass,\n            pi.indoption,\n            pi.indexprs,\n            pi.indpred\n        FROM pg_index pi\n\n        -- filter by tables visible in pg_class\n        INNER JOIN edgedbsql_VER.pg_class_tables pr ON pi.indrelid = pr.oid\n\n        -- find indexes that are on virtual tables and on `id` columns\n        LEFT JOIN LATERAL (\n            SELECT TRUE AS t\n            FROM pg_attribute pa\n            WHERE pa.attrelid = pi.indrelid\n              AND pa.attnum = ANY(pi.indkey)\n              AND pa.attname = 'id'\n        ) is_id ON TRUE\n\n        -- for our tables show only primary key indexes\n        LEFT JOIN edgedbsql_VER.virtual_tables vt ON vt.pg_type_id = pr.reltype\n        WHERE vt.id IS NULL OR is_id.t IS NOT NULL\n        \"\"\",\n        ),\n        make_wrapper_view('pg_index'),\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_class_\"),\n            materialized=True,\n            query=\"\"\"\n        -- tables\n        SELECT pc.*\n        FROM edgedbsql_VER.pg_class_tables pc\n\n        UNION\n\n        -- indexes\n        SELECT pc.*\n        FROM pg_class pc\n        JOIN pg_index pi ON pc.oid = pi.indexrelid\n\n        UNION\n\n        -- compound types (tuples)\n        SELECT\n            pc.oid,\n            edgedbsql_VER._long_name(pc.reltype::text, tup.name) as relname,\n            nsdef.oid as relnamespace,\n            pc.reltype,\n            pc.reloftype,\n            pc.relowner,\n            pc.relam,\n            pc.relfilenode,\n            pc.reltablespace,\n            pc.relpages,\n            pc.reltuples,\n            pc.relallvisible,\n            pc.reltoastrelid,\n            pc.relhasindex,\n            pc.relisshared,\n            pc.relpersistence,\n            pc.relkind,\n            pc.relnatts,\n            0 as relchecks, -- don't care about CHECK constraints\n            pc.relhasrules,\n            pc.relhastriggers,\n            pc.relhassubclass,\n            pc.relrowsecurity,\n            pc.relforcerowsecurity,\n            pc.relispopulated,\n            pc.relreplident,\n            pc.relispartition,\n            pc.relrewrite,\n            pc.relfrozenxid,\n            pc.relminmxid,\n            pc.relacl,\n            pc.reloptions,\n            pc.relpartbound\n        FROM pg_class pc\n        JOIN edgedb_VER.\"_SchemaTuple\" tup ON tup.backend_id = pc.reltype\n        JOIN (\n            SELECT edgedbsql_VER.uuid_to_oid(id) AS oid\n            FROM edgedb_VER.\"_SchemaModule\"\n            WHERE name = 'default'\n        ) nsdef ON TRUE\n        \"\"\",\n        ),\n        make_wrapper_view(\"pg_class\"),\n        # Because we hide some columns and\n        # because pg_dump expects attnum to be sequential numbers\n        # we have to invent new attnums with ROW_NUMBER().\n        # Since attnum is used elsewhere, we need to know the mapping from\n        # constructed attnum into underlying attnum.\n        # To do that, we have pg_attribute_ext view with additional\n        # attnum_internal column.\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_attribute_ext\"),\n            materialized=True,\n            query=r\"\"\"\n        SELECT attrelid,\n            attname,\n            atttypid,\n            attstattarget,\n            attlen,\n            attnum,\n            attnum as attnum_internal,\n            attndims,\n            attcacheoff,\n            atttypmod,\n            attbyval,\n            attstorage,\n            attalign,\n            attnotnull,\n            atthasdef,\n            atthasmissing,\n            attidentity,\n            attgenerated,\n            attisdropped,\n            attislocal,\n            attinhcount,\n            attcollation,\n            attacl,\n            attoptions,\n            attfdwoptions,\n            null::int[] as attmissingval\n        FROM pg_attribute pa\n        JOIN pg_class pc ON pa.attrelid = pc.oid\n        JOIN pg_namespace pn ON pc.relnamespace = pn.oid\n        LEFT JOIN edgedb_VER.\"_SchemaTuple\" tup ON tup.backend_id = pc.reltype\n        WHERE\n            nspname IN ('pg_catalog', 'pg_toast', 'information_schema')\n            OR\n            tup.backend_id IS NOT NULL\n\n        UNION ALL\n\n        SELECT pc_oid as attrelid,\n            col_name as attname,\n            COALESCE(atttypid, 25) as atttypid, -- defaults to TEXT\n            COALESCE(attstattarget, -1) as attstattarget,\n            COALESCE(attlen, -1) as attlen,\n            (ROW_NUMBER() OVER (\n                PARTITION BY pc_oid\n                ORDER BY col_position, col_name\n            ) - 6)::smallint AS attnum,\n            t.attnum as attnum_internal,\n            COALESCE(attndims, 0) as attndims,\n            COALESCE(attcacheoff, -1) as attcacheoff,\n            COALESCE(atttypmod, -1) as atttypmod,\n            COALESCE(attbyval, FALSE) as attbyval,\n            COALESCE(attstorage, 'x') as attstorage,\n            COALESCE(attalign, 'i') as attalign,\n            required as attnotnull,\n            -- Always report no default, to avoid expr trouble\n            false as atthasdef,\n            COALESCE(atthasmissing, FALSE) as atthasmissing,\n            COALESCE(attidentity, '') as attidentity,\n            COALESCE(attgenerated, '') as attgenerated,\n            COALESCE(attisdropped, FALSE) as attisdropped,\n            COALESCE(attislocal, TRUE) as attislocal,\n            COALESCE(attinhcount, 0) as attinhcount,\n            COALESCE(attcollation, 0) as attcollation,\n            attacl,\n            attoptions,\n            attfdwoptions,\n            null::int[] as attmissingval\n        FROM (\n        SELECT\n            COALESCE(\n                spec.name, -- for special columns\n                sp.name || case when sl.id is not null then '_id' else '' end,\n                pa.attname -- for system columns\n            ) as col_name,\n            COALESCE(spec.position, 2) AS col_position,\n            (sp.required IS TRUE OR spec.k IS NOT NULL) as required,\n            pc.oid AS pc_oid,\n            pa.*\n\n        FROM edgedb_VER.\"_SchemaPointer\" sp\n        JOIN edgedbsql_VER.virtual_tables vt ON vt.id = sp.source\n        JOIN pg_class pc ON pc.reltype = vt.pg_type_id\n\n        -- try to find existing pg_attribute (it will not exist for computeds)\n        LEFT JOIN pg_attribute pa ON (\n            pa.attrelid = pc.oid AND CASE\n                WHEN length(pa.attname) = 36 -- if column name is uuid\n                THEN pa.attname = sp.id::text -- compare uuids\n                ELSE pa.attname = sp.name -- for id, source, target\n            END\n        )\n\n        -- positions for special pointers\n        -- duplicate id get both id and __type__ columns out of it\n        LEFT JOIN (\n            VALUES  ('id', 'id', 0),\n                    ('id', '__type__', 1),\n                    ('source', 'source', 0),\n                    ('target', 'target', 1)\n        ) spec(k, name, position) ON (spec.k = pa.attname)\n\n        -- needed for attaching `_id`\n        LEFT JOIN edgedb_VER.\"_SchemaLink\" sl ON sl.id = sp.id\n\n        WHERE pa.attname IS NOT NULL -- non-computed pointers\n           OR sp.expr IS NOT NULL AND sp.cardinality <> 'Many' -- computeds\n\n        UNION ALL\n\n        -- special case: multi properties source and target\n        -- (this is needed, because schema does not create pointers for\n        -- these two columns)\n        SELECT\n            pa.attname AS col_name,\n            spec.position as position,\n            TRUE as required,\n            pa.attrelid as pc_oid,\n            pa.*\n        FROM edgedb_VER.\"_SchemaProperty\" sp\n        JOIN pg_class pc ON pc.relname = sp.id::TEXT\n        JOIN pg_attribute pa ON pa.attrelid = pc.oid\n\n        -- positions for special pointers\n        JOIN (\n            VALUES  ('source', 0),\n                    ('target', 1)\n        ) spec(k, position) ON (spec.k = pa.attname)\n\n        WHERE\n            sp.cardinality = 'Many' -- multi\n            AND sp.expr IS NULL -- non-computed\n\n        UNION ALL\n\n        -- special case: system columns\n        SELECT\n            pa.attname AS col_name,\n            pa.attnum as position,\n            TRUE as required,\n            pa.attrelid as pc_oid,\n            pa.*\n        FROM pg_attribute pa\n        JOIN pg_class pc ON pc.oid = pa.attrelid\n        JOIN edgedbsql_VER.virtual_tables vt ON vt.pg_type_id = pc.reltype\n        WHERE pa.attnum < 0\n        ) t\n        \"\"\",\n        ),\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_attribute\"),\n            query=\"\"\"\n        SELECT\n          attrelid,\n          attname,\n          atttypid,\n          attstattarget,\n          attlen,\n          attnum,\n          attndims,\n          attcacheoff,\n          atttypmod,\n          attbyval,\n          attstorage,\n          attalign,\n          attnotnull,\n          atthasdef,\n          atthasmissing,\n          attidentity,\n          attgenerated,\n          attisdropped,\n          attislocal,\n          attinhcount,\n          attcollation,\n          attacl,\n          attoptions,\n          attfdwoptions,\n          attmissingval,\n          'pg_catalog.pg_attribute'::regclass::oid as tableoid,\n          xmin,\n          cmin,\n          xmax,\n          cmax,\n          ctid\n        FROM edgedbsql_VER.pg_attribute_ext\n        \"\"\",\n        ),\n\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_database\"),\n            query=f\"\"\"\n        SELECT\n            oid,\n            frontend_name.n as datname,\n            datdba,\n            encoding,\n            {'datlocprovider,' if backend_version.major >= 15 else ''}\n            datcollate,\n            datctype,\n            datistemplate,\n            datallowconn,\n            {'dathasloginevt,' if backend_version.major >= 17 else ''}\n            datconnlimit,\n            0::oid AS datlastsysoid,\n            datfrozenxid,\n            datminmxid,\n            dattablespace,\n            {'datlocale,' if backend_version.major >= 17 else ''}\n            {'daticurules,' if backend_version.major >= 16 else ''}\n            {'datcollversion,' if backend_version.major >= 15 else ''}\n            datacl,\n            tableoid, xmin, cmin, xmax, cmax, ctid\n        FROM\n            pg_database,\n            LATERAL (\n                SELECT edgedb_VER.get_database_frontend_name(datname) AS n\n            ) frontend_name,\n            LATERAL (\n                SELECT edgedb_VER.get_database_metadata(frontend_name.n) AS j\n            ) metadata\n        WHERE\n            metadata.j->>'tenant_id' = edgedb_VER.get_backend_tenant_id()\n            AND NOT (metadata.j->'builtin')::bool\n        \"\"\",\n        ),\n\n        # HACK: there were problems with pg_dump when exposing this table, so\n        # I've added WHERE FALSE. The query could be simplified, but it may\n        # be needed in the future. Its EXPLAIN cost is 0..0 anyway.\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_stats\"),\n            query=\"\"\"\n        SELECT n.nspname AS schemaname,\n            c.relname AS tablename,\n            a.attname,\n            s.stainherit AS inherited,\n            s.stanullfrac AS null_frac,\n            s.stawidth AS avg_width,\n            s.stadistinct AS n_distinct,\n            NULL::real[] AS most_common_vals,\n            s.stanumbers1 AS most_common_freqs,\n            s.stanumbers1 AS histogram_bounds,\n            s.stanumbers1[1] AS correlation,\n            NULL::real[] AS most_common_elems,\n            s.stanumbers1 AS most_common_elem_freqs,\n            s.stanumbers1 AS elem_count_histogram\n        FROM pg_statistic s\n        JOIN pg_class c ON c.oid = s.starelid\n        JOIN edgedbsql_VER.pg_attribute_ext a ON (\n            c.oid = a.attrelid and a.attnum_internal = s.staattnum\n        )\n        LEFT JOIN pg_namespace n ON n.oid = c.relnamespace\n        WHERE FALSE\n        \"\"\",\n        ),\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_constraint\"),\n            query=r\"\"\"\n        -- primary keys for:\n        --  - objects tables (that contains id)\n        --  - link tables (that contains source and target)\n        -- there exists a unique constraint for each of these\n        SELECT\n          pc.oid,\n          vt.table_name || '_pk' AS conname,\n          pc.connamespace,\n          'p'::\"char\" AS contype,\n          pc.condeferrable,\n          pc.condeferred,\n          pc.convalidated,\n          pc.conrelid,\n          pc.contypid,\n          pc.conindid,\n          pc.conparentid,\n          NULL::oid AS confrelid,\n          NULL::\"char\" AS confupdtype,\n          NULL::\"char\" AS confdeltype,\n          NULL::\"char\" AS confmatchtype,\n          pc.conislocal,\n          pc.coninhcount,\n          pc.connoinherit,\n          CASE WHEN pa.attname = 'id'\n            THEN ARRAY[1]::int2[] -- id will always have attnum 1\n            ELSE ARRAY[1, 2]::int2[] -- source and target\n          END AS conkey,\n          NULL::int2[] AS confkey,\n          NULL::oid[] AS conpfeqop,\n          NULL::oid[] AS conppeqop,\n          NULL::oid[] AS conffeqop,\n          NULL::int2[] AS confdelsetcols,\n          NULL::oid[] AS conexclop,\n          pc.conbin,\n          pc.tableoid, pc.xmin, pc.cmin, pc.xmax, pc.cmax, pc.ctid\n        FROM pg_constraint pc\n        JOIN edgedbsql_VER.pg_class_tables pct ON pct.oid = pc.conrelid\n        JOIN edgedbsql_VER.virtual_tables vt ON vt.pg_type_id = pct.reltype\n        JOIN pg_attribute pa\n          ON (pa.attrelid = pct.oid\n              AND pa.attnum = ANY(conkey)\n              AND pa.attname IN ('id', 'source')\n             )\n        WHERE contype = 'u' -- our ids and all links will have unique constraint\n\n        UNION ALL\n\n        -- foreign keys for object tables\n        SELECT\n          -- uuid_to_oid needs \"extra\" arg to disambiguate from the link table\n          -- keys below\n          edgedbsql_VER.uuid_to_oid(sl.id, 0) as oid,\n          vt.table_name || '_fk_' || sl.name AS conname,\n          edgedbsql_VER.uuid_to_oid(vt.module_id) AS connamespace,\n          'f'::\"char\" AS contype,\n          FALSE AS condeferrable,\n          FALSE AS condeferred,\n          TRUE AS convalidated,\n          pc.oid AS conrelid,\n          0::oid AS contypid,\n          0::oid AS conindid, -- let's hope this is not needed\n          0::oid AS conparentid,\n          pc_target.oid AS confrelid,\n          'a'::\"char\" AS confupdtype,\n          'a'::\"char\" AS confdeltype,\n          's'::\"char\" AS confmatchtype,\n          TRUE AS conislocal,\n          0::int2 AS coninhcount,\n          TRUE AS connoinherit,\n          ARRAY[pa.attnum]::int2[] AS conkey,\n          ARRAY[1]::int2[] AS confkey, -- id will always have attnum 1\n          ARRAY['uuid_eq'::regproc]::oid[] AS conpfeqop,\n          ARRAY['uuid_eq'::regproc]::oid[] AS conppeqop,\n          ARRAY['uuid_eq'::regproc]::oid[] AS conffeqop,\n          NULL::int2[] AS confdelsetcols,\n          NULL::oid[] AS conexclop,\n          NULL::pg_node_tree AS conbin,\n          pa.tableoid, pa.xmin, pa.cmin, pa.xmax, pa.cmax, pa.ctid\n        FROM edgedbsql_VER.virtual_tables vt\n        JOIN pg_class pc ON pc.reltype = vt.pg_type_id\n        JOIN edgedb_VER.\"_SchemaLink\" sl\n          ON sl.source = vt.id -- AND COALESCE(sl.cardinality = 'One', TRUE)\n        JOIN edgedbsql_VER.virtual_tables vt_target\n          ON sl.target = vt_target.id\n        JOIN pg_class pc_target ON pc_target.reltype = vt_target.pg_type_id\n        JOIN edgedbsql_VER.pg_attribute pa\n          ON pa.attrelid = pc.oid\n         AND pa.attname = sl.name || '_id'\n\n        UNION ALL\n\n        -- foreign keys for:\n        -- - multi link tables (source & target),\n        -- - multi property tables (source),\n        -- - single link with link properties (source & target),\n        -- these constraints do not actually exist, so we emulate it entierly\n        SELECT\n            -- uuid_to_oid needs \"extra\" arg to disambiguate from other\n            -- constraints using this pointer\n            edgedbsql_VER.uuid_to_oid(sp.id, spec.attnum) AS oid,\n            vt.table_name || '_fk_' || spec.name AS conname,\n            edgedbsql_VER.uuid_to_oid(vt.module_id) AS connamespace,\n            'f'::\"char\" AS contype,\n            FALSE AS condeferrable,\n            FALSE AS condeferred,\n            TRUE AS convalidated,\n            pc.oid AS conrelid,\n            pc.reltype AS contypid,\n            0::oid AS conindid, -- TODO\n            0::oid AS conparentid,\n            pcf.oid AS confrelid,\n            'r'::\"char\" AS confupdtype,\n            'r'::\"char\" AS confdeltype,\n            's'::\"char\" AS confmatchtype,\n            TRUE AS conislocal,\n            0::int2 AS coninhcount,\n            TRUE AS connoinherit,\n            ARRAY[spec.attnum]::int2[] AS conkey,\n            ARRAY[1]::int2[] AS confkey,     -- id will have attnum 1\n            ARRAY['uuid_eq'::regproc]::oid[] AS conpfeqop,\n            ARRAY['uuid_eq'::regproc]::oid[] AS conppeqop,\n            ARRAY['uuid_eq'::regproc]::oid[] AS conffeqop,\n            NULL::int2[] AS confdelsetcols,\n            NULL::oid[] AS conexclop,\n            pc.relpartbound AS conbin,\n            pc.tableoid,\n            pc.xmin,\n            pc.cmin,\n            pc.xmax,\n            pc.cmax,\n            pc.ctid\n        FROM edgedb_VER.\"_SchemaPointer\" sp\n\n        -- find links with link properties\n        LEFT JOIN LATERAL (\n            SELECT sl.id\n            FROM edgedb_VER.\"_SchemaLink\" sl\n            LEFT JOIN edgedb_VER.\"_SchemaProperty\" AS slp ON slp.source = sl.id\n            GROUP BY sl.id\n            HAVING COUNT(*) > 2\n        ) link_props ON link_props.id = sp.id\n\n        JOIN pg_class pc ON pc.relname = sp.id::TEXT\n        JOIN edgedbsql_VER.virtual_tables vt ON vt.pg_type_id = pc.reltype\n\n        -- duplicate each row for source and target\n        JOIN LATERAL (VALUES\n            ('source', 1::int2, sp.source),\n            ('target', 2::int2, sp.target)\n        ) spec(name, attnum, foreign_id) ON TRUE\n        JOIN edgedbsql_VER.virtual_tables vtf ON vtf.id = spec.foreign_id\n        JOIN pg_class pcf ON pcf.reltype = vtf.pg_type_id\n\n        WHERE\n            sp.cardinality = 'Many' OR link_props.id IS NOT NULL\n            AND sp.computable IS NOT TRUE\n            AND sp.internal IS NOT TRUE\n        \"\"\"\n        ),\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_statistic\"),\n            query=\"\"\"\n        SELECT\n            starelid,\n            a.attnum as staattnum,\n            stainherit,\n            stanullfrac,\n            stawidth,\n            stadistinct,\n            stakind1,\n            stakind2,\n            stakind3,\n            stakind4,\n            stakind5,\n            staop1,\n            staop2,\n            staop3,\n            staop4,\n            staop5,\n            stacoll1,\n            stacoll2,\n            stacoll3,\n            stacoll4,\n            stacoll5,\n            stanumbers1,\n            stanumbers2,\n            stanumbers3,\n            stanumbers4,\n            stanumbers5,\n            NULL::real[] AS stavalues1,\n            NULL::real[] AS stavalues2,\n            NULL::real[] AS stavalues3,\n            NULL::real[] AS stavalues4,\n            NULL::real[] AS stavalues5,\n            s.tableoid, s.xmin, s.cmin, s.xmax, s.cmax, s.ctid\n        FROM pg_statistic s\n        JOIN edgedbsql_VER.pg_attribute_ext a ON (\n            a.attrelid = s.starelid AND a.attnum_internal = s.staattnum\n        )\n        \"\"\",\n        ),\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_statistic_ext\"),\n            query=\"\"\"\n        SELECT\n            oid,\n            stxrelid,\n            stxname,\n            stxnamespace,\n            stxowner,\n            stxstattarget,\n            stxkeys,\n            stxkind,\n            NULL::pg_node_tree as stxexprs,\n            tableoid, xmin, cmin, xmax, cmax, ctid\n        FROM pg_statistic_ext\n        \"\"\",\n        ),\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_statistic_ext_data\"),\n            query=\"\"\"\n        SELECT\n            stxoid,\n            stxdndistinct,\n            stxddependencies,\n            stxdmcv,\n            NULL::oid AS stxdexpr,\n            tableoid, xmin, cmin, xmax, cmax, ctid\n        FROM pg_statistic_ext_data\n        \"\"\",\n        ),\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_rewrite\"),\n            query=\"\"\"\n        SELECT pr.*, pr.tableoid, pr.xmin, pr.cmin, pr.xmax, pr.cmax, pr.ctid\n        FROM pg_rewrite pr\n        JOIN edgedbsql_VER.pg_class pn ON pr.ev_class = pn.oid\n        \"\"\",\n        ),\n\n        # HACK: Automatically generated cast function for ranges/multiranges\n        # was causing issues for pg_dump. So at the end of the day we opt for\n        # not exposing any casts at all here since there is no real reason for\n        # this compatibility layer that is read-only to have elaborate casts\n        # present.\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_cast\"),\n            query=\"\"\"\n        SELECT pc.*, pc.tableoid, pc.xmin, pc.cmin, pc.xmax, pc.cmax, pc.ctid\n        FROM pg_cast pc\n        WHERE FALSE\n        \"\"\",\n        ),\n        # Omit all funcitons for now.\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_proc\"),\n            query=\"\"\"\n        SELECT *, tableoid, xmin, cmin, xmax, cmax, ctid\n        FROM pg_proc\n        WHERE FALSE\n        \"\"\",\n        ),\n        # Omit all operators for now.\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_operator\"),\n            query=\"\"\"\n        SELECT *, tableoid, xmin, cmin, xmax, cmax, ctid\n        FROM pg_operator\n        WHERE FALSE\n        \"\"\",\n        ),\n        # Omit all triggers for now.\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_trigger\"),\n            query=\"\"\"\n        SELECT *, tableoid, xmin, cmin, xmax, cmax, ctid\n        FROM pg_trigger\n        WHERE FALSE\n        \"\"\",\n        ),\n        # Omit all subscriptions for now.\n        # This table is queried by pg_dump with COUNT(*) when user does not\n        # have permissions to access it. This should be allowed, but the\n        # view expands the query to all columns, which is not allowed.\n        # So we have to construct an empty view with correct signature that\n        # does not reference pg_subscription.\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_subscription\"),\n            query=\"\"\"\n        SELECT\n            NULL::oid AS oid,\n            NULL::oid AS subdbid,\n            NULL::name AS subname,\n            NULL::oid AS subowner,\n            NULL::boolean AS subenabled,\n            NULL::text AS subconninfo,\n            NULL::name AS subslotname,\n            NULL::text AS subsynccommit,\n            NULL::oid AS subpublications,\n            tableoid, xmin, cmin, xmax, cmax, ctid\n        FROM pg_namespace\n        WHERE FALSE\n        \"\"\",\n        ),\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_tables\"),\n            query=\"\"\"\n        SELECT\n            n.nspname AS schemaname,\n            c.relname AS tablename,\n            pg_get_userbyid(c.relowner) AS tableowner,\n            t.spcname AS tablespace,\n            c.relhasindex AS hasindexes,\n            c.relhasrules AS hasrules,\n            c.relhastriggers AS hastriggers,\n            c.relrowsecurity AS rowsecurity\n        FROM edgedbsql_VER.pg_class c\n        LEFT JOIN edgedbsql_VER.pg_namespace n ON n.oid = c.relnamespace\n        LEFT JOIN pg_tablespace t ON t.oid = c.reltablespace\n        WHERE c.relkind = ANY (ARRAY['r'::\"char\", 'p'::\"char\"])\n        \"\"\",\n        ),\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_views\"),\n            query=\"\"\"\n        SELECT\n            n.nspname AS schemaname,\n            c.relname AS viewname,\n            pg_get_userbyid(c.relowner) AS viewowner,\n            pg_get_viewdef(c.oid) AS definition\n        FROM edgedbsql_VER.pg_class c\n        LEFT JOIN edgedbsql_VER.pg_namespace n ON n.oid = c.relnamespace\n        WHERE c.relkind = 'v'::\"char\"\n        \"\"\",\n        ),\n        # Omit all descriptions (comments), becase all non-system comments\n        # are our internal implementation details.\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_description\"),\n            query=\"\"\"\n        SELECT\n            *,\n            tableoid, xmin, cmin, xmax, cmax, ctid\n        FROM pg_description\n        WHERE FALSE\n        \"\"\",\n        ),\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_settings\"),\n            query=\"\"\"\n            select * from edgedbsql_VER.pg_show_all_settings()\n        \"\"\",\n        ),\n        # A helper view for the `SHOW` SQL command.\n        # See also NormalizedPgSettingsView, InterpretConfigValueToJsonFunction\n        # as well as the Postgres C function ShowGUCOption.\n        trampoline.VersionedView(\n            name=(\"edgedbsql\", \"pg_settings_for_show\"),\n            query=r\"\"\"\n        SELECT\n            s.name AS name,\n            CASE\n                WHEN vartype = 'bool'\n                THEN (\n                    CASE\n                    WHEN lower(setting) = any(ARRAY['on', 'true', 'yes', '1'])\n                    THEN 'on'\n                    ELSE 'off'\n                    END\n                )\n\n                WHEN vartype = 'enum' OR vartype = 'string'\n                THEN setting\n\n                WHEN vartype = 'integer' OR vartype = 'real'\n                THEN (\n                    CASE\n                    WHEN setting::numeric > 0 AND unit.unit IS NOT NULL\n                    THEN (setting::numeric * unit.multiplier)::text || unit.unit\n                    ELSE setting\n                    END\n                )\n\n                ELSE\n                    edgedb_VER.raise(\n                        NULL::text,\n                        msg => (\n                            'unknown configuration type \"' ||\n                            COALESCE(vartype, '<NULL>') ||\n                            '\"'\n                        )\n                    )\n            END AS setting,\n            short_desc AS description\n\n        FROM\n            edgedbsql_VER.pg_settings AS s,\n\n            LATERAL (\n                SELECT regexp_match(\n                    s.unit, '^(\\d*)\\s*([a-zA-Z]{1,3})$') AS v\n            ) AS _unit,\n\n            LATERAL (\n                SELECT\n                    COALESCE(\n                        CASE\n                            WHEN _unit.v[1] = '' THEN 1\n                            ELSE _unit.v[1]::int\n                        END,\n                        1\n                    ) AS multiplier,\n                    COALESCE(_unit.v[2], '') AS unit\n            ) AS unit\n        \"\"\"\n        ),\n    ]\n\n    # We expose most of the views as empty tables, just to prevent errors when\n    # the tools do introspection.\n    # For the tables that it turns out are actually needed, we handcraft the\n    # views that expose the actual data.\n    # I've been cautious about exposing too much data, for example limiting\n    # pg_type to pg_catalog and pg_toast namespaces.\n    views: list[dbops.View] = []\n    views.extend(tables_and_columns)\n\n    for table_name, columns in sql_introspection.INFORMATION_SCHEMA.items():\n        if table_name in [\"tables\", \"columns\"]:\n            continue\n        views.append(\n            trampoline.VersionedView(\n                name=(\"edgedbsql\", table_name),\n                query=\"SELECT {} LIMIT 0\".format(\n                    \",\".join(\n                        f\"NULL::information_schema.{type} AS {name}\"\n                        for name, type, _ver_since in columns\n                    )\n                ),\n            )\n        )\n\n    PG_TABLES_SKIP = {\n        'pg_type',\n        'pg_attribute',\n        'pg_namespace',\n        'pg_class',\n        'pg_database',\n        'pg_proc',\n        'pg_operator',\n        'pg_pltemplate',\n        'pg_stats',\n        'pg_stats_ext_exprs',\n        'pg_statistic',\n        'pg_statistic_ext',\n        'pg_statistic_ext_data',\n        'pg_rewrite',\n        'pg_cast',\n        'pg_index',\n        'pg_constraint',\n        'pg_trigger',\n        'pg_subscription',\n        'pg_tables',\n        'pg_views',\n        'pg_description',\n        'pg_settings',\n    }\n\n    PG_TABLES_WITH_SYSTEM_COLS = {\n        'pg_aggregate',\n        'pg_am',\n        'pg_amop',\n        'pg_amproc',\n        'pg_attrdef',\n        'pg_attribute',\n        'pg_auth_members',\n        'pg_authid',\n        'pg_cast',\n        'pg_class',\n        'pg_collation',\n        'pg_constraint',\n        'pg_conversion',\n        'pg_database',\n        'pg_db_role_setting',\n        'pg_default_acl',\n        'pg_depend',\n        'pg_enum',\n        'pg_event_trigger',\n        'pg_extension',\n        'pg_foreign_data_wrapper',\n        'pg_foreign_server',\n        'pg_foreign_table',\n        'pg_index',\n        'pg_inherits',\n        'pg_init_privs',\n        'pg_language',\n        'pg_largeobject',\n        'pg_largeobject_metadata',\n        'pg_namespace',\n        'pg_opclass',\n        'pg_operator',\n        'pg_opfamily',\n        'pg_partitioned_table',\n        'pg_policy',\n        'pg_publication',\n        'pg_publication_rel',\n        'pg_range',\n        'pg_replication_origin',\n        'pg_rewrite',\n        'pg_seclabel',\n        'pg_sequence',\n        'pg_shdepend',\n        'pg_shdescription',\n        'pg_shseclabel',\n        'pg_statistic',\n        'pg_statistic_ext',\n        'pg_statistic_ext_data',\n        'pg_subscription_rel',\n        'pg_tablespace',\n        'pg_transform',\n        'pg_trigger',\n        'pg_ts_config',\n        'pg_ts_config_map',\n        'pg_ts_dict',\n        'pg_ts_parser',\n        'pg_ts_template',\n        'pg_type',\n        'pg_user_mapping',\n    }\n\n    SYSTEM_COLUMNS = ['tableoid', 'xmin', 'cmin', 'xmax', 'cmax', 'ctid']\n\n    def construct_pg_view(\n        table_name: str, backend_version: params.BackendVersion\n    ) -> Optional[dbops.View]:\n        pg_columns = sql_introspection.PG_CATALOG[table_name]\n\n        columns = []\n        has_columns = False\n        for c_name, c_typ, c_ver_since in pg_columns:\n            if c_ver_since <= backend_version.major:\n                columns.append('o.' + c_name)\n                has_columns = True\n            elif c_typ:\n                columns.append(f'NULL::{c_typ} as {c_name}')\n            else:\n                columns.append(f'NULL as {c_name}')\n        if not has_columns:\n            return None\n\n        if table_name in PG_TABLES_WITH_SYSTEM_COLS:\n            for c_name in SYSTEM_COLUMNS:\n                columns.append('o.' + c_name)\n\n        return trampoline.VersionedView(\n            name=(\"edgedbsql\", table_name),\n            query=f\"SELECT {','.join(columns)} FROM pg_catalog.{table_name} o\",\n        )\n\n    views.extend(pg_catalog_views)\n\n    for table_name in sql_introspection.PG_CATALOG.keys():\n        if table_name in PG_TABLES_SKIP:\n            continue\n        if v := construct_pg_view(table_name, backend_version):\n            views.append(v)\n\n    util_functions = [\n        # A 1:1 PL/pgSQL replication of the Postgres SplitIdentifierString\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'split_identifier_string'),\n            args=(\n                ('rawstring', 'text',),\n                ('separator', 'text',),\n            ),\n            returns=('text[]',),\n            language=\"plpgsql\",\n            volatility=\"immutable\",\n            text=r\"\"\"\nDECLARE\n    namelist text[] := '{}';\n    pos integer := 1;\n    len integer;\n    c char;\n    sep_char char;\n    curname text;\n    in_quote boolean;\n    db_encoding text := getdatabaseencoding();\nBEGIN\n    -- Initialization\n    IF length(separator) != 1 THEN\n        RAISE EXCEPTION 'Separator must be a single character';\n    END IF;\n    sep_char := substring(separator FROM 1 FOR 1);\n    len := length(rawstring);\n\n    -- Skip leading whitespace\n    WHILE pos <= len LOOP\n        c := substring(rawstring FROM pos FOR 1);\n        IF c IN (' ', '\\t', '\\n', '\\r', '\\f') THEN\n            pos := pos + 1;\n        ELSE\n            EXIT;\n        END IF;\n    END LOOP;\n\n    -- Allow empty string\n    IF pos > len THEN\n        RETURN namelist;\n    END IF;\n\n    -- At the top of the loop, we are at start of a new identifier.\n    LOOP\n        IF substring(rawstring FROM pos FOR 1) = '\"' THEN\n            -- Quoted name --- collapse quote-quote pairs, no downcasing\n            pos := pos + 1;\n            curname := '';\n            in_quote := TRUE;\n            WHILE pos <= len LOOP\n                c := substring(rawstring FROM pos FOR 1);\n                IF c = '\"' THEN\n                    IF pos < len\n                        AND substring(rawstring FROM pos + 1 FOR 1) = '\"'\n                    THEN\n                        -- Collapse adjacent quotes into one quote,\n                        -- and look again\n                        curname := curname || '\"';\n                        pos := pos + 2;\n                    ELSE\n                        -- Found end of quoted name\n                        pos := pos + 1;\n                        in_quote := FALSE;\n                        EXIT;\n                    END IF;\n                ELSE\n                    curname := curname || c;\n                    pos := pos + 1;\n                END IF;\n            END LOOP;\n\n            -- Mismatched quotes\n            IF in_quote THEN\n                RAISE EXCEPTION 'Unterminated quoted identifier';\n            END IF;\n\n        ELSE\n            -- Unquoted name --- extends to separator or whitespace\n            curname := '';\n            WHILE pos <= len LOOP\n                c := substring(rawstring FROM pos FOR 1);\n                IF c = sep_char OR c IN (' ', '\\t', '\\n', '\\r', '\\f') THEN\n                    EXIT;\n                END IF;\n                curname := curname || c;\n                pos := pos + 1;\n            END LOOP;\n\n            IF curname = '' THEN\n                RAISE EXCEPTION 'Empty unquoted identifier';\n            END IF;\n\n            -- Downcase the identifier\n            curname := lower(curname);\n        END IF;\n\n        -- Truncate name if it's overlength\n        IF octet_length(curname) > 63 THEN\n            RAISE NOTICE 'identifier \"%\" will be truncated', curname;\n            curname := convert_from(\n                substring(convert_to(curname, db_encoding) FROM 1 FOR 63),\n                db_encoding\n            );\n        END IF;\n\n        -- Finished isolating current name --- add it to list\n        namelist := array_append(namelist, curname);\n\n        -- Skip trailing whitespace\n        WHILE pos <= len LOOP\n            c := substring(rawstring FROM pos FOR 1);\n            IF c IN (' ', '\\t', '\\n', '\\r', '\\f') THEN\n                pos := pos + 1;\n            ELSE\n                EXIT;\n            END IF;\n        END LOOP;\n\n        IF pos > len THEN\n            EXIT; -- End of string\n        ELSIF substring(rawstring FROM pos FOR 1) = sep_char THEN\n            pos := pos + 1;\n            -- Skip leading whitespace for next\n            WHILE pos <= len LOOP\n                c := substring(rawstring FROM pos FOR 1);\n                IF c IN (' ', '\\t', '\\n', '\\r', '\\f') THEN\n                    pos := pos + 1;\n                ELSE\n                    EXIT;\n                END IF;\n            END LOOP;\n        ELSE\n            RAISE EXCEPTION 'Invalid character at position %: \"%\"', pos, c;\n        END IF;\n    END LOOP;\n\n    RETURN namelist;\nEND;\n            \"\"\",\n        ),\n        # current_schemas() is an emulation of Postgres current_schemas(),\n        # fetch_search_path() and recomputeNamespacePath() internal functions.\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'current_schemas'),\n            args=(\n                ('include_implicit', 'bool',),\n            ),\n            returns=('name[]',),\n            language=\"plpgsql\",\n            volatility=\"stable\",\n            text=\"\"\"\nDECLARE\n    search_path_str text;\n    schema_list text[];\n    rv name[] := '{}'::name[];\n    is_valid_namespace boolean;\n    has_pg_catalog boolean;\n    resolved_schema text;\nBEGIN\n    -- Get the current search_path from the emulated pg_settings view\n    SELECT COALESCE(setting, 'public') INTO search_path_str\n    FROM edgedbsql_VER.pg_settings\n    WHERE name = 'search_path';\n\n    -- Split using our custom function with comma separator\n    schema_list := edgedbsql_VER.split_identifier_string(search_path_str, ',');\n\n    -- Handle implicit schemas if requested\n    IF include_implicit THEN\n        -- Temporary schema is not supported yet\n\n        -- Check if pg_catalog is already present\n        has_pg_catalog := 'pg_catalog' = ANY(schema_list);\n\n        -- Add pg_catalog if not present and GUC variables allow it\n        IF NOT has_pg_catalog THEN\n            rv := array_append(rv, 'pg_catalog'::name);\n        END IF;\n    END IF;\n\n    -- Process each schema element\n    FOR i IN 1..array_length(schema_list, 1) LOOP\n        -- Handle $user substitution\n        IF schema_list[i] = '$user' THEN\n            resolved_schema := current_user;\n        ELSE\n            resolved_schema := schema_list[i];\n        END IF;\n\n        -- Check existence in namespace catalog\n        IF EXISTS (\n            SELECT 1\n            FROM edgedbsql_VER.pg_namespace\n            WHERE nspname = resolved_schema\n        ) THEN\n            rv := array_append(rv, resolved_schema::name);\n        END IF;\n    END LOOP;\n\n    RETURN rv;\nEND;\n            \"\"\",\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'to_regclass'),\n            args=(\n                ('name_or_oid', 'text',),\n            ),\n            returns=('regclass',),\n            language=\"plpgsql\",\n            text=\"\"\"\nDECLARE\n    parts text[];\n    result regclass;\nBEGIN\n    IF name_or_oid = '-' THEN\n        RETURN 0::regclass;\n    END IF;\n\n    IF name_or_oid ~ '^[0-9]+$' THEN\n        RETURN name_or_oid::oid::regclass;\n    END IF;\n\n    parts := parse_ident(name_or_oid);\n    IF array_length(parts, 1) = 1 THEN\n        SELECT pc.oid::regclass INTO result\n        FROM unnest(edgedbsql_VER.current_schemas(true))\n            WITH ORDINALITY AS ns(nspname, ord)\n        JOIN edgedbsql_VER.pg_namespace pn\n            USING (nspname)\n        JOIN edgedbsql_VER.pg_class pc\n            ON pn.oid = pc.relnamespace\n        WHERE pc.relname = parts[1]\n        ORDER BY ns.ord\n        LIMIT 1;\n    ELSEIF array_length(parts, 1) = 2 THEN\n        SELECT pc.oid::regclass INTO result\n        FROM edgedbsql_VER.pg_class pc\n        JOIN edgedbsql_VER.pg_namespace pn\n            ON pn.oid = pc.relnamespace\n        WHERE relname = parts[2] AND nspname = parts[1];\n    ELSE\n        RAISE EXCEPTION\n            'improper relation name (too many dotted names): %', name_or_oid;\n    END IF;\n    RETURN result;\nEND;\n            \"\"\"\n        ),\n        # Unlike pg_catalog.to_regclass(), edgedbsql.to_regclass() also takes\n        # numeric parameters to support compiled `::regclass` typecasting.\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'to_regclass'),\n            args=(\n                ('oid', 'integer',),\n            ),\n            returns=('regclass',),\n            volatility=\"stable\",\n            text=\"\"\"\n                SELECT oid::regclass\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'to_regclass'),\n            args=(\n                ('oid', 'smallint',),\n            ),\n            returns=('regclass',),\n            volatility=\"stable\",\n            text=\"\"\"\n                SELECT oid::regclass\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'to_regclass'),\n            args=(\n                ('oid', 'bigint',),\n            ),\n            returns=('regclass',),\n            volatility=\"stable\",\n            text=\"\"\"\n                SELECT oid::regclass\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'to_regclass'),\n            args=(\n                ('oid', 'oid',),\n            ),\n            returns=('regclass',),\n            volatility=\"stable\",\n            text=\"\"\"\n                SELECT oid::regclass\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'has_database_privilege'),\n            args=(\n                ('database_name', 'text'),\n                ('privilege', 'text'),\n            ),\n            returns=('bool',),\n            text=\"\"\"\n                SELECT has_database_privilege(oid, privilege)\n                FROM edgedbsql_VER.pg_database\n                WHERE datname = database_name\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'has_database_privilege'),\n            args=(\n                ('database_oid', 'oid'),\n                ('privilege', 'text'),\n            ),\n            returns=('bool',),\n            text=\"\"\"\n                SELECT has_database_privilege(database_oid, privilege)\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'has_schema_privilege'),\n            args=(\n                ('schema_name', 'text'),\n                ('privilege', 'text'),\n            ),\n            returns=('bool',),\n            text=\"\"\"\n            SELECT COALESCE((\n                SELECT has_schema_privilege(oid, privilege)\n                FROM edgedbsql_VER.pg_namespace\n                WHERE nspname = schema_name\n            ), TRUE);\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'has_schema_privilege'),\n            args=(\n                ('schema_oid', 'oid'),\n                ('privilege', 'text'),\n            ),\n            returns=('bool',),\n            text=\"\"\"\n                SELECT COALESCE(\n                    has_schema_privilege(schema_oid, privilege), TRUE\n                )\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'has_table_privilege'),\n            args=(\n                ('table_name', 'text'),\n                ('privilege', 'text'),\n            ),\n            returns=('bool',),\n            text=\"\"\"\n                SELECT has_table_privilege(\n                    edgedbsql_VER.to_regclass(table_name), privilege)\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'has_table_privilege'),\n            args=(\n                ('table_oid', 'oid'),\n                ('privilege', 'text'),\n            ),\n            returns=('bool',),\n            text=\"\"\"\n                SELECT has_table_privilege(table_oid, privilege)\n            \"\"\"\n        ),\n\n        # pg_catalog.has_column_privilege will return NULL for computed and\n        # static columns. So we COALESCE to TRUE.\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'has_column_privilege'),\n            args=(\n                ('tbl', 'oid'),\n                ('col', 'smallint'),\n                ('privilege', 'text'),\n            ),\n            returns=('bool',),\n            text=\"\"\"\n              SELECT COALESCE((\n                SELECT has_column_privilege(tbl, col, privilege)\n              ), TRUE)\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'has_column_privilege'),\n            args=(\n                ('tbl', 'text'),\n                ('col', 'smallint'),\n                ('privilege', 'text'),\n            ),\n            returns=('bool',),\n            text=\"\"\"\n              SELECT COALESCE((\n                SELECT has_column_privilege(\n                    edgedbsql_VER.to_regclass(tbl), col, privilege)\n              ), TRUE)\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'has_column_privilege'),\n            args=(\n                ('tbl', 'oid'),\n                ('col', 'text'),\n                ('privilege', 'text'),\n            ),\n            returns=('bool',),\n            text=\"\"\"\n              SELECT COALESCE((\n                SELECT has_column_privilege(tbl, attnum_internal, privilege)\n                FROM edgedbsql_VER.pg_attribute_ext pa\n                WHERE attrelid = tbl AND attname = col\n              ), TRUE)\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'has_column_privilege'),\n            args=(\n                ('tbl', 'text'),\n                ('col', 'text'),\n                ('privilege', 'text'),\n            ),\n            returns=('bool',),\n            text=\"\"\"\n              SELECT COALESCE((\n                SELECT has_column_privilege(pc.oid, attnum_internal, privilege)\n                FROM edgedbsql_VER.pg_attribute_ext pa,\n                LATERAL (SELECT edgedbsql_VER.to_regclass(tbl) AS oid) pc\n                WHERE pa.attrelid = pc.oid AND pa.attname = col\n              ), TRUE)\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'has_any_column_privilege'),\n            args=(\n                ('tbl', 'oid'),\n                ('privilege', 'text'),\n            ),\n            returns=('bool',),\n            text=\"\"\"\n                SELECT has_any_column_privilege(tbl, privilege)\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'has_any_column_privilege'),\n            args=(\n                ('tbl', 'text'),\n                ('privilege', 'text'),\n            ),\n            returns=('bool',),\n            text=\"\"\"\n                SELECT has_any_column_privilege(\n                    edgedbsql_VER.to_regclass(tbl), privilege)\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', '_pg_truetypid'),\n            args=(\n                ('att', ('edgedbsql_VER', 'pg_attribute')),\n                ('typ', ('edgedbsql_VER', 'pg_type')),\n            ),\n            returns=('oid',),\n            volatility='IMMUTABLE',\n            strict=True,\n            text=\"\"\"\n                SELECT CASE\n                    WHEN typ.typtype = 'd' THEN typ.typbasetype\n                    ELSE att.atttypid\n                END\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', '_pg_truetypmod'),\n            args=(\n                ('att', ('edgedbsql_VER', 'pg_attribute')),\n                ('typ', ('edgedbsql_VER', 'pg_type')),\n            ),\n            returns=('int4',),\n            volatility='IMMUTABLE',\n            strict=True,\n            text=\"\"\"\n                SELECT CASE\n                    WHEN typ.typtype = 'd' THEN typ.typtypmod\n                    ELSE att.atttypmod\n                END\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=('edgedbsql', 'pg_table_is_visible'),\n            args=[\n                ('id', ('oid',)),\n                ('search_path', ('text[]',)),\n            ],\n            returns=('bool',),\n            volatility='stable',\n            text=r'''\n                SELECT pc.relnamespace IN (\n                    SELECT oid\n                    FROM edgedbsql_VER.pg_namespace pn\n                    WHERE pn.nspname IN (select * from unnest(search_path))\n                )\n                FROM edgedbsql_VER.pg_class pc\n                WHERE id = pc.oid\n            '''\n        ),\n        trampoline.VersionedFunction(\n            # Used instead of pg_catalog.format_type in pg_dump.\n            name=('edgedbsql', '_format_type'),\n            args=[\n                ('typeoid', ('oid',)),\n                ('typemod', ('integer',)),\n            ],\n            returns=('text',),\n            volatility='STABLE',\n            text=r'''\n                SELECT\n                    CASE\n                        -- arrays\n                        WHEN t.typcategory = 'A' THEN (\n                            SELECT\n                                quote_ident(nspname) || '.' ||\n                                quote_ident(el.typname) || tm.mod || '[]'\n                            FROM edgedbsql_VER.pg_namespace\n                            WHERE oid = el.typnamespace\n                        )\n\n                        -- composite (tuples) and types in irregular schemas\n                        WHEN (\n                            t.typcategory = 'C' OR COALESCE(tn.nspname IN (\n                                'edgedb', 'edgedbt', 'edgedbpub', 'edgedbstd',\n                                'edgedb_VER', 'edgedbstd_VER'\n                            ), TRUE)\n                        ) THEN (\n                            SELECT\n                                quote_ident(nspname) || '.' ||\n                                quote_ident(t.typname) || tm.mod\n                            FROM edgedbsql_VER.pg_namespace\n                            WHERE oid = t.typnamespace\n                        )\n                        ELSE format_type(typeoid, typemod)\n                    END\n                FROM edgedbsql_VER.pg_type t\n                LEFT JOIN pg_namespace tn ON t.typnamespace = tn.oid\n                LEFT JOIN edgedbsql_VER.pg_type el ON t.typelem = el.oid\n\n                CROSS JOIN (\n                    SELECT\n                        CASE\n                            WHEN typemod >= 0 THEN '(' || typemod::text || ')'\n                            ELSE ''\n                        END AS mod\n                ) as tm\n\n                WHERE t.oid = typeoid\n            ''',\n        ),\n        trampoline.VersionedFunction(\n            name=(\"edgedbsql\", \"pg_get_constraintdef\"),\n            args=[\n                ('conid', ('oid',)),\n            ],\n            returns=('text',),\n            volatility='stable',\n            text=r\"\"\"\n                -- Wrap in a subquery SELECT so that we get a clear failure\n                -- if something is broken and this returns multiple rows.\n                -- (By default it would silently return the first.)\n                SELECT (\n                SELECT CASE\n                    WHEN contype = 'p' THEN\n                    'PRIMARY KEY(' || (\n                        SELECT string_agg('\"' || attname || '\"', ', ')\n                        FROM edgedbsql_VER.pg_attribute\n                        WHERE attrelid = conrelid AND attnum = ANY(conkey)\n                    ) || ')'\n                    WHEN contype = 'f' THEN\n                    'FOREIGN KEY (\"' || (\n                        SELECT attname\n                        FROM edgedbsql_VER.pg_attribute\n                        WHERE attrelid = conrelid AND attnum = ANY(conkey)\n                    ) || '\")' || ' REFERENCES \"'\n                    || pn.nspname || '\".\"' || pc.relname || '\"(id)'\n                    ELSE ''\n                    END\n                FROM edgedbsql_VER.pg_constraint con\n                LEFT JOIN edgedbsql_VER.pg_class_tables pc ON pc.oid = confrelid\n                LEFT JOIN edgedbsql_VER.pg_namespace pn\n                  ON pc.relnamespace = pn.oid\n                WHERE con.oid = conid\n                )\n            \"\"\"\n        ),\n        trampoline.VersionedFunction(\n            name=(\"edgedbsql\", \"pg_get_constraintdef\"),\n            args=[\n                ('conid', ('oid',)),\n                ('pretty', ('bool',)),\n            ],\n            returns=('text',),\n            volatility='stable',\n            text=r\"\"\"\n                SELECT pg_get_constraintdef(conid)\n            \"\"\"\n        ),\n    ]\n\n    return (\n        [cast(dbops.Command, dbops.CreateFunction(uuid_to_oid))]\n        + [dbops.CreateView(virtual_tables)]\n        + [\n            cast(dbops.Command, dbops.CreateFunction(long_name)),\n            cast(dbops.Command, dbops.CreateFunction(type_rename)),\n            cast(dbops.Command, dbops.CreateFunction(namespace_rename)),\n            cast(dbops.Command, dbops.CreateFunction(fe_pg_settings)),\n        ]\n        + [dbops.CreateView(view) for view in views]\n        + [dbops.CreateFunction(func) for func in util_functions]\n    )\n\n\n@functools.cache\ndef generate_sql_information_schema_refresh(\n    backend_version: params.BackendVersion\n) -> dbops.Command:\n    refresh = dbops.CommandGroup()\n    for command in _generate_sql_information_schema(backend_version):\n        if (\n            isinstance(command, dbops.CreateView)\n            and command.view.materialized\n        ):\n            refresh.add_command(dbops.Query(\n                text=f'REFRESH MATERIALIZED VIEW {q(*command.view.name)}'\n            ))\n    return refresh\n\n\nclass ObjectAncestorsView(trampoline.VersionedView):\n    \"\"\"A trampolined and explicit version of _SchemaObjectType__ancestors\"\"\"\n\n    query = r'''\n        SELECT source, target, index\n        FROM edgedb_VER.\"_SchemaObjectType__ancestors\"\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_object_ancestors'),\n            query=self.query,\n        )\n\n\nclass LinksView(trampoline.VersionedView):\n    \"\"\"A trampolined and explicit version of _SchemaLink\"\"\"\n\n    query = r'''\n        SELECT id, name, source, target\n        FROM edgedb_VER.\"_SchemaLink\"\n    '''\n\n    def __init__(self) -> None:\n        super().__init__(\n            name=('edgedb', '_schema_links'),\n            query=self.query,\n        )\n\n\ndef get_config_type_views(\n    schema: s_schema.Schema,\n    conf: s_objtypes.ObjectType,\n    scope: Optional[qltypes.ConfigScope],\n    existing_view_columns: Optional[dict[str, list[str]]]=None,\n) -> dbops.CommandGroup:\n    commands = dbops.CommandGroup()\n\n    cfg_views, _ = _generate_config_type_view(\n        schema,\n        conf,\n        scope=scope,\n        path=[],\n        rptr=None,\n        existing_view_columns=existing_view_columns,\n    )\n    commands.add_commands([\n        dbops.CreateView(\n            (trampoline.VersionedView if tn[0] == 'edgedbstd' else dbops.View)(\n                name=tn, query=trampoline.fixup_query(q)\n            ),\n            or_replace=True,\n        )\n        for tn, q in cfg_views\n    ])\n\n    return commands\n\n\ndef generate_drop_views(\n    group: Sequence[dbops.Command | trampoline.Trampoline],\n    preblock: dbops.PLBlock,\n) -> None:\n    for cv in reversed(list(group)):\n        dv: Any\n        if isinstance(cv, dbops.CreateView):\n            # We try deleting both a MATERIALIZED and not materialized\n            # version, since that allows us to switch between them\n            # more easily.\n            dv = dbops.CommandGroup()\n            dv.add_command(dbops.DropView(\n                cv.view.name,\n                conditions=[dbops.ViewExists(cv.view.name)],\n            ))\n            dv.add_command(dbops.DropView(\n                cv.view.name,\n                conditions=[dbops.ViewExists(cv.view.name, materialized=True)],\n                materialized=True,\n            ))\n        elif isinstance(cv, dbops.CreateFunction):\n            dv = dbops.DropFunction(\n                cv.function.name,\n                args=cv.function.args or (),\n                has_variadic=bool(cv.function.has_variadic),\n                if_exists=True,\n            )\n        elif isinstance(cv, trampoline.Trampoline):\n            dv = cv.drop()\n        else:\n            raise AssertionError(f'unsupported support view command {cv}')\n        dv.generate(preblock)\n\n\ndef get_config_views(\n    schema: s_schema.Schema,\n    existing_view_columns: Optional[dict[str, list[str]]]=None,\n) -> dbops.CommandGroup:\n    commands = dbops.CommandGroup()\n\n    conf = schema.get('cfg::Config', type=s_objtypes.ObjectType)\n    commands.add_command(\n        get_config_type_views(\n            schema, conf, scope=None,\n            existing_view_columns=existing_view_columns,\n        ),\n    )\n\n    conf = schema.get('cfg::InstanceConfig', type=s_objtypes.ObjectType)\n    commands.add_command(\n        get_config_type_views(\n            schema, conf, scope=qltypes.ConfigScope.INSTANCE,\n            existing_view_columns=existing_view_columns,\n        ),\n    )\n\n    conf = schema.get('cfg::DatabaseConfig', type=s_objtypes.ObjectType)\n    commands.add_command(\n        get_config_type_views(\n            schema, conf, scope=qltypes.ConfigScope.DATABASE,\n            existing_view_columns=existing_view_columns,\n        ),\n    )\n\n    return commands\n\n\ndef get_synthetic_type_views(\n    schema: s_schema.Schema,\n    backend_params: params.BackendRuntimeParams,\n) -> dbops.CommandGroup:\n    commands = dbops.CommandGroup()\n\n    commands.add_command(get_config_views(schema))\n\n    for dbview in _generate_branch_views(schema):\n        commands.add_command(dbops.CreateView(dbview, or_replace=True))\n\n    for extview in _generate_extension_views(schema):\n        commands.add_command(dbops.CreateView(extview, or_replace=True))\n    for extview in _generate_extension_migration_views(schema):\n        commands.add_command(dbops.CreateView(extview, or_replace=True))\n\n    if backend_params.has_create_role:\n        role_views = _generate_role_views(schema)\n    else:\n        role_views = _generate_single_role_views(schema)\n    for roleview in role_views:\n        commands.add_command(dbops.CreateView(roleview, or_replace=True))\n\n    for verview in _generate_schema_ver_views(schema):\n        commands.add_command(dbops.CreateView(verview, or_replace=True))\n\n    if backend_params.has_stat_statements:\n        for stats_view in _generate_stats_views(schema):\n            commands.add_command(dbops.CreateView(stats_view, or_replace=True))\n        commands.add_command(\n            dbops.CreateFunction(\n                ResetQueryStatsFunction(True), or_replace=True\n            )\n        )\n\n    return commands\n\n\ndef _get_wrapper_views() -> dbops.CommandGroup:\n    # Create some trampolined wrapper views around _Schema types we need\n    # to reference from functions.\n    wrapper_commands = dbops.CommandGroup()\n    wrapper_commands.add_command(\n        dbops.CreateView(ObjectAncestorsView(), or_replace=True))\n    wrapper_commands.add_command(\n        dbops.CreateView(LinksView(), or_replace=True))\n    return wrapper_commands\n\n\ndef get_support_views(\n    schema: s_schema.Schema,\n    backend_params: params.BackendRuntimeParams,\n) -> tuple[dbops.CommandGroup, list[trampoline.Trampoline]]:\n    commands = dbops.CommandGroup()\n\n    schema_alias_views = _generate_schema_alias_views(\n        schema, s_name.UnqualName('schema'))\n\n    InhObject = schema.get(\n        'schema::InheritingObject', type=s_objtypes.ObjectType)\n    InhObject__ancestors = InhObject.getptr(\n        schema, s_name.UnqualName('ancestors'), type=s_links.Link)\n    schema_alias_views.append(\n        _generate_schema_alias_view(schema, InhObject__ancestors))\n\n    ObjectType = schema.get(\n        'schema::ObjectType', type=s_objtypes.ObjectType)\n    ObjectType__ancestors = ObjectType.getptr(\n        schema, s_name.UnqualName('ancestors'), type=s_links.Link)\n    schema_alias_views.append(\n        _generate_schema_alias_view(schema, ObjectType__ancestors))\n\n    for alias_view in schema_alias_views:\n        commands.add_command(dbops.CreateView(alias_view, or_replace=True))\n\n    synthetic_types = get_synthetic_type_views(schema, backend_params)\n    commands.add_command(synthetic_types)\n\n    wrapper_commands = _get_wrapper_views()\n    commands.add_command(wrapper_commands)\n\n    sys_alias_views = _generate_schema_alias_views(\n        schema, s_name.UnqualName('sys'))\n\n    # Include sys::Role::member_of and sys::Role::permissions\n    # to support DescribeRolesAsDDLFunction\n    SysRole = schema.get(\n        'sys::Role', type=s_objtypes.ObjectType)\n    SysRole__member_of = SysRole.getptr(\n        schema, s_name.UnqualName('member_of'))\n    SysRole__permissions = SysRole.getptr(\n        schema, s_name.UnqualName('permissions'))\n    SysRole__branches = SysRole.getptr(\n        schema, s_name.UnqualName('branches'))\n    sys_alias_views.append(\n        _generate_schema_alias_view(schema, SysRole__member_of)\n    )\n    sys_alias_views.append(\n        _generate_schema_alias_view(schema, SysRole__permissions)\n    )\n    sys_alias_views.append(\n        _generate_schema_alias_view(schema, SysRole__branches)\n    )\n\n    for alias_view in sys_alias_views:\n        commands.add_command(dbops.CreateView(alias_view, or_replace=True))\n\n    commands.add_commands(\n        _generate_sql_information_schema(\n            backend_params.instance_params.version\n        )\n    )\n\n    # The synthetic type views (cfg::, sys::) need to be trampolined\n    trampolines = []\n    trampolines.extend(trampoline_command(synthetic_types))\n    trampolines.extend(trampoline_command(wrapper_commands))\n\n    return commands, trampolines\n\n\nasync def generate_support_views(\n    conn: PGConnection,\n    schema: s_schema.Schema,\n    backend_params: params.BackendRuntimeParams,\n) -> list[trampoline.Trampoline]:\n    commands, trampolines = get_support_views(schema, backend_params)\n    block = dbops.PLTopBlock()\n    commands.generate(block)\n    await _execute_block(conn, block)\n    return trampolines\n\n\nasync def generate_support_functions(\n    conn: PGConnection,\n    schema: s_schema.Schema,\n) -> list[trampoline.Trampoline]:\n    commands = dbops.CommandGroup()\n\n    cmds = [\n        dbops.CreateFunction(GetPgTypeForEdgeDBTypeFunction2(),\n                             or_replace=True),\n        dbops.CreateFunction(IssubclassFunction()),\n        dbops.CreateFunction(IssubclassFunction2()),\n        dbops.CreateFunction(GetSchemaObjectNameFunction()),\n        dbops.CreateFunction(ApproximateCount(), or_replace=True),\n    ]\n    commands.add_commands(cmds)\n\n    block = dbops.PLTopBlock()\n    commands.generate(block)\n    await _execute_block(conn, block)\n    return trampoline_functions(cmds)\n\n\ndef _get_regenerated_config_support_functions(\n    config_spec: edbconfig.Spec,\n) -> dbops.CommandGroup:\n    # Regenerate functions dependent on config spec.\n    commands = dbops.CommandGroup()\n\n    funcs = [\n        ApplySessionConfigFunction(config_spec),\n        PostgresJsonConfigValueToFrontendConfigValueFunction(config_spec),\n    ]\n\n    cmds = [dbops.CreateFunction(func, or_replace=True) for func in funcs]\n    commands.add_commands(cmds)\n\n    return commands\n\n\nasync def regenerate_config_support_functions(\n    conn: PGConnection,\n    config_spec: edbconfig.Spec,\n) -> None:\n    # Regenerate functions dependent on config spec.\n    commands = _get_regenerated_config_support_functions(config_spec)\n    block = dbops.PLTopBlock()\n    commands.generate(block)\n    await _execute_block(conn, block)\n\n\nasync def generate_more_support_functions(\n    conn: PGConnection,\n    compiler: edbcompiler.Compiler,\n    schema: s_schema.Schema,\n    testmode: bool,\n) -> list[trampoline.Trampoline]:\n    commands = dbops.CommandGroup()\n\n    cmds = [\n        dbops.CreateFunction(\n            DescribeRolesAsDDLFunction(schema), or_replace=True\n        ),\n        dbops.CreateFunction(\n            AllRoleMembershipsFunction(schema), or_replace=True\n        ),\n        dbops.CreateFunction(GetSequenceBackendNameFunction()),\n        dbops.CreateFunction(DumpSequencesFunction()),\n    ]\n    commands.add_commands(cmds)\n\n    block = dbops.PLTopBlock()\n    commands.generate(block)\n    await _execute_block(conn, block)\n    return trampoline_functions(cmds)\n\n\ndef _build_key_source(\n    schema: s_schema.Schema,\n    exc_props: Iterable[s_pointers.Pointer],\n    rptr: Optional[s_pointers.Pointer],\n    source_idx: str,\n) -> str:\n    if exc_props:\n        restargets = []\n        for prop in exc_props:\n            pname = prop.get_shortname(schema).name\n            restarget = f'(q{source_idx}.val)->>{ql(pname)}'\n            restargets.append(restarget)\n\n        targetlist = ','.join(restargets)\n\n        keysource = f'''\n            (SELECT\n                ARRAY[{targetlist}] AS key\n            ) AS k{source_idx}'''\n    else:\n        assert rptr is not None\n        rptr_name = rptr.get_shortname(schema).name\n        keysource = f'''\n            (SELECT\n                ARRAY[\n                    (CASE WHEN q{source_idx}.val = 'null'::jsonb\n                     THEN NULL\n                     ELSE {ql(rptr_name)}\n                     END)\n                ] AS key\n            ) AS k{source_idx}'''\n\n    return keysource\n\n\ndef _build_key_expr(\n    key_components: list[str],\n    versioned: bool,\n) -> str:\n    prefix = 'edgedb_VER' if versioned else 'edgedb'\n    key_expr = ' || '.join(key_components)\n    final_keysource = f'''\n        (SELECT\n            (CASE WHEN array_position(q.v, NULL) IS NULL\n             THEN\n                 {prefix}.uuid_generate_v5(\n                     '{DATABASE_ID_NAMESPACE}'::uuid,\n                     array_to_string(q.v, ';')\n                 )\n             ELSE NULL\n             END) AS key\n         FROM\n            (SELECT {key_expr} AS v) AS q\n        )'''\n\n    return final_keysource\n\n\ndef _build_data_source(\n    schema: s_schema.Schema,\n    rptr: s_pointers.Pointer,\n    source_idx: int,\n    *,\n    always_array: bool = False,\n    alias: Optional[str] = None,\n) -> str:\n\n    rptr_name = rptr.get_shortname(schema).name\n    rptr_card = rptr.get_cardinality(schema)\n    rptr_multi = rptr_card.is_multi()\n\n    if alias is None:\n        alias = f'q{source_idx + 1}'\n    else:\n        alias = f'q{alias}'\n\n    if rptr_multi:\n        sourceN = f'''\n            (SELECT jel.val\n                FROM\n                jsonb_array_elements(\n                    (q{source_idx}.val)->{ql(rptr_name)}) AS jel(val)\n            ) AS {alias}'''\n    else:\n        proj = '[0]' if always_array else ''\n        sourceN = f'''\n            (SELECT\n                (q{source_idx}.val){proj}->{ql(rptr_name)} AS val\n            ) AS {alias}'''\n\n    return sourceN\n\n\ndef _escape_like(s: str) -> str:\n    return s.replace('\\\\', '\\\\\\\\').replace('%', '\\\\%').replace('_', '\\\\_')\n\n\ndef _generate_config_type_view(\n    schema: s_schema.Schema,\n    stype: s_objtypes.ObjectType,\n    *,\n    scope: Optional[qltypes.ConfigScope],\n    path: list[tuple[s_pointers.Pointer, list[s_pointers.Pointer]]],\n    rptr: Optional[s_pointers.Pointer],\n    existing_view_columns: Optional[dict[str, list[str]]],\n    override_exclusive_props: Optional[list[s_pointers.Pointer]] = None,\n    _memo: Optional[set[s_obj.Object]] = None,\n) -> tuple[\n    list[tuple[tuple[str, str], str]],\n    list[s_pointers.Pointer],\n]:\n    X = xdedent.escape\n\n    exc = schema.get('std::exclusive', type=s_constr.Constraint)\n\n    if scope is not None:\n        if scope is qltypes.ConfigScope.INSTANCE:\n            max_source = \"'system override'\"\n        elif scope is qltypes.ConfigScope.DATABASE:\n            max_source = \"'database'\"\n        else:\n            raise AssertionError(f'unexpected config scope: {scope!r}')\n    else:\n        max_source = 'NULL'\n\n    if _memo is None:\n        _memo = set()\n\n    _memo.add(stype)\n\n    views = []\n\n    sources = []\n\n    ext_cfg = schema.get('cfg::ExtensionConfig', type=s_objtypes.ObjectType)\n    is_ext_cfg = stype.issubclass(schema, ext_cfg)\n    if is_ext_cfg:\n        rptr = None\n    is_rptr_ext_cfg = False\n    # For extension configs, we want to use the trampolined version,\n    # since we know it must exist already and don't want to have to\n    # recreate the views on update.\n    versioned = not is_ext_cfg or stype == ext_cfg\n    prefix = 'edgedb_VER' if versioned else 'edgedb'\n\n    if not path:\n        if is_ext_cfg:\n            # Extension configs get one object per scope.\n            cfg_name = str(stype.get_name(schema))\n\n            escaped_name = _escape_like(cfg_name)\n            source0 = f'''\n                (SELECT\n                    (SELECT jsonb_object_agg(\n                      substr(name, {len(cfg_name) + 3}), value) AS val\n                    FROM {prefix}._read_sys_config(\n                      NULL, scope::edgedb._sys_config_source_t) cfg\n                    WHERE name LIKE {ql(escaped_name + '%')}\n                    ) AS val, scope::text AS scope, scope_id AS scope_id\n                    FROM (VALUES\n                        (NULL, '{CONFIG_ID[None]}'::uuid),\n                        ('database',\n                         '{CONFIG_ID[qltypes.ConfigScope.DATABASE]}'::uuid)\n                    ) AS s(scope, scope_id)\n                ) AS q0\n            '''\n        elif rptr is None:\n            # This is the root config object.\n            source0 = f'''\n                (SELECT jsonb_object_agg(name, value) AS val\n                FROM {prefix}._read_sys_config(NULL, {max_source}) cfg)\n                AS q0'''\n        else:\n            rptr_name = rptr.get_shortname(schema).name\n            rptr_source = not_none(rptr.get_source(schema))\n            is_rptr_ext_cfg = rptr_source.issubclass(schema, ext_cfg)\n            if is_rptr_ext_cfg:\n                versioned = False\n                prefix = 'edgedb'\n\n                cfg_name = str(rptr_source.get_name(schema)) + '::' + rptr_name\n                escaped_name = _escape_like(cfg_name)\n\n                source0 = f'''\n                    (SELECT el.val AS val, s.scope::text AS scope,\n                            s.scope_id AS scope_id\n                     FROM (VALUES\n                         (NULL, '{CONFIG_ID[None]}'::uuid),\n                         ('database',\n                          '{CONFIG_ID[qltypes.ConfigScope.DATABASE]}'::uuid)\n                     ) AS s(scope, scope_id),\n                     LATERAL (\n                         SELECT (value::jsonb) AS val\n                         FROM {prefix}._read_sys_config(\n                           NULL, scope::edgedb._sys_config_source_t) cfg\n                         WHERE name LIKE {ql(escaped_name + '%')}\n                     ) AS cfg,\n                     LATERAL jsonb_array_elements(cfg.val) AS el(val)\n                    ) AS q0\n                '''\n\n            else:\n                source0 = f'''\n                    (SELECT el.val\n                     FROM\n                        (SELECT (value::jsonb) AS val\n                        FROM {prefix}._read_sys_config(NULL, {max_source})\n                        WHERE name = {ql(rptr_name)}) AS cfg,\n                        LATERAL jsonb_array_elements(cfg.val) AS el(val)\n                    ) AS q0'''\n\n        sources.append(source0)\n        key_start = 0\n    else:\n        # XXX: The second level is broken for extension configs.\n        # Can we solve this without code duplication?\n        root = path[0][0]\n        root_source = not_none(root.get_source(schema))\n        is_root_ext_cfg = root_source.issubclass(schema, ext_cfg)\n        assert not is_root_ext_cfg, (\n            \"nested conf objects not yet supported for ext configs\")\n\n        key_start = 0\n\n        for i, (l, exc_props) in enumerate(path):\n            l_card = l.get_cardinality(schema)\n            l_multi = l_card.is_multi()\n            l_name = l.get_shortname(schema).name\n\n            if i == 0:\n                if l_multi:\n                    sourceN = f'''\n                        (SELECT el.val\n                        FROM\n                            (SELECT (value::jsonb) AS val\n                            FROM {prefix}._read_sys_config(NULL, {max_source})\n                            WHERE name = {ql(l_name)}) AS cfg,\n                            LATERAL jsonb_array_elements(cfg.val) AS el(val)\n                        ) AS q{i}'''\n                else:\n                    sourceN = f'''\n                        (SELECT (value::jsonb) AS val\n                        FROM {prefix}._read_sys_config(NULL, {max_source}) cfg\n                        WHERE name = {ql(l_name)}) AS q{i}'''\n            else:\n                sourceN = _build_data_source(schema, l, i - 1)\n\n            sources.append(sourceN)\n            sources.append(_build_key_source(schema, exc_props, l, str(i)))\n\n            if exc_props:\n                key_start = i\n\n    exclusive_props = []\n    single_links = []\n    multi_links = []\n    multi_props = []\n    target_cols: dict[s_pointers.Pointer, str] = {}\n    where = ''\n\n    path_steps = [p.get_shortname(schema).name for p, _ in path]\n\n    if rptr is not None:\n        self_idx = len(path)\n\n        # Generate a source rvar for _this_ target\n        rptr_name = rptr.get_shortname(schema).name\n        path_steps.append(rptr_name)\n\n        if self_idx > 0:\n            sourceN = _build_data_source(schema, rptr, self_idx - 1)\n            sources.append(sourceN)\n    else:\n        self_idx = 0\n\n    sval = f'(q{self_idx}.val)'\n\n    for pp_name, pp in stype.get_pointers(schema).items(schema):\n        pn = str(pp_name)\n        if pn in ('id', '__type__'):\n            continue\n\n        pp_type = pp.get_target(schema)\n        assert pp_type is not None\n        pp_card = pp.get_cardinality(schema)\n        pp_multi = pp_card.is_multi()\n        pp_psi = types.get_pointer_storage_info(pp, schema=schema)\n        pp_col = pp_psi.column_name\n\n        if isinstance(pp, s_links.Link):\n            if pp_multi:\n                multi_links.append(pp)\n            else:\n                single_links.append(pp)\n        else:\n            pp_cast = _make_json_caster(\n                schema, pp_type, versioned=versioned\n            )\n\n            if pp_multi:\n                multi_props.append((pp, pp_cast))\n            else:\n                extract_col = (\n                    f'{pp_cast(f\"{sval}->{ql(pn)}\")} AS {qi(pp_col)}')\n\n                target_cols[pp] = extract_col\n\n                constraints = pp.get_constraints(schema).objects(schema)\n                if any(c.issubclass(schema, exc) for c in constraints):\n                    exclusive_props.append(pp)\n\n    if override_exclusive_props:\n        exclusive_props = [\n            stype.getptr(\n                schema, s_name.UnqualName(p.get_shortname(schema).name)\n            )\n            for p in override_exclusive_props\n        ]\n\n    exclusive_props.sort(key=lambda p: p.get_shortname(schema).name)\n\n    if is_ext_cfg:\n        # Extension configs get custom keys based on their type name\n        # and the scope, since we create one object per scope.\n        key_components = [\n            f'ARRAY[{ql(str(stype.get_name(schema)))}]',\n            \"ARRAY[coalesce(q0.scope, 'session')]\"\n        ]\n        final_keysource = f'{_build_key_expr(key_components, versioned)} AS k'\n        sources.append(final_keysource)\n\n        key_expr = 'k.key'\n        where = f\"q0.val IS NOT NULL\"\n\n    elif exclusive_props or rptr:\n        sources.append(\n            _build_key_source(schema, exclusive_props, rptr, str(self_idx)))\n\n        key_components = [f'k{i}.key' for i in range(key_start, self_idx + 1)]\n        if is_rptr_ext_cfg:\n            assert rptr_source\n            key_components = [\n                f'ARRAY[{ql(str(rptr_source.get_name(schema)))}]',\n                \"ARRAY[coalesce(q0.scope, 'session')]\"\n            ] + key_components\n\n        final_keysource = f'{_build_key_expr(key_components, versioned)} AS k'\n        sources.append(final_keysource)\n\n        key_expr = 'k.key'\n\n        tname = str(stype.get_name(schema))\n        where = f\"{key_expr} IS NOT NULL AND ({sval}->>'_tname') = {ql(tname)}\"\n\n    else:\n        key_expr = f\"'{CONFIG_ID[scope]}'::uuid\"\n\n        key_components = []\n\n    id_ptr = stype.getptr(schema, s_name.UnqualName('id'))\n    target_cols[id_ptr] = f'{X(key_expr)} AS id'\n\n    base_sources = list(sources)\n\n    for link in single_links:\n        link_name = link.get_shortname(schema).name\n        link_type = link.get_target(schema)\n        link_psi = types.get_pointer_storage_info(link, schema=schema)\n        link_col = link_psi.column_name\n\n        if str(link_type.get_name(schema)) == 'cfg::AbstractConfig':\n            target_cols[link] = f'q0.scope_id AS {qi(link_col)}'\n            continue\n\n        if rptr is not None:\n            target_path = path + [(rptr, exclusive_props)]\n        else:\n            target_path = path\n\n        target_views, target_exc_props = _generate_config_type_view(\n            schema,\n            link_type,\n            scope=scope,\n            path=target_path,\n            rptr=link,\n            existing_view_columns=existing_view_columns,\n            _memo=_memo,\n        )\n\n        for descendant in link_type.descendants(schema):\n            if descendant not in _memo:\n                desc_views, _ = _generate_config_type_view(\n                    schema,\n                    descendant,\n                    scope=scope,\n                    path=target_path,\n                    rptr=link,\n                    existing_view_columns=existing_view_columns,\n                    override_exclusive_props=target_exc_props,\n                    _memo=_memo,\n                )\n                views.extend(desc_views)\n\n        target_source = _build_data_source(\n            schema, link, self_idx, alias=link_name,\n            always_array=rptr is None,\n        )\n        sources.append(target_source)\n\n        target_key_source = _build_key_source(\n            schema, target_exc_props, link, source_idx=link_name)\n        sources.append(target_key_source)\n\n        target_key_components = key_components + [f'k{link_name}.key']\n\n        target_key = _build_key_expr(target_key_components, versioned)\n        target_cols[link] = f'({X(target_key)}) AS {qi(link_col)}'\n\n        views.extend(target_views)\n\n    # You can't change the order of a postgres view... so\n    # we have to maintain the original order.\n    #\n    # If we are applying patches that modify the config views,\n    # then we will have an existing_view_columns map that tells us\n    # the existing order in postgres.\n    # If it isn't already in that map, then we order based on\n    # the order in the pointers refdict, which will be the order\n    # the pointers were created, *if* they were added to the in-memory\n    # schema in this process.  (If it was loaded from reflection, that\n    # order won't be preserved, which is why we need existing_view_columns).\n    #\n    # FIXME: We should consider adding enough info to the schema to not need\n    # this complication.\n    existing_indexes = {\n        v: i for i, v in enumerate(existing_view_columns.get(str(stype.id), []))\n    } if existing_view_columns else {}\n    ptr_indexes = {}\n    for i, v in enumerate(stype.get_pointers(schema).objects(schema)):\n        # First try the id\n        if (eidx := existing_indexes.get(str(v.id))) is not None:\n            idx = (0, eidx)\n        # Certain columns use their actual names, so try the actual\n        # name also.\n        elif (\n            eidx := existing_indexes.get(v.get_shortname(schema).name)\n        ) is not None:\n            idx = (0, eidx)\n        # Not already in the database, use the order in pointers refdict\n        else:\n            idx = (1, i)\n        ptr_indexes[v] = idx\n\n    target_cols_sorted = sorted(\n        target_cols.items(), key=lambda p: ptr_indexes[p[0]]\n    )\n\n    target_cols_str = ',\\n'.join([x for _, x in target_cols_sorted if x])\n\n    fromlist = ',\\n'.join(f'LATERAL {X(s)}' for s in sources)\n\n    target_query = xdedent.xdedent(f'''\n        SELECT\n            {X(target_cols_str)}\n        FROM\n            {X(fromlist)}\n    ''')\n\n    if where:\n        target_query += f'\\nWHERE\\n    {where}'\n\n    views.append((tabname(schema, stype), target_query))\n\n    for link in multi_links:\n        target_sources = list(base_sources)\n\n        link_name = link.get_shortname(schema).name\n        link_type = link.get_target(schema)\n\n        if rptr is not None:\n            target_path = path + [(rptr, exclusive_props)]\n        else:\n            target_path = path\n\n        target_views, target_exc_props = _generate_config_type_view(\n            schema,\n            link_type,\n            scope=scope,\n            path=target_path,\n            rptr=link,\n            existing_view_columns=existing_view_columns,\n            _memo=_memo,\n        )\n        views.extend(target_views)\n\n        for descendant in link_type.descendants(schema):\n            if descendant not in _memo:\n                desc_views, _ = _generate_config_type_view(\n                    schema,\n                    descendant,\n                    scope=scope,\n                    path=target_path,\n                    rptr=link,\n                    existing_view_columns=existing_view_columns,\n                    override_exclusive_props=target_exc_props,\n                    _memo=_memo,\n                )\n                views.extend(desc_views)\n\n        # HACK: For computable links (just extensions hopefully?), we\n        # want to compile the targets as a side effect, but we don't\n        # want to actually include them in the view.\n        if link.get_computable(schema):\n            continue\n\n        target_source = _build_data_source(\n            schema, link, self_idx, alias=link_name)\n        target_sources.append(target_source)\n\n        target_key_source = _build_key_source(\n            schema, target_exc_props, link, source_idx=link_name)\n        target_sources.append(target_key_source)\n\n        target_key_components = key_components + [f'k{link_name}.key']\n        target_key = _build_key_expr(target_key_components, versioned)\n\n        target_fromlist = ',\\n'.join(f'LATERAL {X(s)}' for s in target_sources)\n\n        link_query = xdedent.xdedent(f'''\\\n            SELECT\n                q.source,\n                q.target\n            FROM\n                (SELECT\n                    {X(key_expr)} AS source,\n                    {X(target_key)} AS target\n                FROM\n                    {X(target_fromlist)}\n                ) q\n            WHERE\n                q.target IS NOT NULL\n            ''')\n\n        views.append((tabname(schema, link), link_query))\n\n    for prop, pp_cast in multi_props:\n        target_sources = list(sources)\n\n        pn = prop.get_shortname(schema).name\n\n        target_source = _build_data_source(\n            schema, prop, self_idx, alias=pn)\n        target_sources.append(target_source)\n\n        target_fromlist = ',\\n'.join(f'LATERAL {X(s)}' for s in target_sources)\n\n        link_query = xdedent.xdedent(f'''\\\n            SELECT\n                {X(key_expr)} AS source,\n                {pp_cast(f'q{pn}.val')} AS target\n            FROM\n                {X(target_fromlist)}\n        ''')\n        if where:\n            link_query += f'\\nWHERE\\n    {where}'\n\n        views.append((tabname(schema, prop), link_query))\n\n    return views, exclusive_props\n\n\nasync def _execute_block(\n    conn: PGConnection,\n    block: dbops.SQLBlock,\n) -> None:\n    await execute_sql_script(conn, block.to_string())\n\n\nasync def execute_sql_script(\n    conn: PGConnection,\n    sql_text: str,\n) -> None:\n    from edb.server import pgcon\n\n    if debug.flags.bootstrap:\n        debug.header('Bootstrap Script')\n        if len(sql_text) > 102400:\n            # Make sure we don't hog CPU by attempting to highlight\n            # huge scripts.\n            print(sql_text)\n        else:\n            debug.dump_code(sql_text, lexer='sql')\n\n    try:\n        await conn.sql_execute(sql_text.encode(\"utf-8\"))\n    except pgcon.BackendError as e:\n        position = e.get_field('P')\n        internal_position = e.get_field('p')\n        context = e.get_field('W')\n        if context:\n            pl_func_line_m = re.search(\n                r'^PL/pgSQL function inline_code_block line (\\d+).*',\n                context, re.M)\n\n            if pl_func_line_m:\n                pl_func_line = int(pl_func_line_m.group(1))\n        else:\n            pl_func_line = None\n\n        point = None\n        text = None\n\n        if position is not None:\n            point = int(position) - 1\n            text = sql_text\n\n        elif internal_position is not None:\n            point = int(internal_position) - 1\n            text = e.get_field('q')\n\n        elif pl_func_line:\n            point = ql_parser.offset_of_line(sql_text, pl_func_line)\n            text = sql_text\n\n        if point is not None:\n            assert text\n            span = qlast.Span(\n                None, text, start=point, end=point, context_lines=30\n            )\n            exceptions.replace_context(e, span)\n\n        raise\n"
  },
  {
    "path": "edb/pgsql/params.py",
    "content": "# Copyright (C) 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, Mapping, NamedTuple\n\nimport enum\nimport functools\nimport locale\n\nfrom edb import buildmeta\n\n\nBackendVersion = buildmeta.BackendVersion\n\n\nclass BackendCapabilities(enum.IntFlag):\n\n    NONE = 0\n    #: Whether CREATE ROLE .. SUPERUSER is allowed\n    SUPERUSER_ACCESS = 1 << 0\n    #: Whether reading PostgreSQL configuration files\n    #: via pg_file_settings is allowed\n    CONFIGFILE_ACCESS = 1 << 1\n    #: Whether the PostgreSQL server supports the C.UTF-8 locale\n    C_UTF8_LOCALE = 1 << 2\n    #: Whether CREATE ROLE is allowed\n    CREATE_ROLE = 1 << 3\n    #: Whether CREATE DATABASE is allowed\n    CREATE_DATABASE = 1 << 4\n    #: Whether extension \"edb_stat_statements\" is available\n    STAT_STATEMENTS = 1 << 5\n\n\nALL_BACKEND_CAPABILITIES = (\n    BackendCapabilities.SUPERUSER_ACCESS\n    | BackendCapabilities.CONFIGFILE_ACCESS\n    | BackendCapabilities.C_UTF8_LOCALE\n    | BackendCapabilities.CREATE_ROLE\n    | BackendCapabilities.CREATE_DATABASE\n    | BackendCapabilities.STAT_STATEMENTS\n)\n\n\nclass BackendInstanceParams(NamedTuple):\n\n    capabilities: BackendCapabilities\n    version: BackendVersion\n    tenant_id: str\n    base_superuser: Optional[str] = None\n    max_connections: int = 500\n    reserved_connections: int = 0\n\n    ext_schema: str = \"edgedbext\"\n    \"\"\"A Postgres schema where extensions can be created.\"\"\"\n\n    existing_exts: Optional[Mapping[str, str]] = None\n    \"\"\"A map of preexisting extensions in the target backend with schemas.\"\"\"\n\n\nclass BackendRuntimeParams(NamedTuple):\n\n    instance_params: BackendInstanceParams\n    session_authorization_role: Optional[str] = None\n\n    @property\n    def tenant_id(self) -> str:\n        return self.instance_params.tenant_id\n\n    @property\n    def has_superuser_access(self) -> bool:\n        return bool(\n            self.instance_params.capabilities\n            & BackendCapabilities.SUPERUSER_ACCESS\n        )\n\n    @property\n    def has_configfile_access(self) -> bool:\n        return bool(\n            self.instance_params.capabilities\n            & BackendCapabilities.CONFIGFILE_ACCESS\n        )\n\n    @property\n    def has_c_utf8_locale(self) -> bool:\n        return bool(\n            self.instance_params.capabilities\n            & BackendCapabilities.C_UTF8_LOCALE\n        )\n\n    @property\n    def has_create_role(self) -> bool:\n        return bool(\n            self.instance_params.capabilities\n            & BackendCapabilities.CREATE_ROLE\n        )\n\n    @property\n    def has_create_database(self) -> bool:\n        return bool(\n            self.instance_params.capabilities\n            & BackendCapabilities.CREATE_DATABASE\n        )\n\n    @property\n    def has_stat_statements(self) -> bool:\n        return self.has_superuser_access and bool(\n            self.instance_params.capabilities\n            & BackendCapabilities.STAT_STATEMENTS\n        )\n\n\n@functools.lru_cache\ndef get_default_runtime_params(\n    **instance_params: Any,\n) -> BackendRuntimeParams:\n    capabilities = ALL_BACKEND_CAPABILITIES\n    if not _is_c_utf8_locale_present():\n        capabilities &= ~BackendCapabilities.C_UTF8_LOCALE\n    instance_params.setdefault('capabilities', capabilities)\n    if 'tenant_id' not in instance_params:\n        instance_params = dict(\n            tenant_id=buildmeta.get_default_tenant_id(),\n            **instance_params,\n        )\n    if 'version' not in instance_params:\n        try:\n            version = buildmeta.get_pg_version()\n        except buildmeta.MetadataError as _:\n            # HACK: if get_pg_version fails, this means we have no pg_config,\n            # which happens for edgedb-ls. It is invoking pg compiler from\n            # schema delta. Ideally, schema delta would not need pg compiler,\n            # but that would require a lot of cleanups.\n            version = BackendVersion(\n                major=100,\n                minor=0,\n                micro=0,\n                releaselevel='final',\n                serial=0,\n                string='100.0'\n            )\n\n        instance_params = dict(\n            version=version,\n            **instance_params,\n        )\n\n    return BackendRuntimeParams(\n        instance_params=BackendInstanceParams(**instance_params),\n    )\n\n\ndef _is_c_utf8_locale_present() -> bool:\n    try:\n        locale.setlocale(locale.LC_CTYPE, 'C.UTF-8')\n    except Exception:\n        return False\n    else:\n        # We specifically don't use locale.getlocale(), because\n        # it can lie and return a non-existent locale due to PEP 538.\n        locale.setlocale(locale.LC_CTYPE, '')\n        return True\n"
  },
  {
    "path": "edb/pgsql/parser/.gitignore",
    "content": "/*.c\n"
  },
  {
    "path": "edb/pgsql/parser/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2010-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nimport json\n\nfrom edb.pgsql import ast as pgast\n\nfrom . import ast_builder\nfrom . import parser\nfrom .parser import (\n    Source,\n    NormalizedSource,\n    deserialize,\n)\n\n\n__all__ = (\n    \"parse\",\n    \"Source\",\n    \"NormalizedSource\",\n    \"deserialize\"\n)\n\n\ndef parse(\n    sql_query: str, propagate_spans: bool = False\n) -> list[pgast.Query | pgast.Statement]:\n    ast_json = parser.pg_parse(sql_query)\n\n    return ast_builder.build_stmts(\n        json.loads(ast_json),\n        sql_query,\n        propagate_spans,\n    )\n"
  },
  {
    "path": "edb/pgsql/parser/ast_builder.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2010-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport dataclasses\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n    Sequence,\n    cast,\n)\n\nfrom edb.common import span\nfrom edb.common.parsing import Span\n\nfrom edb.pgsql import ast as pgast\nfrom edb.edgeql import ast as qlast\nfrom edb.pgsql.parser.exceptions import PSqlUnsupportedError, get_node_name\n\n\n@dataclasses.dataclass(kw_only=True)\nclass Context:\n    source_sql: str\n    has_fallback = False\n\n\n# Node = bool | str | int | float | List[Any] | dict[str, Any]\nNode = Any\n\n\ndef build_stmts(\n    node: Node, source_sql: str, propagate_spans: bool\n) -> list[pgast.Query | pgast.Statement]:\n    ctx = Context(source_sql=source_sql)\n\n    try:\n        res = [_build_stmt(node[\"stmt\"], ctx) for node in node[\"stmts\"]]\n    except IndexError:\n        raise PSqlUnsupportedError()\n    except PSqlUnsupportedError as e:\n        if e.node and \"location\" in e.node:\n            e.location = e.node[\"location\"]\n            assert e.location\n            e.message += f\" near location {e.location}:\"\n            e.message += source_sql[e.location : (e.location + 50)]\n        raise\n\n    if propagate_spans:\n        # we need to do a full pass of span propagation, because some\n        # nodes (CommonTableExpr) have span, but their children don't (Insert).\n        span.SpanPropagator(full_pass=True).container_visit(res)\n\n    return res\n\n\ndef _maybe[T](\n    node: Node, ctx: Context, name: str, builder: Callable[[Node, Context], T]\n) -> Optional[T]:\n    if name in node:\n        return builder(node[name], ctx)\n    return None\n\n\ndef _ident(t: Any) -> Any:\n    return t\n\n\ndef _list[T, U](\n    node: Node,\n    ctx: Context,\n    name: str,\n    builder: Callable[[Node, Context], T],\n    mapper: Callable[[T], U] = _ident,\n    *,\n    unwrap: Optional[str] = None,\n) -> list[U]:\n    if unwrap is not None:\n        return [\n            mapper(builder(_unwrap(n, unwrap), ctx)) for n in node.get(name, [])\n        ]\n    else:\n        return [mapper(builder(n, ctx)) for n in node.get(name, [])]\n\n\ndef _maybe_list[T, U](\n    node: Node,\n    ctx: Context,\n    name: str,\n    builder: Callable[[Node, Context], T],\n    mapper: Callable[[T], U] = _ident,\n    *,\n    unwrap: Optional[str] = None,\n) -> Optional[list[U]]:\n    return (\n        _list(node, ctx, name, builder, mapper, unwrap=unwrap)\n        if name in node\n        else None\n    )\n\n\ndef _enum[T](\n    _ty: type[T],\n    node: Node,\n    ctx: Context,\n    builders: dict[str, Callable[[Node, Context], T]],\n    fallbacks: Sequence[Callable[[Node, Context], T]] = (),\n) -> T:\n    outer_fallback = ctx.has_fallback\n    ctx.has_fallback = False\n\n    try:\n        for name in builders:\n            if name in node:\n                builder = builders[name]\n                return builder(node[name], ctx)\n\n        ctx.has_fallback = True\n        for fallback in fallbacks:\n            res = fallback(node, ctx)\n            if res:\n                return res\n\n        if outer_fallback:\n            return None  # type: ignore\n\n        raise PSqlUnsupportedError(\n            node, \", \".join(get_node_name(k) for k in node.keys())\n        )\n    finally:\n        ctx.has_fallback = outer_fallback\n\n\ndef _build_str(node: Node, _: Context) -> str:\n    node = _unwrap_string(node)\n    return str(node)\n\n\ndef _build_bool(node: Node, _: Context) -> bool:\n    assert isinstance(node, bool)\n    return node\n\n\ndef _bool_or_false(node: Node, name: str) -> bool:\n    return node[name] if name in node else False\n\n\ndef _unwrap(node: Node, name: str) -> Node:\n    if isinstance(node, dict) and name in node:\n        return node[name]\n    return node\n\n\ndef _unwrap_boolean(n: Node) -> Node:\n    n = _unwrap(n, 'Boolean')\n    n = _unwrap(n, 'str')\n    n = _unwrap(n, 'boolval')\n    n = _unwrap(n, 'boolval')\n    if isinstance(n, dict) and len(n) == 0:\n        n = False\n    return n\n\n\ndef _unwrap_int(n: Node) -> Node:\n    n = _unwrap(n, 'Integer')\n    n = _unwrap(n, 'str')\n    n = _unwrap(n, 'ival')\n    n = _unwrap(n, 'ival')\n    if isinstance(n, dict) and len(n) == 0:\n        n = 0\n    return n\n\n\ndef _unwrap_float(n: Node) -> Node:\n    n = _unwrap(n, 'Float')\n    n = _unwrap(n, 'str')\n    n = _unwrap(n, 'fval')\n    n = _unwrap(n, 'fval')\n    return n\n\n\ndef _unwrap_string(n: Node) -> Node:\n    n = _unwrap(n, 'String')\n    n = _unwrap(n, 'str')\n    n = _unwrap(n, 'sval')\n    n = _unwrap(n, 'sval')\n    return n\n\n\ndef _as_column_ref(name: str) -> pgast.ColumnRef:\n    return pgast.ColumnRef(\n        name=(name,),\n    )\n\n\ndef _build_span(n: Node, c: Context) -> Optional[Span]:\n    if 'location' not in n:\n        return None\n\n    return Span(\n        filename=None,\n        buffer=c.source_sql,\n        start=n[\"location\"],\n        end=n[\"location\"],\n    )\n\n\ndef _build_stmt(node: Node, c: Context) -> pgast.Query | pgast.Statement:\n    return _enum(\n        pgast.Query | pgast.Statement,  # type: ignore\n        node,\n        c,\n        {\n            \"VariableSetStmt\": _build_variable_set_stmt,\n            \"VariableShowStmt\": _build_variable_show_stmt,\n            \"TransactionStmt\": _build_transaction_stmt,\n            \"PrepareStmt\": _build_prepare,\n            \"ExecuteStmt\": _build_execute,\n            \"DeallocateStmt\": _build_deallocate,\n            \"CreateStmt\": _build_create,\n            \"CreateTableAsStmt\": _build_create_table_as,\n            \"LockStmt\": _build_lock,\n            \"CopyStmt\": _build_copy,\n        },\n        [_build_query],\n    )\n\n\ndef _build_query(node: Node, c: Context) -> pgast.Query:\n    return _enum(\n        pgast.Query,\n        node,\n        c,\n        {\n            \"SelectStmt\": _build_select_stmt,\n            \"InsertStmt\": _build_insert_stmt,\n            \"UpdateStmt\": _build_update_stmt,\n            \"DeleteStmt\": _build_delete_stmt,\n        },\n    )\n\n\ndef _build_select_stmt(n: Node, c: Context) -> pgast.SelectStmt:\n    op = _maybe(n, c, \"op\", _build_str)\n    if op:\n        op = op[6:]\n        if op == \"NONE\":\n            op = None\n    return pgast.SelectStmt(\n        distinct_clause=(\n            _maybe(n, c, \"distinctClause\", _build_distinct)  # type: ignore\n        ),\n        target_list=_maybe_list(n, c, \"targetList\", _build_res_target) or [],\n        from_clause=_maybe_list(n, c, \"fromClause\", _build_base_range_var)\n        or [],\n        where_clause=_maybe(n, c, \"whereClause\", _build_base_expr),\n        group_clause=_maybe_list(n, c, \"groupClause\", _build_base),\n        having_clause=_maybe(n, c, \"havingClause\", _build_base_expr),\n        window_clause=_maybe_list(n, c, \"windowClause\", _build_base),\n        values=_maybe_list(n, c, \"valuesLists\", _build_base_expr),\n        sort_clause=_maybe_list(n, c, \"sortClause\", _build_sort_by),\n        limit_offset=_maybe(n, c, \"limitOffset\", _build_base_expr),\n        limit_count=_maybe(n, c, \"limitCount\", _build_base_expr),\n        locking_clause=_maybe_list(\n            n, c, \"lockingClause\", _build_locking_clause\n        ),\n        op=op,\n        all=n[\"all\"] if \"all\" in n else False,\n        larg=_maybe(n, c, \"larg\", _build_select_stmt),\n        rarg=_maybe(n, c, \"rarg\", _build_select_stmt),\n        ctes=_maybe(n, c, \"withClause\", _build_ctes),\n    )\n\n\ndef _build_insert_stmt(n: Node, c: Context) -> pgast.InsertStmt:\n    select_stmt = _maybe(n, c, \"selectStmt\", _build_stmt)\n    assert select_stmt is None or isinstance(select_stmt, pgast.Query)\n\n    return pgast.InsertStmt(\n        relation=_build_rel_range_var(n[\"relation\"], c),\n        returning_list=(\n            _maybe_list(n, c, \"returningList\", _build_res_target) or []\n        ),\n        cols=_maybe_list(n, c, \"cols\", _build_insert_target),\n        select_stmt=select_stmt,\n        on_conflict=_maybe(n, c, \"onConflictClause\", _build_on_conflict),\n        ctes=_maybe(n, c, \"withClause\", _build_ctes),\n    )\n\n\ndef _build_update_stmt(n: Node, c: Context) -> pgast.UpdateStmt:\n    return pgast.UpdateStmt(\n        relation=_build_rel_range_var(n[\"relation\"], c),\n        targets=_maybe(n, c, \"targetList\", _build_update_targets) or [],\n        where_clause=_maybe(n, c, \"whereClause\", _build_base_expr),\n        from_clause=(\n            _maybe_list(n, c, \"fromClause\", _build_base_range_var) or []\n        ),\n        returning_list=(\n            _maybe_list(n, c, \"returningList\", _build_res_target) or []\n        ),\n        ctes=_maybe(n, c, \"withClause\", _build_ctes),\n    )\n\n\ndef _build_delete_stmt(n: Node, c: Context) -> pgast.DeleteStmt:\n    return pgast.DeleteStmt(\n        relation=_build_rel_range_var(n[\"relation\"], c),\n        returning_list=(\n            _maybe_list(n, c, \"returningList\", _build_res_target) or []\n        ),\n        where_clause=_maybe(n, c, \"whereClause\", _build_base_expr),\n        using_clause=(\n            _maybe_list(n, c, \"usingClause\", _build_base_range_var) or []\n        ),\n        ctes=_maybe(n, c, \"withClause\", _build_ctes),\n    )\n\n\ndef _build_lock(n: Node, c: Context) -> pgast.LockStmt:\n    MODES = {\n        1: 'ACCESS SHARE',\n        2: 'ROW SHARE',\n        3: 'ROW EXCLUSIVE',\n        4: 'SHARE UPDATE EXCLUSIVE',\n        5: 'SHARE',\n        6: 'SHARE ROW EXCLUSIVE',\n        7: 'EXCLUSIVE',\n        8: 'ACCESS EXCLUSIVE',\n    }\n\n    return pgast.LockStmt(\n        relations=_list(n, c, \"relations\", _build_base_range_var),\n        mode=MODES[n['mode']],\n        no_wait=_bool_or_false(n, 'nowait'),\n    )\n\n\ndef _build_variable_set_stmt(n: Node, c: Context) -> pgast.Statement:\n    tx_only_vars = {\n        \"transaction_isolation\",\n        \"transaction_read_only\",\n        \"transaction_deferrable\",\n    }\n    if n[\"kind\"] == \"VAR_RESET\":\n        return pgast.VariableResetStmt(\n            name=n[\"name\"],\n            scope=(\n                pgast.OptionsScope.TRANSACTION\n                if n[\"name\"] in tx_only_vars\n                else pgast.OptionsScope.SESSION\n            ),\n            span=_build_span(n, c),\n        )\n\n    if n[\"kind\"] == \"VAR_RESET_ALL\":\n        return pgast.VariableResetStmt(\n            name=None,\n            scope=pgast.OptionsScope.SESSION,\n            span=_build_span(n, c),\n        )\n\n    if n[\"kind\"] == \"VAR_SET_MULTI\":\n        name = n[\"name\"]\n        if name == \"TRANSACTION\" or name == \"SESSION CHARACTERISTICS\":\n            return pgast.SetTransactionStmt(\n                options=_build_transaction_options(n[\"args\"], c),\n                scope=(\n                    pgast.OptionsScope.TRANSACTION\n                    if name == \"TRANSACTION\"\n                    else pgast.OptionsScope.SESSION\n                ),\n            )\n\n    if n[\"kind\"] == \"VAR_SET_VALUE\":\n        return pgast.VariableSetStmt(\n            name=n[\"name\"],\n            args=pgast.ArgsList(args=_list(n, c, \"args\", _build_base_expr)),\n            scope=(\n                pgast.OptionsScope.TRANSACTION\n                if n[\"name\"] in tx_only_vars\n                or (\"is_local\" in n and n[\"is_local\"])\n                else pgast.OptionsScope.SESSION\n            ),\n            span=_build_span(n, c),\n        )\n\n    if n[\"kind\"] == \"VAR_SET_DEFAULT\":\n        return pgast.VariableResetStmt(\n            name=n[\"name\"],\n            scope=(\n                pgast.OptionsScope.TRANSACTION\n                if n[\"name\"] in tx_only_vars\n                or (\"is_local\" in n and n[\"is_local\"])\n                else pgast.OptionsScope.SESSION\n            ),\n            span=_build_span(n, c),\n        )\n\n    raise PSqlUnsupportedError(n)\n\n\ndef _build_variable_show_stmt(n: Node, c: Context) -> pgast.Statement:\n    return pgast.VariableShowStmt(\n        name=n[\"name\"],\n        span=_build_span(n, c),\n    )\n\n\ndef _build_transaction_stmt(n: Node, c: Context) -> pgast.TransactionStmt:\n    def begin(n: Node, c: Context) -> pgast.BeginStmt:\n        return pgast.BeginStmt(\n            options=_maybe(n, c, \"options\", _build_transaction_options)\n        )\n\n    def start(n: Node, c: Context) -> pgast.StartStmt:\n        return pgast.StartStmt(\n            options=_maybe(n, c, \"options\", _build_transaction_options)\n        )\n\n    def commit(n: Node, _: Context) -> pgast.CommitStmt:\n        return pgast.CommitStmt(chain=_bool_or_false(n, \"chain\"))\n\n    def rollback(n: Node, _: Context) -> pgast.RollbackStmt:\n        return pgast.RollbackStmt(chain=_bool_or_false(n, \"chain\"))\n\n    def savepoint(n: Node, _: Context) -> pgast.SavepointStmt:\n        return pgast.SavepointStmt(savepoint_name=n[\"savepoint_name\"])\n\n    def release(n: Node, _: Context) -> pgast.ReleaseStmt:\n        return pgast.ReleaseStmt(savepoint_name=n[\"savepoint_name\"])\n\n    def rollback_to(n: Node, _: Context) -> pgast.RollbackToStmt:\n        return pgast.RollbackToStmt(savepoint_name=n[\"savepoint_name\"])\n\n    def prepare(n: Node, _: Context) -> pgast.PrepareTransaction:\n        return pgast.PrepareTransaction(gid=n[\"gid\"])\n\n    def commit_prepared(n: Node, _: Context) -> pgast.CommitPreparedStmt:\n        return pgast.CommitPreparedStmt(gid=n[\"gid\"])\n\n    def rollback_prepared(n: Node, _: Context) -> pgast.RollbackPreparedStmt:\n        return pgast.RollbackPreparedStmt(gid=n[\"gid\"])\n\n    kinds = {\n        \"BEGIN\": begin,\n        \"START\": start,\n        \"COMMIT\": commit,\n        \"ROLLBACK\": rollback,\n        \"SAVEPOINT\": savepoint,\n        \"RELEASE\": release,\n        \"ROLLBACK_TO\": rollback_to,\n        \"PREPARE\": prepare,\n        \"COMMIT_PREPARED\": commit_prepared,\n        \"ROLLBACK_PREPARED\": rollback_prepared,\n    }\n\n    kind = cast(str, n[\"kind\"]).removeprefix(\"TRANS_STMT_\")\n    if kind not in kinds:\n        raise PSqlUnsupportedError(n)\n\n    return kinds[kind](n, c)\n\n\ndef _build_transaction_options(\n    nodes: list[Node], c: Context\n) -> pgast.TransactionOptions:\n    options = {}\n    for n in nodes:\n        if \"DefElem\" not in n:\n            continue\n        def_e = n[\"DefElem\"]\n        if not (\"defname\" in def_e and \"arg\" in def_e):\n            continue\n        options[def_e[\"defname\"]] = _build_base_expr(def_e[\"arg\"], c)\n    return pgast.TransactionOptions(options=options)\n\n\ndef _build_prepare(n: Node, c: Context) -> pgast.PrepareStmt:\n    return pgast.PrepareStmt(\n        name=n[\"name\"],\n        argtypes=_maybe_list(n, c, \"argtypes\", _build_type_name),\n        query=_build_base_relation(n[\"query\"], c),\n    )\n\n\ndef _build_execute(n: Node, c: Context) -> pgast.ExecuteStmt:\n    return pgast.ExecuteStmt(\n        name=n[\"name\"], params=_maybe_list(n, c, \"params\", _build_base_expr)\n    )\n\n\ndef _build_deallocate(n: Node, c: Context) -> pgast.DeallocateStmt:\n    return pgast.DeallocateStmt(name=n[\"name\"])\n\n\ndef _build_create_table_as(n: Node, c: Context) -> pgast.CreateTableAsStmt:\n    return pgast.CreateTableAsStmt(\n        into=_build_create(n['into'], c),\n        query=_build_query(n['query'], c),\n        with_no_data=_bool_or_false(n['into'], 'skipData'),\n    )\n\n\ndef _build_create(n: Node, c: Context) -> pgast.CreateStmt:\n    def _build_on_commit(n: str, _c: Context) -> Optional[str]:\n        on_commit = n[9:]\n        return on_commit if on_commit != 'NOOP' else None\n\n    relation = n['relation'] if 'relation' in n else n['rel']\n\n    return pgast.CreateStmt(\n        relation=_build_relation(relation, c),\n        table_elements=_maybe_list(n, c, 'tableElts', _build_table_element)\n        or [],\n        span=_build_span(n, c),\n        on_commit=_maybe(n, c, 'oncommit', _build_on_commit),\n    )\n\n\ndef _build_table_element(n: Node, c: Context) -> pgast.TableElement:\n    return _enum(\n        pgast.TableElement,\n        n,\n        c,\n        {\n            \"ColumnDef\": _build_column_def,\n        },\n    )\n\n\ndef _build_column_def(n: Node, c: Context) -> pgast.ColumnDef:\n    is_not_null = False\n    default_expr = None\n    if 'constraints' in n:\n        for constraint in n['constraints']:\n            constraint = _unwrap(constraint, 'Constraint')\n\n            if constraint['contype'] == 'CONSTR_NOTNULL':\n                is_not_null = True\n            if constraint['contype'] == 'CONSTR_DEFAULT':\n                is_not_null = True\n                default_expr = _maybe(n, c, 'raw_expr', _build_base_expr)\n\n    return pgast.ColumnDef(\n        name=n['colname'],\n        typename=_build_type_name(n['typeName'], c),\n        default_expr=default_expr,\n        is_not_null=is_not_null,\n        span=_build_span(n, c),\n    )\n\n\ndef _build_base(n: Node, c: Context) -> pgast.Base:\n    return _enum(\n        pgast.Base,\n        n,\n        c,\n        {\n            \"CommonTableExpr\": _build_cte,\n        },\n        [_build_base_expr],  # type: ignore\n    )\n\n\ndef _build_base_expr(node: Node, c: Context) -> pgast.BaseExpr:\n    return _enum(\n        pgast.BaseExpr,\n        node,\n        c,\n        {\n            \"ResTarget\": _build_res_target,\n            \"FuncCall\": _build_func_call,\n            \"CoalesceExpr\": _build_coalesce,\n            \"List\": _build_implicit_row,\n            \"A_Expr\": _build_a_expr,\n            \"A_ArrayExpr\": _build_array_expr,\n            \"A_Const\": _build_const,\n            \"A_Indirection\": _build_indirection,\n            \"BoolExpr\": _build_bool_expr,\n            \"CaseExpr\": _build_case_expr,\n            \"TypeCast\": _build_type_cast,\n            \"NullTest\": _build_null_test,\n            \"BooleanTest\": _build_boolean_test,\n            \"RowExpr\": _build_row_expr,\n            \"SubLink\": _build_sub_link,\n            \"ParamRef\": _build_param_ref,\n            \"SetToDefault\": _build_keyword(\"DEFAULT\"),  # type: ignore\n            \"SQLValueFunction\": _build_sql_value_function,\n            \"CollateClause\": _build_collate_clause,\n            \"MinMaxExpr\": _build_min_max_expr,\n        },\n        [_build_base_range_var, _build_indirection_op],  # type: ignore\n    )\n\n\ndef _build_distinct(nodes: list[Node], c: Context) -> list[pgast.Base]:\n    # For some reason, plain DISTINCT is parsed as [{}]\n    # In our AST this is represented by [pgast.Star()]\n    if len(nodes) == 1 and len(nodes[0]) == 0:\n        return [pgast.Star()]\n    return [_build_base_expr(n, c) for n in nodes]\n\n\ndef _build_indirection(n: Node, c: Context) -> pgast.Indirection:\n    return pgast.Indirection(\n        arg=_build_base_expr(n['arg'], c),\n        indirection=_list(n, c, 'indirection', _build_indirection_op),\n    )\n\n\ndef _build_indirection_op(n: Node, c: Context) -> pgast.IndirectionOp:\n    return _enum(\n        pgast.IndirectionOp,  # type: ignore\n        n,\n        c,\n        {\n            'A_Indices': _build_index_or_slice,\n            'Star': _build_star,\n            'ColumnRef': _build_column_ref,\n            'String': _build_record_indirection_op,\n        },\n    )\n\n\ndef _build_record_indirection_op(\n    n: Node, c: Context\n) -> pgast.RecordIndirectionOp:\n    return pgast.RecordIndirectionOp(name=_build_str(n, c))\n\n\ndef _build_ctes(n: Node, c: Context) -> list[pgast.CommonTableExpr]:\n    is_recursive = _maybe(n, c, 'recursive', lambda x, _: bool(x)) or False\n\n    ctes: list[pgast.CommonTableExpr] = _list(n, c, \"ctes\", _build_cte)\n    for cte in ctes:\n        cte.recursive = is_recursive\n    return ctes\n\n\ndef _build_cte(n: Node, c: Context) -> pgast.CommonTableExpr:\n    n = _unwrap(n, \"CommonTableExpr\")\n\n    materialized = None\n    if n[\"ctematerialized\"] == \"CTEMaterializeAlways\":\n        materialized = True\n    elif n[\"ctematerialized\"] == \"CTEMaterializeNever\":\n        materialized = False\n\n    return pgast.CommonTableExpr(\n        name=n[\"ctename\"],\n        query=_build_query(n[\"ctequery\"], c),\n        recursive=False,\n        aliascolnames=_maybe_list(n, c, \"aliascolnames\", _build_str),\n        materialized=materialized,\n        span=_build_span(n, c),\n    )\n\n\ndef _build_keyword(name: str) -> Callable[[Node, Context], pgast.Keyword]:\n    return lambda n, c: pgast.Keyword(name=name, span=_build_span(n, c))\n\n\ndef _build_param_ref(n: Node, c: Context) -> pgast.ParamRef:\n    return pgast.ParamRef(number=n.get(\"number\", 0), span=_build_span(n, c))\n\n\ndef _build_collate_clause(n: Node, c: Context) -> pgast.CollateClause:\n    return pgast.CollateClause(\n        arg=_build_base_expr(n['arg'], c),\n        collname=tuple(_list(n, c, 'collname', _build_str)),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_min_max_expr(n: Node, c: Context) -> pgast.MinMaxExpr:\n    if n['op'] == 'IS_GREATEST':\n        op = 'GREATEST'\n    elif n['op'] == 'IS_LEAST':\n        op = 'LEAST'\n    else:\n        raise PSqlUnsupportedError(n)\n\n    return pgast.MinMaxExpr(\n        op=op,\n        args=_list(n, c, 'args', _build_base_expr),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_sql_value_function(n: Node, c: Context) -> pgast.SQLValueFunction:\n    op = n[\"op\"].removeprefix(\"SVFOP_\")\n\n    op_mapping = {\n        \"CURRENT_DATE\": pgast.SQLValueFunctionOP.CURRENT_DATE,\n        \"CURRENT_TIME\": pgast.SQLValueFunctionOP.CURRENT_TIME,\n        \"CURRENT_TIME_N\": pgast.SQLValueFunctionOP.CURRENT_TIME_N,\n        \"CURRENT_TIMESTAMP\": pgast.SQLValueFunctionOP.CURRENT_TIMESTAMP,\n        \"CURRENT_TIMESTAMP_N\": pgast.SQLValueFunctionOP.CURRENT_TIMESTAMP_N,\n        \"LOCALTIME\": pgast.SQLValueFunctionOP.LOCALTIME,\n        \"LOCALTIME_N\": pgast.SQLValueFunctionOP.LOCALTIME_N,\n        \"LOCALTIMESTAMP\": pgast.SQLValueFunctionOP.LOCALTIMESTAMP,\n        \"LOCALTIMESTAMP_N\": pgast.SQLValueFunctionOP.LOCALTIMESTAMP_N,\n        \"CURRENT_ROLE\": pgast.SQLValueFunctionOP.CURRENT_ROLE,\n        \"CURRENT_USER\": pgast.SQLValueFunctionOP.CURRENT_USER,\n        \"USER\": pgast.SQLValueFunctionOP.USER,\n        \"SESSION_USER\": pgast.SQLValueFunctionOP.SESSION_USER,\n        \"CURRENT_CATALOG\": pgast.SQLValueFunctionOP.CURRENT_CATALOG,\n        \"CURRENT_SCHEMA\": pgast.SQLValueFunctionOP.CURRENT_SCHEMA,\n    }\n\n    if op not in op_mapping:\n        raise PSqlUnsupportedError(n)\n\n    return pgast.SQLValueFunction(\n        op=op_mapping[op], arg=_maybe(n, c, \"xpr\", _build_base_expr)\n    )\n\n\ndef _build_sub_link(n: Node, c: Context) -> pgast.SubLink:\n    typ = n[\"subLinkType\"]\n    if typ == \"EXISTS_SUBLINK\":\n        operator = \"EXISTS\"\n    elif typ == \"NOT_EXISTS_SUBLINK\":\n        operator = \"NOT_EXISTS\"\n    elif typ == \"ALL_SUBLINK\":\n        operator = \"ALL\"\n    elif typ == \"ANY_SUBLINK\":\n        operator = \"= ANY\"\n    elif typ == \"EXPR_SUBLINK\":\n        operator = None\n    elif typ == \"ARRAY_SUBLINK\":\n        operator = \"ARRAY\"\n    else:\n        raise PSqlUnsupportedError(n)\n\n    return pgast.SubLink(\n        operator=operator,\n        expr=_build_query(n[\"subselect\"], c),\n        test_expr=_maybe(n, c, 'testexpr', _build_base_expr),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_row_expr(n: Node, c: Context) -> pgast.BaseExpr:\n    args: list[pgast.BaseExpr] = (\n        _maybe_list(n, c, \"args\", _build_base_expr) or []\n    )\n\n    format = n.get('row_format', None)\n    if format in {'COERCE_EXPLICIT_CALL', 'COERCE_EXPLICIT_CAST'}:\n        return pgast.RowExpr(args=args, span=_build_span(n, c))\n    else:\n        return pgast.ImplicitRowExpr(args=args, span=_build_span(n, c))\n\n\ndef _build_boolean_test(n: Node, c: Context) -> pgast.BooleanTest:\n    return pgast.BooleanTest(\n        arg=_build_base_expr(n[\"arg\"], c),\n        negated=n[\"booltesttype\"].startswith(\"IS_NOT\"),\n        is_true=n[\"booltesttype\"].endswith(\"TRUE\"),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_null_test(n: Node, c: Context) -> pgast.NullTest:\n    return pgast.NullTest(\n        arg=_build_base_expr(n[\"arg\"], c),\n        negated=n[\"nulltesttype\"] == \"IS_NOT_NULL\",\n        span=_build_span(n, c),\n    )\n\n\ndef _build_type_cast(n: Node, c: Context) -> pgast.TypeCast:\n    return pgast.TypeCast(\n        arg=_build_base_expr(n[\"arg\"], c),\n        type_name=_build_type_name(n[\"typeName\"], c),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_type_name(n: Node, c: Context) -> pgast.TypeName:\n    n = _unwrap(n, \"TypeName\")\n\n    name: tuple[str, ...] = tuple(_list(n, c, \"names\", _build_str))\n\n    # we don't escape char properly, so let's just resolve it during parsing\n    if name == (\"char\",):\n        name = (\"pg_catalog\", \"char\")\n\n    def unwrap_int_builder(n: Node, _c: Context) -> Node:\n        return _unwrap_int(n)\n\n    return pgast.TypeName(\n        name=name,\n        setof=_bool_or_false(n, \"setof\"),\n        typmods=None,\n        array_bounds=_maybe_list(n, c, \"arrayBounds\", unwrap_int_builder),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_case_expr(n: Node, c: Context) -> pgast.CaseExpr:\n    return pgast.CaseExpr(\n        arg=_maybe(n, c, \"arg\", _build_base_expr),\n        args=_list(n, c, \"args\", _build_case_when),\n        defresult=_maybe(n, c, \"defresult\", _build_base_expr),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_case_when(n: Node, c: Context) -> pgast.CaseWhen:\n    n = _unwrap(n, \"CaseWhen\")\n    return pgast.CaseWhen(\n        expr=_build_base_expr(n[\"expr\"], c),\n        result=_build_base_expr(n[\"result\"], c),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_bool_expr(n: Node, c: Context) -> pgast.Expr:\n    name = _build_str(n[\"boolop\"], c)[0:-5]\n    args = list(n[\"args\"])\n    res = pgast.Expr(\n        name=name,\n        lexpr=_build_base_expr(args.pop(0), c) if len(args) > 1 else None,\n        rexpr=_build_base_expr(args.pop(0), c) if len(args) > 0 else None,\n        span=_build_span(n, c),\n    )\n    while len(args) > 0:\n        res = pgast.Expr(\n            name=_build_str(n[\"boolop\"], c)[0:-5],\n            lexpr=res,\n            rexpr=_build_base_expr(args.pop(0), c) if len(args) > 0 else None,\n            span=_build_span(n, c),\n        )\n    return res\n\n\ndef _build_base_range_var(n: Node, c: Context) -> pgast.BaseRangeVar:\n    return _enum(\n        pgast.BaseRangeVar,\n        n,\n        c,\n        {\n            \"RangeVar\": _build_rel_range_var,\n            \"JoinExpr\": _build_join_expr,\n            \"RangeFunction\": _build_range_function,\n            \"RangeSubselect\": _build_range_subselect,\n        },\n    )\n\n\ndef _build_const(n: Node, c: Context) -> pgast.BaseConstant:\n    n = _unwrap(n, \"val\")\n    span = _build_span(n, c)\n\n    if \"Null\" in n or \"isnull\" in n:\n        return pgast.NullConstant(span=span)\n\n    if \"Boolean\" in n or \"boolval\" in n:\n        return pgast.BooleanConstant(val=_unwrap_boolean(n), span=span)\n\n    if \"Integer\" in n or \"ival\" in n:\n        return pgast.NumericConstant(val=str(_unwrap_int(n)), span=span)\n\n    if \"Float\" in n or \"fval\" in n:\n        return pgast.NumericConstant(val=_unwrap_float(n), span=span)\n\n    if \"String\" in n or \"sval\" in n:\n        return pgast.StringConstant(val=_unwrap_string(n), span=span)\n    if \"BitString\" in n or \"bsval\" in n:\n        n = _unwrap(n, 'BitString')\n        n = _unwrap(n, 'str')\n        n = _unwrap(n, 'bsval')\n        n = _unwrap(n, 'bsval')\n        return pgast.BitStringConstant(kind=n[0], val=n[1:], span=span)\n    raise PSqlUnsupportedError(n)\n\n\ndef _build_range_subselect(n: Node, c: Context) -> pgast.RangeSubselect:\n    return pgast.RangeSubselect(\n        alias=_maybe(n, c, \"alias\", _build_alias) or pgast.Alias(aliasname=\"\"),\n        lateral=_bool_or_false(n, \"lateral\"),\n        subquery=_build_query(n[\"subquery\"], c),\n    )\n\n\ndef _build_range_function(n: Node, c: Context) -> pgast.RangeFunction:\n    return pgast.RangeFunction(\n        alias=_maybe(n, c, \"alias\", _build_alias) or pgast.Alias(aliasname=\"\"),\n        lateral=_bool_or_false(n, \"lateral\"),\n        with_ordinality=_bool_or_false(n, \"ordinality\"),\n        is_rowsfrom=_bool_or_false(n, \"is_rowsfrom\"),\n        functions=[\n            _build_base_expr(fn, c)\n            for fn in n[\"functions\"][0][\"List\"][\"items\"]\n            if len(fn) > 0\n        ],\n    )\n\n\ndef _build_join_expr(n: Node, c: Context) -> pgast.JoinExpr:\n    return pgast.JoinExpr(\n        alias=_maybe(n, c, \"alias\", _build_alias) or pgast.Alias(aliasname=\"\"),\n        larg=_build_base_range_var(n[\"larg\"], c),\n        joins=[\n            pgast.JoinClause(\n                type=n[\"jointype\"][5:],\n                rarg=_build_base_range_var(n[\"rarg\"], c),\n                using_clause=_maybe_list(\n                    n, c, \"usingClause\", _build_str, _as_column_ref\n                ),\n                quals=_maybe(n, c, \"quals\", _build_base_expr),\n            )\n        ],\n    )\n\n\ndef _build_rel_range_var(n: Node, c: Context) -> pgast.RelRangeVar:\n    return pgast.RelRangeVar(\n        alias=_maybe(n, c, \"alias\", _build_alias) or pgast.Alias(aliasname=\"\"),\n        relation=_build_relation(n, c),\n        include_inherited=_bool_or_false(n, \"inh\"),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_alias(n: Node, c: Context) -> pgast.Alias:\n    return pgast.Alias(\n        aliasname=_build_str(n[\"aliasname\"], c),\n        colnames=_maybe_list(n, c, \"colnames\", _build_str),\n    )\n\n\ndef _build_base_relation(n: Node, c: Context) -> pgast.BaseRelation:\n    return _enum(\n        pgast.BaseRelation,\n        n,\n        c,\n        {\n            \"SelectStmt\": _build_select_stmt,\n            \"Relation\": _build_relation,\n        },\n    )\n\n\ndef _build_relation(n: Node, c: Context) -> pgast.Relation:\n    return pgast.Relation(\n        name=_maybe(n, c, \"relname\", _build_str),\n        catalogname=_maybe(n, c, \"catalogname\", _build_str),\n        schemaname=_maybe(n, c, \"schemaname\", _build_str),\n        is_temporary=_maybe(n, c, \"relpersistence\", lambda n, _c: n == 't'),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_implicit_row(n: Node, c: Context) -> pgast.ImplicitRowExpr:\n    if isinstance(n, list):\n        n = n[0]\n    n = _unwrap(n, \"List\")\n\n    return pgast.ImplicitRowExpr(\n        args=[_build_base_expr(e, c) for e in n[\"items\"] if len(e) > 0],\n    )\n\n\ndef _build_array_expr(n: Node, c: Context) -> pgast.ArrayExpr:\n    return pgast.ArrayExpr(elements=_list(n, c, \"elements\", _build_base_expr))\n\n\ndef _build_a_expr(n: Node, c: Context) -> pgast.BaseExpr:\n    names: list[str] = _list(n, c, 'name', _build_str)\n    if names[0] == 'pg_catalog':\n        names.pop(0)\n    name = names.pop(0)\n\n    if n[\"kind\"] == \"AEXPR_OP\":\n        pass\n    elif n[\"kind\"] in (\"AEXPR_LIKE\", \"AEXPR_ILIKE\"):\n        op = name\n        if op.endswith(\"*\"):\n            name = \"ILIKE\"\n        else:\n            name = \"LIKE\"\n        if op.startswith(\"!\"):\n            name = \"NOT \" + name\n    elif n[\"kind\"] == \"AEXPR_IN\":\n        if name == \"<>\":\n            name = \"NOT IN\"\n        else:\n            name = \"IN\"\n    elif n[\"kind\"] in (\"AEXPR_OP_ANY\", \"AEXPR_OP_ALL\"):\n        operator = \"ANY\" if n[\"kind\"] == \"AEXPR_OP_ANY\" else \"ALL\"\n\n        return pgast.SubLink(\n            operator=name + \" \" + operator,\n            test_expr=_maybe(n, c, \"lexpr\", _build_base_expr),\n            expr=_build_base_expr(n[\"rexpr\"], c),\n            span=_build_span(n, c),\n        )\n    elif n['kind'] == 'AEXPR_NULLIF':\n        return pgast.FuncCall(\n            name=('nullif',),\n            args=[\n                _build_base_expr(n['lexpr'], c),\n                _build_base_expr(n['rexpr'], c),\n            ],\n        )\n    elif n['kind'] == 'AEXPR_DISTINCT':\n        name = 'IS DISTINCT FROM'\n    elif n['kind'] == 'AEXPR_NOT_DISTINCT':\n        name = 'IS NOT DISTINCT FROM'\n    else:\n        raise PSqlUnsupportedError(n)\n\n    return pgast.Expr(\n        name=name,\n        lexpr=_maybe(n, c, \"lexpr\", _build_base_expr),\n        rexpr=_maybe(n, c, \"rexpr\", _build_base_expr),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_func_call(n: Node, c: Context) -> pgast.FuncCall:\n    n = _unwrap(n, \"FuncCall\")\n\n    return pgast.FuncCall(\n        name=tuple(_list(n, c, \"funcname\", _build_str)),\n        args=_maybe_list(n, c, \"args\", _build_base_expr) or [],\n        agg_order=(\n            _maybe_list(n, c, \"aggOrder\", _build_sort_by)\n            or _maybe_list(n, c, \"agg_order\", _build_sort_by)\n        ),\n        agg_filter=_maybe(n, c, \"aggFilter\", _build_base_expr),\n        agg_star=_bool_or_false(n, \"agg_star\"),\n        agg_distinct=_bool_or_false(n, \"agg_distinct\"),\n        agg_within_group=_bool_or_false(n, \"agg_within_group\"),\n        over=_maybe(n, c, \"over\", _build_window_def),\n        with_ordinality=_bool_or_false(n, \"withOrdinality\"),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_coalesce(n: Node, c: Context) -> pgast.CoalesceExpr:\n    return pgast.CoalesceExpr(\n        args=_list(n, c, \"args\", _build_base_expr),\n    )\n\n\ndef _build_index_or_slice(n: Node, c: Context) -> pgast.Slice | pgast.Index:\n    if 'is_slice' in n and n['is_slice']:\n        return pgast.Slice(\n            lidx=_build_base_expr(n['lidx'], c),\n            ridx=_build_base_expr(n['uidx'], c),\n        )\n    else:\n        idx = n['lidx'] if 'lidx' in n else n['uidx']\n        return pgast.Index(\n            idx=_build_base_expr(idx, c),\n        )\n\n\ndef _build_res_target(n: Node, c: Context) -> pgast.ResTarget:\n    n = _unwrap(n, \"ResTarget\")\n    return pgast.ResTarget(\n        name=_maybe(n, c, \"name\", _build_str),\n        val=_build_base_expr(n[\"val\"], c),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_insert_target(n: Node, c: Context) -> pgast.InsertTarget:\n    n = _unwrap(n, \"ResTarget\")\n    return pgast.InsertTarget(\n        name=_build_str(n['name'], c),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_update_targets(\n    target_list: list[Node], c: Context\n) -> list[pgast.UpdateTarget | pgast.MultiAssignRef]:\n    targets: list[pgast.UpdateTarget | pgast.MultiAssignRef] = []\n    while len(target_list) > 0:\n        val: dict = target_list[0][\"ResTarget\"][\"val\"]\n        if first_mar := val.get(\"MultiAssignRef\", None):\n            ncolumns = first_mar['ncolumns']\n\n            columns: list[str] = []\n            for _ in range(ncolumns):\n                target = target_list.pop(0)\n                mar = target['ResTarget']\n\n                if 'indirection' in mar:\n                    raise PSqlUnsupportedError(\n                        val, f\"multi-assign SET with indirection\"\n                    )\n\n                columns.append(mar['name'])\n\n            targets.append(_build_multi_assign_ref(first_mar, columns, c))\n        else:\n            target = target_list.pop(0)\n            targets.append(_build_update_target(target, c))\n\n    return targets\n\n\ndef _build_multi_assign_ref(\n    n: Node, columns: list[str], c: Context\n) -> pgast.MultiAssignRef:\n    return pgast.MultiAssignRef(\n        source=_build_base_expr(n['source'], c),\n        columns=columns,\n        span=_build_span(n, c),\n    )\n\n\ndef _build_update_target(\n    n: Node, c: Context\n) -> pgast.UpdateTarget | pgast.MultiAssignRef:\n    n = _unwrap(n, \"ResTarget\")\n    return pgast.UpdateTarget(\n        name=_build_str(n['name'], c),\n        val=_build_base_expr(n['val'], c),\n        indirection=_maybe_list(n, c, \"indirection\", _build_indirection_op),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_window_def(n: Node, c: Context) -> pgast.WindowDef:\n    return pgast.WindowDef(\n        name=_maybe(n, c, \"name\", _build_str),\n        refname=_maybe(n, c, \"refname\", _build_str),\n        partition_clause=_maybe_list(n, c, \"partitionClause\", _build_base_expr),\n        order_clause=_maybe_list(n, c, \"orderClause\", _build_sort_by),\n        frame_options=None,\n        start_offset=_maybe(n, c, \"startOffset\", _build_base_expr),\n        end_offset=_maybe(n, c, \"endOffset\", _build_base_expr),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_sort_by(n: Node, c: Context) -> pgast.SortBy:\n    n = _unwrap(n, \"SortBy\")\n    return pgast.SortBy(\n        node=_build_base_expr(n[\"node\"], c),\n        dir=_maybe(n, c, \"sortby_dir\", _build_sort_order),\n        nulls=_maybe(n, c, \"sortby_nulls\", _build_nones_order),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_locking_clause(n: Node, c: Context) -> pgast.LockingClause:\n    n = _unwrap(n, \"LockingClause\")\n\n    match n[\"strength\"]:\n        case \"LCS_FORUPDATE\":\n            strength = pgast.LockClauseStrength.UPDATE\n        case \"LCS_FORNOKEYUPDATE\":\n            strength = pgast.LockClauseStrength.NO_KEY_UPDATE\n        case \"LCS_FORSHARE\":\n            strength = pgast.LockClauseStrength.SHARE\n        case \"LCS_FORKEYSHARE\":\n            strength = pgast.LockClauseStrength.KEY_SHARE\n        case lcs:\n            raise PSqlUnsupportedError(n, f\"FOR lock strength: {lcs}\")\n\n    if pol := n.get(\"waitPolicy\"):\n        wait_policy = getattr(pgast.LockWaitPolicy, pol, None)\n        if wait_policy is None:\n            raise PSqlUnsupportedError(n, f\"FOR wait policy: {pol}\")\n    else:\n        wait_policy = None\n\n    return pgast.LockingClause(\n        strength=strength,\n        locked_rels=_maybe_list(\n            n, c, \"lockedRels\", _build_rel_range_var, unwrap=\"RangeVar\"\n        ),\n        wait_policy=wait_policy,\n    )\n\n\ndef _build_nones_order(n: Node, _c: Context) -> Optional[qlast.NonesOrder]:\n    if n == \"SORTBY_NULLS_FIRST\":\n        return qlast.NonesFirst\n    if n == \"SORTBY_NULLS_LAST\":\n        return qlast.NonesLast\n    return None\n\n\ndef _build_sort_order(n: Node, _c: Context) -> Optional[qlast.SortOrder]:\n    if n == \"SORTBY_DESC\":\n        return qlast.SortOrder.Desc\n    if n == \"SORTBY_ASC\":\n        return qlast.SortOrder.Asc\n    if n == \"SORTBY_DEFAULT\":\n        return None\n    raise PSqlUnsupportedError(n)\n\n\ndef _build_column_ref(n: Node, c: Context) -> pgast.ColumnRef:\n    return pgast.ColumnRef(\n        name=_list(n, c, \"fields\", _build_string_or_star),\n        optional=_maybe(n, c, \"optional\", _build_bool),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_on_conflict(n: Node, c: Context) -> pgast.OnConflictClause:\n    action_str = _build_str(n[\"action\"], c)\n    if action_str == \"ONCONFLICT_NOTHING\":\n        action = pgast.OnConflictAction.DO_NOTHING\n    elif action_str == \"ONCONFLICT_UPDATE\":\n        action = pgast.OnConflictAction.DO_UPDATE\n    else:\n        raise PSqlUnsupportedError(n, f\"ON CONFLICT {action_str}\")\n\n    return pgast.OnConflictClause(\n        action=action,\n        target=_maybe(n, c, \"infer\", _build_on_conflict_target),\n        update_list=_maybe(n, c, \"targetList\", _build_update_targets) or [],\n        update_where=_maybe(n, c, \"whereClause\", _build_base_expr),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_on_conflict_target(n: Node, c: Context) -> pgast.OnConflictTarget:\n    return pgast.OnConflictTarget(\n        index_elems=_maybe_list(n, c, \"indexElems\", _build_index_elem),\n        index_where=_maybe(n, c, \"whereClause\", _build_base_expr),\n        constraint_name=_maybe(n, c, \"conname\", _build_str),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_index_elem(n: Node, c: Context) -> pgast.IndexElem:\n    n = _unwrap(n, 'IndexElem')\n\n    expr: pgast.BaseExpr\n    if 'name' in n:\n        expr = pgast.ColumnRef(name=(n['name'],))\n    elif 'indexcolname' in n:\n        expr = pgast.ColumnRef(name=(n['name'],))\n    elif 'expr' in n:\n        expr = _build_base_expr(n['expr'], c)\n    else:\n        raise PSqlUnsupportedError(n)\n\n    # TODO:\n    # collation\n    # opclass\n    # opclassopts\n\n    return pgast.IndexElem(\n        expr=expr,\n        ordering=_build_sort_order(n['ordering'], c),\n        nulls_ordering=_build_nones_order(n['nulls_ordering'], c),\n        span=_build_span(n, c),\n    )\n\n\ndef _build_star(_n: Node, _c: Context) -> pgast.Star | str:\n    return pgast.Star()\n\n\ndef _build_string_or_star(node: Node, c: Context) -> pgast.Star | str:\n    return _enum(\n        pgast.Star | str,  # type: ignore\n        node,\n        c,\n        {\"String\": _build_str, \"A_Star\": _build_star},\n    )\n\n\ndef _build_copy_format(n: Node, c: Context) -> pgast.CopyFormat:\n    val = _build_str(n, c)\n    if val == 'text':\n        return pgast.CopyFormat.TEXT\n    if val == 'csv':\n        return pgast.CopyFormat.CSV\n    if val == 'binary':\n        return pgast.CopyFormat.BINARY\n    raise PSqlUnsupportedError(val, f\"{val} format\")\n\n\ndef _build_copy_options(nodes: list[Node], c: Context) -> pgast.CopyOptions:\n    opt = pgast.CopyOptions()\n    for n in nodes:\n        if 'DefElem' not in n or 'defname' not in n['DefElem']:\n            continue\n        n = n['DefElem']\n        def_name = n['defname']\n        arg = n['arg'] if 'arg' in n else None\n\n        if def_name == 'format' and arg:\n            opt.format = _build_copy_format(arg, c)\n\n        elif def_name == 'freeze':\n            opt.freeze = _build_str(arg, c) == 'true' if arg else True\n\n        elif def_name == 'delimiter' and arg:\n            opt.delimiter = _build_str(arg, c)\n\n        elif def_name == 'null' and arg:\n            opt.null = _build_str(arg, c)\n\n        elif def_name == 'header' and arg:\n            opt.header = _build_str(arg, c) == 'true' if arg else True\n\n        elif def_name == 'quote' and arg:\n            opt.quote = _build_str(arg, c)\n\n        elif def_name == 'escape' and arg:\n            opt.escape = _build_str(arg, c)\n\n        elif def_name == 'force_quote':\n            arg = _unwrap(arg, 'List')\n            opt.force_quote = _list(arg, c, 'items', _build_str)\n\n        elif def_name == 'force_not_null':\n            arg = _unwrap(arg, 'List')\n            opt.force_not_null = _list(arg, c, 'items', _build_str)\n\n        elif def_name == 'force_null':\n            arg = _unwrap(arg, 'List')\n            opt.force_null = _list(arg, c, 'items', _build_str)\n\n        elif def_name == 'encoding':\n            opt.encoding = _build_str(arg, c)\n    return opt\n\n\ndef _build_copy(n: Node, c: Context) -> pgast.CopyStmt:\n    return pgast.CopyStmt(\n        relation=_maybe(n, c, 'relation', _build_relation),\n        colnames=_maybe_list(n, c, 'attlist', _build_str),\n        query=_maybe(n, c, 'query', _build_query),\n        is_from=_bool_or_false(n, 'is_from'),\n        is_program=_bool_or_false(n, 'is_program'),\n        filename=_maybe(n, c, 'filename', _build_str),\n        options=(\n            _maybe(n, c, 'options', _build_copy_options) or pgast.CopyOptions()\n        ),\n        where_clause=_maybe(n, c, \"whereClause\", _build_base_expr),\n        span=_build_span(n, c),\n    )\n"
  },
  {
    "path": "edb/pgsql/parser/exceptions.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2010-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport re\nfrom typing import Any, Optional\n\n\nclass PSqlParseError(Exception):\n    pass\n\n\nclass PSqlSyntaxError(PSqlParseError):\n    def __init__(\n        self,\n        message: str,\n        cursor_pos: int,  # 0-based\n        query_source: str,\n    ):\n        self.message = message\n        self.cursor_pos = cursor_pos\n        self.query_source = query_source\n\n    def __str__(self):\n        return self.message\n\n\nclass PSqlUnsupportedError(PSqlParseError):\n    node: Optional[Any]\n    location: Optional[int]\n    message: str\n\n    def __init__(self, node: Optional[Any] = None, feat: Optional[str] = None):\n        self.node = node\n        self.location = None\n        self.message = \"not supported\"\n        if feat:\n            self.message += f\": {feat}\"\n\n    def __str__(self):\n        return self.message\n\n\ndef get_node_name(name: str) -> str:\n    \"\"\"\n    Given a node name (CreateTableStmt), this function tries to guess the SQL\n    command text (CREATE TABLE).\n    \"\"\"\n\n    name = name.removesuffix('Stmt').removesuffix('Expr')\n    name = re.sub(r'(?<!^)(?=[A-Z])', ' ', name)\n    return name.upper()\n"
  },
  {
    "path": "edb/pgsql/parser/parser.pxd",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2010-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom libc.stdint cimport uint8_t\n\nfrom edb.server.pgproto.pgproto cimport (\n    ReadBuffer,\n    WriteBuffer,\n)\n\n\ncdef class Source:\n    cdef:\n        str _text\n        bytes _serialized\n        bytes _cache_key\n\n    cdef WriteBuffer _serialize(self)\n\n\ncdef class NormalizedSource(Source):\n    cdef:\n        str _orig_text\n        int _highest_extern_param_id\n        list _extracted_constants\n"
  },
  {
    "path": "edb/pgsql/parser/parser.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2010-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import (\n    Any,\n    NamedTuple,\n    Optional,\n)\n\nimport enum\nimport hashlib\n\nfrom .exceptions import PSqlSyntaxError\n\n\nfrom edb.server.pgproto.pgproto cimport (\n    FRBuffer,\n    ReadBuffer,\n    WriteBuffer,\n)\n\nfrom libc.stdint cimport int8_t, uint8_t, int32_t\n\n\ncdef extern from \"pg_query.h\":\n    ctypedef struct PgQueryError:\n        char *message\n        int lineno\n        int cursorpos\n\n    ctypedef struct PgQueryParseResult:\n        char *parse_tree\n        PgQueryError *error\n\n    ctypedef struct PgQueryNormalizeConstLocation:\n        int location\n        int length\n        int param_id\n        int token\n        char *val\n\n    ctypedef struct PgQueryNormalizeResult:\n        char *normalized_query\n        PgQueryError *error\n        PgQueryNormalizeConstLocation *clocations\n        int clocations_count\n        int highest_extern_param_id\n\n    PgQueryParseResult pg_query_parse(const char *input)\n    void pg_query_free_parse_result(PgQueryParseResult result)\n\n    PgQueryNormalizeResult pg_query_normalize(const char *input)\n    void pg_query_free_normalize_result(PgQueryNormalizeResult result)\n\n\ncdef extern from \"protobuf/pg_query.pb-c.h\":\n    ctypedef struct ProtobufCEnumValue:\n        const char *name\n        const char *c_name\n        int value\n\n    ctypedef struct ProtobufCEnumDescriptor:\n        pass\n\n    ProtobufCEnumDescriptor pg_query__token__descriptor\n\n    const ProtobufCEnumValue *protobuf_c_enum_descriptor_get_value(\n        const ProtobufCEnumDescriptor *desc, int value)\n\n\ndef pg_parse(query: str) -> str:\n    cdef:\n        PgQueryParseResult result\n        bytes queryb\n\n    queryb = query.encode(\"utf-8\")\n    result = pg_query_parse(queryb)\n\n    if result.error:\n        error = PSqlSyntaxError(\n            result.error.message.decode('utf8'),\n            result.error.cursorpos,\n            query,\n        )\n        pg_query_free_parse_result(result)\n        raise error\n\n    result_utf8 = result.parse_tree.decode('utf8')\n    pg_query_free_parse_result(result)\n    return result_utf8\n\n\nclass LiteralTokenType(enum.StrEnum):\n    FCONST = \"FCONST\"\n    SCONST = \"SCONST\"\n    BCONST = \"BCONST\"\n    XCONST = \"XCONST\"\n    ICONST = \"ICONST\"\n    TRUE_P = \"TRUE_P\"\n    FALSE_P = \"FALSE_P\"\n\n\nclass PgLiteralTypeOID(enum.IntEnum):\n    BOOL = 16\n    INT4 = 23\n    TEXT = 25\n    UNKNOWN = 705\n    VARBIT = 1562\n    NUMERIC = 1700\n\n\nclass NormalizedQuery(NamedTuple):\n    text: str\n    highest_extern_param_id: int\n    extracted_constants: list[tuple[int, LiteralTokenType, bytes]]\n\n\ndef pg_normalize(query: str) -> NormalizedQuery:\n    cdef:\n        PgQueryNormalizeResult result\n        PgQueryNormalizeConstLocation loc\n        const ProtobufCEnumValue *token\n        int i\n        bytes queryb\n        bytes const\n\n    queryb = query.encode(\"utf-8\")\n    result = pg_query_normalize(queryb)\n\n    try:\n        if result.error:\n            error = PSqlSyntaxError(\n                result.error.message.decode('utf8'),\n                result.error.cursorpos,\n                query,\n            )\n            raise error\n\n        normalized_query = result.normalized_query.decode('utf8')\n        consts = []\n        for i in range(result.clocations_count):\n            loc = result.clocations[i]\n            if loc.length != -1:\n                if loc.param_id < 0:\n                    # Negative param_id means *relative* to highest explicit\n                    # param id (after taking the absolute value).\n                    param_id = (\n                        abs(loc.param_id)\n                        + result.highest_extern_param_id\n                    )\n                else:\n                    # Otherwise it's the absolute param id.\n                    param_id = loc.param_id\n                if loc.val != NULL:\n                    token = protobuf_c_enum_descriptor_get_value(\n                        &pg_query__token__descriptor, loc.token)\n                    if token == NULL:\n                        raise RuntimeError(\n                            f\"could not lookup pg_query enum descriptor \"\n                            f\"for token value {loc.token}\"\n                        )\n                    consts.append((\n                        param_id,\n                        LiteralTokenType(bytes(token.name).decode(\"ascii\")),\n                        bytes(loc.val),\n                    ))\n\n        return NormalizedQuery(\n            text=normalized_query,\n            highest_extern_param_id=result.highest_extern_param_id,\n            extracted_constants=consts,\n        )\n    finally:\n        pg_query_free_normalize_result(result)\n\n\ncdef ReadBuffer _init_deserializer(serialized: bytes, tag: uint8_t, cls: str):\n    cdef ReadBuffer buf\n\n    buf = ReadBuffer.new_message_parser(serialized)\n\n    if <uint8_t>buf.read_byte() != tag:\n        raise ValueError(f\"malformed {cls} serialization\")\n\n    return buf\n\n\ncdef class Source:\n    def __init__(\n        self,\n        text: str,\n        serialized: Optional[bytes] = None,\n    ) -> None:\n        self._text = text\n        if serialized is not None:\n            self._serialized = serialized\n        else:\n            self._serialized = b''\n        self._cache_key = b''\n\n    @classmethod\n    def _tag(self) -> int:\n        return 0\n\n    cdef WriteBuffer _serialize(self):\n        cdef WriteBuffer buf = WriteBuffer.new()\n        buf.write_byte(<int8_t>self._tag())\n        buf.write_len_prefixed_utf8(self._text)\n        return buf\n\n    def serialize(self) -> bytes:\n        if not self._serialized:\n            self._serialized = bytes(self._serialize())\n        return self._serialized\n\n    @classmethod\n    def from_serialized(cls, serialized: bytes) -> Source:\n        cdef ReadBuffer buf\n\n        buf = _init_deserializer(serialized, cls._tag(), cls.__name__)\n        text = buf.read_len_prefixed_utf8()\n\n        return Source(text, serialized)\n\n    def text(self) -> str:\n        return self._text\n\n    def original_text(self) -> str:\n        return self._text\n\n    def cache_key(self) -> bytes:\n        if not self._cache_key:\n            h = hashlib.blake2b(self._tag().to_bytes())\n            h.update(bytes(self.text(), 'UTF-8'))\n\n            # Include types of extracted constants\n            for extra_type_oid in self.extra_type_oids():\n                h.update(extra_type_oid.to_bytes(8, signed=True))\n            self._cache_key = h.digest()\n\n        return self._cache_key\n\n    def variables(self) -> dict[str, Any]:\n        return {}\n\n    def first_extra(self) -> Optional[int]:\n        return None\n\n    def extra_counts(self) -> Sequence[int]:\n        return []\n\n    def extra_blobs(self) -> list[bytes]:\n        return []\n\n    def extra_formatted_as_text(self) -> bool:\n        return True\n\n    def extra_type_oids(self) -> Sequence[int]:\n        return ()\n\n    @classmethod\n    def from_string(cls, text: str) -> Source:\n        return Source(text)\n\n    def denormalized(self) -> Source:\n        return self\n\n\ncdef class NormalizedSource(Source):\n    def __init__(\n        self,\n        normalized: NormalizedQuery,\n        orig_text: str,\n        serialized: Optional[bytes] = None,\n    ) -> None:\n        super().__init__(text=normalized.text, serialized=serialized)\n        self._extracted_constants = list(\n            sorted(normalized.extracted_constants, key=lambda i: i[0]),\n        )\n        self._highest_extern_param_id = normalized.highest_extern_param_id\n        self._orig_text = orig_text\n\n    @classmethod\n    def _tag(cls) -> int:\n        return 1\n\n    def original_text(self) -> str:\n        return self._orig_text\n\n    cdef WriteBuffer _serialize(self):\n        cdef WriteBuffer buf\n\n        buf = Source._serialize(self)\n        buf.write_len_prefixed_utf8(self._orig_text)\n        buf.write_int32(<int32_t>self._highest_extern_param_id)\n        buf.write_int32(<int32_t>len(self._extracted_constants))\n        for param_id, token, val in self._extracted_constants:\n            buf.write_int32(<int32_t>param_id)\n            buf.write_len_prefixed_utf8(token.value)\n            buf.write_len_prefixed_bytes(val)\n\n        return buf\n\n    def variables(self) -> dict[str, bytes]:\n        return {f\"${n}\": v for n, _, v in self._extracted_constants}\n\n    def first_extra(self) -> Optional[int]:\n        return (\n            self._highest_extern_param_id\n            if self._extracted_constants\n            else None\n        )\n\n    def extra_counts(self) -> Sequence[int]:\n        return [len(self._extracted_constants)]\n\n    def extra_blobs(self) -> list[bytes]:\n        cdef WriteBuffer buf\n        buf = WriteBuffer.new()\n        for _, _, v in self._extracted_constants:\n            buf.write_len_prefixed_bytes(v)\n\n        return [bytes(buf)]\n\n    def extra_type_oids(self) -> Sequence[int]:\n        oids = []\n        for _, token, _ in self._extracted_constants:\n            if token is LiteralTokenType.FCONST:\n                oids.append(PgLiteralTypeOID.NUMERIC)\n            elif token is LiteralTokenType.ICONST:\n                oids.append(PgLiteralTypeOID.INT4)\n            elif (\n                token is LiteralTokenType.FALSE_P\n                or token is LiteralTokenType.TRUE_P\n            ):\n                oids.append(PgLiteralTypeOID.BOOL)\n            elif token is LiteralTokenType.SCONST:\n                oids.append(PgLiteralTypeOID.UNKNOWN)\n            elif (\n                token is LiteralTokenType.XCONST\n                or token is LiteralTokenType.BCONST\n            ):\n                oids.append(PgLiteralTypeOID.VARBIT)\n            else:\n                raise AssertionError(f\"unexpected literal token type: {token}\")\n\n        return oids\n\n    @classmethod\n    def from_string(cls, text: str) -> NormalizedSource:\n        normalized = pg_normalize(text)\n        return NormalizedSource(normalized, text)\n\n    @classmethod\n    def from_serialized(cls, serialized: bytes) -> NormalizedSource:\n        cdef ReadBuffer buf\n\n        buf = _init_deserializer(serialized, cls._tag(), cls.__name__)\n        text = buf.read_len_prefixed_utf8()\n        orig_text = buf.read_len_prefixed_utf8()\n        highest_extern_param_id = buf.read_int32()\n        n_constants = buf.read_int32()\n        consts = []\n        for _ in range(n_constants):\n            param_id = buf.read_int32()\n            token = buf.read_len_prefixed_utf8()\n            val = buf.read_len_prefixed_bytes()\n            consts.append((param_id, LiteralTokenType(token), val))\n\n        return NormalizedSource(\n            NormalizedQuery(\n                text=text,\n                highest_extern_param_id=highest_extern_param_id,\n                extracted_constants=consts,\n            ),\n            orig_text,\n            serialized,\n        )\n\n    def denormalized(self) -> Source:\n        return Source.from_string(self._orig_text)\n\ndef deserialize(serialized: bytes) -> Source:\n    if serialized[0] == 0:\n        return Source.from_serialized(serialized)\n    elif serialized[0] == 1:\n        return NormalizedSource.from_serialized(serialized)\n\n    raise ValueError(f\"Invalid type/version byte: {serialized[0]}\")\n"
  },
  {
    "path": "edb/pgsql/patches.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Patches to apply to databases\"\"\"\n\nfrom __future__ import annotations\n\n\ndef get_patch_level(num_patches: int) -> int:\n    return sum(p.startswith('edgeql+schema') for p, _ in PATCHES[:num_patches])\n\n\ndef get_version_key(num_patches: int) -> str:\n    \"\"\"Produce a version key to add to instdata keys after major patches.\n\n    Patches that modify the schema class layout and introspection queries\n    are not safe to downgrade from. So for such patches, we add a version\n    suffix to the names of the core instdata entries that we would need to\n    update, so that we don't clobber the old version.\n\n    After a downgrade, we'll have more patches applied than we\n    actually know exist in the running version, but since we compute\n    the key based on the number of schema layout patches that we can\n    *see*, we still compute the right key.\n    \"\"\"\n    level = get_patch_level(num_patches)\n    if level == 0:\n        return ''\n    else:\n        return f'_v{level}'\n\n\n\"\"\"\nThe actual list of patches. The patches are (kind, script) pairs.\n\nThe current kinds are:\n * sql - simply runs a SQL script\n * metaschema-sql - create a function from metaschema\n * edgeql - runs an edgeql DDL command\n * edgeql+schema - runs an edgeql DDL command and updates the std schemas\n *                 NOTE: objects and fields added to the reflschema must\n *                 have their patch_level set to the `get_patch_level` value\n *                 for this patch.\n * edgeql+user_ext|<extname> - updates extensions installed in user databases\n *                           - should be paired with an ext-pkg patch\n * ...+config - updates config views\n * ext-pkg - installs an extension package given a name\n * repair - fix up inconsistencies in *user* schemas\n * sql-introspection - refresh all sql introspection views\n * ...+testmode - only run the patch in testmode. Works with any patch kind.\n\"\"\"\nPATCHES: list[tuple[str, str]] = [\n]\n"
  },
  {
    "path": "edb/pgsql/patches_6x.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Patches copied over from 6.x.\n\nDeeply unfortunately, we need to be able to apply the user_ext patches...\n\n\"\"\"\n\nfrom __future__ import annotations\n\n\n\"\"\"\nThe actual list of patches. The patches are (kind, script) pairs.\n\nThe current kinds are:\n * sql - simply runs a SQL script\n * metaschema-sql - create a function from metaschema\n * edgeql - runs an edgeql DDL command\n * edgeql+schema - runs an edgeql DDL command and updates the std schemas\n *                 NOTE: objects and fields added to the reflschema must\n *                 have their patch_level set to the `get_patch_level` value\n *                 for this patch.\n * edgeql+user_ext|<extname> - updates extensions installed in user databases\n *                           - should be paired with an ext-pkg patch\n * ...+config - updates config views\n * ext-pkg - installs an extension package given a name\n * repair - fix up inconsistencies in *user* schemas\n * sql-introspection - refresh all sql introspection views\n * ...+testmode - only run the patch in testmode. Works with any patch kind.\n\"\"\"\nPATCHES: list[tuple[str, str]] = [\n    # 6.0b2\n    # One of the sql-introspection's adds a param with a default to\n    # uuid_to_oid, so we need to drop the original to avoid ambiguity.\n    ('sql', '''\ndrop function if exists edgedbsql_v6_2f20b3fed0.uuid_to_oid(uuid) cascade\n'''),\n    ('sql-introspection', ''),\n    ('metaschema-sql', 'SysConfigFullFunction'),\n    # 6.0rc1\n    ('edgeql+schema+config+testmode', '''\nCREATE SCALAR TYPE cfg::TestEnabledDisabledEnum\n    EXTENDING enum<Enabled, Disabled>;\nALTER TYPE cfg::AbstractConfig {\n    CREATE PROPERTY __check_function_bodies -> cfg::TestEnabledDisabledEnum {\n        CREATE ANNOTATION cfg::internal := 'true';\n        CREATE ANNOTATION cfg::backend_setting := '\"check_function_bodies\"';\n        SET default := cfg::TestEnabledDisabledEnum.Enabled;\n    };\n};\n'''),\n    ('metaschema-sql', 'PostgresConfigValueToJsonFunction'),\n    ('metaschema-sql', 'SysConfigFullFunction'),\n    ('edgeql', '''\nALTER FUNCTION\nstd::assert_single(\n    input: SET OF anytype,\n    NAMED ONLY message: OPTIONAL str = <str>{},\n) {\n    SET volatility := 'Immutable';\n};\nALTER FUNCTION\nstd::assert_exists(\n    input: SET OF anytype,\n    NAMED ONLY message: OPTIONAL str = <str>{},\n) {\n    SET volatility := 'Immutable';\n};\nALTER FUNCTION\nstd::assert_distinct(\n    input: SET OF anytype,\n    NAMED ONLY message: OPTIONAL str = <str>{},\n) {\n    SET volatility := 'Immutable';\n};\n'''),\n     ('edgeql+schema+config', '''\nCREATE SCALAR TYPE sys::TransactionAccessMode\n    EXTENDING enum<ReadOnly, ReadWrite>;\n\n\nCREATE SCALAR TYPE sys::TransactionDeferrability\n    EXTENDING enum<Deferrable, NotDeferrable>;\n\nALTER TYPE cfg::AbstractConfig {\n    CREATE REQUIRED PROPERTY default_transaction_isolation\n        -> sys::TransactionIsolation\n    {\n        CREATE ANNOTATION cfg::affects_compilation := 'true';\n        CREATE ANNOTATION cfg::backend_setting :=\n            '\"default_transaction_isolation\"';\n        CREATE ANNOTATION std::description :=\n            'Controls the default isolation level of each new transaction, \\\n            including implicit transactions. Defaults to `Serializable`. \\\n            Note that changing this to a lower isolation level implies \\\n            that the transactions are also read-only by default regardless \\\n            of the value of the `default_transaction_access_mode` setting.';\n        SET default := sys::TransactionIsolation.Serializable;\n    };\n\n    CREATE REQUIRED PROPERTY default_transaction_access_mode\n        -> sys::TransactionAccessMode\n    {\n        CREATE ANNOTATION cfg::affects_compilation := 'true';\n        CREATE ANNOTATION std::description :=\n            'Controls the default read-only status of each new transaction, \\\n            including implicit transactions. Defaults to `ReadWrite`. \\\n            Note that if `default_transaction_isolation` is set to any value \\\n            other than Serializable this parameter is implied to be \\\n            `ReadOnly` regardless of the actual value.';\n        SET default := sys::TransactionAccessMode.ReadWrite;\n    };\n\n    CREATE REQUIRED PROPERTY default_transaction_deferrable\n        -> sys::TransactionDeferrability\n    {\n        CREATE ANNOTATION cfg::backend_setting :=\n            '\"default_transaction_deferrable\"';\n        CREATE ANNOTATION std::description :=\n            'Controls the default deferrable status of each new transaction. \\\n            It currently has no effect on read-write transactions or those \\\n            operating at isolation levels lower than `Serializable`. \\\n            The default is `NotDeferrable`.';\n        SET default := sys::TransactionDeferrability.NotDeferrable;\n    };\n};\n'''),\n    # 6.2\n    ('ext-pkg', 'ai'),\n    ('edgeql+user_ext+config|ai', '''\nalter type ext::ai::EmbeddingModel {\n    drop annotation\n      ext::ai::embedding_model_max_batch_tokens;\n    create annotation\n      ext::ai::embedding_model_max_batch_tokens := \"8191\";\n}\n'''),\n    # 6.3\n    ('repair', ''),  # For #8466\n    # 6.5\n    ('sql-introspection', ''),  # For #8511\n    ('edgeql+user_ext|ai', r'''\nupdate ext::ai::ChatPrompt filter .name = 'builtin::rag-default' set {\n    messages += (insert ext::ai::ChatPromptMessage {\n                    participant_role := ext::ai::ChatParticipantRole.User,\n                    content := (\n                        \"Query: {query}\\n\\\n                         Answer: \"\n                    ),\n                })\n}\n'''),  # For #8553\n    # 6.6\n    ('edgeql+schema', ''),  # For #8554\n    ('ext-pkg', 'ai'),  # For #8521, #8646\n    ('edgeql+user_ext+config|ai', '''\n    create function ext::ai::search(\n        object: anyobject,\n        query: str,\n    ) -> optional tuple<object: anyobject, distance: float64>\n    {\n        create annotation std::description := '\n            Search an object using its ext::ai::index index.\n            Gets an embedding for the query from the ai provider then\n            returns objects that match the specified semantic query and the\n            similarity score.\n        ';\n        set volatility := 'Stable';\n        # Needed to pick up the indexes when used in ORDER BY.\n        set prefer_subquery_args := true;\n        set server_param_conversions :=\n            '{\"query\": [\"ai_text_embedding\", \"object\"]}';\n        using sql expression;\n    };\n\n    alter scalar type ext::ai::ProviderAPIStyle\n        extending enum<OpenAI, Anthropic, Ollama>;\n\n    create type ext::ai::OllamaProviderConfig\n      extending ext::ai::ProviderConfig {\n        alter property name {\n            set protected := true;\n            set default := 'builtin::ollama';\n        };\n\n        alter property display_name {\n            set protected := true;\n            set default := 'Ollama';\n        };\n\n        alter property api_url {\n            set default := 'http://localhost:11434/api'\n        };\n\n        alter property secret {\n            set default := ''\n        };\n\n        alter property api_style {\n            set protected := true;\n            set default := ext::ai::ProviderAPIStyle.Ollama;\n        };\n    };\n\n    # Ollama embedding models\n    create abstract type ext::ai::OllamaLlama_3_2_Model\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"llama3.2\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::ollama\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"131072\";\n    };\n\n    create abstract type ext::ai::OllamaLlama_3_3_Model\n        extending ext::ai::TextGenerationModel\n    {\n        alter annotation\n            ext::ai::model_name := \"llama3.3\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::ollama\";\n        alter annotation\n            ext::ai::text_gen_model_context_window := \"131072\";\n    };\n\n    create abstract type ext::ai::OllamaNomicEmbedTextModel\n        extending ext::ai::EmbeddingModel\n    {\n        alter annotation\n            ext::ai::model_name := \"nomic-embed-text\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::ollama\";\n        alter annotation\n            ext::ai::embedding_model_max_input_tokens := \"8192\";\n        alter annotation\n            ext::ai::embedding_model_max_batch_tokens := \"8192\";\n        alter annotation\n            ext::ai::embedding_model_max_output_dimensions := \"768\";\n    };\n\n    create abstract type ext::ai::OllamaBgeM3Model\n        extending ext::ai::EmbeddingModel\n    {\n        alter annotation\n            ext::ai::model_name := \"bge-m3\";\n        alter annotation\n            ext::ai::model_provider := \"builtin::ollama\";\n        alter annotation\n            ext::ai::embedding_model_max_input_tokens := \"8192\";\n        alter annotation\n            ext::ai::embedding_model_max_batch_tokens := \"8192\";\n        alter annotation\n            ext::ai::embedding_model_max_output_dimensions := \"1024\";\n    };\n'''),\n    # 6.8\n    ('edgeql+user+remove_pointless_triggers', ''),\n    ('edgeql', '''\nCREATE FUNCTION\nstd::to_bytes(j: std::json) -> std::bytes {\n    CREATE ANNOTATION std::description :=\n        'Convert a json value to a binary UTF-8 string.';\n    SET volatility := 'Immutable';\n    USING (to_bytes(to_str(j)));\n};\n'''),\n    ('metaschema-sql', 'ArrayIndexWithBoundsFunction'),\n    ('metaschema-sql', 'ArraySliceFunction'),\n    ('metaschema-sql', 'StringIndexWithBoundsFunction'),\n    ('metaschema-sql', 'BytesIndexWithBoundsFunction'),\n    ('metaschema-sql', 'StringSliceFunction'),\n    ('metaschema-sql', 'BytesSliceFunction'),\n    ('metaschema-sql', 'JSONIndexByTextFunction'),\n    ('metaschema-sql', 'JSONIndexByIntFunction'),\n    ('metaschema-sql', 'JSONSliceFunction'),\n    ('edgeql', '''\nCREATE MODULE std::lang;\n\nCREATE MODULE std::lang::go;\nCREATE ABSTRACT ANNOTATION std::lang::go::type;\n\nCREATE MODULE std::lang::js;\nCREATE ABSTRACT ANNOTATION std::lang::js::type;\n\nCREATE MODULE std::lang::py;\nCREATE ABSTRACT ANNOTATION std::lang::py::type;\n\nCREATE MODULE std::lang::rs;\nCREATE ABSTRACT ANNOTATION std::lang::rs::type;\n'''),\n    # 6.9\n    ('edgeql', '''\nCREATE FUNCTION\nstd::__pg_generate_series(\n    `start`: std::int64,\n    stop: std::int64\n) -> SET OF std::int64\n{\n    SET volatility := 'Immutable';\n    USING SQL FUNCTION 'generate_series';\n};\n'''),\n\n    # !!!!!! 7.x !!!!!\n    ('edgeql+user_ext+config|auth', '''\n    create type ext::auth::OneTimeCode extending ext::auth::Auditable {\n        create required property code_hash: std::bytes {\n            create constraint exclusive;\n            create annotation std::description :=\n                \"The securely hashed one-time code.\";\n        };\n        create required property expires_at: std::datetime {\n            create annotation std::description :=\n                \"The date and time when the code expires.\";\n        };\n        create index on (.expires_at);\n\n        create required link factor: ext::auth::Factor {\n            on target delete delete source;\n        };\n    };\n\n    create scalar type ext::auth::AuthenticationAttemptType extending std::enum<\n        SignIn,\n        EmailVerification,\n        PasswordReset,\n        MagicLink,\n        OneTimeCode\n    >;\n\n    create type ext::auth::AuthenticationAttempt\n    extending ext::auth::Auditable {\n        create required link factor: ext::auth::Factor {\n            on target delete delete source;\n        };\n        create required property attempt_type:\n            ext::auth::AuthenticationAttemptType {\n            create annotation std::description :=\n                \"The type of authentication attempt being made.\";\n        };\n        create required property successful: std::bool {\n            create annotation std::description :=\n                \"Whether this authentication attempt was successful.\";\n        };\n    };\n\n    create scalar type ext::auth::VerificationMethod\n    extending std::enum<Link, Code>;\n\n    alter type ext::auth::EmailPasswordProviderConfig {\n        create required property verification_method:\n            ext::auth::VerificationMethod {\n            set default := ext::auth::VerificationMethod.Link;\n        };\n    };\n\n    alter type ext::auth::WebAuthnProviderConfig {\n        create required property verification_method:\n            ext::auth::VerificationMethod {\n            set default := ext::auth::VerificationMethod.Link;\n        };\n    };\n\n    alter type ext::auth::MagicLinkProviderConfig {\n        create required property verification_method:\n            ext::auth::VerificationMethod {\n            set default := ext::auth::VerificationMethod.Link;\n        };\n    };\n\n    alter scalar type ext::auth::WebhookEvent extending std::enum<\n        IdentityCreated,\n        IdentityAuthenticated,\n        EmailFactorCreated,\n        EmailVerified,\n        EmailVerificationRequested,\n        PasswordResetRequested,\n        MagicLinkRequested,\n        OneTimeCodeRequested,\n        OneTimeCodeVerified,\n    >;\n\n'''),\n]\n"
  },
  {
    "path": "edb/pgsql/resolver/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Optional\n\nimport copy\nimport dataclasses\n\nfrom edb.common import debug\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import codegen as pgcodegen\nfrom edb.schema import schema as s_schema\n\nfrom edb.server.compiler import dbstate, enums\n\nfrom . import dispatch\nfrom . import context\nfrom . import expr  # NOQA\nfrom . import relation  # NOQA\nfrom . import command  # NOQA\n\nOptions = context.Options\n\n\n@dataclasses.dataclass(kw_only=True, eq=False, frozen=True, repr=False)\nclass ResolvedSQL:\n    # AST representing the query that can be sent to PostgreSQL\n    ast: pgast.Base\n\n    # Optionally, AST representing the query returning data in EdgeQL\n    # format (i.e. single-column output).\n    edgeql_output_format_ast: Optional[pgast.Base]\n\n    # Special behavior for \"tag\" of \"CommandComplete\" message of this query.\n    command_complete_tag: Optional[dbstate.CommandCompleteTag]\n\n    # query parameters\n    params: list[dbstate.SQLParam]\n\n    capabilities: enums.Capability = enums.Capability.NONE\n\n\ndef resolve(\n    query: pgast.Query | pgast.CopyStmt,\n    schema: s_schema.Schema,\n    options: context.Options,\n) -> ResolvedSQL:\n\n    if debug.flags.sql_input:\n        debug.header('SQL Input')\n\n        debug_sql_text = pgcodegen.generate_source(\n            query, reordered=True, pretty=True\n        )\n        debug.dump_code(debug_sql_text, lexer='sql')\n\n    ctx = context.ResolverContextLevel(\n        None, context.ContextSwitchMode.EMPTY, schema=schema, options=options\n    )\n\n    _ = context.ResolverContext(initial=ctx)\n\n    command.init_external_params(query, ctx)\n    top_level_ctes = command.compile_dml(query, ctx=ctx)\n\n    resolved: pgast.Base\n    if isinstance(query, pgast.Query):\n        resolved, resolved_table = dispatch.resolve_relation(query, ctx=ctx)\n    elif isinstance(query, pgast.CopyStmt):\n        resolved = dispatch.resolve(query, ctx=ctx)\n        resolved_table = None\n    else:\n        raise AssertionError()\n\n    if limit := ctx.options.implicit_limit:\n        resolved = apply_implicit_limit(resolved, limit, resolved_table, ctx)\n\n    command.fini_external_params(ctx)\n\n    if top_level_ctes:\n        assert isinstance(resolved, pgast.Query)\n        if not resolved.ctes:\n            resolved.ctes = []\n        resolved.ctes.extend(top_level_ctes)\n\n    # when the top-level query is DML statement, clients will expect a tag in\n    # the CommandComplete message that describes the number of modified rows.\n    # Since our resolved SQL does not have a top-level DML stmt, we need to\n    # override that tag.\n    command_complete_tag: Optional[dbstate.CommandCompleteTag] = None\n    if isinstance(query, pgast.DMLQuery):\n        prefix: str\n        if isinstance(query, pgast.InsertStmt):\n            prefix = 'INSERT 0 '\n        elif isinstance(query, pgast.DeleteStmt):\n            prefix = 'DELETE '\n        elif isinstance(query, pgast.UpdateStmt):\n            prefix = 'UPDATE '\n\n        if query.returning_list:\n            # resolved SQL will return a result, we count those rows\n            command_complete_tag = dbstate.TagCountMessages(prefix=prefix)\n        else:\n            # resolved SQL will contain an injected COUNT clause\n            # we instruct io process to unpack that\n            command_complete_tag = dbstate.TagUnpackRow(prefix=prefix)\n\n    if debug.flags.sql_output:\n        debug.header('SQL Output')\n\n        debug_sql_text = pgcodegen.generate_source(\n            resolved, pretty=True, reordered=True\n        )\n        debug.dump_code(debug_sql_text, lexer='sql')\n\n    if options.include_edgeql_io_format_alternative:\n        edgeql_output_format_ast = copy.copy(resolved)\n        if e := as_plain_select(edgeql_output_format_ast, resolved_table, ctx):\n            # Turn the query into one that returns a ROW.\n            #\n            # We need to do this by injecting a new query and putting\n            # the old one in its FROM clause, since things like\n            # DISTINCT/ORDER BY care about what exact columns are in\n            # the target list.\n            columns = []\n            for i, target in enumerate(e.target_list):\n                if not target.name:\n                    e.target_list[i] = target = target.replace(name=f'__i~{i}')\n                    assert target.name\n                columns.append(pgast.ColumnRef(name=(target.name,)))\n\n            edgeql_output_format_ast = pgast.SelectStmt(\n                target_list=[\n                    pgast.ResTarget(\n                        val=expr.construct_row_expr(columns, ctx=ctx)\n                    )\n                ],\n                from_clause=[pgast.RangeSubselect(\n                    subquery=e,\n                    alias=pgast.Alias(aliasname='r'),\n                )],\n                ctes=e.ctes,\n            )\n            e.ctes = []\n    else:\n        edgeql_output_format_ast = None\n\n    return ResolvedSQL(\n        ast=resolved,\n        edgeql_output_format_ast=edgeql_output_format_ast,\n        command_complete_tag=command_complete_tag,\n        params=ctx.query_params,\n        capabilities=ctx.env.capabilities,\n    )\n\n\ndef as_plain_select(\n    query: pgast.Base,\n    table: Optional[context.Table],\n    ctx: context.ResolverContextLevel,\n) -> Optional[pgast.SelectStmt]:\n    if not isinstance(query, pgast.Query):\n        return None\n    assert table\n\n    if (\n        isinstance(query, pgast.SelectStmt)\n        and not query.op\n        and not query.values\n    ):\n        return query\n\n    table.alias = \"t\"\n    return pgast.SelectStmt(\n        from_clause=[\n            pgast.RangeSubselect(\n                subquery=query,\n                alias=pgast.Alias(aliasname=\"t\"),\n            )\n        ],\n        target_list=[\n            pgast.ResTarget(\n                name=f'column{index + 1}',\n                val=expr.resolve_column_kind(table, c.kind, ctx=ctx),\n            )\n            for index, c in enumerate(table.columns)\n        ],\n    )\n\n\ndef apply_implicit_limit(\n    expr: pgast.Base,\n    limit: int,\n    table: Optional[context.Table],\n    ctx: context.ResolverContextLevel,\n) -> pgast.Base:\n    e = as_plain_select(expr, table, ctx)\n    if not e:\n        return expr\n\n    if e.limit_count is None:\n        e.limit_count = pgast.NumericConstant(val=str(limit))\n    return e\n"
  },
  {
    "path": "edb/pgsql/resolver/command.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"SQL resolver that compiles public SQL to internal SQL which is executable\nin our internal Postgres instance.\"\"\"\n\nfrom typing import Optional, Iterable, Mapping\nimport dataclasses\nimport functools\nimport uuid\n\nfrom edb.server.pgcon import errors as pgerror\nfrom edb.server.compiler import dbstate\n\nfrom edb.common import ast\nfrom edb.common.typeutils import not_none\n\nfrom edb import errors\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql.parser import parser as pg_parser\nfrom edb.pgsql import compiler as pgcompiler\nfrom edb.pgsql import types as pgtypes\nfrom edb.pgsql.compiler import enums as pgce\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\nfrom edb.edgeql import compiler as qlcompiler\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtypeutils\n\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import links as s_links\nfrom edb.schema import properties as s_properties\nfrom edb.schema import name as sn\nfrom edb.schema import types as s_types\nfrom edb.schema import utils as s_utils\nfrom edb.server.compiler import enums\n\nfrom . import dispatch\nfrom . import context\nfrom . import expr as pg_res_expr\nfrom . import relation as pg_res_rel\n\nContext = context.ResolverContextLevel\n\n\n@dispatch._resolve.register\ndef resolve_CopyStmt(stmt: pgast.CopyStmt, *, ctx: Context) -> pgast.CopyStmt:\n\n    query: Optional[pgast.Query]\n\n    if stmt.query:\n        query = dispatch.resolve(stmt.query, ctx=ctx)\n\n    elif stmt.relation:\n        relation, table = dispatch.resolve_relation(stmt.relation, ctx=ctx)\n        table.reference_as = ctx.alias_generator.get('rel')\n\n        selected_columns = _pull_columns_from_table(\n            table,\n            ((c, stmt.span) for c in stmt.colnames) if stmt.colnames else None,\n        )\n\n        # The relation being copied is potentially a view and views cannot be\n        # copied if referenced by name, so we just always wrap it into a SELECT.\n\n        # This is probably a view based on edgedb schema, so wrap it into\n        # a select query.\n        query = pgast.SelectStmt(\n            from_clause=[\n                pgast.RelRangeVar(\n                    alias=pgast.Alias(aliasname=table.reference_as),\n                    relation=relation,\n                )\n            ],\n            target_list=[\n                pgast.ResTarget(\n                    val=pg_res_expr.resolve_column_kind(table, c.kind, ctx=ctx)\n                )\n                for c in selected_columns\n            ],\n        )\n    else:\n        raise AssertionError('CopyStmt must either have relation or query set')\n\n    # WHERE\n    where = dispatch.resolve_opt(stmt.where_clause, ctx=ctx)\n\n    # COPY will always be top-level, so we must extract CTEs\n    if not query.ctes:\n        query.ctes = list()\n    query.ctes.extend(ctx.ctes_buffer)\n    ctx.ctes_buffer.clear()\n\n    _validate_no_params(ctx)\n\n    return pgast.CopyStmt(\n        relation=None,\n        colnames=None,\n        query=query,\n        is_from=stmt.is_from,\n        is_program=stmt.is_program,\n        filename=stmt.filename,\n        # TODO: forbid some options?\n        options=stmt.options,\n        where_clause=where,\n    )\n\n\ndef _validate_no_params(ctx: Context):\n    if len(ctx.query_params) == 0:\n        return\n    has_globals = any(\n        isinstance(p, dbstate.SQLParamGlobal) and not p.is_permission\n        for p in ctx.query_params\n    )\n    has_permissions = any(\n        isinstance(p, dbstate.SQLParamGlobal) and p.is_permission\n        for p in ctx.query_params\n    )\n    hint: str | None\n    if has_globals or has_permissions:\n        if has_globals and has_permissions:\n            offender = \"globals or permissions\"\n        elif has_globals:\n            offender = \"globals\"\n        elif has_permissions:\n            offender = \"permissions\"\n        offender += \" in computed properties and access policies\"\n        hint = \"To disable policies, set apply_access_policies_pg := false\"\n    else:\n        offender = \"query parameters\"\n        hint = None\n\n    raise errors.QueryError(\n        f\"COPY cannot use {offender}\",\n        pgext_code=pgerror.ERROR_FEATURE_NOT_SUPPORTED,\n        hint=hint,\n    )\n\n\ndef _pull_columns_from_table(\n    table: context.Table,\n    col_names: Optional[Iterable[tuple[str, pgast.Span | None]]],\n) -> list[context.Column]:\n    if not col_names:\n        return [c for c in table.columns if not c.hidden]\n\n    col_map: dict[str, context.Column] = {\n        col.name: col for col in table.columns\n    }\n\n    res = []\n    for name, span in col_names:\n        col = col_map.get(name, None)\n        if not col:\n            raise errors.QueryError(\n                f'column {name} does not exist',\n                span=span,\n            )\n        res.append(col)\n    return res\n\n\ndef compile_dml(\n    stmt: pgast.Base, *, ctx: Context\n) -> list[pgast.CommonTableExpr]:\n    # extract all dml stmts\n    dml_stmts_sql = _collect_dml_stmts(stmt)\n    if len(dml_stmts_sql) == 0:\n        return []\n\n    # un-compile each SQL dml stmt into EdgeQL\n    stmts = [_uncompile_dml_stmt(s, ctx=ctx) for s in dml_stmts_sql]\n\n    # merge EdgeQL stmts & compile to SQL\n    ctx.compiled_dml, ctes = _compile_uncompiled_dml(stmts, ctx=ctx)\n\n    return ctes\n\n\ndef _collect_dml_stmts(stmt: pgast.Base) -> list[pgast.DMLQuery]:\n    if not isinstance(stmt, pgast.Query):\n        return []\n\n    # DML can only be in the top-level statement or its CTEs.\n    # If it is in any of the nested CTEs, throw errors later on\n    res: list[pgast.DMLQuery] = []\n    if stmt.ctes:\n        for cte in stmt.ctes:\n            if isinstance(cte.query, pgast.DMLQuery):\n                res.append(cte.query)\n\n    if isinstance(stmt, pgast.DMLQuery):\n        res.append(stmt)\n    return res\n\n\nExternalRel = tuple[pgast.BaseRelation, tuple[pgce.PathAspect, ...]]\n\n\n@dataclasses.dataclass(kw_only=True, eq=False, repr=False)\nclass UncompiledDML:\n    # the input DML node\n    input: pgast.Query\n\n    # schema object associated with the table that is the subject of the DML\n    subject: s_objtypes.ObjectType | s_links.Link | s_properties.Property\n\n    # EdgeQL equivalent to the input node\n    ql_stmt: qlast.Expr\n\n    # additional params needed during compilation of the edgeql node\n    ql_returning_shape: list[qlast.ShapeElement]\n    ql_singletons: set[irast.PathId]\n    ql_anchors: Mapping[str, irast.PathId]\n    external_rels: Mapping[irast.PathId, ExternalRel]\n\n    # list of column names of the subject type, along with pointer name\n    # these columns will be available within RETURNING clause\n    subject_columns: list[tuple[str, str]]\n\n    stype_refs: dict[uuid.UUID, list[qlast.Set]]\n\n    # data needed for stitching the compiled ast into the resolver output\n    early_result: context.CompiledDML\n\n\n@functools.singledispatch\ndef _uncompile_dml_stmt(stmt: pgast.DMLQuery, *, ctx: Context):\n    \"\"\"\n    Takes an SQL DML query and produces an equivalent EdgeQL query plus a bunch\n    of metadata needed to extract associated CTEs from result of the EdgeQL\n    compiler.\n\n    In this context:\n    - subject is the object type/pointer being updated,\n    - source is the source of the subject (when subject is a pointer),\n    - value is the relation that provides new value to be inserted/updated,\n    - ptr-s are (usually) pointers on the subject.\n    \"\"\"\n\n    raise dispatch._raise_unsupported(stmt)\n\n\ndef _uncompile_dml_subject(\n    rvar: pgast.RelRangeVar, *, ctx: Context\n) -> tuple[\n    context.Table, s_objtypes.ObjectType | s_links.Link | s_properties.Property\n]:\n    \"\"\"\n    Determines the subject object of a DML operation.\n    This can either be an ObjectType or a Pointer for link tables.\n    \"\"\"\n\n    assert isinstance(rvar.relation, pgast.Relation)\n    _sub_rel, sub_table = pg_res_rel.resolve_relation(\n        rvar.relation, include_inherited=False, ctx=ctx\n    )\n    if not sub_table.schema_id:\n        # this happens doing DML on an introspection table or (somehow) a CTE\n        raise errors.QueryError(\n            msg=f'cannot write into table \"{sub_table.name}\"',\n            pgext_code=pgerror.ERROR_UNDEFINED_TABLE,\n        )\n\n    sub = ctx.schema.get_by_id(sub_table.schema_id)\n    assert isinstance(\n        sub, (s_objtypes.ObjectType, s_links.Link, s_properties.Property)\n    )\n    return sub_table, sub\n\n\ndef _uncompile_subject_columns(\n    sub: s_objtypes.ObjectType | s_links.Link | s_properties.Property,\n    sub_table: context.Table,\n    res: UncompiledDML,\n    *,\n    ctx: Context,\n):\n    '''\n    Instruct UncompiledDML to wrap the EdgeQL DML into a select shape that\n    selects all pointers.\n    This is applied when a RETURNING clause is present and these columns might\n    be used in the clause.\n    '''\n    for column in sub_table.columns:\n        if column.hidden:\n            continue\n        _, ptr_name, _ = _get_pointer_for_column(column, sub, ctx)\n        res.subject_columns.append((column.name, ptr_name))\n\n\n@_uncompile_dml_stmt.register\ndef _uncompile_insert_stmt(\n    stmt: pgast.InsertStmt, *, ctx: Context\n) -> UncompiledDML:\n    # determine the subject object\n    sub_table, sub = _uncompile_dml_subject(stmt.relation, ctx=ctx)\n\n    expected_columns = _pull_columns_from_table(\n        sub_table,\n        ((c.name, c.span) for c in stmt.cols) if stmt.cols else None,\n    )\n\n    res: UncompiledDML\n    if isinstance(sub, s_objtypes.ObjectType):\n        res = _uncompile_insert_object_stmt(\n            stmt, sub, sub_table, expected_columns, ctx=ctx\n        )\n    elif isinstance(sub, (s_links.Link, s_properties.Property)):\n        res = _uncompile_insert_pointer_stmt(\n            stmt, sub, sub_table, expected_columns, ctx=ctx\n        )\n    else:\n        raise NotImplementedError()\n\n    if stmt.returning_list:\n        _uncompile_subject_columns(sub, sub_table, res, ctx=ctx)\n    return res\n\n\ndef _uncompile_insert_object_stmt(\n    stmt: pgast.InsertStmt,\n    sub: s_objtypes.ObjectType,\n    sub_table: context.Table,\n    expected_columns: list[context.Column],\n    *,\n    ctx: Context,\n) -> UncompiledDML:\n    \"\"\"\n    Translates a 'SQL INSERT into an object type table' to an EdgeQL insert.\n    \"\"\"\n\n    subject_id = irast.PathId.from_type(\n        ctx.schema,\n        sub,\n        env=None,\n    )\n\n    # handle DEFAULT and prepare the value relation\n    value_relation, expected_columns = _uncompile_default_value(\n        stmt.select_stmt, stmt.ctes, expected_columns, sub, ctx=ctx\n    )\n\n    # if we are sure that we are inserting a single row\n    # we can skip for-loops and iterators, which produces better SQL\n    is_value_single = _has_at_most_one_row(stmt.select_stmt)\n\n    # prepare anchors for inserted value columns\n    value_name = ctx.alias_generator.get('ins_val')\n    iterator_name = ctx.alias_generator.get('ins_iter')\n    value_id = irast.PathId.from_type(\n        ctx.schema,\n        sub,\n        typename=sn.QualName('__derived__', value_name),\n        env=None,\n    )\n\n    value_ql: qlast.PathElement = (\n        qlast.IRAnchor(name=value_name)\n        if is_value_single\n        else qlast.ObjectRef(name=iterator_name)\n    )\n\n    # a phantom relation that is supposed to hold the inserted value\n    # (in the resolver, this will be replaced by the real value relation)\n    value_cte_name = ctx.alias_generator.get('ins_value')\n    value_rel = pgast.Relation(\n        name=value_cte_name,\n        strip_output_namespaces=True,\n    )\n    value_columns = []\n    insert_shape = []\n    stype_refs: dict[uuid.UUID, list[qlast.Set]] = {}\n    for index, expected_col in enumerate(expected_columns):\n        ptr, ptr_name, is_link = _get_pointer_for_column(expected_col, sub, ctx)\n        value_columns.append((expected_col.name, ptr_name, is_link))\n\n        # inject type annotation into value relation\n        _try_inject_ptr_type_cast(value_relation, index, ptr, ctx)\n\n        # prepare the outputs of the source CTE\n        ptr_id = _get_ptr_id(value_id, ptr, ctx)\n        output = pgast.ColumnRef(name=(ptr_name,), nullable=True)\n        if is_link:\n            value_rel.path_outputs[(ptr_id, pgce.PathAspect.IDENTITY)] = output\n            value_rel.path_outputs[(ptr_id, pgce.PathAspect.VALUE)] = output\n        else:\n            value_rel.path_outputs[(ptr_id, pgce.PathAspect.VALUE)] = output\n\n        if ptr_name == 'id':\n            value_rel.path_outputs[(value_id, pgce.PathAspect.VALUE)] = output\n\n        # prepare insert shape that will use the paths from source_outputs\n        insert_shape.append(\n            _construct_assign_element_for_ptr(\n                value_ql,\n                ptr_name,\n                ptr,\n                is_link,\n                ctx,\n                stype_refs,\n            )\n        )\n\n    # source needs an iterator column, so we need to invent one\n    # Here we only decide on the name of that iterator column, the actual column\n    # is generated later, when resolving the DML stmt.\n    value_iterator = ctx.alias_generator.get('iter')\n    output = pgast.ColumnRef(name=(value_iterator,))\n    value_rel.path_outputs[(value_id, pgce.PathAspect.ITERATOR)] = output\n    if not any(c.name == 'id' for c in expected_columns):\n        value_rel.path_outputs[(value_id, pgce.PathAspect.VALUE)] = output\n\n    # construct the EdgeQL DML AST\n    sub_name = sub.get_name(ctx.schema)\n    ql_stmt_insert = qlast.InsertQuery(\n        subject=s_utils.name_to_ast_ref(sub_name),\n        shape=insert_shape,\n    )\n\n    ql_stmt: qlast.Expr = ql_stmt_insert\n    if not is_value_single:\n        # value relation might contain multiple rows\n        # to express this in EdgeQL, we must wrap `insert` into a `for` query\n        ql_stmt = qlast.ForQuery(\n            iterator=qlast.Path(steps=[qlast.IRAnchor(name=value_name)]),\n            iterator_alias=iterator_name,\n            result=ql_stmt,\n        )\n\n    # on conflict\n    conflict = _uncompile_on_conflict(\n        stmt, sub, sub_table, value_id, ctx, stype_refs\n    )\n    if conflict:\n        ql_stmt_insert.unless_conflict = conflict.ql_unless_conflict\n\n    ql_returning_shape: list[qlast.ShapeElement] = []\n    if stmt.returning_list:\n        # construct the shape that will extract all needed column of the subject\n        # table (because they might be be used by RETURNING clause)\n        for column in sub_table.columns:\n            if column.hidden:\n                continue\n            _, ptr_name, _ = _get_pointer_for_column(column, sub, ctx)\n            ql_returning_shape.append(\n                qlast.ShapeElement(\n                    expr=qlast.Path(steps=[qlast.Ptr(name=ptr_name)]),\n                )\n            )\n\n    ql_singletons = {value_id}\n    ql_anchors = {value_name: value_id}\n    external_rels: dict[irast.PathId, ExternalRel] = {\n        value_id: (\n            value_rel,\n            (pgce.PathAspect.SOURCE,),\n        )\n    }\n\n    subject_columns = None\n    if conflict and conflict.update_name is not None:\n        assert conflict.update_id\n        assert conflict.update_input_placeholder\n\n        # inject path_output for identity aspect\n        # (that will be injected after resolving)\n        conflict.update_input_placeholder.path_outputs[\n            (conflict.update_id, pgce.PathAspect.VALUE)\n        ] = pgast.ColumnRef(name=('id',))\n        conflict.update_input_placeholder.path_outputs[\n            (conflict.update_id, pgce.PathAspect.IDENTITY)\n        ] = pgast.ColumnRef(name=('id',))\n\n        # register __cu__ as singleton and provided by an external rel\n        ql_singletons.add(conflict.update_id)\n        ql_anchors[conflict.update_name] = conflict.update_id\n        external_rels[conflict.update_id] = (\n            conflict.update_input_placeholder,\n            (\n                pgce.PathAspect.SOURCE,\n                pgce.PathAspect.VALUE,\n                pgce.PathAspect.IDENTITY,\n            ),\n        )\n\n        # subject columns are needed to pull them into the \"conflict update\" rel\n        subject_columns = []\n        for column in sub_table.columns:\n            if column.hidden:\n                continue\n            ptr, _, _ = _get_pointer_for_column(column, sub, ctx)\n            path_id = _get_ptr_id(subject_id, ptr, ctx)\n            subject_columns.append((column.name, path_id))\n\n    return UncompiledDML(\n        input=stmt,\n        subject=sub,\n        ql_stmt=ql_stmt,\n        ql_returning_shape=ql_returning_shape,\n        ql_singletons=ql_singletons,\n        ql_anchors=ql_anchors,\n        external_rels=external_rels,\n        stype_refs=stype_refs,\n        early_result=context.CompiledDML(\n            value_cte_name=value_cte_name,\n            value_relation_input=value_relation,\n            value_columns=value_columns,\n            value_iterator_name=value_iterator,\n\n            conflict_update_input=conflict.update_input if conflict else None,\n            conflict_update_name=conflict.update_name if conflict else None,\n            conflict_update_iterator=(\n                conflict.update_iterator if conflict else None\n            ),\n            subject_id=subject_id,\n            subject_columns=subject_columns,\n            value_id=value_id,\n            # these will be populated after compilation\n            output_ctes=[],\n            output_relation_name='',\n            output_namespace={},\n        ),\n        # these will be populated by _uncompile_dml_stmt\n        subject_columns=[],\n    )\n\n\n@dataclasses.dataclass(kw_only=True, frozen=True, repr=False, eq=False)\nclass UncompileOnConflict:\n    ql_unless_conflict: tuple[qlast.Expr | None, qlast.Expr | None]\n\n    # relation (that still has to be resolved) which will provide the values of\n    # columns that must be updated by the ON CONFLICT clause\n    update_input: Optional[pgast.Query] = None\n\n    # name of the CTE that will contain resolved update_input\n    update_name: Optional[str] = None\n\n    # name of the IR anchor which provides the iterator to the update stmt\n    update_iterator: Optional[str] = None\n\n    # IR id which can be used as an anchor that will refer to the update_input\n    update_id: Optional[irast.PathId] = None\n\n    # a dummy relation that has all necessary path_outputs set, so it can be\n    # passed into external_rels\n    update_input_placeholder: Optional[pgast.Relation] = None\n\n\n# Uncompiles pg INSERT ON CONFLICT into edgeql UNLESS CONFLICT.\n# Will produce:\n# - qlast unless conflict that contain an empty node or an update stmt,\n# - update_input relation (that provides values to the UPDATE stmt) and a bunch\n#   of related variables needed for compiling it.\ndef _uncompile_on_conflict(\n    stmt: pgast.InsertStmt,\n    sub: s_objtypes.ObjectType,\n    sub_table: context.Table,\n    value_id: irast.PathId,\n    ctx: Context,\n    stype_refs: dict[uuid.UUID, list[qlast.Set]]\n) -> Optional[UncompileOnConflict]:\n    if not stmt.on_conflict:\n        return None\n\n    # determine the target constraint\n    on_clause: Optional[qlast.Expr] = None\n    if stmt.on_conflict.target:\n        tgt = stmt.on_conflict.target\n        if tgt.constraint_name:\n            raise errors.UnsupportedFeatureError(\n                'ON CONFLICT ON CONSTRAINT',\n                span=tgt.span,\n            )\n        if tgt.index_where:\n            raise errors.UnsupportedFeatureError(\n                'ON CONFLICT WHERE',\n                span=tgt.span,\n            )\n        index_col_names: list[tuple[str, qlast.Span | None]] = []\n        for e in tgt.index_elems or []:\n            if e.nulls_ordering or e.ordering:\n                raise errors.UnsupportedFeatureError(\n                    'ON CONFLICT index ordering',\n                    span=tgt.span,\n                )\n            if not (\n                isinstance(e.expr, pgast.ColumnRef)\n                and len(e.expr.name) == 1\n                and isinstance(e.expr.name[0], str)\n            ):\n                raise errors.UnsupportedFeatureError(\n                    'ON CONFLICT supports only plain column names',\n                )\n            index_col_names.append((e.expr.name[0], e.expr.span))\n\n        index_cols = _pull_columns_from_table(sub_table, iter(index_col_names))\n\n        index_paths: list[qlast.Expr] = []\n        for index_col in index_cols:\n            _ptr, ptr_name, _is_link = _get_pointer_for_column(\n                index_col, sub, ctx\n            )\n\n            index_paths.append(qlast.Path(\n                partial=True, steps=[qlast.Ptr(name=ptr_name)]\n            ))\n        on_clause = qlast.Tuple(elements=index_paths)\n\n    if stmt.on_conflict.action == pgast.OnConflictAction.DO_NOTHING:\n        return UncompileOnConflict(\n            ql_unless_conflict=(None, None)  # contraints columns, update\n        )\n\n    if not on_clause:\n        raise errors.QueryError(\n            'ON CONFLICT DO UPDATE requires index specification by column '\n            'names',\n            pgext_code=pgerror.ERROR_SYNTAX_ERROR,\n            span=stmt.on_conflict.span,\n        )\n\n    # determine names of updated columns\n    update_col_names = []\n    assert stmt.on_conflict.update_list is not None\n    for col in stmt.on_conflict.update_list:\n        if isinstance(col, pgast.MultiAssignRef):\n            raise errors.UnsupportedFeatureError(\n                'ON CONFLICT UPDATE of multiple columns at once',\n                span=col.span,\n            )\n        assert isinstance(col, pgast.UpdateTarget)\n        if col.indirection:\n            raise errors.UnsupportedFeatureError(\n                'ON CONFLICT UPDATE with index indirection',\n                span=col.span,\n            )\n        update_col_names.append((col.name, col.span))\n\n    update_columns = _pull_columns_from_table(\n        sub_table,\n        iter(update_col_names),\n    )\n\n    # construct update shape\n    update_name = ctx.alias_generator.get('cu')\n    iterator_name = ctx.alias_generator.get('cu_iter')\n    update_id = irast.PathId.from_type(\n        ctx.schema,\n        sub,\n        typename=sn.QualName('__derived__', update_name),\n        env=None,\n    )\n\n    # the shape of the edge ql update we will generate\n    conflict_update_shape = []\n\n    # IR anchor and placeholder relation for the relation that will provide\n    # values for each of the updated columns. This relation will be replaced\n    # by the resolved sql relation.\n    conflict_source_ql = qlast.Path(steps=[qlast.ObjectRef(name=iterator_name)])\n    update_input_placeholder = pgast.Relation(\n        name=update_name,\n        strip_output_namespaces=True,\n    )\n    for column in update_columns:\n        ptr, ptr_name, is_link = _get_pointer_for_column(column, sub, ctx)\n\n        # prepare the outputs of the source CTE\n        ptr_id = _get_ptr_id(update_id, ptr, ctx)\n        output = pgast.ColumnRef(name=(ptr_name,), nullable=True)\n        if is_link:\n            update_input_placeholder.path_outputs[\n                (ptr_id, pgce.PathAspect.IDENTITY)] = output\n            update_input_placeholder.path_outputs[\n                (ptr_id, pgce.PathAspect.VALUE)] = output\n        else:\n            update_input_placeholder.path_outputs[\n                (ptr_id, pgce.PathAspect.VALUE)] = output\n\n        conflict_update_shape.append(\n            _construct_assign_element_for_ptr(\n                conflict_source_ql,\n                ptr_name,\n                ptr,\n                is_link,\n                ctx,\n                stype_refs,\n            )\n        )\n\n    sub_name = sub.get_name(ctx.schema)\n    ql_update = qlast.UpdateQuery(\n        subject=qlast.Path(\n            steps=[s_utils.name_to_ast_ref(sub_name)]\n        ),\n        shape=conflict_update_shape,\n        where=qlast.BinOp(\n            left=qlast.Path(steps=[\n                qlast.ObjectRef(name=iterator_name), qlast.Ptr(name='id')\n            ]),\n            op='=',\n            right=qlast.Path(steps=[\n                s_utils.name_to_ast_ref(sub_name), qlast.Ptr(name='id')\n            ]),\n        )\n    )\n\n    # update_value relation has to be evaluated *for each conflicting row*\n    # to express this in EdgeQL, we must wrap `update` into a `for` query\n    ql_stmt = qlast.ForQuery(\n        iterator=qlast.Path(steps=[qlast.IRAnchor(name=update_name)]),\n        iterator_alias=iterator_name,\n        result=ql_update,\n    )\n\n    # the relation that we will later resolve and which contains all columns\n    # that the user specified to need to be updated. When this is resolved,\n    # it will replace conflict_source_rel CTE in the final compiled output.\n    update_input = pgast.SelectStmt(\n        target_list=[\n            pgast.ResTarget(\n                name=ut.name,\n                val=ut.val,\n                # TODO: UpdateTarget indirections\n            )\n            for ut in stmt.on_conflict.update_list\n            if isinstance(ut, pgast.UpdateTarget)\n        ],\n        where_clause=stmt.on_conflict.update_where,\n    )\n    return UncompileOnConflict(\n        ql_unless_conflict=(on_clause, ql_stmt),\n        update_name=update_name,\n        update_id=update_id,\n        update_input=update_input,\n        update_input_placeholder=update_input_placeholder,\n    )\n\n\ndef _construct_assign_element_for_ptr(\n    source_ql: qlast.PathElement,\n    ptr_name: str,\n    ptr: s_pointers.Pointer,\n    is_link: bool,\n    ctx: context.ResolverContextLevel,\n    stype_refs: dict[uuid.UUID, list[qlast.Set]],\n):\n    ptr_ql: qlast.Expr = qlast.Path(\n        steps=[\n            source_ql,\n            qlast.Ptr(name=ptr_name),\n        ]\n    )\n\n    if is_link:\n        # Convert UUIDs into objects.\n        assert isinstance(ptr_ql, qlast.Path)\n\n        target = ptr.get_target(ctx.schema)\n        assert isinstance(target, s_objtypes.ObjectType)\n\n        ptr_ql = _construct_cast_from_uuid_to_obj_type(\n            ptr_ql, target, stype_refs, optional=True, ctx=ctx\n        )\n\n    return qlast.ShapeElement(\n        expr=qlast.Path(steps=[qlast.Ptr(name=ptr_name)]),\n        operation=qlast.ShapeOperation(op=qlast.ShapeOp.ASSIGN),\n        compexpr=ptr_ql,\n    )\n\n\ndef _construct_cast_from_uuid_to_obj_type(\n    ptr_ql: qlast.Path,\n    object: s_objtypes.ObjectType,\n    stype_refs: dict[uuid.UUID, list[qlast.Set]],\n    *,\n    optional: bool,\n    ctx: Context,\n) -> qlast.Expr:\n    # Constructs AST that converts a UUID provided by ptr_ql to an object type.\n\n    # This mechanism similar to overlays in IR->SQL compiler.\n    # Makes sure that when an object is inserted, later casts from UUID do find\n    # this object. This is needed because this cast is part of the\n    # \"under-the-hood\" mechanism and is not visible to the user. They perceive\n    # plain UUID insertion and they expect FOREIGN KEY constrains to reject\n    # invalid UUIDs.\n\n    # Constructs qlast equivalent to:\n    #   for i in ptr_ql union\n    #     assert_exists((\n    #       select {type_name, #all preceding DML clauses#}\n    #       filter .id = i.id\n    #       limit 1\n    #     ))\n    #   else {}\n\n    object_name: sn.Name = object.get_name(ctx.schema)\n\n    ptr_id_ql = qlast.Path(steps=ptr_ql.steps + [qlast.Ptr(name='id')])\n    if optional:\n        ptr_iter = ctx.alias_generator.get('i')\n        id_ql = qlast.Path(steps=[qlast.ObjectRef(name=ptr_iter)])\n    else:\n        id_ql = ptr_id_ql\n\n    stype_ref = qlast.Set(\n        elements=[\n            qlast.Path(steps=[s_utils.name_to_ast_ref(object_name)]),\n            # here we later inject references to preceding inserts of this type\n        ]\n    )\n    if object.id not in stype_refs:\n        stype_refs[object.id] = []\n    stype_refs[object.id].append(stype_ref)\n\n    res: qlast.Expr = qlast.FunctionCall(\n        func=('std', 'assert_exists'),\n        args=[\n            qlast.SelectQuery(\n                result=stype_ref,\n                where=qlast.BinOp(\n                    left=qlast.Path(partial=True, steps=[qlast.Ptr(name='id')]),\n                    op='=',\n                    right=id_ql,\n                ),\n                # this is needed for cardinality check only: there will\n                # always be at most one object with matching id. It will be\n                # either an existing object or a newly inserted one.\n                limit=qlast.Constant.integer(1),\n            )\n        ],\n        kwargs={\n            'message': qlast.BinOp(\n                left=qlast.Constant.string(\n                    f'object type {object_name} with id \\''\n                ),\n                op='++',\n                right=qlast.BinOp(\n                    left=qlast.TypeCast(\n                        expr=id_ql,\n                        type=qlast.TypeName(\n                            maintype=qlast.ObjectRef(module='std', name='str'),\n                        ),\n                    ),\n                    op='++',\n                    right=qlast.Constant.string(f'\\' does not exist'),\n                ),\n            )\n        },\n    )\n\n    if optional:\n        res = qlast.ForQuery(\n            iterator=ptr_id_ql,\n            iterator_alias=ptr_iter,\n            result=res,\n        )\n\n    return res\n\n\ndef _add_pointer(\n    source: s_objtypes.ObjectType,\n    name: str,\n    target_scls: s_types.Type,\n    *,\n    ctx: Context,\n) -> s_pointers.Pointer:\n\n    base_name = 'link' if target_scls.is_object_type() else 'property'\n    base = ctx.schema.get(\n        sn.QualName('std', base_name),\n        type=s_pointers.Pointer,\n    )\n\n    ctx.schema, ptr = base.derive_ref(\n        ctx.schema,\n        source,\n        name=base.get_derived_name(\n            ctx.schema, source, derived_name_base=sn.QualName('__', name)\n        ),\n        target=target_scls,\n        inheritance_refdicts={'pointers'},\n        mark_derived=True,\n        transient=True,\n    )\n    return ptr\n\n\ndef _uncompile_insert_pointer_stmt(\n    stmt: pgast.InsertStmt,\n    sub: s_links.Link | s_properties.Property,\n    sub_table: context.Table,\n    expected_columns: list[context.Column],\n    *,\n    ctx: Context,\n) -> UncompiledDML:\n    \"\"\"\n    Translates a SQL 'INSERT INTO a link / multi-property table' into\n    an `EdgeQL update SourceObject { subject: ... }`.\n    \"\"\"\n\n    if stmt.on_conflict:\n        raise errors.UnsupportedFeatureError(\n            'ON CONFLICT is not yet supported for link tables',\n            span=stmt.on_conflict.span,\n        )\n\n    if not any(c.name == 'source' for c in expected_columns):\n        raise errors.QueryError(\n            'column source is required when inserting into link tables',\n            span=stmt.span,\n        )\n    if not any(c.name == 'target' for c in expected_columns):\n        raise errors.QueryError(\n            'column target is required when inserting into link tables',\n            span=stmt.span,\n        )\n\n    sub_source = sub.get_source(ctx.schema)\n    assert isinstance(sub_source, s_objtypes.ObjectType)\n    sub_target = sub.get_target(ctx.schema)\n    assert sub_target\n\n    # handle DEFAULT and prepare the value relation\n    value_relation, expected_columns = _uncompile_default_value(\n        stmt.select_stmt, stmt.ctes, expected_columns, sub, ctx=ctx\n    )\n\n    # if we are sure that we are inserting a single row\n    # we can skip for-loops and iterators, which produces better SQL\n    # is_value_single = _has_at_most_one_row(stmt.select_stmt)\n    is_value_single = False\n\n    free_obj_ty = ctx.schema.get('std::FreeObject', type=s_objtypes.ObjectType)\n    ctx.schema, dummy_ty = free_obj_ty.derive_subtype(\n        ctx.schema,\n        name=sn.QualName('__derived__', ctx.alias_generator.get('ins_ty')),\n        mark_derived=True,\n        transient=True,\n    )\n\n    src_ptr = _add_pointer(dummy_ty, '__source__', sub_source, ctx=ctx)\n    tgt_ptr = _add_pointer(dummy_ty, '__target__', sub_target, ctx=ctx)\n\n    # prepare anchors for inserted value columns\n    value_name = ctx.alias_generator.get('ins_val')\n    iterator_name = ctx.alias_generator.get('ins_iter')\n    base_id = irast.PathId.from_type(\n        ctx.schema,\n        dummy_ty,\n        typename=sn.QualName('__derived__', value_name),\n        env=None,\n    )\n    source_id = _get_ptr_id(base_id, src_ptr, ctx=ctx)\n    target_id = _get_ptr_id(base_id, tgt_ptr, ctx=ctx)\n\n    value_ql: qlast.PathElement = (\n        qlast.IRAnchor(name=value_name)\n        if is_value_single\n        else qlast.ObjectRef(name=iterator_name)\n    )\n\n    # a phantom relation that is supposed to hold the inserted value\n    # (in the resolver, this will be replaced by the real value relation)\n    value_cte_name = ctx.alias_generator.get('ins_value')\n    value_rel = pgast.Relation(\n        name=value_cte_name,\n        strip_output_namespaces=True,\n    )\n    value_columns: list[tuple[str, str, bool]] = []\n    for index, expected_col in enumerate(expected_columns):\n        ptr: Optional[s_pointers.Pointer] = None\n        if expected_col.name == 'source':\n            ptr_name = 'source'\n            is_link = True\n            ptr_id = source_id\n        elif expected_col.name == 'target':\n            ptr_name = 'target'\n            is_link = isinstance(sub, s_links.Link)\n            ptr = sub\n            ptr_id = target_id\n        else:\n            # link pointer\n\n            assert isinstance(sub, s_links.Link)\n            ptr_name = expected_col.name\n            ptr = sub.maybe_get_ptr(ctx.schema, sn.UnqualName(ptr_name))\n            assert ptr\n            lprop_tgt = not_none(ptr.get_target(ctx.schema))\n            lprop_ptr = _add_pointer(dummy_ty, ptr_name, lprop_tgt, ctx=ctx)\n            ptr_id = _get_ptr_id(base_id, lprop_ptr, ctx=ctx)\n\n            is_link = False\n\n        var = pgast.ColumnRef(name=(ptr_name,), nullable=True)\n        value_rel.path_outputs[(ptr_id, pgce.PathAspect.VALUE)] = var\n\n        # inject type annotation into value relation\n        if is_link:\n            _try_inject_type_cast(\n                value_relation, index, pgast.TypeName(name=('uuid',))\n            )\n        else:\n            assert ptr\n            _try_inject_ptr_type_cast(value_relation, index, ptr, ctx)\n\n        value_columns.append((expected_col.name, ptr_name, is_link))\n\n    # source needs an iterator column, so we need to invent one\n    # Here we only decide on the name of that iterator column, the actual column\n    # is generated later, when resolving the DML stmt.\n    value_iterator = ctx.alias_generator.get('iter')\n    var = pgast.ColumnRef(name=(value_iterator,))\n    value_rel.path_outputs[(base_id, pgce.PathAspect.ITERATOR)] = var\n    value_rel.path_outputs[(base_id, pgce.PathAspect.VALUE)] = var\n\n    # construct the EdgeQL DML AST\n    stype_refs: dict[uuid.UUID, list[qlast.Set]] = {}\n\n    sub_name = sub.get_shortname(ctx.schema)\n\n    target_ql: qlast.Expr = qlast.Path(\n        steps=[value_ql, qlast.Ptr(name='__target__')]\n    )\n\n    if isinstance(sub_target, s_objtypes.ObjectType):\n        assert isinstance(target_ql, qlast.Path)\n        target_ql = _construct_cast_from_uuid_to_obj_type(\n            target_ql, sub_target, stype_refs, optional=True, ctx=ctx\n        )\n\n    ql_ptr_val: qlast.Expr\n    if isinstance(sub, s_links.Link):\n        ql_ptr_val = qlast.Shape(\n            expr=target_ql,\n            elements=[\n                qlast.ShapeElement(\n                    expr=qlast.Path(\n                        steps=[qlast.Ptr(name=ptr_name, type='property')],\n                    ),\n                    compexpr=qlast.Path(\n                        steps=[\n                            value_ql,\n                            # qlast.Ptr(name=sub_name.name),\n                            qlast.Ptr(name=ptr_name),\n                        ],\n                    ),\n                )\n                for ptr_name, _, _ in value_columns\n                if ptr_name not in ('source', 'target')\n            ],\n        )\n    else:\n        # multi pointer\n        ql_ptr_val = target_ql\n\n    source_ql_p = qlast.Path(steps=[value_ql, qlast.Ptr(name='__source__')])\n    # XXX: rewrites are getting missed when we do this cast! Now, we\n    # *want* rewrites getting missed tbh, but I think it's a broader\n    # bug.\n    source_ql = _construct_cast_from_uuid_to_obj_type(\n        source_ql_p,\n        sub_source,\n        stype_refs,\n        optional=True,\n        ctx=ctx,\n    )\n\n    is_multi = sub.get_cardinality(ctx.schema) == qltypes.SchemaCardinality.Many\n\n    # Update the source_ql directly -- the filter is done there.\n    ql_stmt: qlast.Expr = qlast.UpdateQuery(\n        subject=source_ql,\n        shape=[\n            qlast.ShapeElement(\n                expr=qlast.Path(steps=[qlast.Ptr(name=sub_name.name)]),\n                operation=(\n                    qlast.ShapeOperation(op=qlast.ShapeOp.APPEND)\n                    if is_multi\n                    else qlast.ShapeOperation(op=qlast.ShapeOp.ASSIGN)\n                ),\n                compexpr=ql_ptr_val,\n            )\n        ],\n    )\n    if not is_value_single:\n        # value relation might contain multiple rows\n        # to express this in EdgeQL, we must wrap `insert` into a `for` query\n        ql_stmt = qlast.ForQuery(\n            iterator=qlast.Path(steps=[qlast.IRAnchor(name=value_name)]),\n            iterator_alias=iterator_name,\n            result=ql_stmt,\n        )\n\n    ql_returning_shape: list[qlast.ShapeElement] = []\n    if stmt.returning_list:\n        # construct the shape that will extract all needed column of the subject\n        # table (because they might be be used by RETURNING clause)\n        for column in sub_table.columns:\n            if column.hidden:\n                continue\n            if column.name in ('source', 'target'):\n                # no need to include in shape, they will be present anyway\n                continue\n\n            ql_returning_shape.append(\n                qlast.ShapeElement(\n                    expr=qlast.Path(steps=[qlast.Ptr(name=column.name)]),\n                    compexpr=qlast.Path(\n                        partial=True,\n                        steps=[\n                            qlast.Ptr(name=sub_name.name),\n                            qlast.Ptr(name=column.name, type='property'),\n                        ],\n                    ),\n                )\n            )\n\n    return UncompiledDML(\n        input=stmt,\n        subject=sub,\n        ql_stmt=ql_stmt,\n        ql_returning_shape=ql_returning_shape,\n        ql_singletons={base_id},\n        ql_anchors={value_name: base_id},\n        external_rels={\n            base_id: (\n                value_rel,\n                (pgce.PathAspect.SOURCE,),\n            )\n        },\n        stype_refs=stype_refs,\n        early_result=context.CompiledDML(\n            value_cte_name=value_cte_name,\n            value_relation_input=value_relation,\n            value_columns=value_columns,\n            value_iterator_name=value_iterator,\n            # these will be populated after compilation\n            output_ctes=[],\n            output_relation_name='',\n            output_namespace={},\n        ),\n        # these will be populated by _uncompile_dml_stmt\n        subject_columns=[],\n    )\n\n\ndef _has_at_most_one_row(query: pgast.Query | None) -> bool:\n    if not query:\n        return True\n    return False\n\n\ndef _compile_standalone_default(\n    col: context.Column,\n    sub: s_objtypes.ObjectType | s_links.Link | s_properties.Property,\n    ctx: Context,\n) -> pgast.BaseExpr:\n    ptr, _, _ = _get_pointer_for_column(col, sub, ctx)\n    default = ptr.get_default(ctx.schema)\n    if default is None:\n        return pgast.NullConstant()\n\n    # TODO(?): Support defaults that reference the object being inserted.\n    # That seems like a pretty heavy lift in this scenario, though.\n\n    options = qlcompiler.CompilerOptions(\n        make_globals_empty=False,\n        apply_user_access_policies=ctx.options.apply_access_policies,\n    )\n    compiled = default.compiled(ctx.schema, options=options, context=None)\n\n    sql_tree = pgcompiler.compile_ir_to_sql_tree(\n        compiled.irast,\n        output_format=pgcompiler.OutputFormat.NATIVE_INTERNAL,\n        alias_generator=ctx.alias_generator,\n    )\n    merge_params(sql_tree, compiled.irast, ctx)\n\n    assert isinstance(sql_tree.ast, pgast.BaseExpr)\n    return sql_tree.ast\n\n\ndef _uncompile_default_value(\n    value_query: Optional[pgast.Query],\n    value_ctes: Optional[list[pgast.CommonTableExpr]],\n    expected_columns: list[context.Column],\n    sub: s_objtypes.ObjectType | s_links.Link | s_properties.Property,\n    *,\n    ctx: Context,\n) -> tuple[pgast.BaseRelation, list[context.Column]]:\n    # INSERT INTO x DEFAULT VALUES\n    if not value_query:\n        value_query = pgast.SelectStmt(values=[])\n        # edgeql compiler will provide default values\n        # (and complain about missing ones)\n        expected_columns = []\n        return value_query, expected_columns\n\n    # VALUES (DEFAULT)\n    if isinstance(value_query, pgast.SelectStmt) and value_query.values:\n        # find DEFAULT keywords in VALUES\n\n        def is_default(e: pgast.BaseExpr) -> bool:\n            return isinstance(e, pgast.Keyword) and e.name == 'DEFAULT'\n\n        default_columns: dict[int, int] = {}\n        for row in value_query.values:\n            assert isinstance(row, pgast.ImplicitRowExpr)\n\n            for to_remove, col in enumerate(row.args):\n                if is_default(col):\n                    default_columns[to_remove] = (\n                        default_columns.setdefault(to_remove, 0) + 1\n                    )\n\n        # remove DEFAULT keywords and expected columns,\n        # so EdgeQL insert will not get those columns, which will use the\n        # property defaults.\n        for to_remove in sorted(default_columns, reverse=True):\n            if default_columns[to_remove] != len(value_query.values):\n                continue\n                raise errors.QueryError(\n                    'DEFAULT keyword is supported only when '\n                    'used for a column in all rows',\n                    span=value_query.span,\n                    pgext_code=pgerror.ERROR_FEATURE_NOT_SUPPORTED,\n                )\n\n            del expected_columns[to_remove]\n\n            for r_index, row in enumerate(value_query.values):\n                assert isinstance(row, pgast.ImplicitRowExpr)\n                assert is_default(row.args[to_remove])\n                cols = list(row.args)\n                del cols[to_remove]\n                value_query.values[r_index] = row.replace(args=cols)\n\n        # Go back through and compile any left over\n        for r_index, row in enumerate(value_query.values):\n            assert isinstance(row, pgast.ImplicitRowExpr)\n            if not any(is_default(col) for col in row.args):\n                continue\n\n            cols = list(row.args)\n            for i, col in enumerate(row.args):\n                if is_default(col):\n                    cols[i] = _compile_standalone_default(\n                        expected_columns[i], sub, ctx=ctx\n                    )\n            value_query.values[r_index] = row.replace(args=cols)\n\n        if (\n            len(value_query.values) > 0\n            and isinstance(value_query.values[0], pgast.ImplicitRowExpr)\n            and len(value_query.values[0].args) == 0\n        ):\n            # special case: `VALUES (), (), ..., ()`\n            # This is syntactically incorrect, so we transform it into:\n            # `SELECT FROM (VALUES (NULL), (NULL), ..., (NULL)) _`\n            value_query = pgast.SelectStmt(\n                target_list=[],\n                from_clause=[\n                    pgast.RangeSubselect(\n                        subquery=pgast.SelectStmt(\n                            values=[\n                                pgast.ImplicitRowExpr(\n                                    args=[pgast.NullConstant()]\n                                )\n                                for _ in value_query.values\n                            ]\n                        ),\n                        alias=pgast.Alias(aliasname='_'),\n                    )\n                ],\n            )\n\n    # compile these CTEs as they were defined on value relation\n    assert not value_query.ctes\n    value_query.ctes = value_ctes\n\n    return value_query, expected_columns\n\n\n@_uncompile_dml_stmt.register\ndef _uncompile_delete_stmt(\n    stmt: pgast.DeleteStmt, *, ctx: Context\n) -> UncompiledDML:\n    # determine the subject object\n    sub_table, sub = _uncompile_dml_subject(stmt.relation, ctx=ctx)\n\n    res: UncompiledDML\n    if isinstance(sub, s_objtypes.ObjectType):\n        res = _uncompile_delete_object_stmt(stmt, sub, sub_table, ctx=ctx)\n    elif isinstance(sub, (s_links.Link, s_properties.Property)):\n        res = _uncompile_delete_pointer_stmt(stmt, sub, sub_table, ctx=ctx)\n    else:\n        raise NotImplementedError()\n\n    if stmt.returning_list:\n        _uncompile_subject_columns(sub, sub_table, res, ctx=ctx)\n    return res\n\n\ndef _uncompile_delete_object_stmt(\n    stmt: pgast.DeleteStmt,\n    sub: s_objtypes.ObjectType,\n    sub_table: context.Table,\n    *,\n    ctx: Context,\n) -> UncompiledDML:\n    \"\"\"\n    Translates a 'SQL DELETE of object type table' to an EdgeQL delete.\n    \"\"\"\n\n    # prepare value relation\n    # For deletes, value relation contains a single column of ids of all the\n    # objects that need to be deleted. We construct this relation from WHERE\n    # and USING clauses of DELETE.\n\n    assert isinstance(stmt.relation, pgast.RelRangeVar)\n    val_sub_rvar = stmt.relation.alias.aliasname or stmt.relation.relation.name\n    assert val_sub_rvar\n    value_relation = pgast.SelectStmt(\n        ctes=stmt.ctes,\n        target_list=[\n            pgast.ResTarget(\n                val=pgast.ColumnRef(\n                    name=(val_sub_rvar, 'id'),\n                )\n            )\n        ],\n        from_clause=[\n            pgast.RelRangeVar(\n                relation=stmt.relation.relation,\n                alias=pgast.Alias(aliasname=val_sub_rvar),\n                # DELETE ONLY\n                include_inherited=stmt.relation.include_inherited,\n            )\n        ]\n        + stmt.using_clause,\n        where_clause=stmt.where_clause,\n    )\n    stmt.ctes = []\n\n    # prepare anchors for inserted value columns\n    value_name = ctx.alias_generator.get('del_val')\n    value_id = irast.PathId.from_type(\n        ctx.schema,\n        sub,\n        typename=sn.QualName('__derived__', value_name),\n        env=None,\n    )\n\n    value_ql = qlast.IRAnchor(name=value_name)\n\n    # a phantom relation that contains a single column, which is the id of all\n    # the objects that should be deleted.\n    value_cte_name = ctx.alias_generator.get('del_value')\n    value_rel = pgast.Relation(\n        name=value_cte_name,\n        strip_output_namespaces=True,\n    )\n    value_columns = [('id', 'id', False)]\n\n    output_var = pgast.ColumnRef(name=('id',), nullable=False)\n    value_rel.path_outputs[(value_id, pgce.PathAspect.IDENTITY)] = output_var\n    value_rel.path_outputs[(value_id, pgce.PathAspect.VALUE)] = output_var\n    value_rel.path_outputs[(value_id, pgce.PathAspect.ITERATOR)] = output_var\n\n    # construct the EdgeQL DML AST\n    sub_name = sub.get_name(ctx.schema)\n    where = qlast.BinOp(\n        left=qlast.Path(partial=True, steps=[qlast.Ptr(name='id')]),\n        op='IN',\n        right=qlast.Path(steps=[value_ql, qlast.Ptr(name='id')]),\n    )\n\n    ql_stmt: qlast.Expr = qlast.DeleteQuery(\n        subject=qlast.Path(steps=[s_utils.name_to_ast_ref(sub_name)]),\n        where=where,\n    )\n\n    ql_returning_shape: list[qlast.ShapeElement] = []\n    if stmt.returning_list:\n        # construct the shape that will extract all needed column of the subject\n        # table (because they might be be used by RETURNING clause)\n        for column in sub_table.columns:\n            if column.hidden:\n                continue\n            _, ptr_name, _ = _get_pointer_for_column(column, sub, ctx)\n            ql_returning_shape.append(\n                qlast.ShapeElement(\n                    expr=qlast.Path(steps=[qlast.Ptr(name=ptr_name)]),\n                )\n            )\n\n    return UncompiledDML(\n        input=stmt,\n        subject=sub,\n        ql_stmt=ql_stmt,\n        ql_returning_shape=ql_returning_shape,\n        ql_singletons={value_id},\n        ql_anchors={value_name: value_id},\n        external_rels={\n            value_id: (\n                value_rel,\n                (pgce.PathAspect.SOURCE,),\n            )\n        },\n        stype_refs={},\n        early_result=context.CompiledDML(\n            value_cte_name=value_cte_name,\n            value_relation_input=value_relation,\n            value_columns=value_columns,\n            value_iterator_name=None,\n            # these will be populated after compilation\n            output_ctes=[],\n            output_relation_name='',\n            output_namespace={},\n        ),\n        # these will be populated by _uncompile_dml_stmt\n        subject_columns=[],\n    )\n\n\ndef _uncompile_delete_pointer_stmt(\n    stmt: pgast.DeleteStmt,\n    sub: s_links.Link | s_properties.Property,\n    sub_table: context.Table,\n    *,\n    ctx: Context,\n) -> UncompiledDML:\n    \"\"\"\n    Translates a SQL 'DELETE FROM a link / multi-property table' into\n    an EdgeQL `update SourceObject { pointer := ... }.pointer`.\n    \"\"\"\n\n    sub_source = sub.get_source(ctx.schema)\n    assert isinstance(sub_source, s_objtypes.ObjectType)\n    sub_target = sub.get_target(ctx.schema)\n    assert sub_target\n\n    # prepare value relation\n    # For link deletes, value relation contains two columns: source and target\n    # of all links that need to be deleted. We construct this relation from\n    # WHERE and USING clauses of DELETE.\n\n    assert isinstance(stmt.relation, pgast.RelRangeVar)\n    val_sub_rvar = stmt.relation.alias.aliasname or stmt.relation.relation.name\n    assert val_sub_rvar\n    value_relation = pgast.SelectStmt(\n        ctes=stmt.ctes,\n        target_list=[\n            pgast.ResTarget(\n                val=pgast.ColumnRef(\n                    name=(val_sub_rvar, 'source'),\n                )\n            ),\n            pgast.ResTarget(\n                val=pgast.ColumnRef(\n                    name=(val_sub_rvar, 'target'),\n                )\n            ),\n        ],\n        from_clause=[\n            pgast.RelRangeVar(\n                relation=stmt.relation.relation,\n                alias=pgast.Alias(aliasname=val_sub_rvar),\n            )\n        ]\n        + stmt.using_clause,\n        where_clause=stmt.where_clause,\n    )\n    stmt.ctes = []\n\n    # if we are sure that we are updating a single source object,\n    # we can skip for-loops and iterators, which produces better SQL\n    is_value_single = False\n\n    # prepare anchors for inserted value columns\n    value_name = ctx.alias_generator.get('ins_val')\n    iterator_name = ctx.alias_generator.get('ins_iter')\n    source_id = irast.PathId.from_type(\n        ctx.schema,\n        sub_source,\n        typename=sn.QualName('__derived__', value_name),\n        env=None,\n    )\n    link_ref = irtypeutils.ptrref_from_ptrcls(\n        schema=ctx.schema, ptrcls=sub, cache=None, typeref_cache=None\n    )\n    value_id: irast.PathId = source_id.extend(ptrref=link_ref)\n\n    value_ql: qlast.PathElement = (\n        qlast.IRAnchor(name=value_name)\n        if is_value_single\n        else qlast.ObjectRef(name=iterator_name)\n    )\n\n    # a phantom relation that is supposed to hold the two source and target\n    # columns of rows that need to be deleted.\n    value_cte_name = ctx.alias_generator.get('del_value')\n    value_rel = pgast.Relation(\n        name=value_cte_name,\n        strip_output_namespaces=True,\n    )\n    value_columns = [('source', 'source', False), ('target', 'target', False)]\n\n    var = pgast.ColumnRef(name=('source',), nullable=True)\n    value_rel.path_outputs[(source_id, pgce.PathAspect.VALUE)] = var\n    value_rel.path_outputs[(source_id, pgce.PathAspect.IDENTITY)] = var\n\n    tgt_id = value_id.tgt_path()\n    var = pgast.ColumnRef(name=('target',), nullable=True)\n    value_rel.path_outputs[(tgt_id, pgce.PathAspect.VALUE)] = var\n    value_rel.path_outputs[(tgt_id, pgce.PathAspect.IDENTITY)] = var\n\n    # source needs an iterator column, so we need to invent one\n    # Here we only decide on the name of that iterator column, the actual column\n    # is generated later, when resolving the DML stmt.\n    value_iterator = ctx.alias_generator.get('iter')\n    var = pgast.ColumnRef(name=(value_iterator,))\n    value_rel.path_outputs[(source_id, pgce.PathAspect.ITERATOR)] = var\n    value_rel.path_outputs[(value_id, pgce.PathAspect.ITERATOR)] = var\n\n    # construct the EdgeQL DML AST\n    sub_name = sub.get_name(ctx.schema)\n    sub_source_name = sub_source.get_name(ctx.schema)\n    sub_target_name = sub_target.get_name(ctx.schema)\n\n    sub_name = sub.get_shortname(ctx.schema)\n\n    ql_sub_source_ref = s_utils.name_to_ast_ref(sub_source_name)\n    ql_sub_target_ref = s_utils.name_to_ast_ref(sub_target_name)\n\n    ql_ptr_val: qlast.Expr = qlast.Path(\n        steps=[value_ql, qlast.Ptr(name=sub_name.name)]\n    )\n    if isinstance(sub, s_links.Link):\n        ql_ptr_val = qlast.TypeCast(\n            expr=ql_ptr_val,\n            type=qlast.TypeName(maintype=ql_sub_target_ref),\n        )\n\n    ql_stmt: qlast.Expr = qlast.UpdateQuery(\n        subject=qlast.Path(steps=[ql_sub_source_ref]),\n        where=qlast.BinOp(  # ObjectType == value.source\n            left=qlast.Path(steps=[ql_sub_source_ref]),\n            op='=',\n            right=qlast.Path(steps=[value_ql]),\n        ),\n        shape=[\n            qlast.ShapeElement(\n                expr=qlast.Path(steps=[qlast.Ptr(name=sub_name.name)]),\n                operation=qlast.ShapeOperation(op=qlast.ShapeOp.SUBTRACT),\n                compexpr=ql_ptr_val,\n            )\n        ],\n    )\n    if not is_value_single:\n        # value relation might contain multiple rows\n        # to express this in EdgeQL, we must wrap `delete` into a `for` query\n        ql_stmt = qlast.ForQuery(\n            iterator=qlast.Path(steps=[qlast.IRAnchor(name=value_name)]),\n            iterator_alias=iterator_name,\n            result=ql_stmt,\n        )\n\n    # append .pointer onto the shape, so the resulting CTE contains the pointer\n    # data, not the subject table\n    # ql_stmt = qlast.Path(\n        # steps=[ql_stmt, qlast.Ptr(name=sub_name.name)]\n    # )\n\n    ql_returning_shape: list[qlast.ShapeElement] = []\n    if stmt.returning_list:\n        # construct the shape that will extract all needed column of the subject\n        # table (because they might be be used by RETURNING clause)\n        for column in sub_table.columns:\n            if column.hidden:\n                continue\n            if column.name in ('source', 'target'):\n                # no need to include in shape, they will be present anyway\n                continue\n\n            ql_returning_shape.append(\n                qlast.ShapeElement(\n                    expr=qlast.Path(steps=[qlast.Ptr(name=column.name)]),\n                    compexpr=qlast.Path(\n                        partial=True,\n                        steps=[\n                            qlast.Ptr(name=sub_name.name),\n                            qlast.Ptr(name=column.name, type='property'),\n                        ],\n                    ),\n                )\n            )\n\n    return UncompiledDML(\n        input=stmt,\n        subject=sub,\n        ql_stmt=ql_stmt,\n        ql_returning_shape=ql_returning_shape,\n        ql_singletons={source_id},\n        ql_anchors={value_name: source_id},\n        external_rels={\n            source_id: (\n                value_rel,\n                (pgce.PathAspect.SOURCE,),\n            )\n        },\n        stype_refs={},\n        early_result=context.CompiledDML(\n            value_cte_name=value_cte_name,\n            value_relation_input=value_relation,\n            value_columns=value_columns,\n            value_iterator_name=value_iterator,\n            # these will be populated after compilation\n            output_ctes=[],\n            output_relation_name='',\n            output_namespace={},\n        ),\n        # these will be populated by _uncompile_dml_stmt\n        subject_columns=[],\n    )\n\n\n@_uncompile_dml_stmt.register\ndef _uncompile_update_stmt(\n    stmt: pgast.UpdateStmt, *, ctx: Context\n) -> UncompiledDML:\n    # determine the subject object\n    sub_table, sub = _uncompile_dml_subject(stmt.relation, ctx=ctx)\n\n    # convert the general repr of SET clause into a list of columns\n    update_targets: list[pgast.UpdateTarget] = []\n    for target in stmt.targets:\n        if isinstance(target, pgast.UpdateTarget):\n            if target.indirection:\n                raise errors.QueryError(\n                    'indirections in UPDATE SET not supported',\n                    pgext_code=pgerror.ERROR_FEATURE_NOT_SUPPORTED,\n                    span=stmt.span,\n                )\n\n            update_targets.append(target)\n        elif isinstance(target, pgast.MultiAssignRef):\n\n            if not isinstance(\n                target.source, (pgast.ImplicitRowExpr, pgast.RowExpr)\n            ):\n                raise errors.QueryError(\n                    'multi-assigns UPDATE SET are supported only for plain row '\n                    'literals (`ROW(...)`)',\n                    pgext_code=pgerror.ERROR_FEATURE_NOT_SUPPORTED,\n                    span=stmt.span,\n                )\n\n            update_targets.extend(\n                pgast.UpdateTarget(name=c, val=v, span=v.span)\n                for (c, v) in zip(target.columns, target.source.args)\n            )\n        else:\n            raise NotImplementedError()\n\n    set_columns = _pull_columns_from_table(\n        sub_table,\n        ((c.name, c.span) for c in update_targets),\n    )\n    column_updates = list(zip(set_columns, (c.val for c in update_targets)))\n\n    res: UncompiledDML\n    if isinstance(sub, s_objtypes.ObjectType):\n        res = _uncompile_update_object_stmt(\n            stmt, sub, sub_table, column_updates, ctx=ctx\n        )\n    elif isinstance(sub, (s_links.Link, s_properties.Property)):\n        raise errors.QueryError(\n            f'UPDATE of link tables is not supported',\n            pgext_code=pgerror.ERROR_FEATURE_NOT_SUPPORTED,\n            span=stmt.span,\n        )\n    else:\n        raise NotImplementedError()\n\n    if stmt.returning_list:\n        _uncompile_subject_columns(sub, sub_table, res, ctx=ctx)\n    return res\n\n\ndef _uncompile_update_object_stmt(\n    stmt: pgast.UpdateStmt,\n    sub: s_objtypes.ObjectType,\n    sub_table: context.Table,\n    column_updates: list[tuple[context.Column, pgast.BaseExpr]],\n    *,\n    ctx: Context,\n) -> UncompiledDML:\n    \"\"\"\n    Translates a 'SQL UPDATE into an object type table' to an EdgeQL update.\n    \"\"\"\n\n    def is_default(e: pgast.BaseExpr) -> bool:\n        return isinstance(e, pgast.Keyword) and e.name == 'DEFAULT'\n\n    # prepare value relation\n\n    # For updates, value relation contains:\n    # - `id` column, that contains the id of the subject,\n    # - one column for each of the pointers on the subject to be updated,\n    # We construct this relation from WHERE and FROM clauses of UPDATE.\n\n    assert isinstance(stmt.relation, pgast.RelRangeVar)\n    val_sub_rvar = stmt.relation.alias.aliasname or stmt.relation.relation.name\n    assert val_sub_rvar\n    value_relation = pgast.SelectStmt(\n        ctes=stmt.ctes,\n        target_list=[\n            pgast.ResTarget(\n                val=pgast.ColumnRef(\n                    name=(val_sub_rvar, 'id'),\n                )\n            )\n        ]\n        + [\n            pgast.ResTarget(val=val, name=c.name)\n            for c, val in column_updates\n            if not is_default(val)  # skip DEFAULT column updates\n        ],\n        from_clause=[\n            pgast.RelRangeVar(\n                relation=stmt.relation.relation,\n                alias=pgast.Alias(aliasname=val_sub_rvar),\n                # UPDATE ONLY\n                include_inherited=stmt.relation.include_inherited,\n            )\n        ]\n        + stmt.from_clause,\n        where_clause=stmt.where_clause,\n    )\n    stmt.ctes = []\n\n    # prepare anchors for inserted value columns\n    value_name = ctx.alias_generator.get('upd_val')\n    iterator_name = ctx.alias_generator.get('upd_iter')\n    value_id = irast.PathId.from_type(\n        ctx.schema,\n        sub,\n        typename=sn.QualName('__derived__', value_name),\n        env=None,\n    )\n\n    value_ql: qlast.PathElement = qlast.ObjectRef(name=iterator_name)\n\n    # a phantom relation that is supposed to hold the inserted value\n    # (in the resolver, this will be replaced by the real value relation)\n    value_cte_name = ctx.alias_generator.get('upd_value')\n    value_rel = pgast.Relation(\n        name=value_cte_name,\n        strip_output_namespaces=True,\n    )\n\n    output_var = pgast.ColumnRef(name=('id',))\n    value_rel.path_outputs[(value_id, pgce.PathAspect.ITERATOR)] = output_var\n    value_rel.path_outputs[(value_id, pgce.PathAspect.VALUE)] = output_var\n\n    value_columns = [('id', 'id', False)]\n    update_shape = []\n    stype_refs: dict[uuid.UUID, list[qlast.Set]] = {}\n    for index, (col, val) in enumerate(column_updates):\n        ptr, ptr_name, is_link = _get_pointer_for_column(col, sub, ctx)\n        if not is_default(val):\n            value_columns.append((col.name, ptr_name, is_link))\n\n        # inject type annotation into value relation\n        _try_inject_ptr_type_cast(value_relation, index + 1, ptr, ctx)\n\n        # prepare the outputs of the source CTE\n        ptr_id = _get_ptr_id(value_id, ptr, ctx)\n        output_var = pgast.ColumnRef(name=(ptr_name,), nullable=True)\n        if is_link:\n            value_rel.path_outputs[(ptr_id, pgce.PathAspect.IDENTITY)] = (\n                output_var\n            )\n            value_rel.path_outputs[(ptr_id, pgce.PathAspect.VALUE)] = output_var\n        else:\n            value_rel.path_outputs[(ptr_id, pgce.PathAspect.VALUE)] = output_var\n\n        # prepare insert shape that will use the paths from source_outputs\n        if is_default(val):\n            # special case: DEFAULT\n            default_ql: qlast.Expr\n            if ptr.get_default(ctx.schema) is None:\n                default_ql = qlast.Set(elements=[])  # NULL\n            else:\n                default_ql = qlast.Path(\n                    steps=[qlast.SpecialAnchor(name='__default__')]\n                )\n            update_shape.append(\n                qlast.ShapeElement(\n                    expr=qlast.Path(steps=[qlast.Ptr(name=ptr_name)]),\n                    operation=qlast.ShapeOperation(op=qlast.ShapeOp.ASSIGN),\n                    compexpr=default_ql,\n                )\n            )\n        else:\n            # base case\n            update_shape.append(\n                _construct_assign_element_for_ptr(\n                    value_ql,\n                    ptr_name,\n                    ptr,\n                    is_link,\n                    ctx,\n                    stype_refs,\n                )\n            )\n\n    # construct the EdgeQL DML AST\n    sub_name = sub.get_name(ctx.schema)\n    ql_sub_ref = s_utils.name_to_ast_ref(sub_name)\n\n    where = qlast.BinOp(  # ObjectType == value.source\n        left=qlast.Path(steps=[ql_sub_ref]),\n        op='=',\n        right=qlast.Path(steps=[value_ql]),\n    )\n\n    ql_stmt: qlast.Expr = qlast.UpdateQuery(\n        subject=qlast.Path(steps=[ql_sub_ref]),\n        where=where,\n        shape=update_shape,\n    )\n\n    # value relation might contain multiple rows\n    # to express this in EdgeQL, we must wrap `update` into a `for` query\n    ql_stmt = qlast.ForQuery(\n        iterator=qlast.Path(steps=[qlast.IRAnchor(name=value_name)]),\n        iterator_alias=iterator_name,\n        result=ql_stmt,\n    )\n\n    ql_returning_shape: list[qlast.ShapeElement] = []\n    if stmt.returning_list:\n        # construct the shape that will extract all needed column of the subject\n        # table (because they might be be used by RETURNING clause)\n        for column in sub_table.columns:\n            if column.hidden:\n                continue\n            _, ptr_name, _ = _get_pointer_for_column(column, sub, ctx)\n            ql_returning_shape.append(\n                qlast.ShapeElement(\n                    expr=qlast.Path(steps=[qlast.Ptr(name=ptr_name)]),\n                )\n            )\n\n    return UncompiledDML(\n        input=stmt,\n        subject=sub,\n        ql_stmt=ql_stmt,\n        ql_returning_shape=ql_returning_shape,\n        ql_singletons={value_id},\n        ql_anchors={value_name: value_id},\n        external_rels={\n            value_id: (\n                value_rel,\n                (pgce.PathAspect.SOURCE,),\n            )\n        },\n        stype_refs=stype_refs,\n        early_result=context.CompiledDML(\n            value_cte_name=value_cte_name,\n            value_relation_input=value_relation,\n            value_columns=value_columns,\n            value_iterator_name=None,\n            # these will be populated after compilation\n            output_ctes=[],\n            output_relation_name='',\n            output_namespace={},\n        ),\n        # these will be populated by _uncompile_dml_stmt\n        subject_columns=[],\n    )\n\n\ndef _compile_uncompiled_dml(\n    stmts: list[UncompiledDML], ctx: context.ResolverContextLevel\n) -> tuple[\n    Mapping[pgast.Query, context.CompiledDML],\n    list[pgast.CommonTableExpr],\n]:\n    \"\"\"\n    Compiles *all* DML statements in the query.\n\n    Statements must already be uncompiled into equivalent EdgeQL statements.\n    Will merge the statements into one large shape of all DML queries and\n    compile that with a single invocation of EdgeQL compiler.\n\n    Returns:\n    - mapping from the original SQL statement into CompiledDML and\n    - a list of \"global\" CTEs that should be injected at the end of top-level\n      CTE list.\n    \"\"\"\n\n    # merge params\n    singletons = set()\n    anchors: dict[str, irast.PathId] = {}\n    for stmt in stmts:\n        singletons.update(stmt.ql_singletons)\n        anchors.update(stmt.ql_anchors)\n\n    # construct the main query\n    ql_aliases: list[qlast.Alias] = []\n    ql_stmt_shape: list[qlast.ShapeElement] = []\n    ql_stmt_shape_names = []\n    inserts_by_type: dict[uuid.UUID, list[str]] = {}\n    for index, stmt in enumerate(stmts):\n\n        # fixup references to stypes that have been modified be previous inserts\n        # for more info, see _construct_cast_from_uuid_to_obj_type\n        for stype_id, ref_sets in stmt.stype_refs.items():\n            if insert_names := inserts_by_type.get(stype_id, None):\n                for ref_set in ref_sets:\n                    for name in insert_names:\n                        ref_set.elements.append(\n                            qlast.Path(steps=[qlast.ObjectRef(name=name)])\n                        )\n\n        # the main thing\n        name = f'dml_{index}'\n        ql_stmt_shape_names.append(name)\n        ql_aliases.append(\n            qlast.AliasedExpr(\n                alias=name,\n                expr=stmt.ql_stmt,\n            )\n        )\n        ql_stmt_shape.append(\n            qlast.ShapeElement(\n                expr=qlast.Path(steps=[qlast.Ptr(name=name)]),\n                compexpr=qlast.Shape(\n                    expr=qlast.Path(steps=[qlast.ObjectRef(name=name)]),\n                    elements=stmt.ql_returning_shape,\n                ),\n            )\n        )\n\n        # save inserts for later fixups\n        if isinstance(stmt.input, pgast.InsertStmt) and stmt.subject.id:\n            if stmt.subject.id not in inserts_by_type:\n                inserts_by_type[stmt.subject.id] = []\n            inserts_by_type[stmt.subject.id].append(name)\n\n    ql_stmt = qlast.SelectQuery(\n        aliases=ql_aliases,\n        result=qlast.Shape(expr=None, elements=ql_stmt_shape),\n    )\n\n    ir_stmt: irast.Statement\n    try:\n        # compile synthetic ql statement into SQL\n        options = qlcompiler.CompilerOptions(\n            modaliases={None: 'default'},\n            make_globals_empty=False,\n            singletons=singletons,\n            anchors=anchors,\n            allow_user_specified_id=ctx.options.allow_user_specified_id,\n            apply_user_access_policies=ctx.options.apply_access_policies\n        )\n        ir_stmt = qlcompiler.compile_ast_to_ir(\n            ql_stmt,\n            schema=ctx.schema,\n            options=options,\n        )\n        external_rels, ir_stmts = _merge_and_prepare_external_rels(\n            ir_stmt, stmts, ql_stmt_shape_names\n        )\n        sql_result = pgcompiler.compile_ir_to_sql_tree(\n            ir_stmt,\n            external_rels=external_rels,\n            output_format=pgcompiler.OutputFormat.NATIVE_INTERNAL,\n            alias_generator=ctx.alias_generator,\n            sql_dml_mode=True,\n        )\n\n        merge_params(sql_result, ir_stmt, ctx)\n\n    except errors.QueryError as e:\n        raise errors.QueryError(\n            msg=e.args[0],\n            details=e.details,\n            hint=e.hint,\n            # not sure if this is ok, but it is better than InternalServerError,\n            # which is the default\n            pgext_code=pgerror.ERROR_DATA_EXCEPTION,\n        )\n    except errors.UnsupportedFeatureError as e:\n        raise errors.QueryError(\n            msg=e.args[0],\n            position=e.get_position(),\n            details=e.details,\n            hint=e.hint,\n            pgext_code=pgerror.ERROR_FEATURE_NOT_SUPPORTED,\n        )\n\n    assert isinstance(sql_result.ast, pgast.Query)\n    assert sql_result.ast.ctes\n    ctes = list(sql_result.ast.ctes)\n\n    result = {}\n    for stmt, ir_mutating_stmt in zip(stmts, ir_stmts):\n        stmt_ctes = _collect_stmt_ctes(ctes, ir_mutating_stmt)\n\n        # Find the output CTE of the DML operation. We do this in two different\n        # ways:\n        # - look for SQL DML on the subject relation. This is used for\n        #   operations on link tables. Kinda hacky.\n        # - use the `output_for_dml`, which will be set on the CTE that contains\n        #   the union of all SQL DML stmts that are generated for an IR DML.\n        #   There might be multiple because: 1) inheritance, which stores child\n        #   objects in seprate tables, 2) unless conflict that contains another\n        #   DML stmt.\n        output_cte: pgast.CommonTableExpr | None\n        if isinstance(stmt.subject, (s_pointers.Pointer)):\n            subject_id = str(stmt.subject.id)\n            output_cte = next(\n                c\n                for c in reversed(stmt_ctes)\n                if isinstance(c.query, pgast.DMLQuery)\n                and isinstance(c.query.relation, pgast.RelRangeVar)\n                and c.query.relation.relation.name == subject_id\n            )\n        else:\n            output_cte = next(\n                (c for c in stmt_ctes if c.output_of_dml == ir_mutating_stmt),\n                None,\n            )\n        assert output_cte, 'cannot find the output CTE of a DML stmt'\n        output_rel = output_cte.query\n\n        # This \"output_rel\" must contain entry in path_namespace for each column\n        # of the subject table. This is ensured by applying a shape on the ql\n        # dml stmt, which selects all pointers. Although the shape is not\n        # constructed in CTEs (so we discard it), it causes values for pointers\n        # to be read from DML CTEs, which makes the appear in the path_namespace\n\n        # prepare a map from pointer name into pgast\n        ptr_map: dict[tuple[str, str], pgast.BaseExpr] = {}\n        for (ptr_id, aspect), output_var in output_rel.path_outputs.items():\n            qual_name = ptr_id.rptr_name()\n            if not qual_name:\n                ptr_map['id', aspect] = output_var\n            else:\n                ptr_map[qual_name.name, aspect] = output_var\n        output_namespace: dict[str, pgast.BaseExpr] = {}\n        for col_name, ptr_name in stmt.subject_columns:\n            val = ptr_map.get((ptr_name, 'serialized'), None)\n            if not val:\n                val = ptr_map.get((ptr_name, 'value'), None)\n            if not val:\n                val = ptr_map.get((ptr_name, 'identity'), None)\n            if ptr_name in ('source', 'target'):\n                val = pgast.ColumnRef(name=(ptr_name,))\n            assert val, f'{ptr_name} was in shape, but not in path_namespace'\n            output_namespace[col_name] = val\n\n        result[stmt.input] = context.CompiledDML(\n            value_cte_name=stmt.early_result.value_cte_name,\n            value_relation_input=stmt.early_result.value_relation_input,\n            value_columns=stmt.early_result.value_columns,\n            value_iterator_name=stmt.early_result.value_iterator_name,\n            conflict_update_input=stmt.early_result.conflict_update_input,\n            conflict_update_name=stmt.early_result.conflict_update_name,\n            conflict_update_iterator=stmt.early_result.conflict_update_iterator,\n            subject_id=stmt.early_result.subject_id,\n            subject_columns=stmt.early_result.subject_columns,\n            value_id=stmt.early_result.value_id,\n            env=sql_result.env,\n            output_ctes=stmt_ctes,\n            output_relation_name=output_cte.name,\n            output_namespace=output_namespace,\n        )\n\n    # The remaining CTEs do not belong to any one specific DML statement and\n    # should be included to at the end of the top-level query.\n    # They were probably generated by \"after all\" triggers.\n\n    return result, ctes\n\n\ndef _collect_stmt_ctes(\n    ctes: list[pgast.CommonTableExpr],\n    ir_stmt: irast.MutatingStmt\n) -> list[pgast.CommonTableExpr]:\n    # We compile all SQL DML queries in a single EdgeQL query.\n    # Result is an enormous SQL query with many CTEs.\n    # This function looks through these CTEs and matches them to the original\n    # IR stmt that they originate from.\n\n    # It will pop elements from ctes list until it reaches the main CTE for\n    # the IR stmt.\n\n    # When looking for \"CTEs of a stmt\" we also want to include stmts that\n    # originate from the UNLESS CONFLICT clause.\n    ir_stmts = {ir_stmt}\n    if isinstance(ir_stmt, irast.InsertStmt):\n        if ir_stmt.on_conflict and ir_stmt.on_conflict.else_ir:\n            else_expr = ir_stmt.on_conflict.else_ir.expr\n            assert isinstance(else_expr, irast.SelectStmt), else_expr\n            assert else_expr.iterator_stmt\n            dml_stmt = else_expr.result.expr\n            assert isinstance(dml_stmt, irast.MutatingStmt), dml_stmt\n            ir_stmts.add(dml_stmt)\n\n    stmt_ctes = []\n    found_it = False\n    while len(ctes) > 0:\n        matches = ctes[0].for_dml_stmt in ir_stmts\n        if not matches and found_it:\n            # use all matching CTEs plus all preceding\n            break\n        if matches:\n            found_it = True\n\n        stmt_ctes.append(ctes.pop(0))\n    return stmt_ctes\n\n\ndef _merge_and_prepare_external_rels(\n    ir_stmt: irast.Statement,\n    stmts: list[UncompiledDML],\n    stmt_names: list[str],\n) -> tuple[\n    Mapping[irast.PathId, ExternalRel],\n    list[irast.MutatingStmt],\n]:\n    \"\"\"Construct external rels used for compiling all DML statements at once.\"\"\"\n\n    # This should be straight-forward, but because we put DML into shape\n    # elements, ql compiler will put each binding into a separate namespace.\n    # So we need to find the correct path_id for each DML stmt in the IR by\n    # looking at the paths in the shape elements.\n\n    assert isinstance(ir_stmt.expr, irast.SetE)\n    assert isinstance(ir_stmt.expr.expr, irast.SelectStmt)\n\n    ir_shape = ir_stmt.expr.expr.result.shape\n    assert ir_shape\n\n    # extract stmt name from the shape elements\n    shape_elements_by_name = {}\n    for b, _ in ir_shape:\n        rptr_name = b.path_id.rptr_name()\n        if not rptr_name:\n            continue\n        shape_elements_by_name[rptr_name.name] = b.expr.expr\n\n    external_rels: dict[irast.PathId, ExternalRel] = {}\n    ir_stmts = []\n    for stmt, name in zip(stmts, stmt_names):\n        # find the associated binding (this is real funky)\n        element = shape_elements_by_name[name]\n\n        while not isinstance(element, irast.MutatingStmt):\n            if isinstance(element, irast.SelectStmt):\n                element = element.result.expr\n            elif isinstance(element, irast.Pointer):\n                element = element.source.expr\n            elif isinstance(element, irast.OperatorCall):\n                element = element.args[0].expr.expr\n            else:\n                raise NotImplementedError('cannot find mutating stmt')\n        ir_stmts.append(element)\n\n        subject_path_id = element.result.path_id\n        subject_namespace = subject_path_id.namespace\n\n        # add all external rels, but add the namespace to their output's ids\n        for rel_id, (rel, aspects) in stmt.external_rels.items():\n            for (out_id, out_asp), out in list(rel.path_outputs.items()):\n                # HACK: this is a hacky hack to get the path_id used by the\n                # pointers within the DML statement's namespace\n                out_id = out_id.replace_namespace(subject_namespace)\n                out_id._prefix = out_id._get_prefix(1).replace_namespace(set())\n                rel.path_outputs[out_id, out_asp] = out\n            external_rels[rel_id] = (rel, aspects)\n    return external_rels, ir_stmts\n\n\n@dispatch._resolve_relation.register\ndef resolve_DMLQuery(\n    stmt: pgast.DMLQuery, *, include_inherited: bool, ctx: Context\n) -> tuple[pgast.Query, context.Table]:\n    assert stmt.relation\n\n    if ctx.subquery_depth >= 2:\n        raise errors.QueryError(\n            'WITH clause containing a data-modifying statement must be at '\n            'the top level',\n            span=stmt.span,\n            pgext_code=pgerror.ERROR_FEATURE_NOT_SUPPORTED,\n        )\n\n    subject_alias: str | None = None\n    if stmt.relation.alias and stmt.relation.alias.aliasname:\n        subject_alias = stmt.relation.alias.aliasname\n    assert stmt.relation.relation.name\n    subject_name = (stmt.relation.relation.name, subject_alias)\n\n    compiled_dml = ctx.compiled_dml[stmt]\n\n    _resolve_dml_value_rel(compiled_dml, ctx=ctx)\n\n    if compiled_dml.conflict_update_input is not None:\n        _resolve_conflict_update_rel(compiled_dml, subject_name, ctx=ctx)\n\n    ctx.ctes_buffer.extend(compiled_dml.output_ctes)\n    ctx.env.capabilities |= enums.Capability.MODIFICATIONS\n\n    return _fini_resolve_dml(stmt, compiled_dml, ctx=ctx)\n\n\ndef _resolve_dml_value_rel(\n    compiled_dml: context.CompiledDML, *, ctx: Context\n):\n\n    # resolve the value relation\n    with ctx.child() as sctx:\n        # this subctx is needed so it is not deemed as top-level which would\n        # extract and attach CTEs, but not make the available to all\n        # following CTEs\n\n        # but it is not a \"real\" subquery context\n        sctx.subquery_depth -= 1\n\n        val_rel, val_table = dispatch.resolve_relation(\n            compiled_dml.value_relation_input, ctx=sctx\n        )\n        assert isinstance(val_rel, pgast.Query)\n\n    if len(compiled_dml.value_columns) != len(val_table.columns):\n        col_names = ', '.join(c for c, _, _ in compiled_dml.value_columns)\n        raise errors.QueryError(\n            f'Expected {len(compiled_dml.value_columns)} columns '\n            f'({col_names}), but got {len(val_table.columns)}',\n            span=compiled_dml.value_relation_input.span,\n        )\n\n    if val_rel.ctes:\n        ctx.ctes_buffer.extend(val_rel.ctes)\n        val_rel.ctes = None\n\n    val_table.alias = ctx.alias_generator.get('rel')\n\n    # wrap the value relation into a \"pre-projection\",\n    # so we can add type casts for link ids and an iterator column\n    pre_projection_needed = compiled_dml.value_iterator_name or (\n        any(cast_to_uuid for _, _, cast_to_uuid in compiled_dml.value_columns)\n    )\n    if pre_projection_needed:\n        value_target_list: list[pgast.ResTarget] = []\n        for val_col, (_col_name, ptr_name, cast_to_uuid) in zip(\n            val_table.columns, compiled_dml.value_columns\n        ):\n            # prepare pre-projection of this pointer value\n            val_col_pg = pg_res_expr.resolve_column_kind(\n                val_table, val_col.kind, ctx=ctx\n            )\n            if cast_to_uuid:\n                val_col_pg = pgast.TypeCast(\n                    arg=val_col_pg, type_name=pgast.TypeName(name=('uuid',))\n                )\n            value_target_list.append(\n                pgast.ResTarget(name=ptr_name, val=val_col_pg)\n            )\n\n        if compiled_dml.value_iterator_name:\n            # source needs an iterator column, so we need to invent one\n            # The name of the column was invented before (in pre-processing) so\n            # it could be used in DML CTEs.\n            value_target_list.append(\n                pgast.ResTarget(\n                    name=compiled_dml.value_iterator_name,\n                    val=pgast.FuncCall(\n                        name=('edgedb', 'uuid_generate_v4'),\n                        args=(),\n                    ),\n                )\n            )\n\n        val_rel = pgast.SelectStmt(\n            from_clause=[\n                pgast.RangeSubselect(\n                    subquery=val_rel,\n                    alias=pgast.Alias(aliasname=val_table.alias),\n                )\n            ],\n            target_list=value_target_list,\n        )\n\n    value_cte = pgast.CommonTableExpr(\n        name=compiled_dml.value_cte_name,\n        query=val_rel,\n    )\n    ctx.ctes_buffer.append(value_cte)\n\n\ndef _resolve_conflict_update_rel(\n    compiled_dml: context.CompiledDML,\n    subject_name: tuple[str, str | None],\n    *,\n    ctx: Context,\n):\n    # Resolves the relation that provides the rows that should be updated in\n    # ON CONFLICT DO UDPATE\n\n    # This is done by manipualting CTEs produced by our pg compiler compiling\n    # `insert Subject { ... } unless conflict (update Subject set {...})`\n    # There is a few relevant CTEs for this:\n    # - \"value\": provides rows to be inserted\n    #   (produced by resolver just before this function),\n    # - \"else\": provides iterator over rows that are in conflict and should not\n    #    be inserted but updated instead (generated by pgcompiler),\n    # - \"conflict update\": provides rows to be updated,\n    #   (produced by this function)\n    # - \"ins_contents\": computes values of the inserted shape,\n    #   (generated by pg compiler, anti-joined against \"else\")\n\n    # \"conflict update\" needs two relational inputs:\n    # - subject relation of rows that exist in the database and are in conflict,\n    # - excluded relation which are rows provided to the insert.\n\n    # \"subject\" is provided by \"else\", \"excluded\" is provided by \"value\"\n    # To correlate the two, we join them on the iterator that is generated in\n    # the \"value\" CTE. We cannot use real ids for this because they do not yet\n    # exist for these objects, since they are computed in \"ins_contents\".\n\n    # Additionally, we need to correlate \"conflict update\" rel with the EdgeQL\n    # update stmt. This is done via EdgeQL where clause, which compares ids of\n    # the ids of the subject and the update iterator.\n\n    if not (\n        compiled_dml.conflict_update_name\n        and compiled_dml.conflict_update_input\n    ):\n        return\n\n    from edb.pgsql.compiler import enums as pgce\n    from edb.pgsql.compiler import astutils as pg_astutils\n\n    # Find \"else\" CTE in the list of compiled CTEs\n    # This is comparing by CTE name, which is hacky and error prone\n    # We are guaranteed to have only one such CTE, since these CTEs all\n    # belong to a single insert stmt.\n    else_index, else_cte = next(\n        (i, cte) for i, cte in enumerate(compiled_dml.output_ctes)\n        if cte.name.startswith('else')\n    )\n\n    # Apply a view_map_id_map to get around the fact that rvar map path_ids\n    # contain namespaces due to use using for loops around the insert stmt.\n    assert compiled_dml.subject_id\n    else_cte.query.view_path_id_map[compiled_dml.subject_id] = (\n        next(iter(else_cte.query.view_path_id_map.keys()))\n    )\n\n    # Include 'excluded' rel var in scope\n    # This is outside of the inner scope, so the subject table takes precedence\n    ctx.scope.tables.append(\n        context.Table(\n            name='excluded',\n            reference_as='excluded',\n            columns=[\n                context.Column(\n                    name=col_name,\n                    kind=context.ColumnByName(reference_as=ptr_name),\n                )\n                for col_name, ptr_name, _ in compiled_dml.value_columns\n            ]\n        )\n    )\n\n    # resolve the relation\n    with ctx.child() as sctx:\n        # this subctx is needed so it is not deemed as top-level which would\n        # extract and attach CTEs, but not make the available to all\n        # following CTEs\n\n        # include subject rel var in scope\n        sctx.scope.tables.append(\n            context.Table(\n                name=subject_name[0],\n                alias=subject_name[1],\n                reference_as='else',\n                columns=[\n                    context.Column(\n                        name=col_name,\n                        kind=context.ColumnByName(\n                            reference_as=_get_path_id_output(\n                                else_cte.query,\n                                path_id,\n                                compiled_dml,\n                            )\n                        ),\n                    )\n                    for col_name, path_id in compiled_dml.subject_columns or []\n                ]\n            )\n        )\n\n        # the important bit: resolve \"conflict update\" rel\n        cu_rel, _ = dispatch.resolve_relation(\n            compiled_dml.conflict_update_input, ctx=sctx\n        )\n        assert isinstance(cu_rel, pgast.SelectStmt)\n\n        # inject the 'excluded' rel var (from \"value\" CTE)\n        cu_rel.from_clause.append(\n            pgast.RelRangeVar(\n                relation=pgast.Relation(name=compiled_dml.value_cte_name),\n                alias=pgast.Alias(aliasname='excluded'),\n            )\n        )\n        # inject the subject rel var (from \"else\" CTE)\n        cu_rel.from_clause.append(\n            pgast.RelRangeVar(\n                relation=pgast.Relation(name=else_cte.name),\n                alias=pgast.Alias(aliasname='else'),\n            )\n        )\n\n        # inject interator column, which we can pull from excluded\n        assert compiled_dml.value_iterator_name\n        cu_rel.target_list.append(pgast.ResTarget(\n            val=pgast.ColumnRef(\n                name=('excluded', compiled_dml.value_iterator_name)\n            ),\n        ))\n\n        # inject subject id from \"else\" rvar\n        subject_id_col = _get_path_id_output(\n            else_cte.query, compiled_dml.subject_id, compiled_dml,\n            aspect=pgce.PathAspect.IDENTITY\n        )\n        cu_rel.target_list.append(pgast.ResTarget(\n            val=pgast.ColumnRef(\n                name=('else', subject_id_col),\n            ),\n            name='id'\n        ))\n\n        # add a join condition for \"excluded\" and \"subject\" rvars\n\n        # We start with a plain value_id, but because of for loops, rvar_map\n        # will contain path_ids polluted with namespaces. So instead of a plain\n        # one, we find a path_id from rvar_map that matches the plain one in all\n        # but the namespace.\n        value_id = next(\n            p for p, _ in else_cte.query.path_rvar_map.keys()\n            if p.replace_namespace(set()) == compiled_dml.value_id\n        )\n        # pull value iterator from the else CTE\n        value_iter = _get_path_id_output(\n            else_cte.query, value_id, compiled_dml\n        )\n        cu_rel.where_clause = pg_astutils.extend_binop(\n            cu_rel.where_clause,\n            pgast.Expr(\n                lexpr=pgast.ColumnRef(name=('else', value_iter)),\n                name='=',\n                rexpr=pgast.ColumnRef(\n                    name=('excluded', compiled_dml.value_iterator_name)\n                ),\n            )\n        )\n\n    # convert the resolved \"conflict update\" into a flat list of ctes\n    conflict_ctes = []\n    if cu_rel.ctes:\n        conflict_ctes.extend(cu_rel.ctes)\n        cu_rel.ctes = None\n    cu_cte = pgast.CommonTableExpr(\n        name=compiled_dml.conflict_update_name,\n        query=cu_rel,\n    )\n    conflict_ctes.append(cu_cte)\n\n    # combine compiled CTEs and CTEs from \"conflict update\"\n    compiled_dml.output_ctes = (\n        compiled_dml.output_ctes[0:else_index + 1]\n        + conflict_ctes\n        + compiled_dml.output_ctes[else_index + 1:]\n    )\n\n\n# Invokes pg compiler machinery to pull value for columns out of a query\ndef _get_path_id_output(\n    query: pgast.Query,\n    path_id: irast.PathId,\n    compiled_dml: context.CompiledDML,\n    *,\n    aspect: pgce.PathAspect = pgce.PathAspect.VALUE,\n) -> str:\n    # The mere fact that this is used outside of the pg compiler signals\n    # that this is hacky.\n    assert compiled_dml.env\n    output = pgcompiler.pathctx.get_path_output(\n        query, path_id, aspect=aspect, env=compiled_dml.env\n    )\n    assert isinstance(output, pgast.ColumnRef)\n    name = output.name[-1]\n    assert isinstance(name, str)\n    return name\n\n\ndef _fini_resolve_dml(\n    stmt: pgast.DMLQuery, compiled_dml: context.CompiledDML, *, ctx: Context\n) -> tuple[pgast.Query, context.Table]:\n    if stmt.returning_list:\n        assert isinstance(stmt.relation.relation, pgast.Relation)\n        assert stmt.relation.relation.name\n\n        res_query, res_table = _resolve_returning_rows(\n            stmt.returning_list,\n            compiled_dml.output_relation_name,\n            compiled_dml.output_namespace,\n            stmt.relation.relation.name,\n            stmt.relation.alias.aliasname,\n            ctx,\n        )\n    else:\n        if ctx.subquery_depth == 0:\n            # when a top-level DML query have a RETURNING clause,\n            # we inject a COUNT(*) clause so we can efficiently count\n            # modified rows which will be converted into CommandComplete tag.\n            res_query = pgast.SelectStmt(\n                target_list=[\n                    pgast.ResTarget(\n                        val=pgast.FuncCall(\n                            name=('count',), agg_star=True, args=[]\n                        ),\n                    )\n                ],\n                from_clause=[\n                    pgast.RelRangeVar(\n                        relation=pgast.Relation(\n                            name=compiled_dml.output_relation_name\n                        )\n                    )\n                ],\n            )\n        else:\n            # nested DML queries without RETURNING does not need any result\n            res_query = pgast.SelectStmt()\n        res_table = context.Table()\n\n    if not res_query.ctes:\n        res_query.ctes = []\n    res_query.ctes.extend(pg_res_rel.extract_ctes_from_ctx(ctx))\n\n    return res_query, res_table\n\n\ndef _resolve_returning_rows(\n    returning_list: list[pgast.ResTarget],\n    output_relation_name: str,\n    output_namespace: Mapping[str, pgast.BaseExpr],\n    subject_name: str,\n    subject_alias: Optional[str],\n    ctx: context.ResolverContextLevel,\n) -> tuple[pgast.Query, context.Table]:\n    # \"output\" is the relation that provides the values of the subject table\n    # after the DML operation.\n    # It contains data you'd get by having `RETURNING *` on the DML.\n    output_rvar_name = ctx.alias_generator.get('output')\n    output_query = pgast.SelectStmt(\n        from_clause=[\n            pgast.RelRangeVar(\n                relation=pgast.Relation(name=output_relation_name),\n            )\n        ]\n    )\n    output_table = context.Table(\n        name=subject_name,\n        alias=subject_alias,\n        reference_as=output_rvar_name,\n    )\n\n    for col_name, val in output_namespace.items():\n        output_query.target_list.append(\n            pgast.ResTarget(name=col_name, val=val)\n        )\n        output_table.columns.append(\n            context.Column(\n                name=col_name,\n                kind=context.ColumnByName(reference_as=col_name),\n            )\n        )\n\n    with ctx.empty() as sctx:\n        sctx.scope.tables.append(output_table)\n\n        returning_query = pgast.SelectStmt(\n            from_clause=[\n                pgast.RangeSubselect(\n                    alias=pgast.Alias(aliasname=output_rvar_name),\n                    subquery=output_query,\n                )\n            ],\n            target_list=[],\n        )\n        returning_table = context.Table()\n\n        names: set[str] = set()\n        for t in returning_list:\n            targets, columns = pg_res_expr.resolve_ResTarget(\n                t, existing_names=names, ctx=sctx\n            )\n            returning_query.target_list.extend(targets)\n            returning_table.columns.extend(columns)\n    return returning_query, returning_table\n\n\ndef _get_pointer_for_column(\n    col: context.Column,\n    subject: s_objtypes.ObjectType | s_links.Link | s_properties.Property,\n    ctx: context.ResolverContextLevel,\n) -> tuple[s_pointers.Pointer, str, bool]:\n    if isinstance(\n        subject, (s_links.Link, s_properties.Property)\n    ) and col.name in ('source', 'target'):\n        return subject, col.name, False\n    assert not isinstance(subject, s_properties.Property)\n\n    is_link = False\n    ptr_name = col.name\n    if col.name.endswith('_id'):\n        # If the name ends with _id, and a single link exists with that name,\n        # then we are referring to the link.\n        root_name = ptr_name[0:-3]\n        if (\n            (link := subject.maybe_get_ptr(\n                ctx.schema, sn.UnqualName(root_name), type=s_links.Link\n            ))\n            and link.singular(ctx.schema)\n        ):\n            ptr_name = root_name\n            is_link = True\n\n    ptr = subject.maybe_get_ptr(ctx.schema, sn.UnqualName(ptr_name))\n    assert ptr\n    return ptr, ptr_name, is_link\n\n\ndef _get_ptr_id(\n    source_id: irast.PathId,\n    ptr: s_pointers.Pointer,\n    ctx: context.ResolverContextLevel,\n) -> irast.PathId:\n    ptrref = irtypeutils.ptrref_from_ptrcls(\n        schema=ctx.schema, ptrcls=ptr, cache=None, typeref_cache=None\n    )\n    return source_id.extend(ptrref=ptrref)\n\n\ndef _try_inject_ptr_type_cast(\n    rel: pgast.BaseRelation, index: int, ptr: s_pointers.Pointer, ctx: Context\n):\n    ptr_name = ptr.get_shortname(ctx.schema).name\n\n    tgt_pg: tuple[str, ...]\n    if ptr_name == 'id' or isinstance(ptr, s_links.Link):\n        tgt_pg = ('uuid',)\n    else:\n        tgt = ptr.get_target(ctx.schema)\n        assert tgt\n        tgt_pg = pgtypes.pg_type_from_object(ctx.schema, tgt)\n    _try_inject_type_cast(rel, index, pgast.TypeName(name=tgt_pg))\n\n\ndef _try_inject_type_cast(\n    rel: pgast.BaseRelation,\n    pos: int,\n    ty: pgast.TypeName,\n):\n    \"\"\"\n    If a relation is simple, injects type annotation for a column.\n    This is needed for Postgres to correctly infer the type so it will be able\n    to bind to correct parameter types. For example:\n\n    INSERT x (a, b) VALUES ($1, $2)\n\n    is compiled into something like:\n\n    WITH cte AS (VALUES ($1, $2))\n    INSERT x (a, b) SELECT * FROM cte\n\n    This function adds type casts into `cte`.\n    \"\"\"\n\n    if not isinstance(rel, pgast.SelectStmt):\n        return\n\n    if rel.values:\n        for row_i, row in enumerate(rel.values):\n            if isinstance(row, pgast.ImplicitRowExpr) and pos < len(row.args):\n                args = list(row.args)\n                args[pos] = pgast.TypeCast(arg=args[pos], type_name=ty)\n                rel.values[row_i] = row.replace(args=args)\n\n    elif rel.target_list and pos < len(rel.target_list):\n        target = rel.target_list[pos]\n        rel.target_list[pos] = target.replace(\n            val=pgast.TypeCast(arg=target.val, type_name=ty)\n        )\n\n\ndef merge_params(\n    sql_result: pgcompiler.CompileResult, ir_stmt: irast.Statement, ctx: Context\n):\n    # Merge the params produced by the main compiler with params for the rest of\n    # the query that the resolved is keeping track of.\n    param_remapping: dict[int, int] = {}\n    for arg_name, arg in sql_result.argmap.items():\n        # find the global\n        glob = next(g for g in ir_stmt.globals if g.name == arg_name)\n\n        # search for existing params for this global\n        existing_param = next(\n            (\n                p\n                for p in ctx.query_params\n                if isinstance(p, dbstate.SQLParamGlobal)\n                and p.global_name == glob.global_name\n            ),\n            None,\n        )\n        internal_index: int\n        if existing_param is not None:\n            internal_index = existing_param.internal_index\n        else:\n            # append a new param\n            internal_index = len(ctx.query_params) + 1\n\n            pg_type = pgtypes.pg_type_from_ir_typeref(\n                glob.ir_type.base_type or glob.ir_type\n            )\n            ctx.query_params.append(\n                dbstate.SQLParamGlobal(\n                    global_name=glob.global_name,\n                    pg_type=pg_type,\n                    is_permission=glob.is_permission,\n                    internal_index=internal_index,\n                )\n            )\n\n        # remap if necessary\n        if internal_index != arg.index:\n            param_remapping[arg.index] = internal_index\n    if len(param_remapping) > 0:\n        ParamMapper(param_remapping).visit(sql_result.ast)\n\n\nclass ParamMapper(ast.NodeVisitor):\n\n    def __init__(self, mapping: dict[int, int]) -> None:\n        super().__init__()\n        self.mapping = mapping\n\n    def visit_ParamRef(self, p: pgast.ParamRef) -> None:\n        p.number = self.mapping[p.number]\n\n\ndef init_external_params(query: pgast.Base, ctx: Context):\n    counter = ParamCounter()\n    counter.node_visit(query)\n    for _ in range(0, counter.param_count - len(ctx.options.normalized_params)):\n        ctx.query_params.append(dbstate.SQLParamExternal())\n    for param_type_oid in ctx.options.normalized_params:\n        ctx.query_params.append(dbstate.SQLParamExtractedConst(\n            type_oid=param_type_oid\n        ))\n\n\nclass ParamCounter(ast.NodeVisitor):\n    def __init__(self) -> None:\n        super().__init__()\n        self.param_count = 0\n\n    def visit_ParamRef(self, p: pgast.ParamRef) -> None:\n        if self.param_count < p.number:\n            self.param_count = p.number\n\n\ndef fini_external_params(ctx: Context):\n    for param in ctx.query_params:\n        if (\n            not param.used\n            and isinstance(param, dbstate.SQLParamExtractedConst)\n            and param.type_oid == pg_parser.PgLiteralTypeOID.UNKNOWN\n        ):\n            param.type_oid = pg_parser.PgLiteralTypeOID.TEXT\n"
  },
  {
    "path": "edb/pgsql/resolver/context.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nfrom copy import deepcopy\nfrom typing import Optional, Sequence, Mapping, Any\nfrom dataclasses import dataclass, field\nimport enum\nimport uuid\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql.compiler import aliases\n\nfrom edb.common import compiler\nfrom edb.server.compiler import dbstate, enums\n\nfrom edb.schema import schema as s_schema\nfrom edb.schema import objects as s_objects\nfrom edb.schema import pointers as s_pointers\n\n\n@dataclass(frozen=True, kw_only=True, repr=False, match_args=False)\nclass Options:\n    current_database: str\n\n    current_query: str\n\n    # schemas that will be searched when idents don't have an explicit one\n    search_path: Sequence[str]\n\n    # allow setting id in inserts\n    allow_user_specified_id: bool\n\n    # apply access policies to select & dml statements\n    apply_access_policies: bool\n\n    # whether to generate an EdgeQL-compatible single-column output variant.\n    include_edgeql_io_format_alternative: Optional[bool]\n\n    # makes sure that output does not contain duplicated column names\n    disambiguate_column_names: bool\n\n    # Type oids of parameters that have taken place of constants during query\n    # normalization.\n    # When this is non-empty, the resolver is allowed to raise\n    # DisableNormalization to recompile the query without normalization.\n    normalized_params: list[int]\n\n    # Apply a limit to the number of rows in the top-level query\n    implicit_limit: Optional[int]\n\n\n@dataclass(kw_only=True)\nclass Scope:\n    \"\"\"\n    Information about that objects are visible at a specific point in an\n    SQL query.\n\n    Scope is modified during resolving of a query, when new tables are\n    discovered in FROM or JOIN or new columns declared in SELECT's projection.\n\n    After a query is done resolving, resulting relations are extracted from its\n    scope and inserted into parent scope.\n    \"\"\"\n\n    # RangeVars (table instances) in this query\n    tables: list[Table] = field(default_factory=lambda: [])\n\n    # Common Table Expressions\n    ctes: list[CTE] = field(default_factory=lambda: [])\n\n    # Pairs of columns of the same name that have been compared in a USING\n    # clause. This makes unqualified references to their name them un-ambiguous.\n    # The fourth tuple element is the join type.\n    factored_columns: list[tuple[str, Table, Table, str]] = field(\n        default_factory=lambda: []\n    )\n\n\n@dataclass(kw_only=True)\nclass Table:\n    # The schema id of the object that is the source of this table\n    schema_id: Optional[uuid.UUID] = None\n\n    # Public SQL\n    name: Optional[str] = None\n    alias: Optional[str] = None\n\n    columns: list[Column] = field(default_factory=lambda: [])\n\n    # Internal SQL\n    reference_as: Optional[str] = None\n\n    # For ambiguous references, this fields determines lookup order.\n    # Higher value is matched before lower.\n    # Aliases from current relation have higher precedence in GROUP BY\n    # than columns of input rel vars (tables).\n    # Columns from parent scopes have lower precedence\n    # than columns of input rel vars (tables).\n    precedence: int = 0\n\n    # True when this relation is compiled to a direct reference to the\n    # underlying table, without any views or CTEs.\n    # Is the condition for usage of locking clauses.\n    is_direct_relation: bool = False\n\n    def __str__(self) -> str:\n        columns = ', '.join(str(c) for c in self.columns)\n        alias = f'{self.alias} = ' if self.alias else ''\n        return f'{alias}{self.name or \"<unnamed>\"}({columns})'\n\n\n@dataclass(kw_only=True)\nclass CTE:\n    name: Optional[str] = None\n    columns: list[Column] = field(default_factory=lambda: [])\n\n\n@dataclass(kw_only=True)\nclass Column:\n    # Public SQL\n    name: str\n\n    # When true, column is not included when selecting *\n    # Used for system columns\n    # https://www.postgresql.org/docs/14/ddl-system-columns.html\n    hidden: bool = False\n\n    kind: ColumnKind\n\n    def __str__(self) -> str:\n        return self.name or '<unnamed>'\n\n\nclass ColumnKind:\n    # When a column is referenced, implementation of this class determined\n    # into what it is compiled to.\n    # The base case is ColumnByName, which just means that it compiles to an\n    # identifier to a column.\n    pass\n\n\n@dataclass(kw_only=True)\nclass ColumnByName(ColumnKind):\n    # Internal SQL column name\n    reference_as: str\n\n\n@dataclass(kw_only=True)\nclass ColumnStaticVal(ColumnKind):\n    # Value that can be used instead referencing the column.\n    # Used from __type__ only, so that's why it is UUID (for now).\n    val: uuid.UUID\n\n\n@dataclass(kw_only=True)\nclass ColumnComputable(ColumnKind):\n    # An EdgeQL computable property. To get the AST for this column, EdgeQL\n    # compiler needs to be invoked.\n    pointer: s_pointers.Pointer\n\n\n@dataclass(kw_only=True)\nclass ColumnPgExpr(ColumnKind):\n    # Value that was provided by some special resolver path.\n    expr: pgast.BaseExpr\n\n\n@dataclass(kw_only=True, eq=False, slots=True, repr=False)\nclass CompiledDML:\n    # relation that provides the DML value. not yet resolved.\n    value_cte_name: str\n\n    # relation that provides the DML value. not yet resolved.\n    value_relation_input: pgast.BaseRelation\n\n    # columns that are expected to be produced by the value relation\n    # contains: column name, ptr name, is_link\n    value_columns: list[tuple[str, str, bool]]\n\n    # name of the column in the value relation, that should provide the identity\n    value_iterator_name: Optional[str]\n\n    # for INSERTs, relation that provides values for UPDATE that happens ON\n    # CONFLICT. not yet resolved\n    conflict_update_input: Optional[pgast.BaseRelation] = None\n\n    # for INSERTs, name of CTE that provides values for UPDATE that happens ON\n    # CONFLICT\n    conflict_update_name: Optional[str] = None\n\n    conflict_update_iterator: Optional[str] = None\n\n    subject_id: Optional[Any] = None\n    subject_columns: list[tuple[str, Any]] | None = None\n    value_id: Optional[Any] = None\n    env: Optional[Any] = None\n\n    # CTEs that perform the operation\n    output_ctes: list[pgast.CommonTableExpr]\n\n    # name of the CTE that contains the output of the insert\n    output_relation_name: str\n\n    # mapping from output column names into output vars\n    output_namespace: Mapping[str, pgast.BaseExpr]\n\n\nclass ContextSwitchMode(enum.Enum):\n    EMPTY = enum.auto()\n    CHILD = enum.auto()\n    LATERAL = enum.auto()\n\n\n@dataclass(kw_only=True)\nclass Environment:\n    \"\"\"Static compilation environment.\"\"\"\n\n    # Capabilities required by the query\n    capabilities: enums.Capability = enums.Capability.NONE\n\n\nclass ResolverContextLevel(compiler.ContextLevel):\n\n    # Compilation environment common for all context levels.\n    env: Environment\n\n    schema: s_schema.Schema\n    alias_generator: aliases.AliasGenerator\n\n    # Visible names in scope\n    scope: Scope\n\n    # 0 for top-level statement, 1 for its CTEs/sub-relations/links\n    # and so on for all subqueries.\n    subquery_depth: int\n\n    # List of CTEs to add the top-level statement.\n    # This is used, for example, by DML compilation to ensure that all DML is\n    # in the top-level WITH binding.\n    ctes_buffer: list[pgast.CommonTableExpr]\n\n    # A mapping of from objects to CTEs that provide an \"inheritance view\",\n    # which is basically a union of all of their descendant's tables.\n    inheritance_ctes: dict[s_objects.InheritingObject, str]\n\n    compiled_dml: Mapping[pgast.Query, CompiledDML]\n\n    options: Options\n\n    query_params: list[dbstate.SQLParam]\n    \"\"\"List of params needed by the compiled query. Gets populated during\n    compilation and also includes params needed for globals, from calls to ql\n    compiler.\"\"\"\n\n    def __init__(\n        self,\n        prevlevel: Optional[ResolverContextLevel],\n        mode: ContextSwitchMode,\n        *,\n        schema: Optional[s_schema.Schema] = None,\n        options: Optional[Options] = None,\n    ) -> None:\n        if prevlevel is None:\n            assert schema\n            assert options\n\n            self.env = Environment()\n            self.schema = schema\n            self.options = options\n            self.scope = Scope()\n            self.alias_generator = aliases.AliasGenerator()\n            self.subquery_depth = 0\n            self.ctes_buffer = []\n            self.inheritance_ctes = dict()\n            self.compiled_dml = dict()\n            self.query_params = []\n\n        else:\n            self.env = prevlevel.env\n            self.schema = prevlevel.schema\n            self.options = prevlevel.options\n            self.alias_generator = prevlevel.alias_generator\n\n            self.subquery_depth = prevlevel.subquery_depth + 1\n            self.ctes_buffer = prevlevel.ctes_buffer\n            self.inheritance_ctes = prevlevel.inheritance_ctes\n            self.compiled_dml = prevlevel.compiled_dml\n            self.query_params = prevlevel.query_params\n\n            if mode == ContextSwitchMode.EMPTY:\n                self.scope = Scope(ctes=prevlevel.scope.ctes)\n            elif mode == ContextSwitchMode.CHILD:\n                self.scope = deepcopy(prevlevel.scope)\n                for t in self.scope.tables:\n                    t.precedence -= 1\n            elif mode == ContextSwitchMode.LATERAL:\n                self.scope = deepcopy(prevlevel.scope)\n\n    def empty(\n        self,\n    ) -> compiler.CompilerContextManager[ResolverContextLevel]:\n        \"\"\"Create a new empty context\"\"\"\n        return self.new(ContextSwitchMode.EMPTY)\n\n    def child(self) -> compiler.CompilerContextManager[ResolverContextLevel]:\n        \"\"\"Clone current context, prevent changes from leaking to parent\"\"\"\n        return self.new(ContextSwitchMode.CHILD)\n\n    def lateral(self) -> compiler.CompilerContextManager[ResolverContextLevel]:\n        \"\"\"Clone current context, prevent changes from leaking to parent\"\"\"\n        return self.new(ContextSwitchMode.LATERAL)\n\n\nclass ResolverContext(compiler.CompilerContext[ResolverContextLevel]):\n    ContextLevelClass = ResolverContextLevel\n    default_mode = ContextSwitchMode.EMPTY\n"
  },
  {
    "path": "edb/pgsql/resolver/dispatch.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport functools\nimport typing\nimport re\n\nfrom edb.server.pgcon import errors as pgerror\nfrom edb.pgsql import ast as pgast\nfrom edb import errors\n\nfrom . import context\n\n\n@functools.singledispatch\ndef _resolve(\n    expr: pgast.Base, *, ctx: context.ResolverContextLevel\n) -> pgast.Base:\n    _raise_unsupported(expr)\n\n\ndef resolve[Base_T: pgast.Base](\n    expr: Base_T, *, ctx: context.ResolverContextLevel\n) -> Base_T:\n    res = _resolve(expr, ctx=ctx)\n    return typing.cast(Base_T, res.replace(span=expr.span))\n\n\ndef resolve_opt[Base_T: pgast.Base](\n    node: typing.Optional[Base_T], *, ctx: context.ResolverContextLevel\n) -> typing.Optional[Base_T]:\n    if not node:\n        return None\n    return resolve(node, ctx=ctx)\n\n\ndef resolve_list[Base_T: pgast.Base](\n    exprs: typing.Sequence[Base_T], *, ctx: context.ResolverContextLevel\n) -> list[Base_T]:\n    return [resolve(e, ctx=ctx) for e in exprs]\n\n\ndef resolve_opt_list[Base_T: pgast.Base](\n    exprs: typing.Optional[list[Base_T]],\n    *,\n    ctx: context.ResolverContextLevel,\n) -> typing.Optional[list[Base_T]]:\n    if not exprs:\n        return None\n    return resolve_list(exprs, ctx=ctx)\n\n\ndef resolve_relation(\n    rel: pgast.BaseRelation,\n    *,\n    include_inherited: bool = True,\n    ctx: context.ResolverContextLevel,\n) -> tuple[pgast.BaseRelation, context.Table]:\n    rel, tab = _resolve_relation(\n        rel, include_inherited=include_inherited, ctx=ctx\n    )\n    return rel.replace(span=rel.span), tab\n\n\n@functools.singledispatch\ndef _resolve_relation(\n    rel: pgast.BaseRelation,\n    *,\n    include_inherited: bool,\n    ctx: context.ResolverContextLevel,\n) -> tuple[pgast.BaseRelation, context.Table]:\n    _raise_unsupported(rel)\n\n\n@_resolve.register\ndef _resolve_BaseRelation(\n    rel: pgast.BaseRelation, *, ctx: context.ResolverContextLevel\n) -> pgast.BaseRelation:\n    # use _resolve_BaseRelation in normal _resolve dispatch\n\n    rel, _ = resolve_relation(rel, ctx=ctx)\n    return rel\n\n\ndef _raise_unsupported(expr: pgast.Base) -> typing.Never:\n    pretty_name = expr.__class__.__name__\n    pretty_name = pretty_name.removesuffix('Stmt')\n    # title case to spaces\n    pretty_name = re.sub(r'(?<!^)(?=[A-Z])', ' ', pretty_name).upper()\n\n    raise errors.UnsupportedFeatureError(\n        f'not supported: {pretty_name}',\n        span=expr.span,\n        pgext_code=pgerror.ERROR_FEATURE_NOT_SUPPORTED,\n    )\n"
  },
  {
    "path": "edb/pgsql/resolver/expr.py",
    "content": "#\n#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"SQL resolver that compiles public SQL to internal SQL which is executable\nin our internal Postgres instance.\"\"\"\n\nfrom typing import (\n    Iterable,\n    Optional,\n    Iterator,\n    Sequence,\n    cast,\n)\nimport uuid\n\nfrom edb import errors\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import common\nfrom edb.pgsql.parser import parser as pg_parser\nfrom edb.pgsql.common import quote_ident as qi\nfrom edb.pgsql import compiler as pgcompiler\nfrom edb.pgsql.compiler import enums as pgce\n\nfrom edb.schema import types as s_types\nfrom edb.schema import pointers as s_pointers\n\nfrom edb.ir import ast as irast\n\nfrom edb.edgeql import compiler as qlcompiler\n\nfrom edb.server.pgcon import errors as pgerror\nfrom edb.server.compiler import dbstate\n\nfrom . import dispatch\nfrom . import context\nfrom . import static\nfrom . import command\n\nContext = context.ResolverContextLevel\n\n\ndef infer_alias(res_target: pgast.ResTarget) -> Optional[str]:\n    if res_target.name:\n        return res_target.name\n\n    val = res_target.val\n\n    if isinstance(val, pgast.TypeCast):\n        val = val.arg\n\n    if isinstance(val, pgast.FuncCall):\n        return val.name[-1]\n\n    if isinstance(val, pgast.ImplicitRowExpr):\n        return 'row'\n\n    # if just name has been selected, use it as the alias\n    if isinstance(val, pgast.ColumnRef):\n        name = val.name\n        if isinstance(name[-1], str):\n            return name[-1]\n\n    return None\n\n\n# this function cannot go though dispatch,\n# because it may return multiple nodes, due to * notation\ndef resolve_ResTarget(\n    res_target: pgast.ResTarget,\n    *,\n    existing_names: set[str],\n    ctx: Context,\n) -> tuple[Sequence[pgast.ResTarget], Sequence[context.Column]]:\n    targets, columns = _resolve_ResTarget(\n        res_target, existing_names=existing_names, ctx=ctx\n    )\n\n    return (targets, columns)\n\n\ndef _resolve_ResTarget(\n    res_target: pgast.ResTarget,\n    *,\n    existing_names: set[str],\n    ctx: Context,\n) -> tuple[Sequence[pgast.ResTarget], Sequence[context.Column]]:\n    alias = infer_alias(res_target)\n\n    # special case for ColumnRef for handing wildcards\n    if not alias and isinstance(res_target.val, pgast.ColumnRef):\n        col_res = _lookup_column(res_target.val, ctx)\n\n        res = []\n        columns = []\n        for table, column in col_res:\n            val = resolve_column_kind(table, column.kind, ctx=ctx)\n\n            # make sure name is not duplicated\n            # this behavior is technically different then Postgres, but EdgeDB\n            # protocol does not support duplicate names. And we doubt that\n            # anyone is depending on original behavior.\n            nam: str = column.name\n            if nam in existing_names:\n                # prefix with table name\n                rel_var_name = table.alias or table.name\n                if rel_var_name:\n                    nam = rel_var_name + '_' + nam\n            if nam in existing_names:\n                if ctx.options.disambiguate_column_names:\n                    raise errors.QueryError(\n                        f'duplicate column name: `{nam}`',\n                        span=res_target.span,\n                        pgext_code=pgerror.ERROR_UNDEFINED_COLUMN,\n                    )\n            existing_names.add(nam)\n\n            res.append(\n                pgast.ResTarget(\n                    name=nam,\n                    val=val,\n                )\n            )\n            columns.append(\n                context.Column(\n                    name=nam,\n                    hidden=column.hidden,\n                    kind=column.kind,\n                )\n            )\n        return (res, columns)\n\n    # base case\n    val = dispatch.resolve(res_target.val, ctx=ctx)\n\n    # special case for statically-evaluated FuncCall\n    if (\n        not alias\n        and isinstance(val, pgast.StringConstant)\n        and isinstance(res_target.val, pgast.FuncCall)\n    ):\n        alias = static.name_in_pg_catalog(res_target.val.name)\n\n    if alias in existing_names:\n        # duplicate name\n\n        if res_target.name:\n            # explicit duplicate name: error out\n            if ctx.options.disambiguate_column_names:\n                raise errors.QueryError(\n                    f'duplicate column name: `{alias}`',\n                    span=res_target.span,\n                    pgext_code=pgerror.ERROR_UNDEFINED_COLUMN,\n                )\n        else:\n            # inferred duplicate name: use generated alias instead\n\n            # this behavior is technically different than Postgres, but it is\n            # also not documented and users should not be relying on it.\n            # It does help us in some cases\n            # (passing `SELECT a.id, b.id` into DML).\n            alias = None\n\n    name: str = alias or ctx.alias_generator.get('col')\n    existing_names.add(name)\n\n    col = context.Column(\n        name=name, kind=context.ColumnByName(reference_as=name)\n    )\n    new_target = pgast.ResTarget(val=val, name=name, span=res_target.span)\n    return (new_target,), (col,)\n\n\ndef resolve_column_kind(\n    table: context.Table, column: context.ColumnKind, *, ctx: Context\n) -> pgast.BaseExpr:\n    match column:\n        case context.ColumnByName(reference_as=reference_as):\n            if table.reference_as:\n                return pgast.ColumnRef(name=(table.reference_as, reference_as))\n            else:\n                # In some cases tables might not have an assigned alias\n                # because that is not syntactically possible (COPY), or because\n                # the table being referenced is currently being assembled\n                # (e.g. ORDER BY refers to a newly defined column).\n\n                # So we make an assumption that in such cases, this will not\n                # be ambiguous. I think this is not strictly correct.\n                return pgast.ColumnRef(name=(reference_as,))\n\n        case context.ColumnStaticVal(val=val):\n            # special case: __type__ static value\n            return _uuid_const(val)\n        case context.ColumnPgExpr(expr=e):\n            return e\n        case context.ColumnComputable(pointer=pointer):\n            expr = pointer.get_expr(ctx.schema)\n            assert expr\n\n            source = pointer.get_source(ctx.schema)\n\n            subject_id: irast.PathId\n            source_id: irast.PathId\n            if isinstance(source, s_types.Type):\n                subject_id = irast.PathId.from_type(\n                    ctx.schema, source, env=None\n                )\n                source_id = subject_id\n            else:\n                assert isinstance(source, s_pointers.Pointer)\n                subject_id = irast.PathId.from_pointer(\n                    ctx.schema, source, env=None\n                )\n                s = source.get_source(ctx.schema)\n                assert isinstance(s, s_types.Type)\n                source_id = irast.PathId.from_type(ctx.schema, s, env=None)\n\n            singletons = [source]\n            options = qlcompiler.CompilerOptions(\n                modaliases={None: 'default'},\n                anchors={'__source__': source},\n                path_prefix_anchor='__source__',\n                singletons=singletons,\n                make_globals_empty=False,\n                apply_user_access_policies=ctx.options.apply_access_policies,\n            )\n            compiled = expr.compiled(ctx.schema, options=options, context=None)\n\n            subject_rel = pgast.Relation(name=table.reference_as)\n            if isinstance(source, s_types.Type):\n                subject_rel.path_outputs = {\n                    (source_id, pgce.PathAspect.IDENTITY): pgast.ColumnRef(\n                        name=('id',)\n                    )\n                }\n            else:\n                subject_rel.path_outputs = {\n                    (source_id, pgce.PathAspect.IDENTITY): pgast.ColumnRef(\n                        name=('source',)\n                    )\n                }\n            subject_rel_var = pgast.RelRangeVar(\n                alias=pgast.Alias(aliasname=table.reference_as),\n                relation=subject_rel,\n            )\n\n            sql_tree = pgcompiler.compile_ir_to_sql_tree(\n                compiled.irast,\n                external_rvars={\n                    (subject_id, pgce.PathAspect.SOURCE): subject_rel_var,\n                    (subject_id, pgce.PathAspect.VALUE): subject_rel_var,\n                    (source_id, pgce.PathAspect.IDENTITY): subject_rel_var,\n                },\n                output_format=pgcompiler.OutputFormat.NATIVE_INTERNAL,\n                alias_generator=ctx.alias_generator,\n            )\n            command.merge_params(sql_tree, compiled.irast, ctx)\n\n            assert isinstance(sql_tree.ast, pgast.BaseExpr)\n            return sql_tree.ast\n        case _:\n            raise NotImplementedError(column)\n\n\n@dispatch._resolve.register\ndef resolve_ColumnRef(\n    column_ref: pgast.ColumnRef, *, ctx: Context\n) -> pgast.BaseExpr:\n    res = _lookup_column(column_ref, ctx)\n    table, column = res[0]\n\n    if len(res) != 1:\n        # Lookup can have multiple results only when using *.\n        assert table.reference_as\n        return pgast.ColumnRef(name=(table.reference_as, pgast.Star()))\n\n    return resolve_column_kind(table, column.kind, ctx=ctx)\n\n\ndef _uuid_const(val: uuid.UUID):\n    return pgast.TypeCast(\n        arg=pgast.StringConstant(val=str(val)),\n        type_name=pgast.TypeName(name=('uuid',)),\n    )\n\n\ndef _lookup_column(\n    column_ref: pgast.ColumnRef,\n    ctx: Context,\n) -> Sequence[tuple[context.Table, context.Column]]:\n    matched_columns: list[tuple[context.Table, context.Column]] = []\n\n    name = column_ref.name\n    col_name: str | pgast.Star\n\n    if len(name) == 1:\n        # look for the column in all tables\n        col_name = name[0]\n\n        if isinstance(col_name, pgast.Star):\n            return [\n                (t, c)\n                for t in ctx.scope.tables\n                # Only look at the highest precedence level for\n                # *. That is, we take everything in our local FROM\n                # clauses but not stuff in enclosing queries, if we\n                # are a subquery.\n                if t.precedence == 0\n                for c in t.columns\n                if not c.hidden\n            ]\n\n        for table in ctx.scope.tables:\n            matched_columns.extend(_lookup_in_table(col_name, table))\n\n        if not matched_columns:\n            # is it a reference to a rel var?\n            try:\n                tab = _lookup_table(col_name, ctx)\n                assert tab.reference_as\n                col = context.Column(\n                    name=tab.reference_as,\n                    kind=context.ColumnByName(reference_as=tab.reference_as),\n                )\n                return [(context.Table(), col)]\n            except errors.QueryError:\n                pass\n\n    elif len(name) >= 2:\n        # look for the column in the specific table\n        tab_name, col_name = name[-2:]\n\n        try:\n            table = _lookup_table(cast(str, tab_name), ctx)\n        except errors.QueryError as e:\n            e.set_span(column_ref.span)\n            raise\n\n        if isinstance(col_name, pgast.Star):\n            return [(table, c) for c in table.columns if not c.hidden]\n        else:\n            matched_columns.extend(_lookup_in_table(col_name, table))\n\n    if not matched_columns:\n        raise errors.QueryError(\n            f'column {qi(col_name, force=True)} does not exist',\n            span=column_ref.span,\n            pgext_code=pgerror.ERROR_UNDEFINED_COLUMN,\n        )\n\n    # apply precedence\n    if len(matched_columns) > 1:\n        max_precedence = max(t.precedence for t, _ in matched_columns)\n        matched_columns = [\n            (t, c) for t, c in matched_columns if t.precedence == max_precedence\n        ]\n\n    # when ambiguous references have been used in USING clause,\n    # we resolve them to first or the second column or a COALESCE of the two.\n    if (\n        len(matched_columns) == 2\n        and matched_columns[0][1].name == matched_columns[1][1].name\n    ):\n        matched_name = matched_columns[0][1].name\n        matched_tables = [t for t, _c in matched_columns]\n\n        for c_name, t_left, t_right, join_type in ctx.scope.factored_columns:\n            if matched_name != c_name:\n                continue\n            if not (t_left in matched_tables and t_right in matched_tables):\n                continue\n\n            c_left = next(c for c in t_left.columns if c.name == c_name)\n            c_right = next(c for c in t_right.columns if c.name == c_name)\n\n            if join_type == 'INNER' or join_type == 'LEFT':\n                matched_columns = [(t_left, c_left)]\n            elif join_type == 'RIGHT':\n                matched_columns = [(t_right, c_right)]\n            elif join_type == 'FULL':\n                coalesce = pgast.CoalesceExpr(\n                    args=[\n                        resolve_column_kind(t_left, c_left.kind, ctx=ctx),\n                        resolve_column_kind(t_right, c_right.kind, ctx=ctx),\n                    ]\n                )\n                c_coalesce = context.Column(\n                    name=c_name,\n                    kind=context.ColumnPgExpr(expr=coalesce),\n                )\n                matched_columns = [(t_left, c_coalesce)]\n            else:\n                raise NotImplementedError()\n            break\n\n    if len(matched_columns) > 1:\n        potential_tables = ', '.join([t.name or '' for t, _ in matched_columns])\n        raise errors.QueryError(\n            f'ambiguous column `{col_name}` could belong to '\n            f'following tables: {potential_tables}',\n            span=column_ref.span,\n        )\n\n    return matched_columns\n\n\ndef _lookup_in_table(\n    col_name: str, table: context.Table\n) -> Iterator[tuple[context.Table, context.Column]]:\n    for column in table.columns:\n        if column.name == col_name:\n            yield (table, column)\n\n\ndef _maybe_lookup_table(tab_name: str, ctx: Context) -> context.Table | None:\n    matched_tables: list[context.Table] = []\n    for t in ctx.scope.tables:\n        t_name = t.alias or t.name\n        if t_name == tab_name:\n            matched_tables.append(t)\n\n    if not matched_tables:\n        return None\n\n    # apply precedence\n    if len(matched_tables) > 1:\n        max_precedence = max(t.precedence for t in matched_tables)\n        matched_tables = [\n            t for t in matched_tables if t.precedence == max_precedence\n        ]\n\n    if len(matched_tables) > 1:\n        raise errors.QueryError(f'ambiguous table `{tab_name}`')\n\n    table = matched_tables[0]\n    return table\n\n\ndef _lookup_table(tab_name: str, ctx: Context) -> context.Table:\n    table = _maybe_lookup_table(tab_name, ctx=ctx)\n    if table is None:\n        raise errors.QueryError(f'cannot find table `{tab_name}`')\n    return table\n\n\n@dispatch._resolve.register\ndef resolve_SubLink(\n    sub_link: pgast.SubLink,\n    *,\n    ctx: Context,\n) -> pgast.SubLink:\n    with ctx.child() as subctx:\n        expr = dispatch.resolve(sub_link.expr, ctx=subctx)\n\n    return pgast.SubLink(\n        operator=sub_link.operator,\n        expr=expr,\n        test_expr=dispatch.resolve_opt(sub_link.test_expr, ctx=ctx),\n    )\n\n\n@dispatch._resolve.register\ndef resolve_Expr(expr: pgast.Expr, *, ctx: Context) -> pgast.Expr:\n    return pgast.Expr(\n        name=expr.name,\n        lexpr=dispatch.resolve(expr.lexpr, ctx=ctx) if expr.lexpr else None,\n        rexpr=dispatch.resolve(expr.rexpr, ctx=ctx) if expr.rexpr else None,\n    )\n\n\n@dispatch._resolve.register\ndef resolve_TypeCast(\n    expr: pgast.TypeCast,\n    *,\n    ctx: Context,\n) -> pgast.BaseExpr:\n    pg_catalog_name = static.name_in_pg_catalog(expr.type_name.name)\n    if pg_catalog_name == 'regclass' and not expr.type_name.array_bounds:\n        return static.cast_to_regclass(expr.arg, ctx)\n\n    return pgast.TypeCast(\n        arg=dispatch.resolve(expr.arg, ctx=ctx),\n        type_name=expr.type_name,\n    )\n\n\n@dispatch._resolve.register\ndef resolve_BaseConstant(\n    expr: pgast.BaseConstant,\n    *,\n    ctx: Context,\n) -> pgast.BaseConstant:\n    return expr\n\n\n@dispatch._resolve.register\ndef resolve_CaseExpr(\n    expr: pgast.CaseExpr,\n    *,\n    ctx: Context,\n) -> pgast.CaseExpr:\n    return pgast.CaseExpr(\n        arg=dispatch.resolve_opt(expr.arg, ctx=ctx),\n        args=dispatch.resolve_list(expr.args, ctx=ctx),\n        defresult=dispatch.resolve_opt(expr.defresult, ctx=ctx),\n    )\n\n\n@dispatch._resolve.register\ndef resolve_CaseWhen(\n    expr: pgast.CaseWhen,\n    *,\n    ctx: Context,\n) -> pgast.CaseWhen:\n    return pgast.CaseWhen(\n        expr=dispatch.resolve(expr.expr, ctx=ctx),\n        result=dispatch.resolve(expr.result, ctx=ctx),\n    )\n\n\n@dispatch._resolve.register\ndef resolve_SortBy(\n    expr: pgast.SortBy,\n    *,\n    ctx: Context,\n) -> pgast.SortBy:\n    return pgast.SortBy(\n        node=dispatch.resolve(expr.node, ctx=ctx),\n        dir=expr.dir,\n        nulls=expr.nulls,\n    )\n\n\n@dispatch._resolve.register\ndef resolve_LockingClause(\n    expr: pgast.LockingClause,\n    *,\n    ctx: Context,\n) -> pgast.LockingClause:\n    tables: list[context.Table] = []\n    if expr.locked_rels is not None:\n        for rvar in expr.locked_rels:\n            assert rvar.relation.name\n            table = _lookup_table(rvar.relation.name, ctx=ctx)\n            tables.append(table)\n    else:\n        tables.extend(ctx.scope.tables)\n\n    # validate that the locking clause can be used on these tables\n    for table in tables:\n        if table.schema_id and not table.is_direct_relation:\n            raise errors.QueryError(\n                f'locking clause not supported: `{table.name or table.alias}` '\n                'must not have child types or access policies',\n                pgext_code=pgerror.ERROR_FEATURE_NOT_SUPPORTED,\n            )\n\n    return pgast.LockingClause(\n        strength=expr.strength,\n        locked_rels=[\n            pgast.RelRangeVar(relation=pgast.Relation(name=table.reference_as))\n            for table in tables\n        ],\n        wait_policy=expr.wait_policy,\n    )\n\n\nfunc_calls_remapping: dict[tuple[str, ...], tuple[str, ...]] = {\n    ('information_schema', '_pg_truetypid'): (\n        common.versioned_schema('edgedbsql'),\n        '_pg_truetypid',\n    ),\n    ('information_schema', '_pg_truetypmod'): (\n        common.versioned_schema('edgedbsql'),\n        '_pg_truetypmod',\n    ),\n    ('pg_catalog', 'format_type'): (\n        common.versioned_schema('edgedbsql'),\n        '_format_type',\n    ),\n    ('format_type',): (\n        common.versioned_schema('edgedbsql'),\n        '_format_type',\n    ),\n    ('pg_catalog', 'pg_get_constraintdef'): (\n        common.versioned_schema('edgedbsql'),\n        'pg_get_constraintdef',\n    ),\n    ('pg_get_constraintdef',): (\n        common.versioned_schema('edgedbsql'),\n        'pg_get_constraintdef',\n    ),\n}\n\nfuncs_with_text_args: set[str] = {\n    'num_nulls',\n    'num_nonnulls',\n    'int8inc_any',\n    'int8dec_any',\n    'pg_typeof',\n    'pg_collation_for',\n    'concat',\n    'concat_ws',\n    'format',\n    'count',\n    'pg_column_size',\n    'json_build_array',\n    'jsonb_build_array',\n    'json_build_object',\n    'jsonb_build_object',\n    'json_object_agg',\n    'jsonb_object_agg',\n    'json_object_agg_strict',\n    'jsonb_object_agg_strict',\n    'json_object_agg_unique',\n    'jsonb_object_agg_unique',\n    'json_object_agg_unique_strict',\n    'jsonb_object_agg_unique_strict',\n}\n\n\n@dispatch._resolve.register\ndef resolve_FuncCall(\n    call: pgast.FuncCall,\n    *,\n    ctx: Context,\n) -> pgast.BaseExpr:\n    # Special case: some function calls (mostly from pg_catalog) are\n    # intercepted and statically evaluated.\n    if res := static.eval_FuncCall(call, ctx=ctx):\n        return res\n\n    # Remap function name and default to the original name.\n    # Effectively, this exposes all non-remapped functions.\n    name = func_calls_remapping.get(call.name, call.name)\n\n    args = dispatch.resolve_list(call.args, ctx=ctx)\n\n    # If arg is a param, add type annotations, so function can be resolved.\n    # For example, `json_build_object($1, $2)` must be injected with annotations\n    # See maybe_annotate_param for more info\n    name_in_pg = static.name_in_pg_catalog(call.name)\n    unknown_as = 'text' if name_in_pg in funcs_with_text_args else 'unknown'\n    args = [\n        maybe_annotate_param(a, unknown_as=unknown_as, ctx=ctx) for a in args\n    ]\n\n    res = pgast.FuncCall(\n        name=name,\n        args=args,\n        agg_order=dispatch.resolve_opt_list(call.agg_order, ctx=ctx),\n        agg_filter=dispatch.resolve_opt(call.agg_filter, ctx=ctx),\n        agg_star=call.agg_star,\n        agg_distinct=call.agg_distinct,\n        agg_within_group=call.agg_within_group,\n        over=dispatch.resolve_opt(call.over, ctx=ctx),\n        with_ordinality=call.with_ordinality,\n    )\n\n    return res\n\n\n@dispatch._resolve.register\ndef resolve_WindowDef(\n    expr: pgast.WindowDef,\n    *,\n    ctx: Context,\n) -> pgast.WindowDef:\n    return pgast.WindowDef(\n        partition_clause=dispatch.resolve_opt_list(\n            expr.partition_clause, ctx=ctx\n        ),\n        order_clause=dispatch.resolve_opt_list(expr.order_clause, ctx=ctx),\n        start_offset=dispatch.resolve_opt(expr.start_offset, ctx=ctx),\n        end_offset=dispatch.resolve_opt(expr.end_offset, ctx=ctx),\n    )\n\n\n@dispatch._resolve.register\ndef resolve_CoalesceExpr(\n    expr: pgast.CoalesceExpr,\n    *,\n    ctx: Context,\n) -> pgast.CoalesceExpr:\n    return pgast.CoalesceExpr(args=dispatch.resolve_list(expr.args, ctx=ctx))\n\n\n@dispatch._resolve.register\ndef resolve_NullTest(\n    expr: pgast.NullTest,\n    *,\n    ctx: Context,\n) -> pgast.NullTest:\n    return pgast.NullTest(\n        arg=dispatch.resolve(expr.arg, ctx=ctx), negated=expr.negated\n    )\n\n\n@dispatch._resolve.register\ndef resolve_BooleanTest(\n    expr: pgast.BooleanTest,\n    *,\n    ctx: Context,\n) -> pgast.BooleanTest:\n    return pgast.BooleanTest(\n        arg=dispatch.resolve(expr.arg, ctx=ctx),\n        negated=expr.negated,\n        is_true=expr.is_true,\n    )\n\n\n@dispatch._resolve.register\ndef resolve_ImplicitRowExpr(\n    expr: pgast.ImplicitRowExpr,\n    *,\n    ctx: Context,\n) -> pgast.ImplicitRowExpr:\n    return pgast.ImplicitRowExpr(\n        args=dispatch.resolve_list(expr.args, ctx=ctx),\n    )\n\n\n@dispatch._resolve.register\ndef resolve_RowExpr(\n    expr: pgast.RowExpr,\n    *,\n    ctx: Context,\n) -> pgast.RowExpr:\n    return construct_row_expr(\n        dispatch.resolve_list(expr.args, ctx=ctx),\n        ctx=ctx,\n    )\n\n\ndef construct_row_expr(\n    args: Iterable[pgast.BaseExpr], *, ctx: Context\n) -> pgast.RowExpr:\n    # Constructs a ROW and maybe injects type casts for params.\n\n    return pgast.RowExpr(\n        args=[maybe_annotate_param(a, unknown_as='text', ctx=ctx) for a in args]\n    )\n\n\ndef maybe_annotate_param(\n    expr: pgast.BaseExpr,\n    *,\n    unknown_as: str = 'unknown',\n    ctx: Context,\n):\n    # If the expression is a param whose type is `unknown`, we need to inject a\n    # type cast that passes this information to Postgres.\n    # Ideally, we could inject type `unknown`, but that would not work for some\n    # cases, such as ROW('hello') or json_build_object('hello', TRUE).\n    # So for special cases, where string literals are known to represent text,\n    # we inject text and otherwise, we inject unknown.\n\n    if isinstance(expr, pgast.ParamRef):\n        param = ctx.query_params[expr.number - 1]\n        if (\n            isinstance(param, dbstate.SQLParamExtractedConst)\n            and param.type_oid == pg_parser.PgLiteralTypeOID.UNKNOWN\n        ):\n            return pgast.TypeCast(\n                arg=expr, type_name=pgast.TypeName(name=(unknown_as,))\n            )\n    return expr\n\n\n@dispatch._resolve.register\ndef resolve_ParamRef(\n    expr: pgast.ParamRef,\n    *,\n    ctx: Context,\n) -> pgast.ParamRef:\n    # external params map one-to-one to internal params\n    if expr.number < 1:\n        raise errors.QueryError(\n            f'param out of bounds: ${expr.number}',\n            pgext_code=pgerror.ERROR_UNDEFINED_PARAMETER,\n            hint='query parameters start with 1',\n        )\n\n    param = ctx.query_params[expr.number - 1]\n    param.used = True\n\n    return expr\n\n\n@dispatch._resolve.register\ndef resolve_ArrayExpr(\n    expr: pgast.ArrayExpr,\n    *,\n    ctx: Context,\n) -> pgast.ArrayExpr:\n    return pgast.ArrayExpr(\n        elements=dispatch.resolve_list(expr.elements, ctx=ctx)\n    )\n\n\n@dispatch._resolve.register\ndef resolve_Indirection(\n    expr: pgast.Indirection,\n    *,\n    ctx: Context,\n) -> pgast.Indirection:\n    return pgast.Indirection(\n        arg=dispatch.resolve(expr.arg, ctx=ctx),\n        indirection=dispatch.resolve_list(expr.indirection, ctx=ctx),\n    )\n\n\n@dispatch._resolve.register\ndef resolve_RecordIndirectionOp(\n    expr: pgast.RecordIndirectionOp,\n    *,\n    ctx: Context,\n) -> pgast.RecordIndirectionOp:\n    return expr\n\n\n@dispatch._resolve.register\ndef resolve_Slice(\n    expr: pgast.Slice,\n    *,\n    ctx: Context,\n) -> pgast.Slice:\n    return pgast.Slice(\n        lidx=dispatch.resolve_opt(expr.lidx, ctx=ctx),\n        ridx=dispatch.resolve_opt(expr.ridx, ctx=ctx),\n    )\n\n\n@dispatch._resolve.register\ndef resolve_Index(\n    expr: pgast.Index,\n    *,\n    ctx: Context,\n) -> pgast.Index:\n    return pgast.Index(\n        idx=dispatch.resolve(expr.idx, ctx=ctx),\n    )\n\n\n@dispatch._resolve.register\ndef resolve_SQLValueFunction(\n    expr: pgast.SQLValueFunction,\n    *,\n    ctx: Context,\n) -> pgast.BaseExpr:\n    return static.eval_SQLValueFunction(expr, ctx=ctx)\n\n\n@dispatch._resolve.register\ndef resolve_CollateClause(\n    expr: pgast.CollateClause,\n    *,\n    ctx: Context,\n) -> pgast.BaseExpr:\n    return pgast.CollateClause(\n        arg=dispatch.resolve(expr.arg, ctx=ctx), collname=expr.collname\n    )\n\n\n@dispatch._resolve.register\ndef resolve_MinMaxExpr(\n    expr: pgast.MinMaxExpr,\n    *,\n    ctx: Context,\n) -> pgast.BaseExpr:\n    return pgast.MinMaxExpr(\n        op=expr.op,\n        args=dispatch.resolve_list(expr.args, ctx=ctx),\n    )\n"
  },
  {
    "path": "edb/pgsql/resolver/range_functions.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Declarations of supported range functions\"\"\"\n\nCOLUMNS = {\n    'json_array_elements': ['value'],\n    'json_array_elements_text': ['value'],\n    'json_each': ['key', 'value'],\n    'json_each_text': ['key', 'value'],\n    'jsonb_array_elements': ['value'],\n    'jsonb_array_elements_text': ['value'],\n    'jsonb_each': ['key', 'value'],\n    'jsonb_each_text': ['key', 'value'],\n    'pg_available_extension_versions': [\n        'name',\n        'version',\n        'superuser',\n        'trusted',\n        'relocatable',\n        'schema',\n        'requires',\n        'comment',\n    ],\n    'pg_available_extensions': ['name', 'default_version', 'comment'],\n    'pg_config': ['name', 'setting'],\n    'pg_control_checkpoint': [\n        'checkpoint_lsn',\n        'redo_lsn',\n        'redo_wal_file',\n        'timeline_id',\n        'prev_timeline_id',\n        'full_page_writes',\n        'next_xid',\n        'next_oid',\n        'next_multixact_id',\n        'next_multi_offset',\n        'oldest_xid',\n        'oldest_commit_ts_xid',\n        'checkpoint_time',\n        'newest_commit_ts_xid',\n        'oldest_multi_dbid',\n        'oldest_multi_xid',\n        'oldest_active_xid',\n        'oldest_xid_dbid',\n    ],\n    'pg_control_init': [\n        'max_data_alignment',\n        'database_block_size',\n        'blocks_per_segment',\n        'wal_block_size',\n        'bytes_per_wal_segment',\n        'max_identifier_length',\n        'max_index_columns',\n        'max_toast_chunk_size',\n        'large_object_chunk_size',\n        'float8_pass_by_value',\n        'data_page_checksum_version',\n    ],\n    'pg_control_recovery': [\n        'min_recovery_end_lsn',\n        'min_recovery_end_timeline',\n        'backup_start_lsn',\n        'backup_end_lsn',\n        'end_of_backup_record_required',\n    ],\n    'pg_control_system': [\n        'pg_control_version',\n        'catalog_version_no',\n        'system_identifier',\n        'pg_control_last_modified',\n    ],\n    'pg_copy_logical_replication_slot': [\n        'slot_name',\n        'slot_name',\n        'lsn',\n        'slot_name',\n        'lsn',\n        'lsn',\n    ],\n    'pg_copy_physical_replication_slot': [\n        'slot_name',\n        'lsn',\n        'slot_name',\n        'lsn',\n    ],\n    'pg_create_logical_replication_slot': ['slot_name', 'lsn'],\n    'pg_create_physical_replication_slot': ['slot_name', 'lsn'],\n    'pg_cursor': [\n        'name',\n        'statement',\n        'is_holdable',\n        'is_binary',\n        'is_scrollable',\n        'creation_time',\n    ],\n    'pg_event_trigger_ddl_commands': [\n        'classid',\n        'objid',\n        'objsubid',\n        'command_tag',\n        'object_type',\n        'schema_name',\n        'object_identity',\n        'in_extension',\n        'command',\n    ],\n    'pg_event_trigger_dropped_objects': [\n        'classid',\n        'objid',\n        'objsubid',\n        'original',\n        'normal',\n        'is_temporary',\n        'object_type',\n        'schema_name',\n        'object_name',\n        'object_identity',\n        'address_names',\n        'address_args',\n    ],\n    'pg_event_trigger_table_rewrite_oid': ['oid'],\n    'pg_extension_update_paths': ['source', 'target', 'path'],\n    'pg_get_backend_memory_contexts': [\n        'name',\n        'ident',\n        'parent',\n        'level',\n        'total_bytes',\n        'total_nblocks',\n        'free_bytes',\n        'free_chunks',\n        'used_bytes',\n    ],\n    'pg_get_catalog_foreign_keys': [\n        'fktable',\n        'fkcols',\n        'pktable',\n        'pkcols',\n        'is_array',\n        'is_opt',\n    ],\n    'pg_get_keywords': ['word', 'catcode', 'barelabel', 'catdesc', 'baredesc'],\n    'pg_get_multixact_members': ['xid', 'mode'],\n    'pg_get_object_address': ['classid', 'objid', 'objsubid'],\n    'pg_get_publication_tables': ['relid'],\n    'pg_get_replication_slots': [\n        'slot_name',\n        'plugin',\n        'slot_type',\n        'datoid',\n        'temporary',\n        'active',\n        'active_pid',\n        'xmin',\n        'catalog_xmin',\n        'restart_lsn',\n        'confirmed_flush_lsn',\n        'wal_status',\n        'safe_wal_size',\n        'two_phase',\n    ],\n    'pg_get_shmem_allocations': ['name', 'off', 'size', 'allocated_size'],\n    'pg_hba_file_rules': [\n        'line_number',\n        'type',\n        'database',\n        'user_name',\n        'address',\n        'netmask',\n        'auth_method',\n        'options',\n        'error',\n    ],\n    'pg_identify_object': ['type', 'schema', 'name', 'identity'],\n    'pg_identify_object_as_address': ['type', 'object_names', 'object_args'],\n    'pg_last_committed_xact': ['xid', 'timestamp', 'roident'],\n    'pg_lock_status': [\n        'locktype',\n        'database',\n        'relation',\n        'page',\n        'tuple',\n        'virtualxid',\n        'transactionid',\n        'classid',\n        'objid',\n        'objsubid',\n        'virtualtransaction',\n        'pid',\n        'fastpath',\n        'waitstart',\n        'granted',\n        'mode',\n    ],\n    'pg_logical_slot_get_binary_changes': ['lsn', 'xid', 'data'],\n    'pg_logical_slot_get_changes': ['lsn', 'xid', 'data'],\n    'pg_logical_slot_peek_binary_changes': ['lsn', 'xid', 'data'],\n    'pg_logical_slot_peek_changes': ['lsn', 'xid', 'data'],\n    'pg_ls_archive_statusdir': ['name', 'size', 'modification'],\n    'pg_ls_logdir': ['name', 'size', 'modification'],\n    'pg_ls_tmpdir': [\n        'name',\n        'size',\n        'name',\n        'modification',\n        'size',\n        'modification',\n    ],\n    'pg_ls_waldir': ['name', 'size', 'modification'],\n    'pg_mcv_list_items': [\n        'index',\n        'values',\n        'nulls',\n        'frequency',\n        'base_frequency',\n    ],\n    'pg_options_to_table': ['option_name', 'option_value'],\n    'pg_partition_ancestors': ['relid'],\n    'pg_partition_tree': ['relid', 'parentrelid', 'isleaf', 'level'],\n    'pg_prepared_statement': [\n        'name',\n        'statement',\n        'prepare_time',\n        'parameter_types',\n        'from_sql',\n        'generic_plans',\n        'custom_plans',\n    ],\n    'pg_prepared_xact': ['transaction', 'gid', 'prepared', 'ownerid', 'dbid'],\n    'pg_replication_slot_advance': ['slot_name', 'end_lsn'],\n    'pg_sequence_parameters': [\n        'start_value',\n        'minimum_value',\n        'maximum_value',\n        'increment',\n        'cycle_option',\n        'cache_size',\n        'data_type',\n    ],\n    'pg_show_all_file_settings': [\n        'sourcefile',\n        'sourceline',\n        'seqno',\n        'name',\n        'setting',\n        'applied',\n        'error',\n    ],\n    'pg_show_all_settings': [\n        'name',\n        'setting',\n        'unit',\n        'category',\n        'short_desc',\n        'extra_desc',\n        'context',\n        'vartype',\n        'source',\n        'min_val',\n        'max_val',\n        'enumvals',\n        'boot_val',\n        'reset_val',\n        'sourcefile',\n        'sourceline',\n        'pending_restart',\n    ],\n    'pg_show_replication_origin_status': [\n        'local_id',\n        'external_id',\n        'remote_lsn',\n        'local_lsn',\n    ],\n    'pg_stat_file': [\n        'size',\n        'size',\n        'access',\n        'modification',\n        'access',\n        'modification',\n        'change',\n        'change',\n        'creation',\n        'isdir',\n        'creation',\n        'isdir',\n    ],\n    'pg_stat_get_activity': [\n        'datid',\n        'pid',\n        'usesysid',\n        'application_name',\n        'state',\n        'query',\n        'wait_event_type',\n        'wait_event',\n        'xact_start',\n        'query_start',\n        'ssl',\n        'backend_start',\n        'state_change',\n        'client_addr',\n        'client_hostname',\n        'client_port',\n        'backend_xid',\n        'backend_xmin',\n        'query_id',\n        'leader_pid',\n        'gss_enc',\n        'gss_princ',\n        'gss_auth',\n        'ssl_issuer_dn',\n        'ssl_client_serial',\n        'ssl_client_dn',\n        'sslbits',\n        'sslcipher',\n        'sslversion',\n        'backend_type',\n    ],\n    'pg_stat_get_archiver': [\n        'archived_count',\n        'last_archived_wal',\n        'last_archived_time',\n        'failed_count',\n        'last_failed_wal',\n        'last_failed_time',\n        'stats_reset',\n    ],\n    'pg_stat_get_progress_info': [\n        'pid',\n        'datid',\n        'relid',\n        'param1',\n        'param2',\n        'param3',\n        'param4',\n        'param5',\n        'param6',\n        'param7',\n        'param20',\n        'param18',\n        'param17',\n        'param16',\n        'param15',\n        'param14',\n        'param13',\n        'param12',\n        'param11',\n        'param10',\n        'param9',\n        'param8',\n        'param19',\n    ],\n    'pg_stat_get_replication_slot': [\n        'slot_name',\n        'spill_txns',\n        'spill_count',\n        'spill_bytes',\n        'stream_txns',\n        'stream_count',\n        'stream_bytes',\n        'total_txns',\n        'total_bytes',\n        'stats_reset',\n    ],\n    'pg_stat_get_slru': [\n        'name',\n        'blks_zeroed',\n        'blks_hit',\n        'blks_read',\n        'blks_written',\n        'blks_exists',\n        'flushes',\n        'truncates',\n        'stats_reset',\n    ],\n    'pg_stat_get_subscription': [\n        'subid',\n        'relid',\n        'pid',\n        'received_lsn',\n        'last_msg_send_time',\n        'last_msg_receipt_time',\n        'latest_end_lsn',\n        'latest_end_time',\n    ],\n    'pg_stat_get_wal': [\n        'wal_records',\n        'wal_fpi',\n        'wal_bytes',\n        'wal_buffers_full',\n        'wal_write',\n        'wal_sync',\n        'wal_write_time',\n        'wal_sync_time',\n        'stats_reset',\n    ],\n    'pg_stat_get_wal_receiver': [\n        'pid',\n        'status',\n        'receive_start_lsn',\n        'receive_start_tli',\n        'written_lsn',\n        'flushed_lsn',\n        'received_tli',\n        'last_msg_send_time',\n        'last_msg_receipt_time',\n        'latest_end_lsn',\n        'latest_end_time',\n        'slot_name',\n        'sender_host',\n        'sender_port',\n        'conninfo',\n    ],\n    'pg_stat_get_wal_senders': [\n        'pid',\n        'state',\n        'sent_lsn',\n        'write_lsn',\n        'flush_lsn',\n        'replay_lsn',\n        'write_lag',\n        'flush_lag',\n        'replay_lag',\n        'sync_priority',\n        'sync_state',\n        'reply_time',\n    ],\n    'pg_stop_backup': ['lsn', 'labelfile', 'spcmapfile'],\n    'pg_timezone_abbrevs': ['abbrev', 'utc_offset', 'is_ds'],\n    'pg_timezone_names': ['name', 'abbrev', 'utc_offset', 'is_dst'],\n    'pg_walfile_name_offset': ['file_name', 'file_offset'],\n    'pg_xact_commit_timestamp_origin': ['timestamp', 'roident'],\n}\n\n# retrieved with\nr'''\nWITH\n    procedures AS (\n        SELECT *\n        FROM pg_proc\n        WHERE proname NOT ILIKE 'ts\\_%'\n          AND proname NOT ILIKE '\\_%'\n          AND proname != 'unnest'\n          AND proname != 'aclexplode'\n    ),\n    pro_args AS (\n        SELECT proname,\n            UNNEST(proargnames) AS argname,\n            UNNEST(proargmodes) AS argmode,\n            GENERATE_SERIES(0, 10, 1) AS argn\n        FROM procedures\n    ),\n    pro_outputs AS (\n        SELECT *\n        FROM pro_args\n        WHERE argmode = 'o'\n        ORDER BY proname, argn\n    )\nSELECT proname, ARRAY_AGG(argname)\nFROM pro_outputs\nGROUP BY proname;\n'''\n"
  },
  {
    "path": "edb/pgsql/resolver/range_var.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"SQL resolver that compiles public SQL to internal SQL which is executable\nin our internal Postgres instance.\"\"\"\n\nimport functools\nfrom typing import Optional, Iterable, cast\n\nfrom edb import errors\nfrom edb.common.parsing import Span\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import common as pgcommon\nfrom edb.pgsql.compiler import astutils as pgastutils\n\nfrom . import dispatch\nfrom . import context\nfrom . import range_functions\nfrom . import expr\n\nContext = context.ResolverContextLevel\n\n\ndef resolve_BaseRangeVar(\n    range_var: pgast.BaseRangeVar, *, ctx: Context\n) -> pgast.BaseRangeVar:\n    # handle join that returns multiple tables and does not use alias\n    if isinstance(range_var, pgast.JoinExpr):\n        return _resolve_JoinExpr(range_var, ctx=ctx)\n\n    # generate internal alias\n    internal_alias = ctx.alias_generator.get('rel')\n    alias = pgast.Alias(\n        aliasname=internal_alias, colnames=range_var.alias.colnames\n    )\n\n    # general case\n    node, table = _resolve_range_var(range_var, alias, ctx=ctx)\n    node = node.replace(span=range_var.span)\n\n    # infer public name and internal alias\n    table.alias = range_var.alias.aliasname\n    table.reference_as = internal_alias\n\n    # pull result relation of inner scope into outer scope\n    ctx.scope.tables.append(table)\n    return node\n\n\n@functools.singledispatch\ndef _resolve_range_var(\n    ir: pgast.BaseRangeVar,\n    alias: pgast.Alias,\n    *,\n    ctx: context.ResolverContextLevel,\n) -> tuple[pgast.BaseRangeVar, context.Table]:\n    raise ValueError(f'no SQL resolve handler for {ir.__class__}')\n\n\n@_resolve_range_var.register\ndef _resolve_RelRangeVar(\n    range_var: pgast.RelRangeVar,\n    alias: pgast.Alias,\n    *,\n    ctx: Context,\n) -> tuple[pgast.BaseRangeVar, context.Table]:\n    with ctx.child() as subctx:\n        relation: pgast.BaseRelation | pgast.CommonTableExpr\n        if isinstance(range_var.relation, pgast.BaseRelation):\n            relation, table = dispatch.resolve_relation(\n                range_var.relation,\n                include_inherited=range_var.include_inherited,\n                ctx=subctx,\n            )\n        else:\n            relation, cte = resolve_CommonTableExpr(\n                range_var.relation, ctx=subctx\n            )\n            table = context.Table(\n                name=cte.name,\n                columns=cte.columns,\n                reference_as=cte.name,\n            )\n\n    table.columns = [\n        context.Column(\n            name=alias or col.name,\n            hidden=col.hidden,\n            kind=(\n                context.ColumnByName(reference_as=alias) if alias else col.kind\n            ),\n        )\n        for col, alias in _zip_column_alias(\n            table.columns, alias, ctx=range_var.span\n        )\n    ]\n\n    rel: pgast.BaseRangeVar\n    if isinstance(relation, pgast.Relation):\n        rel = pgast.RelRangeVar(relation=relation, alias=alias)\n    else:\n        assert isinstance(relation, pgast.Query)\n        rel = pgast.RangeSubselect(subquery=relation, alias=alias)\n    return (rel, table)\n\n\n@_resolve_range_var.register\ndef _resolve_RangeSubselect(\n    range_var: pgast.RangeSubselect,\n    alias: pgast.Alias,\n    *,\n    ctx: Context,\n) -> tuple[pgast.BaseRangeVar, context.Table]:\n    with ctx.lateral() if range_var.lateral else ctx.child() as subctx:\n        subquery, subtable = dispatch.resolve_relation(\n            range_var.subquery, ctx=subctx\n        )\n\n        result = context.Table(\n            name=range_var.alias.aliasname,\n            reference_as=alias.aliasname,\n            columns=[\n                context.Column(\n                    name=alias or col.name,\n                    kind=context.ColumnByName(\n                        reference_as=alias if alias else col.name\n                    ),\n                )\n                for col, alias in _zip_column_alias(\n                    subtable.columns, alias, ctx=range_var.span\n                )\n            ],\n        )\n        alias = pgast.Alias(\n            aliasname=alias.aliasname,\n            colnames=[\n                cast(context.ColumnByName, c.kind).reference_as\n                for c in result.columns\n            ],\n        )\n\n    node = pgast.RangeSubselect(\n        subquery=cast(pgast.Query, subquery),\n        alias=alias,\n        lateral=range_var.lateral,\n    )\n    return node, result\n\n\ndef _resolve_JoinExpr(\n    range_var: pgast.JoinExpr,\n    *,\n    ctx: Context,\n) -> pgast.BaseRangeVar:\n    larg = resolve_BaseRangeVar(range_var.larg, ctx=ctx)\n    ltable = ctx.scope.tables[len(ctx.scope.tables) - 1]\n\n    assert len(range_var.joins) == 1, (\n        \"pg resolver should always produce non-flattened joins\"\n    )\n    join = range_var.joins[0]\n\n    rarg = resolve_BaseRangeVar(join.rarg, ctx=ctx)\n    rtable = ctx.scope.tables[len(ctx.scope.tables) - 1]\n\n    quals: Optional[pgast.BaseExpr] = None\n    if join.quals:\n        quals = dispatch.resolve(join.quals, ctx=ctx)\n\n    if join.using_clause:\n        for c in join.using_clause:\n            assert len(c.name) == 1\n            assert isinstance(c.name[-1], str)\n            c_name = c.name[-1]\n\n            with ctx.child() as subctx:\n                subctx.scope.tables = [ltable]\n                l_expr = dispatch.resolve(c, ctx=subctx)\n            with ctx.child() as subctx:\n                subctx.scope.tables = [rtable]\n                r_expr = dispatch.resolve(c, ctx=subctx)\n\n            ctx.scope.factored_columns.append(\n                (c_name, ltable, rtable, join.type)\n            )\n\n            quals = pgastutils.extend_binop(\n                quals,\n                pgast.Expr(\n                    name='=',\n                    lexpr=l_expr,\n                    rexpr=r_expr,\n                ),\n            )\n\n    return pgast.JoinExpr(\n        larg=larg,\n        joins=[\n            pgast.JoinClause(\n                type=join.type,\n                rarg=rarg,\n                quals=quals,\n            )\n        ],\n    )\n\n\ndef resolve_CommonTableExpr(\n    cte: pgast.CommonTableExpr, *, ctx: Context\n) -> tuple[pgast.CommonTableExpr, context.CTE]:\n    reference_as = None\n\n    with ctx.child() as subctx:\n        aliascolnames = cte.aliascolnames\n\n        if isinstance(cte.query, pgast.SelectStmt):\n            # When no explicit column names were given, we look into the actual\n            # select to see if we can extract the column names from that\n            # instead. This is needed for some RECURSIVE CTEs.\n\n            if not aliascolnames:\n                if isinstance(cte.query.larg, pgast.SelectStmt):\n                    if res := _infer_col_aliases(cte.query.larg):\n                        aliascolnames = res\n\n            if not aliascolnames:\n                if isinstance(cte.query.rarg, pgast.SelectStmt):\n                    if res := _infer_col_aliases(cte.query.rarg):\n                        aliascolnames = res\n\n        if cte.recursive and aliascolnames:\n            reference_as = [\n                subctx.alias_generator.get('col') for _ in aliascolnames\n            ]\n            columns = [\n                context.Column(\n                    name=col, kind=context.ColumnByName(reference_as=ref_as)\n                )\n                for col, ref_as in zip(aliascolnames, reference_as)\n            ]\n            subctx.scope.ctes.append(\n                context.CTE(name=cte.name, columns=columns)\n            )\n\n        query, table = dispatch.resolve_relation(cte.query, ctx=subctx)\n\n        result = context.CTE(name=cte.name, columns=[])\n\n        alias = pgast.Alias(aliasname=cte.name, colnames=aliascolnames)\n\n        for col, al in _zip_column_alias(table.columns, alias, cte.span):\n            result.columns.append(\n                context.Column(\n                    name=al or col.name,\n                    kind=context.ColumnByName(reference_as=col.name),\n                )\n            )\n\n        if reference_as:\n            for col, ref_as in zip(result.columns, reference_as):\n                col.kind = context.ColumnByName(reference_as=ref_as)\n\n    node = pgast.CommonTableExpr(\n        name=cte.name,\n        span=cte.span,\n        aliascolnames=reference_as,\n        query=cast(pgast.Query, query),\n        recursive=cte.recursive,\n        materialized=cte.materialized,\n    )\n    return node, result\n\n\ndef _infer_col_aliases(query: pgast.SelectStmt) -> Optional[list[str]]:\n    aliases = [expr.infer_alias(t) for t in query.target_list]\n    if not all(aliases):\n        return None\n    return cast(list[str], aliases)\n\n\n@_resolve_range_var.register\ndef _resolve_RangeFunction(\n    range_var: pgast.RangeFunction,\n    alias: pgast.Alias,\n    *,\n    ctx: Context,\n) -> tuple[pgast.BaseRangeVar, context.Table]:\n    with ctx.lateral() if range_var.lateral else ctx.child() as subctx:\n        functions: list[pgast.BaseExpr] = []\n        col_names = []\n        for function in range_var.functions:\n            match function:\n                case pgast.FuncCall():\n                    name = function.name[len(function.name) - 1]\n                    if name in range_functions.COLUMNS:\n                        col_names.extend(range_functions.COLUMNS[name])\n                    elif name == 'unnest':\n                        col_names.extend('unnest' for _ in function.args)\n                    else:\n                        col_names.append(name)\n                    functions.append(dispatch.resolve(function, ctx=subctx))\n                case pgast.SQLValueFunction(op=op):\n                    # If SQLValueFunction gets statically evaluated, we need to\n                    # wrap it into a subquery, otherwise it is syntactically\n                    # incorrect. E.g. `SELECT * FROM current_user`, should be\n                    # compiled to `SELECT * FROM (SELECT 'admin')`\n\n                    val = dispatch.resolve(function, ctx=subctx)\n\n                    name = pgcommon.get_sql_value_function_op(op)\n                    range = pgast.RangeSubselect(\n                        subquery=pgast.SelectStmt(\n                            target_list=[pgast.ResTarget(val=val, name=name)]\n                        ),\n                        alias=pgast.Alias(\n                            aliasname=alias.aliasname,\n                            colnames=[name],\n                        ),\n                    )\n\n                    column = context.Column(\n                        name=name,\n                        kind=context.ColumnByName(reference_as=name),\n                    )\n                    table = context.Table(columns=[column])\n\n                    return range, table\n                case _:\n                    functions.append(dispatch.resolve(function, ctx=subctx))\n\n        inferred_columns = [\n            context.Column(\n                name=name, kind=context.ColumnByName(reference_as='')\n            )\n            for name in col_names\n        ]\n\n        if range_var.with_ordinality:\n            inferred_columns.append(\n                context.Column(\n                    name='ordinality',\n                    kind=context.ColumnByName(reference_as='ordinality'),\n                )\n            )\n\n        table = context.Table(\n            columns=[\n                context.Column(\n                    name=al or col.name,\n                    kind=context.ColumnByName(\n                        reference_as=al or ctx.alias_generator.get('col')\n                    ),\n                )\n                for col, al in _zip_column_alias(\n                    inferred_columns, alias, ctx=range_var.span\n                )\n            ]\n        )\n\n        alias = pgast.Alias(\n            aliasname=alias.aliasname,\n            colnames=[\n                cast(context.ColumnByName, c.kind).reference_as\n                for c in table.columns\n                if not c.hidden\n            ],\n        )\n\n        node = pgast.RangeFunction(\n            lateral=range_var.lateral,\n            with_ordinality=range_var.with_ordinality,\n            is_rowsfrom=range_var.is_rowsfrom,\n            functions=functions,\n            alias=alias,\n        )\n        return node, table\n\n\ndef _zip_column_alias(\n    columns: list[context.Column],\n    alias: pgast.Alias,\n    ctx: Optional[Span],\n) -> Iterable[tuple[context.Column, Optional[str]]]:\n    if not alias.colnames:\n        return map(lambda c: (c, None), columns)\n\n    columns = [c for c in columns if not c.hidden]\n\n    if len(columns) != len(alias.colnames):\n        from edb.server.pgcon import errors as pgerror\n\n        raise errors.QueryError(\n            f'Table alias for `{alias.aliasname}` contains '\n            f'{len(alias.colnames)} columns, but the query resolves to '\n            f'{len(columns)} columns',\n            span=ctx,\n            pgext_code=pgerror.ERROR_INVALID_COLUMN_REFERENCE,\n        )\n    return zip(columns, alias.colnames)\n"
  },
  {
    "path": "edb/pgsql/resolver/relation.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"SQL resolver that compiles public SQL to internal SQL which is executable\nin our internal Postgres instance.\"\"\"\n\nfrom typing import Optional, cast\nimport uuid\n\nfrom edb import errors\nfrom edb.server.pgcon import errors as pgerror\n\nfrom edb.edgeql import qltypes\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import common as pgcommon\nfrom edb.pgsql import codegen as pgcodegen\nfrom edb.pgsql import inheritance as pginheritance\nfrom edb.pgsql import types as pgtypes\n\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import links as s_links\nfrom edb.schema import properties as s_properties\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import sources as s_sources\nfrom edb.schema import name as sn\n\nfrom . import dispatch\nfrom . import context\nfrom . import range_var\nfrom . import expr\nfrom . import sql_introspection\nfrom . import command\n\nContext = context.ResolverContextLevel\n\n\n@dispatch._resolve_relation.register\ndef resolve_SelectStmt(\n    stmt: pgast.SelectStmt, *, include_inherited: bool, ctx: Context\n) -> tuple[pgast.SelectStmt, context.Table]:\n    # CTEs\n    ctes: list[pgast.CommonTableExpr] = []\n    if stmt.ctes:\n        for cte in stmt.ctes:\n            cte, tab = range_var.resolve_CommonTableExpr(cte, ctx=ctx)\n            ctes.extend(extract_ctes_from_ctx(ctx))\n            ctes.append(cte)\n            ctx.scope.ctes.append(tab)\n\n    # VALUES\n    if stmt.values:\n        values = dispatch.resolve_list(stmt.values, ctx=ctx)\n        relation = pgast.SelectStmt(\n            values=values, ctes=ctes + extract_ctes_from_ctx(ctx)\n        )\n\n        first_val = values[0]\n        assert isinstance(first_val, pgast.ImplicitRowExpr)\n        table = context.Table(\n            columns=[\n                context.Column(\n                    name=f'column{index + 1}',\n                    kind=context.ColumnByName(\n                        reference_as=f'column{index + 1}'\n                    ),\n                )\n                for index, _ in enumerate(first_val.args)\n            ]\n        )\n        return relation, table\n\n    # UNION\n    if stmt.larg or stmt.rarg:\n        assert stmt.larg and stmt.rarg\n\n        with ctx.child() as subctx:\n            larg, ltable = dispatch.resolve_relation(stmt.larg, ctx=subctx)\n\n        with ctx.child() as subctx:\n            rarg, rtable = dispatch.resolve_relation(stmt.rarg, ctx=subctx)\n\n        # validate equal columns from both sides\n        if len(ltable.columns) != len(rtable.columns):\n            raise errors.QueryError(\n                f'{stmt.op} requires equal number of columns in both sides',\n                span=stmt.span,\n            )\n\n        relation = stmt.replace(\n            larg=cast(pgast.Query, larg),\n            rarg=cast(pgast.Query, rarg),\n            ctes=ctes + extract_ctes_from_ctx(ctx),\n        )\n        return (relation, ltable)\n\n    # FROM\n    from_clause: list[pgast.BaseRangeVar] = []\n    for clause in stmt.from_clause:\n        from_clause.append(range_var.resolve_BaseRangeVar(clause, ctx=ctx))\n\n    # WHERE\n    where = dispatch.resolve_opt(stmt.where_clause, ctx=ctx)\n\n    # GROUP BY\n    with ctx.child() as subctx:\n        register_projections(stmt.target_list, ctx=subctx)\n\n        group_clause = dispatch.resolve_opt_list(stmt.group_clause, ctx=subctx)\n\n    # HAVING\n    having = dispatch.resolve_opt(stmt.having_clause, ctx=ctx)\n\n    # SELECT projection\n    table = context.Table()\n    target_list: list[pgast.ResTarget] = []\n    names: set[str] = set()\n    for t in stmt.target_list:\n        targets, columns = expr.resolve_ResTarget(\n            t, existing_names=names, ctx=ctx\n        )\n\n        target_list.extend(targets)\n        table.columns.extend(columns)\n        names.update(c.name for c in columns)\n\n    distinct_clause = None\n    if stmt.distinct_clause:\n        distinct_clause = [\n            (c if isinstance(c, pgast.Star) else dispatch.resolve(c, ctx=ctx))\n            for c in stmt.distinct_clause\n        ]\n\n    # order by can refer to columns in SELECT projection, so we need to add\n    # table.columns into scope\n    projected_table = context.Table(\n        columns=[\n            context.Column(\n                name=c.name,\n                kind=context.ColumnByName(reference_as=c.name),\n            )\n            for c, target in zip(table.columns, stmt.target_list)\n            if target.name\n            and (\n                not isinstance(target.val, pgast.ColumnRef)\n                or target.val.name[-1] != target.name\n            )\n        ]\n    )\n    if len(projected_table.columns) > 0:\n        ctx.scope.tables.append(projected_table)\n\n    sort_clause = dispatch.resolve_opt_list(stmt.sort_clause, ctx=ctx)\n    limit_offset = dispatch.resolve_opt(stmt.limit_offset, ctx=ctx)\n    limit_count = dispatch.resolve_opt(stmt.limit_count, ctx=ctx)\n    locking_clause = dispatch.resolve_opt_list(stmt.locking_clause, ctx=ctx)\n\n    ctes.extend(extract_ctes_from_ctx(ctx))\n\n    res = pgast.SelectStmt(\n        distinct_clause=distinct_clause,\n        from_clause=from_clause,\n        target_list=target_list,\n        group_clause=group_clause,\n        having_clause=having,\n        where_clause=where,\n        sort_clause=sort_clause,\n        limit_offset=limit_offset,\n        limit_count=limit_count,\n        locking_clause=locking_clause,\n        ctes=ctes if len(ctes) > 0 else None,\n    )\n    return (\n        res,\n        table,\n    )\n\n\n# If current context is top-level, return additional CTEs that need to be\n# injected. They were probably generated by DML.\ndef extract_ctes_from_ctx(\n    ctx: context.ResolverContextLevel,\n) -> list[pgast.CommonTableExpr]:\n    if ctx.subquery_depth != 0:\n        return []\n\n    res = list(ctx.ctes_buffer)\n    ctx.ctes_buffer.clear()\n    return res\n\n\ndef register_projections(target_list: list[pgast.ResTarget], *, ctx: Context):\n    # add aliases from target_list into scope\n\n    table = context.Table()\n\n    for target in target_list:\n        if not target.name:\n            continue\n\n        table.columns.append(\n            context.Column(\n                name=target.name,\n                kind=context.ColumnByName(reference_as=target.name),\n            )\n        )\n    ctx.scope.tables.append(table)\n\n\nPG_TOAST_TABLE: list[\n    tuple[sql_introspection.ColumnName, sql_introspection.ColumnType, int]\n] = [\n    ('chunk_id', None, 13),\n    ('chunk_seq', None, 13),\n    ('chunk_data', None, 13),\n]\n\n\n@dispatch._resolve_relation.register\ndef resolve_relation(\n    relation: pgast.Relation, *, include_inherited: bool, ctx: Context\n) -> tuple[pgast.BaseRelation, context.Table]:\n    assert relation.name\n    rel: pgast.BaseRelation\n\n    if relation.catalogname and relation.catalogname != 'postgres':\n        raise errors.QueryError(\n            f'queries cross databases are not supported',\n            span=relation.span,\n        )\n\n    # try information_schema, pg_catalog and pg_toast\n    preset_tables = None\n    if relation.schemaname == 'information_schema':\n        preset_tables = (\n            sql_introspection.INFORMATION_SCHEMA,\n            pgcommon.versioned_schema('edgedbsql'),\n        )\n    elif not relation.schemaname or relation.schemaname == 'pg_catalog':\n        preset_tables = (\n            sql_introspection.PG_CATALOG,\n            pgcommon.versioned_schema('edgedbsql'),\n        )\n    elif relation.schemaname == 'pg_toast':\n        preset_tables = ({relation.name: PG_TOAST_TABLE}, 'pg_toast')\n\n    if preset_tables and relation.name in preset_tables[0]:\n        cols = [\n            context.Column(name=n, kind=context.ColumnByName(reference_as=n))\n            for n, _type, _ver_since in preset_tables[0][relation.name]\n        ]\n        cols.extend(_construct_system_columns())\n        table = context.Table(\n            name=relation.name, columns=cols, is_direct_relation=True\n        )\n        rel = pgast.Relation(name=relation.name, schemaname=preset_tables[1])\n\n        return rel, table\n\n    schema_name = relation.schemaname\n\n    # try a CTE\n    if not schema_name or schema_name == 'public':\n        cte = next((t for t in ctx.scope.ctes if t.name == relation.name), None)\n        if cte:\n            table = context.Table(name=cte.name, columns=cte.columns.copy())\n            return pgast.Relation(name=cte.name, schemaname=None), table\n\n    def public_to_default(s: str) -> str:\n        # make sure to match `public`, `public::blah`, but not `public_blah`\n        if s == 'public':\n            return 'default'\n        if s.startswith('public::'):\n            return 'default' + s[6:]\n        return s\n\n    # lookup the object in schema\n    schemas = [schema_name] if schema_name else ctx.options.search_path\n    modules = [public_to_default(s) for s in schemas]\n\n    obj: Optional[s_sources.Source | s_properties.Property] = None\n    for module in modules:\n        if obj:\n            break\n\n        object_name = sn.QualName(module, relation.name)\n        obj = ctx.schema.get(  # type: ignore\n            object_name,\n            None,\n            module_aliases={None: 'default'},\n            type=s_objtypes.ObjectType,\n        )\n\n    # try pointer table\n    for module in modules:\n        if obj:\n            break\n        obj = _lookup_pointer_table(module, relation.name, ctx)\n\n    if not obj:\n        rel_name = pgcodegen.generate_source(relation)\n        raise errors.QueryError(\n            f'unknown table `{rel_name}`',\n            span=relation.span,\n            pgext_code=pgerror.ERROR_UNDEFINED_TABLE,\n        )\n\n    # extract table name\n    table = context.Table(schema_id=obj.id, name=relation.name)\n\n    # extract table columns\n    # when changing this, make sure to update sql information_schema\n    columns: list[context.Column] = []\n\n    if isinstance(obj, s_sources.Source):\n        pointers = obj.get_pointers(ctx.schema).objects(ctx.schema)\n\n        for p in pointers:\n            card = p.get_cardinality(ctx.schema)\n            if card.is_multi():\n                continue\n\n            columns.append(_construct_column(p, ctx))\n    else:\n        for c in ['source', 'target']:\n            columns.append(\n                context.Column(\n                    name=c, kind=context.ColumnByName(reference_as=c)\n                )\n            )\n\n    def column_order_key(c: context.Column) -> tuple[int, str]:\n        spec = {'id': 0, 'source': 0, 'target': 1}\n        order: int\n        if isinstance(c.kind, context.ColumnByName):\n            order = spec.get(c.kind.reference_as, 2)\n        else:\n            order = 2\n        return (order, c.name or '')\n\n    # sort by name but put `id` first\n    columns.sort(key=column_order_key)\n    table.columns.extend(columns)\n\n    table.columns.extend(_construct_system_columns())\n\n    if ctx.options.apply_access_policies and _has_access_policies(obj, ctx):\n        if isinstance(obj, s_objtypes.ObjectType):\n            rel = _compile_read_of_obj_table(obj, include_inherited, table, ctx)\n        else:\n            assert isinstance(obj, (s_links.Link | s_pointers.Pointer))\n            rel = _compile_read_of_link_table(\n                obj, include_inherited, table, ctx\n            )\n    else:\n        if include_inherited and _has_sub_types(obj, ctx):\n            rel = _relation_of_inheritance_cte(obj, ctx)\n        else:\n            rel = _relation_of_table(obj, table, ctx)\n\n    return rel, table\n\n\ndef _has_access_policies(\n    obj: s_sources.Source | s_properties.Property, ctx: Context\n):\n    if isinstance(obj, s_pointers.Pointer):\n        source = obj.get_source(ctx.schema)\n        assert isinstance(source, (s_objtypes.ObjectType, s_links.Link))\n        if isinstance(source, s_objtypes.ObjectType):\n            obj = source\n        elif isinstance(source, s_links.Link):\n            source = source.get_source(ctx.schema)\n            assert isinstance(source, s_objtypes.ObjectType)\n            obj = source\n        else:\n            return False\n    assert isinstance(obj, s_objtypes.ObjectType)\n\n    policies = obj.get_access_policies(ctx.schema)\n    return len(policies) > 0\n\n\ndef _has_sub_types(obj: s_sources.Source | s_properties.Property, ctx: Context):\n    return len(obj.children(ctx.schema)) > 0\n\n\ndef _relation_of_table(\n    obj: s_sources.Source | s_properties.Property,\n    table: context.Table,\n    ctx: Context,\n) -> pgast.Relation:\n    schemaname, dbname = pgcommon.get_backend_name(\n        ctx.schema, obj, aspect='table', catenate=False\n    )\n    relation = pgast.Relation(name=dbname, schemaname=schemaname)\n\n    table.is_direct_relation = True\n    # When referencing actual tables, we need to statically provide __type__,\n    # since this column does not exist in the database.\n    for col in table.columns:\n        if col.name == '__type__':\n            col.kind = context.ColumnStaticVal(val=obj.id)\n            break\n\n    return relation\n\n\ndef _relation_of_inheritance_cte(\n    obj: s_sources.Source | s_properties.Property, ctx: Context\n) -> pgast.Relation:\n    if obj not in ctx.inheritance_ctes:\n        cte = pgast.CommonTableExpr(\n            name=ctx.alias_generator.get('inh'),\n            query=pginheritance.get_inheritance_view(ctx.schema, obj),\n        )\n        ctx.ctes_buffer.append(cte)\n        ctx.inheritance_ctes[obj] = cte.name\n    return pgast.Relation(name=ctx.inheritance_ctes[obj])\n\n\ndef _lookup_pointer_table(\n    module: str, name: str, ctx: Context\n) -> Optional[s_links.Link | s_properties.Property]:\n    # Pointer tables are either:\n    # - multi link tables\n    # - single link tables with at least one property besides source and target\n    # - multi property tables\n\n    if '.' not in name:\n        return None\n    object_name, link_name = name.split('.')\n    object_name_qual = sn.QualName(module, object_name)\n\n    parent: s_objtypes.ObjectType = ctx.schema.get(  # type: ignore\n        object_name_qual,\n        None,\n        module_aliases={None: 'default'},\n        type=s_objtypes.ObjectType,\n    )\n    if not parent:\n        return None\n\n    pointer = parent.maybe_get_ptr(\n        ctx.schema, sn.UnqualName.from_string(link_name)\n    )\n\n    if not pointer:\n        return None\n    if pointer.get_computable(ctx.schema) or pointer.get_internal(ctx.schema):\n        return None\n\n    match pointer:\n        case s_links.Link():\n            if pointer.get_cardinality(ctx.schema).is_single():\n                # single links only for tables with at least one property\n                # besides source and target\n                l_pointers = pointer.get_pointers(ctx.schema).objects(\n                    ctx.schema\n                )\n                if len(l_pointers) <= 2:\n                    return None\n\n            return pointer\n\n        case s_properties.Property():\n            if pointer.get_cardinality(ctx.schema).is_single():\n                return None\n            return pointer\n\n    raise NotImplementedError()\n\n\ndef _construct_column(p: s_pointers.Pointer, ctx: Context) -> context.Column:\n    short_name = p.get_shortname(ctx.schema)\n\n    col_name: str\n    kind: context.ColumnKind\n\n    if isinstance(p, s_properties.Property):\n        col_name = short_name.name\n\n        if p.get_computable(ctx.schema):\n            kind = context.ColumnComputable(pointer=p)\n        elif p.is_link_source_property(ctx.schema):\n            kind = context.ColumnByName(reference_as='source')\n        elif p.is_link_target_property(ctx.schema):\n            kind = context.ColumnByName(reference_as='target')\n        elif p.is_id_pointer(ctx.schema):\n            kind = context.ColumnByName(reference_as='id')\n        else:\n            _, dbname = pgcommon.get_backend_name(ctx.schema, p, catenate=False)\n            kind = context.ColumnByName(reference_as=dbname)\n\n    elif isinstance(p, s_links.Link):\n        if p.get_computable(ctx.schema):\n            col_name = short_name.name + '_id'\n            kind = context.ColumnComputable(pointer=p)\n        elif short_name.name == '__type__':\n            col_name = '__type__'\n            kind = context.ColumnByName(reference_as='__type__')\n        else:\n            col_name = short_name.name + '_id'\n            _, dbname = pgcommon.get_backend_name(ctx.schema, p, catenate=False)\n            kind = context.ColumnByName(reference_as=dbname)\n\n    return context.Column(name=col_name, kind=kind)\n\n\ndef _construct_system_columns() -> list[context.Column]:\n    return [\n        context.Column(\n            name=c, kind=context.ColumnByName(reference_as=c), hidden=True\n        )\n        for c in ['tableoid', 'xmin', 'cmin', 'xmax', 'cmax', 'ctid']\n    ]\n\n\ndef _compile_read_of_obj_table(\n    obj: s_objtypes.ObjectType | s_links.Link,\n    include_inherited: bool,\n    table: context.Table,\n    ctx: Context,\n) -> pgast.Relation:\n    from edb.edgeql import ast as qlast\n    from edb.edgeql import compiler as qlcompiler\n    from edb.ir import ast as irast\n    from edb.pgsql import compiler as pgcompiler\n    from edb.pgsql.compiler import enums as pgce\n\n    obj_name: sn.QualName = obj.get_name(ctx.schema)\n    assert obj_name\n    ql_stmt = qlast.SelectQuery(\n        result=qlast.Path(\n            steps=[qlast.ObjectRef(module=obj_name.module, name=obj_name.name)]\n        )\n    )\n\n    if not include_inherited:\n        ql_stmt.where = qlast.BinOp(\n            left=qlast.Path(\n                partial=True,\n                steps=[qlast.Ptr(name='__type__'), qlast.Ptr(name='id')],\n            ),\n            op='=',\n            right=qlast.TypeCast(\n                expr=qlast.Constant.string(str(obj.id)),\n                type=qlast.TypeName(maintype=qlast.ObjectRef(name='uuid')),\n            ),\n        )\n\n    ir_stmt = qlcompiler.compile_ast_to_ir(\n        ql_stmt,\n        ctx.schema,\n        options=qlcompiler.CompilerOptions(apply_user_access_policies=True),\n    )\n\n    sql_tree = pgcompiler.compile_ir_to_sql_tree(\n        ir_stmt,\n        output_format=pgcompiler.OutputFormat.NATIVE_INTERNAL,\n        alias_generator=ctx.alias_generator,\n    )\n    command.merge_params(sql_tree, ir_stmt, ctx)\n\n    # add CTEs to resolver's CTE buffer\n    assert isinstance(sql_tree.ast, pgast.Query)\n    if sql_tree.ast.ctes:\n        ctx.ctes_buffer.extend(sql_tree.ast.ctes)\n        sql_tree.ast.ctes.clear()\n\n    SYSTEM_COLS = {'tableoid', 'xmin', 'cmin', 'xmax', 'cmax', 'ctid'}\n\n    # pull all expected columns out of the result rvar\n    obj_id: irast.PathId\n    if isinstance(obj, s_objtypes.ObjectType):\n        obj_id = irast.PathId.from_type(ctx.schema, obj, env=None)\n    else:\n        obj_id = irast.PathId.from_pointer(ctx.schema, obj, env=None)\n    for column in table.columns:\n        if not isinstance(column.kind, context.ColumnByName):\n            continue\n\n        if column.kind.reference_as in {'id', 'source', 'target', '__type__'}:\n            ptr = obj.getptr(\n                ctx.schema, sn.UnqualName.from_string(column.kind.reference_as)\n            )\n            ptr_id = irast.PathId.from_pointer(ctx.schema, ptr, env=None)\n        elif column.kind.reference_as in SYSTEM_COLS:\n            el_name = sn.QualName('__object__', column.kind.reference_as)\n            ptr_ref = irast.SpecialPointerRef(\n                name=el_name,\n                shortname=el_name,\n                out_source=obj_id.target,\n                out_target=pgtypes.pg_oid_typeref,\n                out_cardinality=qltypes.Cardinality.AT_MOST_ONE,\n            )\n            ptr_id = obj_id.extend(ptrref=ptr_ref)\n        else:\n            ptr = ctx.schema.get_by_id(\n                uuid.UUID(column.kind.reference_as), type=s_pointers.Pointer\n            )\n            ptr_id = irast.PathId.from_pointer(ctx.schema, ptr, env=None)\n\n        output = pgcompiler.pathctx.get_path_output(\n            sql_tree.ast,\n            ptr_id,\n            aspect=pgce.PathAspect.VALUE,\n            env=sql_tree.env,\n        )\n        assert isinstance(output, pgast.ColumnRef)\n        assert isinstance(output.name[-1], str)\n\n        # override how this column will be referenced as\n        column.kind.reference_as = output.name[-1]\n\n    cte_name = ctx.alias_generator.get('tbl')\n    ctx.ctes_buffer.append(\n        pgast.CommonTableExpr(\n            name=cte_name,\n            query=sql_tree.ast,\n        )\n    )\n    return pgast.Relation(name=cte_name)\n\n\ndef _compile_read_of_link_table(\n    obj: s_links.Link | s_properties.Property,\n    include_inherited: bool,\n    table: context.Table,\n    ctx: Context,\n) -> pgast.BaseRelation:\n    # get CTE that will provide source relation, with access policies applied\n    source = obj.get_source(ctx.schema)\n    assert isinstance(source, (s_objtypes.ObjectType, s_links.Link))\n    source_table = context.Table(\n        schema_id=source.id,\n        columns=[\n            context.Column(\n                name=\"id\", kind=context.ColumnByName(reference_as=\"id\")\n            )\n        ],\n    )\n    source_rel = _compile_read_of_obj_table(\n        source, include_inherited, source_table, ctx\n    )\n    source_table_id = source_table.columns[0].kind\n    assert isinstance(source_table_id, context.ColumnByName)\n\n    # get name of link table (with inheritance)\n    if obj not in ctx.inheritance_ctes:\n        cte = pgast.CommonTableExpr(\n            name=ctx.alias_generator.get('inh'),\n            query=pginheritance.get_inheritance_view(ctx.schema, obj),\n        )\n        ctx.ctes_buffer.append(cte)\n        ctx.inheritance_ctes[obj] = cte.name\n    link_table_name = ctx.inheritance_ctes[obj]\n\n    # inner join source table with the link table\n    target_list = []\n    for c in table.columns:\n        if not isinstance(c.kind, context.ColumnByName):\n            continue\n        target_list.append(\n            pgast.ResTarget(\n                val=pgast.ColumnRef(name=(\"l\", c.kind.reference_as))\n            )\n        )\n\n    return pgast.SelectStmt(\n        from_clause=[\n            pgast.JoinExpr(\n                larg=pgast.RelRangeVar(\n                    relation=source_rel, alias=pgast.Alias(aliasname=\"s\")\n                ),\n                joins=[\n                    pgast.JoinClause(\n                        type=\"INNER\",\n                        rarg=pgast.RelRangeVar(\n                            relation=pgast.Relation(name=link_table_name),\n                            alias=pgast.Alias(aliasname=\"l\"),\n                        ),\n                        quals=pgast.Expr(\n                            name=\"=\",\n                            lexpr=pgast.ColumnRef(\n                                name=(\"s\", source_table_id.reference_as)\n                            ),\n                            rexpr=pgast.ColumnRef(name=(\"l\", \"source\")),\n                        ),\n                    ),\n                ],\n            ),\n        ],\n        target_list=target_list,\n    )\n"
  },
  {
    "path": "edb/pgsql/resolver/sql_introspection.py",
    "content": "# AUTOGENERATED FROM _localdev postgres instance WITH\n#    $ edb gen-sql-introspection\n\n\"\"\"Declarations of information schema and pg_catalog\"\"\"\n\n\nColumnName = str\nColumnType = str | None\n\nINFORMATION_SCHEMA: dict[str, list[tuple[ColumnName, ColumnType, int]]] = {\n    \"administrable_role_authorizations\": [\n        (\"grantee\", \"sql_identifier\", 13),\n        (\"role_name\", \"sql_identifier\", 13),\n        (\"is_grantable\", \"yes_or_no\", 13),\n    ],\n    \"applicable_roles\": [\n        (\"grantee\", \"sql_identifier\", 13),\n        (\"role_name\", \"sql_identifier\", 13),\n        (\"is_grantable\", \"yes_or_no\", 13),\n    ],\n    \"attributes\": [\n        (\"udt_catalog\", \"sql_identifier\", 13),\n        (\"udt_schema\", \"sql_identifier\", 13),\n        (\"udt_name\", \"sql_identifier\", 13),\n        (\"attribute_name\", \"sql_identifier\", 13),\n        (\"ordinal_position\", \"cardinal_number\", 13),\n        (\"attribute_default\", \"character_data\", 13),\n        (\"is_nullable\", \"yes_or_no\", 13),\n        (\"data_type\", \"character_data\", 13),\n        (\"character_maximum_length\", \"cardinal_number\", 13),\n        (\"character_octet_length\", \"cardinal_number\", 13),\n        (\"character_set_catalog\", \"sql_identifier\", 13),\n        (\"character_set_schema\", \"sql_identifier\", 13),\n        (\"character_set_name\", \"sql_identifier\", 13),\n        (\"collation_catalog\", \"sql_identifier\", 13),\n        (\"collation_schema\", \"sql_identifier\", 13),\n        (\"collation_name\", \"sql_identifier\", 13),\n        (\"numeric_precision\", \"cardinal_number\", 13),\n        (\"numeric_precision_radix\", \"cardinal_number\", 13),\n        (\"numeric_scale\", \"cardinal_number\", 13),\n        (\"datetime_precision\", \"cardinal_number\", 13),\n        (\"interval_type\", \"character_data\", 13),\n        (\"interval_precision\", \"cardinal_number\", 13),\n        (\"attribute_udt_catalog\", \"sql_identifier\", 13),\n        (\"attribute_udt_schema\", \"sql_identifier\", 13),\n        (\"attribute_udt_name\", \"sql_identifier\", 13),\n        (\"scope_catalog\", \"sql_identifier\", 13),\n        (\"scope_schema\", \"sql_identifier\", 13),\n        (\"scope_name\", \"sql_identifier\", 13),\n        (\"maximum_cardinality\", \"cardinal_number\", 13),\n        (\"dtd_identifier\", \"sql_identifier\", 13),\n        (\"is_derived_reference_attribute\", \"yes_or_no\", 13),\n    ],\n    \"character_sets\": [\n        (\"character_set_catalog\", \"sql_identifier\", 13),\n        (\"character_set_schema\", \"sql_identifier\", 13),\n        (\"character_set_name\", \"sql_identifier\", 13),\n        (\"character_repertoire\", \"sql_identifier\", 13),\n        (\"form_of_use\", \"sql_identifier\", 13),\n        (\"default_collate_catalog\", \"sql_identifier\", 13),\n        (\"default_collate_schema\", \"sql_identifier\", 13),\n        (\"default_collate_name\", \"sql_identifier\", 13),\n    ],\n    \"check_constraint_routine_usage\": [\n        (\"constraint_catalog\", \"sql_identifier\", 13),\n        (\"constraint_schema\", \"sql_identifier\", 13),\n        (\"constraint_name\", \"sql_identifier\", 13),\n        (\"specific_catalog\", \"sql_identifier\", 13),\n        (\"specific_schema\", \"sql_identifier\", 13),\n        (\"specific_name\", \"sql_identifier\", 13),\n    ],\n    \"check_constraints\": [\n        (\"constraint_catalog\", \"sql_identifier\", 13),\n        (\"constraint_schema\", \"sql_identifier\", 13),\n        (\"constraint_name\", \"sql_identifier\", 13),\n        (\"check_clause\", \"character_data\", 13),\n    ],\n    \"collation_character_set_applicability\": [\n        (\"collation_catalog\", \"sql_identifier\", 13),\n        (\"collation_schema\", \"sql_identifier\", 13),\n        (\"collation_name\", \"sql_identifier\", 13),\n        (\"character_set_catalog\", \"sql_identifier\", 13),\n        (\"character_set_schema\", \"sql_identifier\", 13),\n        (\"character_set_name\", \"sql_identifier\", 13),\n    ],\n    \"collations\": [\n        (\"collation_catalog\", \"sql_identifier\", 13),\n        (\"collation_schema\", \"sql_identifier\", 13),\n        (\"collation_name\", \"sql_identifier\", 13),\n        (\"pad_attribute\", \"character_data\", 13),\n    ],\n    \"column_column_usage\": [\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"column_name\", \"sql_identifier\", 13),\n        (\"dependent_column\", \"sql_identifier\", 13),\n    ],\n    \"column_domain_usage\": [\n        (\"domain_catalog\", \"sql_identifier\", 13),\n        (\"domain_schema\", \"sql_identifier\", 13),\n        (\"domain_name\", \"sql_identifier\", 13),\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"column_name\", \"sql_identifier\", 13),\n    ],\n    \"column_options\": [\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"column_name\", \"sql_identifier\", 13),\n        (\"option_name\", \"sql_identifier\", 13),\n        (\"option_value\", \"character_data\", 13),\n    ],\n    \"column_privileges\": [\n        (\"grantor\", \"sql_identifier\", 13),\n        (\"grantee\", \"sql_identifier\", 13),\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"column_name\", \"sql_identifier\", 13),\n        (\"privilege_type\", \"character_data\", 13),\n        (\"is_grantable\", \"yes_or_no\", 13),\n    ],\n    \"column_udt_usage\": [\n        (\"udt_catalog\", \"sql_identifier\", 13),\n        (\"udt_schema\", \"sql_identifier\", 13),\n        (\"udt_name\", \"sql_identifier\", 13),\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"column_name\", \"sql_identifier\", 13),\n    ],\n    \"columns\": [\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"column_name\", \"sql_identifier\", 13),\n        (\"ordinal_position\", \"cardinal_number\", 13),\n        (\"column_default\", \"character_data\", 13),\n        (\"is_nullable\", \"yes_or_no\", 13),\n        (\"data_type\", \"character_data\", 13),\n        (\"character_maximum_length\", \"cardinal_number\", 13),\n        (\"character_octet_length\", \"cardinal_number\", 13),\n        (\"numeric_precision\", \"cardinal_number\", 13),\n        (\"numeric_precision_radix\", \"cardinal_number\", 13),\n        (\"numeric_scale\", \"cardinal_number\", 13),\n        (\"datetime_precision\", \"cardinal_number\", 13),\n        (\"interval_type\", \"character_data\", 13),\n        (\"interval_precision\", \"cardinal_number\", 13),\n        (\"character_set_catalog\", \"sql_identifier\", 13),\n        (\"character_set_schema\", \"sql_identifier\", 13),\n        (\"character_set_name\", \"sql_identifier\", 13),\n        (\"collation_catalog\", \"sql_identifier\", 13),\n        (\"collation_schema\", \"sql_identifier\", 13),\n        (\"collation_name\", \"sql_identifier\", 13),\n        (\"domain_catalog\", \"sql_identifier\", 13),\n        (\"domain_schema\", \"sql_identifier\", 13),\n        (\"domain_name\", \"sql_identifier\", 13),\n        (\"udt_catalog\", \"sql_identifier\", 13),\n        (\"udt_schema\", \"sql_identifier\", 13),\n        (\"udt_name\", \"sql_identifier\", 13),\n        (\"scope_catalog\", \"sql_identifier\", 13),\n        (\"scope_schema\", \"sql_identifier\", 13),\n        (\"scope_name\", \"sql_identifier\", 13),\n        (\"maximum_cardinality\", \"cardinal_number\", 13),\n        (\"dtd_identifier\", \"sql_identifier\", 13),\n        (\"is_self_referencing\", \"yes_or_no\", 13),\n        (\"is_identity\", \"yes_or_no\", 13),\n        (\"identity_generation\", \"character_data\", 13),\n        (\"identity_start\", \"character_data\", 13),\n        (\"identity_increment\", \"character_data\", 13),\n        (\"identity_maximum\", \"character_data\", 13),\n        (\"identity_minimum\", \"character_data\", 13),\n        (\"identity_cycle\", \"yes_or_no\", 13),\n        (\"is_generated\", \"character_data\", 13),\n        (\"generation_expression\", \"character_data\", 13),\n        (\"is_updatable\", \"yes_or_no\", 13),\n    ],\n    \"constraint_column_usage\": [\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"column_name\", \"sql_identifier\", 13),\n        (\"constraint_catalog\", \"sql_identifier\", 13),\n        (\"constraint_schema\", \"sql_identifier\", 13),\n        (\"constraint_name\", \"sql_identifier\", 13),\n    ],\n    \"constraint_table_usage\": [\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"constraint_catalog\", \"sql_identifier\", 13),\n        (\"constraint_schema\", \"sql_identifier\", 13),\n        (\"constraint_name\", \"sql_identifier\", 13),\n    ],\n    \"data_type_privileges\": [\n        (\"object_catalog\", \"sql_identifier\", 13),\n        (\"object_schema\", \"sql_identifier\", 13),\n        (\"object_name\", \"sql_identifier\", 13),\n        (\"object_type\", \"character_data\", 13),\n        (\"dtd_identifier\", \"sql_identifier\", 13),\n    ],\n    \"domain_constraints\": [\n        (\"constraint_catalog\", \"sql_identifier\", 13),\n        (\"constraint_schema\", \"sql_identifier\", 13),\n        (\"constraint_name\", \"sql_identifier\", 13),\n        (\"domain_catalog\", \"sql_identifier\", 13),\n        (\"domain_schema\", \"sql_identifier\", 13),\n        (\"domain_name\", \"sql_identifier\", 13),\n        (\"is_deferrable\", \"yes_or_no\", 13),\n        (\"initially_deferred\", \"yes_or_no\", 13),\n    ],\n    \"domain_udt_usage\": [\n        (\"udt_catalog\", \"sql_identifier\", 13),\n        (\"udt_schema\", \"sql_identifier\", 13),\n        (\"udt_name\", \"sql_identifier\", 13),\n        (\"domain_catalog\", \"sql_identifier\", 13),\n        (\"domain_schema\", \"sql_identifier\", 13),\n        (\"domain_name\", \"sql_identifier\", 13),\n    ],\n    \"domains\": [\n        (\"domain_catalog\", \"sql_identifier\", 13),\n        (\"domain_schema\", \"sql_identifier\", 13),\n        (\"domain_name\", \"sql_identifier\", 13),\n        (\"data_type\", \"character_data\", 13),\n        (\"character_maximum_length\", \"cardinal_number\", 13),\n        (\"character_octet_length\", \"cardinal_number\", 13),\n        (\"character_set_catalog\", \"sql_identifier\", 13),\n        (\"character_set_schema\", \"sql_identifier\", 13),\n        (\"character_set_name\", \"sql_identifier\", 13),\n        (\"collation_catalog\", \"sql_identifier\", 13),\n        (\"collation_schema\", \"sql_identifier\", 13),\n        (\"collation_name\", \"sql_identifier\", 13),\n        (\"numeric_precision\", \"cardinal_number\", 13),\n        (\"numeric_precision_radix\", \"cardinal_number\", 13),\n        (\"numeric_scale\", \"cardinal_number\", 13),\n        (\"datetime_precision\", \"cardinal_number\", 13),\n        (\"interval_type\", \"character_data\", 13),\n        (\"interval_precision\", \"cardinal_number\", 13),\n        (\"domain_default\", \"character_data\", 13),\n        (\"udt_catalog\", \"sql_identifier\", 13),\n        (\"udt_schema\", \"sql_identifier\", 13),\n        (\"udt_name\", \"sql_identifier\", 13),\n        (\"scope_catalog\", \"sql_identifier\", 13),\n        (\"scope_schema\", \"sql_identifier\", 13),\n        (\"scope_name\", \"sql_identifier\", 13),\n        (\"maximum_cardinality\", \"cardinal_number\", 13),\n        (\"dtd_identifier\", \"sql_identifier\", 13),\n    ],\n    \"element_types\": [\n        (\"object_catalog\", \"sql_identifier\", 13),\n        (\"object_schema\", \"sql_identifier\", 13),\n        (\"object_name\", \"sql_identifier\", 13),\n        (\"object_type\", \"character_data\", 13),\n        (\"collection_type_identifier\", \"sql_identifier\", 13),\n        (\"data_type\", \"character_data\", 13),\n        (\"character_maximum_length\", \"cardinal_number\", 13),\n        (\"character_octet_length\", \"cardinal_number\", 13),\n        (\"character_set_catalog\", \"sql_identifier\", 13),\n        (\"character_set_schema\", \"sql_identifier\", 13),\n        (\"character_set_name\", \"sql_identifier\", 13),\n        (\"collation_catalog\", \"sql_identifier\", 13),\n        (\"collation_schema\", \"sql_identifier\", 13),\n        (\"collation_name\", \"sql_identifier\", 13),\n        (\"numeric_precision\", \"cardinal_number\", 13),\n        (\"numeric_precision_radix\", \"cardinal_number\", 13),\n        (\"numeric_scale\", \"cardinal_number\", 13),\n        (\"datetime_precision\", \"cardinal_number\", 13),\n        (\"interval_type\", \"character_data\", 13),\n        (\"interval_precision\", \"cardinal_number\", 13),\n        (\"udt_catalog\", \"sql_identifier\", 13),\n        (\"udt_schema\", \"sql_identifier\", 13),\n        (\"udt_name\", \"sql_identifier\", 13),\n        (\"scope_catalog\", \"sql_identifier\", 13),\n        (\"scope_schema\", \"sql_identifier\", 13),\n        (\"scope_name\", \"sql_identifier\", 13),\n        (\"maximum_cardinality\", \"cardinal_number\", 13),\n        (\"dtd_identifier\", \"sql_identifier\", 13),\n    ],\n    \"enabled_roles\": [\n        (\"role_name\", \"sql_identifier\", 13),\n    ],\n    \"foreign_data_wrapper_options\": [\n        (\"foreign_data_wrapper_catalog\", \"sql_identifier\", 13),\n        (\"foreign_data_wrapper_name\", \"sql_identifier\", 13),\n        (\"option_name\", \"sql_identifier\", 13),\n        (\"option_value\", \"character_data\", 13),\n    ],\n    \"foreign_data_wrappers\": [\n        (\"foreign_data_wrapper_catalog\", \"sql_identifier\", 13),\n        (\"foreign_data_wrapper_name\", \"sql_identifier\", 13),\n        (\"authorization_identifier\", \"sql_identifier\", 13),\n        (\"library_name\", \"character_data\", 13),\n        (\"foreign_data_wrapper_language\", \"character_data\", 13),\n    ],\n    \"foreign_server_options\": [\n        (\"foreign_server_catalog\", \"sql_identifier\", 13),\n        (\"foreign_server_name\", \"sql_identifier\", 13),\n        (\"option_name\", \"sql_identifier\", 13),\n        (\"option_value\", \"character_data\", 13),\n    ],\n    \"foreign_servers\": [\n        (\"foreign_server_catalog\", \"sql_identifier\", 13),\n        (\"foreign_server_name\", \"sql_identifier\", 13),\n        (\"foreign_data_wrapper_catalog\", \"sql_identifier\", 13),\n        (\"foreign_data_wrapper_name\", \"sql_identifier\", 13),\n        (\"foreign_server_type\", \"character_data\", 13),\n        (\"foreign_server_version\", \"character_data\", 13),\n        (\"authorization_identifier\", \"sql_identifier\", 13),\n    ],\n    \"foreign_table_options\": [\n        (\"foreign_table_catalog\", \"sql_identifier\", 13),\n        (\"foreign_table_schema\", \"sql_identifier\", 13),\n        (\"foreign_table_name\", \"sql_identifier\", 13),\n        (\"option_name\", \"sql_identifier\", 13),\n        (\"option_value\", \"character_data\", 13),\n    ],\n    \"foreign_tables\": [\n        (\"foreign_table_catalog\", \"sql_identifier\", 13),\n        (\"foreign_table_schema\", \"sql_identifier\", 13),\n        (\"foreign_table_name\", \"sql_identifier\", 13),\n        (\"foreign_server_catalog\", \"sql_identifier\", 13),\n        (\"foreign_server_name\", \"sql_identifier\", 13),\n    ],\n    \"information_schema_catalog_name\": [\n        (\"catalog_name\", \"sql_identifier\", 13),\n    ],\n    \"key_column_usage\": [\n        (\"constraint_catalog\", \"sql_identifier\", 13),\n        (\"constraint_schema\", \"sql_identifier\", 13),\n        (\"constraint_name\", \"sql_identifier\", 13),\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"column_name\", \"sql_identifier\", 13),\n        (\"ordinal_position\", \"cardinal_number\", 13),\n        (\"position_in_unique_constraint\", \"cardinal_number\", 13),\n    ],\n    \"parameters\": [\n        (\"specific_catalog\", \"sql_identifier\", 13),\n        (\"specific_schema\", \"sql_identifier\", 13),\n        (\"specific_name\", \"sql_identifier\", 13),\n        (\"ordinal_position\", \"cardinal_number\", 13),\n        (\"parameter_mode\", \"character_data\", 13),\n        (\"is_result\", \"yes_or_no\", 13),\n        (\"as_locator\", \"yes_or_no\", 13),\n        (\"parameter_name\", \"sql_identifier\", 13),\n        (\"data_type\", \"character_data\", 13),\n        (\"character_maximum_length\", \"cardinal_number\", 13),\n        (\"character_octet_length\", \"cardinal_number\", 13),\n        (\"character_set_catalog\", \"sql_identifier\", 13),\n        (\"character_set_schema\", \"sql_identifier\", 13),\n        (\"character_set_name\", \"sql_identifier\", 13),\n        (\"collation_catalog\", \"sql_identifier\", 13),\n        (\"collation_schema\", \"sql_identifier\", 13),\n        (\"collation_name\", \"sql_identifier\", 13),\n        (\"numeric_precision\", \"cardinal_number\", 13),\n        (\"numeric_precision_radix\", \"cardinal_number\", 13),\n        (\"numeric_scale\", \"cardinal_number\", 13),\n        (\"datetime_precision\", \"cardinal_number\", 13),\n        (\"interval_type\", \"character_data\", 13),\n        (\"interval_precision\", \"cardinal_number\", 13),\n        (\"udt_catalog\", \"sql_identifier\", 13),\n        (\"udt_schema\", \"sql_identifier\", 13),\n        (\"udt_name\", \"sql_identifier\", 13),\n        (\"scope_catalog\", \"sql_identifier\", 13),\n        (\"scope_schema\", \"sql_identifier\", 13),\n        (\"scope_name\", \"sql_identifier\", 13),\n        (\"maximum_cardinality\", \"cardinal_number\", 13),\n        (\"dtd_identifier\", \"sql_identifier\", 13),\n        (\"parameter_default\", \"character_data\", 13),\n    ],\n    \"referential_constraints\": [\n        (\"constraint_catalog\", \"sql_identifier\", 13),\n        (\"constraint_schema\", \"sql_identifier\", 13),\n        (\"constraint_name\", \"sql_identifier\", 13),\n        (\"unique_constraint_catalog\", \"sql_identifier\", 13),\n        (\"unique_constraint_schema\", \"sql_identifier\", 13),\n        (\"unique_constraint_name\", \"sql_identifier\", 13),\n        (\"match_option\", \"character_data\", 13),\n        (\"update_rule\", \"character_data\", 13),\n        (\"delete_rule\", \"character_data\", 13),\n    ],\n    \"role_column_grants\": [\n        (\"grantor\", \"sql_identifier\", 13),\n        (\"grantee\", \"sql_identifier\", 13),\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"column_name\", \"sql_identifier\", 13),\n        (\"privilege_type\", \"character_data\", 13),\n        (\"is_grantable\", \"yes_or_no\", 13),\n    ],\n    \"role_routine_grants\": [\n        (\"grantor\", \"sql_identifier\", 13),\n        (\"grantee\", \"sql_identifier\", 13),\n        (\"specific_catalog\", \"sql_identifier\", 13),\n        (\"specific_schema\", \"sql_identifier\", 13),\n        (\"specific_name\", \"sql_identifier\", 13),\n        (\"routine_catalog\", \"sql_identifier\", 13),\n        (\"routine_schema\", \"sql_identifier\", 13),\n        (\"routine_name\", \"sql_identifier\", 13),\n        (\"privilege_type\", \"character_data\", 13),\n        (\"is_grantable\", \"yes_or_no\", 13),\n    ],\n    \"role_table_grants\": [\n        (\"grantor\", \"sql_identifier\", 13),\n        (\"grantee\", \"sql_identifier\", 13),\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"privilege_type\", \"character_data\", 13),\n        (\"is_grantable\", \"yes_or_no\", 13),\n        (\"with_hierarchy\", \"yes_or_no\", 13),\n    ],\n    \"role_udt_grants\": [\n        (\"grantor\", \"sql_identifier\", 13),\n        (\"grantee\", \"sql_identifier\", 13),\n        (\"udt_catalog\", \"sql_identifier\", 13),\n        (\"udt_schema\", \"sql_identifier\", 13),\n        (\"udt_name\", \"sql_identifier\", 13),\n        (\"privilege_type\", \"character_data\", 13),\n        (\"is_grantable\", \"yes_or_no\", 13),\n    ],\n    \"role_usage_grants\": [\n        (\"grantor\", \"sql_identifier\", 13),\n        (\"grantee\", \"sql_identifier\", 13),\n        (\"object_catalog\", \"sql_identifier\", 13),\n        (\"object_schema\", \"sql_identifier\", 13),\n        (\"object_name\", \"sql_identifier\", 13),\n        (\"object_type\", \"character_data\", 13),\n        (\"privilege_type\", \"character_data\", 13),\n        (\"is_grantable\", \"yes_or_no\", 13),\n    ],\n    \"routine_column_usage\": [\n        (\"specific_catalog\", \"sql_identifier\", 14),\n        (\"specific_schema\", \"sql_identifier\", 14),\n        (\"specific_name\", \"sql_identifier\", 14),\n        (\"routine_catalog\", \"sql_identifier\", 14),\n        (\"routine_schema\", \"sql_identifier\", 14),\n        (\"routine_name\", \"sql_identifier\", 14),\n        (\"table_catalog\", \"sql_identifier\", 14),\n        (\"table_schema\", \"sql_identifier\", 14),\n        (\"table_name\", \"sql_identifier\", 14),\n        (\"column_name\", \"sql_identifier\", 14),\n    ],\n    \"routine_privileges\": [\n        (\"grantor\", \"sql_identifier\", 13),\n        (\"grantee\", \"sql_identifier\", 13),\n        (\"specific_catalog\", \"sql_identifier\", 13),\n        (\"specific_schema\", \"sql_identifier\", 13),\n        (\"specific_name\", \"sql_identifier\", 13),\n        (\"routine_catalog\", \"sql_identifier\", 13),\n        (\"routine_schema\", \"sql_identifier\", 13),\n        (\"routine_name\", \"sql_identifier\", 13),\n        (\"privilege_type\", \"character_data\", 13),\n        (\"is_grantable\", \"yes_or_no\", 13),\n    ],\n    \"routine_routine_usage\": [\n        (\"specific_catalog\", \"sql_identifier\", 14),\n        (\"specific_schema\", \"sql_identifier\", 14),\n        (\"specific_name\", \"sql_identifier\", 14),\n        (\"routine_catalog\", \"sql_identifier\", 14),\n        (\"routine_schema\", \"sql_identifier\", 14),\n        (\"routine_name\", \"sql_identifier\", 14),\n    ],\n    \"routine_sequence_usage\": [\n        (\"specific_catalog\", \"sql_identifier\", 14),\n        (\"specific_schema\", \"sql_identifier\", 14),\n        (\"specific_name\", \"sql_identifier\", 14),\n        (\"routine_catalog\", \"sql_identifier\", 14),\n        (\"routine_schema\", \"sql_identifier\", 14),\n        (\"routine_name\", \"sql_identifier\", 14),\n        (\"sequence_catalog\", \"sql_identifier\", 14),\n        (\"sequence_schema\", \"sql_identifier\", 14),\n        (\"sequence_name\", \"sql_identifier\", 14),\n    ],\n    \"routine_table_usage\": [\n        (\"specific_catalog\", \"sql_identifier\", 14),\n        (\"specific_schema\", \"sql_identifier\", 14),\n        (\"specific_name\", \"sql_identifier\", 14),\n        (\"routine_catalog\", \"sql_identifier\", 14),\n        (\"routine_schema\", \"sql_identifier\", 14),\n        (\"routine_name\", \"sql_identifier\", 14),\n        (\"table_catalog\", \"sql_identifier\", 14),\n        (\"table_schema\", \"sql_identifier\", 14),\n        (\"table_name\", \"sql_identifier\", 14),\n    ],\n    \"routines\": [\n        (\"specific_catalog\", \"sql_identifier\", 13),\n        (\"specific_schema\", \"sql_identifier\", 13),\n        (\"specific_name\", \"sql_identifier\", 13),\n        (\"routine_catalog\", \"sql_identifier\", 13),\n        (\"routine_schema\", \"sql_identifier\", 13),\n        (\"routine_name\", \"sql_identifier\", 13),\n        (\"routine_type\", \"character_data\", 13),\n        (\"module_catalog\", \"sql_identifier\", 13),\n        (\"module_schema\", \"sql_identifier\", 13),\n        (\"module_name\", \"sql_identifier\", 13),\n        (\"udt_catalog\", \"sql_identifier\", 13),\n        (\"udt_schema\", \"sql_identifier\", 13),\n        (\"udt_name\", \"sql_identifier\", 13),\n        (\"data_type\", \"character_data\", 13),\n        (\"character_maximum_length\", \"cardinal_number\", 13),\n        (\"character_octet_length\", \"cardinal_number\", 13),\n        (\"character_set_catalog\", \"sql_identifier\", 13),\n        (\"character_set_schema\", \"sql_identifier\", 13),\n        (\"character_set_name\", \"sql_identifier\", 13),\n        (\"collation_catalog\", \"sql_identifier\", 13),\n        (\"collation_schema\", \"sql_identifier\", 13),\n        (\"collation_name\", \"sql_identifier\", 13),\n        (\"numeric_precision\", \"cardinal_number\", 13),\n        (\"numeric_precision_radix\", \"cardinal_number\", 13),\n        (\"numeric_scale\", \"cardinal_number\", 13),\n        (\"datetime_precision\", \"cardinal_number\", 13),\n        (\"interval_type\", \"character_data\", 13),\n        (\"interval_precision\", \"cardinal_number\", 13),\n        (\"type_udt_catalog\", \"sql_identifier\", 13),\n        (\"type_udt_schema\", \"sql_identifier\", 13),\n        (\"type_udt_name\", \"sql_identifier\", 13),\n        (\"scope_catalog\", \"sql_identifier\", 13),\n        (\"scope_schema\", \"sql_identifier\", 13),\n        (\"scope_name\", \"sql_identifier\", 13),\n        (\"maximum_cardinality\", \"cardinal_number\", 13),\n        (\"dtd_identifier\", \"sql_identifier\", 13),\n        (\"routine_body\", \"character_data\", 13),\n        (\"routine_definition\", \"character_data\", 13),\n        (\"external_name\", \"character_data\", 13),\n        (\"external_language\", \"character_data\", 13),\n        (\"parameter_style\", \"character_data\", 13),\n        (\"is_deterministic\", \"yes_or_no\", 13),\n        (\"sql_data_access\", \"character_data\", 13),\n        (\"is_null_call\", \"yes_or_no\", 13),\n        (\"sql_path\", \"character_data\", 13),\n        (\"schema_level_routine\", \"yes_or_no\", 13),\n        (\"max_dynamic_result_sets\", \"cardinal_number\", 13),\n        (\"is_user_defined_cast\", \"yes_or_no\", 13),\n        (\"is_implicitly_invocable\", \"yes_or_no\", 13),\n        (\"security_type\", \"character_data\", 13),\n        (\"to_sql_specific_catalog\", \"sql_identifier\", 13),\n        (\"to_sql_specific_schema\", \"sql_identifier\", 13),\n        (\"to_sql_specific_name\", \"sql_identifier\", 13),\n        (\"as_locator\", \"yes_or_no\", 13),\n        (\"created\", \"time_stamp\", 13),\n        (\"last_altered\", \"time_stamp\", 13),\n        (\"new_savepoint_level\", \"yes_or_no\", 13),\n        (\"is_udt_dependent\", \"yes_or_no\", 13),\n        (\"result_cast_from_data_type\", \"character_data\", 13),\n        (\"result_cast_as_locator\", \"yes_or_no\", 13),\n        (\"result_cast_char_max_length\", \"cardinal_number\", 13),\n        (\"result_cast_char_octet_length\", \"cardinal_number\", 13),\n        (\"result_cast_char_set_catalog\", \"sql_identifier\", 13),\n        (\"result_cast_char_set_schema\", \"sql_identifier\", 13),\n        (\"result_cast_char_set_name\", \"sql_identifier\", 13),\n        (\"result_cast_collation_catalog\", \"sql_identifier\", 13),\n        (\"result_cast_collation_schema\", \"sql_identifier\", 13),\n        (\"result_cast_collation_name\", \"sql_identifier\", 13),\n        (\"result_cast_numeric_precision\", \"cardinal_number\", 13),\n        (\"result_cast_numeric_precision_radix\", \"cardinal_number\", 13),\n        (\"result_cast_numeric_scale\", \"cardinal_number\", 13),\n        (\"result_cast_datetime_precision\", \"cardinal_number\", 13),\n        (\"result_cast_interval_type\", \"character_data\", 13),\n        (\"result_cast_interval_precision\", \"cardinal_number\", 13),\n        (\"result_cast_type_udt_catalog\", \"sql_identifier\", 13),\n        (\"result_cast_type_udt_schema\", \"sql_identifier\", 13),\n        (\"result_cast_type_udt_name\", \"sql_identifier\", 13),\n        (\"result_cast_scope_catalog\", \"sql_identifier\", 13),\n        (\"result_cast_scope_schema\", \"sql_identifier\", 13),\n        (\"result_cast_scope_name\", \"sql_identifier\", 13),\n        (\"result_cast_maximum_cardinality\", \"cardinal_number\", 13),\n        (\"result_cast_dtd_identifier\", \"sql_identifier\", 13),\n    ],\n    \"schemata\": [\n        (\"catalog_name\", \"sql_identifier\", 13),\n        (\"schema_name\", \"sql_identifier\", 13),\n        (\"schema_owner\", \"sql_identifier\", 13),\n        (\"default_character_set_catalog\", \"sql_identifier\", 13),\n        (\"default_character_set_schema\", \"sql_identifier\", 13),\n        (\"default_character_set_name\", \"sql_identifier\", 13),\n        (\"sql_path\", \"character_data\", 13),\n    ],\n    \"sequences\": [\n        (\"sequence_catalog\", \"sql_identifier\", 13),\n        (\"sequence_schema\", \"sql_identifier\", 13),\n        (\"sequence_name\", \"sql_identifier\", 13),\n        (\"data_type\", \"character_data\", 13),\n        (\"numeric_precision\", \"cardinal_number\", 13),\n        (\"numeric_precision_radix\", \"cardinal_number\", 13),\n        (\"numeric_scale\", \"cardinal_number\", 13),\n        (\"start_value\", \"character_data\", 13),\n        (\"minimum_value\", \"character_data\", 13),\n        (\"maximum_value\", \"character_data\", 13),\n        (\"increment\", \"character_data\", 13),\n        (\"cycle_option\", \"yes_or_no\", 13),\n    ],\n    \"sql_features\": [\n        (\"feature_id\", \"character_data\", 13),\n        (\"feature_name\", \"character_data\", 13),\n        (\"sub_feature_id\", \"character_data\", 13),\n        (\"sub_feature_name\", \"character_data\", 13),\n        (\"is_supported\", \"yes_or_no\", 13),\n        (\"is_verified_by\", \"character_data\", 13),\n        (\"comments\", \"character_data\", 13),\n    ],\n    \"sql_implementation_info\": [\n        (\"implementation_info_id\", \"character_data\", 13),\n        (\"implementation_info_name\", \"character_data\", 13),\n        (\"integer_value\", \"cardinal_number\", 13),\n        (\"character_value\", \"character_data\", 13),\n        (\"comments\", \"character_data\", 13),\n    ],\n    \"sql_parts\": [\n        (\"feature_id\", \"character_data\", 13),\n        (\"feature_name\", \"character_data\", 13),\n        (\"is_supported\", \"yes_or_no\", 13),\n        (\"is_verified_by\", \"character_data\", 13),\n        (\"comments\", \"character_data\", 13),\n    ],\n    \"sql_sizing\": [\n        (\"sizing_id\", \"cardinal_number\", 13),\n        (\"sizing_name\", \"character_data\", 13),\n        (\"supported_value\", \"cardinal_number\", 13),\n        (\"comments\", \"character_data\", 13),\n    ],\n    \"table_constraints\": [\n        (\"constraint_catalog\", \"sql_identifier\", 13),\n        (\"constraint_schema\", \"sql_identifier\", 13),\n        (\"constraint_name\", \"sql_identifier\", 13),\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"constraint_type\", \"character_data\", 13),\n        (\"is_deferrable\", \"yes_or_no\", 13),\n        (\"initially_deferred\", \"yes_or_no\", 13),\n        (\"enforced\", \"yes_or_no\", 13),\n        (\"nulls_distinct\", \"yes_or_no\", 15),\n    ],\n    \"table_privileges\": [\n        (\"grantor\", \"sql_identifier\", 13),\n        (\"grantee\", \"sql_identifier\", 13),\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"privilege_type\", \"character_data\", 13),\n        (\"is_grantable\", \"yes_or_no\", 13),\n        (\"with_hierarchy\", \"yes_or_no\", 13),\n    ],\n    \"tables\": [\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"table_type\", \"character_data\", 13),\n        (\"self_referencing_column_name\", \"sql_identifier\", 13),\n        (\"reference_generation\", \"character_data\", 13),\n        (\"user_defined_type_catalog\", \"sql_identifier\", 13),\n        (\"user_defined_type_schema\", \"sql_identifier\", 13),\n        (\"user_defined_type_name\", \"sql_identifier\", 13),\n        (\"is_insertable_into\", \"yes_or_no\", 13),\n        (\"is_typed\", \"yes_or_no\", 13),\n        (\"commit_action\", \"character_data\", 13),\n    ],\n    \"transforms\": [\n        (\"udt_catalog\", \"sql_identifier\", 13),\n        (\"udt_schema\", \"sql_identifier\", 13),\n        (\"udt_name\", \"sql_identifier\", 13),\n        (\"specific_catalog\", \"sql_identifier\", 13),\n        (\"specific_schema\", \"sql_identifier\", 13),\n        (\"specific_name\", \"sql_identifier\", 13),\n        (\"group_name\", \"sql_identifier\", 13),\n        (\"transform_type\", \"character_data\", 13),\n    ],\n    \"triggered_update_columns\": [\n        (\"trigger_catalog\", \"sql_identifier\", 13),\n        (\"trigger_schema\", \"sql_identifier\", 13),\n        (\"trigger_name\", \"sql_identifier\", 13),\n        (\"event_object_catalog\", \"sql_identifier\", 13),\n        (\"event_object_schema\", \"sql_identifier\", 13),\n        (\"event_object_table\", \"sql_identifier\", 13),\n        (\"event_object_column\", \"sql_identifier\", 13),\n    ],\n    \"triggers\": [\n        (\"trigger_catalog\", \"sql_identifier\", 13),\n        (\"trigger_schema\", \"sql_identifier\", 13),\n        (\"trigger_name\", \"sql_identifier\", 13),\n        (\"event_manipulation\", \"character_data\", 13),\n        (\"event_object_catalog\", \"sql_identifier\", 13),\n        (\"event_object_schema\", \"sql_identifier\", 13),\n        (\"event_object_table\", \"sql_identifier\", 13),\n        (\"action_order\", \"cardinal_number\", 13),\n        (\"action_condition\", \"character_data\", 13),\n        (\"action_statement\", \"character_data\", 13),\n        (\"action_orientation\", \"character_data\", 13),\n        (\"action_timing\", \"character_data\", 13),\n        (\"action_reference_old_table\", \"sql_identifier\", 13),\n        (\"action_reference_new_table\", \"sql_identifier\", 13),\n        (\"action_reference_old_row\", \"sql_identifier\", 13),\n        (\"action_reference_new_row\", \"sql_identifier\", 13),\n        (\"created\", \"time_stamp\", 13),\n    ],\n    \"udt_privileges\": [\n        (\"grantor\", \"sql_identifier\", 13),\n        (\"grantee\", \"sql_identifier\", 13),\n        (\"udt_catalog\", \"sql_identifier\", 13),\n        (\"udt_schema\", \"sql_identifier\", 13),\n        (\"udt_name\", \"sql_identifier\", 13),\n        (\"privilege_type\", \"character_data\", 13),\n        (\"is_grantable\", \"yes_or_no\", 13),\n    ],\n    \"usage_privileges\": [\n        (\"grantor\", \"sql_identifier\", 13),\n        (\"grantee\", \"sql_identifier\", 13),\n        (\"object_catalog\", \"sql_identifier\", 13),\n        (\"object_schema\", \"sql_identifier\", 13),\n        (\"object_name\", \"sql_identifier\", 13),\n        (\"object_type\", \"character_data\", 13),\n        (\"privilege_type\", \"character_data\", 13),\n        (\"is_grantable\", \"yes_or_no\", 13),\n    ],\n    \"user_defined_types\": [\n        (\"user_defined_type_catalog\", \"sql_identifier\", 13),\n        (\"user_defined_type_schema\", \"sql_identifier\", 13),\n        (\"user_defined_type_name\", \"sql_identifier\", 13),\n        (\"user_defined_type_category\", \"character_data\", 13),\n        (\"is_instantiable\", \"yes_or_no\", 13),\n        (\"is_final\", \"yes_or_no\", 13),\n        (\"ordering_form\", \"character_data\", 13),\n        (\"ordering_category\", \"character_data\", 13),\n        (\"ordering_routine_catalog\", \"sql_identifier\", 13),\n        (\"ordering_routine_schema\", \"sql_identifier\", 13),\n        (\"ordering_routine_name\", \"sql_identifier\", 13),\n        (\"reference_type\", \"character_data\", 13),\n        (\"data_type\", \"character_data\", 13),\n        (\"character_maximum_length\", \"cardinal_number\", 13),\n        (\"character_octet_length\", \"cardinal_number\", 13),\n        (\"character_set_catalog\", \"sql_identifier\", 13),\n        (\"character_set_schema\", \"sql_identifier\", 13),\n        (\"character_set_name\", \"sql_identifier\", 13),\n        (\"collation_catalog\", \"sql_identifier\", 13),\n        (\"collation_schema\", \"sql_identifier\", 13),\n        (\"collation_name\", \"sql_identifier\", 13),\n        (\"numeric_precision\", \"cardinal_number\", 13),\n        (\"numeric_precision_radix\", \"cardinal_number\", 13),\n        (\"numeric_scale\", \"cardinal_number\", 13),\n        (\"datetime_precision\", \"cardinal_number\", 13),\n        (\"interval_type\", \"character_data\", 13),\n        (\"interval_precision\", \"cardinal_number\", 13),\n        (\"source_dtd_identifier\", \"sql_identifier\", 13),\n        (\"ref_dtd_identifier\", \"sql_identifier\", 13),\n    ],\n    \"user_mapping_options\": [\n        (\"authorization_identifier\", \"sql_identifier\", 13),\n        (\"foreign_server_catalog\", \"sql_identifier\", 13),\n        (\"foreign_server_name\", \"sql_identifier\", 13),\n        (\"option_name\", \"sql_identifier\", 13),\n        (\"option_value\", \"character_data\", 13),\n    ],\n    \"user_mappings\": [\n        (\"authorization_identifier\", \"sql_identifier\", 13),\n        (\"foreign_server_catalog\", \"sql_identifier\", 13),\n        (\"foreign_server_name\", \"sql_identifier\", 13),\n    ],\n    \"view_column_usage\": [\n        (\"view_catalog\", \"sql_identifier\", 13),\n        (\"view_schema\", \"sql_identifier\", 13),\n        (\"view_name\", \"sql_identifier\", 13),\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"column_name\", \"sql_identifier\", 13),\n    ],\n    \"view_routine_usage\": [\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"specific_catalog\", \"sql_identifier\", 13),\n        (\"specific_schema\", \"sql_identifier\", 13),\n        (\"specific_name\", \"sql_identifier\", 13),\n    ],\n    \"view_table_usage\": [\n        (\"view_catalog\", \"sql_identifier\", 13),\n        (\"view_schema\", \"sql_identifier\", 13),\n        (\"view_name\", \"sql_identifier\", 13),\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n    ],\n    \"views\": [\n        (\"table_catalog\", \"sql_identifier\", 13),\n        (\"table_schema\", \"sql_identifier\", 13),\n        (\"table_name\", \"sql_identifier\", 13),\n        (\"view_definition\", \"character_data\", 13),\n        (\"check_option\", \"character_data\", 13),\n        (\"is_updatable\", \"yes_or_no\", 13),\n        (\"is_insertable_into\", \"yes_or_no\", 13),\n        (\"is_trigger_updatable\", \"yes_or_no\", 13),\n        (\"is_trigger_deletable\", \"yes_or_no\", 13),\n        (\"is_trigger_insertable_into\", \"yes_or_no\", 13),\n    ]\n}\nPG_CATALOG: dict[str, list[tuple[ColumnName, ColumnType, int]]] = {\n    \"pg_aggregate\": [\n        (\"aggfnoid\", \"regproc\", 13),\n        (\"aggkind\", \"\\\"char\\\"\", 13),\n        (\"aggnumdirectargs\", \"smallint\", 13),\n        (\"aggtransfn\", \"regproc\", 13),\n        (\"aggfinalfn\", \"regproc\", 13),\n        (\"aggcombinefn\", \"regproc\", 13),\n        (\"aggserialfn\", \"regproc\", 13),\n        (\"aggdeserialfn\", \"regproc\", 13),\n        (\"aggmtransfn\", \"regproc\", 13),\n        (\"aggminvtransfn\", \"regproc\", 13),\n        (\"aggmfinalfn\", \"regproc\", 13),\n        (\"aggfinalextra\", \"boolean\", 13),\n        (\"aggmfinalextra\", \"boolean\", 13),\n        (\"aggfinalmodify\", \"\\\"char\\\"\", 13),\n        (\"aggmfinalmodify\", \"\\\"char\\\"\", 13),\n        (\"aggsortop\", \"oid\", 13),\n        (\"aggtranstype\", \"oid\", 13),\n        (\"aggtransspace\", \"integer\", 13),\n        (\"aggmtranstype\", \"oid\", 13),\n        (\"aggmtransspace\", \"integer\", 13),\n        (\"agginitval\", \"text\", 13),\n        (\"aggminitval\", \"text\", 13),\n    ],\n    \"pg_am\": [\n        (\"oid\", \"oid\", 13),\n        (\"amname\", \"name\", 13),\n        (\"amhandler\", \"regproc\", 13),\n        (\"amtype\", \"\\\"char\\\"\", 13),\n    ],\n    \"pg_amop\": [\n        (\"oid\", \"oid\", 13),\n        (\"amopfamily\", \"oid\", 13),\n        (\"amoplefttype\", \"oid\", 13),\n        (\"amoprighttype\", \"oid\", 13),\n        (\"amopstrategy\", \"smallint\", 13),\n        (\"amoppurpose\", \"\\\"char\\\"\", 13),\n        (\"amopopr\", \"oid\", 13),\n        (\"amopmethod\", \"oid\", 13),\n        (\"amopsortfamily\", \"oid\", 13),\n    ],\n    \"pg_amproc\": [\n        (\"oid\", \"oid\", 13),\n        (\"amprocfamily\", \"oid\", 13),\n        (\"amproclefttype\", \"oid\", 13),\n        (\"amprocrighttype\", \"oid\", 13),\n        (\"amprocnum\", \"smallint\", 13),\n        (\"amproc\", \"regproc\", 13),\n    ],\n    \"pg_attrdef\": [\n        (\"oid\", \"oid\", 13),\n        (\"adrelid\", \"oid\", 13),\n        (\"adnum\", \"smallint\", 13),\n        (\"adbin\", \"pg_node_tree\", 13),\n    ],\n    \"pg_attribute\": [\n        (\"attrelid\", \"oid\", 13),\n        (\"attname\", \"name\", 13),\n        (\"atttypid\", \"oid\", 13),\n        (\"attlen\", \"smallint\", 13),\n        (\"attnum\", \"smallint\", 13),\n        (\"attcacheoff\", \"integer\", 13),\n        (\"atttypmod\", \"integer\", 13),\n        (\"attndims\", \"smallint\", 13),\n        (\"attbyval\", \"boolean\", 13),\n        (\"attalign\", \"\\\"char\\\"\", 13),\n        (\"attstorage\", \"\\\"char\\\"\", 13),\n        (\"attcompression\", \"\\\"char\\\"\", 14),\n        (\"attnotnull\", \"boolean\", 13),\n        (\"atthasdef\", \"boolean\", 13),\n        (\"atthasmissing\", \"boolean\", 13),\n        (\"attidentity\", \"\\\"char\\\"\", 13),\n        (\"attgenerated\", \"\\\"char\\\"\", 13),\n        (\"attisdropped\", \"boolean\", 13),\n        (\"attislocal\", \"boolean\", 13),\n        (\"attinhcount\", \"smallint\", 13),\n        (\"attcollation\", \"oid\", 13),\n        (\"attstattarget\", \"smallint\", 13),\n        (\"attacl\", None, 13),\n        (\"attoptions\", None, 13),\n        (\"attfdwoptions\", None, 13),\n        (\"attmissingval\", None, 13),\n    ],\n    \"pg_auth_members\": [\n        (\"oid\", \"oid\", 16),\n        (\"roleid\", \"oid\", 13),\n        (\"member\", \"oid\", 13),\n        (\"grantor\", \"oid\", 13),\n        (\"admin_option\", \"boolean\", 13),\n        (\"inherit_option\", \"boolean\", 16),\n        (\"set_option\", \"boolean\", 16),\n    ],\n    \"pg_authid\": [\n        (\"oid\", \"oid\", 13),\n        (\"rolname\", \"name\", 13),\n        (\"rolsuper\", \"boolean\", 13),\n        (\"rolinherit\", \"boolean\", 13),\n        (\"rolcreaterole\", \"boolean\", 13),\n        (\"rolcreatedb\", \"boolean\", 13),\n        (\"rolcanlogin\", \"boolean\", 13),\n        (\"rolreplication\", \"boolean\", 13),\n        (\"rolbypassrls\", \"boolean\", 13),\n        (\"rolconnlimit\", \"integer\", 13),\n        (\"rolpassword\", \"text\", 13),\n        (\"rolvaliduntil\", \"timestamp with time zone\", 13),\n    ],\n    \"pg_available_extension_versions\": [\n        (\"name\", \"name\", 13),\n        (\"version\", \"text\", 13),\n        (\"installed\", \"boolean\", 13),\n        (\"superuser\", \"boolean\", 13),\n        (\"trusted\", \"boolean\", 13),\n        (\"relocatable\", \"boolean\", 13),\n        (\"schema\", \"name\", 13),\n        (\"requires\", None, 13),\n        (\"comment\", \"text\", 13),\n    ],\n    \"pg_available_extensions\": [\n        (\"name\", \"name\", 13),\n        (\"default_version\", \"text\", 13),\n        (\"installed_version\", \"text\", 13),\n        (\"comment\", \"text\", 13),\n    ],\n    \"pg_backend_memory_contexts\": [\n        (\"name\", \"text\", 14),\n        (\"ident\", \"text\", 14),\n        (\"parent\", \"text\", 14),\n        (\"level\", \"integer\", 14),\n        (\"total_bytes\", \"bigint\", 14),\n        (\"total_nblocks\", \"bigint\", 14),\n        (\"free_bytes\", \"bigint\", 14),\n        (\"free_chunks\", \"bigint\", 14),\n        (\"used_bytes\", \"bigint\", 14),\n    ],\n    \"pg_cast\": [\n        (\"oid\", \"oid\", 13),\n        (\"castsource\", \"oid\", 13),\n        (\"casttarget\", \"oid\", 13),\n        (\"castfunc\", \"oid\", 13),\n        (\"castcontext\", \"\\\"char\\\"\", 13),\n        (\"castmethod\", \"\\\"char\\\"\", 13),\n    ],\n    \"pg_class\": [\n        (\"oid\", \"oid\", 13),\n        (\"relname\", \"name\", 13),\n        (\"relnamespace\", \"oid\", 13),\n        (\"reltype\", \"oid\", 13),\n        (\"reloftype\", \"oid\", 13),\n        (\"relowner\", \"oid\", 13),\n        (\"relam\", \"oid\", 13),\n        (\"relfilenode\", \"oid\", 13),\n        (\"reltablespace\", \"oid\", 13),\n        (\"relpages\", \"integer\", 13),\n        (\"reltuples\", \"real\", 13),\n        (\"relallvisible\", \"integer\", 13),\n        (\"reltoastrelid\", \"oid\", 13),\n        (\"relhasindex\", \"boolean\", 13),\n        (\"relisshared\", \"boolean\", 13),\n        (\"relpersistence\", \"\\\"char\\\"\", 13),\n        (\"relkind\", \"\\\"char\\\"\", 13),\n        (\"relnatts\", \"smallint\", 13),\n        (\"relchecks\", \"smallint\", 13),\n        (\"relhasrules\", \"boolean\", 13),\n        (\"relhastriggers\", \"boolean\", 13),\n        (\"relhassubclass\", \"boolean\", 13),\n        (\"relrowsecurity\", \"boolean\", 13),\n        (\"relforcerowsecurity\", \"boolean\", 13),\n        (\"relispopulated\", \"boolean\", 13),\n        (\"relreplident\", \"\\\"char\\\"\", 13),\n        (\"relispartition\", \"boolean\", 13),\n        (\"relrewrite\", \"oid\", 13),\n        (\"relfrozenxid\", \"xid\", 13),\n        (\"relminmxid\", \"xid\", 13),\n        (\"relacl\", None, 13),\n        (\"reloptions\", None, 13),\n        (\"relpartbound\", \"pg_node_tree\", 13),\n    ],\n    \"pg_collation\": [\n        (\"oid\", \"oid\", 13),\n        (\"collname\", \"name\", 13),\n        (\"collnamespace\", \"oid\", 13),\n        (\"collowner\", \"oid\", 13),\n        (\"collprovider\", \"\\\"char\\\"\", 13),\n        (\"collisdeterministic\", \"boolean\", 13),\n        (\"collencoding\", \"integer\", 13),\n        (\"collcollate\", \"text\", 13),\n        (\"collctype\", \"text\", 13),\n        (\"colllocale\", \"text\", 17),\n        (\"collicurules\", \"text\", 16),\n        (\"collversion\", \"text\", 13),\n    ],\n    \"pg_config\": [\n        (\"name\", \"text\", 13),\n        (\"setting\", \"text\", 13),\n    ],\n    \"pg_constraint\": [\n        (\"oid\", \"oid\", 13),\n        (\"conname\", \"name\", 13),\n        (\"connamespace\", \"oid\", 13),\n        (\"contype\", \"\\\"char\\\"\", 13),\n        (\"condeferrable\", \"boolean\", 13),\n        (\"condeferred\", \"boolean\", 13),\n        (\"convalidated\", \"boolean\", 13),\n        (\"conrelid\", \"oid\", 13),\n        (\"contypid\", \"oid\", 13),\n        (\"conindid\", \"oid\", 13),\n        (\"conparentid\", \"oid\", 13),\n        (\"confrelid\", \"oid\", 13),\n        (\"confupdtype\", \"\\\"char\\\"\", 13),\n        (\"confdeltype\", \"\\\"char\\\"\", 13),\n        (\"confmatchtype\", \"\\\"char\\\"\", 13),\n        (\"conislocal\", \"boolean\", 13),\n        (\"coninhcount\", \"smallint\", 13),\n        (\"connoinherit\", \"boolean\", 13),\n        (\"conkey\", None, 13),\n        (\"confkey\", None, 13),\n        (\"conpfeqop\", None, 13),\n        (\"conppeqop\", None, 13),\n        (\"conffeqop\", None, 13),\n        (\"confdelsetcols\", None, 15),\n        (\"conexclop\", None, 13),\n        (\"conbin\", \"pg_node_tree\", 13),\n    ],\n    \"pg_conversion\": [\n        (\"oid\", \"oid\", 13),\n        (\"conname\", \"name\", 13),\n        (\"connamespace\", \"oid\", 13),\n        (\"conowner\", \"oid\", 13),\n        (\"conforencoding\", \"integer\", 13),\n        (\"contoencoding\", \"integer\", 13),\n        (\"conproc\", \"regproc\", 13),\n        (\"condefault\", \"boolean\", 13),\n    ],\n    \"pg_cursors\": [\n        (\"name\", \"text\", 13),\n        (\"statement\", \"text\", 13),\n        (\"is_holdable\", \"boolean\", 13),\n        (\"is_binary\", \"boolean\", 13),\n        (\"is_scrollable\", \"boolean\", 13),\n        (\"creation_time\", \"timestamp with time zone\", 13),\n    ],\n    \"pg_database\": [\n        (\"oid\", \"oid\", 13),\n        (\"datname\", \"name\", 13),\n        (\"datdba\", \"oid\", 13),\n        (\"encoding\", \"integer\", 13),\n        (\"datlocprovider\", \"\\\"char\\\"\", 15),\n        (\"datistemplate\", \"boolean\", 13),\n        (\"datallowconn\", \"boolean\", 13),\n        (\"dathasloginevt\", \"boolean\", 17),\n        (\"datconnlimit\", \"integer\", 13),\n        (\"datfrozenxid\", \"xid\", 13),\n        (\"datminmxid\", \"xid\", 13),\n        (\"dattablespace\", \"oid\", 13),\n        (\"datcollate\", \"text\", 13),\n        (\"datctype\", \"text\", 13),\n        (\"datlocale\", \"text\", 17),\n        (\"daticurules\", \"text\", 16),\n        (\"datcollversion\", \"text\", 15),\n        (\"datacl\", None, 13),\n    ],\n    \"pg_db_role_setting\": [\n        (\"setdatabase\", \"oid\", 13),\n        (\"setrole\", \"oid\", 13),\n        (\"setconfig\", None, 13),\n    ],\n    \"pg_default_acl\": [\n        (\"oid\", \"oid\", 13),\n        (\"defaclrole\", \"oid\", 13),\n        (\"defaclnamespace\", \"oid\", 13),\n        (\"defaclobjtype\", \"\\\"char\\\"\", 13),\n        (\"defaclacl\", None, 13),\n    ],\n    \"pg_depend\": [\n        (\"classid\", \"oid\", 13),\n        (\"objid\", \"oid\", 13),\n        (\"objsubid\", \"integer\", 13),\n        (\"refclassid\", \"oid\", 13),\n        (\"refobjid\", \"oid\", 13),\n        (\"refobjsubid\", \"integer\", 13),\n        (\"deptype\", \"\\\"char\\\"\", 13),\n    ],\n    \"pg_description\": [\n        (\"objoid\", \"oid\", 13),\n        (\"classoid\", \"oid\", 13),\n        (\"objsubid\", \"integer\", 13),\n        (\"description\", \"text\", 13),\n    ],\n    \"pg_enum\": [\n        (\"oid\", \"oid\", 13),\n        (\"enumtypid\", \"oid\", 13),\n        (\"enumsortorder\", \"real\", 13),\n        (\"enumlabel\", \"name\", 13),\n    ],\n    \"pg_event_trigger\": [\n        (\"oid\", \"oid\", 13),\n        (\"evtname\", \"name\", 13),\n        (\"evtevent\", \"name\", 13),\n        (\"evtowner\", \"oid\", 13),\n        (\"evtfoid\", \"oid\", 13),\n        (\"evtenabled\", \"\\\"char\\\"\", 13),\n        (\"evttags\", None, 13),\n    ],\n    \"pg_extension\": [\n        (\"oid\", \"oid\", 13),\n        (\"extname\", \"name\", 13),\n        (\"extowner\", \"oid\", 13),\n        (\"extnamespace\", \"oid\", 13),\n        (\"extrelocatable\", \"boolean\", 13),\n        (\"extversion\", \"text\", 13),\n        (\"extconfig\", None, 13),\n        (\"extcondition\", None, 13),\n    ],\n    \"pg_file_settings\": [\n        (\"sourcefile\", \"text\", 13),\n        (\"sourceline\", \"integer\", 13),\n        (\"seqno\", \"integer\", 13),\n        (\"name\", \"text\", 13),\n        (\"setting\", \"text\", 13),\n        (\"applied\", \"boolean\", 13),\n        (\"error\", \"text\", 13),\n    ],\n    \"pg_foreign_data_wrapper\": [\n        (\"oid\", \"oid\", 13),\n        (\"fdwname\", \"name\", 13),\n        (\"fdwowner\", \"oid\", 13),\n        (\"fdwhandler\", \"oid\", 13),\n        (\"fdwvalidator\", \"oid\", 13),\n        (\"fdwacl\", None, 13),\n        (\"fdwoptions\", None, 13),\n    ],\n    \"pg_foreign_server\": [\n        (\"oid\", \"oid\", 13),\n        (\"srvname\", \"name\", 13),\n        (\"srvowner\", \"oid\", 13),\n        (\"srvfdw\", \"oid\", 13),\n        (\"srvtype\", \"text\", 13),\n        (\"srvversion\", \"text\", 13),\n        (\"srvacl\", None, 13),\n        (\"srvoptions\", None, 13),\n    ],\n    \"pg_foreign_table\": [\n        (\"ftrelid\", \"oid\", 13),\n        (\"ftserver\", \"oid\", 13),\n        (\"ftoptions\", None, 13),\n    ],\n    \"pg_group\": [\n        (\"groname\", \"name\", 13),\n        (\"grosysid\", \"oid\", 13),\n        (\"grolist\", None, 13),\n    ],\n    \"pg_hba_file_rules\": [\n        (\"rule_number\", \"integer\", 16),\n        (\"file_name\", \"text\", 16),\n        (\"line_number\", \"integer\", 13),\n        (\"type\", \"text\", 13),\n        (\"database\", None, 13),\n        (\"user_name\", None, 13),\n        (\"address\", \"text\", 13),\n        (\"netmask\", \"text\", 13),\n        (\"auth_method\", \"text\", 13),\n        (\"options\", None, 13),\n        (\"error\", \"text\", 13),\n    ],\n    \"pg_ident_file_mappings\": [\n        (\"map_number\", \"integer\", 16),\n        (\"file_name\", \"text\", 16),\n        (\"line_number\", \"integer\", 15),\n        (\"map_name\", \"text\", 15),\n        (\"sys_name\", \"text\", 15),\n        (\"pg_username\", \"text\", 15),\n        (\"error\", \"text\", 15),\n    ],\n    \"pg_index\": [\n        (\"indexrelid\", \"oid\", 13),\n        (\"indrelid\", \"oid\", 13),\n        (\"indnatts\", \"smallint\", 13),\n        (\"indnkeyatts\", \"smallint\", 13),\n        (\"indisunique\", \"boolean\", 13),\n        (\"indnullsnotdistinct\", \"boolean\", 15),\n        (\"indisprimary\", \"boolean\", 13),\n        (\"indisexclusion\", \"boolean\", 13),\n        (\"indimmediate\", \"boolean\", 13),\n        (\"indisclustered\", \"boolean\", 13),\n        (\"indisvalid\", \"boolean\", 13),\n        (\"indcheckxmin\", \"boolean\", 13),\n        (\"indisready\", \"boolean\", 13),\n        (\"indislive\", \"boolean\", 13),\n        (\"indisreplident\", \"boolean\", 13),\n        (\"indkey\", None, 13),\n        (\"indcollation\", None, 13),\n        (\"indclass\", None, 13),\n        (\"indoption\", None, 13),\n        (\"indexprs\", \"pg_node_tree\", 13),\n        (\"indpred\", \"pg_node_tree\", 13),\n    ],\n    \"pg_indexes\": [\n        (\"schemaname\", \"name\", 13),\n        (\"tablename\", \"name\", 13),\n        (\"indexname\", \"name\", 13),\n        (\"tablespace\", \"name\", 13),\n        (\"indexdef\", \"text\", 13),\n    ],\n    \"pg_inherits\": [\n        (\"inhrelid\", \"oid\", 13),\n        (\"inhparent\", \"oid\", 13),\n        (\"inhseqno\", \"integer\", 13),\n        (\"inhdetachpending\", \"boolean\", 14),\n    ],\n    \"pg_init_privs\": [\n        (\"objoid\", \"oid\", 13),\n        (\"classoid\", \"oid\", 13),\n        (\"objsubid\", \"integer\", 13),\n        (\"privtype\", \"\\\"char\\\"\", 13),\n        (\"initprivs\", None, 13),\n    ],\n    \"pg_language\": [\n        (\"oid\", \"oid\", 13),\n        (\"lanname\", \"name\", 13),\n        (\"lanowner\", \"oid\", 13),\n        (\"lanispl\", \"boolean\", 13),\n        (\"lanpltrusted\", \"boolean\", 13),\n        (\"lanplcallfoid\", \"oid\", 13),\n        (\"laninline\", \"oid\", 13),\n        (\"lanvalidator\", \"oid\", 13),\n        (\"lanacl\", None, 13),\n    ],\n    \"pg_largeobject\": [\n        (\"loid\", \"oid\", 13),\n        (\"pageno\", \"integer\", 13),\n        (\"data\", \"bytea\", 13),\n    ],\n    \"pg_largeobject_metadata\": [\n        (\"oid\", \"oid\", 13),\n        (\"lomowner\", \"oid\", 13),\n        (\"lomacl\", None, 13),\n    ],\n    \"pg_locks\": [\n        (\"locktype\", \"text\", 13),\n        (\"database\", \"oid\", 13),\n        (\"relation\", \"oid\", 13),\n        (\"page\", \"integer\", 13),\n        (\"tuple\", \"smallint\", 13),\n        (\"virtualxid\", \"text\", 13),\n        (\"transactionid\", \"xid\", 13),\n        (\"classid\", \"oid\", 13),\n        (\"objid\", \"oid\", 13),\n        (\"objsubid\", \"smallint\", 13),\n        (\"virtualtransaction\", \"text\", 13),\n        (\"pid\", \"integer\", 13),\n        (\"mode\", \"text\", 13),\n        (\"granted\", \"boolean\", 13),\n        (\"fastpath\", \"boolean\", 13),\n        (\"waitstart\", \"timestamp with time zone\", 14),\n    ],\n    \"pg_matviews\": [\n        (\"schemaname\", \"name\", 13),\n        (\"matviewname\", \"name\", 13),\n        (\"matviewowner\", \"name\", 13),\n        (\"tablespace\", \"name\", 13),\n        (\"hasindexes\", \"boolean\", 13),\n        (\"ispopulated\", \"boolean\", 13),\n        (\"definition\", \"text\", 13),\n    ],\n    \"pg_namespace\": [\n        (\"oid\", \"oid\", 13),\n        (\"nspname\", \"name\", 13),\n        (\"nspowner\", \"oid\", 13),\n        (\"nspacl\", None, 13),\n    ],\n    \"pg_opclass\": [\n        (\"oid\", \"oid\", 13),\n        (\"opcmethod\", \"oid\", 13),\n        (\"opcname\", \"name\", 13),\n        (\"opcnamespace\", \"oid\", 13),\n        (\"opcowner\", \"oid\", 13),\n        (\"opcfamily\", \"oid\", 13),\n        (\"opcintype\", \"oid\", 13),\n        (\"opcdefault\", \"boolean\", 13),\n        (\"opckeytype\", \"oid\", 13),\n    ],\n    \"pg_operator\": [\n        (\"oid\", \"oid\", 13),\n        (\"oprname\", \"name\", 13),\n        (\"oprnamespace\", \"oid\", 13),\n        (\"oprowner\", \"oid\", 13),\n        (\"oprkind\", \"\\\"char\\\"\", 13),\n        (\"oprcanmerge\", \"boolean\", 13),\n        (\"oprcanhash\", \"boolean\", 13),\n        (\"oprleft\", \"oid\", 13),\n        (\"oprright\", \"oid\", 13),\n        (\"oprresult\", \"oid\", 13),\n        (\"oprcom\", \"oid\", 13),\n        (\"oprnegate\", \"oid\", 13),\n        (\"oprcode\", \"regproc\", 13),\n        (\"oprrest\", \"regproc\", 13),\n        (\"oprjoin\", \"regproc\", 13),\n    ],\n    \"pg_opfamily\": [\n        (\"oid\", \"oid\", 13),\n        (\"opfmethod\", \"oid\", 13),\n        (\"opfname\", \"name\", 13),\n        (\"opfnamespace\", \"oid\", 13),\n        (\"opfowner\", \"oid\", 13),\n    ],\n    \"pg_parameter_acl\": [\n        (\"oid\", \"oid\", 15),\n        (\"parname\", \"text\", 15),\n        (\"paracl\", None, 15),\n    ],\n    \"pg_partitioned_table\": [\n        (\"partrelid\", \"oid\", 13),\n        (\"partstrat\", \"\\\"char\\\"\", 13),\n        (\"partnatts\", \"smallint\", 13),\n        (\"partdefid\", \"oid\", 13),\n        (\"partattrs\", None, 13),\n        (\"partclass\", None, 13),\n        (\"partcollation\", None, 13),\n        (\"partexprs\", \"pg_node_tree\", 13),\n    ],\n    \"pg_policies\": [\n        (\"schemaname\", \"name\", 13),\n        (\"tablename\", \"name\", 13),\n        (\"policyname\", \"name\", 13),\n        (\"permissive\", \"text\", 13),\n        (\"roles\", None, 13),\n        (\"cmd\", \"text\", 13),\n        (\"qual\", \"text\", 13),\n        (\"with_check\", \"text\", 13),\n    ],\n    \"pg_policy\": [\n        (\"oid\", \"oid\", 13),\n        (\"polname\", \"name\", 13),\n        (\"polrelid\", \"oid\", 13),\n        (\"polcmd\", \"\\\"char\\\"\", 13),\n        (\"polpermissive\", \"boolean\", 13),\n        (\"polroles\", None, 13),\n        (\"polqual\", \"pg_node_tree\", 13),\n        (\"polwithcheck\", \"pg_node_tree\", 13),\n    ],\n    \"pg_prepared_statements\": [\n        (\"name\", \"text\", 13),\n        (\"statement\", \"text\", 13),\n        (\"prepare_time\", \"timestamp with time zone\", 13),\n        (\"parameter_types\", None, 13),\n        (\"result_types\", None, 16),\n        (\"from_sql\", \"boolean\", 13),\n        (\"generic_plans\", \"bigint\", 14),\n        (\"custom_plans\", \"bigint\", 14),\n    ],\n    \"pg_prepared_xacts\": [\n        (\"transaction\", \"xid\", 13),\n        (\"gid\", \"text\", 13),\n        (\"prepared\", \"timestamp with time zone\", 13),\n        (\"owner\", \"name\", 13),\n        (\"database\", \"name\", 13),\n    ],\n    \"pg_proc\": [\n        (\"oid\", \"oid\", 13),\n        (\"proname\", \"name\", 13),\n        (\"pronamespace\", \"oid\", 13),\n        (\"proowner\", \"oid\", 13),\n        (\"prolang\", \"oid\", 13),\n        (\"procost\", \"real\", 13),\n        (\"prorows\", \"real\", 13),\n        (\"provariadic\", \"oid\", 13),\n        (\"prosupport\", \"regproc\", 13),\n        (\"prokind\", \"\\\"char\\\"\", 13),\n        (\"prosecdef\", \"boolean\", 13),\n        (\"proleakproof\", \"boolean\", 13),\n        (\"proisstrict\", \"boolean\", 13),\n        (\"proretset\", \"boolean\", 13),\n        (\"provolatile\", \"\\\"char\\\"\", 13),\n        (\"proparallel\", \"\\\"char\\\"\", 13),\n        (\"pronargs\", \"smallint\", 13),\n        (\"pronargdefaults\", \"smallint\", 13),\n        (\"prorettype\", \"oid\", 13),\n        (\"proargtypes\", None, 13),\n        (\"proallargtypes\", None, 13),\n        (\"proargmodes\", None, 13),\n        (\"proargnames\", None, 13),\n        (\"proargdefaults\", \"pg_node_tree\", 13),\n        (\"protrftypes\", None, 13),\n        (\"prosrc\", \"text\", 13),\n        (\"probin\", \"text\", 13),\n        (\"prosqlbody\", \"pg_node_tree\", 14),\n        (\"proconfig\", None, 13),\n        (\"proacl\", None, 13),\n    ],\n    \"pg_publication\": [\n        (\"oid\", \"oid\", 13),\n        (\"pubname\", \"name\", 13),\n        (\"pubowner\", \"oid\", 13),\n        (\"puballtables\", \"boolean\", 13),\n        (\"pubinsert\", \"boolean\", 13),\n        (\"pubupdate\", \"boolean\", 13),\n        (\"pubdelete\", \"boolean\", 13),\n        (\"pubtruncate\", \"boolean\", 13),\n        (\"pubviaroot\", \"boolean\", 13),\n    ],\n    \"pg_publication_namespace\": [\n        (\"oid\", \"oid\", 15),\n        (\"pnpubid\", \"oid\", 15),\n        (\"pnnspid\", \"oid\", 15),\n    ],\n    \"pg_publication_rel\": [\n        (\"oid\", \"oid\", 13),\n        (\"prpubid\", \"oid\", 13),\n        (\"prrelid\", \"oid\", 13),\n        (\"prqual\", \"pg_node_tree\", 15),\n        (\"prattrs\", None, 15),\n    ],\n    \"pg_publication_tables\": [\n        (\"pubname\", \"name\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"tablename\", \"name\", 13),\n        (\"attnames\", None, 15),\n        (\"rowfilter\", \"text\", 15),\n    ],\n    \"pg_range\": [\n        (\"rngtypid\", \"oid\", 13),\n        (\"rngsubtype\", \"oid\", 13),\n        (\"rngmultitypid\", \"oid\", 14),\n        (\"rngcollation\", \"oid\", 13),\n        (\"rngsubopc\", \"oid\", 13),\n        (\"rngcanonical\", \"regproc\", 13),\n        (\"rngsubdiff\", \"regproc\", 13),\n    ],\n    \"pg_replication_origin\": [\n        (\"roident\", \"oid\", 13),\n        (\"roname\", \"text\", 13),\n    ],\n    \"pg_replication_origin_status\": [\n        (\"local_id\", \"oid\", 13),\n        (\"external_id\", \"text\", 13),\n        (\"remote_lsn\", \"pg_lsn\", 13),\n        (\"local_lsn\", \"pg_lsn\", 13),\n    ],\n    \"pg_replication_slots\": [\n        (\"slot_name\", \"name\", 13),\n        (\"plugin\", \"name\", 13),\n        (\"slot_type\", \"text\", 13),\n        (\"datoid\", \"oid\", 13),\n        (\"database\", \"name\", 13),\n        (\"temporary\", \"boolean\", 13),\n        (\"active\", \"boolean\", 13),\n        (\"active_pid\", \"integer\", 13),\n        (\"xmin\", \"xid\", 13),\n        (\"catalog_xmin\", \"xid\", 13),\n        (\"restart_lsn\", \"pg_lsn\", 13),\n        (\"confirmed_flush_lsn\", \"pg_lsn\", 13),\n        (\"wal_status\", \"text\", 13),\n        (\"safe_wal_size\", \"bigint\", 13),\n        (\"two_phase\", \"boolean\", 14),\n        (\"inactive_since\", \"timestamp with time zone\", 17),\n        (\"conflicting\", \"boolean\", 16),\n        (\"invalidation_reason\", \"text\", 17),\n        (\"failover\", \"boolean\", 17),\n        (\"synced\", \"boolean\", 17),\n    ],\n    \"pg_rewrite\": [\n        (\"oid\", \"oid\", 13),\n        (\"rulename\", \"name\", 13),\n        (\"ev_class\", \"oid\", 13),\n        (\"ev_type\", \"\\\"char\\\"\", 13),\n        (\"ev_enabled\", \"\\\"char\\\"\", 13),\n        (\"is_instead\", \"boolean\", 13),\n        (\"ev_qual\", \"pg_node_tree\", 13),\n        (\"ev_action\", \"pg_node_tree\", 13),\n    ],\n    \"pg_roles\": [\n        (\"rolname\", \"name\", 13),\n        (\"rolsuper\", \"boolean\", 13),\n        (\"rolinherit\", \"boolean\", 13),\n        (\"rolcreaterole\", \"boolean\", 13),\n        (\"rolcreatedb\", \"boolean\", 13),\n        (\"rolcanlogin\", \"boolean\", 13),\n        (\"rolreplication\", \"boolean\", 13),\n        (\"rolconnlimit\", \"integer\", 13),\n        (\"rolpassword\", \"text\", 13),\n        (\"rolvaliduntil\", \"timestamp with time zone\", 13),\n        (\"rolbypassrls\", \"boolean\", 13),\n        (\"rolconfig\", None, 13),\n        (\"oid\", \"oid\", 13),\n    ],\n    \"pg_rules\": [\n        (\"schemaname\", \"name\", 13),\n        (\"tablename\", \"name\", 13),\n        (\"rulename\", \"name\", 13),\n        (\"definition\", \"text\", 13),\n    ],\n    \"pg_seclabel\": [\n        (\"objoid\", \"oid\", 13),\n        (\"classoid\", \"oid\", 13),\n        (\"objsubid\", \"integer\", 13),\n        (\"provider\", \"text\", 13),\n        (\"label\", \"text\", 13),\n    ],\n    \"pg_seclabels\": [\n        (\"objoid\", \"oid\", 13),\n        (\"classoid\", \"oid\", 13),\n        (\"objsubid\", \"integer\", 13),\n        (\"objtype\", \"text\", 13),\n        (\"objnamespace\", \"oid\", 13),\n        (\"objname\", \"text\", 13),\n        (\"provider\", \"text\", 13),\n        (\"label\", \"text\", 13),\n    ],\n    \"pg_sequence\": [\n        (\"seqrelid\", \"oid\", 13),\n        (\"seqtypid\", \"oid\", 13),\n        (\"seqstart\", \"bigint\", 13),\n        (\"seqincrement\", \"bigint\", 13),\n        (\"seqmax\", \"bigint\", 13),\n        (\"seqmin\", \"bigint\", 13),\n        (\"seqcache\", \"bigint\", 13),\n        (\"seqcycle\", \"boolean\", 13),\n    ],\n    \"pg_sequences\": [\n        (\"schemaname\", \"name\", 13),\n        (\"sequencename\", \"name\", 13),\n        (\"sequenceowner\", \"name\", 13),\n        (\"data_type\", \"regtype\", 13),\n        (\"start_value\", \"bigint\", 13),\n        (\"min_value\", \"bigint\", 13),\n        (\"max_value\", \"bigint\", 13),\n        (\"increment_by\", \"bigint\", 13),\n        (\"cycle\", \"boolean\", 13),\n        (\"cache_size\", \"bigint\", 13),\n        (\"last_value\", \"bigint\", 13),\n    ],\n    \"pg_settings\": [\n        (\"name\", \"text\", 13),\n        (\"setting\", \"text\", 13),\n        (\"unit\", \"text\", 13),\n        (\"category\", \"text\", 13),\n        (\"short_desc\", \"text\", 13),\n        (\"extra_desc\", \"text\", 13),\n        (\"context\", \"text\", 13),\n        (\"vartype\", \"text\", 13),\n        (\"source\", \"text\", 13),\n        (\"min_val\", \"text\", 13),\n        (\"max_val\", \"text\", 13),\n        (\"enumvals\", None, 13),\n        (\"boot_val\", \"text\", 13),\n        (\"reset_val\", \"text\", 13),\n        (\"sourcefile\", \"text\", 13),\n        (\"sourceline\", \"integer\", 13),\n        (\"pending_restart\", \"boolean\", 13),\n    ],\n    \"pg_shadow\": [\n        (\"usename\", \"name\", 13),\n        (\"usesysid\", \"oid\", 13),\n        (\"usecreatedb\", \"boolean\", 13),\n        (\"usesuper\", \"boolean\", 13),\n        (\"userepl\", \"boolean\", 13),\n        (\"usebypassrls\", \"boolean\", 13),\n        (\"passwd\", \"text\", 13),\n        (\"valuntil\", \"timestamp with time zone\", 13),\n        (\"useconfig\", None, 13),\n    ],\n    \"pg_shdepend\": [\n        (\"dbid\", \"oid\", 13),\n        (\"classid\", \"oid\", 13),\n        (\"objid\", \"oid\", 13),\n        (\"objsubid\", \"integer\", 13),\n        (\"refclassid\", \"oid\", 13),\n        (\"refobjid\", \"oid\", 13),\n        (\"deptype\", \"\\\"char\\\"\", 13),\n    ],\n    \"pg_shdescription\": [\n        (\"objoid\", \"oid\", 13),\n        (\"classoid\", \"oid\", 13),\n        (\"description\", \"text\", 13),\n    ],\n    \"pg_shmem_allocations\": [\n        (\"name\", \"text\", 13),\n        (\"off\", \"bigint\", 13),\n        (\"size\", \"bigint\", 13),\n        (\"allocated_size\", \"bigint\", 13),\n    ],\n    \"pg_shseclabel\": [\n        (\"objoid\", \"oid\", 13),\n        (\"classoid\", \"oid\", 13),\n        (\"provider\", \"text\", 13),\n        (\"label\", \"text\", 13),\n    ],\n    \"pg_stat_activity\": [\n        (\"datid\", \"oid\", 13),\n        (\"datname\", \"name\", 13),\n        (\"pid\", \"integer\", 13),\n        (\"leader_pid\", \"integer\", 13),\n        (\"usesysid\", \"oid\", 13),\n        (\"usename\", \"name\", 13),\n        (\"application_name\", \"text\", 13),\n        (\"client_addr\", \"inet\", 13),\n        (\"client_hostname\", \"text\", 13),\n        (\"client_port\", \"integer\", 13),\n        (\"backend_start\", \"timestamp with time zone\", 13),\n        (\"xact_start\", \"timestamp with time zone\", 13),\n        (\"query_start\", \"timestamp with time zone\", 13),\n        (\"state_change\", \"timestamp with time zone\", 13),\n        (\"wait_event_type\", \"text\", 13),\n        (\"wait_event\", \"text\", 13),\n        (\"state\", \"text\", 13),\n        (\"backend_xid\", \"xid\", 13),\n        (\"backend_xmin\", \"xid\", 13),\n        (\"query_id\", \"bigint\", 14),\n        (\"query\", \"text\", 13),\n        (\"backend_type\", \"text\", 13),\n    ],\n    \"pg_stat_all_indexes\": [\n        (\"relid\", \"oid\", 13),\n        (\"indexrelid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"indexrelname\", \"name\", 13),\n        (\"idx_scan\", \"bigint\", 13),\n        (\"last_idx_scan\", \"timestamp with time zone\", 16),\n        (\"idx_tup_read\", \"bigint\", 13),\n        (\"idx_tup_fetch\", \"bigint\", 13),\n    ],\n    \"pg_stat_all_tables\": [\n        (\"relid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"seq_scan\", \"bigint\", 13),\n        (\"last_seq_scan\", \"timestamp with time zone\", 16),\n        (\"seq_tup_read\", \"bigint\", 13),\n        (\"idx_scan\", \"bigint\", 13),\n        (\"last_idx_scan\", \"timestamp with time zone\", 16),\n        (\"idx_tup_fetch\", \"bigint\", 13),\n        (\"n_tup_ins\", \"bigint\", 13),\n        (\"n_tup_upd\", \"bigint\", 13),\n        (\"n_tup_del\", \"bigint\", 13),\n        (\"n_tup_hot_upd\", \"bigint\", 13),\n        (\"n_tup_newpage_upd\", \"bigint\", 16),\n        (\"n_live_tup\", \"bigint\", 13),\n        (\"n_dead_tup\", \"bigint\", 13),\n        (\"n_mod_since_analyze\", \"bigint\", 13),\n        (\"n_ins_since_vacuum\", \"bigint\", 13),\n        (\"last_vacuum\", \"timestamp with time zone\", 13),\n        (\"last_autovacuum\", \"timestamp with time zone\", 13),\n        (\"last_analyze\", \"timestamp with time zone\", 13),\n        (\"last_autoanalyze\", \"timestamp with time zone\", 13),\n        (\"vacuum_count\", \"bigint\", 13),\n        (\"autovacuum_count\", \"bigint\", 13),\n        (\"analyze_count\", \"bigint\", 13),\n        (\"autoanalyze_count\", \"bigint\", 13),\n    ],\n    \"pg_stat_archiver\": [\n        (\"archived_count\", \"bigint\", 13),\n        (\"last_archived_wal\", \"text\", 13),\n        (\"last_archived_time\", \"timestamp with time zone\", 13),\n        (\"failed_count\", \"bigint\", 13),\n        (\"last_failed_wal\", \"text\", 13),\n        (\"last_failed_time\", \"timestamp with time zone\", 13),\n        (\"stats_reset\", \"timestamp with time zone\", 13),\n    ],\n    \"pg_stat_bgwriter\": [\n        (\"buffers_clean\", \"bigint\", 13),\n        (\"maxwritten_clean\", \"bigint\", 13),\n        (\"buffers_alloc\", \"bigint\", 13),\n        (\"stats_reset\", \"timestamp with time zone\", 13),\n    ],\n    \"pg_stat_checkpointer\": [\n        (\"num_timed\", \"bigint\", 17),\n        (\"num_requested\", \"bigint\", 17),\n        (\"restartpoints_timed\", \"bigint\", 17),\n        (\"restartpoints_req\", \"bigint\", 17),\n        (\"restartpoints_done\", \"bigint\", 17),\n        (\"write_time\", \"double precision\", 17),\n        (\"sync_time\", \"double precision\", 17),\n        (\"buffers_written\", \"bigint\", 17),\n        (\"stats_reset\", \"timestamp with time zone\", 17),\n    ],\n    \"pg_stat_database\": [\n        (\"datid\", \"oid\", 13),\n        (\"datname\", \"name\", 13),\n        (\"numbackends\", \"integer\", 13),\n        (\"xact_commit\", \"bigint\", 13),\n        (\"xact_rollback\", \"bigint\", 13),\n        (\"blks_read\", \"bigint\", 13),\n        (\"blks_hit\", \"bigint\", 13),\n        (\"tup_returned\", \"bigint\", 13),\n        (\"tup_fetched\", \"bigint\", 13),\n        (\"tup_inserted\", \"bigint\", 13),\n        (\"tup_updated\", \"bigint\", 13),\n        (\"tup_deleted\", \"bigint\", 13),\n        (\"conflicts\", \"bigint\", 13),\n        (\"temp_files\", \"bigint\", 13),\n        (\"temp_bytes\", \"bigint\", 13),\n        (\"deadlocks\", \"bigint\", 13),\n        (\"checksum_failures\", \"bigint\", 13),\n        (\"checksum_last_failure\", \"timestamp with time zone\", 13),\n        (\"blk_read_time\", \"double precision\", 13),\n        (\"blk_write_time\", \"double precision\", 13),\n        (\"session_time\", \"double precision\", 14),\n        (\"active_time\", \"double precision\", 14),\n        (\"idle_in_transaction_time\", \"double precision\", 14),\n        (\"sessions\", \"bigint\", 14),\n        (\"sessions_abandoned\", \"bigint\", 14),\n        (\"sessions_fatal\", \"bigint\", 14),\n        (\"sessions_killed\", \"bigint\", 14),\n        (\"stats_reset\", \"timestamp with time zone\", 13),\n    ],\n    \"pg_stat_database_conflicts\": [\n        (\"datid\", \"oid\", 13),\n        (\"datname\", \"name\", 13),\n        (\"confl_tablespace\", \"bigint\", 13),\n        (\"confl_lock\", \"bigint\", 13),\n        (\"confl_snapshot\", \"bigint\", 13),\n        (\"confl_bufferpin\", \"bigint\", 13),\n        (\"confl_deadlock\", \"bigint\", 13),\n        (\"confl_active_logicalslot\", \"bigint\", 16),\n    ],\n    \"pg_stat_gssapi\": [\n        (\"pid\", \"integer\", 13),\n        (\"gss_authenticated\", \"boolean\", 13),\n        (\"principal\", \"text\", 13),\n        (\"encrypted\", \"boolean\", 13),\n        (\"credentials_delegated\", \"boolean\", 16),\n    ],\n    \"pg_stat_io\": [\n        (\"backend_type\", \"text\", 16),\n        (\"object\", \"text\", 16),\n        (\"context\", \"text\", 16),\n        (\"reads\", \"bigint\", 16),\n        (\"read_time\", \"double precision\", 16),\n        (\"writes\", \"bigint\", 16),\n        (\"write_time\", \"double precision\", 16),\n        (\"writebacks\", \"bigint\", 16),\n        (\"writeback_time\", \"double precision\", 16),\n        (\"extends\", \"bigint\", 16),\n        (\"extend_time\", \"double precision\", 16),\n        (\"op_bytes\", \"bigint\", 16),\n        (\"hits\", \"bigint\", 16),\n        (\"evictions\", \"bigint\", 16),\n        (\"reuses\", \"bigint\", 16),\n        (\"fsyncs\", \"bigint\", 16),\n        (\"fsync_time\", \"double precision\", 16),\n        (\"stats_reset\", \"timestamp with time zone\", 16),\n    ],\n    \"pg_stat_progress_analyze\": [\n        (\"pid\", \"integer\", 13),\n        (\"datid\", \"oid\", 13),\n        (\"datname\", \"name\", 13),\n        (\"relid\", \"oid\", 13),\n        (\"phase\", \"text\", 13),\n        (\"sample_blks_total\", \"bigint\", 13),\n        (\"sample_blks_scanned\", \"bigint\", 13),\n        (\"ext_stats_total\", \"bigint\", 13),\n        (\"ext_stats_computed\", \"bigint\", 13),\n        (\"child_tables_total\", \"bigint\", 13),\n        (\"child_tables_done\", \"bigint\", 13),\n        (\"current_child_table_relid\", \"oid\", 13),\n    ],\n    \"pg_stat_progress_basebackup\": [\n        (\"pid\", \"integer\", 13),\n        (\"phase\", \"text\", 13),\n        (\"backup_total\", \"bigint\", 13),\n        (\"backup_streamed\", \"bigint\", 13),\n        (\"tablespaces_total\", \"bigint\", 13),\n        (\"tablespaces_streamed\", \"bigint\", 13),\n    ],\n    \"pg_stat_progress_cluster\": [\n        (\"pid\", \"integer\", 13),\n        (\"datid\", \"oid\", 13),\n        (\"datname\", \"name\", 13),\n        (\"relid\", \"oid\", 13),\n        (\"command\", \"text\", 13),\n        (\"phase\", \"text\", 13),\n        (\"cluster_index_relid\", \"oid\", 13),\n        (\"heap_tuples_scanned\", \"bigint\", 13),\n        (\"heap_tuples_written\", \"bigint\", 13),\n        (\"heap_blks_total\", \"bigint\", 13),\n        (\"heap_blks_scanned\", \"bigint\", 13),\n        (\"index_rebuild_count\", \"bigint\", 13),\n    ],\n    \"pg_stat_progress_copy\": [\n        (\"pid\", \"integer\", 14),\n        (\"datid\", \"oid\", 14),\n        (\"datname\", \"name\", 14),\n        (\"relid\", \"oid\", 14),\n        (\"command\", \"text\", 14),\n        (\"type\", \"text\", 14),\n        (\"bytes_processed\", \"bigint\", 14),\n        (\"bytes_total\", \"bigint\", 14),\n        (\"tuples_processed\", \"bigint\", 14),\n        (\"tuples_excluded\", \"bigint\", 14),\n        (\"tuples_skipped\", \"bigint\", 17),\n    ],\n    \"pg_stat_progress_create_index\": [\n        (\"pid\", \"integer\", 13),\n        (\"datid\", \"oid\", 13),\n        (\"datname\", \"name\", 13),\n        (\"relid\", \"oid\", 13),\n        (\"index_relid\", \"oid\", 13),\n        (\"command\", \"text\", 13),\n        (\"phase\", \"text\", 13),\n        (\"lockers_total\", \"bigint\", 13),\n        (\"lockers_done\", \"bigint\", 13),\n        (\"current_locker_pid\", \"bigint\", 13),\n        (\"blocks_total\", \"bigint\", 13),\n        (\"blocks_done\", \"bigint\", 13),\n        (\"tuples_total\", \"bigint\", 13),\n        (\"tuples_done\", \"bigint\", 13),\n        (\"partitions_total\", \"bigint\", 13),\n        (\"partitions_done\", \"bigint\", 13),\n    ],\n    \"pg_stat_progress_vacuum\": [\n        (\"pid\", \"integer\", 13),\n        (\"datid\", \"oid\", 13),\n        (\"datname\", \"name\", 13),\n        (\"relid\", \"oid\", 13),\n        (\"phase\", \"text\", 13),\n        (\"heap_blks_total\", \"bigint\", 13),\n        (\"heap_blks_scanned\", \"bigint\", 13),\n        (\"heap_blks_vacuumed\", \"bigint\", 13),\n        (\"index_vacuum_count\", \"bigint\", 13),\n        (\"max_dead_tuple_bytes\", \"bigint\", 17),\n        (\"dead_tuple_bytes\", \"bigint\", 17),\n        (\"num_dead_item_ids\", \"bigint\", 17),\n        (\"indexes_total\", \"bigint\", 17),\n        (\"indexes_processed\", \"bigint\", 17),\n    ],\n    \"pg_stat_recovery_prefetch\": [\n        (\"stats_reset\", \"timestamp with time zone\", 15),\n        (\"prefetch\", \"bigint\", 15),\n        (\"hit\", \"bigint\", 15),\n        (\"skip_init\", \"bigint\", 15),\n        (\"skip_new\", \"bigint\", 15),\n        (\"skip_fpw\", \"bigint\", 15),\n        (\"skip_rep\", \"bigint\", 15),\n        (\"wal_distance\", \"integer\", 15),\n        (\"block_distance\", \"integer\", 15),\n        (\"io_depth\", \"integer\", 15),\n    ],\n    \"pg_stat_replication\": [\n        (\"pid\", \"integer\", 13),\n        (\"usesysid\", \"oid\", 13),\n        (\"usename\", \"name\", 13),\n        (\"application_name\", \"text\", 13),\n        (\"client_addr\", \"inet\", 13),\n        (\"client_hostname\", \"text\", 13),\n        (\"client_port\", \"integer\", 13),\n        (\"backend_start\", \"timestamp with time zone\", 13),\n        (\"backend_xmin\", \"xid\", 13),\n        (\"state\", \"text\", 13),\n        (\"sent_lsn\", \"pg_lsn\", 13),\n        (\"write_lsn\", \"pg_lsn\", 13),\n        (\"flush_lsn\", \"pg_lsn\", 13),\n        (\"replay_lsn\", \"pg_lsn\", 13),\n        (\"write_lag\", \"interval\", 13),\n        (\"flush_lag\", \"interval\", 13),\n        (\"replay_lag\", \"interval\", 13),\n        (\"sync_priority\", \"integer\", 13),\n        (\"sync_state\", \"text\", 13),\n        (\"reply_time\", \"timestamp with time zone\", 13),\n    ],\n    \"pg_stat_replication_slots\": [\n        (\"slot_name\", \"text\", 14),\n        (\"spill_txns\", \"bigint\", 14),\n        (\"spill_count\", \"bigint\", 14),\n        (\"spill_bytes\", \"bigint\", 14),\n        (\"stream_txns\", \"bigint\", 14),\n        (\"stream_count\", \"bigint\", 14),\n        (\"stream_bytes\", \"bigint\", 14),\n        (\"total_txns\", \"bigint\", 14),\n        (\"total_bytes\", \"bigint\", 14),\n        (\"stats_reset\", \"timestamp with time zone\", 14),\n    ],\n    \"pg_stat_slru\": [\n        (\"name\", \"text\", 13),\n        (\"blks_zeroed\", \"bigint\", 13),\n        (\"blks_hit\", \"bigint\", 13),\n        (\"blks_read\", \"bigint\", 13),\n        (\"blks_written\", \"bigint\", 13),\n        (\"blks_exists\", \"bigint\", 13),\n        (\"flushes\", \"bigint\", 13),\n        (\"truncates\", \"bigint\", 13),\n        (\"stats_reset\", \"timestamp with time zone\", 13),\n    ],\n    \"pg_stat_ssl\": [\n        (\"pid\", \"integer\", 13),\n        (\"ssl\", \"boolean\", 13),\n        (\"version\", \"text\", 13),\n        (\"cipher\", \"text\", 13),\n        (\"bits\", \"integer\", 13),\n        (\"client_dn\", \"text\", 13),\n        (\"client_serial\", \"numeric\", 13),\n        (\"issuer_dn\", \"text\", 13),\n    ],\n    \"pg_stat_subscription\": [\n        (\"subid\", \"oid\", 13),\n        (\"subname\", \"name\", 13),\n        (\"worker_type\", \"text\", 17),\n        (\"pid\", \"integer\", 13),\n        (\"leader_pid\", \"integer\", 16),\n        (\"relid\", \"oid\", 13),\n        (\"received_lsn\", \"pg_lsn\", 13),\n        (\"last_msg_send_time\", \"timestamp with time zone\", 13),\n        (\"last_msg_receipt_time\", \"timestamp with time zone\", 13),\n        (\"latest_end_lsn\", \"pg_lsn\", 13),\n        (\"latest_end_time\", \"timestamp with time zone\", 13),\n    ],\n    \"pg_stat_subscription_stats\": [\n        (\"subid\", \"oid\", 15),\n        (\"subname\", \"name\", 15),\n        (\"apply_error_count\", \"bigint\", 15),\n        (\"sync_error_count\", \"bigint\", 15),\n        (\"stats_reset\", \"timestamp with time zone\", 15),\n    ],\n    \"pg_stat_sys_indexes\": [\n        (\"relid\", \"oid\", 13),\n        (\"indexrelid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"indexrelname\", \"name\", 13),\n        (\"idx_scan\", \"bigint\", 13),\n        (\"last_idx_scan\", \"timestamp with time zone\", 16),\n        (\"idx_tup_read\", \"bigint\", 13),\n        (\"idx_tup_fetch\", \"bigint\", 13),\n    ],\n    \"pg_stat_sys_tables\": [\n        (\"relid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"seq_scan\", \"bigint\", 13),\n        (\"last_seq_scan\", \"timestamp with time zone\", 16),\n        (\"seq_tup_read\", \"bigint\", 13),\n        (\"idx_scan\", \"bigint\", 13),\n        (\"last_idx_scan\", \"timestamp with time zone\", 16),\n        (\"idx_tup_fetch\", \"bigint\", 13),\n        (\"n_tup_ins\", \"bigint\", 13),\n        (\"n_tup_upd\", \"bigint\", 13),\n        (\"n_tup_del\", \"bigint\", 13),\n        (\"n_tup_hot_upd\", \"bigint\", 13),\n        (\"n_tup_newpage_upd\", \"bigint\", 16),\n        (\"n_live_tup\", \"bigint\", 13),\n        (\"n_dead_tup\", \"bigint\", 13),\n        (\"n_mod_since_analyze\", \"bigint\", 13),\n        (\"n_ins_since_vacuum\", \"bigint\", 13),\n        (\"last_vacuum\", \"timestamp with time zone\", 13),\n        (\"last_autovacuum\", \"timestamp with time zone\", 13),\n        (\"last_analyze\", \"timestamp with time zone\", 13),\n        (\"last_autoanalyze\", \"timestamp with time zone\", 13),\n        (\"vacuum_count\", \"bigint\", 13),\n        (\"autovacuum_count\", \"bigint\", 13),\n        (\"analyze_count\", \"bigint\", 13),\n        (\"autoanalyze_count\", \"bigint\", 13),\n    ],\n    \"pg_stat_user_functions\": [\n        (\"funcid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"funcname\", \"name\", 13),\n        (\"calls\", \"bigint\", 13),\n        (\"total_time\", \"double precision\", 13),\n        (\"self_time\", \"double precision\", 13),\n    ],\n    \"pg_stat_user_indexes\": [\n        (\"relid\", \"oid\", 13),\n        (\"indexrelid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"indexrelname\", \"name\", 13),\n        (\"idx_scan\", \"bigint\", 13),\n        (\"last_idx_scan\", \"timestamp with time zone\", 16),\n        (\"idx_tup_read\", \"bigint\", 13),\n        (\"idx_tup_fetch\", \"bigint\", 13),\n    ],\n    \"pg_stat_user_tables\": [\n        (\"relid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"seq_scan\", \"bigint\", 13),\n        (\"last_seq_scan\", \"timestamp with time zone\", 16),\n        (\"seq_tup_read\", \"bigint\", 13),\n        (\"idx_scan\", \"bigint\", 13),\n        (\"last_idx_scan\", \"timestamp with time zone\", 16),\n        (\"idx_tup_fetch\", \"bigint\", 13),\n        (\"n_tup_ins\", \"bigint\", 13),\n        (\"n_tup_upd\", \"bigint\", 13),\n        (\"n_tup_del\", \"bigint\", 13),\n        (\"n_tup_hot_upd\", \"bigint\", 13),\n        (\"n_tup_newpage_upd\", \"bigint\", 16),\n        (\"n_live_tup\", \"bigint\", 13),\n        (\"n_dead_tup\", \"bigint\", 13),\n        (\"n_mod_since_analyze\", \"bigint\", 13),\n        (\"n_ins_since_vacuum\", \"bigint\", 13),\n        (\"last_vacuum\", \"timestamp with time zone\", 13),\n        (\"last_autovacuum\", \"timestamp with time zone\", 13),\n        (\"last_analyze\", \"timestamp with time zone\", 13),\n        (\"last_autoanalyze\", \"timestamp with time zone\", 13),\n        (\"vacuum_count\", \"bigint\", 13),\n        (\"autovacuum_count\", \"bigint\", 13),\n        (\"analyze_count\", \"bigint\", 13),\n        (\"autoanalyze_count\", \"bigint\", 13),\n    ],\n    \"pg_stat_wal\": [\n        (\"wal_records\", \"bigint\", 14),\n        (\"wal_fpi\", \"bigint\", 14),\n        (\"wal_bytes\", \"numeric\", 14),\n        (\"wal_buffers_full\", \"bigint\", 14),\n        (\"wal_write\", \"bigint\", 14),\n        (\"wal_sync\", \"bigint\", 14),\n        (\"wal_write_time\", \"double precision\", 14),\n        (\"wal_sync_time\", \"double precision\", 14),\n        (\"stats_reset\", \"timestamp with time zone\", 14),\n    ],\n    \"pg_stat_wal_receiver\": [\n        (\"pid\", \"integer\", 13),\n        (\"status\", \"text\", 13),\n        (\"receive_start_lsn\", \"pg_lsn\", 13),\n        (\"receive_start_tli\", \"integer\", 13),\n        (\"written_lsn\", \"pg_lsn\", 13),\n        (\"flushed_lsn\", \"pg_lsn\", 13),\n        (\"received_tli\", \"integer\", 13),\n        (\"last_msg_send_time\", \"timestamp with time zone\", 13),\n        (\"last_msg_receipt_time\", \"timestamp with time zone\", 13),\n        (\"latest_end_lsn\", \"pg_lsn\", 13),\n        (\"latest_end_time\", \"timestamp with time zone\", 13),\n        (\"slot_name\", \"text\", 13),\n        (\"sender_host\", \"text\", 13),\n        (\"sender_port\", \"integer\", 13),\n        (\"conninfo\", \"text\", 13),\n    ],\n    \"pg_stat_xact_all_tables\": [\n        (\"relid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"seq_scan\", \"bigint\", 13),\n        (\"seq_tup_read\", \"bigint\", 13),\n        (\"idx_scan\", \"bigint\", 13),\n        (\"idx_tup_fetch\", \"bigint\", 13),\n        (\"n_tup_ins\", \"bigint\", 13),\n        (\"n_tup_upd\", \"bigint\", 13),\n        (\"n_tup_del\", \"bigint\", 13),\n        (\"n_tup_hot_upd\", \"bigint\", 13),\n        (\"n_tup_newpage_upd\", \"bigint\", 16),\n    ],\n    \"pg_stat_xact_sys_tables\": [\n        (\"relid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"seq_scan\", \"bigint\", 13),\n        (\"seq_tup_read\", \"bigint\", 13),\n        (\"idx_scan\", \"bigint\", 13),\n        (\"idx_tup_fetch\", \"bigint\", 13),\n        (\"n_tup_ins\", \"bigint\", 13),\n        (\"n_tup_upd\", \"bigint\", 13),\n        (\"n_tup_del\", \"bigint\", 13),\n        (\"n_tup_hot_upd\", \"bigint\", 13),\n        (\"n_tup_newpage_upd\", \"bigint\", 16),\n    ],\n    \"pg_stat_xact_user_functions\": [\n        (\"funcid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"funcname\", \"name\", 13),\n        (\"calls\", \"bigint\", 13),\n        (\"total_time\", \"double precision\", 13),\n        (\"self_time\", \"double precision\", 13),\n    ],\n    \"pg_stat_xact_user_tables\": [\n        (\"relid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"seq_scan\", \"bigint\", 13),\n        (\"seq_tup_read\", \"bigint\", 13),\n        (\"idx_scan\", \"bigint\", 13),\n        (\"idx_tup_fetch\", \"bigint\", 13),\n        (\"n_tup_ins\", \"bigint\", 13),\n        (\"n_tup_upd\", \"bigint\", 13),\n        (\"n_tup_del\", \"bigint\", 13),\n        (\"n_tup_hot_upd\", \"bigint\", 13),\n        (\"n_tup_newpage_upd\", \"bigint\", 16),\n    ],\n    \"pg_statio_all_indexes\": [\n        (\"relid\", \"oid\", 13),\n        (\"indexrelid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"indexrelname\", \"name\", 13),\n        (\"idx_blks_read\", \"bigint\", 13),\n        (\"idx_blks_hit\", \"bigint\", 13),\n    ],\n    \"pg_statio_all_sequences\": [\n        (\"relid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"blks_read\", \"bigint\", 13),\n        (\"blks_hit\", \"bigint\", 13),\n    ],\n    \"pg_statio_all_tables\": [\n        (\"relid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"heap_blks_read\", \"bigint\", 13),\n        (\"heap_blks_hit\", \"bigint\", 13),\n        (\"idx_blks_read\", \"bigint\", 13),\n        (\"idx_blks_hit\", \"bigint\", 13),\n        (\"toast_blks_read\", \"bigint\", 13),\n        (\"toast_blks_hit\", \"bigint\", 13),\n        (\"tidx_blks_read\", \"bigint\", 13),\n        (\"tidx_blks_hit\", \"bigint\", 13),\n    ],\n    \"pg_statio_sys_indexes\": [\n        (\"relid\", \"oid\", 13),\n        (\"indexrelid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"indexrelname\", \"name\", 13),\n        (\"idx_blks_read\", \"bigint\", 13),\n        (\"idx_blks_hit\", \"bigint\", 13),\n    ],\n    \"pg_statio_sys_sequences\": [\n        (\"relid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"blks_read\", \"bigint\", 13),\n        (\"blks_hit\", \"bigint\", 13),\n    ],\n    \"pg_statio_sys_tables\": [\n        (\"relid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"heap_blks_read\", \"bigint\", 13),\n        (\"heap_blks_hit\", \"bigint\", 13),\n        (\"idx_blks_read\", \"bigint\", 13),\n        (\"idx_blks_hit\", \"bigint\", 13),\n        (\"toast_blks_read\", \"bigint\", 13),\n        (\"toast_blks_hit\", \"bigint\", 13),\n        (\"tidx_blks_read\", \"bigint\", 13),\n        (\"tidx_blks_hit\", \"bigint\", 13),\n    ],\n    \"pg_statio_user_indexes\": [\n        (\"relid\", \"oid\", 13),\n        (\"indexrelid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"indexrelname\", \"name\", 13),\n        (\"idx_blks_read\", \"bigint\", 13),\n        (\"idx_blks_hit\", \"bigint\", 13),\n    ],\n    \"pg_statio_user_sequences\": [\n        (\"relid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"blks_read\", \"bigint\", 13),\n        (\"blks_hit\", \"bigint\", 13),\n    ],\n    \"pg_statio_user_tables\": [\n        (\"relid\", \"oid\", 13),\n        (\"schemaname\", \"name\", 13),\n        (\"relname\", \"name\", 13),\n        (\"heap_blks_read\", \"bigint\", 13),\n        (\"heap_blks_hit\", \"bigint\", 13),\n        (\"idx_blks_read\", \"bigint\", 13),\n        (\"idx_blks_hit\", \"bigint\", 13),\n        (\"toast_blks_read\", \"bigint\", 13),\n        (\"toast_blks_hit\", \"bigint\", 13),\n        (\"tidx_blks_read\", \"bigint\", 13),\n        (\"tidx_blks_hit\", \"bigint\", 13),\n    ],\n    \"pg_statistic\": [\n        (\"starelid\", \"oid\", 13),\n        (\"staattnum\", \"smallint\", 13),\n        (\"stainherit\", \"boolean\", 13),\n        (\"stanullfrac\", \"real\", 13),\n        (\"stawidth\", \"integer\", 13),\n        (\"stadistinct\", \"real\", 13),\n        (\"stakind1\", \"smallint\", 13),\n        (\"stakind2\", \"smallint\", 13),\n        (\"stakind3\", \"smallint\", 13),\n        (\"stakind4\", \"smallint\", 13),\n        (\"stakind5\", \"smallint\", 13),\n        (\"staop1\", \"oid\", 13),\n        (\"staop2\", \"oid\", 13),\n        (\"staop3\", \"oid\", 13),\n        (\"staop4\", \"oid\", 13),\n        (\"staop5\", \"oid\", 13),\n        (\"stacoll1\", \"oid\", 13),\n        (\"stacoll2\", \"oid\", 13),\n        (\"stacoll3\", \"oid\", 13),\n        (\"stacoll4\", \"oid\", 13),\n        (\"stacoll5\", \"oid\", 13),\n        (\"stanumbers1\", None, 13),\n        (\"stanumbers2\", None, 13),\n        (\"stanumbers3\", None, 13),\n        (\"stanumbers4\", None, 13),\n        (\"stanumbers5\", None, 13),\n        (\"stavalues1\", None, 13),\n        (\"stavalues2\", None, 13),\n        (\"stavalues3\", None, 13),\n        (\"stavalues4\", None, 13),\n        (\"stavalues5\", None, 13),\n    ],\n    \"pg_statistic_ext\": [\n        (\"oid\", \"oid\", 13),\n        (\"stxrelid\", \"oid\", 13),\n        (\"stxname\", \"name\", 13),\n        (\"stxnamespace\", \"oid\", 13),\n        (\"stxowner\", \"oid\", 13),\n        (\"stxkeys\", None, 13),\n        (\"stxstattarget\", \"smallint\", 13),\n        (\"stxkind\", None, 13),\n        (\"stxexprs\", \"pg_node_tree\", 14),\n    ],\n    \"pg_statistic_ext_data\": [\n        (\"stxoid\", \"oid\", 13),\n        (\"stxdinherit\", \"boolean\", 15),\n        (\"stxdndistinct\", \"pg_ndistinct\", 13),\n        (\"stxddependencies\", \"pg_dependencies\", 13),\n        (\"stxdmcv\", \"pg_mcv_list\", 13),\n        (\"stxdexpr\", None, 14),\n    ],\n    \"pg_stats\": [\n        (\"schemaname\", \"name\", 13),\n        (\"tablename\", \"name\", 13),\n        (\"attname\", \"name\", 13),\n        (\"inherited\", \"boolean\", 13),\n        (\"null_frac\", \"real\", 13),\n        (\"avg_width\", \"integer\", 13),\n        (\"n_distinct\", \"real\", 13),\n        (\"most_common_vals\", None, 13),\n        (\"most_common_freqs\", None, 13),\n        (\"histogram_bounds\", None, 13),\n        (\"correlation\", \"real\", 13),\n        (\"most_common_elems\", None, 13),\n        (\"most_common_elem_freqs\", None, 13),\n        (\"elem_count_histogram\", None, 13),\n        (\"range_length_histogram\", None, 17),\n        (\"range_empty_frac\", \"real\", 17),\n        (\"range_bounds_histogram\", None, 17),\n    ],\n    \"pg_stats_ext\": [\n        (\"schemaname\", \"name\", 13),\n        (\"tablename\", \"name\", 13),\n        (\"statistics_schemaname\", \"name\", 13),\n        (\"statistics_name\", \"name\", 13),\n        (\"statistics_owner\", \"name\", 13),\n        (\"attnames\", None, 13),\n        (\"exprs\", None, 14),\n        (\"kinds\", None, 13),\n        (\"inherited\", \"boolean\", 15),\n        (\"n_distinct\", \"pg_ndistinct\", 13),\n        (\"dependencies\", \"pg_dependencies\", 13),\n        (\"most_common_vals\", None, 13),\n        (\"most_common_val_nulls\", None, 13),\n        (\"most_common_freqs\", None, 13),\n        (\"most_common_base_freqs\", None, 13),\n    ],\n    \"pg_stats_ext_exprs\": [\n        (\"schemaname\", \"name\", 14),\n        (\"tablename\", \"name\", 14),\n        (\"statistics_schemaname\", \"name\", 14),\n        (\"statistics_name\", \"name\", 14),\n        (\"statistics_owner\", \"name\", 14),\n        (\"expr\", \"text\", 14),\n        (\"inherited\", \"boolean\", 15),\n        (\"null_frac\", \"real\", 14),\n        (\"avg_width\", \"integer\", 14),\n        (\"n_distinct\", \"real\", 14),\n        (\"most_common_vals\", None, 14),\n        (\"most_common_freqs\", None, 14),\n        (\"histogram_bounds\", None, 14),\n        (\"correlation\", \"real\", 14),\n        (\"most_common_elems\", None, 14),\n        (\"most_common_elem_freqs\", None, 14),\n        (\"elem_count_histogram\", None, 14),\n    ],\n    \"pg_subscription\": [\n        (\"oid\", \"oid\", 13),\n        (\"subdbid\", \"oid\", 13),\n        (\"subskiplsn\", \"pg_lsn\", 15),\n        (\"subname\", \"name\", 13),\n        (\"subowner\", \"oid\", 13),\n        (\"subenabled\", \"boolean\", 13),\n        (\"subbinary\", \"boolean\", 14),\n        (\"substream\", \"\\\"char\\\"\", 14),\n        (\"subtwophasestate\", \"\\\"char\\\"\", 15),\n        (\"subdisableonerr\", \"boolean\", 15),\n        (\"subpasswordrequired\", \"boolean\", 16),\n        (\"subrunasowner\", \"boolean\", 16),\n        (\"subfailover\", \"boolean\", 17),\n        (\"subconninfo\", \"text\", 13),\n        (\"subslotname\", \"name\", 13),\n        (\"subsynccommit\", \"text\", 13),\n        (\"subpublications\", None, 13),\n        (\"suborigin\", \"text\", 16),\n    ],\n    \"pg_subscription_rel\": [\n        (\"srsubid\", \"oid\", 13),\n        (\"srrelid\", \"oid\", 13),\n        (\"srsubstate\", \"\\\"char\\\"\", 13),\n        (\"srsublsn\", \"pg_lsn\", 13),\n    ],\n    \"pg_tables\": [\n        (\"schemaname\", \"name\", 13),\n        (\"tablename\", \"name\", 13),\n        (\"tableowner\", \"name\", 13),\n        (\"tablespace\", \"name\", 13),\n        (\"hasindexes\", \"boolean\", 13),\n        (\"hasrules\", \"boolean\", 13),\n        (\"hastriggers\", \"boolean\", 13),\n        (\"rowsecurity\", \"boolean\", 13),\n    ],\n    \"pg_tablespace\": [\n        (\"oid\", \"oid\", 13),\n        (\"spcname\", \"name\", 13),\n        (\"spcowner\", \"oid\", 13),\n        (\"spcacl\", None, 13),\n        (\"spcoptions\", None, 13),\n    ],\n    \"pg_timezone_abbrevs\": [\n        (\"abbrev\", \"text\", 13),\n        (\"utc_offset\", \"interval\", 13),\n        (\"is_dst\", \"boolean\", 13),\n    ],\n    \"pg_timezone_names\": [\n        (\"name\", \"text\", 13),\n        (\"abbrev\", \"text\", 13),\n        (\"utc_offset\", \"interval\", 13),\n        (\"is_dst\", \"boolean\", 13),\n    ],\n    \"pg_transform\": [\n        (\"oid\", \"oid\", 13),\n        (\"trftype\", \"oid\", 13),\n        (\"trflang\", \"oid\", 13),\n        (\"trffromsql\", \"regproc\", 13),\n        (\"trftosql\", \"regproc\", 13),\n    ],\n    \"pg_trigger\": [\n        (\"oid\", \"oid\", 13),\n        (\"tgrelid\", \"oid\", 13),\n        (\"tgparentid\", \"oid\", 13),\n        (\"tgname\", \"name\", 13),\n        (\"tgfoid\", \"oid\", 13),\n        (\"tgtype\", \"smallint\", 13),\n        (\"tgenabled\", \"\\\"char\\\"\", 13),\n        (\"tgisinternal\", \"boolean\", 13),\n        (\"tgconstrrelid\", \"oid\", 13),\n        (\"tgconstrindid\", \"oid\", 13),\n        (\"tgconstraint\", \"oid\", 13),\n        (\"tgdeferrable\", \"boolean\", 13),\n        (\"tginitdeferred\", \"boolean\", 13),\n        (\"tgnargs\", \"smallint\", 13),\n        (\"tgattr\", None, 13),\n        (\"tgargs\", \"bytea\", 13),\n        (\"tgqual\", \"pg_node_tree\", 13),\n        (\"tgoldtable\", \"name\", 13),\n        (\"tgnewtable\", \"name\", 13),\n    ],\n    \"pg_ts_config\": [\n        (\"oid\", \"oid\", 13),\n        (\"cfgname\", \"name\", 13),\n        (\"cfgnamespace\", \"oid\", 13),\n        (\"cfgowner\", \"oid\", 13),\n        (\"cfgparser\", \"oid\", 13),\n    ],\n    \"pg_ts_config_map\": [\n        (\"mapcfg\", \"oid\", 13),\n        (\"maptokentype\", \"integer\", 13),\n        (\"mapseqno\", \"integer\", 13),\n        (\"mapdict\", \"oid\", 13),\n    ],\n    \"pg_ts_dict\": [\n        (\"oid\", \"oid\", 13),\n        (\"dictname\", \"name\", 13),\n        (\"dictnamespace\", \"oid\", 13),\n        (\"dictowner\", \"oid\", 13),\n        (\"dicttemplate\", \"oid\", 13),\n        (\"dictinitoption\", \"text\", 13),\n    ],\n    \"pg_ts_parser\": [\n        (\"oid\", \"oid\", 13),\n        (\"prsname\", \"name\", 13),\n        (\"prsnamespace\", \"oid\", 13),\n        (\"prsstart\", \"regproc\", 13),\n        (\"prstoken\", \"regproc\", 13),\n        (\"prsend\", \"regproc\", 13),\n        (\"prsheadline\", \"regproc\", 13),\n        (\"prslextype\", \"regproc\", 13),\n    ],\n    \"pg_ts_template\": [\n        (\"oid\", \"oid\", 13),\n        (\"tmplname\", \"name\", 13),\n        (\"tmplnamespace\", \"oid\", 13),\n        (\"tmplinit\", \"regproc\", 13),\n        (\"tmpllexize\", \"regproc\", 13),\n    ],\n    \"pg_type\": [\n        (\"oid\", \"oid\", 13),\n        (\"typname\", \"name\", 13),\n        (\"typnamespace\", \"oid\", 13),\n        (\"typowner\", \"oid\", 13),\n        (\"typlen\", \"smallint\", 13),\n        (\"typbyval\", \"boolean\", 13),\n        (\"typtype\", \"\\\"char\\\"\", 13),\n        (\"typcategory\", \"\\\"char\\\"\", 13),\n        (\"typispreferred\", \"boolean\", 13),\n        (\"typisdefined\", \"boolean\", 13),\n        (\"typdelim\", \"\\\"char\\\"\", 13),\n        (\"typrelid\", \"oid\", 13),\n        (\"typsubscript\", \"regproc\", 14),\n        (\"typelem\", \"oid\", 13),\n        (\"typarray\", \"oid\", 13),\n        (\"typinput\", \"regproc\", 13),\n        (\"typoutput\", \"regproc\", 13),\n        (\"typreceive\", \"regproc\", 13),\n        (\"typsend\", \"regproc\", 13),\n        (\"typmodin\", \"regproc\", 13),\n        (\"typmodout\", \"regproc\", 13),\n        (\"typanalyze\", \"regproc\", 13),\n        (\"typalign\", \"\\\"char\\\"\", 13),\n        (\"typstorage\", \"\\\"char\\\"\", 13),\n        (\"typnotnull\", \"boolean\", 13),\n        (\"typbasetype\", \"oid\", 13),\n        (\"typtypmod\", \"integer\", 13),\n        (\"typndims\", \"integer\", 13),\n        (\"typcollation\", \"oid\", 13),\n        (\"typdefaultbin\", \"pg_node_tree\", 13),\n        (\"typdefault\", \"text\", 13),\n        (\"typacl\", None, 13),\n    ],\n    \"pg_user\": [\n        (\"usename\", \"name\", 13),\n        (\"usesysid\", \"oid\", 13),\n        (\"usecreatedb\", \"boolean\", 13),\n        (\"usesuper\", \"boolean\", 13),\n        (\"userepl\", \"boolean\", 13),\n        (\"usebypassrls\", \"boolean\", 13),\n        (\"passwd\", \"text\", 13),\n        (\"valuntil\", \"timestamp with time zone\", 13),\n        (\"useconfig\", None, 13),\n    ],\n    \"pg_user_mapping\": [\n        (\"oid\", \"oid\", 13),\n        (\"umuser\", \"oid\", 13),\n        (\"umserver\", \"oid\", 13),\n        (\"umoptions\", None, 13),\n    ],\n    \"pg_user_mappings\": [\n        (\"umid\", \"oid\", 13),\n        (\"srvid\", \"oid\", 13),\n        (\"srvname\", \"name\", 13),\n        (\"umuser\", \"oid\", 13),\n        (\"usename\", \"name\", 13),\n        (\"umoptions\", None, 13),\n    ],\n    \"pg_views\": [\n        (\"schemaname\", \"name\", 13),\n        (\"viewname\", \"name\", 13),\n        (\"viewowner\", \"name\", 13),\n        (\"definition\", \"text\", 13),\n    ],\n    \"pg_wait_events\": [\n        (\"type\", \"text\", 17),\n        (\"name\", \"text\", 17),\n        (\"description\", \"text\", 17),\n    ]\n}\n"
  },
  {
    "path": "edb/pgsql/resolver/static.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Static evaluation for SQL.\"\"\"\n\nimport functools\nimport platform\nfrom typing import Optional, Sequence\n\nfrom edb import errors\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql.ast import SQLValueFunctionOP as val_func_op\nfrom edb.pgsql import common\n\nfrom edb.server import defines\nfrom edb.server.pgcon import errors as pgerror\nfrom edb.server.compiler import enums\nfrom edb.server.compiler.sql import DisableNormalization\n\nfrom . import context\nfrom . import dispatch\n\nV = common.versioned_schema\n\nContext = context.ResolverContextLevel\n\n\n@functools.singledispatch\ndef eval(expr: pgast.BaseExpr, *, ctx: Context) -> Optional[pgast.BaseExpr]:\n    \"\"\"\n    Tries to statically evaluate expr, recursing into sub-expressions.\n    Returns None if that is not possible.\n    \"\"\"\n    return None\n\n\ndef eval_list(\n    exprs: list[pgast.BaseExpr], *, ctx: Context\n) -> Optional[list[pgast.BaseExpr]]:\n    \"\"\"\n    Tries to statically evaluate exprs, recursing into sub-expressions.\n    Returns None if that is not possible.\n    Raises DisableNormalization if param refs are encountered.\n    \"\"\"\n    res = []\n    for expr in exprs:\n        r = eval(expr, ctx=ctx)\n        if not r:\n            return None\n        res.append(r)\n    return res\n\n\ndef name_in_pg_catalog(name: Sequence[str]) -> Optional[str]:\n    \"\"\"\n    Strips `pg_catalog.` schema name from an SQL ident that resides in\n    pg_catalog. If name is unqualified, it is deemed to be in pg_catalog,\n    because pg_catalog is always the first schema in search_path.\n    \"\"\"\n\n    if len(name) == 1 or name[0] == 'pg_catalog':\n        return name[-1]\n    return None\n\n\n@eval.register\ndef eval_BaseConstant(\n    expr: pgast.BaseConstant, *, ctx: Context\n) -> Optional[pgast.BaseExpr]:\n    return expr\n\n\n@eval.register\ndef eval_TypeCast(\n    expr: pgast.TypeCast, *, ctx: Context\n) -> Optional[pgast.BaseExpr]:\n    if expr.type_name.array_bounds:\n        return None\n\n    pg_catalog_name = name_in_pg_catalog(expr.type_name.name)\n    if pg_catalog_name == 'regclass':\n        return cast_to_regclass(expr.arg, ctx)\n\n    arg = eval(expr.arg, ctx=ctx)\n    if not arg:\n        return None\n\n    if isinstance(arg, pgast.StringConstant):\n\n        type_name = name_in_pg_catalog(expr.type_name.name)\n\n        if type_name == 'text':\n            return arg\n\n        if type_name == 'bool':\n            string = arg.val.lower()\n\n            if 'true'.startswith(string) or 'yes'.startswith(string):\n                return pgast.BooleanConstant(val=True)\n\n            if 'false'.startswith(string) or 'no'.startswith(string):\n                return pgast.BooleanConstant(val=False)\n            raise errors.QueryError('invalid cast', span=expr.arg.span)\n\n    return None\n\n\n# Functions that are inquiring about privileges of users or schemas.\n# Dict from function name into number of trailing arguments that are passed\n# trough.\nPRIVILEGE_INQUIRY_FUNCTIONS_ARGS = {\n    'has_any_column_privilege': 2,\n    'has_column_privilege': 3,\n    'has_database_privilege': 2,\n    'has_foreign_data_wrapper_privilege': 2,\n    'has_function_privilege': 2,\n    'has_language_privilege': 2,\n    'has_parameter_privilege': 2,\n    'has_schema_privilege': 2,\n    'has_sequence_privilege': 2,\n    'has_server_privilege': 2,\n    'has_table_privilege': 2,\n    'has_tablespace_privilege': 2,\n    'has_type_privilege': 2,\n    'pg_has_role': 2,\n}\n\n# Allowed functions from pg_catalog that start with `pg_`.\n# By default, all such functions are forbidden by default.\n# To see the list of forbidden functions, use `edb ls-forbidden-functions`.\nALLOWED_ADMIN_FUNCTIONS = frozenset(\n    {\n        'pg_is_in_recovery',\n        'pg_is_wal_replay_paused',\n        'pg_get_wal_replay_pause_state',\n        'pg_column_size',\n        'pg_column_compression',\n        'pg_database_size',\n        'pg_indexes_size',\n        'pg_relation_size',\n        'pg_size_bytes',\n        'pg_size_pretty',\n        'pg_table_size',\n        'pg_tablespace_size',\n        'pg_total_relation_size',\n        'pg_relation_filenode',\n        'pg_relation_filepath',\n        'pg_filenode_relation',\n        'pg_char_to_encoding',\n        'pg_column_is_updatable',\n        'pg_conf_load_time',\n        'pg_current_xact_id',\n        'pg_current_xact_id_if_assigned',\n        'pg_describe_object',\n        'pg_encoding_max_length',\n        'pg_encoding_to_char',\n        'pg_get_constraintdef',\n        'pg_get_expr',\n        'pg_get_function_arg_default',\n        'pg_get_function_arguments',\n        'pg_get_function_identity_arguments',\n        'pg_get_function_result',\n        'pg_get_functiondef',\n        'pg_get_indexdef',\n        'pg_get_keywords',\n        'pg_get_multixact_members',\n        'pg_get_object_address',\n        'pg_get_partition_constraintdef',\n        'pg_get_partkeydef',\n        'pg_get_publication_tables',\n        'pg_get_replica_identity_index',\n        'pg_get_replication_slots',\n        'pg_get_ruledef',\n        'pg_get_serial_sequence',\n        'pg_get_shmem_allocations',\n        'pg_get_statisticsobjdef',\n        'pg_get_triggerdef',\n        'pg_get_userbyid',\n        'pg_get_viewdef',\n        'pg_options_to_table',\n        'pg_has_role',\n        'pg_function_is_visible',\n        'pg_opclass_is_visible',\n        'pg_operator_is_visible',\n        'pg_opfamily_is_visible',\n        'pg_statistics_obj_is_visible',\n        'pg_table_is_visible',\n        'pg_ts_config_is_visible',\n        'pg_ts_dict_is_visible',\n        'pg_ts_parser_is_visible',\n        'pg_ts_template_is_visible',\n        'pg_type_is_visible',\n        'pg_index_column_has_property',\n        'pg_index_has_property',\n        'pg_is_in_backup',\n        'pg_is_other_temp_schema',\n        'pg_jit_available',\n        'pg_relation_is_updatable',\n        'pg_sequence_last_value',\n        'pg_sequence_parameters',\n        'pg_timezone_abbrevs',\n        'pg_timezone_names',\n        'pg_typeof',\n        'pg_visible_in_snapshot',\n        'pg_xact_commit_timestamp',\n        'pg_xact_status',\n        'pg_partition_ancestors',\n        'pg_backend_pid',\n        'pg_wal_lsn_diff',\n        'pg_last_wal_replay_lsn',\n        'pg_current_wal_flush_lsn',\n        'pg_relation_is_publishable',\n        'pg_show_all_settings',\n    }\n)\n\nWRAPPED_FUNCTIONS = frozenset(\n    {\n        \"to_regclass\",\n        'pg_show_all_settings',\n    }\n)\n\n\n@eval.register\ndef eval_FuncCall(\n    expr: pgast.FuncCall,\n    *,\n    ctx: Context,\n) -> Optional[pgast.BaseExpr]:\n    if len(expr.name) >= 3:\n        raise errors.QueryError(\"unknown function\", span=expr.span)\n\n    fn_name = name_in_pg_catalog(expr.name)\n    if not fn_name:\n        return None\n\n    if fn_name.startswith('pg_') and fn_name not in ALLOWED_ADMIN_FUNCTIONS:\n        raise errors.QueryError(\n            f\"forbidden function '{fn_name}'\",\n            span=expr.span,\n            pgext_code=pgerror.ERROR_INSUFFICIENT_PRIVILEGE,\n        )\n\n    if fn_name == 'current_schemas':\n        return eval_current_schemas(expr, ctx=ctx)\n\n    if fn_name == 'current_database':\n        return pgast.StringConstant(val=ctx.options.current_database)\n\n    if fn_name == 'current_query':\n        return pgast.StringConstant(val=ctx.options.current_query)\n\n    if fn_name == 'version':\n        from edb import buildmeta\n\n        edgedb_version = buildmeta.get_version_line()\n\n        return pgast.StringConstant(\n            val=\" \".join(\n                [\n                    \"PostgreSQL\",\n                    str(defines.PGEXT_POSTGRES_VERSION),\n                    f\"(Gel {edgedb_version}),\",\n                    platform.architecture()[0],\n                ]\n            ),\n        )\n\n    if fn_name == \"set_config\":\n        # HACK: pg_dump\n        #       - set_config('search_path', '', false)\n        #       - set_config(name, 'view, foreign-table', false)\n        # HACK: pgadmin\n        #       - set_config('bytea_output','hex',false)\n        # HACK: asyncpg\n        #       - set_config('jit', ...)\n        ctx.env.capabilities |= enums.Capability.SQL_SESSION_CONFIG\n\n        if args := eval_list(expr.args, ctx=ctx):\n            name, value, is_local = args\n            if isinstance(name, pgast.StringConstant):\n                if (\n                    isinstance(value, pgast.StringConstant)\n                    and isinstance(is_local, pgast.BooleanConstant)\n                ):\n                    if (\n                        name.val == \"search_path\"\n                        and value.val == \"\"\n                        and not is_local.val\n                    ):\n                        return value\n\n                    if (\n                        name.val == \"bytea_output\"\n                        and value.val == \"hex\"\n                        and not is_local.val\n                    ):\n                        return value\n\n                if name.val == \"jit\":\n                    return value\n\n        elif args := eval_list(expr.args[1:], ctx=ctx):\n            value, is_local = args\n            if (\n                isinstance(value, pgast.StringConstant)\n                and isinstance(is_local, pgast.BooleanConstant)\n            ):\n                if (\n                    value.val == \"view, foreign-table\"\n                    and not is_local.val\n                ):\n                    return value\n\n        raise errors.QueryError(\n            \"function set_config is not supported\",\n            span=expr.span,\n            pgext_code=pgerror.ERROR_FEATURE_NOT_SUPPORTED,\n        )\n\n    if fn_name == 'current_setting':\n        arg = require_string_param(expr, ctx)\n\n        val = None\n        if arg == 'search_path':\n            val = ', '.join(ctx.options.search_path)\n        if val:\n            return pgast.StringConstant(val=val)\n        return expr\n\n    if fn_name == \"pg_filenode_relation\":\n        raise errors.QueryError(\n            f\"function pg_catalog.{fn_name} is not supported\",\n            span=expr.span,\n            pgext_code=pgerror.ERROR_FEATURE_NOT_SUPPORTED,\n        )\n\n    if fn_name == \"pg_get_serial_sequence\":\n        eval_list(expr.args, ctx=ctx)\n        # we do not expose sequences, so any calls to this function returns NULL\n        return pgast.NullConstant()\n\n    if fn_name in WRAPPED_FUNCTIONS:\n        args = eval_list(expr.args, ctx=ctx)\n        return pgast.FuncCall(\n            name=(V('edgedbsql'), fn_name),\n            args=expr.args if args is None else args,\n        )\n\n    cast_arg_to_regclass = {\n        'pg_relation_filenode',\n        'pg_relation_filepath',\n        'pg_relation_size',\n    }\n\n    if fn_name in cast_arg_to_regclass:\n        regclass_oid = cast_to_regclass(expr.args[0], ctx=ctx)\n        return pgast.FuncCall(\n            name=('pg_catalog', fn_name), args=[regclass_oid]\n        )\n\n    if num_allowed_args := PRIVILEGE_INQUIRY_FUNCTIONS_ARGS.get(fn_name, None):\n        # For privilege inquiry functions, we strip the leading user (role),\n        # so the inquiry refers to current user's privileges.\n        # This is needed because the exposed username is not necessarily the\n        # same as the user we use to connect to Postgres instance.\n        # We do not allow creating additional users, so this should not be a\n        # problem anyway.\n        # See: https://www.postgresql.org/docs/15/functions-info.html\n\n        # TODO: deny INSERT, UPDATE and all other unsupported functions\n        fn_args = expr.args[-num_allowed_args:]\n        fn_args = dispatch.resolve_list(fn_args, ctx=ctx)\n\n        # schema and table names need to be remapped. This is accomplished\n        # with wrapper functions defined in metaschema.py.\n        has_wrapper = {\n            'has_database_privilege',\n            'has_schema_privilege',\n            'has_table_privilege',\n            'has_column_privilege',\n            'has_any_column_privilege',\n        }\n        if fn_name in has_wrapper:\n            return pgast.FuncCall(name=(V('edgedbsql'), fn_name), args=fn_args)\n\n        return pgast.FuncCall(name=('pg_catalog', fn_name), args=fn_args)\n\n    if fn_name == 'pg_table_is_visible':\n        arg_0 = dispatch.resolve(expr.args[0], ctx=ctx)\n\n        # our *_is_visible functions need search_path, passed in as an array\n        arg_1 = pgast.ArrayExpr(\n            elements=[\n                pgast.StringConstant(val=v)\n                for v in ctx.options.search_path\n            ]\n        )\n\n        return pgast.FuncCall(\n            name=(V('edgedbsql'), fn_name),\n            args=[arg_0, arg_1]\n        )\n\n    return None\n\n\ndef require_string_param(\n    expr: pgast.FuncCall, ctx: Context\n) -> str:\n    args = eval_list(expr.args, ctx=ctx)\n\n    arg = args[0] if args and len(args) == 1 else None\n    if not isinstance(arg, pgast.StringConstant):\n        raise errors.QueryError(\n            f\"function pg_catalog.{expr.name[-1]} requires a string literal\",\n            span=expr.span,\n            pgext_code=pgerror.ERROR_UNDEFINED_FUNCTION\n        )\n\n    return arg.val\n\n\ndef require_bool_param(\n    expr: pgast.FuncCall, ctx: Context\n) -> bool:\n    args = eval_list(expr.args, ctx=ctx)\n\n    arg = args[0] if args and len(args) == 1 else None\n    if not isinstance(arg, pgast.BooleanConstant):\n        raise errors.QueryError(\n            f\"function pg_catalog.{expr.name[-1]} requires a boolean literal\",\n            span=expr.span,\n            pgext_code=pgerror.ERROR_UNDEFINED_FUNCTION\n        )\n\n    return arg.val\n\n\ndef cast_to_regclass(param: pgast.BaseExpr, ctx: Context) -> pgast.BaseExpr:\n    \"\"\"\n    Equivalent to `::regclass` in SQL.\n\n    Converts a string constant or a oid to a \"registered class\"\n    (fully-qualified name of the table/index/sequence).\n    In practice, type of resulting expression is oid.\n    \"\"\"\n\n    expr = eval(param, ctx=ctx)\n    if expr is None:\n        param = dispatch.resolve(param, ctx=ctx)\n        return pgast.FuncCall(\n            name=(V('edgedbsql'), \"to_regclass\"), args=[param]\n        )\n    elif isinstance(expr, pgast.NullConstant):\n        return pgast.NullConstant()\n    elif isinstance(expr, pgast.StringConstant):\n        return pgast.FuncCall(\n            name=(V('edgedbsql'), \"to_regclass\"),\n            args=[expr],\n        )\n    elif isinstance(expr, pgast.NumericConstant):\n        return pgast.TypeCast(\n            arg=expr,\n            type_name=pgast.TypeName(name=('pg_catalog', 'regclass')),\n        )\n    else:\n        return pgast.FuncCall(\n            name=(V('edgedbsql'), \"to_regclass\"), args=[expr]\n        )\n\n\ndef eval_current_schemas(\n    expr: pgast.FuncCall, ctx: Context\n) -> Optional[pgast.BaseExpr]:\n    include_implicit = require_bool_param(expr, ctx)\n\n    res = []\n    if include_implicit:\n        # if any temporary object has been created in current session,\n        # here we should also append res.append('pg_temp_xxx') were xxx is\n        # a number assigned by the server.\n\n        res.append('pg_catalog')\n    res.extend(ctx.options.search_path)\n\n    return pgast.ArrayExpr(elements=[pgast.StringConstant(val=r) for r in res])\n\n\nVALUE_FUNC_PASS_THROUGH = frozenset({\n    val_func_op.CURRENT_DATE,\n    val_func_op.CURRENT_TIME,\n    val_func_op.CURRENT_TIME_N,\n    val_func_op.CURRENT_TIMESTAMP,\n    val_func_op.CURRENT_TIMESTAMP_N,\n    val_func_op.LOCALTIME,\n    val_func_op.LOCALTIME_N,\n    val_func_op.LOCALTIMESTAMP,\n    val_func_op.LOCALTIMESTAMP_N,\n})\n\nVALUE_FUNC_USER = frozenset({\n    val_func_op.CURRENT_ROLE,\n    val_func_op.CURRENT_USER,\n    val_func_op.USER,\n    val_func_op.SESSION_USER,\n})\n\n\ndef eval_current_user(\n    *,\n    ctx: Context,\n) -> pgast.BaseExpr:\n    from edb.edgeql import ast as qlast\n    from edb.edgeql import compiler as qlcompiler\n    from edb.pgsql import compiler as pgcompiler\n    from . import command\n\n    ql_stmt = qlast.SelectQuery(\n        result=qlast.GlobalExpr(\n            name=qlast.ObjectRef(module='sys', name='current_role'),\n        )\n    )\n    ir_stmt = qlcompiler.compile_ast_to_ir(\n        ql_stmt,\n        ctx.schema,\n        options=qlcompiler.CompilerOptions(),\n    )\n\n    sql_tree = pgcompiler.compile_ir_to_sql_tree(\n        ir_stmt,\n        output_format=pgcompiler.OutputFormat.NATIVE_INTERNAL,\n        alias_generator=ctx.alias_generator,\n    )\n    command.merge_params(sql_tree, ir_stmt, ctx)\n\n    assert isinstance(sql_tree.ast, pgast.BaseExpr)\n    return sql_tree.ast\n\n\n@eval.register\ndef eval_SQLValueFunction(\n    expr: pgast.SQLValueFunction,\n    *,\n    ctx: Context,\n) -> pgast.BaseExpr:\n    if expr.op in VALUE_FUNC_PASS_THROUGH:\n        return expr\n\n    if expr.op in VALUE_FUNC_USER:\n        return eval_current_user(ctx=ctx)\n\n    if expr.op == val_func_op.CURRENT_CATALOG:\n        return pgast.StringConstant(val=ctx.options.current_database)\n\n    if expr.op == val_func_op.CURRENT_SCHEMA:\n        # note: PG also does a check that this schema exists and proceeds to\n        # the next one in the search path\n        return pgast.StringConstant(val=ctx.options.search_path[0])\n\n    # this should never happen\n    raise NotImplementedError()\n\n\n@eval.register\ndef eval_ParamRef(\n    _expr: pgast.ParamRef,\n    *,\n    ctx: Context,\n) -> Optional[pgast.BaseExpr]:\n    if len(ctx.options.normalized_params) > 0:\n        raise DisableNormalization()\n    else:\n        return None\n"
  },
  {
    "path": "edb/pgsql/schemamech.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Optional, Sequence, Collection\n\nimport itertools\nimport dataclasses\n\nfrom edb import errors\n\nfrom edb.ir import ast as irast\nfrom edb.ir import astexpr as irastexpr\nfrom edb.ir import typeutils as irtyputils\nfrom edb.ir import utils as ir_utils\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import parser as ql_parser\nfrom edb.edgeql.compiler import astutils as ql_astutils\n\nfrom edb.schema import name as s_name\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import utils as s_utils\nfrom edb.schema import types as s_types\nfrom edb.schema import constraints as s_constraints\nfrom edb.schema import schema as s_schema\nfrom edb.schema import sources as s_sources\nfrom edb.schema import expr as s_expr\nfrom edb.schema import objects as s_obj\n\nfrom edb.common import ast\nfrom edb.common import parsing\n\nfrom . import ast as pgast\nfrom . import dbops\nfrom . import deltadbops\nfrom . import common\nfrom . import types\nfrom . import compiler\nfrom . import codegen\nfrom .common import qname as qn\n\n\ndef _get_exclusive_refs(tree: irast.Statement) -> Sequence[irast.Base] | None:\n    # Check if the expression is\n    #   std::_is_exclusive(<arg>) [and std::_is_exclusive(<arg>)...]\n\n    expr = tree.expr.expr\n\n    return irastexpr.get_constraint_references(expr)\n\n\n@dataclasses.dataclass(kw_only=True, repr=False, eq=False, slots=True)\nclass PGConstrData:\n    subject_db_name: Optional[tuple[str, str]]\n    expressions: list[ExprData]\n    relative_expressions: list[ExprData]\n    table_type: str\n    except_data: Optional[ExprDataSources]\n\n    scope: Optional[str] = None\n    type: Optional[str] = None\n\n\n@dataclasses.dataclass(kw_only=True, repr=False, eq=False, slots=True)\nclass ExprData:\n    exprdata: ExprDataSources\n    is_multicol: bool\n    is_trivial: bool\n    subject_db_name: Optional[tuple[str, str]] = None\n    except_data: Optional[ExprDataSources] = None\n\n\n@dataclasses.dataclass(kw_only=True, repr=False, eq=False, slots=True)\nclass ExprDataSources:\n    plain: str\n    new: str\n    old: str\n    plain_chunks: Sequence[str]\n\n\ndef _to_source(sql_expr: pgast.Base) -> str:\n    src = codegen.generate_source(sql_expr)\n    # ColumnRefs are the most common thing, and they should be safe to\n    # skip parenthesizing, for deuglification purposes. anything else\n    # we put parens around, to be sure.\n    if not isinstance(sql_expr, pgast.ColumnRef):\n        src = f'({src})'\n    return src\n\n\ndef _edgeql_tree_to_expr_data(\n    sql_expr: pgast.Base, refs: Optional[set[pgast.ColumnRef]] = None\n) -> ExprDataSources:\n    if refs is None:\n        refs = set(\n            ast.find_children(\n                sql_expr, pgast.ColumnRef, lambda n: len(n.name) == 1\n            )\n        )\n\n    plain_expr = _to_source(sql_expr)\n\n    if isinstance(sql_expr, (pgast.RowExpr, pgast.ImplicitRowExpr)):\n        chunks = []\n\n        for elem in sql_expr.args:\n            chunks.append(_to_source(elem))\n    else:\n        chunks = [plain_expr]\n\n    if isinstance(sql_expr, pgast.ColumnRef):\n        refs.add(sql_expr)\n\n    for ref in refs:\n        assert isinstance(ref.name, list)\n        ref.name.insert(0, 'NEW')\n    new_expr = _to_source(sql_expr)\n\n    for ref in refs:\n        assert isinstance(ref.name, list)\n        ref.name[0] = 'OLD'\n    old_expr = _to_source(sql_expr)\n\n    return ExprDataSources(\n        plain=plain_expr, new=new_expr, old=old_expr, plain_chunks=chunks\n    )\n\n\ndef _edgeql_ref_to_pg_constr(\n    subject: s_constraints.ConsistencySubject,\n    origin_subject: s_types.Type | s_pointers.Pointer | None,\n    tree: irast.Base,\n) -> ExprData:\n    sql_res = compiler.compile_ir_to_sql_tree(tree, singleton_mode=True)\n\n    sql_expr: pgast.Base\n    if isinstance(sql_res.ast, pgast.SelectStmt):\n        # XXX: use ast pattern matcher for this\n        from_clause = sql_res.ast.from_clause[0]\n        assert isinstance(from_clause, pgast.RelRangeVar)\n        assert isinstance(from_clause.relation, pgast.CommonTableExpr)\n        sql_expr = from_clause.relation.query.target_list[0].val\n    else:\n        sql_expr = sql_res.ast\n\n    if isinstance(tree, irast.Statement):\n        tree = tree.expr\n\n    if isinstance(tree, irast.Set) and isinstance(tree.expr, irast.SelectStmt):\n        tree = tree.expr.result\n\n    is_multicol = isinstance(sql_expr, (pgast.RowExpr, pgast.ImplicitRowExpr))\n\n    # Determine if the sequence of references are all simple refs, not\n    # expressions.  This influences the type of Postgres constraint used.\n    #\n    is_trivial = isinstance(sql_expr, pgast.ColumnRef) or (\n        isinstance(sql_expr, (pgast.RowExpr, pgast.ImplicitRowExpr))\n        and all(isinstance(el, pgast.ColumnRef) for el in sql_expr.args)\n    )\n\n    # Find all field references\n    #\n    refs = set(\n        ast.find_children(sql_expr, pgast.ColumnRef, lambda n: len(n.name) == 1)\n    )\n\n    if isinstance(subject, s_scalars.ScalarType):\n        # Domain constraint, replace <scalar_name> with VALUE\n        assert origin_subject\n\n        subj_pgname = common.edgedb_name_to_pg_name(str(subject.id))\n        orgsubj_pgname = common.edgedb_name_to_pg_name(str(origin_subject.id))\n\n        for ref in refs:\n            if ref.name != [subj_pgname] and ref.name != [orgsubj_pgname]:\n                raise ValueError(\n                    f'unexpected node reference in '\n                    f'ScalarType constraint: {qn(*ref.name)}'\n                )\n\n            # work around the immutability check\n            object.__setattr__(ref, 'name', ['VALUE'])\n\n    exprdata = _edgeql_tree_to_expr_data(sql_expr, refs=refs)\n\n    # Scalar constraints shouldn't ever fail on NULL\n    if isinstance(subject, s_scalars.ScalarType):\n        exprdata.plain = f\"VALUE IS NULL OR ({exprdata.plain})\"\n\n    return ExprData(\n        exprdata=exprdata, is_multicol=is_multicol, is_trivial=is_trivial\n    )\n\n\n@dataclasses.dataclass(frozen=True)\nclass CompiledConstraintData:\n    subject: s_types.Type | s_pointers.Pointer\n    exclusive_expr_refs: Optional[Sequence[irast.Base]]\n    subject_db_name: Optional[tuple[str, str]]\n    except_data: Optional[ExprDataSources]\n    ir: irast.Statement\n    subject_table_type: str\n\n\ndef _compile_constraint_data(\n    constraint: s_constraints.Constraint,\n    schema: s_schema.Schema,\n    is_optional: bool,\n    *,\n    span: Optional[parsing.Span] = None,\n    type_remaps: Optional[dict[s_obj.Object, s_obj.Object]] = None,\n) -> CompiledConstraintData:\n    sub = constraint.get_subject(schema)\n    assert isinstance(\n        sub, (s_types.Type, s_pointers.Pointer, s_scalars.ScalarType)\n    )\n    subject: s_types.Type | s_pointers.Pointer = sub\n\n    path_prefix_anchor = '__subject__'\n    singletons = frozenset({(subject, is_optional)})\n\n    options = qlcompiler.CompilerOptions(\n        anchors={'__subject__': subject},\n        path_prefix_anchor=path_prefix_anchor,\n        apply_query_rewrites=False,\n        singletons=singletons,\n        schema_object_context=type(constraint),\n        type_remaps=type_remaps if type_remaps is not None else {},\n    )\n\n    final_expr: Optional[s_expr.Expression] = constraint.get_finalexpr(schema)\n    assert final_expr is not None and final_expr.parse() is not None\n    ir = qlcompiler.compile_ast_to_ir(\n        final_expr.parse(),\n        schema,\n        options=options,\n    )\n    assert isinstance(ir, irast.Statement)\n\n    except_ir: Optional[irast.Statement] = None\n    except_data = None\n    if except_expr := constraint.get_except_expr(schema):\n        assert isinstance(except_expr, s_expr.Expression)\n        except_ir = qlcompiler.compile_ast_to_ir(\n            except_expr.parse(),\n            schema,\n            options=options,\n        )\n        except_sql = compiler.compile_ir_to_sql_tree(\n            except_ir, singleton_mode=True\n        )\n        except_data = _edgeql_tree_to_expr_data(except_sql.ast)\n\n    terminal_refs: set[irast.Set] = (\n        ir_utils.get_longest_paths(ir.expr.expr)\n    )\n    if except_ir is not None:\n        terminal_refs.update(\n            ir_utils.get_longest_paths(except_ir.expr)\n        )\n    ref_tables = get_ref_storage_info(ir.schema, terminal_refs)\n\n    if len(ref_tables) > 1:\n        raise errors.InvalidConstraintDefinitionError(\n            f'Constraint {constraint.get_displayname(schema)} on '\n            f'{subject.get_displayname(schema)} is not supported '\n            f'because it would depend on multiple objects',\n            span=span,\n        )\n    elif ref_tables:\n        subject_db_name, info = next(iter(ref_tables.items()))\n        subject_table_type = info[0][3].table_type\n    else:\n        # the expression does don't have any refs: default to the subject table\n\n        subject_table: Optional[s_obj.InheritingObject] | s_types.Type\n        if isinstance(subject, s_pointers.Pointer):\n            subject_table = subject.get_source(schema)\n        else:\n            subject_table = subject\n\n        assert subject_table\n        subject_db_name = common.get_backend_name(\n            schema, subject_table, catenate=False,\n        )\n        subject_table_type = 'ObjectType'\n\n    exclusive_expr_refs = _get_exclusive_refs(ir)\n\n    return CompiledConstraintData(\n        subject,\n        exclusive_expr_refs,\n        subject_db_name,\n        except_data,\n        ir,\n        subject_table_type,\n    )\n\n\ndef _get_compiled_constraint_expr_data(\n    primary_subject: s_constraints.ConsistencySubject,\n    constraint_data: CompiledConstraintData,\n) -> list[ExprData]:\n    exprdatas: list[ExprData] = []\n\n    constraint_subject = (\n        constraint_data.subject\n        if constraint_data.subject != primary_subject else\n        None\n    )\n\n    assert constraint_data.exclusive_expr_refs is not None\n    for ref in constraint_data.exclusive_expr_refs:\n        exprdata = _edgeql_ref_to_pg_constr(\n            primary_subject, constraint_subject, ref\n        )\n        exprdata.subject_db_name = constraint_data.subject_db_name\n        exprdata.except_data = constraint_data.except_data\n        exprdatas.append(exprdata)\n\n    return exprdatas\n\n\ndef table_constraint_requires_triggers(\n    constraint: s_constraints.Constraint,\n    schema: s_schema.Schema,\n    constraint_type: str,\n):\n    subject = constraint.get_subject(schema)\n    cname = constraint.get_shortname(schema)\n    if (\n        isinstance(subject, s_pointers.Pointer)\n        and subject.is_id_pointer(schema)\n        and cname == s_name.QualName('std', 'exclusive')\n    ):\n        return False\n    else:\n        return constraint_type != 'check'\n\n\ndef compile_constraint(\n    subject: s_constraints.ConsistencySubject,\n    constraint: s_constraints.Constraint,\n    schema: s_schema.Schema,\n    span: Optional[parsing.Span],\n) -> SchemaDomainConstraint | SchemaTableConstraint:\n    assert constraint.get_subject(schema) is not None\n    assert isinstance(\n        subject, (s_types.Type, s_pointers.Pointer, s_scalars.ScalarType)\n    )\n\n    constraint_origins = constraint.get_constraint_origins(schema)\n    first_subject = constraint_origins[0].get_subject(schema)\n\n    is_optional = isinstance(\n        first_subject, s_pointers.Pointer\n    ) and not first_subject.get_required(schema)\n\n    constraint_data = _compile_constraint_data(\n        constraint,\n        schema,\n        is_optional,\n        span=span,\n        # Remap the constraint origin to the subject, so that if\n        # we have B <: A, and the constraint references A.foo, it\n        # gets rewritten in the subtype to B.foo. It's OK to only\n        # look at one constraint origin, because if there were\n        # multiple different origins, they couldn't get away with\n        # referring to the type explicitly.\n        type_remaps={first_subject: subject},\n    )\n\n    pg_constr_data = PGConstrData(\n        subject_db_name=constraint_data.subject_db_name,\n        expressions=[],\n        relative_expressions=[],\n        table_type=constraint_data.subject_table_type,\n        except_data=constraint_data.except_data,\n    )\n\n    if constraint_data.exclusive_expr_refs:\n        origin_expr_datas: dict[\n            s_constraints.Constraint, list[ExprData]\n        ] = {}\n        for origin in constraint_origins:\n            if origin == constraint:\n                origin_data = constraint_data\n\n            else:\n                origin_data = _compile_constraint_data(\n                    origin,\n                    schema,\n                    is_optional,\n                )\n\n            origin_expr_datas[origin] = _get_compiled_constraint_expr_data(\n                subject, origin_data\n            )\n\n        # Set constraint expressions\n        expressions: list[ExprData]\n        if constraint in origin_expr_datas:\n            expressions = origin_expr_datas[constraint]\n        else:\n            expressions = _get_compiled_constraint_expr_data(\n                subject, constraint_data\n            )\n\n        pg_constr_data.expressions.extend(expressions)\n\n        # Set relative expressions\n        # These are only needed for constraint triggers.\n        if (\n            not isinstance(constraint.get_subject(schema), s_scalars.ScalarType)\n            and table_constraint_requires_triggers(\n                constraint, schema, 'unique'\n            )\n        ):\n            relatives = list(set(\n                descendant\n                for origin in constraint_origins\n                for descendant in itertools.chain(\n                    [origin], origin.descendants(schema)\n                )\n            ))\n\n            relative_expressions: list[ExprData] = []\n            for relative in relatives:\n                if relative == constraint:\n                    relative_expressions.extend(expressions)\n\n                elif relative in origin_expr_datas:\n                    relative_expressions.extend(origin_expr_datas[relative])\n\n                else:\n                    relative_data = _compile_constraint_data(\n                        relative,\n                        schema,\n                        is_optional,\n                    )\n                    relative_expressions.extend(\n                        _get_compiled_constraint_expr_data(\n                            subject, relative_data\n                        )\n                    )\n\n            pg_constr_data.relative_expressions.extend(relative_expressions)\n\n        pg_constr_data.scope = 'relation'\n        pg_constr_data.type = 'unique'\n\n    else:\n        assert len(constraint_origins) == 1\n        origin_data = (\n            _compile_constraint_data(\n                constraint_origins[0],\n                schema,\n                is_optional,\n            )\n            if constraint_origins[0] != constraint else\n            constraint_data\n        )\n        exprdata = _edgeql_ref_to_pg_constr(\n            subject, origin_data.subject, constraint_data.ir\n        )\n        exprdata.subject_db_name = origin_data.subject_db_name\n        exprdata.except_data = origin_data.except_data\n\n        pg_constr_data.expressions.append(exprdata)\n\n        pg_constr_data.scope = 'row'\n        pg_constr_data.type = 'check'\n\n    if isinstance(constraint.get_subject(schema), s_scalars.ScalarType):\n        return SchemaDomainConstraint(\n            subject=subject,\n            constraint=constraint,\n            pg_constr_data=pg_constr_data,\n            schema=schema,\n        )\n    else:\n        return SchemaTableConstraint(\n            subject=subject,\n            constraint=constraint,\n            pg_constr_data=pg_constr_data,\n            schema=schema,\n        )\n\n\n@dataclasses.dataclass(kw_only=True, repr=False, eq=False, slots=True)\nclass SchemaDomainConstraint:\n    subject: s_constraints.ConsistencySubject\n    constraint: s_constraints.Constraint\n    pg_constr_data: PGConstrData\n    schema: s_schema.Schema\n\n    def _domain_constraint(self, constr: SchemaConstraint):\n        domain_name = constr.pg_constr_data.subject_db_name\n        expressions = constr.pg_constr_data.expressions\n\n        return deltadbops.SchemaConstraintDomainConstraint(\n            domain_name, constr.constraint, expressions, schema=self.schema\n        )\n\n    def create_ops(self):\n        ops = dbops.CommandGroup()\n\n        domconstr = self._domain_constraint(self)\n        add_constr = dbops.AlterDomainAddConstraint(\n            name=domconstr.get_subject_name(quote=False), constraint=domconstr)\n\n        ops.add_command(add_constr)\n\n        return ops\n\n    def alter_ops(\n        self, orig_constr: SchemaConstraint\n    ):\n        ops = dbops.CommandGroup()\n        return ops\n\n    def delete_ops(self):\n        ops = dbops.CommandGroup()\n\n        domconstr = self._domain_constraint(self)\n        add_constr = dbops.AlterDomainDropConstraint(\n            name=domconstr.get_subject_name(quote=False), constraint=domconstr)\n\n        ops.add_command(add_constr)\n\n        return ops\n\n    def enforce_ops(self):\n        ops = dbops.CommandGroup()\n        return ops\n\n    def update_trigger_ops(self) -> dbops.CommandGroup:\n        ops = dbops.CommandGroup()\n        return ops\n\n    def fixup_trigger_ops(self) -> dbops.CommandGroup:\n        ops = dbops.CommandGroup()\n        return ops\n\n\n@dataclasses.dataclass(kw_only=True, repr=False, eq=False, slots=True)\nclass SchemaTableConstraint:\n    subject: s_constraints.ConsistencySubject\n    constraint: s_constraints.Constraint\n    pg_constr_data: PGConstrData\n    schema: s_schema.Schema\n\n    def _table_constraint(\n        self, constr: SchemaConstraint\n    ) -> deltadbops.SchemaConstraintTableConstraint:\n        pg_c = constr.pg_constr_data\n\n        table_name = pg_c.subject_db_name\n        expressions = pg_c.expressions\n        relative_expressions = pg_c.relative_expressions\n        assert table_name\n\n        return deltadbops.SchemaConstraintTableConstraint(\n            table_name,\n            constraint=constr.constraint,\n            exprdata=expressions,\n            relative_exprdata=relative_expressions,\n            except_data=pg_c.except_data,\n            scope=pg_c.scope,\n            type=pg_c.type,\n            table_type=pg_c.table_type,\n            schema=constr.schema,\n        )\n\n    def create_ops(self):\n        ops = dbops.CommandGroup()\n\n        tabconstr = self._table_constraint(self)\n        add_constr = deltadbops.AlterTableAddConstraint(\n            name=tabconstr.get_subject_name(quote=False),\n            constraint=tabconstr,\n        )\n\n        ops.add_command(add_constr)\n\n        return ops\n\n    def alter_ops(\n        self, orig_constr: SchemaConstraint\n    ):\n        ops = dbops.CommandGroup()\n\n        tabconstr = self._table_constraint(self)\n        orig_tabconstr = self._table_constraint(orig_constr)\n\n        alter_constr = deltadbops.AlterTableAlterConstraint(\n            name=tabconstr.get_subject_name(quote=False),\n            constraint=orig_tabconstr,\n            new_constraint=tabconstr,\n        )\n\n        ops.add_command(alter_constr)\n\n        return ops\n\n    def delete_ops(self):\n        ops = dbops.CommandGroup()\n\n        tabconstr = self._table_constraint(self)\n        add_constr = deltadbops.AlterTableDropConstraint(\n            name=tabconstr.get_subject_name(quote=False),\n            constraint=tabconstr,\n        )\n\n        ops.add_command(add_constr)\n\n        return ops\n\n    def enforce_ops(self) -> dbops.CommandGroup:\n        ops = dbops.CommandGroup()\n\n        tabconstr = self._table_constraint(self)\n\n        constr_name = tabconstr.constraint_name()\n        raw_constr_name = tabconstr.constraint_name(quote=False)\n\n        for expr, relative_expr in zip(\n            itertools.cycle(tabconstr._exprdata),\n            tabconstr._relative_exprdata\n        ):\n            exprdata = expr.exprdata\n            relative_exprdata = relative_expr.exprdata\n            old_expr = relative_exprdata.old\n            new_expr = exprdata.new\n\n            assert relative_expr.subject_db_name\n            schemaname, tablename = relative_expr.subject_db_name\n            real_tablename = tabconstr.get_subject_name(quote=False)\n\n            errmsg = 'duplicate key value violates unique ' \\\n                     'constraint {constr}'.format(constr=constr_name)\n            detail = common.quote_literal(\n                f\"Key ({relative_exprdata.plain}) already exists.\"\n            )\n\n            if (\n                isinstance(self.subject, s_pointers.Pointer)\n                and self.pg_constr_data.table_type == 'link'\n            ):\n                key = \"source\"\n            else:\n                key = \"id\"\n\n            except_data = tabconstr._except_data\n            relative_except_data = relative_expr.except_data\n\n            if except_data:\n                assert relative_except_data\n                except_part = f'''\n                    AND ({relative_except_data.old} is not true)\n                    AND ({except_data.new} is not true)\n                '''\n            else:\n                except_part = ''\n\n            check = dbops.Query(\n                f'''\n                SELECT\n                    edgedb_VER.raise(\n                        NULL::text,\n                        'unique_violation',\n                        msg => '{errmsg}',\n                        \"constraint\" => '{raw_constr_name}',\n                        \"table\" => '{tablename}',\n                        \"schema\" => '{schemaname}',\n                        detail => {detail}\n                    )\n                FROM {common.qname(schemaname, tablename)} AS OLD\n                CROSS JOIN {common.qname(*real_tablename)} AS NEW\n                WHERE {old_expr} = {new_expr} and OLD.{key} != NEW.{key}\n                {except_part}\n                INTO _dummy_text;\n                '''\n            )\n            ops.add_command(check)\n\n        return ops\n\n    def update_trigger_ops(self) -> dbops.CommandGroup:\n        ops = dbops.CommandGroup()\n\n        tabconstr = self._table_constraint(self)\n        add_constr = deltadbops.AlterTableUpdateConstraintTrigger(\n            name=tabconstr.get_subject_name(quote=False),\n            constraint=tabconstr,\n        )\n\n        ops.add_command(add_constr)\n\n        return ops\n\n    def fixup_trigger_ops(self) -> dbops.CommandGroup:\n        # Pre 6.8 versions of gel created needless disabled triggers\n        # in some cases. This path (invoked by administer\n        # remove_pointless_triggers()) deletes them.\n        ops = dbops.CommandGroup()\n\n        tabconstr = self._table_constraint(self)\n        add_constr = deltadbops.AlterTableUpdateConstraintTriggerFixup(\n            name=tabconstr.get_subject_name(quote=False),\n            constraint=tabconstr,\n        )\n\n        ops.add_command(add_constr)\n\n        return ops\n\n\nSchemaConstraint = SchemaDomainConstraint | SchemaTableConstraint\n\n\ndef ptr_default_to_col_default(schema, ptr, expr):\n    try:\n        # NOTE: This code currently will only be invoked for scalars.\n        # Blindly cast the default expression into the ptr target\n        # type, validation of the expression type is not the concern\n        # of this function.\n        eql = ql_parser.parse_query(expr.text)\n        eql = ql_astutils.ensure_ql_query(\n            qlast.TypeCast(\n                type=s_utils.typeref_to_ast(\n                    schema, ptr.get_target(schema)),\n                expr=eql,\n            )\n        )\n        ir = qlcompiler.compile_ast_to_ir(eql, schema)\n    except (errors.SchemaError, errors.QueryError):\n        # Reference errors mean that is is a non-constant default\n        # referring to a not-yet-existing objects.\n        return None\n\n    if not ir_utils.is_const(ir):\n        return None\n    if ast.find_children(ir, irast.TupleIndirectionPointer):\n        return None\n\n    try:\n        sql_res = compiler.compile_ir_to_sql_tree(ir, singleton_mode=True)\n    except errors.UnsupportedFeatureError:\n        return None\n    sql_text = _to_source(sql_res.ast)\n\n    return sql_text\n\n\nRefTables = dict[\n    Optional[tuple[str, str]],\n    list[\n        tuple[\n            irast.Set,\n            s_pointers.PointerLike,\n            s_pointers.PointerLike | s_types.Type,\n            types.PointerStorageInfo,\n        ]\n    ],\n]\n\n\ndef get_ref_storage_info(\n    schema: s_schema.Schema, refs: Collection[irast.Set]\n) -> RefTables:\n    link_biased: dict[irast.Set, types.PointerStorageInfo] = {}\n    objtype_biased: dict[irast.Set, types.PointerStorageInfo] = {}\n\n    RefPtr = tuple[\n        s_pointers.PointerLike, s_types.Type | s_pointers.PointerLike\n    ]\n    ref_ptrs: dict[irast.Set, RefPtr] = {}\n    refs = list(refs)\n    for ref in refs:\n        ptr: s_pointers.PointerLike\n        src: s_types.Type | s_pointers.PointerLike\n\n        rptr = ref.expr if isinstance(ref.expr, irast.Pointer) else None\n        if rptr is None:\n            source_typeref = ref.typeref\n            if not irtyputils.is_object(source_typeref):\n                continue\n            if irtyputils.is_free_object(source_typeref):\n                continue\n            schema, t = irtyputils.ir_typeref_to_type(schema, ref.typeref)\n            assert isinstance(t, s_sources.Source)\n            ptr = t.getptr(schema, s_name.UnqualName('id'))\n        else:\n            ptrref = rptr.ptrref\n            schema, ptr = irtyputils.ptrcls_from_ptrref(ptrref, schema=schema)\n            source_typeref = rptr.source.typeref\n\n        if ptr.is_link_property(schema):\n            assert rptr and rptr.source\n            assert isinstance(rptr.source.expr, irast.Pointer)\n            srcref = rptr.source.expr.ptrref\n            schema, src = irtyputils.ptrcls_from_ptrref(\n                srcref, schema=schema)\n            if src.get_is_derived(schema):\n                # This specialized pointer was derived specifically\n                # for the purposes of constraint expr compilation.\n                src = src.get_bases(schema).first(schema)\n        elif ptr.is_tuple_indirection():\n            assert rptr\n            refs.append(rptr.source)  # noqa: B909\n            continue\n        elif ptr.is_type_intersection():\n            assert rptr\n            refs.append(rptr.source)  # noqa: B909\n            continue\n        else:\n            schema, src = irtyputils.ir_typeref_to_type(schema, source_typeref)\n        ref_ptrs[ref] = (ptr, src)\n\n    for ref, (ptr, src) in ref_ptrs.items():\n        ptr_info = types.get_pointer_storage_info(\n            ptr, source=src, resolve_type=False, schema=schema)  # type: ignore\n\n        # See if any of the refs are hosted in pointer tables and others\n        # are not...\n        if ptr_info.table_type == 'link':\n            link_biased[ref] = ptr_info\n        else:\n            objtype_biased[ref] = ptr_info\n\n        if link_biased and objtype_biased:\n            break\n\n    if link_biased and objtype_biased:\n        for ref in objtype_biased.copy():\n            ptr, src = ref_ptrs[ref]\n            ptr_info = types.get_pointer_storage_info(\n                ptr, source=src,  # type: ignore\n                resolve_type=False,\n                link_bias=True,\n                schema=schema,\n            )\n\n            if ptr_info is not None and ptr_info.table_type == 'link':\n                link_biased[ref] = ptr_info\n                objtype_biased.pop(ref)\n\n    ref_tables: RefTables = {}\n\n    for ref, ptr_info in itertools.chain(\n            objtype_biased.items(), link_biased.items()):\n        ptr, src = ref_ptrs[ref]\n\n        try:\n            ref_tables[ptr_info.table_name].append((ref, ptr, src, ptr_info))\n        except KeyError:\n            ref_tables[ptr_info.table_name] = [(ref, ptr, src, ptr_info)]\n\n    return ref_tables\n"
  },
  {
    "path": "edb/pgsql/trampoline.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Support for namespacing and trampolining the standard library.\n\nThe idea here is that all of the functions, tables, and views in\nedgedb/edgedbstd/edgedbsql should be moved into namespaced libraries\nof the form `edgedb_VER`, where VER will be substituted with some\nversion identifier.\n\nThen, for anything that might be referenced by a function, constraint,\netc, we will create a *trampoline* in the un-suffixed namespace.\nWhen doing (eventually) in-place version upgrades, we will create the\nnew namespace and then update the trampolines to point to it.\n\nCURRENT STATUS: So far, functions and views are mostly\nnamespaced. Standard library schema object tables aren't yet.\n\"\"\"\n\nfrom __future__ import annotations\nfrom typing import (\n    TYPE_CHECKING,\n    Optional,\n    Sequence,\n)\n\nimport abc\nimport copy\nimport dataclasses\n\n\nfrom . import common\nfrom . import dbops\n\n\nq = common.qname\nqi = common.quote_ident\nql = common.quote_literal\n\n\nV = common.versioned_schema\n\n\ndef fixup_query(query: str) -> str:\n    for s in common.VERSIONED_SCHEMAS:\n        query = query.replace(f\"{s}_VER\", V(s))\n    return query\n\n\nclass VersionedFunction(dbops.Function):\n    if TYPE_CHECKING:\n        # What the volatility of the trampoline wrapper should be.\n        # This is sometimes immutable even when the underlying is\n        # stable, for functions that must be immutable so they can go\n        # into indexes/constraints but might do something technically\n        # stable (like raise an error).\n        #\n        # This allows the real function to be inlined while allowing\n        # the wrapper to get used in indexes/constraints.\n        wrapper_volatility: Optional[str]\n\n        def __init__(\n            self,\n            name: tuple[str, ...],\n            *,\n            args: Optional[Sequence[dbops.FunctionArg]] = None,\n            returns: str | tuple[str, ...],\n            text: str,\n            volatility: str = \"volatile\",\n            language: str = \"sql\",\n            has_variadic: Optional[bool] = None,\n            strict: bool = False,\n            parallel_safe: bool = False,\n            set_returning: bool = False,\n\n            wrapper_volatility: Optional[str] = None,\n        ):\n            pass\n\n    else:\n        def __init__(self, *args, wrapper_volatility=None, **kwargs):\n            super().__init__(*args, **kwargs)\n            self.name = (\n                common.maybe_versioned_schema(self.name[0]), *self.name[1:])\n            self.text = fixup_query(self.text)\n\n            self.wrapper_volatility = wrapper_volatility\n\n            if self.args:\n                nargs = []\n                for arg in self.args:\n                    if isinstance(arg, tuple) and isinstance(arg[1], tuple):\n                        new_name = (\n                            arg[1][0].replace('_VER', V('')), *arg[1][1:])\n                        arg = (arg[0], new_name, *arg[2:])\n                    nargs.append(arg)\n                self.args = nargs\n\n\nclass VersionedView(dbops.View):\n    if not TYPE_CHECKING:\n        def __init__(self, *args, **kwargs):\n            super().__init__(*args, **kwargs)\n            self.name = (\n                common.maybe_versioned_schema(self.name[0]), *self.name[1:])\n            self.query = fixup_query(self.query)\n\n\n@dataclasses.dataclass\nclass Trampoline:\n    name: tuple[str, str]\n\n    @abc.abstractmethod\n    def make(self) -> dbops.Command:\n        pass\n\n    @abc.abstractmethod\n    def drop(self) -> dbops.Command:\n        pass\n\n\n@dataclasses.dataclass\nclass TrampolineFunction(Trampoline):\n    func: dbops.Function\n\n    def make(self) -> dbops.Command:\n        return dbops.CreateFunction(self.func, or_replace=True)\n\n    def drop(self) -> dbops.Command:\n        return dbops.DropFunction(\n            self.func.name,\n            args=self.func.args or (),\n            has_variadic=bool(self.func.has_variadic),\n            if_exists=True,\n        )\n\n\n@dataclasses.dataclass\nclass TrampolineView(Trampoline):\n    old_name: tuple[str, str]\n\n    def make(self) -> dbops.Command:\n        return dbops.Query(f'''\n            PERFORM {V('edgedb')}._create_trampoline_view(\n                {ql(q(*self.old_name))}, {ql(self.name[0])}, {ql(self.name[1])})\n        ''')\n\n    def drop(self) -> dbops.Command:\n        return dbops.DropView(\n            self.name,\n            conditions=[dbops.ViewExists(self.name)],\n        )\n\n\ndef make_trampoline(func: dbops.Function) -> TrampolineFunction:\n    new_func = copy.copy(func)\n    schema, name = func.name\n    namespace = V('')\n    assert schema.endswith(namespace), schema\n    new_func.name = (schema[:-len(namespace)], name)\n\n    args = []\n    for arg in (func.args or ()):\n        if isinstance(arg, str):\n            args.append(arg)\n        else:\n            assert arg[0]\n            args.append(arg[0])\n    args = [qi(arg) for arg in args]\n    if func.has_variadic:\n        args[-1] = f'VARIADIC {args[-1]}'\n\n    new_func.text = f'select {q(*func.name)}({\", \".join(args)})'\n    new_func.language = 'sql'\n    new_func.strict = False\n    if isinstance(func, VersionedFunction) and func.wrapper_volatility:\n        new_func.volatility = func.wrapper_volatility\n    return TrampolineFunction(new_func.name, new_func)\n\n\ndef make_table_trampoline(fullname: tuple[str, str]) -> TrampolineView:\n    schema, name = fullname\n    namespace = V('')\n    assert schema.endswith(namespace), schema\n    new_name = (schema[:-len(namespace)], name)\n\n    return TrampolineView(new_name, fullname)\n\n\ndef make_view_trampoline(view: dbops.View) -> TrampolineView:\n    return make_table_trampoline(view.name)\n"
  },
  {
    "path": "edb/pgsql/types.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2010-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport dataclasses\nimport uuid\nfrom typing import Literal, Optional, cast, overload\n\nfrom edb.common.typeutils import not_none\nfrom edb.common import lru\n\nfrom edb.ir import ast as irast\nfrom edb.ir import typeutils as irtyputils\n\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import name as sn\nfrom edb.schema import objects as s_obj\nfrom edb.schema import schema as s_schema\nfrom edb.schema import types as s_types\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import properties as s_properties\n\nfrom . import common\n\n\nbase_type_name_map = {\n    s_obj.get_known_type_id('std::str'): ('text',),\n    s_obj.get_known_type_id('std::int64'): ('int8',),\n    s_obj.get_known_type_id('std::int32'): ('int4',),\n    s_obj.get_known_type_id('std::int16'): ('int2',),\n    s_obj.get_known_type_id('std::decimal'): ('numeric',),\n    s_obj.get_known_type_id('std::bigint'): ('edgedbt', 'bigint_t'),\n    s_obj.get_known_type_id('std::bool'): ('bool',),\n    s_obj.get_known_type_id('std::float64'): ('float8',),\n    s_obj.get_known_type_id('std::float32'): ('float4',),\n    s_obj.get_known_type_id('std::uuid'): ('uuid',),\n    s_obj.get_known_type_id('std::datetime'): ('edgedbt', 'timestamptz_t'),\n    s_obj.get_known_type_id('std::duration'): ('edgedbt', 'duration_t',),\n    s_obj.get_known_type_id('std::bytes'): ('bytea',),\n    s_obj.get_known_type_id('std::json'): ('jsonb',),\n\n    s_obj.get_known_type_id('std::cal::local_datetime'):\n        ('edgedbt', 'timestamp_t'),\n    s_obj.get_known_type_id('std::cal::local_date'): ('edgedbt', 'date_t'),\n    s_obj.get_known_type_id('std::cal::local_time'): ('time',),\n    s_obj.get_known_type_id('std::cal::relative_duration'):\n        ('edgedbt', 'relative_duration_t'),\n    s_obj.get_known_type_id('std::cal::date_duration'):\n        ('edgedbt', 'date_duration_t'),\n\n    s_obj.get_known_type_id('cfg::memory'): ('edgedbt', 'memory_t'),\n\n    s_obj.get_known_type_id('std::pg::json'): ('json',),\n    s_obj.get_known_type_id('std::pg::timestamptz'): ('timestamptz',),\n    s_obj.get_known_type_id('std::pg::timestamp'): ('timestamp',),\n    s_obj.get_known_type_id('std::pg::date'): ('date',),\n    s_obj.get_known_type_id('std::pg::interval'): ('interval',),\n}\n\ntype_to_range_name_map = {\n    ('int4',): ('int4range',),\n    ('int8',): ('int8range',),\n    ('numeric',): ('numrange',),\n    ('float4',): ('edgedb', 'float32_range_t'),\n    ('float8',): ('edgedb', 'float64_range_t'),\n    ('edgedbt', 'timestamptz_t'): ('edgedb', 'datetime_range_t'),\n    ('edgedbt', 'timestamp_t'): ('edgedb', 'local_datetime_range_t'),\n    # cal::local_date uses the built-in daterange instead of a custom\n    # one that actually uses edgedbt.date_t as its subtype. This is\n    # because cal::local_date is discrete, and its range type should\n    # get canonicalized. Defining a canonicalization function for a\n    # custom range is a big hassle, and daterange already has the\n    # correct canonicalization function\n    ('edgedbt', 'date_t'): ('daterange',),\n    ('timestamptz',): ('tstzrange',),\n    ('timestamp',): ('tsrange',),\n    ('date',): ('daterange',),\n}\n\n# Construct a multirange map based on type_to_range_name_map by replacing\n# 'range' with 'multirange' in the names.\n#\n# The multiranges are created automatically when ranges are created. They\n# have the same names except with \"multi\" in front of the \"range\".\ntype_to_multirange_name_map = {}\nfor key, val in type_to_range_name_map.items():\n    *pre, name = val\n    pre.append(name.replace('range', 'multirange'))\n    type_to_multirange_name_map[key] = tuple(pre)\n\n\nbase_type_name_map_r = {\n    'character varying': sn.QualName('std', 'str'),\n    'character': sn.QualName('std', 'str'),\n    'text': sn.QualName('std', 'str'),\n    'numeric': sn.QualName('std', 'decimal'),\n    'edgedbt.bigint_t': sn.QualName('std', 'bigint'),\n    'bigint_t': sn.QualName('std', 'bigint'),\n    'int4': sn.QualName('std', 'int32'),\n    'integer': sn.QualName('std', 'int32'),\n    'bigint': sn.QualName('std', 'int64'),\n    'int8': sn.QualName('std', 'int64'),\n    'int2': sn.QualName('std', 'int16'),\n    'smallint': sn.QualName('std', 'int16'),\n    'boolean': sn.QualName('std', 'bool'),\n    'bool': sn.QualName('std', 'bool'),\n    'double precision': sn.QualName('std', 'float64'),\n    'float8': sn.QualName('std', 'float64'),\n    'real': sn.QualName('std', 'float32'),\n    'float4': sn.QualName('std', 'float32'),\n    'uuid': sn.QualName('std', 'uuid'),\n    'timestamp with time zone': sn.QualName('std', 'datetime'),\n    'edgedbt.timestamptz_t': sn.QualName('std', 'datetime'),\n    'timestamptz_t': sn.QualName('std', 'datetime'),\n    'timestamptz': sn.QualName('std', 'datetime'),\n    'duration_t': sn.QualName('std', 'duration'),\n    'edgedbt.duration_t': sn.QualName('std', 'duration'),\n    'interval': sn.QualName('std', 'duration'),\n    'bytea': sn.QualName('std', 'bytes'),\n    'jsonb': sn.QualName('std', 'json'),\n\n    'timestamp': sn.QualName('std::cal', 'local_datetime'),\n    'timestamp_t': sn.QualName('std::cal', 'local_datetime'),\n    'edgedbt.timestamp_t': sn.QualName('std::cal', 'local_datetime'),\n    'date': sn.QualName('std::cal', 'local_date'),\n    'date_t': sn.QualName('std::cal', 'local_date'),\n    'edgedbt.date_t': sn.QualName('std::cal', 'local_date'),\n    'time': sn.QualName('std::cal', 'local_time'),\n    'relative_duration_t': sn.QualName('std::cal', 'relative_duration'),\n    'edgedbt.relative_duration_t': sn.QualName('std::cal', 'relative_duration'),\n    'date_duration_t': sn.QualName('std::cal', 'date_duration'),\n    'edgedbt.date_duration_t': sn.QualName('std::cal', 'date_duration'),\n\n    'edgedbt.memory_t': sn.QualName('cfg', 'memory'),\n    'memory_t': sn.QualName('cfg', 'memory'),\n\n    'json': sn.QualName('std::pg', 'json'),\n}\n\npg_tsvector_typeref = irast.TypeRef(\n    id=uuid.UUID('44d73839-8882-419f-80e5-84f7a3402919'),\n    name_hint=sn.QualName('pg_catalog', 'tsvector'),\n    is_scalar=True,\n    sql_type='pg_catalog.tsvector',\n)\n\npg_oid_typeref = irast.TypeRef(\n    id=uuid.UUID('44d73839-8882-419f-80e5-84f7a3402920'),\n    name_hint=sn.QualName('pg_catalog', 'oid'),\n    is_scalar=True,\n    sql_type='pg_catalog.oid',\n)\n\npg_langs = {\n    'simple',\n    'arabic',\n    'armenian',\n    'basque',\n    'catalan',\n    'danish',\n    'dutch',\n    'english',\n    'finnish',\n    'french',\n    'german',\n    'greek',\n    'hindi',\n    'hungarian',\n    'indonesian',\n    'irish',\n    'italian',\n    'lithuanian',\n    'nepali',\n    'norwegian',\n    'portuguese',\n    'romanian',\n    'russian',\n    'serbian',\n    'spanish',\n    'swedish',\n    'tamil',\n    'turkish',\n    'yiddish',\n}\n\n\npg_langs_by_iso_639_3 = {\n    'ara': 'arabic',\n    'hye': 'armenian',\n    'eus': 'basque',\n    'cat': 'catalan',\n    'dan': 'danish',\n    'nld': 'dutch',\n    'eng': 'english',\n    'fin': 'finnish',\n    'fra': 'french',\n    'deu': 'german',\n    'ell': 'greek',\n    'hin': 'hindi',\n    'hun': 'hungarian',\n    'ind': 'indonesian',\n    'gle': 'irish',\n    'ita': 'italian',\n    'lit': 'lithuanian',\n    'npi': 'nepali',\n    'nor': 'norwegian',\n    'por': 'portuguese',\n    'ron': 'romanian',\n    'rus': 'russian',\n    'srp': 'serbian',\n    'spa': 'spanish',\n    'swe': 'swedish',\n    'tam': 'tamil',\n    'tur': 'turkish',\n    'yid': 'yiddish',\n}\n\n\ndef to_regconfig(language: str) -> str:\n    \"Analogous to edgedb.fts_to_regconfig function in metaschema\"\n    language = language.lower()\n    if language.startswith('xxx_'):\n        return language[4:]\n    else:\n        return pg_langs_by_iso_639_3.get(language, language)\n\n\ndef is_builtin_scalar(\n    schema: s_schema.Schema, scalar: s_scalars.ScalarType\n) -> bool:\n    return scalar.id in base_type_name_map\n\n\ndef type_has_stable_oid(typ: s_types.Type) -> bool:\n    pg_type = base_type_name_map.get(typ.id)\n    return pg_type is not None and len(pg_type) == 1\n\n\ndef get_scalar_base(\n    schema: s_schema.Schema, scalar: s_scalars.ScalarType\n) -> tuple[str, ...]:\n    if base := base_type_name_map.get(scalar.id):\n        return base\n\n    for ancestor in scalar.get_ancestors(schema).objects(schema):\n        if not ancestor.get_abstract(schema):\n            # Check if base is fundamental, if not, then it is\n            # another domain.\n            if base := base_type_name_map.get(ancestor.id):\n                pass\n            elif typstr := ancestor.resolve_sql_type(schema):\n                base = tuple(typstr.split('.'))\n            else:\n                base = common.get_backend_name(\n                    schema, ancestor, catenate=False)\n                assert base\n\n            return base\n\n    raise ValueError(f'cannot determine backend type for scalar type '\n                     f'{scalar.get_name(schema)}')\n\n\ndef pg_type_from_scalar(\n    schema: s_schema.Schema, scalar: s_scalars.ScalarType\n) -> tuple[str, ...]:\n\n    if scalar.is_polymorphic(schema):\n        return ('anynonarray',)\n\n    column_type = base_type_name_map.get(scalar.id)\n    if column_type:\n        pass\n    elif typstr := scalar.resolve_sql_type(schema):\n        column_type = tuple(typstr.split('.'))\n    else:\n        column_type = common.get_backend_name(schema, scalar, catenate=False)\n    assert column_type\n\n    return column_type\n\n\ndef pg_type_array(tp: tuple[str, ...]) -> tuple[str, ...]:\n    if len(tp) == 1:\n        return (tp[0] + '[]',)\n    else:\n        return (tp[0], tp[1] + '[]')\n\n\ndef pg_type_range(tp: tuple[str, ...]) -> tuple[str, ...]:\n    return type_to_range_name_map[tp]\n\n\ndef pg_type_multirange(tp: tuple[str, ...]) -> tuple[str, ...]:\n    return type_to_multirange_name_map[tp]\n\n\ndef pg_type_from_object(\n    schema: s_schema.Schema, obj: s_obj.Object, persistent_tuples: bool = False\n) -> tuple[str, ...]:\n\n    if isinstance(obj, s_scalars.ScalarType):\n        return pg_type_from_scalar(schema, obj)\n\n    elif isinstance(obj, s_types.Type) and obj.is_anytuple(schema):\n        return ('record',)\n\n    elif isinstance(obj, s_types.Tuple):\n        if persistent_tuples:\n            return cast(\n                tuple[str, ...],\n                common.get_tuple_backend_name(obj.id, catenate=False),\n            )\n        else:\n            return ('record',)\n\n    elif isinstance(obj, s_types.Array):\n        if obj.is_polymorphic(schema):\n            return ('anyarray',)\n        else:\n            tp = pg_type_from_object(\n                schema, obj.get_subtypes(schema)[0],\n                persistent_tuples=persistent_tuples)\n            return pg_type_array(tp)\n\n    elif isinstance(obj, s_types.Range):\n        if obj.is_polymorphic(schema):\n            return ('anyrange',)\n        else:\n            tp = pg_type_from_object(\n                schema, obj.get_subtypes(schema)[0],\n                persistent_tuples=persistent_tuples)\n            return pg_type_range(tp)\n\n    elif isinstance(obj, s_types.MultiRange):\n        if obj.is_polymorphic(schema):\n            return ('anymultirange',)\n        else:\n            tp = pg_type_from_object(\n                schema, obj.get_subtypes(schema)[0],\n                persistent_tuples=persistent_tuples)\n            return pg_type_multirange(tp)\n\n    elif isinstance(obj, s_objtypes.ObjectType):\n        return ('uuid',)\n\n    elif isinstance(obj, s_types.Type) and obj.is_any(schema):\n        return ('anyelement',)\n\n    else:\n        raise ValueError(f'could not determine PG type for {obj!r}')\n\n\ndef pg_type_from_ir_typeref(\n    ir_typeref: irast.TypeRef,\n    *,\n    serialized: bool = False,\n    persistent_tuples: bool = False,\n) -> tuple[str, ...]:\n\n    if irtyputils.is_array(ir_typeref):\n        if (irtyputils.is_generic(ir_typeref)\n                or (irtyputils.is_abstract(ir_typeref.subtypes[0])\n                    and irtyputils.is_scalar(ir_typeref.subtypes[0]))):\n            return ('anyarray',)\n        elif irtyputils.is_array(ir_typeref.subtypes[0]):\n            return ('record[]',)\n        else:\n            tp = pg_type_from_ir_typeref(\n                ir_typeref.subtypes[0],\n                serialized=serialized,\n                persistent_tuples=persistent_tuples)\n            return pg_type_array(tp)\n\n    elif irtyputils.is_range(ir_typeref):\n        if (irtyputils.is_generic(ir_typeref)\n                or (irtyputils.is_abstract(ir_typeref.subtypes[0])\n                    and irtyputils.is_scalar(ir_typeref.subtypes[0]))):\n            return ('anyrange',)\n        else:\n            tp = pg_type_from_ir_typeref(\n                ir_typeref.subtypes[0],\n                serialized=serialized,\n                persistent_tuples=persistent_tuples)\n            return pg_type_range(tp)\n\n    elif irtyputils.is_multirange(ir_typeref):\n        if (irtyputils.is_generic(ir_typeref)\n                or (irtyputils.is_abstract(ir_typeref.subtypes[0])\n                    and irtyputils.is_scalar(ir_typeref.subtypes[0]))):\n            return ('anymultirange',)\n        else:\n            tp = pg_type_from_ir_typeref(\n                ir_typeref.subtypes[0],\n                serialized=serialized,\n                persistent_tuples=persistent_tuples)\n            return pg_type_multirange(tp)\n\n    elif irtyputils.is_anytuple(ir_typeref):\n        return ('record',)\n\n    elif irtyputils.is_tuple(ir_typeref):\n        if ir_typeref.material_type:\n            material = ir_typeref.material_type\n        else:\n            material = ir_typeref\n\n        if persistent_tuples or material.in_schema:\n            return cast(\n                tuple[str, str],\n                common.get_tuple_backend_name(material.id, catenate=False),\n            )\n        else:\n            return ('record',)\n\n    elif irtyputils.is_any(ir_typeref) or irtyputils.is_anyobject(ir_typeref):\n        return ('anyelement',)\n\n    else:\n        if ir_typeref.material_type:\n            material = ir_typeref.material_type\n        else:\n            material = ir_typeref\n\n        if irtyputils.is_object(material):\n            if serialized:\n                return ('record',)\n            else:\n                return ('uuid',)\n        elif irtyputils.is_abstract(material):\n            return ('anynonarray',)\n        elif material.custom_sql_serialization and serialized:\n            return tuple(material.custom_sql_serialization.split('.'))\n        elif material.sql_type:\n            return tuple(material.sql_type.split('.'))\n        else:\n            pg_type = base_type_name_map.get(material.id)\n            if pg_type is None:\n                real_name_hint = material.orig_name_hint or material.name_hint\n                assert isinstance(real_name_hint, sn.QualName)\n                # User-defined scalar type\n                pg_type = common.get_scalar_backend_name(\n                    material.id, real_name_hint.module, catenate=False)\n\n            return pg_type\n\n\nTableInfo = tuple[tuple[str, str], str, str]\n\n\ndef _source_table_info(\n    schema: s_schema.Schema, pointer: s_pointers.Pointer,\n    versioned: bool,\n) -> TableInfo:\n    table = common.get_backend_name(\n        schema, not_none(pointer.get_source(schema)),\n        catenate=False, versioned=versioned,\n    )\n    ptr_name = pointer.get_shortname(schema).name\n    if ptr_name.startswith('__') or ptr_name == 'id':\n        col_name = ptr_name\n    else:\n        col_name = str(pointer.id)\n    table_type = 'ObjectType'\n\n    return table, table_type, col_name\n\n\ndef _pointer_table_info(\n    schema: s_schema.Schema, pointer: s_pointers.Pointer,\n    versioned: bool,\n) -> TableInfo:\n    table = common.get_backend_name(\n        schema, pointer, catenate=False, versioned=versioned)\n    col_name = 'target'\n    table_type = 'link'\n\n    return table, table_type, col_name\n\n\ndef _resolve_type(\n    schema: s_schema.Schema, pointer: s_pointers.Pointer\n) -> tuple[str, ...]:\n    column_type: tuple[str, ...]\n\n    pointer_target = pointer.get_target(schema)\n    if pointer_target is not None:\n        if pointer_target.is_object_type():\n            column_type = ('uuid',)\n        elif pointer_target.is_tuple(schema):\n            column_type = common.get_backend_name(\n                schema, pointer_target, catenate=False\n            )\n        else:\n            column_type = pg_type_from_object(\n                schema, pointer_target, persistent_tuples=True\n            )\n    else:\n        # The target may not be known in circular object-to-object\n        # linking scenarios.\n        column_type = ('uuid',)\n\n    return column_type\n\n\ndef _pointer_storable_in_source(\n    schema: s_schema.Schema, pointer: s_pointers.Pointer\n) -> bool:\n    return pointer.singular(schema)\n\n\ndef _pointer_storable_in_pointer(\n    schema: s_schema.Schema, pointer: s_pointers.Pointer\n) -> bool:\n    return not pointer.singular(schema) or pointer.has_user_defined_properties(\n        schema\n    )\n\n\n@lru.per_job_lru_cache()\ndef get_pointer_storage_info(\n    pointer: s_pointers.Pointer,\n    *,\n    schema: s_schema.Schema,\n    source: Optional[s_obj.InheritingObject] = None,\n    resolve_type: bool = True,\n    versioned: bool = True,\n    link_bias: bool = False,\n) -> PointerStorageInfo:\n    assert not pointer.is_non_concrete(\n        schema\n    ), \"only specialized pointers can be stored\"\n    if pointer.get_computable(schema):\n        material_ptrcls = None\n    else:\n        schema, material_ptrcls = pointer.material_type(schema)\n    if material_ptrcls is not None:\n        pointer = material_ptrcls\n\n    if source is None:\n        source = pointer.get_source(schema)\n\n    is_lprop = pointer.is_link_property(schema)\n\n    if resolve_type and schema is None:\n        msg = 'PointerStorageInfo needs a schema to resolve column_type'\n        raise ValueError(msg)\n\n    if is_lprop and pointer.issubclass(\n        schema, schema.get('std::target', type=s_obj.SubclassableObject)\n    ):\n        # Normalize link@target to link\n        assert isinstance(source, s_pointers.Pointer)\n        pointer = source\n        is_lprop = False\n\n    if isinstance(pointer, irast.TupleIndirectionLink):\n        table = None\n        table_type = 'ObjectType'\n        col_name = pointer.get_shortname(schema).name\n    elif is_lprop:\n        assert source\n        table = common.get_backend_name(\n            schema, source, catenate=False, versioned=versioned)\n        table_type = 'link'\n        if pointer.get_shortname(schema).name == 'source':\n            col_name = 'source'\n        else:\n            col_name = str(pointer.id)\n    else:\n        if isinstance(source, s_scalars.ScalarType):\n            # This is a pseudo-link on an scalar (__type__)\n            table = None\n            table_type = 'ObjectType'\n            col_name = None\n        elif _pointer_storable_in_source(schema, pointer) and not link_bias:\n            table, table_type, col_name = _source_table_info(\n                schema, pointer, versioned=versioned\n            )\n        elif _pointer_storable_in_pointer(schema, pointer):\n            table, table_type, col_name = _pointer_table_info(\n                schema, pointer, versioned=versioned,\n            )\n        else:\n            return None  # type: ignore\n\n    if resolve_type:\n        column_type = _resolve_type(schema, pointer)\n    else:\n        column_type = None\n\n    return PointerStorageInfo(\n        table_name=table,\n        table_type=table_type,\n        column_name=col_name,  # type: ignore\n        column_type=column_type,  # type: ignore\n    )\n\n\n@dataclasses.dataclass(kw_only=True, eq=False, slots=True)\nclass PointerStorageInfo:\n\n    table_name: Optional[tuple[str, str]]\n    table_type: str\n    column_name: str\n    column_type: tuple[str, str]\n\n\n@overload\ndef get_ptrref_storage_info(\n    ptrref: irast.BasePointerRef,\n    *,\n    resolve_type: bool = ...,\n    link_bias: Literal[False] = False,\n    allow_missing: Literal[False] = False,\n    versioned: bool = True,\n) -> PointerStorageInfo: ...\n\n\n@overload\ndef get_ptrref_storage_info(\n    ptrref: irast.BasePointerRef,\n    *,\n    resolve_type: bool = ...,\n    link_bias: bool = ...,\n    allow_missing: bool = ...,\n    versioned: bool = True,\n) -> Optional[PointerStorageInfo]: ...\n\n\ndef get_ptrref_storage_info(\n    ptrref: irast.BasePointerRef,\n    *,\n    resolve_type: bool = True,\n    link_bias: bool = False,\n    allow_missing: bool = False,\n    # XXX\n    versioned: bool = True,\n) -> Optional[PointerStorageInfo]:\n    # We wrap the real version because of bad mypy interactions\n    # with lru_cache.\n    return _get_ptrref_storage_info(\n        ptrref,\n        resolve_type=resolve_type,\n        link_bias=link_bias,\n        allow_missing=allow_missing,\n        versioned=versioned,\n    )\n\n\n@lru.per_job_lru_cache()\ndef _get_ptrref_storage_info(\n    ptrref: irast.BasePointerRef,\n    *,\n    resolve_type: bool = True,\n    link_bias: bool = False,\n    allow_missing: bool = False,\n    versioned: bool = False,\n) -> Optional[PointerStorageInfo]:\n\n    if ptrref.material_ptr:\n        ptrref = ptrref.material_ptr\n\n    if ptrref.out_cardinality is None:\n        # Guard against the IR generator failure to populate the PointerRef\n        # cardinality correctly.\n        raise RuntimeError(\n            f'cannot determine backend storage parameters for the '\n            f'{ptrref.name!r} pointer: the cardinality is not known')\n\n    target = ptrref.out_target\n\n    if isinstance(\n        ptrref, (irast.TupleIndirectionPointerRef, irast.SpecialPointerRef)\n    ):\n        table = None\n        table_type = 'ObjectType'\n        col_name = ptrref.shortname.name\n\n    elif ptrref.source_ptr is not None:\n        # link property\n        assert isinstance(ptrref, irast.PointerRef)\n        source_ptr = ptrref.source_ptr\n\n        table = common.get_pointer_backend_name(\n            source_ptr.id, source_ptr.name.module, catenate=False,\n            versioned=versioned,\n        )\n        table_type = 'link'\n        if ptrref.shortname.name in ('source', 'target'):\n            col_name = ptrref.shortname.name\n        else:\n            col_name = str(ptrref.id)\n    else:\n        assert isinstance(ptrref, irast.PointerRef)\n        source = ptrref.out_source\n\n        if irtyputils.is_scalar(source):\n            # This is a pseudo-link on an scalar (__type__)\n            table = None\n            table_type = 'ObjectType'\n            col_name = None\n\n        elif _ptrref_storable_in_source(ptrref) and not link_bias:\n            assert isinstance(source.name_hint, sn.QualName)\n            # XXX: TRAMPOLINE\n            table = common.get_objtype_backend_name(\n                source.id, source.name_hint.module, catenate=False,\n                versioned=versioned,\n\n            )\n            ptrname = ptrref.shortname.name\n            if ptrname.startswith('__') or ptrname == 'id':\n                col_name = ptrname\n            else:\n                col_name = str(ptrref.id)\n            table_type = 'ObjectType'\n\n        elif _ptrref_storable_in_pointer(ptrref):\n            table = common.get_pointer_backend_name(\n                ptrref.id, ptrref.name.module, catenate=False,\n                versioned=versioned)\n            col_name = 'target'\n            table_type = 'link'\n\n        elif not link_bias and not allow_missing:\n            raise RuntimeError(\n                f'cannot determine backend storage parameters for the '\n                f'{ptrref.name} pointer: unexpected characteristics')\n\n        else:\n            return None\n\n    column_type: tuple[str, ...] | None\n    if resolve_type:\n        if irtyputils.is_object(target):\n            column_type = ('uuid',)\n        else:\n            column_type = pg_type_from_ir_typeref(\n                target, persistent_tuples=True)\n    else:\n        column_type = None\n\n    return PointerStorageInfo(\n        table_name=table,\n        table_type=table_type,\n        column_name=col_name,  # type: ignore\n        column_type=column_type,  # type: ignore\n    )\n\n\ndef _ptrref_storable_in_source(ptrref: irast.BasePointerRef) -> bool:\n    return ptrref.out_cardinality.is_single()\n\n\ndef _ptrref_storable_in_pointer(ptrref: irast.BasePointerRef) -> bool:\n    if ptrref.union_components:\n        return all(\n            _ptrref_storable_in_pointer(c) for c in ptrref.union_components\n        )\n    else:\n        return (\n            ptrref.out_cardinality.is_multi()\n            or ptrref.has_properties\n        )\n\n\ndef has_table(\n    obj: Optional[s_obj.InheritingObject], schema: s_schema.Schema\n) -> bool:\n    \"\"\"Returns True for all schema objects that need a postgres table\"\"\"\n    assert obj\n\n    if isinstance(obj, s_objtypes.ObjectType):\n        return not (\n            obj.is_compound_type(schema) or\n            obj.get_is_derived(schema) or\n            obj.is_view(schema)\n        )\n\n    assert isinstance(obj, s_pointers.Pointer)\n\n    if obj.is_pure_computable(schema) or obj.get_is_derived(schema):\n        return False\n    elif obj.is_non_concrete(schema):\n        return (\n            not isinstance(obj, s_properties.Property)\n            and str(obj.get_name(schema)) != 'std::link'\n        )\n    elif obj.is_link_property(schema):\n        return not obj.singular(schema)\n    elif not has_table(obj.get_source(schema), schema):\n        return False\n    else:\n        ptr_stor_info = get_pointer_storage_info(\n            obj, resolve_type=False, schema=schema, link_bias=True)\n\n        return (\n            ptr_stor_info is not None\n            and ptr_stor_info.table_type == 'link'\n        )\n"
  },
  {
    "path": "edb/protocol/.gitignore",
    "content": "/*.c\n"
  },
  {
    "path": "edb/protocol/README",
    "content": "Protocol documentation and testing utilities.\nThe actual protocol implementation is in the edb.server package.\n"
  },
  {
    "path": "edb/protocol/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport enum\n\nfrom . import messages\nfrom . import render_utils\n\nfrom .messages import *  # NoQA\n\n\ndef render(\n    obj: type[enum.Enum] | type[messages.Struct]\n) -> str:\n    if issubclass(obj, messages.Struct):\n        return obj.render()\n    else:\n        assert issubclass(obj, enum.Enum)\n\n        buf = render_utils.RenderBuffer()\n        buf.write(f'enum {obj.__name__} {{')\n        with buf.indent():\n            for membername, member in obj.__members__.items():\n                buf.write(\n                    f'{membername.ljust(messages._PAD - 1)} = '\n                    f'{member.value:#x};'\n                )\n        buf.write('};')\n        return str(buf)\n"
  },
  {
    "path": "edb/protocol/enums.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport enum\n\n\nclass Cardinality(enum.Enum):\n    # Cardinality isn't applicable for the query:\n    # * the query is a command like CONFIGURE that\n    #   does not return any data;\n    # * the query is composed of multiple queries.\n    NO_RESULT = 0x6e\n\n    # Cardinality is 1 or 0\n    AT_MOST_ONE = 0x6f\n\n    # Cardinality is 1\n    ONE = 0x41\n\n    # Cardinality is >= 0\n    MANY = 0x6d\n\n    # Cardinality is >= 1\n    AT_LEAST_ONE = 0x4d\n"
  },
  {
    "path": "edb/protocol/messages.py",
    "content": "# mypy: ignore-errors\n\n#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nimport enum\nimport io\nimport typing\n\nfrom edb.common import binwrapper\nfrom .enums import Cardinality\n\nfrom . import render_utils\n\n\n_PAD = 16\n\n\nclass CType:\n    pass\n\n\nclass Scalar(CType):\n\n    cname = None\n\n    def __init__(\n        self, doc: typing.Optional[str] = None, *, default: typing.Any = None\n    ) -> None:\n        self.doc = doc\n        self.default = default\n\n    def validate(self, val: typing.Any) -> bool:\n        raise NotImplementedError\n\n    def parse(self, buffer: binwrapper.BinWrapper) -> any:\n        raise NotImplementedError\n\n    def dump(self, val: typing.Any, buffer: binwrapper.BinWrapper) -> None:\n        raise NotImplementedError\n\n    def render_field(\n        self, fieldname: str, buf: render_utils.RenderBuffer\n    ) -> None:\n        cname = self.cname\n        if cname is None:\n            raise NotImplementedError\n\n        if self.default and isinstance(self.default, int):\n            buf.write(\n                f'{cname.ljust(_PAD - 1)} {fieldname} = {self.default:#x};')\n        elif self.default:\n            buf.write(\n                f'{cname.ljust(_PAD - 1)} {fieldname} = {self.default};')\n        else:\n            buf.write(\n                f'{cname.ljust(_PAD - 1)} {fieldname};')\n\n\nclass UInt8(Scalar):\n\n    cname = 'uint8'\n\n    def validate(self, val: typing.Any) -> bool:\n        return isinstance(val, int) and (0 <= val <= 255)\n\n    def parse(self, buffer: binwrapper.BinWrapper) -> any:\n        return buffer.read_ui8()\n\n    def dump(self, val: int, buffer: binwrapper.BinWrapper) -> None:\n        buffer.write_ui8(val)\n\n\nclass UInt16(Scalar):\n\n    cname = 'uint16'\n\n    def validate(self, val: typing.Any) -> bool:\n        return isinstance(val, int) and (0 <= val <= 2 ** 16 - 1)\n\n    def parse(self, buffer: binwrapper.BinWrapper) -> any:\n        return buffer.read_ui16()\n\n    def dump(self, val: int, buffer: binwrapper.BinWrapper) -> None:\n        buffer.write_ui16(val)\n\n\nclass UInt32(Scalar):\n\n    cname = 'uint32'\n\n    def validate(self, val: typing.Any) -> bool:\n        return isinstance(val, int) and (0 <= val <= 2 ** 32 - 1)\n\n    def parse(self, buffer: binwrapper.BinWrapper) -> any:\n        return buffer.read_ui32()\n\n    def dump(self, val: int, buffer: binwrapper.BinWrapper) -> None:\n        buffer.write_ui32(val)\n\n\nclass UInt64(Scalar):\n\n    cname = 'uint64'\n\n    def validate(self, val: typing.Any) -> bool:\n        return isinstance(val, int) and (0 <= val <= 2 ** 64 - 1)\n\n    def parse(self, buffer: binwrapper.BinWrapper) -> any:\n        return buffer.read_ui64()\n\n    def dump(self, val: int, buffer: binwrapper.BinWrapper) -> None:\n        buffer.write_ui64(val)\n\n\nclass Bytes(Scalar):\n\n    cname = 'bytes'\n\n    def validate(self, val: typing.Any) -> bool:\n        return isinstance(val, bytes)\n\n    def parse(self, buffer: binwrapper.BinWrapper) -> any:\n        return buffer.read_len32_prefixed_bytes()\n\n    def dump(self, val: bytes, buffer: binwrapper.BinWrapper) -> None:\n        buffer.write_len32_prefixed_bytes(val)\n\n\nclass String(Scalar):\n\n    cname = 'string'\n\n    def validate(self, val: typing.Any) -> bool:\n        return isinstance(val, str)\n\n    def parse(self, buffer: binwrapper.BinWrapper) -> any:\n        return buffer.read_len32_prefixed_bytes().decode('utf-8')\n\n    def dump(self, val: str, buffer: binwrapper.BinWrapper) -> None:\n        buffer.write_len32_prefixed_bytes(val.encode('utf-8'))\n\n\nclass UUID(Scalar):\n\n    cname = 'uuid'\n\n    def validate(self, val: typing.Any) -> bool:\n        return isinstance(val, bytes) and len(val) == 16\n\n    def parse(self, buffer: binwrapper.BinWrapper) -> any:\n        return buffer.read_bytes(16)\n\n    def dump(self, val: bytes, buffer: binwrapper.BinWrapper) -> None:\n        assert isinstance(val, bytes) and len(val) == 16\n        buffer.write_bytes(val)\n\n\nclass ArrayOf(CType):\n\n    def __init__(\n        self,\n        length_in: type[CType],\n        element: CType | type[Struct],\n        doc: str = None,\n    ) -> None:\n        self.length_in = length_in()\n        self.element = element\n        self.doc = doc\n\n    def validate(self, val: typing.Any) -> bool:\n        if not isinstance(val, list) or not self.length_in.validate(len(val)):\n            return False\n\n        if isinstance(self.element, CType):\n            return all(self.element.validate(x) for x in val)\n        else:\n            return all(isinstance(x, self.element) for x in val)\n\n    def parse(self, buffer: binwrapper.BinWrapper) -> any:\n        length = self.length_in.parse(buffer)\n        result = []\n        for _ in range(length):\n            result.append(self.element.parse(buffer))\n        return result\n\n    def dump(self, val: list, buffer: binwrapper.BinWrapper) -> None:\n        self.length_in.dump(len(val), buffer)\n        for el in val:\n            self.element.dump(el, buffer)\n\n    def render_field(\n        self, fieldname: str, buf: render_utils.RenderBuffer\n    ) -> None:\n        self.length_in.render_field(f'num_{fieldname}', buf)\n        self.element.render_field(f'{fieldname}[num_{fieldname}]', buf)\n\n\nclass FixedArrayOf(CType):\n\n    def __init__(\n        self,\n        length: int,\n        element: CType | type[Struct],\n        doc: typing.Optional[str]=None\n    ) -> None:\n        self.length = length\n        self.element = element\n        self.doc = doc\n\n    def validate(self, val: typing.Any) -> bool:\n        if not isinstance(val, list) or len(val) != self.length:\n            return False\n\n        if isinstance(self.element, CType):\n            return all(self.element.validate(x) for x in val)\n        else:\n            return all(isinstance(x, self.element) for x in val)\n\n    def parse(self, buffer: binwrapper.BinWrapper) -> any:\n        result = []\n        for _ in range(self.length):\n            result.append(self.element.parse(buffer))\n        return result\n\n    def dump(self, val: list, buffer: binwrapper.BinWrapper) -> None:\n        assert len(val) == self.length\n        self.length_in.dump(self.length, buffer)\n        for el in val:\n            self.element.dump(el, buffer)\n\n    def render_field(\n        self, fieldname: str, buf: render_utils.RenderBuffer\n    ) -> None:\n        self.element.render_field(f'{fieldname}[{self.length}]', buf)\n\n\nclass EnumOf(CType):\n\n    def __init__(\n        self,\n        value_in: type[Scalar],\n        enum: type[enum.Enum],\n        doc: typing.Optional[str]=None,\n    ) -> None:\n        self.value_in = value_in()\n        self.enum = enum\n        self.doc = doc\n\n    def validate(self, val: typing.Any) -> bool:\n        if isinstance(val, self.enum):\n            return True\n        if not self.value_in.validate(val):\n            return False\n        try:\n            self.enum(val)\n        except ValueError:\n            return False\n        else:\n            return True\n\n    def parse(self, buffer: binwrapper.BinWrapper) -> any:\n        result = self.value_in.parse(buffer)\n        return self.enum(result)\n\n    def dump(self, val: typing.Any, buffer: binwrapper.BinWrapper) -> None:\n        self.value_in.dump(val.value, buffer)\n\n    def render_field(\n        self, fieldname: str, buf: render_utils.RenderBuffer\n    ) -> None:\n        typename = f'{self.value_in.cname}<{self.enum.__name__}>'\n        buf.write(f'{typename.ljust(_PAD - 1)} {fieldname};')\n\n\nclass Struct:\n\n    _fields: dict[str, CType | type[Struct]] = {}\n\n    def __init_subclass__(cls, *, abstract=False):\n        if abstract:\n            return\n\n        fields = {}\n\n        for name in cls.__dict__:\n            attr = cls.__dict__[name]\n            if name.startswith('__') or callable(attr):\n                continue\n\n            if not isinstance(attr, CType):\n                raise TypeError(\n                    f'field {cls.__name__}.{name!r} must be a Type')\n            else:\n                fields[name] = attr\n\n        cls._fields = fields\n\n    def __init__(self, **args: typing.Any):\n        for fieldname in ['mtype', 'message_length']:\n            if fieldname in args:\n                raise ValueError(\n                    f'cannot construct instance of {type(self).__name__}: '\n                    f'{fieldname!r} field is not supposed to be passed to '\n                    f'the constructor')\n\n        for fieldname, field in type(self)._fields.items():\n            if fieldname in ['mtype', 'message_length']:\n                continue\n            try:\n                arg = args[fieldname]\n            except KeyError:\n                raise ValueError(\n                    f'cannot construct instance of {type(self).__name__}: '\n                    f'the {fieldname!r} field is missing')\n            if (\n                isinstance(field, CType) and not field.validate(arg) or\n                isinstance(field, type) and not isinstance(arg, field)\n            ):\n                raise ValueError(\n                    f'cannot construct instance of {type(self).__name__}: '\n                    f'invalid value {arg!r} for the {fieldname!r} field')\n\n            setattr(self, fieldname, arg)\n\n    @classmethod\n    def parse(cls, buffer: binwrapper.BinWrapper) -> Struct:\n        kwargs: dict[str, any] = {}\n        for fieldname, field in cls._fields.items():\n            if fieldname in {'mtype', 'message_length'}:\n                continue\n            kwargs[fieldname] = field.parse(buffer)\n        return cls(**kwargs)\n\n    @classmethod\n    def dump(cls, val: Struct, buffer: binwrapper.BinWrapper) -> None:\n        fields = val._fields\n        for fieldname, field in fields.items():\n            if fieldname in {'mtype', 'message_length'}:\n                continue\n            fval = getattr(val, fieldname)\n            field.dump(fval, buffer)\n\n    def __repr__(self):\n        res = [f'<{type(self).__name__}']\n        for fieldname in type(self)._fields:\n            if fieldname in {'mtype', 'message_length'}:\n                continue\n            val = getattr(self, fieldname)\n            res.append(f' {fieldname}={val!r}')\n        res.append('>')\n        return ''.join(res)\n\n    @classmethod\n    def render_field(\n        cls, fieldname: str, buf: render_utils.RenderBuffer\n    ) -> None:\n        buf.write(f'{cls.__name__.ljust(_PAD - 1)} {fieldname};')\n\n    @classmethod\n    def render(cls) -> str:\n        buf = render_utils.RenderBuffer()\n\n        buf.write(f'struct {cls.__name__} {{')\n        with buf.indent():\n            for fieldname, field in cls._fields.items():\n                if field.doc:\n                    buf.write_comment(field.doc)\n                field.render_field(fieldname, buf)\n                buf.newline()\n\n        if buf.lastline() == '':\n            buf.popline()\n\n        buf.write('};')\n        return str(buf)\n\n\nclass KeyValue(Struct):\n    code = UInt16('Key code (specific to the type of the Message).')\n    value = Bytes('Value data.')\n\n\nclass Annotation(Struct):\n    name = String('Name of the annotation')\n    value = String('Value of the annotation (in JSON format).')\n\n\nKeyValues = ArrayOf(UInt16, KeyValue, 'A set of key-value pairs.')\nAnnotations = ArrayOf(UInt16, Annotation, 'A set of annotations.')\nMessageLength = UInt32('Length of message contents in bytes, including self.')\n\nMessageType = (lambda letter: UInt8(f\"Message type ('{letter}').\",\n                                    default=ord(letter)))\n\n\nclass Message(Struct, abstract=True):\n    pass\n\n\nclass ServerMessage(Message, abstract=True):\n\n    index: dict[int, list[type[ServerMessage]]] = {}\n\n    def __init_subclass__(cls):\n        super().__init_subclass__()\n\n        if 'mtype' not in cls._fields:\n            raise TypeError(f'mtype field is missing for {cls}')\n        if 'message_length' not in cls._fields:\n            raise TypeError(f'message_length field is missing for {cls}')\n\n        cls.index.setdefault(cls._fields['mtype'].default, []).append(cls)\n\n    @classmethod\n    def parse(cls, mtype: int, data: bytes) -> ServerMessage:\n        iobuf = io.BytesIO(data)\n        buffer = binwrapper.BinWrapper(iobuf)\n\n        kwargs: dict[str, any] = {}\n\n        msg_types = cls.index.get(mtype)\n        if not msg_types:\n            raise ValueError(f\"unspecced message type {chr(mtype)!r}\")\n        if len(msg_types) > 1:\n            raise ValueError(f\"multiple specs for message type {chr(mtype)!r}\")\n        msg_type = msg_types[0]\n\n        for fieldname, field in msg_type._fields.items():\n            if fieldname in {'mtype', 'message_length'}:\n                continue\n            kwargs[fieldname] = field.parse(buffer)\n\n        if len(iobuf.read(1)):\n            raise ValueError(\n                f'buffer is not empty after parsing {chr(mtype)!r} message')\n\n        return msg_type(**kwargs)\n\n\nclass ClientMessage(Message, abstract=True):\n\n    def __init_subclass__(cls):\n        super().__init_subclass__()\n\n        if 'mtype' not in cls._fields:\n            raise TypeError(f'mtype field is missing for {cls}')\n        if 'message_length' not in cls._fields:\n            raise TypeError(f'message_length field is missing for {cls}')\n\n    def dump(self) -> bytes:\n        iobuf = io.BytesIO()\n        buf = binwrapper.BinWrapper(iobuf)\n        fields = type(self)._fields\n        for fieldname, field in fields.items():\n            if fieldname in {'mtype', 'message_length'}:\n                continue\n            val = getattr(self, fieldname)\n            field.dump(val, buf)\n\n        dumped = iobuf.getvalue()\n        return (\n            fields['mtype'].default.to_bytes(1, 'big') +\n            (len(dumped) + 4).to_bytes(4, 'big') +\n            dumped\n        )\n\n\n###############################################################################\n# Protocol Messages Definitions\n###############################################################################\n\n\nclass InputLanguage(enum.Enum):\n\n    EDGEQL = 0x45  # b'E'\n    SQL = 0x53  # b'S'\n\n\nclass OutputFormat(enum.Enum):\n\n    BINARY = 0x62\n    JSON = 0x6a\n    JSON_ELEMENTS = 0x4a\n    NONE = 0x6e\n\n\nclass Capability(enum.IntFlag):\n\n    MODIFICATIONS     = 1 << 0    # noqa\n    SESSION_CONFIG    = 1 << 1    # noqa\n    TRANSACTION       = 1 << 2    # noqa\n    DDL               = 1 << 3    # noqa\n    PERSISTENT_CONFIG = 1 << 4    # noqa\n    ALL               = 0xFFFFFFFFFFFFFFFF  # noqa\n\n\nclass CompilationFlag(enum.IntFlag):\n\n    INJECT_OUTPUT_TYPE_IDS   = 1 << 0    # noqa\n    INJECT_OUTPUT_TYPE_NAMES = 1 << 1    # noqa\n    INJECT_OUTPUT_OBJECT_IDS = 1 << 2    # noqa\n\n\nclass DumpFlag(enum.IntFlag):\n\n    DUMP_SECRETS = 1 << 0    # noqa\n\n\nclass ErrorSeverity(enum.Enum):\n    ERROR = 120\n    FATAL = 200\n    PANIC = 255\n\n\nclass ErrorResponse(ServerMessage):\n\n    mtype = MessageType('E')\n    message_length = MessageLength\n    severity = EnumOf(UInt8, ErrorSeverity, 'Message severity.')\n    error_code = UInt32('Message code.')\n    message = String('Error message.')\n    attributes = ArrayOf(UInt16, KeyValue, 'Error attributes.')\n\n\nclass MessageSeverity(enum.Enum):\n    DEBUG = 20\n    INFO = 40\n    NOTICE = 60\n    WARNING = 80\n\n\nclass LogMessage(ServerMessage):\n\n    mtype = MessageType('L')\n    message_length = MessageLength\n    severity = EnumOf(UInt8, MessageSeverity, 'Message severity.')\n    code = UInt32('Message code.')\n    text = String('Message text.')\n    annotations = ArrayOf(UInt16, Annotation, 'Message annotations.')\n\n\nclass TransactionState(enum.Enum):\n\n    NOT_IN_TRANSACTION = 0x49\n    IN_TRANSACTION = 0x54\n    IN_FAILED_TRANSACTION = 0x45\n\n\nclass ReadyForCommand(ServerMessage):\n\n    mtype = MessageType('Z')\n    message_length = MessageLength\n    annotations = Annotations\n    transaction_state = EnumOf(UInt8, TransactionState, 'Transaction state.')\n\n\nclass RestoreReady(ServerMessage):\n\n    mtype = MessageType('+')\n    message_length = MessageLength\n    annotations = Annotations\n    jobs = UInt16('Number of parallel jobs for restore, currently always \"1\"')\n\n\nclass DataElement(Struct):\n\n    data = ArrayOf(UInt32, UInt8(), 'Encoded output data.')\n\n\nclass CommandComplete(ServerMessage):\n\n    mtype = MessageType('C')\n    message_length = MessageLength\n    annotations = Annotations\n    capabilities = EnumOf(UInt64, Capability,\n                          'A bit mask of allowed capabilities.')\n    status = String('Command status.')\n\n    state_typedesc_id = UUID('State data descriptor ID.')\n    state_data = Bytes('Encoded state data.')\n\n\nclass CommandDataDescription(ServerMessage):\n\n    mtype = MessageType('T')\n    message_length = MessageLength\n    annotations = Annotations\n    capabilities = EnumOf(UInt64, Capability,\n                          'A bit mask of allowed capabilities.')\n    result_cardinality = EnumOf(\n        UInt8, Cardinality, 'Actual result cardinality.')\n    input_typedesc_id = UUID('Argument data descriptor ID.')\n    input_typedesc = Bytes('Argument data descriptor.')\n    output_typedesc_id = UUID('Output data descriptor ID.')\n    output_typedesc = Bytes('Output data descriptor.')\n\n\nclass StateDataDescription(ServerMessage):\n\n    mtype = MessageType('s')\n    message_length = MessageLength\n    typedesc_id = UUID('Updated state data descriptor ID.')\n    typedesc = Bytes('State data descriptor.')\n\n\nclass Data(ServerMessage):\n\n    mtype = MessageType('D')\n    message_length = MessageLength\n\n    data = ArrayOf(\n        UInt16,\n        DataElement,\n        'Encoded output data array. The array is currently always of size 1.'\n    )\n\n\nclass DumpTypeInfo(Struct):\n\n    type_name = String()\n    type_class = String()\n    type_id = UUID()\n\n\nclass DumpObjectDesc(Struct):\n\n    object_id = UUID()\n    description = Bytes()\n    dependencies = ArrayOf(UInt16, UUID())\n\n\nclass DumpHeader(ServerMessage):\n\n    mtype = MessageType('@')\n    message_length = MessageLength\n    attributes = KeyValues\n    major_ver = UInt16('Major version of Gel.')\n    minor_ver = UInt16('Minor version of Gel.')\n    schema_ddl = String('Schema.')\n    types = ArrayOf(UInt32, DumpTypeInfo, 'Type identifiers.')\n    descriptors = ArrayOf(UInt32, DumpObjectDesc, 'Object descriptors.')\n\n\nclass DumpBlock(ServerMessage):\n\n    mtype = MessageType('=')\n    message_length = MessageLength\n    attributes = KeyValues\n\n\nclass ServerKeyData(ServerMessage):\n\n    mtype = MessageType('K')\n    message_length = MessageLength\n    data = FixedArrayOf(32, UInt8(), 'Key data.')\n\n\nclass ParameterStatus(ServerMessage):\n\n    mtype = MessageType('S')\n    message_length = MessageLength\n    name = Bytes('Parameter name.')\n    value = Bytes('Parameter value.')\n\n\nclass ParameterStatus_SystemConfig(Struct):\n\n    typedesc = ArrayOf(UInt32, UInt8(), 'Type descriptor prefixed with '\n                                        'type descriptor uuid.')\n    data = FixedArrayOf(1, DataElement, 'Configuration settings data.')\n\n\nclass ProtocolExtension(Struct):\n\n    name = String('Extension name.')\n    annotations = ArrayOf(UInt16, Annotation, 'A set of extension annotaions.')\n\n\nclass ServerHandshake(ServerMessage):\n\n    mtype = MessageType('v')\n    message_length = MessageLength\n    major_ver = UInt16('maximum supported or client-requested '\n                       'protocol major version, whichever is greater.')\n    minor_ver = UInt16('maximum supported or client-requested '\n                       'protocol minor version, whichever is greater.')\n    extensions = ArrayOf(\n        UInt16, ProtocolExtension, 'Supported protocol extensions.')\n\n\nclass AuthenticationOK(ServerMessage):\n\n    mtype = MessageType('R')\n    message_length = MessageLength\n    auth_status = UInt32('Specifies that this message contains '\n                         'a successful authentication indicator.',\n                         default=0x0)\n\n\nclass AuthenticationRequiredSASLMessage(ServerMessage):\n\n    mtype = MessageType('R')\n    message_length = MessageLength\n    auth_status = UInt32('Specifies that this message contains '\n                         'a SASL authentication request.',\n                         default=0x0A)\n    methods = ArrayOf(UInt32, String(),\n                      'A list of supported SASL authentication methods.')\n\n\nclass AuthenticationSASLContinue(ServerMessage):\n\n    mtype = MessageType('R')\n    message_length = MessageLength\n    auth_status = UInt32('Specifies that this message contains '\n                         'a SASL challenge.',\n                         default=0x0B)\n    sasl_data = Bytes('Mechanism-specific SASL data.')\n\n\nclass AuthenticationSASLFinal(ServerMessage):\n\n    mtype = MessageType('R')\n    message_length = MessageLength\n    auth_status = UInt32('Specifies that SASL authentication '\n                         'has completed.',\n                         default=0x0C)\n    sasl_data = Bytes()\n\n\nclass Dump(ClientMessage):\n\n    mtype = MessageType('>')\n    message_length = MessageLength\n    annotations = Annotations\n    flags = EnumOf(UInt64, DumpFlag, 'A bit mask of dump options.')\n\n\nclass Sync(ClientMessage):\n\n    mtype = MessageType('S')\n    message_length = MessageLength\n\n\nclass Flush(ClientMessage):\n\n    mtype = MessageType('H')\n    message_length = MessageLength\n\n\nclass Restore(ClientMessage):\n\n    mtype = MessageType('<')\n    message_length = MessageLength\n    attributes = KeyValues\n    jobs = UInt16(\n        'Number of parallel jobs for restore (only \"1\" is supported)')\n    header_data = Bytes(\n        'Original DumpHeader packet data excluding mtype and message_length')\n\n\nclass RestoreBlock(ClientMessage):\n\n    mtype = MessageType('=')\n    message_length = MessageLength\n    block_data = Bytes(\n        'Original DumpBlock packet data excluding mtype and message_length')\n\n\nclass RestoreEof(ClientMessage):\n\n    mtype = MessageType('.')\n    message_length = MessageLength\n\n\nclass Parse(ClientMessage):\n\n    mtype = MessageType('P')\n    message_length = MessageLength\n    annotations = Annotations\n    allowed_capabilities = EnumOf(UInt64, Capability,\n                                  'A bit mask of allowed capabilities.')\n    compilation_flags = EnumOf(UInt64, CompilationFlag,\n                               'A bit mask of query options.')\n    implicit_limit = UInt64('Implicit LIMIT clause on returned sets.')\n    input_language = EnumOf(UInt8, InputLanguage, 'Command source language.')\n    output_format = EnumOf(UInt8, OutputFormat, 'Data output format.')\n    expected_cardinality = EnumOf(UInt8, Cardinality,\n                                  'Expected result cardinality.')\n    command_text = String('Command text.')\n    state_typedesc_id = UUID('State data descriptor ID.')\n    state_data = Bytes('Encoded state data.')\n\n\nclass Execute(ClientMessage):\n\n    mtype = MessageType('O')\n    message_length = MessageLength\n    annotations = Annotations\n    allowed_capabilities = EnumOf(UInt64, Capability,\n                                  'A bit mask of allowed capabilities.')\n    compilation_flags = EnumOf(UInt64, CompilationFlag,\n                               'A bit mask of query options.')\n    implicit_limit = UInt64('Implicit LIMIT clause on returned sets.')\n    input_language = EnumOf(UInt8, InputLanguage, 'Command source language.')\n    output_format = EnumOf(UInt8, OutputFormat, 'Data output format.')\n    expected_cardinality = EnumOf(UInt8, Cardinality,\n                                  'Expected result cardinality.')\n    command_text = String('Command text.')\n    state_typedesc_id = UUID('State data descriptor ID.')\n    state_data = Bytes('Encoded state data.')\n\n    input_typedesc_id = UUID('Argument data descriptor ID.')\n    output_typedesc_id = UUID('Output data descriptor ID.')\n    arguments = Bytes('Encoded argument data.')\n\n\nclass ConnectionParam(Struct):\n\n    name = String()\n    value = String()\n\n\nclass ClientHandshake(ClientMessage):\n\n    mtype = MessageType('V')\n    message_length = MessageLength\n    major_ver = UInt16('Requested protocol major version.')\n    minor_ver = UInt16('Requested protocol minor version.')\n    params = ArrayOf(UInt16, ConnectionParam, 'Connection parameters.')\n    extensions = ArrayOf(\n        UInt16, ProtocolExtension, 'Requested protocol extensions.')\n\n\nclass Terminate(ClientMessage):\n\n    mtype = MessageType('X')\n    message_length = MessageLength\n\n\nclass AuthenticationSASLInitialResponse(ClientMessage):\n\n    mtype = MessageType('p')\n    message_length = MessageLength\n    method = String('Name of the SASL authentication mechanism '\n                    'that the client selected.')\n    sasl_data = Bytes('Mechanism-specific \"Initial Response\" data.')\n\n\nclass AuthenticationSASLResponse(ClientMessage):\n\n    mtype = MessageType('r')\n    message_length = MessageLength\n    sasl_data = Bytes('Mechanism-specific response data.')\n"
  },
  {
    "path": "edb/protocol/protocol.pxd",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom gel.protocol.asyncio_proto cimport AsyncIOProtocol\n\n\ncdef class Protocol(AsyncIOProtocol):\n    cdef:\n        public bytes last_state\n\n    cdef parse_command_complete_message(self)\n    cdef encode_state(self, state)\n\n\ncdef class Connection:\n\n    cdef:\n        object _transport\n        readonly list inbox\n        AsyncIOProtocol _protocol\n"
  },
  {
    "path": "edb/protocol/protocol.pyi",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import Any\n\nfrom . import messages\n\nclass Connection:\n    async def connect(self) -> None:\n        ...\n\n    async def execute(self, query: str, state_id: bytes, state: bytes) -> None:\n        ...\n\n    async def sync(self) -> bytes:\n        ...\n\n    async def recv(self) -> messages.ServerMessage:\n        ...\n\n    async def recv_match(\n        self,\n        msgcls: type[messages.ServerMessage],\n        _ignore_msg: type[messages.ServerMessage] | None,\n        **fields: Any,\n    ) -> messages.ServerMessage:\n        ...\n\n    async def send(self, *msgs: messages.ClientMessage) -> None:\n        ...\n\n    async def aclose(self) -> None:\n        ...\n"
  },
  {
    "path": "edb/protocol/protocol.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport asyncio\nimport re\nimport time\n\nfrom gel import con_utils\nfrom gel import enums\nfrom gel.protocol.asyncio_proto cimport AsyncIOProtocol\nfrom gel.protocol.protocol cimport ReadBuffer, WriteBuffer\n\nfrom . import messages\n\n\ncdef class Protocol(AsyncIOProtocol):\n    cdef parse_command_complete_message(self):\n        cdef WriteBuffer buf = WriteBuffer.new()\n        self.ignore_headers()\n        self.last_capabilities = enums.Capability(self.buffer.read_int64())\n        self.last_status = self.buffer.read_len_prefixed_bytes()\n        state_typedesc_id = self.buffer.read_bytes(16)\n        buf.write_len_prefixed_bytes(self.buffer.read_len_prefixed_bytes())\n        if state_typedesc_id != b'\\x00' * 16:\n            self.last_state = bytes(buf)\n        self.buffer.finish_message()\n\n    cdef encode_state(self, state):\n        if self.last_state is None:\n            return AsyncIOProtocol.encode_state(self, None)\n        else:\n            return self.state_type_id, self.last_state\n\n\ncdef class Connection:\n\n    def __init__(self, pr, tr):\n        self._protocol = pr\n        self._transport = tr\n        self.inbox = []\n\n    async def connect(self):\n        await self._protocol.connect()\n\n    async def execute(self, query, state_id=b'\\0' * 16, state=b''):\n        await self.send(\n            messages.Execute(\n                annotations=[],\n                command_text=query,\n                input_language=messages.InputLanguage.EDGEQL,\n                output_format=messages.OutputFormat.NONE,\n                expected_cardinality=messages.Cardinality.MANY,\n                allowed_capabilities=messages.Capability.ALL,\n                compilation_flags=messages.CompilationFlag(9),\n                implicit_limit=0,\n                input_typedesc_id=b'\\0' * 16,\n                output_typedesc_id=b'\\0' * 16,\n                state_typedesc_id=state_id,\n                arguments=b'',\n                state_data=state,\n            ),\n            messages.Sync(),\n        )\n        await self.recv_match(\n            messages.CommandComplete,\n            _ignore_msg=messages.StateDataDescription,\n        )\n        await self.recv_match(messages.ReadyForCommand)\n\n    async def sync(self):\n        await self.send(messages.Sync())\n        reply = await self.recv()\n        if not isinstance(reply, messages.ReadyForCommand):\n            raise AssertionError(\n                f'invalid response for Sync request: {reply!r}')\n        return reply.transaction_state\n\n    async def recv(self):\n        while True:\n            await self._protocol.wait_for_message()\n            mtype = self._protocol.buffer.get_message_type()\n            data = self._protocol.buffer.consume_message()\n            msg = messages.ServerMessage.parse(mtype, data)\n\n            if isinstance(msg, messages.LogMessage):\n                self.inbox.append(msg)\n                continue\n\n            return msg\n\n    async def recv_match(self, msgcls, _ignore_msg=None, **fields):\n        message = await self.recv()\n        if _ignore_msg is not None and isinstance(message, _ignore_msg):\n            message = await self.recv()\n        if not isinstance(message, msgcls):\n            raise AssertionError(\n                f'expected for {msgcls.__name__} message, received '\n                f'{type(message).__name__}: {message!r}')\n        for fieldname, expected in fields.items():\n            val = getattr(message, fieldname)\n            if isinstance(expected, str):\n                if not re.match(expected, val):\n                    raise AssertionError(\n                        f'{msgcls.__name__}.{fieldname} value {val!r} '\n                        f'does not match expected regexp {expected!r}')\n            else:\n                if expected != val:\n                    raise AssertionError(\n                        f'{msgcls.__name__}.{fieldname} value {val!r} '\n                        f'does not equal to expected {expected!r}')\n        return message\n\n    async def send(self, *msgs: messages.ClientMessage):\n        cdef WriteBuffer buf\n\n        for msg in msgs:\n            out = msg.dump()\n            buf = WriteBuffer.new()\n            buf.write_bytes(out)\n            self._protocol.write(buf)\n\n    async def aclose(self):\n        # TODO: Fix when edgedb-python implements proper cancellation\n        asyncio.get_running_loop().call_soon(lambda: self._protocol.abort())\n        await self._protocol.wait_for_disconnect()\n\n\nasync def new_connection(\n    dsn: str = None,\n    *,\n    host: str = None,\n    port: int = None,\n    user: str = None,\n    password: str = None,\n    secret_key: str = None,\n    branch: str = None,\n    database: str = None,\n    timeout: float = 60,\n    tls_ca: str = None,\n    tls_ca_file: str = None,\n    tls_security: str = 'default',\n    credentials: str = None,\n    credentials_file: str = None,\n    **kwargs\n):\n    connect_config, client_config = con_utils.parse_connect_arguments(\n        dsn=dsn,\n        host=host,\n        port=port,\n        user=user,\n        password=password,\n        secret_key=secret_key,\n        database=database,\n        branch=branch,\n        timeout=timeout,\n        command_timeout=None,\n        server_settings=None,\n        tls_ca=tls_ca,\n        tls_ca_file=tls_ca_file,\n        tls_security=tls_security,\n        tls_server_name=None,\n        wait_until_available=timeout,\n        credentials=credentials,\n        credentials_file=credentials_file,\n        **kwargs\n    )\n\n    loop = asyncio.get_running_loop()\n\n    last_error = None\n    addr = None\n    for addr in [connect_config.address]:\n        before = time.monotonic()\n        try:\n            if timeout <= 0:\n                raise asyncio.TimeoutError\n\n            protocol_factory = lambda: Protocol(connect_config, loop)\n\n            if isinstance(addr, str):\n                connector = loop.create_unix_connection(\n                    protocol_factory, addr)\n            else:\n                connector = loop.create_connection(\n                    protocol_factory, *addr,\n                    ssl=connect_config.ssl_ctx if tls_security else None,\n                )\n\n            before = time.monotonic()\n            try:\n                tr, pr = await asyncio.wait_for(connector, timeout=timeout)\n            finally:\n                timeout -= time.monotonic() - before\n\n            return Connection(pr, tr)\n\n        except (OSError, asyncio.TimeoutError, ConnectionError) as ex:\n            last_error = ex\n\n    raise last_error\n"
  },
  {
    "path": "edb/protocol/render_utils.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Optional\n\nimport contextlib\nimport textwrap\n\n\nclass RenderBuffer:\n\n    ilevel: int\n    buf: list[str]\n\n    def __init__(self):\n        self.ilevel = 0\n        self.buf = []\n\n    def write(self, line: str) -> None:\n        self.buf.append(' ' * (self.ilevel * 2) + line)\n\n    def newline(self) -> None:\n        self.buf.append('')\n\n    def lastline(self) -> Optional[str]:\n        return self.buf[-1] if len(self.buf) else None\n\n    def popline(self) -> str:\n        return self.buf.pop()\n\n    def write_comment(self, comment: str) -> None:\n        lines = textwrap.wrap(comment, width=40)\n        for line in lines:\n            self.write(f'// {line}')\n\n    def __str__(self):\n        return '\\n'.join(self.buf)\n\n    @contextlib.contextmanager\n    def indent(self):\n        self.ilevel += 1\n        try:\n            yield\n        finally:\n            self.ilevel -= 1\n"
  },
  {
    "path": "edb/schema/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2013-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom .name import QualName  # NOQA\n\nfrom .objects import Object, ObjectMeta  # NOQA\nfrom .schema import Schema  # NOQA\nfrom .modules import Module  # NOQA\n"
  },
  {
    "path": "edb/schema/_types.py",
    "content": "# AUTOGENERATED FROM \"edb/api/types.txt\" WITH\n#    $ edb gen-types\n\n\nfrom __future__ import annotations\n\n\nimport uuid\n\nfrom edb.common import uuidgen\nfrom edb.schema import name as sn\n\n\nUUID: type[uuid.UUID] = uuidgen.UUID\n\n\nTYPE_IDS = {\n    sn.name_from_string('anytype'):\n        UUID('00000000-0000-0000-0000-000000000001'),\n    sn.name_from_string('anytuple'):\n        UUID('00000000-0000-0000-0000-000000000002'),\n    sn.name_from_string('anyobject'):\n        UUID('00000000-0000-0000-0000-000000000003'),\n    sn.name_from_string('std'):\n        UUID('00000000-0000-0000-0000-0000000000f0'),\n    sn.name_from_string('empty-tuple'):\n        UUID('00000000-0000-0000-0000-0000000000ff'),\n    sn.name_from_string('std::uuid'):\n        UUID('00000000-0000-0000-0000-000000000100'),\n    sn.name_from_string('std::str'):\n        UUID('00000000-0000-0000-0000-000000000101'),\n    sn.name_from_string('std::bytes'):\n        UUID('00000000-0000-0000-0000-000000000102'),\n    sn.name_from_string('std::int16'):\n        UUID('00000000-0000-0000-0000-000000000103'),\n    sn.name_from_string('std::int32'):\n        UUID('00000000-0000-0000-0000-000000000104'),\n    sn.name_from_string('std::int64'):\n        UUID('00000000-0000-0000-0000-000000000105'),\n    sn.name_from_string('std::float32'):\n        UUID('00000000-0000-0000-0000-000000000106'),\n    sn.name_from_string('std::float64'):\n        UUID('00000000-0000-0000-0000-000000000107'),\n    sn.name_from_string('std::decimal'):\n        UUID('00000000-0000-0000-0000-000000000108'),\n    sn.name_from_string('std::bool'):\n        UUID('00000000-0000-0000-0000-000000000109'),\n    sn.name_from_string('std::datetime'):\n        UUID('00000000-0000-0000-0000-00000000010a'),\n    sn.name_from_string('std::duration'):\n        UUID('00000000-0000-0000-0000-00000000010e'),\n    sn.name_from_string('std::json'):\n        UUID('00000000-0000-0000-0000-00000000010f'),\n    sn.name_from_string('std::bigint'):\n        UUID('00000000-0000-0000-0000-000000000110'),\n    sn.name_from_string('std::cal::local_datetime'):\n        UUID('00000000-0000-0000-0000-00000000010b'),\n    sn.name_from_string('std::cal::local_date'):\n        UUID('00000000-0000-0000-0000-00000000010c'),\n    sn.name_from_string('std::cal::local_time'):\n        UUID('00000000-0000-0000-0000-00000000010d'),\n    sn.name_from_string('std::cal::relative_duration'):\n        UUID('00000000-0000-0000-0000-000000000111'),\n    sn.name_from_string('std::cal::date_duration'):\n        UUID('00000000-0000-0000-0000-000000000112'),\n    sn.name_from_string('cfg::memory'):\n        UUID('00000000-0000-0000-0000-000000000130'),\n    sn.name_from_string('std::pg::json'):\n        UUID('00000000-0000-0000-0000-000001000001'),\n    sn.name_from_string('std::pg::timestamptz'):\n        UUID('00000000-0000-0000-0000-000001000002'),\n    sn.name_from_string('std::pg::timestamp'):\n        UUID('00000000-0000-0000-0000-000001000003'),\n    sn.name_from_string('std::pg::date'):\n        UUID('00000000-0000-0000-0000-000001000004'),\n    sn.name_from_string('std::pg::interval'):\n        UUID('00000000-0000-0000-0000-000001000005'),\n}\n"
  },
  {
    "path": "edb/schema/abc.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nimport typing\n\n\nclass Reducible:\n    \"\"\"An interface implemented by all non-builtin objects stored in schema.\"\"\"\n\n    def schema_reduce(self) -> typing.Any:\n        \"\"\"Return a primitive representation of the object.\n\n        The return value must consist of primitive Python objects.\n        \"\"\"\n        raise NotImplementedError\n\n    @classmethod\n    def schema_restore(\n        cls,\n        data: typing.Any,\n    ) -> Reducible:\n        \"\"\"Restore object from data returned by *schema_reduce*.\"\"\"\n        raise NotImplementedError\n"
  },
  {
    "path": "edb/schema/annos.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import (\n    AbstractSet,\n    Any,\n    Callable,\n    Optional,\n    TypeVar,\n    cast,\n    TYPE_CHECKING,\n)\n\nimport json\n\nfrom edb import errors\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.edgeql import qltypes\n\nfrom . import delta as sd\nfrom . import name as sn\nfrom . import referencing\nfrom . import objects as so\nfrom . import utils\n\nif TYPE_CHECKING:\n    from . import schema as s_schema\n\n\nclass AnnotationValue(\n    referencing.ReferencedInheritingObject,\n    qlkind=qltypes.SchemaObjectClass.ANNOTATION,\n    reflection=so.ReflectionMethod.AS_LINK,\n    reflection_link='annotation',\n    data_safe=True,\n):\n\n    subject = so.SchemaField(\n        so.Object, compcoef=1.0, default=None, inheritable=False)\n\n    # N.B: This is really an Annotation, and we even patch it up below\n    # to be one, but we can't reference it here because it hasn't been\n    # declared. (And the tricks used for this sort of thing elsewhere\n    # don't work for reflection AS_LINK)\n    annotation = so.SchemaField(\n        so.Object, compcoef=0.429, ddl_identity=True)\n\n    value = so.SchemaField(\n        str, compcoef=0.909)\n\n    def get_annotation(self, schema: s_schema.Schema) -> Annotation:\n        return self.get_field_value(  # type: ignore[no-any-return]\n            schema, 'annotation')\n\n    def should_propagate(self, schema: s_schema.Schema) -> bool:\n        return self.get_annotation(schema).get_inheritable(schema)\n\n    def __str__(self) -> str:\n        return '<{}: at 0x{:x}>'.format(self.__class__.__name__, id(self))\n\n    __repr__ = __str__\n\n    @classmethod\n    def get_schema_class_displayname(cls) -> str:\n        return 'annotation'\n\n    def get_verbosename(\n        self, schema: s_schema.Schema, *, with_parent: bool = False\n    ) -> str:\n        vn = super().get_verbosename(schema)\n        if with_parent:\n            subject = self.get_subject(schema)\n            assert subject is not None\n            pvn = subject.get_verbosename(schema, with_parent=True)\n            return f'{vn} of {pvn}'\n        else:\n            return vn\n\n\nT = TypeVar(\"T\")\n\n\nclass AnnotationSubject(so.Object):\n\n    annotations_refs = so.RefDict(\n        attr='annotations',\n        ref_cls=AnnotationValue)\n\n    annotations = so.SchemaField(\n        so.ObjectIndexByShortname[AnnotationValue],\n        inheritable=False, ephemeral=True, coerce=True, compcoef=0.909,\n        default=so.DEFAULT_CONSTRUCTOR)\n\n    def get_annotation(\n        self,\n        schema: s_schema.Schema,\n        name: sn.QualName,\n    ) -> Optional[str]:\n        attrval = self.get_annotations(schema).get(schema, name, None)\n        return attrval.get_value(schema) if attrval is not None else None\n\n    def must_get_annotation(\n        self,\n        schema: s_schema.Schema,\n        name: sn.QualName,\n    ) -> str:\n        annotation_text = self.get_annotation(schema, name)\n        if annotation_text is None:\n            vn = self.get_verbosename(schema, with_parent=True)\n            raise errors.SchemaDefinitionError(\n                f\"annotation {name} on {vn} is not set\")\n\n        return annotation_text\n\n    def get_json_annotation(\n        self,\n        schema: s_schema.Schema,\n        name: sn.QualName,\n        t: Callable[[Any], T],\n    ) -> Optional[T]:\n        annotation_text = self.get_annotation(schema, name)\n        if annotation_text is None:\n            return None\n        else:\n            try:\n                value = json.loads(annotation_text)\n            except Exception:\n                vn = self.get_verbosename(schema, with_parent=True)\n                raise errors.SchemaDefinitionError(\n                    f\"annotation {name} on {vn} is not set to \"\n                    f\"a valid JSON value\")\n\n            try:\n                return t(value)\n            except Exception as e:\n                vn = self.get_verbosename(schema, with_parent=True)\n                raise errors.SchemaDefinitionError(\n                    f\"annotation {name} on {vn} is not set to \"\n                    f\"JSON containing a valid value of type {t}: {e}\"\n                )\n\n    def must_get_json_annotation(\n        self,\n        schema: s_schema.Schema,\n        name: sn.QualName,\n        t: Callable[[Any], T],\n    ) -> T:\n        value = self.get_json_annotation(schema, name, t)\n        if value is None:\n            vn = self.get_verbosename(schema, with_parent=True)\n            raise errors.SchemaDefinitionError(\n                f\"annotation {name} is not set on {vn}\"\n            )\n        else:\n            return value\n\n\nclass Annotation(\n    so.QualifiedObject,\n    so.InheritingObject,\n    AnnotationSubject,\n    qlkind=qltypes.SchemaObjectClass.ANNOTATION,\n    data_safe=True,\n):\n\n    inheritable = so.SchemaField(\n        bool, default=False, compcoef=0.2)\n\n    def get_verbosename(\n        self, schema: s_schema.Schema, *, with_parent: bool = False\n    ) -> str:\n        vn = super().get_verbosename(schema)\n        return f\"abstract {vn}\"\n\n\n# HACK?: Fix up annotation field in AnnotationValue to have the proper type\nAnnotationValue.get_field('annotation').type = Annotation\n\n\nclass AnnotationSubjectCommandContext:\n    pass\n\n\nclass AnnotationSubjectCommand(sd.ObjectCommand[so.Object_T]):\n    pass\n\n\nclass AnnotationCommandContext(sd.ObjectCommandContext[Annotation],\n                               AnnotationSubjectCommandContext):\n    pass\n\n\nclass AnnotationCommand(sd.QualifiedObjectCommand[Annotation],\n                        AnnotationSubjectCommand[Annotation],\n                        context_class=AnnotationCommandContext):\n\n    def get_ast_attr_for_field(\n        self,\n        field: str,\n        astnode: type[qlast.DDLOperation],\n    ) -> Optional[str]:\n        if field in {'abstract', 'inheritable'}:\n            return field\n        else:\n            return super().get_ast_attr_for_field(field, astnode)\n\n\nclass CreateAnnotation(AnnotationCommand, sd.CreateObject[Annotation]):\n    astnode = qlast.CreateAnnotation\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> CreateAnnotation:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        assert isinstance(astnode, qlast.CreateAnnotation)\n\n        cmd.set_attribute_value('inheritable', astnode.inheritable)\n        cmd.set_attribute_value('abstract', True)\n\n        assert isinstance(cmd, CreateAnnotation)\n        return cmd\n\n\nclass RenameAnnotation(AnnotationCommand, sd.RenameObject[Annotation]):\n\n    def _canonicalize(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        scls: Annotation,\n    ) -> None:\n        super()._canonicalize(schema, context, scls)\n\n        # AnnotationValues have names derived from the abstract\n        # annotations. We unfortunately need to go update their names.\n        annot_vals = cast(\n            AbstractSet[AnnotationValue],\n            schema.get_referrers(\n                scls, scls_type=AnnotationValue, field_name='annotation'))\n\n        for ref in annot_vals:\n            if ref.get_implicit_bases(schema):\n                # This annotation value is inherited, and presumably\n                # the rename in parent will propagate.\n                continue\n            ref_name = ref.get_name(schema)\n            quals = list(sn.quals_from_fullname(ref_name))\n            new_ref_name = sn.QualName(\n                name=sn.get_specialized_name(self.new_name, *quals),\n                module=ref_name.module,\n            )\n\n            self.add(self.init_rename_branch(\n                ref,\n                new_ref_name,\n                schema=schema,\n                context=context,\n            ))\n\n\nclass AlterAnnotation(AnnotationCommand, sd.AlterObject[Annotation]):\n    astnode = qlast.AlterAnnotation\n\n\nclass DeleteAnnotation(AnnotationCommand, sd.DeleteObject[Annotation]):\n    astnode = qlast.DropAnnotation\n\n\nclass AnnotationValueCommandContext(sd.ObjectCommandContext[AnnotationValue]):\n    pass\n\n\nclass AnnotationValueCommand(\n    referencing.ReferencedInheritingObjectCommand[AnnotationValue],\n    context_class=AnnotationValueCommandContext,\n    referrer_context_class=AnnotationSubjectCommandContext,\n):\n\n    def _deparse_name(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        name: sn.Name,\n    ) -> qlast.ObjectRef:\n        ref = super()._deparse_name(schema, context, name)\n        # Clear `itemclass`\n        ref.itemclass = None\n        return ref\n\n    @classmethod\n    def _classname_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext,\n    ) -> sn.QualName:\n        parent_ctx = cls.get_referrer_context_or_die(context)\n        assert isinstance(parent_ctx.op, sd.QualifiedObjectCommand)\n        referrer_name = context.get_referrer_name(parent_ctx)\n        base_ref = utils.ast_to_object_shell(\n            astnode.name,\n            modaliases=context.modaliases,\n            schema=schema,\n            metaclass=Annotation,\n        )\n\n        base_name = base_ref.name\n        quals = cls._classname_quals_from_ast(\n            schema, astnode, base_name, referrer_name, context)\n        pnn = sn.get_specialized_name(base_name, str(referrer_name), *quals)\n        return sn.QualName(name=pnn, module=referrer_name.module)\n\n    def populate_ddl_identity(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().populate_ddl_identity(schema, context)\n        if not isinstance(self, sd.CreateObject):\n            anno = self.scls.get_annotation(schema)\n        else:\n            annoname = sn.shortname_from_fullname(self.classname)\n            anno = schema.get(annoname, type=Annotation)\n        self.set_ddl_identity('annotation', anno)\n        return schema\n\n\nclass CreateAnnotationValue(\n    AnnotationValueCommand,\n    referencing.CreateReferencedInheritingObject[AnnotationValue],\n):\n    astnode = qlast.CreateAnnotationValue\n    referenced_astnode = qlast.CreateAnnotationValue\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext\n    ) -> CreateAnnotationValue:\n        assert isinstance(astnode, qlast.CreateAnnotationValue)\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, CreateAnnotationValue)\n        annoname = sn.shortname_from_fullname(cmd.classname)\n\n        value, ir = qlcompiler.evaluate_ast_to_python_val_and_ir(\n            astnode.value, schema=schema)\n\n        if ir.stype.get_name(schema) != sn.QualName('std', 'str'):\n            vn = ir.stype.get_verbosename(schema)\n            raise errors.InvalidValueError(\n                f\"annotation values must be 'std::str', got {vn}\",\n                span=astnode.value.span,\n            )\n\n        anno = utils.ast_objref_to_object_shell(\n            utils.name_to_ast_ref(annoname),\n            metaclass=Annotation,\n            modaliases=context.modaliases,\n            schema=schema,\n        )\n\n        cmd.set_attribute_value('annotation', anno)\n        cmd.set_attribute_value('value', value)\n\n        return cmd\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n        anno = self.get_ddl_identity('annotation')\n        assert anno is not None\n        self.set_attribute_value('internal', True)\n        return schema\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        if op.property == 'value':\n            assert isinstance(op.new_value, str)\n            assert isinstance(node, (\n                qlast.CreateAnnotationValue, qlast.AlterAnnotationValue))\n            node.value = qlast.Constant.string(op.new_value)\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n\nclass AlterAnnotationValueOwned(\n    referencing.AlterOwned[AnnotationValue],\n    AnnotationValueCommand,\n    field='owned',\n    referrer_context_class=AnnotationSubjectCommandContext,\n):\n    pass\n\n\nclass AlterAnnotationValue(\n    AnnotationValueCommand,\n    referencing.AlterReferencedInheritingObject[AnnotationValue],\n):\n\n    astnode = qlast.AlterAnnotationValue\n    referenced_astnode = qlast.AlterAnnotationValue\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext\n    ) -> AlterAnnotationValue:\n        assert isinstance(\n            astnode,\n            (qlast.CreateAnnotationValue, qlast.AlterAnnotationValue),\n        )\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, AlterAnnotationValue)\n\n        if astnode.value is not None:\n            value, ir = qlcompiler.evaluate_ast_to_python_val_and_ir(\n                astnode.value, schema=schema)\n\n            if ir.stype.get_name(schema) != sn.QualName('std', 'str'):\n                vn = ir.stype.get_verbosename(schema)\n                raise errors.InvalidValueError(\n                    f\"annotation values must be 'std::str', got {vn}\",\n                    span=astnode.value.span,\n                )\n\n            cmd.set_attribute_value(\n                'value',\n                value,\n            )\n\n        annoname = sn.shortname_from_fullname(cmd.classname)\n        anno = utils.ast_objref_to_object_shell(\n            utils.name_to_ast_ref(annoname),\n            metaclass=Annotation,\n            modaliases=context.modaliases,\n            schema=schema,\n        )\n        cmd.set_attribute_value('annotation', value=anno, orig_value=anno)\n\n        return cmd\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        if (\n            not self.has_attribute_value('value')\n            and not self.has_attribute_value('owned')\n        ):\n            return None\n        # Skip AlterObject's _get_ast, because we *don't* want to\n        # filter out things without subcommands!\n        return sd.ObjectCommand._get_ast(\n            self, schema, context, parent_node=parent_node)\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty\n    ) -> None:\n        assert isinstance(node, qlast.AlterAnnotationValue)\n\n        if op.property == 'value':\n            assert isinstance(op.new_value, str)\n            node.value = qlast.Constant.string(op.new_value)\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n\nclass RebaseAnnotationValue(\n    AnnotationValueCommand,\n    referencing.RebaseReferencedInheritingObject[AnnotationValue],\n):\n    pass\n\n\nclass RenameAnnotationValue(\n    AnnotationValueCommand,\n    referencing.RenameReferencedInheritingObject[AnnotationValue],\n):\n    pass\n\n\nclass DeleteAnnotationValue(\n    AnnotationValueCommand,\n    referencing.DeleteReferencedInheritingObject[AnnotationValue],\n):\n\n    astnode = qlast.DropAnnotationValue\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext\n    ) -> DeleteAnnotationValue:\n        assert isinstance(astnode, qlast.DropAnnotationValue)\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, DeleteAnnotationValue)\n        annoname = sn.shortname_from_fullname(cmd.classname)\n        anno = utils.ast_objref_to_object_shell(\n            utils.name_to_ast_ref(annoname),\n            metaclass=Annotation,\n            modaliases=context.modaliases,\n            schema=schema,\n        )\n\n        cmd.set_attribute_value('annotation', value=None, orig_value=anno)\n\n        return cmd\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n        anno = self.get_ddl_identity('annotation')\n        assert anno is not None\n        self.set_attribute_value(\n            'annotation',\n            value=None,\n            orig_value=anno,\n        )\n        return schema\n"
  },
  {
    "path": "edb/schema/casts.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, Mapping, cast\n\nfrom edb import errors\nfrom edb.common import lru\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom . import annos as s_anno\nfrom . import delta as sd\nfrom . import functions as s_func\nfrom . import name as sn\nfrom . import objects as so\nfrom . import types as s_types\nfrom . import schema as s_schema\nfrom . import utils\n\n\n_NOT_REACHABLE = 10000000\n\n\ndef _is_reachable(\n    schema: s_schema.Schema,\n    cast_kwargs: Mapping[str, bool],\n    source: s_types.Type,\n    target: s_types.Type,\n    distance: int,\n) -> int:\n\n    if source == target:\n        return distance\n\n    casts = schema.get_casts_to_type(target, **cast_kwargs)\n    if not casts:\n        return _NOT_REACHABLE\n\n    sources = {c.get_from_type(schema) for c in casts}\n\n    distance += 1\n    if source in sources:\n        return distance\n    else:\n        return min(\n            _is_reachable(schema, cast_kwargs, source, s, distance)\n            for s in sources\n        )\n\n\n@lru.per_job_lru_cache()\ndef get_implicit_cast_distance(\n    schema: s_schema.Schema,\n    source: s_types.Type,\n    target: s_types.Type,\n) -> int:\n    dist = _is_reachable(schema, {'implicit': True}, source, target, 0)\n    if dist == _NOT_REACHABLE:\n        return -1\n    else:\n        return dist\n\n\ndef is_implicitly_castable(\n    schema: s_schema.Schema,\n    source: s_types.Type,\n    target: s_types.Type,\n) -> bool:\n    return get_implicit_cast_distance(schema, source, target) >= 0\n\n\n@lru.per_job_lru_cache()\ndef find_common_castable_type(\n    schema: s_schema.Schema,\n    source: s_types.Type,\n    target: s_types.Type,\n) -> Optional[s_types.Type]:\n\n    if get_implicit_cast_distance(schema, target, source) >= 0:\n        return source\n    if get_implicit_cast_distance(schema, source, target) >= 0:\n        return target\n\n    # Elevate target in the castability ladder, and check if\n    # source is castable to it on each step.\n    while True:\n        casts = schema.get_casts_from_type(target, implicit=True)\n        if not casts:\n            return None\n\n        targets = {c.get_to_type(schema) for c in casts}\n\n        if len(targets) > 1:\n            for t in targets:\n                candidate = find_common_castable_type(schema, source, t)\n                if candidate is not None:\n                    return candidate\n            else:\n                return None\n        else:\n            target = next(iter(targets))\n            if get_implicit_cast_distance(schema, source, target) >= 0:\n                return target\n\n\n@lru.per_job_lru_cache()\ndef is_assignment_castable(\n    schema: s_schema.Schema,\n    source: s_types.Type,\n    target: s_types.Type,\n) -> bool:\n\n    # Implicitly castable implies assignment castable.\n    if is_implicitly_castable(schema, source, target):\n        return True\n\n    # Assignment casts are valid only as one-hop casts.\n    casts = schema.get_casts_to_type(target, assignment=True)\n    if not casts:\n        return False\n\n    for c in casts:\n        if c.get_from_type(schema) == source:\n            return True\n    return False\n\n\n@lru.per_job_lru_cache()\ndef is_castable(\n    schema: s_schema.Schema,\n    source: s_types.Type,\n    target: s_types.Type,\n) -> bool:\n\n    # Implicitly castable\n    if is_implicitly_castable(schema, source, target):\n        return True\n\n    elif is_assignment_castable(schema, source, target):\n        return True\n\n    else:\n        casts = schema.get_casts_to_type(target)\n        if not casts:\n            return False\n        else:\n            for c in casts:\n                if c.get_from_type(schema) == source:\n                    return True\n            else:\n                return False\n\n\ndef get_cast_fullname_from_names(\n    from_type: sn.Name,\n    to_type: sn.Name,\n) -> sn.QualName:\n    std = not (\n        (\n            isinstance(from_type, sn.QualName)\n            and sn.UnqualName(from_type.module) not in s_schema.STD_MODULES\n        ) or (\n            isinstance(to_type, sn.QualName)\n            and sn.UnqualName(to_type.module) not in s_schema.STD_MODULES\n        )\n    )\n    module = 'std' if std else '__ext_casts__'\n\n    quals = [str(from_type), str(to_type)]\n    shortname = sn.QualName(module, 'cast')\n    return sn.QualName(\n        module=shortname.module,\n        name=sn.get_specialized_name(shortname, *quals),\n    )\n\n\ndef get_cast_fullname(\n    schema: s_schema.Schema,\n    from_type: s_types.TypeShell[s_types.Type],\n    to_type: s_types.TypeShell[s_types.Type],\n) -> sn.QualName:\n    return get_cast_fullname_from_names(\n        from_type.get_name(schema),\n        to_type.get_name(schema),\n    )\n\n\nclass Cast(\n    so.QualifiedObject,\n    s_anno.AnnotationSubject,\n    s_func.VolatilitySubject,\n    qlkind=qltypes.SchemaObjectClass.CAST,\n    data_safe=True,\n):\n\n    from_type = so.SchemaField(\n        s_types.Type, compcoef=0.5)\n\n    to_type = so.SchemaField(\n        s_types.Type, compcoef=0.5)\n\n    allow_implicit = so.SchemaField(\n        bool, default=False, compcoef=0.4)\n\n    allow_assignment = so.SchemaField(\n        bool, default=False, compcoef=0.4)\n\n    language = so.SchemaField(\n        qlast.Language, default=None, compcoef=0.4, coerce=True)\n\n    from_function = so.SchemaField(\n        str, default=None, compcoef=0.4)\n\n    from_expr = so.SchemaField(\n        bool, default=False, compcoef=0.4)\n\n    from_cast = so.SchemaField(\n        bool, default=False, compcoef=0.4)\n\n    code = so.SchemaField(\n        str, default=None, compcoef=0.4)\n\n\nclass CastCommandContext(sd.ObjectCommandContext[Cast],\n                         s_anno.AnnotationSubjectCommandContext):\n    pass\n\n\nclass CastCommand(sd.QualifiedObjectCommand[Cast],\n                  context_class=CastCommandContext):\n\n    def get_ast_attr_for_field(\n        self,\n        field: str,\n        astnode: type[qlast.DDLOperation],\n    ) -> Optional[str]:\n        if field in {'allow_assignment', 'allow_implicit'}:\n            return field\n        else:\n            return super().get_ast_attr_for_field(field, astnode)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        if not context.stdmode and not context.testmode:\n            raise errors.UnsupportedFeatureError(\n                'user-defined casts are not supported',\n                span=astnode.span\n            )\n\n        return super()._cmd_tree_from_ast(schema, astnode, context)\n\n    @classmethod\n    def _classname_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext,\n    ) -> sn.QualName:\n        assert isinstance(astnode, qlast.CastCommand)\n        modaliases = context.modaliases\n\n        from_type = utils.ast_to_type_shell(\n            astnode.from_type,\n            metaclass=s_types.Type,\n            modaliases=modaliases,\n            schema=schema,\n        )\n\n        to_type = utils.ast_to_type_shell(\n            astnode.to_type,\n            metaclass=s_types.Type,\n            modaliases=modaliases,\n            schema=schema,\n        )\n\n        return get_cast_fullname(schema, from_type, to_type)\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n        schema = s_types.materialize_type_in_attribute(\n            schema, context, self, 'from_type')\n        schema = s_types.materialize_type_in_attribute(\n            schema, context, self, 'to_type')\n        return schema\n\n\nclass CreateCast(CastCommand, sd.CreateObject[Cast]):\n    astnode = qlast.CreateCast\n\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        fullname = self.classname\n        cast = schema.get(fullname, None)\n        if cast:\n            from_type = self.get_attribute_value('from_type')\n            to_type = self.get_attribute_value('to_type')\n\n            raise errors.DuplicateCastDefinitionError(\n                f'a cast from {from_type.get_displayname(schema)!r} '\n                f'to {to_type.get_displayname(schema)!r} is already defined',\n                span=self.span)\n\n        return super()._create_begin(schema, context)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        assert isinstance(astnode, qlast.CreateCast)\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        modaliases = context.modaliases\n\n        from_type = utils.ast_to_type_shell(\n            astnode.from_type,\n            metaclass=s_types.Type,\n            modaliases=modaliases,\n            schema=schema,\n        )\n\n        cmd.set_attribute_value('from_type', from_type)\n\n        to_type = utils.ast_to_type_shell(\n            astnode.to_type,\n            metaclass=s_types.Type,\n            modaliases=modaliases,\n            schema=schema,\n        )\n\n        cmd.set_attribute_value('to_type', to_type)\n\n        cmd.set_attribute_value('allow_implicit', astnode.allow_implicit)\n        cmd.set_attribute_value('allow_assignment', astnode.allow_assignment)\n\n        if astnode.code is not None:\n            cmd.set_attribute_value(\n                'language',\n                astnode.code.language,\n            )\n            if astnode.code.from_function is not None:\n                cmd.set_attribute_value(\n                    'from_function',\n                    astnode.code.from_function,\n                )\n            if astnode.code.code is not None:\n                cmd.set_attribute_value(\n                    'code',\n                    astnode.code.code,\n                )\n            if astnode.code.from_expr is not None:\n                cmd.set_attribute_value(\n                    'from_expr',\n                    astnode.code.from_expr,\n                )\n            if astnode.code.from_cast is not None:\n                cmd.set_attribute_value(\n                    'from_cast',\n                    astnode.code.from_cast,\n                )\n\n        return cmd\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        assert isinstance(node, qlast.CreateCast)\n        new_value: Any = op.new_value\n\n        if op.property == 'from_type':\n            # In a cast we can only have pure types, so this is going\n            # to be a TypeName.\n            node.from_type = cast(qlast.TypeName,\n                                  utils.typeref_to_ast(schema, new_value))\n\n        elif op.property == 'to_type':\n            # In a cast we can only have pure types, so this is going\n            # to be a TypeName.\n            node.to_type = cast(qlast.TypeName,\n                                utils.typeref_to_ast(schema, new_value))\n\n        elif op.property == 'code':\n            if node.code is None:\n                node.code = qlast.CastCode()\n            node.code.code = new_value\n\n        elif op.property == 'language':\n            if node.code is None:\n                node.code = qlast.CastCode()\n            node.code.language = new_value\n\n        elif op.property == 'from_function' and new_value:\n            if node.code is None:\n                node.code = qlast.CastCode()\n            node.code.from_function = new_value\n\n        elif op.property == 'from_expr' and new_value:\n            if node.code is None:\n                node.code = qlast.CastCode()\n            node.code.from_expr = new_value\n\n        elif op.property == 'from_cast' and new_value:\n            if node.code is None:\n                node.code = qlast.CastCode()\n            node.code.from_cast = new_value\n\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n\nclass RenameCast(CastCommand, sd.RenameObject[Cast]):\n    pass\n\n\nclass AlterCast(CastCommand, sd.AlterObject[Cast]):\n    astnode = qlast.AlterCast\n\n\nclass DeleteCast(CastCommand, sd.DeleteObject[Cast]):\n    astnode = qlast.DropCast\n\n    def _delete_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._delete_begin(schema, context)\n        if not context.canonical:\n            from_type = self.scls.get_from_type(schema)\n            if op := from_type.as_type_delete_if_unused(schema):\n                self.add_caused(op)\n            to_type = self.scls.get_to_type(schema)\n            if op := to_type.as_type_delete_if_unused(schema):\n                self.add_caused(op)\n        return schema\n"
  },
  {
    "path": "edb/schema/constraints.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    Mapping,\n    cast,\n    Iterable,\n    TYPE_CHECKING,\n)\nimport re\n\nfrom edb import errors\nfrom edb.common import verutils\n\nfrom edb import edgeql\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.edgeql import qltypes as ft\nfrom edb.edgeql import parser as qlparser\nfrom edb.edgeql import utils as qlutils\nfrom edb.edgeql import qltypes\n\nfrom . import annos as s_anno\nfrom . import delta as sd\nfrom . import expr as s_expr\nfrom . import functions as s_func\nfrom . import inheriting\nfrom . import name as sn\nfrom . import objects as so\nfrom . import types as s_types\nfrom . import pseudo as s_pseudo\nfrom . import referencing\nfrom . import utils\n\n\nif TYPE_CHECKING:\n    from edb.common import parsing as c_parsing\n    from edb.schema import schema as s_schema\n\n\ndef _assert_not_none[T](value: Optional[T]) -> T:\n    if value is None:\n        raise TypeError(\"A value is expected\")\n    return value\n\n\ndef merge_constraint_params(\n    constraint: Constraint,\n    supers: list[Constraint],\n    field_name: str,\n    *,\n    ignore_local: bool,\n    schema: s_schema.Schema,\n) -> Any:\n    if constraint.get_subject(schema) is None:\n        # consistency of abstract constraint params is checked\n        # in CreateConstraint.validate_create\n        return constraint.get_explicit_field_value(schema, field_name, None)\n    else:\n        # concrete constraints cannot redefine parameters and always\n        # inherit from super.\n        return supers[0].get_explicit_field_value(schema, field_name, None)\n\n\ndef constraintname_from_fullname(name: sn.Name) -> sn.QualName:\n    assert isinstance(name, sn.QualName)\n    # the dict key for constraints drops the first qual, which makes\n    # it independent of where it is declared\n    short = sn.shortname_from_fullname(name)\n    quals = sn.quals_from_fullname(name)\n    return sn.QualName(\n        name=sn.get_specialized_name(short, *quals[1:]),\n        module='__',\n    )\n\n\ndef _constraint_object_key(schema: s_schema.Schema, o: so.Object) -> sn.Name:\n    return constraintname_from_fullname(o.get_name(schema))\n\n\nclass ObjectIndexByConstraintName(\n    so.ObjectIndexBase[sn.Name, so.Object_T],\n    key=_constraint_object_key,\n):\n\n    @classmethod\n    def get_key_for_name(\n        cls,\n        schema: s_schema.Schema,\n        name: sn.Name,\n    ) -> sn.Name:\n        return constraintname_from_fullname(name)\n\n\nclass Constraint(\n    referencing.ReferencedInheritingObject,\n    s_func.CallableObject,\n    qlkind=ft.SchemaObjectClass.CONSTRAINT,\n    data_safe=True,\n):\n\n    params = so.SchemaField(\n        s_func.FuncParameterList,\n        coerce=True,\n        compcoef=0.4,\n        default=so.DEFAULT_CONSTRUCTOR,\n        inheritable=True,\n        merge_fn=merge_constraint_params,\n    )\n\n    expr = so.SchemaField(\n        s_expr.Expression, default=None, compcoef=0.909,\n        coerce=True)\n\n    subjectexpr = so.SchemaField(\n        s_expr.Expression,\n        default=None, compcoef=0.833, coerce=True,\n        ddl_identity=True)\n\n    finalexpr = so.SchemaField(\n        s_expr.Expression,\n        default=None, compcoef=0.909, coerce=True)\n\n    except_expr = so.SchemaField(\n        s_expr.Expression,\n        default=None,\n        coerce=True,\n        compcoef=0.909,\n        ddl_identity=True,\n    )\n\n    subject = so.SchemaField(\n        so.Object, default=None, inheritable=False)\n\n    args = so.SchemaField(\n        s_expr.ExpressionList,\n        default=None, coerce=True, inheritable=False,\n        compcoef=0.875, ddl_identity=True)\n\n    delegated = so.SchemaField(\n        bool,\n        default=False,\n        inheritable=False,\n        special_ddl_syntax=True,\n        compcoef=0.9,\n    )\n\n    errmessage = so.SchemaField(\n        str,\n        default=None,\n        compcoef=0.971,\n        allow_ddl_set=True,\n        allow_interpolation=True,\n    )\n\n    is_aggregate = so.SchemaField(\n        bool, default=False, compcoef=0.971, allow_ddl_set=False)\n\n    def get_name_impacting_ancestors(\n        self,\n        schema: s_schema.Schema,\n    ) -> list[Constraint]:\n        if self.is_non_concrete(schema):\n            return []\n        else:\n            return [self.get_nearest_generic_parent(schema)]\n\n    def get_constraint_origins(\n        self, schema: s_schema.Schema\n    ) -> list[Constraint]:\n        \"\"\"\n        Origins of a constraint are the constraints that should actually perform\n        validation on their subjects.\n\n        Example:\n        If we have `Baz <: Bar <: Foo` and `Foo` declares some exclusive\n        constraint, this constraint will be inherited by `Bar` and then `Baz`.\n        But this inherited constraint on `Baz` should not validate exclusivity\n        of the property within just `Baz`, but within all `Foo` objects.\n        That's why origin of the constraint on `Baz` and on `Bar` is the\n        constraint on `Foo`.\n\n        Determining which type is that is non-trivial because of:\n        - multiple inheritance\n          (constraint might originate from multiple unrelated ancestors)\n        - delegated exclusive constraints, which are defined on a parent, but\n          should be exclusive within each of the children.\n\n        We validate constraints using triggers, and this function helps drive\n        their generation.\n        \"\"\"\n\n        # collect origins from all ancestors\n        origins: set[Constraint] = set()\n        for base in self.get_bases(schema).objects(schema):\n            # abstract bases are not an origin\n            if base.is_non_concrete(schema):\n                continue\n            # delegated bases are not an origin\n            if base.get_delegated(schema):\n                continue\n\n            # recurse\n            origins.update(base.get_constraint_origins(schema))\n\n        # if no ancestors have an origin, I am the origin\n        return [self] if not origins else list(origins)\n\n    def is_independent(self, schema: s_schema.Schema) -> bool:\n        return (\n            not self.descendants(schema)\n            and self.get_constraint_origins(schema) == [self]\n        )\n\n    def get_verbosename(\n        self, schema: s_schema.Schema, *, with_parent: bool = False\n    ) -> str:\n        vn = super().get_verbosename(schema, with_parent=with_parent)\n        if self.is_non_concrete(schema):\n            return f'abstract {vn}'\n        else:\n            # concrete constraint must have a subject\n            assert self.get_subject(schema) is not None\n            return vn\n\n    def is_non_concrete(self, schema: s_schema.Schema) -> bool:\n        return self.get_subject(schema) is None\n\n    def get_subject(self, schema: s_schema.Schema) -> ConsistencySubject:\n        return cast(\n            ConsistencySubject,\n            self.get_field_value(schema, 'subject'),\n        )\n\n    def format_error_message(\n        self,\n        schema: s_schema.Schema,\n    ) -> str:\n        subject = self.get_subject(schema)\n\n        title_ann = subject.get_annotation(schema, sn.QualName('std', 'title'))\n        if title_ann:\n            subject_name = title_ann\n        else:\n            short_name = subject.get_shortname(schema)\n            subject_name = short_name.name\n\n        return self.format_error_text(schema, subject_name)\n\n    def format_error_text(\n        self,\n        schema: s_schema.Schema,\n        subject_name: str,\n    ) -> str:\n        text = self.get_errmessage(schema)\n        assert text\n        args: Optional[s_expr.ExpressionList] = self.get_args(schema)\n        if args:\n            args_ql: list[qlast.Base] = [\n                qlast.Path(steps=[qlast.ObjectRef(name=subject_name)]),\n            ]\n\n            args_ql.extend(arg.parse() for arg in args)\n\n            constr_base: Constraint = schema.get(\n                self.get_name(schema), type=type(self))\n\n            index_parameters = qlutils.index_parameters(\n                args_ql,\n                parameters=constr_base.get_params(schema),\n                schema=schema,\n            )\n\n            expr = constr_base.get_field_value(schema, 'expr')\n            expr_ql = qlparser.parse_query(expr.text)\n\n            qlutils.inline_parameters(expr_ql, index_parameters)\n\n            args_map = {name: edgeql.generate_source(val, pretty=False)\n                        for name, val in index_parameters.items()}\n        else:\n            args_map = {'__subject__': subject_name}\n\n        return interpolate_error_text(text, args_map)\n\n    def as_alter_delta(\n        self,\n        other: Constraint,\n        *,\n        self_schema: s_schema.Schema,\n        other_schema: s_schema.Schema,\n        confidence: float,\n        context: so.ComparisonContext,\n    ) -> sd.ObjectCommand[Constraint]:\n        return super().as_alter_delta(\n            other,\n            self_schema=self_schema,\n            other_schema=other_schema,\n            confidence=confidence,\n            context=context,\n        )\n\n    def as_delete_delta(\n        self,\n        *,\n        schema: s_schema.Schema,\n        context: so.ComparisonContext,\n    ) -> sd.ObjectCommand[Constraint]:\n        return super().as_delete_delta(schema=schema, context=context)\n\n    def get_ddl_identity(\n        self,\n        schema: s_schema.Schema,\n    ) -> Optional[dict[str, str]]:\n        ddl_identity = super().get_ddl_identity(schema)\n\n        if (\n            ddl_identity is not None\n            and self.field_is_inherited(schema, 'subjectexpr')\n            and (bases := self.get_bases(schema).objects(schema))\n            and (\n                bases[0].is_non_concrete(schema)\n                or 'subjectexpr' not in (\n                    bases[0].get_ddl_identity(schema) or ())\n            )\n        ):\n            ddl_identity.pop('subjectexpr', None)\n\n        return ddl_identity\n\n    @classmethod\n    def get_root_classes(cls) -> tuple[sn.QualName, ...]:\n        return (\n            sn.QualName(module='std', name='constraint'),\n        )\n\n    @classmethod\n    def get_default_base_name(self) -> sn.QualName:\n        return sn.QualName('std', 'constraint')\n\n\nclass ConsistencySubject(\n    so.QualifiedObject,\n    so.InheritingObject,\n    s_anno.AnnotationSubject,\n):\n    constraints_refs = so.RefDict(\n        attr='constraints',\n        ref_cls=Constraint)\n\n    constraints = so.SchemaField(\n        ObjectIndexByConstraintName[Constraint],\n        inheritable=False, ephemeral=True, coerce=True, compcoef=0.887,\n        default=so.DEFAULT_CONSTRUCTOR\n    )\n\n    def add_constraint(\n        self,\n        schema: s_schema.Schema,\n        constraint: Constraint,\n        replace: bool = False,\n    ) -> s_schema.Schema:\n        return self.add_classref(\n            schema,\n            'constraints',\n            constraint,\n            replace=replace,\n        )\n\n    def can_accept_constraints(self, schema: s_schema.Schema) -> bool:\n        return True\n\n\nclass ConsistencySubjectCommandContext:\n    # context mixin\n    pass\n\n\nclass ConsistencySubjectCommand(\n    inheriting.InheritingObjectCommand[so.InheritingObjectT],\n):\n    pass\n\n\nclass ConstraintCommandContext(sd.ObjectCommandContext[Constraint],\n                               s_anno.AnnotationSubjectCommandContext):\n    pass\n\n\nclass ConstraintCommand(\n    referencing.ReferencedInheritingObjectCommand[Constraint],\n    s_func.CallableCommand[Constraint],\n    context_class=ConstraintCommandContext,\n    referrer_context_class=ConsistencySubjectCommandContext,\n):\n\n    @classmethod\n    def _validate_subcommands(\n        cls,\n        astnode: qlast.DDLOperation,\n    ) -> None:\n        # check that 'subject' and 'subjectexpr' are not set as annotations\n        for command in astnode.commands:\n            if isinstance(command, qlast.SetField):\n                cname = command.name\n                if cname in {'subject', 'subjectexpr'}:\n                    raise errors.InvalidConstraintDefinitionError(\n                        f'{cname} is not a valid constraint annotation',\n                        span=command.span)\n\n    @classmethod\n    def _classname_quals_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        base_name: sn.Name,\n        referrer_name: sn.QualName,\n        context: sd.CommandContext,\n    ) -> tuple[str, ...]:\n        if isinstance(astnode, qlast.CreateConstraint):\n            return ()\n        exprs = []\n        args = cls._constraint_args_from_ast(schema, astnode, context)\n        for arg in args:\n            exprs.append(arg.text)\n\n        assert isinstance(astnode, qlast.ConcreteConstraintOp)\n        if astnode.subjectexpr:\n            # use the normalized text directly from the expression\n            expr = s_expr.Expression.from_ast(\n                astnode.subjectexpr, schema, context.modaliases)\n            exprs.append(expr.text)\n        if astnode.except_expr:\n            # use the normalized text directly from the expression\n            expr = s_expr.Expression.from_ast(\n                astnode.except_expr, schema, context.modaliases)\n            # but mangle it a bit, so that we can distinguish between\n            # on and except when only one is present\n            exprs.append('!' + expr.text)\n\n        return (cls._name_qual_from_exprs(schema, exprs),)\n\n    @classmethod\n    def _classname_quals_from_name(cls, name: sn.QualName) -> tuple[str, ...]:\n        quals = sn.quals_from_fullname(name)\n        return (quals[-1],)\n\n    @classmethod\n    def _constraint_args_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext,\n    ) -> list[s_expr.Expression]:\n        args = []\n        assert isinstance(astnode, qlast.ConcreteConstraintOp)\n\n        if astnode.args:\n            for arg in astnode.args:\n                arg_expr = s_expr.Expression.from_ast(\n                    arg, schema, context.modaliases)\n                args.append(arg_expr)\n\n        return args\n\n    @classmethod\n    def as_inherited_ref_ast(\n        cls,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        name: sn.Name,\n        parent: so.Object,\n    ) -> qlast.ObjectDDL:\n        assert isinstance(parent, Constraint)\n        astnode_cls = cls.referenced_astnode  # type: ignore\n        nref = cls.get_inherited_ref_name(schema, context, parent, name)\n        args = []\n\n        parent_args = parent.get_args(schema)\n        if parent_args:\n            for arg_expr in parent_args:\n                arg = edgeql.parse_fragment(arg_expr.text)\n                args.append(arg)\n\n        subj_expr = parent.get_subjectexpr(schema)\n        if (\n            subj_expr is None\n            # Don't include subjectexpr if it was inherited from an\n            # abstract constraint.\n            or parent.get_nearest_generic_parent(\n                schema).get_subjectexpr(schema) is not None\n        ):\n            subj_expr_ql = None\n        else:\n            subj_expr_ql = edgeql.parse_fragment(subj_expr.text)\n\n        except_expr: s_expr.Expression | None = parent.get_except_expr(schema)\n        if except_expr:\n            except_expr_ql = except_expr.parse()\n        else:\n            except_expr_ql = None\n\n        astnode = astnode_cls(\n            name=nref, args=args, subjectexpr=subj_expr_ql,\n            except_expr=except_expr_ql)\n\n        return cast(qlast.ObjectDDL, astnode)\n\n    def compile_expr_field(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: s_expr.Expression,\n        track_schema_ref_exprs: bool=False,\n    ) -> s_expr.CompiledExpression:\n        from . import pointers as s_pointers\n\n        base: Optional[so.Object] = None\n        if isinstance(self, AlterConstraint):\n            base = self.scls.get_subject(schema)\n        else:\n            referrer_ctx = self.get_referrer_context(context)\n            if referrer_ctx:\n                base = referrer_ctx.op.scls\n\n        if base is not None:\n            assert isinstance(base, (s_types.Type, s_pointers.Pointer))\n            # Concrete constraint\n            if field.name == 'expr':\n                # Concrete constraints cannot redefine the base check\n                # expressions, and so the only way we should get here\n                # is through field inheritance, so check that the\n                # value is compiled and move on.\n                if not value.is_compiled():\n                    mcls = self.get_schema_metaclass()\n                    dn = mcls.get_schema_class_displayname()\n                    raise errors.InternalServerError(\n                        f'uncompiled expression in the {field.name!r} field of'\n                        f' {dn} {self.classname!r}'\n                    )\n                # HACK: Not *really* compiled, but...\n                return value  # type: ignore\n\n            elif field.name in {'subjectexpr', 'finalexpr', 'except_expr'}:\n                compiled = value.compiled(\n                    schema=schema,\n                    options=qlcompiler.CompilerOptions(\n                        modaliases=context.modaliases,\n                        anchors={'__subject__': base},\n                        path_prefix_anchor='__subject__',\n                        singletons=frozenset([base]),\n                        allow_generic_type_output=True,\n                        schema_object_context=self.get_schema_metaclass(),\n                        apply_query_rewrites=False,\n                        track_schema_ref_exprs=track_schema_ref_exprs,\n                    ),\n                    context=context,\n                )\n\n                # compile the expression to sql to preempt errors downstream\n                utils.try_compile_irast_to_sql_tree(compiled, self.span)\n\n                return compiled\n\n            else:\n                return super().compile_expr_field(\n                    schema, context, field, value)\n\n        elif field.name in ('expr', 'subjectexpr'):\n            # Abstract constraint.\n            params = self._get_params(schema, context)\n\n            param_anchors = s_func.get_params_symtable(\n                params,\n                schema,\n                inlined_defaults=False,\n            )\n\n            compiled = value.compiled(\n                schema=schema,\n                options=qlcompiler.CompilerOptions(\n                    modaliases=context.modaliases,\n                    anchors=param_anchors,\n                    func_params=params,\n                    allow_generic_type_output=True,\n                    schema_object_context=self.get_schema_metaclass(),\n                    apply_query_rewrites=not context.stdmode,\n                    track_schema_ref_exprs=track_schema_ref_exprs,\n                ),\n                context=context,\n            )\n\n            # compile the expression to sql to preempt errors downstream\n            utils.try_compile_irast_to_sql_tree(compiled, self.span)\n\n            return compiled\n\n        else:\n            return super().compile_expr_field(\n                schema, context, field, value, track_schema_ref_exprs)\n\n    def get_dummy_expr_field_value(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: Any,\n    ) -> Optional[s_expr.Expression]:\n        if field.name in {'expr', 'subjectexpr', 'finalexpr', 'except_expr'}:\n            return s_expr.Expression(text='false')\n        else:\n            raise NotImplementedError(f'unhandled field {field.name!r}')\n\n    @classmethod\n    def get_inherited_ref_name(\n        cls,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        parent: so.Object,\n        name: sn.Name,\n    ) -> qlast.ObjectRef:\n        bn = sn.shortname_from_fullname(name)\n        return utils.name_to_ast_ref(bn)\n\n    def get_ref_implicit_base_delta(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        refcls: Constraint,\n        implicit_bases: list[Constraint],\n    ) -> inheriting.BaseDelta_T[Constraint]:\n        child_bases = refcls.get_bases(schema).objects(schema)\n\n        return inheriting.delta_bases(\n            [b.get_name(schema) for b in child_bases],\n            [b.get_name(schema) for b in implicit_bases],\n            t=Constraint,\n        )\n\n    def get_ast_attr_for_field(\n        self,\n        field: str,\n        astnode: type[qlast.DDLOperation],\n    ) -> Optional[str]:\n        if field in ('subjectexpr', 'args', 'except_expr'):\n            return field\n        elif (\n            field == 'delegated'\n            and astnode is qlast.CreateConcreteConstraint\n        ):\n            return field\n        else:\n            return super().get_ast_attr_for_field(field, astnode)\n\n    def get_ddl_identity_fields(\n        self,\n        context: sd.CommandContext,\n    ) -> tuple[so.Field[Any], ...]:\n        id_fields = super().get_ddl_identity_fields(context)\n        omit_fields = set()\n        if not self.has_ddl_identity('subjectexpr'):\n            omit_fields.add('subjectexpr')\n        if self.get_referrer_context(context) is None:\n            omit_fields.add('args')\n\n        if omit_fields:\n            return tuple(f for f in id_fields if f.name not in omit_fields)\n        else:\n            return id_fields\n\n    @classmethod\n    def localnames_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> set[str]:\n        localnames = super().localnames_from_ast(\n            schema, astnode, context\n        )\n        # Set up the constraint parameters as part of names to be\n        # ignored in expression normalization.\n        if isinstance(astnode, qlast.CreateConstraint):\n            localnames |= {param.name for param in astnode.params}\n        elif isinstance(astnode, qlast.AlterConstraint):\n            # ALTER ABSTRACT CONSTRAINT doesn't repeat the params,\n            # but we can get them from the schema.\n            objref = astnode.name\n\n            # Merge the context modaliases and the command modaliases.\n            modaliases = dict(context.modaliases)\n            modaliases.update(\n                cls._modaliases_from_ast(schema, astnode, context))\n            # Get the original constraint.\n            constr = schema.get(\n                utils.ast_ref_to_name(objref),\n                module_aliases=modaliases,\n                type=Constraint,\n            )\n\n            localnames |= {param.get_parameter_name(schema) for param in\n                           constr.get_params(schema).objects(schema)}\n\n        return localnames\n\n    def inherit_fields(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        bases: tuple[so.Object, ...],\n        *,\n        fields: Optional[Iterable[str]] = None,\n        ignore_local: bool = False,\n        apply: bool = True,\n    ) -> s_schema.Schema:\n        # Concrete constraints populate a bunch of other fields that\n        # can be based on their abstract parents but don't come from\n        # them. So if we are inheriting a new expr from a potentially\n        # abstract parent, we need to actually inherit all of these\n        # other properties that can be populated by\n        # _populate_concrete_constraint_attrs.\n        #\n        # This is pretty fragile though, and I don't love it.\n\n        if fields is not None:\n            fields = set(fields) | {\n                'subjectexpr', 'finalexpr', 'abstract', 'args'\n            }\n\n        return super().inherit_fields(\n            schema,\n            context,\n            bases,\n            fields=fields,\n            ignore_local=ignore_local,\n            apply=apply,\n        )\n\n    def _populate_concrete_constraint_attrs(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        subject_obj: so.Object,\n        *,\n        name: sn.QualName,\n        subjectexpr: Optional[s_expr.Expression] = None,\n        subjectexpr_inherited: bool = False,\n        span: Optional[c_parsing.Span] = None,\n        args: Optional[Iterable[s_expr.Expression]] = None,\n        **kwargs: Any\n    ) -> None:\n        from edb.ir import ast as irast\n        from edb.ir import utils as ir_utils\n        from . import pointers as s_pointers\n        from . import links as s_links\n        from . import objtypes as s_objtypes\n        from . import scalars as s_scalars\n\n        bases = self.get_resolved_attribute_value(\n            'bases', schema=schema, context=context,\n        )\n        if not bases:\n            bases = self.scls.get_bases(schema)\n        constr_base = bases.objects(schema)[0]\n        # If we have a concrete base, then we should inherit all of\n        # these attrs through the normal inherit_fields() mechanisms,\n        # and populating them ourselves will just mess up\n        # inherited_fields.\n        if not constr_base.is_non_concrete(schema):\n            return\n\n        attrs = dict(kwargs)\n        inherited = dict()\n\n        base_subjectexpr = constr_base.get_field_value(schema, 'subjectexpr')\n        if subjectexpr is None:\n            attrs['subjectexpr'] = subjectexpr\n            inherited['subjectexpr'] = subjectexpr_inherited\n\n            subjectexpr = base_subjectexpr\n        else:\n            if (base_subjectexpr is not None\n                and subjectexpr.text != base_subjectexpr.text):\n                raise errors.InvalidConstraintDefinitionError(\n                    f'subjectexpr is already defined for {name}',\n                    span=span,\n                )\n\n            base_subjectexpr = constr_base.get_subjectexpr(schema)\n            if base_subjectexpr is not None:\n                attrs['subjectexpr'] = base_subjectexpr\n                inherited['subjectexpr'] = True\n\n        if (isinstance(subject_obj, s_scalars.ScalarType)\n                and constr_base.get_is_aggregate(schema)):\n            raise errors.InvalidConstraintDefinitionError(\n                f'{constr_base.get_verbosename(schema)} may not '\n                f'be used on scalar types',\n                span=span,\n            )\n\n        if (\n            subjectexpr is None\n            and isinstance(subject_obj, s_objtypes.ObjectType)\n        ):\n            raise errors.InvalidConstraintDefinitionError(\n                \"constraints on object types must have an 'on' clause\",\n                span=span,\n            )\n\n        expr: s_expr.Expression = constr_base.get_field_value(schema, 'expr')\n        if not expr:\n            raise errors.InvalidConstraintDefinitionError(\n                f'missing constraint expression in {name}')\n\n        # Re-parse instead of using expr.parse, because we mutate\n        # the AST below.\n        expr_ql = qlparser.parse_query(expr.text)\n\n        if subjectexpr is not None:\n            # subject has been redefined\n            subject_ql = subjectexpr.parse()\n\n            assert isinstance(subject_ql, qlast.Base)\n            qlutils.inline_anchors(\n                expr_ql, anchors={'__subject__': subject_ql})\n\n        if not args:\n            args = constr_base.get_field_value(schema, 'args')\n\n        if args:\n            args_ql: list[qlast.Base] = [\n                qlast.Path(steps=[qlast.SpecialAnchor(name='__subject__')]),\n            ]\n            args_ql.extend(arg.parse() for arg in args)\n            args_map = qlutils.index_parameters(\n                args_ql,\n                parameters=constr_base.get_params(schema),\n                schema=schema,\n            )\n            qlutils.inline_parameters(expr_ql, args_map)\n\n        attrs['args'] = args\n\n        assert isinstance(subject_obj, (s_types.Type, s_pointers.Pointer))\n        singletons = frozenset({subject_obj})\n\n        final_expr = s_expr.Expression.from_ast(expr_ql, schema, {}).compiled(\n            schema=schema,\n            options=qlcompiler.CompilerOptions(\n                anchors={'__subject__': subject_obj},\n                path_prefix_anchor='__subject__',\n                singletons=singletons,\n                apply_query_rewrites=False,\n                schema_object_context=self.get_schema_metaclass(),\n            ),\n            context=context,\n        )\n\n        bool_t = schema.get('std::bool', type=s_scalars.ScalarType)\n\n        expr_type = final_expr.irast.stype\n        expr_schema = final_expr.irast.schema\n        if not expr_type.issubclass(expr_schema, bool_t):\n            raise errors.InvalidConstraintDefinitionError(\n                f'{name} constraint expression expected '\n                f'to return a bool value, got '\n                f'{expr_type.get_verbosename(expr_schema)}',\n                span=span\n            )\n\n        except_expr: s_expr.Expression | None = attrs.get('except_expr')\n\n        if subjectexpr is not None:\n            options = qlcompiler.CompilerOptions(\n                anchors={'__subject__': subject_obj},\n                path_prefix_anchor='__subject__',\n                singletons=singletons,\n                apply_query_rewrites=False,\n                schema_object_context=self.get_schema_metaclass(),\n            )\n\n            final_subjectexpr = subjectexpr.compiled(\n                schema=schema, options=options, context=context\n            )\n\n            refs = ir_utils.get_longest_paths(final_expr.irast)\n\n            final_except_expr: s_expr.CompiledExpression | None = None\n            if except_expr:\n                final_except_expr = except_expr.compiled(\n                    schema=schema, options=options, context=context\n                )\n                refs |= ir_utils.get_longest_paths(final_except_expr.irast)\n\n            has_any_multi = has_non_subject_multi = False\n            for ref in refs:\n                while isinstance(ref.expr, irast.Pointer):\n                    rptr = ref.expr\n\n                    if rptr.dir_cardinality.is_multi():\n                        has_any_multi = True\n\n                    # We don't need to look further than the subject,\n                    # which is always valid. (And which is a singleton\n                    # in a constraint expression if it is itself a\n                    # singleton, regardless of other parts of the path.)\n                    if (\n                        isinstance(rptr.ptrref, irast.PointerRef)\n                        and rptr.ptrref.id == subject_obj.id\n                    ):\n                        break\n\n                    if rptr.dir_cardinality.is_multi():\n                        has_non_subject_multi = True\n\n                    if (not isinstance(rptr.ptrref,\n                                       irast.TupleIndirectionPointerRef)\n                            and rptr.ptrref.source_ptr is None\n                            and isinstance(rptr.source.expr, irast.Pointer)):\n                        if isinstance(subject_obj, s_links.Link):\n                            raise errors.InvalidConstraintDefinitionError(\n                                \"link constraints may not access \"\n                                \"the link target\",\n                                span=span\n                            )\n                        else:\n                            raise errors.InvalidConstraintDefinitionError(\n                                \"constraints cannot contain paths with more \"\n                                \"than one hop\",\n                                span=span\n                            )\n\n                    ref = rptr.source\n\n            if has_non_subject_multi and len(refs) > 1:\n                raise errors.InvalidConstraintDefinitionError(\n                    \"cannot reference multiple links or properties in a \"\n                    \"constraint where at least one link or property is MULTI\",\n                    span=span\n                )\n\n            if set_of_op := ir_utils.find_set_of_op(\n                final_subjectexpr.irast,\n                has_any_multi,\n            ):\n                label = (\n                    'function'\n                    if isinstance(set_of_op, irast.FunctionCall) else\n                    'operator'\n                )\n                op_name = str(set_of_op.func_shortname)\n                raise errors.UnsupportedFeatureError(\n                    f\"cannot use SET OF {label} '{op_name}' \"\n                    f\"in a constraint\",\n                    span=set_of_op.span\n                )\n\n            if (\n                final_subjectexpr.irast.volatility\n                != qltypes.Volatility.Immutable\n            ):\n                raise errors.InvalidConstraintDefinitionError(\n                    f'constraint expressions must be immutable',\n                    span=final_subjectexpr.irast.span,\n                )\n\n            if final_except_expr:\n                if (\n                    final_except_expr.irast.volatility\n                    != qltypes.Volatility.Immutable\n                ):\n                    raise errors.InvalidConstraintDefinitionError(\n                        f'constraint expressions must be immutable',\n                        span=final_except_expr.irast.span,\n                    )\n\n        if final_expr.irast.volatility != qltypes.Volatility.Immutable:\n            raise errors.InvalidConstraintDefinitionError(\n                f'constraint expressions must be immutable',\n                span=span,\n            )\n\n        attrs['finalexpr'] = final_expr\n        attrs['params'] = constr_base.get_params(schema)\n        inherited['params'] = True\n        attrs['abstract'] = False\n\n        for k, v in attrs.items():\n            self.set_attribute_value(k, v, inherited=bool(inherited.get(k)))\n\n\nclass CreateConstraint(\n    ConstraintCommand,\n    s_func.CreateCallableObject[Constraint],\n    referencing.CreateReferencedInheritingObject[Constraint],\n):\n\n    astnode = [qlast.CreateConcreteConstraint, qlast.CreateConstraint]\n    referenced_astnode = qlast.CreateConcreteConstraint\n\n    @classmethod\n    def _get_param_desc_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        modaliases: Mapping[Optional[str], str],\n        astnode: qlast.ObjectDDL,\n        *,\n        param_offset: int=0\n    ) -> list[s_func.ParameterDesc]:\n        if not isinstance(astnode, qlast.CallableObjectCommand):\n            # Concrete constraint.\n            return []\n\n        params = super()._get_param_desc_from_ast(\n            schema, modaliases, astnode, param_offset=param_offset + 1)\n\n        params.insert(0, s_func.ParameterDesc(\n            num=param_offset,\n            name=sn.UnqualName('__subject__'),\n            default=None,\n            type=s_pseudo.PseudoTypeShell(name=sn.UnqualName('anytype')),\n            typemod=ft.TypeModifier.SingletonType,\n            kind=ft.ParameterKind.PositionalParam,\n        ))\n\n        return params\n\n    def validate_create(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        super().validate_create(schema, context)\n\n        if self.get_referrer_context(context) is not None:\n            # The checks below apply only to abstract constraints.\n            return\n\n        base_params: Optional[s_func.FuncParameterList] = None\n        base_with_params: Optional[Constraint] = None\n\n        bases = self.get_resolved_attribute_value(\n            'bases',\n            schema=schema,\n            context=context,\n        )\n\n        for base in bases.objects(schema):\n            params = base.get_params(schema)\n            if params and len(params) > 1:\n                # All constraints have __subject__ parameter\n                # auto-injected, hence the \"> 1\" check.\n                if base_params is not None:\n                    raise errors.InvalidConstraintDefinitionError(\n                        f'{self.get_verbosename()} '\n                        f'extends multiple constraints '\n                        f'with parameters',\n                        span=self.span,\n                    )\n                base_params = params\n                base_with_params = base\n\n        if base_params:\n            assert base_with_params is not None\n\n            params = self._get_params(schema, context)\n            if not params or len(params) == 1:\n                # All constraints have __subject__ parameter\n                # auto-injected, hence the \"== 1\" check.\n                raise errors.InvalidConstraintDefinitionError(\n                    f'{self.get_verbosename()} '\n                    f'must define parameters to reflect parameters of '\n                    f'the {base_with_params.get_verbosename(schema)} '\n                    f'it extends',\n                    span=self.span,\n                )\n\n            if len(params) < len(base_params):\n                raise errors.InvalidConstraintDefinitionError(\n                    f'{self.get_verbosename()} '\n                    f'has fewer parameters than the '\n                    f'{base_with_params.get_verbosename(schema)} '\n                    f'it extends',\n                    span=self.span,\n                )\n\n            # Skipping the __subject__ param\n            for base_param, param in zip(base_params.objects(schema)[1:],\n                                         params.objects(schema)[1:]):\n\n                param_name = param.get_parameter_name(schema)\n                base_param_name = base_param.get_parameter_name(schema)\n\n                if param_name != base_param_name:\n                    raise errors.InvalidConstraintDefinitionError(\n                        f'the {param_name!r} parameter of the '\n                        f'{self.get_verbosename()} '\n                        f'must be renamed to {base_param_name!r} '\n                        f'to match the signature of the base '\n                        f'{base_with_params.get_verbosename(schema)} ',\n                        span=self.span,\n                    )\n\n                param_type = param.get_type(schema)\n                base_param_type = base_param.get_type(schema)\n\n                if (\n                    not base_param_type.is_polymorphic(schema)\n                    and param_type.is_polymorphic(schema)\n                ):\n                    raise errors.InvalidConstraintDefinitionError(\n                        f'the {param_name!r} parameter of the '\n                        f'{self.get_verbosename()} cannot '\n                        f'be of generic type because the corresponding '\n                        f'parameter of the '\n                        f'{base_with_params.get_verbosename(schema)} '\n                        f'it extends has a concrete type',\n                        span=self.span,\n                    )\n\n                if (\n                    not base_param_type.is_polymorphic(schema) and\n                    not param_type.is_polymorphic(schema) and\n                    not param_type.implicitly_castable_to(\n                        base_param_type, schema)\n                ):\n                    raise errors.InvalidConstraintDefinitionError(\n                        f'the {param_name!r} parameter of the '\n                        f'{self.get_verbosename()} has type of '\n                        f'{param_type.get_displayname(schema)} that '\n                        f'is not implicitly castable to the '\n                        f'corresponding parameter of the '\n                        f'{base_with_params.get_verbosename(schema)} with '\n                        f'type {base_param_type.get_displayname(schema)}',\n                        span=self.span,\n                    )\n\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        referrer_ctx = self.get_referrer_context(context)\n        if referrer_ctx is None:\n            schema = super()._create_begin(schema, context)\n            return schema\n\n        subject = referrer_ctx.scls\n        assert isinstance(subject, ConsistencySubject)\n        if not subject.can_accept_constraints(schema):\n            raise errors.UnsupportedFeatureError(\n                f'constraints cannot be defined on '\n                f'{subject.get_verbosename(schema)}',\n                span=self.span,\n            )\n\n        if not context.canonical:\n            props = self.get_attributes(schema, context)\n            props.pop('name')\n            props.pop('subject', None)\n            fullname = self.classname\n            shortname = sn.shortname_from_fullname(fullname)\n            assert isinstance(shortname, sn.QualName), \\\n                \"expected qualified name\"\n            self._populate_concrete_constraint_attrs(\n                schema,\n                context,\n                subject_obj=subject,\n                name=shortname,\n                subjectexpr_inherited=self.is_attribute_inherited(\n                    'subjectexpr'),\n                span=self.span,\n                **props,\n            )\n\n            self.set_attribute_value('subject', subject)\n\n        return super()._create_begin(schema, context)\n\n    @classmethod\n    def as_inherited_ref_cmd(\n        cls,\n        *,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        astnode: qlast.ObjectDDL,\n        bases: list[Constraint],\n        referrer: so.Object,\n    ) -> sd.ObjectCommand[Constraint]:\n        cmd = super().as_inherited_ref_cmd(\n            schema=schema,\n            context=context,\n            astnode=astnode,\n            bases=bases,\n            referrer=referrer,\n        )\n\n        args = cls._constraint_args_from_ast(schema, astnode, context)\n        if args:\n            cmd.set_attribute_value('args', args)\n\n        subj_expr = bases[0].get_subjectexpr(schema)\n        if subj_expr is not None:\n            cmd.set_attribute_value('subjectexpr', subj_expr, inherited=True)\n\n        params = bases[0].get_params(schema)\n        if params is not None:\n            cmd.set_attribute_value('params', params, inherited=True)\n\n        return cmd\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> CreateConstraint:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        if isinstance(astnode, qlast.CreateConcreteConstraint):\n            if astnode.delegated:\n                cmd.set_attribute_value('delegated', astnode.delegated)\n\n            args = cls._constraint_args_from_ast(schema, astnode, context)\n            if args:\n                cmd.set_attribute_value('args', args)\n\n        elif isinstance(astnode, qlast.CreateConstraint):\n            params = cls._get_param_desc_from_ast(\n                schema, context.modaliases, astnode)\n\n            for param in params:\n                if param.get_kind(schema) is ft.ParameterKind.NamedOnlyParam:\n                    raise errors.InvalidConstraintDefinitionError(\n                        'named only parameters are not allowed '\n                        'in this context',\n                        span=astnode.span)\n\n                if param.get_default(schema) is not None:\n                    raise errors.InvalidConstraintDefinitionError(\n                        'constraints do not support parameters '\n                        'with defaults',\n                        span=astnode.span)\n\n            if cmd.get_attribute_value('return_type') is None:\n                cmd.set_attribute_value(\n                    'return_type',\n                    schema.get('std::bool'),\n                )\n\n            if cmd.get_attribute_value('return_typemod') is None:\n                cmd.set_attribute_value(\n                    'return_typemod',\n                    ft.TypeModifier.SingletonType,\n                )\n\n        assert isinstance(astnode, (qlast.CreateConstraint,\n                                    qlast.CreateConcreteConstraint))\n        # 'subjectexpr' can be present in either astnode type\n        if astnode.subjectexpr:\n            orig_text = cls.get_orig_expr_text(schema, astnode, 'subjectexpr')\n\n            expr_ql: qlast.Expr\n            if (\n                orig_text is not None\n                and context.compat_ver_is_before(\n                    (1, 0, verutils.VersionStage.ALPHA, 6)\n                )\n            ):\n                # Versions prior to a6 used a different expression\n                # normalization strategy, so we must renormalize the\n                # expression.\n                expr_ql = qlcompiler.renormalize_compat(\n                    astnode.subjectexpr,\n                    orig_text,\n                    schema=schema,\n                    localnames=context.localnames,\n                )\n            else:\n                expr_ql = astnode.subjectexpr\n\n            subjectexpr = s_expr.Expression.from_ast(\n                expr_ql,\n                schema,\n                context.modaliases,\n                context.localnames,\n            )\n\n            cmd.set_attribute_value(\n                'subjectexpr',\n                subjectexpr,\n            )\n\n        if (\n            isinstance(astnode, qlast.CreateConcreteConstraint)\n            and astnode.except_expr\n        ):\n            except_expr = s_expr.Expression.from_ast(\n                astnode.except_expr,\n                schema,\n                context.modaliases,\n                context.localnames,\n            )\n\n            cmd.set_attribute_value('except_expr', except_expr)\n\n        cls._validate_subcommands(astnode)\n        assert isinstance(cmd, CreateConstraint)\n        return cmd\n\n    def _skip_param(self, props: dict[str, Any]) -> bool:\n        pname = s_func.Parameter.paramname_from_fullname(props['name'])\n        return pname == '__subject__'\n\n    def _get_params_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n    ) -> list[tuple[int, qlast.FuncParamDecl]]:\n        if isinstance(node, qlast.CreateConstraint):\n            return super()._get_params_ast(schema, context, node)\n        else:\n            return []\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        if (\n            op.property == 'args'\n            and isinstance(node, (qlast.CreateConcreteConstraint,\n                                  qlast.AlterConcreteConstraint))\n        ):\n            assert isinstance(op.new_value, s_expr.ExpressionList)\n            args = []\n            for arg in op.new_value:\n                exprast = arg.parse()\n                assert isinstance(exprast, qlast.Expr), \"expected qlast.Expr\"\n                args.append(exprast)\n            node.args = args\n            return\n\n        super()._apply_field_ast(schema, context, node, op)\n\n    @classmethod\n    def _classbases_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext,\n    ) -> list[so.ObjectShell[Constraint]]:\n        if isinstance(astnode, qlast.CreateConcreteConstraint):\n            classname = cls._classname_from_ast(schema, astnode, context)\n            base_name = sn.shortname_from_fullname(classname)\n            assert isinstance(base_name, sn.QualName), \\\n                \"expected qualified name\"\n            base = utils.ast_objref_to_object_shell(\n                qlast.ObjectRef(\n                    module=base_name.module,\n                    name=base_name.name,\n                ),\n                metaclass=Constraint,\n                schema=schema,\n                modaliases=context.modaliases,\n            )\n            return [base]\n        else:\n            return super()._classbases_from_ast(schema, astnode, context)\n\n\nclass RenameConstraint(\n    ConstraintCommand,\n    s_func.RenameCallableObject[Constraint],\n    referencing.RenameReferencedInheritingObject[Constraint],\n):\n    @classmethod\n    def _classname_quals_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        base_name: sn.Name,\n        referrer_name: sn.QualName,\n        context: sd.CommandContext,\n    ) -> tuple[str, ...]:\n        parent_op = cls.get_parent_op(context)\n        assert isinstance(parent_op.classname, sn.QualName)\n        return cls._classname_quals_from_name(parent_op.classname)\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._alter_begin(schema, context)\n\n        if not context.canonical and self.scls.get_abstract(schema):\n            self._propagate_ref_rename(schema, context, self.scls)\n\n        return schema\n\n\nclass AlterConstraintOwned(\n    referencing.AlterOwned[Constraint],\n    ConstraintCommand,\n    field='owned',\n    referrer_context_class=ConsistencySubjectCommandContext,\n):\n    pass\n\n\nclass AlterConstraint(\n    ConstraintCommand,\n    referencing.AlterReferencedInheritingObject[Constraint],\n):\n    astnode = [qlast.AlterConcreteConstraint, qlast.AlterConstraint]\n    referenced_astnode = qlast.AlterConcreteConstraint\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        referrer_ctx = self.get_referrer_context(context)\n        if referrer_ctx is None:\n            schema = super()._alter_begin(schema, context)\n            return schema\n\n        subject = referrer_ctx.scls\n        assert isinstance(subject, ConsistencySubject)\n\n        if not context.canonical:\n            props = self.get_attributes(schema, context)\n            props.pop('name', None)\n            props.pop('subject', None)\n            props.pop('expr', None)\n            args = props.pop('args', None)\n            if not args:\n                args = self.scls.get_args(schema)\n            subjectexpr = props.pop('subjectexpr', None)\n            subjectexpr_inherited = self.is_attribute_inherited('subjectexpr')\n            if not subjectexpr:\n                subjectexpr_inherited = self.scls.field_is_inherited(\n                    schema, 'subjectexpr')\n                subjectexpr = self.scls.get_subjectexpr(schema)\n            fullname = self.classname\n            shortname = sn.shortname_from_fullname(fullname)\n            assert isinstance(shortname, sn.QualName), \\\n                \"expected qualified name\"\n            self._populate_concrete_constraint_attrs(\n                schema,\n                context,\n                subject_obj=subject,\n                name=shortname,\n                subjectexpr=subjectexpr,\n                subjectexpr_inherited=subjectexpr_inherited,\n                args=args,\n                span=self.span,\n                **props,\n            )\n\n        return super()._alter_begin(schema, context)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> AlterConstraint:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, AlterConstraint)\n\n        if isinstance(astnode, (qlast.CreateConcreteConstraint,\n                                qlast.AlterConcreteConstraint)):\n            if getattr(astnode, 'delegated', False):\n                assert isinstance(astnode, qlast.CreateConcreteConstraint)\n                cmd.set_attribute_value('delegated', astnode.delegated)\n\n            new_name = None\n            for op in cmd.get_subcommands(type=RenameConstraint):\n                new_name = op.new_name\n\n            if new_name is not None:\n                cmd.set_attribute_value('name', new_name)\n\n        cls._validate_subcommands(astnode)\n        return cmd\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        if self.scls.get_abstract(schema):\n            return super()._get_ast(schema, context, parent_node=parent_node)\n\n        # We need to make sure to include subjectexpr and args\n        # in the AST, since they are really part of the name.\n        op = self.as_inherited_ref_ast(\n            schema, context, self.scls.get_name(schema),\n            self.scls,\n        )\n        self._apply_fields_ast(schema, context, op)\n\n        if (op is not None and hasattr(op, 'commands') and\n                not op.commands):\n            return None\n\n        return op\n\n    def validate_alter(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        super().validate_alter(schema, context)\n\n        self_delegated = self.get_attribute_value('delegated')\n        if not self_delegated:\n            return\n\n        concrete_bases = [\n            b for b in self.scls.get_bases(schema).objects(schema)\n            if not b.is_non_concrete(schema) and not b.get_delegated(schema)\n        ]\n        if concrete_bases:\n            tgt_repr = self.scls.get_verbosename(schema, with_parent=True)\n            bases_repr = ', '.join(\n                b.get_subject(schema).get_verbosename(schema, with_parent=True)\n                for b in concrete_bases\n            )\n            raise errors.InvalidConstraintDefinitionError(\n                f'cannot redefine {tgt_repr} as delegated:'\n                f' it is defined as non-delegated in {bases_repr}',\n                span=self.span,\n            )\n\n    def canonicalize_alter_from_external_ref(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        if (\n            not self.get_attribute_value('abstract')\n            and (subjectexpr :=\n                 self.get_attribute_value('subjectexpr')) is not None\n        ):\n            assert isinstance(subjectexpr, s_expr.Expression)\n\n            # To compute the new name, we construct an AST of the\n            # constraint, since that is the infrastructure we have for\n            # computing the classname.\n            name = sn.shortname_from_fullname(self.classname)\n            assert isinstance(name, sn.QualName), \"expected qualified name\"\n            ast = qlast.CreateConcreteConstraint(\n                name=qlast.ObjectRef(name=name.name, module=name.module),\n                subjectexpr=subjectexpr.parse(),\n                args=[],\n            )\n            quals = sn.quals_from_fullname(self.classname)\n            new_name = self._classname_from_ast_and_referrer(\n                schema, sn.QualName.from_string(quals[0]), ast, context)\n            if new_name == self.classname:\n                return\n\n            rename = self.scls.init_delta_command(\n                schema, sd.RenameObject, new_name=new_name)\n            rename.set_attribute_value(\n                'name', value=new_name, orig_value=self.classname)\n            self.add(rename)\n\n    def _get_params(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_func.FuncParameterList:\n        return self.scls.get_params(schema)\n\n\nclass DeleteConstraint(\n    ConstraintCommand,\n    referencing.DeleteReferencedInheritingObject[Constraint],\n    s_func.DeleteCallableObject[Constraint],\n):\n    astnode = [qlast.DropConcreteConstraint, qlast.DropConstraint]\n    referenced_astnode = qlast.DropConcreteConstraint\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        if op.property == 'args':\n            assert isinstance(op.old_value, s_expr.ExpressionList)\n            assert isinstance(node, qlast.DropConcreteConstraint)\n            node.args = [arg.parse() for arg in op.old_value]\n            return\n\n        super()._apply_field_ast(schema, context, node, op)\n\n\nclass RebaseConstraint(\n    ConstraintCommand,\n    referencing.RebaseReferencedInheritingObject[Constraint],\n):\n    # finalexpr is fully determined by a bunch of ddl_identity fields,\n    # so it should be inherited by compute_inherited_fields if they are\n    EXTRA_INHERITED_FIELDS = {'finalexpr'}\n\n    def _get_bases_for_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        bases: tuple[so.ObjectShell[Constraint], ...],\n    ) -> tuple[so.ObjectShell[Constraint], ...]:\n        return ()\n\n\ndef interpolate_error_text(text: str, args: dict[str, str]) -> str:\n    \"\"\"\n    Converts message template \"hello {world}! {nope}{{world}}\" and\n    arguments {\"world\": \"Alice\", \"hell\": \"Eve\"}\n    into \"hello Alice! {world}\".\n    \"\"\"\n\n    regex = r\"\\{\\{.*\\}\\}|\\{([A-Za-z_0-9]+)\\}\"\n\n    formatted = \"\"\n    last_start = 0\n    for match in re.finditer(regex, text, flags=0):\n        formatted += text[last_start : match.start()]\n        last_start = match.end()\n\n        if match[1] is None:\n            # escape double curly braces\n            formatted += match[0][1:-1]\n        elif match[1] in args:\n            # lookup an arg\n            formatted += args[match[1]]\n        else:\n            # arg not found\n            formatted += match[0]\n\n    formatted += text[last_start:]\n    return formatted\n"
  },
  {
    "path": "edb/schema/database.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom edb import errors\n\nfrom edb.common import struct\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\nfrom edb.schema import defines as s_def\n\nfrom . import annos as s_anno\nfrom . import delta as sd\nfrom . import objects as so\nfrom . import schema as s_schema\n\nfrom typing import cast\n\n\nclass Branch(\n    so.ExternalObject,\n    s_anno.AnnotationSubject,\n    qlkind=qltypes.SchemaObjectClass.DATABASE,\n    data_safe=False,\n):\n    pass\n\n\nclass BranchCommandContext(sd.ObjectCommandContext[Branch]):\n    pass\n\n\nclass BranchCommand(\n    sd.ExternalObjectCommand[Branch],\n    context_class=BranchCommandContext,\n):\n\n    def _validate_name(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        name = self.get_attribute_value('name')\n        if len(str(name)) > s_def.MAX_NAME_LENGTH:\n            span = self.get_attribute_span('name')\n            raise errors.SchemaDefinitionError(\n                f'Branch names longer than {s_def.MAX_NAME_LENGTH} '\n                f'characters are not supported',\n                span=span,\n            )\n\n\nclass CreateBranch(BranchCommand, sd.CreateExternalObject[Branch]):\n\n    astnode = qlast.CreateDatabase\n    template = struct.Field(str, default=None)\n    branch_type = struct.Field(\n        qlast.BranchType, default=qlast.BranchType.EMPTY)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> CreateBranch:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, CreateBranch)\n\n        assert isinstance(astnode, qlast.CreateDatabase)\n        if astnode.template is not None:\n            cmd.template = astnode.template.name\n\n        if (\n            astnode.branch_type == qlast.BranchType.TEMPLATE\n            and not context.testmode\n        ):\n            raise errors.EdgeQLSyntaxError(\n                f'unexpected TEMPLATE',\n                span=astnode.span,\n            )\n\n        cmd.branch_type = astnode.branch_type\n\n        return cmd\n\n    def validate_create(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        # no call to super().validate_create() as we don't want to enforce\n        # rules that hold for any other schema objects\n        self._validate_name(schema, context)\n\n\nclass AlterBranch(BranchCommand, sd.AlterExternalObject[Branch]):\n    astnode = qlast.AlterDatabase\n\n    def validate_alter(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        super().validate_alter(schema, context)\n        self._validate_name(schema, context)\n\n\nclass DropBranch(BranchCommand, sd.DeleteExternalObject[Branch]):\n    astnode = qlast.DropDatabase\n\n    def _validate_legal_command(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        super()._validate_legal_command(schema, context)\n        if self.classname.name in s_def.EDGEDB_SPECIAL_DBS:\n            raise errors.ExecutionError(\n                f\"database {self.classname.name!r} cannot be dropped\"\n            )\n\n\nclass RenameBranch(BranchCommand, sd.RenameObject[Branch]):\n    # databases are ExternalObjects, so they might not be properly\n    # present in the schema, so we can't do a proper rename.\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        scls = self.get_parent_op(context).scls\n        self.scls = cast(Branch, scls)\n        return schema\n"
  },
  {
    "path": "edb/schema/ddl.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Callable,\n    Optional,\n    Iterable,\n    Mapping,\n    cast,\n    TYPE_CHECKING,\n)\n\nfrom collections import defaultdict\nimport itertools\n\nfrom edb import errors\n\nfrom edb import edgeql\nfrom edb.common import debug\nfrom edb.common import uuidgen\nfrom edb.common import verutils\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import declarative as s_decl\n\nfrom . import delta as sd\nfrom . import expr as s_expr\nfrom . import extensions as s_ext\nfrom . import functions as s_func\nfrom . import migrations as s_migr\nfrom . import modules as s_mod\nfrom . import name as sn\nfrom . import objects as so\nfrom . import objtypes as s_objtypes\nfrom . import ordering as s_ordering\nfrom . import pseudo as s_pseudo\nfrom . import schema as s_schema\nfrom . import types as s_types\nfrom . import version as s_ver\n\n\nif TYPE_CHECKING:\n    import uuid\n\n\ndef delta_schemas(\n    schema_a: Optional[s_schema.Schema],\n    schema_b: s_schema.Schema,\n    *,\n    included_modules: Optional[Iterable[sn.Name]]=None,\n    excluded_modules: Optional[Iterable[sn.Name]]=None,\n    included_items: Optional[Iterable[sn.Name]]=None,\n    excluded_items: Optional[Iterable[sn.Name]]=None,\n    schema_a_filters: Iterable[\n        Callable[[s_schema.Schema, so.Object], bool]\n    ] = (),\n    schema_b_filters: Iterable[\n        Callable[[s_schema.Schema, so.Object], bool]\n    ] = (),\n    include_module_diff: bool=True,\n    include_std_diff: bool=False,\n    include_derived_types: bool=True,\n    include_extensions: bool=False,\n    linearize_delta: bool=True,\n    descriptive_mode: bool=False,\n    generate_prompts: bool=False,\n    guidance: Optional[so.DeltaGuidance]=None,\n) -> sd.DeltaRoot:\n    \"\"\"Return difference between *schema_a* and *schema_b*.\n\n    The returned object is a delta tree that, when applied to\n    *schema_a* results in *schema_b*.\n\n    Args:\n        schema_a:\n            Schema to use as a starting state.  If ``None``,\n            then a schema with only standard modules is assumed,\n            unless *include_std_diff* is ``True``, in which case\n            an entirely empty schema is assumed as a starting point.\n\n        schema_b:\n            Schema to use as the ending state.\n\n        included_modules:\n            Optional list of modules to include in the delta.\n\n        excluded_modules:\n            Optional list of modules to exlude from the delta.\n            Takes precedence over *included_modules*.\n            NOTE: standard library modules are always excluded,\n            unless *include_std_diff* is ``True``.\n\n        included_items:\n            Optional list of names of objects to include in the delta.\n\n        excluded_items:\n            Optional list of names of objects to exclude from the delta.\n            Takes precedence over *included_items*.\n\n        schema_a_filters:\n            Optional list of additional filters to place on *schema_a*.\n\n        schema_b_filters:\n            Optional list of additional filters to place on *schema_b*.\n\n        include_module_diff:\n            Whether to include create/drop module operations\n            in the delta diff.\n\n        include_std_diff:\n            Whether to include the standard library in the diff.\n\n        include_derived_types:\n            Whether to include derived types, like unions, in the diff.\n\n        linearize_delta:\n            Whether the resulting diff should be properly ordered\n            using the dependencies between objects.\n\n        descriptive_mode:\n            DESCRIBE AS TEXT mode.\n\n        generate_prompts:\n            Whether to generate prompts that can be used in\n            DESCRIBE MIGRATION.\n\n        guidance:\n            Optional explicit guidance to schema diff.\n\n    Returns:\n        A :class:`schema.delta.DeltaRoot` instances representing\n        the delta between *schema_a* and *schema_b*.\n    \"\"\"\n\n    result = sd.DeltaRoot()\n\n    schema_a_filters = list(schema_a_filters)\n    schema_b_filters = list(schema_b_filters)\n    context = so.ComparisonContext(\n        generate_prompts=generate_prompts,\n        descriptive_mode=descriptive_mode,\n        guidance=guidance,\n    )\n\n    if schema_a is None:\n        if include_std_diff:\n            schema_a = s_schema.EMPTY_SCHEMA\n        else:\n            schema_a = schema_b\n\n            def _filter(schema: s_schema.Schema, obj: so.Object) -> bool:\n                return (\n                    (\n                        isinstance(obj, so.QualifiedObject)\n                        and (\n                            obj.get_name(schema).get_module_name()\n                            in s_schema.STD_MODULES\n                        )\n                    ) or (\n                        isinstance(obj, s_mod.Module)\n                        and obj.get_name(schema) in s_schema.STD_MODULES\n                    )\n                )\n            schema_a_filters.append(_filter)\n\n    my_modules = {\n        m.get_name(schema_b)\n        for m in schema_b.get_objects(\n            type=s_mod.Module,\n            extra_filters=schema_b_filters,\n        )\n    }\n\n    other_modules = {\n        m.get_name(schema_a)\n        for m in schema_a.get_objects(\n            type=s_mod.Module,\n            extra_filters=schema_a_filters,\n        )\n    }\n\n    added_modules = my_modules - other_modules\n    dropped_modules = other_modules - my_modules\n\n    if included_modules is not None:\n        included_modules = set(included_modules)\n\n        added_modules &= included_modules\n        dropped_modules &= included_modules\n    else:\n        included_modules = set()\n\n    if excluded_modules is None:\n        excluded_modules = set()\n    else:\n        excluded_modules = set(excluded_modules)\n\n    if not include_std_diff:\n        excluded_modules.update(s_schema.STD_MODULES)\n\n        def _filter(schema: s_schema.Schema, obj: so.Object) -> bool:\n            return not obj.get_builtin(schema)\n\n        schema_a_filters.append(_filter)\n        schema_b_filters.append(_filter)\n\n    # In theory, __derived__ is ephemeral and should not need to be\n    # included.  In practice, unions created by computed links put\n    # persistent things into __derived__ and need to be included in\n    # diffs.\n    # TODO: Fix this.\n    if not include_derived_types:\n        excluded_modules.add(sn.UnqualName('__derived__'))\n\n    excluded_modules.add(sn.UnqualName('__ext_casts__'))\n    excluded_modules.add(sn.UnqualName('__ext_index_matches__'))\n\n    # Don't analyze the objects from extensions.\n    if not include_extensions and isinstance(schema_b, s_schema.ChainedSchema):\n        ext_packages = schema_b._global_schema.get_objects(\n            type=s_ext.ExtensionPackage)\n        ext_mods = set()\n        for pkg in ext_packages:\n            if not (modname := pkg.get_ext_module(schema_b)):\n                continue\n            if schema_a and schema_a.get_referrers(pkg):\n                ext_mods.add(sn.UnqualName(modname))\n                for submod in schema_a.get_modules():\n                    submod_name = submod.get_name(schema_a)\n                    assert isinstance(submod_name, sn.UnqualName)\n                    if submod_name.name.startswith(modname + '::'):\n                        ext_mods.add(submod_name)\n\n            if schema_b.get_referrers(pkg):\n                ext_mods.add(sn.UnqualName(modname))\n                for submod in schema_b.get_modules():\n                    submod_name = submod.get_name(schema_b)\n                    assert isinstance(submod_name, sn.UnqualName)\n                    if submod_name.name.startswith(modname + '::'):\n                        ext_mods.add(submod_name)\n\n        for ext_mod in ext_mods:\n            if ext_mod not in included_modules:\n                excluded_modules.add(ext_mod)\n\n    if excluded_modules:\n        added_modules -= excluded_modules\n        dropped_modules -= excluded_modules\n\n    if included_items is not None:\n        included_items = set(included_items)\n\n    if excluded_items is not None:\n        excluded_items = set(excluded_items)\n\n    if include_module_diff:\n        for added_module in sorted(added_modules):\n            if (\n                guidance is None\n                or (\n                    (s_mod.Module, added_module)\n                    not in guidance.banned_creations\n                )\n            ):\n                mod = schema_b.get_global(s_mod.Module, added_module, None)\n                assert mod is not None\n                create = mod.as_create_delta(\n                    schema=schema_b,\n                    context=context,\n                )\n                assert isinstance(create, sd.CreateObject)\n                create.if_not_exists = True\n                # We currently fully assume that modules are created\n                # or deleted and never renamed.  This is fine, because module\n                # objects are never actually referenced directly, only by\n                # the virtue of being the leading part of a fully-qualified\n                # name.\n                create.set_annotation('confidence', 1.0)\n\n                result.add(create)\n\n    excluded_classes = (\n        so.GlobalObject,\n        s_mod.Module,\n        s_func.Parameter,\n        s_pseudo.PseudoType,\n        s_migr.Migration,\n    )\n\n    schemaclasses = [\n        schemacls\n        for schemacls in so.ObjectMeta.get_schema_metaclasses()\n        if (\n            not issubclass(schemacls, excluded_classes)\n            and not schemacls.is_abstract()\n        )\n    ]\n\n    assert not context.renames\n    # We retry performing the diff until we stop finding new renames\n    # and deletions. This allows us to be agnostic to the order that\n    # we process schemaclasses.\n    old_count = -1, -1\n    while old_count != (len(context.renames), len(context.deletions)):\n        old_count = len(context.renames), len(context.deletions)\n\n        objects = sd.DeltaRoot()\n\n        for sclass in schemaclasses:\n            filters: list[Callable[[s_schema.Schema, so.Object], bool]] = []\n\n            if not issubclass(sclass, so.QualifiedObject):\n                # UnqualifiedObjects (like anonymous tuples and arrays)\n                # should not use an included_modules filter.\n                incl_modules = None\n            else:\n                if issubclass(sclass, so.DerivableObject):\n                    def _only_generic(\n                        schema: s_schema.Schema,\n                        obj: so.Object,\n                    ) -> bool:\n                        assert isinstance(obj, so.DerivableObject)\n                        return obj.is_non_concrete(schema) or (\n                            isinstance(obj, s_types.Type)\n                            and obj.get_from_global(schema)\n                        )\n                    filters.append(_only_generic)\n                incl_modules = included_modules\n\n            new = schema_b.get_objects(\n                type=sclass,\n                included_modules=incl_modules,\n                excluded_modules=excluded_modules,\n                included_items=included_items,\n                excluded_items=excluded_items,\n                extra_filters=filters + schema_b_filters,\n            )\n            old = schema_a.get_objects(\n                type=sclass,\n                included_modules=incl_modules,\n                excluded_modules=excluded_modules,\n                included_items=included_items,\n                excluded_items=excluded_items,\n                extra_filters=filters + schema_a_filters,\n            )\n\n            objects.add(\n                sd.delta_objects(\n                    old,\n                    new,\n                    sclass=sclass,\n                    old_schema=schema_a,\n                    new_schema=schema_b,\n                    context=context,\n                )\n            )\n\n    # We don't propertly understand the dependencies on extensions, so\n    # instead of having s_ordering sort them, we just put all\n    # CreateExtension commands first and all DeleteExtension commands\n    # last.\n    create_exts: list[s_ext.CreateExtension] = []\n    delete_exts = []\n    for cmd in list(objects.get_subcommands()):\n        if isinstance(cmd, s_ext.CreateExtension):\n            cmd.canonical = False\n            objects.discard(cmd)\n            create_exts.append(cmd)\n        elif isinstance(cmd, s_ext.DeleteExtension):\n            cmd.canonical = False\n            objects.discard(cmd)\n            delete_exts.append(cmd)\n\n    if linearize_delta:\n        objects = s_ordering.linearize_delta(\n            objects, old_schema=schema_a, new_schema=schema_b)\n\n    if include_derived_types:\n        result.add(objects)\n    else:\n        for cmd in objects.get_subcommands():\n            if isinstance(cmd, s_objtypes.ObjectTypeCommand):\n                if isinstance(cmd, s_objtypes.DeleteObjectType):\n                    relevant_schema = schema_a\n                else:\n                    relevant_schema = schema_b\n\n                obj = cast(s_objtypes.ObjectType,\n                           relevant_schema.get(cmd.classname))\n                if obj.is_union_type(relevant_schema):\n                    continue\n\n            result.add(cmd)\n\n    if include_module_diff:\n        # Process dropped modules in *reverse* sorted order, so that\n        # `foo::bar` gets dropped before `foo`.\n        for dropped_module in reversed(sorted(dropped_modules)):\n            if (\n                guidance is None\n                or (\n                    (s_mod.Module, dropped_module)\n                    not in guidance.banned_deletions\n                )\n            ):\n                mod = schema_a.get_global(s_mod.Module, dropped_module, None)\n                assert mod is not None\n                dropped = mod.as_delete_delta(\n                    schema=schema_a,\n                    context=context,\n                )\n                dropped.set_annotation('confidence', 1.0)\n\n                result.add(dropped)\n\n    create_exts_sorted = sd.sort_by_cross_refs_key(\n        schema_b, create_exts, key=lambda x: x.scls)\n    delete_exts_sorted = sd.sort_by_cross_refs_key(\n        schema_a, delete_exts, key=lambda x: x.scls)\n\n    for op in create_exts_sorted:\n        result.prepend(op)\n    result.update(delete_exts_sorted)\n\n    return result\n\n\ndef cmd_from_ddl(\n    stmt: qlast.DDLOperation,\n    *,\n    context: Optional[sd.CommandContext]=None,\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    testmode: bool=False\n) -> sd.Command:\n    ddl = s_expr.imprint_expr_context(stmt, modaliases)\n    assert isinstance(ddl, qlast.DDLOperation)\n\n    if context is None:\n        context = sd.CommandContext(\n            schema=schema, modaliases=modaliases, testmode=testmode)\n\n    res = sd.compile_ddl(schema, ddl, context=context)\n    context.early_renames.clear()\n    return res\n\n\ndef apply_sdl(\n    sdl_document: qlast.Schema,\n    *,\n    base_schema: s_schema.Schema,\n    stdmode: bool = False,\n    testmode: bool = False,\n) -> tuple[s_schema.Schema, list[errors.EdgeDBError]]:\n    # group declarations by module\n    documents: dict[str, list[qlast.DDLCommand]] = defaultdict(list)\n    # initialize the \"default\" module\n    documents[s_mod.DEFAULT_MODULE_ALIAS] = []\n    extensions = {}\n    futures = {}\n\n    def collect(\n        decl: qlast.ObjectDDL | qlast.ModuleDeclaration,\n        module: Optional[str],\n    ) -> None:\n        # declarations are either in a module block or fully-qualified\n        if isinstance(decl, qlast.ModuleDeclaration):\n            new_mod = (\n                f'{module}::{decl.name.name}' if module else decl.name.name)\n            # make sure the new one is present\n            documents.setdefault(new_mod, [])\n            for sdecl in decl.declarations:\n                collect(sdecl, new_mod)\n        elif isinstance(decl, qlast.CreateExtension):\n            assert not module\n            extensions[decl.name.name] = decl\n        elif isinstance(decl, qlast.CreateFuture):\n            assert not module\n            futures[decl.name.name] = decl\n        else:\n            assert isinstance(decl, qlast.ObjectDDL)\n            assert module or decl.name.module is not None\n            if decl.name.module is None:\n                assert module\n                name = module\n            else:\n                name = (\n                    f'{module}::{decl.name.name}'\n                    if module else decl.name.module)\n\n            documents[name].append(decl)\n\n    context = sd.CommandContext(\n        modaliases={},\n        schema=base_schema,\n        stdmode=stdmode,\n        testmode=testmode,\n        declarative=True,\n    )\n\n    for decl in sdl_document.declarations:\n        collect(decl, None)\n\n    target_schema = base_schema\n    warnings = []\n\n    def process(ddl_stmt: qlast.DDLCommand) -> None:\n        nonlocal target_schema\n        delta = sd.DeltaRoot()\n        with context(sd.DeltaRootContext(schema=target_schema, op=delta)):\n            cmd = cmd_from_ddl(\n                ddl_stmt, schema=target_schema, modaliases={},\n                context=context, testmode=testmode)\n\n            delta.add(cmd)\n            target_schema = delta.apply(target_schema, context)\n            context.schema = target_schema\n        warnings.extend(delta.warnings)\n\n    # Process all the extensions first, since sdl_to_ddl needs to be\n    # able to see their contents.  While we do so, also collect any\n    # transitive dependency extensions and add those as well.  We this\n    # dependency resolution automatically as part of SDL processing\n    # instead of when doing CREATE EXTENSION because I didn't want\n    # *DROP EXTENSION* to automatically drop transitive dependencies,\n    # and so CREATE EXTENSION shouldn't either, symmetrically.\n    extensions_done = set()\n\n    def process_ext(ddl_stmt: qlast.CreateExtension) -> None:\n        name = ddl_stmt.name.name\n        pkg = s_ext.get_package(\n            sn.UnqualName(name),\n            (\n                verutils.parse_version(ddl_stmt.version.value)\n                if ddl_stmt.version else None\n            ),\n            base_schema,\n        )\n\n        pkg_ver = pkg.get_version(base_schema)\n        if (name, pkg_ver) in extensions_done:\n            return\n        extensions_done.add((name, pkg_ver))\n\n        if pkg:\n            for dep in pkg.get_dependencies(base_schema):\n                if '>=' not in dep:\n                    builtin = (\n                        'built-in ' if pkg.get_builtin(base_schema) else ''\n                    )\n                    raise errors.SchemaError(\n                        f'{builtin}extension {name} missing version for {dep}')\n                dep, dep_version = dep.split('>=')\n\n                process_ext(\n                    qlast.CreateExtension(\n                        name=qlast.ObjectRef(name=dep),\n                        version=qlast.Constant.string(value=dep_version),\n                    )\n                )\n\n        process(ddl_stmt)\n\n    ddl_stmt: qlast.DDLCommand\n    for ddl_stmt in extensions.values():\n        process_ext(ddl_stmt)\n\n    # Now, sort the main body of SDL and apply it.\n    ddl_stmts = s_decl.sdl_to_ddl(target_schema, documents)\n\n    if debug.flags.sdl_loading:\n        debug.header('SDL loading script')\n        for ddl_stmt in itertools.chain(\n            extensions.values(), futures.values(), ddl_stmts\n        ):\n            ddl_stmt.dump_edgeql()\n\n    for ddl_stmt in itertools.chain(futures.values(), ddl_stmts):\n        process(ddl_stmt)\n\n    return target_schema, warnings\n\n\ndef apply_ddl_script(\n    ddl_text: str,\n    *,\n    schema: s_schema.Schema,\n    modaliases: Optional[Mapping[Optional[str], str]] = None,\n    stdmode: bool = False,\n    testmode: bool = False,\n) -> s_schema.Schema:\n\n    schema, _ = apply_ddl_script_ex(\n        ddl_text,\n        schema=schema,\n        modaliases=modaliases,\n        stdmode=stdmode,\n        testmode=testmode,\n    )\n\n    return schema\n\n\ndef apply_ddl_script_ex(\n    ddl_text: str,\n    *,\n    schema: s_schema.Schema,\n    modaliases: Optional[Mapping[Optional[str], str]] = None,\n    stdmode: bool = False,\n    internal_schema_mode: bool = False,\n    testmode: bool = False,\n    store_migration_sdl: bool=False,\n    schema_object_ids: Optional[\n        Mapping[tuple[sn.Name, Optional[str]], uuid.UUID]\n    ]=None,\n    compat_ver: Optional[verutils.Version] = None,\n) -> tuple[s_schema.Schema, sd.DeltaRoot]:\n\n    delta = sd.DeltaRoot()\n\n    if modaliases is None:\n        modaliases = {}\n\n    for ddl_stmt in edgeql.parse_block(ddl_text):\n        if not isinstance(ddl_stmt, qlast.DDLCommand):\n            raise AssertionError(f'expected DDLCommand node, got {ddl_stmt!r}')\n        schema, cmd = delta_and_schema_from_ddl(\n            ddl_stmt,\n            schema=schema,\n            modaliases=modaliases,\n            stdmode=stdmode,\n            internal_schema_mode=internal_schema_mode,\n            testmode=testmode,\n            store_migration_sdl=store_migration_sdl,\n            schema_object_ids=schema_object_ids,\n            compat_ver=compat_ver,\n        )\n\n        delta.add(cmd)\n\n    return schema, delta\n\n\ndef delta_from_ddl(\n    ddl_stmt: qlast.DDLCommand,\n    *,\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    stdmode: bool=False,\n    testmode: bool=False,\n    store_migration_sdl: bool=False,\n    schema_object_ids: Optional[\n        Mapping[tuple[sn.Name, Optional[str]], uuid.UUID]\n    ]=None,\n    compat_ver: Optional[verutils.Version] = None,\n) -> sd.DeltaRoot:\n    _, cmd = delta_and_schema_from_ddl(\n        ddl_stmt,\n        schema=schema,\n        modaliases=modaliases,\n        stdmode=stdmode,\n        testmode=testmode,\n        store_migration_sdl=store_migration_sdl,\n        schema_object_ids=schema_object_ids,\n        compat_ver=compat_ver,\n    )\n    return cmd\n\n\ndef delta_and_schema_from_ddl(\n    ddl_stmt: qlast.DDLCommand,\n    *,\n    schema: s_schema.Schema,\n    modaliases: Mapping[Optional[str], str],\n    stdmode: bool=False,\n    internal_schema_mode: bool=False,\n    testmode: bool=False,\n    store_migration_sdl: bool=False,\n    schema_object_ids: Optional[\n        Mapping[tuple[sn.Name, Optional[str]], uuid.UUID]\n    ]=None,\n    compat_ver: Optional[verutils.Version] = None,\n) -> tuple[s_schema.Schema, sd.DeltaRoot]:\n    delta = sd.DeltaRoot()\n    context = sd.CommandContext(\n        modaliases=modaliases,\n        schema=schema,\n        stdmode=stdmode,\n        internal_schema_mode=internal_schema_mode,\n        testmode=testmode,\n        store_migration_sdl=store_migration_sdl,\n        schema_object_ids=schema_object_ids,\n        compat_ver=compat_ver,\n    )\n\n    with context(sd.DeltaRootContext(schema=schema, op=delta)):\n        cmd = cmd_from_ddl(\n            ddl_stmt,\n            schema=schema,\n            modaliases=modaliases,\n            context=context,\n            testmode=testmode,\n        )\n        if debug.flags.delta_plan:\n            debug.header('Delta Plan Input')\n            debug.dump(cmd)\n\n        schema = cmd.apply(schema, context)\n\n        if not stdmode:\n            if not isinstance(\n                cmd,\n                (sd.GlobalObjectCommand, sd.ExternalObjectCommand),\n            ):\n                ver = schema.get_global(\n                    s_ver.SchemaVersion, '__schema_version__')\n                ver_cmd = ver.init_delta_command(schema, sd.AlterObject)\n                ver_cmd.set_attribute_value('version', uuidgen.uuid1mc())\n                schema = ver_cmd.apply(schema, context)\n                delta.add(ver_cmd)\n            elif not isinstance(cmd, sd.ExternalObjectCommand):\n                gver = schema.get_global(\n                    s_ver.GlobalSchemaVersion, '__global_schema_version__')\n                g_ver_cmd = gver.init_delta_command(schema, sd.AlterObject)\n                g_ver_cmd.set_attribute_value('version', uuidgen.uuid1mc())\n                schema = g_ver_cmd.apply(schema, context)\n                delta.add(g_ver_cmd)\n\n        delta.add(cmd)\n\n    delta.canonical = True\n    return schema, delta\n\n\ndef ddlast_from_delta(\n    schema_a: Optional[s_schema.Schema],\n    schema_b: s_schema.Schema,\n    delta: sd.DeltaRoot,\n    *,\n    sdlmode: bool = False,\n    testmode: bool = False,\n    descriptive_mode: bool = False,\n    include_ext_version: bool = True,\n) -> dict[qlast.DDLOperation, sd.Command]:\n    context = sd.CommandContext(\n        descriptive_mode=descriptive_mode,\n        declarative=sdlmode,\n        testmode=testmode,\n        include_ext_version=include_ext_version,\n    )\n\n    if schema_a is None:\n        schema = schema_b\n        update_schema = False\n    else:\n        schema = schema_a\n        update_schema = True\n\n    stmts = {}\n    for command in delta.get_subcommands():\n        with context(sd.DeltaRootContext(schema=schema, op=delta)):\n            # The reason we do this instead of just directly using the new\n            # schema is to populate the renames field of the context.\n            # We do this one part at a time to avoid referring to things\n            # that have not been renamed yet.\n            # XXX: Is this fine-grained enough, though?\n            if update_schema:\n                schema = command.apply(schema, context)\n\n            ql_ast = command.get_ast(schema, context)\n            if ql_ast:\n                stmts[ql_ast] = command\n\n    return stmts\n\n\ndef statements_from_delta(\n    schema_a: Optional[s_schema.Schema],\n    schema_b: s_schema.Schema,\n    delta: sd.DeltaRoot,\n    *,\n    sdlmode: bool = False,\n    descriptive_mode: bool = False,\n    # Used for backwards compatibility with older migration text.\n    uppercase: bool = False,\n    limit_ref_classes: Iterable[so.ObjectMeta] = tuple(),\n    include_ext_version: bool = True,\n) -> tuple[tuple[str, qlast.DDLOperation, sd.Command], ...]:\n\n    stmts = ddlast_from_delta(\n        schema_a,\n        schema_b,\n        delta,\n        sdlmode=sdlmode,\n        descriptive_mode=descriptive_mode,\n        include_ext_version=include_ext_version,\n    )\n\n    ql_classes_src = {\n        scls.get_ql_class() for scls in limit_ref_classes\n    }\n\n    ql_classes = {q for q in ql_classes_src if q is not None}\n\n    # If we're generating SDL and it includes modules, try to nest the\n    # module contents in the actual modules.\n    processed: list[tuple[qlast.DDLOperation, sd.Command]] = []\n    unqualified: list[tuple[qlast.DDLOperation, sd.Command]] = []\n    modules = dict()\n    for stmt_ast, cmd in stmts.items():\n        if sdlmode:\n            if isinstance(stmt_ast, qlast.CreateModule):\n                # Record the module stubs.\n                modules[stmt_ast.name.name] = stmt_ast\n                stmt_ast.commands = []\n                processed.append((stmt_ast, cmd))\n\n            elif (\n                modules\n                and not isinstance(stmt_ast, qlast.UnqualifiedObjectCommand)\n            ):\n                # This SDL included creation of modules, so we will try to\n                # nest the declarations in them.\n                assert isinstance(stmt_ast, qlast.CreateObject)\n                assert stmt_ast.name.module is not None\n                module = modules[stmt_ast.name.module]\n                module.commands.append(stmt_ast)\n                # Strip the module from the object name, since we nest\n                # them in a module already.\n                stmt_ast.name.module = None\n\n            elif isinstance(stmt_ast, qlast.UnqualifiedObjectCommand):\n                unqualified.append((stmt_ast, cmd))\n\n            else:\n                processed.append((stmt_ast, cmd))\n\n        else:\n            processed.append((stmt_ast, cmd))\n\n    text = []\n    for stmt_ast, cmd in itertools.chain(unqualified, processed):\n        stmt_text = edgeql.generate_source(\n            stmt_ast,\n            sdlmode=sdlmode,\n            descmode=descriptive_mode,\n            limit_ref_classes=ql_classes,\n            uppercase=uppercase,\n        )\n        text.append((stmt_text + ';', stmt_ast, cmd))\n\n    return tuple(text)\n\n\ndef text_from_delta(\n    schema_a: Optional[s_schema.Schema],\n    schema_b: s_schema.Schema,\n    delta: sd.DeltaRoot,\n    *,\n    sdlmode: bool = False,\n    descriptive_mode: bool = False,\n    limit_ref_classes: Iterable[so.ObjectMeta] = tuple(),\n    include_ext_version: bool = True,\n) -> str:\n    stmts = statements_from_delta(\n        schema_a,\n        schema_b,\n        delta,\n        sdlmode=sdlmode,\n        descriptive_mode=descriptive_mode,\n        limit_ref_classes=limit_ref_classes,\n        include_ext_version=include_ext_version,\n    )\n    return '\\n'.join(text for text, _, _ in stmts)\n\n\ndef ddl_text_from_delta(\n    schema_a: Optional[s_schema.Schema],\n    schema_b: s_schema.Schema,\n    delta: sd.DeltaRoot,\n    *,\n    include_ext_version: bool = True,\n) -> str:\n    \"\"\"Return DDL text corresponding to a delta plan.\n\n    Args:\n        schema_a:\n            The original schema (or None if starting from empty/std)\n        schema_b:\n            The schema to which the *delta* has **already** been\n            applied.\n        delta:\n            The delta plan.\n\n    Returns:\n        DDL text corresponding to *delta*.\n    \"\"\"\n    return text_from_delta(\n        schema_a,\n        schema_b,\n        delta,\n        sdlmode=False,\n        include_ext_version=include_ext_version,\n    )\n\n\ndef sdl_text_from_delta(\n    schema_a: Optional[s_schema.Schema],\n    schema_b: s_schema.Schema,\n    delta: sd.DeltaRoot,\n) -> str:\n    \"\"\"Return SDL text corresponding to a delta plan.\n\n    Args:\n        schema_a:\n            The original schema (or None if starting from empty/std)\n        schema_b:\n            The schema to which the *delta* has **already** been\n            applied.\n        delta:\n            The delta plan.\n\n    Returns:\n        SDL text corresponding to *delta*.\n    \"\"\"\n    return text_from_delta(schema_a, schema_b, delta, sdlmode=True)\n\n\ndef descriptive_text_from_delta(\n    schema_a: Optional[s_schema.Schema],\n    schema_b: s_schema.Schema,\n    delta: sd.DeltaRoot,\n    *,\n    limit_ref_classes: Iterable[so.ObjectMeta]=tuple(),\n) -> str:\n    \"\"\"Return descriptive text corresponding to a delta plan.\n\n    Args:\n        schema_a:\n            The original schema (or None if starting from empty/std)\n        schema_b:\n            The schema to which the *delta* has **already** been\n            applied.\n        delta:\n            The delta plan.\n        limit_ref_classes:\n            If specified, limit the output of referenced objects\n            to the specified classes.\n\n    Returns:\n        Descriptive text corresponding to *delta*.\n    \"\"\"\n    return text_from_delta(\n        schema_a,\n        schema_b,\n        delta,\n        sdlmode=True,\n        descriptive_mode=True,\n        limit_ref_classes=limit_ref_classes,\n    )\n\n\ndef ddl_text_from_schema(\n    schema: s_schema.Schema,\n    *,\n    included_modules: Optional[Iterable[sn.Name]] = None,\n    excluded_modules: Optional[Iterable[sn.Name]] = None,\n    included_items: Optional[Iterable[sn.Name]] = None,\n    excluded_items: Optional[Iterable[sn.Name]] = None,\n    included_ref_classes: Iterable[so.ObjectMeta] = tuple(),\n    include_module_ddl: bool = True,\n    include_std_ddl: bool = False,\n    include_migrations: bool = False,\n) -> str:\n    diff = delta_schemas(\n        schema_a=None,\n        schema_b=schema,\n        included_modules=included_modules,\n        excluded_modules=excluded_modules,\n        included_items=included_items,\n        excluded_items=excluded_items,\n        include_module_diff=include_module_ddl,\n        include_std_diff=include_std_ddl,\n        include_derived_types=False,\n    )\n    if include_migrations:\n        context = so.ComparisonContext()\n        for mig in s_migr.get_ordered_migrations(schema):\n            diff.add(mig.as_create_delta(schema, context))\n\n    return ddl_text_from_delta(None, schema, diff,\n                               include_ext_version=not include_migrations)\n\n\ndef sdl_text_from_schema(\n    schema: s_schema.Schema,\n    *,\n    included_modules: Optional[Iterable[sn.Name]] = None,\n    excluded_modules: Optional[Iterable[sn.Name]] = None,\n    included_items: Optional[Iterable[sn.Name]] = None,\n    excluded_items: Optional[Iterable[sn.Name]] = None,\n    included_ref_classes: Iterable[so.ObjectMeta] = tuple(),\n    include_module_ddl: bool = True,\n    include_std_ddl: bool = False,\n) -> str:\n    diff = delta_schemas(\n        schema_a=None,\n        schema_b=schema,\n        included_modules=included_modules,\n        excluded_modules=excluded_modules,\n        included_items=included_items,\n        excluded_items=excluded_items,\n        include_module_diff=include_module_ddl,\n        include_std_diff=include_std_ddl,\n        include_derived_types=False,\n        linearize_delta=False,\n    )\n    return sdl_text_from_delta(None, schema, diff)\n\n\ndef descriptive_text_from_schema(\n    schema: s_schema.Schema,\n    *,\n    included_modules: Optional[Iterable[sn.Name]] = None,\n    excluded_modules: Optional[Iterable[sn.Name]] = None,\n    included_items: Optional[Iterable[sn.Name]] = None,\n    excluded_items: Optional[Iterable[sn.Name]] = None,\n    included_ref_classes: Iterable[so.ObjectMeta] = tuple(),\n    include_module_ddl: bool = True,\n    include_std_ddl: bool = False,\n    include_derived_types: bool = False,\n) -> str:\n    diff = delta_schemas(\n        schema_a=None,\n        schema_b=schema,\n        included_modules=included_modules,\n        excluded_modules=excluded_modules,\n        included_items=included_items,\n        excluded_items=excluded_items,\n        include_module_diff=include_module_ddl,\n        include_std_diff=include_std_ddl,\n        include_derived_types=False,\n        linearize_delta=False,\n        descriptive_mode=True,\n    )\n    return descriptive_text_from_delta(\n        None, schema, diff, limit_ref_classes=included_ref_classes)\n"
  },
  {
    "path": "edb/schema/defines.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n# Maximum length of Postgres tenant ID.\nMAX_TENANT_ID_LENGTH = 10\n\n# Maximum length of names that are reflected 1:1 to Postgres:\nMAX_NAME_LENGTH = 63 - MAX_TENANT_ID_LENGTH - 1 - 1\n#                 ^                           ^   ^\n#    max Postgres name len     tenant_id scheme   tenant_id separator\n\n# Maximum number of arguments supported by SQL functions.\nMAX_FUNC_ARG_COUNT = 100\n\nEDGEDB_SUPERUSER = 'admin'\nEDGEDB_OLD_SUPERUSER = 'edgedb'\nEDGEDB_TEMPLATE_DB = '__edgedbtpl__'\nEDGEDB_SYSTEM_DB = '__edgedbsys__'\n\nEDGEDB_SPECIAL_DBS = {EDGEDB_TEMPLATE_DB, EDGEDB_SYSTEM_DB}\n"
  },
  {
    "path": "edb/schema/delta.py",
    "content": "# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    AbstractSet,\n    Any,\n    Callable,\n    cast,\n    ClassVar,\n    Generator,\n    Generic,\n    Hashable,\n    Iterable,\n    Iterator,\n    Mapping,\n    NoReturn,\n    Optional,\n    overload,\n    Self,\n    Sequence,\n    TypeVar,\n)\n\nimport collections\nimport collections.abc\nimport contextlib\nimport functools\nimport itertools\nimport uuid\n\nfrom edb import errors\n\nfrom edb.common import adapter\nfrom edb.common import ast\nfrom edb.common import checked\nfrom edb.common import markup\nfrom edb.common import ordered\nfrom edb.common import parsing\nfrom edb.common import struct\nfrom edb.common import topological\nfrom edb.common import typing_inspect\nfrom edb.common import verutils\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import compiler as qlcompiler\n\nfrom . import expr as s_expr\nfrom . import name as sn\nfrom . import objects as so\nfrom . import schema as s_schema\nfrom . import utils\n\n\ndef delta_objects(\n    old_in: Iterable[so.Object_T],\n    new_in: Iterable[so.Object_T],\n    sclass: type[so.Object_T],\n    *,\n    parent_confidence: Optional[float] = None,\n    context: so.ComparisonContext,\n    old_schema: s_schema.Schema,\n    new_schema: s_schema.Schema,\n) -> DeltaRoot:\n\n    delta = DeltaRoot()\n\n    # TODO: Previously, we attempted to do an optimization based on\n    # computing a hash_criteria of each object, in order to discard\n    # unchanged objects. Because hash_criteria returns values that\n    # include schema objects, the optimization didn't work; it wasn't\n    # cheap, so I removed it. But the general idea is sound and should\n    # be revisited.\n    old = {o.get_name(old_schema): o for o in old_in}\n    new = {o.get_name(new_schema): o for o in new_in}\n\n    # If an object exists with the same name in both the old and the\n    # new schemas, we don't compare those objects to anything but\n    # each other. This makes our runtime linear in most common cases,\n    # though the worst case remains quadratic.\n    #\n    # This unfortunately means that we have trouble understanding\n    # \"chain renames\" (Foo -> Bar, Bar -> Baz), which may be worth\n    # addressing in the future, but we fail to understand that for\n    # other reasons also.\n\n    # Collect all the pairs of objects with the same name in both schemas.\n    pairs = [\n        (new[k], o) for k, o in old.items() if k in new\n    ]\n    # Then collect the cross product of all the other objects.\n    pairs.extend(\n        itertools.product(\n            [o for k, o in new.items() if k not in old],\n            [o for k, o in old.items() if k not in new],\n        )\n    )\n\n    full_matrix: list[tuple[so.Object_T, so.Object_T, float]] = []\n\n    # If there are any renames that are already decided on, honor those first\n    renames_x: set[sn.Name] = set()\n    renames_y: set[sn.Name] = set()\n    for y in old.values():\n        rename = context.renames.get((type(y), y.get_name(old_schema)))\n        if rename:\n            renames_x.add(rename.new_name)\n            renames_y.add(rename.classname)\n\n    if context.guidance is not None:\n        guidance = context.guidance\n\n        # In these functions, we need to look at the actual object to\n        # figure out the type instead of just using sclass because\n        # sclass might be an abstract parent like Pointer.\n        def can_create(obj: so.Object_T, name: sn.Name) -> bool:\n            return (type(obj), name) not in guidance.banned_creations\n\n        def can_alter(\n            obj: so.Object_T, old_name: sn.Name, new_name: sn.Name\n        ) -> bool:\n            return (\n                (type(obj), (old_name, new_name))\n                not in guidance.banned_alters)\n\n        def can_delete(obj: so.Object_T, name: sn.Name) -> bool:\n            return (type(obj), name) not in guidance.banned_deletions\n    else:\n        def can_create(obj: so.Object_T, name: sn.Name) -> bool:\n            return True\n\n        def can_alter(\n            obj: so.Object_T, old_name: sn.Name, new_name: sn.Name\n        ) -> bool:\n            return True\n\n        def can_delete(obj: so.Object_T, name: sn.Name) -> bool:\n            return True\n\n    for x, y in pairs:\n        x_name = x.get_name(new_schema)\n        y_name = y.get_name(old_schema)\n\n        similarity = y.compare(\n            x,\n            our_schema=old_schema,\n            their_schema=new_schema,\n            context=context,\n        )\n        # If similarity for an alter is 1.0, that means there is no\n        # actual change. We keep that, since otherwise we will generate\n        # extra drop/create pairs when we are already done.\n        if similarity < 1.0 and not can_alter(y, y_name, x_name):\n            similarity = 0.0\n\n        full_matrix.append((x, y, similarity))\n\n    full_matrix.sort(\n        key=lambda v: (\n            1.0 - v[2],\n            str(v[0].get_name(new_schema)),\n            str(v[1].get_name(old_schema)),\n        ),\n    )\n\n    full_matrix_x = {}\n    full_matrix_y = {}\n\n    seen_x = set()\n    seen_y = set()\n    x_alter_variants: dict[so.Object_T, int] = collections.defaultdict(int)\n    y_alter_variants: dict[so.Object_T, int] = collections.defaultdict(int)\n    comparison_map: dict[so.Object_T, tuple[float, so.Object_T]] = {}\n    comparison_map_y: dict[so.Object_T, tuple[float, so.Object_T]] = {}\n\n    # Find the top similarity pairs\n    for x, y, similarity in full_matrix:\n        if x not in seen_x and y not in seen_y:\n            comparison_map[x] = (similarity, y)\n            comparison_map_y[y] = (similarity, x)\n            seen_x.add(x)\n            seen_y.add(y)\n\n        if x not in full_matrix_x:\n            full_matrix_x[x] = (similarity, y)\n\n        if y not in full_matrix_y:\n            full_matrix_y[y] = (similarity, x)\n\n        if (\n            can_alter(y, y.get_name(old_schema), x.get_name(new_schema))\n            and full_matrix_x[x][0] != 1.0\n            and full_matrix_y[y][0] != 1.0\n        ):\n            x_alter_variants[x] += 1\n            y_alter_variants[y] += 1\n\n    alters = []\n    alter_pairs = []\n\n    if comparison_map:\n        if issubclass(sclass, so.InheritingObject):\n            # Generate the diff from the top of the inheritance\n            # hierarchy, since changes to parent objects may inform\n            # how the delta in child objects is treated.\n            order_x = cast(\n                Iterable[so.Object_T],\n                sort_by_inheritance(\n                    new_schema,\n                    cast(Iterable[so.InheritingObject], comparison_map),\n                ),\n            )\n        else:\n            order_x = comparison_map\n\n        for x in order_x:\n            confidence, y = comparison_map[x]\n            x_name = x.get_name(new_schema)\n            y_name = y.get_name(old_schema)\n\n            already_has = x_name == y_name and x_name not in renames_x\n            if (\n                (0.6 < confidence < 1.0 and can_alter(y, y_name, x_name))\n                or (\n                    (not can_create(x, x_name) or not can_delete(y, y_name))\n                    and can_alter(y, y_name, x_name)\n                )\n                or x_name in renames_x\n            ):\n                alter_pairs.append((x, y))\n\n                alter = y.as_alter_delta(\n                    other=x,\n                    context=context,\n                    self_schema=old_schema,\n                    other_schema=new_schema,\n                    confidence=confidence,\n                )\n\n                # If we are basically certain about this alter,\n                # make the confidence 1.0, unless child steps\n                # are not confident.\n                if not (\n                    (x_alter_variants[x] > 1 or (\n                        not already_has and can_create(x, x_name)))\n                    and parent_confidence != 1.0\n                ):\n                    cons = [\n                        sub.get_annotation('confidence')\n                        for sub in alter.get_subcommands(type=ObjectCommand)\n                    ]\n                    confidence = min(\n                        [1.0, *[c for c in cons if c is not None]])\n\n                alter.set_annotation('confidence', confidence)\n                alters.append(alter)\n            elif confidence == 1.0:\n                alter_pairs.append((x, y))\n\n    created = ordered.OrderedSet(new.values()) - {x for x, _ in alter_pairs}\n\n    for x in created:\n        x_name = x.get_name(new_schema)\n        if can_create(x, x_name) and x_name not in renames_x:\n            create = x.as_create_delta(schema=new_schema, context=context)\n            if x_alter_variants[x] > 0 and parent_confidence != 1.0:\n                confidence = full_matrix_x[x][0]\n            else:\n                confidence = 1.0\n            create.set_annotation('confidence', confidence)\n            delta.add(create)\n\n    delta.update(alters)\n\n    deleted_order: Iterable[so.Object_T]\n    deleted = ordered.OrderedSet(old.values()) - {y for _, y in alter_pairs}\n\n    if issubclass(sclass, so.InheritingObject):\n        deleted_order = sort_by_inheritance(  # type: ignore[assignment]\n            old_schema,\n            cast(Iterable[so.InheritingObject], deleted),\n        )\n    else:\n        deleted_order = deleted\n\n    for y in deleted_order:\n        y_name = y.get_name(old_schema)\n        if can_delete(y, y_name) and y_name not in renames_y:\n            delete = y.as_delete_delta(schema=old_schema, context=context)\n            if y_alter_variants[y] > 0 and parent_confidence != 1.0:\n                confidence = full_matrix_y[y][0]\n            else:\n                confidence = 1.0\n            delete.set_annotation('confidence', confidence)\n            delta.add(delete)\n\n    return delta\n\n\ndef sort_by_inheritance(\n    schema: s_schema.Schema,\n    objs: Iterable[so.InheritingObjectT],\n) -> tuple[so.InheritingObjectT, ...]:\n    graph = {}\n    for x in objs:\n        graph[x] = topological.DepGraphEntry(\n            item=x,\n            deps=ordered.OrderedSet(x.get_ancestors(schema).objects(schema)),\n            extra=False,\n        )\n\n    return topological.sort(graph, allow_unresolved=True)\n\n\nT = TypeVar(\"T\")\n\n\ndef sort_by_cross_refs_key(\n    schema: s_schema.Schema,\n    objs: Iterable[T], *,\n    key: Callable[[T], so.Object],\n) -> tuple[T, ...]:\n    \"\"\"Sort an iterable of objects according to cross-references between them.\n\n    Return a toplogical ordering of a graph of objects joined by references.\n    It is assumed that the graph has no cycles.\n    \"\"\"\n    graph = {}\n\n    # We want to report longer cycles before trivial self references,\n    # since cycles with (for example) computed properties will *also*\n    # lead to self references (because the computed property gets\n    # inlined, essentially).\n    self_ref = None\n    for entry in objs:\n        x = key(entry)\n        referrers = schema.get_referrers(x)\n        if x in referrers:\n            self_ref = x\n        graph[x] = topological.DepGraphEntry(\n            item=entry,\n            deps={ref for ref in referrers\n                  if not x.is_parent_ref(schema, ref) and x != ref},\n            extra=False,\n        )\n\n    res = topological.sort(graph, allow_unresolved=True)\n\n    if self_ref:\n        raise topological.CycleError(\n            f\"{self_ref!r} refers to itself\", item=self_ref)\n\n    return res\n\n\ndef sort_by_cross_refs[ObjectT: so.Object](\n    schema: s_schema.Schema,\n    objs: Iterable[ObjectT],\n) -> tuple[ObjectT, ...]:\n    return sort_by_cross_refs_key(schema, objs, key=lambda x: x)\n\n\nclass CommandMeta(\n    adapter.Adapter,\n    struct.MixedStructMeta,\n):\n\n    _astnode_map: dict[type[qlast.DDLOperation], type[Command]] = {}\n\n    def __new__[CommandMeta_T: CommandMeta](\n        mcls: type[CommandMeta_T],\n        name: str,\n        bases: tuple[type, ...],\n        dct: dict[str, Any],\n        *,\n        context_class: Optional[type[CommandContextToken[Command]]] = None,\n        **kwargs: Any,\n    ) -> CommandMeta_T:\n        cls = super().__new__(mcls, name, bases, dct, **kwargs)\n\n        if context_class is not None:\n            cast(Command, cls)._context_class = context_class\n\n        return cls\n\n    def __init__(\n        cls,\n        name: str,\n        bases: tuple[type, ...],\n        clsdict: dict[str, Any],\n        *,\n        adapts: Optional[type] = None,\n        **kwargs: Any,\n    ) -> None:\n        adapter.Adapter.__init__(cls, name, bases, clsdict, adapts=adapts)\n        struct.MixedStructMeta.__init__(cls, name, bases, clsdict)\n        astnodes = clsdict.get('astnode')\n        if astnodes and not isinstance(astnodes, (list, tuple)):\n            astnodes = [astnodes]\n        if astnodes:\n            cls.register_astnodes(astnodes)\n\n    def register_astnodes(\n        cls,\n        astnodes: Iterable[type[qlast.DDLCommand]],\n    ) -> None:\n        mapping = type(cls)._astnode_map\n\n        for astnode in astnodes:\n            existing = mapping.get(astnode)\n            if existing:\n                msg = ('duplicate EdgeQL AST node to command mapping: ' +\n                       '{!r} is already declared for {!r}')\n                raise TypeError(msg.format(astnode, existing))\n\n            mapping[astnode] = cast(type[\"Command\"], cls)\n\n\n# We use _DummyObject for contexts where an instance of an object is\n# required by type signatures, and the actual reference will be quickly\n# replaced by a real object.\n_dummy_object = so.Object(\n    _private_id=uuid.UUID('C0FFEE00-C0DE-0000-0000-000000000000'),\n)\n\n\nCommand_T = TypeVar(\"Command_T\", bound=\"Command\")\nCommand_T_co = TypeVar(\"Command_T_co\", bound=\"Command\", covariant=True)\n\n\nclass Command(\n    struct.MixedStruct,\n    markup.MarkupCapableMixin,\n    metaclass=CommandMeta,\n):\n    span = struct.Field(parsing.Span, default=None)\n    canonical = struct.Field(bool, default=False)\n\n    _context_class: Optional[type[CommandContextToken[Command]]] = None\n\n    #: An optional list of commands that are prerequisites of this\n    #: command and must run before any of the operations in this\n    #: command or its subcommands in ops or caused_ops.\n    before_ops: list[Command]\n    #: An optional list of subcommands that are considered to be\n    #: integral part of this command.\n    ops: list[Command]\n    #: An optional list of commands that are _caused_ by this command,\n    #: such as any propagation to children or any other side-effects\n    #: that are not considered integral to this command.\n    caused_ops: list[Command]\n\n    #: AlterObjectProperty lookup table for get|set_attribute_value\n    _attrs: dict[str, AlterObjectProperty]\n    #: AlterSpecialObjectField lookup table\n    _special_attrs: dict[str, AlterSpecialObjectField[so.Object]]\n\n    def __init__(self, **kwargs: Any) -> None:\n        super().__init__(**kwargs)\n        self.ops = []\n        self.before_ops = []\n        self.caused_ops = []\n        self.qlast: qlast.DDLOperation\n        self._attrs = {}\n        self._special_attrs = {}\n\n    def dump(self) -> None:\n        markup.dump(self)\n\n    def copy(self: Self) -> Self:\n        result = super().copy()\n        result.before_ops = [op.copy() for op in self.before_ops]\n        result.ops = [op.copy() for op in self.ops]\n        result.caused_ops = [op.copy() for op in self.caused_ops]\n        return result\n\n    def get_verb(self) -> str:\n        \"\"\"Return a verb representing this command in infinitive form.\"\"\"\n        raise NotImplementedError\n\n    def get_friendly_description(\n        self,\n        *,\n        parent_op: Optional[Command] = None,\n        schema: Optional[s_schema.Schema] = None,\n        object: Any = None,\n        object_desc: Optional[str] = None,\n    ) -> str:\n        \"\"\"Return a friendly description of this command in imperative mood.\n\n        The result is used in error messages and other user-facing renderings\n        of the command.\n        \"\"\"\n        raise NotImplementedError\n\n    @classmethod\n    def adapt(cls: type[Command_T], obj: Command) -> Command_T:\n        result = obj.copy_with_class(cls)\n        mcls = cast(CommandMeta, type(cls))\n        for op in obj.get_prerequisites():\n            result.add_prerequisite(mcls.adapt(op))\n        for op in obj.get_subcommands(\n            include_prerequisites=False,\n            include_caused=False,\n        ):\n            result.add(mcls.adapt(op))\n        for op in obj.get_caused():\n            result.add_caused(mcls.adapt(op))\n        return result\n\n    def is_data_safe(self) -> bool:\n        return False\n\n    def get_required_user_input(self) -> list[dict[str, str]]:\n        return []\n\n    def record_diff_annotations(\n        self,\n        *,\n        schema: s_schema.Schema,\n        orig_schema: Optional[s_schema.Schema],\n        context: so.ComparisonContext,\n        orig_object: Optional[so.Object],\n        object: Optional[so.Object],\n    ) -> None:\n        \"\"\"Record extra information on a delta obtained by diffing schemas.\n\n        This provides an apportunity for a delta command to annotate itself\n        in schema diff schenarios (i.e. migrations).\n\n        Args:\n            schema:\n                Final schema of a migration.\n\n            orig_schema:\n                Original schema of a migration.\n\n            context:\n                Schema comparison context.\n        \"\"\"\n        pass\n\n    def resolve_obj_collection(\n        self,\n        value: Any,\n        schema: s_schema.Schema,\n    ) -> Sequence[so.Object]:\n        sequence: Sequence[so.Object]\n        if isinstance(value, so.ObjectCollection):\n            sequence = value.objects(schema)\n        else:\n            sequence = []\n            for v in value:\n                if isinstance(v, so.Shell):\n                    val = v.resolve(schema)\n                else:\n                    val = v\n                sequence.append(val)\n        return sequence\n\n    def _resolve_attr_value(\n        self,\n        value: Any,\n        fname: str,\n        field: so.Field[Any],\n        schema: s_schema.Schema,\n    ) -> Any:\n        ftype = field.type\n\n        if isinstance(value, so.Shell):\n            value = value.resolve(schema)\n        else:\n            if issubclass(ftype, so.ObjectDict):\n                if isinstance(value, so.ObjectDict):\n                    items = dict(value.items(schema))\n                elif isinstance(value, collections.abc.Mapping):\n                    items = {}\n                    for k, v in value.items():\n                        if isinstance(v, so.Shell):\n                            val = v.resolve(schema)\n                        else:\n                            val = v\n                        items[k] = val\n\n                value = ftype.create(schema, items)\n\n            elif issubclass(ftype, so.ObjectCollection):\n                sequence = self.resolve_obj_collection(value, schema)\n                value = ftype.create(schema, sequence)\n\n            else:\n                value = field.coerce_value(schema, value)\n\n        return value\n\n    def enumerate_attributes(self) -> tuple[str, ...]:\n        return tuple(self._attrs)\n\n    def _enumerate_attribute_cmds(self) -> tuple[AlterObjectProperty, ...]:\n        return tuple(self._attrs.values())\n\n    def has_attribute_value(self, attr_name: str) -> bool:\n        return attr_name in self._attrs or attr_name in self._special_attrs\n\n    def _get_simple_attribute_set_cmd(\n        self,\n        attr_name: str,\n    ) -> Optional[AlterObjectProperty]:\n        return self._attrs.get(attr_name)\n\n    def _get_attribute_set_cmd(\n        self,\n        attr_name: str,\n    ) -> Optional[AlterObjectProperty]:\n        cmd = self._get_simple_attribute_set_cmd(attr_name)\n        if cmd is None:\n            special_cmd = self._special_attrs.get(attr_name)\n            if special_cmd is not None:\n                cmd = special_cmd._get_attribute_set_cmd(attr_name)\n        return cmd\n\n    def get_attribute_value(\n        self,\n        attr_name: str,\n    ) -> Any:\n        op = self._get_attribute_set_cmd(attr_name)\n        if op is not None:\n            return op.new_value\n        else:\n            return None\n\n    def get_local_attribute_value(\n        self,\n        attr_name: str,\n    ) -> Any:\n        \"\"\"Return the new value of field, if not inherited.\"\"\"\n        op = self._get_attribute_set_cmd(attr_name)\n        if op is not None and not op.new_inherited:\n            return op.new_value\n        else:\n            return None\n\n    def get_orig_attribute_value(\n        self,\n        attr_name: str,\n    ) -> Any:\n        op = self._get_attribute_set_cmd(attr_name)\n        if op is not None:\n            return op.old_value\n        else:\n            return None\n\n    def is_attribute_inherited(\n        self,\n        attr_name: str,\n    ) -> bool:\n        op = self._get_attribute_set_cmd(attr_name)\n        if op is not None:\n            return op.new_inherited\n        else:\n            return False\n\n    def is_attribute_computed(\n        self,\n        attr_name: str,\n    ) -> bool:\n        op = self._get_attribute_set_cmd(attr_name)\n        if op is not None:\n            return op.new_computed\n        else:\n            return False\n\n    def get_attribute_span(\n        self,\n        attr_name: str,\n    ) -> Optional[parsing.Span]:\n        op = self._get_attribute_set_cmd(attr_name)\n        if op is not None:\n            return op.span\n        else:\n            return None\n\n    def set_attribute_value(\n        self,\n        attr_name: str,\n        value: Any,\n        *,\n        orig_value: Any = None,\n        inherited: bool = False,\n        orig_inherited: Optional[bool] = None,\n        computed: bool = False,\n        from_default: bool = False,\n        orig_computed: Optional[bool] = None,\n        span: Optional[parsing.Span] = None,\n    ) -> Command:\n        orig_op = op = self._get_simple_attribute_set_cmd(attr_name)\n        if op is None:\n            op = AlterObjectProperty(property=attr_name, new_value=value)\n        else:\n            op.new_value = value\n\n        if orig_inherited is None:\n            orig_inherited = inherited\n        op.new_inherited = inherited\n        op.old_inherited = orig_inherited\n\n        if orig_computed is None:\n            orig_computed = computed\n        op.new_computed = computed\n        op.old_computed = orig_computed\n        op.from_default = from_default\n\n        if span is not None:\n            op.span = span\n        if orig_value is not None:\n            op.old_value = orig_value\n\n        if orig_op is None:\n            self.add(op)\n\n        return op\n\n    def discard_attribute(self, attr_name: str) -> None:\n        op = self._get_attribute_set_cmd(attr_name)\n        if op is not None:\n            self.discard(op)\n\n    def __iter__(self) -> NoReturn:\n        raise TypeError(f'{type(self)} object is not iterable')\n\n    @overload\n    def get_subcommands(\n        self,\n        *,\n        type: type[Command_T],\n        metaclass: Optional[type[so.Object]] = None,\n        exclude: type[Command] | tuple[type[Command], ...] | None = None,\n        include_prerequisites: bool = True,\n        include_caused: bool = True,\n    ) -> tuple[Command_T, ...]:\n        ...\n\n    @overload\n    def get_subcommands(\n        self,\n        *,\n        type: None = None,\n        metaclass: Optional[type[so.Object]] = None,\n        exclude: type[Command] | tuple[type[Command], ...] | None = None,\n        include_prerequisites: bool = True,\n        include_caused: bool = True,\n    ) -> tuple[Command, ...]:\n        ...\n\n    def get_subcommands(\n        self,\n        *,\n        type: type[Command_T] | None = None,\n        metaclass: Optional[type[so.Object]] = None,\n        exclude: type[Command] | tuple[type[Command], ...] | None = None,\n        include_prerequisites: bool = True,\n        include_caused: bool = True,\n    ) -> tuple[Command, ...]:\n        ops: Iterable[Command] = self.ops\n        if include_prerequisites:\n            ops = itertools.chain(self.before_ops, ops)\n\n        if include_caused:\n            ops = itertools.chain(ops, self.caused_ops)\n\n        filters = []\n\n        if type is not None:\n            t = type\n            filters.append(lambda i: isinstance(i, t))\n\n        if exclude is not None:\n            ex = exclude\n            filters.append(lambda i: not isinstance(i, ex))\n\n        if metaclass is not None:\n            mcls = metaclass\n            filters.append(\n                lambda i: (\n                    isinstance(i, ObjectCommand)\n                    and issubclass(i.get_schema_metaclass(), mcls)\n                )\n            )\n\n        if filters:\n            return tuple(filter(lambda i: all(f(i) for f in filters), ops))\n        else:\n            return tuple(ops)\n\n    @overload\n    def get_prerequisites(\n        self,\n        *,\n        type: type[Command_T],\n    ) -> tuple[Command_T, ...]:\n        ...\n\n    @overload\n    def get_prerequisites(\n        self,\n        *,\n        type: None = None,\n    ) -> tuple[Command, ...]:\n        ...\n\n    def get_prerequisites(\n        self,\n        *,\n        type: type[Command_T] | None = None,\n    ) -> tuple[Command, ...]:\n        if type is not None:\n            t = type\n            return tuple(filter(lambda i: isinstance(i, t), self.before_ops))\n        else:\n            return tuple(self.before_ops)\n\n    @overload\n    def get_caused(\n        self,\n        *,\n        type: type[Command_T],\n    ) -> tuple[Command_T, ...]:\n        ...\n\n    @overload\n    def get_caused(\n        self,\n        *,\n        type: None = None,\n    ) -> tuple[Command, ...]:\n        ...\n\n    def get_caused(\n        self,\n        *,\n        type: type[Command_T] | None = None,\n    ) -> tuple[Command, ...]:\n        if type is not None:\n            t = type\n            return tuple(filter(lambda i: isinstance(i, t), self.caused_ops))\n        else:\n            return tuple(self.caused_ops)\n\n    def has_subcommands(self) -> bool:\n        return bool(self.ops) or bool(self.before_ops) or bool(self.caused_ops)\n\n    def get_nonattr_subcommand_count(self) -> int:\n        attr_cmds = (AlterObjectProperty, AlterSpecialObjectField)\n        return len(self.get_subcommands(exclude=attr_cmds))\n\n    def get_nonattr_special_subcommand_count(self) -> int:\n        attr_cmds = (AlterObjectProperty,)\n        return len(self.get_subcommands(exclude=attr_cmds))\n\n    def prepend_prerequisite(self, command: Command) -> None:\n        if isinstance(command, CommandGroup):\n            for op in reversed(command.get_subcommands()):\n                self.prepend_prerequisite(op)\n        else:\n            self.before_ops.insert(0, command)\n\n    def add_prerequisite(self, command: Command) -> None:\n        if isinstance(command, CommandGroup):\n            self.before_ops.extend(command.get_subcommands())\n        else:\n            self.before_ops.append(command)\n\n    def prepend_caused(self, command: Command) -> None:\n        if isinstance(command, CommandGroup):\n            for op in reversed(command.get_subcommands()):\n                self.prepend_caused(op)\n        else:\n            self.caused_ops.insert(0, command)\n\n    def add_caused(self, command: Command) -> None:\n        if isinstance(command, CommandGroup):\n            self.caused_ops.extend(command.get_subcommands())\n        else:\n            self.caused_ops.append(command)\n\n    def prepend(self, command: Command) -> None:\n        if isinstance(command, CommandGroup):\n            for op in reversed(command.get_subcommands()):\n                self.prepend(op)\n        else:\n            if isinstance(command, AlterObjectProperty):\n                self._attrs[command.property] = command\n            elif isinstance(command, AlterSpecialObjectField):\n                self._special_attrs[command._field] = command\n            self.ops.insert(0, command)\n\n    def add(self, command: Command) -> None:\n        if isinstance(command, CommandGroup):\n            self.update(command.get_subcommands())\n        else:\n            if isinstance(command, AlterObjectProperty):\n                self._attrs[command.property] = command\n            elif isinstance(command, AlterSpecialObjectField):\n                self._special_attrs[command._field] = command\n            self.ops.append(command)\n\n    def update(self, commands: Iterable[Command]) -> None:\n        for command in commands:\n            self.add(command)\n\n    def replace(self, existing: Command, new: Command) -> None:  # type: ignore\n        try:\n            i = self.ops.index(existing)\n            self.ops[i] = new\n            return\n        except ValueError:\n            pass\n        try:\n            i = self.before_ops.index(existing)\n            self.before_ops[i] = new\n            return\n        except ValueError:\n            pass\n        i = self.caused_ops.index(existing)\n        self.caused_ops[i] = new\n\n    def replace_all(self, commands: Iterable[Command]) -> None:\n        self.ops.clear()\n        self._attrs.clear()\n        self._special_attrs.clear()\n        self.update(commands)\n\n    def discard(self, command: Command) -> None:\n        try:\n            self.ops.remove(command)\n        except ValueError:\n            pass\n        try:\n            self.before_ops.remove(command)\n        except ValueError:\n            pass\n        try:\n            self.caused_ops.remove(command)\n        except ValueError:\n            pass\n        if isinstance(command, AlterObjectProperty):\n            self._attrs.pop(command.property)\n        elif isinstance(command, AlterSpecialObjectField):\n            self._special_attrs.pop(command._field)\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        return schema\n\n    def apply_prerequisites(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        for op in self.get_prerequisites():\n            schema = op.apply(schema, context)\n        return schema\n\n    def apply_subcommands(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        for op in self.get_subcommands(\n            include_prerequisites=False,\n            include_caused=False,\n        ):\n            if not isinstance(op, AlterObjectProperty):\n                schema = op.apply(schema, context=context)\n        return schema\n\n    def apply_caused(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        for op in self.get_caused():\n            schema = op.apply(schema, context)\n        return schema\n\n    def get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        context_class = type(self).get_context_class()\n        assert context_class is not None\n        with context(context_class(schema=schema, op=self)):\n            return self._get_ast(schema, context, parent_node=parent_node)\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        raise NotImplementedError\n\n    def _log_all_renames(self, context: CommandContext) -> None:\n        if isinstance(self, RenameObject):\n            context.early_renames[self.classname] = self.new_name\n\n        for subcmd in self.get_subcommands():\n            subcmd._log_all_renames(context)\n\n    @classmethod\n    def get_orig_expr_text(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        name: str,\n    ) -> Optional[str]:\n        orig_text_expr = qlast.get_ddl_field_value(astnode, f'orig_{name}')\n        if orig_text_expr:\n            orig_text = qlcompiler.evaluate_ast_to_python_val(\n                orig_text_expr, schema=schema)\n        else:\n            orig_text = None\n\n        return orig_text  # type: ignore\n\n    @classmethod\n    def command_for_ast_node(\n        cls,\n        astnode: qlast.DDLOperation,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> type[Command]:\n        return cls\n\n    @classmethod\n    def _modaliases_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: CommandContext,\n    ) -> dict[Optional[str], str]:\n        modaliases = {}\n        if isinstance(astnode, qlast.DDLCommand) and astnode.aliases:\n            for alias in astnode.aliases:\n                if isinstance(alias, qlast.ModuleAliasDecl):\n                    modaliases[alias.alias] = alias.module\n\n        return modaliases\n\n    @classmethod\n    def localnames_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: CommandContext,\n    ) -> set[str]:\n        localnames: set[str] = set()\n        if isinstance(astnode, qlast.DDLCommand) and astnode.aliases:\n            for alias in astnode.aliases:\n                if isinstance(alias, qlast.AliasedExpr):\n                    localnames.add(alias.alias)\n\n        return localnames\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: CommandContext,\n    ) -> Command:\n        cmd = cls._cmd_from_ast(schema, astnode, context)\n        cmd.span = astnode.span\n        cmd.qlast = astnode\n        ctx = context.current()\n        if ctx is not None and type(ctx) is cls.get_context_class():\n            ctx.op = cmd\n\n        if astnode.commands:\n            for subastnode in astnode.commands:\n                subcmd = compile_ddl(schema, subastnode, context=context)\n                if subcmd is not None:\n                    cmd.add(subcmd)\n\n        return cmd\n\n    @classmethod\n    def _cmd_from_ast(\n        cls: type[Command_T],\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: CommandContext,\n    ) -> Command:\n        return cls()\n\n    @classmethod\n    def as_markup(cls, self: Command, *, ctx: markup.Context) -> markup.Markup:\n        node = markup.elements.lang.TreeNode(name=str(self))\n\n        def _markup(dd: Command) -> None:\n            if isinstance(dd, AlterObjectProperty):\n                diff = markup.elements.doc.ValueDiff(\n                    before=repr(dd.old_value), after=repr(dd.new_value))\n\n                if dd.new_inherited:\n                    diff.comment = 'inherited'\n                elif dd.new_computed:\n                    diff.comment = 'computed'\n\n                node.add_child(label=dd.property, node=diff)\n            else:\n                node.add_child(node=markup.serialize(dd, ctx=ctx))\n\n        prereqs = self.get_prerequisites()\n        caused = self.get_caused()\n\n        if prereqs:\n            node.add_child(\n                node=markup.elements.doc.Marker(text='prerequsites'))\n            for dd in prereqs:\n                _markup(dd)\n        if subs := self.get_subcommands(\n            include_prerequisites=False,\n            include_caused=False,\n        ):\n            # Only label regular subcommands if there are prereqs or\n            # caused actions, and so there is room for confusion\n            if prereqs or caused:\n                node.add_child(node=markup.elements.doc.Marker(text='main'))\n            for dd in subs:\n                _markup(dd)\n        if caused:\n            node.add_child(node=markup.elements.doc.Marker(text='caused'))\n            for dd in caused:\n                _markup(dd)\n\n        return node\n\n    @classmethod\n    def get_context_class(\n        cls: type[Command_T],\n    ) -> Optional[type[CommandContextToken[Command_T]]]:\n        return cls._context_class  # type: ignore\n\n    @classmethod\n    def get_context_class_or_die(\n        cls: type[Command_T],\n    ) -> type[CommandContextToken[Command_T]]:\n        ctxcls = cls.get_context_class()\n        if ctxcls is None:\n            raise RuntimeError(f'context class not defined for {cls}')\n        return ctxcls\n\n    def formatfields(\n        self,\n        formatter: str = 'str',\n    ) -> Iterator[tuple[str, str]]:\n        \"\"\"Return an iterator over fields formatted using `formatter`.\"\"\"\n        for name, field in self.__class__._fields.items():\n            value = getattr(self, name)\n            default = field.default\n            formatter_obj = field.formatters.get(formatter)\n            if formatter_obj and value != default:\n                yield (name, formatter_obj(value))\n\n\nclass Nop(Command):\n    pass\n\n\n# Similarly to _dummy_object, we use _dummy_command for places where\n# the typing requires an object, but we don't have it just yet.\n_dummy_command = Command()\n\n\nCommandList = checked.CheckedList[Command]\n\n\nclass CommandGroup(Command):\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        schema = self.apply_prerequisites(schema, context)\n        schema = self.apply_subcommands(schema, context)\n        schema = self.apply_caused(schema, context)\n        return schema\n\n\nclass CommandContextToken(Generic[Command_T_co]):  # noqa: UP046\n    original_schema: s_schema.Schema\n    op: Command_T_co\n    modaliases: Mapping[Optional[str], str]\n    localnames: AbstractSet[str]\n    inheritance_merge: Optional[bool]\n    inheritance_refdicts: Optional[AbstractSet[str]]\n    mark_derived: Optional[bool]\n    enable_recursion: Optional[bool]\n    transient_derivation: Optional[bool]\n    # Whether to skip creating @source/@target properties on links.\n    # Typically this is set whenever transient_derivation is,\n    # (so that it doesn't get set on transiet views, etc),\n    # except when compiling aliases where we need to produce\n    # fully populated links.\n    # This is a surprisingly valuable optimization (25% on a big schema).\n    slim_links: Optional[bool]\n\n    def __init__(\n        self,\n        schema: s_schema.Schema,\n        op: Command_T_co,\n        *,\n        modaliases: Optional[Mapping[Optional[str], str]] = None,\n        # localnames are the names defined locally via with block or\n        # as function parameters and should not be fully-qualified\n        localnames: AbstractSet[str] = frozenset(),\n    ) -> None:\n        self.original_schema = schema\n        self.op = op\n        self.modaliases = modaliases if modaliases is not None else {}\n        self.localnames = localnames\n        self.inheritance_merge = None\n        self.inheritance_refdicts = None\n        self.mark_derived = None\n        self.enable_recursion = None\n        self.transient_derivation = None\n        self.slim_links = None\n\n\nclass CommandContextWrapper(Generic[Command_T_co]):  # noqa: UP046\n    def __init__(\n        self,\n        context: CommandContext,\n        token: CommandContextToken[Command_T_co],\n    ) -> None:\n        self.context = context\n        self.token = token\n\n    def __enter__(self) -> CommandContextToken[Command_T_co]:\n        self.context.push(self.token)\n        return self.token\n\n    def __exit__(\n        self,\n        exc_type: type[Exception],\n        exc_value: Exception,\n        traceback: Any,\n    ) -> None:\n        self.context.pop()\n\n\nclass CommandContext:\n    def __init__(\n        self,\n        *,\n        schema: Optional[s_schema.Schema] = None,\n        modaliases: Optional[Mapping[Optional[str], str]] = None,\n        localnames: AbstractSet[str] = frozenset(),\n        declarative: bool = False,\n        stdmode: bool = False,\n        testmode: bool = False,\n        internal_schema_mode: bool = False,\n        disable_dep_verification: bool = False,\n        store_migration_sdl: bool = False,\n        descriptive_mode: bool = False,\n        schema_object_ids: Optional[\n            Mapping[tuple[sn.Name, Optional[str]], uuid.UUID]\n        ] = None,\n        backend_runtime_params: Optional[Any] = None,\n        compat_ver: Optional[verutils.Version] = None,\n        include_ext_version: bool = True,\n    ) -> None:\n        self.stack: list[CommandContextToken[Command]] = []\n        self._cache: dict[Hashable, Any] = {}\n        self._values: dict[Hashable, Any] = {}\n        self.declarative = declarative\n        self.schema = schema\n        self._modaliases = modaliases if modaliases is not None else {}\n        self._localnames = localnames\n        self.stdmode = stdmode\n        self.stable_ids = stdmode\n        self.internal_schema_mode = internal_schema_mode\n        self.testmode = testmode\n        self.descriptive_mode = descriptive_mode\n        self.disable_dep_verification = disable_dep_verification\n        self.store_migration_sdl = store_migration_sdl\n        self.renames: dict[sn.Name, sn.Name] = {}\n        self.early_renames: dict[sn.Name, sn.Name] = {}\n        self.renamed_objs: set[so.Object] = set()\n        self.change_log: dict[tuple[type[so.Object], str], set[so.Object]] = (\n            collections.defaultdict(set))\n        self.schema_object_ids = schema_object_ids\n        self.backend_runtime_params = backend_runtime_params\n        self.affected_finalization: dict[\n            Command,\n            list[tuple[Command, AlterObject[so.Object], list[str]]],\n        ] = collections.defaultdict(list)\n        self.compat_ver = compat_ver\n        self.include_ext_version = include_ext_version\n\n    @property\n    def modaliases(self) -> Mapping[Optional[str], str]:\n        maps = [t.modaliases for t in reversed(self.stack)]\n        maps.append(self._modaliases)\n        return collections.ChainMap(*maps)  # type: ignore\n\n    @property\n    def localnames(self) -> set[str]:\n        ign: set[str] = set()\n        for ctx in reversed(self.stack):\n            ign.update(ctx.localnames)\n        ign.update(self._localnames)\n        return ign\n\n    @property\n    def inheritance_merge(self) -> Optional[bool]:\n        for ctx in reversed(self.stack):\n            if ctx.inheritance_merge is not None:\n                return ctx.inheritance_merge\n        return None\n\n    @property\n    def mark_derived(self) -> Optional[bool]:\n        for ctx in reversed(self.stack):\n            if ctx.mark_derived is not None:\n                return ctx.mark_derived\n        return None\n\n    @property\n    def inheritance_refdicts(self) -> Optional[AbstractSet[str]]:\n        for ctx in reversed(self.stack):\n            if ctx.inheritance_refdicts is not None:\n                return ctx.inheritance_refdicts\n        return None\n\n    @property\n    def enable_recursion(self) -> bool:\n        for ctx in reversed(self.stack):\n            if ctx.enable_recursion is not None:\n                return ctx.enable_recursion\n\n        return True\n\n    @property\n    def transient_derivation(self) -> bool:\n        for ctx in reversed(self.stack):\n            if ctx.transient_derivation is not None:\n                return ctx.transient_derivation\n\n        return False\n\n    @property\n    def slim_links(self) -> bool:\n        return any(ctx.slim_links for ctx in self.stack)\n\n    @property\n    def canonical(self) -> bool:\n        return any(ctx.op.canonical for ctx in self.stack)\n\n    def in_deletion(self, offset: int = 0) -> bool:\n        \"\"\"Return True if any object is being deleted in this context.\n\n        :param offset:\n            The offset in the context stack to start looking at.\n\n        :returns:\n            True if any object is being deleted in this context starting\n            from *offset* in the stack.\n        \"\"\"\n        return any(isinstance(ctx.op, DeleteObject)\n                   for ctx in self.stack[:-offset if offset else None])\n\n    def is_deleting(self, obj: so.Object) -> bool:\n        \"\"\"Return True if *obj* is being deleted in this context.\n\n        :param obj:\n            The object in question.\n\n        :returns:\n            True if *obj* is being deleted in this context.\n        \"\"\"\n        return any(isinstance(ctx.op, DeleteObject)\n                   and ctx.op.scls == obj for ctx in self.stack)\n\n    def is_creating(self, obj: so.Object) -> bool:\n        \"\"\"Return True if *obj* is being created in this context.\n\n        :param obj:\n            The object in question.\n\n        :returns:\n            True if *obj* is being created in this context.\n        \"\"\"\n        return any(isinstance(ctx.op, CreateObject)\n                   and getattr(ctx.op, 'scls', None) == obj\n                   for ctx in self.stack)\n\n    def is_altering(self, obj: so.Object) -> bool:\n        \"\"\"Return True if *obj* is being altered in this context.\n\n        :param obj:\n            The object in question.\n\n        :returns:\n            True if *obj* is being altered in this context.\n        \"\"\"\n        return any(isinstance(ctx.op, AlterObject)\n                   and getattr(ctx.op, 'scls', None) == obj\n                   for ctx in self.stack)\n\n    def push(self, token: CommandContextToken[Command]) -> None:\n        self.stack.append(token)\n\n    def pop(self) -> CommandContextToken[Command]:\n        return self.stack.pop()\n\n    def get_referrer_name(\n        self,\n        referrer_ctx: CommandContextToken[ObjectCommand[so.Object]],\n    ) -> sn.QualName:\n        referrer_name = referrer_ctx.op.classname\n        renamed = self.early_renames.get(referrer_name)\n        if renamed:\n            referrer_name = renamed\n        else:\n            renamed = self.renames.get(referrer_name)\n            if renamed:\n                referrer_name = renamed\n        assert isinstance(referrer_name, sn.QualName)\n        return referrer_name\n\n    @overload\n    def get(\n        self,\n        cls: type[ObjectCommandContext[so.Object_T]],\n    ) -> Optional[ObjectCommandContext[so.Object_T]]:\n        ...\n\n    @overload\n    def get(\n        self,\n        cls: type[Command_T] | type[CommandContextToken[Command_T]],\n    ) -> Optional[CommandContextToken[Command_T]]:\n        ...\n\n    def get(\n        self,\n        cls: type[Command_T] | type[CommandContextToken[Command_T]],\n    ) -> Optional[CommandContextToken[Command_T]]:\n        ctxcls: Any\n        if issubclass(cls, Command):\n            ctxcls = cls.get_context_class()\n            assert ctxcls is not None\n        else:\n            ctxcls = cls\n\n        for item in reversed(self.stack):\n            if isinstance(item, ctxcls):\n                return item  # type: ignore\n\n        return None\n\n    def get_ancestor(\n        self,\n        cls: type[Command] | type[CommandContextToken[Command]],\n        op: Optional[Command] = None,\n    ) -> Optional[CommandContextToken[Command]]:\n        if issubclass(cls, Command):\n            ctxcls = cls.get_context_class()\n            assert ctxcls is not None\n        else:\n            ctxcls = cls\n\n        if op is not None:\n            for item in list(reversed(self.stack)):\n                if isinstance(item, ctxcls) and item.op is not op:\n                    return item\n        else:\n            for item in list(reversed(self.stack))[1:]:\n                if isinstance(item, ctxcls):\n                    return item\n\n        return None\n\n    def get_topmost_ancestor(\n        self,\n        cls: type[Command] | type[CommandContextToken[Command]],\n    ) -> Optional[CommandContextToken[Command]]:\n        if issubclass(cls, Command):\n            ctxcls = cls.get_context_class()\n            assert ctxcls is not None\n        else:\n            ctxcls = cls\n\n        for item in self.stack:\n            if isinstance(item, ctxcls):\n                return item\n\n        return None\n\n    def top(self) -> CommandContextToken[Command]:\n        if self.stack:\n            return self.stack[0]\n        else:\n            raise KeyError('command context stack is empty')\n\n    def current(self) -> CommandContextToken[Command]:\n        if self.stack:\n            return self.stack[-1]\n        else:\n            raise KeyError('command context stack is empty')\n\n    def parent(self) -> Optional[CommandContextToken[Command]]:\n        if len(self.stack) > 1:\n            return self.stack[-2]\n        else:\n            return None\n\n    def copy(self) -> CommandContext:\n        ctx = CommandContext()\n        ctx.stack = self.stack[:]\n        return ctx\n\n    def cache_value(self, key: Hashable, value: Any) -> None:\n        self._cache[key] = value\n\n    def get_cached(self, key: Hashable) -> Any:\n        return self._cache.get(key)\n\n    def drop_cache(self, key: Hashable) -> None:\n        self._cache.pop(key, None)\n\n    def store_value(self, key: Hashable, value: Any) -> None:\n        self._values[key] = value\n\n    def get_value(self, key: Hashable) -> Any:\n        return self._values.get(key)\n\n    @contextlib.contextmanager\n    def suspend_dep_verification(self) -> Iterator[CommandContext]:\n        dep_ver = self.disable_dep_verification\n        self.disable_dep_verification = True\n        try:\n            yield self\n        finally:\n            self.disable_dep_verification = dep_ver\n\n    def __call__(\n        self,\n        token: CommandContextToken[Command_T],\n    ) -> CommandContextWrapper[Command_T]:\n        return CommandContextWrapper(self, token)\n\n    def compat_ver_is_before(\n        self,\n        ver: tuple[int, int, verutils.VersionStage, int],\n    ) -> bool:\n        return self.compat_ver is not None and self.compat_ver < ver\n\n\nclass ContextStack:\n\n    def __init__(\n        self,\n        contexts: Iterable[CommandContextWrapper[Command]],\n    ) -> None:\n        self._contexts = list(contexts)\n\n    def push(self, ctx: CommandContextWrapper[Command]) -> None:\n        self._contexts.append(ctx)\n\n    def pop(self) -> None:\n        self._contexts.pop()\n\n    @contextlib.contextmanager\n    def __call__(self) -> Generator[None, None, None]:\n        with contextlib.ExitStack() as stack:\n            for ctx in self._contexts:\n                stack.enter_context(ctx)  # type: ignore\n            yield\n\n\nclass DeltaRootContext(CommandContextToken[\"DeltaRoot\"]):\n    pass\n\n\nclass DeltaRoot(CommandGroup, context_class=DeltaRootContext):\n\n    def __init__(self, **kwargs: Any) -> None:\n        super().__init__(**kwargs)\n        self.new_types: set[uuid.UUID] = set()\n        self.warnings: list[errors.EdgeDBError] = []\n\n    @classmethod\n    def from_commands(cls, *cmds: Command) -> DeltaRoot:\n        delta = DeltaRoot()\n        delta.update(cmds)\n        return delta\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        with context(DeltaRootContext(schema=schema, op=self)):\n            schema = self.apply_prerequisites(schema, context)\n            schema = self.apply_subcommands(schema, context)\n            schema = self.apply_caused(schema, context)\n\n        return schema\n\n    def is_data_safe(self) -> bool:\n        return all(\n            subcmd.is_data_safe()\n            for subcmd in self.get_subcommands()\n        )\n\n\nclass Query(Command):\n    \"\"\"A special delta command representing a non-DDL query.\n\n    These are found in migrations.\n    \"\"\"\n\n    astnode = qlast.DDLQuery\n\n    expr = struct.Field(s_expr.Expression)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: CommandContext,\n    ) -> Command:\n        assert isinstance(astnode, qlast.DDLQuery)\n        return cls(\n            span=astnode.span,\n            expr=s_expr.Expression.from_ast(\n                astnode.query,\n                schema=schema,\n                modaliases=context.modaliases,\n                localnames=context.localnames,\n            ),\n        )\n\n    @classmethod\n    def as_markup(cls, self: Command, *, ctx: markup.Context) -> markup.Markup:\n        node = super().as_markup(self, ctx=ctx)\n        assert isinstance(node, markup.elements.lang.TreeNode)\n        assert isinstance(self, Query)\n        qltext = self.expr.text\n        node.add_child(node=markup.elements.lang.MultilineString(str=qltext))\n        return node\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n        if not self.expr.is_compiled():\n            self.expr = self.expr.compiled(\n                schema,\n                options=qlcompiler.CompilerOptions(\n                    modaliases=context.modaliases,\n                    apply_query_rewrites=False,\n                ),\n                context=context,\n            )\n        return schema\n\n\n_command_registry: dict[\n    tuple[str, type[so.Object]],\n    type[ObjectCommand[so.Object]]\n] = {}\n\n\ndef get_object_command_class[Command_T: Command](\n    cmdtype: type[Command_T],\n    schema_metaclass: type[so.Object],\n) -> Optional[type[Command_T]]:\n    assert issubclass(cmdtype, ObjectCommand)\n    return _command_registry.get(  # type: ignore\n        (cmdtype._delta_action, schema_metaclass),\n    )\n\n\ndef get_object_command_class_or_die[Command_T: Command](\n    cmdtype: type[Command_T],\n    schema_metaclass: type[so.Object],\n) -> type[Command_T]:\n    cmdcls = get_object_command_class(cmdtype, schema_metaclass)\n    if cmdcls is None:\n        raise TypeError(f'missing {cmdtype.__name__} implementation '\n                        f'for {schema_metaclass.__name__}')\n    return cmdcls\n\n\nclass ObjectCommand[Object_T: so.Object](Command):\n    \"\"\"Base class for all Object-related commands.\"\"\"\n\n    #: Full name of the object this command operates on.\n    classname = struct.Field(sn.Name)\n    #: An optional set of values neceessary to render the command in DDL.\n    ddl_identity = struct.Field(\n        dict,  # type: ignore\n        default=None,\n    )\n    #: An optional dict of metadata annotations for this command.\n    annotations = struct.Field(\n        dict,  # type: ignore\n        default=None,\n    )\n    #: Auxiliary object information that might be necessary to process\n    #: this command, derived from object fields.\n    aux_object_data = struct.Field(\n        dict,  # type: ignore\n        default=None,\n    )\n    #: When this command is produced by a breakup of a larger command\n    #: subtree, *orig_cmd_type* would contain the type of the original\n    #: command.\n    orig_cmd_type = struct.Field(\n        CommandMeta,\n        default=None,\n    )\n\n    #: Is this from an expression change being propagated.\n    #: FIXME: Every place this is used is a hack and some are bugs.\n    from_expr_propagation = struct.Field(bool, default=False)\n\n    scls: Object_T\n    _delta_action: ClassVar[str]\n    _schema_metaclass: ClassVar[  # type: ignore\n        Optional[type[Object_T]]\n    ] = None\n    astnode: ClassVar[type[qlast.DDLOperation] | list[type[qlast.DDLOperation]]]\n\n    def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:\n        # Check if the command subclass has been parametrized with\n        # a concrete schema object class, and if so, record the\n        # argument to be made available via get_schema_metaclass().\n        super().__init_subclass__(*args, **kwargs)\n        generic_bases = typing_inspect.get_generic_bases(cls)\n        mcls: Optional[type[so.Object]] = None\n        for gb in generic_bases:\n            base_origin = typing_inspect.get_origin(gb)\n            # Find the <ObjectCommand>[Type] base, where ObjectCommand\n            # is any ObjectCommand subclass.\n            if (\n                base_origin is not None\n                and issubclass(base_origin, ObjectCommand)\n            ):\n                args = typing_inspect.get_args(gb)\n                if len(args) != 1:\n                    raise AssertionError(\n                        'expected only one argument to ObjectCommand generic')\n                arg_0 = args[0]\n                if not typing_inspect.is_typevar(arg_0):\n                    assert issubclass(arg_0, so.Object)\n                    if not arg_0.is_abstract():\n                        mcls = arg_0\n                        break\n\n        if mcls is not None:\n            existing = getattr(cls, '_schema_metaclass', None)\n            if existing is not None and existing is not mcls:\n                raise TypeError(\n                    f'cannot redefine schema class of {cls.__name__} to '\n                    f'{mcls.__name__}: a superclass has already defined it as '\n                    f'{existing.__name__}'\n                )\n            cls._schema_metaclass = mcls\n\n        # If this is a command adapter rather than the actual\n        # command, skip the command class registration.\n        if not cls.has_adaptee():\n            delta_action = getattr(cls, '_delta_action', None)\n            schema_metaclass = getattr(cls, '_schema_metaclass', None)\n            if schema_metaclass is not None and delta_action is not None:\n                key = delta_action, schema_metaclass\n                cmdcls = _command_registry.get(key)\n                if cmdcls is not None:\n                    raise TypeError(\n                        f'Action {cls._delta_action!r} for '\n                        f'{schema_metaclass} is already claimed by {cmdcls}'\n                    )\n                _command_registry[key] = cls  # type: ignore\n\n    @classmethod\n    def _classname_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: CommandContext,\n    ) -> sn.Name:\n        return sn.UnqualName(astnode.name.name)\n\n    @classmethod\n    def _cmd_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: CommandContext,\n    ) -> ObjectCommand[Object_T]:\n        assert isinstance(astnode, qlast.ObjectDDL), 'expected ObjectDDL'\n        classname = cls._classname_from_ast(schema, astnode, context)\n        return cls(classname=classname)\n\n    def is_data_safe(self) -> bool:\n        if self.get_schema_metaclass()._data_safe:\n            return True\n        else:\n            return all(\n                subcmd.is_data_safe()\n                for subcmd in self.get_subcommands()\n            )\n\n    def get_required_user_input(self) -> list[dict[str, str]]:\n        result: list[dict[str, str]] = []\n        if ann := self.get_annotation('required_input'):\n            result.append(ann)\n        for cmd in self.get_subcommands():\n            result.extend(cmd.get_required_user_input())\n        return result\n\n    def get_friendly_description(\n        self,\n        *,\n        parent_op: Optional[Command] = None,\n        schema: Optional[s_schema.Schema] = None,\n        object: Any = None,\n        object_desc: Optional[str] = None,\n    ) -> str:\n        \"\"\"Return a friendly description of this command in imperative mood.\n\n        The result is used in error messages and other user-facing renderings\n        of the command.\n        \"\"\"\n        object_desc = self.get_friendly_object_name_for_description(\n            parent_op=parent_op,\n            schema=schema,\n            object=object,\n            object_desc=object_desc,\n        )\n        return f'{self.get_verb()} {object_desc}'\n\n    def get_user_prompt(\n        self,\n        *,\n        parent_op: Optional[Command] = None,\n    ) -> tuple[CommandKey, str]:\n        \"\"\"Return a human-friendly prompt describing this operation.\"\"\"\n\n        # The prompt is determined by the *innermost* subcommand as\n        # long as all its parents have exactly one child.  The tree\n        # traversal stops on fragments and CreateObject commands,\n        # since there is no point to prompt about the creation of\n        # object innards.\n        if (\n            not isinstance(self, AlterObjectFragment)\n            and (\n                not isinstance(self, CreateObject)\n                and (\n                    self.orig_cmd_type is None\n                    or not issubclass(\n                        self.orig_cmd_type, CreateObject\n                    )\n                )\n            )\n        ):\n            from . import referencing as s_referencing\n\n            subcommands = self.get_subcommands(\n                type=ObjectCommand,\n                exclude=(AlterObjectProperty, s_referencing.AlterOwned),\n            )\n            if len(subcommands) == 1:\n                subcommand = subcommands[0]\n                if isinstance(subcommand, AlterObjectFragment):\n                    return subcommand.get_user_prompt(parent_op=parent_op)\n                else:\n                    return subcommand.get_user_prompt(parent_op=self)\n\n        desc = self.get_friendly_description(parent_op=parent_op)\n        prompt_text = f'did you {desc}?'\n        prompt_id = get_object_command_key(self)\n        assert prompt_id is not None\n        return prompt_id, prompt_text\n\n    def validate_object(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> None:\n        pass\n\n    @classmethod\n    def get_parent_op(\n        cls,\n        context: CommandContext,\n    ) -> ObjectCommand[so.Object]:\n        parent = context.parent()\n        if parent is None:\n            raise AssertionError(f'{cls!r} has no parent context')\n        op = parent.op\n        assert isinstance(op, ObjectCommand)\n        return op\n\n    @classmethod\n    @functools.lru_cache()\n    def _get_special_handler(\n        cls,\n        field_name: str,\n    ) -> Optional[type[AlterSpecialObjectField[so.Object]]]:\n        if (\n            issubclass(cls, AlterObjectOrFragment)\n            and not issubclass(cls, AlterSpecialObjectField)\n        ):\n            schema_cls = cls.get_schema_metaclass()\n            return get_special_field_alter_handler(field_name, schema_cls)\n        else:\n            return None\n\n    def set_attribute_value(\n        self,\n        attr_name: str,\n        value: Any,\n        *,\n        orig_value: Any = None,\n        inherited: bool = False,\n        orig_inherited: Optional[bool] = None,\n        computed: bool = False,\n        orig_computed: Optional[bool] = None,\n        from_default: bool = False,\n        span: Optional[parsing.Span] = None,\n        disallow_special: bool = False,\n    ) -> Command:\n        special = (\n            type(self)._get_special_handler(attr_name)\n            if not disallow_special else None\n        )\n        op = self._get_attribute_set_cmd(attr_name)\n        top_op: Optional[Command] = None\n\n        if orig_inherited is None:\n            orig_inherited = inherited\n\n        if orig_computed is None:\n            orig_computed = computed\n\n        if op is None:\n            op = AlterObjectProperty(\n                property=attr_name,\n                new_value=value,\n                old_value=orig_value,\n                new_inherited=inherited,\n                old_inherited=orig_inherited,\n                new_computed=computed,\n                old_computed=orig_computed,\n                from_default=from_default,\n                span=span,\n            )\n\n            top_op = self._special_attrs.get(attr_name)\n\n            if top_op is None and special is not None:\n                top_op = special(classname=self.classname)\n                self.add(top_op)\n\n            if top_op:\n                top_op.add(op)\n            else:\n                self.add(op)\n                top_op = op\n\n            return top_op\n        else:\n            op.new_value = value\n            op.new_inherited = inherited\n            op.old_inherited = orig_inherited\n\n            op.new_computed = computed\n            op.old_computed = orig_computed\n            op.from_default = from_default\n\n            if span is not None:\n                op.span = span\n            if orig_value is not None:\n                op.old_value = orig_value\n\n            return op\n\n    def _fix_referencing_expr_after_rename(\n        self,\n        schema: s_schema.Schema,\n        cmd: ObjectCommand[so.Object],\n        fn: str,\n        context: CommandContext,\n        expr: s_expr.Expression,\n    ) -> s_expr.Expression:\n        if isinstance(self, RenameObject):\n            new_name = self.new_name\n        elif (fops := self.get_subcommands(type=RenameObject)):\n            new_name = fops[0].new_name\n        else:\n            raise AssertionError(\"not a rename!\")\n\n        # Recompile the expression with reference tracking on so that we\n        # can clean up the ast.\n        field = cmd.get_schema_metaclass().get_field(fn)\n        compiled = cmd.compile_expr_field(\n            schema, context, field, expr,\n            track_schema_ref_exprs=True)\n        assert compiled.irast.schema_ref_exprs is not None\n\n        # Now that the compilation is done, try to do the fixup.\n        new_shortname = sn.shortname_from_fullname(new_name)\n        old_shortname = sn.shortname_from_fullname(self.classname).name\n        for ref in compiled.irast.schema_ref_exprs.get(self.scls, []):\n            assert isinstance(\n                ref,\n                (qlast.ObjectRef, qlast.FunctionCall, qlast.Ptr)\n            ), f\"only support object refs and func calls but got {ref}\"\n            if isinstance(ref, qlast.FunctionCall):\n                ref.func = ((new_shortname.module, new_shortname.name)\n                            if isinstance(new_shortname, sn.QualName)\n                            else new_shortname.name)\n            elif (\n                isinstance(ref, (qlast.Ptr, qlast.ObjectRef))\n                and ref.name == old_shortname\n            ):\n                ref.name = new_shortname.name\n                if (\n                    isinstance(new_shortname, sn.QualName)\n                    and isinstance(ref, qlast.ObjectRef)\n                    and new_shortname.module != \"__\"\n                ):\n                    ref.module = new_shortname.module\n\n        # say as_fragment=True as a hack to avoid renormalizing it\n        out = s_expr.Expression.from_ast(\n            compiled.parse(), schema, modaliases={}, as_fragment=True)\n        return out\n\n    def _propagate_if_expr_refs(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        action: str,\n        include_self: bool=True,\n        include_ancestors: bool=False,\n        extra_refs: Optional[dict[so.Object, list[str]]]=None,\n        filter: type[so.Object] | tuple[type[so.Object], ...] | None = None,\n        metadata_only: bool=False,\n    ) -> s_schema.Schema:\n\n        # If we are a rename or contain a rename, we need to fix up expressions\n        if (\n            isinstance(self, RenameObject)\n            or self.get_subcommands(type=RenameObject)\n        ):\n            fixer = self._fix_referencing_expr_after_rename\n        else:\n            fixer = None\n\n        scls = self.scls\n        expr_refs: dict[so.Object, list[str]] = {}\n        if include_self:\n            expr_refs.update(s_expr.get_expr_referrers(schema, scls))\n        if include_ancestors and isinstance(scls, so.InheritingObject):\n            for anc in scls.get_ancestors(schema).objects(schema):\n                expr_refs.update(s_expr.get_expr_referrers(schema, anc))\n        if extra_refs:\n            expr_refs.update(extra_refs)\n        if filter is not None:\n            expr_refs = {\n                k: v for k, v in expr_refs.items() if isinstance(k, filter)}\n\n        if expr_refs:\n            try:\n                sorted_ref_objs = sort_by_cross_refs(schema, expr_refs.keys())\n            except topological.CycleError as e:\n                assert e.item is not None\n\n                item_vn = e.item.get_verbosename(schema, with_parent=True)\n\n                if e.path:\n                    # Recursion involving more than one schema object.\n                    rec_vn = e.path[-1].get_verbosename(\n                        schema, with_parent=True)\n                    # Sort for output determinism\n                    vn1, vn2 = sorted([rec_vn, item_vn])\n                    msg = (\n                        f'definition dependency cycle between {vn1} and {vn2}'\n                    )\n                else:\n                    # A single schema object with a recursive definition.\n                    msg = f'{item_vn} is defined recursively'\n\n                raise errors.InvalidDefinitionError(msg) from e\n\n            ref_desc = []\n            for ref in sorted_ref_objs:\n                cmd_drop: Command\n                cmd_create: Command\n\n                fns = expr_refs[ref]\n                this_ref_desc = []\n                for fn in fns:\n                    if fn == 'expr':\n                        fdesc = 'expression'\n                    else:\n                        sfn = type(ref).get_field(fn).sname\n                        fdesc = f\"{sfn.replace('_', ' ')} expression\"\n\n                    vn = ref.get_verbosename(schema, with_parent=True)\n\n                    this_ref_desc.append(f'{fdesc} of {vn}')\n\n                # Alter the affected entity to change the body to\n                # a dummy version (removing the dependency) and\n                # then reset the body to original expression.\n                delta_drop, cmd_drop, _ = ref.init_delta_branch(\n                    schema, context, cmdtype=AlterObject)\n                delta_create, cmd_create, ctx_stack = ref.init_delta_branch(\n                    schema, context, cmdtype=AlterObject,\n                    possible_parent=self,  # type: ignore\n                )\n\n                cmd_drop.from_expr_propagation = True\n                cmd_create.from_expr_propagation = True\n\n                # Mark it metadata_only so that if it actually gets\n                # applied, only the metadata is changed but not\n                # the real underlying schema.\n                if metadata_only:\n                    cmd_drop.metadata_only = True\n                    cmd_create.metadata_only = True\n\n                # Treat the drop as canonical, since we only need\n                # to eliminate the reference, not get to a fully\n                # consistent state, and the canonicalization can\n                # mess up \"associated\" attributes.\n                cmd_drop.canonical = True\n\n                for fn, cur_ref_desc in zip(fns, this_ref_desc):\n                    value: s_expr.Expression | None = (\n                        ref.get_explicit_field_value(schema, fn, None))\n                    if value is None:\n                        continue\n\n                    try:\n                        # Compute a dummy value\n                        dummy = cmd_create.get_dummy_expr_field_value(\n                            schema,\n                            context,\n                            field=type(ref).get_field(fn),\n                            value=ref.get_field_value(schema, fn)\n                        )\n                    except NotImplementedError:\n                        ref_desc.append(cur_ref_desc)\n                    else:\n                        # Do the switcheroos\n                        # Strip the \"compiled\" out of the expression\n                        value = s_expr.Expression.not_compiled(value)\n                        # We don't run the fixer on inherited fields because\n                        # they can't have changed (and because running it\n                        # on inherited constraint finalexprs breaks\n                        # the extra parens in it...)\n                        if fixer and not ref.field_is_inherited(schema, fn):\n                            with ctx_stack():\n                                value = fixer(\n                                    schema, cmd_create, fn, context, value)\n\n                        cmd_drop.set_attribute_value(fn, dummy)\n                        cmd_create.set_attribute_value(\n                            fn,\n                            value,\n                            inherited=ref.field_is_inherited(schema, fn),\n                            computed=ref.field_is_computed(schema, fn),\n                        )\n\n                context.affected_finalization[self].append(\n                    (delta_create, cmd_create, this_ref_desc)\n                )\n                schema = delta_drop.apply(schema, context)\n\n            if ref_desc:\n                expr_s = (\n                    'an expression' if len(ref_desc) == 1 else 'expressions')\n                ref_desc_s = \"\\n - \" + \"\\n - \".join(ref_desc)\n\n                raise errors.SchemaDefinitionError(\n                    f'cannot {action} because it is used in {expr_s}',\n                    details=(\n                        f'{scls.get_verbosename(schema)} is used in:'\n                        f'{ref_desc_s}'\n                    )\n                )\n\n        return schema\n\n    def _finalize_affected_refs(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n\n        # There might be dependencies between the things we need to\n        # fix up (a computed property and a constraint on it, for\n        # example, requires us to fix up the computed property first),\n        # so sort by dependency order.\n        objs_to_cmds: dict[\n            so.Object, list[tuple[Command, AlterObject[so.Object], list[str]]]\n        ] = {}\n        for delta, cmd, refdesc in context.affected_finalization.get(self, []):\n            if schema.has_object(cmd.scls.id):\n                cmds = objs_to_cmds.setdefault(cmd.scls, [])\n                cmds.append((delta, cmd, refdesc))\n        objs = sort_by_cross_refs(schema, objs_to_cmds.keys())\n\n        for obj in reversed(objs):\n            for delta, cmd, refdesc in objs_to_cmds[obj]:\n                try:\n                    cmd.canonicalize_alter_from_external_ref(schema, context)\n                    schema = delta.apply(schema, context)\n\n                    if not context.canonical and delta:\n                        # We need to force the attributes to be resolved so\n                        # that expressions get compiled *now* under a schema\n                        # where they are correct, and not later, when more\n                        # renames may have broken them.\n                        assert isinstance(cmd, ObjectCommand)\n                        res_attrs = cmd.get_resolved_attributes(schema, context)\n                        for key, value in res_attrs.items():\n                            cmd.set_attribute_value(key, value)\n\n                        # HACK: Apply constraint of pointers in innards, because\n                        # when converting a pointer to a computed pointer,\n                        # constraints need to be adjusted before the column is\n                        # dropped. We cannot drop the column later because we\n                        # need mainain the ordering of drops of any children\n                        # pointers.\n                        from . import constraints as s_constraints\n\n                        if isinstance(cmd, s_constraints.ConstraintCommand):\n                            self.add(delta)\n                        else:\n                            # base case\n                            self.add_caused(delta)\n                except errors.QueryError as e:\n                    orig_schema = context.current().original_schema\n                    desc = self.get_friendly_description(schema=orig_schema)\n                    raise errors.SchemaDefinitionError(\n                        f'cannot {desc} because this affects'\n                        f' {\" and \".join(refdesc)}',\n                        details=e.args[0],\n                    ) from e\n\n        return schema\n\n    def _get_computed_status_of_fields(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> dict[str, bool]:\n        result = {}\n        mcls = self.get_schema_metaclass()\n        for op in self._enumerate_attribute_cmds():\n            field = mcls.get_field(op.property)\n            if not field.ephemeral:\n                result[op.property] = op.new_computed\n\n        return result\n\n    def _update_computed_fields(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        update: Mapping[str, bool],\n    ) -> None:\n        raise NotImplementedError\n\n    def _append_subcmd_ast(\n        self,\n        schema: s_schema.Schema,\n        node: qlast.DDLOperation,\n        subcmd: Command,\n        context: CommandContext,\n    ) -> None:\n        subnode = subcmd.get_ast(schema, context, parent_node=node)\n        if subnode is not None:\n            node.commands.append(subnode)\n\n    def _get_ast_node(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> type[qlast.DDLOperation]:\n        # TODO: how to handle the following type: ignore?\n        # in this class, astnode is always a Type[DDLOperation],\n        # but the current design of constraints handles it as\n        # a List[Type[DDLOperation]]\n        return type(self).astnode  # type: ignore\n\n    def _deparse_name(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        name: sn.Name,\n    ) -> qlast.ObjectRef:\n        qlclass = self.get_schema_metaclass().get_ql_class()\n\n        if isinstance(name, sn.QualName):\n            nname = sn.shortname_from_fullname(name)\n            assert isinstance(nname, sn.QualName), \\\n                \"expected qualified name\"\n            ref = qlast.ObjectRef(\n                module=nname.module, name=nname.name, itemclass=qlclass)\n        else:\n            ref = qlast.ObjectRef(module='', name=str(name), itemclass=qlclass)\n\n        return ref\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        astnode = self._get_ast_node(schema, context)\n\n        if astnode.get_field('name'):\n            # We need to be able to catch both renames of the object\n            # itself, which might have a long name (for pointers, for\n            # example) as well as an object being referenced by\n            # shortname, if this is (for example) a concrete\n            # constraint and the abstract constraint was renamed.\n            name = context.early_renames.get(self.classname, self.classname)\n            name = sn.shortname_from_fullname(name)\n            if self.classname not in context.early_renames:\n                name = context.early_renames.get(name, name)\n            op = astnode(  # type: ignore\n                name=self._deparse_name(schema, context, name),\n            )\n        else:\n            op = astnode()\n\n        self._apply_fields_ast(schema, context, op)\n\n        return op\n\n    def _apply_fields_ast(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        node: qlast.DDLOperation,\n    ) -> None:\n        mcls = self.get_schema_metaclass()\n\n        if not isinstance(self, DeleteObject):\n            fops = self.get_subcommands(type=AlterObjectProperty)\n            for fop in sorted(fops, key=lambda f: f.property):\n                field = mcls.get_field(fop.property)\n                if fop.new_value is not None:\n                    new_value = fop.new_value\n                else:\n                    new_value = field.get_default()\n\n                if (\n                    (\n                        # Only include fields that are not inherited\n                        # and that have their value actually changed.\n                        not fop.new_inherited\n                        or context.descriptive_mode\n                        or self.ast_ignore_ownership()\n                        or self.ast_ignore_field_ownership(fop.property)\n                    )\n                    and (\n                        fop.old_value != new_value\n                        or fop.old_inherited != fop.new_inherited\n                        or fop.old_computed != fop.new_computed\n                    )\n                ):\n                    self._apply_field_ast(schema, context, node, fop)\n\n        if not isinstance(self, AlterObjectFragment):\n            for field in self.get_ddl_identity_fields(context):\n                ast_attr = self.get_ast_attr_for_field(field.name, type(node))\n                if (\n                    ast_attr is not None\n                    and not getattr(node, ast_attr, None)\n                    and (\n                        field.required\n                        or self.has_ddl_identity(field.name)\n                    )\n                ):\n                    ddl_id = self.get_ddl_identity(field.name)\n                    attr_val: Any\n                    if issubclass(field.type, s_expr.Expression):\n                        assert isinstance(ddl_id, s_expr.Expression)\n                        attr_val = ddl_id.parse()\n                    elif issubclass(field.type, s_expr.ExpressionList):\n                        assert isinstance(ddl_id, s_expr.ExpressionList)\n                        attr_val = [e.parse() for e in ddl_id]\n                    elif issubclass(field.type, s_expr.ExpressionDict):\n                        assert isinstance(ddl_id, s_expr.ExpressionDict)\n                        attr_val = {\n                            name: e.parse()\n                            for name, e in ddl_id.items()\n                        }\n                    else:\n                        raise AssertionError(\n                            f'unexpected type of ddl_identity'\n                            f' field: {field.type!r}'\n                        )\n\n                    setattr(node, ast_attr, attr_val)\n\n            # Keep subcommands from refdicts and alter fragments (like\n            # rename, rebase) in order when producing DDL asts\n            refdicts = tuple(x.ref_cls for x in mcls.get_refdicts())\n            for op in self.get_subcommands():\n                if (\n                    isinstance(op, AlterObjectFragment)\n                    or (isinstance(op, ObjectCommand) and\n                        issubclass(op.get_schema_metaclass(), refdicts))\n                ):\n                    self._append_subcmd_ast(schema, node, op, context)\n\n        else:\n            for op in self.get_subcommands(type=AlterObjectFragment):\n                self._append_subcmd_ast(schema, node, op, context)\n\n        if isinstance(node, qlast.DropObject):\n            def _is_drop(ddl: qlast.DDLOperation) -> bool:\n                return (\n                    isinstance(ddl, (qlast.DropObject, qlast.AlterObject))\n                    and all(_is_drop(sub) for sub in ddl.commands)\n                )\n\n            # Deletes in the AST shouldn't have subcommands, so we\n            # drop them.  To try to make sure we aren't papering\n            # over bugs by dropping things we dont expect, make\n            # sure every subcommand was also a delete (or an alter\n            # containing only deletes)\n            assert all(_is_drop(sub) for sub in node.commands)\n            node.commands = []\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        node: qlast.DDLOperation,\n        op: AlterObjectProperty,\n    ) -> None:\n        if op.property != 'name':\n            subnode = op._get_ast(schema, context, parent_node=node)\n            if subnode is not None:\n                node.commands.append(subnode)\n\n    def get_ast_attr_for_field(\n        self,\n        field: str,\n        astnode: type[qlast.DDLOperation],\n    ) -> Optional[str]:\n        return None\n\n    def get_ddl_identity_fields(\n        self,\n        context: CommandContext,\n    ) -> tuple[so.Field[Any], ...]:\n        mcls = self.get_schema_metaclass()\n        return tuple(f for f in mcls.get_fields().values() if f.ddl_identity)\n\n    @classmethod\n    def maybe_get_schema_metaclass(cls) -> Optional[type[Object_T]]:\n        return cls._schema_metaclass\n\n    @classmethod\n    def get_schema_metaclass(cls) -> type[Object_T]:\n        if cls._schema_metaclass is None:\n            raise TypeError(f'schema metaclass not set for {cls}')\n        return cls._schema_metaclass\n\n    @classmethod\n    def get_other_command_class[ObjectCommand_T: ObjectCommand[so.Object]](\n        cls,\n        cmdtype: type[ObjectCommand_T],\n    ) -> type[ObjectCommand_T]:\n        mcls = cls.get_schema_metaclass()\n        return get_object_command_class_or_die(cmdtype, mcls)\n\n    def _validate_legal_command(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> None:\n        from . import functions as s_func\n\n        if (not context.stdmode and not context.testmode and\n                not isinstance(self, s_func.ParameterCommand)):\n\n            if (\n                isinstance(self.classname, sn.QualName)\n                and (modroot := self.classname.get_root_module_name())\n                and modroot in s_schema.STD_MODULES\n                and not (\n                    modroot == s_schema.EXT_MODULE\n                    and context.transient_derivation\n                )\n            ):\n                raise errors.SchemaDefinitionError(\n                    f'cannot {self._delta_action} {self.get_verbosename()}: '\n                    f'module {modroot} is read-only',\n                    span=self.span)\n\n    def get_verbosename(self, parent: Optional[str] = None) -> str:\n        mcls = self.get_schema_metaclass()\n        return mcls.get_verbosename_static(self.classname, parent=parent)\n\n    def get_displayname(self) -> str:\n        mcls = self.get_schema_metaclass()\n        return mcls.get_displayname_static(self.classname)\n\n    def get_friendly_object_name_for_description(\n        self,\n        *,\n        parent_op: Optional[Command] = None,\n        schema: Optional[s_schema.Schema] = None,\n        object: Optional[Object_T] = None,\n        object_desc: Optional[str] = None,\n    ) -> str:\n        if object_desc is not None:\n            return object_desc\n        else:\n            if object is None:\n                object = cast(\n                    Object_T,\n                    getattr(self, 'scls', cast(Object_T, _dummy_object)),\n                )\n\n            if object is _dummy_object or schema is None:\n                if not isinstance(parent_op, ObjectCommand):\n                    parent_desc = None\n                else:\n                    parent_desc = parent_op.get_verbosename()\n                object_desc = self.get_verbosename(parent=parent_desc)\n            else:\n                object_desc = object.get_verbosename(schema, with_parent=True)\n\n            return object_desc\n\n    @overload\n    def get_object(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        name: Optional[sn.Name] = None,\n        default: Object_T | so.NoDefaultT = so.NoDefault,\n        span: Optional[parsing.Span] = None,\n    ) -> Object_T:\n        ...\n\n    @overload\n    def get_object(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        name: Optional[sn.Name] = None,\n        default: None = None,\n        span: Optional[parsing.Span] = None,\n    ) -> Optional[Object_T]:\n        ...\n\n    def get_object(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        name: Optional[sn.Name] = None,\n        default: Object_T | so.NoDefaultT | None = so.NoDefault,\n        span: Optional[parsing.Span] = None,\n    ) -> Optional[Object_T]:\n        metaclass = self.get_schema_metaclass()\n        if name is None:\n            name = self.classname\n            rename = context.renames.get(name)\n            if rename is not None:\n                name = rename\n        return schema.get_global(metaclass, name, default=default)\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        \"\"\"Resolve, canonicalize and amend field mutations in this command.\n\n        This is called just before the object described by this command\n        is created or updated but after all prerequisite commands have\n        been applied, so it is safe to resolve object shells and do\n        other schema inquiries here.\n        \"\"\"\n        return schema\n\n    def update_field_status(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> None:\n        computed_status = self._get_computed_status_of_fields(schema, context)\n        self._update_computed_fields(schema, context, computed_status)\n\n    def populate_ddl_identity(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        return schema\n\n    def get_resolved_attribute_value(\n        self,\n        attr_name: str,\n        *,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> Any:\n        raw_value = self.get_attribute_value(attr_name)\n        if raw_value is None:\n            return None\n\n        value = context.get_cached((self, 'attribute', attr_name))\n        if value is None:\n            value = self.resolve_attribute_value(\n                attr_name,\n                raw_value,\n                schema=schema,\n                context=context,\n            )\n            context.cache_value((self, 'attribute', attr_name), value)\n\n        return value\n\n    def resolve_attribute_value(\n        self,\n        attr_name: str,\n        raw_value: Any,\n        *,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> Any:\n        metaclass = self.get_schema_metaclass()\n        field = metaclass.get_field(attr_name)\n        if field is None:\n            raise errors.SchemaDefinitionError(\n                f'got AlterObjectProperty command for '\n                f'invalid field: {metaclass.__name__}.{attr_name}')\n\n        value = self._resolve_attr_value(\n            raw_value, attr_name, field, schema)\n\n        if isinstance(value, s_expr.Expression):\n            if not value.is_compiled():\n                value = self.compile_expr_field(schema, context, field, value)\n\n            if id := self.get_attribute_value('id'):\n                value.set_origin(id, attr_name)\n        elif isinstance(value, s_expr.ExpressionDict):\n            compiled = {}\n            obj_id = self.get_attribute_value('id')\n            for k, v in value.items():\n                if not v.is_compiled():\n                    v = self.compile_expr_field(schema, context, field, v)\n                    if obj_id:\n                        v.set_origin(obj_id, attr_name)\n                compiled[k] = v\n            value = compiled\n\n        return value\n\n    def get_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> dict[str, Any]:\n        result = {}\n\n        for attr in self.enumerate_attributes():\n            result[attr] = self.get_attribute_value(attr)\n\n        return result\n\n    def get_resolved_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> dict[str, Any]:\n        result = {}\n\n        for attr in self.enumerate_attributes():\n            result[attr] = self.get_resolved_attribute_value(\n                attr, schema=schema, context=context)\n\n        return result\n\n    def get_orig_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> dict[str, Any]:\n        result = {}\n\n        for attr in self.enumerate_attributes():\n            result[attr] = self.get_orig_attribute_value(attr)\n\n        return result\n\n    def get_specified_attribute_value(\n        self,\n        field: str,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> Optional[Any]:\n        \"\"\"Fetch the specified (not computed) value of a field.\n\n        If the command is an alter, it will fall back to the value in\n        the schema.\n\n        Return None if there is no specified value or if the specified\n        value is being reset.\n        \"\"\"\n        spec = self.get_attribute_value(field)\n\n        is_alter = (\n            isinstance(self, AlterObject)\n            or (\n                isinstance(self, AlterObjectFragment)\n                and isinstance(self.get_parent_op(context), AlterObject)\n            )\n        )\n        if (\n            is_alter\n            and spec is None\n            and not self.has_attribute_value(field)\n            and field not in self.scls.get_computed_fields(schema)\n        ):\n            spec = self.scls.get_explicit_field_value(\n                schema, field, default=None)\n\n        return spec\n\n    def compile_expr_field(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        field: so.Field[Any],\n        value: Any,\n        track_schema_ref_exprs: bool=False,\n    ) -> s_expr.CompiledExpression:\n        cdn = self.get_schema_metaclass().get_schema_class_displayname()\n        raise errors.InternalServerError(\n            f'uncompiled expression in the field {field.name!r} of '\n            f'{cdn} {self.classname!r}'\n        )\n\n    def get_dummy_expr_field_value(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        field: so.Field[Any],\n        value: Any,\n    ) -> Optional[s_expr.Expression]:\n        \"\"\"Return a dummy value for an expression stored in *field*.\n\n        Schema class command implementations should overload this\n        to specify a dummy value for an expression field, which is necessary\n        when doing dependency type and name propagation switcheroo in\n        _propagate_if_expr_refs() / _finalize_affected_refs().\n        \"\"\"\n        raise NotImplementedError\n\n    def _create_begin(\n        self, schema: s_schema.Schema, context: CommandContext\n    ) -> s_schema.Schema:\n        raise NotImplementedError\n\n    def new_context(\n        self: ObjectCommand[Object_T],\n        schema: s_schema.Schema,\n        context: CommandContext,\n        scls: Object_T,\n    ) -> CommandContextWrapper[ObjectCommand[Object_T]]:\n        ctxcls = type(self).get_context_class()\n        assert ctxcls is not None\n        return context(\n            ctxcls(schema=schema, op=self, scls=scls),  # type: ignore\n        )\n\n    def get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        dummy = cast(Object_T, _dummy_object)\n\n        context_class = type(self).get_context_class()\n        if context_class is not None:\n            with self.new_context(schema, context, dummy):\n                return self._get_ast(schema, context, parent_node=parent_node)\n        else:\n            return self._get_ast(schema, context, parent_node=parent_node)\n\n    def get_ddl_identity(self, aspect: str) -> Any:\n        if self.ddl_identity is None:\n            raise LookupError(f'{self!r} has no DDL identity information')\n        value = self.ddl_identity.get(aspect)\n        if value is None:\n            raise LookupError(f'{self!r} has no {aspect!r} in DDL identity')\n        return value\n\n    def has_ddl_identity(self, aspect: str) -> bool:\n        return (\n            self.ddl_identity is not None\n            and self.ddl_identity.get(aspect) is not None\n        )\n\n    def set_ddl_identity(self, aspect: str, value: Any) -> None:\n        if self.ddl_identity is None:\n            self.ddl_identity = {}\n\n        self.ddl_identity[aspect] = value\n\n    def maybe_get_object_aux_data(self, field: str) -> Any:\n        if self.aux_object_data is None:\n            return None\n        else:\n            value = self.aux_object_data.get(field)\n            if value is None:\n                return None\n            else:\n                return value\n\n    def get_object_aux_data(self, field: str) -> Any:\n        if self.aux_object_data is None:\n            raise LookupError(f'{self!r} has no auxiliary object information')\n        value = self.aux_object_data.get(field)\n        if value is None:\n            raise LookupError(\n                f'{self!r} has no {field!r} in auxiliary object information')\n        return value\n\n    def has_object_aux_data(self, field: str) -> bool:\n        return (\n            self.aux_object_data is not None\n            and self.aux_object_data.get(field) is not None\n        )\n\n    def set_object_aux_data(self, field: str, value: Any) -> None:\n        if self.aux_object_data is None:\n            self.aux_object_data = {}\n\n        self.aux_object_data[field] = value\n\n    def get_annotation(self, name: str) -> Any:\n        if self.annotations is None:\n            return None\n        else:\n            return self.annotations.get(name)\n\n    def set_annotation(self, name: str, value: Any) -> None:\n        if self.annotations is None:\n            self.annotations = {}\n        self.annotations[name] = value\n\n    def ast_ignore_ownership(self) -> bool:\n        \"\"\"Whether to force generating an AST even though it isn't owned\"\"\"\n        return False\n\n    def ast_ignore_field_ownership(self, field: str) -> bool:\n        \"\"\"Whether to force generating an AST even though it isn't owned\"\"\"\n        return False\n\n\nclass ObjectCommandContext[Object_T: so.Object](\n    CommandContextToken[ObjectCommand[Object_T]]\n):\n\n    def __init__(\n        self,\n        schema: s_schema.Schema,\n        op: ObjectCommand[Object_T],\n        scls: Object_T,\n        *,\n        modaliases: Optional[Mapping[Optional[str], str]] = None,\n        localnames: AbstractSet[str] = frozenset(),\n    ) -> None:\n        super().__init__(\n            schema, op, modaliases=modaliases, localnames=localnames)\n        self.scls = scls\n\n\nclass QualifiedObjectCommand(ObjectCommand[so.QualifiedObject_T]):\n\n    classname = struct.Field(sn.QualName)\n\n    @classmethod\n    def _classname_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: CommandContext,\n    ) -> sn.QualName:\n        objref = astnode.name\n        module = context.modaliases.get(objref.module, objref.module)\n        if module is None:\n            raise errors.SchemaDefinitionError(\n                f'unqualified name and no default module set',\n                span=objref.span,\n            )\n\n        return sn.QualName(module=module, name=objref.name)\n\n    @overload\n    def get_object(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        name: Optional[sn.Name] = None,\n        default: so.QualifiedObject_T | so.NoDefaultT = so.NoDefault,\n        span: Optional[parsing.Span] = None,\n    ) -> so.QualifiedObject_T:\n        ...\n\n    @overload\n    def get_object(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        name: Optional[sn.Name] = None,\n        default: None = None,\n        span: Optional[parsing.Span] = None,\n    ) -> Optional[so.QualifiedObject_T]:\n        ...\n\n    def get_object(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        name: Optional[sn.Name] = None,\n        default: so.QualifiedObject_T | so.NoDefaultT | None = so.NoDefault,\n        span: Optional[parsing.Span] = None,\n    ) -> Optional[so.QualifiedObject_T]:\n        if name is None:\n            name = self.classname\n            rename = context.renames.get(name)\n            if rename is not None:\n                name = rename\n        metaclass = self.get_schema_metaclass()\n        if span is None:\n            span = self.span\n        return schema.get(\n            name, type=metaclass, default=default, span=span)\n\n\nclass GlobalObjectCommand(ObjectCommand[so.GlobalObject_T]):\n    pass\n\n\nclass ExternalObjectCommand(ObjectCommand[so.ExternalObject_T]):\n    pass\n\n\nclass CreateObject[Object_T: so.Object](ObjectCommand[Object_T]):\n    _delta_action = 'create'\n\n    # If the command is conditioned with IF NOT EXISTS\n    if_not_exists = struct.Field(bool, default=False)\n\n    def is_data_safe(self) -> bool:\n        # Creations are always data-safe.\n        return True\n\n    @classmethod\n    def command_for_ast_node(\n        cls,\n        astnode: qlast.DDLOperation,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> type[ObjectCommand[Object_T]]:\n        assert isinstance(astnode, qlast.CreateObject), \"expected CreateObject\"\n\n        if astnode.sdl_alter_if_exists:\n            modaliases = cls._modaliases_from_ast(schema, astnode, context)\n            dummy_op = cls(\n                classname=sn.QualName('placeholder', 'placeholder'))\n            ctxcls = cast(\n                type[ObjectCommandContext[Object_T]],\n                cls.get_context_class_or_die(),\n            )\n            ctx = ctxcls(\n                schema,\n                op=dummy_op,\n                scls=cast(Object_T, _dummy_object),\n                modaliases=modaliases,\n            )\n            with context(ctx):\n                classname = cls._classname_from_ast(schema, astnode, context)\n            mcls = cls.get_schema_metaclass()\n            if schema.get(classname, default=None) is not None:\n                return get_object_command_class_or_die(\n                    AlterObject, mcls)\n\n        return cls\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: CommandContext,\n    ) -> Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(astnode, qlast.CreateObject)\n        assert isinstance(cmd, CreateObject)\n\n        cmd.if_not_exists = astnode.create_if_not_exists\n\n        cmd.set_attribute_value('name', cmd.classname)\n\n        if getattr(astnode, 'abstract', False):\n            cmd.set_attribute_value('abstract', True)\n\n        return cmd\n\n    def get_verb(self) -> str:\n        return 'create'\n\n    def validate_create(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> None:\n        # Check if functions by this name exist\n        obj_name = self.get_attribute_value('name')\n        if obj_name is not None and not sn.is_fullname(str(obj_name)):\n            from . import functions as s_func\n            funcs = s_func.lookup_functions(obj_name, tuple(), schema=schema)\n            if funcs:\n                raise errors.SchemaError(\n                    f'{funcs[0].get_verbosename(schema)} already exists')\n\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        self._validate_legal_command(schema, context)\n\n        schema = self.apply_prerequisites(schema, context)\n\n        if not context.canonical:\n            schema = self.populate_ddl_identity(schema, context)\n            schema = self.canonicalize_attributes(schema, context)\n            self.update_field_status(schema, context)\n            self.validate_create(schema, context)\n\n        metaclass = self.get_schema_metaclass()\n\n        props = self.get_resolved_attributes(schema, context)\n        if not props.get('id'):\n            if context.schema_object_ids is not None:\n                specified_id = self.get_prespecified_id(context)\n                if specified_id is not None:\n                    props['id'] = specified_id\n\n        # This takes the span of the delta command and attaches it to the schema\n        # object. In practice, this means that span of DDL CREATE and SDL cmds\n        # is saved to the schema.\n        # But only to the in-memory repr of schema, since span is marked as\n        # ephemeral. This is because spans are large and not really needed in\n        # normal schema work, but are needed for language server.\n        if self.span and 'span' not in props:\n            props['span'] = self.span\n\n        schema, self.scls = metaclass.create_in_schema(\n            schema, stable_ids=context.stable_ids, **props)\n\n        if not self.get_attribute_value('id'):\n            # Record the generated ID.\n            self.set_attribute_value('id', self.scls.id)\n\n        return schema\n\n    def get_prespecified_id(\n        self,\n        context: CommandContext, *,\n        id_field: str = 'id',\n    ) -> Optional[uuid.UUID]:\n        if context.schema_object_ids is None:\n            return None\n\n        mcls = self.get_schema_metaclass()\n        qlclass: Optional[str]\n        if issubclass(mcls, so.QualifiedObject):\n            qlclass = None\n        else:\n            qlclass = mcls.get_ql_class_or_die()\n\n        objname = self.classname\n        if context.compat_ver_is_before(\n            (1, 0, verutils.VersionStage.ALPHA, 5)\n        ):\n            # Pre alpha.5 used to have a different name mangling scheme.\n            objname = sn.compat_name_remangle(str(objname))\n\n        if id_field != 'id':\n            qlclass = f'{qlclass}-{id_field}'\n\n        key = (objname, qlclass)\n        return context.schema_object_ids.get(key)\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n\n        self.set_attribute_value('builtin', context.stdmode)\n        if not self.has_attribute_value('internal'):\n            self.set_attribute_value('internal', context.internal_schema_mode)\n        return schema\n\n    def _update_computed_fields(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        update: Mapping[str, bool],\n    ) -> None:\n        computed_fields = {n for n, v in update.items() if v}\n        if computed_fields:\n            self.set_attribute_value(\n                'computed_fields', frozenset(computed_fields))\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        node = super()._get_ast(schema, context, parent_node=parent_node)\n        if node is not None and self.if_not_exists:\n            assert isinstance(node, qlast.CreateObject)\n            node.create_if_not_exists = True\n        return node\n\n    def _create_innards(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        return self.apply_subcommands(schema, context)\n\n    def _create_finalize(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        if not context.canonical:\n            # This is rarely triggered.\n            schema = self._finalize_affected_refs(schema, context)\n            self.validate_object(schema, context)\n        return schema\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        with self.new_context(schema, context, _dummy_object):  # type: ignore\n            if self.if_not_exists:\n                scls = self.get_object(schema, context, default=None)\n\n                if scls is not None:\n                    parent_ctx = context.parent()\n                    if parent_ctx is not None and not self.canonical:\n                        parent_ctx.op.discard(self)\n\n                    self.scls = scls\n                    return schema\n\n            schema = self._create_begin(schema, context)\n            ctx = context.current()\n            objctx = cast(ObjectCommandContext[Object_T], ctx)\n            objctx.scls = self.scls\n            schema = self._create_innards(schema, context)\n            schema = self.apply_caused(schema, context)\n            schema = self._create_finalize(schema, context)\n        return schema\n\n\nclass CreateExternalObject(\n    CreateObject[so.ExternalObject_T],\n    ExternalObjectCommand[so.ExternalObject_T],\n):\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        with self.new_context(schema, context, _dummy_object):  # type: ignore\n            if self.if_not_exists:\n                raise NotImplementedError(\n                    'if_not_exists not implemented for external objects')\n\n            schema = self._create_begin(schema, context)\n            schema = self._create_innards(schema, context)\n            schema = self.apply_caused(schema, context)\n            schema = self._create_finalize(schema, context)\n\n        return schema\n\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        self._validate_legal_command(schema, context)\n\n        if not context.canonical:\n            schema = self.populate_ddl_identity(schema, context)\n            schema = self.canonicalize_attributes(schema, context)\n            self.update_field_status(schema, context)\n            self.validate_create(schema, context)\n\n        props = self.get_resolved_attributes(schema, context)\n        metaclass = self.get_schema_metaclass()\n\n        obj_id = props.get('id')\n        if obj_id is None:\n            obj_id = metaclass._prepare_id(schema, context.stable_ids, props)\n            self.set_attribute_value('id', obj_id)\n\n        self.scls = metaclass._create_from_id(obj_id)\n\n        return schema\n\n\nclass AlterObjectOrFragment[Object_T: so.Object](ObjectCommand[Object_T]):\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n        # Hydrate the ALTER fields with original field values,\n        # if not present.\n        for cmd in self.get_subcommands(type=AlterObjectProperty):\n            if cmd.old_value is None:\n                cmd.old_value = self.scls.get_explicit_field_value(\n                    schema, cmd.property, default=None)\n        return schema\n\n    def validate_alter(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> None:\n        self._validate_legal_command(schema, context)\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        schema = self.apply_prerequisites(schema, context)\n        if not context.canonical:\n            schema = self.populate_ddl_identity(schema, context)\n            schema = self.canonicalize_attributes(schema, context)\n            self.update_field_status(schema, context)\n            self.validate_alter(schema, context)\n\n        props = self.get_resolved_attributes(schema, context)\n        return self.scls.update(schema, props)\n\n    def _alter_innards(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        return self.apply_subcommands(schema, context)\n\n    def _populate_link_reflection_fields(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> None:\n        \"\"\"For objects reflected with AS_LINK, populate all attributes\n\n        This is kind of a hack around reflection.writer (... and edgeql)\n        deficiencies, where reflection needs anything that is reflected\n        as a linkprop to actually be present in the Alter, or it will\n        be lost.\n        \"\"\"\n        if isinstance(self, AlterSpecialObjectField):\n            return\n        mcls = self.get_schema_metaclass()\n        for name in mcls.get_schema_fields().keys():\n            if not self.has_attribute_value(name):\n                try:\n                    value = self.scls.get_explicit_field_value(\n                        schema, name\n                    )\n                except so.FieldValueNotFoundError:\n                    continue\n                self.set_attribute_value(\n                    name,\n                    value,\n                    orig_value=value,\n                    inherited=self.scls.field_is_inherited(schema, name),\n                    computed=self.scls.field_is_computed(schema, name),\n                    disallow_special=True,\n                )\n\n    def _alter_finalize(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        schema = self._finalize_affected_refs(schema, context)\n        if not context.canonical:\n            self.validate_object(schema, context)\n            mcls = self.get_schema_metaclass()\n            if mcls.get_reflection_method() == so.ReflectionMethod.AS_LINK:\n                self._populate_link_reflection_fields(schema, context)\n        return schema\n\n    def _update_computed_fields(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        update: Mapping[str, bool],\n    ) -> None:\n        cur_comp_fields = self.scls.get_computed_fields(schema)\n        comp_fields = set(cur_comp_fields)\n        for fn, computed in update.items():\n            if computed:\n                comp_fields.add(fn)\n            else:\n                comp_fields.discard(fn)\n\n        if cur_comp_fields != comp_fields:\n            if comp_fields:\n                self.set_attribute_value(\n                    'computed_fields',\n                    frozenset(comp_fields),\n                    orig_value=cur_comp_fields if cur_comp_fields else None,\n                )\n            else:\n                self.set_attribute_value(\n                    'computed_fields',\n                    None,\n                    orig_value=cur_comp_fields if cur_comp_fields else None,\n                )\n\n\nclass AlterObjectFragment[Object_T: so.Object](AlterObjectOrFragment[Object_T]):\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        # AlterObjectFragment must be executed in the context\n        # of a parent AlterObject command.\n        scls = self.get_parent_op(context).scls\n        self.scls = cast(Object_T, scls)\n\n        schema = self._alter_begin(schema, context)\n        schema = self._alter_innards(schema, context)\n        schema = self.apply_caused(schema, context)\n        schema = self._alter_finalize(schema, context)\n\n        return schema\n\n    @classmethod\n    def get_parent_op(\n        cls,\n        context: CommandContext,\n    ) -> ObjectCommand[so.Object]:\n        op = context.current().op\n        assert isinstance(op, ObjectCommand)\n        return op\n\n\nclass RenameObject[Object_T: so.Object](AlterObjectFragment[Object_T]):\n    _delta_action = 'rename'\n\n    astnode = qlast.Rename\n\n    new_name = struct.Field(sn.Name)\n\n    def is_data_safe(self) -> bool:\n        # Renames are always data-safe.\n        return True\n\n    def get_verb(self) -> str:\n        return 'rename'\n\n    def get_friendly_description(\n        self,\n        *,\n        parent_op: Optional[Command] = None,\n        schema: Optional[s_schema.Schema] = None,\n        object: Any = None,\n        object_desc: Optional[str] = None,\n    ) -> str:\n        object_desc = self.get_friendly_object_name_for_description(\n            parent_op=parent_op,\n            schema=schema,\n            object=object,\n            object_desc=object_desc,\n        )\n        mcls = self.get_schema_metaclass()\n        new_name = mcls.get_displayname_static(self.new_name)\n        return f\"rename {object_desc} to '{new_name}'\"\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        scls = self.scls\n        context.renames[self.classname] = self.new_name\n        context.renamed_objs.add(scls)\n\n        # Propagate the change, but only if it wasn't handled by the\n        # enclosing Alter.\n        if context.current().op not in context.affected_finalization:\n            vn = scls.get_verbosename(schema)\n            schema = self._propagate_if_expr_refs(\n                schema,\n                context,\n                action=f'rename {vn}',\n                metadata_only=True,\n            )\n\n        if not context.canonical:\n            self.set_attribute_value(\n                'name',\n                value=self.new_name,\n                orig_value=self.classname,\n            )\n\n        return super()._alter_begin(schema, context)\n\n    def _alter_innards(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        if not context.canonical:\n            self._canonicalize(schema, context, self.scls)\n        return super()._alter_innards(schema, context)\n\n    def init_rename_branch(\n        self,\n        ref: so.Object,\n        new_ref_name: sn.Name,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> Command:\n\n        ref_root, ref_alter, _ = ref.init_delta_branch(\n            schema, context, AlterObject)\n\n        ref_alter.add(\n            ref.init_delta_command(\n                schema,\n                RenameObject,\n                new_name=new_ref_name,\n            ),\n        )\n\n        return ref_root\n\n    def _canonicalize(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        scls: Object_T,\n    ) -> None:\n        mcls = self.get_schema_metaclass()\n\n        for refdict in mcls.get_refdicts():\n            all_refs = set(\n                scls.get_field_value(schema, refdict.attr).objects(schema)\n            )\n\n            ref: so.Object\n            for ref in all_refs:\n                ref_name = ref.get_name(schema)\n                quals = list(sn.quals_from_fullname(ref_name))\n                assert isinstance(self.new_name, sn.QualName)\n                quals[0] = str(self.new_name)\n                shortname = sn.shortname_from_fullname(ref_name)\n                new_ref_name = sn.QualName(\n                    name=sn.get_specialized_name(shortname, *quals),\n                    module=self.new_name.module,\n                )\n                self.add(self.init_rename_branch(\n                    ref,\n                    new_ref_name,\n                    schema=schema,\n                    context=context,\n                ))\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        astnode = self._get_ast_node(schema, context)\n        ref = self._deparse_name(schema, context, self.new_name)\n        ref.itemclass = None\n        orig_ref = self._deparse_name(schema, context, self.classname)\n\n        # Ha, ha! Do it recursively to force any renames in children!\n        self._log_all_renames(context)\n\n        if (orig_ref.module, orig_ref.name) != (ref.module, ref.name):\n            return astnode(new_name=ref)  # type: ignore\n        else:\n            return None\n\n    @classmethod\n    def _cmd_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: CommandContext,\n    ) -> RenameObject[Object_T]:\n        parent_ctx = context.current()\n        parent_op = parent_ctx.op\n        assert isinstance(parent_op, ObjectCommand)\n        parent_class = parent_op.get_schema_metaclass()\n        rename_class = get_object_command_class_or_die(\n            RenameObject, parent_class)\n        return rename_class._rename_cmd_from_ast(schema, astnode, context)\n\n    @classmethod\n    def _rename_cmd_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: CommandContext,\n    ) -> RenameObject[Object_T]:\n        assert isinstance(astnode, qlast.Rename)\n\n        parent_ctx = context.current()\n        parent_op = parent_ctx.op\n        assert isinstance(parent_op, ObjectCommand)\n        parent_class = parent_op.get_schema_metaclass()\n        rename_class = get_object_command_class_or_die(\n            RenameObject, parent_class)\n\n        new_name = cls._classname_from_ast(schema, astnode, context)\n\n        # Populate the early_renames map of the context as we go, since\n        # in-flight renames will affect the generated names of later\n        # operations.\n        context.early_renames[parent_op.classname] = new_name\n\n        return rename_class(\n            classname=parent_op.classname,\n            new_name=new_name,\n        )\n\n\nclass AlterObject[Object_T: so.Object](AlterObjectOrFragment[Object_T]):\n    _delta_action = 'alter'\n\n    #: If True, apply the command only if the object exists.\n    if_exists = struct.Field(bool, default=False)\n\n    #: If True, only apply changes to properties, not \"real\" schema changes\n    metadata_only = struct.Field(bool, default=False)\n\n    def get_verb(self) -> str:\n        return 'alter'\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: CommandContext,\n    ) -> Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, AlterObject)\n\n        if getattr(astnode, 'abstract', False):\n            cmd.set_attribute_value('abstract', True)\n\n        return cmd\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        node = super()._get_ast(schema, context, parent_node=parent_node)\n        if (node is not None and hasattr(node, 'commands') and\n                not node.commands):\n            # Alter node without subcommands.  Occurs when all\n            # subcommands have been filtered out of DDL stream,\n            # so filter it out as well.\n            node = None\n        return node\n\n    def canonicalize_alter_from_external_ref(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> None:\n        \"\"\"Canonicalize an ALTER command triggered by a modification of a\n        an object referred to by an expression in this object.\"\"\"\n        pass\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n\n        if not context.canonical and self.if_exists:\n            scls = self.get_object(schema, context, default=None)\n            if scls is None:\n                context.current().op.discard(self)\n                return schema\n        else:\n            scls = self.get_object(schema, context)\n\n        self.scls = scls\n\n        with self.new_context(schema, context, scls):\n            schema = self._alter_begin(schema, context)\n            schema = self._alter_innards(schema, context)\n            schema = self.apply_caused(schema, context)\n            schema = self._alter_finalize(schema, context)\n\n        return schema\n\n\nclass DeleteObject[Object_T: so.Object](ObjectCommand[Object_T]):\n    _delta_action = 'delete'\n\n    #: If True, apply the command only if the object exists.\n    if_exists = struct.Field(bool, default=False)\n\n    #: If True, apply the command only if the object has no referrers\n    #: in the schema.\n    if_unused = struct.Field(bool, default=False)\n\n    def get_verb(self) -> str:\n        return 'drop'\n\n    def is_data_safe(self) -> bool:\n        # Deletions are only safe if the entire object class\n        # has been declared as data-safe.\n        return self.get_schema_metaclass()._data_safe\n\n    def _delete_begin(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        from . import ordering\n\n        self._validate_legal_command(schema, context)\n        schema = self.apply_prerequisites(schema, context)\n\n        if not context.canonical:\n            schema = self.populate_ddl_identity(schema, context)\n            schema = self.canonicalize_attributes(schema, context)\n\n            if not context.get_value(('delcanon', self)):\n                commands = self._canonicalize(schema, context, self.scls)\n                root = DeltaRoot()\n                root.update(commands)\n                root = ordering.linearize_delta(root, schema, schema)\n                self.update(root.get_subcommands())\n\n        return schema\n\n    def _canonicalize(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        scls: Object_T,\n    ) -> list[Command]:\n        mcls = self.get_schema_metaclass()\n        commands: list[Command] = []\n\n        for refdict in mcls.get_refdicts():\n            deleted_refs = set()\n\n            all_refs = set(\n                scls.get_field_value(schema, refdict.attr).objects(schema)\n            )\n\n            refcmds = cast(\n                tuple[ObjectCommand[so.Object], ...],\n                self.get_subcommands(metaclass=refdict.ref_cls),\n            )\n\n            for op in refcmds:\n                deleted_ref: so.Object = schema.get(op.classname)\n                deleted_refs.add(deleted_ref)\n\n            # Add implicit Delete commands for any local refs not\n            # deleted explicitly.\n            for ref in all_refs - deleted_refs:\n                op = ref.init_delta_command(schema, DeleteObject)\n                assert isinstance(op, DeleteObject)\n                subcmds = op._canonicalize(schema, context, ref)\n                op.update(subcmds)\n                commands.append(op)\n\n        # Record the fact that DeleteObject._canonicalize\n        # was called on this object to guard against possible\n        # duplicate calls.\n        context.store_value(('delcanon', self), True)\n\n        return commands\n\n    def _delete_innards(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        return self.apply_subcommands(schema, context)\n\n    def _delete_finalize(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        ref_strs = []\n\n        if not context.canonical and not context.disable_dep_verification:\n            refs = schema.get_referrers(self.scls)\n            ctx = context.current()\n            assert ctx is not None\n            orig_schema = ctx.original_schema\n            if refs:\n                for ref in refs:\n                    if (not self._is_deleting_ref(schema, context, ref)\n                            and ref.is_blocking_ref(orig_schema, self.scls)):\n                        ref_strs.append(\n                            ref.get_verbosename(orig_schema, with_parent=True))\n\n            if ref_strs:\n                vn = self.scls.get_verbosename(orig_schema, with_parent=True)\n                dn = self.scls.get_displayname(orig_schema)\n                detail = '; '.join(f'{ref_str} depends on {dn}'\n                                   for ref_str in ref_strs)\n                raise errors.SchemaError(\n                    f'cannot drop {vn} because '\n                    f'other objects in the schema depend on it',\n                    details=detail,\n                )\n\n        schema = schema.delete(self.scls)\n\n        if not context.canonical:\n            schema = self._finalize_affected_refs(schema, context)\n\n        return schema\n\n    def _is_deleting_ref(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        ref: so.Object,\n    ) -> bool:\n        if context.is_deleting(ref):\n            return True\n\n        for op in self.get_prerequisites():\n            if isinstance(op, DeleteObject) and op.scls == ref:\n                return True\n\n        return False\n\n    def _has_outside_references(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> bool:\n        # Check if the subject of this command has any outside references\n        # minus any current expiring refs and minus structural child refs\n        # (e.g. source backref in pointers of an object type).\n        refs = [\n            ref\n            for ref in schema.get_referrers(self.scls)\n            if not ref.is_parent_ref(schema, self.scls)\n            and not context.is_deleting(ref)\n        ]\n\n        return bool(refs)\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        if self.if_exists:\n            scls = self.get_object(schema, context, default=None)\n            if scls is None:\n                context.current().op.discard(self)\n                return schema\n        else:\n            scls = self.get_object(schema, context)\n\n        self.scls = scls\n\n        with self.new_context(schema, context, scls):\n            if (\n                self.if_unused\n                and self._has_outside_references(schema, context)\n            ):\n                parent_ctx = context.parent()\n                if parent_ctx is not None:\n                    parent_ctx.op.discard(self)\n\n                return schema\n\n            schema = self._delete_begin(schema, context)\n            schema = self._delete_innards(schema, context)\n            schema = self.apply_caused(schema, context)\n            schema = self._delete_finalize(schema, context)\n\n        return schema\n\n\nclass AlterExternalObject[ExternalObject_T: so.ExternalObject](\n    AlterObject[ExternalObject_T],\n    ExternalObjectCommand[ExternalObject_T],\n):\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        schema = self.apply_prerequisites(schema, context)\n        return schema\n\n    def _alter_innards(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        return self.apply_subcommands(schema, context)\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        self.scls = _dummy_object  # type: ignore\n\n        with self.new_context(schema, context, self.scls):\n            schema = self._alter_begin(schema, context)\n            schema = self._alter_innards(schema, context)\n            schema = self.apply_caused(schema, context)\n            schema = self._alter_finalize(schema, context)\n\n        return schema\n\n\nclass DeleteExternalObject[ExternalObject_T: so.ExternalObject](\n    DeleteObject[ExternalObject_T],\n    ExternalObjectCommand[ExternalObject_T],\n):\n    def _delete_begin(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        self._validate_legal_command(schema, context)\n        return schema\n\n    def _delete_innards(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        for op in self.get_subcommands(metaclass=so.Object):\n            schema = op.apply(schema, context=context)\n\n        return schema\n\n    def _delete_finalize(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        return schema\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n    ) -> s_schema.Schema:\n        self.scls = _dummy_object  # type: ignore\n\n        with self.new_context(schema, context, self.scls):\n            schema = self._delete_begin(schema, context)\n            schema = self._delete_innards(schema, context)\n            schema = self.apply_caused(schema, context)\n            schema = self._delete_finalize(schema, context)\n\n        return schema\n\n\nspecial_field_alter_handlers: dict[\n    str,\n    dict[type[so.Object], type[AlterSpecialObjectField[so.Object]]],\n] = {}\n\n\nclass AlterSpecialObjectField[Object_T: so.Object](\n    AlterObjectFragment[Object_T]\n):\n    \"\"\"Base class for AlterObjectFragment implementations for special fields.\n\n    When the generic `AlterObjectProperty` handling of field value transitions\n    is insufficient, declare a subclass of this to implement custom handling.\n    \"\"\"\n\n    _field: ClassVar[str]\n\n    def __init_subclass__(\n        cls,\n        *,\n        field: Optional[str] = None,\n        **kwargs: Any,\n    ) -> None:\n        super().__init_subclass__(**kwargs)\n\n        if field is None:\n            if any(\n                issubclass(b, AlterSpecialObjectField)\n                for b in cls.__mro__[1:]\n            ):\n                return\n            else:\n                raise TypeError(\n                    \"AlterSpecialObjectField.__init_subclass__() missing \"\n                    \"1 required keyword-only argument: 'field'\"\n                )\n\n        handlers = special_field_alter_handlers.get(field)\n        if handlers is None:\n            handlers = special_field_alter_handlers[field] = {}\n\n        schema_metaclass = cls.get_schema_metaclass()\n        handlers[schema_metaclass] = cls  # type: ignore\n        cls._field = field\n\n    def clone(self, name: sn.Name) -> AlterSpecialObjectField[Object_T]:\n        return struct.Struct.replace(self, classname=name)\n\n    @classmethod\n    def _cmd_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: CommandContext,\n    ) -> ObjectCommand[Object_T]:\n        this_op = context.current().op\n        assert isinstance(this_op, ObjectCommand)\n        return cls(classname=this_op.classname)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: CommandContext,\n    ) -> Command:\n        assert isinstance(astnode, qlast.SetField)\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        cmd.add(AlterObjectProperty.regular_cmd_from_ast(\n            schema, astnode, context))\n        return cmd\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        attrs = self._enumerate_attribute_cmds()\n        assert len(attrs) == 1, \"expected one attribute command\"\n        return attrs[0]._get_ast(schema, context, parent_node=parent_node)\n\n    def get_verb(self) -> str:\n        return f'alter the {self._field} of'\n\n\ndef get_special_field_alter_handler(\n    field: str,\n    schema_cls: type[so.Object],\n) -> Optional[type[AlterSpecialObjectField[so.Object]]]:\n    \"\"\"Return a custom handler for the field value transition, if any.\n\n    Returns a subclass of AlterSpecialObjectField, when in the context\n    of an AlterObject operation, and a special handler has been declared.\n    \"\"\"\n    field_handlers = special_field_alter_handlers.get(field)\n    if field_handlers is None:\n        return None\n    return field_handlers.get(schema_cls)\n\n\ndef get_special_field_create_handler(\n    field: str,\n    schema_cls: type[so.Object],\n) -> Optional[type[AlterSpecialObjectField[so.Object]]]:\n    \"\"\"Return a custom handler for the field value transition, if any.\n\n    Returns a subclass of AlterSpecialObjectField, when in the context\n    of an CreateObject operation, and a special handler has been declared.\n\n    For now this is just a hacky special case:\n      the 'required' field of Pointers. If that changes, we should generalize\n      the mechanism.\n    \"\"\"\n    if field != 'required':\n        return None\n    return get_special_field_alter_handler(field, schema_cls)\n\n\ndef get_special_field_alter_handler_for_context(\n    field: str,\n    context: CommandContext,\n) -> Optional[type[AlterSpecialObjectField[so.Object]]]:\n    \"\"\"Return a custom handler for the field value transition, if any.\n\n    Returns a subclass of AlterSpecialObjectField, when in the context\n    of an AlterObject operation, and a special handler has been declared.\n    \"\"\"\n    this_op = context.current().op\n    if (\n        isinstance(this_op, AlterObjectOrFragment)\n        and not isinstance(this_op, AlterSpecialObjectField)\n    ):\n        mcls = this_op.get_schema_metaclass()\n        return get_special_field_alter_handler(field, mcls)\n    elif isinstance(this_op, CreateObject):\n        mcls = this_op.get_schema_metaclass()\n        return get_special_field_create_handler(field, mcls)\n    else:\n        return None\n\n\nclass AlterObjectProperty(Command):\n    astnode = qlast.SetField\n\n    property = struct.Field(str)\n    old_value = struct.Field[Any](object, default=None)\n    new_value = struct.Field[Any](object, default=None)\n    old_inherited = struct.Field(bool, default=False)\n    new_inherited = struct.Field(bool, default=False)\n    new_computed = struct.Field(bool, default=False)\n    old_computed = struct.Field(bool, default=False)\n    from_default = struct.Field(bool, default=False)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: CommandContext,\n    ) -> Command:\n        assert isinstance(astnode, qlast.SetField)\n        handler = get_special_field_alter_handler_for_context(\n            astnode.name, context)\n        if handler is not None:\n            return handler._cmd_tree_from_ast(schema, astnode, context)\n        else:\n            return cls.regular_cmd_from_ast(schema, astnode, context)\n\n    @classmethod\n    def regular_cmd_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.SetField,\n        context: CommandContext,\n    ) -> Command:\n        propname = astnode.name\n        parent_ctx = context.current()\n        parent_op = parent_ctx.op\n        assert isinstance(parent_op, ObjectCommand)\n        parent_cls = parent_op.get_schema_metaclass()\n\n        if (\n            propname.startswith('orig_')\n            and context.compat_ver_is_before(\n                (1, 0, verutils.VersionStage.ALPHA, 8)\n            )\n            and not parent_cls.has_field(propname)\n        ):\n            return Nop()\n        else:\n            try:\n                field = parent_cls.get_field(propname)\n            except LookupError:\n                raise errors.SchemaDefinitionError(\n                    f'{propname!r} is not a valid field',\n                    span=astnode.span)\n\n        if not (\n            astnode.special_syntax\n            or field.allow_ddl_set\n            or context.stdmode\n            or context.testmode\n        ):\n            raise errors.SchemaDefinitionError(\n                f'{propname!r} is not a valid field',\n                span=astnode.span)\n\n        if field.name == 'id' and not isinstance(parent_op, CreateObject):\n            raise errors.SchemaDefinitionError(\n                f'cannot alter object id',\n                span=astnode.span)\n\n        ast_value: Optional[qlast.Expr | qlast.TypeExpr] = astnode.value\n        if field.obj_names_as_string:\n            inliner = NameToStringConverter()\n            ast_value = cast(\n                Optional[qlast.Expr | qlast.TypeExpr],\n                inliner.visit(ast_value),\n            )\n\n        new_value: Any\n\n        if field.type is s_expr.Expression:\n            if ast_value is None:\n                new_value = None\n            else:\n                assert isinstance(ast_value, qlast.Expr)\n                orig_text = cls.get_orig_expr_text(\n                    schema, parent_op.qlast, field.name)\n\n                if (\n                    orig_text is not None\n                    and context.compat_ver_is_before(\n                        (1, 0, verutils.VersionStage.ALPHA, 6)\n                    )\n                ):\n                    # Versions prior to a6 used a different expression\n                    # normalization strategy, so we must renormalize the\n                    # expression.\n                    expr_ql = qlcompiler.renormalize_compat(\n                        ast_value,\n                        orig_text,\n                        schema=schema,\n                        localnames=context.localnames,\n                    )\n                else:\n                    expr_ql = ast_value\n\n                new_value = s_expr.Expression.from_ast(\n                    expr_ql,\n                    schema,\n                    context.modaliases,\n                    context.localnames,\n                )\n        else:\n            if (\n                isinstance(ast_value, qlast.Set)\n                and not ast_value.elements\n            ):\n                # empty set\n                new_value = None\n\n            elif isinstance(ast_value, qlast.Tuple):\n                new_value = tuple(\n                    qlcompiler.evaluate_ast_to_python_val(\n                        el, schema=schema)\n                    for el in ast_value.elements\n                )\n\n            # Handle object references\n            elif (\n                isinstance(ast_value, qlast.Path)\n                and not ast_value.partial\n                and len(ast_value.steps) == 1\n                and isinstance(ast_value.steps[0], qlast.ObjectRef)\n            ):\n\n                new_value = utils.ast_to_object_shell(\n                    ast_value.steps[0],\n                    metaclass=so.Object,\n                    modaliases=context.modaliases,\n                    schema=schema,\n                )\n                if issubclass(field.type, so.ObjectCollection):\n                    new_value = [new_value]\n\n            # ... and sets of object references\n            # It is kind of a bummer the way this is special cased, though\n            elif (\n                isinstance(ast_value, qlast.Set)\n                and all(\n                    isinstance(v, qlast.Path)\n                    and not v.partial\n                    and len(v.steps) == 1\n                    and isinstance(v.steps[0], qlast.ObjectRef)\n                    for v in ast_value.elements\n                )\n            ):\n                new_value = [\n                    utils.ast_to_object_shell(\n                        v.steps[0],\n                        metaclass=so.Object,\n                        modaliases=context.modaliases,\n                        schema=schema,\n                    )\n                    for v in ast_value.elements\n                    if isinstance(v, qlast.Path)\n                    and isinstance(v.steps[0], qlast.ObjectRef)\n                ]\n\n            elif isinstance(ast_value, qlast.TypeExpr):\n                from . import types as s_types\n\n                if not isinstance(parent_op, QualifiedObjectCommand):\n                    raise AssertionError(\n                        'cannot determine module for derived compound type: '\n                        'parent operation is not a QualifiedObjectCommand'\n                    )\n\n                new_value = utils.ast_to_type_shell(\n                    ast_value,\n                    metaclass=s_types.Type,\n                    module=parent_op.classname.module,\n                    modaliases=context.modaliases,\n                    schema=schema,\n                )\n                if issubclass(field.type, so.ObjectCollection):\n                    new_value = [new_value]\n\n            # ... and sets of object references\n            # It is kind of a bummer the way this is special cased, though\n            elif (\n                isinstance(ast_value, qlast.Set)\n                and all(\n                    isinstance(v, qlast.TypeExpr)\n                    for v in ast_value.elements\n                )\n            ):\n                from . import types as s_types\n\n                new_value = [\n                    utils.ast_to_type_shell(\n                        v,\n                        metaclass=s_types.Type,\n                        modaliases=context.modaliases,\n                        schema=schema,\n                    )\n                    for v in ast_value.elements\n                    if isinstance(v, qlast.TypeExpr)\n                ]\n\n            elif (\n                isinstance(ast_value, qlast.StrInterp)\n                and field.allow_interpolation\n            ):\n                new_value = utils.str_interpolation_to_old_style(ast_value)\n            else:\n                try:\n                    new_value = qlcompiler.evaluate_ast_to_python_val(\n                        ast_value, schema=schema) if ast_value else None\n                except Exception:\n                    raise\n                if new_value is not None:\n                    new_value = field.coerce_value(schema, new_value)\n\n        return cls(\n            property=propname,\n            new_value=new_value,\n            span=astnode.span,\n        )\n\n    def is_data_safe(self) -> bool:\n        # Field alterations on existing schema objects\n        # generally represent semantic changes and are\n        # reversible.  Non-safe field alters are normally\n        # represented by a dedicated subcommand, such as\n        # SetLinkType.\n        return True\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        value = self.new_value\n\n        new_value_empty = (\n            value is None\n            or (\n                utils.is_nontrivial_container(value) is not None and not value\n            )\n        )\n        old_value_empty = (\n            self.old_value is None\n            or (\n                utils.is_nontrivial_container(self.old_value) is not None\n                and not self.old_value\n            )\n        )\n\n        parent_ctx = context.current()\n        parent_op = parent_ctx.op\n        assert isinstance(parent_op, ObjectCommand)\n        assert parent_node is not None\n        parent_cls = parent_op.get_schema_metaclass()\n        field = parent_cls.get_field(self.property)\n        if field is None:\n            raise errors.SchemaDefinitionError(\n                f'{self.property!r} is not a valid field',\n                span=self.span)\n\n        if self.property == 'id':\n            return None\n\n        parent_node_attr = parent_op.get_ast_attr_for_field(\n            field.name, type(parent_node))\n\n        if (\n            not field.allow_ddl_set\n            and not (\n                field.special_ddl_syntax\n                and isinstance(parent_node, qlast.AlterObject)\n            )\n            and self.property != 'expr'\n            and parent_node_attr is None\n        ):\n            # Don't produce any AST if:\n            #\n            # * a field does not have the \"allow_ddl_set\" option, unless\n            #   it's an 'expr' field.\n            #\n            #   'expr' fields come from the \"USING\" clause and are specially\n            #   treated in parser and codegen.\n            return None\n\n        if (\n            (\n                self.new_inherited\n                and not self.old_inherited\n                and not old_value_empty\n            ) or (\n                self.new_computed\n                and not self.old_computed\n                and not self.old_inherited\n                and not old_value_empty\n            )\n        ):\n            # The field became inherited or computed, in which case we should\n            # generate a RESET.\n            return qlast.SetField(\n                name=self.property,\n                value=None,\n                special_syntax=field.special_ddl_syntax,\n            )\n\n        if self.new_inherited or self.new_computed:\n            # We don't want to show inherited or computed properties unless\n            # we are in \"descriptive_mode\" ...\n            if not context.descriptive_mode:\n                return None\n\n            if not (\n                field.describe_visibility\n                & so.DescribeVisibilityFlags.SHOW_IF_DERIVED\n            ):\n                # ... or if the field shouldn't be shown when inherited\n                # or computed.\n                return None\n\n            if (\n                not (\n                    field.describe_visibility\n                    & so.DescribeVisibilityFlags.SHOW_IF_DEFAULT\n                ) and field.default == value\n            ):\n                # ... or if the field should not be shown when the value\n                # mathdes the default.\n                return None\n\n            parentop_sn = sn.shortname_from_fullname(parent_op.classname).name\n            if self.property == 'default' and parentop_sn == 'id':\n                # ... or if it's 'default' for the 'id' property\n                # (special case).\n                return None\n\n        if self.from_default:\n            if not context.descriptive_mode:\n                return None\n\n            if not (\n                field.describe_visibility\n                & so.DescribeVisibilityFlags.SHOW_IF_DEFAULT\n            ):\n                # ... or if the field should not be shown when the value\n                # mathdes the default.\n                return None\n\n        if new_value_empty:\n            if old_value_empty:\n                return None\n            else:\n                value = None\n        elif issubclass(field.type, s_expr.Expression):\n            return self._get_expr_field_ast(\n                schema,\n                context,\n                parent_op=parent_op,\n                field=field,\n                parent_node=parent_node,\n                parent_node_attr=parent_node_attr,\n            )\n        elif issubclass(field.type, so.ObjectCollection):\n            value = qlast.Set(elements=[\n                # HACK: This is wrong, but it's good enough.\n                cast(qlast.Expr, utils.shell_to_ast(schema, v))\n                for v in (value or ())\n            ])\n\n        elif parent_node_attr is not None:\n            setattr(parent_node, parent_node_attr, value)\n            return None\n        elif (v := utils.is_nontrivial_container(value)) and v is not None:\n            value = qlast.Tuple(elements=[\n                utils.const_ast_from_python(el) for el in v\n            ])\n        elif isinstance(value, uuid.UUID):\n            value = qlast.TypeCast(\n                expr=qlast.Constant.string(str(value)),\n                type=qlast.TypeName(\n                    maintype=qlast.ObjectRef(\n                        name='uuid',\n                        module='std',\n                    )\n                )\n            )\n        elif isinstance(value, so.ObjectShell):\n            value = utils.shell_to_ast(schema, value)\n        else:\n            value = utils.const_ast_from_python(value)\n\n        return qlast.SetField(\n            name=self.property,\n            value=value,\n            special_syntax=field.special_ddl_syntax,\n        )\n\n    def _get_expr_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: CommandContext,\n        *,\n        parent_op: ObjectCommand[so.Object],\n        field: so.Field[Any],\n        parent_node: qlast.DDLOperation,\n        parent_node_attr: Optional[str],\n    ) -> Optional[qlast.DDLOperation]:\n        from edb import edgeql\n\n        assert isinstance(\n            self.new_value,\n            (s_expr.Expression, s_expr.ExpressionShell),\n        )\n\n        expr_ql = edgeql.parse_fragment(self.new_value.text)\n\n        if parent_node is not None and parent_node_attr is not None:\n            setattr(parent_node, parent_node_attr, expr_ql)\n            return None\n        else:\n            return qlast.SetField(\n                name=self.property,\n                value=expr_ql,\n                special_syntax=(\n                    self.property == 'expr' or field.special_ddl_syntax\n                ),\n            )\n\n    def __repr__(self) -> str:\n        return '<%s.%s \"%s\":\"%s\"->\"%s\">' % (\n            self.__class__.__module__, self.__class__.__name__,\n            self.property, self.old_value, self.new_value)\n\n    def get_friendly_description(\n        self,\n        *,\n        parent_op: Optional[Command] = None,\n        schema: Optional[s_schema.Schema] = None,\n        object: Any = None,\n        object_desc: Optional[str] = None,\n    ) -> str:\n        if parent_op is not None:\n            assert isinstance(parent_op, ObjectCommand)\n            object_desc = parent_op.get_friendly_object_name_for_description(\n                schema=schema,\n                object=object,\n                object_desc=object_desc,\n            )\n            return f'alter the {self.property} of {object_desc}'\n        else:\n            return f'alter the {self.property} of schema object'\n\n\nclass NameToStringConverter(ast.NodeTransformer):\n\n    def visit_Path(self, node: qlast.Path) -> qlast.Base:\n        if (\n            len(node.steps) == 1\n            and (obj_name := node.steps[0])\n            and isinstance(obj_name, qlast.ObjectRef)\n        ):\n            if obj_name.module is None:\n                raise errors.SchemaDefinitionError(\n                    f\"Object name must be fully qualified.\",\n                    span=node.span,\n                )\n\n            return qlast.Constant.string(f\"{obj_name.module}::{obj_name.name}\")\n\n        raise errors.SchemaDefinitionError(\n            f\"Object references are not allowed here.\",\n            span=node.span,\n        )\n\n\ndef compile_ddl(\n    schema: s_schema.Schema,\n    astnode: qlast.DDLOperation,\n    *,\n    context: Optional[CommandContext]=None,\n) -> Command:\n\n    if context is None:\n        context = CommandContext()\n\n    astnode_type = type(astnode)\n    primary_cmdcls = CommandMeta._astnode_map.get(astnode_type)\n    if primary_cmdcls is None:\n        for astnode_type_base in astnode_type.__mro__[1:]:\n            primary_cmdcls = CommandMeta._astnode_map.get(astnode_type_base)\n            if primary_cmdcls is not None:\n                break\n        else:\n            raise AssertionError(\n                f'no delta command class for AST node {astnode!r}')\n\n    cmdcls = primary_cmdcls.command_for_ast_node(astnode, schema, context)\n\n    context_class = cmdcls.get_context_class()\n    if context_class is not None:\n        modaliases = cmdcls._modaliases_from_ast(schema, astnode, context)\n        localnames = cmdcls.localnames_from_ast(schema, astnode, context)\n        ctxcls = cast(\n            type[ObjectCommandContext[so.Object]],\n            context_class,\n        )\n        ctx = ctxcls(\n            schema,\n            op=cast(ObjectCommand[so.Object], _dummy_command),\n            scls=_dummy_object,\n            modaliases=modaliases,\n            localnames=localnames,\n        )\n        with context(ctx):\n            cmd = cmdcls._cmd_tree_from_ast(schema, astnode, context)\n    else:\n        cmd = cmdcls._cmd_tree_from_ast(schema, astnode, context)\n\n    return cmd\n\n\ndef get_object_delta_command[\n    Object_T: so.Object, ObjectCommand_T: ObjectCommand[so.Object]\n](\n    *,\n    objtype: type[Object_T],\n    cmdtype: type[ObjectCommand_T],\n    schema: s_schema.Schema,\n    name: sn.Name,\n    ddl_identity: Optional[Mapping[str, Any]] = None,\n    **kwargs: Any,\n) -> ObjectCommand_T:\n\n    cmdcls = cast(\n        type[ObjectCommand_T],\n        get_object_command_class_or_die(cmdtype, objtype),\n    )\n\n    return cmdcls(\n        classname=name,\n        ddl_identity=dict(ddl_identity) if ddl_identity is not None else None,\n        **kwargs,\n    )\n\n\nCommandKey = tuple[str, type[so.Object], sn.Name, Optional[sn.Name]]\n\n\ndef get_object_command_key(delta: ObjectCommand[Any]) -> CommandKey:\n    if delta.orig_cmd_type is not None:\n        cmdtype = delta.orig_cmd_type\n    else:\n        cmdtype = type(delta)\n\n    new_name = (\n        getattr(delta, 'new_name', None)\n        or delta.get_annotation('new_name')\n    )\n    mcls = delta.get_schema_metaclass()\n    return cmdtype.__name__, mcls, delta.classname, new_name\n\n\ndef get_object_command_id(key: CommandKey) -> str:\n    cmdclass_name, mcls, name, new_name = key\n    qlcls = mcls.get_ql_class_or_die()\n    extra = ' TO ' + str(new_name) if new_name else ''\n    return f'{cmdclass_name} {qlcls} {name}{extra}'\n\n\ndef apply[S: s_schema.Schema](\n    delta: Command,\n    *,\n    schema: S,\n    context: Optional[CommandContext] = None,\n) -> S:\n    if context is None:\n        context = CommandContext()\n\n    if not isinstance(delta, DeltaRoot):\n        root = DeltaRoot()\n        root.add(delta)\n    else:\n        root = delta\n\n    return cast(S, root.apply(schema, context))\n"
  },
  {
    "path": "edb/schema/expr.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n    AbstractSet,\n    Iterable,\n    Mapping,\n    Sequence,\n    TYPE_CHECKING,\n)\n\nimport copy\nimport uuid\n\nfrom edb.common import checked\nfrom edb.common import struct\n\nfrom edb.edgeql import ast as qlast_\nfrom edb.edgeql import codegen as qlcodegen\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.edgeql import parser as qlparser\nfrom edb.edgeql import qltypes\n\nfrom . import objects as so\nfrom . import name as sn\nfrom . import delta as sd\n\n\nif TYPE_CHECKING:\n    from edb.schema import schema as s_schema\n    from edb.schema import types as s_types\n\n    from edb.ir import ast as irast_\n\n\nclass Expression(struct.MixedRTStruct, so.ObjectContainer):\n\n    text = struct.Field(str, frozen=True)\n    # mypy wants an argument to the ObjectSet generic, but\n    # that wouldn't work for struct.Field, since subscripted\n    # generics are not types.\n    refs = struct.Field(\n        so.ObjectSet,  # type: ignore\n        coerce=True,\n        default=None,\n        frozen=True,\n    )\n\n    # A string describing the provenance of the expression, used to\n    # help annotate the parser contexts. We don't store it explicitly\n    # in the database or explicitly populate it when creating\n    # Expressions, but instead populate it in resolve_attribute_value\n    # and when reading in the schema.\n    origin = struct.Field(str, default=None)\n\n    def __init__(\n        self,\n        *args: Any,\n        _qlast: Optional[qlast_.Expr] = None,\n        _irast: Optional[irast_.Statement] = None,\n        **kwargs: Any\n    ) -> None:\n        super().__init__(*args, **kwargs)\n        self._qlast = _qlast\n        self._irast = _irast\n\n    def __getstate__(self) -> dict[str, Any]:\n        return {\n            'text': self.text,\n            'refs': self.refs,\n            '_qlast': None,\n            '_irast': None,\n        }\n\n    def __setstate__(self, state: Mapping[str, Any]) -> None:\n        # Since `origin` is omitted from the pickled schema, it needs to be\n        # explicitly set to `None` when loading pickles.\n        super().__setstate__({\"origin\": None, **state})\n\n    def __eq__(self, rhs: object) -> bool:\n        if not isinstance(rhs, Expression):\n            return NotImplemented\n        return (\n            self.text == rhs.text\n            and self.refs == rhs.refs\n            and self.origin == rhs.origin\n        )\n\n    def parse(self) -> qlast_.Expr:\n        \"\"\"Parse the expression text into an AST. Cached.\"\"\"\n\n        if self._qlast is None:\n            self._qlast = qlparser.parse_fragment(\n                self.text, filename=f'<{self.origin}>' if self.origin else \"\")\n        return self._qlast\n\n    @property\n    def irast(self) -> Optional[irast_.Statement]:\n        return self._irast\n\n    def set_origin(self, id: uuid.UUID, field: str) -> None:\n        \"\"\"\n        Set the origin of the expression based on field and enclosing object.\n\n        We base the origin on the id of the object, not on its name, because\n        these strings should be useful to a client, which can't do a lookup\n        based on the mangled internal names.\n        \"\"\"\n        self.origin = f'{id} {field}'\n\n    def is_compiled(self) -> bool:\n        return self.refs is not None\n\n    def _refs_keys(\n        self, schema: s_schema.Schema\n    ) -> set[tuple[type[so.Object], sn.Name]]:\n        return {\n            (type(x), x.get_name(schema))\n            for x in (self.refs.objects(schema) if self.refs else ())\n        }\n\n    @classmethod\n    def compare_values(\n        cls: type[Expression],\n        ours: Expression,\n        theirs: Expression,\n        *,\n        our_schema: s_schema.Schema,\n        their_schema: s_schema.Schema,\n        context: so.ComparisonContext,\n        compcoef: float,\n    ) -> float:\n        if not ours and not theirs:\n            return 1.0\n        elif not ours or not theirs:\n            return compcoef\n\n        # If the new and old versions share a reference to an object\n        # that is being deleted, then we must delete this object as well.\n        our_refs = ours._refs_keys(our_schema)\n        their_refs = theirs._refs_keys(their_schema)\n        if (our_refs & their_refs) & context.deletions.keys():\n            return 0.0\n\n        if ours.text == theirs.text:\n            return 1.0\n        else:\n            return compcoef\n\n    @classmethod\n    def from_ast(\n        cls: type[Expression],\n        qltree: qlast_.Expr,\n        schema: s_schema.Schema,\n        modaliases: Optional[Mapping[Optional[str], str]] = None,\n        localnames: AbstractSet[str] = frozenset(),\n        *,\n        as_fragment: bool = False,\n    ) -> Expression:\n        if modaliases is None:\n            modaliases = {}\n        if not as_fragment:\n            qlcompiler.normalize(\n                qltree,\n                schema=schema,\n                modaliases=modaliases,\n                localnames=localnames\n            )\n\n        norm_text = qlcodegen.generate_source(qltree, pretty=False)\n\n        return Expression(\n            text=norm_text,\n            _qlast=qltree,\n        )\n\n    def not_compiled(self) -> Expression:\n        return Expression(text=self.text, origin=self.origin)\n\n    def compiled(\n        self,\n        schema: s_schema.Schema,\n        *,\n        options: Optional[qlcompiler.CompilerOptions] = None,\n        as_fragment: bool = False,\n        detached: bool = False,\n        find_extra_refs: Optional[\n            Callable[[irast_.Set], set[so.Object]]\n        ] = None,\n        context: Optional[sd.CommandContext],\n    ) -> CompiledExpression:\n\n        from edb.ir import ast as irast_\n        from edb.edgeql import ast as qlast\n\n        if as_fragment:\n            ir: irast_.Command = qlcompiler.compile_ast_fragment_to_ir(\n                self.parse(),\n                schema=schema,\n                options=options,\n            )\n        else:\n            ql_expr = self.parse()\n            if detached:\n                ql_expr = qlast.DetachedExpr(\n                    expr=ql_expr,\n                    preserve_path_prefix=True,\n                )\n\n            ir = qlcompiler.compile_ast_to_ir(\n                ql_expr,\n                schema=schema,\n                options=options,\n            )\n\n        assert isinstance(ir, irast_.Statement)\n\n        if context and ir.warnings:\n            delta_root = context.top().op\n            if isinstance(delta_root, sd.DeltaRoot):\n                delta_root.warnings.extend(ir.warnings)\n\n        # XXX: ref stuff - why doesn't it go into the delta tree? - temporary??\n        srefs: set[so.Object] = {\n            ref for ref in ir.schema_refs if schema.has_object(ref.id)\n        }\n\n        if find_extra_refs is not None:\n            srefs |= find_extra_refs(ir.expr)\n\n        return CompiledExpression(\n            text=self.text,\n            refs=so.ObjectSet.create(schema, srefs),\n            _qlast=self.parse(),\n            _irast=ir,\n            origin=self.origin,\n        )\n\n    def ensure_compiled(\n        self,\n        schema: s_schema.Schema,\n        *,\n        options: Optional[qlcompiler.CompilerOptions] = None,\n        as_fragment: bool = False,\n        context: Optional[sd.CommandContext],\n    ) -> CompiledExpression:\n        if self._irast:\n            return self  # type: ignore\n        else:\n            return self.compiled(\n                schema, options=options, as_fragment=as_fragment,\n                context=context)\n\n    def assert_compiled(self) -> CompiledExpression:\n        if self._irast:\n            return self  # type: ignore\n        else:\n            raise AssertionError(\n                f\"uncompiled expression {self.text!r} (origin: {self.origin})\")\n\n    @classmethod\n    def from_ir(\n        cls: type[Expression],\n        expr: Expression,\n        ir: irast_.Statement,\n        schema: s_schema.Schema,\n    ) -> CompiledExpression:\n        return CompiledExpression(\n            text=expr.text,\n            refs=so.ObjectSet.create(schema, ir.schema_refs),\n            _qlast=expr.parse(),\n            _irast=ir,\n            origin=expr.origin,\n        )\n\n    def as_shell(self, schema: s_schema.Schema) -> ExpressionShell:\n        return ExpressionShell(\n            text=self.text,\n            refs=(\n                r.as_shell(schema) for r in self.refs.objects(schema)\n            ) if self.refs is not None else None,\n            _qlast=self._qlast,\n        )\n\n    def schema_reduce(\n        self,\n    ) -> tuple[\n        str,\n        tuple[\n            str,\n            Optional[tuple[type, ...] | type],\n            tuple[uuid.UUID, ...],\n            tuple[tuple[str, Any], ...],\n        ],\n        Optional[str],\n    ]:\n        assert self.refs is not None, 'expected expression to be compiled'\n        return (\n            self.text,\n            self.refs.schema_reduce(),\n            self.origin,\n        )\n\n    @classmethod\n    def schema_restore(\n        cls,\n        data: tuple[\n            str,\n            tuple[\n                str,\n                Optional[tuple[type, ...] | type],\n                tuple[uuid.UUID, ...],\n                tuple[tuple[str, Any], ...],\n            ],\n            Optional[str],\n        ],\n    ) -> Expression:\n        text, refs_data, origin = data\n        return Expression(\n            text=text,\n            refs=so.ObjectCollection.schema_restore(refs_data),\n            origin=origin,\n        )\n\n    @classmethod\n    def schema_refs_from_data(\n        cls,\n        data: tuple[\n            str,\n            tuple[\n                str,\n                Optional[tuple[type, ...] | type],\n                tuple[uuid.UUID, ...],\n                tuple[tuple[str, Any], ...],\n            ],\n        ],\n    ) -> frozenset[uuid.UUID]:\n        return so.ObjectCollection.schema_refs_from_data(data[1])\n\n    @property\n    def ir_statement(self) -> irast_.Statement:\n        \"\"\"Assert this expr is a compiled EdgeQL statement and return its IR\"\"\"\n        from edb.ir import ast as irast_\n\n        if not self.is_compiled():\n            raise AssertionError('expected a compiled expression')\n        ir = self.irast\n        if not isinstance(ir, irast_.Statement):\n            raise AssertionError(\n                'expected the result of an expression to be a Statement')\n        return ir\n\n    @property\n    def stype(self) -> s_types.Type:\n        return self.ir_statement.stype\n\n    @property\n    def cardinality(self) -> qltypes.Cardinality:\n        return self.ir_statement.cardinality\n\n    @property\n    def schema(self) -> s_schema.Schema:\n        return self.ir_statement.schema\n\n\nclass CompiledExpression(Expression):\n    refs = struct.Field(\n        so.ObjectSet,  # type: ignore\n        coerce=True,\n        frozen=True,\n    )\n\n    def __init__(\n        self,\n        *args: Any,\n        _qlast: Optional[qlast_.Expr] = None,\n        _irast: irast_.Statement,\n        **kwargs: Any\n    ) -> None:\n        super().__init__(*args, _qlast=_qlast, _irast=_irast, **kwargs)\n\n    @property\n    def irast(self) -> irast_.Statement:\n        assert self._irast\n        return self._irast\n\n    def as_python_value(self) -> Any:\n        return qlcompiler.evaluate_ir_statement_to_python_val(self.irast)\n\n\nclass ExpressionShell(so.Shell):\n\n    def __init__(\n        self,\n        *,\n        text: str,\n        refs: Optional[Iterable[so.ObjectShell[so.Object]]],\n        _qlast: Optional[qlast_.Expr] = None,\n        _irast: Optional[irast_.Statement] = None,\n    ) -> None:\n        self.text = text\n        self.refs = tuple(refs) if refs is not None else None\n        self._qlast = _qlast\n        self._irast = _irast\n\n    def resolve(self, schema: s_schema.Schema) -> Expression:\n        cls = CompiledExpression if self._irast else Expression\n        return cls(\n            text=self.text,\n            refs=so.ObjectSet.create(\n                schema,\n                [s.resolve(schema) for s in self.refs],\n            ) if self.refs is not None else None,\n            _qlast=self._qlast,\n            _irast=self._irast,  # type: ignore[arg-type]\n        )\n\n    def parse(self) -> qlast_.Expr:\n        if self._qlast is None:\n            self._qlast = qlparser.parse_fragment(self.text)\n        return self._qlast\n\n    def __repr__(self) -> str:\n        if self.refs is None:\n            refs = 'N/A'\n        else:\n            refs = ', '.join(repr(obj) for obj in self.refs)\n        return f'<ExpressionShell {self.text} refs=({refs})>'\n\n\nclass ExpressionList(checked.FrozenCheckedList[Expression]):\n\n    @staticmethod\n    def merge_values(\n        target: so.Object,\n        sources: Sequence[so.Object],\n        field_name: str,\n        *,\n        ignore_local: bool = False,\n        schema: s_schema.Schema,\n    ) -> Any:\n        if not ignore_local:\n            result = target.get_explicit_field_value(schema, field_name, None)\n        else:\n            result = None\n        for source in sources:\n            theirs = source.get_explicit_field_value(schema, field_name, None)\n            if theirs:\n                if result is None:\n                    result = theirs[:]\n                else:\n                    result.extend(theirs)\n\n        return result\n\n    @classmethod\n    def compare_values(\n        cls: type[ExpressionList],\n        ours: Optional[ExpressionList],\n        theirs: Optional[ExpressionList],\n        *,\n        our_schema: s_schema.Schema,\n        their_schema: s_schema.Schema,\n        context: so.ComparisonContext,\n        compcoef: float,\n    ) -> float:\n        \"\"\"See the comment in Object.compare_values\"\"\"\n        if not ours and not theirs:\n            basecoef = 1.0\n        elif (not ours or not theirs) or (len(ours) != len(theirs)):\n            basecoef = 0.2\n        else:\n            similarity = []\n\n            for expr1, expr2 in zip(ours, theirs):\n                similarity.append(\n                    Expression.compare_values(\n                        expr1, expr2, our_schema=our_schema,\n                        their_schema=their_schema, context=context,\n                        compcoef=compcoef))\n\n            basecoef = sum(similarity) / len(similarity)\n\n        return basecoef + (1 - basecoef) * compcoef\n\n\nclass ExpressionDict(checked.CheckedDict[str, Expression]):\n\n    @staticmethod\n    def merge_values(\n        target: so.Object,\n        sources: Sequence[so.Object],\n        field_name: str,\n        *,\n        ignore_local: bool = False,\n        schema: s_schema.Schema,\n    ) -> Any:\n        result = None\n        # Assume that sources are given in MRO order, so we need to reverse\n        # them to figure out the merged vaue.\n        for source in reversed(sources):\n            theirs = source.get_explicit_field_value(schema, field_name, None)\n            if theirs:\n                if result is None:\n                    result = dict(theirs)\n                else:\n                    result.update(theirs)\n\n        # Finally merge the most relevant data.\n        if not ignore_local:\n            ours = target.get_explicit_field_value(schema, field_name, None)\n            if result is None:\n                result = ours\n            elif ours:\n                result.update(ours)\n\n        return result\n\n    @classmethod\n    def compare_values(\n        cls: type[ExpressionDict],\n        ours: Optional[ExpressionDict],\n        theirs: Optional[ExpressionDict],\n        *,\n        our_schema: s_schema.Schema,\n        their_schema: s_schema.Schema,\n        context: so.ComparisonContext,\n        compcoef: float,\n    ) -> float:\n        \"\"\"See the comment in Object.compare_values\"\"\"\n        if not ours and not theirs:\n            basecoef = 1.0\n        elif (not ours or not theirs) or (len(ours) != len(theirs)):\n            basecoef = 0.2\n        elif set(ours.keys()) != set(theirs.keys()):\n            # Same length dicts can still have different keys, which is\n            # similar to having mismatched length.\n            basecoef = 0.2\n        else:\n            # We have the same keys, so just compare the values.\n            similarity = []\n\n            for ((_, expr1), (_, expr2)) in zip(\n                sorted(ours.items()), sorted(theirs.items())\n            ):\n                similarity.append(\n                    Expression.compare_values(\n                        expr1, expr2, our_schema=our_schema,\n                        their_schema=their_schema, context=context,\n                        compcoef=compcoef))\n\n            basecoef = sum(similarity) / len(similarity)\n\n        return basecoef + (1 - basecoef) * compcoef\n\n\nEXPRESSION_TYPES = (\n    Expression, ExpressionList, ExpressionDict\n)\n\n\ndef imprint_expr_context(\n    qltree: qlast_.Base,\n    modaliases: Mapping[Optional[str], str],\n) -> qlast_.Base:\n    # Imprint current module aliases as explicit\n    # alias declarations in the expression.\n\n    if (isinstance(qltree, qlast_.BaseConstant)\n            or qltree is None\n            or (isinstance(qltree, qlast_.Set)\n                and not qltree.elements)\n            or (isinstance(qltree, qlast_.Array)\n                and all(isinstance(el, qlast_.BaseConstant)\n                        for el in qltree.elements))):\n        # Leave constants alone.\n        return qltree\n\n    if isinstance(qltree, qlast_.Expr):\n        qltree = qlast_.SelectQuery(result=qltree, implicit=True)\n    else:\n        assert isinstance(qltree, (qlast_.Command, qlast_.DDLCommand))\n        qltree = copy.copy(qltree)\n        qltree.aliases = (\n            list(qltree.aliases) if qltree.aliases is not None else None)\n    assert isinstance(qltree, (qlast_.Query, qlast_.Command))\n\n    existing_aliases: dict[Optional[str], str] = {}\n    for alias in (qltree.aliases or ()):\n        if isinstance(alias, qlast_.ModuleAliasDecl):\n            existing_aliases[alias.alias] = alias.module\n\n    aliases_to_add = set(modaliases) - set(existing_aliases)\n    for alias_name in aliases_to_add:\n        if qltree.aliases is None:\n            qltree.aliases = []\n        qltree.aliases.append(\n            qlast_.ModuleAliasDecl(\n                alias=alias_name,\n                module=modaliases[alias_name],\n            )\n        )\n\n    return qltree\n\n\ndef get_expr_referrers(\n    schema: s_schema.Schema, obj: so.Object\n) -> dict[so.Object, list[str]]:\n    \"\"\"Return schema referrers with refs in expressions.\"\"\"\n\n    refs: dict[tuple[type[so.Object], str], frozenset[so.Object]] = (\n        schema.get_referrers_ex(obj))\n    result: dict[so.Object, list[str]] = {}\n\n    for (mcls, fn), referrers in refs.items():\n        field = mcls.get_field(fn)\n        if issubclass(field.type, (Expression, ExpressionList)):\n            for ref in referrers:\n                result.setdefault(ref, []).append(fn)\n\n    return result\n"
  },
  {
    "path": "edb/schema/expraliases.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, TYPE_CHECKING\n\nfrom edb import errors\nfrom edb.common import parsing\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.edgeql import qltypes\nfrom edb.edgeql.compiler import astutils as qlastutils\n\nfrom . import annos as s_anno\nfrom . import expr as s_expr\nfrom . import delta as sd\nfrom . import name as sn\nfrom . import objects as so\nfrom . import types as s_types\n\n\nif TYPE_CHECKING:\n    from edb.ir import ast as irast\n    from . import schema as s_schema\n\n\nclass Alias(\n    so.QualifiedObject,\n    s_anno.AnnotationSubject,\n    qlkind=qltypes.SchemaObjectClass.ALIAS,\n    data_safe=True,\n):\n\n    expr = so.SchemaField(\n        s_expr.Expression,\n        default=None,\n        coerce=True,\n        compcoef=0.909,\n    )\n\n    type = so.SchemaField(\n        s_types.Type,\n        compcoef=0.909,\n    )\n\n    created_types = so.SchemaField(\n        so.ObjectSet[s_types.Type],\n        default=so.DEFAULT_CONSTRUCTOR,\n    )\n\n\nclass AliasCommandContext(\n    sd.ObjectCommandContext[so.Object],\n    s_anno.AnnotationSubjectCommandContext\n):\n    pass\n\n\nclass AliasLikeCommand(\n    sd.QualifiedObjectCommand[so.QualifiedObject_T],\n):\n    \"\"\"Common code for \"alias-likes\": that is, aliases and globals\n\n    Aliases and computed globals behave extremely similarly, except\n    for a few annoying differences that need to be handled in the\n    subclasses with appropriate overloads:\n      * In aliases, the type field name is 'type', while for computed\n        globals it is 'target'. This annoying discrepency is because\n        computed globals also share a bunch of code paths with pointers,\n        which use 'target', and so there needed to be a mismatch on one\n        of the sides. Handled by overloading TYPE_FIELD_NAME.\n      * For aliases, it is the generated view type that gets the real\n        name and the alias that gets the mangled one. For globals,\n        the real global needs to get the real name, so that the name\n        does not depend on whether it is computed or not.\n        This is handled by overloading _get_alias_name, which computes\n        the name of the alias type (and _classname_from_ast).\n      * Also aliases *always* are alias-like, while globals only are when\n        computed. This is handled by overloading _is_computable.\n\n    Computed globals also have explicit 'required' and 'cardinality' fields,\n    which are managed explicitly in the globals code.\n    \"\"\"\n\n    TYPE_FIELD_NAME = ''\n    ALIAS_LIKE_EXPR_FIELDS: tuple[str, ...] = ()\n\n    @classmethod\n    def _get_alias_name(cls, type_name: sn.QualName) -> sn.QualName:\n        raise NotImplementedError\n\n    @classmethod\n    def _is_computable(\n        cls, obj: so.QualifiedObject_T, schema: s_schema.Schema\n    ) -> bool:\n        raise NotImplementedError\n\n    # Generic code\n\n    def _delete_alias_types(\n        self,\n        scls: so.QualifiedObject_T,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        unset_type: bool = True,\n    ) -> sd.CommandGroup:\n        from . import globals as s_globals\n        from . import ordering as s_ordering\n\n        assert isinstance(scls, (Alias, s_globals.Global))\n        types: so.ObjectSet[s_types.Type] = scls.get_created_types(schema)\n        created = types.objects(schema)\n\n        delta = sd.DeltaRoot()\n\n        # Unset created_types and type/target, so the types can be dropped\n        alter_alias = scls.init_delta_command(schema, sd.AlterObject)\n        alter_alias.canonical = True\n        # This would usually not be needed, as both Alias.type and\n        # Global.target have ON TARGET DELETE DEFERRED RESTRICT.\n        # But because we are using \"if_unused\", we want to delete references\n        # to these types so they get dropped if had been the only ref.\n        if unset_type:\n            # (there are cases when we don't need to unset the type, such as\n            # when a computed global has been converted to a non-computed one)\n            alter_alias.add(\n                sd.AlterObjectProperty(\n                    property=self.TYPE_FIELD_NAME, new_value=None\n                )\n            )\n        alter_alias.set_attribute_value('created_types', set())\n\n        for dep_type in created:\n            if_unused = isinstance(dep_type, s_types.Collection)\n\n            drop_dep = dep_type.init_delta_command(\n                schema, sd.DeleteObject, if_exists=True, if_unused=if_unused\n            )\n            subcmds = drop_dep._canonicalize(schema, context, dep_type)\n            drop_dep.update(subcmds)\n\n            delta.add(drop_dep)\n\n        delta = s_ordering.linearize_delta(\n            delta, old_schema=schema, new_schema=schema\n        )\n        delta.prepend(alter_alias)\n        return delta\n\n    @classmethod\n    def get_type(\n        cls, obj: so.QualifiedObject_T, schema: s_schema.Schema\n    ) -> s_types.Type:\n        obj = obj.get_field_value(schema, cls.TYPE_FIELD_NAME)\n        assert isinstance(obj, s_types.Type)\n        return obj\n\n    @classmethod\n    def _mangle_name(\n        cls,\n        type_name: sn.QualName,\n        *,\n        include_module_in_name: bool,\n    ) -> sn.QualName:\n        base_name = (\n            type_name\n            if include_module_in_name else\n            type_name.get_local_name()\n        )\n        quals = (cls.get_schema_metaclass().get_schema_class_displayname(),)\n        pnn = sn.get_specialized_name(base_name, str(type_name), *quals)\n        name = sn.QualName(name=pnn, module=type_name.module)\n        assert isinstance(name, sn.QualName)\n        return name\n\n    def get_dummy_expr_field_value(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: Any,\n    ) -> Optional[s_expr.Expression]:\n        if field.name in self.ALIAS_LIKE_EXPR_FIELDS:\n            rt = self.get_type(self.scls, schema)\n            return s_types.type_dummy_expr(rt, schema)\n        else:\n            raise NotImplementedError(f'unhandled field {field.name!r}')\n\n    def _handle_alias_op(\n        self,\n        *,\n        expr: s_expr.Expression,\n        classname: sn.QualName,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        is_alter: bool = False,\n        span: Optional[parsing.Span] = None,\n    ) -> tuple[\n        sd.Command,\n        s_types.TypeShell[s_types.Type],\n        s_expr.Expression,\n        set[so.ObjectShell[s_types.Type]],\n    ]:\n        pschema = schema\n\n        # For alters, drop the alias first, use the schema without the alias\n        # for compilation of the new alias expr\n        drop_old_types_cmd: Optional[sd.Command] = None\n        if is_alter:\n            drop_old_types_cmd = self._delete_alias_types(\n                self.scls, schema, context)\n            with context.suspend_dep_verification():\n                pschema = drop_old_types_cmd.apply(pschema, context)\n\n        ir = compile_alias_expr(\n            expr.parse(),\n            classname,\n            pschema,\n            context,\n            span=span,\n        )\n\n        expr = s_expr.Expression.from_ir(expr, ir, schema=schema)\n\n        is_global = (self.get_schema_metaclass().\n                     get_schema_class_displayname() == 'global')\n        cmd, type_shell, created_types = _create_alias_types(\n            expr=expr,\n            classname=classname,\n            schema=schema,\n            is_global=is_global,\n            span=span,\n        )\n        if drop_old_types_cmd:\n            cmd.prepend(drop_old_types_cmd)\n\n        return cmd, type_shell, expr, created_types\n\n\nclass AliasCommand(\n    AliasLikeCommand[Alias],\n    context_class=AliasCommandContext,\n):\n    TYPE_FIELD_NAME = 'type'\n    ALIAS_LIKE_EXPR_FIELDS = ('expr',)\n\n    @classmethod\n    def _get_alias_name(cls, type_name: sn.QualName) -> sn.QualName:\n        alias_name = sn.shortname_from_fullname(type_name)\n        assert isinstance(alias_name, sn.QualName), \"expected qualified name\"\n        return alias_name\n\n    @classmethod\n    def _is_computable(cls, obj: Alias, schema: s_schema.Schema) -> bool:\n        return True\n\n    @classmethod\n    def _classname_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext,\n    ) -> sn.QualName:\n        type_name = super()._classname_from_ast(schema, astnode, context)\n        return cls._mangle_name(type_name, include_module_in_name=True)\n\n    def compile_expr_field(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: s_expr.Expression,\n        track_schema_ref_exprs: bool=False,\n    ) -> s_expr.CompiledExpression:\n        assert field.name == 'expr'\n        classname = sn.shortname_from_fullname(self.classname)\n        assert isinstance(classname, sn.QualName), \\\n            \"expected qualified name\"\n        return value.compiled(\n            schema=schema,\n            options=qlcompiler.CompilerOptions(\n                derived_target_module=classname.module,\n                modaliases=context.modaliases,\n                in_ddl_context_name='alias definition',\n                schema_object_context=self.get_schema_metaclass(),\n                track_schema_ref_exprs=track_schema_ref_exprs,\n            ),\n            context=context,\n        )\n\n\nclass CreateAliasLike(\n    AliasLikeCommand[so.QualifiedObject_T],\n    sd.CreateObject[so.QualifiedObject_T],\n):\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        if not context.canonical and self.get_attribute_value('expr'):\n            alias_name = self._get_alias_name(self.classname)\n\n            # generated types might conflict with existing types\n            if other_obj := schema.get(alias_name, default=None):\n                vn = other_obj.get_verbosename(schema, with_parent=True)\n                raise errors.SchemaError(f'{vn} already exists')\n\n            type_cmd, type_shell, expr, created_types = self._handle_alias_op(\n                expr=self.get_attribute_value('expr'),\n                classname=alias_name,\n                schema=schema,\n                context=context,\n                span=self.get_attribute_span('expr'),\n            )\n            self.add_prerequisite(type_cmd)\n            self.set_attribute_value('expr', expr)\n            self.set_attribute_value(\n                self.TYPE_FIELD_NAME, type_shell, computed=True)\n            self.set_attribute_value('created_types', created_types)\n\n        return super()._create_begin(schema, context)\n\n\nclass CreateAlias(\n    CreateAliasLike[Alias],\n    AliasCommand,\n):\n    astnode = qlast.CreateAlias\n\n\nclass RenameAliasLike(\n    AliasLikeCommand[so.QualifiedObject_T],\n    sd.RenameObject[so.QualifiedObject_T],\n):\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        if not context.canonical and self._is_computable(self.scls, schema):\n            assert isinstance(self.new_name, sn.QualName)\n            new_alias_name = self._get_alias_name(self.new_name)\n            alias_type = self.get_type(self.scls, schema)\n            alter_cmd = alias_type.init_delta_command(schema, sd.AlterObject)\n            rename_cmd = alias_type.init_delta_command(\n                schema,\n                sd.RenameObject,\n                new_name=new_alias_name,\n            )\n            alter_cmd.add(rename_cmd)\n            self.add_prerequisite(alter_cmd)\n\n        return super()._alter_begin(schema, context)\n\n\nclass RenameAlias(RenameAliasLike[Alias], AliasCommand):\n    pass\n\n\nclass AlterAliasLike(\n    AliasLikeCommand[so.QualifiedObject_T],\n    sd.AlterObject[so.QualifiedObject_T],\n):\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        if not context.canonical:\n            schema = self._propagate_if_expr_refs(\n                schema,\n                context,\n                action=self.get_friendly_description(schema=schema),\n            )\n\n            expr = self.get_attribute_value('expr')\n            is_computable = self._is_computable(self.scls, schema)\n            if expr:\n                alias_name = self._get_alias_name(self.classname)\n                type_cmd, type_shell, expr, created_tys = self._handle_alias_op(\n                    expr=expr,\n                    classname=alias_name,\n                    schema=schema,\n                    context=context,\n                    is_alter=is_computable,\n                    span=self.get_attribute_span('expr'),\n                )\n\n                self.add_prerequisite(type_cmd)\n\n                self.set_attribute_value('expr', expr)\n                self.set_attribute_value(\n                    self.TYPE_FIELD_NAME, type_shell, computed=True)\n\n                self.set_attribute_value('created_types', created_tys)\n\n                # Clear out the type field in the schema *now*,\n                # before we call the parent _alter_begin, which will\n                # run prerequisites. This prevents the type reference\n                # from interferring with deletion. (And the deletion of\n                # the type has to be done as a prereq, since it needs\n                # to precede the creation of the replacement type\n                # with the same name.)\n                schema = schema.unset_field(\n                    self.scls, self.TYPE_FIELD_NAME)\n            else:\n                # there is no expr\n\n                if is_computable and self.has_attribute_value('expr'):\n                    # this is a global that just had its expr unset\n                    self.add(\n                        self._delete_alias_types(\n                            self.scls, schema, context, unset_type=False\n                        )\n                    )\n\n        return super()._alter_begin(schema, context)\n\n\nclass AlterAlias(\n    AlterAliasLike[Alias],\n    AliasCommand,\n):\n    astnode = qlast.AlterAlias\n\n\nclass DeleteAliasLike[QualifiedObject_T: so.QualifiedObject](\n    AliasLikeCommand[QualifiedObject_T],\n    sd.DeleteObject[QualifiedObject_T],\n):\n    def _canonicalize(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        scls: QualifiedObject_T,\n    ) -> list[sd.Command]:\n        ops = super()._canonicalize(schema, context, scls)\n        if self._is_computable(scls, schema):\n            ops.append(self._delete_alias_types(scls, schema, context))\n        return ops\n\n\nclass DeleteAlias(\n    DeleteAliasLike[Alias],\n    AliasCommand,\n):\n    astnode = qlast.DropAlias\n\n\ndef compile_alias_expr(\n    expr: qlast.Expr,\n    classname: sn.QualName,\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    span: Optional[parsing.Span] = None,\n) -> irast.Statement:\n    cached: Optional[irast.Statement] = (\n        context.get_cached((expr, classname)))\n    if cached is not None:\n        return cached\n\n    expr = qlastutils.ensure_ql_query(expr)\n\n    ir = qlcompiler.compile_ast_to_ir(\n        expr,\n        schema,\n        options=qlcompiler.CompilerOptions(\n            derived_target_module=classname.module,\n            result_view_name=classname,\n            modaliases=context.modaliases,\n            schema_view_mode=True,\n            schema_object_context=Alias,\n            in_ddl_context_name='alias definition',\n            bootstrap_mode=context.stdmode,\n        ),\n    )\n\n    if ir.volatility.is_volatile():\n        raise errors.SchemaDefinitionError(\n            f'volatile functions are not permitted in schema-defined '\n            f'computed expressions',\n            span=span,\n        )\n\n    context.cache_value((expr, classname), ir)\n\n    return ir\n\n\ndef _create_alias_types(\n    *,\n    expr: s_expr.CompiledExpression,\n    classname: sn.QualName,\n    schema: s_schema.Schema,\n    is_global: bool,\n    span: Optional[parsing.Span] = None,\n) -> tuple[\n    sd.Command,\n    s_types.TypeShell[s_types.Type],\n    set[so.ObjectShell[s_types.Type]],\n]:\n    from . import ordering as s_ordering\n    from edb.ir import utils as irutils\n\n    ir = expr.irast\n    new_schema = ir.schema\n\n    derived_delta = sd.DeltaRoot()\n\n    created_type_shells: set[so.ObjectShell[s_types.Type]] = set()\n\n    for ty_id in irutils.collect_schema_types(ir.expr):\n        ty = new_schema.get_by_id(ty_id, type=s_types.Type)\n        name = ty.get_name(new_schema)\n\n        if schema.has_object(ty_id):\n            # This is not a new type.\n\n            # Add any existing collection types to the `create_types` of aliases\n            # and computed globals. This adds a reference which prevents their\n            # deletion as other types are deleted.\n            if (\n                not ty.get_from_alias(schema)\n                and isinstance(ty, s_types.Collection)\n            ):\n                created_type_shells.add(\n                    so.ObjectShell(name=name, schemaclass=type(ty))\n                )\n            continue\n\n        if (\n            not isinstance(ty, s_types.Collection)\n            and not _has_alias_name_prefix(classname, name)\n        ):\n            # not all created types are visible from the root, so they don't\n            # need to be created in the schema\n            continue\n\n        # Schema views in derive an alias subtype for their expressions, which\n        # are stored in the schema with `from_alias` set to True. Aliases will\n        # also store any new types from their expressions into the schema.\n        #\n        # This is not an issue for most derived types since they are used only\n        # in their alias expression.\n        #\n        # However, collections which are not expr aliases that are created in\n        # an alias expression may be used in other places and should be\n        # not be created as `from_alias` or other alias/global associated\n        # fields.\n        if (\n            not isinstance(ty, s_types.Collection)\n            or isinstance(ty, s_types.CollectionExprAlias)\n        ):\n            new_schema = ty.update(\n                new_schema,\n                dict(\n                    alias_is_persistent=True,\n                    expr_type=s_types.ExprType.Select,\n                    from_alias=True,\n                    from_global=is_global,\n                ),\n            )\n        new_schema = ty.update(\n            new_schema,\n            dict(\n                internal=False,\n                builtin=False,\n            ),\n        )\n        if isinstance(ty, s_types.Collection):\n            new_schema = ty.set_field_value(new_schema, 'is_persistent', True)\n\n        derived_delta.add(\n            ty.as_create_delta(\n                schema=new_schema, context=so.ComparisonContext()\n            )\n        )\n        created_type_shells.add(so.ObjectShell(name=name, schemaclass=type(ty)))\n\n    derived_delta = s_ordering.linearize_delta(\n        derived_delta, old_schema=schema, new_schema=new_schema\n    )\n\n    type_cmd = None\n    for op in derived_delta.get_subcommands():\n        assert isinstance(op, sd.ObjectCommand)\n        if op.classname == classname:\n            type_cmd = op\n            break\n    assert type_cmd\n\n    type_cmd.set_attribute_value('expr', expr)\n\n    result = sd.CommandGroup()\n    result.update(derived_delta.get_subcommands())\n\n    type_shell = s_types.TypeShell(\n        name=classname,\n        origname=classname,\n        schemaclass=type_cmd.get_schema_metaclass(),\n        span=span,\n    )\n    return result, type_shell, created_type_shells\n\n\ndef _has_alias_name_prefix(\n    alias_name: sn.QualName,\n    name: sn.Name,\n) -> bool:\n    return alias_name == name or (\n        isinstance(name, sn.QualName)\n        and name.module == alias_name.module\n        and name.name.startswith(f'__{alias_name.name}__')\n    )\n"
  },
  {
    "path": "edb/schema/extensions.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, Iterator, cast\n\nimport collections\nimport contextlib\nimport uuid\n\nfrom edb import errors\n\nfrom edb.common import verutils\nfrom edb.common import struct\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\nfrom edb.edgeql import parser as qlparser\n\nfrom edb.common import checked\n\nfrom . import annos as s_anno\nfrom . import casts as s_casts\nfrom . import delta as sd\nfrom . import indexes as s_indexes\nfrom . import name as sn\nfrom . import objects as so\nfrom . import schema as s_schema\n\n\nclass ExtensionPackage(\n    so.GlobalObject,\n    s_anno.AnnotationSubject,\n    qlkind=qltypes.SchemaObjectClass.EXTENSION_PACKAGE,\n    data_safe=False,\n):\n\n    # Note: !!!!!!\n    # ExtensionPackage, like all GlobalObjects, needs to store its\n    # data in globally stored JSON instead of via reflection schema.\n    # When you add a field to ExtensionPackage, you must also update\n    # CreateExtensionPackage in pgsql/delta.py and\n    # _generate_extension_views in metaschema to store and retrieve\n    # the data from json.\n\n    version = so.SchemaField(\n        verutils.Version,\n        compcoef=0.9,\n    )\n\n    script = so.SchemaField(\n        str,\n        compcoef=0.9,\n    )\n\n    sql_extensions = so.SchemaField(\n        checked.FrozenCheckedSet[str],\n        default=so.DEFAULT_CONSTRUCTOR,\n        coerce=True,\n        inheritable=False,\n        compcoef=0.9,\n    )\n\n    sql_setup_script = so.SchemaField(\n        str, default=None, compcoef=0.9)\n\n    sql_teardown_script = so.SchemaField(\n        str, default=None, compcoef=0.9)\n\n    ext_module = so.SchemaField(\n        str, default=None, compcoef=0.9)\n\n    # It uses str instead of direct references so we can stick\n    # versions in there eventually\n    dependencies = so.SchemaField(\n        checked.FrozenCheckedSet[str],\n        default=so.DEFAULT_CONSTRUCTOR,\n        coerce=True,\n        inheritable=False,\n        compcoef=0.9,\n    )\n\n    @classmethod\n    def get_shortname_static(cls, name: sn.Name) -> sn.UnqualName:\n        return sn.UnqualName(sn.shortname_from_fullname(name).name)\n\n    @classmethod\n    def get_displayname_static(cls, name: sn.Name) -> str:\n        shortname = cls.get_shortname_static(name)\n        return shortname.name\n\n\nclass ExtensionPackageMigration(\n    so.GlobalObject,\n    s_anno.AnnotationSubject,\n    qlkind=qltypes.SchemaObjectClass.EXTENSION_PACKAGE_MIGRATION,\n    data_safe=False,\n):\n\n    # Note: !!!!!!\n    # ExtensionPackageMigration, like all GlobalObjects, needs to store its\n    # data in globally stored JSON instead of via reflection schema.\n    # When you add a field to ExtensionPackageMigration, you must also update\n    # CreateExtensionPackageMigration in pgsql/delta.py and\n    # _generate_extension_views in metaschema to store and retrieve\n    # the data from json.\n\n    from_version = so.SchemaField(\n        verutils.Version,\n        compcoef=0.9,\n    )\n\n    to_version = so.SchemaField(\n        verutils.Version,\n        compcoef=0.9,\n    )\n\n    script = so.SchemaField(\n        str,\n        compcoef=0.9,\n    )\n\n    sql_early_script = so.SchemaField(\n        str, default=None, compcoef=0.9)\n\n    sql_late_script = so.SchemaField(\n        str, default=None, compcoef=0.9)\n\n    @classmethod\n    def get_shortname_static(cls, name: sn.Name) -> sn.UnqualName:\n        return sn.UnqualName(sn.shortname_from_fullname(name).name)\n\n    @classmethod\n    def get_displayname_static(cls, name: sn.Name) -> str:\n        shortname = cls.get_shortname_static(name)\n        return shortname.name\n\n\nclass Extension(\n    so.Object,\n    qlkind=qltypes.SchemaObjectClass.EXTENSION,\n    data_safe=False,\n):\n\n    package = so.SchemaField(\n        ExtensionPackage,\n        compcoef=0.9,\n    )\n\n    dependencies = so.SchemaField(\n        so.ObjectList['Extension'],\n        default=so.DEFAULT_CONSTRUCTOR,\n        coerce=True,\n        inheritable=False,\n        compcoef=0.9,\n    )\n\n    @classmethod\n    def create_in_schema[Schema_T: s_schema.Schema](\n        cls: type[Extension],\n        schema: Schema_T,\n        stable_ids: bool = False,\n        *,\n        id: Optional[uuid.UUID] = None,\n        **data: Any,\n    ) -> tuple[Schema_T, Extension]:\n        name = data['name']\n        pkg = data['package']\n\n        if existing_ext := schema.get_global(Extension, name, default=None):\n            vn = existing_ext.get_verbosename(schema)\n            existing_pkg = existing_ext.get_package(schema)\n            raise errors.SchemaError(\n                f'cannot install {vn} version {pkg.get_version(schema)}: '\n                f'version {existing_pkg.get_version(schema)} is already '\n                f'installed'\n            )\n\n        return super().create_in_schema(schema, stable_ids, id=id, **data)\n\n\nclass ExtensionPackageCommandContext(\n    sd.ObjectCommandContext[ExtensionPackage],\n    s_anno.AnnotationSubjectCommandContext,\n):\n    pass\n\n\nclass ExtensionPackageCommand(\n    sd.GlobalObjectCommand[ExtensionPackage],\n    s_anno.AnnotationSubjectCommand[ExtensionPackage],\n    context_class=ExtensionPackageCommandContext,\n):\n\n    @classmethod\n    def _classname_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext\n    ) -> sn.UnqualName:\n        assert isinstance(astnode, qlast.ExtensionPackageCommand)\n        parsed_version = verutils.parse_version(astnode.version.value)\n        quals = ['pkg', str(parsed_version)]\n        pnn = sn.get_specialized_name(sn.UnqualName(astnode.name.name), *quals)\n        return sn.UnqualName(pnn)\n\n\ndef get_package(\n    name: sn.Name, version: Optional[verutils.Version], schema: s_schema.Schema\n) -> ExtensionPackage:\n    filters = [\n        lambda schema, pkg: (\n            pkg.get_shortname(schema) == name\n        )\n    ]\n    # Version specs are always implicitly >=.\n    if version is not None:\n        filters.append(\n            lambda schema, pkg: (\n                pkg.get_version(schema) >= version\n            )\n        )\n\n    pkgs = list(schema.get_objects(\n        type=ExtensionPackage,\n        extra_filters=filters,\n    ))\n\n    if not pkgs:\n        dname = str(name)\n        if version is None:\n            raise errors.SchemaError(\n                f'cannot create extension {dname!r}:'\n                f' extension package {dname!r} does'\n                f' not exist'\n            )\n        else:\n            raise errors.SchemaError(\n                f'cannot create extension {dname!r}:'\n                f' extension package {dname!r} version'\n                f' {str(version)!r} does not exist'\n            )\n\n    pkgs.sort(key=lambda pkg: pkg.get_version(schema))\n    # If the exact version exists, then use it. Otherwise, take the\n    # newest version.\n    if pkgs[0].get_version(schema) == version:\n        return pkgs[0]\n    else:\n        return pkgs[-1]\n\n\ndef get_package_migrations(\n    name: sn.Name,\n    from_version: verutils.Version,\n    to_version: verutils.Version,\n    schema: s_schema.Schema,\n) -> list[ExtensionPackageMigration]:\n    # TODO: We need to figure out migration chains\n    #\n    # That will have some fiddliness, though, with when SQL extension\n    # upgrades and SQL scripts run?\n    filters = [\n        lambda schema, mig: (\n            mig.get_shortname(schema) == name\n        )\n    ]\n\n    migs = list(schema.get_objects(\n        type=ExtensionPackageMigration,\n        extra_filters=filters,\n    ))\n    # Build a graph of available migrations.  We make this\n    # complicated, just in case, but probably it will be simple.\n    # TODO: What about missing packages?\n    graph: dict[\n        verutils.Version,\n        list[tuple[verutils.Version, ExtensionPackageMigration]],\n    ] = {}\n    for mig in migs:\n        fromv = mig.get_from_version(schema)\n        tov = mig.get_to_version(schema)\n        graph.setdefault(fromv, []).append((tov, mig))\n    for tgts in graph.values():\n        tgts.sort()\n\n    # BFS it out\n    sources = {}\n    todo = collections.deque([from_version])\n    while todo:\n        cur_node = todo.popleft()\n        if cur_node == to_version:\n            break\n        for next_ver, mig in graph.get(cur_node, []):\n            if next_ver not in sources:\n                sources[next_ver] = cur_node, mig\n                todo.append(next_ver)\n    else:\n        dname = str(name)\n        raise errors.SchemaError(\n            f'cannot create migrate extension {dname!r} from '\n            f'{from_version} to {to_version}'\n        )\n\n    # Trace back the path\n    mig_path = []\n    while cur_node in sources:\n        cur_node, mig = sources[cur_node]\n        mig_path.append(mig)\n    mig_path.reverse()\n\n    return mig_path\n\n\n# XXX: Trying to CREATE/DROP these from within a transaction managed\n# to get me stuck getting \"Cannot serialize global DDL\" errors.\n#\n# I'm haven't fully investigated whether it is actually sensible to do\n# this kind of global command in a transaction, but we currently allow\n# it and some tests do it.\nclass CreateExtensionPackage(\n    ExtensionPackageCommand,\n    sd.CreateObject[ExtensionPackage],\n):\n    astnode = qlast.CreateExtensionPackage\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> CreateExtensionPackage:\n        if not context.stdmode and not context.testmode:\n            raise errors.UnsupportedFeatureError(\n                'user-defined extension packages are not supported yet',\n                span=astnode.span\n            )\n\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, CreateExtensionPackage)\n        assert isinstance(astnode, qlast.CreateExtensionPackage)\n        assert astnode.body.text is not None\n\n        parsed_version = verutils.parse_version(astnode.version.value)\n        cmd.set_attribute_value('version', parsed_version)\n        cmd.set_attribute_value('script', astnode.body.text)\n        cmd.set_attribute_value('builtin', context.stdmode)\n\n        if not cmd.has_attribute_value('internal'):\n            cmd.set_attribute_value('internal', False)\n\n        return cmd\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        assert isinstance(node, qlast.CreateExtensionPackage)\n        if op.property == 'script':\n            node.body = qlast.NestedQLBlock(\n                text=op.new_value,\n                commands=cast(\n                    list[qlast.DDLOperation],\n                    qlparser.parse_block(op.new_value)),\n            )\n        elif op.property == 'version':\n            node.version = qlast.Constant.string(\n                value=str(op.new_value),\n            )\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n\nclass DeleteExtensionPackage(\n    ExtensionPackageCommand,\n    sd.DeleteObject[ExtensionPackage],\n):\n    astnode = qlast.DropExtensionPackage\n\n    def _delete_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        if (\n            not context.stdmode\n            and not context.testmode\n            and self.scls.get_builtin(schema)\n        ):\n            name = self.scls.get_shortname(schema)\n            raise errors.UnsupportedFeatureError(\n                f\"cannot drop builtin extension package '{name}'\",\n                span=self.span,\n            )\n\n        return super()._delete_begin(schema, context)\n\n\nclass ExtensionPackageMigrationCommandContext(\n    sd.ObjectCommandContext[ExtensionPackageMigration],\n    s_anno.AnnotationSubjectCommandContext,\n):\n    pass\n\n\nclass ExtensionPackageMigrationCommand(\n    sd.GlobalObjectCommand[ExtensionPackageMigration],\n    s_anno.AnnotationSubjectCommand[ExtensionPackageMigration],\n    context_class=ExtensionPackageCommandContext,\n):\n\n    @classmethod\n    def _classname_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext\n    ) -> sn.UnqualName:\n        assert isinstance(astnode, (\n            qlast.CreateExtensionPackageMigration,\n            qlast.DropExtensionPackageMigration,\n        ))\n        from_version = verutils.parse_version(astnode.from_version.value)\n        to_version = verutils.parse_version(astnode.to_version.value)\n        quals = ['pkg-migration', str(from_version), str(to_version)]\n        pnn = sn.get_specialized_name(sn.UnqualName(astnode.name.name), *quals)\n        return sn.UnqualName(pnn)\n\n\nclass CreateExtensionPackageMigration(\n    ExtensionPackageMigrationCommand,\n    sd.CreateObject[ExtensionPackageMigration],\n):\n    astnode = qlast.CreateExtensionPackageMigration\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> CreateExtensionPackageMigration:\n        if not context.stdmode and not context.testmode:\n            raise errors.UnsupportedFeatureError(\n                'user-defined extension packages are not supported yet',\n                span=astnode.span\n            )\n\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, CreateExtensionPackageMigration)\n        assert isinstance(astnode, qlast.CreateExtensionPackageMigration)\n        assert astnode.body.text is not None\n\n        from_version = verutils.parse_version(astnode.from_version.value)\n        cmd.set_attribute_value('from_version', from_version)\n        to_version = verutils.parse_version(astnode.to_version.value)\n        cmd.set_attribute_value('to_version', to_version)\n        cmd.set_attribute_value('script', astnode.body.text)\n        cmd.set_attribute_value('builtin', context.stdmode)\n\n        if not cmd.has_attribute_value('internal'):\n            cmd.set_attribute_value('internal', False)\n\n        return cmd\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        assert isinstance(node, qlast.CreateExtensionPackageMigration)\n        if op.property == 'script':\n            node.body = qlast.NestedQLBlock(\n                text=op.new_value,\n                commands=cast(\n                    list[qlast.DDLOperation],\n                    qlparser.parse_block(op.new_value)),\n            )\n        elif op.property == 'from_version':\n            node.from_version = qlast.Constant.string(\n                value=str(op.new_value),\n            )\n        elif op.property == 'to_version':\n            node.to_version = qlast.Constant.string(\n                value=str(op.new_value),\n            )\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n\nclass DeleteExtensionPackageMigration(\n    ExtensionPackageMigrationCommand,\n    sd.DeleteObject[ExtensionPackageMigration],\n):\n    astnode = qlast.DropExtensionPackageMigration\n\n    def _delete_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        if (\n            not context.stdmode\n            and not context.testmode\n            and self.scls.get_builtin(schema)\n        ):\n            name = self.scls.get_shortname(schema)\n            raise errors.UnsupportedFeatureError(\n                f\"cannot drop builtin extension package migration '{name}'\",\n                span=self.span,\n            )\n\n        return super()._delete_begin(schema, context)\n\n\nclass ExtensionCommandContext(\n    sd.ObjectCommandContext[Extension],\n):\n    pass\n\n\nclass ExtensionCommand(\n    sd.ObjectCommand[Extension],\n    context_class=ExtensionCommandContext,\n):\n    def _get_dependencies(\n        self,\n        pkg: ExtensionPackage,\n        schema: s_schema.Schema,\n    ) -> list[Extension]:\n        deps = []\n        for dep_name in pkg.get_dependencies(schema):\n            if '>=' not in dep_name:\n                builtin = 'built-in ' if pkg.get_builtin(schema) else ''\n                raise errors.SchemaError(\n                    f'{builtin}extension {self.classname} missing '\n                    f'version for {dep_name}')\n            dep_name, dep_version_s = dep_name.split('>=')\n            dep = schema.get_global(Extension, dep_name, default=None)\n            if not dep:\n                raise errors.SchemaError(\n                    f'cannot create extension {self.get_displayname()!r} '\n                    f'version {str(pkg.get_version(schema))!r}: '\n                    f'it depends on extension {dep_name} which has not been'\n                    f' created'\n                )\n            dep_version = verutils.parse_version(dep_version_s)\n            real_version = dep.get_package(schema).get_version(schema)\n            if dep_version > real_version:\n                raise errors.SchemaError(\n                    f'cannot create extension {self.get_displayname()!r} '\n                    f'version {str(pkg.get_version(schema))!r}: '\n                    f'it depends on extension {dep_name}, but the wrong '\n                    f'version is installed: {real_version} is present but '\n                    f'{dep_version} is required'\n                )\n\n            deps.append(dep)\n\n        return deps\n\n\n@contextlib.contextmanager\ndef _extension_mode(context: sd.CommandContext) -> Iterator[None]:\n    # TODO: We'll want to be a bit more discriminating once we support\n    # user extensions, and not set stable_ids then?\n    stable_ids = context.stable_ids\n    testmode = context.testmode\n    declarative = context.declarative\n    context.stable_ids = True\n    context.testmode = True\n    context.declarative = False\n    try:\n        yield\n    finally:\n        context.stable_ids = stable_ids\n        context.testmode = testmode\n        context.declarative = declarative\n\n\nclass CreateExtension(\n    ExtensionCommand,\n    sd.CreateObject[Extension],\n):\n    astnode = qlast.CreateExtension\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        with _extension_mode(context):\n            return super().apply(schema, context)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext\n    ) -> CreateExtension:\n        assert isinstance(astnode, qlast.CreateExtension)\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, CreateExtension)\n\n        if astnode.version is not None:\n            parsed_version = verutils.parse_version(astnode.version.value)\n            cmd.set_attribute_value('version', parsed_version)\n\n        return cmd\n\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._create_begin(schema, context)\n\n        if not context.canonical:\n            package = self.scls.get_package(schema)\n\n            module = package.get_ext_module(schema)\n            if module:\n                module_name = sn.UnqualName(module)\n                if module_name.get_root_module_name() != s_schema.EXT_MODULE:\n                    builtin = 'built-in ' if package.get_builtin(schema) else ''\n                    raise errors.SchemaError(\n                        f'{builtin}extension {self.classname} has invalid '\n                        f'module \"{module}\": '\n                        f'extension modules must begin with \"ext::\"'\n                    )\n\n            script = package.get_script(schema)\n            if script:\n                block, _ = qlparser.parse_extension_package_body_block(script)\n                for subastnode in block.commands:\n                    subcmd = sd.compile_ddl(\n                        schema, subastnode, context=context)\n                    if subcmd is not None:\n                        self.add(subcmd)\n\n        return schema\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n        pkg: ExtensionPackage\n\n        if pkg_attr := self.get_attribute_value('package'):\n            pkg = pkg_attr.resolve(schema)\n        else:\n            # If we're restoring a dump ignore the extension package version\n            # as the current EdgeDB might have a different version available\n            # and we don't have a way to select specific versions yet.\n            #\n            # Use `compat_ver` as a way to detect that we're working with a\n            # dump rather than some other operation.\n            if context.compat_ver is not None:\n                version = None\n            else:\n                version = self.get_attribute_value('version')\n\n            pkg = get_package(self.classname, version, schema)\n\n        self.discard_attribute('version')\n\n        self.set_attribute_value('package', pkg)\n\n        deps = self._get_dependencies(pkg, schema)\n        self.set_attribute_value('dependencies', deps)\n\n        return schema\n\n    # XXX: I think this is wrong, but it might not matter ever.\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        node = super()._get_ast(schema, context, parent_node=parent_node)\n        assert isinstance(node, qlast.CreateExtension)\n        pkg = self.get_resolved_attribute_value(\n            'package', schema=schema, context=context)\n        # When performing dumps we don't want to include the extension version\n        # as we're not guaranteed that the same version will be avaialble when\n        # restoring the dump. We also have no mechanism of installing a specific\n        # extension version, yet.\n        if context.include_ext_version:\n            node.version = qlast.Constant.string(\n                value=str(pkg.get_version(schema))\n            )\n        return node\n\n\nclass AlterExtension(\n    ExtensionCommand,\n    sd.AlterObject[Extension],\n):\n    astnode = qlast.AlterExtension\n\n    to_version = struct.Field(verutils.Version, default=None)\n    migration = struct.Field(ExtensionPackageMigration, default=None)\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        with _extension_mode(context):\n            return super().apply(schema, context)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext\n    ) -> AlterExtension:\n        assert isinstance(astnode, qlast.AlterExtension)\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, AlterExtension)\n\n        cmd.to_version = verutils.parse_version(astnode.to_version.value)\n\n        return cmd\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        # HACK: AlterObject insists on filtering out any ALTERs\n        # without subcommands, but we don't have any, so skip\n        # AlterObject.\n        return super(sd.AlterObject, self)._get_ast(\n            schema, context, parent_node=parent_node\n        )\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        if op.property == 'package':\n            assert isinstance(node, qlast.AlterExtension)\n            package = op.new_value.resolve(schema)\n            node.to_version = qlast.Constant.string(\n                str(package.get_version(schema))\n            )\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n        pkg: ExtensionPackage\n\n        if pkg_attr := self.get_attribute_value('package'):\n            pkg = pkg_attr.resolve(schema)\n        else:\n            assert self.to_version\n            pkg = get_package(self.classname, self.to_version, schema)\n\n        self.set_attribute_value('package', pkg)\n\n        deps = self._get_dependencies(pkg, schema)\n        self.set_attribute_value('dependencies', deps)\n\n        return schema\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        from_version = self.scls.get_package(schema).get_version(schema)\n        schema = super()._alter_begin(schema, context)\n\n        if not context.canonical:\n            assert self.to_version\n\n            if not self.migration:\n                migrations = get_package_migrations(\n                    self.classname, from_version, self.to_version, schema\n                )\n            else:\n                migrations = [self.migration]\n\n            if len(migrations) == 1:\n                self.migration = migrations[0]\n\n                script = self.migration.get_script(schema)\n                if script:\n                    block, _ = qlparser.parse_extension_package_body_block(\n                        script)\n                    for subastnode in block.commands:\n                        subcmd = sd.compile_ddl(\n                            schema, subastnode, context=context)\n                        if subcmd is not None:\n                            self.add(subcmd)\n            else:\n                for migration in migrations:\n                    self.add(AlterExtension(\n                        classname=self.classname,\n                        to_version=migration.get_to_version(schema),\n                        migration=migration,\n                    ))\n\n        return schema\n\n\nclass DeleteExtension(\n    ExtensionCommand,\n    sd.DeleteObject[Extension],\n):\n\n    astnode = qlast.DropExtension\n\n    def _delete_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        module = self.scls.get_package(schema).get_ext_module(schema)\n        schema = super()._delete_begin(schema, context)\n\n        if context.canonical or not module:\n            return schema\n\n        # If the extension included a module, delete everything in it.\n        from . import ddl as s_ddl\n\n        module_name = sn.UnqualName(module)\n\n        def _name_in_mod(name: sn.Name) -> bool:\n            return (\n                (\n                    isinstance(name, sn.QualName)\n                    and (\n                        name.module == module\n                        or name.module.startswith(module + '::')\n                    )\n                )\n                or (\n                    isinstance(name, sn.UnqualName)\n                    and (\n                        name == module_name\n                        or name.name.startswith(module + '::')\n                    )\n                )\n            )\n\n        # Clean up the casts separately because we can't keep them in\n        # our own module, so we keep them in __ext_casts__. (Cast\n        # names are derived solely from the names of their from and to\n        # types, which means that if we have a cast between ext::a::T\n        # and ext::b::S, we wouldn't have a way to distinguish which\n        # is should be.)\n        casts_cleanup: list[sd.Command] = []\n        for obj in schema.get_objects(\n            included_modules=(sn.UnqualName('__ext_casts__'),),\n            type=s_casts.Cast,\n        ):\n            if (\n                _name_in_mod(obj.get_from_type(schema).get_name(schema))\n                or _name_in_mod(obj.get_to_type(schema).get_name(schema))\n            ):\n                drop = obj.init_delta_command(\n                    schema,\n                    sd.DeleteObject,\n                )\n                casts_cleanup.append(drop)\n\n        # Similarly, index matches are kept in __ext_index_matches__. We can\n        # remove them first since nothing else depends on them.\n        for im in schema.get_objects(\n            included_modules=(sn.UnqualName('__ext_index_matches__'),),\n            type=s_indexes.IndexMatch,\n        ):\n            if (\n                _name_in_mod(im.get_valid_type(schema).get_name(schema))\n                or _name_in_mod(im.get_index(schema).get_name(schema))\n            ):\n                self.add(im.init_delta_command(schema, sd.DeleteObject))\n\n        def filt(schema: s_schema.Schema, obj: so.Object) -> bool:\n            return not _name_in_mod(obj.get_name(schema)) or obj == self.scls\n\n        # We handle deleting the module contents in a heavy-handed way:\n        # do a schema diff.\n        module_names = [\n            m.get_name(schema)\n            for m in schema.get_modules()\n            if _name_in_mod(m.get_name(schema))\n        ]\n        delta = s_ddl.delta_schemas(\n            schema, schema,\n            included_modules=module_names,\n            schema_b_filters=[filt],\n            include_extensions=True,\n            linearize_delta=True,\n        )\n        # The output of delta_schemas is really just intended to be\n        # dumped as an AST. So, sigh, just do that, and then read it\n        # back.\n        #\n        # This is horrific, but it does actually work and is built\n        # around codepaths that are heavily tested.\n        from . import ddl\n        for subast in ddl.ddlast_from_delta(None, schema, delta):\n            # We want to clean the casts right before we're cleaning the\n            # scalar types. Cleaning casts earlier may cause issues with\n            # functions that use casts in their signatures as part of the\n            # default expression.\n            if casts_cleanup and isinstance(subast, qlast.DropScalarType):\n                self.update(casts_cleanup)\n                casts_cleanup.clear()\n\n            self.add(sd.compile_ddl(schema, subast, context=context))\n\n        return schema\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        with _extension_mode(context):\n            return super().apply(schema, context)\n"
  },
  {
    "path": "edb/schema/functions.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport abc\nimport types\nimport uuid\nimport builtins\nfrom typing import (\n    Any, Optional, TypeVar, Iterable, Mapping, Sequence, cast, TYPE_CHECKING,\n)\n\nfrom edb import errors\n\nfrom edb.common import ast\nfrom edb.common import parsing\nfrom edb.common import struct\nfrom edb.common import verutils\nfrom edb.common import lru\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.edgeql import qltypes as ft\nfrom edb.edgeql import parser as qlparser\nfrom edb.edgeql import qltypes\nfrom edb.common import uuidgen\n\nfrom . import annos as s_anno\nfrom . import delta as sd\nfrom . import expr as s_expr\nfrom . import globals as s_globals\nfrom . import name as sn\nfrom . import objects as so\nfrom . import permissions as s_permissions\nfrom . import referencing\nfrom . import types as s_types\nfrom . import utils\nfrom . import schema as s_schema\n\n\nif TYPE_CHECKING:\n    from edb.edgeql.compiler import context as qlcontext\n    from edb.ir import ast as irast\n\n\nFUNC_NAMESPACE = uuidgen.UUID('80cd3b19-bb51-4659-952d-6bb03e3347d7')\n\n\ndef param_as_str(\n    schema: s_schema.Schema,\n    param: ParameterDesc | Parameter,\n) -> str:\n    ret = []\n    kind = param.get_kind(schema)\n    typemod = param.get_typemod(schema)\n    default = param.get_default(schema)\n\n    if kind is not ft.ParameterKind.PositionalParam:\n        ret.append(kind.to_edgeql())\n        ret.append(' ')\n\n    ret.append(f'{param.get_parameter_name(schema)}: ')\n\n    if typemod is not ft.TypeModifier.SingletonType:\n        ret.append(typemod.to_edgeql())\n        ret.append(' ')\n\n    paramt: s_types.Type | s_types.TypeShell[s_types.Type]\n    if isinstance(param, ParameterDesc):\n        paramt = param.get_type_shell(schema)\n    else:\n        paramt = param.get_type(schema)\n\n    ret.append(paramt.get_displayname(schema))\n\n    if default is not None:\n        ret.append(f'={default.text}')\n\n    return ''.join(ret)\n\n\ndef canonical_param_sort[ParameterLike_T: \"ParameterLike\"](\n    schema: s_schema.Schema,\n    params: Iterable[ParameterLike_T],\n) -> tuple[ParameterLike_T, ...]:\n\n    canonical_order = []\n    named = []\n    variadic = None\n\n    for param in params:\n        param_kind = param.get_kind(schema)\n\n        if param_kind is ft.ParameterKind.PositionalParam:\n            canonical_order.append(param)\n        elif param_kind is ft.ParameterKind.NamedOnlyParam:\n            named.append(param)\n        else:\n            variadic = param\n\n    if variadic is not None:\n        canonical_order.append(variadic)\n\n    if named:\n        named.sort(key=lambda p: p.get_name(schema))\n        named.extend(canonical_order)\n        canonical_order = named\n\n    return tuple(canonical_order)\n\n\ndef param_is_inherited(\n    schema: s_schema.Schema,\n    func: CallableObject,\n    param: ParameterLike,\n) -> bool:\n    qualname = sn.get_specialized_name(\n        sn.UnqualName(param.get_parameter_name(schema)),\n        str(func.get_name(schema)),\n    )\n    param_name = param.get_name(schema)\n    assert isinstance(param_name, sn.QualName)\n    return qualname != param_name.name\n\n\nclass ParameterLike:\n\n    def get_parameter_name(self, schema: s_schema.Schema) -> str:\n        raise NotImplementedError\n\n    def get_name(self, schema: s_schema.Schema) -> sn.Name:\n        raise NotImplementedError\n\n    def get_kind(self, _: s_schema.Schema) -> ft.ParameterKind:\n        raise NotImplementedError\n\n    def get_default(self, _: s_schema.Schema) -> Optional[s_expr.Expression]:\n        raise NotImplementedError\n\n    def get_type(self, _: s_schema.Schema) -> s_types.Type:\n        raise NotImplementedError\n\n    def get_typemod(self, _: s_schema.Schema) -> ft.TypeModifier:\n        raise NotImplementedError\n\n    def as_str(self, schema: s_schema.Schema) -> str:\n        raise NotImplementedError\n\n\n# Non-schema description of a parameter.\nclass ParameterDesc(ParameterLike):\n\n    num: int\n    name: sn.Name\n    default: Optional[s_expr.Expression]\n    type: s_types.TypeShell[s_types.Type]\n    typemod: ft.TypeModifier\n    kind: ft.ParameterKind\n\n    def __init__(\n        self,\n        *,\n        num: int,\n        name: sn.Name,\n        default: Optional[s_expr.Expression],\n        type: s_types.TypeShell[s_types.Type],\n        typemod: ft.TypeModifier,\n        kind: ft.ParameterKind,\n    ) -> None:\n        self.num = num\n        self.name = name\n        self.default = default\n        self.type = type\n        self.typemod = typemod\n        self.kind = kind\n\n    @classmethod\n    def from_ast(\n        cls,\n        schema: s_schema.Schema,\n        modaliases: Mapping[Optional[str], str],\n        num: int,\n        astnode: qlast.FuncParamDecl,\n    ) -> ParameterDesc:\n\n        paramd = None\n        if astnode.default is not None:\n            paramd = s_expr.Expression.from_ast(\n                astnode.default, schema, modaliases, as_fragment=True)\n\n        paramt_ast = astnode.type\n\n        if astnode.kind is ft.ParameterKind.VariadicParam:\n            paramt_ast = qlast.TypeName(\n                maintype=qlast.ObjectRef(\n                    name='array',\n                ),\n                subtypes=[paramt_ast],\n            )\n\n        paramt = utils.ast_to_type_shell(\n            paramt_ast,\n            metaclass=s_types.Type,\n            modaliases=modaliases,\n            schema=schema,\n        )\n\n        return cls(\n            num=num,\n            name=sn.UnqualName(astnode.name),\n            type=paramt,\n            typemod=astnode.typemod,\n            kind=astnode.kind,\n            default=paramd\n        )\n\n    def get_parameter_name(self, schema: s_schema.Schema) -> str:\n        return str(self.name)\n\n    def get_name(self, schema: s_schema.Schema) -> sn.Name:\n        return self.name\n\n    def get_kind(self, _: s_schema.Schema) -> ft.ParameterKind:\n        return self.kind\n\n    def get_default(self, _: s_schema.Schema) -> Optional[s_expr.Expression]:\n        return self.default\n\n    def get_type(self, schema: s_schema.Schema) -> s_types.Type:\n        return self.type.resolve(schema)\n\n    def get_type_shell(\n        self,\n        schema: s_schema.Schema,\n    ) -> s_types.TypeShell[s_types.Type]:\n        return self.type\n\n    def get_typemod(self, _: s_schema.Schema) -> ft.TypeModifier:\n        return self.typemod\n\n    def as_str(self, schema: s_schema.Schema) -> str:\n        return param_as_str(schema, self)\n\n    @classmethod\n    def from_create_delta(\n        cls,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        cmd: CreateParameter,\n    ) -> tuple[s_schema.Schema, ParameterDesc]:\n        props = cmd.get_attributes(schema, context)\n        props['name'] = Parameter.paramname_from_fullname(props['name'])\n        if not isinstance(props['type'], s_types.TypeShell):\n            paramt = props['type'].as_shell(schema)\n        else:\n            paramt = props['type']\n        return schema, cls(\n            num=props['num'],\n            name=props['name'],\n            type=paramt,\n            typemod=props['typemod'],\n            kind=props['kind'],\n            default=props.get('default'),\n        )\n\n    def get_fqname(\n        self,\n        schema: s_schema.Schema,\n        func_fqname: sn.QualName,\n    ) -> sn.QualName:\n        return sn.QualName(\n            func_fqname.module,\n            sn.get_specialized_name(self.get_name(schema), str(func_fqname))\n        )\n\n    def as_create_delta(\n        self,\n        schema: s_schema.Schema,\n        func_fqname: sn.QualName,\n        *,\n        context: sd.CommandContext,\n    ) -> sd.CreateObject[Parameter]:\n        CreateParameter = sd.get_object_command_class_or_die(\n            sd.CreateObject, Parameter)\n\n        param_name = self.get_fqname(schema, func_fqname)\n\n        cmd = CreateParameter(classname=param_name)\n        cmd.set_attribute_value('name', param_name)\n        cmd.set_attribute_value('type', self.type)\n\n        for attr in ('num', 'typemod', 'kind', 'default'):\n            cmd.set_attribute_value(attr, getattr(self, attr))\n\n        return cmd\n\n\ndef _params_are_all_required_singletons(\n    params: Sequence[ParameterLike],\n    schema: s_schema.Schema,\n) -> bool:\n    return all(\n        param.get_kind(schema) is not ft.ParameterKind.VariadicParam\n        and param.get_typemod(schema) is ft.TypeModifier.SingletonType\n        and param.get_default(schema) is None\n        for param in params\n    )\n\n\ndef make_func_param(\n    *,\n    name: str,\n    type: qlast.TypeExpr,\n    typemod: qltypes.TypeModifier = qltypes.TypeModifier.SingletonType,\n    kind: qltypes.ParameterKind,\n    default: Optional[qlast.Expr] = None,\n) -> qlast.FuncParamDecl:\n    # If the param is variadic, strip the array from the type in the schema\n    if kind is ft.ParameterKind.VariadicParam:\n        assert (\n            isinstance(type, qlast.TypeName)\n            and isinstance(type.maintype, qlast.ObjectRef)\n            and type.maintype.name == 'array'\n            and type.subtypes\n        )\n        type = type.subtypes[0]\n\n    return qlast.FuncParamDecl(\n        name=name,\n        type=type,\n        typemod=typemod,\n        kind=kind,\n        default=default,\n    )\n\n\nclass Parameter(\n    so.ObjectFragment,\n    so.Object,  # Help reflection figure out the right db MRO\n    ParameterLike,\n    qlkind=ft.SchemaObjectClass.PARAMETER,\n    data_safe=True,\n):\n\n    num = so.SchemaField(\n        int, compcoef=0.4)\n\n    default = so.SchemaField(\n        s_expr.Expression, default=None, compcoef=0.4)\n\n    type = so.SchemaField(\n        s_types.Type, compcoef=0.4)\n\n    typemod = so.SchemaField(\n        ft.TypeModifier,\n        default=ft.TypeModifier.SingletonType,\n        coerce=True, compcoef=0.4)\n\n    kind = so.SchemaField(\n        ft.ParameterKind, coerce=True, compcoef=0.4)\n\n    @classmethod\n    def paramname_from_fullname(cls, fullname: sn.Name) -> str:\n        parts = str(fullname.name).split('@', 1)\n        if len(parts) == 2:\n            return sn.unmangle_name(parts[0])\n        else:\n            return fullname.name\n\n    def get_verbosename(\n        self,\n        schema: s_schema.Schema,\n        *,\n        with_parent: bool = False,\n    ) -> str:\n        vn = super().get_verbosename(schema)\n        if with_parent:\n            pfns = [r for r in schema.get_referrers(self)\n                    if isinstance(r, CallableObject)]\n            if pfns:\n                pvn = pfns[0].get_verbosename(schema, with_parent=True)\n                return f'{vn} of {pvn}'\n            else:\n                return vn\n        else:\n            return vn\n\n    @classmethod\n    def get_shortname_static(cls, name: sn.Name) -> sn.QualName:\n        assert isinstance(name, sn.QualName)\n        return sn.QualName(\n            module='__',\n            name=cls.paramname_from_fullname(name),\n        )\n\n    @classmethod\n    def get_displayname_static(cls, name: sn.Name) -> str:\n        shortname = cls.get_shortname_static(name)\n        return shortname.name\n\n    def get_parameter_name(self, schema: s_schema.Schema) -> str:\n        fullname = self.get_name(schema)\n        return self.paramname_from_fullname(fullname)\n\n    def get_ir_default(\n        self, *, schema: s_schema.Schema, context: sd.CommandContext,\n    ) -> irast.Statement:\n        from edb.ir import utils as irutils\n\n        defexpr = self.get_default(schema)\n        assert defexpr is not None\n        defexpr = defexpr.compiled(\n            as_fragment=True,\n            schema=schema,\n            context=context,\n        )\n        ir = defexpr.irast\n        if not irutils.is_const(ir.expr):\n            raise ValueError('expression not constant')\n        return ir\n\n    def as_str(self, schema: s_schema.Schema) -> str:\n        return param_as_str(schema, self)\n\n    @classmethod\n    def compare_field_value[T](\n        cls,\n        field: so.Field[builtins.type[T]],\n        our_value: T,\n        their_value: T,\n        *,\n        our_schema: s_schema.Schema,\n        their_schema: s_schema.Schema,\n        context: so.ComparisonContext,\n    ) -> float:\n        # Only compare the actual param name, not the full name.\n        if field.name == 'name':\n            assert isinstance(our_value, sn.Name)\n            assert isinstance(their_value, sn.Name)\n            if (\n                cls.paramname_from_fullname(our_value) ==\n                cls.paramname_from_fullname(their_value)\n            ):\n                return 1.0\n\n        return super().compare_field_value(\n            field,\n            our_value,\n            their_value,\n            our_schema=our_schema,\n            their_schema=their_schema,\n            context=context,\n        )\n\n    def get_ast(self, schema: s_schema.Schema) -> qlast.FuncParamDecl:\n        default = self.get_default(schema)\n        kind = self.get_kind(schema)\n\n        return make_func_param(\n            name=self.get_parameter_name(schema),\n            type=utils.typeref_to_ast(schema, self.get_type(schema)),\n            typemod=self.get_typemod(schema),\n            kind=kind,\n            default=default.parse() if default else None,\n        )\n\n\nclass CallableCommandContext(sd.ObjectCommandContext['CallableObject'],\n                             s_anno.AnnotationSubjectCommandContext):\n    pass\n\n\nclass ParameterCommandContext(sd.ObjectCommandContext[Parameter]):\n    pass\n\n\n# type ignore below, because making Parameter\n# a referencing.ReferencedObject breaks the code\nclass ParameterCommand(\n    referencing.ReferencedObjectCommandBase[Parameter],  # type: ignore\n    context_class=ParameterCommandContext,\n    referrer_context_class=CallableCommandContext\n):\n\n    is_strong_ref = struct.Field(bool, default=True)\n\n    def get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        # ParameterCommand cannot have its own AST because it is a\n        # side-effect of a FunctionCommand.\n        return None\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n        return s_types.materialize_type_in_attribute(\n            schema, context, self, 'type')\n\n    def compile_expr_field(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: s_expr.Expression,\n        track_schema_ref_exprs: bool=False,\n    ) -> s_expr.CompiledExpression:\n        if field.name == 'default':\n            return value.compiled(\n                schema=schema,\n                as_fragment=True,\n                options=qlcompiler.CompilerOptions(\n                    modaliases=context.modaliases,\n                    schema_object_context=self.get_schema_metaclass(),\n                    apply_query_rewrites=not context.stable_ids,\n                    track_schema_ref_exprs=track_schema_ref_exprs,\n                ),\n                context=context,\n            )\n        else:\n            return super().compile_expr_field(\n                schema, context, field, value, track_schema_ref_exprs)\n\n    def get_dummy_expr_field_value(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: Any,\n    ) -> Optional[s_expr.Expression]:\n        if field.name == 'default':\n            type = self.scls.get_type(schema)\n            return s_types.type_dummy_expr(type, schema)\n        else:\n            raise NotImplementedError(f'unhandled field {field.name!r}')\n\n\nclass CreateParameter(ParameterCommand, sd.CreateObject[Parameter]):\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        for sub in cmd.get_subcommands(type=sd.AlterObjectProperty):\n            if sub.property == 'default':\n                sub.new_value = [sub.new_value]\n\n        return cmd\n\n\nclass DeleteParameter(ParameterCommand, sd.DeleteObject[Parameter]):\n    def _delete_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._delete_begin(schema, context)\n        if not context.canonical:\n            typ = self.scls.get_type(schema)\n            if op := typ.as_type_delete_if_unused(schema):\n                self.add_caused(op)\n        return schema\n\n\nclass RenameParameter(ParameterCommand, sd.RenameObject[Parameter]):\n    pass\n\n\nclass AlterParameter(ParameterCommand, sd.AlterObject[Parameter]):\n    pass\n\n\nclass ParameterLikeList(abc.ABC):\n\n    @abc.abstractmethod\n    def get_by_name(\n        self,\n        schema: s_schema.Schema,\n        name: str,\n    ) -> Optional[ParameterLike]:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def as_str(self, schema: s_schema.Schema) -> str:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def has_polymorphic(self, schema: s_schema.Schema) -> bool:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def has_set_of(self, schema: s_schema.Schema) -> bool:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def has_objects(self, schema: s_schema.Schema) -> bool:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def find_named_only(\n        self,\n        schema: s_schema.Schema,\n    ) -> Mapping[str, ParameterLike]:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def find_variadic(\n        self,\n        schema: s_schema.Schema,\n    ) -> Optional[ParameterLike]:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def has_required_params(\n        self,\n        schema: s_schema.Schema,\n    ) -> bool:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def objects(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[ParameterLike, ...]:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def get_in_canonical_order(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[ParameterLike, ...]:\n        raise NotImplementedError\n\n\nclass FuncParameterList(so.ObjectList[Parameter], ParameterLikeList):\n\n    def get_by_name(\n        self,\n        schema: s_schema.Schema,\n        name: str,\n    ) -> Optional[Parameter]:\n        for param in self.objects(schema):\n            if param.get_parameter_name(schema) == name:\n                return param\n        return None\n\n    def as_str(self, schema: s_schema.Schema) -> str:\n        ret = []\n        for param in self.objects(schema):\n            ret.append(param.as_str(schema))\n        return '(' + ', '.join(ret) + ')'\n\n    def has_polymorphic(self, schema: s_schema.Schema) -> bool:\n        return any(\n            p.get_type(schema).is_polymorphic(schema)\n            for p in self.objects(schema)\n        )\n\n    def has_type_mod(\n        self, schema: s_schema.Schema, mod: ft.TypeModifier\n    ) -> bool:\n        return any(p.get_typemod(schema) is mod for p in self.objects(schema))\n\n    def has_set_of(self, schema: s_schema.Schema) -> bool:\n        return self.has_type_mod(schema, ft.TypeModifier.SetOfType)\n\n    def has_objects(self, schema: s_schema.Schema) -> bool:\n        return any(\n            p.get_type(schema).is_object_type() for p in self.objects(schema)\n        )\n\n    def find_named_only(\n        self,\n        schema: s_schema.Schema,\n    ) -> Mapping[str, Parameter]:\n        named = {}\n        for param in self.objects(schema):\n            if param.get_kind(schema) is ft.ParameterKind.NamedOnlyParam:\n                named[param.get_parameter_name(schema)] = param\n\n        return types.MappingProxyType(named)\n\n    def find_variadic(self, schema: s_schema.Schema) -> Optional[Parameter]:\n        for param in self.objects(schema):\n            if param.get_kind(schema) is ft.ParameterKind.VariadicParam:\n                return param\n        return None\n\n    def has_required_params(self, schema: s_schema.Schema) -> bool:\n        return any(\n            param.get_kind(schema) is not ft.ParameterKind.VariadicParam\n            and param.get_default(schema) is None\n            for param in self.objects(schema)\n        )\n\n    def get_in_canonical_order(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[Parameter, ...]:\n        return canonical_param_sort(schema, self.objects(schema))\n\n    def get_ast(self, schema: s_schema.Schema) -> list[qlast.FuncParamDecl]:\n        result = []\n        for param in self.objects(schema):\n            result.append(param.get_ast(schema))\n        return result\n\n    @classmethod\n    def compare_values(\n        cls,\n        ours_o: so.ObjectCollection[Parameter],\n        theirs_o: so.ObjectCollection[Parameter],\n        *,\n        our_schema: s_schema.Schema,\n        their_schema: s_schema.Schema,\n        context: so.ComparisonContext,\n        compcoef: float,\n    ) -> float:\n        ours = list(ours_o.objects(our_schema))\n        theirs = list(theirs_o.objects(their_schema))\n\n        # Because parameter lists can't currently be ALTERed, any\n        # changes are catastrophic, so return compcoef on any mismatch\n        # at all.\n        if len(ours) != len(theirs):\n            return compcoef\n        for param1, param2 in zip(ours, theirs):\n            coef = param1.compare(\n                param2, our_schema=our_schema,\n                their_schema=their_schema, context=context)\n            if coef != 1.0:\n                return compcoef\n\n        return 1.0\n\n\nclass VolatilitySubject(so.Object):\n\n    volatility = so.SchemaField(\n        ft.Volatility, default=ft.Volatility.Volatile,\n        compcoef=0.4, coerce=True, allow_ddl_set=True)\n\n\nclass CallableLike:\n    \"\"\"A minimal callable object interface required by multidispatch.\"\"\"\n\n    def has_inlined_defaults(self, schema: s_schema.Schema) -> bool:\n        raise NotImplementedError\n\n    def get_params(self, schema: s_schema.Schema) -> ParameterLikeList:\n        raise NotImplementedError\n\n    def get_return_type(self, schema: s_schema.Schema) -> s_types.Type:\n        raise NotImplementedError\n\n    def get_return_typemod(self, schema: s_schema.Schema) -> ft.TypeModifier:\n        raise NotImplementedError\n\n    def get_signature_as_str(self, schema: s_schema.Schema) -> str:\n        raise NotImplementedError\n\n    def get_verbosename(self, schema: s_schema.Schema) -> str:\n        raise NotImplementedError\n\n    def get_abstract(self, schema: s_schema.Schema) -> bool:\n        raise NotImplementedError\n\n\nCallableObjectT = TypeVar('CallableObjectT', bound='CallableObject')\n\n\nclass CallableObject(\n    so.QualifiedObject,\n    s_anno.AnnotationSubject,\n    CallableLike,\n):\n\n    params = so.SchemaField(\n        FuncParameterList,\n        coerce=True, compcoef=0.4, default=so.DEFAULT_CONSTRUCTOR,\n        inheritable=False, simpledelta=False)\n\n    return_type = so.SchemaField(\n        s_types.Type, compcoef=0.2)\n\n    return_typemod = so.SchemaField(\n        ft.TypeModifier, compcoef=0.4, coerce=True)\n\n    abstract = so.SchemaField(\n        bool, default=False, inheritable=False, compcoef=0.909)\n\n    impl_is_strict = so.SchemaField(\n        bool, default=True, compcoef=0.4)\n\n    # Kind of a hack: indicates that when possible we should pass arguments\n    # to this function as a subquery-as-an-expression. This is important for\n    # functions that see use in ORDER BY clauses that need indexes.\n    # The compilation strategy this asks for /should/ work in general,\n    # but I didn't want to make a major codegen change in an rc3.\n    # We should consider doing this a different way.\n    prefer_subquery_args = so.SchemaField(\n        bool, default=False, compcoef=0.9)\n\n    # Some set of calls are allowed in singleton expressions\n    is_singleton_set_of = so.SchemaField(\n        bool, default=False, compcoef=0.4)\n\n    def as_create_delta(\n        self: CallableObjectT,\n        schema: s_schema.Schema,\n        context: so.ComparisonContext,\n    ) -> sd.CreateObject[CallableObjectT]:\n        delta = super().as_create_delta(schema, context)\n\n        new_params = self.get_params(schema).objects(schema)\n        for p in new_params:\n            if not param_is_inherited(schema, self, p):\n                delta.add_prerequisite(\n                    p.as_create_delta(schema=schema, context=context),\n                )\n\n        return delta\n\n    def as_alter_delta(\n        self: CallableObjectT,\n        other: CallableObjectT,\n        *,\n        self_schema: s_schema.Schema,\n        other_schema: s_schema.Schema,\n        confidence: float,\n        context: so.ComparisonContext,\n    ) -> sd.ObjectCommand[CallableObjectT]:\n        delta = super().as_alter_delta(\n            other,\n            self_schema=self_schema,\n            other_schema=other_schema,\n            confidence=confidence,\n            context=context,\n        )\n\n        old_params = self.get_params(self_schema).objects(self_schema)\n        oldcoll = [\n            p for p in old_params\n            if not param_is_inherited(self_schema, self, p)\n        ]\n\n        new_params = other.get_params(other_schema).objects(other_schema)\n        newcoll = [\n            p for p in new_params\n            if not param_is_inherited(other_schema, other, p)\n        ]\n\n        delta.add_prerequisite(\n            sd.delta_objects(\n                oldcoll,\n                newcoll,\n                sclass=Parameter,\n                context=context,\n                old_schema=self_schema,\n                new_schema=other_schema,\n            ),\n        )\n\n        return delta\n\n    def as_delete_delta(\n        self: CallableObjectT,\n        *,\n        schema: s_schema.Schema,\n        context: so.ComparisonContext,\n    ) -> sd.ObjectCommand[CallableObjectT]:\n        delta = super().as_delete_delta(schema=schema, context=context)\n        old_params = self.get_params(schema).objects(schema)\n        for p in old_params:\n            if not param_is_inherited(schema, self, p):\n                delta.add(p.as_delete_delta(schema=schema, context=context))\n\n        return delta\n\n    @classmethod\n    def _get_fqname_quals(\n        cls,\n        schema: s_schema.Schema,\n        params: list[ParameterDesc],\n    ) -> tuple[str, ...]:\n        quals: list[str] = []\n        canonical_order = canonical_param_sort(schema, params)\n        for param in canonical_order:\n            pt = param.get_type_shell(schema)\n\n            pt_id = str(pt.get_name(schema))\n            quals.append(pt_id)\n            pk = param.get_kind(schema)\n            if pk is ft.ParameterKind.NamedOnlyParam:\n                quals.append(f'$NO-{param.get_name(schema)}-{pt_id}$')\n            elif pk is ft.ParameterKind.VariadicParam:\n                quals.append(f'$V$')\n\n        return tuple(quals)\n\n    @classmethod\n    def get_fqname(\n        cls,\n        schema: s_schema.Schema,\n        shortname: sn.QualName,\n        params: list[ParameterDesc],\n        *extra_quals: str,\n    ) -> sn.QualName:\n\n        quals = cls._get_fqname_quals(schema, params)\n        return sn.QualName(\n            module=shortname.module,\n            name=sn.get_specialized_name(shortname, *(quals + extra_quals)))\n\n    def has_inlined_defaults(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_blocking_ref(\n        self,\n        schema: s_schema.Schema,\n        reference: so.Object,\n    ) -> bool:\n        # Parameters cannot be deleted via DDL syntax,\n        # so the only possible scenario is the deletion of\n        # the host function.\n        return not isinstance(reference, Parameter)\n\n\nclass ParametrizedCommand(sd.ObjectCommand[so.Object_T]):\n    def _get_params(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> FuncParameterList:\n        params = self.get_attribute_value('params')\n        result: Any\n        if params is None:\n            param_list = []\n            for cr_param in self.get_subcommands(type=ParameterCommand):\n                param = schema.get(cr_param.classname, type=Parameter)\n                param_list.append(param)\n            result = FuncParameterList.create(schema, param_list)\n        elif isinstance(params, so.ObjectCollectionShell):\n            result = params.resolve(schema)\n        else:\n            result = params\n\n        assert isinstance(result, FuncParameterList)\n        return result\n\n    @classmethod\n    def _get_param_desc_from_params_ast(\n        cls,\n        schema: s_schema.Schema,\n        modaliases: Mapping[Optional[str], str],\n        params: list[qlast.FuncParamDecl],\n        *,\n        param_offset: int=0,\n    ) -> list[ParameterDesc]:\n        return [\n            ParameterDesc.from_ast(schema, modaliases, num, param)\n            for num, param in enumerate(params, param_offset)\n        ]\n\n    @classmethod\n    def _get_param_desc_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        modaliases: Mapping[Optional[str], str],\n        astnode: qlast.ObjectDDL,\n        *,\n        param_offset: int=0,\n    ) -> list[ParameterDesc]:\n        if not hasattr(astnode, 'params'):\n            # Some Callables, like the concrete constraints,\n            # have no params in their AST.\n            return []\n        assert isinstance(astnode, qlast.CallableObjectCommand)\n        return cls._get_param_desc_from_params_ast(\n            schema, modaliases, astnode.params, param_offset=param_offset)\n\n    @classmethod\n    def _get_param_desc_from_delta(\n        cls,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        cmd: sd.Command,\n    ) -> tuple[s_schema.Schema, list[ParameterDesc]]:\n        params = []\n        for subcmd in cmd.get_subcommands(type=CreateParameter):\n            schema, param = ParameterDesc.from_create_delta(\n                schema, context, subcmd)\n            params.append(param)\n\n        return schema, params\n\n\nclass CallableCommand(sd.QualifiedObjectCommand[CallableObjectT],\n                      ParametrizedCommand[CallableObjectT]):\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n        return s_types.materialize_type_in_attribute(\n            schema, context, self, 'return_type')\n\n\nclass RenameCallableObject(\n    CallableCommand[CallableObjectT],\n    sd.RenameObject[CallableObjectT],\n):\n    def _canonicalize(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        scls: CallableObjectT,\n    ) -> None:\n        super()._canonicalize(schema, context, scls)\n\n        # Don't do anything for concrete constraints\n        if not isinstance(scls, Function) and not scls.get_abstract(schema):\n            return\n\n        # params don't get picked up by the base _canonicalize because\n        # they aren't RefDicts (and use a different mangling scheme to\n        # boot), so we need to do it ourselves.\n        param_list = scls.get_params(schema)\n        params = CallableCommand._get_param_desc_from_params_ast(\n            schema, context.modaliases, param_list.get_ast(schema))\n\n        assert isinstance(self.new_name, sn.QualName)\n        for dparam, oparam in zip(params, param_list.objects(schema)):\n            self.add(self.init_rename_branch(\n                oparam,\n                dparam.get_fqname(schema, self.new_name),\n                schema=schema,\n                context=context,\n            ))\n\n\nclass AlterCallableObject(\n    CallableCommand[CallableObjectT],\n    sd.AlterObject[CallableObjectT],\n):\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.CallableObjectCommand]:\n        node = cast(\n            Optional[qlast.CallableObjectCommand],\n            # Skip AlterObject's _get_ast, since we don't want to\n            # filter things without subcommands. (Since updating\n            # nativecode isn't a subcommand in the AST.)\n            super(sd.AlterObject, self)._get_ast(\n                schema, context, parent_node=parent_node)\n        )\n\n        if not node:\n            return None\n\n        scls = self.get_object(schema, context)\n        node.params = scls.get_params(schema).get_ast(schema)\n\n        return node\n\n    def _alter_innards(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._alter_innards(schema, context)\n\n        for op in self.get_subcommands(metaclass=Parameter):\n            schema = op.apply(schema, context=context)\n\n        return schema\n\n\nclass CreateCallableObject(\n    CallableCommand[CallableObjectT],\n    sd.CreateObject[CallableObjectT],\n):\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(astnode, qlast.CreateObject)\n        assert isinstance(cmd, CreateCallableObject)\n\n        params = cls._get_param_desc_from_ast(\n            schema, context.modaliases, astnode)\n\n        for param in params:\n            # as_create_delta requires the specific type\n            cmd.add_prerequisite(param.as_create_delta(\n                schema, cmd.classname, context=context))\n\n        if hasattr(astnode, 'returning'):\n            assert isinstance(astnode, (qlast.CreateOperator,\n                                        qlast.CreateFunction))\n            modaliases = context.modaliases\n\n            return_type = utils.ast_to_type_shell(\n                astnode.returning,\n                metaclass=s_types.Type,\n                modaliases=modaliases,\n                module=cmd.classname.module,\n                schema=schema,\n            )\n\n            cmd.set_attribute_value(\n                'return_type', return_type)\n            cmd.set_attribute_value(\n                'return_typemod', astnode.returning_typemod)\n\n        return cmd\n\n    def get_resolved_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> dict[str, Any]:\n        params = self._get_params(schema, context)\n        props = super().get_resolved_attributes(schema, context)\n        props['params'] = params\n        return props\n\n    def _skip_param(self, props: dict[str, Any]) -> bool:\n        return False\n\n    def _get_params_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n    ) -> list[tuple[int, qlast.FuncParamDecl]]:\n        params: list[tuple[int, qlast.FuncParamDecl]] = []\n        for op in self.get_subcommands(type=ParameterCommand):\n            props = op.get_resolved_attributes(schema, context)\n            if self._skip_param(props):\n                continue\n\n            num: int = props['num']\n            default: Optional[s_expr.Expression] = props.get('default')\n            param = make_func_param(\n                name=Parameter.paramname_from_fullname(props['name']),\n                type=utils.typeref_to_ast(schema, props['type']),\n                typemod=props['typemod'],\n                kind=props['kind'],\n                default=default.parse() if default is not None else None,\n            )\n            params.append((num, param))\n\n        params.sort(key=lambda e: e[0])\n\n        return params\n\n        node.params = [p[1] for p in params]\n\n    def _apply_fields_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n    ) -> None:\n        super()._apply_fields_ast(schema, context, node)\n        params = self._get_params_ast(schema, context, node)\n        if isinstance(node, qlast.CallableObjectCommand):\n            node.params = [p[1] for p in params]\n\n\nclass DeleteCallableObject(\n    CallableCommand[CallableObjectT],\n    sd.DeleteObject[CallableObjectT],\n):\n    def _delete_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._delete_begin(schema, context)\n        scls = self.scls\n        if (\n            not context.canonical\n            # Don't do anything for concrete constraints\n            and (isinstance(scls, Function) or scls.get_abstract(schema))\n        ):\n            for param in scls.get_params(schema).objects(schema):\n                self.add(param.init_delta_command(schema, sd.DeleteObject))\n\n            return_type = scls.get_return_type(schema)\n            if op := return_type.as_type_delete_if_unused(schema):\n                self.add_caused(op)\n\n        return schema\n\n\nclass Function(\n    CallableObject,\n    VolatilitySubject,\n    qlkind=ft.SchemaObjectClass.FUNCTION,\n    data_safe=True,\n):\n\n    used_globals = so.SchemaField(\n        so.ObjectSet[s_globals.Global],\n        coerce=True,\n        default=so.DEFAULT_CONSTRUCTOR,\n        inheritable=False\n    )\n\n    used_permissions = so.SchemaField(\n        so.ObjectSet[s_permissions.Permission],\n        coerce=True,\n        default=so.DEFAULT_CONSTRUCTOR,\n        inheritable=False,\n    )\n\n    required_permissions = so.SchemaField(\n        so.ObjectSet[s_permissions.Permission],\n        coerce=True,\n        default=so.DEFAULT_CONSTRUCTOR,\n        inheritable=False,\n        allow_ddl_set=True,\n        compcoef=0.8,\n    )\n\n    # A backend_name that is shared between all overloads of the same\n    # function, to make them independent from the actual name.\n    backend_name = so.SchemaField(\n        uuid.UUID,\n        default=None,\n    )\n\n    code = so.SchemaField(\n        str, default=None, compcoef=0.4)\n\n    # Function body, when language is EdgeQL\n    nativecode = so.SchemaField(\n        s_expr.Expression, default=None, compcoef=0.9,\n        reflection_name='body')\n\n    language = so.SchemaField(\n        qlast.Language, default=None, compcoef=0.4, coerce=True,\n        reflection_name='language_real')\n\n    reflected_language = so.SchemaField(\n        str, reflection_name='language')\n\n    from_function = so.SchemaField(\n        str, default=None, compcoef=0.4)\n\n    from_expr = so.SchemaField(\n        bool, default=False, compcoef=0.4)\n\n    force_return_cast = so.SchemaField(\n        bool, default=False, compcoef=0.9)\n\n    sql_func_has_out_params = so.SchemaField(\n        bool, default=False, compcoef=0.9)\n\n    error_on_null_result = so.SchemaField(\n        str, default=None, compcoef=0.9)\n\n    #: For a generic function, if True, indicates that the\n    #: optionality of the result set should be the same as\n    #: of the generic argument.  (See std::assert_single).\n    preserves_optionality = so.SchemaField(\n        bool, default=False, compcoef=0.99)\n\n    #: For a generic function, if True, indicates that the\n    #: upper cardinality of the result set should be the same as\n    #: of the generic argument.  (See std::assert_exists).\n    preserves_upper_cardinality = so.SchemaField(\n        bool, default=False, compcoef=0.99)\n\n    initial_value = so.SchemaField(\n        s_expr.Expression, default=None, compcoef=0.4, coerce=True)\n\n    # This flag indicates that this function is intended to be used as\n    # a generic fallback implementation for a particular polymorphic\n    # function. The fallback implementation is exempted from the\n    # limitation that all polymorphic functions have to map to the\n    # same function in Postgres. There can only be at most one\n    # fallback implementation for any given polymorphic function.\n    #\n    # The flag is intended for internal use for standard library\n    # functions.\n    fallback = so.SchemaField(\n        bool,\n        default=False,\n        inheritable=False,\n        compcoef=0.909,\n    )\n\n    is_inlined = so.SchemaField(bool, default=False)\n\n    # A json string which describes any server param conversions to apply.\n    #\n    # The data should take the form: dict[str, str | list[str]]\n    #\n    # The key should be the names of the converted params.\n    # The value should be either: the conversion name, or a list of strings\n    # where the first item is the name of the conversion.\n    #\n    # If the value is a list, the additional items act as parameters to the\n    # conversion.\n    server_param_conversions = so.SchemaField(\n        str, default=None, compcoef=0.0,\n        # HACK: We don't actually allow users to set this in DDL, but\n        # we want to do it in one of our test suite schemas, and\n        # unless we set allow_ddl_set, it won't get DESCRIBEd\n        # correctly, which breaks patch and upgrade testing.\n        # So we do this check explicitly.\n        allow_ddl_set=True,\n    )\n\n    def has_inlined_defaults(self, schema: s_schema.Schema) -> bool:\n        # This can be relaxed to just `language is EdgeQL` when we\n        # support non-constant defaults.\n        return bool(self.get_language(schema) is qlast.Language.EdgeQL and\n                    self.get_params(schema).find_named_only(schema))\n\n    def get_signature_as_str(\n        self,\n        schema: s_schema.Schema,\n    ) -> str:\n        params = self.get_params(schema)\n        sn = self.get_shortname(schema)\n        return f\"{sn}{params.as_str(schema)}\"\n\n    def get_verbosename(\n        self,\n        schema: s_schema.Schema,\n        *,\n        with_parent: bool=False,\n    ) -> str:\n        return f\"function '{self.get_signature_as_str(schema)}'\"\n\n    def find_object_param_overloads(\n        self,\n        schema: s_schema.Schema,\n        *,\n        span: Optional[parsing.Span] = None,\n    ) -> Optional[tuple[list[Function], int]]:\n        \"\"\"Find if this function overloads another in object parameter.\n\n        If so, check the following rules:\n\n            - in the signatures of functions, only the overloaded object\n              parameter must differ, the number and the types of other\n              parameters must be the same across all object-overloaded\n              functions;\n            - the names of arguments in object-overloaded functions must\n              match.\n\n        If there are object overloads, return a tuple containing the list\n        of overloaded functions and the position of the overloaded parameter.\n        \"\"\"\n        params = self.get_params(schema)\n        if not params.has_objects(schema):\n            return None\n\n        new_params = params.objects(schema)\n        new_pt = tuple(p.get_type(schema) for p in new_params)\n\n        diff_param = -1\n        overloads = []\n        sn = self.get_shortname(schema)\n        for f in lookup_functions(sn, schema=schema):\n            if f == self:\n                continue\n\n            f_params = f.get_params(schema)\n            if not f_params.has_objects(schema):\n                continue\n\n            ext_params = f_params.objects(schema)\n            ext_pt = (p.get_type(schema) for p in ext_params)\n\n            this_diff_param = -1\n            non_obj_param_diff = False\n            multi_overload = False\n\n            for i, (new_t, ext_t) in enumerate(zip(new_pt, ext_pt)):\n                if new_t != ext_t:\n                    if new_t.is_object_type() and ext_t.is_object_type():\n                        if (\n                            this_diff_param != -1\n                            or (\n                                this_diff_param != -1\n                                and diff_param != -1\n                                and diff_param != this_diff_param\n                            )\n                            or non_obj_param_diff\n                        ):\n                            multi_overload = True\n                            break\n                        else:\n                            this_diff_param = i\n                    else:\n                        non_obj_param_diff = True\n                        if this_diff_param != -1:\n                            multi_overload = True\n                            break\n\n            if this_diff_param != -1:\n                if not multi_overload:\n                    multi_overload = len(new_params) != len(ext_params)\n\n                if multi_overload:\n                    # Multiple dispatch of object-taking functions is\n                    # not supported.\n                    my_sig = self.get_signature_as_str(schema)\n                    other_sig = f.get_signature_as_str(schema)\n                    raise errors.UnsupportedFeatureError(\n                        f'cannot create the `{my_sig}` function: '\n                        f'overloading an object type-receiving '\n                        f'function with differences in the remaining '\n                        f'parameters is not supported',\n                        span=span,\n                        details=(\n                            f\"Other function is defined as `{other_sig}`\"\n                        )\n                    )\n\n                if not all(\n                    new_p.get_parameter_name(schema)\n                    == ext_p.get_parameter_name(schema)\n                    for new_p, ext_p in zip(new_params, ext_params)\n                ):\n                    # And also _all_ parameter names must match due to\n                    # current implementation constraints.\n                    my_sig = self.get_signature_as_str(schema)\n                    other_sig = f.get_signature_as_str(schema)\n                    raise errors.UnsupportedFeatureError(\n                        f'cannot create the `{my_sig}` '\n                        f'function: overloading an object type-receiving '\n                        f'function with differences in the names of '\n                        f'parameters is not supported',\n                        span=span,\n                        details=(\n                            f\"Other function is defined as `{other_sig}`\"\n                        )\n                    )\n\n                if not all(\n                    new_p.get_typemod(schema)\n                    == ext_p.get_typemod(schema)\n                    for new_p, ext_p in zip(new_params, ext_params)\n                ):\n                    # And also _all_ parameter names must match due to\n                    # current implementation constraints.\n                    my_sig = self.get_signature_as_str(schema)\n                    other_sig = f.get_signature_as_str(schema)\n                    raise errors.UnsupportedFeatureError(\n                        f'cannot create the `{my_sig}` '\n                        f'function: overloading an object type-receiving '\n                        f'function with differences in the type modifiers of '\n                        f'parameters is not supported',\n                        span=span,\n                        details=(\n                            f\"Other function is defined as `{other_sig}`\"\n                        )\n                    )\n\n                if (\n                    new_params[this_diff_param].get_typemod(schema) !=\n                    ft.TypeModifier.SingletonType\n                ):\n                    my_sig = self.get_signature_as_str(schema)\n                    raise errors.UnsupportedFeatureError(\n                        f'cannot create the `{my_sig}` function: '\n                        f'object type-receiving '\n                        f'functions may not be overloaded on an OPTIONAL '\n                        f'parameter',\n                        span=span,\n                    )\n\n                diff_param = this_diff_param\n                overloads.append(f)\n\n        if diff_param == -1:\n            return None\n        else:\n            return (overloads, diff_param)\n\n\nclass FunctionCommandContext(CallableCommandContext):\n    pass\n\n\nclass FunctionCommand(\n    CallableCommand[Function],\n    context_class=FunctionCommandContext,\n):\n\n    @classmethod\n    def _classname_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext,\n    ) -> sn.QualName:\n        # _classname_from_ast signature expects qlast.ObjectDDL,\n        # but _get_param_desc_from_ast expects a ObjectDDL,\n        # which is more specific\n        assert isinstance(astnode, qlast.ObjectDDL)\n        name = super()._classname_from_ast(schema, astnode, context)\n\n        params = cls._get_param_desc_from_ast(\n            schema, context.modaliases, astnode)\n\n        return cls.get_schema_metaclass().get_fqname(schema, name, params)\n\n    def get_ast_attr_for_field(\n        self,\n        field: str,\n        astnode: type[qlast.DDLOperation],\n    ) -> Optional[str]:\n        if field == 'nativecode':\n            return 'nativecode'\n        else:\n            return super().get_ast_attr_for_field(field, astnode)\n\n    def validate_object(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        if not context.stdmode and not context.testmode:\n            if self.scls.get_server_param_conversions(schema):\n                raise errors.InvalidFunctionDefinitionError(\n                    f'setting server_param_conversions is not supported in '\n                    f'user-defined functions',\n                    span=self.span)\n\n    def compile_expr_field(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: s_expr.Expression,\n        track_schema_ref_exprs: bool=False,\n    ) -> s_expr.CompiledExpression:\n        if field.name == 'initial_value':\n            return value.compiled(\n                schema=schema,\n                options=qlcompiler.CompilerOptions(\n                    allow_generic_type_output=True,\n                    schema_object_context=self.get_schema_metaclass(),\n                    apply_query_rewrites=not context.stdmode,\n                    track_schema_ref_exprs=track_schema_ref_exprs,\n                ),\n                context=context,\n            )\n        elif field.name == 'nativecode':\n            return self.compile_this_function(\n                schema,\n                context,\n                value,\n                track_schema_ref_exprs,\n            )\n        else:\n            return super().compile_expr_field(\n                schema, context, field, value, track_schema_ref_exprs)\n\n    def get_dummy_expr_field_value(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: Any,\n    ) -> Optional[s_expr.Expression]:\n        if field.name == 'nativecode':\n            func = schema.get(self.classname, type=Function)\n            rt = func.get_return_type(schema)\n            return s_types.type_dummy_expr(rt, schema)\n        else:\n            raise NotImplementedError(f'unhandled field {field.name!r}')\n\n    def _get_attribute_value(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        name: str,\n    ) -> Any:\n        val = self.get_resolved_attribute_value(\n            name,\n            schema=schema,\n            context=context,\n        )\n        mcls = self.get_schema_metaclass()\n        if val is None:\n            field = mcls.get_field(name)\n            assert isinstance(field, so.SchemaField)\n            val = field.default\n\n        if val is None:\n            raise AssertionError(\n                f'missing required {name} for {mcls.__name__}'\n            )\n        return val\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n        # When volatility is altered, we need to force a\n        # reconsideration of nativecode if it exists in order to check\n        # it against the new volatility or compute the volatility on a\n        # RESET.  This is kind of unfortunate.\n        if (\n            isinstance(self, sd.AlterObject)\n            and self.has_attribute_value('volatility')\n            and not self.has_attribute_value('nativecode')\n            and (nativecode := self.scls.get_nativecode(schema)) is not None\n        ):\n            self.set_attribute_value(\n                'nativecode',\n                nativecode.not_compiled()\n            )\n\n        # Resolving 'nativecode' has side effects on has_dml and\n        # volatility, so force it to happen as part of\n        # canonicalization of attributes.\n        super().get_resolved_attribute_value(\n            'nativecode', schema=schema, context=context)\n        return schema\n\n    def compile_this_function(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        body: s_expr.Expression,\n        track_schema_ref_exprs: bool=False,\n    ) -> s_expr.CompiledExpression:\n        params = self._get_params(schema, context)\n        language = self._get_attribute_value(schema, context, 'language')\n        return_type = self._get_attribute_value(schema, context, 'return_type')\n        return_typemod = self._get_attribute_value(\n            schema, context, 'return_typemod')\n\n        expr = compile_function(\n            schema,\n            context,\n            body=body,\n            func_name=self.classname,\n            params=params,\n            language=language,\n            return_type=return_type,\n            return_typemod=return_typemod,\n            track_schema_ref_exprs=track_schema_ref_exprs,\n        )\n\n        ir = expr.irast\n\n        spec_volatility: Optional[ft.Volatility] = (\n            self.get_specified_attribute_value('volatility', schema, context))\n\n        if spec_volatility is None:\n            self.set_attribute_value('volatility', ir.volatility, computed=True)\n\n        # If a volatility is specified, it can be more volatile than the\n        # inferred volatility but not less.\n        if spec_volatility is not None and spec_volatility < ir.volatility:\n            # When restoring from old versions, just ignore the problem\n            # and use the inferred volatility\n            if context.compat_ver_is_before(\n                (1, 0, verutils.VersionStage.ALPHA, 8)\n            ):\n                self.set_attribute_value('volatility', ir.volatility)\n            else:\n                raise errors.InvalidFunctionDefinitionError(\n                    f'volatility mismatch in function declared as '\n                    f'{str(spec_volatility).lower()}',\n                    details=f'Actual volatility is '\n                            f'{str(ir.volatility).lower()}',\n                    span=body.parse().span,\n                )\n\n        globs = {\n            schema.get(glob.global_name, type=s_globals.Global)\n            for glob in ir.globals\n            if not glob.is_permission\n        }\n        self.set_attribute_value('used_globals', globs)\n\n        permissions = {\n            schema.get(glob.global_name, type=s_permissions.Permission)\n            for glob in ir.globals\n            if glob.is_permission\n        }\n        self.set_attribute_value('used_permissions', permissions)\n\n        return expr\n\n    @classmethod\n    def localnames_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> set[str]:\n        localnames = super().localnames_from_ast(\n            schema, astnode, context\n        )\n        if isinstance(astnode, (qlast.CreateFunction, qlast.AlterFunction)):\n            localnames |= {param.name for param in astnode.params}\n\n        return localnames\n\n\nclass CreateFunction(CreateCallableObject[Function], FunctionCommand):\n    astnode = qlast.CreateFunction\n\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        from edb.ir import utils as irutils\n\n        fullname = self.classname\n        shortname = sn.shortname_from_fullname(fullname)\n        schema, cp = self._get_param_desc_from_delta(schema, context, self)\n        signature = f'{shortname}({\", \".join(p.as_str(schema) for p in cp)})'\n\n        if func := schema.get(fullname, None):\n            raise errors.DuplicateFunctionDefinitionError(\n                f'cannot create the `{signature}` function: '\n                f'a function with the same signature '\n                f'is already defined',\n                span=self.span)\n\n        if not context.canonical:\n            fullname = self.classname\n            shortname = sn.shortname_from_fullname(fullname)\n            if backend_name := self.get_prespecified_id(\n                    context, id_field='backend_name'):\n                pass\n            elif others := lookup_functions(\n                sn.QualName(fullname.module, shortname.name), (), schema=schema\n            ):\n                backend_name = others[0].get_backend_name(schema)\n            elif context.stdmode:\n                backend_name = uuidgen.uuid5(FUNC_NAMESPACE, str(fullname))\n            else:\n                backend_name = uuidgen.uuid1mc()\n            if not self.has_attribute_value('backend_name'):\n                self.set_attribute_value('backend_name', backend_name)\n\n            if (\n                self.has_attribute_value(\"code\")\n                or self.has_attribute_value(\"nativecode\")\n            ) and not self.has_attribute_value('impl_is_strict'):\n                self.set_attribute_value(\n                    'impl_is_strict',\n                    _params_are_all_required_singletons(cp, schema),\n                )\n\n        # Check if other schema objects with the same name (ignoring\n        # signature, of course) exist.\n        if other := schema.get(\n                sn.QualName(fullname.module, shortname.name), None):\n            raise errors.SchemaError(\n                f'{other.get_verbosename(schema)} already exists')\n\n        schema = super()._create_begin(schema, context)\n\n        params: FuncParameterList = self.scls.get_params(schema)\n\n        language = self.scls.get_language(schema)\n        return_type = self.scls.get_return_type(schema)\n        return_typemod = self.scls.get_return_typemod(schema)\n        from_function = self.scls.get_from_function(schema)\n        has_polymorphic = params.has_polymorphic(schema)\n        has_set_of = params.has_set_of(schema)\n        has_objects = params.has_objects(schema)\n        polymorphic_return_type = return_type.is_polymorphic(schema)\n        named_only = params.find_named_only(schema)\n        fallback = self.scls.get_fallback(schema)\n        preserves_opt = self.scls.get_preserves_optionality(schema)\n        preserves_upper_card = self.scls.get_preserves_upper_cardinality(\n            schema)\n\n        if preserves_opt and not has_set_of:\n            raise errors.InvalidFunctionDefinitionError(\n                f'cannot create `{signature}` function: '\n                f'\"preserves_optionality\" makes no sense '\n                f'in a non-aggregate function',\n                span=self.span)\n\n        if preserves_upper_card and not has_set_of:\n            raise errors.InvalidFunctionDefinitionError(\n                f'cannot create `{signature}` function: '\n                f'\"preserves_upper_cardinality\" makes no sense '\n                f'in a non-aggregate function',\n                span=self.span)\n\n        if preserves_upper_card and (\n            return_typemod is not ft.TypeModifier.SetOfType\n        ):\n            raise errors.InvalidFunctionDefinitionError(\n                f'cannot create `{signature}` function: '\n                f'\"preserves_upper_cardinality\" makes no sense '\n                f'in a function not returning SET OF',\n                span=self.span)\n\n        # Certain syntax is only allowed in \"EdgeDB developer\" mode,\n        # i.e. when populating std library, etc.\n        if not context.stdmode and not context.testmode:\n            if has_polymorphic or polymorphic_return_type:\n                raise errors.InvalidFunctionDefinitionError(\n                    f'cannot create `{signature}` function: '\n                    f'generic types are not supported in '\n                    f'user-defined functions',\n                    span=self.span)\n            elif from_function:\n                raise errors.InvalidFunctionDefinitionError(\n                    f'cannot create `{signature}` function: '\n                    f'\"USING SQL FUNCTION\" is not supported in '\n                    f'user-defined functions',\n                    span=self.span)\n            elif language != qlast.Language.EdgeQL:\n                raise errors.InvalidFunctionDefinitionError(\n                    f'cannot create `{signature}` function: '\n                    f'\"USING {language}\" is not supported in '\n                    f'user-defined functions',\n                    span=self.span)\n\n        if polymorphic_return_type and not has_polymorphic:\n            raise errors.InvalidFunctionDefinitionError(\n                f'cannot create `{signature}` function: '\n                f'function returns a generic type but has no '\n                f'generic parameters',\n                span=self.span)\n\n        overloaded_funcs = lookup_functions(shortname, (), schema=schema)\n        has_from_function = from_function\n\n        for func in overloaded_funcs:\n            func_params = func.get_params(schema)\n            func_named_only = func_params.find_named_only(schema)\n            func_from_function = func.get_from_function(schema)\n            func_preserves_opt = func.get_preserves_optionality(schema)\n            func_preserves_upper_card = func.get_preserves_upper_cardinality(\n                schema)\n\n            if func_named_only.keys() != named_only.keys():\n                raise errors.InvalidFunctionDefinitionError(\n                    f'cannot create `{signature}` function: '\n                    f'overloading another function with different '\n                    f'named only parameters: '\n                    f'\"{func.get_signature_as_str(schema)}\"',\n                    span=self.span)\n\n            if ((has_polymorphic or func_params.has_polymorphic(schema)) and (\n                    func.get_return_typemod(schema) != return_typemod)):\n\n                func_return_typemod = func.get_return_typemod(schema)\n                raise errors.InvalidFunctionDefinitionError(\n                    f'cannot create the polymorphic `{signature} -> '\n                    f'{return_typemod.to_edgeql()} '\n                    f'{return_type.get_displayname(schema)}` '\n                    f'function: overloading another function with different '\n                    f'return type {func_return_typemod.to_edgeql()} '\n                    f'{func.get_return_type(schema).get_displayname(schema)}',\n                    span=self.span)\n\n            if fallback and func.get_fallback(schema) and self.scls != func:\n                raise errors.InvalidFunctionDefinitionError(\n                    f'cannot create the polymorphic `{signature} -> '\n                    f'{return_typemod.to_edgeql()} '\n                    f'{return_type.get_displayname(schema)}` '\n                    f'function: only one generic fallback per polymorphic '\n                    f'function is allowed',\n                    span=self.span)\n\n            if func_from_function:\n                has_from_function = func_from_function\n\n            if func_preserves_opt != preserves_opt:\n                raise errors.InvalidFunctionDefinitionError(\n                    f'cannot create `{signature}` function: '\n                    f'overloading another function with different '\n                    f'\"preserves_optionality\" attribute: '\n                    f'`{func.get_signature_as_str(schema)}`',\n                    span=self.span)\n\n            if func_preserves_upper_card != preserves_upper_card:\n                raise errors.InvalidFunctionDefinitionError(\n                    f'cannot create `{signature}` function: '\n                    f'overloading another function with different '\n                    f'\"preserves_upper_cardinality\" attribute: '\n                    f'`{func.get_signature_as_str(schema)}`',\n                    span=self.span)\n\n        if has_objects:\n            self.scls.find_object_param_overloads(\n                schema, span=self.span)\n\n        if has_from_function:\n            # Ignore the generic fallback when considering\n            # from_function for polymorphic functions.\n            if (not fallback and from_function != has_from_function or\n                    any(not f.get_fallback(schema) and\n                        f.get_from_function(schema) != has_from_function\n                        for f in overloaded_funcs)):\n                raise errors.InvalidFunctionDefinitionError(\n                    f'cannot create the `{signature}` function: '\n                    f'overloading \"USING SQL FUNCTION\" functions is '\n                    f'allowed only when all functions point to the same '\n                    f'SQL function',\n                    span=self.span)\n\n        if (language == qlast.Language.EdgeQL and\n                any(p.get_typemod(schema) is ft.TypeModifier.SetOfType\n                    for p in params.objects(schema))):\n            raise errors.UnsupportedFeatureError(\n                f'cannot create the `{signature}` function: '\n                f'SET OF parameters in user-defined EdgeQL functions are '\n                f'not supported',\n                span=self.span)\n\n        # check that params of type 'anytype' don't have defaults\n        for p in params.objects(schema):\n            p_default = p.get_default(schema)\n            if p_default is None:\n                continue\n\n            p_type = p.get_type(schema)\n\n            try:\n                ir_default = p.get_ir_default(schema=schema, context=context)\n            except Exception as ex:\n                raise errors.InvalidFunctionDefinitionError(\n                    f'cannot create the `{signature}` function: '\n                    f'invalid default value {p_default.text!r} of parameter '\n                    f'{p.get_displayname(schema)!r}: {ex}',\n                    span=self.span)\n\n            check_default_type = True\n            if p_type.is_polymorphic(schema):\n                if irutils.is_empty(ir_default.expr):\n                    check_default_type = False\n                else:\n                    raise errors.InvalidFunctionDefinitionError(\n                        f'cannot create the `{signature}` function: '\n                        f'polymorphic parameter of type '\n                        f'{p_type.get_displayname(schema)} cannot '\n                        f'have a non-empty default value',\n                        span=self.span)\n            elif (p.get_typemod(schema) is ft.TypeModifier.OptionalType and\n                    irutils.is_empty(ir_default.expr)):\n                check_default_type = False\n\n            if check_default_type:\n                default_type = ir_default.stype\n                if not default_type.assignment_castable_to(\n                    p_type, ir_default.schema\n                ):\n                    raise errors.InvalidFunctionDefinitionError(\n                        f'cannot create the `{signature}` function: '\n                        f'invalid declaration of parameter '\n                        f'{p.get_displayname(schema)!r}: '\n                        f'unexpected type of the default expression: '\n                        f'{default_type.get_displayname(ir_default.schema)}, '\n                        f'expected '\n                        f'{p_type.get_displayname(schema)}',\n                        span=self.span)\n\n        # Make sure variadic parameters do not contain optional types in\n        # user-defined functions\n        if language == qlast.Language.EdgeQL:\n            if variadic := params.find_variadic(schema):\n                typemod = variadic.get_typemod(schema)\n                if typemod is ft.TypeModifier.OptionalType:\n                    raise errors.InvalidFunctionDefinitionError(\n                        f'cannot create the `{signature}` function: '\n                        f'variadic argument '\n                        f'`{variadic.get_displayname(schema)}` '\n                        f'illegally declared with optional type in '\n                        f'user-defined function',\n                        span=self.span)\n\n        return schema\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, CreateFunction)\n\n        reflected_language = 'builtin'\n\n        assert isinstance(astnode, qlast.CreateFunction)\n        if astnode.code is not None:\n            cmd.set_attribute_value(\n                'language',\n                astnode.code.language,\n            )\n            if astnode.code.language is qlast.Language.EdgeQL:\n                reflected_language = 'EdgeQL'\n\n                nativecode_expr: qlast.Base\n\n                if astnode.nativecode is not None:\n                    nativecode_expr = astnode.nativecode\n                else:\n                    assert astnode.code.code is not None\n                    nativecode_expr = qlparser.parse_query(astnode.code.code)\n\n                nativecode = s_expr.Expression.from_ast(\n                    nativecode_expr,\n                    schema,\n                    context.modaliases,\n                    context.localnames,\n                )\n\n                cmd.set_attribute_value(\n                    'nativecode',\n                    nativecode,\n                )\n            elif astnode.code.from_function is not None:\n                cmd.set_attribute_value(\n                    'from_function',\n                    astnode.code.from_function\n                )\n            elif (\n                astnode.code.from_expr is not None\n                and astnode.code.code is None\n            ):\n                cmd.set_attribute_value(\n                    'from_expr',\n                    astnode.code.from_expr,\n                )\n            else:\n                cmd.set_attribute_value(\n                    'code',\n                    astnode.code.code,\n                )\n\n        cmd.set_attribute_value('reflected_language', reflected_language)\n\n        return cmd\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        assert isinstance(node, qlast.CreateFunction)\n        new_value: Any = op.new_value\n\n        if op.property == 'return_type':\n            node.returning = utils.typeref_to_ast(schema, new_value)\n\n        elif op.property == 'return_typemod':\n            node.returning_typemod = new_value\n\n        elif op.property == 'code':\n            if node.code is None:\n                node.code = qlast.FunctionCode()\n            node.code.code = new_value\n\n        elif op.property == 'language':\n            if node.code is None:\n                node.code = qlast.FunctionCode()\n            node.code.language = new_value\n\n        elif op.property == 'from_function' and new_value:\n            if node.code is None:\n                node.code = qlast.FunctionCode()\n            node.code.from_function = new_value\n\n        elif op.property == 'from_expr' and new_value:\n            if node.code is None:\n                node.code = qlast.FunctionCode()\n            node.code.from_expr = new_value\n\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n\nclass RenameFunction(RenameCallableObject[Function], FunctionCommand):\n    @classmethod\n    def _classname_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext,\n    ) -> sn.QualName:\n        ctx = context.current()\n        assert isinstance(ctx.op, AlterFunction)\n        name = sd.QualifiedObjectCommand._classname_from_ast(\n            schema, astnode, context)\n\n        quals = list(sn.quals_from_fullname(ctx.op.classname))\n        out = sn.QualName(\n            name=sn.get_specialized_name(name, *quals),\n            module=name.module\n        )\n        return out\n\n    def validate_alter(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        cur_shortname = sn.shortname_from_fullname(self.classname)\n        cur_name = sn.QualName(self.classname.module, cur_shortname.name)\n\n        new_shortname = sn.shortname_from_fullname(self.new_name)\n        assert isinstance(self.new_name, sn.QualName)\n        new_name = sn.QualName(self.new_name.module, new_shortname.name)\n\n        if cur_name == new_name:\n            return\n\n        existing = lookup_functions(cur_name, schema=schema)\n        if len(existing) > 1:\n            raise errors.SchemaError(\n                'renaming an overloaded function is not allowed',\n                span=self.span)\n\n        target = lookup_functions(new_name, (), schema=schema)\n        if target:\n            raise errors.SchemaError(\n                f\"can not rename function to '{new_name!s}' because \"\n                f\"a function with the same name already exists, and \"\n                f\"renaming into an overload is not supported\",\n                span=self.span)\n\n\nclass AlterFunction(AlterCallableObject[Function], FunctionCommand):\n\n    astnode = qlast.AlterFunction\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._alter_begin(schema, context)\n        scls = self.scls\n\n        if context.canonical:\n            return schema\n\n        if self.has_attribute_value(\"fallback\"):\n            overloaded_funcs = schema._get_by_shortname(\n                Function, self.scls.get_shortname(schema)\n            ) or ()\n\n            if len([func for func in overloaded_funcs\n                    if func.get_fallback(schema)]) > 1:\n                raise errors.InvalidFunctionDefinitionError(\n                    f'cannot alter the polymorphic '\n                    f'{self.scls.get_verbosename(schema)}: '\n                    f'only one generic fallback per polymorphic '\n                    f'function is allowed',\n                    span=self.span)\n\n        # If volatility or nativecode changed, propagate that to\n        # referring exprs\n        if not (\n            self.has_attribute_value(\"volatility\")\n            or self.has_attribute_value(\"nativecode\")\n        ):\n            return schema\n\n        # We also need to propagate changes to \"parent\"\n        # overloads. This is mainly so they can get the proper global\n        # variables updated.\n        extra_refs: Optional[dict[so.Object, list[str]]] = None\n        if (overloaded := scls.find_object_param_overloads(schema)):\n            ov_funcs, ov_idx = overloaded\n            cur_type = (\n                scls.get_params(schema).objects(schema)[ov_idx].\n                get_type(schema)\n            )\n            extra_refs = {\n                f: ['nativecode'] for f in ov_funcs\n                if (f_type := f.get_params(schema).objects(schema)[ov_idx].\n                    get_type(schema))\n                and f_type != cur_type and cur_type.issubclass(schema, f_type)\n            }\n\n        vn = scls.get_verbosename(schema, with_parent=True)\n        schema = self._propagate_if_expr_refs(\n            schema, context, extra_refs=extra_refs,\n            action=f'alter the definition of {vn}')\n\n        return schema\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(astnode, qlast.AlterFunction)\n\n        if astnode.code is not None:\n            if (\n                astnode.code.from_function is not None or\n                astnode.code.from_expr\n            ):\n                raise errors.EdgeQLSyntaxError(\n                    'altering function code is only supported for '\n                    'pure EdgeQL functions',\n                    span=astnode.span\n                )\n\n            nativecode_expr: Optional[qlast.Expr] = None\n            if astnode.nativecode is not None:\n                nativecode_expr = astnode.nativecode\n            elif (\n                astnode.code.language is qlast.Language.EdgeQL\n                and astnode.code.code is not None\n            ):\n                nativecode_expr = qlparser.parse_query(astnode.code.code)\n            else:\n                cmd.set_attribute_value(\n                    'code',\n                    astnode.code.code,\n                )\n\n            if nativecode_expr is not None:\n                nativecode = s_expr.Expression.from_ast(\n                    nativecode_expr,\n                    schema,\n                    context.modaliases,\n                    context.localnames,\n                )\n\n                cmd.set_attribute_value(\n                    'nativecode',\n                    nativecode,\n                )\n\n        return cmd\n\n    def _get_attribute_value(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        name: str,\n    ) -> Any:\n        val = self.get_resolved_attribute_value(\n            name,\n            schema=schema,\n            context=context,\n        )\n        if val is None:\n            val = self.scls.get_field_value(schema, name)\n        if val is None:\n            mcls = self.get_schema_metaclass()\n            raise AssertionError(\n                f'missing required {name} for {mcls.__name__}'\n            )\n\n        return val\n\n    def _get_params(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> FuncParameterList:\n        return self.scls.get_params(schema)\n\n    def canonicalize_alter_from_external_ref(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        # Produce a param desc list which we use to find a new name.\n        param_list = self.scls.get_params(schema)\n        params = CallableCommand._get_param_desc_from_params_ast(\n            schema, context.modaliases, param_list.get_ast(schema))\n        name = sn.shortname_from_fullname(self.classname)\n        assert isinstance(name, sn.QualName), \"expected qualified name\"\n        new_fname = CallableObject.get_fqname(schema, name, params)\n        if new_fname == self.classname:\n            return\n\n        # Do the rename\n        rename = self.scls.init_delta_command(\n            schema, sd.RenameObject, new_name=new_fname)\n        rename.set_attribute_value(\n            'name', value=new_fname, orig_value=self.classname)\n        self.add(rename)\n\n\nclass DeleteFunction(DeleteCallableObject[Function], FunctionCommand):\n    astnode = qlast.DropFunction\n\n    def _apply_fields_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n    ) -> None:\n        super()._apply_fields_ast(schema, context, node)\n\n        params = []\n        for op in self.get_subcommands(type=ParameterCommand):\n            props = op.get_orig_attributes(schema, context)\n            num: int = props['num']\n            param = make_func_param(\n                name=Parameter.paramname_from_fullname(props['name']),\n                type=utils.typeref_to_ast(schema, props['type']),\n                typemod=props['typemod'],\n                kind=props['kind'],\n            )\n            params.append((num, param))\n\n        params.sort(key=lambda e: e[0])\n\n        assert isinstance(node, qlast.CallableObjectCommand)\n        node.params = [p[1] for p in params]\n\n\ndef get_params_symtable(\n    params: FuncParameterList,\n    schema: s_schema.Schema,\n    *,\n    inlined_defaults: bool,\n) -> dict[str, qlast.Expr]:\n\n    anchors: dict[str, qlast.Expr] = {}\n\n    defaults_mask = qlast.TypeCast(\n        expr=qlast.FunctionParameter(name='__defaults_mask__'),\n        type=qlast.TypeName(\n            maintype=qlast.ObjectRef(\n                module='std',\n                name='bytes',\n            ),\n        ),\n    )\n\n    for pi, p in enumerate(params.get_in_canonical_order(schema)):\n        p_shortname = p.get_parameter_name(schema)\n        p_is_optional = (\n            p.get_typemod(schema) is not ft.TypeModifier.SingletonType\n        )\n        anchors[p_shortname] = qlast.TypeCast(\n            expr=qlast.FunctionParameter(name=p_shortname),\n            cardinality_mod=(\n                qlast.CardinalityModifier.Optional if p_is_optional else None\n            ),\n            type=utils.typeref_to_ast(schema, p.get_type(schema)),\n        )\n\n        p_default = p.get_default(schema)\n        if p_default is None:\n            continue\n\n        if not inlined_defaults:\n            continue\n\n        anchors[p_shortname] = qlast.IfElse(\n            condition=qlast.BinOp(\n                left=qlast.FunctionCall(\n                    func=('std', 'bytes_get_bit'),\n                    args=[\n                        defaults_mask,\n                        qlast.Constant.integer(pi),\n                    ]),\n                op='=',\n                right=qlast.Constant.integer(0),\n            ),\n            if_expr=anchors[p_shortname],\n            else_expr=qlast.OptionalExpr(expr=p_default.parse()),\n        )\n\n    return anchors\n\n\ndef compile_function(\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    *,\n    body: s_expr.Expression,\n    func_name: sn.QualName,\n    params: FuncParameterList,\n    language: qlast.Language,\n    return_type: s_types.Type,\n    return_typemod: ft.TypeModifier,\n    track_schema_ref_exprs: bool=False,\n) -> s_expr.CompiledExpression:\n    assert language is qlast.Language.EdgeQL\n\n    compiled = body.compiled(\n        schema,\n        options=get_compiler_options(\n            schema,\n            context,\n            func_name=func_name,\n            params=params,\n            track_schema_ref_exprs=track_schema_ref_exprs,\n        ),\n        context=context,\n    )\n\n    ir = compiled.irast\n    schema = ir.schema\n\n    if (not ir.stype.issubclass(schema, return_type)\n            and not ir.stype.implicitly_castable_to(return_type, schema)):\n        raise errors.InvalidFunctionDefinitionError(\n            f'return type mismatch in function declared to return '\n            f'{return_type.get_verbosename(schema)}',\n            details=f'Actual return type is '\n                    f'{ir.stype.get_verbosename(schema)}',\n            span=body.parse().span,\n        )\n\n    if (return_typemod is not ft.TypeModifier.SetOfType\n            and ir.cardinality.is_multi()):\n        raise errors.InvalidFunctionDefinitionError(\n            f'return cardinality mismatch in function declared to return '\n            f'a singleton',\n            details=(\n                f'Function may return a set with more than one element.'\n            ),\n            span=body.parse().span,\n        )\n    elif (return_typemod is ft.TypeModifier.SingletonType\n            and ir.cardinality.can_be_zero()):\n        raise errors.InvalidFunctionDefinitionError(\n            f'return cardinality mismatch in function declared to return '\n            f'exactly one value',\n            details=(\n                f'Function may return an empty set.'\n            ),\n            span=body.parse().span,\n        )\n\n    return compiled\n\n\ndef compile_function_inline(\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    *,\n    body: s_expr.Expression,\n    func_name: sn.QualName,\n    params: FuncParameterList,\n    language: qlast.Language,\n    return_type: s_types.Type,\n    return_typemod: ft.TypeModifier,\n    track_schema_ref_exprs: bool=False,\n    inlining_context: qlcontext.ContextLevel,\n) -> irast.Set:\n    \"\"\"Compile a function body to be inlined.\"\"\"\n    assert language is qlast.Language.EdgeQL\n\n    from edb.edgeql.compiler import dispatch\n    from edb.edgeql.compiler import pathctx\n    from edb.edgeql.compiler import setgen\n    from edb.edgeql.compiler import stmtctx\n\n    ctx = stmtctx.init_context(\n        schema=schema,\n        options=get_compiler_options(\n            schema,\n            context,\n            func_name=func_name,\n            params=params,\n            track_schema_ref_exprs=track_schema_ref_exprs,\n            inlining_context=inlining_context,\n        ),\n        inlining_context=inlining_context,\n    )\n\n    ql_expr = body.parse()\n\n    # Wrap argument paths\n    param_names: set[str] = {\n        param.get_parameter_name(inlining_context.env.schema)\n        for param in params.objects(inlining_context.env.schema)\n    }\n    argument_path_wrapper = ArgumentPathWrapper(param_names)\n    ql_expr = argument_path_wrapper.visit(ql_expr)\n\n    # Add implicit limit if present\n    if ctx.implicit_limit:\n        ql_expr = qlast.SelectQuery(result=ql_expr, implicit=True)\n        ql_expr.limit = qlast.Constant.integer(ctx.implicit_limit)\n\n    ir_set: irast.Set = dispatch.compile(ql_expr, ctx=ctx)\n\n    # Copy schema back to inlining context\n    if inlining_context:\n        inlining_context.env.schema = ctx.env.schema\n\n    # Create scoped set if necessary\n    if pathctx.get_set_scope(ir_set, ctx=ctx) is None:\n        ir_set = setgen.scoped_set(ir_set, ctx=ctx)\n\n    return ir_set\n\n\nclass ArgumentPathWrapper(ast.NodeTransformer):\n    # Wrap paths based on the inlined arguments which are arguments to other\n    # inlined functions.\n    #\n    # Given the functions:\n    #   function inner(x: int64) -> int64 using (x);\n    #   function outer(x: int64) -> int64 using (inner(x));\n    #\n    # Before inlining the outer function, the irast may look like this:\n    #   FunctionCall outer\n    #     CallArg: Set expr~1: Parameter x\n    #     Body\n    #       Set: FunctionCall inner\n    #         CallArg: Set expr~2: Parameter x\n    #         Body\n    #           SelectStmt: Set expr~2: InlinedParameterExpr\n    #\n    # The outer function will then inline, `Parameter x`:\n    #   FunctionCall outer\n    #     CallArg: Set expr~1: Parameter x\n    #     Body\n    #       Set: FunctionCall inner\n    #         CallArg: Set expr~1: InlinedParameterExpr\n    #         Body\n    #           SelectStmt: Set expr~2: InlinedParameterExpr\n    #\n    # And the definition of `Set expr~2` will be removed.\n    #\n    # To ensure outer function inlines `Parameter x` while keeping the path id\n    # of the inner function, wrap the parameter with a Select:\n    #   FunctionCall outer\n    #     CallArg: Set expr~1: Parameter x\n    #     Body\n    #       Set: FunctionCall inner\n    #         CallArg: Set expr~2: SelectStmt: Set expr~1: Parameter x\n    #         Body\n    #           SelectStmt: Set expr~2: InlinedParameterExpr\n\n    def __init__(\n        self,\n        param_names: set[str],\n    ) -> None:\n        super().__init__()\n        self.param_names = param_names\n\n    def visit_FunctionCall(self, node: qlast.FunctionCall) -> qlast.Base:\n        has_direct_args = False\n        new_args: list[qlast.Expr] = []\n        new_kwargs: dict[str, qlast.Expr] = {}\n\n        for arg in node.args:\n            if (\n                isinstance(arg, qlast.Path)\n                and isinstance(arg.steps[0], qlast.ObjectRef)\n                and arg.steps[0].name in self.param_names\n            ):\n                has_direct_args = True\n                new_args.append(qlast.SelectQuery(result=arg))\n\n            else:\n                new_args.append(arg)\n\n        for arg_name, arg in node.kwargs.items():\n            if (\n                isinstance(arg, qlast.Path)\n                and isinstance(arg.steps[0], qlast.ObjectRef)\n                and arg.steps[0].name in self.param_names\n            ):\n                has_direct_args = True\n                new_kwargs[arg_name] = qlast.SelectQuery(result=arg)\n\n            else:\n                new_kwargs[arg_name] = arg\n\n        if has_direct_args:\n            node = node.replace(args=new_args, kwargs=new_kwargs)\n\n        return cast(qlast.Base, self.generic_visit(node))\n\n\ndef get_compiler_options(\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    *,\n    func_name: sn.QualName,\n    params: FuncParameterList,\n    track_schema_ref_exprs: bool,\n    inlining_context: Optional[qlcontext.ContextLevel] = None,\n) -> qlcompiler.CompilerOptions:\n\n    has_inlined_defaults = (\n        bool(params.find_named_only(schema))\n        and inlining_context is None\n    )\n\n    param_anchors = get_params_symtable(\n        params,\n        schema,\n        inlined_defaults=has_inlined_defaults,\n    )\n\n    return qlcompiler.CompilerOptions(\n        anchors=param_anchors,\n        func_name=(\n            inlining_context.env.options.func_name\n            if inlining_context is not None else\n            func_name\n        ),\n        func_params=(\n            inlining_context.env.options.func_params\n            if inlining_context is not None else\n            params\n        ),\n        json_parameters=(\n            inlining_context.env.options.json_parameters\n            if inlining_context is not None else\n            False\n        ),\n        apply_query_rewrites=not context.stdmode,\n        track_schema_ref_exprs=track_schema_ref_exprs,\n    )\n\n\ndef lookup_functions(\n    name: str | sn.Name,\n    default: tuple[Function, ...] | so.NoDefaultT = so.NoDefault,\n    *,\n    module_aliases: Optional[Mapping[Optional[str], str]] = None,\n    schema: s_schema.Schema,\n) -> tuple[Function, ...]:\n    funcs = s_schema.lookup(\n        schema,\n        name,\n        getter=_get_functions,\n        module_aliases=module_aliases,\n        default=default,\n    )\n\n    if funcs is not so.NoDefault:\n        return funcs\n    else:\n        return s_schema.Schema.raise_bad_reference(\n            name=name, module_aliases=module_aliases, type=Function,\n        )\n\n\n@lru.per_job_lru_cache()\ndef _get_functions(\n    schema: s_schema.Schema,\n    name: sn.Name,\n) -> tuple[Function, ...] | None:\n    return schema._get_by_shortname(Function, name)\n"
  },
  {
    "path": "edb/schema/futures.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Callable, cast\n\nfrom edb import errors\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom . import delta as sd\nfrom . import name as sn\nfrom . import objects as so\nfrom . import schema as s_schema\n\n\nclass FutureBehavior(\n    so.Object,\n    qlkind=qltypes.SchemaObjectClass.FUTURE,\n    data_safe=False,\n):\n    name = so.SchemaField(\n        sn.Name,\n        inheritable=False,\n        compcoef=0.0,  # can't rename\n    )\n\n\nclass FutureBehaviorCommandContext(\n    sd.ObjectCommandContext[FutureBehavior],\n):\n    pass\n\n\n# Unlike extensions, futures are *explicitly* built into the\n# language. Enabling or disabling a futures might require making\n# other changes (recompiling functions that depend on it, for\n# example), so each future is mapped to a handler function that can\n# generate a command.\n_FutureBehaviorHandler = Callable[\n    ['FutureBehaviorCommand', s_schema.Schema, sd.CommandContext, bool],\n    tuple[s_schema.Schema, sd.Command],\n]\n\nFUTURE_HANDLERS: dict[str, _FutureBehaviorHandler] = {}\n\n\ndef register_handler(\n    name: str,\n) -> Callable[[_FutureBehaviorHandler], _FutureBehaviorHandler]:\n    def func(f: _FutureBehaviorHandler) -> _FutureBehaviorHandler:\n        FUTURE_HANDLERS[name] = f\n        return f\n\n    return func\n\n\ndef future_enabled(schema: s_schema.Schema, feat: str) -> bool:\n    return bool(schema.get_global(FutureBehavior, feat, default=None))\n\n\nclass FutureBehaviorCommand(\n    sd.ObjectCommand[FutureBehavior],\n    context_class=FutureBehaviorCommandContext,\n):\n    # A command that gets run after adjusting the future value.\n    # It needs to run *after* the delete, for a 'drop future',\n    # and so it can't use any of the existing varieties of subcommands.\n    #\n    # If anything else ends up needing to do this, we can add another\n    # variety of subcommand.\n    future_cmd: sd.Command | None = None\n\n    def copy(self: FutureBehaviorCommand) -> FutureBehaviorCommand:\n        result = super().copy()\n        if self.future_cmd:\n            result.future_cmd = self.future_cmd.copy()\n        return result\n\n    @classmethod\n    def adapt(\n        cls: type[FutureBehaviorCommand], obj: sd.Command\n    ) -> FutureBehaviorCommand:\n        result = super(FutureBehaviorCommand, cls).adapt(obj)\n        assert isinstance(obj, FutureBehaviorCommand)\n        mcls = cast(sd.CommandMeta, type(cls))\n        if obj.future_cmd:\n            result.future_cmd = mcls.adapt(obj.future_cmd)\n        return result\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().apply(schema, context)\n        if not context.canonical and not isinstance(self, sd.AlterObject):\n            key = str(self.classname)\n            if key not in FUTURE_HANDLERS:\n                raise errors.QueryError(\n                    f\"Unknown future '{str(key)}'\"\n                )\n            schema, cmd = FUTURE_HANDLERS[key](\n                self, schema, context, isinstance(self, sd.CreateObject))\n            self.future_cmd = cmd\n\n        if self.future_cmd:\n            schema = self.future_cmd.apply(schema, context)\n\n        return schema\n\n\nclass CreateFutureBehavior(\n    FutureBehaviorCommand,\n    sd.CreateObject[FutureBehavior],\n):\n    astnode = qlast.CreateFuture\n\n\nclass DeleteFutureBehavior(\n    FutureBehaviorCommand,\n    sd.DeleteObject[FutureBehavior],\n):\n    astnode = qlast.DropFuture\n\n\nclass AlterFutureBehavior(\n    FutureBehaviorCommand,\n    sd.AlterObject[FutureBehavior],\n):\n    pass\n\n\n# These are registered here because they aren't directly related to\n# any schema elements.\n# They are all dummys now, too.\n@register_handler('simple_scoping')\n@register_handler('warn_old_scoping')\n@register_handler('_scoping_noop_test')\ndef toggle_scoping_future(\n    cmd: FutureBehaviorCommand,\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    on: bool,\n) -> tuple[s_schema.Schema, sd.Command]:\n    return schema, sd.CommandGroup()\n"
  },
  {
    "path": "edb/schema/globals.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, TYPE_CHECKING\n\nfrom edb import errors\n\nfrom edb.common import struct\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.edgeql import qltypes\n\nfrom . import annos as s_anno\nfrom . import delta as sd\nfrom . import expr as s_expr\nfrom . import expraliases as s_expraliases\nfrom . import name as sn\nfrom . import objects as so\nfrom . import types as s_types\nfrom . import utils\n\nif TYPE_CHECKING:\n    from edb.schema import schema as s_schema\n\n\nclass Global(\n    so.QualifiedObject,\n    s_anno.AnnotationSubject,\n    qlkind=qltypes.SchemaObjectClass.GLOBAL,\n    data_safe=True,\n):\n\n    target = so.SchemaField(\n        s_types.Type,\n        compcoef=0.85,\n        special_ddl_syntax=True,\n    )\n\n    required = so.SchemaField(\n        bool,\n        default=False,\n        compcoef=0.909,\n        special_ddl_syntax=True,\n        describe_visibility=(\n            so.DescribeVisibilityPolicy.SHOW_IF_EXPLICIT_OR_DERIVED\n        ),\n    )\n\n    cardinality = so.SchemaField(\n        qltypes.SchemaCardinality,\n        default=qltypes.SchemaCardinality.One,\n        compcoef=0.833,\n        coerce=True,\n        special_ddl_syntax=True,\n        describe_visibility=(\n            so.DescribeVisibilityPolicy.SHOW_IF_EXPLICIT_OR_DERIVED\n        ),\n    )\n\n    # Computable globals have this set to an expression\n    # defining them.\n    expr = so.SchemaField(\n        s_expr.Expression,\n        default=None,\n        coerce=True,\n        compcoef=0.909,\n        special_ddl_syntax=True,\n    )\n\n    default = so.SchemaField(\n        s_expr.Expression,\n        allow_ddl_set=True,\n        default=None,\n        coerce=True,\n        compcoef=0.909,\n    )\n\n    created_types = so.SchemaField(\n        so.ObjectSet[s_types.Type],\n        default=so.DEFAULT_CONSTRUCTOR,\n    )\n\n    def is_computable(self, schema: s_schema.Schema) -> bool:\n        return bool(self.get_expr(schema))\n\n    def needs_present_arg(self, schema: s_schema.Schema) -> bool:\n        return bool(self.get_default(schema)) and not self.get_required(schema)\n\n\nclass GlobalCommandContext(\n    sd.ObjectCommandContext[so.Object],\n    s_anno.AnnotationSubjectCommandContext\n):\n    pass\n\n\nclass GlobalCommand(\n    s_expraliases.AliasLikeCommand[Global],\n    context_class=GlobalCommandContext,\n):\n    TYPE_FIELD_NAME = 'target'\n    ALIAS_LIKE_EXPR_FIELDS = ('expr', 'default')\n\n    @classmethod\n    def _get_alias_name(cls, type_name: sn.QualName) -> sn.QualName:\n        return cls._mangle_name(type_name, include_module_in_name=False)\n\n    @classmethod\n    def _is_computable(cls, obj: Global, schema: s_schema.Schema) -> bool:\n        return obj.is_computable(schema)\n\n    def _check_expr(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        expression = self.get_attribute_value('expr')\n        assert isinstance(expression, s_expr.Expression)\n        # If it's not compiled, don't worry about it. This should just\n        # be a dummy expression.\n        if not expression.irast:\n            return schema\n\n        required, card = expression.irast.cardinality.to_schema_value()\n\n        spec_required: Optional[bool] = (\n            self.get_specified_attribute_value('required', schema, context))\n        spec_card: Optional[qltypes.SchemaCardinality] = (\n            self.get_specified_attribute_value('cardinality', schema, context))\n\n        glob_name = self.get_verbosename()\n\n        if spec_required and not required:\n            span = self.get_attribute_span('target')\n            raise errors.SchemaDefinitionError(\n                f'possibly an empty set returned by an '\n                f'expression for the computed '\n                f'{glob_name} '\n                f\"explicitly declared as 'required'\",\n                span=span\n            )\n\n        if (\n            spec_card is qltypes.SchemaCardinality.One\n            and card is not qltypes.SchemaCardinality.One\n        ):\n            span = self.get_attribute_span('target')\n            raise errors.SchemaDefinitionError(\n                f'possibly more than one element returned by an '\n                f'expression for the computed '\n                f'{glob_name} '\n                f\"explicitly declared as 'single'\",\n                span=span\n            )\n\n        if spec_card is None:\n            self.set_attribute_value('cardinality', card, computed=True)\n\n        if spec_required is None:\n            self.set_attribute_value('required', required, computed=True)\n\n        return schema\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n\n        if self.get_attribute_value('expr'):\n            schema = self._check_expr(schema, context)\n\n        schema = s_types.materialize_type_in_attribute(\n            schema, context, self, 'target')\n\n        return schema\n\n    def validate_object(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        scls = self.scls\n        is_computable = scls.is_computable(schema)\n\n        target = scls.get_target(schema)\n\n        if not is_computable:\n            if (\n                scls.get_required(schema)\n                and not scls.get_default(schema)\n            ):\n                raise errors.SchemaDefinitionError(\n                    \"required globals must have a default\",\n                    span=self.span,\n                )\n            if scls.get_cardinality(schema) == qltypes.SchemaCardinality.Many:\n                raise errors.SchemaDefinitionError(\n                    \"non-computed globals may not be multi\",\n                    span=self.span,\n                )\n            if target.contains_object(schema):\n                raise errors.SchemaDefinitionError(\n                    \"non-computed globals may not have have object type\",\n                    span=self.span,\n                )\n\n        default_expr = scls.get_default(schema)\n\n        if default_expr is not None:\n            default_expr = default_expr.ensure_compiled(schema, context=context)\n\n            default_schema = default_expr.irast.schema\n            default_type = default_expr.irast.stype\n\n            span = self.get_attribute_span('default')\n\n            if is_computable:\n                raise errors.SchemaDefinitionError(\n                    f'computed globals may not have default values',\n                    span=span,\n                )\n\n            if not default_type.assignment_castable_to(target, default_schema):\n                raise errors.SchemaDefinitionError(\n                    f'default expression is of invalid type: '\n                    f'{default_type.get_displayname(default_schema)}, '\n                    f'expected {target.get_displayname(schema)}',\n                    span=span,\n                )\n\n            ptr_cardinality = scls.get_cardinality(schema)\n            default_required, default_cardinality = \\\n                default_expr.irast.cardinality.to_schema_value()\n\n            if (ptr_cardinality is qltypes.SchemaCardinality.One\n                    and default_cardinality != ptr_cardinality):\n                raise errors.SchemaDefinitionError(\n                    f'possibly more than one element returned by '\n                    f'the default expression for '\n                    f'{scls.get_verbosename(schema)} declared as '\n                    f\"'single'\",\n                    span=span,\n                )\n\n            if scls.get_required(schema) and not default_required:\n                raise errors.SchemaDefinitionError(\n                    f'possibly no elements returned by '\n                    f'the default expression for '\n                    f'{scls.get_verbosename(schema)} declared as '\n                    f\"'required'\",\n                    span=span,\n                )\n\n            if default_expr.irast.volatility.is_volatile():\n                raise errors.SchemaDefinitionError(\n                    f'{scls.get_verbosename(schema)} has a volatile '\n                    f'default expression, which is not allowed',\n                    span=span,\n                )\n\n    def compile_expr_field(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: s_expr.Expression,\n        track_schema_ref_exprs: bool=False,\n    ) -> s_expr.CompiledExpression:\n        if field.name in {'default', 'expr'}:\n            ptr_name = self.get_verbosename()\n            in_ddl_context_name = None\n            if field.name == 'expr':\n                in_ddl_context_name = f'computed {ptr_name}'\n\n            return value.compiled(\n                schema=schema,\n                options=qlcompiler.CompilerOptions(\n                    modaliases=context.modaliases,\n                    schema_object_context=self.get_schema_metaclass(),\n                    apply_query_rewrites=not context.stdmode,\n                    track_schema_ref_exprs=track_schema_ref_exprs,\n                    in_ddl_context_name=in_ddl_context_name,\n                ),\n                context=context,\n            )\n        else:\n            return super().compile_expr_field(\n                schema, context, field, value, track_schema_ref_exprs)\n\n\nclass CreateGlobal(\n    s_expraliases.CreateAliasLike[Global],\n    GlobalCommand,\n):\n    astnode = qlast.CreateGlobal\n\n    def get_ast_attr_for_field(\n        self,\n        field: str,\n        astnode: type[qlast.DDLOperation],\n    ) -> Optional[str]:\n        if (\n            field == 'required'\n            and issubclass(astnode, qlast.CreateGlobal)\n        ):\n            return 'is_required'\n        elif (\n            field == 'cardinality'\n            and issubclass(astnode, qlast.CreateGlobal)\n        ):\n            return 'cardinality'\n        else:\n            return super().get_ast_attr_for_field(field, astnode)\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        assert isinstance(node, qlast.CreateGlobal)\n        if op.property == 'target':\n            if not node.target:\n                expr: Optional[s_expr.Expression] = (\n                    self.get_attribute_value('expr')\n                )\n                if expr is not None:\n                    node.target = expr.parse()\n                else:\n                    t = op.new_value\n                    assert isinstance(t, (so.Object, so.ObjectShell))\n                    node.target = utils.typeref_to_ast(schema, t)\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        assert isinstance(astnode, qlast.CreateGlobal)\n        assert isinstance(cmd, GlobalCommand)\n\n        if astnode.is_required is not None:\n            cmd.set_attribute_value(\n                'required',\n                astnode.is_required,\n                span=astnode.span,\n            )\n\n        if astnode.cardinality is not None:\n            cmd.set_attribute_value(\n                'cardinality',\n                astnode.cardinality,\n                span=astnode.span,\n            )\n\n        assert astnode.target is not None\n\n        if isinstance(astnode.target, qlast.TypeExpr):\n            type_ref = utils.ast_to_type_shell(\n                astnode.target,\n                metaclass=s_types.Type,\n                modaliases=context.modaliases,\n                schema=schema,\n            )\n            cmd.set_attribute_value(\n                'target',\n                type_ref,\n                span=astnode.target.span,\n            )\n\n        else:\n            # computable\n            qlcompiler.normalize(\n                astnode.target,\n                schema=schema,\n                modaliases=context.modaliases\n            )\n            cmd.set_attribute_value(\n                'expr',\n                s_expr.Expression.from_ast(\n                    astnode.target, schema, context.modaliases,\n                    context.localnames,\n                ),\n            )\n\n        if (\n            cmd.has_attribute_value('expr')\n            and cmd.has_attribute_value('target')\n        ):\n            raise errors.UnsupportedFeatureError(\n                \"cannot specify a type and an expression for a global\",\n                span=astnode.span,\n            )\n\n        return cmd\n\n\nclass RenameGlobal(\n    s_expraliases.RenameAliasLike[Global],\n    GlobalCommand,\n):\n    pass\n\n\nclass AlterGlobal(\n    s_expraliases.AlterAliasLike[Global],\n    GlobalCommand,\n):\n    astnode = qlast.AlterGlobal\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        if not context.canonical:\n            old_expr = self.scls.get_expr(schema)\n            has_expr = self.has_attribute_value('expr')\n            clears_expr = has_expr and not self.get_attribute_value('expr')\n\n            # Force reconsideration of the expression if cardinality\n            # or required is changed.\n            if (\n                (\n                    self.has_attribute_value('cardinality')\n                    or self.has_attribute_value('required')\n                )\n                and not has_expr\n                and old_expr\n            ):\n                self.set_attribute_value(\n                    'expr',\n                    s_expr.Expression.not_compiled(old_expr),\n                )\n\n            # Produce an error when setting a type on something with\n            # an expression\n            if (\n                self.get_attribute_value('target')\n                and (\n                    (self.scls.get_expr(schema) or has_expr)\n                    and not clears_expr\n                )\n            ):\n                raise errors.UnsupportedFeatureError(\n                    \"cannot specify a type and an expression for a global\",\n                    span=self.span,\n                )\n\n            if clears_expr and old_expr:\n                # If the expression was explicitly set to None,\n                # that means that `RESET EXPRESSION` was executed\n                # and this is no longer a computable.\n                computed_fields = self.scls.get_computed_fields(schema)\n                if (\n                    'required' in computed_fields\n                    and not self.has_attribute_value('required')\n                ):\n                    self.set_attribute_value('required', None)\n                if (\n                    'cardinality' in computed_fields\n                    and not self.has_attribute_value('cardinality')\n                ):\n                    self.set_attribute_value('cardinality', None)\n\n            if not old_expr and (old_target := self.scls.get_target(schema)):\n                if op := old_target.as_type_delete_if_unused(schema):\n                    self.add_caused(op)\n\n        return super()._alter_begin(schema, context)\n\n\nclass SetGlobalType(\n    sd.AlterSpecialObjectField[Global],\n    field='target',\n):\n\n    cast_expr = struct.Field(s_expr.Expression, default=None)\n    reset_value = struct.Field(bool, default=False)\n\n    def get_verb(self) -> str:\n        return 'alter the type of'\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        schema = super()._alter_begin(schema, context)\n        scls = self.scls\n\n        orig_target = scls.get_explicit_field_value(\n            orig_schema, 'target', None)\n        new_target = scls.get_target(schema)\n\n        if not orig_target or orig_target == new_target:\n            return schema\n\n        if not context.canonical:\n            if self.cast_expr:\n                raise errors.UnsupportedFeatureError(\n                    f'USING casts for SET TYPE on globals are not supported',\n                    hint='Use RESET TO DEFAULT instead',\n                    span=self.span,\n                )\n\n            if not self.reset_value:\n                raise errors.SchemaDefinitionError(\n                    f\"SET TYPE on global must explicitly reset the \"\n                    f\"global's value\",\n                    hint='Use RESET TO DEFAULT after the type',\n                    span=self.span,\n                )\n\n        return schema\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, SetGlobalType)\n        if (\n            isinstance(astnode, qlast.SetGlobalType)\n            and astnode.cast_expr is not None\n        ):\n            cmd.cast_expr = s_expr.Expression.from_ast(\n                astnode.cast_expr,\n                schema,\n                context.modaliases,\n                context.localnames,\n            )\n        if isinstance(astnode, qlast.SetGlobalType):\n            cmd.reset_value = astnode.reset_value\n\n        return cmd\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        set_field = super()._get_ast(schema, context, parent_node=parent_node)\n        if set_field is None or self.is_attribute_computed('target'):\n            return None\n        else:\n            assert isinstance(set_field, qlast.SetField)\n            assert not isinstance(set_field.value, qlast.Expr)\n\n            case_expr = None\n            if self.cast_expr:\n                assert isinstance(self.cast_expr, s_expr.Expression)\n                case_expr = self.cast_expr.parse()\n\n            return qlast.SetGlobalType(\n                value=set_field.value,\n                cast_expr=case_expr,\n                reset_value=self.reset_value,\n            )\n\n    def record_diff_annotations(\n        self,\n        *,\n        schema: s_schema.Schema,\n        orig_schema: Optional[s_schema.Schema],\n        context: so.ComparisonContext,\n        object: Optional[so.Object],\n        orig_object: Optional[so.Object],\n    ) -> None:\n        super().record_diff_annotations(\n            schema=schema,\n            orig_schema=orig_schema,\n            context=context,\n            orig_object=orig_object,\n            object=object,\n        )\n\n        if orig_schema is None:\n            return\n\n        if (\n            not self.get_orig_attribute_value('expr')\n            and not self.get_attribute_value('expr')\n        ):\n            self.reset_value = True\n\n\nclass DeleteGlobal(\n    s_expraliases.DeleteAliasLike[Global],\n    GlobalCommand,\n):\n    astnode = qlast.DropGlobal\n\n    def _delete_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._delete_begin(schema, context)\n        scls = self.scls\n        if not self._is_computable(scls, schema):\n            target = scls.get_target(schema)\n            if op := target.as_type_delete_if_unused(schema):\n                self.add_caused(op)\n\n        return schema\n"
  },
  {
    "path": "edb/schema/indexes.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    TypeVar,\n    Mapping,\n    Sequence,\n    cast,\n    overload,\n    TYPE_CHECKING,\n)\n\nfrom edb import edgeql\nfrom edb import errors\nfrom edb.common import parsing\nfrom edb.common import verutils\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.edgeql import qltypes\n\nfrom . import annos as s_anno\nfrom . import delta as sd\nfrom . import expr as s_expr\nfrom . import functions as s_func\nfrom . import inheriting\nfrom . import name as sn\nfrom . import pointers as s_pointers\nfrom . import objects as so\nfrom . import referencing\nfrom . import scalars as s_scalars\nfrom . import types as s_types\nfrom . import schema as s_schema\nfrom . import utils\n\n\nif TYPE_CHECKING:\n    from . import objtypes as s_objtypes\n\n\n# The name used for default concrete indexes\nDEFAULT_INDEX = sn.QualName(module='__', name='idx')\n\n\ndef is_index_valid_for_type(\n    index: Index,\n    expr_type: s_types.Type,\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n) -> bool:\n    index_allows_tuples = is_index_supporting_tuples(index, schema)\n\n    for index_match in schema.get_referrers(\n        index, scls_type=IndexMatch, field_name='index',\n    ):\n        valid_type = index_match.get_valid_type(schema)\n        if index_allows_tuples:\n            if is_subclass_or_tuple(expr_type, valid_type, schema):\n                return True\n        elif expr_type.issubclass(schema, valid_type):\n            return True\n\n    if context.testmode and str(index.get_name(schema)) == 'default::test':\n        # For functional tests of abstract indexes.\n        return expr_type.issubclass(\n            schema,\n            schema.get('std::str', type=s_scalars.ScalarType),\n        )\n\n    return False\n\n\ndef is_index_supporting_tuples(\n    index: Index,\n    schema: s_schema.Schema,\n) -> bool:\n    index_name = str(index.get_name(schema))\n    return index_name in {\n        \"std::fts::index\",\n        \"ext::pg_trgm::gin\",\n        \"ext::pg_trgm::gist\",\n        \"std::pg::gist\",\n        \"std::pg::gin\",\n        \"std::pg::brin\",\n    }\n\n\ndef is_subclass_or_tuple(\n    ty: s_types.Type, parent: s_types.Type, schema: s_schema.Schema\n) -> bool:\n    if isinstance(ty, s_types.Tuple):\n        for (_, st) in ty.iter_subtypes(schema):\n            if not st.issubclass(schema, parent):\n                return False\n        return True\n    else:\n        return ty.issubclass(schema, parent)\n\n\ndef _merge_deferrability(\n    a: qltypes.IndexDeferrability,\n    b: qltypes.IndexDeferrability,\n) -> qltypes.IndexDeferrability:\n    if a is b:\n        return a\n    else:\n        if a is qltypes.IndexDeferrability.Prohibited:\n            raise ValueError(f\"{a} and {b} are incompatible\")\n        elif a is qltypes.IndexDeferrability.Permitted:\n            return b\n        else:\n            return a\n\n\ndef merge_deferrability(\n    idx: Index,\n    bases: list[Index],\n    field_name: str,\n    *,\n    ignore_local: bool = False,\n    schema: s_schema.Schema,\n) -> Optional[qltypes.IndexDeferrability]:\n    \"\"\"Merge function for abstract index deferrability.\"\"\"\n\n    return utils.merge_reduce(\n        idx,\n        bases,\n        field_name=field_name,\n        ignore_local=ignore_local,\n        schema=schema,\n        f=_merge_deferrability,\n        type=qltypes.IndexDeferrability,\n    )\n\n\ndef merge_deferred(\n    idx: Index,\n    bases: list[Index],\n    field_name: str,\n    *,\n    ignore_local: bool = False,\n    schema: s_schema.Schema,\n) -> Optional[bool]:\n    \"\"\"Merge function for the DEFERRED qualifier on indexes.\"\"\"\n\n    if idx.is_non_concrete(schema):\n        return None\n\n    if bases:\n        deferrability = next(iter(bases)).get_deferrability(schema)\n    else:\n        deferrability = qltypes.IndexDeferrability.Prohibited\n\n    local_deferred = idx.get_explicit_local_field_value(\n        schema, field_name, None)\n\n    idx_repr = idx.get_verbosename(schema, with_parent=True)\n\n    if not idx.is_defined_here(schema):\n        ignore_local = True\n\n    if ignore_local:\n        return deferrability is qltypes.IndexDeferrability.Required\n    elif local_deferred is None:\n        # No explicit local declaration, derive from abstract index\n        # deferrability.\n        if deferrability is qltypes.IndexDeferrability.Required:\n            raise errors.SchemaDefinitionError(\n                f\"{idx_repr} must be declared as deferred\"\n            )\n        else:\n            return False\n    else:\n        if (\n            local_deferred\n            and deferrability is qltypes.IndexDeferrability.Prohibited\n        ):\n            raise errors.SchemaDefinitionError(\n                f\"{idx_repr} cannot be declared as deferred\"\n            )\n        elif (\n            not local_deferred\n            and deferrability is qltypes.IndexDeferrability.Required\n        ):\n            raise errors.SchemaDefinitionError(\n                f\"{idx_repr} must be declared as deferred\"\n            )\n\n        return local_deferred  # type: ignore\n\n\ndef get_index_match_fullname_from_names(\n    valid_type: sn.Name,\n    index: sn.Name,\n) -> sn.QualName:\n    std = not (\n        (\n            isinstance(valid_type, sn.QualName)\n            and sn.UnqualName(valid_type.module) not in s_schema.STD_MODULES\n        ) or (\n            isinstance(index, sn.QualName)\n            and sn.UnqualName(index.module) not in s_schema.STD_MODULES\n        )\n    )\n    module = 'std' if std else '__ext_index_matches__'\n\n    quals = [str(valid_type), str(index)]\n    shortname = sn.QualName(module, 'index_match')\n    return sn.QualName(\n        module=shortname.module,\n        name=sn.get_specialized_name(shortname, *quals),\n    )\n\n\ndef get_index_match_fullname(\n    schema: s_schema.Schema,\n    valid_type: s_types.TypeShell[s_types.Type],\n    index: so.ObjectShell[Index],\n) -> sn.QualName:\n    return get_index_match_fullname_from_names(\n        valid_type.get_name(schema),\n        index.get_name(schema),\n    )\n\n\nclass Index(\n    referencing.ReferencedInheritingObject,\n    so.InheritingObject,  # Help reflection figure out the right db MRO\n    s_anno.AnnotationSubject,\n    qlkind=qltypes.SchemaObjectClass.INDEX,\n    data_safe=True,\n):\n    # redefine, so we can change compcoef\n    bases = so.SchemaField(\n        so.ObjectList['Index'],  # type: ignore\n        type_is_generic_self=True,\n        default=so.DEFAULT_CONSTRUCTOR,\n        coerce=True,\n        inheritable=False,\n        compcoef=0.0,  # can't rebase\n    )\n\n    subject = so.SchemaField(\n        so.Object,\n        default=None,\n        compcoef=None,\n        inheritable=False,\n    )\n\n    # These can only appear in base abstract index definitions. These\n    # determine how indexes can be configured.\n    params = so.SchemaField(\n        s_func.FuncParameterList,\n        coerce=True,\n        compcoef=0.4,\n        default=so.DEFAULT_CONSTRUCTOR,\n        inheritable=False,\n    )\n\n    # Appears in base abstract index definitions and defines how the index\n    # is represented in postgres.\n    code = so.SchemaField(\n        str,\n        default=None,\n        compcoef=None,\n        inheritable=False,\n        allow_ddl_set=True,\n    )\n\n    # These can appear in abstract indexes extending an existing one in order\n    # to override exisitng parameters. Also they can appear in concrete\n    # indexes.\n    kwargs = so.SchemaField(\n        s_expr.ExpressionDict,\n        coerce=True,\n        compcoef=0,\n        default=so.DEFAULT_CONSTRUCTOR,\n        inheritable=False,\n        ddl_identity=True,\n    )\n\n    type_args = so.SchemaField(\n        so.ObjectList[so.Object],\n        coerce=True,\n        compcoef=0,\n        default=so.DEFAULT_CONSTRUCTOR,\n        inheritable=False,\n    )\n\n    expr = so.SchemaField(\n        s_expr.Expression,\n        default=None,\n        coerce=True,\n        compcoef=0.0,\n        ddl_identity=True,\n    )\n\n    except_expr = so.SchemaField(\n        s_expr.Expression,\n        default=None,\n        coerce=True,\n        compcoef=0.0,\n        ddl_identity=True,\n    )\n\n    deferrability = so.SchemaField(\n        qltypes.IndexDeferrability,\n        default=qltypes.IndexDeferrability.Prohibited,\n        coerce=True,\n        compcoef=0.909,\n        merge_fn=merge_deferrability,\n        allow_ddl_set=True,\n    )\n\n    deferred = so.SchemaField(\n        bool,\n        default=False,\n        compcoef=0.909,\n        special_ddl_syntax=True,\n        describe_visibility=(\n            so.DescribeVisibilityPolicy.SHOW_IF_EXPLICIT_OR_DERIVED\n        ),\n        merge_fn=merge_deferred,\n    )\n\n    # Whether the index is created and populated in pg. Relevant if\n    # build_concurrently is true?\n    active = so.SchemaField(\n        bool,\n        default=True,\n    )\n\n    # XXX: I am not sure this is what I want to do.\n    build_concurrently = so.SchemaField(\n        bool,\n        default=False,\n        compcoef=0.803,\n        allow_ddl_set=True,\n    )\n\n    def __repr__(self) -> str:\n        cls = self.__class__\n        return '<{}.{} {!r} at 0x{:x}>'.format(\n            cls.__module__, cls.__name__, self.id, id(self))\n\n    __str__ = __repr__\n\n    def as_delete_delta(\n        self,\n        *,\n        schema: s_schema.Schema,\n        context: so.ComparisonContext,\n    ) -> sd.ObjectCommand[Index]:\n        delta = super().as_delete_delta(schema=schema, context=context)\n        old_params = self.get_params(schema).objects(schema)\n        for p in old_params:\n            delta.add(p.as_delete_delta(schema=schema, context=context))\n\n        return delta\n\n    def get_verbosename(\n        self, schema: s_schema.Schema, *, with_parent: bool = False\n    ) -> str:\n        # baseline name for indexes\n        vn = self.get_displayname(schema)\n\n        if self.get_abstract(schema):\n            return f\"abstract index '{vn}'\"\n        else:\n            # concrete index must have a subject\n            assert self.get_subject(schema) is not None\n\n            # add kwargs (if any) to the concrete name\n            kwargs = self.get_kwargs(schema)\n            if kwargs:\n                kw = []\n                for key, val in kwargs.items():\n                    kw.append(f'{key}:={val.text}')\n                vn = f'{vn}({\", \".join(kw)})'\n\n            vn = f\"index {vn!r}\"\n\n            if with_parent:\n                return self.add_parent_name(vn, schema)\n            return vn\n\n    def add_parent_name(\n        self,\n        base_name: str,\n        schema: s_schema.Schema,\n    ) -> str:\n        # Remove the placeholder name of the generic index.\n        if base_name == f\"index '{DEFAULT_INDEX}'\":\n            base_name = 'index'\n\n        return super().add_parent_name(base_name, schema)\n\n    def is_non_concrete(self, schema: s_schema.Schema) -> bool:\n        return self.get_subject(schema) is None\n\n    @classmethod\n    def get_shortname_static(cls, name: sn.Name) -> sn.QualName:\n        name = sn.shortname_from_fullname(name)\n        assert isinstance(name, sn.QualName)\n        return name\n\n    def get_all_kwargs(\n        self,\n        schema: s_schema.Schema,\n    ) -> s_expr.ExpressionDict:\n        kwargs = s_expr.ExpressionDict()\n        all_kw = type(self).get_field('kwargs').merge_fn(\n            self,\n            self.get_ancestors(schema).objects(schema),\n            'kwargs',\n            schema=schema,\n        )\n        if all_kw:\n            kwargs.update(all_kw)\n\n        return kwargs\n\n    def get_ddl_identity(\n        self,\n        schema: s_schema.Schema,\n    ) -> Optional[dict[str, Any]]:\n        v = super().get_ddl_identity(schema) or {}\n        v['kwargs'] = self.get_all_kwargs(schema)\n        return v\n\n    def get_root(\n        self,\n        schema: s_schema.Schema,\n    ) -> Index:\n        if not self.get_abstract(schema):\n            name = sn.shortname_from_fullname(self.get_name(schema))\n            index = schema.get(name, type=Index)\n        else:\n            index = self\n\n        if index.get_bases(schema):\n            return index.get_ancestors(schema).objects(schema)[-1]\n        else:\n            return index\n\n    def get_concrete_kwargs(\n        self,\n        schema: s_schema.Schema,\n    ) -> s_expr.ExpressionDict:\n        assert not self.get_abstract(schema)\n\n        root = self.get_root(schema)\n\n        kwargs = self.get_all_kwargs(schema)\n\n        for param in root.get_params(schema).objects(schema):\n            kwname = param.get_parameter_name(schema)\n            if (\n                kwname not in kwargs and\n                (val := param.get_default(schema)) is not None\n            ):\n                kwargs[kwname] = val\n\n        for k, v in kwargs.items():\n            kwargs[k] = v.ensure_compiled(\n                schema,\n                as_fragment=True,\n                options=qlcompiler.CompilerOptions(\n                    schema_object_context=s_func.Parameter,\n                ),\n                context=None,\n            )\n\n        return kwargs\n\n    def get_concrete_kwargs_as_values(\n        self,\n        schema: s_schema.Schema,\n    ) -> dict[str, Any]:\n        kwargs = self.get_concrete_kwargs(schema)\n        return {\n            k: v.assert_compiled().as_python_value()\n            for k, v in kwargs.items()\n        }\n\n    def is_defined_here(\n        self,\n        schema: s_schema.Schema,\n    ) -> bool:\n        \"\"\"\n        Returns True iff the index has not been inherited from a parent subject,\n        and was originally defined on the subject.\n        \"\"\"\n        return all(\n            base.get_abstract(schema)\n            for base in self.get_bases(schema).objects(schema)\n        )\n\n\nIndexableSubject_T = TypeVar('IndexableSubject_T', bound='IndexableSubject')\n\n\nclass IndexableSubject(so.InheritingObject):\n    indexes_refs = so.RefDict(\n        attr='indexes',\n        ref_cls=Index)\n\n    indexes = so.SchemaField(\n        so.ObjectIndexByFullname[Index],\n        inheritable=False, ephemeral=True, coerce=True, compcoef=0.909,\n        default=so.DEFAULT_CONSTRUCTOR)\n\n    def add_index(\n        self,\n        schema: s_schema.Schema,\n        index: Index,\n    ) -> s_schema.Schema:\n        return self.add_classref(schema, 'indexes', index)\n\n\nclass IndexMatch(\n    so.QualifiedObject,\n    s_anno.AnnotationSubject,\n    qlkind=qltypes.SchemaObjectClass.INDEX_MATCH,\n    data_safe=True,\n    abstract=False,\n):\n\n    valid_type = so.SchemaField(\n        s_types.Type, compcoef=0.5)\n\n    index = so.SchemaField(\n        Index, compcoef=0.5)\n\n\nclass IndexSourceCommandContext:\n    pass\n\n\nclass IndexSourceCommand(\n    inheriting.InheritingObjectCommand[IndexableSubject_T],\n):\n    pass\n\n\nclass IndexCommandContext(sd.ObjectCommandContext[Index],\n                          s_anno.AnnotationSubjectCommandContext):\n    pass\n\n\nclass IndexMatchCommandContext(sd.ObjectCommandContext[IndexMatch],\n                               s_anno.AnnotationSubjectCommandContext):\n    pass\n\n\nclass IndexCommand(\n    referencing.ReferencedInheritingObjectCommand[Index],\n    s_func.ParametrizedCommand[Index],\n    context_class=IndexCommandContext,\n    referrer_context_class=IndexSourceCommandContext,\n):\n\n    @classmethod\n    def _classname_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext,\n    ) -> sn.QualName:\n        # We actually want to override how ReferencedObjectCommand determines\n        # the classname\n        #\n        # We need to resolve the name so that we get fully\n        # canonicalized names for things like fts::index, which are\n        # properly std::fts::index.\n        # (We have to do that ourselves here because we are skipping\n        # ReferencedObjectCommand, which would otherwise handle it.)\n        shortname = utils.resolve_name(\n            utils.ast_ref_to_name(astnode.name),\n            modaliases=context.modaliases,\n            schema=schema,\n            metaclass=cls.get_schema_metaclass(),\n        )\n\n        referrer_ctx = cls.get_referrer_context(context)\n        if referrer_ctx is not None:\n\n            referrer_name = referrer_ctx.op.classname\n            assert isinstance(referrer_name, sn.QualName)\n            quals = cls._classname_quals_from_ast(\n                schema, astnode, shortname, referrer_name, context)\n\n            name = sn.QualName(\n                module=referrer_name.module,\n                name=sn.get_specialized_name(\n                    shortname,\n                    str(referrer_name),\n                    *quals,\n                ),\n            )\n        else:\n            name = super()._classname_from_ast(schema, astnode, context)\n\n        return name\n\n    @classmethod\n    def _classname_quals_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        base_name: sn.Name,\n        referrer_name: sn.QualName,\n        context: sd.CommandContext,\n    ) -> tuple[str, ...]:\n        assert isinstance(astnode, qlast.ConcreteIndexCommand)\n        exprs = []\n\n        kwargs = cls._index_kwargs_from_ast(schema, astnode, context)\n        for key, val in kwargs.items():\n            exprs.append(f'{key}:={val.text}')\n\n        # use the normalized text directly from the expression\n        expr = s_expr.Expression.from_ast(\n            astnode.expr, schema, context.modaliases)\n        expr_text = expr.text\n        assert expr_text is not None\n        exprs.append(expr_text)\n\n        if astnode.except_expr:\n            expr = s_expr.Expression.from_ast(\n                astnode.except_expr, schema, context.modaliases)\n            exprs.append('!' + expr.text)\n\n        return (cls._name_qual_from_exprs(schema, exprs),)\n\n    @classmethod\n    def _classname_quals_from_name(cls, name: sn.QualName) -> tuple[str, ...]:\n        quals = sn.quals_from_fullname(name)\n        return tuple(quals[-1:])\n\n    @classmethod\n    def _index_kwargs_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext,\n    ) -> dict[str, s_expr.Expression]:\n        kwargs = dict()\n        # Some abstract indexes and all concrete index commands have kwargs.\n        assert isinstance(astnode, (qlast.CreateIndex,\n                                    qlast.ConcreteIndexCommand))\n\n        for key, val in astnode.kwargs.items():\n            kwargs[key] = s_expr.Expression.from_ast(\n                val, schema, context.modaliases, as_fragment=True)\n\n        return kwargs\n\n    @overload\n    def get_object(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        name: Optional[sn.Name] = None,\n        default: Index | so.NoDefaultT = so.NoDefault,\n        span: Optional[parsing.Span] = None,\n    ) -> Index:\n        ...\n\n    @overload\n    def get_object(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        name: Optional[sn.Name] = None,\n        default: None = None,\n        span: Optional[parsing.Span] = None,\n    ) -> Optional[Index]:\n        ...\n\n    def get_object(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        name: Optional[sn.Name] = None,\n        default: Index | so.NoDefaultT | None = so.NoDefault,\n        span: Optional[parsing.Span] = None,\n    ) -> Optional[Index]:\n        try:\n            return super().get_object(\n                schema, context, name=name,\n                default=default, span=span,\n            )\n        except errors.InvalidReferenceError:\n            referrer_ctx = self.get_referrer_context_or_die(context)\n            referrer = referrer_ctx.scls\n            expr = self.get_ddl_identity('expr')\n            raise errors.InvalidReferenceError(\n                f\"index on ({expr.text}) does not exist on \"\n                f\"{referrer.get_verbosename(schema)}\"\n            ) from None\n\n    @classmethod\n    def _cmd_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.ObjectCommand[Index]:\n        cmd = super()._cmd_from_ast(schema, astnode, context)\n        if isinstance(astnode, qlast.ConcreteIndexCommand):\n            cmd.set_ddl_identity(\n                'expr',\n                s_expr.Expression.from_ast(\n                    astnode.expr,\n                    schema,\n                    context.modaliases,\n                ),\n            )\n        return cmd\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        astnode = super()._get_ast(schema, context, parent_node=parent_node)\n\n        kwargs: Optional[Mapping[str, s_expr.Expression]] = (\n            self.get_resolved_attribute_value(\n                'kwargs',\n                schema=schema,\n                context=context,\n            )\n        )\n        if kwargs and astnode:\n            assert isinstance(astnode, (qlast.CreateIndex,\n                                        qlast.ConcreteIndexCommand))\n            astnode.kwargs = {\n                name: expr.parse() for name, expr in kwargs.items()\n            }\n\n        return astnode\n\n    def get_ast_attr_for_field(\n        self,\n        field: str,\n        astnode: type[qlast.DDLOperation],\n    ) -> Optional[str]:\n        if field in ('kwargs', 'expr', 'except_expr'):\n            return field\n        elif (\n            field == 'deferred'\n            and astnode is qlast.CreateConcreteIndex\n        ):\n            return field\n        else:\n            return super().get_ast_attr_for_field(field, astnode)\n\n    def get_ddl_identity_fields(\n        self,\n        context: sd.CommandContext,\n    ) -> tuple[so.Field[Any], ...]:\n        id_fields = super().get_ddl_identity_fields(context)\n        omit_fields = set()\n\n        if (\n            self.get_attribute_value('abstract')\n            and not self.get_attribute_value('bases')\n        ):\n            # Base abstract indexes don't have kwargs at all.\n            omit_fields.add('kwargs')\n\n        if omit_fields:\n            return tuple(f for f in id_fields if f.name not in omit_fields)\n        else:\n            return id_fields\n\n    def get_friendly_object_name_for_description(\n        self,\n        *,\n        parent_op: Optional[sd.Command] = None,\n        schema: Optional[s_schema.Schema] = None,\n        object: Optional[so.Object_T] = None,\n        object_desc: Optional[str] = None,\n    ) -> str:\n        friendly_name: str = 'index'\n\n        expr: Optional[s_expr.Expression] = None\n        if (\n            self.has_ddl_identity('expr') and\n            (expr := self.get_ddl_identity('expr'))\n        ):\n            expr_text = expr.text\n            if expr_text[0] != '(' or expr_text[-1] != ')':\n                expr_text = '(' + expr_text + ')'\n\n            friendly_name = f\"index on {expr_text}\"\n\n        if not isinstance(parent_op, sd.ObjectCommand):\n            return f\"{friendly_name}\"\n        else:\n            return f\"{friendly_name} of {parent_op.get_verbosename()}\"\n\n    def compile_expr_field(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: s_expr.Expression,\n        track_schema_ref_exprs: bool=False,\n    ) -> s_expr.CompiledExpression:\n        from edb.ir import ast as irast\n        from edb.ir import utils as irutils\n\n        if field.name in {'expr', 'except_expr'}:\n            # type ignore below, for the class is used as mixin\n            parent_ctx = context.get_ancestor(\n                IndexSourceCommandContext,  # type: ignore\n                self\n            )\n            assert parent_ctx is not None\n            assert isinstance(parent_ctx.op, sd.ObjectCommand)\n            subject = parent_ctx.op.get_object(schema, context)\n\n            expr = value.compiled(\n                schema=schema,\n                options=qlcompiler.CompilerOptions(\n                    modaliases=context.modaliases,\n                    schema_object_context=self.get_schema_metaclass(),\n                    anchors={'__subject__': subject},\n                    path_prefix_anchor='__subject__',\n                    singletons=frozenset([subject]),\n                    apply_query_rewrites=False,\n                    track_schema_ref_exprs=track_schema_ref_exprs,\n                    detached=True,\n                ),\n                context=context,\n            )\n\n            # Check that the inferred cardinality is no more than 1\n            if expr.irast.cardinality.is_multi():\n                raise errors.SchemaDefinitionError(\n                    f'possibly more than one element returned by '\n                    f'the index expression where only singletons '\n                    f'are allowed',\n                    span=value.parse().span,\n                )\n\n            if expr.irast.volatility != qltypes.Volatility.Immutable:\n                raise errors.SchemaDefinitionError(\n                    f'index expressions must be immutable',\n                    span=value.parse().span,\n                )\n\n            refs = irutils.get_longest_paths(expr.irast)\n\n            has_multi = False\n            for ref in refs:\n                assert subject\n                # Subject is a singleton in an index expression if it is itself\n                # a singleton, regardless of other parts of the path.\n                if irutils.ref_contains_multi(ref, subject.id):\n                    has_multi = True\n                    break\n\n            if set_of_op := irutils.find_set_of_op(\n                expr.irast,\n                has_multi,\n            ):\n                label = (\n                    'function'\n                    if isinstance(set_of_op, irast.FunctionCall) else\n                    'operator'\n                )\n                op_name = str(set_of_op.func_shortname)\n                raise errors.SchemaDefinitionError(\n                    f\"cannot use SET OF {label} '{op_name}' \"\n                    f\"in an index expression\",\n                    span=set_of_op.span\n                )\n\n            # compile the expression to sql to preempt errors downstream\n            utils.try_compile_irast_to_sql_tree(expr, self.span)\n\n            return expr\n        elif field.name == \"kwargs\":\n            parent_ctx = context.get_ancestor(\n                IndexSourceCommandContext,  # type: ignore\n                self\n            )\n            if parent_ctx is not None:\n                assert isinstance(parent_ctx.op, sd.ObjectCommand)\n                subject = parent_ctx.op.get_object(schema, context)\n                subject_vname = subject.get_verbosename(schema)\n                idx_name = self.get_verbosename(parent=subject_vname)\n            else:\n                idx_name = self.get_verbosename()\n            return type(value).compiled(\n                value,\n                schema=schema,\n                options=qlcompiler.CompilerOptions(\n                    modaliases=context.modaliases,\n                    schema_object_context=self.get_schema_metaclass(),\n                    apply_query_rewrites=not context.stdmode,\n                    track_schema_ref_exprs=track_schema_ref_exprs,\n                    in_ddl_context_name=idx_name,\n                    detached=True,\n                ),\n                context=context,\n            )\n        else:\n            return super().compile_expr_field(\n                schema, context, field, value, track_schema_ref_exprs)\n\n    def get_dummy_expr_field_value(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: Any,\n    ) -> Optional[s_expr.Expression]:\n        if field.name == 'expr':\n            return s_expr.Expression(text='0')\n        else:\n            raise NotImplementedError(f'unhandled field {field.name!r}')\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n\n        referrer_ctx = self.get_referrer_context(context)\n        if referrer_ctx is not None:\n            # Concrete index\n            deferrability = self.get_attribute_value(\"deferrability\")\n            if deferrability is not None:\n                raise errors.SchemaDefinitionError(\n                    \"deferrability can only be specified on abstract indexes\",\n                    span=self.get_attribute_span(\"deferrability\"),\n                )\n        return schema\n\n    def ast_ignore_field_ownership(self, field: str) -> bool:\n        \"\"\"Whether to force generating an AST even though field isn't owned\"\"\"\n        return field == \"deferred\"\n\n    def _append_subcmd_ast(\n        self,\n        schema: s_schema.Schema,\n        node: qlast.DDLOperation,\n        subcmd: sd.Command,\n        context: sd.CommandContext,\n    ) -> None:\n        if isinstance(subcmd, s_anno.AnnotationValueCommand):\n            pname = sn.shortname_from_fullname(subcmd.classname)\n            assert isinstance(pname, sn.QualName)\n            # Skip injected annotations\n            if pname.module == \"ext::ai\":\n                return\n\n        super()._append_subcmd_ast(schema, node, subcmd, context)\n\n\nclass CreateIndex(\n    IndexCommand,\n    referencing.CreateReferencedInheritingObject[Index],\n):\n    astnode = [qlast.CreateConcreteIndex, qlast.CreateIndex]\n    referenced_astnode = qlast.CreateConcreteIndex\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        assert isinstance(cmd, IndexCommand)\n        assert isinstance(astnode, (qlast.CreateConcreteIndex,\n                                    qlast.CreateIndex))\n\n        if isinstance(astnode, qlast.CreateIndex):\n            cmd.set_attribute_value('abstract', True)\n\n            params = cls._get_param_desc_from_ast(\n                schema, context.modaliases, astnode)\n            for param in params:\n                # as_create_delta requires the specific type\n                cmd.add_prerequisite(param.as_create_delta(\n                    schema, cmd.classname, context=context))\n\n            # There are several possibilities for abstract indexes:\n            # 1) base abstract index\n            # 2) an abstract index extending another one\n            # 3) an abstract index listing index fallback alternatives\n            if astnode.bases is None:\n                if astnode.index_types is None:\n                    # This actually defines a new index (1).\n                    pass\n                else:\n                    # This is for index fallback alternatives (3).\n                    raise NotImplementedError(\"Index fallback not implemented\")\n            else:\n                # Extending existing indexes for composition (2).\n                kwargs = cls._index_kwargs_from_ast(schema, astnode, context)\n                if kwargs:\n                    cmd.set_attribute_value('kwargs', kwargs)\n\n        elif isinstance(astnode, qlast.CreateConcreteIndex):\n            orig_text = cls.get_orig_expr_text(schema, astnode, 'expr')\n\n            if (\n                orig_text is not None\n                and context.compat_ver_is_before(\n                    (1, 0, verutils.VersionStage.ALPHA, 6)\n                )\n            ):\n                # Versions prior to a6 used a different expression\n                # normalization strategy, so we must renormalize the\n                # expression.\n                expr_ql = qlcompiler.renormalize_compat(\n                    astnode.expr,\n                    orig_text,\n                    schema=schema,\n                    localnames=context.localnames,\n                )\n            else:\n                expr_ql = astnode.expr\n\n            kwargs = cls._index_kwargs_from_ast(schema, astnode, context)\n            if kwargs:\n                cmd.set_attribute_value('kwargs', kwargs)\n\n            cmd.set_attribute_value(\n                'expr',\n                s_expr.Expression.from_ast(\n                    expr_ql,\n                    schema,\n                    context.modaliases,\n                ),\n            )\n\n            if astnode.except_expr:\n                cmd.set_attribute_value(\n                    'except_expr',\n                    s_expr.Expression.from_ast(\n                        astnode.except_expr,\n                        schema,\n                        context.modaliases,\n                    ),\n                )\n\n            if astnode.deferred:\n                cmd.set_attribute_value(\n                    'deferred',\n                    astnode.deferred,\n                    span=astnode.span,\n                )\n\n            if cmd.get_attribute_span('build_concurrently'):\n                cmd.set_attribute_value(\n                    'active',\n                    False,\n                    span=astnode.span,\n                )\n\n        return cmd\n\n    @classmethod\n    def as_inherited_ref_cmd(\n        cls,\n        *,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        astnode: qlast.ObjectDDL,\n        bases: list[Index],\n        referrer: so.Object,\n    ) -> sd.ObjectCommand[Index]:\n        cmd = super().as_inherited_ref_cmd(\n            schema=schema,\n            context=context,\n            astnode=astnode,\n            bases=bases,\n            referrer=referrer,\n        )\n        assert isinstance(astnode, qlast.ConcreteIndexCommand), astnode\n        if astnode.kwargs:\n            cmd.set_attribute_value(\n                'kwargs',\n                cls._index_kwargs_from_ast(schema, astnode, context),\n            )\n        return cmd\n\n    @classmethod\n    def as_inherited_ref_ast(\n        cls,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        name: sn.Name,\n        parent: referencing.ReferencedObject,\n    ) -> qlast.ObjectDDL:\n        assert isinstance(parent, Index)\n        astnode_cls = cls.referenced_astnode\n\n        expr = parent.get_expr(schema)\n        assert expr is not None\n        expr_ql = edgeql.parse_fragment(expr.text)\n\n        except_expr: s_expr.Expression | None = parent.get_except_expr(schema)\n        if except_expr:\n            except_expr_ql = except_expr.parse()\n        else:\n            except_expr_ql = None\n\n        qlkwargs = {\n            key: val.parse() for key, val in parent.get_kwargs(schema).items()\n        }\n\n        return astnode_cls(\n            name=cls.get_inherited_ref_name(schema, context, parent, name),\n            kwargs=qlkwargs,\n            expr=expr_ql,\n            except_expr=except_expr_ql,\n            deferred=parent.get_deferred(schema),\n        )\n\n    @classmethod\n    def get_inherited_ref_name(\n        cls,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        parent: so.Object,\n        name: sn.Name,\n    ) -> qlast.ObjectRef:\n        bn = sn.shortname_from_fullname(name)\n        return utils.name_to_ast_ref(bn)\n\n    def _validate_kwargs(\n        self,\n        schema: s_schema.Schema,\n        params: s_func.FuncParameterList,\n        kwargs: s_expr.ExpressionDict,\n        ancestor_name: str,\n    ) -> None:\n        if not kwargs:\n            return\n\n        if not params:\n            raise errors.SchemaDefinitionError(\n                f'the {ancestor_name} does not support any parameters',\n                span=self.span\n            )\n\n        # Make sure that the kwargs are valid.\n        for key in kwargs:\n            expr = kwargs[key]\n            param = params.get_by_name(schema, key)\n            if param is None:\n                raise errors.SchemaDefinitionError(\n                    f'the {ancestor_name} does not have a parameter {key!r}',\n                    span=self.span\n                )\n\n            param_type = param.get_type(schema)\n            comp_expr = s_expr.Expression.compiled(\n                expr, schema=schema, context=None)\n            expr_type = comp_expr.irast.stype\n\n            if (\n                not param_type.is_polymorphic(schema) and\n                not expr_type.is_polymorphic(schema) and\n                not expr_type.implicitly_castable_to(\n                    param_type, schema)\n            ):\n                raise errors.SchemaDefinitionError(\n                    f'the {key!r} parameter of the '\n                    f'{self.get_verbosename()} has type of '\n                    f'{expr_type.get_displayname(schema)} that '\n                    f'is not implicitly castable to the '\n                    f'corresponding parameter of the '\n                    f'{ancestor_name} with type '\n                    f'{param_type.get_displayname(schema)}',\n                    span=self.span,\n                )\n\n    def validate_object(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        super().validate_object(schema, context)\n\n        referrer_ctx = self.get_referrer_context(context)\n\n        # Get kwargs if any, so that we can process them later.\n        kwargs = self.get_resolved_attribute_value(\n            'kwargs',\n            schema=schema,\n            context=context,\n        )\n\n        if referrer_ctx is None:\n            # Make sure that all bases are ultimately inherited from the same\n            # root base class.\n            bases = self.get_resolved_attribute_value(\n                'bases',\n                schema=schema,\n                context=context,\n            )\n            if bases:\n                # Users can extend abstract indexes.\n                root = None\n                for base in bases.objects(schema):\n                    lineage = [base] + list(\n                        base.get_ancestors(schema).objects(schema))\n\n                    if root is None:\n                        root = lineage[-1]\n                    elif root != lineage[-1]:\n                        raise errors.SchemaDefinitionError(\n                            f'cannot create {self.get_verbosename()} '\n                            f'because it extends incompatible abstract indxes',\n                            span=self.span\n                        )\n\n                # We should have found a root because we have bases.\n                assert root is not None\n                # Make sure that the kwargs are valid.\n                self._validate_kwargs(\n                    schema,\n                    root.get_params(schema),\n                    kwargs,\n                    root.get_verbosename(schema),\n                )\n\n            else:\n                # Creating new abstract indexes is only allowed in \"EdgeDB\n                # developer\" mode, i.e. when populating std library, etc.\n                if not context.stdmode and not context.testmode:\n                    raise errors.SchemaDefinitionError(\n                        f'cannot create {self.get_verbosename()} '\n                        f'because user-defined abstract indexes are not '\n                        f'supported',\n                        span=self.span\n                    )\n\n            return\n\n        # The checks below apply only to concrete indexes.\n        subject = referrer_ctx.scls\n        assert isinstance(subject, (s_types.Type, s_pointers.Pointer))\n        assert isinstance(subject, IndexableSubject)\n\n        if (\n            is_object_scope_index(schema, self.scls)\n            and isinstance(subject, s_pointers.Pointer)\n        ):\n            dn = self.scls.get_displayname(schema)\n            raise errors.SchemaDefinitionError(\n                f\"{dn} cannot be declared on links\",\n                span=self.span,\n            )\n\n        # Ensure that the name of the index (if given) matches an existing\n        # abstract index.\n        name = sn.shortname_from_fullname(\n            self.get_resolved_attribute_value(\n                'name',\n                schema=schema,\n                context=context,\n            )\n        )\n\n        # HACK: the old concrete indexes all have names in form __::idx, but\n        # this should be the actual name provided. Also the index without name\n        # defaults to '__::idx'.\n        if name != DEFAULT_INDEX and (\n            abs_index := schema.get(name, type=Index)\n        ):\n            # only abstract indexes should have unmangled names\n            assert abs_index.get_abstract(schema)\n            root = abs_index.get_root(schema)\n\n            # For indexes that can only appear once per object, call\n            # get_effective_object_index for its side-effect of\n            # checking the error.\n            if is_exclusive_object_scope_index(schema, self.scls):\n                effective, others = get_effective_object_index(\n                    schema, subject, root.get_name(schema), span=self.span)\n                if effective == self.scls and others:\n                    other = others[0]\n                    if (\n                        other.get_concrete_kwargs_as_values(schema)\n                        != self.scls.get_concrete_kwargs_as_values(schema)\n                    ):\n                        subject_name = subject.get_verbosename(schema)\n                        other_subject = other.get_subject(schema)\n                        assert other_subject\n                        other_name = other_subject.get_verbosename(schema)\n                        raise errors.InvalidDefinitionError(\n                            f\"{root.get_name(schema)} indexes defined for \"\n                            f\"{subject_name} with different \"\n                            f\"parameters than on base type {other_name}\",\n                            span=self.span,\n                        )\n\n            # Make sure that kwargs and parameters match in name and type.\n            # Also make sure that all parameters have values at this point\n            # (either default or provided in kwargs).\n            params = root.get_params(schema)\n            inh_kwargs = self.scls.get_all_kwargs(schema)\n\n            self._validate_kwargs(schema,\n                                  params,\n                                  kwargs,\n                                  abs_index.get_verbosename(schema))\n\n            unused_names = {p.get_parameter_name(schema)\n                            for p in params.objects(schema)}\n            if kwargs:\n                unused_names -= set(kwargs)\n            if inh_kwargs:\n                unused_names -= set(inh_kwargs)\n            if unused_names:\n                # Check that all of these parameters have defaults.\n                for pname in list(unused_names):\n                    param = params.get_by_name(schema, pname)\n                    if param and param.get_default(schema) is not None:\n                        unused_names.discard(pname)\n\n            if unused_names:\n                names = ', '.join(repr(n) for n in sorted(unused_names))\n                raise errors.SchemaDefinitionError(\n                    f'cannot create {self.get_verbosename()} '\n                    f'because the following parameters are still undefined: '\n                    f'{names}.',\n                    span=self.span\n                )\n\n            # Make sure that the concrete index expression type matches the\n            # abstract index type.\n            expr = self.get_resolved_attribute_value(\n                'expr',\n                schema=schema,\n                context=context,\n            )\n            options = qlcompiler.CompilerOptions(\n                anchors={'__subject__': subject},\n                path_prefix_anchor='__subject__',\n                singletons=frozenset([subject]),\n                apply_query_rewrites=False,\n                schema_object_context=self.get_schema_metaclass(),\n            )\n            comp_expr = s_expr.Expression.compiled(\n                expr, schema=schema, options=options, context=context\n            )\n            expr_type = comp_expr.irast.stype\n\n            if not is_index_valid_for_type(\n                root, expr_type, comp_expr.schema, context,\n            ):\n                hint = None\n                if str(name) == 'std::fts::index':\n                    hint = (\n                        'std::fts::document can be constructed with '\n                        'std::fts::with_options(str, ...)'\n                    )\n\n                raise errors.SchemaDefinitionError(\n                    f'index expression ({expr.text}) '\n                    f'is not of a valid type for the '\n                    f'{self.scls.get_verbosename(comp_expr.schema)}',\n                    span=self.span,\n                    details=hint,\n                )\n\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._create_begin(schema, context)\n        referrer_ctx = self.get_referrer_context(context)\n        if (\n            referrer_ctx is not None\n            and not context.canonical\n            and is_ext_ai_index(schema, self.scls)\n        ):\n            schema = self._inject_ext_ai_model_dependency(schema, context)\n\n        return schema\n\n    def _create_innards(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        referrer_ctx = self.get_referrer_context(context)\n        if (\n            referrer_ctx is not None\n            and not context.canonical\n            and is_ext_ai_index(schema, self.scls)\n        ):\n            self._copy_ext_ai_model_annotations(schema, context)\n\n        return super()._create_innards(schema, context)\n\n    def get_resolved_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> dict[str, Any]:\n        params = self._get_params(schema, context)\n        props = super().get_resolved_attributes(schema, context)\n        props['params'] = params\n        return props\n\n    @classmethod\n    def _classbases_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext,\n    ) -> list[so.ObjectShell[Index]]:\n        if (\n            isinstance(astnode, qlast.CreateConcreteIndex)\n            and astnode.name\n            and astnode.name.module != DEFAULT_INDEX.module\n            and astnode.name.name != DEFAULT_INDEX.name\n        ):\n            base = utils.ast_objref_to_object_shell(\n                astnode.name,\n                metaclass=Index,\n                schema=schema,\n                modaliases=context.modaliases,\n            )\n            return [base]\n        else:\n            return super()._classbases_from_ast(schema, astnode, context)\n\n    def _inject_ext_ai_model_dependency(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        model_stype = self._get_referenced_embedding_model(schema, context)\n\n        type_args = so.ObjectList.create(\n            schema,\n            [model_stype],\n        )\n\n        self.set_attribute_value(\n            \"type_args\",\n            type_args.as_shell(schema),\n        )\n\n        return self.scls.update(schema, {\"type_args\": type_args})\n\n    def _copy_ext_ai_model_annotations(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        # Copy ext::ai:: annotations declared on the model specified\n        # by the `embedding_model` kwarg.  This is necessary to avoid\n        # expensive lookups later where the index is used.\n        model_stype = self._get_referenced_embedding_model(schema, context)\n        model_stype_vn = model_stype.get_verbosename(schema)\n        model_annos = model_stype.get_annotations(schema)\n        my_name = self.scls.get_name(schema)\n        idx_defined_here = self.scls.is_defined_here(schema)\n\n        for model_anno in model_annos.objects(schema):\n            anno_name = model_anno.get_shortname(schema)\n            if anno_name.module != \"ext::ai\":\n                continue\n            value = model_anno.get_value(schema)\n            if value is None or value == \"<must override>\":\n                raise errors.SchemaDefinitionError(\n                    f\"{model_stype_vn} is missing a value for the \"\n                    f\"'{anno_name}' annotation\"\n                )\n            anno_sname = sn.get_specialized_name(\n                anno_name,\n                str(my_name),\n            )\n            anno_fqname = sn.QualName(my_name.module, anno_sname)\n            schema1 = model_anno.update(\n                schema,\n                {\n                    \"name\": anno_fqname,\n                    \"subject\": self.scls,\n                },\n            )\n            anno_copy = schema1.get(\n                anno_fqname,\n                type=s_anno.AnnotationValue,\n            )\n\n            anno_cmd: sd.ObjectCommand[s_anno.AnnotationValue]\n            if idx_defined_here:\n                anno_cmd = anno_copy.as_create_delta(\n                    schema1, so.ComparisonContext())\n                anno_cmd.discard_attribute(\"bases\")\n                anno_cmd.discard_attribute(\"ancestors\")\n            else:\n                anno_cmd = sd.get_object_delta_command(\n                    objtype=s_anno.AnnotationValue,\n                    cmdtype=sd.AlterObject,\n                    schema=schema,\n                    name=anno_fqname,\n                )\n                anno_cmd.set_attribute_value(\"owned\", True)\n\n            self.add(anno_cmd)\n\n        model_dimensions = model_stype.must_get_json_annotation(\n            schema,\n            sn.QualName(\"ext::ai\", \"embedding_model_max_output_dimensions\"),\n            int,\n        )\n        supports_shortening = model_stype.must_get_json_annotation(\n            schema,\n            sn.QualName(\"ext::ai\", \"embedding_model_supports_shortening\"),\n            bool,\n        )\n\n        kwargs = self.scls.get_concrete_kwargs_as_values(schema)\n        specified_dimensions = kwargs[\"dimensions\"]\n\n        MAX_DIM = 2000  # pgvector limit\n\n        if specified_dimensions is None:\n            if model_dimensions > MAX_DIM:\n                if not supports_shortening:\n                    raise errors.SchemaDefinitionError(\n                        f\"{model_stype_vn} returns embeddings with over \"\n                        f\"{MAX_DIM} dimensions, does not support embedding \"\n                        f\"shortening, and thus cannot be used with \"\n                        f\"this index\",\n                        span=self.span,\n                    )\n                else:\n                    dimensions = MAX_DIM\n            else:\n                dimensions = model_dimensions\n        else:\n            if specified_dimensions > MAX_DIM:\n                raise errors.SchemaDefinitionError(\n                    f\"cannot use more than {MAX_DIM} dimensions with \"\n                    f\"this index\",\n                    span=self.span,\n                )\n            elif specified_dimensions > model_dimensions:\n                raise errors.SchemaDefinitionError(\n                    f\"{model_stype_vn} does not support more than \"\n                    f\"{model_dimensions} dimensions, \"\n                    f\"got {specified_dimensions}\",\n                    span=self.span,\n                )\n            elif (\n                specified_dimensions != model_dimensions\n                and not supports_shortening\n            ):\n                raise errors.SchemaDefinitionError(\n                    f\"{model_stype_vn} returns embeddings with over \"\n                    f\"{model_dimensions} dimensions, and does not support \"\n                    f\"embedding shortening, and thus {specified_dimensions} \"\n                    f\"cannot be used for this index\",\n                    span=self.span,\n                )\n            else:\n                dimensions = specified_dimensions\n\n        dims_anno_sname = sn.get_specialized_name(\n            sn.QualName(\"ext::ai\", \"embedding_dimensions\"),\n            str(my_name),\n        )\n        alter_anno = sd.get_object_delta_command(\n            objtype=s_anno.AnnotationValue,\n            cmdtype=sd.AlterObject,\n            schema=schema,\n            name=sn.QualName(my_name.module, dims_anno_sname),\n        )\n        alter_anno.set_attribute_value(\"value\", str(dimensions))\n        alter_anno.set_attribute_value(\"owned\", True)\n        self.add(alter_anno)\n\n    def _get_referenced_embedding_model(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_objtypes.ObjectType:\n        # Copy ext::ai:: annotations declared on the model specified\n        # by the `embedding_model` kwarg.  This is necessary to avoid\n        # expensive lookups later where the index is used.\n        kwargs = self.scls.get_concrete_kwargs_as_values(schema)\n        model_name = kwargs[\"embedding_model\"]\n\n        models = get_defined_ext_ai_embedding_models(schema, model_name)\n        if len(models) == 0:\n            raise errors.SchemaDefinitionError(\n                f'undefined embedding model: no subtype of '\n                f'ext::ai::EmbeddingModel is annotated as {model_name!r}',\n                span=self.span,\n            )\n        elif len(models) > 1:\n            models_dn = [\n                model.get_displayname(schema) for model in models.values()\n            ]\n            raise errors.SchemaDefinitionError(\n                f'expecting only one embedding model to be annotated '\n                f'with ext::ai::model_name={model_name!r}: got multiple: '\n                f'{\", \".join(models_dn)}',\n                span=self.span,\n            )\n\n        return next(iter(models.values()))\n\n\nclass RenameIndex(\n    IndexCommand,\n    referencing.RenameReferencedInheritingObject[Index],\n):\n\n    @classmethod\n    def _cmd_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> RenameIndex:\n        return cast(\n            RenameIndex,\n            super()._cmd_from_ast(schema, astnode, context),\n        )\n\n\nclass AlterIndexOwned(\n    IndexCommand,\n    referencing.AlterOwned[Index],\n    field='owned',\n):\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._alter_begin(schema, context)\n        referrer_ctx = self.get_referrer_context(context)\n        if (\n            referrer_ctx is not None\n            and not context.canonical\n            and is_ext_ai_index(schema, self.scls)\n        ):\n            schema = self._fixup_ext_ai_model_annotations(schema, context)\n\n        return schema\n\n    def _fixup_ext_ai_model_annotations(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        # Fixup the special ext::ai annotations that got copied to an\n        # ai index. They are always owned, even if the index is not,\n        # and so we have some hackiness to keep that true when DROP OWNED\n        # is run on the index.\n        # TODO: Can this be rationalized more?\n\n        for ref in self.scls.get_annotations(schema).objects(schema):\n            anno_name = ref.get_shortname(schema)\n            if anno_name.module != \"ext::ai\":\n                continue\n            alter = ref.init_delta_command(schema, sd.AlterObject)\n            alter.set_attribute_value('owned', True)\n            if anno_name.name == 'embedding_dimensions':\n                alter.set_attribute_value(\n                    'value', ref.get_value(schema), inherited=False)\n            schema = alter.apply(schema, context)\n            self.add(alter)\n\n        return schema\n\n\nclass AlterIndex(\n    IndexCommand,\n    referencing.AlterReferencedInheritingObject[Index],\n):\n    astnode = [qlast.AlterConcreteIndex, qlast.AlterIndex]\n    referenced_astnode = qlast.AlterConcreteIndex\n\n    def validate_object(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        super().validate_object(schema, context)\n\n        vn = self.scls.get_verbosename(schema, with_parent=True)\n        if (\n            not self.scls.get_build_concurrently(schema)\n            and not self.scls.get_active(schema)\n        ):\n            raise errors.SchemaDefinitionError(\n                f'{vn} is not active, so build_concurrently may '\n                f'not be cleared',\n                span=self.span,\n            )\n\n    def canonicalize_alter_from_external_ref(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        if (\n            not self.get_attribute_value('abstract')\n            and (indexexpr := self.get_attribute_value('expr')) is not None\n        ):\n            assert isinstance(indexexpr, s_expr.Expression)\n\n            # To compute the new name, we construct an AST of the\n            # index, since that is the infrastructure we have for\n            # computing the classname.\n            name = sn.shortname_from_fullname(self.classname)\n            assert isinstance(name, sn.QualName), \"expected qualified name\"\n            ast = qlast.CreateConcreteIndex(\n                name=qlast.ObjectRef(name=name.name, module=name.module),\n                expr=indexexpr.parse(),\n            )\n            quals = sn.quals_from_fullname(self.classname)\n            new_name = self._classname_from_ast_and_referrer(\n                schema, sn.QualName.from_string(quals[0]), ast, context)\n            if new_name == self.classname:\n                return\n\n            rename = self.scls.init_delta_command(\n                schema, sd.RenameObject, new_name=new_name)\n            rename.set_attribute_value(\n                'name', value=new_name, orig_value=self.classname)\n            self.add(rename)\n\n\nclass DeleteIndex(\n    IndexCommand,\n    referencing.DeleteReferencedInheritingObject[Index],\n):\n    astnode = [qlast.DropConcreteIndex, qlast.DropIndex]\n    referenced_astnode = qlast.DropConcreteIndex\n\n    def _delete_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._delete_begin(schema, context)\n        if not context.canonical:\n            for param in self.scls.get_params(schema).objects(schema):\n                self.add(param.init_delta_command(schema, sd.DeleteObject))\n        return schema\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        if isinstance(astnode, qlast.ConcreteIndexCommand):\n            cmd.set_attribute_value(\n                'expr',\n                s_expr.Expression.from_ast(\n                    astnode.expr, schema, context.modaliases),\n            )\n\n        return cmd\n\n\nclass RebaseIndex(\n    IndexCommand,\n    referencing.RebaseReferencedInheritingObject[Index],\n):\n    pass\n\n\ndef get_effective_object_index(\n    schema: s_schema.Schema,\n    subject: IndexableSubject,\n    base_idx_name: sn.QualName,\n    span: Optional[parsing.Span] = None,\n) -> tuple[Optional[Index], Sequence[Index]]:\n    \"\"\"\n    Returns the effective index of a subject and any overridden fs indexes\n    \"\"\"\n    indexes: so.ObjectIndexByFullname[Index] = subject.get_indexes(schema)\n\n    base = schema.get(base_idx_name, type=Index, default=None)\n    if base is None:\n        # Abstract base index does not exist.\n        return (None, ())\n\n    object_indexes = [\n        ind\n        for ind in indexes.objects(schema)\n        if ind.issubclass(schema, base)\n    ]\n    if len(object_indexes) == 0:\n        return (None, ())\n\n    object_indexes_defined_here = [\n        ind for ind in object_indexes if ind.is_defined_here(schema)\n    ]\n\n    if len(object_indexes_defined_here) > 0:\n        # indexes defined here have priority\n\n        if len(object_indexes_defined_here) > 1:\n            subject_name = subject.get_displayname(schema)\n            raise errors.InvalidDefinitionError(\n                f'multiple {base_idx_name} indexes defined for {subject_name}',\n                span=span,\n            )\n        effective = object_indexes_defined_here[0]\n        overridden = [\n            i for i in object_indexes if i != effective\n        ]\n\n    else:\n        # there are no object-scoped indexes defined on the subject\n        # the inherited indexes take effect\n\n        if len(object_indexes) > 1:\n            subject_name = subject.get_displayname(schema)\n            raise errors.InvalidDefinitionError(\n                f'multiple {base_idx_name} indexes '\n                f'inherited for {subject_name}',\n                span=span,\n            )\n\n        effective = object_indexes[0]\n        overridden = []\n\n    return (effective, overridden)\n\n\nclass IndexMatchCommand(sd.QualifiedObjectCommand[IndexMatch],\n                        context_class=IndexMatchCommandContext):\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        if not context.stdmode and not context.testmode:\n            raise errors.UnsupportedFeatureError(\n                'user-defined index matches are not supported',\n                span=astnode.span\n            )\n\n        return super()._cmd_tree_from_ast(schema, astnode, context)\n\n    @classmethod\n    def _classname_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext,\n    ) -> sn.QualName:\n        assert isinstance(astnode, qlast.IndexMatchCommand)\n        modaliases = context.modaliases\n\n        valid_type = utils.ast_to_type_shell(\n            astnode.valid_type,\n            metaclass=s_types.Type,\n            modaliases=modaliases,\n            schema=schema,\n        )\n\n        index = utils.ast_objref_to_object_shell(\n            astnode.name,\n            metaclass=Index,\n            modaliases=context.modaliases,\n            schema=schema,\n        )\n\n        return get_index_match_fullname(schema, valid_type, index)\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n        schema = s_types.materialize_type_in_attribute(\n            schema, context, self, 'valid_type')\n        return schema\n\n\nclass CreateIndexMatch(IndexMatchCommand, sd.CreateObject[IndexMatch]):\n    astnode = qlast.CreateIndexMatch\n\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        fullname = self.classname\n        index_match = schema.get(fullname, None)\n        if index_match:\n            valid_type = self.get_attribute_value('valid_type')\n            index = self.get_attribute_value('index')\n\n            raise errors.DuplicateDefinitionError(\n                f'an index match for {valid_type.get_displayname(schema)!r} '\n                f'using {index.get_displayname(schema)!r} is already defined',\n                span=self.span)\n\n        return super()._create_begin(schema, context)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        assert isinstance(astnode, qlast.CreateIndexMatch)\n\n        modaliases = context.modaliases\n\n        valid_type = utils.ast_to_type_shell(\n            astnode.valid_type,\n            metaclass=s_types.Type,\n            modaliases=modaliases,\n            schema=schema,\n        )\n        cmd.set_attribute_value('valid_type', valid_type)\n\n        index = utils.ast_objref_to_object_shell(\n            qlast.ObjectRef(\n                module=astnode.name.module,\n                name=astnode.name.name,\n            ),\n            metaclass=Index,\n            modaliases=context.modaliases,\n            schema=schema,\n        )\n        cmd.set_attribute_value('index', index)\n\n        return cmd\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        assert isinstance(node, qlast.CreateIndexMatch)\n        new_value: Any = op.new_value\n\n        if op.property == 'valid_type':\n            # In an index match we can only have pure types, so this is going\n            # to be a TypeName.\n            node.valid_type = cast(qlast.TypeName,\n                                   utils.typeref_to_ast(schema, new_value))\n\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n\nclass DeleteIndexMatch(IndexMatchCommand, sd.DeleteObject[IndexMatch]):\n    astnode = qlast.DropIndexMatch\n\n    def _delete_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._delete_begin(schema, context)\n        if not context.canonical:\n            valid_type = self.scls.get_valid_type(schema)\n            if op := valid_type.as_type_delete_if_unused(schema):\n                self.add_caused(op)\n        return schema\n\n\n# XXX: the below hardcode should be replaced by an index scope\n#      field instead.\ndef is_object_scope_index(\n    schema: s_schema.Schema,\n    index: Index,\n) -> bool:\n    return (\n        is_fts_index(schema, index)\n        or is_ext_ai_index(schema, index)\n    )\n\n\ndef is_exclusive_object_scope_index(\n    schema: s_schema.Schema,\n    index: Index,\n) -> bool:\n    return is_object_scope_index(schema, index)\n\n\ndef is_fts_index(\n    schema: s_schema.Schema,\n    index: Index,\n) -> bool:\n    fts_index = schema.get(sn.QualName(\"std::fts\", \"index\"), type=Index)\n    return index.issubclass(schema, fts_index)\n\n\ndef get_ai_index_id(\n    schema: s_schema.Schema,\n    index: Index,\n) -> str:\n    # TODO: Use the model name?\n    return f'base'\n\n\ndef is_ext_ai_index(\n    schema: s_schema.Schema,\n    index: Index,\n) -> bool:\n    ai_index = schema.get(\n        sn.QualName(\"ext::ai\", \"index\"),\n        type=Index,\n        default=None,\n    )\n    if ai_index is None:\n        return False\n    else:\n        return index.issubclass(schema, ai_index)\n\n\n_embedding_model = sn.QualName(\"ext::ai\", \"EmbeddingModel\")\n_model_name = sn.QualName(\"ext::ai\", \"model_name\")\n\n\ndef get_defined_ext_ai_embedding_models(\n    schema: s_schema.Schema,\n    model_name: Optional[str] = None,\n) -> dict[str, s_objtypes.ObjectType]:\n    from . import objtypes as s_objtypes\n\n    base_embedding_model = schema.get(\n        _embedding_model,\n        type=s_objtypes.ObjectType,\n    )\n\n    def _flt(\n        schema: s_schema.Schema,\n        anno: s_anno.AnnotationValue,\n    ) -> bool:\n        if anno.get_shortname(schema) != _model_name:\n            return False\n\n        subject = anno.get_subject(schema)\n        value = anno.get_value(schema)\n\n        return (\n            value is not None and value != \"<must override>\"\n            and (model_name is None or anno.get_value(schema) == model_name)\n            and isinstance(subject, s_objtypes.ObjectType)\n            and subject.issubclass(schema, base_embedding_model)\n        )\n\n    annos = schema.get_objects(\n        type=s_anno.AnnotationValue,\n        extra_filters=(_flt,),\n    )\n\n    result = {}\n    for anno in annos:\n        subject = anno.get_subject(schema)\n        assert isinstance(subject, s_objtypes.ObjectType)\n        result[anno.get_value(schema)] = subject\n\n    return result\n"
  },
  {
    "path": "edb/schema/inheriting.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    AbstractSet,\n    Iterable,\n    Mapping,\n    Sequence,\n    cast,\n    TYPE_CHECKING,\n)\n\nfrom edb import errors\n\nfrom edb.common import span as edb_span\nfrom edb.common import struct\nfrom edb.edgeql import ast as qlast\nfrom edb.schema import schema as s_schema\n\nfrom . import delta as sd\nfrom . import expr as s_expr\nfrom . import name as sn\nfrom . import objects as so\nfrom . import utils\n\nif TYPE_CHECKING:\n    from edb.schema import referencing as s_referencing\n\n\nclass InheritingObjectCommand[InheritingObjectT: so.InheritingObject](\n    sd.ObjectCommand[InheritingObjectT]\n):\n    def _update_inherited_fields(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        update: Mapping[str, bool],\n    ) -> None:\n        raise NotImplementedError\n\n    def update_field_status(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        super().update_field_status(schema, context)\n        inherited_status = self.compute_inherited_fields(schema, context)\n        self._update_inherited_fields(schema, context, inherited_status)\n\n    def compute_inherited_fields(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> dict[str, bool]:\n        result = {}\n        mcls = self.get_schema_metaclass()\n        for op in self.get_subcommands(type=sd.AlterObjectProperty):\n            field = mcls.get_field(op.property)\n            if field.inheritable and not field.ephemeral:\n                result[op.property] = op.new_inherited\n\n        return result\n\n    def inherit_fields(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        bases: tuple[so.Object, ...],\n        *,\n        fields: Optional[Iterable[str]] = None,\n        ignore_local: bool = False,\n        apply: bool = True,\n    ) -> s_schema.Schema:\n        from . import referencing as s_referencing\n\n        # HACK: Don't inherit fields if the command comes from\n        # expression change propagation. It shouldn't be necessary,\n        # and can cause a knock-on bug: when aliases directly refer to\n        # another alias, they *incorrectly* have 'expr' marked as an\n        # inherited_field, which causes trouble here.\n        # Fixing this in 3.x/4.x would require a schema repair, though.\n        if self.from_expr_propagation:\n            return schema\n\n        mcls = self.get_schema_metaclass()\n        scls = self.scls\n\n        is_owned = (\n            isinstance(scls, s_referencing.ReferencedObject)\n            and scls.get_owned(schema)\n        )\n\n        field_names: Iterable[str]\n        if fields is not None:\n            field_names = set(scls.inheritable_fields()) & set(fields)\n        else:\n            field_names = set(scls.inheritable_fields())\n\n        inherited_fields = scls.get_inherited_fields(schema)\n        inherited_fields_update = {}\n        deferred_complex_ops = []\n\n        # Iterate over mcls.get_schema_fields() instead of field_names for\n        # determinism reasons, and so earlier declared fields get\n        # processed first.\n        for field_name, field in mcls.get_schema_fields().items():\n            if field_name not in field_names:\n                continue\n\n            was_inherited = field_name in inherited_fields\n            ignore_local_field = ignore_local or was_inherited\n\n            try:\n                result = field.merge_fn(\n                    scls,\n                    bases,\n                    field_name,\n                    ignore_local=ignore_local_field,\n                    schema=schema,\n                )\n            except (errors.SchemaDefinitionError, errors.SchemaError) as e:\n                if (span := self.get_attribute_span(field_name)):\n                    e.set_span(span)\n                raise\n\n            if not ignore_local_field:\n                ours = scls.get_explicit_field_value(schema, field_name, None)\n            else:\n                ours = None\n\n            inherited = result is not None and ours is None\n            inherited_fields_update[field_name] = inherited\n\n            if (\n                (\n                    result != ours\n                    or inherited\n                    or (was_inherited and not is_owned)\n                ) or (\n                    result is None and ours is None and ignore_local\n                )\n            ):\n                if (\n                    inherited\n                    and not context.transient_derivation\n                ):\n                    if isinstance(result, s_expr.Expression):\n                        result = self.compile_expr_field(\n                            schema, context, field=field, value=result)\n                    elif isinstance(result, s_expr.ExpressionDict):\n                        compiled = {}\n                        for k, v in result.items():\n                            if not v.is_compiled():\n                                v = self.compile_expr_field(\n                                    schema, context, field, v)\n                            compiled[k] = v\n                        result = compiled\n                sav = self.set_attribute_value(\n                    field_name, result, inherited=inherited)\n                if isinstance(sav, sd.AlterObjectProperty):\n                    schema = self.scls.set_field_value(\n                        schema, field_name, result)\n                else:\n                    # If this isn't a simple AlterObjectProperty, postpone\n                    # its application to _after_ _update_inherited_fields\n                    # so that the inherited_fields computation is correct,\n                    # as each non-trivial AlterSpecialObjectField operation\n                    # updates inherited_fields.\n                    deferred_complex_ops.append(sav)\n\n        self._update_inherited_fields(\n            schema, context, inherited_fields_update)\n        if self.has_attribute_value(\"inherited_fields\"):\n            schema = self.scls.set_field_value(\n                schema,\n                \"inherited_fields\",\n                self.get_attribute_value(\"inherited_fields\"),\n            )\n\n        # In some cases, self will be applied later\n        if apply:\n            for op in deferred_complex_ops:\n                schema = op.apply(schema, context)\n\n        return schema\n\n    def get_inherited_ref_layout(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        refdict: so.RefDict\n    ) -> dict[\n        sn.QualName,\n        tuple[\n            type[\n                s_referencing.CreateReferencedInheritingObject[\n                    s_referencing.ReferencedInheritingObject\n                ]\n            ],\n            qlast.ObjectDDL,\n            list[s_referencing.ReferencedInheritingObject],\n        ],\n    ]:\n        from . import referencing as s_referencing\n\n        attr = refdict.attr\n        bases = self.scls.get_bases(schema)\n        refs: dict[\n            sn.QualName,\n            tuple[\n                type[\n                    s_referencing.CreateReferencedInheritingObject[\n                        s_referencing.ReferencedInheritingObject\n                    ]\n                ],\n                qlast.ObjectDDL,\n                list[s_referencing.ReferencedInheritingObject],\n            ],\n        ] = {}\n\n        ancestors = set(self.scls.get_ancestors(schema).objects(schema))\n        for base in bases.objects(schema) + (self.scls,):\n            base_refs: dict[\n                sn.Name,\n                s_referencing.ReferencedInheritingObject,\n            ] = dict(base.get_field_value(schema, attr).items(schema))\n\n            # Pointers can reference each other if they are computed,\n            # and if they are processed in the wrong order,\n            # recompiling expressions in inherit_field can break, so\n            # we need to sort them by cross refs.\n            # Since inherit_fields doesn't recompile expressions\n            # in transient derivations, we skip the sorting there.\n            if not context.transient_derivation:\n                rev_refs = {v: k for k, v in base_refs.items()}\n                base_refs = {\n                    rev_refs[v]: v\n                    for v in sd.sort_by_cross_refs(schema, base_refs.values())\n                }\n\n                # HACK: Because of issue #5661, we previously did not always\n                # properly discover dependencies on __type__ in computeds.\n                # This was fixed, but it may persist in existing databases.\n                # Currently, expr refs are not compared when diffing schemas,\n                # so a schema repair can't fix this. Thus, in addition to\n                # actually fixing the bug, we hack around it by forcing\n                # __type__ to sort to the front.\n                # TODO: Drop this after cherry-picking.\n                if (tname := sn.UnqualName('__type__')) in base_refs:\n                    base_refs[tname] = base_refs.pop(tname)\n\n            for k, v in reversed(base_refs.items()):\n                if not v.should_propagate(schema):\n                    continue\n                if base == self.scls and not v.get_owned(schema):\n                    continue\n\n                mcls = type(v)\n                create_cmd = sd.get_object_command_class_or_die(\n                    sd.CreateObject, mcls)\n                assert issubclass(\n                    create_cmd,\n                    s_referencing.CreateReferencedInheritingObject,\n                )\n\n                astnode = create_cmd.as_inherited_ref_ast(\n                    schema, context, k, v)\n\n                fqname = create_cmd._classname_from_ast(\n                    schema, astnode, context)\n\n                if fqname not in refs:\n                    refs[fqname] = (create_cmd, astnode, [])\n\n                objs = refs[fqname][2]\n                if base != self.scls:\n                    objs.append(v)\n                elif not objs:\n                    # If we are looking at refs in the base object\n                    # itself, look at the bases of the ref. Any bases\n                    # that we haven't seen already while looking in\n                    # our object bases must be refs to into objects\n                    # that have been dropped from our bases.\n                    #\n                    # To find which bases to keep, we traverse the\n                    # base graph looking for objects with referrers in\n                    # our new ancestor set.\n                    work = list(reversed(v.get_bases(schema).objects(schema)))\n                    while work:\n                        vbase = work.pop()\n                        subj = vbase.get_referrer(schema)\n                        if vbase in objs:\n                            continue\n                        elif subj is None or subj in ancestors:\n                            objs.append(vbase)\n                        else:\n                            work.extend(\n                                reversed(\n                                    vbase.get_bases(schema).objects(schema)))\n\n        return refs\n\n    def get_no_longer_inherited_ref_layout(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        refdict: so.RefDict,\n        present_refs: AbstractSet[sn.QualName],\n    ) -> dict[sn.Name, type[sd.ObjectCommand[so.Object]]]:\n        from . import referencing as s_referencing\n\n        local_refs = self.scls.get_field_value(schema, refdict.attr)\n        dropped_refs: dict[sn.Name, type[sd.ObjectCommand[so.Object]]] = {}\n        for k, v in local_refs.items(schema):\n            if not v.get_owned(schema):\n                mcls = type(v)\n                create_cmd = sd.get_object_command_class_or_die(\n                    sd.CreateObject, mcls)\n                assert issubclass(\n                    create_cmd,\n                    s_referencing.CreateReferencedObject,\n                )\n\n                astnode = create_cmd.as_inherited_ref_ast(\n                    schema, context, k, v)\n\n                fqname = create_cmd._classname_from_ast(\n                    schema, astnode, context)\n\n                if fqname not in present_refs:\n                    delete_cmd = sd.get_object_command_class_or_die(\n                        sd.DeleteObject, mcls)\n                    dropped_refs[fqname] = delete_cmd\n\n        return dropped_refs\n\n    def _fixup_inheritance_refdicts(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        # HACK?: Derived object types and pointers are created\n        # with inheritance_refdicts={'pointers'}, and typically\n        # don't get persisted. However, for globals and aliases,\n        # they *do* get persisted, and will be altered if a parent\n        # is modified. Make sure those alters are also executed\n        # with a restricted inheritance_refdicts or else whether\n        # things like constraints are created on derived views\n        # will be ordering dependent.\n        # TODO: Clean this up--maybe make it driven explicitly by\n        # is_derived, always?\n        if self.scls.get_is_derived(schema):\n            context.current().inheritance_refdicts = {'pointers'}\n            context.current().inheritance_merge = True\n\n    def _recompute_inheritance(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        from . import ordering\n\n        scls = self.scls\n        mcls = type(scls)\n\n        orig_rec = context.current().enable_recursion\n        context.current().enable_recursion = False\n\n        new_ancestors = so.ObjectList[InheritingObjectT].create(\n            schema,\n            so.compute_ancestors(schema, scls),\n        )\n        self.set_attribute_value(\n            'ancestors',\n            new_ancestors,\n            orig_value=scls.get_ancestors(schema),\n        )\n        schema = scls.set_field_value(schema, 'ancestors', new_ancestors)\n\n        bases = scls.get_bases(schema).objects(schema)\n        schema = self.inherit_fields(schema, context, bases)\n\n        deleted_refs = {}\n        for refdict in mcls.get_refdicts():\n            if _needs_refdict(refdict, context):\n                schema, deleted = self._reinherit_classref_dict(\n                    schema, context, refdict)\n                deleted_refs.update(deleted)\n\n        # Finalize the deletes. We need to linearize them, since they might\n        # have dependencies between them.\n        root = sd.DeltaRoot()\n        for fqname, delete_cmd_cls in deleted_refs.items():\n            root.add(delete_cmd_cls(classname=fqname))\n        root = ordering.linearize_delta(root, schema, schema)\n\n        schema = root.apply(schema, context)\n        self.update(root.get_subcommands())\n\n        context.current().enable_recursion = orig_rec\n\n        return schema\n\n    def _reinherit_classref_dict(\n        self: InheritingObjectCommand[InheritingObjectT],\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        refdict: so.RefDict,\n    ) -> tuple[s_schema.Schema,\n               dict[sn.Name, type[sd.ObjectCommand[so.Object]]]]:\n        from edb.schema import referencing as s_referencing\n\n        scls = self.scls\n        refs = self.get_inherited_ref_layout(schema, context, refdict)\n        refnames = set(refs)\n\n        obj_op: InheritingObjectCommand[InheritingObjectT]\n        if isinstance(self, sd.AlterObjectFragment):\n            obj_op = cast(InheritingObjectCommand[InheritingObjectT],\n                          self.get_parent_op(context))\n        else:\n            obj_op = self\n\n        for refalter in obj_op.get_subcommands(metaclass=refdict.ref_cls):\n            if refalter.get_attribute_value('owned'):\n                assert isinstance(refalter, sd.QualifiedObjectCommand)\n                refnames.add(refalter.classname)\n\n        deleted_refs = self.get_no_longer_inherited_ref_layout(\n            schema, context, refdict, refnames)\n        group = sd.CommandGroup()\n\n        for create_cmd, astnode, bases in refs.values():\n            cmd = create_cmd.as_inherited_ref_cmd(\n                schema=schema,\n                context=context,\n                astnode=astnode,\n                bases=bases,\n                referrer=scls,\n            )\n\n            obj = schema.get(cmd.classname, default=None)\n            if obj is None:\n                cmd.set_attribute_value(refdict.backref_attr, scls)\n                group.add(cmd)\n                schema = cmd.apply(schema, context)\n            else:\n                assert isinstance(obj,\n                                  s_referencing.ReferencedInheritingObject)\n                existing_bases = obj.get_implicit_bases(schema)\n                schema, cmd2 = self._rebase_ref(\n                    schema, context, obj, tuple(existing_bases), tuple(bases))\n                group.add(cmd2)\n\n        self.add(group)\n\n        return schema, deleted_refs\n\n    def _rebase_ref_cmd(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        scls: s_referencing.ReferencedInheritingObject,\n        old_bases: Sequence[so.InheritingObject],\n        new_bases: Sequence[so.InheritingObject],\n    ) -> tuple[sd.Command, Optional[sd.Command]]:\n        from . import referencing as s_referencing\n\n        old_base_names = [b.get_name(schema) for b in old_bases]\n        new_base_names = [b.get_name(schema) for b in new_bases]\n\n        removed, added = delta_bases(\n            old_base_names,\n            new_base_names,\n            t=type(scls),\n        )\n\n        rebase = sd.get_object_command_class(\n            RebaseInheritingObject, type(scls))\n\n        alter_cmd_root, alter_cmd, _ = (\n            scls.init_delta_branch(schema, context, sd.AlterObject))\n        assert isinstance(alter_cmd, AlterInheritingObject)\n\n        new_bases_coll = so.ObjectList.create(schema, new_bases)\n        schema = scls.set_field_value(schema, 'bases', new_bases_coll)\n        ancestors = so.compute_ancestors(schema, scls)\n        ancestors_coll = so.ObjectList[\n            s_referencing.ReferencedInheritingObject].create(schema, ancestors)\n\n        if rebase is not None:\n            rebase_cmd = rebase(\n                classname=scls.get_name(schema),\n                removed_bases=removed,\n                added_bases=added,\n            )\n\n            rebase_cmd.set_attribute_value(\n                'bases',\n                new_bases_coll,\n            )\n\n            rebase_cmd.set_attribute_value(\n                'ancestors',\n                ancestors_coll,\n            )\n\n            alter_cmd.add(rebase_cmd)\n\n        alter_cmd.set_attribute_value(\n            'bases',\n            new_bases_coll,\n        )\n\n        alter_cmd.set_attribute_value(\n            'ancestors',\n            ancestors_coll,\n        )\n\n        return alter_cmd_root, rebase_cmd\n\n    def _rebase_ref(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        scls: s_referencing.ReferencedInheritingObject,\n        old_bases: Sequence[so.InheritingObject],\n        new_bases: Sequence[so.InheritingObject],\n    ) -> tuple[s_schema.Schema, sd.Command]:\n        alter_cmd_root, _ = self._rebase_ref_cmd(\n            schema, context, scls, old_bases, new_bases)\n\n        schema = alter_cmd_root.apply(schema, context)\n\n        return schema, alter_cmd_root\n\n    @classmethod\n    def _classbases_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext,\n    ) -> list[so.ObjectShell[InheritingObjectT]]:\n        modaliases = context.modaliases\n\n        base_refs = []\n        for b in getattr(astnode, 'bases', None) or []:\n            obj = utils.ast_to_object_shell(\n                b,\n                modaliases=modaliases,\n                schema=schema,\n                metaclass=cls.get_schema_metaclass(),\n            )\n            base_refs.append(obj)\n\n        classname = cls._classname_from_ast(schema, astnode, context)\n        mcls = cls.get_schema_metaclass()\n        if not base_refs and classname not in mcls.get_root_classes():\n            default_base = mcls.get_default_base_name()\n\n            if default_base is not None and classname != default_base:\n                base_refs.append(\n                    utils.ast_objref_to_object_shell(\n                        utils.name_to_ast_ref(default_base),\n                        metaclass=cls.get_schema_metaclass(),\n                        schema=schema,\n                        modaliases=modaliases,\n                    )\n                )\n\n        return base_refs\n\n    def get_ast_attr_for_field(\n        self,\n        field: str,\n        astnode: type[qlast.DDLOperation],\n    ) -> Optional[str]:\n        if (\n            field in {'abstract'}\n            and issubclass(astnode, qlast.CreateObject)\n        ):\n            return field\n        else:\n            return super().get_ast_attr_for_field(field, astnode)\n\n\nBaseDeltaItem_T = tuple[\n    list[so.ObjectShell[so.InheritingObjectT]],\n    str | tuple[str, so.ObjectShell[so.InheritingObjectT]],\n]\n\n\nBaseDelta_T = tuple[\n    tuple[so.ObjectShell[so.InheritingObjectT], ...],\n    tuple[BaseDeltaItem_T[so.InheritingObjectT], ...],\n]\n\n\ndef delta_bases[InheritingObjectT: so.InheritingObject](\n    old_bases: Iterable[sn.Name],\n    new_bases: Iterable[sn.Name],\n    t: type[InheritingObjectT],\n) -> BaseDelta_T[InheritingObjectT]:\n    dropped = frozenset(old_bases) - frozenset(new_bases)\n    removed_bases = [so.ObjectShell(name=b, schemaclass=t) for b in dropped]\n    common_bases = [b for b in old_bases if b not in dropped]\n\n    added_bases: list[BaseDeltaItem_T[InheritingObjectT]] = []\n    j = 0\n\n    added_set = set()\n    added_base_refs: list[so.ObjectShell[InheritingObjectT]] = []\n\n    if common_bases:\n        for base in new_bases:\n            if common_bases[j] == base:\n                # Found common base, insert the accumulated\n                # list of new bases and continue\n                if added_base_refs:\n                    ref = so.ObjectShell(name=common_bases[j], schemaclass=t)\n                    added_bases.append((added_base_refs, ('BEFORE', ref)))\n                    added_base_refs = []\n                j += 1\n                if j >= len(common_bases):\n                    break\n                else:\n                    continue\n\n            # Base has been inserted at position j\n            added_base_refs.append(so.ObjectShell(name=base, schemaclass=t))\n            added_set.add(base)\n\n    # Finally, add all remaining bases to the end of the list\n    tail_bases = added_base_refs + [\n        so.ObjectShell(name=b, schemaclass=t) for b in new_bases\n        if b not in added_set and b not in common_bases\n    ]\n\n    if tail_bases:\n        added_bases.append((tail_bases, 'LAST'))\n\n    return tuple(removed_bases), tuple(added_bases)\n\n\nclass AlterInherit[InheritingObjectT: so.InheritingObject](sd.Command):\n    astnode = qlast.AlterAddInherit, qlast.AlterDropInherit\n\n    # We temporarily record information about inheritance alterations\n    # here, before converting these into Rebases in AlterObject.  The\n    # goal here is to encode the information in the subcommand stream,\n    # so the positioning is maintained.\n    added_bases = struct.Field(list[tuple[\n        list[so.ObjectShell[InheritingObjectT]],\n        Optional[str | tuple[str, so.ObjectShell[InheritingObjectT]]],\n    ]])\n    dropped_bases = struct.Field(list[so.ObjectShell[InheritingObjectT]])\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astcmd: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> Any:\n        added_bases = []\n        dropped_bases: list[so.ObjectShell[InheritingObjectT]] = []\n\n        parent_op = context.current().op\n        assert isinstance(parent_op, sd.ObjectCommand)\n        parent_mcls = parent_op.get_schema_metaclass()\n\n        if isinstance(astcmd, qlast.AlterDropInherit):\n            dropped_bases.extend(\n                utils.ast_to_object_shell(\n                    b,\n                    metaclass=parent_mcls,\n                    modaliases=context.modaliases,\n                    schema=schema,\n                )\n                for b in astcmd.bases\n            )\n\n        elif isinstance(astcmd, qlast.AlterAddInherit):\n            bases = [\n                utils.ast_to_object_shell(\n                    b,\n                    metaclass=parent_mcls,\n                    modaliases=context.modaliases,\n                    schema=schema,\n                )\n                for b in astcmd.bases\n            ]\n\n            pos_node = astcmd.position\n            pos: Optional[\n                str | tuple[str, so.ObjectShell[InheritingObjectT]]\n            ]\n            if pos_node is not None:\n                if pos_node.ref is not None:\n                    ref = so.ObjectShell(\n                        name=utils.ast_ref_to_name(pos_node.ref),\n                        schemaclass=parent_mcls,\n                    )\n                    pos = (pos_node.position, ref)\n                else:\n                    pos = pos_node.position\n            else:\n                pos = None\n\n            added_bases.append((bases, pos))\n\n        # AlterInheritingObject will turn sequences of AlterInherit\n        # into proper RebaseWhatever commands.\n        return AlterInherit(\n            added_bases=added_bases, dropped_bases=dropped_bases)\n\n\nclass CreateInheritingObject[InheritingObjectT: so.InheritingObject](\n    InheritingObjectCommand[InheritingObjectT],\n    sd.CreateObject[InheritingObjectT],\n):\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n        bases_coll = self.get_resolved_attribute_value(\n            'bases', schema=schema, context=context)\n        bases = () if bases_coll is None else bases_coll.objects(schema)\n        ancestors = so.compute_lineage(schema, bases, self.get_verbosename())\n        ancestors_coll = so.ObjectList[InheritingObjectT].create(\n            schema, ancestors)\n        self.set_attribute_value('ancestors', ancestors_coll.as_shell(schema))\n\n        if context.mark_derived:\n            self.set_attribute_value('is_derived', True)\n\n        return schema\n\n    def _update_inherited_fields(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        update: Mapping[str, bool],\n    ) -> None:\n        inherited_fields = {n for n, v in update.items() if v}\n        if inherited_fields:\n            self.set_attribute_value(\n                'inherited_fields', frozenset(inherited_fields))\n\n    def _create_begin(\n        self, schema: s_schema.Schema, context: sd.CommandContext\n    ) -> s_schema.Schema:\n        schema = super()._create_begin(schema, context)\n\n        if not context.canonical:\n            if context.inheritance_merge is None or context.inheritance_merge:\n                bases_coll = self.get_resolved_attribute_value(\n                    'bases', schema=schema, context=context)\n                if bases_coll is not None:\n                    bases = bases_coll.objects(schema)\n                else:\n                    bases = ()\n                schema = self.inherit_fields(schema, context, bases)\n\n        return schema\n\n    def _create_innards(\n        self, schema: s_schema.Schema, context: sd.CommandContext\n    ) -> s_schema.Schema:\n        if not context.canonical:\n            cmd = sd.CommandGroup()\n            mcls = self.get_schema_metaclass()\n\n            for refdict in mcls.get_refdicts():\n                if _needs_refdict(refdict, context):\n                    cmd.add(self.inherit_classref_dict(\n                        schema, context, refdict))\n\n            self.prepend(cmd)\n\n        return super()._create_innards(schema, context)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        assert isinstance(astnode, qlast.ObjectDDL)\n        bases = cls._classbases_from_ast(schema, astnode, context)\n        spans = [b.span for b in bases if b.span is not None]\n        if spans:\n            span = edb_span.merge_spans(spans)\n        else:\n            span = None\n        cmd.set_attribute_value(\n            'bases',\n            so.ObjectCollectionShell(bases, collection_type=so.ObjectList),\n            span=span,\n        )\n\n        return cmd\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        if op.property == 'bases':\n            explicit_bases = self.get_explicit_bases(\n                schema, context, op.new_value)\n\n            if explicit_bases:\n                if isinstance(node, qlast.CreateObject):\n                    if isinstance(node, qlast.BasedOn):\n                        node.bases = [\n                            qlast.TypeName(maintype=utils.name_to_ast_ref(b))\n                            for b in explicit_bases\n                        ]\n                else:\n                    node.commands.append(\n                        qlast.AlterAddInherit(\n                            bases=[\n                                qlast.TypeName(\n                                    maintype=utils.name_to_ast_ref(b),\n                                )\n                                for b in explicit_bases\n                            ],\n                        )\n                    )\n            else:\n                if isinstance(node, qlast.CreateObject):\n                    if isinstance(node, qlast.BasedOn):\n                        node.bases = []\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n    def get_explicit_bases(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        bases: Any,\n    ) -> list[sn.Name]:\n\n        mcls = self.get_schema_metaclass()\n        default_base = mcls.get_default_base_name()\n        base_names: list[sn.Name]\n\n        if isinstance(bases, so.ObjectCollectionShell):\n            base_names = []\n            for b in bases.items:\n                assert b.name is not None\n                base_names.append(b.name)\n        else:\n            assert isinstance(bases, so.ObjectList)\n            base_names = list(bases.names(schema))\n\n        # Filter out implicit bases\n        explicit_bases = [\n            b\n            for b in base_names\n            if (\n                b != default_base\n                and (\n                    not isinstance(b, sn.QualName)\n                    or sn.shortname_from_fullname(b) == b\n                )\n            )\n        ]\n\n        return explicit_bases\n\n    def inherit_classref_dict(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        refdict: so.RefDict,\n    ) -> sd.CommandGroup:\n        scls = self.scls\n        refs = self.get_inherited_ref_layout(schema, context, refdict)\n        group = sd.CommandGroup()\n\n        for create_cmd, astnode, bases in refs.values():\n            cmd = create_cmd.as_inherited_ref_cmd(\n                schema=schema,\n                context=context,\n                astnode=astnode,\n                bases=bases,\n                referrer=scls,\n            )\n\n            cmd.set_attribute_value(refdict.backref_attr, scls)\n\n            group.add(cmd)\n\n        return group\n\n\nclass AlterInheritingObjectOrFragment[InheritingObjectT: so.InheritingObject](\n    InheritingObjectCommand[InheritingObjectT],\n    sd.AlterObjectOrFragment[InheritingObjectT],\n):\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._alter_begin(schema, context)\n        scls = self.scls\n\n        if not context.canonical:\n            self._fixup_inheritance_refdicts(schema, context)\n\n            props = self.enumerate_attributes()\n            if props:\n                bases = scls.get_bases(schema).objects(schema)\n                schema = self.inherit_fields(\n                    schema,\n                    context,\n                    bases,\n                    fields=props,\n                )\n                if context.enable_recursion:\n                    self._propagate_field_alter(schema, context, scls, props)\n\n        return schema\n\n    def _propagate_field_alter(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        scls: so.InheritingObject,\n        props: tuple[str, ...],\n    ) -> None:\n        if _has_implicit_propagation(context):\n            return\n\n        descendant_names = [\n            d.get_name(schema) for d in scls.ordered_descendants(schema)\n        ]\n\n        for descendant_name in descendant_names:\n            descendant = schema.get(\n                descendant_name, type=so.InheritingObject, default=None\n            )\n            assert descendant, '.inherit_fields caused a drop of a descendant?'\n\n            d_root_cmd, d_alter_cmd, ctx_stack = descendant.init_delta_branch(\n                schema, context, sd.AlterObject)\n\n            d_bases = descendant.get_bases(schema).objects(schema)\n\n            # Copy any special updates over\n            if isinstance(self, sd.AlterSpecialObjectField):\n                d_alter_cmd.add(self.clone(d_alter_cmd.classname))\n\n            with ctx_stack():\n                d_alter_cmd.set_annotation('implicit_propagation', True)\n                assert isinstance(d_alter_cmd, InheritingObjectCommand)\n                schema = d_alter_cmd.inherit_fields(\n                    schema, context, d_bases, fields=props, apply=False\n                )\n\n            self.add_caused(d_root_cmd)\n\n    def _update_inherited_fields(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        update: Mapping[str, bool],\n    ) -> None:\n        cur_inh_fields = self.scls.get_inherited_fields(schema)\n        inh_fields = set(cur_inh_fields)\n        for fn, inherited in update.items():\n            if inherited:\n                inh_fields.add(fn)\n            else:\n                inh_fields.discard(fn)\n\n        if cur_inh_fields != inh_fields:\n            if inh_fields:\n                self.set_attribute_value(\n                    'inherited_fields',\n                    frozenset(inh_fields),\n                    orig_value=cur_inh_fields,\n                )\n            else:\n                self.set_attribute_value(\n                    'inherited_fields',\n                    None,\n                    orig_value=cur_inh_fields,\n                )\n\n\nclass AlterInheritingObject[InheritingObjectT: so.InheritingObject](\n    AlterInheritingObjectOrFragment[InheritingObjectT],\n    sd.AlterObject[InheritingObjectT],\n):\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, AlterInheritingObject)\n        assert isinstance(astnode, qlast.ObjectDDL)\n\n        # Collect sequences of AlterInherit commands and transform them\n        # into real RebaseWhatever commands.\n        added_bases = []\n        dropped_bases = []\n        subcmds = cmd.get_subcommands()\n        for i, sub in enumerate(subcmds):\n            if not isinstance(sub, AlterInherit):\n                continue\n\n            dropped_bases.extend(sub.dropped_bases)\n            added_bases.extend(sub.added_bases)\n\n            if (\n                i + 1 < len(subcmds)\n                and isinstance(subcmds[i + 1], AlterInherit)\n            ):\n                cmd.discard(sub)\n                continue\n\n            # The next command is not an AlterInherit, so it's time to\n            # combine what we've seen and turn it into a rebase.\n\n            parent_class = cmd.get_schema_metaclass()\n            rebase_class = sd.get_object_command_class_or_die(\n                RebaseInheritingObject, parent_class)\n\n            cmd.replace(\n                sub,\n                rebase_class(\n                    classname=cmd.classname,\n                    removed_bases=tuple(dropped_bases),\n                    added_bases=tuple(added_bases)\n                )\n            )\n\n            added_bases.clear()\n            dropped_bases.clear()\n\n        # XXX: I am not totally sure when this will come up?\n        if getattr(astnode, 'bases', None):\n            bases = cls._classbases_from_ast(schema, astnode, context)\n            if bases is not None:\n                _, added = delta_bases(\n                    [],\n                    [b.get_name(schema) for b in bases],\n                    t=cmd.get_schema_metaclass(),\n                )\n\n                rebase = sd.get_object_command_class_or_die(\n                    RebaseInheritingObject, cmd.get_schema_metaclass())\n\n                rebase_cmd = rebase(\n                    classname=cmd.classname,\n                    removed_bases=tuple(),\n                    added_bases=added,\n                )\n\n                cmd.add(rebase_cmd)\n\n        return cmd\n\n\nclass AlterInheritingObjectFragment[T: so.InheritingObject](\n    AlterInheritingObjectOrFragment[T],\n    sd.AlterObjectFragment[T],\n):\n    pass\n\n\nclass RenameInheritingObject[T: so.InheritingObject](\n    AlterInheritingObjectFragment[T],\n    sd.RenameObject[T],\n):\n    pass\n\n\nclass DeleteInheritingObject[T: so.InheritingObject](\n    InheritingObjectCommand[T],\n    sd.DeleteObject[T],\n):\n    pass\n\n\nclass RebaseInheritingObject[InheritingObjectT: so.InheritingObject](\n    AlterInheritingObjectFragment[InheritingObjectT],\n):\n    _delta_action = 'rebase'\n\n    removed_bases = struct.Field(tuple)  # type: ignore\n    added_bases = struct.Field(tuple)  # type: ignore\n\n    EXTRA_INHERITED_FIELDS: set[str] = set()\n\n    def __repr__(self) -> str:\n        return '<%s.%s \"%s\">' % (self.__class__.__module__,\n                                 self.__class__.__name__,\n                                 self.classname)\n\n    def get_verb(self) -> str:\n        # FIXME: We just say 'alter' because it is currently somewhat\n        # inconsistent whether an object rebase on its own will get\n        # placed in its own alter command or whether it will share one\n        # with all the associated rebases of pointers.  Ideally we'd\n        # say 'alter base types of', but with the current machinery it\n        # would still usually say 'alter', so just always do that.\n        return 'alter'\n\n    def _alter_finalize(\n        self, schema: s_schema.Schema, context: sd.CommandContext\n    ) -> s_schema.Schema:\n        schema = super()._alter_finalize(schema, context)\n\n        if not context.canonical:\n            schema = self._recompute_inheritance(schema, context)\n            if (\n                context.enable_recursion\n                and not _has_implicit_propagation(context)\n            ):\n                for descendant in self.scls.ordered_descendants(schema):\n                    d_root_cmd, d_alter_cmd, ctx_stack = (\n                        descendant.init_delta_branch(\n                            schema, context, sd.AlterObject))\n                    assert isinstance(d_alter_cmd, InheritingObjectCommand)\n                    d_alter_cmd.set_annotation('implicit_propagation', True)\n                    with ctx_stack():\n                        d_alter_cmd._fixup_inheritance_refdicts(\n                            schema, context)\n                        schema = d_alter_cmd._recompute_inheritance(\n                            schema, context)\n                    self.add_caused(d_root_cmd)\n\n        return schema\n\n    def compute_inherited_fields(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> dict[str, bool]:\n        result = super().compute_inherited_fields(schema, context)\n\n        # When things like indexes and constraints that use\n        # ddl_identity to define their identity are inherited, the\n        # child should inherit all of those fields, even if the object\n        # is owned in the child.\n        # Make this happen when rebasing.\n        mcls = self.get_schema_metaclass()\n        new_bases = self.get_attribute_value('bases').objects(schema)\n\n        inherit = new_bases and not new_bases[0].get_abstract(schema)\n\n        fields = {\n            field.name for field in mcls.get_fields().values()\n            if field.ddl_identity and field.inheritable\n        }\n        fields.update(self.EXTRA_INHERITED_FIELDS)\n\n        for field in fields:\n            if (\n                inherit\n                and field not in result\n                and bool(\n                    new_bases[0].get_explicit_field_value(schema, field, None)\n                )\n            ):\n                result[field] = True\n\n        return result\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n\n        orig_bases = self.scls.get_bases(schema)\n        new_bases = self._compute_new_bases(schema, context, orig_bases)\n        self.set_attribute_value(\n            'bases',\n            so.ObjectList[InheritingObjectT].create(schema, new_bases),\n            orig_value=orig_bases,\n        )\n\n        return schema\n\n    def _compute_new_bases(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        orig_bases: so.ObjectList[InheritingObjectT],\n    ) -> list[InheritingObjectT]:\n        mcls = self.get_schema_metaclass()\n        default_base_name = mcls.get_default_base_name()\n        ori_bases = list(orig_bases.objects(schema))\n        if default_base_name:\n            default_base: Optional[InheritingObjectT] = self.get_object(\n                schema, context, name=default_base_name)\n            if ori_bases == [default_base]:\n                ori_bases = []\n        else:\n            default_base = None\n\n        removed_bases = {b.name for b in self.removed_bases}\n        bases = [\n            b for b in ori_bases\n            if b.get_name(schema) not in removed_bases\n        ]\n        existing_bases = {\n            b.get_name(schema) for b in bases\n        }\n\n        index = {b.get_name(schema): i for i, b in enumerate(bases)}\n\n        for new_bases, pos in self.added_bases:\n            if isinstance(pos, tuple):\n                pos, ref = pos\n\n            if not pos or pos == 'LAST':\n                idx = len(bases)\n            elif pos == 'FIRST':\n                idx = 0\n            else:\n                idx = index[ref.name]\n\n            bases[idx:idx] = [\n                self.get_object(\n                    schema, context, name=b.name, span=b.span)\n                for b in new_bases if b.name not in existing_bases\n            ]\n            index = {b.get_name(schema): i for i, b in enumerate(bases)}\n\n        if not bases and default_base:\n            bases = [default_base]\n\n        return bases\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        assert parent_node is not None\n\n        dropped = self._get_bases_for_ast(schema, context, self.removed_bases)\n\n        if dropped:\n            parent_node.commands.append(\n                qlast.AlterDropInherit(\n                    bases=[\n                        cast(qlast.TypeName, utils.typeref_to_ast(schema, b))\n                        for b in dropped\n                    ],\n                )\n            )\n\n        for bases, pos in self.added_bases:\n            bases = self._get_bases_for_ast(schema, context, bases)\n            if not bases:\n                continue\n\n            if isinstance(pos, tuple):\n                typ = utils.typeref_to_ast(schema, pos[1])\n                assert isinstance(typ, qlast.TypeName)\n\n                assert isinstance(typ.maintype, qlast.ObjectRef)\n                pos_node = qlast.Position(\n                    position=pos[0],\n                    ref=typ.maintype,\n                )\n\n            else:\n                pos_node = qlast.Position(position=pos)\n\n            parent_node.commands.append(\n                qlast.AlterAddInherit(\n                    bases=[\n                        cast(qlast.TypeName, utils.typeref_to_ast(schema, b))\n                        for b in bases\n                    ],\n                    position=pos_node,\n                )\n            )\n\n        return None\n\n    def _get_bases_for_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        bases: tuple[so.ObjectShell[InheritingObjectT], ...],\n    ) -> tuple[so.ObjectShell[InheritingObjectT], ...]:\n        mcls = self.get_schema_metaclass()\n        roots = set(mcls.get_root_classes())\n        return tuple(b for b in bases if b.name not in roots)\n\n\ndef _needs_refdict(refdict: so.RefDict, context: sd.CommandContext) -> bool:\n    inheritance_refdicts = context.inheritance_refdicts\n    return (\n        inheritance_refdicts is None\n        or refdict.attr in inheritance_refdicts\n    ) and (context.inheritance_merge is None or context.inheritance_merge)\n\n\ndef _has_implicit_propagation(context: sd.CommandContext) -> bool:\n    for ctx in reversed(context.stack):\n        if (\n            isinstance(ctx.op, sd.ObjectCommand)\n            and ctx.op.get_annotation('implicit_propagation')\n        ):\n            return True\n    return False\n"
  },
  {
    "path": "edb/schema/links.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Any, Optional, TYPE_CHECKING\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom edb import errors\n\nfrom . import constraints\nfrom . import delta as sd\nfrom . import inheriting\nfrom . import properties\nfrom . import name as sn\nfrom . import objects as so\nfrom . import pointers\nfrom . import referencing\nfrom . import rewrites as s_rewrites\nfrom . import sources as s_sources\nfrom . import types as s_types\nfrom . import unknown_pointers\nfrom . import utils\nfrom . import expr as s_expr\n\nif TYPE_CHECKING:\n    from . import objtypes as s_objtypes\n    from . import schema as s_schema\n\n\nLinkTargetDeleteAction = qltypes.LinkTargetDeleteAction\nLinkSourceDeleteAction = qltypes.LinkSourceDeleteAction\n\n\ndef merge_actions(\n    target: so.InheritingObject,\n    sources: list[so.Object],\n    field_name: str,\n    *,\n    ignore_local: bool = False,\n    schema: s_schema.Schema,\n) -> Any:\n    if not ignore_local:\n        ours = target.get_explicit_local_field_value(schema, field_name, None)\n    else:\n        ours = None\n    if ours is None:\n        current = None\n        current_from = None\n\n        for source in sources:\n            theirs = source.get_explicit_field_value(schema, field_name, None)\n            if theirs is not None:\n                if current is None:\n                    current = theirs\n                    current_from = source\n                elif current != theirs:\n                    target_source = target.get_source(schema)\n                    current_from_source = current_from.get_source(schema)\n                    source_source = source.get_source(schema)\n\n                    tgt_repr = (\n                        f'{target_source.get_displayname(schema)}.'\n                        f'{target.get_displayname(schema)}'\n                    )\n                    cf_repr = (\n                        f'{current_from_source.get_displayname(schema)}.'\n                        f'{current_from.get_displayname(schema)}'\n                    )\n                    other_repr = (\n                        f'{source_source.get_displayname(schema)}.'\n                        f'{source.get_displayname(schema)}'\n                    )\n\n                    raise errors.SchemaError(\n                        f'cannot implicitly resolve the '\n                        f'`on target delete` action for '\n                        f'{tgt_repr!r}: it is defined as {current} in '\n                        f'{cf_repr!r} and as {theirs} in {other_repr!r}; '\n                        f'to resolve, declare `on target delete` '\n                        f'explicitly on {tgt_repr!r}'\n                    )\n        return current\n    else:\n        return ours\n\n\nclass Link(\n    s_sources.Source,\n    pointers.Pointer,\n    qlkind=qltypes.SchemaObjectClass.LINK,\n    data_safe=False,\n):\n\n    on_target_delete = so.SchemaField(\n        LinkTargetDeleteAction,\n        default=LinkTargetDeleteAction.Restrict,\n        coerce=True,\n        compcoef=0.9,\n        merge_fn=merge_actions)\n\n    on_source_delete = so.SchemaField(\n        LinkSourceDeleteAction,\n        default=LinkSourceDeleteAction.Allow,\n        coerce=True,\n        compcoef=0.9,\n        merge_fn=merge_actions)\n\n    def get_target(self, schema: s_schema.Schema) -> s_objtypes.ObjectType:\n        return self.get_field_value(  # type: ignore[no-any-return]\n            schema, 'target')\n\n    def is_link_property(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def has_user_defined_properties(self, schema: s_schema.Schema) -> bool:\n        return bool([p for p in self.get_pointers(schema).objects(schema)\n                     if not p.is_special_pointer(schema)\n                     and not p.is_pure_computable(schema)])\n\n    def get_source(\n        self, schema: s_schema.Schema\n    ) -> Optional[s_objtypes.ObjectType]:\n        return self.get_field_value(  # type: ignore[no-any-return]\n            schema, 'source')\n\n    def get_source_type(self, schema: s_schema.Schema) -> s_objtypes.ObjectType:\n        source = self.get_source(schema)\n        assert source\n        return source\n\n    def compare(\n        self,\n        other: so.Object,\n        *,\n        our_schema: s_schema.Schema,\n        their_schema: s_schema.Schema,\n        context: so.ComparisonContext,\n    ) -> float:\n        if not isinstance(other, Link):\n            if isinstance(other, pointers.Pointer):\n                return 0.0\n            else:\n                raise NotImplementedError()\n\n        return super().compare(\n            other, our_schema=our_schema,\n            their_schema=their_schema, context=context)\n\n    def set_target(\n        self,\n        schema: s_schema.Schema,\n        target: s_types.Type,\n    ) -> s_schema.Schema:\n        schema = super().set_target(schema, target)\n        tgt_prop = self.maybe_get_ptr(schema, sn.UnqualName('target'))\n        if tgt_prop:\n            schema = tgt_prop.set_target(schema, target)\n        return schema\n\n    @classmethod\n    def get_root_classes(cls) -> tuple[sn.QualName, ...]:\n        return (\n            sn.QualName(module='std', name='link'),\n            sn.QualName(module='schema', name='__type__'),\n        )\n\n    @classmethod\n    def get_default_base_name(self) -> sn.QualName:\n        return sn.QualName('std', 'link')\n\n\nclass LinkSourceCommandContext[Source_T: s_sources.Source](\n    s_sources.SourceCommandContext[Source_T]\n):\n    pass\n\n\nclass LinkSourceCommand[Source_T: s_sources.Source](\n    inheriting.InheritingObjectCommand[Source_T]\n):\n    pass\n\n\nclass LinkCommandContext(\n    pointers.PointerCommandContext,\n    constraints.ConsistencySubjectCommandContext,\n    properties.PropertySourceContext[Link],\n    unknown_pointers.UnknownPointerSourceContext[Link],\n    s_sources.SourceCommandContext[Link],\n    s_rewrites.RewriteSubjectCommandContext,\n):\n    pass\n\n\nclass LinkCommand(\n    properties.PropertySourceCommand[Link],\n    pointers.PointerCommand[Link],\n    context_class=LinkCommandContext,\n    referrer_context_class=LinkSourceCommandContext,\n):\n\n    def _append_subcmd_ast(\n        self,\n        schema: s_schema.Schema,\n        node: qlast.DDLOperation,\n        subcmd: sd.Command,\n        context: sd.CommandContext,\n    ) -> None:\n        if (\n            isinstance(subcmd, pointers.PointerCommand)\n            and subcmd.classname != self.classname\n\n        ):\n            pname = sn.shortname_from_fullname(subcmd.classname)\n            if pname.name in {'source', 'target'}:\n                return\n\n        super()._append_subcmd_ast(schema, node, subcmd, context)\n\n    def validate_object(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        \"\"\"Check that link definition is sound.\"\"\"\n        super().validate_object(schema, context)\n\n        scls = self.scls\n        assert isinstance(scls, Link)\n\n        if not scls.get_owned(schema):\n            return\n\n        target = scls.get_target(schema)\n        assert target is not None\n\n        if not target.is_object_type():\n            span = self.get_attribute_span('target')\n            if isinstance(target, s_types.Array):\n                # Custom error message for link -> array<...>\n                link_dn = scls.get_displayname(schema)\n                el_dn = target.get_subtypes(schema)[0].get_displayname(schema)\n                hint = f\"did you mean 'multi link {link_dn} -> {el_dn}'?\"\n            else:\n                hint = None\n\n            raise errors.InvalidLinkTargetError(\n                f'invalid link target type, expected object type, got '\n                f'{target.get_verbosename(schema)}',\n                span=span,\n                hint=hint,\n            )\n\n        if target.is_free_object_type(schema):\n            span = self.get_attribute_span('target')\n            raise errors.InvalidLinkTargetError(\n                f'{target.get_verbosename(schema)} is not a valid link target',\n                span=span,\n            )\n\n        if (\n            not scls.is_pure_computable(schema)\n            and not scls.get_from_alias(schema)\n            and target.is_view(schema)\n        ):\n            span = self.get_attribute_span('target')\n            raise errors.InvalidLinkTargetError(\n                f'invalid link type: {target.get_displayname(schema)!r}'\n                f' is an expression alias, not a proper object type',\n                span=span,\n            )\n\n        if (\n            scls.get_required(schema) and\n            scls.get_on_target_delete(schema) ==\n                qltypes.LinkTargetDeleteAction.DeferredRestrict\n        ):\n            raise errors.InvalidLinkTargetError(\n                'required links may not use `on target delete '\n                'deferred restrict`',\n                span=self.span,\n            )\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        node = super()._get_ast(schema, context, parent_node=parent_node)\n        # __type__ link is special, and while it exists on every object\n        # it does not have a defined default in the schema (and therefore\n        # it isn't marked as required.)  We intervene here to mark all\n        # __type__ links required when rendering for SDL/TEXT.\n        if context.declarative and node is not None:\n            assert isinstance(node, (qlast.CreateConcreteLink,\n                                     qlast.CreateLink))\n            if node.name.name == '__type__':\n                assert isinstance(node, qlast.CreateConcretePointer)\n                node.is_required = True\n        return node\n\n    def _reinherit_classref_dict(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        refdict: so.RefDict,\n    ) -> tuple[s_schema.Schema,\n               dict[sn.Name, type[sd.ObjectCommand[so.Object]]]]:\n        if self.scls.get_computable(schema) and refdict.attr != 'pointers':\n            # If the link is a computable, the inheritance would only\n            # happen in the case of aliasing, and in that case we only\n            # need to inherit the link properties and nothing else.\n            return schema, {}\n\n        return super()._reinherit_classref_dict(schema, context, refdict)\n\n\nclass CreateLink(\n    pointers.CreatePointer[Link],\n    LinkCommand,\n):\n    astnode = [qlast.CreateConcreteLink, qlast.CreateLink]\n    referenced_astnode = qlast.CreateConcreteLink\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        if isinstance(astnode, qlast.CreateConcreteLink):\n            assert isinstance(cmd, pointers.PointerCommand)\n            cmd._process_create_or_alter_ast(schema, astnode, context)\n        assert isinstance(cmd, sd.Command)\n        return cmd\n\n    def get_ast_attr_for_field(\n        self,\n        field: str,\n        astnode: type[qlast.DDLOperation],\n    ) -> Optional[str]:\n        if (\n            field == 'required'\n            and issubclass(astnode, qlast.CreateConcreteLink)\n        ):\n            return 'is_required'\n        elif (\n            field == 'cardinality'\n            and issubclass(astnode, qlast.CreateConcreteLink)\n        ):\n            return 'cardinality'\n        else:\n            return super().get_ast_attr_for_field(field, astnode)\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        objtype = self.get_referrer_context(context)\n\n        if op.property == 'target' and objtype:\n            # Due to how SDL is processed the underlying AST may be an\n            # AlterConcreteLink, which requires different handling.\n            if isinstance(node, qlast.CreateConcreteLink):\n                if not node.target:\n                    expr: Optional[s_expr.Expression] = (\n                        self.get_attribute_value('expr')\n                    )\n                    if expr is not None:\n                        node.target = expr.parse()\n                    else:\n                        t = op.new_value\n                        assert isinstance(t, (so.Object, so.ObjectShell))\n                        node.target = utils.typeref_to_ast(schema, t)\n            else:\n                old_type = pointers.merge_target(\n                    self.scls,\n                    list(self.scls.get_bases(schema).objects(schema)),\n                    'target',\n                    ignore_local=True,\n                    schema=schema,\n                )\n                assert isinstance(op.new_value, (so.Object, so.ObjectShell))\n                new_type = (\n                    op.new_value.resolve(schema)\n                    if isinstance(op.new_value, so.ObjectShell)\n                    else op.new_value)\n                assert isinstance(new_type, s_types.Type)\n                new_type_ast = utils.typeref_to_ast(schema, op.new_value)\n                cast_expr = None\n                # If the type isn't assignment castable, generate a\n                # USING with a nonsense cast. It shouldn't matter,\n                # since there should be no data to cast, but the DDL side\n                # of things doesn't know that since the command is split up.\n                if old_type and not old_type.assignment_castable_to(\n                        new_type, schema):\n                    cast_expr = qlast.TypeCast(\n                        type=new_type_ast,\n                        expr=qlast.Set(elements=[]),\n                    )\n                node.commands.append(\n                    qlast.SetPointerType(\n                        value=new_type_ast,\n                        cast_expr=cast_expr,\n                    )\n                )\n\n        elif op.property == 'on_target_delete':\n            node.commands.append(qlast.OnTargetDelete(cascade=op.new_value))\n        elif op.property == 'on_source_delete':\n            node.commands.append(qlast.OnSourceDelete(cascade=op.new_value))\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n    def inherit_classref_dict(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        refdict: so.RefDict,\n    ) -> sd.CommandGroup:\n        if self.scls.get_computable(schema) and refdict.attr != 'pointers':\n            # If the link is a computable, the inheritance would only\n            # happen in the case of aliasing, and in that case we only\n            # need to inherit the link properties and nothing else.\n            return sd.CommandGroup()\n\n        cmd = super().inherit_classref_dict(schema, context, refdict)\n\n        if refdict.attr != 'pointers':\n            return cmd\n\n        parent_ctx = self.get_referrer_context(context)\n        if parent_ctx is None:\n            return cmd\n\n        # Skip source and target when compiling stuff that won't ever\n        # go into a real schema.\n        if context.slim_links:\n            return cmd\n\n        base_prop_name = sn.QualName('std', 'source')\n        s_name = sn.get_specialized_name(\n            sn.QualName('__', 'source'), str(self.classname))\n        src_prop_name = sn.QualName(\n            name=s_name, module=self.classname.module)\n\n        src_prop = properties.CreateProperty(\n            classname=src_prop_name,\n            is_strong_ref=True,\n        )\n        src_prop.set_attribute_value('name', src_prop_name)\n        src_prop.set_attribute_value(\n            'bases',\n            so.ObjectList.create(schema, [schema.get(base_prop_name)]),\n        )\n        src_prop.set_attribute_value(\n            'source',\n            self.scls,\n        )\n        src_prop.set_attribute_value(\n            'target',\n            parent_ctx.op.scls,\n        )\n        src_prop.set_attribute_value('required', True)\n        src_prop.set_attribute_value('readonly', True)\n        src_prop.set_attribute_value('owned', True)\n        src_prop.set_attribute_value('from_alias',\n                                     self.scls.get_from_alias(schema))\n        src_prop.set_attribute_value('cardinality',\n                                     qltypes.SchemaCardinality.One)\n\n        cmd.prepend(src_prop)\n\n        base_prop_name = sn.QualName('std', 'target')\n        s_name = sn.get_specialized_name(\n            sn.QualName('__', 'target'), str(self.classname))\n        tgt_prop_name = sn.QualName(\n            name=s_name, module=self.classname.module)\n\n        tgt_prop = properties.CreateProperty(\n            classname=tgt_prop_name,\n            is_strong_ref=True,\n        )\n\n        tgt_prop.set_attribute_value('name', tgt_prop_name)\n        tgt_prop.set_attribute_value(\n            'bases',\n            so.ObjectList.create(schema, [schema.get(base_prop_name)]),\n        )\n        tgt_prop.set_attribute_value(\n            'source',\n            self.scls,\n        )\n        tgt_prop.set_attribute_value(\n            'target',\n            self.get_attribute_value('target'),\n        )\n        tgt_prop.set_attribute_value('required', False)\n        tgt_prop.set_attribute_value('readonly', True)\n        tgt_prop.set_attribute_value('owned', True)\n        tgt_prop.set_attribute_value('from_alias',\n                                     self.scls.get_from_alias(schema))\n        tgt_prop.set_attribute_value('cardinality',\n                                     qltypes.SchemaCardinality.One)\n\n        cmd.prepend(tgt_prop)\n\n        return cmd\n\n\nclass RenameLink(\n    LinkCommand,\n    referencing.RenameReferencedInheritingObject[Link],\n):\n    pass\n\n\nclass RebaseLink(\n    LinkCommand,\n    referencing.RebaseReferencedInheritingObject[Link],\n):\n    pass\n\n\nclass SetLinkType(\n    pointers.SetPointerType[Link],\n    referrer_context_class=LinkSourceCommandContext,\n    field='target',\n):\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._alter_begin(schema, context)\n        scls = self.scls\n\n        new_target = scls.get_target(schema)\n\n        if not context.canonical:\n            # We need to update the target link prop as well\n            tgt_prop = scls.maybe_get_ptr(schema, sn.UnqualName('target'))\n            if tgt_prop:\n                tgt_prop_alter = tgt_prop.init_delta_command(\n                    schema, sd.AlterObject)\n                tgt_prop_alter.set_attribute_value('target', new_target)\n                self.add(tgt_prop_alter)\n\n        return schema\n\n\nclass AlterLinkUpperCardinality(\n    pointers.AlterPointerUpperCardinality[Link],\n    referrer_context_class=LinkSourceCommandContext,\n    field='cardinality',\n):\n    pass\n\n\nclass AlterLinkLowerCardinality(\n    pointers.AlterPointerLowerCardinality[Link],\n    referrer_context_class=LinkSourceCommandContext,\n    field='required',\n):\n    pass\n\n\nclass AlterLinkOwned(\n    referencing.AlterOwned[Link],\n    pointers.PointerCommandOrFragment[Link],\n    referrer_context_class=LinkSourceCommandContext,\n    field='owned',\n):\n    pass\n\n\nclass SetTargetDeletePolicy(sd.Command):\n    astnode = qlast.OnTargetDelete\n\n    @classmethod\n    def _cmd_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.AlterObjectProperty:\n        return sd.AlterObjectProperty(\n            property='on_target_delete'\n        )\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        assert isinstance(astnode, qlast.OnTargetDelete)\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, sd.AlterObjectProperty)\n        cmd.new_value = astnode.cascade\n        return cmd\n\n\nclass SetSourceDeletePolicy(sd.Command):\n    astnode = qlast.OnSourceDelete\n\n    @classmethod\n    def _cmd_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.AlterObjectProperty:\n        return sd.AlterObjectProperty(\n            property='on_source_delete'\n        )\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        assert isinstance(astnode, qlast.OnSourceDelete)\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, sd.AlterObjectProperty)\n        cmd.new_value = astnode.cascade\n        return cmd\n\n\nclass AlterLink(\n    LinkCommand,\n    pointers.AlterPointer[Link],\n):\n    astnode = [qlast.AlterConcreteLink, qlast.AlterLink]\n    referenced_astnode = qlast.AlterConcreteLink\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> AlterLink:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, AlterLink)\n        if isinstance(astnode, qlast.CreateConcreteLink):\n            cmd._process_create_or_alter_ast(schema, astnode, context)\n        else:\n            cmd._process_alter_ast(schema, astnode, context)\n        return cmd\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        if op.property == 'target':\n            if op.new_value:\n                assert isinstance(op.new_value, so.ObjectShell)\n                node.commands.append(\n                    qlast.SetPointerType(\n                        value=utils.typeref_to_ast(schema, op.new_value),\n                    ),\n                )\n        elif op.property == 'computable':\n            if not op.new_value:\n                node.commands.append(\n                    qlast.SetField(\n                        name='expr',\n                        value=None,\n                        special_syntax=True,\n                    ),\n                )\n        elif op.property == 'on_target_delete':\n            node.commands.append(qlast.OnTargetDelete(cascade=op.new_value))\n        elif op.property == 'on_source_delete':\n            node.commands.append(qlast.OnSourceDelete(cascade=op.new_value))\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n\nclass DeleteLink(\n    LinkCommand,\n    pointers.DeletePointer[Link],\n):\n    astnode = [qlast.DropConcreteLink, qlast.DropLink]\n    referenced_astnode = qlast.DropConcreteLink\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        if self.get_orig_attribute_value('from_alias'):\n            # This is an alias type, appropriate DDL would be generated\n            # from the corresponding Alter/DeleteAlias node.\n            return None\n        else:\n            return super()._get_ast(schema, context, parent_node=parent_node)\n"
  },
  {
    "path": "edb/schema/migrations.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Implementation of MIGRATION objects.\"\"\"\n\n\nfrom __future__ import annotations\nfrom typing import Optional, TYPE_CHECKING\n\nfrom edb import errors\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import codegen as qlcodegen\nfrom edb.edgeql import qltypes\nfrom edb.edgeql import parser as qlparser\nimport edb._edgeql_parser as ql_parser\n\nfrom . import delta as sd\nfrom . import name as sn\nfrom . import objects as so\nfrom . import utils as s_utils\n\nif TYPE_CHECKING:\n    from . import schema as s_schema\n\n\nclass Migration(\n    so.Object,\n    qlkind=qltypes.SchemaObjectClass.MIGRATION,\n    data_safe=False,\n):\n\n    parents = so.SchemaField(\n        so.ObjectList[\"Migration\"],\n        default=so.DEFAULT_CONSTRUCTOR,\n        coerce=True,\n    )\n\n    message = so.SchemaField(\n        str,\n        default=None,\n        allow_ddl_set=True,\n    )\n\n    generated_by = so.SchemaField(\n        str,\n        default=None,\n        allow_ddl_set=True,\n    )\n\n    script = so.SchemaField(\n        str,\n    )\n\n    sdl = so.SchemaField(\n        str,\n    )\n\n\nclass MigrationCommandContext(sd.ObjectCommandContext[Migration]):\n    pass\n\n\nclass MigrationCommand(\n    sd.ObjectCommand[Migration],\n    context_class=MigrationCommandContext,\n):\n    pass\n\n\nclass CreateMigration(MigrationCommand, sd.CreateObject[Migration]):\n\n    astnode = qlast.CreateMigration\n\n    @classmethod\n    def _cmd_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> CreateMigration:\n        assert isinstance(astnode, qlast.CreateMigration)\n\n        if astnode.name is not None:\n            specified_name = astnode.name.name\n        else:\n            specified_name = None\n\n        parent_migration = schema.get_last_migration()\n\n        parent: Optional[so.ObjectShell[Migration]]\n\n        if parent_migration is not None:\n            parent = parent_migration.as_shell(schema)\n            parent_name = str(parent.name)\n        else:\n            parent = None\n            parent_name = 'initial'\n\n        if astnode.parent is not None:\n            parent_name = astnode.parent.name\n\n        hasher = ql_parser.Hasher.start_migration(parent_name)\n        if astnode.body.text is not None:\n            # This is an explicitly specified CREATE MIGRATION\n            ddl_text = astnode.body.text\n        elif astnode.body.commands:\n            # An implicit CREATE MIGRATION produced by START MIGRATION\n            ddl_text = ';\\n'.join(\n                qlcodegen.generate_source(stmt, uppercase=True)\n                for stmt in [*astnode.commands, *astnode.body.commands]\n            ) + ';'\n        else:\n            ddl_text = ''\n\n        hasher.add_source(ddl_text)\n        name = hasher.make_migration_id()\n\n        sdl_text: Optional[str] = astnode.target_sdl\n\n        if specified_name is not None and name != specified_name:\n            raise errors.SchemaDefinitionError(\n                f'specified migration name does not match the name derived '\n                f'from the migration contents: {specified_name!r}, expected '\n                f'{name!r}',\n                span=astnode.name.span,\n            )\n\n        if specified_name is not None and schema.has_migration(specified_name):\n            # Note: it's not possible to have duplicate migration without\n            # `specified_name`. Because new one will be based onto the new\n            # parent (and you can't specify parent without a name).\n            raise errors.DuplicateMigrationError(\n                f'migration {name!r} is already applied',\n                span=astnode.name.span,\n            )\n\n        if astnode.parent is not None:\n            if parent_migration is None:\n                if astnode.parent.name.lower() != 'initial':\n                    raise errors.SchemaDefinitionError(\n                        f'specified migration parent does not exist',\n                        span=astnode.parent.span,\n                    )\n            else:\n                astnode_parent = s_utils.ast_objref_to_object_shell(\n                    astnode.parent,\n                    metaclass=Migration,\n                    schema=schema,\n                    modaliases={},\n                )\n\n                actual_parent_name = parent_migration.get_name(schema)\n                if astnode_parent.name != actual_parent_name:\n                    raise errors.SchemaDefinitionError(\n                        f'specified migration parent is not the most recent '\n                        f'migration, expected {str(actual_parent_name)!r}',\n                        span=astnode.parent.span,\n                    )\n\n        cmd = cls(classname=sn.UnqualName(name))\n        cmd.set_attribute_value('script', ddl_text)\n        cmd.set_attribute_value('sdl', sdl_text)\n        cmd.set_attribute_value('builtin', False)\n        cmd.set_attribute_value('internal', False)\n        if parent is not None:\n            cmd.set_attribute_value('parents', [parent])\n\n        return cmd\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> CreateMigration:\n        assert isinstance(astnode, qlast.CreateMigration)\n\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        if astnode.body.commands and not astnode.metadata_only:\n            for subastnode in astnode.body.commands:\n                subcmd = sd.compile_ddl(schema, subastnode, context=context)\n                if subcmd is not None:\n                    cmd.add(subcmd)\n\n        assert isinstance(cmd, CreateMigration)\n\n        return cmd\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        from . import ddl as s_ddl\n\n        new_schema = super().apply(schema, context)\n\n        if (\n            context.store_migration_sdl\n            and not self.get_attribute_value('sdl')\n        ):\n            # If target sdl was not known in advance, compute it now.\n            new_sdl: str = s_ddl.sdl_text_from_schema(new_schema)\n            new_schema = self.scls.set_field_value(new_schema, 'sdl', new_sdl)\n            self.set_attribute_value('sdl', new_sdl)\n\n        return new_schema\n\n    def apply_subcommands(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        assert not self.get_prerequisites() and not self.get_caused()\n        # Renames shouldn't persist between commands in a migration script.\n        context.renames.clear()\n        for op in self.get_subcommands(\n            include_prerequisites=False,\n            include_caused=False,\n        ):\n            if not isinstance(op, sd.AlterObjectProperty):\n                schema = op.apply(schema, context=context)\n                context.renames.clear()\n        return schema\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        node = super()._get_ast(schema, context, parent_node=parent_node)\n        assert isinstance(node, qlast.CreateMigration)\n        node.metadata_only = True\n        return node\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        assert isinstance(node, qlast.CreateMigration)\n        if op.property == 'script':\n            block, _ = qlparser.parse_migration_body_block(op.new_value)\n            node.body = qlast.NestedQLBlock(\n                commands=block.commands,\n                text=op.new_value,\n            )\n        elif op.property == 'parents':\n            if op.new_value and (items := op.new_value.items):\n                assert len(items) == 1\n                parent = next(iter(items))\n                node.parent = s_utils.name_to_ast_ref(parent.get_name(schema))\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n\nclass AlterMigration(MigrationCommand, sd.AlterObject[Migration]):\n\n    astnode = qlast.AlterMigration\n\n\nclass DeleteMigration(MigrationCommand, sd.DeleteObject[Migration]):\n\n    astnode = qlast.DropMigration\n\n\ndef get_ordered_migrations(\n    schema: s_schema.Schema,\n) -> list[Migration]:\n    '''Get all the migrations, in order.\n\n    It would be nice if our toposort could do this for us, but\n    toposort is implemented recursively, and it would be a pain to\n    change that.\n\n    '''\n    output = []\n    mig = schema.get_last_migration()\n    while mig:\n        output.append(mig)\n\n        parents = mig.get_parents(schema).objects(schema)\n        assert len(parents) <= 1, \"only one parent supported currently\"\n        mig = parents[0] if parents else None\n\n    output.reverse()\n\n    return output\n"
  },
  {
    "path": "edb/schema/modules.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n\nfrom edb import errors\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom . import annos as s_anno\nfrom . import delta as sd\nfrom . import name as sn\nfrom . import objects as so\nfrom . import schema as s_schema\n\nRESERVED_MODULE_NAMES = {\n    'super',\n}\n\n\nDEFAULT_MODULE_ALIAS = 'default'\n\n\nclass Module(\n    s_anno.AnnotationSubject,\n    so.Object,  # Help reflection figure out the right db MRO\n    qlkind=qltypes.SchemaObjectClass.MODULE,\n    data_safe=False,\n):\n    # N.B: Modules are not \"qualified\" objects, even though they can\n    # be nested (because they might *not* be nested) and we arrange\n    # for their names to always be represented with an UnqualName.\n    pass\n\n\nclass ModuleCommandContext(sd.ObjectCommandContext[Module]):\n    pass\n\n\nclass ModuleCommand(\n    sd.ObjectCommand[Module],\n    context_class=ModuleCommandContext,\n):\n\n    def _validate_legal_command(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        super()._validate_legal_command(schema, context)\n\n        last = str(self.classname)\n        first = last\n        enclosing = None\n        if '::' in str(self.classname):\n            first, _, _ = str(self.classname).partition('::')\n            enclosing, _, last = str(self.classname).rpartition('::')\n            if not schema.has_module(enclosing):\n                raise errors.UnknownModuleError(\n                    f'module {enclosing!r} is not in this schema')\n\n        if last in RESERVED_MODULE_NAMES:\n            raise errors.SchemaDefinitionError(\n                f\"module {last!r} is a reserved module name\")\n\n        if (\n            not context.stdmode and not context.testmode\n            and sn.UnqualName(first) in s_schema.STD_MODULES\n        ):\n            raise errors.SchemaDefinitionError(\n                f'cannot {self._delta_action} {self.get_verbosename()}: '\n                f'module {first} is read-only',\n                span=self.span)\n\n\nclass CreateModule(ModuleCommand, sd.CreateObject[Module]):\n    astnode = qlast.CreateModule\n\n\nclass AlterModule(ModuleCommand, sd.AlterObject[Module]):\n    astnode = qlast.AlterModule\n\n\nclass RenameModule(ModuleCommand, sd.RenameObject[Module]):\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        raise errors.SchemaError(\n            f'renaming modules is not supported',\n            span=self.span,\n        )\n\n\nclass DeleteModule(ModuleCommand, sd.DeleteObject[Module]):\n    astnode = qlast.DropModule\n\n    def _validate_legal_command(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        super()._validate_legal_command(schema, context)\n\n        # For now, we disallow deleting non-empty modules.\n\n        # Modules aren't actually stored with any direct linkage\n        # to the objects in them, so explicitly search for objects\n        # in the module (excluding the module itself).\n        has_objects = bool(any(schema.get_objects(\n            included_modules=[self.classname],\n            excluded_items=[self.classname],\n        )))\n\n        if has_objects:\n            vn = self.scls.get_verbosename(schema)\n            raise errors.SchemaError(\n                f'cannot drop {vn} because it is not empty'\n            )\n"
  },
  {
    "path": "edb/schema/name.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, TypeVar, NamedTuple, TYPE_CHECKING\n\nimport abc\nimport functools\nimport re\n\nfrom edb import errors\nfrom edb.common import markup\n\n\nNameT = TypeVar(\"NameT\", bound=\"Name\")\nQualNameT = TypeVar(\"QualNameT\", bound=\"QualName\")\nUnqualNameT = TypeVar(\"UnqualNameT\", bound=\"UnqualName\")\n\n\n# Unfortunately, there is no way to convince mypy that QualName\n# and UnqualName are implementations of the Name ABC:\n# NamedTuple doesn't support multiple inheritance, and ABCMeta.register\n# is not supported either.  And so, we must resort to stubbing.\nif TYPE_CHECKING:\n\n    class Name:\n\n        __match_args__ = ('name',)\n\n        name: str\n\n        @classmethod\n        def from_string(cls: type[NameT], name: str) -> NameT:\n            ...\n\n        def get_local_name(self) -> UnqualName:\n            ...\n\n        def get_root_module_name(self) -> UnqualName:\n            ...\n\n        def __lt__(self, other: Any) -> bool:\n            ...\n\n        def __le__(self, other: Any) -> bool:\n            ...\n\n        def __gt__(self, other: Any) -> bool:\n            ...\n\n        def __ge__(self, other: Any) -> bool:\n            ...\n\n        def __str__(self) -> str:\n            ...\n\n        def __repr__(self) -> str:\n            ...\n\n        def __hash__(self) -> int:\n            ...\n\n    class QualName(Name):\n\n        __match_args__ = ('module', 'name')\n\n        module: str\n        name: str\n\n        @classmethod\n        def from_string(\n            cls: type[QualNameT],\n            name: str,\n        ) -> QualNameT:\n            ...\n\n        def __init__(self, module: str, name: str) -> None:\n            ...\n\n        def get_local_name(self) -> UnqualName:\n            ...\n\n        def get_module_name(self) -> Name:\n            ...\n\n    class UnqualName(Name):\n\n        __slots__ = ('name',)\n\n        name: str\n\n        @classmethod\n        def from_string(\n            cls: type[UnqualNameT],\n            name: str,\n        ) -> UnqualNameT:\n            ...\n\n        def __init__(self, name: str) -> None:\n            ...\n\n        def get_local_name(self) -> UnqualName:\n            ...\n\nelse:\n\n    class Name(abc.ABC):  # noqa: B024\n        pass\n\n    class QualName(NamedTuple):\n\n        module: str\n        name: str\n\n        @classmethod\n        def from_string(\n            cls: type[QualNameT],\n            name: str,\n        ) -> QualNameT:\n\n            module, _, nqname = name.rpartition('::')\n\n            if not module:\n                err = (\n                    f'improperly formed name {name!r}: '\n                    f'module is not specified'\n                )\n                raise errors.InvalidReferenceError(err)\n\n            return cls(\n                module=module,\n                name=nqname,\n            )\n\n        def get_local_name(self) -> UnqualName:\n            return UnqualName(self.name)\n\n        def get_module_name(self) -> Name:\n            return UnqualName(self.module)\n\n        def get_root_module_name(self) -> UnqualName:\n            return UnqualName(self.module.partition('::')[0])\n\n        def __str__(self) -> str:\n            return f'{self.module}::{self.name}'\n\n        def __repr__(self) -> str:\n            return f'<QualName {self}>'\n\n    class UnqualName(NamedTuple):\n\n        name: str\n\n        @classmethod\n        def from_string(\n            cls: type[UnqualNameT],\n            name: str,\n        ) -> UnqualNameT:\n            return cls(name)\n\n        def get_local_name(self) -> UnqualName:\n            return self\n\n        def get_root_module_name(self) -> UnqualName:\n            return UnqualName(self.name.partition('::')[0])\n\n        def __str__(self) -> str:\n            return self.name\n\n        def __repr__(self) -> str:\n            return f'<UnqualName {self.name}>'\n\n    Name.register(QualName)\n    Name.register(UnqualName)\n\n\ndef is_qualified(name: str) -> bool:\n    return '::' in name\n\n\ndef name_from_string(name: str) -> Name:\n    if is_qualified(name):\n        return QualName.from_string(name)\n    else:\n        return UnqualName.from_string(name)\n\n\ndef mangle_name(name: str) -> str:\n    return (\n        name\n        .replace('|', '||')\n        .replace('&', '&&')\n        .replace('::', '|')\n        .replace('@', '&')\n    )\n\n\nmangle_re_1 = re.compile(r'(?<![|])\\|(?![|])')\nmangle_re_2 = re.compile(r'(?<![&])&(?![&])')\n\n\ndef unmangle_name(name: str) -> str:\n    name = mangle_re_1.sub('::', name)\n    name = mangle_re_2.sub('@', name)\n    return name.replace('||', '|').replace('&&', '&')\n\n\n@functools.lru_cache(10240)\ndef shortname_from_fullname(fullname: Name) -> Name:\n    name = fullname.name\n    parts = name.split('@', 1)\n    if len(parts) == 2:\n        return name_from_string(unmangle_name(parts[0]))\n    else:\n        return fullname\n\n\nunmangle_re_1 = re.compile(r'\\|+')\n\n\ndef recursively_unmangle_shortname(name: str) -> str:\n    # Any number of pipes becomes a single ::.\n    return unmangle_re_1.sub('::', name)\n\n\n@functools.lru_cache(4096)\ndef quals_from_fullname(fullname: QualName) -> list[str]:\n    _, _, mangled_quals = fullname.name.partition('@')\n    return (\n        [unmangle_name(p) for p in mangled_quals.split('@')]\n        if mangled_quals else []\n    )\n\n\ndef get_specialized_name(basename: Name, *qualifiers: str) -> str:\n    mangled_quals = '@'.join(mangle_name(qual) for qual in qualifiers if qual)\n    return f'{mangle_name(str(basename))}@{mangled_quals}'\n\n\ndef is_fullname(name: str) -> bool:\n    return is_qualified(name) and '@' in name\n\n\ndef compat_get_specialized_name(basename: str, *qualifiers: str) -> str:\n    mangled_quals = '@'.join(\n        compat_mangle_name(qual) for qual in qualifiers if qual\n    )\n    return f'{compat_mangle_name(basename)}@@{mangled_quals}'\n\n\ndef compat_mangle_name(name: str) -> str:\n    return name.replace('::', '|')\n\n\ndef compat_name_remangle(name: str) -> Name:\n    if is_fullname(name):\n        qname = QualName.from_string(name)\n        sn = shortname_from_fullname(qname)\n        quals = list(quals_from_fullname(qname))\n        if quals and is_fullname(quals[0]):\n            quals[0] = str(compat_name_remangle(quals[0]))\n        compat_sn = compat_get_specialized_name(str(sn), *quals)\n        return QualName(name=compat_sn, module=qname.module)\n    else:\n        return name_from_string(name)\n\n\n@markup.serializer.no_ref_detect\n@markup.serializer.serializer.register(Name)\ndef _serialize_to_markup(obj: Name, *, ctx: markup.Context) -> markup.Markup:\n    return markup.elements.lang.Object(\n        id=id(obj), class_module=type(obj).__module__,\n        classname=type(obj).__name__, repr=str(obj))\n"
  },
  {
    "path": "edb/schema/objects.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    ClassVar,\n    Final,\n    Generic,\n    Optional,\n    Protocol,\n    TypeVar,\n    Iterable,\n    Iterator,\n    Mapping,\n    Collection,\n    NamedTuple,\n    cast,\n    Self,\n    TYPE_CHECKING,\n)\n\nimport builtins\nimport collections\nimport collections.abc\nimport copy\nimport enum\nimport re\nimport uuid\n\nfrom edb import errors\nfrom edb.edgeql import qltypes\nfrom edb.common.typeutils import not_none\n\nfrom edb.common import checked\nfrom edb.common import lru\nfrom edb.common import markup\nfrom edb.common import ordered\nfrom edb.common import parametric\nfrom edb.common import parsing\nfrom edb.common import struct\nfrom edb.common import topological\nfrom edb.common import uuidgen\n\nfrom . import abc as s_abc\nfrom . import name as sn\nfrom . import _types\n\n\nif TYPE_CHECKING:\n    from edb.schema import delta as sd\n    from edb.schema import schema as s_schema\n\n    CovT = TypeVar(\"CovT\", covariant=True)\n\n    class MergeFunction(Protocol):\n        def __call__(\n            self,  # not actually part of the signature\n            target: InheritingObject,\n            sources: Iterable[Object],\n            field_name: str,\n            *,\n            ignore_local: bool = False,\n            schema: s_schema.Schema,\n        ) -> Any:\n            ...\n\n    class CollectionFactory(Collection[CovT], Protocol):\n        \"\"\"An unknown collection that can be instantiated from an iterable.\"\"\"\n\n        def __init__(\n            self, from_iter: Optional[Iterable[CovT]] = None\n        ) -> None: ...\n\n\nclass NoDefaultT(enum.Enum):\n    \"\"\"Used as a sentinel indicating that a named argument wasn't passed.\n\n    Trick from https://github.com/python/mypy/issues/7642.\n    \"\"\"\n    NoDefault = 0\n\n\nNoDefault: Final = NoDefaultT.NoDefault\n\n\nclass DefaultConstructorT(enum.Enum):\n    DefaultConstructor = 0\n\n\nDEFAULT_CONSTRUCTOR: Final = DefaultConstructorT.DefaultConstructor\n\n\nObjectContainer_T = TypeVar('ObjectContainer_T', bound='ObjectContainer')\nObject_T = TypeVar(\"Object_T\", bound=\"Object\")\nObject_T_co = TypeVar(\"Object_T_co\", bound=\"Object\", covariant=True)\nObjectCollection_T = TypeVar(\n    \"ObjectCollection_T\",\n    bound=\"ObjectCollection[Object]\",\n)\nHashCriterion = type[\"Object\"] | tuple[str, Any]\n\nTYPE_ID_NAMESPACE = uuidgen.UUID('00e50276-2502-11e7-97f2-27fe51238dbd')\n\n\nclass ReflectionMethod(enum.Enum):\n    \"\"\"Annotation on schema classes telling how to reflect in metaschema.\"\"\"\n\n    #: Straight 1:1 reflection (the default)\n    REGULAR = enum.auto()\n\n    #: Object type for schema class is elided and its properties\n    #: are reflected as link properties.  This is used for certain\n    #: Referenced classes, like AnnotationValue.\n    AS_LINK = enum.auto()\n\n    #: No metaschema reflection at all.\n    NONE = enum.auto()\n\n\ndef default_field_merge(\n    target: InheritingObject,\n    sources: Iterable[Object],\n    field_name: str,\n    *,\n    ignore_local: bool = False,\n    schema: s_schema.Schema,\n) -> Any:\n    \"\"\"The default `MergeFunction`.\"\"\"\n    if not ignore_local:\n        ours = target.get_explicit_local_field_value(schema, field_name, None)\n        if ours is not None:\n            return ours\n\n    for source in sources:\n        theirs = source.get_explicit_field_value(schema, field_name, None)\n        if theirs is not None:\n            return theirs\n\n    return None\n\n\ndef get_known_type_id(\n    typename: str | sn.Name,\n    default: uuid.UUID | NoDefaultT = NoDefault\n) -> uuid.UUID:\n    if isinstance(typename, str):\n        typename = sn.name_from_string(typename)\n    try:\n        return _types.TYPE_IDS[typename]\n    except KeyError:\n        pass\n\n    if default is NoDefault:\n        raise errors.SchemaError(\n            f'failed to lookup named type id for {typename!r}')\n\n    return default\n\n\nclass DeltaGuidance(NamedTuple):\n\n    banned_creations: frozenset[tuple[type[Object], sn.Name]] = frozenset()\n    banned_deletions: frozenset[tuple[type[Object], sn.Name]] = frozenset()\n    banned_alters: frozenset[\n        tuple[type[Object], tuple[sn.Name, sn.Name]]\n    ] = frozenset()\n\n\nclass DescribeVisibilityFlags(enum.IntFlag):\n\n    #: Show the field if it is set explicitly, i.e. not inherited or computed.\n    SHOW_IF_EXPLICIT = 1 << 0\n    #: Show the field if it is inherited or computed.\n    SHOW_IF_DERIVED = 1 << 1\n    #: Show if the field value matches the default.\n    SHOW_IF_DEFAULT = 1 << 2\n\n\nclass DescribeVisibilityPolicy(enum.IntEnum):\n\n    SHOW_IF_EXPLICIT = (\n        DescribeVisibilityFlags.SHOW_IF_EXPLICIT\n    )\n\n    SHOW_IF_EXPLICIT_OR_DERIVED = (\n        DescribeVisibilityFlags.SHOW_IF_EXPLICIT\n        | DescribeVisibilityFlags.SHOW_IF_DERIVED\n        | DescribeVisibilityFlags.SHOW_IF_DEFAULT\n    )\n\n    SHOW_IF_EXPLICIT_OR_DERIVED_NOT_DEFAULT = (\n        DescribeVisibilityFlags.SHOW_IF_EXPLICIT\n        | DescribeVisibilityFlags.SHOW_IF_DERIVED\n    )\n\n\nclass ComparisonContext:\n\n    renames: dict[tuple[type[Object], sn.Name], sd.RenameObject[Object]]\n    deletions: dict[tuple[type[Object], sn.Name], sd.DeleteObject[Object]]\n    guidance: Optional[DeltaGuidance]\n    parent_ops: list[sd.ObjectCommand[Any]]\n\n    def __init__(\n        self,\n        *,\n        generate_prompts: bool = False,\n        descriptive_mode: bool = False,\n        guidance: Optional[DeltaGuidance] = None,\n    ) -> None:\n        self.generate_prompts = generate_prompts\n        self.descriptive_mode = descriptive_mode\n        self.guidance = guidance\n        self.renames = {}\n        self.deletions = {}\n        self.placeholder_ctr: dict[str, int] = collections.Counter()\n        self.parent_ops = []\n\n    def is_deleting(self, schema: s_schema.Schema, obj: Object) -> bool:\n        return (type(obj), obj.get_name(schema)) in self.deletions\n\n    def record_rename(\n        self,\n        op: sd.RenameObject[Object],\n    ) -> None:\n        self.renames[op.get_schema_metaclass(), op.classname] = op\n\n    def is_renaming(self, schema: s_schema.Schema, obj: Object) -> bool:\n        return (type(obj), obj.get_name(schema)) in self.renames\n\n    def get_obj_name(self, schema: s_schema.Schema, obj: Object) -> sn.Name:\n        obj_name = obj.get_name(schema)\n        rename_op = self.renames.get((type(obj), obj_name))\n        if rename_op is not None:\n            return rename_op.new_name\n        else:\n            return obj_name\n\n    def get_placeholder(self, prefix: str) -> str:\n        ctr = self.placeholder_ctr[prefix]\n        self.placeholder_ctr[prefix] += 1\n        if ctr == 0:\n            return f'{prefix}'\n        else:\n            return f'{prefix}_{ctr}'\n\n\n# derived from ProtoField for validation\nclass Field[T](struct.ProtoField):\n\n    __slots__ = (\n        'name',\n        'sname',\n        'type',\n        'type_is_generic_self',\n        'coerce',\n        'compcoef',\n        'inheritable',\n        'simpledelta',\n        'ephemeral',\n        'allow_ddl_set',\n        'ddl_identity',\n        'aux_cmd_data',\n        'special_ddl_syntax',\n        'describe_visibility',\n        'weak_ref',\n        'merge_fn',\n        'reflection_method',\n        'reflection_proxy',\n        'is_reducible',\n        'patch_level',\n        'obj_names_as_string',\n    )\n\n    #: Name of the field on the target class; assigned by ObjectMeta\n    name: str\n    #: The name to use when reflecting the field into the schema.\n    #: The same as name by default, but can be overridden.\n    sname: str\n    #: The type of the value stored in the field\n    type: type[T]\n    #: Specifies if *type* is a generic type of the host object\n    #: this field is defined on.\n    type_is_generic_self: bool\n    #: Whether the field is allowed to automatically coerce\n    #: the input value to the declared type of the field.\n    coerce: bool\n    #: The diffing coefficient to use when comparing field\n    #: values in objects from 0 to 1.\n    compcoef: Optional[float]\n    #: Whether the field value can be inherited.\n    inheritable: bool\n    #: Wheter the field uses the generic AlterObjectProperty\n    #: delta op, or a custom delta command.\n    simpledelta: bool\n    #: If true, the value of the field is not persisted in the\n    #: database.\n    ephemeral: bool\n    #: Whether the field can be set directly using the `SET`\n    #: command in DDL.\n    allow_ddl_set: bool\n    #: Whether the field is used to identify the object\n    #: in DDL operations and schema reflection when object\n    #: name is insufficient.\n    ddl_identity: bool\n    #: Whether the value of this field should be included in the\n    #: aux_object_data for delta commands of objects containing the field.\n    aux_cmd_data: bool\n    #: Whether this field is set using special DDL syntax or a generic\n    #: SET command.\n    special_ddl_syntax: bool\n    #: Determines when this field is shown in\n    #: DESCRIBE AS TEXT [VERBOSE].\n    describe_visibility: DescribeVisibilityPolicy\n    #: Used for fields holding references to objects.  If True,\n    #: the reference is considered \"weak\", i.e. not essential for\n    #: object definition.  The schema and delta linearization\n    #: rely on this to break object reference cycles.\n    weak_ref: bool\n    #: A callable used to merge the value of the field from\n    #: multiple objects.  Most oftenly used by inheritance.\n    merge_fn: MergeFunction\n    #: Defines how the field is reflected into the backend schema storage.\n    reflection_method: ReflectionMethod\n    #: In cases when the value of the field cannot be reflected as a\n    #: direct link (for example, if the value is a non-distinct set),\n    #: this specifies a (ProxyType, linkname) pair of a proxy object type\n    #: and the name of the link within that proxy type.\n    reflection_proxy: Optional[tuple[str, str]]\n    #: Which edgeql+schema patch for the current major version this\n    #: field was introduced in.  Ensures that the data tuples always\n    #: get extended strictly at the end and filters out the field when\n    #: applying earlier patches.\n    patch_level: int\n    #: Interpret any assigned object names as strings.\n    obj_names_as_string: bool\n\n    def __init__(\n        self,\n        type_: builtins.type[T],\n        *,\n        type_is_generic_self: bool = False,\n        coerce: bool = False,\n        compcoef: Optional[float] = None,\n        inheritable: bool = True,\n        simpledelta: bool = True,\n        merge_fn: MergeFunction = default_field_merge,\n        ephemeral: bool = False,\n        weak_ref: bool = False,\n        allow_ddl_set: bool = False,\n        describe_visibility: DescribeVisibilityPolicy = (\n            DescribeVisibilityPolicy.SHOW_IF_EXPLICIT),\n        ddl_identity: bool = False,\n        aux_cmd_data: bool = False,\n        special_ddl_syntax: bool = False,\n        reflection_method: ReflectionMethod = ReflectionMethod.REGULAR,\n        reflection_proxy: Optional[tuple[str, str]] = None,\n        name: Optional[str] = None,\n        reflection_name: Optional[str] = None,\n        patch_level: int = -1,\n        obj_names_as_string: bool = False,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Schema item core attribute definition.\n\n        \"\"\"\n        if not isinstance(type_, type):\n            raise ValueError(f'{type_!r} is not a type')\n\n        self.type = type_\n        self.type_is_generic_self = type_is_generic_self\n        self.coerce = coerce\n        self.allow_ddl_set = allow_ddl_set\n        self.ddl_identity = ddl_identity\n        self.aux_cmd_data = aux_cmd_data\n        self.special_ddl_syntax = special_ddl_syntax\n        self.describe_visibility = describe_visibility\n\n        self.compcoef = compcoef\n        self.inheritable = inheritable\n        self.simpledelta = simpledelta\n        self.weak_ref = weak_ref\n        self.reflection_method = reflection_method\n        self.reflection_proxy = reflection_proxy\n        self.is_reducible = issubclass(type_, s_abc.Reducible)\n        self.patch_level = patch_level\n        self.obj_names_as_string = obj_names_as_string\n\n        if name is not None:\n            self.name = name\n        if reflection_name is not None:\n            self.sname = reflection_name\n\n        if (\n            merge_fn is default_field_merge\n            and callable(\n                type_merge_fn := getattr(self.type, 'merge_values', None)\n            )\n        ):\n            self.merge_fn = type_merge_fn\n        else:\n            self.merge_fn = merge_fn\n\n        self.ephemeral = ephemeral\n\n    def coerce_value(\n        self,\n        schema: s_schema.Schema,\n        value: Any,\n    ) -> Optional[T]:\n        ftype = self.type\n\n        if value is None or isinstance(value, ftype):\n            return value\n\n        if not self.coerce:\n            raise TypeError(\n                f'{self.name} field: expected {ftype} but got {value!r}')\n\n        if issubclass(ftype, (checked.CheckedList,\n                              checked.CheckedSet,\n                              checked.FrozenCheckedList,\n                              checked.FrozenCheckedSet)):\n            casted_list = []\n            # Mypy complains about ambiguity and generics in class vars here,\n            # although the generic in SingleParameter is clearly a type.\n            valtype = ftype.type  # type: ignore\n            # When creating a checked collection field, we may receive either\n            # collections or single items.\n            # If the value is a collection, cast each item separately. If the\n            # value is a single item, cast it directly.\n            if (\n                isinstance(value, Collection)\n                and not isinstance(value, (str, bytes, bytearray))\n            ):\n                for v in value:\n                    if v is not None and not isinstance(v, valtype):\n                        v = valtype(v)\n                    casted_list.append(v)\n            else:\n                casted_list.append(valtype(value))\n\n            value = casted_list\n\n        elif issubclass(ftype, checked.CheckedDict):\n            casted_dict = {}\n            for k, v in value.items():\n                if k is not None and not isinstance(k, ftype.keytype):\n                    k = ftype.keytype(k)\n                if v is not None and not isinstance(v, ftype.valuetype):\n                    v = ftype.valuetype(v)\n                casted_dict[k] = v\n\n            value = casted_dict\n\n        elif issubclass(ftype, ObjectCollection):\n            # Type ignore below because mypy narrowed ftype to\n            # Type[ObjectCollection] and lost track that it's actually\n            # Type[T]\n            return ftype.create(schema, value)  # type: ignore\n\n        elif issubclass(ftype, sn.QualName):\n            return ftype.from_string(value)  # type: ignore\n\n        try:\n            # Type ignore below because Mypy doesn't trust we can instantiate\n            # the type using the value.  We don't trust that either but this\n            # is why there's the try-except block.\n            return ftype(value)  # type: ignore\n        except Exception:\n            raise TypeError(\n                f'cannot coerce {self.name!r} value {value!r} to {ftype}')\n\n    @property\n    def required(self) -> bool:\n        return True\n\n    @property\n    def is_schema_field(self) -> bool:\n        return False\n\n    def get_default(self) -> Any:\n        raise ValueError(f'field {self.name!r} is required and has no default')\n\n    def __get__(\n        self,\n        instance: Optional[Object],\n        owner: builtins.type[Object],\n    ) -> Optional[T]:\n        if instance is not None:\n            return None\n        else:\n            raise AttributeError(\n                f\"type object {owner.__name__!r} \"\n                f\"has no attribute {self.name!r}\"\n            )\n\n    def __repr__(self) -> str:\n        return (\n            f'<{type(self).__name__} name={self.name!r} '\n            f'type={self.type} {id(self):#x}>'\n        )\n\n\nclass SchemaField[Type_T: type](Field[Type_T]):\n\n    __slots__ = ('default', 'hashable', 'allow_ddl_set', 'allow_interpolation',\n                 'index', 'get_default_specialized')\n\n    #: The default value to use for the field.\n    default: Any\n    #: Whether the field participates in object hash.\n    hashable: bool\n    #: Whether it's possible to set the field in DDL.\n    allow_ddl_set: bool\n    #: Whether, when setting the field in DDL, we allow \\(expr)\n    #: string interpolation (and transform it into {field}\n    #: style interpolation).\n    allow_interpolation: bool\n    #: Field index within the object data tuple\n    index: int\n    #: Specialized get_default function\n    get_default_specialized: Callable[[], Any]\n\n    def __init__(\n        self,\n        type: Type_T,\n        *,\n        default: Any = NoDefault,\n        hashable: bool = True,\n        allow_ddl_set: bool = False,\n        allow_interpolation: bool = False,\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(type, **kwargs)\n        self.default = default\n        self.hashable = hashable\n        self.allow_ddl_set = allow_ddl_set\n        self.allow_interpolation = allow_interpolation\n        self.index = -1\n        # Use this instead of get_default if you can; get_default has to\n        # be a method because it comes from the parent\n        self.get_default_specialized = self._make_get_default()\n\n    @property\n    def required(self) -> bool:\n        return self.default is NoDefault\n\n    @property\n    def is_schema_field(self) -> bool:\n        return True\n\n    def get_default(self) -> Any:\n        return self.get_default_specialized()\n\n    def _make_get_default(self) -> Callable[[], Any]:\n        if self.default is NoDefault:\n            def _get_error() -> Any:\n                raise ValueError(\n                    f'field {self.name!r} is required and has no default')\n            return _get_error\n        elif self.default is DEFAULT_CONSTRUCTOR:\n            # ObjectCollection might not be defined yet when we first need\n            # to call this, so we hack it around a bit.\n            if getattr(self.type, 'is_object_collection', False):\n                return self.type.create_empty  # type: ignore\n            else:\n                return self.type\n        else:\n            def _get_simple(value: Any=self.default) -> Any:\n                return value\n\n            return _get_simple\n\n    def __get__(\n        self,\n        instance: Optional[Object],\n        owner: type[Object],\n    ) -> Optional[Type_T]:\n        if instance is not None:\n            raise FieldValueNotFoundError(self.name)\n        else:\n            raise AttributeError(\n                f\"type object {owner.__name__!r} \"\n                f\"has no attribute {self.name!r}\"\n            )\n\n\nclass RefDict(struct.RTStruct):\n\n    attr = struct.Field(\n        str, frozen=True)\n\n    backref_attr = struct.Field(\n        str, default='subject', frozen=True)\n\n    requires_explicit_overloaded = struct.Field(\n        bool, default=False, frozen=True)\n\n    ref_cls: type[Object] = struct.Field(\n        type, frozen=True)\n\n\nclass ObjectContainer(s_abc.Reducible):\n\n    @classmethod\n    def schema_refs_from_data(\n        cls,\n        data: Any,\n    ) -> frozenset[uuid.UUID]:\n        raise NotImplementedError\n\n\nclass ObjectMeta(type):\n\n    _all_types: ClassVar[dict[str, type[Object]]] = {}\n    _schema_types: ClassVar[set[ObjectMeta]] = set()\n    _ql_map: ClassVar[dict[qltypes.SchemaObjectClass, ObjectMeta]] = {}\n    _refdicts_to: ClassVar[\n        dict[ObjectMeta, list[tuple[RefDict, ObjectMeta]]]\n    ] = {}\n\n    # Instance fields (i.e. class fields on types built with ObjectMeta)\n    _displayname: str\n    _fields: dict[str, Field[Any]]\n    _schema_fields: dict[str, SchemaField[Any]]\n    _hashable_fields: set[Field[Any]]  # if f.is_schema_field and f.hashable\n    _sorted_fields: collections.OrderedDict[str, Field[Any]]\n    #: Fields that contain references to objects either directly or\n    #: indirectly.\n    _objref_fields: frozenset[SchemaField[Any]]\n    _reducible_fields: frozenset[SchemaField[Any]]\n    _aux_cmd_data_fields: frozenset[SchemaField[Any]]  # if f.aux_cmd_data\n    _refdicts: collections.OrderedDict[str, RefDict]\n    _refdicts_by_refclass: dict[type, RefDict]\n    _refdicts_by_field: dict[str, RefDict]  # key is rd.attr\n    _ql_class: Optional[qltypes.SchemaObjectClass]\n    _reflection_method: ReflectionMethod\n    _reflection_link: Optional[str]\n    #: Indicates that ALL changes to this object class are safe from the\n    #: standpoint of persistent data.  In other words, changes to the\n    #: object are fully reversible without possible data loss.\n    _data_safe: bool\n    #: Which edgeql+schema patch for the current major version this\n    #: object was introduced in.  Ensures that the data tuples always\n    #: get extended strictly at the end and filters out the field when\n    #: applying earlier patches.\n    _patch_level: int\n\n    #: Whether the type should be abstract in EdgeDB schema.\n    #: This only applies if the type wasn't specified in schema.edgeql.\n    _abstract: Optional[bool]\n\n    def __new__(\n        mcls,\n        name: str,\n        bases: tuple[type, ...],\n        clsdict: dict[str, Any],\n        *,\n        qlkind: Optional[qltypes.SchemaObjectClass] = None,\n        reflection: ReflectionMethod = ReflectionMethod.REGULAR,\n        reflection_link: Optional[str] = None,\n        data_safe: bool = False,\n        abstract: Optional[bool] = None,\n        patch_level: int = -1,\n        **kwargs: Any,\n    ) -> ObjectMeta:\n        refdicts: collections.OrderedDict[str, RefDict]\n\n        fields = {}\n        myfields = {}\n        refdicts = collections.OrderedDict()\n        mydicts = {}\n\n        if name in mcls._all_types:\n            raise TypeError(\n                f'duplicate name for schema class: {name}, already defined'\n                f' as {mcls._all_types[name]!r}'\n            )\n\n        for k, field in tuple(clsdict.items()):\n            if isinstance(field, RefDict):\n                mydicts[k] = field\n                continue\n            if not isinstance(field, struct.ProtoField):\n                continue\n            if not isinstance(field, Field):\n                raise TypeError(\n                    f'cannot create {name} class: schema.objects.Field '\n                    f'expected, got {type(field)}')\n\n            field.name = k\n            if not hasattr(field, 'sname'):\n                field.sname = k\n            myfields[k] = field\n            del clsdict[k]\n\n        try:\n            cls = super().__new__(mcls, name, bases, clsdict, **kwargs)\n        except TypeError as ex:\n            raise TypeError(\n                f'Object metaclass has failed to create class {name}: {ex}')\n\n        for parent in reversed(cls.__mro__):\n            if parent is cls:\n                fields.update(myfields)\n                refdicts.update(mydicts)\n            elif isinstance(parent, ObjectMeta):\n                fields.update({\n                    fn: copy.copy(f)\n                    for fn, f in parent.get_ownfields().items()\n                })\n                refdicts.update({\n                    k: d.copy()\n                    for k, d in parent.get_own_refdicts().items()\n                })\n\n        cls._displayname = re.sub(\n            r'([a-z])([A-Z])', r'\\1 \\2', cls.__name__\n        ).lower()\n\n        cls._data_safe = data_safe\n        cls._abstract = abstract\n        cls._patch_level = patch_level\n        cls._fields = fields\n        cls._schema_fields = {\n            fn: f\n            for fn, f in sorted(fields.items(), key=lambda f: f[1].patch_level)\n            if isinstance(f, SchemaField)\n        }\n        cls._hashable_fields = {\n            f for f in cls._schema_fields.values()\n            if f.hashable\n        }\n        cls._aux_cmd_data_fields = frozenset(\n            f for f in cls._schema_fields.values()\n            if f.aux_cmd_data\n        )\n        cls._sorted_fields = collections.OrderedDict(\n            sorted(fields.items(), key=lambda e: e[0]))\n        cls._objref_fields = frozenset(\n            f for f in cls._schema_fields.values()\n            if issubclass(f.type, ObjectContainer)\n        )\n        cls._reducible_fields = frozenset(\n            f for f in cls._schema_fields.values()\n            if issubclass(f.type, s_abc.Reducible)\n        )\n\n        fa = '{}.{}_fields'.format(cls.__module__, cls.__name__)\n        setattr(cls, fa, myfields)\n\n        for findex, field in enumerate(cls._schema_fields.values()):\n            field.index = findex\n            getter_name = f'get_{field.name}'\n            if getter_name in clsdict:\n                # The getter was defined explicitly, move on.\n                continue\n\n            ftype = field.type\n            # The field getters are hot code as they're essentially\n            # attribute access, so be mindful about what you are adding\n            # into the callables below.\n            if issubclass(ftype, s_abc.Reducible):\n                def reducible_getter(\n                    self: Any,\n                    schema: s_schema.Schema,\n                    *,\n                    _fn: str = field.name,\n                    _fi: int = findex,\n                    _sr: Callable[[Any], s_abc.Reducible] = (\n                        ftype.schema_restore\n                    ),\n                    _fd: Callable[[], Any] = field.get_default,\n                ) -> Any:\n                    v = schema.get_field_raw(self, _fi)\n                    if v is not None:\n                        return _sr(v)\n                    else:\n                        try:\n                            return _fd()\n                        except ValueError:\n                            pass\n\n                        raise FieldValueNotFoundError(\n                            f'{self!r} object has no value '\n                            f'for field {_fn!r}'\n                        )\n\n                setattr(cls, getter_name, reducible_getter)\n\n            elif (\n                field.default is not NoDefault\n                and field.default is not DEFAULT_CONSTRUCTOR\n            ):\n                def regular_default_getter(\n                    self: Any,\n                    schema: s_schema.Schema,\n                    *,\n                    _fi: int = findex,\n                    _fd: Any = field.default,\n                ) -> Any:\n                    v = schema.get_field_raw(self, _fi)\n                    if v is not None:\n                        return v\n                    else:\n                        return _fd\n\n                setattr(cls, getter_name, regular_default_getter)\n\n            else:\n                def regular_getter(\n                    self: Any,\n                    schema: s_schema.Schema,\n                    *,\n                    _fn: str = field.name,\n                    _fi: int = findex,\n                    _fd: Callable[[], Any] = field.get_default,\n                ) -> Any:\n                    v = schema.get_field_raw(self, _fi)\n                    if v is not None:\n                        return v\n                    else:\n                        try:\n                            return _fd()\n                        except ValueError:\n                            pass\n\n                        raise FieldValueNotFoundError(\n                            f'{self!r} object has no value '\n                            f'for field {_fn!r}'\n                        )\n\n                setattr(cls, getter_name, regular_getter)\n\n        non_schema_fields = {field.name for field in fields.values()\n                             if not field.is_schema_field}\n        if non_schema_fields == {'id'} and len(fields) > 1:\n            mcls._schema_types.add(cls)\n            if qlkind is not None:\n                mcls._ql_map[qlkind] = cls\n\n        cls._refdicts_by_refclass = {}\n\n        for dct in refdicts.values():\n            if dct.attr not in cls._fields:\n                raise RuntimeError(\n                    f'object {name} has no refdict field {dct.attr}')\n\n            if cls._fields[dct.attr].inheritable:\n                raise RuntimeError(\n                    f'{name}.{dct.attr} field must not be inheritable')\n            if not cls._fields[dct.attr].ephemeral:\n                raise RuntimeError(\n                    f'{name}.{dct.attr} field must be ephemeral')\n            if not cls._fields[dct.attr].coerce:\n                raise RuntimeError(\n                    f'{name}.{dct.attr} field must be coerced')\n\n            other_dct = cls._refdicts_by_refclass.get(dct.ref_cls)\n            if other_dct is not None:\n                raise TypeError(\n                    'multiple reference dicts for {!r} in '\n                    '{!r}: {!r} and {!r}'.format(dct.ref_cls, cls,\n                                                 dct.attr, other_dct.attr))\n\n            cls._refdicts_by_refclass[dct.ref_cls] = dct\n\n            try:\n                refdicts_to = mcls._refdicts_to[dct.ref_cls]\n            except KeyError:\n                refdicts_to = mcls._refdicts_to[dct.ref_cls] = []\n\n            refdicts_to.append((dct, cls))\n\n        # Refdicts need to be reversed here to respect the __mro__,\n        # as we have iterated over it in reverse above.\n        cls._refdicts = collections.OrderedDict(reversed(refdicts.items()))\n\n        cls._refdicts_by_field = {rd.attr: rd for rd in cls._refdicts.values()}\n\n        setattr(cls, '{}.{}_refdicts'.format(cls.__module__, cls.__name__),\n                     mydicts)\n\n        for f in myfields.values():\n            if (issubclass(f.type, parametric.ParametricType)\n                    and not f.type.is_fully_resolved()):\n                f.type.resolve_types({cls.__name__: cls})\n\n        cls._ql_class = qlkind\n        cls._reflection_method = reflection\n        if reflection is ReflectionMethod.AS_LINK:\n            if reflection_link is None:\n                raise TypeError(\n                    'reflection AS_LINK requires reflection_link to be passed'\n                    ' also'\n                )\n            cls._reflection_link = reflection_link\n        mcls._all_types[name] = cast(type['Object'], cls)\n\n        return cls\n\n    def get_object_reference_fields(cls) -> frozenset[SchemaField[Any]]:\n        return cls._objref_fields\n\n    def get_reducible_fields(cls) -> frozenset[SchemaField[Any]]:\n        return cls._reducible_fields\n\n    def get_aux_cmd_data_fields(cls) -> frozenset[SchemaField[Any]]:\n        return cls._aux_cmd_data_fields\n\n    def has_field(cls, name: str) -> bool:\n        return name in cls._fields\n\n    def get_field(cls, name: str) -> Field[Any]:\n        field = cls._fields.get(name)\n        if field is None:\n            raise LookupError(\n                f'schema class {cls.__name__!r} has no field {name!r}'\n            )\n        return field\n\n    def get_fields(cls, sorted: bool = False) -> Mapping[str, Field[Any]]:\n        return cls._sorted_fields if sorted else cls._fields\n\n    def get_schema_field(cls, name: str) -> SchemaField[Any]:\n        field = cls._schema_fields.get(name)\n        if field is None:\n            raise LookupError(\n                f'schema class {cls.__name__!r} has no schema field {name!r}'\n            )\n        return field\n\n    def get_schema_fields(cls) -> Mapping[str, SchemaField[Any]]:\n        return cls._schema_fields\n\n    def get_ownfields(cls) -> Mapping[str, Field[Any]]:\n        return getattr(  # type: ignore\n            cls,\n            f'{cls.__module__}.{cls.__name__}_fields',\n        )\n\n    def get_own_refdicts(cls) -> Mapping[str, RefDict]:\n        return getattr(  # type: ignore\n            cls,\n            f'{cls.__module__}.{cls.__name__}_refdicts',\n        )\n\n    def get_refdicts(cls) -> Iterator[RefDict]:\n        return iter(cls._refdicts.values())\n\n    def has_refdict(cls, name: str) -> bool:\n        return name in cls._refdicts_by_field\n\n    def get_refdict(cls, name: str) -> RefDict:\n        refdict = cls._refdicts_by_field.get(name)\n        if refdict is None:\n            raise LookupError(\n                f'schema class {cls.__name__!r} has no refdict {name!r}'\n            )\n        return refdict\n\n    def get_refdict_for_class(cls, refcls: type) -> RefDict:\n        for rcls in refcls.__mro__:\n            try:\n                return cls._refdicts_by_refclass[rcls]\n            except KeyError:\n                pass\n        else:\n            raise KeyError(f'{cls} has no refdict for {refcls}')\n\n    def get_referring_classes(cls) -> frozenset[tuple[RefDict, ObjectMeta]]:\n        try:\n            refdicts_to = type(cls)._refdicts_to[cls]\n        except KeyError:\n            return frozenset()\n        else:\n            return frozenset(refdicts_to)\n\n    @property\n    def is_schema_object(cls) -> bool:\n        return cls in ObjectMeta._schema_types\n\n    @classmethod\n    def get_schema_metaclasses(mcls) -> Iterator[type[Object]]:\n        return iter(mcls._all_types.values())\n\n    @classmethod\n    def get_schema_class(mcls, name: str) -> type[Object]:\n        return mcls._all_types[name]\n\n    @classmethod\n    def maybe_get_schema_class(mcls, name: str) -> Optional[type[Object]]:\n        return mcls._all_types.get(name)\n\n    @classmethod\n    def get_schema_metaclass_for_ql_class(\n        mcls, qlkind: qltypes.SchemaObjectClass\n    ) -> type[Object]:\n        cls = mcls._ql_map.get(qlkind)\n        if cls is None:\n            raise LookupError(f'no schema metaclass for {qlkind}')\n        return cast(type[Object], cls)\n\n    def get_ql_class(cls) -> Optional[qltypes.SchemaObjectClass]:\n        return cls._ql_class\n\n    def get_ql_class_or_die(cls) -> qltypes.SchemaObjectClass:\n        if cls._ql_class is not None:\n            return cls._ql_class\n        else:\n            raise LookupError(f'{cls} has no edgeql class string assigned')\n\n    def get_reflection_method(cls) -> ReflectionMethod:\n        return cls._reflection_method\n\n    def get_reflection_link(cls) -> Optional[str]:\n        return cls._reflection_link\n\n\nclass FieldValueNotFoundError(Exception):\n    pass\n\n\nclass Object(ObjectContainer, metaclass=ObjectMeta):\n    \"\"\"Base schema item class.\"\"\"\n\n    __slots__ = ('id',)\n\n    is_global_object = False\n\n    # Unique ID for this schema item.\n    id = Field(\n        uuid.UUID,\n        inheritable=False,\n        simpledelta=False,\n        allow_ddl_set=True,\n    )\n\n    internal = SchemaField(\n        bool,\n        inheritable=False,\n    )\n\n    # Span of source text that contained definition of this object.\n    # This field is ephemeral, which means it not seriliazed and saved\n    # persistently. This is ok, because we only need it for language server.\n    span = SchemaField(\n        parsing.Span,\n        default=None,\n        compcoef=None,\n        hashable=False,\n        ephemeral=True,\n    )\n\n    name = SchemaField(\n        sn.Name,\n        inheritable=False,\n        compcoef=0.670,\n    )\n\n    builtin = SchemaField(\n        bool,\n        default=False,\n        compcoef=0.01,\n        inheritable=False,\n    )\n\n    # Fields that have been computed by the system as opposed to\n    # set explicitly or inherited.\n    computed_fields = SchemaField(\n        checked.FrozenCheckedSet[str],\n        default=DEFAULT_CONSTRUCTOR,\n        coerce=True,\n        inheritable=False,\n        compcoef=0.999,\n    )\n\n    _fields: dict[str, SchemaField[Any]]\n\n    def schema_reduce(self) -> tuple[str, uuid.UUID]:\n        return type(self).__name__, self.id\n\n    @staticmethod\n    @lru.per_job_lru_cache(maxsize=10240)\n    def raw_schema_restore(\n        sclass_name: str,\n        obj_id: uuid.UUID,\n    ) -> Object:\n        sclass = ObjectMeta.get_schema_class(sclass_name)\n        return sclass(_private_id=obj_id)\n\n    @staticmethod\n    def schema_restore(\n        data: tuple[str, uuid.UUID],\n    ) -> Object:\n        sclass_name, obj_id = data\n        return Object.raw_schema_restore(sclass_name, obj_id)\n\n    @classmethod\n    def schema_refs_from_data(\n        cls,\n        data: tuple[str, uuid.UUID],\n    ) -> frozenset[uuid.UUID]:\n        return frozenset((data[1],))\n\n    def get_id(self, schema: s_schema.Schema) -> uuid.UUID:\n        return self.id\n\n    @classmethod\n    def get_schema_class_displayname(cls) -> str:\n        return cls._displayname\n\n    @classmethod\n    def get_shortname_static(cls, name: sn.Name) -> sn.Name:\n        return name\n\n    @classmethod\n    def get_local_name_static(cls, name: sn.Name) -> sn.UnqualName:\n        return cls.get_shortname_static(name).get_local_name()\n\n    @classmethod\n    def get_displayname_static(cls, name: sn.Name) -> str:\n        return str(cls.get_shortname_static(name))\n\n    @classmethod\n    def get_verbosename_static(\n        cls,\n        name: sn.Name,\n        *,\n        parent: Optional[str] = None,\n    ) -> str:\n        clsname = cls.get_schema_class_displayname()\n        dname = cls.get_displayname_static(name)\n        if parent is not None:\n            return f\"{clsname} '{dname}' of {parent}\"\n        else:\n            return f\"{clsname} '{dname}'\"\n\n    @classmethod\n    def is_abstract(cls) -> bool:\n        \"\"\"Return True if this type does NOT represent a concrete schema class.\n        \"\"\"\n        return cls.get_ql_class() is None\n\n    def get_shortname(self, schema: s_schema.Schema) -> sn.Name:\n        return type(self).get_shortname_static(self.get_name(schema))\n\n    def get_local_name(self, schema: s_schema.Schema) -> sn.UnqualName:\n        return type(self).get_local_name_static(self.get_name(schema))\n\n    def get_displayname(self, schema: s_schema.Schema) -> str:\n        return type(self).get_displayname_static(self.get_name(schema))\n\n    def get_verbosename(\n        self, schema: s_schema.Schema, *, with_parent: bool = False\n    ) -> str:\n        clsname = self.get_schema_class_displayname()\n        dname = self.get_displayname(schema)\n        return f\"{clsname} '{dname}'\"\n\n    def __init__(self, *, _private_id: uuid.UUID) -> None:\n        self.id = _private_id\n\n    def __eq__(self, other: Any) -> bool:\n        try:\n            return self.id == other.id  # type: ignore\n        except AttributeError:\n            return NotImplemented\n\n    def __hash__(self) -> int:\n        return hash(self.id)\n\n    @classmethod\n    def _prepare_id(\n        cls,\n        schema: s_schema.Schema,\n        stable_ids: bool,\n        data: dict[str, Any],\n    ) -> uuid.UUID:\n        name = data.get('name')\n        assert isinstance(name, (str, sn.Name))\n\n        try:\n            return get_known_type_id(name)\n        except errors.SchemaError:\n            if stable_ids:\n                # When compiling the standard library, we generate\n                # stable ids based on the internal name and the type's\n                # name. This keeps std schemas compatible across\n                # minor versions at least.\n                return uuidgen.uuid5(\n                    TYPE_ID_NAMESPACE, f'{name}-{cls.__name__}')\n            else:\n                return uuidgen.uuid1mc()\n\n    @classmethod\n    def _create_from_id(cls: type[Self], id: uuid.UUID) -> Self:\n        assert id is not None\n        return cls(_private_id=id)\n\n    @classmethod\n    def create_in_schema[Schema_T: s_schema.Schema](\n        cls: type[Self],\n        schema: Schema_T,\n        stable_ids: bool = False,\n        *,\n        id: Optional[uuid.UUID] = None,\n        **data: Any,\n    ) -> tuple[Schema_T, Self]:\n\n        if not cls.is_schema_object:\n            raise TypeError(f'{cls.__name__} type cannot be created in schema')\n\n        if not data.get('name'):\n            raise RuntimeError(f'cannot create {cls} without a name')\n\n        all_fields = cls.get_schema_fields()\n        obj_data = [None] * len(all_fields)\n        for field_name, value in data.items():\n            field = cls.get_schema_field(field_name)\n            value = field.coerce_value(schema, value)\n            obj_data[field.index] = value\n\n        if id is None:\n            id = cls._prepare_id(schema, stable_ids, data)\n        scls = cls._create_from_id(id)\n        schema = schema.add(id, cls, tuple(obj_data))\n\n        return schema, scls\n\n    # XXX sadly, in the methods below, statically we don't know any better than\n    # \"Any\" since providing the field name as a `str` is the equivalent of\n    # getattr() on a regular class.\n    def get_field_value(\n        self,\n        schema: s_schema.Schema,\n        field_name: str,\n    ) -> Any:\n        field = type(self).get_field(field_name)\n\n        if isinstance(field, SchemaField):\n            val = schema.get_field_raw(self, field.index)\n            if val is not None:\n                if field.is_reducible:\n                    return field.type.schema_restore(val)\n                else:\n                    return val\n            else:\n                try:\n                    return field.get_default()\n                except ValueError:\n                    pass\n        else:\n            try:\n                return object.__getattribute__(self, field_name)\n            except AttributeError:\n                pass\n\n        raise FieldValueNotFoundError(\n            f'{self!r} object has no value for field {field_name!r}')\n\n    def get_explicit_field_value(\n        self,\n        schema: s_schema.Schema,\n        field_name: str,\n        default: Any = NoDefault,\n    ) -> Any:\n        field = type(self).get_field(field_name)\n\n        if isinstance(field, SchemaField):\n            val = schema.get_field_raw(self, field.index)\n            if val is not None:\n                if field.is_reducible:\n                    return field.type.schema_restore(val)\n                else:\n                    return val\n            elif default is not NoDefault:\n                return default\n\n        else:\n            try:\n                return object.__getattribute__(self, field_name)\n            except AttributeError:\n                if default is not NoDefault:\n                    return default\n\n        raise FieldValueNotFoundError(\n            f'{self!r} object has no value for field {field_name!r}')\n\n    def set_field_value(\n        self,\n        schema: s_schema.Schema,\n        name: str,\n        value: Any,\n    ) -> s_schema.Schema:\n        field = type(self)._fields[name]\n        assert field.is_schema_field\n\n        if value is None:\n            return schema.unset_field(self, name)\n        else:\n            value = field.coerce_value(schema, value)\n            return schema.set_field(self, name, value)\n\n    def update(\n        self, schema: s_schema.Schema, updates: dict[str, Any]\n    ) -> s_schema.Schema:\n        fields = type(self)._fields\n\n        updates = updates.copy()\n        for field_name in updates:\n            field = fields[field_name]\n            assert field.is_schema_field\n\n            new_val = updates[field_name]\n            if new_val is not None:\n                new_val = field.coerce_value(schema, new_val)\n                updates[field_name] = new_val\n\n        return schema.update_obj(self, updates)\n\n    def hash_criteria(\n        self: Self, schema: s_schema.Schema\n    ) -> frozenset[HashCriterion]:\n        cls = type(self)\n\n        sig: list[type[Self] | tuple[str, Any]] = [cls]\n        for f in cls._hashable_fields:\n            fn = f.name\n            val = self.get_explicit_field_value(schema, fn, default=None)\n            if val is None:\n                continue\n            elif isinstance(val, collections.abc.MutableSequence):\n                # Turn the list into something hashable so it can be\n                # put in a set.\n                val = tuple(val)\n            elif isinstance(val, collections.abc.MutableMapping):\n                # Turn the dict into something hashable so it can be\n                # put in a set.\n                val = tuple((k, v) for k, v in val.items())\n            sig.append((fn, val))\n\n        return frozenset(sig)\n\n    def compare(\n        self,\n        other: Object,\n        *,\n        our_schema: s_schema.Schema,\n        their_schema: s_schema.Schema,\n        context: ComparisonContext,\n    ) -> float:\n        if (not isinstance(other, self.__class__) and\n                not isinstance(self, other.__class__)):\n            raise NotImplementedError(\n                f'class {self.__class__.__name__!r} and '\n                f'class {other.__class__.__name__!r} are not comparable'\n            )\n\n        cls = type(self)\n\n        similarity = 1.0\n\n        fields = cls.get_fields(sorted=True)\n\n        for field in fields.values():\n            if field.compcoef is None:\n                continue\n\n            fcoef = cls.compare_obj_field_value(\n                field,\n                self,\n                other,\n                our_schema=our_schema,\n                their_schema=their_schema,\n                context=context,\n            )\n\n            similarity *= fcoef\n\n        return similarity\n\n    def is_blocking_ref(\n        self, schema: s_schema.Schema, reference: Object\n    ) -> bool:\n        return True\n\n    def is_parent_ref(\n        self,\n        schema: s_schema.Schema,\n        reference: Object,\n    ) -> bool:\n        \"\"\"Return True if *reference* is a structural ancestor of self.\"\"\"\n        return False\n\n    def is_generated(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    @classmethod\n    def compare_field_value[T](\n        cls,\n        field: Field[type[T]],\n        our_value: T,\n        their_value: T,\n        *,\n        our_schema: s_schema.Schema,\n        their_schema: s_schema.Schema,\n        context: ComparisonContext,\n    ) -> float:\n        if (\n            our_value is not None\n            and their_value is not None\n            and type(our_value) is type(their_value)\n        ):\n            comparator = getattr(type(our_value), 'compare_values', None)\n        else:\n            comparator = getattr(field.type, 'compare_values', None)\n\n        assert field.compcoef is not None\n        if callable(comparator):\n            result = comparator(\n                our_value,\n                their_value,\n                context=context,\n                our_schema=our_schema,\n                their_schema=their_schema,\n                compcoef=field.compcoef,\n            )\n            assert isinstance(result, (float, int))\n            return result\n\n        if our_value != their_value:\n            return field.compcoef\n        else:\n            return 1.0\n\n    @classmethod\n    def compare_obj_field_value[T](\n        cls: type[Self],\n        field: Field[type[T]],\n        ours: Self,\n        theirs: Self,\n        *,\n        our_schema: s_schema.Schema,\n        their_schema: s_schema.Schema,\n        context: ComparisonContext,\n        explicit: bool = False,\n    ) -> float:\n        fname = field.name\n\n        # If a field is not inheritable (and thus cannot be affected\n        # by other objects) and the value is missing, it is exactly\n        # equivalent to that field having the default value instead,\n        # so we should use the default for comparisons. This means\n        # that we perform the comparison as if explicit = False.\n        #\n        # E.g. 'owned' being None and False is semantically\n        # identical and should not be considered a change.\n        if (isinstance(field, SchemaField) and not field.inheritable):\n            explicit = False\n\n        if explicit:\n            our_value = ours.get_explicit_field_value(\n                our_schema, fname, None)\n            their_value = theirs.get_explicit_field_value(\n                their_schema, fname, None)\n        else:\n            our_value = ours.get_field_value(our_schema, fname)\n            their_value = theirs.get_field_value(their_schema, fname)\n\n        similarity = cls.compare_field_value(\n            field,\n            our_value,\n            their_value,\n            our_schema=our_schema,\n            their_schema=their_schema,\n            context=context,\n        )\n\n        # Check to see if this field's computed status has changed.\n        our_cfs = ours.get_computed_fields(our_schema)\n        their_cfs = theirs.get_computed_fields(their_schema)\n\n        fname = field.name\n        if (fname in our_cfs) != (fname in their_cfs):\n            # The change in computed status decreases the similarity.\n            similarity *= 0.95\n\n        return similarity\n\n    @classmethod\n    def compare_values(\n        cls: type[Self],\n        ours: Optional[Object_T],\n        theirs: Optional[Object_T],\n        *,\n        our_schema: s_schema.Schema,\n        their_schema: s_schema.Schema,\n        context: ComparisonContext,\n        compcoef: float,\n    ) -> float:\n        \"\"\"Compare two values and return a coefficient of similarity.\n\n        This is a common callback that is used when we do schema comparisons.\n        *ours* and *theirs* are instances of this class, and *our_schema* and\n        *their_schema* are the corresponding schemas in which the values are\n        defined.  *compcoef* is whatever was specified for the field. The\n        method returns a coefficient of similarity of the values, from ``0``\n        to ``1``.\n        \"\"\"\n        similarity = 1.0\n\n        if ours is not None and theirs is not None:\n            if type(ours) is not type(theirs):\n                similarity /= 1.4\n            else:\n                our_name = context.get_obj_name(our_schema, ours)\n                their_name = theirs.get_name(their_schema)\n                if our_name != their_name:\n                    similarity /= 1.2\n                else:\n                    # If the new and old versions share a reference to\n                    # an object that is being deleted, then we must\n                    # delete this object as well.\n                    if (type(ours), our_name) in context.deletions:\n                        return 0.0\n\n        elif ours is not None or theirs is not None:\n            # one is None but not both\n            similarity /= 1.2\n\n        if similarity < 1.0:\n            return compcoef\n        else:\n            return 1.0\n\n    def refresh_classref(\n        self,\n        schema: s_schema.Schema,\n        collection: str,\n    ) -> s_schema.Schema:\n        refdict = type(self).get_refdict(collection)\n        attr = refdict.attr\n\n        colltype = type(self).get_field(attr).type\n\n        coll = self.get_explicit_field_value(schema, attr, None)\n        if coll is not None:\n            all_coll = colltype.create(schema, coll.objects(schema))\n            schema = self.set_field_value(schema, attr, all_coll)\n\n        return schema\n\n    def add_classref(\n        self,\n        schema: s_schema.Schema,\n        collection: str,\n        obj: Object,\n        replace: bool = False,\n    ) -> s_schema.Schema:\n        refdict = type(self).get_refdict(collection)\n        attr = refdict.attr\n\n        colltype = type(self).get_field(attr).type\n\n        coll = self.get_explicit_field_value(schema, attr, None)\n\n        if coll is not None:\n            schema, all_coll = coll.update(schema, [obj])\n        else:\n            all_coll = colltype.create(schema, [obj])\n\n        schema = self.set_field_value(schema, attr, all_coll)\n\n        return schema\n\n    def field_is_computed(\n        self,\n        schema: s_schema.Schema,\n        field_name: str,\n    ) -> bool:\n        return field_name in self.get_computed_fields(schema)\n\n    def field_is_inherited(\n        self,\n        schema: s_schema.Schema,\n        field_name: str,\n    ) -> bool:\n        return False\n\n    def del_classref(\n        self,\n        schema: s_schema.Schema,\n        collection: str,\n        key: str,\n    ) -> s_schema.Schema:\n        refdict = type(self).get_refdict(collection)\n        attr = refdict.attr\n        coll = self.get_field_value(schema, attr)\n\n        if coll and coll.has(schema, key):\n            schema, coll = coll.delete(schema, [key])\n            schema = self.set_field_value(schema, attr, coll)\n\n        return schema\n\n    def as_shell(\n        self: Self,\n        schema: s_schema.Schema,\n    ) -> ObjectShell[Self]:\n        return ObjectShell(\n            name=self.get_name(schema),\n            displayname=self.get_displayname(schema),\n            schemaclass=type(self),\n        )\n\n    def get_ddl_identity(\n        self,\n        schema: s_schema.Schema,\n    ) -> Optional[dict[str, Any]]:\n        ddl_id_fields = [\n            fn for fn, f in type(self).get_fields().items() if f.ddl_identity\n        ]\n\n        ddl_identity: Optional[dict[str, Any]]\n        if ddl_id_fields:\n            ddl_identity = {}\n            for fn in ddl_id_fields:\n                v = self.get_field_value(schema, fn)\n                if v is not None:\n                    ddl_identity[fn] = v\n        else:\n            ddl_identity = None\n\n        return ddl_identity\n\n    def init_delta_command[\n        ObjectCommand_T: sd.ObjectCommand[Object]\n    ](\n        self,\n        schema: s_schema.Schema,\n        cmdtype: type[ObjectCommand_T],\n        *,\n        classname: Optional[sn.Name] = None,\n        **kwargs: Any,\n    ) -> ObjectCommand_T:\n        from . import delta as sd\n\n        cls = type(self)\n        cmd = sd.get_object_delta_command(\n            objtype=cls,\n            cmdtype=cmdtype,\n            schema=schema,\n            name=classname or self.get_name(schema),\n            ddl_identity=self.get_ddl_identity(schema),\n            **kwargs,\n        )\n        cmd.scls = self\n        self.record_cmd_object_aux_data(schema, cmd)\n        return cmd\n\n    def record_cmd_object_aux_data(\n        self: Self,\n        schema: s_schema.Schema,\n        cmd: sd.ObjectCommand[Any],\n    ) -> None:\n        for field in type(self).get_aux_cmd_data_fields():\n            cmd.set_object_aux_data(\n                field.name,\n                self.get_field_value(schema, field.name),\n            )\n\n    def init_parent_delta_branch(\n        self: Self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        referrer: Optional[Object] = None,\n    ) -> tuple[sd.CommandGroup, sd.Command, sd.ContextStack]:\n        \"\"\"Prepare a parent portion of a command tree for this object.\n\n        This returns a tuple containing:\n\n        - the root (as a ``CommandGroup``) of a nested ``AlterObject`` tree\n          with nodes for each enclosing referrer object;\n        - direct reference to the innermost command in the above tree\n          (may be root if there are no referring objects);\n        - a ``ContextStack`` instance representing the nested CommandContext\n          corresponding to the returned command tree.\n        \"\"\"\n        from . import delta as sd\n        root = sd.CommandGroup()\n        return root, root, sd.ContextStack(())\n\n    def init_delta_branch[ObjectCommand_T: sd.ObjectCommand[Object]](\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        cmdtype: type[ObjectCommand_T],\n        *,\n        classname: Optional[sn.Name] = None,\n        referrer: Optional[Object] = None,\n        possible_parent: Optional[sd.ObjectCommand[Object]] = None,\n        **kwargs: Any,\n    ) -> tuple[sd.Command, ObjectCommand_T, sd.ContextStack]:\n        \"\"\"Make a command subtree for this object.\n\n        This returns a tuple containing:\n\n        - the root (as a ``CommandGroup``) of a nested ``AlterObject`` tree\n          with nodes for each enclosing referrer object and an instance of\n          *cmdtype* as the innermost command;\n        - direct reference to the innermost command in the above tree;\n        - a ``ContextStack`` instance representing the nested CommandContext\n          corresponding to the returned command tree.\n        \"\"\"\n        root_cmd: sd.Command\n        root_cmd, parent_cmd, ctx_stack = self.init_parent_delta_branch(\n            schema=schema,\n            context=context,\n            referrer=referrer,\n        )\n        self_cmd = self.init_delta_command(\n            schema,\n            cmdtype=cmdtype,\n            classname=classname,\n            **kwargs,\n        )\n\n        from . import delta as sd\n\n        # possible_parent allows the caller to tell us what *they* are,\n        # so we can reuse that Alter if we can. The big advantage here is\n        # that it saves needing to do validate_object on the intermediate\n        # objects.\n        if (\n            isinstance(possible_parent, sd.AlterObject)\n            and isinstance(parent_cmd, sd.ObjectCommand)\n            and possible_parent.classname == parent_cmd.classname\n        ):\n            root_cmd = parent_cmd = self_cmd\n        else:\n            parent_cmd.add(self_cmd)\n\n        ctx_stack.push(self_cmd.new_context(schema, context, self))\n        return root_cmd, self_cmd, ctx_stack\n\n    def as_create_delta(\n        self: Self,\n        schema: s_schema.Schema,\n        context: ComparisonContext,\n    ) -> sd.CreateObject[Self]:\n        from . import delta as sd\n\n        cls = type(self)\n        delta = self.init_delta_command(\n            schema,\n            sd.CreateObject,\n            canonical=True,\n        )\n\n        if context.generate_prompts:\n            delta.set_annotation('orig_cmdclass', type(delta))\n\n        ff = cls.get_fields(sorted=True).items()\n        fields = {fn: f for fn, f in ff if f.simpledelta and not f.ephemeral}\n        for fn, f in fields.items():\n            value = self.get_explicit_field_value(schema, fn, None)\n\n            if (\n                value is None\n                and context.descriptive_mode\n                and (\n                    f.describe_visibility\n                    & DescribeVisibilityFlags.SHOW_IF_DERIVED\n                )\n            ):\n                value = self.get_field_value(schema, fn)\n                value_from_default = True\n            else:\n                value_from_default = False\n\n            if f.aux_cmd_data:\n                delta.set_object_aux_data(fn, value)\n            if value is not None:\n                v: Any\n                if issubclass(f.type, ObjectContainer):\n                    v = value.as_shell(schema)\n                else:\n                    v = value\n                self.record_field_create_delta(\n                    schema,\n                    delta,\n                    context=context,\n                    fname=fn,\n                    value=v,\n                    from_default=value_from_default,\n                )\n\n        for refdict in cls.get_refdicts():\n            refcoll: ObjectCollection[Object] = (\n                self.get_field_value(schema, refdict.attr))\n            sorted_refcoll = sorted(\n                refcoll.objects(schema),\n                key=lambda o: o.get_name(schema),\n            )\n            for ref in sorted_refcoll:\n                delta.add(ref.as_create_delta(schema, context))\n\n        return delta\n\n    def as_alter_delta(\n        self: Self,\n        other: Self,\n        *,\n        self_schema: s_schema.Schema,\n        other_schema: s_schema.Schema,\n        confidence: float,\n        context: ComparisonContext,\n    ) -> sd.ObjectCommand[Self]:\n        from . import delta as sd\n\n        cls = type(self)\n        delta = self.init_delta_command(\n            self_schema,\n            sd.AlterObject,\n            canonical=True,\n        )\n\n        delta.set_annotation('confidence', confidence)\n\n        if context.generate_prompts:\n            other_name = other.get_name(other_schema)\n            if self.get_name(self_schema) != other_name:\n                delta.set_annotation('new_name', other_name)\n            delta.set_annotation('orig_cmdclass', type(delta))\n\n        ff = cls.get_fields(sorted=True).items()\n        fields = {fn: f for fn, f in ff if f.simpledelta and not f.ephemeral}\n        for fn, f in fields.items():\n            oldattr_v = self.get_explicit_field_value(self_schema, fn, None)\n            newattr_v = other.get_explicit_field_value(other_schema, fn, None)\n            if f.aux_cmd_data:\n                delta.set_object_aux_data(fn, newattr_v)\n\n            old_v: Any\n            new_v: Any\n\n            if issubclass(f.type, ObjectContainer):\n                if oldattr_v is not None:\n                    old_v = oldattr_v.as_shell(self_schema)\n                else:\n                    old_v = None\n                if newattr_v is not None:\n                    new_v = newattr_v.as_shell(other_schema)\n                else:\n                    new_v = None\n            else:\n                old_v = oldattr_v\n                new_v = newattr_v\n\n            if f.compcoef is not None:\n                fcoef = cls.compare_obj_field_value(\n                    f,\n                    self,\n                    other,\n                    our_schema=self_schema,\n                    their_schema=other_schema,\n                    context=context,\n                    explicit=True,\n                )\n\n                if fcoef != 1.0:\n                    other.record_field_alter_delta(\n                        other_schema,\n                        delta,\n                        context,\n                        fname=fn,\n                        value=new_v,\n                        orig_value=old_v,\n                        orig_schema=self_schema,\n                        orig_object=self,\n                        confidence=confidence,\n                    )\n\n        for refdict in cls.get_refdicts():\n            oldcoll: ObjectCollection[Object] = (\n                self.get_field_value(self_schema, refdict.attr))\n            oldcoll_idx = sorted(\n                oldcoll.objects(self_schema),\n                key=lambda o: o.get_name(self_schema)\n            )\n\n            newcoll: ObjectCollection[Object] = (\n                other.get_field_value(other_schema, refdict.attr))\n            newcoll_idx = sorted(\n                newcoll.objects(other_schema),\n                key=lambda o: o.get_name(other_schema),\n            )\n\n            context.parent_ops.append(delta)\n\n            delta.add(\n                sd.delta_objects(\n                    oldcoll_idx,\n                    newcoll_idx,\n                    sclass=refdict.ref_cls,\n                    parent_confidence=confidence,\n                    context=context,\n                    old_schema=self_schema,\n                    new_schema=other_schema,\n                ),\n            )\n\n            context.parent_ops.pop()\n\n        return delta\n\n    def as_delete_delta(\n        self: Self,\n        *,\n        schema: s_schema.Schema,\n        context: ComparisonContext,\n    ) -> sd.ObjectCommand[Self]:\n        from . import delta as sd\n\n        cls = type(self)\n        delta = self.init_delta_command(\n            schema,\n            sd.DeleteObject,\n            canonical=True,\n        )\n\n        if context.generate_prompts:\n            delta.set_annotation('orig_cmdclass', type(delta))\n\n        context.deletions[type(self), delta.classname] = delta\n\n        ff = cls.get_fields(sorted=True).items()\n        fields = {fn: f for fn, f in ff if f.simpledelta and not f.ephemeral}\n        for fn, f in fields.items():\n            value = self.get_explicit_field_value(schema, fn, None)\n            if f.aux_cmd_data:\n                delta.set_object_aux_data(fn, value)\n            if value is not None:\n                if issubclass(f.type, ObjectContainer):\n                    v = value.as_shell(schema)\n                else:\n                    v = value\n\n                self.record_field_delete_delta(\n                    schema,\n                    delta,\n                    context,\n                    fn,\n                    orig_value=v,\n                )\n\n        for refdict in cls.get_refdicts():\n            refcoll = self.get_field_value(schema, refdict.attr)\n            for ref in refcoll.objects(schema):\n                delta.add(ref.as_delete_delta(schema=schema, context=context))\n\n        return delta\n\n    def record_simple_field_delta(\n        self: Self,\n        schema: s_schema.Schema,\n        delta: sd.ObjectCommand[Self],\n        context: ComparisonContext,\n        *,\n        fname: str,\n        value: Any,\n        orig_value: Any,\n        orig_schema: Optional[s_schema.Schema],\n        orig_object: Optional[Self],\n        from_default: bool = False,\n    ) -> None:\n        computed_fields = self.get_computed_fields(schema)\n        is_computed = fname in computed_fields\n        if orig_schema is not None and orig_object is not None:\n            orig_computed_fields = (\n                orig_object.get_computed_fields(orig_schema))\n            orig_is_computed = fname in orig_computed_fields\n        else:\n            orig_is_computed = is_computed\n\n        cmd = delta.set_attribute_value(\n            fname,\n            value,\n            orig_value=orig_value,\n            computed=is_computed,\n            orig_computed=orig_is_computed,\n            from_default=from_default,\n        )\n\n        context.parent_ops.append(delta)\n        cmd.record_diff_annotations(\n            schema=schema,\n            orig_schema=orig_schema,\n            context=context,\n            object=self,\n            orig_object=orig_object,\n        )\n        context.parent_ops.pop()\n\n    def record_field_create_delta(\n        self: Self,\n        schema: s_schema.Schema,\n        delta: sd.ObjectCommand[Self],\n        context: ComparisonContext,\n        *,\n        fname: str,\n        value: Any,\n        from_default: bool,\n    ) -> None:\n        self.record_simple_field_delta(\n            schema,\n            delta,\n            context,\n            fname=fname,\n            value=value,\n            orig_value=None,\n            orig_schema=None,\n            orig_object=None,\n            from_default=from_default,\n        )\n\n    def record_field_alter_delta(\n        self: Self,\n        schema: s_schema.Schema,\n        delta: sd.ObjectCommand[Self],\n        context: ComparisonContext,\n        *,\n        fname: str,\n        value: Any,\n        orig_value: Any,\n        orig_schema: s_schema.Schema,\n        orig_object: Self,\n        confidence: float,\n    ) -> None:\n        from . import delta as sd\n\n        if fname == 'name':\n            rename_op = orig_object.init_delta_command(\n                orig_schema,\n                sd.RenameObject,\n                new_name=value,\n            )\n\n            rename_op.set_annotation('confidence', confidence)\n\n            self.record_simple_field_delta(\n                schema,\n                rename_op,\n                context,\n                fname=fname,\n                value=value,\n                orig_value=orig_value,\n                orig_schema=orig_schema,\n                orig_object=orig_object,\n            )\n\n            delta.add(rename_op)\n\n            context.record_rename(rename_op)\n        else:\n            self.record_simple_field_delta(\n                schema,\n                delta,\n                context,\n                fname=fname,\n                value=value,\n                orig_value=orig_value,\n                orig_schema=orig_schema,\n                orig_object=orig_object,\n            )\n\n    def record_field_delete_delta(\n        self: Self,\n        schema: s_schema.Schema,\n        delta: sd.ObjectCommand[Self],\n        context: ComparisonContext,\n        fname: str,\n        orig_value: Any,\n    ) -> None:\n        self.record_simple_field_delta(\n            schema,\n            delta,\n            context,\n            fname=fname,\n            value=None,\n            orig_value=orig_value,\n            orig_schema=None,\n            orig_object=None,\n        )\n\n    def dump(self, schema: s_schema.Schema) -> str:\n        return (\n            f'<{type(self).__name__} name={self.get_name(schema)!r} '\n            f'at {id(self):#x}>'\n        )\n\n    def __repr__(self) -> str:\n        return f'<{type(self).__name__} {self.id} at 0x{id(self):#x}>'\n\n\nclass InternalObject(Object):\n    \"\"\"A schema object that is used by the system internally.\n\n    Instances of InternalObject should not appear in schema dumps.\n    \"\"\"\n\n    @classmethod\n    def is_abstract(cls) -> bool:\n        \"\"\"Return True if this type does NOT represent a concrete schema class.\n        \"\"\"\n        return cls is InternalObject\n\n\nclass QualifiedObject(Object):\n\n    name = SchemaField(\n        # ignore below because Mypy doesn't understand fields which are not\n        # inheritable.\n        sn.QualName,  # type: ignore\n        inheritable=False,\n        compcoef=0.670,\n    )\n\n    @classmethod\n    def get_shortname_static(cls, name: sn.Name) -> sn.QualName:\n        result = sn.shortname_from_fullname(name)\n        if not isinstance(result, sn.QualName):\n            assert isinstance(name, sn.QualName)\n            result = sn.QualName(module=name.module, name=result.name)\n        return result\n\n    def get_shortname(self, schema: s_schema.Schema) -> sn.QualName:\n        return type(self).get_shortname_static(self.get_name(schema))\n\n\nQualifiedObject_T = TypeVar('QualifiedObject_T', bound='QualifiedObject')\n\n\nclass ObjectFragment(QualifiedObject):\n    \"\"\"A part of another object that cannot exist independently.\"\"\"\n\n\nclass GlobalObject(Object):\n    is_global_object = True\n\n\nGlobalObject_T = TypeVar('GlobalObject_T', bound='GlobalObject')\n\n\nclass ExternalObject(GlobalObject):\n    \"\"\"An object that is not tracked in a schema, but some external state.\"\"\"\n    pass\n\n\nExternalObject_T = TypeVar('ExternalObject_T', bound='ExternalObject')\n\n\nclass DerivableObject(QualifiedObject):\n\n    def derive_name(\n        self,\n        schema: s_schema.Schema,\n        source: QualifiedObject,\n        *qualifiers: str,\n        derived_name_base: Optional[sn.Name] = None,\n        module: Optional[str] = None,\n    ) -> sn.QualName:\n        source_name = source.get_name(schema)\n        if module is None:\n            module = source_name.module\n        qualifiers = (str(source_name),) + qualifiers\n\n        return derive_name(\n            schema,\n            *qualifiers,\n            module=module,\n            parent=self,\n            derived_name_base=derived_name_base,\n        )\n\n    def is_non_concrete(self, schema: s_schema.Schema) -> bool:\n        return self.get_shortname(schema) == self.get_name(schema)\n\n    def get_derived_name_base(self, schema: s_schema.Schema) -> sn.Name:\n        return self.get_shortname(schema)\n\n    def get_derived_name(\n        self,\n        schema: s_schema.Schema,\n        source: QualifiedObject,\n        *qualifiers: str,\n        mark_derived: bool = False,\n        derived_name_base: Optional[sn.Name] = None,\n        module: Optional[str] = None,\n    ) -> sn.QualName:\n        return self.derive_name(\n            schema, source, *qualifiers,\n            derived_name_base=derived_name_base,\n            module=module)\n\n\nclass Shell:\n    \"\"\"\n    Shells mimic objects, but are not part of the schema.\n    They are construced from AST and are used to hold object data\n    before it is commited to the schema.\n    \"\"\"\n\n    def resolve(self, schema: s_schema.Schema) -> Any:\n        raise NotImplementedError\n\n\nclass ObjectShell(Shell, Generic[Object_T_co]):  # noqa: UP046\n\n    def __init__(\n        self,\n        *,\n        name: sn.Name,\n        schemaclass: type[Object_T_co],\n        displayname: Optional[str] = None,\n        origname: Optional[sn.Name] = None,\n        span: Optional[parsing.Span] = None,\n    ) -> None:\n        self.name = name\n        self.origname = origname\n        self.displayname = displayname\n        self.schemaclass = schemaclass\n        self.span = span\n\n    def get_id(self, schema: s_schema.Schema) -> uuid.UUID:\n        return self.resolve(schema).get_id(schema)\n\n    def resolve(self, schema: s_schema.Schema) -> Object_T_co:\n        if self.name is None:\n            raise TypeError(\n                'cannot resolve anonymous ObjectShell'\n            )\n\n        if isinstance(self.name, sn.QualName):\n            return schema.get(\n                self.name, type=self.schemaclass, span=self.span,\n            )\n        else:\n            return schema.get_global(self.schemaclass, self.name)\n\n    def get_refname(self, schema: s_schema.Schema) -> sn.Name:\n        if self.origname is not None:\n            return self.origname\n        else:\n            # XXX: change get_displayname to return Name\n            return sn.name_from_string(self.get_displayname(schema))\n\n    def get_name(self, schema: s_schema.Schema) -> sn.Name:\n        # this function is needed for polymorphism of Object and ObjectShell\n        return self.name\n\n    def get_displayname(self, schema: s_schema.Schema) -> str:\n        return self.displayname or str(self.name)\n\n    def get_schema_class_displayname(self) -> str:\n        return self.schemaclass.get_schema_class_displayname()\n\n    def __repr__(self) -> str:\n        if self.schemaclass is not None:\n            dn = self.schemaclass.__name__\n        else:\n            dn = 'Object'\n\n        n = self.name or '<anonymous>'\n\n        return f'<{type(self).__name__} {dn}({n!r}) at 0x{id(self):x}>'\n\n\nclass ObjectCollectionDuplicateNameError(Exception):\n    pass\n\n\n# A set of scalars that should be reflected as a multi prop, not as an\n# array.\nclass MultiPropSet[T](\n    checked.FrozenCheckedSet[T],\n):\n    pass\n\n\nclass ObjectCollection[Object_T: \"Object\"](\n    ObjectContainer,\n    parametric.SingleParametricType[Object_T],\n):\n    __slots__ = ('_ids',)\n    is_object_collection = True\n\n    # Even though Object_T would be a correct annotation below,\n    # we want the type to default to base `Object` for cases\n    # when a TypeVar is passed as Object_T.  This is a hack,\n    # of course, because, ideally we'd want to at least default\n    # to the bounds or constraints of the TypeVar, or, even better,\n    # pass the actual type at the call site, but there seems to be\n    # no easy solution to do that.\n    type: ClassVar[type[Object]] = Object  # type: ignore\n    _registry: ClassVar[dict[str, builtins.type[ObjectCollection[Object]]]] = {}\n\n    _container: ClassVar[builtins.type[CollectionFactory[Any]]]\n\n    def __init_subclass__(\n        cls,\n        *,\n        container: Optional[builtins.type[CollectionFactory[Any]]] = None,\n    ) -> None:\n        super().__init_subclass__()\n        if container is not None:\n            cls._container = container\n\n        if not cls.is_anon_parametrized():\n            name = cls.__name__\n            if name in cls._registry:\n                raise TypeError(\n                    f'duplicate name for schema collection class: {name},'\n                    f'already defined as {cls._registry[name]!r}'\n                )\n            else:\n                cls._registry[name] = cls\n\n    @classmethod\n    def get_subclass(cls, name: str) -> builtins.type[ObjectCollection[Object]]:\n        return cls._registry[name]\n\n    def __init__(\n        self,\n        _ids: Collection[uuid.UUID],\n        *,\n        _private_init: bool,\n    ) -> None:\n        if not self.is_fully_resolved():\n            raise TypeError(\n                f\"{type(self)!r} unresolved type parameters\"\n            )\n        self._ids = _ids\n\n    def __len__(self) -> int:\n        return len(self._ids)\n\n    def __eq__(self, other: Any) -> bool:\n        if not isinstance(other, type(self)):\n            return NotImplemented\n        return self._ids == other._ids\n\n    def __hash__(self) -> int:\n        return hash(self._ids)\n\n    def schema_reduce(\n        self,\n    ) -> tuple[\n        str,\n        Optional[tuple[builtins.type, ...] | builtins.type],\n        tuple[uuid.UUID, ...],\n        tuple[tuple[str, Any], ...],\n    ]:\n        cls = type(self)\n        _, (typeargs, ids, attrs) = self.__reduce__()\n        if cls.is_anon_parametrized():\n            clsname = cls.__bases__[0].__name__\n        else:\n            clsname = cls.__name__\n        return (clsname, typeargs, ids, tuple(attrs.items()))\n\n    @staticmethod\n    @lru.per_job_lru_cache(maxsize=10240)\n    def schema_restore(\n        data: tuple[\n            str,\n            Optional[tuple[builtins.type, ...] | builtins.type],\n            tuple[uuid.UUID, ...],\n            tuple[tuple[str, Any], ...],\n        ],\n    ) -> ObjectCollection[Object]:\n        clsname, typeargs, ids, attrs = data\n        scoll_class = ObjectCollection.get_subclass(clsname)\n        return scoll_class.__restore__(typeargs, ids, dict(attrs))\n\n    @classmethod\n    def schema_refs_from_data(\n        cls,\n        data: tuple[\n            str,\n            Optional[tuple[builtins.type, ...] | builtins.type],\n            tuple[uuid.UUID, ...],\n            tuple[tuple[str, Any], ...],\n        ],\n    ) -> frozenset[uuid.UUID]:\n        return frozenset(data[2])\n\n    def __reduce__(self) -> tuple[\n        Callable[..., ObjectCollection[Any]],\n        tuple[\n            Optional[tuple[builtins.type, ...] | builtins.type],\n            tuple[uuid.UUID, ...],\n            dict[str, Any],\n        ],\n    ]:\n        assert type(self).is_fully_resolved(), \\\n            f'{type(self)} parameters are not resolved'\n\n        cls: type[ObjectCollection[Object_T]] = self.__class__\n        types: Optional[tuple[type, ...]] = self.orig_args\n        if types is None or not cls.is_anon_parametrized():\n            typeargs = None\n        else:\n            typeargs = types[0] if len(types) == 1 else types\n        attrs = {k: getattr(self, k) for k in self.__slots__ if k != '_ids'}\n        return (\n            cls.__restore__,\n            (typeargs, tuple(self._ids), attrs)\n        )\n\n    @classmethod\n    def __restore__(\n        cls,\n        typeargs: Optional[tuple[builtins.type, ...] | builtins.type],\n        ids: tuple[uuid.UUID, ...],\n        attrs: dict[str, Any],\n    ) -> ObjectCollection[Object_T]:\n        if typeargs is None or cls.is_anon_parametrized():\n            obj = cls(_ids=ids, **attrs, _private_init=True)\n        else:\n            obj = cls[typeargs](  # type: ignore\n                _ids=ids, **attrs, _private_init=True)\n\n        return obj\n\n    def dump(self, schema: s_schema.Schema) -> str:\n        return (\n            f'<{type(self).__name__} objects='\n            f'[{\", \".join(o.dump(schema) for o in self.objects(schema))}] '\n            f'at {id(self):#x}>'\n        )\n\n    @classmethod\n    def create(\n        cls: builtins.type[ObjectCollection[Object_T]],\n        schema: s_schema.Schema,\n        data: Collection[Object_T] | ObjectCollection[Object_T],\n        **kwargs: Any,\n    ) -> ObjectCollection[Object_T]:\n        ids: list[uuid.UUID] = []\n\n        if isinstance(data, ObjectCollection):\n            ids.extend(data._ids)\n        elif data:\n            for v in data:\n                ids.append(cls._validate_value(schema, v))\n        container: Collection[uuid.UUID] = cls._container(ids)\n        return cls(container, **kwargs, _private_init=True)\n\n    @classmethod\n    def create_empty(cls) -> ObjectCollection[Object_T]:\n        return cls(cls._container(), _private_init=True)\n\n    @classmethod\n    def _validate_value(cls, schema: s_schema.Schema, v: Object) -> uuid.UUID:\n        if not isinstance(v, cls.type):\n            raise TypeError(\n                f'invalid input data for ObjectIndexByShortname: '\n                f'expected {cls.type} values, got {type(v)}')\n\n        if v.id is not None:\n            return v.id\n        else:\n            raise TypeError(f'object {v!r} has no ID!')\n\n    def ids(self) -> tuple[uuid.UUID, ...]:\n        return tuple(self._ids)\n\n    def names(self, schema: s_schema.Schema) -> Collection[sn.Name]:\n        result = []\n\n        for item_id in self._ids:\n            obj = schema.get_by_id(item_id)\n            result.append(obj.get_name(schema))\n\n        return type(self)._container(result)\n\n    def objects(self, schema: s_schema.Schema) -> tuple[Object_T, ...]:\n        # Calling tuple on a list produced by a comprehension instead\n        # of on a generator comprehension is tragically a slight\n        # performance improvement, and this is a hot path.\n        return tuple([\n            schema.get_by_id(iid) for iid in self._ids  # type: ignore\n        ])\n\n    def _object_keys(\n        self, schema: s_schema.Schema\n    ) -> set[tuple[builtins.type[Object], sn.Name]]:\n        return {(type(x), x.get_name(schema)) for x in (self.objects(schema))}\n\n    @classmethod\n    def compare_values(\n        cls,\n        ours: ObjectCollection[Object_T],\n        theirs: ObjectCollection[Object_T],\n        *,\n        our_schema: s_schema.Schema,\n        their_schema: s_schema.Schema,\n        context: ComparisonContext,\n        compcoef: float,\n    ) -> float:\n        # If the new and old versions share a reference to an object\n        # that is being deleted, then we must delete this object as well.\n        our_keys = set(ours._object_keys(our_schema) if ours else {})\n        their_keys = set(theirs._object_keys(their_schema) if theirs else {})\n        if (our_keys & their_keys) & context.deletions.keys():\n            return 0.0\n\n        if ours is not None:\n            our_names = cls._container(\n                context.get_obj_name(our_schema, obj)\n                for obj in ours.objects(our_schema)\n            )\n        else:\n            our_names = cls._container()\n\n        if theirs is not None:\n            their_names = theirs.names(their_schema)\n        else:\n            their_names = cls._container()\n\n        if our_names != their_names:\n            return compcoef\n        else:\n            return 1.0\n\n    def as_shell(\n        self,\n        schema: s_schema.Schema,\n    ) -> ObjectCollectionShell[Object_T]:\n        return ObjectCollectionShell[Object_T](\n            items=[o.as_shell(schema) for o in self.objects(schema)],\n            collection_type=type(self),\n        )\n\n\nclass ObjectCollectionShell[Object_T: \"Object\"](Shell):\n\n    def __init__(\n        self,\n        items: Iterable[ObjectShell[Object_T]],\n        collection_type: type[ObjectCollection[Object_T]],\n    ) -> None:\n        self.items = items\n        self.collection_type = collection_type\n\n    def __iter__(self) -> Iterator[ObjectShell[Object_T]]:\n        return iter(self.items)\n\n    def __bool__(self) -> bool:\n        return bool(self.items)\n\n    def resolve(self, schema: s_schema.Schema) -> ObjectCollection[Object_T]:\n        return self.collection_type.create(\n            schema,\n            [s.resolve(schema) for s in self.items],\n        )\n\n    def __repr__(self) -> str:\n        tn = self.__class__.__name__\n        cn = self.collection_type.__name__\n        items = ', '.join(str(e.name) or '<anonymous>' for e in self.items)\n        return f'<{tn} {cn}({items}) at 0x{id(self):x}>'\n\n\nclass ObjectIndexBase[Key_T, Object_T: Object](\n    ObjectCollection[Object_T],\n    container=tuple,\n):\n    # The keys here are derived, but caching them is a big performance\n    # win (-14% when it was implemented).\n    #\n    # N.B: Because the keys are cached, if the value of the key is\n    # changed, a new collection must be created!\n    # Currently, ObjectIndexBase is used only for refdicts, and so\n    # this update is done in RenameReferencedInheritingObject._alter_begin().\n\n    __slots__ = ('_ids', '_keys')\n    _key: Callable[[\"s_schema.Schema\", Object_T], Key_T]\n\n    def __init_subclass__(\n        cls,\n        *,\n        key: Optional[Callable[[\"s_schema.Schema\", Object_T], Key_T]] = None,\n    ) -> None:\n        super().__init_subclass__()\n        if key is not None:\n            cls._key = key\n\n    @classmethod\n    def get_key_for(cls, schema: s_schema.Schema, obj: Object) -> Key_T:\n        return cls._key(schema, obj)  # type: ignore  # mypy bug?\n\n    @classmethod\n    def get_key_for_name(\n        cls,\n        schema: s_schema.Schema,\n        name: sn.Name,\n    ) -> Key_T:\n        raise NotImplementedError\n\n    @classmethod\n    def create(\n        cls: type[ObjectIndexBase[Key_T, Object_T]],\n        schema: s_schema.Schema,\n        data: Collection[Object_T] | ObjectCollection[Object_T],\n        **kwargs: Any,\n    ) -> ObjectIndexBase[Key_T, Object_T]:\n        if isinstance(data, ObjectIndexBase):\n            keys = data._keys\n        elif isinstance(data, ObjectCollection):\n            _k = cls._key\n            keys = tuple([_k(schema, x) for x in data.objects(schema)])\n        else:\n            _k = cls._key\n            keys = tuple([_k(schema, x) for x in data])\n\n        coll = cast(\n            ObjectIndexBase[Key_T, Object_T],\n            super().create(schema, data, _keys=keys, **kwargs)\n        )\n        coll._check_duplicates(schema)\n        return coll\n\n    def __init__(\n        self,\n        _ids: Collection[uuid.UUID],\n        _keys: Optional[tuple[Key_T, ...]] = None,\n        *,\n        _private_init: bool,\n    ) -> None:\n        super().__init__(_ids, _private_init=_private_init)\n        self._keys = _keys\n\n    def _check_duplicates(self, schema: s_schema.Schema) -> None:\n        uniq = set(self.keys(schema))\n        if len(uniq) != len(self._ids):\n            counts = collections.Counter(self.keys(schema))\n            duplicates = [v for v, count in counts.items() if count > 1]\n            raise ObjectCollectionDuplicateNameError(\n                'object index contains duplicate key(s): ' +\n                ', '.join(repr(d) for d in duplicates))\n\n    @classmethod\n    def compare_values(\n        cls,\n        ours: ObjectCollection[Object_T],\n        theirs: ObjectCollection[Object_T],\n        *,\n        our_schema: s_schema.Schema,\n        their_schema: s_schema.Schema,\n        context: ComparisonContext,\n        compcoef: float,\n    ) -> float:\n\n        if not ours and not theirs:\n            basecoef = 1.0\n        elif not ours or not theirs:\n            basecoef = 0.2\n        else:\n            assert isinstance(ours, ObjectIndexBase)\n            assert isinstance(theirs, ObjectIndexBase)\n            similarity: list[float] = []\n            for k, v in ours.items(our_schema):\n                try:\n                    theirsv = theirs.get(their_schema, k)\n                except KeyError:\n                    # key only in ours\n                    similarity.append(0.2)\n                else:\n                    similarity.append(\n                        v.compare(theirsv, our_schema=our_schema,\n                                  their_schema=their_schema, context=context))\n\n            diff = (\n                set(theirs.keys(their_schema)) -\n                set(ours.keys(our_schema))\n            )\n            similarity.extend(0.2 for k in diff)\n\n            basecoef = sum(similarity) / len(similarity)\n\n        return basecoef + (1 - basecoef) * compcoef\n\n    def add(\n        self, schema: s_schema.Schema, item: Object_T\n    ) -> tuple[s_schema.Schema, Self]:\n        \"\"\"Return a copy of this collection containing the given item.\n\n        If the item is already present in the collection, an\n        ``ObjectIndexDuplicateNameError`` is raised.\n        \"\"\"\n\n        key = type(self)._key(schema, item)\n        if self.has(schema, key):\n            raise ObjectCollectionDuplicateNameError(\n                f'object index already contains the {key!r} key')\n\n        return self.update(schema, [item])\n\n    def update(\n        self, schema: s_schema.Schema, reps: Iterable[Object_T]\n    ) -> tuple[s_schema.Schema, Self]:\n        items = dict(self.items(schema))\n        keyfunc = type(self)._key\n\n        for obj in reps:\n            items[keyfunc(schema, obj)] = obj\n\n        return (\n            schema,\n            cast(Self, type(self).create(schema, items.values())),\n        )\n\n    def delete(\n        self,\n        schema: s_schema.Schema,\n        names: Iterable[Key_T],\n    ) -> tuple[s_schema.Schema, Self]:\n        items = dict(self.items(schema))\n        for name in names:\n            items.pop(name)\n        return (\n            schema,\n            cast(Self, type(self).create(schema, items.values())),\n        )\n\n    def items(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[tuple[Key_T, Object_T], ...]:\n        result = []\n\n        for key, item_id in zip(self.keys(schema), self._ids):\n            obj = schema.get_by_id(item_id)\n            result.append((key, obj))\n\n        return tuple(result)  # type: ignore\n\n    def keys(self, schema: s_schema.Schema) -> tuple[Key_T, ...]:\n        # To support existing pickled schemas that don't have _keys in them,\n        # lazily compute them if they are missing.\n        # FUTURE: Can drop in 5.0.\n        if self._keys is None:\n            _k = type(self)._key\n            self._keys = tuple([_k(schema, x) for x in self.objects(schema)])\n\n        return self._keys\n\n    def has(self, schema: s_schema.Schema, name: Key_T) -> bool:\n        return name in self.keys(schema)\n\n    def get(\n        self,\n        schema: s_schema.Schema,\n        name: Key_T,\n        default: Optional[Object_T | NoDefaultT] = NoDefault,\n    ) -> Optional[Object_T]:\n        # TODO: Should we store an actual dict?\n        for key, item_id in zip(self.keys(schema), self._ids):\n            if name == key:\n                return schema.get_by_id(item_id)  # type: ignore\n\n        if default is NoDefault:\n            raise KeyError(name)\n        else:\n            return default\n\n\ndef _fullname_object_key(schema: s_schema.Schema, o: Object) -> sn.Name:\n    return o.get_name(schema)\n\n\nclass ObjectIndexByFullname(\n    ObjectIndexBase[sn.Name, Object_T],\n    key=_fullname_object_key,\n):\n\n    @classmethod\n    def get_key_for_name(\n        cls,\n        schema: s_schema.Schema,\n        name: sn.Name,\n    ) -> sn.Name:\n        return name\n\n\ndef _shortname_object_key(schema: s_schema.Schema, o: Object) -> sn.Name:\n    return o.get_shortname(schema)\n\n\nclass ObjectIndexByShortname(\n    ObjectIndexBase[sn.Name, Object_T],\n    key=_shortname_object_key,\n):\n\n    @classmethod\n    def get_key_for_name(\n        cls,\n        schema: s_schema.Schema,\n        name: sn.Name,\n    ) -> sn.Name:\n        return sn.shortname_from_fullname(name)\n\n\ndef _unqualified_object_key(\n    schema: s_schema.Schema,\n    o: QualifiedObject,\n) -> sn.UnqualName:\n    return sn.UnqualName(o.get_shortname(schema).name)\n\n\nclass ObjectIndexByUnqualifiedName(\n    ObjectIndexBase[sn.UnqualName, QualifiedObject_T],\n    key=_unqualified_object_key,\n):\n\n    @classmethod\n    def get_key_for_name(\n        cls,\n        schema: s_schema.Schema,\n        name: sn.Name,\n    ) -> sn.UnqualName:\n        return sn.UnqualName(sn.shortname_from_fullname(name).name)\n\n\nclass ObjectDict[Key_T, Object_T: Object](\n    ObjectCollection[Object_T], container=tuple,\n):\n    __slots__ = ('_ids', '_keys')\n\n    # Breaking the Liskov Substitution Principle\n    @classmethod\n    def create(  # type: ignore\n        cls,\n        schema: s_schema.Schema,\n        data: Mapping[Key_T, Object_T] | ObjectDict[Key_T, Object_T],\n        **kwargs: Any,\n    ) -> ObjectDict[Key_T, Object_T]:\n        if isinstance(data, ObjectDict):\n            return super().create(\n                schema,\n                data,\n                _keys=data._keys,\n            )  # type: ignore\n        else:\n            return super().create(\n                schema,\n                data.values(),\n                _keys=tuple(data.keys()),\n            )  # type: ignore\n\n    @classmethod\n    def create_empty(cls) -> ObjectDict[Key_T, Object_T]:\n        return cls(cls._container(), _private_init=True, _keys=tuple())\n\n    @classmethod\n    def compare_values(\n        cls,\n        ours: ObjectCollection[Object_T],\n        theirs: ObjectCollection[Object_T],\n        *,\n        our_schema: s_schema.Schema,\n        their_schema: s_schema.Schema,\n        context: ComparisonContext,\n        compcoef: float,\n    ) -> float:\n        assert isinstance(ours, ObjectDict)\n        assert isinstance(theirs, ObjectDict)\n        if ours.keys(our_schema) != theirs.keys(their_schema):\n            return compcoef\n        return super().compare_values(\n            ours, theirs,\n            our_schema=our_schema, their_schema=their_schema,\n            context=context, compcoef=compcoef)\n\n    def __init__(\n        self,\n        _ids: Collection[uuid.UUID],\n        _keys: tuple[Key_T, ...],\n        *,\n        _private_init: bool,\n    ) -> None:\n        super().__init__(_ids, _private_init=_private_init)\n        self._keys = _keys\n\n    def __eq__(self, other: Any) -> bool:\n        if not isinstance(other, type(self)):\n            return NotImplemented\n        return self._ids == other._ids and self._keys == other._keys\n\n    def __hash__(self) -> int:\n        return hash((self._ids, self._keys))\n\n    def dump(self, schema: s_schema.Schema) -> str:\n        objs = \", \".join(f\"{self._keys[i]}: {o.dump(schema)}\"\n                         for i, o in enumerate(self.objects(schema)))\n        return f'<{type(self).__name__} objects={objs} at {id(self):#x}>'\n\n    def __repr__(self) -> str:\n        items = [f\"{self._keys[i]}: {id}\" for i, id in enumerate(self._ids)]\n        return f'{{{\", \".join(items)}}}'\n\n    def keys(self, schema: s_schema.Schema) -> tuple[Key_T, ...]:\n        return self._keys\n\n    def values(self, schema: s_schema.Schema) -> tuple[Object_T, ...]:\n        return self.objects(schema)\n\n    def items(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[tuple[Key_T, Object_T], ...]:\n        return tuple(zip(self._keys, self.objects(schema)))\n\n    def as_shell(\n        self,\n        schema: s_schema.Schema,\n    ) -> ObjectDictShell[Key_T, Object_T]:\n        return ObjectDictShell(\n            items={k: o.as_shell(schema) for k, o in self.items(schema)},\n            collection_type=type(self),\n        )\n\n\nclass ObjectDictShell[Key_T, Object_T: \"Object\"](\n    ObjectCollectionShell[Object_T],\n):\n\n    items: Mapping[Any, ObjectShell[Object_T]]\n    collection_type: type[ObjectDict[Key_T, Object_T]]\n\n    def __init__(\n        self,\n        items: Mapping[Any, ObjectShell[Object_T]],\n        collection_type: type[ObjectDict[Key_T, Object_T]],\n    ) -> None:\n        self.items = items\n        self.collection_type = collection_type\n\n    def __repr__(self) -> str:\n        tn = self.__class__.__name__\n        cn = self.collection_type.__name__\n        items = ', '.join(f'{k}: {v.name}' for k, v in self.items.items())\n        return f'<{tn} {cn}({items}) at 0x{id(self):x}>'\n\n    def resolve(self, schema: s_schema.Schema) -> ObjectDict[Key_T, Object_T]:\n        return self.collection_type.create(\n            schema,\n            {k: s.resolve(schema) for k, s in self.items.items()},\n        )\n\n\nclass ObjectSet[Object_T: Object](\n    ObjectCollection[Object_T],\n    container=frozenset,\n):\n\n    def __repr__(self) -> str:\n        return f'{{{\", \".join(str(id) for id in self._ids)}}}'\n\n    @classmethod\n    def merge_values(\n        cls: type[ObjectSet[Object_T]],\n        target: Object,\n        sources: Iterable[Object],\n        field_name: str,\n        *,\n        ignore_local: bool = False,\n        schema: s_schema.Schema,\n    ) -> ObjectSet[Object_T]:\n        if not ignore_local:\n            result = target.get_explicit_field_value(schema, field_name, None)\n        else:\n            result = None\n        for source in sources:\n            if source.__class__.get_field(field_name) is None:\n                continue\n            theirs = source.get_explicit_field_value(schema, field_name, None)\n            if theirs:\n                if result is None:\n                    result = theirs\n                else:\n                    result._ids |= theirs._ids\n\n        return result  # type: ignore\n\n\nclass ObjectList[Object_T: Object](\n    ObjectCollection[Object_T],\n    container=tuple,\n):\n\n    def __repr__(self) -> str:\n        return f'ObjectList([{\", \".join(str(id) for id in self._ids)}])'\n\n    def first(self, schema: s_schema.Schema, default: Any = NoDefault) -> Any:\n        # The `Any` return type is so that using methods on Object subclasses\n        # doesn't cause Mypy to complain.\n        try:\n            return next(iter(self.objects(schema)))\n        except StopIteration:\n            pass\n\n        if default is NoDefault:\n            raise IndexError('ObjectList is empty')\n        else:\n            return default\n\n    # Unfortunately, mypy does not support self generics over types with\n    # typevars, so we have to resort to method redifinition.\n    @classmethod\n    def create(\n        cls,\n        schema: s_schema.Schema,\n        data: Iterable[Object_T] | ObjectCollection[Object_T],\n        **kwargs: Any,\n    ) -> ObjectList[Object_T]:\n        return super().create(schema, data, **kwargs)  # type: ignore\n\n\nclass SubclassableObject(Object):\n\n    abstract = SchemaField(\n        bool,\n        default=False,\n        inheritable=False,\n        special_ddl_syntax=True,\n        compcoef=0.909,\n    )\n\n    def _issubclass(\n        self, schema: s_schema.Schema, parent: SubclassableObject\n    ) -> bool:\n        return parent == self\n\n    def issubclass(\n        self,\n        schema: s_schema.Schema,\n        parent: SubclassableObject | tuple[SubclassableObject, ...],\n    ) -> bool:\n        from . import types as s_types\n        if isinstance(parent, tuple):\n            return any(self.issubclass(schema, p) for p in parent)\n        if (\n            isinstance(parent, s_types.Type)\n            and parent.is_anyobject(schema)\n            and isinstance(self, s_types.Type)\n            and self.is_object_type()\n        ):\n            return True\n        if isinstance(parent, s_types.Type) and parent.is_any(schema):\n            return True\n\n        return self._issubclass(schema, parent)\n\n\nInheritingObjectT = TypeVar('InheritingObjectT', bound='InheritingObject')\n\n\nclass InheritingObject(SubclassableObject):\n\n    bases = SchemaField(\n        ObjectList['InheritingObject'],\n        type_is_generic_self=True,\n        default=DEFAULT_CONSTRUCTOR,\n        coerce=True,\n        inheritable=False,\n        compcoef=0.900,\n    )\n\n    ancestors = SchemaField(\n        ObjectList['InheritingObject'],\n        type_is_generic_self=True,\n        default=DEFAULT_CONSTRUCTOR,\n        coerce=True,\n        inheritable=False,\n        compcoef=0.999,\n    )\n\n    # Fields that have been inherited as opposed to set explicitly.\n    inherited_fields = SchemaField(\n        checked.FrozenCheckedSet[str],\n        default=DEFAULT_CONSTRUCTOR,\n        coerce=True,\n        inheritable=False,\n        compcoef=0.999,\n    )\n\n    is_derived = SchemaField(\n        bool,\n        default=False, compcoef=0.909)\n\n    def inheritable_fields(self) -> Iterable[str]:\n        for fn, f in self.__class__.get_fields().items():\n            if f.inheritable and not f.ephemeral:\n                yield fn\n\n    @classmethod\n    def get_default_base_name(self) -> Optional[sn.Name]:\n        return None\n\n    # Redefining bases and ancestors accessors to make them generic\n    def get_bases(\n        self: InheritingObjectT,\n        schema: s_schema.Schema,\n    ) -> ObjectList[InheritingObjectT]:\n        return self.get_field_value(schema, 'bases')  # type: ignore\n\n    def get_ancestors(\n        self: InheritingObjectT,\n        schema: s_schema.Schema,\n    ) -> ObjectList[InheritingObjectT]:\n        return self.get_field_value(schema, 'ancestors')  # type: ignore\n\n    def get_base_names(self, schema: s_schema.Schema) -> Collection[sn.Name]:\n        return self.get_bases(schema).names(schema)\n\n    def maybe_get_topmost_concrete_base(\n        self: InheritingObjectT, schema: s_schema.Schema\n    ) -> Optional[InheritingObjectT]:\n        \"\"\"Get the topmost non-abstract base.\"\"\"\n        lineage = self.get_ancestors(schema).objects(schema)\n        for ancestor in reversed(lineage):\n            if not ancestor.get_abstract(schema):\n                return ancestor\n\n        if not self.get_abstract(schema):\n            return self\n\n        return None\n\n    def get_topmost_concrete_base(\n        self: InheritingObjectT, schema: s_schema.Schema\n    ) -> InheritingObjectT:\n        \"\"\"Get the topmost non-abstract base.\"\"\"\n        base = self.maybe_get_topmost_concrete_base(schema)\n        if not base:\n            raise errors.SchemaError(\n                f'{self.get_verbosename(schema)} has no non-abstract ancestors'\n            )\n        return base\n\n    def get_base_for_cast(self, schema: s_schema.Schema) -> Object:\n        return self.get_topmost_concrete_base(schema)\n\n    @classmethod\n    def get_root_classes(cls) -> tuple[sn.QualName, ...]:\n        return tuple()\n\n    def _issubclass(\n        self,\n        schema: s_schema.Schema,\n        parent: SubclassableObject,\n    ) -> bool:\n        if parent == self:\n            return True\n\n        lineage = self.get_ancestors(schema).objects(schema)\n        return parent in lineage\n\n    def descendants(\n        self: InheritingObjectT, schema: s_schema.Schema\n    ) -> frozenset[InheritingObjectT]:\n        return schema.get_descendants(self)\n\n    def ordered_descendants(\n        self: InheritingObjectT, schema: s_schema.Schema\n    ) -> list[InheritingObjectT]:\n        \"\"\"Return class descendants in ancestral order.\"\"\"\n        graph = {}\n        for descendant in self.descendants(schema):\n            graph[descendant] = topological.DepGraphEntry(\n                item=descendant,\n                deps=ordered.OrderedSet(\n                    descendant.get_bases(schema).objects(schema),\n                ),\n                extra=False,\n            )\n\n        return list(topological.sort(graph, allow_unresolved=True))\n\n    def children(\n        self: InheritingObjectT,\n        schema: s_schema.Schema,\n    ) -> frozenset[InheritingObjectT]:\n        return schema.get_children(self)\n\n    def field_is_inherited(\n        self,\n        schema: s_schema.Schema,\n        field_name: str,\n    ) -> bool:\n        inherited_fields = self.get_inherited_fields(schema)\n        return field_name in inherited_fields\n\n    def get_explicit_local_field_value(\n        self,\n        schema: s_schema.Schema,\n        field_name: str,\n        default: Any = NoDefault,\n    ) -> Any:\n        inherited_fields = self.get_inherited_fields(schema)\n        if field_name not in inherited_fields:\n            return self.get_explicit_field_value(schema, field_name, default)\n        elif default is not NoDefault:\n            return default\n        else:\n            raise FieldValueNotFoundError(\n                f'{self!r} object has no non-inherited value for '\n                f'field {field_name!r}'\n            )\n\n    def allow_ref_propagation(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        refdict: RefDict,\n    ) -> bool:\n        return True\n\n    def as_alter_delta(\n        self: InheritingObjectT,\n        other: InheritingObjectT,\n        *,\n        self_schema: s_schema.Schema,\n        other_schema: s_schema.Schema,\n        confidence: float,\n        context: ComparisonContext,\n    ) -> sd.ObjectCommand[InheritingObjectT]:\n        from . import delta as sd\n        from . import inheriting as s_inh\n\n        delta = super().as_alter_delta(\n            other,\n            self_schema=self_schema,\n            other_schema=other_schema,\n            confidence=confidence,\n            context=context,\n        )\n\n        rebase = sd.get_object_command_class(\n            s_inh.RebaseInheritingObject, type(self))\n\n        old_base_names = tuple(\n            context.get_obj_name(self_schema, base)\n            for base in self.get_bases(self_schema).objects(self_schema)\n        )\n        new_base_names = other.get_bases(other_schema).names(other_schema)\n\n        if old_base_names != new_base_names and rebase is not None:\n            removed, added = s_inh.delta_bases(\n                old_base_names,\n                new_base_names,\n                t=type(self),\n            )\n\n            rebase_cmd = rebase(\n                classname=other.get_name(other_schema),\n                removed_bases=removed,\n                added_bases=added,\n            )\n\n            rebase_cmd.set_attribute_value(\n                'bases',\n                other.get_bases(other_schema).as_shell(other_schema),\n            )\n\n            rebase_cmd.set_attribute_value(\n                'ancestors',\n                other.get_ancestors(other_schema).as_shell(other_schema),\n            )\n            # Trim these from the base alter since they are redundant\n            # and clog up debug output.\n            delta.discard(not_none(delta._get_attribute_set_cmd('bases')))\n            # ancestors might not be in the delta, if it didn't change\n            if anc := delta._get_attribute_set_cmd('ancestors'):\n                delta.discard(anc)\n\n            delta.add(rebase_cmd)\n\n        return delta\n\n    def record_simple_field_delta(\n        self: InheritingObjectT,\n        schema: s_schema.Schema,\n        delta: sd.ObjectCommand[InheritingObjectT],\n        context: ComparisonContext,\n        *,\n        fname: str,\n        value: Any,\n        orig_value: Any,\n        orig_schema: Optional[s_schema.Schema],\n        orig_object: Optional[InheritingObjectT],\n        from_default: bool = False,\n    ) -> None:\n        inherited_fields = self.get_inherited_fields(schema)\n        is_inherited = fname in inherited_fields\n        if orig_schema is not None and orig_object is not None:\n            orig_inherited_fields = (\n                orig_object.get_inherited_fields(orig_schema))\n            orig_is_inherited = fname in orig_inherited_fields\n        else:\n            orig_is_inherited = is_inherited\n\n        computed_fields = self.get_computed_fields(schema)\n        is_computed = fname in computed_fields\n        if orig_schema is not None and orig_object is not None:\n            orig_computed_fields = (\n                orig_object.get_computed_fields(orig_schema))\n            orig_is_computed = fname in orig_computed_fields\n        else:\n            orig_is_computed = is_computed\n\n        cmd = delta.set_attribute_value(\n            fname,\n            value=value,\n            orig_value=orig_value,\n            inherited=is_inherited,\n            orig_inherited=orig_is_inherited,\n            computed=is_computed,\n            orig_computed=orig_is_computed,\n            from_default=from_default,\n        )\n\n        context.parent_ops.append(delta)\n        cmd.record_diff_annotations(\n            schema=schema,\n            orig_schema=orig_schema,\n            context=context,\n            object=self,\n            orig_object=orig_object,\n        )\n        context.parent_ops.pop()\n\n    def get_field_create_delta(\n        self: InheritingObjectT,\n        schema: s_schema.Schema,\n        delta: sd.ObjectCommand[InheritingObjectT],\n        fname: str,\n        value: Any,\n    ) -> None:\n        inherited_fields = self.get_inherited_fields(schema)\n        delta.set_attribute_value(\n            fname,\n            value=value,\n            inherited=fname in inherited_fields,\n        )\n\n    def get_field_alter_delta(\n        self: Self,\n        old_schema: s_schema.Schema,\n        new_schema: s_schema.Schema,\n        delta: sd.ObjectCommand[InheritingObjectT],\n        fname: str,\n        value: Any,\n        orig_value: Any,\n    ) -> None:\n        inherited_fields = self.get_inherited_fields(new_schema)\n        delta.set_attribute_value(\n            fname,\n            value,\n            orig_value=orig_value,\n            inherited=fname in inherited_fields,\n        )\n\n    def get_field_delete_delta(\n        self: Self,\n        schema: s_schema.Schema,\n        delta: sd.ObjectCommand[InheritingObjectT],\n        fname: str,\n        orig_value: Any,\n    ) -> None:\n        inherited_fields = self.get_inherited_fields(schema)\n        delta.set_attribute_value(\n            fname,\n            value=None,\n            orig_value=orig_value,\n            inherited=fname in inherited_fields,\n        )\n\n    @classmethod\n    def compare_obj_field_value[T](\n        cls: type[Self],\n        field: Field[type[T]],\n        ours: Self,\n        theirs: Self,\n        *,\n        our_schema: s_schema.Schema,\n        their_schema: s_schema.Schema,\n        context: ComparisonContext,\n        explicit: bool = False,\n    ) -> float:\n        similarity = super().compare_obj_field_value(\n            field,\n            ours,\n            theirs,\n            our_schema=our_schema,\n            their_schema=their_schema,\n            context=context,\n            explicit=explicit,\n        )\n\n        # Check to see if this field's inherited status has changed.\n        # If so, this is definitely a change.\n        our_ifs = ours.get_inherited_fields(our_schema)\n        their_ifs = theirs.get_inherited_fields(their_schema)\n\n        fname = field.name\n        if (fname in our_ifs) != (fname in their_ifs):\n            # The change in inherited status decreases the similarity.\n            similarity *= 0.95\n\n        return similarity\n\n\nDerivableInheritingObjectT = TypeVar(\n    'DerivableInheritingObjectT',\n    bound='DerivableInheritingObject',\n)\n\n\nclass DerivableInheritingObject(DerivableObject, InheritingObject):\n\n    def get_nearest_non_derived_parent(\n        self: DerivableInheritingObjectT,\n        schema: s_schema.Schema,\n    ) -> DerivableInheritingObjectT:\n        obj = self\n        while obj.get_is_derived(schema):\n            obj = obj.get_bases(schema).first(schema)\n        return obj\n\n    def get_nearest_generic_parent(\n        self: DerivableInheritingObjectT,\n        schema: s_schema.Schema,\n    ) -> DerivableInheritingObjectT:\n        obj = self\n        while not obj.is_non_concrete(schema):\n            obj = obj.get_bases(schema).first(schema)\n        return obj\n\n\n@markup.serializer.serializer.register(Object)\n@markup.serializer.serializer.register(ObjectCollection)\ndef _serialize_to_markup(o: Object, *, ctx: markup.Context) -> markup.Markup:\n    if 'schema' not in ctx.kwargs:\n        orepr = repr(o)\n    else:\n        orepr = o.dump(ctx.kwargs['schema'])\n\n    return markup.elements.lang.Object(\n        id=id(o),\n        class_module=type(o).__module__,\n        classname=type(o).__name__,\n        repr=orepr,\n    )\n\n\ndef _merge_lineage[InheritingObjectT: 'InheritingObject'](\n    lineage: Iterable[list[InheritingObjectT]],\n    subject_name: str,\n) -> list[InheritingObjectT]:\n    result: list[Any] = []\n\n    while True:\n        nonempty = [line for line in lineage if line]\n        if not nonempty:\n            return result\n\n        for line in nonempty:\n            candidate = line[0]\n            tails = [m for m in nonempty if candidate in m[1:]]\n            if not tails:\n                break\n        else:\n            raise errors.SchemaError(\n                f\"could not find consistent ancestor order for {subject_name}\"\n            )\n\n        result.append(candidate)\n\n        for line in nonempty:\n            if line[0] == candidate:\n                del line[0]\n\n\ndef _compute_lineage[InheritingObjectT: 'InheritingObject'](\n    schema: s_schema.Schema,\n    obj: InheritingObjectT,\n    subject_name: str,\n) -> list[InheritingObjectT]:\n    bases = tuple(obj.get_bases(schema).objects(schema))\n    lineage = [[obj]]\n\n    for base in bases:\n        lineage.append(_compute_lineage(schema, base, subject_name))\n    lineage.append(list(bases))\n\n    return _merge_lineage(lineage, subject_name)\n\n\ndef compute_lineage[InheritingObjectT: 'InheritingObject'](\n    schema: s_schema.Schema,\n    bases: Iterable[InheritingObjectT],\n    subject_name: str,\n) -> list[InheritingObjectT]:\n    lineage = []\n    for base in bases:\n        lineage.append(_compute_lineage(schema, base, subject_name))\n    lineage.append(list(bases))\n\n    try:\n        return _merge_lineage(lineage, subject_name)\n    except errors.SchemaError as e:\n        sbases = ', '.join(str(base.get_name(schema)) for base in bases)\n        details = f'type has specified bases: {sbases}'\n        e.set_hint_and_details(hint=e.hint, details=details)\n        raise\n\n\ndef compute_ancestors[InheritingObjectT: 'InheritingObject'](\n    schema: s_schema.Schema,\n    obj: InheritingObjectT,\n) -> list[InheritingObjectT]:\n    return compute_lineage(\n        schema,\n        obj.get_bases(schema).objects(schema),\n        obj.get_verbosename(schema),\n    )\n\n\ndef derive_name(\n    schema: s_schema.Schema,\n    *qualifiers: str,\n    module: str,\n    parent: Optional[DerivableObject] = None,\n    derived_name_base: Optional[sn.Name] = None,\n) -> sn.QualName:\n    if derived_name_base is None:\n        assert parent is not None\n        derived_name_base = parent.get_derived_name_base(schema)\n    name = sn.get_specialized_name(derived_name_base, *qualifiers)\n    return sn.QualName(name=name, module=module)\n"
  },
  {
    "path": "edb/schema/objtypes.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Optional, Iterable, cast\n\nimport collections\n\nfrom edb import errors\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom . import annos as s_anno\nfrom . import constraints\nfrom . import delta as sd\nfrom . import functions as s_func\nfrom . import inheriting\nfrom . import links\nfrom . import properties\nfrom . import name as sn\nfrom . import objects as so\nfrom . import pointers\nfrom . import policies\nfrom . import schema as s_schema\nfrom . import sources\nfrom . import triggers\nfrom . import types as s_types\nfrom . import unknown_pointers\nfrom . import utils\n\n\nclass ObjectTypeRefMixin(so.Object):\n    # We stick access policies and triggers in their own class as a\n    # hack, to allow us to ensure that access_policies comes later in\n    # the refdicts list than pointers does, so that pointers are\n    # always created before access policies when creating an inherited\n    # type.\n    access_policies_refs = so.RefDict(\n        attr='access_policies',\n        requires_explicit_overloaded=True,\n        backref_attr='subject',\n        ref_cls=policies.AccessPolicy)\n\n    access_policies = so.SchemaField(\n        so.ObjectIndexByUnqualifiedName[policies.AccessPolicy],\n        inheritable=False, ephemeral=True, coerce=True, compcoef=0.857,\n        default=so.DEFAULT_CONSTRUCTOR)\n\n    triggers_refs = so.RefDict(\n        attr='triggers',\n        requires_explicit_overloaded=True,\n        backref_attr='subject',\n        ref_cls=triggers.Trigger)\n\n    triggers = so.SchemaField(\n        so.ObjectIndexByUnqualifiedName[triggers.Trigger],\n        inheritable=False, ephemeral=True, coerce=True, compcoef=0.857,\n        default=so.DEFAULT_CONSTRUCTOR)\n\n\nclass ObjectType(\n    sources.Source,\n    constraints.ConsistencySubject,\n    s_types.InheritingType,\n\n    so.InheritingObject,  # Help reflection figure out the right db MRO\n    s_types.Type,  # Help reflection figure out the right db MRO\n    s_anno.AnnotationSubject,  # Help reflection figure out the right db MRO\n    ObjectTypeRefMixin,\n    qlkind=qltypes.SchemaObjectClass.TYPE,\n    data_safe=False,\n):\n\n    union_of = so.SchemaField(\n        so.ObjectSet[\"ObjectType\"],\n        default=so.DEFAULT_CONSTRUCTOR,\n        coerce=True,\n        type_is_generic_self=True,\n        compcoef=0.0,\n    )\n\n    intersection_of = so.SchemaField(\n        so.ObjectSet[\"ObjectType\"],\n        default=so.DEFAULT_CONSTRUCTOR,\n        coerce=True,\n        type_is_generic_self=True,\n    )\n\n    is_opaque_union = so.SchemaField(\n        bool,\n        default=False,\n    )\n\n    def is_object_type(self) -> bool:\n        return True\n\n    def is_free_object_type(self, schema: s_schema.Schema) -> bool:\n        if self.get_name(schema) == sn.QualName('std', 'FreeObject'):\n            return True\n\n        FreeObject = schema.get(\n            'std::FreeObject', type=ObjectType, default=None)\n        if FreeObject is None:\n            # Possible in bootstrap before FreeObject is declared\n            return False\n        else:\n            return self.issubclass(schema, FreeObject)\n\n    def is_fake_object_type(self, schema: s_schema.Schema) -> bool:\n        return self.is_free_object_type(schema)\n\n    def is_material_object_type(self, schema: s_schema.Schema) -> bool:\n        return not (\n            self.is_fake_object_type(schema)\n            or self.is_compound_type(schema)\n            or self.is_view(schema)\n        )\n\n    def is_union_type(self, schema: s_schema.Schema) -> bool:\n        return bool(self.get_union_of(schema))\n\n    def is_intersection_type(self, schema: s_schema.Schema) -> bool:\n        return bool(self.get_intersection_of(schema))\n\n    def is_compound_type(self, schema: s_schema.Schema) -> bool:\n        return self.is_union_type(schema) or self.is_intersection_type(schema)\n\n    def get_displayname(self, schema: s_schema.Schema) -> str:\n        if self.is_view(schema) and not self.get_alias_is_persistent(schema):\n            schema, mtype = self.material_type(schema)\n        else:\n            mtype = self\n\n        union_of = mtype.get_union_of(schema)\n        if union_of:\n            if self.get_is_opaque_union(schema):\n                std_obj = schema.get('std::BaseObject', type=ObjectType)\n                return std_obj.get_displayname(schema)\n            else:\n                comp_dns = sorted(\n                    (c.get_displayname(schema)\n                     for c in union_of.objects(schema)))\n                return '(' + ' | '.join(comp_dns) + ')'\n        else:\n            intersection_of = mtype.get_intersection_of(schema)\n            if intersection_of:\n                comp_dns = sorted(\n                    (c.get_displayname(schema)\n                     for c in intersection_of.objects(schema)))\n                # Elide BaseObject from display, because `& BaseObject`\n                # is a nop.\n                return '(' + ' & '.join(\n                    dn for dn in comp_dns if dn != 'std::BaseObject'\n                ) + ')'\n            elif mtype == self:\n                return super().get_displayname(schema)\n            else:\n                return mtype.get_displayname(schema)\n\n    def getrptrs(\n        self,\n        schema: s_schema.Schema,\n        name: str,\n        *,\n        sources: Iterable[so.Object] = ()\n    ) -> set[links.Link]:\n        if sn.is_qualified(name):\n            raise ValueError(\n                'references to concrete pointers must not be qualified')\n\n        ptrs: set[links.Link] = set()\n\n        ancestor_objects = self.get_ancestors(schema).objects(schema)\n\n        for obj in (self,) + ancestor_objects:\n            ptrs.update(\n                lnk for lnk in schema.get_referrers(\n                    obj, scls_type=links.Link, field_name='target')\n                if (\n                    lnk.get_shortname(schema).name == name\n                    and lnk.get_source_type(schema).is_material_object_type(\n                        schema)\n                    # Only grab the \"base\" pointers\n                    and all(\n                        b.is_non_concrete(schema)\n                        for b in lnk.get_bases(schema).objects(schema)\n                    )\n                    and (not sources or lnk.get_source_type(schema) in sources)\n                )\n            )\n\n        for intersection in self.get_intersection_of(schema).objects(schema):\n            ptrs.update(intersection.getrptrs(schema, name, sources=sources))\n\n        unions = schema.get_referrers(\n            self, scls_type=ObjectType, field_name='union_of')\n\n        for union in unions:\n            ptrs.update(union.getrptrs(schema, name, sources=sources))\n\n        return ptrs\n\n    def get_relevant_triggers(\n        self, kind: qltypes.TriggerKind, schema: s_schema.Schema\n    ) -> list[triggers.Trigger]:\n        return [\n            t for t in self.get_triggers(schema).objects(schema)\n            if kind in t.get_kinds(schema)\n        ]\n\n    def implicitly_castable_to(\n        self, other: s_types.Type, schema: s_schema.Schema\n    ) -> bool:\n        return self.issubclass(schema, other)\n\n    def find_common_implicitly_castable_type(\n        self,\n        other: s_types.Type,\n        schema: s_schema.Schema,\n    ) -> tuple[s_schema.Schema, Optional[ObjectType]]:\n        if not isinstance(other, ObjectType):\n            return schema, None\n\n        nearest_common_ancestors = utils.get_class_nearest_common_ancestors(\n            schema, [self, other]\n        )\n        # We arbitrarily select the first nearest common ancestor\n        nearest_common_ancestor = (\n            nearest_common_ancestors[0] if nearest_common_ancestors else None)\n\n        if nearest_common_ancestor is not None:\n            assert isinstance(nearest_common_ancestor, ObjectType)\n        return (\n            schema,\n            nearest_common_ancestor,\n        )\n\n    @classmethod\n    def get_root_classes(cls) -> tuple[sn.QualName, ...]:\n        return (\n            sn.QualName(module='std', name='BaseObject'),\n            sn.QualName(module='std', name='Object'),\n            sn.QualName(module='std', name='FreeObject'),\n        )\n\n    @classmethod\n    def get_default_base_name(cls) -> sn.QualName:\n        return sn.QualName(module='std', name='Object')\n\n    def _issubclass(\n        self, schema: s_schema.Schema, parent: so.SubclassableObject\n    ) -> bool:\n        if self == parent:\n            return True\n\n        if (\n            (my_union := self.get_union_of(schema))\n            and not self.get_is_opaque_union(schema)\n        ):\n            # A union is considered a subclass of a type, if\n            # ALL its components are subclasses of that type.\n            return all(\n                t._issubclass(schema, parent)\n                for t in my_union.objects(schema)\n            )\n\n        if my_intersection := self.get_intersection_of(schema):\n            # An intersection is considered a subclass of a type, if\n            # ANY of its components are subclasses of that type.\n            return any(\n                t._issubclass(schema, parent)\n                for t in my_intersection.objects(schema)\n            )\n\n        lineage = self.get_ancestors(schema).objects(schema)\n        if parent in lineage:\n            return True\n\n        elif isinstance(parent, ObjectType):\n            if (\n                (parent_union := parent.get_union_of(schema))\n                and not parent.get_is_opaque_union(schema)\n            ):\n                # A type is considered a subclass of a union type,\n                # if it is a subclass of ANY of the union components.\n                return (\n                    parent.get_is_opaque_union(schema)\n                    or any(\n                        self._issubclass(schema, t)\n                        for t in parent_union.objects(schema)\n                    )\n                )\n\n            if parent_intersection := parent.get_intersection_of(schema):\n                # A type is considered a subclass of an intersection type,\n                # if it is a subclass of ALL of the intersection components.\n                return all(\n                    self._issubclass(schema, t)\n                    for t in parent_intersection.objects(schema)\n                )\n\n        return False\n\n    def allow_ref_propagation(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        refdict: so.RefDict,\n    ) -> bool:\n        return not self.is_view(schema) or refdict.attr == 'pointers'\n\n    def as_type_delete_if_unused(\n        self,\n        schema: s_schema.Schema,\n    ) -> Optional[sd.DeleteObject[ObjectType]]:\n        if not self._is_deletable(schema):\n            return None\n\n        # References to aliases can only occur inside other aliases,\n        # so when they go, we need to delete the reference also.\n        # Compound types also need to be deleted when their last\n        # referrer goes.\n        if (\n            self.is_view(schema)\n            and self.get_alias_is_persistent(schema)\n        ) or self.is_compound_type(schema):\n            return self.init_delta_command(\n                schema,\n                sd.DeleteObject,\n                if_unused=True,\n                if_exists=True,\n            )\n        else:\n            return None\n\n    def _test_polymorphic(\n        self, schema: s_schema.Schema, other: s_types.Type\n    ) -> bool:\n        if other.is_anyobject(schema):\n            return True\n        return False\n\n\ndef get_or_create_union_type(\n    schema: s_schema.Schema,\n    components: Iterable[ObjectType],\n    *,\n    transient: bool = False,\n    opaque: bool = False,\n    module: Optional[str] = None,\n) -> tuple[s_schema.Schema, ObjectType, bool]:\n\n    name = s_types.get_union_type_name(\n        (c.get_name(schema) for c in components),\n        opaque=opaque,\n        module=module,\n    )\n\n    objtype = schema.get(name, default=None, type=ObjectType)\n    created = objtype is None\n    if objtype is None:\n        components = list(components)\n\n        std_object = schema.get('std::BaseObject', type=ObjectType)\n\n        schema, objtype = std_object.derive_subtype(\n            schema,\n            name=name,\n            attrs=dict(\n                union_of=so.ObjectSet.create(schema, components),\n                is_opaque_union=opaque,\n                abstract=True,\n            ),\n            transient=transient,\n        )\n\n        if not opaque:\n\n            schema = sources.populate_pointer_set_for_source_union(\n                schema,\n                cast(list[sources.Source], components),\n                objtype,\n                modname=module,\n            )\n\n    return schema, objtype, created\n\n\ndef get_or_create_intersection_type(\n    schema: s_schema.Schema,\n    components: Iterable[ObjectType],\n    *,\n    module: Optional[str] = None,\n    transient: bool = False,\n) -> tuple[s_schema.Schema, ObjectType, bool]:\n\n    name = s_types.get_intersection_type_name(\n        (c.get_name(schema) for c in components),\n        module=module,\n    )\n\n    objtype = schema.get(name, default=None, type=ObjectType)\n    created = objtype is None\n    if objtype is None:\n        components = list(components)\n\n        std_object = schema.get('std::BaseObject', type=ObjectType)\n\n        schema, objtype = std_object.derive_subtype(\n            schema,\n            name=name,\n            attrs=dict(\n                intersection_of=so.ObjectSet.create(schema, components),\n                abstract=True,\n            ),\n            transient=transient,\n        )\n\n        ptrs_dict = collections.defaultdict(list)\n\n        for component in components:\n            for pn, ptr in component.get_pointers(schema).items(schema):\n                ptrs_dict[pn].append(ptr)\n\n        intersection_pointers = {}\n\n        for pn, ptrs in ptrs_dict.items():\n            if len(ptrs) > 1:\n                # The pointer is present in more than one component.\n                schema, ptr = pointers.get_or_create_intersection_pointer(\n                    schema,\n                    ptrname=pn,\n                    source=objtype,\n                    components=set(ptrs),\n                    transient=transient,\n                )\n            else:\n                ptr = ptrs[0]\n\n            intersection_pointers[pn] = ptr\n\n        for pn, ptr in intersection_pointers.items():\n            if objtype.maybe_get_ptr(schema, pn) is None:\n                schema = objtype.add_pointer(schema, ptr)\n\n    assert isinstance(objtype, ObjectType)\n    return schema, objtype, created\n\n\nclass ObjectTypeCommandContext(\n    links.LinkSourceCommandContext[ObjectType],\n    properties.PropertySourceContext[ObjectType],\n    unknown_pointers.UnknownPointerSourceContext[ObjectType],\n    policies.AccessPolicySourceCommandContext[ObjectType],\n    triggers.TriggerSourceCommandContext[ObjectType],\n    sd.ObjectCommandContext[ObjectType],\n    constraints.ConsistencySubjectCommandContext,\n    s_anno.AnnotationSubjectCommandContext,\n):\n    pass\n\n\nclass ObjectTypeCommand(\n    s_types.InheritingTypeCommand[ObjectType],\n    constraints.ConsistencySubjectCommand[ObjectType],\n    sources.SourceCommand[ObjectType],\n    links.LinkSourceCommand[ObjectType],\n    context_class=ObjectTypeCommandContext,\n):\n    def validate_object(\n        self, schema: s_schema.Schema, context: sd.CommandContext\n    ) -> None:\n        if (\n            not context.stdmode\n            and not context.testmode\n            and self.scls.is_material_object_type(schema)\n        ):\n            for base in self.scls.get_bases(schema).objects(schema):\n                name = base.get_name(schema)\n                if (\n                    sn.UnqualName(name.module) in s_schema.STD_MODULES\n                    and name not in (\n                        sn.QualName('std', 'BaseObject'),\n                        sn.QualName('std', 'Object'),\n                    )\n                ):\n                    raise errors.SchemaDefinitionError(\n                        f\"cannot extend system type '{name}'\",\n                        span=self.span,\n                    )\n\n        # Internal consistency check: our stdlib and extension types\n        # shouldn't extend std::Object, which is reserved for user\n        # types.\n        if (\n            self.scls.is_material_object_type(schema)\n            and self.classname.get_root_module_name() in s_schema.STD_MODULES\n        ):\n            for base in self.scls.get_bases(schema).objects(schema):\n                name = base.get_name(schema)\n                if name == sn.QualName('std', 'Object'):\n                    raise errors.SchemaDefinitionError(\n                        f\"standard lib/extension type '{self.classname}' \"\n                        f\"cannot extend std::Object\",\n                        hint=\"try BaseObject\",\n                    )\n\n\nclass CreateObjectType(\n    ObjectTypeCommand,\n    s_types.CreateInheritingType[ObjectType],\n):\n    astnode = qlast.CreateObjectType\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        if (self.get_attribute_value('expr_type')\n                and not self.get_attribute_value('expr')):\n            # This is a nested view type, e.g\n            # __FooAlias_bar produced by  FooAlias := (SELECT Foo { bar: ... })\n            # and should obviously not appear as a top level definition.\n            return None\n        else:\n            return super()._get_ast(schema, context, parent_node=parent_node)\n\n    def _get_ast_node(\n        self, schema: s_schema.Schema, context: sd.CommandContext\n    ) -> type[qlast.DDLOperation]:\n        if self.get_attribute_value('expr_type'):\n            return qlast.CreateAlias\n        else:\n            return super()._get_ast_node(schema, context)\n\n    def _create_finalize(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        if (\n            not context.canonical\n            and self.scls.is_material_object_type(schema)\n        ):\n            # Propagate changes to any functions that depend on\n            # ancestor types in order to recompute the inheritance\n            # situation.\n            schema = self._propagate_if_expr_refs(\n                schema,\n                context,\n                action='creating an object type',\n                include_ancestors=True,\n                filter=s_func.Function,\n            )\n\n        return super()._create_finalize(schema, context)\n\n\nclass RenameObjectType(\n    ObjectTypeCommand,\n    s_types.RenameInheritingType[ObjectType],\n):\n    pass\n\n\nclass RebaseObjectType(\n    ObjectTypeCommand,\n    s_types.RebaseInheritingType[ObjectType],\n):\n    pass\n\n\nclass AlterObjectType(\n    ObjectTypeCommand,\n    s_types.AlterType[ObjectType],\n    inheriting.AlterInheritingObject[ObjectType],\n):\n    astnode = qlast.AlterObjectType\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._alter_begin(schema, context)\n\n        if (\n            not context.canonical\n            and bool(self.get_subcommands(type=policies.AccessPolicyCommand))\n        ):\n            from . import functions\n            # If we have any policy commands, we need to propagate to update\n            # functions. We also need to propagate to anything that updates\n            # an ancestor.\n            #\n            # Note that the ancestor search does not generate\n            # quadratically many updates in the case that this change\n            # was propagated from an ancestor, since the\n            # _propagate_if_expr_refs call in the ancestor temporarily\n            # eliminates the ref!\n            schema = self._propagate_if_expr_refs(\n                schema,\n                context,\n                action=self.get_friendly_description(schema=schema),\n                include_ancestors=True,\n                filter=functions.Function,\n            )\n\n        return schema\n\n    def _alter_finalize(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n\n        if not context.canonical:\n            # If this type is contained in any unions, we need to\n            # update them with any additions or alterations made to\n            # this type. (Deletions are already handled in DeletePointer.)\n            unions = schema.get_referrers(\n                self.scls, scls_type=ObjectType, field_name='union_of')\n\n            orig_disable = context.disable_dep_verification\n\n            for union in unions:\n                if union.get_is_opaque_union(schema):\n                    continue\n\n                delete = union.init_delta_command(schema, sd.DeleteObject)\n\n                context.disable_dep_verification = True\n                delete.apply(schema, context)\n                context.disable_dep_verification = orig_disable\n                # We run the delete to populate the tree, but then instead\n                # of actually deleting the object, we just remove the names.\n                # This is because the pointers in the types we are looking\n                # at might themselves reference the union, so we need\n                # them in the schema to produce the correct as_alter_delta.\n                nschema = _delete_to_delist(delete, schema)\n\n                nschema, nunion, _ = utils.ensure_union_type(\n                    nschema,\n                    types=union.get_union_of(schema).objects(schema),\n                    opaque=union.get_is_opaque_union(schema),\n                    module=union.get_name(schema).module,\n                )\n                assert isinstance(nunion, ObjectType)\n\n                diff = union.as_alter_delta(\n                    other=nunion,\n                    self_schema=schema,\n                    other_schema=nschema,\n                    confidence=1.0,\n                    context=so.ComparisonContext(),\n                )\n\n                schema = diff.apply(schema, context)\n                self.add(diff)\n\n        return super()._alter_finalize(schema, context)\n\n\ndef _delete_to_delist(\n    delete: sd.DeleteObject[so.Object],\n    schema: s_schema.Schema,\n) -> s_schema.Schema:\n    \"\"\"Delist all of the objects mentioned in a delete tree.\n\n    This removes their names from the schema but preserves the actual\n    objects.\n    \"\"\"\n    schema = schema.delist(delete.classname)\n    for sub in delete.get_subcommands(type=sd.DeleteObject):\n        schema = _delete_to_delist(sub, schema)\n    return schema\n\n\nclass DeleteObjectType(\n    ObjectTypeCommand,\n    s_types.DeleteType[ObjectType],\n    inheriting.DeleteInheritingObject[ObjectType],\n):\n    astnode = qlast.DropObjectType\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        if self.get_orig_attribute_value('expr_type'):\n            # This is an alias type, appropriate DDL would be generated\n            # from the corresponding DeleteAlias node.\n            return None\n        else:\n            return super()._get_ast(schema, context, parent_node=parent_node)\n\n    def _delete_finalize(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        if (\n            not context.canonical\n            and self.scls.is_material_object_type(schema)\n        ):\n            # Propagate changes to any functions that depend on\n            # ancestor types in order to recompute the inheritance\n            # situation.\n            schema = self._propagate_if_expr_refs(\n                schema,\n                context,\n                action='deleting an object type',\n                include_self=False,\n                include_ancestors=True,\n                filter=s_func.Function,\n            )\n\n        return super()._delete_finalize(schema, context)\n"
  },
  {
    "path": "edb/schema/operators.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Any, Optional, Mapping\n\nfrom edb import errors\nfrom edb.common import checked\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes as ft\n\nfrom . import delta as sd\nfrom . import functions as s_func\nfrom . import name as sn\nfrom . import objects as so\nfrom . import schema as s_schema\nfrom . import utils\n\n\nclass Operator(\n    s_func.CallableObject,\n    s_func.VolatilitySubject,\n    qlkind=ft.SchemaObjectClass.OPERATOR,\n    data_safe=True,\n):\n\n    operator_kind = so.SchemaField(\n        ft.OperatorKind, coerce=True, compcoef=0.4)\n\n    language = so.SchemaField(\n        qlast.Language, default=None, compcoef=0.4, coerce=True)\n\n    from_operator = so.SchemaField(\n        checked.CheckedList[str], coerce=True,\n        default=None, compcoef=0.4)\n\n    from_function = so.SchemaField(\n        checked.CheckedList[str], coerce=True,\n        default=None, compcoef=0.4)\n\n    from_expr = so.SchemaField(\n        bool, default=False, compcoef=0.4)\n\n    force_return_cast = so.SchemaField(\n        bool, default=False, compcoef=0.9)\n\n    code = so.SchemaField(\n        str, default=None, compcoef=0.4)\n\n    # An unused dummy field. We have this here to make it easier to\n    # test the *removal* of internal schema fields during in-place\n    # upgrades.\n    _dummy_field = so.SchemaField(\n        str, default=None)\n\n    # If this is a derivative operator, *derivative_of* would\n    # contain the name of the origin operator.\n    # For example, the `std::IN` operator has `std::=`\n    # as its origin.\n    derivative_of = so.SchemaField(\n        sn.QualName, coerce=True, default=None, compcoef=0.4)\n\n    commutator = so.SchemaField(\n        sn.QualName, coerce=True, default=None, compcoef=0.99)\n\n    negator = so.SchemaField(\n        sn.QualName, coerce=True, default=None, compcoef=0.99)\n\n    recursive = so.SchemaField(\n        bool, default=False, compcoef=0.4)\n\n    def get_display_signature(self, schema: s_schema.Schema) -> str:\n        params = [\n            p.get_type(schema).get_displayname(schema)\n            for p in self.get_params(schema).objects(schema)\n        ]\n        name = self.get_shortname(schema).name\n        kind = self.get_operator_kind(schema)\n        if kind is ft.OperatorKind.Infix:\n            return f'{params[0]} {name} {params[1]}'\n        elif kind is ft.OperatorKind.Postfix:\n            return f'{params[0]} {name}'\n        elif kind is ft.OperatorKind.Prefix:\n            return f'{name} {params[0]}'\n        elif kind is ft.OperatorKind.Ternary:\n            return f'{name} ({\", \".join(params)})'\n        else:\n            raise ValueError('unexpected operator kind')\n\n    def get_verbosename(\n        self, schema: s_schema.Schema, *, with_parent: bool = False\n    ) -> str:\n        return f'operator \"{self.get_display_signature(schema)}\"'\n\n\nclass OperatorCommandContext(s_func.CallableCommandContext):\n    pass\n\n\nclass OperatorCommand(\n    s_func.CallableCommand[Operator],\n    context_class=OperatorCommandContext,\n):\n\n    def get_ast_attr_for_field(\n        self,\n        field: str,\n        astnode: type[qlast.DDLOperation],\n    ) -> Optional[str]:\n        if field == 'abstract':\n            return field\n        elif field == 'operator_kind':\n            return 'kind'\n        else:\n            return super().get_ast_attr_for_field(field, astnode)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        if not context.stdmode and not context.testmode:\n            raise errors.UnsupportedFeatureError(\n                'user-defined operators are not supported',\n                span=astnode.span\n            )\n\n        return super()._cmd_tree_from_ast(schema, astnode, context)\n\n    @classmethod\n    def _classname_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext,\n    ) -> sn.QualName:\n        assert isinstance(astnode, qlast.OperatorCommand)\n        assert isinstance(astnode, qlast.ObjectDDL)\n        name = super()._classname_from_ast(schema, astnode, context)\n\n        params = cls._get_param_desc_from_ast(\n            schema, context.modaliases, astnode)\n        fqname = cls.get_schema_metaclass().get_fqname(\n            schema, name, params, astnode.kind)\n        assert isinstance(fqname, sn.QualName)\n        return fqname\n\n\nclass CreateOperator(\n    s_func.CreateCallableObject[Operator],\n    OperatorCommand,\n):\n    astnode = qlast.CreateOperator\n\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        fullname = self.classname\n        shortname = sn.shortname_from_fullname(fullname)\n        schema, cp = self._get_param_desc_from_delta(schema, context, self)\n        signature = f'{shortname}({\", \".join(p.as_str(schema) for p in cp)})'\n\n        func = schema.get(fullname, None)\n        if func:\n            raise errors.InvalidOperatorDefinitionError(\n                f'cannot create the `{signature}` operator: '\n                f'an operator with the same signature '\n                f'is already defined',\n                span=self.span)\n\n        schema = super()._create_begin(schema, context)\n\n        params: s_func.FuncParameterList = self.scls.get_params(schema)\n        fullname = self.scls.get_name(schema)\n        shortname = sn.shortname_from_fullname(fullname)\n        return_typemod = self.scls.get_return_typemod(schema)\n        assert isinstance(self.scls, Operator)\n        recursive = self.scls.get_recursive(schema)\n        derivative_of = self.scls.get_derivative_of(schema)\n\n        # an operator must have operands\n        if len(params) == 0:\n            raise errors.InvalidOperatorDefinitionError(\n                f'cannot create the `{signature}` operator: '\n                f'an operator must have operands',\n                span=self.span)\n\n        # We'll need to make sure that there's no mix of recursive and\n        # non-recursive operators being overloaded.\n        all_arrays = all_tuples = all_ranges = True\n        for param in params.objects(schema):\n            ptype = param.get_type(schema)\n            all_arrays = all_arrays and ptype.is_array()\n            all_tuples = all_tuples and ptype.is_tuple(schema)\n            all_ranges = all_ranges and (ptype.is_range()\n                                         or ptype.is_multirange())\n\n        # It's illegal to declare an operator as recursive unless all\n        # of its operands are the same basic type of collection.\n        if recursive and not any([all_arrays, all_tuples, all_ranges]):\n            raise errors.InvalidOperatorDefinitionError(\n                f'cannot create the `{signature}` operator: '\n                f'operands of a recursive operator must either be '\n                f'all arrays or all tuples',\n                span=self.span)\n\n        for oper in lookup_operators(shortname, (), schema=schema):\n            if oper == self.scls:\n                continue\n\n            oper_return_typemod = oper.get_return_typemod(schema)\n            if oper_return_typemod != return_typemod:\n                raise errors.DuplicateOperatorDefinitionError(\n                    f'cannot create the `{signature}` '\n                    f'operator: overloading another operator with different '\n                    f'return type {oper_return_typemod.to_edgeql()} '\n                    f'{oper.get_return_type(schema).name}',\n                    span=self.span)\n\n            oper_derivative_of = oper.get_derivative_of(schema)\n            if oper_derivative_of:\n                raise errors.DuplicateOperatorDefinitionError(\n                    f'cannot create the `{signature}` '\n                    f'operator: there exists a derivative operator of the '\n                    f'same name',\n                    span=self.span)\n            elif derivative_of:\n                raise errors.DuplicateOperatorDefinitionError(\n                    f'cannot create `{signature}` '\n                    f'as a derivative operator: there already exists an '\n                    f'operator of the same name',\n                    span=self.span)\n\n            # Check if there is a recursive/non-recursive operator\n            # overloading.\n            oper_recursive = oper.get_recursive(schema)\n            if recursive != oper_recursive:\n                oper_signature = oper.get_display_signature(schema)\n                oper_all_arrays = oper_all_tuples = oper_all_ranges = True\n                for param in oper.get_params(schema).objects(schema):\n                    ptype = param.get_type(schema)\n                    oper_all_arrays = oper_all_arrays and ptype.is_array()\n                    oper_all_tuples = (\n                        oper_all_tuples\n                        and ptype.is_tuple(schema)\n                    )\n                    oper_all_ranges = oper_all_ranges and (\n                        ptype.is_range() or ptype.is_multirange()\n                    )\n\n                if (all_arrays == oper_all_arrays and\n                        all_tuples == oper_all_tuples and\n                        all_ranges == oper_all_ranges):\n                    new_rec = 'recursive' if recursive else 'non-recursive'\n                    oper_rec = \\\n                        'recursive' if oper_recursive else 'non-recursive'\n\n                    raise errors.InvalidOperatorDefinitionError(\n                        f'cannot create the {new_rec} `{signature}` operator: '\n                        f'overloading a {oper_rec} operator '\n                        f'`{oper_signature}` with a {new_rec} one '\n                        f'is not allowed',\n                        span=self.span)\n\n        return schema\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        assert isinstance(astnode, qlast.CreateOperator)\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        cmd.set_attribute_value(\n            'operator_kind',\n            astnode.kind,\n        )\n\n        if astnode.code is not None:\n            cmd.set_attribute_value(\n                'language',\n                astnode.code.language,\n            )\n            if astnode.code.from_operator is not None:\n                cmd.set_attribute_value(\n                    'from_operator',\n                    astnode.code.from_operator,\n                )\n            if astnode.code.from_function is not None:\n                cmd.set_attribute_value(\n                    'from_function',\n                    astnode.code.from_function,\n                )\n            if astnode.code.code is not None:\n                # TODO: Make operators from code strict when we can?\n                cmd.set_attribute_value(\n                    'impl_is_strict', False\n                )\n                cmd.set_attribute_value(\n                    'code',\n                    astnode.code.code,\n                )\n            if astnode.code.from_expr is not None:\n                cmd.set_attribute_value(\n                    'from_expr',\n                    astnode.code.from_expr,\n                )\n\n        return cmd\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        assert isinstance(node, qlast.CreateOperator)\n        new_value: Any = op.new_value\n\n        if op.property == 'return_type':\n            node.returning = utils.typeref_to_ast(schema, new_value)\n\n        elif op.property == 'return_typemod':\n            node.returning_typemod = new_value\n\n        elif op.property == 'code':\n            if node.code is None:\n                node.code = qlast.OperatorCode()\n            node.code.code = new_value\n\n        elif op.property == 'language':\n            if node.code is None:\n                node.code = qlast.OperatorCode()\n            node.code.language = new_value\n\n        elif op.property == 'from_function' and new_value:\n            if node.code is None:\n                node.code = qlast.OperatorCode()\n            node.code.from_function = new_value\n\n        elif op.property == 'from_expr' and new_value:\n            if node.code is None:\n                node.code = qlast.OperatorCode()\n            node.code.from_expr = new_value\n\n        elif op.property == 'from_operator' and new_value:\n            if node.code is None:\n                node.code = qlast.OperatorCode()\n            node.code.from_operator = tuple(new_value)\n\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n\nclass RenameOperator(sd.RenameObject[Operator], OperatorCommand):\n    pass\n\n\nclass AlterOperator(s_func.AlterCallableObject[Operator], OperatorCommand):\n    astnode = qlast.AlterOperator\n\n\nclass DeleteOperator(s_func.DeleteCallableObject[Operator], OperatorCommand):\n    astnode = qlast.DropOperator\n\n\ndef lookup_operators(\n    name: sn.Name | str,\n    default: tuple[Operator, ...] | so.NoDefaultT = so.NoDefault,\n    *,\n    module_aliases: Optional[Mapping[Optional[str], str]] = None,\n    schema: s_schema.Schema,\n) -> tuple[Operator, ...]:\n    funcs: tuple[Operator, ...] | so.NoDefaultT = s_schema.lookup(\n        schema,\n        name,\n        getter=s_schema._get_operators,\n        module_aliases=module_aliases,\n        default=default,\n    )\n\n    if funcs is not so.NoDefault:\n        return funcs\n    else:\n        return s_schema.Schema.raise_bad_reference(\n            name=name,\n            module_aliases=module_aliases,\n            type=Operator,\n        )\n"
  },
  {
    "path": "edb/schema/ordering.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    Sequence,\n    NamedTuple,\n    TYPE_CHECKING,\n)\n\nimport collections\n\nfrom edb import errors\nfrom edb.common import ordered\nfrom edb.common import topological\n\nfrom . import delta as sd\nfrom . import expraliases as s_expraliases\nfrom . import functions as s_func\nfrom . import indexes as s_indexes\nfrom . import inheriting\nfrom . import name as sn\nfrom . import objects as so\nfrom . import objtypes as s_objtypes\nfrom . import pointers as s_pointers\nfrom . import constraints as s_constraints\nfrom . import referencing\nfrom . import types as s_types\n\nif TYPE_CHECKING:\n    from . import schema as s_schema\n\n\nclass DepGraphEntryExtra(NamedTuple):\n    implicit_ancestors: list[sn.Name]\n\n\nDepGraphKey = tuple[str, str]\nDepGraphEntry = topological.DepGraphEntry[\n    DepGraphKey, tuple[sd.Command, ...], DepGraphEntryExtra,\n]\nDepGraph = dict[DepGraphKey, DepGraphEntry]\n\n\ndef linearize_delta(\n    delta: sd.DeltaRoot,\n    old_schema: Optional[s_schema.Schema],\n    new_schema: s_schema.Schema,\n) -> sd.DeltaRoot:\n    \"\"\"Reorder the *delta* tree in-place to satisfy command dependency order.\n\n    Args:\n        delta:\n            Input delta command tree.\n        old_schema:\n            Schema used to resolve original object state.\n        new_schema:\n            Schema used to resolve final schema state.\n\n    Returns:\n        Input delta tree reordered according to topological ordering of\n        commands.\n    \"\"\"\n\n    # We take the scatter-sort-gather approach here, where the original\n    # tree is broken up into linear branches, which are then sorted\n    # and reassembled back into a tree.\n\n    # A map of commands to root->command paths through the tree.\n    # Nodes are duplicated so the interior nodes of the path are\n    # distinct.\n    opmap: dict[sd.Command, list[sd.Command]] = {}\n    strongrefs: dict[sn.Name, sn.Name] = {}\n\n    for op in _get_sorted_subcommands(delta):\n        _break_down(opmap, strongrefs, [delta, op])\n\n    depgraph: DepGraph = {}\n    renames: dict[sn.Name, sn.Name] = {}\n    renames_r: dict[sn.Name, sn.Name] = {}\n    deletions: set[sn.Name] = set()\n\n    for op in opmap:\n        if isinstance(op, sd.RenameObject):\n            renames[op.classname] = op.new_name\n            renames_r[op.new_name] = op.classname\n        elif isinstance(op, sd.DeleteObject):\n            deletions.add(op.classname)\n\n    for op, opbranch in opmap.items():\n        if isinstance(op, sd.AlterObject) and not op.get_subcommands():\n            continue\n\n        _trace_op(op, opbranch, depgraph, renames,\n                  renames_r, strongrefs, old_schema, new_schema)\n\n    depgraph = dict(filter(lambda i: i[1].item != (), depgraph.items()))\n    everything = set(depgraph)\n    for item in depgraph.values():\n        item.deps &= everything\n        item.weak_deps &= everything\n\n    try:\n        sortedlist = [i[1] for i in topological.sort_ex(depgraph)]\n    except topological.CycleError as ex:\n        cycle = [depgraph[k].item for k in (ex.item,) + ex.path + (ex.item,)]\n        messages = [\n            '  ' + nodes[-1].get_friendly_description(parent_op=nodes[-2])\n            for nodes in cycle\n        ]\n        raise errors.SchemaDefinitionError(\n            'cannot produce migration because of a dependency cycle:\\n'\n            + ' depends on\\n'.join(messages)\n        ) from None\n    reconstructed = reconstruct_tree(sortedlist, depgraph)\n    delta.replace_all(reconstructed.get_subcommands())\n    return delta\n\n\ndef reconstruct_tree(\n    sortedlist: list[DepGraphEntry],\n    depgraph: DepGraph,\n) -> sd.DeltaRoot:\n\n    result = sd.DeltaRoot()\n    # Child to parent mapping.\n    parents: dict[sd.Command, sd.Command] = {}\n    # A mapping of commands to their dependencies.\n    dependencies: dict[sd.Command, set[sd.Command]] = (\n        collections.defaultdict(set))\n    # Current address of command within a tree in the form of\n    # a tuple of indexes where each index represents relative\n    # position within the tree rank.\n    offsets: dict[sd.Command, tuple[int, ...]] = {}\n    # Object commands indexed by command type and object name and\n    # implicitness, where each entry represents the latest seen\n    # command of the type for a particular object.  Implicit commands\n    # are included, but can only be attached to by other implicit\n    # commands.\n    opindex: dict[\n        tuple[type[sd.ObjectCommand[so.Object]], sn.Name, bool],\n        sd.ObjectCommand[so.Object]\n    ] = {}\n\n    def ok_to_attach_to(\n        op_to_attach: sd.Command,\n        op_to_attach_to: sd.ObjectCommand[so.Object],\n        only_if_confident: bool = False,\n    ) -> bool:\n        \"\"\"Determine if a given command can be attached to another.\n\n        Returns True, if *op_to_attach* can be attached to *op_to_attach_to*\n        without violating the dependency order.\n        \"\"\"\n        if only_if_confident and isinstance(op_to_attach, sd.ObjectCommand):\n            # Avoid reattaching the subcommand if confidence is below 100%,\n            # so that granular prompts can be generated.\n            confidence = op_to_attach.get_annotation('confidence')\n            if confidence is not None and confidence < 1.0:\n                return False\n        tgt_offset = offsets[op_to_attach_to]\n        tgt_offset_len = len(tgt_offset)\n        deps = dependencies[op_to_attach]\n        return all(offsets[dep][:tgt_offset_len] <= tgt_offset for dep in deps)\n\n    def attach(\n        opbranch: tuple[sd.Command, ...],\n        new_parent: sd.Command,\n        slice_start: int = 1,\n        as_implicit: bool = False,\n    ) -> None:\n        \"\"\"Attach a portion of a given command branch to another parent.\n\n        Args:\n            opbranch:\n                Command branch to attach to *new_parent*.\n            new_parent:\n                Command node to attach the specified portion of *opbranch* to.\n            slice_start:\n                Offset into *opbranch* that determines which commands\n                get attached.\n            as_implicit:\n                If True, the command branch is considered to be implicit,\n                i.e. it is not recorded in the command index.\n        \"\"\"\n        parent = opbranch[slice_start]\n        op = opbranch[-1]\n        offset_within_parent = new_parent.get_nonattr_subcommand_count()\n        if not isinstance(new_parent, sd.DeltaRoot):\n            parent_offset = offsets[new_parent] + (offset_within_parent,)\n        else:\n            parent_offset = (offset_within_parent,)\n        old_parent = parents[parent]\n        old_parent.discard(parent)\n        new_parent.add_caused(parent)\n        parents[parent] = new_parent\n\n        for i in range(slice_start, len(opbranch)):\n            op = opbranch[i]\n            if isinstance(op, sd.ObjectCommand):\n                ancestor_key = (type(op), op.classname, as_implicit)\n                opindex[ancestor_key] = op\n\n            if op in offsets:\n                op_offset = offsets[op][slice_start:]\n            else:\n                op_offset = (0,) * (i - slice_start)\n\n            offsets[op] = parent_offset + op_offset\n\n    def maybe_replace_preceding(\n        op: sd.Command,\n    ) -> bool:\n        \"\"\"Possibly merge and replace an earlier command with *op*.\n\n        If *op* is a DELETE command, or an ALTER command that has no\n        subcommands, and there is an earlier ALTER command operating\n        on the same object as *op*, merge that command into *op* and\n        replace it with *op*.\n\n        Returns:\n            True if merge and replace happened, False otherwise.\n        \"\"\"\n        if not (\n            isinstance(op, sd.DeleteObject)\n            or (\n                isinstance(op, sd.AlterObject)\n                and op.get_nonattr_subcommand_count() == 0\n            )\n        ):\n            return False\n\n        alter_cmd_cls = sd.get_object_command_class(\n            sd.AlterObject, op.get_schema_metaclass())\n\n        if alter_cmd_cls is None:\n            # ALTER isn't even defined for this object class, bail.\n            return False\n\n        alter_key = ((alter_cmd_cls), op.classname, False)\n        alter_op = opindex.get(alter_key)\n        if alter_op is None:\n            # No preceding ALTER, bail.\n            return False\n\n        if (\n            not ok_to_attach_to(op, alter_op)\n            or (\n                isinstance(parents[op], sd.DeltaRoot)\n                != isinstance(parents[alter_op], sd.DeltaRoot)\n            )\n            or bool(alter_op.get_subcommands(type=sd.RenameObject))\n        ):\n            return False\n\n        for alter_sub in reversed(alter_op.get_prerequisites()):\n            op.prepend_prerequisite(alter_sub)\n            parents[alter_sub] = op\n\n        for alter_sub in reversed(\n            alter_op.get_subcommands(include_prerequisites=False)\n        ):\n            op.prepend(alter_sub)\n            parents[alter_sub] = op\n\n        attached_root = parents[alter_op]\n        attached_root.replace(alter_op, op)\n        opindex[alter_key] = op\n        opindex[type(op), op.classname, False] = op\n        offsets[op] = offsets[alter_op]\n        parents[op] = attached_root\n\n        return True\n\n    def maybe_attach_to_preceding(\n        opbranch: tuple[sd.Command, ...],\n        parent_candidates: list[sn.Name],\n        allowed_op_types: list[type[sd.ObjectCommand[so.Object]]],\n        as_implicit: bool = False,\n        slice_start: int = 1,\n    ) -> bool:\n        \"\"\"Find a parent and attach a given portion of command branch to it.\n\n        Args:\n            opbranch:\n                Command branch to consider.\n            parent_candidates:\n                A list of parent object names to consider when looking for\n                a parent command.\n            allowed_op_types:\n                A list of command types to consider when looking for a\n                parent command.\n            as_implicit:\n                If True, the command branch is considered to be implicit,\n                i.e. it is not recorded in the command index.\n            slice_start:\n                Offset into *opbranch* that determines which commands\n                get attached.\n        \"\"\"\n\n        for candidate in parent_candidates:\n            for op_type in allowed_op_types:\n                parent_op = opindex.get((op_type, candidate, False))\n                # implicit ops are allowed to attach to other implicit\n                # ops. (Since we want them to chain properly in\n                # inheritance order.)\n                if parent_op is None and as_implicit:\n                    parent_op = opindex.get((op_type, candidate, True))\n\n                if (\n                    parent_op is not None\n                    and ok_to_attach_to(\n                        op,\n                        parent_op,\n                        only_if_confident=not as_implicit,\n                    )\n                ):\n                    attach(\n                        opbranch,\n                        parent_op,\n                        as_implicit=as_implicit,\n                        slice_start=slice_start,\n                    )\n                    return True\n\n        return False\n\n    # First, build parents and dependencies maps.\n    for info in sortedlist:\n        opbranch = info.item\n        op = opbranch[-1]\n        for j, pop in enumerate(opbranch[1:]):\n            parents[pop] = opbranch[j]\n        for dep in info.deps:\n            dep_item = depgraph[dep]\n            dep_stack = dep_item.item\n            dep_op = dep_stack[-1]\n            dependencies[op].add(dep_op)\n\n    for info in sortedlist:\n        opbranch = info.item\n        op = opbranch[-1]\n        # Elide empty ALTER statements from output.\n        if isinstance(op, sd.AlterObject) and not op.get_subcommands():\n            continue\n\n        # If applicable, replace a preceding command with this op.\n        if maybe_replace_preceding(op):\n            continue\n\n        if (\n            isinstance(op, sd.ObjectCommand)\n            and not isinstance(op, sd.CreateObject)\n            and info.extra is not None\n            and info.extra.implicit_ancestors\n        ):\n            # This command is deemed to be an implicit effect of another\n            # command, such as when alteration is propagated through the\n            # inheritance chain.  If so, find a command that operates on\n            # a parent object and attach this branch to it.\n            allowed_ops = [type(op)]\n            if isinstance(op, sd.DeleteObject):\n                allowed_ops.append(op.get_other_command_class(sd.DeleteObject))\n\n            if maybe_attach_to_preceding(\n                opbranch,\n                info.extra.implicit_ancestors,\n                allowed_ops,\n                as_implicit=True,\n            ):\n                continue\n\n        # Walking the branch toward root, see if there's a matching\n        # branch prefix we could attach to.\n        for depth, ancestor_op in enumerate(reversed(opbranch[1:-1])):\n            assert isinstance(ancestor_op, sd.ObjectCommand)\n\n            allowed_ops = []\n            create_cmd_t = ancestor_op.get_other_command_class(sd.CreateObject)\n            if type(ancestor_op) is not create_cmd_t:\n                allowed_ops.append(create_cmd_t)\n            allowed_ops.append(type(ancestor_op))\n\n            if maybe_attach_to_preceding(\n                opbranch,\n                [ancestor_op.classname],\n                allowed_ops,\n                slice_start=len(opbranch) - (depth + 1),\n            ):\n                break\n        else:\n            # No branches to attach to, so attach to root.\n            attach(opbranch, result)\n\n    return result\n\n\ndef _command_key(cmd: sd.Command) -> Any:\n    if isinstance(cmd, sd.ObjectCommand):\n        return (cmd.get_schema_metaclass().__name__, cmd.classname)\n    elif isinstance(cmd, sd.AlterObjectProperty):\n        return ('.field', cmd.property)\n    else:\n        return ('_generic', type(cmd).__name__)\n\n\ndef _get_sorted_subcommands(cmd: sd.Command) -> list[sd.Command]:\n    subcommands = list(cmd.get_subcommands())\n    subcommands.sort(key=_command_key)\n    return subcommands\n\n\ndef _break_down(\n    opmap: dict[sd.Command, list[sd.Command]],\n    strongrefs: dict[sn.Name, sn.Name],\n    opbranch: list[sd.Command],\n) -> None:\n    if len(opbranch) > 2:\n        new_opbranch = _extract_op(opbranch)\n    else:\n        new_opbranch = opbranch\n\n    op = new_opbranch[-1]\n\n    breakable_commands = (\n        referencing.ReferencedObjectCommand,\n        sd.RenameObject,\n        inheriting.RebaseInheritingObject,\n    )\n\n    for sub_op in _get_sorted_subcommands(op):\n        if (\n            isinstance(sub_op, sd.AlterObjectProperty)\n            and not isinstance(op, sd.DeleteObject)\n        ):\n            assert isinstance(op, sd.ObjectCommand)\n            mcls = op.get_schema_metaclass()\n            field = mcls.get_field(sub_op.property)\n            # Break a possible reference cycle\n            # (i.e. Type.rptr <-> Pointer.target)\n            if (\n                field.weak_ref\n                or (\n                    isinstance(op, sd.AlterObject)\n                    and issubclass(field.type, so.Object)\n                )\n            ):\n                _break_down(opmap, strongrefs, new_opbranch + [sub_op])\n        elif (\n            isinstance(sub_op, sd.AlterSpecialObjectField)\n            and not isinstance(\n                sub_op,\n                (\n                    referencing.AlterOwned,\n                    s_pointers.SetPointerType,\n                )\n            )\n        ):\n            pass\n        elif (\n            isinstance(sub_op, referencing.ReferencedObjectCommandBase)\n            and sub_op.is_strong_ref\n        ):\n            assert isinstance(op, sd.ObjectCommand)\n            strongrefs[sub_op.classname] = op.classname\n        elif isinstance(sub_op, breakable_commands):\n            _break_down(opmap, strongrefs, new_opbranch + [sub_op])\n\n    # For SET TYPE and friends, we need to make sure that an alter\n    # (with children) makes it into the opmap so it is processed.\n    if (\n        isinstance(op, sd.AlterSpecialObjectField)\n        and not isinstance(op, referencing.AlterOwned)\n    ):\n        opmap[new_opbranch[-2]] = new_opbranch[:-1]\n\n    opmap[op] = new_opbranch\n\n\ndef _trace_op(\n    op: sd.Command,\n    opbranch: list[sd.Command],\n    depgraph: DepGraph,\n    renames: dict[sn.Name, sn.Name],\n    renames_r: dict[sn.Name, sn.Name],\n    strongrefs: dict[sn.Name, sn.Name],\n    old_schema: Optional[s_schema.Schema],\n    new_schema: s_schema.Schema,\n) -> None:\n    def get_deps(key: DepGraphKey) -> DepGraphEntry:\n        try:\n            item = depgraph[key]\n        except KeyError:\n            item = depgraph[key] = DepGraphEntry(\n                item=(),\n                deps=ordered.OrderedSet(),\n                weak_deps=ordered.OrderedSet(),\n            )\n        return item\n\n    def record_field_deps(\n        op: sd.AlterObjectProperty,\n        parent_op: sd.ObjectCommand[so.Object],\n    ) -> str:\n        nvn = None\n        if isinstance(op.new_value, (so.Object, so.ObjectShell)):\n            obj = op.new_value\n            nvn = obj.get_name(new_schema)\n            if nvn is not None:\n                deps.add(('create', str(nvn)))\n                deps.add(('alter', str(nvn)))\n                if nvn in renames_r:\n                    deps.add(('rename', str(renames_r[nvn])))\n\n            if isinstance(obj, so.ObjectShell):\n                obj = obj.resolve(new_schema)\n            # For SET TYPE, we want to finish any rebasing into the\n            # target type before we change the type.\n            if isinstance(obj, so.InheritingObject):\n                for desc in obj.descendants(new_schema):\n                    deps.add(('rebase', str(desc.get_name(new_schema))))\n\n        graph_key = f'{parent_op.classname}%%{op.property}'\n        deps.add(('create', str(parent_op.classname)))\n        deps.add(('alter', str(parent_op.classname)))\n\n        if isinstance(op.old_value, (so.Object, so.ObjectShell)):\n            assert old_schema is not None\n            ovn = op.old_value.get_name(old_schema)\n            if ovn != nvn:\n                ov_item = get_deps(('delete', str(ovn)))\n                ov_item.deps.add((tag, graph_key))\n\n        return graph_key\n\n    def write_dep_matrix(\n        dependent: str,\n        dependent_tags: tuple[str, ...],\n        dependency: str,\n        dependency_tags: tuple[str, ...],\n        *,\n        as_weak: bool = False,\n    ) -> None:\n        for dependent_tag in dependent_tags:\n            item = get_deps((dependent_tag, dependent))\n            for dependency_tag in dependency_tags:\n                if as_weak:\n                    item.weak_deps.add((dependency_tag, dependency))\n                else:\n                    item.deps.add((dependency_tag, dependency))\n\n    def write_ref_deps(\n        ref: so.Object,\n        obj: so.Object,\n        this_name_str: str,\n    ) -> None:\n        ref_name = ref.get_name(new_schema)\n        if ref_name in renames_r:\n            ref_name = renames_r[ref_name]\n        ref_name_str = str(ref_name)\n\n        if ((isinstance(ref, referencing.ReferencedObject)\n                and ref.get_referrer(new_schema) == obj)\n                or (isinstance(obj, referencing.ReferencedObject)\n                    and obj.get_referrer(new_schema) == ref)):\n            # Mostly ignore refs generated by refdict backref, but\n            # make create/alter depend on renames of the backref.\n            # This makes sure that a rename is done before the innards are\n            # modified. DDL doesn't actually require this but some of the\n            # internals for producing the DDL do (since otherwise we can\n            # generate references to the renamed type in our delta before\n            # it is renamed).\n            if tag in ('create', 'alter'):\n                deps.add(('rename', ref_name_str))\n\n            return\n\n        write_dep_matrix(\n            dependent=ref_name_str,\n            dependent_tags=('create', 'alter', 'rebase'),\n            dependency=this_name_str,\n            dependency_tags=('create', 'alter', 'rename'),\n        )\n\n        item = get_deps(('rename', ref_name_str))\n        item.deps.add(('create', this_name_str))\n        item.deps.add(('alter', this_name_str))\n        item.deps.add(('rename', this_name_str))\n\n        if isinstance(ref, s_pointers.Pointer):\n            # The current item is a type referred to by\n            # a link or property in another type.  Set the referring\n            # type and its descendants as weak dependents of the current\n            # item to reduce the number of unnecessary ALTERs in the\n            # final delta, especially ones that might result in SET TYPE\n            # commands being generated.\n            ref_src = ref.get_source(new_schema)\n            if isinstance(ref_src, s_pointers.Pointer):\n                ref_src_src = ref_src.get_source(new_schema)\n                if ref_src_src is not None:\n                    ref_src = ref_src_src\n            if ref_src is not None:\n                for desc in ref_src.descendants(new_schema) | {ref_src}:\n                    desc_name = str(desc.get_name(new_schema))\n\n                    write_dep_matrix(\n                        dependent=desc_name,\n                        dependent_tags=('create', 'alter'),\n                        dependency=this_name_str,\n                        dependency_tags=('create', 'alter', 'rename'),\n                        as_weak=True,\n                    )\n\n    deps: ordered.OrderedSet[tuple[str, str]] = ordered.OrderedSet()\n    graph_key: str\n    implicit_ancestors: list[sn.Name] = []\n\n    if isinstance(op, sd.CreateObject):\n        tag = 'create'\n    elif isinstance(op, sd.AlterObject):\n        tag = 'alter'\n    elif isinstance(op, sd.RenameObject):\n        tag = 'rename'\n    elif isinstance(op, inheriting.RebaseInheritingObject):\n        tag = 'rebase'\n    elif isinstance(op, sd.DeleteObject):\n        tag = 'delete'\n    elif isinstance(op, referencing.AlterOwned):\n        if op.get_attribute_value('owned'):\n            tag = 'setowned'\n        else:\n            tag = 'dropowned'\n    elif isinstance(op, (sd.AlterObjectProperty, sd.AlterSpecialObjectField)):\n        tag = 'field'\n    else:\n        raise RuntimeError(\n            f'unexpected delta command type at top level: {op!r}'\n        )\n\n    if isinstance(op, (sd.DeleteObject, referencing.AlterOwned)):\n        assert old_schema is not None\n        try:\n            obj = get_object(old_schema, op)\n        except errors.InvalidReferenceError:\n            if isinstance(op, sd.DeleteObject) and op.if_exists:\n                # If this is conditional deletion and the object isn't there,\n                # then don't bother with analysis, since this command wouldn't\n                # get executed.\n                return\n            else:\n                raise\n        refs = _get_referrers(old_schema, obj, strongrefs)\n        for ref in refs:\n            ref_name_str = str(ref.get_name(old_schema))\n            if (\n                (\n                    isinstance(obj, referencing.ReferencedObject)\n                    and obj.get_referrer(old_schema) == ref\n                )\n            ):\n                # If the referrer is enclosing the object\n                # (i.e. the reference is a refdict reference),\n                # we sort the enclosed operation first.\n                ref_item = get_deps(('delete', ref_name_str))\n                ref_item.deps.add((tag, str(op.classname)))\n\n            elif (\n                isinstance(ref, referencing.ReferencedInheritingObject)\n                and (\n                    op.classname\n                    in {\n                        b.get_name(old_schema)\n                        for b in ref.get_implicit_ancestors(old_schema)\n                    }\n                )\n                and (\n                    not isinstance(ref, s_pointers.Pointer)\n                    or not ref.get_from_alias(old_schema)\n                )\n            ):\n                # If the ref is an implicit descendant (i.e. an inherited ref),\n                # we also sort it _after_ the parent, because we'll pull\n                # it as a child of the parent op at the time of tree\n                # reassembly.\n                ref_item = get_deps(('delete', ref_name_str))\n                ref_item.deps.add((tag, str(op.classname)))\n\n            elif (\n                isinstance(ref, referencing.ReferencedObject)\n                and ref.get_referrer(old_schema) == obj\n            ):\n                # Skip refdict.backref_attr to avoid dependency cycles.\n                continue\n\n            else:\n                # Otherwise, things must be deleted _after_ their referrers\n                # have been deleted or altered.\n                deps.add(('delete', ref_name_str))\n                # (except for aliases, which in the collection case\n                # specifically need the old target deleted before the\n                # new one is created)\n                if not isinstance(ref, s_expraliases.Alias):\n                    deps.add(('alter', ref_name_str))\n                if type(ref) is type(obj):\n                    deps.add(('rebase', ref_name_str))\n\n                # The deletion of any implicit ancestors needs to come after\n                # the deletion of any referrers also.\n                if isinstance(obj, referencing.ReferencedInheritingObject):\n                    for ancestor in obj.get_implicit_ancestors(old_schema):\n                        ancestor_name = ancestor.get_name(old_schema)\n\n                        anc_item = get_deps(('delete', str(ancestor_name)))\n                        anc_item.deps.add(('delete', ref_name_str))\n\n        if isinstance(obj, referencing.ReferencedObject):\n            if tag == 'delete':\n                # If the object is being deleted and then recreated\n                # via inheritance, that deletion needs to come before\n                # an ancestor gets created (since that will cause our\n                # recreation.)\n                try:\n                    new_obj = get_object(new_schema, op)\n                except errors.InvalidReferenceError:\n                    new_obj = None\n                if isinstance(new_obj, referencing.ReferencedInheritingObject):\n                    for ancestor in new_obj.get_implicit_ancestors(new_schema):\n                        rep_item = get_deps(\n                            ('create', str(ancestor.get_name(new_schema))))\n                        rep_item.deps.add((tag, str(op.classname)))\n\n            referrer = obj.get_referrer(old_schema)\n            if referrer is not None:\n                assert isinstance(referrer, so.QualifiedObject)\n                referrer_name: sn.Name = referrer.get_name(old_schema)\n                if referrer_name in renames_r:\n                    referrer_name = renames_r[referrer_name]\n\n                # A drop needs to come *before* drop owned on the referrer\n                # which will do it itself.\n                if tag == 'delete':\n                    ref_item = get_deps(('dropowned', str(referrer_name)))\n                    ref_item.deps.add(('delete', str(op.classname)))\n\n                # For SET OWNED, we need any rebase of the enclosing\n                # object to come *after*, because otherwise obj could\n                # get dropped before the SET OWNED takes effect.\n                # DROP, also.\n                if tag in ('setowned', 'delete'):\n                    ref_item = get_deps(('rebase', str(referrer_name)))\n                    ref_item.deps.add((tag, str(op.classname)))\n                else:\n                    deps.add(('rebase', str(referrer_name)))\n\n                if (\n                    isinstance(obj, referencing.ReferencedInheritingObject)\n                    and (\n                        not isinstance(obj, s_pointers.Pointer)\n                        or not obj.get_from_alias(old_schema)\n                    )\n                ):\n                    for ancestor in obj.get_implicit_ancestors(old_schema):\n                        ancestor_name = ancestor.get_name(old_schema)\n                        implicit_ancestors.append(ancestor_name)\n\n                        if isinstance(op, referencing.AlterOwned):\n                            anc_item = get_deps(('delete', str(ancestor_name)))\n                            anc_item.deps.add((tag, str(op.classname)))\n\n                        if tag == 'setowned':\n                            # SET OWNED must come before ancestor rebases too\n                            anc_item = get_deps(('rebase', str(ancestor_name)))\n                            anc_item.deps.add(('setowned', str(op.classname)))\n\n        if tag == 'dropowned':\n            deps.add(('alter', str(op.classname)))\n        graph_key = str(op.classname)\n\n    elif isinstance(op, sd.AlterObjectProperty):\n        parent_op = opbranch[-2]\n        assert isinstance(parent_op, sd.ObjectCommand)\n        graph_key = record_field_deps(op, parent_op)\n\n    elif isinstance(op, sd.AlterSpecialObjectField):\n        parent_op = opbranch[-2]\n        assert isinstance(parent_op, sd.ObjectCommand)\n        field_op = op._get_attribute_set_cmd(op._field)\n        assert field_op is not None\n        graph_key = record_field_deps(field_op, parent_op)\n\n    elif isinstance(op, sd.ObjectCommand):\n        # If the object was renamed, use the new name, else use regular.\n        name = renames.get(op.classname, op.classname)\n        obj = get_object(new_schema, op, name)\n        this_name_str = str(op.classname)\n\n        if tag == 'rename':\n            # On renames, we want to delete any references before we\n            # do the rename. This is because for functions and\n            # constraints we implicitly rename the object when\n            # something it references is renamed, and this implicit\n            # rename can interfere with a CREATE/DELETE pair.  So we\n            # make sure to put the DELETE before the RENAME of a\n            # referenced object. (An improvement would be to elide a\n            # CREATE/DELETE pair when it could be implicitly handled\n            # by a rename).\n            assert old_schema\n            old_obj = get_object(old_schema, op, op.classname)\n            for ref in _get_referrers(old_schema, old_obj, strongrefs):\n                deps.add(('delete', str(ref.get_name(old_schema))))\n\n        refs = _get_referrers(new_schema, obj, strongrefs)\n        for ref in refs:\n            write_ref_deps(ref, obj, this_name_str)\n\n        if tag == 'create':\n            # In a delete/create cycle, deletion must obviously\n            # happen first.\n            deps.add(('delete', this_name_str))\n            # Renaming also\n            deps.add(('rename', this_name_str))\n\n            if isinstance(obj, s_func.Function) and old_schema is not None:\n                old_funcs = old_schema._get_by_shortname(\n                    s_func.Function,\n                    sn.shortname_from_fullname(op.classname),\n                ) or ()\n                for old_func in old_funcs:\n                    deps.add(('delete', str(old_func.get_name(old_schema))))\n\n            # Some index types only allow one per object type. Make\n            # sure we drop the old one before creating the new.\n            if (\n                isinstance(obj, s_indexes.Index)\n                and s_indexes.is_exclusive_object_scope_index(new_schema, obj)\n                and old_schema is not None\n                and (subject := obj.get_subject(new_schema))\n                and (old_subject := old_schema.get(\n                    subject.get_name(new_schema),\n                    type=s_objtypes.ObjectType,\n                    default=None\n                ))\n                and (eff_index := s_indexes.get_effective_object_index(\n                    old_schema,\n                    old_subject,\n                    obj.get_root(new_schema).get_name(new_schema),\n                )[0])\n            ):\n                deps.add(('delete', str(eff_index.get_name(old_schema))))\n\n        if tag == 'alter':\n            # Alteration must happen after creation, if any.\n            deps.add(('create', this_name_str))\n            deps.add(('rename', this_name_str))\n            deps.add(('rebase', this_name_str))\n\n        if isinstance(obj, referencing.ReferencedObject):\n            referrer = obj.get_referrer(new_schema)\n            if referrer is not None:\n                assert isinstance(referrer, so.QualifiedObject)\n                referrer_name = referrer.get_name(new_schema)\n                if referrer_name in renames_r:\n                    referrer_name = renames_r[referrer_name]\n                ref_name_str = str(referrer_name)\n                deps.add(('create', ref_name_str))\n                if op.ast_ignore_ownership() or tag == 'rename':\n                    ref_item = get_deps(('rebase', ref_name_str))\n                    ref_item.deps.add((tag, this_name_str))\n                else:\n                    deps.add(('rebase', ref_name_str))\n\n                # Addition and removal of constraints can cause\n                # changes to the cardinality of expressions that refer\n                # to them. Add the appropriate dependencies in.\n                if (\n                    isinstance(obj, s_constraints.Constraint)\n                    and isinstance(referrer, s_pointers.Pointer)\n                ):\n                    refs = _get_referrers(new_schema, referrer, strongrefs)\n                    for ref in refs:\n                        write_ref_deps(ref, referrer, this_name_str)\n\n                if (\n                    isinstance(obj, referencing.ReferencedInheritingObject)\n                    # Changes to owned objects can't necessarily be merged\n                    # in with parents, so we make sure not to.\n                    and not obj.get_owned(new_schema)\n                ):\n                    implicit_ancestors = [\n                        b.get_name(new_schema)\n                        for b in obj.get_implicit_ancestors(new_schema)\n                    ]\n\n                    if not isinstance(op, sd.CreateObject):\n                        assert old_schema is not None\n                        name = renames_r.get(op.classname, op.classname)\n                        old_obj = get_object(old_schema, op, name)\n                        assert isinstance(\n                            old_obj,\n                            referencing.ReferencedInheritingObject,\n                        )\n                        implicit_ancestors += [\n                            b.get_name(old_schema)\n                            for b in old_obj.get_implicit_ancestors(old_schema)\n                        ]\n\n        graph_key = this_name_str\n\n    else:\n        raise AssertionError(f'unexpected op type: {op!r}')\n\n    item = get_deps((tag, graph_key))\n\n    item.item = tuple(opbranch)\n    item.deps |= deps\n    item.extra = DepGraphEntryExtra(\n        implicit_ancestors=[renames_r.get(a, a) for a in implicit_ancestors],\n    )\n\n\ndef get_object(\n    schema: s_schema.Schema,\n    op: sd.ObjectCommand[so.Object],\n    name: Optional[sn.Name] = None,\n) -> so.Object:\n    metaclass = op.get_schema_metaclass()\n    if name is None:\n        name = op.classname\n\n    if issubclass(metaclass, s_types.Collection):\n        if isinstance(name, sn.QualName):\n            return schema.get(name)\n        else:\n            return schema.get_global(metaclass, name)\n    elif not issubclass(metaclass, so.QualifiedObject):\n        obj = schema.get_global(metaclass, name)\n        assert isinstance(obj, so.Object)\n        return obj\n    else:\n        return schema.get(name)\n\n\ndef _get_referrers(\n    schema: s_schema.Schema,\n    obj: so.Object,\n    strongrefs: dict[sn.Name, sn.Name],\n) -> list[so.Object]:\n    refs = schema.get_referrers(obj)\n    result: set[so.Object] = set()\n\n    for ref in refs:\n        if not ref.is_blocking_ref(schema, obj):\n            continue\n\n        referrer: so.Object | None = None\n\n        parent_ref = strongrefs.get(ref.get_name(schema))\n        if parent_ref is not None:\n            referrer = schema.get(parent_ref, default=None)\n\n        if not referrer or obj == referrer:\n            referrer = ref\n\n        result.add(referrer)\n\n    return list(sorted(\n        result,\n        key=lambda o: (type(o).__name__, o.get_name(schema)),\n    ))\n\n\ndef _extract_op(stack: Sequence[sd.Command]) -> list[sd.Command]:\n    parent_op = stack[0]\n    new_stack = [parent_op]\n\n    for stack_op in stack[1:-1]:\n        assert isinstance(stack_op, sd.ObjectCommand)\n        alter_class = stack_op.get_other_command_class(sd.AlterObject)\n        alter_delta = alter_class(\n            classname=stack_op.classname,\n            ddl_identity=stack_op.ddl_identity,\n            aux_object_data=stack_op.aux_object_data,\n            annotations=stack_op.annotations,\n            canonical=stack_op.canonical,\n            orig_cmd_type=type(stack_op),\n        )\n        parent_op.add(alter_delta)\n        parent_op = alter_delta\n        new_stack.append(parent_op)\n\n    stack[-2].discard(stack[-1])\n    parent_op.add(stack[-1])\n    new_stack.append(stack[-1])\n\n    return new_stack\n"
  },
  {
    "path": "edb/schema/permissions.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom . import annos as s_anno\nfrom . import delta as sd\nfrom . import objects as so\n\n\nclass Permission(\n    so.QualifiedObject,\n    s_anno.AnnotationSubject,\n    qlkind=qltypes.SchemaObjectClass.PERMISSION,\n    data_safe=True,\n):\n    pass\n\n\nclass PermissionCommandContext(\n    sd.ObjectCommandContext[Permission],\n    s_anno.AnnotationSubjectCommandContext,\n):\n    pass\n\n\nclass PermissionCommand(\n    sd.QualifiedObjectCommand[Permission],\n    s_anno.AnnotationSubjectCommand[Permission],\n    context_class=PermissionCommandContext,\n):\n    pass\n\n\nclass CreatePermission(\n    PermissionCommand,\n    sd.CreateObject[Permission],\n):\n    astnode = qlast.CreatePermission\n\n\nclass AlterPermission(\n    PermissionCommand,\n    sd.AlterObject[Permission],\n):\n    astnode = qlast.AlterPermission\n\n\nclass DeletePermission(\n    PermissionCommand,\n    sd.DeleteObject[Permission],\n):\n    astnode = qlast.DropPermission\n\n\nclass RenamePermission(\n    PermissionCommand,\n    sd.RenameObject[Permission],\n):\n    pass\n"
  },
  {
    "path": "edb/schema/pointers.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    cast,\n    Iterable,\n    Optional,\n    Self,\n    Sequence,\n    TYPE_CHECKING,\n)\n\nimport abc\nimport collections.abc\nimport enum\nimport json\nimport operator\nimport dataclasses\n\nfrom edb import errors\n\nfrom edb.common import enum as s_enum\nfrom edb.common import struct\nfrom edb.common import parsing\nfrom edb.common import ast\nfrom edb.common.typeutils import not_none\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.edgeql import qltypes\nfrom edb.edgeql import quote as qlquote\n\nfrom . import annos as s_anno\nfrom . import constraints\nfrom . import delta as sd\nfrom . import expr as s_expr\nfrom . import expraliases as s_expraliases\nfrom . import futures as s_futures\nfrom . import inheriting\nfrom . import name as sn\nfrom . import objects as so\nfrom . import referencing\nfrom . import rewrites as s_rewrites\nfrom . import schema as s_schema\nfrom . import types as s_types\nfrom . import scalars as s_scalars\nfrom . import utils\n\n\nif TYPE_CHECKING:\n    from . import objtypes as s_objtypes\n    from . import sources as s_sources\n    from edb.ir import ast as irast\n\n\nclass PointerDirection(s_enum.StrEnum):\n    Outbound = '>'\n    Inbound = '<'\n\n\nclass LineageStatus(enum.Enum):\n    VALID = 0\n    MULTIPLE_COMPUTABLES = 1\n    MIXED = 2\n\n\ndef merge_cardinality(\n    ptr: Pointer,\n    bases: list[Pointer],\n    field_name: str,\n    *,\n    ignore_local: bool,\n    schema: s_schema.Schema,\n) -> Any:\n    current: Optional[qltypes.SchemaCardinality] = None\n    current_from = None\n\n    if not ignore_local:\n        current = ptr.get_explicit_field_value(schema, field_name, None)\n        if current is not None:\n            current_from = ptr\n\n    for base in bases:\n        # ignore abstract pointers\n        if base.is_non_concrete(schema):\n            continue\n\n        nextval: Optional[qltypes.SchemaCardinality] = (\n            base.get_field_value(schema, field_name))\n        if nextval is None:\n            continue\n\n        if current is None:\n            current = nextval\n            current_from = base\n        elif not current.is_known() and nextval is not None:\n            current = nextval\n            current_from = base\n        elif current is not nextval:\n            tgt_repr = ptr.get_verbosename(schema, with_parent=True)\n            assert current_from is not None\n            cf_repr = current_from.get_verbosename(schema, with_parent=True)\n            other_repr = base.get_verbosename(schema, with_parent=True)\n\n            if current.is_known():\n                current_qual = f'defined as {current.as_ptr_qual()!r}'\n            else:\n                current_qual = 'unknown'\n\n            if nextval.is_known():\n                nextval_qual = f'defined as {nextval.as_ptr_qual()!r}'\n            else:\n                nextval_qual = 'unknown'\n\n            raise errors.SchemaDefinitionError(\n                f'cannot redefine the cardinality of '\n                f'{tgt_repr}: it is {current_qual} in {cf_repr} and '\n                f'is {nextval_qual} in {other_repr}.'\n            )\n\n    return current\n\n\ndef merge_readonly(\n    target: Pointer,\n    sources: list[Pointer],\n    field_name: str,\n    *,\n    ignore_local: bool,\n    schema: s_schema.Schema,\n) -> Any:\n\n    current = None\n    current_from = None\n\n    # The target field value is only relevant if it is explicit,\n    # otherwise it should be based on the inherited value.\n    if not ignore_local:\n        current = target.get_explicit_field_value(schema, field_name, None)\n        if current is not None:\n            current_from = target\n\n    for source in list(sources):\n        # ignore abstract pointers\n        if source.is_non_concrete(schema):\n            continue\n\n        # We want the field value including the default, not just\n        # explicit value.\n        nextval = source.get_field_value(schema, field_name)\n        if nextval is not None:\n            if current is None:\n                current = nextval\n                current_from = source\n            elif current is not nextval:\n                assert current_from is not None\n\n                tgt_repr = target.get_verbosename(\n                    schema, with_parent=True)\n                cf_repr = current_from.get_verbosename(\n                    schema, with_parent=True)\n                other_repr = source.get_verbosename(\n                    schema, with_parent=True)\n\n                raise errors.SchemaDefinitionError(\n                    f'cannot redefine the readonly flag of '\n                    f'{tgt_repr}: it is defined '\n                    f'as {current} in {cf_repr} and '\n                    f'as {nextval} in {other_repr}.'\n                )\n\n    return current\n\n\ndef merge_required(\n    ptr: Pointer,\n    bases: list[Pointer],\n    field_name: str,\n    *,\n    ignore_local: bool = False,\n    schema: s_schema.Schema,\n) -> Optional[bool]:\n    \"\"\"Merge function for the REQUIRED qualifier on links and properties.\"\"\"\n\n    local_required = ptr.get_explicit_local_field_value(\n        schema, field_name, None)\n\n    if ignore_local or local_required is None:\n        # No explicit local declaration, so True if any of the bases\n        # have it as required, and False otherwise.\n        return utils.merge_reduce(\n            ptr,\n            bases,\n            field_name=field_name,\n            ignore_local=ignore_local,\n            schema=schema,\n            f=operator.or_,\n            type=bool,\n        )\n    elif local_required:\n        # If set locally and True, just use that.\n        assert isinstance(local_required, bool)\n        return local_required\n    else:\n        # Explicitly set locally as False, check if any of the bases\n        # are REQUIRED, and if so, raise.\n        for base in bases:\n            base_required = base.get_field_value(schema, field_name)\n            if base_required:\n                ptr_repr = ptr.get_verbosename(schema, with_parent=True)\n                base_repr = base.get_verbosename(schema, with_parent=True)\n                raise errors.SchemaDefinitionError(\n                    f'cannot make {ptr_repr} optional: its parent {base_repr} '\n                    f'is defined as required'\n                )\n\n        return False\n\n\ndef merge_target(\n    ptr: Pointer,\n    bases: list[Pointer],\n    field_name: str,\n    *,\n    ignore_local: bool = False,\n    schema: s_schema.Schema,\n) -> Optional[s_types.Type]:\n\n    target = None\n    current_source = None\n\n    for base in bases:\n        base_target = base.get_target(schema)\n        if base_target is None:\n            continue\n\n        if target is None:\n            target = base_target\n            current_source = base.get_source(schema)\n        else:\n            assert current_source is not None\n            source = base.get_source(schema)\n            assert source is not None\n            schema, target = _merge_types(\n                schema,\n                ptr,\n                target,\n                base_target,\n                t1_source=current_source,\n                t2_source=source,\n                allow_contravariant=True,\n            )\n\n    if not ignore_local:\n        local_target = ptr.get_target(schema)\n        if target is None:\n            target = local_target\n        elif local_target is not None:\n            assert current_source is not None\n            schema, target = _merge_types(\n                schema,\n                ptr,\n                target,\n                local_target,\n                t1_source=current_source,\n                t2_source=None,\n            )\n\n    return target\n\n\ndef _merge_types(\n    schema: s_schema.Schema,\n    ptr: Pointer,\n    t1: s_types.Type,\n    t2: s_types.Type,\n    *,\n    t1_source: so.Object,\n    t2_source: Optional[so.Object],\n    allow_contravariant: bool = False,\n) -> tuple[s_schema.Schema, Optional[s_types.Type]]:\n    if t1 == t2:\n        return schema, t1\n\n    # When two pointers are merged, check target compatibility\n    # and return a target that satisfies both specified targets.\n    elif isinstance(t1, s_scalars.ScalarType) != isinstance(\n        t2, s_scalars.ScalarType\n    ):\n        # Mixing a property with a link.\n        vnp = ptr.get_verbosename(schema, with_parent=True)\n        vn = ptr.get_verbosename(schema)\n        t1_vn = t1.get_verbosename(schema)\n        t2_vn = t2.get_verbosename(schema)\n        t1_cls = 'property' if isinstance(t1, s_scalars.ScalarType) else 'link'\n        t2_cls = 'property' if isinstance(t2, s_scalars.ScalarType) else 'link'\n\n        t1_source_vn = t1_source.get_verbosename(schema, with_parent=True)\n        if t2_source is None:\n            raise errors.SchemaError(\n                f'cannot redefine {vnp} as {t2_vn}',\n                details=(\n                    f'{vn} is defined as a {t1_cls} to {t1_vn} in'\n                    f' parent {t1_source_vn}'\n                ),\n            )\n        else:\n            t2_source_vn = t2_source.get_verbosename(schema, with_parent=True)\n            raise errors.SchemaError(\n                f'inherited {vnp} has a type conflict',\n                details=(\n                    f'{vn} is defined as a {t1_cls} to {t1_vn} in'\n                    f' parent {t1_source_vn} and as {t2_cls} in'\n                    f' parent {t2_source_vn}'\n                ),\n            )\n\n    else:\n        assert isinstance(t1, so.SubclassableObject)\n        assert isinstance(t2, so.SubclassableObject)\n\n        if t2.issubclass(schema, t1):\n            # The new target is a subclass of the current target, so\n            # it is a more specific requirement.\n            current_target = t2\n        elif allow_contravariant and t1.issubclass(schema, t2):\n            current_target = t1\n        else:\n            # The new target is not a subclass, of the previously seen\n            # targets, which creates an unresolvable target requirement\n            # conflict.\n            vnp = ptr.get_verbosename(schema, with_parent=True)\n            vn = ptr.get_verbosename(schema)\n            t1_vn = t1.get_verbosename(schema)\n            t2_vn = t2.get_verbosename(schema)\n\n            t1_source_vn = t1_source.get_verbosename(schema, with_parent=True)\n            if t2_source is None:\n                raise errors.SchemaError(\n                    f'cannot redefine {vnp} as {t2_vn}',\n                    details=(\n                        f'{vn} is defined as {t1_vn} in'\n                        f' parent {t1_source_vn}'\n                    ),\n                )\n            else:\n                t2_source_vn = t2_source.get_verbosename(\n                    schema, with_parent=True)\n                raise errors.SchemaError(\n                    f'inherited {vnp} has a type conflict',\n                    details=(\n                        f'{vn} is defined as {t1_vn} in'\n                        f' parent {t1_source_vn} and as {t2_vn} in'\n                        f' parent {t2_source_vn}'\n                    ),\n                )\n\n        return schema, current_target\n\n\ndef get_root_source(\n    obj: Optional[so.Object], schema: s_schema.Schema\n) -> Optional[so.Object]:\n    while isinstance(obj, Pointer):\n        obj = obj.get_source(schema)\n    return obj\n\n\ndef is_view_source(\n    source: Optional[so.Object], schema: s_schema.Schema\n) -> bool:\n    source = get_root_source(source, schema)\n    return isinstance(source, s_types.Type) and source.is_view(schema)\n\n\ndef _get_target_name_in_diff(\n    *,\n    schema: s_schema.Schema,\n    orig_schema: Optional[s_schema.Schema],\n    object: Optional[so.Object],\n    orig_object: Optional[so.Object],\n) -> sn.Name:\n    \"\"\"Compute the target type name for a fill/conv expr\n\n    Called from record_diff_annotations to produce annotation\n    information for migrations.\n    The trickiness here is that this information is generated\n    when producing the diff, where we have somewhat limited\n    information.\n    \"\"\"\n    # Prefer getting the target type from the original object instead\n    # of the new one, for a cheesy reason: if we change both\n    # required/cardinality and target type, we do the cardinality\n    # change before the cast, for reasons of alphabetical order.\n    if isinstance(orig_object, Pointer):\n        assert orig_schema\n        target = orig_object.get_target(orig_schema)\n        return not_none(target).get_name(orig_schema)\n    else:\n        assert isinstance(object, Pointer)\n        target = object.get_target(schema)\n        return not_none(target).get_name(schema)\n\n\nclass Pointer(\n    referencing.NamedReferencedInheritingObject,\n    constraints.ConsistencySubject,\n    s_anno.AnnotationSubject,\n):\n\n    source = so.SchemaField(\n        so.InheritingObject,\n        default=None, compcoef=None,\n        inheritable=False)\n\n    target = so.SchemaField(\n        s_types.Type,\n        merge_fn=merge_target,\n        default=None,\n        compcoef=0.85,\n        special_ddl_syntax=True,\n    )\n\n    required = so.SchemaField(\n        bool,\n        default=False,\n        compcoef=0.909,\n        special_ddl_syntax=True,\n        describe_visibility=(\n            so.DescribeVisibilityPolicy.SHOW_IF_EXPLICIT_OR_DERIVED\n        ),\n        merge_fn=merge_required,\n    )\n\n    readonly = so.SchemaField(\n        bool,\n        allow_ddl_set=True,\n        describe_visibility=(\n            so.DescribeVisibilityPolicy.SHOW_IF_EXPLICIT_OR_DERIVED_NOT_DEFAULT\n        ),\n        default=False,\n        compcoef=0.909,\n        merge_fn=merge_readonly,\n    )\n\n    splat_strategy = so.SchemaField(\n        qltypes.SplatStrategy,\n        allow_ddl_set=True,\n        describe_visibility=(\n            so.DescribeVisibilityPolicy.SHOW_IF_EXPLICIT_OR_DERIVED_NOT_DEFAULT\n        ),\n        coerce=True,\n        default=qltypes.SplatStrategy.Default,\n        compcoef=0.909,\n    )\n\n    secret = so.SchemaField(\n        bool,\n        default=False,\n        compcoef=0.909,\n    )\n\n    protected = so.SchemaField(\n        bool,\n        default=False,\n        compcoef=0.909,\n    )\n\n    linkful = so.SchemaField(\n        bool,\n        default=False,\n        compcoef=0.99,\n        inheritable=False,\n    )\n\n    # For non-derived pointers this is strongly correlated with\n    # \"expr\" below.  Derived pointers might have \"computable\" set,\n    # but expr=None.\n    computable = so.SchemaField(\n        bool,\n        default=False,\n        compcoef=0.99,\n    )\n\n    # True, if this pointer is defined in an Alias.\n    from_alias = so.SchemaField(\n        bool,\n        default=None,\n        compcoef=0.99,\n        # This value needs to be recorded in the delta commands\n        # to signal that we don't want to render this command in DDL.\n        aux_cmd_data=True,\n    )\n\n    # Is this pointer a \"definition site\" of some kind or just a\n    # trivial inheritor. Used to determine whether to use this pointer\n    # or a parent when computing path ids.\n    defined_here = so.SchemaField(\n        bool,\n        inheritable=False,\n        ephemeral=True,\n        default=False)\n\n    # Computable pointers have this set to an expression\n    # defining them.\n    expr = so.SchemaField(\n        s_expr.Expression,\n        default=None,\n        coerce=True,\n        compcoef=0.909,\n        special_ddl_syntax=True,\n    )\n\n    default = so.SchemaField(\n        s_expr.Expression,\n        allow_ddl_set=True,\n        describe_visibility=(\n            so.DescribeVisibilityPolicy.SHOW_IF_EXPLICIT_OR_DERIVED\n        ),\n        default=None,\n        coerce=True,\n        compcoef=0.909,\n    )\n\n    cardinality = so.SchemaField(\n        qltypes.SchemaCardinality,\n        default=qltypes.SchemaCardinality.One,\n        compcoef=0.833,\n        coerce=True,\n        special_ddl_syntax=True,\n        describe_visibility=(\n            so.DescribeVisibilityPolicy.SHOW_IF_EXPLICIT_OR_DERIVED\n        ),\n        merge_fn=merge_cardinality,\n    )\n\n    union_of = so.SchemaField(\n        so.ObjectSet['Pointer'],\n        default=None,\n        coerce=True,\n        type_is_generic_self=True,\n    )\n\n    intersection_of = so.SchemaField(\n        so.ObjectSet['Pointer'],\n        default=None,\n        coerce=True,\n        type_is_generic_self=True,\n    )\n\n    computed_link_alias_is_backward = so.SchemaField(\n        bool,\n        default=None,\n        compcoef=0.99,\n    )\n    computed_link_alias = so.SchemaField(\n        so.Object,\n        default=None,\n        compcoef=0.99,\n    )\n\n    rewrites_refs = so.RefDict(\n        attr=\"rewrites\",\n        requires_explicit_overloaded=True,\n        backref_attr=\"subject\",\n        ref_cls=s_rewrites.Rewrite,\n    )\n\n    rewrites = so.SchemaField(\n        so.ObjectIndexByUnqualifiedName[s_rewrites.Rewrite],\n        inheritable=False,\n        ephemeral=True,\n        coerce=True,\n        compcoef=0.857,\n        default=so.DEFAULT_CONSTRUCTOR,\n    )\n\n    def is_tuple_indirection(self) -> bool:\n        return False\n\n    def is_type_intersection(self) -> bool:\n        return False\n\n    def is_generated(self, schema: s_schema.Schema) -> bool:\n        return bool(self.get_from_alias(schema))\n\n    def get_subject(self, schema: s_schema.Schema) -> Optional[so.Object]:\n        # Required by ReferencedObject\n        return self.get_source(schema)\n\n    @classmethod\n    def get_displayname_static(cls, name: sn.Name) -> str:\n        sn = cls.get_shortname_static(name)\n        if sn.module == '__':\n            return sn.name\n        else:\n            return str(sn)\n\n    def get_verbosename(\n        self,\n        schema: s_schema.Schema,\n        *,\n        with_parent: bool=False,\n    ) -> str:\n        vn = super().get_verbosename(schema)\n        if self.is_non_concrete(schema):\n            return f'abstract {vn}'\n        else:\n            if with_parent:\n                source = self.get_source(schema)\n                assert source is not None\n                pvn = source.get_verbosename(\n                    schema, with_parent=True)\n                return f'{vn} of {pvn}'\n            else:\n                return vn\n\n    def is_scalar(self) -> bool:\n        return False\n\n    def material_type(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[s_schema.Schema, Pointer]:\n        non_derived_parent = self.get_nearest_non_derived_parent(schema)\n        source = non_derived_parent.get_source(schema)\n        if source is None:\n            return schema, self\n        else:\n            return schema, non_derived_parent\n\n    def get_nearest_defined(self, schema: s_schema.Schema) -> Pointer:\n        \"\"\"\n        Find the pointer definition site.\n\n        For view pointers, find the place where the pointer is \"really\"\n        defined that is, either its schema definition site or where it\n        last had a expression defining it.\n        \"\"\"\n        ptrcls = self\n        while (\n            ptrcls.get_is_derived(schema)\n            and not ptrcls.get_defined_here(schema)\n            # schema defined computeds don't have the ephemeral defined_here\n            # set, but they do have expr set, so we check that also.\n            and not ptrcls.get_expr(schema)\n            and (bases := ptrcls.get_bases(schema).objects(schema))\n            and len(bases) == 1\n            and bases[0].get_source(schema)\n        ):\n            ptrcls = bases[0]\n\n        return ptrcls\n\n    def get_near_endpoint(\n        self,\n        schema: s_schema.Schema,\n        direction: PointerDirection,\n    ) -> Optional[so.Object]:\n        if direction == PointerDirection.Outbound:\n            return self.get_source(schema)\n        else:\n            return self.get_target(schema)\n\n    def get_far_endpoint(\n        self,\n        schema: s_schema.Schema,\n        direction: PointerDirection,\n    ) -> Optional[so.Object]:\n        if direction == PointerDirection.Outbound:\n            return self.get_target(schema)\n        else:\n            return self.get_source(schema)\n\n    def set_target(\n        self,\n        schema: s_schema.Schema,\n        target: s_types.Type,\n    ) -> s_schema.Schema:\n        return self.set_field_value(schema, 'target', target)\n\n    def get_derived(\n        self: Self,\n        schema: s_schema.Schema,\n        source: s_sources.Source,\n        target: s_types.Type,\n        *,\n        derived_name_base: Optional[sn.Name] = None,\n        **kwargs: Any\n    ) -> tuple[s_schema.Schema, Self]:\n        fqname = self.derive_name(\n            schema, source, derived_name_base=derived_name_base)\n        ptr = schema.get(fqname, default=None)\n\n        if ptr is None:\n            fqname = self.derive_name(\n                schema,\n                source,\n                str(target.get_name(schema)),\n                derived_name_base=derived_name_base,\n            )\n            ptr = schema.get(fqname, default=None)\n            if ptr is None:\n                schema, ptr = self.derive_ref(\n                    schema, source, target=target,\n                    derived_name_base=derived_name_base, **kwargs)\n        return schema, ptr  # type: ignore\n\n    def get_derived_name_base(\n        self,\n        schema: s_schema.Schema,\n    ) -> sn.QualName:\n        shortname = self.get_shortname(schema)\n        return sn.QualName(module='__', name=shortname.name)\n\n    def derive_ref(\n        self,\n        schema: s_schema.Schema,\n        referrer: so.QualifiedObject,\n        *qualifiers: str,\n        target: Optional[s_types.Type] = None,\n        mark_derived: bool = False,\n        attrs: Optional[dict[str, Any]] = None,\n        dctx: Optional[sd.CommandContext] = None,\n        **kwargs: Any,\n    ) -> tuple[s_schema.Schema, Pointer]:\n        if target is None:\n            if attrs and 'target' in attrs:\n                target = attrs['target']\n            else:\n                target = self.get_target(schema)\n\n        if attrs is None:\n            attrs = {}\n\n        attrs['source'] = referrer\n        attrs['target'] = target\n\n        return super().derive_ref(\n            schema, referrer, mark_derived=mark_derived,\n            dctx=dctx, attrs=attrs, **kwargs)\n\n    def is_pure_computable(self, schema: s_schema.Schema) -> bool:\n        return bool(self.get_expr(schema)) or bool(self.get_computable(schema))\n\n    def is_id_pointer(self, schema: s_schema.Schema) -> bool:\n        local_name = self.get_local_name(schema)\n        if local_name.name != 'id':\n            return False\n\n        from edb.schema import sources as s_sources\n        std_base = schema.get('std::BaseObject', type=s_sources.Source)\n        std_id = std_base.getptr(schema, sn.UnqualName('id'))\n        assert isinstance(std_id, so.SubclassableObject)\n        return self.issubclass(schema, std_id)\n\n    def is_link_source_property(self, schema: s_schema.Schema) -> bool:\n        std_source = schema.get('std::source', type=so.SubclassableObject)\n        return self.issubclass(schema, std_source)\n\n    def is_link_target_property(self, schema: s_schema.Schema) -> bool:\n        std_target = schema.get('std::target', type=so.SubclassableObject)\n        return self.issubclass(schema, std_target)\n\n    def is_endpoint_pointer(self, schema: s_schema.Schema) -> bool:\n        std_source = schema.get('std::source', type=so.SubclassableObject)\n        std_target = schema.get('std::target', type=so.SubclassableObject)\n        return self.issubclass(schema, (std_source, std_target))\n\n    def is_special_pointer(self, schema: s_schema.Schema) -> bool:\n        return self.get_shortname(schema).name in {\n            'source', 'target', 'id'\n        } and (self.is_id_pointer(schema) or self.is_endpoint_pointer(schema))\n\n    @classmethod\n    def is_property(cls) -> bool:\n        # Property overloads\n        return False\n\n    def is_link_property(self, schema: s_schema.Schema) -> bool:\n        raise NotImplementedError\n\n    def is_dumpable(self, schema: s_schema.Schema) -> bool:\n        return (\n            not self.is_pure_computable(schema)\n            and not self.get_shortname(schema).name == '__type__'\n        )\n\n    def is_non_concrete(self, schema: s_schema.Schema) -> bool:\n        return self.get_source(schema) is None\n\n    def get_referrer(self, schema: s_schema.Schema) -> Optional[so.Object]:\n        return self.get_source(schema)\n\n    def get_exclusive_constraints(\n        self, schema: s_schema.Schema\n    ) -> Sequence[constraints.Constraint]:\n        if self.is_non_concrete(schema):\n            raise ValueError(f'{self!r} is not a concrete pointer')\n\n        exclusive = schema.get('std::exclusive', type=constraints.Constraint)\n\n        ptr = self.get_nearest_non_derived_parent(schema)\n\n        constrs = []\n        for constr in ptr.get_constraints(schema).objects(schema):\n            if (\n                constr.issubclass(schema, exclusive)\n                and not constr.get_subjectexpr(schema)\n                and not constr.get_delegated(schema)\n            ):\n                assert not constr.get_except_expr(schema)\n                constrs.append(constr)\n\n        return constrs\n\n    def is_exclusive(self, schema: s_schema.Schema) -> bool:\n        return bool(self.get_exclusive_constraints(schema))\n\n    def singular(\n        self,\n        schema: s_schema.Schema,\n        direction: PointerDirection = PointerDirection.Outbound,\n    ) -> bool:\n        # Determine the cardinality of a given endpoint set.\n        if direction == PointerDirection.Outbound:\n            cardinality = self.get_cardinality(schema)\n            if cardinality is None or not cardinality.is_known():\n                vn = self.get_verbosename(schema, with_parent=True)\n                raise AssertionError(f'cardinality of {vn} is unknown')\n            return cardinality.is_single()\n        else:\n            return self.is_exclusive(schema)\n\n    def get_implicit_bases(self, schema: s_schema.Schema) -> list[Pointer]:\n        bases = super().get_implicit_bases(schema)\n\n        # True implicit bases for pointers will have the same name\n        my_name = self.get_shortname(schema)\n        return [\n            b for b in bases\n            if b.get_shortname(schema) == my_name\n        ]\n\n    def get_implicit_ancestors(self, schema: s_schema.Schema) -> list[Pointer]:\n        ancestors = super().get_implicit_ancestors(schema)\n\n        # True implicit ancestors for pointers will have the same name\n        my_name = self.get_shortname(schema)\n        return [\n            b for b in ancestors\n            if b.get_shortname(schema) == my_name\n        ]\n\n    def has_user_defined_properties(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def allow_ref_propagation(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        refdict: so.RefDict,\n    ) -> bool:\n        object_type = self.get_source(schema)\n        if isinstance(object_type, s_types.Type):\n            return (\n                not object_type.is_view(schema) or refdict.attr == 'pointers')\n        else:\n            return True\n\n    def get_schema_reflection_default(\n        self,\n        schema: s_schema.Schema,\n    ) -> Optional[str]:\n        \"\"\"Return the default expression if this is a reflection of a\n           schema class field and the field has a defined default value.\n        \"\"\"\n        ptr = self.get_nearest_non_derived_parent(schema)\n\n        src = ptr.get_source(schema)\n        if src is None:\n            # This is an abstract pointer\n            return None\n\n        ptr_name = ptr.get_name(schema)\n        if ptr_name.module not in {'schema', 'sys', 'cfg'}:\n            # This isn't a reflection type\n            return None\n\n        if isinstance(src, Pointer):\n            # This is a link property\n            tgt = src.get_target(schema)\n            assert tgt is not None\n            schema_objtype = tgt\n        else:\n            assert isinstance(src, s_types.Type)\n            schema_objtype = src\n\n        assert isinstance(schema_objtype, so.QualifiedObject)\n        src_name = schema_objtype.get_name(schema)\n        mcls = so.ObjectMeta.maybe_get_schema_class(src_name.name)\n        if mcls is None:\n            # This schema class is not (publicly) reflected.\n            return None\n        fname = ptr.get_shortname(schema).name\n        if not mcls.has_field(fname):\n            # This pointer is not a schema field.\n            return None\n        field = mcls.get_field(fname)\n        if not isinstance(field, so.SchemaField):\n            # Not a schema field, no default possible.\n            return None\n        f_default = field.default\n        if (\n            f_default is None\n            or f_default is so.NoDefault\n        ):\n            # No explicit default value.\n            return None\n\n        tgt = ptr.get_target(schema)\n        assert tgt is not None\n\n        if f_default is so.DEFAULT_CONSTRUCTOR:\n            if (\n                issubclass(\n                    field.type,\n                    (collections.abc.Set, collections.abc.Sequence),\n                )\n                and not issubclass(field.type, (str, bytes))\n            ):\n                return f'<{tgt.get_displayname(schema)}>[]'\n            else:\n                return None\n\n        default = qlquote.quote_literal(json.dumps(f_default))\n\n        if tgt.is_enum(schema):\n            return f'<{tgt.get_displayname(schema)}><str>to_json({default})'\n        else:\n            return f'<{tgt.get_displayname(schema)}>to_json({default})'\n\n    def as_create_delta(\n        self,\n        schema: s_schema.Schema,\n        context: so.ComparisonContext,\n    ) -> sd.CreateObject[Pointer]:\n        delta = super().as_create_delta(schema, context)\n\n        # When we are creating a new required property on an existing type,\n        # we need to generate a AlterPointerLowerCardinality so that we can\n        # attach a USING to it.\n        if (\n            context.parent_ops\n            and isinstance(context.parent_ops[-1], sd.AlterObject)\n            and self.get_required(schema)\n            and not self.get_default(schema)\n            and not self.get_computable(schema)\n            and not self.is_link_property(schema)\n            and (required := delta._get_attribute_set_cmd('required'))\n        ):\n            special = sd.get_special_field_alter_handler(\n                'required', type(self))\n            assert special\n            top_op = special(classname=delta.classname)\n            delta.replace(required, top_op)\n            top_op.add(required)\n\n            context.parent_ops.append(delta)\n            top_op.record_diff_annotations(\n                schema=schema,\n                orig_schema=None,\n                object=self,\n                orig_object=None,\n                context=context,\n            )\n            context.parent_ops.pop()\n\n        return delta\n\n    def get_local_rewrite(\n        self, schema: s_schema.Schema, kind: qltypes.RewriteKind\n    ) -> Optional[s_rewrites.Rewrite]:\n        rewrites = self.get_rewrites(schema)\n        if rewrites:\n            for rewrite in rewrites.objects(schema):\n                if rewrite.get_kind(schema) == kind:\n                    return rewrite\n        return None\n\n    def get_rewrite(\n        self, schema: s_schema.Schema, kind: qltypes.RewriteKind\n    ) -> Optional[s_rewrites.Rewrite]:\n        if rw := self.get_local_rewrite(schema, kind):\n            return rw\n        for anc in self.get_ancestors(schema).objects(schema):\n            if rw := anc.get_local_rewrite(schema, kind):\n                return rw\n        return None\n\n\nclass PseudoPointer(abc.ABC):\n    # An abstract base class for pointer-like objects, i.e.\n    # pseudo-links used by the compiler to represent things like\n    # tuple and type intersection.\n    def is_tuple_indirection(self) -> bool:\n        return False\n\n    def is_type_intersection(self) -> bool:\n        return False\n\n    def get_bases(self, schema: s_schema.Schema) -> so.ObjectList[Pointer]:\n        return so.ObjectList.create(schema, [])\n\n    def get_ancestors(self, schema: s_schema.Schema) -> so.ObjectList[Pointer]:\n        return so.ObjectList.create(schema, [])\n\n    @abc.abstractmethod\n    def get_name(self, schema: s_schema.Schema) -> sn.QualName:\n        raise NotImplementedError\n\n    def get_shortname(self, schema: s_schema.Schema) -> sn.QualName:\n        return self.get_name(schema)\n\n    def get_displayname(self, schema: s_schema.Schema) -> str:\n        return str(self.get_name(schema))\n\n    def has_user_defined_properties(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def get_required(self, schema: s_schema.Schema) -> bool:\n        return True\n\n    @abc.abstractmethod\n    def get_cardinality(\n        self, schema: s_schema.Schema\n    ) -> qltypes.SchemaCardinality:\n        raise NotImplementedError\n\n    def get_path_id_name(self, schema: s_schema.Schema) -> sn.QualName:\n        return self.get_name(schema)\n\n    def get_is_derived(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def get_owned(self, schema: s_schema.Schema) -> bool:\n        return True\n\n    def get_union_of(\n        self,\n        schema: s_schema.Schema,\n    ) -> None:\n        return None\n\n    def get_intersection_of(\n        self,\n        schema: s_schema.Schema,\n    ) -> None:\n        return None\n\n    def get_default(\n        self,\n        schema: s_schema.Schema,\n    ) -> Optional[s_expr.Expression]:\n        return None\n\n    def get_expr(self, schema: s_schema.Schema) -> Optional[s_expr.Expression]:\n        return None\n\n    @abc.abstractmethod\n    def get_source(self, schema: s_schema.Schema) -> so.Object:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def get_target(self, schema: s_schema.Schema) -> s_types.Type:\n        raise NotImplementedError\n\n    def get_near_endpoint(\n        self,\n        schema: s_schema.Schema,\n        direction: PointerDirection,\n    ) -> so.Object:\n        if direction is PointerDirection.Outbound:\n            return self.get_source(schema)\n        else:\n            raise AssertionError(\n                f'inbound direction is not valid for {type(self)}'\n            )\n\n    def get_far_endpoint(\n        self,\n        schema: s_schema.Schema,\n        direction: PointerDirection,\n    ) -> so.Object:\n        if direction is PointerDirection.Outbound:\n            return self.get_target(schema)\n        else:\n            raise AssertionError(\n                f'inbound direction is not valid for {type(self)}'\n            )\n\n    def is_link_property(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_non_concrete(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    @abc.abstractmethod\n    def singular(\n        self,\n        schema: s_schema.Schema,\n        direction: PointerDirection = PointerDirection.Outbound,\n    ) -> bool:\n        raise NotImplementedError\n\n    def material_type(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[s_schema.Schema, PseudoPointer]:\n        return schema, self\n\n    def is_pure_computable(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_exclusive(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def get_schema_reflection_default(\n        self,\n        schema: s_schema.Schema,\n    ) -> Optional[str]:\n        return None\n\n\nPointerLike = Pointer | PseudoPointer\n\n\n@dataclasses.dataclass(repr=False, eq=False)\nclass ComputableRef:\n    \"\"\"A shell for a computed target type.\"\"\"\n\n    expr: qlast.Expr\n    specified_type: Optional[s_types.TypeShell[s_types.Type]] = (\n        dataclasses.field(default=None)\n    )\n\n\nclass PointerCommandContext(\n    sd.ObjectCommandContext[Pointer],\n    s_anno.AnnotationSubjectCommandContext,\n    s_rewrites.RewriteSubjectCommandContext,\n):\n    pass\n\n\nclass PointerCommandOrFragment[Pointer_T: Pointer](\n    referencing.ReferencedObjectCommandBase[Pointer_T]\n):\n    def is_property_command(self) -> bool:\n        return self.get_schema_metaclass().is_property()\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n        target_ref = self.get_local_attribute_value('target')\n        inf_target_ref: Optional[s_types.TypeShell[s_types.Type]]\n\n        # When cardinality/required is altered, we need to force a\n        # reconsideration of expr if it exists in order to check\n        # it against the new specifier or compute them on a\n        # RESET. This is kind of unfortunate.\n        if (\n            isinstance(self, sd.AlterObject)\n            and (\n                (\n                    self.has_attribute_value('cardinality')\n                    and not self.is_attribute_inherited('cardinality')\n                ) or (\n                    self.has_attribute_value('required')\n                    and not self.is_attribute_inherited('required')\n                )\n            )\n            and not self.has_attribute_value('expr')\n            and (expr := self.scls.get_expr(schema)) is not None\n        ):\n            self.set_attribute_value(\n                'expr',\n                s_expr.Expression.not_compiled(expr)\n            )\n\n        if isinstance(target_ref, ComputableRef):\n            schema, inf_target_ref = self._parse_computable(\n                target_ref.expr, schema, context)\n        elif (expr := self.get_local_attribute_value('expr')) is not None:\n            assert isinstance(expr, s_expr.Expression)\n            schema = s_types.materialize_type_in_attribute(\n                schema, context, self, 'target')\n            schema, inf_target_ref = self._parse_computable(\n                expr.parse(), schema, context)\n        else:\n            inf_target_ref = None\n\n        if (\n            isinstance(self, sd.CreateObject)\n            and not self.is_property_command()\n        ):\n            self.set_attribute_value('linkful', True)\n\n        if inf_target_ref is not None:\n            if inf_target_ref.has_intersection():\n                raise errors.UnsupportedFeatureError(\n                    (\n                        f'unsupported type intersection in schema '\n                        f'{inf_target_ref.get_name(schema).name}'\n                    ),\n                    hint=(\n                        f'Type intersections are currently '\n                        f'unsupported as valid link targets.'\n                    ),\n                    span=self.span,\n                )\n\n            span = self.get_attribute_span('target')\n            self.set_attribute_value(\n                'target',\n                inf_target_ref,\n                span=span,\n                computed=True,\n            )\n\n        schema = s_types.materialize_type_in_attribute(\n            schema, context, self, 'target')\n\n        expr = self.get_local_attribute_value('expr')\n        if expr is not None:\n            # There is an expression, therefore it is a computable.\n            self.set_attribute_value('computable', True)\n\n        return schema\n\n    def _parse_computable(\n        self,\n        expr: qlast.Expr,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> tuple[\n        s_schema.Schema,\n        s_types.TypeShell[s_types.Type],\n    ]:\n        from edb.ir import ast as irast\n        from edb.ir import typeutils as irtyputils\n        from edb.ir import utils as irutils\n\n        # \"source\" attribute is set automatically as a refdict back-attr\n        parent_ctx = self.get_referrer_context(context)\n        assert parent_ctx is not None\n        source_name = context.get_referrer_name(parent_ctx)\n        assert isinstance(source_name, sn.QualName)\n\n        source = schema.get(source_name)\n        parent_vname = source.get_verbosename(schema)\n        ptr_name = self.get_verbosename(parent=parent_vname)\n        expression = self.compile_expr_field(\n            schema, context,\n            field=Pointer.get_field('expr'),\n            value=s_expr.Expression.from_ast(expr, schema, context.modaliases),\n        )\n\n        target = expression.irast.stype\n        target_shell = target.as_shell(expression.irast.schema)\n        if (\n            isinstance(target_shell, s_types.UnionTypeShell)\n            and target_shell.opaque\n        ):\n            target = schema.get('std::BaseObject', type=s_types.Type)\n            target_shell = target.as_shell(schema)\n\n        orig_expr = expression.irast.expr\n        if isinstance(orig_expr, irast.Set):\n            orig_expr = irutils.unwrap_set(orig_expr)\n        result_expr = orig_expr\n        if isinstance(result_expr, irast.Set):\n            if isinstance(result_expr.expr, irast.Pointer):\n                result_expr, _ = irutils.collapse_type_intersection(\n                    result_expr)\n\n        if self.is_property_command():\n            self.set_attribute_value('linkful', irutils.is_linkful(orig_expr))\n\n        # Process a computable pointer which potentially could be an\n        # aliased link that should inherit link properties.\n        computed_link_alias = None\n        computed_link_alias_is_backward = None\n        if (\n            isinstance(result_expr, irast.Set)\n            and isinstance(result_expr.expr, irast.Pointer)\n            and (expr_rptr := result_expr.expr)\n            and expr_rptr.direction is PointerDirection.Outbound\n            and not isinstance(expr_rptr.source.expr, irast.Pointer)\n            and isinstance(expr_rptr.ptrref, irast.PointerRef)\n            and schema.has_object(expr_rptr.ptrref.id)\n        ):\n            new_schema, aliased_ptr = irtyputils.ptrcls_from_ptrref(\n                expr_rptr.ptrref, schema=schema\n            )\n            # Only pointers coming from the same source as the\n            # alias should be \"inherited\" (in order to preserve\n            # link props). Random paths coming from other sources\n            # get treated same as any other arbitrary expression\n            # in a computable.\n            if (\n                aliased_ptr.get_source(new_schema) == source\n                and isinstance(aliased_ptr, self.get_schema_metaclass())\n            ):\n                schema = new_schema\n                computed_link_alias = aliased_ptr\n                computed_link_alias_is_backward = False\n\n        # Do similar logic, but in reverse, to see if the computed pointer\n        # is a computed backlink that we need to keep track of.\n        if (\n            computed_link_alias is None\n            and isinstance(orig_expr, irast.Set)\n            and isinstance(orig_expr.expr, irast.Pointer)\n            and isinstance(\n                orig_expr.expr.ptrref, irast.TypeIntersectionPointerRef)\n            and len(orig_expr.expr.ptrref.rptr_specialization) == 1\n            and expr_rptr\n            and expr_rptr.direction is not PointerDirection.Outbound\n        ):\n            ptrref = list(orig_expr.expr.ptrref.rptr_specialization)[0]\n            new_schema, aliased_ptr = irtyputils.ptrcls_from_ptrref(\n                ptrref, schema=schema\n            )\n            if (\n                aliased_ptr.get_target(new_schema) == source\n                and not ptrref.out_source.is_opaque_union\n                and isinstance(aliased_ptr, self.get_schema_metaclass())\n            ):\n                computed_link_alias_is_backward = True\n                computed_link_alias = aliased_ptr\n                schema = new_schema\n\n        self.set_attribute_value('computed_link_alias', computed_link_alias)\n        self.set_attribute_value(\n            'computed_link_alias_is_backward', computed_link_alias_is_backward)\n\n        self.set_attribute_value('expr', expression)\n        required, card = expression.irast.cardinality.to_schema_value()\n\n        # Disallow referring to aliases from computed pointers.\n        # We will support this eventually but it is pretty broken now\n        # and best to consistently give an understandable error.\n        for schema_ref in expression.irast.schema_refs:\n            if isinstance(schema_ref, s_expraliases.Alias):\n                span = self.get_attribute_span('target')\n                an = schema_ref.get_verbosename(expression.irast.schema)\n                raise errors.UnsupportedFeatureError(\n                    f'referring to {an} from computed {ptr_name} '\n                    f'is unsupported',\n                    span=span,\n                )\n\n        if (\n            not isinstance(source, Pointer)\n            and not source.is_view(schema)  # type: ignore\n            and target.is_view(expression.irast.schema)\n        ):\n            raise errors.UnsupportedFeatureError(\n                f'including a shape on schema-defined computed links '\n                f'is not yet supported',\n                span=self.span,\n            )\n\n        spec_target: Optional[\n            s_types.TypeShell[s_types.Type] | s_types.Type | ComputableRef\n        ] = (\n            self.get_specified_attribute_value('target', schema, context))\n        spec_required: Optional[bool] = (\n            self.get_specified_attribute_value('required', schema, context))\n        spec_card: Optional[qltypes.SchemaCardinality] = (\n            self.get_specified_attribute_value('cardinality', schema, context))\n\n        if (\n            spec_target is not None\n            and (\n                not isinstance(spec_target, ComputableRef)\n                or (spec_target := spec_target.specified_type) is not None\n            )\n        ):\n            if isinstance(spec_target, s_types.TypeShell):\n                spec_target_type = spec_target.resolve(schema)\n            else:\n                spec_target_type = spec_target\n\n            mschema, inferred_target_type = target.material_type(\n                expression.irast.schema)\n\n            if spec_target_type != inferred_target_type:\n                span = self.get_attribute_span('target')\n                raise errors.SchemaDefinitionError(\n                    f'the type inferred from the expression '\n                    f'of the computed {ptr_name} '\n                    f'is {inferred_target_type.get_verbosename(mschema)}, '\n                    f'which does not match the explicitly specified '\n                    f'{spec_target_type.get_verbosename(schema)}',\n                    span=span\n                )\n\n        if spec_required and not required:\n            span = self.get_attribute_span('target')\n            raise errors.SchemaDefinitionError(\n                f'possibly an empty set returned by an '\n                f'expression for the computed '\n                f'{ptr_name} '\n                f\"explicitly declared as 'required'\",\n                span=span\n            )\n\n        if (\n            spec_card is qltypes.SchemaCardinality.One\n            and card is not qltypes.SchemaCardinality.One\n        ):\n            span = self.get_attribute_span('target')\n            raise errors.SchemaDefinitionError(\n                f'possibly more than one element returned by an '\n                f'expression for the computed '\n                f'{ptr_name} '\n                f\"explicitly declared as 'single'\",\n                span=span\n            )\n\n        if spec_card is None:\n            self.set_attribute_value('cardinality', card, computed=True)\n\n        if spec_required is None:\n            self.set_attribute_value('required', required, computed=True)\n\n        if (\n            not is_view_source(source, schema)\n            and expression.irast.volatility.is_volatile()\n        ):\n            span = self.get_attribute_span('target')\n            raise errors.SchemaDefinitionError(\n                f'volatile functions are not permitted in schema-defined '\n                f'computed expressions',\n                span=span\n            )\n\n        self.set_attribute_value('computable', True)\n\n        return schema, target_shell\n\n    def _compile_expr(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        expr: s_expr.Expression,\n        *,\n        in_ddl_context_name: Optional[str] = None,\n        track_schema_ref_exprs: bool = False,\n        singleton_result_expected: bool = False,\n        target_as_singleton: bool = False,\n        expr_description: Optional[str] = None,\n        no_query_rewrites: bool = False,\n        make_globals_empty: bool = False,\n        span: Optional[parsing.Span] = None,\n        detached: bool = False,\n        should_set_path_prefix_anchor: bool = True\n    ) -> s_expr.CompiledExpression:\n        singletons: list[s_types.Type | Pointer] = []\n\n        parent_ctx = self.get_referrer_context_or_die(context)\n        source = parent_ctx.op.get_object(schema, context)\n\n        if (\n            isinstance(source, Pointer)\n            and not source.get_source(schema)\n        ):\n            # If the source is an abstract link, we need to\n            # make up an object and graft the link onto it,\n            # because the compiler really does not know what\n            # to make of a link without a source or target.\n            from edb.schema import objtypes as s_objtypes\n\n            base_obj = schema.get(\n                s_objtypes.ObjectType.get_default_base_name(),\n                type=s_objtypes.ObjectType\n            )\n            schema, view = base_obj.derive_subtype(\n                schema,\n                name=sn.QualName(\"__derived__\", \"FakeAbstractLinkBase\"),\n                mark_derived=True,\n                transient=True,\n            )\n            schema, source = source.derive_ref(\n                schema,\n                view,\n                target=view,\n                mark_derived=True,\n                transient=True,\n            )\n\n        assert isinstance(source, (s_types.Type, Pointer))\n        singletons = [source]\n\n        if target_as_singleton:\n            src = self.scls.get_source(schema)\n            if isinstance(src, Pointer):\n                # linkprop\n                singletons.append(src)\n            else:\n                singletons.append(self.scls)\n\n        with errors.ensure_span(span or expr.parse().span):\n            options = qlcompiler.CompilerOptions(\n                modaliases=context.modaliases,\n                schema_object_context=self.get_schema_metaclass(),\n                anchors={'__source__': source},\n                path_prefix_anchor=(\n                    '__source__'\n                    if should_set_path_prefix_anchor\n                    else None),\n                singletons=singletons,\n                apply_query_rewrites=(\n                    not context.stdmode and not no_query_rewrites\n                ),\n                make_globals_empty=make_globals_empty,\n                track_schema_ref_exprs=track_schema_ref_exprs,\n                in_ddl_context_name=in_ddl_context_name,\n            )\n\n            compiled = expr.compiled(\n                schema=schema,\n                options=options,\n                detached=detached,\n                context=context,\n            )\n\n            if singleton_result_expected and compiled.cardinality.is_multi():\n                if expr_description is None:\n                    expr_description = 'an expression'\n\n                raise errors.SchemaError(\n                    f'possibly more than one element returned by '\n                    f'{expr_description}, while a singleton is expected'\n                )\n\n            return compiled\n\n    def compile_expr_field(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: s_expr.Expression,\n        track_schema_ref_exprs: bool=False,\n    ) -> s_expr.CompiledExpression:\n\n        if field.name in {'default', 'expr'}:\n            if field.name == 'expr':\n                parent_ctx = self.get_referrer_context_or_die(context)\n                source = parent_ctx.op.get_object(schema, context)\n                parent_vname = source.get_verbosename(schema)\n                ptr_name = self.get_verbosename(parent=parent_vname)\n                in_ddl_context_name = f'computed {ptr_name}'\n                detached = False\n            else:\n                in_ddl_context_name = None\n                detached = True\n\n            # If we are in a link property's default field\n            # do not set path prefix anchor, because link properties\n            # cannot have defaults that reference the object being inserted\n            should_set_path_prefix_anchor = True\n            if field.name == 'default':\n                # We are checking if the parent context is a pointer\n                # (i.e. a link or a property).\n                # If so, do not set the path prefix anchor.\n                parent_ctx = self.get_referrer_context_or_die(context)\n                source = parent_ctx.op.get_object(schema, context)\n                if isinstance(source, Pointer):\n                    should_set_path_prefix_anchor = False\n\n            return self._compile_expr(\n                schema,\n                context,\n                value,\n                in_ddl_context_name=in_ddl_context_name,\n                track_schema_ref_exprs=track_schema_ref_exprs,\n                detached=detached,\n                should_set_path_prefix_anchor=should_set_path_prefix_anchor,\n            )\n        else:\n            return super().compile_expr_field(\n                schema, context, field, value, track_schema_ref_exprs)\n\n    def get_dummy_expr_field_value(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: Any,\n    ) -> Optional[s_expr.Expression]:\n        if field.name == 'expr':\n            return None\n        elif field.name == 'default':\n            return None\n        else:\n            raise NotImplementedError(f'unhandled field {field.name!r}')\n\n\nclass PointerCommand[Pointer_T: Pointer](\n    referencing.NamedReferencedInheritingObjectCommand[Pointer_T],\n    constraints.ConsistencySubjectCommand[Pointer_T],\n    s_anno.AnnotationSubjectCommand[Pointer_T],\n    PointerCommandOrFragment[Pointer_T],\n):\n\n    def _validate_computables(\n        self, schema: s_schema.Schema, context: sd.CommandContext\n    ) -> None:\n        scls = self.scls\n\n        if scls.get_from_alias(schema):\n            return\n\n        is_computable = scls.is_pure_computable(schema)\n        is_owned = scls.get_owned(schema)\n\n        if is_computable:\n            if any(\n                b.is_non_concrete(schema)\n                and str(b.get_name(schema)) not in (\n                    'std::link', 'std::property')\n                for b in scls.get_bases(schema).objects(schema)\n            ):\n                raise errors.SchemaDefinitionError(\n                    f'it is illegal for the computed '\n                    f'{scls.get_verbosename(schema, with_parent=True)} '\n                    f'to extend an abstract '\n                    f'{scls.get_schema_class_displayname()}',\n                    span=self.span,\n                )\n\n        # Get the non-generic, explicitly declared ancestors as the\n        # limitations on computables apply to explicitly declared\n        # pointers, not just a long chain of inherited ones.\n        #\n        # Because this is potentially nested inside a command to\n        # delete a property some ancestors may not be present in the\n        # schema anymore, so we will only consider the ones that still\n        # are (which should still be valid).\n        lineage: list[Pointer_T] = []\n        for iid in scls.get_ancestors(schema)._ids:\n            try:\n                p = cast(Pointer_T, schema.get_by_id(iid))\n                if not p.is_non_concrete(schema) and p.get_owned(schema):\n                    lineage.append(p)\n            except errors.InvalidReferenceError:\n                pass\n\n        if is_owned:\n            # If the current pointer is explicitly declared, add it at\n            # the end of the lineage.\n            lineage.insert(0, scls)\n\n        status = self._validate_lineage(schema, lineage)\n\n        if status is LineageStatus.VALID:\n            return\n\n        if is_computable and is_owned:\n            # Overloading with a computable\n            raise errors.SchemaDefinitionError(\n                f'it is illegal for the computed '\n                f'{scls.get_verbosename(schema, with_parent=True)} '\n                f'to overload an existing '\n                f'{scls.get_schema_class_displayname()}',\n                span=self.span,\n            )\n        else:\n            if status is LineageStatus.MIXED:\n                raise errors.SchemaDefinitionError(\n                    f'it is illegal for the '\n                    f'{scls.get_verbosename(schema, with_parent=True)} '\n                    f'to extend both a computed and a non-computed '\n                    f'{scls.get_schema_class_displayname()}',\n                    span=self.span,\n                )\n            elif status is LineageStatus.MULTIPLE_COMPUTABLES:\n                raise errors.SchemaDefinitionError(\n                    f'it is illegal for the '\n                    f'{scls.get_verbosename(schema, with_parent=True)} '\n                    f'to extend more than one computed '\n                    f'{scls.get_schema_class_displayname()}',\n                    span=self.span,\n                )\n\n    def _validate_lineage(\n        self,\n        schema: s_schema.Schema,\n        lineage: list[Pointer_T],\n    ) -> LineageStatus:\n        if len(lineage) <= 1:\n            # Having at most 1 item in the lineage is always valid.\n            return LineageStatus.VALID\n\n        head, *rest = lineage\n\n        if not head.is_pure_computable(schema):\n            # The rest of the lineage must all be regular\n            if any(b.is_pure_computable(schema) for b in rest):\n                return LineageStatus.MIXED\n            else:\n                return LineageStatus.VALID\n\n        else:\n            # We have a computable with some non-empty lineage. Which\n            # could be valid only if this is some aliasing followed by\n            # regular pointers only.\n            prev_shortname = head.get_shortname(schema)\n            prev_is_comp = True\n            for b in rest:\n                cur_is_comp = b.is_pure_computable(schema)\n                cur_shortname = b.get_shortname(schema)\n                if prev_is_comp:\n                    # Computables cannot overload, but they can alias\n                    # other pointers, however aliases cannot have\n                    # matching shortnames.\n                    if cur_shortname == prev_shortname:\n                        # Names match, so this is illegal.\n                        if cur_is_comp:\n                            return LineageStatus.MULTIPLE_COMPUTABLES\n                        else:\n                            return LineageStatus.MIXED\n\n                else:\n                    # Only regular pointers expected from here on.\n                    if cur_is_comp:\n                        return LineageStatus.MULTIPLE_COMPUTABLES\n\n                prev_shortname = cur_shortname\n                prev_is_comp = cur_is_comp\n\n            # Did not find anything wrong with the computable lineage.\n            return LineageStatus.VALID\n\n    def validate_object(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        \"\"\"Check that pointer definition is sound.\"\"\"\n        from edb.ir import ast as irast\n\n        referrer_ctx = self.get_referrer_context(context)\n        if referrer_ctx is None:\n            return\n\n        self._validate_computables(schema, context)\n\n        scls: Pointer = self.scls\n        if not scls.get_owned(schema):\n            return\n\n        default_expr: Optional[s_expr.Expression] = scls.get_default(schema)\n\n        if default_expr is not None:\n\n            if not default_expr.irast:\n                default_expr = self._compile_expr(\n                    schema, context, default_expr, detached=True,\n                )\n                assert default_expr.irast\n\n            if scls.is_id_pointer(schema):\n                self._check_id_default(\n                    schema, context, default_expr.irast.expr)\n\n            span = self.get_attribute_span('default')\n            ir = default_expr.irast\n            default_schema = ir.schema\n            default_type = ir.stype\n            assert default_type is not None\n            ptr_target = scls.get_target(schema)\n            assert ptr_target is not None\n\n            if (\n                default_type.is_view(default_schema)\n                # Using an alias/global always creates a new subtype view,\n                # but we want to allow those here, so check whether there\n                # is a shape more directly.\n                and not (\n                    len(shape := ir.view_shapes.get(default_type, [])) == 1\n                    and shape[0].is_id_pointer(default_schema)\n                )\n            ):\n                raise errors.SchemaDefinitionError(\n                    f'default expression may not include a shape',\n                    span=span,\n                )\n            if not default_type.assignment_castable_to(\n                    ptr_target, default_schema):\n                raise errors.SchemaDefinitionError(\n                    f'default expression is of invalid type: '\n                    f'{default_type.get_displayname(default_schema)}, '\n                    f'expected {ptr_target.get_displayname(schema)}',\n                    span=span,\n                )\n            # \"required\" status of defaults should not be enforced\n            # because it's impossible to actually guarantee that any\n            # SELECT involving a path is non-empty\n            ptr_cardinality = scls.get_cardinality(schema)\n            _default_required, default_cardinality = \\\n                default_expr.irast.cardinality.to_schema_value()\n\n            if (ptr_cardinality is qltypes.SchemaCardinality.One\n                    and default_cardinality != ptr_cardinality):\n                raise errors.SchemaDefinitionError(\n                    f'possibly more than one element returned by '\n                    f'the default expression for '\n                    f'{scls.get_verbosename(schema)} declared as '\n                    f\"'single'\",\n                    span=span,\n                )\n\n            # prevent references to local links, only properties\n            pointers = ast.find_children(default_expr.irast, irast.Pointer)\n            scls_source = scls.get_source(schema)\n            assert scls_source\n            for pointer in pointers:\n                if pointer.source.typeref.id != scls_source.id:\n                    continue\n                if not isinstance(pointer.ptrref, irast.PointerRef):\n                    continue\n                s_pointer = schema.get_by_id(pointer.ptrref.id, type=Pointer)\n                card = s_pointer.get_cardinality(schema)\n\n                if s_pointer.is_property() and card.is_multi():\n                    raise errors.SchemaDefinitionError(\n                        f\"default expression cannot refer to multi properties \"\n                        \"of inserted object\",\n                        span=span,\n                        hint=\"this is a temporary implementation restriction\",\n                    )\n\n                if not s_pointer.is_property():\n                    raise errors.SchemaDefinitionError(\n                        f\"default expression cannot refer to links \"\n                        \"of inserted object\",\n                        span=span,\n                        hint='this is a temporary implementation restriction'\n                    )\n\n        if (\n            self.scls.get_rewrite(schema, qltypes.RewriteKind.Update)\n            or self.scls.get_rewrite(schema, qltypes.RewriteKind.Insert)\n        ):\n            if self.scls.get_cardinality(schema).is_multi():\n                raise errors.SchemaDefinitionError(\n                    f\"cannot specify a rewrite for \"\n                    f\"{scls.get_verbosename(schema, with_parent=True)} \"\n                    f\"because it is multi\",\n                    span=self.span,\n                    hint='this is a temporary implementation restriction'\n                )\n\n            if self.scls.has_user_defined_properties(schema):\n                raise errors.SchemaDefinitionError(\n                    f\"cannot specify a rewrite for \"\n                    f\"{scls.get_verbosename(schema, with_parent=True)} \"\n                    f\"because it has link properties\",\n                    span=self.span,\n                    hint='this is a temporary implementation restriction'\n                )\n\n    def _check_id_default(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        expr: irast.Base,\n    ) -> None:\n        \"\"\"If default is being set on id, check it against a whitelist\"\"\"\n        from edb.ir import ast as irast\n        from edb.ir import utils as irutils\n\n        # If we add more, we probably want a better mechanism\n        ID_ALLOWLIST = (\n            'std::uuid_generate_v1mc',\n            'std::uuid_generate_v4',\n        )\n\n        while (\n            isinstance(expr, irast.Set)\n            and expr.expr\n            and irutils.is_trivial_select(expr.expr)\n        ):\n            expr = expr.expr.result\n\n        if not (\n            isinstance(expr, irast.Set)\n            and isinstance(expr.expr, irast.FunctionCall)\n            and str(expr.expr.func_shortname) in ID_ALLOWLIST\n        ):\n            span = self.get_attribute_span('default')\n            options = ', '.join(ID_ALLOWLIST)\n            raise errors.SchemaDefinitionError(\n                \"invalid default value for 'id' property\",\n                hint=f'default must be a call to one of: {options}',\n                span=span,\n            )\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, PointerCommand)\n\n        referrer_ctx = cls.get_referrer_context(context)\n        if referrer_ctx is not None:\n            if getattr(astnode, 'declared_overloaded', False):\n                cmd.set_attribute_value('declared_overloaded', True)\n        else:\n            # This is an abstract property/link\n            if cmd.get_attribute_value('default') is not None:\n                typ = cls.get_schema_metaclass().get_schema_class_displayname()\n                raise errors.SchemaDefinitionError(\n                    f\"'default' is not a valid field for an abstract {typ}\",\n                    span=astnode.span)\n        return cmd\n\n    def _process_create_or_alter_ast(\n        self,\n        schema: s_schema.Schema,\n        astnode: qlast.CreateConcretePointer,\n        context: sd.CommandContext,\n    ) -> None:\n        \"\"\"Handle the CREATE {PROPERTY|LINK} AST node.\n\n        This may be called in the context of either Create or Alter.\n        \"\"\"\n        from edb.schema import sources as s_sources\n\n        if astnode.is_required is not None:\n            self.set_attribute_value(\n                'required',\n                astnode.is_required,\n                span=astnode.span,\n            )\n\n        if astnode.cardinality is not None:\n            if isinstance(self, sd.CreateObject):\n                self.set_attribute_value(\n                    'cardinality',\n                    astnode.cardinality,\n                    span=astnode.span,\n                )\n            else:\n                handler = sd.get_special_field_alter_handler_for_context(\n                    'cardinality', context)\n                assert handler is not None\n                set_field = qlast.SetField(\n                    name='cardinality',\n                    value=qlast.Constant.string(\n                        str(astnode.cardinality),\n                    ),\n                    special_syntax=True,\n                    span=astnode.span,\n                )\n                apc = handler._cmd_tree_from_ast(schema, set_field, context)\n                self.add(apc)\n\n        parent_ctx = self.get_referrer_context_or_die(context)\n        source_name = context.get_referrer_name(parent_ctx)\n        self.set_attribute_value(\n            'source',\n            so.ObjectShell(name=source_name, schemaclass=s_sources.Source),\n        )\n\n        target_ref: None | s_types.TypeShell[s_types.Type] | ComputableRef\n\n        if astnode.target:\n            if isinstance(astnode.target, qlast.TypeExpr):\n                target_ref = utils.ast_to_type_shell(\n                    astnode.target,\n                    metaclass=s_types.Type,\n                    modaliases=context.modaliases,\n                    module=source_name.module,\n                    schema=schema,\n                )\n            else:\n                # computable\n                qlcompiler.normalize(\n                    astnode.target,\n                    schema=schema,\n                    modaliases=context.modaliases\n                )\n                target_ref = ComputableRef(astnode.target)\n        else:\n            # Target is inherited.\n            target_ref = None\n\n        if isinstance(self, sd.CreateObject):\n            assert astnode.target is not None\n            self.set_attribute_value(\n                'target',\n                target_ref,\n                span=astnode.target.span,\n            )\n\n        elif target_ref is not None:\n            assert astnode.target is not None\n            self.set_attribute_value(\n                'target',\n                target_ref,\n                span=astnode.target.span,\n            )\n\n    def _process_alter_ast(\n        self,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> None:\n        \"\"\"Handle the ALTER {PROPERTY|LINK} AST node.\"\"\"\n        expr_cmd = qlast.get_ddl_field_command(astnode, 'expr')\n        if expr_cmd is not None:\n            expr = expr_cmd.value\n            if expr is not None:\n                assert isinstance(expr, qlast.Expr)\n                qlcompiler.normalize(\n                    expr,\n                    schema=schema,\n                    modaliases=context.modaliases\n                )\n                target_ref = ComputableRef(\n                    expr,\n                    specified_type=self.get_attribute_value('target'),\n                )\n                self.set_attribute_value(\n                    'target',\n                    target_ref,\n                    span=expr.span,\n                )\n                self.discard_attribute('expr')\n\n\nclass CreatePointer[Pointer_T: Pointer](\n    referencing.CreateReferencedInheritingObject[Pointer_T],\n    PointerCommand[Pointer_T],\n):\n\n    def ast_ignore_ownership(self) -> bool:\n        # If we have a SET REQUIRED with a fill_expr, we need to force\n        # this operation to appear in the AST in a useful position,\n        # even if it normally would be skipped.\n        subs = list(self.get_subcommands(type=AlterPointerLowerCardinality))\n        return len(subs) == 1 and bool(subs[0].fill_expr)\n\n    @classmethod\n    def as_inherited_ref_cmd(\n        cls,\n        *,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        astnode: qlast.ObjectDDL,\n        bases: list[Pointer_T],\n        referrer: so.Object,\n    ) -> sd.ObjectCommand[Pointer_T]:\n        cmd = super().as_inherited_ref_cmd(\n            schema=schema,\n            context=context,\n            astnode=astnode,\n            bases=bases,\n            referrer=referrer,\n        )\n\n        if (\n            (\n                isinstance(referrer, s_types.Type)\n                and referrer.is_view(schema)\n            ) or (\n                isinstance(referrer, Pointer)\n                and referrer.get_from_alias(schema)\n            )\n        ):\n            cmd.set_attribute_value('from_alias', True)\n            cmd.set_object_aux_data('from_alias', True)\n\n        return cmd\n\n\nclass AlterPointer[Pointer_T: Pointer](\n    referencing.AlterReferencedInheritingObject[Pointer_T],\n    PointerCommand[Pointer_T],\n):\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._alter_begin(schema, context)\n\n        if not context.canonical and (\n            self.get_attribute_value('expr') is not None\n            or self.get_orig_attribute_value('expr') is not None\n            or bool(self.get_subcommands(type=constraints.ConstraintCommand))\n            or (\n                self.get_attribute_value('default') is not None\n                and self.scls.is_link_property(schema)\n            )\n        ):\n            extras: dict[so.Object, list[str]] = {}\n            if (\n                self.get_attribute_value('expr') is not None\n                or self.get_orig_attribute_value('expr') is not None\n            ):\n                for constr in (\n                    self.scls.get_constraints(schema).objects(schema)\n                ):\n                    extras[constr] = ['finalexpr']\n\n            # If the expression gets changed, we need to propagate\n            # this change to other expressions referring to this one,\n            # in case there are any cycles caused by this change.\n            #\n            # Also, if constraints are modified, that can affect\n            # cardinality of other expressions using backlinks.\n            #\n            # Also when setting a default on a link property, since\n            # access policies need to be prevented from accessing them.\n            # (Ugh.)\n            #\n            # FIXME: sometimes this can cause a constraint to get\n            # altered because we've created another constraint, which\n            # could change inference\n            schema = self._propagate_if_expr_refs(\n                schema,\n                context,\n                action=self.get_friendly_description(schema=schema),\n                extra_refs=extras,\n            )\n\n        return schema\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> referencing.AlterReferencedInheritingObject[Any]:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, PointerCommand)\n        if isinstance(astnode, qlast.CreateConcreteLink):\n            cmd._process_create_or_alter_ast(schema, astnode, context)\n        else:\n            expr_cmd = qlast.get_ddl_field_command(astnode, 'expr')\n            if expr_cmd is not None:\n                expr = expr_cmd.value\n                if expr is None:\n                    # `RESET EXPRESSION` detected\n                    aop = sd.AlterObjectProperty(\n                        property='expr',\n                        new_value=None,\n                        span=astnode.span,\n                    )\n                    cmd.add(aop)\n\n        assert isinstance(cmd, referencing.AlterReferencedInheritingObject)\n        return cmd\n\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n\n        # Handle `RESET EXPRESSION` here\n        if (\n            self.has_attribute_value('expr')\n            and not self.is_attribute_inherited('expr')\n            and self.get_attribute_value('expr') is None\n        ):\n            self.set_attribute_value('linkful', not self.is_property_command())\n\n            old_expr = self.get_orig_attribute_value('expr')\n            pointer = schema.get(self.classname, type=Pointer)\n            if old_expr is None:\n                # Get the old value from the schema if the old_expr\n                # attribute isn't set.\n                old_expr = pointer.get_expr(schema)\n\n            if old_expr is not None:\n                # If the expression was explicitly set to None,\n                # that means that `RESET EXPRESSION` was executed\n                # and this is no longer a computable.\n\n                self.set_attribute_value('computable', None)\n                computed_fields = pointer.get_computed_fields(schema)\n                if (\n                    'required' in computed_fields\n                    and not self.has_attribute_value('required')\n                ):\n                    self.set_attribute_value('required', None)\n                if (\n                    'cardinality' in computed_fields\n                    and not self.has_attribute_value('cardinality')\n                ):\n                    self.set_attribute_value('cardinality', None)\n                self.set_attribute_value(\n                    'computed_link_alias_is_backward', None)\n                self.set_attribute_value('computed_link_alias', None)\n\n            # Clear the placeholder value for 'expr'.\n            self.set_attribute_value('expr', None)\n\n        return schema\n\n    def canonicalize_alter_from_external_ref(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        # if the delta involves re-setting a computable\n        # expression, then we also need to change the type to the\n        # new expression type\n\n        expr = self.get_attribute_value('expr')\n        if expr is None:\n            # This shouldn't happen, but asserting here doesn't seem quite\n            # right either.\n            return\n\n        assert isinstance(expr, s_expr.Expression)\n        pointer = schema.get(self.classname, type=Pointer)\n        source = cast(s_types.Type, pointer.get_source(schema))\n        expression = expr.compiled(\n            schema=schema,\n            options=qlcompiler.CompilerOptions(\n                modaliases=context.modaliases,\n                anchors={'__source__': source},\n                path_prefix_anchor='__source__',\n                singletons=frozenset([source]),\n                apply_query_rewrites=not context.stdmode,\n            ),\n            context=context,\n        )\n\n        target = expression.irast.stype\n        self.set_attribute_value(\n            'target',\n            target,\n            inherited=pointer.field_is_inherited(schema, 'target'),\n            computed=pointer.field_is_computed(schema, 'target'),\n        )\n\n    def is_data_safe(self) -> bool:\n        # HACK: expr ought to be managed by AlterSpecialObjectField\n        # the way that target/required/cardinality are.\n        return super().is_data_safe() and not (\n            self.get_attribute_value('expr') is not None\n            and self.get_orig_attribute_value('expr') is None\n        )\n\n\nclass DeletePointer[Pointer_T: Pointer](\n    referencing.DeleteReferencedInheritingObject[Pointer_T],\n    PointerCommand[Pointer_T],\n):\n    def _delete_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._delete_begin(schema, context)\n        if (\n            not context.canonical\n            and (target := self.scls.get_target(schema)) is not None\n            and not self.scls.is_endpoint_pointer(schema)\n            and (del_cmd := target.as_type_delete_if_unused(schema)) is not None\n        ):\n            self.add_caused(del_cmd)\n\n        if not context.canonical:\n            # We need to do a propagate here, too, since there could\n            # be backrefs to this pointer that technically reference\n            # us but will be fine if it is deleted.\n            schema = self._propagate_if_expr_refs(\n                schema,\n                context,\n                action=self.get_friendly_description(schema=schema),\n            )\n\n        return schema\n\n    def _canonicalize(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        scls: Pointer_T,\n    ) -> list[sd.Command]:\n        commands = super()._canonicalize(schema, context, scls)\n\n        # Any union type that references this field needs to have it\n        # deleted.\n        unions = schema.get_referrers(\n            self.scls, scls_type=Pointer, field_name='union_of')\n        for union in unions:\n            group, op, _ = union.init_delta_branch(\n                schema, context, sd.DeleteObject)\n            op.update(op._canonicalize(schema, context, union))\n            commands.append(group)\n\n        return commands\n\n\nclass SetPointerType[Pointer_T: Pointer](\n    referencing.ReferencedInheritingObjectCommand[Pointer_T],\n    inheriting.AlterInheritingObjectFragment[Pointer_T],\n    sd.AlterSpecialObjectField[Pointer_T],\n    PointerCommandOrFragment[Pointer_T],\n):\n\n    cast_expr = struct.Field(s_expr.Expression, default=None)\n\n    def get_verb(self) -> str:\n        return 'alter the type of'\n\n    def is_data_safe(self) -> bool:\n        # A computed target means this must be an inferred computed\n        # property, so it is data safe.\n        return self.is_attribute_computed('target')\n\n    def record_diff_annotations(\n        self,\n        *,\n        schema: s_schema.Schema,\n        orig_schema: Optional[s_schema.Schema],\n        context: so.ComparisonContext,\n        object: Optional[so.Object],\n        orig_object: Optional[so.Object],\n    ) -> None:\n        super().record_diff_annotations(\n            schema=schema,\n            orig_schema=orig_schema,\n            context=context,\n            orig_object=orig_object,\n            object=object,\n        )\n\n        if orig_schema is None:\n            return\n\n        if not context.generate_prompts:\n            return\n\n        old_type_shell = self.get_orig_attribute_value('target')\n        new_type_shell = self.get_attribute_value('target')\n\n        assert isinstance(old_type_shell, s_types.TypeShell)\n        assert isinstance(new_type_shell, s_types.TypeShell)\n\n        old_type: Optional[s_types.Type] = None\n\n        try:\n            old_type = old_type_shell.resolve(schema)\n        except errors.InvalidReferenceError:\n            # The original type does not exist in the new schema,\n            # which means either of the two things:\n            # 1) the original type is a collection, in which case we can\n            #    attempt to temporarily recreate it in the new schema to\n            #    check castability;\n            # 2) the original type is not a collection, and was removed\n            #    in the new schema; there is no way for us to infer\n            #    castability and we assume a cast expression is needed.\n            if isinstance(old_type_shell, s_types.CollectionTypeShell):\n                try:\n                    create = old_type_shell.as_create_delta(schema)\n                    schema = sd.apply(create, schema=schema)\n                except errors.InvalidReferenceError:\n                    # A removed type is part of the collection,\n                    # can't do anything about that.\n                    pass\n                else:\n                    old_type = old_type_shell.resolve(schema)\n\n        new_type = new_type_shell.resolve(schema)\n\n        assert len(context.parent_ops) > 1\n        ptr_op = context.parent_ops[-1]\n        src_op = context.parent_ops[-2]\n        is_computable = bool(ptr_op.get_attribute_value('expr'))\n        needs_cast = (\n            old_type is None\n            or self._needs_cast_expr(\n                schema=schema,\n                ptr_op=ptr_op,\n                src_op=src_op,\n                old_type=old_type,\n                new_type=new_type,\n                is_computable=is_computable,\n            )\n        )\n\n        if needs_cast:\n            placeholder_name = context.get_placeholder('cast_expr')\n            desc = self.get_friendly_description(schema=schema)\n            prompt = f'Please specify a conversion expression to {desc}'\n            self.set_annotation('required_input', dict(\n                placeholder=placeholder_name,\n                prompt=prompt,\n                old_type=str(old_type.get_name(schema)) if old_type else None,\n                old_type_is_object=old_type and old_type.is_object_type(),\n                new_type=str(new_type.get_name(schema)),\n                new_type_is_object=new_type.is_object_type(),\n                pointer_name=self.get_displayname(),\n            ))\n\n            self.cast_expr = s_expr.Expression.from_ast(\n                qlast.Placeholder(name=placeholder_name),\n                schema,\n            )\n\n    def _is_endpoint_property(self) -> bool:\n        mcls = self.get_schema_metaclass()\n        shortname = mcls.get_shortname_static(self.classname)\n        quals = sn.quals_from_fullname(self.classname)\n        if not quals:\n            return False\n        else:\n            source = quals[0]\n            return (\n                sn.is_fullname(source)\n                and str(shortname) in {'__::source', '__::target'}\n            )\n\n    def _needs_cast_expr(\n        self,\n        *,\n        schema: s_schema.Schema,\n        ptr_op: sd.ObjectCommand[so.Object],\n        src_op: sd.ObjectCommand[so.Object],\n        old_type: s_types.Type,\n        new_type: s_types.Type,\n        is_computable: bool,\n    ) -> bool:\n        return (\n            not old_type.assignment_castable_to(new_type, schema)\n            and not is_computable\n            and not ptr_op.maybe_get_object_aux_data('from_alias')\n            and self.cast_expr is None\n            and not self._is_endpoint_property()\n            and not (\n                ptr_op.get_attribute_value('declared_overloaded')\n                or isinstance(src_op, sd.CreateObject)\n            )\n        )\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        from edb.ir import utils as irutils\n\n        orig_schema = schema\n        orig_rec = context.current().enable_recursion\n        context.current().enable_recursion = False\n        schema = super()._alter_begin(schema, context)\n        context.current().enable_recursion = orig_rec\n        scls = self.scls\n\n        vn = scls.get_verbosename(schema, with_parent=True)\n\n        orig_target = scls.get_target(orig_schema)\n        new_target = scls.get_target(schema)\n\n        if new_target is None:\n            # This will happen if `RESET TYPE` was called\n            # on a non-inherited type.\n            raise errors.SchemaError(\n                f'cannot RESET TYPE of {vn} because it is not inherited',\n                span=self.span,\n            )\n\n        if not context.canonical and orig_target != new_target:\n            assert orig_target is not None\n            assert new_target is not None\n            ptr_op = self.get_parent_op(context)\n            src_op = self.get_referrer_context_or_die(context).op\n\n            if self._needs_cast_expr(\n                schema=schema,\n                ptr_op=ptr_op,\n                src_op=src_op,\n                old_type=orig_target,\n                new_type=new_target,\n                is_computable=self.scls.is_pure_computable(schema),\n            ):\n                vn = scls.get_verbosename(schema, with_parent=True)\n                ot = orig_target.get_verbosename(schema)\n                nt = new_target.get_verbosename(schema)\n                raise errors.SchemaError(\n                    f'{vn} cannot be cast automatically from '\n                    f'{ot} to {nt}',\n                    hint=(\n                        'You might need to specify a conversion '\n                        'expression in a USING clause'\n                    ),\n                    span=self.span,\n                )\n\n            if self.cast_expr is not None:\n                vn = scls.get_verbosename(schema, with_parent=True)\n                self.cast_expr = self._compile_expr(\n                    schema=orig_schema,\n                    context=context,\n                    expr=self.cast_expr,\n                    target_as_singleton=True,\n                    singleton_result_expected=True,\n                    no_query_rewrites=True,\n                    expr_description=(\n                        f'the USING clause for the alteration of {vn}'\n                    ),\n                )\n\n                using_type = self.cast_expr.stype\n                if not using_type.assignment_castable_to(\n                    new_target,\n                    self.cast_expr.schema,\n                ):\n                    ot = using_type.get_verbosename(self.cast_expr.schema)\n                    nt = new_target.get_verbosename(schema)\n                    raise errors.SchemaError(\n                        f'result of USING clause for the alteration of '\n                        f'{vn} cannot be cast automatically from '\n                        f'{ot} to {nt} ',\n                        hint='You might need to add an explicit cast.',\n                        span=self.span,\n                    )\n                if using_type.is_view(self.cast_expr.schema):\n                    raise errors.SchemaError(\n                        f'result of USING clause for the alteration of '\n                        f'{vn} may not include a shape',\n                        span=self.span,\n                    )\n\n                if irutils.contains_dml(self.cast_expr.ir_statement):\n                    raise errors.SchemaError(\n                        f'USING clause for the alteration of type of {vn} '\n                        f'cannot include mutating statements',\n                        span=self.span,\n                    )\n\n            schema = self._propagate_if_expr_refs(\n                schema,\n                context,\n                action=self.get_friendly_description(schema=schema),\n            )\n\n            if orig_target is not None and scls.is_property():\n                if cleanup_op := orig_target.as_type_delete_if_unused(schema):\n                    parent_op = self.get_parent_op(context)\n                    parent_op.add_caused(cleanup_op)\n\n        if not context.canonical:\n            if context.enable_recursion:\n                self._propagate_ref_field_alter_in_inheritance(\n                    schema,\n                    context,\n                    field_name='target',\n                )\n\n        return schema\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, SetPointerType)\n        if (\n            isinstance(astnode, qlast.SetPointerType)\n            and astnode.cast_expr is not None\n        ):\n            cmd.cast_expr = s_expr.Expression.from_ast(\n                astnode.cast_expr,\n                schema,\n                context.modaliases,\n                context.localnames,\n            )\n\n        return cmd\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        set_field = super()._get_ast(schema, context, parent_node=parent_node)\n        if set_field is None or self.is_attribute_computed('target'):\n            return None\n        else:\n            assert isinstance(set_field, qlast.SetField)\n            assert not isinstance(set_field.value, qlast.Expr)\n            return qlast.SetPointerType(\n                value=set_field.value,\n                cast_expr=(\n                    self.cast_expr.parse()\n                    if self.cast_expr is not None else None\n                )\n            )\n\n\nclass AlterPointerUpperCardinality[Pointer_T: Pointer](\n    referencing.ReferencedInheritingObjectCommand[Pointer_T],\n    inheriting.AlterInheritingObjectFragment[Pointer_T],\n    sd.AlterSpecialObjectField[Pointer_T],\n    PointerCommandOrFragment[Pointer_T],\n):\n    \"\"\"Handler for the \"cardinality\" field changes.\"\"\"\n\n    conv_expr = struct.Field(s_expr.Expression, default=None)\n\n    def get_friendly_description(\n        self,\n        *,\n        parent_op: Optional[sd.Command] = None,\n        schema: Optional[s_schema.Schema] = None,\n        object: Any = None,\n        object_desc: Optional[str] = None,\n    ) -> str:\n        object_desc = self.get_friendly_object_name_for_description(\n            parent_op=parent_op,\n            schema=schema,\n            object=object,\n            object_desc=object_desc,\n        )\n        new_card = self.get_attribute_value('cardinality')\n        if new_card is None:\n            # RESET CARDINALITY (to default)\n            new_card = qltypes.SchemaCardinality.One\n        return (\n            f\"convert {object_desc} to\"\n            f\" {new_card.as_ptr_qual()!r} cardinality\"\n        )\n\n    def is_data_safe(self) -> bool:\n        # A computed target means this must be an inferred computed\n        # property, so it is data safe.\n        if self.is_attribute_computed('cardinality'):\n            return True\n\n        old_val = self.get_orig_attribute_value('cardinality')\n        new_val = self.get_attribute_value('cardinality')\n        if (\n            old_val is qltypes.SchemaCardinality.Many\n            and new_val is qltypes.SchemaCardinality.One\n        ):\n            return False\n        else:\n            return True\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        schema = super()._alter_begin(schema, context)\n        scls = self.scls\n\n        orig_card = scls.get_cardinality(orig_schema)\n        new_card = scls.get_cardinality(schema)\n        is_computed = 'cardinality' in scls.get_computed_fields(schema)\n\n        if orig_card == new_card or is_computed:\n            # The actual value hasn't changed, nothing to do here.\n            return schema\n\n        if not context.canonical:\n            vn = scls.get_verbosename(schema, with_parent=True)\n            desc = self.get_friendly_description(schema=schema)\n            ptr_op = self.get_parent_op(context)\n            src_op = self.get_referrer_context_or_die(context).op\n\n            if self._needs_conv_expr(\n                schema=schema,\n                ptr_op=ptr_op,\n                src_op=src_op,\n            ):\n                vn = scls.get_verbosename(schema, with_parent=True)\n                raise errors.SchemaError(\n                    f'cannot automatically {desc}',\n                    hint=(\n                        'You need to specify a conversion '\n                        'expression in a USING clause'\n                    ),\n                    span=self.span,\n                )\n\n            if self.conv_expr is not None:\n                self.conv_expr = self._compile_expr(\n                    schema=orig_schema,\n                    context=context,\n                    expr=self.conv_expr,\n                    target_as_singleton=False,\n                    singleton_result_expected=True,\n                    no_query_rewrites=True,\n                    expr_description=(\n                        f'the USING clause for the alteration of {vn}'\n                    ),\n                )\n\n                using_type = self.conv_expr.stype\n                ptr_type = scls.get_target(schema)\n                assert ptr_type is not None\n                if not using_type.assignment_castable_to(\n                    ptr_type,\n                    self.conv_expr.schema,\n                ):\n                    ot = using_type.get_verbosename(self.conv_expr.schema)\n                    nt = ptr_type.get_verbosename(schema)\n                    raise errors.SchemaError(\n                        f'result of USING clause for the alteration of '\n                        f'{vn} cannot be cast automatically from '\n                        f'{ot} to {nt} ',\n                        hint='You might need to add an explicit cast.',\n                        span=self.span,\n                    )\n                if using_type.is_view(self.conv_expr.schema):\n                    raise errors.SchemaError(\n                        f'result of USING clause for the alteration of '\n                        f'{vn} may not include a shape',\n                        span=self.span,\n                    )\n\n            schema = self._propagate_if_expr_refs(schema, context, action=desc)\n            self._propagate_ref_field_alter_in_inheritance(\n                schema,\n                context,\n                field_name='cardinality',\n            )\n\n        return schema\n\n    def record_diff_annotations(\n        self,\n        *,\n        schema: s_schema.Schema,\n        orig_schema: Optional[s_schema.Schema],\n        context: so.ComparisonContext,\n        object: Optional[so.Object],\n        orig_object: Optional[so.Object],\n    ) -> None:\n        super().record_diff_annotations(\n            schema=schema,\n            orig_schema=orig_schema,\n            context=context,\n            orig_object=orig_object,\n            object=object,\n        )\n\n        if orig_schema is None:\n            return\n\n        if not context.generate_prompts:\n            return\n\n        assert len(context.parent_ops) > 1\n        ptr_op = context.parent_ops[-1]\n        src_op = context.parent_ops[-2]\n\n        needs_conv_expr = self._needs_conv_expr(\n            schema=schema,\n            ptr_op=ptr_op,\n            src_op=src_op,\n        )\n\n        if needs_conv_expr:\n            placeholder_name = context.get_placeholder('conv_expr')\n            desc = self.get_friendly_description(\n                schema=schema, parent_op=src_op)\n            prompt = (\n                f'Please specify an expression in order to {desc}'\n            )\n\n            type_name = _get_target_name_in_diff(\n                schema=schema, orig_schema=orig_schema,\n                object=object, orig_object=orig_object,\n            )\n            self.set_annotation('required_input', dict(\n                placeholder=placeholder_name,\n                prompt=prompt,\n                type=str(type_name),\n                pointer_name=self.get_displayname(),\n            ))\n\n            self.conv_expr = s_expr.Expression.from_ast(\n                qlast.Placeholder(name=placeholder_name),\n                schema,\n            )\n\n    def _needs_conv_expr(\n        self,\n        *,\n        schema: s_schema.Schema,\n        ptr_op: sd.ObjectCommand[so.Object],\n        src_op: sd.ObjectCommand[so.Object],\n    ) -> bool:\n        old_card = (\n            self.get_orig_attribute_value('cardinality')\n            or qltypes.SchemaCardinality.One\n        )\n        new_card = (\n            self.get_attribute_value('cardinality')\n            or qltypes.SchemaCardinality.One\n        )\n        return (\n            old_card is qltypes.SchemaCardinality.Many\n            and new_card is qltypes.SchemaCardinality.One\n            and not self.is_attribute_computed('cardinality')\n            and not self.is_attribute_inherited('cardinality')\n            and not ptr_op.maybe_get_object_aux_data('from_alias')\n            and self.conv_expr is None\n            and not (\n                ptr_op.get_attribute_value('expr')\n                or ptr_op.get_orig_attribute_value('expr')\n            )\n            and not (\n                ptr_op.get_attribute_value('declared_overloaded')\n                or isinstance(src_op, sd.CreateObject)\n            )\n        )\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, AlterPointerUpperCardinality)\n        if (\n            isinstance(astnode, qlast.SetPointerCardinality)\n            and astnode.conv_expr is not None\n        ):\n            cmd.conv_expr = s_expr.Expression.from_ast(\n                astnode.conv_expr,\n                schema,\n                context.modaliases,\n                context.localnames,\n            )\n\n        return cmd\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        set_field = super()._get_ast(schema, context, parent_node=parent_node)\n        if set_field is None:\n            return None\n        else:\n            assert isinstance(set_field, qlast.SetField)\n            return qlast.SetPointerCardinality(\n                value=set_field.value,\n                conv_expr=(\n                    self.conv_expr.parse()\n                    if self.conv_expr is not None else None\n                )\n            )\n\n\nclass AlterPointerLowerCardinality[Pointer_T: Pointer](\n    referencing.ReferencedInheritingObjectCommand[Pointer_T],\n    inheriting.AlterInheritingObjectFragment[Pointer_T],\n    sd.AlterSpecialObjectField[Pointer_T],\n    PointerCommandOrFragment[Pointer_T],\n):\n    \"\"\"Handler for the \"required\" field changes.\"\"\"\n\n    fill_expr = struct.Field(s_expr.Expression, default=None)\n\n    def get_friendly_description(\n        self,\n        *,\n        parent_op: Optional[sd.Command] = None,\n        schema: Optional[s_schema.Schema] = None,\n        object: Any = None,\n        object_desc: Optional[str] = None,\n    ) -> str:\n        object_desc = self.get_friendly_object_name_for_description(\n            parent_op=parent_op,\n            schema=schema,\n            object=object,\n            object_desc=object_desc,\n        )\n        required = self.get_attribute_value('required')\n        return f\"make {object_desc} {'required' if required else 'optional'}\"\n\n    def is_data_safe(self) -> bool:\n        return True\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        from edb.ir import utils as irutils\n\n        orig_schema = schema\n        schema = super()._alter_begin(schema, context)\n        scls = self.scls\n\n        orig_required = scls.get_required(orig_schema)\n        new_required = scls.get_required(schema)\n        new_card = scls.get_cardinality(schema)\n        is_computed = 'required' in scls.get_computed_fields(schema)\n\n        if orig_required == new_required or is_computed:\n            # The actual value hasn't changed, nothing to do here.\n            return schema\n\n        if not context.canonical:\n            vn = scls.get_verbosename(schema, with_parent=True)\n\n            if self.fill_expr is not None:\n                self.fill_expr = self._compile_expr(\n                    schema=orig_schema,\n                    context=context,\n                    expr=self.fill_expr,\n                    target_as_singleton=True,\n                    singleton_result_expected=new_card.is_single(),\n                    no_query_rewrites=True,\n                    expr_description=(\n                        f'the USING clause for the alteration of {vn}'\n                    ),\n                )\n\n                using_type = self.fill_expr.stype\n                ptr_type = scls.get_target(schema)\n                assert ptr_type is not None\n                if not using_type.assignment_castable_to(\n                    ptr_type,\n                    self.fill_expr.schema,\n                ):\n                    ot = using_type.get_verbosename(self.fill_expr.schema)\n                    nt = ptr_type.get_verbosename(schema)\n                    raise errors.SchemaError(\n                        f'result of USING clause for the alteration of '\n                        f'{vn} cannot be cast automatically from '\n                        f'{ot} to {nt} ',\n                        hint='You might need to add an explicit cast.',\n                        span=self.span,\n                    )\n                if using_type.is_view(self.fill_expr.schema):\n                    raise errors.SchemaError(\n                        f'result of USING clause for the alteration of '\n                        f'{vn} may not include a shape',\n                        span=self.span,\n                    )\n\n                if (\n                    self.scls.is_link_property(schema)\n                    and irutils.contains_dml(self.fill_expr.ir_statement)\n                ):\n                    raise errors.UnsupportedFeatureError(\n                        f'USING clause for the alteration of optionality of '\n                        f'{vn} cannot include mutating statements, '\n                        'because it is a link property',\n                        span=self.span,\n                    )\n\n            schema = self._propagate_if_expr_refs(\n                schema,\n                context,\n                action=(\n                    f'make {vn} {\"required\" if new_required else \"optional\"}'\n                ),\n            )\n\n        return schema\n\n    def record_diff_annotations(\n        self,\n        *,\n        schema: s_schema.Schema,\n        orig_schema: Optional[s_schema.Schema],\n        context: so.ComparisonContext,\n        object: Optional[so.Object],\n        orig_object: Optional[so.Object],\n    ) -> None:\n        super().record_diff_annotations(\n            schema=schema,\n            orig_schema=orig_schema,\n            context=context,\n            orig_object=orig_object,\n            object=object,\n        )\n\n        if not context.generate_prompts:\n            return\n\n        if len(context.parent_ops) <= 1:\n            return\n\n        ptr_op = context.parent_ops[-1]\n        src_op = context.parent_ops[-2]\n\n        needs_fill_expr = self._needs_fill_expr(\n            schema=schema,\n            ptr_op=ptr_op,\n            src_op=src_op,\n        )\n\n        if needs_fill_expr:\n            placeholder_name = context.get_placeholder('fill_expr')\n            desc = self.get_friendly_description(\n                schema=schema, parent_op=src_op)\n            prompt = (\n                f'Please specify an expression to populate existing objects '\n                f'in order to {desc}'\n            )\n\n            type_name = _get_target_name_in_diff(\n                schema=schema, orig_schema=orig_schema,\n                object=object, orig_object=orig_object,\n            )\n\n            self.set_annotation('required_input', dict(\n                placeholder=placeholder_name,\n                prompt=prompt,\n                type=str(type_name),\n                pointer_name=self.get_displayname(),\n            ))\n\n            self.fill_expr = s_expr.Expression.from_ast(\n                qlast.Placeholder(name=placeholder_name),\n                schema,\n            )\n\n    def _needs_fill_expr(\n        self,\n        *,\n        schema: s_schema.Schema,\n        ptr_op: sd.ObjectCommand[so.Object],\n        src_op: sd.ObjectCommand[so.Object],\n    ) -> bool:\n        old_required = self.get_orig_attribute_value('required') or False\n        new_required = self.get_attribute_value('required') or False\n        return (\n            not old_required and new_required\n            and not self.is_attribute_computed('required')\n            and not ptr_op.maybe_get_object_aux_data('from_alias')\n            and self.fill_expr is None\n            and not (\n                ptr_op.get_attribute_value('declared_overloaded')\n                or isinstance(src_op, sd.CreateObject)\n            )\n        )\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, AlterPointerLowerCardinality)\n        if (\n            isinstance(astnode, qlast.SetPointerOptionality)\n            and astnode.fill_expr is not None\n        ):\n            cmd.fill_expr = s_expr.Expression.from_ast(\n                astnode.fill_expr,\n                schema,\n                context.modaliases,\n                context.localnames,\n            )\n\n        return cmd\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        set_field = super()._get_ast(schema, context, parent_node=parent_node)\n        if set_field is None and not self.fill_expr:\n            return None\n        else:\n            if set_field is not None:\n                assert isinstance(set_field, qlast.SetField)\n                value = set_field.value\n            else:\n                req = self.get_attribute_value('required')\n                value = (utils.const_ast_from_python(req) if req is not None\n                         else None)\n\n            return qlast.SetPointerOptionality(\n                value=value,\n                fill_expr=(\n                    self.fill_expr.parse()\n                    if self.fill_expr is not None else None\n                )\n            )\n\n\ndef get_or_create_union_pointer(\n    schema: s_schema.Schema,\n    ptrname: sn.UnqualName,\n    source: s_sources.Source,\n    direction: PointerDirection,\n    components: Iterable[Pointer],\n    *,\n    transient: bool = False,\n    opaque: bool = False,\n    modname: Optional[str] = None,\n) -> tuple[s_schema.Schema, Pointer]:\n    from . import sources as s_sources\n\n    components = list(components)\n\n    if len(components) == 1 and direction is PointerDirection.Outbound:\n        return schema, components[0]\n\n    # We want to transform all the computables in the list of the\n    # components to their respective owned computables. This is to\n    # ensure that mixing multiple inherited copies of the same\n    # computable is actually allowed.\n    comp_set = set()\n    for c in components:\n        if c.is_pure_computable(schema):\n            comp_set.add(_get_nearest_owned(schema, c))\n        else:\n            comp_set.add(c)\n    components = list(comp_set)\n\n    if (\n        any(p.is_pure_computable(schema) for p in components)\n        and len(components) > 1\n        and ptrname.name not in ('__tname__', '__tid__')\n    ):\n        p = components[0]\n        raise errors.SchemaError(\n            f'it is illegal to create a type union that causes '\n            f'a computed {p.get_verbosename(schema)} to mix '\n            f'with other versions of the same {p.get_verbosename(schema)}',\n        )\n\n    if len(components) == 1 and direction is PointerDirection.Outbound:\n        return schema, components[0]\n\n    far_endpoints = [\n        p.get_far_endpoint(schema, direction)\n        for p in components\n    ]\n    targets: Sequence[s_types.Type] = [\n        p for p in far_endpoints\n        if isinstance(p, s_types.Type)\n    ]\n    targets = utils.simplify_union_types(schema, targets)\n\n    target: s_types.Type\n\n    schema, target, _ = utils.ensure_union_type(\n        schema, targets, opaque=opaque, module=modname, transient=transient)\n\n    cardinality = qltypes.SchemaCardinality.One\n    for component in components:\n        if component.get_cardinality(schema) is qltypes.SchemaCardinality.Many:\n            cardinality = qltypes.SchemaCardinality.Many\n            break\n\n    required = all(component.get_required(schema) for component in components)\n    metacls = type(components[0])\n    default_base_name = metacls.get_default_base_name()\n    assert default_base_name is not None\n    genptr = schema.get(default_base_name, type=Pointer)\n\n    if direction is PointerDirection.Inbound:\n        # type ignore below, because the types \"Type\" and \"Source\"\n        # could only be swapped by their common ancestor so.Object,\n        # and here we are considering them both as more specific objects\n        source, target = target, source  # type: ignore\n\n    schema, result = genptr.get_derived(\n        schema,\n        source,\n        target,\n        derived_name_base=sn.QualName(module='__', name=ptrname.name),\n        attrs={\n            'union_of': so.ObjectSet.create(schema, components),\n            'cardinality': cardinality,\n            'required': required,\n        },\n        transient=transient,\n    )\n\n    if isinstance(result, s_sources.Source) and not opaque:\n        # cast below, because in this case the list of Pointer\n        # is also a list of Source (links.Link)\n        schema = s_sources.populate_pointer_set_for_source_union(\n            schema,\n            cast(list[s_sources.Source], components),\n            result,\n            modname=modname,\n        )\n\n    return schema, result\n\n\ndef _get_nearest_owned(\n    schema: s_schema.Schema,\n    pointer: Pointer,\n) -> Pointer:\n    if pointer.get_owned(schema):\n        return pointer\n\n    for p in pointer.get_ancestors(schema).objects(schema):\n        if p.get_owned(schema):\n            return p\n\n    return pointer\n\n\ndef get_or_create_intersection_pointer(\n    schema: s_schema.Schema,\n    ptrname: sn.UnqualName,\n    source: s_objtypes.ObjectType,\n    components: Iterable[Pointer], *,\n    modname: Optional[str] = None,\n    transient: bool = False,\n) -> tuple[s_schema.Schema, Pointer]:\n\n    components = list(components)\n\n    if len(components) == 1:\n        return schema, components[0]\n\n    required = any(component.get_required(schema) for component in components)\n    targets: Sequence[s_types.Type]\n    targets = list(filter(None, [p.get_target(schema) for p in components]))\n    targets = utils.simplify_intersection_types(schema, targets)\n    schema, target, _ = utils.ensure_intersection_type(\n        schema, targets, module=modname)\n\n    cardinality = qltypes.SchemaCardinality.Many\n    for component in components:\n        if component.get_cardinality(schema) is qltypes.SchemaCardinality.One:\n            cardinality = qltypes.SchemaCardinality.One\n            break\n\n    metacls = type(components[0])\n    default_base_name = metacls.get_default_base_name()\n    assert default_base_name is not None\n    genptr = schema.get(default_base_name, type=Pointer)\n\n    schema, result = genptr.get_derived(\n        schema,\n        source,\n        target,\n        derived_name_base=sn.QualName(module='__', name=ptrname.name),\n        attrs={\n            'intersection_of': so.ObjectSet.create(schema, components),\n            'cardinality': cardinality,\n            'required': required,\n        },\n        transient=transient,\n    )\n\n    # We want to transform all the computables in the list of the\n    # components to their respective owned computables. This is to\n    # ensure that mixing multiple inherited copies of the same\n    # computable is actually allowed.\n    comp_set = set()\n    for c in components:\n        if c.is_pure_computable(schema):\n            comp_set.add(_get_nearest_owned(schema, c))\n        else:\n            comp_set.add(c)\n    components = list(comp_set)\n\n    if (\n        any(p.is_pure_computable(schema) for p in components)\n        and len(components) > 1\n        and ptrname.name not in ('__tname__', '__tid__')\n    ):\n        p = components[0]\n        raise errors.SchemaError(\n            f'it is illegal to create a type intersection that causes '\n            f'a computed {p.get_verbosename(schema)} to mix '\n            f'with other versions of the same {p.get_verbosename(schema)}',\n        )\n\n    if len({p.get_cardinality(schema) for p in components}) > 1:\n        p = components[0]\n        raise errors.SchemaError(\n            f'it is illegal to create a type intersection that causes '\n            f'a {p.get_verbosename(schema)} to mix '\n            f'with other versions of {p.get_verbosename(schema)} '\n            f'which have a different cardinality',\n        )\n\n    return schema, result\n\n\n@s_futures.register_handler('no_linkful_computed_splats')\ndef toggle_no_linkful_computed_splats(\n    cmd: s_futures.FutureBehaviorCommand,\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    on: bool,\n) -> tuple[s_schema.Schema, sd.Command]:\n    # Nothing to do because splats can't appear in functions\n    group = sd.CommandGroup()\n    return schema, group\n"
  },
  {
    "path": "edb/schema/policies.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, TYPE_CHECKING\n\nfrom edb import errors\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.edgeql import qltypes\n\nfrom . import annos as s_anno\nfrom . import delta as sd\nfrom . import expr as s_expr\nfrom . import futures as s_futures\nfrom . import name as sn\nfrom . import objects as so\nfrom . import properties as s_props\nfrom . import referencing\nfrom . import schema as s_schema\nfrom . import sources as s_sources\nfrom . import types as s_types\n\nif TYPE_CHECKING:\n    from . import objtypes as s_objtypes\n\n\nclass AccessPolicy(\n    referencing.NamedReferencedInheritingObject,\n    so.InheritingObject,  # Help reflection figure out the right db MRO\n    s_anno.AnnotationSubject,\n    qlkind=qltypes.SchemaObjectClass.ACCESS_POLICY,\n    data_safe=True,\n):\n\n    condition = so.SchemaField(\n        s_expr.Expression,\n        default=None,\n        coerce=True,\n        compcoef=0.909,\n        special_ddl_syntax=True,\n    )\n\n    expr = so.SchemaField(\n        s_expr.Expression,\n        default=None,\n        compcoef=0.909,\n        special_ddl_syntax=True,\n    )\n\n    action = so.SchemaField(\n        qltypes.AccessPolicyAction,\n        coerce=True,\n        compcoef=0.85,\n        special_ddl_syntax=True,\n    )\n\n    access_kinds = so.SchemaField(\n        so.MultiPropSet[qltypes.AccessKind],\n        coerce=True,\n        compcoef=0.85,\n        special_ddl_syntax=True,\n    )\n\n    subject = so.SchemaField(\n        so.InheritingObject,\n        compcoef=None,\n        inheritable=False)\n\n    errmessage = so.SchemaField(\n        str, default=None, compcoef=0.971, allow_ddl_set=True\n    )\n\n    # We don't support SET/DROP OWNED owned on policies so we set its\n    # compcoef to 0.0\n    owned = so.SchemaField(\n        bool,\n        default=False,\n        inheritable=False,\n        compcoef=0.0,\n        reflection_method=so.ReflectionMethod.AS_LINK,\n        special_ddl_syntax=True,\n    )\n\n    def get_expr_refs(self, schema: s_schema.Schema) -> list[so.Object]:\n        objs: list[so.Object] = []\n        if (condition := self.get_condition(schema)) and condition.refs:\n            objs.extend(condition.refs.objects(schema))\n        if (expr := self.get_expr(schema)) and expr.refs:\n            objs.extend(expr.refs.objects(schema))\n        return objs\n\n    def get_subject(self, schema: s_schema.Schema) -> s_objtypes.ObjectType:\n        subj: s_objtypes.ObjectType = self.get_field_value(schema, 'subject')\n        return subj\n\n    def get_original_subject(\n        self, schema: s_schema.Schema\n    ) -> s_objtypes.ObjectType:\n        ancs = (self,) + self.get_ancestors(schema).objects(schema)\n        return ancs[-1].get_subject(schema)\n\n\nclass AccessPolicyCommandContext(\n    sd.ObjectCommandContext[AccessPolicy],\n    s_anno.AnnotationSubjectCommandContext,\n):\n    pass\n\n\nclass AccessPolicySourceCommandContext[Source_T: s_sources.Source](\n    s_sources.SourceCommandContext[Source_T]\n):\n    pass\n\n\nclass AccessPolicyCommand(\n    referencing.NamedReferencedInheritingObjectCommand[AccessPolicy],\n    s_anno.AnnotationSubjectCommand[AccessPolicy],\n    context_class=AccessPolicyCommandContext,\n    referrer_context_class=AccessPolicySourceCommandContext,\n):\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n\n        parent_ctx = self.get_referrer_context_or_die(context)\n        source = parent_ctx.op.scls\n        pol_name = self.get_verbosename(parent=source.get_verbosename(schema))\n\n        for field in ('expr', 'condition'):\n            if (expr := self.get_local_attribute_value(field)) is None:\n                continue\n\n            vname = 'when' if field == 'condition' else 'using'\n\n            expression = self.compile_expr_field(\n                schema, context,\n                field=AccessPolicy.get_field(field),\n                value=expr,\n            )\n\n            span = self.get_attribute_span(field)\n\n            if expression.irast.cardinality.can_be_zero():\n                raise errors.SchemaDefinitionError(\n                    f'possibly an empty set returned by {vname} '\n                    f'expression for the {pol_name} ',\n                    span=span\n                )\n\n            if expression.irast.cardinality.is_multi():\n                raise errors.SchemaDefinitionError(\n                    f'possibly more than one element returned by {vname} '\n                    f'expression for the {pol_name} ',\n                    span=span\n                )\n\n            if expression.irast.volatility.is_volatile():\n                raise errors.SchemaDefinitionError(\n                    f'{pol_name} has a volatile {vname} expression, '\n                    f'which is not allowed',\n                    span=span\n                )\n\n            target = schema.get(sn.QualName('std', 'bool'), type=s_types.Type)\n            expr_type = expression.irast.stype\n            if not expr_type.issubclass(expression.irast.schema, target):\n                span = self.get_attribute_span(field)\n                raise errors.SchemaDefinitionError(\n                    f'{vname} expression for {pol_name} is of invalid type: '\n                    f'{expr_type.get_displayname(expression.irast.schema)}, '\n                    f'expected {target.get_displayname(schema)}',\n                    span=self.span,\n                )\n\n        return schema\n\n    def compile_expr_field(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: s_expr.Expression,\n        track_schema_ref_exprs: bool=False,\n    ) -> s_expr.CompiledExpression:\n        if field.name in {'expr', 'condition'}:\n            parent_ctx = self.get_referrer_context_or_die(context)\n            source = parent_ctx.op.get_object(schema, context)\n            parent_vname = source.get_verbosename(schema)\n            pol_name = self.get_verbosename(parent=parent_vname)\n            in_ddl_context_name = pol_name\n\n            assert isinstance(source, s_types.Type)\n\n            return type(value).compiled(\n                value,\n                schema=schema,\n                options=qlcompiler.CompilerOptions(\n                    modaliases=context.modaliases,\n                    schema_object_context=self.get_schema_metaclass(),\n                    anchors={'__subject__': source},\n                    path_prefix_anchor='__subject__',\n                    singletons=frozenset({source}),\n                    apply_query_rewrites=not context.stdmode,\n                    apply_user_access_policies=False,\n                    track_schema_ref_exprs=track_schema_ref_exprs,\n                    in_ddl_context_name=in_ddl_context_name,\n                    detached=True,\n                ),\n                context=context,\n            )\n        else:\n            return super().compile_expr_field(\n                schema, context, field, value, track_schema_ref_exprs)\n\n    def get_dummy_expr_field_value(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: Any,\n    ) -> Optional[s_expr.Expression]:\n        if field.name in {'expr', 'condition'}:\n            return s_expr.Expression(text='false')\n        else:\n            raise NotImplementedError(f'unhandled field {field.name!r}')\n\n    def validate_object(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        subject = self.scls.get_subject(schema)\n\n        for obj in self.scls.get_expr_refs(schema):\n            if isinstance(obj, s_props.Property):\n                # Disable access of link properties with default\n                # values from all the post-check DML changes. This is\n                # because linkprop default values currently come from\n                # the postgres side, so we don't have access to them\n                # before actually doing the link table inserts.\n                # TODO: Fix this.\n                if (\n                    obj.get_source(schema)\n                    and obj.is_link_property(schema)\n                    and obj.get_default(schema)\n                    and any(\n                        kind.is_data_check()\n                        for kind in self.scls.get_access_kinds(schema)\n                    )\n                ):\n                    pol_name = self.get_verbosename(\n                        parent=subject.get_verbosename(schema))\n                    obj_name = obj.get_verbosename(schema, with_parent=True)\n                    raise errors.UnsupportedFeatureError(\n                        f'insert and update write access policies may not '\n                        f'refer to link properties with default values: '\n                        f'{pol_name} refers to {obj_name}',\n                        span=self.span,\n                    )\n\n\nclass CreateAccessPolicy(\n    AccessPolicyCommand,\n    referencing.CreateReferencedInheritingObject[AccessPolicy],\n):\n    referenced_astnode = astnode = qlast.CreateAccessPolicy\n\n    def get_ast_attr_for_field(\n        self,\n        field: str,\n        astnode: type[qlast.DDLOperation],\n    ) -> Optional[str]:\n        if (\n            field in ('expr', 'condition', 'action', 'access_kinds')\n            and issubclass(astnode, qlast.CreateAccessPolicy)\n        ):\n            return field\n        else:\n            return super().get_ast_attr_for_field(field, astnode)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        assert isinstance(astnode, qlast.CreateAccessPolicy)\n        assert isinstance(cmd, AccessPolicyCommand)\n\n        if astnode.condition is not None:\n            cmd.set_attribute_value(\n                'condition',\n                s_expr.Expression.from_ast(\n                    astnode.condition, schema, context.modaliases,\n                    context.localnames,\n                ),\n                span=astnode.condition.span,\n            )\n\n        if astnode.expr:\n            cmd.set_attribute_value(\n                'expr',\n                s_expr.Expression.from_ast(\n                    astnode.expr, schema, context.modaliases,\n                    context.localnames,\n                ),\n                span=astnode.expr.span,\n            )\n\n        cmd.set_attribute_value('action', astnode.action)\n        cmd.set_attribute_value('access_kinds', astnode.access_kinds)\n\n        return cmd\n\n\nclass RenameAccessPolicy(\n    AccessPolicyCommand,\n    referencing.RenameReferencedInheritingObject[AccessPolicy],\n):\n    pass\n\n\nclass RebaseAccessPolicy(\n    AccessPolicyCommand,\n    referencing.RebaseReferencedInheritingObject[AccessPolicy],\n):\n    pass\n\n\nclass AlterAccessPolicy(\n    AccessPolicyCommand,\n    referencing.AlterReferencedInheritingObject[AccessPolicy],\n):\n    referenced_astnode = astnode = qlast.AlterAccessPolicy\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._alter_begin(schema, context)\n\n        # If either action or access_kinds appears, make sure the\n        # other one does as well, so that _apply_field_ast has\n        # a canonical setup to work with.\n        if (\n            self.has_attribute_value('action')\n            and not self.has_attribute_value('access_kinds')\n        ):\n            self.set_attribute_value(\n                'access_kinds', self.scls.get_access_kinds(schema))\n        elif (\n            self.has_attribute_value('access_kinds')\n            and not self.has_attribute_value('action')\n        ):\n            self.set_attribute_value('action', self.scls.get_action(schema))\n\n        # TODO: We may wish to support this in the future but it will\n        # take some thought.\n        if (\n            self.get_attribute_value('owned')\n            and not self.get_orig_attribute_value('owned')\n        ):\n            raise errors.SchemaDefinitionError(\n                f'cannot alter the definition of inherited access policy '\n                f'{self.scls.get_displayname(schema)}',\n                span=self.span\n            )\n\n        return schema\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        if op.property == 'action':\n            pass\n        elif op.property == 'access_kinds':\n            node.commands.append(\n                qlast.SetAccessPerms(\n                    action=self.get_attribute_value('action'),\n                    access_kinds=op.new_value,\n                )\n            )\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n\n# This is kind of a hack: we never actually instantiate this class, we\n# just use its _cmd_tree_from_ast to produce a command group with two\n# property sets.\nclass AlterAccessPolicyPerms(\n    referencing.ReferencedInheritingObjectCommand[AccessPolicy],\n    referrer_context_class=AccessPolicyCommandContext,\n):\n    referenced_astnode = astnode = qlast.SetAccessPerms\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        assert isinstance(astnode, qlast.SetAccessPerms)\n        cmd = sd.CommandGroup()\n        cmd.add(\n            sd.AlterObjectProperty(\n                property='action',\n                new_value=astnode.action,\n                span=astnode.span,\n            )\n        )\n        cmd.add(\n            sd.AlterObjectProperty(\n                property='access_kinds',\n                new_value=astnode.access_kinds,\n                span=astnode.span,\n            )\n        )\n        return cmd\n\n\nclass DeleteAccessPolicy(\n    AccessPolicyCommand,\n    referencing.DeleteReferencedInheritingObject[AccessPolicy],\n):\n    referenced_astnode = astnode = qlast.DropAccessPolicy\n\n\n@s_futures.register_handler('nonrecursive_access_policies')\ndef toggle_nonrecursive_access_policies(\n    cmd: s_futures.FutureBehaviorCommand,\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    on: bool,\n) -> tuple[s_schema.Schema, sd.Command]:\n    # Nothing to do anymore\n    group = sd.CommandGroup()\n    return schema, group\n"
  },
  {
    "path": "edb/schema/properties.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Any, Optional, TYPE_CHECKING\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom edb import errors\n\nfrom . import constraints\nfrom . import delta as sd\nfrom . import inheriting\nfrom . import name as sn\nfrom . import objects as so\nfrom . import pointers\nfrom . import referencing\nfrom . import rewrites as s_rewrites\nfrom . import sources as s_sources\nfrom . import types as s_types\nfrom . import utils\nfrom . import expr as s_expr\n\nif TYPE_CHECKING:\n    from . import schema as s_schema\n\n\nclass Property(\n    pointers.Pointer,\n    qlkind=qltypes.SchemaObjectClass.PROPERTY,\n    data_safe=False,\n):\n\n    def derive_ref(\n        self,\n        schema: s_schema.Schema,\n        referrer: so.QualifiedObject,\n        *qualifiers: str,\n        target: Optional[s_types.Type] = None,\n        attrs: Optional[dict[str, Any]] = None,\n        **kwargs: Any,\n    ) -> tuple[s_schema.Schema, Property]:\n        from . import links as s_links\n        if target is None:\n            target = self.get_target(schema)\n\n        schema, ptr = super().derive_ref(\n            schema, referrer, target=target, attrs=attrs, **kwargs)\n\n        ptr_sn = str(ptr.get_shortname(schema))\n\n        if ptr_sn == 'std::source':\n            assert isinstance(referrer, s_links.Link)\n            schema = ptr.set_field_value(\n                schema, 'target', referrer.get_source(schema))\n        elif ptr_sn == 'std::target':\n            schema = ptr.set_field_value(\n                schema, 'target', referrer.get_field_value(schema, 'target'))\n\n        assert isinstance(ptr, Property)\n        return schema, ptr\n\n    def compare(\n        self,\n        other: so.Object,\n        *,\n        our_schema: s_schema.Schema,\n        their_schema: s_schema.Schema,\n        context: so.ComparisonContext,\n    ) -> float:\n        if not isinstance(other, Property):\n            if isinstance(other, pointers.Pointer):\n                return 0.0\n            else:\n                raise NotImplementedError\n\n        similarity = super().compare(\n            other, our_schema=our_schema,\n            their_schema=their_schema, context=context)\n\n        if (\n            not self.is_non_concrete(our_schema)\n            and not other.is_non_concrete(their_schema)\n            and self.issubclass(\n                our_schema, our_schema.get('std::source', type=Property)\n            )\n            and other.issubclass(\n                their_schema, their_schema.get('std::source', type=Property)\n            )\n        ):\n            # Make std::source link property ignore differences in its target.\n            # This is consistent with skipping the comparison on Pointer.source\n            # in general.\n            field = self.__class__.get_field('target')\n            target_coef = field.type.compare_values(\n                self.get_target(our_schema),\n                other.get_target(their_schema),\n                our_schema=our_schema,\n                their_schema=their_schema,\n                context=context,\n                compcoef=field.compcoef)\n            if target_coef < 1:\n                similarity *= target_coef\n        return similarity\n\n    def should_propagate(self, schema: s_schema.Schema) -> bool:\n        # @source and @target link props don't propagate to children\n        # because we create new properties with distinct types.\n        return not self.is_endpoint_pointer(schema)\n\n    @classmethod\n    def is_property(cls, schema: Optional[s_schema.Schema]=None) -> bool:\n        return True\n\n    def has_user_defined_properties(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_link_property(self, schema: s_schema.Schema) -> bool:\n        source = self.get_source(schema)\n        if source is None:\n            return False\n        return isinstance(source, pointers.Pointer)\n\n    def allow_ref_propagation(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        refdict: so.RefDict,\n    ) -> bool:\n        source = self.get_source(schema)\n        if isinstance(source, pointers.Pointer):\n            if source.is_non_concrete(schema):\n                return True\n            else:\n                source = source.get_source(schema)\n                assert isinstance(source, s_types.Type)\n                return not source.is_view(schema)\n        else:\n            assert isinstance(source, s_types.Type)\n            return not source.is_view(schema)\n\n    @classmethod\n    def get_root_classes(cls) -> tuple[sn.QualName, ...]:\n        return (\n            sn.QualName(module='std', name='property'),\n        )\n\n    @classmethod\n    def get_default_base_name(self) -> sn.QualName:\n        return sn.QualName('std', 'property')\n\n    def is_blocking_ref(\n        self,\n        schema: s_schema.Schema,\n        reference: so.Object,\n    ) -> bool:\n        return not self.is_endpoint_pointer(schema)\n\n    def init_delta_command[\n        ObjectCommand_T: sd.ObjectCommand[so.Object]\n    ](\n        self,\n        schema: s_schema.Schema,\n        cmdtype: type[ObjectCommand_T],\n        *,\n        classname: Optional[sn.Name] = None,\n        **kwargs: Any,\n    ) -> ObjectCommand_T:\n        delta = super().init_delta_command(\n            schema=schema,\n            cmdtype=cmdtype,\n            classname=classname,\n            **kwargs,\n        )\n        assert isinstance(delta, referencing.ReferencedObjectCommandBase)\n        delta.is_strong_ref = self.is_special_pointer(schema)\n        return delta  # type: ignore\n\n\nclass PropertySourceContext[Source_T: s_sources.Source](\n    s_sources.SourceCommandContext[Source_T]\n):\n    pass\n\n\nclass PropertySourceCommand[Source_T: s_sources.Source](\n    inheriting.InheritingObjectCommand[Source_T],\n):\n    pass\n\n\nclass PropertyCommandContext(\n    pointers.PointerCommandContext,\n    constraints.ConsistencySubjectCommandContext,\n    s_rewrites.RewriteCommandContext,\n):\n    pass\n\n\nclass PropertyCommand(\n    pointers.PointerCommand[Property],\n    context_class=PropertyCommandContext,\n    referrer_context_class=PropertySourceContext,\n):\n\n    def validate_object(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        \"\"\"Check that property definition is sound.\"\"\"\n        super().validate_object(schema, context)\n\n        scls = self.scls\n        if not scls.get_owned(schema):\n            return\n\n        if scls.is_special_pointer(schema):\n            return\n\n        if (\n            scls.is_link_property(schema)\n            and not scls.is_pure_computable(schema)\n        ):\n            # link properties cannot be multi\n            if (self.get_attribute_value('cardinality')\n                    is qltypes.SchemaCardinality.Many):\n                raise errors.InvalidPropertyDefinitionError(\n                    \"multi properties aren't supported for links\",\n                    span=self.span,\n                )\n\n        target_type = scls.get_target(schema)\n        if target_type is None:\n            raise TypeError(f'missing target type in scls {scls}')\n\n        if target_type.is_polymorphic(schema):\n            span = self.get_attribute_span('target')\n            raise errors.InvalidPropertyTargetError(\n                f'invalid property type: '\n                f'{target_type.get_verbosename(schema)} '\n                f'is a generic type',\n                span=span,\n            )\n\n        if (target_type.is_object_type()\n                or (isinstance(target_type, s_types.Collection)\n                    and target_type.contains_object(schema))):\n            span = self.get_attribute_span('target')\n            raise errors.InvalidPropertyTargetError(\n                f'invalid property type: expected a scalar type, '\n                f'or a scalar collection, got '\n                f'{target_type.get_verbosename(schema)}',\n                span=span,\n            )\n\n    def _check_field_errors(self, node: qlast.DDLOperation) -> None:\n        for sub in node.commands:\n            # do not allow link property on properties\n            if isinstance(sub, qlast.CreateConcretePointer):\n                raise errors.InvalidDefinitionError(\n                    f'cannot place a link property on a property',\n                    span=node.span,\n                    hint=(\n                        'Link properties can only be placed on links, whose '\n                        'target types are object types.'\n                    ),\n                )\n            # do not allow on source/target delete on properties\n            if isinstance(sub, (qlast.OnSourceDelete, qlast.OnTargetDelete)):\n                raise errors.InvalidDefinitionError(\n                    f'cannot place a deletion policy on a property',\n                    span=node.span,\n                    hint=(\n                        'Deletion policies can only be placed on links, whose '\n                        'target types are object types.'\n                    ),\n                )\n\n\nclass CreateProperty(\n    PropertyCommand,\n    pointers.CreatePointer[Property],\n):\n    astnode = [qlast.CreateConcreteProperty,\n               qlast.CreateProperty]\n\n    referenced_astnode = qlast.CreateConcreteProperty\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        if isinstance(astnode, qlast.CreateConcreteProperty):\n            assert isinstance(cmd, PropertyCommand)\n            cmd._process_create_or_alter_ast(schema, astnode, context)\n            cmd._check_field_errors(astnode)\n\n        return cmd\n\n    def get_ast_attr_for_field(\n        self,\n        field: str,\n        astnode: type[qlast.DDLOperation],\n    ) -> Optional[str]:\n        if (\n            field == 'required'\n            and issubclass(astnode, qlast.CreateConcreteProperty)\n        ):\n            return 'is_required'\n        elif (\n            field == 'cardinality'\n            and issubclass(astnode, qlast.CreateConcreteProperty)\n        ):\n            return 'cardinality'\n        else:\n            return super().get_ast_attr_for_field(field, astnode)\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        link = context.get(PropertySourceContext)\n\n        if op.property == 'target' and link:\n            if isinstance(node, qlast.CreateConcreteProperty):\n                expr: Optional[s_expr.Expression] = (\n                    self.get_attribute_value('expr')\n                )\n                if expr is not None:\n                    node.target = expr.parse()\n                else:\n                    ref = op.new_value\n                    assert isinstance(ref, (so.Object, so.ObjectShell))\n                    node.target = utils.typeref_to_ast(schema, ref)\n            else:\n                ref = op.new_value\n                assert isinstance(ref, (so.Object, so.ObjectShell))\n                node.commands.append(\n                    qlast.SetPointerType(\n                        value=utils.typeref_to_ast(schema, ref)\n                    )\n                )\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n\nclass RenameProperty(\n    PropertyCommand,\n    referencing.RenameReferencedInheritingObject[Property],\n):\n    pass\n\n\nclass RebaseProperty(\n    PropertyCommand,\n    referencing.RebaseReferencedInheritingObject[Property],\n):\n    pass\n\n\nclass SetPropertyType(\n    pointers.SetPointerType[Property],\n    referrer_context_class=PropertySourceContext,\n    field='target',\n):\n    pass\n\n\nclass AlterPropertyUpperCardinality(\n    pointers.AlterPointerUpperCardinality[Property],\n    referrer_context_class=PropertySourceContext,\n    field='cardinality',\n):\n    pass\n\n\nclass AlterPropertyLowerCardinality(\n    pointers.AlterPointerLowerCardinality[Property],\n    referrer_context_class=PropertySourceContext,\n    field='required',\n):\n    pass\n\n\nclass AlterPropertyOwned(\n    referencing.AlterOwned[Property],\n    pointers.PointerCommandOrFragment[Property],\n    referrer_context_class=PropertySourceContext,\n    field='owned',\n):\n    pass\n\n\nclass AlterProperty(\n    PropertyCommand,\n    pointers.AlterPointer[Property],\n):\n    astnode = [qlast.AlterConcreteProperty,\n               qlast.AlterProperty]\n\n    referenced_astnode = qlast.AlterConcreteProperty\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> AlterProperty:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(cmd, AlterProperty)\n        if isinstance(astnode, qlast.CreateConcreteProperty):\n            cmd._process_create_or_alter_ast(schema, astnode, context)\n        else:\n            cmd._process_alter_ast(schema, astnode, context)\n        cmd._check_field_errors(astnode)\n        return cmd\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        if op.property == 'target':\n            if op.new_value:\n                assert isinstance(op.new_value, so.ObjectShell)\n                node.commands.append(\n                    qlast.SetPointerType(\n                        value=utils.typeref_to_ast(schema, op.new_value),\n                    ),\n                )\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        if self.maybe_get_object_aux_data('from_alias'):\n            # This is an alias type, appropriate DDL would be generated\n            # from the corresponding Alter/DeleteAlias node.\n            return None\n        else:\n            return super()._get_ast(schema, context, parent_node=parent_node)\n\n\nclass DeleteProperty(\n    PropertyCommand,\n    pointers.DeletePointer[Property],\n):\n    astnode = [qlast.DropConcreteProperty,\n               qlast.DropProperty]\n\n    referenced_astnode = qlast.DropConcreteProperty\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        if self.maybe_get_object_aux_data('from_alias'):\n            # This is an alias type, appropriate DDL would be generated\n            # from the corresponding Alter/DeleteAlias node.\n            return None\n        else:\n            return super()._get_ast(schema, context, parent_node=parent_node)\n"
  },
  {
    "path": "edb/schema/pseudo.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Optional, TypeVar, TYPE_CHECKING\n\nfrom edb import errors\nfrom edb.common import parsing\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom . import delta as sd\nfrom . import name as sn\nfrom . import objects as so\nfrom . import types as s_types\n\nif TYPE_CHECKING:\n    from . import schema as s_schema\n\n\nPseudoType_T = TypeVar(\"PseudoType_T\", bound=\"PseudoType\")\n\n\nclass PseudoType(\n    so.InheritingObject,\n    s_types.Type,\n    qlkind=qltypes.SchemaObjectClass.PSEUDO_TYPE,\n):\n\n    @classmethod\n    def get(\n        cls,\n        schema: s_schema.Schema,\n        name: str | sn.Name,\n    ) -> PseudoType:\n        return schema.get_global(PseudoType, name)\n\n    def as_shell(self, schema: s_schema.Schema) -> PseudoTypeShell:\n        return PseudoTypeShell(name=self.get_name(schema))\n\n    def get_bases(\n        self,\n        schema: s_schema.Schema,\n    ) -> so.ObjectList[PseudoType]:\n        return so.ObjectList[PseudoType].create_empty()  # type: ignore\n\n    def get_ancestors(\n        self,\n        schema: s_schema.Schema,\n    ) -> so.ObjectList[PseudoType]:\n        return so.ObjectList[PseudoType].create_empty()  # type: ignore\n\n    def get_abstract(self, schema: s_schema.Schema) -> bool:\n        return True\n\n    def is_polymorphic(self, schema: s_schema.Schema) -> bool:\n        return True\n\n    def material_type(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[s_schema.Schema, PseudoType]:\n        return schema, self\n\n    def is_any(self, schema: s_schema.Schema) -> bool:\n        return str(self.get_name(schema)) == 'anytype'\n\n    def is_anytuple(self, schema: s_schema.Schema) -> bool:\n        return str(self.get_name(schema)) == 'anytuple'\n\n    def is_anyobject(self, schema: s_schema.Schema) -> bool:\n        return str(self.get_name(schema)) == 'anyobject'\n\n    def is_tuple(self, schema: s_schema.Schema) -> bool:\n        return self.is_anytuple(schema)\n\n    def implicitly_castable_to(\n        self, other: s_types.Type, schema: s_schema.Schema\n    ) -> bool:\n        return self == other\n\n    def find_common_implicitly_castable_type(\n        self,\n        other: s_types.Type,\n        schema: s_schema.Schema,\n    ) -> tuple[s_schema.Schema, Optional[PseudoType]]:\n        if self == other:\n            return schema, self\n        else:\n            return schema, None\n\n    def get_common_parent_type_distance(\n        self, other: s_types.Type, schema: s_schema.Schema\n    ) -> int:\n        if self == other:\n            return 0\n        else:\n            return s_types.MAX_TYPE_DISTANCE\n\n    def _test_polymorphic(\n        self, schema: s_schema.Schema, other: s_types.Type\n    ) -> bool:\n        return self == other\n\n    def _to_nonpolymorphic(\n        self, schema: s_schema.Schema, concrete_type: s_types.Type\n    ) -> tuple[s_schema.Schema, s_types.Type]:\n        return schema, concrete_type\n\n    def _resolve_polymorphic(\n        self, schema: s_schema.Schema, concrete_type: s_types.Type\n    ) -> Optional[s_types.Type]:\n        if self.is_any(schema):\n            return concrete_type\n        if self.is_anyobject(schema):\n            if (\n                not concrete_type.is_object_type()\n                or concrete_type.is_polymorphic(schema)\n            ):\n                return None\n            else:\n                return concrete_type\n        elif self.is_anytuple(schema):\n            if (not concrete_type.is_tuple(schema) or\n                    concrete_type.is_polymorphic(schema)):\n                return None\n            else:\n                return concrete_type\n        else:\n            raise ValueError(\n                f'unexpected pseudo type: {self.get_name(schema)}')\n\n\nclass PseudoTypeShell(s_types.TypeShell[PseudoType]):\n\n    def __init__(\n        self,\n        *,\n        name: sn.Name,\n        span: Optional[parsing.Span] = None,\n    ) -> None:\n        super().__init__(\n            name=name, schemaclass=PseudoType, span=span\n        )\n\n    def is_polymorphic(self, schema: s_schema.Schema) -> bool:\n        return True\n\n    def resolve(self, schema: s_schema.Schema) -> PseudoType:\n        return PseudoType.get(schema, self.name)\n\n\nclass PseudoTypeCommandContext(sd.ObjectCommandContext[PseudoType]):\n    pass\n\n\nclass PseudoTypeCommand(\n    s_types.TypeCommand[PseudoType],\n    context_class=PseudoTypeCommandContext,\n):\n    pass\n\n\nclass CreatePseudoType(PseudoTypeCommand, sd.CreateObject[PseudoType]):\n\n    astnode = qlast.CreatePseudoType\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        if not context.stdmode and not context.testmode:\n            raise errors.UnsupportedFeatureError(\n                'user-defined pseudo types are not supported',\n                span=astnode.span\n            )\n\n        return super()._cmd_tree_from_ast(schema, astnode, context)\n"
  },
  {
    "path": "edb/schema/referencing.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    ClassVar,\n    Optional,\n    TypeVar,\n    AbstractSet,\n    Iterable,\n    cast,\n)\n\nimport hashlib\n\nfrom edb import errors\n\nfrom edb.common import struct\n\nfrom edb.edgeql import ast as qlast\n\nfrom . import delta as sd\nfrom . import inheriting\nfrom . import objects as so\nfrom . import schema as s_schema\nfrom . import name as sn\nfrom . import utils\n\n\nReferencedT = TypeVar('ReferencedT', bound='ReferencedObject')\nReferencedInheritingObjectT = TypeVar('ReferencedInheritingObjectT',\n                                      bound='ReferencedInheritingObject')\n\n\n# Q: There are no ReferencedObjects that aren't ReferencedInheritingObject;\n# should we merge them?\nclass ReferencedObject(so.DerivableObject):\n\n    #: True if the object has an explicit definition and is not\n    #: purely inherited.\n    owned = so.SchemaField(\n        bool,\n        default=False,\n        inheritable=False,\n        compcoef=0.909,\n        reflection_method=so.ReflectionMethod.AS_LINK,\n        special_ddl_syntax=True,\n    )\n\n    @classmethod\n    def get_verbosename_static(\n        cls,\n        name: sn.Name,\n        *,\n        parent: Optional[str] = None,\n    ) -> str:\n        clsname = cls.get_schema_class_displayname()\n        dname = cls.get_displayname_static(name)\n        sn = cls.get_shortname_static(name)\n        if sn == name:\n            clsname = f'abstract {clsname}'\n        if parent is not None:\n            return f\"{clsname} '{dname}' of {parent}\"\n        else:\n            return f\"{clsname} '{dname}'\"\n\n    def get_subject(self, schema: s_schema.Schema) -> Optional[so.Object]:\n        # NB: classes that inherit ReferencedObject define a `get_subject`\n        # method dynamically, with `subject = SchemaField`\n        raise NotImplementedError\n\n    def get_referrer(self, schema: s_schema.Schema) -> Optional[so.Object]:\n        return self.get_subject(schema)\n\n    def get_verbosename(\n        self,\n        schema: s_schema.Schema,\n        *,\n        with_parent: bool = False,\n    ) -> str:\n        vn = super().get_verbosename(schema)\n        if with_parent:\n            return self.add_parent_name(vn, schema)\n\n        return vn\n\n    def add_parent_name(\n        self,\n        base_name: str,\n        schema: s_schema.Schema,\n    ) -> str:\n        subject = self.get_subject(schema)\n        if subject is not None:\n            pn = subject.get_verbosename(schema, with_parent=True)\n            return f'{base_name} of {pn}'\n\n        return base_name\n\n    def init_parent_delta_branch(\n        self: ReferencedT,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        referrer: Optional[so.Object] = None,\n    ) -> tuple[\n        sd.CommandGroup,\n        sd.Command,\n        sd.ContextStack,\n    ]:\n        root, parent, ctx_stack = super().init_parent_delta_branch(\n            schema, context, referrer=referrer)\n\n        if referrer is None:\n            referrer = self.get_referrer(schema)\n\n        if referrer is None:\n            return root, parent, ctx_stack\n\n        obj: Optional[so.Object] = referrer\n        object_stack: list[so.Object] = [referrer]\n\n        while obj is not None:\n            if isinstance(obj, ReferencedObject):\n                obj = obj.get_referrer(schema)\n                if obj is not None:\n                    object_stack.append(obj)\n            else:\n                obj = None\n\n        cmd: sd.Command = parent\n        for obj in reversed(object_stack):\n            alter_cmd = obj.init_delta_command(schema, sd.AlterObject)\n            ctx_stack.push(alter_cmd.new_context(schema, context, obj))\n            cmd.add(alter_cmd)\n            cmd = alter_cmd\n\n        return root, cmd, ctx_stack\n\n    def is_parent_ref(\n        self,\n        schema: s_schema.Schema,\n        reference: so.Object,\n    ) -> bool:\n        \"\"\"Return True if *reference* is a structural ancestor of self\"\"\"\n        obj = self.get_referrer(schema)\n        while obj is not None:\n            if obj == reference:\n                return True\n            elif isinstance(obj, ReferencedObject):\n                obj = obj.get_referrer(schema)\n            else:\n                break\n\n        return False\n\n\nclass ReferencedInheritingObject(\n    so.DerivableInheritingObject,\n    ReferencedObject,\n):\n\n    # Indicates that the object has been declared as\n    # explicitly inherited.\n    declared_overloaded = so.SchemaField(\n        bool,\n        default=False,\n        compcoef=None,\n        inheritable=False,\n        ephemeral=True,\n    )\n\n    def should_propagate(self, schema: s_schema.Schema) -> bool:\n        \"\"\"Whether this object should be propagated to subtypes of the owner\"\"\"\n        return True\n\n    def get_implicit_bases(\n        self: ReferencedInheritingObjectT,\n        schema: s_schema.Schema,\n    ) -> list[ReferencedInheritingObjectT]:\n        return [\n            b for b in self.get_bases(schema).objects(schema)\n            if not b.is_non_concrete(schema)\n        ]\n\n    def get_implicit_ancestors(\n        self: ReferencedInheritingObjectT,\n        schema: s_schema.Schema,\n    ) -> list[ReferencedInheritingObjectT]:\n        return [\n            b for b in self.get_ancestors(schema).objects(schema)\n            if not b.is_non_concrete(schema)\n        ]\n\n    def get_name_impacting_ancestors(\n        self: ReferencedInheritingObjectT,\n        schema: s_schema.Schema,\n    ) -> list[ReferencedInheritingObjectT]:\n        \"\"\"Return ancestors that have an impact on the name of this object.\n\n        For most types this is the same as implicit ancestors.\n        (For constraints it is not.)\n        \"\"\"\n        return self.get_implicit_ancestors(schema)\n\n    def is_endpoint_pointer(self, schema: s_schema.Schema) -> bool:\n        # overloaded by Pointer\n        return False\n\n    def as_delete_delta(\n        self: ReferencedInheritingObjectT,\n        *,\n        schema: s_schema.Schema,\n        context: so.ComparisonContext,\n    ) -> sd.ObjectCommand[ReferencedInheritingObjectT]:\n        del_op = super().as_delete_delta(schema=schema, context=context)\n\n        if (\n            self.get_owned(schema)\n            and not self.is_generated(schema)\n            and any(\n                context.is_deleting(schema, ancestor)\n                for ancestor in self.get_implicit_ancestors(schema)\n            )\n        ):\n            owned_op = self.init_delta_command(schema, AlterOwned)\n            owned_op.set_attribute_value('owned', False, orig_value=True)\n            del_op.add(owned_op)\n\n        return del_op\n\n    def record_field_alter_delta(\n        self: ReferencedInheritingObjectT,\n        schema: s_schema.Schema,\n        delta: sd.ObjectCommand[ReferencedInheritingObjectT],\n        context: so.ComparisonContext,\n        *,\n        fname: str,\n        value: Any,\n        orig_value: Any,\n        orig_schema: s_schema.Schema,\n        orig_object: ReferencedInheritingObjectT,\n        confidence: float,\n    ) -> None:\n        super().record_field_alter_delta(\n            schema,\n            delta,\n            context,\n            fname=fname,\n            value=value,\n            orig_value=orig_value,\n            orig_schema=orig_schema,\n            orig_object=orig_object,\n            confidence=confidence,\n        )\n\n        if fname == 'name':\n            if any(\n                context.is_renaming(orig_schema, ancestor)\n                for ancestor in orig_object.get_name_impacting_ancestors(\n                    orig_schema)\n            ):\n                renames = delta.get_subcommands(type=sd.RenameObject)\n                assert len(renames) == 1\n                rename = renames[0]\n                rename.set_annotation('implicit_propagation', True)\n\n    def derive_ref(\n        self: ReferencedInheritingObjectT,\n        schema: s_schema.Schema,\n        referrer: so.QualifiedObject,\n        *qualifiers: str,\n        mark_derived: bool = False,\n        attrs: Optional[dict[str, Any]] = None,\n        dctx: Optional[sd.CommandContext] = None,\n        derived_name_base: Optional[sn.Name] = None,\n        inheritance_merge: bool = True,\n        inheritance_refdicts: Optional[AbstractSet[str]] = None,\n        transient: bool = False,\n        preserve_endpoint_ptrs: bool = False,\n        name: Optional[sn.QualName] = None,\n        **kwargs: Any,\n    ) -> tuple[s_schema.Schema, ReferencedInheritingObjectT]:\n        if name is None:\n            derived_name = self.get_derived_name(\n                schema,\n                referrer,\n                *qualifiers,\n                mark_derived=mark_derived,\n                derived_name_base=derived_name_base,\n            )\n        else:\n            derived_name = name\n\n        if self.get_name(schema) == derived_name:\n            raise errors.SchemaError(\n                f'cannot derive {self!r}({derived_name}) from itself')\n\n        derived_attrs: dict[str, object] = {}\n\n        if attrs is not None:\n            derived_attrs.update(attrs)\n\n        derived_attrs['name'] = derived_name\n        derived_attrs['bases'] = so.ObjectList.create(schema, [self])\n\n        mcls = type(self)\n        referrer_class = type(referrer)\n\n        refdict = referrer_class.get_refdict_for_class(mcls)\n        reftype = referrer_class.get_field(refdict.attr).type\n        refname = reftype.get_key_for_name(schema, derived_name)\n        refcoll = referrer.get_field_value(schema, refdict.attr)\n        existing = refcoll.get(schema, refname, default=None)\n\n        cmdcls = sd.AlterObject if existing is not None else sd.CreateObject\n        cmd: sd.ObjectCommand[ReferencedInheritingObjectT] = (\n            sd.get_object_delta_command(  # type: ignore[type-var, assignment]\n                objtype=type(self),\n                cmdtype=cmdcls,\n                schema=schema,\n                name=derived_name,\n            )\n        )\n\n        for k, v in derived_attrs.items():\n            cmd.set_attribute_value(k, v)\n\n        if existing is not None:\n            new_bases = derived_attrs['bases']\n            old_bases = existing.get_bases(schema)\n\n            if new_bases != old_bases:\n                assert isinstance(new_bases, so.ObjectList)\n                removed_bases, added_bases = inheriting.delta_bases(\n                    [b.get_name(schema) for b in old_bases.objects(schema)],\n                    [b.get_name(schema) for b in new_bases.objects(schema)],\n                    t=type(self),\n                )\n\n                rebase_cmd = sd.get_object_delta_command(\n                    objtype=type(self),\n                    cmdtype=inheriting.RebaseInheritingObject,\n                    schema=schema,\n                    name=derived_name,\n                    added_bases=added_bases,\n                    removed_bases=removed_bases,\n                )\n\n                cmd.add(rebase_cmd)\n\n        context = sd.CommandContext(modaliases={}, schema=schema)\n        delta, parent_cmd, _ = self.init_parent_delta_branch(\n            schema, context, referrer=referrer)\n        root = sd.DeltaRoot()\n        root.add(delta)\n\n        with context(sd.DeltaRootContext(schema=schema, op=root)):\n            if not inheritance_merge:\n                context.current().inheritance_merge = False\n\n            if inheritance_refdicts is not None:\n                context.current().inheritance_refdicts = (\n                    inheritance_refdicts)\n\n            if mark_derived:\n                context.current().mark_derived = True\n\n            if transient:\n                context.current().transient_derivation = True\n                if not preserve_endpoint_ptrs:\n                    context.current().slim_links = True\n\n            parent_cmd.add(cmd)\n            schema = delta.apply(schema, context)\n\n        derived = schema.get(derived_name, type=type(self))\n\n        return schema, derived\n\n\nclass ReferencedObjectCommandBase(sd.QualifiedObjectCommand[ReferencedT]):\n\n    _referrer_context_class: ClassVar[Optional[\n        type[sd.ObjectCommandContext[so.Object]]\n    ]] = None\n\n    #: Whether the referenced command represents a \"strong\" reference,\n    #: i.e. the one that must not be broken out of the enclosing parent\n    #: command when doing dependency reorderings.\n    is_strong_ref = struct.Field(bool, default=False)\n\n    def __init_subclass__(\n        cls,\n        *,\n        referrer_context_class: Optional[\n            type[sd.ObjectCommandContext[so.Object]]\n        ] = None,\n        **kwargs: Any,\n    ) -> None:\n        super().__init_subclass__(**kwargs)\n        if referrer_context_class is not None:\n            cls._referrer_context_class = referrer_context_class\n\n    @classmethod\n    def get_referrer_context_class(\n        cls,\n    ) -> type[sd.ObjectCommandContext[so.Object]]:\n        if cls._referrer_context_class is None:\n            raise TypeError(\n                f'referrer_context_class is not defined for {cls}')\n        return cls._referrer_context_class\n\n    @classmethod\n    def get_referrer_context(\n        cls,\n        context: sd.CommandContext,\n    ) -> Optional[sd.ObjectCommandContext[so.Object]]:\n        \"\"\"Get the context of the command for the referring object, if any.\n\n        E.g. for a `create/alter/etc concrete link` command this would\n        be the context of the `create/alter/etc type` command.\n        \"\"\"\n        ctxcls = cls.get_referrer_context_class()\n        return context.get(ctxcls)\n\n    @classmethod\n    def get_referrer_context_or_die(\n        cls,\n        context: sd.CommandContext,\n    ) -> sd.ObjectCommandContext[so.Object]:\n        ctx = cls.get_referrer_context(context)\n        if ctx is None:\n            raise RuntimeError(f'no referrer context for {cls}')\n        return ctx\n\n    def get_top_referrer_op(\n        self,\n        context: sd.CommandContext,\n    ) -> Optional[sd.ObjectCommand[so.Object]]:\n        op: sd.ObjectCommand[so.Object] = self  # type: ignore\n        while True:\n            if not isinstance(op, ReferencedObjectCommandBase):\n                break\n            ctx = op.get_referrer_context(context)\n            if ctx is None:\n                break\n            op = ctx.op\n        return op\n\n\nclass ReferencedObjectCommand(ReferencedObjectCommandBase[ReferencedT]):\n\n    @classmethod\n    def _classname_from_ast_and_referrer(\n        cls,\n        schema: s_schema.Schema,\n        referrer_name: sn.QualName,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext\n    ) -> sn.QualName:\n        base_ref = utils.ast_to_object_shell(\n            astnode.name,\n            modaliases=context.modaliases,\n            schema=schema,\n            metaclass=cls.get_schema_metaclass(),\n        )\n\n        base_name = sn.shortname_from_fullname(base_ref.name)\n        quals = cls._classname_quals_from_ast(\n            schema, astnode, base_name, referrer_name, context)\n        pnn = sn.get_specialized_name(base_name, str(referrer_name), *quals)\n        return sn.QualName(name=pnn, module=referrer_name.module)\n\n    @classmethod\n    def _classname_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext,\n    ) -> sn.QualName:\n        parent_ctx = cls.get_referrer_context(context)\n        if parent_ctx is not None:\n            assert isinstance(parent_ctx.op, sd.QualifiedObjectCommand)\n            referrer_name = context.get_referrer_name(parent_ctx)\n            name = cls._classname_from_ast_and_referrer(\n                schema, referrer_name, astnode, context\n            )\n        else:\n            name = super()._classname_from_ast(schema, astnode, context)\n\n        assert isinstance(name, sn.QualName)\n        return name\n\n    @classmethod\n    def _classname_from_name(\n        cls,\n        name: sn.QualName,\n        referrer_name: sn.QualName,\n    ) -> sn.QualName:\n        base_name = sn.shortname_from_fullname(name)\n        quals = cls._classname_quals_from_name(name)\n        pnn = sn.get_specialized_name(base_name, str(referrer_name), *quals)\n        return sn.QualName(name=pnn, module=referrer_name.module)\n\n    @classmethod\n    def _classname_quals_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        base_name: sn.Name,\n        referrer_name: sn.QualName,\n        context: sd.CommandContext,\n    ) -> tuple[str, ...]:\n        return ()\n\n    @classmethod\n    def _classname_quals_from_name(\n        cls,\n        name: sn.QualName,\n    ) -> tuple[str, ...]:\n        return ()\n\n    @classmethod\n    def _name_qual_from_exprs(\n        cls, schema: s_schema.Schema, exprs: Iterable[str]\n    ) -> str:\n        m = hashlib.sha1()\n        for expr in exprs:\n            m.update(expr.encode())\n        return m.hexdigest()\n\n    def _get_ast_node(\n        self, schema: s_schema.Schema, context: sd.CommandContext\n    ) -> type[qlast.DDLOperation]:\n        subject_ctx = self.get_referrer_context(context)\n        ref_astnode: Optional[type[qlast.DDLOperation]] = (\n            getattr(self, 'referenced_astnode', None))\n        if subject_ctx is not None and ref_astnode is not None:\n            return ref_astnode\n        else:\n            if isinstance(self.astnode, (list, tuple)):\n                return self.astnode[1]\n            else:\n                return self.astnode\n\n\nclass CreateReferencedObject(\n    ReferencedObjectCommand[ReferencedT],\n    sd.CreateObject[ReferencedT],\n):\n\n    referenced_astnode: ClassVar[type[qlast.ObjectDDL]]\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        if isinstance(astnode, cls.referenced_astnode):\n            objcls = cls.get_schema_metaclass()\n\n            referrer_ctx = cls.get_referrer_context_or_die(context)\n            referrer_class = referrer_ctx.op.get_schema_metaclass()\n            referrer_name = context.get_referrer_name(referrer_ctx)\n            refdict = referrer_class.get_refdict_for_class(objcls)\n\n            cmd.set_attribute_value(\n                refdict.backref_attr,\n                so.ObjectShell(\n                    name=referrer_name,\n                    schemaclass=referrer_class,\n                ),\n            )\n\n            cmd.set_attribute_value('owned', True)\n\n            if getattr(astnode, 'abstract', None):\n                cmd.set_attribute_value('abstract', True)\n\n        return cmd\n\n    def _get_ast_node(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> type[qlast.DDLOperation]:\n        # Render CREATE as ALTER in DDL if the created referenced object is\n        # implicitly inherited from parents.\n        scls = self.get_object(schema, context)\n        assert isinstance(scls, ReferencedInheritingObject)\n        implicit_bases = scls.get_implicit_bases(schema)\n        if (\n            implicit_bases\n            and not context.declarative\n            and not self.ast_ignore_ownership()\n        ):\n            alter = scls.init_delta_command(schema, sd.AlterObject)\n            return alter._get_ast_node(schema, context)\n        else:\n            return super()._get_ast_node(schema, context)\n\n    @classmethod\n    def as_inherited_ref_cmd(\n        cls,\n        *,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        astnode: qlast.ObjectDDL,\n        bases: list[ReferencedT],\n        referrer: so.Object,\n    ) -> sd.ObjectCommand[ReferencedT]:\n        cmd = cls(classname=cls._classname_from_ast(schema, astnode, context))\n        cmd.set_attribute_value('name', cmd.classname)\n        cmd.set_attribute_value(\n            'bases', so.ObjectList.create(schema, bases).as_shell(schema))\n        return cmd\n\n    @classmethod\n    def as_inherited_ref_ast(\n        cls,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        refname: sn.Name,\n        parent: ReferencedObject,\n    ) -> qlast.ObjectDDL:\n        # N.B: If this is overloaded, then as_inherited_ref_cmd\n        # probably needs to be overloaded too.\n        # In particular, any fields that are inherited=False in the schema\n        # but actually need to be inherited (like arguments for constraints\n        # and indexes) likely should be handled there.\n        nref = cls.get_inherited_ref_name(schema, context, parent, refname)\n        astnode_cls = cls.referenced_astnode\n        astnode = astnode_cls(name=nref)\n        assert isinstance(astnode, qlast.ObjectDDL)\n        return astnode\n\n    @classmethod\n    def get_inherited_ref_name(\n        cls,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        parent: ReferencedObject,\n        refname: sn.Name,\n    ) -> qlast.ObjectRef:\n        ref = utils.name_to_ast_ref(refname)\n        if ref.module is None:\n            ref.module = parent.get_shortname(schema).module\n        return ref\n\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._create_begin(schema, context)\n        referrer_ctx = self.get_referrer_context(context)\n        if referrer_ctx is not None:\n            referrer = referrer_ctx.scls\n            referrer_cls = type(referrer)\n            mcls = type(self.scls)\n            refdict = referrer_cls.get_refdict_for_class(mcls)\n            schema = referrer.add_classref(schema, refdict.attr, self.scls)\n        return schema\n\n\nclass DeleteReferencedObjectCommand(\n    ReferencedObjectCommand[ReferencedT],\n    sd.DeleteObject[ReferencedT],\n):\n\n    def _delete_innards(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._delete_innards(schema, context)\n        referrer_ctx = self.get_referrer_context(context)\n        if referrer_ctx is not None:\n            referrer = referrer_ctx.scls\n            schema = self._delete_ref(schema, context, referrer)\n        return schema\n\n    def _delete_ref(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        referrer: so.Object,\n    ) -> s_schema.Schema:\n\n        scls = self.scls\n        referrer_class = type(referrer)\n        mcls = type(scls)\n        refdict = referrer_class.get_refdict_for_class(mcls)\n        reftype = referrer_class.get_field(refdict.attr).type\n        refname = reftype.get_key_for(schema, self.scls)\n\n        return referrer.del_classref(schema, refdict.attr, refname)\n\n\nclass ReferencedInheritingObjectCommand(\n    ReferencedObjectCommand[ReferencedInheritingObjectT],\n    inheriting.InheritingObjectCommand[ReferencedInheritingObjectT],\n):\n\n    def _get_implicit_ref_bases(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        referrer: so.InheritingObject,\n        referrer_field: str,\n        fq_name: sn.QualName,\n    ) -> list[ReferencedInheritingObjectT]:\n\n        ref_field_type = type(referrer).get_field(referrer_field).type\n        assert isinstance(referrer, so.QualifiedObject)\n        child_referrer_bases = referrer.get_bases(schema).objects(schema)\n        implicit_bases = []\n\n        for ref_base in child_referrer_bases:\n            fq_name_in_child = self._classname_from_name(\n                fq_name, ref_base.get_name(schema))\n            refname = ref_field_type.get_key_for_name(schema, fq_name_in_child)\n            parent_coll = ref_base.get_field_value(schema, referrer_field)\n            parent_item = parent_coll.get(schema, refname, default=None)\n            if (\n                parent_item is not None\n                and parent_item.should_propagate(schema)\n                and not context.is_deleting(parent_item)\n            ):\n                implicit_bases.append(parent_item)\n\n        return implicit_bases\n\n    def get_ref_implicit_base_delta(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        refcls: ReferencedInheritingObjectT,\n        implicit_bases: list[ReferencedInheritingObjectT],\n    ) -> inheriting.BaseDelta_T[ReferencedInheritingObjectT]:\n        child_bases = refcls.get_bases(schema).objects(schema)\n\n        default_base = refcls.get_default_base_name()\n        explicit_bases = [\n            b for b in child_bases\n            if b.is_non_concrete(schema) and b.get_name(schema) != default_base\n        ]\n\n        new_bases = implicit_bases + explicit_bases\n        return inheriting.delta_bases(\n            [b.get_name(schema) for b in child_bases],\n            [b.get_name(schema) for b in new_bases],\n            t=type(refcls),\n        )\n\n    def _validate(\n        self, schema: s_schema.Schema, context: sd.CommandContext\n    ) -> None:\n        scls = self.scls\n        implicit_bases = [\n            b for b in scls.get_bases(schema).objects(schema)\n            if not b.is_non_concrete(schema)\n        ]\n\n        referrer_ctx = self.get_referrer_context_or_die(context)\n        objcls = self.get_schema_metaclass()\n        referrer_class = referrer_ctx.op.get_schema_metaclass()\n        refdict = referrer_class.get_refdict_for_class(objcls)\n\n        if context.declarative and scls.get_owned(schema):\n            if (implicit_bases\n                    and refdict.requires_explicit_overloaded\n                    and not self.get_attribute_value('declared_overloaded')):\n\n                ancestry = []\n\n                for obj in implicit_bases:\n                    bref = obj.get_referrer(schema)\n                    assert bref is not None\n                    ancestry.append(bref)\n\n                alist = \", \".join(\n                    str(a.get_shortname(schema)) for a in ancestry\n                )\n                raise errors.SchemaDefinitionError(\n                    f'{self.scls.get_verbosename(schema, with_parent=True)} '\n                    f'must be declared using the `overloaded` keyword because '\n                    f'it is defined in the following ancestor(s): {alist}',\n                    span=self.span,\n                )\n            elif (not implicit_bases\n                    and self.get_attribute_value('declared_overloaded')):\n\n                raise errors.SchemaDefinitionError(\n                    f'{self.scls.get_verbosename(schema, with_parent=True)}: '\n                    f'cannot be declared `overloaded` as there are no '\n                    f'ancestors defining it.',\n                    span=self.span,\n                )\n\n    def get_implicit_bases(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        bases: Any,\n    ) -> list[sn.QualName]:\n\n        mcls = self.get_schema_metaclass()\n        default_base = mcls.get_default_base_name()\n\n        if isinstance(bases, so.ObjectCollectionShell):\n            base_names = [b.get_name(schema) for b in bases.items]\n        elif isinstance(bases, so.ObjectList):\n            base_names = list(bases.names(schema))\n        else:\n            # assume regular iterable of shells\n            base_names = [b.get_name(schema) for b in bases]\n\n        # Filter out explicit bases\n        implicit_bases = [\n            b\n            for b in base_names\n            if (\n                b != default_base\n                and isinstance(b, sn.QualName)\n                and sn.shortname_from_fullname(b) != b\n            )\n        ]\n\n        return implicit_bases\n\n    def _propagate_ref_op(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        scls: ReferencedInheritingObject,\n        cb: Callable[[sd.ObjectCommand[so.Object], sn.Name], None]\n    ) -> None:\n        if inheriting._has_implicit_propagation(context):\n            return\n\n        referrer_ctx = self.get_referrer_context(context)\n        if referrer_ctx:\n            referrer = referrer_ctx.scls\n            referrer_class = type(referrer)\n            mcls = type(scls)\n            refdict = referrer_class.get_refdict_for_class(mcls)\n            reftype = referrer_class.get_field(refdict.attr).type\n            refname = reftype.get_key_for(schema, self.scls)\n        else:\n            refname = self.scls.get_name(schema)\n\n        for descendant in scls.ordered_descendants(schema):\n            d_alter_root, d_alter_cmd, ctx_stack = (\n                descendant.init_delta_branch(schema, context, sd.AlterObject))\n            d_alter_cmd.set_annotation('implicit_propagation', True)\n\n            with ctx_stack():\n                cb(d_alter_cmd, refname)\n\n            self.add_caused(d_alter_root)\n\n    def _propagate_ref_field_alter_in_inheritance(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field_name: str,\n        require_inheritance_consistency: bool = True,\n    ) -> None:\n        \"\"\"Validate and propagate a field alteration to children.\n\n        This method also performs consistency checks against base objects\n        to ensure that the new value matches that of the parents.\n        \"\"\"\n        scls = self.scls\n\n        currently_altered = context.change_log[type(scls), field_name]\n        currently_altered.add(scls)\n\n        if require_inheritance_consistency:\n            implicit_bases = scls.get_implicit_bases(schema)\n            non_altered_bases = []\n\n            value = scls.get_field_value(schema, field_name)\n            for base in {\n                    x for x in implicit_bases if x not in currently_altered}:\n                base_value = base.get_field_value(schema, field_name)\n\n                if isinstance(value, so.SubclassableObject):\n                    if not value.issubclass(schema, base_value):\n                        non_altered_bases.append(base)\n                else:\n                    if value != base_value:\n                        non_altered_bases.append(base)\n\n            # This object is inherited from one or more ancestors that\n            # are not altered in the same op, and this is an error.\n            if non_altered_bases:\n                bases_str = ', '.join(\n                    b.get_verbosename(schema, with_parent=True)\n                    for b in non_altered_bases\n                )\n\n                vn = scls.get_verbosename(schema, with_parent=True)\n                desc = self.get_friendly_description(\n                    schema=schema,\n                    object_desc=f'inherited {vn}',\n                )\n\n                raise errors.SchemaDefinitionError(\n                    f'cannot {desc}',\n                    details=(\n                        f'{vn} is inherited from '\n                        f'{bases_str}'\n                    ),\n                    span=self.span,\n                )\n\n        value = self.get_attribute_value(field_name)\n\n        def _propagate(\n            alter_cmd: sd.ObjectCommand[so.Object],\n            refname: sn.Name,\n        ) -> None:\n            assert isinstance(alter_cmd, sd.QualifiedObjectCommand)\n            s_t: sd.ObjectCommand[ReferencedInheritingObjectT]\n            if isinstance(self, sd.AlterSpecialObjectField):\n                s_t = self.clone(alter_cmd.classname)\n            else:\n                s_t = type(self)(classname=alter_cmd.classname)\n            orig_value = scls.get_explicit_field_value(\n                schema, field_name, default=None)\n            s_t.set_attribute_value(\n                field_name,\n                value,\n                orig_value=orig_value,\n                inherited=True,\n            )\n            alter_cmd.add(s_t)\n\n        self._propagate_ref_op(schema, context, scls, cb=_propagate)\n\n    def _drop_owned_refs(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        refdict: so.RefDict,\n    ) -> s_schema.Schema:\n\n        scls = self.scls\n        refs = scls.get_field_value(schema, refdict.attr)\n\n        ref: ReferencedInheritingObject\n        for ref in refs.objects(schema):\n            inherited = ref.get_implicit_bases(schema)\n            if inherited and ref.get_owned(schema):\n                alter = ref.init_delta_command(schema, sd.AlterObject)\n                alter.set_attribute_value('owned', False, orig_value=True)\n                schema = alter.apply(schema, context)\n                self.add(alter)\n            elif (\n                # drop things that aren't owned and aren't inherited\n                not inherited\n                # endpoint pointers are special because they aren't marked as\n                # inherited even though they basically are\n                and not ref.is_endpoint_pointer(schema)\n            ):\n                drop_ref = ref.init_delta_command(schema, sd.DeleteObject)\n                self.add(drop_ref)\n\n        return schema\n\n\nclass CreateReferencedInheritingObject(\n    CreateReferencedObject[ReferencedInheritingObjectT],\n    inheriting.CreateInheritingObject[ReferencedInheritingObjectT],\n    ReferencedInheritingObjectCommand[ReferencedInheritingObjectT],\n):\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        refctx = type(self).get_referrer_context(context)\n        if refctx is not None:\n            if self.get_attribute_value('from_alias'):\n                return None\n\n            elif (\n                not self.get_attribute_value('owned')\n                and not self.ast_ignore_ownership()\n            ):\n                if context.descriptive_mode:\n                    astnode = super()._get_ast(\n                        schema,\n                        context,\n                        parent_node=parent_node,\n                    )\n                    assert astnode is not None\n\n                    inherited_from = [\n                        sn.quals_from_fullname(b)[0]\n                        for b in self.get_implicit_bases(\n                            schema,\n                            context,\n                            self.get_attribute_value('bases'),\n                        )\n                    ]\n\n                    astnode.system_comment = (\n                        f'inherited from {\", \".join(inherited_from)}'\n                    )\n\n                    return astnode\n                else:\n                    return None\n\n            else:\n                astnode = super()._get_ast(\n                    schema, context, parent_node=parent_node)\n\n                if context.declarative:\n                    scls = self.get_object(schema, context)\n                    assert isinstance(scls, ReferencedInheritingObject)\n                    implicit_bases = scls.get_implicit_bases(schema)\n                    objcls = self.get_schema_metaclass()\n                    referrer_class = refctx.op.get_schema_metaclass()\n                    refdict = referrer_class.get_refdict_for_class(objcls)\n                    if refdict.requires_explicit_overloaded and implicit_bases:\n                        assert isinstance(astnode, qlast.CreateConcretePointer)\n                        astnode.declared_overloaded = True\n\n                return astnode\n        else:\n            return super()._get_ast(schema, context, parent_node=parent_node)\n\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        referrer_ctx = self.get_referrer_context(context)\n        implicit_bases = None\n\n        if referrer_ctx is not None and not context.canonical:\n            objcls = self.get_schema_metaclass()\n            referrer = referrer_ctx.scls\n\n            if isinstance(referrer, so.InheritingObject):\n                referrer_class = referrer_ctx.op.get_schema_metaclass()\n                refdict = referrer_class.get_refdict_for_class(objcls)\n\n                implicit_bases = self._get_implicit_ref_bases(\n                    schema, context, referrer, refdict.attr, self.classname)\n\n                if implicit_bases:\n                    bases = self.get_attribute_value('bases')\n                    if bases:\n                        res_bases = cast(\n                            list[ReferencedInheritingObjectT],\n                            self.resolve_obj_collection(bases, schema))\n                        bases = so.ObjectList.create(\n                            schema,\n                            implicit_bases + [\n                                b for b in res_bases\n                                if b not in implicit_bases\n                            ],\n                        )\n                    else:\n                        bases = so.ObjectList.create(\n                            schema,\n                            implicit_bases,\n                        )\n\n                    self.set_attribute_value('bases', bases.as_shell(schema))\n\n                if referrer.get_is_derived(schema):\n                    self.set_attribute_value('is_derived', True)\n\n        return super()._create_begin(schema, context)\n\n    def _create_innards(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        if (\n            not context.canonical\n            and context.enable_recursion\n            and (referrer_ctx := self.get_referrer_context(context))\n            and isinstance(referrer := referrer_ctx.scls, so.InheritingObject)\n            and self.scls.should_propagate(schema)\n        ):\n            # Propagate the creation of a new ref to\n            # descendants of our referrer.\n            self._propagate_ref_creation(schema, context, referrer)\n\n        return super()._create_innards(schema, context)\n\n    def _create_finalize(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._create_finalize(schema, context)\n        if not context.canonical:\n            referrer_ctx = self.get_referrer_context(context)\n            if referrer_ctx is not None:\n                self._validate(schema, context)\n\n        return schema\n\n    def _propagate_ref_creation(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        referrer: so.InheritingObject,\n    ) -> None:\n\n        get_cmd = sd.get_object_command_class_or_die\n\n        mcls = type(self.scls)\n        referrer_cls = type(referrer)\n        ref_create_cmd = get_cmd(sd.CreateObject, mcls)\n        ref_alter_cmd = get_cmd(sd.AlterObject, mcls)\n        ref_rebase_cmd = get_cmd(inheriting.RebaseInheritingObject, mcls)\n        assert issubclass(ref_create_cmd, CreateReferencedInheritingObject)\n        assert issubclass(ref_rebase_cmd, RebaseReferencedInheritingObject)\n        refdict = referrer_cls.get_refdict_for_class(mcls)\n        parent_fq_refname = self.scls.get_name(schema)\n\n        for child in referrer.children(schema):\n            if not child.allow_ref_propagation(schema, context, refdict):\n                continue\n\n            alter_root, alter, ctx_stack = child.init_delta_branch(\n                schema, context, sd.AlterObject)\n\n            with ctx_stack():\n                # This is needed to get the correct inherited name which will\n                # either be created or rebased.\n                ref_field_type = type(child).get_field(refdict.attr).type\n                refname = ref_field_type.get_key_for_name(\n                    schema, parent_fq_refname)\n\n                astnode = ref_create_cmd.as_inherited_ref_ast(\n                    schema, context, refname, self.scls)\n                fq_name = self._classname_from_ast(schema, astnode, context)\n\n                # We cannot check for ref existence in this child at this\n                # time, because it might get created in a sibling branch\n                # of the delta tree.  Instead, generate a command group\n                # containing Alter(if_exists) and Create(if_not_exists)\n                # to postpone that check until the application time.\n                ref_create = ref_create_cmd.as_inherited_ref_cmd(\n                    schema=schema,\n                    context=context,\n                    astnode=astnode,\n                    bases=[self.scls],\n                    referrer=child,\n                )\n                assert isinstance(ref_create, sd.CreateObject)\n                ref_create.if_not_exists = True\n\n                # Copy any special updates over\n                for special in self.get_subcommands(\n                        type=sd.AlterSpecialObjectField):\n                    ref_create.add(special.clone(ref_create.classname))\n\n                ref_create.set_attribute_value(refdict.backref_attr, child)\n\n                if child.get_is_derived(schema):\n                    # All references in a derived object must\n                    # also be marked as derived, to be consistent\n                    # with derive_subtype().\n                    ref_create.set_attribute_value('is_derived', True)\n\n                ref_alter = ref_alter_cmd(classname=fq_name, if_exists=True)\n                ref_alter.add(ref_rebase_cmd(\n                    classname=fq_name,\n                    implicit=True,\n                    added_bases=(),\n                    removed_bases=(),\n                ))\n\n                alter.add(ref_alter)\n                alter.add(ref_create)\n\n            self.add_caused(alter_root)\n\n\nclass AlterReferencedInheritingObject(\n    ReferencedInheritingObjectCommand[ReferencedInheritingObjectT],\n    inheriting.AlterInheritingObject[ReferencedInheritingObjectT],\n):\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        if self.get_attribute_value('from_alias'):\n            return None\n        else:\n            return super()._get_ast(schema, context, parent_node=parent_node)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        refctx = cls.get_referrer_context(context)\n        # When a referenced object is altered it becomes \"owned\"\n        # by the referrer, _except_ when either an explicit\n        # SET OWNED/DROP OWNED subcommand is present, or\n        # _all_ subcommands are `RESET` subcommands.\n        if (\n            refctx is not None\n            and qlast.get_ddl_field_command(astnode, 'owned') is None\n            and (\n                not cmd.get_subcommands()\n                or not all(\n                    (\n                        isinstance(scmd, sd.AlterObjectProperty)\n                        and scmd.new_value is None\n                    )\n                    for scmd in cmd.get_subcommands()\n                )\n            )\n        ):\n            cmd.set_attribute_value('owned', True)\n\n        assert isinstance(cmd, AlterReferencedInheritingObject)\n        return cmd\n\n    def _alter_finalize(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._alter_finalize(schema, context)\n        scls = self.scls\n        was_owned = scls.get_owned(context.current().original_schema)\n        now_owned = scls.get_owned(schema)\n        if not was_owned and now_owned:\n            self._validate(schema, context)\n        return schema\n\n\nclass RebaseReferencedInheritingObject(\n    ReferencedInheritingObjectCommand[ReferencedInheritingObjectT],\n    inheriting.RebaseInheritingObject[ReferencedInheritingObjectT],\n):\n\n    implicit = struct.Field(bool, default=False)\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n\n        if not context.canonical and self.implicit:\n            mcls = self.get_schema_metaclass()\n            refctx = self.get_referrer_context_or_die(context)\n            referrer = refctx.scls\n            assert isinstance(referrer, so.InheritingObject)\n            refdict = type(referrer).get_refdict_for_class(mcls)\n\n            implicit_bases = self._get_implicit_ref_bases(\n                schema,\n                context,\n                referrer=referrer,\n                referrer_field=refdict.attr,\n                fq_name=self.classname,\n            )\n\n            scls = self.get_object(schema, context)\n            removed_bases, added_bases = self.get_ref_implicit_base_delta(\n                schema,\n                context,\n                scls,\n                implicit_bases=implicit_bases,\n            )\n\n            self.added_bases = added_bases\n            self.removed_bases = removed_bases\n\n        return super().apply(schema, context)\n\n    def _get_bases_for_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        bases: tuple[so.ObjectShell[ReferencedInheritingObjectT], ...],\n    ) -> tuple[so.ObjectShell[ReferencedInheritingObjectT], ...]:\n        bases = super()._get_bases_for_ast(schema, context, bases)\n        implicit_bases = set(self.get_implicit_bases(schema, context, bases))\n        return tuple(b for b in bases if b.name not in implicit_bases)\n\n\nclass RenameReferencedInheritingObject(\n    ReferencedInheritingObjectCommand[ReferencedInheritingObjectT],\n    inheriting.RenameInheritingObject[ReferencedInheritingObjectT],\n):\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        schema = super()._alter_begin(schema, context)\n        scls = self.scls\n\n        referrer_ctx = self.get_referrer_context(context)\n        if referrer_ctx:\n            mcls = self.get_schema_metaclass()\n            referrer_class = referrer_ctx.op.get_schema_metaclass()\n            refdict = referrer_class.get_refdict_for_class(mcls)\n            reftype = referrer_class.get_field(refdict.attr).type\n\n            # Force a refresh of the refdict, since the rename may\n            # have invalidated its cache of names.\n            referrer = referrer_ctx.scls\n            schema = referrer.refresh_classref(schema, refdict.attr)\n\n        if not context.canonical and not scls.is_non_concrete(schema):\n            assert referrer_ctx\n            orig_ref_fqname = scls.get_name(orig_schema)\n            orig_ref_lname = reftype.get_key_for_name(schema, orig_ref_fqname)\n\n            new_ref_fqname = scls.get_name(schema)\n            new_ref_lname = reftype.get_key_for_name(schema, new_ref_fqname)\n\n            # Distinguish between actual local name change and fully-qualified\n            # name change due to structural parent rename.\n            if orig_ref_lname != new_ref_lname:\n                implicit_bases = scls.get_implicit_bases(orig_schema)\n                non_renamed_bases = {\n                    x for x in implicit_bases if x not in context.renamed_objs}\n                # This object is inherited from one or more ancestors that\n                # are not renamed in the same op, and this is an error.\n                if non_renamed_bases:\n                    bases_str = ', '.join(\n                        b.get_verbosename(schema, with_parent=True)\n                        for b in non_renamed_bases\n                    )\n\n                    verb = 'are' if len(non_renamed_bases) > 1 else 'is'\n                    vn = scls.get_verbosename(orig_schema, with_parent=True)\n\n                    raise errors.SchemaDefinitionError(\n                        f'cannot rename inherited {vn}',\n                        details=(\n                            f'{vn} is inherited from '\n                            f'{bases_str}, which {verb} not being renamed'\n                        ),\n                        span=self.span,\n                    )\n\n            self._propagate_ref_rename(schema, context, scls)\n\n        return schema\n\n    def _propagate_ref_rename(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        scls: ReferencedInheritingObject\n    ) -> None:\n        rename_cmdcls = sd.get_object_command_class_or_die(\n            sd.RenameObject, type(scls))\n\n        def _ref_rename(alter_cmd: sd.Command, refname: sn.Name) -> None:\n            astnode = rename_cmdcls.astnode(  # type: ignore\n                new_name=utils.name_to_ast_ref(refname),\n            )\n\n            rename_cmd = rename_cmdcls._rename_cmd_from_ast(\n                schema, astnode, context)\n\n            alter_cmd.add(rename_cmd)\n\n        self._propagate_ref_op(schema, context, scls, cb=_ref_rename)\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        if self.get_annotation('implicit_propagation'):\n            return None\n        else:\n            return super()._get_ast(schema, context, parent_node=parent_node)\n\n\nclass DeleteReferencedInheritingObject(\n    DeleteReferencedObjectCommand[ReferencedInheritingObjectT],\n    inheriting.DeleteInheritingObject[ReferencedInheritingObjectT],\n    ReferencedInheritingObjectCommand[ReferencedInheritingObjectT],\n):\n\n    def _delete_innards(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        if (\n            not context.canonical\n            and (referrer_ctx := self.get_referrer_context(context))\n            and isinstance(referrer := referrer_ctx.scls, so.InheritingObject)\n            and self.scls.should_propagate(schema)\n        ):\n            self._propagate_ref_deletion(schema, context, referrer)\n\n        return super()._delete_innards(schema, context)\n\n    def _propagate_ref_deletion(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        referrer: so.InheritingObject,\n    ) -> None:\n        scls = self.scls\n        self_name = scls.get_name(schema)\n        referrer_class = type(referrer)\n        mcls = type(scls)\n        refdict = referrer_class.get_refdict_for_class(mcls)\n        reftype = referrer_class.get_field(refdict.attr).type\n\n        if (\n            not context.in_deletion(offset=1)\n            and not context.disable_dep_verification\n        ):\n            implicit_bases = set(self._get_implicit_ref_bases(\n                schema, context, referrer, refdict.attr, self_name))\n\n            if implicit_bases:\n                # Cannot remove inherited objects.\n                vn = scls.get_verbosename(schema, with_parent=True)\n                parents = [\n                    b.get_field_value(schema, refdict.backref_attr)\n                    for b in implicit_bases\n                ]\n\n                pnames = '\\n- '.join(\n                    p.get_verbosename(schema, with_parent=True)\n                    for p in parents\n                )\n\n                raise errors.SchemaError(\n                    f'cannot drop inherited {vn}',\n                    span=self.span,\n                    details=f'{vn} is inherited from:\\n- {pnames}'\n                )\n\n        # Sort the children by reverse inheritance order amongst them.\n        # So if we are T and have children A and B and A <: B, we want to\n        # process A first, since we need to rebase it away from T, and then\n        # dropping A will also drop B.\n        for child in reversed(\n            sd.sort_by_inheritance(schema, referrer.children(schema))\n        ):\n            assert isinstance(child, so.QualifiedObject)\n            child_coll = child.get_field_value(schema, refdict.attr)\n            fq_refname_in_child = self._classname_from_name(\n                self_name,\n                child.get_name(schema),\n            )\n            child_refname = reftype.get_key_for_name(\n                schema, fq_refname_in_child)\n            existing = child_coll.get(schema, child_refname, None)\n\n            if existing is not None:\n                alter_root, alter_leaf, ctx_stack = (\n                    existing.init_parent_delta_branch(\n                        schema, context, referrer=child))\n                with ctx_stack():\n                    cmd = self._propagate_child_ref_deletion(\n                        schema, context, refdict, child, existing)\n                    alter_leaf.add(cmd)\n                self.add_caused(alter_root)\n\n    def _propagate_child_ref_deletion(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        refdict: so.RefDict,\n        child: so.InheritingObject,\n        child_ref: ReferencedInheritingObjectT,\n    ) -> sd.Command:\n        name = child_ref.get_name(schema)\n        implicit_bases = self._get_implicit_ref_bases(\n            schema, context, child, refdict.attr, name)\n\n        cmd: sd.Command\n\n        if child_ref.get_owned(schema) or implicit_bases:\n            # Child is either defined locally or is inherited\n            # from another parent, so we need to do a rebase.\n            removed_bases, added_bases = self.get_ref_implicit_base_delta(\n                schema, context, child_ref, implicit_bases)\n\n            rebase_cmd = child_ref.init_delta_command(\n                schema,\n                inheriting.RebaseInheritingObject,\n                added_bases=added_bases,\n                removed_bases=removed_bases,\n            )\n\n            cmd = child_ref.init_delta_command(schema, sd.AlterObject)\n            cmd.add(rebase_cmd)\n        else:\n            # The ref in child should no longer exist.\n            cmd = child_ref.init_delta_command(schema, sd.DeleteObject)\n\n        return cmd\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        refctx = type(self).get_referrer_context(context)\n        if (\n            refctx is not None\n            and not self.get_orig_attribute_value('owned')\n        ):\n            return None\n        else:\n            return super()._get_ast(schema, context, parent_node=parent_node)\n\n\nclass AlterOwned(\n    ReferencedInheritingObjectCommand[ReferencedInheritingObjectT],\n    inheriting.AlterInheritingObjectFragment[ReferencedInheritingObjectT],\n    sd.AlterSpecialObjectField[ReferencedInheritingObjectT],\n):\n\n    _delta_action = 'alterowned'\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        orig_schema = schema\n        schema = super()._alter_begin(schema, context)\n        scls = self.scls\n\n        orig_owned = scls.get_owned(orig_schema)\n        owned = scls.get_owned(schema)\n\n        if (\n            orig_owned != owned\n            and not owned\n            and not context.canonical\n        ):\n            implicit_bases = scls.get_implicit_bases(schema)\n            if not implicit_bases:\n                # ref isn't actually inherited, so cannot be un-owned\n                vn = scls.get_verbosename(schema, with_parent=True)\n                sn = type(scls).get_schema_class_displayname().upper()\n                raise errors.InvalidDefinitionError(\n                    f'cannot drop owned {vn}, as it is not inherited, '\n                    f'use DROP {sn} instead',\n                    span=self.span,\n                )\n\n            # DROP OWNED requires special handling: the object in question\n            # must revert all modification made on top of inherited attributes.\n            bases = scls.get_bases(schema).objects(schema)\n            schema = self.inherit_fields(\n                schema,\n                context,\n                bases,\n                ignore_local=True,\n            )\n\n            for refdict in type(scls).get_refdicts():\n                schema = self._drop_owned_refs(schema, context, refdict)\n\n        return schema\n\n\nclass NamedReferencedInheritingObject(\n    ReferencedInheritingObject,\n):\n    \"\"\"A referenced inheriting object that has an explicit local name.\n\n    That is, things like pointers, access policies, and triggers,\n    which are referenced by another object but have an explicitly\n    specified name.\n    \"\"\"\n    @classmethod\n    def get_displayname_static(cls, name: sn.Name) -> str:\n        sn = cls.get_shortname_static(name)\n        if sn.module == '__':\n            return sn.name\n        else:\n            return str(sn)\n\n    def get_derived_name_base(\n        self,\n        schema: s_schema.Schema,\n    ) -> sn.QualName:\n        shortname = self.get_shortname(schema)\n        return sn.QualName(module='__', name=shortname.name)\n\n\nclass NamedReferencedInheritingObjectCommand(\n    ReferencedInheritingObjectCommand[ReferencedInheritingObjectT],\n):\n    # XXX: Do we want different namespaces for different kinds of objects?\n    @classmethod\n    def _classname_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext,\n    ) -> sn.QualName:\n        referrer_ctx = cls.get_referrer_context(context)\n        if referrer_ctx is not None:\n\n            referrer_name = context.get_referrer_name(referrer_ctx)\n\n            shortname = sn.QualName(module='__', name=astnode.name.name)\n\n            name = sn.QualName(\n                module=referrer_name.module,\n                name=sn.get_specialized_name(shortname, str(referrer_name)),\n            )\n        else:\n            name = super()._classname_from_ast(schema, astnode, context)\n\n        return name\n\n    def _deparse_name(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        name: sn.Name,\n    ) -> qlast.ObjectRef:\n\n        ref = super()._deparse_name(schema, context, name)\n        referrer_ctx = self.get_referrer_context(context)\n        if referrer_ctx is None:\n            return ref\n        else:\n            ref.module = ''\n            return ref\n"
  },
  {
    "path": "edb/schema/reflection/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom .reader import parse_schema, SchemaClassLayout\nfrom .structure import generate_structure\nfrom .structure import SchemaTypeLayout, SchemaReflectionParts\nfrom .writer import generate_metadata_write_edgeql\n\n__all__ = (\n    'generate_structure',\n    'generate_metadata_write_edgeql',\n    'parse_schema',\n    'SchemaTypeLayout',\n    'SchemaClassLayout',\n    'SchemaReflectionParts'\n)\n"
  },
  {
    "path": "edb/schema/reflection/reader.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Any, Callable\n\nimport collections\nimport functools\nimport json\nimport uuid\n\nimport immutables\n\nfrom edb.common import checked\nfrom edb.common import verutils\nfrom edb.common import uuidgen\n\nfrom edb.schema import abc as s_abc\nfrom edb.schema import expr as s_expr\nfrom edb.schema import functions as s_func\nfrom edb.schema import name as s_name\nfrom edb.schema import objects as s_obj\nfrom edb.schema import operators as s_oper\nfrom edb.schema import schema as s_schema\nfrom edb.schema import version as s_ver\n\nfrom . import structure as sr_struct\n\n\nSchemaClassLayout = dict[type[s_obj.Object], sr_struct.SchemaTypeLayout]\n\n\ndef parse_schema(\n    base_schema: s_schema.Schema,\n    data: str | bytes,\n    schema_class_layout: SchemaClassLayout,\n) -> s_schema.FlatSchema:\n    \"\"\"Parse JSON-encoded schema objects and populate the schema with them.\n\n    Args:\n        schema:\n            A schema instance to use as a starting point.\n        data:\n            A JSON-encoded schema object data as returned\n            by an introspection query.\n        schema_class_layout:\n            A mapping describing schema class layout in the reflection,\n            as returned from\n            :func:`schema.reflection.structure.generate_structure`.\n\n    Returns:\n        A schema instance including objects encoded in the provided\n        JSON sequence.\n    \"\"\"\n\n    id_to_type = {}\n    id_to_data = {}\n    name_to_id = {}\n    shortname_to_id = collections.defaultdict(set)\n    globalname_to_id = {}\n    dict_of_dicts: Callable[\n        [],\n        dict[tuple[type[s_obj.Object], str], dict[uuid.UUID, None]],\n    ] = functools.partial(collections.defaultdict, dict)\n    refs_to: dict[\n        uuid.UUID,\n        dict[tuple[type[s_obj.Object], str], dict[uuid.UUID, None]]\n    ] = collections.defaultdict(dict_of_dicts)\n\n    objects: dict[uuid.UUID, tuple[s_obj.Object, dict[str, Any]]] = {}\n    objid: uuid.UUID\n\n    for entry in json.loads(data):\n        _, _, clsname = entry['_tname'].rpartition('::')\n        mcls = s_obj.ObjectMeta.maybe_get_schema_class(clsname)\n        if mcls is None:\n            raise ValueError(\n                f'unexpected type in schema reflection: {clsname}')\n        objid = uuidgen.UUID(entry['id'])\n        objects[objid] = (mcls._create_from_id(objid), entry)\n\n    refdict_updates = {}\n\n    for objid, (obj, entry) in objects.items():\n        mcls = type(obj)\n        name = s_name.name_from_string(entry['name__internal'])\n        layout = schema_class_layout[mcls]\n\n        if (\n            base_schema.has_object(objid)\n            and not isinstance(obj, s_ver.BaseSchemaVersion)\n        ):\n            continue\n\n        if isinstance(obj, s_obj.QualifiedObject):\n            name_to_id[name] = objid\n        else:\n            name = s_name.UnqualName(str(name))\n            globalname_to_id[mcls, name] = objid\n\n        if isinstance(obj, (s_func.Function, s_oper.Operator)):\n            shortname = mcls.get_shortname_static(name)\n            shortname_to_id[mcls, shortname].add(objid)\n\n        id_to_type[objid] = type(obj).__name__\n\n        all_fields = mcls.get_schema_fields()\n        objdata: list[Any] = [None] * len(all_fields)\n        val: Any\n        refid: uuid.UUID\n\n        for k, v in entry.items():\n            desc = layout.get(k)\n            if desc is None:\n                continue\n\n            fn = desc.fieldname\n            field = all_fields.get(fn)\n            if field is None:\n                continue\n            findex = field.index\n\n            if desc.storage is not None:\n                if v is None:\n                    pass\n                elif desc.storage.ptrkind == 'link':\n                    refid = uuidgen.UUID(v['id'])\n                    newobj = objects.get(refid)\n                    if newobj is not None:\n                        val = newobj[0]\n                    else:\n                        val = base_schema.get_by_id(refid)\n                    objdata[findex] = val.schema_reduce()\n                    refs_to[val.id][mcls, fn][objid] = None\n\n                elif desc.storage.ptrkind == 'multi link':\n                    ftype = mcls.get_field(fn).type\n                    if issubclass(ftype, s_obj.ObjectDict):\n                        refids = ftype._container(\n                            uuidgen.UUID(e['value']) for e in v)\n                        refkeys = tuple(e['name'] for e in v)\n                        val = ftype(refids, refkeys, _private_init=True)\n                    else:\n                        refids = ftype._container(\n                            uuidgen.UUID(e['id']) for e in v)\n                        val = ftype(refids, _private_init=True)\n                    objdata[findex] = val.schema_reduce()\n                    for refid in refids:\n                        refs_to[refid][mcls, fn][objid] = None\n\n                elif desc.storage.shadow_ptrkind:\n                    val = entry[f'{k}__internal']\n                    ftype = mcls.get_field(fn).type\n                    if val is not None and type(val) is not ftype:\n                        if issubclass(ftype, s_expr.Expression):\n                            val = _parse_expression(val, objid, k)\n                            for refid in val.refs.ids():\n                                refs_to[refid][mcls, fn][objid] = None\n                        elif issubclass(ftype, s_expr.ExpressionList):\n                            exprs = []\n                            for e_dict in val:\n                                e = _parse_expression(e_dict, objid, k)\n                                assert e.refs is not None\n                                for refid in e.refs.ids():\n                                    refs_to[refid][mcls, fn][objid] = None\n                                exprs.append(e)\n                            val = ftype(exprs)\n                        elif issubclass(ftype, s_expr.ExpressionDict):\n                            expr_dict = dict()\n                            for e_dict in val:\n                                e = _parse_expression(\n                                    e_dict['expr'], objid, k)\n                                assert e.refs is not None\n                                for refid in e.refs.ids():\n                                    refs_to[refid][mcls, fn][objid] = None\n                                expr_dict[e_dict['name']] = e\n                            val = ftype(expr_dict)\n                        elif issubclass(ftype, s_obj.Object):\n                            val = val.id\n                        elif issubclass(ftype, s_name.Name):\n                            if isinstance(obj, s_obj.QualifiedObject):\n                                val = s_name.name_from_string(val)\n                            else:\n                                val = s_name.UnqualName(val)\n                        else:\n                            val = ftype(val)\n\n                    if issubclass(ftype, s_abc.Reducible):\n                        val = val.schema_reduce()\n                    objdata[findex] = val\n\n                else:\n                    ftype = mcls.get_field(fn).type\n                    if type(v) is not ftype:\n                        if issubclass(ftype, verutils.Version):\n                            objdata[findex] = _parse_version(v)\n                        elif issubclass(ftype, s_name.Name):\n                            objdata[findex] = s_name.name_from_string(v)\n                        elif (\n                            issubclass(ftype, checked.ParametricContainer)\n                            and ftype.types\n                            and len(ftype.types) == 1\n                        ):\n                            # Coerce the elements in a parametric container\n                            # type.\n                            # XXX: Or should we do it in the container?\n                            subtyp = ftype.types[0]\n                            objdata[findex] = ftype(\n                                subtyp(x) for x in v)  # type: ignore\n                        else:\n                            objdata[findex] = ftype(v)\n                    else:\n                        objdata[findex] = v\n\n            elif desc.is_refdict:\n                ftype = mcls.get_field(fn).type\n                refids = ftype._container(uuidgen.UUID(e['id']) for e in v)\n                for refid in refids:\n                    refs_to[refid][mcls, fn][objid] = None\n\n                val = ftype(refids, _private_init=True)\n                objdata[findex] = val.schema_reduce()\n                if desc.properties:\n                    for e_dict in v:\n                        refdict_updates[uuidgen.UUID(e_dict['id'])] = {\n                            p: pv for p in desc.properties\n                            if (pv := e_dict[f'@{p}']) is not None\n                        }\n\n        id_to_data[objid] = tuple(objdata)\n\n    for objid, updates in refdict_updates.items():\n        if updates:\n            sclass = s_obj.ObjectMeta.get_schema_class(id_to_type[objid])\n            updated_data = list(id_to_data[objid])\n            for fn, v in updates.items():\n                field = sclass.get_schema_field(fn)\n                updated_data[field.index] = v\n            id_to_data[objid] = tuple(updated_data)\n\n    refs_to_im = {}\n    for referred_id, ref_data in refs_to.items():\n        refs_to_im[referred_id] = immutables.Map((\n            (k, immutables.Map(r)) for k, r in ref_data.items()\n        ))\n\n    return s_schema.FlatSchema()._replace(\n        id_to_type=immutables.Map(id_to_type),\n        id_to_data=immutables.Map(id_to_data),\n        name_to_id=immutables.Map(name_to_id),\n        shortname_to_id=immutables.Map({\n            (k, frozenset(v)) for k, v in shortname_to_id.items()\n        }),\n        globalname_to_id=immutables.Map(globalname_to_id),\n        refs_to=immutables.Map(refs_to_im),\n    )\n\n\ndef _parse_expression(\n    val: dict[str, Any], id: uuid.UUID, field: str\n) -> s_expr.Expression:\n    refids = frozenset(\n        uuidgen.UUID(r) for r in val['refs']\n    )\n    expr = s_expr.Expression(\n        text=val['text'],\n        refs=s_obj.ObjectSet(\n            refids,\n            _private_init=True,\n        ),\n    )\n    expr.set_origin(id, field)\n    return expr\n\n\ndef _parse_version(val: dict[str, Any]) -> verutils.Version:\n    return verutils.Version(\n        major=val['major'],\n        minor=val['minor'],\n        stage=getattr(verutils.VersionStage, val['stage'].upper()),\n        stage_no=val['stage_no'],\n        local=tuple(val['local']),\n    )\n"
  },
  {
    "path": "edb/schema/reflection/structure.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, Sequence, NamedTuple\n\nimport collections\nimport uuid\n\nfrom edb.common import adapter\nfrom edb.common import checked\nfrom edb.common import enum\nfrom edb.common import verutils\n\nfrom edb.edgeql import qltypes\n\nfrom edb.schema import ddl as s_ddl\nfrom edb.schema import delta as sd\nfrom edb.schema import expr as s_expr\nfrom edb.schema import inheriting as s_inh\nfrom edb.schema import links as s_links\nfrom edb.schema import name as sn\nfrom edb.schema import objects as s_obj\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import schema as s_schema\nfrom edb.schema import types as s_types\n\n\nclass FieldType(enum.StrEnum):\n    \"\"\"Field type tag for fields requiring special handling.\"\"\"\n\n    #: An Expression field.\n    EXPR = 'EXPR'\n    #: An ExpressionList field.\n    EXPR_LIST = 'EXPR_LIST'\n    #: An ExpressionDict field.\n    EXPR_DICT = 'EXPR_DICT'\n    #: An ObjectDict field.\n    OBJ_DICT = 'OBJ_DICT'\n    #: All other field types.\n    OTHER = 'OTHER'\n\n\nclass FieldStorage(NamedTuple):\n    \"\"\"Schema object field storage descriptor.\"\"\"\n\n    #: Field type specifying special handling, if necessary.\n    fieldtype: FieldType\n    #: Pointer kind (property or link) and cardinality (single or multi)\n    ptrkind: str\n    #: Fully-qualified pointer target type.\n    ptrtype: str\n    #: Shadow pointer kind, if any.\n    shadow_ptrkind: Optional[str] = None\n    #: Shadow pointer type, if any.\n    shadow_ptrtype: Optional[str] = None\n\n\nclass SchemaFieldDesc(NamedTuple):\n    \"\"\"Schema object field descriptor.\"\"\"\n\n    type: s_types.Type\n    cardinality: qltypes.SchemaCardinality\n    properties: dict[str, tuple[s_types.Type, FieldType]]\n    fieldname: str\n    schema_fieldname: str\n    is_ordered: bool = False\n    reflection_proxy: Optional[tuple[str, str]] = None\n    storage: Optional[FieldStorage] = None\n    is_refdict: bool = False\n\n\n# N.B: Indexed by schema_fieldname\nSchemaTypeLayout = dict[str, SchemaFieldDesc]\n\n\nclass SchemaReflectionParts(NamedTuple):\n\n    intro_schema_delta: sd.Command\n    class_layout: dict[type[s_obj.Object], SchemaTypeLayout]\n    local_intro_parts: list[str]\n    global_intro_parts: list[str]\n\n\ndef _run_ddl(\n    ddl_text: str,\n    *,\n    schema: s_schema.Schema,\n    delta: sd.Command,\n) -> s_schema.Schema:\n\n    schema, cmd = s_ddl.apply_ddl_script_ex(\n        ddl_text,\n        schema=schema,\n        stdmode=True,\n        internal_schema_mode=True,\n    )\n\n    delta.update(cmd.get_subcommands())\n\n    return schema\n\n\ndef _classify_object_field(field: s_obj.Field[Any]) -> FieldStorage:\n    \"\"\"Determine FieldStorage for a given schema class field.\"\"\"\n\n    ftype = field.type\n    shadow_ptr_kind = None\n    shadow_ptr_type = None\n    fieldtype = FieldType.OTHER\n\n    is_array = is_multiprop = False\n    if issubclass(ftype, s_obj.MultiPropSet):\n        is_multiprop = True\n        ftype = ftype.type\n    elif (\n        issubclass(\n            ftype,\n            (checked.CheckedList, checked.FrozenCheckedList,\n             checked.CheckedSet, checked.FrozenCheckedSet))\n        and not issubclass(ftype, s_expr.ExpressionList)\n    ):\n        is_array = True\n        ftype = ftype.type  # type: ignore\n\n    if issubclass(ftype, s_obj.ObjectCollection):\n        ptr_kind = 'multi link'\n        ptr_type = 'schema::Object'\n        if issubclass(ftype, s_obj.ObjectDict):\n            fieldtype = FieldType.OBJ_DICT\n\n    elif issubclass(ftype, s_obj.Object):\n        ptr_kind = 'link'\n        ptr_type = f'schema::{ftype.__name__}'\n\n    elif issubclass(ftype, s_expr.Expression):\n        shadow_ptr_kind = 'property'\n        shadow_ptr_type = 'tuple<text: str, refs: array<uuid>>'\n        ptr_kind = 'property'\n        ptr_type = 'str'\n        fieldtype = FieldType.EXPR\n\n    elif issubclass(ftype, s_expr.ExpressionList):\n        shadow_ptr_kind = 'property'\n        shadow_ptr_type = (\n            'array<tuple<text: str, refs: array<uuid>>>'\n        )\n        ptr_kind = 'property'\n        ptr_type = 'array<str>'\n        fieldtype = FieldType.EXPR_LIST\n\n    elif issubclass(ftype, s_expr.ExpressionDict):\n        shadow_ptr_kind = 'property'\n        shadow_ptr_type = '''array<tuple<\n            name: str,\n            expr: tuple<text: str, refs: array<uuid>>\n        >>'''\n        ptr_kind = 'property'\n        ptr_type = 'array<tuple<name: str, expr: str>>'\n        fieldtype = FieldType.EXPR_DICT\n\n    elif issubclass(ftype, collections.abc.Mapping):\n        ptr_kind = 'property'\n        ptr_type = 'json'\n\n    elif issubclass(ftype, (str, sn.Name)):\n        ptr_kind = 'property'\n        ptr_type = 'str'\n\n        if field.name == 'name':\n            # TODO: consider shadow-reflecting names as tuples\n            shadow_ptr_kind = 'property'\n            shadow_ptr_type = 'str'\n\n    elif issubclass(ftype, bool):\n        ptr_kind = 'property'\n        ptr_type = 'bool'\n\n    elif issubclass(ftype, int):\n        ptr_kind = 'property'\n        ptr_type = 'int64'\n\n    elif issubclass(ftype, uuid.UUID):\n        ptr_kind = 'property'\n        ptr_type = 'uuid'\n\n    elif issubclass(ftype, verutils.Version):\n        ptr_kind = 'property'\n        ptr_type = '''\n            tuple<\n                major: std::int64,\n                minor: std::int64,\n                stage: sys::VersionStage,\n                stage_no: std::int64,\n                local: array<std::str>,\n            >\n        '''\n    else:\n        raise RuntimeError(\n            f'no metaschema reflection for field {field.name} of type {ftype}'\n        )\n\n    if is_multiprop:\n        ptr_kind = 'multi property'\n    if is_array:\n        ptr_type = f'array<{ptr_type}>'\n\n    return FieldStorage(\n        fieldtype=fieldtype,\n        ptrkind=ptr_kind,\n        ptrtype=ptr_type,\n        shadow_ptrkind=shadow_ptr_kind,\n        shadow_ptrtype=shadow_ptr_type,\n    )\n\n\ndef get_schema_name_for_pycls(py_cls: type[s_obj.Object]) -> sn.Name:\n    py_cls_name = py_cls.__name__\n    if issubclass(py_cls, s_obj.GlobalObject):\n        # Global objects, like Role and Database live in the sys:: module\n        return sn.QualName(module='sys', name=py_cls_name)\n    else:\n        return sn.QualName(module='schema', name=py_cls_name)\n\n\ndef get_default_base_for_pycls(py_cls: type[s_obj.Object]) -> sn.Name:\n    if issubclass(py_cls, s_obj.GlobalObject):\n        # Global objects, like Role and Database live in the sys:: module\n        return sn.QualName(module='sys', name='SystemObject')\n    else:\n        return sn.QualName(module='schema', name='Object')\n\n\ndef generate_structure(\n    schema: s_schema.Schema,\n    *,\n    make_funcs: bool=True,\n    patch_level: int=2**30,\n) -> SchemaReflectionParts:\n    \"\"\"Generate schema reflection structure from Python schema classes.\n\n    If specified, patch_level is the \"patch level\" of the currently\n    patch being applied during minor version upgrading. All schema\n    objects with patch levels that are higher will be skipped, to\n    avoid adding things created by later patches prematurely.\n\n    Returns:\n        A quadruple (as a SchemaReflectionParts instance) containing:\n            - Delta, which, when applied to stdlib, yields an enhanced\n              version of the `schema` module that contains all types\n              and properties, not just those that are publicly exposed\n              for introspection.\n            - A mapping, containing type layout description for all\n              schema classes.\n            - A sequence of EdgeQL queries necessary to introspect\n              a database schema.\n            - A sequence of EdgeQL queries necessary to introspect\n              global objects, such as roles and databases.\n\n    \"\"\"\n\n    delta = sd.DeltaRoot()\n    classlayout: dict[\n        type[s_obj.Object],\n        SchemaTypeLayout,\n    ] = {}\n\n    ordered_link = schema.get('schema::ordered', type=s_links.Link)\n\n    if make_funcs:\n        schema = _run_ddl(\n            '''\n            CREATE FUNCTION sys::_get_pg_type_for_edgedb_type(\n                typeid: std::uuid,\n                kind: std::str,\n                elemid: OPTIONAL std::uuid,\n                sql_type: OPTIONAL std::str,\n            ) -> std::int64 {\n                USING SQL FUNCTION 'edgedb.get_pg_type_for_edgedb_type';\n                SET volatility := 'STABLE';\n                SET impl_is_strict := false;\n            };\n\n            CREATE FUNCTION sys::_expr_from_json(\n                data: json\n            ) -> OPTIONAL tuple<text: str, refs: array<uuid>> {\n                USING SQL $$\n                    SELECT\n                        \"data\"->>'text'                     AS text,\n                        coalesce(r.refs, ARRAY[]::uuid[])   AS refs\n                    FROM\n                        (SELECT\n                            array_agg(v::uuid) AS refs\n                         FROM\n                            jsonb_array_elements_text(\"data\"->'refs') AS v\n                        ) AS r\n                    WHERE\n                        jsonb_typeof(\"data\") != 'null'\n                $$;\n                SET volatility := 'IMMUTABLE';\n            };\n\n            # A strictly-internal get config function that bypasses\n            # the redaction of secrets in the public-facing one.\n            CREATE FUNCTION\n            cfg::_get_config_json_internal(\n                NAMED ONLY sources: OPTIONAL array<std::str> = {},\n                NAMED ONLY max_source: OPTIONAL std::str = {}\n            ) -> std::json\n            {\n                USING SQL $$\n                SELECT\n                    coalesce(jsonb_object_agg(cfg.name, cfg), '{}'::jsonb)\n                FROM\n                    edgedb_VER._read_sys_config(\n                        sources::edgedb._sys_config_source_t[],\n                        max_source::edgedb._sys_config_source_t\n                    ) AS cfg\n                $$;\n            };\n\n            ''',\n            schema=schema,\n            delta=delta,\n        )\n\n    py_classes = []\n    for py_cls in s_obj.ObjectMeta.get_schema_metaclasses():\n        if isinstance(py_cls, adapter.Adapter):\n            continue\n\n        if py_cls is s_obj.GlobalObject:\n            continue\n\n        if py_cls._patch_level > patch_level:\n            continue\n\n        py_classes.append(py_cls)\n\n    read_sets: dict[type[s_obj.Object], list[str]] = {}\n\n    for py_cls in py_classes:\n        rschema_name = get_schema_name_for_pycls(py_cls)\n        schema_objtype = schema.get(\n            rschema_name,\n            type=s_objtypes.ObjectType,\n            default=None,\n        )\n\n        bases = []\n        for base in py_cls.__bases__:\n            if base in py_classes:\n                bases.append(get_schema_name_for_pycls(base))\n\n        default_base = get_default_base_for_pycls(py_cls)\n        if not bases and rschema_name != default_base:\n            bases.append(default_base)\n\n        reflection = py_cls.get_reflection_method()\n        is_simple_wrapper = issubclass(py_cls, s_types.CollectionExprAlias)\n\n        if schema_objtype is None:\n            as_abstract = (\n                reflection is s_obj.ReflectionMethod.REGULAR\n                and not is_simple_wrapper\n                and (\n                    py_cls is s_obj.InternalObject\n                    or not issubclass(py_cls, s_obj.InternalObject)\n                )\n                and py_cls._abstract is not False\n            )\n\n            schema = _run_ddl(\n                f'''\n                    CREATE {'ABSTRACT' if as_abstract else ''}\n                    TYPE {rschema_name}\n                    EXTENDING {', '.join(str(b) for b in bases)};\n                ''',\n                schema=schema,\n                delta=delta,\n            )\n\n            schema_objtype = schema.get(\n                rschema_name, type=s_objtypes.ObjectType)\n        else:\n            ex_bases = schema_objtype.get_bases(schema).names(schema)\n            _, added_bases = s_inh.delta_bases(\n                ex_bases,\n                bases,\n                t=type(schema_objtype),\n            )\n\n            if added_bases:\n                for subset, position in added_bases:\n                    # XXX: Don't generate changes for just moving around the\n                    # order of types when the mismatch between python and\n                    # the schema, since it doesn't work anyway and causes mass\n                    # grief when trying to patch the schema.\n                    subset = [x for x in subset if x.name not in ex_bases]\n                    if not subset:\n                        continue\n\n                    if isinstance(position, tuple):\n                        position_clause = (\n                            f'{position[0]} {position[1].name}'\n                        )\n                    else:\n                        position_clause = position\n\n                    bases_expr = ', '.join(str(t.name) for t in subset)\n\n                    stmt = f'''\n                        ALTER TYPE {rschema_name} {{\n                            EXTENDING {bases_expr} {position_clause}\n                        }}\n                    '''\n\n                    schema = _run_ddl(\n                        stmt,\n                        schema=schema,\n                        delta=delta,\n                    )\n\n        if reflection is s_obj.ReflectionMethod.NONE:\n            continue\n\n        referrers = py_cls.get_referring_classes()\n\n        if reflection is s_obj.ReflectionMethod.AS_LINK:\n            if not referrers:\n                raise RuntimeError(\n                    f'schema class {py_cls.__name__} is declared with AS_LINK '\n                    f'reflection method but is not referenced in any RefDict'\n                )\n\n        is_concrete = not schema_objtype.get_abstract(schema)\n\n        if (\n            is_concrete\n            and not is_simple_wrapper\n            and any(\n                not b.get_abstract(schema)\n                for b in schema_objtype.get_ancestors(schema).objects(schema)\n            )\n        ):\n            raise RuntimeError(\n                f'non-abstract {schema_objtype.get_verbosename(schema)} has '\n                f'non-abstract ancestors'\n            )\n\n        read_shape = read_sets[py_cls] = []\n\n        if is_concrete:\n            read_shape.append(\n                '_tname := .__type__[IS schema::ObjectType].name'\n            )\n\n        classlayout[py_cls] = {}\n        ownfields = py_cls.get_ownfields()\n\n        for fn, field in py_cls.get_fields().items():\n            if field.patch_level > patch_level:\n                continue\n\n            sfn = field.sname\n\n            if (\n                field.ephemeral\n                or (\n                    field.reflection_method\n                    is not s_obj.ReflectionMethod.REGULAR\n                )\n            ):\n                continue\n\n            storage = _classify_object_field(field)\n\n            ptr = schema_objtype.maybe_get_ptr(schema, sn.UnqualName(sfn))\n\n            if fn in ownfields:\n                qual = \"REQUIRED\" if field.required else \"OPTIONAL\"\n                otd = \" { ON TARGET DELETE ALLOW }\" if field.weak_ref else \"\"\n                if ptr is None:\n                    schema = _run_ddl(\n                        f'''\n                            ALTER TYPE {rschema_name} {{\n                                CREATE {qual}\n                                {storage.ptrkind} {sfn} -> {storage.ptrtype}\n                                {otd};\n                            }}\n                        ''',\n                        schema=schema,\n                        delta=delta,\n                    )\n                    ptr = schema_objtype.getptr(schema, sn.UnqualName(fn))\n\n                if storage.shadow_ptrkind is not None:\n                    pn = f'{sfn}__internal'\n                    internal_ptr = schema_objtype.maybe_get_ptr(\n                        schema, sn.UnqualName(pn))\n                    if internal_ptr is None:\n                        ptrkind = storage.shadow_ptrkind\n                        ptrtype = storage.shadow_ptrtype\n                        schema = _run_ddl(\n                            f'''\n                                ALTER TYPE {rschema_name} {{\n                                    CREATE {qual}\n                                    {ptrkind} {pn} -> {ptrtype};\n                                }}\n                            ''',\n                            schema=schema,\n                            delta=delta,\n                        )\n\n            else:\n                assert ptr is not None\n\n            if is_concrete:\n                read_ptr = sfn\n\n                if field.type_is_generic_self:\n                    read_ptr = f'{read_ptr}[IS {rschema_name}]'\n\n                if field.reflection_proxy:\n                    _proxy_type, proxy_link = field.reflection_proxy\n                    read_ptr = (\n                        f'{read_ptr}: {{name, value := .{proxy_link}.id}}'\n                    )\n\n                if ptr.issubclass(schema, ordered_link):\n                    read_ptr = f'{read_ptr} ORDER BY @index'\n\n                read_shape.append(read_ptr)\n\n                if storage.shadow_ptrkind is not None:\n                    read_shape.append(f'{sfn}__internal')\n\n            if field.reflection_proxy:\n                proxy_type_name, proxy_link_name = field.reflection_proxy\n                proxy_obj = schema.get(\n                    proxy_type_name, type=s_objtypes.ObjectType)\n                proxy_link_obj = proxy_obj.getptr(\n                    schema, sn.UnqualName(proxy_link_name))\n                tgt = proxy_link_obj.get_target(schema)\n            else:\n                tgt = ptr.get_target(schema)\n            assert tgt is not None\n            cardinality = ptr.get_cardinality(schema)\n            assert cardinality is not None\n            classlayout[py_cls][sfn] = SchemaFieldDesc(\n                fieldname=fn,\n                schema_fieldname=sfn,\n                type=tgt,\n                cardinality=cardinality,\n                properties={},\n                storage=storage,\n                is_ordered=ptr.issubclass(schema, ordered_link),\n                reflection_proxy=field.reflection_proxy,\n            )\n\n    # Second pass: deal with RefDicts, which are reflected as links.\n    for py_cls in py_classes:\n        rschema_name = get_schema_name_for_pycls(py_cls)\n        schema_cls = schema.get(rschema_name, type=s_objtypes.ObjectType)\n\n        for refdict in py_cls.get_own_refdicts().values():\n            if py_cls.get_field(refdict.attr).patch_level > patch_level:\n                continue\n\n            ref_ptr = schema_cls.maybe_get_ptr(\n                schema, sn.UnqualName(refdict.attr))\n            ref_cls = refdict.ref_cls\n            assert issubclass(ref_cls, s_obj.Object)\n            shadow_ref_ptr = None\n            reflect_as_link = (\n                ref_cls.get_reflection_method()\n                is s_obj.ReflectionMethod.AS_LINK\n            )\n\n            if reflect_as_link:\n                reflection_link = ref_cls.get_reflection_link()\n                assert reflection_link is not None\n                target_field = ref_cls.get_field(reflection_link)\n                target_cls = target_field.type\n                shadow_pn = f'{refdict.attr}__internal'\n                shadow_ref_ptr = schema_cls.maybe_get_ptr(\n                    schema, sn.UnqualName(shadow_pn))\n\n            if reflect_as_link and not shadow_ref_ptr:\n                schema = _run_ddl(\n                    f'''\n                        ALTER TYPE {rschema_name} {{\n                            CREATE OPTIONAL MULTI LINK {shadow_pn}\n                            EXTENDING schema::reference\n                             -> {get_schema_name_for_pycls(ref_cls)} {{\n                                 ON TARGET DELETE ALLOW;\n                             }};\n                        }}\n                    ''',\n                    schema=schema,\n                    delta=delta,\n                )\n                shadow_ref_ptr = schema_cls.getptr(\n                    schema, sn.UnqualName(shadow_pn))\n            else:\n                target_cls = ref_cls\n\n            if ref_ptr is None:\n                ptr_type = get_schema_name_for_pycls(target_cls)\n                schema = _run_ddl(\n                    f'''\n                        ALTER TYPE {rschema_name} {{\n                            CREATE OPTIONAL MULTI LINK {refdict.attr}\n                            EXTENDING schema::reference\n                             -> {ptr_type} {{\n                                 ON TARGET DELETE ALLOW;\n                             }};\n                        }}\n                    ''',\n                    schema=schema,\n                    delta=delta,\n                )\n\n                ref_ptr = schema_cls.getptr(\n                    schema, sn.UnqualName(refdict.attr))\n\n            assert isinstance(ref_ptr, s_links.Link)\n\n            if py_cls not in classlayout:\n                classlayout[py_cls] = {}\n\n            # First, fields declared to be reflected as link properties.\n            props = _get_reflected_link_props(\n                ref_ptr=ref_ptr,\n                target_cls=ref_cls,\n                schema=schema,\n            )\n\n            if reflect_as_link:\n                # Then, because it's a passthrough reflection, all scalar\n                # fields of the proxy object.\n                fields_as_props = [\n                    f\n                    for f in ref_cls.get_ownfields().values()\n                    if (\n                        not f.ephemeral\n                        and (\n                            f.reflection_method\n                            is not s_obj.ReflectionMethod.AS_LINK\n                        )\n                        and f.name != refdict.backref_attr\n                        and f.name != ref_cls.get_reflection_link()\n                    )\n                ]\n\n                extra_props = _classify_scalar_object_fields(fields_as_props)\n\n            for field, storage in {**props, **extra_props}.items():\n                sfn = field.sname\n                prop_ptr = ref_ptr.maybe_get_ptr(schema, sn.UnqualName(sfn))\n                if prop_ptr is None:\n                    pty = storage.ptrtype\n                    schema = _run_ddl(\n                        f'''\n                            ALTER TYPE {rschema_name} {{\n                                ALTER LINK {refdict.attr} {{\n                                    CREATE OPTIONAL PROPERTY {sfn} -> {pty};\n                                }}\n                            }}\n                        ''',\n                        schema=schema,\n                        delta=delta,\n                    )\n\n            if shadow_ref_ptr is not None:\n                assert isinstance(shadow_ref_ptr, s_links.Link)\n                shadow_pn = shadow_ref_ptr.get_shortname(schema).name\n                for field, storage in props.items():\n                    sfn = field.sname\n                    prop_ptr = shadow_ref_ptr.maybe_get_ptr(\n                        schema, sn.UnqualName(sfn))\n                    if prop_ptr is None:\n                        pty = storage.ptrtype\n                        schema = _run_ddl(\n                            f'''\n                                ALTER TYPE {rschema_name} {{\n                                    ALTER LINK {shadow_pn} {{\n                                        CREATE OPTIONAL PROPERTY {sfn}\n                                            -> {pty};\n                                    }}\n                                }}\n                            ''',\n                            schema=schema,\n                            delta=delta,\n                        )\n\n    for py_cls in py_classes:\n        rschema_name = get_schema_name_for_pycls(py_cls)\n        schema_cls = schema.get(rschema_name, type=s_objtypes.ObjectType)\n\n        is_concrete = not schema_cls.get_abstract(schema)\n        read_shape = read_sets[py_cls]\n\n        for refdict in py_cls.get_refdicts():\n            if py_cls.get_field(refdict.attr).patch_level > patch_level:\n                continue\n\n            if py_cls not in classlayout:\n                classlayout[py_cls] = {}\n\n            ref_ptr = schema_cls.getptr(\n                schema, sn.UnqualName(refdict.attr), type=s_links.Link)\n            assert ref_ptr\n            tgt = ref_ptr.get_target(schema)\n            assert tgt is not None\n            cardinality = ref_ptr.get_cardinality(schema)\n            assert cardinality is not None\n            classlayout[py_cls][refdict.attr] = SchemaFieldDesc(\n                fieldname=refdict.attr,\n                schema_fieldname=refdict.attr,\n                type=tgt,\n                cardinality=cardinality,\n                properties={},\n                is_ordered=ref_ptr.issubclass(schema, ordered_link),\n                reflection_proxy=None,\n                is_refdict=True,\n            )\n\n            target_cls = refdict.ref_cls\n\n            props = _get_reflected_link_props(\n                ref_ptr=ref_ptr,\n                target_cls=target_cls,\n                schema=schema,\n            )\n\n            reflect_as_link = (\n                target_cls.get_reflection_method()\n                is s_obj.ReflectionMethod.AS_LINK\n            )\n\n            prop_layout = {}\n            extra_prop_layout = {}\n\n            for field, storage in props.items():\n                prop_ptr = ref_ptr.getptr(schema, sn.UnqualName(field.sname))\n                prop_tgt = prop_ptr.get_target(schema)\n                assert prop_tgt is not None\n                prop_layout[field.name] = (prop_tgt, storage.fieldtype)\n\n            if reflect_as_link:\n                # Then, because it's a passthrough reflection, all scalar\n                # fields of the proxy object.\n                fields_as_props = [\n                    f\n                    for f in target_cls.get_ownfields().values()\n                    if (\n                        not f.ephemeral\n                        and (\n                            f.reflection_method\n                            is not s_obj.ReflectionMethod.AS_LINK\n                        )\n                        and f.name != refdict.backref_attr\n                        and f.name != target_cls.get_reflection_link()\n                    )\n                ]\n\n                extra_props = _classify_scalar_object_fields(fields_as_props)\n\n                for field, storage in extra_props.items():\n                    prop_ptr = ref_ptr.getptr(\n                        schema, sn.UnqualName(field.sname))\n                    prop_tgt = prop_ptr.get_target(schema)\n                    assert prop_tgt is not None\n                    extra_prop_layout[field.name] = (\n                        prop_tgt, storage.fieldtype)\n            else:\n                extra_prop_layout = {}\n\n            classlayout[py_cls][refdict.attr].properties.update({\n                **prop_layout, **extra_prop_layout,\n            })\n\n            if reflect_as_link:\n                shadow_tgt = schema.get(\n                    get_schema_name_for_pycls(ref_cls),\n                    type=s_objtypes.ObjectType,\n                )\n\n                iname = f'{refdict.attr}__internal'\n                classlayout[py_cls][iname] = (\n                    SchemaFieldDesc(\n                        fieldname=refdict.attr,\n                        schema_fieldname=iname,\n                        type=shadow_tgt,\n                        cardinality=qltypes.SchemaCardinality.Many,\n                        properties=prop_layout,\n                        is_refdict=True,\n                    )\n                )\n\n            if is_concrete:\n                read_ptr = refdict.attr\n                prop_shape_els = []\n\n                if reflect_as_link:\n                    read_ptr = f'{read_ptr}__internal'\n                    ref_ptr = schema_cls.getptr(\n                        schema,\n                        sn.UnqualName(f'{refdict.attr}__internal'),\n                    )\n\n                for field in props:\n                    sfn = field.sname\n                    prop_shape_els.append(f'@{sfn}')\n\n                if prop_shape_els:\n                    prop_shape = ',\\n'.join(prop_shape_els)\n                    read_ptr = f'{read_ptr}: {{id, {prop_shape}}}'\n\n                if ref_ptr.issubclass(schema, ordered_link):\n                    read_ptr = f'{read_ptr} ORDER BY @index'\n\n                read_shape.append(read_ptr)\n\n    local_parts = []\n    global_parts = []\n    for py_cls, shape_els in read_sets.items():\n        if (\n            not shape_els\n            # The CollectionExprAlias family needs to be excluded\n            # because TupleExprAlias and ArrayExprAlias inherit from\n            # concrete classes and so are picked up from those.\n            or issubclass(py_cls, s_types.CollectionExprAlias)\n        ):\n            continue\n\n        rschema_name = get_schema_name_for_pycls(py_cls)\n        shape = ',\\n'.join(shape_els)\n        qry = f'''\n            SELECT {rschema_name} {{\n                {shape}\n            }}\n        '''\n        if not issubclass(py_cls, (s_types.Collection, s_obj.GlobalObject)):\n            qry += ' FILTER NOT .builtin'\n\n        if issubclass(py_cls, s_obj.GlobalObject):\n            global_parts.append(qry)\n        else:\n            local_parts.append(qry)\n\n    delta.canonical = True\n    return SchemaReflectionParts(\n        intro_schema_delta=delta,\n        class_layout=classlayout,\n        local_intro_parts=local_parts,\n        global_intro_parts=global_parts,\n    )\n\n\ndef _get_reflected_link_props(\n    *,\n    ref_ptr: s_links.Link,\n    target_cls: type[s_obj.Object],\n    schema: s_schema.Schema,\n) -> dict[s_obj.Field[Any], FieldStorage]:\n\n    fields = [\n        f\n        for f in target_cls.get_fields().values()\n        if (\n            not f.ephemeral\n            and (\n                f.reflection_method\n                is s_obj.ReflectionMethod.AS_LINK\n            )\n        )\n    ]\n\n    return _classify_scalar_object_fields(fields)\n\n\ndef _classify_scalar_object_fields(\n    fields: Sequence[s_obj.Field[Any]],\n) -> dict[s_obj.Field[Any], FieldStorage]:\n\n    props = {}\n\n    for field in fields:\n        fn = field.name\n        storage = _classify_object_field(field)\n        if storage.ptrkind != 'property' and fn != 'id':\n            continue\n\n        props[field] = storage\n\n    return props\n"
  },
  {
    "path": "edb/schema/reflection/writer.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Schema reflection helpers.\"\"\"\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n    Collection,\n    cast,\n)\n\nimport functools\nimport json\nimport numbers\nimport textwrap\n\nfrom edb.edgeql import qltypes\n\nfrom edb.schema import constraints as s_constr\nfrom edb.schema import delta as sd\nfrom edb.schema import extensions as s_ext\nfrom edb.schema import objects as so\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import referencing as s_ref\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import schema as s_schema\nfrom edb.schema import types as s_types\n\nfrom edb.schema.reflection import structure as sr_struct\n\n\n@functools.singledispatch\ndef generate_metadata_write_edgeql(\n    cmd: sd.Command,\n    *,\n    classlayout: dict[type[so.Object], sr_struct.SchemaTypeLayout],\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    blocks: list[tuple[str, dict[str, Any]]],\n    internal_schema_mode: bool,\n    stdmode: bool,\n) -> None:\n\n    _hoist_if_unused_deletes(cmd)\n\n    return write_meta(\n        cmd,\n        classlayout=classlayout, schema=schema, context=context,\n        blocks=blocks, internal_schema_mode=internal_schema_mode,\n        stdmode=stdmode)\n\n\ndef _hoist_if_unused_deletes(\n    cmd: sd.Command,\n    target: Optional[sd.DeleteObject[so.Object]] = None,\n) -> None:\n    \"\"\"Hoist up if_unused deletes higher in the tree.\n\n    if_unused deletes for things like union and collection types need\n    to be done *after* the referring object that triggered the\n    deletion is deleted. There is special handling in the write_meta\n    case for DeleteObject to support this, but we need to also handle\n    the case where the delete of the union/collection happens down in\n    a nested delete of a child.\n\n    Work around this by hoisting up if_unused to the outermost enclosing\n    delete.  (We can't just hoist to the actual toplevel, because that\n    might move the command after something that needs to go *after*,\n    like a delete of one of the union components.)\n\n    Don't hoist the if_unused all the way *outside* an extension. We want the\n    effects of deleting an extension to be contained in the DeleteExtension\n    command. If there are union/collection types used outside this extension,\n    they won't be deleted. If the union/collection types are used only by this\n    extension, there is a chance that they also rely on the types *from* the\n    extension. This means that it will be impossible to delete the base types\n    if we defer deleting the union/collection types until all extension\n    content is removed.\n\n    FIXME: Could we instead *generate* the deletions at the outermost point?\n    \"\"\"\n    new_target = target\n    if (\n        not new_target\n        and isinstance(cmd, sd.DeleteObject)\n        and not isinstance(cmd, s_ext.DeleteExtension)\n    ):\n        new_target = cmd\n\n    for sub in cmd.get_subcommands():\n        if (\n            isinstance(sub, sd.DeleteObject)\n            and target\n            and sub.if_unused\n        ):\n            cmd.discard(sub)\n            target.add_caused(sub)\n        else:\n            _hoist_if_unused_deletes(sub, new_target)\n\n\n@functools.singledispatch\ndef write_meta(\n    cmd: sd.Command,\n    *,\n    classlayout: dict[type[so.Object], sr_struct.SchemaTypeLayout],\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    blocks: list[tuple[str, dict[str, Any]]],\n    internal_schema_mode: bool,\n    stdmode: bool,\n) -> None:\n    \"\"\"Generate EdgeQL statements populating schema metadata.\n\n    Args:\n        cmd:\n            Delta command tree for which EdgeQL DML must be generated.\n        classlayout:\n            Schema class layout as returned from\n            :func:`schema.reflection.structure.generate_structure`.\n        schema:\n            A schema instance.\n        context:\n            Delta context corresponding to *cmd*.\n        blocks:\n            A list where a sequence of (edgeql, args) tuples will\n            be appended to.\n        internal_schema_mode:\n            If True, *cmd* represents internal `schema` modifications.\n        stdmode:\n            If True, *cmd* represents a standard library bootstrap DDL.\n    \"\"\"\n    raise NotImplementedError(f\"cannot handle {cmd!r}\")\n\n\ndef _descend(\n    cmd: sd.Command,\n    *,\n    classlayout: dict[type[so.Object], sr_struct.SchemaTypeLayout],\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    blocks: list[tuple[str, dict[str, Any]]],\n    internal_schema_mode: bool,\n    stdmode: bool,\n    prerequisites: bool = False,\n    cmd_filter: Optional[Callable[[sd.Command], bool]] = None,\n) -> None:\n\n    if prerequisites:\n        commands = cmd.get_prerequisites()\n    else:\n        commands = cmd.get_subcommands(include_prerequisites=False)\n\n    if cmd_filter:\n        commands = tuple(filter(cmd_filter, commands))\n\n    def _write_subcommands(commands: Collection[sd.Command]) -> None:\n        for subcmd in commands:\n            if not isinstance(subcmd, sd.AlterObjectProperty):\n                write_meta(\n                    subcmd,\n                    classlayout=classlayout,\n                    schema=schema,\n                    context=context,\n                    blocks=blocks,\n                    internal_schema_mode=internal_schema_mode,\n                    stdmode=stdmode\n                )\n\n    ctxcls = cmd.get_context_class()\n    if ctxcls is not None:\n        if (\n            issubclass(ctxcls, sd.ObjectCommandContext)\n            and isinstance(cmd, sd.ObjectCommand)\n        ):\n            objctxcls = cast(\n                type[sd.ObjectCommandContext[so.Object]],\n                ctxcls,\n            )\n            ctx = objctxcls(schema=schema, op=cmd, scls=sd._dummy_object)\n        else:\n            # I could not find a way to convince mypy here.\n            ctx = ctxcls(schema=schema, op=cmd)  # type: ignore\n\n        with context(ctx):\n            _write_subcommands(commands)\n    else:\n        _write_subcommands(commands)\n\n\n@write_meta.register\ndef write_meta_delta_root(\n    cmd: sd.DeltaRoot,\n    *,\n    classlayout: dict[type[so.Object], sr_struct.SchemaTypeLayout],\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    blocks: list[tuple[str, dict[str, Any]]],\n    internal_schema_mode: bool,\n    stdmode: bool,\n) -> None:\n    _descend(\n        cmd,\n        classlayout=classlayout,\n        schema=schema,\n        context=context,\n        blocks=blocks,\n        internal_schema_mode=internal_schema_mode,\n        stdmode=stdmode,\n    )\n\n\ndef _build_object_mutation_shape(\n    cmd: sd.ObjectCommand[so.Object],\n    *,\n    classlayout: dict[type[so.Object], sr_struct.SchemaTypeLayout],\n    lprop_fields: Optional[\n        dict[str, tuple[s_types.Type, sr_struct.FieldType]]\n    ] = None,\n    lprops_only: bool = False,\n    internal_schema_mode: bool,\n    stdmode: bool,\n    var_prefix: str = '',\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n) -> tuple[str, dict[str, Any]]:\n\n    props = cmd.get_resolved_attributes(schema, context)\n    mcls = cmd.get_schema_metaclass()\n    layout = classlayout[mcls]\n\n    if lprop_fields is None:\n        lprop_fields = {}\n\n    # XXX: This is a hack around the fact that _update_lprops works by\n    # removing all the links and recreating them. Since that will lose\n    # data in situations where not every lprop attribute is specified,\n    # merge AlterOwned props up into the enclosing command. (This avoids\n    # trouble with annotations, which is the main place where we have\n    # multiple interesting lprops at once.)\n    if isinstance(cmd, s_ref.AlterOwned):\n        return '', {}\n    for sub in cmd.get_subcommands(type=s_ref.AlterOwned):\n        props.update(sub.get_resolved_attributes(schema, context))\n\n    assignments = []\n    variables: dict[str, str] = {}\n    if isinstance(cmd, sd.CreateObject):\n        empties = {\n            v.fieldname: None for f, v in layout.items()\n            if (\n                f != 'backend_id'\n                and v.storage is not None\n                and v.storage.ptrkind != 'link'\n                and v.storage.ptrkind != 'multi link'\n            )\n        }\n        all_props = {**empties, **props}\n    else:\n        all_props = props\n\n    for n, v in sorted(all_props.items(), key=lambda i: i[0]):\n        ns = mcls.get_field(n).sname\n\n        lprop_target = lprop_fields.get(n)\n        if lprop_target is not None:\n            target, ftype = lprop_target\n            cardinality = qltypes.SchemaCardinality.One\n            is_ordered = False\n            reflection_proxy = None\n        elif lprops_only:\n            continue\n        else:\n            layout_entry = layout.get(ns)\n            if layout_entry is None:\n                # The field is ephemeral, skip it.\n                continue\n            else:\n                target = layout_entry.type\n                cardinality = layout_entry.cardinality\n                is_ordered = layout_entry.is_ordered\n                reflection_proxy = layout_entry.reflection_proxy\n                assert layout_entry.storage is not None\n                ftype = layout_entry.storage.fieldtype\n\n        target_value: Any\n\n        var_n = f'__{var_prefix}{n}'\n\n        if (\n            issubclass(mcls, s_constr.Constraint)\n            and n == 'params'\n            and isinstance(cmd, s_ref.ReferencedObjectCommand)\n            and cmd.get_referrer_context(context) is not None\n        ):\n            # Constraint args are represented as a `@value` link property\n            # on the `params` link.\n            # TODO: replace this hack by a generic implementation of\n            # an ObjectKeyDict collection that allow associating objects\n            # with arbitrary values (a transposed ObjectDict).\n            target_expr = f\"\"\"assert_distinct((\n                FOR v IN {{ enumerate(json_array_unpack(<json>${var_n})) }}\n                UNION (\n                    SELECT {target.get_name(schema)} {{\n                        @index := v.0,\n                        @value := <str>v.1[1],\n                    }}\n                    FILTER .id = <uuid>v.1[0]\n                )\n            ))\"\"\"\n            args = props.get('args', [])\n            target_value = []\n            if v is not None:\n                for i, param in enumerate(v.objects(schema)):\n                    if i == 0:\n                        # skip the implicit __subject__ parameter\n                        arg_expr = ''\n                    else:\n                        try:\n                            arg = args[i - 1]\n                        except IndexError:\n                            arg_expr = ''\n                        else:\n                            pkind = param.get_kind(schema)\n                            if pkind is qltypes.ParameterKind.VariadicParam:\n                                rest = [arg.text for arg in args[i - 1:]]\n                                arg_expr = f'[{\",\".join(rest)}]'\n                            else:\n                                arg_expr = arg.text\n\n                    target_value.append((str(param.id), arg_expr))\n\n        elif n == 'name':\n            target_expr = f'<str>${var_n}'\n            assignments.append(f'{ns}__internal := <str>${var_n}__internal')\n            if v is not None:\n                target_value = mcls.get_displayname_static(v)\n                variables[f'{var_n}__internal'] = json.dumps(str(v))\n            else:\n                target_value = None\n                variables[f'{var_n}__internal'] = json.dumps(None)\n\n        elif isinstance(target, s_objtypes.ObjectType):\n            if cardinality is qltypes.SchemaCardinality.Many:\n                if ftype is sr_struct.FieldType.OBJ_DICT:\n                    target_expr, target_value = _reflect_object_dict_value(\n                        schema=schema,\n                        value=v,\n                        is_ordered=is_ordered,\n                        value_var_name=var_n,\n                        target=target,\n                        reflection_proxy=reflection_proxy,\n                    )\n                elif is_ordered:\n                    target_expr = f'''(\n                        FOR v IN {{\n                            enumerate(assert_distinct(\n                                <uuid>json_array_unpack(<json>${var_n})\n                            ))\n                        }}\n                        UNION (\n                            SELECT (DETACHED {target.get_name(schema)}) {{\n                                @index := v.0,\n                            }}\n                            FILTER .id = v.1\n                        )\n                    )'''\n                    if v is not None:\n                        target_value = [str(i) for i in v.ids()]\n                    else:\n                        target_value = []\n                else:\n                    target_expr = f'''(\n                        SELECT (DETACHED {target.get_name(schema)})\n                        FILTER .id IN <uuid>json_array_unpack(<json>${var_n})\n                    )'''\n                    if v is not None:\n                        target_value = [str(i) for i in v.ids()]\n                    else:\n                        target_value = []\n            else:\n                target_expr = f'''(\n                    SELECT (DETACHED {target.get_name(schema)})\n                    FILTER .id = <uuid>${var_n}\n                )'''\n                if v is not None:\n                    target_value = str(v.id)\n                else:\n                    target_value = None\n\n        elif ftype is sr_struct.FieldType.EXPR:\n            target_expr = f'<str>${var_n}'\n            if v is not None:\n                target_value = v.text\n            else:\n                target_value = None\n\n            shadow_target_expr = (\n                f'sys::_expr_from_json(<json>${var_n}_expr)'\n            )\n\n            assignments.append(f'{ns}__internal := {shadow_target_expr}')\n            if v is not None:\n                ids = [str(i) for i in v.refs.ids()]\n                variables[f'{var_n}_expr'] = json.dumps(\n                    {'text': v.text, 'refs': ids}\n                )\n            else:\n                variables[f'{var_n}_expr'] = json.dumps(None)\n\n        elif ftype is sr_struct.FieldType.EXPR_LIST:\n            target_expr = f'''\n                array_agg(<str>json_array_unpack(<json>${var_n})[\"text\"])\n            '''\n            if v is not None:\n                target_value = [\n                    {\n                        'text': ex.text,\n                        'refs': (\n                            [str(i) for i in ex.refs.ids()]\n                            if ex.refs else []\n                        )\n                    }\n                    for ex in v\n                ]\n            else:\n                target_value = []\n\n            shadow_target_expr = f'''\n                (SELECT\n                    array_agg(\n                        sys::_expr_from_json(\n                            json_array_unpack(<json>${var_n})\n                        )\n                    )\n                )\n            '''\n\n            assignments.append(f'{ns}__internal := {shadow_target_expr}')\n\n        elif ftype is sr_struct.FieldType.EXPR_DICT:\n            target_expr = f'''\n                (\n                    WITH\n                        orig_json := json_array_unpack(<json>${var_n})\n                    SELECT\n                        array_agg((\n                            for orig_json in orig_json union\n                            (\n                                name := <str>orig_json['name'],\n                                expr := <str>orig_json['expr']['text'],\n                            )\n                        ))\n                )\n            '''\n            if v is not None:\n                target_value = [\n                    {\n                        'name': key,\n                        'expr': {\n                            'text': ex.text,\n                            'refs': (\n                                [str(i) for i in ex.refs.ids()]\n                                if ex.refs else []\n                            )\n                        }\n                    }\n                    for key, ex in v.items()\n                ]\n            else:\n                target_value = []\n\n            shadow_target_expr = f'''\n                (\n                    WITH\n                        orig_json := json_array_unpack(<json>${var_n})\n                    SELECT\n                        array_agg((\n                            for orig_json in orig_json union\n                            (\n                                name := <str>orig_json['name'],\n                                expr := sys::_expr_from_json(\n                                    orig_json['expr']\n                                )\n                            )\n                        ))\n                )\n            '''\n\n            assignments.append(f'{ns}__internal := {shadow_target_expr}')\n\n        elif isinstance(target, s_types.Array):\n            eltype = target.get_element_type(schema)\n            target_expr = f'''\n                array_agg(<{eltype.get_name(schema)}>\n                    json_array_unpack(<json>${var_n}))\n                IF json_typeof(<json>${var_n}) != 'null'\n                ELSE <array<{eltype.get_name(schema)}>>{{}}\n            '''\n            if v is not None:\n                target_value = list(v)\n            else:\n                target_value = None\n\n        else:\n            target_expr = f'${var_n}'\n            if cardinality and cardinality.is_multi():\n                target_expr = f'json_array_unpack(<json>{target_expr})'\n            if target.is_enum(schema):\n                target_expr = f'<str>{target_expr}'\n            target_expr = f'<{target.get_name(schema)}>{target_expr}'\n\n            if v is not None and cardinality.is_multi():\n                target_value = list(v)\n            elif v is None or isinstance(v, numbers.Number):\n                target_value = v\n            else:\n                target_value = str(v)\n\n        if lprop_target is not None:\n            assignments.append(f'@{ns} := {target_expr}')\n        else:\n            assignments.append(f'{ns} := {target_expr}')\n\n        variables[var_n] = json.dumps(target_value)\n\n    object_actually_exists = schema.has_object(cmd.scls.id)\n    if (\n        isinstance(cmd, sd.CreateObject)\n        and object_actually_exists\n        and issubclass(mcls, (s_scalars.ScalarType, s_types.Collection))\n        and not issubclass(mcls, s_types.CollectionExprAlias)\n        and not cmd.get_attribute_value('abstract')\n        and not cmd.get_attribute_value('transient')\n        and not cmd.has_attribute_value('backend_id')\n    ):\n        kind = f'\"schema::{mcls.__name__}\"'\n\n        if issubclass(mcls, (s_types.Array, s_types.Range, s_types.MultiRange)):\n            assignments.append(\n                f'backend_id := sys::_get_pg_type_for_edgedb_type('\n                f'<uuid>$__{var_prefix}id, '\n                f'{kind}, '\n                f'<uuid>$__{var_prefix}element_type, '\n                f'<str>$__{var_prefix}sql_type2), '\n            )\n        else:\n            assignments.append(\n                f'backend_id := sys::_get_pg_type_for_edgedb_type('\n                f'<uuid>$__{var_prefix}id, {kind}, <uuid>{{}}, '\n                f'<str>$__{var_prefix}sql_type2), '\n            )\n        sql_type = None\n        if isinstance(cmd.scls, s_scalars.ScalarType):\n            sql_type, _ = cmd.scls.resolve_sql_type_scheme(schema)\n\n        variables[f'__{var_prefix}id'] = json.dumps(\n            str(cmd.get_attribute_value('id'))\n        )\n        variables[f'__{var_prefix}sql_type2'] = json.dumps(sql_type)\n\n    shape = ',\\n'.join(assignments)\n\n    return shape, variables\n\n\ndef _reflect_object_dict_value(\n    *,\n    schema: s_schema.Schema,\n    value: Optional[so.ObjectDict[str, so.Object]],\n    is_ordered: bool,\n    value_var_name: str,\n    target: s_types.Type,\n    reflection_proxy: Optional[tuple[str, str]],\n) -> tuple[str, Any]:\n\n    if reflection_proxy is not None:\n        # Non-unique ObjectDict, reflecting via a proxy object\n        proxy_type, proxy_link = reflection_proxy\n\n        if is_ordered:\n            target_expr = f'''(\n                FOR v IN {{\n                    enumerate(\n                        json_array_unpack(<json>${value_var_name})\n                    )\n                }}\n                UNION (\n                    INSERT {proxy_type} {{\n                        {proxy_link} := (\n                            SELECT (DETACHED {target.get_name(schema)})\n                            FILTER .id = <uuid>v.1[1]\n                        ),\n                        name := <str>v.1[0],\n                        @index := v.0,\n                    }}\n                )\n            )'''\n        else:\n            target_expr = f'''(\n                FOR v IN {{\n                    json_array_unpack(<json>${value_var_name})\n                }}\n                UNION (\n                    INSERT {proxy_type} {{\n                        {proxy_link} := (\n                            SELECT (DETACHED {target.get_name(schema)})\n                            FILTER .id = <uuid>v[1]\n                        ),\n                        name := <str>v[0],\n                    }}\n                )\n            )'''\n    else:\n        if is_ordered:\n            target_expr = f'''(\n                FOR v IN {{\n                    enumerate(\n                        json_array_unpack(<json>${value_var_name})\n                    )\n                }}\n                UNION (\n                    SELECT (DETACHED {target.get_name(schema)}) {{\n                        name := v.1[0],\n                        @index := v.0,\n                    }}\n                    FILTER .id = <uuid>v.1[1]\n                )\n            )'''\n        else:\n            target_expr = f'''(\n                FOR v IN {{\n                    json_array_unpack(<json>${value_var_name})\n                }}\n                UNION (\n                    SELECT (DETACHED {target.get_name(schema)}) {{\n                        @key := v[0],\n                    }}\n                    FILTER .id = <uuid>v[1]\n                )\n            )'''\n\n    if value is None:\n        target_value = []\n    else:\n        target_value = [(n, str(i.id)) for n, i in value.items(schema)]\n\n    return target_expr, target_value\n\n\n# type ignore below because mypy's wishes of generic parametrization\n# clash with the expectations of singledispatch receiving an actual type.\n@write_meta.register\ndef write_meta_create_object(\n    cmd: sd.CreateObject,  # type: ignore\n    *,\n    classlayout: dict[type[so.Object], sr_struct.SchemaTypeLayout],\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    blocks: list[tuple[str, dict[str, Any]]],\n    internal_schema_mode: bool,\n    stdmode: bool,\n) -> None:\n    _descend(\n        cmd,\n        classlayout=classlayout,\n        schema=schema,\n        context=context,\n        blocks=blocks,\n        prerequisites=True,\n        internal_schema_mode=internal_schema_mode,\n        stdmode=stdmode,\n    )\n\n    mcls = cmd.maybe_get_schema_metaclass()\n    if mcls is not None and not issubclass(mcls, so.GlobalObject):\n        if isinstance(cmd, s_ref.ReferencedObjectCommand):\n            refctx = cmd.get_referrer_context(context)\n        else:\n            refctx = None\n\n        if refctx is None:\n            shape, variables = _build_object_mutation_shape(\n                cmd,\n                classlayout=classlayout,\n                internal_schema_mode=internal_schema_mode,\n                stdmode=stdmode,\n                schema=schema,\n                context=context,\n            )\n\n            insert_query = f'''\n                INSERT schema::{mcls.__name__} {{\n                    {shape}\n                }}\n            '''\n\n            blocks.append((insert_query, variables))\n        else:\n            refop = refctx.op\n            refcls = refop.get_schema_metaclass()\n            refdict = refcls.get_refdict_for_class(mcls)\n            layout = classlayout[refcls][refdict.attr]\n            lprops = layout.properties\n\n            reflect_as_link = (\n                mcls.get_reflection_method() is so.ReflectionMethod.AS_LINK\n            )\n\n            shape, variables = _build_object_mutation_shape(\n                cmd,\n                classlayout=classlayout,\n                lprop_fields=lprops,\n                lprops_only=reflect_as_link,\n                internal_schema_mode=internal_schema_mode,\n                stdmode=stdmode,\n                schema=schema,\n                context=context,\n            )\n\n            assignments = []\n\n            if reflect_as_link:\n                target_link = mcls.get_reflection_link()\n                assert target_link is not None\n                target_field = mcls.get_field(target_link)\n                target = cmd.get_attribute_value(target_link)\n\n                append_query = f'''\n                    SELECT DETACHED schema::{target_field.type.__name__} {{\n                        {shape}\n                    }} FILTER\n                        .name__internal = <str>$__{target_link}\n                '''\n\n                variables[f'__{target_link}'] = (\n                    json.dumps(str(target.get_name(schema)))\n                )\n\n                shadow_clslayout = classlayout[refcls]\n                shadow_link_layout = (\n                    shadow_clslayout[f'{refdict.attr}__internal'])\n                shadow_shape, shadow_variables = _build_object_mutation_shape(\n                    cmd,\n                    classlayout=classlayout,\n                    internal_schema_mode=internal_schema_mode,\n                    lprop_fields=shadow_link_layout.properties,\n                    stdmode=stdmode,\n                    var_prefix='shadow_',\n                    schema=schema,\n                    context=context,\n                )\n\n                variables.update(shadow_variables)\n\n                shadow_append_query = f'''\n                    INSERT schema::{mcls.__name__} {{\n                        {shadow_shape}\n                    }}\n                '''\n\n                assignments.append(f'''\n                    {refdict.attr}__internal += (\n                        {shadow_append_query}\n                    )\n                ''')\n\n            else:\n                append_query = f'''\n                    INSERT schema::{mcls.__name__} {{\n                        {shape}\n                    }}\n                '''\n\n            assignments.append(f'''\n                {refdict.attr} += (\n                    {append_query}\n                )\n            ''')\n\n            update_shape = ',\\n'.join(assignments)\n\n            parent_update_query = f'''\n                UPDATE schema::{refcls.__name__}\n                FILTER .name__internal = <str>$__parent_classname\n                SET {{\n                    {update_shape}\n                }}\n            '''\n\n            ref_name = context.get_referrer_name(refctx)\n            variables['__parent_classname'] = json.dumps(str(ref_name))\n            blocks.append((parent_update_query, variables))\n\n    _descend(\n        cmd,\n        classlayout=classlayout,\n        schema=schema,\n        context=context,\n        blocks=blocks,\n        internal_schema_mode=internal_schema_mode,\n        stdmode=stdmode,\n    )\n\n\n@write_meta.register\ndef write_meta_alter_object(\n    cmd: sd.ObjectCommand,  # type: ignore\n    *,\n    classlayout: dict[type[so.Object], sr_struct.SchemaTypeLayout],\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    blocks: list[tuple[str, dict[str, Any]]],\n    internal_schema_mode: bool,\n    stdmode: bool,\n) -> None:\n    _descend(\n        cmd,\n        classlayout=classlayout,\n        schema=schema,\n        context=context,\n        blocks=blocks,\n        prerequisites=True,\n        internal_schema_mode=internal_schema_mode,\n        stdmode=stdmode,\n    )\n\n    mcls = cmd.maybe_get_schema_metaclass()\n    if mcls is not None and not issubclass(mcls, so.GlobalObject):\n        shape, variables = _build_object_mutation_shape(\n            cmd,\n            classlayout=classlayout,\n            internal_schema_mode=internal_schema_mode,\n            stdmode=stdmode,\n            schema=schema,\n            context=context,\n        )\n\n        if shape:\n            query = f'''\n                UPDATE schema::{mcls.__name__}\n                FILTER .name__internal = <str>$__classname\n                SET {{\n                    {shape}\n                }};\n            '''\n            variables['__classname'] = json.dumps(str(cmd.classname))\n            blocks.append((query, variables))\n\n        if isinstance(cmd, s_ref.ReferencedObjectCommand):\n            refctx = cmd.get_referrer_context(context)\n            if refctx is not None:\n                _update_lprops(\n                    cmd,\n                    classlayout=classlayout,\n                    schema=schema,\n                    blocks=blocks,\n                    context=context,\n                    internal_schema_mode=internal_schema_mode,\n                    stdmode=stdmode,\n                )\n\n    _descend(\n        cmd,\n        classlayout=classlayout,\n        schema=schema,\n        context=context,\n        blocks=blocks,\n        internal_schema_mode=internal_schema_mode,\n        stdmode=stdmode,\n    )\n\n\ndef _update_lprops(\n    cmd: s_ref.ReferencedObjectCommand,  # type: ignore\n    *,\n    classlayout: dict[type[so.Object], sr_struct.SchemaTypeLayout],\n    schema: s_schema.Schema,\n    blocks: list[tuple[str, dict[str, Any]]],\n    context: sd.CommandContext,\n    internal_schema_mode: bool,\n    stdmode: bool,\n) -> None:\n    mcls = cmd.get_schema_metaclass()\n    refctx = cmd.get_referrer_context_or_die(context)\n    refop = refctx.op\n    refcls = refop.get_schema_metaclass()\n    refdict = refcls.get_refdict_for_class(mcls)\n    layout = classlayout[refcls][refdict.attr]\n    lprops = layout.properties\n\n    if not lprops:\n        return\n\n    reflect_as_link = (\n        mcls.get_reflection_method() is so.ReflectionMethod.AS_LINK\n    )\n\n    # N.B: For reflect_as_link AlterObjects, we depend on all of the\n    # relevant fields having been populated in the command, which is\n    # done by _populate_link_reflection_fields.\n    if reflect_as_link:\n        target_link = mcls.get_reflection_link()\n        assert target_link is not None\n        target_field = mcls.get_field(target_link)\n        target_obj = cmd.get_ddl_identity(target_link)\n        if target_obj is None:\n            raise AssertionError(\n                f'cannot find link target in ddl_identity of a command for '\n                f'schema class reflected as link: {cmd!r}'\n            )\n        target_clsname = target_field.type.__name__\n    else:\n        referrer_cls = refop.get_schema_metaclass()\n        target_field = referrer_cls.get_field(refdict.attr)\n        if issubclass(target_field.type, so.ObjectCollection):\n            target_type = target_field.type.type\n        else:\n            target_type = target_field.type\n        target_clsname = target_type.__name__\n        target_link = refdict.attr\n        target_obj = cmd.scls\n\n    shape, append_variables = _build_object_mutation_shape(\n        cmd,\n        classlayout=classlayout,\n        lprop_fields=lprops,\n        lprops_only=True,\n        internal_schema_mode=internal_schema_mode,\n        stdmode=stdmode,\n        schema=schema,\n        context=context,\n    )\n\n    if shape:\n        parent_variables = {}\n        parent_variables[f'__{target_link}'] = json.dumps(str(target_obj.id))\n        ref_name = context.get_referrer_name(refctx)\n        parent_variables['__parent_classname'] = json.dumps(str(ref_name))\n\n        # XXX: we have to do a -= followed by a += because\n        # support for filtered nested link property updates\n        # is currently broken.\n        # This is fragile! If not all of the lprops are specified,\n        # we will drop them.\n\n        assignments = []\n\n        assignments.append(textwrap.dedent(\n            f'''\\\n            {refdict.attr} -= (\n                SELECT DETACHED (schema::{target_clsname})\n                FILTER .id = <uuid>$__{target_link}\n            )'''\n        ))\n\n        if reflect_as_link:\n            parent_variables[f'__{target_link}_shadow'] = (\n                json.dumps(str(cmd.classname)))\n\n            assignments.append(textwrap.dedent(\n                f'''\\\n                {refdict.attr}__internal -= (\n                    SELECT DETACHED (schema::{mcls.__name__})\n                    FILTER .name__internal = <str>$__{target_link}_shadow\n                )'''\n            ))\n\n        update_shape = textwrap.indent(\n            '\\n' + ',\\n'.join(assignments), '    ' * 4)\n\n        parent_update_query = textwrap.dedent(f'''\\\n            UPDATE schema::{refcls.__name__}\n            FILTER .name__internal = <str>$__parent_classname\n            SET {{{update_shape}\n            }}\n        ''')\n\n        blocks.append((parent_update_query, parent_variables))\n\n        assignments = []\n\n        shape = textwrap.indent(f'\\n{shape}', '    ' * 5)\n\n        assignments.append(textwrap.dedent(\n            f'''\\\n            {refdict.attr} += (\n                SELECT DETACHED schema::{target_clsname} {{{shape}\n                }} FILTER .id = <uuid>$__{target_link}\n            )'''\n        ))\n\n        if reflect_as_link:\n            shadow_clslayout = classlayout[refcls]\n            shadow_link_layout = shadow_clslayout[f'{refdict.attr}__internal']\n            shadow_shape, shadow_variables = _build_object_mutation_shape(\n                cmd,\n                classlayout=classlayout,\n                internal_schema_mode=internal_schema_mode,\n                lprop_fields=shadow_link_layout.properties,\n                lprops_only=True,\n                stdmode=stdmode,\n                var_prefix='shadow_',\n                schema=schema,\n                context=context,\n            )\n\n            shadow_shape = textwrap.indent(f'\\n{shadow_shape}', '    ' * 6)\n\n            assignments.append(textwrap.dedent(\n                f'''\\\n                {refdict.attr}__internal += (\n                    SELECT DETACHED schema::{mcls.__name__} {{{shadow_shape}\n                    }} FILTER .name__internal = <str>$__{target_link}_shadow\n                )'''\n            ))\n\n            parent_variables.update(shadow_variables)\n\n        update_shape = textwrap.indent(\n            '\\n' + ',\\n'.join(assignments), '    ' * 4)\n\n        parent_update_query = textwrap.dedent(f'''\n            UPDATE schema::{refcls.__name__}\n            FILTER .name__internal = <str>$__parent_classname\n            SET {{{update_shape}\n            }}\n        ''')\n\n        parent_variables.update(append_variables)\n        blocks.append((parent_update_query, parent_variables))\n\n\n@write_meta.register\ndef write_meta_delete_object(\n    cmd: sd.DeleteObject,  # type: ignore\n    *,\n    classlayout: dict[type[so.Object], sr_struct.SchemaTypeLayout],\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    blocks: list[tuple[str, dict[str, Any]]],\n    internal_schema_mode: bool,\n    stdmode: bool,\n) -> None:\n    _descend(\n        cmd,\n        classlayout=classlayout,\n        schema=schema,\n        context=context,\n        blocks=blocks,\n        prerequisites=True,\n        internal_schema_mode=internal_schema_mode,\n        stdmode=stdmode,\n    )\n\n    defer_filter = (\n        lambda cmd: isinstance(cmd, sd.DeleteObject) and cmd.if_unused\n    )\n    _descend(\n        cmd,\n        classlayout=classlayout,\n        schema=schema,\n        context=context,\n        blocks=blocks,\n        internal_schema_mode=internal_schema_mode,\n        stdmode=stdmode,\n        cmd_filter=lambda cmd: not defer_filter(cmd),\n    )\n\n    mcls = cmd.maybe_get_schema_metaclass()\n    if mcls is not None and not issubclass(mcls, so.GlobalObject):\n        if isinstance(cmd, s_ref.ReferencedObjectCommand):\n            refctx = cmd.get_referrer_context(context)\n        else:\n            refctx = None\n\n        if (\n            refctx is not None\n            and mcls.get_reflection_method() is so.ReflectionMethod.AS_LINK\n        ):\n            refop = refctx.op\n            refcls = refop.get_schema_metaclass()\n            refdict = refcls.get_refdict_for_class(mcls)\n\n            target_link = mcls.get_reflection_link()\n            assert target_link is not None\n\n            target_field = mcls.get_field(target_link)\n            target = cmd.get_orig_attribute_value(target_link)\n\n            parent_variables = {}\n\n            # N.B: In some cases, like repair, where the delta came\n            # directly from diffing, we might have an ObjectShell\n            # instead of an Object, and so no .id.\n            # In that case, just deal with it, and use name instead.\n            # (XXX: We can't always use name because the target object might\n            # be deleted in some cases?)\n            #\n            # An alternate approach would be to try to always force resolve\n            # the fields in these cases, but the straightforward approaches\n            # seemed like they'd hit more cases than we wanted.\n            if isinstance(target, so.ObjectShell):\n                parent_variables[f'__{target_link}'] = (\n                    json.dumps(str(target.get_name(schema)))\n                )\n\n                parent_update_query = f'''\n                    UPDATE schema::{refcls.__name__}\n                    FILTER .name__internal = <str>$__parent_classname\n                    SET {{\n                        {refdict.attr} -= (\n                            SELECT DETACHED\n                              (schema::{target_field.type.__name__})\n                            FILTER .name__internal = <str>$__{target_link}\n                        )\n                    }}\n                '''\n            else:\n                parent_variables[f'__{target_link}'] = (\n                    json.dumps(str(target.id))\n                )\n\n                parent_update_query = f'''\n                    UPDATE schema::{refcls.__name__}\n                    FILTER .name__internal = <str>$__parent_classname\n                    SET {{\n                        {refdict.attr} -= (\n                            SELECT DETACHED\n                              (schema::{target_field.type.__name__})\n                            FILTER .id = <uuid>$__{target_link}\n                        )\n                    }}\n                '''\n\n            ref_name = context.get_referrer_name(refctx)\n            parent_variables['__parent_classname'] = (\n                json.dumps(str(ref_name))\n            )\n\n            blocks.append((parent_update_query, parent_variables))\n\n        # We need to delete any links created via reflection_proxy\n        layout = classlayout[mcls]\n        proxy_links = [\n            link for link, layout_entry in layout.items()\n            if layout_entry.reflection_proxy\n        ]\n\n        to_delete = ['D'] + [f'D.{link}' for link in proxy_links]\n        operations = [f'(DELETE {x})' for x in to_delete]\n        query = f'''\n            WITH D := (SELECT schema::{mcls.__name__}\n                       FILTER .name__internal = <str>$__classname),\n            SELECT {{{\", \".join(operations)}}};\n        '''\n        variables = {'__classname': json.dumps(str(cmd.classname))}\n        blocks.append((query, variables))\n\n    _descend(\n        cmd,\n        classlayout=classlayout,\n        schema=schema,\n        context=context,\n        blocks=blocks,\n        internal_schema_mode=internal_schema_mode,\n        stdmode=stdmode,\n        cmd_filter=defer_filter,\n    )\n\n\n@write_meta.register\ndef write_meta_rename_object(\n    cmd: sd.RenameObject,  # type: ignore\n    *,\n    classlayout: dict[type[so.Object], sr_struct.SchemaTypeLayout],\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    blocks: list[tuple[str, dict[str, Any]]],\n    internal_schema_mode: bool,\n    stdmode: bool,\n) -> None:\n    # Delegate to the more general function, and then record the rename.\n    write_meta_alter_object(\n        cmd,\n        classlayout=classlayout,\n        schema=schema,\n        context=context,\n        blocks=blocks,\n        internal_schema_mode=internal_schema_mode,\n        stdmode=stdmode,\n    )\n\n    context.early_renames[cmd.classname] = cmd.new_name\n\n\n@write_meta.register\ndef write_meta_nop(\n    cmd: sd.Nop,\n    *,\n    classlayout: dict[type[so.Object], sr_struct.SchemaTypeLayout],\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    blocks: list[tuple[str, dict[str, Any]]],\n    internal_schema_mode: bool,\n    stdmode: bool,\n) -> None:\n    pass\n\n\n@write_meta.register\ndef write_meta_query(\n    cmd: sd.Query,\n    *,\n    classlayout: dict[type[so.Object], sr_struct.SchemaTypeLayout],\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    blocks: list[tuple[str, dict[str, Any]]],\n    internal_schema_mode: bool,\n    stdmode: bool,\n) -> None:\n    pass\n"
  },
  {
    "path": "edb/schema/rewrites.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, cast, TYPE_CHECKING\n\nfrom edb import errors\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.edgeql import qltypes\n\nfrom . import annos as s_anno\nfrom . import delta as sd\nfrom . import expr as s_expr\nfrom . import name as sn\nfrom . import inheriting as s_inheriting\nfrom . import objects as so\nfrom . import referencing\nfrom . import schema as s_schema\nfrom . import types as s_types\n\nif TYPE_CHECKING:\n    from . import pointers as s_pointers\n\n\nclass Rewrite(\n    referencing.NamedReferencedInheritingObject,\n    so.InheritingObject,  # Help reflection figure out the right db MRO\n    s_anno.AnnotationSubject,\n    qlkind=qltypes.SchemaObjectClass.REWRITE,\n    data_safe=True,\n):\n\n    kind = so.SchemaField(\n        qltypes.RewriteKind,\n        coerce=True,\n        compcoef=0.0,\n        special_ddl_syntax=True,\n    )\n\n    # 0.0 because we don't support ALTER yet\n    expr = so.SchemaField(\n        s_expr.Expression,\n        compcoef=0.0,\n        special_ddl_syntax=True,\n    )\n\n    subject = so.SchemaField(\n        so.InheritingObject, compcoef=None, inheritable=False\n    )\n\n    def should_propagate(self, schema: s_schema.Schema) -> bool:\n        # Rewrites should override rewrites on properties of an extended object\n        # type. But overriding *objects* would be hard, so we just disable\n        # inheritance for rewrites, and do lookups into parent object types\n        # when retrieving them.\n        return False\n\n    def get_ptr_target(self, schema: s_schema.Schema) -> s_types.Type:\n        pointer: s_pointers.Pointer = cast(\n            's_pointers.Pointer', self.get_subject(schema))\n        ptr_target = pointer.get_target(schema)\n        assert ptr_target\n        return ptr_target\n\n\nclass RewriteCommandContext(\n    sd.ObjectCommandContext[Rewrite],\n    s_anno.AnnotationSubjectCommandContext,\n):\n    pass\n\n\nclass RewriteSubjectCommandContext:\n    pass\n\n\nclass RewriteSubjectCommand(\n    s_inheriting.InheritingObjectCommand[so.InheritingObjectT],\n):\n    pass\n\n\nclass RewriteCommand(\n    referencing.NamedReferencedInheritingObjectCommand[Rewrite],\n    s_anno.AnnotationSubjectCommand[Rewrite],\n    context_class=RewriteCommandContext,\n    referrer_context_class=RewriteSubjectCommandContext,\n):\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n\n        for field in ('expr',):\n            if (expr := self.get_local_attribute_value(field)) is None:\n                continue\n\n            self.compile_expr_field(\n                schema,\n                context,\n                field=Rewrite.get_field(field),\n                value=expr,\n            )\n\n        return schema\n\n    def _get_kind(\n        self,\n        schema: s_schema.Schema,\n    ) -> qltypes.RewriteKind:\n        return self.get_attribute_value('kind') or self.scls.get_kind(schema)\n\n    def compile_expr_field(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: s_expr.Expression,\n        track_schema_ref_exprs: bool = False,\n    ) -> s_expr.CompiledExpression:\n        if field.name == 'expr':\n            from edb.common import ast\n            from edb.ir import ast as irast\n            from edb.ir import pathid\n            from . import pointers as s_pointers\n            from . import objtypes as s_objtypes\n            from . import links as s_links\n\n            parent_ctx = self.get_referrer_context_or_die(context)\n            pointer = parent_ctx.op.scls\n            assert isinstance(pointer, s_pointers.Pointer)\n\n            source = pointer.get_source(schema)\n            if isinstance(source, s_objtypes.ObjectType):\n                subject = source\n            elif isinstance(source, s_links.Link):\n                subject = source.get_target(schema)\n                assert subject\n\n                span = self.get_attribute_span('expr')\n                raise errors.SchemaDefinitionError(\n                    'rewrites on link properties are not supported',\n                    span=span,\n                )\n            else:\n                raise NotImplementedError('unsupported rewrite source')\n\n            # XXX: in_ddl_context_name is disabled for now because\n            # it causes the compiler to reject DML; we might actually\n            # want it for something, though, so we might need to\n            # improve that restriction.\n            # parent_vname = source.get_verbosename(schema)\n            # pol_name = self.get_verbosename(parent=parent_vname)\n            # in_ddl_context_name = pol_name\n\n            kind = self._get_kind(schema)\n\n            anchors: dict[str, s_types.Type | pathid.PathId] = {}\n\n            # __subject__\n            anchors[\"__subject__\"] = pathid.PathId.from_type(\n                schema,\n                subject,\n                typename=sn.QualName(module=\"__derived__\", name=\"__subject__\"),\n                env=None,\n            )\n            # __specified__\n            bool_type = schema.get(\"std::bool\", type=s_types.Type)\n            schema, specified_type = s_types.Tuple.create(\n                schema,\n                named=True,\n                element_types={\n                    pn.name: bool_type\n                    for pn in subject.get_pointers(schema).keys(schema)\n                },\n            )\n            anchors['__specified__'] = specified_type\n\n            # __old__\n            if qltypes.RewriteKind.Update == kind:\n                anchors['__old__'] = pathid.PathId.from_type(\n                    schema,\n                    subject,\n                    typename=sn.QualName(module='__derived__', name='__old__'),\n                    env=None,\n                )\n\n            singletons = frozenset(anchors.values())\n\n            # If the `__specified__` anchor is used, create references to the\n            # matching pointers.\n            #\n            # These references are necessary in order to compute the dependency\n            # and ordering of Rewrite commands when producing DDL.\n            #\n            # If creating Type T with two properties, A and B, such that\n            # A has a Rewrite containing `__specified__.B`.\n            #\n            # Without the references, the DDL may look like:\n            # - Create Type T\n            #   - Create Property A\n            #     - Create Rewrite using __specified__.B\n            #   - Create Property B\n            #\n            # This will cause an issue when compiling the Rewrite. At that\n            # point, the schema will not know about B and so the tuple will not\n            # have element `.B`.\n            #\n            # The reference will cause the reordering of commands and the DDL\n            # may instead look like:\n            # - Create Object O\n            #   - Create Property A\n            #   - Create Property B\n            #   - Alter Property A\n            #     - Create Rewrite using __specified__.B\n            #\n            # With Create Rewrite ordered after Property B, the tuple for\n            # `__specified__` will correctly have element `.B`.\n            def find_extra_refs(ir_expr: irast.Set) -> set[so.Object]:\n                def find_specified(node: irast.TupleIndirectionPointer) -> bool:\n                    return node.source.anchor == '__specified__'\n\n                ref_ptr_names: set[str] = set()\n                for tuple_node in ast.find_children(\n                    ir_expr,\n                    irast.TupleIndirectionPointer,\n                    test_func=find_specified,\n                ):\n                    ref_ptr_names.add(tuple_node.ptrref.name.name)\n\n                ref_ptrs: set[so.Object] = set(\n                    pointer\n                    for pointer in subject.get_pointers(schema).objects(schema)\n                    if pointer.get_shortname(schema).name in ref_ptr_names\n                )\n\n                return ref_ptrs\n\n            return type(value).compiled(\n                value,\n                schema=schema,\n                options=qlcompiler.CompilerOptions(\n                    modaliases=context.modaliases,\n                    schema_object_context=self.get_schema_metaclass(),\n                    path_prefix_anchor=\"__subject__\",\n                    anchors=anchors,\n                    singletons=singletons,\n                    apply_query_rewrites=not context.stdmode,\n                    track_schema_ref_exprs=track_schema_ref_exprs,\n                    # in_ddl_context_name=in_ddl_context_name,\n                    detached=True,\n                ),\n                find_extra_refs=find_extra_refs,\n                context=context,\n            )\n        else:\n            return super().compile_expr_field(\n                schema, context, field, value, track_schema_ref_exprs\n            )\n\n    def get_dummy_expr_field_value(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: Any,\n    ) -> Optional[s_expr.Expression]:\n        if field.name == 'expr':\n            return s_types.type_dummy_expr(\n                self.scls.get_ptr_target(schema), schema)\n        else:\n            raise NotImplementedError(f'unhandled field {field.name!r}')\n\n    def validate_object(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        expr: s_expr.Expression = self.scls.get_expr(schema)\n\n        if not expr.irast:\n            expr = self.compile_expr_field(\n                schema, context, Rewrite.get_field('expr'), expr\n            )\n            assert expr.irast\n\n        ir = expr.irast\n        compiled_schema = ir.schema\n        typ: s_types.Type = ir.stype\n\n        if (\n            typ.is_view(compiled_schema)\n            # Using an alias/global always creates a new subtype view,\n            # but we want to allow those here, so check whether there\n            # is a shape more directly.\n            and not (\n                len(shape := ir.view_shapes.get(typ, [])) == 1\n                and shape[0].is_id_pointer(compiled_schema)\n            )\n        ):\n            span = self.get_attribute_span('expr')\n            raise errors.SchemaDefinitionError(\n                f'rewrite expression may not include a shape',\n                span=span,\n            )\n\n        ptr_target = self.scls.get_ptr_target(compiled_schema)\n        if not typ.assignment_castable_to(ptr_target, compiled_schema):\n            span = self.get_attribute_span('expr')\n            raise errors.SchemaDefinitionError(\n                f'rewrite expression is of invalid type: '\n                f'{typ.get_displayname(compiled_schema)}, '\n                f'expected {ptr_target.get_displayname(compiled_schema)}',\n                span=span,\n            )\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        \"\"\"\n        Converts a single `qlast.RewriteCommand` into multiple\n        `schema.RewriteCommand`s, one for each kind.\n        \"\"\"\n\n        group = sd.CommandGroup()\n\n        assert isinstance(astnode, qlast.RewriteCommand)\n\n        for kind in astnode.kinds:\n            # use kind for the name\n            newnode = astnode.replace(\n                name=qlast.ObjectRef(module='__', name=str(kind)),\n                kinds=kind,\n            )\n\n            cmd = super()._cmd_tree_from_ast(schema, newnode, context)\n            assert isinstance(cmd, RewriteCommand)\n\n            cmd.set_attribute_value('kind', kind)\n            group.add(cmd)\n        return group\n\n\nclass CreateRewrite(\n    RewriteCommand,\n    referencing.CreateReferencedInheritingObject[Rewrite],\n):\n    referenced_astnode = astnode = qlast.CreateRewrite\n\n    def get_ast_attr_for_field(\n        self,\n        field: str,\n        astnode: type[qlast.DDLOperation],\n    ) -> Optional[str]:\n        if field in ('kind', 'expr') and issubclass(\n            astnode, qlast.CreateRewrite\n        ):\n            return field\n        else:\n            return super().get_ast_attr_for_field(field, astnode)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        group = super()._cmd_tree_from_ast(schema, astnode, context)\n        assert isinstance(group, sd.CommandGroup)\n        assert isinstance(astnode, qlast.CreateRewrite)\n\n        for cmd in group.ops:\n            assert isinstance(cmd, CreateRewrite)\n\n            cmd.set_attribute_value(\n                'expr',\n                s_expr.Expression.from_ast(\n                    astnode.expr,\n                    schema,\n                    context.modaliases,\n                    context.localnames,\n                ),\n                span=astnode.expr.span,\n            )\n        return group\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        if op.property == 'kind':\n            assert isinstance(node, qlast.CreateRewrite)\n            node.kinds = [self.get_attribute_value('kind')]\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n\nclass RebaseRewrite(\n    RewriteCommand,\n    referencing.RebaseReferencedInheritingObject[Rewrite],\n):\n    pass\n\n\nclass RenameRewrite(\n    RewriteCommand,\n    referencing.RenameReferencedInheritingObject[Rewrite],\n):\n    pass\n\n\nclass AlterRewrite(\n    RewriteCommand,\n    referencing.AlterReferencedInheritingObject[Rewrite],\n):\n    referenced_astnode = astnode = qlast.AlterRewrite\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._alter_begin(schema, context)\n\n        # TODO: We may wish to support this in the future but it will\n        # take some thought.\n        if self.get_attribute_value(\n            'owned'\n        ) and not self.get_orig_attribute_value('owned'):\n            raise errors.SchemaDefinitionError(\n                f'cannot alter the definition of inherited trigger '\n                f'{self.scls.get_displayname(schema)}',\n                span=self.span,\n            )\n\n        return schema\n\n\nclass DeleteRewrite(\n    RewriteCommand,\n    referencing.DeleteReferencedInheritingObject[Rewrite],\n):\n    referenced_astnode = astnode = qlast.DropRewrite\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        node = super()._get_ast(schema, context, parent_node=parent_node)\n        assert isinstance(node, qlast.DropRewrite)\n        skind = sn.shortname_from_fullname(self.classname).name\n        node.kinds = [qltypes.RewriteKind(skind)]\n        return node\n"
  },
  {
    "path": "edb/schema/roles.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Optional, overload, TYPE_CHECKING\n\nfrom edgedb import scram\n\nfrom edb import errors\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\nfrom edb.schema import defines as s_def\n\nfrom . import annos as s_anno\nfrom . import delta as sd\nfrom . import inheriting\nfrom . import name as sn\nfrom . import objects as so\nfrom . import utils\n\nif TYPE_CHECKING:\n    from edb.schema import schema as s_schema\n\n\nclass Role(\n    so.GlobalObject,\n    so.InheritingObject,\n    s_anno.AnnotationSubject,\n    qlkind=qltypes.SchemaObjectClass.ROLE,\n    data_safe=True,\n):\n\n    superuser = so.SchemaField(\n        bool,\n        default=False,\n        inheritable=False)\n\n    password = so.SchemaField(\n        str,\n        default=None,\n        allow_ddl_set=True,\n        inheritable=False)\n\n    password_hash = so.SchemaField(\n        str,\n        default=None,\n        allow_ddl_set=True,\n        ephemeral=True,\n        inheritable=False)\n\n    permissions = so.SchemaField(\n        so.MultiPropSet[str],\n        default=None,\n        coerce=True,\n        allow_ddl_set=True,\n        obj_names_as_string=True,\n        inheritable=False,\n    )\n\n    branches = so.SchemaField(\n        so.MultiPropSet[str],\n        # default=so.MultiPropSet[str]('*'),\n        # default=('*',),\n        coerce=True,\n        allow_ddl_set=True,\n        inheritable=False,\n    )\n\n    apply_access_policies_pg_default = so.SchemaField(\n        bool,\n        default=None,\n        allow_ddl_set=True,\n        inheritable=True,\n    )\n\n\nclass RoleCommandContext(\n        sd.ObjectCommandContext[Role],\n        s_anno.AnnotationSubjectCommandContext):\n    pass\n\n\nclass RoleCommand(\n    sd.GlobalObjectCommand[Role],\n    inheriting.InheritingObjectCommand[Role],\n    s_anno.AnnotationSubjectCommand[Role],\n    context_class=RoleCommandContext,\n):\n\n    @classmethod\n    def _process_role_body(\n        cls,\n        cmd: sd.Command,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> None:\n        password = cmd.get_attribute_value('password')\n        if password is not None:\n            if cmd.get_attribute_value('password_hash') is not None:\n                raise errors.EdgeQLSyntaxError(\n                    'cannot specify both `password` and `password_hash` in'\n                    ' the same statement',\n                    span=astnode.span,\n                )\n            salted_password = scram.build_verifier(password)\n            cmd.set_attribute_value('password', salted_password)\n\n        password_hash = cmd.get_attribute_value('password_hash')\n        if password_hash is not None:\n            try:\n                scram.parse_verifier(password_hash)\n            except ValueError as e:\n                raise errors.InvalidValueError(\n                    e.args[0],\n                    span=astnode.span)\n            cmd.set_attribute_value('password', password_hash)\n\n    @classmethod\n    def _classbases_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.ObjectDDL,\n        context: sd.CommandContext,\n    ) -> list[so.ObjectShell[Role]]:\n        result = []\n        for b in getattr(astnode, 'bases', None) or []:\n            result.append(utils.ast_objref_to_object_shell(\n                b.maintype,\n                metaclass=Role,\n                schema=schema,\n                modaliases=context.modaliases,\n            ))\n\n        return result\n\n    def _validate_name(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        name = self.get_attribute_value('name')\n        if len(str(name)) > s_def.MAX_NAME_LENGTH:\n            span = self.get_attribute_span('name')\n            raise errors.SchemaDefinitionError(\n                f'Role names longer than {s_def.MAX_NAME_LENGTH} '\n                f'characters are not supported',\n                span=span,\n            )\n\n    def _validate_permissions(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        if (\n            self.has_attribute_value('permissions')\n            and (permissions := self.get_attribute_value('permissions'))\n        ):\n            if 'sys::perm::superuser' in permissions:\n                span = self.get_attribute_span('permissions')\n                raise errors.SchemaDefinitionError(\n                    f'Permission \"sys::perm::superuser\" '\n                    f'cannot be explicitly granted.',\n                    span=span,\n                )\n\n\nclass CreateRole(RoleCommand, inheriting.CreateInheritingObject[Role]):\n    astnode = qlast.CreateRole\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        assert isinstance(astnode, qlast.CreateRole)\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        cmd.set_attribute_value('superuser', astnode.superuser)\n        cls._process_role_body(cmd, schema, astnode, context)\n\n        if not cmd.has_attribute_value('branches'):\n            cmd.set_attribute_value('branches', frozenset(['*']))\n\n        return cmd\n\n    def get_ast_attr_for_field(\n        self,\n        field: str,\n        astnode: type[qlast.DDLOperation],\n    ) -> Optional[str]:\n        if (\n            field == 'superuser'\n            and issubclass(astnode, qlast.CreateRole)\n        ):\n            return 'superuser'\n        else:\n            return super().get_ast_attr_for_field(field, astnode)\n\n    def validate_create(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        super().validate_create(schema, context)\n        self._validate_name(schema, context)\n        self._validate_permissions(schema, context)\n\n\nclass RebaseRole(RoleCommand, inheriting.RebaseInheritingObject[Role]):\n    pass\n\n\nclass RenameRole(RoleCommand, sd.RenameObject[Role]):\n    pass\n\n\nclass AlterRole(RoleCommand, inheriting.AlterInheritingObject[Role]):\n    astnode = qlast.AlterRole\n\n    @overload\n    def get_object(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        name: Optional[sn.Name] = None,\n        default: Role | so.NoDefaultT = so.NoDefault,\n        span: Optional[qlast.Span] = None,\n    ) -> Role:\n        ...\n\n    @overload\n    def get_object(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        name: Optional[sn.Name] = None,\n        default: None = None,\n        span: Optional[qlast.Span] = None,\n    ) -> Optional[Role]:\n        ...\n\n    def get_object(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        name: Optional[sn.Name] = None,\n        default: Role | so.NoDefaultT | None = so.NoDefault,\n        span: Optional[qlast.Span] = None,\n    ) -> Optional[Role]:\n        # On an ALTER ROLE edgedb, if 'edgedb' doesn't exist, fall\n        # back to 'admin'. This mirrors what we do for login and\n        # avoids breaking setup scripts.\n        if name is None and str(self.classname) == 'edgedb':\n            try:\n                return super().get_object(\n                    schema,\n                    context,\n                    span=span,\n                )\n            except errors.InvalidReferenceError:\n                name = sn.UnqualName('admin')\n\n        return super().get_object(\n            schema,\n            context,\n            name=name,\n            default=default,\n            span=span,\n        )\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n        cls._process_role_body(cmd, schema, astnode, context)\n        return cmd\n\n    def validate_alter(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        super().validate_alter(schema, context)\n        self._validate_name(schema, context)\n        self._validate_permissions(schema, context)\n\n\nclass DeleteRole(RoleCommand, inheriting.DeleteInheritingObject[Role]):\n    astnode = qlast.DropRole\n\n    def _validate_legal_command(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        super()._validate_legal_command(schema, context)\n        if self.classname.name == s_def.EDGEDB_SUPERUSER:\n            raise errors.ExecutionError(\n                f\"role {self.classname.name!r} cannot be dropped\"\n            )\n"
  },
  {
    "path": "edb/schema/scalars.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Optional, Iterable, Sequence, cast\n\nfrom edb import errors\n\nfrom edb.common import checked\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom edb.common.typeutils import downcast\n\nfrom . import annos as s_anno\nfrom . import casts as s_casts\nfrom . import constraints\nfrom . import delta as sd\nfrom . import expr as s_expr\nfrom . import inheriting\nfrom . import name as s_name\nfrom . import objects as so\nfrom . import schema as s_schema\nfrom . import types as s_types\nfrom . import utils as s_utils\n\n\nclass ScalarType(\n    s_types.InheritingType,\n    constraints.ConsistencySubject,\n    qlkind=qltypes.SchemaObjectClass.SCALAR_TYPE,\n    data_safe=True,\n):\n\n    default = so.SchemaField(\n        s_expr.Expression, default=None,\n        coerce=True, compcoef=0.909,\n    )\n\n    enum_values = so.SchemaField(\n        checked.FrozenCheckedList[str], default=None,\n        coerce=True, compcoef=0.8,\n    )\n\n    sql_type = so.SchemaField(\n        str, default=None, inheritable=False, compcoef=0.0)\n\n    # A type scheme for supporting type mods in scalar types.\n    # If present, describes what the sql_type of children scalars\n    # should be, such as 'varchar({__arg_0__})'.\n    sql_type_scheme = so.SchemaField(\n        str, default=None, inheritable=False, compcoef=0.0)\n\n    # The number of parameters that the type takes. Currently all parameters\n    # must be integer literals.\n    # This is an internal API and might change.\n    num_params = so.SchemaField(\n        int, default=None,\n        inheritable=False,\n        compcoef=0.0,\n    )\n\n    # Arguments to fill in a parent type's parameterized type scheme.\n    arg_values = so.SchemaField(\n        checked.FrozenCheckedList[str],\n        default=None,\n        inheritable=False,\n        coerce=True,\n        compcoef=0.0,\n    )\n\n    custom_sql_serialization = so.SchemaField(\n        str, default=None, inheritable=False, compcoef=0.0)\n\n    def is_scalar(self) -> bool:\n        return True\n\n    def is_concrete_enum(self, schema: s_schema.Schema) -> bool:\n        return any(\n            str(base.get_name(schema)) == 'std::anyenum'\n            for base in self.get_bases(schema).objects(schema)\n        )\n\n    def is_base_type(\n        self,\n        schema: s_schema.Schema,\n    ) -> bool:\n        \"\"\"Returns true of the type has only abstract bases\"\"\"\n        bases: Sequence[s_types.Type] = self.get_bases(schema).objects(schema)\n        return all(b.get_abstract(schema) for b in bases)\n\n    def is_enum(self, schema: s_schema.Schema) -> bool:\n        return bool(self.get_enum_values(schema))\n\n    def is_sequence(self, schema: s_schema.Schema) -> bool:\n        seq = schema.get('std::sequence', type=ScalarType)\n        return self.issubclass(schema, seq)\n\n    def is_polymorphic(self, schema: s_schema.Schema) -> bool:\n        return self.get_abstract(schema)\n\n    def is_json(self, schema: s_schema.Schema) -> bool:\n        return self.issubclass(\n            schema,\n            schema.get(s_name.QualName('std', 'json'), type=ScalarType),\n        )\n\n    def can_accept_constraints(self, schema: s_schema.Schema) -> bool:\n        return not self.is_enum(schema)\n\n    def _resolve_polymorphic(\n        self,\n        schema: s_schema.Schema,\n        concrete_type: s_types.Type,\n    ) -> Optional[s_types.Type]:\n        if (self.is_polymorphic(schema) and\n                concrete_type.is_scalar() and\n                not concrete_type.is_polymorphic(schema)):\n            return concrete_type\n        return None\n\n    def _to_nonpolymorphic(\n        self,\n        schema: s_schema.Schema,\n        concrete_type: s_types.Type,\n    ) -> tuple[s_schema.Schema, s_types.Type]:\n        if (not concrete_type.is_polymorphic(schema) and\n                concrete_type.issubclass(schema, self)):\n            return schema, concrete_type\n        raise TypeError(\n            f'cannot interpret {concrete_type.get_name(schema)} '\n            f'as {self.get_name(schema)}')\n\n    def _test_polymorphic(\n        self,\n        schema: s_schema.Schema,\n        other: s_types.Type,\n    ) -> bool:\n        if other.is_any(schema):\n            return True\n        else:\n            return self.issubclass(schema, other)\n\n    def assignment_castable_to(\n        self,\n        other: s_types.Type,\n        schema: s_schema.Schema,\n    ) -> bool:\n        if not isinstance(other, ScalarType):\n            return False\n        if self.is_polymorphic(schema) or other.is_polymorphic(schema):\n            return False\n        left = self.get_base_for_cast(schema)\n        right = other.get_base_for_cast(schema)\n        assert isinstance(left, s_types.Type)\n        assert isinstance(right, s_types.Type)\n        return s_casts.is_assignment_castable(schema, left, right)\n\n    def implicitly_castable_to(\n        self,\n        other: s_types.Type,\n        schema: s_schema.Schema,\n    ) -> bool:\n        if not isinstance(other, ScalarType):\n            return False\n        if self.is_polymorphic(schema) or other.is_polymorphic(schema):\n            return False\n        left = self.get_topmost_concrete_base(schema)\n        right = other.get_topmost_concrete_base(schema)\n        assert isinstance(left, s_types.Type)\n        assert isinstance(right, s_types.Type)\n        return s_casts.is_implicitly_castable(schema, left, right)\n\n    def castable_to(\n        self,\n        other: s_types.Type,\n        schema: s_schema.Schema,\n    ) -> bool:\n        \"\"\"Determine if any cast exists between self and *other*.\"\"\"\n        if not isinstance(other, ScalarType):\n            return False\n        if self.is_polymorphic(schema) or other.is_polymorphic(schema):\n            return False\n        left = self.get_topmost_concrete_base(schema)\n        right = other.get_topmost_concrete_base(schema)\n        assert isinstance(left, s_types.Type)\n        assert isinstance(right, s_types.Type)\n        return s_casts.is_castable(schema, left, right)\n\n    def get_implicit_cast_distance(\n        self,\n        other: s_types.Type,\n        schema: s_schema.Schema,\n    ) -> int:\n        if not isinstance(other, ScalarType):\n            return -1\n        if self.is_polymorphic(schema) or other.is_polymorphic(schema):\n            return -1\n        left = self.get_topmost_concrete_base(schema)\n        right = other.get_topmost_concrete_base(schema)\n        return s_casts.get_implicit_cast_distance(schema, left, right)\n\n    def find_common_implicitly_castable_type(\n        self,\n        other: s_types.Type,\n        schema: s_schema.Schema,\n    ) -> tuple[s_schema.Schema, Optional[ScalarType]]:\n\n        if not isinstance(other, ScalarType):\n            return schema, None\n\n        if self.is_polymorphic(schema) and other.is_polymorphic(schema):\n            return schema, self\n\n        left = self.get_topmost_concrete_base(schema)\n        right = other.get_topmost_concrete_base(schema)\n\n        if left == right:\n            return schema, left\n        else:\n            return (\n                schema,\n                cast(\n                    Optional[ScalarType],\n                    s_casts.find_common_castable_type(schema, left, right),\n                )\n            )\n\n    def get_base_for_cast(self, schema: s_schema.Schema) -> so.Object:\n        if self.is_enum(schema):\n            # all enums have to use std::anyenum as base type for casts\n            return schema.get('std::anyenum')\n        else:\n            return super().get_base_for_cast(schema)\n\n    def get_verbosename(\n        self, schema: s_schema.Schema, *, with_parent: bool = False\n    ) -> str:\n        if self.is_enum(schema):\n            clsname = 'enumerated type'\n        else:\n            clsname = self.get_schema_class_displayname()\n        dname = self.get_displayname(schema)\n        return f\"{clsname} '{dname}'\"\n\n    def resolve_sql_type_scheme(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[Optional[str], Optional[str]]:\n        if sql := self.get_sql_type(schema):\n            return sql, None\n        if self.get_arg_values(schema) is None:\n            return None, None\n        bases = self.get_bases(schema).objects(schema)\n        if len(bases) != 1:\n            return None, None\n        if scheme := bases[0].get_sql_type_scheme(schema):\n            base_sql_type = bases[0].get_sql_type(schema)\n            assert base_sql_type is not None\n            return base_sql_type, scheme\n        return None, None\n\n    def resolve_sql_type(\n        self,\n        schema: s_schema.Schema,\n    ) -> Optional[str]:\n        type, scheme = self.resolve_sql_type_scheme(schema)\n        if scheme:\n            return constraints.interpolate_error_text(\n                scheme,\n                {\n                    f'__arg_{i}__': v\n                    for i, v in enumerate(self.get_arg_values(schema) or ())\n                },\n            )\n        else:\n            return type\n\n    def as_alter_delta(\n        self,\n        other: ScalarType,\n        *,\n        self_schema: s_schema.Schema,\n        other_schema: s_schema.Schema,\n        confidence: float,\n        context: so.ComparisonContext,\n    ) -> sd.ObjectCommand[ScalarType]:\n        alter = super().as_alter_delta(\n            other,\n            self_schema=self_schema,\n            other_schema=other_schema,\n            confidence=confidence,\n            context=context,\n        )\n\n        # If this is an enum and enum_values changed, we need to\n        # generate a rebase.\n        old_enum_values = self.get_enum_values(self_schema)\n        enum_values = alter.get_local_attribute_value('enum_values')\n        if old_enum_values and enum_values:\n            assert isinstance(alter.classname, s_name.QualName)\n            rebase = RebaseScalarType(\n                classname=alter.classname,\n                removed_bases=(),\n                added_bases=(\n                    ([AnonymousEnumTypeShell(elements=enum_values)], ''),\n                ),\n            )\n            alter.add(rebase)\n\n        # Changing enum_values is the respoinsiblity of the rebase command.\n        # Either it's in the one we synthesized above, or, the rebase is doomed\n        # to throw. When we run the ddl directly, the ALTER will not have a\n        # enum_values set, so discard here for symmetry.\n        alter.discard_attribute('enum_values')\n        return alter\n\n\nclass AnonymousEnumTypeShell(s_types.TypeShell[ScalarType]):\n\n    elements: Sequence[str]\n\n    def __init__(\n        self,\n        *,\n        name: Optional[s_name.Name] = None,\n        elements: Iterable[str],\n    ) -> None:\n        name = name or s_name.QualName(module='std', name='anyenum')\n        super().__init__(name=name, schemaclass=ScalarType)\n        self.elements = list(elements)\n\n    def resolve(self, schema: s_schema.Schema) -> ScalarType:\n        raise errors.InvalidPropertyDefinitionError(\n            'this type cannot be anonymous',\n            details=(\n                'you may want define this enum first:\\n\\n'\n                '  scalar type MyEnum extending enum<...>;'\n            ),\n        )\n\n\nclass ScalarTypeCommandContext(sd.ObjectCommandContext[ScalarType],\n                               s_anno.AnnotationSubjectCommandContext,\n                               constraints.ConsistencySubjectCommandContext):\n    pass\n\n\nclass ScalarTypeCommand(\n    s_types.InheritingTypeCommand[ScalarType],\n    constraints.ConsistencySubjectCommand[ScalarType],\n    s_anno.AnnotationSubjectCommand[ScalarType],\n    context_class=ScalarTypeCommandContext,\n):\n    def validate_object(\n        self, schema: s_schema.Schema, context: sd.CommandContext\n    ) -> None:\n        if (\n            self.scls.resolve_sql_type_scheme(schema)[0]\n        ):\n            if len(self.scls.get_constraints(schema)):\n                raise errors.SchemaError(\n                    f'parameterized scalar types may not have constraints',\n                    span=self.span,\n                )\n\n        if args := self.scls.get_arg_values(schema):\n            base = self.scls.get_bases(schema).objects(schema)[0]\n            num_params = base.get_num_params(schema)\n            if not num_params:\n                raise errors.SchemaDefinitionError(\n                    f'base type {base.get_name(schema)} does not '\n                    f'accept parameters',\n                    span=self.span,\n                )\n            if num_params != len(args):\n                raise errors.SchemaDefinitionError(\n                    f'incorrect number of arguments provided to base type '\n                    f'{base.get_name(schema)}: expected {num_params} '\n                    f'but got {len(args)}',\n                    span=self.span,\n                )\n\n    def validate_scalar_ancestors(\n        self,\n        ancestors: Sequence[so.SubclassableObject],\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        real_concrete_ancestors = {\n            ancestor for ancestor in ancestors\n            if not ancestor.get_abstract(schema)\n        }\n        # Filter out anything that has a subclass relation with\n        # every other concrete ancestor. This lets us allow chains\n        # of concrete scalar types while prohibiting diamonds (for\n        # example if X <: A, B <: int64 where A, B are concrete).\n        # (If we wanted to allow diamonds, we could instead filter out\n        # anything that has concrete bases.)\n        concrete_ancestors = {\n            c1 for c1 in real_concrete_ancestors\n            if not all(c1 == c2 or c1.issubclass(schema, c2)\n                       or c2.issubclass(schema, c1)\n                       for c2 in real_concrete_ancestors)\n        }\n\n        if len(concrete_ancestors) > 1:\n            raise errors.SchemaError(\n                f'scalar type may not have more than '\n                f'one concrete base type',\n                span=self.span,\n            )\n        abstract = self.get_attribute_value('abstract')\n        enum = self.get_attribute_value('enum_values')\n        if (\n            len(real_concrete_ancestors) < 1\n            and not context.stdmode\n            and not abstract\n            and not enum\n            and not self.get_attribute_value('sql_type')\n        ):\n            if not ancestors:\n                hint = (\n                    f'\\nFor example: scalar type {self.classname.name} '\n                    f'extending str'\n                )\n            else:\n                hint = 'Bases were specified but no concrete bases were found'\n\n            raise errors.SchemaError(\n                f'scalar type must have a concrete base type',\n                span=self.span,\n                hint=hint,\n            )\n\n    def validate_scalar_bases(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        bases = self.get_resolved_attribute_value(\n            'bases', schema=schema, context=context)\n\n        if bases is not None:\n            ancestors = []\n            for base in bases.objects(schema):\n                ancestors.append(base)\n                ancestors.extend(base.get_ancestors(schema).objects(schema))\n\n            self.validate_scalar_ancestors(ancestors, schema, context)\n\n\nclass CreateScalarType(\n    ScalarTypeCommand,\n    s_types.CreateInheritingType[ScalarType],\n):\n    astnode = qlast.CreateScalarType\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(\n            schema, astnode.replace(bases=[]), context)\n\n        if isinstance(cmd, sd.CommandGroup):\n            for subcmd in cmd.get_subcommands():\n                if isinstance(subcmd, cls):\n                    create_cmd: sd.Command = subcmd\n                    break\n            else:\n                raise errors.InternalServerError(\n                    'scalar alias definition did not return CreateScalarType'\n                )\n        else:\n            create_cmd = cmd\n\n        if isinstance(astnode, qlast.CreateScalarType):\n            bases = [\n                s_utils.ast_to_type_shell(\n                    b,\n                    metaclass=ScalarType,\n                    modaliases=context.modaliases,\n                    schema=schema,\n                    allow_generalized_bases=True,\n                )\n                for b in (astnode.bases or [])\n            ]\n            is_enum = any(\n                isinstance(br, AnonymousEnumTypeShell) for br in bases)\n            for ab, b in zip(astnode.bases, bases):\n                if isinstance(b, s_types.CollectionTypeShell):\n                    raise errors.SchemaError(\n                        f'scalar type may not have a collection base type',\n                        span=ab.span,\n                    )\n\n            # We don't support FINAL, but old dumps and migrations specify\n            # it on enum CREATE SCALAR TYPEs, so we need to permit it in those\n            # cases.\n            if not is_enum and astnode.final:\n                raise errors.UnsupportedFeatureError(\n                    f'FINAL is not supported',\n                    span=astnode.span,\n                )\n\n            if is_enum:\n                # This is an enumerated type.\n                if len(bases) > 1:\n                    assert isinstance(astnode, qlast.BasedOn)\n                    raise errors.SchemaError(\n                        f'invalid scalar type definition, enumeration must be'\n                        f' the only supertype specified',\n                        span=astnode.bases[0].span,\n                    )\n                if create_cmd.has_attribute_value('default'):\n                    raise errors.UnsupportedFeatureError(\n                        f'enumerated types do not support defaults',\n                        span=(\n                            create_cmd.get_attribute_span('default')\n                        ),\n                    )\n\n                shell = bases[0]\n                assert isinstance(shell, AnonymousEnumTypeShell)\n                if len(set(shell.elements)) != len(shell.elements):\n                    raise errors.SchemaDefinitionError(\n                        f'enums cannot contain duplicate values',\n                        span=astnode.bases[0].span,\n                    )\n                create_cmd.set_attribute_value('enum_values', shell.elements)\n                create_cmd.set_attribute_value(\n                    'bases',\n                    so.ObjectCollectionShell(\n                        [s_utils.ast_objref_to_object_shell(\n                            s_utils.name_to_ast_ref(\n                                s_name.QualName('std', 'anyenum'),\n                            ),\n                            schema=schema,\n                            metaclass=ScalarType,\n                            modaliases={},\n                        )],\n                        collection_type=so.ObjectList,\n                    )\n                )\n            else:\n                if any(b.extra_args for b in bases):\n                    if len(bases) > 1:\n                        raise errors.SchemaDefinitionError(\n                            'scalars with parameterized bases may '\n                            'only have one',\n                            span=astnode.bases[0].span,\n                        )\n                    base = bases[0]\n                    args = []\n                    for x in (base.extra_args or ()):\n                        if (\n                            not isinstance(x, qlast.TypeExprLiteral)\n                            or not isinstance(x.val, qlast.Constant)\n                            or x.val.kind != qlast.ConstantKind.INTEGER\n                        ):\n                            raise errors.SchemaDefinitionError(\n                                'invalid scalar type argument',\n                                span=x.span,\n                            )\n                        args.append(x.val.value)\n                    cmd.set_attribute_value('arg_values', args)\n\n                cmd.set_attribute_value(\n                    'bases',\n                    so.ObjectCollectionShell(\n                        bases, collection_type=so.ObjectList\n                    ),\n                )\n\n        return cmd\n\n    def _create_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._create_begin(schema, context)\n        if (\n            not context.canonical\n            and not self.scls.get_abstract(schema)\n            and not self.scls.get_transient(schema)\n        ):\n            # Create an array type for this scalar eagerly.\n            # We mostly do this so that we know the `backend_id`\n            # of the array type when running translation of SQL\n            # involving arrays of scalars.\n            schema2, arr_t = s_types.Array.from_subtypes(schema, [self.scls])\n            self.add_caused(arr_t.as_shell(schema2).as_create_delta(schema2))\n\n        return schema\n\n    def validate_create(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        super().validate_create(schema, context)\n        self.validate_scalar_bases(schema, context)\n\n    def _get_ast_node(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> type[qlast.DDLOperation]:\n        if self.get_attribute_value('expr'):\n            return qlast.CreateAlias\n        else:\n            return super()._get_ast_node(schema, context)\n\n    def _apply_field_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        node: qlast.DDLOperation,\n        op: sd.AlterObjectProperty,\n    ) -> None:\n        if op.property == 'default':\n            if op.new_value:\n                assert isinstance(op.new_value, list)\n                op.new_value = op.new_value[0]\n                super()._apply_field_ast(schema, context, node, op)\n\n        elif op.property == 'bases':\n            enum_values = self.get_local_attribute_value('enum_values')\n            if enum_values:\n                assert isinstance(node, qlast.BasedOn)\n                node.bases = [\n                    qlast.TypeName(\n                        maintype=qlast.ObjectRef(name='enum'),\n                        subtypes=[\n                            qlast.TypeName(maintype=qlast.ObjectRef(name=v))\n                            for v in enum_values\n                        ]\n                    )\n                ]\n            else:\n                super()._apply_field_ast(schema, context, node, op)\n                if arg_values := self.get_local_attribute_value('arg_values'):\n                    frags = [\n                        s_expr.Expression(text=x).parse() for x in arg_values]\n                    assert isinstance(node, qlast.BasedOn)\n                    node.bases[0].subtypes = [\n                        qlast.TypeExprLiteral(\n                            val=downcast(qlast.Constant, frag)\n                        )\n                        for frag in frags\n                    ]\n        else:\n            super()._apply_field_ast(schema, context, node, op)\n\n\nclass RenameScalarType(\n    ScalarTypeCommand,\n    s_types.RenameInheritingType[ScalarType],\n):\n    pass\n\n\nclass RebaseScalarType(\n    ScalarTypeCommand,\n    inheriting.RebaseInheritingObject[ScalarType],\n):\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        scls = self.get_object(schema, context)\n        self.scls = scls\n        assert isinstance(scls, ScalarType)\n\n        if self.scls.is_concrete_enum(schema):\n            if self.removed_bases and not self.added_bases:\n                raise errors.SchemaError(\n                    f'cannot DROP EXTENDING enum')\n\n            if self.added_bases:\n                first_bases = self.added_bases[0]\n                new_bases, pos = first_bases\n\n                if len(self.added_bases) > 1 or len(new_bases) > 1:\n                    dn = self.scls.get_displayname(schema)\n                    raise errors.SchemaError(\n                        f'enum {dn} may not have multiple supertypes')\n\n                new_base = new_bases[0]\n                if isinstance(new_base, AnonymousEnumTypeShell):\n                    new_name = _prettyprint_enum(new_base.elements)\n                else:\n                    if isinstance(new_base, so.ObjectShell):\n                        new_base = new_base.resolve(schema)\n                    assert isinstance(new_base, s_types.Type)\n                    new_name = new_base.get_verbosename(schema)\n\n                if self.removed_bases and not scls.is_view(schema):\n                    # enum to enum rebases come without removed_bases\n                    assert not new_base.is_enum(schema)\n                    raise errors.SchemaError(\n                        f'cannot change the base of enum type '\n                        f'{scls.get_displayname(schema)} to {new_name}')\n\n                if pos:\n                    raise errors.SchemaError(\n                        f'cannot add supertype {new_name} '\n                        f'to enum type {scls.get_displayname(schema)}')\n\n            assert isinstance(new_base, AnonymousEnumTypeShell)\n            schema = self._validate_enum_change(\n                scls, new_base.elements, schema)\n\n            schema = super().apply(schema, context)\n\n            self.validate_scalar_bases(schema, context)\n\n        else:\n            old_concrete = self.scls.maybe_get_topmost_concrete_base(schema)\n\n            for b in [b for bs, _ in self.added_bases for b in bs]:\n                if isinstance(b, s_types.CollectionTypeShell):\n                    raise errors.SchemaError(\n                        f'scalar type may not have a collection base type',\n                        span=self.span,\n                    )\n\n            schema = super().apply(schema, context)\n\n            self.validate_scalar_bases(schema, context)\n\n            new_concrete = self.scls.maybe_get_topmost_concrete_base(schema)\n            if old_concrete != new_concrete and not scls.is_view(schema):\n                old_name = (old_concrete.get_displayname(schema) if old_concrete\n                            else 'None')\n\n                if self.scls.is_concrete_enum(schema):\n                    values = self.scls.get_enum_values(schema)\n                    assert values is not None\n                    new_name = _prettyprint_enum(values)\n                elif new_concrete:\n                    new_name = new_concrete.get_displayname(schema)\n                else:\n                    new_name = 'None'\n\n                raise errors.SchemaError(\n                    f'cannot change concrete base of scalar type '\n                    f'{scls.get_displayname(schema)} from '\n                    f'{old_name} to {new_name}')\n\n        return schema\n\n    def validate_scalar_bases(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        super().validate_scalar_bases(schema, context)\n\n        bases = self.get_resolved_attribute_value(\n            'bases', schema=schema, context=context)\n        if bases:\n            obj = self.scls\n            # For each descendant, compute its new ancestors and check\n            # that they are valid for a scalar type.\n            new_schema = obj.set_field_value(schema, 'bases', bases)\n            for desc in obj.descendants(schema):\n                ancestors = so.compute_ancestors(new_schema, desc)\n                self.validate_scalar_ancestors(ancestors, schema, context)\n\n    def _validate_enum_change(\n        self,\n        stype: s_types.Type,\n        new_labels: Sequence[str],\n        schema: s_schema.Schema,\n    ) -> s_schema.Schema:\n        new_set = set(new_labels)\n        if len(new_set) != len(new_labels):\n            raise errors.SchemaError(\n                f'enums cannot contain duplicate values')\n\n        self.set_attribute_value('enum_values', new_labels)\n        schema = stype.set_field_value(schema, 'enum_values', new_labels)\n        return schema\n\n\ndef _prettyprint_enum(elements: Iterable[str]) -> str:\n    return f\"enum<{', '.join(elements)}>\"\n\n\nclass AlterScalarType(\n    ScalarTypeCommand,\n    s_types.AlterType[ScalarType],\n    inheriting.AlterInheritingObject[ScalarType],\n):\n    astnode = qlast.AlterScalarType\n\n\nclass DeleteScalarType(\n    ScalarTypeCommand,\n    s_types.DeleteInheritingType[ScalarType],\n):\n    astnode = qlast.DropScalarType\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        if self.get_orig_attribute_value('expr_type'):\n            # This is an alias type, appropriate DDL would be generated\n            # from the corresponding DeleteAlias node.\n            return None\n        else:\n            return super()._get_ast(schema, context, parent_node=parent_node)\n\n    def _delete_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        if not context.canonical:\n            schema2, arr_typ = s_types.Array.from_subtypes(schema, [self.scls])\n            arr_op = arr_typ.init_delta_command(\n                schema2,\n                sd.DeleteObject,\n                if_exists=True,\n            )\n            self.add_prerequisite(arr_op)\n\n        return super()._delete_begin(schema, context)\n"
  },
  {
    "path": "edb/schema/schema.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import (\n    Any,\n    Callable,\n    Iterable,\n    Iterator,\n    Mapping,\n    NoReturn,\n    Optional,\n    overload,\n    Self,\n    TYPE_CHECKING,\n)\n\nimport abc\nimport collections\nimport itertools\n\nimport immutables as immu\n\nfrom edb import errors\nfrom edb.common import adapter\nfrom edb.common import english\nfrom edb.common import lru\n\nfrom . import casts as s_casts\nfrom . import functions as s_func\nfrom . import migrations as s_migrations\nfrom . import modules as s_mod\nfrom . import name as sn\nfrom . import objects as so\nfrom . import operators as s_oper\nfrom . import pseudo as s_pseudo\nfrom . import types as s_types\n\nif TYPE_CHECKING:\n    import uuid\n    from edb.common import parsing\n\n    Refs_T = immu.Map[\n        uuid.UUID,\n        immu.Map[\n            tuple[type[so.Object], str],\n            immu.Map[uuid.UUID, None],\n        ],\n    ]\n\nEXT_MODULE = sn.UnqualName('ext')\n\nSTD_MODULES = (\n    sn.UnqualName('std'),\n    sn.UnqualName('schema'),\n    sn.UnqualName('std::math'),\n    sn.UnqualName('sys'),\n    sn.UnqualName('sys::perm'),\n    sn.UnqualName('cfg'),\n    sn.UnqualName('cfg::perm'),\n    sn.UnqualName('std::cal'),\n    sn.UnqualName('std::net'),\n    sn.UnqualName('std::net::http'),\n    sn.UnqualName('std::net::perm'),\n    sn.UnqualName('std::pg'),\n    sn.UnqualName('std::_test'),\n    sn.UnqualName('std::fts'),\n    sn.UnqualName('std::lang'),\n    sn.UnqualName('std::lang::go'),\n    sn.UnqualName('std::lang::js'),\n    sn.UnqualName('std::lang::py'),\n    sn.UnqualName('std::lang::rs'),\n    EXT_MODULE,\n    sn.UnqualName('std::enc'),\n)\n\nSPECIAL_MODULES = (\n    sn.UnqualName('__derived__'),\n    sn.UnqualName('__ext_casts__'),\n    sn.UnqualName('__ext_index_matches__'),\n)\n\n# Specifies the order of processing of files and directories in lib/\nSTD_SOURCES = (\n    sn.UnqualName('std'),\n    sn.UnqualName('schema'),\n    sn.UnqualName('math'),\n    sn.UnqualName('sys'),\n    sn.UnqualName('cfg'),\n    sn.UnqualName('cal'),\n    sn.UnqualName('ext'),\n    sn.UnqualName('enc'),\n    sn.UnqualName('pg'),\n    sn.UnqualName('fts'),\n    sn.UnqualName('net'),\n)\nTESTMODE_SOURCES = (\n    sn.UnqualName('_testmode'),\n)\n\n\n# Deep optimization: avoid lookups into so.Object\n_raw_schema_restore = so.Object.raw_schema_restore\n\n\nclass Schema(abc.ABC):\n    '''\n    Data store for objects and their data.\n\n    Objects have:\n    - a class (also called mcls for meta class or scls for schema class),\n    - an id (of type UUID),\n    - data (a tuple of python values),\n    - name (of type sn.Name).\n\n    Objects can be retrieved by:\n    - id,\n    - name (fully qualified),\n    - shortname (only for function and operator class),\n    - references.\n    '''\n\n    @abc.abstractmethod\n    def _get_by_name(\n        self,\n        name: sn.Name,\n    ) -> Optional[so.Object]:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def _get_by_shortname[T: s_func.Function | s_oper.Operator](\n        self,\n        mcls: type[T],\n        shortname: sn.Name,\n    ) -> Optional[tuple[T, ...]]:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def _get_by_globalname[T: so.Object](\n        self, mcls: type[T], name: sn.Name,\n    ) -> Optional[T]:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def add(\n        self,\n        id: uuid.UUID,\n        sclass: type[so.Object],\n        data: tuple[Any, ...],\n    ) -> Self:\n        raise NotImplementedError\n\n    def discard(self: Self, obj: so.Object) -> Self:\n        if self.has_object(obj.id):\n            return self.delete(obj)\n        else:\n            return self\n\n    @abc.abstractmethod\n    def delete(self: Self, obj: so.Object) -> Self:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def delist(self: Self, name: sn.Name) -> Self:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def update_obj(\n        self: Self,\n        obj: so.Object,\n        updates: Mapping[str, Any],\n    ) -> Self:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def get_data_raw(\n        self,\n        obj: so.Object,\n    ) -> Optional[tuple[Any, ...]]:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def get_field_raw(\n        self,\n        obj: so.Object,\n        field_index: int,\n    ) -> Optional[Any]:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def set_field(\n        self: Self,\n        obj: so.Object,\n        field: str,\n        value: Any,\n    ) -> Self:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def unset_field(\n        self: Self,\n        obj: so.Object,\n        field: str,\n    ) -> Self:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def has_object(self, object_id: uuid.UUID) -> bool:\n        raise NotImplementedError\n\n    def has_module(self, name: str) -> bool:\n        return self.get_global(s_mod.Module, name, None) is not None\n\n    def has_migration(self, name: str) -> bool:\n        return self.get_global(s_migrations.Migration, name, None) is not None\n\n    @overload\n    def get_by_id(\n        self,\n        obj_id: uuid.UUID,\n        default: so.Object | so.NoDefaultT = so.NoDefault,\n        *,\n        type: None = None,\n    ) -> so.Object:\n        ...\n\n    @overload\n    def get_by_id(\n        self,\n        obj_id: uuid.UUID,\n        default: so.Object_T | so.NoDefaultT = so.NoDefault,\n        *,\n        type: Optional[type[so.Object_T]] = None,\n    ) -> so.Object_T:\n        ...\n\n    @overload\n    def get_by_id(\n        self,\n        obj_id: uuid.UUID,\n        default: None = None,\n        *,\n        type: Optional[type[so.Object_T]] = None,\n    ) -> Optional[so.Object_T]:\n        ...\n\n    def get_by_id(\n        self,\n        obj_id: uuid.UUID,\n        default: so.Object_T | so.NoDefaultT | None = so.NoDefault,\n        *,\n        type: Optional[type[so.Object_T]] = None,\n    ) -> Optional[so.Object_T]:\n        return self._get_by_id(obj_id, default, type=type)\n\n    @abc.abstractmethod\n    def _get_by_id(\n        self,\n        obj_id: uuid.UUID,\n        default: so.Object_T | so.NoDefaultT | None = so.NoDefault,\n        *,\n        type: Optional[type[so.Object_T]] = None,\n    ) -> Optional[so.Object_T]:\n        raise NotImplementedError\n\n    @overload\n    def get_by_name[T: so.Object](\n        self,\n        name: sn.Name | str,\n        default: T | so.NoDefaultT = so.NoDefault,\n        type: Optional[type[T]] = None,\n        span: Optional[parsing.Span] = None\n    ) -> T: ...\n\n    @overload\n    def get_by_name[T: so.Object](\n        self,\n        name: sn.Name | str,\n        default: None = None,\n        type: Optional[type[T]] = None,\n        span: Optional[parsing.Span] = None\n    ) -> Optional[T]: ...\n\n    def get_by_name[T: so.Object](\n        self,\n        name: sn.Name | str,\n        default: T | so.NoDefaultT | None = so.NoDefault,\n        type: Optional[type[T]] = None,\n        span: Optional[parsing.Span] = None\n    ) -> Optional[T]:\n        \"\"\"Retrieve object by name (not global name or short name)\"\"\"\n\n        if isinstance(name, str):\n            name = sn.QualName.from_string(name)\n        obj = self._get_by_name(name)\n        if obj is not None:\n            if type is not None:\n                if not isinstance(obj, type):\n                    Schema.raise_wrong_type(name, obj.__class__, type, span)\n            return obj  # type: ignore\n        elif default is not so.NoDefault:\n            return default\n        else:\n            Schema.raise_bad_reference(name, type=type)\n\n    def get_by_shortname[T: s_func.Function | s_oper.Operator](\n        self,\n        mcls: type[T],\n        shortname: str | sn.Name,\n        span: Optional[parsing.Span] = None\n    ) -> tuple[T, ...]:\n        \"\"\"Retrieve object by shortname\"\"\"\n\n        if isinstance(shortname, str):\n            shortname = sn.QualName.from_string(shortname)\n        objs = self._get_by_shortname(mcls, shortname)\n        if objs is not None:\n            return objs\n        else:\n            Schema.raise_bad_reference(shortname, type=mcls)\n\n    # TODO: rename to get_by_globalname\n    @overload\n    def get_global[T: so.Object](\n        self,\n        mcls: type[T],\n        name: str | sn.Name,\n        default: T | so.NoDefaultT = so.NoDefault,\n    ) -> T:\n        ...\n\n    # TODO: rename to get_by_globalname\n    @overload\n    def get_global[T: so.Object](\n        self,\n        mcls: type[T],\n        name: str | sn.Name,\n        default: None = None,\n    ) -> Optional[T]:\n        ...\n\n    # TODO: rename to get_by_globalname\n    def get_global[T: so.Object](\n        self,\n        mcls: type[T],\n        name: str | sn.Name,\n        default: T | so.NoDefaultT | None = so.NoDefault,\n    ) -> Optional[T]:\n        if isinstance(name, str):\n            name = sn.UnqualName(name)\n        obj = self._get_by_globalname(mcls, name)\n        if obj is not None:\n            return obj\n        elif default is not so.NoDefault:\n            return default\n        else:\n            Schema.raise_bad_reference(name, type=mcls)\n\n    @overload\n    def get(\n        self,\n        name: str | sn.Name,\n        default: so.Object | so.NoDefaultT = so.NoDefault,\n        *,\n        module_aliases: Optional[Mapping[Optional[str], str]] = None,\n        condition: Optional[Callable[[so.Object], bool]] = None,\n        label: Optional[str] = None,\n        span: Optional[parsing.Span] = None,\n    ) -> so.Object:\n        ...\n\n    @overload\n    def get(\n        self,\n        name: str | sn.Name,\n        default: None,\n        *,\n        module_aliases: Optional[Mapping[Optional[str], str]] = None,\n        condition: Optional[Callable[[so.Object], bool]] = None,\n        label: Optional[str] = None,\n        span: Optional[parsing.Span] = None,\n    ) -> Optional[so.Object]:\n        ...\n\n    @overload\n    def get[T: so.Object](\n        self,\n        name: str | sn.Name,\n        default: T | so.NoDefaultT = so.NoDefault,\n        *,\n        module_aliases: Optional[Mapping[Optional[str], str]] = None,\n        type: type[T],\n        condition: Optional[Callable[[so.Object], bool]] = None,\n        label: Optional[str] = None,\n        span: Optional[parsing.Span] = None,\n    ) -> T:\n        ...\n\n    @overload\n    def get[T: so.Object](\n        self,\n        name: str | sn.Name,\n        default: None,\n        *,\n        module_aliases: Optional[Mapping[Optional[str], str]] = None,\n        type: type[T],\n        condition: Optional[Callable[[so.Object], bool]] = None,\n        label: Optional[str] = None,\n        span: Optional[parsing.Span] = None,\n    ) -> Optional[T]:\n        ...\n\n    @overload\n    def get(\n        self,\n        name: str | sn.Name,\n        default: so.Object | so.NoDefaultT | None = so.NoDefault,\n        *,\n        module_aliases: Optional[Mapping[Optional[str], str]] = None,\n        type: Optional[type[so.Object]] = None,\n        condition: Optional[Callable[[so.Object], bool]] = None,\n        label: Optional[str] = None,\n        span: Optional[parsing.Span] = None,\n    ) -> Optional[so.Object]:\n        ...\n\n    def get(\n        self,\n        name: str | sn.Name,\n        default: so.Object | so.NoDefaultT | None = so.NoDefault,\n        *,\n        module_aliases: Optional[Mapping[Optional[str], str]] = None,\n        type: Optional[type[so.Object]] = None,\n        condition: Optional[Callable[[so.Object], bool]] = None,\n        label: Optional[str] = None,\n        span: Optional[parsing.Span] = None,\n    ) -> Optional[so.Object]:\n\n        def getter(schema: Schema, name: sn.Name) -> Optional[so.Object]:\n            obj = schema._get_by_name(name)\n            if obj is not None and condition is not None:\n                if not condition(obj):\n                    obj = None\n            return obj\n\n        obj = lookup(\n            self,\n            name,\n            getter=getter,\n            default=default,\n            module_aliases=module_aliases,\n        )\n\n        if obj is not so.NoDefault:\n            # We do our own type check, instead of using get_by_id's, so\n            # we can produce a user-facing error message.\n            if obj and type is not None and not isinstance(obj, type):\n                Schema.raise_wrong_type(name, obj.__class__, type, span)\n\n            return obj\n        else:\n            Schema.raise_bad_reference(\n                name=name,\n                label=label,\n                module_aliases=module_aliases,\n                span=span,\n                type=type,\n            )\n\n    @abc.abstractmethod\n    def _get_object_ids(self) -> Iterable[uuid.UUID]:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def _get_global_name_ids(\n        self\n    ) -> Iterable[tuple[type[so.Object], uuid.UUID]]:\n        raise NotImplementedError\n\n    def get_children(\n        self,\n        scls: so.Object_T,\n    ) -> frozenset[so.Object_T]:\n        # Ideally get_referrers needs to be made generic via\n        # an overload on scls_type, but mypy crashes on that.\n        return self.get_referrers(\n            scls,\n            scls_type=type(scls),\n            field_name='bases',\n        )\n\n    def get_descendants(\n        self,\n        scls: so.Object_T,\n    ) -> frozenset[so.Object_T]:\n        return self.get_referrers(\n            scls, scls_type=type(scls), field_name='ancestors')\n\n    def get_objects[Object_T: so.Object](\n        self,\n        *,\n        exclude_stdlib: bool = False,\n        exclude_global: bool = False,\n        exclude_extensions: bool = False,\n        exclude_internal: bool = True,\n        included_modules: Optional[Iterable[sn.Name]] = None,\n        excluded_modules: Optional[Iterable[sn.Name]] = None,\n        included_items: Optional[Iterable[sn.Name]] = None,\n        excluded_items: Optional[Iterable[sn.Name]] = None,\n        type: Optional[type[Object_T]] = None,\n        extra_filters: Iterable[Callable[[Schema, Object_T], bool]] = (),\n    ) -> SchemaIterator[Object_T]:\n        return SchemaIterator[Object_T](\n            self,\n            self._get_object_ids(),\n            exclude_global=exclude_global,\n            exclude_stdlib=exclude_stdlib,\n            exclude_extensions=exclude_extensions,\n            exclude_internal=exclude_internal,\n            included_modules=included_modules,\n            excluded_modules=excluded_modules,\n            included_items=included_items,\n            excluded_items=excluded_items,\n            type=type,\n            extra_filters=extra_filters,\n        )\n\n    def get_modules(self) -> tuple[s_mod.Module, ...]:\n        modules = []\n        for mcls, id in self._get_global_name_ids():\n            if mcls is s_mod.Module:\n                modules.append(mcls(_private_id=id))\n        return tuple(modules)  # type: ignore\n\n    def get_last_migration(self) -> Optional[s_migrations.Migration]:\n        return _get_last_migration(self)\n\n    def get_casts_to_type(\n        self,\n        to_type: s_types.Type,\n        *,\n        implicit: bool = False,\n        assignment: bool = False,\n    ) -> frozenset[s_casts.Cast]:\n        return self._get_casts(\n            to_type,\n            disposition='to_type',\n            implicit=implicit,\n            assignment=assignment,\n        )\n\n    def get_casts_from_type(\n        self,\n        from_type: s_types.Type,\n        *,\n        implicit: bool = False,\n        assignment: bool = False,\n    ) -> frozenset[s_casts.Cast]:\n        return self._get_casts(\n            from_type,\n            disposition='from_type',\n            implicit=implicit,\n            assignment=assignment,\n        )\n\n    @lru.lru_method_cache()\n    def _get_casts(\n        self,\n        stype: s_types.Type,\n        *,\n        disposition: str,\n        implicit: bool = False,\n        assignment: bool = False,\n    ) -> frozenset[s_casts.Cast]:\n        all_casts = self.get_referrers(\n            stype, scls_type=s_casts.Cast, field_name=disposition\n        )\n\n        casts = []\n        for castobj in all_casts:\n            if implicit and not castobj.get_allow_implicit(self):\n                continue\n            if assignment and not castobj.get_allow_assignment(self):\n                continue\n            casts.append(castobj)\n\n        return frozenset(casts)\n\n    @overload\n    def get_referrers[T: so.Object](\n        self,\n        scls: so.Object,\n        *,\n        scls_type: type[T],\n        field_name: Optional[str] = None,\n    ) -> frozenset[T]:\n        ...\n\n    @overload\n    def get_referrers(\n        self,\n        scls: so.Object,\n        *,\n        scls_type: None = None,\n        field_name: Optional[str] = None,\n    ) -> frozenset[so.Object]:\n        ...\n\n    @abc.abstractmethod\n    def get_referrers(\n        self,\n        scls: so.Object,\n        *,\n        scls_type: Optional[type[so.Object]] = None,\n        field_name: Optional[str] = None,\n    ) -> frozenset[so.Object]:\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def get_referrers_ex(\n        self,\n        scls: so.Object,\n        *,\n        scls_type: Optional[type[so.Object_T]] = None,\n    ) -> dict[\n        tuple[type[so.Object_T], str],\n        frozenset[so.Object_T],\n    ]:\n        raise NotImplementedError\n\n    @staticmethod\n    def raise_wrong_type(\n        name: str | sn.Name,\n        actual_type: type[so.Object_T],\n        expected_type: type[so.Object_T],\n        span: Optional[parsing.Span],\n    ) -> NoReturn:\n        refname = str(name)\n\n        actual_type_name = actual_type.get_schema_class_displayname()\n        expected_type_name = expected_type.get_schema_class_displayname()\n        raise errors.InvalidReferenceError(\n            f'{refname!r} exists, but is {english.add_a(actual_type_name)}, '\n            f'not {english.add_a(expected_type_name)}',\n            span=span,\n        )\n\n    @staticmethod\n    def raise_bad_reference(\n        name: str | sn.Name,\n        *,\n        label: Optional[str] = None,\n        module_aliases: Optional[Mapping[Optional[str], str]] = None,\n        span: Optional[parsing.Span] = None,\n        type: Optional[type[so.Object]] = None,\n    ) -> NoReturn:\n        refname = str(name)\n\n        if label is None:\n            if type is not None:\n                label = type.get_schema_class_displayname()\n            else:\n                label = 'schema item'\n\n        if type is not None:\n            if issubclass(type, so.QualifiedObject):\n                if not sn.is_qualified(refname):\n                    if module_aliases is not None:\n                        default_module = module_aliases.get(None)\n                        if default_module is not None:\n                            refname = type.get_displayname_static(\n                                sn.QualName(default_module, refname),\n                            )\n                else:\n                    refname = type.get_displayname_static(\n                        sn.QualName.from_string(refname))\n            else:\n                refname = type.get_displayname_static(\n                    sn.UnqualName.from_string(refname))\n\n        raise errors.InvalidReferenceError(\n            f'{label} {refname!r} does not exist',\n            span=span,\n        )\n\n\nclass FlatSchema(Schema):\n\n    _id_to_data: immu.Map[uuid.UUID, tuple[Any, ...]]\n    _id_to_type: immu.Map[uuid.UUID, str]\n    _name_to_id: immu.Map[sn.Name, uuid.UUID]\n    _shortname_to_id: immu.Map[\n        tuple[type[so.Object], sn.Name],\n        frozenset[uuid.UUID],\n    ]\n    _globalname_to_id: immu.Map[\n        tuple[type[so.Object], sn.Name],\n        uuid.UUID,\n    ]\n    _refs_to: Refs_T\n    _generation: int\n\n    def __init__(self) -> None:\n        self._id_to_data = immu.Map()\n        self._id_to_type = immu.Map()\n        self._shortname_to_id = immu.Map()\n        self._name_to_id = immu.Map()\n        self._globalname_to_id = immu.Map()\n        self._refs_to = immu.Map()\n        self._generation = 0\n\n    def _get_object_ids(self) -> Iterable[uuid.UUID]:\n        return self._id_to_type.keys()\n\n    def _get_global_name_ids(\n        self\n    ) -> Iterable[tuple[type[so.Object], uuid.UUID]]:\n        return (\n            (mcls, id) for (mcls, _name), id in self._globalname_to_id.items()\n        )\n\n    def _replace(\n        self,\n        *,\n        id_to_data: Optional[immu.Map[uuid.UUID, tuple[Any, ...]]] = None,\n        id_to_type: Optional[immu.Map[uuid.UUID, str]] = None,\n        name_to_id: Optional[immu.Map[sn.Name, uuid.UUID]] = None,\n        shortname_to_id: Optional[\n            immu.Map[\n                tuple[type[so.Object], sn.Name],\n                frozenset[uuid.UUID]\n            ]\n        ] = None,\n        globalname_to_id: Optional[\n            immu.Map[tuple[type[so.Object], sn.Name], uuid.UUID]\n        ] = None,\n        refs_to: Optional[Refs_T] = None,\n    ) -> FlatSchema:\n        new = FlatSchema.__new__(FlatSchema)\n\n        if id_to_data is None:\n            new._id_to_data = self._id_to_data\n        else:\n            new._id_to_data = id_to_data\n\n        if id_to_type is None:\n            new._id_to_type = self._id_to_type\n        else:\n            new._id_to_type = id_to_type\n\n        if name_to_id is None:\n            new._name_to_id = self._name_to_id\n        else:\n            new._name_to_id = name_to_id\n\n        if shortname_to_id is None:\n            new._shortname_to_id = self._shortname_to_id\n        else:\n            new._shortname_to_id = shortname_to_id\n\n        if globalname_to_id is None:\n            new._globalname_to_id = self._globalname_to_id\n        else:\n            new._globalname_to_id = globalname_to_id\n\n        if refs_to is None:\n            new._refs_to = self._refs_to\n        else:\n            new._refs_to = refs_to\n\n        new._generation = self._generation + 1\n\n        return new\n\n    def _update_obj_name(\n        self,\n        obj_id: uuid.UUID,\n        sclass: type[so.Object],\n        old_name: Optional[sn.Name],\n        new_name: Optional[sn.Name],\n    ) -> tuple[\n        immu.Map[sn.Name, uuid.UUID],\n        immu.Map[tuple[type[so.Object], sn.Name], frozenset[uuid.UUID]],\n        immu.Map[tuple[type[so.Object], sn.Name], uuid.UUID],\n    ]:\n        name_to_id = self._name_to_id\n        shortname_to_id = self._shortname_to_id\n        globalname_to_id = self._globalname_to_id\n        is_global = not issubclass(sclass, so.QualifiedObject)\n\n        has_sn_cache = issubclass(sclass, (s_func.Function, s_oper.Operator))\n\n        if old_name is not None:\n            if is_global:\n                globalname_to_id = globalname_to_id.delete((sclass, old_name))\n            else:\n                name_to_id = name_to_id.delete(old_name)\n            if has_sn_cache:\n                old_shortname = sn.shortname_from_fullname(old_name)\n                sn_key = (sclass, old_shortname)\n\n                new_ids = shortname_to_id[sn_key] - {obj_id}\n                if new_ids:\n                    shortname_to_id = shortname_to_id.set(sn_key, new_ids)\n                else:\n                    shortname_to_id = shortname_to_id.delete(sn_key)\n\n        if new_name is not None:\n            if is_global:\n                key = (sclass, new_name)\n                if key in globalname_to_id:\n                    other_obj = self.get_by_id(\n                        globalname_to_id[key], type=so.Object)\n                    vn = other_obj.get_verbosename(self, with_parent=True)\n                    raise errors.SchemaError(\n                        f'{vn} already exists')\n                globalname_to_id = globalname_to_id.set(key, obj_id)\n            else:\n                assert isinstance(new_name, sn.QualName)\n                if (\n                    not self.has_module(new_name.module)\n                    and new_name.get_module_name() not in SPECIAL_MODULES\n                ):\n                    raise errors.UnknownModuleError(\n                        f'module {new_name.module!r} is not in this schema')\n\n                if new_name in name_to_id:\n                    other_obj = self.get_by_id(\n                        name_to_id[new_name], type=so.Object)\n                    vn = other_obj.get_verbosename(self, with_parent=True)\n                    raise errors.SchemaError(\n                        f'{vn} already exists')\n                name_to_id = name_to_id.set(new_name, obj_id)\n\n            if has_sn_cache:\n                new_shortname = sn.shortname_from_fullname(new_name)\n                sn_key = (sclass, new_shortname)\n\n                try:\n                    ids = shortname_to_id[sn_key]\n                except KeyError:\n                    ids = frozenset()\n\n                shortname_to_id = shortname_to_id.set(sn_key, ids | {obj_id})\n\n        return name_to_id, shortname_to_id, globalname_to_id\n\n    def update_obj(\n        self,\n        obj: so.Object,\n        updates: Mapping[str, Any],\n    ) -> FlatSchema:\n        if not updates:\n            return self\n\n        obj_id = obj.id\n        sclass = type(obj)\n        all_fields = sclass.get_schema_fields()\n        object_ref_fields = sclass.get_object_reference_fields()\n        reducible_fields = sclass.get_reducible_fields()\n\n        try:\n            data = list(self._id_to_data[obj_id])\n        except KeyError:\n            data = [None] * len(all_fields)\n\n        name_to_id = None\n        shortname_to_id = None\n        globalname_to_id = None\n        orig_refs = {}\n        new_refs = {}\n\n        for fieldname, value in updates.items():\n            field = all_fields[fieldname]\n            findex = field.index\n            if fieldname == 'name':\n                name_to_id, shortname_to_id, globalname_to_id = (\n                    self._update_obj_name(\n                        obj_id,\n                        sclass,\n                        data[findex],\n                        value\n                    )\n                )\n\n            if value is None:\n                if field in reducible_fields and field in object_ref_fields:\n                    orig_value = data[findex]\n                    if orig_value is not None:\n                        orig_refs[fieldname] = (\n                            field.type.schema_refs_from_data(orig_value))\n            else:\n                if field in reducible_fields:\n                    value = value.schema_reduce()\n                    if field in object_ref_fields:\n                        new_refs[fieldname] = (\n                            field.type.schema_refs_from_data(value))\n                        orig_value = data[findex]\n                        if orig_value is not None:\n                            orig_refs[fieldname] = (\n                                field.type.schema_refs_from_data(orig_value))\n\n            data[findex] = value\n\n        id_to_data = self._id_to_data.set(obj_id, tuple(data))\n        refs_to = self._update_refs_to(obj_id, sclass, orig_refs, new_refs)\n\n        return self._replace(name_to_id=name_to_id,\n                             shortname_to_id=shortname_to_id,\n                             globalname_to_id=globalname_to_id,\n                             id_to_data=id_to_data,\n                             refs_to=refs_to)\n\n    def get_data_raw(\n        self,\n        obj: so.Object,\n    ) -> Optional[tuple[Any, ...]]:\n        return self._id_to_data.get(obj.id)\n\n    def get_field_raw(\n        self,\n        obj: so.Object,\n        field_index: int\n    ) -> Optional[Any]:\n        data = self._id_to_data.get(obj.id)\n        assert data, (\n            f'cannot get item data: item {str(obj.id)!r} '\n            f'is not present in the schema {self!r}'\n        )\n        return data[field_index]\n\n    def set_field(\n        self,\n        obj: so.Object,\n        fieldname: str,\n        value: Any,\n    ) -> FlatSchema:\n        obj_id = obj.id\n\n        try:\n            data = self._id_to_data[obj_id]\n        except KeyError:\n            err = (f'cannot set {fieldname!r} value: item {str(obj_id)!r} '\n                   f'is not present in the schema {self!r}')\n            raise errors.SchemaError(err) from None\n\n        sclass = so.ObjectMeta.get_schema_class(self._id_to_type[obj_id])\n\n        field = sclass.get_schema_field(fieldname)\n        findex = field.index\n        is_object_ref = field in sclass.get_object_reference_fields()\n\n        if field in sclass.get_reducible_fields():\n            value = value.schema_reduce()\n\n        name_to_id = None\n        shortname_to_id = None\n        globalname_to_id = None\n        if fieldname == 'name':\n            old_name = data[findex]\n            name_to_id, shortname_to_id, globalname_to_id = (\n                self._update_obj_name(obj_id, sclass, old_name, value)\n            )\n\n        data_list = list(data)\n        data_list[findex] = value\n        new_data = tuple(data_list)\n\n        id_to_data = self._id_to_data.set(obj_id, new_data)\n\n        if not is_object_ref:\n            refs_to = None\n        else:\n            orig_value = data[findex]\n            if orig_value is not None:\n                orig_refs = {\n                    fieldname: field.type.schema_refs_from_data(orig_value),\n                }\n            else:\n                orig_refs = {}\n\n            new_refs = {fieldname: field.type.schema_refs_from_data(value)}\n            refs_to = self._update_refs_to(obj_id, sclass, orig_refs, new_refs)\n\n        return self._replace(\n            name_to_id=name_to_id,\n            shortname_to_id=shortname_to_id,\n            globalname_to_id=globalname_to_id,\n            id_to_data=id_to_data,\n            refs_to=refs_to,\n        )\n\n    def unset_field(\n        self,\n        obj: so.Object,\n        fieldname: str,\n    ) -> FlatSchema:\n        obj_id = obj.id\n\n        try:\n            data = self._id_to_data[obj.id]\n        except KeyError:\n            return self\n\n        sclass = so.ObjectMeta.get_schema_class(self._id_to_type[obj.id])\n        field = sclass.get_schema_field(fieldname)\n        findex = field.index\n\n        name_to_id = None\n        shortname_to_id = None\n        globalname_to_id = None\n        orig_value = data[findex]\n\n        if orig_value is None:\n            return self\n\n        if fieldname == 'name':\n            name_to_id, shortname_to_id, globalname_to_id = (\n                self._update_obj_name(\n                    obj_id,\n                    sclass,\n                    orig_value,\n                    None\n                )\n            )\n\n        data_list = list(data)\n        data_list[findex] = None\n        new_data = tuple(data_list)\n\n        id_to_data = self._id_to_data.set(obj_id, new_data)\n        is_object_ref = field in sclass.get_object_reference_fields()\n\n        if not is_object_ref:\n            refs_to = None\n        else:\n            orig_refs = {\n                fieldname: field.type.schema_refs_from_data(orig_value),\n            }\n            refs_to = self._update_refs_to(obj_id, sclass, orig_refs, None)\n\n        return self._replace(\n            name_to_id=name_to_id,\n            shortname_to_id=shortname_to_id,\n            globalname_to_id=globalname_to_id,\n            id_to_data=id_to_data,\n            refs_to=refs_to,\n        )\n\n    def _update_refs_to(\n        self,\n        object_id: uuid.UUID,\n        sclass: type[so.Object],\n        orig_refs: Optional[Mapping[str, frozenset[uuid.UUID]]],\n        new_refs: Optional[Mapping[str, frozenset[uuid.UUID]]],\n    ) -> Refs_T:\n        objfields = sclass.get_object_reference_fields()\n        if not objfields:\n            return self._refs_to\n\n        with self._refs_to.mutate() as mm:\n            for field in objfields:\n                if not new_refs:\n                    ids = None\n                else:\n                    ids = new_refs.get(field.name)\n\n                if not orig_refs:\n                    orig_ids = None\n                else:\n                    orig_ids = orig_refs.get(field.name)\n\n                if not ids and not orig_ids:\n                    continue\n\n                old_ids: Optional[frozenset[uuid.UUID]]\n                new_ids: Optional[frozenset[uuid.UUID]]\n\n                key = (sclass, field.name)\n\n                if ids and orig_ids:\n                    new_ids = ids - orig_ids\n                    old_ids = orig_ids - ids\n                elif ids:\n                    new_ids = ids\n                    old_ids = None\n                else:\n                    new_ids = None\n                    old_ids = orig_ids\n\n                if new_ids:\n                    for ref_id in new_ids:\n                        try:\n                            refs = mm[ref_id]\n                        except KeyError:\n                            mm[ref_id] = immu.Map((\n                                (key, immu.Map(((object_id, None),))),\n                            ))\n                        else:\n                            try:\n                                field_refs = refs[key]\n                            except KeyError:\n                                field_refs = immu.Map(((object_id, None),))\n                            else:\n                                field_refs = field_refs.set(object_id, None)\n                            mm[ref_id] = refs.set(key, field_refs)\n\n                if old_ids:\n                    for ref_id in old_ids:\n                        refs = mm[ref_id]\n                        field_refs = refs[key].delete(object_id)\n                        if not field_refs:\n                            mm[ref_id] = refs.delete(key)\n                        else:\n                            mm[ref_id] = refs.set(key, field_refs)\n\n            result = mm.finish()\n\n        return result\n\n    def add(\n        self,\n        id: uuid.UUID,\n        sclass: type[so.Object],\n        data: tuple[Any, ...],\n    ) -> FlatSchema:\n        reducible_fields = sclass.get_reducible_fields()\n        if reducible_fields:\n            data_list = list(data)\n            for field in reducible_fields:\n                val = data[field.index]\n                if val is not None:\n                    data_list[field.index] = val.schema_reduce()\n            data = tuple(data_list)\n\n        name_field = sclass.get_schema_field('name')\n        name = data[name_field.index]\n\n        if name in self._name_to_id:\n            other_obj = self.get_by_id(\n                self._name_to_id[name], type=so.Object)\n            vn = other_obj.get_verbosename(self, with_parent=True)\n            raise errors.SchemaError(f'{vn} already exists')\n\n        if id in self._id_to_data:\n            raise errors.SchemaError(\n                f'{sclass.__name__} ({str(id)!r}) is already present '\n                f'in the schema {self!r}')\n\n        object_ref_fields = sclass.get_object_reference_fields()\n        if not object_ref_fields:\n            refs_to = None\n        else:\n            new_refs = {}\n            for field in object_ref_fields:\n                ref_data = data[field.index]\n                if ref_data is not None:\n                    ref = field.type.schema_refs_from_data(ref_data)\n                    new_refs[field.name] = ref\n            refs_to = self._update_refs_to(id, sclass, None, new_refs)\n\n        name_to_id, shortname_to_id, globalname_to_id = self._update_obj_name(\n            id, sclass, None, name)\n\n        updates = dict(\n            id_to_data=self._id_to_data.set(id, data),\n            id_to_type=self._id_to_type.set(id, sclass.__name__),\n            name_to_id=name_to_id,\n            shortname_to_id=shortname_to_id,\n            globalname_to_id=globalname_to_id,\n            refs_to=refs_to,\n        )\n\n        if (\n            issubclass(sclass, so.QualifiedObject)\n            and not self.has_module(name.module)\n            and name.get_module_name() not in SPECIAL_MODULES\n        ):\n            raise errors.UnknownModuleError(\n                f'module {name.module!r} is not in this schema')\n\n        return self._replace(**updates)  # type: ignore\n\n    def delist(self, name: sn.Name) -> FlatSchema:\n        name_to_id = self._name_to_id.delete(name)\n        return self._replace(\n            name_to_id=name_to_id,\n            shortname_to_id=self._shortname_to_id,\n            globalname_to_id=self._globalname_to_id,\n        )\n\n    def delete(self, obj: so.Object) -> FlatSchema:\n        data = self._id_to_data.get(obj.id)\n        if data is None:\n            raise errors.InvalidReferenceError(\n                f'cannot delete {obj!r}: not in this schema')\n\n        sclass = type(obj)\n        name_field = sclass.get_schema_field('name')\n        name = data[name_field.index]\n\n        updates = {}\n\n        name_to_id, shortname_to_id, globalname_to_id = self._update_obj_name(\n            obj.id, sclass, name, None)\n\n        object_ref_fields = sclass.get_object_reference_fields()\n        if not object_ref_fields:\n            refs_to = None\n        else:\n            values = self._id_to_data[obj.id]\n            orig_refs = {}\n            for field in object_ref_fields:\n                ref = values[field.index]\n                if ref is not None:\n                    ref = field.type.schema_refs_from_data(ref)\n                    orig_refs[field.name] = ref\n\n            refs_to = self._update_refs_to(obj.id, sclass, orig_refs, None)\n\n        updates.update(dict(\n            name_to_id=name_to_id,\n            shortname_to_id=shortname_to_id,\n            globalname_to_id=globalname_to_id,\n            id_to_data=self._id_to_data.delete(obj.id),\n            id_to_type=self._id_to_type.delete(obj.id),\n            refs_to=refs_to,\n        ))\n\n        return self._replace(**updates)  # type: ignore\n\n    def get_referrers(\n        self,\n        scls: so.Object,\n        *,\n        scls_type: Optional[type[so.Object_T]] = None,\n        field_name: Optional[str] = None,\n    ) -> frozenset[so.Object_T]:\n        return self._get_referrers(\n            scls, scls_type=scls_type, field_name=field_name\n        )\n\n    @lru.lru_method_cache()\n    def _get_referrers(\n        self,\n        scls: so.Object,\n        *,\n        scls_type: Optional[type[so.Object_T]] = None,\n        field_name: Optional[str] = None,\n    ) -> frozenset[so.Object_T]:\n\n        try:\n            refs = self._refs_to[scls.id]\n        except KeyError:\n            return frozenset()\n        else:\n            referrers: set[so.Object] = set()\n\n            if scls_type is not None:\n                if field_name is not None:\n                    for (st, fn), ids in refs.items():\n                        if issubclass(st, scls_type) and fn == field_name:\n                            referrers.update(\n                                self.get_by_id(objid) for objid in ids)\n                else:\n                    for (st, _), ids in refs.items():\n                        if issubclass(st, scls_type):\n                            referrers.update(\n                                self.get_by_id(objid) for objid in ids)\n            elif field_name is not None:\n                for (_, fn), ids in refs.items():\n                    if fn == field_name:\n                        referrers.update(\n                            self.get_by_id(objid) for objid in ids)\n            else:\n                refids = itertools.chain.from_iterable(refs.values())\n                referrers.update(self.get_by_id(objid) for objid in refids)\n\n            return frozenset(referrers)  # type: ignore\n\n    @lru.lru_method_cache()\n    def get_referrers_ex(\n        self,\n        scls: so.Object,\n        *,\n        scls_type: Optional[type[so.Object_T]] = None,\n    ) -> dict[\n        tuple[type[so.Object_T], str],\n        frozenset[so.Object_T],\n    ]:\n        try:\n            refs = self._refs_to[scls.id]\n        except KeyError:\n            return {}\n        else:\n            result = {}\n\n            if scls_type is not None:\n                for (st, fn), ids in refs.items():\n                    if issubclass(st, scls_type):\n                        result[st, fn] = frozenset(\n                            self.get_by_id(objid) for objid in ids)\n            else:\n                for (st, fn), ids in refs.items():\n                    result[st, fn] = frozenset(  # type: ignore\n                        self.get_by_id(objid) for objid in ids)\n\n            return result  # type: ignore\n\n    def _get_by_id(\n        self,\n        obj_id: uuid.UUID,\n        default: so.Object_T | so.NoDefaultT | None = so.NoDefault,\n        *,\n        type: Optional[type[so.Object_T]] = None,\n    ) -> Optional[so.Object_T]:\n        try:\n            sclass_name = self._id_to_type[obj_id]\n        except KeyError:\n            if default is so.NoDefault:\n                raise LookupError(\n                    f'reference to a non-existent schema item {obj_id}'\n                    f' in schema {self!r}'\n                ) from None\n            else:\n                return default\n        else:\n            obj = _raw_schema_restore(sclass_name, obj_id)\n            if type is not None and not isinstance(obj, type):\n                raise TypeError(\n                    f'schema object {obj_id!r} exists, but is a '\n                    f'{obj.__class__.get_schema_class_displayname()!r}, '\n                    f'not a {type.get_schema_class_displayname()!r}'\n                )\n\n            # Avoid the overhead of cast(Object_T) below\n            return obj  # type: ignore\n\n    # Important micro-optimization\n    if not TYPE_CHECKING:\n        get_by_id = _get_by_id\n\n    def _get_by_globalname[T: so.Object](\n        self, mcls: type[T], name: sn.Name,\n    ) -> Optional[T]:\n        if isinstance(name, str):\n            name = sn.UnqualName(name)\n        obj_id = self._globalname_to_id.get((mcls, name))\n        if obj_id is None:\n            return None\n        return _raw_schema_restore(mcls.__name__, obj_id)  # type: ignore\n\n    def _get_by_shortname[T: s_func.Function | s_oper.Operator](\n        self,\n        mcls: type[T],\n        shortname: sn.Name,\n    ) -> Optional[tuple[T, ...]]:\n        obj_ids = self._shortname_to_id.get((mcls, shortname))\n        if obj_ids is None:\n            return None\n        return tuple(\n            _raw_schema_restore(mcls.__name__, i)  # type: ignore\n            for i in obj_ids\n        )\n\n    def _get_by_name(\n        self,\n        name: sn.Name,\n    ) -> Optional[so.Object]:\n        obj_id = self._name_to_id.get(name)\n        if obj_id is None:\n            return None\n        return self.get_by_id(obj_id)\n\n    def has_object(self, object_id: uuid.UUID) -> bool:\n        return object_id in self._id_to_type\n\n    def get_objects(\n        self,\n        *,\n        exclude_stdlib: bool = False,\n        exclude_global: bool = False,\n        exclude_extensions: bool = False,\n        exclude_internal: bool = True,\n        included_modules: Optional[Iterable[sn.Name]] = None,\n        excluded_modules: Optional[Iterable[sn.Name]] = None,\n        included_items: Optional[Iterable[sn.Name]] = None,\n        excluded_items: Optional[Iterable[sn.Name]] = None,\n        type: Optional[type[so.Object_T]] = None,\n        extra_filters: Iterable[Callable[[Schema, so.Object_T], bool]] = (),\n    ) -> SchemaIterator[so.Object_T]:\n        return SchemaIterator[so.Object_T](\n            self,\n            self._id_to_type,\n            exclude_stdlib=exclude_stdlib,\n            exclude_global=exclude_global,\n            exclude_extensions=exclude_extensions,\n            exclude_internal=exclude_internal,\n            included_modules=included_modules,\n            excluded_modules=excluded_modules,\n            included_items=included_items,\n            excluded_items=excluded_items,\n            type=type,\n            extra_filters=extra_filters,\n        )\n\n    def __repr__(self) -> str:\n        return (\n            f'<{type(self).__name__} gen:{self._generation} at {id(self):#x}>')\n\n\ndef lookup[T](\n    schema: Schema,\n    name: sn.Name | str,\n    *,\n    getter: Callable[[Schema, sn.QualName], T | None],\n    default: T | so.NoDefaultT = so.NoDefault,\n    module_aliases: Optional[Mapping[Optional[str], str]],\n) -> T | so.NoDefaultT:\n    \"\"\"\n    Find something in the schema with a given name.\n\n    This function mostly mirrors edgeql.tracer.resolve_name\n    except:\n    - When searching in std, disallow some modules (often the base modules)\n    - If no result found, return default\n    \"\"\"\n    if isinstance(name, str):\n        name = sn.name_from_string(name)\n\n    obj_name = name.name\n    module = name.module if isinstance(name, sn.QualName) else None\n    orig_module = module\n\n    if module == '__std__':\n        fqname = sn.QualName('std', obj_name)\n        result = getter(schema, fqname)\n        if result is not None:\n            return result\n        else:\n            return default\n\n    # Apply module aliases\n    module = apply_module_aliases(\n        module, module_aliases\n    )\n\n    # Check if something matches the name\n    if module is not None:\n        fqname = sn.QualName(module, obj_name)\n        result = getter(schema, fqname)\n        if result is not None:\n            return result\n\n    # For unqualified names, fallback to std::{obj_name}\n    if orig_module is None:\n        fqname = sn.QualName('std', obj_name)\n        result = getter(schema, fqname)\n        if result is not None:\n            return result\n\n    # For qualified names, fallback to std::{module}::{obj_name}\n    # This is allowed only when there is no top-level module with the same name.\n    if module and not schema.has_module(module.split('::')[0]):\n        fqname = sn.QualName(f'std::{module}', obj_name)\n        result = getter(schema, fqname)\n        if result is not None:\n            return result\n\n    return default\n\n\ndef apply_module_aliases(\n    module: str | None,\n    module_aliases: Optional[Mapping[Optional[str], str]],\n) -> str | None:\n    if module_aliases is not None:\n        # Apply modalias\n        first: Optional[str]\n        if module:\n            first, sep, rest = module.partition('::')\n        else:\n            first, sep, rest = module, '', ''\n\n        fq_module = module_aliases.get(first)\n        if fq_module is not None:\n            module = fq_module + sep + rest\n\n    return module\n\n\nEMPTY_SCHEMA: Schema = FlatSchema()\n\n\ndef upgrade_schema(schema: Schema) -> Schema:\n    \"\"\"Repair a schema object serialized by an older patch version\n\n    When an edgeql+schema patch adds fields to schema types, old\n    serialized schemas will be broken, since their tuples are missing\n    the fields.\n\n    In this situation, we run through all the data tuples and fill\n    them out. The upgraded version will then be cached.\n    \"\"\"\n\n    if isinstance(schema, ChainedSchema):\n        return ChainedSchema(\n            base_schema=upgrade_schema(schema._base_schema),\n            top_schema=upgrade_schema(schema._top_schema),\n            global_schema=upgrade_schema(schema._global_schema),\n        )\n    assert isinstance(schema, FlatSchema)\n\n    cls_fields = {}\n    for py_cls in so.ObjectMeta.get_schema_metaclasses():\n        if isinstance(py_cls, adapter.Adapter):\n            continue\n\n        fields = py_cls._schema_fields.values()\n        cls_fields[py_cls] = sorted(fields, key=lambda f: f.index)\n\n    id_to_data = schema._id_to_data\n    fixes = {}\n    for id, typ_name in schema._id_to_type.items():\n        data = id_to_data[id]\n        obj = so.Object.schema_restore((typ_name, id))\n        typ = type(obj)\n\n        tfields = cls_fields[typ]\n        exp_len = len(tfields)\n        if len(data) < exp_len:\n            ldata = list(data)\n            for _ in range(len(ldata), exp_len):\n                ldata.append(None)\n\n            fixes[id] = tuple(ldata)\n\n    return schema._replace(id_to_data=id_to_data.update(fixes))\n\n\nclass SchemaIterator[Object_T: so.Object]:\n    def __init__(\n        self,\n        schema: Schema,\n        object_ids: Iterable[uuid.UUID],\n        *,\n        exclude_stdlib: bool = False,\n        exclude_global: bool = False,\n        exclude_extensions: bool = False,\n        exclude_internal: bool = True,\n        included_modules: Optional[Iterable[sn.Name]],\n        excluded_modules: Optional[Iterable[sn.Name]],\n        included_items: Optional[Iterable[sn.Name]] = None,\n        excluded_items: Optional[Iterable[sn.Name]] = None,\n        type: Optional[type[Object_T]] = None,\n        extra_filters: Iterable[Callable[[Schema, Object_T], bool]] = (),\n    ) -> None:\n\n        filters = []\n\n        if type is not None:\n            t = type\n            filters.append(lambda schema, obj: isinstance(obj, t))\n\n        if included_modules:\n            modules = frozenset(included_modules)\n            filters.append(\n                lambda schema, obj:\n                    isinstance(obj, so.QualifiedObject) and\n                    obj.get_name(schema).get_module_name() in modules)\n\n        if excluded_modules or exclude_stdlib:\n            excmod: set[sn.Name] = set()\n            if excluded_modules:\n                excmod.update(excluded_modules)\n            if exclude_stdlib:\n                excmod.update(STD_MODULES)\n            filters.append(\n                lambda schema, obj: (\n                    not isinstance(obj, so.QualifiedObject)\n                    or obj.get_name(schema).get_module_name() not in excmod\n                )\n            )\n\n        if included_items:\n            objs = frozenset(included_items)\n            filters.append(\n                lambda schema, obj: obj.get_name(schema) in objs)\n\n        if excluded_items:\n            objs = frozenset(excluded_items)\n            filters.append(\n                lambda schema, obj: obj.get_name(schema) not in objs)\n\n        if exclude_stdlib:\n            filters.append(\n                lambda schema, obj: not isinstance(obj, s_pseudo.PseudoType)\n            )\n\n        if exclude_extensions:\n            filters.append(\n                lambda schema, obj:\n                obj.get_name(schema).get_root_module_name() != EXT_MODULE\n            )\n\n        if exclude_global:\n            filters.append(\n                lambda schema, obj: not isinstance(obj, so.GlobalObject)\n            )\n\n        if exclude_internal:\n            filters.append(\n                lambda schema, obj: not isinstance(obj, so.InternalObject)\n            )\n\n        # Extra filters are last, because they might depend on type.\n        filters.extend(extra_filters)\n\n        self._filters = filters\n        self._schema = schema\n        self._object_ids = object_ids\n\n    def __iter__(self) -> Iterator[Object_T]:\n        filters = self._filters\n        schema = self._schema\n        get_by_id = schema.get_by_id\n        for obj_id in self._object_ids:\n            obj = get_by_id(obj_id)\n            if all(f(self._schema, obj) for f in filters):\n                yield obj  # type: ignore\n\n\nclass ChainedSchema(Schema):\n\n    __slots__ = ('_base_schema', '_top_schema', '_global_schema')\n\n    def __init__(\n        self, base_schema: Schema, top_schema: Schema, global_schema: Schema\n    ) -> None:\n        self._base_schema = base_schema\n        self._top_schema = top_schema\n        self._global_schema = global_schema\n\n    def _get_object_ids(self) -> Iterable[uuid.UUID]:\n        return itertools.chain(\n            self._base_schema._get_object_ids(),\n            self._top_schema._get_object_ids(),\n            self._global_schema._get_object_ids(),\n        )\n\n    def _get_global_name_ids(\n        self\n    ) -> Iterable[tuple[type[so.Object], uuid.UUID]]:\n        return itertools.chain(\n            self._base_schema._get_global_name_ids(),\n            self._top_schema._get_global_name_ids(),\n            self._global_schema._get_global_name_ids(),\n        )\n\n    def get_top_schema(self) -> Schema:\n        return self._top_schema\n\n    def get_global_schema(self) -> Schema:\n        return self._global_schema\n\n    def add(\n        self,\n        id: uuid.UUID,\n        sclass: type[so.Object],\n        data: tuple[Any, ...],\n    ) -> ChainedSchema:\n        if issubclass(sclass, so.GlobalObject):\n            return ChainedSchema(\n                self._base_schema,\n                self._top_schema,\n                self._global_schema.add(id, sclass, data),\n            )\n        else:\n            return ChainedSchema(\n                self._base_schema,\n                self._top_schema.add(id, sclass, data),\n                self._global_schema,\n            )\n\n    def delete(self, obj: so.Object) -> ChainedSchema:\n        if isinstance(obj, so.GlobalObject):\n            return ChainedSchema(\n                self._base_schema,\n                self._top_schema,\n                self._global_schema.delete(obj),\n            )\n        else:\n            return ChainedSchema(\n                self._base_schema,\n                self._top_schema.delete(obj),\n                self._global_schema,\n            )\n\n    def delist(\n        self,\n        name: sn.Name,\n    ) -> ChainedSchema:\n        return ChainedSchema(\n            self._base_schema,\n            self._top_schema.delist(name),\n            self._global_schema,\n        )\n\n    def update_obj(\n        self,\n        obj: so.Object,\n        updates: Mapping[str, Any],\n    ) -> ChainedSchema:\n        if isinstance(obj, so.GlobalObject):\n            return ChainedSchema(\n                self._base_schema,\n                self._top_schema,\n                self._global_schema.update_obj(obj, updates),\n            )\n        else:\n            obj_id = obj.id\n            base_obj = self._base_schema.get_by_id(obj_id, default=None)\n            if (\n                base_obj is not None\n                and not self._top_schema.has_object(obj_id)\n            ):\n                top_schema = self._top_schema.add(\n                    obj_id,\n                    type(base_obj),\n                    self._base_schema.get_data_raw(base_obj),\n                )\n            else:\n                top_schema = self._top_schema\n\n            return ChainedSchema(\n                self._base_schema,\n                top_schema.update_obj(obj, updates),\n                self._global_schema,\n            )\n\n    def get_data_raw(\n        self,\n        obj: so.Object,\n    ) -> Optional[tuple[Any, ...]]:\n        data = self._top_schema.get_data_raw(obj)\n        if data is not None:\n            return data\n        data = self._base_schema.get_data_raw(obj)\n        if data is not None:\n            return data\n        return self._global_schema.get_data_raw(obj)\n\n    def get_field_raw(\n        self,\n        obj: so.Object,\n        field_index: int,\n    ) -> Optional[Any]:\n        if self._top_schema.has_object(obj.id):\n            return self._top_schema.get_field_raw(obj, field_index)\n        if self._base_schema.has_object(obj.id):\n            return self._base_schema.get_field_raw(obj, field_index)\n        if self._global_schema.has_object(obj.id):\n            return self._global_schema.get_field_raw(obj, field_index)\n        raise AssertionError(\n            f'cannot get item data: item {str(obj.id)!r} '\n            f'is not present in the schema {self!r}'\n        )\n\n    def set_field(\n        self,\n        obj: so.Object,\n        fieldname: str,\n        value: Any,\n    ) -> ChainedSchema:\n        if isinstance(obj, so.GlobalObject):\n            return ChainedSchema(\n                self._base_schema,\n                self._top_schema,\n                self._global_schema.set_field(obj, fieldname, value),\n            )\n        else:\n            return ChainedSchema(\n                self._base_schema,\n                self._top_schema.set_field(obj, fieldname, value),\n                self._global_schema,\n            )\n\n    def unset_field(\n        self,\n        obj: so.Object,\n        field: str,\n    ) -> ChainedSchema:\n        if isinstance(obj, so.GlobalObject):\n            return ChainedSchema(\n                self._base_schema,\n                self._top_schema,\n                self._global_schema.unset_field(obj, field),\n            )\n        else:\n            return ChainedSchema(\n                self._base_schema,\n                self._top_schema.unset_field(obj, field),\n                self._global_schema,\n            )\n\n    def get_referrers(\n        self,\n        scls: so.Object,\n        *,\n        scls_type: Optional[type[so.Object_T]] = None,\n        field_name: Optional[str] = None,\n    ) -> frozenset[so.Object_T]:\n        return (\n            self._base_schema.get_referrers(  # type: ignore [return-value]\n                scls,\n                scls_type=scls_type,\n                field_name=field_name,\n            )\n            | self._top_schema.get_referrers(\n                scls,\n                scls_type=scls_type,\n                field_name=field_name,\n            )\n            | self._global_schema.get_referrers(  # type: ignore [operator]\n                scls,\n                scls_type=scls_type,\n                field_name=field_name,\n            )\n        )\n\n    def get_referrers_ex(\n        self,\n        scls: so.Object,\n        *,\n        scls_type: Optional[type[so.Object_T]] = None,\n    ) -> dict[\n        tuple[type[so.Object_T], str],\n        frozenset[so.Object_T],\n    ]:\n        base = self._base_schema.get_referrers_ex(scls, scls_type=scls_type)\n        top = self._top_schema.get_referrers_ex(scls, scls_type=scls_type)\n        gl = self._global_schema.get_referrers_ex(scls, scls_type=scls_type)\n        return {\n            k: (\n                base.get(k, frozenset())\n                | top.get(k, frozenset())\n                | gl.get(k, frozenset())\n            )\n            for k in itertools.chain(base, top)\n        }\n\n    def _get_by_id(\n        self,\n        obj_id: uuid.UUID,\n        default: so.Object_T | so.NoDefaultT | None = so.NoDefault,\n        *,\n        type: Optional[type[so.Object_T]] = None,\n    ) -> Optional[so.Object_T]:\n        obj = self._top_schema.get_by_id(obj_id, type=type, default=None)\n        if obj is None:\n            obj = self._base_schema.get_by_id(\n                obj_id, default=None, type=type)\n            if obj is None:\n                obj = self._global_schema.get_by_id(\n                    obj_id, default=default, type=type)\n        return obj\n\n    # Important micro-optimization\n    if not TYPE_CHECKING:\n        get_by_id = _get_by_id\n\n    def _get_by_globalname[T: so.Object](\n        self, mcls: type[T], name: sn.Name,\n    ) -> Optional[T]:\n        if issubclass(mcls, so.GlobalObject):\n            if o := self._global_schema._get_by_globalname(\n                mcls, name\n            ):\n                return o  # type: ignore\n        if obj := self._top_schema._get_by_globalname(mcls, name):\n            return obj\n        return self._base_schema._get_by_globalname(mcls, name)\n\n    def _get_by_shortname[T: s_func.Function | s_oper.Operator](\n        self,\n        mcls: type[T],\n        shortname: sn.Name,\n    ) -> Optional[tuple[T, ...]]:\n        objs = self._base_schema._get_by_shortname(mcls, shortname)\n        if objs is not None:\n            return objs\n        return self._top_schema._get_by_shortname(mcls, shortname)\n\n    def _get_by_name(\n        self,\n        name: sn.Name,\n    ) -> Optional[so.Object]:\n        objs = self._base_schema._get_by_name(name)\n        if objs is not None:\n            return objs\n        return self._top_schema._get_by_name(name)\n\n    def has_object(self, object_id: uuid.UUID) -> bool:\n        return (\n            self._base_schema.has_object(object_id)\n            or self._top_schema.has_object(object_id)\n            or self._global_schema.has_object(object_id)\n        )\n\n\n@lru.per_job_lru_cache()\ndef _get_operators(\n    schema: Schema,\n    name: sn.Name,\n) -> tuple[s_oper.Operator, ...] | None:\n    return schema._get_by_shortname(s_oper.Operator, name)\n\n\n@lru.per_job_lru_cache()\ndef _get_last_migration(\n    schema: Schema,\n) -> Optional[s_migrations.Migration]:\n\n    migrations: list[s_migrations.Migration] = [\n        mcls(_private_id=id)  # type: ignore\n        for mcls, id in schema._get_global_name_ids()\n        if mcls is s_migrations.Migration\n    ]\n\n    if not migrations:\n        return None\n\n    migration_map = collections.defaultdict(list)\n    root = None\n    for m in migrations:\n        parents = m.get_parents(schema).objects(schema)\n        if not parents:\n            if root is not None:\n                raise errors.InternalServerError(\n                    'multiple migration roots found')\n            root = m\n        for parent in parents:\n            migration_map[parent].append(m)\n\n    if root is None:\n        raise errors.InternalServerError('cannot find migration root')\n\n    latest = root\n    while children := migration_map[latest]:\n        if len(children) > 1:\n            raise errors.InternalServerError(\n                'nonlinear migration history detected')\n        latest = children[0]\n\n    return latest\n"
  },
  {
    "path": "edb/schema/sources.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Optional,\n    Iterable,\n    Sequence,\n    overload,\n    TYPE_CHECKING,\n)\n\n\nfrom edb import errors\n\nfrom . import delta as sd\nfrom . import indexes\nfrom . import name as sn\nfrom . import objects as so\nfrom . import pointers as s_pointers\n\nif TYPE_CHECKING:\n    from . import links\n    from . import schema as s_schema\n\n\nclass SourceCommandContext[Source_T: Source](\n    sd.ObjectCommandContext[Source_T],\n    indexes.IndexSourceCommandContext,\n):\n    # context mixin\n    pass\n\n\nclass SourceCommand[Source_T: Source](\n    indexes.IndexSourceCommand[Source_T]\n):\n    pass\n\n\nclass Source(\n    so.QualifiedObject,\n    indexes.IndexableSubject,\n    so.Object,  # Help reflection figure out the right db MRO\n):\n    pointers_refs = so.RefDict(\n        attr='pointers',\n        requires_explicit_overloaded=True,\n        backref_attr='source',\n        ref_cls=s_pointers.Pointer)\n\n    pointers = so.SchemaField(\n        so.ObjectIndexByUnqualifiedName[s_pointers.Pointer],\n        inheritable=False, ephemeral=True, coerce=True, compcoef=0.857,\n        default=so.DEFAULT_CONSTRUCTOR)\n\n    @overload\n    def maybe_get_ptr[Pointer_T: s_pointers.Pointer](\n        self,\n        schema: s_schema.Schema,\n        name: sn.UnqualName,\n        *,\n        type: type[Pointer_T],\n    ) -> Optional[Pointer_T]:\n        ...\n\n    @overload\n    def maybe_get_ptr(\n        self,\n        schema: s_schema.Schema,\n        name: sn.UnqualName,\n        *,\n        type: Optional[type[s_pointers.Pointer]] = None,\n    ) -> Optional[s_pointers.Pointer]:\n        ...\n\n    def maybe_get_ptr(\n        self,\n        schema: s_schema.Schema,\n        name: sn.UnqualName,\n        *,\n        type: Optional[type[s_pointers.Pointer]] = None,\n    ) -> Optional[s_pointers.Pointer]:\n        ptr = self.get_pointers(schema).get(schema, name, None)\n        if ptr is not None and type is not None and not isinstance(ptr, type):\n            raise AssertionError(\n                f'{self.get_verbosename(schema)} has a the '\n                f' {str(name)!r} pointer, but it is not a'\n                f' {type.get_schema_class_displayname()}'\n            )\n        return ptr\n\n    @overload\n    def getptr[Pointer_T: s_pointers.Pointer](\n        self,\n        schema: s_schema.Schema,\n        name: sn.UnqualName,\n        *,\n        type: type[Pointer_T],\n    ) -> Pointer_T:\n        ...\n\n    @overload\n    def getptr(\n        self,\n        schema: s_schema.Schema,\n        name: sn.UnqualName,\n        *,\n        type: Optional[type[s_pointers.Pointer]] = None,\n    ) -> s_pointers.Pointer:\n        ...\n\n    def getptr(\n        self,\n        schema: s_schema.Schema,\n        name: sn.UnqualName,\n        *,\n        type: Optional[type[s_pointers.Pointer]] = None,\n    ) -> s_pointers.Pointer:\n        ptr = self.maybe_get_ptr(schema, name, type=type)\n        if ptr is None:\n            raise AssertionError(\n                f'{self.get_verbosename(schema)} has no'\n                f' link or property {str(name)!r}'\n            )\n        return ptr\n\n    def getrptrs(\n        self,\n        schema: s_schema.Schema,\n        name: str,\n        *,\n        sources: Iterable[so.Object] = ()\n    ) -> set[links.Link]:\n        return set()\n\n    def add_pointer(\n        self,\n        schema: s_schema.Schema,\n        pointer: s_pointers.Pointer,\n        *,\n        replace: bool = False\n    ) -> s_schema.Schema:\n        schema = self.add_classref(\n            schema, 'pointers', pointer, replace=replace)\n        return schema\n\n    def get_addon_columns(\n        self, schema: s_schema.Schema\n    ) -> Sequence[tuple[str, str, tuple[str, str]]]:\n        \"\"\"\n        Returns a list of columns that are present in the backing table of\n        this source, apart from the columns for pointers.\n        \"\"\"\n        res = []\n        from edb.common import debug\n\n        if not debug.flags.zombodb:\n            fts_index, _ = indexes.get_effective_object_index(\n                schema, self, sn.QualName(\"std::fts\", \"index\")\n            )\n\n            if fts_index:\n                res.append(\n                    (\n                        '__fts_document__',\n                        '__fts_document__',\n                        (\n                            'pg_catalog',\n                            'tsvector',\n                        ),\n                    )\n                )\n\n        ext_ai_index, _ = indexes.get_effective_object_index(\n            schema, self, sn.QualName(\"ext::ai\", \"index\")\n        )\n        if ext_ai_index:\n            idx_id = indexes.get_ai_index_id(schema, ext_ai_index)\n            dimensions = ext_ai_index.must_get_json_annotation(\n                schema,\n                sn.QualName(\n                    \"ext::ai\", \"embedding_dimensions\"),\n                int,\n            )\n            res.append(\n                (\n                    f'__ext_ai_{idx_id}_embedding__',\n                    f'__ext_ai_{idx_id}_embedding__',\n                    (\n                        'edgedb',\n                        f'vector({dimensions})',\n                    ),\n                )\n            )\n\n        return res\n\n\ndef populate_pointer_set_for_source_union(\n    schema: s_schema.Schema,\n    components: list[Source],\n    union: Source,\n    *,\n    modname: Optional[str] = None,\n) -> s_schema.Schema:\n    if modname is None:\n        modname = '__derived__'\n\n    union_pointers = {}\n\n    for pn, ptr in components[0].get_pointers(schema).items(schema):\n        ptrs = [ptr]\n        for component in components[1:]:\n            other_ptr = component.get_pointers(schema).get(\n                schema, pn, None)\n            if other_ptr is None:\n                break\n            ptrs.append(other_ptr)\n\n        if len(ptrs) == len(components):\n            # The pointer is present in all components.\n            if len(ptrs) == 1:\n                ptr = ptrs[0]\n            else:\n                try:\n                    schema, ptr = s_pointers.get_or_create_union_pointer(\n                        schema,\n                        ptrname=pn,\n                        source=union,\n                        direction=s_pointers.PointerDirection.Outbound,\n                        components=set(ptrs),\n                        modname=modname,\n                    )\n                except errors.SchemaError as e:\n                    # ptrs may have different verbose names\n                    # ensure the same one is always chosen\n                    vn = sorted(p.get_verbosename(schema) for p in ptrs)[0]\n                    e.args = (\n                        (f'with {vn} {e.args[0]}',)\n                        + e.args[1:]\n                    )\n                    raise e\n\n            union_pointers[pn] = ptr\n\n    if union_pointers:\n        for pn, ptr in union_pointers.items():\n            if union.maybe_get_ptr(schema, pn) is None:\n                schema = union.add_pointer(schema, ptr)\n\n    return schema\n"
  },
  {
    "path": "edb/schema/std.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport pathlib\n\nfrom edb import lib as stdlib\nfrom edb import errors\nfrom edb.common import uuidgen\n\nfrom edb import schema\nfrom edb.schema import delta as sd\nfrom edb.schema import version as s_ver\n\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.edgeql import parser as qlparser\n\nfrom . import ddl as s_ddl\nfrom . import name as sn\nfrom . import schema as s_schema\n\n\nSCHEMA_ROOT = pathlib.Path(schema.__path__[0])\nLIB_ROOT = pathlib.Path(stdlib.__path__[0])\nQL_COMPILER_ROOT = pathlib.Path(qlcompiler.__path__[0])\nQL_PARSER_ROOT = pathlib.Path(qlparser.__path__[0])\n\nCACHE_SRC_DIRS = (\n    (SCHEMA_ROOT, '.py'),\n    (QL_COMPILER_ROOT, '.py'),\n    (QL_PARSER_ROOT, '.py'),\n    (LIB_ROOT, '.edgeql'),\n)\n\n\ndef get_std_module_text(modname: sn.Name) -> str:\n\n    module_eql = ''\n\n    module_path = LIB_ROOT / str(modname)\n    module_files = []\n\n    if module_path.is_dir():\n        for entry in module_path.iterdir():\n            if entry.is_file() and entry.suffix == '.edgeql':\n                module_files.append(entry)\n    else:\n        module_path = module_path.with_suffix('.edgeql')\n        if not module_path.exists():\n            raise errors.SchemaError(f'std module not found: {modname}')\n        module_files.append(module_path)\n\n    module_files.sort(key=lambda p: p.name)\n\n    for module_file in module_files:\n        with open(module_file) as f:\n            module_eql += '\\n' + f.read()\n\n    return module_eql\n\n\ndef load_std_module(\n    schema: s_schema.Schema,\n    modname: sn.Name,\n) -> s_schema.Schema:\n\n    return s_ddl.apply_ddl_script(\n        get_std_module_text(modname),\n        schema=schema,\n        modaliases={},\n        stdmode=True,\n    )\n\n\nBASE_VERSION = uuidgen.UUID('013d1e23-51ce-11ee-a29d-e1f01853d332')\nGLOBAL_BASE_VERSION = uuidgen.UUID('013d235b-51ce-11ee-be76-bf15d10edfe5')\n\n\ndef make_schema_version(\n    schema: s_schema.Schema,\n) -> tuple[s_schema.Schema, s_ver.CreateSchemaVersion]:\n    context = sd.CommandContext(stdmode=True)\n    sv = sn.UnqualName('__schema_version__')\n    schema_version = s_ver.CreateSchemaVersion(classname=sv)\n    schema_version.set_attribute_value('name', sv)\n    schema_version.set_attribute_value('version', BASE_VERSION)\n    schema_version.set_attribute_value('internal', True)\n    schema = sd.apply(schema_version, schema=schema, context=context)\n    return schema, schema_version\n\n\ndef make_global_schema_version(\n    schema: s_schema.Schema,\n) -> tuple[s_schema.Schema, s_ver.CreateGlobalSchemaVersion]:\n    context = sd.CommandContext(stdmode=True)\n    sv = sn.UnqualName('__global_schema_version__')\n    schema_version = s_ver.CreateGlobalSchemaVersion(classname=sv)\n    schema_version.set_attribute_value('name', sv)\n    schema_version.set_attribute_value('version', GLOBAL_BASE_VERSION)\n    schema_version.set_attribute_value('internal', True)\n    schema = sd.apply(schema_version, schema=schema, context=context)\n    return schema, schema_version\n"
  },
  {
    "path": "edb/schema/triggers.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, AbstractSet, TYPE_CHECKING\n\nfrom edb import errors\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.edgeql import qltypes\n\nfrom . import annos as s_anno\nfrom . import delta as sd\nfrom . import expr as s_expr\nfrom . import name as sn\nfrom . import objects as so\nfrom . import referencing\nfrom . import schema as s_schema\nfrom . import sources as s_sources\nfrom . import types as s_types\n\nif TYPE_CHECKING:\n    from . import objtypes as s_objtypes\n\n\nclass Trigger(\n    referencing.NamedReferencedInheritingObject,\n    so.InheritingObject,  # Help reflection figure out the right db MRO\n    qlkind=qltypes.SchemaObjectClass.TRIGGER,\n    data_safe=True,\n):\n\n    # XXX: compcoef is zero since we don't have syntax yet\n    timing = so.SchemaField(\n        qltypes.TriggerTiming,\n        coerce=True,\n        compcoef=0.0,\n        special_ddl_syntax=True,\n    )\n\n    kinds = so.SchemaField(\n        so.MultiPropSet[qltypes.TriggerKind],\n        coerce=True,\n        compcoef=0.0,\n        special_ddl_syntax=True,\n    )\n\n    scope = so.SchemaField(\n        qltypes.TriggerScope,\n        coerce=True,\n        compcoef=0.0,\n        special_ddl_syntax=True,\n    )\n\n    expr = so.SchemaField(\n        s_expr.Expression,\n        compcoef=0.909,\n        special_ddl_syntax=True,\n    )\n\n    condition = so.SchemaField(\n        s_expr.Expression,\n        default=None,\n        coerce=True,\n        compcoef=0.909,\n        special_ddl_syntax=True,\n    )\n\n    subject = so.SchemaField(\n        so.InheritingObject,\n        compcoef=None,\n        inheritable=False)\n\n    # We don't support SET/DROP OWNED owned on triggers so we set its\n    # compcoef to 0.0\n    owned = so.SchemaField(\n        bool,\n        default=False,\n        inheritable=False,\n        compcoef=0.0,\n        reflection_method=so.ReflectionMethod.AS_LINK,\n        special_ddl_syntax=True,\n    )\n\n    def get_subject(self, schema: s_schema.Schema) -> s_objtypes.ObjectType:\n        subj: s_objtypes.ObjectType = self.get_field_value(schema, 'subject')\n        return subj\n\n\nclass TriggerCommandContext(\n    sd.ObjectCommandContext[Trigger],\n    s_anno.AnnotationSubjectCommandContext,\n):\n    pass\n\n\nclass TriggerSourceCommandContext[Source_T: s_sources.Source](\n    s_sources.SourceCommandContext[Source_T]\n):\n    pass\n\n\nclass TriggerCommand(\n    referencing.NamedReferencedInheritingObjectCommand[Trigger],\n    s_anno.AnnotationSubjectCommand[Trigger],\n    context_class=TriggerCommandContext,\n    referrer_context_class=TriggerSourceCommandContext,\n):\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super().canonicalize_attributes(schema, context)\n        parent_ctx = self.get_referrer_context_or_die(context)\n        source = parent_ctx.op.scls\n        trig_name = self.get_verbosename(parent=source.get_verbosename(schema))\n\n        for field in ('expr', 'condition'):\n            if (expr := self.get_local_attribute_value(field)) is None:\n                continue\n\n            vname = 'when' if field == 'condition' else 'using'\n\n            expression = self.compile_expr_field(\n                schema, context,\n                field=Trigger.get_field(field),\n                value=expr,\n            )\n\n            if field == 'condition':\n                target = schema.get(\n                    sn.QualName('std', 'bool'), type=s_types.Type)\n                expr_type = expression.irast.stype\n                if not expr_type.issubclass(expression.irast.schema, target):\n                    span = self.get_attribute_span(field)\n                    raise errors.SchemaDefinitionError(\n                        f'{vname} expression for {trig_name} is of invalid '\n                        f'type: '\n                        f'{expr_type.get_displayname(expression.irast.schema)}'\n                        f', expected {target.get_displayname(schema)}',\n                        span=span,\n                    )\n\n                if expression.irast.dml_exprs:\n                    raise errors.SchemaDefinitionError(\n                        'data-modifying statements are not allowed in trigger '\n                        'when clauses',\n                        span=expression.irast.dml_exprs[0].span,\n                    )\n\n        return schema\n\n    def _get_scope(\n        self,\n        schema: s_schema.Schema,\n    ) -> qltypes.TriggerScope:\n        return self.get_attribute_value('scope') or self.scls.get_scope(schema)\n\n    def _get_kinds(\n        self,\n        schema: s_schema.Schema,\n    ) -> AbstractSet[qltypes.TriggerKind]:\n        return self.get_attribute_value('kinds') or self.scls.get_kinds(schema)\n\n    def compile_expr_field(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: s_expr.Expression,\n        track_schema_ref_exprs: bool=False,\n    ) -> s_expr.CompiledExpression:\n        if field.name in {'expr', 'condition'}:\n            from edb.ir import pathid\n\n            parent_ctx = self.get_referrer_context_or_die(context)\n            source = parent_ctx.op.get_object(schema, context)\n            assert isinstance(source, s_types.Type)\n            # XXX: in_ddl_context_name is disabled for now because\n            # it causes the compiler to reject DML; we might actually\n            # want it for something, though, so we might need to\n            # improve that restriction.\n            # parent_vname = source.get_verbosename(schema)\n            # pol_name = self.get_verbosename(parent=parent_vname)\n            # in_ddl_context_name = pol_name\n\n            scope = self._get_scope(schema)\n            kinds = self._get_kinds(schema)\n\n            anchors: dict[str, pathid.PathId] = {}\n            if qltypes.TriggerKind.Insert not in kinds:\n                anchors['__old__'] = pathid.PathId.from_type(\n                    schema,\n                    source,\n                    typename=sn.QualName(module='__derived__', name='__old__'),\n                    env=None,\n                )\n            if qltypes.TriggerKind.Delete not in kinds:\n                anchors['__new__'] = pathid.PathId.from_type(\n                    schema,\n                    source,\n                    typename=sn.QualName(module='__derived__', name='__new__'),\n                    env=None,\n                )\n\n            singletons = (\n                frozenset(anchors.values())\n                if scope == qltypes.TriggerScope.Each else frozenset()\n            )\n\n            assert isinstance(source, s_types.Type)\n\n            try:\n                return type(value).compiled(\n                    value,\n                    schema=schema,\n                    options=qlcompiler.CompilerOptions(\n                        modaliases=context.modaliases,\n                        schema_object_context=self.get_schema_metaclass(),\n                        anchors=anchors,\n                        singletons=singletons,\n                        apply_query_rewrites=not context.stdmode,\n                        track_schema_ref_exprs=track_schema_ref_exprs,\n                        # in_ddl_context_name=in_ddl_context_name,\n                        detached=True,\n                        trigger_type=source,\n                        trigger_kinds=kinds,\n                    ),\n                    context=context,\n                )\n            except errors.QueryError as e:\n                if not e.has_span():\n                    e.set_span(\n                        self.get_attribute_span(field.name)\n                    )\n                raise\n        else:\n            return super().compile_expr_field(\n                schema, context, field, value, track_schema_ref_exprs)\n\n    def get_dummy_expr_field_value(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: Any,\n    ) -> Optional[s_expr.Expression]:\n        if field.name in {'expr', 'condition'}:\n            return s_expr.Expression(text='false')\n        else:\n            raise NotImplementedError(f'unhandled field {field.name!r}')\n\n    def validate_object(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        # XXX: verify we don't have the same bug as access policies\n        # where linkprop defaults are broken.\n        # (I think we won't need to, since we'll operate after\n        # the *real* operations)\n        pass\n\n\nclass CreateTrigger(\n    TriggerCommand,\n    referencing.CreateReferencedInheritingObject[Trigger],\n):\n    referenced_astnode = astnode = qlast.CreateTrigger\n\n    def get_ast_attr_for_field(\n        self,\n        field: str,\n        astnode: type[qlast.DDLOperation],\n    ) -> Optional[str]:\n        if (\n            field in ('timing', 'condition', 'kinds', 'scope', 'expr')\n            and issubclass(astnode, qlast.CreateTrigger)\n        ):\n            return field\n        else:\n            return super().get_ast_attr_for_field(field, astnode)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        cmd = super()._cmd_tree_from_ast(schema, astnode, context)\n\n        assert isinstance(astnode, qlast.CreateTrigger)\n        assert isinstance(cmd, TriggerCommand)\n\n        if astnode.expr:\n            cmd.set_attribute_value(\n                'expr',\n                s_expr.Expression.from_ast(\n                    astnode.expr, schema, context.modaliases,\n                    context.localnames,\n                ),\n                span=astnode.expr.span,\n            )\n        if astnode.condition is not None:\n            cmd.set_attribute_value(\n                'condition',\n                s_expr.Expression.from_ast(\n                    astnode.condition, schema, context.modaliases,\n                    context.localnames,\n                ),\n                span=astnode.condition.span,\n            )\n\n        cmd.set_attribute_value('timing', astnode.timing)\n        cmd.set_attribute_value('kinds', astnode.kinds)\n        cmd.set_attribute_value('scope', astnode.scope)\n\n        return cmd\n\n\nclass RenameTrigger(\n    TriggerCommand,\n    referencing.RenameReferencedInheritingObject[Trigger],\n):\n    pass\n\n\nclass RebaseTrigger(\n    TriggerCommand,\n    referencing.RebaseReferencedInheritingObject[Trigger],\n):\n    pass\n\n\nclass AlterTrigger(\n    TriggerCommand,\n    referencing.AlterReferencedInheritingObject[Trigger],\n):\n    referenced_astnode = astnode = qlast.AlterTrigger\n\n    def _alter_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._alter_begin(schema, context)\n\n        # TODO: We may wish to support this in the future but it will\n        # take some thought.\n        if (\n            self.get_attribute_value('owned')\n            and not self.get_orig_attribute_value('owned')\n        ):\n            raise errors.SchemaDefinitionError(\n                f'cannot alter the definition of inherited trigger '\n                f'{self.scls.get_displayname(schema)}',\n                span=self.span\n            )\n\n        return schema\n\n\nclass DeleteTrigger(\n    TriggerCommand,\n    referencing.DeleteReferencedInheritingObject[Trigger],\n):\n    referenced_astnode = astnode = qlast.DropTrigger\n"
  },
  {
    "path": "edb/schema/types.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport collections\nimport collections.abc\nimport enum\nimport typing\nfrom typing import Self\nimport uuid\n\nfrom edb import errors\n\nfrom edb.common import checked\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\nfrom edb.edgeql import compiler as qlcompiler\n\nfrom . import annos as s_anno\nfrom . import casts as s_casts\nfrom . import delta as sd\nfrom . import expr as s_expr\nfrom . import inheriting\nfrom . import name as s_name\nfrom . import objects as so\nfrom . import schema as s_schema\nfrom . import utils\n\nif typing.TYPE_CHECKING:\n    from typing import Any, Iterable, Iterator, Mapping, Optional\n    from typing import AbstractSet, Sequence, Callable\n    from edb.common import parsing\n\n\nMAX_TYPE_DISTANCE = 1_000_000_000\n\n\nclass ExprType(enum.IntEnum):\n    \"\"\"Enumeration to identify the type of an expression in aliases.\"\"\"\n    Select = enum.auto()\n    Insert = enum.auto()\n    Update = enum.auto()\n    Delete = enum.auto()\n    Group = enum.auto()\n\n    def is_update(self) -> bool:\n        return self == ExprType.Update\n\n    def is_insert(self) -> bool:\n        return self == ExprType.Insert\n\n    def is_mutation(self) -> bool:\n        return self != ExprType.Select and self != ExprType.Group\n\n\nTypeT_co = typing.TypeVar('TypeT_co', bound='Type', covariant=True)\nInheritingTypeT = typing.TypeVar('InheritingTypeT', bound='InheritingType')\nCollectionTypeT = typing.TypeVar('CollectionTypeT', bound='Collection')\nCollectionTypeT_co = typing.TypeVar(\n    'CollectionTypeT_co', bound='Collection', covariant=True)\nCollectionExprAliasT = typing.TypeVar(\n    'CollectionExprAliasT', bound='CollectionExprAlias'\n)\n\n\nclass Type(\n    so.SubclassableObject,\n    s_anno.AnnotationSubject,\n):\n    \"\"\"A schema item that is a valid *type*.\"\"\"\n\n    # If this type is an alias, expr will contain an expression that\n    # defines it.\n    expr = so.SchemaField(\n        s_expr.Expression,\n        default=None, coerce=True, compcoef=0.909)\n\n    # For a type representing an expression alias, this would contain the\n    # expression type.  Non-alias types have None here.\n    expr_type = so.SchemaField(\n        ExprType,\n        default=None, compcoef=0.909)\n\n    # True for views.  This should always match the value of\n    # `bool(expr_type)`, but can be exported in the introspection\n    # schema without revealing weird internals.\n    from_alias = so.SchemaField(\n        bool,\n        default=False,\n        # cannot alter type from being produced by an alias into an actual type\n        compcoef=0.0,\n    )\n\n    # True when from a global. The purpose of this is to ensure that\n    # the types from globals and aliases can't be migrated between\n    # each other.\n    from_global = so.SchemaField(\n        bool,\n        default=False, compcoef=0.2)\n\n    # True for aliases defined by CREATE ALIAS, false for local\n    # aliases in queries.\n    alias_is_persistent = so.SchemaField(\n        bool,\n        default=False, compcoef=None)\n\n    # If this type is a view defined by a nested shape expression,\n    # and the nested shape contains references to link properties,\n    # rptr will contain the inbound pointer class.\n    rptr = so.SchemaField(\n        so.Object,\n        weak_ref=True,\n        default=None, compcoef=0.909)\n\n    # The OID by which the backend refers to the type.\n    backend_id = so.SchemaField(\n        int,\n        default=None, inheritable=False)\n\n    # True for types that cannot be persistently stored.\n    # See std::fts::document for an example.\n    transient = so.SchemaField(bool, default=False)\n\n    def compare(\n        self,\n        other: so.Object,\n        *,\n        our_schema: s_schema.Schema,\n        their_schema: s_schema.Schema,\n        context: so.ComparisonContext,\n    ) -> float:\n        # We need to be able to compare objects and scalars, in some places\n        if (\n            isinstance(other, Type)\n            and not isinstance(other, self.__class__)\n            and not isinstance(self, other.__class__)\n        ):\n            return 0.0\n\n        return super().compare(\n            other,\n            our_schema=our_schema, their_schema=their_schema, context=context)\n\n    def is_blocking_ref(\n        self, schema: s_schema.Schema, reference: so.Object\n    ) -> bool:\n        return reference != self.get_rptr(schema)\n\n    def derive_subtype(\n        self: Self,\n        schema: s_schema.Schema,\n        *,\n        name: s_name.QualName,\n        mark_derived: bool = False,\n        attrs: Optional[Mapping[str, Any]] = None,\n        inheritance_merge: bool = True,\n        transient: bool = False,\n        preserve_endpoint_ptrs: bool = False,\n        inheritance_refdicts: Optional[AbstractSet[str]] = None,\n        stdmode: bool = False,\n        **kwargs: Any,\n    ) -> tuple[s_schema.Schema, Self]:\n\n        if self.get_name(schema) == name:\n            raise errors.SchemaError(\n                f'cannot derive {self!r}({name}) from itself')\n\n        derived_attrs: dict[str, object] = {}\n\n        if attrs is not None:\n            derived_attrs.update(attrs)\n\n        derived_attrs['name'] = name\n        derived_attrs['bases'] = so.ObjectList.create(schema, [self])\n        derived_attrs['from_alias'] = bool(derived_attrs.get('expr_type'))\n\n        cmd = sd.get_object_delta_command(\n            objtype=type(self),\n            cmdtype=sd.CreateObject,\n            schema=schema,\n            name=name,\n        )\n\n        for k, v in derived_attrs.items():\n            cmd.set_attribute_value(k, v)\n\n        context = sd.CommandContext(\n            modaliases={},\n            schema=schema,\n            stdmode=stdmode,\n        )\n\n        delta = sd.DeltaRoot()\n\n        with context(sd.DeltaRootContext(schema=schema, op=delta)):\n            if not inheritance_merge:\n                context.current().inheritance_merge = False\n\n            if inheritance_refdicts is not None:\n                context.current().inheritance_refdicts = inheritance_refdicts\n\n            if mark_derived:\n                context.current().mark_derived = True\n\n            if transient:\n                context.current().transient_derivation = True\n                if not preserve_endpoint_ptrs:\n                    context.current().slim_links = True\n\n            delta.add(cmd)\n            schema = delta.apply(schema, context)\n\n        derived = typing.cast(Self, schema.get(name))\n\n        return schema, derived\n\n    def is_object_type(self) -> bool:\n        return False\n\n    def is_free_object_type(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_union_type(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_intersection_type(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_compound_type(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_polymorphic(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_any(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_anytuple(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_anyobject(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_scalar(self) -> bool:\n        return False\n\n    def is_collection(self) -> bool:\n        return False\n\n    def is_array(self) -> bool:\n        return False\n\n    def is_json(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_tuple(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_range(self) -> bool:\n        return False\n\n    def is_multirange(self) -> bool:\n        return False\n\n    def is_enum(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_sequence(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_array_of_arrays(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def is_array_of_tuples(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def find_predicate(\n        self,\n        pred: Callable[[Type], bool],\n        schema: s_schema.Schema,\n    ) -> Optional[Type]:\n        if pred(self):\n            return self\n        else:\n            return None\n\n    def contains_predicate(\n        self,\n        pred: Callable[[Type], bool],\n        schema: s_schema.Schema,\n    ) -> bool:\n        return bool(self.find_predicate(pred, schema))\n\n    def find_generic(self, schema: s_schema.Schema) -> Optional[Type]:\n        return self.find_predicate(\n            lambda x: x.is_any(schema) or x.is_anyobject(schema), schema\n        )\n\n    def contains_object(self, schema: s_schema.Schema) -> bool:\n        return self.contains_predicate(lambda x: x.is_object_type(), schema)\n\n    def contains_json(self, schema: s_schema.Schema) -> bool:\n        return self.contains_predicate(lambda x: x.is_json(schema), schema)\n\n    def find_array(self, schema: s_schema.Schema) -> Optional[Type]:\n        return self.find_predicate(lambda x: x.is_array(), schema)\n\n    def contains_array_of_array(self, schema: s_schema.Schema) -> bool:\n        return self.contains_predicate(\n            lambda x: x.is_array_of_arrays(schema), schema)\n\n    def contains_array_of_tuples(self, schema: s_schema.Schema) -> bool:\n        return self.contains_predicate(\n            lambda x: x.is_array_of_tuples(schema), schema)\n\n    def test_polymorphic(self, schema: s_schema.Schema, poly: Type) -> bool:\n        \"\"\"Check if this type can be matched by a polymorphic type.\n\n        Examples:\n\n            - `array<anyscalar>`.test_polymorphic(`array<anytype>`) -> True\n            - `array<str>`.test_polymorphic(`array<anytype>`) -> True\n            - `array<int64>`.test_polymorphic(`anyscalar`) -> False\n            - `float32`.test_polymorphic(`anyint`) -> False\n            - `int32`.test_polymorphic(`anyint`) -> True\n        \"\"\"\n\n        if not poly.is_polymorphic(schema):\n            raise TypeError('expected a polymorphic type as a second argument')\n\n        if poly.is_any(schema):\n            return True\n        if poly.is_anyobject(schema) and self.is_object_type():\n            return True\n\n        return self._test_polymorphic(schema, poly)\n\n    def resolve_polymorphic(\n        self, schema: s_schema.Schema, other: Type\n    ) -> Optional[Type]:\n        \"\"\"Resolve the polymorphic type component.\n\n        Examples:\n\n            - `array<anytype>`.resolve_polymorphic(`array<int>`) -> `int`\n            - `array<anytype>`.resolve_polymorphic(`tuple<int>`) -> None\n        \"\"\"\n        if not self.is_polymorphic(schema):\n            return None\n\n        return self._resolve_polymorphic(schema, other)\n\n    def to_nonpolymorphic(\n        self: Self, schema: s_schema.Schema, concrete_type: Type\n    ) -> tuple[s_schema.Schema, Type]:\n        \"\"\"Produce an non-polymorphic version of self.\n\n        Example:\n            `array<anytype>`.to_nonpolymorphic(`int`) -> `array<int>`\n            `tuple<int, anytype>`.to_nonpolymorphic(`str`) -> `tuple<int, str>`\n        \"\"\"\n        if not self.is_polymorphic(schema):\n            raise TypeError('non-polymorphic type')\n\n        return self._to_nonpolymorphic(schema, concrete_type)\n\n    def _test_polymorphic(self, schema: s_schema.Schema, other: Type) -> bool:\n        return False\n\n    def _resolve_polymorphic(\n        self,\n        schema: s_schema.Schema,\n        concrete_type: Type,\n    ) -> Optional[Type]:\n        raise NotImplementedError(\n            f'{type(self)} does not support resolve_polymorphic()')\n\n    def _to_nonpolymorphic(\n        self: Self,\n        schema: s_schema.Schema,\n        concrete_type: Type,\n    ) -> tuple[s_schema.Schema, Type]:\n        raise NotImplementedError(\n            f'{type(self)} does not support to_nonpolymorphic()')\n\n    def is_view(self, schema: s_schema.Schema) -> bool:\n        return self.get_from_alias(schema)\n\n    def castable_to(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> bool:\n        if self.implicitly_castable_to(other, schema):\n            return True\n        elif self.assignment_castable_to(other, schema):\n            return True\n        else:\n            return False\n\n    def assignment_castable_to(\n        self, other: Type, schema: s_schema.Schema\n    ) -> bool:\n        return self.implicitly_castable_to(other, schema)\n\n    def implicitly_castable_to(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> bool:\n        return False\n\n    def get_implicit_cast_distance(\n        self, other: Type, schema: s_schema.Schema\n    ) -> int:\n        return -1\n\n    def find_common_implicitly_castable_type(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> tuple[s_schema.Schema, Optional[Type]]:\n        return schema, None\n\n    def get_union_of(\n        self: Self,\n        schema: s_schema.Schema,\n    ) -> Optional[so.ObjectSet[Self]]:\n        return None\n\n    def get_is_opaque_union(self, schema: s_schema.Schema) -> bool:\n        return False\n\n    def get_intersection_of(\n        self: Self,\n        schema: s_schema.Schema,\n    ) -> Optional[so.ObjectSet[Self]]:\n        return None\n\n    def material_type(\n        self: Self, schema: s_schema.Schema\n    ) -> tuple[s_schema.Schema, Self]:\n        return schema, self\n\n    def peel_view(self, schema: s_schema.Schema) -> Type:\n        return self\n\n    def get_common_parent_type_distance(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> int:\n        raise NotImplementedError\n\n    def allow_ref_propagation(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        refdict: so.RefDict,\n    ) -> bool:\n        return not self.is_view(schema)\n\n    def as_shell(\n        self: Self,\n        schema: s_schema.Schema,\n    ) -> TypeShell[Self]:\n        name = typing.cast(s_name.QualName, self.get_name(schema))\n\n        if (\n            (union_of := self.get_union_of(schema))\n            and not self.is_view(schema)\n        ):\n            assert isinstance(self, so.QualifiedObject)\n            return UnionTypeShell(\n                module=name.module,\n                components=[\n                    o.as_shell(schema) for o in union_of.objects(schema)\n                ],\n                opaque=self.get_is_opaque_union(schema),\n                schemaclass=type(self),\n            )\n        elif (\n            (intersection_of := self.get_intersection_of(schema))\n            and not self.is_view(schema)\n        ):\n            assert isinstance(self, so.QualifiedObject)\n            return IntersectionTypeShell(\n                module=name.module,\n                components=[\n                    o.as_shell(schema) for o in intersection_of.objects(schema)\n                ],\n                schemaclass=type(self),\n            )\n        else:\n            return TypeShell(\n                name=name,\n                schemaclass=type(self),\n            )\n\n    def record_cmd_object_aux_data(\n        self,\n        schema: s_schema.Schema,\n        cmd: sd.ObjectCommand[Type],\n    ) -> None:\n        super().record_cmd_object_aux_data(schema, cmd)\n        if self.is_compound_type(schema):\n            cmd.set_object_aux_data('is_compound_type', True)\n\n    def as_type_delete_if_unused(\n        self: Self,\n        schema: s_schema.Schema,\n    ) -> Optional[sd.DeleteObject[Self]]:\n        \"\"\"If this is type is owned by other objects, delete it if unused.\n\n        For types that get created behind the scenes as part of\n        another object, such as collection types and union types, this\n        should generate an appropriate deletion. Otherwise, it should\n        return None.\n        \"\"\"\n\n        return None\n\n    def _is_deletable(\n        self,\n        schema: s_schema.Schema,\n    ) -> bool:\n        # this type was already deleted by some other op\n        # (probably alias types cleanup)\n        return schema.get_by_id(self.id, default=None) is not None\n\n\nclass QualifiedType(so.QualifiedObject, Type):\n    pass\n\n\nclass InheritingType(so.DerivableInheritingObject, QualifiedType):\n\n    def material_type[\n        InheritingTypeT: InheritingType, Schema_T: s_schema.Schema\n    ](\n        self: InheritingTypeT,\n        schema: Schema_T,\n    ) -> tuple[Schema_T, InheritingTypeT]:\n        return schema, self.get_nearest_non_derived_parent(schema)\n\n    def peel_view(self, schema: s_schema.Schema) -> Type:\n        # When self is a view, this returns the class the view\n        # is derived from (which may be another view).  If no\n        # parent class is available, returns self.\n        if self.is_view(schema):\n            return typing.cast(Type, self.get_bases(schema).first(schema))\n        else:\n            return self\n\n    def get_common_parent_type_distance(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> int:\n        if other.is_any(schema) or self.is_any(schema):\n            return MAX_TYPE_DISTANCE\n\n        if not isinstance(other, type(self)):\n            return -1\n\n        if self == other:\n            return 0\n\n        ancestors = utils.get_class_nearest_common_ancestors(\n            schema, [self, other])\n\n        if not ancestors:\n            return -1\n        elif self in ancestors:\n            return 0\n        else:\n            all_ancestors = list(self.get_ancestors(schema).objects(schema))\n            return min(\n                all_ancestors.index(ancestor) + 1 for ancestor in ancestors)\n\n\nclass TypeShell(so.ObjectShell[TypeT_co]):\n\n    schemaclass: type[TypeT_co]\n    extra_args: tuple[qlast.Expr | qlast.TypeExpr, ...] | None\n\n    def __init__(\n        self,\n        *,\n        name: s_name.Name,\n        origname: Optional[s_name.Name] = None,\n        displayname: Optional[str] = None,\n        expr: Optional[str] = None,\n        schemaclass: type[TypeT_co],\n        span: Optional[parsing.Span] = None,\n        extra_args: tuple[qlast.Expr] | None = None,\n    ) -> None:\n        super().__init__(\n            name=name,\n            origname=origname,\n            displayname=displayname,\n            schemaclass=schemaclass,\n            span=span,\n        )\n\n        self.expr = expr\n        self.extra_args = extra_args\n\n    def is_polymorphic(self, schema: s_schema.Schema) -> bool:\n        return self.resolve(schema).is_polymorphic(schema)\n\n    def as_create_delta(\n        self,\n        schema: s_schema.Schema,\n        *,\n        view_name: Optional[s_name.QualName] = None,\n        attrs: Optional[dict[str, Any]] = None,\n    ) -> sd.Command:\n        raise NotImplementedError('unsupported typeshell')\n\n    def has_intersection(self) -> bool:\n        return False\n\n\nclass TypeExprShell(TypeShell[TypeT_co]):\n\n    components: tuple[TypeShell[TypeT_co], ...]\n\n    def __init__(\n        self,\n        *,\n        name: s_name.Name,\n        components: Iterable[TypeShell[TypeT_co]],\n        schemaclass: type[TypeT_co],\n        span: Optional[parsing.Span] = None,\n    ) -> None:\n        super().__init__(\n            name=name,\n            schemaclass=schemaclass,\n            span=span,\n        )\n        self.components = tuple(components)\n\n    def resolve_components(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[TypeT_co, ...]:\n        return tuple(c.resolve(schema) for c in self.components)\n\n    def get_components(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[TypeShell[TypeT_co], ...]:\n        return self.components\n\n    def has_intersection(self) -> bool:\n        return any(\n            c.has_intersection()\n            for c in self.components\n        )\n\n\nclass UnionTypeShell(TypeExprShell[TypeT_co]):\n\n    def __init__(\n        self,\n        *,\n        module: str,\n        components: Iterable[TypeShell[TypeT_co]],\n        opaque: bool = False,\n        schemaclass: type[TypeT_co],\n        span: Optional[parsing.Span] = None,\n    ) -> None:\n        name = get_union_type_name(\n            (c.name for c in components),\n            opaque=opaque,\n            module=module,\n        )\n        super().__init__(\n            name=name,\n            components=components,\n            schemaclass=schemaclass,\n            span=span,\n        )\n        self.opaque = opaque\n\n    def as_create_delta(\n        self,\n        schema: s_schema.Schema,\n        *,\n        view_name: Optional[s_name.QualName] = None,\n        attrs: Optional[dict[str, Any]] = None,\n    ) -> sd.Command:\n        assert isinstance(self.name, s_name.QualName)\n        cmd = CreateUnionType(classname=self.name)\n        for component in self.components:\n            if isinstance(component, TypeExprShell):\n                cmd.add_prerequisite(component.as_create_delta(schema))\n        cmd.set_attribute_value('name', self.name)\n        cmd.set_attribute_value('components', tuple(self.components))\n        cmd.set_attribute_value('is_opaque_union', self.opaque)\n        cmd.set_attribute_value('span', self.span)\n        return cmd\n\n    def __repr__(self) -> str:\n        dn = 'UnionType'\n        comps = ' | '.join(repr(c) for c in self.components)\n        return f'<{type(self).__name__} {dn}({comps}) at 0x{id(self):x}>'\n\n\nclass AlterType[TypeT: Type](sd.AlterObject[TypeT]):\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        if hasattr(self, 'scls') and self.scls.get_from_alias(schema):\n            # This is a nested view type, e.g\n            # __FooAlias_bar produced by  FooAlias := (SELECT Foo { bar: ... })\n            # and should obviously not appear as a top level definition.\n            return None\n        else:\n            return super()._get_ast(schema, context, parent_node=parent_node)\n\n\nclass RenameType[TypeT: Type](sd.RenameObject[TypeT]):\n\n    def _canonicalize(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        scls: TypeT,\n    ) -> None:\n        super()._canonicalize(schema, context, scls)\n\n        # Now, see if there are any compound or collection types using\n        # this type as a component.  We must rename them, as they derive\n        # their names from the names of their component types.\n        # We must be careful about the order in which we consider the\n        # referrers, because they may reference each other as well, and\n        # so we must proceed with renames starting from the simplest type.\n        referrers = collections.defaultdict(set)\n        referrer_map = schema.get_referrers_ex(scls, scls_type=Type)\n        for (_, field_name), objs in referrer_map.items():\n            for obj in objs:\n                referrers[obj].add(field_name)\n\n        ref_order = sd.sort_by_cross_refs(schema, referrers)\n\n        for ref_type in ref_order:\n            field_names = referrers[ref_type]\n            for field_name in field_names:\n                if field_name == 'union_of' or field_name == 'intersection_of':\n                    orig_ref_type_name = ref_type.get_name(schema)\n                    assert isinstance(orig_ref_type_name, s_name.QualName)\n                    components = ref_type.get_field_value(\n                        schema, field_name)\n                    assert components is not None\n                    component_names = set(components.names(schema))\n                    component_names.discard(self.classname)\n                    component_names.add(self.new_name)\n\n                    if field_name == 'union_of':\n                        new_ref_type_name = get_union_type_name(\n                            component_names,\n                            module=orig_ref_type_name.module,\n                            opaque=ref_type.get_is_opaque_union(schema),\n                        )\n                    else:\n                        new_ref_type_name = get_intersection_type_name(\n                            component_names,\n                            module=orig_ref_type_name.module,\n                        )\n\n                    self.add(self.init_rename_branch(\n                        ref_type,\n                        new_ref_type_name,\n                        schema=schema,\n                        context=context,\n                    ))\n                elif (\n                    isinstance(ref_type, Tuple)\n                    and field_name == 'element_types'\n                ):\n                    subtypes = {\n                        k: st.get_name(schema)\n                        for k, st in (\n                            ref_type.get_element_types(schema).items(schema)\n                        )\n                    }\n                    new_tup_type_name = Tuple.generate_name(\n                        subtypes,\n                        named=ref_type.is_named(schema),\n                    )\n                    self.add(self.init_rename_branch(\n                        ref_type,\n                        new_tup_type_name,\n                        schema=schema,\n                        context=context,\n                    ))\n                elif (\n                    isinstance(ref_type, Array)\n                    and field_name == 'element_type'\n                ):\n                    new_arr_type_name = Array.generate_name(\n                        ref_type.get_element_type(schema).get_name(schema)\n                    )\n                    self.add(self.init_rename_branch(\n                        ref_type,\n                        new_arr_type_name,\n                        schema=schema,\n                        context=context,\n                    ))\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        if (\n            self.maybe_get_object_aux_data('is_compound_type')\n            or self.scls.is_view(schema)\n        ):\n            return None\n        else:\n            return super()._get_ast(schema, context, parent_node=parent_node)\n\n\nclass DeleteType[TypeT: Type](sd.DeleteObject[TypeT]):\n\n    def _get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        if self.maybe_get_object_aux_data('is_compound_type'):\n            return None\n        else:\n            return super()._get_ast(schema, context, parent_node=parent_node)\n\n\nclass RenameInheritingType(\n    RenameType[InheritingTypeT],\n    inheriting.RenameInheritingObject[InheritingTypeT],\n):\n    pass\n\n\nclass DeleteInheritingType(\n    DeleteType[InheritingTypeT],\n    inheriting.DeleteInheritingObject[InheritingTypeT],\n):\n    pass\n\n\nclass CompoundTypeCommandContext(sd.ObjectCommandContext[InheritingType]):\n    pass\n\n\nclass CompoundTypeCommand(\n    sd.QualifiedObjectCommand[InheritingType],\n    context_class=CompoundTypeCommandContext,\n):\n    pass\n\n\nclass CreateUnionType(sd.CreateObject[InheritingType], CompoundTypeCommand):\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n\n        from edb.schema import types as s_types\n\n        for cmd in self.get_prerequisites():\n            schema = cmd.apply(schema, context)\n\n        if not context.canonical:\n            components: Sequence[s_types.Type] = [\n                c.resolve(schema)\n                for c in self.get_attribute_value('components')\n            ]\n\n            try:\n                new_schema, union_type, created = utils.ensure_union_type(\n                    schema,\n                    components,\n                    opaque=self.get_attribute_value('is_opaque_union') or False,\n                    module=self.classname.module,\n                )\n            except errors.SchemaError as e:\n                union_name = (\n                    '(' + ' | '.join(sorted(\n                    c.get_displayname(schema)\n                    for c in components\n                    )) + ')'\n                )\n                e.args = (\n                    (f'cannot create union {union_name} {e.args[0]}',)\n                    + e.args[1:]\n                )\n                e.set_span(self.get_attribute_value('span'))\n                raise e\n\n            if created:\n                delta = union_type.as_create_delta(\n                    schema=new_schema,\n                    context=so.ComparisonContext(),\n                )\n\n                self.add(delta)\n\n        for cmd in self.get_subcommands(include_prerequisites=False):\n            schema = cmd.apply(schema, context)\n\n        return schema\n\n\nclass CreateIntersectionType(\n    sd.CreateObject[InheritingType], CompoundTypeCommand\n):\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n\n        from edb.schema import types as s_types\n\n        for cmd in self.get_prerequisites():\n            schema = cmd.apply(schema, context)\n\n        if not context.canonical:\n            components: Sequence[s_types.Type] = [\n                c.resolve(schema)\n                for c in self.get_attribute_value('components')\n            ]\n\n            try:\n                new_schema, intersection_type, created = (\n                    utils.ensure_intersection_type(\n                        schema,\n                        components,\n                        module=self.classname.module,\n                    )\n                )\n            except errors.SchemaError as e:\n                intersection_name = (\n                    '(' + ' | '.join(sorted(\n                    c.get_displayname(schema)\n                    for c in components\n                    )) + ')'\n                )\n                e.args = (\n                    (\n                        f'cannot create intersection '\n                        f'{intersection_name} {e.args[0]}',\n                    )\n                    + e.args[1:]\n                )\n                e.set_span(self.get_attribute_value('span'))\n                raise e\n\n            if created:\n                delta = intersection_type.as_create_delta(\n                    schema=new_schema,\n                    context=so.ComparisonContext(),\n                )\n\n                self.add(delta)\n\n        for cmd in self.get_subcommands(include_prerequisites=False):\n            schema = cmd.apply(schema, context)\n\n        return schema\n\n\nclass IntersectionTypeShell(TypeExprShell[TypeT_co]):\n    def __init__(\n        self,\n        *,\n        module: str,\n        components: Iterable[TypeShell[TypeT_co]],\n        schemaclass: type[TypeT_co],\n        span: parsing.Span | None = None,\n    ) -> None:\n        name = get_intersection_type_name(\n            (c.name for c in components),\n            module=module,\n        )\n\n        super().__init__(\n            name=name,\n            components=components,\n            schemaclass=schemaclass,\n            span=span\n        )\n\n    def as_create_delta(\n        self,\n        schema: s_schema.Schema,\n        *,\n        view_name: Optional[s_name.QualName] = None,\n        attrs: Optional[dict[str, Any]] = None,\n    ) -> sd.Command:\n        assert isinstance(self.name, s_name.QualName)\n        cmd = CreateIntersectionType(classname=self.name)\n        for component in self.components:\n            if isinstance(component, TypeExprShell):\n                cmd.add_prerequisite(component.as_create_delta(schema))\n        cmd.set_attribute_value('name', self.name)\n        cmd.set_attribute_value('components', tuple(self.components))\n        cmd.set_attribute_value('span', self.span)\n        return cmd\n\n    def has_intersection(self) -> bool:\n        return True\n\n\n_collection_impls: dict[str, type[Collection]] = {}\n\n\nclass Collection(Type):\n\n    _schema_name: typing.ClassVar[typing.Optional[str]] = None\n\n    #: True for collection types that are stored in schema persistently\n    is_persistent = so.SchemaField(\n        bool,\n        default=False,\n        compcoef=None,\n    )\n\n    def __init_subclass__(\n        cls,\n        *,\n        schema_name: typing.Optional[str] = None,\n    ) -> None:\n        super().__init_subclass__()\n        if schema_name is not None:\n            if existing := _collection_impls.get(schema_name):\n                raise TypeError(\n                    f\"{schema_name} is already implemented by {existing}\")\n            _collection_impls[schema_name] = cls\n            cls._schema_name = schema_name\n\n    def as_create_delta(\n        self: CollectionTypeT,\n        schema: s_schema.Schema,\n        context: so.ComparisonContext,\n    ) -> sd.CreateObject[CollectionTypeT]:\n        delta = super().as_create_delta(schema=schema, context=context)\n        assert isinstance(delta, sd.CreateObject)\n        if not isinstance(self, CollectionExprAlias):\n            delta.if_not_exists = True\n        return delta\n\n    def as_delete_delta(\n        self: CollectionTypeT,\n        *,\n        schema: s_schema.Schema,\n        context: so.ComparisonContext,\n    ) -> sd.ObjectCommand[CollectionTypeT]:\n        delta = super().as_delete_delta(schema=schema, context=context)\n        assert isinstance(delta, sd.DeleteObject)\n        if not isinstance(self, CollectionExprAlias):\n            delta.if_exists = True\n            if not (\n                isinstance(self, Array)\n                and self.get_element_type(schema).is_scalar()\n            ):\n                # Arrays of scalars are special, because we create them\n                # implicitly and overload reference checks to never\n                # delete them unless the scalar is also deleted.\n                delta.if_unused = True\n        return delta\n\n    @classmethod\n    def get_displayname_static(cls, name: s_name.Name) -> str:\n        if isinstance(name, s_name.QualName):\n            # FIXME: Globals and alias names do mangling but *don't*\n            # duplicate the module name, which most places do.\n            return str(name).split('@', 1)[0]\n        else:\n            return s_name.recursively_unmangle_shortname(str(name))\n\n    @classmethod\n    def get_schema_name(cls) -> str:\n        if cls._schema_name is None:\n            raise TypeError(\n                f\"{cls.get_schema_class_displayname()} is not \"\n                f\"a concrete collection type\"\n            )\n        return cls._schema_name\n\n    def get_generated_name(self, schema: s_schema.Schema) -> s_name.UnqualName:\n        \"\"\"Return collection type name generated from element types.\n\n        Unlike get_name(), which might return a custom name, this will always\n        return a name derived from the names of the collection element type(s).\n        \"\"\"\n        raise NotImplementedError\n\n    def is_polymorphic(self, schema: s_schema.Schema) -> bool:\n        return any(st.is_polymorphic(schema)\n                   for st in self.get_subtypes(schema))\n\n    def find_predicate(\n        self,\n        pred: Callable[[Type], bool],\n        schema: s_schema.Schema,\n    ) -> Optional[Type]:\n        if pred(self):\n            return self\n        for st in self.get_subtypes(schema):\n            res = st.find_predicate(pred, schema)\n            if res is not None:\n                return res\n\n        return None\n\n    def is_collection(self) -> bool:\n        return True\n\n    def get_common_parent_type_distance(\n        self, other: Type, schema: s_schema.Schema\n    ) -> int:\n        if other.is_any(schema):\n            return 1\n\n        if other.__class__ is not self.__class__:\n            return -1\n\n        other = typing.cast(Collection, other)\n        other_types = other.get_subtypes(schema)\n        my_types = self.get_subtypes(schema)\n\n        type_dist = 0\n        for ot, my in zip(other_types, my_types):\n            el_dist = my.get_common_parent_type_distance(ot, schema)\n            if el_dist < 0:\n                return -1\n            else:\n                type_dist += el_dist\n\n        return type_dist\n\n    def _issubclass(\n        self, schema: s_schema.Schema, parent: so.SubclassableObject\n    ) -> bool:\n        if isinstance(parent, Type) and parent.is_any(schema):\n            return True\n        if isinstance(parent, Type) and parent.is_anyobject(schema):\n            if isinstance(self, Type) and self.is_object_type():\n                return True\n\n        if parent.__class__ is not self.__class__:\n            return False\n\n        # The cast below should not be necessary but Mypy does not believe\n        # that a.__class__ == b.__class__ is enough.\n        parent_types = typing.cast(Collection, parent).get_subtypes(schema)\n        my_types = self.get_subtypes(schema)\n\n        for pt, my in zip(parent_types, my_types):\n            if not pt.is_any(schema) and not my.issubclass(schema, pt):\n                return False\n\n        return True\n\n    def issubclass(\n        self,\n        schema: s_schema.Schema,\n        parent: so.SubclassableObject | tuple[so.SubclassableObject, ...],\n    ) -> bool:\n        if isinstance(parent, tuple):\n            return any(self.issubclass(schema, p) for p in parent)\n\n        if isinstance(parent, Type) and parent.is_any(schema):\n            return True\n\n        return self._issubclass(schema, parent)\n\n    def get_subtypes(self, schema: s_schema.Schema) -> tuple[Type, ...]:\n        raise NotImplementedError\n\n    def get_typemods(self, schema: s_schema.Schema) -> Any:\n        return ()\n\n    @classmethod\n    def get_class(cls, schema_name: str) -> type[Collection]:\n        coll_type = _collection_impls.get(schema_name)\n        if coll_type:\n            return coll_type\n        else:\n            raise errors.SchemaError(\n                'unknown collection type: {!r}'.format(schema_name))\n\n    @classmethod\n    def from_subtypes(\n        cls,\n        schema: s_schema.Schema,\n        subtypes: Any,\n        typemods: Any = None,\n    ) -> tuple[s_schema.Schema, Collection]:\n        raise NotImplementedError\n\n    def __repr__(self) -> str:\n        return (\n            f'<{self.__class__.__name__} '\n            f'{self.id} at 0x{id(self):x}>'\n        )\n\n    def dump(self, schema: s_schema.Schema) -> str:\n        return repr(self)\n\n    # We define this specifically to override children\n    @classmethod\n    def get_schema_class_displayname(cls) -> str:\n        return 'collection'\n\n    def as_type_delete_if_unused(\n        self: CollectionTypeT,\n        schema: s_schema.Schema,\n    ) -> Optional[sd.DeleteObject[CollectionTypeT]]:\n        if not self._is_deletable(schema):\n            return None\n\n        return self.init_delta_command(\n            schema,\n            sd.DeleteObject,\n            if_unused=True,\n            if_exists=True,\n        )\n\n\nDimensions = checked.FrozenCheckedList[int]\nArray_T = typing.TypeVar(\"Array_T\", bound=\"Array\")\nArray_T_co = typing.TypeVar(\"Array_T_co\", bound=\"Array\", covariant=True)\n\n\nclass CollectionTypeShell(TypeShell[CollectionTypeT_co]):\n\n    def get_subtypes(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[TypeShell[Type], ...]:\n        raise NotImplementedError\n\n    def is_polymorphic(self, schema: s_schema.Schema) -> bool:\n        return any(\n            st.is_polymorphic(schema) for st in self.get_subtypes(schema)\n        )\n\n\nclass CollectionExprAlias(QualifiedType, Collection):\n\n    @classmethod\n    def get_schema_class_displayname(cls) -> str:\n        return 'expression alias'\n\n    @classmethod\n    def get_underlying_schema_class(cls) -> type[Collection]:\n        \"\"\"Return the concrete collection class for this ExprAlias class.\"\"\"\n        raise NotImplementedError\n\n    def as_underlying_type_delete_if_unused(\n        self,\n        schema: s_schema.Schema,\n    ) -> sd.DeleteObject[Type]:\n        \"\"\"Return a conditional deletion command for the underlying type object\n        \"\"\"\n        return sd.get_object_delta_command(\n            objtype=type(self).get_underlying_schema_class(),\n            cmdtype=sd.DeleteObject,\n            schema=schema,\n            name=self.get_generated_name(schema),\n            if_unused=True,\n            if_exists=True,\n        )\n\n    def as_type_delete_if_unused(\n        self: CollectionExprAliasT,\n        schema: s_schema.Schema,\n    ) -> Optional[sd.DeleteObject[CollectionExprAliasT]]:\n        if not self._is_deletable(schema):\n            return None\n\n        cmd = self.init_delta_command(schema, sd.DeleteObject, if_exists=True)\n        cmd.add_prerequisite(self.as_underlying_type_delete_if_unused(schema))\n        return cmd\n\n\nclass Array(\n    Collection,\n    qlkind=qltypes.SchemaObjectClass.ARRAY_TYPE,\n    schema_name='array',\n):\n\n    element_type = so.SchemaField(\n        Type,\n        # We want a low compcoef so that array types are *never* altered.\n        compcoef=0,\n    )\n\n    dimensions = so.SchemaField(\n        Dimensions,\n        coerce=True,\n        # We want a low compcoef so that array types are *never* altered.\n        compcoef=0,\n    )\n\n    @classmethod\n    def generate_name(\n        cls,\n        element_name: s_name.Name,\n    ) -> s_name.UnqualName:\n        return s_name.UnqualName(\n            f'array<{s_name.mangle_name(str(element_name))}>',\n        )\n\n    @classmethod\n    def create(\n        cls: type[Array_T],\n        schema: s_schema.Schema,\n        *,\n        name: Optional[s_name.Name] = None,\n        id: Optional[uuid.UUID] = None,\n        dimensions: Sequence[int] = (),\n        element_type: Any,\n        **kwargs: Any,\n    ) -> tuple[s_schema.Schema, Array_T]:\n        if not dimensions:\n            dimensions = [-1]\n\n        if dimensions != [-1]:\n            raise errors.UnsupportedFeatureError(\n                f'multi-dimensional arrays are not supported')\n\n        if name is None:\n            name = cls.generate_name(element_type.get_name(schema))\n\n        if isinstance(name, s_name.QualName):\n            result = schema.get(name, type=cls, default=None)\n        else:\n            result = schema.get_global(cls, name, default=None)\n\n        if result is None:\n            schema, result = super().create_in_schema(\n                schema,\n                id=id,\n                name=name,\n                element_type=element_type,\n                dimensions=dimensions,\n                **kwargs,\n            )\n            # Compute material type so that we can retrieve it safely later\n            schema, _ = result.material_type(schema)\n\n        return schema, result\n\n    def get_generated_name(self, schema: s_schema.Schema) -> s_name.UnqualName:\n        return type(self).generate_name(\n            self.get_element_type(schema).get_name(schema),\n        )\n\n    def is_array_of_arrays(self, schema: s_schema.Schema) -> bool:\n        return self.get_element_type(schema).is_array()\n\n    def is_array_of_tuples(self, schema: s_schema.Schema) -> bool:\n        return self.get_element_type(schema).is_tuple(schema)\n\n    def get_displayname(self, schema: s_schema.Schema) -> str:\n        return (\n            f'array<{self.get_element_type(schema).get_displayname(schema)}>')\n\n    def is_array(self) -> bool:\n        return True\n\n    def derive_subtype(\n        self,\n        schema: s_schema.Schema,\n        *,\n        name: s_name.QualName,\n        attrs: Optional[Mapping[str, Any]] = None,\n        **kwargs: Any,\n    ) -> tuple[s_schema.Schema, ArrayExprAlias]:\n        assert not kwargs\n        return ArrayExprAlias.from_subtypes(\n            schema,\n            [self.get_element_type(schema)],\n            self.get_typemods(schema),\n            name=name,\n            **(attrs or {}),\n        )\n\n    def get_subtypes(self, schema: s_schema.Schema) -> tuple[Type, ...]:\n        return (self.get_element_type(schema),)\n\n    def get_typemods(self, schema: s_schema.Schema) -> tuple[Any, ...]:\n        return (self.get_dimensions(schema),)\n\n    def implicitly_castable_to(\n        self, other: Type, schema: s_schema.Schema\n    ) -> bool:\n        if not isinstance(other, Array):\n            return False\n\n        return self.get_element_type(schema).implicitly_castable_to(\n            other.get_element_type(schema), schema)\n\n    def get_implicit_cast_distance(\n        self, other: Type, schema: s_schema.Schema\n    ) -> int:\n        if not isinstance(other, Array):\n            return -1\n\n        return self.get_element_type(schema).get_implicit_cast_distance(\n            other.get_element_type(schema), schema)\n\n    def assignment_castable_to(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> bool:\n        if not isinstance(other, Array):\n            from . import scalars as s_scalars\n\n            if not isinstance(other, s_scalars.ScalarType):\n                return False\n            if other.is_polymorphic(schema):\n                return False\n            right = other.get_base_for_cast(schema)\n            assert isinstance(right, Type)\n            return s_casts.is_assignment_castable(schema, self, right)\n\n        return self.get_element_type(schema).assignment_castable_to(\n            other.get_element_type(schema), schema)\n\n    def castable_to(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> bool:\n        if not isinstance(other, Array):\n            from . import scalars as s_scalars\n\n            if not isinstance(other, s_scalars.ScalarType):\n                return False\n            if other.is_polymorphic(schema):\n                return False\n            right = other.get_base_for_cast(schema)\n            assert isinstance(right, Type)\n            return s_casts.is_assignment_castable(schema, self, right)\n\n        return self.get_element_type(schema).castable_to(\n            other.get_element_type(schema), schema)\n\n    def find_common_implicitly_castable_type(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> tuple[s_schema.Schema, Optional[Array]]:\n\n        if not isinstance(other, Array):\n            return schema, None\n\n        if self == other:\n            return schema, self\n\n        my_el = self.get_element_type(schema)\n        schema, subtype = my_el.find_common_implicitly_castable_type(\n            other.get_element_type(schema), schema)\n\n        if subtype is None:\n            return schema, None\n\n        return Array.from_subtypes(schema, [subtype])\n\n    def _resolve_polymorphic(\n        self,\n        schema: s_schema.Schema,\n        concrete_type: Type,\n    ) -> Optional[Type]:\n        if not isinstance(concrete_type, Array):\n            return None\n\n        return self.get_element_type(schema).resolve_polymorphic(\n            schema, concrete_type.get_element_type(schema))\n\n    def _to_nonpolymorphic(\n        self,\n        schema: s_schema.Schema,\n        concrete_type: Type,\n    ) -> tuple[s_schema.Schema, Array]:\n        st = self.get_subtypes(schema=schema)[0]\n        # TODO: maybe we should have a generic nested polymorphic algo?\n        if isinstance(st, (Range, MultiRange)):\n            schema, newst = st.to_nonpolymorphic(schema, concrete_type)\n        else:\n            newst = concrete_type\n\n        return Array.from_subtypes(schema, (newst,))\n\n    def _test_polymorphic(self, schema: s_schema.Schema, other: Type) -> bool:\n        if other.is_any(schema):\n            return True\n\n        if not isinstance(other, Array):\n            return False\n\n        return self.get_element_type(schema).test_polymorphic(\n            schema, other.get_element_type(schema))\n\n    @classmethod\n    def from_subtypes(\n        cls: type[Array_T],\n        schema: s_schema.Schema,\n        subtypes: Sequence[Type],\n        typemods: Any = None,\n        *,\n        name: Optional[s_name.QualName] = None,\n        **kwargs: Any,\n    ) -> tuple[s_schema.Schema, Array_T]:\n        if len(subtypes) != 1:\n            raise errors.SchemaError(\n                f'unexpected number of subtypes, expecting 1: {subtypes!r}')\n        stype = subtypes[0]\n\n        # One-dimensional unbounded array.\n        dimensions = [-1]\n\n        schema, ty = cls.create(\n            schema,\n            element_type=stype,\n            dimensions=dimensions,\n            name=name,\n            **kwargs,\n        )\n        return schema, ty\n\n    @classmethod\n    def create_shell(\n        cls: type[Self],\n        schema: s_schema.Schema,\n        *,\n        subtypes: Sequence[TypeShell[Type]],\n        typemods: Any = None,\n        name: Optional[s_name.Name] = None,\n        expr: Optional[str] = None,\n    ) -> ArrayTypeShell[Self]:\n        if not typemods:\n            typemods = ([-1],)\n\n        st = next(iter(subtypes))\n\n        return ArrayTypeShell(\n            subtype=st,\n            typemods=typemods,\n            name=name,\n            expr=expr,\n            schemaclass=cls,\n        )\n\n    def as_shell(\n        self: Self,\n        schema: s_schema.Schema,\n    ) -> ArrayTypeShell[Self]:\n        expr = self.get_expr(schema)\n        expr_text = expr.text if expr is not None else None\n        return type(self).create_shell(\n            schema,\n            subtypes=[st.as_shell(schema) for st in self.get_subtypes(schema)],\n            typemods=self.get_typemods(schema),\n            name=self.get_name(schema),\n            expr=expr_text,\n        )\n\n    def material_type(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[s_schema.Schema, Array]:\n        # We need to resolve material types based on the subtype recursively.\n\n        st = self.get_element_type(schema)\n        schema, stm = st.material_type(schema)\n        if stm != st or isinstance(self, ArrayExprAlias):\n            return Array.from_subtypes(\n                schema,\n                [stm],\n                typemods=self.get_typemods(schema),\n            )\n        else:\n            return (schema, self)\n\n\nclass ArrayTypeShell(CollectionTypeShell[Array_T_co]):\n\n    schemaclass: type[Array_T_co]\n\n    def __init__(\n        self,\n        *,\n        name: Optional[s_name.Name],\n        expr: Optional[str] = None,\n        subtype: TypeShell[Type],\n        typemods: tuple[typing.Any, ...],\n        schemaclass: type[Array_T_co],\n    ) -> None:\n        if name is None:\n            name = schemaclass.generate_name(subtype.name)\n\n        super().__init__(name=name, schemaclass=schemaclass, expr=expr)\n        self.subtype = subtype\n        self.typemods = typemods\n\n    def get_subtypes(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[TypeShell[Type], ...]:\n        return (self.subtype,)\n\n    def get_displayname(self, schema: s_schema.Schema) -> str:\n        return f'array<{self.subtype.get_displayname(schema)}>'\n\n    def as_create_delta(\n        self,\n        schema: s_schema.Schema,\n        *,\n        view_name: Optional[s_name.QualName] = None,\n        attrs: Optional[dict[str, Any]] = None,\n    ) -> sd.CommandGroup:\n        ca: CreateArray | CreateArrayExprAlias\n        cmd = sd.CommandGroup()\n        if view_name is None:\n            ca = CreateArray(\n                classname=self.get_name(schema),\n                if_not_exists=True,\n            )\n        else:\n            ca = CreateArrayExprAlias(\n                classname=view_name,\n            )\n\n        el = self.subtype\n        if isinstance(el, CollectionTypeShell):\n            cmd.add(el.as_create_delta(schema))\n\n        ca.set_attribute_value('name', ca.classname)\n        ca.set_attribute_value('element_type', el)\n        ca.set_attribute_value('is_persistent', True)\n        ca.set_attribute_value('abstract', self.is_polymorphic(schema))\n        ca.set_attribute_value('dimensions', self.typemods[0])\n\n        if attrs:\n            for k, v in attrs.items():\n                ca.set_attribute_value(k, v)\n\n        cmd.add(ca)\n\n        return cmd\n\n\nclass ArrayExprAlias(\n    CollectionExprAlias,\n    Array,\n    qlkind=qltypes.SchemaObjectClass.ALIAS,\n):\n    # N.B: Don't add any SchemaFields to this class, they won't be\n    # reflected properly (since this inherits from the concrete Array).\n\n    @classmethod\n    def get_underlying_schema_class(cls) -> type[Collection]:\n        return Array\n\n\nTuple_T = typing.TypeVar('Tuple_T', bound='Tuple')\nTuple_T_co = typing.TypeVar('Tuple_T_co', bound='Tuple', covariant=True)\n\n\nclass Tuple(\n    Collection,\n    qlkind=qltypes.SchemaObjectClass.TUPLE_TYPE,\n    schema_name='tuple',\n):\n\n    named = so.SchemaField(\n        bool,\n        # We want a low compcoef so that tuples are *never* altered.\n        compcoef=0.01,\n    )\n\n    element_types = so.SchemaField(\n        so.ObjectDict[str, Type],\n        coerce=True,\n        # We want a low compcoef so that tuples are *never* altered.\n        compcoef=0.01,\n        # Tuple element types cannot be represented by a direct link,\n        # because the element types may be duplicate, so we need a\n        # proxy object.\n        reflection_proxy=('schema::TupleElement', 'type'),\n    )\n\n    @classmethod\n    def generate_name(\n        cls,\n        element_names: Mapping[str, s_name.Name],\n        named: bool = False,\n    ) -> s_name.UnqualName:\n        if named:\n            st_names = ', '.join(\n                f'{n}:{st}' for n, st in element_names.items()\n            )\n        else:\n            st_names = ', '.join(str(st) for st in element_names.values())\n\n        return s_name.UnqualName(f'tuple<{s_name.mangle_name(st_names)}>')\n\n    @classmethod\n    def create(\n        cls: type[Tuple_T],\n        schema: s_schema.Schema,\n        *,\n        name: Optional[s_name.Name] = None,\n        id: Optional[uuid.UUID] = None,\n        element_types: Mapping[str, Type],\n        named: bool = False,\n        **kwargs: Any,\n    ) -> tuple[s_schema.Schema, Tuple_T]:\n        el_types = so.ObjectDict[str, Type].create(schema, element_types)\n        if name is None:\n            name = cls.generate_name(\n                {n: el.get_name(schema) for n, el in element_types.items()},\n                named,\n            )\n\n        if isinstance(name, s_name.QualName):\n            result = schema.get(name, type=cls, default=None)\n        else:\n            result = schema.get_global(cls, name, default=None)\n\n        if result is None:\n            schema, result = super().create_in_schema(\n                schema,\n                id=id,\n                name=name,\n                named=named,\n                element_types=el_types,\n                **kwargs,\n            )\n            # Compute material type so that we can retrieve it safely later\n            schema, _ = result.material_type(schema)\n\n        return schema, result\n\n    def get_generated_name(self, schema: s_schema.Schema) -> s_name.UnqualName:\n        els = {n: st.get_name(schema) for n, st in self.iter_subtypes(schema)}\n        return type(self).generate_name(els, self.is_named(schema))\n\n    def get_displayname(self, schema: s_schema.Schema) -> str:\n        if self.is_named(schema):\n            st_names = ', '.join(\n                f'{name}: {st.get_displayname(schema)}'\n                for name, st in self.get_element_types(schema).items(schema)\n            )\n        else:\n            st_names = ', '.join(st.get_displayname(schema)\n                                 for st in self.get_subtypes(schema))\n        return f'tuple<{st_names}>'\n\n    def is_tuple(self, schema: s_schema.Schema) -> bool:\n        return True\n\n    def is_named(self, schema: s_schema.Schema) -> bool:\n        return self.get_named(schema)\n\n    def get_element_names(self, schema: s_schema.Schema) -> Sequence[str]:\n        return tuple(self.get_element_types(schema).keys(schema))\n\n    def iter_subtypes(\n        self, schema: s_schema.Schema\n    ) -> Iterator[tuple[str, Type]]:\n        yield from self.get_element_types(schema).items(schema)\n\n    def get_subtypes(self, schema: s_schema.Schema) -> tuple[Type, ...]:\n        return self.get_element_types(schema).values(schema)\n\n    def normalize_index(self, schema: s_schema.Schema, field: str) -> str:\n        if self.is_named(schema) and field.isdecimal():\n            idx = int(field)\n            el_names = self.get_element_names(schema)\n            if idx >= 0 and idx < len(el_names):\n                return el_names[idx]\n            else:\n                raise errors.InvalidReferenceError(\n                    f'{field} is not a member of '\n                    f'{self.get_displayname(schema)}')\n\n        return field\n\n    def index_of(self, schema: s_schema.Schema, field: str) -> int:\n        if field.isdecimal():\n            idx = int(field)\n            el_names = self.get_element_names(schema)\n            if idx >= 0 and idx < len(el_names):\n                if self.is_named(schema):\n                    return el_names.index(field)\n                else:\n                    return idx\n        elif self.is_named(schema):\n            el_names = self.get_element_names(schema)\n            try:\n                return el_names.index(field)\n            except ValueError:\n                pass\n\n        raise errors.InvalidReferenceError(\n            f'{field} is not a member of {self.get_displayname(schema)}')\n\n    def get_subtype(self, schema: s_schema.Schema, field: str) -> Type:\n        # index can be a name or a position\n        if field.isdecimal():\n            idx = int(field)\n            subtypes_l = list(self.get_subtypes(schema))\n            if idx >= 0 and idx < len(subtypes_l):\n                return subtypes_l[idx]\n\n        elif self.is_named(schema):\n            subtypes_d = dict(self.iter_subtypes(schema))\n            if field in subtypes_d:\n                return subtypes_d[field]\n\n        raise errors.InvalidReferenceError(\n            f'{field} is not a member of {self.get_displayname(schema)}')\n\n    def derive_subtype(\n        self,\n        schema: s_schema.Schema,\n        *,\n        name: s_name.QualName,\n        attrs: Optional[Mapping[str, Any]] = None,\n        **kwargs: Any,\n    ) -> tuple[s_schema.Schema, TupleExprAlias]:\n        assert not kwargs\n        return TupleExprAlias.from_subtypes(\n            schema,\n            dict(self.iter_subtypes(schema)),\n            self.get_typemods(schema),\n            name=name,\n            **(attrs or {}),\n        )\n\n    @classmethod\n    def from_subtypes(\n        cls: type[Tuple_T],\n        schema: s_schema.Schema,\n        subtypes: Iterable[Type] | Mapping[str, Type],\n        typemods: Any = None,\n        *,\n        name: Optional[s_name.QualName] = None,\n        **kwargs: Any,\n    ) -> tuple[s_schema.Schema, Tuple_T]:\n        named = False\n        if typemods is not None:\n            named = typemods.get('named', False)\n\n        types: Mapping[str, Type]\n        if isinstance(subtypes, collections.abc.Mapping):\n            types = subtypes\n        else:\n            types = {str(i): type for i, type in enumerate(subtypes)}\n        schema, ty = cls.create(\n            schema, element_types=types, named=named, name=name, **kwargs)\n        return schema, ty\n\n    @classmethod\n    def create_shell(\n        cls: type[Tuple_T],\n        schema: s_schema.Schema,\n        *,\n        subtypes: Mapping[str, TypeShell[Type]],\n        typemods: Any = None,\n        name: Optional[s_name.Name] = None,\n    ) -> TupleTypeShell[Tuple_T]:\n        return TupleTypeShell(\n            subtypes=subtypes,\n            typemods=typemods,\n            name=name,\n            schemaclass=cls,\n        )\n\n    def as_shell(\n        self: Self,\n        schema: s_schema.Schema,\n    ) -> TupleTypeShell[Self]:\n        stshells: dict[str, TypeShell[Type]] = {}\n\n        for n, st in self.iter_subtypes(schema):\n            stshells[n] = st.as_shell(schema)\n\n        return type(self).create_shell(\n            schema,\n            subtypes=stshells,\n            typemods=self.get_typemods(schema),\n            name=self.get_name(schema),\n        )\n\n    def implicitly_castable_to(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> bool:\n        if not isinstance(other, Tuple):\n            return False\n\n        self_subtypes = self.get_subtypes(schema)\n        other_subtypes = other.get_subtypes(schema)\n\n        if len(self_subtypes) != len(other_subtypes):\n            return False\n\n        if (\n            self.is_named(schema)\n            and other.is_named(schema)\n            and (self.get_element_names(schema)\n                 != other.get_element_names(schema))\n        ):\n            return False\n\n        for st, ot in zip(self_subtypes, other_subtypes):\n            if not st.implicitly_castable_to(ot, schema):\n                return False\n\n        return True\n\n    def get_implicit_cast_distance(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> int:\n        if not isinstance(other, Tuple):\n            return -1\n\n        self_subtypes = self.get_subtypes(schema)\n        other_subtypes = other.get_subtypes(schema)\n\n        if len(self_subtypes) != len(other_subtypes):\n            return -1\n\n        if (\n            self.is_named(schema)\n            and other.is_named(schema)\n            and (self.get_element_names(schema)\n                 != other.get_element_names(schema))\n        ):\n            return -1\n\n        total_dist = 0\n\n        for st, ot in zip(self_subtypes, other_subtypes):\n            dist = st.get_implicit_cast_distance(ot, schema)\n            if dist < 0:\n                return -1\n\n            total_dist += dist\n\n        return total_dist\n\n    def assignment_castable_to(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> bool:\n        if not isinstance(other, Tuple):\n            return False\n\n        self_subtypes = self.get_subtypes(schema)\n        other_subtypes = other.get_subtypes(schema)\n\n        if len(self_subtypes) != len(other_subtypes):\n            return False\n\n        if (\n            self.is_named(schema)\n            and other.is_named(schema)\n            and (self.get_element_names(schema)\n                 != other.get_element_names(schema))\n        ):\n            return False\n\n        for st, ot in zip(self_subtypes, other_subtypes):\n            if not st.assignment_castable_to(ot, schema):\n                return False\n\n        return True\n\n    def castable_to(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> bool:\n        if not isinstance(other, Tuple):\n            return False\n\n        self_subtypes = self.get_subtypes(schema)\n        other_subtypes = other.get_subtypes(schema)\n\n        if len(self_subtypes) != len(other_subtypes):\n            return False\n\n        for st, ot in zip(self_subtypes, other_subtypes):\n            if not st.castable_to(ot, schema):\n                return False\n\n        return True\n\n    def find_common_implicitly_castable_type(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> tuple[s_schema.Schema, Optional[Tuple]]:\n\n        if not isinstance(other, Tuple):\n            return schema, None\n\n        if self == other:\n            return schema, self\n\n        subs = self.get_subtypes(schema)\n        other_subs = other.get_subtypes(schema)\n\n        if len(subs) != len(other_subs):\n            return schema, None\n\n        new_types: list[Type] = []\n        for st, ot in zip(subs, other_subs):\n            schema, nt = st.find_common_implicitly_castable_type(ot, schema)\n            if nt is None:\n                return schema, None\n\n            new_types.append(nt)\n\n        if self.is_named(schema) and other.is_named(schema):\n            my_names = self.get_element_names(schema)\n            other_names = other.get_element_names(schema)\n            if my_names == other_names:\n                return Tuple.from_subtypes(\n                    schema, dict(zip(my_names, new_types)), {\"named\": True}\n                )\n\n        return Tuple.from_subtypes(schema, new_types)\n\n    def get_typemods(self, schema: s_schema.Schema) -> dict[str, bool]:\n        return {'named': self.is_named(schema)}\n\n    def _resolve_polymorphic(\n        self,\n        schema: s_schema.Schema,\n        concrete_type: Type,\n    ) -> Optional[Type]:\n        if not isinstance(concrete_type, Tuple):\n            return None\n\n        self_subtypes = self.get_subtypes(schema)\n        other_subtypes = concrete_type.get_subtypes(schema)\n\n        if len(self_subtypes) != len(other_subtypes):\n            return None\n\n        for source, target in zip(self_subtypes, other_subtypes):\n            if source.is_polymorphic(schema):\n                return source.resolve_polymorphic(schema, target)\n\n        return None\n\n    def _to_nonpolymorphic(\n        self: Self,\n        schema: s_schema.Schema,\n        concrete_type: Type,\n    ) -> tuple[s_schema.Schema, Self]:\n        new_types: list[Type] = []\n        for st in self.get_subtypes(schema):\n            if st.is_polymorphic(schema):\n                schema, nst = st.to_nonpolymorphic(schema, concrete_type)\n            else:\n                nst = st\n            new_types.append(nst)\n\n        if self.is_named(schema):\n            return type(self).from_subtypes(\n                schema,\n                dict(zip(self.get_element_names(schema), new_types)),\n                {\"named\": True},\n            )\n\n        return type(self).from_subtypes(schema, new_types)\n\n    def _test_polymorphic(self, schema: s_schema.Schema, other: Type) -> bool:\n        if other.is_any(schema) or other.is_anytuple(schema):\n            return True\n        if not isinstance(other, Tuple):\n            return False\n\n        self_subtypes = self.get_subtypes(schema)\n        other_subtypes = other.get_subtypes(schema)\n\n        if len(self_subtypes) != len(other_subtypes):\n            return False\n\n        return all(st.test_polymorphic(schema, ot)\n                   for st, ot in zip(self_subtypes, other_subtypes))\n\n    def material_type(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[s_schema.Schema, Tuple]:\n        # We need to resolve material types of all the subtypes recursively.\n        new_material_type = False\n        subtypes = {}\n\n        for st_name, st in self.iter_subtypes(schema):\n            schema, stm = st.material_type(schema)\n            if stm != st:\n                new_material_type = True\n            subtypes[st_name] = stm\n\n        if new_material_type or isinstance(self, TupleExprAlias):\n            return Tuple.from_subtypes(\n                schema, subtypes, typemods=self.get_typemods(schema))\n        else:\n            return schema, self\n\n\nclass TupleTypeShell(CollectionTypeShell[Tuple_T_co]):\n\n    schemaclass: type[Tuple_T_co]\n\n    def __init__(\n        self,\n        *,\n        name: Optional[s_name.Name],\n        subtypes: Mapping[str, TypeShell[Type]],\n        typemods: Any = None,\n        schemaclass: type[Tuple_T_co],\n    ) -> None:\n        if name is None:\n            named = typemods is not None and typemods.get('named', False)\n            name = schemaclass.generate_name(\n                {n: st.name for n, st in subtypes.items()},\n                named,\n            )\n\n        super().__init__(name=name, schemaclass=schemaclass)\n        self.subtypes = subtypes\n        self.typemods = typemods\n\n    def get_displayname(self, schema: s_schema.Schema) -> str:\n        st_names = ', '.join(st.get_displayname(schema)\n                             for st in self.get_subtypes(schema))\n        return f'tuple<{st_names}>'\n\n    def get_subtypes(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[TypeShell[Type], ...]:\n        return tuple(self.subtypes.values())\n\n    def iter_subtypes(\n        self,\n        schema: s_schema.Schema,\n    ) -> Iterator[tuple[str, TypeShell[Type]]]:\n        return iter(self.subtypes.items())\n\n    def is_named(self) -> bool:\n        return self.typemods is not None and self.typemods.get('named', False)\n\n    def as_create_delta(\n        self,\n        schema: s_schema.Schema,\n        *,\n        view_name: Optional[s_name.QualName] = None,\n        attrs: Optional[dict[str, Any]] = None,\n    ) -> CreateTuple | CreateTupleExprAlias:\n        ct: CreateTuple | CreateTupleExprAlias\n\n        plain_tuple = self._as_plain_create_delta(schema)\n        if view_name is None:\n            ct = plain_tuple\n        else:\n            ct = CreateTupleExprAlias(classname=view_name)\n            self._populate_create_delta(schema, ct, attrs=attrs)\n\n        for el in self.subtypes.values():\n            if isinstance(el, CollectionTypeShell):\n                ct.add_prerequisite(el.as_create_delta(schema))\n\n        if view_name is not None:\n            ct.add_prerequisite(plain_tuple)\n\n        return ct\n\n    def _as_plain_create_delta(\n        self,\n        schema: s_schema.Schema,\n    ) -> CreateTuple:\n        name = self.schemaclass.generate_name(\n            {n: st.get_name(schema) for n, st in self.subtypes.items()},\n            self.is_named(),\n        )\n        ct = CreateTuple(classname=name, if_not_exists=True)\n        self._populate_create_delta(schema, ct)\n        return ct\n\n    def _populate_create_delta(\n        self,\n        schema: s_schema.Schema,\n        ct: CreateTuple | CreateTupleExprAlias,\n        *,\n        attrs: Optional[dict[str, Any]] = None,\n    ) -> None:\n        named = self.is_named()\n        ct.set_attribute_value('name', ct.classname)\n        ct.set_attribute_value('named', named)\n        ct.set_attribute_value('abstract', self.is_polymorphic(schema))\n        ct.set_attribute_value('is_persistent', True)\n        ct.set_attribute_value('element_types', self.subtypes)\n\n        if attrs:\n            for k, v in attrs.items():\n                ct.set_attribute_value(k, v)\n\n\nclass TupleExprAlias(\n    CollectionExprAlias,\n    Tuple,\n    qlkind=qltypes.SchemaObjectClass.ALIAS,\n):\n    # N.B: Don't add any SchemaFields to this class, they won't be\n    # reflected properly (since this inherits from the concrete Tuple).\n\n    @classmethod\n    def get_underlying_schema_class(cls) -> type[Collection]:\n        return Tuple\n\n\nRange_T = typing.TypeVar('Range_T', bound='Range')\nRange_T_co = typing.TypeVar('Range_T_co', bound='Range', covariant=True)\n\n\nclass Range(\n    Collection,\n    qlkind=qltypes.SchemaObjectClass.RANGE_TYPE,\n    schema_name='range',\n):\n\n    element_type = so.SchemaField(\n        Type,\n        # We want a low compcoef so that range types are *never* altered.\n        compcoef=0,\n    )\n\n    @classmethod\n    def generate_name(\n        cls,\n        element_name: s_name.Name,\n    ) -> s_name.UnqualName:\n        return s_name.UnqualName(\n            f'range<{s_name.mangle_name(str(element_name))}>',\n        )\n\n    @classmethod\n    def create(\n        cls: type[Range_T],\n        schema: s_schema.Schema,\n        *,\n        name: Optional[s_name.Name] = None,\n        id: Optional[uuid.UUID] = None,\n        element_type: Any,\n        **kwargs: Any,\n    ) -> tuple[s_schema.Schema, Range_T]:\n        if name is None:\n            name = cls.generate_name(element_type.get_name(schema))\n\n        if isinstance(name, s_name.QualName):\n            result = schema.get(name, type=cls, default=None)\n        else:\n            result = schema.get_global(cls, name, default=None)\n\n        if result is None:\n            schema, result = super().create_in_schema(\n                schema,\n                id=id,\n                name=name,\n                element_type=element_type,\n                **kwargs,\n            )\n\n        return schema, result\n\n    def get_generated_name(self, schema: s_schema.Schema) -> s_name.UnqualName:\n        return type(self).generate_name(\n            self.get_element_type(schema).get_name(schema),\n        )\n\n    def get_displayname(self, schema: s_schema.Schema) -> str:\n        return (\n            f'range<{self.get_element_type(schema).get_displayname(schema)}>')\n\n    def is_range(self) -> bool:\n        return True\n\n    def derive_subtype(\n        self,\n        schema: s_schema.Schema,\n        *,\n        name: s_name.QualName,\n        attrs: Optional[Mapping[str, Any]] = None,\n        **kwargs: Any,\n    ) -> tuple[s_schema.Schema, RangeExprAlias]:\n        assert not kwargs\n        return RangeExprAlias.from_subtypes(\n            schema,\n            [self.get_element_type(schema)],\n            self.get_typemods(schema),\n            name=name,\n            **(attrs or {}),\n        )\n\n    def get_subtypes(self, schema: s_schema.Schema) -> tuple[Type, ...]:\n        return (self.get_element_type(schema),)\n\n    def implicitly_castable_to(\n        self, other: Type, schema: s_schema.Schema\n    ) -> bool:\n        if not isinstance(other, (Range, MultiRange)):\n            return False\n\n        my_el = self.get_element_type(schema)\n        other_el = other.get_element_type(schema)\n\n        if isinstance(other, MultiRange):\n            # Only valid implicit cast to multirange is the one that preserves\n            # the element type.\n            return my_el.issubclass(schema, other_el)\n\n        return my_el.implicitly_castable_to(other_el, schema)\n\n    def get_implicit_cast_distance(\n        self, other: Type, schema: s_schema.Schema\n    ) -> int:\n        if not isinstance(other, (Range, MultiRange)):\n            return -1\n\n        extra = 1 if isinstance(other, MultiRange) else 0\n        return self.get_element_type(schema).get_implicit_cast_distance(\n            other.get_element_type(schema), schema) + extra\n\n    def assignment_castable_to(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> bool:\n        if not isinstance(other, (Range, MultiRange)):\n            return False\n\n        return self.get_element_type(schema).assignment_castable_to(\n            other.get_element_type(schema), schema)\n\n    def castable_to(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> bool:\n        if not isinstance(other, (Range, MultiRange)):\n            return False\n\n        return self.get_element_type(schema).castable_to(\n            other.get_element_type(schema), schema)\n\n    def find_common_implicitly_castable_type(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> tuple[s_schema.Schema, Optional[RangeLike]]:\n\n        if not isinstance(other, (Range, MultiRange)):\n            return schema, None\n\n        if self == other:\n            return schema, self\n\n        my_el = self.get_element_type(schema)\n        other_el = other.get_element_type(schema)\n\n        if (\n            isinstance(other, MultiRange)\n            and not my_el.issubclass(schema, other_el)\n        ):\n            # Only valid implicit cast to multirange is the one that preserves\n            # the element type.\n            return schema, None\n\n        schema, subtype = my_el.find_common_implicitly_castable_type(\n            other_el, schema)\n\n        if subtype is None:\n            return schema, None\n\n        # Implicitly castable target is based on the `other` subtype because\n        # it may be Range or MultiRange. We also need to account for\n        # CollectionExprAlias.\n        if isinstance(other, CollectionExprAlias):\n            other_t = other.get_underlying_schema_class()\n            # Keeps mypy happy, even though these have to be exactly one of\n            # those two types and not merely subclasses.\n            assert issubclass(other_t, (Range, MultiRange))\n        else:\n            other_t = type(other)\n\n        # mypy is not happy even if I try issubclass or a cast for the result\n        # of get_underlying_schema_class, so I'm casting the return here\n        # return typing.cast(typing.Tuple[s_schema.Schema, RangeLike],\n        #                    other_t.from_subtypes(schema, [subtype]))\n        return other_t.from_subtypes(schema, [subtype])\n\n    def _resolve_polymorphic(\n        self,\n        schema: s_schema.Schema,\n        concrete_type: Type,\n    ) -> Optional[Type]:\n        if not isinstance(concrete_type, Range):\n            return None\n\n        return self.get_element_type(schema).resolve_polymorphic(\n            schema, concrete_type.get_element_type(schema))\n\n    def _to_nonpolymorphic(\n        self,\n        schema: s_schema.Schema,\n        concrete_type: Type,\n    ) -> tuple[s_schema.Schema, Range]:\n        return Range.from_subtypes(schema, (concrete_type,))\n\n    def _test_polymorphic(self, schema: s_schema.Schema, other: Type) -> bool:\n        if other.is_any(schema):\n            return True\n\n        if not isinstance(other, (Range, MultiRange)):\n            return False\n\n        return self.get_element_type(schema).test_polymorphic(\n            schema, other.get_element_type(schema))\n\n    @classmethod\n    def from_subtypes(\n        cls: type[Range_T],\n        schema: s_schema.Schema,\n        subtypes: Sequence[Type],\n        typemods: Any = None,\n        *,\n        name: Optional[s_name.QualName] = None,\n        **kwargs: Any,\n    ) -> tuple[s_schema.Schema, Range_T]:\n        if len(subtypes) != 1:\n            raise errors.SchemaError(\n                f'unexpected number of subtypes, expecting 1: {subtypes!r}')\n        stype = subtypes[0]\n        anypoint = schema.get('std::anypoint', type=Type)\n\n        if not stype.issubclass(schema, anypoint):\n            raise errors.UnsupportedFeatureError(\n                f'unsupported range subtype: {stype.get_displayname(schema)}'\n            )\n\n        return cls.create(\n            schema,\n            element_type=stype,\n            name=name,\n            **kwargs,\n        )\n\n    @classmethod\n    def create_shell(\n        cls: type[Range_T],\n        schema: s_schema.Schema,\n        *,\n        subtypes: Sequence[TypeShell[Type]],\n        typemods: Any = None,\n        name: Optional[s_name.Name] = None,\n    ) -> RangeTypeShell[Range_T]:\n        st = next(iter(subtypes))\n\n        return RangeTypeShell(\n            subtype=st,\n            typemods=typemods,\n            name=name,\n            schemaclass=cls,\n        )\n\n    def as_shell(\n        self: Self,\n        schema: s_schema.Schema,\n    ) -> RangeTypeShell[Self]:\n        return type(self).create_shell(\n            schema,\n            subtypes=[st.as_shell(schema) for st in self.get_subtypes(schema)],\n            typemods=self.get_typemods(schema),\n            name=self.get_name(schema),\n        )\n\n    def material_type(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[s_schema.Schema, Range]:\n        # We need to resolve material types based on the subtype recursively.\n\n        st = self.get_element_type(schema)\n        schema, stm = st.material_type(schema)\n        if stm != st or isinstance(self, RangeExprAlias):\n            return Range.from_subtypes(\n                schema,\n                [stm],\n                typemods=self.get_typemods(schema),\n            )\n        else:\n            return (schema, self)\n\n\nclass RangeTypeShell(CollectionTypeShell[Range_T_co]):\n\n    schemaclass: type[Range_T_co]\n\n    def __init__(\n        self,\n        *,\n        name: Optional[s_name.Name],\n        subtype: TypeShell[Type],\n        typemods: tuple[typing.Any, ...],\n        schemaclass: type[Range_T_co],\n    ) -> None:\n        if name is None:\n            name = schemaclass.generate_name(subtype.name)\n\n        super().__init__(name=name, schemaclass=schemaclass)\n        self.subtype = subtype\n        self.typemods = typemods\n\n    def get_subtypes(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[TypeShell[Type], ...]:\n        return (self.subtype,)\n\n    def get_displayname(self, schema: s_schema.Schema) -> str:\n        return f'range<{self.subtype.get_displayname(schema)}>'\n\n    def as_create_delta(\n        self,\n        schema: s_schema.Schema,\n        *,\n        view_name: Optional[s_name.QualName] = None,\n        attrs: Optional[dict[str, Any]] = None,\n    ) -> sd.CommandGroup:\n        ca: CreateRange | CreateRangeExprAlias\n        cmd = sd.CommandGroup()\n        if view_name is None:\n            ca = CreateRange(\n                classname=self.get_name(schema),\n                if_not_exists=True,\n            )\n        else:\n            ca = CreateRangeExprAlias(\n                classname=view_name,\n            )\n\n        el = self.subtype\n        if isinstance(el, CollectionTypeShell):\n            cmd.add(el.as_create_delta(schema))\n\n        ca.set_attribute_value('name', ca.classname)\n        ca.set_attribute_value('element_type', el)\n        ca.set_attribute_value('is_persistent', True)\n        ca.set_attribute_value('abstract', self.is_polymorphic(schema))\n\n        if attrs:\n            for k, v in attrs.items():\n                ca.set_attribute_value(k, v)\n\n        cmd.add(ca)\n\n        return cmd\n\n\nclass RangeExprAlias(\n    CollectionExprAlias,\n    Range,\n    qlkind=qltypes.SchemaObjectClass.ALIAS,\n):\n    # N.B: Don't add any SchemaFields to this class, they won't be\n    # reflected properly (since this inherits from the concrete Range).\n\n    @classmethod\n    def get_underlying_schema_class(cls) -> type[Collection]:\n        return Range\n\n\nMultiRange_T = typing.TypeVar('MultiRange_T', bound='MultiRange')\nMultiRange_T_co = typing.TypeVar(\n    'MultiRange_T_co', bound='MultiRange', covariant=True)\n\n\nclass MultiRange(\n    Collection,\n    qlkind=qltypes.SchemaObjectClass.MULTIRANGE_TYPE,\n    schema_name='multirange',\n):\n\n    element_type = so.SchemaField(\n        Type,\n        # We want a low compcoef so that multirange types are *never* altered.\n        compcoef=0,\n    )\n\n    @classmethod\n    def generate_name(\n        cls,\n        element_name: s_name.Name,\n    ) -> s_name.UnqualName:\n        return s_name.UnqualName(\n            f'multirange<{s_name.mangle_name(str(element_name))}>',\n        )\n\n    @classmethod\n    def create(\n        cls: type[MultiRange_T],\n        schema: s_schema.Schema,\n        *,\n        name: Optional[s_name.Name] = None,\n        id: Optional[uuid.UUID] = None,\n        element_type: Any,\n        **kwargs: Any,\n    ) -> tuple[s_schema.Schema, MultiRange_T]:\n        if name is None:\n            name = cls.generate_name(element_type.get_name(schema))\n\n        if isinstance(name, s_name.QualName):\n            result = schema.get(name, type=cls, default=None)\n        else:\n            result = schema.get_global(cls, name, default=None)\n\n        if result is None:\n            schema, result = super().create_in_schema(\n                schema,\n                id=id,\n                name=name,\n                element_type=element_type,\n                **kwargs,\n            )\n\n        return schema, result\n\n    def get_generated_name(self, schema: s_schema.Schema) -> s_name.UnqualName:\n        return type(self).generate_name(\n            self.get_element_type(schema).get_name(schema),\n        )\n\n    def get_displayname(self, schema: s_schema.Schema) -> str:\n        return f'''multirange<{self.get_element_type(schema)\n                                   .get_displayname(schema)}>'''\n\n    def is_multirange(self) -> bool:\n        return True\n\n    def derive_subtype(\n        self,\n        schema: s_schema.Schema,\n        *,\n        name: s_name.QualName,\n        attrs: Optional[Mapping[str, Any]] = None,\n        **kwargs: Any,\n    ) -> tuple[s_schema.Schema, MultiRangeExprAlias]:\n        assert not kwargs\n        return MultiRangeExprAlias.from_subtypes(\n            schema,\n            [self.get_element_type(schema)],\n            self.get_typemods(schema),\n            name=name,\n            **(attrs or {}),\n        )\n\n    def get_subtypes(self, schema: s_schema.Schema) -> tuple[Type, ...]:\n        return (self.get_element_type(schema),)\n\n    def implicitly_castable_to(\n        self, other: Type, schema: s_schema.Schema\n    ) -> bool:\n        if not isinstance(other, MultiRange):\n            return False\n\n        return self.get_element_type(schema).implicitly_castable_to(\n            other.get_element_type(schema), schema)\n\n    def get_implicit_cast_distance(\n        self, other: Type, schema: s_schema.Schema\n    ) -> int:\n        if not isinstance(other, MultiRange):\n            return -1\n\n        return self.get_element_type(schema).get_implicit_cast_distance(\n            other.get_element_type(schema), schema)\n\n    def assignment_castable_to(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> bool:\n        if not isinstance(other, MultiRange):\n            return False\n\n        return self.get_element_type(schema).assignment_castable_to(\n            other.get_element_type(schema), schema)\n\n    def castable_to(\n        self,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> bool:\n        if not isinstance(other, MultiRange):\n            return False\n\n        return self.get_element_type(schema).castable_to(\n            other.get_element_type(schema), schema)\n\n    def find_common_implicitly_castable_type(\n        self: MultiRange,\n        other: Type,\n        schema: s_schema.Schema,\n    ) -> tuple[s_schema.Schema, Optional[MultiRange]]:\n\n        if not isinstance(other, MultiRange):\n            return schema, None\n\n        if self == other:\n            return schema, self\n\n        my_el = self.get_element_type(schema)\n        schema, subtype = my_el.find_common_implicitly_castable_type(\n            other.get_element_type(schema), schema)\n\n        if subtype is None:\n            return schema, None\n\n        return MultiRange.from_subtypes(schema, [subtype])\n\n    def _resolve_polymorphic(\n        self,\n        schema: s_schema.Schema,\n        concrete_type: Type,\n    ) -> Optional[Type]:\n        # polymorphic multiranges can resolve using concrete multiranges and\n        # ranges because ranges are implicitly castable into multiranges.\n        if not isinstance(concrete_type, (Range, MultiRange)):\n            return None\n\n        return self.get_element_type(schema).resolve_polymorphic(\n            schema, concrete_type.get_element_type(schema))\n\n    def _to_nonpolymorphic(\n        self,\n        schema: s_schema.Schema,\n        concrete_type: Type,\n    ) -> tuple[s_schema.Schema, MultiRange]:\n        return MultiRange.from_subtypes(schema, (concrete_type,))\n\n    def _test_polymorphic(self, schema: s_schema.Schema, other: Type) -> bool:\n        if other.is_any(schema):\n            return True\n\n        if not isinstance(other, MultiRange):\n            return False\n\n        return self.get_element_type(schema).test_polymorphic(\n            schema, other.get_element_type(schema))\n\n    @classmethod\n    def from_subtypes(\n        cls: type[MultiRange_T],\n        schema: s_schema.Schema,\n        subtypes: Sequence[Type],\n        typemods: Any = None,\n        *,\n        name: Optional[s_name.QualName] = None,\n        **kwargs: Any,\n    ) -> tuple[s_schema.Schema, MultiRange_T]:\n        if len(subtypes) != 1:\n            raise errors.SchemaError(\n                f'unexpected number of subtypes, expecting 1: {subtypes!r}')\n        stype = subtypes[0]\n        anypoint = schema.get('std::anypoint', type=Type)\n\n        if not stype.issubclass(schema, anypoint):\n            raise errors.UnsupportedFeatureError(\n                f'unsupported range subtype: {stype.get_displayname(schema)}'\n            )\n\n        return cls.create(\n            schema,\n            element_type=stype,\n            name=name,\n            **kwargs,\n        )\n\n    @classmethod\n    def create_shell(\n        cls: type[MultiRange_T],\n        schema: s_schema.Schema,\n        *,\n        subtypes: Sequence[TypeShell[Type]],\n        typemods: Any = None,\n        name: Optional[s_name.Name] = None,\n    ) -> MultiRangeTypeShell[MultiRange_T]:\n        st = next(iter(subtypes))\n\n        if name is None:\n            name = cls.generate_name(\n                st.get_name(schema),\n            )\n\n        return MultiRangeTypeShell(\n            subtype=st,\n            typemods=typemods,\n            name=name,\n            schemaclass=cls,\n        )\n\n    def as_shell(\n        self: Self,\n        schema: s_schema.Schema,\n    ) -> MultiRangeTypeShell[Self]:\n        return type(self).create_shell(\n            schema,\n            subtypes=[st.as_shell(schema) for st in self.get_subtypes(schema)],\n            typemods=self.get_typemods(schema),\n            name=self.get_name(schema),\n        )\n\n    def material_type(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[s_schema.Schema, MultiRange]:\n        # We need to resolve material types based on the subtype recursively.\n\n        st = self.get_element_type(schema)\n        schema, stm = st.material_type(schema)\n        if stm != st or isinstance(self, MultiRangeExprAlias):\n            return MultiRange.from_subtypes(\n                schema,\n                [stm],\n                typemods=self.get_typemods(schema),\n            )\n        else:\n            return (schema, self)\n\n\nclass MultiRangeTypeShell(CollectionTypeShell[MultiRange_T_co]):\n\n    schemaclass: type[MultiRange_T_co]\n\n    def __init__(\n        self,\n        *,\n        name: s_name.Name,\n        subtype: TypeShell[Type],\n        typemods: tuple[typing.Any, ...],\n        schemaclass: type[MultiRange_T_co],\n    ) -> None:\n        super().__init__(name=name, schemaclass=schemaclass)\n        self.subtype = subtype\n        self.typemods = typemods\n\n    def get_subtypes(\n        self,\n        schema: s_schema.Schema,\n    ) -> tuple[TypeShell[Type], ...]:\n        return (self.subtype,)\n\n    def get_displayname(self, schema: s_schema.Schema) -> str:\n        return f'multirange<{self.subtype.get_displayname(schema)}>'\n\n    def as_create_delta(\n        self,\n        schema: s_schema.Schema,\n        *,\n        view_name: Optional[s_name.QualName] = None,\n        attrs: Optional[dict[str, Any]] = None,\n    ) -> sd.CommandGroup:\n        ca: CreateMultiRange | CreateMultiRangeExprAlias\n        cmd = sd.CommandGroup()\n        if view_name is None:\n            ca = CreateMultiRange(\n                classname=self.get_name(schema),\n                if_not_exists=True,\n            )\n        else:\n            ca = CreateMultiRangeExprAlias(\n                classname=view_name,\n            )\n\n        el = self.subtype\n        if isinstance(el, CollectionTypeShell):\n            cmd.add(el.as_create_delta(schema))\n\n        ca.set_attribute_value('name', ca.classname)\n        ca.set_attribute_value('element_type', el)\n        ca.set_attribute_value('is_persistent', True)\n        ca.set_attribute_value('abstract', self.is_polymorphic(schema))\n\n        if attrs:\n            for k, v in attrs.items():\n                ca.set_attribute_value(k, v)\n\n        cmd.add(ca)\n\n        return cmd\n\n\nclass MultiRangeExprAlias(\n    CollectionExprAlias,\n    MultiRange,\n    qlkind=qltypes.SchemaObjectClass.ALIAS,\n):\n    # N.B: Don't add any SchemaFields to this class, they won't be\n    # reflected properly (since this inherits from the concrete MultiRange).\n\n    @classmethod\n    def get_underlying_schema_class(cls) -> type[Collection]:\n        return MultiRange\n\n\nRangeLike = Range | MultiRange\n\n\ndef get_union_type_name(\n    component_names: typing.Iterable[s_name.Name],\n    *,\n    opaque: bool = False,\n    module: typing.Optional[str] = None,\n) -> s_name.QualName:\n    sorted_name_list = sorted(\n        str(name).replace('::', ':') for name in component_names)\n    if opaque:\n        nqname = f\"(opaque: {' | '.join(sorted_name_list)})\"\n    else:\n        nqname = f\"({' | '.join(sorted_name_list)})\"\n    return s_name.QualName(name=nqname, module=module or '__derived__')\n\n\ndef get_intersection_type_name(\n    component_names: typing.Iterable[s_name.Name],\n    *,\n    module: typing.Optional[str] = None,\n) -> s_name.QualName:\n    sorted_name_list = sorted(\n        str(name).replace('::', ':') for name in component_names)\n    nqname = f\"({' & '.join(sorted_name_list)})\"\n    return s_name.QualName(name=nqname, module=module or '__derived__')\n\n\ndef ensure_schema_type_expr_type(\n    schema: s_schema.Schema,\n    type_shell: TypeExprShell[Type],\n    parent_cmd: sd.Command,\n    *,\n    span: typing.Optional[parsing.Span] = None,\n    context: sd.CommandContext,\n) -> Optional[sd.Command]:\n\n    name = type_shell.get_name(schema)\n    texpr_type = schema.get(name, default=None, type=Type)\n    cmd = None\n    if texpr_type is None:\n        cmd = type_shell.as_create_delta(schema)\n        if cmd is not None:\n            parent_cmd.add_prerequisite(cmd)\n\n    return cmd\n\n\ndef type_dummy_expr(\n    typ: Type,\n    schema: s_schema.Schema,\n) -> Optional[s_expr.Expression]:\n    if isinstance(typ, so.DerivableInheritingObject):\n        typ = typ.get_nearest_non_derived_parent(schema)\n\n    q = qlast.FunctionCall(\n        func=('__std__', 'assert_exists'),\n        args=[\n            qlast.TypeCast(\n                type=utils.typeref_to_ast(schema, typ),\n                expr=qlast.Set(elements=[]),\n            )\n        ],\n    )\n    return s_expr.Expression.from_ast(q, schema)\n\n\nclass TypeCommand[TypeT: Type](sd.ObjectCommand[TypeT]):\n\n    @classmethod\n    def _get_alias_expr(cls, astnode: qlast.CreateAlias) -> qlast.Expr:\n        expr = qlast.get_ddl_field_value(astnode, 'expr')\n        if expr is None:\n            raise errors.InvalidAliasDefinitionError(\n                f'missing required view expression',\n                span=astnode.span\n            )\n        assert isinstance(expr, qlast.Expr)\n        return expr\n\n    def get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        if self.get_attribute_value('expr'):\n            return None\n        elif (\n            (union_of := self.get_attribute_value('union_of')) is not None\n            and union_of.items\n        ):\n            return None\n        elif (\n            (int_of := self.get_attribute_value('intersection_of')) is not None\n            and int_of.items\n        ):\n            return None\n        else:\n            return super().get_ast(schema, context, parent_node=parent_node)\n\n    def compile_expr_field(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: s_expr.Expression,\n        track_schema_ref_exprs: bool=False,\n    ) -> s_expr.CompiledExpression:\n        # XXX: This seems like pointless duplication of work from\n        # globals/aliases... why is expr even here?\n        # (... because we export it in the introspection schema)\n        assert field.name == 'expr'\n        return value.compiled(\n            schema=schema,\n            options=qlcompiler.CompilerOptions(\n                schema_object_context=self.get_schema_metaclass(),\n                modaliases=context.modaliases,\n                in_ddl_context_name='type definition',\n                track_schema_ref_exprs=track_schema_ref_exprs,\n            ),\n            context=context,\n        )\n\n    def get_dummy_expr_field_value(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        field: so.Field[Any],\n        value: Any,\n    ) -> Optional[s_expr.Expression]:\n        if field.name == 'expr':\n            return type_dummy_expr(self.scls, schema)\n        else:\n            raise NotImplementedError(f'unhandled field {field.name!r}')\n\n    def _create_begin(\n        self, schema: s_schema.Schema, context: sd.CommandContext\n    ) -> s_schema.Schema:\n        schema = super()._create_begin(schema, context)\n        assert isinstance(self.scls, Type)\n        if not self.scls.is_view(schema):\n            delta_root = context.top().op\n            assert isinstance(delta_root, sd.DeltaRoot)\n            delta_root.new_types.add(self.scls.id)\n        return schema\n\n\nclass InheritingTypeCommand(\n    sd.QualifiedObjectCommand[InheritingTypeT],\n    TypeCommand[InheritingTypeT],\n    inheriting.InheritingObjectCommand[InheritingTypeT],\n):\n    def _validate_bases(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        bases: so.ObjectList[InheritingTypeT],\n        shells: Mapping[s_name.QualName, TypeShell[InheritingTypeT]],\n        is_derived: bool,\n    ) -> None:\n        for base in bases.objects(schema):\n            if base.find_generic(schema) is not None or (\n                base.is_free_object_type(schema) and not is_derived\n            ):\n                base_type_name = base.get_displayname(schema)\n                shell = shells.get(base.get_name(schema))\n                raise errors.SchemaError(\n                    f\"{base_type_name!r} cannot be a parent type\",\n                    span=shell.span if shell is not None else None,\n                )\n\n\nclass CreateInheritingType(\n    InheritingTypeCommand[InheritingTypeT],\n    inheriting.CreateInheritingObject[InheritingTypeT],\n):\n    def validate_create(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        super().validate_create(schema, context)\n\n        shells = self.get_attribute_value('bases')\n        if isinstance(shells, so.ObjectList):\n            # XXX: fix set_attribute_value shell hygiene\n            shells = shells.as_shell(schema)\n        shell_map = {s.get_name(schema): s for s in shells}\n        bases = self.get_resolved_attribute_value(\n            'bases',\n            schema=schema,\n            context=context,\n        )\n        self._validate_bases(\n            schema,\n            context,\n            bases,\n            shell_map,\n            is_derived=self.get_attribute_value('is_derived') or False,\n        )\n\n\nclass RebaseInheritingType(\n    InheritingTypeCommand[InheritingTypeT],\n    inheriting.RebaseInheritingObject[InheritingTypeT],\n):\n    def validate_alter(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> None:\n        super().validate_alter(schema, context)\n        shell_map = {}\n        for base_shells, _ in self.added_bases:\n            shell_map.update({s.get_name(schema): s for s in base_shells})\n        bases = self.get_resolved_attribute_value(\n            'bases',\n            schema=schema,\n            context=context,\n        )\n        self._validate_bases(\n            schema,\n            context,\n            bases,\n            shell_map,\n            is_derived=self.scls.get_is_derived(schema),\n        )\n\n\nclass CollectionTypeCommandContext(sd.ObjectCommandContext[Collection]):\n    pass\n\n\nclass CollectionTypeCommand(TypeCommand[CollectionTypeT],\n                            context_class=CollectionTypeCommandContext):\n\n    def get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        # CollectionTypeCommand cannot have its own AST because it is a\n        # side-effect of some other command.\n        return None\n\n\nclass CollectionExprAliasCommand(\n    sd.QualifiedObjectCommand[CollectionExprAliasT],\n    TypeCommand[CollectionExprAliasT],\n    context_class=CollectionTypeCommandContext,\n):\n\n    def get_ast(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        *,\n        parent_node: Optional[qlast.DDLOperation] = None,\n    ) -> Optional[qlast.DDLOperation]:\n        # CollectionTypeCommand cannot have its own AST because it is a\n        # side-effect of some other command.\n        return None\n\n\nclass CreateCollectionType(\n    CollectionTypeCommand[CollectionTypeT],\n    sd.CreateObject[CollectionTypeT],\n):\n    def canonicalize_attributes(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        # Even if we create a collection while setting up internal\n        # things, we don't mark it internal, since something visible\n        # might use it later.\n        self.set_attribute_value('internal', False)\n        return super().canonicalize_attributes(schema, context)\n\n    def validate_object(\n        self, schema: s_schema.Schema, context: sd.CommandContext\n    ) -> None:\n        super().validate_object(schema, context)\n\n        if isinstance(self.scls, (Range, MultiRange)):\n            from . import scalars as s_scalars\n            from edb.pgsql import types as pgtypes\n\n            st = self.scls.get_subtypes(schema)[0]\n\n            # general rule of what's supported\n            supported = (\n                isinstance(st, s_scalars.ScalarType) and st.is_base_type(schema)\n            )\n\n            if supported:\n                # actually test that it's supported\n                try:\n                    pgtypes.pg_type_from_object(schema, self.scls)\n                except Exception:\n                    supported = False\n\n            if not supported:\n                raise errors.UnsupportedFeatureError(\n                    f'unsupported range subtype: {st.get_displayname(schema)}'\n                )\n\n\nclass AlterCollectionType(\n    CollectionTypeCommand[CollectionTypeT],\n    AlterType[CollectionTypeT],\n    sd.AlterObject[CollectionTypeT],\n):\n    pass\n\n\nclass RenameCollectionType(\n    CollectionTypeCommand[CollectionTypeT],\n    RenameType[CollectionTypeT],\n):\n    pass\n\n\nclass DeleteCollectionType(\n    CollectionTypeCommand[CollectionTypeT],\n    sd.DeleteObject[CollectionTypeT],\n):\n    def _delete_begin(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        schema = super()._delete_begin(schema, context)\n        if not context.canonical:\n            for el in self.scls.get_subtypes(schema):\n                if op := el.as_type_delete_if_unused(schema):\n                    self.add_caused(op)\n        return schema\n\n\nclass CreateCollectionExprAlias(\n    CollectionExprAliasCommand[CollectionExprAliasT],\n    sd.CreateObject[CollectionExprAliasT],\n):\n    pass\n\n\nclass DeleteCollectionExprAlias(\n    CollectionExprAliasCommand[CollectionExprAliasT],\n    DeleteCollectionType[CollectionExprAliasT],\n):\n\n    def _canonicalize(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        scls: CollectionExprAliasT,\n    ) -> list[sd.Command]:\n        ops = super()._canonicalize(schema, context, scls)\n        ops.append(scls.as_underlying_type_delete_if_unused(schema))\n        return ops\n\n\nclass CreateTuple(CreateCollectionType[Tuple]):\n    pass\n\n\nclass AlterTuple(AlterCollectionType[Tuple]):\n    pass\n\n\nclass RenameTuple(RenameCollectionType[Tuple]):\n    pass\n\n\nclass CreateTupleExprAlias(CreateCollectionExprAlias[TupleExprAlias]):\n    def _get_ast_node(\n        self, schema: s_schema.Schema, context: sd.CommandContext\n    ) -> type[qlast.CreateAlias]:\n        # Can't just use class-level astnode because that creates a\n        # duplicate in ast -> command mapping.\n        return qlast.CreateAlias\n\n\nclass RenameTupleExprAlias(\n    CollectionExprAliasCommand[TupleExprAlias],\n    sd.RenameObject[TupleExprAlias],\n):\n    pass\n\n\nclass AlterTupleExprAlias(\n    CollectionExprAliasCommand[TupleExprAlias],\n    sd.AlterObject[TupleExprAlias],\n):\n    pass\n\n\nclass CreateArray(CreateCollectionType[Array]):\n    pass\n\n\nclass AlterArray(AlterCollectionType[Array]):\n    pass\n\n\nclass RenameArray(RenameCollectionType[Array]):\n    pass\n\n\nclass CreateArrayExprAlias(CreateCollectionExprAlias[ArrayExprAlias]):\n    def _get_ast_node(\n        self, schema: s_schema.Schema, context: sd.CommandContext\n    ) -> type[qlast.CreateAlias]:\n        # Can't just use class-level astnode because that creates a\n        # duplicate in ast -> command mapping.\n        return qlast.CreateAlias\n\n\nclass RenameArrayExprAlias(\n    CollectionExprAliasCommand[ArrayExprAlias],\n    sd.RenameObject[ArrayExprAlias],\n):\n    pass\n\n\nclass AlterArrayExprAlias(\n    CollectionExprAliasCommand[ArrayExprAlias],\n    sd.AlterObject[ArrayExprAlias],\n):\n    pass\n\n\nclass CreateRange(CreateCollectionType[Range]):\n    pass\n\n\nclass AlterRange(AlterCollectionType[Range]):\n    pass\n\n\nclass RenameRange(RenameCollectionType[Range]):\n    pass\n\n\nclass CreateRangeExprAlias(CreateCollectionExprAlias[RangeExprAlias]):\n    def _get_ast_node(\n        self, schema: s_schema.Schema, context: sd.CommandContext\n    ) -> type[qlast.CreateAlias]:\n        # Can't just use class-level astnode because that creates a\n        # duplicate in ast -> command mapping.\n        return qlast.CreateAlias\n\n\nclass RenameRangeExprAlias(\n    CollectionExprAliasCommand[RangeExprAlias],\n    sd.RenameObject[RangeExprAlias],\n):\n    pass\n\n\nclass AlterRangeExprAlias(\n    CollectionExprAliasCommand[RangeExprAlias],\n    sd.AlterObject[RangeExprAlias],\n):\n    pass\n\n\nclass CreateMultiRange(CreateCollectionType[MultiRange]):\n    pass\n\n\nclass AlterMultiRange(AlterCollectionType[MultiRange]):\n    pass\n\n\nclass RenameMultiRange(RenameCollectionType[MultiRange]):\n    pass\n\n\nclass CreateMultiRangeExprAlias(CreateCollectionExprAlias[MultiRangeExprAlias]):\n    def _get_ast_node(\n        self, schema: s_schema.Schema, context: sd.CommandContext\n    ) -> type[qlast.CreateAlias]:\n        # Can't just use class-level astnode because that creates a\n        # duplicate in ast -> command mapping.\n        return qlast.CreateAlias\n\n\nclass RenameMultiRangeExprAlias(\n    CollectionExprAliasCommand[MultiRangeExprAlias],\n    sd.RenameObject[MultiRangeExprAlias],\n):\n    pass\n\n\nclass AlterMultiRangeExprAlias(\n    CollectionExprAliasCommand[MultiRangeExprAlias],\n    sd.AlterObject[MultiRangeExprAlias],\n):\n    pass\n\n\nclass DeleteTuple(DeleteCollectionType[Tuple]):\n    pass\n\n\nclass DeleteTupleExprAlias(DeleteCollectionExprAlias[TupleExprAlias]):\n    pass\n\n\nclass DeleteArray(DeleteCollectionType[Array]):\n    # Prevent array types from getting deleted unless the element\n    # type is being deleted too.\n    def _has_outside_references(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> bool:\n        if super()._has_outside_references(schema, context):\n            return True\n\n        el_type = self.scls.get_element_type(schema)\n        if el_type.is_scalar() and not context.is_deleting(el_type):\n            return True\n\n        return False\n\n\nclass DeleteArrayExprAlias(DeleteCollectionExprAlias[ArrayExprAlias]):\n    pass\n\n\nclass DeleteRange(DeleteCollectionType[Range]):\n    pass\n\n\nclass DeleteRangeExprAlias(DeleteCollectionExprAlias[RangeExprAlias]):\n    pass\n\n\nclass DeleteMultiRange(DeleteCollectionType[MultiRange]):\n    pass\n\n\nclass DeleteMultiRangeExprAlias(DeleteCollectionExprAlias[MultiRangeExprAlias]):\n    pass\n\n\ndef materialize_type_in_attribute(\n    schema: s_schema.Schema,\n    context: sd.CommandContext,\n    cmd: sd.Command,\n    attrname: str,\n) -> s_schema.Schema:\n    assert isinstance(cmd, sd.ObjectCommand)\n\n    type_ref = cmd.get_local_attribute_value(attrname)\n    if type_ref is None:\n        return schema\n\n    span = cmd.get_attribute_span('target')\n\n    if isinstance(type_ref, TypeExprShell):\n        cc_cmd = ensure_schema_type_expr_type(\n            schema,\n            type_ref,\n            parent_cmd=cmd,\n            span=span,\n            context=context,\n        )\n        if cc_cmd is not None:\n            schema = cc_cmd.apply(schema, context)\n\n    if isinstance(type_ref, CollectionTypeShell):\n        # If the current command is a fragment, we want the collection\n        # creation to live in the parent operation, in order for the\n        # logic to skip it if the object already exists to work.\n        op = (cmd.get_parent_op(context)\n              if isinstance(cmd, sd.AlterObjectFragment) else cmd)\n\n        make_coll = type_ref.as_create_delta(schema)\n        op.add_prerequisite(make_coll)\n        schema = make_coll.apply(schema, context)\n\n    if isinstance(type_ref, TypeShell):\n        try:\n            type_ref.resolve(schema)\n        except errors.InvalidReferenceError as e:\n            refname = type_ref.get_refname(schema)\n            if refname is not None:\n                utils.enrich_schema_lookup_error(\n                    e,\n                    refname,\n                    modaliases=context.modaliases,\n                    schema=schema,\n                    item_type=Type,\n                    span=span,\n                )\n            raise\n        except errors.InvalidPropertyDefinitionError as e:\n            e.set_span(span)\n            raise\n    elif not isinstance(type_ref, Type):\n        raise AssertionError(\n            f'unexpected value in type attribute {attrname!r} of '\n            f'{cmd.get_verbosename()}: {type_ref!r}'\n        )\n\n    return schema\n\n\ndef is_type_compatible(\n    type_a: Type,\n    type_b: Type,\n    *,\n    schema: s_schema.Schema,\n) -> bool:\n    \"\"\"Check whether two types have compatible SQL representations.\n\n    EdgeQL implicit casts need to be turned into explicit casts in\n    some places, since the semantics differ from SQL's.\n    \"\"\"\n\n    schema, material_type_a = type_a.material_type(schema)\n    schema, material_type_b = type_b.material_type(schema)\n\n    def labels_compatible(t_a: Type, t_b: Type) -> bool:\n        if t_a == t_b:\n            return True\n\n        if isinstance(t_a, Tuple) and isinstance(t_b, Tuple):\n            if t_a.get_is_persistent(schema) and t_b.get_is_persistent(schema):\n                return False\n\n            # For tuples, we also (recursively) check that the element\n            # names match\n            return all(\n                name_a == name_b\n                and labels_compatible(st_a, st_b)\n                for (name_a, st_a), (name_b, st_b)\n                in zip(t_a.iter_subtypes(schema),\n                       t_b.iter_subtypes(schema))\n            )\n        elif isinstance(t_a, Array) and isinstance(t_b, Array):\n            t_as = t_a.get_element_type(schema)\n            t_bs = t_b.get_element_type(schema)\n            return (\n                not isinstance(t_as, Tuple) and labels_compatible(t_as, t_bs)\n            )\n        elif isinstance(t_a, Range) and isinstance(t_b, Range):\n            t_as = t_a.get_element_type(schema)\n            t_bs = t_b.get_element_type(schema)\n            return labels_compatible(t_as, t_bs)\n        elif isinstance(t_a, MultiRange) and isinstance(t_b, MultiRange):\n            t_as = t_a.get_element_type(schema)\n            t_bs = t_b.get_element_type(schema)\n            return labels_compatible(t_as, t_bs)\n        else:\n            return True\n\n    return (\n        material_type_b.issubclass(schema, material_type_a)\n        and labels_compatible(material_type_a, material_type_b)\n    )\n"
  },
  {
    "path": "edb/schema/unknown_pointers.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Machinery for handling pointers with an unspecified kind.\n\nMost of the DDL/delta machinery really requires that we know whether\nwe are operating on a link or a property, but our SDL syntax allows\nomitting the specifier. Because the pointer might be computed, it's\nnot possible to resolve this ahead of time, so we build just enough\nmachinery for compiling unknown pointer operations to make\nddl.apply_sdl work.\n\n\"\"\"\n\nfrom __future__ import annotations\n\n\nfrom edb.common import struct\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import parser as qlparser\n\nfrom . import delta as sd\nfrom . import objects as so\nfrom . import properties as s_props\nfrom . import pointers\nfrom . import sources as s_sources\nfrom . import schema as s_schema\n\n\nclass UnknownPointerSourceContext[Source_T: s_sources.Source](\n    s_sources.SourceCommandContext[Source_T]\n):\n    pass\n\n\nclass UnknownPointerCommand(\n    pointers.PointerCommand[pointers.Pointer],\n    context_class=pointers.PointerCommandContext,\n    referrer_context_class=UnknownPointerSourceContext,\n):\n    _schema_metaclass = pointers.Pointer\n\n    def _propagate_ref_creation(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n        referrer: so.InheritingObject,\n    ) -> None:\n        pass\n\n\nclass CreateUnknownPointer(\n    UnknownPointerCommand,\n    pointers.CreatePointer[pointers.Pointer],\n):\n    astnode = qlast.CreateConcreteUnknownPointer\n    referenced_astnode = qlast.CreateConcreteUnknownPointer\n\n    # We stash the original AST node here, so we can reuse it in apply\n    # after we've figured out the type.\n    node = struct.Field(qlast.CreateConcreteUnknownPointer, default=None)\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> sd.Command:\n        assert isinstance(astnode, qlast.CreateConcreteUnknownPointer)\n\n        # We don't need any of the subcommands in order to figure out\n        # the kind, and we avoid needing to get the contexts right if\n        # we skip them.\n        fakenode = astnode.replace(commands=[])\n        cmd = super()._cmd_tree_from_ast(schema, fakenode, context)\n        assert isinstance(cmd, CreateUnknownPointer)\n        cmd._process_create_or_alter_ast(schema, fakenode, context)\n\n        if context.modaliases:\n            astnode = astnode.replace()\n            qlparser.append_module_aliases(astnode, context.modaliases)\n\n        cmd.node = astnode\n\n        return cmd\n\n    def apply(\n        self,\n        schema: s_schema.Schema,\n        context: sd.CommandContext,\n    ) -> s_schema.Schema:\n        # We don't know what the real type of this pointer is, so this\n        # is a two step process:\n        # 1. Apply it using purely generic Pointer code. This doesn't produce\n        #    a fully legitimate result, but will resolve the target.\n        # 2. Check whether the target is an object, and construct a new\n        #    create AST node specialized to pointer or link. Then compile\n        #    that to a delta tree and apply it.\n\n        nschema = super().apply(schema, context)\n        source = self.scls.get_source(nschema)\n        target = self.scls.get_target(nschema)\n        assert source and target\n\n        astnode = self.node\n        assert astnode\n        astcls = (\n            qlast.CreateConcreteLink\n            # It's a link if the target is an object and so is the source.\n            # If the source isn't, it's a link property, which will fail.\n            if target.is_object_type()\n            and not isinstance(source, pointers.Pointer)\n            else qlast.CreateConcreteProperty\n        )\n        astnode = astnode.replace(__class__=astcls)\n\n        ncmd = sd.compile_ddl(schema, astnode, context=context)\n        assert isinstance(ncmd, pointers.CreatePointer)\n\n        rschema = ncmd.apply(schema, context)\n        return rschema\n\n\nclass AlterUnknownPointer(\n    UnknownPointerCommand,\n    pointers.AlterPointer[pointers.Pointer],\n):\n    astnode = qlast.AlterConcreteUnknownPointer\n    referenced_astnode = qlast.AlterConcreteUnknownPointer\n\n    @classmethod\n    def _cmd_tree_from_ast(\n        cls,\n        schema: s_schema.Schema,\n        astnode: qlast.DDLOperation,\n        context: sd.CommandContext,\n    ) -> pointers.AlterPointer[pointers.Pointer]:\n        # For alters that get run as part of apply_sdl, the relevant\n        # object should exist in the schema when _cmd_tree_from_ast is\n        # called, so we can resolve whether it is a link or a property\n        # right away and never need to return an AlterUnknownPointer\n        # object.\n\n        # We don't need any of the subcommands in order to figure out\n        # the kind, and we avoid needing to get the contexts right if\n        # we skip them.\n        fakenode = astnode.replace(commands=[])\n        cmd = super()._cmd_tree_from_ast(schema, fakenode, context)\n\n        obj = cmd.get_object(schema, context)\n        source = obj.get_source(schema)\n        is_prop = (\n            isinstance(obj, s_props.Property)\n            or isinstance(source, pointers.Pointer)\n        )\n\n        astcls = (\n            qlast.AlterConcreteProperty\n            if is_prop\n            else qlast.AlterConcreteLink\n        ) if isinstance(astnode, qlast.AlterObject) else (\n            qlast.CreateConcreteProperty\n            if is_prop\n            else qlast.CreateConcreteLink\n        )\n        astnode = astnode.replace(__class__=astcls)\n        assert isinstance(astnode, qlast.DDLCommand)\n        qlparser.append_module_aliases(astnode, context.modaliases)\n        res = sd.compile_ddl(schema, astnode, context=context)\n        assert isinstance(res, pointers.AlterPointer)\n        return res\n"
  },
  {
    "path": "edb/schema/utils.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n    TypeVar,\n    Iterable,\n    Mapping,\n    Sequence,\n    cast,\n    TYPE_CHECKING,\n)\n\nimport collections\nimport decimal\nimport itertools\n\nfrom edb import errors\n\nfrom edb.common import levenshtein\nfrom edb.edgeql import ast as qlast\n\nfrom edb.ir import statypes\n\nfrom . import name as sn\nfrom . import objects as so\nfrom . import expr as s_expr\n\nif TYPE_CHECKING:\n    from . import objtypes as s_objtypes\n    from . import schema as s_schema\n    from . import types as s_types\n    from edb.common import parsing\n\n    T = TypeVar('T')\n\n\ndef name_to_ast_ref(name: sn.Name) -> qlast.ObjectRef:\n    if isinstance(name, sn.QualName):\n        return qlast.ObjectRef(\n            module=name.module,\n            name=name.name,\n        )\n    else:\n        return qlast.ObjectRef(\n            name=name.name,\n        )\n\n\ndef ast_ref_to_name(ref: qlast.ObjectRef) -> sn.Name:\n    if ref.module:\n        return sn.QualName(name=ref.name, module=ref.module)\n    else:\n        return sn.UnqualName(name=ref.name)\n\n\ndef ast_ref_to_unqualname(ref: qlast.ObjectRef) -> sn.UnqualName:\n    if ref.module:\n        raise errors.InternalServerError(\n            f'unexpected fully-qualified name: {ast_ref_to_name(ref)}',\n            span=ref.span,\n        )\n    else:\n        return sn.UnqualName(name=ref.name)\n\n\ndef resolve_name(\n    lname: sn.Name,\n    *,\n    metaclass: Optional[type[so.Object]] = None,\n    span: Optional[parsing.Span] = None,\n    modaliases: Mapping[Optional[str], str],\n    schema: s_schema.Schema,\n) -> sn.Name:\n    obj = schema.get(\n        lname,\n        type=metaclass,\n        module_aliases=modaliases,\n        default=None,\n        span=span,\n    )\n    if obj is not None:\n        name = obj.get_name(schema)\n    elif isinstance(lname, sn.QualName):\n        name = sn.QualName(\n            module=modaliases.get(lname.module, lname.module),\n            name=lname.name,\n        )\n    elif metaclass is not None and issubclass(metaclass, so.QualifiedObject):\n        actual_module = modaliases.get(None)\n        if actual_module is None:\n            raise errors.InvalidReferenceError(\n                'unqualified name and no default module alias set')\n        name = sn.QualName(module=actual_module, name=lname.name)\n    else:\n        # Do not assume the name is fully-qualified unless asked\n        # explicitly.\n        name = lname\n\n    return name\n\n\ndef ast_objref_to_object_shell(\n    ref: qlast.ObjectRef,\n    *,\n    metaclass: type[so.Object_T],\n    modaliases: Mapping[Optional[str], str],\n    schema: s_schema.Schema,\n) -> so.ObjectShell[so.Object_T]:\n    lname = ast_ref_to_name(ref)\n    name = resolve_name(\n        lname,\n        metaclass=metaclass,\n        modaliases=modaliases,\n        schema=schema,\n        span=ref.span,\n    )\n\n    return so.ObjectShell(\n        name=name,\n        origname=lname,\n        schemaclass=metaclass,\n        span=ref.span,\n    )\n\n\ndef ast_objref_to_type_shell[TypeT: s_types.Type](\n    ref: qlast.ObjectRef,\n    *,\n    metaclass: type[TypeT],\n    modaliases: Mapping[Optional[str], str],\n    schema: s_schema.Schema,\n) -> s_types.TypeShell[TypeT]:\n    from . import types as s_types\n\n    if metaclass is not s_types.Type:\n        mcls = metaclass\n    else:\n        mcls = s_types.QualifiedType  # type: ignore\n\n    lname = ast_ref_to_name(ref)\n    name = resolve_name(\n        lname,\n        metaclass=mcls,\n        modaliases=modaliases,\n        schema=schema,\n        span=ref.span,\n    )\n\n    return s_types.TypeShell(\n        name=name,\n        origname=lname,\n        schemaclass=mcls,\n        span=ref.span,\n    )\n\n\ndef ast_to_type_shell(\n    node: qlast.TypeExpr,\n    *,\n    metaclass: type[s_types.TypeT_co],\n    module: Optional[str] = None,\n    modaliases: Mapping[Optional[str], str],\n    schema: s_schema.Schema,\n    allow_generalized_bases: bool = False,\n) -> s_types.TypeShell[s_types.TypeT_co]:\n\n    if isinstance(node, qlast.TypeOp):\n        return type_op_ast_to_type_shell(\n            node,\n            metaclass=metaclass,\n            module=module,\n            modaliases=modaliases,\n            schema=schema,\n        )\n\n    assert isinstance(node, qlast.TypeName)\n\n    if (node.subtypes is not None\n            and isinstance(node.maintype, qlast.ObjectRef)\n            and node.maintype.name == 'enum'):\n        from . import scalars as s_scalars\n        from edb.pgsql import common as pg_common\n\n        assert node.subtypes\n\n        elements: list[str] = []\n        element_spans: list[Optional[parsing.Span]] = []\n\n        if isinstance(node.subtypes[0], qlast.TypeExprLiteral):\n            # handling enums as literals\n            # eg. enum<'A','B','C'>\n            for subtype_expr_literal in cast(\n                list[qlast.TypeExprLiteral], node.subtypes\n            ):\n                elements.append(subtype_expr_literal.val.value)\n                element_spans.append(subtype_expr_literal.val.span)\n        else:\n            # handling enums as typenames\n            # eg. enum<A,B,C>\n            for subtype_type_name in cast(\n                list[qlast.TypeName], node.subtypes\n            ):\n                if (\n                    not isinstance(subtype_type_name, qlast.TypeName)\n                    or not isinstance(\n                        subtype_type_name.maintype, qlast.ObjectRef\n                    )\n                ):\n                    raise errors.EdgeQLSyntaxError(\n                        f'enums do not support mapped values',\n                        span=subtype_type_name.span,\n                    )\n                elements.append(subtype_type_name.maintype.name)\n                element_spans.append(subtype_type_name.maintype.span)\n\n        for element, element_span in zip(elements, element_spans):\n            if len(element) > pg_common.MAX_ENUM_LABEL_LENGTH:\n                raise errors.SchemaDefinitionError(\n                    f'enum labels cannot exceed '\n                    f'{pg_common.MAX_ENUM_LABEL_LENGTH} characters',\n                    span=element_span,\n                )\n\n        return s_scalars.AnonymousEnumTypeShell(  # type: ignore\n            elements=elements\n        )\n\n    elif node.subtypes is not None:\n        from . import types as s_types\n\n        assert isinstance(node.maintype, qlast.ObjectRef)\n        coll = None\n        try:\n            coll = s_types.Collection.get_class(node.maintype.name)\n        except errors.SchemaError:\n            if not allow_generalized_bases:\n                raise\n\n        subtypes_list: list[s_types.TypeShell[s_types.Type]] = []\n        if coll is None:\n            assert allow_generalized_bases\n            res = ast_objref_to_type_shell(\n                node.maintype,\n                modaliases=modaliases,\n                metaclass=metaclass,\n                schema=schema,\n            )\n            res.extra_args = tuple(node.subtypes)\n            return res\n\n        elif issubclass(coll, s_types.Tuple):\n            # Note: if we used abc Tuple here, then we would need anyway\n            # to assert it is an instance of s_types.Tuple to make mypy happy\n            # (rightly so, because later we use from_subtypes method)\n\n            subtypes: dict[str, s_types.TypeShell[s_types.Type]] = {}\n            # tuple declaration must either be named or unnamed, but not both\n            names = set()\n            named = None\n            unnamed = None\n            for si, st in enumerate(node.subtypes):\n                if st.name:\n                    named = True\n                    type_name = st.name\n\n                    if type_name in names:\n                        raise errors.SchemaError(\n                            f\"named tuple has duplicate field '{type_name}'\",\n                            span=st.span)\n                    names.add(type_name)\n                else:\n                    unnamed = True\n                    type_name = str(si)\n\n                if named is not None and unnamed is not None:\n                    raise errors.EdgeQLSyntaxError(\n                        f'mixing named and unnamed tuple declaration '\n                        f'is not supported',\n                        span=node.subtypes[0].span,\n                    )\n\n                subtypes[type_name] = ast_to_type_shell(\n                    cast(qlast.TypeName, st),\n                    modaliases=modaliases,\n                    metaclass=metaclass,\n                    schema=schema,\n                )\n\n            try:\n                return coll.create_shell(  # type: ignore\n                    schema,\n                    subtypes=subtypes,\n                    typemods={'named': bool(named)},\n                )\n            except errors.SchemaError as e:\n                # all errors raised inside are pertaining to subtypes, so\n                # the context should point to the first subtype\n                e.set_span(node.subtypes[0].span)\n                raise e\n\n        elif issubclass(coll, s_types.Array):\n            for st in node.subtypes:\n                subtypes_list.append(\n                    ast_to_type_shell(\n                        cast(qlast.TypeName, st),\n                        modaliases=modaliases,\n                        metaclass=metaclass,\n                        schema=schema,\n                    )\n                )\n\n            if len(subtypes_list) != 1:\n                raise errors.SchemaError(\n                    f'unexpected number of subtypes,'\n                    f' expecting 1, got {len(subtypes_list)}',\n                    span=node.span,\n                )\n\n            if isinstance(subtypes_list[0], s_types.ArrayTypeShell):\n                raise errors.UnsupportedFeatureError(\n                    'nested arrays are not supported',\n                    span=node.subtypes[0].span,\n                )\n\n            try:\n                return coll.create_shell(  # type: ignore\n                    schema,\n                    subtypes=subtypes_list,\n                )\n            except errors.SchemaError as e:\n                e.set_span(node.span)\n                raise e\n\n        elif issubclass(coll, (s_types.Range, s_types.MultiRange)):\n            for st in node.subtypes:\n                subtypes_list.append(\n                    ast_to_type_shell(\n                        cast(qlast.TypeName, st),\n                        modaliases=modaliases,\n                        metaclass=metaclass,\n                        schema=schema,\n                    )\n                )\n\n            if len(subtypes_list) != 1:\n                raise errors.SchemaError(\n                    f'unexpected number of subtypes,'\n                    f' expecting 1, got {len(subtypes_list)}',\n                    span=node.span,\n                )\n\n            # FIXME: need to check that subtypes are only anypoint\n\n            try:\n                return coll.create_shell(  # type: ignore\n                    schema,\n                    subtypes=subtypes_list,\n                )\n            except errors.SchemaError as e:\n                e.set_span(node.span)\n                raise e\n\n    elif isinstance(node.maintype, qlast.PseudoObjectRef):\n        from . import pseudo as s_pseudo\n        return s_pseudo.PseudoTypeShell(\n            name=sn.UnqualName(node.maintype.name),\n            span=node.maintype.span,\n        )  # type: ignore\n\n    assert isinstance(node.maintype, qlast.ObjectRef)\n\n    return ast_objref_to_type_shell(\n        node.maintype,\n        modaliases=modaliases,\n        metaclass=metaclass,\n        schema=schema,\n    )\n\n\ndef type_op_ast_to_type_shell[TypeT: s_types.Type](\n    node: qlast.TypeOp,\n    *,\n    metaclass: type[TypeT],\n    module: Optional[str] = None,\n    modaliases: Mapping[Optional[str], str],\n    schema: s_schema.Schema,\n) -> s_types.TypeExprShell[TypeT]:\n\n    from . import types as s_types\n\n    if node.op not in [qlast.TypeOpName.OR, qlast.TypeOpName.AND]:\n        raise errors.UnsupportedFeatureError(\n            f'unsupported type expression operator: {node.op}',\n            span=node.span,\n        )\n\n    if module is None:\n        module = modaliases.get(None)\n\n    if module is None:\n        raise errors.InternalServerError(\n            'cannot determine module for derived compound type',\n            span=node.span,\n        )\n\n    left = ast_to_type_shell(\n        node.left,\n        metaclass=metaclass,\n        module=module,\n        modaliases=modaliases,\n        schema=schema,\n    )\n    right = ast_to_type_shell(\n        node.right,\n        metaclass=metaclass,\n        module=module,\n        modaliases=modaliases,\n        schema=schema,\n    )\n\n    CompositeTypeShell = (\n        s_types.UnionTypeShell\n        if node.op == qlast.TypeOpName.OR else\n        s_types.IntersectionTypeShell\n    )\n\n    # Doubled check for s_types.TypeExprShell to reassure mypy\n    if (\n        isinstance(left, CompositeTypeShell)\n        and isinstance(left, s_types.TypeExprShell)\n    ):\n        if (\n            isinstance(right, CompositeTypeShell)\n            and isinstance(right, s_types.TypeExprShell)\n        ):\n            return CompositeTypeShell(\n                components=left.components + right.components,\n                module=module,\n                schemaclass=metaclass,\n                span=node.span,\n            )\n        else:\n            return CompositeTypeShell(\n                components=left.components + (right,),\n                module=module,\n                schemaclass=metaclass,\n                span=node.span,\n            )\n    else:\n        if (\n            isinstance(right, CompositeTypeShell)\n            and isinstance(right, s_types.TypeExprShell)\n        ):\n            return CompositeTypeShell(\n                components=(left,) + right.components,\n                schemaclass=metaclass,\n                module=module,\n                span=node.span,\n            )\n        else:\n            return CompositeTypeShell(\n                components=(left, right),\n                module=module,\n                schemaclass=metaclass,\n                span=node.span,\n            )\n\n\ndef ast_to_object_shell(\n    node: qlast.ObjectRef | qlast.TypeName,\n    *,\n    metaclass: type[so.Object_T],\n    module: Optional[str] = None,\n    modaliases: Mapping[Optional[str], str],\n    schema: s_schema.Schema,\n) -> so.ObjectShell[so.Object_T]:\n    from . import types as s_types\n\n    if isinstance(node, qlast.TypeName):\n        if issubclass(metaclass, s_types.Type):\n            return ast_to_type_shell(  # type: ignore\n                node,\n                metaclass=metaclass,\n                module=module,\n                modaliases=modaliases,\n                schema=schema,\n            )\n        else:\n            objref = node.maintype\n            if node.subtypes:\n                raise AssertionError(\n                    'must pass s_types.Type subclass as type when '\n                    'creating a type shell from type AST'\n                )\n            assert isinstance(objref, qlast.ObjectRef)\n            return ast_objref_to_object_shell(\n                objref,\n                modaliases=modaliases,\n                metaclass=metaclass,\n                schema=schema,\n            )\n    else:\n        return ast_objref_to_object_shell(\n            node,\n            modaliases=modaliases,\n            metaclass=metaclass,\n            schema=schema,\n        )\n\n\ndef typeref_to_ast(\n    schema: s_schema.Schema,\n    ref: so.Object_T | so.ObjectShell[so.Object_T],\n    *,\n    _name: Optional[str] = None,\n    disambiguate_std: bool=False,\n) -> qlast.TypeExpr:\n    from . import types as s_types\n\n    if isinstance(ref, so.ObjectShell):\n        return type_shell_to_ast(schema, ref)\n    else:\n        t = ref\n\n    result: qlast.TypeExpr\n\n    if isinstance(t, s_types.Type) and t.is_any(schema):\n        result = qlast.TypeName(\n            name=_name, maintype=qlast.PseudoObjectRef(name='anytype')\n        )\n    elif isinstance(t, s_types.Type) and t.is_anytuple(schema):\n        result = qlast.TypeName(\n            name=_name, maintype=qlast.PseudoObjectRef(name='anytuple')\n        )\n    elif isinstance(t, s_types.Type) and t.is_anyobject(schema):\n        result = qlast.TypeName(\n            name=_name, maintype=qlast.PseudoObjectRef(name='anyobject')\n        )\n    elif isinstance(t, s_types.Tuple) and t.is_named(schema):\n        result = qlast.TypeName(\n            name=_name,\n            maintype=qlast.ObjectRef(\n                name=t.get_schema_name()\n            ),\n            subtypes=[\n                typeref_to_ast(schema, st, _name=sn,\n                               disambiguate_std=disambiguate_std)\n                for sn, st in t.iter_subtypes(schema)\n            ]\n        )\n    elif isinstance(t, (s_types.Array, s_types.Tuple,\n                        s_types.Range, s_types.MultiRange)):\n        # Here the concrete type Array is used because t.get_schema_name()\n        # is used, which is not defined for more generic collections and abcs\n        result = qlast.TypeName(\n            name=_name,\n            maintype=qlast.ObjectRef(\n                name=t.get_schema_name()\n            ),\n            subtypes=[\n                typeref_to_ast(schema, st,\n                               disambiguate_std=disambiguate_std)\n                for st in t.get_subtypes(schema)\n            ]\n        )\n    elif (\n        isinstance(t, s_types.Type)\n        and (t.is_union_type(schema) or t.is_intersection_type(schema))\n    ):\n        object_set = (\n            t.get_union_of(schema)\n            if t.is_union_type(schema) else\n            t.get_intersection_of(schema)\n        )\n        assert object_set is not None\n\n        component_objects = tuple(object_set.objects(schema))\n        result = typeref_to_ast(\n            schema, component_objects[0], disambiguate_std=disambiguate_std\n        )\n        for component_object in component_objects[1:]:\n            result = qlast.TypeOp(\n                left=result,\n                op=(\n                    qlast.TypeOpName.OR\n                    if t.is_union_type(schema) else\n                    qlast.TypeOpName.AND\n                ),\n                right=typeref_to_ast(\n                    schema, component_object, disambiguate_std=disambiguate_std\n                ),\n            )\n    elif isinstance(t, so.QualifiedObject):\n        t_name = t.get_name(schema)\n        module = t_name.module\n        if disambiguate_std and module == 'std':\n            # If the type is defined in 'std::', replace the module to\n            # '__std__' to handle cases where 'std' name is aliased to\n            # another module.\n            module = '__std__'\n        result = qlast.TypeName(\n            name=_name,\n            maintype=qlast.ObjectRef(\n                module=module,\n                name=t_name.name\n            )\n        )\n    else:\n        raise NotImplementedError(f'cannot represent {t!r} as a shell')\n\n    return result\n\n\ndef shell_to_ast(\n    schema: s_schema.Schema,\n    t: so.ObjectShell[so.Object],\n    *,\n    _name: Optional[str] = None,\n) -> qlast.TypeExpr | qlast.Expr:\n    from . import types as s_types\n\n    if isinstance(t, s_types.TypeShell):\n        return type_shell_to_ast(schema, t, _name=_name)\n    elif isinstance(t, so.ObjectShell):\n        name = t.name\n        if isinstance(name, sn.QualName):\n            qlref = qlast.ObjectRef(\n                module=name.module,\n                name=name.name,\n            )\n        else:\n            qlref = qlast.ObjectRef(\n                module='',\n                name=name.name,\n            )\n        return qlast.Path(steps=[qlref])\n\n    else:\n        raise NotImplementedError(f'cannot represent {t!r} as a type shell')\n\n\ndef type_shell_to_ast(\n    schema: s_schema.Schema,\n    t: so.ObjectShell[so.Object],\n    *,\n    _name: Optional[str] = None,\n) -> qlast.TypeExpr:\n    from . import pseudo as s_pseudo\n    from . import types as s_types\n    from . import scalars as s_scalars\n\n    result: qlast.TypeExpr\n    qlref: qlast.BaseObjectRef\n\n    if isinstance(t, s_pseudo.PseudoTypeShell):\n        if t.name.name not in {'anytype', 'anytuple', 'anyobject'}:\n            raise AssertionError(f'unexpected pseudo type shell: {t.name!r}')\n        result = qlast.TypeName(\n            name=_name, maintype=qlast.PseudoObjectRef(name=t.name.name)\n        )\n    elif isinstance(t, s_types.TupleTypeShell):\n        if t.is_named():\n            result = qlast.TypeName(\n                name=_name,\n                maintype=qlast.ObjectRef(\n                    name='tuple',\n                ),\n                subtypes=[\n                    type_shell_to_ast(schema, st, _name=sn)\n                    for sn, st in t.iter_subtypes(schema)\n                ]\n            )\n        else:\n            result = qlast.TypeName(\n                name=_name,\n                maintype=qlast.ObjectRef(\n                    name='tuple',\n                ),\n                subtypes=[\n                    type_shell_to_ast(schema, st)\n                    for st in t.get_subtypes(schema)\n                ]\n            )\n    elif isinstance(t, s_types.ArrayTypeShell):\n        result = qlast.TypeName(\n            name=_name,\n            maintype=qlast.ObjectRef(\n                name='array',\n            ),\n            subtypes=[\n                type_shell_to_ast(schema, st)\n                for st in t.get_subtypes(schema)\n            ]\n        )\n    elif isinstance(t, s_types.RangeTypeShell):\n        result = qlast.TypeName(\n            name=_name,\n            maintype=qlast.ObjectRef(\n                name='range',\n            ),\n            subtypes=[\n                type_shell_to_ast(schema, st)\n                for st in t.get_subtypes(schema)\n            ]\n        )\n    elif isinstance(t, s_types.MultiRangeTypeShell):\n        result = qlast.TypeName(\n            name=_name,\n            maintype=qlast.ObjectRef(\n                name='multirange',\n            ),\n            subtypes=[\n                type_shell_to_ast(schema, st)\n                for st in t.get_subtypes(schema)\n            ]\n        )\n    elif isinstance(t, s_types.UnionTypeShell):\n        components = t.get_components(schema)\n        result = typeref_to_ast(schema, components[0])\n        for component in components[1:]:\n            result = qlast.TypeOp(\n                left=result,\n                op=qlast.TypeOpName.OR,\n                right=typeref_to_ast(schema, component),\n            )\n    elif isinstance(t, s_types.IntersectionTypeShell):\n        components = t.get_components(schema)\n        result = typeref_to_ast(schema, components[0])\n        for component in components[1:]:\n            result = qlast.TypeOp(\n                left=result,\n                op=qlast.TypeOpName.AND,\n                right=typeref_to_ast(schema, component),\n            )\n    elif isinstance(t, s_scalars.AnonymousEnumTypeShell):\n        result = qlast.TypeName(\n            name=_name,\n            maintype=qlast.ObjectRef(\n                name='enum',\n            ),\n            subtypes=[\n                qlast.TypeName(maintype=qlast.ObjectRef(name=x))\n                for x in t.elements\n            ]\n        )\n    elif isinstance(t, so.ObjectShell):\n        name = t.name\n        if isinstance(name, sn.QualName):\n            qlref = qlast.ObjectRef(\n                module=name.module,\n                name=name.name,\n            )\n        else:\n            qlref = qlast.ObjectRef(\n                module='',\n                name=name.name,\n            )\n        result = qlast.TypeName(\n            name=_name,\n            maintype=qlref,\n        )\n    else:\n        raise NotImplementedError(f'cannot represent {t!r} as a type shell')\n\n    return result\n\n\ndef is_nontrivial_container(value: Any) -> Optional[Iterable[Any]]:\n    trivial_classes = (str, bytes, bytearray, memoryview)\n    if (isinstance(value, collections.abc.Iterable) and\n            not isinstance(value, trivial_classes)):\n        return value\n    else:\n        return None\n\n\ndef get_class_nearest_common_ancestors(\n    schema: s_schema.Schema, classes: Iterable[so.InheritingObjectT]\n) -> list[so.InheritingObjectT]:\n    # First, find the intersection of parents\n    classes = list(classes)\n    first = [classes[0]]\n    first.extend(classes[0].get_ancestors(schema).objects(schema))\n    common = set(first).intersection(\n        *[set(c.get_ancestors(schema).objects(schema)) | {c}\n          for c in classes[1:]])\n    common_list = sorted(common, key=lambda i: first.index(i))\n    nearests: list[so.InheritingObjectT] = []\n    # Then find the common ancestors that don't have any subclasses that\n    # are also nearest common ancestors.\n    for anc in common_list:\n        if not any(x.issubclass(schema, anc) for x in nearests):\n            nearests.append(anc)\n\n    return nearests\n\n\ndef minimize_class_set_by_most_generic(\n    schema: s_schema.Schema, classes: Iterable[so.InheritingObjectT]\n) -> list[so.InheritingObjectT]:\n    \"\"\"Minimize the given set of objects by filtering out all subclasses.\"\"\"\n\n    classes = list(classes)\n    mros = [set(p.get_ancestors(schema).objects(schema)) for p in classes]\n    count = len(classes)\n    smap = itertools.starmap\n\n    # Return only those entries that do not have other entries in their mro\n    result = [\n        scls for i, scls in enumerate(classes)\n        if not any(smap(set.__contains__,\n                        ((mros[i], classes[j])\n                         for j in range(count) if j != i)))\n    ]\n\n    return result\n\n\ndef minimize_class_set_by_least_generic(\n    schema: s_schema.Schema, classes: Iterable[so.InheritingObjectT]\n) -> list[so.InheritingObjectT]:\n    \"\"\"Minimize the given set of objects by filtering out all superclasses.\"\"\"\n\n    classes = list(classes)\n    mros = [set(p.get_ancestors(schema).objects(schema)) | {p}\n            for p in classes]\n    count = len(classes)\n    smap = itertools.starmap\n\n    # Return only those entries that are not present in other entries' mro\n    result = [\n        scls for i, scls in enumerate(classes)\n        if not any(smap(set.__contains__,\n                        ((mros[j], classes[i])\n                         for j in range(count) if j != i)))\n    ]\n\n    return result\n\n\ndef merge_reduce(\n    target: so.InheritingObjectT,\n    sources: Iterable[so.InheritingObjectT],\n    field_name: str,\n    *,\n    ignore_local: bool,\n    schema: s_schema.Schema,\n    f: Callable[[T, T], T],\n    type: type[T],\n) -> Optional[T]:\n    values: list[tuple[T, str]] = []\n    if not ignore_local:\n        ours = target.get_explicit_local_field_value(schema, field_name, None)\n        if ours is not None:\n            vn = target.get_verbosename(schema, with_parent=True)\n            values.append((ours, vn))\n    for source in sources:\n        theirs = source.get_explicit_field_value(schema, field_name, None)\n        if theirs is not None:\n            vn = source.get_verbosename(schema, with_parent=True)\n            values.append((theirs, vn))\n\n    if values:\n        val = values[0][0]\n        desc = values[0][1]\n        cdn = target.get_schema_class_displayname()\n        for other_val, other_desc in values[1:]:\n            try:\n                val = f(val, other_val)\n            except Exception:\n                raise errors.SchemaDefinitionError(\n                    f'invalid {cdn} definition: {field_name} is defined '\n                    f'as {val} in {desc}, but is defined as {other_val} '\n                    f'in {other_desc}, which is incompatible'\n                )\n\n        return val\n    else:\n        return None\n\n\ndef get_nq_name(schema: s_schema.Schema, item: so.Object) -> str:\n    shortname = item.get_shortname(schema)\n    if isinstance(shortname, sn.QualName):\n        return shortname.name\n    else:\n        return str(shortname)\n\n\ndef find_item_suggestions(\n    name: sn.Name,\n    modaliases: Mapping[Optional[str], str],\n    schema: s_schema.Schema,\n    *,\n    item_type: Optional[so.ObjectMeta] = None,\n    condition: Optional[Callable[[so.Object], bool]] = None,\n) -> Iterable[tuple[so.Object, str]]:\n    from . import functions as s_func\n    from . import properties as s_prop\n    from . import links as s_link\n    from . import modules as s_mod\n\n    orig_modname = name.module if isinstance(name, sn.QualName) else None\n\n    suggestions: list[so.Object] = []\n\n    if modname := modaliases.get(orig_modname, None):\n        if schema.get_global(s_mod.Module, modname, None):\n            suggestions.extend(\n                schema.get_objects(\n                    included_modules=[sn.UnqualName(modname)],\n                ),\n            )\n\n        modname = f'std::{modname}'\n        if schema.get_global(s_mod.Module, modname, None):\n            suggestions.extend(\n                schema.get_objects(\n                    included_modules=[sn.UnqualName(modname)],\n                ),\n            )\n\n    if orig_modname:\n        if schema.get_global(s_mod.Module, orig_modname, None):\n            suggestions.extend(\n                schema.get_objects(\n                    included_modules=[sn.UnqualName(orig_modname)],\n                ),\n            )\n\n        modname = f'std::{orig_modname}'\n        if schema.get_global(s_mod.Module, modname, None):\n            suggestions.extend(\n                schema.get_objects(\n                    included_modules=[sn.UnqualName(modname)],\n                ),\n            )\n\n    else:\n        suggestions.extend(\n            schema.get_objects(\n                included_modules=[sn.UnqualName(\"std\")],\n            ),\n        )\n\n    filters = []\n\n    # links and properties are suggested by find_fields_suggestions\n    filters.append(\n        lambda s: not isinstance(s, s_prop.Property)\n        and not isinstance(s, s_link.Link)\n    )\n\n    if condition is not None:\n        filters.append(condition)\n\n    if item_type is not None:\n        it = item_type\n        filters.append(lambda s: isinstance(s, it))\n    else:\n        # When schema class is not specified, only suggest generic objects.\n        filters.append(lambda s: not sn.is_fullname(str(s.get_name(schema))))\n        filters.append(lambda s: not isinstance(s, s_func.CallableObject))\n\n    # Never suggest object fragments.\n    filters.append(lambda s: not isinstance(s, so.ObjectFragment))\n\n    filtered = filter(lambda s: all(f(s) for f in filters), suggestions)\n\n    # Add display names\n    cur_module_name = modaliases.get(None)\n\n    def get_display_name(suggestion: so.Object) -> str:\n        if isinstance(suggestion, so.QualifiedObject):\n            mod = suggestion.get_name(schema).module\n            if mod == \"std\" or mod == cur_module_name:\n                return get_nq_name(schema, suggestion)\n\n        return suggestion.get_displayname(schema)\n\n    return ((s, get_display_name(s)) for s in filtered)\n\n\ndef find_pointer_suggestions(\n    schema: s_schema.Schema,\n    item_type: Optional[so.ObjectMeta],\n    parent: Optional[so.Object],\n) -> Iterable[tuple[so.Object, str]]:\n    from . import pointers as s_pointers\n\n    \"\"\"\n    Suggests pointers (properties or links) from parent object type.\n    If pointer type is not expected, use .name notation.\n    \"\"\"\n    from . import sources as s_sources\n\n    if not isinstance(parent, s_sources.Source):\n        return ()\n\n    pointers_with_names = parent.get_pointers(schema).items(schema)\n    pointers = (pointer for _, pointer in pointers_with_names)\n\n    suggestions = ((s, s.get_displayname(schema)) for s in pointers)\n\n    if item_type is not s_pointers.Pointer:\n        # Prefix with .\n        suggestions = ((s, \".\" + n) for s, n in suggestions)\n\n    return suggestions\n\n\ndef pick_closest_suggestions(\n    name: sn.Name,\n    schema: s_schema.Schema,\n    suggestions: Iterable[tuple[so.Object, str]],\n    limit: int,\n) -> list[tuple[so.Object, str]]:\n    local_name = name.name\n\n    # Compute Levenshtein distance for each suggestion.\n    with_distance: list[tuple[so.Object, str, int]] = [\n        (s, name, levenshtein.distance(local_name, get_nq_name(schema, s)))\n        for s, name in suggestions\n    ]\n\n    # Filter out suggestions that are too dissimilar.\n    max_distance = 3\n    closest = list(filter(lambda s: s[2] < max_distance, with_distance))\n\n    # Sort by proximity, then by whether the suggestion is contains\n    # the source string at the beginning, then by suggestion name.\n    closest.sort(\n        key=lambda s: (\n            s[2],\n            not get_nq_name(schema, s[0]).startswith(local_name),\n            s[1],\n        )\n    )\n\n    return [(s[0], s[1]) for s in closest[:limit]]\n\n\ndef enrich_schema_lookup_error(\n    error: errors.EdgeDBError,\n    item_name: sn.Name,\n    modaliases: Mapping[Optional[str], str],\n    schema: s_schema.Schema,\n    *,\n    item_type: Optional[so.ObjectMeta] = None,\n    suggestion_limit: int = 3,\n    condition: Optional[Callable[[so.Object], bool]] = None,\n    span: Optional[parsing.Span] = None,\n    pointer_parent: Optional[so.Object] = None,\n    hint_text: str = 'did you mean'\n) -> None:\n\n    all_suggestions = itertools.chain(\n        find_item_suggestions(\n            item_name,\n            modaliases,\n            schema,\n            item_type=item_type,\n            condition=condition,\n        ),\n        find_pointer_suggestions(schema, item_type, pointer_parent),\n    )\n\n    suggestions = pick_closest_suggestions(\n        item_name, schema, all_suggestions, suggestion_limit\n    )\n\n    if suggestions:\n        names = [name for _, name in suggestions]\n\n        if len(names) > 1:\n            hint = f'{hint_text} one of these: {\", \".join(names)}?'\n        else:\n            hint = f'{hint_text} {names[0]!r}?'\n\n        error.set_hint_and_details(hint=hint)\n\n    if span is not None:\n        error.set_span(span)\n\n\ndef ensure_union_type(\n    schema: s_schema.Schema,\n    types: Sequence[s_types.Type],\n    *,\n    opaque: bool = False,\n    module: Optional[str] = None,\n    transient: bool = False,\n) -> tuple[s_schema.Schema, s_types.Type, bool]:\n\n    from edb.schema import objtypes as s_objtypes\n\n    if len(types) == 1 and not opaque:\n        return schema, next(iter(types)), False\n\n    seen_scalars = False\n    seen_objtypes = False\n    created = False\n\n    for t in types:\n        if isinstance(t, s_objtypes.ObjectType):\n            if seen_scalars:\n                raise _union_error(schema, types)\n            seen_objtypes = True\n        else:\n            if seen_objtypes:\n                raise _union_error(schema, types)\n            seen_scalars = True\n\n    if seen_scalars:\n        uniontype: s_types.Type = types[0]\n        for t1 in types[1:]:\n\n            schema, common_type = (\n                uniontype.find_common_implicitly_castable_type(t1, schema)\n            )\n\n            if common_type is None:\n                raise _union_error(schema, types)\n            else:\n                uniontype = common_type\n    else:\n        objtypes = cast(\n            Sequence[s_objtypes.ObjectType],\n            types,\n        )\n        schema, uniontype, created = s_objtypes.get_or_create_union_type(\n            schema,\n            components=objtypes,\n            opaque=opaque,\n            module=module,\n            transient=transient,\n        )\n\n    return schema, uniontype, created\n\n\ndef simplify_union_types(\n    schema: s_schema.Schema,\n    types: Sequence[s_types.Type],\n) -> Sequence[s_types.Type]:\n    \"\"\"Minimize the types used to create a union of types.\n\n    Any unions types are unwrapped. Then, any unnecessary subclasses are\n    removed.\n    \"\"\"\n\n    from edb.schema import types as s_types\n\n    components: set[s_types.Type] = set()\n    for t in types:\n        union_of = t.get_union_of(schema)\n        if union_of:\n            components.update(union_of.objects(schema))\n        else:\n            components.add(t)\n\n    if all(isinstance(c, s_types.InheritingType) for c in components):\n        return list(minimize_class_set_by_most_generic(\n            schema,\n            cast(set[s_types.InheritingType], components),\n        ))\n    else:\n        return list(components)\n\n\ndef simplify_union_types_preserve_derived(\n    schema: s_schema.Schema,\n    types: Sequence[s_types.Type],\n) -> Sequence[s_types.Type]:\n    \"\"\"Minimize the types used to create a union of types.\n\n    Any unions types are unwrapped. Then, any unnecessary subclasses are\n    removed.\n\n    Derived types are always preserved for 'std::UNION', 'std::IF', and\n    'std::??'.\n    \"\"\"\n\n    from edb.schema import types as s_types\n\n    components: set[s_types.Type] = set()\n    for t in types:\n        union_of = t.get_union_of(schema)\n        if union_of:\n            components.update(union_of.objects(schema))\n        else:\n            components.add(t)\n\n    derived = set(\n        t\n        for t in components\n        if (\n            isinstance(t, s_types.InheritingType)\n            and t.get_is_derived(schema)\n        )\n    )\n\n    nonderived: Sequence[s_types.Type] = [\n        t\n        for t in components\n        if t not in derived\n    ]\n    nonderived = minimize_class_set_by_most_generic(\n        schema,\n        cast(set[s_types.InheritingType], nonderived),\n    )\n\n    return list(nonderived) + list(derived)\n\n\ndef get_non_overlapping_union(\n    schema: s_schema.Schema,\n    objects: Iterable[so.InheritingObjectT],\n) -> tuple[frozenset[so.InheritingObjectT], bool]:\n\n    all_objects: set[so.InheritingObjectT] = set(objects)\n    non_unique_count = 0\n    for obj in objects:\n        descendants = obj.descendants(schema)\n        non_unique_count += len(descendants) + 1\n        all_objects.update(descendants)\n\n    if non_unique_count == len(all_objects):\n        # The input object set is already non-overlapping\n        return frozenset(objects), False\n    else:\n        return frozenset(all_objects), True\n\n\ndef get_type_expr_non_overlapping_union(\n    type: s_types.Type,\n    schema: s_schema.Schema,\n) -> tuple[frozenset[s_types.Type], bool]:\n    \"\"\"Get a non-overlapping set of the type's descendants\"\"\"\n\n    from edb.schema import types as s_types\n\n    expanded_types = expand_type_expr_descendants(type, schema)\n\n    # filter out subclasses\n    expanded_types = {\n        type\n        for type in expanded_types\n        if not any(\n            type is not other and type.issubclass(schema, other)\n            for other in expanded_types\n        )\n    }\n\n    non_overlapping, union_is_exhaustive = get_non_overlapping_union(\n        schema, cast(set[so.InheritingObject], expanded_types)\n    )\n\n    return cast(frozenset[s_types.Type], non_overlapping), union_is_exhaustive\n\n\ndef expand_type_expr_descendants(\n    type: s_types.Type,\n    schema: s_schema.Schema,\n    *,\n    expand_opaque_union: bool = True,\n) -> set[s_types.Type]:\n    \"\"\"Expand types and type expressions to get descendants\"\"\"\n\n    from edb.schema import types as s_types\n\n    if sub_union := type.get_union_of(schema):\n        # Expanding a union\n        # Get the union of the component descendants\n        return set.union(*(\n            expand_type_expr_descendants(\n                component, schema,\n            )\n            for component in sub_union.objects(schema)\n        ))\n\n    elif sub_intersection := type.get_intersection_of(schema):\n        # Expanding an intersection\n        # Get the intersection of component descendants\n        return set.intersection(*(\n            expand_type_expr_descendants(\n                component, schema\n            )\n            for component in sub_intersection.objects(schema)\n        ))\n\n    elif type.is_view(schema):\n        # When expanding a view, simply unpeel the view.\n        return expand_type_expr_descendants(\n            type.peel_view(schema), schema\n        )\n\n    # Return simple type and all its descendants.\n    # Some types (eg. BaseObject) have non-simple descendants, filter them out.\n    return {type} | {\n        c for c in cast(\n            set[s_types.Type],\n            set(cast(so.InheritingObject, type).descendants(schema))\n        )\n        if (\n            not c.is_union_type(schema)\n            and not c.is_intersection_type(schema)\n            and not c.is_view(schema)\n        )\n    }\n\n\ndef _union_error(\n    schema: s_schema.Schema, components: Iterable[s_types.Type]\n) -> errors.SchemaError:\n    names = ', '.join(sorted(c.get_displayname(schema) for c in components))\n    return errors.SchemaError(f'using incompatible types {names}')\n\n\ndef ensure_intersection_type(\n    schema: s_schema.Schema,\n    types: Sequence[s_types.Type],\n    *,\n    transient: bool = False,\n    module: Optional[str] = None,\n) -> tuple[s_schema.Schema, s_types.Type, bool]:\n\n    from edb.schema import objtypes as s_objtypes\n\n    if len(types) == 1:\n        return schema, next(iter(types)), False\n\n    seen_scalars = False\n    seen_objtypes = False\n\n    for t in types:\n        if t.is_object_type():\n            if seen_scalars:\n                raise _intersection_error(schema, types)\n            seen_objtypes = True\n        else:\n            if seen_objtypes:\n                raise _intersection_error(schema, types)\n            seen_scalars = True\n\n    if seen_scalars:\n        # Non-related scalars and collections cannot for intersection types.\n        raise _intersection_error(schema, types)\n    else:\n        return s_objtypes.get_or_create_intersection_type(\n            schema,\n            components=cast(Iterable[s_objtypes.ObjectType], types),\n            module=module,\n            transient=transient,\n        )\n\n\ndef simplify_intersection_types(\n    schema: s_schema.Schema,\n    types: Sequence[s_types.Type],\n) -> Sequence[s_types.Type]:\n    \"\"\"Minimize the types used to create an intersection of types.\n\n    Any intersection types are unwrapped. Then, any unnecessary superclasses are\n    removed.\n    \"\"\"\n\n    from edb.schema import types as s_types\n\n    components: set[s_types.Type] = set()\n    for t in types:\n        intersection_of = t.get_intersection_of(schema)\n        if intersection_of:\n            components.update(intersection_of.objects(schema))\n        else:\n            components.add(t)\n\n    if all(isinstance(c, s_types.InheritingType) for c in components):\n        return minimize_class_set_by_least_generic(\n            schema,\n            cast(set[s_types.InheritingType], components),\n        )\n    else:\n        return list(components)\n\n\ndef _intersection_error(\n    schema: s_schema.Schema, components: Iterable[s_types.Type]\n) -> errors.SchemaError:\n    names = ', '.join(sorted(c.get_displayname(schema) for c in components))\n    return errors.SchemaError(f'cannot create an intersection of {names}')\n\n\nMAX_INT64 = 2 ** 63 - 1\nMIN_INT64 = -2 ** 63\n\n\ndef const_ast_from_python(val: Any, with_secrets: bool=False) -> qlast.Expr:\n    if isinstance(val, str):\n        return qlast.Constant.string(val)\n    elif isinstance(val, bool):\n        return qlast.Constant.boolean(val)\n    elif isinstance(val, int):\n        if MIN_INT64 <= val <= MAX_INT64:\n            return qlast.Constant.integer(val)\n        else:\n            raise ValueError(f'int64 value out of range: {val}')\n    elif isinstance(val, decimal.Decimal):\n        return qlast.Constant(value=f'{val}n', kind=qlast.ConstantKind.DECIMAL)\n    elif isinstance(val, float):\n        return qlast.Constant(value=str(val), kind=qlast.ConstantKind.FLOAT)\n    elif isinstance(val, bytes):\n        return qlast.BytesConstant(value=val)\n    elif isinstance(val, statypes.Duration):\n        return qlast.TypeCast(\n            type=qlast.TypeName(\n                maintype=qlast.ObjectRef(module='__std__', name='duration'),\n            ),\n            expr=qlast.Constant.string(value=val.to_iso8601()),\n        )\n    elif isinstance(val, statypes.EnumScalarType):\n        qltype = val.get_edgeql_type()\n        return qlast.TypeCast(\n            type=qlast.TypeName(\n                maintype=qlast.ObjectRef(\n                    module=qltype.module, name=qltype.name),\n            ),\n            expr=qlast.Constant.string(value=val.to_str()),\n        )\n    elif isinstance(val, statypes.CompositeType):\n        return qlast.InsertQuery(\n            subject=name_to_ast_ref(sn.name_from_string(val._tspec.name)),\n            shape=[\n                qlast.ShapeElement(\n                    expr=qlast.Path(steps=[qlast.Ptr(name=ptr)]),\n                    compexpr=const_ast_from_python(\n                        getattr(val, ptr), with_secrets=with_secrets\n                    ),\n                )\n                for ptr, typ in val._tspec.fields.items()\n                if not (typ.secret and not with_secrets) and not typ.protected\n            ],\n        )\n    elif isinstance(val, (set, frozenset)):\n        return qlast.Set(elements=[\n            const_ast_from_python(x, with_secrets=with_secrets) for x in val\n        ])\n    elif val is None:\n        return qlast.Set(elements=[])\n    else:\n        raise ValueError(f'unexpected constant type: {type(val)!r}')\n\n\ndef get_config_type_shape(\n    schema: s_schema.Schema,\n    stype: s_objtypes.ObjectType,\n    path: list[qlast.PathElement],\n) -> list[qlast.ShapeElement]:\n    from . import objtypes as s_objtypes\n    shape = [\n        qlast.ShapeElement(\n            expr=qlast.Path(steps=[qlast.Ptr(name='_tname')], ),\n            compexpr=qlast.Path(\n                steps=path + [\n                    qlast.Ptr(name='__type__'),\n                    qlast.Ptr(name='name'),\n                ],\n            ),\n        ),\n    ]\n    seen: set[str] = set()\n\n    stypes = [stype] + list(stype.ordered_descendants(schema))\n\n    for t in stypes:\n        t_name = t.get_name(schema)\n\n        for unqual_pn, p in t.get_pointers(schema).items(schema):\n            pn = str(unqual_pn)\n            if pn in ('id', '__type__') or pn in seen:\n                continue\n\n            elem_path: list[qlast.PathElement] = []\n\n            if t != stype:\n                elem_path.append(\n                    qlast.TypeIntersection(\n                        type=qlast.TypeName(\n                            maintype=qlast.ObjectRef(\n                                module=t_name.module,\n                                name=t_name.name,\n                            ),\n                        ),\n                    ),\n                )\n\n            elem_path.append(qlast.Ptr(name=pn))\n\n            ptype = p.get_target(schema)\n            assert ptype is not None\n            if str(ptype.get_name(schema)) == 'cfg::AbstractConfig':\n                continue\n\n            if isinstance(ptype, s_objtypes.ObjectType):\n                subshape = get_config_type_shape(\n                    schema, ptype, path + elem_path)\n            else:\n                subshape = []\n\n            shape.append(\n                qlast.ShapeElement(\n                    expr=qlast.Path(steps=elem_path),\n                    elements=subshape,\n                ),\n            )\n\n            seen.add(pn)\n\n    return shape\n\n\ndef type_shell_multi_substitute(\n    mapping: dict[sn.Name, s_types.TypeShell[s_types.TypeT_co]],\n    typ: s_types.TypeShell[s_types.TypeT_co],\n    schema: s_schema.Schema,\n) -> s_types.TypeShell[s_types.TypeT_co]:\n    for name, new in mapping.items():\n        typ = type_shell_substitute(name, new, typ, schema)\n    return typ\n\n\ndef type_shell_substitute(\n    name: sn.Name,\n    new: s_types.TypeShell[s_types.TypeT_co],\n    typ: s_types.TypeShell[s_types.TypeT_co],\n    schema: s_schema.Schema,\n) -> s_types.TypeShell[s_types.TypeT_co]:\n    from . import types as s_types\n\n    # arguably this would be better done with a method on the types\n    if typ.name == name:\n        return new\n\n    if isinstance(typ, s_types.UnionTypeShell):\n        assert isinstance(typ.name, sn.QualName)\n        return s_types.UnionTypeShell(\n            module=typ.name.module,\n            schemaclass=typ.schemaclass,\n            opaque=typ.opaque,\n            components=[\n                type_shell_substitute(name, new, c, schema)\n                for c in typ.components\n            ],\n        )\n    elif isinstance(typ, s_types.IntersectionTypeShell):\n        assert isinstance(typ.name, sn.QualName)\n        return s_types.IntersectionTypeShell(\n            module=typ.name.module,\n            schemaclass=typ.schemaclass,\n            components=[\n                type_shell_substitute(name, new, c, schema)\n                for c in typ.components\n            ],\n        )\n    elif isinstance(typ, s_types.ArrayTypeShell):\n        return s_types.ArrayTypeShell(\n            name=None,\n            expr=typ.expr,\n            typemods=typ.typemods,\n            schemaclass=typ.schemaclass,\n            subtype=type_shell_substitute(name, new, typ.subtype, schema),\n        )\n    elif isinstance(typ, s_types.TupleTypeShell):\n        return s_types.TupleTypeShell(\n            name=None,\n            typemods=typ.typemods,\n            schemaclass=typ.schemaclass,\n            subtypes={\n                k: type_shell_substitute(name, new, v, schema)\n                for k, v in typ.subtypes.items()\n            },\n        )\n    elif isinstance(typ, s_types.RangeTypeShell):\n        return s_types.RangeTypeShell(\n            name=None,\n            typemods=typ.typemods,\n            schemaclass=typ.schemaclass,\n            subtype=type_shell_substitute(name, new, typ.subtype, schema),\n        )\n    else:\n        return typ\n\n\ndef try_compile_irast_to_sql_tree(\n    compiled_expr: s_expr.CompiledExpression,\n    span: Optional[parsing.Span]\n) -> None:\n    # compile the expression to sql to preempt errors downstream\n\n    from edb.pgsql import compiler as pg_compiler\n\n    try:\n        pg_compiler.compile_ir_to_sql_tree(\n            compiled_expr.irast,\n            output_format=pg_compiler.OutputFormat.NATIVE,\n            singleton_mode=True,\n        )\n    except errors.EdgeDBError as exception:\n        exception.set_span(span)\n        raise exception\n    except:\n        raise\n\n\ndef str_interpolation_to_old_style(interp: qlast.StrInterp) -> str:\n    r\"\"\"Convert a \\(name) string interpolation to {name} style for schema use.\n\n    The {name} style of string interpolation is used (with special\n    handling) for errmessage in constraints, and we want to also\n    support \\(name) style.\n\n    We (somewhat unfortunately) implement this by converting the\n    \\(name) style *into* the older {name} style.\n\n    It would be somewhat nicer to store the string using \\(name)\n    style, but doing that loses the ability to prevent interpolation\n    by doing \\\\(name). (Since the lexed version is stored in that\n    case, we wouldn't be able to distinguish between the cases.)\n    \"\"\"\n\n    res = interp.prefix\n    for frag in interp.interpolations:\n        match frag.expr:\n            case qlast.Path(\n                partial=False,\n                steps=[\n                    qlast.Anchor(name=name)\n                    | qlast.ObjectRef(name=name, module=None)\n                ]\n            ):\n                res += '{' + name + '}'\n            case _:\n                raise errors.SchemaDefinitionError(\n                    \"only variables are allowed in simple schema \"\n                    \"interpolations\",\n                    span=frag.expr.span,\n                )\n\n        res += frag.suffix\n    return res\n"
  },
  {
    "path": "edb/schema/version.py",
    "content": "# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport uuid\n\nfrom . import delta as sd\nfrom . import objects as so\n\n\nclass BaseSchemaVersion(so.Object):\n\n    version = so.SchemaField(uuid.UUID)\n\n\nclass SchemaVersion(BaseSchemaVersion, so.InternalObject):\n    pass\n\n\nclass SchemaVersionCommandContext(sd.ObjectCommandContext[SchemaVersion]):\n    pass\n\n\nclass SchemaVersionCommand(\n    sd.ObjectCommand[SchemaVersion],\n    context_class=SchemaVersionCommandContext,\n):\n    pass\n\n\nclass CreateSchemaVersion(\n    SchemaVersionCommand,\n    sd.CreateObject[SchemaVersion],\n):\n    pass\n\n\nclass AlterSchemaVersion(\n    SchemaVersionCommand,\n    sd.AlterObject[SchemaVersion],\n):\n    pass\n\n\nclass GlobalSchemaVersion(\n    BaseSchemaVersion, so.InternalObject, so.GlobalObject\n):\n    pass\n\n\nclass GlobalSchemaVersionCommandContext(\n    sd.ObjectCommandContext[GlobalSchemaVersion],\n):\n    pass\n\n\nclass GlobalSchemaVersionCommand(\n    sd.ObjectCommand[GlobalSchemaVersion],\n    context_class=GlobalSchemaVersionCommandContext,\n):\n    pass\n\n\nclass CreateGlobalSchemaVersion(\n    GlobalSchemaVersionCommand,\n    sd.CreateObject[GlobalSchemaVersion],\n):\n    pass\n\n\nclass AlterGlobalSchemaVersion(\n    GlobalSchemaVersionCommand,\n    sd.AlterObject[GlobalSchemaVersion],\n):\n    pass\n"
  },
  {
    "path": "edb/server/.gitignore",
    "content": "*.c\n*.html\n"
  },
  {
    "path": "edb/server/__init__.py",
    "content": "##\n# Copyright (c) 2008-present MagicStack Inc.\n# All rights reserved.\n#\n# See LICENSE for details.\n##\n\nfrom __future__ import annotations\n"
  },
  {
    "path": "edb/server/_rust_native/Cargo.toml",
    "content": "[package]\nname = \"rust_native\"\nversion = \"0.1.0\"\nlicense = \"MIT/Apache-2.0\"\nauthors = [\"MagicStack Inc. <hello@magic.io>\"]\nedition = \"2021\"\n\n[lints]\nworkspace = true\n\n[features]\npython_extension = [\"pyo3/extension-module\", \"pyo3/serde\"]\n\n[dependencies]\npyo3 = { workspace = true }\npyo3_util.workspace = true\nconn_pool = { workspace = true, features = [ \"python_extension\" ] }\npgrust = { workspace = true, features = [ \"python_extension\" ] }\ngel-http = { workspace = true, features = [ \"python_extension\" ] }\ngel-jwt = { workspace = true, features = [ \"python_extension\" ] }\n\n[lib]\ncrate-type = [\"lib\", \"cdylib\"]\npath = \"src/lib.rs\"\n"
  },
  {
    "path": "edb/server/_rust_native/src/lib.rs",
    "content": "use pyo3::{\n    pymodule,\n    types::{PyAnyMethods, PyModule, PyModuleMethods},\n    Bound, PyResult, Python,\n};\nuse pyo3_util::logging::{get_python_logger_level, initialize_logging_in_thread};\n\nconst MODULE_PREFIX: &str = \"edb.server._rust_native\";\n\nfn add_child_module(\n    py: Python,\n    parent: &Bound<PyModule>,\n    name: &str,\n    init_fn: fn(Python, &Bound<PyModule>) -> PyResult<()>,\n) -> PyResult<()> {\n    let full_name = format!(\"{MODULE_PREFIX}.{name}\");\n    let child_module = PyModule::new(py, &full_name)?;\n    init_fn(py, &child_module)?;\n    parent.add(name, &child_module)?;\n\n    // Add the child module to the sys.modules dictionary so it can be imported\n    // by name.\n    let sys_modules = py.import(\"sys\")?.getattr(\"modules\")?;\n    sys_modules.set_item(full_name, child_module)?;\n    Ok(())\n}\n\n#[pymodule]\nfn _rust_native(py: Python, m: &Bound<PyModule>) -> PyResult<()> {\n    // Initialize any logging in this thread to route to \"edb.server\"\n    let level = get_python_logger_level(py, \"edb.server\")?;\n    initialize_logging_in_thread(\"edb.server\", level);\n\n    add_child_module(py, m, \"_conn_pool\", conn_pool::python::_conn_pool)?;\n    add_child_module(py, m, \"_pg_rust\", pgrust::python::_pg_rust)?;\n    add_child_module(py, m, \"_http\", gel_http::python::_gel_http)?;\n    add_child_module(py, m, \"_jwt\", gel_jwt::python::_jwt)?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "edb/server/args.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n    ItemsView,\n    Mapping,\n    NamedTuple,\n    NoReturn,\n    Sequence,\n)\n\nimport logging\nimport os\nimport pathlib\nimport re\nimport warnings\nimport tempfile\n\nimport click\nimport psutil\n\nfrom edb import buildmeta\nfrom edb.common import devmode\nfrom edb.common import enum\nfrom edb.common import typeutils\nfrom edb.schema import defines as schema_defines\nfrom edb.pgsql import params as pgsql_params\n\nfrom . import defines\n\n\nMIB = 1024 * 1024\nRAM_MIB_PER_CONN = 100\nTLS_CERT_FILE_NAME = \"edbtlscert.pem\"\nTLS_KEY_FILE_NAME = \"edbprivkey.pem\"\nJWS_KEY_FILE_NAME = \"edbjwskeys.pem\"\n\n\nlogger = logging.getLogger('edb.server')\n\n\nclass InvalidUsageError(Exception):\n\n    def __init__(self, msg: str, exit_code: int = 2) -> None:\n        super().__init__(msg, exit_code)\n\n\ndef abort(msg: str, *, exit_code: int = 2) -> NoReturn:\n    raise InvalidUsageError(msg, exit_code)\n\n\nclass StartupScript(NamedTuple):\n\n    text: str\n    database: str\n    user: str\n\n\nclass ServerSecurityMode(enum.StrEnum):\n\n    Strict = \"strict\"\n    InsecureDevMode = \"insecure_dev_mode\"\n\n\nclass ServerEndpointSecurityMode(enum.StrEnum):\n\n    Tls = \"tls\"\n    Optional = \"optional\"\n\n\nclass ServerTlsCertMode(enum.StrEnum):\n\n    RequireFile = \"require_file\"\n    SelfSigned = \"generate_self_signed\"\n\n\nclass JOSEKeyMode(enum.StrEnum):\n\n    RequireFile = \"require_file\"\n    Generate = \"generate\"\n\n\nclass ReadinessState(enum.StrEnum):\n\n    Default = \"default\"\n    \"\"\"Default state: serving normally\"\"\"\n\n    NotReady = \"not_ready\"\n    \"\"\"/server/status/ready returns an error, but clients can still connect.\"\"\"\n\n    ReadOnly = \"read_only\"\n    \"\"\"Only read-only queries are allowed.\"\"\"\n\n    Offline = \"offline\"\n    \"\"\"Any existing connections are gracefully terminated and no new\n    connections are accepted.\"\"\"\n\n    Blocked = \"blocked\"\n    \"\"\"Any existing connections are gracefully terminated and all\n    new connections are accepted but are immediately terminated\n    with a ServerBlockedError.\"\"\"\n\n\nclass ServerAuthMethod(enum.StrEnum):\n\n    Auto = \"auto\"\n    Trust = \"Trust\"\n    Scram = \"SCRAM\"\n    JWT = \"JWT\"\n    Password = \"Password\"\n    mTLS = \"mTLS\"\n\n\nclass ServerConnTransport(enum.StrEnum):\n\n    HTTP = \"HTTP\"\n    TCP = \"TCP\"\n    TCP_PG = \"TCP_PG\"\n    SIMPLE_HTTP = \"SIMPLE_HTTP\"\n    HTTP_METRICS = \"HTTP_METRICS\"\n    HTTP_HEALTH = \"HTTP_HEALTH\"\n\n\nclass ReloadTrigger(enum.StrEnum):\n    \"\"\"\n    Configure what triggers the reload of the following config files:\n    1. TLS certificate and key (server config)\n    2. JWS key (server config)\n    3. Multi-tenant config file (server config)\n    4. Readiness state (server or tenant config)\n    5. JWT sub allowlist and revocation list (server or tenant config)\n    6. The TOML config file (server or tenant config)\n    \"\"\"\n\n    Default = \"default\"\n    \"\"\"By default, reload on both SIGHUP and fsevent.\"\"\"\n\n    Never = \"never\"\n    \"\"\"Disable the reload function.\"\"\"\n\n    Signal = \"signal\"\n    \"\"\"Only reload on SIGHUP.\"\"\"\n\n    FileSystemEvent = \"fsevent\"\n    \"\"\"Watch the files for changes and reload when it happens.\"\"\"\n\n\nclass NetWorkerMode(enum.StrEnum):\n    Default = \"default\"\n    Disabled = \"disabled\"\n\n\nclass ServerAuthMethods:\n\n    def __init__(\n        self,\n        methods: Mapping[ServerConnTransport, list[ServerAuthMethod]],\n    ) -> None:\n        self._methods = dict(methods)\n\n    def get(self, transport: ServerConnTransport) -> list[ServerAuthMethod]:\n        return self._methods[transport]\n\n    def items(self) -> ItemsView[ServerConnTransport, list[ServerAuthMethod]]:\n        return self._methods.items()\n\n    def __str__(self):\n        return ','.join(\n            f'{t.lower()}:{'/'.join(m.lower() for m in mm)}'\n            for t, mm in self._methods.items()\n        )\n\n\nDEFAULT_AUTH_METHODS = ServerAuthMethods({\n    ServerConnTransport.TCP: [ServerAuthMethod.Scram],\n    ServerConnTransport.TCP_PG: [ServerAuthMethod.Scram],\n    ServerConnTransport.HTTP: [ServerAuthMethod.JWT],\n    ServerConnTransport.SIMPLE_HTTP: [\n        ServerAuthMethod.Password, ServerAuthMethod.JWT],\n    ServerConnTransport.HTTP_METRICS: [ServerAuthMethod.Auto],\n    ServerConnTransport.HTTP_HEALTH: [ServerAuthMethod.Auto],\n})\n\n\nclass BackendCapabilitySets(NamedTuple):\n    must_be_present: list[pgsql_params.BackendCapabilities]\n    must_be_absent: list[pgsql_params.BackendCapabilities]\n\n\nclass CompilerPoolMode(enum.StrEnum):\n    Default = \"default\"\n    Fixed = \"fixed\"\n    OnDemand = \"on_demand\"\n    Remote = \"remote\"\n    MultiTenant = \"fixed_multi_tenant\"\n\n    def __init__(self, name):\n        self.pool_class = None\n\n    def assign_implementation(self, cls):\n        # decorator function to link this enum with the actual implementation\n        self.pool_class = cls\n        return cls\n\n\nclass ServerConfig(NamedTuple):\n\n    data_dir: pathlib.Path\n    backend_dsn: str\n    backend_adaptive_ha: bool\n    tenant_id: Optional[str]\n    ignore_other_tenants: bool\n    multitenant_config_file: Optional[pathlib.Path]\n    log_level: str\n    log_to: str\n    bootstrap_only: bool\n    inplace_upgrade_prepare: Optional[pathlib.Path]\n    inplace_upgrade_finalize: bool\n    inplace_upgrade_rollback: bool\n    bootstrap_command: str\n    bootstrap_command_file: pathlib.Path\n    default_branch: Optional[str]\n    default_database: Optional[str]\n    default_database_user: Optional[str]\n    devmode: bool\n    testmode: bool\n    bind_addresses: list[str]\n    port: int\n    background: bool\n    pidfile_dir: pathlib.Path\n    daemon_user: str\n    daemon_group: str\n    runstate_dir: pathlib.Path\n    extensions_dir: tuple[pathlib.Path, ...]\n    max_backend_connections: Optional[int]\n    compiler_pool_size: int\n    compiler_worker_branch_limit: int\n    compiler_pool_mode: CompilerPoolMode\n    compiler_pool_addr: tuple[str, int]\n    compiler_pool_tenant_cache_size: int\n    compiler_worker_max_rss: Optional[int]\n\n    echo_runtime_info: bool\n    emit_server_status: str\n    temp_dir: bool\n    auto_shutdown_after: float\n    readiness_state_file: Optional[pathlib.Path]\n    disable_dynamic_system_config: bool\n    reload_config_files: ReloadTrigger\n    net_worker_mode: NetWorkerMode\n    config_file: Optional[pathlib.Path]\n\n    startup_script: Optional[StartupScript]\n    status_sinks: list[Callable[[str], None]]\n\n    tls_cert_file: pathlib.Path\n    tls_key_file: pathlib.Path\n    tls_cert_mode: ServerTlsCertMode\n    tls_client_ca_file: Optional[pathlib.Path]\n\n    jws_key_file: pathlib.Path\n    jose_key_mode: JOSEKeyMode\n    jwt_sub_allowlist_file: Optional[pathlib.Path]\n    jwt_revocation_list_file: Optional[pathlib.Path]\n\n    default_auth_method: ServerAuthMethods\n    security: ServerSecurityMode\n    binary_endpoint_security: ServerEndpointSecurityMode\n    http_endpoint_security: ServerEndpointSecurityMode\n\n    instance_name: str\n\n    backend_capability_sets: BackendCapabilitySets\n\n    admin_ui: bool\n\n    cors_always_allowed_origins: Optional[str]\n\n\nclass PathPath(click.Path):\n    name = 'path'\n\n    def convert(self, value, param, ctx):\n        return pathlib.Path(super().convert(value, param, ctx)).absolute()\n\n\nclass PortType(click.ParamType):\n    name = 'port'\n\n    def convert(self, value, param, ctx):\n        if value == 'auto':\n            return 0\n\n        try:\n            return int(value, 10)\n        except TypeError:\n            self.fail(\n                \"expected string for int() conversion, got \"\n                f\"{value!r} of type {type(value).__name__}\",\n                param,\n                ctx,\n            )\n        except ValueError:\n            self.fail(f\"{value!r} is not a valid integer\", param, ctx)\n\n\nclass BackendCapabilitySet(click.ParamType):\n    name = 'capability'\n\n    def __init__(self):\n        self.choices = {\n            cap.name: cap\n            for cap in pgsql_params.BackendCapabilities\n            if cap.name != 'NONE'\n        }\n\n    def get_metavar(self, param):\n        return \" \".join(f'[[~]{cap}]' for cap in self.choices)\n\n    def convert(self, value, param, ctx):\n        must_be_present = []\n        must_be_absent = []\n        visited = set()\n        for cap_str in value.split():\n            try:\n                if cap_str.startswith(\"~\"):\n                    cap = self.choices[cap_str[1:].upper()]\n                    must_be_absent.append(cap)\n                else:\n                    cap = self.choices[cap_str.upper()]\n                    must_be_present.append(cap)\n                if cap in visited:\n                    self.fail(f\"duplicate capability: {cap_str}\", param, ctx)\n                else:\n                    visited.add(cap)\n            except KeyError:\n                self.fail(\n                    f\"invalid capability: {cap_str}. \"\n                    f\"(choose from {', '.join(self.choices)})\",\n                    param,\n                    ctx,\n                )\n        return BackendCapabilitySets(\n            must_be_present=must_be_present,\n            must_be_absent=must_be_absent,\n        )\n\n\nclass CompilerPoolModeChoice(click.Choice):\n    def __init__(self):\n        super().__init__(\n            list(sorted(\n                set(CompilerPoolMode.__members__.values())\n                - {CompilerPoolMode.Remote}\n            )),\n        )\n\n    def convert(self, value, param, ctx):\n        if value == \"remote\":\n            return CompilerPoolMode.Remote\n        else:\n            return super().convert(value, param, ctx)\n\n\ndef _get_runstate_dir_default() -> str:\n    runstate_dir: Optional[str]\n\n    try:\n        runstate_dir = buildmeta.get_build_metadata_value(\"RUNSTATE_DIR\")\n    except buildmeta.MetadataError:\n        runstate_dir = None\n\n    if runstate_dir is None:\n        runstate_dir = '<data-dir>'\n\n    return runstate_dir\n\n\ndef _validate_max_backend_connections(ctx, param, value):\n    if value is not None and value < defines.BACKEND_CONNECTIONS_MIN:\n        raise click.BadParameter(\n            f'the minimum number of backend connections '\n            f'is {defines.BACKEND_CONNECTIONS_MIN}')\n    return value\n\n\ndef compute_default_max_backend_connections() -> int:\n    total_mem = psutil.virtual_memory().total\n    total_mem_mb = total_mem // MIB\n    if total_mem_mb <= 1024:\n        return defines.BACKEND_CONNECTIONS_MIN\n    else:\n        return max(\n            total_mem_mb // RAM_MIB_PER_CONN,\n            defines.BACKEND_CONNECTIONS_MIN,\n        )\n\n\ndef adjust_testmode_max_connections(max_conns):\n    # Some test cases will start a second EdgeDB server (default\n    # max_backend_connections=5), so we should reserve some backend\n    # connections for that. This is ideally calculated upon the edb test -j\n    # option, but that also depends on the total available memory. We are\n    # hard-coding 15 reserved connections here for simplicity.\n    return max(1, max_conns // 2, max_conns - 30)\n\n\ndef _validate_compiler_pool_size(ctx, param, value):\n    if value is not None and value < defines.BACKEND_COMPILER_POOL_SIZE_MIN:\n        raise click.BadParameter(\n            f'the minimum value for the compiler pool size option '\n            f'is {defines.BACKEND_COMPILER_POOL_SIZE_MIN}')\n    return value\n\n\ndef _validate_compiler_pool_host_port(ctx, param, value):\n    if value is None:\n        return None\n    address = value.split(\":\", 1)\n    if len(address) == 1:\n        return address[0], defines.EDGEDB_REMOTE_COMPILER_PORT\n    else:\n        try:\n            return address[0], int(address[1])\n        except ValueError:\n            raise click.BadParameter(f'port must be int: {address[1]}')\n\n\ndef compute_default_compiler_pool_size() -> int:\n    total_mem = psutil.virtual_memory().total\n    total_mem_mb = total_mem // MIB\n    if total_mem_mb <= 1024:\n        return defines.BACKEND_COMPILER_POOL_SIZE_MIN\n    else:\n        return max(\n            psutil.cpu_count(logical=False) or 0,\n            defines.BACKEND_COMPILER_POOL_SIZE_MIN,\n        )\n\n\ndef _validate_tenant_id(ctx, param, value):\n    if value is not None:\n        if len(value) > schema_defines.MAX_TENANT_ID_LENGTH:\n            raise click.BadParameter(\n                f'cannot be longer than'\n                f' {schema_defines.MAX_TENANT_ID_LENGTH} characters')\n        if not value.isalnum() or not value.isascii():\n            raise click.BadParameter(\n                f'contains invalid characters')\n\n    return value\n\n\ndef _status_sink_file(path: str) -> Callable[[str], None]:\n    def _writer(status: str) -> None:\n        try:\n            with open(path, 'a') as f:\n                print(status, file=f, flush=True)\n        except OSError as e:\n            logger.warning(\n                f'could not write server status to {path!r}: {e.strerror}')\n        except Exception as e:\n            logger.warning(\n                f'could not write server status to {path!r}: {e}')\n\n    return _writer\n\n\ndef _status_sink_fd(fileno: int) -> Callable[[str], None]:\n    def _writer(status: str) -> None:\n        try:\n            with open(fileno, mode='a', closefd=False) as f:\n                print(status, file=f, flush=True)\n        except OSError as e:\n            logger.warning(\n                f'could not write server status to fd://{fileno!r}: '\n                f'{e.strerror}')\n        except Exception as e:\n            logger.warning(\n                f'could not write server status to fd://{fileno!r}: {e}')\n\n    return _writer\n\n\ndef _validate_default_auth_method(\n    ctx: click.Context,\n    param: click.Option | click.Parameter,\n    value: Any,\n) -> ServerAuthMethods | None:\n    if value is None:\n        return None\n\n    methods = dict(DEFAULT_AUTH_METHODS.items())\n\n    names = {v.lower(): v for v in ServerAuthMethod.__members__.values()}\n    method = names.get(value.lower())\n    if method is not None:\n        # Single auth method value.\n        #\n        # HTTP does not support SCRAM, but for backward compatibility\n        # if SCRAM is passed explicitly, default HTTP to JWT.\n        if method in {ServerAuthMethod.Auto, ServerAuthMethod.Scram}:\n            pass\n        else:\n            for t in methods:\n                # HTTP_METRICS and HTTP_HEALTH support only mTLS, but for\n                # backward compatibility, default them to `auto` if unsupported\n                # method is passed explicitly.\n                if t in (\n                    ServerConnTransport.HTTP_METRICS,\n                    ServerConnTransport.HTTP_HEALTH,\n                ):\n                    if method not in (\n                        ServerAuthMethod.Trust,\n                        ServerAuthMethod.mTLS,\n                    ):\n                        continue\n\n                methods[t] = [method]\n    elif \",\" not in value and \":\" not in value:\n        raise click.BadParameter(\n            f\"invalid authentication method: {value}, \"\n            f\"supported values are: {', '.join(names)})\"\n        )\n    else:\n        # Per-transport configuration.\n        transport_specs = value.split(\",\")\n        transport_names = {\n            v.lower(): v\n            for v in ServerConnTransport.__members__.values()\n        }\n        for transport_spec in transport_specs:\n            transport_spec = transport_spec.strip()\n            transport_name, _, method_names = transport_spec.partition(':')\n            if not method_names:\n                raise click.BadParameter(\n                    \"format is <transport>:<method>[/method...][,...]\")\n            transport = transport_names.get(transport_name.lower())\n            if not transport:\n                raise click.BadParameter(\n                    f\"invalid connection transport: {transport_name}, \"\n                    f\"supported values are: {', '.join(transport_names)})\"\n                )\n            transport_methods = []\n            for method_name in method_names.split('/'):\n                method = names.get(method_name)\n                if not method:\n                    raise click.BadParameter(\n                        f\"invalid authentication method: {method_name}, \"\n                        f\"supported values are: {', '.join(names)})\"\n                    )\n                transport_methods.append(method)\n            methods[transport] = transport_methods\n\n    return ServerAuthMethods(methods)\n\n\ndef oxford_comma(els: Sequence[str]) -> str:\n    '''Who gives a fuck?'''\n    assert els\n    if len(els) == 1:\n        return els[0]\n    elif len(els) == 2:\n        return f'{els[0]} and {els[1]}'\n    else:\n        return f'{\", \".join(els[:-1])}, and {els[-1]}'\n\n\nclass EnvvarResolver(click.Option):\n    def resolve_envvar_value(self, ctx: click.Context):\n        if self.envvar is None:\n            return None\n\n        if not isinstance(self.envvar, str):\n            raise click.BadParameter(\n                \"expected a single envvar value but got multiple\")\n\n        file_var = f'{self.envvar}_FILE'\n        alt_var = f'{self.envvar}_ENV'\n\n        old_envvar = self.envvar.replace('GEL_', 'EDGEDB_')\n        old_file_var = f'{old_envvar}_FILE'\n        old_alt_var = f'{old_envvar}_ENV'\n\n        vars_set = []\n        for var, old_var in [\n            (self.envvar, old_envvar),\n            (file_var, old_file_var),\n            (alt_var, old_alt_var),\n        ]:\n            if var in os.environ and old_var in os.environ:\n                print(\n                    f\"Warning: both {var} and {old_var} are specified. \"\n                    f\"{var} will take precedence.\"\n                )\n            if var in os.environ:\n                vars_set.append(var)\n            elif old_var in os.environ:\n                vars_set.append(old_var)\n\n        if len(vars_set) > 1:\n            amt = \"both\" if len(vars_set) == 2 else \"all\"\n            raise click.BadParameter(\n                f'{oxford_comma(vars_set)} are exclusive, '\n                f'but {amt} are set.'\n            )\n\n        var_val = os.environ.get(self.envvar) or os.environ.get(old_envvar)\n        alt_var_val = os.environ.get(alt_var) or os.environ.get(old_alt_var)\n        file_var_val = os.environ.get(file_var) or os.environ.get(old_file_var)\n\n        if alt_var_val:\n            var_val = os.environ.get(alt_var_val)\n\n        if var_val:\n            return var_val\n\n        if file_var_val:\n            try:\n                with open(file_var_val, 'rt') as f:\n                    return f.read()\n            except Exception as e:\n                raise click.BadParameter(\n                    f'could not read the file specified by '\n                    f'{file_var} ({file_var_val!r})') from e\n\n        return None\n\n\nserver_options = typeutils.chain_decorators([\n    click.option(\n        '-D', '--data-dir', type=PathPath(),\n        envvar=\"GEL_SERVER_DATADIR\", cls=EnvvarResolver,\n        help='database cluster directory'),\n    click.option(\n        '--postgres-dsn', type=str, hidden=True,\n        help='[DEPRECATED] DSN of a remote Postgres cluster, if using one'),\n    click.option(\n        '--backend-dsn', type=str,\n        envvar=\"GEL_SERVER_BACKEND_DSN\", cls=EnvvarResolver,\n        help='DSN of a remote backend cluster, if using one. '\n             'Also supports HA clusters, for example: stolon+consul+http://'\n             'localhost:8500/test_cluster'),\n    click.option(\n        '--enable-backend-adaptive-ha', 'backend_adaptive_ha', is_flag=True,\n        help='If backend adaptive HA is enabled, the Gel server will '\n             'monitor the health of the backend cluster and shutdown all '\n             'backend connections if threshold is reached, until reconnected '\n             'again using the same DSN (HA should have updated the DNS '\n             'value). Default is disabled.'),\n    click.option(\n        '--tenant-id',\n        type=str,\n        callback=_validate_tenant_id,\n        envvar=\"GEL_SERVER_TENANT_ID\",\n        cls=EnvvarResolver,\n        help='Specifies the tenant ID of this server when hosting'\n             ' multiple Gel instances on one Postgres cluster.'\n             ' Must be an alphanumeric ASCII string, maximum'\n             f' {schema_defines.MAX_TENANT_ID_LENGTH} characters long.',\n    ),\n    click.option(\n        '--ignore-other-tenants',\n        is_flag=True,\n        help='If set, the server will ignore the presence of another tenant '\n             'in the database instance in single-tenant mode instead of '\n             'exiting with a catalog incompatibility error.'\n    ),\n    click.option(\n        '--multitenant-config-file', type=PathPath(), metavar=\"PATH\",\n        envvar=\"GEL_SERVER_MULTITENANT_CONFIG_FILE\",\n        cls=EnvvarResolver,\n        hidden=True,\n        help='Start the server in multi-tenant mode, with reloadable tenants '\n             'configured in the given file. Each tenant must have a unique '\n             'SNI name as the key to route the traffic correctly, as well as '\n             'a dedicated backend DSN to host the tenant data. See edb/server/'\n             'multitenant.py for config file format. All tenants share the '\n             'same compiler pool, thus the same stdlib. So if any of the '\n             'backends contains test-mode schema, the server should be '\n             'started with --testmode to handle them properly.',\n    ),\n    click.option(\n        '-l', '--log-level',\n        envvar=\"GEL_SERVER_LOG_LEVEL\",\n        cls=EnvvarResolver,\n        default='i',\n        type=click.Choice(\n            ['debug', 'd', 'info', 'i', 'warn', 'w',\n             'error', 'e', 'silent', 's'],\n            case_sensitive=False,\n        ),\n        help=(\n            'Logging level.  Possible values: (d)ebug, (i)nfo, (w)arn, '\n            '(e)rror, (s)ilent'\n        )),\n    click.option(\n        '--log-to',\n        help=('send logs to DEST, where DEST can be a file name, \"syslog\", '\n              'or \"stderr\"'),\n        type=str, metavar='DEST', default='stderr'),\n    click.option(\n        '--bootstrap', is_flag=True, hidden=True,\n        help='[DEPRECATED] bootstrap the database cluster and exit'),\n    click.option(\n        '--bootstrap-only', is_flag=True,\n        envvar=\"GEL_SERVER_BOOTSTRAP_ONLY\", cls=EnvvarResolver,\n        help='bootstrap the database cluster and exit'),\n    click.option(\n        '--inplace-upgrade-prepare', type=PathPath(),\n        envvar=\"GEL_SERVER_INPLACE_UPGRADE_PREPARE\",\n        cls=EnvvarResolver,\n        help='try to do an in-place upgrade with the specified dump file'),\n    click.option(\n        '--inplace-upgrade-rollback', type=bool, is_flag=True,\n        envvar=\"GEL_SERVER_INPLACE_UPGRADE_ROLLBACK\",\n        cls=EnvvarResolver,\n        help='rollback a prepared upgrade'),\n    click.option(\n        '--inplace-upgrade-finalize', type=bool, is_flag=True,\n        envvar=\"GEL_SERVER_INPLACE_UPGRADE_FINALIZE\",\n        cls=EnvvarResolver,\n        help='finalize an in-place upgrade'),\n    click.option(\n        '--default-branch', type=str,\n        help='the name of the default branch to create'),\n    click.option(\n        '--default-database', type=str, hidden=True,\n        help='[DEPRECATED] the name of the default database to create'),\n    click.option(\n        '--default-database-user', type=str, hidden=True,\n        help='[DEPRECATED] the name of the default database owner'),\n    click.option(\n        '--bootstrap-command', metavar=\"QUERIES\",\n        envvar=\"GEL_SERVER_BOOTSTRAP_COMMAND\", cls=EnvvarResolver,\n        help='run the commands when initializing the database. '\n             'Queries are executed by default user within default '\n             'database. May be used with or without `--bootstrap-only`.'),\n    click.option(\n        '--bootstrap-command-file', type=PathPath(), metavar=\"PATH\",\n        help='run the script when initializing the database. '\n             'Script run by default user within default database. '\n             'May be used with or without `--bootstrap-only`.'),\n    click.option(\n        '--bootstrap-script', type=PathPath(),\n        help='[DEPRECATED] use --bootstrap-command-file instead.'),\n    click.option(\n        '--devmode/--no-devmode',\n        help='enable or disable the development mode',\n        default=None),\n    click.option(\n        '--testmode/--no-testmode',\n        help='enable or disable the test mode',\n        default=False),\n    click.option(\n        '-I', '--bind-address', type=str, multiple=True,\n        envvar=\"GEL_SERVER_BIND_ADDRESS\", cls=EnvvarResolver,\n        help='IP addresses to listen on, specify multiple times for more than '\n             'one address to listen on'),\n    click.option(\n        '-P', '--port', type=PortType(), default=None,\n        envvar=\"GEL_SERVER_PORT\", cls=EnvvarResolver,\n        help='port to listen on'),\n    click.option(\n        '-b', '--background', is_flag=True, help='daemonize'),\n    click.option(\n        '--pidfile-dir', type=PathPath(), default=None,\n        help='path to PID file directory, defaults to --runstate-dir'),\n    click.option(\n        '--daemon-user', type=int),\n    click.option(\n        '--daemon-group', type=int),\n    click.option(\n        '--runstate-dir', type=PathPath(), default=None,\n        envvar=\"GEL_SERVER_RUNSTATE_DIR\",\n        cls=EnvvarResolver,\n        help=f'directory where UNIX sockets and other temporary '\n             f'runtime files will be placed ({_get_runstate_dir_default()} '\n             f'by default)'),\n    click.option(\n        '--extensions-dir', type=PathPath(), default=(), multiple=True,\n        envvar=\"GEL_SERVER_EXTENSIONS_DIR\",\n        cls=EnvvarResolver,\n        help=f'directory where third-party extension packages are loaded from'),\n    click.option(\n        '--max-backend-connections', type=int, metavar='NUM',\n        envvar=\"GEL_SERVER_MAX_BACKEND_CONNECTIONS\",\n        cls=EnvvarResolver,\n        help=f'The maximum NUM of connections this Gel instance could make '\n             f'to the backend PostgreSQL cluster. If not set, Gel will '\n             f'detect and calculate the NUM: RAM/100MiB='\n             f'{compute_default_max_backend_connections()} for local '\n             f'Postgres or pg_settings.max_connections for remote Postgres, '\n             f'minus the NUM of --reserved-pg-connections.',\n        callback=_validate_max_backend_connections),\n    click.option(\n        '--compiler-pool-size', type=int, metavar='NUM',\n        envvar=\"GEL_SERVER_COMPILER_POOL_SIZE\",\n        cls=EnvvarResolver,\n        callback=_validate_compiler_pool_size,\n        help='Size of the compiler pool.  When --compiler-pool-mode=fixed or '\n             'fixed_multi_tenant, it is the NUM of compiler worker processes, '\n             f\"defaults to {compute_default_compiler_pool_size()} (you'll see \"\n             '1 extra template process); for on_demand, it is the maximum NUM '\n             'of workers the pool could scale up to, with the same default; '\n             'for remote, it is the maximum NUM of concurrent requests to the '\n             'remote compiler server, defaults to 2.'\n    ),\n    click.option(\n        '--compiler-worker-branch-limit', type=int, metavar='NUM',\n        default=5,\n        envvar=\"GEL_SERVER_COMPILER_WORKER_BRANCH_LIMIT\",\n        cls=EnvvarResolver,\n        help='The maximum NUM of branches each compiler worker could cache up '\n             'to, default is 5.  If the worker serves multiple tenants (as in '\n             '--compiler-pool-mode=fixed_multi_tenant or remote), this tenant '\n             'on that worker will be able to cache up to NUM branches.'\n    ),\n    click.option(\n        '--compiler-pool-mode',\n        type=CompilerPoolModeChoice(),\n        default=CompilerPoolMode.Default.value,\n        envvar=\"GEL_SERVER_COMPILER_POOL_MODE\",\n        cls=EnvvarResolver,\n        help='Choose a mode for the compiler pool to scale. \"fixed\" means the '\n             'pool will not scale and sticks to --compiler-pool-size, while '\n             '\"on_demand\" means the pool will maintain at least 1 worker and '\n             'automatically scale up (to --compiler-pool-size workers ) and '\n             'down to the demand. Defaults to \"fixed\" in production mode and '\n             '\"on_demand\" in development mode.',\n    ),\n    click.option(\n        '--compiler-pool-addr',\n        hidden=True,\n        callback=_validate_compiler_pool_host_port,\n        envvar=\"GEL_SERVER_COMPILER_POOL_ADDR\",\n        cls=EnvvarResolver,\n        help=f'Specify the host[:port] of the compiler pool to connect to, '\n             f'only used if --compiler-pool-mode=remote. Default host is '\n             f'localhost, port is {defines.EDGEDB_REMOTE_COMPILER_PORT}',\n    ),\n    click.option(\n        \"--compiler-pool-tenant-cache-size\",\n        hidden=True,\n        type=int,\n        default=20,\n        envvar=\"GEL_SERVER_COMPILER_POOL_TENANT_CACHE_SIZE\",\n        cls=EnvvarResolver,\n        help=\"Maximum number of tenants for which each compiler worker can \"\n             \"cache their schemas, \"\n             \"only used when --compiler-pool-mode=fixed_multi_tenant\"\n    ),\n    click.option(\n        '--echo-runtime-info', type=bool, default=False, is_flag=True,\n        help='[DEPREATED, use --emit-server-status] '\n             'echo runtime info to stdout; the format is JSON, prefixed by '\n             '\"EDGEDB_SERVER_DATA:\", ended with a new line'),\n    click.option(\n        '--emit-server-status',\n        type=str, default=None, metavar='DEST', multiple=True,\n        help='Instruct the server to emit changes in status to DEST, '\n             'where DEST is a URI specifying a file (file://<path>), '\n             'or a file descriptor (fd://<fileno>).  If the URI scheme '\n             'is not specified, file:// is assumed.'),\n    click.option(\n        '--temp-dir', type=bool, default=False, is_flag=True,\n        help='create a temporary database cluster directory '\n             'that will be automatically purged on server shutdown'),\n    click.option(\n        '--auto-shutdown', type=bool, default=False, is_flag=True, hidden=True,\n        help='shutdown the server after the last ' +\n             'connection is closed'),\n    click.option(\n        '--auto-shutdown-after', type=float, default=-1.0, metavar='N',\n        help='shutdown the server if no client connections were made in the '\n             'last N seconds, if N = 0, shut down after the last client has '\n             'disconnected, N < 0 (default) means no auto shutdown'),\n    click.option(\n        '--tls-cert-file',\n        type=PathPath(),\n        envvar=\"GEL_SERVER_TLS_CERT_FILE\",\n        cls=EnvvarResolver,\n        help='Specifies a path to a file containing a server TLS certificate '\n             'in PEM format, as well as possibly any number of CA '\n             'certificates needed to establish the certificate '\n             'authenticity.  If the file does not exist and the '\n             '--tls-cert-mode option is set to \"generate_self_signed\", a '\n             'self-signed certificate will be automatically created in '\n             'the specified path.'),\n    click.option(\n        '--tls-key-file',\n        type=PathPath(),\n        envvar=\"GEL_SERVER_TLS_KEY_FILE\",\n        cls=EnvvarResolver,\n        help='Specifies a path to a file containing the private key in PEM '\n             'format.  If the file does not exist and the --tls-cert-mode '\n             'option is set to \"generate_self_signed\", the private key will '\n             'be automatically created in the specified path.'),\n    click.option(\n        '--tls-cert-mode',\n        envvar=\"GEL_SERVER_TLS_CERT_MODE\", cls=EnvvarResolver,\n        type=click.Choice(\n            ['default'] + list(ServerTlsCertMode.__members__.values()),\n            case_sensitive=True,\n        ),\n        default='default',\n        help='Specifies what to do when the TLS certificate and key are '\n             'either not specified or are missing.  When set to '\n             '\"require_file\", the TLS certificate and key must be specified '\n             'in the --tls-cert-file and --tls-key-file options and both must '\n             'exist.  When set to \"generate_self_signed\" a new self-signed '\n             'certificate and private key will be generated and placed in the '\n             'path specified by --tls-cert-file/--tls-key-file, if those are '\n             'set, otherwise the generated certificate and key are stored as '\n             f'`{TLS_CERT_FILE_NAME}` and `{TLS_KEY_FILE_NAME}` in the data '\n             'directory, or, if the server is running with --backend-dsn, '\n             'in a subdirectory of --runstate-dir.\\n\\nThe default is '\n             '\"require_file\" when the --security option is set to \"strict\", '\n             'and \"generate_self_signed\" when the --security option is set to '\n             '\"insecure_dev_mode\"'),\n    click.option(\n        '--tls-client-ca-file',\n        type=PathPath(),\n        envvar='EDGEDB_SERVER_TLS_CLIENT_CA_FILE',\n        cls=EnvvarResolver,\n        help='Specifies a path to a file containing a TLS CA certificate to '\n             'verify client certificates on demand. When set, the default '\n             'authentication method of HTTP_METRICS(/metrics) and HTTP_HEALTH'\n             '(/server/*) will also become \"mTLS\", unless explicitly set in '\n             '--default-auth-method. Note, the protection of such HTTP '\n             'endpoints is only complete if --http-endpoint-security is also '\n             'set to `tls`, or they are still accessible in plaintext HTTP.'\n    ),\n    click.option(\n        '--generate-self-signed-cert', type=bool, default=False, is_flag=True,\n        help='DEPRECATED.\\n\\n'\n             'Use --tls-cert-mode=generate_self_signed instead.'),\n    click.option(\n        '--binary-endpoint-security',\n        envvar=\"GEL_SERVER_BINARY_ENDPOINT_SECURITY\",\n        cls=EnvvarResolver,\n        type=click.Choice(\n            ['default', 'tls', 'optional'],\n            case_sensitive=True,\n        ),\n        default='default',\n        help='Specifies the security mode of server binary endpoint. '\n             'When set to `optional`, non-TLS connections are allowed. '\n             'The default is `tls`.',\n    ),\n    click.option(\n        '--http-endpoint-security',\n        envvar=\"GEL_SERVER_HTTP_ENDPOINT_SECURITY\",\n        cls=EnvvarResolver,\n        type=click.Choice(\n            ['default', 'tls', 'optional'],\n            case_sensitive=True,\n        ),\n        default='default',\n        help='Specifies the security mode of server HTTP endpoint. '\n             'When set to `optional`, non-TLS connections are allowed. '\n             'The default is `tls`.',\n    ),\n    click.option(\n        '--security',\n        envvar=\"GEL_SERVER_SECURITY\",\n        cls=EnvvarResolver,\n        type=click.Choice(\n            ['default', 'strict', 'insecure_dev_mode'],\n            case_sensitive=True,\n        ),\n        default='default',\n        help=(\n            'When set to `insecure_dev_mode`, sets the default '\n            'authentication method to `Trust`, enables non-TLS '\n            'client HTTP connections, and implies '\n            '`--tls-cert-mode=generate_self_signed`.  The default is `strict`.'\n        ),\n    ),\n    click.option(\n        '--jws-key-file',\n        type=PathPath(),\n        envvar=\"GEL_SERVER_JWS_KEY_FILE\",\n        cls=EnvvarResolver,\n        hidden=True,\n        help='Specifies a path to a file containing a public key in PEM '\n             'or JSON JWK format used to verify JWT signatures. The file may '\n             'also contain a private key to sign JWT tokens for '\n             'SCRAM-over-HTTP.'),\n    click.option(\n        '--jwe-key-file',\n        type=PathPath(),\n        hidden=True,\n        help='Deprecated: no longer in use.'),\n    click.option(\n        '--jose-key-mode',\n        envvar=\"GEL_SERVER_JOSE_KEY_MODE\", cls=EnvvarResolver,\n        type=click.Choice(\n            ['default'] + list(JOSEKeyMode.__members__.values()),\n            case_sensitive=True,\n        ),\n        hidden=True,\n        default='default',\n        help='Specifies what to do when the JOSE keys are either not '\n             'specified or are missing.  When set to \"require_file\", the JOSE '\n             'keys must be specified in the --jws-key-file and the file must '\n             'exist.  When set to \"generate\", a new key pair will be '\n             'generated and placed in the path specified by --jws-key-file, '\n             'if those are set, otherwise the generated key pairs are stored '\n             f'as `{JWS_KEY_FILE_NAME}` in the data directory, or, if the '\n             'server is running with --backend-dsn, in a subdirectory of '\n             '--runstate-dir.\\n\\nThe default is \"require_file\" when the '\n             '--security option is set to \"strict\", and \"generate\" when the '\n             '--security option is set to \"insecure_dev_mode\"'),\n    click.option(\n        '--jwt-sub-allowlist-file',\n        type=PathPath(),\n        envvar=\"GEL_SERVER_JWT_SUB_ALLOWLIST_FILE\",\n        cls=EnvvarResolver,\n        hidden=True,\n        help='A file where the server can obtain a list of all JWT subjects '\n             'that are allowed to access this instance. '\n             'The file must contain one JWT \"sub\" claim value per line. '\n             'Applies only to the JWT authentication method.'\n    ),\n    click.option(\n        '--jwt-revocation-list-file',\n        type=PathPath(),\n        envvar=\"GEL_SERVER_JWT_REVOCATION_LIST_FILE\",\n        cls=EnvvarResolver,\n        hidden=True,\n        help='A file where the server can obtain a list of all JWT ids '\n             'that are allowed to access this instance. '\n             'The file must contain one JWT \"jti\" claim value per line. '\n             'Applies only to the JWT authentication method.'\n    ),\n    click.option(\n        \"--default-auth-method\",\n        envvar=\"GEL_SERVER_DEFAULT_AUTH_METHOD\", cls=EnvvarResolver,\n        callback=_validate_default_auth_method,\n        type=str,\n        help=(\n            \"The default authentication method to use when none is \"\n            \"explicitly configured. Defaults to 'auto', which means \"\n            \"the SCRAM authentication method for TCP connections and \"\n            \"the JWT authentication method for HTTP-tunneled connections.\"\n        ),\n    ),\n    click.option(\n        \"--readiness-state-file\",\n        envvar=\"GEL_SERVER_READINESS_STATE_FILE\",\n        cls=EnvvarResolver,\n        type=PathPath(),\n        help=(\n            \"Path to a file containing the value for server readiness state. \"\n            \"When it contains 'not_ready' (without quotes), the server will \"\n            \"refuse connections and the '/server/status/ready' check will \"\n            \"return a 503 status.  Every other value, including absense of \"\n            \"file indicates that the server is in the 'ready' state and \"\n            \"can server connections.  The file can be modified when the \"\n            \"server is running.\"\n        ),\n    ),\n    click.option(\n        '--instance-name',\n        envvar=\"GEL_SERVER_INSTANCE_NAME\",\n        cls=EnvvarResolver,\n        type=str, default=None, hidden=True,\n        help='Server instance name.'),\n    click.option(\n        '--backend-capabilities',\n        envvar=\"GEL_SERVER_BACKEND_CAPABILITIES\",\n        cls=EnvvarResolver,\n        type=BackendCapabilitySet(),\n        help=\"A space-separated set of backend capabilities, which are \"\n             \"required to be present, or absent if prefixed with ~. Gel \"\n             \"will only start if the actual backend capabilities match the \"\n             \"specified set. However if the backend was never bootstrapped, \"\n             \"the capabilities prefixed with ~ will be *disabled permanently* \"\n             \"in Gel as if the backend never had them.\"\n    ),\n    click.option(\n        '--version', is_flag=True,\n        help='Show the version and exit.'),\n    click.option(\n        '--admin-ui',\n        envvar=\"GEL_SERVER_ADMIN_UI\",\n        cls=EnvvarResolver,\n        type=click.Choice(\n            ['default', 'enabled', 'disabled'],\n            case_sensitive=True,\n        ),\n        default='default',\n        help='Enable admin UI.'),\n    click.option(\n        '--cors-always-allowed-origins',\n        envvar=\"GEL_SERVER_CORS_ALWAYS_ALLOWED_ORIGINS\",\n        cls=EnvvarResolver,\n        hidden=True,\n        help='A comma separated list of origins to always allow CORS requests '\n             'from regardless of the `cors_allow_orgin` config. The `*` '\n             'character can be used as a wildcard. Intended for use by cloud '\n             'to always allow the cloud UI to make requests to the instance.'\n    ),\n    click.option(\n        '--disable-dynamic-system-config', is_flag=True,\n        envvar=\"GEL_SERVER_DISABLE_DYNAMIC_SYSTEM_CONFIG\",\n        cls=EnvvarResolver,\n        help=\"Disable dynamic configuration of system config values\",\n    ),\n    click.option(\n        \"--reload-config-files\",\n        envvar=\"GEL_SERVER_RELOAD_CONFIG_FILES\", cls=EnvvarResolver,\n        type=click.Choice(\n            list(ReloadTrigger.__members__.values()), case_sensitive=True\n        ),\n        hidden=True,\n        default='default',\n        help='Specifies when to reload the config files. See the docstring of '\n             'ReloadTrigger for more information.',\n    ),\n    click.option(\n        \"--net-worker-mode\",\n        envvar=\"GEL_SERVER_NET_WORKER_MODE\", cls=EnvvarResolver,\n        type=click.Choice(\n            list(NetWorkerMode.__members__.values()), case_sensitive=True\n        ),\n        hidden=True,\n        default='default',\n        help='Controls how the std::net workers work.',\n    ),\n    click.option(\n        \"--config-file\", type=PathPath(), metavar=\"PATH\",\n        envvar=\"GEL_SERVER_CONFIG_FILE\",\n        cls=EnvvarResolver,\n        help='Path to a TOML file to configure the server.',\n        hidden=True,\n    ),\n    click.option(\n        '--compiler-worker-max-rss',\n        type=int,\n        envvar=\"GEL_SERVER_COMPILER_WORKER_MAX_RSS\",\n        cls=EnvvarResolver,\n        help='Maximum allowed RSS (in bytes) per compiler worker process. Any '\n             'worker exceeding this limit will be terminated and recreated. '\n             'Each worker is free from this limit in its first 20-30 hours '\n             'after spawn to avoid infinite restarts or a thundering herd.',\n    ),\n])\n\n\ncompiler_options = typeutils.chain_decorators([\n    click.option(\n        \"--pool-size\",\n        type=int,\n        envvar=\"GEL_COMPILER_POOL_SIZE\",\n        cls=EnvvarResolver,\n        callback=_validate_compiler_pool_size,\n        default=compute_default_compiler_pool_size(),\n        help=f\"Number of compiler worker processes. Defaults to \"\n             f\"{compute_default_compiler_pool_size()}.\",\n    ),\n    click.option(\n        \"--client-schema-cache-size\",\n        type=int,\n        envvar=\"GEL_COMPILER_POOL_TENANT_CACHE_SIZE\",\n        cls=EnvvarResolver,\n        default=20,\n        help=\"Maximum number of clients for which each worker can cache their \"\n             \"schemas, The compiler server is not affected by this setting, \"\n             \"it keeps pickled copies of schemas from all active clients \"\n             \"(each capped by --compiler-worker-branch-limit of the client).\"\n    ),\n    click.option(\n        '-I', '--listen-addresses', type=str, multiple=True,\n        envvar=\"GEL_COMPILER_BIND_ADDRESS\", cls=EnvvarResolver,\n        default=('localhost',),\n        help='IP addresses to listen on, specify multiple times for more than '\n             'one address to listen on. Default: localhost',\n    ),\n    click.option(\n        '-P', '--listen-port', type=PortType(),\n        envvar=\"GEL_COMPILER_SERVER_PORT\", cls=EnvvarResolver,\n        help=f'Port to listen on. '\n             f'Default: {defines.EDGEDB_REMOTE_COMPILER_PORT}',\n    ),\n    click.option(\n        '--runstate-dir', type=PathPath(), default=None,\n        envvar=\"GEL_COMPILER_RUNSTATE_DIR\",\n        cls=EnvvarResolver,\n        help=\"Directory to store UNIX domain socket file for IPC, a temporary \"\n             \"directory will be used if not specified.\",\n    ),\n    click.option(\n        '--metrics-port', type=PortType(),\n        envvar=\"GEL_COMPILER_METRICS_PORT\",\n        cls=EnvvarResolver,\n        help=f'Port to listen on for metrics HTTP API.',\n    ),\n    click.option(\n        '--worker-max-rss',\n        type=int,\n        envvar=\"GEL_COMPILER_WORKER_MAX_RSS\",\n        cls=EnvvarResolver,\n        help='Maximum allowed RSS (in bytes) per worker process. Any worker '\n             'exceeding this limit will be terminated and recreated. '\n             'Each worker is free from this limit in its first 20-30 hours '\n             'after spawn to avoid infinite restarts or a thundering herd.',\n    ),\n])\n\n\ndef parse_args(**kwargs: Any):\n    kwargs['bind_addresses'] = kwargs.pop('bind_address')\n\n    if kwargs['echo_runtime_info']:\n        warnings.warn(\n            \"The `--echo-runtime-info` option is deprecated, use \"\n            \"`--emit-server-status` instead.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n\n    if kwargs['bootstrap']:\n        warnings.warn(\n            \"Option `--bootstrap` is deprecated, use `--bootstrap-only`\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        kwargs['bootstrap_only'] = True\n\n    kwargs.pop('bootstrap', False)\n\n    if kwargs['default_database_user']:\n        if kwargs['default_database_user'] == 'edgedb':\n            warnings.warn(\n                \"Option `--default-database-user` is deprecated.\"\n                \" Role `edgedb` is always created and\"\n                \" no role named after unix user is created any more.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n        else:\n            warnings.warn(\n                \"Option `--default-database-user` is deprecated.\"\n                \" Please create the role explicitly.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n\n    if kwargs['default_database']:\n        if kwargs['default_database'] == 'edgedb':\n            warnings.warn(\n                \"Option `--default-database` is deprecated.\"\n                \" Database `edgedb` is always created and\"\n                \" no database named after unix user is created any more.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n        else:\n            warnings.warn(\n                \"Option `--default-database` is deprecated.\"\n                \" Please create the database explicitly.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n\n    if kwargs['auto_shutdown']:\n        warnings.warn(\n            \"The `--auto-shutdown` option is deprecated, use \"\n            \"`--auto-shutdown-after` instead.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        if kwargs['auto_shutdown_after'] < 0:\n            kwargs['auto_shutdown_after'] = 0\n\n    del kwargs['auto_shutdown']\n\n    if kwargs['postgres_dsn']:\n        warnings.warn(\n            \"The `--postgres-dsn` option is deprecated, use \"\n            \"`--backend-dsn` instead.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        if not kwargs['backend_dsn']:\n            kwargs['backend_dsn'] = kwargs['postgres_dsn']\n\n    del kwargs['postgres_dsn']\n\n    if kwargs['generate_self_signed_cert']:\n        warnings.warn(\n            \"The `--generate-self-signed-cert` option is deprecated, use \"\n            \"`--tls-cert-mode=generate_self_signed` instead.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        if kwargs['tls_cert_mode'] == 'default':\n            kwargs['tls_cert_mode'] = 'generate_self_signed'\n\n    del kwargs['generate_self_signed_cert']\n\n    if os.environ.get('EDGEDB_SERVER_ALLOW_INSECURE_BINARY_CLIENTS') == \"1\":\n        if kwargs['binary_endpoint_security'] == \"tls\":\n            abort(\n                \"The value of deprecated \"\n                \"EDGEDB_SERVER_ALLOW_INSECURE_BINARY_CLIENTS environment \"\n                \"variable disagrees with --binary-endpoint-security\"\n            )\n        else:\n            if kwargs['binary_endpoint_security'] == \"default\":\n                warnings.warn(\n                    \"EDGEDB_SERVER_ALLOW_INSECURE_BINARY_CLIENTS is \"\n                    \"deprecated. Use EDGEDB_SERVER_BINARY_ENDPOINT_SECURITY \"\n                    \"instead.\",\n                    DeprecationWarning,\n                    stacklevel=2,\n                )\n            kwargs['binary_endpoint_security'] = 'optional'\n\n    if os.environ.get('EDGEDB_SERVER_ALLOW_INSECURE_HTTP_CLIENTS') == \"1\":\n        if kwargs['http_endpoint_security'] == \"tls\":\n            abort(\n                \"The value of deprecated \"\n                \"EDGEDB_SERVER_ALLOW_INSECURE_HTTP_CLIENTS environment \"\n                \"variable disagrees with --http-endpoint-security\"\n            )\n        else:\n            if kwargs['http_endpoint_security'] == \"default\":\n                warnings.warn(\n                    \"EDGEDB_SERVER_ALLOW_INSECURE_BINARY_CLIENTS is \"\n                    \"deprecated. Use EDGEDB_SERVER_BINARY_ENDPOINT_SECURITY \"\n                    \"instead.\",\n                    DeprecationWarning,\n                    stacklevel=2,\n                )\n            kwargs['http_endpoint_security'] = 'optional'\n\n    if kwargs['security'] == 'default':\n        if devmode.is_in_dev_mode():\n            kwargs['security'] = 'insecure_dev_mode'\n        else:\n            kwargs['security'] = 'strict'\n\n    if kwargs['security'] == 'insecure_dev_mode':\n        if kwargs['http_endpoint_security'] == 'default':\n            kwargs['http_endpoint_security'] = 'optional'\n        if not kwargs['default_auth_method']:\n            kwargs['default_auth_method'] = ServerAuthMethods({\n                t: [ServerAuthMethod.Trust]\n                for t in ServerConnTransport.__members__.values()\n            })\n        if kwargs['tls_cert_mode'] == 'default':\n            kwargs['tls_cert_mode'] = 'generate_self_signed'\n\n    elif not kwargs['default_auth_method']:\n        kwargs['default_auth_method'] = DEFAULT_AUTH_METHODS\n\n    transport_methods = dict(kwargs['default_auth_method'].items())\n    for transport in ServerConnTransport.__members__.values():\n        methods = transport_methods[transport]\n        if ServerAuthMethod.Auto in methods:\n            pos = methods.index(ServerAuthMethod.Auto)\n            if transport in (\n                ServerConnTransport.HTTP_METRICS,\n                ServerConnTransport.HTTP_HEALTH,\n            ):\n                if kwargs['tls_client_ca_file'] is None:\n                    method = ServerAuthMethod.Trust\n                else:\n                    method = ServerAuthMethod.mTLS\n                methods[pos] = method\n            else:\n                methods = (\n                    methods[:pos]\n                    + DEFAULT_AUTH_METHODS.get(transport)\n                    + methods[pos + 1:]\n                )\n            transport_methods[transport] = [method]\n        elif transport in (\n            ServerConnTransport.HTTP_METRICS,\n            ServerConnTransport.HTTP_HEALTH,\n        ):\n            if ServerAuthMethod.mTLS in methods:\n                if kwargs['tls_client_ca_file'] is None:\n                    abort('--tls-client-ca-file is required '\n                          'for mTLS authentication')\n\n            if not all(\n                m is ServerAuthMethod.Trust or m is ServerAuthMethod.mTLS\n                for m in methods\n            ):\n                abort(\n                    f'--default-auth-method of {transport} can only be one '\n                    f'of: {ServerAuthMethod.Trust}, {ServerAuthMethod.mTLS} '\n                    f'or {ServerAuthMethod.Auto}'\n                )\n    kwargs['default_auth_method'] = ServerAuthMethods(transport_methods)\n\n    if kwargs['binary_endpoint_security'] == 'default':\n        kwargs['binary_endpoint_security'] = 'tls'\n\n    if kwargs['http_endpoint_security'] == 'default':\n        kwargs['http_endpoint_security'] = 'tls'\n\n    if kwargs['tls_cert_mode'] == 'default':\n        kwargs['tls_cert_mode'] = 'require_file'\n\n    if kwargs['jose_key_mode'] == 'default':\n        kwargs['jose_key_mode'] = 'generate'\n\n    kwargs['security'] = ServerSecurityMode(kwargs['security'])\n    kwargs['binary_endpoint_security'] = ServerEndpointSecurityMode(\n        kwargs['binary_endpoint_security'])\n    kwargs['http_endpoint_security'] = ServerEndpointSecurityMode(\n        kwargs['http_endpoint_security'])\n    kwargs['tls_cert_mode'] = ServerTlsCertMode(kwargs['tls_cert_mode'])\n    kwargs['jose_key_mode'] = JOSEKeyMode(kwargs['jose_key_mode'])\n\n    if kwargs['compiler_pool_mode'] == 'default':\n        if kwargs['multitenant_config_file']:\n            kwargs['compiler_pool_mode'] = 'fixed_multi_tenant'\n        elif devmode.is_in_dev_mode():\n            kwargs['compiler_pool_mode'] = 'on_demand'\n        else:\n            kwargs['compiler_pool_mode'] = 'fixed'\n    kwargs['compiler_pool_mode'] = CompilerPoolMode(\n        kwargs['compiler_pool_mode']\n    )\n    if kwargs['compiler_pool_size'] is None:\n        if kwargs['compiler_pool_mode'] == CompilerPoolMode.Remote:\n            # this reflects to a local semaphore to control concurrency,\n            # 2 means this is a small EdgeDB instance that could only issue\n            # at max 2 concurrent compile requests at a time.\n            kwargs['compiler_pool_size'] = 2\n        else:\n            kwargs['compiler_pool_size'] = compute_default_compiler_pool_size()\n    if kwargs['compiler_pool_mode'] == CompilerPoolMode.Remote:\n        if kwargs['compiler_pool_addr'] is None:\n            kwargs['compiler_pool_addr'] = (\n                \"localhost\", defines.EDGEDB_REMOTE_COMPILER_PORT\n            )\n        if kwargs['compiler_worker_max_rss'] is not None:\n            abort('cannot set --compiler-worker-max-rss when using '\n                  '--compiler-pool-mode=remote')\n\n    elif kwargs['compiler_pool_addr'] is not None:\n        abort('--compiler-pool-addr is only meaningful '\n              'under --compiler-pool-mode=remote')\n\n    if kwargs['temp_dir']:\n        if kwargs['data_dir']:\n            abort('--temp-dir is incompatible with --data-dir/-D')\n        if kwargs['runstate_dir']:\n            abort('--temp-dir is incompatible with --runstate-dir')\n        if kwargs['backend_dsn']:\n            abort('--temp-dir is incompatible with --backend-dsn')\n        if kwargs['multitenant_config_file']:\n            abort('--temp-dir is incompatible with --multitenant-config-file')\n        kwargs['data_dir'] = kwargs['runstate_dir'] = pathlib.Path(\n            tempfile.mkdtemp())\n    else:\n        if not kwargs['data_dir']:\n            if kwargs['backend_dsn'] or kwargs['multitenant_config_file']:\n                pass\n            elif devmode.is_in_dev_mode():\n                data_dir = devmode.get_dev_mode_data_dir()\n                if not data_dir.parent.exists():\n                    data_dir.parent.mkdir(exist_ok=True, parents=True)\n\n                kwargs[\"data_dir\"] = data_dir\n            else:\n                abort('Please specify the instance data directory '\n                      'using the -D argument or the address of a remote '\n                      'backend cluster using the --backend-dsn argument')\n        elif kwargs['backend_dsn']:\n            abort('The -D and --backend-dsn options are mutually exclusive.')\n        elif kwargs['multitenant_config_file']:\n            abort('The -D and --multitenant-config-file options '\n                  'are mutually exclusive.')\n\n    if kwargs['tls_key_file'] and not kwargs['tls_cert_file']:\n        abort('When --tls-key-file is set, --tls-cert-file must also be set.')\n\n    if kwargs['tls_cert_file'] and not kwargs['tls_key_file']:\n        abort('When --tls-cert-file is set, --tls-key-file must also be set.')\n\n    self_signing = kwargs['tls_cert_mode'] is ServerTlsCertMode.SelfSigned\n\n    if not kwargs['tls_cert_file']:\n        if kwargs['data_dir']:\n            tls_cert_file = kwargs['data_dir'] / TLS_CERT_FILE_NAME\n            tls_key_file = kwargs['data_dir'] / TLS_KEY_FILE_NAME\n        elif self_signing:\n            tls_cert_file = pathlib.Path('<runstate>') / TLS_CERT_FILE_NAME\n            tls_key_file = pathlib.Path('<runstate>') / TLS_KEY_FILE_NAME\n        else:\n            abort(\n                \"no TLS certificate specified and certificate auto-generation\"\n                \" has not been requested; see help for --tls-cert-mode\",\n                exit_code=10,\n            )\n        kwargs['tls_cert_file'] = tls_cert_file\n        kwargs['tls_key_file'] = tls_key_file\n\n    if not kwargs['bootstrap_only'] and not self_signing:\n        if not kwargs['tls_cert_file'].exists():\n            abort(\n                f\"TLS certificate file \\\"{kwargs['tls_cert_file']}\\\"\"\n                \" does not exist and certificate auto-generation has not been\"\n                \" requested; see help for --tls-cert-mode\",\n                exit_code=10,\n            )\n\n    if (\n        kwargs['tls_cert_file'].exists()\n        and not kwargs['tls_cert_file'].is_file()\n    ):\n        abort(\n            f\"TLS certificate file \\\"{kwargs['tls_cert_file']}\\\"\"\n            \" is not a regular file\"\n        )\n\n    if (\n        kwargs['tls_key_file'].exists()\n        and not kwargs['tls_key_file'].is_file()\n    ):\n        abort(\n            f\"TLS private key file \\\"{kwargs['tls_key_file']}\\\"\"\n            \" is not a regular file\"\n        )\n\n    generate_jose = kwargs['jose_key_mode'] is JOSEKeyMode.Generate\n\n    if not kwargs['jws_key_file']:\n        if kwargs['data_dir']:\n            jws_key_file = kwargs['data_dir'] / JWS_KEY_FILE_NAME\n        elif generate_jose:\n            jws_key_file = pathlib.Path('<runstate>') / JWS_KEY_FILE_NAME\n        else:\n            abort(\n                \"no JWS key specified and JOSE keys auto-generation\"\n                \" has not been requested; see help for --jose-key-mode\",\n                exit_code=11,\n            )\n        kwargs['jws_key_file'] = jws_key_file\n    del kwargs['jwe_key_file']\n\n    if not kwargs['bootstrap_only'] and not generate_jose:\n        if not kwargs['jws_key_file'].exists():\n            abort(\n                f\"JWS key file \\\"{kwargs['jws_key_file']}\\\" does not exist\"\n            )\n\n    if (\n        kwargs['jws_key_file'].exists() and\n        not kwargs['jws_key_file'].is_file()\n    ):\n        abort(\n            f\"JWT key file \\\"{kwargs['jws_key_file']}\\\"\"\n            \" is not a regular file\"\n        )\n\n    if kwargs['log_level']:\n        kwargs['log_level'] = kwargs['log_level'].lower()[0]\n\n    if kwargs['bootstrap_script']:\n        if not kwargs['bootstrap_command_file']:\n            warnings.warn(\n                \"The `--bootstrap-script` option is deprecated, use \"\n                \"`--bootstrap-command-file` instead.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n            kwargs['bootstrap_command_file'] = kwargs['bootstrap_script']\n        else:\n            warnings.warn(\n                \"Both `--bootstrap-command-file` and `--bootstrap-script` \"\n                \"were specified, but are mutually exclusive. \"\n                \"Ignoring the deprecated `--bootstrap-script` option.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n\n    del kwargs['bootstrap_script']\n\n    if kwargs['multitenant_config_file']:\n        for name in (\n            \"tenant_id\",\n            \"backend_dsn\",\n            \"backend_adaptive_ha\",\n            \"bootstrap_only\",\n            \"inplace_upgrade\",\n            \"bootstrap_command\",\n            \"bootstrap_command_file\",\n            \"instance_name\",\n            \"max_backend_connections\",\n            \"readiness_state_file\",\n            \"jwt_sub_allowlist_file\",\n            \"jwt_revocation_list_file\",\n            \"config_file\",\n        ):\n            if kwargs.get(name):\n                opt = \"--\" + name.replace(\"_\", \"-\")\n                abort(f\"The {opt} and --multitenant-config-file options \"\n                      f\"are mutually exclusive.\")\n        if kwargs['compiler_pool_mode'] is not CompilerPoolMode.MultiTenant:\n            abort(\"must use --compiler-pool-mode=fixed_multi_tenant \"\n                  \"in multi-tenant mode\")\n\n    bootstrap_script_text: Optional[str]\n    if kwargs['bootstrap_command_file']:\n        with open(kwargs['bootstrap_command_file']) as f:\n            bootstrap_script_text = f.read()\n    elif kwargs['bootstrap_command']:\n        bootstrap_script_text = kwargs['bootstrap_command']\n    else:\n        bootstrap_script_text = None\n\n    if bootstrap_script_text is None:\n        startup_script = None\n    else:\n        startup_script = StartupScript(\n            text=bootstrap_script_text,\n            database=(\n                kwargs['default_branch'] or\n                kwargs['default_database'] or\n                defines.EDGEDB_SUPERUSER_DB\n            ),\n            user=(\n                kwargs['default_database_user'] or\n                defines.EDGEDB_SUPERUSER\n            ),\n        )\n\n    status_sinks = []\n\n    if status_sink_addrs := kwargs['emit_server_status']:\n        for status_sink_addr in status_sink_addrs:\n            if status_sink_addr.startswith('file://'):\n                status_sink = _status_sink_file(\n                    status_sink_addr[len('file://'):])\n            elif status_sink_addr.startswith('fd://'):\n                fileno_str = status_sink_addr[len('fd://'):]\n                try:\n                    fileno = int(fileno_str)\n                except ValueError:\n                    abort(\n                        f'invalid file descriptor number in '\n                        f'--emit-server-status: {fileno_str!r}'\n                    )\n\n                status_sink = _status_sink_fd(fileno)\n            elif m := re.match(r'^(\\w+)://', status_sink_addr):\n                abort(\n                    f'unsupported destination scheme in --emit-server-status: '\n                    f'{m.group(1)}'\n                )\n            else:\n                # Assume it's a file.\n                status_sink = _status_sink_file(status_sink_addr)\n\n            status_sinks.append(status_sink)\n\n    kwargs['backend_capability_sets'] = (\n        kwargs.pop('backend_capabilities') or BackendCapabilitySets([], [])\n    )\n\n    if kwargs['admin_ui'] == 'default':\n        if devmode.is_in_dev_mode():\n            kwargs['admin_ui'] = 'enabled'\n        else:\n            kwargs['admin_ui'] = 'disabled'\n\n    kwargs['admin_ui'] = kwargs['admin_ui'] == 'enabled'\n\n    if not kwargs['instance_name']:\n        if devmode.is_in_dev_mode():\n            kwargs['instance_name'] = '_localdev'\n        else:\n            kwargs['instance_name'] = '_unknown'\n\n    kwargs['reload_config_files'] = ReloadTrigger(\n        kwargs['reload_config_files']\n    )\n    kwargs['net_worker_mode'] = NetWorkerMode(kwargs['net_worker_mode'])\n\n    for disallowed, replacement in (\n        (\n            'EDGEDB_SERVER_CONFIG_cfg::listen_addresses',\n            'GEL_SERVER_BIND_ADDRESS',\n        ),\n        (\n            'EDGEDB_SERVER_CONFIG_cfg::listen_port',\n            'GEL_SERVER_PORT',\n        ),\n        (\n            'GEL_SERVER_CONFIG_cfg::listen_addresses',\n            'GEL_SERVER_BIND_ADDRESS',\n        ),\n        (\n            'GEL_SERVER_CONFIG_cfg::listen_port',\n            'GEL_SERVER_PORT',\n        ),\n    ):\n        if disallowed in os.environ:\n            abort(f\"{disallowed} is disallowed; use {replacement} instead\")\n\n    return ServerConfig(\n        startup_script=startup_script,\n        status_sinks=status_sinks,\n        **kwargs,\n    )\n"
  },
  {
    "path": "edb/server/auth.py",
    "content": "import datetime\nimport pathlib\n\nfrom typing import TYPE_CHECKING, Iterable, Optional, Any\n\nif TYPE_CHECKING:\n    class SigningCtx:\n        def __init__(self) -> None: ...\n        def set_issuer(self, issuer: str) -> None: ...\n        def set_audience(self, audience: str) -> None: ...\n        def set_expiry(self, expiry: int) -> None: ...\n        def set_not_before(self, not_before: int) -> None: ...\n\n    class ValidationCtx:\n        def __init__(self) -> None: ...\n        def allow(\n            self,\n            claim: str,\n            values: list[str] | Iterable[str],\n        ) -> None: ...\n        def deny(\n            self,\n            claim: str,\n            values: list[str] | Iterable[str],\n        ) -> None: ...\n        def require(self, claim: str) -> None: ...\n        def reject(self, claim: str) -> None: ...\n        def ignore(self, claim: str) -> None: ...\n        def require_expiry(self) -> None: ...\n        def ignore_expiry(self) -> None: ...\n\n    class JWKSet:\n        @staticmethod\n        def from_hs256_key(key: bytes) -> \"JWKSet\": ...\n        def __init__(self) -> None: ...\n        def generate(self, *, kid: Optional[str], kty: str) -> None: ...\n        def add(self, **kwargs: Any) -> None: ...\n        def load(self, keys: str) -> int: ...\n        def load_json(self, keys: str) -> int: ...\n        def export_pem(self, *, private_keys: bool=True) -> bytes: ...\n        def export_json(self, *, private_keys: bool=True) -> bytes: ...\n        def can_sign(self) -> bool: ...\n        def can_validate(self) -> bool: ...\n        def has_public_keys(self) -> bool: ...\n        def has_private_keys(self) -> bool: ...\n        def has_symmetric_keys(self) -> bool: ...\n        def sign(\n            self, claims: dict[str, Any], *, ctx: Optional[SigningCtx] = None\n        ) -> str: ...\n        def validate(\n            self, token: str, *, ctx: Optional[ValidationCtx] = None\n        ) -> dict[str, Any]: ...\n        @property\n        def default_signing_context(self) -> SigningCtx: ...\n        @property\n        def default_validation_context(self) -> ValidationCtx: ...\n\n    class JWKSetCache:\n        def __init__(self, expiry_seconds: int) -> None: ...\n        # Returns a tuple of (is_fresh, registry)\n        def get(self, key: str) -> tuple[bool, Optional[JWKSet]]: ...\n        def set(self, key: str, registry: JWKSet) -> None: ...\n\n    def generate_gel_token(\n        registry: JWKSet,\n        *,\n        instances: Optional[list[str] | Iterable[str]] = None,\n        roles: Optional[list[str] | Iterable[str]] = None,\n        databases: Optional[list[str] | Iterable[str]] = None,\n        **kwargs: Any,\n    ) -> str: ...\n\n    def validate_gel_token(\n        registry: JWKSet,\n        token: str,\n        user: str,\n        dbname: str,\n        instance_name: str,\n    ) -> str | None: ...\nelse:\n    from edb.server._rust_native._jwt import (\n        JWKSet, JWKSetCache, generate_gel_token, validate_gel_token, SigningCtx, ValidationCtx  # noqa\n    )\n\n\ndef load_secret_key(key_file: pathlib.Path) -> JWKSet:\n    try:\n        with open(key_file, 'rb') as kf:\n            jws_key = JWKSet()\n            jws_key.load(kf.read().decode('ascii'))\n    except Exception as e:\n        raise SecretKeyReadError(f\"cannot load JWS key {key_file}: {e}\") from e\n    if not jws_key.can_validate():\n        raise SecretKeyReadError(\n            f\"the cluster JWS key file {key_file} does not \"\n            f\"contain a valid key for token validation (RSA, EC or \"\n            f\"HMAC-SHA256)\")\n\n    # TODO: We should also add a default issuer and add that to the allow-list.\n\n    # Default to one day expiry for tokens -- we will probably tighten this up\n    jws_key.default_signing_context.set_expiry(86400)\n    # 60 second leeway for not before\n    jws_key.default_signing_context.set_not_before(60)\n\n    return jws_key\n\n\ndef generate_jwk(keys_file: pathlib.Path) -> None:\n    key = JWKSet()\n    # kid is yyyymmdd\n    kid = datetime.datetime.now(datetime.timezone.utc).strftime(\"%Y%m%d\")\n    key.generate(kid=kid, kty='ES256')\n    if keys_file.name.endswith(\".pem\"):\n        with keys_file.open(\"wb\") as f:\n            f.write(key.export_pem())\n    elif keys_file.name.endswith(\".json\"):\n        with keys_file.open(\"wb\") as f:\n            f.write(key.export_json())\n    else:\n        raise ValueError(f\"Unsupported key file extension {keys_file.suffix}. \"\n                         \"Use .pem or .json extension when generating a key.\")\n\n    keys_file.chmod(0o600)\n\n\nclass SecretKeyReadError(Exception):\n    pass\n"
  },
  {
    "path": "edb/server/bootstrap.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n    Iterable,\n    Mapping,\n    Awaitable,\n    NamedTuple,\n    TYPE_CHECKING,\n    cast,\n)\n\nimport dataclasses\nimport enum\nimport json\nimport logging\nimport os\nimport pathlib\nimport pickle\nimport re\nimport struct\nimport textwrap\n\nfrom edb import buildmeta\nfrom edb import errors\n\nfrom edb import edgeql\nfrom edb.ir import typeutils as irtyputils\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import codegen as qlcodegen\nfrom edb.edgeql import qltypes\n\nfrom edb.common import debug\nfrom edb.common import devmode\nfrom edb.common import retryloop\nfrom edb.common import uuidgen\n\nfrom edb.schema import ddl as s_ddl\nfrom edb.schema import delta as sd\nfrom edb.schema import extensions as s_exts\nfrom edb.schema import functions as s_func\nfrom edb.schema import modules as s_mod\nfrom edb.schema import name as sn\nfrom edb.schema import objects as s_obj\nfrom edb.schema import properties as s_props\nfrom edb.schema import reflection as s_refl\nfrom edb.schema import schema as s_schema\nfrom edb.schema import std as s_std\nfrom edb.schema import types as s_types\nfrom edb.schema import utils as s_utils\nfrom edb.schema import version as s_ver\n\nfrom edb.server import args as edbargs\nfrom edb.server import config\nfrom edb.server import compiler as edbcompiler\nfrom edb.server.compiler import dbstate\nfrom edb.server import defines as edbdef\nfrom edb.server import pgcluster\nfrom edb.server import pgcon\n\nfrom edb.pgsql import common as pg_common\nfrom edb.pgsql import dbops\nfrom edb.pgsql import delta as delta_cmds\nfrom edb.pgsql import metaschema\nfrom edb.pgsql import params\nfrom edb.pgsql import patches\nfrom edb.pgsql import trampoline\nfrom edb.pgsql.common import quote_ident as qi\n\nfrom edgedb import scram\n\nif TYPE_CHECKING:\n    import uuid\n\n\nlogger = logging.getLogger('edb.server')\nSTDLIB_CACHE_FILE_NAME = 'backend-stdlib.pickle'\n\n\nclass ClusterMode(enum.IntEnum):\n    pristine = 0\n    regular = 1\n    single_role = 2\n    single_database = 3\n\n\n# A simple connection proxy that reconnects and retries queries\n# on connection errors.  Helps defeat flaky connections and/or\n# flaky Postgres servers (Digital Ocean managed instances are\n# one example that has a weird setup that crashes a helper\n# process when we bootstrap, breaking other connections).\nclass PGConnectionProxy:\n    def __init__(\n        self,\n        cluster: pgcluster.BaseCluster,\n        *,\n        source_description: str,\n        dbname: Optional[str] = None,\n        log_listener: Optional[Callable[[str, str], None]] = None,\n    ):\n        self._conn: Optional[pgcon.PGConnection] = None\n        self._cluster = cluster\n        self._dbname = dbname\n        self._log_listener = log_listener or _pg_log_listener\n        self._source_description = source_description\n\n    async def connect(self) -> None:\n        if self._conn is not None:\n            self._conn.terminate()\n\n        if self._dbname:\n            self._conn = await self._cluster.connect(\n                source_description=self._source_description,\n                database=self._dbname\n            )\n        else:\n            self._conn = await self._cluster.connect(\n                source_description=self._source_description\n            )\n\n        if self._log_listener is not None:\n            self._conn.add_log_listener(self._log_listener)\n\n    def _on_retry(self, exc: Optional[BaseException]) -> None:\n        logger.warning(\n            f'Retrying bootstrap SQL query due to connection error: '\n            f'{type(exc)}({exc})',\n        )\n        self.terminate()\n\n    async def _retry_conn_errors[T](\n        self,\n        task: Callable[[], Awaitable[T]],\n    ) -> T:\n        rloop = retryloop.RetryLoop(\n            backoff=retryloop.exp_backoff(),\n            timeout=5.0,\n            ignore=(\n                ConnectionError,\n                pgcon.BackendConnectionError,\n            ),\n            retry_cb=self._on_retry,\n        )\n        async for iteration in rloop:\n            async with iteration:\n                if self._conn is None:\n                    await self.connect()\n                result = await task()\n\n        return result\n\n    async def sql_execute(self, sql: bytes) -> None:\n        async def _task() -> None:\n            assert self._conn is not None\n            await self._conn.sql_execute(sql)\n        return await self._retry_conn_errors(_task)\n\n    async def sql_fetch(\n        self,\n        sql: bytes,\n        *,\n        args: tuple[bytes, ...] | list[bytes] = (),\n    ) -> list[tuple[bytes, ...]]:\n        async def _task() -> list[tuple[bytes, ...]]:\n            assert self._conn is not None\n            return await self._conn.sql_fetch(sql, args=args)\n        return await self._retry_conn_errors(_task)\n\n    async def sql_fetch_val(\n        self,\n        sql: bytes,\n        *,\n        args: tuple[bytes, ...] | list[bytes] = (),\n    ) -> bytes:\n        async def _task() -> bytes:\n            assert self._conn is not None\n            return await self._conn.sql_fetch_val(sql, args=args)\n        return await self._retry_conn_errors(_task)\n\n    async def sql_fetch_col(\n        self,\n        sql: bytes,\n        *,\n        args: tuple[bytes, ...] | list[bytes] = (),\n    ) -> list[bytes]:\n        async def _task() -> list[bytes]:\n            assert self._conn is not None\n            return await self._conn.sql_fetch_col(sql, args=args)\n        return await self._retry_conn_errors(_task)\n\n    def terminate(self) -> None:\n        if self._conn is not None:\n            self._conn.terminate()\n            self._conn = None\n\n\n@dataclasses.dataclass\nclass BootstrapContext:\n\n    cluster: pgcluster.BaseCluster\n    conn: PGConnectionProxy | pgcon.PGConnection\n    args: edbargs.ServerConfig\n    mode: Optional[ClusterMode] = None\n\n\nasync def _execute(conn, query):\n    return await metaschema.execute_sql_script(conn, query)\n\n\nasync def _execute_block(conn, block: dbops.SQLBlock) -> None:\n\n    if not block.is_transactional():\n        stmts = block.get_statements()\n    else:\n        stmts = [block.to_string()]\n\n    for stmt in stmts:\n        await _execute(conn, stmt)\n\n\ndef _execute_edgeql_ddl[Schema_T: s_schema.Schema](\n    schema: Schema_T,\n    ddltext: str,\n    stdmode: bool = True,\n) -> Schema_T:\n    context = sd.CommandContext(stdmode=stdmode)\n\n    for ddl_cmd in edgeql.parse_block(ddltext):\n        assert isinstance(ddl_cmd, qlast.DDLCommand)\n        delta_command = s_ddl.delta_from_ddl(\n            ddl_cmd, modaliases={}, schema=schema, stdmode=stdmode)\n\n        schema = delta_command.apply(schema, context)  # type: ignore\n\n    return schema\n\n\nasync def _ensure_edgedb_supergroup(\n    ctx: BootstrapContext,\n    role_name: str,\n    *,\n    member_of: Iterable[str] = (),\n    members: Iterable[str] = (),\n) -> None:\n    member_of = set(member_of)\n    backend_params = ctx.cluster.get_runtime_params()\n    superuser_role = backend_params.instance_params.base_superuser\n    if superuser_role:\n        # If the cluster is exposing an explicit superuser role,\n        # become a member of that instead of creating a superuser\n        # role directly.\n        member_of.add(superuser_role)\n\n    pg_role_name = ctx.cluster.get_role_name(role_name)\n\n    role = dbops.Role(\n        name=pg_role_name,\n        superuser=backend_params.has_superuser_access,\n        allow_login=False,\n        allow_createdb=True,\n        allow_createrole=True,\n        membership=member_of,\n        members=members,\n    )\n\n    create_role = dbops.CreateRole(\n        role,\n        neg_conditions=[dbops.RoleExists(pg_role_name)],\n    )\n\n    block = dbops.PLTopBlock()\n    create_role.generate(block)\n\n    await _execute_block(ctx.conn, block)\n\n\nasync def _ensure_edgedb_role(\n    ctx: BootstrapContext,\n    role_name: str,\n    *,\n    superuser: bool = False,\n    builtin: bool = False,\n    objid: Optional[uuid.UUID] = None,\n) -> uuid.UUID:\n    member_of = set()\n    if superuser:\n        member_of.add(edbdef.EDGEDB_SUPERGROUP)\n\n    if objid is None:\n        objid = uuidgen.uuid1mc()\n\n    members = set()\n    login_role = ctx.cluster.get_connection_params().user\n    assert login_role is not None\n    sup_role = ctx.cluster.get_role_name(edbdef.EDGEDB_SUPERUSER)\n    if login_role != sup_role:\n        members.add(login_role)\n\n    backend_params = ctx.cluster.get_runtime_params()\n    pg_role_name = ctx.cluster.get_role_name(role_name)\n    role = dbops.Role(\n        name=pg_role_name,\n        superuser=superuser and backend_params.has_superuser_access,\n        allow_login=True,\n        allow_createdb=True,\n        allow_createrole=True,\n        membership=[ctx.cluster.get_role_name(m) for m in member_of],\n        members=members,\n        metadata=dict(\n            id=str(objid),\n            name=role_name,\n            tenant_id=backend_params.tenant_id,\n            builtin=builtin,\n            branches=['*'],\n        ),\n    )\n\n    create_role = dbops.CreateRole(\n        role,\n        neg_conditions=[dbops.RoleExists(pg_role_name)],\n    )\n\n    block = dbops.PLTopBlock()\n    create_role.generate(block)\n\n    await _execute_block(ctx.conn, block)\n\n    return objid\n\n\nasync def _get_cluster_mode(ctx: BootstrapContext) -> ClusterMode:\n    backend_params = ctx.cluster.get_runtime_params()\n    tenant_id = backend_params.tenant_id\n\n    # First, check the existence of EDGEDB_SUPERGROUP - the role which is\n    # usually created at the beginning of bootstrap.\n    is_default_tenant = tenant_id == buildmeta.get_default_tenant_id()\n    ignore_others = is_default_tenant and ctx.args.ignore_other_tenants\n    if is_default_tenant:\n        result = await ctx.conn.sql_fetch_col(\n            b\"\"\"\n            SELECT\n                r.rolname\n            FROM\n                pg_catalog.pg_roles AS r\n            WHERE\n                r.rolname LIKE ('%' || $1)\n            \"\"\",\n            args=[\n                edbdef.EDGEDB_SUPERGROUP.encode(\"utf-8\"),\n            ],\n        )\n    else:\n        result = await ctx.conn.sql_fetch_col(\n            b\"\"\"\n            SELECT\n                r.rolname\n            FROM\n                pg_catalog.pg_roles AS r\n            WHERE\n                r.rolname = $1\n            \"\"\",\n            args=[\n                ctx.cluster.get_role_name(\n                    edbdef.EDGEDB_SUPERGROUP).encode(\"utf-8\"),\n            ],\n        )\n\n    if result:\n        if not ignore_others:\n            # Either our tenant slot is occupied, or there is\n            # a default tenant present.\n            return ClusterMode.regular\n\n        # We were explicitly asked to ignore the other default tenant,\n        # so check specifically if our tenant slot is occupied and ignore\n        # the others.\n        # This mode is used for in-place upgrade.\n        for rolname in result:\n            other_tenant_id = rolname[: -(len(edbdef.EDGEDB_SUPERGROUP) + 1)]\n            if other_tenant_id == tenant_id.encode(\"utf-8\"):\n                return ClusterMode.regular\n\n    # Then, check if the current database was bootstrapped in single-db mode.\n    has_instdata = await ctx.conn.sql_fetch_val(\n        trampoline.fixup_query('''\n            SELECT\n                tablename\n            FROM\n                pg_catalog.pg_tables\n            WHERE\n                schemaname = 'edgedbinstdata_VER'\n                AND tablename = 'instdata'\n        ''').encode('utf-8'),\n    )\n    if has_instdata:\n        return ClusterMode.single_database\n\n    # At last, check for single-role-bootstrapped instance by trying to find\n    # the Gel System DB with the assumption that we are not running in\n    # single-db mode. If not found, this is a pristine backend cluster.\n    if is_default_tenant:\n        result = await ctx.conn.sql_fetch_col(\n            b'''\n                SELECT datname\n                FROM pg_database\n                WHERE datname LIKE '%' || $1\n            ''',\n            args=(\n                edbdef.EDGEDB_SYSTEM_DB.encode(\"utf-8\"),\n            ),\n        )\n    else:\n        result = await ctx.conn.sql_fetch_col(\n            b'''\n                SELECT datname\n                FROM pg_database\n                WHERE datname = $1\n            ''',\n            args=(\n                ctx.cluster.get_db_name(\n                    edbdef.EDGEDB_SYSTEM_DB).encode(\"utf-8\"),\n            ),\n        )\n\n    if result:\n        if not ignore_others:\n            # Either our tenant slot is occupied, or there is\n            # a default tenant present.\n            return ClusterMode.single_role\n\n        # We were explicitly asked to ignore the other default tenant,\n        # so check specifically if our tenant slot is occupied and ignore\n        # the others.\n        # This mode is used for in-place upgrade.\n        for dbname in result:\n            other_tenant_id = dbname[: -(len(edbdef.EDGEDB_SYSTEM_DB) + 1)]\n            if other_tenant_id == tenant_id.encode(\"utf-8\"):\n                return ClusterMode.single_role\n\n    return ClusterMode.pristine\n\n\nasync def _create_edgedb_template_database(\n    ctx: BootstrapContext,\n) -> uuid.UUID:\n    backend_params = ctx.cluster.get_runtime_params()\n    have_c_utf8 = backend_params.has_c_utf8_locale\n\n    logger.info('Creating template database...')\n    block = dbops.SQLBlock()\n    dbid = uuidgen.uuid1mc()\n    db = dbops.Database(\n        ctx.cluster.get_db_name(edbdef.EDGEDB_TEMPLATE_DB),\n        owner=ctx.cluster.get_role_name(edbdef.EDGEDB_SUPERUSER),\n        is_template=True,\n        lc_collate='C',\n        lc_ctype='C.UTF-8' if have_c_utf8 else 'en_US.UTF-8',\n        encoding='UTF8',\n        metadata=dict(\n            id=str(dbid),\n            tenant_id=backend_params.tenant_id,\n            name=edbdef.EDGEDB_TEMPLATE_DB,\n            builtin=True,\n        ),\n    )\n\n    dbops.CreateDatabase(db, template='template0').generate(block)\n    await _execute_block(ctx.conn, block)\n    return dbid\n\n\nasync def _store_static_bin_cache_conn(\n    conn: metaschema.PGConnection,\n    key: str,\n    data: bytes,\n) -> None:\n\n    text = trampoline.fixup_query(f\"\"\"\\\n        INSERT INTO edgedbinstdata_VER.instdata (key, bin)\n        VALUES(\n            {pg_common.quote_literal(key)},\n            {pg_common.quote_bytea_literal(data)}\n        )\n    \"\"\")\n\n    await _execute(conn, text)\n\n\nasync def _store_static_bin_cache(\n    ctx: BootstrapContext,\n    key: str,\n    data: bytes,\n) -> None:\n    await _store_static_bin_cache_conn(ctx.conn, key, data)\n\n\nasync def _store_static_text_cache(\n    ctx: BootstrapContext,\n    key: str,\n    data: str,\n) -> None:\n\n    text = trampoline.fixup_query(f\"\"\"\\\n        INSERT INTO edgedbinstdata_VER.instdata (key, text)\n        VALUES(\n            {pg_common.quote_literal(key)},\n            {pg_common.quote_literal(data)}::text\n        )\n    \"\"\")\n\n    await _execute(ctx.conn, text)\n\n\nasync def _store_static_json_cache(\n    ctx: BootstrapContext,\n    key: str,\n    data: str,\n) -> None:\n\n    text = trampoline.fixup_query(f\"\"\"\\\n        INSERT INTO edgedbinstdata_VER.instdata (key, json)\n        VALUES(\n            {pg_common.quote_literal(key)},\n            {pg_common.quote_literal(data)}::jsonb\n        )\n    \"\"\")\n\n    await _execute(ctx.conn, text)\n\n\ndef _process_delta_params[Schema_T: s_schema.Schema](\n    delta: sd.Command,\n    schema: Schema_T,\n    params: params.BackendRuntimeParams,\n    stdmode: bool=True,\n    **kwargs,\n) -> tuple[\n    Schema_T,\n    delta_cmds.MetaCommand,\n    delta_cmds.CreateTrampolines,\n]:\n    \"\"\"Adapt and process the delta command.\"\"\"\n\n    if debug.flags.delta_plan:\n        debug.header('Delta Plan')\n        debug.dump(delta, schema=schema)\n\n    context = sd.CommandContext(stdmode=True)\n\n    if not delta.canonical:\n        # Canonicalize\n        sd.apply(delta, schema=schema)\n\n    delta_pg: delta_cmds.MetaCommand = delta_cmds.CommandMeta.adapt(delta)  # type: ignore\n    context = sd.CommandContext(\n        stdmode=stdmode,\n        backend_runtime_params=params,\n        **kwargs,\n    )\n    schema = sd.apply(delta_pg, schema=schema, context=context)\n\n    if debug.flags.delta_pgsql_plan:\n        debug.header('PgSQL Delta Plan')\n        debug.dump(delta_pg, schema=schema)\n\n    if isinstance(delta_pg, delta_cmds.DeltaRoot):\n        out = delta_pg.create_trampolines\n    else:\n        out = delta_cmds.CreateTrampolines()\n\n    return schema, delta_pg, out\n\n\ndef _process_delta[Schema_T: s_schema.Schema](\n    ctx: BootstrapContext,\n    delta: sd.Command,\n    schema: Schema_T,\n) -> tuple[\n    Schema_T,\n    delta_cmds.MetaCommand,\n    delta_cmds.CreateTrampolines,\n]:\n    \"\"\"Adapt and process the delta command.\"\"\"\n    return _process_delta_params(\n        delta, schema, ctx.cluster.get_runtime_params()\n    )\n\n\ndef compile_bootstrap_script(\n    compiler: edbcompiler.Compiler,\n    schema: s_schema.Schema,\n    eql: str,\n    *,\n    bootstrap_mode: bool = True,\n    expected_cardinality_one: bool = False,\n    output_format: edbcompiler.OutputFormat = edbcompiler.OutputFormat.JSON,\n) -> tuple[s_schema.Schema, str]:\n    ctx = edbcompiler.new_compiler_context(\n        compiler_state=compiler.state,\n        user_schema=schema,\n        expected_cardinality_one=expected_cardinality_one,\n        json_parameters=True,\n        output_format=output_format,\n        bootstrap_mode=bootstrap_mode,\n        log_ddl_as_migrations=False,\n    )\n\n    return edbcompiler.compile_edgeql_script(ctx, eql)\n\n\ndef compile_single_query(\n    eql: str,\n    compilerctx: edbcompiler.CompileContext,\n) -> str:\n    ql_source = edgeql.Source.from_string(eql)\n    units = edbcompiler.compile(ctx=compilerctx, source=ql_source).units\n    assert len(units) == 1\n    return units[0].sql.decode()\n\n\ndef _get_all_subcommands(\n    cmd: sd.Command, type: Optional[type[sd.Command]] = None\n) -> list[sd.Command]:\n    cmds = []\n\n    def go(cmd):\n        if not type or isinstance(cmd, type):\n            cmds.append(cmd)\n        for sub in cmd.get_subcommands():\n            go(sub)\n\n    go(cmd)\n    return cmds\n\n\ndef _get_schema_object_ids(\n    delta: sd.Command,\n) -> Mapping[tuple[sn.Name, Optional[str]], uuid.UUID]:\n    schema_object_ids = {}\n    for cmd in _get_all_subcommands(delta, sd.CreateObject):\n        assert isinstance(cmd, sd.CreateObject)\n        mcls = cmd.get_schema_metaclass()\n        if issubclass(mcls, s_obj.QualifiedObject):\n            qlclass = None\n        else:\n            qlclass = mcls.get_ql_class_or_die()\n\n        id = cmd.get_attribute_value('id')\n        schema_object_ids[cmd.classname, qlclass] = id\n\n        # backend_name in callables is a lot *like* an id, in that it gets\n        # randomly generated and needs to match between things.\n        if isinstance(cmd, s_func.CreateCallableObject):\n            backend_name = cmd.get_attribute_value('backend_name')\n            if backend_name:\n                schema_object_ids[\n                    cmd.classname, f'{qlclass}-backend_name'] = backend_name\n\n    return schema_object_ids\n\n\ndef prepare_repair_patch(\n    stdschema: s_schema.Schema,\n    reflschema: s_schema.Schema,\n    userschema: s_schema.Schema,\n    globalschema: s_schema.Schema,\n    schema_class_layout: s_refl.SchemaClassLayout,\n    backend_params: params.BackendRuntimeParams,\n) -> str:\n    compiler = edbcompiler.new_compiler(\n        std_schema=stdschema,\n        reflection_schema=reflschema,\n        schema_class_layout=schema_class_layout\n    )\n\n    compilerctx = edbcompiler.new_compiler_context(\n        compiler_state=compiler.state,\n        global_schema=globalschema,\n        user_schema=userschema,\n    )\n    res = edbcompiler.repair_schema(compilerctx)\n    if not res:\n        return \"\"\n    sql, _ = res\n\n    return sql.decode('utf-8')\n\n\nPatchEntry = tuple[tuple[str, ...], tuple[str, ...], dict[str, Any]]\n\n\nasync def get_existing_view_columns(\n    conn: pgcon.PGConnection | PGConnectionProxy,\n) -> dict[str, list[str]]:\n    # Find all the config views (they are pg_classes where\n    # there is also a table with the same name but \"_dummy\"\n    # at the end) and collect all their columns in order.\n    schema = pg_common.versioned_schema(\"edgedbstd\")\n    return json.loads(await conn.sql_fetch_val(f'''\\\n        select json_object_agg(v.relname, (\n            select json_agg(a.attname order by a.attnum)\n            from pg_catalog.pg_attribute as a\n            where v.oid = a.attrelid\n        ))\n        from pg_catalog.pg_class as v\n        inner join pg_catalog.pg_tables as t\n        on v.relname || '_dummy' = t.tablename\n        -- Filter for just our namespace!\n        inner join pg_catalog.pg_namespace as ns\n        on v.relnamespace = ns.oid\n        where ns.nspname = '{schema}' OR ns.nspname = 'edgedbpub'\n\n    '''.encode('utf-8')))\n\n\nasync def gather_patch_info(\n    num: int,\n    kind: str,\n    patch: str,\n    conn: pgcon.PGConnection | PGConnectionProxy,\n) -> Optional[dict[str, list[str]]]:\n    \"\"\"Fetch info for a patch that needs to use the connection.\n\n    Currently, the only thing we need is, for config updates, the\n    order that columns appear in the config views in SQL. We need this\n    because we need to preserve that order when we update the\n    view.\n    \"\"\"\n\n    if '+config' in kind:\n        return await get_existing_view_columns(conn)\n    else:\n        return None\n\n\ndef prepare_patch(\n    num: int,\n    kind: str,\n    patch: str,\n    schema: s_schema.Schema,\n    reflschema: s_schema.Schema,\n    schema_class_layout: s_refl.SchemaClassLayout,\n    backend_params: params.BackendRuntimeParams,\n    patch_info: Optional[dict[str, list[str]]],\n    user_schema: Optional[s_schema.Schema]=None,\n    global_schema: Optional[s_schema.Schema]=None,\n    *,\n    dbname: Optional[str]=None,\n) -> PatchEntry:\n    val = f'{pg_common.quote_literal(json.dumps(num + 1))}::jsonb'\n    # TODO: This is an INSERT because 2.0 shipped without num_patches.\n    # We can just make this an UPDATE for 3.0\n    update = trampoline.fixup_query(f\"\"\"\\\n        INSERT INTO edgedbinstdata_VER.instdata (key, json)\n        VALUES('num_patches', {val})\n        ON CONFLICT (key)\n        DO UPDATE SET json = {val};\n    \"\"\")\n\n    existing_view_columns = patch_info\n\n    if '+testmode' in kind:\n        if schema.get('cfg::TestSessionConfig', default=None):\n            kind = kind.replace('+testmode', '')\n        else:\n            return (update,), (), {}\n\n    # Pure SQL patches are simple\n    if kind == 'sql':\n        return (patch, update), (), {}\n\n    # metaschema-sql: just recreate a function from metaschema\n    if kind == 'metaschema-sql':\n        func = getattr(metaschema, patch)\n        create = dbops.CreateFunction(func(), or_replace=True)\n        block = dbops.PLTopBlock()\n        create.generate(block)\n        return (block.to_string(), update), (), {}\n\n    if kind.startswith('repair'):\n        assert not patch\n        if not user_schema:\n            return (update,), (), dict(is_user_update=True)\n        assert global_schema\n\n        if kind.startswith('repair+user_ext'):\n            # Only run a userext update if the extension we are trying to\n            # update is installed.\n            extension_name = kind.split('|')[-1]\n            extension = user_schema.get_global(\n                s_exts.Extension, extension_name, default=None)\n\n            if not extension:\n                return (update,), (), {}\n\n        # TODO: Implement the last-repair-only optimization?\n        try:\n            logger.info(\"repairing database '%s'\", dbname)\n            sql = prepare_repair_patch(\n                schema,\n                reflschema,\n                user_schema,\n                global_schema,\n                schema_class_layout,\n                backend_params\n            )\n        except errors.EdgeDBError as e:\n            if isinstance(e, errors.InternalServerError):\n                raise\n            raise errors.SchemaError(\n                f'Could not repair schema inconsistencies in '\n                f'database branch \"{dbname}\". Probably the schema is '\n                f'no longer valid due to a bug fix.\\n'\n                f'Downgrade to the last working version, fix '\n                f'the schema issue, and try again.'\n            ) from e\n\n        return (update, sql), (), {}\n\n    # EdgeQL and reflection schema patches need to be compiled.\n    current_block = dbops.PLTopBlock()\n    preblock = current_block.add_block()\n    subblock = current_block.add_block()\n\n    std_plans = []\n\n    updates: dict[str, Any] = {}\n\n    global_schema_update = kind == 'ext-pkg'\n    sys_update_only = global_schema_update or kind.endswith('+globalonly')\n\n    if kind == 'ext-pkg':\n        # N.B: We process this without actually having the global\n        # schema present, so we don't do any check for if it already\n        # exists. The backend code will overwrite an older version's\n        # JSON in the global metadata if it was already present.\n        patch = s_std.get_std_module_text(sn.UnqualName(f'ext/{patch}'))\n\n    if (\n        kind == 'edgeql'\n        or kind == 'ext-pkg'\n        or kind.startswith('edgeql+schema')\n    ):\n        assert '+user_ext' not in kind\n\n        for ddl_cmd in edgeql.parse_block(patch):\n            if not isinstance(ddl_cmd, qlast.DDLCommand):\n                assert isinstance(ddl_cmd, qlast.Query)\n                ddl_cmd = qlast.DDLQuery(query=ddl_cmd)\n            # First apply it to the regular schema, just so we can update\n            # stdschema\n            delta_command = s_ddl.delta_from_ddl(\n                ddl_cmd, modaliases={}, schema=schema, stdmode=True)\n            schema, _, _ = _process_delta_params(\n                delta_command, schema, backend_params)\n\n            # We need to extract all ids of new objects created when\n            # applying it to the regular schema, so that we can make sure\n            # to use the same ids in the reflschema.\n            schema_object_ids = _get_schema_object_ids(delta_command)\n\n            # Then apply it to the reflschema, which we will use to drive\n            # the actual table updating.\n            delta_command = s_ddl.delta_from_ddl(\n                ddl_cmd, modaliases={}, schema=reflschema,\n                schema_object_ids=schema_object_ids, stdmode=True)\n            reflschema, plan, tplan = _process_delta_params(\n                delta_command, reflschema, backend_params)\n            std_plans.append(delta_command)\n            plan.generate(subblock)\n            tplan.generate(subblock)\n\n        metadata_user_schema = reflschema\n\n    elif kind.startswith('edgeql+user') or kind.startswith('edgeql+user_ext'):\n        assert '+schema' not in kind\n\n        # There isn't anything to do on the system database for\n        # user updates.\n        if user_schema is None:\n            return (update,), (), dict(is_user_update=True)\n\n        if kind.startswith('edgeql+user_ext'):\n            # Only run a userext update if the extension we are trying to\n            # update is installed.\n            extension_name = kind.split('|')[-1]\n            extension = user_schema.get_global(\n                s_exts.Extension, extension_name, default=None)\n\n            if not extension:\n                return (update,), (), {}\n\n        assert global_schema\n        cschema = s_schema.ChainedSchema(\n            schema,\n            user_schema,\n            global_schema,\n        )\n\n        for ddl_cmd in edgeql.parse_block(patch):\n            if not isinstance(ddl_cmd, qlast.DDLCommand):\n                assert isinstance(ddl_cmd, qlast.Query)\n                ddl_cmd = qlast.DDLQuery(query=ddl_cmd)\n\n            delta_command = s_ddl.delta_from_ddl(\n                ddl_cmd, modaliases={}, schema=cschema,\n                stdmode=False,\n                testmode=True,\n            )\n            # Prune any AlterSchemaVersion commands, because they\n            # won't work, since we defer all the\n            # compile_schema_storage_in_delta calls to the end.\n            for sub in delta_command.get_subcommands(\n                type=s_ver.AlterSchemaVersion\n            ):\n                delta_command.discard(sub)\n            cschema, plan, tplan = _process_delta_params(\n                delta_command, cschema, backend_params)\n            std_plans.append(delta_command)\n            plan.generate(subblock)\n            tplan.generate(subblock)\n\n        if '+config' in kind:\n            views = metaschema.get_config_views(cschema, existing_view_columns)\n            views.generate(subblock)\n\n        metadata_user_schema = cschema.get_top_schema()\n\n    elif kind == 'sql-introspection':\n        support_view_commands = dbops.CommandGroup()\n        support_view_commands.add_commands(\n            metaschema._generate_sql_information_schema(\n                backend_params.instance_params.version\n            )\n        )\n        support_view_commands.generate(subblock)\n\n        metaschema.generate_drop_views(list(support_view_commands), preblock)\n\n        metadata_user_schema = reflschema\n\n    else:\n        raise AssertionError(f'unknown patch type {kind}')\n\n    if kind.startswith('edgeql+schema'):\n        # If we are modifying the schema layout, we need to rerun\n        # generate_structure to collect schema changes not reflected\n        # in the public schema and to discover the new introspection\n        # query.\n        reflection = s_refl.generate_structure(\n            reflschema,\n            make_funcs=False,\n            patch_level=patches.get_patch_level(num),\n        )\n\n        reflschema, plan, tplan = _process_delta_params(\n            reflection.intro_schema_delta, reflschema, backend_params)\n        plan.generate(subblock)\n        tplan.generate(subblock)\n\n        compiler = edbcompiler.new_compiler(\n            std_schema=schema,\n            reflection_schema=reflschema,\n            schema_class_layout=schema_class_layout\n        )\n\n        local_intro_sql, global_intro_sql = compile_intro_queries_stdlib(\n            compiler=compiler,\n            user_schema=reflschema,\n            reflection=reflection,\n        )\n\n        updates.update(dict(\n            classlayout=reflection.class_layout,\n            local_intro_query=local_intro_sql.encode('utf-8'),\n            global_intro_query=global_intro_sql.encode('utf-8'),\n        ))\n\n        # This part is wildly hinky\n        # We need to delete all the support views and recreate them at the end\n        support_view_commands = dbops.CommandGroup()\n        support_view_commands.add_commands([\n            dbops.CreateView(view)\n            for view in metaschema._generate_schema_alias_views(\n                reflschema, sn.UnqualName('schema')\n            ) + metaschema._generate_schema_alias_views(\n                reflschema, sn.UnqualName('sys')\n            )\n        ])\n        support_view_commands.add_commands(\n            metaschema._generate_sql_information_schema(\n                backend_params.instance_params.version\n            )\n        )\n        wrapper_views = metaschema._get_wrapper_views()\n        support_view_commands.add_commands(list(wrapper_views))\n        trampolines = metaschema.trampoline_command(wrapper_views)\n\n        metaschema.generate_drop_views(\n            tuple(support_view_commands) + tuple(trampolines),\n            preblock,\n        )\n\n        # Now add the trampolines to support_view_commands\n        support_view_commands.add_commands([t.make() for t in trampolines])\n\n        # We want to limit how much unconditional work we do, so only recreate\n        # extension views if requested.\n        if '+exts' in kind:\n            for extview in metaschema._generate_extension_views(reflschema):\n                support_view_commands.add_command(\n                    dbops.CreateView(extview, or_replace=True))\n\n        # Though we always update the instdata for the config system,\n        # because it is currently the most convenient way to make sure\n        # all the versioned fields get updated.\n        config_spec = config.load_spec_from_schema(schema)\n\n        # Similarly, only do config system updates if requested.\n        if '+config' in kind:\n            support_view_commands.add_command(\n                metaschema.get_config_views(schema, existing_view_columns))\n            support_view_commands.add_command(\n                metaschema._get_regenerated_config_support_functions(\n                    config_spec\n                )\n            )\n\n        (\n            sysqueries,\n            report_configs_typedesc_1_0,\n            report_configs_typedesc_2_0,\n        ) = compile_sys_queries(\n            reflschema,\n            compiler,\n            config_spec,\n        )\n        updates.update(dict(\n            sysqueries=json.dumps(sysqueries).encode('utf-8'),\n            report_configs_typedesc_1_0=report_configs_typedesc_1_0,\n            report_configs_typedesc_2_0=report_configs_typedesc_2_0,\n            configspec=config.spec_to_json(config_spec).encode('utf-8'),\n        ))\n\n        support_view_commands.generate(subblock)\n\n    compiler = edbcompiler.new_compiler(\n        std_schema=schema,\n        reflection_schema=reflschema,\n        schema_class_layout=schema_class_layout\n    )\n\n    compilerctx = edbcompiler.new_compiler_context(\n        compiler_state=compiler.state,\n        user_schema=metadata_user_schema,\n        bootstrap_mode=user_schema is None,\n    )\n\n    for std_plan in std_plans:\n        edbcompiler.compile_schema_storage_in_delta(\n            ctx=compilerctx,\n            delta=std_plan,\n            block=subblock,\n        )\n\n    patch = current_block.to_string()\n\n    if debug.flags.delta_execute:\n        debug.header('Patch Script')\n        debug.dump_code(patch, lexer='sql')\n\n    if not global_schema_update:\n        updates.update(dict(\n            std_and_reflection_schema=(schema, reflschema),\n        ))\n\n    bins = (\n        'std_and_reflection_schema', 'global_schema', 'classlayout',\n        'report_configs_typedesc_1_0', 'report_configs_typedesc_2_0',\n    )\n    rawbin = (\n        'report_configs_typedesc_1_0', 'report_configs_typedesc_2_0',\n    )\n    jsons = (\n        'sysqueries', 'configspec',\n    )\n    # This is unversioned because it is consumed by a function in metaschema.\n    # (And only by a function in metaschema.)\n    unversioned = (\n        'configspec',\n    )\n    # Just for the system database, we need to update the cached pickle\n    # of everything.\n    version_key = patches.get_version_key(num + 1)\n    sys_updates: tuple[str, ...] = ()\n\n    spatches: tuple[str, ...] = (patch,)\n    for k, v in updates.items():\n        key = f\"'{k}{version_key}'\" if k not in unversioned else f\"'{k}'\"\n        if k in bins:\n            if k not in rawbin:\n                v = pickle.dumps(v, protocol=pickle.HIGHEST_PROTOCOL)\n            val = f'{pg_common.quote_bytea_literal(v)}'\n            sys_updates += (trampoline.fixup_query(f'''\n                INSERT INTO edgedbinstdata_VER.instdata (key, bin)\n                VALUES({key}, {val})\n                ON CONFLICT (key)\n                DO UPDATE SET bin = {val};\n            '''),)\n        else:\n            typ, col = ('jsonb', 'json') if k in jsons else ('text', 'text')\n            val = f'{pg_common.quote_literal(v.decode(\"utf-8\"))}::{typ}'\n            sys_updates += (trampoline.fixup_query(f'''\n                INSERT INTO edgedbinstdata_VER.instdata (key, {col})\n                VALUES({key}, {val})\n                ON CONFLICT (key)\n                DO UPDATE SET {col} = {val};\n            '''),)\n        if k in unversioned:\n            spatches += (sys_updates[-1],)\n\n    # If we're updating the global schema (for extension packages,\n    # perhaps), only run the script once, on the system connection.\n    # Since the state is global, we only should update it once.\n    regular_updates: tuple[str, ...]\n    if sys_update_only:\n        regular_updates = (update,)\n        sys_updates = (patch,) + sys_updates\n    else:\n        regular_updates = spatches + (update,)\n        # FIXME: This is a hack to make the is_user_update cases\n        # work (by ensuring we can always read their current state),\n        # but this is actually a pretty dumb approach and we can do\n        # better.\n        regular_updates += sys_updates\n\n    return regular_updates, sys_updates, updates\n\n\nasync def create_branch(\n    cluster: pgcluster.BaseCluster,\n    schema: s_schema.Schema,\n    conn: metaschema.PGConnection,\n    src_dbname: str,\n    tgt_dbname: str,\n    mode: str,\n    backend_id_fixup_sql: bytes,\n) -> None:\n    \"\"\"Create a new database (branch) based on an existing one.\"\"\"\n\n    # Dump the edgedbpub schema that holds user data and any\n    # extensions.  Also dump edgedbext, which can unfortunately\n    # include some tables/views for the AI extension.  (And some\n    # extensions, which get created with IF NOT EXISTS, so that is\n    # fine.)\n    schema_dump = await cluster.dump_database(\n        src_dbname,\n        include_schemas=('edgedbpub', 'edgedbext'),\n        include_extensions=('*',),\n        schema_only=True,\n    )\n\n    # Tuples types are always kept in edgedbpub, but some already\n    # exist from the std schema, so we need to skip those. We also\n    # need to skip recreating the schema. This requires doing some\n    # annoying postprocessing.\n    to_skip = [\n        str(obj.id) for obj in schema.get_objects(type=s_types.Tuple)\n    ]\n    old_lines = schema_dump.decode('utf-8').split('\\n')\n    new_lines = []\n    skipping = False\n    for line in old_lines:\n        if line == ');' and skipping:\n            skipping = False\n            continue\n        elif line.startswith('CREATE SCHEMA'):\n            continue\n        elif line.startswith('CREATE TYPE'):\n            if any(skip in line for skip in to_skip):\n                skipping = True\n        elif line == 'SET transaction_timeout = 0;':\n            continue\n\n        if skipping:\n            continue\n        new_lines.append(line)\n    s_schema_dump = '\\n'.join(new_lines)\n\n    await conn.sql_execute(s_schema_dump.encode('utf-8'))\n\n    # Copy database config variables over directly\n    copy_cfg_query = f'''\n        select edgedb._copy_database_configs(\n            {pg_common.quote_literal(src_dbname)})\n    '''.encode('utf-8')\n    await conn.sql_execute(copy_cfg_query)\n\n    # HACK: Empty out all schema multi property tables. This is\n    # because the original template has the stdschema in it, and so we\n    # use --on-conflict-do-nothing to avoid conflicts since the dump\n    # will have that in it too. That works, except for multi properties\n    # where it won't conflict, and modules, which might have a different\n    # 'default' module on each side. (Since it isn't in the stdschema,\n    # and could have an old id persisted from an in-place upgrade.)\n    to_delete: set[s_obj.Object] = {\n        prop for prop in schema.get_objects(type=s_props.Property)\n        if prop.get_cardinality(schema).is_multi()\n        and prop.get_name(schema).module not in irtyputils.VIEW_MODULES\n    }\n    to_delete.add(schema.get('schema::Module'))\n\n    for target in to_delete:\n        name = pg_common.get_backend_name(schema, target, catenate=True)\n        await conn.sql_execute(f'delete from {name}'.encode('utf-8'))\n\n    await conn.sql_execute(trampoline.fixup_query(f'''\n        delete from edgedbinstdata_VER.instdata where key = 'configspec_ext'\n    ''').encode('utf-8'))\n\n    # Do the dump/restore for the data. We always need to copy over\n    # edgedbstd, since it has the reflected schema. We copy over\n    # edgedbpub when it is a data branch.\n    data_arg = ['--table=edgedbpub.*'] if mode == qlast.BranchType.DATA else []\n    dump_args = [\n        '--data-only',\n        '--table=edgedbstd.*',\n        f'--table={pg_common.versioned_schema(\"edgedbstd\")}.*',\n        '--table=edgedb._db_config',\n        f'--table={pg_common.versioned_schema(\"edgedbinstdata\")}.instdata',\n        *data_arg,\n        '--disable-triggers',\n        # We need to use --inserts so that we can use --on-conflict-do-nothing.\n        # (See above, in discussion of the HACK.)\n        '--inserts',\n        '--rows-per-insert=100',\n        '--on-conflict-do-nothing',\n    ]\n    await cluster._copy_database(\n        src_dbname, tgt_dbname, dump_args, [],\n    )\n\n    # Restore the search_path as the dump might have altered it.\n    await conn.sql_execute(\n        b\"SELECT pg_catalog.set_config('search_path', 'edgedb', false)\")\n\n    # Fixup the backend ids in the schema to match what is actually in pg.\n    await conn.sql_execute(backend_id_fixup_sql)\n\n\nclass StdlibBits(NamedTuple):\n\n    #: User-visible std.\n    stdschema: s_schema.Schema\n    #: Shadow extended schema for reflection..\n    reflschema: s_schema.Schema\n    #: Standard portion of the global schema\n    global_schema: s_schema.Schema\n    #: SQL text of the procedure to initialize `std` in Postgres.\n    sqltext: str\n    #: SQL text of the procedure to create all `std` scalars for inplace\n    #: upgrades\n    inplace_upgrade_scalar_text: str\n    #: SQL text of the procedure to recreate all extension packages\n    #: for inplace upgrades.\n    inplace_upgrade_extension_packages_text: str\n    #: Descriptors of all the needed trampolines\n    trampolines: list[trampoline.Trampoline]\n    #: A set of ids of all types in std.\n    types: set[uuid.UUID]\n    #: Schema class reflection layout.\n    classlayout: dict[type[s_obj.Object], s_refl.SchemaTypeLayout]\n    #: Schema introspection SQL query.\n    local_intro_query: str\n    #: Global object introspection SQL query.\n    global_intro_query: str\n    #: Number of patches already baked into the stdlib.\n    num_patches: int\n    # TODO: All of sysqueries ought to go here, right?\n    # It would speed up instance creation a bit at little cost.\n    # (Oh, maybe testmode screws this idea up?)\n\n\ndef _make_stdlib(\n    ctx: BootstrapContext,\n    testmode: bool,\n    global_ids: Mapping[str, uuid.UUID],\n) -> StdlibBits:\n    schema: s_schema.ChainedSchema = s_schema.ChainedSchema(\n        s_schema.EMPTY_SCHEMA,\n        s_schema.EMPTY_SCHEMA,\n        s_schema.EMPTY_SCHEMA,\n    )\n    for special_mod in s_schema.SPECIAL_MODULES:\n        schema, _ = s_mod.Module.create_in_schema(\n            schema,\n            name=special_mod,\n            stable_ids=True,\n        )\n\n    current_block = dbops.PLTopBlock()\n\n    trampolines = []\n\n    std_texts = []\n    for modname in s_schema.STD_SOURCES:\n        std_texts.append(s_std.get_std_module_text(modname))\n\n    if testmode:\n        for modname in s_schema.TESTMODE_SOURCES:\n            std_texts.append(s_std.get_std_module_text(modname))\n\n    ddl_text = '\\n'.join(std_texts)\n    types: set[uuid.UUID] = set()\n    std_plans: list[sd.Command] = []\n\n    specials = []\n\n    def _collect_special(cmd):\n        if isinstance(\n            cmd,\n            (dbops.CreateEnum, delta_cmds.CreateExtensionPackage),\n        ):\n            specials.append(cmd)\n        elif isinstance(cmd, dbops.CommandGroup):\n            for sub in cmd.commands:\n                _collect_special(sub)\n        elif isinstance(cmd, delta_cmds.MetaCommand):\n            for sub in cmd.pgops:\n                _collect_special(sub)\n\n    for ddl_cmd in edgeql.parse_block(ddl_text):\n        assert isinstance(ddl_cmd, qlast.DDLCommand)\n        delta_command = s_ddl.delta_from_ddl(\n            ddl_cmd, modaliases={}, schema=schema, stdmode=True)\n\n        # Apply and adapt delta, build native delta plan, which\n        # will also update the schema.\n        schema, plan, tplan = _process_delta(ctx, delta_command, schema)\n        assert isinstance(plan, delta_cmds.DeltaRoot)\n        std_plans.append(delta_command)\n        _collect_special(plan)\n\n        types.update(plan.new_types)\n        plan.generate(current_block)\n        trampolines.extend(tplan.trampolines)\n\n    _, schema_version = s_std.make_schema_version(schema)\n    schema, plan, tplan = _process_delta(ctx, schema_version, schema)\n    std_plans.append(schema_version)\n    plan.generate(current_block)\n    trampolines.extend(tplan.trampolines)\n\n    stdglobals = '\\n'.join([\n        f'''CREATE SUPERUSER ROLE {edbdef.EDGEDB_SUPERUSER} {{\n            SET id := <uuid>'{global_ids[edbdef.EDGEDB_SUPERUSER]}'\n        }};''',\n    ])\n\n    schema = _execute_edgeql_ddl(schema, stdglobals)\n\n    _, global_schema_version = s_std.make_global_schema_version(schema)\n    schema, plan, tplan = _process_delta(ctx, global_schema_version, schema)\n    std_plans.append(global_schema_version)\n    plan.generate(current_block)\n    trampolines.extend(tplan.trampolines)\n\n    reflection = s_refl.generate_structure(schema)\n    reflschema, reflplan, treflplan = _process_delta(\n        ctx, reflection.intro_schema_delta, schema)\n\n    # Any collection types that made it into reflschema need to get\n    # to get pulled back into the stdschema, or else they will be in\n    # an inconsistent state.\n    for obj in reflschema.get_objects(type=s_types.Collection):\n        if not schema.has_object(obj.id):\n            delta = sd.DeltaRoot()\n            delta.add(obj.as_shell(reflschema).as_create_delta(reflschema))\n            schema = cast(\n                s_schema.ChainedSchema,\n                delta.apply(schema, sd.CommandContext(stdmode=True))\n            )\n    assert isinstance(schema, s_schema.ChainedSchema)\n\n    assert current_block is not None\n    reflplan.generate(current_block)\n    trampolines.extend(treflplan.trampolines)\n    subblock = current_block.add_block()\n\n    compiler = edbcompiler.new_compiler(\n        std_schema=schema.get_top_schema(),\n        reflection_schema=reflschema.get_top_schema(),\n        schema_class_layout=reflection.class_layout,  # type: ignore\n    )\n\n    backend_runtime_params = ctx.cluster.get_runtime_params()\n    compilerctx = edbcompiler.new_compiler_context(\n        compiler_state=compiler.state,\n        user_schema=reflschema.get_top_schema(),\n        global_schema=schema.get_global_schema(),\n        bootstrap_mode=True,\n        backend_runtime_params=backend_runtime_params,\n    )\n\n    for std_plan in std_plans:\n        edbcompiler.compile_schema_storage_in_delta(\n            ctx=compilerctx,\n            delta=std_plan,\n            block=subblock,\n        )\n\n    compilerctx = edbcompiler.new_compiler_context(\n        compiler_state=compiler.state,\n        user_schema=reflschema.get_top_schema(),\n        global_schema=schema.get_global_schema(),\n        bootstrap_mode=True,\n        internal_schema_mode=True,\n        backend_runtime_params=backend_runtime_params,\n    )\n    edbcompiler.compile_schema_storage_in_delta(\n        ctx=compilerctx,\n        delta=reflection.intro_schema_delta,\n        block=subblock,\n    )\n\n    sqltext = current_block.to_string()\n\n    local_intro_sql, global_intro_sql = compile_intro_queries_stdlib(\n        compiler=compiler,\n        user_schema=reflschema.get_top_schema(),\n        global_schema=schema.get_global_schema(),\n        reflection=reflection,\n    )\n\n    # Sigh, we need to be able to create all std scalar types that\n    # might get added.\n    #\n    # TODO: Also collect tuple types, and generalize enum handling to\n    # be able to *add* enum fields. (Which will involve some\n    # pl/pgsql??)\n    scalar_block = dbops.PLTopBlock()\n    extension_package_block = dbops.PLTopBlock()\n    for cmd in specials:\n        if isinstance(cmd, dbops.CreateEnum):\n            ncmd = dbops.CreateEnum(\n                dbops.Enum(cmd.name, cmd.values),\n                neg_conditions=[dbops.EnumExists(cmd.name)],\n            )\n            ncmd.generate(scalar_block)\n        elif isinstance(cmd, delta_cmds.CreateExtensionPackage):\n            cmd.generate(extension_package_block)\n\n    # Got it!\n    return StdlibBits(\n        stdschema=schema.get_top_schema(),\n        reflschema=reflschema.get_top_schema(),\n        global_schema=schema.get_global_schema(),\n        sqltext=sqltext,\n        inplace_upgrade_scalar_text=scalar_block.to_string(),\n        inplace_upgrade_extension_packages_text=(\n            extension_package_block.to_string()),\n        trampolines=trampolines,\n        types=types,\n        classlayout=reflection.class_layout,\n        local_intro_query=local_intro_sql,\n        global_intro_query=global_intro_sql,\n        num_patches=len(patches.PATCHES),\n    )\n\n\nasync def _amend_stdlib(\n    ctx: BootstrapContext,\n    ddl_text: str,\n    stdlib: StdlibBits,\n) -> tuple[StdlibBits, str, list[trampoline.Trampoline]]:\n    schema = s_schema.ChainedSchema(\n        s_schema.EMPTY_SCHEMA,\n        stdlib.stdschema,\n        stdlib.global_schema,\n    )\n    reflschema = stdlib.reflschema\n\n    topblock = dbops.PLTopBlock()\n    plans = []\n    trampolines = []\n\n    context = sd.CommandContext(stdmode=True)\n\n    for ddl_cmd in edgeql.parse_block(ddl_text):\n        assert isinstance(ddl_cmd, qlast.DDLCommand)\n        delta_command = s_ddl.delta_from_ddl(\n            ddl_cmd, modaliases={}, schema=schema, stdmode=True)\n\n        # Apply and adapt delta, build native delta plan, which\n        # will also update the schema.\n        schema, plan, tplan = _process_delta(ctx, delta_command, schema)\n        reflschema = delta_command.apply(reflschema, context)\n        plan.generate(topblock)\n        plans.append(plan)\n        trampolines.extend(tplan.trampolines)\n\n    compiler = edbcompiler.new_compiler(\n        std_schema=schema.get_top_schema(),\n        reflection_schema=reflschema,\n        schema_class_layout=stdlib.classlayout,  # type: ignore\n    )\n\n    compilerctx = edbcompiler.new_compiler_context(\n        compiler_state=compiler.state,\n        user_schema=schema.get_top_schema(),\n        global_schema=schema.get_global_schema(),\n    )\n    for plan in plans:\n        edbcompiler.compile_schema_storage_in_delta(\n            ctx=compilerctx,\n            delta=plan,\n            block=topblock,\n        )\n\n    sqltext = topblock.to_string()\n\n    return stdlib._replace(\n        stdschema=schema.get_top_schema(),\n        global_schema=schema.get_global_schema(),\n        reflschema=reflschema,\n    ), sqltext, trampolines\n\n\ndef compile_intro_queries_stdlib(\n    *,\n    compiler: edbcompiler.Compiler,\n    user_schema: s_schema.Schema,\n    global_schema: s_schema.Schema=s_schema.EMPTY_SCHEMA,\n    reflection: s_refl.SchemaReflectionParts,\n) -> tuple[str, str]:\n    compilerctx = edbcompiler.new_compiler_context(\n        compiler_state=compiler.state,\n        user_schema=user_schema,\n        global_schema=global_schema,\n        schema_reflection_mode=True,\n        output_format=edbcompiler.OutputFormat.JSON_ELEMENTS,\n    )\n\n    # The introspection query bits are returned in chunks\n    # because it's a large UNION and we currently generate SQL\n    # that is much harder for Postgres to plan as opposed to a\n    # straight flat UNION.\n    sql_intro_local_parts = []\n    sql_intro_global_parts = []\n    for intropart in reflection.local_intro_parts:\n        sql_intro_local_parts.append(\n            compile_single_query(\n                intropart,\n                compilerctx=compilerctx,\n            ),\n        )\n\n    for intropart in reflection.global_intro_parts:\n        sql_intro_global_parts.append(\n            compile_single_query(\n                intropart,\n                compilerctx=compilerctx,\n            ),\n        )\n\n    local_intro_sql = ' UNION ALL '.join(\n        f'({x})' for x in sql_intro_local_parts)\n    local_intro_sql = f'''\n        WITH intro(c) AS ({local_intro_sql})\n        SELECT json_agg(intro.c) FROM intro\n    '''\n\n    global_intro_sql = ' UNION ALL '.join(\n        f'({x})' for x in sql_intro_global_parts)\n    global_intro_sql = f'''\n        WITH intro(c) AS ({global_intro_sql})\n        SELECT json_agg(intro.c) FROM intro\n    '''\n\n    return local_intro_sql, global_intro_sql\n\n\ndef _calculate_src_hash() -> bytes:\n    return buildmeta.hash_dirs(\n        buildmeta.get_cache_src_dirs(),\n        extra_files=[\n            __file__,\n            pathlib.Path(__file__).parent.parent / 'buildmeta.py',\n        ],\n    )\n\n\ndef _get_cache_dir() -> pathlib.Path | None:\n    if specified_cache_dir := os.environ.get('_EDGEDB_WRITE_DATA_CACHE_TO'):\n        return pathlib.Path(specified_cache_dir)\n    else:\n        return None\n\n\ndef read_data_cache(\n    file_name: str,\n    pickled: bool,\n    *,\n    src_hash: bytes | None = None,\n    cache_dir: pathlib.Path | None = None,\n) -> Any:\n    if src_hash is None:\n        src_hash = _calculate_src_hash()\n    if cache_dir is None:\n        cache_dir = _get_cache_dir()\n    return buildmeta.read_data_cache(\n        src_hash, file_name, source_dir=cache_dir, pickled=pickled)\n\n\ndef cleanup_tpldbdump(tpldbdump: bytes) -> bytes:\n    # Excluding the \"edgedbext\" schema above apparently\n    # doesn't apply to extensions created in that schema,\n    # so we have to resort to commenting out extension\n    # statements in the dump.\n    tpldbdump = re.sub(\n        rb'^(CREATE|COMMENT ON) EXTENSION.*$',\n        rb'-- \\g<0>',\n        tpldbdump,\n        flags=re.MULTILINE,\n    )\n\n    # PostgreSQL 14 emits multirange_type_name in RANGE definitions,\n    # elide these to preserve compatibility with earlier servers.\n    tpldbdump = re.sub(\n        rb',\\s*multirange_type_name\\s*=[^,\\n]+',\n        rb'',\n        tpldbdump,\n        flags=re.MULTILINE,\n    )\n\n    # PostgreSQL 17 adds a transaction_timeout config setting that\n    # didn't exist on earlier versions.\n    tpldbdump = re.sub(\n        rb'^SET transaction_timeout = 0;$',\n        rb'',\n        tpldbdump,\n        flags=re.MULTILINE,\n    )\n\n    tpldbdump = re.sub(\n        rb'^CREATE SCHEMA ',\n        rb'CREATE SCHEMA IF NOT EXISTS ',\n        tpldbdump,\n        flags=re.MULTILINE,\n    )\n\n    return tpldbdump\n\n\nasync def _init_stdlib(\n    ctx: BootstrapContext,\n    testmode: bool,\n    global_ids: Mapping[str, uuid.UUID],\n) -> tuple[\n    StdlibBits,\n    config.Spec,\n    edbcompiler.Compiler,\n]:\n    in_dev_mode = devmode.is_in_dev_mode()\n    conn = ctx.conn\n    cluster = ctx.cluster\n    args = ctx.args\n\n    tpldbdump_cache = 'backend-tpldbdump.sql'\n    src_hash = _calculate_src_hash()\n    cache_dir = _get_cache_dir()\n\n    stdlib: Optional[StdlibBits] = read_data_cache(\n        STDLIB_CACHE_FILE_NAME,\n        pickled=True,\n        src_hash=src_hash,\n        cache_dir=cache_dir,\n    )\n    tpldbdump_package = read_data_cache(\n        tpldbdump_cache,\n        pickled=True,\n        src_hash=src_hash,\n        cache_dir=cache_dir,\n    )\n\n    tpldbdump, tpldbdump_inplace = None, None\n    if tpldbdump_package:\n        tpldbdump, tpldbdump_inplace = tpldbdump_package\n    else:\n        assert not args.inplace_upgrade_prepare, (\n            \"Gel must have a valid bootstrap cache to use inplace upgrade\"\n        )\n\n    stdlib_was_none = stdlib is None\n    if stdlib is None:\n        logger.info('Compiling the standard library...')\n        stdlib = _make_stdlib(\n            ctx, in_dev_mode or testmode, global_ids)\n\n    config_spec = config.load_spec_from_schema(stdlib.stdschema)\n\n    # If we recompiled the stdlib or need to generate a tpldbdump, we\n    # need to generate bootstrap commands and trampolines, and update\n    # the stdlib's trampolines if we compiled it.\n    bootstrap_commands = None\n    if stdlib_was_none or tpldbdump is None:\n        bootstrap_commands, bootstrap_trampolines = (\n            metaschema.get_bootstrap_commands(config_spec)\n        )\n        if stdlib_was_none:\n            stdlib = stdlib._replace(\n                trampolines=bootstrap_trampolines + stdlib.trampolines\n            )\n\n    trampolines = []\n\n    # We need to set this up early, since later code depends on the\n    # backend_instance_params of the instdata table. But it also\n    # obviously can't go into the tpldbdump, since it is dynamic.\n    trampolines.extend(await metaschema.generate_instdata_table(\n        conn,\n    ))\n    await _populate_misc_instance_data(ctx)\n\n    backend_params = cluster.get_runtime_params()\n    if not args.inplace_upgrade_prepare:\n        logger.info('Creating the necessary PostgreSQL extensions...')\n        await metaschema.create_pg_extensions(conn, backend_params)\n\n    trampolines.extend(stdlib.trampolines)\n\n    eff_tpldbdump = (\n        tpldbdump_inplace if args.inplace_upgrade_prepare else tpldbdump)\n    if eff_tpldbdump is None:\n        logger.info('Populating internal SQL structures...')\n        assert bootstrap_commands is not None\n        block = dbops.PLTopBlock()\n\n        fixed_bootstrap_commands = metaschema.get_fixed_bootstrap_commands()\n        fixed_bootstrap_commands.generate(block)\n\n        bootstrap_commands.generate(block)\n        await _execute_block(conn, block)\n        logger.info('Executing the standard library...')\n        await _execute(conn, stdlib.sqltext)\n\n        if in_dev_mode or cache_dir:\n            tpl_db_name = edbdef.EDGEDB_TEMPLATE_DB\n            tpl_pg_db_name = cluster.get_db_name(tpl_db_name)\n            tpldbdump = await cluster.dump_database(\n                tpl_pg_db_name,\n                exclude_schemas=[\n                    pg_common.versioned_schema('edgedbinstdata'),\n                    'edgedbext',\n                    backend_params.instance_params.ext_schema,\n                ],\n                dump_object_owners=False,\n            )\n\n            tpldbdump = cleanup_tpldbdump(tpldbdump)\n\n            # The instance metadata doesn't go in the dump, so collect\n            # it ourselves.\n            global_metadata = await conn.sql_fetch_val(\n                trampoline.fixup_query(\n                    \"SELECT edgedb_VER.get_database_metadata($1)::json\"\n                ).encode(\"utf-8\"),\n                args=[tpl_db_name.encode(\"utf-8\")],\n            )\n            global_metadata = json.loads(global_metadata)\n\n            pl_block = dbops.PLTopBlock()\n\n            set_metadata_text = dbops.SetMetadata(\n                dbops.DatabaseWithTenant(name=tpl_db_name),\n                global_metadata,\n            ).code_with_block(pl_block)\n\n            set_single_db_metadata_text = dbops.SetSingleDBMetadata(\n                edbdef.EDGEDB_TEMPLATE_DB, global_metadata\n            ).code_with_block(pl_block)\n\n            pl_block.add_command(textwrap.dedent(trampoline.fixup_query(f\"\"\"\\\n                IF (edgedb_VER.get_backend_capabilities()\n                    & {int(params.BackendCapabilities.CREATE_DATABASE)}) != 0\n                THEN\n                {textwrap.indent(set_metadata_text, '    ')}\n                ELSE\n                {textwrap.indent(set_single_db_metadata_text, '    ')}\n                END IF\n                \"\"\")))\n\n            text = pl_block.to_string()\n\n            tpldbdump += b'\\n' + text.encode('utf-8')\n\n            tpldbdump_inplace = await cluster.dump_database(\n                tpl_pg_db_name,\n                include_schemas=[\n                    pg_common.versioned_schema('edgedb'),\n                    pg_common.versioned_schema('edgedbstd'),\n                    pg_common.versioned_schema('edgedbsql'),\n                ],\n                dump_object_owners=False,\n            )\n            tpldbdump_inplace = (\n                stdlib.inplace_upgrade_scalar_text.encode('utf-8') +\n                cleanup_tpldbdump(tpldbdump_inplace)\n            )\n\n            buildmeta.write_data_cache(\n                (tpldbdump, tpldbdump_inplace),\n                src_hash,\n                tpldbdump_cache,\n                pickled=True,\n                target_dir=cache_dir,\n            )\n\n            buildmeta.write_data_cache(\n                stdlib,\n                src_hash,\n                STDLIB_CACHE_FILE_NAME,\n                target_dir=cache_dir,\n            )\n    else:\n        logger.info('Initializing the standard library...')\n        await _execute(conn, eff_tpldbdump.decode('utf-8'))\n        # Restore the search_path as the dump might have altered it.\n        await conn.sql_execute(\n            b\"SELECT pg_catalog.set_config('search_path', 'edgedb', false)\")\n\n    if not in_dev_mode and testmode:\n        # Running tests on a production build.\n        for modname in s_schema.TESTMODE_SOURCES:\n            stdlib, testmode_sql, new_trampolines = await _amend_stdlib(\n                ctx,\n                s_std.get_std_module_text(modname),\n                stdlib,\n            )\n            await conn.sql_execute(testmode_sql.encode(\"utf-8\"))\n            trampolines.extend(new_trampolines)\n        # _testmode includes extra config settings, so make sure\n        # those are picked up...\n        config_spec = config.load_spec_from_schema(stdlib.stdschema)\n        # ...and that config functions dependent on it are regenerated\n        await metaschema.regenerate_config_support_functions(conn, config_spec)\n\n    logger.info('Finalizing database setup...')\n\n    # Make sure that schema backend_id properties are in sync with\n    # the database.\n    # XXX: is ScalarType sufficient here?\n\n    compiler = edbcompiler.new_compiler(\n        std_schema=stdlib.stdschema,\n        reflection_schema=stdlib.reflschema,\n        schema_class_layout=stdlib.classlayout,\n        global_intro_query=stdlib.global_intro_query,\n        local_intro_query=stdlib.local_intro_query,\n    )\n    _, sql = compile_bootstrap_script(\n        compiler,\n        stdlib.reflschema,\n        '''\n        SELECT schema::ScalarType {\n            id,\n            backend_id,\n        } FILTER .builtin AND NOT (.abstract ?? False);\n        ''',\n        expected_cardinality_one=False,\n    )\n    schema = stdlib.stdschema\n    typemap = await conn.sql_fetch_val(sql.encode(\"utf-8\"))\n    for entry in json.loads(typemap):\n        t = schema.get_by_id(uuidgen.UUID(entry['id']))\n        schema = t.set_field_value(\n            schema, 'backend_id', entry['backend_id'])\n\n    # Patch functions referring to extensions, because\n    # some backends require extensions to be hosted in\n    # hardcoded schemas (e.g. Heroku)\n    await metaschema.patch_pg_extensions(conn, backend_params)\n\n    stdlib = stdlib._replace(stdschema=schema)\n    version_key = patches.get_version_key(stdlib.num_patches)\n\n    # stdschema and reflschema are combined in one pickle to preserve sharing\n    await _store_static_bin_cache(\n        ctx,\n        f'std_and_reflection_schema{version_key}',\n        pickle.dumps(\n            (schema, stdlib.reflschema),\n            protocol=pickle.HIGHEST_PROTOCOL,\n        ),\n    )\n\n    await _store_static_bin_cache(\n        ctx,\n        f'global_schema{version_key}',\n        pickle.dumps(stdlib.global_schema, protocol=pickle.HIGHEST_PROTOCOL),\n    )\n\n    await _store_static_bin_cache(\n        ctx,\n        f'classlayout{version_key}',\n        pickle.dumps(stdlib.classlayout, protocol=pickle.HIGHEST_PROTOCOL),\n    )\n\n    await _store_static_text_cache(\n        ctx,\n        f'local_intro_query{version_key}',\n        stdlib.local_intro_query,\n    )\n\n    await _store_static_text_cache(\n        ctx,\n        f'global_intro_query{version_key}',\n        stdlib.global_intro_query,\n    )\n\n    trampolines.extend(await metaschema.generate_support_views(\n        conn, stdlib.reflschema, cluster.get_runtime_params()\n    ))\n    trampolines.extend(\n        await metaschema.generate_support_functions(conn, stdlib.reflschema)\n    )\n\n    compiler = edbcompiler.new_compiler(\n        std_schema=schema,\n        reflection_schema=stdlib.reflschema,\n        schema_class_layout=stdlib.classlayout,\n        global_intro_query=stdlib.global_intro_query,\n        local_intro_query=stdlib.local_intro_query,\n    )\n\n    trampolines.extend(\n        await metaschema.generate_more_support_functions(\n            conn, compiler, stdlib.reflschema, testmode\n        )\n    )\n\n    await _store_static_json_cache(\n        ctx,\n        'configspec',\n        config.spec_to_json(config_spec),\n    )\n    await _store_static_json_cache(\n        ctx,\n        'configspec_ext',\n        json.dumps({}),\n    )\n\n    # Create all the trampolines\n    tramps = dbops.CommandGroup()\n    tramps.add_commands([t.make() for t in trampolines])\n    block = dbops.PLTopBlock()\n    tramps.generate(block)\n\n    if args.inplace_upgrade_prepare:\n        trampoline_text = block.to_string()\n        await _store_static_text_cache(\n            ctx,\n            f'trampoline_pivot_query',\n            trampoline_text,\n        )\n        await _store_static_text_cache(\n            ctx,\n            f'global_schema_update_query',\n            stdlib.inplace_upgrade_extension_packages_text,\n        )\n    else:\n        await _execute_block(conn, block)\n\n    return stdlib, config_spec, compiler\n\n\nasync def _init_defaults(schema, compiler, conn):\n    script = '''\n        CREATE MODULE default;\n    '''\n\n    schema, sql = compile_bootstrap_script(\n        compiler, schema, script, bootstrap_mode=False\n    )\n    await _execute(conn, sql)\n    return schema\n\n\nasync def _configure(\n    ctx: BootstrapContext,\n    config_spec: config.Spec,\n    schema: s_schema.Schema,\n    compiler: edbcompiler.Compiler,\n) -> None:\n    settings: Mapping[str, config.SettingValue] = {}\n\n    config_json = config.to_json(config_spec, settings, include_source=False)\n    block = dbops.PLTopBlock()\n    metadata = {'sysconfig': json.loads(config_json)}\n    if ctx.cluster.get_runtime_params().has_create_database:\n        dbops.UpdateMetadata(\n            dbops.DatabaseWithTenant(name=edbdef.EDGEDB_SYSTEM_DB),\n            metadata,\n        ).generate(block)\n    else:\n        dbops.UpdateSingleDBMetadata(\n            edbdef.EDGEDB_SYSTEM_DB, metadata,\n        ).generate(block)\n\n    await _execute_block(ctx.conn, block)\n\n    backend_params = ctx.cluster.get_runtime_params()\n    for setname in config_spec:\n        setting = config_spec[setname]\n        if (\n            setting.backend_setting\n            and setting.default is not None\n            and (\n                # Do not attempt to run CONFIGURE INSTANCE on\n                # backends that don't support it.\n                # TODO: this should be replaced by instance-wide\n                #       emulation at backend connection time.\n                backend_params.has_configfile_access\n            )\n        ):\n            script = qlcodegen.generate_source(\n                qlast.ConfigSet(\n                    name=qlast.ObjectRef(name=setting.name),\n                    scope=qltypes.ConfigScope.INSTANCE,\n                    expr=s_utils.const_ast_from_python(setting.default),\n                )\n            )\n            schema, sql = compile_bootstrap_script(compiler, schema, script)\n            await _execute(ctx.conn, sql)\n\n\ndef compile_sys_queries(\n    schema: s_schema.Schema,\n    compiler: edbcompiler.Compiler,\n    config_spec: config.Spec,\n) -> tuple[dict[str, str], bytes, bytes]:\n    queries = {}\n\n    _, sql = compile_bootstrap_script(\n        compiler,\n        schema,\n        'SELECT cfg::_get_config_json_internal()',\n        expected_cardinality_one=True,\n    )\n\n    queries['config'] = sql\n\n    _, sql = compile_bootstrap_script(\n        compiler,\n        schema,\n        \"SELECT cfg::_get_config_json_internal(sources := ['database'])\",\n        expected_cardinality_one=True,\n    )\n\n    queries['dbconfig'] = sql\n\n    _, sql = compile_bootstrap_script(\n        compiler,\n        schema,\n        \"\"\"\n        SELECT cfg::_get_config_json_internal(max_source := 'system override')\n        \"\"\",\n        expected_cardinality_one=True,\n    )\n\n    queries['sysconfig'] = sql\n\n    _, sql = compile_bootstrap_script(\n        compiler,\n        schema,\n        \"\"\"\n        SELECT cfg::_get_config_json_internal(max_source := 'postgres client')\n        \"\"\",\n        expected_cardinality_one=True,\n    )\n\n    queries['sysconfig_default'] = sql\n\n    _, sql = compile_bootstrap_script(\n        compiler,\n        schema,\n        f\"\"\"SELECT (\n            SELECT sys::Branch\n            FILTER .name != \"{edbdef.EDGEDB_TEMPLATE_DB}\"\n        ).name\"\"\",\n        expected_cardinality_one=False,\n    )\n\n    queries['listdbs'] = sql\n\n    role_query = '''\n        SELECT sys::Role {\n            name,\n            superuser,\n            password,\n            branches,\n            all_permissions,\n            apply_access_policies_pg_default,\n        };\n    '''\n    _, sql = compile_bootstrap_script(\n        compiler,\n        schema,\n        role_query,\n        expected_cardinality_one=False,\n    )\n    queries['roles'] = sql\n\n    tids_query = '''\n        SELECT schema::ScalarType {\n            id,\n            backend_id,\n        } FILTER .id IN <uuid>json_array_unpack(<json>$ids);\n    '''\n    _, sql = compile_bootstrap_script(\n        compiler,\n        schema,\n        tids_query,\n        expected_cardinality_one=False,\n    )\n\n    queries['backend_tids'] = sql\n\n    # When we restore a database from a dump, OIDs for non-system\n    # Postgres types might get skewed as they are not part of the dump.\n    # A good example of that is `std::bigint` which is implemented as\n    # a custom domain type. The OIDs are stored under\n    # `schema::Object.backend_id` property and are injected into\n    # array query arguments.\n    #\n    # The code below re-syncs backend_id properties of Gel builtin\n    # types with the actual OIDs in the DB.\n    backend_id_fixup_edgeql = '''\n        UPDATE schema::ScalarType\n        FILTER\n            NOT (.abstract ?? False)\n            AND NOT (.transient ?? False)\n        SET {\n            backend_id := sys::_get_pg_type_for_edgedb_type(\n                .id,\n                .__type__.name,\n                <uuid>{},\n                [is schema::ScalarType].sql_type ?? (\n                    select [is schema::ScalarType]\n                    .bases[is schema::ScalarType] limit 1\n                ).sql_type,\n            )\n        };\n        UPDATE schema::Tuple\n        FILTER\n            NOT (.abstract ?? False)\n            AND NOT (.transient ?? False)\n        SET {\n            backend_id := sys::_get_pg_type_for_edgedb_type(\n                .id,\n                .__type__.name,\n                <uuid>{},\n                [is schema::ScalarType].sql_type ?? (\n                    select [is schema::ScalarType]\n                    .bases[is schema::ScalarType] limit 1\n                ).sql_type,\n            )\n        };\n        UPDATE {schema::Range, schema::MultiRange}\n        FILTER\n            NOT (.abstract ?? False)\n            AND NOT (.transient ?? False)\n        SET {\n            backend_id := sys::_get_pg_type_for_edgedb_type(\n                .id,\n                .__type__.name,\n                .element_type.id,\n                <str>{},\n            )\n        };\n        UPDATE schema::Array\n        FILTER\n            NOT (.abstract ?? False)\n            AND NOT (.transient ?? False)\n            AND NOT .element_type IS schema::Array\n        SET {\n            backend_id := sys::_get_pg_type_for_edgedb_type(\n                .id,\n                .__type__.name,\n                .element_type.id,\n                <str>{},\n            )\n        };\n    '''\n    _, sql = compile_bootstrap_script(\n        compiler,\n        schema,\n        backend_id_fixup_edgeql,\n    )\n    queries['backend_id_fixup'] = sql\n\n    report_settings: list[str] = []\n    for setname in config_spec:\n        setting = config_spec[setname]\n        if setting.report:\n            report_settings.append(setname)\n\n    report_configs_query = f'''\n        SELECT assert_single(cfg::Config {{\n            {', '.join(report_settings)}\n        }});\n    '''\n\n    units = edbcompiler.compile(\n        ctx=edbcompiler.new_compiler_context(\n            compiler_state=compiler.state,\n            user_schema=schema,\n            expected_cardinality_one=True,\n            json_parameters=False,\n            output_format=edbcompiler.OutputFormat.BINARY,\n            bootstrap_mode=True,\n        ),\n        source=edgeql.Source.from_string(report_configs_query),\n    ).units\n    assert len(units) == 1\n\n    report_configs_typedesc_2_0 = units[0].out_type_id + units[0].out_type_data\n    queries['report_configs'] = units[0].sql.decode()\n\n    units = edbcompiler.compile(\n        ctx=edbcompiler.new_compiler_context(\n            compiler_state=compiler.state,\n            user_schema=schema,\n            expected_cardinality_one=True,\n            json_parameters=False,\n            output_format=edbcompiler.OutputFormat.BINARY,\n            bootstrap_mode=True,\n            protocol_version=(1, 0),\n        ),\n        source=edgeql.Source.from_string(report_configs_query),\n    ).units\n    assert len(units) == 1\n    report_configs_typedesc_1_0 = units[0].out_type_id + units[0].out_type_data\n\n    return (\n        queries,\n        report_configs_typedesc_1_0,\n        report_configs_typedesc_2_0,\n    )\n\n\nasync def _populate_misc_instance_data(\n    ctx: BootstrapContext,\n) -> dict[str, Any]:\n\n    mock_auth_nonce = scram.generate_nonce()\n    json_instance_data = {\n        'version': dict(buildmeta.get_version_dict()),\n        'catver': edbdef.EDGEDB_CATALOG_VERSION,\n        'mock_auth_nonce': mock_auth_nonce,\n    }\n\n    await _store_static_json_cache(\n        ctx,\n        'instancedata',\n        json.dumps(json_instance_data),\n    )\n\n    backend_params = ctx.cluster.get_runtime_params()\n    instance_params = backend_params.instance_params\n    await _store_static_json_cache(\n        ctx,\n        'backend_instance_params',\n        json.dumps(instance_params._asdict()),\n    )\n\n    if not backend_params.has_create_role:\n        json_single_role_metadata = {\n            'id': str(uuidgen.uuid1mc()),\n            'name': edbdef.EDGEDB_SUPERUSER,\n            'tenant_id': backend_params.tenant_id,\n            'builtin': False,\n        }\n        await _store_static_json_cache(\n            ctx,\n            'single_role_metadata',\n            json.dumps(json_single_role_metadata),\n        )\n\n    if not backend_params.has_create_database:\n        await _store_static_json_cache(\n            ctx,\n            f'{edbdef.EDGEDB_TEMPLATE_DB}metadata',\n            json.dumps({}),\n        )\n        await _store_static_json_cache(\n            ctx,\n            f'{edbdef.EDGEDB_SYSTEM_DB}metadata',\n            json.dumps({}),\n        )\n\n    await _store_static_json_cache(\n        ctx,\n        'sql_default_fe_settings',\n        json.dumps(\n            [\n                {\"name\": key, \"value\": pg_common.setting_to_sql(key, val)}\n                for key, val in dbstate.DEFAULT_SQL_FE_SETTINGS.items()\n            ]\n        )\n    )\n\n    return json_instance_data\n\n\nasync def _create_edgedb_database(\n    ctx: BootstrapContext,\n    database: str,\n    owner: str,\n    *,\n    builtin: bool = False,\n    objid: Optional[uuid.UUID] = None,\n) -> uuid.UUID:\n    logger.info(f'Creating database: {database}')\n    block = dbops.SQLBlock()\n    if objid is None:\n        objid = uuidgen.uuid1mc()\n    instance_params = ctx.cluster.get_runtime_params().instance_params\n    db = dbops.Database(\n        ctx.cluster.get_db_name(database),\n        owner=ctx.cluster.get_role_name(owner),\n        metadata=dict(\n            id=str(objid),\n            tenant_id=instance_params.tenant_id,\n            name=database,\n            builtin=builtin,\n        ),\n    )\n    tpl_db = ctx.cluster.get_db_name(edbdef.EDGEDB_TEMPLATE_DB)\n    dbops.CreateDatabase(db, template=tpl_db).generate(block)\n\n    # Background tasks on some hosted provides like DO seem to sometimes make\n    # their own connections to the template DB, so do a retry loop on it.\n    rloop = retryloop.RetryLoop(\n        backoff=retryloop.exp_backoff(),\n        timeout=10.0,\n        ignore=pgcon.errors.BackendError,\n    )\n    async for iteration in rloop:\n        async with iteration:\n            await _execute_block(ctx.conn, block)\n\n    return objid\n\n\nasync def _set_edgedb_database_metadata(\n    ctx: BootstrapContext,\n    database: str,\n    *,\n    objid: Optional[uuid.UUID] = None,\n) -> uuid.UUID:\n    logger.info(f'Configuring database: {database}')\n    block = dbops.SQLBlock()\n    if objid is None:\n        objid = uuidgen.uuid1mc()\n    instance_params = ctx.cluster.get_runtime_params().instance_params\n    db = dbops.Database(ctx.cluster.get_db_name(database))\n    metadata = dict(\n        id=str(objid),\n        tenant_id=instance_params.tenant_id,\n        name=database,\n        builtin=False,\n    )\n    dbops.SetMetadata(db, metadata).generate(block)\n    await _execute_block(ctx.conn, block)\n    return objid\n\n\ndef _pg_log_listener(severity, message):\n    if severity == 'WARNING':\n        level = logging.WARNING\n    else:\n        level = logging.DEBUG\n    logger.log(level, message)\n\n\nasync def _get_instance_data(\n    conn: metaschema.PGConnection,\n    *,\n    versioned: bool=True,\n) -> dict[str, Any]:\n    schema = 'edgedbinstdata_VER' if versioned else 'edgedbinstdata'\n    data = await conn.sql_fetch_val(\n        trampoline.fixup_query(f\"\"\"\n        SELECT json::json\n        FROM {schema}.instdata\n        WHERE key = 'instancedata'\n        \"\"\").encode('utf-8'),\n    )\n    return json.loads(data)\n\n\nasync def _check_catalog_compatibility(\n    ctx: BootstrapContext,\n) -> PGConnectionProxy:\n    tenant_id = ctx.cluster.get_runtime_params().tenant_id\n    if ctx.mode == ClusterMode.single_database:\n        sys_db = await ctx.conn.sql_fetch_val(\n            trampoline.fixup_query(\"\"\"\n            SELECT current_database()\n            FROM edgedbinstdata_VER.instdata\n            WHERE key = $1 AND json->>'tenant_id' = $2\n            \"\"\").encode('utf-8'),\n            args=[\n                f\"{edbdef.EDGEDB_TEMPLATE_DB}metadata\".encode(\"utf-8\"),\n                tenant_id.encode(\"utf-8\"),\n            ],\n        )\n    else:\n        is_default_tenant = tenant_id == buildmeta.get_default_tenant_id()\n\n        if is_default_tenant:\n            sys_db = await ctx.conn.sql_fetch_val(\n                b\"\"\"\n                SELECT datname\n                FROM pg_database\n                WHERE datname LIKE '%' || $1\n                ORDER BY\n                    datname = $1,\n                    datname DESC\n                LIMIT 1\n                \"\"\",\n                args=[\n                    edbdef.EDGEDB_SYSTEM_DB.encode(\"utf-8\"),\n                ],\n            )\n        else:\n            sys_db = await ctx.conn.sql_fetch_val(\n                b\"\"\"\n                SELECT datname\n                FROM pg_database\n                WHERE datname = $1\n                \"\"\",\n                args=[\n                    ctx.cluster.get_db_name(\n                        edbdef.EDGEDB_SYSTEM_DB).encode(\"utf-8\"),\n                ],\n            )\n\n    if not sys_db:\n        raise errors.ConfigurationError(\n            'database instance is corrupt',\n            details=(\n                f'The database instance does not appear to have been fully '\n                f'initialized or has been corrupted.'\n            )\n        )\n\n    conn = PGConnectionProxy(\n        ctx.cluster,\n        source_description=\"_check_catalog_compatibility\",\n        dbname=sys_db.decode(\"utf-8\")\n    )\n\n    try:\n        # versioned=False so we can properly fail on version/catalog mismatches.\n        instancedata = await _get_instance_data(conn, versioned=False)\n        datadir_version = instancedata.get('version')\n        if datadir_version:\n            datadir_major = datadir_version.get('major')\n\n        expected_ver = buildmeta.get_version()\n        datadir_catver = instancedata.get('catver')\n        expected_catver = edbdef.EDGEDB_CATALOG_VERSION\n\n        status = dict(\n            data_catalog_version=datadir_catver,\n            expected_catalog_version=expected_catver,\n        )\n\n        if datadir_major != expected_ver.major:\n            for status_sink in ctx.args.status_sinks:\n                status_sink(f'INCOMPATIBLE={json.dumps(status)}')\n            raise errors.ConfigurationError(\n                'database instance incompatible with this version of Gel',\n                details=(\n                    f'The database instance was initialized with '\n                    f'Gel version {datadir_major}, '\n                    f'which is incompatible with this version '\n                    f'{expected_ver.major}'\n                ),\n                hint=(\n                    f'You need to either recreate the instance and upgrade '\n                    f'using dump/restore, or do an inplace upgrade.'\n                )\n            )\n\n        if datadir_catver != expected_catver:\n            for status_sink in ctx.args.status_sinks:\n                status_sink(f'INCOMPATIBLE={json.dumps(status)}')\n            raise errors.ConfigurationError(\n                'database instance incompatible with this version of Gel',\n                details=(\n                    f'The database instance was initialized with '\n                    f'Gel format version {datadir_catver}, '\n                    f'but this version of the server expects '\n                    f'format version {expected_catver}'\n                ),\n                hint=(\n                    f'You need to either recreate the instance and upgrade '\n                    f'using dump/restore, or do an inplace upgrade.'\n                )\n            )\n    except Exception:\n        conn.terminate()\n        raise\n\n    return conn\n\n\ndef _check_capabilities(ctx: BootstrapContext) -> None:\n    caps = ctx.cluster.get_runtime_params().instance_params.capabilities\n    for cap in ctx.args.backend_capability_sets.must_be_present:\n        if not caps & cap:\n            raise errors.ConfigurationError(\n                f\"the backend doesn't have necessary capability: \"\n                f\"{cap.name}\"\n            )\n    for cap in ctx.args.backend_capability_sets.must_be_absent:\n        if caps & cap:\n            raise errors.ConfigurationError(\n                f\"the backend was already bootstrapped with capability: \"\n                f\"{cap.name}\"\n            )\n\n\nasync def _pg_ensure_database_not_connected(\n    conn: metaschema.PGConnection,\n    dbname: str,\n) -> None:\n    conns = await conn.sql_fetch_col(\n        b\"\"\"\n        SELECT\n            pid\n        FROM\n            pg_stat_activity\n        WHERE\n            datname = $1\n        \"\"\",\n        args=[dbname.encode(\"utf-8\")],\n    )\n\n    if conns:\n        raise errors.ExecutionError(\n            f'database {dbname!r} is being accessed by other users')\n\n\nasync def _start(ctx: BootstrapContext) -> edbcompiler.Compiler:\n    conn = await _check_catalog_compatibility(ctx)\n\n    try:\n        caps = await conn.sql_fetch_val(\n            b\"SELECT edgedb.get_backend_capabilities()\")\n        ctx.cluster.overwrite_capabilities(struct.Struct('!Q').unpack(caps)[0])\n        _check_capabilities(ctx)\n\n        return await edbcompiler.new_compiler_from_pg(conn)\n\n    finally:\n        conn.terminate()\n\n\nasync def _bootstrap_edgedb_super_roles(ctx: BootstrapContext) -> uuid.UUID:\n    await _ensure_edgedb_supergroup(\n        ctx,\n        edbdef.EDGEDB_SUPERGROUP,\n    )\n\n    superuser_uid = await _ensure_edgedb_role(\n        ctx,\n        edbdef.EDGEDB_SUPERUSER,\n        superuser=True,\n        builtin=True,\n    )\n\n    superuser = ctx.cluster.get_role_name(edbdef.EDGEDB_SUPERUSER)\n    await _execute(ctx.conn, f'SET ROLE {qi(superuser)}')\n\n    return superuser_uid\n\n\nasync def _bootstrap(\n    ctx: BootstrapContext,\n    no_template: bool=False,\n) -> tuple[\n    StdlibBits,\n    edbcompiler.Compiler\n]:\n    args = ctx.args\n    cluster = ctx.cluster\n    backend_params = cluster.get_runtime_params()\n\n    if backend_params.instance_params.version < edbdef.MIN_POSTGRES_VERSION:\n        min_ver = '.'.join(str(v) for v in edbdef.MIN_POSTGRES_VERSION)\n        raise errors.ConfigurationError(\n            'unsupported backend',\n            details=(\n                f'Gel requires PostgreSQL version {min_ver} or later, '\n                f'while the specified backend reports itself as '\n                f'{backend_params.instance_params.version.string}.'\n            )\n        )\n\n    _check_capabilities(ctx)\n\n    if backend_params.has_create_role:\n        superuser_uid = await _bootstrap_edgedb_super_roles(ctx)\n    else:\n        superuser_uid = uuidgen.uuid1mc()\n\n    using_template = backend_params.has_create_database and not no_template\n\n    if using_template:\n        if not args.inplace_upgrade_prepare:\n            new_template_db_id = await _create_edgedb_template_database(ctx)\n        # XXX: THIS IS WRONG, RIGHT?\n        else:\n            new_template_db_id = uuidgen.uuid1mc()\n        tpl_db = cluster.get_db_name(edbdef.EDGEDB_TEMPLATE_DB)\n        conn = PGConnectionProxy(\n            cluster,\n            source_description=\"_bootstrap\",\n            dbname=tpl_db\n        )\n\n        tpl_ctx = dataclasses.replace(ctx, conn=conn)\n    else:\n        new_template_db_id = uuidgen.uuid1mc()\n        tpl_ctx = ctx\n\n    in_dev_mode = devmode.is_in_dev_mode()\n    # Protect against multiple Gel tenants from trying to bootstrap\n    # on the same cluster in devmode, as that is both a waste of resources\n    # and might result in broken stdlib cache.\n    if in_dev_mode:\n        await tpl_ctx.conn.sql_execute(b\"SELECT pg_advisory_lock(3987734529)\")\n\n    try:\n        # Some of the views need access to the _edgecon_state table and the\n        # _dml_dummy table, so set it up.\n        tmp_table_query = (\n            pgcon.SETUP_TEMP_TABLE_SCRIPT + pgcon.SETUP_DML_DUMMY_TABLE_SCRIPT\n        )\n        await _execute(tpl_ctx.conn, tmp_table_query)\n\n        stdlib, config_spec, compiler = await _init_stdlib(\n            tpl_ctx,\n            testmode=args.testmode,\n            global_ids={\n                edbdef.EDGEDB_SUPERUSER: superuser_uid,\n                edbdef.EDGEDB_TEMPLATE_DB: new_template_db_id,\n            }\n        )\n        (\n            sysqueries,\n            report_configs_typedesc_1_0,\n            report_configs_typedesc_2_0,\n        ) = compile_sys_queries(\n            stdlib.reflschema,\n            compiler,\n            config_spec,\n        )\n\n        # Update schema backend_ids to match the reality after\n        await tpl_ctx.conn.sql_execute(\n            sysqueries['backend_id_fixup'].encode('utf-8')\n        )\n\n        version_key = patches.get_version_key(stdlib.num_patches)\n        await _store_static_json_cache(\n            tpl_ctx,\n            f'sysqueries{version_key}',\n            json.dumps(sysqueries),\n        )\n\n        await _store_static_bin_cache(\n            tpl_ctx,\n            f'report_configs_typedesc_1_0{version_key}',\n            report_configs_typedesc_1_0,\n        )\n\n        await _store_static_bin_cache(\n            tpl_ctx,\n            f'report_configs_typedesc_2_0{version_key}',\n            report_configs_typedesc_2_0,\n        )\n\n        await _store_static_json_cache(\n            tpl_ctx,\n            'num_patches',\n            json.dumps(stdlib.num_patches),\n        )\n\n        default_branch = args.default_branch or edbdef.EDGEDB_SUPERUSER_DB\n        await _store_static_text_cache(\n            tpl_ctx,\n            'default_branch',\n            default_branch,\n        )\n\n        schema = s_schema.EMPTY_SCHEMA\n        if not no_template:\n            schema = await _init_defaults(schema, compiler, tpl_ctx.conn)\n\n        # Run analyze on the template database, so that new dbs start\n        # with up-to-date statistics.\n        await tpl_ctx.conn.sql_execute(b\"ANALYZE\")\n\n    finally:\n        if in_dev_mode:\n            await tpl_ctx.conn.sql_execute(\n                b\"SELECT pg_advisory_unlock(3987734529)\",\n            )\n\n        if using_template:\n            # Close the connection to the template database\n            # and wait until it goes away on the server side\n            # so that we can safely use the template for new\n            # databases.\n            #\n            # The timeout is set weirdly high, because we were getting\n            # frequent timeouts in macos-x86_64 release builds when it\n            # was set to 10s.\n            conn.terminate()\n            rloop = retryloop.RetryLoop(\n                backoff=retryloop.exp_backoff(),\n                timeout=60.0,\n                ignore=errors.ExecutionError,\n            )\n\n            async for iteration in rloop:\n                async with iteration:\n                    await _pg_ensure_database_not_connected(ctx.conn, tpl_db)\n\n    if args.inplace_upgrade_prepare:\n        pass\n    elif backend_params.has_create_database:\n        await _create_edgedb_database(\n            ctx,\n            edbdef.EDGEDB_SYSTEM_DB,\n            edbdef.EDGEDB_SUPERUSER,\n            builtin=True,\n        )\n\n        sys_conn = PGConnectionProxy(\n            cluster,\n            source_description=\"_bootstrap\",\n            dbname=cluster.get_db_name(edbdef.EDGEDB_SYSTEM_DB),\n        )\n\n        try:\n            await _configure(\n                dataclasses.replace(ctx, conn=sys_conn),\n                config_spec=config_spec,\n                schema=schema,\n                compiler=compiler,\n            )\n        finally:\n            sys_conn.terminate()\n    else:\n        await _configure(\n            ctx,\n            config_spec=config_spec,\n            schema=schema,\n            compiler=compiler,\n        )\n\n    if args.inplace_upgrade_prepare:\n        pass\n    elif backend_params.has_create_database:\n        await _create_edgedb_database(\n            ctx,\n            default_branch,\n            args.default_database_user or edbdef.EDGEDB_SUPERUSER,\n        )\n    else:\n        await _set_edgedb_database_metadata(\n            ctx,\n            default_branch,\n        )\n\n    if (\n        backend_params.has_create_role\n        and args.default_database_user\n        and args.default_database_user != edbdef.EDGEDB_SUPERUSER\n    ):\n        await _ensure_edgedb_role(\n            ctx,\n            args.default_database_user,\n            superuser=True,\n        )\n\n        def_role = ctx.cluster.get_role_name(args.default_database_user)\n        await _execute(ctx.conn, f\"SET ROLE {qi(def_role)}\")\n\n    if (\n        backend_params.has_create_database\n        and args.default_database\n        and args.default_database != default_branch\n    ):\n        await _create_edgedb_database(\n            ctx,\n            args.default_database,\n            args.default_database_user or edbdef.EDGEDB_SUPERUSER,\n        )\n\n    return stdlib, compiler\n\n\nasync def ensure_bootstrapped(\n    cluster: pgcluster.BaseCluster,\n    args: edbargs.ServerConfig,\n) -> tuple[bool, edbcompiler.Compiler]:\n    \"\"\"Bootstraps Gel instance if it hasn't been bootstrapped already.\n\n    Returns True if bootstrap happened and False if the instance was already\n    bootstrapped, along with the bootstrap compiler state.\n    \"\"\"\n    pgconn = PGConnectionProxy(\n        cluster,\n        source_description=\"ensure_bootstrapped\"\n    )\n    ctx = BootstrapContext(cluster=cluster, conn=pgconn, args=args)\n\n    try:\n        mode = await _get_cluster_mode(ctx)\n        ctx = dataclasses.replace(ctx, mode=mode)\n        if mode == ClusterMode.pristine:\n            _, compiler = await _bootstrap(ctx)\n            return True, compiler\n        else:\n            compiler = await _start(ctx)\n            return False, compiler\n    finally:\n        pgconn.terminate()\n"
  },
  {
    "path": "edb/server/cache/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom .stmt_cache import StatementsCache\n\n\n__all__ = ('StatementsCache',)\n"
  },
  {
    "path": "edb/server/cache/stmt_cache.pxd",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ncdef class StatementsCache:\n\n    cdef:\n        object _dict\n        int _maxsize\n        object _dict_move_to_end\n        object _dict_get\n\n    cpdef get(self, key, default)\n    cpdef needs_cleanup(self)\n    cpdef cleanup_one(self)\n    cpdef resize(self, int maxsize)\n"
  },
  {
    "path": "edb/server/cache/stmt_cache.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport collections\n\n\ncdef object _LRU_MARKER = object()\n\n\ncdef class StatementsCache:\n\n    # We use an OrderedDict for LRU implementation.  Operations:\n    #\n    # * We use a simple `__setitem__` to push a new entry:\n    #       `entries[key] = new_entry`\n    #   That will push `new_entry` to the *end* of the entries dict.\n    #\n    # * When we have a cache hit, we call\n    #       `entries.move_to_end(key, last=True)`\n    #   to move the entry to the *end* of the entries dict.\n    #\n    # * When we need to remove entries to maintain `max_size`, we call\n    #       `entries.popitem(last=False)`\n    #   to remove an entry from the *beginning* of the entries dict.\n    #\n    # So new entries and hits are always promoted to the end of the\n    # entries dict, whereas the unused one will group in the\n    # beginning of it.\n\n    def __init__(self, *, maxsize):\n        self.resize(maxsize)\n        self._dict = collections.OrderedDict()\n        self._dict_move_to_end = self._dict.move_to_end\n        self._dict_get = self._dict.get\n\n    cpdef get(self, key, default):\n        o = self._dict_get(key, _LRU_MARKER)\n        if o is _LRU_MARKER:\n            return default\n        self._dict_move_to_end(key)  # last=True\n        return o\n\n    cpdef needs_cleanup(self):\n        return len(self._dict) > self._maxsize\n\n    cpdef cleanup_one(self):\n        return self._dict.popitem(last=False)\n\n    cpdef resize(self, int maxsize):\n        if maxsize <= 0:\n            raise ValueError(\n                f'maxsize is expected to be greater than 0, got {maxsize}')\n        self._maxsize = maxsize\n\n    def items(self):\n        return self._dict.items()\n\n    def clear(self):\n        self._dict.clear()\n\n    def pop(self, key, default=_LRU_MARKER):\n        if default is _LRU_MARKER:\n            return self._dict.pop(key)\n        else:\n            return self._dict.pop(key, default)\n\n    def __getitem__(self, key):\n        o = self._dict[key]\n        self._dict_move_to_end(key)  # last=True\n        return o\n\n    def __setitem__(self, key, o):\n        if key in self._dict:\n            self._dict[key] = o\n            self._dict_move_to_end(key)  # last=True\n        else:\n            self._dict[key] = o\n\n    def __delitem__(self, key):\n        del self._dict[key]\n\n    def __contains__(self, key):\n        return key in self._dict\n\n    def __len__(self):\n        return len(self._dict)\n\n    def __iter__(self):\n        return iter(self._dict)\n"
  },
  {
    "path": "edb/server/compiler/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for tbhe specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom .compiler import Compiler, CompilerState\nfrom .compiler import CompileContext, CompilerDatabaseState\nfrom .compiler import compile_edgeql_script\nfrom .compiler import new_compiler, new_compiler_from_pg, new_compiler_context\nfrom .compiler import compile, compile_schema_storage_in_delta\nfrom .compiler import maybe_force_database_error\nfrom .dbstate import QueryUnit, QueryUnitGroup\nfrom .enums import Capability, Cardinality\nfrom .enums import InputFormat, OutputFormat, InputLanguage\nfrom .explain import analyze_explain_output\nfrom .ddl import repair_schema\nfrom .rpc import CompilationRequest\n\n__all__ = (\n    'Cardinality',\n    'CompilationRequest',\n    'Compiler',\n    'CompilerState',\n    'CompileContext',\n    'CompilerDatabaseState',\n    'QueryUnit',\n    'QueryUnitGroup',\n    'Capability',\n    'InputFormat',\n    'InputLanguage',\n    'OutputFormat',\n    'analyze_explain_output',\n    'compile_edgeql_script',\n    'maybe_force_database_error',\n    'new_compiler',\n    'new_compiler_from_pg',\n    'new_compiler_context',\n    'compile',\n    'compile_schema_storage_in_delta',\n    'repair_schema',\n)\n"
  },
  {
    "path": "edb/server/compiler/compiler.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    AbstractSet,\n    Iterable,\n    Mapping,\n    MutableMapping,\n    Sequence,\n    NamedTuple,\n    cast,\n    TYPE_CHECKING,\n)\n\nimport dataclasses\nimport functools\nimport json\nimport hashlib\nimport pickle\nimport textwrap\nimport time\nimport uuid\n\nimport immutables\n\nfrom edb import buildmeta\nfrom edb import errors\n\nfrom edb.common.typeutils import not_none\n\nfrom edb.server import config\nfrom edb.server import defines\nfrom edb.server import instdata\n\nfrom edb import edgeql\nfrom edb.common import debug\nfrom edb import graphql\nfrom edb.common import turbo_uuid\nfrom edb.common import verutils\nfrom edb.common import uuidgen\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import codegen as qlcodegen\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.edgeql import qltypes\n\nfrom edb.ir import staeval as ireval\nfrom edb.ir import statypes\nfrom edb.ir import ast as irast\n\nfrom edb.schema import ddl as s_ddl\nfrom edb.schema import delta as s_delta\nfrom edb.schema import extensions as s_ext\nfrom edb.schema import functions as s_func\nfrom edb.schema import links as s_links\nfrom edb.schema import properties as s_props\nfrom edb.schema import modules as s_mod\nfrom edb.schema import name as s_name\nfrom edb.schema import objects as s_obj\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import reflection as s_refl\nfrom edb.schema import roles as s_role\nfrom edb.schema import schema as s_schema\nfrom edb.schema import types as s_types\nfrom edb.schema import version as s_ver\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import compiler as pg_compiler\nfrom edb.pgsql import codegen as pg_codegen\nfrom edb.pgsql import common as pg_common\nfrom edb.pgsql import debug as pg_debug\nfrom edb.pgsql import dbops as pg_dbops\nfrom edb.pgsql import params as pg_params\nfrom edb.pgsql import parser as pg_parser\nfrom edb.pgsql import patches as pg_patches\nfrom edb.pgsql import types as pg_types\nfrom edb.pgsql import delta as pg_delta\n\nfrom . import config as config_compiler\nfrom . import dbstate\nfrom . import enums\nfrom . import explain\nfrom . import sertypes\nfrom . import status\nfrom . import ddl\nfrom . import rpc\nfrom . import sql\n\nif TYPE_CHECKING:\n    from edb.pgsql import metaschema\n    SQLDescriptors = list[\n        tuple[\n            tuple[bytes, bytes, list[dbstate.Param], int], tuple[bytes, bytes]\n        ]\n    ]\n\n\nEMPTY_MAP: immutables.Map[Any, Any] = immutables.Map()\n\n\n@dataclasses.dataclass(frozen=True)\nclass CompilerDatabaseState:\n\n    user_schema: s_schema.Schema\n    global_schema: s_schema.Schema\n    cached_reflection: immutables.Map[str, tuple[str, ...]]\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass CompileContext:\n\n    compiler_state: CompilerState\n    state: dbstate.CompilerConnectionState\n    output_format: enums.OutputFormat\n    expected_cardinality_one: bool\n    protocol_version: defines.ProtocolVersion\n    expect_rollback: bool = False\n    json_parameters: bool = False\n    schema_reflection_mode: bool = False\n    force_testmode: bool = False\n    implicit_limit: int = 0\n    inline_typeids: bool = False\n    inline_typenames: bool = False\n    inline_objectids: bool = True\n    schema_object_ids: Optional[\n        Mapping[tuple[s_name.Name, Optional[str]], uuid.UUID]] = None\n    source: Optional[edgeql.Source | graphql.Source | pg_parser.Source] = None\n    backend_runtime_params: pg_params.BackendRuntimeParams = dataclasses.field(\n        default_factory=pg_params.get_default_runtime_params\n    )\n    compat_ver: Optional[verutils.Version] = None\n    bootstrap_mode: bool = False\n    internal_schema_mode: bool = False\n    log_ddl_as_migrations: bool = True\n    dump_restore_mode: bool = False\n    notebook: bool = False\n    branch_name: Optional[str] = None\n    role_name: Optional[str] = None\n    cache_key: Optional[uuid.UUID] = None\n\n    def get_cache_mode(self) -> config.QueryCacheMode:\n        return config.QueryCacheMode.effective(\n            _get_config_val(self, 'query_cache_mode')\n        )\n\n    def _assert_not_in_migration_block(self, ql: qlast.Base) -> None:\n        \"\"\"Check that a START MIGRATION block is *not* active.\"\"\"\n        current_tx = self.state.current_tx()\n        mstate = current_tx.get_migration_state()\n        if mstate is not None:\n            stmt = status.get_status(ql).decode()\n            raise errors.QueryError(\n                f'cannot execute {stmt} in a migration block',\n                span=ql.span,\n            )\n\n    def _assert_in_migration_block(\n        self, ql: qlast.Base\n    ) -> dbstate.MigrationState:\n        \"\"\"Check that a START MIGRATION block *is* active.\"\"\"\n        current_tx = self.state.current_tx()\n        mstate = current_tx.get_migration_state()\n        if mstate is None:\n            stmt = status.get_status(ql).decode()\n            raise errors.QueryError(\n                f'cannot execute {stmt} outside of a migration block',\n                span=ql.span,\n            )\n        return mstate\n\n    def _assert_not_in_migration_rewrite_block(self, ql: qlast.Base) -> None:\n        \"\"\"Check that a START MIGRATION REWRITE block is *not* active.\"\"\"\n        current_tx = self.state.current_tx()\n        mstate = current_tx.get_migration_rewrite_state()\n        if mstate is not None:\n            stmt = status.get_status(ql).decode()\n            raise errors.QueryError(\n                f'cannot execute {stmt} in a migration rewrite block',\n                span=ql.span,\n            )\n\n    def _assert_in_migration_rewrite_block(\n        self, ql: qlast.Base\n    ) -> dbstate.MigrationRewriteState:\n        \"\"\"Check that a START MIGRATION REWRITE block *is* active.\"\"\"\n        current_tx = self.state.current_tx()\n        mstate = current_tx.get_migration_rewrite_state()\n        if mstate is None:\n            stmt = status.get_status(ql).decode()\n            raise errors.QueryError(\n                f'cannot execute {stmt} outside of a migration rewrite block',\n                span=ql.span,\n            )\n        return mstate\n\n    def is_testmode(self) -> bool:\n        return (\n            self.force_testmode or _get_config_val(self, '__internal_testmode')\n        )\n\n\nDEFAULT_MODULE_ALIASES_MAP: immutables.Map[Optional[str], str] = (\n    immutables.Map({None: s_mod.DEFAULT_MODULE_ALIAS}))\n\n\ndef compile_edgeql_script(\n    ctx: CompileContext,\n    eql: str,\n) -> tuple[s_schema.Schema, str]:\n\n    sql = _compile_ql_script(ctx, eql)\n    new_schema = ctx.state.current_tx().get_schema(\n        ctx.compiler_state.std_schema)\n    assert isinstance(new_schema, s_schema.ChainedSchema)\n    return new_schema.get_top_schema(), sql\n\n\ndef new_compiler(\n    std_schema: s_schema.Schema,\n    reflection_schema: s_schema.Schema,\n    schema_class_layout: s_refl.SchemaClassLayout,\n    *,\n    backend_runtime_params: Optional[pg_params.BackendRuntimeParams] = None,\n    local_intro_query: Optional[str] = None,\n    global_intro_query: Optional[str] = None,\n    config_spec: Optional[config.Spec] = None,\n) -> Compiler:\n    \"\"\"Create and return a compiler instance.\"\"\"\n\n    if not backend_runtime_params:\n        backend_runtime_params = pg_params.get_default_runtime_params()\n\n    if not config_spec:\n        config_spec = config.load_spec_from_schema(std_schema)\n\n    return Compiler(CompilerState(\n        std_schema=std_schema,\n        refl_schema=reflection_schema,\n        schema_class_layout=schema_class_layout,\n        backend_runtime_params=backend_runtime_params,\n        config_spec=config_spec,\n        local_intro_query=local_intro_query,\n        global_intro_query=global_intro_query,\n    ))\n\n\nasync def new_compiler_from_pg(con: metaschema.PGConnection) -> Compiler:\n    num_patches = await get_patch_count(con)\n\n    std_schema, reflection_schema = await load_std_and_reflection_schema(\n        con, num_patches)\n\n    return new_compiler(\n        std_schema=std_schema,\n        reflection_schema=reflection_schema,\n        schema_class_layout=await load_schema_class_layout(\n            con, num_patches\n        ),\n        local_intro_query=await load_schema_intro_query(\n            con, num_patches, 'local_intro_query'\n        ),\n        global_intro_query=await load_schema_intro_query(\n            con, num_patches, 'global_intro_query'\n        ),\n        config_spec=None,\n    )\n\n\ndef new_compiler_context(\n    *,\n    compiler_state: CompilerState,\n    user_schema: s_schema.Schema,\n    global_schema: s_schema.Schema=s_schema.EMPTY_SCHEMA,\n    modaliases: Optional[Mapping[Optional[str], str]] = None,\n    expected_cardinality_one: bool = False,\n    json_parameters: bool = False,\n    schema_reflection_mode: bool = False,\n    output_format: enums.OutputFormat = enums.OutputFormat.BINARY,\n    bootstrap_mode: bool = False,\n    internal_schema_mode: bool = False,\n    force_testmode: bool = False,\n    protocol_version: defines.ProtocolVersion = defines.CURRENT_PROTOCOL,\n    backend_runtime_params: Optional[pg_params.BackendRuntimeParams] = None,\n    log_ddl_as_migrations: bool = True,\n) -> CompileContext:\n    \"\"\"Create and return an ad-hoc compiler context.\"\"\"\n\n    state = dbstate.CompilerConnectionState(\n        user_schema=user_schema,\n        global_schema=global_schema,\n        modaliases=immutables.Map(modaliases) if modaliases else EMPTY_MAP,\n        session_config=EMPTY_MAP,\n        database_config=EMPTY_MAP,\n        system_config=EMPTY_MAP,\n        cached_reflection=EMPTY_MAP,\n    )\n\n    ctx = CompileContext(\n        compiler_state=compiler_state,\n        state=state,\n        output_format=output_format,\n        expected_cardinality_one=expected_cardinality_one,\n        json_parameters=json_parameters,\n        schema_reflection_mode=schema_reflection_mode,\n        bootstrap_mode=bootstrap_mode,\n        internal_schema_mode=internal_schema_mode,\n        force_testmode=force_testmode,\n        protocol_version=protocol_version,\n        backend_runtime_params=(\n            backend_runtime_params or pg_params.get_default_runtime_params()\n        ),\n        log_ddl_as_migrations=log_ddl_as_migrations,\n    )\n\n    return ctx\n\n\nasync def get_patch_count(backend_conn: metaschema.PGConnection) -> int:\n    \"\"\"Get the number of applied patches.\"\"\"\n    num_patches = await instdata.get_instdata(\n        backend_conn, 'num_patches', 'json')\n    res: int = json.loads(num_patches) if num_patches else 0\n    return res\n\n\nasync def load_std_and_reflection_schema(\n    backend_conn: metaschema.PGConnection,\n    patches: int,\n) -> tuple[s_schema.Schema, s_schema.Schema]:\n    vkey = pg_patches.get_version_key(patches)\n\n    # stdschema and reflschema are combined in one pickle to preserve sharing.\n    key = f\"std_and_reflection_schema{vkey}\"\n    data = await instdata.get_instdata(backend_conn, key, 'bin')\n    try:\n        std_schema: s_schema.Schema\n        refl_schema: s_schema.Schema\n        std_schema, refl_schema = pickle.loads(data)\n        if vkey != pg_patches.get_version_key(len(pg_patches.PATCHES)):\n            std_schema = s_schema.upgrade_schema(std_schema)\n            refl_schema = s_schema.upgrade_schema(refl_schema)\n        return (std_schema, refl_schema)\n    except Exception as e:\n        raise RuntimeError(\n            'could not load std schema pickle') from e\n\n\nasync def load_schema_intro_query(\n    backend_conn: metaschema.PGConnection,\n    patches: int,\n    kind: str,\n) -> str:\n    kind += pg_patches.get_version_key(patches)\n    return (\n        await instdata.get_instdata(backend_conn, kind, 'text')\n    ).decode('utf-8')\n\n\nasync def load_schema_class_layout(\n    backend_conn: metaschema.PGConnection,\n    patches: int,\n) -> s_refl.SchemaClassLayout:\n    key = f'classlayout{pg_patches.get_version_key(patches)}'\n    data = await instdata.get_instdata(backend_conn, key, 'bin')\n    try:\n        return cast(s_refl.SchemaClassLayout, pickle.loads(data))\n    except Exception as e:\n        raise RuntimeError(\n            'could not load schema class layout pickle') from e\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass CompilerState:\n\n    std_schema: s_schema.Schema\n    refl_schema: s_schema.Schema\n    schema_class_layout: s_refl.SchemaClassLayout\n\n    backend_runtime_params: pg_params.BackendRuntimeParams\n    config_spec: config.Spec\n\n    local_intro_query: Optional[str]\n    global_intro_query: Optional[str]\n\n    @functools.cached_property\n    def state_serializer_factory(self) -> sertypes.StateSerializerFactory:\n        # TODO: This factory will probably need to become per-db once\n        # config spec differs between databases. See also #5836.\n        return sertypes.StateSerializerFactory(\n            self.std_schema, self.config_spec\n        )\n\n    @functools.cached_property\n    def compilation_config_serializer(\n        self\n    ) -> sertypes.CompilationConfigSerializer:\n        return (\n            self.state_serializer_factory.make_compilation_config_serializer()\n        )\n\n\nclass Compiler:\n\n    state: CompilerState\n\n    def __init__(self, state: CompilerState):\n        self.state = state\n\n    @staticmethod\n    def _try_compile_rollback(\n        eql: edgeql.Source | bytes\n    ) -> tuple[dbstate.QueryUnitGroup, int]:\n        source: str | edgeql.Source\n        if isinstance(eql, edgeql.Source):\n            source = eql\n        else:\n            source = eql.decode()\n        statements = edgeql.parse_block(source)\n\n        stmt = statements[0]\n        unit = None\n        if isinstance(stmt, qlast.RollbackTransaction):\n            sql = b'ROLLBACK;'\n            unit = dbstate.QueryUnit(\n                status=b'ROLLBACK',\n                sql=sql,\n                tx_rollback=True,\n                cacheable=False)\n\n        elif isinstance(stmt, qlast.RollbackToSavepoint):\n            sql = f'ROLLBACK TO {pg_common.quote_ident(stmt.name)};'.encode()\n            unit = dbstate.QueryUnit(\n                status=b'ROLLBACK TO SAVEPOINT',\n                sql=sql,\n                tx_savepoint_rollback=True,\n                sp_name=stmt.name,\n                cacheable=False)\n\n        if unit is not None:\n            rv = dbstate.QueryUnitGroup()\n            rv.append(unit)\n            return rv, len(statements) - 1\n\n        raise errors.TransactionError(\n            'expected a ROLLBACK or ROLLBACK TO SAVEPOINT command'\n        )  # pragma: no cover\n\n    def compile_notebook(\n        self,\n        user_schema: s_schema.Schema,\n        global_schema: s_schema.Schema,\n        reflection_cache: immutables.Map[str, tuple[str, ...]],\n        database_config: immutables.Map[str, config.SettingValue],\n        system_config: immutables.Map[str, config.SettingValue],\n        queries: list[str],\n        protocol_version: defines.ProtocolVersion,\n        implicit_limit: int = 0,\n    ) -> list[\n        tuple[\n            bool,\n            dbstate.QueryUnit | tuple[str, str, dict[int, str]]\n        ]\n    ]:\n\n        state = dbstate.CompilerConnectionState(\n            user_schema=user_schema,\n            global_schema=global_schema,\n            modaliases=DEFAULT_MODULE_ALIASES_MAP,\n            session_config=EMPTY_MAP,\n            database_config=database_config,\n            system_config=system_config,\n            cached_reflection=reflection_cache,\n        )\n\n        state.start_tx()\n\n        result: list[\n            tuple[\n                bool,\n                dbstate.QueryUnit | tuple[str, str, dict[int, str]]\n            ]\n        ] = []\n\n        for query in queries:\n            try:\n                source = edgeql.Source.from_string(query)\n\n                ctx = CompileContext(\n                    compiler_state=self.state,\n                    state=state,\n                    output_format=enums.OutputFormat.BINARY,\n                    expected_cardinality_one=False,\n                    implicit_limit=implicit_limit,\n                    inline_typeids=False,\n                    inline_typenames=True,\n                    json_parameters=False,\n                    source=source,\n                    protocol_version=protocol_version,\n                    notebook=True,\n                )\n\n                result.append(\n                    (False, compile(ctx=ctx, source=source)[0]))\n            except Exception as ex:\n                fields = {}\n                typename = 'Error'\n                if (isinstance(ex, errors.EdgeDBError) and\n                        type(ex) is not errors.EdgeDBError):\n                    fields = ex._attrs\n                    typename = type(ex).__name__\n                result.append(\n                    (True, (typename, str(ex), fields)))\n                break\n\n        return result\n\n    def compile_sql(\n        self,\n        user_schema: s_schema.Schema,\n        global_schema: s_schema.Schema,\n        reflection_cache: immutables.Map[str, tuple[str, ...]],\n        database_config: immutables.Map[str, config.SettingValue],\n        system_config: immutables.Map[str, config.SettingValue],\n        source: pg_parser.Source,\n        tx_state: dbstate.SQLTransactionState,\n        prepared_stmt_map: Mapping[str, str],\n        current_database: str,\n        current_user: str,\n    ) -> list[dbstate.SQLQueryUnit]:\n        state = dbstate.CompilerConnectionState(\n            user_schema=user_schema,\n            global_schema=global_schema,\n            modaliases=DEFAULT_MODULE_ALIASES_MAP,\n            session_config=EMPTY_MAP,\n            database_config=database_config,\n            system_config=system_config,\n            cached_reflection=reflection_cache,\n        )\n        schema = state.current_tx().get_schema(self.state.std_schema)\n\n        setting = database_config.get('allow_user_specified_id', None)\n        allow_user_specified_id = None\n        if setting and setting.value:\n            allow_user_specified_id = sql.is_setting_truthy(setting.value)\n\n        setting = database_config.get('apply_access_policies_pg', None)\n        apply_access_policies_pg = None\n        if setting is not None:\n            apply_access_policies_pg = sql.is_setting_truthy(setting.value)\n\n        return sql.compile_sql(\n            source,\n            schema=schema,\n            tx_state=tx_state,\n            prepared_stmt_map=prepared_stmt_map,\n            current_database=current_database,\n            allow_user_specified_id=allow_user_specified_id,\n            apply_access_policies=apply_access_policies_pg,\n            disambiguate_column_names=False,\n            backend_runtime_params=self.state.backend_runtime_params,\n            protocol_version=defines.POSTGRES_PROTOCOL,\n        )[0]\n\n    def compile_serialized_request(\n        self,\n        user_schema: s_schema.Schema,\n        global_schema: s_schema.Schema,\n        reflection_cache: immutables.Map[str, tuple[str, ...]],\n        database_config: Optional[immutables.Map[str, config.SettingValue]],\n        system_config: Optional[immutables.Map[str, config.SettingValue]],\n        serialized_request: bytes,\n        original_query: str,\n    ) -> tuple[\n        dbstate.QueryUnitGroup | SQLDescriptors,\n        Optional[dbstate.CompilerConnectionState]\n    ]:\n        request = rpc.CompilationRequest.deserialize(\n            serialized_request,\n            original_query,\n            self.state.compilation_config_serializer,\n        )\n\n        return self.compile(\n            user_schema=user_schema,\n            global_schema=global_schema,\n            reflection_cache=reflection_cache,\n            database_config=database_config,\n            system_config=system_config,\n            request=request,\n        )\n\n    def compile(\n        self,\n        *,\n        user_schema: s_schema.Schema,\n        global_schema: s_schema.Schema,\n        reflection_cache: immutables.Map[str, tuple[str, ...]],\n        database_config: Optional[immutables.Map[str, config.SettingValue]],\n        system_config: Optional[immutables.Map[str, config.SettingValue]],\n        request: rpc.CompilationRequest,\n    ) -> tuple[dbstate.QueryUnitGroup | SQLDescriptors,\n               Optional[dbstate.CompilerConnectionState]]:\n\n        if request.input_language is enums.InputLanguage.SQL_PARAMS:\n            assert isinstance(request.source, rpc.SQLParamsSource)\n            return (\n                self.compile_sql_descriptors(\n                    user_schema,\n                    global_schema,\n                    request.protocol_version,\n                    request.source.types_in_out,\n                ),\n                # state is None -- we know we're not\n                # in a transaction and compilation of params\n                # couldn't have started it.\n                None,\n            )\n\n        sess_config = request.session_config\n        if sess_config is None:\n            sess_config = EMPTY_MAP\n\n        if database_config is None:\n            database_config = EMPTY_MAP\n\n        if system_config is None:\n            system_config = EMPTY_MAP\n\n        sess_modaliases = request.modaliases\n        if sess_modaliases is None:\n            sess_modaliases = DEFAULT_MODULE_ALIASES_MAP\n\n        state = dbstate.CompilerConnectionState(\n            user_schema=user_schema,\n            global_schema=global_schema,\n            modaliases=sess_modaliases,\n            session_config=sess_config,\n            database_config=database_config,\n            system_config=system_config,\n            cached_reflection=reflection_cache,\n        )\n\n        ctx = CompileContext(\n            compiler_state=self.state,\n            state=state,\n            output_format=request.output_format,\n            expected_cardinality_one=request.expect_one,\n            implicit_limit=request.implicit_limit,\n            inline_typeids=request.inline_typeids,\n            inline_typenames=request.inline_typenames,\n            inline_objectids=request.inline_objectids,\n            json_parameters=request.input_format is enums.InputFormat.JSON,\n            source=request.source,\n            protocol_version=request.protocol_version,\n            role_name=request.role_name,\n            branch_name=request.branch_name,\n            cache_key=request.get_cache_key(),\n        )\n\n        match request.input_language:\n            case enums.InputLanguage.EDGEQL:\n                assert isinstance(request.source, edgeql.Source)\n                unit_group = compile(ctx=ctx, source=request.source)\n            case enums.InputLanguage.GRAPHQL:\n                assert isinstance(request.source, graphql.Source)\n                unit_group = compile_graphql(\n                    ctx=ctx,\n                    source=request.source,\n                    variables=request.key_params,\n                )\n            case enums.InputLanguage.SQL:\n                assert isinstance(request.source, pg_parser.Source)\n                unit_group = compile_sql_as_unit_group(\n                    ctx=ctx, source=request.source)\n            case _:\n                raise NotImplementedError(\n                    f\"unnsupported input language: {request.input_language}\")\n\n        tx_started = False\n        for unit in unit_group:\n            if unit.tx_id:\n                tx_started = True\n                break\n\n        if tx_started:\n            return unit_group, ctx.state\n        else:\n            return unit_group, None\n\n    def compile_serialized_request_in_tx(\n        self,\n        state: dbstate.CompilerConnectionState,\n        txid: int,\n        serialized_request: bytes,\n        original_query: str,\n        expect_rollback: bool = False,\n    ) -> tuple[\n        dbstate.QueryUnitGroup | SQLDescriptors,\n        Optional[dbstate.CompilerConnectionState]\n    ]:\n        request = rpc.CompilationRequest.deserialize(\n            serialized_request,\n            original_query,\n            self.state.compilation_config_serializer,\n        )\n        return self.compile_in_tx(\n            state=state,\n            txid=txid,\n            request=request,\n            expect_rollback=expect_rollback,\n        )\n\n    def compile_in_tx(\n        self,\n        *,\n        state: dbstate.CompilerConnectionState,\n        txid: int,\n        request: rpc.CompilationRequest,\n        expect_rollback: bool = False,\n    ) -> tuple[\n        dbstate.QueryUnitGroup | SQLDescriptors,\n        Optional[dbstate.CompilerConnectionState]\n    ]:\n        if request.input_language is enums.InputLanguage.SQL_PARAMS:\n            tx = state.current_tx()\n            assert isinstance(request.source, rpc.SQLParamsSource)\n            return (\n                self.compile_sql_descriptors(\n                    tx.get_user_schema(),\n                    tx.get_global_schema(),\n                    request.protocol_version,\n                    request.source.types_in_out,\n                ),\n                # state is the same.\n                state,\n            )\n\n        # Apply session differences if any\n        if (\n            request.modaliases is not None\n            and state.current_tx().get_modaliases() != request.modaliases\n        ):\n            state.current_tx().update_modaliases(request.modaliases)\n        if (\n            (session_config := request.session_config) is not None\n            and state.current_tx().get_session_config() != session_config\n        ):\n            state.current_tx().update_session_config(session_config)\n\n        if (\n            expect_rollback and\n            state.current_tx().id != txid and\n            not state.can_sync_to_savepoint(txid)\n        ):\n            # This is a special case when COMMIT MIGRATION fails, the compiler\n            # doesn't have the right transaction state, so we just roll back.\n            assert isinstance(request.source, edgeql.Source)\n            return self._try_compile_rollback(request.source)[0], state\n        else:\n            state.sync_tx(txid)\n\n        ctx = CompileContext(\n            compiler_state=self.state,\n            state=state,\n            output_format=request.output_format,\n            expected_cardinality_one=request.expect_one,\n            implicit_limit=request.implicit_limit,\n            inline_typeids=request.inline_typeids,\n            inline_typenames=request.inline_typenames,\n            inline_objectids=request.inline_objectids,\n            source=request.source,\n            protocol_version=request.protocol_version,\n            json_parameters=request.input_format is enums.InputFormat.JSON,\n            expect_rollback=expect_rollback,\n            cache_key=request.get_cache_key(),\n        )\n\n        match request.input_language:\n            case enums.InputLanguage.EDGEQL:\n                assert isinstance(request.source, edgeql.Source)\n                unit_group = compile(ctx=ctx, source=request.source)\n            case enums.InputLanguage.GRAPHQL:\n                assert isinstance(request.source, graphql.Source)\n                unit_group = compile_graphql(\n                    ctx=ctx,\n                    source=request.source,\n                    variables=request.key_params,\n                )\n            case enums.InputLanguage.SQL:\n                assert isinstance(request.source, pg_parser.Source)\n                unit_group = compile_sql_as_unit_group(\n                    ctx=ctx, source=request.source)\n            case _:\n                raise NotImplementedError(\n                    f\"unnsupported input language: {request.input_language}\")\n\n        return unit_group, ctx.state\n\n    def compile_sql_descriptors(\n        self,\n        user_schema: s_schema.Schema,\n        global_schema: s_schema.Schema,\n        protocol_version: defines.ProtocolVersion,\n        types_in_out: list[tuple[list[str], list[tuple[str, str]]]],\n    ) -> SQLDescriptors:\n        schema = s_schema.ChainedSchema(\n            self.state.std_schema,\n            user_schema,\n            global_schema,\n        )\n\n        result = []\n\n        for in_out in types_in_out:\n            assert isinstance(in_out, tuple) and len(in_out) == 2\n\n            t_in = []\n            params = []\n            for idx, id in enumerate(in_out[0]):\n                param_name = str(idx + 1)\n                param_type = schema.get_by_id(turbo_uuid.UUID(id))\n                assert isinstance(param_type, s_types.Type)\n                param_required = False  # SQL arguments can always be NULL\n\n                if isinstance(param_type, s_types.Array):\n                    array_type_id = param_type.get_element_type(schema).id\n                else:\n                    array_type_id = None\n\n                t_in.append(\n                    (\n                        param_name,\n                        param_type,\n                        param_required,\n                    )\n                )\n\n                params.append(\n                    dbstate.Param(\n                        name=param_name,\n                        required=param_required,\n                        array_type_id=array_type_id,\n                        outer_idx=None,  # no script support for SQL\n                        sub_params=None,  # no tuple args support for SQL\n                        typename=str(param_type.get_name(schema)),\n                    )\n                )\n\n            input_desc, input_desc_id = sertypes.describe_params(\n                schema=schema, params=t_in,\n                protocol_version=protocol_version,\n            )\n\n            t_out = {\n                name: cast(s_types.Type, schema.get_by_id(turbo_uuid.UUID(id)))\n                for name, id in in_out[1]\n            }\n            assert all(isinstance(t, s_types.Type) for t in t_out.values())\n\n            output_desc, output_desc_id = sertypes.describe_sql_result(\n                schema=schema, row=t_out,\n                protocol_version=protocol_version,\n            )\n\n            result.append((\n                (input_desc, input_desc_id.bytes, params, len(params)),\n                (output_desc, output_desc_id.bytes)\n            ))\n\n        return result\n\n    def interpret_backend_error(\n        self,\n        user_schema: bytes,\n        global_schema: bytes,\n        error_fields: dict[str, str],\n        from_graphql: bool,\n    ) -> errors.EdgeDBError:\n        from . import errormech\n\n        schema = s_schema.ChainedSchema(\n            self.state.std_schema,\n            pickle.loads(user_schema),\n            pickle.loads(global_schema),\n        )\n        rv: errors.EdgeDBError = errormech.interpret_backend_error(\n            schema, error_fields, from_graphql=from_graphql\n        )\n        return rv\n\n    def parse_json_schema(\n        self,\n        schema_json: bytes,\n        base_schema: s_schema.Schema | None,\n    ) -> s_schema.Schema:\n        if base_schema is None:\n            base_schema = self.state.std_schema\n        else:\n            base_schema = s_schema.ChainedSchema(\n                self.state.std_schema,\n                s_schema.EMPTY_SCHEMA,\n                base_schema,\n            )\n\n        return s_refl.parse_schema(\n            base_schema=base_schema,\n            data=schema_json,\n            schema_class_layout=self.state.schema_class_layout,\n        )\n\n    def parse_db_config(\n        self, db_config_json: bytes, user_schema: s_schema.Schema\n    ) -> immutables.Map[str, config.SettingValue]:\n        spec = config.ChainedSpec(\n            self.state.config_spec,\n            config.load_ext_spec_from_schema(\n                user_schema,\n                self.state.std_schema,\n            ),\n        )\n        return config.from_json(spec, db_config_json)\n\n    def parse_global_schema(self, global_schema_json: bytes) -> bytes:\n        global_schema = self.parse_json_schema(global_schema_json, None)\n        return pickle.dumps(global_schema, -1)\n\n    def parse_user_schema_db_config(\n        self,\n        user_schema_json: bytes,\n        db_config_json: bytes,\n        global_schema_pickle: bytes,\n    ) -> dbstate.ParsedDatabase:\n        global_schema = pickle.loads(global_schema_pickle)\n        user_schema = self.parse_json_schema(user_schema_json, global_schema)\n        db_config = self.parse_db_config(db_config_json, user_schema)\n        ext_config_settings = config.load_ext_settings_from_schema(\n            s_schema.ChainedSchema(\n                self.state.std_schema,\n                user_schema,\n                s_schema.EMPTY_SCHEMA,\n            )\n        )\n        state_serializer = self.state.state_serializer_factory.make(\n            user_schema,\n            global_schema,\n            defines.CURRENT_PROTOCOL,\n        )\n        return dbstate.ParsedDatabase(\n            user_schema_pickle=pickle.dumps(user_schema, -1),\n            schema_version=_get_schema_version(user_schema),\n            database_config=db_config,\n            ext_config_settings=ext_config_settings,\n            protocol_version=defines.CURRENT_PROTOCOL,\n            state_serializer=state_serializer,\n            feature_used_metrics=ddl.produce_feature_used_metrics(\n                self.state, user_schema\n            ),\n        )\n\n    def make_state_serializer(\n        self,\n        protocol_version: defines.ProtocolVersion,\n        user_schema_pickle: bytes,\n        global_schema_pickle: bytes,\n    ) -> sertypes.StateSerializer:\n        user_schema = pickle.loads(user_schema_pickle)\n        global_schema = pickle.loads(global_schema_pickle)\n        return self.state.state_serializer_factory.make(\n            user_schema,\n            global_schema,\n            protocol_version,\n        )\n\n    def make_compilation_config_serializer(\n        self,\n    ) -> sertypes.CompilationConfigSerializer:\n        return self.state.compilation_config_serializer\n\n    def describe_database_dump(\n        self,\n        user_schema_json: bytes,\n        global_schema_json: bytes,\n        db_config_json: bytes,\n        protocol_version: defines.ProtocolVersion,\n        with_secrets: bool,\n    ) -> DumpDescriptor:\n        global_schema = self.parse_json_schema(global_schema_json, None)\n        user_schema = self.parse_json_schema(user_schema_json, global_schema)\n        database_config = self.parse_db_config(db_config_json, user_schema)\n        schema = s_schema.ChainedSchema(\n            self.state.std_schema,\n            user_schema,\n            global_schema\n        )\n\n        sys_config_ddl = config.to_edgeql(\n            self.state.config_spec, database_config, with_secrets=with_secrets,\n        )\n        # We need to put extension DDL configs *after* we have\n        # reloaded the schema\n        user_config_ddl = config.to_edgeql(\n            config.load_ext_spec_from_schema(\n                user_schema, self.state.std_schema),\n            database_config,\n            with_secrets=with_secrets,\n        )\n\n        schema_ddl = s_ddl.ddl_text_from_schema(\n            schema, include_migrations=True)\n\n        ids, sequences = get_obj_ids(schema)\n        raw_ids = [(name, cls, id.bytes) for name, cls, id in ids]\n\n        objtypes = schema.get_objects(\n            type=s_objtypes.ObjectType,\n            exclude_stdlib=True,\n        )\n        descriptors = []\n\n        cfg_object = schema.get('cfg::ConfigObject', type=s_objtypes.ObjectType)\n        for objtype in objtypes:\n            if objtype.is_union_type(schema) or objtype.is_view(schema):\n                continue\n            if objtype.issubclass(schema, cfg_object):\n                continue\n            descriptors.extend(_describe_object(schema, objtype,\n                                                protocol_version))\n\n        dynamic_ddl = []\n        if sequences:\n            seq_ids = ', '.join(\n                pg_common.quote_literal(str(seq_id))\n                for seq_id in sequences\n            )\n            dynamic_ddl.append(\n                f'SELECT edgedb._dump_sequences(ARRAY[{seq_ids}]::uuid[])'\n            )\n\n        return DumpDescriptor(\n            schema_ddl='\\n'.join([sys_config_ddl, schema_ddl, user_config_ddl]),\n            schema_dynamic_ddl=tuple(dynamic_ddl),\n            schema_ids=raw_ids,\n            blocks=descriptors,\n        )\n\n    def _reprocess_restore_config(\n        self,\n        stmts: Iterable[qlast.Base],\n    ) -> list[qlast.Base]:\n        '''Do any rewrites to the restore script needed.\n\n        This is intended to patch over certain backwards incompatible\n        changes to config. We try not to do that too much, but when we\n        do, dumps still need to work.\n        '''\n\n        new_stmts = []\n        smtp_config = {}\n\n        for stmt in stmts:\n            # ext::auth::SMTPConfig got removed and moved into a cfg\n            # object, so intercept those and rewrite them.\n            if (\n                isinstance(stmt, qlast.ConfigSet)\n                and stmt.name.module == 'ext::auth::SMTPConfig'\n            ):\n                smtp_config[stmt.name.name] = stmt.expr\n            else:\n                new_stmts.append(stmt)\n\n        if smtp_config:\n            # Do the rewrite of SMTPConfig\n            smtp_config['name'] = qlast.Constant.string('_default')\n\n            new_stmts.append(\n                qlast.ConfigInsert(\n                    scope=qltypes.ConfigScope.DATABASE,\n                    name=qlast.ObjectRef(\n                        module='cfg', name='SMTPProviderConfig'\n                    ),\n                    shape=[\n                        qlast.ShapeElement(\n                            expr=qlast.Path(steps=[qlast.Ptr(name=name)]),\n                            compexpr=expr,\n                        )\n                        for name, expr in smtp_config.items()\n                    ],\n                )\n            )\n            new_stmts.append(\n                qlast.ConfigSet(\n                    scope=qltypes.ConfigScope.DATABASE,\n                    name=qlast.ObjectRef(\n                        name='current_email_provider_name'\n                    ),\n                    expr=qlast.Constant.string('_default'),\n                )\n            )\n\n        return new_stmts\n\n    def describe_database_restore(\n        self,\n        user_schema_pickle: bytes,\n        global_schema_pickle: bytes,\n        dump_server_ver_str: str,\n        dump_catalog_version: Optional[int],\n        schema_ddl: bytes,\n        schema_ids: list[tuple[str, str, bytes]],\n        blocks: list[tuple[bytes, bytes]],  # type_id, typespec\n        protocol_version: defines.ProtocolVersion,\n    ) -> RestoreDescriptor:\n        schema_object_ids = {\n            (\n                s_name.name_from_string(name),\n                qltype if qltype else None\n            ): uuidgen.from_bytes(objid)\n            for name, qltype, objid in schema_ids\n        }\n\n        dump_server_ver = verutils.parse_version(dump_server_ver_str)\n\n        # catalog_version didn't exist until late in the 3.0 cycle,\n        # but we can just treat that as being version 0\n        dump_catalog_version = dump_catalog_version or 0\n\n        state = dbstate.CompilerConnectionState(\n            user_schema=pickle.loads(user_schema_pickle),\n            global_schema=pickle.loads(global_schema_pickle),\n            modaliases=DEFAULT_MODULE_ALIASES_MAP,\n            session_config=EMPTY_MAP,\n            database_config=EMPTY_MAP,\n            system_config=EMPTY_MAP,\n            cached_reflection=EMPTY_MAP,\n        )\n\n        ctx = CompileContext(\n            compiler_state=self.state,\n            state=state,\n            output_format=enums.OutputFormat.BINARY,\n            expected_cardinality_one=False,\n            compat_ver=dump_server_ver,\n            schema_object_ids=schema_object_ids,\n            log_ddl_as_migrations=False,\n            protocol_version=protocol_version,\n            dump_restore_mode=True,\n        )\n\n        ctx.state.start_tx()\n\n        dump_with_extraneous_computables = (\n            dump_server_ver < (1, 0, verutils.VersionStage.ALPHA, 8)\n        )\n\n        dump_with_ptr_item_id = dump_with_extraneous_computables\n\n        allow_dml_in_functions = (\n            dump_server_ver < (1, 0, verutils.VersionStage.BETA, 1)\n        )\n\n        # This change came late in the 3.0 dev cycle, and with it we\n        # switched to using catalog versions for this, so that nightly\n        # dumps might work.\n        dump_with_dunder_type = (\n            dump_catalog_version < 2023_02_16_00_00\n        )\n\n        schema_ddl_text = schema_ddl.decode('utf-8')\n\n        if allow_dml_in_functions:\n            schema_ddl_text = (\n                'CONFIGURE CURRENT DATABASE '\n                'SET allow_dml_in_functions := true;\\n'\n                + schema_ddl_text\n            )\n\n        ddl_source = edgeql.Source.from_string(schema_ddl_text)\n\n        # The state serializer generated below is somehow inappropriate,\n        # so it's simply ignored here and the I/O process will do it on its own\n        commands = edgeql.parse_block(ddl_source)\n        statements = self._reprocess_restore_config(commands)\n        units = _try_compile_ast(\n            ctx=ctx, source=ddl_source, statements=statements\n        ).units\n\n        _check_force_database_error(ctx, scope='restore')\n\n        schema = ctx.state.current_tx().get_schema(\n            ctx.compiler_state.std_schema)\n\n        # The AI extension needs to run some code before restoring data.\n        # TODO: Generalize this mechanism.\n        if schema.get_global(s_ext.Extension, 'ai', default=None):\n            from edb.pgsql import delta_ext_ai\n            ddl_source = edgeql.Source.from_string(\n                delta_ext_ai.get_ext_ai_pre_restore_script(schema))\n            units += compile(ctx=ctx, source=ddl_source).units\n\n        restore_blocks = []\n        tables = []\n        repopulate_units = []\n        for schema_object_id_bytes, typedesc in blocks:\n            schema_object_id = uuidgen.from_bytes(schema_object_id_bytes)\n            obj = schema.get_by_id(schema_object_id)\n            desc = sertypes.parse(typedesc, protocol_version)\n            elided_col_set = set()\n            mending_desc: list[Optional[DataMendingDescriptor]] = []\n\n            if isinstance(obj, s_props.Property):\n                assert isinstance(desc, sertypes.NamedTupleDesc)\n                desc_ptrs = list(desc.fields.keys())\n                cols = {\n                    'source': 'source',\n                    'target': 'target',\n                }\n\n                mending_desc.append(None)\n                mending_desc.append(_get_ptr_mending_desc(schema, obj))\n\n                if dump_with_ptr_item_id:\n                    elided_col_set.add('ptr_item_id')\n                    mending_desc.append(None)\n\n            elif isinstance(obj, s_links.Link):\n                assert isinstance(desc, sertypes.NamedTupleDesc)\n                desc_ptrs = list(desc.fields.keys())\n\n                cols = {}\n                ptrs = dict(obj.get_pointers(schema).items(schema))\n                for ptr_name in desc_ptrs:\n                    if dump_with_ptr_item_id and ptr_name == 'ptr_item_id':\n                        elided_col_set.add(ptr_name)\n                        cols[ptr_name] = ptr_name\n                        mending_desc.append(None)\n                    else:\n                        ptr = ptrs[s_name.UnqualName(ptr_name)]\n                        if (\n                            dump_with_extraneous_computables\n                            and ptr.is_pure_computable(schema)\n                        ):\n                            elided_col_set.add(ptr_name)\n                            mending_desc.append(None)\n\n                        if not ptr.is_dumpable(schema):\n                            continue\n\n                        stor_info = pg_types.get_pointer_storage_info(\n                            ptr,\n                            schema=schema,\n                            source=obj,\n                            link_bias=True,\n                        )\n\n                        cols[ptr_name] = stor_info.column_name\n                        mending_desc.append(\n                            _get_ptr_mending_desc(schema, ptr))\n\n            elif isinstance(obj, s_objtypes.ObjectType):\n                assert isinstance(desc, sertypes.ShapeDesc)\n                desc_ptrs = list(desc.fields.keys())\n\n                cols = {}\n                ptrs = dict(obj.get_pointers(schema).items(schema))\n                addons = {\n                    name: (col, type)\n                    for name, col, type in obj.get_addon_columns(schema)\n                }\n\n                for ptr_name in desc_ptrs:\n                    # If the pointer was one of our \"addon columns\"\n                    # (fts and ai shadow index columns), restore it\n                    # directly.\n                    #\n                    # N.B: This will need to become more sophisticated\n                    # if (when) we change the naming of any of our\n                    # addons.\n                    if ptr_name in addons:\n                        col, _type = addons[ptr_name]\n                        cols[ptr_name] = col\n                        mending_desc.append(None)\n                        continue\n\n                    ptr = ptrs[s_name.UnqualName(ptr_name)]\n                    if (\n                        dump_with_extraneous_computables\n                        and ptr.is_pure_computable(schema)\n                    ) or (\n                        dump_with_dunder_type\n                        and ptr_name == '__type__'\n                    ):\n                        elided_col_set.add(ptr_name)\n                        mending_desc.append(None)\n\n                    if not ptr.is_dumpable(schema):\n                        continue\n\n                    stor_info = pg_types.get_pointer_storage_info(\n                        ptr,\n                        schema=schema,\n                        source=obj,\n                    )\n\n                    if stor_info.table_type == 'ObjectType':\n                        ptr_name = ptr.get_shortname(schema).name\n                        cols[ptr_name] = stor_info.column_name\n                        mending_desc.append(\n                            _get_ptr_mending_desc(schema, ptr))\n\n                cmd = pg_delta.get_reindex_sql(obj, desc, schema)\n                if cmd:\n                    repopulate_units.append(cmd)\n\n            else:\n                raise AssertionError(\n                    f'unexpected object type in restore '\n                    f'type descriptor: {obj!r}'\n                )\n\n            _check_dump_layout(\n                frozenset(desc_ptrs),\n                frozenset(cols),\n                elided_col_set,\n                label=obj.get_verbosename(schema, with_parent=True),\n            )\n\n            table_name = pg_common.get_backend_name(\n                schema, obj, catenate=True)\n\n            elided_cols = tuple(i for i, pn in enumerate(desc_ptrs)\n                                if pn in elided_col_set)\n\n            col_list = (\n                pg_common.quote_ident(cols[pn])\n                for pn in desc_ptrs\n                if pn not in elided_col_set\n            )\n\n            stmt = (\n                f'COPY {table_name} '\n                f'({\", \".join(col_list)})'\n                f'FROM STDIN WITH (FORMAT binary, FREEZE true)'\n            ).encode()\n\n            restore_blocks.append(\n                RestoreBlockDescriptor(\n                    schema_object_id=schema_object_id,\n                    sql_copy_stmt=stmt,\n                    compat_elided_cols=elided_cols,\n                    data_mending_desc=tuple(mending_desc),\n                )\n            )\n\n            tables.append(table_name)\n\n        return RestoreDescriptor(\n            units=units,\n            blocks=restore_blocks,\n            tables=tables,\n            repopulate_units=repopulate_units,\n        )\n\n    def analyze_explain_output(\n        self,\n        query_asts_pickled: bytes,\n        data: list[list[bytes]],\n    ) -> bytes:\n        return explain.analyze_explain_output(\n            query_asts_pickled, data, self.state.std_schema)\n\n    def validate_schema_equivalence(\n        self,\n        schema_a: bytes,\n        schema_b: bytes,\n        global_schema: bytes,\n        conn_state_pickle: Optional[bytes],\n    ) -> None:\n        if conn_state_pickle:\n            conn_state = pickle.loads(conn_state_pickle)\n            if (\n                conn_state\n                and (\n                    conn_state.current_tx().get_migration_state()\n                    or conn_state.current_tx().get_migration_rewrite_state()\n                )\n            ):\n                return\n        ddl.validate_schema_equivalence(\n            self.state,\n            pickle.loads(schema_a),\n            pickle.loads(schema_b),\n            pickle.loads(global_schema),\n        )\n\n    def compile_structured_config(\n        self,\n        objects: Mapping[str, config_compiler.ConfigObject],\n        source: str | None = None,\n        allow_nested: bool = False,\n    ) -> dict[str, immutables.Map[str, config.SettingValue]]:\n        # XXX: only config in the stdlib is supported currently, so the only\n        # key allowed in objects is \"cfg::Config\". API for future compatibility\n        if list(objects) != [\"cfg::Config\"]:\n            difference = set(objects) - {\"cfg::Config\"}\n            raise NotImplementedError(\n                f\"unsupported config: {', '.join(difference)}\"\n            )\n\n        return config_compiler.compile_structured_config(\n            objects,\n            spec=self.state.config_spec,\n            schema=self.state.std_schema,\n            source=source,\n            allow_nested=allow_nested,\n        )\n\n\ndef compile_schema_storage_in_delta(\n    ctx: CompileContext,\n    delta: s_delta.Command,\n    block: pg_dbops.SQLBlock,\n    context: Optional[s_delta.CommandContext] = None,\n) -> None:\n\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n\n    funcblock = block.add_block()\n    cmdblock = block.add_block()\n\n    meta_blocks: list[tuple[str, dict[str, Any]]] = []\n\n    # Use a provided context if one was passed in, which lets us\n    # used the cached values for resolved properties. (Which is\n    # important, since if there were renames we won't necessarily\n    # be able to resolve them just using the new schema.)\n    if not context:\n        context = s_delta.CommandContext()\n    else:\n        context.renames.clear()\n        context.early_renames.clear()\n\n    s_refl.generate_metadata_write_edgeql(\n        delta,\n        classlayout=ctx.compiler_state.schema_class_layout,\n        schema=schema,\n        context=context,\n        blocks=meta_blocks,\n        internal_schema_mode=ctx.internal_schema_mode,\n        stdmode=ctx.bootstrap_mode,\n    )\n\n    cache = current_tx.get_cached_reflection()\n\n    with cache.mutate() as cache_mm:\n        for eql, args in meta_blocks:\n            eql_hash = hashlib.sha1(eql.encode()).hexdigest()\n            fname = (pg_common.versioned_schema('edgedb'), f'__rh_{eql_hash}')\n\n            if eql_hash in cache_mm:\n                argnames = cache_mm[eql_hash]\n            else:\n                sql, argmap = _compile_schema_storage_stmt(ctx, eql)\n                argnames = tuple(arg.name for arg in argmap)\n\n                func = pg_dbops.Function(\n                    name=fname,\n                    args=[(argname, 'json') for argname in argnames],\n                    returns='json',\n                    text=sql,\n                )\n\n                # We drop first instead of using or_replace, in case\n                # something about the arguments changed.\n                df = pg_dbops.DropFunction(\n                    name=func.name,\n                    args=func.args or (),\n                    # Use a condition instead of if_exists ot reduce annoying\n                    # debug spew from postgres.\n                    conditions=[pg_dbops.FunctionExists(\n                        name=func.name,\n                        args=func.args or (),\n                    )],\n                )\n                df.generate(funcblock)\n\n                cf = pg_dbops.CreateFunction(func)\n                cf.generate(funcblock)\n\n                cache_mm[eql_hash] = argnames\n\n            argvals = []\n            for argname in argnames:\n                argvals.append(pg_common.quote_literal(args[argname]))\n\n            cmdblock.add_command(textwrap.dedent(f'''\\\n                PERFORM {pg_common.qname(*fname)}({\", \".join(argvals)});\n            '''))\n\n    ctx.state.current_tx().update_cached_reflection(cache_mm.finish())\n\n\ndef _compile_schema_storage_stmt(\n    ctx: CompileContext,\n    eql: str,\n    output_format: enums.OutputFormat = enums.OutputFormat.JSON,\n) -> tuple[str, Sequence[dbstate.Param]]:\n\n    schema = ctx.state.current_tx().get_schema(ctx.compiler_state.std_schema)\n\n    try:\n        # Switch to the shadow introspection/reflection schema.\n        ctx.state.current_tx().update_schema(\n            # Trick dbstate to set the effective schema\n            # to refl_schema.\n            s_schema.ChainedSchema(\n                ctx.compiler_state.std_schema,\n                ctx.compiler_state.refl_schema,\n                s_schema.EMPTY_SCHEMA\n            )\n        )\n\n        newctx = CompileContext(\n            compiler_state=ctx.compiler_state,\n            state=ctx.state,\n            json_parameters=True,\n            schema_reflection_mode=True,\n            output_format=output_format,\n            expected_cardinality_one=False,\n            bootstrap_mode=ctx.bootstrap_mode,\n            protocol_version=ctx.protocol_version,\n            backend_runtime_params=ctx.backend_runtime_params,\n        )\n\n        source = edgeql.Source.from_string(eql)\n        unit_group = compile(ctx=newctx, source=source)\n\n        sql_stmts = []\n        for u in unit_group:\n            stmt = u.sql.strip()\n            if not stmt.endswith(b';'):\n                stmt += b';'\n\n            sql_stmts.append(stmt)\n\n        if len(sql_stmts) > 1:\n            raise errors.InternalServerError(\n                'compilation of schema update statement'\n                ' yielded more than one SQL statement'\n            )\n\n        sql = sql_stmts[0].strip(b';').decode()\n        argmap: Optional[Sequence[dbstate.Param]] = unit_group[0].in_type_args\n        if argmap is None:\n            argmap = ()\n\n        return sql, argmap\n\n    finally:\n        # Restore the regular schema.\n        ctx.state.current_tx().update_schema(schema)\n\n\ndef _get_schema_version(user_schema: s_schema.Schema) -> uuid.UUID:\n    ver = user_schema.get_global(s_ver.SchemaVersion, \"__schema_version__\")\n    return ver.get_version(user_schema)\n\n\ndef _compile_ql_script(\n    ctx: CompileContext,\n    eql: str,\n) -> str:\n\n    source = edgeql.Source.from_string(eql)\n    unit_group = compile(ctx=ctx, source=source)\n\n    sql_stmts = []\n    for u in unit_group:\n        stmt = u.sql.strip()\n        if not stmt.endswith(b';'):\n            stmt += b';'\n\n        sql_stmts.append(stmt)\n\n    return b'\\n'.join(sql_stmts).decode()\n\n\ndef _get_compile_options(\n    ctx: CompileContext,\n    *,\n    is_explain: bool = False,\n    no_implicit_fields: bool = False,\n) -> qlcompiler.CompilerOptions:\n    can_have_implicit_fields = not no_implicit_fields and (\n        ctx.output_format is enums.OutputFormat.BINARY\n    )\n\n    return qlcompiler.CompilerOptions(\n        modaliases=ctx.state.current_tx().get_modaliases(),\n        implicit_tid_in_shapes=(\n            can_have_implicit_fields and ctx.inline_typeids\n        ),\n        implicit_tname_in_shapes=(\n            can_have_implicit_fields and ctx.inline_typenames\n        ),\n        implicit_id_in_shapes=(\n            can_have_implicit_fields and ctx.inline_objectids\n        ),\n        json_parameters=ctx.json_parameters,\n        implicit_limit=ctx.implicit_limit,\n        bootstrap_mode=ctx.bootstrap_mode,\n        dump_restore_mode=ctx.dump_restore_mode,\n        apply_query_rewrites=(\n            not ctx.bootstrap_mode\n            and not ctx.schema_reflection_mode\n            and not ctx.dump_restore_mode  # HMMM\n            and not bool(\n                _get_config_val(ctx, '__internal_no_apply_query_rewrites'))\n        ),\n        apply_user_access_policies=_get_config_val(\n            ctx, 'apply_access_policies'),\n        allow_user_specified_id=_get_config_val(\n            ctx, 'allow_user_specified_id') or ctx.schema_reflection_mode,\n        is_explain=is_explain,\n        testmode=ctx.is_testmode(),\n        schema_reflection_mode=(\n            ctx.schema_reflection_mode\n            or _get_config_val(ctx, '__internal_query_reflschema')\n        ),\n    )\n\n\n# Types and default values for EXPLAIN parameters\nEXPLAIN_PARAMS = dict(\n    buffers=('std::bool', False),\n    execute=('std::bool', True),\n)\n\n\ndef _compile_ql_explain(\n    ctx: CompileContext,\n    ql: qlast.ExplainStmt,\n    *,\n    script_info: Optional[irast.ScriptInfo] = None,\n) -> dbstate.BaseQuery:\n    args = {k: v for k, (_, v) in EXPLAIN_PARAMS.items()}\n\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n\n    # Evaluate and typecheck arguments\n    if ql.args:\n        for el in ql.args.elements:\n            name = el.name.name\n            if name not in EXPLAIN_PARAMS:\n                raise errors.QueryError(\n                    f\"unknown ANALYZE argument '{name}'\",\n                    span=el.span,\n                )\n            arg_ir = qlcompiler.compile_ast_to_ir(\n                el.val,\n                schema=schema,\n                options=qlcompiler.CompilerOptions(\n                    modaliases=current_tx.get_modaliases(),\n                ),\n            )\n            exp_typ = schema.get(EXPLAIN_PARAMS[name][0], type=s_types.Type)\n            if not arg_ir.stype.issubclass(schema, exp_typ):\n                raise errors.QueryError(\n                    f\"incorrect type for ANALYZE argument '{name}': \"\n                    f\"expected '{exp_typ.get_name(schema)}', \"\n                    f\"got '{arg_ir.stype.get_name(schema)}'\",\n                    span=el.span,\n                )\n\n            args[name] = ireval.evaluate_to_python_val(arg_ir.expr, schema)\n\n    analyze = 'ANALYZE true, ' if args['execute'] else ''\n    buffers = 'BUFFERS, ' if args['buffers'] else ''\n    exp_command = f'EXPLAIN ({analyze}{buffers}FORMAT JSON, VERBOSE true)'\n\n    ctx = dataclasses.replace(\n        ctx,\n        inline_typeids=False,\n        inline_typenames=False,\n        implicit_limit=0,\n        output_format=enums.OutputFormat.BINARY,\n    )\n\n    config_vals = _get_compilation_config_vals(ctx)\n    modaliases = ctx.state.current_tx().get_modaliases()\n    explain_data = (config_vals, args, modaliases)\n\n    query = _compile_ql_query(\n        ctx, ql.query, script_info=script_info,\n        explain_data=explain_data, cacheable=False)\n    if isinstance(query, dbstate.NullQuery):\n        raise errors.QueryError(\n            f\"cannot ANALYZE inside of a migration\",\n            span=ql.span,\n        )\n\n    assert query.sql\n\n    out_type_data, out_type_id = sertypes.describe(\n        schema,\n        schema.get(\"std::str\", type=s_types.Type),\n        protocol_version=ctx.protocol_version,\n    )\n\n    sql_bytes = exp_command.encode('utf-8') + query.sql\n    sql_hash = _hash_sql(\n        sql_bytes,\n        mode=str(ctx.output_format).encode(),\n        intype=query.in_type_id,\n        outtype=out_type_id.bytes)\n\n    return dataclasses.replace(\n        query,\n        is_explain=True,\n        run_and_rollback=args['execute'],\n        cacheable=False,\n        sql=sql_bytes,\n        sql_hash=sql_hash,\n        cardinality=enums.Cardinality.ONE,\n        out_type_data=out_type_data,\n        out_type_id=out_type_id.bytes,\n    )\n\n\ndef _compile_ql_administer(\n    ctx: CompileContext,\n    ql: qlast.AdministerStmt,\n    *,\n    script_info: Optional[irast.ScriptInfo] = None,\n) -> dbstate.BaseQuery:\n    if ql.expr.func == 'statistics_update':\n        res = ddl.administer_statistics_update(ctx, ql)\n    elif ql.expr.func == 'schema_repair':\n        res = ddl.administer_repair_schema(ctx, ql)\n    elif ql.expr.func == 'reindex':\n        res = ddl.administer_reindex(ctx, ql)\n    elif ql.expr.func == 'vacuum':\n        res = ddl.administer_vacuum(ctx, ql)\n    elif ql.expr.func == 'prepare_upgrade':\n        res = ddl.administer_prepare_upgrade(ctx, ql)\n    elif ql.expr.func == '_remove_pointless_triggers':\n        res = ddl.administer_remove_pointless_triggers(ctx, ql)\n    elif ql.expr.func == 'concurrent_index_build':\n        res = ddl.administer_concurrent_index_build(ctx, ql)\n    elif ql.expr.func == 'fixup_backend_upgrade':\n        res = ddl.administer_fixup_backend_upgrade(ctx, ql)\n    else:\n        raise errors.QueryError(\n            'Unknown ADMINISTER function',\n            span=ql.expr.span,\n        )\n\n    if debug.flags.delta_execute or debug.flags.delta_execute_ddl:\n        debug.header('ADMINISTER script')\n        debug.dump_code(res.sql, lexer='sql')\n\n    return res\n\n\ndef _compile_ql_query(\n    ctx: CompileContext,\n    ql: qlast.Query | qlast.Command,\n    *,\n    script_info: Optional[irast.ScriptInfo] = None,\n    source: Optional[edgeql.Source] = None,\n    cacheable: bool = True,\n    migration_block_query: bool = False,\n    explain_data: object = None,\n) -> dbstate.Query | dbstate.NullQuery:\n\n    is_explain = explain_data is not None\n    current_tx = ctx.state.current_tx()\n\n    sql_info: dict[str, Any] = {}\n    if (\n        not ctx.bootstrap_mode\n        and ctx.backend_runtime_params.has_stat_statements\n        and not ctx.schema_reflection_mode\n    ):\n        spec = ctx.compiler_state.config_spec\n        cconfig = config.to_json_obj(\n            spec,\n            {\n                **current_tx.get_system_config(),\n                **current_tx.get_database_config(),\n                **current_tx.get_session_config(),\n            },\n            setting_filter=lambda v: v.name in spec\n                                     and spec[v.name].affects_compilation,\n            include_source=False,\n        )\n        extras: dict[str, Any] = {\n            'cc': dict(sorted(cconfig.items())),  # compilation_config\n            'pv': ctx.protocol_version,  # protocol_version\n            'of': ctx.output_format,  # output_format\n            'e1': ctx.expected_cardinality_one,  # expect_one\n            'il': ctx.implicit_limit,  # implicit_limit\n            'ii': ctx.inline_typeids,  # inline_typeids\n            'in': ctx.inline_typenames,  # inline_typenames\n            'io': ctx.inline_objectids,  # inline_objectids\n        }\n        modaliases = dict(current_tx.get_modaliases())\n        # dn: default_namespace\n        extras['dn'] = modaliases.pop(None, defines.DEFAULT_MODULE_ALIAS)\n        if modaliases:\n            # na: namespace_aliases\n            extras['na'] = dict(sorted(modaliases.items()))\n\n        sql_info.update({\n            'query': qlcodegen.generate_source(ql),\n            'type': defines.QueryType.EdgeQL,\n            'extras': json.dumps(extras),\n        })\n        id_hash = hashlib.blake2b(digest_size=16)\n        id_hash.update(\n            json.dumps(sql_info).encode(defines.EDGEDB_ENCODING)\n        )\n        sql_info['id'] = str(uuidgen.from_bytes(id_hash.digest()))\n\n    base_schema = (\n        ctx.compiler_state.std_schema\n        if not _get_config_val(ctx, '__internal_query_reflschema')\n        else ctx.compiler_state.refl_schema\n    )\n    schema = current_tx.get_schema(base_schema)\n\n    options = _get_compile_options(ctx, is_explain=is_explain)\n    ir = qlcompiler.compile_ast_to_ir(\n        ql,\n        schema=schema,\n        script_info=script_info,\n        options=options,\n    )\n    result_cardinality = enums.cardinality_from_ir_value(ir.cardinality)\n\n    # This low-hanging-fruit is temporary; persistent cache should cover all\n    # cacheable cases properly in future changes.\n    use_persistent_cache = (\n        cacheable\n        and not ctx.bootstrap_mode\n        and script_info is None\n        and ctx.cache_key is not None\n    )\n    cache_mode = ctx.get_cache_mode()\n\n    sql_res = pg_compiler.compile_ir_to_sql_tree(\n        ir,\n        expected_cardinality_one=ctx.expected_cardinality_one,\n        output_format=_convert_format(ctx.output_format),\n        json_parameters=options.json_parameters,\n        backend_runtime_params=ctx.backend_runtime_params,\n        is_explain=options.is_explain,\n        cache_as_function=(use_persistent_cache\n                       and cache_mode is config.QueryCacheMode.PgFunc),\n        versioned_stdlib=True,\n    )\n\n    sql_text = pg_codegen.generate_source(sql_res.ast)\n    func_call_sql = None\n\n    pg_debug.dump_ast_and_query(sql_res.ast, ir)\n\n    if use_persistent_cache and cache_mode is config.QueryCacheMode.PgFunc:\n        cache_sql, func_call_ast = _build_cache_function(ctx, ir, sql_res)\n        func_call_sql = pg_codegen.generate_source(func_call_ast)\n    elif (\n        use_persistent_cache and cache_mode is config.QueryCacheMode.RegInline\n    ):\n        cache_sql = (b\"\", b\"\")\n    else:\n        cache_sql = None\n\n    if (\n        (mstate := current_tx.get_migration_state())\n        and not migration_block_query\n    ):\n        if isinstance(ql, qlast.Query):\n            mstate = mstate._replace(\n                accepted_cmds=(\n                    mstate.accepted_cmds + (qlast.DDLQuery(query=ql),)\n                )\n            )\n            current_tx.update_migration_state(mstate)\n\n        return dbstate.NullQuery()\n\n    # If requested, embed the EdgeQL text in the SQL.\n    if debug.flags.edgeql_text_in_sql and source:\n        sql_info['edgeql'] = source.text()\n    if sql_info:\n        sql_info_prefix = '-- ' + json.dumps(sql_info) + '\\n'\n    else:\n        sql_info_prefix = ''\n\n    globals = None\n    permissions = None\n    json_permissions = None\n    if ir.globals:\n        globals = [\n            (str(glob.global_name), glob.has_present_arg)\n            for glob in ir.globals\n            if not glob.is_permission\n        ]\n        permissions = [\n            str(glob.global_name)\n            for glob in ir.globals\n            if glob.is_permission\n        ]\n    if options.json_parameters:\n        # In JSON parameters mode, keep only the synthetic globals,\n        # and report the permissions as needing to be injected into\n        # the JSON.\n        if globals:\n            globals = [g for g in globals if g[0].startswith('__::')]\n        json_permissions, permissions = permissions, []\n\n    required_permissions = None\n    if ir.required_permissions:\n        required_permissions = [\n            str(perm.get_name(schema)) for perm in ir.required_permissions\n        ]\n\n    out_type_id: uuid.UUID\n    if ctx.output_format is enums.OutputFormat.NONE:\n        out_type_id = sertypes.NULL_TYPE_ID\n        out_type_data = sertypes.NULL_TYPE_DESC\n        result_cardinality = enums.Cardinality.NO_RESULT\n    elif ctx.output_format is enums.OutputFormat.BINARY:\n        out_type_data, out_type_id = sertypes.describe(\n            ir.schema, ir.stype,\n            ir.view_shapes, ir.view_shapes_metadata,\n            inline_typenames=ctx.inline_typenames,\n            protocol_version=ctx.protocol_version)\n    else:\n        out_type_data, out_type_id = sertypes.describe(\n            ir.schema,\n            ir.schema.get(\"std::str\", type=s_types.Type),\n            protocol_version=ctx.protocol_version,\n        )\n\n    in_type_args, in_type_data, in_type_id = describe_params(\n        ctx, ir, sql_res.argmap, script_info\n    )\n\n    server_param_conversions: Optional[\n        list[dbstate.ServerParamConversion]\n    ] = None\n    if isinstance(ir, irast.Statement) and ir.server_param_conversions:\n        # The irast.ServerParamConversion we get from the ql compiler contains\n        # either a script_param_index or a constant value.\n        #\n        # A script_param_index can refer to either an actual query param or a\n        # constant that was normalized out of a query.\n        #\n        # A constant value is used as is by the server.\n\n        server_param_conversions = [\n            dbstate.ServerParamConversion(\n                param_name=p.param_name,\n                conversion_name=p.conversion_name,\n                additional_info=p.additional_info,\n                script_param_index=p.script_param_index,\n                constant_value=p.constant_value\n            )\n            for p in ir.server_param_conversions\n        ]\n\n    sql_hash = _hash_sql(\n        sql_text.encode(defines.EDGEDB_ENCODING),\n        mode=str(ctx.output_format).encode(),\n        intype=in_type_id.bytes,\n        outtype=out_type_id.bytes)\n\n    cache_func_call = None\n    if func_call_sql is not None:\n        func_call_sql_hash = _hash_sql(\n            func_call_sql.encode(defines.EDGEDB_ENCODING),\n            mode=str(ctx.output_format).encode(),\n            intype=in_type_id.bytes,\n            outtype=out_type_id.bytes,\n        )\n        cache_func_call = (\n            (sql_info_prefix + func_call_sql).encode(defines.EDGEDB_ENCODING),\n            func_call_sql_hash,\n        )\n\n    if is_explain:\n        if isinstance(ir.schema, s_schema.ChainedSchema):\n            # Strip the std schema out\n            ir.schema = s_schema.ChainedSchema(\n                top_schema=ir.schema._top_schema,\n                global_schema=ir.schema._global_schema,\n                base_schema=s_schema.EMPTY_SCHEMA,\n            )\n        query_asts = pickle.dumps((ql, ir, sql_res.ast, explain_data))\n    else:\n        query_asts = None\n\n    return dbstate.Query(\n        sql=(sql_info_prefix + sql_text).encode(defines.EDGEDB_ENCODING),\n        sql_hash=sql_hash,\n        cache_sql=cache_sql,\n        cache_func_call=cache_func_call,\n        cardinality=result_cardinality,\n        globals=globals,\n        permissions=permissions,\n        required_permissions=required_permissions,\n        json_permissions=json_permissions,\n        in_type_id=in_type_id.bytes,\n        in_type_data=in_type_data,\n        in_type_args=in_type_args,\n        out_type_id=out_type_id.bytes,\n        out_type_data=out_type_data,\n        server_param_conversions=server_param_conversions,\n        cacheable=cacheable,\n        has_dml=bool(ir.dml_exprs),\n        query_asts=query_asts,\n        warnings=ir.warnings,\n        unsafe_isolation_dangers=ir.unsafe_isolation_dangers,\n    )\n\n\ndef _build_cache_function(\n    ctx: CompileContext,\n    ir: irast.Statement,\n    sql_res: pg_compiler.CompileResult,\n) -> tuple[tuple[bytes, bytes], pgast.Base]:\n    sql_ast = sql_res.ast\n    assert ctx.cache_key is not None\n    key = ctx.cache_key.hex\n    returns_record = False\n    set_returning = True\n    return_type: tuple[str, ...] = (\"unknown\",)\n    match ctx.output_format:\n        case enums.OutputFormat.NONE:\n            # CONFIGURE commands are never cached; other queries are actually\n            # wrapped with a count() call in top_output_as_value(), so set the\n            # return_type to reflect that fact. This was set to `void`, leading\n            # to issues that certain exceptions are not raised as expected when\n            # wrapped with a function returning (setof) void - reproducible\n            # with test_edgeql_casts_json_12() and EDGEDB_TEST_REPEATS=1.\n            return_type = (\"int\",)\n            if ir.stype.is_object_type() or ir.stype.is_tuple(ir.schema):\n                returns_record = True\n\n        case enums.OutputFormat.BINARY:\n            if ir.stype.is_object_type():\n                return_type = (\"record\",)\n                returns_record = True\n            else:\n                return_type = pg_types.pg_type_from_ir_typeref(\n                    ir.expr.typeref.base_type or ir.expr.typeref,\n                    serialized=True,\n                )\n                if ir.stype.is_tuple(ir.schema):\n                    returns_record = return_type == ('record',)\n\n        case enums.OutputFormat.JSON:\n            return_type = (\"json\",)\n\n        case enums.OutputFormat.JSON_ELEMENTS:\n            return_type = (\"json\",)\n    if returns_record:\n        assert isinstance(sql_ast, pgast.ReturningQuery)\n        sql_ast.target_list.append(\n            pgast.ResTarget(\n                name=\"sentinel\",\n                val=pgast.BooleanConstant(val=True),\n            ),\n        )\n\n    # XXX: we need to put the version in the key\n    fname = (pg_common.versioned_schema(\"edgedb\"), f\"__qh_{key}\")\n    func = pg_dbops.Function(\n        name=fname,\n        args=[(None, arg) for arg in sql_res.cached_params or []],\n        returns=return_type,\n        set_returning=set_returning,\n        text=pg_codegen.generate_source(sql_ast),\n    )\n    if not ir.dml_exprs:\n        func.volatility = \"stable\"\n    cf = pg_dbops.SQLBlock()\n    pg_dbops.CreateFunction(func).generate(cf)\n    df = pg_dbops.SQLBlock()\n    pg_dbops.DropFunction(\n        name=func.name,\n        args=func.args or (),\n        # Use a condition instead of if_exists ot reduce annoying\n        # debug spew from postgres.\n        conditions=[pg_dbops.FunctionExists(\n            name=func.name,\n            args=func.args or (),\n        )],\n    ).generate(df)\n    func_call = pgast.FuncCall(\n        name=fname,\n        args=[\n            pgast.TypeCast(\n                arg=pgast.ParamRef(number=i),\n                type_name=pgast.TypeName(name=arg),\n            )\n            for i, arg in enumerate(sql_res.cached_params or [], 1)\n        ],\n        coldeflist=[],\n    )\n    if returns_record:\n        func_call.coldeflist.extend(\n            [\n                pgast.ColumnDef(\n                    name=\"result\",\n                    typename=pgast.TypeName(name=(\"record\",)),\n                ),\n                pgast.ColumnDef(\n                    name=\"sentinel\",\n                    typename=pgast.TypeName(name=(\"bool\",)),\n                ),\n            ]\n        )\n        sql_ast = pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(val=pgast.ColumnRef(name=(\"result\",))),\n            ],\n            from_clause=[\n                pgast.RangeFunction(\n                    functions=[func_call],\n                    is_rowsfrom=True,\n                ),\n            ],\n        )\n    else:\n        sql_ast = pgast.SelectStmt(\n            target_list=[pgast.ResTarget(val=func_call)],\n        )\n    cache_sql = (\n        cf.to_string().encode(defines.EDGEDB_ENCODING),\n        df.to_string().encode(defines.EDGEDB_ENCODING),\n    )\n    return cache_sql, sql_ast\n\n\ndef describe_params(\n    ctx: CompileContext,\n    ir: irast.Statement | irast.ConfigCommand,\n    argmap: dict[str, pgast.Param],\n    script_info: Optional[irast.ScriptInfo],\n) -> tuple[Optional[list[dbstate.Param]], bytes, uuid.UUID]:\n    in_type_args = None\n    params: list[tuple[str, s_types.Type, bool]] = []\n    assert ir.schema\n    if ir.params:\n        params, in_type_args = _extract_params(\n            ir.params,\n            argmap=argmap,\n            script_info=script_info,\n            schema=ir.schema,\n            ctx=ctx,\n        )\n\n    in_type_data, in_type_id = sertypes.describe_params(\n        schema=ir.schema,\n        params=params,\n        protocol_version=ctx.protocol_version,\n    )\n    return in_type_args, in_type_data, in_type_id\n\n\ndef _compile_ql_transaction(\n    ctx: CompileContext, ql: qlast.Transaction\n) -> dbstate.TxControlQuery:\n\n    cacheable = True\n\n    modaliases = None\n    final_user_schema: Optional[s_schema.Schema] = None\n    final_cached_reflection = None\n    final_global_schema: Optional[s_schema.Schema] = None\n    sp_name = None\n    sp_id = None\n    iso = None\n\n    if ctx.expect_rollback and not isinstance(\n        ql, (qlast.RollbackTransaction, qlast.RollbackToSavepoint)\n    ):\n        raise errors.TransactionError(\n            'expected a ROLLBACK or ROLLBACK TO SAVEPOINT command'\n        )\n\n    if isinstance(ql, qlast.StartTransaction):\n        ctx._assert_not_in_migration_block(ql)\n\n        ctx.state.start_tx()\n\n        # Compute the effective isolation level\n        iso_config: statypes.TransactionIsolation = _get_config_val(\n            ctx, \"default_transaction_isolation\"\n        )\n        default_iso = iso_config.to_qltypes()\n        iso = ql.isolation\n        if iso is None:\n            iso = default_iso\n\n        # Compute the effective access mode\n        access = ql.access\n        if access is None:\n            access_mode: statypes.TransactionAccessMode = _get_config_val(\n                ctx, \"default_transaction_access_mode\"\n            )\n            access = access_mode.to_qltypes()\n\n        sqls = f'START TRANSACTION ISOLATION LEVEL {iso.value} {access.value}'\n        if ql.deferrable is not None:\n            sqls += f' {ql.deferrable.value}'\n        sqls += ';'\n        sql = sqls.encode()\n\n        action = dbstate.TxAction.START\n        cacheable = False\n\n    elif isinstance(ql, qlast.CommitTransaction):\n        ctx._assert_not_in_migration_block(ql)\n\n        cur_tx = ctx.state.current_tx()\n        final_user_schema = cur_tx.get_user_schema_if_updated()\n        final_cached_reflection = cur_tx.get_cached_reflection_if_updated()\n        final_global_schema = cur_tx.get_global_schema_if_updated()\n\n        new_state = ctx.state.commit_tx()\n        modaliases = new_state.modaliases\n\n        sql = b'COMMIT'\n        cacheable = False\n        action = dbstate.TxAction.COMMIT\n\n    elif isinstance(ql, qlast.RollbackTransaction):\n        new_state = ctx.state.rollback_tx()\n        modaliases = new_state.modaliases\n\n        sql = b'ROLLBACK'\n        cacheable = False\n        action = dbstate.TxAction.ROLLBACK\n\n    elif isinstance(ql, qlast.DeclareSavepoint):\n        tx = ctx.state.current_tx()\n        sp_id = tx.declare_savepoint(ql.name)\n\n        pgname = pg_common.quote_ident(ql.name)\n        sql = f'SAVEPOINT {pgname}'.encode()\n\n        cacheable = False\n        action = dbstate.TxAction.DECLARE_SAVEPOINT\n\n        sp_name = ql.name\n\n    elif isinstance(ql, qlast.ReleaseSavepoint):\n        ctx.state.current_tx().release_savepoint(ql.name)\n        pgname = pg_common.quote_ident(ql.name)\n        sql = f'RELEASE SAVEPOINT {pgname}'.encode()\n        action = dbstate.TxAction.RELEASE_SAVEPOINT\n\n    elif isinstance(ql, qlast.RollbackToSavepoint):\n        tx = ctx.state.current_tx()\n        new_state = tx.rollback_to_savepoint(ql.name)\n        modaliases = new_state.modaliases\n\n        pgname = pg_common.quote_ident(ql.name)\n        sql = f'ROLLBACK TO SAVEPOINT {pgname};'.encode()\n        cacheable = False\n        action = dbstate.TxAction.ROLLBACK_TO_SAVEPOINT\n        sp_name = ql.name\n\n    else:  # pragma: no cover\n        raise ValueError(f'expected a transaction AST node, got {ql!r}')\n\n    return dbstate.TxControlQuery(\n        sql=sql,\n        action=action,\n        cacheable=cacheable,\n        modaliases=modaliases,\n        user_schema=final_user_schema,\n        cached_reflection=final_cached_reflection,\n        global_schema=final_global_schema,\n        sp_name=sp_name,\n        sp_id=sp_id,\n        isolation_level=iso,\n        feature_used_metrics=(\n            ddl.produce_feature_used_metrics(\n                ctx.compiler_state, final_user_schema\n            ) if final_user_schema else None\n        ),\n    )\n\n\ndef _compile_ql_sess_state(\n    ctx: CompileContext, ql: qlast.SessionCommand\n) -> dbstate.SessionStateQuery:\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n\n    aliases = ctx.state.current_tx().get_modaliases()\n\n    if isinstance(ql, qlast.SessionSetAliasDecl):\n        try:\n            schema.get_global(s_mod.Module, ql.decl.module)\n        except errors.InvalidReferenceError:\n            raise errors.UnknownModuleError(\n                f'module {ql.decl.module!r} does not exist'\n            ) from None\n\n        aliases = aliases.set(ql.decl.alias, ql.decl.module)\n\n    elif isinstance(ql, qlast.SessionResetModule):\n        aliases = aliases.set(None, s_mod.DEFAULT_MODULE_ALIAS)\n\n    elif isinstance(ql, qlast.SessionResetAllAliases):\n        aliases = DEFAULT_MODULE_ALIASES_MAP\n\n    elif isinstance(ql, qlast.SessionResetAliasDecl):\n        aliases = aliases.delete(ql.alias)\n\n    else:  # pragma: no cover\n        raise errors.InternalServerError(\n            f'unsupported SET command type {type(ql)!r}')\n\n    ctx.state.current_tx().update_modaliases(aliases)\n\n    return dbstate.SessionStateQuery()\n\n\ndef _get_config_spec(\n    ctx: CompileContext, config_op: config.Operation\n) -> config.Spec:\n    config_spec = ctx.compiler_state.config_spec\n    if config_op.setting_name not in config_spec:\n        # We don't typically bother tracking the user config spec in\n        # the compiler workers (to avoid needing to bother with\n        # transmitting, caching, or computing it). If we hit a config\n        # op that needs it, load the spec.\n        config_spec = config.ChainedSpec(\n            config_spec,\n            config.load_ext_spec_from_schema(\n                ctx.state.current_tx().get_user_schema(),\n                ctx.compiler_state.std_schema,\n            ),\n        )\n    return config_spec\n\n\ndef _inject_config_cache_clear(sql_ast: pgast.Base) -> pgast.Base:\n    \"\"\"Inject a call to clear the config cache into a config op.\n\n    The trickiness here is that we can't just do the delete in a\n    statement before the config op, since RESET config ops query the\n    views and so might populate the cache, and we can't do it in a\n    statement directly after (unless we rework the server), since then\n    the query won't return anything.\n\n    So we instead fiddle around with the query to inject a call.\n    \"\"\"\n    assert isinstance(sql_ast, pgast.Query)\n    ctes = sql_ast.ctes or []\n    sql_ast.ctes = None\n\n    ctes.append(pgast.CommonTableExpr(\n        name=\"_conv_rel\",\n        query=sql_ast,\n    ))\n    clear_qry = pgast.SelectStmt(\n        target_list=[\n            pgast.ResTarget(\n                name=\"_dummy\",\n                val=pgast.FuncCall(\n                    name=('edgedb', '_clear_sys_config_cache'),\n                    args=[],\n                ),\n            ),\n        ],\n    )\n    ctes.append(pgast.CommonTableExpr(\n        name=\"_clear_cache\",\n        query=clear_qry,\n        materialized=True,\n    ))\n    force_qry = pgast.UpdateStmt(\n        targets=[pgast.UpdateTarget(\n            name='flag', val=pgast.BooleanConstant(val=True)\n        )],\n        relation=pgast.RelRangeVar(relation=pgast.Relation(\n            name='_dml_dummy')),\n        where_clause=pgast.Expr(\n            name=\"=\",\n            lexpr=pgast.ColumnRef(name=[\"id\"]),\n            rexpr=pgast.SelectStmt(\n                from_clause=[pgast.RelRangeVar(relation=ctes[-1])],\n                target_list=[\n                    pgast.ResTarget(\n                        val=pgast.FuncCall(\n                            name=('count',), args=[pgast.Star()]),\n                    )\n                ],\n            ),\n        )\n    )\n\n    if (\n        not isinstance(sql_ast, pgast.DMLQuery)\n        or sql_ast.returning_list\n    ):\n        ctes.append(pgast.CommonTableExpr(\n            name=\"_force_clear\",\n            query=force_qry,\n            materialized=True,\n        ))\n        sql_ast = pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(val=pgast.ColumnRef(\n                    name=[\"_conv_rel\", pgast.Star()])),\n            ],\n            ctes=ctes,\n            from_clause=[\n                pgast.RelRangeVar(relation=ctes[-3]),\n            ],\n        )\n    else:\n        sql_ast = force_qry\n        force_qry.ctes = ctes\n\n    return sql_ast\n\n\ndef _compile_ql_config_op(\n    ctx: CompileContext, ql: qlast.ConfigOp\n) -> dbstate.SessionStateQuery:\n\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n\n    session_config = current_tx.get_session_config()\n    database_config = current_tx.get_database_config()\n\n    if ql.scope is not qltypes.ConfigScope.SESSION:\n        ctx._assert_not_in_migration_block(ql)\n\n    if (\n        ql.scope is qltypes.ConfigScope.INSTANCE\n        and not current_tx.is_implicit()\n    ):\n        raise errors.QueryError(\n            'CONFIGURE INSTANCE cannot be executed in a transaction block')\n\n    options = _get_compile_options(ctx, no_implicit_fields=True)\n    options.in_server_config_op = True\n\n    ir = qlcompiler.compile_ast_to_ir(\n        ql,\n        schema=schema,\n        options=options,\n    )\n\n    globals = None\n    if ir.globals:\n        globals = [\n            (str(glob.global_name), glob.has_present_arg)\n            for glob in ir.globals\n            if not glob.is_permission\n        ]\n\n    if isinstance(ir, irast.Statement):\n        cfg_ir = ir.expr.expr\n    else:\n        cfg_ir = ir\n\n    is_backend_setting = bool(getattr(cfg_ir, 'backend_setting', None))\n    requires_restart = bool(getattr(cfg_ir, 'requires_restart', False))\n    is_system_config = bool(getattr(cfg_ir, 'is_system_config', False))\n\n    sql_res = pg_compiler.compile_ir_to_sql_tree(\n        ir,\n        backend_runtime_params=ctx.backend_runtime_params,\n    )\n\n    sql_ast = sql_res.ast\n    if not ctx.bootstrap_mode and ql.scope in (\n        qltypes.ConfigScope.DATABASE,\n        qltypes.ConfigScope.SESSION,\n    ):\n        sql_ast = _inject_config_cache_clear(sql_ast)\n\n    pretty = bool(\n        debug.flags.edgeql_compile or debug.flags.edgeql_compile_sql_text)\n    sql_text = pg_codegen.generate_source(\n        sql_ast,\n        pretty=pretty,\n    )\n    if pretty:\n        debug.dump_code(sql_text, lexer='sql')\n\n    sql = sql_text.encode()\n\n    in_type_args, in_type_data, in_type_id = describe_params(\n        ctx, ir, sql_res.argmap, None\n    )\n\n    if ql.scope is qltypes.ConfigScope.SESSION:\n        config_op = ireval.evaluate_to_config_op(ir, schema=schema)\n\n        session_config = config_op.apply(\n            _get_config_spec(ctx, config_op),\n            session_config,\n        )\n        current_tx.update_session_config(session_config)\n\n    elif ql.scope is qltypes.ConfigScope.DATABASE:\n        try:\n            config_op = ireval.evaluate_to_config_op(ir, schema=schema)\n        except ireval.UnsupportedExpressionError:\n            # This is a complex config object operation, the\n            # op will be produced by the compiler as json.\n            config_op = None\n        else:\n            database_config = config_op.apply(\n                _get_config_spec(ctx, config_op),\n                database_config,\n            )\n            current_tx.update_database_config(database_config)\n\n    elif ql.scope in (\n            qltypes.ConfigScope.INSTANCE, qltypes.ConfigScope.GLOBAL):\n        try:\n            config_op = ireval.evaluate_to_config_op(ir, schema=schema)\n        except ireval.UnsupportedExpressionError:\n            # This is a complex config object operation, the\n            # op will be produced by the compiler as json.\n            config_op = None\n    else:\n        raise AssertionError(f'unexpected configuration scope: {ql.scope}')\n\n    return dbstate.SessionStateQuery(\n        sql=sql,\n        is_backend_setting=is_backend_setting,\n        is_system_config=is_system_config,\n        config_scope=ql.scope,\n        requires_restart=requires_restart,\n        config_op=config_op,\n        globals=globals,\n        in_type_args=in_type_args,\n        in_type_data=in_type_data,\n        in_type_id=in_type_id.bytes,\n    )\n\n\ndef _compile_dispatch_ql(\n    ctx: CompileContext,\n    ql: qlast.Base,\n    source: Optional[edgeql.Source] = None,\n    *,\n    in_script: bool=False,\n    script_info: Optional[irast.ScriptInfo] = None,\n) -> tuple[dbstate.BaseQuery, enums.Capability]:\n    if isinstance(ql, qlast.MigrationCommand):\n        query = ddl.compile_dispatch_ql_migration(\n            ctx, ql, in_script=in_script\n        )\n        if isinstance(query, dbstate.MigrationControlQuery):\n            capability = enums.Capability.DDL\n            if query.tx_action:\n                capability |= enums.Capability.TRANSACTION\n            return query, capability\n        elif isinstance(query, dbstate.DDLQuery):\n            return query, enums.Capability.DDL\n        else:  # DESCRIBE CURRENT MIGRATION\n            return query, enums.Capability(0)\n\n    elif isinstance(ql, qlast.DDLCommand):\n        query = ddl.compile_and_apply_ddl_stmt(ctx, ql, source=source)\n        capability = enums.Capability.DDL\n        if isinstance(ql, (qlast.GlobalObjectCommand)):\n            capability |= enums.Capability.GLOBAL_DDL\n\n        return (query, capability)\n\n    elif isinstance(ql, qlast.Transaction):\n        return (\n            _compile_ql_transaction(ctx, ql),\n            enums.Capability.TRANSACTION,\n        )\n\n    elif isinstance(ql, qlast.SessionCommand):\n        return (\n            _compile_ql_sess_state(ctx, ql),\n            enums.Capability.SESSION_CONFIG,\n        )\n\n    elif isinstance(ql, qlast.ConfigOp):\n        if ql.scope is qltypes.ConfigScope.SESSION:\n            capability = enums.Capability.SESSION_CONFIG\n        elif ql.scope is qltypes.ConfigScope.GLOBAL:\n            # We want the notebook protocol to be able to SET\n            # GLOBAL but not CONFIGURE SESSION, but they are\n            # merged in the capabilities header. Splitting them\n            # out introduces compatability headaches, so for now\n            # we keep them merged and hack around it for the notebook.\n            if ctx.notebook:\n                capability = enums.Capability(0)\n            else:\n                capability = enums.Capability.SESSION_CONFIG\n        elif ql.scope is qltypes.ConfigScope.DATABASE:\n            capability = (\n                enums.Capability.PERSISTENT_CONFIG\n                | enums.Capability.BRANCH_CONFIG\n            )\n        else:\n            capability = (\n                enums.Capability.PERSISTENT_CONFIG\n                | enums.Capability.INSTANCE_CONFIG\n            )\n        return (\n            _compile_ql_config_op(ctx, ql),\n            capability,\n        )\n\n    elif isinstance(ql, qlast.ExplainStmt):\n        query = _compile_ql_explain(ctx, ql, script_info=script_info)\n        caps = enums.Capability.ANALYZE\n        if (\n            isinstance(query, (dbstate.Query, dbstate.SimpleQuery))\n            and query.has_dml\n        ):\n            caps |= enums.Capability.MODIFICATIONS\n        return (query, caps)\n\n    elif isinstance(ql, qlast.AdministerStmt):\n        query = _compile_ql_administer(ctx, ql, script_info=script_info)\n        caps = enums.Capability.ADMINISTER\n        return (query, caps)\n\n    else:\n        assert isinstance(ql, (qlast.Query, qlast.Command))\n        query = _compile_ql_query(\n            ctx, ql, source=source, script_info=script_info)\n        caps = enums.Capability(0)\n        if isinstance(ql, qlast.DescribeStmt):\n            caps |= enums.Capability.DESCRIBE\n        if (\n            isinstance(query, (dbstate.Query, dbstate.SimpleQuery))\n            and query.has_dml\n        ):\n            caps |= enums.Capability.MODIFICATIONS\n        return (query, caps)\n\n\ndef compile_graphql(\n    *,\n    ctx: CompileContext,\n    source: graphql.Source,\n    variables: Optional[Mapping[str, object]],\n) -> dbstate.QueryUnitGroup:\n    current_tx = ctx.state.current_tx()\n\n    gql_op = graphql.compile_graphql(\n        ctx.compiler_state.std_schema,\n        current_tx.get_user_schema(),\n        current_tx.get_global_schema(),\n        current_tx.get_database_config(),\n        current_tx.get_system_config(),\n        source.text(),\n        tokens=source.tokens(),\n        substitutions=source.substitutions(),\n        extracted_variables=source.variables(),\n        variables=variables,\n        native_input=True,\n    )\n    eql_source = edgeql.Source.from_string(\n        edgeql.generate_source(gql_op.edgeql_ast, pretty=True),\n    )\n\n    qug = compile(ctx=ctx, source=eql_source)\n    if gql_op.cache_deps_vars:\n        qug.graphql_key_variables = sorted(gql_op.cache_deps_vars)\n\n    # No warnings in graphql, yet\n    for qu in qug:\n        qu.warnings = ()\n    qug.warnings = None\n\n    return qug\n\n\ndef compile(\n    *,\n    ctx: CompileContext,\n    source: edgeql.Source,\n) -> dbstate.QueryUnitGroup:\n    current_tx = ctx.state.current_tx()\n    if current_tx.get_migration_state() is not None:\n        original = edgeql.Source.from_string(source.text())\n        ctx = dataclasses.replace(\n            ctx,\n            source=original,\n            implicit_limit=0,\n        )\n        return _try_compile(ctx=ctx, source=original)\n\n    try:\n        return _try_compile(ctx=ctx, source=source)\n    except errors.EdgeQLSyntaxError as original_err:\n        if isinstance(source, edgeql.NormalizedSource):\n            # try non-normalized source\n            try:\n                original = edgeql.Source.from_string(source.text())\n                ctx = dataclasses.replace(ctx, source=original)\n                _try_compile(ctx=ctx, source=original)\n            except errors.EdgeQLSyntaxError as denormalized_err:\n                raise denormalized_err\n            except Exception:\n                raise original_err\n            else:\n                raise AssertionError(\n                    \"Normalized query is broken while original is valid\")\n        else:\n            raise original_err\n\n\ndef compile_sql_as_unit_group(\n    *,\n    ctx: CompileContext,\n    source: edgeql.Source,\n) -> dbstate.QueryUnitGroup:\n\n    setting = _get_config_val(ctx, 'allow_user_specified_id')\n    allow_user_specified_id = None\n    if setting:\n        allow_user_specified_id = sql.is_setting_truthy(setting)\n\n    # Note that unlike SQL over PostgreSQL protocol we use\n    # the general access policy toggle, not the SQL-specific one.\n    apply_access_policies = None\n    setting = _get_config_val(ctx, 'apply_access_policies')\n    if setting is not None:\n        apply_access_policies = sql.is_setting_truthy(setting)\n\n    tx_state = ctx.state.current_tx()\n    schema = tx_state.get_schema(ctx.compiler_state.std_schema)\n\n    settings = dbstate.DEFAULT_SQL_FE_SETTINGS\n    sql_tx_state = dbstate.SQLTransactionState(\n        in_tx=not tx_state.is_implicit(),\n        settings=settings,\n        in_tx_settings=settings,\n        in_tx_local_settings=settings,\n        savepoints=[\n            (not_none(tx.name), settings, settings)\n            for tx in tx_state._savepoints.values()\n        ],\n    )\n\n    sql_units, force_non_normalized = sql.compile_sql(\n        source,\n        schema=schema,\n        tx_state=sql_tx_state,\n        prepared_stmt_map={},\n        current_database=ctx.branch_name or \"<unknown>\",\n        allow_user_specified_id=allow_user_specified_id,\n        apply_access_policies=apply_access_policies,\n        include_edgeql_io_format_alternative=True,\n        allow_prepared_statements=False,\n        disambiguate_column_names=True,\n        backend_runtime_params=ctx.backend_runtime_params,\n        protocol_version=ctx.protocol_version,\n        implicit_limit=ctx.implicit_limit,\n    )\n\n    qug = dbstate.QueryUnitGroup(\n        cardinality=sql_units[-1].cardinality,\n        cacheable=True,\n        force_non_normalized=force_non_normalized,\n    )\n\n    for sql_unit in sql_units:\n        if sql_unit.eql_format_query is not None:\n            value_sql = sql_unit.eql_format_query.encode(\"utf-8\")\n            intro_sql = sql_unit.query.encode(\"utf-8\")\n        else:\n            value_sql = sql_unit.query.encode(\"utf-8\")\n            intro_sql = None\n        if isinstance(sql_unit.command_complete_tag, dbstate.TagPlain):\n            status = sql_unit.command_complete_tag.tag\n        elif isinstance(\n            sql_unit.command_complete_tag,\n            (dbstate.TagCountMessages, dbstate.TagUnpackRow),\n        ):\n            status = sql_unit.command_complete_tag.prefix.encode(\"utf-8\")\n        elif sql_unit.command_complete_tag is None:\n            status = b\"SELECT\"  # XXX\n        else:\n            raise AssertionError(\n                f\"unexpected SQLQueryUnit.command_complete_tag type: \"\n                f\"{sql_unit.command_complete_tag}\"\n            )\n\n        globals = []\n        permissions = []\n        for sp in sql_unit.params or ():\n            if not isinstance(sp, dbstate.SQLParamGlobal):\n                continue\n\n            if not sp.is_permission:\n                globals.append((str(sp.global_name), False))\n            else:\n                permissions.append(str(sp.global_name))\n\n        unit = dbstate.QueryUnit(\n            sql=value_sql,\n            introspection_sql=intro_sql,\n            status=status,\n            cardinality=(\n                enums.Cardinality.NO_RESULT\n                if ctx.output_format is enums.OutputFormat.NONE\n                else sql_unit.cardinality\n            ),\n            capabilities=sql_unit.capabilities,\n            globals=globals,\n            permissions=permissions,\n            output_format=(\n                enums.OutputFormat.NONE\n                if (\n                    ctx.output_format is enums.OutputFormat.NONE\n                    or sql_unit.cardinality is enums.Cardinality.NO_RESULT\n                )\n                else enums.OutputFormat.BINARY\n            ),\n            source_map=sql_unit.source_map,\n            sql_prefix_len=sql_unit.prefix_len,\n        )\n        match sql_unit.tx_action:\n            case dbstate.TxAction.START:\n                ctx.state.start_tx()\n                tx_state = ctx.state.current_tx()\n                unit.tx_id = tx_state.id\n            case dbstate.TxAction.COMMIT:\n                ctx.state.commit_tx()\n                unit.tx_commit = True\n            case dbstate.TxAction.ROLLBACK:\n                ctx.state.rollback_tx()\n                unit.tx_rollback = True\n            case dbstate.TxAction.DECLARE_SAVEPOINT:\n                assert sql_unit.sp_name is not None\n                unit.tx_savepoint_declare = True\n                unit.sp_id = tx_state.declare_savepoint(sql_unit.sp_name)\n                unit.sp_name = sql_unit.sp_name\n            case dbstate.TxAction.ROLLBACK_TO_SAVEPOINT:\n                assert sql_unit.sp_name is not None\n                tx_state.rollback_to_savepoint(sql_unit.sp_name)\n                unit.tx_savepoint_rollback = True\n                unit.sp_name = sql_unit.sp_name\n            case dbstate.TxAction.RELEASE_SAVEPOINT:\n                assert sql_unit.sp_name is not None\n                tx_state.release_savepoint(sql_unit.sp_name)\n                unit.sp_name = sql_unit.sp_name\n            case None:\n                unit.cacheable = sql_unit.cacheable\n            case _:\n                raise AssertionError(\n                    f\"unexpected SQLQueryUnit.tx_action: {sql_unit.tx_action}\"\n                )\n\n        qug.append(unit)\n\n    return qug\n\n\ndef _try_compile(\n    *,\n    ctx: CompileContext,\n    source: edgeql.Source,\n) -> dbstate.QueryUnitGroup:\n    if ctx.is_testmode():\n        # This is a bad but simple way to emulate a slow compilation for tests.\n        # Ideally, we should have a testmode function that is hooked to sleep\n        # as `simple_special_case`, or wait for a notification from the test.\n        sentinel = \"# EDGEDB_TEST_COMPILER_SLEEP = \"\n        text = source.text()\n        if text.startswith(sentinel):\n            time.sleep(float(text[len(sentinel):text.index(\"\\n\")]))\n\n    statements = edgeql.parse_block(source)\n    return _try_compile_ast(statements=statements, source=source, ctx=ctx)\n\n\ndef _try_compile_ast(\n    *,\n    ctx: CompileContext,\n    statements: Sequence[qlast.Base],\n    source: edgeql.Source,\n) -> dbstate.QueryUnitGroup:\n    if ctx.is_testmode():\n        # This is a bad but simple way to emulate a slow compilation for tests.\n        # Ideally, we should have a testmode function that is hooked to sleep\n        # as `simple_special_case`, or wait for a notification from the test.\n        sentinel = \"# EDGEDB_TEST_COMPILER_SLEEP = \"\n        text = source.text()\n        if text.startswith(sentinel):\n            time.sleep(float(text[len(sentinel):text.index(\"\\n\")]))\n\n    statements_len = len(statements)\n\n    if not len(statements):  # pragma: no cover\n        raise errors.ProtocolError('nothing to compile')\n\n    rv = dbstate.QueryUnitGroup()\n\n    is_script = statements_len > 1\n    script_info = None\n    if is_script:\n        if ctx.expect_rollback:\n            # We are in a failed transaction expecting a rollback, while a\n            # script cannot be a rollback\n            raise errors.TransactionError(\n                'expected a ROLLBACK or ROLLBACK TO SAVEPOINT command'\n            )\n\n        script_info = qlcompiler.preprocess_script(\n            statements,\n            schema=ctx.state.current_tx().get_schema(\n                ctx.compiler_state.std_schema),\n            options=_get_compile_options(ctx)\n        )\n        non_trailing_ctx = dataclasses.replace(\n            ctx, output_format=enums.OutputFormat.NONE)\n\n    final_user_schema: Optional[s_schema.Schema] = None\n\n    for i, stmt in enumerate(statements):\n        is_trailing_stmt = i == statements_len - 1\n        stmt_ctx = ctx if is_trailing_stmt else non_trailing_ctx\n\n        _check_force_database_error(stmt_ctx, stmt)\n\n        comp, capabilities = _compile_dispatch_ql(\n            stmt_ctx,\n            stmt,\n            source=source if not is_script else None,\n            script_info=script_info,\n            in_script=is_script,\n        )\n\n        unit, user_schema = _make_query_unit(\n            ctx=ctx,\n            stmt_ctx=stmt_ctx,\n            stmt=stmt,\n            is_script=is_script,\n            is_trailing_stmt=is_trailing_stmt,\n            comp=comp,\n            capabilities=capabilities,\n        )\n\n        rv.append(unit)\n\n        if user_schema is not None:\n            final_user_schema = user_schema\n\n    if script_info:\n        if ctx.state.current_tx().is_implicit():\n            if ctx.state.current_tx().get_migration_state():\n                raise errors.QueryError(\n                    \"Cannot leave an incomplete migration in scripts\"\n                )\n            if ctx.state.current_tx().get_migration_rewrite_state():\n                raise errors.QueryError(\n                    \"Cannot leave an incomplete migration rewrite \"\n                    \"in scripts\"\n                )\n\n        params, in_type_args = _extract_params(\n            list(script_info.params.values()),\n            argmap=None, script_info=None, schema=script_info.schema,\n            ctx=ctx)\n\n        in_type_data, in_type_id = sertypes.describe_params(\n            schema=script_info.schema,\n            params=params,\n            protocol_version=ctx.protocol_version,\n        )\n        rv.in_type_id = in_type_id.bytes\n        rv.in_type_args = in_type_args\n        rv.in_type_data = in_type_data\n\n    if final_user_schema is not None:\n        rv.state_serializer = ctx.compiler_state.state_serializer_factory.make(\n            final_user_schema,\n            ctx.state.current_tx().get_global_schema(),\n            ctx.protocol_version,\n        )\n\n    # Sanity checks\n    for unit in rv:  # pragma: no cover\n        na_cardinality = (\n            unit.cardinality is enums.Cardinality.NO_RESULT\n        )\n        if unit.cacheable and (\n            unit.config_ops or unit.modaliases or unit.user_schema or\n            unit.cached_reflection\n        ):\n            raise errors.InternalServerError(\n                f'QueryUnit {unit!r} is cacheable but has config/aliases')\n\n        if not na_cardinality and (\n                unit.tx_commit or\n                unit.tx_rollback or\n                unit.tx_savepoint_rollback or\n                unit.out_type_id is sertypes.NULL_TYPE_ID or\n                unit.system_config or\n                unit.config_ops or\n                unit.modaliases or\n                unit.has_set or\n                unit.has_ddl or\n                not unit.sql_hash):\n            raise errors.InternalServerError(\n                f'unit has invalid \"cardinality\": {unit!r}')\n\n    multi_card = rv.cardinality in (\n        enums.Cardinality.MANY, enums.Cardinality.AT_LEAST_ONE,\n    )\n    if multi_card and ctx.expected_cardinality_one:\n        raise errors.ResultCardinalityMismatchError(\n            f'the query has cardinality {unit.cardinality.name} '\n            f'which does not match the expected cardinality ONE')\n\n    return rv\n\n\ndef _make_query_unit(\n    *,\n    ctx: CompileContext,\n    stmt_ctx: CompileContext,\n    stmt: qlast.Base,\n    is_script: bool,\n    is_trailing_stmt: bool,\n    comp: dbstate.BaseQuery,\n    capabilities: enums.Capability,\n) -> tuple[dbstate.QueryUnit, Optional[s_schema.Schema]]:\n\n    # Initialize user_schema_version with the version this query is\n    # going to be compiled upon. This can be overwritten later by DDLs.\n    try:\n        schema_version = _get_schema_version(\n            stmt_ctx.state.current_tx().get_user_schema()\n        )\n    except errors.InvalidReferenceError:\n        schema_version = None\n\n    unit = dbstate.QueryUnit(\n        sql=b\"\",\n        status=status.get_status(stmt),\n        cardinality=enums.Cardinality.NO_RESULT,\n        capabilities=capabilities,\n        output_format=stmt_ctx.output_format,\n        cache_key=ctx.cache_key,\n        user_schema_version=schema_version,\n        warnings=comp.warnings,\n        unsafe_isolation_dangers=comp.unsafe_isolation_dangers,\n    )\n\n    if not comp.is_transactional:\n        if is_script:\n            raise errors.QueryError(\n                f'cannot execute {status.get_status(stmt).decode()} '\n                f'with other commands in one block',\n                span=stmt.span,\n            )\n\n        if not ctx.state.current_tx().is_implicit():\n            raise errors.QueryError(\n                f'cannot execute {status.get_status(stmt).decode()} '\n                f'in a transaction',\n                span=stmt.span,\n            )\n\n        unit.is_transactional = False\n\n    final_user_schema: Optional[s_schema.Schema] = None\n\n    if isinstance(comp, dbstate.Query):\n        unit.sql = comp.sql\n        unit.cache_sql = comp.cache_sql\n        unit.cache_func_call = comp.cache_func_call\n        unit.globals = comp.globals\n        unit.permissions = comp.permissions\n        unit.json_permissions = comp.json_permissions\n        unit.required_permissions = comp.required_permissions\n        unit.in_type_args = comp.in_type_args\n\n        unit.sql_hash = comp.sql_hash\n\n        unit.out_type_data = comp.out_type_data\n        unit.out_type_id = comp.out_type_id\n        unit.in_type_data = comp.in_type_data\n        unit.in_type_id = comp.in_type_id\n\n        unit.server_param_conversions = comp.server_param_conversions\n\n        unit.cacheable = comp.cacheable\n\n        if comp.is_explain:\n            unit.is_explain = True\n            unit.query_asts = comp.query_asts\n\n        if comp.run_and_rollback:\n            unit.run_and_rollback = True\n\n        if is_trailing_stmt:\n            unit.cardinality = comp.cardinality\n\n    elif isinstance(comp, dbstate.SimpleQuery):\n        unit.sql = comp.sql\n        unit.in_type_args = comp.in_type_args\n\n    elif isinstance(comp, dbstate.DDLQuery):\n        unit.sql = comp.sql\n        unit.db_op_trailer = comp.db_op_trailer\n        unit.create_db = comp.create_db\n        unit.drop_db = comp.drop_db\n        unit.drop_db_reset_connections = comp.drop_db_reset_connections\n        unit.create_db_template = comp.create_db_template\n        unit.create_db_mode = comp.create_db_mode\n        unit.ddl_stmt_id = comp.ddl_stmt_id\n        unit.early_non_tx_sql = comp.early_non_tx_sql\n        if not ctx.dump_restore_mode:\n            if comp.user_schema is not None:\n                final_user_schema = comp.user_schema\n                unit.user_schema = pickle.dumps(comp.user_schema, -1)\n                unit.user_schema_version = (\n                    _get_schema_version(comp.user_schema)\n                )\n                unit.extensions, unit.ext_config_settings = (\n                    _extract_extensions(ctx, comp.user_schema)\n                )\n            unit.feature_used_metrics = comp.feature_used_metrics\n            if comp.cached_reflection is not None:\n                unit.cached_reflection = \\\n                    pickle.dumps(comp.cached_reflection, -1)\n            if comp.global_schema is not None:\n                unit.global_schema = pickle.dumps(comp.global_schema, -1)\n                unit.roles = _extract_roles(comp.global_schema)\n\n        unit.config_ops.extend(comp.config_ops)\n\n    elif isinstance(comp, dbstate.TxControlQuery):\n        if is_script:\n            raise errors.QueryError(\n                \"Explicit transaction control commands cannot be executed \"\n                \"in an implicit transaction block\"\n            )\n        unit.sql = comp.sql\n        unit.cacheable = comp.cacheable\n        unit.tx_isolation_level = comp.isolation_level\n\n        if not ctx.dump_restore_mode:\n            if comp.user_schema is not None:\n                final_user_schema = comp.user_schema\n                unit.user_schema = pickle.dumps(comp.user_schema, -1)\n                unit.user_schema_version = (\n                    _get_schema_version(comp.user_schema)\n                )\n                unit.extensions, unit.ext_config_settings = (\n                    _extract_extensions(ctx, comp.user_schema)\n                )\n            unit.feature_used_metrics = comp.feature_used_metrics\n            if comp.cached_reflection is not None:\n                unit.cached_reflection = \\\n                    pickle.dumps(comp.cached_reflection, -1)\n            if comp.global_schema is not None:\n                unit.global_schema = pickle.dumps(comp.global_schema, -1)\n                unit.roles = _extract_roles(comp.global_schema)\n\n        if comp.modaliases is not None:\n            unit.modaliases = comp.modaliases\n\n        if comp.action == dbstate.TxAction.START:\n            if unit.tx_id is not None:\n                raise errors.InternalServerError(\n                    'already in transaction')\n            unit.tx_id = ctx.state.current_tx().id\n        elif comp.action == dbstate.TxAction.COMMIT:\n            unit.tx_commit = True\n        elif comp.action == dbstate.TxAction.ROLLBACK:\n            unit.tx_rollback = True\n        elif comp.action is dbstate.TxAction.ROLLBACK_TO_SAVEPOINT:\n            unit.tx_savepoint_rollback = True\n            unit.sp_name = comp.sp_name\n        elif comp.action is dbstate.TxAction.DECLARE_SAVEPOINT:\n            unit.tx_savepoint_declare = True\n            unit.sp_name = comp.sp_name\n            unit.sp_id = comp.sp_id\n\n    elif isinstance(comp, dbstate.MigrationControlQuery):\n        unit.sql = comp.sql\n        unit.cacheable = comp.cacheable\n\n        if not ctx.dump_restore_mode:\n            if comp.user_schema is not None:\n                final_user_schema = comp.user_schema\n                unit.user_schema = pickle.dumps(comp.user_schema, -1)\n                unit.user_schema_version = (\n                    _get_schema_version(comp.user_schema)\n                )\n                unit.extensions, unit.ext_config_settings = (\n                    _extract_extensions(ctx, comp.user_schema)\n                )\n            if comp.cached_reflection is not None:\n                unit.cached_reflection = \\\n                    pickle.dumps(comp.cached_reflection, -1)\n        unit.ddl_stmt_id = comp.ddl_stmt_id\n\n        if comp.modaliases is not None:\n            unit.modaliases = comp.modaliases\n\n        if comp.tx_action == dbstate.TxAction.START:\n            if unit.tx_id is not None:\n                raise errors.InternalServerError(\n                    'already in transaction')\n            unit.tx_id = ctx.state.current_tx().id\n        elif comp.tx_action == dbstate.TxAction.COMMIT:\n            unit.tx_commit = True\n            unit.append_tx_op = True\n        elif comp.tx_action == dbstate.TxAction.ROLLBACK:\n            unit.tx_rollback = True\n            unit.append_tx_op = True\n        elif comp.action == dbstate.MigrationAction.ABORT:\n            unit.tx_abort_migration = True\n\n    elif isinstance(comp, dbstate.SessionStateQuery):\n        unit.sql = comp.sql\n        unit.globals = comp.globals\n\n        if comp.config_scope is qltypes.ConfigScope.INSTANCE:\n            if not ctx.state.current_tx().is_implicit() or is_script:\n                raise errors.QueryError(\n                    'CONFIGURE INSTANCE cannot be executed in a '\n                    'transaction block')\n\n            unit.system_config = True\n        elif comp.config_scope is qltypes.ConfigScope.GLOBAL:\n            unit.needs_readback = True\n\n        elif comp.config_scope is qltypes.ConfigScope.DATABASE:\n            unit.database_config = True\n            unit.needs_readback = True\n\n        if comp.is_backend_setting:\n            unit.backend_config = True\n        if comp.requires_restart:\n            unit.config_requires_restart = True\n        if comp.is_system_config:\n            unit.is_system_config = True\n\n        unit.modaliases = ctx.state.current_tx().get_modaliases()\n\n        if comp.config_op is not None:\n            unit.config_ops.append(comp.config_op)\n\n        if comp.in_type_args:\n            unit.in_type_args = comp.in_type_args\n        if comp.in_type_data:\n            unit.in_type_data = comp.in_type_data\n        if comp.in_type_id:\n            unit.in_type_id = comp.in_type_id\n\n        unit.has_set = True\n        unit.output_format = enums.OutputFormat.NONE\n\n    elif isinstance(comp, dbstate.MaintenanceQuery):\n        unit.sql = comp.sql\n\n    elif isinstance(comp, dbstate.NullQuery):\n        pass\n\n    else:  # pragma: no cover\n        raise errors.InternalServerError('unknown compile state')\n\n    if unit.in_type_args:\n        unit.in_type_args_real_count = sum(\n            len(p.sub_params[0]) if p.sub_params else 1\n            for p in unit.in_type_args\n        )\n\n    if unit.warnings:\n        for warning in unit.warnings:\n            warning.__traceback__ = None\n    if unit.unsafe_isolation_dangers:\n        for warning in unit.unsafe_isolation_dangers:\n            warning.__traceback__ = None\n\n    return unit, final_user_schema\n\n\ndef _extract_params(\n    params: list[irast.Param],\n    *,\n    schema: s_schema.Schema,\n    argmap: Optional[dict[str, pgast.Param]],\n    script_info: Optional[irast.ScriptInfo],\n    ctx: CompileContext,\n) -> tuple[list[tuple[str, s_types.Type, bool]], list[dbstate.Param]]:\n    first_param = next(iter(params)) if params else None\n    has_named_params = first_param and not first_param.name.isdecimal()\n\n    if (src := ctx.source) is not None:\n        first_extra = src.first_extra()\n    else:\n        first_extra = None\n\n    all_params = script_info.params.values() if script_info else params\n    total_params = len([p for p in all_params if not p.is_sub_param])\n    user_params = first_extra if first_extra is not None else total_params\n\n    if script_info is not None:\n        outer_mapping = {n: i for i, n in enumerate(script_info.params)}\n        # Count however many of *our* arguments are user_params\n        user_params = sum(\n            outer_mapping[n.name] < user_params for n in params\n            if not n.is_sub_param)\n    else:\n        outer_mapping = None\n\n    oparams: list[Optional[tuple[str, s_obj.Object, bool]]] = (\n        [None] * user_params)\n    in_type_args: list[Optional[dbstate.Param]] = [None] * user_params\n    for idx, param in enumerate(params):\n        if param.is_sub_param:\n            continue\n        if argmap is not None:\n            sql_param = argmap[param.name]\n            idx = sql_param.logical_index - 1\n        if idx >= user_params:\n            continue\n\n        if ctx.json_parameters:\n            schema_type = schema.get('std::json')\n        else:\n            schema_type = param.schema_type\n\n        array_tid = None\n        if isinstance(schema_type, s_types.Array):\n            el_type = schema_type.get_element_type(schema)\n            array_tid = el_type.id\n\n        # NB: We'll need to turn this off for script args\n        if (\n            not script_info\n            and not has_named_params\n            and str(idx) != param.name\n        ):\n            raise RuntimeError(\n                'positional argument name disagrees '\n                'with its actual position')\n\n        oparams[idx] = (\n            param.name,\n            schema_type,\n            param.required,\n        )\n\n        if param.sub_params:\n            array_tids: list[Optional[uuid.UUID]] = []\n            for p in param.sub_params.params:\n                if isinstance(p.schema_type, s_types.Array):\n                    el_type = p.schema_type.get_element_type(schema)\n                    array_tids.append(el_type.id)\n                else:\n                    array_tids.append(None)\n\n            sub_params = (\n                array_tids, param.sub_params.trans_type.flatten())\n        else:\n            sub_params = None\n\n        in_type_args[idx] = dbstate.Param(\n            name=param.name,\n            required=param.required,\n            array_type_id=array_tid,\n            outer_idx=outer_mapping[param.name] if outer_mapping else None,\n            sub_params=sub_params,\n            typename=str(schema_type.get_name(schema)),\n        )\n\n    return oparams, in_type_args  # type: ignore[return-value]\n\n\ndef get_obj_ids(\n    schema: s_schema.Schema,\n    *,\n    include_extras: bool=False,\n) -> tuple[list[tuple[str, str, uuid.UUID]], list[uuid.UUID]]:\n    all_objects: Iterable[s_obj.Object] = schema.get_objects(\n        exclude_stdlib=True,\n        exclude_global=True,\n    )\n    ids = []\n    sequences = []\n    for obj in all_objects:\n        if isinstance(obj, s_obj.QualifiedObject):\n            ql_class = ''\n        else:\n            ql_class = str(type(obj).get_ql_class_or_die())\n\n        name = str(obj.get_name(schema))\n        ids.append((\n            name,\n            ql_class,\n            obj.id,\n        ))\n\n        if isinstance(obj, s_types.Type) and obj.is_sequence(schema):\n            sequences.append(obj.id)\n\n        if include_extras and isinstance(obj, s_func.Function):\n            backend_name = obj.get_backend_name(schema)\n            if backend_name:\n                ids.append((\n                    name,\n                    f'{ql_class or None}-backend_name',\n                    backend_name,\n                ))\n\n    return ids, sequences\n\n\ndef _describe_object(\n    schema: s_schema.Schema,\n    source: s_obj.Object,\n    protocol_version: defines.ProtocolVersion,\n) -> list[DumpBlockDescriptor]:\n\n    cols = []\n    shape = []\n    ptrdesc: list[DumpBlockDescriptor] = []\n\n    if isinstance(source, s_props.Property):\n        schema, prop_tuple = s_types.Tuple.from_subtypes(\n            schema,\n            {\n                'source': schema.get('std::uuid', type=s_types.Type),\n                'target': not_none(source.get_target(schema)),\n            },\n            {'named': True},\n        )\n\n        type_data, type_id = sertypes.describe(\n            schema,\n            prop_tuple,\n            follow_links=False,\n            protocol_version=protocol_version,\n        )\n\n        cols.extend([\n            'source',\n            'target',\n        ])\n\n    elif isinstance(source, s_links.Link):\n        props = {}\n\n        for ptr in source.get_pointers(schema).objects(schema):\n            if not ptr.is_dumpable(schema):\n                continue\n\n            stor_info = pg_types.get_pointer_storage_info(\n                ptr,\n                schema=schema,\n                source=source,\n                link_bias=True,\n            )\n\n            cols.append(stor_info.column_name)\n\n            props[ptr.get_shortname(schema).name] = not_none(\n                ptr.get_target(schema))\n\n        schema, link_tuple = s_types.Tuple.from_subtypes(\n            schema,\n            props,\n            {'named': True},\n        )\n\n        type_data, type_id = sertypes.describe(\n            schema,\n            link_tuple,\n            follow_links=False,\n            protocol_version=protocol_version,\n        )\n\n    else:\n        assert isinstance(source, s_objtypes.ObjectType)\n        for ptr in source.get_pointers(schema).objects(schema):\n            if not ptr.is_dumpable(schema):\n                continue\n\n            stor_info = pg_types.get_pointer_storage_info(\n                ptr,\n                schema=schema,\n                source=source,\n            )\n\n            if stor_info.table_type == 'ObjectType':\n                cols.append(stor_info.column_name)\n                shape.append(ptr)\n\n            link_stor_info = pg_types.get_pointer_storage_info(\n                ptr,\n                schema=schema,\n                source=source,\n                link_bias=True,\n            )\n\n            if link_stor_info is not None:\n                ptrdesc.extend(_describe_object(schema, ptr,\n                                                protocol_version))\n\n        # For any addon columns (currently fts and ai shadow index\n        # columns), generate a fake pointer to put in the descriptor\n        # and include them in the dump.\n        nschema = schema\n        for (name, col, _type) in source.get_addon_columns(schema):\n            nschema, fake_ptr = _add_fake_property(source, name, nschema)\n            cols.append(col)\n            shape.append(fake_ptr)\n\n        type_data, type_id = sertypes.describe(\n            nschema,\n            source,\n            view_shapes={source: shape},\n            follow_links=False,\n            protocol_version=protocol_version,\n        )\n\n    table_name = pg_common.get_backend_name(\n        schema, source, catenate=True\n    )\n\n    stmt = (\n        f'COPY {table_name} '\n        f'({\", \".join(pg_common.quote_ident(c) for c in cols)}) '\n        f'TO STDOUT WITH BINARY'\n    ).encode()\n\n    return [DumpBlockDescriptor(\n        schema_object_id=source.id,\n        schema_object_class=type(source).get_ql_class_or_die(),\n        schema_deps=tuple(p.schema_object_id for p in ptrdesc),\n        type_desc_id=type_id,\n        type_desc=type_data,\n        sql_copy_stmt=stmt,\n    )] + ptrdesc\n\n\ndef _check_dump_layout(\n    dump_els: AbstractSet[str],\n    schema_els: AbstractSet[str],\n    elided_els: AbstractSet[str],\n    label: str,\n) -> None:\n    extra_els = dump_els - (schema_els | elided_els)\n    if extra_els:\n        raise RuntimeError(\n            f'dump data tuple of {label} has extraneous elements: '\n            f'{\", \".join(extra_els)}'\n        )\n\n    missing_els = schema_els - dump_els\n    if missing_els:\n        raise RuntimeError(\n            f'dump data tuple of {label} has missing elements: '\n            f'{\", \".join(missing_els)}'\n        )\n\n\ndef _get_ptr_mending_desc(\n    schema: s_schema.Schema,\n    ptr: s_pointers.Pointer,\n) -> Optional[DataMendingDescriptor]:\n    ptr_type = ptr.get_target(schema)\n    if isinstance(ptr_type, (s_types.Array, s_types.Tuple)):\n        return _get_data_mending_desc(schema, ptr_type)\n    else:\n        return None\n\n\ndef _get_data_mending_desc(\n    schema: s_schema.Schema,\n    typ: s_types.Type,\n) -> Optional[DataMendingDescriptor]:\n    if isinstance(typ, (s_types.Tuple, s_types.Array)):\n        elements = tuple(\n            _get_data_mending_desc(schema, element)\n            for element in typ.get_subtypes(schema)\n        )\n    else:\n        elements = tuple()\n\n    if pg_types.type_has_stable_oid(typ):\n        return None\n    else:\n        return DataMendingDescriptor(\n            schema_type_id=typ.id,\n            schema_object_class=type(typ).get_ql_class_or_die(),\n            elements=elements,\n            needs_mending=bool(\n                isinstance(typ, (s_types.Tuple, s_types.Array))\n                and any(elements)\n            )\n        )\n\n\ndef _add_fake_property(\n    source: s_objtypes.ObjectType,\n    name: str,\n    schema: s_schema.Schema,\n) -> tuple[s_schema.Schema, s_props.Property]:\n    base = schema.get(\n        s_name.QualName('std', 'property'),\n        type=s_props.Property,\n    )\n    derived_name = s_obj.derive_name(\n        schema,\n        str(source.get_name(schema)),\n        module='__derived__',\n        derived_name_base=s_name.UnqualName(name),\n        parent=base,\n    )\n    return base.derive_ref(\n        schema,\n        source,\n        name=derived_name,\n        target=schema.get('std::bytes', type=s_types.Type),\n    )\n\n\ndef maybe_force_database_error(\n    val: Optional[str],\n    *,\n    scope: str,\n) -> None:\n    # Check the string directly for false to skip a deserialization\n    if val is None or val == 'false':\n        return\n    try:\n        err = json.loads(val)\n        if not err:\n            return\n\n        scopes = err.get('_scopes', ['query'])\n        if scope not in scopes:\n            return\n        versions = err.get('_versions')\n        if versions and buildmeta.get_version_string() not in versions:\n            return\n\n        errcls = errors.EdgeDBError.get_error_class_from_name(err['type'])\n        if context := err.get('context'):\n            filename = context.get('filename')\n            position = tuple(\n                context.get(k) for k in ('line', 'col', 'start', 'end')\n            )\n        else:\n            filename = None\n            position = None\n\n        errval = errcls(\n            msg=err.get('message'),\n            hint=err.get('hint'),\n            details=err.get('details'),\n            filename=filename,\n            position=position,\n        )\n    except Exception:\n        raise errors.ConfigurationError(\n            \"invalid 'force_database_error' value'\")\n\n    raise errval\n\n\ndef _check_force_database_error(\n    ctx: CompileContext,\n    ql: Optional[qlast.Base]=None,\n    *,\n    scope: str='query',\n) -> None:\n    if isinstance(ql, qlast.ConfigOp):\n        return\n\n    val = _get_config_val(ctx, 'force_database_error')\n    if isinstance(ql, qlast.DDLCommand):\n        maybe_force_database_error(val, scope='ddl')\n    maybe_force_database_error(val, scope=scope)\n\n\ndef _get_config_val(\n    ctx: CompileContext,\n    name: str,\n) -> Any:\n    current_tx = ctx.state.current_tx()\n    return config.lookup(\n        name,\n        current_tx.get_session_config(),\n        current_tx.get_database_config(),\n        current_tx.get_system_config(),\n        spec=ctx.compiler_state.config_spec,\n        allow_unrecognized=True,\n    )\n\n\ndef _get_compilation_config_vals(ctx: CompileContext) -> Any:\n    assert ctx.compiler_state.config_spec is not None\n    return {\n        k: _get_config_val(ctx, k)\n        for k in ctx.compiler_state.config_spec\n        if ctx.compiler_state.config_spec[k].affects_compilation\n    }\n\n\n_OUTPUT_FORMAT_MAP = {\n    enums.OutputFormat.BINARY: pg_compiler.OutputFormat.NATIVE,\n    enums.OutputFormat.JSON: pg_compiler.OutputFormat.JSON,\n    enums.OutputFormat.JSON_ELEMENTS: pg_compiler.OutputFormat.JSON_ELEMENTS,\n    enums.OutputFormat.NONE: pg_compiler.OutputFormat.NONE,\n}\n\n\ndef _convert_format(inp: enums.OutputFormat) -> pg_compiler.OutputFormat:\n    try:\n        return _OUTPUT_FORMAT_MAP[inp]\n    except KeyError:\n        raise RuntimeError(f\"Output format {inp!r} is not supported\")\n\n\ndef _hash_sql(sql: bytes, **kwargs: bytes) -> bytes:\n    h = hashlib.sha1(sql)\n    for param, val in kwargs.items():\n        h.update(param.encode('latin1'))\n        h.update(val)\n    return h.hexdigest().encode('latin1')\n\n\ndef _extract_extensions(\n    ctx: CompileContext, user_schema: s_schema.Schema\n) -> tuple[set[str], list[config.Setting]]:\n    # XXX: Do we need to return None if extensions/config_spec didn't change?\n    names = {\n        ext.get_name(user_schema).name\n        for ext in user_schema.get_objects(type=s_ext.Extension)\n    }\n    if names:\n        schema = s_schema.ChainedSchema(\n            ctx.compiler_state.std_schema, user_schema, s_schema.EMPTY_SCHEMA\n        )\n        settings = config.load_ext_settings_from_schema(schema)\n    else:\n        settings = []\n    return names, settings\n\n\ndef _extract_roles(\n    global_schema: s_schema.Schema,\n) -> immutables.Map[str, immutables.Map[str, Any]]:\n    extracted_roles = {}\n    schema_roles = global_schema.get_objects(type=s_role.Role)\n    for role in schema_roles:\n        role_name = str(role.get_name(global_schema))\n        extracted_roles[role_name] = dict(\n            name=role_name,\n            superuser=role.get_superuser(global_schema),\n            password=role.get_password(global_schema),\n            branches=list(sorted(role.get_branches(global_schema))),\n            apply_access_policies_pg_default=(\n                role.get_apply_access_policies_pg_default(global_schema)\n            ),\n        )\n\n    # To populate all_permissions, combine the permissions of each role\n    # and its ancestors.\n    role_memberships: MutableMapping[s_role.Role, list[s_role.Role]] = {}\n    role_permissions: MutableMapping[s_role.Role, Sequence[str]] = {}\n    for role in schema_roles:\n        role_memberships[role] = list(\n            role.get_ancestors(global_schema).objects(global_schema)\n        )\n        role_permissions[role] = list(sorted(\n            role.get_permissions(global_schema) or ()\n        ))\n\n    for role in schema_roles:\n        role_name = str(role.get_name(global_schema))\n        extracted_roles[role_name]['all_permissions'] = tuple(set(\n            p\n            for m in [role] + role_memberships.get(role, [])\n            for p in role_permissions[m]\n        ))\n\n    # Convert everything into immutable maps\n    return immutables.Map({\n        name: immutables.Map(role)\n        for name, role in extracted_roles.items()\n    })\n\n\nclass DumpDescriptor(NamedTuple):\n\n    schema_ddl: str\n    schema_dynamic_ddl: tuple[str, ...]\n    schema_ids: list[tuple[str, str, bytes]]\n    blocks: Sequence[DumpBlockDescriptor]\n\n\nclass DumpBlockDescriptor(NamedTuple):\n\n    schema_object_id: uuid.UUID\n    schema_object_class: qltypes.SchemaObjectClass\n    schema_deps: tuple[uuid.UUID, ...]\n    type_desc_id: uuid.UUID\n    type_desc: bytes\n    sql_copy_stmt: bytes\n\n\nclass RestoreDescriptor(NamedTuple):\n\n    units: Sequence[dbstate.QueryUnit]\n    blocks: Sequence[RestoreBlockDescriptor]\n    tables: Sequence[str]\n    repopulate_units: Sequence[str]\n\n\nclass DataMendingDescriptor(NamedTuple):\n\n    #: The identifier of the EdgeDB type\n    schema_type_id: uuid.UUID\n    #: The kind of a type we are dealing with\n    schema_object_class: qltypes.SchemaObjectClass\n    #: If type is a collection, mending descriptors of element types\n    elements: tuple[Optional[DataMendingDescriptor], ...] = tuple()\n    #: Whether a datum represented by this descriptor will need mending\n    needs_mending: bool = False\n\n\nclass RestoreBlockDescriptor(NamedTuple):\n\n    #: The identifier of the schema object this data is for.\n    schema_object_id: uuid.UUID\n    #: The COPY SQL statement for this block.\n    sql_copy_stmt: bytes\n    #: For compatibility with old dumps, a list of column indexes\n    #: that should be ignored in the COPY stream.\n    compat_elided_cols: tuple[int, ...]\n    #: If the tuple requires mending of unstable Postgres OIDs in data,\n    #: this will contain the recursive descriptor on which parts of\n    #: each datum need mending.\n    data_mending_desc: tuple[Optional[DataMendingDescriptor], ...]\n"
  },
  {
    "path": "edb/server/compiler/config.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Iterable, Mapping, Sequence\n\nimport dataclasses\nimport datetime\nimport functools\n\nimport immutables\n\nfrom edb import errors\nfrom edb.common import typeutils\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import parser as qlparser\nfrom edb.edgeql import qltypes\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.ir import ast as irast\nfrom edb.ir import staeval as ireval\nfrom edb.server import config\nfrom edb.schema import name as sn\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import schema as s_schema\nfrom edb.schema import types as s_types\nfrom edb.schema import utils as s_utils\n\nConfigInput = (\n    str\n    | int\n    | float\n    | bool\n    | datetime.datetime\n    | datetime.date\n    | datetime.time\n    | Sequence[\"ConfigInput\"]\n    | Mapping[str, \"ConfigInput\"]\n    | None\n)\nConfigObject = Mapping[str, ConfigInput]\n\n\n@dataclasses.dataclass\nclass Context:\n    schema: s_schema.Schema\n    obj_type: s_objtypes.ObjectType\n    qual_name: str\n    options: qlcompiler.CompilerOptions\n\n    def get_ptr(self, name: str) -> s_pointers.Pointer:\n        un = sn.UnqualName(name)\n        schema = self.schema\n        ty = self.obj_type\n        ancestors = ty.get_ancestors(schema).objects(schema)\n        for t in (ty,) + ancestors:\n            if (rv := t.maybe_get_ptr(schema, un)) is not None:\n                return rv\n        raise errors.ConfigurationError(\n            f\"{ty.get_shortname(schema)!s} does not have field: {name!r}\"\n        )\n\n    def get_full_name(self, ptr: s_pointers.Pointer) -> str:\n        return f\"{self.qual_name}::{ptr.get_local_name(self.schema)}\"\n\n    def is_multi(self, ptr: s_pointers.Pointer) -> bool:\n        return ptr.get_cardinality(self.schema).is_multi()\n\n    def get_type[TypeT: s_types.Type](\n        self, ptr: s_pointers.Pointer, *, type: type[TypeT]\n    ) -> TypeT:\n        rv = ptr.get_target(self.schema)\n        if not isinstance(rv, type):\n            raise TypeError(f\"{ptr!r}.target is not {type:r}\")\n        return rv\n\n    def get_ref(self, ptr: s_pointers.Pointer) -> qlast.ObjectRef:\n        ty = self.get_type(ptr, type=s_types.QualifiedType)\n        ty_name = ty.get_shortname(self.schema)\n        return qlast.ObjectRef(name=ty_name.name, module=ty_name.module)\n\n    def cast(\n        self, expr: qlast.Expr, *, ptr: s_pointers.Pointer\n    ) -> qlast.TypeCast:\n        return qlast.TypeCast(\n            expr=expr,\n            type=qlast.TypeName(maintype=self.get_ref(ptr)),\n        )\n\n\n@functools.singledispatch\ndef compile_input_to_ast(\n    value: ConfigInput, *, ptr: s_pointers.Pointer, ctx: Context\n) -> qlast.Expr:\n    raise errors.ConfigurationError(\n        f\"unsupported input type {type(value)!r} for {ctx.get_full_name(ptr)}\"\n    )\n\n\n@compile_input_to_ast.register\ndef compile_input_str(\n    value: str, *, ptr: s_pointers.Pointer, ctx: Context\n) -> qlast.Expr:\n    if value.startswith(\"{{\") and value.endswith(\"}}\"):\n        return qlparser.parse_fragment(value[2:-2])\n    ty = ctx.get_type(ptr, type=s_types.QualifiedType)\n    if ty.is_enum(ctx.schema):\n        ty_name = ty.get_shortname(ctx.schema)\n        return qlast.Path(\n            steps=[\n                qlast.ObjectRef(name=ty_name.name, module=ty_name.module),\n                qlast.Ptr(name=value),\n            ]\n        )\n    else:\n        return ctx.cast(qlast.Constant.string(value), ptr=ptr)\n\n\n@compile_input_to_ast.register\ndef compile_input_scalar(\n    value: int | float | bool, *, ptr: s_pointers.Pointer, ctx: Context\n) -> qlast.Expr:\n    return ctx.cast(s_utils.const_ast_from_python(value), ptr=ptr)\n\n\n@compile_input_to_ast.register(dict)\n@compile_input_to_ast.register(immutables.Map)\ndef compile_input_mapping(\n    value: Mapping[str, ConfigInput],\n    *,\n    ptr: s_pointers.Pointer,\n    ctx: Context,\n) -> qlast.Expr:\n    if \"_tname\" in value:\n        tname = value[\"_tname\"]\n        if not isinstance(tname, str):\n            raise errors.ConfigurationError(\n                f\"type of `_tname` must be str, got: {type(tname)!r}\"\n            )\n        obj_type = ctx.schema.get(tname, type=s_objtypes.ObjectType)\n    else:\n        try:\n            obj_type = ctx.get_type(ptr, type=s_objtypes.ObjectType)\n        except TypeError:\n            raise errors.ConfigurationError(\n                f\"unsupported input type {type(value)!r} \"\n                f\"for {ctx.get_full_name(ptr)}\"\n            )\n    obj_name = obj_type.get_shortname(ctx.schema)\n    new_ctx = Context(\n        schema=ctx.schema,\n        obj_type=obj_type,\n        qual_name=ctx.get_full_name(ptr),\n        options=ctx.options,\n    )\n    return qlast.InsertQuery(\n        subject=qlast.ObjectRef(name=obj_name.name, module=obj_name.module),\n        shape=list(compile_dict_to_shape(value, ctx=new_ctx).values()),\n    )\n\n\ndef compile_dict_to_shape(\n    values: Mapping[str, ConfigInput], *, ctx: Context\n) -> dict[str, qlast.ShapeElement]:\n    rv = {}\n    for name, value in values.items():\n        if name == \"_tname\":\n            continue\n        ptr = ctx.get_ptr(name)\n        expr: qlast.Expr\n        if ctx.is_multi(ptr) and not isinstance(value, str):\n            if not typeutils.is_container(value) or isinstance(value, Mapping):\n                raise errors.ConfigurationError(\n                    f\"{ctx.get_full_name(ptr)} must be a sequence, \"\n                    f\"got type: {type(value)!r}\"\n                )\n            assert isinstance(value, Iterable)\n            expr = qlast.Set(\n                elements=[\n                    compile_input_to_ast(v, ptr=ptr, ctx=ctx) for v in value\n                ]\n            )\n        else:\n            expr = compile_input_to_ast(value, ptr=ptr, ctx=ctx)\n        rv[name] = qlast.ShapeElement(\n            expr=qlast.Path(steps=[qlast.Ptr(name=name)]), compexpr=expr\n        )\n    return rv\n\n\ndef compile_ast_to_operation(\n    obj_name: str,\n    field_name: str,\n    expr: qlast.Expr,\n    *,\n    schema: s_schema.Schema,\n    options: qlcompiler.CompilerOptions,\n    allow_nested: bool = True,\n) -> config.Operation:\n    cmd: qlast.ConfigOp\n    if isinstance(expr, qlast.InsertQuery):\n        if not allow_nested:\n            raise errors.ConfigurationError(\n                \"nested config object is not allowed\"\n            )\n        cmd = qlast.ConfigInsert(\n            name=expr.subject,\n            scope=qltypes.ConfigScope.INSTANCE,\n            shape=expr.shape,\n        )\n    else:\n        field_name_ref = qlast.ObjectRef(name=field_name)\n        if obj_name != \"cfg::Config\":\n            field_name_ref.module = obj_name\n        cmd = qlast.ConfigSet(\n            name=field_name_ref,\n            scope=qltypes.ConfigScope.INSTANCE,\n            expr=expr,\n        )\n    ir = qlcompiler.compile_ast_to_ir(cmd, schema=schema, options=options)\n    if (\n        isinstance(ir, irast.ConfigSet)\n        or isinstance(ir, irast.Statement)\n        and isinstance((ir := ir.expr.expr), irast.ConfigInsert)\n    ):\n        return ireval.evaluate_to_config_op(ir, schema=schema)\n\n    raise errors.InternalServerError(f\"unrecognized IR: {type(ir)!r}\")\n\n\ndef compile_structured_config(\n    objects: Mapping[str, ConfigObject],\n    *,\n    spec: config.Spec,\n    schema: s_schema.Schema,\n    source: str | None = None,\n    allow_nested: bool = True,\n) -> dict[str, immutables.Map[str, config.SettingValue]]:\n    options = qlcompiler.CompilerOptions(\n        modaliases={None: \"cfg\"},\n        in_server_config_op=True,\n    )\n    rv = {}\n    for obj_name, input_values in objects.items():\n        storage: immutables.Map[str, config.SettingValue] = immutables.Map()\n        ctx = Context(\n            schema=schema,\n            obj_type=schema.get(obj_name, type=s_objtypes.ObjectType),\n            qual_name=obj_name,\n            options=options,\n        )\n        shape = compile_dict_to_shape(input_values, ctx=ctx)\n        for field_name, shape_el in shape.items():\n            if isinstance(shape_el.compexpr, qlast.Set):\n                elements = shape_el.compexpr.elements\n                if not elements:\n                    continue\n\n                if isinstance(elements[0], qlast.InsertQuery):\n                    for ast in shape_el.compexpr.elements:\n                        op = compile_ast_to_operation(\n                            obj_name,\n                            field_name,\n                            ast,\n                            schema=schema,\n                            options=options,\n                            allow_nested=allow_nested,\n                        )\n                        storage = op.apply(spec, storage, source=source)\n                    continue\n\n            assert shape_el.compexpr is not None\n            op = compile_ast_to_operation(\n                obj_name,\n                field_name,\n                shape_el.compexpr,\n                schema=schema,\n                options=options,\n                allow_nested=allow_nested,\n            )\n            storage = op.apply(spec, storage, source=source)\n\n        rv[obj_name] = storage\n\n    return rv\n"
  },
  {
    "path": "edb/server/compiler/dbstate.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    Iterator,\n    NamedTuple,\n    Self,\n    cast,\n)\n\nimport dataclasses\nimport enum\nimport io\nimport pickle\nimport time\nimport uuid\n\nimport immutables\n\nfrom edb import errors\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\nfrom edb.schema import delta as s_delta\nfrom edb.schema import migrations as s_migrations\nfrom edb.schema import objects as s_obj\nfrom edb.schema import schema as s_schema\nfrom edb.schema import name as s_name\n\nfrom edb.server import config\nfrom edb.server import defines\n\nfrom edb.pgsql import codegen as pgcodegen\n\nfrom . import enums\nfrom . import sertypes\n\n\nclass TxAction(enum.IntEnum):\n    START = 1\n    COMMIT = 2\n    ROLLBACK = 3\n\n    DECLARE_SAVEPOINT = 4\n    RELEASE_SAVEPOINT = 5\n    ROLLBACK_TO_SAVEPOINT = 6\n\n\nclass MigrationAction(enum.IntEnum):\n    START = 1\n    POPULATE = 2\n    DESCRIBE = 3\n    ABORT = 4\n    COMMIT = 5\n    REJECT_PROPOSED = 6\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass BaseQuery:\n    sql: bytes\n    is_transactional: bool = True\n    has_dml: bool = False\n    cache_sql: Optional[tuple[bytes, bytes]] = dataclasses.field(\n        kw_only=True, default=None\n    )  # (persist, evict)\n    cache_func_call: Optional[tuple[bytes, bytes]] = dataclasses.field(\n        kw_only=True, default=None\n    )\n    warnings: tuple[errors.EdgeDBError, ...] = dataclasses.field(\n        kw_only=True, default=()\n    )\n    unsafe_isolation_dangers: tuple[errors.UnsafeIsolationLevelError, ...] = (\n        dataclasses.field(kw_only=True, default=())\n    )\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass NullQuery(BaseQuery):\n    sql: bytes = b\"\"\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass ServerParamConversion:\n    param_name: str\n    conversion_name: str\n    additional_info: tuple[str, ...]\n\n    # If the parameter is a query parameter, track its bind_args index.\n    script_param_index: Optional[int] = None\n\n    # If the parameter is a constant value, pass to directly to the server.\n    constant_value: Optional[Any] = None\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass Query(BaseQuery):\n    sql_hash: bytes\n\n    cardinality: enums.Cardinality\n\n    out_type_data: bytes\n    out_type_id: bytes\n    in_type_data: bytes\n    in_type_id: bytes\n    in_type_args: Optional[list[Param]] = None\n\n    globals: Optional[list[tuple[str, bool]]] = None\n    permissions: Optional[list[str]] = None\n    json_permissions: Optional[list[str]] = None\n    required_permissions: Optional[list[str]] = None\n\n    server_param_conversions: Optional[list[ServerParamConversion]] = None\n\n    cacheable: bool = True\n    is_explain: bool = False\n    query_asts: Any = None\n    run_and_rollback: bool = False\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass SimpleQuery(BaseQuery):\n    # XXX: Temporary hack, since SimpleQuery will die\n    in_type_args: Optional[list[Param]] = None\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass SessionStateQuery(BaseQuery):\n    sql: bytes = b\"\"\n    config_scope: Optional[qltypes.ConfigScope] = None\n    is_backend_setting: bool = False\n    requires_restart: bool = False\n    is_system_config: bool = False\n    config_op: Optional[config.Operation] = None\n    is_transactional: bool = True\n    globals: Optional[list[tuple[str, bool]]] = None\n\n    in_type_data: Optional[bytes] = None\n    in_type_id: Optional[bytes] = None\n    in_type_args: Optional[list[Param]] = None\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass DDLQuery(BaseQuery):\n    user_schema: Optional[s_schema.Schema]\n    feature_used_metrics: Optional[dict[str, float]]\n    global_schema: Optional[s_schema.Schema] = None\n    cached_reflection: Any = None\n    is_transactional: bool = True\n    create_db: Optional[str] = None\n    drop_db: Optional[str] = None\n    drop_db_reset_connections: bool = False\n    create_db_template: Optional[str] = None\n    create_db_mode: Optional[qlast.BranchType] = None\n    db_op_trailer: tuple[bytes, ...] = ()\n    ddl_stmt_id: Optional[str] = None\n    config_ops: list[config.Operation] = dataclasses.field(default_factory=list)\n    early_non_tx_sql: Optional[tuple[bytes, ...]] = None\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass TxControlQuery(BaseQuery):\n    action: TxAction\n    cacheable: bool\n\n    modaliases: Optional[immutables.Map[Optional[str], str]]\n\n    isolation_level: Optional[qltypes.TransactionIsolationLevel] = None\n\n    user_schema: Optional[s_schema.Schema] = None\n    global_schema: Optional[s_schema.Schema] = None\n    cached_reflection: Any = None\n    feature_used_metrics: Optional[dict[str, float]] = None\n\n    sp_name: Optional[str] = None\n    sp_id: Optional[int] = None\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass MigrationControlQuery(BaseQuery):\n    action: MigrationAction\n    tx_action: Optional[TxAction]\n    cacheable: bool\n\n    modaliases: Optional[immutables.Map[Optional[str], str]]\n\n    user_schema: Optional[s_schema.Schema] = None\n    cached_reflection: Any = None\n    ddl_stmt_id: Optional[str] = None\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass MaintenanceQuery(BaseQuery):\n    pass\n\n\n@dataclasses.dataclass(frozen=True)\nclass Param:\n    name: str\n    required: bool\n    array_type_id: Optional[uuid.UUID]\n    outer_idx: Optional[int]\n    sub_params: Optional[tuple[list[Optional[uuid.UUID]], tuple[Any, ...]]]\n    typename: str\n\n\n#############################\n\n\n@dataclasses.dataclass(kw_only=True)\nclass QueryUnit:\n    sql: bytes\n\n    introspection_sql: Optional[bytes] = None\n\n    # Status-line for the compiled command; returned to front-end\n    # in a CommandComplete protocol message if the command is\n    # executed successfully.  When a QueryUnit contains multiple\n    # EdgeQL queries, the status reflects the last query in the unit.\n    status: bytes\n\n    cache_key: Optional[uuid.UUID] = None\n    cache_sql: Optional[tuple[bytes, bytes]] = None  # (persist, evict)\n    cache_func_call: Optional[tuple[bytes, bytes]] = None  # (sql, hash)\n\n    # Output format of this query unit\n    output_format: enums.OutputFormat = enums.OutputFormat.NONE\n\n    # Set only for units that contain queries that can be cached\n    # as prepared statements in Postgres.\n    sql_hash: bytes = b\"\"\n\n    # True if all statements in *sql* can be executed inside a transaction.\n    # If False, they will be executed separately.\n    is_transactional: bool = True\n\n    # SQL to run *before* the main command, non transactionally\n    early_non_tx_sql: Optional[tuple[bytes, ...]] = None\n\n    # Capabilities used in this query\n    capabilities: enums.Capability = enums.Capability(0)\n\n    # True if this unit contains SET commands.\n    has_set: bool = False\n\n    # If tx_id is set, it means that the unit\n    # starts a new transaction.\n    tx_id: Optional[int] = None\n\n    # If this is the start of the transaction, the isolation level of it.\n    tx_isolation_level: Optional[qltypes.TransactionIsolationLevel] = None\n\n    # True if this unit is single 'COMMIT' command.\n    # 'COMMIT' is always compiled to a separate QueryUnit.\n    tx_commit: bool = False\n\n    # True if this unit is single 'ROLLBACK' command.\n    # 'ROLLBACK' is always compiled to a separate QueryUnit.\n    tx_rollback: bool = False\n\n    # True if this unit is single 'ROLLBACK TO SAVEPOINT' command.\n    # 'ROLLBACK TO SAVEPOINT' is always compiled to a separate QueryUnit.\n    tx_savepoint_rollback: bool = False\n    tx_savepoint_declare: bool = False\n\n    # True if this unit is `ABORT MIGRATION` command within a transaction,\n    # that means abort_migration and tx_rollback cannot be both True\n    tx_abort_migration: bool = False\n\n    # For SAVEPOINT commands, the name and sp_id\n    sp_name: Optional[str] = None\n    sp_id: Optional[int] = None\n\n    # True if it is safe to cache this unit.\n    cacheable: bool = False\n\n    # If non-None, contains a name of the DB that is about to be\n    # created/deleted. If it's the former, the IO process needs to\n    # introspect the new db. If it's the later, the server should\n    # close all inactive unused pooled connections to it.\n    create_db: Optional[str] = None\n    drop_db: Optional[str] = None\n    drop_db_reset_connections: bool = False\n\n    # If non-None, contains a name of the DB that will be used as\n    # a template database to create the database. The server should\n    # close all inactive unused pooled connections to the template db.\n    create_db_template: Optional[str] = None\n    create_db_mode: Optional[str] = None\n\n    # If a branch command needs extra SQL commands to be performed,\n    # those would end up here.\n    db_op_trailer: tuple[bytes, ...] = ()\n\n    # If non-None, the DDL statement will emit data packets marked\n    # with the indicated ID.\n    ddl_stmt_id: Optional[str] = None\n\n    # Cardinality of the result set.  Set to NO_RESULT if the\n    # unit represents multiple queries compiled as one script.\n    cardinality: enums.Cardinality = enums.Cardinality.NO_RESULT\n\n    out_type_data: bytes = sertypes.NULL_TYPE_DESC\n    out_type_id: bytes = sertypes.NULL_TYPE_ID.bytes\n    in_type_data: bytes = sertypes.NULL_TYPE_DESC\n    in_type_id: bytes = sertypes.NULL_TYPE_ID.bytes\n    in_type_args: Optional[list[Param]] = None\n    in_type_args_real_count: int = 0\n    globals: Optional[list[tuple[str, bool]]] = None\n    permissions: Optional[list[str]] = None\n    json_permissions: Optional[list[str]] = None\n    required_permissions: Optional[list[str]] = None\n\n    server_param_conversions: Optional[list[ServerParamConversion]] = None\n\n    warnings: tuple[errors.EdgeDBError, ...] = ()\n    unsafe_isolation_dangers: tuple[errors.UnsafeIsolationLevelError, ...] = ()\n\n    # Set only when this unit contains a CONFIGURE INSTANCE command.\n    system_config: bool = False\n    # Set only when this unit contains a CONFIGURE DATABASE command.\n    database_config: bool = False\n    # Set only when this unit contains an operation that needs to have\n    # its results read back in the middle of the script.\n    # (SET GLOBAL, CONFIGURE DATABASE)\n    needs_readback: bool = False\n    # Whether any configuration change requires a server restart\n    config_requires_restart: bool = False\n    # Set only when this unit contains a CONFIGURE command which\n    # alters a backend configuration setting.\n    backend_config: bool = False\n    # Set only when this unit contains a CONFIGURE command which\n    # alters a system configuration setting.\n    is_system_config: bool = False\n    config_ops: list[config.Operation] = dataclasses.field(default_factory=list)\n    modaliases: Optional[immutables.Map[Optional[str], str]] = None\n\n    # If present, represents the future schema state after\n    # the command is run. The schema is pickled.\n    user_schema: Optional[bytes] = None\n    # If present, represents updated metrics about feature use induced\n    # by the new user_schema.\n    feature_used_metrics: Optional[dict[str, float]] = None\n\n    # Unlike user_schema, user_schema_version usually exist, pointing to the\n    # latest user schema, which is self.user_schema if changed, or the user\n    # schema this QueryUnit was compiled upon.\n    user_schema_version: uuid.UUID | None = None\n    cached_reflection: Optional[bytes] = None\n    extensions: Optional[set[str]] = None\n    ext_config_settings: Optional[list[config.Setting]] = None\n\n    # If present, represents the future global schema state\n    # after the command is run. The schema is pickled.\n    global_schema: Optional[bytes] = None\n    roles: immutables.Map[str, immutables.Map[str, Any]] | None = None\n\n    is_explain: bool = False\n    query_asts: Any = None\n    run_and_rollback: bool = False\n    append_tx_op: bool = False\n\n    # Translation source map.\n    source_map: Optional[pgcodegen.SourceMap] = None\n    # For SQL queries, the length of the query prefix applied\n    # after translation.\n    sql_prefix_len: int = 0\n\n    @property\n    def has_ddl(self) -> bool:\n        return bool(self.capabilities & enums.Capability.DDL)\n\n    @property\n    def tx_control(self) -> bool:\n        return (\n            bool(self.tx_id)\n            or self.tx_rollback\n            or self.tx_commit\n            or self.tx_savepoint_declare\n            or self.tx_savepoint_rollback\n        )\n\n    def serialize(self) -> bytes:\n        rv = io.BytesIO()\n        rv.write(b\"\\x01\")  # 1 byte of version number\n        pickle.dump(self, rv, -1)\n        return rv.getvalue()\n\n    @classmethod\n    def deserialize(cls, data: bytes) -> Self:\n        buf = memoryview(data)\n        match buf[0]:\n            case 0x00 | 0x01:\n                return pickle.loads(buf[1:])  # type: ignore[no-any-return]\n        raise ValueError(f\"Bad version number: {buf[0]}\")\n\n    def maybe_use_func_cache(self) -> None:\n        if self.cache_func_call is not None:\n            sql, sql_hash = self.cache_func_call\n            self.sql = sql\n            self.sql_hash = sql_hash\n\n\n@dataclasses.dataclass\nclass QueryUnitGroup:\n    # All capabilities used by any query units in this group\n    capabilities: enums.Capability = enums.Capability(0)\n\n    # True if it is safe to cache this unit.\n    cacheable: bool = True\n\n    # True if any query unit has transaction control commands, like COMMIT,\n    # ROLLBACK, START TRANSACTION or SAVEPOINT-related commands\n    tx_control: bool = False\n\n    # Cardinality of the result set.  Set to NO_RESULT if the\n    # unit group is not expected or desired to return data.\n    cardinality: enums.Cardinality = enums.Cardinality.NO_RESULT\n\n    out_type_data: bytes = sertypes.NULL_TYPE_DESC\n    out_type_id: bytes = sertypes.NULL_TYPE_ID.bytes\n    in_type_data: bytes = sertypes.NULL_TYPE_DESC\n    in_type_id: bytes = sertypes.NULL_TYPE_ID.bytes\n    in_type_args: Optional[list[Param]] = None\n    in_type_args_real_count: int = 0\n    globals: Optional[list[tuple[str, bool]]] = None\n    permissions: Optional[list[str]] = None\n    json_permissions: Optional[list[str]] = None\n    required_permissions: Optional[list[str]] = None\n\n    server_param_conversions: Optional[list[ServerParamConversion]] = None\n    unit_converted_param_indexes: Optional[dict[int, list[int]]] = None\n\n    warnings: Optional[list[errors.EdgeDBError]] = None\n    unsafe_isolation_dangers: (\n        Optional[list[errors.UnsafeIsolationLevelError]]\n    ) = None\n\n    # Cacheable QueryUnit is serialized in the compiler, so that the I/O server\n    # doesn't need to serialize it again for persistence.\n    _units: list[QueryUnit | bytes] = dataclasses.field(default_factory=list)\n    # This is a I/O server-only cache for unpacked QueryUnits\n    _unpacked_units: list[QueryUnit] | None = None\n\n    state_serializer: Optional[sertypes.StateSerializer] = None\n\n    cache_state: int = 0\n    tx_seq_id: int = 0\n\n    force_non_normalized: bool = False\n\n    graphql_key_variables: Optional[list[str]] = None\n\n    @property\n    def units(self) -> list[QueryUnit]:\n        if self._unpacked_units is None:\n            self._unpacked_units = [\n                QueryUnit.deserialize(unit) if isinstance(unit, bytes) else unit\n                for unit in self._units\n            ]\n        return self._unpacked_units\n\n    def __iter__(self) -> Iterator[QueryUnit]:\n        return iter(self.units)\n\n    def __len__(self) -> int:\n        return len(self._units)\n\n    def __getitem__(self, item: int) -> QueryUnit:\n        return self.units[item]\n\n    def maybe_get_serialized(self, item: int) -> bytes | None:\n        unit = self._units[item]\n        if isinstance(unit, bytes):\n            return unit\n        return None\n\n    def append(\n        self,\n        query_unit: QueryUnit,\n        serialize: bool = True,\n    ) -> None:\n        self.capabilities |= query_unit.capabilities\n\n        if not query_unit.cacheable:\n            self.cacheable = False\n\n        if query_unit.tx_control:\n            self.tx_control = True\n\n        self.cardinality = query_unit.cardinality\n        self.out_type_data = query_unit.out_type_data\n        self.out_type_id = query_unit.out_type_id\n        self.in_type_data = query_unit.in_type_data\n        self.in_type_id = query_unit.in_type_id\n        self.in_type_args = query_unit.in_type_args\n        self.in_type_args_real_count = query_unit.in_type_args_real_count\n        if query_unit.globals is not None:\n            if self.globals is None:\n                self.globals = []\n            self.globals.extend(query_unit.globals)\n        if query_unit.permissions is not None:\n            if self.permissions is None:\n                self.permissions = []\n            self.permissions.extend(query_unit.permissions)\n        if query_unit.json_permissions is not None:\n            if self.json_permissions is None:\n                self.json_permissions = []\n            self.json_permissions.extend(query_unit.json_permissions)\n        if query_unit.required_permissions is not None:\n            if self.required_permissions is None:\n                self.required_permissions = []\n            for perm in query_unit.required_permissions:\n                if perm not in self.required_permissions:\n                    self.required_permissions.append(perm)\n        if query_unit.server_param_conversions is not None:\n            if self.server_param_conversions is None:\n                self.server_param_conversions = []\n            if self.unit_converted_param_indexes is None:\n                self.unit_converted_param_indexes = {}\n\n            # De-duplicate param conversions and store information about which\n            # units access which converted params.\n            # If two units request the same conversion on the same parameter,\n            # we should assume the conversion is stable and only do it once.\n            unit_index = len(self._units)\n            converted_param_indexes: list[int] = []\n            for spc in query_unit.server_param_conversions:\n                if spc in self.server_param_conversions:\n                    converted_param_indexes.append(\n                        self.server_param_conversions.index(spc)\n                    )\n                else:\n                    converted_param_indexes.append(\n                        len(self.server_param_conversions)\n                    )\n                    self.server_param_conversions.append(spc)\n            self.unit_converted_param_indexes[unit_index] = (\n                converted_param_indexes\n            )\n\n        if query_unit.warnings is not None:\n            if self.warnings is None:\n                self.warnings = []\n            self.warnings.extend(query_unit.warnings)\n        if query_unit.unsafe_isolation_dangers is not None:\n            if self.unsafe_isolation_dangers is None:\n                self.unsafe_isolation_dangers = []\n            self.unsafe_isolation_dangers.extend(\n                query_unit.unsafe_isolation_dangers)\n\n        if not serialize or query_unit.cache_sql is None:\n            self._units.append(query_unit)\n        else:\n            self._units.append(query_unit.serialize())\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass PreparedStmtOpData:\n    \"\"\"Common prepared statement metadata\"\"\"\n\n    stmt_name: str\n    \"\"\"Original statement name as passed by the frontend\"\"\"\n\n    be_stmt_name: bytes = b\"\"\n    \"\"\"Computed statement name as passed to the backend\"\"\"\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass PrepareData(PreparedStmtOpData):\n    \"\"\"PREPARE statement data\"\"\"\n\n    query: str\n    \"\"\"Translated query string\"\"\"\n    source_map: Optional[pgcodegen.SourceMap] = None\n    \"\"\"Translation source map\"\"\"\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass ExecuteData(PreparedStmtOpData):\n    \"\"\"EXECUTE statement data\"\"\"\n\n    pass\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass DeallocateData(PreparedStmtOpData):\n    \"\"\"DEALLOCATE statement data\"\"\"\n\n    pass\n\n\n@dataclasses.dataclass(kw_only=True)\nclass SQLQueryUnit:\n    query: str = dataclasses.field(repr=False)\n    \"\"\"Translated query text.\"\"\"\n\n    prefix_len: int = 0\n    source_map: Optional[pgcodegen.SourceMap] = None\n    \"\"\"Translation source map.\"\"\"\n\n    eql_format_query: Optional[str] = dataclasses.field(\n        repr=False, default=None)\n    \"\"\"Translated query text returning data in single-column format.\"\"\"\n\n    orig_query: str = dataclasses.field(repr=False)\n    \"\"\"Original query text before translation.\"\"\"\n\n    # True if it is safe to cache this unit.\n    cacheable: bool = True\n\n    cardinality: enums.Cardinality = enums.Cardinality.NO_RESULT\n\n    capabilities: enums.Capability = enums.Capability.NONE\n\n    fe_settings: SQLSettings\n    \"\"\"Frontend-only settings effective during translation of this unit.\"\"\"\n\n    tx_action: Optional[TxAction] = None\n    tx_chain: bool = False\n    sp_name: Optional[str] = None\n\n    prepare: Optional[PrepareData] = None\n    execute: Optional[ExecuteData] = None\n    deallocate: Optional[DeallocateData] = None\n\n    set_vars: Optional[dict[Optional[str], Optional[SQLSetting]]] = None\n    is_local: bool = False\n\n    stmt_name: bytes = b\"\"\n    \"\"\"Computed prepared statement name for this query.\"\"\"\n\n    frontend_only: bool = False\n    \"\"\"Whether the query is completely emulated outside of backend and so\n    the response should be synthesized also.\"\"\"\n\n    command_complete_tag: Optional[CommandCompleteTag] = None\n    \"\"\"When set, CommandComplete for this query will be overridden.\n    This is useful, for example, for setting the tag of DML statements,\n    which return the number of modified rows.\"\"\"\n\n    params: Optional[list[SQLParam]] = None\n\n\nclass CommandCompleteTag:\n    \"\"\"Dictates the tag of CommandComplete message that concludes this query.\"\"\"\n\n\n@dataclasses.dataclass(kw_only=True)\nclass TagPlain(CommandCompleteTag):\n    \"\"\"Set the tag verbatim\"\"\"\n\n    tag: bytes\n\n\n@dataclasses.dataclass(kw_only=True)\nclass TagCountMessages(CommandCompleteTag):\n    \"\"\"Count DataRow messages in the response and set the tag to\n    f'{prefix} {count_of_messages}'.\"\"\"\n\n    prefix: str\n\n\n@dataclasses.dataclass(kw_only=True)\nclass TagUnpackRow(CommandCompleteTag):\n    \"\"\"Intercept a single DataRow message with a single column which represents\n    the number of modified rows.\n    Sets the CommandComplete tag to f'{prefix} {modified_rows}'.\"\"\"\n\n    prefix: str\n\n\nclass SQLParam:\n    # Internal query param. Represents params in the compiled SQL, so the params\n    # that are sent to PostgreSQL.\n\n    # True for params that are actually used in the compiled query.\n    used: bool = False\n\n\n@dataclasses.dataclass(kw_only=True, eq=False, slots=True, repr=False)\nclass SQLParamExternal(SQLParam):\n    # An internal query param whose value is provided by an external param.\n    # So a user-visible param.\n\n    # External params share the index with internal params\n    pass\n\n\n@dataclasses.dataclass(kw_only=True, eq=False, slots=True, repr=False)\nclass SQLParamExtractedConst(SQLParam):\n    # An internal query param whose value is a constant that this param has\n    # replaced during query normalization.\n\n    type_oid: int\n\n\n@dataclasses.dataclass(kw_only=True, eq=False, slots=True, repr=False)\nclass SQLParamGlobal(SQLParam):\n    # An internal query param whose value is provided by a global.\n\n    global_name: s_name.QualName\n\n    pg_type: tuple[str, ...]\n\n    is_permission: bool\n\n    internal_index: int\n\n\n@dataclasses.dataclass\nclass ParsedDatabase:\n    user_schema_pickle: bytes\n    schema_version: uuid.UUID\n    database_config: immutables.Map[str, config.SettingValue]\n    ext_config_settings: list[config.Setting]\n    feature_used_metrics: dict[str, float]\n\n    protocol_version: defines.ProtocolVersion\n    state_serializer: sertypes.StateSerializer\n\n\nSQLSetting = tuple[str | int | float, ...]\nSQLSettings = immutables.Map[Optional[str], Optional[SQLSetting]]\nDEFAULT_SQL_SETTINGS: SQLSettings = immutables.Map()\nDEFAULT_SQL_FE_SETTINGS: SQLSettings = immutables.Map(\n    {\n        \"search_path\": (\"public\",),\n        \"server_version\": cast(SQLSetting, (defines.PGEXT_POSTGRES_VERSION,)),\n        \"server_version_num\": cast(\n            SQLSetting, (defines.PGEXT_POSTGRES_VERSION_NUM,)\n        ),\n    }\n)\n\n\n@dataclasses.dataclass\nclass SQLTransactionState:\n    in_tx: bool\n    settings: SQLSettings\n    in_tx_settings: Optional[SQLSettings]\n    in_tx_local_settings: Optional[SQLSettings]\n    savepoints: list[tuple[str, SQLSettings, SQLSettings]]\n\n    def current_fe_settings(self) -> SQLSettings:\n        if self.in_tx:\n            return self.in_tx_local_settings or DEFAULT_SQL_FE_SETTINGS\n        else:\n            return self.settings or DEFAULT_SQL_FE_SETTINGS\n\n    def get(self, name: str) -> Optional[SQLSetting]:\n        if self.in_tx:\n            # For easier access, in_tx_local_settings is always a superset of\n            # in_tx_settings; in_tx_settings only keeps track of non-local\n            # settings, so that the local settings don't go across tx bounds\n            assert self.in_tx_local_settings\n            return self.in_tx_local_settings[name]\n        else:\n            return self.settings[name]\n\n    def apply(self, query_unit: SQLQueryUnit) -> None:\n        if query_unit.tx_action == TxAction.COMMIT:\n            self.in_tx = False\n            self.settings = self.in_tx_settings  # type: ignore\n            self.in_tx_settings = None\n            self.in_tx_local_settings = None\n            self.savepoints.clear()\n        elif query_unit.tx_action == TxAction.ROLLBACK:\n            self.in_tx = False\n            self.in_tx_settings = None\n            self.in_tx_local_settings = None\n            self.savepoints.clear()\n        elif query_unit.tx_action == TxAction.DECLARE_SAVEPOINT:\n            assert query_unit.sp_name is not None\n            assert self.in_tx_settings is not None\n            assert self.in_tx_local_settings is not None\n            self.savepoints.append(\n                (\n                    query_unit.sp_name,\n                    self.in_tx_settings,\n                    self.in_tx_local_settings,\n                )\n            )\n        elif query_unit.tx_action == TxAction.ROLLBACK_TO_SAVEPOINT:\n            while self.savepoints:\n                sp_name, settings, local_settings = self.savepoints[-1]\n                if query_unit.sp_name == sp_name:\n                    self.in_tx_settings = settings\n                    self.in_tx_local_settings = local_settings\n                    break\n                else:\n                    self.savepoints.pop(0)\n            else:\n                raise errors.TransactionError(\n                    f'savepoint \"{query_unit.sp_name}\" does not exist'\n                )\n        if not self.in_tx:\n            # Always start an implicit transaction here, because in the\n            # compiler, multiple apply() calls only happen in simple query,\n            # and any query would start an implicit transaction. For example,\n            # we need to support a single ROLLBACK without a matching BEGIN\n            # rolling back an implicit transaction.\n            self.in_tx = True\n            self.in_tx_settings = self.settings\n            self.in_tx_local_settings = self.settings\n        if query_unit.frontend_only and query_unit.set_vars:\n            for name, value in query_unit.set_vars.items():\n                self.set(name, value, query_unit.is_local)\n\n    def set(\n        self, name: Optional[str], value: Optional[SQLSetting], is_local: bool\n    ) -> None:\n        def _set(attr_name: str) -> None:\n            settings = getattr(self, attr_name)\n            if value is None:\n                if name in settings:\n                    settings = settings.delete(name)\n            else:\n                settings = settings.set(name, value)\n            setattr(self, attr_name, settings)\n\n        if self.in_tx:\n            _set(\"in_tx_local_settings\")\n            if not is_local:\n                _set(\"in_tx_settings\")\n        elif not is_local:\n            _set(\"settings\")\n\n\n#############################\n\n\nclass ProposedMigrationStep(NamedTuple):\n    statements: tuple[str, ...]\n    confidence: float\n    prompt: str\n    prompt_id: str\n    data_safe: bool\n    required_user_input: tuple[dict[str, str], ...]\n    # This isn't part of the output data, but is used to figure out\n    # what to prohibit when something is rejected.\n    operation_key: s_delta.CommandKey\n\n    def to_json(self) -> dict[str, Any]:\n        return {\n            \"statements\": [{\"text\": stmt} for stmt in self.statements],\n            \"confidence\": self.confidence,\n            \"prompt\": self.prompt,\n            \"prompt_id\": self.prompt_id,\n            \"data_safe\": self.data_safe,\n            \"required_user_input\": list(self.required_user_input),\n        }\n\n\nclass MigrationState(NamedTuple):\n    parent_migration: Optional[s_migrations.Migration]\n    initial_schema: s_schema.Schema\n    initial_savepoint: Optional[str]\n    target_schema: s_schema.Schema\n    guidance: s_obj.DeltaGuidance\n    accepted_cmds: tuple[qlast.Base, ...]\n    last_proposed: Optional[tuple[ProposedMigrationStep, ...]]\n\n\nclass MigrationRewriteState(NamedTuple):\n    initial_savepoint: Optional[str]\n    target_schema: s_schema.Schema\n    accepted_migrations: tuple[qlast.CreateMigration, ...]\n\n\nclass TransactionState(NamedTuple):\n    id: int\n    name: Optional[str]\n    local_user_schema: s_schema.Schema | None\n    global_schema: s_schema.Schema\n    modaliases: immutables.Map[Optional[str], str]\n    session_config: immutables.Map[str, config.SettingValue]\n    database_config: immutables.Map[str, config.SettingValue]\n    system_config: immutables.Map[str, config.SettingValue]\n    cached_reflection: immutables.Map[str, tuple[str, ...]]\n    tx: Transaction\n    migration_state: Optional[MigrationState] = None\n    migration_rewrite_state: Optional[MigrationRewriteState] = None\n\n    @property\n    def user_schema(self) -> s_schema.Schema:\n        if self.local_user_schema is None:\n            return self.tx.root_user_schema\n        else:\n            return self.local_user_schema\n\n\nclass Transaction:\n\n    # Fields that affects the state key are listed here. The key is used\n    # to determine if we can reuse a previously-pickled state, so remember\n    # to update get_state_key() below when adding new fields affecting the\n    # state key.  See also edb/server/compiler_pool/worker.py\n    _id: int\n    _savepoints: dict[int, TransactionState]\n    _current: TransactionState\n\n    # backref to the owning state object\n    _constate: CompilerConnectionState\n\n    def __init__(\n        self,\n        constate: CompilerConnectionState,\n        *,\n        user_schema: s_schema.Schema,\n        global_schema: s_schema.Schema,\n        modaliases: immutables.Map[Optional[str], str],\n        session_config: immutables.Map[str, config.SettingValue],\n        database_config: immutables.Map[str, config.SettingValue],\n        system_config: immutables.Map[str, config.SettingValue],\n        cached_reflection: immutables.Map[str, tuple[str, ...]],\n        implicit: bool = True,\n    ) -> None:\n        assert not isinstance(user_schema, s_schema.ChainedSchema)\n\n        self._constate = constate\n\n        self._id = constate._new_txid()\n        self._implicit = implicit\n\n        self._current = TransactionState(\n            id=self._id,\n            name=None,\n            local_user_schema=(\n                None if user_schema is self.root_user_schema else user_schema\n            ),\n            global_schema=global_schema,\n            modaliases=modaliases,\n            session_config=session_config,\n            database_config=database_config,\n            system_config=system_config,\n            cached_reflection=cached_reflection,\n            tx=self,\n        )\n\n        self._state0 = self._current\n        self._savepoints = {}\n\n    def get_state_key(self) -> tuple[int, tuple[int, ...], TransactionState]:\n        return (\n            self._id,\n            tuple(self._savepoints.keys()),\n            self._current,  # TransactionState is immutable\n        )\n\n    @property\n    def id(self) -> int:\n        return self._id\n\n    @property\n    def root_user_schema(self) -> s_schema.Schema:\n        return self._constate.root_user_schema\n\n    def is_implicit(self) -> bool:\n        return self._implicit\n\n    def make_explicit(self) -> None:\n        if self._implicit:\n            self._implicit = False\n        else:\n            raise errors.TransactionError(\"already in explicit transaction\")\n\n    def declare_savepoint(self, name: str) -> int:\n        if self.is_implicit():\n            raise errors.TransactionError(\n                \"savepoints can only be used in transaction blocks\"\n            )\n\n        return self._declare_savepoint(name)\n\n    def start_migration(self) -> str:\n        name = str(uuid.uuid4())\n        self._declare_savepoint(name)\n        return name\n\n    def _declare_savepoint(self, name: str) -> int:\n        sp_id = self._constate._new_txid()\n        sp_state = self._current._replace(id=sp_id, name=name)\n        self._savepoints[sp_id] = sp_state\n        self._constate._savepoints_log[sp_id] = sp_state\n        return sp_id\n\n    def rollback_to_savepoint(self, name: str) -> TransactionState:\n        if self.is_implicit():\n            raise errors.TransactionError(\n                \"savepoints can only be used in transaction blocks\"\n            )\n\n        return self._rollback_to_savepoint(name)\n\n    def abort_migration(self, name: str) -> None:\n        self._rollback_to_savepoint(name)\n\n    def _rollback_to_savepoint(self, name: str) -> TransactionState:\n        sp_ids_to_erase = []\n        for sp in reversed(self._savepoints.values()):\n            if sp.name == name:\n                self._current = sp\n                break\n\n            sp_ids_to_erase.append(sp.id)\n        else:\n            raise errors.TransactionError(f\"there is no {name!r} savepoint\")\n\n        for sp_id in sp_ids_to_erase:\n            self._savepoints.pop(sp_id)\n\n        return sp\n\n    def release_savepoint(self, name: str) -> None:\n        if self.is_implicit():\n            raise errors.TransactionError(\n                \"savepoints can only be used in transaction blocks\"\n            )\n\n        self._release_savepoint(name)\n\n    def commit_migration(self, name: str) -> None:\n        self._release_savepoint(name)\n\n    def _release_savepoint(self, name: str) -> None:\n        sp_ids_to_erase = []\n        for sp in reversed(self._savepoints.values()):\n            sp_ids_to_erase.append(sp.id)\n\n            if sp.name == name:\n                break\n        else:\n            raise errors.TransactionError(f\"there is no {name!r} savepoint\")\n\n        for sp_id in sp_ids_to_erase:\n            self._savepoints.pop(sp_id)\n\n    def get_schema(self, std_schema: s_schema.Schema) -> s_schema.Schema:\n        return s_schema.ChainedSchema(\n            std_schema,\n            self._current.user_schema,\n            self._current.global_schema,\n        )\n\n    def get_user_schema(self) -> s_schema.Schema:\n        return self._current.user_schema\n\n    def get_user_schema_if_updated(self) -> Optional[s_schema.Schema]:\n        if self._current.user_schema is self._state0.user_schema:\n            return None\n        else:\n            return self._current.user_schema\n\n    def get_global_schema(self) -> s_schema.Schema:\n        return self._current.global_schema\n\n    def get_global_schema_if_updated(self) -> Optional[s_schema.Schema]:\n        if self._current.global_schema is self._state0.global_schema:\n            return None\n        else:\n            return self._current.global_schema\n\n    def get_modaliases(self) -> immutables.Map[Optional[str], str]:\n        return self._current.modaliases\n\n    def get_session_config(self) -> immutables.Map[str, config.SettingValue]:\n        return self._current.session_config\n\n    def get_database_config(self) -> immutables.Map[str, config.SettingValue]:\n        return self._current.database_config\n\n    def get_system_config(self) -> immutables.Map[str, config.SettingValue]:\n        return self._current.system_config\n\n    def get_cached_reflection_if_updated(\n        self,\n    ) -> Optional[immutables.Map[str, tuple[str, ...]]]:\n        if self._current.cached_reflection == self._state0.cached_reflection:\n            return None\n        else:\n            return self._current.cached_reflection\n\n    def get_cached_reflection(self) -> immutables.Map[str, tuple[str, ...]]:\n        return self._current.cached_reflection\n\n    def get_migration_state(self) -> Optional[MigrationState]:\n        return self._current.migration_state\n\n    def get_migration_rewrite_state(self) -> Optional[MigrationRewriteState]:\n        return self._current.migration_rewrite_state\n\n    def update_schema(self, new_schema: s_schema.Schema) -> None:\n        assert isinstance(new_schema, s_schema.ChainedSchema)\n        user_schema = new_schema.get_top_schema()\n        assert isinstance(user_schema, s_schema.Schema)\n        global_schema = new_schema.get_global_schema()\n        assert isinstance(global_schema, s_schema.Schema)\n        self._current = self._current._replace(\n            local_user_schema=user_schema,\n            global_schema=global_schema,\n        )\n\n    def update_modaliases(\n        self, new_modaliases: immutables.Map[Optional[str], str]\n    ) -> None:\n        self._current = self._current._replace(modaliases=new_modaliases)\n\n    def update_session_config(\n        self, new_config: immutables.Map[str, config.SettingValue]\n    ) -> None:\n        self._current = self._current._replace(session_config=new_config)\n\n    def update_database_config(\n        self, new_config: immutables.Map[str, config.SettingValue]\n    ) -> None:\n        self._current = self._current._replace(database_config=new_config)\n\n    def update_cached_reflection(\n        self,\n        new: immutables.Map[str, tuple[str, ...]],\n    ) -> None:\n        self._current = self._current._replace(cached_reflection=new)\n\n    def update_migration_state(self, mstate: Optional[MigrationState]) -> None:\n        self._current = self._current._replace(migration_state=mstate)\n\n    def update_migration_rewrite_state(\n        self, mrstate: Optional[MigrationRewriteState]\n    ) -> None:\n        self._current = self._current._replace(migration_rewrite_state=mrstate)\n\n\nCStateStateType = tuple[dict[int, TransactionState], Transaction, int]\n\n\nclass CompilerConnectionState:\n    __slots__ = (\"_savepoints_log\", \"_current_tx\", \"_tx_count\", \"_user_schema\")\n\n    # Fields that affects the state key are listed here. The key is used\n    # to determine if we can reuse a previously-pickled state, so remember\n    # to update get_state_key() below when adding new fields affecting the\n    # state key.  See also edb/server/compiler_pool/worker.py\n    _tx_count: int\n    _savepoints_log: dict[int, TransactionState]\n    _current_tx: Transaction\n\n    _user_schema: Optional[s_schema.Schema]\n\n    def __init__(\n        self,\n        *,\n        user_schema: s_schema.Schema,\n        global_schema: s_schema.Schema,\n        modaliases: immutables.Map[Optional[str], str],\n        session_config: immutables.Map[str, config.SettingValue],\n        database_config: immutables.Map[str, config.SettingValue],\n        system_config: immutables.Map[str, config.SettingValue],\n        cached_reflection: immutables.Map[str, tuple[str, ...]],\n    ):\n        self._user_schema = user_schema\n        self._tx_count = time.monotonic_ns()\n        self._init_current_tx(\n            user_schema=user_schema,\n            global_schema=global_schema,\n            modaliases=modaliases,\n            session_config=session_config,\n            database_config=database_config,\n            system_config=system_config,\n            cached_reflection=cached_reflection,\n        )\n        self._savepoints_log = {}\n\n    def get_state_key(self) -> tuple[tuple[int, ...], int, tuple[Any, ...]]:\n        # This would be much more efficient if CompilerConnectionState\n        # and TransactionState objects were immutable. But they are not,\n        # so we have\n        return (\n            tuple(self._savepoints_log.keys()),\n            self._tx_count,\n            self._current_tx.get_state_key(),\n        )\n\n    def __getstate__(self) -> CStateStateType:\n        return self._savepoints_log, self._current_tx, self._tx_count\n\n    def __setstate__(self, state: CStateStateType) -> None:\n        self._savepoints_log, self._current_tx, self._tx_count = state\n        self._user_schema = None\n\n    @property\n    def root_user_schema(self) -> s_schema.Schema:\n        assert self._user_schema is not None\n        return self._user_schema\n\n    def set_root_user_schema(self, user_schema: s_schema.Schema) -> None:\n        self._user_schema = user_schema\n\n    def _new_txid(self) -> int:\n        self._tx_count += 1\n        return self._tx_count\n\n    def _init_current_tx(\n        self,\n        *,\n        user_schema: s_schema.Schema,\n        global_schema: s_schema.Schema,\n        modaliases: immutables.Map[Optional[str], str],\n        session_config: immutables.Map[str, config.SettingValue],\n        database_config: immutables.Map[str, config.SettingValue],\n        system_config: immutables.Map[str, config.SettingValue],\n        cached_reflection: immutables.Map[str, tuple[str, ...]],\n    ) -> None:\n        self._current_tx = Transaction(\n            self,\n            user_schema=user_schema,\n            global_schema=global_schema,\n            modaliases=modaliases,\n            session_config=session_config,\n            database_config=database_config,\n            system_config=system_config,\n            cached_reflection=cached_reflection,\n        )\n\n    def can_sync_to_savepoint(self, spid: int) -> bool:\n        return spid in self._savepoints_log\n\n    def sync_to_savepoint(self, spid: int) -> None:\n        \"\"\"Synchronize the compiler state with the current DB state.\"\"\"\n\n        if not self.can_sync_to_savepoint(spid):\n            raise RuntimeError(f\"failed to lookup savepoint with id={spid}\")\n\n        sp = self._savepoints_log[spid]\n        self._current_tx = sp.tx\n        self._current_tx._current = sp\n        self._current_tx._id = spid\n\n        # Cleanup all savepoints declared after the one we rolled back to\n        # in the transaction we have now set as current.\n        for id in tuple(self._current_tx._savepoints):\n            if id > spid:\n                self._current_tx._savepoints.pop(id)\n\n        # Cleanup all savepoints declared after the one we rolled back to\n        # in the global savepoints log.\n        for id in tuple(self._savepoints_log):\n            if id > spid:\n                self._savepoints_log.pop(id)\n\n    def current_tx(self) -> Transaction:\n        return self._current_tx\n\n    def start_tx(self) -> None:\n        if self._current_tx.is_implicit():\n            self._current_tx.make_explicit()\n        else:\n            raise errors.TransactionError(\"already in transaction\")\n\n    def rollback_tx(self) -> TransactionState:\n        # Note that we might not be in a transaction as we allow\n        # ROLLBACKs outside of transaction blocks (just like Postgres).\n\n        prior_state = self._current_tx._state0\n\n        self._init_current_tx(\n            user_schema=prior_state.user_schema,\n            global_schema=prior_state.global_schema,\n            modaliases=prior_state.modaliases,\n            session_config=prior_state.session_config,\n            database_config=prior_state.database_config,\n            system_config=prior_state.system_config,\n            cached_reflection=prior_state.cached_reflection,\n        )\n\n        return prior_state\n\n    def commit_tx(self) -> TransactionState:\n        if self._current_tx.is_implicit():\n            raise errors.TransactionError(\"cannot commit: not in transaction\")\n\n        latest_state = self._current_tx._current\n\n        self._init_current_tx(\n            user_schema=latest_state.user_schema,\n            global_schema=latest_state.global_schema,\n            modaliases=latest_state.modaliases,\n            session_config=latest_state.session_config,\n            database_config=latest_state.database_config,\n            system_config=latest_state.system_config,\n            cached_reflection=latest_state.cached_reflection,\n        )\n\n        return latest_state\n\n    def sync_tx(self, txid: int) -> None:\n        if self._current_tx.id == txid:\n            return\n\n        if self.can_sync_to_savepoint(txid):\n            self.sync_to_savepoint(txid)\n            return\n\n        raise errors.InternalServerError(\n            f\"failed to lookup transaction or savepoint with id={txid}\"\n        )  # pragma: no cover\n"
  },
  {
    "path": "edb/server/compiler/ddl.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional\n\nimport dataclasses\nimport json\nimport textwrap\nimport uuid\n\nfrom edb import errors\n\nfrom edb import edgeql\nfrom edb.common import debug\nfrom edb.common import ast\nfrom edb.common import uuidgen\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import codegen as qlcodegen\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.edgeql import qltypes\nfrom edb.edgeql import quote as qlquote\n\n\nfrom edb.schema import annos as s_annos\nfrom edb.schema import constraints as s_constraints\nfrom edb.schema import database as s_db\nfrom edb.schema import ddl as s_ddl\nfrom edb.schema import delta as s_delta\nfrom edb.schema import expraliases as s_expraliases\nfrom edb.schema import futures as s_futures\nfrom edb.schema import functions as s_func\nfrom edb.schema import globals as s_globals\nfrom edb.schema import indexes as s_indexes\nfrom edb.schema import links as s_links\nfrom edb.schema import migrations as s_migrations\nfrom edb.schema import objects as s_obj\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import policies as s_policies\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import properties as s_properties\nfrom edb.schema import rewrites as s_rewrites\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import schema as s_schema\nfrom edb.schema import triggers as s_triggers\nfrom edb.schema import utils as s_utils\nfrom edb.schema import version as s_ver\n\nfrom edb.pgsql import common as pg_common\nfrom edb.pgsql import delta as pg_delta\nfrom edb.pgsql import dbops as pg_dbops\n\nfrom . import dbstate\nfrom . import compiler\n\n\nNIL_QUERY = b\"SELECT LIMIT 0\"\n\n\ndef compile_and_apply_ddl_stmt(\n    ctx: compiler.CompileContext,\n    stmt: qlast.DDLCommand,\n    source: Optional[edgeql.Source] = None,\n) -> dbstate.DDLQuery:\n    query, _ = _compile_and_apply_ddl_stmt(ctx, stmt, source)\n    return query\n\n\ndef _compile_and_apply_ddl_stmt(\n    ctx: compiler.CompileContext,\n    stmt: qlast.DDLCommand,\n    source: Optional[edgeql.Source] = None,\n) -> tuple[dbstate.DDLQuery, Optional[pg_dbops.SQLBlock]]:\n    if isinstance(stmt, qlast.GlobalObjectCommand):\n        ctx._assert_not_in_migration_block(stmt)\n\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n\n    mstate = current_tx.get_migration_state()\n    if (\n        mstate is None\n        and not ctx.bootstrap_mode\n        and ctx.log_ddl_as_migrations\n        and not isinstance(\n            stmt,\n            (\n                qlast.CreateMigration,\n                qlast.GlobalObjectCommand,\n                qlast.DropMigration,\n            ),\n        )\n    ):\n        allow_bare_ddl = compiler._get_config_val(ctx, 'allow_bare_ddl')\n        if allow_bare_ddl != \"AlwaysAllow\":\n            raise errors.QueryError(\n                \"bare DDL statements are not allowed on this database branch\",\n                hint=\"Use the migration commands instead.\",\n                details=(\n                    f\"The `allow_bare_ddl` configuration variable \"\n                    f\"is set to {str(allow_bare_ddl)!r}.  The \"\n                    f\"`edgedb migrate` command normally sets this \"\n                    f\"to avoid accidental schema changes outside of \"\n                    f\"the migration flow.\"\n                ),\n                span=stmt.span,\n            )\n        cm = qlast.CreateMigration(  # type: ignore\n            body=qlast.NestedQLBlock(\n                commands=[stmt],\n            ),\n            commands=[\n                qlast.SetField(\n                    name='generated_by',\n                    value=qlast.Path(\n                        steps=[\n                            qlast.ObjectRef(\n                                name='MigrationGeneratedBy', module='schema'\n                            ),\n                            qlast.Ptr(name='DDLStatement'),\n                        ]\n                    ),\n                )\n            ],\n        )\n        return _compile_and_apply_ddl_stmt(ctx, cm)\n\n    assert isinstance(stmt, qlast.DDLCommand)\n    new_schema, delta = s_ddl.delta_and_schema_from_ddl(\n        stmt,\n        schema=schema,\n        modaliases=current_tx.get_modaliases(),\n        **_get_delta_context_args(ctx),\n    )\n\n    if debug.flags.delta_plan:\n        debug.header('Canonical Delta Plan')\n        debug.dump(delta, schema=schema)\n\n    if mstate := current_tx.get_migration_state():\n        mstate = mstate._replace(\n            accepted_cmds=mstate.accepted_cmds + (stmt,),\n        )\n\n        last_proposed = mstate.last_proposed\n        if last_proposed:\n            if last_proposed[0].required_user_input or last_proposed[\n                0\n            ].prompt_id.startswith(\"Rename\"):\n                # Cannot auto-apply the proposed DDL\n                # if user input is required.\n                # Also skip auto-applying for renames, since\n                # renames often force a bunch of rethinking.\n                mstate = mstate._replace(last_proposed=None)\n            else:\n                proposed_stmts = last_proposed[0].statements\n                ddl_script = '\\n'.join(proposed_stmts)\n\n                if source and source.text() == ddl_script:\n                    # The client has confirmed the proposed migration step,\n                    # advance the proposed script.\n                    mstate = mstate._replace(\n                        last_proposed=last_proposed[1:],\n                    )\n                else:\n                    # The client replied with a statement that does not\n                    # match what was proposed, reset the proposed script\n                    # to force script regeneration on next DESCRIBE.\n                    mstate = mstate._replace(last_proposed=None)\n\n        current_tx.update_migration_state(mstate)\n        current_tx.update_schema(new_schema)\n\n        query = dbstate.DDLQuery(\n            sql=NIL_QUERY,\n            user_schema=current_tx.get_user_schema(),\n            is_transactional=True,\n            warnings=tuple(delta.warnings),\n            feature_used_metrics=None,\n        )\n\n        return query, None\n\n    store_migration_sdl = compiler._get_config_val(ctx, 'store_migration_sdl')\n    if (\n        isinstance(stmt, qlast.CreateMigration)\n        and store_migration_sdl == 'AlwaysStore'\n    ):\n        stmt.target_sdl = s_ddl.sdl_text_from_schema(new_schema)\n\n    # If we are in a migration rewrite, we also don't actually\n    # apply the DDL, just record it. (The DDL also needs to be a\n    # CreateMigration.)\n    if mrstate := current_tx.get_migration_rewrite_state():\n        if not isinstance(stmt, qlast.CreateMigration):\n            # This will always fail, and gives us the error we need\n            ctx._assert_not_in_migration_rewrite_block(stmt)\n            # Tell this to the type checker\n            raise AssertionError()\n\n        mrstate = mrstate._replace(\n            accepted_migrations=(mrstate.accepted_migrations + (stmt,))\n        )\n        current_tx.update_migration_rewrite_state(mrstate)\n\n        current_tx.update_schema(new_schema)\n\n        query = dbstate.DDLQuery(\n            sql=NIL_QUERY,\n            user_schema=current_tx.get_user_schema(),\n            is_transactional=True,\n            warnings=tuple(delta.warnings),\n            feature_used_metrics=None,\n        )\n\n        return query, None\n\n    # Apply and adapt delta, build native delta plan, which\n    # will also update the schema.\n    block, new_types, config_ops = _process_delta(ctx, delta)\n\n    ddl_stmt_id: Optional[str] = None\n    is_transactional = block.is_transactional()\n    if not is_transactional:\n        if not isinstance(stmt, qlast.DatabaseCommand):\n            raise AssertionError(\n                f\"unexpected non-transaction DDL command type: {stmt}\")\n        sql_stmts = block.get_statements()\n        sql = sql_stmts[0].encode(\"utf-8\")\n        db_op_trailer = tuple(stmt.encode(\"utf-8\") for stmt in sql_stmts[1:])\n    else:\n        if new_types:\n            # Inject a query returning backend OIDs for the newly\n            # created types.\n            ddl_stmt_id = str(uuidgen.uuid1mc())\n            new_type_ids = [\n                f'{pg_common.quote_literal(tid)}::uuid' for tid in new_types\n            ]\n            # Return newly-added type id mapping via the indirect\n            # return channel (see PGConnection.last_indirect_return)\n            new_types_sql = textwrap.dedent(f\"\"\"\\\n                PERFORM edgedb.indirect_return(\n                    json_build_object(\n                        'ddl_stmt_id',\n                        {pg_common.quote_literal(ddl_stmt_id)},\n                        'new_types',\n                        (SELECT\n                            json_object_agg(\n                                \"id\"::text,\n                                json_build_array(\"backend_id\", \"name\")\n                            )\n                            FROM\n                            edgedb_VER.\"_SchemaType\"\n                            WHERE\n                                \"id\" = any(ARRAY[\n                                    {', '.join(new_type_ids)}\n                                ])\n                        )\n                    )::text\n                )\"\"\"\n            )\n\n            block.add_command(pg_dbops.Query(text=new_types_sql).code())\n\n        sql = block.to_string().encode('utf-8')\n        db_op_trailer = ()\n\n    create_db = None\n    drop_db = None\n    drop_db_reset_connections = False\n    create_db_template = None\n    create_db_mode = None\n    if isinstance(stmt, qlast.DropDatabase):\n        drop_db = stmt.name.name\n        drop_db_reset_connections = stmt.force\n    elif isinstance(stmt, qlast.CreateDatabase):\n        create_db = stmt.name.name\n        create_db_template = stmt.template.name if stmt.template else None\n        create_db_mode = stmt.branch_type\n    elif isinstance(stmt, qlast.AlterDatabase):\n        for cmd in stmt.commands:\n            if isinstance(cmd, qlast.Rename):\n                drop_db = stmt.name.name\n                create_db = cmd.new_name.name\n                drop_db_reset_connections = stmt.force\n\n    if debug.flags.delta_execute_ddl:\n        debug.header('Delta Script (DDL Only)')\n        # The schema updates are always the last statement, so grab\n        # everything but\n        code = '\\n\\n'.join(block.get_statements()[:-1])\n        debug.dump_code(code, lexer='sql')\n    if debug.flags.delta_execute:\n        debug.header('Delta Script')\n        debug.dump_code(sql + b\"\\n\".join(db_op_trailer), lexer='sql')\n\n    new_user_schema = current_tx.get_user_schema_if_updated()\n    query = dbstate.DDLQuery(\n        sql=sql,\n        is_transactional=is_transactional,\n        create_db=create_db,\n        drop_db=drop_db,\n        drop_db_reset_connections=drop_db_reset_connections,\n        create_db_template=create_db_template,\n        create_db_mode=create_db_mode,\n        db_op_trailer=db_op_trailer,\n        ddl_stmt_id=ddl_stmt_id,\n        user_schema=new_user_schema,\n        cached_reflection=current_tx.get_cached_reflection_if_updated(),\n        global_schema=current_tx.get_global_schema_if_updated(),\n        config_ops=config_ops,\n        warnings=tuple(delta.warnings),\n        feature_used_metrics=(\n            produce_feature_used_metrics(ctx.compiler_state, new_user_schema)\n            if new_user_schema else None\n        ),\n    )\n\n    return query, block\n\n\ndef _new_delta_context(\n    ctx: compiler.CompileContext, args: Any = None\n) -> s_delta.CommandContext:\n    return s_delta.CommandContext(\n        backend_runtime_params=ctx.compiler_state.backend_runtime_params,\n        internal_schema_mode=ctx.internal_schema_mode,\n        **(_get_delta_context_args(ctx) if args is None else args),\n    )\n\n\ndef _get_delta_context_args(ctx: compiler.CompileContext) -> dict[str, Any]:\n    \"\"\"Get the args needed for delta_and_schema_from_ddl\"\"\"\n    return dict(\n        stdmode=ctx.bootstrap_mode,\n        testmode=ctx.is_testmode(),\n        store_migration_sdl=(\n            compiler._get_config_val(ctx, 'store_migration_sdl')\n        ) == 'AlwaysStore',\n        schema_object_ids=ctx.schema_object_ids,\n        compat_ver=ctx.compat_ver,\n    )\n\n\ndef _process_delta(\n    ctx: compiler.CompileContext,\n    delta: s_delta.DeltaRoot,\n    context_args: Any = None,\n) -> tuple[pg_dbops.SQLBlock, frozenset[str], Any]:\n    \"\"\"Adapt and process the delta command.\"\"\"\n\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n\n    pgdelta = pg_delta.CommandMeta.adapt(delta)\n    assert isinstance(pgdelta, pg_delta.DeltaRoot)\n    context = _new_delta_context(ctx, context_args)\n    schema = pgdelta.apply(schema, context)\n    current_tx.update_schema(schema)\n\n    if debug.flags.delta_pgsql_plan:\n        debug.header('PgSQL Delta Plan')\n        debug.dump(pgdelta, schema=schema)\n\n    db_cmd = any(\n        isinstance(c, s_db.BranchCommand) for c in pgdelta.get_subcommands()\n    )\n\n    if db_cmd:\n        block = pg_dbops.SQLBlock()\n        new_types: frozenset[str] = frozenset()\n    else:\n        block = pg_dbops.PLTopBlock()\n        new_types = frozenset(str(tid) for tid in pgdelta.new_types)\n\n    # Generate SQL DDL for the delta.\n    pgdelta.generate(block)  # type: ignore\n    # XXX: We would prefer for there to not be trampolines ever after bootstrap\n    pgdelta.create_trampolines.generate(block)  # type: ignore\n\n    # Generate schema storage SQL (DML into schema storage tables).\n    subblock = block.add_block()\n    compiler.compile_schema_storage_in_delta(\n        ctx, pgdelta, subblock, context=context\n    )\n\n    # Performance hack; we really want trivial migration commands\n    # (that only mutate the migration log) to not trigger a pg_catalog\n    # view refresh, since many get issued as part of MIGRATION\n    # REWRITEs.\n    all_migration_tweaks = all(\n        isinstance(\n            cmd, (s_ver.AlterSchemaVersion, s_migrations.MigrationCommand)\n        )\n        and not cmd.get_subcommands(type=s_delta.ObjectCommand)\n        for cmd in delta.get_subcommands()\n    )\n\n    if not ctx.bootstrap_mode and not all_migration_tweaks:\n        from edb.pgsql import metaschema\n        refresh = metaschema.generate_sql_information_schema_refresh(\n            ctx.compiler_state.backend_runtime_params.instance_params.version\n        )\n        refresh.generate(subblock)\n\n    return block, new_types, pgdelta.config_ops\n\n\ndef compile_dispatch_ql_migration(\n    ctx: compiler.CompileContext,\n    ql: qlast.MigrationCommand,\n    *,\n    in_script: bool,\n) -> dbstate.BaseQuery:\n    if ctx.expect_rollback and not isinstance(\n        ql, (qlast.AbortMigration, qlast.AbortMigrationRewrite)\n    ):\n        # Only allow ABORT MIGRATION to pass when expecting a rollback\n        if ctx.state.current_tx().get_migration_state() is None:\n            raise errors.TransactionError(\n                'expected a ROLLBACK or ROLLBACK TO SAVEPOINT command'\n            )\n        else:\n            raise errors.TransactionError(\n                'expected a ROLLBACK or ABORT MIGRATION command'\n            )\n\n    match ql:\n        case qlast.CreateMigration():\n            ctx._assert_not_in_migration_block(ql)\n\n            return compile_and_apply_ddl_stmt(ctx, ql)\n\n        case qlast.StartMigration():\n            return _start_migration(ctx, ql, in_script)\n\n        case qlast.PopulateMigration():\n            return _populate_migration(ctx, ql)\n\n        case qlast.DescribeCurrentMigration():\n            return _describe_current_migration(ctx, ql)\n\n        case qlast.AlterCurrentMigrationRejectProposed():\n            return _alter_current_migration_reject_proposed(ctx, ql)\n\n        case qlast.CommitMigration():\n            return _commit_migration(ctx, ql)\n\n        case qlast.AbortMigration():\n            return _abort_migration(ctx, ql)\n\n        case qlast.DropMigration():\n            ctx._assert_not_in_migration_block(ql)\n\n            return compile_and_apply_ddl_stmt(ctx, ql)\n\n        case qlast.StartMigrationRewrite():\n            return _start_migration_rewrite(ctx, ql, in_script)\n\n        case qlast.CommitMigrationRewrite():\n            return _commit_migration_rewrite(ctx, ql)\n\n        case qlast.AbortMigrationRewrite():\n            return _abort_migration_rewrite(ctx, ql)\n\n        case qlast.ResetSchema():\n            return _reset_schema(ctx, ql)\n\n        case _:\n            raise AssertionError(f'unexpected migration command: {ql}')\n\n\ndef _start_migration(\n    ctx: compiler.CompileContext,\n    ql: qlast.StartMigration,\n    in_script: bool,\n) -> dbstate.BaseQuery:\n    ctx._assert_not_in_migration_block(ql)\n\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n\n    if current_tx.is_implicit() and not in_script:\n        savepoint_name = None\n        tx_cmd = qlast.StartTransaction()\n        tx_query = compiler._compile_ql_transaction(ctx, tx_cmd)\n        query = dbstate.MigrationControlQuery(\n            sql=tx_query.sql,\n            action=dbstate.MigrationAction.START,\n            tx_action=tx_query.action,\n            cacheable=False,\n            modaliases=None,\n        )\n    else:\n        savepoint_name = current_tx.start_migration()\n        query = dbstate.MigrationControlQuery(\n            sql=NIL_QUERY,\n            action=dbstate.MigrationAction.START,\n            tx_action=None,\n            cacheable=False,\n            modaliases=None,\n        )\n\n    if isinstance(ql.target, qlast.CommittedSchema):\n        mrstate = ctx._assert_in_migration_rewrite_block(ql)\n        target_schema = mrstate.target_schema\n\n    else:\n        assert ctx.compiler_state.std_schema is not None\n        base_schema = s_schema.ChainedSchema(\n            ctx.compiler_state.std_schema,\n            s_schema.EMPTY_SCHEMA,\n            current_tx.get_global_schema(),\n        )\n        target_schema, warnings = s_ddl.apply_sdl(\n            ql.target,\n            base_schema=base_schema,\n            testmode=ctx.is_testmode(),\n        )\n\n        if not (\n            s_futures.future_enabled(target_schema, 'simple_scoping')\n            or s_futures.future_enabled(target_schema, 'warn_old_scoping')\n        ):\n            warnings += (\n                errors.DeprecatedScopingError(\n                    f\"\\nSchema does not have 'using future simple_scoping'.\\n\"\n                    f\"Non-simple_scoping will be removed in Gel 8.0.\\n\"\n                    f\"See https://docs.geldata.com/reference/edgeql/\"\n                    f\"path_resolution\\n\"\n                ),\n            )\n\n        query = dataclasses.replace(query, warnings=tuple(warnings))\n\n    current_tx.update_migration_state(\n        dbstate.MigrationState(\n            parent_migration=schema.get_last_migration(),\n            initial_schema=schema,\n            initial_savepoint=savepoint_name,\n            guidance=s_obj.DeltaGuidance(),\n            target_schema=target_schema,\n            accepted_cmds=tuple(),\n            last_proposed=None,\n        ),\n    )\n    return query\n\n\ndef _populate_migration(\n    ctx: compiler.CompileContext,\n    ql: qlast.PopulateMigration,\n) -> dbstate.BaseQuery:\n    mstate = ctx._assert_in_migration_block(ql)\n\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n\n    diff = s_ddl.delta_schemas(\n        schema,\n        mstate.target_schema,\n        guidance=mstate.guidance,\n    )\n    if debug.flags.delta_plan:\n        debug.header('Populate Migration Diff')\n        debug.dump(diff, schema=schema)\n\n    new_ddl: tuple[qlast.DDLCommand, ...] = tuple(\n        s_ddl.ddlast_from_delta(  # type: ignore\n            schema,\n            mstate.target_schema,\n            diff,\n            testmode=ctx.is_testmode(),\n        ),\n    )\n    all_ddl = mstate.accepted_cmds + new_ddl\n    mstate = mstate._replace(\n        accepted_cmds=all_ddl,\n        last_proposed=None,\n    )\n    if debug.flags.delta_plan:\n        debug.header('Populate Migration DDL AST')\n        text = []\n        for cmd in new_ddl:\n            debug.dump(cmd)\n            text.append(qlcodegen.generate_source(cmd, pretty=True))\n        debug.header('Populate Migration DDL Text')\n        debug.dump_code(';\\n'.join(text) + ';')\n    current_tx.update_migration_state(mstate)\n\n    delta_context = _new_delta_context(ctx)\n\n    # We want to make *certain* that the DDL we generate\n    # produces the correct schema when applied, so we reload\n    # the diff from the AST instead of just relying on the\n    # delta tree. We do this check because it is *very\n    # important* that we not emit DDL that moves the schema\n    # into the wrong state.\n    #\n    # The actual check for whether the schema matches is done\n    # by DESCRIBE CURRENT MIGRATION AS JSON, to populate the\n    # 'complete' flag.\n    if debug.flags.delta_plan:\n        debug.header('Populate Migration Applied Diff')\n    for cmd in new_ddl:\n        reloaded_diff = s_ddl.delta_from_ddl(\n            cmd,\n            schema=schema,\n            modaliases=current_tx.get_modaliases(),\n            **_get_delta_context_args(ctx),\n        )\n        schema = reloaded_diff.apply(schema, delta_context)\n        if debug.flags.delta_plan:\n            debug.dump(reloaded_diff, schema=schema)\n\n    current_tx.update_schema(schema)\n\n    return dbstate.MigrationControlQuery(\n        sql=NIL_QUERY,\n        tx_action=None,\n        action=dbstate.MigrationAction.POPULATE,\n        cacheable=False,\n        modaliases=None,\n    )\n\n\ndef _describe_current_migration(\n    ctx: compiler.CompileContext,\n    ql: qlast.DescribeCurrentMigration,\n) -> dbstate.BaseQuery:\n    mstate = ctx._assert_in_migration_block(ql)\n\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n\n    if ql.language is qltypes.DescribeLanguage.DDL:\n        text = []\n        for stmt in mstate.accepted_cmds:\n            # Generate uppercase DDL commands for backwards\n            # compatibility with older migration text.\n            text.append(\n                qlcodegen.generate_source(stmt, pretty=True, uppercase=True)\n            )\n\n        if text:\n            description = ';\\n'.join(text) + ';'\n        else:\n            description = ''\n\n        desc_ql = edgeql.parse_query(\n            f'SELECT {qlquote.quote_literal(description)}')\n        return compiler._compile_ql_query(\n            ctx,\n            desc_ql,\n            cacheable=False,\n            migration_block_query=True,\n        )\n\n    if ql.language is qltypes.DescribeLanguage.JSON:\n        confirmed = []\n        for stmt in mstate.accepted_cmds:\n            confirmed.append(\n                # Add a terminating semicolon to match\n                # \"proposed\", which is created by\n                # s_ddl.statements_from_delta.\n                #\n                # Also generate uppercase DDL commands for\n                # backwards compatibility with older migration\n                # text.\n                qlcodegen.generate_source(stmt, pretty=True, uppercase=True)\n                + ';',\n            )\n\n        if mstate.last_proposed is None:\n            guided_diff = s_ddl.delta_schemas(\n                schema,\n                mstate.target_schema,\n                generate_prompts=True,\n                guidance=mstate.guidance,\n            )\n            if debug.flags.delta_plan:\n                debug.header('DESCRIBE CURRENT MIGRATION AS JSON delta')\n                debug.dump(guided_diff)\n\n            proposed_ddl = s_ddl.statements_from_delta(\n                schema, mstate.target_schema, guided_diff, uppercase=True\n            )\n            proposed_steps = []\n\n            if proposed_ddl:\n                for ddl_text, ddl_ast, top_op in proposed_ddl:\n                    assert isinstance(top_op, s_delta.ObjectCommand)\n\n                    # get_ast has a lot of logic for figuring\n                    # out when an op is implicit in a parent\n                    # op. get_user_prompt does not have any of\n                    # that sort of logic, which makes it\n                    # susceptible to producing overly broad\n                    # messages. To avoid duplicating that sort\n                    # of logic, we recreate the delta from the\n                    # AST, and extract a user prompt from\n                    # *that*.\n                    # This is stupid, and it is slow.\n                    top_op2 = s_ddl.cmd_from_ddl(\n                        ddl_ast,\n                        schema=schema,\n                        modaliases=current_tx.get_modaliases(),\n                    )\n                    assert isinstance(top_op2, s_delta.ObjectCommand)\n                    prompt_key2, prompt_text = top_op2.get_user_prompt()\n\n                    # Similarly, some placeholders may not have made\n                    # it into the actual query, so filter them out.\n                    used_placeholders = {\n                        p.name\n                        for p in ast.find_children(ddl_ast, qlast.Placeholder)\n                    }\n                    required_user_input = tuple(\n                        inp\n                        for inp in top_op.get_required_user_input()\n                        if inp['placeholder'] in used_placeholders\n                    )\n\n                    # The prompt_id still needs to come from\n                    # the original op, though, since\n                    # orig_cmd_class is lost in ddl.\n                    prompt_key, _ = top_op.get_user_prompt()\n                    prompt_id = s_delta.get_object_command_id(prompt_key)\n                    confidence = top_op.get_annotation('confidence')\n                    assert confidence is not None\n\n                    step = dbstate.ProposedMigrationStep(\n                        statements=(ddl_text,),\n                        confidence=confidence,\n                        prompt=prompt_text,\n                        prompt_id=prompt_id,\n                        data_safe=top_op.is_data_safe(),\n                        required_user_input=required_user_input,\n                        operation_key=prompt_key2,\n                    )\n                    proposed_steps.append(step)\n\n                proposed_desc = proposed_steps[0].to_json()\n            else:\n                proposed_desc = None\n\n            mstate = mstate._replace(\n                last_proposed=tuple(proposed_steps),\n            )\n\n            current_tx.update_migration_state(mstate)\n        else:\n            if mstate.last_proposed:\n                proposed_desc = mstate.last_proposed[0].to_json()\n            else:\n                proposed_desc = None\n\n        extra = {}\n\n        complete = False\n        if proposed_desc is None:\n            diff = s_ddl.delta_schemas(schema, mstate.target_schema)\n            complete = not bool(diff.get_subcommands())\n            if debug.flags.delta_plan and not complete:\n                debug.header('DESCRIBE CURRENT MIGRATION AS JSON mismatch')\n                debug.dump(diff)\n            if not complete:\n                extra['debug_diff'] = debug.dumps(diff)\n\n        desc = (\n            json.dumps(\n                {\n                    'parent': (\n                        str(mstate.parent_migration.get_name(schema))\n                        if mstate.parent_migration is not None\n                        else 'initial'\n                    ),\n                    'complete': complete,\n                    'confirmed': confirmed,\n                    'proposed': proposed_desc,\n                    **extra,\n                }\n            )\n        )\n\n        desc_ql = edgeql.parse_query(\n            f'SELECT to_json({qlquote.quote_literal(desc)})'\n        )\n        return compiler._compile_ql_query(\n            ctx,\n            desc_ql,\n            cacheable=False,\n            migration_block_query=True,\n        )\n\n    raise AssertionError(\n        f'DESCRIBE CURRENT MIGRATION AS {ql.language}' f' is not implemented'\n    )\n\n\ndef _alter_current_migration_reject_proposed(\n    ctx: compiler.CompileContext,\n    ql: qlast.AlterCurrentMigrationRejectProposed,\n) -> dbstate.BaseQuery:\n    mstate = ctx._assert_in_migration_block(ql)\n\n    current_tx = ctx.state.current_tx()\n\n    if not mstate.last_proposed:\n        # XXX: Or should we compute what the proposal would be?\n        new_guidance = mstate.guidance\n    else:\n        last = mstate.last_proposed[0]\n        cmdclass_name, mcls, classname, new_name = last.operation_key\n        if new_name is None:\n            new_name = classname\n\n        if cmdclass_name.startswith('Create'):\n            new_guidance = mstate.guidance._replace(\n                banned_creations=mstate.guidance.banned_creations\n                | {\n                    (mcls, classname),\n                }\n            )\n        elif cmdclass_name.startswith('Delete'):\n            new_guidance = mstate.guidance._replace(\n                banned_deletions=mstate.guidance.banned_deletions\n                | {\n                    (mcls, classname),\n                }\n            )\n        else:\n            new_guidance = mstate.guidance._replace(\n                banned_alters=mstate.guidance.banned_alters\n                | {\n                    (mcls, (classname, new_name)),\n                }\n            )\n\n    mstate = mstate._replace(\n        guidance=new_guidance,\n        last_proposed=None,\n    )\n    current_tx.update_migration_state(mstate)\n\n    return dbstate.MigrationControlQuery(\n        sql=NIL_QUERY,\n        tx_action=None,\n        action=dbstate.MigrationAction.REJECT_PROPOSED,\n        cacheable=False,\n        modaliases=None,\n    )\n\n\ndef _commit_migration(\n    ctx: compiler.CompileContext,\n    ql: qlast.CommitMigration,\n) -> dbstate.BaseQuery:\n    mstate = ctx._assert_in_migration_block(ql)\n\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n\n    diff = s_ddl.delta_schemas(schema, mstate.target_schema)\n    if list(diff.get_subcommands()):\n        raise errors.QueryError(\n            'cannot commit incomplete migration',\n            hint=(\n                'Please finish the migration by specifying the'\n                ' remaining DDL operations or run POPULATE MIGRATION'\n                ' to let the system populate the outstanding DDL'\n                ' automatically.'\n            ),\n            span=ql.span,\n        )\n\n    if debug.flags.delta_plan:\n        debug.header('Commit Migration DDL AST')\n        text = []\n        for cmd in mstate.accepted_cmds:\n            debug.dump(cmd)\n            text.append(qlcodegen.generate_source(cmd, pretty=True))\n        debug.header('Commit Migration DDL Text')\n        debug.dump_code(';\\n'.join(text) + ';')\n\n    last_migration = schema.get_last_migration()\n    if last_migration:\n        last_migration_ref = s_utils.name_to_ast_ref(\n            last_migration.get_name(schema),\n        )\n    else:\n        last_migration_ref = None\n\n    target_sdl: Optional[str] = None\n    store_migration_sdl = compiler._get_config_val(ctx, 'store_migration_sdl')\n    if store_migration_sdl == 'AlwaysStore':\n        target_sdl = s_ddl.sdl_text_from_schema(schema)\n\n    create_migration = qlast.CreateMigration(  # type: ignore\n        body=qlast.NestedQLBlock(\n            commands=mstate.accepted_cmds  # type: ignore\n        ),\n        parent=last_migration_ref,\n        target_sdl=target_sdl,\n    )\n\n    current_tx.update_schema(mstate.initial_schema)\n    current_tx.update_migration_state(None)\n\n    # If we are in a migration rewrite, don't actually apply\n    # the change, just record it.\n    if mrstate := current_tx.get_migration_rewrite_state():\n        current_tx.update_schema(mstate.target_schema)\n        mrstate = mrstate._replace(\n            accepted_migrations=(\n                mrstate.accepted_migrations + (create_migration,)\n            )\n        )\n        current_tx.update_migration_rewrite_state(mrstate)\n\n        return dbstate.MigrationControlQuery(\n            sql=NIL_QUERY,\n            action=dbstate.MigrationAction.COMMIT,\n            tx_action=None,\n            cacheable=False,\n            modaliases=None,\n        )\n\n    current_tx.update_schema(mstate.initial_schema)\n    current_tx.update_migration_state(None)\n\n    ddl_query = compile_and_apply_ddl_stmt(\n        ctx,\n        create_migration,\n    )\n\n    if mstate.initial_savepoint:\n        current_tx.commit_migration(mstate.initial_savepoint)\n        tx_action = None\n    else:\n        tx_action = dbstate.TxAction.COMMIT\n\n    return dbstate.MigrationControlQuery(\n        sql=ddl_query.sql,\n        ddl_stmt_id=ddl_query.ddl_stmt_id,\n        action=dbstate.MigrationAction.COMMIT,\n        tx_action=tx_action,\n        cacheable=False,\n        modaliases=None,\n        user_schema=ctx.state.current_tx().get_user_schema(),\n        cached_reflection=(current_tx.get_cached_reflection_if_updated()),\n    )\n\n\ndef _abort_migration(\n    ctx: compiler.CompileContext,\n    ql: qlast.AbortMigration,\n) -> dbstate.BaseQuery:\n    mstate = ctx._assert_in_migration_block(ql)\n\n    current_tx = ctx.state.current_tx()\n\n    if mstate.initial_savepoint:\n        current_tx.abort_migration(mstate.initial_savepoint)\n        sql = NIL_QUERY\n        tx_action = None\n    else:\n        tx_cmd = qlast.RollbackTransaction()\n        tx_query = compiler._compile_ql_transaction(ctx, tx_cmd)\n        sql = tx_query.sql\n        tx_action = tx_query.action\n\n    current_tx.update_migration_state(None)\n    return dbstate.MigrationControlQuery(\n        sql=sql,\n        action=dbstate.MigrationAction.ABORT,\n        tx_action=tx_action,\n        cacheable=False,\n        modaliases=None,\n    )\n\n\ndef _start_migration_rewrite(\n    ctx: compiler.CompileContext,\n    ql: qlast.StartMigrationRewrite,\n    in_script: bool,\n) -> dbstate.BaseQuery:\n    ctx._assert_not_in_migration_block(ql)\n    ctx._assert_not_in_migration_rewrite_block(ql)\n\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n\n    # Start a transaction if we aren't in one already\n    if current_tx.is_implicit() and not in_script:\n        savepoint_name = None\n        tx_cmd = qlast.StartTransaction()\n        tx_query = compiler._compile_ql_transaction(ctx, tx_cmd)\n        query = dbstate.MigrationControlQuery(\n            sql=tx_query.sql,\n            action=dbstate.MigrationAction.START,\n            tx_action=tx_query.action,\n            cacheable=False,\n            modaliases=None,\n        )\n    else:\n        savepoint_name = current_tx.start_migration()\n        query = dbstate.MigrationControlQuery(\n            sql=NIL_QUERY,\n            action=dbstate.MigrationAction.START,\n            tx_action=None,\n            cacheable=False,\n            modaliases=None,\n        )\n\n        # Start from an empty schema except for `module default`\n    base_schema = s_schema.ChainedSchema(\n        ctx.compiler_state.std_schema,\n        s_schema.EMPTY_SCHEMA,\n        current_tx.get_global_schema(),\n    )\n    new_base_schema, _ = s_ddl.apply_sdl(\n        qlast.Schema(\n            declarations=[\n                qlast.ModuleDeclaration(\n                    name=qlast.ObjectRef(name='default'),\n                    declarations=[],\n                )\n            ]\n        ),\n        base_schema=base_schema,\n    )\n\n    # Set our current schema to be the empty one\n    current_tx.update_schema(new_base_schema)\n    current_tx.update_migration_rewrite_state(\n        dbstate.MigrationRewriteState(\n            target_schema=schema,\n            initial_savepoint=savepoint_name,\n            accepted_migrations=tuple(),\n        ),\n    )\n\n    return query\n\n\ndef _commit_migration_rewrite(\n    ctx: compiler.CompileContext,\n    ql: qlast.CommitMigrationRewrite,\n) -> dbstate.BaseQuery:\n    ctx._assert_not_in_migration_block(ql)\n    mrstate = ctx._assert_in_migration_rewrite_block(ql)\n\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n\n    diff = s_ddl.delta_schemas(schema, mrstate.target_schema)\n    if list(diff.get_subcommands()):\n        if debug.flags.delta_plan:\n            debug.header(\"COMMIT MIGRATION REWRITE mismatch\")\n            diff.dump()\n        raise errors.QueryError(\n            'cannot commit migration rewrite: schema resulting '\n            'from rewrite does not match committed schema',\n            span=ql.span,\n        )\n\n    schema = mrstate.target_schema\n    current_tx.update_schema(schema)\n    current_tx.update_migration_rewrite_state(None)\n\n    cmds: list[qlast.DDLCommand] = []\n    # Now we find all the migrations...\n    migrations = s_migrations.get_ordered_migrations(schema)\n    for mig in reversed(migrations):\n        cmds.append(\n            qlast.DropMigration(\n                name=qlast.ObjectRef(name=mig.get_name(schema).name)\n            )\n        )\n    for acc_cmd in mrstate.accepted_migrations:\n        acc_cmd.metadata_only = True\n        cmds.append(acc_cmd)\n\n    if debug.flags.delta_plan:\n        debug.header('COMMIT MIGRATION REWRITE DDL text')\n        for cm in cmds:\n            cm.dump_edgeql()\n\n    block = pg_dbops.PLTopBlock()\n    for cmd in cmds:\n        _, ddl_block = _compile_and_apply_ddl_stmt(ctx, cmd)\n        assert isinstance(ddl_block, pg_dbops.PLBlock)\n        # We know nothing serious can be in that query\n        # except for the SQL, so it's fine to just discard\n        # it all.\n        for stmt in ddl_block.get_statements():\n            block.add_command(stmt)\n\n    if mrstate.initial_savepoint:\n        current_tx.commit_migration(mrstate.initial_savepoint)\n        tx_action = None\n    else:\n        tx_action = dbstate.TxAction.COMMIT\n\n    return dbstate.MigrationControlQuery(\n        sql=block.to_string().encode(\"utf-8\"),\n        action=dbstate.MigrationAction.COMMIT,\n        tx_action=tx_action,\n        cacheable=False,\n        modaliases=None,\n        user_schema=ctx.state.current_tx().get_user_schema(),\n        cached_reflection=(current_tx.get_cached_reflection_if_updated()),\n    )\n\n\ndef _abort_migration_rewrite(\n    ctx: compiler.CompileContext,\n    ql: qlast.AbortMigrationRewrite,\n) -> dbstate.BaseQuery:\n    mrstate = ctx._assert_in_migration_rewrite_block(ql)\n\n    current_tx = ctx.state.current_tx()\n\n    if mrstate.initial_savepoint:\n        current_tx.abort_migration(mrstate.initial_savepoint)\n        sql = NIL_QUERY\n        tx_action = None\n    else:\n        tx_cmd = qlast.RollbackTransaction()\n        tx_query = compiler._compile_ql_transaction(ctx, tx_cmd)\n        sql = tx_query.sql\n        tx_action = tx_query.action\n\n    current_tx.update_migration_state(None)\n    current_tx.update_migration_rewrite_state(None)\n    query = dbstate.MigrationControlQuery(\n        sql=sql,\n        action=dbstate.MigrationAction.ABORT,\n        tx_action=tx_action,\n        cacheable=False,\n        modaliases=None,\n    )\n\n    return query\n\n\ndef _reset_schema(\n    ctx: compiler.CompileContext,\n    ql: qlast.ResetSchema,\n) -> dbstate.BaseQuery:\n    ctx._assert_not_in_migration_block(ql)\n    ctx._assert_not_in_migration_rewrite_block(ql)\n\n    if ql.target.name != 'initial':\n        raise errors.QueryError(\n            f'Unknown schema version \"{ql.target.name}\". '\n            'Currently, only revision supported is \"initial\"',\n            span=ql.target.span,\n        )\n\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n\n    empty_schema = s_schema.ChainedSchema(\n        ctx.compiler_state.std_schema,\n        s_schema.EMPTY_SCHEMA,\n        current_tx.get_global_schema(),\n    )\n    empty_schema, _ = s_ddl.apply_sdl(  # type: ignore\n        qlast.Schema(\n            declarations=[\n                qlast.ModuleDeclaration(\n                    name=qlast.ObjectRef(name='default'),\n                    declarations=[],\n                )\n            ]\n        ),\n        base_schema=empty_schema,\n    )\n\n    # diff and create migration that drops all objects\n    diff = s_ddl.delta_schemas(schema, empty_schema)\n    new_ddl: tuple[qlast.DDLCommand, ...] = tuple(\n        s_ddl.ddlast_from_delta(schema, empty_schema, diff),  # type: ignore\n    )\n    create_mig = qlast.CreateMigration(  # type: ignore\n        body=qlast.NestedQLBlock(commands=tuple(new_ddl)),  # type: ignore\n    )\n    ddl_query, ddl_block = _compile_and_apply_ddl_stmt(ctx, create_mig)\n    assert ddl_block is not None\n\n    # delete all migrations\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n\n    migrations = s_delta.sort_by_cross_refs(\n        schema,\n        schema.get_objects(type=s_migrations.Migration),\n    )\n    for mig in migrations:\n        drop_mig = qlast.DropMigration(\n            name=qlast.ObjectRef(name=mig.get_name(schema).name),\n        )\n        _, mig_block = _compile_and_apply_ddl_stmt(ctx, drop_mig)\n        assert isinstance(mig_block, pg_dbops.PLBlock)\n        for stmt in mig_block.get_statements():\n            ddl_block.add_command(stmt)\n\n    return dbstate.MigrationControlQuery(\n        sql=ddl_block.to_string().encode(\"utf-8\"),\n        ddl_stmt_id=ddl_query.ddl_stmt_id,\n        action=dbstate.MigrationAction.COMMIT,\n        tx_action=None,\n        cacheable=False,\n        modaliases=None,\n        user_schema=current_tx.get_user_schema(),\n        cached_reflection=(current_tx.get_cached_reflection_if_updated()),\n    )\n\n\n_FEATURE_NAMES: dict[type[s_obj.Object], str] = {\n    s_annos.AnnotationValue: 'annotation',\n    s_policies.AccessPolicy: 'policy',\n    s_triggers.Trigger: 'trigger',\n    s_rewrites.Rewrite: 'rewrite',\n    s_globals.Global: 'global',\n    s_expraliases.Alias: 'alias',\n    s_func.Function: 'function',\n    s_indexes.Index: 'index',\n    s_scalars.ScalarType: 'scalar',\n    s_migrations.Migration: 'migration',\n}\n\n\ndef produce_feature_used_metrics(\n    compiler_state: compiler.CompilerState,\n    user_schema: s_schema.Schema,\n) -> dict[str, float]:\n    schema = s_schema.ChainedSchema(\n        compiler_state.std_schema,\n        user_schema,\n        # Skipping global schema is a little dodgy but not that bad\n        s_schema.EMPTY_SCHEMA,\n    )\n\n    features: dict[str, float] = {}\n\n    def _track(key: str) -> None:\n        features[key] = features.get(key, 0) + 1\n\n    # TODO(perf): Should we optimize peeking into the innards directly\n    # so we can skip creating the proxies?\n    for obj in user_schema.get_objects(\n        type=s_obj.Object, exclude_extensions=True,\n    ):\n        typ = type(obj)\n        if (key := _FEATURE_NAMES.get(typ)):\n            _track(key)\n\n        if isinstance(obj, s_globals.Global) and obj.get_expr(user_schema):\n            _track('computed_global')\n        elif (\n            isinstance(obj, s_properties.Property)\n        ):\n            if obj.get_expr(user_schema):\n                _track('computed_property')\n            elif obj.get_cardinality(schema).is_multi():\n                _track('multi_property')\n\n            if (\n                obj.is_link_property(schema)\n                and not obj.is_special_pointer(schema)\n            ):\n                _track('link_property')\n        elif (\n            isinstance(obj, s_links.Link)\n            and obj.get_expr(user_schema)\n        ):\n            _track('computed_link')\n        elif (\n            isinstance(obj, s_indexes.Index)\n            and s_indexes.is_fts_index(schema, obj)\n        ):\n            _track('fts')\n        elif (\n            isinstance(obj, s_constraints.Constraint)\n            and not (\n                (subject := obj.get_subject(schema))\n                and isinstance(subject, s_properties.Property)\n                and subject.is_special_pointer(schema)\n            )\n        ):\n            _track('constraint')\n            exclusive_constr = schema.get(\n                'std::exclusive', type=s_constraints.Constraint\n            )\n            if not obj.issubclass(schema, exclusive_constr):\n                _track('constraint_expr')\n        elif (\n            isinstance(obj, s_objtypes.ObjectType)\n            and len(obj.get_bases(schema).objects(schema)) > 1\n        ):\n            _track('multiple_inheritance')\n        elif (\n            isinstance(obj, s_objtypes.ObjectType)\n            and obj.is_material_object_type(schema)\n        ):\n            _track('object_type')\n        elif (\n            isinstance(obj, s_scalars.ScalarType)\n            and obj.is_enum(schema)\n        ):\n            _track('enum')\n\n    return features\n\n\ndef repair_schema(\n    ctx: compiler.CompileContext,\n) -> Optional[tuple[bytes, Any]]:\n    \"\"\"Repair inconsistencies in the schema caused by bug fixes\n\n    Works by comparing the actual current schema to the schema we get\n    from reloading the DDL description of the schema and then directly\n    applying the diff.\n    \"\"\"\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n\n    empty_schema = s_schema.ChainedSchema(\n        ctx.compiler_state.std_schema,\n        s_schema.EMPTY_SCHEMA,\n        current_tx.get_global_schema(),\n    )\n\n    context_args = _get_delta_context_args(ctx)\n    context_args.update(dict(\n        testmode=True,\n    ))\n\n    text = s_ddl.ddl_text_from_schema(schema)\n    reloaded_schema, _ = s_ddl.apply_ddl_script_ex(\n        text,\n        schema=empty_schema,\n        **context_args,\n    )\n\n    delta = s_ddl.delta_schemas(\n        schema,\n        reloaded_schema,\n    )\n    mismatch = bool(delta.get_subcommands())\n    if not mismatch:\n        return None\n\n    if debug.flags.delta_plan:\n        debug.header('Repair Delta')\n        debug.dump(delta)\n\n    if not delta.is_data_safe():\n        raise AssertionError(\n            'Repair script for version upgrade is not data safe'\n        )\n\n    # Update the schema version also\n    context = _new_delta_context(ctx, context_args)\n    ver = schema.get_global(\n        s_ver.SchemaVersion, '__schema_version__')\n    reloaded_schema = ver.set_field_value(\n        reloaded_schema, 'version', ver.get_version(schema))\n    ver_cmd = ver.init_delta_command(schema, s_delta.AlterObject)\n    ver_cmd.set_attribute_value('version', uuidgen.uuid1mc())\n    reloaded_schema = ver_cmd.apply(reloaded_schema, context)\n    delta.add(ver_cmd)\n\n    # Apply and adapt delta, build native delta plan, which\n    # will also update the schema.\n    block, new_types, config_ops = _process_delta(ctx, delta, context_args)\n    is_transactional = block.is_transactional()\n    assert not new_types\n    assert is_transactional\n    sql = block.to_string().encode('utf-8')\n\n    if debug.flags.delta_execute:\n        debug.header('Repair Delta Script')\n        debug.dump_code(sql, lexer='sql')\n\n    return sql, config_ops\n\n\ndef administer_repair_schema(\n    ctx: compiler.CompileContext,\n    ql: qlast.AdministerStmt,\n) -> dbstate.BaseQuery:\n    if ql.expr.args or ql.expr.kwargs:\n        raise errors.QueryError(\n            'repair_schema() does not take arguments',\n            span=ql.expr.span,\n        )\n\n    current_tx = ctx.state.current_tx()\n\n    res = repair_schema(ctx)\n    if not res:\n        return dbstate.MaintenanceQuery(sql=b\"\")\n    sql, config_ops = res\n\n    return dbstate.DDLQuery(\n        sql=sql,\n        user_schema=current_tx.get_user_schema_if_updated(),\n        global_schema=current_tx.get_global_schema_if_updated(),\n        config_ops=config_ops,\n        feature_used_metrics=None,\n    )\n\n\ndef administer_fixup_backend_upgrade(\n    ctx: compiler.CompileContext,\n    ql: qlast.AdministerStmt,\n) -> dbstate.BaseQuery:\n    if ql.expr.args or ql.expr.kwargs:\n        raise errors.QueryError(\n            'fixup_backend_upgrade() does not take arguments',\n            span=ql.expr.span,\n        )\n\n    from edb.pgsql import metaschema\n\n    block = pg_dbops.PLTopBlock()\n\n    cmds = metaschema._generate_sql_information_schema(\n        ctx.compiler_state.backend_runtime_params.instance_params.version\n    )\n    metaschema.generate_drop_views(cmds, block)\n\n    cmd_group = pg_dbops.CommandGroup()\n    cmd_group.add_commands(cmds)\n    cmd_group.generate(block)\n\n    assert block.is_transactional()\n\n    return dbstate.DDLQuery(\n        sql=block.to_string().encode('utf-8'),\n        user_schema=None,\n        feature_used_metrics=None,\n    )\n\n\ndef remove_pointless_triggers(\n    schema: s_schema.Schema,\n) -> pg_dbops.CommandGroup:\n    from edb.pgsql import schemamech\n\n    constraints = schema.get_objects(\n        exclude_stdlib=True,\n        type=s_constraints.Constraint,\n    )\n\n    cmds = pg_dbops.CommandGroup()\n\n    for constraint in constraints:\n        if not pg_delta.ConstraintCommand.constraint_is_effective(\n            schema, constraint\n        ):\n            continue\n\n        subject = constraint.get_subject(schema)\n        bconstr = schemamech.compile_constraint(\n            subject, constraint, schema, None\n        )\n\n        # Q: we could also use update_trigger_ops, which would\n        # generate more useless code but avoid the need for an extra\n        # code path?\n        cmds.add_command(bconstr.fixup_trigger_ops())\n\n    return cmds\n\n\ndef administer_remove_pointless_triggers(\n    ctx: compiler.CompileContext,\n    ql: qlast.AdministerStmt,\n) -> dbstate.BaseQuery:\n    if ql.expr.args or ql.expr.kwargs:\n        raise errors.QueryError(\n            '_remove_pointless_triggers() does not take arguments',\n            span=ql.expr.span,\n        )\n    if not ctx.is_testmode():\n        raise errors.QueryError(\n            '_remove_pointless_triggers() is for testmode only',\n            span=ql.expr.span,\n        )\n\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n\n    block = pg_dbops.PLTopBlock()\n    remove_pointless_triggers(schema).generate(block)\n    src = block.to_string()\n\n    if debug.flags.delta_execute_ddl or debug.flags.delta_execute:\n        debug.header('remove_pointless_triggers')\n        debug.dump_code(src, lexer='sql')\n\n    return dbstate.DDLQuery(\n        sql=src.encode('utf-8'),\n        user_schema=ctx.state.current_tx().get_user_schema(),\n        feature_used_metrics=None,\n    )\n\n\ndef administer_reindex(\n    ctx: compiler.CompileContext,\n    ql: qlast.AdministerStmt,\n) -> dbstate.BaseQuery:\n    from edb.ir import ast as irast\n    from edb.ir import typeutils as irtypeutils\n    from edb.ir import utils as irutils\n\n    from edb.schema import objtypes as s_objtypes\n    from edb.schema import constraints as s_constraints\n    from edb.schema import indexes as s_indexes\n\n    if len(ql.expr.args) != 1 or ql.expr.kwargs:\n        raise errors.QueryError(\n            'reindex() takes exactly one position argument',\n            span=ql.expr.span,\n        )\n\n    arg = ql.expr.args[0]\n    match arg:\n        case qlast.Path(\n            steps=[qlast.ObjectRef()],\n            partial=False,\n        ):\n            ptr = False\n        case qlast.Path(\n            steps=[qlast.ObjectRef(), qlast.Ptr()],\n            partial=False,\n        ):\n            ptr = True\n        case _:\n            raise errors.QueryError(\n                'argument to reindex() must be an object type',\n                span=arg.span,\n            )\n\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n    modaliases = current_tx.get_modaliases()\n\n    ir: irast.Statement = qlcompiler.compile_ast_to_ir(\n        arg,\n        schema=schema,\n        options=qlcompiler.CompilerOptions(\n            modaliases=modaliases\n        ),\n    )\n    expr = irutils.unwrap_set(ir.expr)\n    if ptr:\n        if (\n            not expr.expr\n            or not isinstance(expr.expr, irast.Pointer)\n        ):\n            raise errors.QueryError(\n                'invalid pointer argument to reindex()',\n                span=arg.span,\n            )\n        rptr = expr.expr\n        source = rptr.source\n    else:\n        rptr = None\n        source = expr\n    schema, obj = irtypeutils.ir_typeref_to_type(schema, source.typeref)\n\n    if (\n        not isinstance(obj, s_objtypes.ObjectType)\n        or not obj.is_material_object_type(schema)\n    ):\n        raise errors.QueryError(\n            'argument to reindex() must be a regular object type',\n            span=arg.span,\n        )\n\n    tables: set[s_pointers.Pointer | s_objtypes.ObjectType] = set()\n    pindexes: set[\n        s_constraints.Constraint | s_indexes.Index | s_pointers.Pointer\n    ] = set()\n\n    commands = []\n    if not rptr:\n        # On a type, we just reindex the type and its descendants\n        tables.update({obj} | {\n            desc for desc in obj.descendants(schema)\n            if desc.is_material_object_type(schema)\n        })\n    else:\n        # On a pointer, we reindex any indexes and constraints, as well as\n        # any link indexes (which might be table indexes on a link table)\n        if not isinstance(rptr.ptrref, irast.PointerRef):\n            raise errors.QueryError(\n                'invalid pointer argument to reindex()',\n                span=arg.span,\n            )\n        schema, ptrcls = irtypeutils.ptrcls_from_ptrref(\n            rptr.ptrref, schema=schema)\n\n        indexes = set(schema.get_referrers(ptrcls, scls_type=s_indexes.Index))\n\n        exclusive = schema.get('std::exclusive', type=s_constraints.Constraint)\n        constrs = {\n            c for c in\n            schema.get_referrers(ptrcls, scls_type=s_constraints.Constraint)\n            if c.issubclass(schema, exclusive)\n        }\n\n        pindexes.update(indexes | constrs)\n        pindexes.update({\n            desc for pindex in pindexes for desc in pindex.descendants(schema)\n        })\n\n        # For links, collect any single link indexes and any link table indexes\n        if not ptrcls.is_property():\n            ptrclses = {ptrcls} | {\n                desc for desc in ptrcls.descendants(schema)\n                if isinstance(\n                    (src := desc.get_source(schema)), s_objtypes.ObjectType)\n                and src.is_material_object_type(schema)\n            }\n\n            card = ptrcls.get_cardinality(schema)\n            if card.is_single():\n                pindexes.update(ptrclses)\n            if card.is_multi() or ptrcls.has_user_defined_properties(schema):\n                tables.update(ptrclses)\n\n    commands = [\n        f'REINDEX TABLE '\n        f'{pg_common.get_backend_name(schema, table)};'\n        for table in tables\n    ] + [\n        f'REINDEX INDEX '\n        f'{pg_common.get_backend_name(schema, pindex, aspect=\"index\")};'\n        for pindex in pindexes\n    ]\n\n    block = pg_dbops.PLTopBlock()\n    for command in commands:\n        block.add_command(command)\n\n    return dbstate.MaintenanceQuery(sql=block.to_string().encode(\"utf-8\"))\n\n\ndef _identify_administer_tables_and_cols(\n    ctx: compiler.CompileContext,\n    call: qlast.FunctionCall,\n) -> list[str]:\n    from edb.ir import ast as irast\n    from edb.ir import typeutils as irtypeutils\n    from edb.schema import objtypes as s_objtypes\n\n    args: list[tuple[irast.Pointer | None, s_objtypes.ObjectType]] = []\n    current_tx = ctx.state.current_tx()\n    schema = current_tx.get_schema(ctx.compiler_state.std_schema)\n    modaliases = current_tx.get_modaliases()\n\n    for arg in call.args:\n        match arg:\n            case qlast.Path(\n                steps=[qlast.ObjectRef()],\n                partial=False,\n            ):\n                ptr = False\n            case qlast.Path(\n                steps=[qlast.ObjectRef(), qlast.Ptr()],\n                partial=False,\n            ):\n                ptr = True\n            case _:\n                raise errors.QueryError(\n                    'argument to vacuum() must be an object type '\n                    'or a link or property reference',\n                    span=arg.span,\n                )\n\n        ir: irast.Statement = qlcompiler.compile_ast_to_ir(\n            arg,\n            schema=schema,\n            options=qlcompiler.CompilerOptions(\n                modaliases=modaliases\n            ),\n        )\n        expr = ir.expr\n        if ptr:\n            if (\n                not expr.expr\n                or not isinstance(expr.expr, irast.SelectStmt)\n                or not isinstance(expr.expr.result.expr, irast.Pointer)\n            ):\n                raise errors.QueryError(\n                    'invalid pointer argument to vacuum()',\n                    span=arg.span,\n                )\n            rptr = expr.expr.result.expr\n            source = rptr.source\n        else:\n            rptr = None\n            source = expr\n        schema, obj = irtypeutils.ir_typeref_to_type(schema, source.typeref)\n\n        if (\n            not isinstance(obj, s_objtypes.ObjectType)\n            or not obj.is_material_object_type(schema)\n        ):\n            raise errors.QueryError(\n                'argument to vacuum() must be an object type '\n                'or a link or property reference',\n                span=arg.span,\n            )\n        args.append((rptr, obj))\n\n    tables: set[s_pointers.Pointer | s_objtypes.ObjectType] = set()\n\n    for arg, (rptr, obj) in zip(call.args, args):\n        if not rptr:\n            # On a type, we just vacuum the type and its descendants\n            tables.update({obj} | {\n                desc for desc in obj.descendants(schema)\n                if desc.is_material_object_type(schema)\n            })\n        else:\n            # On a pointer, we must go over the pointer and its descendants\n            # so that we may retrieve any link talbes if necessary.\n            if not isinstance(rptr.ptrref, irast.PointerRef):\n                raise errors.QueryError(\n                    'invalid pointer argument to vacuum()',\n                    span=arg.span,\n                )\n            schema, ptrcls = irtypeutils.ptrcls_from_ptrref(\n                rptr.ptrref, schema=schema)\n\n            card = ptrcls.get_cardinality(schema)\n            if not (\n                card.is_multi() or ptrcls.has_user_defined_properties(schema)\n            ):\n                vn = ptrcls.get_verbosename(schema, with_parent=True)\n                if ptrcls.is_property():\n                    raise errors.QueryError(\n                        f'{vn} is not a valid argument to vacuum() '\n                        f'because it is not a multi property',\n                        span=arg.span,\n                    )\n                else:\n                    raise errors.QueryError(\n                        f'{vn} is not a valid argument to vacuum() '\n                        f'because it is neither a multi link nor '\n                        f'does it have link properties',\n                        span=arg.span,\n                    )\n\n            ptrclses = {ptrcls} | {\n                desc for desc in ptrcls.descendants(schema)\n                if isinstance(\n                    (src := desc.get_source(schema)), s_objtypes.ObjectType)\n                and src.is_material_object_type(schema)\n            }\n            tables.update(ptrclses)\n\n    return [\n        pg_common.get_backend_name(schema, table)\n        for table in tables\n    ]\n\n\ndef administer_vacuum(\n    ctx: compiler.CompileContext,\n    ql: qlast.AdministerStmt,\n) -> dbstate.BaseQuery:\n    # check that the kwargs are valid\n    kwargs: dict[str, str] = {}\n    for name, val in ql.expr.kwargs.items():\n        if name not in ('statistics_update', 'full'):\n            raise errors.QueryError(\n                f'unrecognized keyword argument {name!r} for vacuum()',\n                span=val.span,\n            )\n        elif (\n            not isinstance(val, qlast.Constant)\n            or val.kind != qlast.ConstantKind.BOOLEAN\n        ):\n            raise errors.QueryError(\n                f'argument {name!r} for vacuum() must be a boolean literal',\n                span=val.span,\n            )\n        kwargs[name] = val.value\n\n    option_map = {\n        \"statistics_update\": \"ANALYZE\",\n        \"full\": \"FULL\",\n    }\n    command = \"VACUUM\"\n    options = \",\".join(\n        f\"{option_map[k.lower()]} {v.upper()}\"\n        for k, v in kwargs.items()\n    )\n    if options:\n        command += f\" ({options})\"\n    command += \" \" + \", \".join(\n        _identify_administer_tables_and_cols(ctx, ql.expr),\n    )\n\n    return dbstate.MaintenanceQuery(\n        sql=command.encode('utf-8'),\n        is_transactional=False,\n    )\n\n\ndef administer_statistics_update(\n    ctx: compiler.CompileContext,\n    ql: qlast.AdministerStmt,\n) -> dbstate.BaseQuery:\n    for name, val in ql.expr.kwargs.items():\n        raise errors.QueryError(\n            f'unrecognized keyword argument {name!r} for statistics_update()',\n            span=val.span,\n        )\n\n    command = \"ANALYZE \" + \", \".join(\n        _identify_administer_tables_and_cols(ctx, ql.expr),\n    )\n\n    return dbstate.MaintenanceQuery(\n        sql=command.encode('utf-8'),\n        is_transactional=True,\n    )\n\n\ndef administer_prepare_upgrade(\n    ctx: compiler.CompileContext,\n    ql: qlast.AdministerStmt,\n) -> dbstate.BaseQuery:\n\n    user_schema = ctx.state.current_tx().get_user_schema()\n    global_schema = ctx.state.current_tx().get_global_schema()\n\n    schema = s_schema.ChainedSchema(\n        ctx.compiler_state.std_schema,\n        user_schema,\n        global_schema\n    )\n\n    schema_ddl = s_ddl.ddl_text_from_schema(\n        schema, include_migrations=True)\n    ids, _ = compiler.get_obj_ids(schema, include_extras=True)\n    json_ids = [(name, cls, str(id)) for name, cls, id in ids]\n\n    obj = dict(\n        ddl=schema_ddl, ids=json_ids\n    )\n\n    desc_ql = edgeql.parse_query(\n        f'SELECT to_json({qlquote.quote_literal(json.dumps(obj))})'\n    )\n    return compiler._compile_ql_query(\n        ctx,\n        desc_ql,\n        cacheable=False,\n        migration_block_query=True,\n    )\n\n\ndef _get_index(\n    ctx: compiler.CompileContext,\n    ql: qlast.AdministerStmt,\n) -> tuple[s_indexes.Index, s_schema.Schema]:\n    if len(ql.expr.args) != 1 or ql.expr.kwargs:\n        raise errors.QueryError(\n            f'{ql.expr.func}() takes exactly one positional argument',\n            span=ql.expr.span,\n        )\n\n    # This is janky, and we shouldn't do it.\n    arg = ql.expr.args[0]\n    match arg:\n        case qlast.TypeCast(\n            type=qlast.TypeName(\n                maintype=qlast.ObjectRef(\n                    name='uuid',\n                    module='std' | None,\n                ),\n                subtypes=None,\n            ),\n            expr=qlast.Constant(\n                kind=qlast.ConstantKind.STRING,\n                value=id_string,\n            )\n        ):\n            pass\n        case _:\n            raise errors.QueryError(\n                f'argument to {ql.expr.func}() must be a uuid literal',\n                span=arg.span,\n            )\n\n    try:\n        id = uuid.UUID(id_string)\n    except ValueError:\n        raise errors.QueryError(\"Invalid index id\")\n\n    user_schema = ctx.state.current_tx().get_user_schema()\n    global_schema = ctx.state.current_tx().get_global_schema()\n\n    schema = s_schema.ChainedSchema(\n        ctx.compiler_state.std_schema,\n        user_schema,\n        global_schema\n    )\n\n    index = schema.get_by_id(id, type=s_indexes.Index)\n    return index, schema\n\n\ndef administer_concurrent_index_build(\n    ctx: compiler.CompileContext,\n    ql: qlast.AdministerStmt,\n) -> dbstate.BaseQuery:\n    index, schema = _get_index(ctx, ql)\n\n    if not index.get_build_concurrently(schema):\n        raise errors.QueryError(\"Index was not created concurrently\")\n    if index.get_active(schema):\n        raise errors.QueryError(\"Index is already active\")\n\n    delta_context = _new_delta_context(ctx)\n\n    create_index = pg_delta.CreateIndex.create_index(\n        index, schema, delta_context\n    )\n\n    block = pg_dbops.SQLBlock()\n    block.set_non_transactional()\n    create_index.generate(block)\n    # HACK: Separate out the real index command and the comments\n    # (where do the comments even get done??)\n    assert isinstance(block.commands[0], pg_dbops.PLBlock)\n    statements = block.commands[0].get_statements()\n    index_command, comments = statements\n\n    # Update the schema::Index to set `active = true`\n    block = pg_dbops.PLTopBlock()\n    context = s_delta.CommandContext()\n    delta_root = s_delta.DeltaRoot()\n    root, alter_index, _ = index.init_delta_branch(\n        schema, context, s_delta.AlterObject\n    )\n    alter_index.set_attribute_value('active', True)\n    delta_root.add(root)\n\n    nschema = delta_root.apply(schema, context)\n\n    # Construct the command\n    compiler.compile_schema_storage_in_delta(ctx, delta_root, block, context)\n    block.add_command(comments)\n    sql = block.to_string().encode('utf-8')\n\n    if debug.flags.delta_execute_ddl:\n        debug.header('ADMINISTER concurrent_index_build(...)')\n        debug.dump_code(index_command, lexer='sql')\n        debug.dump_code(sql, lexer='sql')\n\n    assert isinstance(nschema, s_schema.ChainedSchema)\n\n    return dbstate.DDLQuery(\n        early_non_tx_sql=(index_command.encode('utf-8'),),\n        sql=sql,\n        is_transactional=False,\n        feature_used_metrics=None,\n        user_schema=nschema.get_top_schema(),\n    )\n\n\ndef validate_schema_equivalence(\n    state: compiler.CompilerState,\n    schema_a: s_schema.Schema,\n    schema_b: s_schema.Schema,\n    global_schema: s_schema.Schema,\n) -> None:\n    schema_a_full = s_schema.ChainedSchema(\n        state.std_schema,\n        schema_a,\n        global_schema,\n    )\n    schema_b_full = s_schema.ChainedSchema(\n        state.std_schema,\n        schema_b,\n        global_schema,\n    )\n\n    diff = s_ddl.delta_schemas(schema_a_full, schema_b_full)\n    complete = not bool(diff.get_subcommands())\n    if not complete:\n        if debug.flags.delta_plan:\n            debug.header('COMPARE SCHEMAS MISMATCH')\n            debug.dump(diff)\n        raise AssertionError(\n            f'schemas did not match after introspection:\\n{debug.dumps(diff)}'\n        )\n"
  },
  {
    "path": "edb/server/compiler/enums.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Callable, TypeVar, TYPE_CHECKING\n\nimport enum\n\nfrom edb.common import enum as strenum\nfrom edb.edgeql import qltypes as ir\nfrom edb.protocol.enums import Cardinality as Cardinality\n\n\nif TYPE_CHECKING:\n    Error_T = TypeVar('Error_T')\n\n\nTypeTag = ir.TypeTag\n\n\nclass Capability(enum.IntFlag):\n\n    # Capability flags that are part of the protocol.\n    # Can be picked up with the PROTO_CAPS mask.\n    MODIFICATIONS     = 1 << 0    # noqa\n    SESSION_CONFIG    = 1 << 1    # noqa\n    TRANSACTION       = 1 << 2    # noqa\n    DDL               = 1 << 3    # noqa\n    PERSISTENT_CONFIG = 1 << 4    # noqa\n\n    # Internal only capability flags.\n    GLOBAL_DDL        = 1 << 57   # noqa\n    SQL_SESSION_CONFIG= 1 << 58   # noqa\n    BRANCH_CONFIG     = 1 << 59   # noqa\n    INSTANCE_CONFIG   = 1 << 60   # noqa\n    DESCRIBE          = 1 << 61   # noqa\n    ANALYZE           = 1 << 62   # noqa\n    ADMINISTER        = 1 << 63   # noqa\n\n    PROTO_CAPS        = (1 << 32) - 1  # noqa\n    ALL               = (1 << 64) - 1  # noqa\n    WRITE             = (MODIFICATIONS | DDL | PERSISTENT_CONFIG)  # noqa\n    NONE              = 0  # noqa\n\n    def make_error(\n        self,\n        allowed: Capability,\n        error_constructor: Callable[[str], Error_T],\n        reason: str,\n    ) -> Error_T:\n        for item in Capability:\n            if item & allowed:\n                continue\n            if self & item:\n                return error_constructor(\n                    f\"cannot execute {CAPABILITY_TITLES[item]}: {reason}\")\n        raise AssertionError(\n            f\"extra capability not found in\"\n            f\" {self} allowed {allowed}\"\n        )\n\n\nCAPABILITY_TITLES = {\n    Capability.MODIFICATIONS: 'data modification queries',\n    Capability.SESSION_CONFIG: 'session configuration queries',\n    Capability.TRANSACTION: 'transaction control commands',\n    Capability.DDL: 'DDL commands',\n    Capability.PERSISTENT_CONFIG: 'configuration commands',\n    Capability.ADMINISTER: 'ADMINISTER commands',\n    Capability.DESCRIBE: 'DESCRIBE commands',\n    Capability.ANALYZE: 'ANALYZE commands',\n    Capability.INSTANCE_CONFIG: 'instance configuration commands',\n    Capability.BRANCH_CONFIG: 'database branch configuration commands',\n    Capability.SQL_SESSION_CONFIG: 'sql session configuration commands',\n    Capability.GLOBAL_DDL: 'instance-wide DDL commands',\n}\n\n\nclass OutputFormat(strenum.StrEnum):\n    BINARY = 'BINARY'\n    JSON = 'JSON'\n    JSON_ELEMENTS = 'JSON_ELEMENTS'\n    NONE = 'NONE'\n\n\nclass InputFormat(strenum.StrEnum):\n    BINARY = 'BINARY'\n    JSON = 'JSON'\n\n\nclass InputLanguage(strenum.StrEnum):\n    EDGEQL = 'EDGEQL'\n    SQL = 'SQL'\n    SQL_PARAMS = 'SQL_PARAMS'\n    GRAPHQL = 'GRAPHQL'\n\n\ndef cardinality_from_ir_value(card: ir.Cardinality) -> Cardinality:\n    if card is ir.Cardinality.AT_MOST_ONE:\n        return Cardinality.AT_MOST_ONE\n    elif card is ir.Cardinality.ONE:\n        return Cardinality.ONE\n    elif card is ir.Cardinality.MANY:\n        return Cardinality.MANY\n    elif card is ir.Cardinality.AT_LEAST_ONE:\n        return Cardinality.AT_LEAST_ONE\n    else:\n        raise ValueError(\n            f\"Cardinality.from_ir_value() got an invalid input: {card}\"\n        )\n"
  },
  {
    "path": "edb/server/compiler/errormech.py",
    "content": "# mypy: allow-untyped-defs, allow-incomplete-defs\n\n#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, NamedTuple\n\nimport json\nimport re\n\nfrom edb import errors\nfrom edb.common import value_dispatch\nfrom edb.common import uuidgen\n\nfrom edb.graphql import types as gql_types\n\nfrom edb.pgsql.parser import exceptions as parser_errors\n\nfrom edb.schema import name as sn\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import schema as s_schema\nfrom edb.schema import constraints as s_constraints\n\nfrom edb.pgsql import common\nfrom edb.pgsql import types\n\nfrom edb.server.pgcon import errors as pgerrors\n\n\nclass SchemaRequired:\n    '''A sentinel used to signal that a particular error requires a schema.'''\n\n\n# Error codes that always require the schema to be resolved. There are\n# other error codes that only require the schema under certain\n# circumstances.\nSCHEMA_CODES = frozenset({\n    pgerrors.ERROR_INVALID_TEXT_REPRESENTATION,\n    pgerrors.ERROR_NUMERIC_VALUE_OUT_OF_RANGE,\n    pgerrors.ERROR_INVALID_DATETIME_FORMAT,\n    pgerrors.ERROR_DATETIME_FIELD_OVERFLOW,\n})\n\n\nclass ErrorDetails(NamedTuple):\n    message: str\n    detail: Optional[str] = None\n    detail_json: Optional[dict[str, Any]] = None\n    code: Optional[str] = None\n    schema_name: Optional[str] = None\n    table_name: Optional[str] = None\n    column_name: Optional[str] = None\n    constraint_name: Optional[str] = None\n    errcls: Optional[type[errors.EdgeDBError]] = None\n\n\nconstraint_errors = frozenset({\n    pgerrors.ERROR_INTEGRITY_CONSTRAINT_VIOLATION,\n    pgerrors.ERROR_RESTRICT_VIOLATION,\n    pgerrors.ERROR_NOT_NULL_VIOLATION,\n    pgerrors.ERROR_FOREIGN_KEY_VIOLATION,\n    pgerrors.ERROR_UNIQUE_VIOLATION,\n    pgerrors.ERROR_CHECK_VIOLATION,\n    pgerrors.ERROR_EXCLUSION_VIOLATION,\n})\n\nbranch_errors = {\n    pgerrors.ERROR_INVALID_CATALOG_NAME: errors.UnknownDatabaseError,\n    pgerrors.ERROR_DUPLICATE_DATABASE: errors.DuplicateDatabaseDefinitionError,\n}\n\ndirectly_mappable: dict[str, type | tuple[type, str]] = {\n    pgerrors.ERROR_DIVISION_BY_ZERO: errors.DivisionByZeroError,\n    pgerrors.ERROR_INTERVAL_FIELD_OVERFLOW: errors.NumericOutOfRangeError,\n    pgerrors.ERROR_READ_ONLY_SQL_TRANSACTION: (\n        errors.TransactionError,\n        \"Modifications not allowed in a read-only transaction\"\n    ),\n    pgerrors.ERROR_SERIALIZATION_FAILURE: errors.TransactionSerializationError,\n    pgerrors.ERROR_DEADLOCK_DETECTED: errors.TransactionDeadlockError,\n    pgerrors.ERROR_OBJECT_IN_USE: errors.ExecutionError,\n    pgerrors.ERROR_IDLE_IN_TRANSACTION_TIMEOUT:\n        errors.IdleTransactionTimeoutError,\n    pgerrors.ERROR_QUERY_CANCELLED: errors.QueryTimeoutError,\n    pgerrors.ERROR_INVALID_ROW_COUNT_IN_LIMIT_CLAUSE: errors.InvalidValueError,\n    pgerrors.ERROR_INVALID_ROW_COUNT_IN_RESULT_OFFSET_CLAUSE: (\n        errors.InvalidValueError),\n    pgerrors.ERROR_INVALID_REGULAR_EXPRESSION: errors.InvalidValueError,\n    pgerrors.ERROR_INVALID_LOGARITHM_ARGUMENT: errors.InvalidValueError,\n    pgerrors.ERROR_INVALID_POWER_ARGUMENT: errors.InvalidValueError,\n    pgerrors.ERROR_INSUFFICIENT_PRIVILEGE: errors.AccessPolicyError,\n    pgerrors.ERROR_PROGRAM_LIMIT_EXCEEDED: errors.InvalidValueError,\n    pgerrors.ERROR_DATA_EXCEPTION: errors.InvalidValueError,\n    pgerrors.ERROR_CHARACTER_NOT_IN_REPERTOIRE: errors.InvalidValueError,\n}\n\n\nconstraint_res = {\n    'cardinality': re.compile(r'^.*\".*_cardinality_idx\".*$'),\n    'link_target': re.compile(r'^.*link target constraint$'),\n    'constraint': re.compile(r'^.*;schemaconstr(?:#\\d+)?\".*$'),\n    'idconstraint': re.compile(r'^.*\".*_pkey\".*$'),\n    'newconstraint': re.compile(r'^.*violate the new constraint.*$'),\n    'id': re.compile(r'^.*\"(?:\\w+)_data_pkey\".*$'),\n    'link_target_del': re.compile(r'^.*link target policy$'),\n    'scalar': re.compile(\n        r'^value for domain ([\\w\\.]+) violates check constraint \"(.+)\"'\n    ),\n}\n\n\nrange_constraints = frozenset({\n    'timestamptz_t_check',\n    'timestamp_t_check',\n    'date_t_check',\n})\n\n\npgtype_re = re.compile(\n    '|'.join(fr'\\b{key}\\b' for key in types.base_type_name_map_r))\nenum_re = re.compile(\n    r'(?P<p>enum) (?P<v>edgedb([\\w-]+).\"(?P<id>[\\w-]+)_domain\")')\n\ncache_function_re = re.compile(\n    r'^function edgedb_.*\\.__qh_.* does not exist$')\n\ntype_in_access_policy_re = re.compile(r'(\\w+|`.+?`)::(\\w+|`.+?`)')\n\n\ndef gql_translate_pgtype_inner(schema, msg):\n    \"\"\"Try to replace any internal pg type name with a GraphQL type name\"\"\"\n\n    # Mapping base types\n    def base_type_map(name: str) -> str:\n        result = gql_types.EDB_TO_GQL_SCALARS_MAP.get(\n            str(types.base_type_name_map_r.get(name))\n        )\n\n        if result is None:\n            return name\n        else:\n            return result.name\n\n    translated = pgtype_re.sub(\n        lambda r: base_type_map(r.group(0)),\n        msg,\n    )\n\n    if translated != msg:\n        return translated\n\n    def replace(r):\n        type_id = uuidgen.UUID(r.group('id'))\n        stype = schema.get_by_id(type_id, None)\n        gql_name = gql_types.GQLCoreSchema.get_gql_name(\n            stype.get_name(schema))\n        if stype:\n            return f'{r.group(\"p\")} {gql_name!r}'\n        else:\n            return f'{r.group(\"p\")} {r.group(\"v\")}'\n\n    translated = enum_re.sub(replace, msg)\n\n    return translated\n\n\ndef gql_replace_type_names_in_text(msg):\n    return type_in_access_policy_re.sub(\n        lambda m: gql_types.GQLCoreSchema.get_gql_name(\n            sn.QualName.from_string(m.group(0))),\n        msg,\n    )\n\n\ndef eql_translate_pgtype_inner(schema, msg):\n    \"\"\"Try to replace any internal pg type name with an edgedb type name\"\"\"\n    translated = pgtype_re.sub(\n        lambda r: str(types.base_type_name_map_r.get(r.group(0), r.group(0))),\n        msg,\n    )\n\n    if translated != msg:\n        return translated\n\n    def replace(r):\n        type_id = uuidgen.UUID(r.group('id'))\n        stype = schema.get_by_id(type_id, None)\n        if stype:\n            return f'{r.group(\"p\")} {stype.get_displayname(schema)!r}'\n        else:\n            return f'{r.group(\"p\")} {r.group(\"v\")}'\n\n    translated = enum_re.sub(replace, msg)\n\n    return translated\n\n\ndef translate_pgtype(schema, msg, from_graphql=False):\n    \"\"\"Try to translate a message that might refer to internal pg types.\n\n    We *want* to replace internal pg type names with edgedb names, but only\n    when they actually refer to types.\n    The messages aren't really structured well enough to support this properly,\n    so we approximate it by only doing the replacement *before* the first colon\n    in the message, so if a user does `<int64>\"bigint\"`, and we get the message\n    'invalid input syntax for type bigint: \"bigint\"', we do the right thing.\n    \"\"\"\n\n    leading, *rest = msg.split(':')\n    if from_graphql:\n        leading_translated = gql_translate_pgtype_inner(schema, leading)\n    else:\n        leading_translated = eql_translate_pgtype_inner(schema, leading)\n    return ':'.join([leading_translated, *rest])\n\n\ndef get_error_details(fields):\n    # See https://www.postgresql.org/docs/current/protocol-error-fields.html\n    # for the full list of PostgreSQL error message fields.\n    message = fields.get('M')\n\n    detail = fields.get('D')\n    detail_json = None\n    if detail and detail.startswith('{'):\n        detail_json = json.loads(detail)\n        detail = None\n\n    if detail_json:\n        errcode = detail_json.get('code')\n        if errcode:\n            try:\n                errcls = type(errors.EdgeDBError).get_error_class_from_code(\n                    errcode)\n            except LookupError:\n                pass\n            else:\n                return ErrorDetails(\n                    errcls=errcls, message=message, detail_json=detail_json)\n\n    code = fields['C']\n    schema_name = fields.get('s')\n    table_name = fields.get('t')\n    column_name = fields.get('c')\n    constraint_name = fields.get('n')\n\n    return ErrorDetails(\n        message=message, detail=detail, detail_json=detail_json, code=code,\n        schema_name=schema_name, table_name=table_name,\n        column_name=column_name, constraint_name=constraint_name\n    )\n\n\ndef get_generic_exception_from_err_details(err_details):\n    err = None\n    if err_details.errcls is not None:\n        err = err_details.errcls(err_details.message)\n        if err_details.errcls is not errors.InternalServerError:\n            err.set_linecol(\n                err_details.detail_json.get('line', -1),\n                err_details.detail_json.get('column', -1))\n    return err\n\n\n#########################################################################\n# Static errors interpretation\n#########################################################################\n\n\ndef static_interpret_backend_error(fields, from_graphql=False):\n    err_details = get_error_details(fields)\n    # handle some generic errors if possible\n    err = get_generic_exception_from_err_details(err_details)\n    if err is not None:\n        return err\n\n    return static_interpret_by_code(\n        err_details.code, err_details, from_graphql=from_graphql)\n\n\n@value_dispatch.value_dispatch\ndef static_interpret_by_code(\n    _code: str,\n    err_details: ErrorDetails,\n    from_graphql: bool = False,\n):\n    return errors.InternalServerError(err_details.message)\n\n\n@static_interpret_by_code.register_for_all(branch_errors.keys())\ndef _static_interpret_branch_errors(\n    code: str,\n    err_details: ErrorDetails,\n    from_graphql: bool = False,\n):\n    errcls = branch_errors[code]\n\n    msg = err_details.message.replace('database', 'branch', 1)\n\n    return errcls(msg)\n\n\n@static_interpret_by_code.register_for_all(directly_mappable.keys())\ndef _static_interpret_directly_mappable(\n    code: str,\n    err_details: ErrorDetails,\n    from_graphql: bool = False,\n):\n    mapped = directly_mappable[code]\n    if isinstance(mapped, type):\n        errcls = mapped\n        err_message = err_details.message\n    else:\n        errcls, err_message = mapped\n\n    if from_graphql:\n        msg = gql_replace_type_names_in_text(err_message)\n    else:\n        msg = err_message\n\n    return errcls(msg)\n\n\n@static_interpret_by_code.register_for_all(constraint_errors)\ndef _static_interpret_constraint_errors(\n    code: str,\n    err_details: ErrorDetails,\n    from_graphql: bool = False,\n):\n    if code == pgerrors.ERROR_NOT_NULL_VIOLATION:\n        if err_details.table_name or err_details.column_name:\n            return SchemaRequired\n        else:\n            return errors.InternalServerError(err_details.message)\n\n    for errtype, ere in constraint_res.items():\n        m = ere.match(err_details.message)\n        if m:\n            error_type = errtype\n            break\n    else:\n        return errors.InternalServerError(err_details.message)\n\n    if error_type == 'cardinality':\n        return errors.CardinalityViolationError('cardinality violation')\n\n    elif error_type == 'link_target':\n        if err_details.detail_json:\n            srcname = err_details.detail_json.get('source')\n            ptrname = err_details.detail_json.get('pointer')\n            target = err_details.detail_json.get('target')\n            expected = err_details.detail_json.get('expected')\n\n            if srcname and ptrname:\n                srcname = sn.QualName.from_string(srcname)\n                ptrname = sn.QualName.from_string(ptrname)\n                lname = '{}.{}'.format(srcname, ptrname.name)\n            else:\n                lname = ''\n\n            msg = (\n                f'invalid target for link {lname!r}: {target!r} '\n                f'(expecting {expected!r})'\n            )\n\n        else:\n            msg = 'invalid target for link'\n\n        return errors.UnknownLinkError(msg)\n\n    elif error_type == 'link_target_del':\n        if from_graphql:\n            msg = gql_replace_type_names_in_text(err_details.message)\n        else:\n            msg = err_details.message\n\n        return errors.ConstraintViolationError(\n            msg, details=err_details.detail)\n\n    elif error_type == 'constraint':\n        if err_details.constraint_name is None:\n            return errors.InternalServerError(err_details.message)\n\n        constraint_id, _, _ = err_details.constraint_name.rpartition(';')\n\n        try:\n            uuidgen.UUID(constraint_id)\n        except ValueError:\n            return errors.InternalServerError(err_details.message)\n\n        return SchemaRequired\n\n    elif error_type == 'idconstraint':\n        if err_details.constraint_name is None:\n            return errors.InternalServerError(err_details.message)\n\n        constraint_id, _, _ = err_details.constraint_name.rpartition('_')\n\n        try:\n            uuidgen.UUID(constraint_id)\n        except ValueError:\n            return errors.InternalServerError(err_details.message)\n\n        return SchemaRequired\n\n    elif error_type == 'newconstraint':\n        # We can reconstruct what went wrong from the schema_name,\n        # table_name, and column_name. But we don't expect\n        # constraint_name to be present (because the constraint is\n        # not yet present in the schema?).\n        if (err_details.schema_name and err_details.table_name and\n                err_details.column_name):\n            return SchemaRequired\n\n        else:\n            return errors.InternalServerError(err_details.message)\n\n    elif error_type == 'scalar':\n        return SchemaRequired\n\n    elif error_type == 'id':\n        return errors.ConstraintViolationError(\n            'unique link constraint violation')\n\n\n@static_interpret_by_code.register_for_all(SCHEMA_CODES)\ndef _static_interpret_schema_errors(\n    code: str,\n    err_details: ErrorDetails,\n    from_graphql: bool = False,\n):\n    if code == pgerrors.ERROR_INVALID_DATETIME_FORMAT:\n        hint = None\n        if err_details.detail_json:\n            hint = err_details.detail_json.get('hint')\n\n        if err_details.message.startswith('missing required time zone'):\n            return errors.InvalidValueError(err_details.message, hint=hint)\n        elif err_details.message.startswith('unexpected time zone'):\n            return errors.InvalidValueError(err_details.message, hint=hint)\n\n    return SchemaRequired\n\n\n@static_interpret_by_code.register(pgerrors.ERROR_UNDEFINED_FUNCTION)\ndef _static_interpret_undefined_function(\n    _code: str,\n    err_details: ErrorDetails,\n    from_graphql: bool = False,\n):\n    if cache_function_re.match(err_details.message):\n        return errors.QueryCacheInvalidationError(\n            'Cache invalidation caused query failure; retry the query'\n        )\n\n    return errors.InternalServerError(err_details.message)\n\n\n@static_interpret_by_code.register(pgerrors.ERROR_INVALID_PARAMETER_VALUE)\ndef _static_interpret_invalid_param_value(\n    _code: str,\n    err_details: ErrorDetails,\n    from_graphql: bool = False,\n):\n    error_message_context = ''\n    if err_details.detail_json:\n        error_message_context = (\n            err_details.detail_json.get('error_message_context', '')\n        )\n\n    return errors.InvalidValueError(\n        error_message_context + err_details.message,\n        details=err_details.detail if err_details.detail else None,\n    )\n\n\n@static_interpret_by_code.register(pgerrors.ERROR_WRONG_OBJECT_TYPE)\ndef _static_interpret_wrong_object_type(\n    _code: str,\n    err_details: ErrorDetails,\n    from_graphql: bool = False,\n):\n    if err_details.column_name:\n        return SchemaRequired\n\n    hint = None\n    error_message_context = ''\n    if err_details.detail_json:\n        hint = err_details.detail_json.get('hint')\n        error_message_context = (\n            err_details.detail_json.get('error_message_context', '')\n        )\n\n    return errors.InvalidValueError(\n        error_message_context + err_details.message,\n        details=err_details.detail if err_details.detail else None,\n        hint=hint,\n    )\n\n\n@static_interpret_by_code.register(pgerrors.ERROR_CARDINALITY_VIOLATION)\ndef _static_interpret_cardinality_violation(\n    _code: str,\n    err_details: ErrorDetails,\n    from_graphql: bool = False,\n):\n\n    if (err_details.constraint_name == 'std::assert_single'\n            or err_details.constraint_name == 'std::assert_exists'):\n        return errors.CardinalityViolationError(err_details.message)\n\n    elif err_details.constraint_name == 'std::assert_distinct':\n        return errors.ConstraintViolationError(err_details.message)\n\n    elif err_details.constraint_name == 'std::assert':\n        return errors.QueryAssertionError(err_details.message)\n\n    elif err_details.constraint_name == 'set abstract':\n        return errors.ConstraintViolationError(err_details.message)\n\n    return errors.InternalServerError(err_details.message)\n\n\n@static_interpret_by_code.register(pgerrors.ERROR_FEATURE_NOT_SUPPORTED)\ndef _static_interpret_feature_not_supported(\n    _code: str,\n    err_details: ErrorDetails,\n    from_graphql: bool = False,\n):\n    return errors.UnsupportedBackendFeatureError(err_details.message)\n\n\n#########################################################################\n# Errors interpretation that requires a schema\n#########################################################################\n\n\ndef interpret_backend_error(schema, fields, from_graphql=False):\n    # all generic errors are static and have been handled by this point\n\n    err_details = get_error_details(fields)\n    hint = None\n    if err_details.detail_json:\n        hint = err_details.detail_json.get('hint')\n\n    return interpret_by_code(err_details.code, schema, err_details, hint,\n                             from_graphql=from_graphql)\n\n\n@value_dispatch.value_dispatch\ndef interpret_by_code(code, schema, err_details, hint, from_graphql=False):\n    return errors.InternalServerError(err_details.message)\n\n\n@interpret_by_code.register_for_all(constraint_errors)\ndef _interpret_constraint_errors(\n    code: str,\n    schema: s_schema.Schema,\n    err_details: ErrorDetails,\n    hint: Optional[str],\n    from_graphql: bool = False,\n):\n    details = None\n    if code == pgerrors.ERROR_NOT_NULL_VIOLATION:\n        colname = err_details.column_name\n        if colname:\n            if colname.startswith('??'):\n                ptr_id, *_ = colname[2:].partition('_')\n            else:\n                ptr_id = colname\n            if ptr_id == 'id':\n                assert err_details.table_name\n                obj_type: s_objtypes.ObjectType = schema.get_by_id(\n                    uuidgen.UUID(err_details.table_name),\n                    type=s_objtypes.ObjectType,\n                )\n                pointer = obj_type.getptr(schema, sn.UnqualName('id'))\n            else:\n                pointer = common.get_object_from_backend_name(\n                    schema, s_pointers.Pointer, ptr_id\n                )\n            pname = pointer.get_verbosename(schema, with_parent=True)\n        else:\n            pname = None\n\n        if pname is not None:\n            if err_details.detail_json:\n                object_id = err_details.detail_json.get('object_id')\n                if object_id is not None:\n                    details = f'Failing object id is {str(object_id)!r}.'\n\n            if from_graphql:\n                pname = gql_replace_type_names_in_text(pname)\n\n            return errors.MissingRequiredError(\n                f'missing value for required {pname}',\n                details=details,\n                hint=hint,\n            )\n        else:\n            return errors.InternalServerError(err_details.message)\n\n    error_type = None\n    match = None\n\n    for errtype, ere in constraint_res.items():\n        m = ere.match(err_details.message)\n        if m:\n            error_type = errtype\n            match = m\n            break\n    # no need for else clause since it would have been handled by\n    # the static version\n\n    if error_type == 'constraint' or error_type == 'idconstraint':\n        assert err_details.constraint_name\n\n        # similarly, if we're here it's because we have a constraint_id\n        if error_type == 'constraint':\n            constraint_id_s, _, _ = err_details.constraint_name.rpartition(';')\n            assert err_details.constraint_name\n            constraint_id = uuidgen.UUID(constraint_id_s)\n            constraint = schema.get_by_id(\n                constraint_id, type=s_constraints.Constraint\n            )\n        else:\n            # Primary key violations give us the table name, so\n            # look through that for the constraint\n            obj_id, _, _ = err_details.constraint_name.rpartition('_')\n            obj_type = schema.get_by_id(\n                uuidgen.UUID(obj_id), type=s_objtypes.ObjectType\n            )\n            obj_ptr = obj_type.getptr(schema, sn.UnqualName('id'))\n            constraint = obj_ptr.get_exclusive_constraints(schema)[0]\n\n        # msg is for the \"end user\" that should not mention pointers and object\n        # type it is also affected by setting `errmessage` in user schema.\n        msg = constraint.format_error_message(schema)\n\n        # details is for the \"developer\" that must explain what's going on\n        # under the hood. It contains verbose descriptions of object involved.\n        subject = constraint.get_subject(schema)\n        subject_description = subject.get_verbosename(schema, with_parent=True)\n        constraint_description = constraint.get_verbosename(schema)\n        details = f'violated {constraint_description} on {subject_description}'\n\n        if from_graphql:\n            msg = gql_replace_type_names_in_text(msg)\n            details = gql_replace_type_names_in_text(details)\n\n        return errors.ConstraintViolationError(msg, details=details)\n    elif error_type == 'newconstraint':\n        # If we're here, it means that we already validated that\n        # schema_name, table_name and column_name all exist.\n        #\n        # NOTE: this should never occur in GraphQL mode.\n        tabname = (err_details.schema_name, err_details.table_name)\n        source = common.get_object_from_backend_name(\n            schema, s_objtypes.ObjectType, tabname)\n        source_name = source.get_displayname(schema)\n        pointer = common.get_object_from_backend_name(\n            schema, s_pointers.Pointer, err_details.column_name)\n        pointer_name = pointer.get_shortname(schema).name\n\n        return errors.ConstraintViolationError(\n            f'Existing {source_name}.{pointer_name} '\n            f'values violate the new constraint')\n    elif error_type == 'scalar':\n        assert match\n        domain_name = match.group(1)\n        # NOTE: We don't attempt to change the name of the scalar type even if\n        # the error comes from GraphQL because we map custom scalars onto\n        # their underlying base types.\n        stype_name = types.base_type_name_map_r.get(domain_name)\n        if stype_name:\n            if match.group(2) in range_constraints:\n                msg = f'{str(stype_name)!r} value out of range'\n            else:\n                msg = f'invalid value for scalar type {str(stype_name)!r}'\n        else:\n            msg = translate_pgtype(schema, err_details.message)\n        return errors.InvalidValueError(msg)\n\n    return errors.InternalServerError(err_details.message)\n\n\n@interpret_by_code.register(pgerrors.ERROR_INVALID_TEXT_REPRESENTATION)\ndef _interpret_invalid_text_repr(\n    code: str,\n    schema: s_schema.Schema,\n    err_details: ErrorDetails,\n    hint: Optional[str],\n    from_graphql: bool = False,\n):\n    return errors.InvalidValueError(\n        translate_pgtype(schema, err_details.message,\n                         from_graphql=from_graphql)\n    )\n\n\n@interpret_by_code.register(pgerrors.ERROR_NUMERIC_VALUE_OUT_OF_RANGE)\ndef _interpret_numeric_out_of_range(\n    code: str,\n    schema: s_schema.Schema,\n    err_details: ErrorDetails,\n    hint: Optional[str],\n    from_graphql: bool = False,\n):\n    return errors.NumericOutOfRangeError(\n        translate_pgtype(schema, err_details.message,\n                         from_graphql=from_graphql)\n    )\n\n\n@interpret_by_code.register(pgerrors.ERROR_INVALID_DATETIME_FORMAT)\n@interpret_by_code.register(pgerrors.ERROR_DATETIME_FIELD_OVERFLOW)\ndef _interpret_invalid_datetime(\n    code: str,\n    schema: s_schema.Schema,\n    err_details: ErrorDetails,\n    hint: Optional[str],\n    from_graphql: bool = False,\n):\n    return errors.InvalidValueError(\n        translate_pgtype(schema, err_details.message,\n                         from_graphql=from_graphql),\n        hint=hint,\n    )\n\n\n@interpret_by_code.register(pgerrors.ERROR_WRONG_OBJECT_TYPE)\ndef _interpret_wrong_object_type(\n    code: str,\n    schema: s_schema.Schema,\n    err_details: ErrorDetails,\n    hint: Optional[str],\n    from_graphql: bool = False,\n):\n    # NOTE: this should never occur in GraphQL mode due to schema/query\n    # validation.\n\n    if (\n        err_details.message == 'covariance error' and\n        err_details.column_name is not None and\n        err_details.table_name is not None\n    ):\n        ptr = schema.get_by_id(uuidgen.UUID(err_details.column_name))\n        wrong_obj = schema.get_by_id(uuidgen.UUID(err_details.table_name))\n        assert isinstance(ptr, (s_pointers.Pointer, s_pointers.PseudoPointer))\n        target = ptr.get_target(schema)\n        assert target is not None\n\n        vn = ptr.get_verbosename(schema, with_parent=True)\n        return errors.InvalidLinkTargetError(\n            f\"invalid target for {vn}: '{wrong_obj.get_name(schema)}'\"\n            f\" (expecting '{target.get_name(schema)}')\"\n        )\n\n    return errors.InternalServerError(err_details.message)\n\n\ndef static_interpret_psql_parse_error(\n    exc: parser_errors.PSqlParseError\n) -> errors.EdgeDBError:\n    res: errors.EdgeDBError\n    if isinstance(exc, parser_errors.PSqlSyntaxError):\n        res = errors.EdgeQLSyntaxError(str(exc))\n        res.set_position(exc.cursor_pos - 1, None)\n        res.compute_line_col(exc.query_source)\n    elif isinstance(exc, parser_errors.PSqlUnsupportedError):\n        res = errors.UnsupportedFeatureError(str(exc))\n        if exc.location is not None:\n            res.set_position(exc.location, None)\n    else:\n        res = errors.InternalServerError(str(exc))\n\n    return res\n"
  },
  {
    "path": "edb/server/compiler/explain/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Optional\n\nimport dataclasses\nimport json\nimport logging\nimport pickle\n\nimport immutables\n\nfrom edb import buildmeta\nfrom edb.common import debug\nfrom edb.edgeql import ast as qlast\nfrom edb.ir import ast as irast\nfrom edb.pgsql import ast as pgast\nfrom edb.schema import schema as s_schema\n\nfrom . import coarse_grained\nfrom . import fine_grained\nfrom . import ir_analyze\nfrom . import pg_tree\nfrom . import to_json\n\n\nlog = logging.getLogger(__name__)\n\n\n# \"affects_compilation\" config vals that we don't actually want to report out.\n# This turns out to be a majority of them\nOMITTED_CONFIG_VALS = {\n    \"allow_dml_in_functions\", \"allow_bare_ddl\", \"force_database_error\",\n}\n\n\n@dataclasses.dataclass\nclass Arguments(to_json.ToJson):\n    execute: bool\n    buffers: bool\n\n\n@dataclasses.dataclass(frozen=True)\nclass AnalyzeContext:\n    schema: s_schema.Schema\n    modaliases: immutables.Map[Optional[str], str]\n    reverse_mod_aliases: dict[str, Optional[str]]\n\n\ndef analyze_explain_output(\n    query_asts_pickled: bytes,\n    data: list[list[bytes]],\n    std_schema: s_schema.Schema,\n) -> bytes:\n    if debug.flags.edgeql_explain:\n        debug.header('Explain')\n\n    ql: qlast.Base\n    ir: irast.Statement\n    pg: pgast.Base\n    ql, ir, pg, explain_data = pickle.loads(query_asts_pickled)\n    config_vals, args, modaliases = explain_data\n    args = Arguments(**args)\n\n    schema = ir.schema\n    # We omit the std schema when serializing, so put it back\n    if isinstance(schema, s_schema.ChainedSchema):\n        schema = s_schema.ChainedSchema(\n            top_schema=schema._top_schema,\n            global_schema=schema._global_schema,\n            base_schema=std_schema\n        )\n\n    assert len(data) == 1 and len(data[0]) == 1\n    plan = json.loads(data[0][0])\n    assert len(plan) == 1\n    plan = debug_tree = plan[0]['Plan']\n\n    info = None\n    fg_tree = None\n    cg_tree = None\n    try:\n        ctx = AnalyzeContext(\n            schema=schema,\n            modaliases=modaliases,\n            # This has last alias wins strategy. Do we need reverse?\n            reverse_mod_aliases={v: k for k, v in modaliases.items()},\n        )\n        info = ir_analyze.analyze_queries(ql, ir, pg, ctx)\n        debug_tree = pg_tree.Plan.from_json(plan, ctx)\n        fg_tree, index = fine_grained.build(debug_tree, info, args)\n        if debug.flags.edgeql_explain:\n            debug.dump(fg_tree)\n            debug.dump(info)\n        cg_tree = coarse_grained.build(fg_tree, info, index)\n    except Exception as e:\n        log.exception(\"Error building explain model\", exc_info=e)\n\n    config_vals = {\n        k: v for k, v in config_vals.items() if k not in OMITTED_CONFIG_VALS\n    }\n    globals_used = sorted([\n        str(k) for k in ir.globals if not k.is_permission\n    ])\n\n    if info:\n        buffers = info.buffers\n    elif ql.span:\n        buffers = [ql.span.buffer]\n    else:\n        buffers = []  # should never happen\n\n    output = {\n        'config_vals': config_vals,\n        'globals_used': globals_used,\n        'module_aliases': dict(modaliases),\n        'arguments': args,\n        'version': buildmeta.get_version_string(),\n        'buffers': buffers,\n        'debug_info': {\n            'full_plan': debug_tree,\n            'analysis_info': info,\n        },\n        'fine_grained': fg_tree,\n        'coarse_grained': cg_tree,\n    }\n\n    return json.dumps(output, default=to_json.json_hook).encode('utf-8')\n"
  },
  {
    "path": "edb/server/compiler/explain/casefold.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport re\n\n# This matches spaces, minus or an empty string that comes before capital\n# letter (and not at the start of the string).\n# And is used to replace that word boundary for the underscore.\n# It handles cases like this:\n# * `Foo Bar` -- title case -- matches space\n# * `FooBar` -- CamelCase -- matches empty string before `Bar`\n# * `Some-word` -- words with dash -- matches dash\nword_boundary_re = re.compile(r'(?<!^)(?<!\\s|-)[\\s-]*(?=[A-Z])')\n\n\ndef to_snake_case(name: str) -> str:\n    # note this only covers cases we have not all possible cases of\n    # case conversion\n    return word_boundary_re.sub('_', name).lower()\n\n\ndef to_camel_case(name: str) -> str:\n    # note this only covers cases we have not all possible cases of\n    # case conversion\n    return word_boundary_re.sub('', name)\n"
  },
  {
    "path": "edb/server/compiler/explain/coarse_grained.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Optional, Iterator\n\nimport dataclasses\nimport enum\nimport uuid\n\nfrom edb.server.compiler.explain import to_json\nfrom edb.server.compiler.explain import ir_analyze\nfrom edb.server.compiler.explain import pg_tree\nfrom edb.server.compiler.explain import fine_grained\n\n\nCOST_KEYS = frozenset((\n    'plan_rows',\n    'plan_width',\n    'self_cost',\n    'total_cost',\n    'startup_cost',\n))\n\n\nclass _Index:\n    by_id: dict[int, _PlanInfo]\n    by_alias: dict[str, _PlanInfo]\n\n    def __init__(self, plan: fine_grained.Plan, idx: fine_grained.Index):\n        by_id = {}\n        ancestors: list[fine_grained.Plan] = []\n\n        def index(node: fine_grained.Plan) -> None:\n            pinfo = _PlanInfo(\n                plan=node,\n                ancestors=list(reversed(ancestors)),\n            )\n            by_id[id(node)] = pinfo\n            ancestors.append(node)\n            try:\n                for sub in node.subplans:\n                    index(sub)\n            finally:\n                ancestors.pop()\n\n        index(plan)\n        self.by_id = by_id\n        self.by_alias = {a: by_id[id(p)] for a, p in idx.by_alias.items()}\n\n\n@dataclasses.dataclass\nclass _PlanInfo:\n    plan: fine_grained.Plan\n    ancestors: list[fine_grained.Plan]\n    shape_mark: Optional[str] = None\n\n    @property\n    def id(self) -> uuid.UUID:\n        return self.plan.pipeline[-1].plan_id\n\n    def self_and_ancestors(self, index: _Index) -> Iterator[_PlanInfo]:\n        yield self\n        for node in self.ancestors:\n            yield index.by_id[id(node)]\n\n\n@dataclasses.dataclass\nclass Node(to_json.ToJson, pg_tree.CostMixin):\n    plan_id: uuid.UUID\n    relations: frozenset[str]\n    contexts: Optional[list[ir_analyze.ContextDesc]]\n    children: list[Child]\n\n\n# Note: clients should consider this open-ended list\nclass ChildKind(enum.Enum):\n    POINTER = \"pointer\"  # TODO(tailhook) property/link ?\n    FILTER = \"filter\"\n\n\n@dataclasses.dataclass\nclass Child(to_json.ToJson):\n    kind: ChildKind\n    name: Optional[str]  # currently set only for POINTER\n    node: Node\n\n\ndef _scan_relations(\n    path: str, plan: fine_grained.Plan, index: _Index\n) -> Iterator[pg_tree.Relation]:\n    info = index.by_id[id(plan)]\n    if info.shape_mark == path or info.shape_mark is None:\n        for stage in plan.pipeline:\n            if relation := getattr(stage, 'relation_name', None):\n                yield relation\n        for node in plan.subplans:\n            yield from _scan_relations(path, node, index)\n\n\ndef _build_shape(\n    path: str,\n    plan: fine_grained.Plan,\n    shape: ir_analyze.ShapeInfo,\n    contexts: Optional[list[ir_analyze.ContextDesc]],\n    index: _Index,\n) -> Node:\n    # Coarse-grained tree is built like this:\n    #\n    # 1. Scan IR we find all the shapes, and mark aliases that belong to\n    #    them or their pointers (done in ir_analyze module)\n    # 2. For each shape and property we try to find the node of fine-grained\n    #    tree that represents them (by using alias and walking up).\n    # 3. And we output tree containing only those nodes marked in step (2)\n\n    _shape_mark(path, shape, index)\n\n    pointers = {}\n    for name, pointer in shape.pointers.items():\n        subpath = f\"{path}.{name}\"\n\n        if (\n            pointer.main_alias is not None and\n            (c_info := index.by_alias.get(pointer.main_alias)) is not None\n        ):\n            info = c_info\n        else:\n            for alias in pointer.aliases:\n                if c_info := index.by_alias.get(alias):\n                    info = c_info\n                    break\n            else:\n                continue\n\n        start = info\n        last_context = info.plan.contexts\n        for plan_info in info.self_and_ancestors(index):\n            mark = plan_info.shape_mark\n            if mark is not None and mark != subpath:\n                break\n            start = plan_info\n            if start.plan.contexts:\n                last_context = start.plan.contexts\n\n        pointers[name] = _build_shape(\n            f\"{path}.{name}\",\n            start.plan,\n            pointer,\n            last_context,\n            index,\n        )\n\n    relations = frozenset(_scan_relations(path, plan, index))\n\n    # sometimes context can be in inner node, hoist it\n    if (\n        not contexts and\n        (main_alias := shape.main_alias) and\n        (main_info := index.by_alias.get(main_alias))\n    ):\n        alias = main_alias\n        contexts = main_info.plan.contexts\n\n    top = plan.pipeline[0]\n    return Node(\n        plan_id=plan.pipeline[0].plan_id,\n        relations=relations,\n        children=[Child(kind=ChildKind.POINTER, name=name, node=node)\n                  for name, node in pointers.items()],\n        contexts=contexts,\n        # cost vars\n        startup_cost=top.startup_cost,\n        total_cost=top.total_cost,\n        plan_rows=top.plan_rows,\n        plan_width=top.plan_width,\n        actual_startup_time=top.actual_startup_time,\n        actual_total_time=top.actual_total_time,\n        actual_rows=top.actual_rows,\n        actual_loops=top.actual_loops,\n        shared_hit_blocks=top.shared_hit_blocks,\n        shared_read_blocks=top.shared_read_blocks,\n        shared_dirtied_blocks=top.shared_dirtied_blocks,\n        shared_written_blocks=top.shared_written_blocks,\n        local_hit_blocks=top.local_hit_blocks,\n        local_read_blocks=top.local_read_blocks,\n        local_dirtied_blocks=top.local_dirtied_blocks,\n        local_written_blocks=top.local_written_blocks,\n        temp_read_blocks=top.temp_read_blocks,\n        temp_written_blocks=top.temp_written_blocks,\n    )\n\n\ndef _shape_mark(path: str, shape: ir_analyze.ShapeInfo, index: _Index) -> None:\n    path_prefix = path + \".\"\n    for alias in shape.all_aliases:\n        info = index.by_alias.get(alias)\n        if not info:\n            continue\n        for plan_info in info.self_and_ancestors(index):\n            if plan_info.shape_mark:\n                break\n            plan_info.shape_mark = path\n\n    for name, _subshape in shape.pointers.items():\n        subpath = f\"{path}.{name}\"\n        for alias in shape.all_aliases:\n            info = index.by_alias.get(alias)\n            if not info:\n                continue\n            for plan_info in info.self_and_ancestors(index):\n                cur_mark = plan_info.shape_mark\n                if cur_mark is None:\n                    plan_info.shape_mark = subpath\n                elif cur_mark == path:\n                    break\n                elif cur_mark == subpath:\n                    break\n                elif cur_mark.startswith(path_prefix):\n                    # Two pointers met together, this means it's a\n                    # branching point. We need to cleanup all pointers\n                    # from the ancestors now (just continue loop and it'll\n                    # do the job)\n                    plan_info.shape_mark = path\n\n\ndef build(\n    plan: fine_grained.Plan,\n    info: ir_analyze.AnalysisInfo,\n    index: fine_grained.Index\n) -> Node:\n    idx = _Index(plan, index)\n    return _build_shape('🌳', plan, info.shape_tree, plan.contexts, idx)\n"
  },
  {
    "path": "edb/server/compiler/explain/fine_grained.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, Iterable\n\nimport uuid\nimport dataclasses\n\nfrom edb.server.compiler.explain import to_json\nfrom edb.server.compiler.explain import pg_tree\nfrom edb.server.compiler.explain import ir_analyze\nfrom edb.server.compiler import explain\n\n\nPropValue = str | int | float | list[str | int | float]\n\n\n@dataclasses.dataclass\nclass Prop(to_json.ToJson):\n    title: str\n    value: PropValue\n    type: Optional[pg_tree.PropType]\n    important: bool\n\n    @property\n    def attribute_name(self) -> str:\n        return self.title\n\n\nclass Properties(to_json.ToJson):\n\n    def __init__(self, props: Iterable[Prop]):\n        self._props = {p.attribute_name: p for p in props}\n\n    def to_json(self) -> Any:\n        return list(self._props.values())\n\n    def __repr__(self) -> str:\n        return repr({k: v.value for k, v in self._props.items()})\n\n\n@dataclasses.dataclass(kw_only=True)\nclass Stage(to_json.ToJson, pg_tree.CostMixin):\n    plan_type: str\n    plan_id: uuid.UUID\n    properties: Properties\n\n    def __getattr__(self, name: str) -> PropValue:\n        try:\n            return self.properties._props[name].value\n        except KeyError:\n            raise AttributeError(name) from None\n\n\n@dataclasses.dataclass\nclass Plan(to_json.ToJson):\n    contexts: Optional[list[ir_analyze.ContextDesc]]\n    pipeline: list[Stage]\n    subplans: list[Plan]\n    alias: Optional[str] = None\n\n\n@dataclasses.dataclass\nclass Index:\n    by_id: dict[uuid.UUID, Plan]\n    by_alias: dict[str, Plan]\n\n\ndef context_diff(\n    left: Optional[list[ir_analyze.ContextDesc]],\n    right: Optional[list[ir_analyze.ContextDesc]],\n) -> list[ir_analyze.ContextDesc]:\n    if not left:\n        return []\n    if not right:\n        return left\n    result = [ctx for ctx in left if ctx not in right]\n    return result\n\n\ndef context_intersect(\n    left: Optional[list[ir_analyze.ContextDesc]],\n    right: Optional[list[ir_analyze.ContextDesc]],\n) -> list[ir_analyze.ContextDesc]:\n    if not left:\n        return []\n    if not right:\n        return []\n    return [ctx for ctx in left if ctx in right]\n\n\ndef context_optimize(\n    items: Optional[list[ir_analyze.ContextDesc]],\n) -> Optional[list[ir_analyze.ContextDesc]]:\n    if not items:\n        return None\n    # We assume that context are ordered:\n    # 1. In single location (alias): from the most specific to the broadest\n    # 2. Location that belong to single buffer or alias are subsequent\n    #\n    # Postgres marks by alias the most specific thing (i.e. table scan mostly)\n    # But since we try to hoist context to nearest node having no context, that\n    # usually matches broadest context. Although, this is just a heuristic.\n    #\n    # So we only keep the last context from each group (alias/buffer) by\n    # squashing contexts that are inside of each other\n    result: list[ir_analyze.ContextDesc] = []\n    for ctx in reversed(items):\n        for maybe_parent in result:\n            if ctx.is_subcontext_of(maybe_parent):\n                break\n        else:\n            result.append(ctx)\n    result.reverse()\n    return result\n\n\nclass TreeBuilder:\n    alias_info: dict[str, ir_analyze.AliasInfo]\n    by_id: dict[uuid.UUID, Plan]\n    by_alias: dict[str, Plan]\n\n    def __init__(self, info: ir_analyze.AnalysisInfo):\n        self.alias_info = info.alias_info\n        self.by_alias = {}\n        self.by_id = {}\n\n    def build(self, plan: pg_tree.Plan, args: explain.Arguments) -> Plan:\n        # For fine-grained tree (this one will be displayed in \\verbose mode or\n        # whatever we name it) we do three things:\n\n        # 1. Remove cheap scalar Result nodes. In my examples, they are:\n        #    variable in LIMIT clause, or scalar expressions, like string\n        #    concatenation. We ensure that eliminated nodes are less than 1\n        #    percent of parent node cost/time,\n\n        # 2. Squash nested nodes having one child into pipeline list. This\n        #    should allow less nested presentation of the tree.\n        #\n        # 3. For contexts:\n        #    a) Hoist them through the tree of one-child node\n        #    b) If contexts of all children are equal we move context to higher\n        #       level\n        #    c) If contexts of children are partly equal, we move equal\n        #       contexts to parent removing them from children\n        #    d) Eliminate overlapping contexts after that\n\n        # 3c, works for things like x := count(.a) + count(.b). There are two\n        # nodes, one starting from .a and one from .b and both of them have\n        # contexts up to the whole expression starting from x :=.\n\n        pipeline = []\n        aliases = set()\n\n        pipeline.append(self._make_stage(plan))\n        alias = getattr(plan, 'alias', None)\n        if alias:\n            aliases.add(alias)\n\n        plans = _filter_plans(plan, args)\n        while len(plans) == 1 and not alias:\n            node = plans[0]\n            pipeline.append(self._make_stage(node))\n            plans = _filter_plans(node, args)\n            alias = getattr(node, 'alias', None)\n            if alias:\n                aliases.add(alias)\n\n        subplans = [self.build(subplan, args)\n                    for subplan in plans]\n\n        alias_info = self.alias_info.get(alias) if alias else None\n        contexts = alias_info.contexts if alias_info else None\n        if not contexts and subplans and (contexts := subplans[0].contexts):\n            # hoist contexts that are common in child branches\n            for ch_plan in subplans[1:]:\n                if inner_contexts := ch_plan.contexts:\n                    contexts = context_intersect(contexts, inner_contexts)\n\n            if contexts:  # some contexts are hoisted\n                for (sub, node) in zip(subplans, plans):\n                    sub.contexts = context_diff(sub.contexts, contexts)\n                    if (\n                        not sub.contexts and\n                        (subalias := getattr(node, 'alias', None))\n                    ):\n                        aliases.add(subalias)\n\n        # optimize after hoisting\n        for sub in subplans:\n            sub.contexts = context_optimize(sub.contexts)\n\n        result = Plan(\n            contexts=contexts,\n            pipeline=pipeline,\n            subplans=subplans,\n        )\n\n        for stage in pipeline:\n            self.by_id[stage.plan_id] = result\n        # Note: this overwrites children with this alias by this node\n        # when contexts are hoisted, which is a good thing\n        for alias in aliases:\n            self.by_alias[alias] = result\n\n        return result\n\n    def _get_contexts(\n        self,\n        plan: pg_tree.Plan,\n    ) -> Optional[list[ir_analyze.ContextDesc]]:\n        if not (alias := getattr(plan, 'alias', None)):\n            return None\n        if not (ainfo := self.alias_info.get(alias)):\n            return None\n        return ainfo.contexts\n\n    def _make_stage(self, plan: pg_tree.Plan) -> Stage:\n        properties = []\n        for name, prop in plan.get_props().items():\n            if (value := getattr(plan, name, None)) is not None:\n                properties.append(Prop(\n                    title=name,\n                    value=value,\n                    type=prop.enum_type,\n                    important=prop.important,\n                ))\n\n        return Stage(\n            plan_type=type(plan).__name__,\n            plan_id=plan.plan_id,\n            properties=Properties(properties),\n            # cost vars\n            startup_cost=plan.startup_cost,\n            total_cost=plan.total_cost,\n            plan_rows=plan.plan_rows,\n            plan_width=plan.plan_width,\n            actual_startup_time=plan.actual_startup_time,\n            actual_total_time=plan.actual_total_time,\n            actual_rows=plan.actual_rows,\n            actual_loops=plan.actual_loops,\n            shared_hit_blocks=plan.shared_hit_blocks,\n            shared_read_blocks=plan.shared_read_blocks,\n            shared_dirtied_blocks=plan.shared_dirtied_blocks,\n            shared_written_blocks=plan.shared_written_blocks,\n            local_hit_blocks=plan.local_hit_blocks,\n            local_read_blocks=plan.local_read_blocks,\n            local_dirtied_blocks=plan.local_dirtied_blocks,\n            local_written_blocks=plan.local_written_blocks,\n            temp_read_blocks=plan.temp_read_blocks,\n            temp_written_blocks=plan.temp_written_blocks,\n        )\n\n\ndef _filter_plans(\n    node: pg_tree.Plan, args: explain.Arguments\n) -> list[pg_tree.Plan]:\n    min_cost = node.total_cost * 0.01\n    # TODO(tailhook) maybe we should scan inner plans to figure out that\n    # there are no inner contexts in the children\n    plans = [\n        p\n        for p in node.plans\n        if not isinstance(p, pg_tree.Result) or\n        p.total_cost > min_cost or p.plan_rows > 1\n    ]\n    return plans\n\n\ndef build(\n    plan: pg_tree.Plan,\n    info: ir_analyze.AnalysisInfo,\n    args: explain.Arguments,\n) -> tuple[Plan, Index]:\n    tree = TreeBuilder(info)\n    result = tree.build(plan, args)\n    result.contexts = context_optimize(result.contexts)\n    index = Index(by_id=tree.by_id, by_alias=tree.by_alias)\n    return result, index\n"
  },
  {
    "path": "edb/server/compiler/explain/ir_analyze.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nfrom __future__ import annotations\nfrom typing import Any, Optional, Iterator, cast\n\nimport dataclasses\n\nfrom edb.common import ast\nfrom edb.common import debug\n\nfrom edb.edgeql import ast as qlast\n\nfrom edb.ir import ast as irast\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql.compiler import astutils\n\nfrom edb.server.compiler import explain\nfrom edb.server.compiler.explain import to_json\n\n\n@dataclasses.dataclass(eq=True, frozen=True)\nclass ContextDesc(to_json.ToJson):\n    start: int\n    end: int\n    buffer_idx: int\n    text: str\n\n    def is_subcontext_of(self, other: ContextDesc) -> bool:\n        return (\n            self.buffer_idx == other.buffer_idx and\n            self.start >= other.start and\n            self.end <= other.end\n        )\n\n\n@dataclasses.dataclass\nclass AliasInfo(to_json.ToJson):\n    contexts: list[ContextDesc]\n\n\n@dataclasses.dataclass\nclass ShapeInfo(to_json.ToJson):\n    aliases: set[str]\n    pointers: dict[str, ShapeInfo]\n    main_alias: Optional[str] = None\n\n    @property\n    def all_aliases(self) -> Iterator[str]:\n        if self.main_alias:\n            yield self.main_alias\n        yield from self.aliases\n\n\n@dataclasses.dataclass\nclass AnalysisInfo(to_json.ToJson):\n    alias_info: dict[str, AliasInfo]\n    buffers: list[str]\n    shape_tree: ShapeInfo\n\n\nclass VisitShapes(ast.NodeVisitor):\n    ir_node_to_alias: dict[irast.Set, str] = {}\n    skip_hidden = True\n    extra_skips = frozenset(('shape', 'source', 'target'))\n\n    def __init__(self, ir_node_to_alias: dict[irast.Set, str], **kwargs: Any):\n        self.ir_node_to_alias = ir_node_to_alias\n        self.current_shape = ShapeInfo(aliases=set(), pointers={})\n        super().__init__(**kwargs)\n\n    def visit_Set(self, node: irast.Set) -> Any:\n        alias = self.ir_node_to_alias.get(node)\n        if not alias:\n            return self.generic_visit(node)\n\n        if not node.shape:\n            self.current_shape.aliases.add(alias)\n            return self.generic_visit(node)\n\n        parent_shape = self.current_shape\n        parent_shape.main_alias = alias\n        parent_shape.aliases.discard(alias)\n        for (item, _oper) in node.shape:\n            if not (rptr_name := item.path_id.rptr_name()):\n                continue\n            name = str(rptr_name.name)\n\n            self.current_shape = self.current_shape.pointers.setdefault(\n                name,\n                ShapeInfo(aliases=set(), pointers={}),\n            )\n            try:\n                self.generic_visit(item)\n            finally:\n                self.current_shape = parent_shape\n\n        # Simple scalar expressions have the same alias for some reason\n        # so we have to discard them\n        for sub in parent_shape.pointers.values():\n            sub.aliases.discard(parent_shape.main_alias)\n            sub.aliases.difference_update(parent_shape.aliases)\n\n        return self.generic_visit(node)  # this skips node.shape\n\n\n# Do a bunch of analysis of the queries. Currently we produce more\n# info than we actually consume, since we are still in a somewhat\n# exploratory phase.\ndef analyze_queries(\n    ql: qlast.Base,\n    ir: irast.Statement,\n    pg: pgast.Base,\n    ctx: explain.AnalyzeContext,\n) -> AnalysisInfo:\n    debug_spew = debug.flags.edgeql_explain\n\n    assert ql.span\n    contexts = {(ql.span.buffer, ql.span.filename): 0}\n\n    def get_context(node: irast.Set) -> ContextDesc:\n        assert node.span, node\n        span = node.span\n        key = span.buffer, span.filename\n        if (idx := contexts.get(key)) is None:\n            idx = len(contexts)\n            contexts[key] = idx\n        text = span.buffer[span.start:span.end]\n\n        return ContextDesc(\n            start=span.start,\n            end=span.end,\n            buffer_idx=idx,\n            text=text,\n        )\n\n    rvars = ast.find_children(pg, pgast.BaseRangeVar)\n    queries = ast.find_children(pg, pgast.Query)\n\n    # Map subqueries back to their rvars\n    subq_to_rvar: dict[pgast.Query, pgast.RangeSubselect] = {}\n    for rvar in rvars:\n        if isinstance(rvar, pgast.RangeSubselect):\n            assert rvar.subquery not in subq_to_rvar\n            for subq in astutils.each_query_in_set(rvar.subquery):\n                subq_to_rvar[subq] = rvar\n\n    # Find all *references* to an rvar in path_rvar_maps\n    # Maps rvars to the queries that join them\n    reverse_path_rvar_map: dict[\n        pgast.BaseRangeVar,\n        list[pgast.Query],\n    ] = {}\n    for qry in queries:\n        qrvars = []\n        if isinstance(qry, (pgast.SelectStmt, pgast.UpdateStmt)):\n            qrvars.extend(qry.from_clause)\n        if isinstance(qry, pgast.DeleteStmt):\n            qrvars.extend(qry.using_clause)\n\n        for orvar in qrvars:\n            for rvar in astutils.each_base_rvar(orvar):\n                reverse_path_rvar_map.setdefault(rvar, []).append(qry)\n\n    # Map aliases to rvars and then to path ids\n    aliases = {\n        rvar.alias.aliasname: rvar for rvar in rvars if rvar.alias.aliasname\n    }\n\n    alias_contexts: dict[str, list[ContextDesc]] = {}\n    ir_node_to_alias: dict[irast.Set, str] = {}\n\n    # Try to produce good contexts\n    # KEY FACT: We often duplicate code for with bindings. This means\n    # we want to expose that through the contexts we include.\n    for alias, rvar in aliases.items():\n        # Run up the tree looking both for contexts to associate with\n        # and the next node in the tree to go up to\n        asets = []\n        while True:\n            ns = cast(list[irast.Set], rvar.ir_origins or [])\n            if len(ns) >= 1 and ns[0].span:\n                if ns[0] not in asets:\n                    asets.append(ns[0])\n\n            for node in ns:\n                ir_node_to_alias[node] = alias\n                break\n\n            # Find the enclosing\n            sources = reverse_path_rvar_map.get(rvar, ())\n            if debug_spew:\n                print(f'SOURCES for {alias} 1/{len(ns)}', sources)\n            if sources:\n                source = sources[0]\n                if source not in subq_to_rvar:\n                    break\n            else:\n                break\n\n            rvar = subq_to_rvar[source]\n\n        spans = [get_context(x) for x in asets if x.span]\n        if debug_spew:\n            print(alias, asets)\n            for x in asets:\n                debug.dump(x.span)\n\n        # Using the first set of contexts found\n        alias_contexts.setdefault(alias, spans)\n\n    alias_info = {\n        alias: AliasInfo(\n            contexts=alias_contexts.pop(alias, []),\n        )\n        for alias in aliases\n    }\n\n    visitor = VisitShapes(ir_node_to_alias=ir_node_to_alias)\n    visitor.visit(ir)\n    shape_tree = visitor.current_shape\n\n    return AnalysisInfo(\n        alias_info=alias_info,\n        buffers=[text for text, _filename in contexts.keys()],\n        shape_tree=shape_tree,\n    )\n"
  },
  {
    "path": "edb/server/compiler/explain/pg_tree.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# The types here modeled closely after postgres explain output.\n# See postgres/src/backend/commands/explain.c\n#\nfrom __future__ import annotations\nfrom typing import (\n    Annotated,\n    Any,\n    ClassVar,\n    Optional,\n    TypeVar,\n    Union,\n    Sequence,\n    get_args,\n    get_origin,\n    get_type_hints,\n    NewType,\n    Text,\n)\n\nimport dataclasses\nimport enum\nimport re\nimport uuid\n\nfrom edb.common import ast\n\nfrom edb.schema import constraints as s_constr\nfrom edb.schema import indexes as s_indexes\nfrom edb.schema import name as sn\nfrom edb.schema import objects as so\nfrom edb.schema import pointers as s_pointers\n\nfrom edb.server.compiler import explain\nfrom edb.server.compiler.explain import casefold\nfrom edb.server.compiler.explain import to_json\n\n\nuuid_core = '[a-f0-9]{8}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{12}'\nuuid_re = re.compile(\n    rf'(\\.?\"?({uuid_core})\"?)',\n    re.I\n)\n\nT = TypeVar('T')\nFromJsonT = TypeVar('FromJsonT', bound='FromJson')\n\n\nclass FromJson(ast.AST, to_json.ToJson):\n    @classmethod\n    def from_json(\n        cls: type[FromJsonT],\n        json: dict[str, Any],\n        ctx: explain.AnalyzeContext,\n    ) -> FromJsonT:\n        annotations = get_type_hints(cls)\n        result = cls()\n        for name, value in json.items():\n            name = casefold.to_snake_case(name)\n            if not (prop := annotations.get(name)):\n                # extra values are okay\n                setattr(result, name, value)\n                continue\n\n            if get_origin(prop) is Annotated:\n                prop = get_args(prop)[0]\n            if get_origin(prop) is Union:  # actually an option\n                prop = get_args(prop)[0]\n                if value is None:\n                    setattr(result, name, value)\n                    continue\n\n            if prop is Index:\n                setattr(result, name, _translate_index(value, ctx))\n            elif prop is Relation:\n                setattr(result, name, _translate_relation(value, ctx))\n            elif get_origin(prop) is list:\n                inner = get_args(prop)[0]\n                if type(inner) is type and issubclass(inner, FromJson):\n                    setattr(result, name,\n                            [inner.from_json(v, ctx) for v in value])\n                else:\n                    setattr(result, name, value)\n            elif type(prop) is type and issubclass(prop, FromJson):\n                setattr(result, name, prop.from_json(value, ctx))\n            else:\n                setattr(result, name, value)\n\n        # lists are always there for convenience\n        for name, prop in annotations.items():\n            name = casefold.to_snake_case(name)\n            if (\n                get_origin(prop) is list and\n                getattr(result, name, None) is None\n            ):\n                setattr(result, name, [])\n\n        return result\n\n    def to_json(self) -> Any:\n        dic = super().to_json()\n        dic['node_type'] = self.__class__.__name__\n        return dic\n\n\ndef _obj_to_name(\n    sobj: so.Object,\n    ctx: explain.AnalyzeContext,\n    dotted: bool=False,\n) -> str:\n    if isinstance(sobj, s_pointers.Pointer):\n        # If a pointer is on the RHS of a dot, just use\n        # the short name. But otherwise, grab the source\n        # and link it up\n        s = str(sobj.get_shortname(ctx.schema).name)\n        if sobj.is_link_property(ctx.schema):\n            s = f'@{s}'\n        if not dotted and (src := sobj.get_source(ctx.schema)):\n            src_name = _translate_name(\n                src.get_name(ctx.schema),\n                ctx.reverse_mod_aliases,\n            )\n            s = f'{src_name}.{s}'\n    elif isinstance(sobj, s_constr.Constraint):\n        s = sobj.get_verbosename(ctx.schema, with_parent=True)\n    elif isinstance(sobj, s_indexes.Index):\n        s = sobj.get_verbosename(ctx.schema, with_parent=True)\n        if expr := sobj.get_expr(ctx.schema):\n            s += f' on ({expr.text})'\n    else:\n        s = _translate_name(\n            sobj.get_name(ctx.schema),\n            ctx.reverse_mod_aliases,\n        )\n\n    if dotted:\n        s = '.' + s\n\n    return s\n\n\ndef _translate_index(name: str, ctx: explain.AnalyzeContext) -> Index:\n    # Try to replace all ids with textual names\n    had_index = False\n    for (full, m) in uuid_re.findall(name):\n        uid = uuid.UUID(m)\n        sobj = ctx.schema.get_by_id(uid, default=None)\n        if sobj:\n            had_index |= isinstance(sobj, s_indexes.Index)\n            dotted = full[0] == '.'\n            s = _obj_to_name(sobj, ctx, dotted=dotted)\n            name = uuid_re.sub(s, name, count=1)\n\n    name = name.replace('_source_target_key', ' forward link index')\n    name = name.replace(';schemaconstr', '')\n    name = name.replace('_target_key', ' backward link index')\n    # If the index name is from an actual index or constraint,\n    # the `_index` part of the name just total noise, but if it\n    # is from a link, it might be slightly informative\n    if had_index:\n        name = name.replace('_index', '')\n    else:\n        name = name.replace('_index', ' index')\n    return Index(name)\n\n\ndef _translate_relation(name: str, ctx: explain.AnalyzeContext) -> Relation:\n    try:\n        id = uuid.UUID(name)\n    except ValueError:\n        # For introspection queries there are tables are named pg_*\n        return Relation(name)\n    return Relation(_obj_to_name(ctx.schema.get_by_id(id), ctx))\n\n\ndef _translate_name(\n    name: sn.Name,\n    reverse_mod_aliases: dict[str, Optional[str]],\n) -> str:\n    if not isinstance(name, sn.QualName):\n        return str(name)\n    if name.module in reverse_mod_aliases:\n        module = reverse_mod_aliases[name.module]\n        if module is None:\n            return name.name\n        else:\n            return f\"{module}::{name.name}\"\n    else:\n        module = name.module\n        suffix = f\"::{name.name}\"\n        while True:\n            # looking for the longest prefix first\n            try:\n                prefix, submodule = module.rsplit(\"::\", 1)\n            except ValueError:\n                return str(name)\n            suffix = f\"::{submodule}{suffix}\"\n\n            # Note: we don't strip default alias here so only absolute paths\n            # are generated\n            if aliased := reverse_mod_aliases.get(prefix):\n                return aliased + suffix\n\n            module = prefix\n\n# Legend:\n# * show, shown -- something visible in the text explain\n# * if xxx -- means some condition when parameter exists, option to explain\n# * str values also often have a list of options in the comment\n#   (we do not use enums, because no exhaustivity guarantee)\n# * `kB` is unit for these values\n# * no key with list == empty list\n\n\nExpr = NewType('Expr', str)\nKbytes = NewType('Kbytes', int)\nMillis = NewType('Millis', float)\nIndex = NewType('Index', str)\nRelation = NewType('Relation', str)\n\n\nclass PropType(enum.Enum):\n    KBYTES = \"kB\"\n    MILLIS = \"ms\"\n    EXPR = \"expr\"\n    INDEX = \"index\"\n    RELATION = \"relation\"\n    TEXT = \"text\"\n    INT = \"int\"\n    FLOAT = \"float\"\n\n    LIST_KBYTES = \"list:kB\"\n    LIST_MILLIS = \"list:ms\"\n    LIST_EXPR = \"list:expr\"\n    LIST_INDEX = \"list:index\"\n    LIST_RELATION = \"list:relation\"\n    LIST_TEXT = \"list:text\"\n    LIST_INT = \"list:int\"\n    LIST_FLOAT = \"list:float\"\n\n\nTYPES = {\n    Kbytes: PropType.KBYTES,\n    Millis: PropType.MILLIS,\n    Expr: PropType.EXPR,\n    Index: PropType.INDEX,\n    Relation: PropType.RELATION,\n    str: PropType.TEXT,\n    int: PropType.INT,\n    float: PropType.FLOAT,\n}\n\n\nclass Important:\n    __slots__ = ()\n\n\nimportant = Important()\n\n\n@dataclasses.dataclass\nclass PropInfo:\n    type: type[object]\n    enum_type: PropType\n    important: bool\n\n\nclass JitOptions(FromJson):\n    # show all\n    inlining: bool\n    expressions: bool\n    optimization: bool\n    deforming: bool\n\n\nclass JitTiming(FromJson):\n    generation: float  # ms\n    inilining: float  # ms\n    optimization: float  # ms\n    emission: float  # ms\n    total: float  # ms\n\n\nclass JitInfo(FromJson):\n    functions: int\n    options: JitOptions\n    timing: JitTiming\n\n\nclass Worker(FromJson):\n    worker_number: int\n    actual_startup_time: Optional[float]  # if timing\n    actual_total_time: Optional[float]  # if timing\n    actual_rows: Optional[float]\n    actual_loops: Optional[float]\n\n    jit: Optional[JitInfo]  # if bunch of options\n\n\nPlanT = TypeVar('PlanT', bound='Plan')\n\n\n@dataclasses.dataclass(kw_only=True)\nclass CostMixin:\n    # if cost\n    startup_cost: float\n    total_cost: float\n    plan_rows: float\n    plan_width: int\n\n    # if analyze (zeros if never executed)\n    actual_startup_time: Optional[float] = None  # if timing\n    actual_total_time: Optional[float] = None  # if timing\n    actual_rows: Optional[float] = None\n    actual_loops: Optional[float] = None\n\n    # if buffers\n    shared_hit_blocks: Optional[int] = None\n    shared_read_blocks: Optional[int] = None\n    shared_dirtied_blocks: Optional[int] = None\n    shared_written_blocks: Optional[int] = None\n    local_hit_blocks: Optional[int] = None\n    local_read_blocks: Optional[int] = None\n    local_dirtied_blocks: Optional[int] = None\n    local_written_blocks: Optional[int] = None\n    temp_read_blocks: Optional[int] = None\n    temp_written_blocks: Optional[int] = None\n\n\nclass Plan(FromJson, CostMixin):\n    # TODO(tailhook) output is lost somewhere\n    node_type: str\n    plan_id: uuid.UUID\n    parent_relationship: Optional[str]\n    subplan_name: Optional[str]  # shown\n    parallel_aware: bool  # true always shown as a prefix of node name\n    async_capable: bool  # true always shown as a prefix of node name\n    workers: Sequence[Worker]  # shown if non-empty\n\n    plans: list[Plan]\n\n    __subclasses: ClassVar[dict[str, type[Plan]]] = dict()\n\n    def __init_subclass__(cls, **kwargs: Any):\n        super().__init_subclass__(**kwargs)\n        cls.__subclasses[cls.__name__] = cls\n\n    @classmethod\n    def from_json(\n        cls,\n        json: dict[str, Any],\n        ctx: explain.AnalyzeContext,\n    ) -> Plan:\n        copy = json.copy()\n        copy['plan_id'] = uuid.uuid4()\n        node_type = casefold.to_camel_case(copy.pop(\"Node Type\"))\n        subclass = cls.__subclasses.get(node_type, cls)\n        return super(Plan, subclass).from_json(copy, ctx)\n\n    @classmethod\n    def get_props(cls) -> dict[str, PropInfo]:\n        result = {}\n        for name, prop in get_type_hints(cls, include_extras=True).items():\n            if name in CostMixin.__annotations__:\n                # these are stored in the node itself\n                continue\n            if get_origin(prop) is Annotated:\n                imp = important in get_args(prop)\n                prop = get_args(prop)[0]\n            else:\n                imp = False\n            try:\n                if get_origin(prop) is list:\n                    enum_type = PropType[\"LIST_\" + TYPES[prop].name]\n                elif get_origin(prop) is Union:  # optional\n                    enum_type = TYPES[get_args(prop)[0]]\n                else:\n                    enum_type = TYPES[prop]\n            except KeyError:\n                # Unknown types are skipped, they are probably\n                # nested structures, we don't support yet, and plan_id\n                continue\n\n            result[name] = PropInfo(\n                type=prop,\n                enum_type=enum_type,\n                important=imp,\n            )\n        return result\n\n\n# Base types\n\nclass BaseScan(Plan):\n    schema: Optional[str]  # if verbose\n\n    # It should have been required, but in ModifyTable it's optional, so\n    # we try to make it compatible. We don't rely on it being required in\n    # the code anyways.\n    alias: Optional[str]\n\n\nclass RelationScan(BaseScan):\n    # It should have been required, but in ModifyTable it's optional, so\n    # we try to make it compatible. We don't rely on it being required in\n    # the code anyways.\n    relation_name: Annotated[Optional[Relation], important]\n\n\nclass FilterScan(FromJson):  # mixin\n    filter: Expr\n    rows_removed_by_filter: Annotated[Optional[float], important]\n\n\n# Specific types\n\nclass Result(Plan, FilterScan):\n    one_time_filter: Expr\n\n\nclass ProjectSet(Plan):\n    pass\n\n\nclass TargetTable(FromJson):\n    schema: Optional[str]  # if verbose\n    alias: Optional[str]\n    relation_name: Annotated[Optional[Relation], important]\n    cte_name: Optional[str]\n    tuplestore_name: Optional[str]\n    tablefunction_name: Optional[str]\n    function_name: Optional[str]\n    # Also pluggable explain FDW\n\n\nclass ModifyTable(RelationScan, TargetTable):\n    operation: str  # title\n    target_tables: list[TargetTable]  # show, if mult otherwise inherited props\n\n    # Looks like conflict is only possible for single table, but\n    # it's not clear\n    #\n    # if conflict\n    conflict_resolution: Optional[str]  # NOTHING, UPDATE\n    conflict_arbiter_indexes: list[str]\n    conflict_filter: Expr\n    rows_removed_by_conflict_filter: float\n\n    tuples_inserted: Optional[float]  # if analyze\n    conflicting_tuples: Optional[float]  # if analyze\n\n\nclass Append(Plan):\n    pass\n\n\nclass MergeAppend(Plan):\n    sort_key: Annotated[list[Expr], important]  # show\n    presorted_key: Annotated[list[Expr], important]\n\n\nclass RecursiveUnion(Plan):\n    pass\n\n\nclass BitmapAnd(Plan):\n    pass\n\n\nclass BitmapOr(Plan):\n    pass\n\n\nclass UniqueJoin(Plan, FilterScan):\n    inner_unique: bool\n\n    # Inner, Left, Full, Right, Semi, Anti, show\n    join_type: Annotated[str, important]\n\n    join_filter: Expr\n    rows_removed_by_join_filter: Optional[float]\n\n\nclass NestedLoop(UniqueJoin):\n    pass\n\n\nclass MergeJoin(UniqueJoin):\n    merge_cond: Expr\n\n\nclass HashJoin(UniqueJoin):\n    hash_cond: Expr\n\n\nclass SeqScan(RelationScan, FilterScan):\n    pass\n\n\nclass SampleScan(RelationScan, FilterScan):\n    sampling_method: Annotated[Text, important]  # show\n    sampling_parameters: list[str]\n    repeatable_seed: Annotated[Optional[str], important]  # show\n\n\nclass Gather(Plan, FilterScan):\n    workers_planned: int\n    workers_launched: Optional[int]  # analyze\n    params_evaluated: Optional[list[str]]\n    single_copy: bool\n\n\nclass GatherMerge(Plan, FilterScan):\n    workers_planned: int\n    workers_launched: Optional[int]  # analyze\n    params_evaluated: Optional[list[str]]\n\n\nclass IndexScan(RelationScan, FilterScan):\n    # Backwards, Forward, NoMovement, show: opt Backward\n    scan_direction: Annotated[str, important]\n\n    index_name: Annotated[Index, important]  # show\n    index_cond: Expr\n    rows_removed_by_index_recheck: Annotated[Optional[float], important]\n    order_by: Expr\n\n\nclass IndexOnlyScan(IndexScan):\n    heap_fetches: Optional[float]  # if analyze\n\n\nclass BitmapIndexScan(Plan):\n    index_name: Annotated[Index, important]  # show\n    index_cond: Expr\n\n\nclass BitmapHeapScan(RelationScan, FilterScan):\n    recheck_cond: Expr\n    rows_removed_by_index_recheck: Optional[float]\n    exact_heap_blocks: Optional[int]  # if analyze, show\n    lossy_heap_blocks: Optional[int]  # if analyze, show\n\n\nclass TidScan(RelationScan, FilterScan):\n    tid_cond: Expr\n\n\nclass TidRangeScan(RelationScan, FilterScan):\n    tid_cond: Expr\n\n\nclass SubqueryScan(Plan, FilterScan):\n    pass\n\n\nclass FunctionScan(BaseScan, FilterScan):\n    function_name: str\n    function_call: Expr  # if verbose\n\n\nclass TableFunctionScan(BaseScan, FilterScan):\n    table_function_name: str  # always == 'xmltable'\n    table_function_call: Expr  # if verbose\n\n\nclass ValuesScan(BaseScan, FilterScan):\n    pass\n\n\nclass CTEScan(BaseScan, FilterScan):\n    cte_name: str\n\n\nclass NamedTuplestoreScan(BaseScan, FilterScan):\n    tuplestore_name: str\n\n\nclass WorkTableScan(BaseScan, FilterScan):\n    cte_name: str\n\n\nclass ForeignScan(RelationScan, FilterScan):\n    operation: Annotated[Optional[str], important]  # show: title\n\n\nclass CustomScan(RelationScan, FilterScan):\n    custom_plan_provider: Optional[str]\n    # extra info that is gather via custom function :shrug:\n\n\nclass Materialize(Plan):\n    pass\n\n\nclass MemoizeWorker(Worker):\n    # show if analyze && cache_misses > 0 (probably if enabled)\n    cache_hits: int\n    cache_misses: int\n    cache_evictions: int\n    cache_overflows: int\n    peak_memory_usage: int  # kB\n\n\nclass Memoize(Plan):\n    cache_key: str  # show\n    cache_mode: str  # {binary, logical}, show\n\n    # show if analyze && cache_misses > 0 (probably if enabled)\n    cache_hits: int\n    cache_misses: int\n    cache_evictions: int\n    cache_overflows: int\n    peak_memory_usage: int  # kB\n\n    workers: Sequence[MemoizeWorker]\n\n\nclass SortWorker(Worker):\n    sort_method: str  # show\n    sort_space_used: int  # show, kB\n    sort_space_type: str  # show\n\n\nclass Sort(Plan):\n    sort_key: Annotated[list[Expr], important]  # show\n    presorted_key: list[Expr]\n\n    sort_method: Annotated[str, important]  # show\n    # * still in progress\n    # * top-N heapsort\n    # * quicksort\n    # * external sort\n    # * external merge\n\n    sort_space_used: Annotated[Kbytes, important]  # show, kB\n    sort_space_type: Annotated[Text, important]  # Disk, Memory, show\n\n    workers: Sequence[SortWorker]  # overrides\n\n\nclass SortSpaceInfo(FromJson):\n    average_sort_space_used: Annotated[Kbytes, important]  # kB, show\n    peak_sort_space_used: Annotated[Kbytes, important]  # kB, show\n\n\nclass SortGroupsInfo(FromJson):\n    group_count: int\n    sort_methods_used: list[str]  # see Sort.sort_method\n    sort_space_memory: SortSpaceInfo  # show non-zero\n    sort_space_disk: SortSpaceInfo  # show non-zero\n\n\nclass IncrementalSortWorker(Worker):\n    full_sort_groups: Optional[SortGroupsInfo]  # show\n    pre_sorted_groups: Optional[SortGroupsInfo]  # show\n\n\nclass IncrementalSort(Plan):\n    sort_key: Annotated[list[Expr], important]  # show\n    presorted_key: list[Expr]\n\n    full_sort_groups: Optional[SortGroupsInfo]  # show\n    pre_sorted_groups: Optional[SortGroupsInfo]  # show\n\n    workers: Sequence[SortWorker]  # overrides\n\n\nclass Group(Plan, FilterScan):\n    pass\n\n\nclass Aggregate(Plan, FilterScan):\n    strategy: Annotated[str, important]  # show: title\n    partial_mode: Annotated[str, important]   # Partial, Finalize, Simple\n\n\nclass WindowAgg(Plan):\n    pass\n\n\nclass Unique(Plan):\n    pass\n\n\nclass SetOp(Plan):\n    strategy: str  # Sorted, Hashed, show: title: SetOp, HashSetOp\n    command: str  # Intersect, Intersect All, Except, ExceptAll, show\n\n\nclass LockRows(Plan):\n    pass\n\n\nclass Limit(Plan):\n    pass\n\n\nclass Hash(Plan):\n    hash_buckets: Annotated[int, important]  # show\n    original_hash_buckets: int  # show if differs\n    hash_batches: Annotated[int, important]  # show\n    original_hash_batches: int  # show if differs\n    peak_memory_usage: Annotated[Kbytes, important]  # kB  # show\n"
  },
  {
    "path": "edb/server/compiler/explain/to_json.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import Any\n\nimport enum\nimport uuid\n\nfrom edb.ir import statypes\n\n\nclass ToJson:\n    def to_json(self) -> Any:\n        return {k: v for k, v in self.__dict__.items() if v is not None}\n\n\ndef json_hook(value: Any) -> Any:\n    if isinstance(value, ToJson):\n        return value.to_json()\n    elif isinstance(value, uuid.UUID):\n        return str(value)\n    elif isinstance(value, enum.Enum):\n        return value.value\n    elif isinstance(value, (frozenset, set)):\n        return list(value)\n    elif isinstance(value, statypes.ScalarType):\n        return value.to_json()\n    raise TypeError(f\"Cannot serialize {value!r}\")\n"
  },
  {
    "path": "edb/server/compiler/rpc.pxd",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\ncimport cython\n\ncdef char serialize_output_format(val)\ncdef deserialize_output_format(char mode)\ncdef char serialize_input_language(val)\ncdef deserialize_input_language(char mode)\n\n\n@cython.final\ncdef class SQLParamsSource:\n    cdef:\n        object _cached_key\n        object _serialized\n        readonly object types_in_out\n\n\n@cython.final\ncdef class CompilationRequest:\n    cdef:\n        object serializer\n\n        readonly object source\n        readonly object protocol_version\n        readonly object input_language\n        readonly object output_format\n        readonly object input_format\n        readonly bint expect_one\n        readonly int implicit_limit\n        readonly bint inline_typeids\n        readonly bint inline_typenames\n        readonly bint inline_objectids\n        readonly str role_name\n        readonly str branch_name\n\n        readonly object modaliases\n        readonly object session_config\n        object database_config\n        object system_config\n        object schema_version\n        readonly object key_params\n\n        bytes serialized_cache\n        object cache_key\n\n    cdef _serialize(self)\n\n\n@cython.final\ncdef class CompilationRequestIdHandle:\n    cdef:\n        object cache_key\n"
  },
  {
    "path": "edb/server/compiler/rpc.pyi",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport typing\nimport uuid\n\nimport immutables\n\nfrom edb import edgeql\nfrom edb.server import defines, config\nfrom edb.server.compiler import sertypes, enums\n\nfrom edb import graphql\nfrom edb.pgsql import parser as pgparser\n\nclass SQLParamsSource:\n    types_in_out: list[tuple[list[str], list[tuple[str, str]]]]\n\n    def cache_key(self) -> bytes:\n        ...\n\n    def serialize(self) -> bytes:\n        ...\n\n    @staticmethod\n    def deserialize(data: bytes) -> SQLParamsSource:\n        ...\n\n    def text(self) -> str:\n        ...\n\nclass CompilationRequest:\n    source: edgeql.Source | graphql.Source | pgparser.Source | SQLParamsSource\n    protocol_version: defines.ProtocolVersion\n    input_language: enums.InputLanguage\n    output_format: enums.OutputFormat\n    input_format: enums.InputFormat\n    expect_one: bool\n    implicit_limit: int\n    inline_typeids: bool\n    inline_typenames: bool\n    inline_objectids: bool\n    role_name: str\n    branch_name: str\n\n    modaliases: immutables.Map[str | None, str] | None\n    session_config: immutables.Map[str, config.SettingValue] | None\n    key_params: typing.Mapping[str, object] | None = None\n\n    def __init__(\n        self,\n        *,\n        source: edgeql.Source | graphql.Source | pgparser.Source,\n        protocol_version: defines.ProtocolVersion,\n        schema_version: uuid.UUID,\n        compilation_config_serializer: sertypes.CompilationConfigSerializer,\n        input_language: enums.InputLanguage = enums.InputLanguage.EDGEQL,\n        output_format: enums.OutputFormat = enums.OutputFormat.BINARY,\n        input_format: enums.InputFormat = enums.InputFormat.BINARY,\n        expect_one: bool = False,\n        implicit_limit: int = 0,\n        inline_typeids: bool = False,\n        inline_typenames: bool = False,\n        inline_objectids: bool = True,\n        modaliases: typing.Mapping[str | None, str] | None = None,\n        session_config: typing.Mapping[str, config.SettingValue] | None = None,\n        database_config: typing.Mapping[str, config.SettingValue] | None = None,\n        system_config: typing.Mapping[str, config.SettingValue] | None = None,\n        role_name: str = defines.EDGEDB_SUPERUSER,\n        branch_name: str = defines.EDGEDB_SUPERUSER_DB,\n        key_params: typing.Mapping[str, object] | None = None,\n    ):\n        ...\n\n    def set_modaliases(\n        self, value: typing.Mapping[str | None, str] | None\n    ) -> CompilationRequest:\n        ...\n\n    def set_session_config(\n        self, value: typing.Mapping[str, config.SettingValue] | None\n    ) -> CompilationRequest:\n        ...\n\n    def set_database_config(\n            self, value: typing.Mapping[str, config.SettingValue] | None\n    ) -> CompilationRequest:\n        ...\n\n    def set_system_config(\n        self, value: typing.Mapping[str, config.SettingValue] | None\n    ) -> CompilationRequest:\n        ...\n\n    def set_schema_version(self, version: uuid.UUID) -> CompilationRequest:\n        ...\n\n    def serialize(self) -> bytes:\n        ...\n\n    @classmethod\n    def deserialize(\n        cls,\n        data: bytes,\n        query_text: str,\n        compilation_config_serializer: sertypes.CompilationConfigSerializer,\n    ) -> CompilationRequest:\n        ...\n\n    def get_cache_key(self) -> uuid.UUID:\n        ...\n"
  },
  {
    "path": "edb/server/compiler/rpc.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import (\n    Mapping,\n)\n\nimport json\nimport pickle\nimport hashlib\nimport uuid\n\ncimport cython\nimport immutables\n\nfrom edb import edgeql, errors\nfrom edb.common import uuidgen\nfrom edb.edgeql import qltypes\nfrom edb.edgeql import tokenizer\nfrom edb.graphql import tokenizer as gql_tokenizer\nfrom edb.server import config, defines\nfrom edb.server.pgproto.pgproto cimport WriteBuffer, ReadBuffer\nfrom edb.pgsql import parser as pgparser\n\nfrom . import enums, sertypes\n\ncdef object OUT_FMT_BINARY = enums.OutputFormat.BINARY\ncdef object OUT_FMT_JSON = enums.OutputFormat.JSON\ncdef object OUT_FMT_JSON_ELEMENTS = enums.OutputFormat.JSON_ELEMENTS\ncdef object OUT_FMT_NONE = enums.OutputFormat.NONE\n\ncdef object IN_FMT_BINARY = enums.InputFormat.BINARY\ncdef object IN_FMT_JSON = enums.InputFormat.JSON\n\ncdef object IN_LANG_EDGEQL = enums.InputLanguage.EDGEQL\ncdef object IN_LANG_SQL = enums.InputLanguage.SQL\ncdef object IN_LANG_SQL_PARAMS = enums.InputLanguage.SQL_PARAMS\ncdef object IN_LANG_GRAPHQL = enums.InputLanguage.GRAPHQL\n\ncdef char MASK_JSON_PARAMETERS  = 1 << 0\ncdef char MASK_EXPECT_ONE       = 1 << 1\ncdef char MASK_INLINE_TYPEIDS   = 1 << 2\ncdef char MASK_INLINE_TYPENAMES = 1 << 3\ncdef char MASK_INLINE_OBJECTIDS = 1 << 4\n\n\ncdef char serialize_output_format(val):\n    if val is OUT_FMT_BINARY:\n        return b'b'\n    elif val is OUT_FMT_JSON:\n        return b'j'\n    elif val is OUT_FMT_JSON_ELEMENTS:\n        return b'J'\n    elif val is OUT_FMT_NONE:\n        return b'n'\n    else:\n        raise AssertionError(\"unreachable\")\n\n\ncdef deserialize_output_format(char mode):\n    if mode == b'b':\n        return OUT_FMT_BINARY\n    elif mode == b'j':\n        return OUT_FMT_JSON\n    elif mode == b'J':\n        return OUT_FMT_JSON_ELEMENTS\n    elif mode == b'n':\n        return OUT_FMT_NONE\n    else:\n        raise errors.BinaryProtocolError(\n            f'unknown output format {mode.to_bytes(1, \"big\")!r}')\n\n\ncdef char serialize_input_language(val):\n    if val is IN_LANG_EDGEQL:\n        return b'E'\n    elif val is IN_LANG_SQL:\n        return b'S'\n    elif val is IN_LANG_SQL_PARAMS:\n        return b'P'\n    elif val is IN_LANG_GRAPHQL:\n        return b'G'\n    else:\n        raise errors.BinaryProtocolError(f'unknown input language {val!r}')\n\n\ncdef deserialize_input_language(char lang):\n    if lang == b'E':\n        return IN_LANG_EDGEQL\n    elif lang == b'S':\n        return IN_LANG_SQL\n    elif lang == b'P':\n        return IN_LANG_SQL_PARAMS\n    elif lang == b'G':\n        return IN_LANG_GRAPHQL\n    else:\n        raise errors.BinaryProtocolError(\n            f'unknown input language {lang.to_bytes(1, \"big\")!r}')\n\n\n@cython.final\ncdef class SQLParamsSource:\n\n    def __init__(\n        self,\n        types_in_out: list[tuple[list[str], list[tuple[str, str]]]]\n    ):\n        self.types_in_out = types_in_out\n        self._cached_key = None\n        self._serialized = None\n\n    def cache_key(self):\n        if self._cached_key is not None:\n            return self._cached_key\n\n        if self._serialized is None:\n            self.serialize()\n\n        self._cached_key = hashlib.blake2b(self._serialized).digest()\n        return self._cached_key\n\n    def text(self):\n        return ''\n\n    def serialize(self):\n        if self._serialized is not None:\n            return self._serialized\n        self._serialized = pickle.dumps(self.types_in_out, -1)\n        return self._serialized\n\n    @staticmethod\n    def deserialize(data: bytes):\n        types_in_out = pickle.loads(data)\n        return SQLParamsSource(types_in_out)\n\n\n@cython.final\ncdef class CompilationRequest:\n    def __cinit__(\n        self,\n        *,\n        source: edgeql.Source,\n        protocol_version: defines.ProtocolVersion,\n        schema_version: uuid.UUID,\n        compilation_config_serializer: sertypes.CompilationConfigSerializer,\n        input_language: enums.InputLanguage = enums.InputLanguage.EDGEQL,\n        output_format: enums.OutputFormat = OUT_FMT_BINARY,\n        input_format: enums.InputFormat = IN_FMT_BINARY,\n        expect_one: bint = False,\n        implicit_limit: int = 0,\n        inline_typeids: bint = False,\n        inline_typenames: bint = False,\n        inline_objectids: bint = True,\n        modaliases: Mapping[str | None, str] | None = None,\n        session_config: Mapping[str, config.SettingValue] | None = None,\n        database_config: Mapping[str, config.SettingValue] | None = None,\n        system_config: Mapping[str, config.SettingValue] | None = None,\n        role_name: str = defines.EDGEDB_SUPERUSER,\n        branch_name: str = defines.EDGEDB_SUPERUSER_DB,\n        key_params: Mapping[str, object] | None = None,\n    ):\n        self.serializer = compilation_config_serializer\n        self.source = source\n        self.protocol_version = protocol_version\n        self.input_language = input_language\n        self.output_format = output_format\n        self.input_format = input_format\n        self.expect_one = expect_one\n        self.implicit_limit = implicit_limit\n        self.inline_typeids = inline_typeids\n        self.inline_typenames = inline_typenames\n        self.inline_objectids = inline_objectids\n        self.schema_version = schema_version\n        self.modaliases = modaliases\n        self.session_config = session_config\n        self.database_config = database_config\n        self.system_config = system_config\n        self.role_name = role_name\n        self.branch_name = branch_name\n        self.key_params = key_params\n\n        self.serialized_cache = None\n        self.cache_key = None\n\n    def __copy__(self):\n        cdef CompilationRequest rv\n\n        rv = CompilationRequest(\n            source=self.source,\n            protocol_version=self.protocol_version,\n            schema_version=self.schema_version,\n            compilation_config_serializer=self.serializer,\n            input_language=self.input_language,\n            output_format=self.output_format,\n            input_format=self.input_format,\n            expect_one=self.expect_one,\n            implicit_limit=self.implicit_limit,\n            inline_typeids=self.inline_typeids,\n            inline_typenames=self.inline_typenames,\n            inline_objectids=self.inline_objectids,\n            modaliases=self.modaliases,\n            session_config=self.session_config,\n            database_config=self.database_config,\n            system_config=self.system_config,\n            role_name=self.role_name,\n            branch_name=self.branch_name,\n            key_params=self.key_params,\n        )\n        rv.serialized_cache = self.serialized_cache\n        rv.cache_key = self.cache_key\n        return rv\n\n    def set_modaliases(self, value) -> CompilationRequest:\n        self.modaliases = value\n        self.serialized_cache = None\n        self.cache_key = None\n        return self\n\n    def set_session_config(self, value) -> CompilationRequest:\n        self.session_config = value\n        self.serialized_cache = None\n        self.cache_key = None\n        return self\n\n    def set_database_config(self, value) -> CompilationRequest:\n        self.database_config = value\n        self.serialized_cache = None\n        self.cache_key = None\n        return self\n\n    def set_system_config(self, value) -> CompilationRequest:\n        self.system_config = value\n        self.serialized_cache = None\n        self.cache_key = None\n        return self\n\n    def set_schema_version(self, version: uuid.UUID) -> CompilationRequest:\n        self.schema_version = version\n        self.serialized_cache = None\n        self.cache_key = None\n        return self\n\n    def set_key_params(self, key_params) -> CompilationRequest:\n        self.key_params = key_params\n        self.serialized_cache = None\n        self.cache_key = None\n        return self\n\n    @classmethod\n    def deserialize(\n        cls,\n        data: bytes,\n        query_text: str,\n        compilation_config_serializer: sertypes.CompilationConfigSerializer,\n    ) -> CompilationRequest:\n        return _deserialize_comp_req(\n            data, query_text, compilation_config_serializer)\n\n    def serialize(self) -> bytes:\n        if self.serialized_cache is None:\n            self._serialize()\n        return self.serialized_cache\n\n    def get_cache_key(self) -> uuid.UUID:\n        if self.cache_key is None:\n            self._serialize()\n        return self.cache_key\n\n    cdef _serialize(self):\n        cdef WriteBuffer buf\n\n        hash_obj, buf = _serialize_comp_req(self)\n        cache_key = hash_obj.digest()\n        buf.write_bytes(cache_key)\n        self.cache_key = uuidgen.from_bytes(cache_key)\n        self.serialized_cache = bytes(buf)\n\n    def __hash__(self):\n        return hash(self.get_cache_key())\n\n    def __eq__(self, rhs) -> bool:\n        cdef:\n            CompilationRequest other\n\n        if not isinstance(rhs, CompilationRequest):\n            return NotImplemented\n        other = rhs\n\n        return (\n            self.source.cache_key() == other.source.cache_key() and\n            self.protocol_version == other.protocol_version and\n            self.input_language == other.input_language and\n            self.output_format == other.output_format and\n            self.input_format == other.input_format and\n            self.expect_one == other.expect_one and\n            self.implicit_limit == other.implicit_limit and\n            self.inline_typeids == other.inline_typeids and\n            self.inline_typenames == other.inline_typenames and\n            self.inline_objectids == other.inline_objectids and\n            self.role_name == other.role_name and\n            self.branch_name == other.branch_name and\n            self.key_params == other.key_params\n        )\n\n\n# A handle class to allow deleting cache entries just by their cache_key.\n@cython.final\ncdef class CompilationRequestIdHandle:\n    def __cinit__(\n        self,\n        cache_key: uuid.UUID\n    ):\n        self.cache_key = cache_key\n\n    def __hash__(self):\n        return hash(self.cache_key)\n\n    def __eq__(self, rhs) -> bool:\n        ty = type(rhs)\n        if ty is CompilationRequestIdHandle:\n            return self.cache_key == rhs.cache_key\n        elif ty is CompilationRequest:\n            return self.cache_key == rhs.get_cache_key()\n        else:\n            return NotImplemented\n\n\ncdef CompilationRequest _deserialize_comp_req(\n    data: bytes,\n    query_text: str,\n    compilation_config_serializer: sertypes.CompilationConfigSerializer,\n):\n    cdef:\n        ReadBuffer buf = ReadBuffer.new_message_parser(data)\n        CompilationRequest req\n\n    if data[0] == 1:\n        req = _deserialize_comp_req_v1(\n            buf, query_text, compilation_config_serializer)\n    else:\n        raise errors.UnsupportedProtocolVersionError(\n            f\"unsupported compile cache: version {data[0]}\"\n        )\n\n    # Cache key is always trailing regardless of the version.\n    req.cache_key = uuidgen.from_bytes(buf.read_bytes(16))\n    req.serialized_cache = data\n\n    return req\n\n\ncdef _deserialize_comp_req_v1(\n    buf: ReadBuffer,\n    query_text: str,\n    compilation_config_serializer: sertypes.CompilationConfigSerializer,\n):\n    # Format:\n    #\n    # * 1 byte of version (0)\n    # * 1 byte of bit flags:\n    #   * json_parameters\n    #   * expect_one\n    #   * inline_typeids\n    #   * inline_typenames\n    #   * inline_objectids\n    # * protocol_version (major: int64, minor: int16)\n    # * 1 byte output_format (the same as in the binary protocol)\n    # * implicit_limit: int64\n    # * Module aliases:\n    #   * length: int32 (negative means the modaliases is None)\n    #   * For each alias pair:\n    #      * 1 byte, 0 if the name is None\n    #      * else, C-String as the name\n    #      * C-String as the alias\n    # * Key parameter values: int32-length prefixed json object, -1 if None\n    # * Session config type descriptor\n    #   * 16 bytes type ID\n    #   * int32-length-prefixed serialized type descriptor\n    # * Session config: int32-length-prefixed serialized data\n    # * Serialized Source or NormalizedSource without the original query\n    #   string\n    # * The schema version ID.\n    # * 1 byte input language (the same as in the binary protocol)\n    # * role_name as a UTF-8 encoded string\n    # * branch_name as a UTF-8 encoded string\n\n    cdef char flags\n\n    assert buf.read_byte() == 1  # version\n\n    flags = buf.read_byte()\n    if flags & MASK_JSON_PARAMETERS > 0:\n        input_format = IN_FMT_JSON\n    else:\n        input_format = IN_FMT_BINARY\n    expect_one = flags & MASK_EXPECT_ONE > 0\n    inline_typeids = flags & MASK_INLINE_TYPEIDS > 0\n    inline_typenames = flags & MASK_INLINE_TYPENAMES > 0\n    inline_objectids = flags & MASK_INLINE_OBJECTIDS > 0\n\n    protocol_version = buf.read_int16(), buf.read_int16()\n    output_format = deserialize_output_format(buf.read_byte())\n    implicit_limit = buf.read_int64()\n\n    size = buf.read_int32()\n    if size >= 0:\n        modaliases = []\n        for _ in range(size):\n            if buf.read_byte():\n                k = buf.read_null_str().decode(\"utf-8\")\n            else:\n                k = None\n            v = buf.read_null_str().decode(\"utf-8\")\n            modaliases.append((k, v))\n        modaliases = immutables.Map(modaliases)\n    else:\n        modaliases = None\n\n    key_params_str = buf.read_len_prefixed_utf8()\n    if key_params_str:\n        key_params = immutables.Map(json.loads(key_params_str))\n    else:\n        key_params = None\n\n    serializer = compilation_config_serializer\n    type_id = uuidgen.from_bytes(buf.read_bytes(16))\n    if type_id == serializer.type_id:\n        buf.read_len_prefixed_bytes()\n    else:\n        serializer = sertypes.CompilationConfigSerializer(\n            type_id, buf.read_len_prefixed_bytes(), defines.CURRENT_PROTOCOL\n        )\n\n    data = buf.read_len_prefixed_bytes()\n    if data:\n        session_config = immutables.Map(\n            (\n                k,\n                config.SettingValue(\n                    name=k,\n                    value=v,\n                    source='session',\n                    scope=qltypes.ConfigScope.SESSION,\n                )\n            ) for k, v in serializer.decode(data).items()\n        )\n    else:\n        session_config = None\n\n    serialized_source = buf.read_len_prefixed_bytes()\n    schema_version = uuidgen.from_bytes(buf.read_bytes(16))\n\n    input_language = deserialize_input_language(buf.read_byte())\n    role_name = buf.read_len_prefixed_utf8()\n    branch_name = buf.read_len_prefixed_utf8()\n\n    if input_language is enums.InputLanguage.EDGEQL:\n        source = tokenizer.deserialize(serialized_source, query_text)\n    elif input_language is enums.InputLanguage.SQL:\n        source = pgparser.deserialize(serialized_source)\n    elif input_language is enums.InputLanguage.SQL_PARAMS:\n        source = SQLParamsSource.deserialize(serialized_source)\n    elif input_language is enums.InputLanguage.GRAPHQL:\n        source = gql_tokenizer.deserialize(serialized_source, query_text)\n    else:\n        raise AssertionError(\n            f\"unexpected source language in serialized \"\n            f\"CompilationRequest: {input_language}\"\n        )\n\n    req = CompilationRequest(\n        source=source,\n        protocol_version=protocol_version,\n        schema_version=schema_version,\n        compilation_config_serializer=serializer,\n        input_language=input_language,\n        output_format=output_format,\n        input_format=input_format,\n        expect_one=expect_one,\n        implicit_limit=implicit_limit,\n        inline_typeids=inline_typeids,\n        inline_typenames=inline_typenames,\n        inline_objectids=inline_objectids,\n        modaliases=modaliases,\n        session_config=session_config,\n        role_name=role_name,\n        branch_name=branch_name,\n        key_params=key_params,\n    )\n\n    return req\n\n\ncdef _serialize_comp_req(req: CompilationRequest):\n    # Please see _deserialize_comp_req_v1 for the format doc\n\n    cdef:\n        char version = 1, flags\n        WriteBuffer out = WriteBuffer.new()\n\n    out.write_byte(version)\n\n    flags = (\n        (MASK_JSON_PARAMETERS if req.input_format is IN_FMT_JSON else 0) |\n        (MASK_EXPECT_ONE if req.expect_one else 0) |\n        (MASK_INLINE_TYPEIDS if req.inline_typeids else 0) |\n        (MASK_INLINE_TYPENAMES if req.inline_typenames else 0) |\n        (MASK_INLINE_OBJECTIDS if req.inline_objectids else 0)\n    )\n    out.write_byte(flags)\n\n    out.write_int16(req.protocol_version[0])\n    out.write_int16(req.protocol_version[1])\n    out.write_byte(serialize_output_format(req.output_format))\n    out.write_int64(req.implicit_limit)\n\n    if req.modaliases is None:\n        out.write_int32(-1)\n    else:\n        out.write_int32(len(req.modaliases))\n        for k, v in sorted(\n            req.modaliases.items(),\n            key=lambda i: (0, i[0]) if i[0] is None else (1, i[0])\n        ):\n            if k is None:\n                out.write_byte(0)\n            else:\n                out.write_byte(1)\n                out.write_str(k, \"utf-8\")\n            out.write_str(v, \"utf-8\")\n\n    if req.key_params is None:\n        key_params_str = b''\n    else:\n        key_params_str = json.dumps(req.key_params).encode('utf-8')\n    out.write_len_prefixed_bytes(key_params_str)\n\n    type_id, desc = req.serializer.describe()\n    out.write_bytes(type_id.bytes)\n    out.write_len_prefixed_bytes(desc)\n\n    hash_obj = hashlib.blake2b(memoryview(out), digest_size=16)\n    hash_obj.update(req.source.cache_key())\n\n    if req.session_config is None:\n        session_config = b\"\"\n    else:\n        session_config = req.serializer.encode_configs(\n            req.session_config\n        )\n    out.write_len_prefixed_bytes(session_config)\n\n    # Build config that affects compilation: session -> database -> system.\n    # This is only used for calculating cache_key, while session\n    # config itreq is separately stored above in the serialized format.\n    serialized_comp_config = req.serializer.encode_configs(\n        req.system_config, req.database_config, req.session_config\n    )\n    hash_obj.update(serialized_comp_config)\n\n    # Must set_schema_version() before serializing compilation request\n    assert req.schema_version is not None\n    hash_obj.update(req.schema_version.bytes)\n\n    out.write_len_prefixed_bytes(req.source.serialize())\n    out.write_bytes(req.schema_version.bytes)\n\n    out.write_byte(serialize_input_language(req.input_language))\n    hash_obj.update(req.input_language.value.encode(\"utf-8\"))\n\n    role_name = req.role_name.encode(\"utf-8\")\n    out.write_len_prefixed_bytes(role_name)\n    hash_obj.update(role_name)\n\n    branch_name = req.branch_name.encode(\"utf-8\")\n    out.write_len_prefixed_bytes(branch_name)\n    hash_obj.update(branch_name)\n\n    return hash_obj, out\n"
  },
  {
    "path": "edb/server/compiler/sertypes.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    ClassVar,\n    Literal,\n    Optional,\n    Iterable,\n    Mapping,\n    Sequence,\n    cast,\n    overload,\n)\n\nimport collections.abc\nimport dataclasses\nimport enum\nimport functools\nimport io\nimport struct\nimport uuid\n\nimport immutables\n\nfrom edb import errors\nfrom edb.common import binwrapper\nfrom edb.common import lru\nfrom edb.common import uuidgen\nfrom edb.common import value_dispatch\n\nfrom edb.protocol import enums as p_enums\nfrom edb.server import config\nfrom edb.server import defines as edbdef\n\nfrom edb.edgeql import qltypes\n\nfrom edb.schema import name as s_name\nfrom edb.schema import globals as s_globals\nfrom edb.schema import links as s_links\nfrom edb.schema import objects as s_obj\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import schema as s_schema\nfrom edb.schema import types as s_types\n\nfrom edb.ir import ast as irast\n\nfrom edb.ir import statypes\n\nfrom . import enums\n\n\n_int32_packer = cast(Callable[[int], bytes], struct.Struct('!l').pack)\n_uint32_packer = cast(Callable[[int], bytes], struct.Struct('!L').pack)\n_uint16_packer = cast(Callable[[int], bytes], struct.Struct('!H').pack)\n_uint8_packer = cast(Callable[[int], bytes], struct.Struct('!B').pack)\n_int64_struct = struct.Struct('!q')\n_float32_struct = struct.Struct('!f')\n\n\nEMPTY_TUPLE_ID = s_obj.get_known_type_id('empty-tuple')\nEMPTY_TUPLE_DESC = b'\\x04' + EMPTY_TUPLE_ID.bytes + b'\\x00\\x00'\n\nUUID_TYPE_ID = s_obj.get_known_type_id('std::uuid')\nSTR_TYPE_ID = s_obj.get_known_type_id('std::str')\n\nNULL_TYPE_ID = uuidgen.UUID(b'\\x00' * 16)\nNULL_TYPE_DESC = b''\n\n\nclass DescriptorTag(bytes, enum.Enum):\n    SET = b'\\x00'\n    SHAPE = b'\\x01'\n    BASE_SCALAR = b'\\x02'\n    SCALAR = b'\\x03'\n    TUPLE = b'\\x04'\n    NAMEDTUPLE = b'\\x05'\n    ARRAY = b'\\x06'\n    ENUM = b'\\x07'\n    INPUT_SHAPE = b'\\x08'\n    RANGE = b'\\x09'\n    OBJECT = b'\\x0a'\n    COMPOUND = b'\\x0b'\n    MULTIRANGE = b'\\x0c'\n    SQL_ROW = b'\\x0d'\n\n    ANNO_TYPENAME = b'\\xff'\n\n\nclass ShapePointerFlags(enum.IntFlag):\n    IS_IMPLICIT = enum.auto()\n    IS_LINKPROP = enum.auto()\n    IS_LINK = enum.auto()\n\n\nclass CompoundOp(enum.IntEnum):\n    UNION = 1 << 0\n    INTERSECTION = 1 << 1\n\n\nEMPTY_BYTEARRAY = bytearray()\n\n\ndef _encode_str(data: str) -> bytes:\n    return data.encode('utf-8')\n\n\ndef _decode_str(data: bytes) -> str:\n    return data.decode('utf-8')\n\n\ndef _encode_bool(data: bool) -> bytes:\n    return b'\\x01' if data else b'\\x00'\n\n\ndef _decode_bool(data: bytes) -> bool:\n    return bool(data[0])\n\n\ndef _encode_int64(data: int) -> bytes:\n    return _int64_struct.pack(data)\n\n\ndef _decode_int64(data: bytes) -> int:\n    return _int64_struct.unpack(data)[0]  # type: ignore [no-any-return]\n\n\ndef _encode_float32(data: float) -> bytes:\n    return _float32_struct.pack(data)\n\n\ndef _decode_float32(data: bytes) -> float:\n    return _float32_struct.unpack(data)[0]  # type: ignore [no-any-return]\n\n\ndef _string_packer(s: str) -> bytes:\n    s_bytes = s.encode('utf-8')\n    return _uint32_packer(len(s_bytes)) + s_bytes\n\n\ndef _name_packer(n: s_name.Name) -> bytes:\n    return _string_packer(str(n))\n\n\ndef _bool_packer(b: bool) -> bytes:\n    return b'\\x01' if b else b'\\x00'\n\n\ndef cardinality_from_ptr(\n    ptr: s_pointers.Pointer | s_globals.Global,\n    schema: s_schema.Schema,\n) -> enums.Cardinality:\n    required = ptr.get_required(schema)\n    schema_card = ptr.get_cardinality(schema)\n    ir_card = qltypes.Cardinality.from_schema_value(required, schema_card)\n    return enums.cardinality_from_ir_value(ir_card)\n\n\nInputShapeElement = tuple[str, s_types.Type, enums.Cardinality]\nInputShapeMap = Mapping[s_types.Type, Iterable[InputShapeElement]]\n\nViewShapeMap = Mapping[s_obj.Object, list[s_pointers.Pointer]]\nViewShapeMetadataMap = Mapping[s_types.Type, irast.ViewShapeMetadata]\n\n\nclass Context:\n    def __init__(\n        self,\n        *,\n        schema: s_schema.Schema,\n        protocol_version: edbdef.ProtocolVersion,\n        view_shapes: ViewShapeMap = immutables.Map(),\n        view_shapes_metadata: ViewShapeMetadataMap = immutables.Map(),\n        follow_links: bool = True,\n        inline_typenames: bool = False,\n        name_filter: str = \"\",\n    ) -> None:\n        self.schema = schema\n        self.view_shapes = view_shapes\n        self.view_shapes_metadata = view_shapes_metadata\n        self.protocol_version = protocol_version\n        self.follow_links = follow_links\n        self.inline_typenames = inline_typenames\n        self.name_filter = name_filter\n\n        self.buffer: list[bytes] = []\n        self.anno_buffer: list[bytes] = []\n        self.uuid_to_pos: dict[uuid.UUID, int] = {}\n\n    def derive(self) -> Context:\n        ctx = type(self)(\n            schema=self.schema,\n            protocol_version=self.protocol_version,\n            view_shapes=self.view_shapes,\n            view_shapes_metadata=self.view_shapes_metadata,\n            follow_links=self.follow_links,\n            inline_typenames=self.inline_typenames,\n            name_filter=self.name_filter,\n        )\n        ctx.buffer = self.buffer.copy()\n        ctx.anno_buffer = self.anno_buffer.copy()\n        ctx.uuid_to_pos = self.uuid_to_pos.copy()\n        return ctx\n\n\ndef _get_collection_type_id(\n    coll_type: str,\n    subtypes: list[uuid.UUID],\n    element_names: list[str] | None = None,\n) -> uuid.UUID:\n    if coll_type == 'tuple' and not subtypes:\n        return s_obj.get_known_type_id('empty-tuple')\n\n    string_id = f'{coll_type}\\x00{\":\".join(map(str, subtypes))}'\n    if element_names:\n        string_id += f'\\x00{\":\".join(element_names)}'\n    return uuidgen.uuid5(s_obj.TYPE_ID_NAMESPACE, string_id)\n\n\ndef _get_object_shape_id(\n    coll_type: str,\n    subtypes: list[uuid.UUID],\n    element_names: Optional[list[str]] = None,\n    cardinalities: Optional[list[enums.Cardinality]] = None,\n    *,\n    links_props: Optional[list[bool]] = None,\n    links: Optional[list[bool]] = None,\n    has_implicit_fields: bool = False,\n) -> uuid.UUID:\n    parts = [coll_type]\n    parts.append(\":\".join(map(str, subtypes)))\n    if element_names:\n        parts.append(\":\".join(element_names))\n    if cardinalities:\n        parts.append(\":\".join(chr(c._value_) for c in cardinalities))\n    string_id = \"\\x00\".join(parts)\n    string_id += f'{has_implicit_fields!r};{links_props!r};{links!r}'\n    return uuidgen.uuid5(s_obj.TYPE_ID_NAMESPACE, string_id)\n\n\ndef _get_set_type_id(basetype_id: uuid.UUID) -> uuid.UUID:\n    return uuidgen.uuid5(\n        s_obj.TYPE_ID_NAMESPACE, 'set-of::' + str(basetype_id))\n\n\ndef _register_type_id(\n    type_id: uuid.UUID,\n    ctx: Context,\n) -> uuid.UUID:\n    if type_id not in ctx.uuid_to_pos:\n        ctx.uuid_to_pos[type_id] = len(ctx.uuid_to_pos)\n    return type_id\n\n\ndef _describe_set(\n    t: s_types.Type,\n    *,\n    ctx: Context,\n) -> uuid.UUID:\n    type_id = _describe_type(t, ctx=ctx)\n    set_id = _get_set_type_id(type_id)\n    if set_id in ctx.uuid_to_pos:\n        return set_id\n\n    buf = []\n\n    # .tag\n    buf.append(DescriptorTag.SET._value_)\n    # .id\n    buf.append(set_id.bytes)\n    # .type\n    buf.append(_type_ref_id_packer(type_id, ctx=ctx))\n\n    return _finish_typedesc(set_id, buf, ctx=ctx)\n\n\n# The encoding format is documented in edb/api/types.txt.\n@functools.singledispatch\ndef _describe_type(t: s_types.Type, *, ctx: Context) -> uuid.UUID:\n    raise errors.InternalServerError(\n        f'cannot describe type {t.get_name(ctx.schema)}')\n\n\ndef _type_ref_packer(t: s_types.Type, *, ctx: Context) -> bytes:\n    \"\"\"Return typedesc representation of a type reference.\"\"\"\n    return _type_ref_id_packer(_describe_type(t, ctx=ctx), ctx=ctx)\n\n\ndef _type_ref_id_packer(type_id: uuid.UUID, *, ctx: Context) -> bytes:\n    \"\"\"Return typedesc representation of a type reference by type id.\"\"\"\n    return _uint16_packer(ctx.uuid_to_pos[type_id])\n\n\ndef _type_ref_seq_packer(ts: Sequence[s_types.Type], *, ctx: Context) -> bytes:\n    \"\"\"Return typedesc representation of a sequence of type references.\"\"\"\n    result = _uint16_packer(len(ts))\n    for t in ts:\n        result += _type_ref_packer(t, ctx=ctx)\n    return result\n\n\ndef _type_ref_id_seq_packer(ts: Sequence[uuid.UUID], *, ctx: Context) -> bytes:\n    \"\"\"Return typedesc representation of a sequence of type id references.\"\"\"\n    result = _uint16_packer(len(ts))\n    for t in ts:\n        result += _type_ref_id_packer(t, ctx=ctx)\n    return result\n\n\ndef _finish_typedesc(\n    type_id: uuid.UUID,\n    buf: list[bytes],\n    *,\n    ctx: Context,\n) -> uuid.UUID:\n    desc = b''.join(buf)\n    if ctx.protocol_version >= (2, 0):\n        ctx.buffer.append(_uint32_packer(len(desc)))\n    ctx.buffer.append(desc)\n    return _register_type_id(type_id, ctx=ctx)\n\n\n# Tuple -> TupleTypeDescriptor\n@_describe_type.register\ndef _describe_tuple(t: s_types.Tuple, *, ctx: Context) -> uuid.UUID:\n    subtypes = [\n        _describe_type(st, ctx=ctx)\n        for st in t.get_subtypes(ctx.schema)\n    ]\n\n    is_named = t.is_named(ctx.schema)\n    if is_named:\n        element_names = list(t.get_element_names(ctx.schema))\n        assert len(element_names) == len(subtypes)\n        tag = DescriptorTag.NAMEDTUPLE\n    else:\n        element_names = None\n        tag = DescriptorTag.TUPLE\n\n    type_id = _get_collection_type_id(\n        t.get_schema_name(), subtypes, element_names)\n\n    if type_id in ctx.uuid_to_pos:\n        return type_id\n\n    buf = []\n\n    # .tag\n    buf.append(tag._value_)\n    # .id\n    buf.append(type_id.bytes)\n\n    if ctx.protocol_version >= (2, 0):\n        # .name\n        buf.append(_name_packer(t.get_name(ctx.schema)))\n        # .schema_defined\n        buf.append(_bool_packer(t.get_is_persistent(ctx.schema)))\n        # .ancestors\n        buf.append(_type_ref_seq_packer([], ctx=ctx))\n\n    # .element_count\n    buf.append(_uint16_packer(len(subtypes)))\n    if element_names is not None:\n        # .elements\n        for el_name, el_type_id in zip(element_names, subtypes):\n            # TupleElement.name\n            buf.append(_string_packer(el_name))\n            # TupleElement.type\n            buf.append(_type_ref_id_packer(el_type_id, ctx=ctx))\n    else:\n        # .element_types\n        for el_type_id in subtypes:\n            buf.append(_type_ref_id_packer(el_type_id, ctx=ctx))\n\n    return _finish_typedesc(type_id, buf, ctx=ctx)\n\n\n# Array -> ArrayTypeDescriptor\n@_describe_type.register\ndef _describe_array(t: s_types.Array, *, ctx: Context) -> uuid.UUID:\n    subtypes = [\n        _describe_type(st, ctx=ctx)\n        for st in t.get_subtypes(ctx.schema)\n    ]\n\n    assert len(subtypes) == 1\n    type_id = _get_collection_type_id(t.get_schema_name(), subtypes)\n\n    if type_id in ctx.uuid_to_pos:\n        return type_id\n\n    buf = []\n\n    # .tag\n    buf.append(DescriptorTag.ARRAY._value_)\n    # .id\n    buf.append(type_id.bytes)\n\n    if ctx.protocol_version >= (2, 0):\n        # .name\n        buf.append(_name_packer(t.get_name(ctx.schema)))\n        # .schema_defined\n        buf.append(_bool_packer(t.get_is_persistent(ctx.schema)))\n        # .ancestors\n        buf.append(_type_ref_seq_packer([], ctx=ctx))\n\n    # .type\n    buf.append(_type_ref_id_packer(subtypes[0], ctx=ctx))\n    # .dimension_count (currently always 1)\n    buf.append(_uint16_packer(1))\n    # .dimensions (currently always unbounded)\n    buf.append(_int32_packer(-1))\n\n    return _finish_typedesc(type_id, buf, ctx=ctx)\n\n\n# Range -> RangeTypeDescriptor\n@_describe_type.register\ndef _describe_range(t: s_types.Range, *, ctx: Context) -> uuid.UUID:\n    subtypes = [\n        _describe_type(st, ctx=ctx)\n        for st in t.get_subtypes(ctx.schema)\n    ]\n\n    assert len(subtypes) == 1\n    type_id = _get_collection_type_id(t.get_schema_name(), subtypes)\n\n    if type_id in ctx.uuid_to_pos:\n        return type_id\n\n    buf = []\n\n    # .tag\n    buf.append(DescriptorTag.RANGE._value_)\n    # .id\n    buf.append(type_id.bytes)\n\n    if ctx.protocol_version >= (2, 0):\n        # .name\n        buf.append(_name_packer(t.get_name(ctx.schema)))\n        # .schema_defined\n        buf.append(_bool_packer(t.get_is_persistent(ctx.schema)))\n        # .ancestors\n        buf.append(_type_ref_seq_packer([], ctx=ctx))\n\n    # .type\n    buf.append(_type_ref_id_packer(subtypes[0], ctx=ctx))\n\n    return _finish_typedesc(type_id, buf, ctx=ctx)\n\n\n# MultiRange -> MultiRangeTypeDescriptor\n@_describe_type.register\ndef _describe_multirange(t: s_types.MultiRange, *, ctx: Context) -> uuid.UUID:\n    subtypes = [\n        _describe_type(st, ctx=ctx)\n        for st in t.get_subtypes(ctx.schema)\n    ]\n\n    assert len(subtypes) == 1\n    type_id = _get_collection_type_id(t.get_schema_name(), subtypes)\n\n    if type_id in ctx.uuid_to_pos:\n        return type_id\n\n    buf = []\n\n    # .tag\n    buf.append(DescriptorTag.MULTIRANGE._value_)\n    # .id\n    buf.append(type_id.bytes)\n\n    if ctx.protocol_version >= (2, 0):\n        # .name\n        buf.append(_name_packer(t.get_name(ctx.schema)))\n        # .schema_defined\n        buf.append(_bool_packer(t.get_is_persistent(ctx.schema)))\n        # .ancestors\n        buf.append(_type_ref_seq_packer([], ctx=ctx))\n\n    # .type\n    buf.append(_type_ref_id_packer(subtypes[0], ctx=ctx))\n\n    return _finish_typedesc(type_id, buf, ctx=ctx)\n\n\n# ObjectType (representing a shape) -> ObjectShapeDescriptor\n@_describe_type.register\ndef _describe_object_shape(\n    t: s_objtypes.ObjectType,\n    *,\n    ctx: Context,\n) -> uuid.UUID:\n    ctx.schema, mt = t.material_type(ctx.schema)\n    base_type_name = str(mt.get_name(ctx.schema))\n\n    subtypes = []\n    element_names = []\n    link_props = []\n    links = []\n    cardinalities: list[enums.Cardinality] = []\n    sources = []\n\n    metadata = ctx.view_shapes_metadata.get(t)\n    implicit_id = metadata is not None and metadata.has_implicit_id\n\n    for ptr in ctx.view_shapes.get(t, ()):\n        name = ptr.get_shortname(ctx.schema).name\n        if not name.startswith(ctx.name_filter):\n            continue\n        name = name.removeprefix(ctx.name_filter)\n        if ptr.singular(ctx.schema):\n            if isinstance(ptr, s_links.Link) and not ctx.follow_links:\n                uuid_t = ctx.schema.get('std::uuid', type=s_scalars.ScalarType)\n                subtype_id = _describe_type(uuid_t, ctx=ctx)\n            else:\n                tgt = ptr.get_target(ctx.schema)\n                assert tgt is not None\n                subtype_id = _describe_type(tgt, ctx=ctx)\n        else:\n            if isinstance(ptr, s_links.Link) and not ctx.follow_links:\n                raise errors.InternalServerError(\n                    'cannot describe multi links when follow_links=False'\n                )\n            else:\n                tgt = ptr.get_target(ctx.schema)\n                assert tgt is not None\n                subtype_id = _describe_set(tgt, ctx=ctx)\n        subtypes.append(subtype_id)\n        element_names.append(name)\n        link_props.append(False)\n        links.append(not ptr.is_property())\n        cardinalities.append(cardinality_from_ptr(ptr, ctx.schema))\n        ctx.schema, material_ptr = ptr.material_type(ctx.schema)\n        ptr_source = material_ptr.get_source(ctx.schema)\n        assert isinstance(ptr_source, s_objtypes.ObjectType)\n        ctx.schema, ptr_source = ptr_source.material_type(ctx.schema)\n        assert ptr_source is not None\n        sources.append(ptr_source)\n\n    t_rptr = t.get_rptr(ctx.schema)\n    if t_rptr is not None and (rptr_ptrs := ctx.view_shapes.get(t_rptr)):\n        # There are link properties in the mix\n        for ptr in rptr_ptrs:\n            tgt = ptr.get_target(ctx.schema)\n            assert tgt is not None\n            if ptr.singular(ctx.schema):\n                subtype_id = _describe_type(tgt, ctx=ctx)\n            else:\n                subtype_id = _describe_set(tgt, ctx=ctx)\n            subtypes.append(subtype_id)\n            element_names.append(ptr.get_shortname(ctx.schema).name)\n            link_props.append(True)\n            links.append(False)\n            cardinalities.append(cardinality_from_ptr(ptr, ctx.schema))\n            # XXX: link properties do not support polymorphism currently\n            sources.append(mt)\n\n    assert len(subtypes) == len(element_names)\n    type_id = _get_object_shape_id(\n        base_type_name,\n        subtypes,\n        element_names,\n        cardinalities,\n        links_props=link_props,\n        links=links,\n        has_implicit_fields=implicit_id,\n    )\n\n    if type_id in ctx.uuid_to_pos:\n        return type_id\n\n    is_free_object_type = t.is_free_object_type(ctx.schema)\n\n    buf = []\n\n    # .tag\n    buf.append(DescriptorTag.SHAPE._value_)\n    # .id\n    buf.append(type_id.bytes)\n\n    if ctx.protocol_version >= (2, 0):\n        # .ephemeral_free_shape\n        buf.append(_bool_packer(is_free_object_type))\n        # .type\n        if is_free_object_type:\n            buf.append(_uint16_packer(0))\n        else:\n            obj_type_id = _describe_object_type(mt, ctx=ctx)\n            buf.append(_type_ref_id_packer(obj_type_id, ctx=ctx))\n\n    # .element_count\n    buf.append(_uint16_packer(len(subtypes)))\n    # .elements\n    for el_name, el_type_id, el_lp, el_l, el_c, el_src in (\n        zip(element_names, subtypes, link_props, links, cardinalities, sources)\n    ):\n        flags = 0\n        if el_lp:\n            flags |= ShapePointerFlags.IS_LINKPROP\n        if (implicit_id and el_name == 'id') or el_name == '__tid__':\n            if el_type_id != UUID_TYPE_ID:\n                raise errors.InternalServerError(\n                    f\"{el_name!r} is expected to be a 'std::uuid' singleton\")\n            flags |= ShapePointerFlags.IS_IMPLICIT\n        elif el_name == '__tname__':\n            if el_type_id != STR_TYPE_ID:\n                raise errors.InternalServerError(\n                    f\"{el_name!r} is expected to be a 'std::str' singleton\")\n            flags |= ShapePointerFlags.IS_IMPLICIT\n        if el_l:\n            flags |= ShapePointerFlags.IS_LINK\n\n        # ShapeElement.flags\n        buf.append(_uint32_packer(flags))\n        # ShapeElement.cardinality\n        buf.append(_uint8_packer(el_c.value))\n\n        # ShapeElement.name\n        buf.append(_string_packer(el_name))\n        # ShapeElement.type\n        buf.append(_type_ref_id_packer(el_type_id, ctx=ctx))\n\n        if ctx.protocol_version >= (2, 0):\n            # .source_type\n            if not is_free_object_type:\n                src_type_id = _describe_object_type(el_src, ctx=ctx)\n                buf.append(_type_ref_id_packer(src_type_id, ctx=ctx))\n            else:\n                buf.append(_uint16_packer(0))\n\n    return _finish_typedesc(type_id, buf, ctx=ctx)\n\n\ndef _describe_object_type(\n    t: s_objtypes.ObjectType,\n    *,\n    ctx: Context,\n) -> uuid.UUID:\n    if t.is_compound_type(ctx.schema):\n        return _describe_compound_object_type(t, ctx=ctx)\n    else:\n        return _describe_regular_object_type(t, ctx=ctx)\n\n\n# ObjectType (regular) -> ObjectTypeDescriptor\ndef _describe_regular_object_type(\n    t: s_objtypes.ObjectType,\n    *,\n    ctx: Context,\n) -> uuid.UUID:\n    if ctx.protocol_version < (2, 0):\n        raise AssertionError(\n            f\"cannot describe material object type {t.get_name(ctx.schema)!r} \"\n            f\"in protocol < 2.0\"\n        )\n\n    buf = []\n    type_id = t.id\n\n    if type_id in ctx.uuid_to_pos:\n        # already described\n        return type_id\n\n    # .tag\n    buf.append(DescriptorTag.OBJECT._value_)\n    # .id\n    buf.append(type_id.bytes)\n    # .name\n    buf.append(_name_packer(t.get_name(ctx.schema)))\n    # .schema_defined\n    buf.append(_bool_packer(True))\n\n    return _finish_typedesc(type_id, buf, ctx=ctx)\n\n\n# ObjectType (compound) -> CompoundTypeDescriptor\ndef _describe_compound_object_type(\n    t: s_objtypes.ObjectType,\n    *,\n    ctx: Context,\n) -> uuid.UUID:\n    if ctx.protocol_version < (2, 0):\n        raise AssertionError(\n            f\"cannot describe compound object type {t.get_name(ctx.schema)!r} \"\n            \"in protocol < 2.0\"\n        )\n\n    buf = []\n    type_id = t.id\n\n    if type_id in ctx.uuid_to_pos:\n        # already described\n        return type_id\n\n    components = t.get_union_of(ctx.schema).objects(ctx.schema)\n    if components:\n        op = CompoundOp.UNION\n    else:\n        components = t.get_intersection_of(ctx.schema).objects(ctx.schema)\n        if not components:\n            raise AssertionError(\n                f\"{t.get_name(ctx.schema)} is not a compound type\")\n        op = CompoundOp.INTERSECTION\n\n    # .tag\n    buf.append(DescriptorTag.COMPOUND._value_)\n    # .id\n    buf.append(type_id.bytes)\n    # .name\n    buf.append(_name_packer(t.get_name(ctx.schema)))\n    # .schema_defined\n    buf.append(_bool_packer(False))\n    # .op\n    buf.append(_uint8_packer(op))\n    # .components\n    buf.append(_type_ref_id_seq_packer(\n        [_describe_object_type(c, ctx=ctx) for c in components],\n        ctx=ctx,\n    ))\n\n    return _finish_typedesc(type_id, buf, ctx=ctx)\n\n\n@_describe_type.register\ndef _describe_scalar_type(\n    t: s_scalars.ScalarType,\n    *,\n    ctx: Context,\n) -> uuid.UUID:\n    ctx.schema, smt = t.material_type(ctx.schema)\n    type_id = smt.id\n    if type_id in ctx.uuid_to_pos:\n        # already described\n        return type_id\n\n    if smt.is_enum(ctx.schema):\n        return _describe_enum(smt, ctx=ctx)\n    else:\n        return _describe_regular_scalar(smt, ctx=ctx)\n\n\n# ScalarType (regular) -> [Base]ScalarTypeDescriptor\ndef _describe_regular_scalar(\n    t: s_scalars.ScalarType,\n    *,\n    ctx: Context,\n) -> uuid.UUID:\n    buf = []\n    fundamental_type = t.get_topmost_concrete_base(ctx.schema)\n    type_id = t.id\n    type_is_fundamental = t == fundamental_type\n\n    if ctx.protocol_version >= (2, 0):\n        # .tag\n        buf.append(DescriptorTag.SCALAR._value_)\n        # .id\n        buf.append(type_id.bytes)\n        # .name\n        buf.append(_name_packer(t.get_name(ctx.schema)))\n        # .schema_defined\n        buf.append(_bool_packer(True))\n        # .ancestors_count\n        # .ancestors\n        if type_is_fundamental:\n            buf.append(_uint16_packer(0))\n        else:\n            ancestors = []\n            for ancestor in t.get_ancestors(ctx.schema).objects(ctx.schema):\n                ancestors.append(ancestor)\n                if ancestor == fundamental_type:\n                    break\n            buf.append(_type_ref_seq_packer(ancestors, ctx=ctx))\n    else:\n        if type_is_fundamental:\n            # .tag\n            buf.append(DescriptorTag.BASE_SCALAR._value_)\n            # .id\n            buf.append(type_id.bytes)\n        else:\n            # .tag\n            buf.append(DescriptorTag.SCALAR._value_)\n            # .id\n            buf.append(type_id.bytes)\n            # .base_type_pos\n            buf.append(_type_ref_packer(fundamental_type, ctx=ctx))\n            if ctx.inline_typenames:\n                _add_annotation(t, ctx=ctx)\n\n    return _finish_typedesc(type_id, buf, ctx=ctx)\n\n\n# ScalarType (enum) -> EnumTypeDescriptor\ndef _describe_enum(\n    enum: s_scalars.ScalarType,\n    *,\n    ctx: Context,\n) -> uuid.UUID:\n    buf = []\n    enum_values = enum.get_enum_values(ctx.schema)\n    assert enum_values is not None\n\n    type_id = enum.id\n\n    # .tag\n    buf.append(DescriptorTag.ENUM._value_)\n    # .id\n    buf.append(type_id.bytes)\n\n    if ctx.protocol_version >= (2, 0):\n        # .name\n        buf.append(_name_packer(enum.get_name(ctx.schema)))\n        # .schema_defined\n        buf.append(_bool_packer(True))\n        # .ancestors\n        ancestors = []\n        topmost = enum.get_topmost_concrete_base(ctx.schema)\n        if enum != topmost:\n            for ancestor in enum.get_ancestors(ctx.schema).objects(ctx.schema):\n                ancestors.append(ancestor)\n                if ancestor == topmost:\n                    break\n        buf.append(_type_ref_seq_packer(ancestors, ctx=ctx))\n\n    # .member_count\n    buf.append(_uint16_packer(len(enum_values)))\n    # .members\n    for enum_val in enum_values:\n        buf.append(_string_packer(enum_val))\n\n    if ctx.protocol_version < (2, 0) and ctx.inline_typenames:\n        _add_annotation(enum, ctx=ctx)\n\n    return _finish_typedesc(type_id, buf, ctx=ctx)\n\n\n@overload\ndef describe_input_shape(\n    t: s_types.Type,\n    input_shapes: InputShapeMap,\n    *,\n    prepare_state: Literal[False],\n    ctx: Context,\n) -> uuid.UUID:\n    ...\n\n\n@overload\ndef describe_input_shape(\n    t: s_types.Type,\n    input_shapes: InputShapeMap,\n    *,\n    ctx: Context,\n) -> uuid.UUID:\n    ...\n\n\n@overload\ndef describe_input_shape(\n    t: s_types.Type,\n    input_shapes: InputShapeMap,\n    *,\n    prepare_state: Literal[True],\n    ctx: Context,\n) -> None:\n    ...\n\n\ndef describe_input_shape(\n    t: s_types.Type,\n    input_shapes: InputShapeMap,\n    *,\n    prepare_state: bool = False,\n    ctx: Context,\n) -> Optional[uuid.UUID]:\n    if t in input_shapes:\n        element_names = []\n        subtypes = []\n        cardinalities = []\n        for name, subtype, cardinality in input_shapes[t]:\n            if (\n                cardinality == enums.Cardinality.MANY or\n                cardinality == enums.Cardinality.AT_LEAST_ONE\n            ):\n                subtype_id = _describe_set(subtype, ctx=ctx)\n            else:\n                subtype_id = describe_input_shape(\n                    subtype, input_shapes, ctx=ctx)\n            element_names.append(name)\n            subtypes.append(subtype_id)\n            cardinalities.append(cardinality)\n\n        assert len(subtypes) == len(element_names)\n\n        if prepare_state:\n            return None\n\n        ctx.schema, mt = t.material_type(ctx.schema)\n        base_type_name = str(mt.get_name(ctx.schema))\n\n        type_id = _get_object_shape_id(\n            base_type_name, subtypes, element_names, cardinalities)\n\n        if type_id in ctx.uuid_to_pos:\n            return type_id\n\n        buf = []\n        # .tag\n        buf.append(DescriptorTag.INPUT_SHAPE._value_)\n        # .id\n        buf.append(type_id.bytes)\n        # .element_count\n        buf.append(_uint16_packer(len(subtypes)))\n        # .elements\n        for el_name, el_type_id, el_c in (\n            zip(element_names, subtypes, cardinalities)\n        ):\n            # ShapeElement.flags\n            buf.append(_uint32_packer(0))\n            # ShapeElement.cardinality\n            buf.append(_uint8_packer(el_c.value))\n            # ShapeElement.name\n            buf.append(_string_packer(el_name))\n            # ShapeElement.type\n            buf.append(_type_ref_id_packer(el_type_id, ctx=ctx))\n\n        return _finish_typedesc(type_id, buf, ctx=ctx)\n    else:\n        return _describe_type(t, ctx=ctx)\n\n\ndef _add_annotation(t: s_types.Type, *, ctx: Context) -> None:\n    buf = []\n    # .tag\n    buf.append(DescriptorTag.ANNO_TYPENAME._value_)\n    # .id\n    buf.append(t.id.bytes)\n    # .annotation\n    buf.append(_string_packer(t.get_displayname(ctx.schema)))\n\n    desc = b''.join(buf)\n    if ctx.protocol_version >= (2, 0):\n        ctx.anno_buffer.append(_uint32_packer(len(desc)))\n    ctx.anno_buffer.append(desc)\n\n\ndef describe_params(\n    *,\n    schema: s_schema.Schema,\n    params: list[tuple[str, s_types.Type, bool]],\n    protocol_version: edbdef.ProtocolVersion,\n) -> tuple[bytes, uuid.UUID]:\n    if not params:\n        return NULL_TYPE_DESC, NULL_TYPE_ID\n\n    ctx = Context(\n        schema=schema,\n        protocol_version=protocol_version,\n    )\n    params_buf = []\n\n    subtypes = []\n    element_names = []\n    cardinalities = []\n\n    for param_name, param_type, param_req in params:\n        param_type_id = _describe_type(param_type, ctx=ctx)\n        # ShapeElement.flags\n        params_buf.append(_uint32_packer(0))\n        # ShapeElement.cardinality\n        card = (\n            p_enums.Cardinality.ONE if param_req else\n            p_enums.Cardinality.AT_MOST_ONE\n        )\n        cardinalities.append(card)\n        params_buf.append(_uint8_packer(card._value_))\n        # ShapeElement.name\n        params_buf.append(_string_packer(param_name))\n        element_names.append(param_name)\n        # ShapeElement.type\n        params_buf.append(_type_ref_id_packer(param_type_id, ctx=ctx))\n        subtypes.append(param_type_id)\n        if protocol_version >= (2, 0):\n            # ShapeElement.source_type\n            params_buf.append(_uint16_packer(0))\n\n    params_id = _get_object_shape_id(\n        \"std::FreeObject\", subtypes, element_names, cardinalities)\n\n    params_shape = [\n        DescriptorTag.SHAPE._value_,\n        params_id.bytes,\n    ]\n\n    if protocol_version >= (2, 0):\n        # .ephemeral_free_shape\n        params_shape.append(_bool_packer(True))\n        # .type\n        params_shape.append(_uint16_packer(0))\n\n    params_shape.extend([\n        _uint16_packer(len(params)),\n        *params_buf,\n    ])\n\n    _finish_typedesc(params_id, params_shape, ctx=ctx)\n\n    full_params = b''.join([\n        *ctx.buffer,\n        *ctx.anno_buffer,\n    ])\n\n    return full_params, params_id\n\n\ndef describe_sql_result(\n    *,\n    schema: s_schema.Schema,\n    row: dict[str, s_types.Type],\n    protocol_version: edbdef.ProtocolVersion,\n) -> tuple[bytes, uuid.UUID]:\n    ctx = Context(\n        schema=schema,\n        protocol_version=protocol_version,\n    )\n\n    params_buf = []\n\n    subtypes = []\n    element_names = []\n\n    for rel_name, rel_t in row.items():\n        rel_type_id = _describe_type(rel_t, ctx=ctx)\n        # SQLRecordElement.name\n        params_buf.append(_string_packer(rel_name))\n        element_names.append(rel_name)\n        # SQLRecordElement.type\n        params_buf.append(_type_ref_id_packer(rel_type_id, ctx=ctx))\n        subtypes.append(rel_type_id)\n\n    rec_id = _get_object_shape_id(\"SQLRow\", subtypes, element_names)\n\n    record_body_bytes = [\n        DescriptorTag.SQL_ROW._value_,\n        rec_id.bytes,\n    ]\n\n    record_body_bytes.extend([\n        _uint16_packer(len(row)),\n        *params_buf,\n    ])\n\n    _finish_typedesc(rec_id, record_body_bytes, ctx=ctx)\n\n    record = b''.join([\n        *ctx.buffer,\n        *ctx.anno_buffer,\n    ])\n\n    return record, rec_id\n\n\ndef describe(\n    schema: s_schema.Schema,\n    typ: s_types.Type,\n    view_shapes: ViewShapeMap = immutables.Map(),\n    view_shapes_metadata: ViewShapeMetadataMap = immutables.Map(),\n    *,\n    protocol_version: edbdef.ProtocolVersion,\n    follow_links: bool = True,\n    inline_typenames: bool = False,\n    name_filter: str = \"\",\n) -> tuple[bytes, uuid.UUID]:\n    ctx = Context(\n        schema=schema,\n        view_shapes=view_shapes,\n        view_shapes_metadata=view_shapes_metadata,\n        protocol_version=protocol_version,\n        follow_links=follow_links,\n        inline_typenames=inline_typenames,\n        name_filter=name_filter,\n    )\n    type_id = _describe_type(typ, ctx=ctx)\n    out = b''.join(ctx.buffer) + b''.join(ctx.anno_buffer)\n    return out, type_id\n\n\n#\n# Type descriptor parsing\n#\n\nclass ParseContext:\n    def __init__(\n        self,\n        protocol_version: edbdef.ProtocolVersion,\n    ) -> None:\n        self.protocol_version = protocol_version\n        self.codecs_list: list[TypeDesc] = []\n\n\ndef parse(\n    typedesc: bytes,\n    protocol_version: edbdef.ProtocolVersion,\n) -> TypeDesc:\n    \"\"\"Unmarshal a byte stream with one or more type descriptors.\"\"\"\n    ctx = ParseContext(protocol_version)\n    buf = io.BytesIO(typedesc)\n    wrapped = binwrapper.BinWrapper(buf)\n    while buf.tell() < len(typedesc):\n        _parse(wrapped, ctx=ctx)\n    if not ctx.codecs_list:\n        raise errors.InternalServerError('could not parse type descriptor')\n    return ctx.codecs_list[-1]\n\n\ndef _parse(desc: binwrapper.BinWrapper, ctx: ParseContext) -> None:\n    \"\"\"Unmarshal the next type descriptor from the byte stream.\"\"\"\n    if ctx.protocol_version >= (2, 0):\n        # .length\n        desc.read_bytes(4)\n\n    t = desc.read_bytes(1)\n\n    try:\n        tag = DescriptorTag(t)\n    except ValueError:\n        if (t[0] >= 0x80 and t[0] <= 0xff):\n            # Ignore all type annotations.\n            _parse_string(desc)\n            return\n        else:\n            raise NotImplementedError(\n                f'no codec implementation for Gel data kind {hex(t[0])}')\n    else:\n        ctx.codecs_list.append(_parse_descriptor(tag, desc, ctx=ctx))\n\n\n#\n# Parsing helpers\n#\n\ndef _parse_type_id(desc: binwrapper.BinWrapper) -> uuid.UUID:\n    return uuidgen.from_bytes(desc.read_bytes(16))\n\n\ndef _parse_bool(desc: binwrapper.BinWrapper) -> bool:\n    return bool(desc.read_bytes(1)[0])\n\n\ndef _parse_string(desc: binwrapper.BinWrapper) -> str:\n    b = desc.read_len32_prefixed_bytes()\n    try:\n        return b.decode(\"utf-8\")\n    except UnicodeDecodeError as e:\n        raise errors.InternalServerError(\n            f\"malformed type descriptor: invalid UTF-8 string \"\n            f\"at stream position {desc.tell()}\") from e\n\n\ndef _parse_strings(desc: binwrapper.BinWrapper) -> list[str]:\n    num = desc.read_ui16()\n    return [_parse_string(desc) for _ in range(num)]\n\n\ndef _parse_type_ref(\n    desc: binwrapper.BinWrapper,\n    *,\n    ctx: ParseContext,\n) -> TypeDesc:\n    offset = desc.read_ui16()\n    try:\n        return ctx.codecs_list[offset]\n    except KeyError:\n        raise errors.InternalServerError(\n            f\"malformed type descriptor: dangling type reference: {offset} \"\n            f\"at stream position {desc.tell()}\") from None\n\n\ndef _parse_type_refs(\n    desc: binwrapper.BinWrapper,\n    *,\n    ctx: ParseContext,\n) -> list[TypeDesc]:\n    els = desc.read_ui16()\n    return [_parse_type_ref(desc, ctx=ctx) for _ in range(els)]\n\n\n#\n# Parsing dispatch.\n#\n\n@value_dispatch.value_dispatch\ndef _parse_descriptor(\n    tag: DescriptorTag,\n    desc: binwrapper.BinWrapper,\n    ctx: ParseContext,\n) -> TypeDesc:\n    raise AssertionError(\n        f'no codec implementation for Gel data kind {tag._name_}')\n\n\n@_parse_descriptor.register(DescriptorTag.SET)\ndef _parse_set_descriptor(\n    _tag: DescriptorTag,\n    desc: binwrapper.BinWrapper,\n    ctx: ParseContext,\n) -> SetDesc:\n    # .id\n    tid = _parse_type_id(desc)\n    # .type\n    subtype = _parse_type_ref(desc, ctx=ctx)\n\n    return SetDesc(tid=tid, subtype=subtype)\n\n\n@_parse_descriptor.register(DescriptorTag.OBJECT)\ndef _parse_object_descriptor(\n    _tag: DescriptorTag,\n    desc: binwrapper.BinWrapper,\n    ctx: ParseContext,\n) -> ObjectDesc:\n    if ctx.protocol_version < (2, 0):\n        raise errors.ProtocolError(\n            \"unexpected ObjectTypeDescriptor in protocol \"\n            f\"{ctx.protocol_version[0]}.{ctx.protocol_version[1]}\")\n\n    # .id\n    tid = _parse_type_id(desc)\n    # .name\n    name = _parse_string(desc)\n    # .schema_defined\n    schema_defined = _parse_bool(desc)\n\n    return ObjectDesc(tid=tid, name=name, schema_defined=schema_defined)\n\n\n@_parse_descriptor.register(DescriptorTag.COMPOUND)\ndef _parse_compound_descriptor(\n    _tag: DescriptorTag,\n    desc: binwrapper.BinWrapper,\n    ctx: ParseContext,\n) -> CompoundDesc:\n    if ctx.protocol_version < (2, 0):\n        raise errors.ProtocolError(\n            \"unexpected CompoundTypeDescriptor in protocol \"\n            f\"{ctx.protocol_version[0]}.{ctx.protocol_version[1]}\")\n\n    # .id\n    tid = _parse_type_id(desc)\n    # .name\n    name = _parse_string(desc)\n    # .schema_defined\n    schema_defined = _parse_bool(desc)\n    # .op\n    op_byte = desc.read_ui8()\n    try:\n        op = CompoundOp(op_byte)\n    except ValueError:\n        raise errors.ProtocolError(\n            f\"unexpected op in CompoundTypeDescriptor: {hex(op_byte)}\"\n        )\n    # .components\n    components = _parse_type_refs(desc, ctx=ctx)\n\n    return CompoundDesc(\n        tid=tid,\n        name=name,\n        schema_defined=schema_defined,\n        op=op,\n        components=components,\n    )\n\n\n@_parse_descriptor.register(DescriptorTag.SHAPE)\ndef _parse_shape_descriptor(\n    _tag: DescriptorTag,\n    desc: binwrapper.BinWrapper,\n    ctx: ParseContext,\n) -> ShapeDesc:\n    # .id\n    tid = _parse_type_id(desc)\n\n    objtype = None\n    if ctx.protocol_version >= (2, 0):\n        # .ephemeral_free_shape\n        ephemeral_free_shape = _parse_bool(desc)\n        if ephemeral_free_shape:\n            desc.read_ui16()\n        else:\n            objtype = _parse_type_ref(desc, ctx=ctx)\n\n    # .element_count\n    els = desc.read_ui16()\n    # .elements\n    fields = {}\n    flags = {}\n    cardinalities = {}\n    sources = {}\n    for _ in range(els):\n        # ShapeElement.flags\n        flag = desc.read_ui32()\n        # ShapeElement.cardinality\n        cardinality = enums.Cardinality(desc.read_bytes(1)[0])\n        # ShapeElement.name\n        name = _parse_string(desc)\n        # ShapeElement.type\n        subtype = _parse_type_ref(desc, ctx=ctx)\n        if ctx.protocol_version >= (2, 0):\n            # ShapeElement.source_type\n            sources[name] = _parse_type_ref(desc, ctx=ctx)\n\n        fields[name] = subtype\n        flags[name] = flag\n        if cardinality:\n            cardinalities[name] = cardinality\n\n    return ShapeDesc(\n        tid=tid,\n        type=objtype,\n        flags=flags,\n        fields=fields,\n        cardinalities=cardinalities,\n        sources=sources,\n    )\n\n\n@_parse_descriptor.register(DescriptorTag.INPUT_SHAPE)\ndef _parse_input_shape_descriptor(\n    _tag: DescriptorTag,\n    desc: binwrapper.BinWrapper,\n    ctx: ParseContext,\n) -> InputShapeDesc:\n    # .id\n    tid = _parse_type_id(desc)\n    # .element_count\n    els = desc.read_ui16()\n    # .elements\n    input_fields = {}\n    flags = {}\n    cardinalities = {}\n    fields_list = []\n    for idx in range(els):\n        # ShapeElement.flags\n        flag = desc.read_ui32()\n        # ShapeElement.cardinality\n        cardinality = enums.Cardinality(desc.read_bytes(1)[0])\n        # ShapeElement.name\n        name = _parse_string(desc)\n        # ShapeElement.type\n        subtype = _parse_type_ref(desc, ctx=ctx)\n\n        fields_list.append((name, subtype))\n        input_fields[name] = idx, subtype\n        flags[name] = flag\n        if cardinality:\n            cardinalities[name] = cardinality\n\n    return InputShapeDesc(\n        fields_list=fields_list,\n        tid=tid,\n        flags=flags,\n        fields=input_fields,\n        cardinalities=cardinalities,\n    )\n\n\n@_parse_descriptor.register(DescriptorTag.BASE_SCALAR)\ndef _parse_base_scalar_descriptor(\n    _tag: DescriptorTag,\n    desc: binwrapper.BinWrapper,\n    ctx: ParseContext,\n) -> BaseScalarDesc:\n    if ctx.protocol_version >= (2, 0):\n        raise errors.ProtocolError(\n            \"unexpected BaseScalarDescriptor in protocol \"\n            f\"{ctx.protocol_version[0]}.{ctx.protocol_version[1]}\")\n\n    # .id\n    tid = _parse_type_id(desc)\n\n    return BaseScalarDesc(tid=tid)\n\n\n@_parse_descriptor.register(DescriptorTag.SCALAR)\ndef _parse_scalar_descriptor(\n    _tag: DescriptorTag,\n    desc: binwrapper.BinWrapper,\n    ctx: ParseContext,\n) -> ScalarDesc:\n    # .id\n    tid = _parse_type_id(desc)\n\n    if ctx.protocol_version >= (2, 0):\n        # .name\n        name = _parse_string(desc)\n        # .schema_defined\n        schema_defined = _parse_bool(desc)\n        # .ancestors\n        ancestors = _parse_type_refs(desc, ctx=ctx)\n        if ancestors:\n            fundamental_type = ancestors[-1]\n        else:\n            fundamental_type = None\n    else:\n        name = None\n        schema_defined = None\n        fundamental_type = _parse_type_ref(desc, ctx=ctx)\n        ancestors = None\n\n    return ScalarDesc(\n        tid=tid,\n        name=name,\n        schema_defined=schema_defined,\n        fundamental_type=fundamental_type,\n        ancestors=ancestors,\n    )\n\n\n@_parse_descriptor.register(DescriptorTag.TUPLE)\ndef _parse_tuple_descriptor(\n    _tag: DescriptorTag,\n    desc: binwrapper.BinWrapper,\n    ctx: ParseContext,\n) -> TupleDesc:\n    # .id\n    tid = _parse_type_id(desc)\n\n    if ctx.protocol_version >= (2, 0):\n        # .name\n        name = _parse_string(desc)\n        # .schema_defined\n        schema_defined = _parse_bool(desc)\n        # .ancestors\n        ancestors = _parse_type_refs(desc, ctx=ctx)\n    else:\n        name = None\n        schema_defined = None\n        ancestors = None\n\n    # .element_count\n    # .elements\n    tuple_fields = _parse_type_refs(desc, ctx=ctx)\n\n    return TupleDesc(\n        tid=tid,\n        name=name,\n        schema_defined=schema_defined,\n        ancestors=ancestors,\n        fields=tuple_fields,\n    )\n\n\n@_parse_descriptor.register(DescriptorTag.NAMEDTUPLE)\ndef _parse_namedtuple_descriptor(\n    _tag: DescriptorTag,\n    desc: binwrapper.BinWrapper,\n    ctx: ParseContext,\n) -> NamedTupleDesc:\n    # .id\n    tid = _parse_type_id(desc)\n\n    if ctx.protocol_version >= (2, 0):\n        # .name\n        name = _parse_string(desc)\n        # .schema_defined\n        schema_defined = _parse_bool(desc)\n        # .ancestors\n        ancestors = _parse_type_refs(desc, ctx=ctx)\n    else:\n        name = None\n        schema_defined = None\n        ancestors = None\n\n    # .element_count\n    els = desc.read_ui16()\n    fields = {}\n    for _ in range(els):\n        # TupleElement.name\n        el_name = _parse_string(desc)\n        # TupleElement.type\n        fields[el_name] = _parse_type_ref(desc, ctx=ctx)\n\n    return NamedTupleDesc(\n        tid=tid,\n        name=name,\n        schema_defined=schema_defined,\n        ancestors=ancestors,\n        fields=fields,\n    )\n\n\n@_parse_descriptor.register(DescriptorTag.ENUM)\ndef _parse_enum_descriptor(\n    _tag: DescriptorTag,\n    desc: binwrapper.BinWrapper,\n    ctx: ParseContext,\n) -> EnumDesc:\n    # .id\n    tid = _parse_type_id(desc)\n\n    if ctx.protocol_version >= (2, 0):\n        # .name\n        name = _parse_string(desc)\n        # .schema_defined\n        schema_defined = _parse_bool(desc)\n        # .ancestors\n        ancestors = _parse_type_refs(desc, ctx=ctx)\n    else:\n        name = None\n        schema_defined = None\n        ancestors = None\n\n    # .member_count\n    # .members\n    names = _parse_strings(desc)\n\n    return EnumDesc(\n        tid=tid,\n        name=name,\n        schema_defined=schema_defined,\n        ancestors=ancestors,\n        names=names,\n    )\n\n\n@_parse_descriptor.register(DescriptorTag.ARRAY)\ndef _parse_array_descriptor(\n    _tag: DescriptorTag,\n    desc: binwrapper.BinWrapper,\n    ctx: ParseContext,\n) -> ArrayDesc:\n    # .id\n    tid = _parse_type_id(desc)\n\n    if ctx.protocol_version >= (2, 0):\n        # .name\n        name = _parse_string(desc)\n        # .schema_defined\n        schema_defined = _parse_bool(desc)\n        # .ancestors\n        ancestors = _parse_type_refs(desc, ctx=ctx)\n    else:\n        name = None\n        schema_defined = None\n        ancestors = None\n\n    # .type\n    subtype = _parse_type_ref(desc, ctx=ctx)\n    # .dimension_count\n    els = desc.read_ui16()\n    if els != 1:\n        raise NotImplementedError(\n            'cannot handle arrays with more than one dimension')\n    # .dimensions\n    dim_len = desc.read_i32()\n    if dim_len != -1:\n        raise NotImplementedError(\n            'cannot handle arrays with non-infinite dimensions')\n\n    return ArrayDesc(\n        tid=tid,\n        name=name,\n        schema_defined=schema_defined,\n        ancestors=ancestors,\n        dim_len=dim_len,\n        subtype=subtype,\n    )\n\n\n@_parse_descriptor.register(DescriptorTag.RANGE)\ndef _parse_range_descriptor(\n    _tag: DescriptorTag,\n    desc: binwrapper.BinWrapper,\n    ctx: ParseContext,\n) -> RangeDesc:\n    # .id\n    tid = _parse_type_id(desc)\n\n    if ctx.protocol_version >= (2, 0):\n        # .name\n        name = _parse_string(desc)\n        # .schema_defined\n        schema_defined = _parse_bool(desc)\n        # .ancestors\n        ancestors = _parse_type_refs(desc, ctx=ctx)\n    else:\n        name = None\n        schema_defined = None\n        ancestors = None\n\n    # .type\n    subtype = _parse_type_ref(desc, ctx=ctx)\n\n    return RangeDesc(\n        tid=tid,\n        name=name,\n        schema_defined=schema_defined,\n        ancestors=ancestors,\n        inner=subtype,\n    )\n\n\n@_parse_descriptor.register(DescriptorTag.MULTIRANGE)\ndef _parse_multirange_descriptor(\n    _tag: DescriptorTag,\n    desc: binwrapper.BinWrapper,\n    ctx: ParseContext,\n) -> MultiRangeDesc:\n    # .id\n    tid = _parse_type_id(desc)\n\n    if ctx.protocol_version >= (2, 0):\n        # .name\n        name = _parse_string(desc)\n        # .schema_defined\n        schema_defined = _parse_bool(desc)\n        # .ancestors\n        ancestors = _parse_type_refs(desc, ctx=ctx)\n    else:\n        name = None\n        schema_defined = None\n        ancestors = None\n\n    # .type\n    subtype = _parse_type_ref(desc, ctx=ctx)\n\n    return MultiRangeDesc(\n        tid=tid,\n        name=name,\n        schema_defined=schema_defined,\n        ancestors=ancestors,\n        inner=subtype,\n    )\n\n\ndef _make_global_rep(typ: s_types.Type, ctx: Context) -> object:\n    if isinstance(typ, s_types.Tuple):\n        subtyps = typ.get_subtypes(ctx.schema)\n        return (\n            int(enums.TypeTag.TUPLE),\n            tuple(subtyp.id for subtyp in subtyps),\n            tuple(_make_global_rep(subtyp, ctx) for subtyp in subtyps),\n        )\n    elif isinstance(typ, s_types.Array):\n        subtyp = typ.get_element_type(ctx.schema)\n        return (\n            int(enums.TypeTag.ARRAY), subtyp.id, _make_global_rep(subtyp, ctx))\n    else:\n        return None\n\n\nclass StateSerializerFactory:\n    def __init__(self, std_schema: s_schema.Schema, config_spec: config.Spec):\n        \"\"\"\n        {\n            module := 'default',\n            aliases := [ ('alias', 'module::target'), ... ],\n            config := cfg::Config {\n                session_idle_transaction_timeout: <duration>'0:05:00',\n                query_execution_timeout: <duration>'0:00:00',\n                allow_bare_ddl: AlwaysAllow,\n                apply_access_policies: true,\n            },\n            globals := { key := value, ... },\n        }\n\n        \"\"\"\n        schema = std_schema\n        str_type = schema.get('std::str', type=s_scalars.ScalarType)\n        free_obj = schema.get('std::FreeObject', type=s_objtypes.ObjectType)\n        schema, self._state_type = derive_alias(schema, free_obj, 'state_type')\n\n        # aliases := { ('alias1', 'mod::type'), ... }\n        schema, alias_tuple = s_types.Tuple.from_subtypes(\n            schema, [str_type, str_type])\n        schema, aliases_array = s_types.Array.from_subtypes(\n            schema, [alias_tuple])\n\n        schema, self.globals_type = derive_alias(\n            schema, free_obj, 'state_globals')\n\n        # config := cfg::Config { session_cfg1, session_cfg2, ... }\n        schema, config_type = derive_alias(\n            schema, free_obj, 'state_config'\n        )\n        config_shape = self._make_config_shape(config_spec, schema)\n\n        # Build type descriptors and codecs for compiler RPC\n        # comp_config := cfg::Config { comp_cfg1, comp_cfg2, ... }\n        schema, self._comp_config_type = derive_alias(\n            schema, free_obj, 'comp_config'\n        )\n        self._comp_config_shape: tuple[InputShapeElement, ...] = (\n            self._make_config_shape(\n                config_spec,\n                schema,\n                lambda setting: setting.affects_compilation,\n            )\n        )\n\n        self._input_shapes: immutables.Map[\n            s_types.Type,\n            tuple[InputShapeElement, ...],\n        ] = immutables.Map([\n            (config_type, config_shape),\n            (self._state_type, (\n                (\"module\", str_type, enums.Cardinality.AT_MOST_ONE),\n                (\"aliases\", aliases_array, enums.Cardinality.AT_MOST_ONE),\n                (\"config\", config_type, enums.Cardinality.AT_MOST_ONE),\n            ))\n        ])\n        self.config_type = config_type\n        self._schema = schema\n        self._contexts: dict[edbdef.ProtocolVersion, Context] = {}\n\n    @staticmethod\n    def _make_config_shape(\n        config_spec: config.Spec,\n        schema: s_schema.Schema,\n        matches: Callable[[Any], bool] = lambda setting: not setting.system,\n    ) -> tuple[InputShapeElement, ...]:\n        config_shape: list[InputShapeElement] = []\n\n        for setting in config_spec.values():\n            if matches(setting):\n                setting_type_name = setting.schema_type_name\n                setting_type = schema.get(setting_type_name, type=s_types.Type)\n                config_shape.append(\n                    (\n                        setting.name,\n                        setting_type,\n                        enums.Cardinality.MANY if setting.set_of else\n                        enums.Cardinality.AT_MOST_ONE,\n                    )\n                )\n        return tuple(sorted(config_shape))\n\n    def make(\n        self,\n        user_schema: s_schema.Schema,\n        global_schema: s_schema.Schema,\n        protocol_version: edbdef.ProtocolVersion,\n    ) -> StateSerializer:\n        ctx = self._contexts.get(protocol_version)\n        if ctx is None:\n            ctx = Context(\n                schema=self._schema,\n                protocol_version=protocol_version,\n            )\n            self._contexts[protocol_version] = ctx\n            describe_input_shape(\n                self._state_type,\n                self._input_shapes,\n                prepare_state=True,\n                ctx=ctx,\n            )\n\n        ctx = ctx.derive()\n        ctx.schema = s_schema.ChainedSchema(\n            self._schema, user_schema, global_schema)\n\n        # Update the config shape with any extension configs\n        ext_config_spec = config.load_ext_spec_from_schema(\n            user_schema, self._schema)\n        new_config = self._make_config_shape(ext_config_spec, ctx.schema)\n        full_config = self._input_shapes[self.config_type] + new_config\n\n        globals_shape = []\n        global_reps = {}\n        # Only look at user_schema for globals, since system defined ones\n        # are only set by the system.\n        for g in user_schema.get_objects(type=s_globals.Global):\n            if g.is_computable(ctx.schema):\n                continue\n            name = str(g.get_name(ctx.schema))\n            s_type = g.get_target(ctx.schema)\n            if isinstance(s_type, (s_types.Array, s_types.Tuple)):\n                global_reps[name] = _make_global_rep(s_type, ctx)\n            cardinality = cardinality_from_ptr(g, ctx.schema)\n            globals_shape.append((name, s_type, cardinality))\n\n        type_id = describe_input_shape(\n            self._state_type,\n            self._input_shapes.update({\n                self.globals_type: tuple(sorted(globals_shape)),\n                self.config_type: full_config,\n                self._state_type: self._input_shapes[self._state_type] + (\n                    (\n                        \"globals\",\n                        self.globals_type,\n                        enums.Cardinality.AT_MOST_ONE,\n                    ),\n                )\n            }),\n            ctx=ctx,\n        )\n\n        type_data = b''.join(ctx.buffer)\n        return StateSerializer(\n            type_id, type_data, global_reps, protocol_version\n        )\n\n    def make_compilation_config_serializer(self) -> CompilationConfigSerializer:\n        ctx = Context(\n            schema=self._schema,\n            protocol_version=edbdef.CURRENT_PROTOCOL,\n        )\n        type_id = describe_input_shape(\n            self._comp_config_type,\n            {self._comp_config_type: self._comp_config_shape},\n            ctx=ctx\n        )\n        type_data = b''.join(ctx.buffer)\n        return CompilationConfigSerializer(\n            type_id,\n            type_data,\n            edbdef.CURRENT_PROTOCOL\n        )\n\n\nclass InputShapeSerializer:\n    def __init__(\n        self,\n        type_id: uuid.UUID,\n        type_data: bytes,\n        protocol_version: edbdef.ProtocolVersion,\n    ) -> None:\n        self._type_id = type_id\n        self._type_data = type_data\n        self._protocol_version = protocol_version\n\n    @functools.cached_property\n    def _codec(self) -> InputShapeDesc:\n        codec = parse(self._type_data, self._protocol_version)\n        assert isinstance(codec, InputShapeDesc)\n        return codec\n\n    @property\n    def type_id(self) -> uuid.UUID:\n        return self._type_id\n\n    def describe(self) -> tuple[uuid.UUID, bytes]:\n        return self._type_id, self._type_data\n\n    def encode(self, state: Any) -> bytes:\n        return self._codec.encode(state)\n\n    def decode(self, state: bytes) -> Any:\n        return self._codec.decode(state)\n\n\nclass StateSerializer(InputShapeSerializer):\n    def __init__(\n        self,\n        type_id: uuid.UUID,\n        type_data: bytes,\n        global_reps: dict[str, object],\n        protocol_version: edbdef.ProtocolVersion,\n    ) -> None:\n        super().__init__(type_id, type_data, protocol_version)\n        self._global_reps = global_reps\n\n    @functools.cached_property\n    def _codec(self) -> InputShapeDesc:\n        codec = super()._codec\n\n        # Global values are directly used in Postgres, so we don't need to\n        # encode/decode them in the I/O server. This feature doesn't worth a\n        # separate switch in the type desc, so we just hack it in here.\n        _, globals_type_desc = codec.fields['globals']\n        assert isinstance(globals_type_desc, InputShapeDesc)\n        globals_type_desc.__dict__['data_raw'] = True\n\n        return codec\n\n    def get_global_type_rep(\n        self,\n        global_name: str,\n    ) -> Optional[object]:\n        return self._global_reps.get(global_name)\n\n\nclass CompilationConfigSerializer(InputShapeSerializer):\n    @lru.lru_method_cache(64)\n    def encode_configs(\n        self, *configs: immutables.Map[str, config.SettingValue] | None\n    ) -> bytes:\n        state: dict[str, Any] = {}\n        for conf in configs:\n            if conf is not None:\n                state.update((k, v.value) for k, v in conf.items())\n        return self.encode(state)\n\n\ndef derive_alias(\n    schema: s_schema.Schema,\n    parent: s_objtypes.ObjectType,\n    qualifier: str,\n) -> tuple[s_schema.Schema, s_types.InheritingType]:\n    return parent.derive_subtype(\n        schema,\n        name=s_obj.derive_name(\n            schema,\n            qualifier,\n            module='__derived__',\n            parent=parent,\n        ),\n        mark_derived=True,\n        inheritance_refdicts={'pointers'},\n        attrs={'expr_type': s_types.ExprType.Select},\n    )\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass TypeDesc:\n    tid: uuid.UUID\n\n    def encode(self, data: Any) -> bytes:\n        raise NotImplementedError\n\n    def decode(self, data: bytes) -> Any:\n        raise NotImplementedError\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass SequenceDesc(TypeDesc):\n    subtype: TypeDesc\n    impl: ClassVar[type[s_obj.CollectionFactory[Any]]]\n\n    def encode(self, data: collections.abc.Collection[Any]) -> bytes:\n        if not data:\n            return b''.join((\n                _uint32_packer(0),\n                _uint32_packer(0),\n                _uint32_packer(0),\n            ))\n        bufs = [\n            _uint32_packer(1),\n            _uint32_packer(0),\n            _uint32_packer(0),\n            _uint32_packer(len(data)),\n            _uint32_packer(1),\n        ]\n        for item in data:\n            if item is None:\n                bufs.append(_int32_packer(-1))\n            else:\n                item_bytes = self.subtype.encode(item)\n                bufs.append(_uint32_packer(len(item_bytes)))\n                bufs.append(item_bytes)\n        return b''.join(bufs)\n\n    def decode(self, data: bytes) -> collections.abc.Collection[Any]:\n        buf = io.BytesIO(data)\n        wrapped = binwrapper.BinWrapper(buf)\n        ndims = wrapped.read_ui32()\n        if ndims == 0:\n            return self.impl()\n        assert ndims == 1\n        wrapped.read_ui32()\n        wrapped.read_ui32()\n        data_len = wrapped.read_ui32()\n        assert wrapped.read_ui32() == 1\n        return self.impl(\n            self.subtype.decode(wrapped.read_len32_prefixed_bytes())\n            for _ in range(data_len)\n        )\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass SetDesc(SequenceDesc):\n    impl = frozenset\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass ShapeDesc(TypeDesc):\n    type: Optional[TypeDesc]\n    fields: dict[str, TypeDesc]\n    flags: dict[str, int]\n    cardinalities: dict[str, enums.Cardinality]\n    sources: dict[str, TypeDesc]\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass SchemaTypeDesc(TypeDesc):\n    name: Optional[str] = None\n    schema_defined: Optional[bool] = None\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass ObjectDesc(SchemaTypeDesc):\n    pass\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass CompoundDesc(SchemaTypeDesc):\n    op: CompoundOp\n    components: list[TypeDesc]\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass BaseScalarDesc(SchemaTypeDesc):\n    codecs: ClassVar[dict[\n        uuid.UUID,\n        tuple[Callable[[Any], bytes], Callable[[bytes], Any]]\n    ]] = {\n        s_obj.get_known_type_id('std::duration'): (\n            statypes.Duration.encode,\n            statypes.Duration.decode,\n        ),\n        s_obj.get_known_type_id('std::str'): (\n            _encode_str,\n            _decode_str,\n        ),\n        s_obj.get_known_type_id('std::bool'): (\n            _encode_bool,\n            _decode_bool,\n        ),\n        s_obj.get_known_type_id('std::int64'): (\n            _encode_int64,\n            _decode_int64,\n        ),\n        s_obj.get_known_type_id('std::float32'): (\n            _encode_float32,\n            _decode_float32,\n        ),\n    }\n\n    def encode(self, data: Any) -> bytes:\n        if codecs := self.codecs.get(self.tid):\n            return codecs[0](data)\n        raise NotImplementedError\n\n    def decode(self, data: bytes) -> Any:\n        if codecs := self.codecs.get(self.tid):\n            return codecs[1](data)\n        raise NotImplementedError\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass ScalarDesc(BaseScalarDesc):\n    fundamental_type: Optional[TypeDesc]\n    ancestors: Optional[list[TypeDesc]]\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass NamedTupleDesc(SchemaTypeDesc):\n    fields: dict[str, TypeDesc]\n    ancestors: Optional[list[TypeDesc]]\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass TupleDesc(SchemaTypeDesc):\n    fields: list[TypeDesc]\n    ancestors: Optional[list[TypeDesc]]\n\n    def encode(self, data: collections.abc.Sequence[Any]) -> bytes:\n        bufs = [_uint32_packer(len(self.fields))]\n        for idx, desc in enumerate(self.fields):\n            bufs.append(_uint32_packer(0))\n            item = desc.encode(data[idx])\n            bufs.append(_uint32_packer(len(item)))\n            bufs.append(item)\n        return b''.join(bufs)\n\n    def decode(self, data: bytes) -> tuple[Any, ...]:\n        buf = io.BytesIO(data)\n        wrapped = binwrapper.BinWrapper(buf)\n        assert wrapped.read_ui32() == len(self.fields)\n        rv = []\n        for desc in self.fields:\n            wrapped.read_ui32()\n            rv.append(desc.decode(wrapped.read_len32_prefixed_bytes()))\n        return tuple(rv)\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass EnumDesc(SchemaTypeDesc):\n    names: list[str]\n    ancestors: Optional[list[TypeDesc]]\n\n    @functools.cached_property\n    def _decoder(self) -> Callable[[bytes], Any]:\n        assert self.name is not None\n        pytype = statypes.maybe_get_python_type_for_scalar_type_name(self.name)\n        if pytype is not None and issubclass(pytype, statypes.ScalarType):\n            return pytype.decode\n        else:\n            return _decode_str\n\n    @functools.cached_property\n    def _encoder(self) -> Callable[[Any], bytes]:\n        assert self.name is not None\n        pytype = statypes.maybe_get_python_type_for_scalar_type_name(self.name)\n        if pytype is not None and issubclass(pytype, statypes.ScalarType):\n            return pytype.encode\n        else:\n            return _encode_str\n\n    def encode(self, data: Any) -> bytes:\n        return self._encoder(data)\n\n    def decode(self, data: bytes) -> Any:\n        return self._decoder(data)\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass ArrayDesc(SequenceDesc, SchemaTypeDesc):\n    ancestors: Optional[list[TypeDesc]]\n    dim_len: int\n    impl = list\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass RangeDesc(SchemaTypeDesc):\n    ancestors: Optional[list[TypeDesc]]\n    inner: TypeDesc\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass MultiRangeDesc(SchemaTypeDesc):\n    ancestors: Optional[list[TypeDesc]]\n    inner: TypeDesc\n\n\n@dataclasses.dataclass(frozen=True, kw_only=True)\nclass InputShapeDesc(TypeDesc):\n    fields: dict[str, tuple[int, TypeDesc]]\n    fields_list: list[tuple[str, TypeDesc]]\n    flags: dict[str, int]\n    cardinalities: dict[str, enums.Cardinality]\n    data_raw: bool = False\n\n    def encode(self, data: Mapping[str, Any]) -> bytes:\n        bufs = [b'']\n        count = 0\n        for key, desc_tuple in self.fields.items():\n            if key not in data:\n                continue\n            value = data[key]\n\n            idx, desc = desc_tuple\n            bufs.append(_uint32_packer(idx))\n\n            if value is None:\n                bufs.append(_int32_packer(-1))\n            else:\n                if not self.data_raw:\n                    value = desc.encode(value)\n                bufs.append(_uint32_packer(len(value)))\n                bufs.append(value)\n\n            count += 1\n        bufs[0] = _uint32_packer(count)\n        return b''.join(bufs)\n\n    def decode(self, data: bytes) -> dict[str, Any]:\n        rv = {}\n        buf = io.BytesIO(data)\n        wrapped = binwrapper.BinWrapper(buf)\n        for _ in range(wrapped.read_ui32()):\n            idx = wrapped.read_ui32()\n            name, desc = self.fields_list[idx]\n            item_data = wrapped.read_nullable_len32_prefixed_bytes()\n            if item_data is None:\n                cardinality = self.cardinalities.get(name)\n                if cardinality == enums.Cardinality.ONE:\n                    raise errors.CardinalityViolationError(\n                        f\"State '{name}' expects exactly 1 value, 0 given\"\n                    )\n                elif cardinality == enums.Cardinality.AT_LEAST_ONE:\n                    raise errors.CardinalityViolationError(\n                        f\"State '{name}' expects at least 1 value, 0 given\"\n                    )\n\n            if self.data_raw or item_data is None:\n                rv[name] = item_data\n            else:\n                rv[name] = desc.decode(item_data)\n        return rv\n"
  },
  {
    "path": "edb/server/compiler/sql.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Mapping, Sequence, TYPE_CHECKING, Optional, Any\n\nimport dataclasses\nimport functools\nimport hashlib\nimport immutables\nimport json\n\nfrom edb import errors\nfrom edb.common import ast\nfrom edb.common import uuidgen\nfrom edb.common import debug\nfrom edb.server import defines\n\nfrom edb.schema import schema as s_schema\n\nfrom edb.pgsql import ast as pgast\nfrom edb.pgsql import common as pg_common\nfrom edb.pgsql import codegen as pg_codegen\nfrom edb.pgsql import params as pg_params\nfrom edb.pgsql import parser as pg_parser\n\nfrom . import dbstate\nfrom . import enums\n\nif TYPE_CHECKING:\n    from edb.pgsql import resolver as pg_resolver\n\n\n# Frontend-only settings. Maps setting name into their mutability flag.\nFE_SETTINGS_MUTABLE: immutables.Map[str, bool] = immutables.Map(\n    {\n        'search_path': True,\n        'allow_user_specified_id': True,\n        'apply_access_policies_pg': True,\n        'server_version': False,\n        'server_version_num': False,\n    }\n)\n\n\nclass DisableNormalization(BaseException):\n    # An exception that indicates that the compiler cannot work with this query\n    # because the constants have been extracted and replaced with parameters.\n    # When raised, the query will be recompiled without normalization.\n    pass\n\n\ndef compile_sql(\n    source: pg_parser.Source,\n    *,\n    schema: s_schema.Schema,\n    tx_state: dbstate.SQLTransactionState,\n    prepared_stmt_map: Mapping[str, str],\n    current_database: str,\n    allow_user_specified_id: Optional[bool],\n    apply_access_policies: Optional[bool],\n    include_edgeql_io_format_alternative: bool = False,\n    allow_prepared_statements: bool = True,\n    disambiguate_column_names: bool,\n    backend_runtime_params: pg_params.BackendRuntimeParams,\n    protocol_version: defines.ProtocolVersion,\n    implicit_limit: Optional[int] = None,\n) -> tuple[list[dbstate.SQLQueryUnit], bool]:\n    def _try(\n        q: str, normalized_params: list[int]\n    ) -> list[dbstate.SQLQueryUnit]:\n        return _compile_sql(\n            q,\n            orig_query_str=source.original_text(),\n            schema=schema,\n            tx_state=tx_state,\n            prepared_stmt_map=prepared_stmt_map,\n            current_database=current_database,\n            allow_user_specified_id=allow_user_specified_id,\n            apply_access_policies=apply_access_policies,\n            include_edgeql_io_format_alternative=(\n                include_edgeql_io_format_alternative),\n            allow_prepared_statements=allow_prepared_statements,\n            disambiguate_column_names=disambiguate_column_names,\n            backend_runtime_params=backend_runtime_params,\n            protocol_version=protocol_version,\n            normalized_params=normalized_params,\n            implicit_limit=implicit_limit,\n        )\n\n    normalized_params = list(source.extra_type_oids())\n    try:\n        try:\n            return _try(source.text(), normalized_params), False\n        except DisableNormalization:\n            # compiler requested non-normalized query (it needs it for static\n            # evaluation)\n            try:\n                if isinstance(source, pg_parser.NormalizedSource):\n                    units = _try(source.original_text(), [])\n                    # Unit isn't cacheable, since the key is the\n                    # extracted version.\n                    # TODO: Can we tell the server to cache using non-extracted?\n                    for unit in units:\n                        unit.cacheable = False\n                    return units, True\n            except DisableNormalization:\n                pass\n\n            raise AssertionError(\n                \"compiler is requesting query normalization to be disabled,\"\n                \"but it already is disabled\"\n            )\n    except errors.EdgeDBError as original_err:\n        if isinstance(source, pg_parser.NormalizedSource):\n            # try non-normalized source\n            try:\n                _try(source.original_text(), [])\n            except errors.EdgeDBError as denormalized_err:\n                raise denormalized_err\n            except Exception:\n                raise original_err\n            else:\n                raise AssertionError(\n                    \"Normalized query is broken while original is valid\")\n        else:\n            raise original_err\n\n\ndef _build_constant_extraction_map(\n    src: pgast.Base,\n    out: pgast.Base,\n) -> pg_codegen.BaseSourceMap:\n    \"\"\"Traverse two ASTs in parallel and build a source map between them.\n\n    The ASTs should *mostly* line up. When they don't, that is\n    considered a leaf.\n\n    This is used to translate SQL spans reported on a normalized query\n    to ones that make sense on the pre-normalization version.\n\n    Note that we only use this map for errors reported during the\n    \"parse\" phase, so we don't need to worry about it being reused\n    with different constants.\n    \"\"\"\n    tdata = pg_codegen.BaseSourceMap(\n        source_start=src.span.start if src.span else 0,\n        # HACK: I don't know why, but this - 1 helps a lot.\n        output_start=out.span.start - 1 if out.span else 0,\n    )\n    if type(src) is not type(out):\n        return tdata\n    children = tdata.children\n    for (k1, v1), (k2, v2) in zip(ast.iter_fields(src), ast.iter_fields(out)):\n        assert k1 == k2\n\n        if isinstance(v1, pgast.Base) and isinstance(v2, pgast.Base):\n            children.append(_build_constant_extraction_map(v1, v2))\n        elif (\n            isinstance(v1, (tuple, list)) and isinstance(v2, (tuple, list))\n        ):\n            for v1e, v2e in zip(v1, v2):\n                if isinstance(v1e, pgast.Base) and isinstance(v2e, pgast.Base):\n                    children.append(_build_constant_extraction_map(v1e, v2e))\n        elif (\n            isinstance(v1, dict) and isinstance(v2, dict)\n        ):\n            for k, v1e in v1.items():\n                v2e = v2.get(k)\n                if isinstance(v1e, pgast.Base) and isinstance(v2e, pgast.Base):\n                    children.append(_build_constant_extraction_map(v1e, v2e))\n\n    children.sort(key=lambda k: k.output_start)\n\n    return tdata\n\n\ndef _compile_sql(\n    query_str: str,\n    *,\n    orig_query_str: Optional[str] = None,\n    schema: s_schema.Schema,\n    tx_state: dbstate.SQLTransactionState,\n    prepared_stmt_map: Mapping[str, str],\n    current_database: str,\n    allow_user_specified_id: Optional[bool],\n    apply_access_policies: Optional[bool],\n    include_edgeql_io_format_alternative: bool = False,\n    allow_prepared_statements: bool = True,\n    disambiguate_column_names: bool,\n    backend_runtime_params: pg_params.BackendRuntimeParams,\n    protocol_version: defines.ProtocolVersion,\n    normalized_params: list[int],\n    implicit_limit: Optional[int] = None,\n) -> list[dbstate.SQLQueryUnit]:\n    opts = ResolverOptionsPartial(\n        query_str=query_str,\n        current_database=current_database,\n        allow_user_specified_id=allow_user_specified_id,\n        apply_access_policies=apply_access_policies,\n        include_edgeql_io_format_alternative=(\n            include_edgeql_io_format_alternative\n        ),\n        disambiguate_column_names=disambiguate_column_names,\n        normalized_params=normalized_params,\n        implicit_limit=implicit_limit,\n    )\n\n    # orig_stmts are the statements prior to constant extraction\n    stmts = pg_parser.parse(query_str, propagate_spans=True)\n    if orig_query_str and orig_query_str != query_str:\n        orig_stmts = pg_parser.parse(orig_query_str, propagate_spans=True)\n    else:\n        orig_stmts = stmts\n\n    sql_units = []\n    for stmt, orig_stmt in zip(stmts, orig_stmts):\n        orig_text = pg_codegen.generate_source(stmt)\n        fe_settings = tx_state.current_fe_settings()\n        track_stats = False\n\n        extract_data = _build_constant_extraction_map(orig_stmt, stmt)\n\n        unit = dbstate.SQLQueryUnit(\n            orig_query=pg_codegen.generate_source(orig_stmt),\n            fe_settings=fe_settings,\n            # by default, the query is sent to PostgreSQL unchanged\n            query=orig_text,\n        )\n\n        if isinstance(stmt, (pgast.VariableSetStmt, pgast.VariableResetStmt)):\n            if protocol_version != defines.POSTGRES_PROTOCOL:\n                from edb.pgsql import resolver as pg_resolver\n                pg_resolver.dispatch._raise_unsupported(stmt)\n\n            value: Optional[dbstate.SQLSetting]\n            if isinstance(stmt, pgast.VariableSetStmt):\n                value = pg_arg_list_to_python(stmt.args)\n            else:\n                value = None\n\n            fe_only = stmt.name and (\n                # GOTCHA: setting is frontend-only regardless of its mutability\n                stmt.name in FE_SETTINGS_MUTABLE\n                or stmt.name.startswith('global ')\n            )\n\n            if fe_only:\n                assert stmt.name\n                if not FE_SETTINGS_MUTABLE.get(stmt.name, True):\n                    raise errors.QueryError(\n                        f'parameter \"{stmt.name}\" cannot be changed',\n                        pgext_code='55P02',  # cant_change_runtime_param\n                    )\n\n                unit.set_vars = {stmt.name: value}\n                unit.frontend_only = True\n                unit.command_complete_tag = dbstate.TagPlain(\n                    tag=(\n                        b\"SET\"\n                        if isinstance(stmt, pgast.VariableSetStmt)\n                        else b\"RESET\"\n                    )\n                )\n            elif stmt.scope == pgast.OptionsScope.SESSION:\n                unit.set_vars = {stmt.name: value}\n\n            unit.is_local = stmt.scope == pgast.OptionsScope.TRANSACTION\n            if not unit.is_local:\n                unit.capabilities |= enums.Capability.SESSION_CONFIG\n\n            unit.capabilities |= enums.Capability.SQL_SESSION_CONFIG\n\n        elif isinstance(stmt, pgast.VariableShowStmt):\n            if protocol_version != defines.POSTGRES_PROTOCOL:\n                from edb.pgsql import resolver as pg_resolver\n                pg_resolver.dispatch._raise_unsupported(stmt)\n\n            stmt = _compile_show_command(stmt)\n            unit.query = pg_codegen.generate_source(stmt)\n            unit.command_complete_tag = dbstate.TagPlain(tag=b\"SHOW\")\n\n        elif isinstance(stmt, pgast.SetTransactionStmt):\n            if protocol_version != defines.POSTGRES_PROTOCOL:\n                from edb.pgsql import resolver as pg_resolver\n                pg_resolver.dispatch._raise_unsupported(stmt)\n\n            if stmt.scope == pgast.OptionsScope.SESSION:\n                unit.set_vars = {\n                    f\"default_{name}\": (\n                        (\n                            value.val\n                            if isinstance(value, pgast.StringConstant)\n                            else pg_codegen.generate_source(value)\n                        ),\n                    )\n                    for name, value in stmt.options.options.items()\n                }\n\n            unit.capabilities |= enums.Capability.SQL_SESSION_CONFIG\n\n        elif isinstance(stmt, (pgast.BeginStmt, pgast.StartStmt)):\n            unit.tx_action = dbstate.TxAction.START\n            unit.command_complete_tag = dbstate.TagPlain(\n                tag=(\n                    b\"START TRANSACTION\"\n                    if isinstance(stmt, pgast.StartStmt)\n                    else b\"BEGIN\"\n                )\n            )\n        elif isinstance(stmt, pgast.CommitStmt):\n            unit.tx_action = dbstate.TxAction.COMMIT\n            unit.tx_chain = stmt.chain or False\n            unit.command_complete_tag = dbstate.TagPlain(tag=b\"COMMIT\")\n\n        elif isinstance(stmt, pgast.RollbackStmt):\n            unit.tx_action = dbstate.TxAction.ROLLBACK\n            unit.tx_chain = stmt.chain or False\n            unit.command_complete_tag = dbstate.TagPlain(tag=b\"ROLLBACK\")\n\n        elif isinstance(stmt, pgast.SavepointStmt):\n            unit.tx_action = dbstate.TxAction.DECLARE_SAVEPOINT\n            unit.sp_name = stmt.savepoint_name\n            unit.command_complete_tag = dbstate.TagPlain(tag=b\"SAVEPOINT\")\n\n        elif isinstance(stmt, pgast.ReleaseStmt):\n            unit.tx_action = dbstate.TxAction.RELEASE_SAVEPOINT\n            unit.sp_name = stmt.savepoint_name\n            unit.command_complete_tag = dbstate.TagPlain(tag=b\"RELEASE\")\n\n        elif isinstance(stmt, pgast.RollbackToStmt):\n            unit.tx_action = dbstate.TxAction.ROLLBACK_TO_SAVEPOINT\n            unit.sp_name = stmt.savepoint_name\n            unit.command_complete_tag = dbstate.TagPlain(tag=b\"ROLLBACK\")\n\n        elif isinstance(stmt, pgast.TwoPhaseTransactionStmt):\n            raise NotImplementedError(\n                \"two-phase transactions are not supported\"\n            )\n        elif isinstance(stmt, pgast.PrepareStmt):\n            if not allow_prepared_statements:\n                raise errors.UnsupportedFeatureError(\n                    \"SQL prepared statements are not supported\"\n                )\n\n            if not isinstance(stmt.query, (pgast.Query, pgast.CopyStmt)):\n                from edb.pgsql import resolver as pg_resolver\n                pg_resolver.dispatch._raise_unsupported(stmt.query)\n\n            # Translate the underlying query.\n            stmt_resolved, stmt_source, _ = resolve_query(\n                stmt.query, schema, tx_state, opts\n            )\n            if stmt.argtypes:\n                param_types = []\n                for pt in stmt.argtypes:\n                    param_types.append(pg_codegen.generate_source(pt))\n                param_text = f\"({', '.join(param_types)})\"\n            else:\n                param_text = \"\"\n\n            sql_trailer = f\"{param_text} AS ({stmt_source.text})\"\n\n            mangled_stmt_name = compute_stmt_name(\n                f\"PREPARE {pg_common.quote_ident(stmt.name)}{sql_trailer}\",\n                tx_state,\n            )\n\n            sql_text = (\n                f\"PREPARE {pg_common.quote_ident(mangled_stmt_name)}\"\n                f\"{sql_trailer}\"\n            )\n\n            unit.query = sql_text\n            unit.prepare = dbstate.PrepareData(\n                stmt_name=stmt.name,\n                be_stmt_name=mangled_stmt_name.encode(\"utf-8\"),\n                query=stmt_source.text,\n                source_map=stmt_source.source_map,\n            )\n            unit.command_complete_tag = dbstate.TagPlain(tag=b\"PREPARE\")\n            unit.capabilities |= stmt_resolved.capabilities\n            track_stats = True\n\n        elif isinstance(stmt, pgast.ExecuteStmt):\n            if not allow_prepared_statements:\n                raise errors.UnsupportedFeatureError(\n                    \"SQL prepared statements are not supported\"\n                )\n\n            orig_name = stmt.name\n            mangled_name = prepared_stmt_map.get(orig_name)\n            if not mangled_name:\n                raise errors.QueryError(\n                    f\"prepared statement \\\"{orig_name}\\\" does \" f\"not exist\",\n                    pgext_code='26000',  # invalid_sql_statement_name\n                )\n            stmt.name = mangled_name\n\n            unit.query = pg_codegen.generate_source(stmt)\n            unit.execute = dbstate.ExecuteData(\n                stmt_name=orig_name,\n                be_stmt_name=mangled_name.encode(\"utf-8\"),\n            )\n            unit.cardinality = enums.Cardinality.MANY\n            track_stats = True\n\n        elif isinstance(stmt, pgast.DeallocateStmt):\n            if not allow_prepared_statements:\n                raise errors.UnsupportedFeatureError(\n                    \"SQL prepared statements are not supported\"\n                )\n            orig_name = stmt.name\n            mangled_name = prepared_stmt_map.get(orig_name)\n            if not mangled_name:\n                raise errors.QueryError(\n                    f\"prepared statement \\\"{orig_name}\\\" does \" f\"not exist\",\n                    pgext_code='26000',  # invalid_sql_statement_name\n                )\n            stmt.name = mangled_name\n\n            unit.query = pg_codegen.generate_source(stmt)\n            unit.deallocate = dbstate.DeallocateData(\n                stmt_name=orig_name,\n                be_stmt_name=mangled_name.encode(\"utf-8\"),\n            )\n            unit.command_complete_tag = dbstate.TagPlain(tag=b\"DEALLOCATE\")\n\n        elif isinstance(stmt, pgast.LockStmt):\n            if stmt.mode not in ('ACCESS SHARE', 'ROW SHARE', 'SHARE'):\n                raise NotImplementedError(\"exclusive lock is not supported\")\n            # just ignore\n            unit.query = \"DO $$ BEGIN END $$;\"\n        elif isinstance(stmt, (pgast.Query, pgast.CopyStmt)):\n            if (\n                protocol_version != defines.POSTGRES_PROTOCOL\n                and isinstance(stmt, pgast.CopyStmt)\n            ):\n                from edb.pgsql import resolver as pg_resolver\n                pg_resolver.dispatch._raise_unsupported(stmt)\n\n            stmt_resolved, stmt_source, edgeql_fmt_src = resolve_query(\n                stmt, schema, tx_state, opts\n            )\n            unit.query = stmt_source.text\n            unit.source_map = stmt_source.source_map\n            if stmt_source.source_map:\n                unit.source_map = (\n                    pg_codegen.ChainedSourceMap([\n                        stmt_source.source_map,\n                        extract_data,\n                    ])\n                )\n\n            if edgeql_fmt_src is not None:\n                unit.eql_format_query = edgeql_fmt_src.text\n                # We don't do anything with the translation data for\n                # this query, since postgres typically doesn't report\n                # out error positions that didn't get reported during\n                # the \"parse\" phase.\n            unit.command_complete_tag = stmt_resolved.command_complete_tag\n            unit.params = stmt_resolved.params\n            if isinstance(stmt, pgast.DMLQuery) and not stmt.returning_list:\n                unit.cardinality = enums.Cardinality.NO_RESULT\n            else:\n                unit.cardinality = enums.Cardinality.MANY\n            unit.capabilities |= stmt_resolved.capabilities\n            track_stats = True\n        else:\n            from edb.pgsql import resolver as pg_resolver\n            pg_resolver.dispatch._raise_unsupported(stmt)\n\n        unit.stmt_name = compute_stmt_name(unit.query, tx_state).encode(\"utf-8\")\n\n        sql_info: dict[str, Any] = {}\n        if track_stats and backend_runtime_params.has_stat_statements:\n            cconfig: dict[str, dbstate.SQLSetting] = {\n                k: v for k, v in fe_settings.items()\n                if k is not None and v is not None and k in FE_SETTINGS_MUTABLE\n            }\n            cconfig.pop('server_version', None)\n            cconfig.pop('server_version_num', None)\n            if allow_user_specified_id is not None:\n                cconfig.setdefault(\n                    'allow_user_specified_id',\n                    ('true' if allow_user_specified_id else 'false',),\n                )\n            if apply_access_policies is not None:\n                cconfig.setdefault(\n                    'apply_access_policies',\n                    ('true' if apply_access_policies else 'false',),\n                )\n            search_path = parse_search_path(cconfig.pop(\"search_path\", (\"\",)))\n            cconfig = dict(sorted((k, v) for k, v in cconfig.items()))\n            extras = {\n                'cc': cconfig,  # compilation_config\n                'pv': protocol_version,  # protocol_version\n                'dn': ', '.join(search_path),  # default_namespace\n            }\n            sql_info['query'] = orig_text,\n            sql_info['type'] = defines.QueryType.SQL,\n            sql_info['extras'] = json.dumps(extras),\n            id_hash = hashlib.blake2b(digest_size=16)\n            id_hash.update(\n                json.dumps(sql_info).encode(defines.EDGEDB_ENCODING)\n            )\n            sql_info['id'] = str(uuidgen.from_bytes(id_hash.digest()))\n\n        if debug.flags.sql_text_in_sql:\n            sql_info['sql'] = orig_query_str or query_str\n\n        if sql_info:\n            prefix = ''.join([\n                '-- ',\n                json.dumps(sql_info),\n                '\\n',\n            ])\n            unit.prefix_len = len(prefix)\n            unit.query = prefix + unit.query\n            if unit.eql_format_query is not None:\n                unit.eql_format_query = prefix + unit.eql_format_query\n\n        if unit.tx_action is not None:\n            unit.capabilities |= enums.Capability.TRANSACTION\n\n        tx_state.apply(unit)\n        sql_units.append(unit)\n\n    if not sql_units:\n        # Cluvio will try to execute an empty query\n        sql_units.append(\n            dbstate.SQLQueryUnit(\n                orig_query='',\n                query='',\n                fe_settings=tx_state.current_fe_settings(),\n            )\n        )\n\n    return sql_units\n\n\ndef _compile_show_command(stmt: pgast.VariableShowStmt) -> pgast.Query:\n    pg_settings_for_show = pgast.RelRangeVar(\n        relation=pgast.Relation(\n            schemaname=pg_common.versioned_schema('edgedbsql'),\n            name='pg_settings_for_show',\n        )\n    )\n\n    if stmt.name.lower() == 'all':\n        return pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    val=pgast.ColumnRef(name=[pgast.Star()])\n                )\n            ],\n            from_clause=[pg_settings_for_show],\n        )\n\n    elif stmt.name.startswith(\"global \"):\n        # SELECT coalesce(\n        #     (SELECT value\n        #      FROM _edgecon_state\n        #      WHERE name = 'global ..' AND type = 'L'\n        #     ),\n        #     (SELECT value\n        #      FROM _edgecon_state\n        #      WHERE name = 'global ..' AND type = 'S'\n        #     )\n        # ) #>> '{}' AS \"global ..\"\n        sublinks: list[pgast.Base] = [\n            pgast.SubLink(\n                operator=None,\n                expr=pgast.SelectStmt(\n                    target_list=[\n                        pgast.ResTarget(\n                            val=pgast.ColumnRef(name=['value']),\n                        ),\n                    ],\n                    from_clause=[\n                        pgast.RelRangeVar(\n                            relation=pgast.Relation(\n                                name='_edgecon_state'\n                            ),\n                        ),\n                    ],\n                    where_clause=pgast.Expr(\n                        name='AND',\n                        lexpr=pgast.Expr(\n                            name='=',\n                            lexpr=pgast.ColumnRef(name=['name']),\n                            rexpr=pgast.StringConstant(val=stmt.name),\n                        ),\n                        rexpr=pgast.Expr(\n                            name='=',\n                            lexpr=pgast.ColumnRef(name=['type']),\n                            rexpr=pgast.StringConstant(val=typ),\n                        ),\n                    ),\n                )\n            )\n            for typ in [\"L\", \"S\"]\n        ]\n        return pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    name=stmt.name,\n                    val=pgast.Expr(\n                        name='#>>',\n                        lexpr=pgast.CoalesceExpr(args=sublinks),\n                        rexpr=pgast.StringConstant(val='{}'),\n                    ),\n                )\n            ]\n        )\n\n    else:\n        return pgast.SelectStmt(\n            target_list=[\n                pgast.ResTarget(\n                    name=stmt.name,\n                    val=pgast.ColumnRef(name=['setting']),\n                ),\n            ],\n            from_clause=[pg_settings_for_show],\n            where_clause=pgast.Expr(\n                name='=',\n                lexpr=pgast.ColumnRef(name=['name']),\n                rexpr=pgast.StringConstant(val=stmt.name),\n            )\n        )\n\n\n@dataclasses.dataclass(kw_only=True, eq=False, repr=False)\nclass ResolverOptionsPartial:\n    current_database: str\n    query_str: str\n    allow_user_specified_id: Optional[bool]\n    apply_access_policies: Optional[bool]\n    include_edgeql_io_format_alternative: Optional[bool]\n    disambiguate_column_names: bool\n    normalized_params: list[int]\n    implicit_limit: Optional[int]\n\n\ndef resolve_query(\n    stmt: pgast.Query | pgast.CopyStmt,\n    schema: s_schema.Schema,\n    tx_state: dbstate.SQLTransactionState,\n    opts: ResolverOptionsPartial,\n) -> tuple[\n    pg_resolver.ResolvedSQL,\n    pg_codegen.SQLSource,\n    Optional[pg_codegen.SQLSource],\n]:\n    from edb.pgsql import resolver as pg_resolver\n\n    search_path: Sequence[str] = (\"public\",)\n    try:\n        setting = tx_state.get(\"search_path\")\n    except KeyError:\n        setting = None\n    search_path = parse_search_path(setting)\n\n    allow_user_specified_id = lookup_bool_setting(\n        tx_state, 'allow_user_specified_id'\n    )\n    if allow_user_specified_id is None:\n        allow_user_specified_id = opts.allow_user_specified_id\n    if allow_user_specified_id is None:\n        allow_user_specified_id = False\n\n    apply_access_policies = lookup_bool_setting(\n        tx_state, 'apply_access_policies_pg'\n    )\n    if apply_access_policies is None:\n        apply_access_policies = opts.apply_access_policies\n    if apply_access_policies is None:\n        apply_access_policies = True\n\n    options = pg_resolver.Options(\n        current_database=opts.current_database,\n        current_query=opts.query_str,\n        search_path=search_path,\n        allow_user_specified_id=allow_user_specified_id,\n        apply_access_policies=apply_access_policies,\n        include_edgeql_io_format_alternative=(\n            opts.include_edgeql_io_format_alternative\n        ),\n        disambiguate_column_names=opts.disambiguate_column_names,\n        normalized_params=opts.normalized_params,\n        implicit_limit=opts.implicit_limit,\n    )\n    resolved = pg_resolver.resolve(stmt, schema, options)\n    source = pg_codegen.generate(resolved.ast, with_source_map=True)\n    if resolved.edgeql_output_format_ast is not None:\n        edgeql_format_source = pg_codegen.generate(\n            resolved.edgeql_output_format_ast,\n            with_source_map=True,\n        )\n    else:\n        edgeql_format_source = None\n    return resolved, source, edgeql_format_source\n\n\ndef lookup_bool_setting(\n    tx_state: dbstate.SQLTransactionState, name: str\n) -> Optional[bool]:\n    try:\n        setting = tx_state.get(name)\n    except KeyError:\n        setting = None\n    if setting and setting[0]:\n        return is_setting_truthy(setting[0])\n    return None\n\n\ndef is_setting_truthy(value: str | int | float) -> bool | None:\n    if isinstance(value, int):\n        return value != 0\n    if isinstance(value, str):\n        value = value.lower()\n        if value == 'o':\n            # ambigious\n            return None\n\n        truthy_values = ('on', 'true', 'yes', '1')\n        if any(t.startswith(value) for t in truthy_values):\n            return True\n\n        falsy_values = ('off', 'false', 'no', '0')\n        if any(t.startswith(value) for t in falsy_values):\n            return False\n    return None\n\n\ndef compute_stmt_name(text: str, tx_state: dbstate.SQLTransactionState) -> str:\n    stmt_hash = hashlib.sha1(text.encode(\"utf-8\"))\n    for setting_name in sorted(FE_SETTINGS_MUTABLE):\n        try:\n            setting_value = tx_state.get(setting_name)\n        except KeyError:\n            pass\n        else:\n            stmt_hash.update(f\"{setting_name}:{setting_value}\".encode(\"utf-8\"))\n    return f\"edb{stmt_hash.hexdigest()}\"\n\n\n@functools.cache\ndef parse_search_path(search_path_str: list[str | int | float]) -> list[str]:\n    return [part for part in search_path_str if isinstance(part, str)]\n\n\ndef pg_arg_list_to_python(expr: pgast.ArgsList) -> dbstate.SQLSetting:\n    return tuple(pg_const_to_python(a) for a in expr.args)\n\n\ndef pg_const_to_python(expr: pgast.BaseExpr) -> str | int | float:\n    \"Converts a pg const expression into a Python value\"\n\n    if isinstance(expr, pgast.StringConstant):\n        return expr.val\n\n    if isinstance(expr, pgast.NumericConstant):\n        try:\n            return int(expr.val)\n        except ValueError:\n            return float(expr.val)\n\n    raise NotImplementedError()\n"
  },
  {
    "path": "edb/server/compiler/status.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport functools\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\n\n\n@functools.singledispatch\ndef get_status(ql: qlast.Base) -> bytes:\n    raise NotImplementedError(\n        f'cannot get status for the {type(ql).__name__!r} AST node'\n    )\n\n\n@get_status.register(qlast.CreateObject)\ndef _ddl_create(ql: qlast.CreateObject) -> bytes:\n    return f'CREATE {get_schema_class(ql)}'.encode()\n\n\n@get_status.register(qlast.AlterObject)\ndef _ddl_alter(ql: qlast.AlterObject) -> bytes:\n    return f'ALTER {get_schema_class(ql)}'.encode()\n\n\n@get_status.register(qlast.DropObject)\ndef _ddl_drop(ql: qlast.DropObject) -> bytes:\n    return f'DROP {get_schema_class(ql)}'.encode()\n\n\ndef get_schema_class(ql: qlast.ObjectDDL) -> qltypes.SchemaObjectClass:\n    osc = qltypes.SchemaObjectClass\n    match ql:\n        case qlast.DatabaseCommand(flavor='BRANCH'):\n            return osc.BRANCH\n        case qlast.DatabaseCommand(flavor='DATABASE'):\n            return osc.DATABASE\n        case qlast.FutureCommand():\n            return osc.FUTURE\n        case qlast.ModuleCommand():\n            return osc.MODULE\n        case qlast.RoleCommand():\n            return osc.ROLE\n        case qlast.PropertyCommand():\n            return osc.PROPERTY\n        case qlast.ObjectTypeCommand():\n            return osc.TYPE\n        case qlast.AliasCommand():\n            return osc.ALIAS\n        case qlast.GlobalCommand():\n            return osc.GLOBAL\n        case qlast.PermissionCommand():\n            return osc.PERMISSION\n        case qlast.LinkCommand():\n            return osc.LINK\n        case qlast.IndexCommand():\n            return osc.INDEX\n        case qlast.AccessPolicyCommand():\n            return osc.INDEX_MATCH\n        case qlast.TriggerCommand():\n            return osc.TRIGGER\n        case qlast.RewriteCommand():\n            return osc.REWRITE\n        case qlast.FunctionCommand():\n            return osc.FUNCTION\n        case qlast.OperatorCommand():\n            return osc.OPERATOR\n        case qlast.CastCommand():\n            return osc.CAST\n        case qlast.MigrationCommand():\n            return osc.MIGRATION\n        case qlast.ExtensionPackageCommand():\n            return osc.EXTENSION_PACKAGE\n        case qlast.ExtensionPackageMigrationCommand():\n            return osc.EXTENSION_PACKAGE_MIGRATION\n        case qlast.ExtensionCommand():\n            return osc.EXTENSION\n        case qlast.ExtensionCommand():\n            return osc.EXTENSION\n        case qlast.AnnotationCommand():\n            return osc.ANNOTATION\n        case qlast.PseudoTypeCommand():\n            return osc.PSEUDO_TYPE\n        case qlast.ScalarTypeCommand():\n            return osc.SCALAR_TYPE\n        case qlast.ConstraintCommand():\n            return osc.CONSTRAINT\n        case qlast.AccessPolicyCommand():\n            # Why is this duplicate here?\n            return osc.ACCESS_POLICY\n\n        case _:\n            raise AssertionError('unimplemented')\n\n\n@get_status.register(qlast.StartMigration)\ndef _ddl_migr_start(ql: qlast.Base) -> bytes:\n    return b'START MIGRATION'\n\n\n@get_status.register(qlast.CreateMigration)\ndef _ddl_migr_create(ql: qlast.Base) -> bytes:\n    return b'CREATE MIGRATION'\n\n\n@get_status.register(qlast.CommitMigration)\ndef _ddl_migr_commit(ql: qlast.Base) -> bytes:\n    return b'COMMIT MIGRATION'\n\n\n@get_status.register(qlast.DropMigration)\ndef _ddl_migr_drop(ql: qlast.Base) -> bytes:\n    return b'DROP MIGRATION'\n\n\n@get_status.register(qlast.AlterMigration)\ndef _ddl_migr_alter(ql: qlast.Base) -> bytes:\n    return b'ALTER MIGRATION'\n\n\n@get_status.register(qlast.AbortMigration)\ndef _ddl_migr_abort(ql: qlast.Base) -> bytes:\n    return b'ABORT MIGRATION'\n\n\n@get_status.register(qlast.PopulateMigration)\ndef _ddl_migr_populate(ql: qlast.Base) -> bytes:\n    return b'POPULATE MIGRATION'\n\n\n@get_status.register(qlast.DescribeCurrentMigration)\ndef _ddl_migr_describe_current(ql: qlast.Base) -> bytes:\n    return b'DESCRIBE CURRENT MIGRATION'\n\n\n@get_status.register(qlast.AlterCurrentMigrationRejectProposed)\ndef _ddl_migr_alter_current(ql: qlast.Base) -> bytes:\n    return b'ALTER CURRENT MIGRATION'\n\n\n@get_status.register(qlast.StartMigrationRewrite)\ndef _ddl_migr_rw_start(ql: qlast.Base) -> bytes:\n    return b'START MIGRATION REWRITE'\n\n\n@get_status.register(qlast.CommitMigrationRewrite)\ndef _ddl_migr_rw_commit(ql: qlast.Base) -> bytes:\n    return b'COMMIT MIGRATION REWRITE'\n\n\n@get_status.register(qlast.AbortMigrationRewrite)\ndef _ddl_migr_rw_abort(ql: qlast.Base) -> bytes:\n    return b'ABORT MIGRATION REWRITE'\n\n\n@get_status.register(qlast.ResetSchema)\ndef _ddl_migr_reset_schema(ql: qlast.Base) -> bytes:\n    return b'RESET SCHEMA'\n\n\n@get_status.register(qlast.SelectQuery)\n@get_status.register(qlast.GroupQuery)\n@get_status.register(qlast.InternalGroupQuery)\n@get_status.register(qlast.ForQuery)\ndef _select(ql: qlast.Base) -> bytes:\n    return b'SELECT'\n\n\n@get_status.register(qlast.InsertQuery)\ndef _insert(ql: qlast.Base) -> bytes:\n    return b'INSERT'\n\n\n@get_status.register(qlast.UpdateQuery)\ndef _update(ql: qlast.Base) -> bytes:\n    return b'UPDATE'\n\n\n@get_status.register(qlast.DeleteQuery)\ndef _delete(ql: qlast.Base) -> bytes:\n    return b'DELETE'\n\n\n@get_status.register(qlast.StartTransaction)\ndef _tx_start(ql: qlast.Base) -> bytes:\n    return b'START TRANSACTION'\n\n\n@get_status.register(qlast.CommitTransaction)\ndef _tx_commit(ql: qlast.Base) -> bytes:\n    return b'COMMIT TRANSACTION'\n\n\n@get_status.register(qlast.RollbackTransaction)\ndef _tx_rollback(ql: qlast.Base) -> bytes:\n    return b'ROLLBACK TRANSACTION'\n\n\n@get_status.register(qlast.DeclareSavepoint)\ndef _tx_sp_declare(ql: qlast.Base) -> bytes:\n    return b'DECLARE SAVEPOINT'\n\n\n@get_status.register(qlast.RollbackToSavepoint)\ndef _tx_sp_rollback(ql: qlast.Base) -> bytes:\n    return b'ROLLBACK TO SAVEPOINT'\n\n\n@get_status.register(qlast.ReleaseSavepoint)\ndef _tx_sp_release(ql: qlast.Base) -> bytes:\n    return b'RELEASE SAVEPOINT'\n\n\n@get_status.register(qlast.SessionSetAliasDecl)\ndef _sess_set_alias(ql: qlast.Base) -> bytes:\n    return b'SET ALIAS'\n\n\n@get_status.register(qlast.SessionResetAliasDecl)\n@get_status.register(qlast.SessionResetModule)\n@get_status.register(qlast.SessionResetAllAliases)\ndef _sess_reset_alias(ql: qlast.Base) -> bytes:\n    return b'RESET ALIAS'\n\n\n@get_status.register(qlast.ConfigOp)\ndef _sess_set_config(ql: qlast.ConfigOp) -> bytes:\n    if ql.scope == qltypes.ConfigScope.GLOBAL:\n        if isinstance(ql, qlast.ConfigSet):\n            return b'SET GLOBAL'\n        else:\n            return b'RESET GLOBAL'\n    else:\n        return f'CONFIGURE {ql.scope}'.encode('ascii')\n\n\n@get_status.register(qlast.DescribeStmt)\ndef _describe(ql: qlast.Base) -> bytes:\n    return f'DESCRIBE'.encode()\n\n\n@get_status.register(qlast.Rename)\ndef _rename(ql: qlast.Base) -> bytes:\n    return f'RENAME'.encode()\n\n\n@get_status.register(qlast.ExplainStmt)\ndef _explain(ql: qlast.Base) -> bytes:\n    return b'ANALYZE QUERY'\n\n\n@get_status.register(qlast.AdministerStmt)\ndef _administer(ql: qlast.Base) -> bytes:\n    return b'ADMINISTER'\n\n\n@get_status.register(qlast.DDLQuery)\ndef _query(ql: qlast.DDLQuery) -> bytes:\n    return get_status(ql.query)\n"
  },
  {
    "path": "edb/server/compiler_pool/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom .pool import create_compiler_pool, AbstractPool\n\n\n__all__ = ('create_compiler_pool', 'AbstractPool')\n"
  },
  {
    "path": "edb/server/compiler_pool/amsg.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Callable, cast, Generator, Optional\n\nimport asyncio\nimport os\nimport socket\nimport struct\n\nOnPidCallback = Callable[[\"HubProtocol\", asyncio.Transport, int, int], None]\nOnConnectionLostCallback = Callable[[Optional[int]], None]\n\n\n_uint64_unpacker = struct.Struct('!Q').unpack\n_uint64_packer = struct.Struct('!Q').pack\n\n\nclass MessageStream:\n    \"\"\"Data stream that yields messages.\"\"\"\n\n    _buffer: bytes\n    _curmsg_len: int\n\n    def __init__(self) -> None:\n        self._buffer = b''\n        self._curmsg_len = -1\n\n    def feed_data(self, data: bytes) -> Generator[bytes, None, None]:\n        # TODO: rewrite to avoid buffer copies.\n        self._buffer += data\n        while self._buffer:\n            if self._curmsg_len == -1:\n                if len(self._buffer) >= 8:\n                    self._curmsg_len = _uint64_unpacker(self._buffer[:8])[0]\n                    self._buffer = self._buffer[8:]\n                else:\n                    return\n\n            if self._curmsg_len > 0 and len(self._buffer) >= self._curmsg_len:\n                msg = self._buffer[:self._curmsg_len]\n                self._buffer = self._buffer[self._curmsg_len:]\n                self._curmsg_len = -1\n                yield msg\n            else:\n                return\n\n\nclass HubProtocol(asyncio.Protocol):\n    \"\"\"The Protocol used on the hub side connecting to workers.\"\"\"\n\n    _loop: asyncio.AbstractEventLoop\n    _transport: Optional[asyncio.Transport]\n    _closed: bool\n    _stream: MessageStream\n    _resp_waiters: dict[int, asyncio.Future[memoryview]]\n    _on_pid: OnPidCallback\n    _on_connection_lost: OnConnectionLostCallback\n    _pid: Optional[int]\n\n    def __init__(\n        self,\n        *,\n        loop: asyncio.AbstractEventLoop,\n        on_pid: OnPidCallback,\n        on_connection_lost: OnConnectionLostCallback,\n    ) -> None:\n        self._loop = loop\n        self._transport = None\n        self._closed = False\n        self._stream = MessageStream()\n        self._resp_waiters = {}\n        self._on_pid = on_pid\n        self._on_connection_lost = on_connection_lost\n        self._pid = None\n\n    def connection_made(self, tr: asyncio.BaseTransport) -> None:\n        self._transport = cast(asyncio.Transport, tr)\n\n    def send(\n        self,\n        req_id: int,\n        waiter: asyncio.Future[memoryview],\n        payload: bytes,\n    ) -> None:\n        if req_id in self._resp_waiters:\n            raise RuntimeError('FramedProtocol: duplicate request ID')\n        assert self._transport is not None\n        self._resp_waiters[req_id] = waiter\n        self._transport.writelines(\n            (_uint64_packer(len(payload) + 8), _uint64_packer(req_id), payload)\n        )\n\n    def process_message(self, msg: bytes) -> None:\n        msgview = memoryview(msg)\n        req_id = _uint64_unpacker(msgview[:8])[0]\n        waiter = self._resp_waiters.pop(req_id, None)\n        if waiter is None:\n            # This could have happened if the previous request got cancelled.\n            return\n        if not waiter.done():\n            waiter.set_result(msgview[8:])\n\n    def data_received(self, data: bytes) -> None:\n        if self._pid is None:\n            assert self._transport is not None\n            pid_data = data[:8]\n            version = _uint64_unpacker(data[8:16])[0]\n            data = data[16:]\n            self._pid = _uint64_unpacker(pid_data)[0]\n            self._on_pid(self, self._transport, self._pid, version)\n        for msg in self._stream.feed_data(data):\n            self.process_message(msg)\n\n    def connection_lost(self, exc: Optional[Exception]) -> None:\n        self._closed = True\n\n        if self._resp_waiters:\n            if exc is not None:\n                for waiter in self._resp_waiters.values():\n                    waiter.set_exception(exc)\n            else:\n                for waiter in self._resp_waiters.values():\n                    waiter.set_exception(ConnectionError(\n                        'lost connection to the worker during a call'))\n            self._resp_waiters = {}\n\n        self._on_connection_lost(self._pid)\n\n\nclass HubConnection:\n    \"\"\"An abstraction of the hub connections to the workers.\"\"\"\n\n    _transport: asyncio.Transport\n    _protocol: HubProtocol\n    _loop: asyncio.AbstractEventLoop\n    _req_id_cnt: int\n    _version: int\n    _aborted: bool\n\n    def __init__(\n        self,\n        transport: asyncio.Transport,\n        protocol: HubProtocol,\n        loop: asyncio.AbstractEventLoop,\n        version: int,\n    ) -> None:\n        self._transport = transport\n        self._protocol = protocol\n        self._loop = loop\n        self._req_id_cnt = 0\n        self._version = version\n        self._aborted = False\n\n    def is_closed(self) -> bool:\n        return self._protocol._closed\n\n    async def request(self, data: bytes) -> memoryview:\n        self._req_id_cnt += 1\n        req_id = self._req_id_cnt\n\n        waiter = self._loop.create_future()\n        self._protocol.send(req_id, waiter, data)\n        return await waiter\n\n    def abort(self) -> None:\n        self._aborted = True\n        self._transport.abort()\n\n\nclass WorkerConnection:\n    \"\"\"Connection object used by the worker's process.\"\"\"\n\n    _sock: Optional[socket.socket]\n    _stream: MessageStream\n\n    def __init__(self, sockname: str, version: int) -> None:\n        self._sock = socket.socket(socket.AF_UNIX)\n        self._sock.connect(sockname)\n        self._sock.sendall(\n            _uint64_packer(os.getpid()) + _uint64_packer(version)\n        )\n        self._stream = MessageStream()\n\n    def _on_message(self, msg: bytes) -> tuple[int, memoryview]:\n        msgview = memoryview(msg)\n        req_id = _uint64_unpacker(msgview[:8])[0]\n        return req_id, msgview[8:]\n\n    def reply(self, req_id: int, payload: bytes) -> None:\n        assert self._sock is not None\n        self._sock.sendall(\n            b\"\".join(\n                (\n                    _uint64_packer(len(payload) + 8),\n                    _uint64_packer(req_id),\n                    payload,\n                )\n            )\n        )\n\n    def iter_request(self) -> Generator[tuple[int, memoryview], None, None]:\n        while True:\n            data = b'' if self._sock is None else self._sock.recv(4096)\n            if not data:\n                # EOF received - abort\n                self.abort()\n                return\n            yield from map(self._on_message, self._stream.feed_data(data))\n\n    def abort(self) -> None:\n        if self._sock is not None:\n            self._sock.close()\n            self._sock = None\n\n\nclass ServerProtocol:\n    def worker_connected(self, pid: int, version: int) -> None:\n        pass\n\n    def worker_disconnected(self, pid: int) -> None:\n        pass\n\n\nclass Server:\n\n    _sockname: str\n    _loop: asyncio.AbstractEventLoop\n    _srv: Optional[asyncio.AbstractServer]\n    _pids: dict[int, HubConnection]\n    _proto: ServerProtocol\n\n    def __init__(\n        self,\n        sockname: str,\n        loop: asyncio.AbstractEventLoop,\n        server_protocol: ServerProtocol,\n    ) -> None:\n        self._sockname = sockname\n        self._loop = loop\n        self._srv = None\n        self._pids = {}\n        self._proto = server_protocol\n\n    def _on_pid_connected(\n        self,\n        proto: HubProtocol,\n        tr: asyncio.Transport,\n        pid: int,\n        version: int,\n    ) -> None:\n        assert pid not in self._pids\n        self._pids[pid] = HubConnection(tr, proto, self._loop, version)\n        self._proto.worker_connected(pid, version)\n\n    def _on_pid_disconnected(self, pid: Optional[int]) -> None:\n        if not pid:\n            return\n        if pid in self._pids:\n            self._pids.pop(pid)\n            self._proto.worker_disconnected(pid)\n\n    def _proto_factory(self) -> HubProtocol:\n        return HubProtocol(\n            loop=self._loop,\n            on_pid=self._on_pid_connected,\n            on_connection_lost=self._on_pid_disconnected,\n        )\n\n    def get_by_pid(self, pid: int) -> HubConnection:\n        return self._pids[pid]\n\n    async def start(self) -> None:\n        self._srv = await self._loop.create_unix_server(\n            self._proto_factory,\n            path=self._sockname)\n\n    async def stop(self) -> None:\n        if self._srv is None:\n            return\n        self._srv.close()\n        for con in self._pids.values():\n            con.abort()\n        await self._srv.wait_closed()\n\n    def kill_outdated_worker(self, current_version: int) -> None:\n        for conn in self._pids.values():\n            if conn._version < current_version and not conn._aborted:\n                conn.abort()\n                break\n"
  },
  {
    "path": "edb/server/compiler_pool/multitenant_worker.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Callable, Optional, NamedTuple, Sequence\n\nimport pickle\n\nimport immutables\n\nfrom edb import edgeql\nfrom edb import graphql\n\nfrom edb.common import debug\nfrom edb.common import uuidgen\nfrom edb.pgsql import params as pgparams\nfrom edb.schema import schema as s_schema\nfrom edb.server import compiler\nfrom edb.server import config\nfrom edb.server import defines\n\nfrom . import state\nfrom . import worker_proc\n\n\nINITED: bool = False\nclients: immutables.Map[int, ClientSchema] = immutables.Map()\nBACKEND_RUNTIME_PARAMS: pgparams.BackendRuntimeParams = (\n    pgparams.get_default_runtime_params()\n)\nCOMPILER: compiler.Compiler\nLAST_STATE: Optional[compiler.dbstate.CompilerConnectionState] = None\nSTD_SCHEMA: s_schema.Schema\n\n\nclass ClientSchema(NamedTuple):\n    dbs: state.DatabasesState\n    global_schema: s_schema.Schema\n    instance_config: immutables.Map[str, config.SettingValue]\n\n\ndef __init_worker__(\n    init_args_pickled: bytes,\n) -> None:\n    global INITED\n    global BACKEND_RUNTIME_PARAMS\n    global COMPILER\n    global STD_SCHEMA\n\n    (\n        backend_runtime_params,\n        std_schema,\n        refl_schema,\n        schema_class_layout,\n    ) = pickle.loads(init_args_pickled)\n\n    INITED = True\n    BACKEND_RUNTIME_PARAMS = backend_runtime_params\n    STD_SCHEMA = std_schema\n\n    COMPILER = compiler.new_compiler(\n        std_schema,\n        refl_schema,\n        schema_class_layout,\n        backend_runtime_params=backend_runtime_params,\n        config_spec=None,\n    )\n\n\ndef __sync__(client_id, pickled_schema, invalidation) -> None:\n    global clients\n\n    for cid in invalidation:\n        try:\n            clients = clients.delete(cid)\n        except KeyError:\n            pass\n    try:\n        client_schema: ClientSchema = clients.get(client_id)  # type: ignore\n        if pickled_schema:\n            if client_schema is None:\n                dbs = {\n                    dbname: state.DatabaseState(\n                        dbname,\n                        (\n                            None\n                            if pickled_state.user_schema is None\n                            else pickle.loads(pickled_state.user_schema)\n                        ),\n                        pickle.loads(pickled_state.reflection_cache),\n                        pickle.loads(pickled_state.database_config),\n                    )\n                    for dbname, pickled_state in pickled_schema.dbs.items()\n                }\n                if debug.flags.server:\n                    print(client_id, \"FULL SYNC: \", list(dbs))\n                client_schema = ClientSchema(\n                    immutables.Map(dbs),\n                    pickle.loads(pickled_schema.global_schema),\n                    pickle.loads(pickled_schema.instance_config),\n                )\n                clients = clients.set(client_id, client_schema)\n            else:\n                updates = {}\n                dbs = client_schema.dbs\n                if pickled_schema.dbs is not None:\n                    for dbname, pickled_state in pickled_schema.dbs.items():\n                        db_state = dbs.get(dbname)\n                        if db_state is None:\n                            assert pickled_state.user_schema is not None\n                            assert pickled_state.reflection_cache is not None\n                            assert pickled_state.database_config is not None\n                            db_state = state.DatabaseState(\n                                dbname,\n                                pickle.loads(pickled_state.user_schema),\n                                pickle.loads(pickled_state.reflection_cache),\n                                pickle.loads(pickled_state.database_config),\n                            )\n                            if debug.flags.server:\n                                print(client_id, \"DIFF SYNC ADD: \", dbname)\n                            dbs = dbs.set(dbname, db_state)\n                        else:\n                            db_updates = {}\n                            if pickled_state.user_schema is not None:\n                                db_updates[\"user_schema\"] = pickle.loads(\n                                    pickled_state.user_schema\n                                )\n                            if pickled_state.reflection_cache is not None:\n                                db_updates[\"reflection_cache\"] = pickle.loads(\n                                    pickled_state.reflection_cache\n                                )\n                            if pickled_state.database_config is not None:\n                                db_updates[\"database_config\"] = pickle.loads(\n                                    pickled_state.database_config\n                                )\n                            if db_updates:\n                                if debug.flags.server:\n                                    print(\n                                        client_id, \"DIFF SYNC UPDATE: \", dbname\n                                    )\n                                val = dbs.get(dbname)\n                                dbs = dbs.set(\n                                    dbname,\n                                    val._replace(**db_updates),  # type: ignore\n                                )\n                if pickled_schema.dropped_dbs is not None:\n                    for dbname in pickled_schema.dropped_dbs:\n                        if debug.flags.server:\n                            print(client_id, \"DIFF SYNC DROP: \", dbname)\n                        dbs = dbs.delete(dbname)\n                if dbs is not client_schema.dbs:\n                    updates[\"dbs\"] = dbs\n                if pickled_schema.global_schema is not None:\n                    updates[\"global_schema\"] = pickle.loads(\n                        pickled_schema.global_schema\n                    )\n                if pickled_schema.instance_config is not None:\n                    updates[\"instance_config\"] = pickle.loads(\n                        pickled_schema.instance_config\n                    )\n                if updates:\n                    client_schema = client_schema._replace(\n                        **updates  # type: ignore\n                    )\n                    clients = clients.set(client_id, client_schema)\n        else:\n            assert client_schema is not None\n\n    except Exception as ex:\n        raise state.FailedStateSync(\n            f\"failed to sync worker state: {type(ex).__name__}({ex})\"\n        ) from ex\n\n\ndef compile(\n    client_id: int,\n    dbname: str,\n    *compile_args: Any,\n    **compile_kwargs: Any,\n):\n    client_schema = clients[client_id]\n    db = client_schema.dbs[dbname]\n    units, cstate = COMPILER.compile_serialized_request(\n        db.user_schema,\n        client_schema.global_schema,\n        db.reflection_cache,\n        db.database_config,\n        client_schema.instance_config,\n        *compile_args,\n        **compile_kwargs,\n    )\n\n    pickled_state = None\n    if cstate is not None:\n        global LAST_STATE\n        LAST_STATE = cstate\n        pickled_state = pickle.dumps(cstate, -1)\n\n    return units, pickled_state\n\n\ndef compile_in_tx(\n    _,\n    client_id: Optional[int],\n    dbname: Optional[str],\n    user_schema: Optional[bytes],\n    cstate,\n    *args,\n    **kwargs,\n):\n    global LAST_STATE\n    if cstate == state.REUSE_LAST_STATE_MARKER:\n        assert LAST_STATE is not None\n        cstate = LAST_STATE\n    else:\n        cstate = pickle.loads(cstate)\n        if client_id is None:\n            assert user_schema is not None\n            cstate.set_root_user_schema(pickle.loads(user_schema))\n        else:\n            assert dbname is not None\n            client_schema = clients[client_id]\n            db = client_schema.dbs[dbname]\n            cstate.set_root_user_schema(db.user_schema)\n    units, cstate = COMPILER.compile_serialized_request_in_tx(\n        cstate, *args, **kwargs)\n    LAST_STATE = cstate\n    return units, pickle.dumps(cstate, -1)\n\n\ndef compile_notebook(\n    client_id: int,\n    dbname: str,\n    *compile_args: Any,\n    **compile_kwargs: Any,\n):\n    global clients\n    client_schema = clients[client_id]\n    db = client_schema.dbs[dbname]\n\n    return COMPILER.compile_notebook(\n        db.user_schema,\n        client_schema.global_schema,\n        db.reflection_cache,\n        db.database_config,\n        client_schema.instance_config,\n        *compile_args,\n        **compile_kwargs,\n    )\n\n\ndef compile_graphql(\n    client_id: int,\n    dbname: str,\n    *compile_args: Any,\n    **compile_kwargs: Any,\n):\n    global clients\n    client_schema = clients[client_id]\n    db = client_schema.dbs[dbname]\n\n    gql_op = graphql.compile_graphql(\n        STD_SCHEMA,\n        db.user_schema,\n        client_schema.global_schema,\n        db.database_config,\n        client_schema.instance_config,\n        *compile_args,\n        **compile_kwargs\n    )\n\n    source = edgeql.Source.from_string(\n        edgeql.generate_source(gql_op.edgeql_ast, pretty=True),\n    )\n\n    cfg_ser = COMPILER.state.compilation_config_serializer\n    request = compiler.CompilationRequest(\n        source=source,\n        protocol_version=defines.CURRENT_PROTOCOL,\n        schema_version=uuidgen.uuid4(),\n        compilation_config_serializer=cfg_ser,\n        output_format=compiler.OutputFormat.JSON,\n        input_format=compiler.InputFormat.JSON,\n        expect_one=True,\n        implicit_limit=0,\n        inline_typeids=False,\n        inline_typenames=False,\n        inline_objectids=False,\n        modaliases=None,\n        session_config=None,\n    )\n\n    unit_group, _ = COMPILER.compile(\n        user_schema=db.user_schema,\n        global_schema=client_schema.global_schema,\n        reflection_cache=db.reflection_cache,\n        database_config=db.database_config,\n        system_config=client_schema.instance_config,\n        request=request,\n    )\n\n    return unit_group, gql_op\n\n\ndef compile_sql(\n    client_id: int,\n    dbname: str,\n    *compile_args: Any,\n    **compile_kwargs: Any,\n):\n    client_schema = clients[client_id]\n    db = client_schema.dbs[dbname]\n    return COMPILER.compile_sql(\n        db.user_schema,\n        client_schema.global_schema,\n        db.reflection_cache,\n        db.database_config,\n        client_schema.instance_config,\n        *compile_args,\n        **compile_kwargs,\n    )\n\n\ndef call_for_client(\n    client_id: int,\n    pickled_schema: Optional[bytes],\n    invalidation: Sequence[int],\n    msg: Optional[bytes],\n    *args: Any,\n) -> Any:\n    __sync__(client_id, pickled_schema, invalidation)\n    if msg is None:\n        methname, dbname, *compile_args = args\n    else:\n        assert args == ()\n        methname, args = pickle.loads(msg)\n        (\n            dbname,\n\n            # These are pass-thru arguments from Gel server, they are already\n            # utilized in the compiler server and forwarded to us through\n            # \"pickled_schema\" argument, so we don't need them here.\n            evicted_dbs,\n            user_schema,\n            reflection_cache,\n            global_schema,\n            database_config,\n            system_config,\n\n            *compile_args,\n        ) = args\n\n    if methname == \"compile\":\n        meth = compile\n    elif methname == \"compile_notebook\":\n        meth = compile_notebook\n    elif methname == \"compile_graphql\":\n        meth = compile_graphql\n    elif methname == \"compile_sql\":\n        meth = compile_sql\n    else:\n        raise NotImplementedError(\n            f\"call_for_client() is not implemented for {methname!r} method. \"\n        )\n    return meth(client_id, dbname, *compile_args)\n\n\ndef get_handler(methname: str) -> Callable[..., Any]:\n    meth: Callable[..., Any]\n    if methname == \"__init_worker__\":\n        meth = __init_worker__\n    else:\n        if not INITED:\n            raise RuntimeError(\n                \"call on uninitialized compiler worker\"\n            )\n        if methname == \"call_for_client\":\n            meth = call_for_client\n        elif methname == \"compile_in_tx\":\n            meth = compile_in_tx\n        else:\n            meth = getattr(COMPILER, methname)\n    return meth\n\n\nif __name__ == \"__main__\":\n    try:\n        worker_proc.main(get_handler)\n    except KeyboardInterrupt:\n        pass\n"
  },
  {
    "path": "edb/server/compiler_pool/pool.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    cast,\n    Hashable,\n    Mapping,\n    NamedTuple,\n    Optional,\n    TYPE_CHECKING,\n)\n\nimport asyncio\nimport collections\nimport dataclasses\nimport functools\nimport hmac\nimport logging\nimport os\nimport os.path\nimport pickle\nimport random\nimport signal\nimport subprocess\nimport sys\nimport time\n\nimport immutables\nimport psutil\n\nfrom edb.common import debug\nfrom edb.common import lru\n\nfrom edb.pgsql import params as pgparams\n\nfrom edb.schema import reflection as s_refl\nfrom edb.schema import schema as s_schema\nfrom edb.server import args as srvargs\nfrom edb.server import config\nfrom edb.server import dbview\nfrom edb.server import defines\nfrom edb.server import metrics\n\nfrom . import amsg\nfrom . import queue\nfrom . import state\n\nif TYPE_CHECKING:\n    from edb import errors\n    from edb import graphql\n    from edb.server.compiler import compiler\n    from edb.server.compiler import config as config_compiler\n    from edb.server.compiler import dbstate\n    from edb.server.compiler import sertypes\n\nSyncStateCallback = Callable[[], None]\nSyncFinalizer = Callable[[], None]\nConfig = immutables.Map[str, config.SettingValue]\nInitArgs = tuple[\n    pgparams.BackendRuntimeParams,\n    s_schema.Schema,\n    s_schema.Schema,\n    s_refl.SchemaClassLayout,\n    bytes,\n    Config,\n]\nMultiTenantInitArgs = tuple[\n    pgparams.BackendRuntimeParams,\n    s_schema.Schema,\n    s_schema.Schema,\n    s_refl.SchemaClassLayout,\n]\nRemoteInitArgsPickle = tuple[bytes, bytes, bytes, bytes]\nPreArgs = tuple[Any, ...]\n\n\nPROCESS_INITIAL_RESPONSE_TIMEOUT: float = 60.0\nKILL_TIMEOUT: float = 10.0\nHEALTH_CHECK_MIN_INTERVAL: float = float(\n    os.getenv(\"GEL_COMPILER_HEALTH_CHECK_MIN_INTERVAL\", 10)\n)\nHEALTH_CHECK_TIMEOUT: float = float(\n    os.getenv(\"GEL_COMPILER_HEALTH_CHECK_TIMEOUT\", 10)\n)\nADAPTIVE_SCALE_UP_WAIT_TIME: float = 3.0\nADAPTIVE_SCALE_DOWN_WAIT_TIME: float = 60.0\nWORKER_PKG: str = __name__.rpartition('.')[0] + '.'\nDEFAULT_CLIENT: str = 'default'\nHIGH_RSS_GRACE_PERIOD: tuple[int, int] = (20 * 3600, 30 * 3600)\nCURRENT_COMPILER_PROTOCOL = 2\n\n\nlogger = logging.getLogger(\"edb.server\")\nlog_metrics = logging.getLogger(\"edb.server.metrics\")\n\n\n# Inherit sys.path so that import system can find worker class\n# in unittests.\n_ENV = os.environ.copy()\n_ENV['PYTHONPATH'] = ':'.join(sys.path)\n\n\n@functools.lru_cache(maxsize=4)\ndef _pickle_memoized(obj: Any) -> bytes:\n    return pickle.dumps(obj, -1)\n\n\nclass BaseWorker:\n\n    _dbs: collections.OrderedDict[str, state.PickledDatabaseState]\n    _backend_runtime_params: pgparams.BackendRuntimeParams\n    _std_schema: s_schema.Schema\n    _refl_schema: s_schema.Schema\n    _schema_class_layout: s_refl.SchemaClassLayout\n    _global_schema_pickle: bytes\n    _system_config: Config\n    _last_pickled_state: Optional[bytes]\n\n    _con: Optional[amsg.HubConnection]\n    _last_used: float\n    _closed: bool\n\n    def __init__(\n        self,\n        backend_runtime_params: pgparams.BackendRuntimeParams,\n        std_schema: s_schema.Schema,\n        refl_schema: s_schema.Schema,\n        schema_class_layout: s_refl.SchemaClassLayout,\n        global_schema_pickle: bytes,\n        system_config: Config,\n    ) -> None:\n        self._dbs = collections.OrderedDict()\n        self._backend_runtime_params = backend_runtime_params\n        self._std_schema = std_schema\n        self._refl_schema = refl_schema\n        self._schema_class_layout = schema_class_layout\n        self._global_schema_pickle = global_schema_pickle\n        self._system_config = system_config\n        self._last_pickled_state = None\n\n        self._con = None\n        self._last_used = time.monotonic()\n        self._closed = False\n\n    def get_db(self, name: str) -> Optional[state.PickledDatabaseState]:\n        rv = self._dbs.get(name)\n        if rv is not None:\n            self._dbs.move_to_end(name, last=False)\n        return rv\n\n    def set_db(self, name: str, db: state.PickledDatabaseState) -> None:\n        self._dbs[name] = db\n        self._dbs.move_to_end(name, last=False)\n\n    def prepare_evict_db(self, keep: int) -> list[str]:\n        return list(self._dbs.keys())[keep:]\n\n    def evict_db(self, name: str) -> Optional[state.PickledDatabaseState]:\n        return self._dbs.pop(name, None)\n\n    async def call(\n        self,\n        method_name: str,\n        *args: Any,\n        sync_state: Optional[SyncStateCallback] = None,\n    ) -> Any:\n        assert not self._closed\n        assert self._con is not None\n\n        if self._con.is_closed():\n            raise RuntimeError(\n                'the connection to the compiler worker process is '\n                'unexpectedly closed')\n\n        data = await self._request(method_name, args)\n\n        status, *result = pickle.loads(data)\n\n        self._last_used = time.monotonic()\n\n        if status == 0:\n            if sync_state is not None:\n                sync_state()\n            return result[0]\n        elif status == 1:\n            exc, tb = result\n            if (sync_state is not None and\n                    not isinstance(exc, state.FailedStateSync)):\n                sync_state()\n            exc.__formatted_error__ = tb\n            raise exc\n        else:\n            exc = RuntimeError(\n                'could not serialize result in worker subprocess')\n            exc.__formatted_error__ = result[0]\n            raise exc\n\n    async def _request(\n        self,\n        method_name: str,\n        args: tuple[Any, ...],\n    ) -> memoryview:\n        assert self._con is not None\n        msg = pickle.dumps((method_name, args))\n        return await self._con.request(msg)\n\n\nclass Worker(BaseWorker):\n\n    _pid: int\n    _proc: psutil.Process\n    _manager: BaseLocalPool\n    _server: amsg.Server\n    _allow_high_rss_until: float\n\n    def __init__(\n        self,\n        manager: BaseLocalPool,\n        server: amsg.Server,\n        pid: int,\n        *args: Any,\n    ) -> None:\n        super().__init__(*args)\n\n        self._pid = pid\n        self._proc = psutil.Process(pid)\n        self._manager = manager\n        self._server = server\n        grace_period = random.SystemRandom().randint(*HIGH_RSS_GRACE_PERIOD)\n        self._allow_high_rss_until = time.monotonic() + grace_period\n\n    async def _attach(self, init_args_pickled: bytes) -> None:\n        self._manager._stats_spawned += 1\n\n        self._con = self._server.get_by_pid(self._pid)\n\n        await self.call(\n            '__init_worker__',\n            init_args_pickled,\n        )\n\n    def set_db(self, name: str, db: state.PickledDatabaseState) -> None:\n        pid = str(self._pid)\n        old_size: Optional[int] = None\n        if (old_db := self._dbs.get(name)) is not None:\n            old_size = old_db.get_estimated_size()\n        super().set_db(name, db)\n        metrics.compiler_process_schema_size.inc(\n            db.get_estimated_size() - (old_size or 0), pid, DEFAULT_CLIENT\n        )\n        if old_size is None:\n            action = 'cache-add'\n            metrics.compiler_process_branches.set(\n                len(self._dbs), pid, DEFAULT_CLIENT\n            )\n        else:\n            action = 'cache-update'\n        metrics.compiler_process_branch_actions.inc(\n            1, pid, DEFAULT_CLIENT, action\n        )\n\n    def evict_db(self, name: str) -> Optional[state.PickledDatabaseState]:\n        pid = str(self._pid)\n        db = self._dbs.get(name)\n        super().evict_db(name)\n        if db is not None:\n            metrics.compiler_process_schema_size.dec(\n                db.get_estimated_size(), pid, DEFAULT_CLIENT\n            )\n            metrics.compiler_process_branch_actions.inc(\n                1, pid, DEFAULT_CLIENT, 'cache-evict'\n            )\n        return db\n\n    def get_pid(self) -> int:\n        return self._pid\n\n    def get_rss(self) -> int:\n        return self._proc.memory_info().rss\n\n    def maybe_close_for_high_rss(self, max_rss: int) -> bool:\n        if time.monotonic() > self._allow_high_rss_until:\n            rss = self.get_rss()\n            if rss > max_rss:\n                logger.info(\n                    'worker process with PID %d exceeds high RSS limit '\n                    '(%d > %d), killing now',\n                    self._pid, rss, max_rss,\n                )\n                self.close()\n                return True\n\n        return False\n\n    def close(self) -> None:\n        if self._closed:\n            return\n        self._closed = True\n        metrics.compiler_process_kills.inc()\n        self._manager._stats_killed += 1\n        self._manager._workers.pop(self._pid, None)\n        self._manager._report_worker(self, action=\"kill\")\n        try:\n            os.kill(self._pid, signal.SIGTERM)\n        except ProcessLookupError:\n            pass\n\n\nclass AbstractPool[\n    BaseWorker_T: BaseWorker, InitArgs_T, InitArgsPickle_T,\n]:\n\n    _loop: asyncio.AbstractEventLoop\n    _worker_branch_limit: int\n    _backend_runtime_params: pgparams.BackendRuntimeParams\n    _std_schema: s_schema.Schema\n    _refl_schema: s_schema.Schema\n    _schema_class_layout: s_refl.SchemaClassLayout\n    _dbindex: Optional[dbview.DatabaseIndex] = None\n    _last_active_time: float\n\n    def __init__(\n        self,\n        *,\n        loop: asyncio.AbstractEventLoop,\n        worker_branch_limit: int,\n        **kwargs: Any,\n    ) -> None:\n        self._loop = loop\n        self._worker_branch_limit = worker_branch_limit\n        self._init(kwargs)\n\n    def _init(self, kwargs: dict[str, Any]) -> None:\n        self._backend_runtime_params = kwargs[\"backend_runtime_params\"]\n        self._std_schema = kwargs[\"std_schema\"]\n        self._refl_schema = kwargs[\"refl_schema\"]\n        self._schema_class_layout = kwargs[\"schema_class_layout\"]\n        self._dbindex = kwargs.get(\"dbindex\")\n        self._last_active_time = 0\n\n    def _get_init_args(self) -> tuple[InitArgs_T, InitArgsPickle_T]:\n        assert self._dbindex is not None\n        return self._make_cached_init_args(\n            *self._dbindex.get_cached_compiler_args()\n        )\n\n    def _make_cached_init_args(\n        self,\n        global_schema_pickle: bytes,\n        system_config: Config,\n    ) -> tuple[InitArgs_T, InitArgsPickle_T]:\n        raise NotImplementedError\n\n    def _make_init_args(\n        self,\n        global_schema_pickle: bytes,\n        system_config: Config,\n    ) -> InitArgs:\n        return (\n            self._backend_runtime_params,\n            self._std_schema,\n            self._refl_schema,\n            self._schema_class_layout,\n            global_schema_pickle,\n            system_config,\n        )\n\n    async def start(self) -> None:\n        raise NotImplementedError\n\n    async def stop(self) -> None:\n        raise NotImplementedError\n\n    def get_template_pid(self) -> Optional[int]:\n        return None\n\n    async def _compute_compile_preargs(\n        self,\n        method_name: str,\n        worker: BaseWorker_T,\n        dbname: str,\n        user_schema_pickle: bytes,\n        global_schema_pickle: bytes,\n        reflection_cache: state.ReflectionCache,\n        database_config: Config,\n        system_config: Config,\n    ) -> tuple[PreArgs, Optional[SyncStateCallback], SyncFinalizer]:\n\n        def sync_worker_state_cb(\n            *,\n            worker: BaseWorker_T,\n            dbname: str,\n            user_schema_pickle: Optional[bytes] = None,\n            global_schema_pickle: Optional[bytes] = None,\n            reflection_cache: Optional[state.ReflectionCache] = None,\n            database_config: Optional[Config] = None,\n            system_config: Optional[Config] = None,\n            evicted_dbs: Optional[list[str]] = None,\n        ):\n            if evicted_dbs is not None:\n                for name in evicted_dbs:\n                    worker.evict_db(name)\n\n            worker_db = worker.get_db(dbname)\n            if worker_db is None:\n                assert user_schema_pickle is not None\n                assert reflection_cache is not None\n                assert global_schema_pickle is not None\n                assert database_config is not None\n                assert system_config is not None\n\n                worker.set_db(\n                    dbname,\n                    state.PickledDatabaseState(\n                        user_schema_pickle=user_schema_pickle,\n                        reflection_cache=reflection_cache,\n                        database_config=database_config,\n                    ),\n                )\n                worker._global_schema_pickle = global_schema_pickle\n                worker._system_config = system_config\n            else:\n                if (\n                    user_schema_pickle is not None\n                    or reflection_cache is not None\n                    or database_config is not None\n                ):\n                    worker.set_db(\n                        dbname,\n                        state.PickledDatabaseState(\n                            user_schema_pickle=(\n                                user_schema_pickle\n                                or worker_db.user_schema_pickle\n                            ),\n                            reflection_cache=(\n                                worker_db.reflection_cache\n                                if reflection_cache is None\n                                else reflection_cache\n                            ),\n                            database_config=(\n                                worker_db.database_config\n                                if database_config is None\n                                else database_config\n                            ),\n                        ),\n                    )\n\n                if global_schema_pickle is not None:\n                    worker._global_schema_pickle = global_schema_pickle\n                if system_config is not None:\n                    worker._system_config = system_config\n\n        worker_db = worker.get_db(dbname)\n        preargs: list[Any] = [method_name, dbname]\n        to_update: dict[str, Any] = {}\n        branch_cache_hit = True\n\n        if worker_db is None:\n            branch_cache_hit = False\n            evicted_dbs = worker.prepare_evict_db(\n                self._worker_branch_limit - 1\n            )\n            preargs.extend([\n                evicted_dbs,\n                user_schema_pickle,\n                _pickle_memoized(reflection_cache),\n                global_schema_pickle,\n                _pickle_memoized(database_config),\n                _pickle_memoized(system_config),\n            ])\n            to_update = {\n                'evicted_dbs': evicted_dbs,\n                'user_schema_pickle': user_schema_pickle,\n                'reflection_cache': reflection_cache,\n                'global_schema_pickle': global_schema_pickle,\n                'database_config': database_config,\n                'system_config': system_config,\n            }\n        else:\n            preargs.append([])  # evicted_dbs\n\n            if worker_db.user_schema_pickle is not user_schema_pickle:\n                branch_cache_hit = False\n                preargs.append(user_schema_pickle)\n                to_update['user_schema_pickle'] = user_schema_pickle\n            else:\n                preargs.append(None)\n\n            if worker_db.reflection_cache is not reflection_cache:\n                branch_cache_hit = False\n                preargs.append(_pickle_memoized(reflection_cache))\n                to_update['reflection_cache'] = reflection_cache\n            else:\n                preargs.append(None)\n\n            if worker._global_schema_pickle is not global_schema_pickle:\n                preargs.append(global_schema_pickle)\n                to_update['global_schema_pickle'] = global_schema_pickle\n            else:\n                preargs.append(None)\n\n            if worker_db.database_config is not database_config:\n                branch_cache_hit = False\n                preargs.append(_pickle_memoized(database_config))\n                to_update['database_config'] = database_config\n            else:\n                preargs.append(None)\n\n            if worker._system_config is not system_config:\n                preargs.append(_pickle_memoized(system_config))\n                to_update['system_config'] = system_config\n            else:\n                preargs.append(None)\n\n        self._report_branch_request(worker, branch_cache_hit)\n\n        if to_update:\n            callback = functools.partial(\n                sync_worker_state_cb,\n                worker=worker,\n                dbname=dbname,\n                **to_update\n            )\n        else:\n            callback = None\n\n        return tuple(preargs), callback, lambda: None\n\n    def _report_branch_request(\n        self,\n        worker: BaseWorker_T,\n        cache_hit: bool,\n        client: str = DEFAULT_CLIENT,\n    ) -> None:\n        pass\n\n    async def _acquire_worker(\n        self,\n        *,\n        condition: Optional[queue.AcquireCondition[BaseWorker_T]] = None,\n        weighter: Optional[queue.Weighter[BaseWorker_T]] = None,\n        **compiler_args: Any,\n    ) -> BaseWorker_T:\n        raise NotImplementedError\n\n    def _release_worker(\n        self,\n        worker: BaseWorker_T,\n        *,\n        put_in_front: bool = True,\n    ) -> None:\n        raise NotImplementedError\n\n    async def compile(\n        self,\n        dbname: str,\n        user_schema_pickle: bytes,\n        global_schema_pickle: bytes,\n        reflection_cache: state.ReflectionCache,\n        database_config: Config,\n        system_config: Config,\n        *compile_args: Any,\n        **compiler_args: Any,\n    ) -> tuple[dbstate.QueryUnitGroup, bytes, int]:\n        fini = lambda: None\n        worker = await self._acquire_worker(**compiler_args)\n        try:\n            preargs, sync_state, fini = await self._compute_compile_preargs(\n                \"compile\",\n                worker,\n                dbname,\n                user_schema_pickle,\n                global_schema_pickle,\n                reflection_cache,\n                database_config,\n                system_config,\n            )\n\n            result = await worker.call(\n                *preargs,\n                *compile_args,\n                sync_state=sync_state\n            )\n            worker._last_pickled_state = result[1]\n            if len(result) == 2:\n                return *result, 0\n            else:\n                return result\n\n        finally:\n            fini()\n            self._release_worker(worker)\n\n    async def compile_in_tx(\n        self,\n        dbname: str,\n        user_schema_pickle: bytes,\n        txid: int,\n        pickled_state: bytes,\n        state_id: int,\n        *compile_args: Any,\n        **compiler_args: Any,\n    ) -> tuple[dbstate.QueryUnitGroup, bytes, int]:\n        # When we compile a query, the compiler returns a tuple:\n        # a QueryUnit and the state the compiler is in if it's in a\n        # transaction.  The state contains the information about all savepoints\n        # and transient schema changes, so the next time we need to\n        # compile a new query in this transaction the state is needed\n        # to be passed to the next compiler compiling it.\n        #\n        # The compile state can be quite heavy and contain multiple versions\n        # of schema, configs, and other session-related data. So the compiler\n        # worker pickles it before sending it to the IO process, and the\n        # IO process doesn't need to ever unpickle it.\n        #\n        # There's one crucial optimization we do here though. We try to\n        # find the compiler process that we used before, that already has\n        # this state unpickled. If we can find it, it means that the\n        # compiler process won't have to waste time unpickling the state.\n        #\n        # We use \"is\" in `w._last_pickled_state is pickled_state` deliberately,\n        # because `pickled_state` is saved on the Worker instance and\n        # stored in edgecon; we never modify it, so `is` is sufficient and\n        # is faster than `==`.\n        worker = await self._acquire_worker(\n            condition=lambda w: (w._last_pickled_state is pickled_state),\n            compiler_args=compiler_args,\n        )\n\n        dbname_arg = None\n        user_schema_pickle_arg = None\n        if worker._last_pickled_state is pickled_state:\n            # Since we know that this particular worker already has the\n            # state, we don't want to waste resources transferring the\n            # state over the network. So we replace the state with a marker,\n            # that the compiler process will recognize.\n            pickled_state = state.REUSE_LAST_STATE_MARKER\n        else:\n            worker_db = worker.get_db(dbname)\n            if (\n                worker_db is not None\n                and worker_db.user_schema_pickle is user_schema_pickle\n            ):\n                dbname_arg = dbname\n            else:\n                user_schema_pickle_arg = user_schema_pickle\n\n        try:\n            units, new_pickled_state = await worker.call(\n                'compile_in_tx',\n                dbname_arg,\n                user_schema_pickle_arg,\n                pickled_state,\n                txid,\n                *compile_args\n            )\n            worker._last_pickled_state = new_pickled_state\n            return units, new_pickled_state, 0\n\n        finally:\n            # Put the worker at the end of the queue so that the chance\n            # of reusing it later (and maximising the chance of\n            # the w._last_pickled_state is pickled_state` check returning\n            # `True` is higher.\n            self._release_worker(worker, put_in_front=False)\n\n    async def compile_notebook(\n        self,\n        dbname: str,\n        user_schema_pickle: bytes,\n        global_schema_pickle: bytes,\n        reflection_cache: state.ReflectionCache,\n        database_config: Config,\n        system_config: Config,\n        *compile_args: Any,\n        **compiler_args: Any,\n    ) -> list[\n        tuple[\n            bool,\n            dbstate.QueryUnit | tuple[str, str, dict[int, str]]\n        ]\n    ]:\n        fini = lambda: None\n        worker = await self._acquire_worker(**compiler_args)\n        try:\n            preargs, sync_state, fini = await self._compute_compile_preargs(\n                \"compile_notebook\",\n                worker,\n                dbname,\n                user_schema_pickle,\n                global_schema_pickle,\n                reflection_cache,\n                database_config,\n                system_config,\n            )\n\n            return await worker.call(\n                *preargs,\n                *compile_args,\n                sync_state=sync_state\n            )\n\n        finally:\n            fini()\n            self._release_worker(worker)\n\n    async def compile_graphql(\n        self,\n        dbname: str,\n        user_schema_pickle: bytes,\n        global_schema_pickle: bytes,\n        reflection_cache: state.ReflectionCache,\n        database_config: Config,\n        system_config: Config,\n        *compile_args: Any,\n        **compiler_args: Any,\n    ) -> graphql.TranspiledOperation:\n        fini = lambda: None\n        worker = await self._acquire_worker(**compiler_args)\n        try:\n            preargs, sync_state, fini = await self._compute_compile_preargs(\n                \"compile_graphql\",\n                worker,\n                dbname,\n                user_schema_pickle,\n                global_schema_pickle,\n                reflection_cache,\n                database_config,\n                system_config,\n            )\n\n            return await worker.call(\n                *preargs,\n                *compile_args,\n                sync_state=sync_state\n            )\n\n        finally:\n            fini()\n            self._release_worker(worker)\n\n    async def compile_sql(\n        self,\n        dbname: str,\n        user_schema_pickle: bytes,\n        global_schema_pickle: bytes,\n        reflection_cache: state.ReflectionCache,\n        database_config: Config,\n        system_config: Config,\n        *compile_args: Any,\n        **compiler_args: Any,\n    ) -> list[dbstate.SQLQueryUnit]:\n        fini = lambda: None\n        worker = await self._acquire_worker(**compiler_args)\n        try:\n            preargs, sync_state, fini = await self._compute_compile_preargs(\n                \"compile_sql\",\n                worker,\n                dbname,\n                user_schema_pickle,\n                global_schema_pickle,\n                reflection_cache,\n                database_config,\n                system_config,\n            )\n\n            return await worker.call(\n                *preargs,\n                *compile_args,\n                sync_state=sync_state\n            )\n        finally:\n            fini()\n            self._release_worker(worker)\n\n    # We use a helper function instead of just fully generating the\n    # functions in order to make the backtraces a little better.\n    async def _simple_call(self, name: str, *args: Any, **kwargs: Any) -> Any:\n        worker = await self._acquire_worker()\n        try:\n            return await worker.call(\n                name,\n                *args,\n                **kwargs\n            )\n\n        finally:\n            self._release_worker(worker)\n\n    async def interpret_backend_error(\n        self,\n        user_schema: bytes,\n        global_schema: bytes,\n        error_fields: dict[str, str],\n        from_graphql: bool,\n    ) -> errors.EdgeDBError:\n        return await self._simple_call(\n            'interpret_backend_error',\n            user_schema,\n            global_schema,\n            error_fields,\n            from_graphql,\n        )\n\n    async def parse_global_schema(self, global_schema_json: bytes) -> bytes:\n        return await self._simple_call(\n            'parse_global_schema', global_schema_json\n        )\n\n    async def parse_user_schema_db_config(\n        self,\n        user_schema_json: bytes,\n        db_config_json: bytes,\n        global_schema_pickle: bytes,\n    ) -> dbstate.ParsedDatabase:\n        return await self._simple_call(\n            'parse_user_schema_db_config',\n            user_schema_json,\n            db_config_json,\n            global_schema_pickle,\n        )\n\n    async def make_state_serializer(\n        self,\n        protocol_version: defines.ProtocolVersion,\n        user_schema_pickle: bytes,\n        global_schema_pickle: bytes,\n    ) -> sertypes.StateSerializer:\n        return await self._simple_call(\n            'make_state_serializer',\n            protocol_version,\n            user_schema_pickle,\n            global_schema_pickle,\n        )\n\n    async def make_compilation_config_serializer(\n        self\n    ) -> sertypes.CompilationConfigSerializer:\n        return await self._simple_call('make_compilation_config_serializer')\n\n    async def describe_database_dump(\n        self,\n        user_schema_json: bytes,\n        global_schema_json: bytes,\n        db_config_json: bytes,\n        protocol_version: defines.ProtocolVersion,\n        with_secrets: bool,\n    ) -> compiler.DumpDescriptor:\n        return await self._simple_call(\n            'describe_database_dump',\n            user_schema_json,\n            global_schema_json,\n            db_config_json,\n            protocol_version,\n            with_secrets,\n        )\n\n    async def describe_database_restore(\n        self,\n        user_schema_pickle: bytes,\n        global_schema_pickle: bytes,\n        dump_server_ver_str: str,\n        dump_catalog_version: Optional[int],\n        schema_ddl: bytes,\n        schema_ids: list[tuple[str, str, bytes]],\n        blocks: list[tuple[bytes, bytes]],  # type_id, typespec\n        protocol_version: defines.ProtocolVersion,\n    ) -> compiler.RestoreDescriptor:\n        return await self._simple_call(\n            'describe_database_restore',\n            user_schema_pickle,\n            global_schema_pickle,\n            dump_server_ver_str,\n            dump_catalog_version,\n            schema_ddl,\n            schema_ids,\n            blocks,\n            protocol_version,\n        )\n\n    async def analyze_explain_output(\n        self,\n        query_asts_pickled: bytes,\n        data: list[list[bytes]],\n    ) -> bytes:\n        return await self._simple_call(\n            'analyze_explain_output', query_asts_pickled, data\n        )\n\n    async def validate_schema_equivalence(\n        self,\n        schema_a: bytes,\n        schema_b: bytes,\n        global_schema: bytes,\n        conn_state_pickle: Optional[bytes],\n    ) -> None:\n        return await self._simple_call(\n            'validate_schema_equivalence',\n            schema_a,\n            schema_b,\n            global_schema,\n            conn_state_pickle,\n        )\n\n    async def compile_structured_config(\n        self,\n        objects: Mapping[str, config_compiler.ConfigObject],\n        source: str | None = None,\n        allow_nested: bool = False,\n    ) -> dict[str, Config]:\n        return await self._simple_call(\n            'compile_structured_config', objects, source, allow_nested\n        )\n\n    def get_debug_info(self) -> dict[str, Any]:\n        return {}\n\n    def get_size_hint(self) -> int:\n        raise NotImplementedError\n\n    def refresh_metrics(self) -> None:\n        pass\n\n    def _maybe_update_last_active_time(self) -> None:\n        if sys.exc_info()[0] is None:\n            self._last_active_time = time.monotonic()\n\n    async def health_check(self) -> bool:\n        elapsed = time.monotonic() - self._last_active_time\n        if elapsed > HEALTH_CHECK_MIN_INTERVAL:\n            async with asyncio.timeout(HEALTH_CHECK_TIMEOUT):\n                await self.make_compilation_config_serializer()\n            self._maybe_update_last_active_time()\n        return True\n\n\nclass BaseLocalPool[Worker_T: Worker, InitArgs_T](\n    AbstractPool[Worker_T, InitArgs_T, bytes],\n    amsg.ServerProtocol,\n    asyncio.SubprocessProtocol,\n):\n\n    _worker_class: type[Worker_T]\n    _worker_mod: str = \"worker\"\n    _workers_queue: queue.WorkerQueue[Worker_T]\n    _workers: dict[int, Worker_T]\n\n    _poolsock_name: str\n    _pool_size: int\n    _worker_max_rss: Optional[int]\n    _server: Optional[amsg.Server]\n    _ready_evt: asyncio.Event\n    _running: Optional[bool]\n    _stats_spawned: int\n    _stats_killed: int\n\n    def __init__(\n        self,\n        *,\n        runstate_dir: str,\n        pool_size: int,\n        worker_max_rss: Optional[int] = None,\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(**kwargs)\n\n        self._poolsock_name = os.path.join(runstate_dir, 'ipc')\n        assert len(self._poolsock_name) <= (\n            defines.MAX_RUNSTATE_DIR_PATH\n            + defines.MAX_UNIX_SOCKET_PATH_LENGTH\n            + 1\n        ), \"pool IPC socket length exceeds maximum allowed\"\n\n        assert pool_size >= 1\n        self._pool_size = pool_size\n        self._worker_max_rss = worker_max_rss\n        self._workers = {}\n\n        self._server = amsg.Server(self._poolsock_name, self._loop, self)\n        self._ready_evt = asyncio.Event()\n\n        self._running = None\n\n        self._stats_spawned = 0\n        self._stats_killed = 0\n\n    def _report_branch_request(\n        self, worker: Worker_T, cache_hit: bool, client: str = DEFAULT_CLIENT\n    ) -> None:\n        pid = str(worker.get_pid())\n        metrics.compiler_process_branch_actions.inc(\n            1, pid, client, 'request'\n        )\n        if cache_hit:\n            metrics.compiler_process_branch_actions.inc(\n                1, pid, client, 'cache-hit'\n            )\n\n    def is_running(self) -> bool:\n        return bool(self._running)\n\n    async def _attach_worker(self, pid: int) -> Optional[Worker_T]:\n        if not self._running:\n            return None\n        assert self._server is not None\n        logger.debug(\"Sending init args to worker with PID %s.\", pid)\n        init_args, init_args_pickled = self._get_init_args()\n        worker = self._worker_class(  # type: ignore\n            self,\n            self._server,\n            pid,\n            *init_args,\n        )\n        await worker._attach(init_args_pickled)\n        self._report_worker(worker)\n\n        self._workers[pid] = worker\n        self._workers_queue.release(worker)\n        self._worker_attached()\n\n        logger.debug(\"started compiler worker process (PID %s)\", pid)\n        if (\n            not self._ready_evt.is_set()\n            and len(self._workers) == self._pool_size\n        ):\n            logger.info(\n                f\"started {self._pool_size} compiler worker \"\n                f\"process{'es' if self._pool_size > 1 else ''}\",\n            )\n            self._ready_evt.set()\n\n        return worker\n\n    def _worker_attached(self) -> None:\n        pass\n\n    def worker_connected(self, pid: int, version: int) -> None:\n        logger.debug(\"Worker with PID %s connected.\", pid)\n        self._loop.create_task(self._attach_worker(pid))\n        metrics.compiler_process_spawns.inc()\n        metrics.current_compiler_processes.inc()\n\n    def worker_disconnected(self, pid: int) -> None:\n        logger.debug(\"Worker with PID %s disconnected.\", pid)\n        self._workers.pop(pid, None)\n        metrics.current_compiler_processes.dec()\n\n        expect = str(pid)\n\n        def pid_filter(pid_str: str, *remaining_tags) -> bool:\n            return pid_str == expect\n\n        metrics.compiler_process_memory.clear(pid_filter)\n        metrics.compiler_process_schema_size.clear(pid_filter)\n        metrics.compiler_process_branches.clear(pid_filter)\n        metrics.compiler_process_branch_actions.clear(pid_filter)\n        metrics.compiler_process_client_actions.clear(pid_filter)\n\n    async def start(self) -> None:\n        if self._running is not None:\n            raise RuntimeError(\n                'the compiler pool has already been started once')\n        assert self._server is not None\n\n        self._workers_queue = queue.WorkerQueue(self._loop)\n\n        await self._server.start()\n        self._running = True\n\n        await self._start()\n\n        await self._wait_ready()\n\n    async def _wait_ready(self) -> None:\n        await asyncio.wait_for(\n            self._ready_evt.wait(),\n            PROCESS_INITIAL_RESPONSE_TIMEOUT\n        )\n\n    async def _create_compiler_process(\n        self, numproc: Optional[int] = None, version: int = 0\n    ) -> asyncio.SubprocessTransport:\n        # Create a new compiler process. When numproc is None, a single\n        # standalone compiler worker process is started; if numproc is an int,\n        # a compiler template process will be created, which will then fork\n        # itself into `numproc` actual worker processes and run as a supervisor\n\n        env = _ENV\n        if debug.flags.server:\n            env = {'EDGEDB_DEBUG_SERVER': '1', **_ENV}\n\n        cmdline = [sys.executable]\n        if sys.flags.isolated:\n            cmdline.append('-I')\n\n        cmdline.extend([\n            '-m', WORKER_PKG + self._worker_mod,\n            '--sockname', self._poolsock_name,\n            '--version-serial', str(version),\n        ])\n        if numproc:\n            cmdline.extend([\n                '--numproc', str(numproc),\n            ])\n\n        transport, _ = await self._loop.subprocess_exec(\n            lambda: self,\n            *cmdline,\n            env=env,\n            stdin=subprocess.DEVNULL,\n            stdout=None,\n            stderr=None,\n        )\n        return transport\n\n    async def _start(self) -> None:\n        raise NotImplementedError\n\n    async def stop(self) -> None:\n        if not self._running:\n            return\n        self._running = False\n\n        assert self._server is not None\n        await self._server.stop()\n        self._server = None\n\n        self._workers_queue = queue.WorkerQueue(self._loop)\n        self._workers.clear()\n\n        await self._stop()\n\n    async def _stop(self) -> None:\n        raise NotImplementedError\n\n    def _report_worker(\n        self, worker: Worker_T, *, action: str = \"spawn\"\n    ) -> None:\n        action = action.capitalize()\n        if not action.endswith(\"e\"):\n            action += \"e\"\n        action += \"d\"\n        log_metrics.info(\n            \"%s a compiler worker with PID %d; pool=%d;\"\n            + \" spawned=%d; killed=%d\",\n            action,\n            worker.get_pid(),\n            len(self._workers),\n            self._stats_spawned,\n            self._stats_killed,\n        )\n\n    async def _acquire_worker(\n        self,\n        *,\n        condition: Optional[queue.AcquireCondition[Worker_T]] = None,\n        weighter: Optional[queue.Weighter[Worker_T]] = None,\n        **compiler_args: Any,\n    ) -> Worker_T:\n        start_time = time.monotonic()\n        try:\n            while (\n                worker := await self._workers_queue.acquire(\n                    condition=condition, weighter=weighter\n                )\n            ).get_pid() not in self._workers:\n                # The worker was disconnected; skip to the next one.\n                pass\n        except TimeoutError:\n            metrics.compiler_pool_queue_errors.inc(1.0, \"timeout\")\n            raise\n        except Exception:\n            metrics.compiler_pool_queue_errors.inc(1.0, \"ise\")\n            raise\n        else:\n            metrics.compiler_pool_wait_time.observe(\n                time.monotonic() - start_time\n            )\n            return worker\n\n    def _release_worker(\n        self,\n        worker: Worker_T,\n        *,\n        put_in_front: bool = True,\n    ) -> None:\n        # Skip disconnected workers\n        if worker.get_pid() in self._workers:\n            if self._worker_max_rss is not None:\n                if worker.maybe_close_for_high_rss(self._worker_max_rss):\n                    return\n            self._workers_queue.release(worker, put_in_front=put_in_front)\n        self._maybe_update_last_active_time()\n\n    def get_debug_info(self) -> dict[str, Any]:\n        return dict(\n            worker_pids=list(self._workers.keys()),\n            template_pid=self.get_template_pid(),\n        )\n\n    def refresh_metrics(self) -> None:\n        for w in self._workers.values():\n            metrics.compiler_process_memory.set(w.get_rss(), str(w.get_pid()))\n\n    async def health_check(self) -> bool:\n        if not (\n            self._running\n            and self._ready_evt.is_set()\n            and len(self._workers) > 0\n        ):\n            return False\n        return await super().health_check()\n\n\nclass FixedPoolImpl[Worker_T: Worker, InitArgs_T](\n    BaseLocalPool[Worker_T, InitArgs_T],\n):\n\n    _template_transport: Optional[asyncio.SubprocessTransport]\n    _template_proc_scheduled: bool\n    _template_proc_version: int\n\n    def __init__(self, **kwargs) -> None:\n        super().__init__(**kwargs)\n        self._template_transport = None\n        self._template_proc_scheduled = False\n        self._template_proc_version = 0\n\n    def _worker_attached(self) -> None:\n        if not self._running:\n            return\n        assert self._server is not None\n        if len(self._workers) > self._pool_size:\n            self._server.kill_outdated_worker(self._template_proc_version)\n\n    def worker_connected(self, pid: int, version: int) -> None:\n        if not self._running:\n            return\n        assert self._server is not None\n        if version < self._template_proc_version:\n            logger.debug(\n                \"Outdated worker with PID %s connected; discard now.\", pid\n            )\n            self._server.get_by_pid(pid).abort()\n            metrics.compiler_process_spawns.inc()\n        else:\n            super().worker_connected(pid, version)\n\n    def process_exited(self) -> None:\n        # Template process exited\n        self._template_transport = None\n        if self._running:\n            logger.error(\"Template compiler process exited; recreating now.\")\n            self._schedule_template_proc(0)\n\n    def get_template_pid(self) -> Optional[int]:\n        if self._template_transport is None:\n            return None\n        else:\n            return self._template_transport.get_pid()\n\n    async def _start(self) -> None:\n        await self._create_template_proc(retry=False)\n\n    async def _create_template_proc(self, retry: bool = True) -> None:\n        self._template_proc_scheduled = False\n        if not self._running:\n            return\n        self._template_proc_version += 1\n        try:\n            # Create the template process, which will then fork() into numproc\n            # child processes and manage them, so that we don't have to manage\n            # the actual compiler worker processes in the main process.\n            self._template_transport = await self._create_compiler_process(\n                numproc=self._pool_size,\n                version=self._template_proc_version,\n            )\n        except Exception:\n            if retry:\n                if self._running:\n                    t = defines.BACKEND_COMPILER_TEMPLATE_PROC_RESTART_INTERVAL\n                    logger.exception(\n                        f\"Unexpected error occurred creating template compiler\"\n                        f\" process; retry in {t} second{'s' if t > 1 else ''}.\"\n                    )\n                    self._schedule_template_proc(t)\n            else:\n                raise\n\n    def _schedule_template_proc(self, sleep: float) -> None:\n        if self._template_proc_scheduled:\n            return\n        self._template_proc_scheduled = True\n        self._loop.call_later(\n            sleep, self._loop.create_task, self._create_template_proc()\n        )\n\n    async def _stop(self) -> None:\n        trans, self._template_transport = self._template_transport, None\n        if trans is not None:\n            trans.terminate()\n            await trans._wait()  # type: ignore\n            trans.close()\n\n    def get_size_hint(self) -> int:\n        return self._pool_size\n\n\n@srvargs.CompilerPoolMode.Fixed.assign_implementation\nclass FixedPool(FixedPoolImpl[Worker, InitArgs]):\n\n    _worker_class = Worker\n\n    @lru.lru_method_cache(1)\n    def _make_cached_init_args(\n        self,\n        global_schema_pickle: bytes,\n        system_config: Config,\n    ) -> tuple[InitArgs, bytes]:\n        init_args = self._make_init_args(\n            global_schema_pickle, system_config\n        )\n        pickled_args = pickle.dumps(init_args, -1)\n        return init_args, pickled_args\n\n\n@srvargs.CompilerPoolMode.OnDemand.assign_implementation\nclass SimpleAdaptivePool(BaseLocalPool[Worker, InitArgs]):\n\n    _worker_class = Worker\n    _worker_transports: dict[int, asyncio.SubprocessTransport]\n    _expected_num_workers: int\n    _scale_down_handle: Optional[asyncio.Handle]\n    _max_num_workers: int\n    _cleanups: dict[int, asyncio.Future]\n\n    def __init__(self, *, pool_size: int, **kwargs: Any) -> None:\n        super().__init__(pool_size=1, **kwargs)\n        self._worker_transports = {}\n        self._expected_num_workers = 0\n        self._scale_down_handle = None\n        self._max_num_workers = pool_size\n        self._cleanups = {}\n\n    @lru.lru_method_cache(1)\n    def _make_cached_init_args(\n        self,\n        global_schema_pickle: bytes,\n        system_config: Config,\n    ) -> tuple[InitArgs, bytes]:\n        init_args = self._make_init_args(\n            global_schema_pickle, system_config\n        )\n        pickled_args = pickle.dumps(init_args, -1)\n        return init_args, pickled_args\n\n    async def _start(self) -> None:\n        async with asyncio.TaskGroup() as g:\n            for _i in range(self._pool_size):\n                g.create_task(self._create_worker())\n\n    async def _stop(self) -> None:\n        self._expected_num_workers = 0\n        transports, self._worker_transports = self._worker_transports, {}\n        for transport in transports.values():\n            await transport._wait()  # type: ignore\n            transport.close()\n        for cleanup in list(self._cleanups.values()):\n            await cleanup\n\n    async def _acquire_worker(\n        self,\n        *,\n        condition: Optional[queue.AcquireCondition[Worker]] = None,\n        weighter: Optional[queue.Weighter[Worker]] = None,\n        **compiler_args: Any,\n    ) -> Worker:\n        scale_up_handle = None\n        if (\n            self._running\n            and self._workers_queue.qsize() == 0\n            and (\n                len(self._workers)\n                == self._expected_num_workers\n                < self._max_num_workers\n            )\n        ):\n            scale_up_handle = self._loop.call_later(\n                ADAPTIVE_SCALE_UP_WAIT_TIME, self._maybe_scale_up\n            )\n        if self._scale_down_handle is not None:\n            self._scale_down_handle.cancel()\n            self._scale_down_handle = None\n        try:\n            return await super()._acquire_worker(\n                condition=condition, weighter=weighter, **compiler_args\n            )\n        finally:\n            if scale_up_handle is not None:\n                scale_up_handle.cancel()\n\n    def _release_worker(\n        self,\n        worker: Worker,\n        *,\n        put_in_front: bool = True,\n    ) -> None:\n        if self._scale_down_handle is not None:\n            self._scale_down_handle.cancel()\n            self._scale_down_handle = None\n        super()._release_worker(worker, put_in_front=put_in_front)\n        if (\n            self._running and\n            self._workers_queue.count_waiters() == 0 and\n            len(self._workers) > self._pool_size\n        ):\n            self._scale_down_handle = self._loop.call_later(\n                ADAPTIVE_SCALE_DOWN_WAIT_TIME,\n                self._scale_down,\n            )\n\n    async def _wait_on_dying(\n        self,\n        pid: int,\n        trans: asyncio.SubprocessTransport,\n    ) -> None:\n        await trans._wait()  # type: ignore\n        self._cleanups.pop(pid)\n\n    def worker_disconnected(self, pid: int) -> None:\n        num_workers_before = len(self._workers)\n        super().worker_disconnected(pid)\n        trans = self._worker_transports.pop(pid, None)\n        if trans:\n            trans.close()\n            # amsg.Server notifies us when the *pipe* to the worker closes,\n            # so we need to fire off a task to make sure that we wait for\n            # the worker to exit, in order to avoid a warning.\n            self._cleanups[pid] = (\n                self._loop.create_task(self._wait_on_dying(pid, trans)))\n        if not self._running:\n            return\n        if len(self._workers) < self._pool_size:\n            # The auto-scaler will not scale down below the pool_size, so we\n            # should restart the unexpectedly-exited worker process.\n            logger.warning(\n                \"Compiler worker process[%d] exited unexpectedly; \"\n                \"start a new one now.\", pid\n            )\n            self._loop.create_task(self._create_worker())\n            self._expected_num_workers = len(self._workers)\n        elif num_workers_before == self._expected_num_workers:\n            # This is likely the case when a worker died unexpectedly, and we\n            # don't want to restart the worker because the auto-scaler will\n            # start a new one again if necessary.\n            self._expected_num_workers = len(self._workers)\n\n    def process_exited(self) -> None:\n        if self._running:\n            for pid, transport in list(self._worker_transports.items()):\n                if transport.is_closing():\n                    self._worker_transports.pop(pid, None)\n\n    async def _create_worker(self) -> None:\n        # Creates a single compiler worker process.\n        transport = await self._create_compiler_process()\n        self._worker_transports[transport.get_pid()] = transport\n        self._expected_num_workers += 1\n\n    def _maybe_scale_up(self) -> None:\n        if not self._running:\n            return\n        logger.info(\n            \"A compile request has waited for more than %d seconds, \"\n            \"spawn a new compiler worker process now.\",\n            ADAPTIVE_SCALE_UP_WAIT_TIME,\n        )\n        self._loop.create_task(self._create_worker())\n\n    def _scale_down(self) -> None:\n        self._scale_down_handle = None\n        if not self._running or len(self._workers) <= self._pool_size:\n            return\n        logger.info(\n            \"The compiler pool is not used in %d seconds, scaling down to %d.\",\n            ADAPTIVE_SCALE_DOWN_WAIT_TIME, self._pool_size,\n        )\n        self._expected_num_workers = self._pool_size\n        for worker in sorted(\n            self._workers.values(), key=lambda w: w._last_used\n        )[:-self._pool_size]:\n            worker.close()\n\n    def get_size_hint(self) -> int:\n        return self._max_num_workers\n\n\nclass RemoteWorker(BaseWorker):\n    _con: amsg.HubConnection\n    _secret: bytes\n\n    def __init__(\n        self,\n        con: amsg.HubConnection,\n        secret: bytes,\n        *args: Any,\n    ) -> None:\n        super().__init__(*args)\n        self._con = con\n        self._secret = secret\n\n    def close(self) -> None:\n        if self._closed:\n            return\n        self._closed = True\n        self._con.abort()\n\n    async def _request(\n        self,\n        method_name: str,\n        args: tuple[Any, ...],\n    ) -> memoryview:\n        msg = pickle.dumps((method_name, args))\n        digest = hmac.digest(self._secret, msg, \"sha256\")\n        return await self._con.request(digest + msg)\n\n\n@srvargs.CompilerPoolMode.Remote.assign_implementation\nclass RemotePool(AbstractPool[RemoteWorker, InitArgs, RemoteInitArgsPickle]):\n\n    _pool_addr: tuple[str, int]\n    _worker: Optional[asyncio.Future[RemoteWorker]]\n    _sync_lock: asyncio.Lock\n    _semaphore: asyncio.BoundedSemaphore\n    _pool_size: int\n    _secret: bytes\n\n    def __init__(\n        self,\n        *,\n        address: tuple[str, int],\n        pool_size: int,\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(**kwargs)\n        self._pool_addr = address\n        self._worker = None\n        self._sync_lock = asyncio.Lock()\n        self._semaphore = asyncio.BoundedSemaphore(pool_size)\n        self._pool_size = pool_size\n        secret = os.environ.get(\"_EDGEDB_SERVER_COMPILER_POOL_SECRET\")\n        if not secret:\n            raise AssertionError(\n                \"_EDGEDB_SERVER_COMPILER_POOL_SECRET environment variable \"\n                \"is not set\"\n            )\n        self._secret = secret.encode()\n\n    async def start(self, *, retry: bool = False) -> None:\n        if self._worker is None:\n            self._worker = self._loop.create_future()\n        try:\n            def on_pid(*args: Any) -> None:\n                self._loop.create_task(self._connection_made(retry, *args))\n\n            await self._loop.create_connection(\n                lambda: amsg.HubProtocol(\n                    loop=self._loop,\n                    on_pid=on_pid,\n                    on_connection_lost=self._connection_lost,\n                ),\n                *self._pool_addr,\n            )\n        except Exception:\n            if not retry:\n                raise\n            if self._worker is not None:\n                self._loop.call_later(1, lambda: self._loop.create_task(\n                    self.start(retry=True)\n                ))\n        else:\n            if not retry:\n                await self._worker\n\n    async def stop(self) -> None:\n        if self._worker is not None:\n            worker, self._worker = self._worker, None\n            if worker.done():\n                (await worker).close()\n\n    @lru.lru_method_cache(1)\n    def _make_cached_init_args(\n        self,\n        global_schema_pickle: bytes,\n        system_config: Config,\n    ) -> tuple[InitArgs, RemoteInitArgsPickle]:\n        init_args = self._make_init_args(\n            global_schema_pickle,\n            system_config,\n        )\n        std_args = (\n            self._std_schema, self._refl_schema, self._schema_class_layout\n        )\n        client_args = (self._backend_runtime_params,)\n        return init_args, (\n            pickle.dumps(std_args, -1),\n            pickle.dumps(client_args, -1),\n            global_schema_pickle,\n            pickle.dumps(system_config, -1),\n        )\n\n    async def _connection_made(\n        self,\n        retry: bool,\n        protocol: amsg.HubProtocol,\n        transport: asyncio.Transport,\n        _pid: int,\n        version: int,\n    ) -> None:\n        if self._worker is None:\n            return\n        compiler_protocol = CURRENT_COMPILER_PROTOCOL\n        try:\n            init_args, init_args_pickled = self._get_init_args()\n            worker = RemoteWorker(\n                amsg.HubConnection(transport, protocol, self._loop, version),\n                self._secret,\n                *init_args,\n            )\n            await worker.call(\n                '__init_server__',\n                compiler_protocol,\n                defines.EDGEDB_CATALOG_VERSION,\n                init_args_pickled,\n            )\n        except state.IncompatibleClient as ex:\n            transport.abort()\n            if self._worker is not None:\n                self._worker.set_exception(ex)\n                self._worker = None\n        except BaseException as ex:\n            transport.abort()\n            if self._worker is not None:\n                if retry:\n                    await self.start(retry=True)\n                else:\n                    self._worker.set_exception(ex)\n                    self._worker = None\n        else:\n            if self._worker is not None:\n                self._worker.set_result(worker)\n\n    def _connection_lost(self, _pid: Optional[int]) -> None:\n        if self._worker is not None:\n            self._worker = self._loop.create_future()\n            self._loop.create_task(self.start(retry=True))\n\n    async def _acquire_worker(self, **compiler_args: Any) -> RemoteWorker:\n        start_time = time.monotonic()\n        try:\n            await self._semaphore.acquire()\n            assert self._worker is not None\n            rv = await self._worker\n        except TimeoutError:\n            metrics.compiler_pool_queue_errors.inc(1.0, \"timeout\")\n            raise\n        except Exception:\n            metrics.compiler_pool_queue_errors.inc(1.0, \"ise\")\n            raise\n        else:\n            metrics.compiler_pool_wait_time.observe(\n                time.monotonic() - start_time\n            )\n            return rv\n\n    def _release_worker(\n        self,\n        worker: RemoteWorker,\n        *,\n        put_in_front: bool = True,\n    ) -> None:\n        self._semaphore.release()\n        self._maybe_update_last_active_time()\n\n    async def compile_in_tx(\n        self,\n        dbname: str,\n        user_schema_pickle: bytes,\n        txid: int,\n        pickled_state: bytes,\n        state_id: int,\n        *compile_args: Any,\n        **compiler_args: Any,\n    ) -> tuple[dbstate.QueryUnitGroup, bytes, int]:\n        worker = await self._acquire_worker()\n        try:\n            return await worker.call(\n                'compile_in_tx',\n                state_id,\n                None,  # client_id\n                None,  # dbname\n                None,  # user_schema_pickle\n                state.REUSE_LAST_STATE_MARKER,\n                txid,\n                *compile_args\n            )\n        except state.StateNotFound:\n            return await worker.call(\n                'compile_in_tx',\n                0,  # state_id\n                None,  # client_id\n                None,  # dbname\n                user_schema_pickle,\n                pickled_state,\n                txid,\n                *compile_args\n            )\n        finally:\n            self._release_worker(worker)\n\n    async def _compute_compile_preargs(\n        self, *args: Any\n    ) -> tuple[PreArgs, Optional[SyncStateCallback], SyncFinalizer]:\n        # State sync with the compiler server is serialized with _sync_lock,\n        # also blocking any other compile requests that may sync state, so as\n        # to avoid inconsistency. Meanwhile, we'd like to avoid locking when\n        # sync is not needed (callback is None), so we have 2 fast paths here:\n        #\n        #   1. When _sync_lock is not held AND sync is not needed here, or\n        #   2. after acquiring _sync_lock, we found that sync is not needed.\n        #\n        # In such cases, we avoid locking or release the lock immediately, so\n        # that concurrent compile requests can proceed in parallel.\n        preargs: PreArgs = ()\n        callback: Optional[SyncStateCallback] = lambda: None\n        fini = lambda: None\n\n        if not self._sync_lock.locked():\n            # Case 1: check if we need to sync state.\n            (\n                preargs, callback, fini\n            ) = await super()._compute_compile_preargs(*args)\n\n        if callback is not None:\n            # Check again with the lock acquired\n            del preargs, callback\n            await self._sync_lock.acquire()\n            (\n                preargs, callback, fini\n            ) = await super()._compute_compile_preargs(*args)\n            if callback:\n                # State sync is only considered done when we received a\n                # successful response from the compiler server, when we\n                # update the local state in the worker in the `callback`\n                # function. We should usually release the lock after the\n                # `callback`, but we must also release it if anything\n                # failed along the way.\n                fini = lambda: self._sync_lock.release()\n            else:\n                # Case 2: no state sync needed, release the lock immediately.\n                self._sync_lock.release()\n        return preargs, callback, fini\n\n    def get_debug_info(self) -> dict[str, Any]:\n        return dict(\n            address=\"{}:{}\".format(*self._pool_addr),\n            size=self._semaphore._bound_value,  # type: ignore\n            free=self._semaphore._value,  # type: ignore\n        )\n\n    def get_size_hint(self) -> int:\n        return self._pool_size\n\n    async def health_check(self) -> bool:\n        if self._worker is None or not self._worker.done():\n            return False\n        return await super().health_check()\n\n\n@dataclasses.dataclass\nclass TenantSchema:\n    client_id: int\n    dbs: collections.OrderedDict[str, state.PickledDatabaseState]\n    global_schema_pickle: bytes\n    system_config: Config\n\n    def get_db(self, name: str) -> Optional[state.PickledDatabaseState]:\n        rv = self.dbs.get(name)\n        if rv is not None:\n            self.dbs.move_to_end(name, last=False)\n        return rv\n\n    def set_db(self, name: str, db: state.PickledDatabaseState) -> None:\n        self.dbs[name] = db\n        self.dbs.move_to_end(name, last=False)\n\n    def prepare_evict_db(self, keep: int) -> list[str]:\n        return list(self.dbs.keys())[keep:]\n\n    def evict_db(self, name: str) -> None:\n        self.dbs.pop(name, None)\n\n    def get_estimated_size(self) -> int:\n        return sum(db.get_estimated_size() for db in self.dbs.values())\n\n\nclass PickledState(NamedTuple):\n    user_schema: Optional[bytes]\n    reflection_cache: Optional[bytes]\n    database_config: Optional[bytes]\n\n\nclass PickledSchema(NamedTuple):\n    dbs: Optional[immutables.Map[str, PickledState]] = None\n    global_schema: Optional[bytes] = None\n    instance_config: Optional[bytes] = None\n    dropped_dbs: tuple = ()\n\n\nclass BaseMultiTenantWorker[\n    TenantStore_T, BaseLocalPool_T: BaseLocalPool\n](Worker):\n\n    _manager: BaseLocalPool_T\n    _cache: collections.OrderedDict[int, TenantStore_T]\n    _invalidated_clients: list[int]\n    _last_used_by_client: dict[int, float]\n    _client_names: dict[int, str]\n\n    def __init__(\n        self,\n        manager: BaseLocalPool_T,\n        server: amsg.Server,\n        pid: int,\n        backend_runtime_params: pgparams.BackendRuntimeParams,\n        std_schema: s_schema.Schema,\n        refl_schema: s_schema.Schema,\n        schema_class_layout: s_refl.SchemaClassLayout,\n    ):\n        super().__init__(\n            manager,\n            server,\n            pid,\n            backend_runtime_params,\n            std_schema,\n            refl_schema,\n            schema_class_layout,\n            None,\n            None,\n        )\n        self._cache = collections.OrderedDict()\n        self._invalidated_clients = []\n        self._last_used_by_client = {}\n        self._client_names = {}\n        self._init()\n\n    def _init(self) -> None:\n        pass\n\n    def get_tenant_schema(\n        self, client_id: int, *, touch: bool = True\n    ) -> Optional[TenantStore_T]:\n        rv = self._cache.get(client_id)\n        if rv is not None and touch:\n            self._cache.move_to_end(client_id, last=False)\n        return rv\n\n    def set_tenant_schema(\n        self, client_id: int, tenant_schema: TenantStore_T\n    ) -> None:\n        self._cache[client_id] = tenant_schema\n        self._cache.move_to_end(client_id, last=False)\n        self._last_used_by_client[client_id] = time.monotonic()\n\n    def cache_size(self) -> int:\n        return len(self._cache) - len(self._invalidated_clients)\n\n    def last_used(self, client_id: int) -> float:\n        return self._last_used_by_client.get(client_id, 0)\n\n    def flush_invalidation(self) -> list[int]:\n        evicted = 0\n        pid_str = str(self.get_pid())\n        evicted_names = set()\n\n        client_ids, self._invalidated_clients = self._invalidated_clients, []\n        for client_id in client_ids:\n            if self._cache.pop(client_id, None) is not None:\n                evicted += 1\n            self._last_used_by_client.pop(client_id, None)\n\n            client_name = self._client_names.pop(client_id, None)\n            if client_name is not None:\n                evicted_names.add(client_name)\n\n        if evicted:\n            metrics.compiler_process_client_actions.inc(\n                evicted, pid_str, 'cache-evict'\n            )\n        if evicted_names:\n            def tag_filter(pid: str, client: str, *remaining_tags) -> bool:\n                return pid == pid_str and client in evicted_names\n\n            metrics.compiler_process_schema_size.clear(tag_filter)\n            metrics.compiler_process_branches.clear(tag_filter)\n            metrics.compiler_process_branch_actions.clear(tag_filter)\n\n        return client_ids\n\n    def set_client_name(self, client_id: int, name: Optional[str]) -> None:\n        if client_id not in self._client_names:\n            self._client_names[client_id] = name or f'unknown-{client_id}'\n\n    def get_client_name(self, client_id: int) -> str:\n        return self._client_names.get(client_id) or f'unknown-{client_id}'\n\n\nclass MultiTenantWorker(\n    BaseMultiTenantWorker[TenantSchema, \"MultiTenantPool\"]\n):\n\n    current_client_id: Optional[int]\n\n    def _init(self) -> None:\n        self.current_client_id = None\n\n    def invalidate(self, client_id: int) -> None:\n        if client_id in self._cache:\n            self._invalidated_clients.append(client_id)\n\n    def maybe_invalidate_last(self) -> None:\n        if self.cache_size() == self._manager.cache_size:\n            client_id = next(reversed(self._cache))\n            self._invalidated_clients.append(client_id)\n\n    def get_invalidation(self) -> list[int]:\n        return self._invalidated_clients[:]\n\n\n@srvargs.CompilerPoolMode.MultiTenant.assign_implementation\nclass MultiTenantPool(FixedPoolImpl[MultiTenantWorker, MultiTenantInitArgs]):\n    _worker_class = MultiTenantWorker\n    _worker_mod = \"multitenant_worker\"\n\n    def __init__(self, *, cache_size: int, **kwargs: Any) -> None:\n        super().__init__(**kwargs)\n        self._cache_size = cache_size\n\n    @property\n    def cache_size(self) -> int:\n        return self._cache_size\n\n    def drop_tenant(self, client_id: int) -> None:\n        for worker in self._workers.values():\n            worker.invalidate(client_id)\n\n    @lru.method_cache\n    def _get_init_args(self) -> tuple[MultiTenantInitArgs, bytes]:\n        init_args = (\n            self._backend_runtime_params,\n            self._std_schema,\n            self._refl_schema,\n            self._schema_class_layout,\n        )\n        return init_args, pickle.dumps(init_args, -1)\n\n    def _weighter(\n        self,\n        client_id: int,\n        worker: MultiTenantWorker,\n    ) -> queue.Comparable:\n        tenant_schema = worker.get_tenant_schema(client_id, touch=False)\n        return (\n            bool(tenant_schema),\n            worker.last_used(client_id)\n            if tenant_schema\n            else self._cache_size - worker.cache_size(),\n        )\n\n    async def _acquire_worker(\n        self,\n        *,\n        condition: Optional[queue.AcquireCondition[MultiTenantWorker]] = None,\n        weighter: Optional[queue.Weighter[MultiTenantWorker]] = None,\n        **compiler_args: Any\n    ) -> MultiTenantWorker:\n        client_id: Optional[int] = compiler_args.get(\"client_id\")\n        if weighter is None and client_id is not None:\n            weighter = functools.partial(self._weighter, client_id)\n        rv = await super()._acquire_worker(\n            condition=condition, weighter=weighter, **compiler_args\n        )\n        rv.current_client_id = client_id\n        if client_id is not None:\n            rv.set_client_name(client_id, compiler_args.get(\"client_name\"))\n        return rv\n\n    def _release_worker(\n        self,\n        worker: MultiTenantWorker,\n        *,\n        put_in_front: bool = True,\n    ) -> None:\n        worker.current_client_id = None\n        super()._release_worker(worker, put_in_front=put_in_front)\n\n    async def _compute_compile_preargs(\n        self,\n        method_name: str,\n        worker: MultiTenantWorker,\n        dbname: str,\n        user_schema_pickle: bytes,\n        global_schema_pickle: bytes,\n        reflection_cache: state.ReflectionCache,\n        database_config: Config,\n        system_config: Config,\n    ) -> tuple[PreArgs, Optional[SyncStateCallback], SyncFinalizer]:\n        assert isinstance(worker, MultiTenantWorker)\n\n        def sync_worker_state_cb(\n            *,\n            worker: MultiTenantWorker,\n            client_id: int,\n            client_name: str,\n            dbname: str,\n            evicted_dbs: list[str],\n            user_schema_pickle: Optional[bytes] = None,\n            global_schema_pickle: Optional[bytes] = None,\n            reflection_cache: Optional[state.ReflectionCache] = None,\n            database_config: Optional[Config] = None,\n            instance_config: Optional[Config] = None,\n        ) -> None:\n            pid = str(worker.get_pid())\n            tenant_schema = worker.get_tenant_schema(client_id)\n            if tenant_schema is None:\n                assert user_schema_pickle is not None\n                assert reflection_cache is not None\n                assert global_schema_pickle is not None\n                assert database_config is not None\n                assert instance_config is not None\n                assert len(evicted_dbs) == 0\n\n                tenant_schema = TenantSchema(\n                    client_id,\n                    collections.OrderedDict(\n                        {\n                            dbname: state.PickledDatabaseState(\n                                user_schema_pickle,\n                                reflection_cache,\n                                database_config,\n                            ),\n                        }\n                    ),\n                    global_schema_pickle,\n                    instance_config,\n                )\n                worker.set_tenant_schema(client_id, tenant_schema)\n\n                metrics.compiler_process_branch_actions.inc(\n                    1, pid, client_name, 'cache-add'\n                )\n                metrics.compiler_process_client_actions.inc(\n                    1, pid, 'cache-add'\n                )\n\n            else:\n                for name in evicted_dbs:\n                    tenant_schema.evict_db(name)\n                if evicted_dbs:\n                    metrics.compiler_process_branch_actions.inc(\n                        len(evicted_dbs), pid, client_name, 'cache-evict'\n                    )\n\n                worker_db = tenant_schema.get_db(dbname)\n                if worker_db is None:\n                    assert user_schema_pickle is not None\n                    assert reflection_cache is not None\n                    assert database_config is not None\n\n                    tenant_schema.set_db(\n                        dbname,\n                        state.PickledDatabaseState(\n                            user_schema_pickle=user_schema_pickle,\n                            reflection_cache=reflection_cache,\n                            database_config=database_config,\n                        ),\n                    )\n                    metrics.compiler_process_branch_actions.inc(\n                        1, pid, client_name, 'cache-add'\n                    )\n                    metrics.compiler_process_client_actions.inc(\n                        1, pid, 'cache-update'\n                    )\n\n                elif (\n                    user_schema_pickle is not None\n                    or reflection_cache is not None\n                    or database_config is not None\n                ):\n                    tenant_schema.set_db(\n                        dbname,\n                        state.PickledDatabaseState(\n                            user_schema_pickle=(\n                                user_schema_pickle\n                                or worker_db.user_schema_pickle\n                            ),\n                            reflection_cache=(\n                                reflection_cache or worker_db.reflection_cache\n                            ),\n                            database_config=(\n                                database_config or worker_db.database_config\n                            ),\n                        )\n                    )\n                    metrics.compiler_process_branch_actions.inc(\n                        1, pid, client_name, 'cache-update'\n                    )\n                    metrics.compiler_process_client_actions.inc(\n                        1, pid, 'cache-update'\n                    )\n\n                if global_schema_pickle is not None:\n                    tenant_schema.global_schema_pickle = global_schema_pickle\n                if instance_config is not None:\n                    tenant_schema.system_config = instance_config\n\n            worker.flush_invalidation()\n\n            metrics.compiler_process_schema_size.set(\n                tenant_schema.get_estimated_size(), pid, client_name\n            )\n            metrics.compiler_process_branches.set(\n                len(tenant_schema.dbs), pid, client_name\n            )\n\n        client_id = worker.current_client_id\n        assert client_id is not None\n        client_name = worker.get_client_name(client_id)\n        tenant_schema = worker.get_tenant_schema(client_id, touch=False)\n        to_update: dict[str, Hashable]\n        evicted_dbs = []\n        branch_cache_hit = True\n        if tenant_schema is None:\n            branch_cache_hit = False\n            # make room for the new client in this worker\n            worker.maybe_invalidate_last()\n            to_update = {\n                \"user_schema_pickle\": user_schema_pickle,\n                \"reflection_cache\": reflection_cache,\n                \"global_schema_pickle\": global_schema_pickle,\n                \"database_config\": database_config,\n                \"instance_config\": system_config,\n            }\n        else:\n            worker_db = tenant_schema.get_db(dbname)\n            if worker_db is None:\n                branch_cache_hit = False\n                evicted_dbs = tenant_schema.prepare_evict_db(\n                    self._worker_branch_limit - 1\n                )\n                to_update = {\n                    \"user_schema_pickle\": user_schema_pickle,\n                    \"reflection_cache\": reflection_cache,\n                    \"database_config\": database_config,\n                }\n            else:\n                to_update = {}\n                if worker_db.user_schema_pickle is not user_schema_pickle:\n                    branch_cache_hit = False\n                    to_update[\"user_schema_pickle\"] = user_schema_pickle\n                if worker_db.reflection_cache is not reflection_cache:\n                    branch_cache_hit = False\n                    to_update[\"reflection_cache\"] = reflection_cache\n                if worker_db.database_config is not database_config:\n                    branch_cache_hit = False\n                    to_update[\"database_config\"] = database_config\n            if (\n                tenant_schema.global_schema_pickle\n                is not global_schema_pickle\n            ):\n                to_update[\"global_schema_pickle\"] = global_schema_pickle\n            if tenant_schema.system_config is not system_config:\n                to_update[\"instance_config\"] = system_config\n\n        self._report_branch_request(worker, branch_cache_hit, client_name)\n\n        if to_update:\n            pickled = {\n                k.removesuffix(\"_pickle\"): v\n                if k.endswith(\"_pickle\")\n                else _pickle_memoized(v)\n                for k, v in to_update.items()\n            }\n            if any(f in pickled for f in PickledState._fields):\n                db_state = PickledState(\n                    **{\n                        f: pickled.pop(f, None)\n                        for f in PickledState._fields\n                    }  # type: ignore\n                )\n                pickled[\"dbs\"] = immutables.Map([(dbname, db_state)])\n            pickled_schema = PickledSchema(\n                dropped_dbs=tuple(evicted_dbs), **pickled  # type: ignore\n            )\n            callback = functools.partial(\n                sync_worker_state_cb,\n                worker=worker,\n                client_id=client_id,\n                client_name=client_name,\n                dbname=dbname,\n                evicted_dbs=evicted_dbs,\n                **to_update,  # type: ignore\n            )\n        else:\n            pickled_schema = None\n            callback = None\n            metrics.compiler_process_client_actions.inc(\n                1, str(worker.get_pid()), 'cache-hit'\n            )\n\n        return (\n            \"call_for_client\",\n            client_id,\n            pickled_schema,\n            worker.get_invalidation(),\n            None,  # forwarded msg is only used in remote compiler server\n            method_name,\n            dbname,\n        ), callback, lambda: None\n\n    async def compile_in_tx(\n        self,\n        dbname: str,\n        user_schema_pickle: bytes,\n        txid: int,\n        pickled_state: bytes,\n        state_id: int,\n        *compile_args: Any,\n        **compiler_args: Any,\n    ) -> tuple[dbstate.QueryUnitGroup, bytes, int]:\n        client_id: int = compiler_args[\"client_id\"]\n\n        # Prefer a worker we used last time in the transaction (condition), or\n        # (weighter) one with the user schema at tx start so that we can pass\n        # over only the pickled state. Then prefer the least-recently used one\n        # if many workers passed any check in the weighter, or the most vacant.\n        def weighter(w: MultiTenantWorker) -> queue.Comparable:\n            if ts := w.get_tenant_schema(client_id, touch=False):\n                # Don't use ts.get_db() here to avoid confusing the LRU queue\n                if db := ts.dbs.get(dbname):\n                    return (\n                        True,\n                        db.user_schema_pickle is user_schema_pickle,\n                        w.last_used(client_id),\n                    )\n                else:\n                    return True, False, w.last_used(client_id)\n            else:\n                return False, False, self._cache_size - w.cache_size()\n\n        worker = await self._acquire_worker(\n            condition=lambda w: (w._last_pickled_state is pickled_state),\n            weighter=cast(queue.Weighter, weighter),\n            **compiler_args,\n        )\n\n        # Avoid sending information that we know the worker already have.\n        dbname_arg = None\n        client_id_arg = None\n        user_schema_pickle_arg = None\n\n        if worker._last_pickled_state is pickled_state:\n            pickled_state = state.REUSE_LAST_STATE_MARKER\n        else:\n            tenant_schema = worker.get_tenant_schema(client_id)\n            if tenant_schema is None:\n                # Just pass state + root user schema if this is a new client in\n                # the worker; we don't want to initialize the client as we\n                # don't have enough information to do so.\n                user_schema_pickle_arg = user_schema_pickle\n            else:\n                # Don't use ts.get_db() here to avoid confusing the LRU queue\n                worker_db = tenant_schema.dbs.get(dbname)\n                if worker_db is None:\n                    # The worker has the client but not the database\n                    user_schema_pickle_arg = user_schema_pickle\n                elif worker_db.user_schema_pickle is user_schema_pickle:\n                    # Avoid sending the root user schema because the worker has\n                    # it - just send client_id + dbname to reference it, as\n                    # well as the state of course.\n                    dbname_arg = dbname\n                    client_id_arg = client_id\n                    # Touch dbname to bump it in the LRU queue\n                    tenant_schema.get_db(dbname)\n                else:\n                    # The worker has a different root user schema\n                    user_schema_pickle_arg = user_schema_pickle\n\n        try:\n            units, new_pickled_state = await worker.call(\n                'compile_in_tx',\n                # multitenant_worker is also used in MultiSchemaPool for remote\n                # compilers where the first argument \"state_id\" is used to find\n                # worker without passing the pickled state. Here in multi-\n                # tenant mode, we already have the pickled state, so \"state_id\"\n                # is not used. Just prepend a fake ID to comply to the API.\n                0,  # state_id\n                client_id_arg,\n                dbname_arg,\n                user_schema_pickle_arg,\n                pickled_state,\n                txid,\n                *compile_args\n            )\n            worker._last_pickled_state = new_pickled_state\n            return units, new_pickled_state, 0\n\n        finally:\n            self._release_worker(worker, put_in_front=False)\n\n\nasync def create_compiler_pool[AbstractPool_T: AbstractPool](\n    *,\n    runstate_dir: str,\n    pool_size: int,\n    worker_branch_limit: int,\n    backend_runtime_params: pgparams.BackendRuntimeParams,\n    std_schema: s_schema.Schema,\n    refl_schema: s_schema.Schema,\n    schema_class_layout: s_refl.SchemaClassLayout,\n    pool_class: type[AbstractPool_T],\n    **kwargs: Any,\n) -> AbstractPool_T:\n    assert issubclass(pool_class, AbstractPool)\n    loop = asyncio.get_running_loop()\n    pool = pool_class(\n        loop=loop,\n        pool_size=pool_size,\n        worker_branch_limit=worker_branch_limit,\n        runstate_dir=runstate_dir,\n        backend_runtime_params=backend_runtime_params,\n        std_schema=std_schema,\n        refl_schema=refl_schema,\n        schema_class_layout=schema_class_layout,\n        **kwargs,\n    )\n\n    await pool.start()\n    return pool\n"
  },
  {
    "path": "edb/server/compiler_pool/queue.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nimport asyncio\nimport collections\nimport typing\n\n\nW2 = typing.TypeVar('W2', contravariant=True)\n\n\nclass AcquireCondition(typing.Protocol[W2]):\n\n    def __call__(self, worker: W2) -> bool:\n        ...\n\n\nclass Comparable(typing.Protocol):\n\n    def __gt__(self, other: typing.Self) -> bool:\n        ...\n\n\nclass Weighter(typing.Protocol[W2]):\n\n    def __call__(self, worker: W2) -> Comparable:\n        ...\n\n\nclass WorkerQueue[W]:\n\n    loop: asyncio.AbstractEventLoop\n\n    _waiters: collections.deque[asyncio.Future[None]]\n    _queue: collections.deque[W]\n\n    def __init__(\n        self,\n        loop: asyncio.AbstractEventLoop,\n    ) -> None:\n        self._loop = loop\n        self._waiters = collections.deque()\n        self._queue = collections.deque()\n\n    async def acquire(\n        self,\n        *,\n        condition: typing.Optional[AcquireCondition[W]] = None,\n        weighter: typing.Optional[Weighter[W]] = None,\n    ) -> W:\n        # There can be a race between a waiter scheduled for to wake up\n        # and a worker being stolen (due to quota being enforced,\n        # for example).  In which case the waiter might get finally\n        # woken up with an empty queue -- hence we use a `while` loop here.\n        attempts = 0\n        while not self._queue:\n            waiter = self._loop.create_future()\n\n            attempts += 1\n            if attempts > 1:\n                # If the waiter was woken up only to discover that\n                # it needs to wait again, we don't want it to lose\n                # its place in the waiters queue.\n                self._waiters.appendleft(waiter)\n            else:\n                # On the first attempt the waiter goes to the end\n                # of the waiters queue.\n                self._waiters.append(waiter)\n\n            try:\n                await waiter\n            except Exception:\n                if not waiter.done():\n                    waiter.cancel()\n                try:\n                    self._waiters.remove(waiter)\n                except ValueError:\n                    # The waiter could be removed from self._waiters\n                    # by a previous release() call.\n                    pass\n                if self._queue and not waiter.cancelled():\n                    # We were woken up by release(), but can't take\n                    # the call.  Wake up the next in line.\n                    self._wakeup_next_waiter()\n                raise\n\n        if len(self._queue) > 1:\n            if condition is not None:\n                for w in self._queue:\n                    if condition(w):\n                        self._queue.remove(w)\n                        return w\n            if weighter is not None:\n                rv = self._queue[0]\n                weight = weighter(rv)\n                it = iter(self._queue)\n                next(it)  # skip the first\n                for w in it:\n                    new_weight = weighter(w)\n                    if new_weight > weight:\n                        weight = new_weight\n                        rv = w\n                self._queue.remove(rv)\n                return rv\n\n        return self._queue.popleft()\n\n    def release(self, worker: W, *, put_in_front: bool=True) -> None:\n        if put_in_front:\n            self._queue.appendleft(worker)\n        else:\n            self._queue.append(worker)\n        self._wakeup_next_waiter()\n\n    def qsize(self) -> int:\n        return len(self._queue)\n\n    def count_waiters(self) -> int:\n        return len(self._waiters)\n\n    def _wakeup_next_waiter(self) -> None:\n        while self._waiters:\n            waiter = self._waiters.popleft()\n            if not waiter.done():\n                waiter.set_result(None)\n                break\n"
  },
  {
    "path": "edb/server/compiler_pool/server.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Any, Callable, cast, NamedTuple, Optional, Sequence\n\nimport asyncio\nimport hmac\nimport functools\nimport logging\nimport os\nimport pathlib\nimport pickle\nimport secrets\nimport tempfile\nimport traceback\n\nimport click\nimport httptools\nimport immutables\n\nfrom edb.common import debug\nfrom edb.common import lru\nfrom edb.common import markup\nfrom edb.server import metrics\nfrom edb.server import args as srvargs\nfrom edb.server import defines\nfrom edb.server import logsetup\n\nfrom . import amsg\nfrom . import pool as pool_mod\nfrom . import queue\nfrom . import worker_proc\nfrom . import state as state_mod\n\n\n_client_id_seq = 0\n_tx_state_id_seq = 0\nlogger = logging.getLogger(\"edb.server\")\n\n\ndef next_tx_state_id():\n    global _tx_state_id_seq\n    _tx_state_id_seq = (_tx_state_id_seq + 1) % (2 ** 63 - 1)\n    return _tx_state_id_seq\n\n\nclass PickledState(NamedTuple):\n    user_schema: Optional[bytes]\n    reflection_cache: Optional[bytes]\n    database_config: Optional[bytes]\n\n    def diff(self, other: PickledState):\n        # Compare this state with the other state, generate a new state with\n        # fields from this state which are different in the other state, while\n        # the identical fields are left None, so that we can send the minimum\n        # diff to the worker to update the changed fields only.\n        user_schema = reflection_cache = database_config = None\n        if self.user_schema is not other.user_schema:\n            user_schema = self.user_schema\n        if self.reflection_cache is not other.reflection_cache:\n            reflection_cache = self.reflection_cache\n        if self.database_config is not other.database_config:\n            database_config = self.database_config\n        return PickledState(user_schema, reflection_cache, database_config)\n\n    def get_estimated_size(self) -> int:\n        rv = 0\n        if self.user_schema is not None:\n            rv += len(self.user_schema)\n        if self.reflection_cache is not None:\n            rv += len(self.reflection_cache)\n        if self.database_config is not None:\n            rv += len(self.database_config)\n        return rv\n\n\nclass ClientSchema(NamedTuple):\n    dbs: immutables.Map[str, PickledState]\n    global_schema: Optional[bytes]\n    instance_config: Optional[bytes]\n    dropped_dbs: tuple\n\n    def diff(self, other: ClientSchema) -> tuple[ClientSchema, int, int]:\n        # Compare this schema with the other schema, generate a new schema with\n        # fields from this schema which are different in the other schema,\n        # while the identical fields are left None, so that we can send the\n        # minimum diff to the worker to update the changed fields only.\n        # NOTE: this is a deep diff that compares all children fields.\n        dropped_dbs = tuple(\n            dbname for dbname in other.dbs if dbname not in self.dbs\n        )\n        added = 0\n        updated = 0\n        dbs: immutables.Map[str, PickledState] = immutables.Map()\n        for dbname, state in self.dbs.items():\n            other_state = other.dbs.get(dbname)\n            if other_state is None:\n                dbs = dbs.set(dbname, state)\n                added += 1\n            elif state is not other_state:\n                dbs = dbs.set(dbname, state.diff(other_state))\n                updated += 1\n        global_schema = instance_config = None\n        if self.global_schema is not other.global_schema:\n            global_schema = self.global_schema\n        if self.instance_config is not other.instance_config:\n            instance_config = self.instance_config\n        return (\n            ClientSchema(dbs, global_schema, instance_config, dropped_dbs),\n            added,\n            updated,\n        )\n\n    def get_estimated_size(self) -> int:\n        return sum(db.get_estimated_size() for db in self.dbs.values())\n\n\nclass Worker(pool_mod.BaseMultiTenantWorker[ClientSchema, \"MultiSchemaPool\"]):\n\n    _last_pickled_state_id: int\n\n    def _init(self) -> None:\n        self._last_pickled_state_id = 0\n\n    def invalidate(self, client_id: int) -> None:\n        if self._cache.pop(client_id, None):\n            self._invalidated_clients.append(client_id)\n        self._last_used_by_client.pop(client_id, None)\n\n    def invalidate_last(self, cache_size: int) -> None:\n        if len(self._cache) == cache_size:\n            client_id, _ = self._cache.popitem(last=True)\n            self._invalidated_clients.append(client_id)\n            self._last_used_by_client.pop(client_id, None)\n\n    async def call(\n        self,\n        method_name: str,\n        *args: Any,\n        sync_state: Optional[pool_mod.SyncStateCallback] = None,\n        msg: Optional[bytes] = None,\n    ) -> Any:\n        assert not self._closed\n        assert self._con is not None\n\n        if self._con.is_closed():\n            raise RuntimeError(\n                \"the connection to the compiler worker process is \"\n                \"unexpectedly closed\"\n            )\n\n        if msg is None:\n            msg = pickle.dumps((method_name, args))\n        return await self._con.request(msg)\n\n\nclass MultiSchemaPool(\n    pool_mod.FixedPoolImpl[Worker, pool_mod.MultiTenantInitArgs]\n):\n    _worker_class = Worker\n    _worker_mod = \"multitenant_worker\"\n    _workers: dict[int, Worker]  # type: ignore\n\n    _catalog_version: Optional[int]\n    _inited: asyncio.Event\n    _clients: dict[int, ClientSchema]\n    _client_names: dict[int, str]\n    _secret: bytes\n\n    def __init__(self, cache_size, *, secret, **kwargs):\n        super().__init__(**kwargs)\n        self._catalog_version = None\n        self._inited = asyncio.Event()\n        self._cache_size = cache_size\n        self._clients = {}\n        self._client_names = {}\n        self._secret = secret\n\n    def _init(self, kwargs: dict[str, Any]) -> None:\n        # this is deferred to _init_server()\n        pass\n\n    @lru.method_cache\n    def _get_init_args(self) -> tuple[pool_mod.MultiTenantInitArgs, bytes]:\n        init_args = (\n            self._backend_runtime_params,\n            self._std_schema,\n            self._refl_schema,\n            self._schema_class_layout,\n        )\n        return init_args, pickle.dumps(init_args, -1)\n\n    async def _attach_worker(self, pid: int) -> Optional[Worker]:\n        if not self._running:\n            return None\n        if not self._inited.is_set():\n            await self._inited.wait()\n        return await super()._attach_worker(pid)\n\n    async def _wait_ready(self) -> None:\n        pass\n\n    async def _init_server(\n        self,\n        client_id: int,\n        client_name: str,\n        compiler_protocol: int,\n        catalog_version: int,\n        init_args_pickled: pool_mod.RemoteInitArgsPickle,\n    ) -> None:\n        if compiler_protocol > pool_mod.CURRENT_COMPILER_PROTOCOL:\n            raise state_mod.IncompatibleClient(\"compiler_protocol\")\n\n        (\n            std_args_pickled,\n            client_args_pickled,\n            global_schema_pickle,\n            system_config_pickled,\n        ) = init_args_pickled\n        backend_runtime_params, = pickle.loads(client_args_pickled)\n        if self._inited.is_set():\n            logger.debug(\"New client %d connected.\", client_id)\n            assert self._catalog_version is not None\n            if self._catalog_version != catalog_version:\n                raise state_mod.IncompatibleClient(\"catalog_version\")\n            if self._backend_runtime_params != backend_runtime_params:\n                raise state_mod.IncompatibleClient(\"backend_runtime_params\")\n        else:\n            (\n                self._std_schema,\n                self._refl_schema,\n                self._schema_class_layout,\n            ) = pickle.loads(std_args_pickled)\n            self._backend_runtime_params = backend_runtime_params\n            assert self._catalog_version is None\n            self._catalog_version = catalog_version\n            self._inited.set()\n            logger.info(\n                \"New client %d connected, compiler server initialized.\",\n                client_id,\n            )\n        self._clients[client_id] = ClientSchema(\n            dbs=immutables.Map(),\n            global_schema=global_schema_pickle,\n            instance_config=system_config_pickled,\n            dropped_dbs=(),\n        )\n        self._client_names[client_id] = client_name\n\n    def _sync(\n        self,\n        *,\n        client_id: int,\n        dbname: str,\n        evicted_dbs: list[str],\n        user_schema: Optional[bytes],\n        reflection_cache: Optional[bytes],\n        global_schema: Optional[bytes],\n        database_config: Optional[bytes],\n        system_config: Optional[bytes],\n    ) -> bool:\n        \"\"\"Sync the client state in the compiler server.\n\n        The client state is carried over with the compile(), compile_sql(),\n        compile_notebook(), compile_graphql() calls.\n\n        Returns True if the client state changed, False otherwise.\n        \"\"\"\n        # EdgeDB instance syncs the schema with the compiler server\n        client = self._clients[client_id]\n        client_updates: dict[str, Any] = {}\n        dbs = client.dbs.mutate()\n        dbs_changed = False\n        if evicted_dbs:\n            for name in evicted_dbs:\n                if dbs.pop(name, None) is not None:\n                    dbs_changed = True\n\n        db = dbs.get(dbname)\n        if db is None:\n            assert user_schema is not None\n            assert reflection_cache is not None\n            assert database_config is not None\n            dbs[dbname] = PickledState(\n                user_schema, reflection_cache, database_config\n            )\n            dbs_changed = True\n        else:\n            updates = {}\n\n            if user_schema is not None:\n                updates[\"user_schema\"] = user_schema\n            if reflection_cache is not None:\n                updates[\"reflection_cache\"] = reflection_cache\n            if database_config is not None:\n                updates[\"database_config\"] = database_config\n\n            if updates:\n                db = db._replace(**updates)\n                dbs[dbname] = db\n                dbs_changed = True\n\n        if global_schema is not None:\n            client_updates[\"global_schema\"] = global_schema\n\n        if system_config is not None:\n            client_updates[\"instance_config\"] = system_config\n\n        if dbs_changed:\n            client_updates[\"dbs\"] = dbs.finish()\n\n        if client_updates:\n            self._clients[client_id] = client._replace(**client_updates)\n            return True\n        else:\n            return False\n\n    def _weighter(self, client_id: int, worker: Worker) -> queue.Comparable:\n        client_schema = worker.get_tenant_schema(client_id, touch=False)\n        return (\n            bool(client_schema),\n            worker.last_used(client_id)\n            if client_schema\n            else self._cache_size - worker.cache_size(),\n        )\n\n    async def _call_for_client(\n        self,\n        *,\n        client_id: int,\n        method_name: str,\n        dbname: str,\n        evicted_dbs: list[str],\n        user_schema: Optional[bytes],\n        reflection_cache: Optional[bytes],\n        global_schema: Optional[bytes],\n        database_config: Optional[bytes],\n        system_config: Optional[bytes],\n        args: tuple[Any, ...],\n        msg: memoryview,\n    ) -> Any:\n        try:\n            updated = self._sync(\n                client_id=client_id,\n                dbname=dbname,\n                evicted_dbs=evicted_dbs,\n                user_schema=user_schema,\n                reflection_cache=reflection_cache,\n                global_schema=global_schema,\n                database_config=database_config,\n                system_config=system_config,\n            )\n        except Exception as ex:\n            raise state_mod.FailedStateSync(\n                f\"failed to sync compiler server state: \"\n                f\"{type(ex).__name__}({ex})\"\n            ) from ex\n        worker = await self._acquire_worker(\n            weighter=functools.partial(self._weighter, client_id)\n        )\n        try:\n            pid = str(worker.get_pid())\n            client_schema = self._clients[client_id]\n            client_name = self._client_names[client_id]\n            worker.set_client_name(client_id, client_name)\n            diff: Optional[ClientSchema] = client_schema\n            cache = worker.get_tenant_schema(client_id)\n            extra_args: tuple[Any, ...] = ()\n            metrics.compiler_process_branch_actions.inc(\n                1, pid, client_name, 'request'\n            )\n            if cache is client_schema:\n                # client schema is already in sync, don't send again\n                diff = None\n                msg_arg = bytes(msg)\n                metrics.compiler_process_client_actions.inc(\n                    1, pid, 'cache-hit'\n                )\n                metrics.compiler_process_branch_actions.inc(\n                    1, pid, client_name, 'cache-hit'\n                )\n\n            else:\n                metrics.compiler_process_schema_size.set(\n                    client_schema.get_estimated_size(), pid, client_name\n                )\n                metrics.compiler_process_branches.set(\n                    len(client_schema.dbs), pid, client_name\n                )\n\n                if cache is None:\n                    # make room for the new client in this worker\n                    worker.invalidate_last(self._cache_size)\n                    metrics.compiler_process_branch_actions.inc(\n                        len(client_schema.dbs), pid, client_name, 'cache-add'\n                    )\n                    metrics.compiler_process_client_actions.inc(\n                        1, pid, 'cache-add'\n                    )\n\n                else:\n                    # only send the difference in user schema\n                    diff, dbs_added, dbs_updated = client_schema.diff(cache)\n                    if dbname not in diff.dbs:\n                        metrics.compiler_process_branch_actions.inc(\n                            1, pid, client_name, 'cache-hit'\n                        )\n                    if dbs_added:\n                        metrics.compiler_process_branch_actions.inc(\n                            dbs_added, pid, client_name, 'cache-add'\n                        )\n                    if dbs_updated:\n                        metrics.compiler_process_branch_actions.inc(\n                            dbs_updated, pid, client_name, 'cache-update'\n                        )\n                    if diff.dropped_dbs:\n                        metrics.compiler_process_branch_actions.inc(\n                            len(diff.dropped_dbs),\n                            pid,\n                            client_name,\n                            'cache-evict',\n                        )\n                    metrics.compiler_process_client_actions.inc(\n                        1, pid, 'cache-update'\n                    )\n\n                if updated:\n                    # re-pickle the request if user schema changed\n                    msg_arg = None\n                    extra_args = (method_name, dbname, *args)\n                else:\n                    msg_arg = bytes(msg)\n            invalidation = worker.flush_invalidation()\n            resp = await worker.call(\n                \"call_for_client\",\n                client_id,\n                diff,\n                invalidation,\n                msg_arg,\n                *extra_args,\n            )\n            status, *data = pickle.loads(resp)\n            if status == 0:\n                worker.set_tenant_schema(client_id, client_schema)\n                if method_name == \"compile\":\n                    _units, new_pickled_state = data[0]\n                    if new_pickled_state:\n                        sid = next_tx_state_id()\n                        worker._last_pickled_state_id = sid\n                        resp = pickle.dumps((0, (*data[0], sid)), -1)\n            elif status == 1:\n                exc, _tb = data\n                if not isinstance(exc, state_mod.FailedStateSync):\n                    worker.set_tenant_schema(client_id, client_schema)\n            else:\n                exc = RuntimeError(\n                    \"could not serialize result in worker subprocess\"\n                )\n                exc.__formatted_error__ = data[0]\n                raise exc\n\n            return resp\n        finally:\n            self._release_worker(worker)\n\n    async def compile_in_tx_(\n        self,\n        state_id: int,\n        client_id: Optional[int],\n        dbname: Optional[str],\n        user_schema_pickle: Optional[bytes],\n        pickled_state: bytes,\n        txid: int,\n        *compile_args: Any,\n        msg: bytes,\n    ):\n        if pickled_state == state_mod.REUSE_LAST_STATE_MARKER:\n            worker = await self._acquire_worker(\n                condition=lambda w: (w._last_pickled_state_id == state_id)\n            )\n            if worker._last_pickled_state_id != state_id:\n                self._release_worker(worker)\n                raise state_mod.StateNotFound()\n        else:\n            worker = await self._acquire_worker()\n        try:\n            resp = await worker.call(\n                \"compile_in_tx\",\n                state_id,\n                client_id,\n                dbname,\n                user_schema_pickle,\n                pickled_state,\n                txid,\n                *compile_args,\n                msg=msg,\n            )\n            status, *data = pickle.loads(resp)\n            if status == 0:\n                state_id = worker._last_pickled_state_id = next_tx_state_id()\n                resp = pickle.dumps((0, (*data[0], state_id)), -1)\n            return resp\n        finally:\n            self._release_worker(worker, put_in_front=False)\n\n    async def _request(self, method_name: str, msg: bytes) -> Any:\n        worker = await self._acquire_worker()\n        try:\n            return await worker.call(method_name, msg=msg)\n        finally:\n            self._release_worker(worker)\n\n    async def handle_client_call(\n        self,\n        protocol: CompilerServerProtocol,\n        req_id: int,\n        msg: memoryview,\n    ) -> None:\n        client_id = protocol.client_id\n        digest = msg[:32]\n        msg = msg[32:]\n        try:\n            expected_digest = hmac.digest(self._secret, msg, \"sha256\")\n            if not hmac.compare_digest(digest, expected_digest):\n                raise AssertionError(\"message signature verification failed\")\n\n            method_name, args = pickle.loads(msg)\n            if method_name != \"__init_server__\":\n                await self._ready_evt.wait()\n            if method_name == \"__init_server__\":\n                await self._init_server(client_id, protocol.client_name, *args)\n                pickled = pickle.dumps((0, None), -1)\n            elif method_name in {\n                \"compile\",\n                \"compile_notebook\",\n                \"compile_graphql\",\n                \"compile_sql\",\n            }:\n                (\n                    dbname,\n                    evicted_dbs,\n                    user_schema,\n                    reflection_cache,\n                    global_schema,\n                    database_config,\n                    system_config,\n                    *args,\n                ) = args\n                pickled = await self._call_for_client(\n                    client_id=client_id,\n                    method_name=method_name,\n                    dbname=dbname,\n                    evicted_dbs=evicted_dbs,\n                    user_schema=user_schema,\n                    reflection_cache=reflection_cache,\n                    global_schema=global_schema,\n                    database_config=database_config,\n                    system_config=system_config,\n                    args=args,\n                    msg=msg,\n                )\n            elif method_name == \"compile_in_tx\":\n                pickled = await self.compile_in_tx_(*args, msg=bytes(msg))\n            else:\n                pickled = await self._request(method_name, bytes(msg))\n        except Exception as ex:\n            worker_proc.prepare_exception(ex)\n            if debug.flags.server and not isinstance(\n                ex, state_mod.StateNotFound\n            ):\n                markup.dump(ex)\n            data = (1, ex, traceback.format_exc())\n            try:\n                pickled = pickle.dumps(data, -1)\n            except Exception as ex:\n                ex_tb = traceback.format_exc()\n                ex_str = f\"{ex}:\\n\\n{ex_tb}\"\n                pickled = pickle.dumps((2, ex_str), -1)\n        protocol.reply(req_id, pickled)\n\n    def client_disconnected(self, client_id: int) -> None:\n        logger.debug(\"Client %d disconnected, invalidating cache.\", client_id)\n        self._clients.pop(client_id, None)\n        self._client_names.pop(client_id, None)\n        for worker in self._workers.values():\n            worker.invalidate(client_id)\n\n\nclass CompilerServerProtocol(asyncio.Protocol):\n    _pool: MultiSchemaPool\n    _loop: asyncio.AbstractEventLoop\n    _stream: amsg.MessageStream\n    _transport: Optional[asyncio.Transport]\n    _client_id: int\n    _client_name: str\n\n    def __init__(\n        self,\n        pool: MultiSchemaPool,\n        loop: asyncio.AbstractEventLoop,\n    ) -> None:\n        global _client_id_seq\n        self._pool = pool\n        self._loop = loop\n        self._stream = amsg.MessageStream()\n        self._transport = None\n        self._client_id = _client_id_seq = _client_id_seq + 1\n        self._client_name = 'unknown'\n\n    def connection_made(self, transport: asyncio.BaseTransport) -> None:\n        self._transport = cast(asyncio.Transport, transport)\n        self._transport.write(\n            amsg._uint64_packer(os.getpid()) + amsg._uint64_packer(0)\n        )\n        peername = transport.get_extra_info('peername')\n        try:\n            self._client_name = '%s:%d' % peername\n        except Exception:\n            self._client_name = str(peername)\n\n    def connection_lost(self, exc: Optional[Exception]) -> None:\n        self._transport = None\n        self._pool.client_disconnected(self._client_id)\n\n    def data_received(self, data: bytes) -> None:\n        for msg in self._stream.feed_data(data):\n            msgview = memoryview(msg)\n            req_id = amsg._uint64_unpacker(msgview[:8])[0]\n            self._loop.create_task(\n                self._pool.handle_client_call(self, req_id, msgview[8:])\n            )\n\n    @property\n    def client_id(self) -> int:\n        return self._client_id\n\n    @property\n    def client_name(self) -> str:\n        return self._client_name\n\n    def reply(self, req_id: int, resp: bytes) -> None:\n        if self._transport is None:\n            return\n        self._transport.write(\n            b\"\".join(\n                (\n                    amsg._uint64_packer(len(resp) + 8),\n                    amsg._uint64_packer(req_id),\n                    resp,\n                )\n            )\n        )\n\n\nclass MetricsProtocol(asyncio.Protocol):\n    _pool: MultiSchemaPool\n    transport: Optional[asyncio.Transport]\n    parser: httptools.HttpRequestParser\n    url: Optional[bytes]\n\n    def __init__(self, pool: MultiSchemaPool) -> None:\n        self._pool = pool\n        self.transport = None\n        self.parser = httptools.HttpRequestParser(self)\n        self.url = None\n\n    def connection_made(self, transport: asyncio.BaseTransport) -> None:\n        self.transport = cast(asyncio.Transport, transport)\n\n    def data_received(self, data: bytes) -> None:\n        try:\n            self.parser.feed_data(data)\n        except Exception as ex:\n            logger.exception(ex)\n\n    def on_url(self, url: bytes) -> None:\n        self.url = url\n\n    def on_message_complete(self) -> None:\n        match self.parser.get_method().upper(), self.url:\n            case b\"GET\", b\"/ready\":\n                self.respond(\"200 OK\", \"OK\")\n\n            case b\"GET\", b\"/metrics\":\n                self._pool.refresh_metrics()\n                self.respond(\n                    \"200 OK\",\n                    metrics.registry.generate(),\n                    \"Content-Type: text/plain; version=0.0.4; charset=utf-8\",\n                )\n\n            case _:\n                self.respond(\"404 Not Found\", \"Not Found\")\n\n    def respond(\n        self,\n        status: str,\n        content: str,\n        *extra_headers: str,\n        encoding: str = \"utf-8\",\n    ) -> None:\n        content_bytes = content.encode(encoding)\n        response = [\n            f\"HTTP/{self.parser.get_http_version()} {status}\",\n            f\"Content-Length: {len(content_bytes)}\",\n            *extra_headers,\n            \"\",\n            \"\",\n        ]\n\n        assert self.transport is not None\n        self.transport.write(\"\\r\\n\".join(response).encode(\"ascii\"))\n        self.transport.write(content_bytes)\n        if not self.parser.should_keep_alive():\n            self.transport.close()\n\n\nasync def server_main(\n    listen_addresses: Sequence[str],\n    listen_port: Optional[int],\n    pool_size: int,\n    client_schema_cache_size: int,\n    runstate_dir: Optional[str | pathlib.Path],\n    metrics_port: Optional[int],\n    worker_max_rss: Optional[int],\n):\n    logsetup.setup_logging('i', 'stderr')\n    if listen_port is None:\n        listen_port = defines.EDGEDB_REMOTE_COMPILER_PORT\n    if runstate_dir is None:\n        temp_runstate_dir = tempfile.TemporaryDirectory(prefix='edbcompiler-')\n        runstate_dir = temp_runstate_dir.name\n        logger.debug(\"Using temporary runstate dir: %s\", runstate_dir)\n    else:\n        temp_runstate_dir = None\n        runstate_dir = str(runstate_dir)\n\n    secret = os.environ.get(\"_EDGEDB_SERVER_COMPILER_POOL_SECRET\")\n    if not secret:\n        logger.warning(\n            \"_EDGEDB_SERVER_COMPILER_POOL_SECRET is not set, \"\n            f\"compilation requests will fail\")\n        secret = secrets.token_urlsafe()\n\n    try:\n        loop = asyncio.get_running_loop()\n        pool = MultiSchemaPool(\n            loop=loop,\n            runstate_dir=runstate_dir,\n            pool_size=pool_size,\n            worker_branch_limit=0,  # compiler server doesn't use this limit\n            cache_size=client_schema_cache_size,\n            secret=secret.encode(),\n            worker_max_rss=worker_max_rss,\n        )\n        await pool.start()\n        try:\n            async with asyncio.TaskGroup() as tg:\n                tg.create_task(\n                    _run_server(\n                        loop,\n                        listen_addresses,\n                        listen_port,\n                        lambda: CompilerServerProtocol(pool, loop),\n                        \"compile\",\n                    )\n                )\n                if metrics_port:\n                    tg.create_task(\n                        _run_server(\n                            loop,\n                            listen_addresses,\n                            metrics_port,\n                            lambda: MetricsProtocol(pool),\n                            \"metrics\",\n                        )\n                    )\n        finally:\n            await pool.stop()\n    finally:\n        if temp_runstate_dir is not None:\n            temp_runstate_dir.cleanup()\n\n\nasync def _run_server(\n    loop: asyncio.AbstractEventLoop,\n    listen_addresses: Sequence[str],\n    listen_port: int,\n    protocol: Callable[[], asyncio.Protocol],\n    purpose: str,\n) -> None:\n    server = await loop.create_server(\n        protocol,\n        listen_addresses,\n        listen_port,\n        start_serving=False,\n    )\n    if len(listen_addresses) == 1:\n        logger.info(\n            \"Listening for %s on %s:%s\",\n            purpose,\n            listen_addresses[0],\n            listen_port,\n        )\n    else:\n        logger.info(\n            \"Listening for %s on [%s]:%s\",\n            purpose,\n            \",\".join(listen_addresses),\n            listen_port,\n        )\n    try:\n        await server.serve_forever()\n    finally:\n        server.close()\n        await server.wait_closed()\n\n\n@click.command()\n@srvargs.compiler_options\ndef main(**kwargs: Any) -> None:\n    asyncio.run(server_main(**kwargs))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "edb/server/compiler_pool/state.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport typing\n\nimport immutables\n\nfrom edb.schema import schema\nfrom edb.server import config\n\n\nReflectionCache = immutables.Map[str, tuple[str, ...]]\n\n\nclass DatabaseState(typing.NamedTuple):\n    name: str\n    user_schema: schema.Schema\n    reflection_cache: ReflectionCache\n    database_config: immutables.Map[str, config.SettingValue]\n\n\nDatabasesState = immutables.Map[str, DatabaseState]\n\n\nclass PickledDatabaseState(typing.NamedTuple):\n    user_schema_pickle: bytes\n    reflection_cache: ReflectionCache\n    database_config: immutables.Map[str, config.SettingValue]\n\n    def get_estimated_size(self) -> int:\n        return (\n            len(self.user_schema_pickle) +\n            len(self.reflection_cache) * 128 +\n            len(self.database_config) * 128\n        )\n\n\nclass FailedStateSync(Exception):\n    pass\n\n\nclass StateNotFound(Exception):\n    pass\n\n\nclass IncompatibleClient(Exception):\n    pass\n\n\nREUSE_LAST_STATE_MARKER = b'REUSE_LAST_STATE_MARKER'\n"
  },
  {
    "path": "edb/server/compiler_pool/worker.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Mapping, Optional\n\nimport pickle\n\nimport immutables\n\nfrom edb import edgeql\nfrom edb import graphql\nfrom edb.common import uuidgen\nfrom edb.pgsql import params as pgparams\nfrom edb.schema import schema as s_schema\nfrom edb.server import compiler\nfrom edb.server import config\nfrom edb.server import defines\n\nfrom . import state\nfrom . import worker_proc\n\n\nINITED: bool = False\nDBS: state.DatabasesState = immutables.Map()\nBACKEND_RUNTIME_PARAMS: pgparams.BackendRuntimeParams = \\\n    pgparams.get_default_runtime_params()\nCOMPILER: compiler.Compiler\nLAST_STATE: Optional[compiler.dbstate.CompilerConnectionState] = None\nLAST_STATE_PICKLE: Optional[bytes] = None\nSTD_SCHEMA: s_schema.Schema\nGLOBAL_SCHEMA: s_schema.Schema\nINSTANCE_CONFIG: immutables.Map[str, config.SettingValue]\n\n\ndef __init_worker__(\n    init_args_pickled: bytes,\n) -> None:\n    global INITED\n    global BACKEND_RUNTIME_PARAMS\n    global COMPILER\n    global STD_SCHEMA\n    global GLOBAL_SCHEMA\n    global INSTANCE_CONFIG\n\n    (\n        backend_runtime_params,\n        std_schema,\n        refl_schema,\n        schema_class_layout,\n        global_schema_pickle,\n        system_config,\n    ) = pickle.loads(init_args_pickled)\n\n    INITED = True\n    BACKEND_RUNTIME_PARAMS = backend_runtime_params\n    STD_SCHEMA = std_schema\n    GLOBAL_SCHEMA = pickle.loads(global_schema_pickle)\n    INSTANCE_CONFIG = system_config\n\n    COMPILER = compiler.new_compiler(\n        std_schema,\n        refl_schema,\n        schema_class_layout,\n        backend_runtime_params=BACKEND_RUNTIME_PARAMS,\n        config_spec=None,\n    )\n\n\ndef __sync__(\n    dbname: str,\n    evicted_dbs: list[str],\n    user_schema: Optional[bytes],\n    reflection_cache: Optional[bytes],\n    global_schema: Optional[bytes],\n    database_config: Optional[bytes],\n    system_config: Optional[bytes],\n) -> state.DatabaseState:\n    global DBS\n    global GLOBAL_SCHEMA\n    global INSTANCE_CONFIG\n\n    try:\n        if evicted_dbs:\n            dbs = DBS.mutate()\n            for name in evicted_dbs:\n                dbs.pop(name, None)\n            DBS = dbs.finish()\n\n        db = DBS.get(dbname)\n        if db is None:\n            assert user_schema is not None\n            assert reflection_cache is not None\n            assert database_config is not None\n            user_schema_unpacked = pickle.loads(user_schema)\n            reflection_cache_unpacked = pickle.loads(reflection_cache)\n            database_config_unpacked = pickle.loads(database_config)\n            db = state.DatabaseState(\n                dbname,\n                user_schema_unpacked,\n                reflection_cache_unpacked,\n                database_config_unpacked,\n            )\n            DBS = DBS.set(dbname, db)\n        else:\n            updates = {}\n\n            if user_schema is not None:\n                updates['user_schema'] = pickle.loads(user_schema)\n            if reflection_cache is not None:\n                updates['reflection_cache'] = pickle.loads(reflection_cache)\n            if database_config is not None:\n                updates['database_config'] = pickle.loads(database_config)\n\n            if updates:\n                db = db._replace(**updates)\n                DBS = DBS.set(dbname, db)\n\n        if global_schema is not None:\n            GLOBAL_SCHEMA = pickle.loads(global_schema)\n\n        if system_config is not None:\n            INSTANCE_CONFIG = pickle.loads(system_config)\n\n    except Exception as ex:\n        raise state.FailedStateSync(\n            f'failed to sync worker state: {type(ex).__name__}({ex})') from ex\n\n    return db\n\n\ndef compile(\n    dbname: str,\n    evicted_dbs: list[str],\n    user_schema: Optional[bytes],\n    reflection_cache: Optional[bytes],\n    global_schema: Optional[bytes],\n    database_config: Optional[bytes],\n    system_config: Optional[bytes],\n    *compile_args: Any,\n    **compile_kwargs: Any,\n):\n    db = __sync__(\n        dbname,\n        evicted_dbs,\n        user_schema,\n        reflection_cache,\n        global_schema,\n        database_config,\n        system_config,\n    )\n\n    units, cstate = COMPILER.compile_serialized_request(\n        db.user_schema,\n        GLOBAL_SCHEMA,\n        db.reflection_cache,\n        db.database_config,\n        INSTANCE_CONFIG,\n        *compile_args,\n        **compile_kwargs\n    )\n\n    global LAST_STATE, LAST_STATE_PICKLE\n\n    LAST_STATE = cstate\n    LAST_STATE_PICKLE = None\n    if cstate is not None:\n        LAST_STATE_PICKLE = pickle.dumps(cstate, -1)\n\n    return units, LAST_STATE_PICKLE\n\n\ndef compile_in_tx(\n    dbname: Optional[str], user_schema: Optional[bytes], cstate, *args, **kwargs\n):\n    global LAST_STATE, LAST_STATE_PICKLE\n\n    prev_last_state_key = None\n    if cstate == state.REUSE_LAST_STATE_MARKER:\n        assert LAST_STATE is not None\n        cstate = LAST_STATE\n        prev_last_state_key = cstate.get_state_key()\n    else:\n        cstate = pickle.loads(cstate)\n        LAST_STATE_PICKLE = None\n        if dbname is None:\n            assert user_schema is not None\n            cstate.set_root_user_schema(pickle.loads(user_schema))\n        else:\n            cstate.set_root_user_schema(DBS[dbname].user_schema)\n    units, cstate = COMPILER.compile_serialized_request_in_tx(\n        cstate, *args, **kwargs)\n\n    LAST_STATE = cstate\n\n    # We don't want to continuously re-pickle transaction state\n    # for every new query in a transaction that doesn't actually change\n    # its state in every query. I.e. it doesn't run DDL, configures\n    # new session aliases, configs, or globals.\n    if (prev_last_state_key is None or\n        LAST_STATE_PICKLE is None or\n        prev_last_state_key != cstate.get_state_key()\n    ):\n        LAST_STATE_PICKLE = pickle.dumps(cstate, -1)\n\n    return units, LAST_STATE_PICKLE\n\n\ndef compile_notebook(\n    dbname: str,\n    evicted_dbs: list[str],\n    user_schema: Optional[bytes],\n    reflection_cache: Optional[bytes],\n    global_schema: Optional[bytes],\n    database_config: Optional[bytes],\n    system_config: Optional[bytes],\n    *compile_args: Any,\n    **compile_kwargs: Any,\n):\n    db = __sync__(\n        dbname,\n        evicted_dbs,\n        user_schema,\n        reflection_cache,\n        global_schema,\n        database_config,\n        system_config,\n    )\n\n    return COMPILER.compile_notebook(\n        db.user_schema,\n        GLOBAL_SCHEMA,\n        db.reflection_cache,\n        db.database_config,\n        INSTANCE_CONFIG,\n        *compile_args,\n        **compile_kwargs\n    )\n\n\ndef compile_graphql(\n    dbname: str,\n    evicted_dbs: list[str],\n    user_schema: Optional[bytes],\n    reflection_cache: Optional[bytes],\n    global_schema: Optional[bytes],\n    database_config: Optional[bytes],\n    system_config: Optional[bytes],\n    session_config: Mapping[str, Any],\n    *compile_args: Any,\n    **compile_kwargs: Any,\n) -> tuple[compiler.QueryUnitGroup, graphql.TranspiledOperation]:\n    db = __sync__(\n        dbname,\n        evicted_dbs,\n        user_schema,\n        reflection_cache,\n        global_schema,\n        database_config,\n        system_config,\n    )\n\n    gql_op = graphql.compile_graphql(\n        STD_SCHEMA,\n        db.user_schema,\n        GLOBAL_SCHEMA,\n        db.database_config,\n        INSTANCE_CONFIG,\n        *compile_args,\n        **compile_kwargs\n    )\n\n    source = edgeql.Source.from_string(\n        edgeql.generate_source(gql_op.edgeql_ast, pretty=True),\n    )\n\n    cfg_ser = COMPILER.state.compilation_config_serializer\n    request = compiler.CompilationRequest(\n        source=source,\n        protocol_version=defines.CURRENT_PROTOCOL,\n        schema_version=uuidgen.uuid4(),\n        compilation_config_serializer=cfg_ser,\n        output_format=compiler.OutputFormat.JSON,\n        input_format=compiler.InputFormat.JSON,\n        expect_one=True,\n        implicit_limit=0,\n        inline_typeids=False,\n        inline_typenames=False,\n        inline_objectids=False,\n        modaliases=None,\n        session_config=session_config,\n    )\n\n    unit_group, _ = COMPILER.compile(\n        user_schema=db.user_schema,\n        global_schema=GLOBAL_SCHEMA,\n        reflection_cache=db.reflection_cache,\n        database_config=db.database_config,\n        system_config=INSTANCE_CONFIG,\n        request=request,\n    )\n\n    return unit_group, gql_op  # type: ignore[return-value]\n\n\ndef compile_sql(\n    dbname: str,\n    evicted_dbs: list[str],\n    user_schema: Optional[bytes],\n    reflection_cache: Optional[bytes],\n    global_schema: Optional[bytes],\n    database_config: Optional[bytes],\n    system_config: Optional[bytes],\n    *compile_args: Any,\n    **compile_kwargs: Any,\n):\n    db = __sync__(\n        dbname,\n        evicted_dbs,\n        user_schema,\n        reflection_cache,\n        global_schema,\n        database_config,\n        system_config,\n    )\n\n    return COMPILER.compile_sql(\n        db.user_schema,\n        GLOBAL_SCHEMA,\n        db.reflection_cache,\n        db.database_config,\n        INSTANCE_CONFIG,\n        *compile_args,\n        **compile_kwargs\n    )\n\n\ndef get_handler(methname):\n    if methname == \"__init_worker__\":\n        meth = __init_worker__\n    else:\n        if not INITED:\n            raise RuntimeError(\n                \"call on uninitialized compiler worker\"\n            )\n        if methname == \"compile\":\n            meth = compile\n        elif methname == \"compile_in_tx\":\n            meth = compile_in_tx\n        elif methname == \"compile_notebook\":\n            meth = compile_notebook\n        elif methname == \"compile_graphql\":\n            meth = compile_graphql\n        elif methname == \"compile_sql\":\n            meth = compile_sql\n        else:\n            meth = getattr(COMPILER, methname)\n    return meth\n\n\nif __name__ == \"__main__\":\n    try:\n        worker_proc.main(get_handler)\n    except KeyboardInterrupt:\n        pass\n"
  },
  {
    "path": "edb/server/compiler_pool/worker_proc.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport argparse\nimport gc\nimport os\nimport pickle\nimport signal\nimport sys\nimport time\nimport traceback\n\nfrom edb.common import debug\nfrom edb.common import devmode\nfrom edb.common import markup\nfrom edb.common import lru\nfrom edb.edgeql import parser as ql_parser\n\nfrom . import amsg\n\n\n# \"created continuously\" means the interval between two consecutive spawns\n# is less than NUM_SPAWNS_RESET_INTERVAL seconds.\nNUM_SPAWNS_RESET_INTERVAL = 1\n\n\ndef worker(sockname, version_serial, get_handler):\n    con = amsg.WorkerConnection(sockname, version_serial)\n    try:\n        for req_id, req in con.iter_request():\n            try:\n                methname, args = pickle.loads(req)\n                meth = get_handler(methname)\n            except Exception as ex:\n                prepare_exception(ex)\n                if debug.flags.server:\n                    markup.dump(ex)\n                data = (1, ex, traceback.format_exc())\n            else:\n                try:\n                    res = meth(*args)\n                    data = (0, res)\n                except Exception as ex:\n                    prepare_exception(ex)\n                    if debug.flags.server:\n                        markup.dump(ex)\n                    data = (1, ex, traceback.format_exc())\n\n            try:\n                pickled = pickle.dumps(data, -1)\n            except Exception as ex:\n                ex_tb = traceback.format_exc()\n                ex_str = f\"{ex}:\\n\\n{ex_tb}\"\n                pickled = pickle.dumps((2, ex_str), -1)\n\n            con.reply(req_id, pickled)\n\n            # Now that we have responded, clear the compiler LRU\n            # caches to avoid hanging onto heavy objects like schemas.\n            lru.clear_lru_caches()\n    finally:\n        con.abort()\n\n\ndef run_worker(sockname, version_serial, get_handler):\n    with devmode.CoverageConfig.enable_coverage_if_requested():\n        worker(sockname, version_serial, get_handler)\n\n\ndef prepare_exception(ex):\n    clear_exception_frames(ex)\n    if ex.__traceback__ is not None:\n        ex.__traceback__ = ex.__traceback__.tb_next\n\n\ndef clear_exception_frames(er):\n    def _clear_exception_frames(er, visited):\n        if er in visited:\n            return er\n        visited.add(er)\n\n        traceback.clear_frames(er.__traceback__)\n\n        if er.__cause__ is not None:\n            er.__cause__ = _clear_exception_frames(er.__cause__, visited)\n        if er.__context__ is not None:\n            er.__context__ = _clear_exception_frames(er.__context__, visited)\n\n        return er\n\n    visited = set()\n    _clear_exception_frames(er, visited)\n\n\ndef listen_for_debugger():\n    if debug.flags.pydebug_listen:\n        import debugpy\n        debugpy.listen(38781)\n\n\ndef main(get_handler):\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--sockname\")\n    parser.add_argument(\"--numproc\")\n    parser.add_argument(\"--version-serial\", type=int)\n    args = parser.parse_args()\n\n    sys.setrecursionlimit(2000)\n\n    ql_parser.preload_spec()\n    gc.freeze()\n\n    listen_for_debugger()\n\n    if args.numproc is None:\n        # Run a single worker process\n        run_worker(args.sockname, args.version_serial, get_handler)\n        return\n\n    numproc = int(args.numproc)\n    assert numproc >= 1\n\n    # Abort the template process if more than `max_worker_spawns`\n    # new workers are created continuously - it probably means the\n    # worker cannot start correctly.\n    max_worker_spawns = numproc * 2\n\n    children = set()\n    continuous_num_spawns = 0\n\n    for _ in range(int(args.numproc)):\n        # spawn initial workers\n        if pid := os.fork():\n            # main process\n            children.add(pid)\n            continuous_num_spawns += 1\n        else:\n            # child process\n            break\n    else:\n        # main process - redirect SIGTERM to SystemExit and wait for children\n        signal.signal(signal.SIGTERM, lambda *_: exit(os.EX_OK))\n        last_spawn_timestamp = time.monotonic()\n\n        try:\n            while children:\n                pid, status = os.wait()\n                children.remove(pid)\n                ec = os.waitstatus_to_exitcode(status)\n                if ec > 0 or -ec not in {0, signal.SIGINT}:\n                    # restart the child process if killed or ending abnormally,\n                    # unless we tried too many times continuously\n                    now = time.monotonic()\n                    if now - last_spawn_timestamp > NUM_SPAWNS_RESET_INTERVAL:\n                        continuous_num_spawns = 0\n                    last_spawn_timestamp = now\n                    continuous_num_spawns += 1\n                    if continuous_num_spawns > max_worker_spawns:\n                        # GOTCHA: we shouldn't return here because we need the\n                        # exception handler below to clean up the workers\n                        exit(os.EX_UNAVAILABLE)\n\n                    if pid := os.fork():\n                        # main process\n                        children.add(pid)\n                    else:\n                        # child process\n                        break\n            else:\n                # main process - all children ended normally\n                return\n        except BaseException as e:  # includes SystemExit and KeyboardInterrupt\n            # main process - kill and wait for the remaining workers to exit\n            try:\n                signal.signal(signal.SIGTERM, signal.SIG_DFL)\n                for pid in children:\n                    try:\n                        os.kill(pid, signal.SIGTERM)\n                    except OSError:\n                        pass\n                try:\n                    while children:\n                        pid, status = os.wait()\n                        children.discard(pid)\n                except OSError:\n                    pass\n            finally:\n                raise e\n\n    # child process - clear the SIGTERM handler for potential Rust impl\n    signal.signal(signal.SIGTERM, signal.SIG_DFL)\n    run_worker(args.sockname, args.version_serial, get_handler)\n"
  },
  {
    "path": "edb/server/config/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Mapping, TypedDict\n\nimport enum\n\nimport immutables\n\nfrom edb import errors\nfrom edb.edgeql.qltypes import ConfigScope\n\nfrom .ops import OpCode, Operation, SettingValue\nfrom .ops import (\n    spec_to_json, to_json_obj, to_json, from_json, set_value, to_edgeql\n)\nfrom .ops import value_from_json, value_to_json_value\nfrom .spec import (\n    Spec, FlatSpec, ChainedSpec, Setting,\n    load_spec_from_schema, load_ext_spec_from_schema,\n    load_ext_settings_from_schema,\n)\nfrom .types import ConfigType, CompositeConfigType\nfrom .types import QueryCacheMode\n\n\n__all__ = (\n    'lookup',\n    'Spec', 'FlatSpec', 'ChainedSpec', 'Setting', 'SettingValue',\n    'spec_to_json', 'to_json_obj', 'to_json', 'to_edgeql', 'from_json',\n    'set_value', 'value_from_json', 'value_to_json_value',\n    'ConfigScope', 'OpCode', 'Operation',\n    'ConfigType', 'CompositeConfigType',\n    'load_spec_from_schema', 'load_ext_spec_from_schema',\n    'load_ext_settings_from_schema',\n    'get_compilation_config',\n    'QueryCacheMode',\n    'ConState', 'ConStateType',\n)\n\n\n# See edb/server/pgcon/connect.py for documentation of the types\nclass ConStateType(enum.StrEnum):\n    session_config = \"C\"\n    backend_session_config = \"B\"\n    command_line_argument = \"A\"\n    environment_variable = \"E\"\n    config_file = \"F\"\n\n\nclass ConState(TypedDict):\n    name: str\n    value: Any\n    type: ConStateType\n\n\ndef lookup(\n    name: str,\n    *configs: Mapping[str, SettingValue],\n    spec: Spec,\n    allow_unrecognized: bool = False,\n) -> Any:\n    assert len(configs) > 0\n\n    try:\n        setting = spec[name]\n    except (KeyError, TypeError):\n        if allow_unrecognized:\n            return None\n        else:\n            raise errors.ConfigurationError(\n                f'unrecognized configuration parameter {name!r}')\n\n    for c in configs:\n        try:\n            setting_value = c[name]\n        except KeyError:\n            pass\n        else:\n            return setting_value.value\n    else:\n        return setting.default\n\n\ndef get_compilation_config(\n    config: Mapping[str, SettingValue],\n    *,\n    spec: Spec,\n) -> immutables.Map[str, SettingValue]:\n    return immutables.Map((\n        (k, v)\n        for k, v in config.items()\n        if k in spec\n        if spec[k].affects_compilation\n    ))\n\n\ndef _serialize_val(v: object) -> object:\n    if isinstance(v, frozenset):\n        return [_serialize_val(x) for x in v]\n    elif isinstance(v, CompositeConfigType):\n        return v.to_json_value(redacted=True)\n    else:\n        return v\n\n\ndef debug_serialize_config(\n    cfg: Mapping[str, SettingValue],\n) -> Any:\n    return {\n        name:\n        {'redacted': True} if value.secret\n        else _serialize_val(value.value)\n        for name, value in cfg.items()\n    }\n"
  },
  {
    "path": "edb/server/config/ops.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\n\nimport base64\nimport json\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n    Iterable,\n    Mapping,\n    Collection,\n    NamedTuple,\n    TYPE_CHECKING,\n    TypeGuard,\n)\n\nimport immutables\n\nfrom edb import errors\nfrom edb.common import enum\nfrom edb.common import typeutils\n\nfrom edb.ir import statypes\n\nfrom edb.edgeql import codegen as qlcodegen\nfrom edb.edgeql import qltypes\n\nfrom edb.schema import objects as s_obj\nfrom edb.schema import utils as s_utils\n\nfrom . import spec\nfrom . import types\n\n\nMAX_CONFIG_SET_SIZE = 128\n\n\nclass OpCode(enum.StrEnum):\n\n    CONFIG_ADD = 'ADD'\n    CONFIG_REM = 'REM'\n    CONFIG_SET = 'SET'\n    CONFIG_RESET = 'RESET'\n\n\nclass SettingValue(NamedTuple):\n\n    name: str\n    value: Any\n    source: str\n    scope: qltypes.ConfigScope\n    # We track this just so that we can redact secret values in our\n    # debug endpoints.\n    secret: bool = False\n\n\nif TYPE_CHECKING:\n    SettingsMap = immutables.Map[str, SettingValue]\n\n\ndef _issubclass[T_type: type](\n    typ: type | types.ConfigTypeSpec, parent: T_type\n) -> TypeGuard[T_type]:\n    return isinstance(typ, type) and issubclass(typ, parent)\n\n\ndef coerce_single_value(setting: spec.Setting, value: Any) -> Any:\n    if isinstance(setting.type, type) and isinstance(value, setting.type):\n        return value\n    elif (isinstance(value, str) and\n          _issubclass(setting.type, statypes.Duration)):\n        return statypes.Duration(value)\n    elif (isinstance(value, (str, int)) and\n          _issubclass(setting.type, statypes.ConfigMemory)):\n        return statypes.ConfigMemory(value)\n    elif (isinstance(value, str) and\n          _issubclass(setting.type, statypes.EnumScalarType)):\n        return setting.type(value)\n    else:\n        raise errors.ConfigurationError(\n            f'invalid value type for the {setting.name!r} setting')\n\n\ndef _check_object_set_uniqueness(\n    setting: spec.Setting, objs: Iterable[types.CompositeConfigType]\n) -> frozenset[types.CompositeConfigType]:\n    \"\"\"Check the unique constraints for an object set\"\"\"\n\n    new_values = set()\n    exclusive_keys: dict[tuple[str, str], Any] = {}\n    for new_value in objs:\n        tspec = new_value._tspec\n        for name in tspec.fields:\n            if (val := getattr(new_value, name, None)) is None:\n                continue\n            if (site := tspec.get_field_unique_site(name)):\n                key = (site.name, name)\n                current = exclusive_keys.setdefault(key, set())\n                if val in current:\n                    raise errors.ConstraintViolationError(\n                        f'{setting.type.__name__}.{name} '\n                        f'violates exclusivity constraint'\n                    )\n                current.add(val)\n\n        if new_value in new_values:\n            raise errors.ConstraintViolationError(\n                f'{setting.type.__name__} has no unique values'\n            )\n        new_values.add(new_value)\n\n    if len(new_values) > MAX_CONFIG_SET_SIZE:\n        raise errors.ConfigurationError(\n            f'invalid value for the '\n            f'{setting.name!r} setting: set is too large')\n\n    return frozenset(new_values)\n\n\ndef coerce_object_set(\n    spec: spec.Spec, setting: spec.Setting, values: Any\n) -> Any:\n    assert isinstance(setting.type, types.ConfigTypeSpec)\n    if not setting.set_of and len(values) > 1:\n        raise errors.ConstraintViolationError(\n            f'cannot have multiple values for single setting {setting.name!r}'\n        )\n\n    return _check_object_set_uniqueness(\n        setting,\n        (\n            types.CompositeConfigType.from_pyvalue(\n                jv, spec=spec, tspec=setting.type)\n            for jv in values\n        ),\n    )\n\n\nclass Operation(NamedTuple):\n\n    opcode: OpCode\n    scope: qltypes.ConfigScope\n    setting_name: str\n    value: str | int | bool | Collection[str | int | bool | None] | None\n\n    def get_setting(self, spec: spec.Spec) -> spec.Setting:\n        try:\n            return spec[self.setting_name]\n        except KeyError:\n            raise errors.ConfigurationError(\n                f'unknown setting {self.setting_name!r}') from None\n\n    def coerce_value(\n        self,\n        spec: spec.Spec,\n        setting: spec.Setting,\n        *,\n        allow_missing: bool = False,\n    ):\n        if isinstance(setting.type, types.ConfigTypeSpec):\n            try:\n                if self.opcode is OpCode.CONFIG_SET:\n                    return coerce_object_set(spec, setting, self.value)\n                else:\n                    return types.CompositeConfigType.from_pyvalue(\n                        self.value, spec=spec, tspec=setting.type,\n                        allow_missing=allow_missing,\n                    )\n            except (ValueError, TypeError):\n                raise errors.ConfigurationError(\n                    f'invalid value type for the {setting.name!r} setting')\n        elif setting.set_of:\n            if self.value is None and allow_missing:\n                return None\n            elif not typeutils.is_container(self.value):\n                raise errors.ConfigurationError(\n                    f'invalid value type for the '\n                    f'{setting.name!r} setting')\n            else:\n                val = frozenset(\n                    coerce_single_value(setting, v)\n                    for v in self.value)  # type: ignore\n                if len(val) > MAX_CONFIG_SET_SIZE:\n                    raise errors.ConfigurationError(\n                        f'invalid value for the '\n                        f'{setting.name!r} setting: set is too large')\n                return val\n\n        else:\n            try:\n                return coerce_single_value(setting, self.value)\n            except errors.ConfigurationError:\n                if self.value is None and allow_missing:\n                    return None\n                else:\n                    raise\n\n    def coerce_global_value(\n        self, *, allow_missing: bool = False\n    ) -> Optional[bytes]:\n        if allow_missing and self.value is None:\n            return None\n        else:\n            assert isinstance(self.value, str)\n            b = base64.b64decode(self.value)\n            # Input comes prefixed with length; if the length is -1,\n            # the value has explicitly been set to {}.\n            return b[4:] if b[:4] != b'\\xff\\xff\\xff\\xff' else None\n\n    def apply(\n        self,\n        spec: spec.Spec,\n        storage: SettingsMap,\n        *,\n        source: str | None = None,\n    ) -> SettingsMap:\n\n        allow_missing = (\n            self.opcode is OpCode.CONFIG_REM\n            or self.opcode is OpCode.CONFIG_RESET\n        )\n\n        if self.scope != qltypes.ConfigScope.GLOBAL:\n            setting = self.get_setting(spec)\n            value = self.coerce_value(\n                spec, setting, allow_missing=allow_missing)\n        else:\n            setting = None\n            value = self.coerce_global_value(allow_missing=allow_missing)\n\n        if self.opcode is OpCode.CONFIG_SET:\n            storage = self._set_value(storage, value, source=source)\n\n        elif self.opcode is OpCode.CONFIG_RESET:\n            try:\n                storage = storage.delete(self.setting_name)\n            except KeyError:\n                pass\n\n        elif self.opcode is OpCode.CONFIG_ADD:\n            assert setting\n            if not isinstance(setting.type, types.ConfigTypeSpec):\n                raise errors.InternalServerError(\n                    f'unexpected CONFIGURE SET += on a primitive '\n                    f'configuration parameter: {self.setting_name}'\n                )\n\n            exist_setting = storage.get(self.setting_name)\n            if exist_setting is not None:\n                exist_value = exist_setting.value\n            else:\n                exist_value = setting.default\n\n            new_value = _check_object_set_uniqueness(\n                setting, list(exist_value) + [value])\n            storage = self._set_value(storage, new_value, source=source)\n\n        elif self.opcode is OpCode.CONFIG_REM:\n            assert setting\n            if not isinstance(setting.type, types.ConfigTypeSpec):\n                raise errors.InternalServerError(\n                    f'unexpected CONFIGURE SET -= on a primitive '\n                    f'configuration parameter: {self.setting_name}'\n                )\n\n            exist_setting = storage.get(self.setting_name)\n            if exist_setting is not None:\n                exist_value = exist_setting.value\n            else:\n                exist_value = setting.default\n            new_value = exist_value - {value}\n            storage = self._set_value(storage, new_value, source=source)\n\n        return storage\n\n    def _set_value(\n        self,\n        storage: SettingsMap,\n        value: Any,\n        *,\n        source: str | None = None,\n    ) -> SettingsMap:\n\n        if source is None:\n            if self.scope is qltypes.ConfigScope.INSTANCE:\n                source = 'system override'\n            elif self.scope is qltypes.ConfigScope.DATABASE:\n                source = 'database'\n            elif self.scope is qltypes.ConfigScope.SESSION:\n                source = 'session'\n            elif self.scope is qltypes.ConfigScope.GLOBAL:\n                source = 'global'\n            else:\n                raise AssertionError(f'unexpected config scope: {self.scope}')\n\n        return set_value(\n            storage,\n            self.setting_name,\n            value,\n            source=source,\n            scope=self.scope,\n        )\n\n    @classmethod\n    def from_json(cls, json_value: str) -> Operation:\n        op_str, scope_str, name, value = json.loads(json_value)\n        return Operation(\n            opcode=OpCode(op_str),\n            scope=qltypes.ConfigScope(scope_str),\n            setting_name=name,\n            value=value,\n        )\n\n\ndef spec_to_json(spec: spec.Spec):\n    dct = {}\n\n    for setting in spec.values():\n        if _issubclass(setting.type, str):\n            typeid = s_obj.get_known_type_id('std::str')\n        elif _issubclass(setting.type, bool):\n            typeid = s_obj.get_known_type_id('std::bool')\n        elif _issubclass(setting.type, int):\n            typeid = s_obj.get_known_type_id('std::int64')\n        elif _issubclass(setting.type, float):\n            typeid = s_obj.get_known_type_id('std::float32')\n        elif _issubclass(setting.type, types.ConfigType):\n            typeid = setting.type.get_edgeql_typeid()\n        elif _issubclass(setting.type, statypes.Duration):\n            typeid = s_obj.get_known_type_id('std::duration')\n        elif _issubclass(setting.type, statypes.ConfigMemory):\n            typeid = s_obj.get_known_type_id('cfg::memory')\n        elif _issubclass(setting.type, statypes.EnumScalarType):\n            typeid = setting.type.get_edgeql_typeid()\n        elif isinstance(setting.type, types.ConfigTypeSpec):\n            typeid = types.CompositeConfigType.get_edgeql_typeid()\n        else:\n            raise RuntimeError(\n                f'cannot serialize type for config setting {setting.name}')\n\n        typemod = qltypes.TypeModifier.SingletonType\n        if setting.set_of:\n            typemod = qltypes.TypeModifier.SetOfType\n\n        dct[setting.name] = {\n            'default': value_to_json_value(setting, setting.default),\n            'internal': setting.internal,\n            'system': setting.system,\n            'typeid': str(typeid),\n            'typemod': str(typemod),\n            'backend_setting': setting.backend_setting,\n            'report': setting.report,\n        }\n\n    return json.dumps(dct)\n\n\ndef value_to_json_value(setting: spec.Setting, value: Any):\n    if setting.set_of:\n        if isinstance(setting.type, types.ConfigTypeSpec):\n            return [v.to_json_value() for v in value]\n        else:\n            return list(value)\n    else:\n        if isinstance(setting.type, types.ConfigTypeSpec):\n            # We always store objects as list at the top-level, even\n            # if they are single, because it simplifies things in the\n            # config handling SQL.\n            return [value.to_json_value()] if value is not None else []\n        elif (_issubclass(setting.type, statypes.ScalarType) and\n                value is not None):\n            return value.to_json()\n        else:\n            return value\n\n\ndef value_from_json_value(spec: spec.Spec, setting: spec.Setting, value: Any):\n    if setting.set_of:\n        if isinstance(setting.type, types.ConfigTypeSpec):\n            return frozenset(\n                types.CompositeConfigType.from_pyvalue(\n                    v, spec=spec, tspec=setting.type,\n                )\n                for v in value\n            )\n        else:\n            return frozenset(value)\n    else:\n        if isinstance(setting.type, types.ConfigTypeSpec):\n            if not value:\n                return None\n            if len(value) > 1:\n                raise errors.ConfigurationError(\n                    f'multiple entries for single object {setting.name}'\n                )\n            return types.CompositeConfigType.from_pyvalue(\n                value[0], spec=spec, tspec=setting.type,\n            )\n        elif _issubclass(setting.type, statypes.Duration):\n            return statypes.Duration.from_iso8601(value)\n        elif _issubclass(setting.type, statypes.ConfigMemory):\n            return statypes.ConfigMemory(value)\n        elif _issubclass(setting.type, statypes.EnumScalarType):\n            return setting.type(value)\n        else:\n            return value\n\n\ndef value_from_json(spec, setting, value: str):\n    return value_from_json_value(spec, setting, json.loads(value))\n\n\ndef value_to_edgeql_const(\n    type: type | types.ConfigTypeSpec,\n    value: Any,\n    with_secrets: bool,\n) -> str:\n    ql = s_utils.const_ast_from_python(value, with_secrets=with_secrets)\n    return qlcodegen.generate_source(ql)\n\n\ndef to_json_obj(\n    spec: spec.Spec,\n    storage: Mapping[str, SettingValue],\n    *,\n    setting_filter: Optional[Callable[[SettingValue], bool]] = None,\n    include_source: bool = True,\n) -> dict[str, Any]:\n    dct = {}\n    for name, value in storage.items():\n        if setting_filter is None or setting_filter(value):\n            setting = spec[name]\n            val = value_to_json_value(setting, value.value)\n            if include_source:\n                dct[name] = {\n                    'name': name,\n                    'source': value.source,\n                    'scope': str(value.scope),\n                    'value': val,\n                }\n            else:\n                dct[name] = val\n    return dct\n\n\ndef to_json(\n    spec: spec.Spec,\n    storage: Mapping[str, SettingValue],\n    *,\n    setting_filter: Optional[Callable[[SettingValue], bool]] = None,\n    include_source: bool = True,\n) -> str:\n    dct = to_json_obj(\n        spec,\n        storage,\n        setting_filter=setting_filter,\n        include_source=include_source,\n    )\n    return json.dumps(dct)\n\n\ndef from_json(spec: spec.Spec, js: str | bytes) -> SettingsMap:\n    base: SettingsMap = immutables.Map()\n    with base.mutate() as mm:\n        dct = json.loads(js)\n\n        if not isinstance(dct, dict):\n            raise errors.ConfigurationError(\n                'invalid JSON: top-level dict was expected')\n\n        for key, value in dct.items():\n            setting = spec.get(key)\n            if setting is None:\n                # If the setting isn't in the spec, that's probably because\n                # we've downgraded minor versions. Don't worry about it.\n                continue\n\n            mm[key] = SettingValue(\n                name=key,\n                value=value_from_json_value(spec, setting, value['value']),\n                source=value['source'],\n                scope=qltypes.ConfigScope(value['scope']),\n                secret=setting.secret,\n            )\n\n    return mm.finish()\n\n\ndef to_edgeql(\n    spec: spec.Spec,\n    storage: Mapping[str, SettingValue],\n    with_secrets: bool,\n) -> str:\n    stmts = []\n\n    for name, value in storage.items():\n        if name not in spec:\n            continue\n        setting = spec[name]\n        if setting.secret and not with_secrets:\n            continue\n        if setting.protected:\n            continue\n        if isinstance(setting.type, types.ConfigTypeSpec):\n            values = value.value if setting.set_of else [value.value]\n            for x in values:\n                # We look at the specific type of the object because\n                # a subtype could have a secret that the parent doesn't.\n                if x._tspec.has_secret and not with_secrets:\n                    continue\n                val = value_to_edgeql_const(\n                    setting.type, x, with_secrets=with_secrets\n                )\n                stmt = f'CONFIGURE {value.scope.to_edgeql()}\\n{val};'\n                stmts.append(stmt)\n        else:\n            val = value_to_edgeql_const(\n                setting.type, value.value, with_secrets=with_secrets\n            )\n            stmt = f'CONFIGURE {value.scope.to_edgeql()} SET {name} := {val};'\n            stmts.append(stmt)\n\n    return '\\n'.join(stmts)\n\n\ndef set_value(\n    storage: SettingsMap,\n    name: str,\n    value: Any,\n    source: str,\n    scope: qltypes.ConfigScope,\n) -> SettingsMap:\n\n    secret = name in storage and storage[name].secret\n\n    return storage.set(\n        name,\n        SettingValue(name=name, value=value, source=source, scope=scope,\n                     secret=secret),\n    )\n"
  },
  {
    "path": "edb/server/config/spec.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom abc import abstractmethod\n\nimport collections.abc\nimport dataclasses\nimport json\nfrom typing import Any, Optional, Iterator, Sequence\n\nfrom edb.edgeql import compiler as qlcompiler\nfrom edb.ir import staeval\nfrom edb.ir import statypes\nfrom edb.schema import links as s_links\nfrom edb.schema import name as sn\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import schema as s_schema\n\nfrom edb.common.typeutils import downcast\n\nfrom . import types\n\n\nSETTING_TYPES = {\n    str,\n    int,\n    bool,\n    float,\n}\n\n\n@dataclasses.dataclass(frozen=True, eq=True)\nclass Setting:\n\n    name: str\n    type: type | types.ConfigTypeSpec\n    default: Any\n    schema_type_name: Optional[sn.Name] = None\n    set_of: bool = False\n    system: bool = False\n    internal: bool = False\n    requires_restart: bool = False\n    backend_setting: Optional[str] = None\n    report: bool = False\n    affects_compilation: bool = False\n    enum_values: Optional[Sequence[str]] = None\n    required: bool = True\n    secret: bool = False\n    protected: bool = False\n\n    session_restricted: bool = False\n    session_permission: Optional[str] = None\n\n    def __post_init__(self) -> None:\n        if (\n            self.type not in SETTING_TYPES\n            and not isinstance(self.type, types.ConfigTypeSpec)\n            and not (\n                isinstance(self.type, type)\n                and issubclass(self.type, statypes.ScalarType)\n            )\n        ):\n            raise ValueError(\n                f'invalid config setting {self.name!r}: '\n                f'type is expected to be either one of '\n                f'{{str, int, bool, float}} '\n                f'or an edb.server.config.types.ConfigType ',\n                f'or edb.ir.statypes.ScalarType subclass')\n\n        if self.set_of:\n            if not isinstance(self.default, frozenset):\n                raise ValueError(\n                    f'invalid config setting {self.name!r}: \"SET OF\" settings '\n                    f'must have frozenset() as a default value, got '\n                    f'{self.default!r}')\n\n            if self.default:\n                # SET OF settings shouldn't have non-empty defaults,\n                # as otherwise there are multiple semantical ambiguities:\n                # * Can a user add a new element to the set?\n                # * What happens of a user discards all elements from the set?\n                #   Does the set become non-empty because the default would\n                #   propagate?\n                # * etc.\n                raise ValueError(\n                    f'invalid config setting {self.name!r}: \"SET OF\" settings '\n                    f'should not have defaults')\n\n        else:\n            if (\n                not self.backend_setting\n                and isinstance(self.type, type)\n                and (\n                    (self.default and not isinstance(self.default, self.type))\n                    or (self.default is None and self.required)\n                )\n\n            ):\n                raise ValueError(\n                    f'invalid config setting {self.name!r}: '\n                    f'the default {self.default!r} '\n                    f'is not instance of {self.type}')\n\n        if self.report and not self.system:\n            raise ValueError('only instance settings can be reported')\n\n\nclass Spec(collections.abc.Mapping):\n    @abstractmethod\n    def get_type_by_name(self, name: str) -> types.ConfigTypeSpec:\n        raise NotImplementedError\n\n    @abstractmethod\n    def __iter__(self) -> Iterator[str]:\n        raise NotImplementedError\n\n    @abstractmethod\n    def __getitem__(self, name: str) -> Setting:\n        raise NotImplementedError\n\n    @abstractmethod\n    def __contains__(self, name: object) -> bool:\n        raise NotImplementedError\n\n    @abstractmethod\n    def __len__(self) -> int:\n        raise NotImplementedError\n\n\nclass FlatSpec(Spec):\n    def __init__(self, *settings: Setting):\n        self._settings = tuple(settings)\n        self._by_name = {s.name: s for s in self._settings}\n        self._types_by_name: dict[str, types.ConfigTypeSpec] = {}\n\n        for s in self._settings:\n            if isinstance(s.type, types.ConfigTypeSpec):\n                self._register_type(s.type)\n\n    def _register_type(self, t: types.ConfigTypeSpec) -> None:\n        self._types_by_name[t.name] = t\n        for subclass in t.children:\n            self._register_type(downcast(types.ConfigTypeSpec, subclass))\n\n        for field in t.fields.values():\n            f_type = field.type\n            if isinstance(f_type, types.ConfigTypeSpec):\n                self._register_type(f_type)\n\n    def get_type_by_name(self, name: str) -> types.ConfigTypeSpec:\n        return self._types_by_name[name]\n\n    def __iter__(self) -> Iterator[str]:\n        return iter(self._by_name)\n\n    def __getitem__(self, name: str) -> Setting:\n        return self._by_name[name]\n\n    def __contains__(self, name: object) -> bool:\n        return name in self._by_name\n\n    def __len__(self) -> int:\n        return len(self._settings)\n\n\nclass ChainedSpec(Spec):\n    def __init__(self, base: Spec, top: Spec):\n        self._base = base\n        self._top = top\n\n    def get_type_by_name(self, name: str) -> types.ConfigTypeSpec:\n        try:\n            return self._top.get_type_by_name(name)\n        except KeyError:\n            return self._base.get_type_by_name(name)\n\n    def __iter__(self) -> Iterator[str]:\n        yield from self._top\n        yield from self._base\n\n    def __getitem__(self, name: str) -> Setting:\n        if name in self._top:\n            return self._top[name]\n        else:\n            return self._base[name]\n        return self._by_name[name]\n\n    def __contains__(self, name: object) -> bool:\n        return name in self._top or name in self._base\n\n    def __len__(self) -> int:\n        return len(self._top) + len(self._base)\n\n\ndef load_spec_from_schema(\n    schema: s_schema.Schema,\n    only_exts: bool=False,\n    validate: bool=True,\n) -> Spec:\n    settings = []\n    if not only_exts:\n        cfg = schema.get('cfg::Config', type=s_objtypes.ObjectType)\n        settings.extend(_load_spec_from_type(schema, cfg))\n\n    settings.extend(load_ext_settings_from_schema(schema))\n\n    # Make sure there aren't any dangling ConfigObject children\n    if validate:\n        cfg_object = schema.get('cfg::ConfigObject', type=s_objtypes.ObjectType)\n        for child in cfg_object.children(schema):\n            if not schema.get_referrers(\n                child, scls_type=s_links.Link, field_name='target'\n            ):\n                raise RuntimeError(\n                    f'cfg::ConfigObject child {child.get_name(schema)} has no '\n                    f'links pointing at it (did you mean cfg::ExtensionConfig?)'\n                )\n\n    return FlatSpec(*settings)\n\n\ndef load_ext_settings_from_schema(schema: s_schema.Schema) -> list[Setting]:\n    settings = []\n    ext_cfg = schema.get('cfg::ExtensionConfig', type=s_objtypes.ObjectType)\n    for ecfg in ext_cfg.descendants(schema):\n        if not ecfg.get_abstract(schema):\n            settings.extend(_load_spec_from_type(schema, ecfg))\n    return settings\n\n\ndef load_ext_spec_from_schema(\n    user_schema: s_schema.Schema,\n    std_schema: s_schema.Schema,\n) -> Spec:\n    schema = s_schema.ChainedSchema(\n        std_schema, user_schema, s_schema.EMPTY_SCHEMA\n    )\n    return load_spec_from_schema(schema, only_exts=True)\n\n\ndef _load_spec_from_type(\n    schema: s_schema.Schema, cfg: s_objtypes.ObjectType\n) -> list[Setting]:\n    settings = []\n\n    cfg_name = str(cfg.get_name(schema))\n    is_root = cfg_name == 'cfg::Config'\n    for ptr_name, p in cfg.get_pointers(schema).items(schema):\n        pn = str(ptr_name)\n        if pn in ('id', '__type__') or p.get_computable(schema):\n            continue\n\n        ptype = p.get_target(schema)\n        assert ptype\n\n        # Skip backlinks to the base object. The will get plenty of\n        # special treatment.\n        if str(ptype.get_name(schema)) == 'cfg::AbstractConfig':\n            continue\n\n        pytype: type | types.ConfigTypeSpec\n        if isinstance(ptype, s_objtypes.ObjectType):\n            pytype = staeval.object_type_to_spec(\n                ptype, schema,\n                spec_class=types.ConfigTypeSpec,\n            )\n        elif isinstance(ptype, s_scalars.ScalarType):\n            pytype = staeval.scalar_type_to_python_type(ptype, schema)\n        else:\n            raise RuntimeError(f\"unsupported config value type: {ptype}\")\n\n        attributes = {}\n        for a, v in p.get_annotations(schema).items(schema):\n            if isinstance(a, sn.QualName) and a.module == 'cfg':\n                try:\n                    jv = json.loads(v.get_value(schema))\n                except json.JSONDecodeError:\n                    raise RuntimeError(\n                        f'Config annotation {a} on {p.get_name(schema)} '\n                        f'is not valid json'\n                    )\n                attributes[a] = jv\n\n        ptr_card = p.get_cardinality(schema)\n        set_of = ptr_card.is_multi()\n        backend_setting = attributes.get(\n            sn.QualName('cfg', 'backend_setting'), None)\n        required = p.get_required(schema)\n\n        deflt_expr = p.get_default(schema)\n        if deflt_expr is not None:\n            deflt = qlcompiler.evaluate_to_python_val(\n                deflt_expr.text, schema=schema)\n            if set_of and not isinstance(deflt, frozenset):\n                deflt = frozenset((deflt,))\n        else:\n            if set_of:\n                deflt = frozenset()\n            elif backend_setting is None and required:\n                raise RuntimeError(f'cfg::Config.{pn} has no default')\n            else:\n                deflt = None\n\n        if not is_root:\n            pn = f'{cfg_name}::{pn}'\n\n        session_cfg_permissions = attributes.get(\n            sn.QualName('cfg', 'session_cfg_permissions'), None\n        )\n        session_restricted = session_cfg_permissions != '*'\n\n        setting = Setting(\n            pn,\n            type=pytype,\n            schema_type_name=ptype.get_name(schema),\n            set_of=set_of,\n            internal=attributes.get(sn.QualName('cfg', 'internal'), False),\n            system=attributes.get(sn.QualName('cfg', 'system'), False),\n            requires_restart=attributes.get(\n                sn.QualName('cfg', 'requires_restart'), False),\n            backend_setting=backend_setting,\n            report=attributes.get(\n                sn.QualName('cfg', 'report'), None),\n            affects_compilation=attributes.get(\n                sn.QualName('cfg', 'affects_compilation'), False),\n            default=deflt,\n            enum_values=(\n                ptype.get_enum_values(schema)\n                if isinstance(ptype, s_scalars.ScalarType)\n                else None\n            ),\n            required=required,\n            secret=p.get_secret(schema),\n            protected=p.get_protected(schema),\n\n            session_restricted=session_restricted,\n            session_permission=(\n                session_cfg_permissions if session_restricted else None\n            ),\n        )\n\n        settings.append(setting)\n\n    return settings\n"
  },
  {
    "path": "edb/server/config/types.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Any, TYPE_CHECKING, TypeGuard\n\nimport enum\nimport platform\n\nfrom edb import errors\nfrom edb.common import typeutils\nfrom edb.common import typing_inspect\nfrom edb.schema import objects as s_obj\n\nfrom edb.ir import statypes\n\n\nif TYPE_CHECKING:\n    from . import spec\n\n\ndef _issubclass[T_type: type](\n    typ: type | statypes.CompositeTypeSpec, parent: T_type\n) -> TypeGuard[T_type]:\n    return isinstance(typ, type) and issubclass(typ, parent)\n\n\nclass ConfigTypeSpec(statypes.CompositeTypeSpec):\n    def __call__(self, **kwargs) -> CompositeConfigType:\n        return CompositeConfigType(self, **kwargs)\n\n    def from_pyvalue(\n        self, v, *, spec, allow_missing=False\n    ) -> CompositeConfigType:\n        return CompositeConfigType.from_pyvalue(\n            v, tspec=self, spec=spec, allow_missing=allow_missing\n        )\n\n\nclass ConfigType:\n\n    @classmethod\n    def from_pyvalue(cls, v, *, tspec, spec, allow_missing=False):\n        \"\"\"Subclasses override this to allow creation from Python scalars.\"\"\"\n        raise NotImplementedError\n\n    @classmethod\n    def from_json_value(cls, v, *, tspec, spec):\n        raise NotImplementedError\n\n    def to_json_value(self):\n        raise NotImplementedError\n\n    @classmethod\n    def get_edgeql_typeid(cls):\n        raise NotImplementedError\n\n\nclass CompositeConfigType(ConfigType, statypes.CompositeType):\n    _compare_keys: tuple[str, ...]\n\n    def __init__(self, tspec: statypes.CompositeTypeSpec, **kwargs) -> None:\n        object.__setattr__(self, '_tspec', tspec)\n        for f in tspec.fields.values():\n            if f.name in kwargs:\n                object.__setattr__(self, f.name, kwargs[f.name])\n            elif f.default is not statypes.MISSING:\n                object.__setattr__(self, f.name, f.default)\n        object.__setattr__(self, '_compare_keys', tuple(\n            f.name for f in tspec.fields.values() if f.unique\n        ))\n\n    def __setattr__(self, k, v) -> None:\n        raise TypeError(f\"{self._tspec.name} is immutable\")\n\n    def __eq__(self, rhs: Any) -> bool:\n        if (\n            not isinstance(rhs, CompositeConfigType)\n            or self._tspec != rhs._tspec\n        ):\n            return NotImplemented\n        compare_keys = self._compare_keys\n        return (\n            tuple(getattr(self, k) for k in compare_keys)\n            == tuple(getattr(rhs, k) for k in compare_keys)\n        )\n\n    def __hash__(self) -> int:\n        return hash(tuple(getattr(self, k) for k in self._compare_keys))\n\n    def __repr__(self) -> str:\n        body = ', '.join(\n            f'{f.name}={getattr(self, f.name)!r}'\n            for f in self._tspec.fields.values()\n            if hasattr(self, f.name)\n        )\n        return f'{self._tspec.name}({body})'\n\n    @classmethod\n    def from_pyvalue(\n        cls,\n        data,\n        *,\n        tspec: statypes.CompositeTypeSpec,\n        spec: spec.Spec,\n        allow_missing=False,\n    ) -> CompositeConfigType:\n        if allow_missing and data is None:\n            return None  # type: ignore\n\n        if not isinstance(data, dict):\n            raise cls._err(tspec, f'expected a dict value, got {type(data)!r}')\n\n        data = dict(data)\n        tname = data.pop('_tname', None)\n        if tname is not None:\n            tspec = spec.get_type_by_name(tname)\n        assert tspec\n\n        fields = tspec.fields\n\n        items = {}\n        inv_keys = []\n        for fieldname, value in data.items():\n            field = fields.get(fieldname)\n            if field is None:\n                if value is None:\n                    # This may happen when data is produced by\n                    # a polymorphic config query.\n                    pass\n                else:\n                    inv_keys.append(fieldname)\n\n                continue\n\n            f_type = field.type\n\n            if value is None:\n                # Config queries return empty pointer values as None.\n                continue\n\n            if typing_inspect.is_generic_type(f_type):\n                container = typing_inspect.get_origin(f_type)\n                if container not in (frozenset, list):\n                    raise RuntimeError(\n                        f'invalid type annotation on '\n                        f'{tspec.name}.{fieldname}: '\n                        f'{f_type!r} is not supported')\n\n                eltype = typing_inspect.get_args(f_type, evaluate=True)[0]\n                if isinstance(value, eltype):\n                    value = container((value,))\n                elif (typeutils.is_container(value)\n                        and all(isinstance(v, eltype) for v in value)):\n                    value = container(value)\n                else:\n                    raise cls._err(\n                        tspec,\n                        f'invalid {fieldname!r} field value: expecting '\n                        f'{eltype.__name__} or a list thereof, but got '\n                        f'{type(value).__name__}'\n                    )\n\n            elif (isinstance(f_type, ConfigTypeSpec)\n                    and isinstance(value, dict)):\n\n                tname = value.get('_tname', None)\n                if tname is not None:\n                    actual_f_type = spec.get_type_by_name(tname)\n                else:\n                    actual_f_type = f_type\n                    value['_tname'] = f_type.name\n\n                value = cls.from_pyvalue(value, tspec=actual_f_type, spec=spec)\n\n            elif (\n                _issubclass(f_type, statypes.Duration)\n                and isinstance(value, str)\n            ):\n                value = statypes.Duration.from_iso8601(value)\n            elif (\n                _issubclass(f_type, statypes.ConfigMemory)\n                and isinstance(value, str | int)\n            ):\n                value = statypes.ConfigMemory(value)\n            elif (\n                _issubclass(f_type, statypes.EnumScalarType)\n                and isinstance(value, str)\n            ):\n                value = f_type(value)\n\n            elif not isinstance(f_type, type) or not isinstance(value, f_type):\n                raise cls._err(\n                    tspec,\n                    f'invalid {fieldname!r} field value: expecting '\n                    f'{f_type.__name__}, but got {type(value).__name__}'\n                )\n\n            items[fieldname] = value\n\n        if inv_keys:\n            sinv_keys = ', '.join(repr(r) for r in inv_keys)\n            raise cls._err(tspec, f'unknown fields: {sinv_keys}')\n\n        for fieldname, field in fields.items():\n            if fieldname not in items and field.default is statypes.MISSING:\n                if allow_missing:\n                    items[fieldname] = None\n                else:\n                    raise cls._err(\n                        tspec, f'missing required field: {fieldname!r}'\n                    )\n\n        try:\n            return cls(tspec, **items)\n        except (TypeError, ValueError) as ex:\n            raise cls._err(tspec, str(ex))\n\n    @classmethod\n    def get_edgeql_typeid(cls):\n        return s_obj.get_known_type_id('std::json')\n\n    @classmethod\n    def from_json_value(cls, s, *, tspec: statypes.CompositeTypeSpec, spec):\n        return cls.from_pyvalue(s, tspec=tspec, spec=spec)\n\n    def to_json_value(self, redacted: bool = False):\n        dct = {}\n        dct['_tname'] = self._tspec.name\n\n        for f in self._tspec.fields.values():\n            f_type = f.type\n            value = getattr(self, f.name)\n            if redacted and f.secret and value is not None:\n                value = {'redacted': True}\n            elif (isinstance(f_type, statypes.CompositeTypeSpec)\n                    and value is not None):\n                value = value.to_json_value(redacted=redacted)\n            elif typing_inspect.is_generic_type(f_type):\n                value = list(value) if value is not None else []\n            elif (_issubclass(f_type, statypes.ScalarType) and\n                  value is not None):\n                value = value.to_json()\n\n            dct[f.name] = value\n\n        return dct\n\n    @classmethod\n    def _err(\n        cls, tspec: statypes.CompositeTypeSpec, msg: str\n    ) -> errors.ConfigurationError:\n        return errors.ConfigurationError(\n            f'invalid {tspec.name.lower()!r} value: {msg}')\n\n\nclass QueryCacheMode(enum.StrEnum):\n    InMemory = \"InMemory\"\n    RegInline = \"RegInline\"\n    PgFunc = \"PgFunc\"\n    Default = \"Default\"\n\n    @classmethod\n    def effective(cls, value: str | None) -> QueryCacheMode:\n        if value is None:\n            rv = cls.Default\n        else:\n            rv = cls(value)\n        if rv is QueryCacheMode.Default:\n            # Persistent cache disabled for now by default on arm64 linux\n            # because of observed problems in CI test runs.\n            if platform.system() == 'Linux' and platform.machine() == 'arm64':\n                rv = QueryCacheMode.InMemory\n            else:\n                rv = QueryCacheMode.PgFunc\n        return rv\n"
  },
  {
    "path": "edb/server/connpool/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nimport os\nfrom .pool import Pool as Pool1Impl, _NaivePool  # NoQA\nfrom .pool2 import Pool as Pool2Impl\n\n# During the transition period we allow for the pool to be swapped out. The\n# current default is to use the old pool, however this will be switched to use\n# the new pool once we've fully implemented all required features.\nif os.environ.get(\"EDGEDB_USE_NEW_CONNPOOL\", \"\") == \"1\":\n    Pool = Pool2Impl\n    Pool2 = Pool1Impl\nelse:\n    # The two pools have the same effective type shape\n    Pool = Pool1Impl  # type: ignore\n    Pool2 = Pool2Impl  # type: ignore\n\n__all__ = ('Pool', 'Pool2')\n"
  },
  {
    "path": "edb/server/connpool/config.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nimport logging\n\nMIN_CONN_TIME_THRESHOLD = 0.01\nMIN_QUERY_TIME_THRESHOLD = 0.001\nMIN_LOG_TIME_THRESHOLD = 1\nMIN_IDLE_TIME_BEFORE_GC = 120\nCONNECT_FAILURE_RETRIES = 3\nSTATS_COLLECT_INTERVAL = 0.1\n\nlogger = logging.getLogger(\"edb.server\")\n"
  },
  {
    "path": "edb/server/connpool/pool.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nimport typing\n\nimport asyncio\nimport collections\nimport dataclasses\nimport time\n\nfrom . import rolavg\nfrom . import config\nfrom .config import logger\n\nCP1 = typing.TypeVar('CP1', covariant=True)\nCP2 = typing.TypeVar('CP2', contravariant=True)\n\n\nclass Connector(typing.Protocol[CP1]):\n\n    def __call__(self, dbname: str) -> typing.Awaitable[CP1]:\n        pass\n\n\nclass Disconnector(typing.Protocol[CP2]):\n\n    def __call__(self, conn: CP2) -> typing.Awaitable[None]:\n        pass\n\n\nclass StatsCollector(typing.Protocol):\n\n    def __call__(self, stats: Snapshot) -> None:\n        pass\n\n\n@dataclasses.dataclass\nclass BlockSnapshot:\n    dbname: str\n    nwaiters_avg: int\n    nconns: int\n    npending: int\n    nwaiters: int\n    quota: int\n\n\n@dataclasses.dataclass\nclass SnapshotLog:\n    timestamp: float\n    event: str\n    dbname: str\n    value: int\n\n\n@dataclasses.dataclass\nclass Snapshot:\n    timestamp: float\n    capacity: int\n    blocks: list[BlockSnapshot]\n    log: list[SnapshotLog]\n\n    failed_connects: int\n    failed_disconnects: int\n    successful_connects: int\n    successful_disconnects: int\n\n\n@dataclasses.dataclass\nclass ConnectionState:\n    in_use_since: float = 0\n    in_use: bool = False\n    in_stack_since: float = 0\n\n\nclass Block[C]:\n    # A Block holds a number of connections to the same backend database.\n    # A Pool consists of one or more blocks; blocks are the basic unit of\n    # connection pool algorithm, while the pool itself also takes care of\n    # balancing resources between Blocks (because all the blocks share the same\n    # PostgreSQL `max_connections` limit), based on realtime statistics the\n    # block collected and populated.\n    #\n    # Instead of the regular round-robin queue, EdgeDB adopted an LIFO stack\n    # (conn_stack) for the connections - the most recently used connections are\n    # always yielded first. This allows us to run \"garbage collection\" or\n    # \"connection stealing\" to recycle the unused connections from the bottom\n    # of the stack (the least recently used ones), so that other blocks could\n    # reuse the spared resource.\n    #\n    # Block is coroutine-safe. Multiple tasks acquiring connections will be put\n    # in a waiters' queue (conn_waiters), if the demand cannot be fulfilled\n    # immediately without blocking/awaiting. When connections are ready in the\n    # stack, the next task in the queue will be woken up to continue.\n\n    loop: asyncio.AbstractEventLoop\n    dbname: str\n    conns: dict[C, ConnectionState]\n    quota: int\n    pending_conns: int\n    last_connect_timestamp: float\n\n    conn_acquired_num: int\n    conn_waiters_num: int\n    conn_waiters: collections.deque[asyncio.Future[None]]\n    conn_stack: collections.deque[C]\n    connect_failures_num: int\n\n    querytime_avg: rolavg.RollingAverage\n    nwaiters_avg: rolavg.RollingAverage\n    suppressed: bool\n\n    _cached_calibrated_demand: float\n\n    _is_log_batching: bool\n    _last_log_timestamp: float\n    _log_events: dict[str, int]\n\n    def __init__(\n        self,\n        dbname: str,\n        loop: asyncio.AbstractEventLoop,\n    ) -> None:\n        self.dbname = dbname\n        self.conns = {}\n        self.quota = 1\n        self.pending_conns = 0\n        self.last_connect_timestamp = 0\n\n        self.loop = loop\n\n        self.conn_acquired_num = 0\n        self.conn_waiters_num = 0\n        self.conn_waiters = collections.deque()\n        self.conn_stack = collections.deque()\n        self.connect_failures_num = 0\n\n        self.querytime_avg = rolavg.RollingAverage(history_size=20)\n        self.nwaiters_avg = rolavg.RollingAverage(history_size=3)\n        self.suppressed = False\n\n        self._is_log_batching = False\n        self._last_log_timestamp = 0\n        self._log_events = {}\n\n    def count_conns(self) -> int:\n        # The total number of connections in this block, including:\n        #  - Future connections that are still pending in connecting\n        #  - Idle connections in the stack\n        #  - Acquired connections (not in the stack)\n        return len(self.conns) + self.pending_conns\n\n    def count_waiters(self) -> int:\n        # The number of tasks that are blocked in acquire()\n        return self.conn_waiters_num\n\n    def count_queued_conns(self) -> int:\n        # Number of connections in the stack/pool\n        return len(self.conn_stack)\n\n    def count_pending_conns(self) -> int:\n        # Number of future connections that are still pending in connecting\n        return self.pending_conns\n\n    def count_conns_over_quota(self) -> int:\n        # How many connections over the quota\n        return max(self.count_conns() - self.quota, 0)\n\n    def count_approx_available_conns(self) -> int:\n        # It's approximate because there might be a race when a connection\n        # is being returned to the pool but not yet acquired by a waiter,\n        # in which case the number isn't going to be accurate.\n        return max(\n            self.count_conns() -\n            self.conn_acquired_num -\n            self.conn_waiters_num,\n            0\n        )\n\n    def inc_acquire_counter(self) -> None:\n        self.conn_acquired_num += 1\n\n    def dec_acquire_counter(self) -> None:\n        self.conn_acquired_num -= 1\n\n    def try_steal(\n        self, only_older_than: typing.Optional[float] = None\n    ) -> typing.Optional[C]:\n        # Try to take one unused connection from the block without blocking.\n        # If only_older_than is provided, only the connection that was put in\n        # the stack before the given timestamp is returned. None will be\n        # returned if we cannot find such connection in this block.\n\n        if not self.conn_stack:\n            return None\n\n        if only_older_than is not None:\n            # We only need to check the bottom of the stack - higher items in\n            # the stack only have larger timestamps\n            oldest_conn = self.conn_stack[0]\n            if self.conns[oldest_conn].in_stack_since > only_older_than:\n                return None\n\n        return self.conn_stack.popleft()\n\n    async def try_acquire(self, *, attempts: int = 1) -> typing.Optional[C]:\n        self.conn_waiters_num += 1\n        try:\n            # Skip the waiters' queue if we can grab a connection from the\n            # stack immediately - this is not completely fair, but it's\n            # extremely hard to always take the shortcut and starve the queue\n            # without blocking the main loop, so we are fine here. (This is\n            # also how asyncio.Queue is implemented.)\n            if not self.conn_stack:\n                waiter = self.loop.create_future()\n\n                if attempts > 1:\n                    # If the waiter was woken up only to discover that\n                    # it needs to wait again, we don't want it to lose\n                    # its place in the waiters queue.\n                    self.conn_waiters.appendleft(waiter)\n                else:\n                    # On the first attempt the waiter goes to the end\n                    # of the waiters queue.\n                    self.conn_waiters.append(waiter)\n\n                try:\n                    await waiter\n                except Exception:\n                    if not waiter.done():\n                        waiter.cancel()\n                    try:\n                        self.conn_waiters.remove(waiter)\n                    except ValueError:\n                        # The waiter could be removed from self.conn_waiters\n                        # by a previous release() call.\n                        pass\n                    if self.conn_stack and not waiter.cancelled():\n                        # We were woken up by release(), but can't take\n                        # the call.  Wake up the next in line.\n                        self._wakeup_next_waiter()\n                    raise\n\n            # There can be a race between a waiter scheduled for to wake up\n            # and a connection being stolen (due to quota being enforced,\n            # for example).  In which case the waiter might get finally\n            # woken up with an empty queue -- hence the 'try'.\n            # acquire will put a while loop around this\n\n            # Yield the most recently used connection from the top of the stack\n            if self.conn_stack:\n                return self.conn_stack.pop()\n            else:\n                return None\n        finally:\n            self.conn_waiters_num -= 1\n\n    async def acquire(self) -> C:\n        attempts = 1\n        while (c := await self.try_acquire(attempts=attempts)) is None:\n            attempts += 1\n        return c\n\n    def release(self, conn: C) -> None:\n        # Put the connection (back) to the top of the stack,\n        self.conn_stack.append(conn)\n        # refresh the timestamp,\n        self.conns[conn].in_stack_since = time.monotonic()\n        # and call the queue.\n        self._wakeup_next_waiter()\n\n    def abort_waiters(self, e: Exception) -> None:\n        # Propagate the given exception to all tasks that are waiting in\n        # acquire() - this usually means the underlying connect() is failing\n        while self.conn_waiters:\n            waiter = self.conn_waiters.popleft()\n            if not waiter.done():\n                waiter.set_exception(e)\n\n    def _wakeup_next_waiter(self) -> None:\n        while self.conn_waiters:\n            waiter = self.conn_waiters.popleft()\n            if not waiter.done():\n                waiter.set_result(None)\n                break\n\n    def log_connection(self, event: str, timestamp: float = 0) -> None:\n        if not timestamp:\n            timestamp = time.monotonic()\n\n        # Add to the backlog if we're in batching, regardless of the time\n        if self._is_log_batching:\n            self._log_events[event] = self._log_events.setdefault(event, 0) + 1\n\n        # Time check only if we're not in batching\n        elif timestamp - self._last_log_timestamp > \\\n            config.MIN_LOG_TIME_THRESHOLD:\n            logger.info(\n                \"Connection %s to backend database: %s\", event, self.dbname\n            )\n            self._last_log_timestamp = timestamp\n\n        # Start batching if logging is too frequent, add timer only once here\n        else:\n            self._is_log_batching = True\n            self._log_events = {event: 1}\n            self.loop.call_later(\n                config.MIN_LOG_TIME_THRESHOLD, self._log_batched_conns,\n            )\n\n    def _log_batched_conns(self) -> None:\n        logger.info(\n            \"Backend connections to database %s: %s \"\n            \"in at least the last %.1f seconds.\",\n            self.dbname,\n            ', '.join(\n                f'{num} were {event}'\n                for event, num in self._log_events.items()\n            ),\n            config.MIN_LOG_TIME_THRESHOLD,\n        )\n        self._is_log_batching = False\n        self._last_log_timestamp = time.monotonic()\n\n\nclass BasePool[C]:\n\n    _connect_cb: Connector[C]\n    _disconnect_cb: Disconnector[C]\n    _stats_cb: typing.Optional[StatsCollector]\n\n    _max_capacity: int  # total number of connections allowed in the pool\n    _cur_capacity: int  # counter of all connections (with pending) in the pool\n\n    _loop: typing.Optional[asyncio.AbstractEventLoop]\n    _current_snapshot: typing.Optional[Snapshot]\n\n    _blocks: collections.OrderedDict[str, Block[C]]\n    # Mapping from dbname to the Block instances, also used as a queue in a\n    # starving situation when the blocks are fed with connections in a round-\n    # robin fashion, see also Pool._tick().\n\n    _is_starving: bool\n    # Indicates if any block is starving for connections, this usually means\n    # the number of active blocks is greater than the pool max capacity.\n\n    _failed_connects: int\n    _failed_disconnects: int\n    _successful_connects: int\n    _successful_disconnects: int\n\n    _conntime_avg: rolavg.RollingAverage\n\n    def __init__(\n        self,\n        *,\n        connect: Connector[C],\n        disconnect: Disconnector[C],\n        max_capacity: int,\n        stats_collector: typing.Optional[StatsCollector]=None,\n    ) -> None:\n        self._connect_cb = connect\n        self._disconnect_cb = disconnect\n        self._stats_cb = stats_collector\n\n        self._max_capacity = max_capacity\n        self._cur_capacity = 0\n\n        self._loop = None\n        self._current_snapshot = None\n\n        self._blocks = collections.OrderedDict()\n        self._is_starving = False\n        self._running = True\n\n        self._failed_connects = 0\n        self._failed_disconnects = 0\n        self._successful_connects = 0\n        self._successful_disconnects = 0\n\n        self._conntime_avg = rolavg.RollingAverage(history_size=10)\n\n    async def close(self) -> None:\n        self._running = False\n\n    @property\n    def max_capacity(self) -> int:\n        return self._max_capacity\n\n    @property\n    def current_capacity(self) -> int:\n        return self._cur_capacity\n\n    @property\n    def failed_connects(self) -> int:\n        return self._failed_connects\n\n    @property\n    def failed_disconnects(self) -> int:\n        return self._failed_disconnects\n\n    @property\n    def active_conns(self) -> int:\n        return self.current_capacity - self._get_pending_conns()\n\n    def _get_pending_conns(self) -> int:\n        return sum(\n            block.count_pending_conns() for block in self._blocks.values()\n        )\n\n    def _get_loop(self) -> asyncio.AbstractEventLoop:\n        if self._loop is None:\n            self._loop = asyncio.get_running_loop()\n        return self._loop\n\n    def _build_snapshot(self, *, now: float) -> Snapshot:\n        bstats: list[BlockSnapshot] = []\n        for block in self._blocks.values():\n            bstats.append(\n                BlockSnapshot(\n                    dbname=block.dbname,\n                    nwaiters_avg=round(block.nwaiters_avg.avg()),\n                    nconns=len(block.conns),\n                    npending=block.count_pending_conns(),\n                    nwaiters=block.count_waiters(),\n                    quota=block.quota,\n                )\n            )\n\n        bstats.sort(key=lambda b: b.dbname)\n\n        return Snapshot(\n            timestamp=now,\n            blocks=bstats,\n            capacity=self._cur_capacity,\n            log=[],\n\n            failed_connects=self._failed_connects,\n            failed_disconnects=self._failed_disconnects,\n            successful_connects=self._successful_connects,\n            successful_disconnects=self._successful_disconnects,\n        )\n\n    def _capture_snapshot(self, *, now: float) -> None:\n        if self._stats_cb is None:\n            return None\n\n        assert self._current_snapshot is None\n        self._current_snapshot = self._build_snapshot(now=now)\n\n    def _report_snapshot(self) -> None:\n        if self._stats_cb is None:\n            return\n        assert self._current_snapshot is not None\n        self._stats_cb(self._current_snapshot)\n        self._current_snapshot = None\n\n    def _log_to_snapshot(\n        self,\n        *,\n        dbname: str,\n        event: str,\n        value: int=0,\n        now: float=0,\n    ) -> None:\n        if self._stats_cb is None:\n            return\n        if now == 0:\n            now = time.monotonic()\n        assert self._current_snapshot is not None\n        self._current_snapshot.log.append(\n            SnapshotLog(\n                timestamp=now,\n                dbname=dbname,\n                event=event,\n                value=value,\n            )\n        )\n\n    def _new_block(self, dbname: str) -> Block[C]:\n        assert dbname not in self._blocks\n        block: Block[C] = Block(dbname, self._get_loop())\n        self._blocks[dbname] = block\n        block.quota = 1\n        if self._is_starving:\n            self._blocks.move_to_end(dbname, last=False)\n        return block\n\n    def _drop_block(self, block: Block[C]) -> None:\n        assert not block.count_waiters()\n        assert not block.count_conns()\n        assert not block.quota\n        self._blocks.pop(block.dbname)\n\n    def _get_block(self, dbname: str) -> Block[C]:\n        block = self._blocks.get(dbname)\n        if block is None:\n            block = self._new_block(dbname)\n        return block\n\n    async def _connect(\n        self, block: Block[C], started_at: float, event: str\n    ) -> None:\n        logger.debug(\n            \"Establishing new connection to backend database: %s\", block.dbname\n        )\n        try:\n            conn = await self._connect_cb(block.dbname)\n        except Exception as e:\n            self._failed_connects += 1\n            self._cur_capacity -= 1\n\n            logger.error(\n                \"Failed to establish a new connection to backend database: %s\",\n                block.dbname,\n                exc_info=self._running,\n            )\n            block.connect_failures_num += 1\n\n            if getattr(e, 'fields', {}).get('C') == '3D000':\n                # 3D000 - INVALID CATALOG NAME, database does not exist\n                # Skip retry and propagate the error immediately\n                if block.connect_failures_num <= config.CONNECT_FAILURE_RETRIES:\n                    block.connect_failures_num = (\n                        config.CONNECT_FAILURE_RETRIES + 1)\n\n            if block.connect_failures_num > config.CONNECT_FAILURE_RETRIES:\n                # Abort all waiters on this block and propagate the error, as\n                # we don't have a mapping between waiters and _connect() tasks\n                block.abort_waiters(e)\n            else:\n                # We must retry immediately here (without sleeping), or _tick()\n                # will jump in and schedule more retries than what we expected.\n                self._schedule_new_conn(block, event)\n            return\n        else:\n            # reset the failure counter if we got the connection back\n            block.connect_failures_num = 0\n        finally:\n            ended_at = time.monotonic()\n            self._conntime_avg.add(ended_at - started_at)\n            block.pending_conns -= 1\n        self._successful_connects += 1\n        block.conns[conn] = ConnectionState()\n        block.last_connect_timestamp = ended_at\n\n        # Release the connection to block waiters.\n        block.release(conn)\n        block.log_connection(event, ended_at)\n\n    async def _disconnect(self, conn: C, block: Block[C]) -> None:\n        logger.debug(\n            \"Discarding a connection to backend database: %s\", block.dbname\n        )\n        try:\n            await self._disconnect_cb(conn)\n        except Exception:\n            self._failed_disconnects += 1\n            raise\n        else:\n            self._successful_disconnects += 1\n        finally:\n            self._cur_capacity -= 1\n\n    async def _transfer(\n        self,\n        from_block: Block[C],\n        from_conn: C,\n        to_block: Block[C],\n        started_at: float,\n    ) -> None:\n        self._log_to_snapshot(dbname=from_block.dbname, event='transfer-from')\n        await self._disconnect(from_conn, from_block)\n        from_block.log_connection('transferred out')\n        self._cur_capacity += 1\n        await self._connect(to_block, started_at, 'transferred in')\n\n    def _schedule_transfer(\n        self,\n        from_block: Block[C],\n        from_conn: C,\n        to_block: Block[C],\n    ) -> None:\n        started_at = time.monotonic()\n        assert not from_block.conns[from_conn].in_use\n        from_block.conns.pop(from_conn)\n        to_block.pending_conns += 1\n        if self._is_starving:\n            self._blocks.move_to_end(to_block.dbname, last=True)\n            self._blocks.move_to_end(from_block.dbname, last=True)\n        self._get_loop().create_task(\n            self._transfer(from_block, from_conn, to_block, started_at))\n\n    def _schedule_new_conn(\n        self, block: Block[C], event: str = 'established'\n    ) -> None:\n        started_at = time.monotonic()\n        self._cur_capacity += 1\n        block.pending_conns += 1\n        if self._is_starving:\n            self._blocks.move_to_end(block.dbname, last=True)\n        self._log_to_snapshot(\n            dbname=block.dbname, event='connect', value=block.count_conns())\n        self._get_loop().create_task(self._connect(block, started_at, event))\n\n    def _schedule_discard(self, block: Block[C], conn: C) -> None:\n        self._get_loop().create_task(self._discard_conn(block, conn))\n\n    async def _discard_conn(self, block: Block[C], conn: C) -> None:\n        assert not block.conns[conn].in_use\n        block.conns.pop(conn)\n        self._log_to_snapshot(\n            dbname=block.dbname, event='disconnect', value=block.count_conns())\n        await self._disconnect(conn, block)\n        block.log_connection(\"discarded\")\n\n\nclass Pool[C](BasePool[C]):\n    # The backend database connection pool implementation in EdgeDB, managing\n    # connections to multiple databases of a single PostgreSQL cluster,\n    # optimized for quality of service (QoS) so that connection acquisitions\n    # and distribution are automatically balanced in a relatively fair way.\n    # Connections to the same database are managed in a Block (see above).\n    #\n    # Conceptually, the Pool has 4 runtime modes (separately optimized):\n    #   Mode A: managing connections to only one database\n    #   Mode B: multiple databases, below max capacity\n    #   Mode C: reached max capacity, some tasks are waiting for connections\n    #   Mode D: some blocks are starving with zero connection\n    #\n    # Mode A is close to a regular connection pool - new connections are only\n    # created when there are not enough spare ones in the pool, and used\n    # connections are released back to the pool, cached for next acquisition\n    # (unless being idle for too long and GC will recycle them). As a\n    # simplified mode, there is usually a shortcut to return early for Mode A\n    # in the same code base shared with other modes.\n    #\n    # Mode B is simply an extension of Mode A for multiple databases. Each\n    # block in Mode B acts just like Mode A, with minimal difference like less\n    # aggressive connection creation. Different blocks could freely create new\n    # connections when needed, racing with each other organically by the demand\n    # for Postgres connections.\n    #\n    # Mode C is when things get complicated. Without being able to create more\n    # connections, pending connection requests can only be satisfied by either\n    # a released connection from the same block, or the pool as the arbiter has\n    # to \"transfer\" a connection from another block. This is achieved by\n    # rebalancing the pool based on calculated per-block quotas recalibrated\n    # in periodic \"ticks\" (see _tick()).\n    #\n    # In extreme cases, the number of blocks may go beyond the max capacity.\n    # This is Mode D when even each block takes only at most one connection,\n    # there are still some starved blocks that have no connections at all.\n    # Mode D reuses the framework of Mode C but runs separate logic in a\n    # different if-else branch. In short, the pool reallocates the limited\n    # total number of connections to different blocks in a round-robin fashion.\n\n    _new_blocks_waitlist: collections.OrderedDict[Block[C], bool]\n    _blocks_over_quota: list[Block[C]]\n    _nacquires: int\n    _htick: typing.Optional[asyncio.Handle]\n    _to_drop: list[Block[C]]\n    _gc_interval: float  # minimum seconds between GC runs\n    _gc_requests: int  # number of GC requests\n\n    def __init__(\n        self,\n        *,\n        connect: Connector[C],\n        disconnect: Disconnector[C],\n        max_capacity: int,\n        stats_collector: typing.Optional[StatsCollector]=None,\n        min_idle_time_before_gc: float = config.MIN_IDLE_TIME_BEFORE_GC,\n    ) -> None:\n        super().__init__(\n            connect=connect,\n            disconnect=disconnect,\n            stats_collector=stats_collector,\n            max_capacity=max_capacity,\n        )\n\n        self._new_blocks_waitlist = collections.OrderedDict()\n\n        self._blocks_over_quota = []\n\n        self._nacquires = 0\n        self._htick = None\n        self._first_tick = True\n        self._to_drop = []\n        self._gc_interval = min_idle_time_before_gc\n        self._gc_requests = 0\n\n    def _maybe_schedule_tick(self) -> None:\n        if self._first_tick:\n            self._first_tick = False\n            self._capture_snapshot(now=time.monotonic())\n\n        # Only schedule a tick under Mode C/D, and schedule at most one tick\n        # at a time.\n        if not self._nacquires or self._htick is not None:\n            return\n\n        self._htick = self._get_loop().call_later(\n            max(self._conntime_avg.avg(), config.MIN_CONN_TIME_THRESHOLD),\n            self._tick\n        )\n\n    def _tick(self) -> None:\n        self._htick = None\n        if self._nacquires:\n            # Schedule the next tick if we're still in Mode C/D.\n            self._maybe_schedule_tick()\n\n        now = time.monotonic()\n\n        self._report_snapshot()\n        self._capture_snapshot(now=now)\n\n        # If we're managing connections to only one PostgreSQL DB (Mode A),\n        # bail out early. Just give the one and only block we have the max\n        # possible quota (which is needed only for logging purposes.)\n        nblocks = len(self._blocks)\n        if nblocks <= 1:\n            self._is_starving = False\n            if nblocks:\n                first_block = next(iter(self._blocks.values()))\n                first_block.quota = self._max_capacity\n                first_block.nwaiters_avg.add(first_block.count_waiters())\n            return\n\n        # Go over all the blocks and calculate:\n        #  - \"nwaiters\" - number of connection acquisitions\n        #    (including pending and acquired, per block and total)\n        #  - First round of per-block quota ( := nwaiters )\n        #  - Calibrated demand (per block and total)\n        #  - If any block is starving / Mode D\n        need_conns_at_least = 0\n        total_nwaiters = 0\n        total_calibrated_demand: float = 0\n        min_demand = float('inf')\n        self._to_drop.clear()\n        for block in self._blocks.values():\n            nwaiters = block.count_waiters() + block.conn_acquired_num\n            block.quota = nwaiters  # will likely be overwritten below\n            total_nwaiters += nwaiters\n            block.nwaiters_avg.add(nwaiters)\n            nwaiters_avg = block.nwaiters_avg.avg()\n            if nwaiters_avg and not block.suppressed:\n                # GOTCHA: this is a counter of blocks that need at least 1\n                # connection. If this number is greater than _max_capacity,\n                # some block will be starving with zero connection.\n                need_conns_at_least += 1\n            else:\n                if not block.count_conns():\n                    self._to_drop.append(block)\n                    continue\n\n            demand = (\n                max(nwaiters_avg, nwaiters) *\n                max(block.querytime_avg.avg(), config.MIN_QUERY_TIME_THRESHOLD)\n            )\n            total_calibrated_demand += demand\n            block._cached_calibrated_demand = demand\n            if min_demand > demand:\n                min_demand = demand\n\n        was_starving = self._is_starving\n        self._is_starving = need_conns_at_least >= self._max_capacity\n        if self._to_drop:\n            for block in self._to_drop:\n                self._drop_block(block)\n\n        if not total_nwaiters:\n            # No connection acquisition, nothing to do here.\n            return\n\n        if total_nwaiters < self._max_capacity:\n            # The total demand for connections is lower than our max capacity,\n            # we could bail out early.\n\n            if self._cur_capacity >= self._max_capacity:\n                # GOTCHA: this is still Mode C, because the total_nwaiters\n                # number doesn't include the unused connections in the stacks\n                # if any. Therefore, the rebalance here is necessary to shrink\n                # those blocks and transfer the connection quota to the\n                # starving ones (or they will block). We could simply depend on\n                # the already-set quota based on nwaiters, and skip the regular\n                # Mode C quota calculation below.\n                self._maybe_rebalance()\n\n            else:\n                # If we still have space for more connections (Mode B), don't\n                # actively rebalance the pool just yet - rebalance will kick in\n                # when the max capacity is hit; or we'll depend on the garbage\n                # collection to shrink the over-quota blocks.\n                pass\n\n            return\n\n        if self._is_starving:\n            # Mode D: recalculate the per-block quota.\n\n            for block in tuple(self._blocks.values()):\n                nconns = block.count_conns()\n                if nconns == 1:\n                    if (\n                        now - block.last_connect_timestamp <\n                            max(self._conntime_avg.avg(),\n                                config.MIN_CONN_TIME_THRESHOLD)\n                    ):\n                        # let it keep its connection\n                        block.quota = 1\n                    else:\n                        block.quota = 0\n                        self._blocks.move_to_end(block.dbname, last=True)\n                elif nconns > 1:\n                    block.quota = 0\n                    self._blocks.move_to_end(block.dbname, last=True)\n                else:\n                    block.quota = 1\n                    self._blocks.move_to_end(block.dbname, last=True)\n\n                if block.quota:\n                    self._log_to_snapshot(\n                        dbname=block.dbname, event='set-quota',\n                        value=block.quota)\n                else:\n                    self._log_to_snapshot(\n                        dbname=block.dbname, event='reset-quota')\n\n            if not was_starving and self._new_blocks_waitlist:\n                # Mode D assumes all connections are already in use or to be\n                # used, depending on their `release()` to schedule transfers.\n                # When just entering Mode D, there can be a special case when\n                # no further `release()` will be called because all acquired\n                # connections were returned to the pool before `_tick()` got a\n                # chance to set `self._is_starving`, while some other blocks\n                # are literally starving to death (blocked forever).\n                #\n                # This branch handles this particular case, by stealing\n                # connections from the idle blocks and try to free them into\n                # the starving blocks.\n\n                for block in list(self._blocks.values()):\n                    while self._should_free_conn(block):\n                        if (conn := block.try_steal()) is None:\n                            # no more from this block\n                            break\n\n                        elif not self._maybe_free_into_starving_blocks(\n                            block, conn\n                        ):\n                            # put back the last stolen connection if we\n                            # don't need to steal anymore\n                            self._release_unused(block, conn)\n                            return\n\n        else:\n            # Mode C: distribute the total connections by calibrated demand\n            # setting the per-block quota, then trigger rebalance.\n\n            capacity_left = self._max_capacity\n            if min_demand / total_calibrated_demand * self._max_capacity < 1:\n                for block in self._blocks.values():\n                    demand = block._cached_calibrated_demand\n                    if not demand:\n                        block.quota = 0\n                        self._log_to_snapshot(\n                            dbname=block.dbname, event='reset-quota')\n\n                    k = (self._max_capacity * demand) / total_calibrated_demand\n                    if 0 < k <= 1:\n                        block.quota = 1\n                        self._log_to_snapshot(\n                            dbname=block.dbname, event='set-quota',\n                            value=block.quota)\n                        capacity_left -= 1\n\n            assert capacity_left > 0\n\n            acc: float = 0\n            for block in self._blocks.values():\n                demand = block._cached_calibrated_demand\n                if not demand:\n                    continue\n\n                old_acc = acc\n                acc += (\n                    (capacity_left * demand) / total_calibrated_demand\n                )\n                block.quota = round(acc) - round(old_acc)\n\n                self._log_to_snapshot(\n                    dbname=block.dbname, event='set-quota', value=block.quota)\n\n            self._maybe_rebalance()\n\n    def _maybe_rebalance(self) -> None:\n        if self._is_starving:\n            return\n\n        self._blocks_over_quota.clear()\n\n        for block in self._blocks.values():\n            nconns = block.count_conns()\n            quota = block.quota\n            if nconns > quota:\n                self._try_shrink_block(block)\n                if block.count_conns() > quota:\n                    # If the block is still over quota, add it to a list so\n                    # that other blocks could steal connections from it\n                    self._blocks_over_quota.append(block)\n            elif nconns < quota:\n                while (\n                    block.count_conns() < quota and\n                    self._cur_capacity < self._max_capacity\n                ):\n                    self._schedule_new_conn(block)\n\n        if self._blocks_over_quota:\n            self._blocks_over_quota.sort(\n                key=lambda b: b.count_conns_over_quota(),\n                reverse=True\n            )\n\n    def _should_free_conn(self, from_block: Block[C]) -> bool:\n        # First, if we only manage one connection to one PostgreSQL DB --\n        # we don't need to bother with rebalancing the pool. So we bail out.\n        if len(self._blocks) <= 1:\n            return False\n\n        from_block_size = from_block.count_conns()\n\n        # Second, we bail out if:\n        #\n        # * the pool isn't starving, i.e. we have a fewer number of\n        #   different DB connections than the max number of connections\n        #   allowed;\n        #\n        # AND\n        #\n        # * the `from_block` block has fewer connections than its quota.\n        if not self._is_starving and from_block_size <= from_block.quota:\n            return False\n\n        # Third, we bail out if:\n        #\n        # * the pool is starving;\n        #\n        # AND YET\n        #\n        # * the `from_block` block has only one connection;\n        #\n        # AND\n        #\n        # * the block has active waiters in its queue;\n        #\n        # AND\n        #\n        # * the block has been holding its last and only connection for\n        #   less time than the average time it spends on connecting to\n        #   PostgreSQL.\n        if (\n            self._is_starving and\n            from_block_size == 1 and\n            from_block.count_waiters() and\n            (time.monotonic() - from_block.last_connect_timestamp) <\n                max(self._conntime_avg.avg(), config.MIN_CONN_TIME_THRESHOLD)\n        ):\n            return False\n\n        return True\n\n    def _maybe_free_into_starving_blocks(\n        self,\n        from_block: Block[C],\n        conn: C,\n    ) -> bool:\n        label, to_block = self._find_most_starving_block()\n        if to_block is None or to_block is from_block:\n            return False\n        assert label is not None\n\n        self._schedule_transfer(from_block, conn, to_block)\n\n        self._log_to_snapshot(\n            dbname=to_block.dbname,\n            event=label,\n            value=1,\n        )\n\n        return True\n\n    def _try_shrink_block(self, block: Block[C]) -> None:\n        while (\n            block.count_conns_over_quota() and\n            self._should_free_conn(block)\n        ):\n            if (conn := block.try_steal()) is not None:\n                _, to_block = self._find_most_starving_block()\n                if to_block is not None:\n                    self._schedule_transfer(block, conn, to_block)\n                else:\n                    self._schedule_discard(block, conn)\n            else:\n                break\n\n    def _try_steal_conn(self, for_block: Block[C]) -> bool:\n        if not self._blocks_over_quota:\n            return False\n        for block in self._blocks_over_quota:\n            if block is for_block or not self._should_free_conn(block):\n                continue\n            if (conn := block.try_steal()) is not None:\n                self._log_to_snapshot(\n                    dbname=block.dbname, event='conn-stolen')\n                self._schedule_transfer(block, conn, for_block)\n                return True\n        return False\n\n    def _find_most_starving_block(\n        self,\n    ) -> tuple[typing.Optional[str], typing.Optional[Block[C]]]:\n        to_block = None\n\n        # Find if there are any newly created blocks waiting for their\n        # first connection.\n        while self._new_blocks_waitlist:\n            block, _ = self._new_blocks_waitlist.popitem(last=False)\n            if block.count_conns() or not block.count_waiters():\n                # This block is already initialized. Skip it.\n                # This branch shouldn't happen.\n                continue\n            to_block = block\n            break\n\n        if to_block is not None:\n            return 'first-conn', to_block\n\n        # Find if there are blocks without a single connection.\n        # Find the one that is starving the most.\n        max_need = 0\n        for block in self._blocks.values():\n            block_size = block.count_conns()\n            block_demand = block.count_waiters()\n\n            if block_size or not block_demand or block.suppressed:\n                continue\n\n            if block_demand > max_need:\n                max_need = block_demand\n                to_block = block\n\n        if to_block is not None:\n            return 'revive-conn', to_block\n\n        # Find all blocks that are under quota and award the most\n        # starving one.\n        max_need = 0\n        for block in self._blocks.values():\n            block_size = block.count_conns()\n            block_quota = block.quota\n            if block_quota > block_size and not block.suppressed:\n                need = block_quota - block_size\n                if need > max_need:\n                    max_need = need\n                    to_block = block\n\n        if to_block:\n            return 'redist-conn', to_block\n\n        return None, None\n\n    async def _acquire(self, dbname: str) -> C:\n        block = self._get_block(dbname)\n        block.suppressed = False\n\n        room_for_new_conns = self._cur_capacity < self._max_capacity\n        block_nconns = block.count_conns()\n\n        if room_for_new_conns:\n            # First, schedule new connections if needed.\n            if len(self._blocks) == 1:\n                # Managing connections to only one DB and can open more\n                # connections.  Or this is before the first tick.\n                if block.count_queued_conns() <= 1:\n                    # Only keep at most 1 spare connection in the ready queue.\n                    # When concurrent tasks are racing for the spare\n                    # connections in the same loop iteration, early requesters\n                    # will retrieve the spare connections immediately without\n                    # context switch (block.acquire() will not \"block\" in\n                    # await). Therefore, we will create just enough new\n                    # connections for the number of late requesters plus one.\n                    self._schedule_new_conn(block)\n            elif (\n                not block_nconns or\n                block_nconns < block.quota or\n                not block.count_approx_available_conns()\n            ):\n                # Block has no connections at all, or not enough connections.\n                self._schedule_new_conn(block)\n\n            return await block.acquire()\n\n        if not block_nconns:\n            # This is a block without any connections.\n            # Request one of the next released connections to be\n            # reallocated for this block.\n            if not self._try_steal_conn(block):\n                self._new_blocks_waitlist[block] = True\n            return await block.acquire()\n\n        if block_nconns < block.quota:\n            # Let's see if we can steal a connection from some block\n            # that's over quota and open a new one.\n            self._try_steal_conn(block)\n            return await block.acquire()\n\n        return await block.acquire()\n\n    def _run_gc(self) -> None:\n        loop = self._get_loop()\n\n        if self._is_starving:\n            # Bail out early if any block is starving, try GC later\n            loop.call_later(self._gc_interval, self._run_gc)\n            return\n\n        if self._gc_requests > 1:\n            # Schedule to run one more GC for requests before this run\n            self._gc_requests = 1\n            loop.call_later(self._gc_interval, self._run_gc)\n\n        else:\n            # We will take care of the only GC request and pause GC\n            self._gc_requests = 0\n\n        # Make sure the unused connections stay in the pool for at least one\n        # GC interval. So theoretically unused connections are usually GC-ed\n        # within 1-2 GC intervals.\n        only_older_than = time.monotonic() - self._gc_interval\n        for block in self._blocks.values():\n            while (conn := block.try_steal(only_older_than)) is not None:\n                self._schedule_discard(block, conn)\n\n    async def acquire(self, dbname: str) -> C:\n        self._nacquires += 1\n        self._maybe_schedule_tick()\n        try:\n            conn = await self._acquire(dbname)\n        finally:\n            self._nacquires -= 1\n\n        block = self._blocks[dbname]\n        assert not block.conns[conn].in_use\n        block.inc_acquire_counter()\n        block.conns[conn].in_use = True\n        block.conns[conn].in_use_since = time.monotonic()\n\n        return conn\n\n    def release(self, dbname: str, conn: C, *, discard: bool = False) -> None:\n        try:\n            block = self._blocks[dbname]\n        except KeyError:\n            raise RuntimeError(\n                f'cannot release connection {conn!r}: {dbname!r} database '\n                f'is not known to the pool'\n            ) from None\n\n        try:\n            conn_state = block.conns[conn]\n        except KeyError:\n            raise RuntimeError(\n                f'cannot release connection {conn!r}: the connection does not '\n                f'belong to the pool'\n            ) from None\n\n        if not conn_state.in_use:\n            raise RuntimeError(\n                f'cannot release connection {conn!r}: the connection was '\n                f'never acquired from the pool'\n            ) from None\n\n        block.dec_acquire_counter()\n        block.querytime_avg.add(time.monotonic() - conn_state.in_use_since)\n        conn_state.in_use = False\n        conn_state.in_use_since = 0\n\n        self._maybe_schedule_tick()\n\n        if not (\n            self._should_free_conn(block)\n            and self._maybe_free_into_starving_blocks(block, conn)\n        ):\n            if discard:\n                # Concurrent `acquire()` may be waiting to reuse the released\n                # connection here - as we should discard this one, let's just\n                # schedule a new one in the same block.\n                self._schedule_discard(block, conn)\n                self._schedule_new_conn(block)\n            else:\n                self._release_unused(block, conn)\n\n    def _release_unused(self, block: Block[C], conn: C) -> None:\n        block.release(conn)\n\n        # Only request for GC if the connection is released unused\n        self._gc_requests += 1\n        if self._gc_requests == 1:\n            # Only schedule GC for the very first request - following\n            # requests will be grouped into the next GC\n            self._get_loop().call_later(self._gc_interval, self._run_gc)\n\n    async def prune_inactive_connections(self, dbname: str) -> None:\n        try:\n            block = self._blocks[dbname]\n        except KeyError:\n            return None\n\n        # Mark the block as suppressed, so that nothing will be\n        # transferred to it. It will be unsuppressed if anything\n        # actually tries to connect.\n        # TODO: Is it possible to safely drop the block?\n        block.suppressed = True\n\n        conns = []\n        while (conn := block.try_steal()) is not None:\n            conns.append(conn)\n\n        while not block.count_waiters() and block.pending_conns:\n            # try_acquire, because it can get stolen\n            if c := await block.try_acquire():\n                conns.append(c)\n\n        if conns:\n            await asyncio.gather(\n                *(self._discard_conn(block, conn) for conn in conns),\n                return_exceptions=True\n            )\n\n    async def prune_all_connections(self) -> None:\n        # Brutally close all connections. This is used by HA failover.\n        coros = []\n        for block in self._blocks.values():\n            block.conn_stack.clear()\n            for conn in block.conns:\n                coros.append(self._disconnect(conn, block))\n            block.conns.clear()\n            self._log_to_snapshot(\n                dbname=block.dbname, event='disconnect', value=0)\n        await asyncio.gather(*coros, return_exceptions=True)\n        # We don't have to worry about pending_conns here -\n        # Tenant._pg_connect() will honor the failover and raise an error.\n\n    def iterate_connections(self) -> typing.Iterator[C]:\n        for block in self._blocks.values():\n            for conn in block.conns:\n                yield conn\n\n\nclass _NaivePool[C](BasePool[C]):\n    \"\"\"Implements a rather naive and flawed balancing algorithm.\n\n    Should only be used for for testing purposes.\n    \"\"\"\n\n    _conns: dict[str, set[C]]\n    _last_tick: float\n\n    def __init__(\n        self,\n        connect: Connector[C],\n        disconnect: Disconnector[C],\n        max_capacity: int,\n        stats_collector: typing.Optional[StatsCollector]=None,\n        min_idle_time_before_gc: float = config.MIN_IDLE_TIME_BEFORE_GC,\n    ) -> None:\n        super().__init__(\n            connect=connect,\n            disconnect=disconnect,\n            stats_collector=stats_collector,\n            max_capacity=max_capacity,\n        )\n        self._conns = {}\n        self._last_tick = 0\n\n    def _maybe_tick(self) -> None:\n        now = time.monotonic()\n\n        if self._last_tick == 0:\n            # First time `_tick()` is run.\n            self._capture_snapshot(now=now)\n            self._last_tick = now\n            return\n\n        if now - self._last_tick < 0.1:\n            # Not enough time passed since the last tick.\n            return\n\n        self._last_tick = now\n\n        self._report_snapshot()\n        self._capture_snapshot(now=now)\n\n    async def _steal_conn(self, for_block: Block[C]) -> None:\n        # A simplified connection stealing implementation.\n        # First, tries to steal one from the blocks queue unconditionally.\n        for block in self._blocks.values():\n            if block is for_block:\n                continue\n            if (conn := block.try_steal()) is not None:\n                self._log_to_snapshot(\n                    dbname=block.dbname, event='conn-stolen')\n                self._schedule_transfer(block, conn, for_block)\n                self._blocks.move_to_end(block.dbname, last=True)\n                return\n        # If all the blocks are busy, simply wait in the queue to get one.\n        for block in self._blocks.values():\n            if block is for_block:\n                continue\n            if block.count_conns():\n                conn = await block.acquire()\n                self._log_to_snapshot(\n                    dbname=block.dbname, event='conn-stolen')\n                self._schedule_transfer(block, conn, for_block)\n                self._blocks.move_to_end(block.dbname, last=True)\n                return\n\n    async def acquire(self, dbname: str) -> C:\n        self._maybe_tick()\n\n        block = self._get_block(dbname)\n\n        if self._cur_capacity < self._max_capacity:\n            self._schedule_new_conn(block)\n        elif not block.count_conns():\n            # As a new block, steal one connection from other blocks if the\n            # max capacity is reached. We cannot depend on the transfer logic\n            # in `release()`, because it would hang if no other block releases.\n            await self._steal_conn(block)\n\n        return await block.acquire()\n\n    def release(self, dbname: str, conn: C) -> None:\n        self._maybe_tick()\n        this_block = self._get_block(dbname)\n\n        if this_block.count_conns() < this_block.count_waiters():\n            this_block.release(conn)\n            return\n\n        max_need = 0\n        to_block = None\n        for block in self._blocks.values():\n            block_size = block.count_conns()\n            block_demand = block.count_waiters()\n\n            if not block_size and block_demand:\n                need = block_demand * 1000\n            elif block_size < block_demand:\n                need = block_demand - block_size\n            else:\n                continue\n\n            if need > max_need:\n                max_need = block_demand\n                to_block = block\n\n        if to_block is this_block or to_block is None:\n            this_block.release(conn)\n            return\n\n        self._schedule_transfer(this_block, conn, to_block)\n\n        self._log_to_snapshot(\n            dbname=to_block.dbname,\n            event='free',\n            value=1,\n        )\n"
  },
  {
    "path": "edb/server/connpool/pool2.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nimport edb.server._rust_native._conn_pool as _rust\nimport asyncio\nimport time\nimport typing\nimport dataclasses\nimport pickle\n\nfrom . import config\nfrom .config import logger\nfrom edb.server import rust_async_channel\n\nCP1 = typing.TypeVar('CP1', covariant=True)\nCP2 = typing.TypeVar('CP2', contravariant=True)\n\n\nclass Connector(typing.Protocol[CP1]):\n\n    def __call__(self, dbname: str) -> typing.Awaitable[CP1]:\n        pass\n\n\nclass Disconnector(typing.Protocol[CP2]):\n\n    def __call__(self, conn: CP2) -> typing.Awaitable[None]:\n        pass\n\n\n@dataclasses.dataclass\nclass BlockSnapshot:\n    dbname: str\n    nwaiters_avg: int\n    nconns: int\n    npending: int\n    nwaiters: int\n    quota: int\n\n\n@dataclasses.dataclass\nclass SnapshotLog:\n    timestamp: float\n    event: str\n    dbname: str\n    value: int\n\n\n@dataclasses.dataclass\nclass Snapshot:\n    timestamp: float\n    capacity: int\n    blocks: list[BlockSnapshot]\n    log: list[SnapshotLog]\n\n    failed_connects: int\n    failed_disconnects: int\n    successful_connects: int\n    successful_disconnects: int\n\n\nclass StatsCollector(typing.Protocol):\n\n    def __call__(self, stats: Snapshot) -> None:\n        pass\n\n\n# Connections must be hashable because we use them to reverse-lookup\n# an internal ID.\nclass Pool[C: typing.Hashable]:\n    _pool: _rust.ConnPool\n    _next_conn_id: int\n    _failed_connects: int\n    _failed_disconnects: int\n    _successful_connects: int\n    _successful_disconnects: int\n    _cur_capacity: int\n    _max_capacity: int\n    _task: typing.Optional[asyncio.Task[None]]\n    _acquires: dict[int, asyncio.Future[int]]\n    _prunes: dict[int, asyncio.Future[None]]\n    _conns: dict[int, C]\n    _errors: dict[int, BaseException]\n    _conns_held: dict[C, int]\n    _loop: asyncio.AbstractEventLoop\n    _counts: typing.Any\n    _stats_collector: typing.Optional[StatsCollector]\n\n    def __init__(\n        self,\n        *,\n        connect: Connector[C],\n        disconnect: Disconnector[C],\n        max_capacity: int,\n        stats_collector: typing.Optional[StatsCollector] = None,\n        min_idle_time_before_gc: float = config.MIN_IDLE_TIME_BEFORE_GC,\n    ) -> None:\n        # Re-load the logger if it's been mocked for testing\n        global logger\n        logger = config.logger\n\n        logger.info(\n            f'Creating a connection pool with max_capacity={max_capacity}'\n        )\n        self._connect = connect\n        self._disconnect = disconnect\n        self._pool = _rust.ConnPool(\n            max_capacity, min_idle_time_before_gc, config.STATS_COLLECT_INTERVAL\n        )\n        self._max_capacity = max_capacity\n        self._cur_capacity = 0\n        self._next_conn_id = 0\n        self._acquires = {}\n        self._conns = {}\n        self._errors = {}\n        self._conns_held = {}\n        self._prunes = {}\n\n        self._loop = asyncio.get_running_loop()\n        self._channel = rust_async_channel.RustAsyncChannel(\n            self._pool._channel,\n            self._process_message,\n        )\n\n        self._task = self._loop.create_task(self._boot(self._channel))\n\n        self._failed_connects = 0\n        self._failed_disconnects = 0\n        self._successful_connects = 0\n        self._successful_disconnects = 0\n\n        self._counts = None\n        self._stats_collector = stats_collector\n        if stats_collector:\n            stats_collector(self._build_snapshot(now=time.monotonic()))\n\n        pass\n\n    def __del__(self) -> None:\n        if self._task:\n            self._task.cancel()\n            self._task = None\n\n    async def close(self) -> None:\n        if self._task:\n            # Cancel the currently-executing futures\n            for acq in self._acquires.values():\n                acq.set_exception(asyncio.CancelledError())\n            for prune in self._prunes.values():\n                prune.set_exception(asyncio.CancelledError())\n            logger.info(\"Closing connection pool...\")\n            task = self._task\n            self._task = None\n            task.cancel()\n            try:\n                await task\n            except asyncio.exceptions.CancelledError:\n                pass\n            self._pool = None\n            logger.info(\"Closed connection pool\")\n\n    async def _boot(\n        self,\n        channel: rust_async_channel.RustAsyncChannel,\n    ) -> None:\n        logger.info(\"Python-side connection pool booted\")\n        try:\n            await channel.run()\n        finally:\n            channel.close()\n\n    def _try_read(self) -> None:\n        if self._channel:\n            self._channel.read_hint()\n\n    def _process_message(self, msg: typing.Any) -> None:\n        # If we're closing, don't dispatch any operations\n        if not self._task:\n            return\n        if msg[0] == 0:\n            if f := self._acquires.pop(msg[1], None):\n                f.set_result(msg[2])\n            else:\n                logger.warning(f\"Duplicate result for acquire {msg[1]}\")\n        elif msg[0] == 1:\n            self._loop.create_task(self._perform_connect(msg[1], msg[2]))\n        elif msg[0] == 2:\n            self._loop.create_task(self._perform_disconnect(msg[1]))\n        elif msg[0] == 3:\n            self._loop.create_task(self._perform_reconnect(msg[1], msg[2]))\n        elif msg[0] == 4:\n            self._loop.create_task(self._perform_prune(msg[1]))\n        elif msg[0] == 5:\n            # Note that we might end up with duplicated messages at shutdown\n            error = self._errors.pop(msg[2], None)\n            if error:\n                if f := self._acquires.pop(msg[1], None):\n                    f.set_exception(error)\n                else:\n                    logger.warning(f\"Duplicate exception for acquire {msg[1]}\")\n        elif msg[0] == 6:\n            # Pickled metrics\n            self._counts = pickle.loads(msg[1])\n            if self._stats_collector:\n                self._stats_collector(\n                    self._build_snapshot(now=time.monotonic())\n                )\n        else:\n            logger.critical(f'Unexpected message: {msg}')\n\n    async def _perform_connect(self, id: int, db: str) -> None:\n        self._cur_capacity += 1\n        try:\n            self._conns[id] = await self._connect(db)\n            self._successful_connects += 1\n            if self._pool:\n                self._pool._completed(id)\n        except Exception as e:\n            self._errors[id] = e\n            if self._pool:\n                self._pool._failed(id, e)\n\n    async def _perform_disconnect(self, id: int) -> None:\n        try:\n            conn = self._conns.pop(id)\n            await self._disconnect(conn)\n            self._successful_disconnects += 1\n            self._cur_capacity -= 1\n            if self._pool:\n                self._pool._completed(id)\n        except Exception as e:\n            self._cur_capacity -= 1\n            if self._pool:\n                self._pool._failed(id, e)\n\n    async def _perform_reconnect(self, id: int, db: str) -> None:\n        try:\n            # Note that we cannot hold this connection here as there is an\n            # implicit expectation that the connection will GC after disconnect\n            # but before reconnect.\n            conn = self._conns.pop(id)\n            await self._disconnect(conn)\n            self._successful_disconnects += 1\n            try:\n                self._conns[id] = await self._connect(db)\n                self._successful_connects += 1\n                if self._pool:\n                    self._pool._completed(id)\n            except Exception as e:\n                self._errors[id] = e\n                if self._pool:\n                    self._pool._failed(id, e)\n\n        except Exception as e:\n            del self._conns[id]\n            self._cur_capacity -= 1\n            if self._pool:\n                self._pool._failed(id, e)\n\n    async def _perform_prune(self, id: int) -> None:\n        self._prunes[id].set_result(None)\n\n    async def acquire(self, dbname: str) -> C:\n        \"\"\"Acquire a connection from the database. This connection must be\n        released.\"\"\"\n        if not self._task:\n            raise asyncio.CancelledError()\n        for i in range(config.CONNECT_FAILURE_RETRIES + 1):\n            id = self._next_conn_id\n            self._next_conn_id += 1\n            acquire: asyncio.Future[int] = asyncio.Future()\n            self._acquires[id] = acquire\n            self._pool._acquire(id, dbname)\n            self._try_read()\n            # This may throw!\n            try:\n                conn = await acquire\n                c = self._conns[conn]\n                self._conns_held[c] = id\n                return c\n            except Exception as e:\n                # 3D000 - INVALID CATALOG NAME, database does not exist\n                # Skip retry and propagate the error immediately\n                if getattr(e, 'fields', {}).get('C') == '3D000':\n                    raise\n\n                # Allow the final exception to escape\n                if i == config.CONNECT_FAILURE_RETRIES:\n                    logger.exception(\n                        'Failed to acquire connection, will not '\n                        f'retry {dbname} ({self._cur_capacity}'\n                        'active)'\n                    )\n                    raise\n                logger.exception(\n                    'Failed to acquire connection, will retry: '\n                    f'{dbname} ({self._cur_capacity} active)'\n                )\n        raise AssertionError(\"Unreachable end of loop\")\n\n    def release(self, dbname: str, conn: C, discard: bool = False) -> None:\n        \"\"\"Releases a connection back into the pool, discarding or returning it\n        in the background.\"\"\"\n        id = self._conns_held.pop(conn)\n        if discard:\n            self._pool._discard(id)\n        else:\n            self._pool._release(id)\n        self._try_read()\n\n    async def prune_inactive_connections(self, dbname: str) -> None:\n        if not self._task:\n            raise asyncio.CancelledError()\n        id = self._next_conn_id\n        self._next_conn_id += 1\n        self._prunes[id] = asyncio.Future()\n        self._pool._prune(id, dbname)\n        await self._prunes[id]\n        del self._prunes[id]\n\n    async def prune_all_connections(self) -> None:\n        # Brutally close all connections. This is used by HA failover.\n        coros = []\n        for conn in self._conns.values():\n            coros.append(self._disconnect(conn))\n        await asyncio.gather(*coros, return_exceptions=True)\n\n    @property\n    def active_conns(self) -> int:\n        return len(self._conns_held)\n\n    def iterate_connections(self) -> typing.Iterator[C]:\n        for conn in self._conns.values():\n            yield conn\n\n    def _build_snapshot(self, *, now: float) -> Snapshot:\n        blocks: list[BlockSnapshot] = []\n        if self._counts:\n            block_stats = self._counts['blocks']\n            for dbname, stats in block_stats.items():\n                v = stats['value']\n                block_snapshot = BlockSnapshot(\n                    dbname=dbname,\n                    nconns=v[_rust.METRIC_ACTIVE],\n                    nwaiters_avg=v[_rust.METRIC_WAITING],\n                    npending=v[_rust.METRIC_CONNECTING]\n                    + v[_rust.METRIC_RECONNECTING],\n                    nwaiters=v[_rust.METRIC_WAITING],\n                    quota=stats['target'],\n                )\n                blocks.append(block_snapshot)\n            pass\n\n        return Snapshot(\n            timestamp=now,\n            blocks=blocks,\n            capacity=self._cur_capacity,\n            log=[],\n            failed_connects=self._failed_connects,\n            failed_disconnects=self._failed_disconnects,\n            successful_connects=self._successful_connects,\n            successful_disconnects=self._successful_disconnects,\n        )\n\n    @property\n    def max_capacity(self) -> int:\n        return self._max_capacity\n\n    @property\n    def current_capacity(self) -> int:\n        return self._cur_capacity\n\n    @property\n    def failed_connects(self) -> int:\n        return self._failed_connects\n\n    @property\n    def failed_disconnects(self) -> int:\n        return self._failed_disconnects\n"
  },
  {
    "path": "edb/server/connpool/rolavg.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nclass RollingAverage:\n\n    __slots__ = ('_hist_size', '_hist', '_pos', '_cached_avg')\n\n    _hist_size: int\n    _pos: int\n    _hist: list[float]\n    _cached_avg: float\n\n    def __init__(self, *, history_size: int):\n        self._hist = [0] * history_size\n        self._pos = 0\n        self._hist_size = history_size\n        self._cached_avg = 0\n\n    def add(self, n: float) -> None:\n        self._hist[self._pos % self._hist_size] = n\n        self._pos += 1\n        self._cached_avg = 0\n\n    def avg(self) -> float:\n        if self._cached_avg:\n            return self._cached_avg\n\n        self._cached_avg = (\n            sum(self._hist) / max(min(self._pos, self._hist_size), 1)\n        )\n\n        return self._cached_avg\n"
  },
  {
    "path": "edb/server/consul.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright EdgeDB Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Optional\n\nimport asyncio\nimport json\nimport logging\nimport random\nimport urllib.parse\n\nimport httptools\n\nfrom edb.common import asyncwatcher\n\n\nlogger = logging.getLogger(\"edb.server.consul\")\n\n\nclass ConsulKVProtocol(asyncwatcher.AsyncWatcherProtocol):\n    def __init__(\n        self,\n        watcher: asyncwatcher.AsyncWatcher,\n        consul_host: str,\n        key: str,\n    ) -> None:\n        assert not key.startswith(\"/\"), \"absolute path rewrites Consul KV URL\"\n        super().__init__(watcher)\n        self._host = consul_host\n        self._key = key\n        self._watcher = watcher\n        self._parser = httptools.HttpResponseParser(self)\n        self._last_modify_index: Optional[str] = None\n        self._buffers: list[bytes] = []\n\n    def data_received(self, data: bytes) -> None:\n        self._parser.feed_data(data)\n\n    def on_status(self, status: bytes) -> None:\n        status_code = self._parser.get_status_code()\n        if status_code != 200:\n            logger.debug(\n                \"Consul is returning non-200 responses: %s %r\",\n                status_code,\n                status,\n            )\n            if self._transport is not None:\n                self._transport.close()\n\n    def on_body(self, body: bytes) -> None:\n        self._buffers.append(body)\n\n    def on_message_complete(self) -> None:\n        try:\n            code = self._parser.get_status_code()\n            if code == 200:\n                self._watcher.incr_metrics_counter(\"watch-update\")\n                payload = json.loads(b\"\".join(self._buffers))[0]\n                last_modify_index = payload[\"ModifyIndex\"]\n                self._watcher.on_update(payload[\"Value\"])\n                if self._last_modify_index == last_modify_index:\n                    self._watcher.incr_metrics_counter(\"watch-timeout\")\n                    self._last_modify_index = None\n                else:\n                    self._last_modify_index = last_modify_index\n            else:\n                self._watcher.incr_metrics_counter(f\"watch-err-{code}\")\n            self.request()\n\n        finally:\n            self._buffers.clear()\n\n    def request(self) -> None:\n        delay = self._watcher.consume_tokens(1)\n        if delay > 0:\n            asyncio.get_running_loop().call_later(\n                delay + random.random() * 0.1, self.request\n            )\n            return\n        uri = urllib.parse.urljoin(\"/v1/kv/\", self._key)\n        if self._last_modify_index is not None:\n            uri += f\"?index={self._last_modify_index}\"\n        if self._transport is None or self._transport.is_closing():\n            logger.error(\"cannot perform Consul request: connection is closed\")\n            return\n        self._transport.write(\n            f\"GET {uri} HTTP/1.1\\r\\n\"\n            f\"Host: {self._host}\\r\\n\"\n            f\"\\r\\n\".encode()\n        )\n\n    def close(self) -> None:\n        if self._transport is not None and not self._transport.is_closing():\n            self._transport.close()\n            self._transport = None\n"
  },
  {
    "path": "edb/server/daemon/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2012-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom .pidfile import PidFile  # NOQA\nfrom .exceptions import DaemonError  # NOQA\nfrom .daemon import DaemonContext  # NOQA\n"
  },
  {
    "path": "edb/server/daemon/daemon.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2012-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Implementation of PEP 3143.\"\"\"\n\nfrom __future__ import annotations\nfrom typing import Optional\n\nimport atexit\nimport io\nimport os\nimport signal\n\nfrom . import lib, pidfile as pidfile_module\nfrom .exceptions import DaemonError\n\n\nclass DaemonContext:\n    def __init__(\n        self,\n        *,\n        pidfile: Optional[os.PathLike] = None,\n        files_preserve: Optional[list] = None,\n        working_directory: str = '/',\n        umask: int = 0o022,\n        uid: Optional[int] = None,\n        gid: Optional[int] = None,\n        detach_process: Optional[bool] = None,\n        prevent_core: bool = True,\n        stdin: Optional[io.FileIO] = None,\n        stdout: Optional[io.FileIO] = None,\n        stderr: Optional[io.FileIO] = None,\n        signal_map: Optional[dict] = None\n    ):\n\n        self.pidfile = os.fspath(pidfile) if pidfile is not None else None\n        self.files_preserve = files_preserve\n        self.working_directory = working_directory\n        self.umask = umask\n        self.prevent_core = prevent_core\n        self.signal_map = signal_map\n\n        if stdin is not None and not isinstance(stdin, str):\n            lib.validate_stream(stdin, stream_name='stdin')\n        self.stdin = stdin\n\n        if stdout is not None and not isinstance(stdout, str):\n            lib.validate_stream(stdout, stream_name='stdout')\n        self.stdout = stdout\n\n        if stderr is not None and not isinstance(stderr, str):\n            lib.validate_stream(stderr, stream_name='stderr')\n        self.stderr = stderr\n\n        self.uid = uid\n        self.gid = gid\n\n        if detach_process is None:\n            self.detach_process = lib.is_detach_process_context_required()\n        else:\n            self.detach_process = detach_process\n\n        self._is_open = False\n        self._close_stdin = self._close_stdout = self._close_stderr = None\n        self._stdin_name = self._stdout_name = self._stderr_name = None\n        self._pidfile = None\n\n    is_open = property(lambda self: self._is_open)\n\n    def open(self):\n        if self._is_open:\n            return\n\n        self._init_pidfile()\n\n        if self.prevent_core:\n            lib.prevent_core_dump()\n\n        lib.change_umask(self.umask)\n        lib.change_working_directory(self.working_directory)\n\n        # Test that we can write to log files/output right after\n        # chdir call\n        self._test_sys_streams()\n\n        if self.uid is not None:\n            lib.change_process_uid(self.uid)\n\n        if self.gid is not None:\n            lib.change_process_gid(self.gid)\n\n        if self.detach_process:\n            lib.detach_process_context()\n\n        self._setup_signals()\n\n        if self._pidfile is not None:\n            self._pidfile.acquire()\n\n        self._close_all_open_files()\n        self._open_sys_streams()\n\n        self._is_open = True\n        atexit.register(self.close)\n\n    def close(self):\n        if not self._is_open:\n            return\n\n        atexit.unregister(self.close)\n\n        if self._pidfile is not None:\n            self._pidfile.release()\n            self._pidfile = None\n\n        self._close_sys_streams()\n\n        self._is_open = False\n\n    def _close_sys_streams(self):\n        if self._close_stdin:\n            self._close_stdin.close()\n            self._close_stdin = None\n        self.stdin = None\n\n        if self._close_stdout:\n            self._close_stdout.close()\n            self._close_stdout = None\n        self.stdout = None\n\n        if self._close_stderr:\n            self._close_stderr.close()\n            self._close_stderr = None\n        self.stderr = None\n\n    def _test_sys_streams(self):\n        stderr = self.stderr or self._stderr_name\n        if isinstance(stderr, str):\n            open(stderr, 'at').close()\n\n        stdout = self.stdout or self._stdout_name\n        if isinstance(stdout, str):\n            open(stdout, 'at').close()\n\n    def _open_sys_streams(self):\n        stdin = self.stdin or self._stdin_name\n        if isinstance(stdin, str):\n            self._stdin_name = stdin\n            self._close_stdin = stdin = open(stdin, 'rt')\n        else:\n            self._stdin_name = getattr(stdin, 'name', None)\n        lib.redirect_stream('stdin', stdin)\n\n        stderr = self.stderr or self._stderr_name\n        if isinstance(stderr, str):\n            self._stderr_name = stderr\n            self._close_stderr = stderr = open(stderr, 'at')\n        else:\n            self._stderr_name = getattr(stderr, 'name', None)\n        lib.redirect_stream('stderr', stderr)\n\n        stdout = self.stdout or self._stdout_name\n        if isinstance(stdout, str):\n            self._stdout_name = stdout\n            self._close_stdout = stdout = open(stdout, 'at')\n        else:\n            self._stdout_name = getattr(stdout, 'name', None)\n        lib.redirect_stream('stdout', stdout)\n\n    def signal_reopen_sys_streams(self, signal_number, stack_frame):\n        self._close_sys_streams()\n        self._open_sys_streams()\n\n    def signal_terminate(self, signal_number, stack_frame):\n        raise SystemExit('Termination on signal {}'.format(signal_number))\n\n    def __enter__(self):\n        self.open()\n        return self\n\n    def __exit__(self, exc_type, exc, exc_tb):\n        self.close()\n\n    def _close_all_open_files(self):\n        excl = set()\n\n        if self.files_preserve:\n            excl.update(self.files_preserve)\n\n        if self.stderr and not isinstance(self.stderr, str):\n            excl.add(self.stderr.fileno())\n\n        if self.stdin and not isinstance(self.stdin, str):\n            excl.add(self.stdin.fileno())\n\n        if self.stdout and not isinstance(self.stdout, str):\n            excl.add(self.stdout.fileno())\n\n        if self._pidfile is not None:\n            pidfile = self._pidfile.fileno\n            if pidfile is not None:\n                excl.add(pidfile)\n\n        lib.close_all_open_files(excl)\n\n    def _setup_signals(self):\n        signal_map = {\n            'SIGTSTP': None,\n            'SIGTTIN': None,\n            'SIGTTOU': None,\n            'SIGTERM': 'signal_terminate',\n            'SIGHUP': 'signal_reopen_sys_streams'\n        }\n\n        if self.signal_map:\n            signal_map.update(self.signal_map)\n\n        for name, handler in signal_map.items():\n            if isinstance(name, str):\n                try:\n                    num = getattr(signal, name)\n                except AttributeError:\n                    raise DaemonError('Invalid signal name {!r}'.format(name))\n            elif isinstance(name, int):\n                if name < 1 or name >= signal.NSIG:\n                    raise DaemonError(\n                        'Invalid signal number {!r}'.format(name))\n                num = name\n            else:\n                raise DaemonError(\n                    'Invalid signal {!r}, str or int expected'.format(name))\n\n            if handler is None:\n                signal.signal(num, signal.SIG_IGN)\n            elif isinstance(handler, str):\n                try:\n                    handler = getattr(self, handler)\n                except AttributeError:\n                    raise DaemonError(\n                        'Invalid signal {!r} handler name {!r}'.format(\n                            name, handler))\n                signal.signal(num, handler)\n            else:\n                if not callable(handler):\n                    raise DaemonError(\n                        'Excpected callable signal {!r} handler: {!r}'.format(\n                            name, handler))\n                signal.signal(num, handler)\n\n    def _init_pidfile(self):\n        if self.pidfile is None:\n            return\n\n        if isinstance(self.pidfile, str):\n            self._pidfile = pidfile_module.PidFile(self.pidfile)\n        else:\n            if isinstance(self.pidfile, pidfile_module.PidFile):\n                if self.pidfile.locked:\n                    raise DaemonError(\n                        'Pidfile object is already locked; '\n                        'unable to initialize daemon context')\n                self._pidfile = self.pidfile\n            else:\n                raise DaemonError(\n                    'Invalid pidfile, str of PidFile expected, got {!r}'.\n                    format(self.pidfile))\n"
  },
  {
    "path": "edb/server/daemon/exceptions.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2012-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom edb import errors\n\n\nclass DaemonError(errors.InternalServerError):\n    pass\n"
  },
  {
    "path": "edb/server/daemon/lib.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2012-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Optional\n\nimport errno\nimport io\nimport os\nimport fcntl\nimport logging\nimport resource\nimport stat\nimport socket\nimport sys\n\nfrom .exceptions import DaemonError\n\nlogger = logging.getLogger('edb.server.daemon')\n\n\ndef is_process_running(pid: int):\n    \"\"\"Check if there is a running process with `pid`.\"\"\"\n    try:\n        os.kill(pid, 0)\n        return True\n    except OSError as ex:\n        if ex.errno == errno.ESRCH:\n            return False\n        else:\n            raise\n\n\ndef lock_file(fileno: int):\n    \"\"\"Lock file.  Returns ``True`` if succeeded, ``False`` otherwise.\"\"\"\n    try:\n        # Try to lock file exclusively and in non-blocking fashion\n        fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB)\n    except IOError:\n        return False\n    else:\n        return True\n\n\ndef make_readonly(path: str):\n    \"\"\"Make a file read-only.\"\"\"\n    assert os.path.isfile(path)\n    os.chmod(path, stat.S_IROTH | stat.S_IRUSR | stat.S_IRGRP)\n\n\ndef change_working_directory(path: str):\n    \"\"\"Change the working directory for this process.\"\"\"\n    try:\n        os.chdir(path)\n    except OSError as ex:\n        raise DaemonError(\n            'Unable to change working directory to {!r}'.format(path)) from ex\n\n\ndef change_process_gid(gid: int):\n    \"\"\"Change the GID of this process.\n\n    Requires appropriate OS privileges for this process.\n    \"\"\"\n    try:\n        os.setgid(gid)\n    except OSError as ex:\n        raise DaemonError(\n            'Unable to change the owning GID to {!r}'.format(gid)) from ex\n\n\ndef change_process_uid(uid: int):\n    \"\"\"Change the UID of this process.\n\n    Requires appropriate OS privileges for this process.\n    \"\"\"\n    try:\n        os.setuid(uid)\n    except OSError as ex:\n        raise DaemonError(\n            'Unable to change the owning UID to {!r}'.format(uid)) from ex\n\n\ndef change_umask(mask: int):\n    \"\"\"Change process umask.\"\"\"\n    try:\n        os.umask(mask)\n    except (OSError, OverflowError) as ex:\n        raise DaemonError('Unable to set process umask to {:#o}'.format(\n            mask)) from ex\n\n\ndef prevent_core_dump():\n    \"\"\"Prevent this process from generating a core dump.\"\"\"\n    core_resource = resource.RLIMIT_CORE\n\n    try:\n        resource.getrlimit(core_resource)\n    except ValueError as ex:\n        raise DaemonError(\n            'Unable to limit core dump size: '\n            'system does not support RLIMIT_CORE resource limit') from ex\n\n    # Set hard & soft limits to 0, i.e. no core dump at all\n    resource.setrlimit(core_resource, (0, 0))\n\n\ndef detach_process_context():\n    \"\"\"Datach process context.\n\n    Does it in three steps:\n\n    1. Forks and exists parent process.\n    This detaches us from shell, and since the child will have a new\n    PID but will inherit the Group PID from parent, the new process\n    will not be a group leader.\n\n    2. Call 'setsid' to create a new session.\n    This makes the process a session leader of a new session, process\n    becomes the process group leader of a new process group and it\n    doesn't have a controlling terminal.\n\n    3. Form and exit parent again.\n    This guarantees that the daemon is not a session leader, which\n    prevents it from acquiring a controlling terminal.\n\n    Reference: “Advanced Programming in the Unix Environment”,\n    section 13.3, by W. Richard Stevens.\n    \"\"\"\n    def fork_and_exit_parent(error_message):\n        try:\n            if os.fork() > 0:\n                # Don't need to call 'sys.exit', as we don't want to\n                # run any python interpreter clean-up handlers\n                os._exit(0)\n        except OSError as ex:\n            raise DaemonError(\n                '{}: [{}] {}'.format(error_message, ex.errno,\n                                     ex.strerror)) from ex\n\n    fork_and_exit_parent(error_message='Failed the first fork')\n    os.setsid()\n    fork_and_exit_parent(error_message='Failed the second fork')\n\n\ndef is_process_started_by_init():\n    \"\"\"Determine if the current process is started by 'init'.\"\"\"\n    # The 'init' process has its PID set to 1.\n    return os.getppid() == 1\n\n\ndef is_socket(fd):\n    \"\"\"Determine if the file descriptor is a socket.\"\"\"\n    file_socket = socket.fromfd(fd, socket.AF_INET, socket.SOCK_RAW)\n\n    try:\n        file_socket.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE)\n    except socket.error as ex:\n        return ex.args[0] != errno.ENOTSOCK\n    else:\n        return True\n\n\ndef is_process_started_by_superserver():\n    \"\"\"Determine if the current process is started by the superserver.\"\"\"\n    # The internet superserver creates a network socket, and\n    # attaches it to the standard streams of the child process.\n\n    try:\n        fileno = sys.__stdin__.fileno()\n    except Exception:\n        return False\n    else:\n        return is_socket(fileno)\n\n\ndef is_detach_process_context_required():\n    \"\"\"Determine whether detaching process context is required.\n\n    Returns ``True`` if:\n        - Process was started by `init`; or\n        - Process was started by `inetd`.\n    \"\"\"\n    return not is_process_started_by_init(\n    ) and not is_process_started_by_superserver()\n\n\ndef get_max_fileno(default: int=2048):\n    \"\"\"Return the maximum number of open file descriptors.\"\"\"\n    limit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]\n    if limit == resource.RLIM_INFINITY:\n        return default\n    return limit\n\n\ndef try_close_fileno(fileno: int):\n    \"\"\"Try to close fileno.\"\"\"\n    try:\n        os.close(fileno)\n    except OSError as ex:\n        if ex.errno != errno.EBADF:\n            raise DaemonError(\n                'Failed to close file descriptor {}'.format(fileno))\n\n\ndef close_all_open_files(exclude: Optional[set] = None):\n    \"\"\"Close all open file descriptors.\"\"\"\n    maxfd = get_max_fileno()\n    if exclude:\n        for fd in reversed(range(maxfd)):\n            if fd not in exclude:\n                try_close_fileno(fd)\n    else:\n        for fd in reversed(range(maxfd)):\n            try_close_fileno(fd)\n\n\ndef redirect_stream(stream_name: str, target_stream: io.FileIO):\n    \"\"\"Redirect a system stream to the specified file.\n\n    If ``target_stream`` is None - redirect to devnull.\n    \"\"\"\n    if target_stream is None:\n        target_fd = os.open(os.devnull, os.O_RDWR)\n    else:\n        target_fd = target_stream.fileno()\n\n    system_stream = getattr(sys, stream_name)\n    os.dup2(target_fd, system_stream.fileno())\n    setattr(sys, '__{}__'.format(stream_name), system_stream)\n\n\ndef validate_stream(stream, *, stream_name):\n    \"\"\"Check if `stream` is an open io.IOBase instance.\"\"\"\n    if not isinstance(stream, io.IOBase):\n        raise DaemonError(\n            'Invalid {} stream object, an instance of io.IOBase is expected'.\n            format(stream_name))\n\n    if stream.closed:\n        raise DaemonError('Stream {} is already closed'.format(stream_name))\n"
  },
  {
    "path": "edb/server/daemon/pidfile.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2012-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport os\nimport errno\n\nfrom .exceptions import DaemonError\nfrom . import lib\n\n\nclass PidFile:\n    def __init__(self, path, *, pid=None, data=None):\n        self._path = path\n        self._data = data\n        self._pid = pid\n        self._file = None\n        self._locked = False\n\n    locked = property(lambda self: self._locked)\n\n    def _prepare_file_content(self):\n        buf = ''\n        if self._pid is None:\n            buf += str(os.getpid())\n        else:\n            buf += str(self._pid)\n        if self._data:\n            buf += '\\n\\n{}'.format(self._data)\n        return buf\n\n    def acquire(self):\n        if self.locked:\n            # No point in allowing re-entrance\n            raise DaemonError('pid file is already acquired')\n\n        path = self._path\n        pidfile_dir = os.path.dirname(path)\n        if not os.path.isdir(pidfile_dir):\n            raise DaemonError(\n                f\"cannot create pid file: {pidfile_dir} \"\n                f\"does not exist or is not a directory\"\n            )\n\n        if os.path.exists(path):\n            if self.is_locked(path):\n                raise DaemonError(\n                    'pid file {!r} exists and belongs to a '\n                    'running process'.format(path))\n            os.unlink(path)\n\n        self._file = open(path, 'wt')\n\n        fileno = self._file.fileno()\n        if not lib.lock_file(fileno):\n            raise DaemonError('pid file {!r} already locked'.format(path))\n\n        self._file.write(self._prepare_file_content())\n        self._file.flush()\n\n        lib.make_readonly(path)\n\n        self._locked = True\n\n    def fileno(self):\n        if self._locked:\n            return self._file.fileno()\n\n    def release(self):\n        if not self.locked:\n            raise DaemonError('pid file is already released')\n\n        if self._file:\n            self._file.close()\n            self._file = None\n\n        if os.path.exists(self._path):\n            os.remove(self._path)\n\n        self._locked = False\n\n    def __enter__(self):\n        self.acquire()\n        return self\n\n    def __exit__(self, exc_type, exc, exc_tb):\n        self.release()\n\n    @classmethod\n    def is_locked(cls, path):\n        if os.path.exists(path):\n            # If pid file already exists - check if it belongs to a\n            # running process.  If not - it should be safe to remove it.\n            try:\n                with open(path, 'rt') as f:\n                    pid = int(f.readline())\n                    if lib.is_process_running(pid):\n                        return True\n            except OSError as er:\n                if er.errno == errno.ENOENT:\n                    # ENOENT - No such file or directory\n                    # Race - file did exist when we checked if it exists, but\n                    # got deleted before 'with open' was executed\n                    return False\n                raise\n        return False\n\n    @classmethod\n    def read(cls, path):\n        with open(path, 'rt') as f:\n            pid = int(f.readline())\n            f.readline()\n            data = f.read()\n\n        return pid, (data or None)\n"
  },
  {
    "path": "edb/server/dbview/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom .dbview import DatabaseIndex, Database, DatabaseConnectionView\n\n__all__ = ('DatabaseIndex', 'Database', 'DatabaseConnectionView')\n"
  },
  {
    "path": "edb/server/dbview/dbview.pxd",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ncimport cython\n\nfrom libc.stdint cimport uint64_t\n\nfrom edb.server.cache cimport stmt_cache\n\ncdef DEFAULT_STATE\n\ncpdef enum SideEffects:\n\n    SchemaChanges = 1 << 0\n    DatabaseConfigChanges = 1 << 1\n    InstanceConfigChanges = 1 << 2\n    GlobalSchemaChanges = 1 << 3\n    DatabaseChanges = 1 << 4\n\n\n@cython.final\ncdef class CompiledQuery:\n    cdef public object query_unit_group\n    cdef public object first_extra  # Optional[int]\n    cdef public object extra_counts\n    cdef public object extra_blobs\n    cdef public bint extra_formatted_as_text\n    cdef public object extra_type_oids\n    cdef public object request\n    cdef public object recompiled_cache\n    cdef public bint use_pending_func_cache\n    cdef public object tag\n\n    cdef bytes make_query_prefix(self)\n\n\ncdef class DatabaseIndex:\n    cdef:\n        dict _dbs\n        object _server\n        object _tenant\n        object _sys_config\n        object _comp_sys_config\n        object _std_schema\n        object _global_schema_pickle\n        object _default_sysconfig\n        object _sys_config_spec\n        object _cached_compiler_args\n\n    cdef invalidate_caches(self)\n    cdef inline set_current_branches(self)\n\n\ncdef class Database:\n\n    cdef:\n        stmt_cache.StatementsCache _eql_to_compiled\n        object _cache_locks\n        object _sql_to_compiled\n        DatabaseIndex _index\n        object _views\n        object _introspection_lock\n        object _state_serializers\n        readonly object user_config_spec\n\n        object _cache_worker_task\n        object _cache_queue\n        object _cache_notify_task\n        object _cache_notify_queue\n\n        uint64_t _tx_seq\n        object _active_tx_list\n        object _func_cache_gt_tx_seq\n\n        readonly str name\n        readonly object schema_version\n        readonly object dbver\n        readonly object db_config\n        readonly bytes user_schema_pickle\n        readonly object reflection_cache\n        readonly object backend_ids\n        readonly object backend_oid_to_id\n        readonly object extensions\n        readonly object _feature_used_metrics\n        readonly int dml_queries_executed\n\n    cdef _invalidate_caches(self)\n    cdef _cache_compiled_query(self, key, compiled)\n    cdef _new_view(self, query_cache, protocol_version, role_name)\n    cdef _remove_view(self, view)\n    cdef _observe_auth_ext_config(self)\n    cdef _set_backend_ids(self, types)\n    cdef _update_backend_ids(self, new_types)\n    cdef _set_extensions(\n        self,\n        extensions,\n    )\n    cdef _set_feature_used_metrics(self, feature_used_metrics)\n    cdef _set_and_signal_new_user_schema(\n        self,\n        new_schema_pickle,\n        schema_version,\n        extensions,\n        ext_config_settings,\n        feature_used_metrics,\n        reflection_cache=?,\n        backend_ids=?,\n        db_config=?,\n        start_stop_extensions=?,\n    )\n    cpdef start_stop_extensions(self)\n    cdef get_state_serializer(self, protocol_version)\n    cpdef set_state_serializer(self, protocol_version, serializer)\n    cdef inline uint64_t tx_seq_begin_tx(self)\n    cdef inline tx_seq_end_tx(self, uint64_t seq)\n\n\ncdef class DatabaseConnectionView:\n\n    cdef:\n        Database _db\n        bint _query_cache_enabled\n        object _protocol_version\n        str _role_name\n        public bint is_transient\n        # transient dbviews won't cause an immediate error in\n        # ensure_database_not_connected(..., close_frontend_conns=False),\n        # which is usually called from `DROP BRANCH` or `CREATE ... FROM`.\n        # Although, transient dbviews users should guarantee the transient use\n        # of pgcons, because _pg_ensure_database_not_connected() may still time\n        # out `DROP BRANCH` if the transient pgcon is not released soon enough.\n\n        # State properties\n        object _config\n        object _in_tx_config\n\n        object _globals\n        object _in_tx_globals\n\n        object _modaliases\n        object _in_tx_modaliases\n\n        object _state_serializer\n        object _in_tx_state_serializer\n        object _command_state_serializer\n\n        tuple _session_state_db_cache\n        tuple _session_state_cache\n\n        object _txid\n        object _in_tx_db_config\n        object _in_tx_savepoints\n        object _in_tx_root_user_schema_pickle\n        object _in_tx_user_schema_pickle\n        object _in_tx_user_schema_version\n        object _in_tx_user_config_spec\n        object _in_tx_global_schema_pickle\n        object _in_tx_new_types\n        int _in_tx_dbver\n        bint _in_tx\n        uint64_t _in_tx_capabilities\n        bint _in_tx_with_sysconfig\n        bint _in_tx_with_dbconfig\n        bint _in_tx_with_set\n        bint _tx_error\n        uint64_t _in_tx_seq\n        object _in_tx_isolation_level\n\n        uint64_t _capability_mask\n\n        object _last_comp_state\n        int _last_comp_state_id\n\n        dict _sys_globals\n\n        object __weakref__\n\n    cdef _reset_tx_state(self)\n    cdef inline _check_in_tx_error(self, query_unit_group)\n\n    cdef clear_tx_error(self)\n    cdef rollback_tx_to_savepoint(self, name)\n    cdef declare_savepoint(self, name, spid)\n    cdef recover_aliases_and_config(self, modaliases, config, globals)\n    cdef abort_tx(self)\n\n    cpdef in_tx(self)\n    cpdef in_tx_error(self)\n\n    cdef cache_compiled_query(self, object key, object query_unit_group)\n    cdef lookup_compiled_query(self, object key)\n    cdef as_compiled(self, query_req, query_unit_group, bint use_metrics=?)\n\n    cdef tx_error(self)\n\n    cdef start(self, query_unit)\n    cdef start_tx(self)\n    cdef _apply_in_tx(self, query_unit)\n    cdef start_implicit(self, query_unit)\n    cdef on_error(self)\n    cdef on_success(self, query_unit, new_types)\n    cdef commit_implicit_tx(\n        self,\n        user_schema,\n        extensions,\n        ext_config_settings,\n        global_schema,\n        roles,\n        cached_reflection,\n        feature_used_metrics,\n    )\n\n    cdef get_user_config_spec(self)\n    cpdef get_config_spec(self)\n\n    cpdef get_session_config(self)\n    cdef set_session_config(self, new_conf)\n\n    cpdef get_globals(self)\n    cpdef set_globals(self, new_globals)\n    cpdef get_global_value(self, k)\n\n    cdef get_state_serializer(self)\n    cdef set_state_serializer(self, new_serializer)\n\n    cpdef get_database_config(self)\n    cdef set_database_config(self, new_conf)\n\n    cdef get_system_config(self)\n    cpdef get_compilation_system_config(self)\n    cdef config_lookup(self, name)\n\n    cdef set_modaliases(self, new_aliases)\n    cpdef get_modaliases(self)\n\n    cdef bytes serialize_state(self)\n    cdef bint is_state_desc_changed(self)\n    cdef describe_state(self)\n    cdef encode_state(self)\n    cdef check_session_config_perms(self, keys)\n    cdef decode_state(self, type_id, data)\n    cdef decode_json_session_config(self, json_session_config)\n    cdef bint needs_commit_after_state_sync(self)\n\n    cdef check_capabilities(\n        self,\n        query_unit,\n        allowed_capabilities,\n        error_constructor,\n        reason,\n        unsafe_isolation_dangers,\n    )\n"
  },
  {
    "path": "edb/server/dbview/dbview.pyi",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import (\n    Any,\n    Awaitable,\n    Callable,\n    Hashable,\n    Iterator,\n    Mapping,\n    Optional,\n    TypeAlias,\n)\n\nimport uuid\n\nimport immutables\n\nfrom edb.schema import schema as s_schema\n\nfrom edb.server import config\nfrom edb.server import pgcon\nfrom edb.server import server\nfrom edb.server import tenant\nfrom edb.server.compiler import dbstate\nfrom edb.server.compiler import sertypes\n\nConfig: TypeAlias = Mapping[str, config.SettingValue]\n\nclass CompiledQuery:\n    query_unit_group: dbstate.QueryUnitGroup\n\nclass Database:\n    name: str\n    dbver: int\n    db_config: Config\n    extensions: set[str]\n    user_config_spec: config.Spec\n    dml_queries_executed: int\n\n    @property\n    def server(self) -> server.Server:\n        ...\n\n    @property\n    def tenant(self) -> tenant.Tenant:\n        ...\n\n    def stop(self) -> None:\n        ...\n\n    async def monitor(\n        self,\n        worker: Callable[[], Awaitable[None]],\n        name: str,\n    ) -> None:\n        ...\n\n    async def cache_worker(self) -> None:\n        ...\n\n    async def cache_notifier(self) -> None:\n        ...\n\n    def start_stop_extensions(self) -> None:\n        ...\n\n    def cache_compiled_sql(\n        self,\n        key: Hashable,\n        compiled: list[dbstate.SQLQueryUnit],\n        schema_version: uuid.UUID,\n    ) -> None:\n        ...\n\n    def lookup_compiled_sql(\n        self,\n        key: Hashable,\n    ) -> Optional[list[dbstate.SQLQueryUnit]]:\n        ...\n\n    def set_state_serializer(\n        self,\n        protocol_version: tuple[int, int],\n        serializer: sertypes.StateSerializer,\n    ) -> None:\n        pass\n\n    def hydrate_cache(self, query_cache: list[tuple[bytes, ...]]) -> None:\n        ...\n\n    def invalidate_cache_entries(self, to_invalidate: list[uuid.UUID]) -> None:\n        ...\n\n    def clear_query_cache(self) -> None:\n        ...\n\n    def iter_views(self) -> Iterator[DatabaseConnectionView]:\n        ...\n\n    def get_query_cache_size(self) -> int:\n        ...\n\n    async def introspection(self) -> None:\n        ...\n\n    def lookup_config(self, name: str) -> Any:\n        ...\n\n    def is_introspected(self) -> bool:\n        ...\n\nclass DatabaseConnectionView:\n    def in_tx(self) -> bool:\n        ...\n\n    def in_tx_error(self) -> bool:\n        ...\n\n    def get_session_config(self) -> Config:\n        ...\n\n    def get_modaliases(self) -> Mapping[str | None, str]:\n        ...\n\nclass DatabaseIndex:\n    def __init__(\n        self,\n        tenant: tenant.Tenant,\n        *,\n        std_schema: s_schema.Schema,\n        global_schema_pickle: bytes,\n        sys_config: Config,\n        default_sysconfig: Config,\n        sys_config_spec: config.Spec,\n    ) -> None:\n        ...\n\n    def count_connections(self, dbname: str) -> int:\n        ...\n\n    def get_sys_config(self) -> Config:\n        ...\n\n    def get_compilation_system_config(self) -> Config:\n        ...\n\n    def update_sys_config(self, sys_config: Config) -> None:\n        ...\n\n    def has_db(self, dbname: str) -> bool:\n        ...\n\n    def get_db(self, dbname) -> Database:\n        ...\n\n    def maybe_get_db(self, dbname) -> Optional[Database]:\n        ...\n\n    def get_global_schema_pickle(self) -> bytes:\n        ...\n\n    def update_global_schema(self, global_schema_pickle: bytes) -> None:\n        ...\n\n    def register_db(\n        self,\n        dbname: str,\n        *,\n        user_schema_pickle: Optional[bytes],\n        schema_version: Optional[uuid.UUID],\n        db_config: Optional[Config],\n        reflection_cache: Optional[Mapping[str, tuple[str, ...]]],\n        backend_ids: Optional[Mapping[str, tuple[int, str]]],\n        extensions: Optional[set[str]],\n        ext_config_settings: Optional[list[config.Setting]],\n        early: bool = False,\n        feature_used_metrics: Optional[Mapping[str, float]] = ...,\n    ) -> Database:\n        ...\n\n    def unregister_db(self, dbname: str) -> None:\n        ...\n\n    def iter_dbs(self) -> Iterator[Database]:\n        ...\n\n    async def apply_system_config_op(\n        self,\n        conn: pgcon.PGConnection,\n        op: config.Operation,\n    ) -> None:\n        ...\n\n    def new_view(\n        self,\n        dbname: str,\n        *,\n        query_cache: bool,\n        protocol_version: tuple[int, int],\n        role_name: str,\n    ) -> DatabaseConnectionView:\n        ...\n\n    def remove_view(\n        self,\n        view: DatabaseConnectionView,\n    ) -> None:\n        ...\n\n    def invalidate_caches(self) -> None:\n        ...\n\n    def get_cached_compiler_args(\n        self,\n    ) -> tuple[\n        bytes,\n        immutables.Map[str, config.SettingValue],\n    ]:\n        ...\n\n    def lookup_config(self, name: str) -> Any:\n        ...\n"
  },
  {
    "path": "edb/server/dbview/dbview.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import (\n    Optional, Sequence\n)\n\nimport asyncio\nimport base64\nimport copy\nimport json\nimport logging\nimport os.path\nimport pickle\nimport struct\nimport time\nimport typing\nimport uuid\nimport weakref\n\nimport immutables\n\nfrom edb import errors\nfrom edb.common import debug, lru, uuidgen, asyncutil, span\nfrom edb import edgeql\nfrom edb.edgeql import qltypes\nfrom edb.schema import schema as s_schema\nfrom edb.schema import name as s_name\nfrom edb.server import compiler, defines, config, metrics, pgcon\nfrom edb.server.compiler import dbstate, enums, sertypes\nfrom edb.server.protocol import execute\nfrom edb.pgsql import dbops\nfrom edb.server.pgcon import errors as pgerror\n\nfrom edb.server.protocol import ai_ext\n\ncimport cython\n\nfrom edb.server.compiler cimport rpc\nfrom edb.server.protocol.args_ser cimport (\n    recode_global,\n)\n\n__all__ = (\n    'DatabaseIndex',\n    'DatabaseConnectionView',\n    'SideEffects',\n    'Database'\n)\n\ncdef uint64_t PROTO_CAPS = enums.Capability.PROTO_CAPS\n\ncdef DEFAULT_MODALIASES = immutables.Map({None: defines.DEFAULT_MODULE_ALIAS})\ncdef DEFAULT_CONFIG = immutables.Map()\ncdef DEFAULT_GLOBALS = immutables.Map()\ncdef DEFAULT_STATE = json.dumps([]).encode('utf-8')\n\ncdef INT32_PACKER = struct.Struct('!l').pack\n\ncdef int VER_COUNTER = 0\ncdef DICTDEFAULT = (None, None)\ncdef object logger = logging.getLogger('edb.server')\n\ncdef uint64_t DML_CAPABILITIES = compiler.Capability.MODIFICATIONS\ncdef uint64_t DDL_CAPABILITIES = compiler.Capability.DDL\n\nDEF TEXT_OID = 25\n\n# Mapping from oids of PostgreSQL types into corresponding EdgeQL type.\n# Needed only for pg types that do not exist in EdgeQL, such as pg_catalog.name\ncdef TYPES_SQL_ONLY = immutables.Map({\n    18: \"00000000-0000-0000-0000-000000000101\", # char -> str\n    19: \"00000000-0000-0000-0000-000000000101\", # pgcatalog.name -> str\n    24: \"00000000-0000-0000-0000-000000000101\", # regproc -> str\n    26: \"00000000-0000-0000-0000-000000000104\", # oid -> int32\n    28: \"00000000-0000-0000-0000-000000000104\", # xid -> int32\n    29: \"00000000-0000-0000-0000-000000000104\", # cid -> int32\n    194: \"00000000-0000-0000-0000-000000000101\", # pg_node_tree -> str\n })\n\ncdef next_dbver():\n    global VER_COUNTER\n    VER_COUNTER += 1\n    return VER_COUNTER\n\n\n\ncdef enum CacheState:\n    Pending = 0,\n    Present,\n    Evicted\n\n\n@cython.final\ncdef class CompiledQuery:\n\n    def __init__(\n        self,\n        query_unit_group: dbstate.QueryUnitGroup,\n        first_extra: Optional[int]=None,\n        extra_counts=(),\n        extra_blobs=(),\n        extra_formatted_as_text: bool = False,\n        extra_type_oids: Sequence[int] = (),\n        request=None,\n        recompiled_cache=None,\n        use_pending_func_cache=False,\n    ):\n        self.query_unit_group = query_unit_group\n        self.first_extra = first_extra\n        self.extra_counts = extra_counts\n        self.extra_blobs = extra_blobs\n        self.extra_formatted_as_text = extra_formatted_as_text\n        self.extra_type_oids = tuple(extra_type_oids)\n        self.request = request\n        self.recompiled_cache = recompiled_cache\n        self.use_pending_func_cache = use_pending_func_cache\n\n    cdef bytes make_query_prefix(self):\n        data = {}\n        if self.tag:\n            data['tag'] = self.tag\n        # maintenance reminder: please also update _amend_typedesc_in_sql()\n        if data:\n            data_bytes = json.dumps(data).encode(defines.EDGEDB_ENCODING)\n            return b''.join([b'-- ', data_bytes, b'\\n'])\n        else:\n            return b''\n\n\ncdef class Database:\n\n    # Global LRU cache of compiled queries\n    _eql_to_compiled: stmt_cache.StatementsCache[uuid.UUID, dbstate.QueryUnitGroup]\n\n    def __init__(\n        self,\n        DatabaseIndex index,\n        str name,\n        *,\n        bytes user_schema_pickle,\n        object schema_version,\n        object db_config,\n        object reflection_cache,\n        object backend_ids,\n        object extensions,\n        object ext_config_settings,\n        object feature_used_metrics,\n    ):\n        self.name = name\n\n        self.schema_version = schema_version\n        self.dbver = next_dbver()\n\n        self._index = index\n        self._views = weakref.WeakSet()\n        self._state_serializers = {}\n\n        self._introspection_lock = asyncio.Lock()\n\n        self._eql_to_compiled = stmt_cache.StatementsCache(\n            maxsize=self.lookup_config('query_cache_size')\n        )\n        self._cache_locks = {}\n        self._sql_to_compiled = lru.LRUMapping(\n            maxsize=self.lookup_config('query_cache_size')\n        )\n\n        # Tracks the active transactions and their creation sequence. The\n        # sequence ID is incremental-only. ID 0 is reserved as a non-exist ID.\n        self._tx_seq = 0  # most-recently used transaction sequence ID\n        self._active_tx_list = {}  # name it \"list\" to emphasize the order\n\n        # Also an ordered dict of func cache present in DB but still using\n        # inline SQL due to active transactions.\n        self._func_cache_gt_tx_seq = {}\n\n        self.db_config = db_config\n        self.user_schema_pickle = user_schema_pickle\n        if ext_config_settings is not None:\n            self.user_config_spec = config.FlatSpec(*ext_config_settings)\n        self.reflection_cache = reflection_cache\n        self._set_backend_ids(backend_ids)\n        self.extensions = set()\n        self._set_extensions(extensions)\n        self._observe_auth_ext_config()\n\n        self._feature_used_metrics = {}\n        self._set_feature_used_metrics(feature_used_metrics)\n\n        self._cache_worker_task = self._cache_queue = None\n        self._cache_notify_task = self._cache_notify_queue = None\n        self._cache_queue = asyncio.Queue()\n        self._cache_worker_task = asyncio.create_task(\n            self.monitor(self.cache_worker, 'cache_worker'))\n        # Queue of (key: str, is_add: bool) pairs. is_add signals\n        # whether it is an addition or deletion.\n        self._cache_notify_queue = asyncio.Queue()\n        self._cache_notify_task = asyncio.create_task(\n            self.monitor(self.cache_notifier, 'cache_notifier'))\n\n        self.dml_queries_executed = 0\n\n    @property\n    def server(self):\n        return self._index._server\n\n    @property\n    def tenant(self):\n        return self._index._tenant\n\n    def stop(self):\n        if self._cache_worker_task:\n            self._cache_worker_task.cancel()\n            self._cache_worker_task = None\n        if self._cache_notify_task:\n            self._cache_notify_task.cancel()\n            self._cache_notify_task = None\n        self._set_extensions(set())\n        self._set_feature_used_metrics({})\n        self.start_stop_extensions()\n\n    async def monitor(self, worker, name):\n        while True:\n            try:\n                await worker()\n            except Exception as ex:\n                debug.dump(ex)\n                metrics.background_errors.inc(\n                    1.0, self.tenant._instance_name, name\n                )\n                # Give things time to recover, since the likely\n                # failure mode here is a failover or some such.\n                await asyncio.sleep(0.1)\n\n    async def cache_worker(self):\n        while True:\n            # First, handle any evictions\n            keys = []\n            while self._eql_to_compiled.needs_cleanup():\n                query_req, unit_group = self._eql_to_compiled.cleanup_one()\n                if len(unit_group) == 1 and unit_group.cache_state == 1:\n                    keys.append(query_req.get_cache_key())\n                    self._func_cache_gt_tx_seq.pop(query_req, None)\n                unit_group.cache_state = CacheState.Evicted\n            if keys:\n                await self.tenant.evict_query_cache(self.name, keys)\n                for key in keys:\n                    self._cache_notify_queue.put_nowait(\n                        (str(key), False)\n                    )\n\n            # Now, populate the cache\n            # Empty the queue, for batching reasons.\n            # N.B: This empty/get_nowait loop is safe because this is\n            # an asyncio Queue. If it was threaded, it would be racy.\n            ops = [await self._cache_queue.get()]\n            while not self._cache_queue.empty():\n                ops.append(self._cache_queue.get_nowait())\n            # Filter ops for only what we need\n            ops = [\n                (query_req, units) for query_req, units in ops\n                if len(units) == 1\n                and units[0].cache_sql\n                and units.cache_state == CacheState.Pending\n            ]\n            if not ops:\n                continue\n\n            g = execute.build_cache_persistence_units(ops)\n            async with self.tenant.with_pgcon(self.name) as conn:\n                try:\n                    await g.execute(conn, self)\n                except Exception as e:\n                    logger.warning(\"Failed to persist function cache\",\n                                exc_info=True)\n                    continue\n\n            for query_req, units in ops:\n                units.cache_state = CacheState.Present\n                if self._active_tx_list:\n                    # Any active tx would delay the time we flip to func cache\n                    units.tx_seq_id = self._tx_seq\n                    self._func_cache_gt_tx_seq[query_req] = units\n                else:\n                    units[0].maybe_use_func_cache()\n                self._cache_notify_queue.put_nowait(\n                    (str(units[0].cache_key), True)\n                )\n\n    cdef inline uint64_t tx_seq_begin_tx(self):\n        self._tx_seq += 1\n        self._active_tx_list[self._tx_seq] = True\n        return self._tx_seq\n\n    cdef inline tx_seq_end_tx(self, uint64_t seq):\n        # Remove the ending transaction from the active list\n        if not self._active_tx_list.pop(seq, False):\n            return\n\n        # Stop early if we don't have func cache to activate\n        if not self._func_cache_gt_tx_seq:\n            return\n\n        if self._active_tx_list:\n            # Grab the seq ID of the oldest active transaction\n            active_tx = next(iter(self._active_tx_list.keys()))\n        else:\n            # If all tx ended, we should just activate all pending func cache\n            for units in self._func_cache_gt_tx_seq.values():\n                units[0].maybe_use_func_cache()\n            self._func_cache_gt_tx_seq.clear()\n            return\n\n        # Or else, keep activating func cache until the oldest active tx\n        drops = []\n        for query_req, units in self._func_cache_gt_tx_seq.items():\n            if units.tx_seq_id < active_tx:\n                units[0].maybe_use_func_cache()\n                drops.append(query_req)\n            else:\n                break\n        for query_req in drops:\n            self._func_cache_gt_tx_seq.pop(query_req)\n\n    async def cache_notifier(self):\n        await asyncutil.debounce(\n            lambda: self._cache_notify_queue.get(),\n            lambda keys: self.tenant.signal_sysevent(\n                'query-cache-changes',\n                dbname=self.name,\n                to_add=[k for k, b in keys if b],\n                to_invalidate=[k for k, b in keys if not b],\n            ),\n            max_wait=1.0,\n            delay_amt=0.2,\n            # 100 keys will take up about 4000 bytes, which\n            # fits in the 8000 allowed in events.\n            max_batch_size=100,\n        )\n\n    cdef _set_extensions(self, extensions):\n        # Update metrics about extension use\n        tname = self.tenant.get_instance_name()\n        for ext in self.extensions:\n            if ext not in extensions:\n                metrics.extension_used.dec(1, tname, ext)\n\n        for ext in extensions:\n            if ext not in self.extensions:\n                metrics.extension_used.inc(1, tname, ext)\n\n        self.extensions = extensions\n\n    cdef _set_feature_used_metrics(self, feature_used_metrics):\n        # Update metrics about feature use\n        #\n        # We store the old feature use metrics so that we can\n        # incrementally update them after DDL without needing to look\n        # at the other database branches\n        if feature_used_metrics is None:\n            return\n\n        tname = self.tenant.get_instance_name()\n        keys = self._feature_used_metrics.keys() | feature_used_metrics.keys()\n        for key in keys:\n            # Update the count of how many times the feature is used\n            metrics.feature_used.inc(\n                feature_used_metrics.get(key, 0.0)\n                - self._feature_used_metrics.get(key, 0.0),\n                tname,\n                key,\n            )\n            # Update the count of branches using the feature at all\n            metrics.feature_used_branches.inc(\n                (feature_used_metrics.get(key, 0.0) > 0)\n                - (self._feature_used_metrics.get(key, 0.0) > 0),\n                tname,\n                key,\n            )\n\n        self._feature_used_metrics = feature_used_metrics\n\n    cdef _set_and_signal_new_user_schema(\n        self,\n        new_schema_pickle,\n        schema_version,\n        extensions,\n        ext_config_settings,\n        feature_used_metrics,\n        reflection_cache=None,\n        backend_ids=None,\n        db_config=None,\n        start_stop_extensions=True,\n    ):\n        if new_schema_pickle is None:\n            raise AssertionError('new_schema is not supposed to be None')\n\n        self.schema_version = schema_version\n        self.dbver = next_dbver()\n\n        self.user_schema_pickle = new_schema_pickle\n        self._set_extensions(extensions)\n        self.user_config_spec = config.FlatSpec(*ext_config_settings)\n\n        self._set_feature_used_metrics(feature_used_metrics)\n\n        if backend_ids is not None:\n            self._set_backend_ids(backend_ids)\n        if reflection_cache is not None:\n            self.reflection_cache = reflection_cache\n        if db_config is not None:\n            self.db_config = db_config\n            self._observe_auth_ext_config()\n        self._invalidate_caches()\n        if start_stop_extensions:\n            self.start_stop_extensions()\n\n    cpdef start_stop_extensions(self):\n        if \"ai\" in self.extensions:\n            ai_ext.start_extension(self.tenant, self.name)\n        else:\n            ai_ext.stop_extension(self.tenant, self.name)\n\n    cdef _observe_auth_ext_config(self):\n        key = \"ext::auth::AuthConfig::providers\"\n        if (\n            self.db_config is not None and\n            self.user_config_spec is not None and\n            key in self.user_config_spec\n        ):\n            providers = config.lookup(\n                key,\n                self.db_config,\n                spec=self.user_config_spec,\n            )\n            metrics.auth_providers.set(\n                len(providers),\n                self.tenant.get_instance_name(),\n                self.name,\n            )\n\n    cdef _set_backend_ids(self, types):\n        self.backend_ids = {}\n        self.backend_oid_to_id = dict(TYPES_SQL_ONLY)\n        if types != None:\n            self._update_backend_ids(types)\n\n    cdef _update_backend_ids(self, new_types):\n        self.backend_ids.update(new_types)\n        self.backend_oid_to_id.update({\n            v[0]: k for k, v in new_types.items()\n            if v[0] is not None\n        })\n\n    cdef _invalidate_caches(self):\n        self._sql_to_compiled.clear()\n        self._index.invalidate_caches()\n\n    cdef _cache_compiled_query(self, key, compiled: dbstate.QueryUnitGroup):\n        # `dbver` must be the schema version `compiled` was compiled upon\n        assert compiled.cacheable\n\n        if key in self._eql_to_compiled:\n            # We already have a cached query for the current user schema\n            return\n\n        self._eql_to_compiled[key] = compiled\n\n        if self._cache_queue is not None:\n            self._cache_queue.put_nowait((key, compiled))\n\n    def cache_compiled_sql(self, key, compiled: list[str], schema_version):\n        existing, ver = self._sql_to_compiled.get(key, DICTDEFAULT)\n        if existing is not None and ver == self.schema_version:\n            # We already have a cached query for a more recent DB version.\n            return\n        if not all(unit.cacheable for unit in compiled):\n            return\n\n        # Store the matching schema version, see also the comments at origin\n        self._sql_to_compiled[key] = compiled, schema_version\n\n    def lookup_compiled_sql(self, key):\n        rv, cached_ver = self._sql_to_compiled.get(key, DICTDEFAULT)\n        if rv is not None and cached_ver != self.schema_version:\n            rv = None\n        return rv\n\n    cdef _new_view(self, query_cache, protocol_version, role_name):\n        view = DatabaseConnectionView(\n            self,\n            query_cache=query_cache,\n            protocol_version=protocol_version,\n            role_name=role_name,\n        )\n        self._views.add(view)\n        return view\n\n    cdef _remove_view(self, view):\n        self._views.remove(view)\n\n    cdef get_state_serializer(self, protocol_version):\n        return self._state_serializers.get(protocol_version)\n\n    cpdef set_state_serializer(self, protocol_version, serializer):\n        old_serializer = self._state_serializers.get(protocol_version)\n        if (\n            old_serializer is None or\n            old_serializer.type_id != serializer.type_id\n        ):\n            # also invalidate other protocol versions\n            self._state_serializers = {protocol_version: serializer}\n            return serializer\n        else:\n            return old_serializer\n\n    def hydrate_cache(self, query_cache):\n        warning_count = 0\n        for _, in_data, out_data in query_cache:\n            try:\n                query_req = rpc.CompilationRequest.deserialize(\n                    in_data,\n                    \"\",\n                    self.server.compilation_config_serializer,\n                )\n\n                if query_req not in self._eql_to_compiled:\n                    unit = dbstate.QueryUnit.deserialize(out_data)\n                    group = dbstate.QueryUnitGroup()\n                    group.append(unit, serialize=False)\n                    group.cache_state = CacheState.Present\n                    if self._active_tx_list:\n                        # Any active transaction would delay the time we flip\n                        # to function cache\n                        group.tx_seq_id = self._tx_seq\n                        self._func_cache_gt_tx_seq[query_req] = group\n                    else:\n                        group[0].maybe_use_func_cache()\n                    self._eql_to_compiled[query_req] = group\n            except Exception as e:\n                if warning_count < 0:\n                    warning_count -= 1\n                elif warning_count < 10:\n                    logger.warning(\"skipping incompatible cache item: %s\", e)\n                    warning_count += 1\n                else:\n                    logger.warning(\n                        \"too many incompatible cache items, \"\n                        \"skipping the following warnings\"\n                    )\n                    warning_count = -warning_count - 1\n        if warning_count < 0:\n            logger.warning(\n                \"skipped %d incompatible cache items\", -warning_count\n            )\n\n    def invalidate_cache_entry_object(self, obj):\n        self._eql_to_compiled.pop(obj, None)\n\n    def invalidate_cache_entries(self, to_invalidate):\n        for key in to_invalidate:\n            handle = rpc.CompilationRequestIdHandle(key)\n            self._eql_to_compiled.pop(handle, None)\n\n    def clear_query_cache(self):\n        self._eql_to_compiled.clear()\n\n    def iter_views(self):\n        yield from self._views\n\n    def get_query_cache_size(self):\n        return len(self._eql_to_compiled) + len(self._sql_to_compiled)\n\n    async def introspection(self):\n        if self.user_schema_pickle is None:\n            async with self._introspection_lock:\n                if self.user_schema_pickle is None:\n                    await self.tenant.introspect_db(self.name)\n\n    def is_introspected(self):\n        return self.user_schema_pickle is not None\n\n    def lookup_config(self, name: str):\n        spec = self._index._sys_config_spec\n        if self.user_config_spec is not None:\n            spec = config.ChainedSpec(spec, self.user_config_spec)\n        return config.lookup(\n            name,\n            self.db_config or DEFAULT_CONFIG,\n            self._index._sys_config,\n            spec=spec,\n        )\n\n\ncdef class DatabaseConnectionView:\n\n    def __init__(\n        self, db: Database, *, query_cache, protocol_version, role_name: str\n    ):\n        self._db = db\n\n        self._query_cache_enabled = query_cache\n        self._protocol_version = protocol_version\n\n        self._modaliases = DEFAULT_MODALIASES\n        self._config = DEFAULT_CONFIG\n        self._globals = DEFAULT_GLOBALS\n        self._session_state_db_cache = None\n        self._session_state_cache = None\n        self._state_serializer = None\n        self._role_name = role_name\n\n        # N.B: If we add anything that is not a string or list of string, we'll\n        # need to adjust get_global_value to encode differently.\n        self._sys_globals = {\n            'sys::current_role': self._role_name,\n            'sys::current_permissions': list(self.get_permissions()[1])\n        }\n\n        if db.name == defines.EDGEDB_SYSTEM_DB:\n            # Make system database read-only.\n            self._capability_mask = <uint64_t>(\n                compiler.Capability.ALL\n                & ~compiler.Capability.DDL\n                & ~compiler.Capability.MODIFICATIONS\n            )\n        else:\n            self._capability_mask = <uint64_t>compiler.Capability.ALL\n\n        self._last_comp_state = None\n        self._last_comp_state_id = 0\n\n        self._in_tx_seq = 0\n        self._reset_tx_state()\n\n    def __del__(self):\n        # In any case if _reset_tx_state() is not called, remove self from\n        # ACTIVE_TX_LIST to be safe\n        self._db.tx_seq_end_tx(self._in_tx_seq)\n\n    cdef _reset_tx_state(self):\n        self._db.tx_seq_end_tx(self._in_tx_seq)\n        self._in_tx_seq = 0\n        self._txid = None\n        self._in_tx = False\n        self._in_tx_config = None\n        self._in_tx_globals = None\n        self._in_tx_db_config = None\n        self._in_tx_modaliases = None\n        self._in_tx_savepoints = []\n        self._in_tx_capabilities = 0\n        self._in_tx_with_sysconfig = False\n        self._in_tx_with_dbconfig = False\n        self._in_tx_with_set = False\n        self._in_tx_root_user_schema_pickle = None\n        self._in_tx_user_schema_pickle = None\n        self._in_tx_user_schema_version = None\n        self._in_tx_global_schema_pickle = None\n        self._in_tx_new_types = {}\n        self._in_tx_user_config_spec = None\n        self._in_tx_state_serializer = None\n        self._tx_error = False\n        self._in_tx_dbver = 0\n        self._in_tx_isolation_level = None\n\n    cdef clear_tx_error(self):\n        self._tx_error = False\n\n    cdef rollback_tx_to_savepoint(self, name):\n        self._tx_error = False\n        # See also CompilerConnectionState.rollback_to_savepoint().\n        while self._in_tx_savepoints:\n            if self._in_tx_savepoints[-1][0] == name:\n                break\n            else:\n                self._in_tx_savepoints.pop()\n        else:\n            raise RuntimeError(\n                f'savepoint {name} not found')\n\n        _, spid, (\n            modaliases, config, globals, state_serializer\n        ) = self._in_tx_savepoints[-1]\n        self._txid = spid\n        self.set_modaliases(modaliases)\n        self.set_session_config(config)\n        self.set_globals(globals)\n        self.set_state_serializer(state_serializer)\n\n    cdef declare_savepoint(self, name, spid):\n        state = (\n            self.get_modaliases(),\n            self.get_session_config(),\n            self.get_globals(),\n            self.get_state_serializer(),\n        )\n        self._in_tx_savepoints.append((name, spid, state))\n\n    cdef recover_aliases_and_config(self, modaliases, config, globals):\n        assert not self._in_tx\n        self.set_modaliases(modaliases)\n        self.set_session_config(config)\n        self.set_globals(globals)\n\n    cdef abort_tx(self):\n        if not self.in_tx():\n            raise errors.InternalServerError('abort_tx(): not in transaction')\n        self._reset_tx_state()\n\n    cpdef get_session_config(self):\n        if self._in_tx:\n            return self._in_tx_config\n        else:\n            return self._config\n\n    cpdef get_globals(self):\n        if self._in_tx:\n            return self._in_tx_globals\n        else:\n            return self._globals\n\n    cpdef get_global_value(self, k):\n        if k in self._sys_globals:\n            # N.B: Currently only str and list[str]\n            sys_global = self._sys_globals[k]\n            encoded: bytes\n            if isinstance(sys_global, str):\n                encoded = sys_global.encode('utf-8')\n            elif isinstance(sys_global, list):\n                encoded = b''\n                encoded += b'\\x00\\x00\\x00\\x01' # ndims\n                encoded += b'\\x00\\x00\\x00\\x00' # flags\n                encoded += TEXT_OID.to_bytes(4, 'big') # array_tid\n                encoded += len(sys_global).to_bytes(4, 'big') # count\n                encoded += b'\\x00\\x00\\x00\\x01' # bound\n                for elem in sys_global:\n                    elem_encoded = elem.encode('utf-8')\n                    encoded += len(elem_encoded).to_bytes(4, 'big')\n                    encoded += elem_encoded\n            else:\n                raise NotImplementedError\n            return encoded, True\n        else:\n            entry = self.get_globals().get(k)\n            if entry:\n                return entry.value, True\n            else:\n                return None, False\n\n    cdef get_state_serializer(self):\n        if self._in_tx:\n            return self._in_tx_state_serializer\n        else:\n            if self._state_serializer is None:\n                self._state_serializer = self._db.get_state_serializer(\n                    self._protocol_version\n                )\n            return self._state_serializer\n\n    cdef set_state_serializer(self, new_serializer):\n        if self._in_tx:\n            if (\n                self._in_tx_state_serializer is None or\n                self._in_tx_state_serializer.type_id != new_serializer.type_id\n            ):\n                self._in_tx_state_serializer = new_serializer\n        else:\n            # Use the same object as the database to avoid duplicate cache\n            self._state_serializer = self._db.set_state_serializer(\n                self._protocol_version, new_serializer\n            )\n\n    cdef get_user_config_spec(self):\n        if self._in_tx:\n            return self._in_tx_user_config_spec\n        else:\n            return self._db.user_config_spec\n\n    cpdef get_config_spec(self):\n        return config.ChainedSpec(\n            self._db._index._sys_config_spec,\n            self.get_user_config_spec(),\n        )\n\n    cdef set_session_config(self, new_conf):\n        if self._in_tx:\n            self._in_tx_config = new_conf\n        else:\n            self._config = new_conf\n\n    cpdef set_globals(self, new_globals):\n        if self._in_tx:\n            self._in_tx_globals = new_globals\n        else:\n            self._globals = new_globals\n\n    cpdef get_database_config(self):\n        if self._in_tx:\n            return self._in_tx_db_config\n        else:\n            return self._db.db_config\n\n    cdef set_database_config(self, new_conf):\n        if self._in_tx:\n            self._in_tx_db_config = new_conf\n        else:\n            # N.B: If we *aren't* in a transaction, we rely on calling\n            # process_side_effects() promptly to introspect the new\n            # state.\n            # (We do it this way to avoid potential races between\n            # multiple connections do db configs.)\n            pass\n\n\n    cdef get_system_config(self):\n        return self._db._index.get_sys_config()\n\n    cpdef get_compilation_system_config(self):\n        return self._db._index.get_compilation_system_config()\n\n    cdef set_modaliases(self, new_aliases):\n        if self._in_tx:\n            self._in_tx_modaliases = new_aliases\n        else:\n            self._modaliases = new_aliases\n\n    cpdef get_modaliases(self):\n        if self._in_tx:\n            return self._in_tx_modaliases\n        else:\n            return self._modaliases\n\n    def get_user_schema_pickle(self):\n        if self._in_tx:\n            return self._in_tx_user_schema_pickle\n        else:\n            return self._db.user_schema_pickle\n\n    def get_global_schema_pickle(self):\n        if self._in_tx:\n            return self._in_tx_global_schema_pickle\n        else:\n            return self._db._index._global_schema_pickle\n\n    def resolve_backend_type_id(self, type_id):\n        type_id = str(type_id)\n\n        if self._in_tx:\n            try:\n                tinfo = self._in_tx_new_types[type_id]\n            except KeyError:\n                pass\n            else:\n                return int(tinfo[0])\n\n        tinfo = self._db.backend_ids.get(type_id)\n        if tinfo is None:\n            raise RuntimeError(\n                f'cannot resolve backend OID for type {type_id}')\n\n        return int(tinfo[0])\n\n    cdef bytes serialize_state(self):\n        cdef list state\n        if self._in_tx:\n            raise errors.InternalServerError(\n                'no need to serialize state while in transaction')\n\n        dbver = self._db.dbver\n        if self._session_state_db_cache is not None:\n            if self._session_state_db_cache[0] == (self._config, dbver):\n                return self._session_state_db_cache[1]\n\n        state = []\n        if self._config and self._config != DEFAULT_CONFIG:\n            settings = self.get_config_spec()\n            for sval in self._config.values():\n                setting = settings[sval.name]\n                kind = 'B' if setting.backend_setting else 'C'\n                jval = config.value_to_json_value(setting, sval.value)\n                state.append({\"name\": sval.name, \"value\": jval, \"type\": kind})\n\n        # Include the database version in the state so that we are forced\n        # to clear the config cache on dbver changes.\n        state.append(\n            {\"name\": '__dbver__', \"value\": dbver, \"type\": 'C'})\n\n        spec = json.dumps(state).encode('utf-8')\n        self._session_state_db_cache = ((self._config, dbver), spec)\n        return spec\n\n    cdef bint is_state_desc_changed(self):\n        # We may have executed a query, or COMMIT/ROLLBACK - just use the\n        # serializer we preserved before. NOTE: the schema might have been\n        # concurrently changed from other sessions, we should not reload\n        # serializer from self._db here so that our state can be serialized\n        # properly, and the Execute stays atomic.\n        serializer = self.get_state_serializer()\n\n        if self._command_state_serializer is not None:\n            # If the resulting descriptor is the same as the input, return None\n            if serializer.type_id == self._command_state_serializer.type_id:\n                if self._in_tx:\n                    # There's a case when DDL was executed but the state schema\n                    # wasn't affected, so it's enough to keep just one copy.\n                    self._in_tx_state_serializer = (\n                        self._command_state_serializer\n                    )\n                return False\n\n            # Update with the new serializer for upcoming encoding\n            self._command_state_serializer = serializer\n\n        return True\n\n    cdef describe_state(self):\n        return self.get_state_serializer().describe()\n\n    cdef encode_state(self):\n        modaliases = self.get_modaliases()\n        session_config = self.get_session_config()\n        globals_ = self.get_globals()\n\n        if self._session_state_cache is None:\n            if (\n                session_config == DEFAULT_CONFIG and\n                modaliases == DEFAULT_MODALIASES and\n                globals_ == DEFAULT_GLOBALS\n            ):\n                return sertypes.NULL_TYPE_ID, b\"\"\n\n        serializer = self._command_state_serializer\n        self._command_state_serializer = None\n        if not self.in_tx():\n            # After encode_state(), self._state_serializer is no longer used if\n            # not in a transaction. So it should be cleared\n            self._state_serializer = None\n\n        if self._session_state_cache is not None:\n            if (\n                modaliases, session_config, globals_, serializer.type_id.bytes\n            ) == self._session_state_cache[:4]:\n                return sertypes.NULL_TYPE_ID, b\"\"\n\n        self._session_state_cache = None\n\n        state = {}\n        try:\n            if modaliases[None] != defines.DEFAULT_MODULE_ALIAS:\n                state['module'] = modaliases[None]\n        except KeyError:\n            pass\n        else:\n            modaliases = modaliases.delete(None)\n        if modaliases:\n            state['aliases'] = list(modaliases.items())\n        if session_config:\n            state['config'] = {k: v.value for k, v in session_config.items()}\n        if globals_:\n            state['globals'] = {k: v.value for k, v in globals_.items()}\n        return serializer.type_id, serializer.encode(state)\n\n    cdef check_session_config_perms(self, keys):\n        is_superuser, permissions = self.get_permissions()\n        if not is_superuser:\n            settings = self.get_config_spec()\n            for k in keys:\n                setting = settings[k]\n                if setting.session_restricted and not (\n                    setting.session_permission\n                    and setting.session_permission in permissions\n                ):\n                    raise errors.DisabledCapabilityError(\n                        f'role {self._role_name} does not have permission to '\n                        f'configure session config variable {k}'\n                    )\n\n    cdef decode_state(self, type_id, data):\n        serializer = self.get_state_serializer()\n        self._command_state_serializer = serializer\n\n        if type_id == sertypes.NULL_TYPE_ID.bytes:\n            self.set_modaliases(DEFAULT_MODALIASES)\n            self.set_session_config(DEFAULT_CONFIG)\n            self.set_globals(DEFAULT_GLOBALS)\n            self._session_state_cache = None\n            return\n\n        if type_id != serializer.type_id.bytes:\n            self._command_state_serializer = None\n            raise errors.StateMismatchError(\n                \"Cannot decode state: type mismatch\"\n            )\n\n        if self._session_state_cache is not None:\n            if type_id == self._session_state_cache[3]:\n                if data == self._session_state_cache[4]:\n                    return\n\n        state = serializer.decode(data)\n        aliases = dict(state.get('aliases', []))\n        aliases[None] = state.get('module', defines.DEFAULT_MODULE_ALIAS)\n        aliases = immutables.Map(aliases)\n\n        config_obj = state.get('config', {})\n        self.check_session_config_perms(config_obj)\n        session_config = immutables.Map({\n            k: config.SettingValue(\n                name=k,\n                value=v,\n                source='session',\n                scope=qltypes.ConfigScope.SESSION,\n            ) for k, v in config_obj.items()\n        })\n        globals_ = immutables.Map({\n            k: config.SettingValue(\n                name=k,\n                value=recode_global(self, v, serializer.get_global_type_rep(k)),\n                source='global',\n                scope=qltypes.ConfigScope.GLOBAL,\n            ) for k, v in state.get('globals', {}).items()\n        })\n        self.set_modaliases(aliases)\n        self.set_session_config(session_config)\n        self.set_globals(globals_)\n        self._session_state_cache = (\n            aliases, session_config, globals_, type_id, data\n        )\n\n    cdef decode_json_session_config(self, json_session_config):\n        if not json_session_config:\n            return\n\n        settings = self.get_config_spec()\n\n        self.check_session_config_perms(json_session_config)\n\n        session_config = self.get_session_config()\n        for k, v in json_session_config.items():\n            op = config.Operation(\n                config.OpCode.CONFIG_SET,\n                qltypes.ConfigScope.SESSION,\n                k,\n                v,\n            )\n            session_config = op.apply(settings, session_config)\n        self.set_session_config(session_config)\n\n    cdef bint needs_commit_after_state_sync(self):\n        return any(\n            tx_conf in self._config\n            for tx_conf in [\n                \"default_transaction_isolation\",\n                \"default_transaction_deferrable\",\n                # default_transaction_access_mode is not yet a backend config\n            ]\n        )\n\n    property txid:\n        def __get__(self):\n            return self._txid\n\n    property dbname:\n        def __get__(self):\n            return self._db.name\n\n    property reflection_cache:\n        def __get__(self):\n            return self._db.reflection_cache\n\n    property dbver:\n        def __get__(self):\n            if self._in_tx and self._in_tx_dbver:\n                return self._in_tx_dbver\n            return self._db.dbver\n\n    property schema_version:\n        def __get__(self):\n            if self._in_tx and self._in_tx_user_schema_version:\n                return self._in_tx_user_schema_version\n            return self._db.schema_version\n\n    @property\n    def server(self):\n        return self._db._index._server\n\n    @property\n    def tenant(self):\n        return self._db._index._tenant\n\n    def get_permissions(self) -> tuple[bool, Sequence[str]]:\n        if role_desc := self.tenant.get_roles().get(self._role_name):\n            return (\n                bool(role_desc.get('superuser')),\n                (role_desc.get('all_permissions') or ())\n            )\n        return False, ()\n\n    def get_role_capability(self) -> enums.Capability:\n        if capability := self.tenant.get_role_capabilities().get(\n            self._role_name\n        ):\n            return capability\n        return enums.Capability.NONE\n\n    cpdef in_tx(self):\n        return self._in_tx\n\n    cpdef in_tx_error(self):\n        return self._tx_error\n\n    cdef cache_compiled_query(self, object key, object query_unit_group):\n        assert query_unit_group.cacheable\n\n        if self._tx_error or self._in_tx_capabilities & DDL_CAPABILITIES:\n            return\n\n        self._db._cache_compiled_query(key, query_unit_group)\n\n    cdef lookup_compiled_query(self, object key):\n        if (\n            self._tx_error\n            or not self._query_cache_enabled\n            or self._in_tx_capabilities & DDL_CAPABILITIES\n        ):\n            return None\n\n        return self._db._eql_to_compiled.get(key, None)\n\n    cdef tx_error(self):\n        if self._in_tx:\n            self._tx_error = True\n\n    cdef start(self, query_unit):\n        if self._tx_error:\n            self.raise_in_tx_error()\n\n        if query_unit.tx_id is not None:\n            self._txid = query_unit.tx_id\n            self.start_tx()\n\n        if self._in_tx:\n            self._apply_in_tx(query_unit)\n\n    cdef start_tx(self):\n        state_serializer = self.get_state_serializer()\n        self._in_tx = True\n        self._in_tx_config = self._config\n        self._in_tx_globals = self._globals\n        self._in_tx_db_config = self._db.db_config\n        self._in_tx_modaliases = self._modaliases\n        self._in_tx_root_user_schema_pickle = self._db.user_schema_pickle\n        self._in_tx_user_schema_pickle = self._db.user_schema_pickle\n        self._in_tx_user_schema_version = self._db.schema_version\n        self._in_tx_global_schema_pickle = \\\n            self._db._index._global_schema_pickle\n        self._in_tx_user_config_spec = self._db.user_config_spec\n        self._in_tx_state_serializer = state_serializer\n        self._in_tx_dbver = self._db.dbver\n        self._in_tx_seq = self._db.tx_seq_begin_tx()\n\n    cdef _apply_in_tx(self, query_unit):\n        self._in_tx_capabilities |= query_unit.capabilities\n        if query_unit.system_config:\n            self._in_tx_with_sysconfig = True\n        if query_unit.database_config:\n            self._in_tx_with_dbconfig = True\n        if query_unit.has_set:\n            self._in_tx_with_set = True\n        if query_unit.user_schema is not None:\n            self._in_tx_dbver = next_dbver()\n            self._in_tx_user_schema_pickle = query_unit.user_schema\n            self._in_tx_user_schema_version = query_unit.user_schema_version\n            self._in_tx_user_config_spec = config.FlatSpec(\n                *query_unit.ext_config_settings\n            )\n        if query_unit.global_schema is not None:\n            self._in_tx_global_schema_pickle = query_unit.global_schema\n        if query_unit.tx_isolation_level:\n            self._in_tx_isolation_level = query_unit.tx_isolation_level\n\n    cdef start_implicit(self, query_unit):\n        if self._tx_error:\n            self.raise_in_tx_error()\n\n        if not self._in_tx:\n            self.start_tx()\n\n        self._apply_in_tx(query_unit)\n\n    cdef on_error(self):\n        self.tx_error()\n\n    cdef on_success(self, query_unit, new_types):\n        side_effects = 0\n\n        if not self._in_tx:\n            if query_unit.capabilities & DML_CAPABILITIES:\n                self._db.dml_queries_executed += 1\n            if new_types:\n                self._db._update_backend_ids(new_types)\n            if query_unit.user_schema is not None:\n                self._db._set_and_signal_new_user_schema(\n                    query_unit.user_schema,\n                    query_unit.user_schema_version,\n                    query_unit.extensions,\n                    query_unit.ext_config_settings,\n                    query_unit.feature_used_metrics,\n                    pickle.loads(query_unit.cached_reflection)\n                        if query_unit.cached_reflection is not None\n                        else None,\n                )\n                side_effects |= SideEffects.SchemaChanges\n            if query_unit.system_config:\n                side_effects |= SideEffects.InstanceConfigChanges\n            if query_unit.database_config:\n                side_effects |= SideEffects.DatabaseConfigChanges\n            if query_unit.create_db:\n                side_effects |= SideEffects.DatabaseChanges\n            if query_unit.drop_db:\n                side_effects |= SideEffects.DatabaseChanges\n            if query_unit.global_schema is not None:\n                side_effects |= SideEffects.GlobalSchemaChanges\n                self._db._index.update_global_schema(query_unit.global_schema)\n                self._db.tenant.set_roles(query_unit.roles)\n        else:\n            if new_types:\n                self._in_tx_new_types.update(new_types)\n\n        if query_unit.modaliases is not None:\n            self.set_modaliases(query_unit.modaliases)\n\n        if query_unit.tx_commit:\n            if not self._in_tx:\n                # This shouldn't happen because compiler has\n                # checks around that.\n                raise errors.InternalServerError(\n                    '\"commit\" outside of a transaction')\n            self._config = self._in_tx_config\n            self._modaliases = self._in_tx_modaliases\n            self._globals = self._in_tx_globals\n\n            if self._in_tx_capabilities & DML_CAPABILITIES:\n                self._db.dml_queries_executed += 1\n            if self._in_tx_new_types:\n                self._db._update_backend_ids(self._in_tx_new_types)\n            if query_unit.user_schema is not None:\n                self._db._set_and_signal_new_user_schema(\n                    query_unit.user_schema,\n                    query_unit.user_schema_version,\n                    query_unit.extensions,\n                    query_unit.ext_config_settings,\n                    query_unit.feature_used_metrics,  # XXX? does this get set?\n                    pickle.loads(query_unit.cached_reflection)\n                        if query_unit.cached_reflection is not None\n                        else None,\n                )\n                side_effects |= SideEffects.SchemaChanges\n            if self._in_tx_with_sysconfig:\n                side_effects |= SideEffects.InstanceConfigChanges\n            if self._in_tx_with_dbconfig:\n                side_effects |= SideEffects.DatabaseConfigChanges\n            if query_unit.global_schema is not None:\n                side_effects |= SideEffects.GlobalSchemaChanges\n                self._db._index.update_global_schema(query_unit.global_schema)\n                self._db.tenant.set_roles(query_unit.roles)\n\n            self._reset_tx_state()\n\n        elif query_unit.tx_rollback:\n            # Note that we might not be in a transaction as we allow\n            # ROLLBACKs outside of transaction blocks (just like Postgres).\n            # TODO: That said, we should send a *warning* when a ROLLBACK\n            # is executed outside of a tx.\n            self._reset_tx_state()\n\n        return side_effects\n\n    cdef commit_implicit_tx(\n        self,\n        user_schema,\n        extensions,\n        ext_config_settings,\n        global_schema,\n        roles,\n        cached_reflection,\n        feature_used_metrics,\n    ):\n        assert self._in_tx\n        side_effects = 0\n\n        self._config = self._in_tx_config\n        self._modaliases = self._in_tx_modaliases\n        self._globals = self._in_tx_globals\n\n        if self._in_tx_new_types:\n            self._db._update_backend_ids(self._in_tx_new_types)\n        if user_schema is not None:\n            self._db._set_and_signal_new_user_schema(\n                user_schema,\n                self._in_tx_user_schema_version,\n                extensions,\n                ext_config_settings,\n                feature_used_metrics,\n                pickle.loads(cached_reflection)\n                    if cached_reflection is not None\n                    else None\n            )\n            side_effects |= SideEffects.SchemaChanges\n        if self._in_tx_with_sysconfig:\n            side_effects |= SideEffects.InstanceConfigChanges\n        if self._in_tx_with_dbconfig:\n            side_effects |= SideEffects.DatabaseConfigChanges\n        if global_schema is not None:\n            side_effects |= SideEffects.GlobalSchemaChanges\n            self._db._index.update_global_schema(global_schema)\n            self._db.tenant.set_roles(roles)\n\n        self._reset_tx_state()\n        return side_effects\n\n    cdef config_lookup(self, name):\n        return self.server.config_lookup(\n            name,\n            self.get_session_config(),\n            self.get_database_config(),\n            self.get_system_config(),\n        )\n\n    async def recompile_cached_queries(\n        self,\n        user_schema,\n        schema_version,\n        send_log_message: typing.Callable[[int, str], None] | None = None,\n    ):\n        compiler_pool = self.server.get_compiler_pool()\n        compile_concurrency = max(1, compiler_pool.get_size_hint() // 2)\n        concurrency_control = asyncio.Semaphore(compile_concurrency)\n        rv = []\n\n        recompile_timeout = self.config_lookup(\n            \"auto_rebuild_query_cache_timeout\",\n        )\n\n        loop = asyncio.get_running_loop()\n        t0 = loop.time()\n        if recompile_timeout is not None:\n            stop_time = t0 + recompile_timeout.to_microseconds() / 1e6\n        else:\n            stop_time = None\n\n        async def recompile_request(query_req: rpc.CompilationRequest):\n            async with concurrency_control:\n                try:\n                    if stop_time is not None and loop.time() > stop_time:\n                        return\n\n                    database_config = self.get_database_config()\n                    system_config = self.get_compilation_system_config()\n                    query_req = copy.copy(query_req)\n                    query_req.set_schema_version(schema_version)\n                    query_req.set_database_config(database_config)\n                    query_req.set_system_config(system_config)\n                    async with asyncio.timeout_at(stop_time):\n                        unit_group, _, _ = await compiler_pool.compile(\n                            self.dbname,\n                            user_schema,\n                            self.get_global_schema_pickle(),\n                            self.reflection_cache,\n                            database_config,\n                            system_config,\n                            query_req.serialize(),\n                            \"<unknown>\",\n                            client_id=self.tenant.client_id,\n                            client_name=self.tenant.get_instance_name(),\n                        )\n                except Exception:\n                    # ignore cache entry that cannot be recompiled\n                    pass\n                else:\n                    rv.append((query_req, unit_group))\n\n        async with asyncio.TaskGroup() as g:\n            req: rpc.CompilationRequest\n            cnt = 0\n            # Reversed so that we compile more recently used first.\n            for req, grp in reversed(self._db._eql_to_compiled.items()):\n                if (\n                    len(grp) == 1\n                    # Only recompile queries from the *latest* version,\n                    # to avoid quadratic slowdown problems.\n                    and req.schema_version == self.schema_version\n                    # SQL queries require _amend_typedesc_in_sql() with a\n                    # backend connection, which is not available here.\n                    and req.input_language != enums.InputLanguage.SQL\n                ):\n                    cnt += 1\n                    g.create_task(recompile_request(req))\n\n            if send_log_message:\n                send_log_message(\n                    errors.MigrationStatusMessage.get_code(),\n                    f'Recompiling {cnt} cached queries'\n                )\n\n        return rv\n\n    async def apply_config_ops(self, conn, ops):\n        settings = self.get_config_spec()\n\n        for op in ops:\n            if op.scope is config.ConfigScope.INSTANCE:\n                assert conn is not None\n                await self._db._index.apply_system_config_op(conn, op)\n            elif op.scope is config.ConfigScope.DATABASE:\n                self.set_database_config(\n                    op.apply(settings, self.get_database_config()),\n                )\n            elif op.scope is config.ConfigScope.SESSION:\n                self.check_session_config_perms([op.setting_name])\n                self.set_session_config(\n                    op.apply(settings, self.get_session_config()),\n                )\n            elif op.scope is config.ConfigScope.GLOBAL:\n                self.set_globals(\n                    op.apply(settings, self.get_globals()),\n                )\n\n    @staticmethod\n    def raise_in_tx_error():\n        raise errors.TransactionError(\n            'current transaction is aborted, '\n            'commands ignored until end of transaction block'\n        ) from None\n\n    async def parse(\n        self,\n        query_req: rpc.CompilationRequest,\n        cached_globally: bint = False,\n        use_metrics: bint = True,\n        allow_capabilities: uint64_t = <uint64_t>compiler.Capability.ALL,\n        pgcon: pgcon.PGConnection | None = None,\n        tag: str | None = None,\n        send_log_message: typing.Callable[[int, str], None] | None = None,\n    ) -> CompiledQuery:\n        query_unit_group = None\n        if self._query_cache_enabled:\n            if cached_globally:\n                # WARNING: only set cached_globally to True when the query is\n                # strictly referring to only shared stable objects in user\n                # schema or anything from std schema, for example:\n                #     YES:  select ext::auth::UIConfig { ... }\n                #     NO:   select default::User { ... }\n                query_unit_group = (\n                    self.server.system_compile_cache.get(query_req)\n                )\n            else:\n                query_unit_group = self.lookup_compiled_query(query_req)\n\n            # Fast-path to skip all the locks if it's a cache HIT\n            if query_unit_group is not None:\n                return self.as_compiled(\n                    query_req, query_unit_group, use_metrics)\n\n        lock = None\n        schema_version = self.schema_version\n\n        # Lock on the query compilation to avoid other coroutines running\n        # the same compile and waste computational resources\n        if cached_globally:\n            lock_table = self.server.system_compile_cache_locks\n        else:\n            lock_table = self._db._cache_locks\n        while True:\n            # We need a loop here because schema_version is a part of the key,\n            # there could be a DDL while we're waiting for the lock.\n            lock = lock_table.get(query_req)\n            if lock is None:\n                lock = asyncio.Lock()\n                lock_table[query_req] = lock\n            await lock.acquire()\n            if self.schema_version == schema_version:\n                break\n            else:\n                lock.release()\n                if not lock._waiters:\n                    del lock_table[query_req]\n                schema_version = self.schema_version\n                # Updating the schema_version will make query_req a new key\n                query_req.set_schema_version(schema_version)\n\n        try:\n            # Check the cache again with the lock acquired\n            if self._query_cache_enabled:\n                if cached_globally:\n                    query_unit_group = (\n                        self.server.system_compile_cache.get(query_req)\n                    )\n                else:\n                    query_unit_group = self.lookup_compiled_query(query_req)\n                if query_unit_group is not None:\n                    return self.as_compiled(\n                        query_req, query_unit_group, use_metrics)\n\n            try:\n                query_unit_group = await self._compile(query_req)\n            except (errors.EdgeQLSyntaxError, errors.InternalServerError):\n                raise\n            except errors.EdgeDBError:\n                if self.in_tx_error():\n                    # Because we are in an error state it's more reasonable\n                    # to fail with TransactionError(\"commands ignored\")\n                    # rather than with a potentially more cryptic error.\n                    # An exception from this rule are syntax errors and\n                    # ISEs, because these could arise while the user is\n                    # trying to properly rollback this failed transaction.\n                    self.raise_in_tx_error()\n                else:\n                    raise\n\n            self.check_capabilities(\n                query_unit_group,\n                allow_capabilities,\n                errors.DisabledCapabilityError,\n                \"disabled by the client\",\n                # In parse, we don't raise any errors based on\n                # unsafe_isolation_dangers. We do report them to the\n                # client in an annotation, though.\n                unsafe_isolation_dangers=None,\n            )\n            self._check_in_tx_error(query_unit_group)\n\n            if query_req.input_language is enums.InputLanguage.SQL:\n                if len(query_unit_group) > 1:\n                    raise errors.UnsupportedFeatureError(\n                        \"multi-statement SQL scripts are not supported yet\"\n                    )\n\n                if pgcon is None:\n                    raise errors.InternalServerError(\n                        \"a valid backend connection is required to fully \"\n                        \"compile a query in SQL mode\",\n                    )\n                await self._amend_typedesc_in_sql(\n                    query_req,\n                    query_unit_group,\n                    pgcon,\n                    tag,\n                )\n\n            if self._query_cache_enabled and query_unit_group.cacheable:\n                if cached_globally:\n                    self.server.system_compile_cache[query_req] = (\n                        query_unit_group\n                    )\n                else:\n                    self.cache_compiled_query(query_req, query_unit_group)\n        finally:\n            if lock is not None:\n                lock.release()\n                if not lock._waiters:\n                    del lock_table[query_req]\n\n        recompiled_cache = None\n        if (\n            not self.in_tx()\n            or len(query_unit_group) > 0\n            and query_unit_group[0].tx_commit\n        ):\n            # Recompile all cached queries if:\n            #  * Issued a DDL or committing a tx with DDL (recompilation\n            #    before in-tx DDL needs to fix _in_tx_capabilities caching 1st)\n            #  * Config.auto_rebuild_query_cache is turned on\n            #\n            # Ideally we should compute the proper user_schema, database_config\n            # and system_config for recompilation from server/compiler.py with\n            # proper handling of config values. For now we just use the values\n            # in the current dbview and not support certain marginal cases.\n            user_schema = None\n            user_schema_version = None\n            for unit in query_unit_group:\n                if unit.tx_rollback:\n                    break\n                if unit.user_schema:\n                    user_schema = unit.user_schema\n                    user_schema_version = unit.user_schema_version\n            if user_schema and not self.config_lookup(\n                \"auto_rebuild_query_cache\",\n            ):\n                user_schema = None\n            if user_schema:\n                recompiled_cache = await self.recompile_cached_queries(\n                    user_schema,\n                    user_schema_version,\n                    send_log_message=send_log_message,\n                )\n\n        if use_metrics:\n            if query_req.input_language is enums.InputLanguage.EDGEQL:\n                metrics.edgeql_query_compilations.inc(\n                    1.0, self.tenant.get_instance_name(), 'compiler'\n                )\n            else:\n                metrics.sql_compilations.inc(\n                    1.0, self.tenant.get_instance_name()\n                )\n\n        source = query_req.source\n        if query_unit_group.force_non_normalized:\n            source = source.denormalized()\n        return CompiledQuery(\n            query_unit_group=query_unit_group,\n            first_extra=source.first_extra(),\n            extra_counts=source.extra_counts(),\n            extra_blobs=source.extra_blobs(),\n            extra_formatted_as_text=source.extra_formatted_as_text(),\n            extra_type_oids=source.extra_type_oids(),\n            request=query_req,\n            recompiled_cache=recompiled_cache,\n        )\n\n    async def _amend_typedesc_in_sql(\n        self,\n        query_req: rpc.CompilationRequest,\n        qug: dbstate.QueryUnitGroup,\n        pgcon: pgcon.PGConnection,\n        tag: str | None,\n    ) -> None:\n        # The SQL QueryUnitGroup as initially returned from the compiler\n        # is missing the input/output type descriptors because we currently\n        # don't run static SQL type inference.  To mend that we ask Postgres\n        # to infer the the result types (as an OID tuple) and then use\n        # our OID -> scalar type mapping to construct an EdgeQL free shape with\n        # corresponding properties which we then send to the compiler to\n        # compute the type descriptors.\n        to_describe = []\n\n        desc_map = {}\n        source = query_req.source\n        if qug.force_non_normalized:\n            source = source.denormalized()\n\n        first_extra = source.first_extra()\n        num_injected_params = 0\n        if qug.globals is not None:\n            num_injected_params += len(qug.globals)\n        if qug.permissions is not None:\n            num_injected_params += len(qug.permissions)\n        if first_extra is not None:\n            extra_type_oids = source.extra_type_oids()\n            all_type_oids = [0] * first_extra + extra_type_oids\n            num_injected_params += len(extra_type_oids)\n        else:\n            all_type_oids = []\n\n        for i, query_unit in enumerate(qug):\n            intro_sql = query_unit.introspection_sql\n            if intro_sql is None:\n                intro_sql = query_unit.sql\n            if tag is not None:\n                # maintenance reminder: please also update make_query_prefix()\n                tag_json = json.dumps({\"tag\": tag})\n                intro_sql = b''.join([\n                    b'-- ',\n                    tag_json.encode(defines.EDGEDB_ENCODING),\n                    b'\\n',\n                    intro_sql,\n                ])\n            try:\n                param_desc, result_desc = await pgcon.sql_describe(\n                    intro_sql, all_type_oids)\n            except pgerror.BackendError as ex:\n                ex._from_sql = True\n                if 'P' in ex.fields:\n                    ex.fields['P'] = str(\n                        int(ex.fields['P']) - query_unit.sql_prefix_len\n                    )\n                if query_unit.source_map:\n                    ex._source_map = query_unit.source_map\n\n                raise\n\n            result_types = []\n            for col, toid in result_desc:\n                edb_type_id = self._db.backend_oid_to_id.get(toid)\n                if edb_type_id is None:\n                    raise errors.UnsupportedFeatureError(\n                        f\"unsupported SQL type in column \\\"{col}\\\" \"\n                        f\"with type OID {toid}\"\n                    )\n\n                result_types.append((col, edb_type_id))\n            params = []\n            if num_injected_params:\n                param_desc = param_desc[:-num_injected_params]\n            for pi, toid in enumerate(param_desc):\n                edb_type_id = self._db.backend_oid_to_id.get(toid)\n                if edb_type_id is None:\n                    raise errors.UnsupportedFeatureError(\n                        f\"unsupported type in SQL parameter ${pi} \"\n                        f\"with type OID {toid}\"\n                    )\n\n                params.append(edb_type_id)\n\n            to_describe.append((params, result_types))\n            desc_map[len(to_describe) - 1] = i\n\n        if to_describe:\n            desc_qug = await self._compile_sql_descriptors(\n                query_req, to_describe)\n\n            for i, desc_qu in enumerate(desc_qug):\n                qu_i = desc_map[i]\n\n                if query_req.output_format is not enums.OutputFormat.NONE:\n                    qug[qu_i].out_type_data = desc_qu[1][0]\n                    qug[qu_i].out_type_id = desc_qu[1][1]\n\n                qug[qu_i].in_type_data = desc_qu[0][0]\n                qug[qu_i].in_type_id = desc_qu[0][1]\n                qug[qu_i].in_type_args = desc_qu[0][2]\n                qug[qu_i].in_type_args_real_count = desc_qu[0][3]\n\n            # XXX We don't support SQL scripts just yet, so for now\n            # we can just copy the last QU's descriptors and\n            # apply them to the whole group (IOW a group is really\n            # a group of ONE now.)\n            # In near future we'll need to properly implement arg\n            # remap.\n            if query_req.output_format is not enums.OutputFormat.NONE:\n                qug.out_type_data = desc_qug[-1][1][0]\n                qug.out_type_id = desc_qug[-1][1][1]\n\n            qug.in_type_data = desc_qug[-1][0][0]\n            qug.in_type_id = desc_qug[-1][0][1]\n            qug.in_type_args = desc_qug[-1][0][2]\n            qug.in_type_args_real_count = desc_qug[-1][0][3]\n\n    cdef inline _check_in_tx_error(self, query_unit_group):\n        if self.in_tx_error():\n            # The current transaction is aborted, so we must fail\n            # all commands except ROLLBACK or ROLLBACK TO SAVEPOINT.\n            first = query_unit_group[0]\n            if (\n                not (\n                    first.tx_rollback\n                    or first.tx_savepoint_rollback\n                    or first.tx_abort_migration\n                ) or len(query_unit_group) > 1\n            ):\n                self.raise_in_tx_error()\n\n    cdef as_compiled(self, query_req, query_unit_group, bint use_metrics=True):\n        cdef use_pending_func_cache = False\n        if query_unit_group.cache_state == 1:\n            use_pending_func_cache = (\n                not self._in_tx_seq\n                or self._in_tx_seq > query_unit_group.tx_seq_id\n            )\n\n        self._check_in_tx_error(query_unit_group)\n        if use_metrics:\n            metrics.edgeql_query_compilations.inc(\n                1.0, self.tenant.get_instance_name(), 'cache'\n            )\n\n        source = query_req.source\n        return CompiledQuery(\n            query_unit_group=query_unit_group,\n            first_extra=source.first_extra(),\n            extra_counts=source.extra_counts(),\n            extra_blobs=source.extra_blobs(),\n            extra_formatted_as_text=source.extra_formatted_as_text(),\n            extra_type_oids=source.extra_type_oids(),\n            use_pending_func_cache=use_pending_func_cache,\n        )\n\n    async def _compile(\n        self,\n        query_req: rpc.CompilationRequest,\n    ) -> dbstate.QueryUnitGroup:\n        compiler_pool = self._db._index._server.get_compiler_pool()\n\n        started_at = time.monotonic()\n        try:\n            if self.in_tx():\n                result = await compiler_pool.compile_in_tx(\n                    self.dbname,\n                    self._in_tx_root_user_schema_pickle,\n                    self.txid,\n                    self._last_comp_state,\n                    self._last_comp_state_id,\n                    query_req.serialize(),\n                    query_req.source.text(),\n                    self.in_tx_error(),\n                    client_id=self.tenant.client_id,\n                    client_name=self.tenant.get_instance_name(),\n                )\n            else:\n                result = await compiler_pool.compile(\n                    self.dbname,\n                    self.get_user_schema_pickle(),\n                    self.get_global_schema_pickle(),\n                    self.reflection_cache,\n                    self.get_database_config(),\n                    self.get_compilation_system_config(),\n                    query_req.serialize(),\n                    query_req.source.text(),\n                    client_id=self.tenant.client_id,\n                    client_name=self.tenant.get_instance_name(),\n                )\n        finally:\n            metrics.edgeql_query_compilation_duration.observe(\n                time.monotonic() - started_at,\n                self.tenant.get_instance_name(),\n            )\n            metrics.query_compilation_duration.observe(\n                time.monotonic() - started_at,\n                self.tenant.get_instance_name(),\n                \"edgeql\",\n            )\n\n        unit_group, self._last_comp_state, self._last_comp_state_id = result\n\n        return unit_group\n\n    async def _compile_sql_descriptors(\n        self,\n        query_req: rpc.CompilationRequest,\n        types_in_out: defines.ProtocolVersion,\n    ) -> dbstate.QueryUnitGroup:\n        compiler_pool = self._db._index._server.get_compiler_pool()\n\n        cfg_ser = self.server.compilation_config_serializer\n        req = rpc.CompilationRequest(\n            source=rpc.SQLParamsSource(types_in_out),\n            protocol_version=query_req.protocol_version,\n            schema_version=query_req.schema_version,\n            input_language=enums.InputLanguage.SQL_PARAMS,\n            compilation_config_serializer=cfg_ser,\n        )\n\n        return await self._compile(req)\n\n    cdef check_capabilities(\n        self,\n        query_unit,\n        allowed_capabilities,\n        error_constructor,\n        reason,\n        unsafe_isolation_dangers,\n    ):\n        query_capabilities = query_unit.capabilities\n\n        if query_capabilities & ~self._capability_mask:\n            # _capability_mask is currently only used for system database\n            raise query_capabilities.make_error(\n                self._capability_mask,\n                errors.UnsupportedCapabilityError,\n                \"system database is read-only\",\n            )\n\n        if (query_capabilities & PROTO_CAPS) & ~allowed_capabilities:\n            raise query_capabilities.make_error(\n                allowed_capabilities,\n                error_constructor,\n                reason,\n            )\n\n        role_capability = self.get_role_capability()\n        if query_capabilities & ~role_capability:\n            raise query_capabilities.make_error(\n                role_capability,\n                error_constructor,\n                f\"role {self._role_name} does not have permission\",\n            )\n\n        if self.tenant.is_readonly():\n            if query_capabilities & enums.Capability.WRITE:\n                readiness_reason = self.tenant.get_readiness_reason()\n                msg = \"the server is currently in read-only mode\"\n                if readiness_reason:\n                    msg = f\"{msg}: {readiness_reason}\"\n                raise query_capabilities.make_error(\n                    ~enums.Capability.WRITE,\n                    errors.DisabledCapabilityError,\n                    msg,\n                )\n\n        if query_unit.required_permissions:\n            is_superuser, permissions = self.get_permissions()\n            if not is_superuser:\n                for perm in query_unit.required_permissions:\n                    if perm not in permissions:\n                        missing = sorted(\n                            set(query_unit.required_permissions)\n                            - set(permissions)\n                        )\n                        plural = 's' if len(missing) > 1 else ''\n                        raise errors.DisabledCapabilityError(\n                            f'role {self._role_name} does not have required '\n                            f'permission{plural}: {\", \".join(missing)}'\n                        )\n\n        has_write = query_capabilities & enums.Capability.WRITE\n        if has_write and unsafe_isolation_dangers:\n            isolation = None\n            # Sigh! We have two different isolation level enumerations!\n            if self.in_tx():\n                isolation = self._in_tx_isolation_level\n            else:\n                isolation = self.config_lookup(\n                    \"default_transaction_isolation\"\n                )\n                if isolation and isolation.to_str() == \"RepeatableRead\":\n                    isolation = (\n                        qltypes.TransactionIsolationLevel.REPEATABLE_READ)\n                else:\n                    isolation = qltypes.TransactionIsolationLevel.SERIALIZABLE\n\n            not_serializable = (\n                isolation != qltypes.TransactionIsolationLevel.SERIALIZABLE\n            )\n            if not_serializable:\n                body = '\\n'.join(\n                    ' - ' + str(e) for e in unsafe_isolation_dangers\n                )\n                raise errors.UnsafeIsolationLevelError(\n                    f\"Can not execute query with transaction isolation level \"\n                    f\"{isolation} because: \\n{body}\",\n                )\n\n        if not self.in_tx() and has_write:\n            access_mode = self.config_lookup(\"default_transaction_access_mode\")\n            if access_mode and access_mode.to_str() == \"ReadOnly\":\n                raise query_capabilities.make_error(\n                    ~enums.Capability.WRITE,\n                    errors.TransactionError,\n                    \"default_transaction_access_mode is set to ReadOnly\",\n                )\n\n    async def reload_state_serializer(self):\n        # This should only happen once when a different protocol version is\n        # used after schema change, or non-current version of protocol is used\n        # for the first time after database introspection.  Because such cases\n        # are rare, we'd rather do it lazily here than enumerating all protocol\n        # versions making several serializers in every schema change.\n        compiler_pool = self._db._index._server.get_compiler_pool()\n        state_serializer = await compiler_pool.make_state_serializer(\n            self._protocol_version,\n            self.get_user_schema_pickle(),\n            self.get_global_schema_pickle(),\n        )\n        self.set_state_serializer(state_serializer)\n\n\ncdef class DatabaseIndex:\n\n    def __init__(\n        self,\n        tenant,\n        *,\n        std_schema,\n        global_schema_pickle,\n        sys_config,\n        default_sysconfig,  # system config without system override\n        sys_config_spec,\n    ):\n        self._dbs = {}\n        self._server = tenant.server\n        self._tenant = tenant\n        self._std_schema = std_schema\n        self._global_schema_pickle = global_schema_pickle\n        self._default_sysconfig = default_sysconfig\n        self._sys_config_spec = sys_config_spec\n        self.update_sys_config(sys_config)\n        self._cached_compiler_args = None\n\n    def count_connections(self, dbname: str):\n        try:\n            db = self._dbs[dbname]\n        except KeyError:\n            return 0\n\n        return sum(1 for dbv in (<Database>db)._views if not dbv.is_transient)\n\n    def get_sys_config(self):\n        return self._sys_config\n\n    def get_compilation_system_config(self):\n        return self._comp_sys_config\n\n    def update_sys_config(self, sys_config):\n        cdef Database db\n        for db in self._dbs.values():\n            db.dbver = next_dbver()\n\n        with self._default_sysconfig.mutate() as mm:\n            mm.update(sys_config)\n            sys_config = mm.finish()\n        self._sys_config = sys_config\n        self._comp_sys_config = config.get_compilation_config(\n            sys_config, spec=self._sys_config_spec)\n        self.invalidate_caches()\n\n    def has_db(self, dbname):\n        return dbname in self._dbs\n\n    def get_db(self, dbname):\n        try:\n            return self._dbs[dbname]\n        except KeyError:\n            raise errors.UnknownDatabaseError(\n                f'database branch {dbname!r} does not exist')\n\n    def maybe_get_db(self, dbname):\n        return self._dbs.get(dbname)\n\n    def get_global_schema_pickle(self):\n        return self._global_schema_pickle\n\n    def update_global_schema(self, global_schema_pickle):\n        self._global_schema_pickle = global_schema_pickle\n        self.invalidate_caches()\n\n    def register_db(\n        self,\n        dbname,\n        *,\n        user_schema_pickle,\n        schema_version,\n        db_config,\n        reflection_cache,\n        backend_ids,\n        extensions,\n        ext_config_settings,\n        early=False,\n        feature_used_metrics=None,\n    ):\n        cdef Database db\n        db = self._dbs.get(dbname)\n        if db is not None:\n            db._set_and_signal_new_user_schema(\n                user_schema_pickle,\n                schema_version,\n                extensions,\n                ext_config_settings,\n                feature_used_metrics,\n                reflection_cache,\n                backend_ids,\n                db_config,\n                not early,\n            )\n        else:\n            db = Database(\n                self,\n                dbname,\n                user_schema_pickle=user_schema_pickle,\n                schema_version=schema_version,\n                db_config=db_config,\n                reflection_cache=reflection_cache,\n                backend_ids=backend_ids,\n                extensions=extensions,\n                ext_config_settings=ext_config_settings,\n                feature_used_metrics=feature_used_metrics,\n            )\n            self._dbs[dbname] = db\n            if not early:\n                db.start_stop_extensions()\n        self.set_current_branches()\n        return db\n\n    def unregister_db(self, dbname):\n        db = self._dbs.pop(dbname)\n        db.stop()\n        self.set_current_branches()\n\n    cdef inline set_current_branches(self):\n        metrics.current_branches.set(\n            sum(\n                dbname != defines.EDGEDB_SYSTEM_DB\n                for dbname in self._dbs\n            ),\n            self._tenant.get_instance_name(),\n        )\n        metrics.current_introspected_branches.set(\n            sum(\n                dbname != defines.EDGEDB_SYSTEM_DB\n                and db.user_schema_pickle is not None\n                for dbname, db in self._dbs.items()\n            ),\n            self._tenant.get_instance_name(),\n        )\n\n    def iter_dbs(self):\n        return iter(self._dbs.values())\n\n    async def _save_system_overrides(self, conn, spec):\n        data = config.to_json(\n            spec,\n            self._sys_config,\n            setting_filter=lambda v: v.source == 'system override',\n            include_source=False,\n        )\n        block = dbops.PLTopBlock()\n        metadata = {'sysconfig': json.loads(data)}\n        if self._tenant.get_backend_runtime_params().has_create_database:\n            dbops.UpdateMetadata(\n                dbops.Database(\n                    name=self._tenant.get_pg_dbname(defines.EDGEDB_SYSTEM_DB),\n                ),\n                metadata,\n            ).generate(block)\n        else:\n            dbops.UpdateSingleDBMetadata(\n                defines.EDGEDB_SYSTEM_DB, metadata\n            ).generate(block)\n        await conn.sql_execute(block.to_string().encode())\n\n    async def apply_system_config_op(self, conn, op):\n        spec = self._sys_config_spec\n\n        op_value = op.get_setting(spec)\n        if op.opcode is not None:\n            allow_missing = (\n                op.opcode is config.OpCode.CONFIG_REM\n                or op.opcode is config.OpCode.CONFIG_RESET\n            )\n            op_value = op.coerce_value(\n                spec, op_value, allow_missing=allow_missing)\n\n        # _save_system_overrides *must* happen before\n        # the callbacks below, because certain config changes\n        # may cause the backend connection to drop.\n        self.update_sys_config(\n            op.apply(spec, self._sys_config)\n        )\n\n        await self._save_system_overrides(conn, spec)\n\n        if op.opcode is config.OpCode.CONFIG_ADD:\n            await self._server._on_system_config_add(op.setting_name, op_value)\n        elif op.opcode is config.OpCode.CONFIG_REM:\n            await self._server._on_system_config_rem(op.setting_name, op_value)\n        elif op.opcode is config.OpCode.CONFIG_SET:\n            await self._server._on_system_config_set(op.setting_name, op_value)\n        elif op.opcode is config.OpCode.CONFIG_RESET:\n            await self._server._on_system_config_reset(op.setting_name)\n        else:\n            raise errors.UnsupportedFeatureError(\n                f'unsupported config operation: {op.opcode}')\n\n        if op.opcode is config.OpCode.CONFIG_ADD:\n            await self._server._after_system_config_add(\n                op.setting_name, op_value)\n        elif op.opcode is config.OpCode.CONFIG_REM:\n            await self._server._after_system_config_rem(\n                op.setting_name, op_value)\n        elif op.opcode is config.OpCode.CONFIG_SET:\n            await self._server._after_system_config_set(\n                op.setting_name, op_value)\n        elif op.opcode is config.OpCode.CONFIG_RESET:\n            await self._server._after_system_config_reset(\n                op.setting_name)\n\n    def new_view(\n        self,\n        dbname: str,\n        *,\n        query_cache: bool,\n        protocol_version,\n        role_name: str,\n    ):\n        db = self.get_db(dbname)\n        return (<Database>db)._new_view(\n            query_cache, protocol_version, role_name\n        )\n\n    def remove_view(self, view: DatabaseConnectionView):\n        db = self.get_db(view.dbname)\n        return (<Database>db)._remove_view(view)\n\n    cdef invalidate_caches(self):\n        self._cached_compiler_args = None\n\n    def get_cached_compiler_args(self):\n        if self._cached_compiler_args is None:\n            self._cached_compiler_args = (\n                self._global_schema_pickle, self._comp_sys_config\n            )\n        return self._cached_compiler_args\n\n    def lookup_config(self, name: str):\n        return config.lookup(\n            name,\n            self._sys_config,\n            spec=self._sys_config_spec,\n        )\n"
  },
  {
    "path": "edb/server/defines.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import TypeAlias\n\nimport enum\n\nfrom edb import buildmeta\n\nfrom edb.common import enum as s_enum\nfrom edb.schema import defines as s_def\n\n\nEDGEDB_PORT = 5656\nEDGEDB_REMOTE_COMPILER_PORT = 5660\nEDGEDB_SUPERGROUP = 'edgedb_supergroup'\nEDGEDB_SUPERUSER = s_def.EDGEDB_SUPERUSER\nEDGEDB_OLD_SUPERUSER = s_def.EDGEDB_OLD_SUPERUSER\nEDGEDB_TEMPLATE_DB = s_def.EDGEDB_TEMPLATE_DB\nEDGEDB_OLD_DEFAULT_DB = 'edgedb'\nEDGEDB_SUPERUSER_DB = 'main'\nEDGEDB_SYSTEM_DB = s_def.EDGEDB_SYSTEM_DB\nEDGEDB_ENCODING = 'utf-8'\nEDGEDB_VISIBLE_METADATA_PREFIX = r'Gel metadata follows, do not modify.\\n'\n\nEDGEDB_SPECIAL_DBS = s_def.EDGEDB_SPECIAL_DBS\n\nEDGEDB_CATALOG_VERSION = buildmeta.EDGEDB_CATALOG_VERSION\nMIN_POSTGRES_VERSION = (14, 0)\n\n# Resource limit on open FDs for the server process.\n# By default, at least on macOS, the max number of open FDs\n# is 256, which is low and can cause 'edb test' to hang.\n# We try to bump the rlimit on server start if pemitted.\nEDGEDB_MIN_RLIMIT_NOFILE = 2048\n\nBACKEND_CONNECTIONS_MIN = 4\nBACKEND_COMPILER_POOL_SIZE_MIN = 1\n\n# The time in seconds to wait before restarting the template compiler process\n# after it exits unexpectedly.\nBACKEND_COMPILER_TEMPLATE_PROC_RESTART_INTERVAL = 1\n\n_MAX_QUERIES_CACHE_SYSTEM = 1000\n\n_QUERY_ROLLING_AVG_LEN = 10\n_QUERIES_ROLLING_AVG_LEN = 300\n\nDEFAULT_MODULE_ALIAS = 'default'\n\n# The maximum length of a Unix socket relative to runstate dir.\n# 21 is the length of the longest socket we might use, which\n# is the admin socket (.s.EDGEDB.admin.xxxxx).\nMAX_UNIX_SOCKET_PATH_LENGTH = 21\n\n# 104 is the maximum Unix socket path length on BSD/Darwin, whereas\n# Linux is constrained to 108.\nMAX_RUNSTATE_DIR_PATH = 104 - MAX_UNIX_SOCKET_PATH_LENGTH - 1\n\nHTTP_PORT_QUERY_CACHE_SIZE = 1000\n\n# The time in seconds the Gel server shall wait between retries to connect\n# to the system database after the connection was broken during runtime.\nSYSTEM_DB_RECONNECT_INTERVAL = 1\n\nProtocolVersion: TypeAlias = tuple[int, int]\n\nMIN_PROTOCOL: ProtocolVersion = (1, 0)\nCURRENT_PROTOCOL: ProtocolVersion = (3, 0)\n\n# Emulated PG binary protocol\nPOSTGRES_PROTOCOL: ProtocolVersion = (-3, 0)\n\nMIN_SUGGESTED_CLIENT_POOL_SIZE = 10\nMAX_SUGGESTED_CLIENT_POOL_SIZE = 100\n\n_TLS_CERT_RELOAD_MAX_RETRIES = 5\n_TLS_CERT_RELOAD_EXP_INTERVAL = 0.1\n\nPGEXT_POSTGRES_VERSION = 13.9\nPGEXT_POSTGRES_VERSION_NUM = 130009\n\n# The time in seconds the Gel server will wait for a tenant to be gracefully\n# shutdown when removed from a multi-tenant host.\nMULTITENANT_TENANT_DESTROY_TIMEOUT = 30\n\n\nclass TxIsolationLevel(s_enum.StrEnum):\n    RepeatableRead = 'REPEATABLE READ'\n    Serializable = 'SERIALIZABLE'\n\n\n# Mapping to the backend `edb_stat_statements.stmt_type` values,\n# as well as `sys::QueryType` in edb/lib/sys.edgeql\nclass QueryType(enum.IntEnum):\n    EdgeQL = 1\n    SQL = 2\n"
  },
  {
    "path": "edb/server/ha/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n"
  },
  {
    "path": "edb/server/ha/adaptive.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import Optional\n\nimport asyncio\nimport enum\nimport logging\nimport os\n\nfrom edb.server import metrics\n\nfrom . import base\n\n\nUNHEALTHY_MIN_TIME = int(os.getenv(\n    'EDGEDB_SERVER_BACKEND_ADAPTIVE_HA_UNHEALTHY_MIN_TIME', 30\n))\nUNEXPECTED_DISCONNECTS_THRESHOLD = int(os.getenv(\n    'EDGEDB_SERVER_BACKEND_ADAPTIVE_HA_DISCONNECT_PERCENT', 60\n)) / 100\n\nlogger = logging.getLogger(\"edb.pgcluster\")\n\n\nclass State(enum.Enum):\n    HEALTHY = 1\n    UNHEALTHY = 2\n    FAILOVER = 3\n\n\nclass AdaptiveHASupport:\n    # Adaptive HA support is used to detect HA backends that does not actively\n    # send clear failover signals to EdgeDB. It can be enabled through command\n    # line argument --enable-backend-adaptive-ha.\n    #\n    # This class evaluates the events on the backend connection pool into 3\n    # states representing the status of the backend:\n    #\n    #   * Healthy - all is good\n    #   * Unhealthy - a staging state before failover\n    #   * Failover - backend failover is in process\n    #\n    # When entering Unhealthy state, we will start to count events for a\n    # threshold; when reached, we'll switch to Failover state - that means we\n    # will actively disconnect all backend connections and wait for sys_pgcon\n    # to reconnect. In any of the 3 states, client connections will not be\n    # dropped. Whether the clients could issue queries is irrelevant to the 3\n    # states - `BackendUnavailableError` or `BackendInFailoverError` is only\n    # raised if the sys_pgcon is broken. But even with that said,\n    # `BackendUnavailableError` is only seen in Unhealthy (not always), and\n    # Failover always means `BackendInFailoverError` for any queries.\n    #\n    # Rules of state switches:\n    #\n    # Unhealthy -> Healthy\n    #   * Successfully connected to a non-hot-standby backend.\n    #   * Data received from any pgcon (not implemented).\n    #\n    # Unhealthy -> Failover\n    #   * More than 60% (UNEXPECTED_DISCONNECTS_THRESHOLD) of existing pgcons\n    #     are \"unexpectedly disconnected\" (number of existing pgcons is\n    #     captured at the moment we change to Unhealthy state, and maintained\n    #     on \"expected disconnects\" too).\n    #   * (and) In Unhealthy state for more than UNHEALTHY_MIN_TIME seconds.\n    #   * (and) sys_pgcon is down.\n    #   * (or) Postgres shutdown/hot-standby notification received.\n    #\n    # Healthy -> Unhealthy\n    #   * Any unexpected disconnect.\n    #   * (or) Failed to connect due to ConnectionError (not implemented).\n    #   * (or) Last active time is greater than 10 seconds (depends on the\n    #     sys_pgcon idle-poll interval) (not implemented).\n    #\n    # Healthy -> Failover\n    #   * Postgres shutdown/hot-standby notification received.\n    #\n    # Failover -> Healthy\n    #   * Successfully connected to a non-hot-standby backend.\n    #   * (and) sys_pgcon is healthy.\n\n    _state: State\n    _unhealthy_timer_handle: Optional[asyncio.TimerHandle]\n\n    def __init__(self, cluster_protocol: base.ClusterProtocol, tag: str):\n        self._cluster_protocol = cluster_protocol\n        self._state = State.UNHEALTHY\n        self._pgcon_count = 0\n        self._unexpected_disconnects = 0\n        self._unhealthy_timer_handle = None\n        self._sys_pgcon_healthy = False\n        self._tag = tag\n\n    def incr_metrics_counter(self, event: str, value: float = 1.0) -> None:\n        metrics.ha_events_total.inc(value, f\"adaptive://{self._tag}\", event)\n\n    def set_state_failover(self, *, call_on_switch_over=True):\n        self._state = State.FAILOVER\n        self._reset()\n        if call_on_switch_over:\n            logger.critical(\"adaptive: HA failover detected\")\n            self.incr_metrics_counter(\"failover\")\n            self._cluster_protocol.on_switch_over()\n\n    def on_pgcon_broken(self, is_sys_pgcon: bool):\n        if is_sys_pgcon:\n            self._sys_pgcon_healthy = False\n        if self._state == State.HEALTHY:\n            self.incr_metrics_counter(\"unhealthy\")\n            self._state = State.UNHEALTHY\n            self._unexpected_disconnects = 1\n            self._unhealthy_timer_handle = (\n                asyncio.get_running_loop().call_later(\n                    UNHEALTHY_MIN_TIME, self._maybe_failover\n                )\n            )\n            self._pgcon_count = max(\n                self._cluster_protocol.get_active_pgcon_num(), 0\n            ) + 1\n            logger.warning(\n                \"adaptive: Backend HA cluster is unhealthy. \"\n                \"Captured number of pgcons: %d\",\n                self._pgcon_count,\n            )\n        elif self._state == State.UNHEALTHY:\n            self._unexpected_disconnects += 1\n            if self._unhealthy_timer_handle is None:\n                self._maybe_failover()\n\n    def on_pgcon_lost(self):\n        if self._state == State.UNHEALTHY:\n            self._pgcon_count = max(1, self._pgcon_count - 1)\n            logger.debug(\n                \"on_pgcon_lost: decreasing captured pgcon count to: %d\",\n                self._pgcon_count,\n            )\n            if self._unhealthy_timer_handle is None:\n                self._maybe_failover()\n\n    def on_pgcon_made(self, is_sys_pgcon: bool):\n        if is_sys_pgcon:\n            self._sys_pgcon_healthy = True\n        if self._state == State.UNHEALTHY:\n            self.incr_metrics_counter(\"healthy\")\n            self._state = State.HEALTHY\n            logger.info(\"adaptive: Backend HA cluster is healthy\")\n            self._reset()\n        elif self._state == State.FAILOVER:\n            if self._sys_pgcon_healthy:\n                self.incr_metrics_counter(\"healthy\")\n                self._state = State.HEALTHY\n                logger.info(\n                    \"adaptive: Backend HA cluster has recovered from failover\"\n                )\n\n    def _reset(self):\n        self._pgcon_count = 0\n        self._unexpected_disconnects = 0\n        if self._unhealthy_timer_handle is not None:\n            self._unhealthy_timer_handle.cancel()\n            self._unhealthy_timer_handle = None\n\n    def _maybe_failover(self):\n        logger.debug(\n            \"_maybe_failover: unexpected disconnects: %d\",\n            self._unexpected_disconnects,\n        )\n        self._unhealthy_timer_handle = None\n        if (\n            self._unexpected_disconnects / self._pgcon_count\n            >= UNEXPECTED_DISCONNECTS_THRESHOLD\n        ) and not self._sys_pgcon_healthy:\n            self.set_state_failover()\n"
  },
  {
    "path": "edb/server/ha/base.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Callable, Optional\n\nimport urllib.parse\n\nfrom edb.common import asyncwatcher\nfrom edb.server import metrics\n\n\nclass ClusterProtocol:\n    def on_switch_over(self):\n        pass\n\n    def get_active_pgcon_num(self) -> int:\n        raise NotImplementedError()\n\n\nclass HABackend(asyncwatcher.AsyncWatcher):\n    def __init__(self) -> None:\n        super().__init__()\n        self._failover_cb: Optional[Callable[[], None]] = None\n\n    async def get_cluster_consensus(self) -> tuple[str, int]:\n        raise NotImplementedError\n\n    def get_master_addr(self) -> Optional[tuple[str, int]]:\n        raise NotImplementedError\n\n    def set_failover_callback(self, cb: Optional[Callable[[], None]]) -> None:\n        self._failover_cb = cb\n\n    @property\n    def dsn(self) -> str:\n        raise NotImplementedError\n\n    def incr_metrics_counter(self, event: str, value: float = 1.0) -> None:\n        metrics.ha_events_total.inc(value, self.dsn, event)\n\n\ndef get_backend(parsed_dsn: urllib.parse.ParseResult) -> Optional[HABackend]:\n    backend, _, sub_scheme = parsed_dsn.scheme.partition(\"+\")\n    if backend == \"stolon\":\n        from . import stolon\n\n        return stolon.get_backend(sub_scheme, parsed_dsn)\n\n    return None\n"
  },
  {
    "path": "edb/server/ha/stolon.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Any, Optional\n\nimport asyncio\nimport base64\nimport functools\nimport json\nimport logging\nimport os\nimport ssl\nimport urllib.parse\n\nfrom edb.common import asyncwatcher\nfrom edb.common import token_bucket\nfrom edb.server import consul\n\nfrom . import base\n\nlogger = logging.getLogger(\"edb.server\")\n\n\nclass StolonBackend(base.HABackend):\n    _master_addr: Optional[tuple[str, int]]\n\n    def __init__(self) -> None:\n        super().__init__()\n        self._master_addr = None\n\n    async def get_cluster_consensus(self) -> tuple[str, int]:\n        if self._master_addr is None:\n            started_by_us = await self.start_watching()\n            try:\n                assert self._waiter is None\n                self._waiter = asyncio.get_running_loop().create_future()\n                await self._waiter\n            finally:\n                if started_by_us:\n                    self.stop_watching()\n                    await self.wait_stopped_watching()\n        assert self._master_addr\n        return self._master_addr\n\n    def get_master_addr(self) -> Optional[tuple[str, int]]:\n        return self._master_addr\n\n    def _on_update(self, payload: bytes) -> None:\n        try:\n            data = json.loads(base64.b64decode(payload))\n        except (TypeError, ValueError):\n            logger.exception(f\"could not decode Stolon cluster data\")\n            return\n\n        # Successful Consul response, reset retry backoff\n        self._retry_attempt = 0\n\n        cluster_status = data.get(\"cluster\", {}).get(\"status\", {})\n        master_db = cluster_status.get(\"master\")\n        cluster_phase = cluster_status.get(\"phase\")\n        if cluster_phase != \"normal\":\n            logger.debug(\"Stolon cluster phase: %r\", cluster_phase)\n\n        if not master_db:\n            return\n\n        master_status = (\n            data.get(\"dbs\", {}).get(master_db, {}).get(\"status\", {})\n        )\n        master_healthy = master_status.get(\"healthy\")\n        if not master_healthy:\n            logger.warning(\"Stolon reports unhealthy master Postgres.\")\n            return\n\n        master_host = master_status.get(\"listenAddress\")\n        master_port = master_status.get(\"port\")\n        if not master_host or not master_port:\n            return\n        master_addr = master_host, int(master_port)\n        if master_addr != self._master_addr:\n            if self._master_addr is None:\n                logger.info(\"Discovered master Postgres at %r\", master_addr)\n                self._master_addr = master_addr\n            else:\n                logger.critical(\n                    f\"Switching over the master Postgres from %r to %r\",\n                    self._master_addr,\n                    master_addr,\n                )\n                self._master_addr = master_addr\n                if self._failover_cb is not None:\n                    self.incr_metrics_counter(\"failover\")\n                    self._failover_cb()\n\n        if self._waiter is not None:\n            if not self._waiter.done():\n                self._waiter.set_result(None)\n            self._waiter = None\n\n\nclass StolonConsulBackend(StolonBackend):\n    def __init__(\n        self,\n        cluster_name: str,\n        *,\n        host: str = \"127.0.0.1\",\n        port: int = 8500,\n        ssl: Optional[ssl.SSLContext] = None,\n    ) -> None:\n        super().__init__()\n        self._cluster_name = cluster_name\n        self._host = host\n        self._port = port\n        self._ssl = ssl\n\n        # This means we can request for 10 consecutive requests immediately\n        # after each response without delay, and then we're capped to 0.1\n        # request(token) per second, or 1 request per 10 seconds.\n        cap = float(os.environ.get(\"EDGEDB_SERVER_CONSUL_TOKEN_CAPACITY\", 10))\n        rate = float(os.environ.get(\"EDGEDB_SERVER_CONSUL_TOKEN_RATE\", 0.1))\n        self._token_bucket = token_bucket.TokenBucket(cap, rate)\n\n    async def _start_watching(self) -> asyncwatcher.AsyncWatcherProtocol:\n        _, pr = await asyncio.get_running_loop().create_connection(\n            functools.partial(\n                consul.ConsulKVProtocol,\n                self,\n                self._host,\n                f\"stolon/cluster/{self._cluster_name}/clusterdata\",\n            ),\n            self._host,\n            self._port,\n            ssl=self._ssl,\n        )\n        return pr  # type: ignore [return-value]\n\n    @functools.cached_property\n    def dsn(self) -> str:\n        proto = \"http\" if self._ssl is None else \"https\"\n        return (\n            f\"stolon+consul+{proto}://\"\n            f\"{self._host}:{self._port}/{self._cluster_name}\"\n        )\n\n    def consume_tokens(self, tokens: int) -> float:\n        return self._token_bucket.consume(tokens)\n\n\ndef get_backend(\n    sub_scheme: str, parsed_dsn: urllib.parse.ParseResult\n) -> StolonBackend:\n    name = parsed_dsn.path.lstrip(\"/\")\n    if not name:\n        raise ValueError(\"Stolon requires cluster name in the URI as path.\")\n\n    cls = None\n    storage, _, wire_protocol = sub_scheme.partition(\"+\")\n    if storage == \"consul\":\n        cls = StolonConsulBackend\n    if not cls:\n        raise ValueError(f\"{parsed_dsn.scheme} is not supported\")\n    if wire_protocol not in {\"\", \"http\", \"https\"}:\n        raise ValueError(f\"Wire protocol {wire_protocol} is not supported\")\n\n    args: dict[str, Any] = {}\n    if parsed_dsn.hostname:\n        args[\"host\"] = parsed_dsn.hostname\n    if parsed_dsn.port:\n        args[\"port\"] = parsed_dsn.port\n    if wire_protocol == \"https\":\n        args[\"ssl\"] = ssl.create_default_context()\n\n    return cls(name, **args)\n"
  },
  {
    "path": "edb/server/http.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nfrom typing import (\n    Any,\n    Mapping,\n    Optional,\n    Self,\n    Callable,\n)\n\nimport asyncio\nimport dataclasses\nimport logging\nimport json as json_lib\nimport urllib.parse\nimport time\nfrom http import HTTPStatus as HTTPStatus\n\nfrom edb.server._rust_native._http import Http\nfrom . import rust_async_channel\n\nlogger = logging.getLogger(\"edb.server\")\nHeaderType = Optional[list[tuple[str, str]] | dict[str, str]]\n\n\n@dataclasses.dataclass(frozen=True)\nclass HttpStat:\n    response_time_ms: int\n    error_code: int\n    response_body_size: int\n    response_content_type: str\n    request_body_size: int\n    request_content_type: str\n    method: str\n    streaming: bool\n\n\nStatCallback = Callable[[HttpStat], None]\n\n\nclass HttpClient:\n    def __init__(\n        self,\n        limit: int,\n        user_agent: str = \"EdgeDB\",\n        stat_callback: Optional[StatCallback] = None,\n    ):\n        self._task = None\n        self._client = None\n        self._limit = limit\n        self._skip_reads = 0\n        self._loop: Optional[asyncio.AbstractEventLoop] = (\n            asyncio.get_running_loop()\n        )\n        self._task = None\n        self._streaming: dict[int, asyncio.Queue[Any]] = {}\n        self._next_id = 0\n        self._requests: dict[int, asyncio.Future] = {}\n        self._user_agent = user_agent\n        self._stat_callback = stat_callback\n\n    def __del__(self) -> None:\n        if not self.closed():\n            logger.error(f\"HttpClient {id(self)} was not closed\")\n\n    def close(self) -> None:\n        if not self.closed():\n            if self._task is not None:\n                self._task.cancel()\n                self._task = None\n            self._loop = None\n            self._client = None\n\n    def closed(self) -> bool:\n        return self._task is None and self._loop is None\n\n    def _ensure_task(self):\n        if self.closed():\n            raise Exception(\"HttpClient was closed\")\n        if self._task is None:\n            self._client = Http(self._limit)\n            self._task = self._loop.create_task(self._boot(self._client))\n\n    def _ensure_client(self):\n        if self._client is None:\n            raise Exception(\"HttpClient was closed\")\n        return self._client\n\n    def _safe_close(self, id):\n        if self._client is not None:\n            self._client._close(id)\n\n    def _safe_ack(self, id):\n        if self._client is not None:\n            self._client._ack_sse(id)\n\n    def _update_limit(self, limit: int):\n        if self._client is not None and limit != self._limit:\n            self._limit = limit\n            self._client._update_limit(limit)\n\n    def _process_headers(self, headers: HeaderType) -> list[tuple[str, str]]:\n        if headers is None:\n            return []\n        if isinstance(headers, Mapping):\n            return [(k, v) for k, v in headers.items()]\n        if isinstance(headers, list):\n            return headers\n        raise ValueError(f\"Invalid headers type: {type(headers)}\")\n\n    def _process_content(\n        self,\n        headers: list[tuple[str, str]],\n        data: bytes | str | dict[str, str] | None = None,\n        json: Any | None = None,\n    ) -> bytes:\n        if json is not None:\n            data = json_lib.dumps(json).encode('utf-8')\n            headers.append(('Content-Type', 'application/json'))\n        elif isinstance(data, str):\n            data = data.encode('utf-8')\n        elif isinstance(data, dict):\n            data = urllib.parse.urlencode(data).encode('utf-8')\n            headers.append(\n                ('Content-Type', 'application/x-www-form-urlencoded')\n            )\n        elif data is None:\n            data = bytes()\n        elif isinstance(data, bytes):\n            pass\n        else:\n            raise ValueError(f\"Invalid content type: {type(data)}\")\n        return data\n\n    def _process_path(self, path: str) -> str:\n        return path\n\n    def with_context(\n        self,\n        *,\n        base_url: Optional[str] = None,\n        headers: HeaderType = None,\n        url_munger: Optional[Callable[[str], str]] = None,\n    ) -> Self:\n        \"\"\"Create an HttpClient with common optional base URL and headers that\n        will be applied to all requests.\"\"\"\n        return HttpClientContext(\n            http_client=self,\n            base_url=base_url,\n            headers=headers,\n            url_munger=url_munger,\n        )  # type: ignore\n\n    async def request(\n        self,\n        *,\n        method: str,\n        path: str,\n        headers: HeaderType = None,\n        data: bytes | str | dict[str, str] | None = None,\n        json: Any | None = None,\n        cache: bool = False,\n    ) -> tuple[int, bytearray, dict[str, str]]:\n        self._ensure_task()\n        path = self._process_path(path)\n        headers_list = self._process_headers(headers)\n        headers_list.append((\"User-Agent\", self._user_agent))\n        data = self._process_content(headers_list, data, json)\n        id = self._next_id\n        self._next_id += 1\n        self._requests[id] = asyncio.Future()\n        start_time = time.monotonic()\n        try:\n            self._ensure_client()._request(\n                id, path, method, data, headers_list, cache\n            )\n            resp = await self._requests[id]\n            if self._stat_callback:\n                status_code, body, headers = resp\n                self._stat_callback(\n                    HttpStat(\n                        response_time_ms=int(\n                            (time.monotonic() - start_time) * 1000\n                        ),\n                        error_code=status_code,\n                        response_body_size=len(body),\n                        response_content_type=dict(headers_list).get(\n                            'content-type', ''\n                        ),\n                        request_body_size=len(data),\n                        request_content_type=dict(headers_list).get(\n                            'content-type', ''\n                        ),\n                        method=method,\n                        streaming=False,\n                    )\n                )\n            return resp\n        finally:\n            del self._requests[id]\n\n    async def get(\n        self,\n        path: str,\n        *,\n        headers: HeaderType = None,\n        cache: bool = False,\n    ) -> Response:\n        result = await self.request(\n            method=\"GET\", path=path, data=None, headers=headers, cache=cache\n        )\n        return Response.from_tuple(result)\n\n    async def post(\n        self,\n        path: str,\n        *,\n        headers: HeaderType = None,\n        data: bytes | str | dict[str, str] | None = None,\n        json: Any | None = None,\n    ) -> Response:\n        result = await self.request(\n            method=\"POST\", path=path, data=data, json=json, headers=headers\n        )\n        return Response.from_tuple(result)\n\n    async def stream_sse(\n        self,\n        path: str,\n        *,\n        method: str = \"POST\",\n        headers: HeaderType = None,\n        data: bytes | str | dict[str, str] | None = None,\n        json: Any | None = None,\n    ) -> Response | ResponseSSE:\n        self._ensure_task()\n        path = self._process_path(path)\n        headers_list = self._process_headers(headers)\n        headers_list.append((\"User-Agent\", self._user_agent))\n        data = self._process_content(headers_list, data, json)\n\n        id = self._next_id\n        self._next_id += 1\n        self._requests[id] = asyncio.Future()\n        start_time = time.monotonic()\n        try:\n            self._ensure_client()._request_sse(\n                id, path, method, data, headers_list\n            )\n            resp = await self._requests[id]\n            if self._stat_callback:\n                if id in self._streaming:\n                    status_code, _ = resp\n                    body = b''\n                else:\n                    assert len(resp) >= 2\n                    status_code, body = resp[0:2]\n                self._stat_callback(\n                    HttpStat(\n                        response_time_ms=int(\n                            (time.monotonic() - start_time) * 1000\n                        ),\n                        error_code=status_code,\n                        response_body_size=len(body),\n                        response_content_type=dict(headers_list).get(\n                            'content-type', ''\n                        ),\n                        request_body_size=len(data),\n                        request_content_type=dict(headers_list).get(\n                            'content-type', ''\n                        ),\n                        method=method,\n                        streaming=id in self._streaming,\n                    )\n                )\n            if id in self._streaming:\n                # Valid to call multiple times\n                cancel = lambda: self._safe_close(id)\n                # Acknowledge SSE message (for backpressure)\n                ack = lambda: self._safe_ack(id)\n                return ResponseSSE.from_tuple(\n                    resp, self._streaming[id], cancel, ack\n                )\n            return Response.from_tuple(resp)\n        finally:\n            del self._requests[id]\n\n    async def _boot(self, client) -> None:\n        logger.info(f\"HTTP client initialized, user_agent={self._user_agent}\")\n        try:\n            channel = rust_async_channel.RustAsyncChannel(\n                client._channel, self._process_message\n            )\n            try:\n                await channel.run()\n            finally:\n                channel.close()\n        except Exception:\n            logger.error(f\"Error in HTTP client\", exc_info=True)\n            raise\n\n    def _process_message(self, msg: tuple[Any, ...]) -> None:\n        try:\n            msg_type, id, data = msg\n            if msg_type == 0:  # Error\n                if id in self._requests:\n                    self._requests[id].set_exception(Exception(data))\n                if id in self._streaming:\n                    self._streaming[id].put_nowait(None)\n                    del self._streaming[id]\n            elif msg_type == 1:  # Response\n                if id in self._requests:\n                    self._requests[id].set_result(data)\n            elif msg_type == 2:  # SSEStart\n                if id in self._requests:\n                    self._streaming[id] = asyncio.Queue()\n                    self._requests[id].set_result(data)\n            elif msg_type == 3:  # SSEEvent\n                if id in self._streaming:\n                    self._streaming[id].put_nowait(data)\n            elif msg_type == 4:  # SSEEnd\n                if id in self._streaming:\n                    self._streaming[id].put_nowait(None)\n                    del self._streaming[id]\n        except Exception as e:\n            logger.error(f\"Error processing message: {e}\", exc_info=True)\n            raise\n\n    async def __aenter__(self) -> Self:\n        return self\n\n    async def __aexit__(self, *args) -> None:  # type: ignore\n        self.close()\n\n\nclass HttpClientContext(HttpClient):\n    def __init__(\n        self,\n        http_client: HttpClient,\n        url_munger: Callable[[str], str] | None = None,\n        headers: HeaderType = None,\n        base_url: str | None = None,\n    ):\n        self.url_munger = url_munger\n        self.http_client = http_client\n        self.base_url = base_url\n        self.headers = super()._process_headers(headers)\n\n    # HttpClientContext does not need to be closed\n    def __del__(self):\n        pass\n\n    def closed(self):\n        return super().closed()\n\n    def close(self):\n        pass\n\n    async def __aenter__(self) -> Self:\n        return self\n\n    async def __aexit__(self, *args) -> None:  # type: ignore\n        pass\n\n    def _process_headers(self, headers):\n        headers = super()._process_headers(headers)\n        headers += self.headers\n        return headers\n\n    def _process_path(self, path):\n        path = super()._process_path(path)\n        if self.base_url is not None:\n            path = self.base_url + path\n        if self.url_munger is not None:\n            path = self.url_munger(path)\n        return path\n\n    async def request(\n        self,\n        *,\n        method,\n        path,\n        headers=None,\n        data=None,\n        json=None,\n        cache=False,\n    ):\n        path = self._process_path(path)\n        headers = self._process_headers(headers)\n        return await self.http_client.request(\n            method=method,\n            path=path,\n            headers=headers,\n            data=data,\n            json=json,\n            cache=cache,\n        )\n\n    async def stream_sse(\n        self, path, *, method=\"POST\", headers=None, data=None, json=None\n    ):\n        path = self._process_path(path)\n        headers = self._process_headers(headers)\n        return await self.http_client.stream_sse(\n            path, method=method, headers=headers, data=data, json=json\n        )\n\n\nclass CaseInsensitiveDict(dict):\n    def __init__(self, data: Optional[list[tuple[str, str]]] = None):\n        super().__init__()\n        if data:\n            for k, v in data:\n                self[k.lower()] = v\n\n    def __setitem__(self, key: str, value: str):\n        super().__setitem__(key.lower(), value)\n\n    def __getitem__(self, key: str):\n        return super().__getitem__(key.lower())\n\n    def get(self, key: str, default=None):\n        return super().get(key.lower(), default)\n\n    def update(self, *args, **kwargs: str) -> None:\n        if args:\n            data = args[0]\n            if isinstance(data, Mapping):\n                for key, value in data.items():\n                    self[key] = value\n            else:\n                for key, value in data:\n                    self[key] = value\n        for key, value in kwargs.items():\n            self[key] = value\n\n\n@dataclasses.dataclass(frozen=True)\nclass Response:\n    status_code: int\n    body: bytearray\n    headers: CaseInsensitiveDict\n    is_streaming: bool = False\n\n    @classmethod\n    def from_tuple(cls, data: tuple[int, bytearray, dict[str, str]]):\n        status_code, body, headers_list = data\n        headers = CaseInsensitiveDict([(k, v) for k, v in headers_list.items()])\n        return cls(status_code, body, headers)\n\n    def json(self):\n        return json_lib.loads(self.body.decode('utf-8'))\n\n    def bytes(self):\n        return bytes(self.body)\n\n    @property\n    def text(self) -> str:\n        return self.body.decode('utf-8')\n\n    async def __aenter__(self) -> Self:\n        return self\n\n    async def __aexit__(self, exc_type, exc_value, traceback):\n        pass\n\n\n@dataclasses.dataclass(frozen=True)\nclass ResponseSSE:\n    status_code: int\n    headers: CaseInsensitiveDict\n    _stream: asyncio.Queue = dataclasses.field(repr=False)\n    _cancel: Callable[[], None] = dataclasses.field(repr=False)\n    _ack: Callable[[], None] = dataclasses.field(repr=False)\n    _closed: list[bool] = dataclasses.field(default_factory=lambda: [False])\n    is_streaming: bool = True\n\n    @classmethod\n    def from_tuple(\n        cls,\n        data: tuple[int, dict[str, str]],\n        stream: asyncio.Queue,\n        cancel: Callable[[], None],\n        ack: Callable[[], None],\n    ):\n        status_code, headers = data\n        headers = CaseInsensitiveDict([(k, v) for k, v in headers.items()])\n        return cls(status_code, headers, stream, cancel, ack)\n\n    @dataclasses.dataclass(frozen=True)\n    class SSEEvent:\n        event: str\n        data: str\n        id: Optional[str] = None\n\n        def json(self):\n            return json_lib.loads(self.data)\n\n    def close(self):\n        if not self.closed():\n            self._closed[0] = True\n            self._cancel()\n\n    def closed(self) -> bool:\n        return self._closed[0]\n\n    def __del__(self):\n        if not self.closed():\n            logger.error(f\"ResponseSSE {id(self)} was not closed\")\n\n    def __aiter__(self):\n        return self\n\n    async def __anext__(self):\n        if self.closed():\n            raise StopAsyncIteration\n        next = await self._stream.get()\n        try:\n            if next is None:\n                self.close()\n                raise StopAsyncIteration\n            id, data, event = next\n            return self.SSEEvent(event, data, id)\n        finally:\n            self._ack()\n\n    async def __aenter__(self) -> Self:\n        return self\n\n    async def __aexit__(self, exc_type, exc_value, traceback):\n        self.close()\n"
  },
  {
    "path": "edb/server/inplace_upgrade.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Iterator,\n    Optional,\n    Sequence,\n)\n\nimport dataclasses\nimport json\nimport logging\nimport os\n\n\nfrom edb import edgeql\nfrom edb import errors\nfrom edb.edgeql import ast as qlast\n\nfrom edb.common import debug\nfrom edb.common import uuidgen\n\nfrom edb.schema import ddl as s_ddl\nfrom edb.schema import delta as sd\nfrom edb.schema import extensions as s_exts\nfrom edb.schema import functions as s_func\nfrom edb.schema import links as s_links\nfrom edb.schema import name as sn\nfrom edb.schema import objtypes as s_objtypes\nfrom edb.schema import reflection as s_refl\nfrom edb.schema import scalars as s_scalars\nfrom edb.schema import schema as s_schema\nfrom edb.schema import types as s_types\nfrom edb.schema import version as s_ver\n\nfrom edb.server import args as edbargs\nfrom edb.server import bootstrap\nfrom edb.server import config\nfrom edb.server import compiler as edbcompiler\nfrom edb.server.compiler import ddl as compiler_ddl\nfrom edb.server import defines as edbdef\nfrom edb.server import instdata\nfrom edb.server import pgcluster\nfrom edb.server import pgcon\n\nfrom edb.pgsql import common as pg_common\nfrom edb.pgsql import dbops\nfrom edb.pgsql import patches as pg_patches\nfrom edb.pgsql import metaschema\nfrom edb.pgsql import trampoline\n\n\nlogger = logging.getLogger('edb.server')\n\nPGCon = bootstrap.PGConnectionProxy | pgcon.PGConnection\n\n\ndef _is_stdlib_target(\n    t: s_objtypes.ObjectType,\n    schema: s_schema.Schema,\n) -> bool:\n    if intersection := t.get_intersection_of(schema):\n        return any((_is_stdlib_target(it, schema)\n                    for it in intersection.objects(schema)))\n    elif union := t.get_union_of(schema):\n        return any((_is_stdlib_target(ut, schema)\n                    for ut in union.objects(schema)))\n\n    name = t.get_name(schema)\n\n    if name == sn.QualName('std', 'Object'):\n        return False\n    return t.get_name(schema).get_module_name() in s_schema.STD_MODULES\n\n\ndef _compile_inheritance_schema_fixup(\n    ctx: bootstrap.BootstrapContext,\n    current_block: dbops.PLBlock,\n    schema: s_schema.ChainedSchema,\n    keys: dict[str, Any],\n) -> None:\n    \"\"\"Compile schema-specific fixups for added stdlib types.\"\"\"\n    backend_params = ctx.cluster.get_runtime_params()\n\n    # Recompile functions that reference stdlib types (like\n    # std::BaseObject or schema::Object), since new subtypes may have\n    # been added.\n    to_recompile = schema._top_schema.get_objects(type=s_func.Function)\n    for func in to_recompile:\n        if func.get_name(schema).get_root_module_name() == s_schema.EXT_MODULE:\n            continue\n        # If none of the types referenced in the function are standard\n        # library types, we don't need to recompile.\n        if not (\n            (expr := func.get_nativecode(schema))\n            and expr.refs\n            and any(\n                isinstance(dep, s_objtypes.ObjectType)\n                and _is_stdlib_target(dep, schema)\n                for dep in expr.refs.objects(schema)\n            )\n        ):\n            continue\n\n        alter_func = func.init_delta_command(\n            schema, sd.AlterObject\n        )\n        alter_func.set_attribute_value(\n            'nativecode', func.get_nativecode(schema)\n        )\n        alter_func.canonical = True\n\n        # N.B: We are ignoring the schema changes, since we aren't\n        # updating the schema version.\n        _, plan, _ = bootstrap._process_delta_params(\n            sd.DeltaRoot.from_commands(alter_func),\n            schema,\n            backend_params,\n            stdmode=False,\n            **keys,\n        )\n        plan.generate(current_block)\n\n    # Regenerate on_target_delete triggers for any links targeting a\n    # stdlib type.\n    links = schema._top_schema.get_objects(type=s_links.Link)\n    for link in links:\n        if link.get_name(schema).get_root_module_name() == s_schema.EXT_MODULE:\n            continue\n        source = link.get_source(schema)\n        if (\n            not source\n            or not source.is_material_object_type(schema)\n            or link.get_computable(schema)\n            or link.get_shortname(schema).name == '__type__'\n            or not _is_stdlib_target(link.get_target(schema), schema)\n        ):\n            continue\n\n        pol = link.get_on_target_delete(schema)\n        # HACK: Set the policy in a temporary in-memory schema to be\n        # something else, so that we can set it back to the real value\n        # and pgdelta will generate code for it.\n        fake_pol = (\n            s_links.LinkTargetDeleteAction.Allow\n            if pol == s_links.LinkTargetDeleteAction.Restrict\n            else s_links.LinkTargetDeleteAction.Restrict\n        )\n        fake_schema = link.set_field_value(schema, 'on_target_delete', fake_pol)\n\n        alter_delta, alter_link, _ = link.init_delta_branch(\n            schema, sd.CommandContext(), sd.AlterObject\n        )\n        alter_link.set_attribute_value('on_target_delete', pol)\n\n        # N.B: We are ignoring the schema changes, since we aren't\n        # updating the schema version.\n        _, plan, _ = bootstrap._process_delta_params(\n            sd.DeltaRoot.from_commands(alter_delta),\n            fake_schema,\n            backend_params,\n            stdmode=False,\n            **keys,\n        )\n        plan.generate(current_block)\n\n\ndef _compile_schema_fixup(\n    ctx: bootstrap.BootstrapContext,\n    schema: s_schema.ChainedSchema,\n    keys: dict[str, Any],\n) -> dbops.PLBlock:\n    \"\"\"Compile any schema-specific fixes that need to be applied.\"\"\"\n    current_block = dbops.PLTopBlock()\n    _compile_inheritance_schema_fixup(ctx, current_block, schema, keys)\n\n    # Remove pointless triggers that existed before 6.8\n    compiler_ddl.remove_pointless_triggers(schema).generate(current_block)\n\n    return current_block\n\n\nasync def _collect_6x_upgrade_patches(\n    ctx: bootstrap.BootstrapContext,\n    schema: s_schema.Schema,\n) -> tuple[list[qlast.Command], bool, bool]:\n    from edb.pgsql import patches_6x\n\n    cmds: list[qlast.Command] = []\n\n    # If that table doesn't exist, we aren't upgrading from 6.x, so\n    # don't worry. (Which means, at this point, a 7.x -> dev/nightly\n    # upgrade.)\n    try:\n        res = await ctx.conn.sql_fetch_val(\n            f\"\"\"\n            SELECT json::json FROM edgedbinstdata_v6_2f20b3fed0.instdata\n            WHERE key = 'num_patches'\n            \"\"\".encode('utf-8'),\n        )\n    except pgcon.BackendError:\n        return [], False, False\n\n    needs_config = False\n    jnum = json.loads(res)\n    for kind, patch in patches_6x.PATCHES[jnum:]:\n\n        if not kind.startswith('edgeql+user_ext'):\n            continue\n        # Only run a userext update if the extension we are trying to\n        # update is installed.\n        extension_name = kind.split('|')[-1]\n        extension = schema.get_global(\n            s_exts.Extension, extension_name, default=None)\n        if not extension:\n            continue\n\n        if '+config' in kind:\n            needs_config |= True\n\n        for ddl_cmd in edgeql.parse_block(patch):\n            if not isinstance(ddl_cmd, qlast.DDLCommand):\n                assert isinstance(ddl_cmd, qlast.Query)\n                ddl_cmd = qlast.DDLQuery(query=ddl_cmd)\n            cmds.append(ddl_cmd)\n\n    # 6.2 introduced a change to EmbeddingModel (the addition of a new\n    # default value) that requires a repair to sync the user schema up\n    # with, since ai index annotations get copied into objects.\n    needs_repair = bool(\n        schema.get_global(s_exts.Extension, 'ai', default=None)\n    )\n\n    return cmds, needs_repair, needs_config\n\n\ndef _subcommands_preorder(cmd: sd.Command) -> Iterator[sd.Command]:\n    yield cmd\n    for sub in cmd.get_subcommands():\n        yield from _subcommands_preorder(sub)\n\n\nasync def _apply_ddl_schema_storage(\n    ddl_cmd: qlast.Command,\n    ctx: bootstrap.BootstrapContext,\n    backend_params,\n    keys: dict[str, Any],\n    compilerctx: edbcompiler.CompileContext,\n    schema: s_schema.Schema,\n    schema_object_ids: dict[tuple[sn.Name, Any], uuidgen.UUID],\n    fake_backend_ids: bool=False,\n\n) -> tuple[s_schema.ChainedSchema, str]:\n    # applies ddl schema storage but not the real ddl\n    # returns that, though\n    current_block = dbops.PLTopBlock()\n\n    if debug.flags.sdl_loading:\n        ddl_cmd.dump_edgeql()\n\n    assert isinstance(ddl_cmd, qlast.DDLCommand)\n    delta_command = s_ddl.delta_from_ddl(\n        ddl_cmd, modaliases={}, schema=schema,\n        schema_object_ids=schema_object_ids,\n        **keys,\n    )\n    # Prune any AlterSchemaVersion commands, because they won't work,\n    # since all the compile_schema_storage_in_delta commands run right\n    # away, while if we run a fixup block, it is during finalize.\n    sub: sd.Command\n    for sub in delta_command.get_subcommands(\n        type=s_ver.AlterSchemaVersion\n    ):\n        delta_command.discard(sub)\n    # This hack is quite frustrating: since the actual changes to the\n    # pg schema (for extensions) happen *after* the reflection schema\n    # updates (during finalization), backend_ids for new scalars\n    # aren't ready, so we force reflection to *not* try computing them\n    # now, and we'll get them later.\n    if fake_backend_ids:\n        for sub in _subcommands_preorder(delta_command):\n            if not isinstance(sub, sd.CreateObject):\n                continue\n            mcls = sub.get_schema_metaclass()\n            if (\n                issubclass(mcls, (s_scalars.ScalarType, s_types.Collection))\n                and not issubclass(mcls, s_types.CollectionExprAlias)\n            ):\n                sub.set_attribute_value('backend_id', 0)\n\n    schema, plan, tplan = bootstrap._process_delta_params(\n        delta_command,\n        schema,\n        backend_params,\n        stdmode=False,\n        **keys,\n    )\n\n    compilerctx.state.current_tx().update_schema(schema)\n\n    fixup_block = dbops.PLTopBlock()\n    plan.generate(fixup_block)\n    # # ???\n    # tplan.generate(current_block)\n    fixup_ddl = fixup_block.to_string()\n\n    context = sd.CommandContext(**keys)\n    edbcompiler.compile_schema_storage_in_delta(\n        ctx=compilerctx,\n        delta=plan,\n        block=current_block,\n        context=context,\n    )\n\n    # TODO: Should we batch them all up?\n    patch = current_block.to_string()\n\n    if debug.flags.delta_execute:\n        debug.header('Patch Script')\n        debug.dump_code(patch, lexer='sql')\n\n    await ctx.conn.sql_execute(patch.encode('utf-8'))\n\n    assert isinstance(schema, s_schema.ChainedSchema)\n    return schema, fixup_ddl\n\n\nasync def _upgrade_one(\n    ctx: bootstrap.BootstrapContext,\n    state: edbcompiler.CompilerState,\n    global_schema: s_schema.Schema,\n    std_global_schema: s_schema.Schema,\n    upgrade_data: Optional[Any],\n) -> None:\n    if not upgrade_data:\n        return\n\n    backend_params = ctx.cluster.get_runtime_params()\n    assert backend_params.has_create_database\n\n    ddl = upgrade_data['ddl']\n    # ids:\n    schema_object_ids = {\n        (\n            sn.name_from_string(name), qltype if qltype else None\n        ): uuidgen.UUID(objid)\n        for name, qltype, objid in upgrade_data['ids']\n    }\n\n    # Load the schemas\n    schema = s_schema.ChainedSchema(\n        state.std_schema,\n        s_schema.EMPTY_SCHEMA,\n        global_schema,\n    )\n\n    compilerctx = edbcompiler.new_compiler_context(\n        compiler_state=state,\n        user_schema=schema.get_top_schema(),\n        bootstrap_mode=False,  # MAYBE?\n    )\n\n    keys: dict[str, Any] = dict(\n        testmode=True,\n    )\n\n    # Apply the DDL, but usually *only* execute the schema storage part!!\n    # For the actual core DDL, only do schema storage.\n    # Sometimes we have to run extension patch code, and then we\n    # need to run the actual code.\n    for ddl_cmd in edgeql.parse_block(ddl):\n        schema, _ = await _apply_ddl_schema_storage(\n            ddl_cmd,\n            ctx, backend_params, keys, compilerctx, schema, schema_object_ids\n        )\n\n    schema_fixup = ''\n    upgrade_patches, needs_repair, needs_config = (\n        await _collect_6x_upgrade_patches(ctx, schema)\n    )\n    for ddl_cmd in upgrade_patches:\n        schema, fixup = await _apply_ddl_schema_storage(\n            ddl_cmd,\n            ctx, backend_params, keys, compilerctx, schema,\n            # Empty schema_object_ids because this isn't actually user\n            # objects anymore, and because reusing the\n            # schema_object_ids led to a subtle issue:\n            # when altering a computed Global in a patch, the underlying\n            # ObjectType got deleted and replaced with a new one with\n            # identical id, which caused the link policy to spuriously\n            # fail!\n            schema_object_ids={},\n            fake_backend_ids=True,\n        )\n        schema_fixup += fixup\n\n    if upgrade_patches:\n        version_key = pg_patches.get_version_key(len(pg_patches.PATCHES))\n        sysqueries = json.loads(await instdata.get_instdata(\n            ctx.conn, f'sysqueries{version_key}', 'json'))\n        schema_fixup += sysqueries['backend_id_fixup']\n    if needs_config:\n        existing_view_columns = await bootstrap.get_existing_view_columns(\n            ctx.conn)\n        cfg_block = dbops.PLTopBlock()\n        metaschema.get_config_views(schema, existing_view_columns).generate(\n            cfg_block)\n        schema_fixup += cfg_block.to_string()\n\n    # If we need to do a schema repair... do it\n    if needs_repair:\n        # We want to do the repair against the *new* global schema\n        # (which is std_global_schema), but there might be non-std\n        # extensions installed, so we chain it with the original\n        # global_schema to get it to work.\n        confused_global_schema = s_schema.ChainedSchema(\n            global_schema,\n            std_global_schema,\n            s_schema.EMPTY_SCHEMA,\n        )\n\n        repair = bootstrap.prepare_repair_patch(\n            state.std_schema,\n            state.refl_schema,\n            schema.get_top_schema(),\n            confused_global_schema,\n            state.schema_class_layout,\n            backend_params,\n        )\n        schema_fixup += repair\n\n    # Refresh the pg_catalog materialized views\n    current_block = dbops.PLTopBlock()\n    refresh = metaschema.generate_sql_information_schema_refresh(\n        backend_params.instance_params.version\n    )\n    refresh.generate(current_block)\n    patch = current_block.to_string()\n    await ctx.conn.sql_execute(patch.encode('utf-8'))\n\n    new_local_spec = config.load_spec_from_schema(\n        schema,\n        only_exts=True,\n        # suppress validation because we might be in an intermediate state\n        validate=False,\n    )\n    spec_json = config.spec_to_json(new_local_spec)\n    await ctx.conn.sql_execute(trampoline.fixup_query(f'''\\\n        UPDATE\n            edgedbinstdata_VER.instdata\n        SET\n            json = {pg_common.quote_literal(spec_json)}\n        WHERE\n            key = 'configspec_ext';\n    ''').encode('utf-8'))\n\n    # Compile the fixup script for the schema and stash it away\n    schema_fixup += _compile_schema_fixup(ctx, schema, keys).to_string()\n    await bootstrap._store_static_text_cache(\n        ctx,\n        f'schema_fixup_query',\n        schema_fixup,\n    )\n\n\nDEP_CHECK_QUERY = r'''\nwith\n-- Fetch all the object types we care about.\nall_objs AS (\n  select objs.oid, ns.nspname as nspname, objs.name, objs.typ\n  from (\n    select\n      oid as oid, relname as name,\n      (case when relkind = 'v' then 'view' else 'table' end) as typ,\n      relnamespace as namespace\n    from pg_catalog.pg_class\n    union all\n    select\n      oid as oid, typname as name, 'type' as typ, typnamespace as namespace\n    from pg_catalog.pg_type\n    union all\n    select\n      oid as oid, proname as name, 'function' as typ, pronamespace as namespace\n    from pg_catalog.pg_proc\n  ) as objs\n  inner join pg_catalog.pg_namespace ns on objs.namespace = ns.oid\n),\n-- Fetch pg_depend along with some special handling of internal deps.\ncdeps AS (\n  select dep.objid, dep.refobjid, dep.deptype\n  from pg_catalog.pg_depend dep\n  union\n  -- if there is an incoming 'i' dep to an obj A from B, treat all\n  -- other outgoing deps from B as outgoing from A. We do this because\n  -- the actual query in a view is stored in a pg_rewrite that *depends on*\n  -- the view. (Seems backward.)\n  select i.refobjid, c.refobjid, c.deptype\n  from pg_catalog.pg_depend i\n  inner join pg_catalog.pg_depend c\n  on i.objid = c.objid\n  where i.refobjid != c.refobjid and i.deptype = 'i'\n)\n-- Get any dependencies from outside our namespaces into them.\nselect src.typ, src.nspname, src.name, tgt.typ, tgt.nspname, tgt.name\nfrom all_objs src\ninner join cdeps dep on src.oid = dep.objid\ninner join all_objs tgt on tgt.oid = dep.refobjid\nwhere true\nand NOT src.nspname = ANY ({namespaces})\nand tgt.nspname = ANY ({namespaces})\nand dep.deptype != 'i'\n'''\n\n\nasync def _delete_schemas(\n    conn: PGCon,\n    to_delete: Sequence[str]\n) -> None:\n    # To add a bit more safety, check whether there are any\n    # dependencies on the modules we want to delete from outside those\n    # modules since the only way to delete non-empty schemas in\n    # postgres is CASCADE.\n    namespaces = (\n        f'ARRAY[{\", \".join(pg_common.quote_literal(k) for k in to_delete)}]'\n    )\n    qry = DEP_CHECK_QUERY.format(namespaces=namespaces)\n    existing_deps = await conn.sql_fetch(qry.encode('utf-8'))\n    if existing_deps:\n        # All of the fields are text, so decode them all\n        sdeps = [\n            tuple(x.decode('utf-8') for x in row)\n            for row in existing_deps\n        ]\n\n        messages = [\n            f'{st} {pg_common.qname(ss, sn)} depends on '\n            f'{tt} {pg_common.qname(ts, tn)}\\n'\n            for st, ss, sn, tt, ts, tn in sdeps\n        ]\n\n        raise AssertionError(\n            'Dependencies to old schemas still exist: \\n%s'\n            % ''.join(messages)\n        )\n\n    # It is *really* dumb the way that CASCADE works in postgres.\n    await conn.sql_execute(f\"\"\"\n        drop schema {', '.join(to_delete)} cascade\n    \"\"\".encode('utf-8'))\n\n\nasync def _get_namespaces(\n    conn: PGCon,\n) -> list[str]:\n    return json.loads(await conn.sql_fetch_val(\"\"\"\n        select json_agg(nspname) from pg_namespace\n        where nspname like 'edgedb%\\\\_v%'\n    \"\"\".encode('utf-8')))\n\n\nasync def _finalize_one(\n    ctx: bootstrap.BootstrapContext,\n    database: str,\n) -> None:\n    conn = ctx.conn\n\n    # If the upgrade is already finalized, skip it. This lets us be\n    # resilient to crashes during the finalization process, which may\n    # leave some databases upgraded but not all.\n    if (await instdata.get_instdata(conn, 'upgrade_finalized', 'text')) == b'1':\n        logger.info(f\"Database upgrade already finalized\")\n        return\n\n    trampoline_query = await instdata.get_instdata(\n        conn, 'trampoline_pivot_query', 'text')\n    fixup_query = await instdata.get_instdata(\n        conn, 'schema_fixup_query', 'text')\n\n    await conn.sql_execute(trampoline_query)\n    if fixup_query:\n        await conn.sql_execute(fixup_query)\n\n    # For the template database (which is upgraded *last*, after all\n    # others have succeeded), run the commands to update the global\n    # schema. (To populate the extension packages.)\n    if database == edbdef.EDGEDB_TEMPLATE_DB:\n        global_schema_update_query = await instdata.get_instdata(\n            conn, 'global_schema_update_query', 'text')\n        await conn.sql_execute(global_schema_update_query)\n\n    namespaces = await _get_namespaces(ctx.conn)\n\n    cur_suffix = pg_common.versioned_schema(\"\")\n    to_delete = [x for x in namespaces if not x.endswith(cur_suffix)]\n\n    await _delete_schemas(conn, to_delete)\n\n    await bootstrap._store_static_text_cache(\n        ctx,\n        f'upgrade_finalized',\n        '1',\n    )\n\n\nasync def _get_databases(\n    ctx: bootstrap.BootstrapContext,\n) -> list[str]:\n    cluster = ctx.cluster\n\n    tpl_db = cluster.get_db_name(edbdef.EDGEDB_TEMPLATE_DB)\n    conn = await cluster.connect(\n        source_description=\"inplace upgrade\",\n        database=tpl_db\n    )\n\n    # FIXME: Use the sys query instead?\n    try:\n        databases = json.loads(await conn.sql_fetch_val(\n            trampoline.fixup_query(\"\"\"\n                SELECT json_agg(name) FROM edgedb_VER.\"_SysBranch\";\n            \"\"\").encode('utf-8'),\n        ))\n    finally:\n        conn.terminate()\n\n    # DEBUG VELOCITY HACK: You can add a failing database to EARLY\n    # when trying to upgrade the whole suite.\n    #\n    # Note: We put template last, since when deleting, we need it to\n    # stay around so we can query all branches.\n    EARLY: tuple[str, ...] = ()\n    databases.sort(\n        key=lambda k: (k == edbdef.EDGEDB_TEMPLATE_DB, k not in EARLY, k)\n    )\n\n    return databases\n\n\nasync def _get_global_schema(\n    ctx: bootstrap.BootstrapContext,\n    state: edbcompiler.CompilerState,\n) -> s_schema.Schema:\n    cluster = ctx.cluster\n\n    tpl_db = cluster.get_db_name(edbdef.EDGEDB_TEMPLATE_DB)\n    conn = await cluster.connect(\n        source_description=\"inplace upgrade\",\n        database=tpl_db\n    )\n\n    # FIXME: Use the sys query instead?\n    assert state.global_intro_query\n    try:\n        json_data = await conn.sql_fetch_val(\n            state.global_intro_query.encode('utf-8'))\n    finally:\n        conn.terminate()\n\n    return s_refl.parse_schema(\n        base_schema=state.std_schema,\n        data=json_data,\n        schema_class_layout=state.schema_class_layout,\n    )\n\n\nasync def _rollback_one(\n    ctx: bootstrap.BootstrapContext,\n) -> None:\n    conn = ctx.conn\n\n    namespaces = await _get_namespaces(conn)\n    if pg_common.versioned_schema(\"edgedb\") not in namespaces:\n        logger.info(f\"Database already rolled back or not prepared; skipping\")\n        return\n\n    if (await instdata.get_instdata(conn, 'upgrade_finalized', 'text')) == b'1':\n        logger.info(f\"Database upgrade already finalized\")\n        raise errors.ConfigurationError(\n            f\"attempting to rollback database that has already begun \"\n            f\"finalization: retry finalize instead\"\n        )\n\n    cur_suffix = pg_common.versioned_schema(\"\")\n    to_delete = [x for x in namespaces if x.endswith(cur_suffix)]\n\n    await _delete_schemas(conn, to_delete)\n\n\nasync def _rollback_all(\n    ctx: bootstrap.BootstrapContext,\n) -> None:\n    cluster = ctx.cluster\n    databases = await _get_databases(ctx)\n\n    for database in databases:\n        if database == os.environ.get(\n            'EDGEDB_UPGRADE_ROLLBACK_ERROR_INJECTION'\n        ):\n            raise AssertionError(f'failure injected on {database}')\n\n        conn = bootstrap.PGConnectionProxy(\n            cluster,\n            source_description='inplace upgrade: rollback all',\n            dbname=cluster.get_db_name(database),\n        )\n        try:\n            subctx = dataclasses.replace(ctx, conn=conn)\n\n            logger.info(f\"Rolling back preparation of database '{database}'\")\n            await _rollback_one(ctx=subctx)\n        finally:\n            conn.terminate()\n\n\nasync def _upgrade_all(\n    ctx: bootstrap.BootstrapContext,\n) -> None:\n    cluster = ctx.cluster\n\n    stdlib, compiler = (await bootstrap._bootstrap(ctx))\n    state = compiler.state\n    databases = await _get_databases(ctx)\n\n    std_global_schema = stdlib.global_schema\n    global_schema = await _get_global_schema(ctx, state)\n\n    assert ctx.args.inplace_upgrade_prepare\n    with open(ctx.args.inplace_upgrade_prepare) as f:\n        upgrade_data = json.load(f)\n\n    for database in databases:\n        if database == edbdef.EDGEDB_TEMPLATE_DB:\n            continue\n\n        conn = bootstrap.PGConnectionProxy(\n            cluster,\n            source_description=\"inplace upgrade: upgrade all\",\n            dbname=cluster.get_db_name(database)\n        )\n        try:\n            subctx = dataclasses.replace(ctx, conn=conn)\n\n            logger.info(f\"Upgrading database '{database}'\")\n            await bootstrap._bootstrap(ctx=subctx, no_template=True)\n\n            logger.info(f\"Populating schema tables for '{database}'\")\n            await _upgrade_one(\n                ctx=subctx,\n                state=state,\n                global_schema=global_schema,\n                std_global_schema=std_global_schema,\n                upgrade_data=upgrade_data.get(database),\n            )\n        finally:\n            conn.terminate()\n\n\nasync def _finalize_all(\n    ctx: bootstrap.BootstrapContext,\n) -> None:\n    cluster = ctx.cluster\n    databases = await _get_databases(ctx)\n\n    async def go(\n        message: str,\n        finish_message: Optional[str],\n        final_command: bytes,\n        inject_failure_on: Optional[str]=None,\n    ) -> None:\n        for database in databases:\n            conn = await cluster.connect(\n                source_description=\"inplace upgrade: finish\",\n                database=cluster.get_db_name(database)\n            )\n\n            tmp_table_query = (\n                pgcon.SETUP_TEMP_TABLE_SCRIPT\n                + pgcon.SETUP_DML_DUMMY_TABLE_SCRIPT\n            )\n            await conn.sql_execute(tmp_table_query.encode('utf-8'))\n\n            try:\n                subctx = dataclasses.replace(ctx, conn=conn)\n\n                logger.info(f\"{message} database '{database}'\")\n                await conn.sql_execute(b'START TRANSACTION')\n                # DEBUG HOOK: Inject a failure if specified\n                if database == inject_failure_on:\n                    raise AssertionError(f'failure injected on {database}')\n\n                await _finalize_one(subctx, database)\n                await conn.sql_execute(final_command)\n                if finish_message:\n                    logger.info(f\"{finish_message} database '{database}'\")\n            finally:\n                conn.terminate()\n\n    inject_failure = os.environ.get('EDGEDB_UPGRADE_FINALIZE_ERROR_INJECTION')\n\n    # Test all of the pivots in transactions we rollback, to make sure\n    # that they work. This ensures that if there is a bug in the pivot\n    # scripts on some database, we fail before any irreversible\n    # changes are made to any database.\n    #\n    # *Then*, apply them all for real. They may fail\n    # when applying for real, but that should be due to a crash or\n    # some such, and so the user should be able to retry.\n    #\n    # We wanted to apply them all inside transactions and then commit\n    # the transactions, but that requires holding open potentially too\n    # many connections.\n    await go(\"Testing pivot of\", None, b'ROLLBACK')\n    await go(\"Pivoting\", \"Finished pivoting\", b'COMMIT', inject_failure)\n\n\nasync def inplace_upgrade(\n    cluster: pgcluster.BaseCluster,\n    args: edbargs.ServerConfig,\n) -> None:\n    \"\"\"Perform some or all of the inplace upgrade operations\"\"\"\n    pgconn = bootstrap.PGConnectionProxy(\n        cluster,\n        source_description=\"inplace_upgrade\"\n    )\n    ctx = bootstrap.BootstrapContext(cluster=cluster, conn=pgconn, args=args)\n\n    try:\n        # XXX: Do we need to do this?\n        mode = await bootstrap._get_cluster_mode(ctx)\n        ctx = dataclasses.replace(ctx, mode=mode)\n\n        if args.inplace_upgrade_rollback:\n            await _rollback_all(ctx)\n\n        if args.inplace_upgrade_prepare:\n            await _upgrade_all(ctx)\n\n        if args.inplace_upgrade_finalize:\n            await _finalize_all(ctx)\n\n    finally:\n        pgconn.terminate()\n"
  },
  {
    "path": "edb/server/instdata.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    TYPE_CHECKING,\n)\n\n\nfrom edb.pgsql import common as pg_common\n\nif TYPE_CHECKING:\n    from edb.pgsql import metaschema\n\n\nasync def get_instdata(\n    backend_conn: metaschema.PGConnection,\n    key: str,\n    field: str,\n    versioned: bool = True,\n) -> bytes | Any:\n    if field == 'json':\n        field = 'json::json'\n\n    if versioned:\n        schema = pg_common.versioned_schema('edgedbinstdata')\n    else:\n        schema = 'edgedbinstdata'\n    return await backend_conn.sql_fetch_val(\n        f\"\"\"\n        SELECT {field} FROM {schema}.instdata\n        WHERE key = $1\n        \"\"\".encode('utf-8'),\n        args=[key.encode(\"utf-8\")],\n    )\n"
  },
  {
    "path": "edb/server/logsetup.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport contextlib\nimport copy\nimport io\nimport logging\nimport logging.handlers\nimport sys\nimport warnings\n\nfrom edb.common import debug\nfrom edb.common import term\n\n\nLOG_LEVELS = {\n    'S': 'SILENT',\n    'D': 'DEBUG',\n    'I': 'INFO',\n    'E': 'ERROR',\n    'W': 'WARN',\n    'WARN': 'WARN',\n    'ERROR': 'ERROR',\n    'CRITICAL': 'CRITICAL',\n    'INFO': 'INFO',\n    'DEBUG': 'DEBUG',\n    'SILENT': 'SILENT'\n}\n\n\nclass Dark16:\n    critical = term.Style16(color='white', bgcolor='red', bold=True)\n    error = term.Style16(color='white', bgcolor='red')\n    default = term.Style16(color='white', bgcolor='blue')\n    pid = date = term.Style16(color='black', bold=True)\n    name = term.Style16(color='black', bold=True)\n    message = term.Style16()\n\n\nclass Dark256:\n    critical = term.Style256(color='#c6c6c6', bgcolor='#870000', bold=True)\n    error = term.Style256(color='#c6c6c6', bgcolor='#870000')\n    warning = term.Style256(color='#c6c6c6', bgcolor='#5f00d7')\n    info = term.Style256(color='#c6c6c6', bgcolor='#005f00')\n    default = term.Style256(color='#c6c6c6', bgcolor='#000087')\n    pid = date = term.Style256(color='#626262', bold=True)\n    name = term.Style256(color='#A2A2A2')\n    message = term.Style16()\n\n\nclass EdgeDBLogFormatter(logging.Formatter):\n\n    default_time_format = '%Y-%m-%dT%H:%M:%S'\n    default_msec_format = '%s.%03d'\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.__styles = None\n        self._colorize = term.use_colors()\n\n        if self._colorize:\n            self._init_styles()\n\n    def _init_styles(self):\n        if not self.__styles:\n            if term.max_colors() >= 255:\n                self.__styles = Dark256()\n            else:\n                self.__styles = Dark16()\n\n    def formatTime(self, record, datefmt=None):\n        time = super().formatTime(record, datefmt=datefmt)\n        if self._colorize:\n            time = self.__styles.date.apply(time)\n        return time\n\n    def formatException(self, ei):\n        sio = io.StringIO()\n        with contextlib.redirect_stdout(sio):\n            sys.excepthook(*ei)\n\n        s = sio.getvalue()\n        sio.close()\n        if s[-1:] == \"\\n\":\n            s = s[:-1]\n\n        return s\n\n    def format(self, record):\n        if self._colorize:\n            record = copy.copy(record)\n\n            level = record.levelname\n            level_style = getattr(self.__styles, level.lower(),\n                                  self.__styles.default)\n            record.levelname = level_style.apply(level)\n            record.process = self.__styles.pid.apply(str(record.process))\n            record.message = self.__styles.message.apply(record.getMessage())\n            record.name = self.__styles.name.apply(record.name)\n\n        return super().format(record)\n\n\nclass EdgeDBLogHandler(logging.StreamHandler):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        fmt = EdgeDBLogFormatter(\n            '{levelname} {process} {tenant} {asctime} {name}: {message}',\n            style='{')\n\n        self.setFormatter(fmt)\n\n\nIGNORE_DEPRECATIONS_IN = {\n    'graphql',\n    'promise',\n}\n\n\ndef setup_logging(log_level, log_destination):\n    log_level = log_level.upper()\n    try:\n        log_level = LOG_LEVELS[log_level]\n    except KeyError:\n        raise RuntimeError('Invalid logging level {!r}'.format(log_level))\n\n    if log_level == 'SILENT':\n        logger = logging.getLogger()\n        logger.disabled = True\n        logger.setLevel(logging.CRITICAL)\n        return\n\n    if log_destination == 'syslog':\n        fmt = logging.Formatter(\n            '{processName}[{process}]: {tenant}: {name}: {message}',\n            style='{')\n        handler = logging.handlers.SysLogHandler(\n            '/dev/log',\n            facility=logging.handlers.SysLogHandler.LOG_DAEMON)\n        handler.setFormatter(fmt)\n\n    elif log_destination == 'stderr':\n        handler = EdgeDBLogHandler()\n\n    else:\n        fmt = logging.Formatter(\n            '{levelname} {process} {tenant} {asctime} {name}: {message}',\n            style='{')\n        handler = logging.FileHandler(log_destination)\n        handler.setFormatter(fmt)\n\n    log_level = logging._checkLevel(log_level)\n\n    logger = logging.getLogger()\n    logger.setLevel(log_level)\n    logger.addHandler(handler)\n\n    # Channel warnings into logging system\n    logging.captureWarnings(True)\n\n    # Show DeprecationWarnings by default ...\n    warnings.simplefilter('default', category=DeprecationWarning)\n    # ... except for some third-party` modules.\n    for ignored_module in IGNORE_DEPRECATIONS_IN:\n        warnings.filterwarnings('ignore', category=DeprecationWarning,\n                                module=ignored_module)\n\n    if not debug.flags.log_metrics:\n        log_metrics = logging.getLogger('edb.server.metrics')\n        log_metrics.setLevel(logging.ERROR)\n"
  },
  {
    "path": "edb/server/main.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    Iterator,\n    Mapping,\n    NoReturn,\n    TYPE_CHECKING,\n)\n\nfrom edb.common.log import early_setup\n# ruff: noqa: E402\nearly_setup()\n\nimport asyncio\nimport contextlib\nimport json\nimport logging\nimport os\nimport os.path\nimport pathlib\n\nimport immutables\nimport resource\nimport signal\nimport sys\nimport tempfile\n\nimport click\nimport setproctitle\nimport uvloop\n\nfrom edb import buildmeta\nfrom edb import errors\nfrom edb.ir import statypes\nfrom edb.common import exceptions\nfrom edb.common import devmode\nfrom edb.common import signalctl\nfrom edb.common import debug\n\nfrom . import config\nfrom . import args as srvargs\nfrom . import compiler as edbcompiler\nfrom . import daemon\nfrom . import defines\nfrom . import logsetup\nfrom . import pgcluster\nfrom . import service_manager\n\n\nif TYPE_CHECKING:\n    from . import server\n    from edb.server import bootstrap\nelse:\n    # Import server lazily to make sure that most of imports happen\n    # under coverage (if we're testing with it).  Otherwise\n    # coverage will fail to detect that \"import edb...\" lines\n    # actually were run.\n    server = None\n\n\nlogger = logging.getLogger('edb.server')\n_server_initialized = False\n\n\ndef abort(msg, *args, exit_code=1) -> NoReturn:\n    logger.critical(msg, *args)\n    sys.exit(exit_code)\n\n\n@contextlib.contextmanager\ndef _ensure_runstate_dir(\n    default_runstate_dir: Optional[pathlib.Path],\n    specified_runstate_dir: Optional[pathlib.Path]\n) -> Iterator[pathlib.Path]:\n    temp_runstate_dir = None\n\n    if specified_runstate_dir is None:\n        if default_runstate_dir is None:\n            temp_runstate_dir = tempfile.TemporaryDirectory(prefix='edbrun-')\n            runstate_parent = pathlib.Path(temp_runstate_dir.name)\n        else:\n            runstate_parent = default_runstate_dir\n\n        try:\n            runstate_dir = buildmeta.get_runstate_path(runstate_parent)\n        except buildmeta.MetadataError:\n            abort(\n                f'cannot determine the runstate directory location; '\n                f'please use --runstate-dir to specify the correct location')\n    else:\n        runstate_dir = specified_runstate_dir\n\n    runstate_dir = pathlib.Path(runstate_dir)\n\n    if not runstate_dir.exists():\n        try:\n            runstate_dir.mkdir(parents=True)\n        except PermissionError as ex:\n            abort(\n                f'cannot create the runstate directory: '\n                f'{ex!s}; please use --runstate-dir to specify '\n                f'the correct location')\n\n    if not os.path.isdir(runstate_dir):\n        abort(f'{str(runstate_dir)!r} is not a directory; please use '\n              f'--runstate-dir to specify the correct location')\n\n    try:\n        yield runstate_dir\n    finally:\n        if temp_runstate_dir is not None:\n            temp_runstate_dir.cleanup()\n\n\n@contextlib.contextmanager\ndef _internal_state_dir(\n    runstate_dir: pathlib.Path, args: srvargs.ServerConfig\n) -> Iterator[tuple[pathlib.Path, srvargs.ServerConfig]]:\n    try:\n        with tempfile.TemporaryDirectory(prefix=\"\", dir=runstate_dir) as td:\n            if (\n                args.tls_cert_file\n                and '<runstate>' in str(args.tls_cert_file)\n            ):\n                args = args._replace(\n                    tls_cert_file=pathlib.Path(\n                        str(args.tls_cert_file).replace(\n                            '<runstate>', td)\n                    ),\n                    tls_key_file=pathlib.Path(\n                        str(args.tls_key_file).replace(\n                            '<runstate>', td)\n                    )\n                )\n            if (\n                args.jws_key_file\n                and '<runstate>' in str(args.jws_key_file)\n            ):\n                args = args._replace(\n                    jws_key_file=pathlib.Path(\n                        str(args.jws_key_file).replace(\n                            '<runstate>', td)\n                    ),\n                )\n            tdp = pathlib.Path(td)\n            yield tdp, args\n    except PermissionError as ex:\n        abort(f'cannot write to the runstate directory: '\n              f'{ex!s}; please fix the permissions or use '\n              f'--runstate-dir to specify the correct location')\n\n\nasync def _init_cluster(\n    cluster: pgcluster.BaseCluster, args: srvargs.ServerConfig\n) -> tuple[bool, edbcompiler.Compiler]:\n    from edb.server import bootstrap\n\n    new_instance = await bootstrap.ensure_bootstrapped(cluster, args)\n    global _server_initialized\n    _server_initialized = True\n\n    return new_instance\n\n\ndef _init_parsers():\n    # Initialize parsers that are used in the server process.\n    from edb.edgeql import parser as ql_parser\n\n    ql_parser.preload_spec()\n\n\nasync def _run_server(\n    cluster: pgcluster.BaseCluster,\n    args: srvargs.ServerConfig,\n    runstate_dir: pathlib.Path,\n    internal_runstate_dir: pathlib.Path,\n    *,\n    do_setproctitle: bool,\n    new_instance: bool,\n    compiler: edbcompiler.Compiler,\n    init_con_data: list[config.ConState],\n):\n\n    sockets = service_manager.get_activation_listen_sockets()\n\n    if sockets:\n        logger.info(\"detected service manager socket activation\")\n\n    with signalctl.SignalController(signal.SIGINT, signal.SIGTERM) as sc:\n        from . import tenant as edbtenant\n\n        # max_backend_connections should've been calculated already by now\n        assert args.max_backend_connections is not None\n        tenant = edbtenant.Tenant(\n            cluster,\n            instance_name=args.instance_name,\n            max_backend_connections=args.max_backend_connections,\n            backend_adaptive_ha=args.backend_adaptive_ha,\n            extensions_dir=args.extensions_dir,\n        )\n        tenant.set_init_con_data(init_con_data)\n        tenant.set_reloadable_files(\n            readiness_state_file=args.readiness_state_file,\n            jwt_sub_allowlist_file=args.jwt_sub_allowlist_file,\n            jwt_revocation_list_file=args.jwt_revocation_list_file,\n            config_file=args.config_file,\n        )\n        ss = server.Server(\n            runstate_dir=runstate_dir,\n            internal_runstate_dir=internal_runstate_dir,\n            compiler_pool_size=args.compiler_pool_size,\n            compiler_worker_branch_limit=args.compiler_worker_branch_limit,\n            compiler_pool_mode=args.compiler_pool_mode,\n            compiler_pool_addr=args.compiler_pool_addr,\n            compiler_worker_max_rss=args.compiler_worker_max_rss,\n            nethosts=args.bind_addresses,\n            netport=args.port,\n            listen_sockets=tuple(s for ss in sockets.values() for s in ss),\n            auto_shutdown_after=args.auto_shutdown_after,\n            echo_runtime_info=args.echo_runtime_info,\n            status_sinks=args.status_sinks,\n            startup_script=args.startup_script,\n            binary_endpoint_security=args.binary_endpoint_security,\n            http_endpoint_security=args.http_endpoint_security,\n            default_auth_method=args.default_auth_method,\n            testmode=args.testmode,\n            daemonized=args.background,\n            pidfile_dir=args.pidfile_dir,\n            new_instance=new_instance,\n            admin_ui=args.admin_ui,\n            cors_always_allowed_origins=args.cors_always_allowed_origins,\n            disable_dynamic_system_config=args.disable_dynamic_system_config,\n            compiler_state=compiler.state,\n            tenant=tenant,\n            use_monitor_fs=args.reload_config_files in [\n                srvargs.ReloadTrigger.Default,\n                srvargs.ReloadTrigger.FileSystemEvent,\n            ],\n            net_worker_mode=args.net_worker_mode,\n        )\n        magic_smtp = os.getenv('EDGEDB_MAGIC_SMTP_CONFIG')\n        if magic_smtp:\n            await tenant.load_sidechannel_configs(\n                json.loads(magic_smtp), compiler=compiler\n            )\n        if args.config_file:\n            await tenant.load_config_file(compiler)\n        # This coroutine runs as long as the server,\n        # and compiler(.state) is *heavy*, so make sure we don't\n        # keep a reference to it.\n        del compiler\n        await sc.wait_for(ss.init())\n\n        (\n            tls_cert_newly_generated, jws_keys_newly_generated\n        ) = await ss.maybe_generate_pki(args, ss)\n\n        if args.bootstrap_only:\n            if args.startup_script and new_instance:\n                await sc.wait_for(ss.run_startup_script_and_exit())\n            return\n\n        ss.init_tls(\n            args.tls_cert_file,\n            args.tls_key_file,\n            tls_cert_newly_generated,\n            args.tls_client_ca_file,\n        )\n\n        ss.init_jwcrypto(args.jws_key_file, jws_keys_newly_generated)\n        ss.start_watching_files()\n\n        def load_configuration(_signum):\n            if args.reload_config_files not in [\n                srvargs.ReloadTrigger.Default,\n                srvargs.ReloadTrigger.Signal,\n            ]:\n                logger.info(\n                    \"SIGHUP received, but reload on signal is disabled\"\n                )\n                return\n\n            logger.info(\"reloading configuration\")\n            try:\n                if args.readiness_state_file:\n                    tenant.reload_readiness_state()\n                ss.reload_tls(\n                    args.tls_cert_file,\n                    args.tls_key_file,\n                    args.tls_client_ca_file,\n                )\n                ss.load_jwcrypto(args.jws_key_file)\n                tenant.reload_config_file.schedule()\n            except Exception:\n                logger.critical(\n                    \"Unexpected error occurred during reload configuration; \"\n                    \"shutting down.\",\n                    exc_info=True,\n                )\n                ss.request_shutdown()\n\n        try:\n            await sc.wait_for(ss.start())\n\n            if do_setproctitle:\n                setproctitle.setproctitle(\n                    f\"edgedb-server-{ss.get_listen_port()}\"\n                )\n\n            # Notify systemd that we've started up.\n            service_manager.sd_notify('READY=1')\n\n            with signalctl.SignalController(signal.SIGHUP) as reload_ctl:\n                reload_ctl.add_handler(\n                    load_configuration,\n                    signals=(signal.SIGHUP,)\n                )\n\n                try:\n                    await sc.wait_for(ss.serve_forever())\n                except signalctl.SignalError as e:\n                    logger.info('Received signal: %s.', e.signo)\n        finally:\n            service_manager.sd_notify('STOPPING=1')\n            logger.info('Shutting down.')\n            await sc.wait_for(ss.stop())\n\n\nasync def _get_local_pgcluster(\n    args: srvargs.ServerConfig,\n    runstate_dir: pathlib.Path,\n    tenant_id: str,\n) -> tuple[pgcluster.Cluster, srvargs.ServerConfig]:\n    pg_max_connections = args.max_backend_connections\n    if not pg_max_connections:\n        max_conns = srvargs.compute_default_max_backend_connections()\n        pg_max_connections = max_conns\n        if args.testmode:\n            max_conns = srvargs.adjust_testmode_max_connections(max_conns)\n            logger.info(f'Configuring Postgres max_connections='\n                        f'{pg_max_connections} under test mode.')\n        args = args._replace(max_backend_connections=max_conns)\n        logger.info(f'Using {max_conns} max backend connections based on '\n                    f'total memory.')\n\n    cluster = await pgcluster.get_local_pg_cluster(\n        args.data_dir,\n        runstate_dir=runstate_dir,\n        # Plus two below to account for system connections.\n        max_connections=pg_max_connections + 2,\n        tenant_id=tenant_id,\n        log_level=args.log_level,\n    )\n    cluster.update_connection_params(\n        user='postgres',\n        database='template1',\n        server_settings={\n            \"application_name\": f'edgedb_instance_{args.instance_name}',\n        }\n    )\n    return cluster, args\n\n\nasync def _get_remote_pgcluster(\n    args: srvargs.ServerConfig,\n    tenant_id: str,\n) -> tuple[pgcluster.RemoteCluster, srvargs.ServerConfig]:\n\n    cluster = await pgcluster.get_remote_pg_cluster(\n        args.backend_dsn,\n        tenant_id=tenant_id,\n        specified_capabilities=args.backend_capability_sets,\n    )\n\n    instance_params = cluster.get_runtime_params().instance_params\n    max_conns = (\n        instance_params.max_connections -\n        instance_params.reserved_connections)\n    if not args.max_backend_connections:\n        logger.info(f'Detected {max_conns} backend connections available.')\n        if args.testmode:\n            max_conns = srvargs.adjust_testmode_max_connections(max_conns)\n            logger.info(f'Using max_backend_connections={max_conns} '\n                        f'under test mode.')\n        args = args._replace(max_backend_connections=max_conns)\n    elif args.max_backend_connections > max_conns:\n        abort(f'--max-backend-connections is too large for this backend; '\n              f'detected maximum available NUM: {max_conns}')\n\n    cluster.update_connection_params(server_settings={\n        'application_name': f'edgedb_instance_{args.instance_name}'\n    })\n\n    return cluster, args\n\n\ndef _patch_stdlib_testmode(\n    stdlib: bootstrap.StdlibBits\n) -> bootstrap.StdlibBits:\n    from edb import edgeql\n    from edb.pgsql import delta as delta_cmds\n    from edb.pgsql import params as pg_params\n    from edb.edgeql import ast as qlast\n    from edb.schema import ddl as s_ddl\n    from edb.schema import delta as sd\n    from edb.schema import schema as s_schema\n    from edb.schema import std as s_std\n\n    schema: s_schema.Schema = s_schema.ChainedSchema(\n        s_schema.EMPTY_SCHEMA,\n        stdlib.stdschema,\n        stdlib.global_schema,\n    )\n    reflschema = stdlib.reflschema\n    ctx = sd.CommandContext(\n        stdmode=True,\n        backend_runtime_params=pg_params.get_default_runtime_params(),\n    )\n\n    for modname in s_schema.TESTMODE_SOURCES:\n        ddl_text = s_std.get_std_module_text(modname)\n        for ddl_cmd in edgeql.parse_block(ddl_text):\n            assert isinstance(ddl_cmd, qlast.DDLCommand)\n            delta = s_ddl.delta_from_ddl(\n                ddl_cmd, modaliases={}, schema=schema, stdmode=True\n            )\n            if not delta.canonical:\n                sd.apply(delta, schema=schema)\n            delta = delta_cmds.CommandMeta.adapt(delta)\n            schema = sd.apply(delta, schema=schema, context=ctx)\n            reflschema = delta.apply(reflschema, ctx)\n\n    assert isinstance(schema, s_schema.ChainedSchema)\n    return stdlib._replace(\n        stdschema=schema.get_top_schema(),\n        global_schema=schema.get_global_schema(),\n        reflschema=reflschema,\n    )\n\n\nasync def run_server(\n    args: srvargs.ServerConfig,\n    *,\n    do_setproctitle: bool=False,\n    runstate_dir: pathlib.Path,\n) -> None:\n    from . import server as server_mod\n    global server\n    server = server_mod\n\n    logsetup.setup_logging(args.log_level, args.log_to)\n\n    logger.info(f\"starting Gel server {buildmeta.get_version_line()}\")\n    if args.multitenant_config_file:\n        logger.info(\"configured as a multitenant instance\")\n    else:\n        logger.info(f'instance name: {args.instance_name!r}')\n    if devmode.is_in_dev_mode():\n        logger.info(f'development mode active')\n\n    if fd_str := os.environ.get(\"EDGEDB_SERVER_EXTERNAL_LOCK_FD\"):\n        try:\n            fd = int(fd_str)\n        except ValueError:\n            logger.info(\"Invalid EDGEDB_SERVER_EXTERNAL_LOCK_FD\")\n        else:\n            os.set_inheritable(fd, False)\n    if fd_str := os.environ.get(\"GEL_SERVER_EXTERNAL_LOCK_FD\"):\n        try:\n            fd = int(fd_str)\n        except ValueError:\n            logger.info(\"Invalid GEL_SERVER_EXTERNAL_LOCK_FD\")\n        else:\n            os.set_inheritable(fd, False)\n\n    logger.debug(\n        f\"defaulting to the '{args.default_auth_method}' authentication method\"\n    )\n\n    if debug.flags.pydebug_listen:\n        import debugpy\n        debugpy.listen(38782)\n\n    _init_parsers()\n\n    pg_cluster_init_by_us = False\n\n    if args.tenant_id is None:\n        tenant_id = buildmeta.get_default_tenant_id()\n    else:\n        tenant_id = f'C{args.tenant_id}'\n\n    cluster: pgcluster.Cluster | pgcluster.RemoteCluster\n\n    runstate_dir_str = str(runstate_dir)\n    runstate_dir_str_len = len(\n        runstate_dir_str.encode(\n            sys.getfilesystemencoding(),\n            errors=sys.getfilesystemencodeerrors(),\n        ),\n    )\n    if runstate_dir_str_len > defines.MAX_RUNSTATE_DIR_PATH:\n        abort(\n            f'the length of the specified path for server run state '\n            f'exceeds the maximum of {defines.MAX_RUNSTATE_DIR_PATH} '\n            f'bytes: {runstate_dir_str!r} ({runstate_dir_str_len} bytes)',\n            exit_code=11,\n        )\n\n    if args.multitenant_config_file:\n        from edb.schema import reflection as s_refl\n        from . import bootstrap\n        from . import multitenant\n\n        try:\n            stdlib: bootstrap.StdlibBits | None\n            stdlib = bootstrap.read_data_cache(\n                bootstrap.STDLIB_CACHE_FILE_NAME, pickled=True\n            )\n            if stdlib is None:\n                abort(\n                    \"Cannot run multi-tenant server \"\n                    \"without pre-compiled standard library\"\n                )\n            if args.testmode:\n                # In multitenant mode, the server/compiler is started without a\n                # backend and will be connected to many backends. That means we\n                # cannot load the stdlib from a certain backend; instead, the\n                # pre-compiled stdlib is always in use. This means that we need\n                # to explicitly enable --testmode starting a multitenant server\n                # in order to handle backends with test-mode schema properly.\n                try:\n                    stdlib = _patch_stdlib_testmode(stdlib)\n                except errors.SchemaError:\n                    # The pre-compiled standard library already has test-mode\n                    # schema; ignore the patching error.\n                    pass\n\n            compiler = edbcompiler.new_compiler(\n                stdlib.stdschema,\n                stdlib.reflschema,\n                stdlib.classlayout,\n                config_spec=None,\n            )\n            reflection = s_refl.generate_structure(\n                stdlib.reflschema, make_funcs=False,\n            )\n            (\n                local_intro_sql, global_intro_sql\n            ) = bootstrap.compile_intro_queries_stdlib(\n                compiler=compiler,\n                user_schema=stdlib.reflschema,\n                reflection=reflection,\n            )\n            del reflection\n            compiler_state = edbcompiler.CompilerState(\n                std_schema=compiler.state.std_schema,\n                refl_schema=compiler.state.refl_schema,\n                schema_class_layout=stdlib.classlayout,\n                backend_runtime_params=(\n                    compiler.state.backend_runtime_params\n                ),\n                config_spec=compiler.state.config_spec,\n                local_intro_query=local_intro_sql,\n                global_intro_query=global_intro_sql,\n            )\n            del local_intro_sql, global_intro_sql\n            (\n                sys_queries,\n                report_configs_typedesc_1_0,\n                report_configs_typedesc_2_0,\n            ) = bootstrap.compile_sys_queries(\n                stdlib.reflschema,\n                compiler,\n                compiler_state.config_spec,\n            )\n\n            sys_config, backend_settings, init_con_data = (\n                initialize_static_cfg(\n                    args,\n                    is_remote_cluster=True,\n                    compiler=compiler,\n                )\n            )\n            del compiler\n            if backend_settings:\n                abort(\n                    'Static backend settings for remote backend are '\n                    'not supported'\n                )\n            with _internal_state_dir(runstate_dir, args) as (\n                int_runstate_dir,\n                args,\n            ):\n                return await multitenant.run_server(\n                    args,\n                    sys_config=sys_config,\n                    sys_queries={\n                        key: sql.encode(\"utf-8\")\n                        for key, sql in sys_queries.items()\n                    },\n                    report_config_typedesc={\n                        (1, 0): report_configs_typedesc_1_0,\n                        (2, 0): report_configs_typedesc_2_0,\n                    },\n                    runstate_dir=runstate_dir,\n                    internal_runstate_dir=int_runstate_dir,\n                    do_setproctitle=do_setproctitle,\n                    compiler_state=compiler_state,\n                    init_con_data=init_con_data,\n                )\n        except server.StartupError as e:\n            abort(str(e))\n\n    try:\n        if args.data_dir:\n            cluster, args = await _get_local_pgcluster(\n                args, runstate_dir, tenant_id)\n        elif args.backend_dsn:\n            cluster, args = await _get_remote_pgcluster(args, tenant_id)\n        else:\n            # This should have been checked by main() already,\n            # but be extra careful.\n            abort('neither the data directory nor the remote Postgres DSN '\n                  'have been specified')\n    except pgcluster.ClusterError as e:\n        abort(str(e))\n\n    try:\n        pg_cluster_init_by_us = await cluster.ensure_initialized()\n        cluster_status = await cluster.get_status()\n        logger.debug(\"postgres cluster status: %s\", cluster_status)\n\n        if isinstance(cluster, pgcluster.Cluster):\n            is_local_cluster = True\n            if cluster_status == 'running':\n                # Refuse to start local instance on an occupied datadir,\n                # as it's very likely that Postgres was orphaned by an\n                # earlier unclean exit of EdgeDB.\n                main_pid = cluster.get_main_pid() or '<unknown>'\n                abort(\n                    f'a PostgreSQL instance (PID {main_pid}) is already '\n                    f'running in data directory \"{args.data_dir}\", please '\n                    f'stop it to proceed'\n                )\n            elif cluster_status == 'stopped':\n                await cluster.start()\n            else:\n                abort('could not initialize data directory \"%s\"',\n                      args.data_dir)\n        else:\n            # We expect the remote cluster to be running\n            is_local_cluster = False\n            if cluster_status != \"running\":\n                abort('specified PostgreSQL instance is not running')\n\n        logger.info(\"postgres cluster is running\")\n\n        if (\n            args.inplace_upgrade_prepare\n            or args.inplace_upgrade_finalize\n            or args.inplace_upgrade_rollback\n        ):\n            from . import inplace_upgrade\n            await inplace_upgrade.inplace_upgrade(cluster, args)\n            return\n\n        new_instance, compiler = await _init_cluster(cluster, args)\n\n        _, backend_settings, init_con_data = initialize_static_cfg(\n            args,\n            is_remote_cluster=not is_local_cluster,\n            compiler=compiler,\n        )\n\n        if is_local_cluster:\n            if new_instance or backend_settings:\n                logger.info('Restarting server to reload configuration...')\n                await cluster.stop()\n                await cluster.start(server_settings=backend_settings)\n        elif backend_settings:\n            abort(\n                'Static backend settings for remote backend are not supported'\n            )\n        del backend_settings\n\n        if (\n            not args.bootstrap_only\n            or args.bootstrap_command_file\n            or args.bootstrap_command\n            or (\n                args.tls_cert_mode\n                is srvargs.ServerTlsCertMode.SelfSigned\n            )\n            or (\n                args.jose_key_mode\n                is srvargs.JOSEKeyMode.Generate\n            )\n        ):\n            instance_name = args.instance_name\n            database = pgcluster.get_database_backend_name(\n                defines.EDGEDB_TEMPLATE_DB,\n                tenant_id=tenant_id,\n            ) if args.data_dir else None\n            server_settings = {\n                'application_name': f'edgedb_instance_{instance_name}',\n                'edgedb.instance_name': instance_name,\n                'edgedb.server_version': buildmeta.get_version_json(),\n            }\n            if database:\n                cluster.update_connection_params(\n                    database=database,\n                    server_settings=server_settings\n                )\n            else:\n                cluster.update_connection_params(\n                    server_settings=server_settings\n                )\n\n            with _internal_state_dir(runstate_dir, args) as (\n                int_runstate_dir,\n                args,\n            ):\n                await _run_server(\n                    cluster,\n                    args,\n                    runstate_dir,\n                    int_runstate_dir,\n                    do_setproctitle=do_setproctitle,\n                    new_instance=new_instance,\n                    compiler=compiler,\n                    init_con_data=init_con_data,\n                )\n\n    except server.StartupError as e:\n        abort(str(e))\n\n    except BaseException:\n        if pg_cluster_init_by_us and not _server_initialized:\n            logger.warning(\n                'server bootstrap did not complete successfully, '\n                'removing the data directory')\n            if await cluster.get_status() == 'running':\n                await cluster.stop()\n            cluster.destroy()\n        raise\n\n    finally:\n        if args.temp_dir:\n            if await cluster.get_status() == 'running':\n                await cluster.stop()\n            cluster.destroy()\n        elif await cluster.get_status() == 'running':\n            await cluster.stop()\n\n\ndef bump_rlimit_nofile() -> None:\n    try:\n        fno_soft, fno_hard = resource.getrlimit(resource.RLIMIT_NOFILE)\n    except resource.error:\n        logger.warning('could not read RLIMIT_NOFILE')\n    else:\n        if fno_soft < defines.EDGEDB_MIN_RLIMIT_NOFILE:\n            try:\n                resource.setrlimit(\n                    resource.RLIMIT_NOFILE,\n                    (min(defines.EDGEDB_MIN_RLIMIT_NOFILE, fno_hard),\n                     fno_hard))\n            except resource.error:\n                logger.warning('could not set RLIMIT_NOFILE')\n\n\ndef server_main(**kwargs: Any) -> None:\n    exceptions.install_excepthook()\n\n    bump_rlimit_nofile()\n\n    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())\n\n    if kwargs['devmode'] is not None:\n        devmode.enable_dev_mode(kwargs['devmode'])\n\n    try:\n        server_args = srvargs.parse_args(**kwargs)\n    except srvargs.InvalidUsageError as e:\n        abort(e.args[0], exit_code=e.args[1])\n\n    if server_args.data_dir:\n        default_runstate_dir = server_args.data_dir\n    else:\n        default_runstate_dir = None\n\n    specified_runstate_dir: Optional[pathlib.Path]\n    if server_args.runstate_dir:\n        specified_runstate_dir = server_args.runstate_dir\n    elif server_args.bootstrap_only:\n        # When bootstrapping a new EdgeDB instance it is often necessary\n        # to avoid using the main runstate dir due to lack of permissions,\n        # possibility of conflict with another running instance, etc.\n        # The --bootstrap mode is also often runs unattended, i.e.\n        # as a post-install hook during package installation.\n        specified_runstate_dir = default_runstate_dir\n    else:\n        specified_runstate_dir = None\n\n    runstate_dir_mgr = _ensure_runstate_dir(\n        default_runstate_dir,\n        specified_runstate_dir,\n    )\n\n    with runstate_dir_mgr as runstate_dir:\n        if server_args.background:\n            daemon_opts: dict[str, Any] = {'detach_process': True}\n            if server_args.daemon_user:\n                daemon_opts['uid'] = server_args.daemon_user\n            if server_args.daemon_group:\n                daemon_opts['gid'] = server_args.daemon_group\n            with daemon.DaemonContext(**daemon_opts):\n                asyncio.run(run_server(\n                    server_args,\n                    runstate_dir=runstate_dir,\n                ))\n        else:\n            with devmode.CoverageConfig.enable_coverage_if_requested():\n                asyncio.run(run_server(\n                    server_args,\n                    runstate_dir=runstate_dir,\n                ))\n\n\n@click.group(\n    'Gel Server',\n    invoke_without_command=True,\n    context_settings=dict(help_option_names=['-h', '--help'])\n)\n@srvargs.server_options\n@click.pass_context\ndef main(ctx, version=False, **kwargs):\n    if kwargs.get('testmode') and 'GEL_TEST_CATALOG_VERSION' in os.environ:\n        buildmeta.EDGEDB_CATALOG_VERSION = int(\n            os.environ['GEL_TEST_CATALOG_VERSION']\n        )\n    elif kwargs.get('testmode') and 'EDGEDB_TEST_CATALOG_VERSION' in os.environ:\n        buildmeta.EDGEDB_CATALOG_VERSION = int(\n            os.environ['EDGEDB_TEST_CATALOG_VERSION']\n        )\n    if version:\n        print(f\"gel-server, version {buildmeta.get_version()}\")\n        sys.exit(0)\n    if ctx.invoked_subcommand is None:\n        server_main(**kwargs)\n\n\n@main.command(hidden=True)\n@srvargs.compiler_options\ndef compiler(**kwargs):\n    from edb.server.compiler_pool import server as compiler_server\n\n    asyncio.run(compiler_server.server_main(**kwargs))\n\n\ndef main_dev():\n    devmode.enable_dev_mode()\n    main()\n\n\ndef initialize_static_cfg(\n    args: srvargs.ServerConfig,\n    is_remote_cluster: bool,\n    compiler: edbcompiler.Compiler,\n) -> tuple[\n    Mapping[str, config.SettingValue], dict[str, str], list[config.ConState]\n]:\n    result = {}\n    init_con_script_data: list[config.ConState] = []\n    backend_settings = {}\n    config_spec = compiler.state.config_spec\n    sources = {\n        config.ConStateType.command_line_argument: \"command line argument\",\n        config.ConStateType.environment_variable: \"environment variable\",\n    }\n\n    def add_config_values(obj: dict[str, Any], source: config.ConStateType):\n        settings = compiler.compile_structured_config(\n            {\"cfg::Config\": obj}, source=sources[source]\n        )[\"cfg::Config\"]\n        for name, value in settings.items():\n            setting = config_spec[name]\n\n            if is_remote_cluster:\n                if setting.backend_setting and setting.requires_restart:\n                    if source == config.ConStateType.command_line_argument:\n                        where = \"on command line\"\n                    else:\n                        where = \"as an environment variable\"\n                    raise server.StartupError(\n                        f\"Can't set config {name!r} {where} when using \"\n                        f\"a remote Postgres cluster\"\n                    )\n            init_con_script_data.append({\n                \"name\": name,\n                \"value\": config.value_to_json_value(setting, value.value),\n                \"type\": source,\n            })\n            result[name] = value\n            if setting.backend_setting:\n                backend_val = value.value\n                if isinstance(backend_val, statypes.ScalarType):\n                    backend_val = backend_val.to_backend_str()\n                backend_settings[setting.backend_setting] = str(backend_val)\n\n    values: dict[str, Any] = {}\n    translate_env = {\n        \"EDGEDB_SERVER_BIND_ADDRESS\": \"listen_addresses\",\n        \"EDGEDB_SERVER_PORT\": \"listen_port\",\n        \"GEL_SERVER_BIND_ADDRESS\": \"listen_addresses\",\n        \"GEL_SERVER_PORT\": \"listen_port\",\n    }\n    for name, value in os.environ.items():\n        if cfg := translate_env.get(name):\n            values[cfg] = value\n        else:\n            cfg = name.removeprefix(\"EDGEDB_SERVER_CONFIG_cfg::\")\n            if cfg != name:\n                values[cfg] = value\n            else:\n                cfg = name.removeprefix(\"GEL_SERVER_CONFIG_cfg::\")\n                if cfg != name:\n                    values[cfg] = value\n    if values:\n        add_config_values(values, config.ConStateType.environment_variable)\n\n    values = {}\n    if args.bind_addresses:\n        values[\"listen_addresses\"] = args.bind_addresses\n    if args.port:\n        values[\"listen_port\"] = args.port\n    if values:\n        add_config_values(values, config.ConStateType.command_line_argument)\n\n    return immutables.Map(result), backend_settings, init_con_script_data\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "edb/server/metrics.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport os\nimport sys\n\nfrom edb.common import prometheus as prom\n\n\nregistry = prom.Registry(prefix='edgedb_server')\n\nCOUNT_BUCKETS = prom.per_order_buckets(\n    1, 10000, entries_per_order=2,\n)\nBYTES_BUCKETS = prom.per_order_buckets(\n    32, 2**20, entries_per_order=1, base=2,\n)\n\ncompiler_process_spawns = registry.new_counter(\n    'compiler_process_spawns_total',\n    'Total number of compiler processes spawned.'\n)\n\ncompiler_process_kills = registry.new_counter(\n    'compiler_process_kills_total',\n    'Total number of compiler processes killed.',\n)\n\ncurrent_compiler_processes = registry.new_gauge(\n    'compiler_processes_current',\n    'Current number of active compiler processes.'\n)\n\ncompiler_process_memory = registry.new_labeled_gauge(\n    'compiler_process_memory_bytes',\n    'Current memory usage of compiler processes in bytes.',\n    labels=('pid',),\n)\n\ncompiler_process_schema_size = registry.new_labeled_gauge(\n    'compiler_process_schema_size_bytes',\n    'Current size of compiler process schema cache in bytes.',\n    labels=('pid', 'client'),\n)\n\ncompiler_process_branches = registry.new_labeled_gauge(\n    'compiler_process_branches',\n    'Total number of branches cached in each compiler process.',\n    labels=('pid', 'client'),\n)\n\ncompiler_process_branch_actions = registry.new_labeled_counter(\n    'compiler_process_branch_actions_total',\n    'Number of different branch actions happened in each '\n    'compiler process.',\n    labels=('pid', 'client', 'action'),\n)\n\ncompiler_process_client_actions = registry.new_labeled_counter(\n    'compiler_process_client_actions_total',\n    'Number of different client actions happened in each '\n    'compiler process.',\n    labels=('pid', 'action'),\n)\n\ncompiler_pool_wait_time = registry.new_histogram(\n    'compiler_pool_wait_time',\n    'Time it takes to acquire a compiler process.',\n    unit=prom.Unit.SECONDS,\n)\n\ncompiler_pool_queue_errors = registry.new_labeled_counter(\n    'compiler_pool_queue_errors_total',\n    'Number of compiler pool errors in queue.',\n    labels=('type',),\n)\n\ncurrent_branches = registry.new_labeled_gauge(\n    'branches_current',\n    'Current number of branches.',\n    labels=('tenant',),\n)\n\ncurrent_introspected_branches = registry.new_labeled_gauge(\n    'branches_introspected_current',\n    'Current number of branches whose schemas are introspected.',\n    labels=('tenant',),\n)\n\ntotal_backend_connections = registry.new_labeled_counter(\n    'backend_connections_total',\n    'Total number of backend connections established.',\n    labels=('tenant',),\n)\n\ncurrent_backend_connections = registry.new_labeled_gauge(\n    'backend_connections_current',\n    'Current number of active backend connections.',\n    labels=('tenant',),\n)\n\nbackend_connection_establishment_errors = registry.new_labeled_counter(\n    'backend_connection_establishment_errors_total',\n    'Number of times the server could not establish a backend connection.',\n    labels=('tenant',),\n)\n\nbackend_connection_establishment_latency = registry.new_labeled_histogram(\n    'backend_connection_establishment_latency',\n    'Time it takes to establish a backend connection.',\n    unit=prom.Unit.SECONDS,\n    labels=('tenant',),\n)\n\nbackend_connection_aborted = registry.new_labeled_counter(\n    'backend_connections_aborted_total',\n    'Number of aborted backend connections.',\n    labels=('tenant', 'pgcode')\n)\n\nbackend_query_duration = registry.new_labeled_histogram(\n    'backend_query_duration',\n    'Time it takes to run a query on a backend connection.',\n    unit=prom.Unit.SECONDS,\n    labels=('tenant',),\n)\n\ntotal_client_connections = registry.new_labeled_counter(\n    'client_connections_total',\n    'Total number of clients.',\n    labels=('tenant',),\n)\n\ncurrent_client_connections = registry.new_labeled_gauge(\n    'client_connections_current',\n    'Current number of active clients.',\n    labels=('tenant',),\n)\n\nidle_client_connections = registry.new_labeled_counter(\n    'client_connections_idle_total',\n    'Total number of forcefully closed idle client connections.',\n    labels=('tenant',),\n)\n\nclient_connection_duration = registry.new_labeled_histogram(\n    'client_connection_duration',\n    'Time a client connection is open.',\n    unit=prom.Unit.SECONDS,\n    labels=('tenant', 'interface'),\n)\n\nedgeql_query_compilations = registry.new_labeled_counter(\n    'edgeql_query_compilations_total',\n    'Number of compiled/cached queries or scripts.',\n    labels=('tenant', 'path')\n)\n\nedgeql_query_compilation_duration = registry.new_labeled_histogram(\n    'edgeql_query_compilation_duration',\n    'Time it takes to compile an EdgeQL query or script.',\n    unit=prom.Unit.SECONDS,\n    labels=('tenant',),\n)\n\ngraphql_query_compilations = registry.new_labeled_counter(\n    'graphql_query_compilations_total',\n    'Number of compiled/cached GraphQL queries.',\n    labels=('tenant', 'path')\n)\n\nquery_compilation_duration = registry.new_labeled_histogram(\n    'query_compilation_duration',\n    'Time it takes to compile a query or script.',\n    unit=prom.Unit.SECONDS,\n    labels=('tenant', 'interface'),\n)\n\nsql_queries = registry.new_labeled_counter(\n    'sql_queries_total',\n    'Number of SQL queries.',\n    labels=('tenant',)\n)\n\nsql_compilations = registry.new_labeled_counter(\n    'sql_compilations_total',\n    'Number of SQL compilations.',\n    labels=('tenant',)\n)\n\nqueries_per_connection = registry.new_labeled_histogram(\n    'queries_per_connection',\n    'Number of queries per connection.',\n    buckets=COUNT_BUCKETS,\n    labels=('tenant', 'interface'),\n)\n\nquery_size = registry.new_labeled_histogram(\n    'query_size',\n    'The size of a query.',\n    unit=prom.Unit.BYTES,\n    buckets=BYTES_BUCKETS,\n    labels=('tenant', 'interface'),\n)\n\nbackground_errors = registry.new_labeled_counter(\n    'background_errors_total',\n    'Number of unhandled errors in background server routines.',\n    labels=('tenant', 'source')\n)\n\ntransaction_serialization_errors = registry.new_labeled_counter(\n    'transaction_serialization_errors_total',\n    'Number of transaction serialization errors.',\n    labels=('tenant',)\n)\n\nconnection_errors = registry.new_labeled_counter(\n    'connection_errors_total',\n    'Number of network connection errors.',\n    labels=('tenant',)\n)\n\nha_events_total = registry.new_labeled_counter(\n    \"ha_events_total\",\n    \"Number of each high-availability watch event.\",\n    labels=(\"dsn\", \"event\"),\n)\n\nauth_api_calls = registry.new_labeled_counter(\n    \"auth_api_calls_total\",\n    \"Number of API calls to the Auth extension.\",\n    labels=(\"tenant\",),\n)\n\nauth_ui_renders = registry.new_labeled_counter(\n    \"auth_ui_renders_total\",\n    \"Number of UI pages rendered by the Auth extension.\",\n    labels=(\"tenant\",),\n)\n\nauth_providers = registry.new_labeled_gauge(\n    'auth_providers',\n    'Number of Auth providers configured.',\n    labels=('tenant', 'branch'),\n)\n\nextension_used = registry.new_labeled_gauge(\n    'extension_used_branch_count_current',\n    'How many branches an extension is used by.',\n    labels=('tenant', 'extension'),\n)\n\nfeature_used_branches = registry.new_labeled_gauge(\n    'feature_used_branch_count_current',\n    'How many branches a schema feature is used by.',\n    labels=('tenant', 'feature'),\n)\nfeature_used = registry.new_labeled_gauge(\n    'feature_used_num_count_current',\n    'How many times a schema feature is used.',\n    labels=('tenant', 'feature'),\n)\n\nauth_successful_logins = registry.new_labeled_counter(\n    \"auth_successful_logins_total\",\n    \"Number of successful logins in the Auth extension.\",\n    labels=(\"tenant\",),\n)\n\nauth_provider_jwkset_fetch_success = registry.new_labeled_counter(\n    \"auth_provider_jwkset_fetch_success_total\",\n    \"Number of successful Auth extension JWK Set fetches.\",\n    labels=(\"provider\",),\n)\n\nauth_provider_jwkset_fetch_errors = registry.new_labeled_counter(\n    \"auth_provider_jwkset_fetch_errors_total\",\n    \"Number of failed Auth extension JWK Set fetches.\",\n    labels=(\"provider\",),\n)\n\nauth_provider_token_validation_success = registry.new_labeled_counter(\n    \"auth_provider_token_validation_success_total\",\n    \"Number of successful Auth extension provider token validations.\",\n    labels=(\"provider\",),\n)\n\nauth_provider_token_validation_errors = registry.new_labeled_counter(\n    \"auth_provider_token_validation_errors_total\",\n    \"Number of failed Auth extension provider token validations.\",\n    labels=(\"provider\",),\n)\n\notc_initiated_total = registry.new_labeled_counter(\n    \"otc_initiated_total\",\n    \"Number of one-time codes initiated.\",\n    labels=(\"tenant\",),\n)\n\notc_verified_total = registry.new_labeled_counter(\n    \"otc_verified_total\",\n    \"Number of one-time codes successfully verified.\",\n    labels=(\"tenant\",),\n)\n\notc_failed_total = registry.new_labeled_counter(\n    \"otc_failed_total\",\n    \"Number of one-time code verification failures.\",\n    labels=(\"tenant\", \"reason\"),\n)\n\nmt_tenants_total = registry.new_gauge(\n    'mt_tenants_current',\n    'Total number of currently-registered tenants.',\n)\n\nmt_config_reloads = registry.new_counter(\n    'mt_config_reloads_total',\n    'Total number of the main multi-tenant config file reloads.',\n)\n\nmt_config_reload_errors = registry.new_counter(\n    'mt_config_reload_errors_total',\n    'Total number of the main multi-tenant config file reload errors.',\n)\n\nmt_tenant_add_total = registry.new_labeled_counter(\n    'mt_tenant_add_total',\n    'Total number of new tenants the server attempted to add.',\n    labels=(\"tenant\",),\n)\n\nmt_tenant_add_errors = registry.new_labeled_counter(\n    'mt_tenant_add_errors_total',\n    'Total number of tenants the server failed to add.',\n    labels=(\"tenant\",),\n)\n\nmt_tenant_remove_total = registry.new_labeled_counter(\n    'mt_tenant_remove_total',\n    'Total number of tenants the server attempted to remove.',\n    labels=(\"tenant\",),\n)\n\nmt_tenant_remove_errors = registry.new_labeled_counter(\n    'mt_tenant_remove_errors_total',\n    'Total number of tenants the server failed to remove.',\n    labels=(\"tenant\",),\n)\n\nmt_tenant_reload_total = registry.new_labeled_counter(\n    'mt_tenant_reload_total',\n    'Total number of tenants the server attempted to reload.',\n    labels=(\"tenant\",),\n)\n\nmt_tenant_reload_errors = registry.new_labeled_counter(\n    'mt_tenant_reload_errors_total',\n    'Total number of tenants the server failed to reload.',\n    labels=(\"tenant\",),\n)\n\nif os.name == 'posix' and (sys.platform == 'linux' or sys.platform == 'darwin'):\n    open_fds = registry.new_gauge(\n        'open_fds',\n        'Number of open file descriptors.',\n    )\n\n    max_open_fds = registry.new_gauge(\n        'max_open_fds',\n        'Maximum number of open file descriptors.',\n    )\n\n# Implement a function that monitors the number of open file descriptors\n# and updates the metrics accordingly. This will be replaced with a more\n# efficient implementation in Rust at a later date.\n\n\ndef monitor_open_fds_linux():\n    import time\n    while True:\n        max_open_fds.set(os.sysconf('SC_OPEN_MAX'))\n        # To get the current number of open files, stat /proc/self/fd/\n        # and get the size. If zero, count the number of entries in the\n        # directory.\n        #\n        # This is supported in modern Linux kernels.\n        # https://github.com/torvalds/linux/commit/f1f1f2569901ec5b9d425f2e91c09a0e320768f3\n        try:\n            st = os.stat('/proc/self/fd/')\n            if st.st_size == 0:\n                open_fds.set(len(os.listdir('/proc/self/fd/')))\n            else:\n                open_fds.set(st.st_size)\n        except Exception:\n            open_fds.set(-1)\n\n        time.sleep(30)\n\n\ndef monitor_open_fds_macos():\n    import time\n    while True:\n        max_open_fds.set(os.sysconf('SC_OPEN_MAX'))\n        # Iterate the contents of /dev/fd to list all entries.\n        # We assume that MacOS isn't going to be running a large installation\n        # of EdgeDB on a single machine.\n        try:\n            open_fds.set(len(os.listdir('/dev/fd')))\n        except Exception:\n            open_fds.set(-1)\n\n        time.sleep(30)\n\n\ndef start_monitoring_open_fds():\n    import threading\n\n    # Supported only on Linux and macOS.\n    if os.name == 'posix':\n        if sys.platform == 'darwin':\n            threading.Thread(\n                target=monitor_open_fds_macos,\n                name='open_fds_monitor',\n                daemon=True\n            ).start()\n        elif sys.platform == 'linux':\n            threading.Thread(\n                target=monitor_open_fds_linux,\n                name='open_fds_monitor',\n                daemon=True\n            ).start()\n\n\nstart_monitoring_open_fds()\n"
  },
  {
    "path": "edb/server/multitenant.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Any, Iterator, Mapping, MutableMapping, Sequence, TypedDict\n\nimport asyncio\nimport collections\nimport json\nimport logging\nimport pathlib\nimport signal\nimport sys\nimport weakref\n\nimport setproctitle\n\nfrom edb import buildmeta\nfrom edb import errors\nfrom edb.common import retryloop\nfrom edb.common import signalctl\nfrom edb.common.log import current_tenant\nfrom edb.pgsql import params as pgparams\nfrom edb.server import compiler as edbcompiler\nfrom edb.server import metrics\n\nfrom . import args as srvargs\nfrom . import config\nfrom . import defines\nfrom . import pgcluster\nfrom . import server\nfrom . import tenant as edbtenant\nfrom .compiler_pool import pool as compiler_pool\n\nlogger = logging.getLogger(\"edb.server\")\n\n\nTenantConfig = TypedDict(\n    \"TenantConfig\",\n    {\n        \"instance-name\": str,\n        \"backend-dsn\": str,\n        \"max-backend-connections\": int,\n        \"tenant-id\": str,\n        \"backend-adaptive-ha\": bool,\n        \"jwt-sub-allowlist-file\": str,\n        \"jwt-revocation-list-file\": str,\n        \"readiness-state-file\": str,\n        \"admin\": bool,\n        \"config-file\": str,\n    },\n)\n\n\nclass MultiTenantServer(server.BaseServer):\n    _config_file: pathlib.Path\n    _sys_config: Mapping[str, config.SettingValue]\n    _init_con_data: list[config.ConState]\n\n    _tenants_by_sslobj: MutableMapping\n    _tenants_conf: dict[str, dict[str, str]]\n    _last_tenants_conf: dict[str, dict[str, str]]\n    _tenants_lock: MutableMapping[str, asyncio.Lock]\n    _tenants_serial: dict[str, int]\n    _tenants: dict[str, edbtenant.Tenant]\n    _admin_tenant: edbtenant.Tenant | None\n\n    _task_group: asyncio.TaskGroup | None\n    _task_serial: int\n\n    def __init__(\n        self,\n        config_file: pathlib.Path,\n        *,\n        compiler_pool_tenant_cache_size: int,\n        sys_config: Mapping[str, config.SettingValue],\n        init_con_data: list[config.ConState],\n        sys_queries: Mapping[str, bytes],\n        report_config_typedesc: dict[defines.ProtocolVersion, bytes],\n        **kwargs,\n    ):\n        super().__init__(**kwargs)\n        self._config_file = config_file\n        self._sys_config = sys_config\n        self._init_con_data = init_con_data\n        self._compiler_pool_tenant_cache_size = compiler_pool_tenant_cache_size\n\n        self._tenants_by_sslobj = weakref.WeakKeyDictionary()\n        self._tenants_conf = {}\n        self._last_tenants_conf = {}\n        self._tenants_lock = collections.defaultdict(asyncio.Lock)\n        self._tenants_serial = {}\n        self._tenants = {}\n        self._admin_tenant = None\n\n        self._task_group = asyncio.TaskGroup()\n        self._task_serial = 0\n        self._sys_queries = sys_queries\n        self._report_config_typedesc = report_config_typedesc\n\n    def _get_sys_config(self) -> Mapping[str, config.SettingValue]:\n        return self._sys_config\n\n    def _sni_callback(self, sslobj, server_name, _sslctx):\n        if server_name is None:\n            self._tenants_by_sslobj[sslobj] = edbtenant.host_tenant\n        elif tenant := self._tenants.get(server_name):\n            self._tenants_by_sslobj[sslobj] = tenant\n\n    def get_default_tenant(self) -> edbtenant.Tenant:\n        raise errors.UnknownTenantError(\n            \"No such tenant configured.\",\n            hint=\"Please try again later, or \"\n                 \"double check the SNI/server name in TLS connection\",\n        )\n\n    def retrieve_tenant(self, sslobj) -> edbtenant.Tenant | None:\n        return self._tenants_by_sslobj.pop(sslobj, None)\n\n    def iter_tenants(self) -> Iterator[edbtenant.Tenant]:\n        return iter(self._tenants.values())\n\n    async def _before_start_servers(self) -> None:\n        assert self._task_group is not None\n        await self._task_group.__aenter__()\n        fs = self.reload_tenants()\n\n        def reload_config_file():\n            logger.info(\"Reloading multi-tenant config file.\")\n            self.reload_tenants()\n\n        self.monitor_fs(self._config_file, reload_config_file)\n\n        if fs:\n            await asyncio.wait(fs)\n\n    def _get_status(self) -> dict[str, Any]:\n        status = super()._get_status()\n        tenants = {}\n        for server_name, tenant in self._tenants.items():\n            tenants[server_name] = {\n                \"tenant_id\": tenant.tenant_id,\n            }\n        status[\"tenants\"] = tenants\n        return status\n\n    def _get_backend_runtime_params(self) -> pgparams.BackendRuntimeParams:\n        return pgparams.get_default_runtime_params()\n\n    async def stop(self):\n        await super().stop()\n        if self._task_group is not None:\n            await self._task_group.__aexit__(*sys.exc_info())\n        try:\n            for tenant in self._tenants.values():\n                tenant.stop()\n            for tenant in self._tenants.values():\n                await tenant.wait_stopped()\n                metrics.mt_tenants_total.dec()\n        finally:\n            for tenant in self._tenants.values():\n                tenant.terminate_sys_pgcon()\n\n    def reload_tenants(self) -> Sequence[asyncio.Future]:\n        metrics.mt_config_reloads.inc()\n        try:\n            with self._config_file.open() as cf:\n                conf = json.load(cf)\n            self._last_tenants_conf = self._tenants_conf\n            rv = []\n            for sni, tenant_conf in conf.items():\n                if sni not in self._tenants_conf:\n                    rv.append(\n                        self._create_task(self._add_tenant, sni, tenant_conf)\n                    )\n            for sni in self._tenants_conf:\n                if sni in conf:\n                    rv.append(\n                        self._create_task(self._reload_tenant, sni, conf[sni])\n                    )\n                else:\n                    rv.append(self._create_task(self._remove_tenant, sni))\n            self._tenants_conf = conf\n            return rv\n        except Exception:\n            metrics.mt_config_reload_errors.inc()\n            raise\n\n    def _create_task(self, method, *args) -> asyncio.Task:\n        self._task_serial += 1\n        assert self._task_group is not None\n        return self._task_group.create_task(method(self._task_serial, *args))\n\n    async def _create_tenant(self, conf: TenantConfig) -> edbtenant.Tenant:\n        cluster = await pgcluster.get_remote_pg_cluster(\n            conf[\"backend-dsn\"], tenant_id=conf.get(\"tenant-id\")\n        )\n        instance_params = cluster.get_runtime_params().instance_params\n        max_conns = (\n            instance_params.max_connections\n            - instance_params.reserved_connections\n        )\n        if \"max-backend-connections\" not in conf:\n            logger.info(f\"Detected {max_conns} backend connections available.\")\n            if self._testmode:\n                max_conns = srvargs.adjust_testmode_max_connections(max_conns)\n                logger.info(\n                    f\"Using max_backend_connections={max_conns} \"\n                    f\"under test mode.\"\n                )\n        elif conf[\"max-backend-connections\"] > max_conns:\n            raise server.StartupError(\n                f\"--max-backend-connections is too large for this backend; \"\n                f\"detected maximum available NUM: {max_conns}\"\n            )\n        else:\n            max_conns = conf[\"max-backend-connections\"]\n\n        cluster.update_connection_params(server_settings={\n            \"application_name\": f'edgedb_instance_{conf[\"instance-name\"]}',\n            \"edgedb.instance_name\": conf[\"instance-name\"],\n            \"edgedb.server_version\": buildmeta.get_version_json(),\n        })\n\n        if self._jws_key is None:\n            raise server.StartupError(\n                \"No secret key\"\n            )\n\n        tenant = edbtenant.Tenant(\n            cluster,\n            instance_name=conf[\"instance-name\"],\n            max_backend_connections=max_conns,\n            backend_adaptive_ha=conf.get(\"backend-adaptive-ha\", False),\n        )\n        tenant.set_init_con_data(self._init_con_data)\n        config_file = conf.get(\"config-file\")\n        tenant.set_reloadable_files(\n            readiness_state_file=conf.get(\"readiness-state-file\"),\n            jwt_sub_allowlist_file=conf.get(\"jwt-sub-allowlist-file\"),\n            jwt_revocation_list_file=conf.get(\"jwt-revocation-list-file\"),\n            config_file=config_file,\n        )\n        tenant.set_server(self)\n        tenant.load_jwcrypto(self._jws_key)\n        if config_file:\n            await tenant.load_config_file(self.get_compiler_pool())\n        try:\n            await tenant.init_sys_pgcon()\n            await tenant.init(compat_check=True)\n            tenant.start_watching_files()\n            await tenant.start_accepting_new_tasks()\n            tenant.start_running()\n\n            if conf.get(\"admin\", False):\n                # There can be only one \"admin\" tenant, the behavior of setting\n                # multiple tenants with `\"admin\": true` is undefined.\n                self._admin_tenant = tenant\n\n            return tenant\n        except Exception:\n            await self._destroy_tenant(tenant)\n            raise\n\n    def _get_admin_tenant(self) -> edbtenant.Tenant:\n        if self._admin_tenant is None:\n            return super()._get_admin_tenant()\n        else:\n            return self._admin_tenant\n\n    async def _destroy_tenant(self, tenant: edbtenant.Tenant):\n        try:\n            if tenant.is_online():\n                tenant.set_readiness_state(\n                    srvargs.ReadinessState.Offline, \"tenant is removed\"\n                )\n            tenant.stop_accepting_connections()\n            tenant.stop()\n            try:\n                await asyncio.wait_for(\n                    tenant.wait_stopped(),\n                    defines.MULTITENANT_TENANT_DESTROY_TIMEOUT,\n                )\n            except asyncio.TimeoutError:\n                logger.warning(\n                    \"Tenant removal is taking too long; \"\n                    \"brutally shutdown the tenant now\"\n                )\n            assert isinstance(\n                self._compiler_pool, compiler_pool.MultiTenantPool\n            )\n            self._compiler_pool.drop_tenant(tenant.client_id)\n        finally:\n            tenant.terminate_sys_pgcon()\n\n    async def _add_tenant(self, serial: int, sni: str, conf: TenantConfig):\n        def _warn(e):\n            logger.warning(\n                \"Failed to add Tenant %s, retrying. Reason: %s\", sni, e\n            )\n\n        async def _add_tenant():\n            current_tenant.set(conf[\"instance-name\"])\n            metrics.mt_tenant_add_total.inc(1.0, current_tenant.get())\n            rloop = retryloop.RetryLoop(\n                backoff=retryloop.exp_backoff(),\n                timeout=300,\n                ignore=Exception,\n                retry_cb=_warn,\n            )\n            async for iteration in rloop:\n                async with iteration:\n                    async with self._tenants_lock[sni]:\n                        if serial > self._tenants_serial.get(sni, 0):\n                            if sni not in self._tenants:\n                                tenant = await self._create_tenant(conf)\n                                self._tenants[sni] = tenant\n                                metrics.mt_tenants_total.inc()\n                                logger.info(\"Added Tenant %s\", sni)\n                            self._tenants_serial[sni] = serial\n\n        try:\n            with signalctl.SignalController(\n                signal.SIGINT, signal.SIGTERM\n            ) as sc:\n                await sc.wait_for(_add_tenant())\n        except signalctl.SignalError:\n            pass\n        except Exception:\n            logger.critical(\"Failed to add Tenant %s\", sni, exc_info=True)\n            async with self._tenants_lock[sni]:\n                if serial > self._tenants_serial.get(sni, 0):\n                    self._tenants_conf.pop(sni, None)\n            metrics.mt_tenant_add_errors.inc(1.0, conf[\"instance-name\"])\n\n    async def _remove_tenant(self, serial: int, sni: str):\n        tenant = None\n        try:\n            async with self._tenants_lock[sni]:\n                if serial > self._tenants_serial.get(sni, 0):\n                    if sni in self._tenants:\n                        tenant = self._tenants.pop(sni)\n                        metrics.mt_tenant_remove_total.inc(\n                            1.0, tenant.get_instance_name()\n                        )\n                        current_tenant.set(tenant.get_instance_name())\n                        await self._destroy_tenant(tenant)\n                        metrics.mt_tenants_total.dec()\n                        logger.info(\"Removed Tenant %s\", sni)\n                    self._tenants_serial[sni] = serial\n        except Exception:\n            logger.critical(\"Failed to remove Tenant %s\", sni, exc_info=True)\n            metrics.mt_tenant_remove_errors.inc(\n                1.0, tenant.get_instance_name() if tenant else 'unknown'\n            )\n\n    async def _reload_tenant(self, serial: int, sni: str, conf: TenantConfig):\n        tenant = None\n        try:\n            async with self._tenants_lock[sni]:\n                if serial > self._tenants_serial.get(sni, 0):\n                    if tenant := self._tenants.get(sni):\n                        metrics.mt_tenant_reload_total.inc(\n                            1.0, tenant.get_instance_name()\n                        )\n                        current_tenant.set(tenant.get_instance_name())\n\n                        orig = self._last_tenants_conf.get(sni, {})\n                        diff = set(orig.keys()) - set(conf)\n                        for k, v in conf.items():\n                            if orig.get(k) != v:\n                                diff.add(k)\n                        diff -= {\n                            \"readiness-state-file\",\n                            \"jwt-sub-allowlist-file\",\n                            \"jwt-revocation-list-file\",\n                            \"config-file\",\n                        }\n                        if diff:\n                            logger.warning(\n                                \"The following config of tenant %s changed, \"\n                                \"but reloading them is not yet supported: %s\",\n                                sni,\n                                \", \".join(diff),\n                            )\n\n                        if not tenant.set_reloadable_files(\n                            readiness_state_file=conf.get(\n                                \"readiness-state-file\"),\n                            jwt_sub_allowlist_file=conf.get(\n                                \"jwt-sub-allowlist-file\"),\n                            jwt_revocation_list_file=conf.get(\n                                \"jwt-revocation-list-file\"),\n                            config_file=conf.get(\"config-file\"),\n                        ):\n                            # none of the reloadable values was modified\n                            return\n\n                        tenant.reload()\n                        logger.info(\"Reloaded Tenant %s\", sni)\n\n                    # GOTCHA: reloading tenant doesn't increase the tenant\n                    # serial because a reload shouldn't prevent a concurrent\n                    # removing of the tenant.\n        except Exception:\n            logger.critical(\"Failed to reload Tenant %s\", sni, exc_info=True)\n            metrics.mt_tenant_reload_errors.inc(\n                1.0, tenant.get_instance_name() if tenant else 'unknown'\n            )\n\n    def get_debug_info(self):\n        parent = super().get_debug_info()\n        parent[\"tenants\"] = {\n            name: tenant.get_debug_info()\n            for name, tenant in self._tenants.items()\n        }\n        return parent\n\n    def _get_compiler_args(self) -> dict[str, Any]:\n        args = super()._get_compiler_args()\n        args[\"cache_size\"] = self._compiler_pool_tenant_cache_size\n        return args\n\n\nasync def run_server(\n    args: srvargs.ServerConfig,\n    *,\n    sys_config: Mapping[str, config.SettingValue],\n    init_con_data: list[config.ConState],\n    sys_queries: Mapping[str, bytes],\n    report_config_typedesc: dict[defines.ProtocolVersion, bytes],\n    runstate_dir: pathlib.Path,\n    internal_runstate_dir: pathlib.Path,\n    do_setproctitle: bool,\n    compiler_state: edbcompiler.CompilerState,\n):\n    multitenant_config_file = args.multitenant_config_file\n    assert multitenant_config_file is not None\n\n    with signalctl.SignalController(signal.SIGINT, signal.SIGTERM) as sc:\n        ss = MultiTenantServer(\n            multitenant_config_file,\n            sys_config=sys_config,\n            init_con_data=init_con_data,\n            sys_queries=sys_queries,\n            report_config_typedesc=report_config_typedesc,\n            runstate_dir=runstate_dir,\n            internal_runstate_dir=internal_runstate_dir,\n            nethosts=args.bind_addresses,\n            netport=args.port,\n            listen_sockets=(),\n            auto_shutdown_after=args.auto_shutdown_after,\n            echo_runtime_info=args.echo_runtime_info,\n            status_sinks=args.status_sinks,\n            binary_endpoint_security=args.binary_endpoint_security,\n            http_endpoint_security=args.http_endpoint_security,\n            default_auth_method=args.default_auth_method,\n            testmode=args.testmode,\n            admin_ui=args.admin_ui,\n            cors_always_allowed_origins=args.cors_always_allowed_origins,\n            disable_dynamic_system_config=args.disable_dynamic_system_config,\n            compiler_pool_size=args.compiler_pool_size,\n            compiler_worker_branch_limit=args.compiler_worker_branch_limit,\n            compiler_pool_mode=srvargs.CompilerPoolMode.MultiTenant,\n            compiler_pool_addr=args.compiler_pool_addr,\n            compiler_pool_tenant_cache_size=(\n                args.compiler_pool_tenant_cache_size\n            ),\n            compiler_worker_max_rss=args.compiler_worker_max_rss,\n            compiler_state=compiler_state,\n            use_monitor_fs=args.reload_config_files in [\n                srvargs.ReloadTrigger.Default,\n                srvargs.ReloadTrigger.FileSystemEvent,\n            ],\n        )\n        # This coroutine runs as long as the server,\n        # and compiler_state is *heavy*, so make sure we don't\n        # keep a reference to it.\n        del compiler_state\n        await sc.wait_for(ss.init())\n\n        (\n            tls_cert_newly_generated, jws_keys_newly_generated\n        ) = await ss.maybe_generate_pki(args, ss)\n        ss.init_tls(\n            args.tls_cert_file,\n            args.tls_key_file,\n            tls_cert_newly_generated,\n            args.tls_client_ca_file,\n        )\n        ss.init_jwcrypto(args.jws_key_file, jws_keys_newly_generated)\n        ss.start_watching_files()\n\n        def load_configuration(_signum):\n            if args.reload_config_files not in [\n                srvargs.ReloadTrigger.Default,\n                srvargs.ReloadTrigger.Signal,\n            ]:\n                logger.info(\n                    \"SIGHUP received, but reload on signal is disabled\"\n                )\n                return\n\n            logger.info(\"reloading configuration\")\n            try:\n                ss.reload_tls(\n                    args.tls_cert_file,\n                    args.tls_key_file,\n                    args.tls_client_ca_file,\n                )\n                ss.load_jwcrypto(args.jws_key_file)\n                ss.reload_tenants()\n            except Exception:\n                logger.critical(\n                    \"Unexpected error occurred during reload configuration; \"\n                    \"shutting down.\",\n                    exc_info=True,\n                )\n                ss.request_shutdown()\n\n        try:\n            await sc.wait_for(ss.start())\n            if do_setproctitle:\n                setproctitle.setproctitle(\n                    f\"edgedb-server-{ss.get_listen_port()}\"\n                )\n            with signalctl.SignalController(signal.SIGHUP) as reload_ctl:\n                reload_ctl.add_handler(\n                    load_configuration, signals=(signal.SIGHUP,)\n                )\n                try:\n                    await sc.wait_for(ss.serve_forever())\n                except signalctl.SignalError as e:\n                    logger.info(\"Received signal: %s.\", e.signo)\n        finally:\n            logger.info(\"Shutting down.\")\n            await sc.wait_for(ss.stop())\n"
  },
  {
    "path": "edb/server/net_worker.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nimport dataclasses\nimport json\nimport typing\nimport asyncio\nimport logging\nimport base64\n\nfrom edb.ir import statypes\nfrom edb.server import defines\nfrom edb.server.protocol import execute\nfrom edb.server.http import HttpClient\nfrom edb.common import retryloop\nfrom . import dbview\n\nif typing.TYPE_CHECKING:\n    from edb.server import server as edbserver\n    from edb.server import tenant as edbtenant\n\nlogger = logging.getLogger(\"edb.server.net_worker\")\n\nPOLLING_INTERVAL = statypes.Duration(microseconds=10 * 1_000_000)  # 10 seconds\n# TODO: Make this configurable via server config\nNET_HTTP_REQUEST_TTL = statypes.Duration(\n    microseconds=3600 * 1_000_000\n)  # 1 hour\n\n\n@dataclasses.dataclass\nclass TenantState:\n    # For each database, we track the last (db.dml_queries_executed,\n    # db.dbver) that we saw. We track dbver so that we can detect\n    # a database being dropped and recreated.\n    database_counts: dict[str, tuple[int, int]]\n    http_client: typing.Any\n\n\nasync def _http_task(tenant: edbtenant.Tenant, state: TenantState) -> None:\n    http_max_connections = tenant._server.config_lookup(\n        'http_max_connections', tenant.get_sys_config()\n    )\n    http_client = state.http_client\n    http_client._update_limit(http_max_connections)\n    seen_counts = {}\n\n    try:\n        # TODO: I think this TaskGroup approach might not be the right\n        # approach here. It is fragile to failures and means that slow\n        # queries can cause things to wait on them.\n        async with (asyncio.TaskGroup() as g,):\n            for db in list(tenant.iter_dbs()):\n                if db.name == defines.EDGEDB_SYSTEM_DB:\n                    # Don't run the net_worker for system database\n                    continue\n                if not tenant.is_database_connectable(db.name):\n                    # Don't run the net_worker if the database is not\n                    # connectable, e.g. being dropped\n                    continue\n                cur_seen = state.database_counts.get(db.name, (-1, -1))\n\n                # We only do the polling for net requests on branches\n                # that have seen DML since our last execute.\n                #\n                # TODO: It would be even better if we only ran when\n                # there were queries that actually touched\n                # ScheduledRequest, but I'm still musing over how to\n                # thread the data around in a way that isn't just a\n                # total hack.\n                if cur_seen == (db.dml_queries_executed, db.dbver):\n                    seen_counts[db.name] = cur_seen\n                    continue\n\n                new_key = db.dml_queries_executed + 1, db.dbver\n                try:\n                    json_bytes = await execute.parse_execute_json(\n                        db,\n                        \"\"\"\n                        with\n                            PENDING_REQUESTS := (\n                                select std::net::http::ScheduledRequest\n                                filter .state = std::net::RequestState.Pending\n                            ),\n                            UPDATED := (\n                                update PENDING_REQUESTS\n                                set {\n                                    state := std::net::RequestState.InProgress,\n                                    updated_at := datetime_of_statement(),\n                                }\n                            ),\n                        select UPDATED {\n                            id,\n                            method,\n                            url,\n                            body,\n                            headers,\n                        }\n                        \"\"\",\n                        cached_globally=True,\n                        tx_isolation=defines.TxIsolationLevel.RepeatableRead,\n                        query_tag='gel/net',\n                    )\n                    seen_counts[db.name] = new_key\n                except Exception as ex:\n                    # If the query fails (because the database branch\n                    # has been racily deleted, maybe), ignore an keep\n                    # going. We don't want the failure to bubble up\n                    # and cause tasks in the task group to die.\n                    logger.debug(\n                        \"HTTP net_worker query failed \"\n                        \"(instance: %s, branch: %s)\",\n                        tenant.get_instance_name(),\n                        db,\n                        exc_info=ex,\n                    )\n                    continue\n\n                pending_requests: list[dict] = json.loads(json_bytes)\n                for pending_request in pending_requests:\n                    request = ScheduledRequest(**pending_request)\n                    g.create_task(handle_request(http_client, db, request))\n    except Exception as ex:\n        logger.debug(\n            \"HTTP send failed (instance: %s)\",\n            tenant.get_instance_name(),\n            exc_info=ex,\n        )\n\n    state.database_counts = seen_counts\n\n\ndef create_http(tenant: edbtenant.Tenant):\n    return TenantState(\n        http_client=tenant.get_http_client(originator=\"std::net\"),\n        database_counts={},\n    )\n\n\nasync def http(server: edbserver.BaseServer) -> None:\n    tenant_http = dict()\n\n    while True:\n        tenant_set = set()\n        try:\n            tasks = []\n            for tenant in server.iter_tenants():\n                if tenant.accept_new_tasks:\n                    tenant_set.add(tenant)\n                    if tenant not in tenant_http:\n                        tenant_http[tenant] = create_http(tenant)\n                    tasks.append(\n                        tenant.create_task(\n                            _http_task(tenant, tenant_http[tenant]),\n                            interruptable=True,\n                        )\n                    )\n            # Remove unused tenant_http entries\n            for tenant in list(tenant_http.keys()):\n                if tenant not in tenant_set:\n                    del tenant_http[tenant]\n            if tasks:\n                await asyncio.wait(tasks)\n        except Exception as ex:\n            logger.debug(\"HTTP worker failed\", exc_info=ex)\n        finally:\n            await asyncio.sleep(\n                POLLING_INTERVAL.to_microseconds() / 1_000_000.0\n            )\n\n\n@dataclasses.dataclass\nclass ScheduledRequest:\n    id: str\n    method: str\n    url: str\n    body: typing.Optional[bytes]\n    headers: typing.Optional[list[dict]]\n\n    def __post_init__(self):\n        if self.body is not None:\n            self.body = base64.b64decode(self.body).decode('utf-8').encode()\n\n\nasync def handle_request(\n    client: HttpClient, db: dbview.Database, request: ScheduledRequest\n) -> None:\n    response_status = None\n    response_body = None\n    response_headers = None\n    failure = None\n\n    try:\n        headers = (\n            [(header[\"name\"], header[\"value\"]) for header in request.headers]\n            if request.headers\n            else None\n        )\n        response = await client.request(\n            method=request.method,\n            path=request.url,\n            data=request.body,\n            headers=headers,\n        )\n        response_status, response_bytes, response_hdict = response\n        response_body = bytes(response_bytes)\n        response_headers = list(response_hdict.items())\n        request_state = 'Completed'\n    except Exception as ex:\n        request_state = 'Failed'\n        failure = {\n            'kind': 'NetworkError',\n            'message': str(ex),\n        }\n\n    def _warn(e):\n        logger.warning(\n            \"Failed to update std::net::http record, retrying. Reason: %s\", e\n        )\n\n    async def _update_request():\n        rloop = retryloop.RetryLoop(\n            backoff=retryloop.exp_backoff(),\n            timeout=300,\n            ignore=(Exception,),\n            retry_cb=_warn,\n        )\n        async for iteration in rloop:\n            async with iteration:\n                await execute.parse_execute_json(\n                    db,\n                    \"\"\"\n                    with\n                        nh as module std::net::http,\n                        net as module std::net,\n                        state := <net::RequestState>$state,\n                        failure := <\n                            optional tuple<\n                                kind: net::RequestFailureKind,\n                                message: str\n                            >\n                        >to_json(<str>$failure),\n                        response_status := <optional int16>$response_status,\n                        response_body := <optional bytes>$response_body,\n                        response_headers :=\n                            <optional array<tuple<str, str>>>$response_headers,\n                        response := (\n                            if state = net::RequestState.Completed\n                            then (\n                                insert nh::Response {\n                                    created_at := datetime_of_statement(),\n                                    status := assert_exists(response_status),\n                                    body := response_body,\n                                    headers := response_headers,\n                                }\n                            )\n                            else (<nh::Response>{})\n                        ),\n                    update nh::ScheduledRequest filter .id = <uuid>$id\n                    set {\n                        state := state,\n                        response := response,\n                        failure := failure,\n                        updated_at := datetime_of_statement(),\n                    };\n                    \"\"\",\n                    variables={\n                        'id': request.id,\n                        'state': request_state,\n                        'response_status': response_status,\n                        'response_body': response_body,\n                        'response_headers': response_headers,\n                        'failure': json.dumps(failure),\n                    },\n                    cached_globally=True,\n                    tx_isolation=defines.TxIsolationLevel.RepeatableRead,\n                    query_tag='gel/net',\n                )\n\n    await _update_request()\n\n\nasync def _delete_requests(\n    db: dbview.Database, expires_in: statypes.Duration\n) -> None:\n    def _warn(e):\n        logger.warning(\n            \"Failed to delete std::net::http::ScheduledRequest, retrying.\"\n            \" Reason: %s\",\n            e,\n        )\n\n    rloop = retryloop.RetryLoop(\n        backoff=retryloop.exp_backoff(),\n        timeout=300,\n        ignore=(Exception,),\n        retry_cb=_warn,\n    )\n    async for iteration in rloop:\n        async with iteration:\n            if not db.tenant.is_database_connectable(db.name):\n                # Don't run the net_worker if the database is not\n                # connectable, e.g. being dropped\n                continue\n            result_json = await execute.parse_execute_json(\n                db,\n                \"\"\"\n                with requests := (\n                    select std::net::http::ScheduledRequest filter\n                    .state != std::net::RequestState.Pending\n                    and (datetime_of_statement() - .updated_at) >\n                    <duration>$expires_in\n                )\n                select count((delete requests));\n                \"\"\",\n                variables={\"expires_in\": expires_in.to_backend_str()},\n                cached_globally=True,\n                tx_isolation=defines.TxIsolationLevel.RepeatableRead,\n                query_tag='gel/net',\n            )\n            result: list[int] = json.loads(result_json)\n            if result[0] > 0:\n                logger.debug(f\"Deleted {result[0]} requests\")\n            else:\n                logger.debug(f\"No requests to delete\")\n\n\nasync def _gc(tenant: edbtenant.Tenant, expires_in: statypes.Duration) -> None:\n    try:\n        async with asyncio.TaskGroup() as g:\n            for db in tenant.iter_dbs():\n                if db.name == defines.EDGEDB_SYSTEM_DB:\n                    continue\n                g.create_task(_delete_requests(db, expires_in))\n    except Exception as ex:\n        logger.debug(\n            \"GC of std::net::http::ScheduledRequest failed (instance: %s)\",\n            tenant.get_instance_name(),\n            exc_info=ex,\n        )\n\n\nasync def gc(server: edbserver.BaseServer) -> None:\n    while True:\n        tasks = [\n            tenant.create_task(\n                _gc(tenant, NET_HTTP_REQUEST_TTL), interruptable=True\n            )\n            for tenant in server.iter_tenants()\n            if tenant.accept_new_tasks\n        ]\n        try:\n            if tasks:\n                await asyncio.wait(tasks)\n        except Exception as ex:\n            logger.debug(\n                \"GC of std::net::http::ScheduledRequest failed\", exc_info=ex\n            )\n        finally:\n            await asyncio.sleep(\n                NET_HTTP_REQUEST_TTL.to_microseconds() / 1_000_000.0\n            )\n"
  },
  {
    "path": "edb/server/pgcluster.py",
    "content": "# Copyright (C) 2016-present MagicStack Inc. and the EdgeDB authors.\n# Copyright (C) 2016-present the asyncpg authors and contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"PostgreSQL cluster management.\"\"\"\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n    Iterable,\n    Mapping,\n    Sequence,\n    Coroutine,\n    Unpack,\n    cast,\n    TYPE_CHECKING,\n)\n\nimport asyncio\nimport copy\nimport hashlib\nimport json\nimport logging\nimport os\nimport os.path\nimport pathlib\nimport re\nimport shlex\nimport shutil\nimport signal\nimport struct\nimport textwrap\nimport urllib.parse\n\nfrom edb import buildmeta\nfrom edb import errors\nfrom edb.common import lru\nfrom edb.common import supervisor\nfrom edb.common import uuidgen\n\nfrom edb.server import args as srvargs\nfrom edb.server import defines\nfrom edb.server import pgconnparams\nfrom edb.server.ha import base as ha_base\nfrom edb.pgsql import common as pgcommon\nfrom edb.pgsql import params as pgparams\n\nif TYPE_CHECKING:\n    from edb.server import pgcon\n\nlogger = logging.getLogger('edb.pgcluster')\npg_dump_logger = logging.getLogger('pg_dump')\npg_restore_logger = logging.getLogger('pg_restore')\npg_ctl_logger = logging.getLogger('pg_ctl')\npg_config_logger = logging.getLogger('pg_config')\ninitdb_logger = logging.getLogger('initdb')\npostgres_logger = logging.getLogger('postgres')\n\nget_database_backend_name = pgcommon.get_database_backend_name\nget_role_backend_name = pgcommon.get_role_backend_name\n\nEDGEDB_SERVER_SETTINGS = {\n    'client_encoding': 'utf-8',\n    # DO NOT raise client_min_messages above NOTICE level\n    # because server indirect block return machinery relies\n    # on NoticeResponse as the data channel.\n    'client_min_messages': 'NOTICE',\n    'search_path': 'edgedb',\n    'timezone': 'UTC',\n    'intervalstyle': 'iso_8601',\n    'jit': 'off',\n    'default_transaction_isolation': 'serializable',\n}\n\n\nclass ClusterError(Exception):\n    pass\n\n\nclass PostgresPidFileNotReadyError(Exception):\n    \"\"\"Raised on an attempt to read non-existent or bad Postgres PID file\"\"\"\n\n\nclass BaseCluster:\n\n    def __init__(\n        self,\n        *,\n        instance_params: Optional[pgparams.BackendInstanceParams] = None,\n    ) -> None:\n        self._connection_addr: Optional[tuple[str, int]] = None\n        self._connection_params: pgconnparams.ConnectionParams = \\\n            pgconnparams.ConnectionParams(server_settings=EDGEDB_SERVER_SETTINGS)\n        self._pg_config_data: dict[str, str] = {}\n        self._pg_bin_dir: Optional[pathlib.Path] = None\n        if instance_params is None:\n            self._instance_params = (\n                pgparams.get_default_runtime_params().instance_params)\n        else:\n            self._instance_params = instance_params\n\n    def get_db_name(self, db_name: str) -> str:\n        if (\n            not self._instance_params.capabilities\n            & pgparams.BackendCapabilities.CREATE_DATABASE\n        ):\n            assert (\n                db_name == defines.EDGEDB_SUPERUSER_DB\n            ), f\"db_name={db_name} is not allowed\"\n            rv = self.get_connection_params().database\n            assert rv is not None\n            return rv\n        return get_database_backend_name(\n            db_name,\n            tenant_id=self._instance_params.tenant_id,\n        )\n\n    def get_role_name(self, role_name: str) -> str:\n        if (\n            not self._instance_params.capabilities\n            & pgparams.BackendCapabilities.CREATE_ROLE\n        ):\n            assert (\n                role_name == defines.EDGEDB_SUPERUSER\n            ), f\"role_name={role_name} is not allowed\"\n            rv = self.get_connection_params().user\n            assert rv is not None\n            return rv\n\n        return get_database_backend_name(\n            role_name,\n            tenant_id=self._instance_params.tenant_id,\n        )\n\n    async def start(\n        self,\n        wait: int = 60,\n        *,\n        server_settings: Optional[Mapping[str, str]] = None,\n        **opts: Any,\n    ) -> None:\n        raise NotImplementedError\n\n    async def stop(self, wait: int = 60) -> None:\n        raise NotImplementedError\n\n    def destroy(self) -> None:\n        raise NotImplementedError\n\n    async def connect(self,\n                      *,\n                      source_description: str,\n                      apply_init_script: bool = False,\n                      **kwargs: Unpack[pgconnparams.CreateParamsKwargs]\n    ) -> pgcon.PGConnection:\n        \"\"\"Connect to this cluster, with optional overriding parameters. If\n        overriding parameters are specified, they are applied to a copy of the\n        connection parameters before the connection takes place.\"\"\"\n        from edb.server import pgcon\n\n        connection = copy.copy(self.get_connection_params())\n        addr = self._get_connection_addr()\n        assert addr is not None\n        connection.update(hosts=[addr])\n        connection.update(**kwargs)\n        conn = await pgcon.pg_connect(\n            connection,\n            source_description=source_description,\n            backend_params=self.get_runtime_params(),\n            apply_init_script=apply_init_script,\n        )\n        return conn\n\n    async def start_watching(\n        self, failover_cb: Optional[Callable[[], None]] = None\n    ) -> None:\n        pass\n\n    def stop_watching(self) -> None:\n        pass\n\n    def get_runtime_params(self) -> pgparams.BackendRuntimeParams:\n        params = self.get_connection_params()\n        login_role: Optional[str] = params.user\n        sup_role = self.get_role_name(defines.EDGEDB_SUPERUSER)\n        return pgparams.BackendRuntimeParams(\n            instance_params=self._instance_params,\n            session_authorization_role=(\n                None if login_role == sup_role else login_role\n            ),\n        )\n\n    def overwrite_capabilities(\n        self, caps: pgparams.BackendCapabilities\n    ) -> None:\n        self._instance_params = self._instance_params._replace(\n            capabilities=caps\n        )\n\n    def update_connection_params(\n        self,\n        **kwargs: Unpack[pgconnparams.CreateParamsKwargs],\n    ) -> None:\n        self._connection_params.update(**kwargs)\n\n    def get_pgaddr(self) -> pgconnparams.ConnectionParams:\n        assert self._connection_params is not None\n        addr = self._get_connection_addr()\n        assert addr is not None\n        params = copy.copy(self._connection_params)\n        params.update(hosts=[addr])\n        return params\n\n    def get_connection_params(\n        self,\n    ) -> pgconnparams.ConnectionParams:\n        assert self._connection_params is not None\n        return self._connection_params\n\n    def _get_connection_addr(self) -> Optional[tuple[str, int]]:\n        return self._connection_addr\n\n    def is_managed(self) -> bool:\n        raise NotImplementedError\n\n    async def get_status(self) -> str:\n        raise NotImplementedError\n\n    def _dump_restore_conn_args(\n        self,\n        dbname: str,\n    ) -> tuple[list[str], dict[str, str]]:\n        params = copy.copy(self.get_connection_params())\n        addr = self._get_connection_addr()\n        assert addr is not None\n        params.update(database=dbname, hosts=[addr])\n\n        args = [\n            f'--dbname={params.database}',\n            f'--host={params.host}',\n            f'--port={params.port}',\n            f'--username={params.user}',\n        ]\n\n        env = os.environ.copy()\n        if params.password:\n            env['PGPASSWORD'] = params.password\n\n        return args, env\n\n    async def dump_database(\n        self,\n        dbname: str,\n        *,\n        exclude_schemas: Iterable[str] = (),\n        exclude_tables: Iterable[str] = (),\n        include_schemas: Iterable[str] = (),\n        include_tables: Iterable[str] = (),\n        include_extensions: Iterable[str] = (),\n        schema_only: bool = False,\n        dump_object_owners: bool = True,\n        create_database: bool = False,\n    ) -> bytes:\n        status = await self.get_status()\n        if status != 'running':\n            raise ClusterError('cannot dump: cluster is not running')\n\n        if self._pg_bin_dir is None:\n            await self.lookup_postgres()\n        pg_dump = self._find_pg_binary('pg_dump')\n\n        conn_args, env = self._dump_restore_conn_args(dbname)\n\n        args = [\n            pg_dump,\n            '--inserts',\n            *conn_args,\n        ]\n\n        if not dump_object_owners:\n            args.append('--no-owner')\n        if schema_only:\n            args.append('--schema-only')\n        if create_database:\n            args.append('--create')\n\n        configs = [\n            ('exclude-schema', exclude_schemas),\n            ('exclude-table', exclude_tables),\n            ('schema', include_schemas),\n            ('table', include_tables),\n            ('extension', include_extensions),\n        ]\n        for flag, vals in configs:\n            for val in vals:\n                args.append(f'--{flag}={val}')\n\n        stdout_lines, _, _ = await _run_logged_subprocess(\n            args,\n            logger=pg_dump_logger,\n            log_stdout=False,\n            env=env,\n        )\n        return b'\\n'.join(stdout_lines)\n\n    async def _copy_database(\n        self,\n        src_dbname: str,\n        tgt_dbname: str,\n        src_args: list[str],\n        tgt_args: list[str],\n    ) -> None:\n        status = await self.get_status()\n        if status != 'running':\n            raise ClusterError('cannot dump: cluster is not running')\n\n        if self._pg_bin_dir is None:\n            await self.lookup_postgres()\n        pg_dump = self._find_pg_binary('pg_dump')\n        # We actually just use psql to restore, because it is more\n        # tolerant of version differences.\n        # TODO: Maybe use pg_restore when we know we match the backend version?\n        pg_restore = self._find_pg_binary('psql')\n\n        src_conn_args, src_env = self._dump_restore_conn_args(src_dbname)\n        tgt_conn_args, _tgt_env = self._dump_restore_conn_args(tgt_dbname)\n\n        dump_args = [\n            pg_dump, '--verbose', *src_conn_args, *src_args\n        ]\n        restore_args = [\n            pg_restore, *tgt_conn_args, *tgt_args\n        ]\n\n        rpipe, wpipe = os.pipe()\n        wpipef = os.fdopen(wpipe, \"wb\")\n\n        try:\n            # N.B: uvloop will waitpid() on the child process even if we don't\n            # actually await on it due to a later error.\n            dump_p, dump_out_r, dump_err_r = await _start_logged_subprocess(\n                dump_args,\n                logger=pg_dump_logger,\n                override_stdout=wpipef,\n                log_stdout=False,\n                capture_stdout=False,\n                capture_stderr=False,\n                env=src_env,\n            )\n\n            res_p, res_out_r, res_err_r = await _start_logged_subprocess(\n                restore_args,\n                logger=pg_restore_logger,\n                stdin=rpipe,\n                capture_stdout=False,\n                capture_stderr=False,\n                log_stdout=True,\n                log_stderr=True,\n                env=src_env,\n            )\n        finally:\n            wpipef.close()\n            os.close(rpipe)\n\n        dump_exit_code, _, _, restore_exit_code, _, _ = await asyncio.gather(\n            dump_p.wait(), dump_out_r, dump_err_r,\n            res_p.wait(), res_out_r, res_err_r,\n        )\n\n        if dump_exit_code != 0 and dump_exit_code != -signal.SIGPIPE:\n            raise errors.ExecutionError(\n                f'branch failed: {dump_args[0]} exited with status '\n                f'{dump_exit_code}'\n            )\n        if restore_exit_code != 0:\n            raise errors.ExecutionError(\n                f'branch failed: '\n                f'{restore_args[0]} exited with status {restore_exit_code}'\n            )\n\n    def _find_pg_binary(self, binary: str) -> str:\n        assert self._pg_bin_dir is not None\n        bpath = self._pg_bin_dir / binary\n        if not bpath.is_file():\n            raise ClusterError(\n                'could not find {} executable: '.format(binary) +\n                '{!r} does not exist or is not a file'.format(bpath))\n\n        return str(bpath)\n\n    def _subprocess_error(\n        self,\n        name: str,\n        exitcode: int,\n        stderr: Optional[bytes],\n    ) -> ClusterError:\n        if stderr:\n            return ClusterError(\n                f'{name} exited with status {exitcode}:\\n'\n                + textwrap.indent(stderr.decode(), ' ' * 4),\n            )\n        else:\n            return ClusterError(\n                f'{name} exited with status {exitcode}',\n            )\n\n    async def lookup_postgres(self) -> None:\n        self._pg_bin_dir = await get_pg_bin_dir()\n\n    def get_client_id(self) -> int:\n        return 0\n\n\nclass Cluster(BaseCluster):\n    def __init__(\n        self,\n        data_dir: pathlib.Path,\n        *,\n        runstate_dir: Optional[pathlib.Path] = None,\n        instance_params: Optional[pgparams.BackendInstanceParams] = None,\n        log_level: str = 'i',\n    ):\n        super().__init__(instance_params=instance_params)\n        self._data_dir = data_dir\n        self._runstate_dir = (\n            runstate_dir if runstate_dir is not None else data_dir)\n        self._daemon_pid: Optional[int] = None\n        self._daemon_process: Optional[asyncio.subprocess.Process] = None\n        self._daemon_supervisor: Optional[supervisor.Supervisor] = None\n        self._log_level = log_level\n\n    def is_managed(self) -> bool:\n        return True\n\n    def get_data_dir(self) -> pathlib.Path:\n        return self._data_dir\n\n    def get_main_pid(self) -> Optional[int]:\n        return self._daemon_pid\n\n    async def get_status(self) -> str:\n        stdout_lines, stderr_lines, exit_code = (\n            await _run_logged_text_subprocess(\n                [self._pg_ctl, 'status', '-D', str(self._data_dir)],\n                logger=pg_ctl_logger,\n                check=False,\n            )\n        )\n\n        if (\n            exit_code == 4\n            or not os.path.exists(self._data_dir)\n            or not os.listdir(self._data_dir)\n        ):\n            return 'not-initialized'\n        elif exit_code == 3:\n            return 'stopped'\n        elif exit_code == 0:\n            output = '\\n'.join(stdout_lines)\n            r = re.match(r'.*PID\\s?:\\s+(\\d+).*', output)\n            if not r:\n                raise ClusterError(\n                    f'could not parse pg_ctl status output: {output}')\n            self._daemon_pid = int(r.group(1))\n            if self._connection_addr is None:\n                self._connection_addr = self._connection_addr_from_pidfile()\n            return 'running'\n        else:\n            stderr_text = '\\n'.join(stderr_lines)\n            raise ClusterError(\n                f'`pg_ctl status` exited with status {exit_code}:\\n'\n                + textwrap.indent(stderr_text, ' ' * 4),\n            )\n\n    async def ensure_initialized(self, **settings: Any) -> bool:\n        cluster_status = await self.get_status()\n\n        if cluster_status == 'not-initialized':\n            logger.info(\n                'Initializing database cluster in %s', self._data_dir)\n\n            have_c_utf8 = self.get_runtime_params().has_c_utf8_locale\n            await self.init(\n                username='postgres',\n                locale='C.UTF-8' if have_c_utf8 else 'en_US.UTF-8',\n                lc_collate='C',\n                encoding='UTF8',\n            )\n            self.reset_hba()\n            self.add_hba_entry(\n                type='local',\n                database='all',\n                user='postgres',\n                auth_method='trust'\n            )\n            self.add_hba_entry(\n                type='local',\n                database='replication',\n                user='postgres',\n                auth_method='trust'\n            )\n            return True\n        else:\n            return False\n\n    async def init(self, **settings: str) -> None:\n        \"\"\"Initialize cluster.\"\"\"\n        if await self.get_status() != 'not-initialized':\n            raise ClusterError(\n                'cluster in {!r} has already been initialized'.format(\n                    self._data_dir))\n\n        if settings:\n            settings_args = ['--{}={}'.format(k.replace('_', '-'), v)\n                             for k, v in settings.items()]\n            extra_args = ['-o'] + [' '.join(settings_args)]\n        else:\n            extra_args = []\n\n        await _run_logged_subprocess(\n            [self._pg_ctl, 'init', '-D', str(self._data_dir)] + extra_args,\n            logger=initdb_logger,\n        )\n\n    async def start(\n        self,\n        wait: int = 60,\n        *,\n        server_settings: Optional[Mapping[str, str]] = None,\n        **opts: str,\n    ) -> None:\n        \"\"\"Start the cluster.\"\"\"\n        status = await self.get_status()\n        if status == 'running':\n            return\n        elif status == 'not-initialized':\n            raise ClusterError(\n                'cluster in {!r} has not been initialized'.format(\n                    self._data_dir))\n\n        extra_args = ['--{}={}'.format(k, v) for k, v in opts.items()]\n\n        start_settings = {\n            'listen_addresses': '',  # we use Unix sockets\n            'unix_socket_permissions': '0700',\n            'unix_socket_directories': str(self._runstate_dir),\n            # here we are not setting superuser_reserved_connections because\n            # we're using superuser only now (so all connections available),\n            # and we don't support reserving connections for now\n            'max_connections': str(self._instance_params.max_connections),\n            # From Postgres docs:\n            #\n            #   You might need to raise this value if you have queries that\n            #   touch many different tables in a single transaction, e.g.,\n            #   query of a parent table with many children.\n            #\n            # EdgeDB queries might touch _lots_ of tables, especially in deep\n            # inheritance hierarchies.  This is especially important in low\n            # `max_connections` scenarios.\n            'max_locks_per_transaction': 1024,\n            'max_pred_locks_per_transaction': 1024,\n            \"shared_preload_libraries\": \",\".join(\n                [\n                    \"edb_stat_statements\",\n                ]\n            ),\n            \"edb_stat_statements.track_planning\": \"true\",\n            # Required for pg_basebackup --incremental to work\n            \"summarize_wal\": \"on\",\n        }\n\n        if os.getenv('EDGEDB_DEBUG_PGSERVER'):\n            start_settings['log_min_messages'] = 'info'\n            start_settings['log_statement'] = 'all'\n        else:\n            log_level_map = {\n                'd': 'INFO',\n                'i': 'WARNING',  # NOTICE in Postgres is quite noisy\n                'w': 'WARNING',\n                'e': 'ERROR',\n                's': 'PANIC',\n            }\n            start_settings['log_min_messages'] = log_level_map[self._log_level]\n            start_settings['log_statement'] = 'none'\n            start_settings['log_line_prefix'] = ''\n\n        if server_settings:\n            start_settings.update(server_settings)\n\n        ssl_key = start_settings.get('ssl_key_file')\n        if ssl_key:\n            # Make sure server certificate key file has correct permissions.\n            keyfile = os.path.join(self._data_dir, 'srvkey.pem')\n            assert isinstance(ssl_key, str)\n            shutil.copy(ssl_key, keyfile)\n            os.chmod(keyfile, 0o600)\n            start_settings['ssl_key_file'] = keyfile\n\n        for k, v in start_settings.items():\n            extra_args.extend(['-c', '{}={}'.format(k, v)])\n\n        self._daemon_process, *loggers = await _start_logged_subprocess(\n            [self._postgres, '-D', str(self._data_dir), *extra_args],\n            capture_stdout=False,\n            capture_stderr=False,\n            logger=postgres_logger,\n            log_processor=postgres_log_processor,\n        )\n        self._daemon_pid = self._daemon_process.pid\n\n        sup = await supervisor.Supervisor.create(name=\"postgres loggers\")\n        for logger_coro in loggers:\n            sup.create_task(logger_coro)\n        self._daemon_supervisor = sup\n\n        await self._test_connection(timeout=wait)\n\n    async def reload(self) -> None:\n        \"\"\"Reload server configuration.\"\"\"\n        status = await self.get_status()\n        if status != 'running':\n            raise ClusterError('cannot reload: cluster is not running')\n\n        await _run_logged_subprocess(\n            [self._pg_ctl, 'reload', '-D', str(self._data_dir)],\n            logger=pg_ctl_logger,\n        )\n\n    async def stop(self, wait: int = 60) -> None:\n        await _run_logged_subprocess(\n            [\n                self._pg_ctl,\n                'stop', '-D', str(self._data_dir),\n                '-t', str(wait), '-m', 'fast'\n            ],\n            logger=pg_ctl_logger,\n        )\n\n        if (\n            self._daemon_process is not None and\n            self._daemon_process.returncode is None\n        ):\n            self._daemon_process.terminate()\n            await asyncio.wait_for(self._daemon_process.wait(), timeout=wait)\n\n        if self._daemon_supervisor is not None:\n            await self._daemon_supervisor.cancel()\n            self._daemon_supervisor = None\n\n    def destroy(self) -> None:\n        shutil.rmtree(self._data_dir)\n\n    def reset_hba(self) -> None:\n        \"\"\"Remove all records from pg_hba.conf.\"\"\"\n        pg_hba = os.path.join(self._data_dir, 'pg_hba.conf')\n\n        try:\n            with open(pg_hba, 'w'):\n                pass\n        except IOError as e:\n            raise ClusterError(\n                'cannot modify HBA records: {}'.format(e)) from e\n\n    def add_hba_entry(\n        self,\n        *,\n        type: str = 'host',\n        database: str,\n        user: str,\n        address: Optional[str] = None,\n        auth_method: str,\n        auth_options: Optional[Mapping[str, Any]] = None,\n    ) -> None:\n        \"\"\"Add a record to pg_hba.conf.\"\"\"\n        if type not in {'local', 'host', 'hostssl', 'hostnossl'}:\n            raise ValueError('invalid HBA record type: {!r}'.format(type))\n\n        pg_hba = os.path.join(self._data_dir, 'pg_hba.conf')\n\n        record = '{} {} {}'.format(type, database, user)\n\n        if type != 'local':\n            if address is None:\n                raise ValueError(\n                    '{!r} entry requires a valid address'.format(type))\n            else:\n                record += ' {}'.format(address)\n\n        record += ' {}'.format(auth_method)\n\n        if auth_options is not None:\n            record += ' ' + ' '.join(\n                '{}={}'.format(k, v) for k, v in auth_options.items())\n\n        try:\n            with open(pg_hba, 'a') as f:\n                print(record, file=f)\n        except IOError as e:\n            raise ClusterError(\n                'cannot modify HBA records: {}'.format(e)) from e\n\n    async def trust_local_connections(self) -> None:\n        self.reset_hba()\n\n        self.add_hba_entry(type='local', database='all',\n                           user='all', auth_method='trust')\n        self.add_hba_entry(type='host', address='127.0.0.1/32',\n                           database='all', user='all',\n                           auth_method='trust')\n        self.add_hba_entry(type='host', address='::1/128',\n                           database='all', user='all',\n                           auth_method='trust')\n        status = await self.get_status()\n        if status == 'running':\n            await self.reload()\n\n    async def lookup_postgres(self) -> None:\n        await super().lookup_postgres()\n        self._pg_ctl = self._find_pg_binary('pg_ctl')\n        self._postgres = self._find_pg_binary('postgres')\n\n    def _get_connection_addr(self) -> tuple[str, int]:\n        if self._connection_addr is None:\n            self._connection_addr = self._connection_addr_from_pidfile()\n\n        return self._connection_addr\n\n    def _connection_addr_from_pidfile(self) -> tuple[str, int]:\n        pidfile = os.path.join(self._data_dir, 'postmaster.pid')\n\n        try:\n            with open(pidfile, 'rt') as f:\n                piddata = f.read()\n        except FileNotFoundError:\n            raise PostgresPidFileNotReadyError\n\n        lines = piddata.splitlines()\n\n        if len(lines) < 6:\n            # A complete postgres pidfile is at least 6 lines\n            raise PostgresPidFileNotReadyError\n\n        pmpid = int(lines[0])\n        if self._daemon_pid and pmpid != self._daemon_pid:\n            # This might be an old pidfile left from previous postgres\n            # daemon run.\n            raise PostgresPidFileNotReadyError\n\n        portnum = int(lines[3])\n        sockdir = lines[4]\n        hostaddr = lines[5]\n\n        if sockdir:\n            if sockdir[0] != '/':\n                # Relative sockdir\n                sockdir = os.path.normpath(\n                    os.path.join(self._data_dir, sockdir))\n            host_str = sockdir\n        elif hostaddr:\n            host_str = hostaddr\n        else:\n            raise PostgresPidFileNotReadyError\n\n        if host_str == '*':\n            host_str = 'localhost'\n        elif host_str == '0.0.0.0':\n            host_str = '127.0.0.1'\n        elif host_str == '::':\n            host_str = '::1'\n\n        return (host_str, portnum)\n\n    async def _test_connection(self, timeout: int = 60) -> str:\n        from edb.server import pgcon\n\n        self._connection_addr = None\n        connected = False\n\n        params = pgconnparams.ConnectionParams(\n            user=\"postgres\",\n            database=\"postgres\")\n\n        for n in range(timeout + 9):\n            # pg usually comes up pretty quickly, but not so quickly\n            # that we don't hit the wait case. Make our first several\n            # waits pretty short, to shave almost a second off the\n            # happy case.\n            sleep_time = 1.0 if n >= 10 else 0.1\n\n            try:\n                conn_addr = self._get_connection_addr()\n            except PostgresPidFileNotReadyError:\n                try:\n                    assert self._daemon_process is not None\n                    code = await asyncio.wait_for(\n                        self._daemon_process.wait(),\n                        sleep_time\n                    )\n                except asyncio.TimeoutError:\n                    # means that the postgres process is still alive\n                    pass\n                else:\n                    # the postgres process has exited prematurely\n                    raise ClusterError(f\"The backend exited with {code}\")\n\n                continue\n\n            try:\n                params.update(hosts=[conn_addr])\n                con = await asyncio.wait_for(\n                    pgcon.pg_connect(\n                        params,\n                        source_description=f\"{self.__class__}._test_connection\",\n                        backend_params=self.get_runtime_params(),\n                        apply_init_script=False,\n                    ),\n                    timeout=5,\n                )\n            except (\n                OSError,\n                asyncio.TimeoutError,\n                pgcon.BackendConnectionError,\n            ) as e:\n                if n % 10 == 0 and 0 < n < timeout + 9 - 1:\n                    logger.error(\"cannot connect to the backend cluster:\"\n                                 \" %s, retrying...\", e)\n                await asyncio.sleep(sleep_time)\n                continue\n            except pgcon.BackendError:\n                # Any other error other than ServerNotReadyError or\n                # ConnectionError is interpreted to indicate the server is\n                # up.\n                break\n            else:\n                connected = True\n                con.terminate()\n                break\n\n        if connected:\n            return 'running'\n        else:\n            return 'not-initialized'\n\n\nclass RemoteCluster(BaseCluster):\n    def __init__(\n        self,\n        *,\n        connection_addr: tuple[str, int],\n        connection_params: pgconnparams.ConnectionParams,\n        instance_params: Optional[pgparams.BackendInstanceParams] = None,\n        ha_backend: Optional[ha_base.HABackend] = None,\n    ):\n        super().__init__(instance_params=instance_params)\n        self._connection_params = connection_params\n        self._connection_params.update(\n            server_settings=EDGEDB_SERVER_SETTINGS\n        )\n        self._connection_addr = connection_addr\n        self._ha_backend = ha_backend\n\n    def _get_connection_addr(self) -> Optional[tuple[str, int]]:\n        if self._ha_backend is not None:\n            return self._ha_backend.get_master_addr()\n        return self._connection_addr\n\n    async def ensure_initialized(self, **settings: Any) -> bool:\n        return False\n\n    def is_managed(self) -> bool:\n        return False\n\n    async def get_status(self) -> str:\n        return 'running'\n\n    def init(self, **settings: str) -> Optional[str]:\n        pass\n\n    async def start(\n        self,\n        wait: int = 60,\n        *,\n        server_settings: Optional[Mapping[str, str]] = None,\n        **opts: Any,\n    ) -> None:\n        pass\n\n    async def stop(self, wait: int = 60) -> None:\n        pass\n\n    def destroy(self) -> None:\n        pass\n\n    def reset_hba(self) -> None:\n        raise ClusterError('cannot modify HBA records of unmanaged cluster')\n\n    def add_hba_entry(\n        self,\n        *,\n        type: str = 'host',\n        database: str,\n        user: str,\n        address: Optional[str] = None,\n        auth_method: str,\n        auth_options: Optional[Mapping[str, Any]] = None,\n    ) -> None:\n        raise ClusterError('cannot modify HBA records of unmanaged cluster')\n\n    async def start_watching(\n        self, failover_cb: Optional[Callable[[], None]] = None\n    ) -> None:\n        if self._ha_backend is not None:\n            self._ha_backend.set_failover_callback(failover_cb)\n            await self._ha_backend.start_watching()\n\n    def stop_watching(self) -> None:\n        if self._ha_backend is not None:\n            self._ha_backend.stop_watching()\n\n    @lru.method_cache\n    def get_client_id(self) -> int:\n        tenant_id = self._instance_params.tenant_id\n        if self._ha_backend is not None:\n            backend_dsn = self._ha_backend.dsn\n        else:\n            assert self._connection_addr is not None\n            assert self._connection_params is not None\n            host, port = self._connection_addr\n            database = self._connection_params.database\n            backend_dsn = f\"postgres://{host}:{port}/{database}\"\n        data = f\"{backend_dsn}|{tenant_id}\".encode(\"utf-8\")\n        digest = hashlib.blake2b(data, digest_size=8).digest()\n        rv: int = struct.unpack(\"q\", digest)[0]\n        return rv\n\n\nasync def get_pg_bin_dir() -> pathlib.Path:\n    pg_config_data = await get_pg_config()\n    pg_bin_dir = pg_config_data.get('bindir')\n    if not pg_bin_dir:\n        raise ClusterError(\n            'pg_config output did not provide the BINDIR value')\n    return pathlib.Path(pg_bin_dir)\n\n\nasync def get_pg_config() -> dict[str, str]:\n    stdout_lines, _, _ = await _run_logged_text_subprocess(\n        [str(buildmeta.get_pg_config_path())],\n        logger=pg_config_logger,\n    )\n\n    config = {}\n    for line in stdout_lines:\n        k, eq, v = line.partition('=')\n        if eq:\n            config[k.strip().lower()] = v.strip()\n\n    return config\n\n\nasync def get_local_pg_cluster(\n    data_dir: pathlib.Path,\n    *,\n    runstate_dir: Optional[pathlib.Path] = None,\n    max_connections: Optional[int] = None,\n    tenant_id: Optional[str] = None,\n    log_level: Optional[str] = None,\n) -> Cluster:\n    if log_level is None:\n        log_level = 'i'\n    if tenant_id is None:\n        tenant_id = buildmeta.get_default_tenant_id()\n    instance_params = None\n    if max_connections is not None:\n        instance_params = pgparams.get_default_runtime_params(\n            max_connections=max_connections,\n            tenant_id=tenant_id,\n        ).instance_params\n    cluster = Cluster(\n        data_dir=data_dir,\n        runstate_dir=runstate_dir,\n        instance_params=instance_params,\n        log_level=log_level,\n    )\n    await cluster.lookup_postgres()\n    return cluster\n\n\nasync def get_remote_pg_cluster(\n    dsn: str,\n    *,\n    tenant_id: Optional[str] = None,\n    specified_capabilities: Optional[srvargs.BackendCapabilitySets] = None,\n) -> RemoteCluster:\n    from edb.server import pgcon\n    parsed = urllib.parse.urlparse(dsn)\n    ha_backend = None\n\n    if parsed.scheme not in {'postgresql', 'postgres'}:\n        ha_backend = ha_base.get_backend(parsed)\n        if ha_backend is None:\n            raise ValueError(\n                'invalid DSN: scheme is expected to be \"postgresql\", '\n                '\"postgres\" or one of the supported HA backend, '\n                'got {!r}'.format(parsed.scheme))\n\n        addr = await ha_backend.get_cluster_consensus()\n        dsn = 'postgresql://{}:{}'.format(*addr)\n\n        if parsed.query:\n            # Allow passing through Postgres connection parameters from the HA\n            # backend DSN as \"pg\" prefixed query strings. For example, an HA\n            # backend DSN with `?pgpassword=123` will result an actual backend\n            # DSN with `?password=123`. They have higher priority than the `PG`\n            # prefixed environment variables like `PGPASSWORD`.\n            pq = urllib.parse.parse_qs(parsed.query, strict_parsing=True)\n            query = {}\n            for k, v in pq.items():\n                if k.startswith(\"pg\") and k not in [\"pghost\", \"pgport\"]:\n                    if isinstance(v, list):\n                        val = v[-1]\n                    else:\n                        val = cast(str, v)\n                    query[k[2:]] = val\n            if query:\n                dsn += f\"?{urllib.parse.urlencode(query)}\"\n\n    if tenant_id is None:\n        t_id = buildmeta.get_default_tenant_id()\n    else:\n        t_id = tenant_id\n\n    async def _get_cluster_type(\n        conn: pgcon.PGConnection,\n    ) -> tuple[type[RemoteCluster], Optional[str]]:\n        managed_clouds = {\n            'rds_superuser': RemoteCluster,    # Amazon RDS\n            'cloudsqlsuperuser': RemoteCluster,    # GCP Cloud SQL\n            'azure_pg_admin': RemoteCluster,    # Azure Postgres\n        }\n\n        managed_cloud_super = await conn.sql_fetch_val(\n            b\"\"\"\n                SELECT\n                    rolname\n                FROM\n                    pg_roles\n                WHERE\n                    rolname IN (SELECT json_array_elements_text($1::json))\n                LIMIT\n                    1\n            \"\"\",\n            args=[json.dumps(list(managed_clouds)).encode(\"utf-8\")],\n        )\n\n        if managed_cloud_super is not None:\n            rolname = managed_cloud_super.decode(\"utf-8\")\n            return managed_clouds[rolname], rolname\n        else:\n            return RemoteCluster, None\n\n    async def _detect_capabilities(\n        conn: pgcon.PGConnection,\n    ) -> pgparams.BackendCapabilities:\n        from edb.server import pgcon\n        from edb.server.pgcon import errors\n\n        caps = pgparams.BackendCapabilities.NONE\n\n        try:\n            cur_cluster_name = await conn.sql_fetch_val(\n                b\"\"\"\n                SELECT\n                    setting\n                FROM\n                    pg_file_settings\n                WHERE\n                    setting = 'cluster_name'\n                    AND sourcefile = ((\n                        SELECT setting\n                        FROM pg_settings WHERE name = 'data_directory'\n                    ) || '/postgresql.auto.conf')\n                \"\"\",\n            )\n        except pgcon.BackendPrivilegeError:\n            configfile_access = False\n        else:\n            try:\n                await conn.sql_execute(b\"\"\"\n                    ALTER SYSTEM SET cluster_name = 'edgedb-test'\n                \"\"\")\n            except pgcon.BackendPrivilegeError:\n                configfile_access = False\n            except pgcon.BackendError as e:\n                # Stolon keeper symlinks postgresql.auto.conf to /dev/null\n                # making ALTER SYSTEM fail with InternalServerError,\n                # see https://github.com/sorintlab/stolon/pull/343\n                if 'could not fsync file \"postgresql.auto.conf\"' in e.args[0]:\n                    configfile_access = False\n                else:\n                    raise\n            else:\n                configfile_access = True\n\n                if cur_cluster_name:\n                    cn = pgcommon.quote_literal(\n                        cur_cluster_name.decode(\"utf-8\"))\n                    await conn.sql_execute(\n                        f\"\"\"\n                        ALTER SYSTEM SET cluster_name = {cn}\n                        \"\"\".encode(\"utf-8\"),\n                    )\n                else:\n                    await conn.sql_execute(\n                        b\"\"\"\n                        ALTER SYSTEM SET cluster_name = DEFAULT\n                        \"\"\",\n                    )\n\n        if configfile_access:\n            caps |= pgparams.BackendCapabilities.CONFIGFILE_ACCESS\n\n        await conn.sql_execute(b\"START TRANSACTION\")\n        rname = str(uuidgen.uuid1mc())\n\n        try:\n            await conn.sql_execute(\n                f\"CREATE ROLE {pgcommon.quote_ident(rname)} WITH SUPERUSER\"\n                .encode(\"utf-8\"),\n            )\n        except pgcon.BackendPrivilegeError:\n            can_make_superusers = False\n        except pgcon.BackendError as e:\n            if e.code_is(\n                errors.ERROR_INTERNAL_ERROR\n            ) and \"not in permitted superuser list\" in str(e):\n                # DigitalOcean raises a custom error:\n                # XX000: Role ... not in permitted superuser list\n                can_make_superusers = False\n            else:\n                raise\n        else:\n            can_make_superusers = True\n        finally:\n            await conn.sql_execute(b\"ROLLBACK\")\n\n        if can_make_superusers:\n            caps |= pgparams.BackendCapabilities.SUPERUSER_ACCESS\n\n        coll = await conn.sql_fetch_val(b\"\"\"\n            SELECT collname FROM pg_collation\n            WHERE lower(replace(collname, '-', '')) = 'c.utf8' LIMIT 1;\n        \"\"\")\n\n        if coll is not None:\n            caps |= pgparams.BackendCapabilities.C_UTF8_LOCALE\n\n        roles = json.loads(await conn.sql_fetch_val(\n            b\"\"\"\n            SELECT json_build_object(\n                'rolcreaterole', rolcreaterole,\n                'rolcreatedb', rolcreatedb\n            )\n            FROM pg_roles\n            WHERE rolname = (SELECT current_user);\n            \"\"\",\n        ))\n\n        if roles['rolcreaterole']:\n            caps |= pgparams.BackendCapabilities.CREATE_ROLE\n        if roles['rolcreatedb']:\n            caps |= pgparams.BackendCapabilities.CREATE_DATABASE\n\n        stats_ver = await conn.sql_fetch_val(b\"\"\"\n            SELECT default_version FROM pg_available_extensions\n            WHERE name = 'edb_stat_statements';\n        \"\"\")\n        if stats_ver in (b\"1.0\",):\n            caps |= pgparams.BackendCapabilities.STAT_STATEMENTS\n\n        return caps\n\n    async def _get_pg_settings(\n        conn: pgcon.PGConnection,\n        name: str,\n    ) -> str:\n        return await conn.sql_fetch_val(  # type: ignore\n            b\"SELECT setting FROM pg_settings WHERE name = $1\",\n            args=[name.encode(\"utf-8\")],\n        )\n\n    async def _get_reserved_connections(\n        conn: pgcon.PGConnection,\n    ) -> int:\n        rv = int(\n            await _get_pg_settings(conn, 'superuser_reserved_connections')\n        )\n        for name in [\n            'rds.rds_superuser_reserved_connections',\n        ]:\n            value = await _get_pg_settings(conn, name)\n            if value:\n                rv += int(value)\n        return rv\n\n    probe_connection = pgconnparams.ConnectionParams(dsn=dsn)\n    conn = await pgcon.pg_connect(\n        probe_connection,\n        source_description=\"remote cluster probe\",\n        backend_params=pgparams.get_default_runtime_params(),\n        apply_init_script=False\n    )\n    params = conn.connection\n    addr = conn.addr\n\n    try:\n        data = json.loads(await conn.sql_fetch_val(\n            b\"\"\"\n            SELECT json_build_object(\n                'user', current_user,\n                'dbname', current_database(),\n                'connlimit', (\n                    select rolconnlimit\n                    from pg_roles\n                    where rolname = current_user\n                )\n            )\"\"\",\n        ))\n        params.update(\n            user=data[\"user\"],\n            database=data[\"dbname\"]\n        )\n        cluster_type, superuser_name = await _get_cluster_type(conn)\n        max_connections = data[\"connlimit\"]\n        pg_max_connections = await _get_pg_settings(conn, 'max_connections')\n        if max_connections == -1 or not isinstance(max_connections, int):\n            max_connections = pg_max_connections\n        else:\n            max_connections = min(max_connections, pg_max_connections)\n        capabilities = await _detect_capabilities(conn)\n\n        if (\n            specified_capabilities is not None\n            and specified_capabilities.must_be_absent\n        ):\n            disabled = []\n            for cap in specified_capabilities.must_be_absent:\n                if capabilities & cap:\n                    capabilities &= ~cap\n                    disabled.append(cap)\n            if disabled:\n                logger.info(\n                    f\"the following backend capabilities are explicitly \"\n                    f\"disabled by server command line: \"\n                    f\"{', '.join(str(cap.name) for cap in disabled)}\"\n                )\n\n        if t_id != buildmeta.get_default_tenant_id():\n            # GOTCHA: This tenant_id check cannot protect us from running\n            # multiple EdgeDB servers using the default tenant_id with\n            # different catalog versions on the same backend. However, that\n            # would fail during bootstrap in single-role/database mode.\n            if not capabilities & pgparams.BackendCapabilities.CREATE_ROLE:\n                raise ClusterError(\n                    \"The remote backend doesn't support CREATE ROLE; \"\n                    \"multi-tenancy is disabled.\"\n                )\n            if not capabilities & pgparams.BackendCapabilities.CREATE_DATABASE:\n                raise ClusterError(\n                    \"The remote backend doesn't support CREATE DATABASE; \"\n                    \"multi-tenancy is disabled.\"\n                )\n\n        pg_ver_string = conn.get_server_parameter_status(\"server_version\")\n        if pg_ver_string is None:\n            raise ClusterError(\n                \"remote server did not report its version \"\n                \"in ParameterStatus\")\n\n        if capabilities & pgparams.BackendCapabilities.CREATE_DATABASE:\n            # If we can create databases, assume we're free to create\n            # extensions in them as well.\n            ext_schema = \"edgedbext\"\n            existing_exts = {}\n        else:\n            ext_schema = (await conn.sql_fetch_val(\n                b'''\n                SELECT COALESCE(\n                    (SELECT schema_name FROM information_schema.schemata\n                    WHERE schema_name = 'heroku_ext'),\n                    'edgedbext')\n                ''',\n            )).decode(\"utf-8\")\n\n            existing_exts_data = await conn.sql_fetch(\n                b\"\"\"\n                SELECT\n                    extname,\n                    nspname\n                FROM\n                    pg_extension\n                    INNER JOIN pg_namespace\n                        ON (pg_extension.extnamespace = pg_namespace.oid)\n                \"\"\"\n            )\n\n            existing_exts = {\n                r[0].decode(\"utf-8\"): r[1].decode(\"utf-8\")\n                for r in existing_exts_data\n            }\n\n        instance_params = pgparams.BackendInstanceParams(\n            capabilities=capabilities,\n            version=buildmeta.parse_pg_version(pg_ver_string),\n            base_superuser=superuser_name,\n            max_connections=int(max_connections),\n            reserved_connections=await _get_reserved_connections(conn),\n            tenant_id=t_id,\n            ext_schema=ext_schema,\n            existing_exts=existing_exts,\n        )\n    finally:\n        conn.terminate()\n\n    return cluster_type(\n        connection_addr=addr,\n        connection_params=params,\n        instance_params=instance_params,\n        ha_backend=ha_backend,\n    )\n\n\nasync def _run_logged_text_subprocess(\n    args: Sequence[str],\n    logger: logging.Logger,\n    level: int = logging.DEBUG,\n    check: bool = True,\n    log_stdout: bool = True,\n    timeout: Optional[float] = None,\n    **kwargs: Any,\n) -> tuple[list[str], list[str], int]:\n    stdout_lines, stderr_lines, exit_code = await _run_logged_subprocess(\n        args,\n        logger=logger,\n        level=level,\n        check=check,\n        log_stdout=log_stdout,\n        timeout=timeout,\n        **kwargs,\n    )\n\n    return (\n        [line.decode() for line in stdout_lines],\n        [line.decode() for line in stderr_lines],\n        exit_code,\n    )\n\n\nasync def _run_logged_subprocess(\n    args: Sequence[str],\n    logger: logging.Logger,\n    level: int = logging.DEBUG,\n    check: bool = True,\n    log_stdout: bool = True,\n    log_stderr: bool = True,\n    capture_stdout: bool = True,\n    capture_stderr: bool = True,\n    timeout: Optional[float] = None,\n    stdin: Any = asyncio.subprocess.PIPE,\n    **kwargs: Any,\n) -> tuple[list[bytes], list[bytes], int]:\n    process, stdout_reader, stderr_reader = await _start_logged_subprocess(\n        args,\n        logger=logger,\n        level=level,\n        log_stdout=log_stdout,\n        log_stderr=log_stderr,\n        capture_stdout=capture_stdout,\n        capture_stderr=capture_stderr,\n        stdin=stdin,\n        **kwargs,\n    )\n\n    if isinstance(stdin, int) and stdin >= 0:\n        os.close(stdin)\n\n    exit_code, stdout_lines, stderr_lines = await asyncio.wait_for(\n        asyncio.gather(process.wait(), stdout_reader, stderr_reader),\n        timeout=timeout,\n    )\n\n    if exit_code != 0 and check:\n        stderr_text = b'\\n'.join(stderr_lines).decode()\n        raise ClusterError(\n            f'{args[0]} exited with status {exit_code}:\\n'\n            + textwrap.indent(stderr_text, ' ' * 4),\n        )\n    else:\n        return stdout_lines, stderr_lines, exit_code\n\n\nasync def _start_logged_subprocess(\n    args: Sequence[str],\n    *,\n    logger: logging.Logger,\n    level: int = logging.DEBUG,\n    override_stdout: Any = None,\n    override_stderr: Any = None,\n    log_stdout: bool = True,\n    log_stderr: bool = True,\n    capture_stdout: bool = True,\n    capture_stderr: bool = True,\n    stdin: Any = asyncio.subprocess.PIPE,\n    log_processor: Optional[Callable[[str], tuple[str, int]]] = None,\n    **kwargs: Any,\n) -> tuple[\n    asyncio.subprocess.Process,\n    Coroutine[Any, Any, list[bytes]],\n    Coroutine[Any, Any, list[bytes]],\n]:\n    logger.log(\n        level,\n        f'running `{\" \".join(shlex.quote(arg) for arg in args)}`'\n    )\n\n    process = await asyncio.create_subprocess_exec(\n        *args,\n        stdin=stdin,\n        stdout=(\n            override_stdout\n            if override_stdout\n            else asyncio.subprocess.PIPE\n            if log_stdout or capture_stdout\n            else asyncio.subprocess.DEVNULL\n        ),\n        stderr=(\n            override_stderr\n            if override_stderr\n            else asyncio.subprocess.PIPE\n            if log_stderr or capture_stderr\n            else asyncio.subprocess.DEVNULL\n        ),\n        limit=2 ** 20,  # 1 MiB\n        **kwargs,\n    )\n\n    if log_stderr or capture_stderr:\n        assert override_stderr is None\n        assert process.stderr is not None\n        stderr_reader = _capture_and_log_subprocess_output(\n            process.pid,\n            process.stderr,\n            logger,\n            level,\n            log_processor,\n            capture_output=capture_stderr,\n            log_output=log_stderr,\n        )\n    else:\n        stderr_reader = _dummy()\n\n    if log_stdout or capture_stdout:\n        assert override_stdout is None\n        assert process.stdout is not None\n        stdout_reader = _capture_and_log_subprocess_output(\n            process.pid,\n            process.stdout,\n            logger,\n            level,\n            log_processor,\n            capture_output=capture_stdout,\n            log_output=log_stdout,\n        )\n    else:\n        stdout_reader = _dummy()\n\n    return process, stdout_reader, stderr_reader\n\n\nasync def _capture_and_log_subprocess_output(\n    pid: int,\n    stream: asyncio.StreamReader,\n    logger: logging.Logger,\n    level: int,\n    log_processor: Optional[Callable[[str], tuple[str, int]]] = None,\n    *,\n    capture_output: bool,\n    log_output: bool,\n) -> list[bytes]:\n    lines = []\n    while not stream.at_eof():\n        line = await _safe_readline(stream)\n        if line or not stream.at_eof():\n            line = line.rstrip(b'\\n')\n            if capture_output:\n                lines.append(line)\n            if log_output:\n                log_line = line.decode()\n                if log_processor is not None:\n                    log_line, level = log_processor(log_line)\n                logger.log(level, log_line, extra={\"process\": pid})\n    return lines\n\n\nasync def _safe_readline(stream: asyncio.StreamReader) -> bytes:\n    try:\n        line = await stream.readline()\n    except ValueError:\n        line = b\"<too long>\"\n\n    return line\n\n\nasync def _dummy() -> list[bytes]:\n    return []\n\n\npostgres_to_python_level_map = {\n    \"DEBUG5\": logging.DEBUG,\n    \"DEBUG4\": logging.DEBUG,\n    \"DEBUG3\": logging.DEBUG,\n    \"DEBUG2\": logging.DEBUG,\n    \"DEBUG1\": logging.DEBUG,\n    \"INFO\": logging.INFO,\n    \"NOTICE\": logging.INFO,\n    \"LOG\": logging.INFO,\n    \"WARNING\": logging.WARNING,\n    \"ERROR\": logging.ERROR,\n    \"FATAL\": logging.CRITICAL,\n    \"PANIC\": logging.CRITICAL,\n}\n\npostgres_log_re = re.compile(r'^(\\w+):\\s*(.*)$')\n\npostgres_specific_msg_level_map = {\n    \"terminating connection due to administrator command\": logging.INFO,\n    \"the database system is shutting down\": logging.INFO,\n}\n\n\ndef postgres_log_processor(msg: str) -> tuple[str, int]:\n    if m := postgres_log_re.match(msg):\n        postgres_level = m.group(1)\n        msg = m.group(2)\n        level = postgres_specific_msg_level_map.get(\n            msg,\n            postgres_to_python_level_map.get(postgres_level, logging.INFO),\n        )\n    else:\n        level = logging.INFO\n\n    return msg, level\n"
  },
  {
    "path": "edb/server/pgcon/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom .errors import (\n    BackendError,\n    BackendConnectionError,\n    BackendPrivilegeError,\n    BackendCatalogNameError,\n)\n\nfrom .pgcon import (\n    PGConnection,\n)\nfrom .connect import (\n    pg_connect,\n    SETUP_TEMP_TABLE_SCRIPT,\n    SETUP_CONFIG_CACHE_SCRIPT,\n    SETUP_DML_DUMMY_TABLE_SCRIPT,\n    RESET_STATIC_CFG_SCRIPT,\n)\n\n__all__ = (\n    'pg_connect',\n    'PGConnection',\n    'BackendError',\n    'BackendConnectionError',\n    'BackendPrivilegeError',\n    'BackendCatalogNameError',\n    'SETUP_TEMP_TABLE_SCRIPT',\n    'SETUP_CONFIG_CACHE_SCRIPT',\n    'SETUP_DML_DUMMY_TABLE_SCRIPT',\n    'RESET_STATIC_CFG_SCRIPT'\n)\n"
  },
  {
    "path": "edb/server/pgcon/connect.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nimport logging\nimport textwrap\n\nfrom edb.pgsql.common import quote_ident as pg_qi\nfrom edb.pgsql.common import versioned_schema\nfrom edb.pgsql import params as pg_params\nfrom edb.server import pgcon\n\nfrom . import errors as pgerror\nfrom . import rust_transport\n\nlogger = logging.getLogger('edb.server')\n\nINIT_CON_SCRIPT: bytes | None = None\n\n# The '_edgecon_state table' is used to store information about\n# the current session. The `type` column is one character, with one\n# of the following values:\n#\n# * 'C': a session-level config setting\n#\n# * 'B': a session-level config setting that's implemented by setting\n#   a corresponding Postgres config setting.\n# * 'A': an instance-level config setting from command-line arguments\n# * 'E': an instance-level config setting from environment variable\n# * 'F': an instance/tenant-level config setting from the TOML config file\n#\n# * 'S': a session-level pg-ext config setting (frontend-only)\n# * 'L': a transaction-level pg-ext config setting (frontend-only)\n# * 'P': a session-level pg-ext config setting that's implemented by setting\n#   a corresponding backend config setting (not stored in _edgecon_state)\n#\n# Please also update ConStateType in edb/server/config/__init__.py if changed.\nSETUP_TEMP_TABLE_SCRIPT = '''\n        CREATE TEMPORARY TABLE _edgecon_state (\n            name text NOT NULL,\n            value jsonb NOT NULL,\n            type text NOT NULL CHECK(\n                type = 'C' OR type = 'B' OR type = 'A' OR type = 'E'\n                OR type = 'F' OR type = 'S' OR type = 'L'),\n            UNIQUE(name, type)\n        );\n'''.strip()\nSETUP_CONFIG_CACHE_SCRIPT = '''\n        CREATE TEMPORARY TABLE _config_cache (\n            source edgedb._sys_config_source_t,\n            value edgedb._sys_config_val_t NOT NULL\n        );\n'''.strip()\n\n# A empty dummy table used when we need to emit no-op DML.\n#\n# This is used by scan_check_ctes in the pgsql compiler to\n# force the evaluation of error checking.\nSETUP_DML_DUMMY_TABLE_SCRIPT = '''\n        CREATE TEMPORARY TABLE _dml_dummy (\n            id int8,\n            flag bool,\n            unique(id)\n        );\n        INSERT INTO _dml_dummy VALUES (0, false);\n'''.strip()\n\nRESET_STATIC_CFG_SCRIPT: bytes = b'''\n    WITH x1 AS (\n        DELETE FROM _config_cache\n    )\n    DELETE FROM _edgecon_state WHERE type = 'A' OR type = 'E' OR type = 'F';\n'''\n\n\ndef _build_init_con_script(*, check_pg_is_in_recovery: bool) -> bytes:\n    if check_pg_is_in_recovery:\n        pg_is_in_recovery = ('''\n        SELECT CASE WHEN pg_is_in_recovery() THEN\n            edgedb.raise(\n                NULL::bigint,\n                'read_only_sql_transaction',\n                msg => 'cannot use a hot standby'\n            )\n        END;\n        ''').strip()\n    else:\n        pg_is_in_recovery = ''\n\n    edgedb_schema = versioned_schema('edgedb')\n    return textwrap.dedent(f'''\n        {pg_is_in_recovery}\n\n        {SETUP_TEMP_TABLE_SCRIPT}\n        {SETUP_CONFIG_CACHE_SCRIPT}\n        {SETUP_DML_DUMMY_TABLE_SCRIPT}\n\n        CREATE CONSTRAINT TRIGGER _edgecon_state_local_reset\n        AFTER INSERT ON _edgecon_state\n        DEFERRABLE INITIALLY DEFERRED\n        FOR EACH ROW\n        EXECUTE FUNCTION {edgedb_schema}._clear_fe_local_sql_settings();\n\n        PREPARE _clear_state AS\n            WITH x1 AS (\n                DELETE FROM _config_cache\n            )\n            DELETE FROM _edgecon_state\n                WHERE type = 'C' OR type = 'B' or type = 'S';\n\n        PREPARE _apply_state(jsonb) AS\n            INSERT INTO\n                _edgecon_state(name, value, type)\n            SELECT\n                (CASE\n                    WHEN e->'type' = '\"B\"'::jsonb\n                    THEN edgedb._apply_session_config(e->>'name', e->'value')\n                    ELSE e->>'name'\n                END) AS name,\n                e->'value' AS value,\n                e->>'type' AS type\n            FROM\n                jsonb_array_elements($1::jsonb) AS e;\n\n        PREPARE _reset_session_config AS\n            SELECT edgedb._reset_session_config();\n\n        PREPARE _apply_sql_state(jsonb) AS\n            WITH be AS (\n                SELECT\n                    pg_catalog.set_config(\n                        e->>'name', e->>'value', false\n                    ) AS value\n                FROM\n                    jsonb_array_elements($1::jsonb) AS e\n                WHERE\n                    e->'type' = '\"P\"'::jsonb\n            ),\n            fe AS (\n                INSERT INTO\n                    _edgecon_state(name, value, type)\n                SELECT\n                    e->>'name' AS name,\n                    e->'value' AS value,\n                    e->>'type' AS type\n                FROM\n                    jsonb_array_elements($1::jsonb) AS e\n                WHERE\n                    e->'type' = '\"S\"'::jsonb\n                RETURNING 1\n            )\n            SELECT 1 FROM be, fe;\n\n        PREPARE _set_sql_state(text, text, text) AS\n            INSERT INTO\n                _edgecon_state(name, type, value)\n            VALUES\n                ($1, $2, pg_catalog.to_jsonb($3))\n            ON CONFLICT (name, type) DO UPDATE\n                SET value = pg_catalog.to_jsonb($3);\n\n        PREPARE _reset_sql_state(text, text) AS\n            DELETE FROM\n                _edgecon_state\n            WHERE\n                name = $1 AND type = $2;\n\n        PREPARE _reset_sql_state_all AS\n            DELETE FROM\n                _edgecon_state\n            WHERE\n                type = 'S' OR type = 'L';\n    ''').strip().encode('utf-8')\n\n\nasync def pg_connect(\n    dsn_or_connection: str | rust_transport.ConnectionParams,\n    *,\n    backend_params: pg_params.BackendRuntimeParams,\n    source_description: str,\n    apply_init_script: bool = True,\n) -> pgcon.PGConnection:\n    global INIT_CON_SCRIPT\n\n    if isinstance(dsn_or_connection, str):\n        connection = rust_transport.ConnectionParams(dsn=dsn_or_connection)\n    else:\n        connection = dsn_or_connection\n\n    # Note that we intentionally differ from the libpq connection behaviour\n    # here: if we fail to connect with SSL enabled, we DO NOT retry with SSL\n    # disabled.\n    pgrawcon, pgconn = await rust_transport.create_postgres_connection(\n        connection,\n        lambda: pgcon.PGConnection(dbname=connection.database),\n        source_description=source_description,\n    )\n\n    connection = pgrawcon.connection\n    pgconn.connection = pgrawcon.connection\n    pgconn.parameter_status = pgrawcon.state.parameters\n    cancellation_key = pgrawcon.state.cancellation_key\n    if cancellation_key:\n        pgconn.backend_pid = cancellation_key[0]\n        pgconn.backend_secret = cancellation_key[1]\n    pgconn.is_ssl = pgrawcon.state.ssl\n    pgconn.addr = pgrawcon.addr\n\n    if (\n        backend_params.has_create_role\n        and backend_params.session_authorization_role\n    ):\n        sup_role = backend_params.session_authorization_role\n        if connection.user != sup_role:\n            # We used to use SET SESSION AUTHORIZATION here, there're some\n            # security differences over SET ROLE, but as we don't allow\n            # accessing Postgres directly through EdgeDB, SET ROLE is mostly\n            # fine here. (Also hosted backends like Postgres on DigitalOcean\n            # support only SET ROLE)\n            await pgconn.sql_execute(f'SET ROLE {pg_qi(sup_role)}'.encode())\n\n    if 'in_hot_standby' in pgconn.parameter_status:\n        # in_hot_standby is always present in Postgres 14 and above\n        if pgconn.parameter_status['in_hot_standby'] == 'on':\n            # Abort if we're connecting to a hot standby\n            pgconn.terminate()\n            raise pgerror.BackendError(\n                fields=dict(\n                    M=\"cannot use a hot standby\",\n                    C=pgerror.ERROR_READ_ONLY_SQL_TRANSACTION,\n                )\n            )\n\n    if apply_init_script:\n        if INIT_CON_SCRIPT is None:\n            INIT_CON_SCRIPT = _build_init_con_script(\n                # On lower versions of Postgres we use pg_is_in_recovery() to\n                # check if it is a hot standby, and error out if it is.\n                check_pg_is_in_recovery=(\n                    'in_hot_standby' not in pgconn.parameter_status\n                ),\n            )\n        try:\n            try:\n                await pgconn.sql_execute(INIT_CON_SCRIPT)\n            except pgcon.BackendError:\n                from edb.pgsql import dbops, metaschema\n\n                # ClearFELocalSQLSettingsFunction is needed by the\n                # INIT_CON_SCRIPT, so we cannot simply patch it up\n                # in the regular edb/pgsql/patches.py\n                block = dbops.PLTopBlock()\n                func = metaschema.ClearFELocalSQLSettingsFunction()\n                dbops.CreateFunction(func, or_replace=True).generate(block)\n\n                await pgconn.sql_execute(block.to_string().encode('utf-8'))\n                await pgconn.sql_execute(INIT_CON_SCRIPT)\n        except Exception:\n            logger.exception(\n                f\"Failed to run init script for {pgconn.connection.to_dsn()}\"\n            )\n            await pgconn.close()\n            raise\n\n    return pgconn\n"
  },
  {
    "path": "edb/server/pgcon/cpythonx.pxd",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ncdef extern from \"Python.h\":\n    void* PyMem_Calloc(size_t nelem, size_t elsize)\n"
  },
  {
    "path": "edb/server/pgcon/errors.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nERROR_FEATURE_NOT_SUPPORTED = '0A000'\n\nERROR_CARDINALITY_VIOLATION = '21000'\n\n# Class 22 — Data Exception\nERROR_DATA_EXCEPTION = '22000'\nERROR_NUMERIC_VALUE_OUT_OF_RANGE = '22003'\nERROR_INVALID_DATETIME_FORMAT = '22007'\nERROR_DATETIME_FIELD_OVERFLOW = '22008'\nERROR_DIVISION_BY_ZERO = '22012'\nERROR_INTERVAL_FIELD_OVERFLOW = '22015'\nERROR_CHARACTER_NOT_IN_REPERTOIRE = '22021'\nERROR_INVALID_PARAMETER_VALUE = '22023'\nERROR_INVALID_TEXT_REPRESENTATION = '22P02'\nERROR_INVALID_REGULAR_EXPRESSION = '2201B'\nERROR_INVALID_LOGARITHM_ARGUMENT = '2201E'\nERROR_INVALID_POWER_ARGUMENT = '2201F'\nERROR_INVALID_ROW_COUNT_IN_LIMIT_CLAUSE = '2201W'\nERROR_INVALID_ROW_COUNT_IN_RESULT_OFFSET_CLAUSE = '2201X'\n\n# Class 23 — Integrity Constraint Violation\nERROR_INTEGRITY_CONSTRAINT_VIOLATION = '23000'\nERROR_RESTRICT_VIOLATION = '23001'\nERROR_NOT_NULL_VIOLATION = '23502'\nERROR_FOREIGN_KEY_VIOLATION = '23503'\nERROR_UNIQUE_VIOLATION = '23505'\nERROR_CHECK_VIOLATION = '23514'\nERROR_EXCLUSION_VIOLATION = '23P01'\n\n# Class 25 - Invalid Transaction State\nERRCODE_IN_FAILED_SQL_TRANSACTION = '25P02'\nERROR_IDLE_IN_TRANSACTION_TIMEOUT = '25P03'\nERROR_READ_ONLY_SQL_TRANSACTION = '25006'\n\nERROR_INVALID_SQL_STATEMENT_NAME = '26000'\n\n# Class 28 - Invalid Authorization Specification\nERROR_INVALID_AUTHORIZATION_SPECIFICATION = '28000'\nERROR_INVALID_PASSWORD = '28P01'\n\nERROR_INVALID_CATALOG_NAME = '3D000'\nERROR_INVALID_CURSOR_NAME = '34000'\n\nERROR_SERIALIZATION_FAILURE = '40001'\nERROR_DEADLOCK_DETECTED = '40P01'\n\n# Class 42 - Syntax Error or Access Rule Violation\nERROR_WRONG_OBJECT_TYPE = '42809'\nERROR_INSUFFICIENT_PRIVILEGE = '42501'\nERROR_UNDEFINED_COLUMN = '42703'\nERROR_UNDEFINED_FUNCTION = '42883'\nERROR_UNDEFINED_TABLE = '42P01'\nERROR_UNDEFINED_PARAMETER = '42P02'\nERROR_DUPLICATE_DATABASE = '42P04'\nERROR_SYNTAX_ERROR = '42601'\nERROR_DUPLICATE_CURSOR = '42P03'\nERROR_DUPLICATE_PREPARED_STATEMENT = '42P05'\nERROR_INVALID_COLUMN_REFERENCE = '42P10'\n\nERROR_PROGRAM_LIMIT_EXCEEDED = '54000'\n\nERROR_OBJECT_IN_USE = '55006'\n\nERROR_QUERY_CANCELLED = '57014'\nERROR_CANNOT_CONNECT_NOW = '57P03'\n\n# Class 08 - Connection Exception\nERROR_CONNECTION_CLIENT_CANNOT_CONNECT = '08001'\nERROR_CONNECTION_DOES_NOT_EXIST = '08003'\nERROR_CONNECTION_REJECTION = '08004'\nERROR_CONNECTION_FAILURE = '08006'\nERROR_PROTOCOL_VIOLATION = '08P01'\n\nERROR_INTERNAL_ERROR = 'XX000'\n\nCONNECTION_ERROR_CODES = [\n    ERROR_CANNOT_CONNECT_NOW,\n    ERROR_CONNECTION_CLIENT_CANNOT_CONNECT,\n    ERROR_CONNECTION_DOES_NOT_EXIST,\n    ERROR_CONNECTION_REJECTION,\n    ERROR_CONNECTION_FAILURE,\n]\n\n\nclass BackendError(Exception):\n\n    def __init__(self, *, fields: dict[str, str]) -> None:\n        msg = fields.get('M', f'error code {fields[\"C\"]}')\n        self.fields = fields\n        super().__init__(msg)\n\n    def code_is(self, code: str) -> bool:\n        return self.fields[\"C\"] == code\n\n    def get_field(self, field: str) -> str | None:\n        return self.fields.get(field)\n\n\ndef get_error_class(fields: dict[str, str]) -> type[BackendError]:\n    return error_class_map.get(fields[\"C\"], BackendError)\n\n\nclass BackendQueryCancelledError(BackendError):\n    pass\n\n\nclass BackendConnectionError(BackendError):\n    pass\n\n\nclass BackendPrivilegeError(BackendError):\n    pass\n\n\nclass BackendCatalogNameError(BackendError):\n    pass\n\n\nerror_class_map = {\n    ERROR_CANNOT_CONNECT_NOW: BackendConnectionError,\n    ERROR_CONNECTION_CLIENT_CANNOT_CONNECT: BackendConnectionError,\n    ERROR_CONNECTION_DOES_NOT_EXIST: BackendConnectionError,\n    ERROR_CONNECTION_REJECTION: BackendConnectionError,\n    ERROR_CONNECTION_FAILURE: BackendConnectionError,\n    ERROR_INSUFFICIENT_PRIVILEGE: BackendPrivilegeError,\n    ERROR_QUERY_CANCELLED: BackendQueryCancelledError,\n    ERROR_INVALID_CATALOG_NAME: BackendCatalogNameError,\n}\n\n\ndef _build_fields(code, message, severity=\"ERROR\", detail=None, hint=None):\n    fields = {\n        \"S\": severity,\n        \"V\": severity,\n        \"C\": code,\n        \"M\": message,\n    }\n    if detail is not None:\n        fields[\"D\"] = detail\n    if hint is not None:\n        fields[\"H\"] = hint\n    return fields\n\n\ndef new(\n    code, message, severity=\"ERROR\", detail=None, hint=None, **extra_fields\n):\n    fields = _build_fields(code, message, severity, detail, hint)\n    fields.update(extra_fields)\n    return get_error_class(fields)(fields=fields)\n\n\nclass FeatureNotSupported(BackendError):\n    def __init__(self, message=\"feature not supported\", **kwargs):\n        super().__init__(\n            fields=_build_fields(ERROR_FEATURE_NOT_SUPPORTED, message, **kwargs)\n        )\n\n\nclass ProtocolViolation(BackendError):\n    def __init__(self, message=\"protocol violation\", **kwargs):\n        super().__init__(\n            fields=_build_fields(ERROR_PROTOCOL_VIOLATION, message, **kwargs)\n        )\n\n\nclass CannotConnectNowError(BackendError):\n    def __init__(self, message=\"cannot connect now\", **kwargs):\n        super().__init__(\n            fields=_build_fields(ERROR_CANNOT_CONNECT_NOW, message, **kwargs)\n        )\n\n\nclass InvalidAuthSpec(BackendError):\n    def __init__(self, message=\"invalid authorization specification\", **kwargs):\n        super().__init__(\n            fields=_build_fields(\n                ERROR_INVALID_AUTHORIZATION_SPECIFICATION, message, **kwargs\n            )\n        )\n"
  },
  {
    "path": "edb/server/pgcon/pgcon.pxd",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\ninclude \"./pgcon_sql.pxd\"\n\ncimport cython\ncimport cpython\n\nfrom libc.stdint cimport int8_t, uint8_t, int16_t, uint16_t, \\\n                         int32_t, uint32_t, int64_t, uint64_t\n\nfrom edb.server.pgproto.pgproto cimport (\n    WriteBuffer,\n    ReadBuffer,\n    FRBuffer,\n)\n\nfrom edb.server.dbview cimport dbview\nfrom edb.server.pgproto.debug cimport PG_DEBUG\n\nfrom edb.server.cache cimport stmt_cache\n\n\ncdef enum PGTransactionStatus:\n    PQTRANS_IDLE = 0                 # connection idle\n    PQTRANS_ACTIVE = 1               # command in progress\n    PQTRANS_INTRANS = 2              # idle, within transaction block\n    PQTRANS_INERROR = 3              # idle, within failed transaction\n    PQTRANS_UNKNOWN = 4              # cannot determine status\n\n\ncdef enum PGAuthenticationState:\n    PGAUTH_SUCCESSFUL = 0\n    PGAUTH_REQUIRED_KERBEROS = 2\n    PGAUTH_REQUIRED_PASSWORD = 3\n    PGAUTH_REQUIRED_PASSWORDMD5 = 5\n    PGAUTH_REQUIRED_SCMCRED = 6\n    PGAUTH_REQUIRED_GSS = 7\n    PGAUTH_REQUIRED_GSS_CONTINUE = 8\n    PGAUTH_REQUIRED_SSPI = 9\n    PGAUTH_REQUIRED_SASL = 10\n    PGAUTH_SASL_CONTINUE = 11\n    PGAUTH_SASL_FINAL = 12\n\n\n@cython.final\ncdef class PGConnection:\n\n    cdef:\n        ReadBuffer buffer\n\n        object loop\n        str dbname\n\n        object transport\n        object msg_waiter\n\n        readonly bint connected\n        object connected_fut\n\n        int32_t waiting_for_sync\n        PGTransactionStatus xact_status\n\n        public int32_t backend_pid\n        public int32_t backend_secret\n        public object parameter_status\n\n        readonly object aborted_with_error\n\n        stmt_cache.StatementsCache prep_stmts\n        list last_parse_prep_stmts\n\n        list log_listeners\n\n        bint debug\n\n        public object connection\n        public object addr\n        object server\n        object tenant\n        bint is_system_db\n        bint close_requested\n\n        readonly bint idle\n\n        object cancel_fut\n\n        bint _is_ssl\n\n        public object pinned_by\n\n        object last_state\n        bint state_reset_needs_commit\n        public object last_init_con_data\n\n        str last_indirect_return\n\n        PGSQLConnection _sql\n\n    cdef before_command(self)\n\n    cdef write(self, buf)\n    cdef write_sync(self, WriteBuffer outbuf)\n\n    cdef parse_error_message(self)\n    cdef char parse_sync_message(self)\n    cdef parse_parameter_status_message(self)\n\n    cdef parse_notification(self)\n    cdef fallthrough(self)\n    cdef fallthrough_idle(self)\n\n    cdef bint before_prepare(\n        self, bytes stmt_name, int dbver, WriteBuffer outbuf)\n    cdef write_sync(self, WriteBuffer outbuf)\n    cdef send_sync(self)\n\n    cdef make_clean_stmt_message(self, bytes stmt_name)\n    cdef send_query_unit_group(\n        self, object query_unit_group, bint sync,\n        object bind_datas, bytes state,\n        ssize_t start, ssize_t end, int dbver, object parse_array,\n        object query_prefix,\n        bint needs_commit_state,\n    )\n\n    cdef _rewrite_copy_data(\n        self,\n        WriteBuffer wbuf,\n        char *data,\n        ssize_t data_len,\n        ssize_t ncols,\n        tuple elide_cols,\n        dict type_id_map,\n        tuple data_mending_desc,\n    )\n\n    cdef _mend_copy_datum(\n        self,\n        WriteBuffer wbuf,\n        FRBuffer *rbuf,\n        object mending_desc,\n        dict type_id_map,\n    )\n\n    cdef inline str get_tenant_label(self)\n    cpdef set_stmt_cache_size(self, int maxsize)\n"
  },
  {
    "path": "edb/server/pgcon/pgcon.pyi",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n)\n\nimport asyncio\n\nfrom edb.server import defines as edbdef\nfrom edb.server import pgconnparams\n\nclass BackendError(Exception):\n    def get_field(self, field: str) -> str | None: ...\n    def code_is(self, code: str) -> bool: ...\n\nclass BackendConnectionError(BackendError): ...\nclass BackendPrivilegeError(BackendError): ...\nclass BackendCatalogNameError(BackendError): ...\n\nclass PGConnection(asyncio.Protocol):\n\n    idle: bool\n    backend_pid: int\n    connection: pgconnparams.ConnectionParams\n    addr: tuple[str, int]\n    parameter_status: dict[str, str]\n    backend_secret: int\n    is_ssl: bool\n    last_init_con_data: object\n\n    def __init__(self, dbname): ...\n    async def close(self): ...\n    async def sql_execute(\n        self,\n        sql: bytes | tuple[bytes, ...],\n        *,\n        tx_isolation: edbdef.TxIsolationLevel | None = None,\n    ) -> None: ...\n    async def sql_fetch(\n        self,\n        sql: bytes,\n        *,\n        args: tuple[bytes, ...] | list[bytes] = (),\n        use_prep_stmt: bool = False,\n        state: Optional[bytes] = None,\n        tx_isolation: edbdef.TxIsolationLevel | None = None,\n    ) -> list[tuple[bytes, ...]]: ...\n    async def sql_fetch_val(\n        self,\n        sql: bytes,\n        *,\n        args: tuple[bytes, ...] | list[bytes] = (),\n        use_prep_stmt: bool = False,\n        state: Optional[bytes] = None,\n        tx_isolation: edbdef.TxIsolationLevel | None = None,\n    ) -> bytes: ...\n    async def sql_fetch_col(\n        self,\n        sql: bytes,\n        *,\n        args: tuple[bytes, ...] | list[bytes] = (),\n        use_prep_stmt: bool = False,\n        state: Optional[bytes] = None,\n        tx_isolation: edbdef.TxIsolationLevel | None = None,\n    ) -> list[bytes]: ...\n    async def sql_describe(\n        self,\n        sql: bytes,\n        param_type_oids: list[int] | None = None,\n    ) -> tuple[list[int], list[tuple[str, int]]]: ...\n    def terminate(self) -> None: ...\n    def add_log_listener(self, cb: Callable[[str, str], None]) -> None: ...\n    def get_server_parameter_status(self, parameter: str) -> Optional[str]: ...\n    def set_stmt_cache_size(self, size: int) -> None: ...\n    def set_server(self, server: object) -> None: ...\n    async def signal_sysevent(self, event: str, *, dbname: str) -> None: ...\n    def abort(self) -> None: ...\n    def is_healthy(self) -> bool: ...\n    async def listen_for_sysevent(self) -> None: ...\n    def mark_as_system_db(self) -> None: ...\n    def set_tenant(self, tenant: Any) -> None: ...\n    def is_cancelling(self) -> bool: ...\n    def start_pg_cancellation(self) -> None: ...\n    def finish_pg_cancellation(self) -> None: ...\n\nSETUP_TEMP_TABLE_SCRIPT: str\nSETUP_CONFIG_CACHE_SCRIPT: str\nSETUP_DML_DUMMY_TABLE_SCRIPT: str\n"
  },
  {
    "path": "edb/server/pgcon/pgcon.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import (\n    Any,\n    Callable,\n    Dict,\n    Optional,\n)\n\nimport asyncio\nimport contextlib\nimport decimal\nimport codecs\nimport hashlib\nimport json\nimport logging\nimport os.path\nimport sys\nimport struct\nimport textwrap\nimport time\n\ncimport cython\ncimport cpython\n\nfrom . cimport cpythonx\n\nfrom libc.stdint cimport int8_t, uint8_t, int16_t, uint16_t, \\\n                         int32_t, uint32_t, int64_t, uint64_t, \\\n                         UINT32_MAX\n\nfrom edb import errors\nfrom edb.edgeql import qltypes\nfrom edb.pgsql import common as pgcommon\nfrom edb.pgsql.common import quote_literal as pg_ql\nfrom edb.pgsql import codegen as pg_codegen\n\nfrom edb.server.pgproto cimport hton\nfrom edb.server.pgproto cimport pgproto\nfrom edb.server.pgproto.pgproto cimport (\n    WriteBuffer,\n    ReadBuffer,\n\n    FRBuffer,\n    frb_init,\n    frb_read,\n    frb_get_len,\n    frb_slice_from,\n)\n\nfrom edb.server import compiler\nfrom edb.server.compiler import dbstate\nfrom edb.server import defines\nfrom edb.server.cache cimport stmt_cache\nfrom edb.server.dbview cimport dbview\nfrom edb.server.protocol cimport args_ser\nfrom edb.server.protocol cimport pg_ext\nfrom edb.server import metrics\n\nfrom edb.server.protocol cimport frontend\n\nfrom edb.common import debug\n\nfrom . import errors as pgerror\n\nDEF DATA_BUFFER_SIZE = 100_000\nDEF PREP_STMTS_CACHE = 100\n\nDEF COPY_SIGNATURE = b\"PGCOPY\\n\\377\\r\\n\\0\"\n\ncdef object CARD_NO_RESULT = compiler.Cardinality.NO_RESULT\ncdef object FMT_NONE = compiler.OutputFormat.NONE\ncdef dict POSTGRES_SHUTDOWN_ERR_CODES = {\n    '57P01': 'admin_shutdown',\n    '57P02': 'crash_shutdown',\n}\n\ncdef object EMPTY_SQL_STATE = b\"{}\"\ncdef WriteBuffer NO_ARGS = args_ser.combine_raw_args()\n\ncdef object logger = logging.getLogger('edb.server')\n\ninclude \"./pgcon_sql.pyx\"\n\n\n@cython.final\ncdef class EdegDBCodecContext(pgproto.CodecContext):\n\n    cdef:\n        object _codec\n\n    def __cinit__(self):\n        self._codec = codecs.lookup('utf-8')\n\n    cpdef get_text_codec(self):\n        return self._codec\n\n    cdef is_encoding_utf8(self):\n        return True\n\n\n@cython.final\ncdef class PGConnection:\n\n    def __init__(self, dbname):\n        self.buffer = ReadBuffer()\n\n        self.loop = asyncio.get_running_loop()\n        self.dbname = dbname\n\n        self.connection = None\n        self.transport = None\n        self.msg_waiter = None\n\n        self.prep_stmts = stmt_cache.StatementsCache(maxsize=PREP_STMTS_CACHE)\n\n        self.connected_fut = self.loop.create_future()\n        self.connected = False\n\n        self.waiting_for_sync = 0\n        self.xact_status = PQTRANS_UNKNOWN\n\n        self.backend_pid = -1\n        self.backend_secret = -1\n        self.parameter_status = dict()\n\n        self.last_parse_prep_stmts = []\n        self.debug = debug.flags.server_proto\n\n        self.last_indirect_return = None\n\n        self.log_listeners = []\n\n        self.server = None\n        self.tenant = None\n        self.is_system_db = False\n        self.close_requested = False\n\n        self.pinned_by = None\n\n        self.idle = True\n        self.cancel_fut = None\n\n        self._is_ssl = False\n\n        # Set to the error the connection has been aborted with\n        # by the backend.\n        self.aborted_with_error = None\n\n        # Session State Management\n        # ------------------------\n        # Due to the fact that backend sessions are not pinned to frontend\n        # sessions (EdgeQL, SQL, etc.) out of transactions, we need to sync\n        # the backend state with the frontend state before executing queries.\n        #\n        # For performance reasons, we try to avoid syncing the state by\n        # remembering the last state we've synced (last_state), and prefer\n        # backend connection with the same state as the frontend.\n        #\n        # Syncing the state is done by resetting the session state as a whole,\n        # followed by applying the new state, so that we don't have to track\n        # individual config resets. Again for performance reasons, the state\n        # sync is usually applied in the same implicit transaction as the\n        # actual query in order to avoid extra round trips.\n        #\n        # Though, there are exceptions when we need to sync the state in a\n        # separate transaction by inserting a SYNC message before the actual\n        # query. This is because either that the query itself is a START\n        # TRANSACTION / non-transactional command and a few other cases (see\n        # _parse_execute() below), or the state change affects new transaction\n        # creation like changing the `default_transaction_isolation` or its\n        # siblings (see `needs_commit_state` parameters). In such cases, we\n        # remember the `last_state` immediately after we received the\n        # ReadyForQuery message caused by the SYNC above, if there are no\n        # errors happened during state sync. Otherwise, we only remember\n        # `last_state` after the implicit transaction ends successfully, when\n        # we're sure the state is synced permanently.\n        #\n        # The actual queries may also change the session state. Regardless of\n        # how we synced state previously, we always remember the `last_state`\n        # after successful executions (also after transactions without errors,\n        # implicit or explicit).\n        #\n        # Finally, resetting an existing session state that was positive in\n        # `needs_commit_state` also requires a commit, because the new state\n        # may not have `needs_commit_state`. To achieve this, we remember the\n        # previous `needs_commit_state` in `state_reset_needs_commit` and\n        # always insert a SYNC in the next state sync if it's True. Also, if\n        # the actual queries modified those `default_transaction_*` settings,\n        # we also need to set `state_reset_needs_commit` to True for the next\n        # state sync(reset). See `needs_commit_after_state_sync()` functions\n        # in dbview classes (EdgeQL and SQL).\n        self.last_state = dbview.DEFAULT_STATE\n        self.state_reset_needs_commit = False\n\n        self._sql = PGSQLConnection(self)\n\n    cpdef set_stmt_cache_size(self, int maxsize):\n        self.prep_stmts.resize(maxsize)\n\n    @property\n    def is_ssl(self):\n        return self._is_ssl\n\n    @is_ssl.setter\n    def is_ssl(self, value):\n        self._is_ssl = value\n\n    def debug_print(self, *args):\n        print(\n            '::PGCONN::',\n            hex(id(self)),\n            f'pgpid: {self.backend_pid}',\n            *args,\n            file=sys.stderr,\n        )\n\n    def in_tx(self):\n        return (\n            self.xact_status == PQTRANS_INTRANS or\n            self.xact_status == PQTRANS_INERROR\n        )\n\n    def is_cancelling(self):\n        return self.cancel_fut is not None\n\n    def start_pg_cancellation(self):\n        if self.cancel_fut is not None:\n            raise RuntimeError('another cancellation is in progress')\n        self.cancel_fut = self.loop.create_future()\n\n    def finish_pg_cancellation(self):\n        assert self.cancel_fut is not None\n        self.cancel_fut.set_result(True)\n\n    def get_server_parameter_status(self, parameter: str) -> Optional[str]:\n        return self.parameter_status.get(parameter)\n\n    def abort(self):\n        if not self.transport:\n            return\n        self.close_requested = True\n        self.transport.abort()\n        self.transport = None\n        self.connected = False\n        self.prep_stmts.clear()\n\n    def terminate(self):\n        if not self.transport:\n            return\n        self.close_requested = True\n        self.write(WriteBuffer.new_message(b'X').end_message())\n        self.transport.close()\n        self.transport = None\n        self.connected = False\n        self.prep_stmts.clear()\n\n        if self.msg_waiter and not self.msg_waiter.done():\n            self.msg_waiter.set_exception(ConnectionAbortedError())\n            self.msg_waiter = None\n\n    async def close(self):\n        self.terminate()\n\n    def set_tenant(self, tenant):\n        self.tenant = tenant\n        self.server = tenant.server\n\n    def mark_as_system_db(self):\n        if self.tenant.get_backend_runtime_params().has_create_database:\n            assert defines.EDGEDB_SYSTEM_DB in self.dbname\n        self.is_system_db = True\n\n    def add_log_listener(self, cb):\n        self.log_listeners.append(cb)\n\n    async def listen_for_sysevent(self):\n        try:\n            if self.tenant.get_backend_runtime_params().has_create_database:\n                assert defines.EDGEDB_SYSTEM_DB in self.dbname\n            await self.sql_execute(b'LISTEN __edgedb_sysevent__;')\n        except Exception:\n            try:\n                self.abort()\n            finally:\n                raise\n\n    async def signal_sysevent(self, event, **kwargs):\n        if self.tenant.get_backend_runtime_params().has_create_database:\n            assert defines.EDGEDB_SYSTEM_DB in self.dbname\n        event = json.dumps({\n            'event': event,\n            'server_id': self.server._server_id,\n            'args': kwargs,\n        })\n        query = f\"\"\"\n            SELECT pg_notify(\n                '__edgedb_sysevent__',\n                {pg_ql(event)}\n            )\n        \"\"\".encode()\n        await self.sql_execute(query)\n\n    async def sync(self):\n        if self.waiting_for_sync:\n            raise RuntimeError('a \"sync\" has already been requested')\n\n        self.before_command()\n        try:\n            self.waiting_for_sync += 1\n            self.write(_SYNC_MESSAGE)\n\n            while True:\n                if not self.buffer.take_message():\n                    await self.wait_for_message()\n                mtype = self.buffer.get_message_type()\n\n                if mtype == b'Z':\n                    self.parse_sync_message()\n                    return\n                else:\n                    self.fallthrough()\n        finally:\n            await self.after_command()\n\n    async def wait_for_sync(self):\n        error = None\n        try:\n            while True:\n                if not self.buffer.take_message():\n                    await self.wait_for_message()\n                mtype = self.buffer.get_message_type()\n                if mtype == b'Z':\n                    return self.parse_sync_message()\n                elif mtype == b'E':\n                    # ErrorResponse\n                    er_cls, fields = self.parse_error_message()\n                    error = er_cls(fields=fields)\n                else:\n                    if not self.parse_notification():\n                        if PG_DEBUG or self.debug:\n                            self.debug_print(f'PGCon.wait_for_sync: discarding '\n                                            f'{chr(mtype)!r} message')\n                        self.buffer.discard_message()\n        finally:\n            if error is not None:\n                # Postgres might send an ErrorResponse if, e.g.\n                # in implicit transaction fails to commit due to\n                # serialization conflicts.\n                raise error\n\n    cdef inline str get_tenant_label(self):\n        if self.tenant is None:\n            return \"system\"\n        else:\n            return self.tenant.get_instance_name()\n\n    cdef bint before_prepare(\n        self,\n        bytes stmt_name,\n        int dbver,\n        WriteBuffer outbuf,\n    ):\n        cdef bint parse = 1\n\n        while self.prep_stmts.needs_cleanup():\n            stmt_name_to_clean, _ = self.prep_stmts.cleanup_one()\n            if self.debug:\n                self.debug_print(f\"discarding ps {stmt_name_to_clean!r}\")\n            outbuf.write_buffer(\n                self.make_clean_stmt_message(stmt_name_to_clean))\n\n        if stmt_name in self.prep_stmts:\n            if self.prep_stmts[stmt_name] == dbver:\n                parse = 0\n            else:\n                if self.debug:\n                    self.debug_print(f\"discarding ps {stmt_name!r}\")\n                outbuf.write_buffer(\n                    self.make_clean_stmt_message(stmt_name))\n                del self.prep_stmts[stmt_name]\n\n        return parse\n\n    cdef write_sync(self, WriteBuffer outbuf):\n        outbuf.write_bytes(_SYNC_MESSAGE)\n        self.waiting_for_sync += 1\n\n    cdef send_sync(self):\n        self.write(_SYNC_MESSAGE)\n        self.waiting_for_sync += 1\n\n    def _build_apply_state_req(self, bytes serstate, WriteBuffer out):\n        cdef:\n            WriteBuffer buf\n\n        if self.debug:\n            self.debug_print(\"Syncing state: \", serstate)\n\n        buf = WriteBuffer.new_message(b'B')\n        buf.write_bytestring(b'')  # portal name\n        buf.write_bytestring(b'_clear_state')  # statement name\n        buf.write_int16(0)  # number of format codes\n        buf.write_int16(0)  # number of parameters\n        buf.write_int16(0)  # number of result columns\n        out.write_buffer(buf.end_message())\n\n        buf = WriteBuffer.new_message(b'E')\n        buf.write_bytestring(b'')  # portal name\n        buf.write_int32(0)  # limit: 0 - return all rows\n        out.write_buffer(buf.end_message())\n\n        buf = WriteBuffer.new_message(b'B')\n        buf.write_bytestring(b'')  # portal name\n        buf.write_bytestring(b'_reset_session_config')  # statement name\n        buf.write_int16(0)  # number of format codes\n        buf.write_int16(0)  # number of parameters\n        buf.write_int16(0)  # number of result columns\n        out.write_buffer(buf.end_message())\n\n        buf = WriteBuffer.new_message(b'E')\n        buf.write_bytestring(b'')  # portal name\n        buf.write_int32(0)  # limit: 0 - return all rows\n        out.write_buffer(buf.end_message())\n\n        if serstate is not None:\n            buf = WriteBuffer.new_message(b'B')\n            buf.write_bytestring(b'')  # portal name\n            buf.write_bytestring(b'_apply_state')  # statement name\n            buf.write_int16(1)  # number of format codes\n            buf.write_int16(1)  # binary\n            buf.write_int16(1)  # number of parameters\n            buf.write_int32(len(serstate) + 1)\n            buf.write_byte(1)  # jsonb format version\n            buf.write_bytes(serstate)\n            buf.write_int16(0)  # number of result columns\n            out.write_buffer(buf.end_message())\n\n            buf = WriteBuffer.new_message(b'E')\n            buf.write_bytestring(b'')  # portal name\n            buf.write_int32(0)  # limit: 0 - return all rows\n            out.write_buffer(buf.end_message())\n\n    def _build_apply_sql_state_req(self, bytes state, WriteBuffer out):\n        cdef:\n            WriteBuffer buf\n\n        buf = WriteBuffer.new_message(b'B')\n        buf.write_bytestring(b'')  # portal name\n        buf.write_bytestring(b'_clear_state')  # statement name\n        buf.write_int16(0)  # number of format codes\n        buf.write_int16(0)  # number of parameters\n        buf.write_int16(0)  # number of result columns\n        out.write_buffer(buf.end_message())\n\n        buf = WriteBuffer.new_message(b'E')\n        buf.write_bytestring(b'')  # portal name\n        buf.write_int32(0)  # limit: 0 - return all rows\n        out.write_buffer(buf.end_message())\n\n        buf = WriteBuffer.new_message(b'B')\n        buf.write_bytestring(b'')  # portal name\n        buf.write_bytestring(b'_reset_session_config')  # statement name\n        buf.write_int16(0)  # number of format codes\n        buf.write_int16(0)  # number of parameters\n        buf.write_int16(0)  # number of result columns\n        out.write_buffer(buf.end_message())\n\n        buf = WriteBuffer.new_message(b'E')\n        buf.write_bytestring(b'')  # portal name\n        buf.write_int32(0)  # limit: 0 - return all rows\n        out.write_buffer(buf.end_message())\n\n        if state != EMPTY_SQL_STATE:\n            buf = WriteBuffer.new_message(b'B')\n            buf.write_bytestring(b'')  # portal name\n            buf.write_bytestring(b'_apply_sql_state')  # statement name\n            buf.write_int16(1)  # number of format codes\n            buf.write_int16(1)  # binary\n            buf.write_int16(1)  # number of parameters\n            buf.write_int32(len(state) + 1)\n            buf.write_byte(1)  # jsonb format version\n            buf.write_bytes(state)\n            buf.write_int16(0)  # number of result columns\n            out.write_buffer(buf.end_message())\n\n            buf = WriteBuffer.new_message(b'E')\n            buf.write_bytestring(b'')  # portal name\n            buf.write_int32(0)  # limit: 0 - return all rows\n            out.write_buffer(buf.end_message())\n\n    async def _parse_apply_state_resp(self, int expected_completed):\n        cdef:\n            int num_completed = 0\n\n        while True:\n            if not self.buffer.take_message():\n                await self.wait_for_message()\n            mtype = self.buffer.get_message_type()\n\n            if mtype == b'2' or mtype == b'D':\n                # BindComplete or Data\n                self.buffer.discard_message()\n\n            elif mtype == b'E':\n                er_cls, er_fields = self.parse_error_message()\n                raise er_cls(fields=er_fields)\n\n            elif mtype == b'C':\n                self.buffer.discard_message()\n                num_completed += 1\n                if num_completed == expected_completed:\n                    return\n            else:\n                self.fallthrough()\n\n    @contextlib.asynccontextmanager\n    async def parse_execute_script_context(self):\n        self.before_command()\n        started_at = time.monotonic()\n        try:\n            try:\n                yield\n            finally:\n                while self.waiting_for_sync:\n                    await self.wait_for_sync()\n        finally:\n            metrics.backend_query_duration.observe(\n                time.monotonic() - started_at, self.get_tenant_label()\n            )\n            await self.after_command()\n\n    cdef send_query_unit_group(\n        self, object query_unit_group, bint sync,\n        object bind_datas, bytes state,\n        ssize_t start, ssize_t end, int dbver, object parse_array,\n        object query_prefix,\n        bint needs_commit_state,\n    ):\n        # parse_array is an array of booleans for output with the same size as\n        # the query_unit_group, indicating if each unit is freshly parsed\n        cdef:\n            WriteBuffer out\n            WriteBuffer buf\n            WriteBuffer bind_data\n            bytes stmt_name\n            ssize_t idx = start\n            bytes sql\n            tuple sqls\n\n        out = WriteBuffer.new()\n        parsed = set()\n\n        if state is not None and start == 0:\n            self._build_apply_state_req(state, out)\n            # N.B: Condition here needs to match that in wait_for_state_resp\n            if needs_commit_state or self.state_reset_needs_commit:\n                self.write_sync(out)\n\n        # Build the parse_array first, closing statements if needed before\n        # actually executing any command that may fail, in order to ensure\n        # self.prep_stmts is always in sync with the actual open statements\n        for query_unit in query_unit_group.units[start:end]:\n            if query_unit.system_config:\n                raise RuntimeError(\n                    \"CONFIGURE INSTANCE command is not allowed in scripts\"\n                )\n            stmt_name = query_unit.sql_hash\n            if stmt_name:\n                # The same EdgeQL query may show up twice in the same script.\n                # We just need to know and skip if we've already parsed the\n                # same query within current send batch, because self.prep_stmts\n                # will be updated before the next batch, with maybe a different\n                # dbver after DDL.\n                if stmt_name not in parsed and self.before_prepare(\n                    stmt_name, dbver, out\n                ):\n                    parse_array[idx] = True\n                    parsed.add(stmt_name)\n            idx += 1\n        idx = start\n\n        for query_unit, bind_data in zip(\n            query_unit_group.units[start:end], bind_datas):\n            stmt_name = query_unit.sql_hash\n            sql = query_unit.sql\n            if query_prefix:\n                sql = query_prefix + sql\n            if stmt_name:\n                if parse_array[idx]:\n                    buf = WriteBuffer.new_message(b'P')\n                    buf.write_bytestring(stmt_name)\n                    buf.write_bytestring(sql)\n                    buf.write_int16(0)\n                    out.write_buffer(buf.end_message())\n                    metrics.query_size.observe(\n                        len(sql),\n                        self.get_tenant_label(),\n                        'compiled',\n                    )\n\n                buf = WriteBuffer.new_message(b'B')\n                buf.write_bytestring(b'')  # portal name\n                buf.write_bytestring(stmt_name)\n                buf.write_buffer(bind_data)\n                out.write_buffer(buf.end_message())\n\n                buf = WriteBuffer.new_message(b'E')\n                buf.write_bytestring(b'')  # portal name\n                buf.write_int32(0)  # limit: 0 - return all rows\n                out.write_buffer(buf.end_message())\n\n            else:\n                buf = WriteBuffer.new_message(b'P')\n                buf.write_bytestring(b'')  # statement name\n                buf.write_bytestring(sql)\n                buf.write_int16(0)\n                out.write_buffer(buf.end_message())\n                metrics.query_size.observe(\n                    len(sql), self.get_tenant_label(), 'compiled'\n                )\n\n                buf = WriteBuffer.new_message(b'B')\n                buf.write_bytestring(b'')  # portal name\n                buf.write_bytestring(b'')  # statement name\n                buf.write_buffer(bind_data)\n                out.write_buffer(buf.end_message())\n\n                buf = WriteBuffer.new_message(b'E')\n                buf.write_bytestring(b'')  # portal name\n                buf.write_int32(0)  # limit: 0 - return all rows\n                out.write_buffer(buf.end_message())\n\n            idx += 1\n\n        if sync:\n            self.write_sync(out)\n        else:\n            out.write_bytes(FLUSH_MESSAGE)\n\n        self.write(out)\n\n    async def force_error(self):\n        self.before_command()\n\n        # Send a bogus parse that will cause an error to be generated\n        out = WriteBuffer.new()\n        buf = WriteBuffer.new_message(b'P')\n        buf.write_bytestring(b'')\n        buf.write_bytestring(b'<INTERNAL ERROR IN GEL PGCON>')\n        buf.write_int16(0)\n\n        # Then do a sync to get everything executed and lined back up\n        out.write_buffer(buf.end_message())\n        self.write_sync(out)\n\n        self.write(out)\n\n        try:\n            await self.wait_for_sync()\n        except pgerror.BackendError as e:\n            pass\n        else:\n            raise RuntimeError(\"Didn't get expected error!\")\n        finally:\n            await self.after_command()\n\n    async def wait_for_state_resp(\n        self, bytes state, bint state_sync, bint needs_commit_state\n    ):\n        # N.B: Condition here needs to match that in send_query_unit_group\n        if state_sync or self.state_reset_needs_commit:\n            try:\n                await self._parse_apply_state_resp(2 if state is None else 3)\n            finally:\n                await self.wait_for_sync()\n            self.last_state = state\n            self.state_reset_needs_commit = needs_commit_state\n        else:\n            await self._parse_apply_state_resp(2 if state is None else 3)\n\n    async def wait_for_command(\n        self,\n        object query_unit,\n        bint parse,\n        int dbver,\n        *,\n        bint ignore_data,\n        frontend.AbstractFrontendConnection fe_conn = None,\n    ):\n        cdef WriteBuffer buf = None\n\n        result = None\n        while True:\n            if not self.buffer.take_message():\n                await self.wait_for_message()\n            mtype = self.buffer.get_message_type()\n\n            try:\n                if mtype == b'D':\n                    # DataRow\n                    if ignore_data:\n                        self.buffer.discard_message()\n                    elif fe_conn is None:\n                        ncol = self.buffer.read_int16()\n                        row = []\n                        for i in range(ncol):\n                            coll = self.buffer.read_int32()\n                            if coll == -1:\n                                row.append(None)\n                            else:\n                                row.append(self.buffer.read_bytes(coll))\n                        if result is None:\n                            result = []\n                        result.append(row)\n                    else:\n                        if buf is None:\n                            buf = WriteBuffer.new()\n\n                        self.buffer.redirect_messages(buf, b'D', 0)\n                        if buf.len() >= DATA_BUFFER_SIZE:\n                            fe_conn.write(buf)\n                            buf = None\n\n                elif mtype == b'C':  ## result\n                    # CommandComplete\n                    self.buffer.discard_message()\n                    if buf is not None:\n                        fe_conn.write(buf)\n                        buf = None\n                    return result\n\n                elif mtype == b'1':\n                    # ParseComplete\n                    self.buffer.discard_message()\n                    if parse:\n                        self.prep_stmts[query_unit.sql_hash] = dbver\n\n                elif mtype == b'E':  ## result\n                    # ErrorResponse\n                    er_cls, er_fields = self.parse_error_message()\n                    raise er_cls(fields=er_fields)\n\n                elif mtype == b'n':\n                    # NoData\n                    self.buffer.discard_message()\n\n                elif mtype == b's':  ## result\n                    # PortalSuspended\n                    self.buffer.discard_message()\n                    return result\n\n                elif mtype == b'2':\n                    # BindComplete\n                    self.buffer.discard_message()\n\n                elif mtype == b'3':\n                    # CloseComplete\n                    self.buffer.discard_message()\n\n                elif mtype == b'I':  ## result\n                    # EmptyQueryResponse\n                    self.buffer.discard_message()\n\n                else:\n                    self.fallthrough()\n\n            finally:\n                self.buffer.finish_message()\n\n    async def _describe(\n        self,\n        query: bytes,\n        param_type_oids: Optional[list[int]],\n    ):\n        cdef:\n            WriteBuffer out\n\n        out = WriteBuffer.new()\n\n        buf = WriteBuffer.new_message(b\"P\")  # Parse\n        buf.write_bytestring(b\"\")\n        buf.write_bytestring(query)\n        if param_type_oids:\n            buf.write_int16(len(param_type_oids))\n            for oid in param_type_oids:\n                buf.write_int32(<int32_t>oid)\n        else:\n            buf.write_int16(0)\n        out.write_buffer(buf.end_message())\n\n        buf = WriteBuffer.new_message(b\"D\")  # Describe\n        buf.write_byte(b\"S\")\n        buf.write_bytestring(b\"\")\n        out.write_buffer(buf.end_message())\n\n        out.write_bytes(FLUSH_MESSAGE)\n\n        self.write(out)\n\n        param_desc = None\n        result_desc = None\n\n        try:\n            buf = None\n            while True:\n                if not self.buffer.take_message():\n                    await self.wait_for_message()\n                mtype = self.buffer.get_message_type()\n\n                try:\n                    if mtype == b'1':\n                        # ParseComplete\n                        self.buffer.discard_message()\n\n                    elif mtype == b't':\n                        # ParameterDescription\n                        param_desc = self._decode_param_desc(self.buffer)\n\n                    elif mtype == b'T':\n                        # RowDescription\n                        result_desc = self._decode_row_desc(self.buffer)\n                        break\n\n                    elif mtype == b'n':\n                        # NoData\n                        self.buffer.discard_message()\n                        param_desc = []\n                        result_desc = []\n                        break\n\n                    elif mtype == b'E':  ## result\n                        # ErrorResponse\n                        er_cls, er_fields = self.parse_error_message()\n                        raise er_cls(fields=er_fields)\n\n                    else:\n                        self.fallthrough()\n\n                finally:\n                    self.buffer.finish_message()\n        except Exception:\n            self.send_sync()\n            await self.wait_for_sync()\n            raise\n\n        if param_desc is None:\n            raise RuntimeError(\n                \"did not receive ParameterDescription from backend \"\n                \"in response to Describe\"\n            )\n\n        if result_desc is None:\n            raise RuntimeError(\n                \"did not receive RowDescription from backend \"\n                \"in response to Describe\"\n            )\n\n        return param_desc, result_desc\n\n    def _decode_param_desc(self, buf: ReadBuffer):\n        cdef:\n            int16_t nparams\n            uint32_t p_oid\n            list result = []\n\n        nparams = buf.read_int16()\n\n        for _ in range(nparams):\n            p_oid = <uint32_t>buf.read_int32()\n            result.append(p_oid)\n\n        return result\n\n    def _decode_row_desc(self, buf: ReadBuffer):\n        cdef:\n            int16_t nfields\n\n            bytes f_name\n            uint32_t f_table_oid\n            int16_t f_column_num\n            uint32_t f_dt_oid\n            int16_t f_dt_size\n            int32_t f_dt_mod\n            int16_t f_format\n\n            list result\n\n        nfields = buf.read_int16()\n\n        result = []\n        for _ in range(nfields):\n            f_name = buf.read_null_str()\n            f_table_oid = <uint32_t>buf.read_int32()\n            f_column_num = buf.read_int16()\n            f_dt_oid = <uint32_t>buf.read_int32()\n            f_dt_size = buf.read_int16()\n            f_dt_mod = buf.read_int32()\n            f_format = buf.read_int16()\n\n            result.append((f_name.decode(\"utf-8\"), f_dt_oid))\n\n        return result\n\n    async def sql_describe(\n        self,\n        query: bytes,\n        param_type_oids: Optional[list[int]] = None,\n    ) -> tuple[list[int], list[tuple[str, int]]]:\n        self.before_command()\n        started_at = time.monotonic()\n        try:\n            return await self._describe(query, param_type_oids)\n        finally:\n            await self.after_command()\n\n    async def _parse_execute(\n        self,\n        query,\n        frontend.AbstractFrontendConnection fe_conn,\n        WriteBuffer bind_data,\n        bint use_prep_stmt,\n        bytes state,\n        int dbver,\n        bint use_pending_func_cache,\n        tx_isolation,\n        list param_data_types,\n        bytes query_prefix,\n        bint needs_commit_state,\n    ):\n        cdef:\n            WriteBuffer out\n            WriteBuffer buf\n            bytes stmt_name\n            bytes sql\n            tuple sqls\n            bytes prologue_sql\n            bytes epilogue_sql\n\n            int32_t dat_len\n\n            bint parse = 1\n            bint state_sync = 0\n\n            bint has_result = query.cardinality is not CARD_NO_RESULT\n            bint discard_result = (\n                fe_conn is not None and query.output_format == FMT_NONE)\n\n            uint64_t msgs_num\n            uint64_t msgs_executed = 0\n            uint64_t i\n\n        out = WriteBuffer.new()\n\n        if state is not None:\n            self._build_apply_state_req(state, out)\n            if (\n                query.tx_id\n                or not query.is_transactional\n                or query.run_and_rollback\n                or tx_isolation is not None\n                or needs_commit_state\n                or self.state_reset_needs_commit\n            ):\n                # This query has START TRANSACTION or non-transactional command\n                # like CREATE DATABASE in it.\n                # Restoring state must be performed in a separate\n                # implicit transaction (otherwise START TRANSACTION DEFERRABLE\n                # or CREATE DATABASE (since PG 14.7) would fail).\n                # Hence - inject a SYNC after a state restore step.\n                state_sync = 1\n                self.write_sync(out)\n\n        if query.run_and_rollback or tx_isolation is not None:\n            if self.in_tx():\n                sp_name = f'_edb_{time.monotonic_ns()}'\n                prologue_sql = f'SAVEPOINT {sp_name}'.encode('utf-8')\n            else:\n                sp_name = None\n                prologue_sql = b'START TRANSACTION'\n                if tx_isolation is not None:\n                    prologue_sql += (\n                        f' ISOLATION LEVEL {tx_isolation._value_}'\n                        .encode('utf-8')\n                    )\n\n            buf = WriteBuffer.new_message(b'P')\n            buf.write_bytestring(b'')\n            buf.write_bytestring(prologue_sql)\n            buf.write_int16(0)\n            out.write_buffer(buf.end_message())\n\n            buf = WriteBuffer.new_message(b'B')\n            buf.write_bytestring(b'')  # portal name\n            buf.write_bytestring(b'')  # statement name\n            buf.write_int16(0)  # number of format codes\n            buf.write_int16(0)  # number of parameters\n            buf.write_int16(0)  # number of result columns\n            out.write_buffer(buf.end_message())\n\n            buf = WriteBuffer.new_message(b'E')\n            buf.write_bytestring(b'')  # portal name\n            buf.write_int32(0)  # limit: 0 - return all rows\n            out.write_buffer(buf.end_message())\n\n            # Insert a SYNC as a boundary of the parsing logic later\n            self.write_sync(out)\n\n        if use_pending_func_cache and query.cache_func_call:\n            sql, stmt_name = query.cache_func_call\n            sqls = (query_prefix + sql,)\n        else:\n            sqls = (query_prefix + query.sql,) + query.db_op_trailer\n            stmt_name = query.sql_hash\n\n        msgs_num = <uint64_t>(len(sqls))\n\n        if use_prep_stmt:\n            parse = self.before_prepare(stmt_name, dbver, out)\n        else:\n            stmt_name = b''\n\n        if parse:\n            if len(self.last_parse_prep_stmts):\n                for stmt_name_to_clean in self.last_parse_prep_stmts:\n                    out.write_buffer(\n                        self.make_clean_stmt_message(stmt_name_to_clean))\n                self.last_parse_prep_stmts.clear()\n\n            if stmt_name == b'' and msgs_num > 1:\n                i = 0\n                for sql in sqls:\n                    pname = b'__p%d__' % i\n                    self.last_parse_prep_stmts.append(pname)\n                    buf = WriteBuffer.new_message(b'P')\n                    buf.write_bytestring(pname)\n                    buf.write_bytestring(sql)\n                    buf.write_int16(0)\n                    out.write_buffer(buf.end_message())\n                    i += 1\n                    metrics.query_size.observe(\n                        len(sql), self.get_tenant_label(), 'compiled'\n                    )\n            else:\n                if len(sqls) != 1:\n                    raise errors.InternalServerError(\n                        'cannot PARSE more than one SQL query '\n                        'in non-anonymous mode')\n                msgs_num = 1\n                buf = WriteBuffer.new_message(b'P')\n                buf.write_bytestring(stmt_name)\n                buf.write_bytestring(sqls[0])\n                if param_data_types:\n                    buf.write_int16(len(param_data_types))\n                    for oid in param_data_types:\n                        buf.write_int32(<int32_t>oid)\n                else:\n                    buf.write_int16(0)\n                out.write_buffer(buf.end_message())\n                metrics.query_size.observe(\n                    len(sqls[0]), self.get_tenant_label(), 'compiled'\n                )\n\n        assert bind_data is not None\n        if stmt_name == b'' and msgs_num > 1:\n            for s in self.last_parse_prep_stmts:\n                buf = WriteBuffer.new_message(b'B')\n                buf.write_bytestring(b'')  # portal name\n                buf.write_bytestring(s)  # statement name\n                buf.write_buffer(bind_data)\n                out.write_buffer(buf.end_message())\n\n                buf = WriteBuffer.new_message(b'E')\n                buf.write_bytestring(b'')  # portal name\n                buf.write_int32(0)  # limit: 0 - return all rows\n                out.write_buffer(buf.end_message())\n        else:\n            buf = WriteBuffer.new_message(b'B')\n            buf.write_bytestring(b'')  # portal name\n            buf.write_bytestring(stmt_name)  # statement name\n            buf.write_buffer(bind_data)\n            out.write_buffer(buf.end_message())\n\n            buf = WriteBuffer.new_message(b'E')\n            buf.write_bytestring(b'')  # portal name\n            buf.write_int32(0)  # limit: 0 - return all rows\n            out.write_buffer(buf.end_message())\n\n        if query.run_and_rollback or tx_isolation is not None:\n            if query.run_and_rollback:\n                if sp_name:\n                    sql = f'ROLLBACK TO SAVEPOINT {sp_name}'.encode('utf-8')\n                else:\n                    sql = b'ROLLBACK'\n            else:\n                sql = b'COMMIT'\n\n            buf = WriteBuffer.new_message(b'P')\n            buf.write_bytestring(b'')\n            buf.write_bytestring(sql)\n            buf.write_int16(0)\n            out.write_buffer(buf.end_message())\n\n            buf = WriteBuffer.new_message(b'B')\n            buf.write_bytestring(b'')  # portal name\n            buf.write_bytestring(b'')  # statement name\n            buf.write_int16(0)  # number of format codes\n            buf.write_int16(0)  # number of parameters\n            buf.write_int16(0)  # number of result columns\n            out.write_buffer(buf.end_message())\n\n            buf = WriteBuffer.new_message(b'E')\n            buf.write_bytestring(b'')  # portal name\n            buf.write_int32(0)  # limit: 0 - return all rows\n            out.write_buffer(buf.end_message())\n        elif query.append_tx_op:\n            if query.tx_commit:\n                sql = b'COMMIT'\n            elif query.tx_rollback:\n                sql = b'ROLLBACK'\n            else:\n                raise errors.InternalServerError(\n                    \"QueryUnit.append_tx_op is set but none of the \"\n                    \"Query.tx_<foo> properties are\"\n                )\n\n            buf = WriteBuffer.new_message(b'P')\n            buf.write_bytestring(b'')\n            buf.write_bytestring(sql)\n            buf.write_int16(0)\n            out.write_buffer(buf.end_message())\n\n            buf = WriteBuffer.new_message(b'B')\n            buf.write_bytestring(b'')  # portal name\n            buf.write_bytestring(b'')  # statement name\n            buf.write_int16(0)  # number of format codes\n            buf.write_int16(0)  # number of parameters\n            buf.write_int16(0)  # number of result columns\n            out.write_buffer(buf.end_message())\n\n            buf = WriteBuffer.new_message(b'E')\n            buf.write_bytestring(b'')  # portal name\n            buf.write_int32(0)  # limit: 0 - return all rows\n            out.write_buffer(buf.end_message())\n\n        self.write_sync(out)\n        self.write(out)\n\n        result = None\n\n        try:\n            if state is not None:\n                await self.wait_for_state_resp(\n                    state, state_sync, needs_commit_state)\n\n            if query.run_and_rollback or tx_isolation is not None:\n                await self.wait_for_sync()\n\n            buf = None\n            while True:\n                if not self.buffer.take_message():\n                    await self.wait_for_message()\n                mtype = self.buffer.get_message_type()\n\n                try:\n                    if mtype == b'D':\n                        # DataRow\n                        if discard_result:\n                            self.buffer.discard_message()\n                            continue\n                        if not has_result and fe_conn is not None:\n                            raise errors.InternalServerError(\n                                f'query that was inferred to have '\n                                f'no data returned received a DATA package; '\n                                f'query: {sqls}')\n\n                        if fe_conn is None:\n                            ncol = self.buffer.read_int16()\n                            row = []\n                            for i in range(ncol):\n                                dat_len = self.buffer.read_int32()\n                                if dat_len == -1:\n                                    row.append(None)\n                                else:\n                                    row.append(\n                                        self.buffer.read_bytes(dat_len))\n                            if result is None:\n                                result = []\n                            result.append(row)\n                        else:\n                            if buf is None:\n                                buf = WriteBuffer.new()\n\n                            self.buffer.redirect_messages(buf, b'D', 0)\n                            if buf.len() >= DATA_BUFFER_SIZE:\n                                fe_conn.write(buf)\n                                buf = None\n\n                    elif mtype == b'C':  ## result\n                        # CommandComplete\n                        self.buffer.discard_message()\n                        if buf is not None:\n                            fe_conn.write(buf)\n                            buf = None\n                        msgs_executed += 1\n                        if msgs_executed == msgs_num:\n                            break\n\n                    elif mtype == b'1' and parse:\n                        # ParseComplete\n                        self.buffer.discard_message()\n                        self.prep_stmts[stmt_name] = dbver\n\n                    elif mtype == b'E':  ## result\n                        # ErrorResponse\n                        er_cls, er_fields = self.parse_error_message()\n                        raise er_cls(fields=er_fields)\n\n                    elif mtype == b'n':\n                        # NoData\n                        self.buffer.discard_message()\n\n                    elif mtype == b's':  ## result\n                        # PortalSuspended\n                        self.buffer.discard_message()\n                        break\n\n                    elif mtype == b'2':\n                        # BindComplete\n                        self.buffer.discard_message()\n\n                    elif mtype == b'I':  ## result\n                        # EmptyQueryResponse\n                        self.buffer.discard_message()\n                        break\n\n                    elif mtype == b'3':\n                        # CloseComplete\n                        self.buffer.discard_message()\n\n                    else:\n                        self.fallthrough()\n\n                finally:\n                    self.buffer.finish_message()\n        finally:\n            await self.wait_for_sync()\n\n        return result\n\n    async def parse_execute(\n        self,\n        *,\n        query,\n        WriteBuffer bind_data = NO_ARGS,\n        list param_data_types = None,\n        frontend.AbstractFrontendConnection fe_conn = None,\n        bint use_prep_stmt = False,\n        bytes state = None,\n        int dbver = 0,\n        bint use_pending_func_cache = 0,\n        tx_isolation = None,\n        query_prefix = None,\n        bint needs_commit_state = False,\n    ):\n        self.before_command()\n        started_at = time.monotonic()\n        try:\n            return await self._parse_execute(\n                query,\n                fe_conn,\n                bind_data,\n                use_prep_stmt,\n                state,\n                dbver,\n                use_pending_func_cache,\n                tx_isolation,\n                param_data_types,\n                query_prefix or b'',\n                needs_commit_state,\n            )\n        finally:\n            metrics.backend_query_duration.observe(\n                time.monotonic() - started_at, self.get_tenant_label()\n            )\n            await self.after_command()\n\n    async def sql_fetch(\n        self,\n        sql: bytes,\n        *,\n        args: tuple[bytes, ...] | list[bytes] = (),\n        use_prep_stmt: bool = False,\n        state: Optional[bytes] = None,\n        tx_isolation: defines.TxIsolationLevel | None = None,\n    ) -> list[tuple[bytes, ...]]:\n        if use_prep_stmt:\n            sql_digest = hashlib.sha1()\n            sql_digest.update(sql)\n            sql_hash = sql_digest.hexdigest().encode('latin1')\n        else:\n            sql_hash = None\n\n        query = compiler.QueryUnit(\n            sql=sql,\n            sql_hash=sql_hash,\n            status=b\"\",\n        )\n\n        return await self.parse_execute(\n            query=query,\n            bind_data=args_ser.combine_raw_args(args),\n            use_prep_stmt=use_prep_stmt,\n            state=state,\n            tx_isolation=tx_isolation,\n        )\n\n    async def sql_fetch_val(\n        self,\n        sql: bytes,\n        *,\n        args: tuple[bytes, ...] | list[bytes] = (),\n        use_prep_stmt: bool = False,\n        state: Optional[bytes] = None,\n        tx_isolation: defines.TxIsolationLevel | None = None,\n    ) -> bytes:\n        data = await self.sql_fetch(\n            sql,\n            args=args,\n            use_prep_stmt=use_prep_stmt,\n            state=state,\n            tx_isolation=tx_isolation,\n        )\n        if data is None or len(data) == 0:\n            return None\n        elif len(data) > 1:\n            raise RuntimeError(\n                f\"received too many rows for sql_fetch_val({sql!r})\")\n        row = data[0]\n        if len(row) != 1:\n            raise RuntimeError(\n                f\"received too many columns for sql_fetch_val({sql!r})\")\n        return row[0]\n\n    async def sql_fetch_col(\n        self,\n        sql: bytes,\n        *,\n        args: tuple[bytes, ...] | list[bytes] = (),\n        use_prep_stmt: bool = False,\n        state: Optional[bytes] = None,\n        tx_isolation: defines.TxIsolationLevel | None = None,\n    ) -> list[bytes]:\n        data = await self.sql_fetch(\n            sql,\n            args=args,\n            use_prep_stmt=use_prep_stmt,\n            state=state,\n            tx_isolation=tx_isolation,\n        )\n        if not data:\n            return []\n        else:\n            if len(data[0]) != 1:\n                raise RuntimeError(\n                    f\"received too many columns for sql_fetch_col({sql!r})\")\n            return [row[0] for row in data]\n\n    async def _sql_execute(self, bytes sql):\n        cdef:\n            WriteBuffer out\n            WriteBuffer buf\n\n        out = WriteBuffer.new()\n\n        buf = WriteBuffer.new_message(b'Q')\n        buf.write_bytestring(sql)\n        out.write_buffer(buf.end_message())\n        self.waiting_for_sync += 1\n\n        self.write(out)\n\n        exc = None\n        result = None\n\n        while True:\n            if not self.buffer.take_message():\n                await self.wait_for_message()\n            mtype = self.buffer.get_message_type()\n\n            try:\n                if mtype == b'D':\n                    self.buffer.discard_message()\n\n                elif mtype == b'T':\n                    # RowDescription\n                    self.buffer.discard_message()\n\n                elif mtype == b'C':\n                    # CommandComplete\n                    self.buffer.discard_message()\n\n                elif mtype == b'E':\n                    # ErrorResponse\n                    exc = self.parse_error_message()\n\n                elif mtype == b'I':\n                    # EmptyQueryResponse\n                    self.buffer.discard_message()\n\n                elif mtype == b'Z':\n                    self.parse_sync_message()\n                    break\n\n                else:\n                    self.fallthrough()\n\n            finally:\n                self.buffer.finish_message()\n\n        if exc is not None:\n            raise exc[0](fields=exc[1])\n        else:\n            return result\n\n    async def sql_execute(self, sql: bytes | tuple[bytes, ...]) -> None:\n        self.before_command()\n        started_at = time.monotonic()\n\n        if isinstance(sql, tuple):\n            sql_string = b\";\\n\".join(sql)\n        else:\n            sql_string = sql\n\n        try:\n            return await self._sql_execute(sql_string)\n        finally:\n            metrics.backend_query_duration.observe(\n                time.monotonic() - started_at, self.get_tenant_label()\n            )\n            await self.after_command()\n\n    async def sql_apply_state(\n        self,\n        dbv: pg_ext.ConnectionView,\n    ):\n        self.before_command()\n        try:\n            state = dbv.serialize_state()\n            if state is not None:\n                buf = WriteBuffer.new()\n                self._build_apply_sql_state_req(state, buf)\n                self.write_sync(buf)\n                self.write(buf)\n\n                await self._parse_apply_state_resp(\n                    2 if state != EMPTY_SQL_STATE else 1\n                )\n                await self.wait_for_sync()\n                self.last_state = state\n                self.state_reset_needs_commit = (\n                    dbv.needs_commit_after_state_sync())\n        finally:\n            await self.after_command()\n\n    async def sql_extended_query(\n        self,\n        actions,\n        fe_conn: frontend.AbstractFrontendConnection,\n        dbver: int,\n        dbv: pg_ext.ConnectionView,\n    ) -> tuple[bool, bool]:\n        self.before_command()\n        try:\n            state = self._sql._write_sql_extended_query(actions, dbver, dbv)\n            if state is not None:\n                await self._parse_apply_state_resp(\n                    2 if state != EMPTY_SQL_STATE else 1\n                )\n                await self.wait_for_sync()\n                self.last_state = state\n                self.state_reset_needs_commit = (\n                    dbv.needs_commit_after_state_sync())\n            try:\n                return await self._sql._parse_sql_extended_query(\n                    actions,\n                    fe_conn,\n                    dbver,\n                    dbv,\n                )\n            finally:\n                if not dbv.in_tx():\n                    self.last_state = dbv.serialize_state()\n                    self.state_reset_needs_commit = (\n                        dbv.needs_commit_after_state_sync())\n        finally:\n            await self.after_command()\n\n    def _write_error_position(\n        self,\n        msg_buf: WriteBuffer,\n        query: bytes,\n        pos_bytes: bytes,\n        source_map: Optional[pg_codegen.SourceMap],\n        offset: int = 0,\n    ):\n        if source_map:\n            pos = int(pos_bytes.decode('utf8'))\n            if offset > 0 or pos + offset > 0:\n                pos += offset\n            pos = source_map.translate(pos)\n            # pg uses 1-based indexes\n            pos += 1\n            pos_bytes = str(pos).encode('utf8')\n            msg_buf.write_byte(b'P') # Position\n        else:\n            msg_buf.write_byte(b'q')  # Internal query\n            msg_buf.write_bytestring(query)\n            msg_buf.write_byte(b'p')  # Internal position\n        msg_buf.write_bytestring(pos_bytes)\n\n    def load_last_ddl_return(self, object query_unit):\n        if query_unit.ddl_stmt_id:\n            data = self.last_indirect_return\n            if data:\n                ret = json.loads(data)\n                if ret['ddl_stmt_id'] != query_unit.ddl_stmt_id:\n                    raise RuntimeError(\n                        'unrecognized data notice after a DDL command: '\n                        'data_stmt_id do not match: expected '\n                        f'{query_unit.ddl_stmt_id!r}, got '\n                        f'{ret[\"ddl_stmt_id\"]!r}'\n                    )\n                return ret\n            else:\n                raise RuntimeError(\n                    'missing the required data notice after a DDL command'\n                )\n\n    async def _dump(self, block, output_queue, fragment_suggested_size):\n        cdef:\n            WriteBuffer buf\n            WriteBuffer qbuf\n            WriteBuffer out\n\n        qbuf = WriteBuffer.new_message(b'Q')\n        qbuf.write_bytestring(block.sql_copy_stmt)\n        qbuf.end_message()\n\n        self.write(qbuf)\n        self.waiting_for_sync += 1\n\n        er = None\n        out = None\n        i = 0\n        while True:\n            if not self.buffer.take_message():\n                await self.wait_for_message()\n            mtype = self.buffer.get_message_type()\n\n            if mtype == b'H':\n                # CopyOutResponse\n                self.buffer.discard_message()\n\n            elif mtype == b'd':\n                # CopyData\n                if out is None:\n                    out = WriteBuffer.new()\n\n                    if i == 0:\n                        # The first COPY IN message is prefixed with\n                        # `COPY_SIGNATURE` -- strip it.\n                        first = self.buffer.consume_message()\n                        if first[:len(COPY_SIGNATURE)] != COPY_SIGNATURE:\n                            raise RuntimeError('invalid COPY IN message')\n\n                        buf = WriteBuffer.new_message(b'd')\n                        buf.write_bytes(first[len(COPY_SIGNATURE) + 8:])\n                        buf.end_message()\n                        out.write_buffer(buf)\n\n                        if out._length >= fragment_suggested_size:\n                            await output_queue.put((block, i, out))\n                            i += 1\n                            out = None\n\n                        if (not self.buffer.take_message() or\n                                self.buffer.get_message_type() != b'd'):\n                            continue\n\n                self.buffer.redirect_messages(\n                    out, b'd', fragment_suggested_size)\n\n                if out._length >= fragment_suggested_size:\n                    self.transport.pause_reading()\n                    await output_queue.put((block, i, out))\n                    self.transport.resume_reading()\n                    i += 1\n                    out = None\n\n            elif mtype == b'c':\n                # CopyDone\n                self.buffer.discard_message()\n\n            elif mtype == b'C':\n                # CommandComplete\n                if out is not None:\n                    await output_queue.put((block, i, out))\n                self.buffer.discard_message()\n\n            elif mtype == b'E':\n                er = self.parse_error_message()\n\n            elif mtype == b'Z':\n                self.parse_sync_message()\n                break\n\n            else:\n                self.fallthrough()\n\n        if er is not None:\n            raise er[0](fields=er[1])\n\n    async def dump(self, input_queue, output_queue, fragment_suggested_size):\n        self.before_command()\n        try:\n            while True:\n                try:\n                    block = input_queue.pop()\n                except IndexError:\n                    await output_queue.put(None)\n                    return\n\n                await self._dump(block, output_queue, fragment_suggested_size)\n        finally:\n            # In case we errored while the transport was suspended.\n            self.transport.resume_reading()\n            await self.after_command()\n\n    async def _restore(self, restore_block, bytes data, dict type_map):\n        cdef:\n            WriteBuffer buf\n            WriteBuffer qbuf\n            WriteBuffer out\n\n            char* cbuf\n            ssize_t clen\n            ssize_t ncols\n\n        qbuf = WriteBuffer.new_message(b'Q')\n        qbuf.write_bytestring(restore_block.sql_copy_stmt)\n        qbuf.end_message()\n\n        self.write(qbuf)\n        self.waiting_for_sync += 1\n\n        er = None\n        while True:\n            if not self.buffer.take_message():\n                await self.wait_for_message()\n            mtype = self.buffer.get_message_type()\n\n            if mtype == b'G':\n                # CopyInResponse\n                self.buffer.read_byte()\n                ncols = self.buffer.read_int16()\n                self.buffer.discard_message()\n                break\n\n            elif mtype == b'E':\n                er = self.parse_error_message()\n\n            elif mtype == b'Z':\n                self.parse_sync_message()\n                break\n\n            else:\n                self.fallthrough()\n\n        if er is not None:\n            raise er[0](fields=er[1])\n\n        buf = WriteBuffer.new()\n        cpython.PyBytes_AsStringAndSize(data, &cbuf, &clen)\n        if (\n            restore_block.compat_elided_cols\n            or any(desc for desc in restore_block.data_mending_desc)\n        ):\n            self._rewrite_copy_data(\n                buf,\n                cbuf,\n                clen,\n                ncols,\n                restore_block.data_mending_desc,\n                type_map,\n                restore_block.compat_elided_cols,\n            )\n        else:\n            if cbuf[0] != b'd':\n                raise RuntimeError('unexpected dump data message structure')\n            ln = <uint32_t>hton.unpack_int32(cbuf + 1)\n            buf.write_byte(b'd')\n            buf.write_int32(ln + len(COPY_SIGNATURE) + 8)\n            buf.write_bytes(COPY_SIGNATURE)\n            buf.write_int32(0)\n            buf.write_int32(0)\n            buf.write_cstr(cbuf + 5, clen - 5)\n\n        self.write(buf)\n\n        qbuf = WriteBuffer.new_message(b'c')\n        qbuf.end_message()\n        self.write(qbuf)\n\n        while True:\n            if not self.buffer.take_message():\n                await self.wait_for_message()\n            mtype = self.buffer.get_message_type()\n\n            if mtype == b'C':\n                # CommandComplete\n                self.buffer.discard_message()\n\n            elif mtype == b'E':\n                er = self.parse_error_message()\n\n            elif mtype == b'Z':\n                self.parse_sync_message()\n                break\n\n        if er is not None:\n            raise er[0](fields=er[1])\n\n    cdef _rewrite_copy_data(\n        self,\n        WriteBuffer wbuf,\n        char* data,\n        ssize_t data_len,\n        ssize_t ncols,\n        tuple data_mending_desc,\n        dict type_id_map,\n        tuple elided_cols,\n    ):\n        \"\"\"Rewrite the binary COPY stream.\"\"\"\n        cdef:\n            FRBuffer rbuf\n            FRBuffer datum_buf\n            ssize_t i\n            ssize_t real_ncols\n            int8_t *elide\n            int8_t elided\n            int32_t datum_len\n            char copy_msg_byte\n            int16_t copy_msg_ncols\n            const char *datum\n            bint first = True\n            bint received_eof = False\n\n        real_ncols = ncols + len(elided_cols)\n        frb_init(&rbuf, data, data_len)\n\n        elide = <int8_t*>cpythonx.PyMem_Calloc(\n            <size_t>real_ncols, sizeof(int8_t))\n\n        try:\n            for col in elided_cols:\n                elide[col] = 1\n\n            mbuf = WriteBuffer.new()\n\n            while frb_get_len(&rbuf):\n                if received_eof:\n                    raise RuntimeError('received CopyData after EOF')\n                mbuf.start_message(b'd')\n\n                copy_msg_byte = frb_read(&rbuf, 1)[0]\n                if copy_msg_byte != b'd':\n                    raise RuntimeError(\n                        'unexpected dump data message structure')\n                frb_read(&rbuf, 4)\n\n                if first:\n                    mbuf.write_bytes(COPY_SIGNATURE)\n                    mbuf.write_int32(0)\n                    mbuf.write_int32(0)\n                    first = False\n\n                copy_msg_ncols = hton.unpack_int16(frb_read(&rbuf, 2))\n                if copy_msg_ncols == -1:\n                    # BINARY COPY EOF marker\n                    mbuf.write_int16(copy_msg_ncols)\n                    received_eof = True\n                    mbuf.end_message()\n                    wbuf.write_buffer(mbuf)\n                    mbuf.reset()\n                    continue\n                else:\n                    mbuf.write_int16(<int16_t>ncols)\n\n                # Tuple data\n                for i in range(real_ncols):\n                    datum_len = hton.unpack_int32(frb_read(&rbuf, 4))\n                    elided = elide[i]\n                    if not elided:\n                        mbuf.write_int32(datum_len)\n                    if datum_len != -1:\n                        datum = frb_read(&rbuf, datum_len)\n\n                        if not elided:\n                            datum_mending_desc = data_mending_desc[i]\n                            if (\n                                datum_mending_desc is not None\n                                and datum_mending_desc.needs_mending\n                            ):\n                                frb_init(&datum_buf, datum, datum_len)\n                                self._mend_copy_datum(\n                                    mbuf,\n                                    &datum_buf,\n                                    datum_mending_desc,\n                                    type_id_map,\n                                )\n                            else:\n                                mbuf.write_cstr(datum, datum_len)\n\n                mbuf.end_message()\n                wbuf.write_buffer(mbuf)\n                mbuf.reset()\n        finally:\n            cpython.PyMem_Free(elide)\n\n    cdef _mend_copy_datum(\n        self,\n        WriteBuffer wbuf,\n        FRBuffer *rbuf,\n        object mending_desc,\n        dict type_id_map,\n    ):\n        cdef:\n            ssize_t remainder\n            int32_t ndims\n            int32_t i\n            int32_t nelems\n            int32_t dim\n            const char *buf\n            FRBuffer elem_buf\n            int32_t elem_len\n            object elem_mending_desc\n\n        kind = mending_desc.schema_object_class\n\n        if kind is qltypes.SchemaObjectClass.ARRAY_TYPE:\n            # Dimensions and flags\n            buf = frb_read(rbuf, 8)\n            ndims = hton.unpack_int32(buf)\n            wbuf.write_cstr(buf, 8)\n            elem_mending_desc = mending_desc.elements[0]\n            # Discard the original element OID.\n            frb_read(rbuf, 4)\n            # Write the correct element OID.\n            elem_type_id = elem_mending_desc.schema_type_id\n            elem_type_oid = type_id_map[elem_type_id]\n            wbuf.write_int32(<int32_t>elem_type_oid)\n\n            if ndims == 0:\n                # Empty array\n                return\n\n            if ndims != 1:\n                raise ValueError(\n                    'unexpected non-single dimension array'\n                )\n\n            if mending_desc.needs_mending:\n                # dim and lbound\n                buf = frb_read(rbuf, 8)\n                nelems = hton.unpack_int32(buf)\n                wbuf.write_cstr(buf, 8)\n\n                for i in range(nelems):\n                    elem_len = hton.unpack_int32(frb_read(rbuf, 4))\n                    wbuf.write_int32(elem_len)\n                    frb_slice_from(&elem_buf, rbuf, elem_len)\n                    self._mend_copy_datum(\n                        wbuf,\n                        &elem_buf,\n                        mending_desc.elements[0],\n                        type_id_map,\n                    )\n\n        elif kind is qltypes.SchemaObjectClass.TUPLE_TYPE:\n            nelems = hton.unpack_int32(frb_read(rbuf, 4))\n            wbuf.write_int32(nelems)\n\n            for i in range(nelems):\n                elem_mending_desc = mending_desc.elements[i]\n                if elem_mending_desc is not None:\n                    # Discard the original element OID.\n                    frb_read(rbuf, 4)\n                    # Write the correct element OID.\n                    elem_type_id = elem_mending_desc.schema_type_id\n                    elem_type_oid = type_id_map[elem_type_id]\n                    wbuf.write_int32(<int32_t>elem_type_oid)\n\n                    elem_len = hton.unpack_int32(frb_read(rbuf, 4))\n                    wbuf.write_int32(elem_len)\n\n                    if elem_len != -1:\n                        frb_slice_from(&elem_buf, rbuf, elem_len)\n\n                        if elem_mending_desc.needs_mending:\n                            self._mend_copy_datum(\n                                wbuf,\n                                &elem_buf,\n                                elem_mending_desc,\n                                type_id_map,\n                            )\n                        else:\n                            wbuf.write_frbuf(&elem_buf)\n                else:\n                    buf = frb_read(rbuf, 8)\n                    wbuf.write_cstr(buf, 8)\n                    elem_len = hton.unpack_int32(buf + 4)\n                    if elem_len != -1:\n                        wbuf.write_cstr(frb_read(rbuf, elem_len), elem_len)\n\n        wbuf.write_frbuf(rbuf)\n\n    async def restore(self, restore_block, bytes data, dict type_map):\n        self.before_command()\n        try:\n            await self._restore(restore_block, data, type_map)\n        finally:\n            await self.after_command()\n\n    def is_healthy(self):\n        return (\n            self.connected and\n            self.idle and\n            self.cancel_fut is None and\n            not self.waiting_for_sync and\n            not self.in_tx()\n        )\n\n    cdef before_command(self):\n        if not self.connected:\n            raise RuntimeError(\n                'pgcon: cannot issue new command: not connected')\n\n        if self.waiting_for_sync:\n            raise RuntimeError(\n                'pgcon: cannot issue new command; waiting for sync')\n\n        if not self.idle:\n            raise RuntimeError(\n                'pgcon: cannot issue new command; '\n                'another command is in progress')\n\n        if self.cancel_fut is not None:\n            raise RuntimeError(\n                'pgcon: cannot start new command while cancelling the '\n                'previous one')\n\n        self.idle = False\n        self.last_indirect_return = None\n\n    async def after_command(self):\n        if self.idle:\n            raise RuntimeError('pgcon: idle while running a command')\n\n        if self.cancel_fut is not None:\n            await self.cancel_fut\n            self.cancel_fut = None\n            self.idle = True\n\n            # If we were cancelling a command in Postgres there can be a\n            # race between us calling `pg_cancel_backend()` and us receiving\n            # the results of the successfully executed command.  If this\n            # happens, we might get the *next command* cancelled. To minimize\n            # the chance of that we do another SYNC.\n            await self.sync()\n\n        else:\n            self.idle = True\n\n    cdef write(self, buf):\n        self.transport.write(memoryview(buf))\n\n    cdef fallthrough(self):\n        if self.parse_notification():\n            return\n\n        cdef:\n            char mtype = self.buffer.get_message_type()\n\n        # Process a sync, or else the state machine might hang\n        # forever... but still fail!\n        if mtype == b'Z':\n            self.parse_sync_message()\n\n        raise RuntimeError(\n            f'unexpected message type {chr(mtype)!r}')\n\n    cdef fallthrough_idle(self):\n        cdef char mtype\n\n        while self.buffer.take_message():\n            if self.parse_notification():\n                continue\n\n            mtype = self.buffer.get_message_type()\n            if mtype != b'E':  # ErrorResponse\n                raise RuntimeError(\n                    f'unexpected message type {chr(mtype)!r} '\n                    f'in IDLE state')\n\n            # We have an error message sent to us by the backend.\n            # It is not safe to assume that the connection\n            # is alive. We assume that it's dead and should be\n            # marked as \"closed\".\n\n            try:\n                er_cls, fields = self.parse_error_message()\n                self.aborted_with_error = er_cls(fields=fields)\n\n                pgcode = fields['C']\n                metrics.backend_connection_aborted.inc(\n                    1.0, self.get_tenant_label(), pgcode\n                )\n\n                if pgcode in POSTGRES_SHUTDOWN_ERR_CODES:\n                    pgreason = POSTGRES_SHUTDOWN_ERR_CODES[pgcode]\n                    pgmsg = fields.get('M', pgreason)\n\n                    logger.debug(\n                        'backend connection aborted with a shutdown '\n                        'error code %r(%s): %s',\n                        pgcode, pgreason, pgmsg\n                    )\n\n                    if self.is_system_db:\n                        self.tenant.set_pg_unavailable_msg(pgmsg)\n                        self.tenant.on_sys_pgcon_failover_signal()\n\n                else:\n                    pgmsg = fields.get('M', '<empty message>')\n                    logger.debug(\n                        'backend connection aborted with an '\n                        'error code %r: %s',\n                        pgcode, pgmsg\n                    )\n            finally:\n                self.abort()\n\n    cdef parse_notification(self):\n        cdef:\n            char mtype = self.buffer.get_message_type()\n\n        if mtype == b'S':\n            # ParameterStatus\n            name, value = self.parse_parameter_status_message()\n            if self.is_system_db:\n                self.tenant.on_sys_pgcon_parameter_status_updated(name, value)\n            self.parameter_status[name] = value\n            return True\n\n        elif mtype == b'A':\n            # NotificationResponse\n            self.buffer.read_int32()  # discard pid\n            channel = self.buffer.read_null_str().decode()\n            payload = self.buffer.read_null_str().decode()\n            self.buffer.finish_message()\n\n            if not self.is_system_db:\n                # The server is still initializing, or we're getting\n                # notification from a non-system-db connection.\n                return True\n\n            if channel == '__edgedb_sysevent__':\n                event_data = json.loads(payload)\n                event = event_data.get('event')\n\n                server_id = event_data.get('server_id')\n                if server_id == self.server._server_id:\n                    # We should only react to notifications sent\n                    # by other edgedb servers. Reacting to events\n                    # generated by this server must be implemented\n                    # at a different layer.\n                    return True\n\n                logger.debug(\"received system event: %s\", event)\n\n                event_payload = event_data.get('args')\n                if event == 'schema-changes':\n                    dbname = event_payload['dbname']\n                    self.tenant.on_remote_ddl(dbname)\n                elif event == 'database-config-changes':\n                    dbname = event_payload['dbname']\n                    self.tenant.on_remote_database_config_change(dbname)\n                elif event == 'system-config-changes':\n                    self.tenant.on_remote_system_config_change()\n                elif event == 'global-schema-changes':\n                    self.tenant.on_global_schema_change()\n                elif event == 'database-changes':\n                    self.tenant.on_remote_database_changes()\n                elif event == 'ensure-database-not-used':\n                    dbname = event_payload['dbname']\n                    self.tenant.on_remote_database_quarantine(dbname)\n                elif event == 'query-cache-changes':\n                    dbname = event_payload['dbname']\n                    to_add = event_payload.get('to_add')\n                    to_invalidate = event_payload.get('to_invalidate')\n                    self.tenant.on_remote_query_cache_change(\n                        dbname, to_add=to_add, to_invalidate=to_invalidate\n                    )\n                else:\n                    raise AssertionError(f'unexpected system event: {event!r}')\n\n            return True\n\n        elif mtype == b'N':\n            # NoticeResponse\n            _, fields = self.parse_error_message()\n            severity = fields.get('V')\n            message = fields.get('M')\n            detail = fields.get('D')\n            if (\n                severity == \"NOTICE\"\n                and message.startswith(\"edb:notice:indirect_return\")\n            ):\n                self.last_indirect_return = detail\n            elif self.log_listeners:\n                for listener in self.log_listeners:\n                    self.loop.call_soon(listener, severity, message)\n            return True\n\n        return False\n\n    cdef parse_error_message(self):\n        cdef:\n            char code\n            str value\n            dict fields = {}\n            object err_cls\n\n        while True:\n            code = self.buffer.read_byte()\n            if code == 0:\n                break\n            value = self.buffer.read_null_str().decode()\n            fields[chr(code)] = value\n\n        self.buffer.finish_message()\n\n        err_cls = pgerror.get_error_class(fields)\n        if self.debug:\n            self.debug_print('ERROR', err_cls.__name__, fields)\n\n        return err_cls, fields\n\n    cdef char parse_sync_message(self):\n        cdef char status\n\n        if not self.waiting_for_sync:\n            raise RuntimeError('unexpected sync')\n        self.waiting_for_sync -= 1\n\n        assert self.buffer.get_message_type() == b'Z'\n\n        status = self.buffer.read_byte()\n\n        if status == b'I':\n            self.xact_status = PQTRANS_IDLE\n        elif status == b'T':\n            self.xact_status = PQTRANS_INTRANS\n        elif status == b'E':\n            self.xact_status = PQTRANS_INERROR\n        else:\n            self.xact_status = PQTRANS_UNKNOWN\n\n        if self.debug:\n            self.debug_print('SYNC MSG', self.xact_status, chr(status))\n\n        self.buffer.finish_message()\n        return status\n\n    cdef parse_parameter_status_message(self):\n        cdef:\n            str name\n            str value\n        assert self.buffer.get_message_type() == b'S'\n        name = self.buffer.read_null_str().decode()\n        value = self.buffer.read_null_str().decode()\n        self.buffer.finish_message()\n        if self.debug:\n            self.debug_print('PARAMETER STATUS MSG', name, value)\n        return name, value\n\n    cdef make_clean_stmt_message(self, bytes stmt_name):\n        cdef WriteBuffer buf\n        buf = WriteBuffer.new_message(b'C')\n        buf.write_byte(b'S')\n        buf.write_bytestring(stmt_name)\n        return buf.end_message()\n\n    async def wait_for_message(self):\n        if self.buffer.take_message():\n            return\n        if self.transport is None:\n            raise ConnectionAbortedError()\n        self.msg_waiter = self.loop.create_future()\n        await self.msg_waiter\n\n    def connection_made(self, transport):\n        if self.transport is not None:\n            raise RuntimeError('connection_made: invalid connection status')\n        self.transport = transport\n        self.connected = True\n        self.connected_fut.set_result(True)\n        self.connected_fut = None\n\n    def connection_lost(self, exc):\n        # Mark the connection as disconnected, so that `self.is_healthy()`\n        # surely returns False for this connection.\n        self.connected = False\n\n        self.transport = None\n\n        if self.pinned_by is not None:\n            pinned_by = self.pinned_by\n            self.pinned_by = None\n            pinned_by.on_aborted_pgcon(self)\n\n        if self.is_system_db:\n            self.tenant.on_sys_pgcon_connection_lost(exc)\n        elif self.tenant is not None:\n            if not self.close_requested:\n                self.tenant.on_pgcon_broken()\n            else:\n                self.tenant.on_pgcon_lost()\n\n        if self.connected_fut is not None and not self.connected_fut.done():\n            self.connected_fut.set_exception(ConnectionAbortedError())\n            return\n\n        if self.msg_waiter is not None and not self.msg_waiter.done():\n            self.msg_waiter.set_exception(ConnectionAbortedError())\n            self.msg_waiter = None\n\n    def pause_writing(self):\n        pass\n\n    def resume_writing(self):\n        pass\n\n    def data_received(self, data):\n        self.buffer.feed_data(data)\n\n        if self.connected and self.idle:\n            assert self.msg_waiter is None\n            self.fallthrough_idle()\n\n        elif (self.msg_waiter is not None and\n                self.buffer.take_message() and\n                not self.msg_waiter.cancelled()):\n            self.msg_waiter.set_result(True)\n            self.msg_waiter = None\n\n    def eof_received(self):\n        pass\n\n\n# Underscored name for _SYNC_MESSAGE because it should always be emitted\n# using write_sync(), which properly counts them\ncdef bytes _SYNC_MESSAGE = bytes(WriteBuffer.new_message(b'S').end_message())\ncdef bytes FLUSH_MESSAGE = bytes(WriteBuffer.new_message(b'H').end_message())\n\ncdef EdegDBCodecContext DEFAULT_CODEC_CONTEXT = EdegDBCodecContext()\n\ncdef inline int16_t read_int16(data: bytes):\n    return int.from_bytes(data[0:2], \"big\", signed=True)\n\ncdef inline int32_t read_int32(data: bytes):\n    return int.from_bytes(data[0:4], \"big\", signed=True)\n"
  },
  {
    "path": "edb/server/pgcon/pgcon_sql.pxd",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom edb.server.pgproto.pgproto cimport (\n    WriteBuffer,\n    ReadBuffer,\n    FRBuffer,\n)\n\ncdef enum PGAction:\n    START_IMPLICIT_TX = 0\n    PARSE = 1\n    BIND = 2\n    DESCRIBE_STMT = 3\n    DESCRIBE_STMT_ROWS = 4\n    DESCRIBE_PORTAL = 5\n    EXECUTE = 6\n    CLOSE_STMT = 7\n    CLOSE_PORTAL = 8\n    FLUSH = 9\n    SYNC = 10\n\n\ncdef class PGMessage:\n    cdef:\n        PGAction action\n        bytes stmt_name\n        bytes portal_name\n        str orig_portal_name\n        object args\n        object query_unit\n        bint frontend_only\n        bint valid\n        bint injected\n\n        object orig_query\n        object fe_settings\n\n    cdef inline bint is_frontend_only(self)\n    cdef inline bint is_valid(self)\n    cdef inline bint is_injected(self)\n\n\ncdef class PGSQLConnection:\n    cdef:\n        PGConnection con\n\n    cdef _rewrite_sql_error_response(self, PGMessage action, WriteBuffer buf)\n"
  },
  {
    "path": "edb/server/pgcon/pgcon_sql.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\ncdef class PGMessage:\n    def __init__(\n        self,\n        PGAction action,\n        bytes stmt_name=None,\n        str portal_name=None,\n        args=None,\n        query_unit=None,\n        fe_settings=None,\n        injected=False,\n        bytes force_portal_name=None,\n    ):\n        self.action = action\n        self.stmt_name = stmt_name\n        self.orig_portal_name = portal_name\n        if force_portal_name is not None:\n            self.portal_name = force_portal_name\n        elif portal_name:\n            self.portal_name = b'u' + portal_name.encode(\"utf-8\")\n        else:\n            self.portal_name = b''\n        self.args = args\n        self.query_unit = query_unit\n\n        self.fe_settings = fe_settings\n        self.valid = True\n        self.injected = injected\n        if self.query_unit is not None:\n            self.frontend_only = self.query_unit.frontend_only\n        else:\n            self.frontend_only = False\n\n    cdef inline bint is_frontend_only(self):\n        return self.frontend_only\n\n    def invalidate(self):\n        self.valid = False\n\n    cdef inline bint is_valid(self):\n        return self.valid\n\n    cdef inline bint is_injected(self):\n        return self.injected\n\n    def as_injected(self) -> PGMessage:\n        return PGMessage(\n            action=self.action,\n            stmt_name=self.stmt_name,\n            portal_name=self.orig_portal_name,\n            args=self.args,\n            query_unit=self.query_unit,\n            fe_settings=self.fe_settings,\n            injected=True,\n        )\n\n    def __repr__(self):\n        rv = []\n        if self.action == PGAction.START_IMPLICIT_TX:\n            rv.append(\"START_IMPLICIT_TX\")\n        elif self.action == PGAction.PARSE:\n            rv.append(\"PARSE\")\n        elif self.action == PGAction.BIND:\n            rv.append(\"BIND\")\n        elif self.action == PGAction.DESCRIBE_STMT:\n            rv.append(\"DESCRIBE_STMT\")\n        elif self.action == PGAction.DESCRIBE_STMT_ROWS:\n            rv.append(\"DESCRIBE_STMT_ROWS\")\n        elif self.action == PGAction.DESCRIBE_PORTAL:\n            rv.append(\"DESCRIBE_PORTAL\")\n        elif self.action == PGAction.EXECUTE:\n            rv.append(\"EXECUTE\")\n        elif self.action == PGAction.CLOSE_STMT:\n            rv.append(\"CLOSE_STMT\")\n        elif self.action == PGAction.CLOSE_PORTAL:\n            rv.append(\"CLOSE_PORTAL\")\n        elif self.action == PGAction.FLUSH:\n            rv.append(\"FLUSH\")\n        elif self.action == PGAction.SYNC:\n            rv.append(\"SYNC\")\n        if self.stmt_name is not None:\n            rv.append(f\"stmt_name={self.stmt_name}\")\n        if self.orig_portal_name is not None:\n            rv.append(f\"portal_name={self.orig_portal_name!r}\")\n        if self.args is not None:\n            rv.append(f\"args={self.args}\")\n        rv.append(f\"frontend_only={self.is_frontend_only()}\")\n        rv.append(f\"injected={self.is_injected()}\")\n        if self.query_unit is not None:\n            rv.append(f\"query_unit={self.query_unit}\")\n        if len(rv) > 1:\n            rv.insert(1, \":\")\n        return \" \".join(rv)\n\ncdef class PGSQLConnection:\n    def __init__(self, con):\n        self.con = con\n\n    def _write_sql_extended_query(\n        self,\n        actions,\n        dbver: int,\n        dbv: pg_ext.ConnectionView,\n    ) -> bytes:\n        cdef:\n            WriteBuffer buf, msg_buf\n            PGMessage action\n            bint be_parse\n\n        buf = WriteBuffer.new()\n        state = None\n        if not dbv.in_tx():\n            state = dbv.serialize_state()\n            self.con._build_apply_sql_state_req(state, buf)\n            # We need to close the implicit transaction with a SYNC here\n            # because the next command may be e.g. \"BEGIN DEFERRABLE\".\n            self.con.write_sync(buf)\n        prepared = set()\n        for action in actions:\n            if action.is_frontend_only():\n                continue\n\n            be_parse = True\n            if action.action == PGAction.PARSE:\n                sql_text, data = action.args[:2]\n                if action.stmt_name in prepared:\n                    action.frontend_only = True\n                else:\n                    if action.stmt_name:\n                        be_parse = self.con.before_prepare(\n                            action.stmt_name, dbver, buf\n                        )\n                    if not be_parse:\n                        if self.con.debug:\n                            self.con.debug_print(\n                                'Parse cache hit', action.stmt_name, sql_text)\n                        action.frontend_only = True\n                if not action.is_frontend_only():\n                    prepared.add(action.stmt_name)\n                    msg_buf = WriteBuffer.new_message(b'P')\n                    msg_buf.write_bytestring(action.stmt_name)\n                    msg_buf.write_bytestring(sql_text)\n                    msg_buf.write_bytes(data)\n                    buf.write_buffer(msg_buf.end_message())\n                    metrics.query_size.observe(\n                        len(sql_text), self.con.get_tenant_label(), 'compiled'\n                    )\n                    if self.con.debug:\n                        self.con.debug_print(\n                            'Parse', action.stmt_name, sql_text, data\n                        )\n\n            elif action.action == PGAction.BIND:\n                if action.query_unit is not None and action.query_unit.prepare:\n                    be_stmt_name = action.query_unit.prepare.be_stmt_name\n                    if be_stmt_name in prepared:\n                        action.frontend_only = True\n                    else:\n                        if be_stmt_name:\n                            be_parse = self.con.before_prepare(\n                                be_stmt_name, dbver, buf\n                            )\n                        if not be_parse:\n                            if self.con.debug:\n                                self.con.debug_print(\n                                    'Parse cache hit', be_stmt_name)\n                            action.frontend_only = True\n                            prepared.add(be_stmt_name)\n\n                if action.is_frontend_only():\n                    pass\n                elif action.query_unit is not None and isinstance(\n                    action.query_unit.command_complete_tag, dbstate.TagUnpackRow\n                ):\n                    # in this case we are intercepting the only result row so\n                    # we want to set its encoding to be binary\n                    msg_buf = WriteBuffer.new_message(b'B')\n                    msg_buf.write_bytestring(action.portal_name)\n                    msg_buf.write_bytestring(action.stmt_name)\n\n                    # skim over param format codes\n                    param_formats = read_int16(action.args[0:2])\n                    offset = 2 + param_formats * 2\n\n                    # skim over param values\n                    params = read_int16(action.args[offset:offset+2])\n                    offset += 2\n                    for p in range(params):\n                        size = read_int32(action.args[offset:offset+4])\n                        if size == -1:  # special case: NULL\n                            size = 0\n                        offset += 4 + size\n                    msg_buf.write_bytes(action.args[0:offset])\n\n                    # set the result formats\n                    msg_buf.write_int16(1)  # number of columns\n                    msg_buf.write_int16(1)  # binary encoding\n                    buf.write_buffer(msg_buf.end_message())\n                else:\n                    msg_buf = WriteBuffer.new_message(b'B')\n                    msg_buf.write_bytestring(action.portal_name)\n                    msg_buf.write_bytestring(action.stmt_name)\n                    msg_buf.write_bytes(action.args)\n                    buf.write_buffer(msg_buf.end_message())\n\n            elif (\n                action.action\n                in (PGAction.DESCRIBE_STMT, PGAction.DESCRIBE_STMT_ROWS)\n            ):\n                msg_buf = WriteBuffer.new_message(b'D')\n                msg_buf.write_byte(b'S')\n                msg_buf.write_bytestring(action.stmt_name)\n                buf.write_buffer(msg_buf.end_message())\n\n            elif action.action == PGAction.DESCRIBE_PORTAL:\n                msg_buf = WriteBuffer.new_message(b'D')\n                msg_buf.write_byte(b'P')\n                msg_buf.write_bytestring(action.portal_name)\n                buf.write_buffer(msg_buf.end_message())\n\n            elif action.action == PGAction.EXECUTE:\n                if action.query_unit is not None and action.query_unit.prepare:\n                    be_stmt_name = action.query_unit.prepare.be_stmt_name\n\n                    if be_stmt_name in prepared:\n                        action.frontend_only = True\n                    else:\n                        if be_stmt_name:\n                            be_parse = self.con.before_prepare(\n                                be_stmt_name, dbver, buf\n                            )\n                        if not be_parse:\n                            if self.con.debug:\n                                self.con.debug_print(\n                                    'Parse cache hit', be_stmt_name)\n                            action.frontend_only = True\n                            prepared.add(be_stmt_name)\n\n                if (\n                    action.query_unit is not None\n                    and action.query_unit.deallocate is not None\n                    and self.con.before_prepare(\n                        action.query_unit.deallocate.be_stmt_name, dbver, buf\n                    )\n                ):\n                    # This prepared statement does not actually exist\n                    # on this connection, so there's nothing to DEALLOCATE.\n                    action.frontend_only = True\n\n                if action.is_frontend_only():\n                    pass\n                elif action.query_unit is not None and isinstance(\n                    action.query_unit.command_complete_tag,\n                    (dbstate.TagCountMessages, dbstate.TagUnpackRow),\n                ):\n                    # when executing TagUnpackRow, don't pass the limit through\n                    msg_buf = WriteBuffer.new_message(b'E')\n                    msg_buf.write_bytestring(action.portal_name)\n                    msg_buf.write_int32(0)\n                    buf.write_buffer(msg_buf.end_message())\n                else:\n                    # base case\n                    msg_buf = WriteBuffer.new_message(b'E')\n                    msg_buf.write_bytestring(action.portal_name)\n                    msg_buf.write_int32(action.args)\n                    buf.write_buffer(msg_buf.end_message())\n\n            elif action.action == PGAction.CLOSE_PORTAL:\n                if action.query_unit is not None and action.query_unit.prepare:\n                    be_stmt_name = action.query_unit.prepare.be_stmt_name\n                    if be_stmt_name in prepared:\n                        action.frontend_only = True\n\n                if not action.is_frontend_only():\n                    msg_buf = WriteBuffer.new_message(b'C')\n                    msg_buf.write_byte(b'P')\n                    msg_buf.write_bytestring(action.portal_name)\n                    buf.write_buffer(msg_buf.end_message())\n\n            elif action.action == PGAction.CLOSE_STMT:\n                if action.query_unit is not None and action.query_unit.prepare:\n                    be_stmt_name = action.query_unit.prepare.be_stmt_name\n                    if be_stmt_name in prepared:\n                        action.frontend_only = True\n\n                if not action.is_frontend_only():\n                    msg_buf = WriteBuffer.new_message(b'C')\n                    msg_buf.write_byte(b'S')\n                    msg_buf.write_bytestring(action.stmt_name)\n                    buf.write_buffer(msg_buf.end_message())\n\n            elif action.action == PGAction.FLUSH:\n                msg_buf = WriteBuffer.new_message(b'H')\n                buf.write_buffer(msg_buf.end_message())\n\n            elif action.action == PGAction.SYNC:\n                self.con.write_sync(buf)\n\n        if action.action not in (PGAction.SYNC, PGAction.FLUSH):\n            # Make sure _parse_sql_extended_query() complete by sending a FLUSH\n            # to the backend, but we won't flush the results to the frontend\n            # because it's not requested.\n            msg_buf = WriteBuffer.new_message(b'H')\n            buf.write_buffer(msg_buf.end_message())\n\n        self.con.write(buf)\n        return state\n\n    async def _parse_sql_extended_query(\n        self,\n        actions,\n        fe_conn: frontend.AbstractFrontendConnection,\n        dbver: int,\n        dbv: pg_ext.ConnectionView,\n    ) -> tuple[bool, bool]:\n        cdef:\n            WriteBuffer buf, msg_buf\n            PGMessage action\n            bint ignore_till_sync = False\n            int32_t row_count\n\n        buf = WriteBuffer.new()\n        rv = True\n\n        for action in actions:\n            if self.con.debug:\n                self.con.debug_print(\n                    'ACTION', action, 'ignore_till_sync=', ignore_till_sync\n                )\n\n            if ignore_till_sync and action.action != PGAction.SYNC:\n                continue\n            elif action.action == PGAction.FLUSH:\n                if buf.len() > 0:\n                    fe_conn.write(buf)\n                    fe_conn.flush()\n                    buf = WriteBuffer.new()\n                continue\n            elif action.action == PGAction.START_IMPLICIT_TX:\n                dbv.start_implicit()\n                continue\n            elif action.is_frontend_only():\n                if action.action == PGAction.PARSE:\n                    if not action.is_injected():\n                        msg_buf = WriteBuffer.new_message(b'1')\n                        buf.write_buffer(msg_buf.end_message())\n                elif action.action == PGAction.BIND:\n                    dbv.create_portal(\n                        action.orig_portal_name, action.query_unit\n                    )\n                    if not action.is_injected():\n                        msg_buf = WriteBuffer.new_message(b'2')  # BindComplete\n                        buf.write_buffer(msg_buf.end_message())\n                elif action.action == PGAction.DESCRIBE_STMT:\n                    # ParameterDescription\n                    if not action.is_injected():\n                        msg_buf = WriteBuffer.new_message(b't')\n                        msg_buf.write_int16(0)  # number of parameters\n                        buf.write_buffer(msg_buf.end_message())\n                elif action.action == PGAction.EXECUTE:\n                    if action.query_unit.set_vars is not None:\n                        assert len(action.query_unit.set_vars) == 1\n                        # CommandComplete\n                        msg_buf = WriteBuffer.new_message(b'C')\n                        if next(\n                            iter(action.query_unit.set_vars.values())\n                        ) is None:\n                            msg_buf.write_bytestring(b'RESET')\n                        else:\n                            msg_buf.write_bytestring(b'SET')\n                        buf.write_buffer(msg_buf.end_message())\n                    elif not action.is_injected():\n                        # NoData\n                        msg_buf = WriteBuffer.new_message(b'n')\n                        buf.write_buffer(msg_buf.end_message())\n                        # CommandComplete\n                        msg_buf = WriteBuffer.new_message(b'C')\n                        assert isinstance(\n                            action.query_unit.command_complete_tag,\n                            dbstate.TagPlain,\n                        ), \"emulated SQL unit has no command_tag\"\n                        plain = action.query_unit.command_complete_tag\n                        msg_buf.write_bytestring(plain.tag)\n                        buf.write_buffer(msg_buf.end_message())\n\n                    dbv.on_success(action.query_unit)\n                    fe_conn.on_success(action.query_unit)\n                elif action.action == PGAction.CLOSE_PORTAL:\n                    dbv.close_portal_if_exists(action.orig_portal_name)\n                    if not action.is_injected():\n                        msg_buf = WriteBuffer.new_message(b'3') # CloseComplete\n                        buf.write_buffer(msg_buf.end_message())\n                elif action.action == PGAction.CLOSE_STMT:\n                    if not action.is_injected():\n                        msg_buf = WriteBuffer.new_message(b'3')  # CloseComplete\n                        buf.write_buffer(msg_buf.end_message())\n                if (\n                    action.action == PGAction.DESCRIBE_STMT or\n                    action.action == PGAction.DESCRIBE_PORTAL\n                ):\n                    if action.query_unit.set_vars is not None:\n                        msg_buf = WriteBuffer.new_message(b'n')  # NoData\n                        buf.write_buffer(msg_buf.end_message())\n                continue\n\n            row_count = 0\n            while True:\n                if not self.con.buffer.take_message():\n                    if buf.len() > 0:\n                        fe_conn.write(buf)\n                        fe_conn.flush()\n                        buf = WriteBuffer.new()\n                    await self.con.wait_for_message()\n\n                mtype = self.con.buffer.get_message_type()\n                if self.con.debug:\n                    self.con.debug_print(f'recv backend message: {chr(mtype)!r}')\n                    if ignore_till_sync:\n                        self.con.debug_print(\"ignoring until SYNC\")\n\n                if ignore_till_sync and mtype != b'Z':\n                    self.con.buffer.discard_message()\n                    continue\n\n                if (\n                    mtype == b'3'\n                    and action.action != PGAction.CLOSE_PORTAL\n                    and action.action != PGAction.CLOSE_STMT\n                ):\n                    # before_prepare() initiates LRU cleanup for\n                    # prepared statements, so CloseComplete may\n                    # appear here.\n                    self.con.buffer.discard_message()\n                    continue\n\n                # ParseComplete\n                if mtype == b'1' and action.action == PGAction.PARSE:\n                    self.con.buffer.finish_message()\n                    if self.con.debug:\n                        self.con.debug_print('PARSE COMPLETE MSG')\n                    if action.stmt_name:\n                        self.con.prep_stmts[action.stmt_name] = dbver\n                    if not action.is_injected():\n                        msg_buf = WriteBuffer.new_message(mtype)\n                        buf.write_buffer(msg_buf.end_message())\n                    break\n\n                # BindComplete\n                elif mtype == b'2' and action.action == PGAction.BIND:\n                    self.con.buffer.finish_message()\n                    if self.con.debug:\n                        self.con.debug_print('BIND COMPLETE MSG')\n                    if action.query_unit is not None:\n                        dbv.create_portal(\n                            action.orig_portal_name, action.query_unit\n                        )\n                    if not action.is_injected():\n                        msg_buf = WriteBuffer.new_message(mtype)\n                        buf.write_buffer(msg_buf.end_message())\n                    break\n\n                elif (\n                    # RowDescription or NoData\n                    mtype == b'T' or mtype == b'n'\n                ) and (\n                    action.action == PGAction.DESCRIBE_STMT or\n                    action.action == PGAction.DESCRIBE_STMT_ROWS or\n                    action.action == PGAction.DESCRIBE_PORTAL\n                ):\n                    data = self.con.buffer.consume_message()\n                    if self.con.debug:\n                        self.con.debug_print('END OF DESCRIBE', mtype)\n                    if (\n                        mtype == b'T'\n                        and action.query_unit is not None\n                        and isinstance(\n                            action.query_unit.command_complete_tag,\n                            dbstate.TagUnpackRow,\n                        )\n                    ):\n                        # TagUnpackRow converts RowDescription into NoData\n                        msg_buf = WriteBuffer.new_message(b'n')\n                        buf.write_buffer(msg_buf.end_message())\n\n                    elif not action.is_injected() and not (\n                        mtype == b'n' and\n                        action.action == PGAction.DESCRIBE_STMT_ROWS\n                    ):\n                        msg_buf = WriteBuffer.new_message(mtype)\n                        msg_buf.write_bytes(data)\n                        buf.write_buffer(msg_buf.end_message())\n                    break\n\n                elif (\n                    mtype == b't'  # ParameterDescription\n                    and action.action == PGAction.DESCRIBE_STMT_ROWS\n                ):\n                    self.con.buffer.consume_message()\n\n                elif (\n                    mtype == b't'  # ParameterDescription\n                ):\n                    # remap parameter descriptions\n\n                    # The \"external\" parameters (that are visible to the user)\n                    # don't include the internal params for globals and\n                    # extracted constants.\n                    # This chunk of code remaps the descriptions of internal\n                    # params into external ones.\n                    self.con.buffer.read_int16()  # count_internal\n                    data_internal = self.con.buffer.consume_message()\n\n                    msg_buf = WriteBuffer.new_message(b't')\n                    external_params: int64_t = 0\n                    if (\n                        action.query_unit is not None\n                        and action.query_unit.params\n                    ):\n                        for index, param in enumerate(action.query_unit.params):\n                            if not isinstance(param, dbstate.SQLParamExternal):\n                                break\n                            external_params = index + 1\n\n                    msg_buf.write_int16(external_params)\n                    msg_buf.write_bytes(data_internal[0:external_params * 4])\n\n                    buf.write_buffer(msg_buf.end_message())\n\n                elif (\n                    mtype == b'T'  # RowDescription\n                    and action.action == PGAction.EXECUTE\n                    and action.query_unit is not None\n                    and isinstance(\n                        action.query_unit.command_complete_tag,\n                        dbstate.TagUnpackRow,\n                    )\n                ):\n                    data = self.con.buffer.consume_message()\n\n                    # tell the frontend connection that there is NoData\n                    # because we intercept and unpack the DataRow.\n                    msg_buf = WriteBuffer.new_message(b'n')\n                    buf.write_buffer(msg_buf.end_message())\n                elif (\n                    mtype == b'D'  # DataRow\n                    and action.action == PGAction.EXECUTE\n                    and action.query_unit is not None\n                    and isinstance(\n                        action.query_unit.command_complete_tag,\n                        dbstate.TagUnpackRow,\n                    )\n                ):\n                    # unpack a single row with a single column\n                    data = self.con.buffer.consume_message()\n\n                    field_size = read_int32(data[2:6])\n                    val_bytes = data[6:6 + field_size]\n\n                    row_count = int.from_bytes(val_bytes, \"big\", signed=True)\n                elif (\n                    # CommandComplete, EmptyQueryResponse, PortalSuspended\n                    mtype == b'C' or mtype == b'I' or mtype == b's'\n                ) and action.action == PGAction.EXECUTE:\n                    data = self.con.buffer.consume_message()\n                    if self.con.debug:\n                        self.con.debug_print('END OF EXECUTE', mtype)\n                    if action.query_unit is not None:\n                        fe_conn.on_success(action.query_unit)\n                        dbv.on_success(action.query_unit)\n\n                        if action.query_unit.prepare is not None:\n                            be_stmt_name = action.query_unit.prepare.be_stmt_name\n                            if be_stmt_name:\n                                if self.con.debug:\n                                    self.con.debug_print(\n                                        f\"remembering ps {be_stmt_name}, \"\n                                        f\"dbver {dbver}\"\n                                    )\n                                self.con.prep_stmts[be_stmt_name] = dbver\n\n                    if (\n                        not action.is_injected()\n                        and action.query_unit is not None\n                        and action.query_unit.command_complete_tag\n                    ):\n                        tag = action.query_unit.command_complete_tag\n\n                        msg_buf = WriteBuffer.new_message(mtype)\n                        if isinstance(tag, dbstate.TagPlain):\n                            msg_buf.write_bytestring(tag.tag)\n\n                        elif isinstance(tag, (dbstate.TagCountMessages, dbstate.TagUnpackRow)):\n                            msg_buf.write_bytes(bytes(tag.prefix, \"utf-8\"))\n\n                            # This should return the number of modified rows by\n                            # the top-level query, but we are returning the\n                            # count of rows in the response. These two will\n                            # always match because our compiled DML with always\n                            # have a top-level SELECT with same number of rows\n                            # as the DML stmt somewhere in the the CTEs.\n                            msg_buf.write_str(str(row_count), \"utf-8\")\n\n                        buf.write_buffer(msg_buf.end_message())\n\n                    elif not action.is_injected():\n                        msg_buf = WriteBuffer.new_message(mtype)\n                        msg_buf.write_bytes(data)\n                        buf.write_buffer(msg_buf.end_message())\n                    break\n\n                # CloseComplete\n                elif mtype == b'3' and action.action == PGAction.CLOSE_PORTAL:\n                    self.con.buffer.finish_message()\n                    if self.con.debug:\n                        self.con.debug_print('CLOSE COMPLETE MSG (PORTAL)')\n                    dbv.close_portal_if_exists(action.orig_portal_name)\n                    if not action.is_injected():\n                        msg_buf = WriteBuffer.new_message(mtype)\n                        buf.write_buffer(msg_buf.end_message())\n                    break\n\n                elif mtype == b'3' and action.action == PGAction.CLOSE_STMT:\n                    self.con.buffer.finish_message()\n                    if self.con.debug:\n                        self.con.debug_print('CLOSE COMPLETE MSG (STATEMENT)')\n                    if not action.is_injected():\n                        msg_buf = WriteBuffer.new_message(mtype)\n                        buf.write_buffer(msg_buf.end_message())\n                    break\n\n                elif mtype == b'E':  # ErrorResponse\n                    rv = False\n                    if self.con.debug:\n                        self.con.debug_print('ERROR RESPONSE MSG')\n                    if action.query_unit is not None:\n                        fe_conn.on_error(action.query_unit)\n                    dbv.on_error()\n                    self._rewrite_sql_error_response(action, buf)\n                    fe_conn.write(buf)\n                    fe_conn.flush()\n                    buf = WriteBuffer.new()\n                    ignore_till_sync = True\n                    break\n\n                elif mtype == b'Z':  # ReadyForQuery\n                    ignore_till_sync = False\n                    dbv.end_implicit()\n                    status = self.con.parse_sync_message()\n                    msg_buf = WriteBuffer.new_message(b'Z')\n                    msg_buf.write_byte(status)\n                    buf.write_buffer(msg_buf.end_message())\n\n                    fe_conn.write(buf)\n                    fe_conn.flush()\n                    return True, True\n\n                else:\n                    if not action.is_injected():\n                        if self.con.debug:\n                            self.con.debug_print('REDIRECT OTHER MSG', mtype)\n                        messages_redirected = self.con.buffer.redirect_messages(\n                            buf, mtype, 0\n                        )\n\n                        # DataRow\n                        if mtype == b'D':\n                            row_count += messages_redirected\n                    else:\n                        logger.warning(\n                            f\"discarding unexpected backend message: \"\n                            f\"{chr(mtype)!r}\"\n                        )\n                        self.con.buffer.discard_message()\n\n        if buf.len() > 0:\n            fe_conn.write(buf)\n        return rv, False\n\n    cdef _rewrite_sql_error_response(self, PGMessage action, WriteBuffer buf):\n        cdef WriteBuffer msg_buf\n\n        if action.action == PGAction.PARSE:\n            msg_buf = WriteBuffer.new_message(b'E')\n            while True:\n                field_type = self.con.buffer.read_byte()\n                if field_type == b'P':  # Position\n                    if action.query_unit is None:\n                        source_map = None\n                        offset = 0\n                    else:\n                        qu = action.query_unit\n                        source_map = qu.source_map\n                        offset = -qu.prefix_len\n                    self.con._write_error_position(\n                        msg_buf,\n                        action.args[0],\n                        self.con.buffer.read_null_str(),\n                        source_map,\n                        offset,\n                    )\n                    continue\n                else:\n                    msg_buf.write_byte(field_type)\n                    if field_type == b'\\0':\n                        break\n                msg_buf.write_bytestring(\n                    self.con.buffer.read_null_str()\n                )\n            self.con.buffer.finish_message()\n            buf.write_buffer(msg_buf.end_message())\n        elif action.action in (\n            PGAction.BIND,\n            PGAction.EXECUTE,\n            PGAction.DESCRIBE_PORTAL,\n            PGAction.CLOSE_PORTAL,\n        ):\n            portal_name = action.orig_portal_name\n            msg_buf = WriteBuffer.new_message(b'E')\n            message = None\n            while True:\n                field_type = self.con.buffer.read_byte()\n                if field_type == b'C':  # Code\n                    msg_buf.write_byte(field_type)\n                    code = self.con.buffer.read_null_str()\n                    msg_buf.write_bytestring(code)\n                    if code == b'34000':\n                        message = f'cursor \"{portal_name}\" does not exist'\n                    elif code == b'42P03':\n                        message = f'cursor \"{portal_name}\" already exists'\n                elif field_type == b'M' and message:\n                    msg_buf.write_byte(field_type)\n                    msg_buf.write_bytestring(\n                        message.encode('utf-8')\n                    )\n                elif field_type == b'P':\n                    if action.query_unit is not None:\n                        qu = action.query_unit\n                        query_text = qu.query.encode(\"utf-8\")\n                        if qu.prepare is not None:\n                            offset = -55\n                            source_map = qu.prepare.source_map\n                        else:\n                            offset = 0\n                            source_map = qu.source_map\n                        offset -= qu.prefix_len\n                    else:\n                        query_text = b\"\"\n                        source_map = None\n                        offset = 0\n\n                    self.con._write_error_position(\n                        msg_buf,\n                        query_text,\n                        self.con.buffer.read_null_str(),\n                        source_map,\n                        offset,\n                    )\n                else:\n                    msg_buf.write_byte(field_type)\n                    if field_type == b'\\0':\n                        break\n                    msg_buf.write_bytestring(\n                        self.con.buffer.read_null_str()\n                    )\n            self.con.buffer.finish_message()\n            buf.write_buffer(msg_buf.end_message())\n        else:\n            data = self.con.buffer.consume_message()\n            msg_buf = WriteBuffer.new_message(b'E')\n            msg_buf.write_bytes(data)\n            buf.write_buffer(msg_buf.end_message())\n"
  },
  {
    "path": "edb/server/pgcon/rust_transport.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"\nThis module implements a Rust-based transport for PostgreSQL connections.\n\nThe PGRawConn class provides a high-level interface for establishing and\nmanaging PostgreSQL connections using a Rust-implemented state machine. It\nhandles the complexities of connection establishment, including SSL negotiation\nand authentication, while presenting a simple asyncio-like transport interface\nto the caller.\n\"\"\"\n\nfrom __future__ import annotations\nfrom typing import Optional, Protocol, Callable, Any\n\nimport asyncio\nimport ssl as ssl_module\nimport socket\nimport warnings\n\nfrom enum import Enum, auto\nfrom dataclasses import dataclass\n\nfrom edb.server._rust_native import _pg_rust as pgrust\nfrom edb.server.pgconnparams import (\n    ConnectionParams,\n    SSLMode,\n    get_pg_home_directory,\n)\n\nfrom . import errors as pgerror\n\nTCP_KEEPIDLE = 24\nTCP_KEEPINTVL = 2\nTCP_KEEPCNT = 3\nDEFAULT_CONNECT_TIMEOUT = 60\n\n\nclass ConnectionStateType(Enum):\n    CONNECTING = 0\n    SSL_CONNECTING = auto()\n    AUTHENTICATING = auto()\n    SYNCHRONIZING = auto()\n    READY = auto()\n\n\nclass Authentication(Enum):\n    NONE = 0\n    TRUST = auto()\n    PASSWORD = auto()\n    MD5 = auto()\n    SCRAM_SHA256 = auto()\n\n\n@dataclass\nclass PGState:\n    parameters: dict[str, str]\n    cancellation_key: Optional[tuple[int, int]]\n    auth: Optional[Authentication]\n    server_error: Optional[list[tuple[str, str]]]\n    ssl: bool\n\n\nclass ConnectionStateUpdate(Protocol):\n    def send(self, message: memoryview) -> None: ...\n    def upgrade(self) -> None: ...\n    def parameter(self, name: str, value: str) -> None: ...\n    def cancellation_key(self, pid: int, key: int) -> None: ...\n    def state_changed(self, state: int) -> None: ...\n    def auth(self, auth: int) -> None: ...\n\n\nStateChangeCallback = Callable[[ConnectionStateType], None]\n\n\ndef _parse_tls_version(tls_version: str) -> ssl_module.TLSVersion:\n    if tls_version.startswith('SSL'):\n        raise ValueError(f\"Unsupported TLS version: {tls_version}\")\n    try:\n        return ssl_module.TLSVersion[tls_version.replace('.', '_')]\n    except KeyError:\n        raise ValueError(f\"No such TLS version: {tls_version}\")\n\n\ndef _create_ssl(ssl_config: dict[str, Any]):\n    sslmode = SSLMode.parse(ssl_config['sslmode'])\n    ssl = ssl_module.SSLContext(ssl_module.PROTOCOL_TLS_CLIENT)\n    ssl.check_hostname = sslmode >= SSLMode.verify_full\n    if sslmode < SSLMode.require:\n        ssl.verify_mode = ssl_module.CERT_NONE\n    else:\n        if ssl_config['sslrootcert']:\n            ssl.load_verify_locations(ssl_config['sslrootcert'])\n            ssl.verify_mode = ssl_module.CERT_REQUIRED\n        else:\n            if sslmode == SSLMode.require:\n                ssl.verify_mode = ssl_module.CERT_NONE\n        if ssl_config['sslcrl']:\n            ssl.load_verify_locations(ssl_config['sslcrl'])\n            ssl.verify_flags |= ssl_module.VERIFY_CRL_CHECK_CHAIN\n    if ssl_config['sslkey'] and ssl_config['sslcert']:\n        ssl.load_cert_chain(\n            ssl_config['sslcert'],\n            ssl_config['sslkey'],\n            ssl_config['sslpassword'] or '',\n        )\n    if ssl_config['ssl_max_protocol_version']:\n        ssl.maximum_version = _parse_tls_version(\n            ssl_config['ssl_max_protocol_version']\n        )\n    if ssl_config['ssl_min_protocol_version']:\n        ssl.minimum_version = _parse_tls_version(\n            ssl_config['ssl_min_protocol_version']\n        )\n    # OpenSSL 1.1.1 keylog file\n    if hasattr(ssl, 'keylog_filename'):\n        if ssl_config['keylog_filename']:\n            ssl.keylog_filename = ssl_config['keylog_filename']\n    return ssl\n\n\nclass PGConnectionProtocol(asyncio.Protocol):\n    \"\"\"A protocol that manages the initial connection and authentication process\n    for PostgreSQL.\n\n    This protocol acts as an intermediary between the raw socket connection and\n    the user's protocol.\n    \"\"\"\n\n    def __init__(\n        self,\n        hostname: Optional[str],\n        state: pgrust.PyConnectionState,\n        pg_state: PGState,\n        complete_callback: Callable[\n            [asyncio.BaseTransport], tuple[PGRawConn, asyncio.Protocol]\n        ],\n    ):\n        self.state = state\n        self.pg_state = pg_state\n        self.ready_future: asyncio.Future = asyncio.Future()\n        self.ready_future.add_done_callback(self._cleanup)\n        self._complete_callback = complete_callback\n        self._host = hostname\n        self._transport: Optional[asyncio.Transport] = None\n\n    def _cleanup(self, _fut: asyncio.Future) -> None:\n        # IMPORTANT: break Python/Rust ref cycle\n        self.state.update = None\n        self.state = None\n\n    def data_received(self, data: bytes):\n        if self.ready_future.done():\n            return\n\n        try:\n            self.state.drive_message(memoryview(data))\n            if self.state.is_ready():\n                assert self._transport is not None\n                self.ready_future.set_result(\n                    self._complete_callback(self._transport)\n                )\n        except Exception as e:\n            if not self.ready_future.done():\n                self.ready_future.set_exception(ConnectionError(e))\n\n    def connection_lost(self, exc):\n        if self.ready_future.done():\n            return\n        if exc:\n            self.ready_future.set_exception(exc)\n        else:\n            ex = pgerror.new(\n                pgerror.ERROR_CONNECTION_FAILURE,\n                \"Unexpected connection error\",\n            )\n            ex.__cause__ = exc\n            self.ready_future.set_exception(ex)\n\n    # This may be called multiple times if we upgrade to SSL\n    def connection_made(self, transport) -> None:\n        try:\n            if self._transport is None:\n                # Initial connection\n                self._transport = transport\n                self.state.update = self\n                self.state.drive_initial()\n            else:\n                # Upgrade path\n                self._transport = transport\n        except Exception:\n            pass\n        return super().connection_made(transport)\n\n    def send(self, message: memoryview) -> None:\n        assert self._transport is not None\n        self._transport.write(bytes(message))\n\n    def upgrade(self) -> None:\n        asyncio.create_task(self._upgrade_to_ssl())\n\n    async def _upgrade_to_ssl(self):\n        transport = self._transport\n        assert transport is not None\n        try:\n            ssl_context = _create_ssl(self.state.config)\n            loop = asyncio.get_running_loop()\n            new_transport = await loop.start_tls(\n                transport,\n                self,\n                ssl_context,\n                server_side=False,\n                ssl_handshake_timeout=None,\n                server_hostname=self._host,\n            )\n            self._transport = new_transport\n            self.state.drive_ssl_ready()\n            self.pg_state.ssl = True\n        except Exception as e:\n            if not self.ready_future.done():\n                self.ready_future.set_exception(e)\n            transport.abort()\n\n    def parameter(self, name: str, value: str) -> None:\n        self.pg_state.parameters[name] = value\n\n    def cancellation_key(self, pid: int, key: int) -> None:\n        self.pg_state.cancellation_key = (pid, key)\n\n    def state_changed(self, _: int) -> None:\n        pass\n\n    def auth(self, auth: int) -> None:\n        self.pg_state.auth = Authentication(auth)\n\n    def server_error(self, error: list[tuple[str, str]]) -> None:\n        if not self.ready_future.done():\n            self.ready_future.set_exception(\n                pgerror.BackendConnectionError(fields=dict(error))\n            )\n\n\nclass PGRawConn(asyncio.Transport):\n    def __init__(\n        self,\n        source_description: Optional[str],\n        connection: ConnectionParams,\n        raw_transport: asyncio.Transport,\n        pg_state: PGState,\n        addr: tuple[str, int],\n    ):\n        super().__init__()\n        self.source_description = source_description\n        self.connection = connection\n        self.raw_transport = raw_transport\n        self._pg_state = pg_state\n        self.addr = addr\n\n    @property\n    def state(self):\n        return self._pg_state\n\n    def write(self, data: bytes | bytearray | memoryview):\n        self.raw_transport.write(data)\n\n    def close(self):\n        self.raw_transport.close()\n\n    def is_closing(self):\n        return self.raw_transport.is_closing()\n\n    def get_extra_info(self, name: str, default=None):\n        return self.raw_transport.get_extra_info(name, default)\n\n    def pause_reading(self):\n        self.raw_transport.pause_reading()\n\n    def resume_reading(self):\n        self.raw_transport.resume_reading()\n\n    def is_reading(self):\n        return self.raw_transport.is_reading()\n\n    def abort(self):\n        self.raw_transport.abort()\n\n    def __repr__(self):\n        params = ', '.join(\n            f\"{k}={v}\" for k, v in self._pg_state.parameters.items()\n        )\n        auth_str = (\n            f\", auth={self._pg_state.auth.name}\" if self._pg_state.auth else \"\"\n        )\n        source_str = (\n            f\", source={self.source_description}\"\n            if self.source_description\n            else \"\"\n        )\n        raw_repr = repr(self.raw_transport)\n        dsn = self.connection._params\n        return (\n            f\"<PGRawConn: connected{auth_str}{source_str}, {params}, \"\n            f\"dsn={dsn}, raw_connection={raw_repr}>\"\n        )\n\n    def __del__(self):\n        if not self.raw_transport.is_closing():\n            warnings.warn(\n                f\"unclosed connection {repr(self)}\",\n                ResourceWarning,\n                stacklevel=2,\n            )\n            self.raw_transport.abort()\n\n\nasync def _create_connection_to(\n    protocol_factory: Callable[[Optional[str], str, int], PGConnectionProtocol],\n    address_family: str,\n    host: str | bytes,\n    hostname: str,\n    port: int,\n) -> tuple[asyncio.Transport, PGConnectionProtocol]:\n    if address_family == \"unix\":\n        t, protocol = await asyncio.get_running_loop().create_unix_connection(\n            lambda: protocol_factory(None, hostname, port), path=host  # type: ignore\n        )\n        return (t, protocol)\n    else:\n        t, protocol = await asyncio.get_running_loop().create_connection(\n            lambda: protocol_factory(hostname, hostname, port), str(host), port\n        )\n        _set_tcp_keepalive(t)\n        return (t, protocol)\n\n\nasync def _create_connection(\n    protocol_factory: Callable[[Optional[str], str, int], PGConnectionProtocol],\n    connect_timeout: Optional[int],\n    host_candidates: list[tuple[str, str | bytes, str, int]],\n) -> tuple[asyncio.Transport, PGConnectionProtocol]:\n    e = None\n    for address_family, host, hostname, port in host_candidates:\n        try:\n            async with asyncio.timeout(\n                connect_timeout if connect_timeout else DEFAULT_CONNECT_TIMEOUT\n            ):\n                return await _create_connection_to(\n                    protocol_factory, address_family, host, hostname, port\n                )\n        except asyncio.TimeoutError as ex:\n            raise pgerror.new(\n                pgerror.ERROR_CONNECTION_FAILURE,\n                \"timed out connecting to backend\",\n            ) from ex\n        except Exception as ex:\n            e = ex\n            continue\n    raise ConnectionError(\n        f\"Failed to connect to any of the provided hosts: {host_candidates}\"\n    ) from e\n\n\ndef _set_tcp_keepalive(transport):\n    # TCP keepalive was initially added here for special cases where idle\n    # connections are dropped silently on GitHub Action running test suite\n    # against AWS RDS. We are keeping the TCP keepalive for generic\n    # Postgres connections as the kernel overhead is considered low, and\n    # in certain cases it does save us some reconnection time.\n    #\n    # In case of high-availability Postgres, TCP keepalive is necessary to\n    # disconnect from a failing master node, if no other failover information\n    # is available.\n    sock = transport.get_extra_info('socket')\n    sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)\n\n    # TCP_KEEPIDLE: the time (in seconds) the connection needs to remain idle\n    # before TCP starts sending keepalive probes. This is socket.TCP_KEEPIDLE\n    # on Linux, and socket.TCP_KEEPALIVE on macOS from Python 3.10.\n    if hasattr(socket, 'TCP_KEEPIDLE'):\n        sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, TCP_KEEPIDLE)\n    if hasattr(socket, 'TCP_KEEPALIVE'):\n        sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPALIVE, TCP_KEEPIDLE)\n\n    # TCP_KEEPINTVL: The time (in seconds) between individual keepalive probes.\n    if hasattr(socket, 'TCP_KEEPINTVL'):\n        sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, TCP_KEEPINTVL)\n\n    # TCP_KEEPCNT: The maximum number of keepalive probes TCP should send\n    # before dropping the connection.\n    if hasattr(socket, 'TCP_KEEPCNT'):\n        sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, TCP_KEEPCNT)\n\n\ndef complete_connection_callback(\n    host, port, source_description, state, protocol_factory, pg_state\n) -> Callable[[asyncio.BaseTransport], tuple[PGRawConn, asyncio.Protocol]]:\n    def complete_connection(upgraded_transport):\n        conn = PGRawConn(\n            source_description,\n            ConnectionParams._create(state.config),\n            upgraded_transport,\n            pg_state,\n            (host, port),\n        )\n\n        # We've successfully upgraded the protocol at this point, and the remote\n        # PG server is sitting in the idle state, waiting for us to send the\n        # next message. We transition to the user protocol here, synthesizing\n        # a `connection_made` event.\n        user_protocol = protocol_factory()\n        upgraded_transport.set_protocol(user_protocol)\n\n        # Notify the user protocol of successful connection\n        user_protocol.connection_made(conn)\n        return conn, user_protocol\n\n    return complete_connection\n\n\nasync def create_postgres_connection[P: asyncio.Protocol](\n    dsn: str | ConnectionParams,\n    protocol_factory: Callable[[], P],\n    *,\n    source_description: Optional[str] = None,\n) -> tuple[PGRawConn, P]:\n    \"\"\"\n    Open a PostgreSQL connection to the address specified by the DSN or\n    ConnectionParams, creating the user protocol from the protocol_factory.\n\n    This method establishes the connection asynchronously. When successful, it\n    returns a (PGRawConn, protocol) pair.\n    \"\"\"\n    if isinstance(dsn, str):\n        dsn = ConnectionParams(dsn=dsn)\n    connect_timeout = dsn.connect_timeout\n    try:\n        state = pgrust.PyConnectionState(\n            dsn._params, \"postgres\", get_pg_home_directory()\n        )\n    except Exception as e:\n        raise ValueError(e)\n    pg_state = PGState(\n        parameters={},\n        cancellation_key=None,\n        auth=None,\n        server_error=None,\n        ssl=False,\n    )\n\n    # The PGConnectionProtocol will drive the PyConnectionState from network\n    # bytes it receives, as well as driving the connection from the messages\n    # from PyConnectionState.\n    connect_protocol_factory = (\n        lambda hostname, host, port: PGConnectionProtocol(\n            hostname,\n            state,\n            pg_state,\n            complete_connection_callback(\n                host,\n                port,\n                source_description,\n                state,\n                protocol_factory,\n                pg_state,\n            ),\n        )\n    )\n\n    # Create a transport to the backend based off the host candidates.\n    host_candidates = await asyncio.get_running_loop().run_in_executor(\n        executor=None, func=lambda: state.config.host_candidates\n    )\n    _, protocol = await _create_connection(\n        connect_protocol_factory,\n        connect_timeout,\n        host_candidates,\n    )\n\n    conn, user_protocol = await protocol.ready_future\n    return conn, user_protocol\n"
  },
  {
    "path": "edb/server/pgconnparams.py",
    "content": "# Copyright (C) 2016-present MagicStack Inc. and the EdgeDB authors.\n# Copyright (C) 2016-present the asyncpg authors and contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import TypedDict, NotRequired, Optional, Unpack, Self, Any\n\nimport enum\nimport pathlib\nimport platform\n\nfrom edb.server._rust_native._pg_rust import PyConnectionParams\n\n\n_system = platform.uname().system\nif _system == 'Windows':\n    import ctypes.wintypes\n\n    CSIDL_APPDATA = 0x001A\n\n    def get_pg_home_directory() -> Optional[str]:\n        # We cannot simply use expanduser() as that returns the user's\n        # home directory, whereas Postgres stores its config in\n        # %AppData% on Windows.\n        buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)\n        r = ctypes.windll.shell32.SHGetFolderPathW(  # type: ignore\n            0, CSIDL_APPDATA, 0, 0, buf\n        )\n        if r:\n            return None\n        else:\n            return str(pathlib.Path(buf.value) / 'postgresql')\n\nelse:\n\n    def get_pg_home_directory() -> Optional[str]:\n        try:\n            return str(pathlib.Path.home())\n        except RuntimeError:\n            # This can happen if the home directory is not set\n            return None\n\n\nclass SSLMode(enum.IntEnum):\n    disable = 0\n    allow = 1\n    prefer = 2\n    require = 3\n    verify_ca = 4\n    verify_full = 5\n\n    @classmethod\n    def parse(cls, sslmode: str) -> Self:\n        value: Self = getattr(cls, sslmode.replace('-', '_'))\n        assert value is not None, f\"Invalid SSL mode: {sslmode}\"\n        return value\n\n\nclass CreateParamsKwargs(TypedDict, total=False):\n    dsn: NotRequired[str]\n    hosts: NotRequired[Optional[list[tuple[str, int]]]]\n    host: NotRequired[Optional[str]]\n    user: NotRequired[Optional[str]]\n    password: NotRequired[Optional[str]]\n    database: NotRequired[Optional[str]]\n    server_settings: NotRequired[Optional[dict[str, str]]]\n    sslmode: NotRequired[Optional[SSLMode]]\n    sslrootcert: NotRequired[Optional[str]]\n    connect_timeout: NotRequired[Optional[int]]\n\n\nclass ConnectionParams:\n    \"\"\"\n    A Python representation of the Rust connection parameters that are\n    passed back during connection/parse.\n\n    This class encapsulates the connection parameters used for establishing\n    a connection to a PostgreSQL database.\n    \"\"\"\n\n    _params: PyConnectionParams\n\n    def __init__(self, **kwargs: Unpack[CreateParamsKwargs]) -> None:\n        dsn = kwargs.pop(\"dsn\", None)\n        if dsn:\n            self._params = PyConnectionParams(dsn)\n        else:\n            self._params = PyConnectionParams(None)\n        self.update(**kwargs)\n\n    @classmethod\n    def _create(\n        cls,\n        params: dict[str, Any],\n    ) -> Self:\n        instance = super().__new__(cls)\n        instance._params = params\n        return instance\n\n    def update(self, **kwargs: Unpack[CreateParamsKwargs]) -> None:\n        if dsn := kwargs.pop('dsn', None):\n            params = PyConnectionParams(dsn)\n            for k, v in params.to_dict().items():\n                self._params[k] = v\n        if server_settings := kwargs.pop(\"server_settings\", None):\n            for k2, v2 in server_settings.items():\n                self._params.update_server_settings(k2, v2)\n        if host_specs := kwargs.pop(\"hosts\", None):\n            hosts, ports = zip(*host_specs)\n            self._params['host'] = ','.join(hosts)\n            self._params['port'] = ','.join(map(str, ports))\n        if (ssl_mode := kwargs.pop(\"sslmode\", None)) is not None:\n            mode: SSLMode = ssl_mode\n            self._params[\"sslmode\"] = mode.name\n        if connect_timeout := kwargs.pop(\"connect_timeout\", None):\n            self._params[\"connect_timeout\"] = str(connect_timeout)\n        for k, v in kwargs.items():\n            if k == \"database\":\n                k = \"dbname\"\n            self._params[k] = v\n\n    def clear_server_settings(self) -> None:\n        self._params.clear_server_settings()\n\n    def resolve(self) -> Self:\n        return self._create(\n            self._params.resolve(\"\", get_pg_home_directory()),\n        )\n\n    def __copy__(self) -> Self:\n        return self._create(self._params.clone())\n\n    @property\n    def hosts(self) -> Optional[list[tuple[dict[str, Any], int]]]:\n        return self._params['hosts']  # type: ignore\n\n    @property\n    def host(self) -> Optional[str]:\n        return self._params['host']  # type: ignore\n\n    @property\n    def port(self) -> Optional[int]:\n        return self._params['port']  # type: ignore\n\n    @property\n    def user(self) -> Optional[str]:\n        return self._params['user']  # type: ignore\n\n    @property\n    def password(self) -> Optional[str]:\n        return self._params['password']  # type: ignore\n\n    @property\n    def database(self) -> Optional[str]:\n        return self._params['dbname']  # type: ignore\n\n    @property\n    def connect_timeout(self) -> Optional[int]:\n        connect_timeout = self._params['connect_timeout']\n        return int(connect_timeout) if connect_timeout else None\n\n    @property\n    def sslmode(self) -> Optional[SSLMode]:\n        sslmode = self._params['sslmode']\n        return SSLMode.parse(sslmode) if sslmode is not None else None\n\n    def to_dsn(self) -> str:\n        dsn: str = self._params.to_dsn()\n        return dsn\n\n    @property\n    def __dict__(self) -> dict[str, Any]:\n        to_dict: dict[str, str] = self._params.to_dict()\n        database = to_dict.pop('dbname', None)\n        if database:\n            to_dict['database'] = database\n        return to_dict\n\n    @__dict__.setter\n    def __dict__(self, value: dict[str, Any]) -> None:\n        new_params = self._params.__class__()\n        try:\n            for k, v in value.items():\n                new_params[k] = v\n            self._params = new_params\n        except Exception:\n            raise ValueError(\"Failed to update __dict__\")\n\n    def __repr__(self) -> Any:\n        return self._params.__repr__()\n"
  },
  {
    "path": "edb/server/protocol/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom . import protocol\n\nHttpProtocol = protocol.HttpProtocol\n\n\n__all__ = ('HttpProtocol',)\n"
  },
  {
    "path": "edb/server/protocol/ai_ext.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom dataclasses import dataclass, field\nfrom typing import (\n    cast,\n    Any,\n    AsyncIterator,\n    ClassVar,\n    Literal,\n    NoReturn,\n    Optional,\n    Sequence,\n    TYPE_CHECKING,\n)\n\nimport abc\nimport asyncio\nimport contextlib\nimport contextvars\nimport itertools\nimport json\nimport logging\nimport uuid\n\nimport tiktoken\nfrom mistral_common.tokens.tokenizers import mistral as mistral_tokenizer\n\nfrom edb import errors\nfrom edb.common import asyncutil\nfrom edb.common import debug\nfrom edb.common import enum as s_enum\nfrom edb.common import markup\nfrom edb.common import uuidgen\nfrom edb.common.typeutils import not_none\n\nfrom edb.server import compiler, http\nfrom edb.server import defines as edbdef\nfrom edb.server.compiler import sertypes\nfrom edb.server.protocol import execute\nfrom edb.server.protocol import request_scheduler as rs\n\nif TYPE_CHECKING:\n    from edb.server import dbview\n    from edb.server import tenant as srv_tenant\n    from edb.server import pgcon\n    from edb.server.protocol import protocol\n\n\nlogger = logging.getLogger(\"edb.server.ai_ext\")\nkeepalive_token = \"ai-index-builder\"\n\n\nclass AIExtError(Exception):\n    http_status: ClassVar[http.HTTPStatus] = (\n        http.HTTPStatus.INTERNAL_SERVER_ERROR)\n\n    def __init__(\n        self,\n        *args: object,\n        json: Optional[dict[str, Any]] = None,\n    ) -> None:\n        super().__init__(*args)\n        self._json = json\n\n    def get_http_status(self) -> http.HTTPStatus:\n        return self.__class__.http_status\n\n    def json(self) -> dict[str, Any]:\n        if self._json is not None:\n            return self._json\n        else:\n            return {\n                \"message\": str(self.args[0]),\n                \"type\": self.__class__.__name__,\n            }\n\n\nclass AIProviderError(AIExtError):\n    pass\n\n\nclass ConfigurationError(AIExtError):\n    pass\n\n\nclass InternalError(AIExtError):\n    pass\n\n\nclass BadRequestError(AIExtError):\n    http_status = http.HTTPStatus.BAD_REQUEST\n\n\nclass ApiStyle(s_enum.StrEnum):\n    OpenAI = 'OpenAI'\n    Anthropic = 'Anthropic'\n    Ollama = 'Ollama'\n\n\nclass Tokenizer(abc.ABC):\n\n    @abc.abstractmethod\n    def encode(self, text: str) -> list[int]:\n        \"\"\"Encode text into tokens.\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def encode_padding(self) -> int:\n        \"\"\"How many special characters are added to encodings?\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def decode(self, tokens: list[int]) -> str:\n        \"\"\"Decode tokens into text.\"\"\"\n        raise NotImplementedError\n\n    def shorten_to_token_length(\n        self, text: str, token_length: int\n    ) -> tuple[str, int]:\n        \"\"\"Truncate text to a maximum token length.\"\"\"\n        encoded = self.encode(text)\n        if len(encoded) > token_length:\n            encoded = encoded[:token_length]\n        return self.decode(encoded), len(encoded)\n\n\nclass OpenAITokenizer(Tokenizer):\n\n    _instances: dict[str, OpenAITokenizer] = {}\n\n    encoding: Any\n\n    @classmethod\n    def for_model(cls, model_name: str) -> OpenAITokenizer:\n        if model_name in cls._instances:\n            return cls._instances[model_name]\n\n        tokenizer = OpenAITokenizer()\n        tokenizer.encoding = tiktoken.encoding_for_model(model_name)\n        cls._instances[model_name] = tokenizer\n\n        return tokenizer\n\n    def encode(self, text: str) -> list[int]:\n        return cast(list[int], self.encoding.encode(text))\n\n    def encode_padding(self) -> int:\n        return 0\n\n    def decode(self, tokens: list[int]) -> str:\n        return cast(str, self.encoding.decode(tokens))\n\n\nclass MistralTokenizer(Tokenizer):\n\n    _instances: dict[str, MistralTokenizer] = {}\n\n    tokenizer: Any\n\n    @classmethod\n    def for_model(cls, model_name: str) -> MistralTokenizer:\n        if model_name in cls._instances:\n            return cls._instances[model_name]\n\n        assert model_name == 'mistral-embed'\n\n        tokenizer = MistralTokenizer()\n        tokenizer.tokenizer = mistral_tokenizer.MistralTokenizer.v1()\n        cls._instances[model_name] = tokenizer\n\n        return tokenizer\n\n    def encode(self, text: str) -> list[int]:\n        # V1 tokenizer wraps input text with control tokens [INST] [/INST].\n        #\n        # While these count towards the overal token limit, how special tokens\n        # are applied to embedding requests is not documented. For now, directly\n        # pass the text into the inner tokenizer.\n        tokenized = self.tokenizer.instruct_tokenizer.tokenizer.encode(\n            text, bos=False, eos=False\n        )\n        return cast(list[int], tokenized)\n\n    def encode_padding(self) -> int:\n        # V1 tokenizer wraps input text with control tokens [INST] [/INST].\n        #\n        # This is only 2 tokens, and testing shows that mistral-embed does add\n        # two tokens to embeddings inputs. However, this is not documented, so\n        # add some extra leeway in case things change.\n        #\n        # Note, other models may use significantly more control tokens.\n        return 16\n\n    def decode(self, tokens: list[int]) -> str:\n        return cast(str, self.tokenizer.decode(tokens))\n\n\nclass OllamaTokenizer(Tokenizer):\n\n    \"\"\"\n    Simply counts the number of characters.\n    A tokenizer API is in progress, but unlikely to be released soon.\n    \"\"\"\n\n    _instances: dict[str, OllamaTokenizer] = {}\n\n    @classmethod\n    def for_model(cls, model_name: str) -> OllamaTokenizer:\n        if model_name in cls._instances:\n            return cls._instances[model_name]\n\n        tokenizer = OllamaTokenizer()\n        cls._instances[model_name] = tokenizer\n\n        return tokenizer\n\n    def encode(self, text: str) -> list[int]:\n        return [ord(c) for c in text]\n\n    def encode_padding(self) -> int:\n        return 0\n\n    def decode(self, tokens: list[int]) -> str:\n        return ''.join(chr(c) for c in tokens)\n\n\nclass TestTokenizer(Tokenizer):\n\n    _instances: dict[str, TestTokenizer] = {}\n\n    @classmethod\n    def for_model(cls, model_name: str) -> TestTokenizer:\n        if model_name in cls._instances:\n            return cls._instances[model_name]\n\n        tokenizer = TestTokenizer()\n        cls._instances[model_name] = tokenizer\n\n        return tokenizer\n\n    def encode(self, text: str) -> list[int]:\n        return [ord(c) for c in text]\n\n    def encode_padding(self) -> int:\n        return 0\n\n    def decode(self, tokens: list[int]) -> str:\n        return ''.join(chr(c) for c in tokens)\n\n\ndef get_model_tokenizer(\n    provider_name: str,\n    model_name: str,\n) -> Optional[Tokenizer]:\n    \"\"\"Get the tokenizer for a given provider and model\"\"\"\n    if provider_name == 'builtin::openai':\n        return OpenAITokenizer.for_model(model_name)\n    elif provider_name == 'builtin::mistral':\n        return MistralTokenizer.for_model(model_name)\n    if provider_name == 'builtin::ollama':\n        return OllamaTokenizer.for_model(model_name)\n    elif provider_name == 'custom::test':\n        return TestTokenizer.for_model(model_name)\n    else:\n        return None\n\n\n@dataclass(frozen=True)\nclass ProviderConfig:\n    name: str\n    display_name: str\n    api_url: str\n    client_id: str\n    secret: str\n    api_style: ApiStyle\n\n    def get_embeddings_from_result(\n        self, embeddings_result: bytes\n    ) -> list[list[float]]:\n        \"\"\"Decode and extract the embeddings from an embeddings request.\"\"\"\n\n        decoded_result = json.loads(\n            embeddings_result.decode(\"utf-8\")\n        )\n\n        if self.api_style == ApiStyle.Ollama:\n            return cast(\n                list[list[float]],\n                decoded_result[\"embeddings\"],\n            )\n        else:\n            return cast(\n                list[list[float]],\n                [\n                    entry_result[\"embedding\"]\n                    for entry_result in decoded_result[\"data\"]\n                ],\n            )\n\n\n@dataclass(frozen=True, kw_only=True)\nclass BaseModel:\n    name: str\n    provider: str\n\n    name_annotation: ClassVar[str] = \"ext::ai::model_name\"\n    provider_annotation: ClassVar[str] = \"ext::ai::model_provider\"\n\n\n@dataclass(frozen=True, kw_only=True)\nclass EmbeddingModel (BaseModel):\n    max_input_tokens: int\n    max_batch_tokens: int\n    max_batch_size: int | None\n    max_output_dimensions: int\n    supports_shortening: bool\n\n    gel_type: ClassVar[str] = \"ext::ai::EmbeddingModel\"\n\n    max_model_input_tokens_annotation: ClassVar[str] = (\n        \"ext::ai::embedding_model_max_input_tokens\"\n    )\n    max_batch_tokens_annotation: ClassVar[str] = (\n        \"ext::ai::embedding_model_max_batch_tokens\"\n    )\n    max_batch_size_annotation: ClassVar[str] = (\n        \"ext::ai::embedding_model_max_batch_size\"\n    )\n    max_output_dimensions_annotation: ClassVar[str] = (\n        \"ext::ai::embedding_model_max_output_dimensions\"\n    )\n    supports_shortening_annotation: ClassVar[str] = (\n        \"ext::ai::embedding_model_supports_shortening\"\n    )\n\n\n@dataclass(frozen=True, kw_only=True)\nclass TextGenerationModel (BaseModel):\n    gel_type: ClassVar[str] = \"ext::ai::TextGenerationModel\"\n\n\ndef start_extension(\n    tenant: srv_tenant.Tenant,\n    dbname: str,\n) -> None:\n    task_name = _get_builder_task_name(dbname)\n    task = tenant.get_task(task_name)\n    if task is None:\n        logger.info(f\"starting AI extension tasks on database {dbname!r}\")\n        tenant.create_task(\n            _ext_ai_index_builder_controller_loop(tenant, dbname),\n            interruptable=True,\n            name=task_name,\n        )\n\n\ndef stop_extension(\n    tenant: srv_tenant.Tenant,\n    dbname: str,\n) -> None:\n    task_name = _get_builder_task_name(dbname)\n    task = tenant.get_task(task_name)\n    if task is not None:\n        logger.info(f\"stopping AI extension tasks on database {dbname!r}\")\n        task.cancel()\n\n\ndef _get_builder_task_name(dbname: str) -> str:\n    return f\"ext::ai::index builder on database {dbname!r}\"\n\n\n_task_name = contextvars.ContextVar(\n    \"ext_ai_index_builder_task_name\", default=\"-\")\n\n\nasync def _ext_ai_index_builder_controller_loop(\n    tenant: srv_tenant.Tenant,\n    dbname: str,\n) -> None:\n    task_name = _get_builder_task_name(dbname)\n    _task_name.set(task_name)\n    logger.info(f\"started {task_name}\")\n    db = tenant.get_db(dbname=dbname)\n    holding_lock = False\n\n    await db.introspection()\n    naptime_cfg = db.lookup_config(\"ext::ai::Config::indexer_naptime\")\n    naptime = naptime_cfg.to_microseconds() / 1000000\n\n    provider_schedulers: dict[str, ProviderScheduler] = {}\n\n    try:\n        while tenant.accept_new_tasks:\n            if not db.tenant.is_database_connectable(dbname):\n                # Don't do work if the database is not connectable,\n                # e.g. being dropped\n                await asyncio.sleep(naptime)\n                continue\n\n            models = []\n            sleep_timer: rs.Timer = rs.Timer(None, False)\n            try:\n                async with tenant.with_pgcon(dbname) as pgconn:\n                    models = await _ext_ai_fetch_active_models(pgconn)\n                    if models:\n                        if not holding_lock:\n                            holding_lock = await _ext_ai_lock(tenant, pgconn)\n                        if holding_lock:\n                            provider_contexts = _prepare_provider_contexts(\n                                db,\n                                pgconn,\n                                tenant.get_http_client(originator=\"ai/index\"),\n                                models,\n                                provider_schedulers,\n                                naptime,\n                            )\n                            try:\n                                sleep_timer = (\n                                    await _ext_ai_index_builder_work(\n                                        provider_schedulers,\n                                        provider_contexts,\n                                    )\n                                )\n                            finally:\n                                if not sleep_timer.is_ready_and_urgent():\n                                    await asyncutil.deferred_shield(\n                                        _ext_ai_unlock(tenant))\n                                    tenant.server.remove_keepalive_token(\n                                        (\n                                            keepalive_token,\n                                            tenant.get_instance_name(),\n                                            dbname,\n                                        )\n                                    )\n                                    holding_lock = False\n            except Exception:\n                logger.error(\n                    f\"caught error in {task_name}\",\n                    exc_info=tenant.accept_new_tasks,\n                )\n\n            if not tenant.accept_new_tasks:\n                break\n\n            if not sleep_timer.is_ready_and_urgent():\n                delay = sleep_timer.remaining_time(naptime)\n                if delay == naptime:\n                    logger.debug(\n                        f\"{task_name}: \"\n                        f\"No work. Napping for {naptime:.2f} seconds.\"\n                    )\n                await asyncio.sleep(delay)\n\n    finally:\n        logger.info(f\"stopped {task_name}\")\n\n\nasync def _ext_ai_fetch_active_models(\n    pgconn: pgcon.PGConnection,\n) -> list[tuple[int, str, str]]:\n    models = await pgconn.sql_fetch(\n        b\"\"\"\n            SELECT\n                id,\n                name,\n                provider\n            FROM\n                edgedbext.ai_active_embedding_models\n        \"\"\",\n    )\n\n    result = []\n    if models:\n        for model in models:\n            result.append((\n                int.from_bytes(model[0], byteorder=\"big\", signed=True),\n                model[1].decode(\"utf-8\"),\n                model[2].decode(\"utf-8\"),\n            ))\n\n    return result\n\n\n# The _ext_ai_lock() is a long-term lock held in the system pgcon. It is used\n# in the index builder job above guarding multiple alternating database pgcons\n# and outgoing HTTP requests (free up pgcons while waiting for a response from\n# external services), so that different Gel tenants on the same backend\n# run this job exclusively.\n#\n# The following implementation is also safe to be used by multiple tasks within\n# the same tenant (though at the time of writing, there is only one such task\n# per tenant). To achieve this, we added an extra query on pg_locks to check if\n# it's already held by another task, because advisory locks allow reentrancy\n# from the same session (the same sys_pgcon). And to avoid racing conditions,\n# we use another advisory lock over the 2 queries of check-and-lock in the\n# local session. This also means, one must use _ext_ai_lock() instead of an\n# individual lock of the 2 locks here to avoid misuse.\n#\n# If you are editing the magic numbers here: make sure it fits in a Postgres\n# Oid type (uint32), or you'll need to change the `classid` query below.\n_EXT_AI_ADVISORY_LOCK = b\"3987734540\"\n_EXT_AI_ADVISORY_LOCK_LOCK = b\"3987734541\"\n\n\nasync def _ext_ai_lock(\n    tenant: srv_tenant.Tenant,\n    pgconn: pgcon.PGConnection,\n) -> bool:\n    # We use transaction-level advisory locks to ensure releasing\n    await pgconn.sql_execute(b\"START TRANSACTION\")\n    try:\n        b = await pgconn.sql_fetch_val(\n            b\"SELECT pg_try_advisory_xact_lock(\"\n            + _EXT_AI_ADVISORY_LOCK_LOCK\n            + b\")\"\n        )\n        if b == b'\\x01':\n            lock_free = await pgconn.sql_fetch_val(\n                b'''\n                SELECT NOT EXISTS (\n                    SELECT\n                        1\n                    FROM\n                        pg_locks\n                    WHERE\n                        locktype = 'advisory'\n                        AND classid = 0\n                        AND objid = \\\n                ''' + _EXT_AI_ADVISORY_LOCK + b')')\n            if lock_free == b'\\x01':\n                async with tenant.use_sys_pgcon() as syscon:\n                    # The long-term holding lock must be on session-level\n                    b = await syscon.sql_fetch_val(\n                        b\"SELECT pg_try_advisory_lock(\"\n                        + _EXT_AI_ADVISORY_LOCK\n                        + b\")\"\n                    )\n                    return b == b'\\x01'\n    finally:\n        await pgconn.sql_execute(b\"ROLLBACK\")\n\n    return False\n\n\nasync def _ext_ai_unlock(\n    tenant: srv_tenant.Tenant,\n) -> None:\n    async with tenant.use_sys_pgcon() as syscon:\n        await syscon.sql_fetch_val(\n            b\"SELECT pg_advisory_unlock(\" + _EXT_AI_ADVISORY_LOCK + b\")\")\n\n\ndef _prepare_provider_contexts(\n    db: dbview.Database,\n    pgconn: pgcon.PGConnection,\n    http_client: http.HttpClient,\n    models: list[tuple[int, str, str]],\n    provider_schedulers: dict[str, ProviderScheduler],\n    naptime: float,\n) -> dict[str, ProviderContext]:\n\n    models_by_provider: dict[str, list[str]] = {}\n    for entry in models:\n        model_name = entry[1]\n        provider_name = entry[2]\n        try:\n            models_by_provider[provider_name].append(model_name)\n        except KeyError:\n            m = models_by_provider[provider_name] = []\n            m.append(model_name)\n\n    # Drop any extra providers, they were probably deleted.\n    unused_provider_names = {\n        provider_name\n        for provider_name in provider_schedulers.keys()\n        if provider_name not in models_by_provider\n    }\n    for unused_provider_name in unused_provider_names:\n        provider_schedulers.pop(unused_provider_name, None)\n\n    # Create contexts\n    provider_contexts = {}\n\n    for provider_name, provider_models in models_by_provider.items():\n        if provider_name not in provider_schedulers:\n            # Create new schedulers if necessary\n            provider_schedulers[provider_name] = ProviderScheduler(\n                service=rs.Service(\n                    limits={'requests': None, 'tokens': None},\n                ),\n                provider_name=provider_name,\n            )\n        provider_scheduler = provider_schedulers[provider_name]\n\n        if not provider_scheduler.timer.is_ready():\n            continue\n\n        provider_contexts[provider_name] = ProviderContext(\n            naptime=naptime,\n            db=db,\n            pgconn=pgconn,\n            http_client=http_client,\n            provider_models=provider_models,\n        )\n\n    return provider_contexts\n\n\nasync def _ext_ai_index_builder_work(\n    provider_schedulers: dict[str, ProviderScheduler],\n    provider_contexts: dict[str, ProviderContext],\n) -> rs.Timer:\n\n    async with asyncio.TaskGroup() as g:\n        for provider_name, provider_scheduler in provider_schedulers.items():\n            if provider_name not in provider_contexts:\n                continue\n\n            provider_context = provider_contexts[provider_name]\n            g.create_task(provider_scheduler.process(provider_context))\n\n    sleep_timer = rs.Timer.combine(\n        provider_scheduler.timer\n        for provider_scheduler in provider_schedulers.values()\n    )\n    if sleep_timer is not None:\n        return sleep_timer\n    else:\n        # Return any non-urgent timer\n        return rs.Timer(None, False)\n\n\n@dataclass(frozen=True)\nclass EmbeddingsData:\n    embeddings: bytes\n\n\n@dataclass\nclass ProviderContext(rs.Context):\n\n    db: dbview.Database\n    pgconn: pgcon.PGConnection\n    http_client: http.HttpClient\n    provider_models: list[str]\n\n\n@dataclass\nclass ProviderScheduler(rs.Scheduler[EmbeddingsData]):\n\n    provider_name: str = ''\n\n    # If a text is too long for a model, it will be excluded from embeddings\n    # to prevent pointlessly wasting requests and tokens.\n    # An embedding index may have its `truncate_to_max` flag switched. If the\n    # flag is on, previously excluded inputs will be truncated and processed.\n    model_excluded_ids: dict[str, list[str]] = field(default_factory=dict)\n\n    async def get_params(\n        self, context: rs.Context,\n    ) -> Optional[Sequence[EmbeddingsParams]]:\n        assert isinstance(context, ProviderContext)\n        rv = await _generate_embeddings_params(\n            context.db,\n            context.pgconn,\n            context.http_client,\n            self.provider_name,\n            context.provider_models,\n            self.model_excluded_ids,\n            tokens_rate_limit=(\n                self.service.limits['tokens'].total\n                if self.service.limits['tokens'] is not None else\n                None\n            ),\n        )\n        if rv:\n            context.db.server.add_keepalive_token(\n                (\n                    keepalive_token,\n                    context.db.tenant.get_instance_name(),\n                    context.db.name,\n                )\n            )\n        return rv\n\n    def finalize(self, execution_report: rs.ExecutionReport) -> None:\n        task_name = _task_name.get()\n\n        for message in execution_report.known_error_messages:\n            logger.error(\n                f\"{task_name}: \"\n                f\"Could not generate embeddings for {self.provider_name} \"\n                f\"due to an internal error: {message}\"\n            )\n\n\n@dataclass(frozen=True, kw_only=True)\nclass EmbeddingsParams(rs.Params[EmbeddingsData]):\n    pgconn: pgcon.PGConnection\n    http_client: http.HttpClient\n    provider: ProviderConfig\n    model_name: str\n    inputs: list[tuple[PendingEmbedding, str]]\n    token_count: int\n    shortening: Optional[int]\n    user: Optional[str]\n\n    def costs(self) -> dict[str, int]:\n        return {\n            'requests': 1,\n            'tokens': self.token_count,\n        }\n\n    def create_request(self) -> EmbeddingsRequest:\n        return EmbeddingsRequest(self)\n\n\nclass EmbeddingsRequest(rs.Request[EmbeddingsData]):\n\n    async def run(self) -> Optional[rs.Result[EmbeddingsData]]:\n        task_name = _task_name.get()\n\n        try:\n            assert isinstance(self.params, EmbeddingsParams)\n            result = await _generate_embeddings(\n                self.params.provider,\n                self.params.model_name,\n                [input[1] for input in self.params.inputs],\n                self.params.shortening,\n                self.params.user,\n                self.params.http_client,\n            )\n            result.pgconn = self.params.pgconn\n            result.pending_entries = [\n                input[0] for input in self.params.inputs\n            ]\n            return result\n        except AIExtError as e:\n            logger.error(f\"{task_name}: {e}\")\n            return None\n        except Exception as e:\n            logger.error(\n                f\"{task_name}: could not generate embeddings \"\n                f\"due to an internal error: {e}\"\n            )\n            return None\n\n\nclass EmbeddingsResult(rs.Result[EmbeddingsData]):\n\n    provider_cfg: ProviderConfig\n    pgconn: Optional[Any] = None\n    pending_entries: Optional[list[PendingEmbedding]] = None\n\n    async def finalize(self) -> None:\n        if isinstance(self.data, rs.Error):\n            return\n        if self.pgconn is None or self.pending_entries is None:\n            return\n\n        # Entries must line up with the embeddings data:\n        # - `_generate_embeddings` produces produces embeddings data matching\n        #   the order of its inputs\n        #\n        # Entries must be grouped by target rel:\n        # - `_generate_embeddings_params` sorts inputs by target rel before\n        groups = itertools.groupby(\n            self.pending_entries, key=lambda e: (e.target_rel, e.target_attr),\n        )\n        offset = 0\n        for (rel, attr), items in groups:\n            ids = [item.id for item in items]\n            await _update_embeddings_in_db(\n                self.pgconn,\n                self.provider_cfg,\n                rel,\n                attr,\n                ids,\n                self.data.embeddings,\n                offset,\n            )\n            offset += len(ids)\n\n\nasync def _generate_embeddings_params(\n    db: dbview.Database,\n    pgconn: pgcon.PGConnection,\n    http_client: http.HttpClient,\n    provider_name: str,\n    provider_models: list[str],\n    model_excluded_ids: dict[str, list[str]],\n    *,\n    tokens_rate_limit: Optional[int | Literal['unlimited']],\n) -> Optional[list[EmbeddingsParams]]:\n    task_name = _task_name.get()\n\n    try:\n        provider_cfg = _get_provider_config(\n            db=db, provider_name=provider_name)\n    except LookupError as e:\n        logger.error(f\"{task_name}: {e}\")\n        return None\n\n    embedding_models = await _get_embedding_models(\n        db, provider_models\n    )\n\n    model_pending_entries: dict[str, list[PendingEmbedding]] = {}\n\n    for model_name in provider_models:\n        logger.debug(\n            f\"{task_name} considering {model_name!r} \"\n            f\"indexes via {provider_name!r}\"\n        )\n\n        pending_entries = await _get_pending_embeddings(\n            pgconn, model_name, model_excluded_ids\n        )\n\n        if not pending_entries:\n            continue\n\n        logger.debug(\n            f\"{task_name} found {len(pending_entries)} entries to index\"\n        )\n\n        try:\n            model_list = model_pending_entries[model_name]\n        except KeyError:\n            model_list = model_pending_entries[model_name] = []\n\n        model_list.extend(pending_entries)\n\n    embeddings_params: list[EmbeddingsParams] = []\n\n    for model_name, pending_entries in model_pending_entries.items():\n        embedding_model = embedding_models[model_name]\n\n        groups = itertools.groupby(\n            pending_entries, key=lambda e: e.target_dims_shortening\n        )\n        for shortening, part_iter in groups:\n            part = list(part_iter)\n            part_texts = [(p.text, p.truncate_to_max) for p in part]\n\n            batches, excluded_indexes = batch_texts(\n                part_texts,\n                get_model_tokenizer(provider_name, model_name),\n                max_input_tokens=embedding_model.max_input_tokens,\n                max_batch_tokens=embedding_model.max_batch_tokens,\n                max_batch_size=embedding_model.max_batch_size,\n            )\n\n            if excluded_indexes:\n                if model_name not in model_excluded_ids:\n                    model_excluded_ids[model_name] = []\n                model_excluded_ids[model_name].extend(\n                    part[excluded_index].id.hex\n                    for excluded_index in excluded_indexes\n                )\n\n            for batch in batches:\n                inputs = [\n                    (part[entry.input_index], entry.input_text)\n                    for entry in batch.entries\n                ]\n\n                # Sort the batches by target_rel. This groups embeddings\n                # for each table together.\n                # This is necessary for `EmbeddingsResult.finalize()`\n                inputs.sort(key=lambda e: e[0].target_rel)\n\n                embeddings_params.append(EmbeddingsParams(\n                    pgconn=pgconn,\n                    provider=provider_cfg,\n                    model_name=model_name,\n                    inputs=inputs,\n                    token_count=batch.token_count,\n                    shortening=shortening,\n                    user=None,\n                    http_client=http_client,\n                ))\n\n    return embeddings_params\n\n\n@dataclass(frozen=True, kw_only=True)\nclass TextBatchEntry:\n    input_index: int\n    input_text: str\n\n\n@dataclass(frozen=True, kw_only=True)\nclass TextBatch:\n    entries: list[TextBatchEntry]\n    token_count: int\n\n\ndef batch_texts(\n    texts: list[tuple[str, bool]],\n    tokenizer: Optional[Tokenizer],\n    *,\n    max_input_tokens: int,\n    max_batch_tokens: int,\n    max_batch_size: int | None,\n) -> tuple[list[TextBatch], list[int]]:\n    \"\"\"Given a list of texts and whether each can be truncated, produce a list\n    of valid texts to batch.\n\n    Additionally, returns a list of indexes of texts which are too long and\n    should be excluded from future embeddings requests.\n    \"\"\"\n    excluded_indexes: list[int] = []\n\n    if tokenizer:\n        input_indexes: list[int] = []\n        input_texts: list[str] = []\n\n        for index, (text, allowed_to_truncate) in enumerate(texts):\n            ensured = _ensure_text_token_length(\n                text,\n                allowed_to_truncate,\n                tokenizer,\n                max_input_tokens\n            )\n\n            if ensured is None:\n                # If the text is too long, mark it as excluded and\n                # skip.\n                excluded_indexes.append(index)\n                continue\n\n            input_indexes.append(index)\n            input_texts.append(ensured)\n\n        # Group the valid texts into batches based on token count\n        batched_inputs = _batch_embeddings_inputs(\n            tokenizer, input_texts, max_batch_tokens, max_batch_size\n        )\n\n        # Gather results\n        batches = [\n            TextBatch(\n                entries=[\n                    TextBatchEntry(\n                        input_index=input_indexes[index],\n                        input_text=input_texts[index],\n                    )\n                    for index in batch_input_indexes\n                ],\n                token_count=token_count\n            )\n            for batch_input_indexes, token_count in batched_inputs\n        ]\n\n    elif max_batch_size:\n        batch_count = (len(texts) - 1) // max_batch_size + 1\n        batches = [\n            TextBatch(\n                entries=[\n                    TextBatchEntry(\n                        input_index=index,\n                        input_text=texts[index][0],\n                    )\n                    for index in range(\n                        batch_index * max_batch_size,\n                        min((batch_index + 1) * max_batch_size, len(texts))\n                    )\n                ],\n                token_count=0,\n            )\n            for batch_index in range(batch_count)\n        ]\n\n    else:\n        batches = [\n            TextBatch(\n                entries=[\n                    TextBatchEntry(\n                        input_index=index,\n                        input_text=texts[index][0],\n                    )\n                    for index in range(len(texts))\n                ],\n                token_count=0,\n            )\n        ]\n\n    return (batches, excluded_indexes)\n\n\ndef _ensure_text_token_length(\n    text: str,\n    allowed_to_truncate: bool,\n    tokenizer: Tokenizer,\n    max_token_count: int,\n) -> Optional[str]:\n    \"\"\"Ensure text does not exceed the allowed token length.\n\n    If the text is ok, return the text unchanged.\n\n    If the text is too long and `allowed_to_truncate` is true, return the\n    text truncated.\n\n    Otherwise return None.\n    \"\"\"\n\n    truncate_length = max_token_count - tokenizer.encode_padding()\n\n    if allowed_to_truncate:\n        text, current_token_count = (\n            tokenizer.shorten_to_token_length(text, truncate_length)\n        )\n\n    else:\n        current_token_count = len(tokenizer.encode(text))\n\n        if current_token_count > truncate_length:\n            return None\n\n    return text\n\n\n@dataclass(frozen=True, kw_only=True)\nclass PendingEmbedding:\n    id: uuid.UUID\n    text: str\n    target_rel: str\n    target_attr: str\n    target_dims_shortening: Optional[int]\n    truncate_to_max: bool\n\n\nasync def _get_pending_embeddings(\n    pgconn: pgcon.PGConnection,\n    model_name: str,\n    model_excluded_ids: dict[str, list[str]],\n) -> list[PendingEmbedding]:\n    task_name = _task_name.get()\n\n    where_clause = \"\"\n    if (\n        model_name in model_excluded_ids\n        and (excluded_ids := model_excluded_ids[model_name])\n    ):\n        # Only exclude long text if it won't be auto-truncated.\n        logger.debug(\n            f\"{task_name} skipping {len(excluded_ids)} indexes \"\n            f\"for {model_name!r}\"\n        )\n        where_clause = (f\"\"\"\n            WHERE\n                q.\"id\" not in ({','.join(\n                    \"'\" + excluded_id + \"'\"\n                    for excluded_id in excluded_ids\n                )})\n                OR q.\"truncate_to_max\"\n        \"\"\")\n\n    entries = await pgconn.sql_fetch(\n        f\"\"\"\n        SELECT\n            *\n        FROM\n            (\n                SELECT\n                    \"id\",\n                    \"text\",\n                    \"target_rel\",\n                    \"target_attr\",\n                    \"target_dims_shortening\",\n                    \"truncate_to_max\"\n                FROM\n                    edgedbext.\"ai_pending_embeddings_{model_name}\"\n                LIMIT\n                    500\n            ) AS q\n        {where_clause}\n        ORDER BY\n            q.\"target_dims_shortening\"\n        \"\"\".encode(),\n        tx_isolation=edbdef.TxIsolationLevel.RepeatableRead,\n    )\n\n    if not entries:\n        return []\n\n    result = []\n    for entry in entries:\n        result.append(PendingEmbedding(\n            id=uuidgen.from_bytes(entry[0]),\n            text=entry[1].decode(\"utf-8\"),\n            target_rel=entry[2].decode(),\n            target_attr=entry[3].decode(),\n            target_dims_shortening=(\n                int.from_bytes(\n                    entry[4],\n                    byteorder=\"big\",\n                    signed=False,\n                )\n                if entry[4] is not None else\n                None\n            ),\n            truncate_to_max=bool.from_bytes(entry[5]),\n        ))\n\n    return result\n\n\ndef _batch_embeddings_inputs(\n    tokenizer: Tokenizer,\n    inputs: list[str],\n    max_batch_tokens: int,\n    max_batch_size: int | None,\n) -> list[tuple[list[int], int]]:\n    \"\"\"Create batches of embeddings inputs.\n\n    Returns batches which are a tuple of:\n    - Indexes of input strings grouped to avoid exceeding the max_batch_token\n    - The batch's token count\n    \"\"\"\n\n    # Get token counts\n    input_token_counts = [\n        len(tokenizer.encode(input))\n        for input in inputs\n    ]\n\n    # Get indexes of inputs, sorted from shortest to longest by token count\n    # Use the text itself as a tie breaker for consistency\n    unbatched_input_indexes = list(range(len(inputs)))\n    unbatched_input_indexes.sort(\n        key=lambda index: (input_token_counts[index], inputs[index]),\n        reverse=False,\n    )\n\n    def unbatched_token_count(unbatched_index: int) -> int:\n        return input_token_counts[unbatched_input_indexes[unbatched_index]]\n\n    # Remove any inputs that are larger than the maximum\n    while (\n        unbatched_input_indexes\n        and unbatched_token_count(-1) > max_batch_tokens\n    ):\n        unbatched_input_indexes.pop()\n\n    batches: list[tuple[list[int], int]] = []\n    while unbatched_input_indexes:\n        # Start with the largest available input\n        batch_input_indexes = [unbatched_input_indexes[-1]]\n        batch_token_count = unbatched_token_count(-1)\n        unbatched_input_indexes.pop()\n\n        if batch_token_count < max_batch_tokens:\n            # Then add the smallest available input as long as long as the\n            # max batch token and input counts aren't exceeded\n            unbatched_index = 0\n            while (\n                unbatched_index < len(unbatched_input_indexes)\n                and (\n                    max_batch_size is None\n                    or len(batch_input_indexes) < max_batch_size\n                )\n            ):\n                if (\n                    batch_token_count + unbatched_token_count(unbatched_index)\n                    <= max_batch_tokens\n                ):\n                    batch_input_indexes.append(\n                        unbatched_input_indexes[unbatched_index]\n                    )\n                    batch_token_count += unbatched_token_count(unbatched_index)\n                    unbatched_input_indexes.pop(unbatched_index)\n                else:\n                    unbatched_index += 1\n\n        batches.append((batch_input_indexes, batch_token_count))\n\n    return batches\n\n\nasync def _update_embeddings_in_db(\n    pgconn: pgcon.PGConnection,\n    provider_cfg: ProviderConfig,\n    rel: str,\n    attr: str,\n    ids: list[uuid.UUID],\n    embeddings: bytes,\n    offset: int,\n) -> int:\n\n    id_array = '{' + ', '.join(f'\"{id.hex}\"' for id in ids) + '}'\n    entries = await pgconn.sql_fetch_val(\n        f\"\"\"\n        WITH upd AS (\n            UPDATE {rel} AS target\n            SET\n                {attr} = (\n                    (embeddings.data)::text::edgedb.vector)\n            FROM\n                (\n                    SELECT\n                        row_number() over () AS n,\n                        j.data\n                    FROM\n                        (SELECT\n                            data\n                        FROM\n                            json_array_elements($1::json) AS data\n                        OFFSET\n                            $3::text::int\n                        ) AS j\n                ) AS embeddings,\n                unnest($2::text::text[]) WITH ORDINALITY AS ids(id, n)\n            WHERE\n                embeddings.\"n\" = ids.\"n\"\n                AND target.\"id\" = ids.\"id\"::uuid\n            RETURNING\n                target.\"id\"\n        )\n        SELECT count(*)::text FROM upd\n        \"\"\".encode(),\n        args=(\n            str(provider_cfg.get_embeddings_from_result(embeddings)).encode(),\n            id_array.encode(),\n            str(offset).encode(),\n        ),\n        tx_isolation=edbdef.TxIsolationLevel.RepeatableRead,\n    )\n\n    return int(entries.decode())\n\n\nasync def _generate_embeddings(\n    provider: ProviderConfig,\n    model_name: str,\n    inputs: list[str],\n    shortening: Optional[int],\n    user: Optional[str],\n    http_client: http.HttpClient,\n) -> EmbeddingsResult:\n    task_name = _task_name.get()\n    count = len(inputs)\n    suf = \"s\" if count > 1 else \"\"\n    logger.debug(\n        f\"{task_name} generating embeddings via {model_name!r} \"\n        f\"of {provider.name!r} for {len(inputs)} object{suf}\"\n    )\n\n    if provider.api_style == ApiStyle.OpenAI:\n        result = await _generate_openai_embeddings(\n            provider, model_name, inputs, shortening, user, http_client\n        )\n    elif provider.api_style == ApiStyle.Ollama:\n        result = await _generate_ollama_embeddings(\n            provider, model_name, inputs, shortening, http_client\n        )\n    else:\n        raise RuntimeError(\n            f\"unsupported model provider API style: {provider.api_style}, \"\n            f\"provider: {provider.name}\"\n        )\n\n    result.provider_cfg = provider\n    return result\n\n\nasync def _generate_openai_embeddings(\n    provider: ProviderConfig,\n    model_name: str,\n    inputs: list[str],\n    shortening: Optional[int],\n    user: Optional[str],\n    http_client: http.HttpClient,\n) -> EmbeddingsResult:\n\n    headers = {\n        \"Authorization\": f\"Bearer {provider.secret}\",\n    }\n    if provider.name == \"builtin::openai\" and provider.client_id:\n        headers[\"OpenAI-Organization\"] = provider.client_id\n    client = http_client.with_context(\n        headers=headers,\n        base_url=provider.api_url,\n    )\n\n    params: dict[str, Any] = {\n        \"model\": model_name,\n        \"encoding_format\": \"float\",\n        \"input\": inputs,\n    }\n    if shortening is not None:\n        params[\"dimensions\"] = shortening\n\n    if user is not None:\n        params[\"user\"] = user\n\n    result = await client.post(\n        \"/embeddings\",\n        json=params,\n    )\n\n    error = None\n    if result.status_code >= 400:\n        error = rs.Error(\n            message=(\n                f\"API call to generate embeddings failed with status \"\n                f\"{result.status_code}: {result.text}\"\n            ),\n            retry=(\n                # If the request fails with 429 - too many requests, it can be\n                # retried\n                result.status_code == 429\n            ),\n        )\n\n    return EmbeddingsResult(\n        data=(error if error else EmbeddingsData(result.bytes())),\n        limits=_read_openai_limits(result),\n    )\n\n\ndef _read_openai_header_field(\n    result: Any,\n    field_names: list[str],\n) -> Optional[int]:\n    # Return the value of the first requested field available\n    try:\n        for field_name in field_names:\n            if field_name in result.headers:\n                header_value = result.headers[field_name]\n                return int(header_value) if header_value is not None else None\n\n    except (ValueError, TypeError):\n        pass\n\n    return None\n\n\ndef _read_openai_limits(\n    result: Any,\n) -> dict[str, rs.Limits]:\n    request_limit = _read_openai_header_field(\n        result,\n        [\n            'x-ratelimit-limit-project-requests',\n            'x-ratelimit-limit-requests',\n        ],\n    )\n    request_remaining = _read_openai_header_field(\n        result,\n        [\n            'x-ratelimit-remaining-project-requests',\n            'x-ratelimit-remaining-requests',\n        ],\n    )\n\n    token_limit = _read_openai_header_field(\n        result,\n        [\n            'x-ratelimit-limit-project-tokens',\n            'x-ratelimit-limit-tokens',\n        ],\n    )\n\n    token_remaining = _read_openai_header_field(\n        result,\n        [\n            'x-ratelimit-remaining-project-tokens',\n            'x-ratelimit-remaining-tokens',\n        ],\n    )\n\n    return {\n        'requests': rs.Limits(\n            total=request_limit,\n            remaining=request_remaining,\n        ),\n        'tokens': rs.Limits(\n            total=token_limit,\n            remaining=token_remaining,\n        ),\n    }\n\n\nasync def _generate_ollama_embeddings(\n    provider: ProviderConfig,\n    model_name: str,\n    inputs: list[str],\n    shortening: Optional[int],\n    http_client: http.HttpClient,\n) -> EmbeddingsResult:\n\n    headers: dict[str, str] = {}\n    client = http_client.with_context(\n        headers=headers,\n        base_url=provider.api_url,\n    )\n\n    params: dict[str, Any] = {\n        \"model\": model_name,\n        \"input\": inputs,\n    }\n    if shortening is not None:\n        params[\"dimensions\"] = shortening\n\n    result = await client.post(\n        \"/embed\",\n        json=params,\n    )\n\n    error = None\n    if result.status_code >= 400:\n        error = rs.Error(\n            message=(\n                f\"API call to generate embeddings failed with status \"\n                f\"{result.status_code}: {result.text}\"\n            ),\n            retry=(\n                # If the request fails with 429 - too many requests, it can be\n                # retried\n                result.status_code == 429\n            ),\n        )\n\n    return EmbeddingsResult(\n        data=(error if error else EmbeddingsData(result.bytes())),\n        limits=_ollama_limits(),\n    )\n\n\ndef _ollama_limits() -> dict[str, rs.Limits]:\n    return {\n        'requests': rs.Limits(\n            total='unlimited',\n            remaining=None,\n        ),\n        'tokens': rs.Limits(\n            total='unlimited',\n            remaining=None,\n        ),\n    }\n\n\nasync def _start_chat(\n    *,\n    protocol: protocol.HttpProtocol,\n    request: protocol.HttpRequest,\n    response: protocol.HttpResponse,\n    provider: ProviderConfig,\n    http_client: http.HttpClient,\n    model_name: str,\n    messages: list[dict[str, Any]],\n    stream: bool,\n    temperature: Optional[float],\n    top_p: Optional[float],\n    max_tokens: Optional[int],\n    seed: Optional[int],\n    safe_prompt: Optional[bool],\n    top_k: Optional[int],\n    logit_bias: Optional[dict[int, int]],\n    logprobs: Optional[bool],\n    user: Optional[str],\n    tools: Optional[list[dict[str, Any]]],\n) -> None:\n    if provider.api_style == ApiStyle.OpenAI:\n        await _start_openai_chat(\n            protocol=protocol,\n            request=request,\n            response=response,\n            provider=provider,\n            http_client=http_client,\n            model_name=model_name,\n            messages=messages,\n            stream=stream,\n            temperature=temperature,\n            top_p=top_p,\n            max_tokens=max_tokens,\n            seed=seed,\n            safe_prompt=safe_prompt,\n            logit_bias=logit_bias,\n            logprobs=logprobs,\n            user=user,\n            tools=tools,\n        )\n    elif provider.api_style == ApiStyle.Anthropic:\n        await _start_anthropic_chat(\n            protocol=protocol,\n            request=request,\n            response=response,\n            provider=provider,\n            http_client=http_client,\n            model_name=model_name,\n            messages=messages,\n            stream=stream,\n            temperature=temperature,\n            top_p=top_p,\n            top_k=top_k,\n            tools=tools,\n            max_tokens=max_tokens,\n        )\n    elif provider.api_style == ApiStyle.Ollama:\n        await _start_ollama_chat(\n            protocol=protocol,\n            request=request,\n            response=response,\n            provider=provider,\n            http_client=http_client,\n            model_name=model_name,\n            messages=messages,\n            stream=stream,\n            temperature=temperature,\n            top_p=top_p,\n            top_k=top_k,\n            tools=tools,\n        )\n    else:\n        raise RuntimeError(\n            f\"unsupported model provider API style: {provider.api_style}, \"\n            f\"provider: {provider.name}\"\n        )\n\n\n@contextlib.asynccontextmanager\nasync def aconnect_sse(\n    client: http.HttpClient,\n    method: str,\n    url: str,\n    **kwargs: Any,\n) -> AsyncIterator[http.ResponseSSE]:\n    headers = kwargs.pop(\"headers\", {})\n    headers[\"Accept\"] = \"text/event-stream\"\n    headers[\"Cache-Control\"] = \"no-store\"\n\n    stm = await client.stream_sse(\n        method=method,\n        path=url,\n        headers=headers,\n        **kwargs\n    )\n    if isinstance(stm, http.Response):\n        raise AIProviderError(\n            f\"API call to generate chat completions failed with status \"\n            f\"{stm.status_code}: {stm.text}\"\n        )\n    async with stm as response:\n        if response.status_code >= 400:\n            # Unlikely that we have a streaming response with a non-200 result\n            raise AIProviderError(\n                f\"API call to generate chat completions failed with status \"\n                f\"{response.status_code}\"\n            )\n        yield response\n\n\nasync def _start_openai_like_chat(\n    *,\n    protocol: protocol.HttpProtocol,\n    request: protocol.HttpRequest,\n    response: protocol.HttpResponse,\n    provider_name: str,\n    client: http.HttpClient,\n    model_name: str,\n    messages: list[dict[str, Any]],\n    stream: bool,\n    temperature: Optional[float],\n    top_p: Optional[float],\n    max_tokens: Optional[int],\n    seed: Optional[int],\n    safe_prompt: Optional[bool],\n    logit_bias: Optional[dict[int, int]],\n    logprobs: Optional[bool],\n    user: Optional[str],\n    tools: Optional[list[dict[str, Any]]],\n) -> None:\n    isOpenAI = provider_name == \"builtin::openai\"\n\n    params: dict[str, Any] = {\n        \"model\": model_name,\n        \"messages\": messages,\n    }\n    if temperature is not None:\n        params[\"temperature\"] = temperature\n    if top_p is not None:\n        params[\"top_p\"] = top_p\n    if tools is not None:\n        params[\"tools\"] = tools\n    if isOpenAI and logit_bias is not None:\n        params[\"logit_bias\"] = logit_bias\n    if isOpenAI and logprobs is not None:\n        params[\"logprobs\"] = logprobs\n    if isOpenAI and user is not None:\n        params[\"user\"] = user\n    if not isOpenAI and safe_prompt is not None:\n        params[\"safe_prompt\"] = safe_prompt\n    if max_tokens is not None:\n        if isOpenAI:\n            params[\"max_completion_tokens\"] = max_tokens\n        else:\n            params[\"max_tokens\"] = max_tokens\n    if seed is not None:\n        if isOpenAI:\n            params[\"seed\"] = seed\n        else:\n            params[\"random_seed\"] = seed\n\n    if stream:\n        async with aconnect_sse(\n            client,\n            method=\"POST\",\n            url=\"/chat/completions\",\n            json={\n                **params,\n                \"stream\": True,\n            }\n        ) as event_source:\n            # we need tool_index and finish_reason to correctly\n            # send 'content_block_stop' chunk for tool call messages\n            tool_index = 0\n            finish_reason = \"unknown\"\n\n            async for sse in event_source:\n                if not response.sent:\n                    response.status = http.HTTPStatus.OK\n                    response.content_type = b'text/event-stream'\n                    response.close_connection = False\n                    response.custom_headers[\"Cache-Control\"] = \"no-cache\"\n                    protocol.write(request, response)\n\n                if sse.event != \"message\":\n                    continue\n\n                if sse.data == \"[DONE]\":\n                    # mistral doesn't send finish_reason for tool calls\n                    if finish_reason == \"unknown\":\n                        event = (\n                            b'event: content_block_stop\\n'\n                            + b'data: {\"type\": \"content_block_stop\",'\n                            + b'\"index\": ' + str(tool_index).encode() + b'}\\n\\n'\n                        )\n                        protocol.write_raw(event)\n                    event = (\n                        b'event: message_stop\\n'\n                        + b'data: {\"type\": \"message_stop\"}\\n\\n'\n                    )\n                    protocol.write_raw(event)\n                    break\n\n                message = sse.json()\n                if message.get(\"object\") == \"chat.completion.chunk\":\n                    data = message.get(\"choices\")[0]\n                    delta = data.get(\"delta\")\n                    role = delta.get(\"role\")\n                    tool_calls = delta.get(\"tool_calls\")\n\n                    if role:\n                        event_data = json.dumps({\n                            \"type\": \"message_start\",\n                            \"message\": {\n                                \"id\": message[\"id\"],\n                                \"role\": role,\n                                \"model\": message[\"model\"],\n                                \"usage\": message.get(\"usage\")\n                            },\n                        }).encode(\"utf-8\")\n                        event = (\n                            b'event: message_start\\n'\n                            + b'data: ' + event_data + b'\\n\\n'\n                        )\n                        protocol.write_raw(event)\n\n                        event_data = json.dumps({\n                            \"type\": \"content_block_start\",\n                            \"index\": 0,\n                            \"content_block\": {\n                                \"type\": \"text\",\n                                \"text\": \"\"\n                            }\n                        }).encode(\"utf-8\")\n                        event = (\n                            b'event: content_block_start\\n'\n                            + b'data: ' + event_data + b'\\n\\n'\n                        )\n                        protocol.write_raw(event)\n\n                        # if there's only one openai tool call it shows up here\n                        if tool_calls:\n                            for tool_call in tool_calls:\n                                tool_index = tool_call[\"index\"]\n                                event_data = json.dumps({\n                                    \"type\": \"content_block_start\",\n                                    \"index\": tool_call[\"index\"] + 1,\n                                    \"content_block\": {\n                                        \"id\": tool_call[\"id\"],\n                                        \"type\": \"tool_use\",\n                                        \"name\": tool_call[\"function\"][\"name\"],\n                                        \"args\":\n                                        tool_call[\"function\"][\"arguments\"],\n                                    },\n                                }).encode(\"utf-8\")\n\n                                event = (\n                                    b'event: content_block_start\\n'\n                                    + b'data:' + event_data + b'\\n\\n'\n                                )\n                                protocol.write_raw(event)\n                    # if there are few openai tool calls, they show up here\n                    # mistral tool calls always show up here\n                    elif tool_calls:\n                        # OpenAI provides index, Mistral doesn't\n                        for index, tool_call in enumerate(tool_calls):\n                            currentIndex = tool_call.get(\"index\") or index\n                            if tool_call.get(\"type\") == \"function\" or \\\n                            \"id\" in tool_call:\n                                if currentIndex > 0:\n                                    tool_index = currentIndex\n                                    # send the stop chunk for the previous tool\n                                    event = (\n                                        b'event: content_block_stop\\n'\n                                        + b'data: { \\\n                                        \"type\": \"content_block_stop\",'\n                                        + b'\"index\": '\n                                        + str(currentIndex).encode()\n                                        + b'}\\n\\n'\n                                    )\n                                    protocol.write_raw(event)\n\n                                event_data = json.dumps({\n                                    \"type\": \"content_block_start\",\n                                    \"index\": currentIndex + 1,\n                                    \"content_block\": {\n                                        \"id\": tool_call.get(\"id\"),\n                                        \"type\": \"tool_use\",\n                                        \"name\": tool_call[\"function\"][\"name\"],\n                                        \"args\":\n                                        tool_call[\"function\"][\"arguments\"],\n                                    },\n                                }).encode(\"utf-8\")\n\n                                event = (\n                                    b'event: content_block_start\\n'\n                                    + b'data:' + event_data + b'\\n\\n'\n                                )\n                                protocol.write_raw(event)\n                            else:\n                                event_data = json.dumps({\n                                        \"type\": \"content_block_delta\",\n                                        \"index\": currentIndex + 1,\n                                        \"delta\": {\n                                            \"type\": \"tool_call_delta\",\n                                            \"args\":\n                                             tool_call[\"function\"][\"arguments\"],\n                                        },\n                                    }).encode(\"utf-8\")\n                                event = (\n                                    b'event: content_block_delta\\n'\n                                    + b'data:' + event_data + b'\\n\\n'\n                                )\n                                protocol.write_raw(event)\n                    elif finish_reason := data.get(\"finish_reason\"):\n                        index = (\n                            tool_index + 1\n                            if finish_reason == \"tool_calls\"\n                            else 0\n                        )\n                        event = (\n                            b'event: content_block_stop\\n'\n                            + b'data: {\"type\": \"content_block_stop\",'\n                            + b'\"index\": ' + str(index).encode() + b'}\\n\\n'\n                        )\n                        protocol.write_raw(event)\n\n                        event_data = json.dumps({\n                            \"type\": \"message_delta\",\n                            \"delta\": {\n                                \"stop_reason\": finish_reason,\n                            },\n                            \"usage\": message.get(\"usage\")\n                        }).encode(\"utf-8\")\n                        event = (\n                            b'event: message_delta\\n'\n                            + b'data: ' + event_data + b'\\n\\n'\n                        )\n                        protocol.write_raw(event)\n\n                    else:\n                        event_data = json.dumps({\n                            \"type\": \"content_block_delta\",\n                            \"index\": 0,\n                             \"delta\": {\n                                \"type\": \"text_delta\",\n                                \"text\": delta.get(\"content\"),\n                            },\n                            \"logprobs\": data.get(\"logprobs\"),\n                        }).encode(\"utf-8\")\n\n                        event = (\n                            b'event: content_block_delta\\n'\n                            + b'data:' + event_data + b'\\n\\n'\n                        )\n                        protocol.write_raw(event)\n\n            protocol.close()\n    else:\n        result = await client.post(\n            \"/chat/completions\",\n            json={\n                **params\n            }\n        )\n\n        if result.status_code >= 400:\n            raise AIProviderError(\n                f\"API call to generate chat completions failed with status \"\n                f\"{result.status_code}: {result.text}\"\n            )\n\n        response.status = http.HTTPStatus.OK\n\n        result_data = result.json()\n        choice = result_data[\"choices\"][0]\n        tool_calls = choice[\"message\"].get(\"tool_calls\")\n        tool_calls_formatted = [\n            {\n                \"id\": tool_call[\"id\"],\n                \"type\": tool_call[\"type\"],\n                \"name\": tool_call[\"function\"][\"name\"],\n                \"args\": json.loads(tool_call[\"function\"][\"arguments\"]),\n            }\n            for tool_call in tool_calls or []\n        ]\n\n        body = {\n            \"id\": result_data[\"id\"],\n            \"model\": result_data[\"model\"],\n            \"text\": choice[\"message\"][\"content\"],\n            \"finish_reason\": choice.get(\"finish_reason\"),\n            \"usage\": result_data.get(\"usage\"),\n            \"logprobs\": choice.get(\"logprobs\"),\n            \"tool_calls\": tool_calls_formatted,\n        }\n        response.content_type = b'application/json'\n        response.body = json.dumps(body).encode(\"utf-8\")\n\n\nasync def _start_openai_chat(\n    *,\n    protocol: protocol.HttpProtocol,\n    request: protocol.HttpRequest,\n    response: protocol.HttpResponse,\n    provider: ProviderConfig,\n    http_client: http.HttpClient,\n    model_name: str,\n    messages: list[dict[str, Any]],\n    stream: bool,\n    temperature: Optional[float],\n    top_p: Optional[float],\n    max_tokens: Optional[int],\n    seed: Optional[int],\n    safe_prompt: Optional[bool],\n    logit_bias: Optional[dict[int, int]],\n    logprobs: Optional[bool],\n    user: Optional[str],\n    tools: Optional[list[dict[str, Any]]],\n) -> None:\n    headers = {\n        \"Authorization\": f\"Bearer {provider.secret}\",\n    }\n\n    if provider.name == \"builtin::openai\" and provider.client_id:\n        headers[\"OpenAI-Organization\"] = provider.client_id\n\n    client = http_client.with_context(\n        base_url=provider.api_url,\n        headers=headers,\n    )\n\n    await _start_openai_like_chat(\n        protocol=protocol,\n        request=request,\n        response=response,\n        provider_name=provider.name,\n        client=client,\n        model_name=model_name,\n        messages=messages,\n        stream=stream,\n        temperature=temperature,\n        top_p=top_p,\n        max_tokens=max_tokens,\n        seed=seed,\n        safe_prompt=safe_prompt,\n        logit_bias=logit_bias,\n        logprobs=logprobs,\n        user=user,\n        tools=tools,\n    )\n\n\n# Anthropic differs from OpenAI and Mistral as there's no tool chunk:\n# tool_call(tool_use) is part of the assistant chunk, and\n# tool_result is part of the user chunk.\nasync def _start_anthropic_chat(\n    *,\n    protocol: protocol.HttpProtocol,\n    request: protocol.HttpRequest,\n    response: protocol.HttpResponse,\n    provider: ProviderConfig,\n    http_client: http.HttpClient,\n    model_name: str,\n    messages: list[dict[str, Any]],\n    stream: bool,\n    temperature: Optional[float],\n    top_p: Optional[float],\n    top_k: Optional[int],\n    tools: Optional[list[dict[str, Any]]],\n    max_tokens: Optional[int],\n) -> None:\n    headers = {\n        \"x-api-key\": f\"{provider.secret}\",\n    }\n\n    if provider.name == \"builtin::anthropic\":\n        headers[\"anthropic-version\"] = \"2023-06-01\"\n        headers[\"anthropic-beta\"] = \"messages-2023-12-15\"\n\n    client = http_client.with_context(\n        headers={\n            \"anthropic-version\": \"2023-06-01\",\n            \"anthropic-beta\": \"messages-2023-12-15\",\n            \"x-api-key\": f\"{provider.secret}\",\n        },\n        base_url=provider.api_url,\n    )\n\n    anthropic_messages = []\n    system_prompt_parts = []\n\n    for message in messages:\n        if message[\"role\"] == \"system\":\n            system_prompt_parts.append(message[\"content\"])\n\n        elif message[\"role\"] == \"assistant\" and \"tool_calls\" in message:\n            # Anthropic fails when an assistant chunk has multiple tool calls\n            # and is followed by several tool_result chunks (or a user chunk\n            # with multiple tool_results). Each assistant chunk should have\n            # only 1 tool_use, followed by 1 tool_result chunk.\n            for tool_call in message[\"tool_calls\"]:\n                msg = {\n                    \"role\": \"assistant\",\n                    \"content\": [\n                        {\n                            \"id\": tool_call[\"id\"],\n                            \"type\": \"tool_use\",\n                            \"name\": tool_call[\"function\"][\"name\"],\n                            \"input\": json.loads(\n                                tool_call[\"function\"][\"arguments\"]),\n                        }\n                    ],\n                }\n                anthropic_messages.append(msg)\n        # Check if message is a tool result\n        elif message[\"role\"] == \"tool\":\n            tool_result = {\n                \"role\": \"user\",\n                \"content\": [\n                    {\n                        \"type\": \"tool_result\",\n                        \"tool_use_id\": message[\"tool_call_id\"],\n                        \"content\": message[\"content\"]\n                    }\n                ],\n            }\n            anthropic_messages.append(tool_result)\n        else:\n            anthropic_messages.append(message)\n\n    system_prompt = \"\\n\".join(system_prompt_parts)\n\n    # Each tool_use chunk must be followed by a matching tool_result chunk\n    reordered_messages = []\n\n    # Separate tool_result messages by tool_use_id for faster access\n    tool_result_map = {\n        item[\"content\"][0][\"tool_use_id\"]: item\n        for item in anthropic_messages\n        if item[\"role\"] == \"user\" and isinstance(item[\"content\"][0], dict)\n          and item[\"content\"][0][\"type\"] == \"tool_result\"\n    }\n\n    for message in anthropic_messages:\n        if message[\"role\"] == \"assistant\":\n            reordered_messages.append(message)\n            if isinstance(message[\"content\"], list):\n                for item in message[\"content\"]:\n                    if item[\"type\"] == \"tool_use\":\n                        # find the matching user tool_result message\n                        tool_use_id = item[\"id\"]\n                        if tool_use_id in tool_result_map:\n                            reordered_messages.append(\n                                tool_result_map[tool_use_id])\n        # append user message that is not tool_result\n        elif not (message[\"role\"] == \"user\"\n                  and isinstance(message[\"content\"][0], dict)\n                  and message[\"content\"][0][\"type\"] == \"tool_result\"):\n            reordered_messages.append(message)\n\n    params = {\n        \"model\": model_name,\n        \"messages\": reordered_messages,\n        \"system\": system_prompt,\n        **({\"temperature\": temperature} if temperature is not None else {}),\n        **({\"top_p\": top_p} if top_p is not None else {}),\n        **{\"max_tokens\": max_tokens if max_tokens is not None else 4096},\n        **({\"top_k\": top_k} if top_k is not None else {}),\n        **({\"tools\": tools} if tools is not None else {}),\n    }\n\n    if stream:\n        async with aconnect_sse(\n            client,\n            method=\"POST\",\n            url=\"/messages\",\n            json={\n                **params,\n                \"stream\": True,\n            }\n        ) as event_source:\n            tool_index = 0\n            async for sse in event_source:\n                if not response.sent:\n                    response.status = http.HTTPStatus.OK\n                    response.content_type = b'text/event-stream'\n                    response.close_connection = False\n                    response.custom_headers[\"Cache-Control\"] = \"no-cache\"\n                    protocol.write(request, response)\n\n                if sse.event == \"message_start\":\n                    message = sse.json()[\"message\"]\n                    for k in tuple(message):\n                        if k not in {\"id\", \"type\", \"role\", \"model\", \"usage\"}:\n                            del message[k]\n                    message[\"usage\"] = {\n                        \"prompt_tokens\": message[\"usage\"][\"input_tokens\"],\n                        \"completion_tokens\": message[\"usage\"][\"output_tokens\"]\n                    }\n                    message_data = json.dumps(message).encode(\"utf-8\")\n                    event = (\n                        b'event: message_start\\n'\n                        + b'data: {\"type\": \"message_start\",'\n                        + b'\"message\":' + message_data + b'}\\n\\n'\n                    )\n                    protocol.write_raw(event)\n\n                elif sse.event == \"content_block_start\":\n                    sse_data = json.loads(sse.data)\n                    protocol.write_raw(\n                        b'event: content_block_start\\n'\n                        + b'data: ' + json.dumps(sse_data).encode(\"utf-8\")\n                        + b'\\n\\n'\n                    )\n                    # we don't send content_block_stop event when text\n                    # chunk ends, should be okay since we don't consume\n                    # this event on the client side\n                    data = sse.json()\n                    if (\n                        \"content_block\" in data\n                        and data[\"content_block\"].get(\"type\") == \"tool_use\"\n                    ):\n                        currentIndex = data[\"index\"]\n                        if currentIndex > 0:\n                            tool_index = currentIndex\n                            event_data = json.dumps({\n                                \"type\": \"content_block_stop\",\n                                \"index\": currentIndex - 1})\n                            protocol.write_raw(\n                                b'event: content_block_stop\\n'\n                                + b'data: ' + event_data.encode(\"utf-8\")\n                                + b'\\n\\n'\n                            )\n                elif sse.event == \"content_block_delta\":\n                    message = sse.json()\n                    # it is always dict irl but test is failing\n                    delta = message.get(\"delta\")\n                    if delta and delta.get(\"type\") == \"input_json_delta\":\n                        delta[\"type\"] = \"tool_call_delta\"\n\n                    if delta and \"partial_json\" in delta:\n                        delta[\"args\"] = delta.pop(\"partial_json\")\n\n                    event_data = json.dumps(message)\n                    event = (\n                        b'event: content_block_delta\\n'\n                        + b'data: ' + event_data.encode(\"utf-8\") + b'\\n\\n'\n                    )\n                    protocol.write_raw(event)\n                elif sse.event == \"message_delta\":\n                    message = sse.json()\n                    if message[\"delta\"][\"stop_reason\"] == \"tool_use\":\n                        event = (\n                            b'event: content_block_stop\\n'\n                            + b'data: {\"type\": \"content_block_stop\",'\n                            + b'\"index\": '\n                            + str(tool_index).encode(\"utf-8\")\n                            + b'}\\n\\n'\n                        )\n                        protocol.write_raw(event)\n\n                    event_data = json.dumps({\n                            \"type\": \"message_delta\",\n                            \"delta\": message[\"delta\"],\n                            \"usage\": {\"completion_tokens\":\n                                      message[\"usage\"][\"output_tokens\"]}\n                    })\n                    event = (\n                            b'event: message_delta\\n'\n                            + b'data: ' + event_data.encode(\"utf-8\") + b'\\n\\n'\n                        )\n\n                    protocol.write_raw(event)\n                elif sse.event == \"message_stop\":\n                    event = (\n                        b'event: message_stop\\n'\n                        + b'data: {\"type\": \"message_stop\"}\\n\\n'\n                    )\n                    protocol.write_raw(event)\n                    # needed because stream doesn't close itself\n                    protocol.close()\n            protocol.close()\n    else:\n        result = await client.post(\n            \"/messages\",\n            json={\n                **params\n            }\n        )\n        if result.status_code >= 400:\n            raise AIProviderError(\n                f\"API call to generate chat completions failed with status \"\n                f\"{result.status_code}: {result.text}\"\n            )\n\n        response.status = http.HTTPStatus.OK\n        response.content_type = b'application/json'\n\n        result_data = result.json()\n        tool_calls = [\n            item\n            for item in result_data[\"content\"]\n            if item.get(\"type\") == \"tool_use\"\n        ]\n        tool_calls_formatted = [\n            {\n                \"id\": tool_call[\"id\"],\n                \"type\": \"function\",\n                \"name\": tool_call[\"name\"],\n                \"args\": tool_call[\"input\"],\n            }\n            for tool_call in tool_calls\n        ]\n\n        body = {\n            \"id\": result_data[\"id\"],\n            \"model\": result_data[\"model\"],\n            \"text\": next((item[\"text\"]\n                          for item in result_data[\"content\"]\n                          if item.get(\"type\") == \"text\"), \"\"),\n            \"finish_reason\": result_data[\"stop_reason\"],\n             \"usage\": {\n                \"prompt_tokens\": result_data[\"usage\"][\"input_tokens\"],\n                \"completion_tokens\": result_data[\"usage\"][\"output_tokens\"]\n            },\n            \"tool_calls\": tool_calls_formatted,\n        }\n\n        response.body = json.dumps(\n            body\n        ).encode(\"utf-8\")\n\n\nasync def _start_ollama_chat(\n    *,\n    protocol: protocol.HttpProtocol,\n    request: protocol.HttpRequest,\n    response: protocol.HttpResponse,\n    provider: ProviderConfig,\n    http_client: http.HttpClient,\n    model_name: str,\n    messages: list[dict[str, Any]],\n    stream: bool,\n    temperature: Optional[float],\n    top_p: Optional[float],\n    top_k: Optional[int],\n    tools: Optional[list[dict[str, Any]]],\n) -> None:\n\n    # The default API doesn't produce a SSE stream. Use the experimental\n    # OpenAI-like API if we need a stream.\n    base_url = provider.api_url\n    if stream:\n        base_url = '/'.join(\n            base_url.split('/')[:-1] + ['v1']\n        )\n\n    # Generate params differently for stream and non-stream modes since they\n    # use different APIs.\n    options = {\n        **({\"temperature\": temperature} if temperature is not None else {}),\n        **({\"top_p\": top_p} if top_p is not None else {}),\n        **({\"top_k\": top_k} if top_k is not None else {}),\n    }\n\n    if stream:\n        params = {\n            \"model\": model_name,\n            \"messages\": messages,\n            \"options\": options,\n        }\n\n        # Only include tools in streaming params if no tool messages are\n        # provided.\n        if tools is not None and not any(\n            message[\"role\"] == \"tool\"\n            for message in messages\n        ):\n            params[\"tools\"] = tools\n\n    else:\n        converted_messages = []\n        for message in messages:\n            if message[\"role\"] == \"user\":\n                # Ollama can't handle content block messages.\n                # Unpack them into separate messages.\n                if isinstance(message[\"content\"], str):\n                    converted_messages.append(message)\n                else:\n                    # array of content blocks\n                    for block in message[\"content\"]:\n                        if block[\"type\"] != \"text\":\n                            raise TypeError(\n                                f\"Unsupported content type: '{block[\"type\"]}'. \"\n                                f\"For non-text content, use streamed mode.\"\n                            )\n                        converted_messages.append({\n                            \"role\": message[\"role\"],\n                            \"content\": block[\"text\"],\n                        })\n\n            elif message[\"role\"] == \"assistant\":\n                # Gel http API packs arguments into a string, but ollama\n                # requires plain json.\n                converted_messages.append({\n                    \"role\": message[\"role\"],\n                    \"content\": message[\"content\"],\n                    \"tool_calls\": [\n                        {\n                            \"id\": tool_call[\"id\"],\n                            \"function\": {\n                                \"name\": tool_call[\"function\"][\"name\"],\n                                \"arguments\": json.loads(\n                                    tool_call[\"function\"][\"arguments\"]\n                                ),\n                            }\n                        }\n                        for tool_call in message[\"tool_calls\"]\n                    ]\n                })\n\n            else:\n                converted_messages.append(message)\n\n        params = {\n            \"model\": model_name,\n            \"messages\": converted_messages,\n            \"options\": options,\n            **({\"tools\": tools} if tools is not None else {}),\n        }\n\n    client = http_client.with_context(\n        headers={},\n        base_url=base_url,\n    )\n\n    if stream:\n        async with aconnect_sse(\n            client,\n            method=\"POST\",\n            url=\"/chat/completions\",\n            json={\n                **params,\n                \"stream\": True,\n            }\n        ) as event_source:\n            # we need tool_index and finish_reason to correctly\n            # send 'content_block_stop' chunk for tool call messages\n            tool_index = 0\n            finish_reason: Optional[str] = \"unknown\"\n\n            started = False\n\n            async for sse in event_source:\n                if not response.sent:\n                    response.status = http.HTTPStatus.OK\n                    response.content_type = b'text/event-stream'\n                    response.close_connection = False\n                    response.custom_headers[\"Cache-Control\"] = \"no-cache\"\n                    protocol.write(request, response)\n\n                if sse.event != \"message\":\n                    continue\n\n                if sse.data == \"[DONE]\":\n                    # mistral doesn't send finish_reason for tool calls\n                    if finish_reason == \"unknown\":\n                        event = (\n                            b'event: content_block_stop\\n'\n                            + b'data: {\"type\": \"content_block_stop\",'\n                            + b'\"index\": ' + str(tool_index).encode() + b'}\\n\\n'\n                        )\n                        protocol.write_raw(event)\n                    event = (\n                        b'event: message_stop\\n'\n                        + b'data: {\"type\": \"message_stop\"}\\n\\n'\n                    )\n                    protocol.write_raw(event)\n                    break\n\n                message = sse.json()\n                if message.get(\"object\") == \"chat.completion.chunk\":\n                    choices = message.get(\"choices\")\n                    data = choices[0] if choices else {}\n                    delta = data.get(\"delta\") or {}\n                    role = delta.get(\"role\")\n                    tool_calls = delta.get(\"tool_calls\")\n\n                    # Unlike OpenAI, Ollama includes the role in every event.\n                    # Just create a new start event.\n                    if not started:\n                        event_data = json.dumps({\n                            \"type\": \"message_start\",\n                            \"message\": {\n                                \"id\": message[\"id\"],\n                                \"role\": role,\n                                \"model\": message[\"model\"],\n                                \"usage\": message.get(\"usage\")\n                            },\n                        }).encode(\"utf-8\")\n                        event = (\n                            b'event: message_start\\n'\n                            + b'data: ' + event_data + b'\\n\\n'\n                        )\n                        protocol.write_raw(event)\n\n                        event_data = json.dumps({\n                            \"type\": \"content_block_start\",\n                            \"index\": 0,\n                            \"content_block\": {\n                                \"type\": \"text\",\n                                \"text\": \"\"\n                            }\n                        }).encode(\"utf-8\")\n                        event = (\n                            b'event: content_block_start\\n'\n                            + b'data: ' + event_data + b'\\n\\n'\n                        )\n                        protocol.write_raw(event)\n                        started = True\n\n                    if tool_calls:\n                        for index, tool_call in enumerate(tool_calls):\n                            currentIndex = tool_call.get(\"index\") or index\n                            if tool_call.get(\"type\") == \"function\" or \\\n                            \"id\" in tool_call:\n                                if currentIndex > 0:\n                                    tool_index = currentIndex\n                                    # send the stop chunk for the previous tool\n                                    event = (\n                                        b'event: content_block_stop\\n'\n                                        + b'data: { \\\n                                        \"type\": \"content_block_stop\",'\n                                        + b'\"index\": '\n                                        + str(currentIndex).encode()\n                                        + b'}\\n\\n'\n                                    )\n                                    protocol.write_raw(event)\n\n                                event_data = json.dumps({\n                                    \"type\": \"content_block_start\",\n                                    \"index\": currentIndex + 1,\n                                    \"content_block\": {\n                                        \"id\": tool_call.get(\"id\"),\n                                        \"type\": \"tool_use\",\n                                        \"name\": tool_call[\"function\"][\"name\"],\n                                        \"args\":\n                                        tool_call[\"function\"][\"arguments\"],\n                                    },\n                                }).encode(\"utf-8\")\n\n                                event = (\n                                    b'event: content_block_start\\n'\n                                    + b'data:' + event_data + b'\\n\\n'\n                                )\n                                protocol.write_raw(event)\n                            else:\n                                event_data = json.dumps({\n                                        \"type\": \"content_block_delta\",\n                                        \"index\": currentIndex + 1,\n                                        \"delta\": {\n                                            \"type\": \"tool_call_delta\",\n                                            \"args\":\n                                             tool_call[\"function\"][\"arguments\"],\n                                        },\n                                    }).encode(\"utf-8\")\n                                event = (\n                                    b'event: content_block_delta\\n'\n                                    + b'data:' + event_data + b'\\n\\n'\n                                )\n                                protocol.write_raw(event)\n                    elif finish_reason := data.get(\"finish_reason\"):\n                        index = (\n                            tool_index + 1\n                            if finish_reason == \"tool_calls\"\n                            else 0\n                        )\n                        event = (\n                            b'event: content_block_stop\\n'\n                            + b'data: {\"type\": \"content_block_stop\",'\n                            + b'\"index\": ' + str(index).encode() + b'}\\n\\n'\n                        )\n                        protocol.write_raw(event)\n\n                        event_data = json.dumps({\n                            \"type\": \"message_delta\",\n                            \"delta\": {\n                                \"stop_reason\": finish_reason,\n                            },\n                            \"usage\": message.get(\"usage\")\n                        }).encode(\"utf-8\")\n                        event = (\n                            b'event: message_delta\\n'\n                            + b'data: ' + event_data + b'\\n\\n'\n                        )\n                        protocol.write_raw(event)\n\n                    else:\n                        event_data = json.dumps({\n                            \"type\": \"content_block_delta\",\n                            \"index\": 0,\n                             \"delta\": {\n                                \"type\": \"text_delta\",\n                                \"text\": delta.get(\"content\"),\n                            },\n                            \"logprobs\": data.get(\"logprobs\"),\n                        }).encode(\"utf-8\")\n\n                        event = (\n                            b'event: content_block_delta\\n'\n                            + b'data:' + event_data + b'\\n\\n'\n                        )\n                        protocol.write_raw(event)\n\n            protocol.close()\n    else:\n        result = await client.post(\n            \"/chat\",\n            json={\n                **params,\n                \"stream\": False\n            }\n        )\n        if result.status_code >= 400:\n            raise AIProviderError(\n                f\"API call to generate chat completions failed with status \"\n                f\"{result.status_code}: {result.text}\"\n            )\n\n        response.status = http.HTTPStatus.OK\n        response.content_type = b'application/json'\n\n        result_data = result.json()\n\n        tool_calls = result_data[\"message\"].get(\"tool_calls\")\n        tool_calls_formatted = [\n            {\n                # Ollama does not provide tool call ids for non-stream.\n                # Use enumerate to generate a placeholder.\n                \"id\": f\"call_{tool_call_id}\",\n                # Ollama doesn't provide the tool call type but it should\n                # always be 'function'.\n                \"type\": \"function\",\n                \"name\": tool_call[\"function\"][\"name\"],\n                \"args\": tool_call[\"function\"][\"arguments\"],\n            }\n            for tool_call_id, tool_call in enumerate(tool_calls or [])\n        ]\n\n        body = {\n            \"model\": result_data[\"model\"],\n            \"text\": result_data[\"message\"][\"content\"],\n            \"finish_reason\": result_data[\"done_reason\"],\n            \"usage\": {\n                \"prompt_tokens\": result_data[\"prompt_eval_count\"],\n                \"completion_tokens\": result_data[\"eval_count\"]\n            },\n            \"tool_calls\": tool_calls_formatted,\n        }\n\n        # Ollama has no documented tools response\n\n        response.body = json.dumps(\n            body\n        ).encode(\"utf-8\")\n\n\n#\n# HTTP API\n#\n\nasync def handle_request(\n    protocol: protocol.HttpProtocol,\n    request: protocol.HttpRequest,\n    response: protocol.HttpResponse,\n    db: dbview.Database,\n    role_name: str,\n    args: list[str],\n    tenant: srv_tenant.Tenant,\n) -> None:\n    if len(args) != 1 or args[0] not in {\"rag\", \"embeddings\"}:\n        response.body = b'Unknown path'\n        response.status = http.HTTPStatus.NOT_FOUND\n        response.close_connection = True\n        return\n    if request.method != b\"POST\":\n        response.body = b\"Invalid request method\"\n        response.status = http.HTTPStatus.METHOD_NOT_ALLOWED\n        response.close_connection = True\n        return\n    if request.content_type != b\"application/json\":\n        response.body = b\"Expected application/json input\"\n        response.status = http.HTTPStatus.BAD_REQUEST\n        response.close_connection = True\n        return\n\n    await db.introspection()\n\n    try:\n        if args[0] == \"rag\":\n            await _handle_rag_request(\n                protocol, request, response, db, role_name, tenant\n            )\n        elif args[0] == \"embeddings\":\n            await _handle_embeddings_request(\n                request, response, db, role_name, tenant\n            )\n        else:\n            response.body = b'Unknown path'\n            response.status = http.HTTPStatus.NOT_FOUND\n            response.close_connection = True\n            return\n    except Exception as ex:\n        if not isinstance(ex, AIExtError):\n            ex = InternalError(str(ex))\n\n        if not isinstance(ex, BadRequestError):\n            logger.error(f\"error while handling a /{args[0]} request: {ex}\")\n\n        response.status = ex.get_http_status()\n        response.content_type = b'application/json'\n        response.body = json.dumps(ex.json()).encode(\"utf-8\")\n        response.close_connection = True\n        return\n\n\nasync def _handle_rag_request(\n    protocol: protocol.HttpProtocol,\n    request: protocol.HttpRequest,\n    response: protocol.HttpResponse,\n    db: dbview.Database,\n    role_name: str,\n    tenant: srv_tenant.Tenant,\n) -> None:\n    try:\n        http_client = tenant.get_http_client(originator=\"ai/rag\")\n\n        body = json.loads(request.body)\n\n        if not isinstance(body, dict):\n            raise TypeError(\n                'the body of the request must be a JSON object')\n\n        context = body.get('context')\n        if context is None:\n            raise TypeError(\n                'missing required \"context\" object in request')\n        if not isinstance(context, dict):\n            raise TypeError(\n                '\"context\" value in request is not a valid JSON object')\n\n        ctx_query = context.get(\"query\")\n        ctx_variables = context.get(\"variables\")\n        ctx_globals = context.get(\"globals\")\n        ctx_max_obj_count = context.get(\"max_object_count\")\n\n        if not ctx_query:\n            raise TypeError(\n                'missing required \"query\" in request \"context\" object')\n\n        if ctx_variables is not None and not isinstance(ctx_variables, dict):\n            raise TypeError('\"variables\" must be a JSON object')\n\n        if ctx_globals is not None and not isinstance(ctx_globals, dict):\n            raise TypeError('\"globals\" must be a JSON object')\n\n        model = cast(str, body.get('model'))\n        if not model:\n            raise TypeError(\n                'missing required \"model\" in request')\n\n        query = body.get('query')\n        if not query:\n            raise TypeError(\n                'missing required \"query\" in request')\n\n        stream = body.get('stream')\n        if stream is None:\n            stream = False\n        elif not isinstance(stream, bool):\n            raise TypeError('\"stream\" must be a boolean')\n\n        if ctx_max_obj_count is None:\n            ctx_max_obj_count = 5\n        elif not isinstance(ctx_max_obj_count, int) or ctx_max_obj_count <= 0:\n            raise TypeError(\n                '\"context.max_object_count\" must be a positive integer')\n\n        prompt_id = None\n        prompt_name = None\n        custom_prompt = None\n        custom_prompt_messages: list[dict[str, Any]] = []\n\n        prompt = body.get(\"prompt\")\n\n        if prompt is None:\n            prompt_name = \"builtin::rag-default\"\n        else:\n            if not isinstance(prompt, dict):\n                raise TypeError(\n                    '\"prompt\" value in request must be a JSON object')\n            prompt_name = prompt.get(\"name\")\n            prompt_id = prompt.get(\"id\")\n            custom_prompt = prompt.get(\"custom\")\n\n            if prompt_name and prompt_id:\n                raise TypeError(\n                    \"prompt.id and prompt.name are mutually exclusive\"\n                )\n\n            if custom_prompt:\n                if not isinstance(custom_prompt, list):\n                    raise TypeError(\n                        (\n                            \"prompt.custom must be a list, where each element \"\n                            \"is one of the following types:\\n\"\n                            \"{ role: 'system', content: str },\\n\"\n                            \"{ role: 'user', content: [{ type: 'text', \"\n                            \"text: str }] },\\n\"\n                            \"{ role: 'assistant', content: str, \"\n                            \"optional tool_calls: [{id: str, type: 'function',\"\n                            \" function: { name: str, arguments: str }}] },\\n\"\n                            \"{ role: 'tool', content: str, tool_call_id: str }\"\n                        )\n                    )\n                for entry in custom_prompt:\n                    if not isinstance(entry, dict) or not entry.get(\"role\"):\n                        raise TypeError(\n                            (\n                                \"each prompt.custom entry must be a \"\n                                \"dictionary of one of the following types:\\n\"\n                                \"{ role: 'system', content: str },\\n\"\n                                \"{ role: 'user', content: [{ type: 'text', \"\n                                \"text: str }] },\\n\"\n                                \"{ role: 'assistant', content: str, \"\n                                \"optional tool_calls: [{id: str, \"\n                                \"type: 'function', function: { \"\n                                \"name: str, arguments: str }}] },\\n\"\n                                \"{ role: 'tool', content: str, \"\n                                \"tool_call_id: str }\"\n                            )\n                        )\n\n                    entry_role = entry.get('role')\n                    if entry_role == 'system':\n                        if not isinstance(entry.get(\"content\"), str):\n                            raise TypeError(\n                                \"System message content has to be string.\"\n                            )\n                    elif entry_role == 'user':\n                        if not isinstance(entry.get(\"content\"), list):\n                            raise TypeError(\n                                (\n                                    \"User message content has to be a list of \"\n                                    \"{ type: 'text', text: str }\"\n                                )\n                            )\n                        for content_entry in entry[\"content\"]:\n                            if content_entry.get(\n                                \"type\"\n                            ) != \"text\" or not isinstance(\n                                content_entry.get(\"text\"), str\n                            ):\n                                raise TypeError(\n                                    (\n                                        \"Element of user message content has to\"\n                                        \"be of type { type: 'text', text: str }\"\n                                    )\n                                )\n                    elif entry_role == 'assistant':\n                        if not isinstance(entry.get(\"content\"), str):\n                            raise TypeError(\n                                \"Assistant message content has to be string\"\n                            )\n\n                        tool_calls = entry.get(\"tool_calls\")\n                        if tool_calls:\n                            if not isinstance(tool_calls, list):\n                                raise TypeError(\n                                    (\n                                        \"Assistant tool calls must be\"\n                                        \"a list of:\\n\"\n                                        \"{id: str, type: 'function', function:\"\n                                        \" {name: str, arguments: str }}\"\n                                    )\n                                )\n\n                            for call in tool_calls:\n                                if (\n                                    not isinstance(call, dict)\n                                    or not isinstance(call.get(\"id\"), str)\n                                    or call.get(\"type\") != \"function\"\n                                    or not isinstance(\n                                        call.get(\"function\"), dict\n                                    )\n                                    or not isinstance(\n                                        call[\"function\"].get(\"name\"), str\n                                    )\n                                    or not isinstance(\n                                        call[\"function\"].get(\"arguments\"),\n                                        str,\n                                    )\n                                ):\n                                    raise TypeError(\n                                        (\n                                            \"A tool call must be of type:\\n\"\n                                            \"{id: str, type: 'function', \"\n                                            \"function: { name: str, \"\n                                            \"arguments: str }}\"\n                                        )\n                                    )\n\n                    elif entry_role == 'tool':\n                        if not isinstance(entry.get(\"content\"), str):\n                            raise TypeError(\n                                \"Tool message content has to be string.\"\n                            )\n                        if not isinstance(entry.get(\"tool_call_id\"), str):\n                            raise TypeError(\n                                \"Tool message tool_call_id has to be string.\"\n                            )\n                    else:\n                        raise TypeError(\n                            (\n                                \"Message role must match one of these: \"\n                                \"system, user, assistant, tool.\"\n                            )\n                        )\n                    custom_prompt_messages.append(entry)\n    except Exception as ex:\n        raise BadRequestError(ex.args[0])\n\n    provider_name: str\n    model_name: str\n    try_builtin = False\n    if ':' in model:\n        parts = model.split(':')\n        if len(parts) > 2:\n            raise BadRequestError(\n                f\"Invalid model uri, ':' used more than once: {model}\"\n            )\n        provider_name = parts[0]\n        model_name = parts[1]\n        try_builtin = True\n\n    else:\n        provider_name = await _get_model_provider(\n            db,\n            base_model_type=TextGenerationModel.gel_type,\n            model_name=model,\n        )\n        model_name = model\n\n    chat_provider = _get_provider_config(\n        db, provider_name, try_builtin=try_builtin\n    )\n\n    vector_provider, vector_query = await _generate_embeddings_for_type(\n        db,\n        http_client,\n        ctx_query,\n        content=query,\n        role_name=role_name,\n    )\n\n    ctx_query = f\"\"\"\n        WITH\n            __query := <array<float32>>std::to_json(<str>$input),\n            search := ext::ai::search(({ctx_query}), __query),\n            context := (\n                for s in search union (\n                    (ext::ai::to_context(s.object), s.distance)\n                )\n            )\n        SELECT (\n            SELECT context\n            ORDER BY\n                .1 ASC EMPTY LAST\n            LIMIT\n                <int64>$limit\n        ).0\n    \"\"\"\n    if ctx_variables is None:\n        ctx_variables = {}\n\n    ctx_variables[\"input\"] = str(\n        vector_provider.get_embeddings_from_result(vector_query)[0]\n    )\n    ctx_variables[\"limit\"] = ctx_max_obj_count\n\n    context = await _edgeql_query_json(\n        db=db,\n        query=ctx_query,\n        variables=ctx_variables,\n        globals_=ctx_globals,\n        role_name=role_name,\n    )\n    if len(context) == 0:\n        raise BadRequestError(\n            'query did not match any data in specified context',\n        )\n\n    prompt_query = \"\"\"\n        SELECT\n            ext::ai::ChatPrompt {\n                messages: {\n                    participant_role,\n                    content,\n                },\n            }\n        FILTER\n    \"\"\"\n\n    if prompt_id or prompt_name:\n        prompt_variables = {}\n        if prompt_name:\n            prompt_query += \".name = <str>$prompt_name\"\n            prompt_variables[\"prompt_name\"] = prompt_name\n        elif prompt_id:\n            prompt_query += \".id = <uuid><str>$prompt_id\"\n            prompt_variables[\"prompt_id\"] = prompt_id\n\n        prompts = await _edgeql_query_json(\n            db=db,\n            role_name=role_name,\n            query=prompt_query,\n            variables=prompt_variables,\n        )\n        if len(prompts) == 0:\n            raise BadRequestError(\"could not find the specified chat prompt\")\n\n        prompt = prompts[0]\n    else:\n        prompt = {\n            \"messages\": [],\n        }\n\n    prompt_messages: list[dict[str, Any]] = []\n    for message in prompt[\"messages\"]:\n        if message[\"participant_role\"] == \"User\":\n            content = message[\"content\"].format(\n                context=\"\\n\".join(context),\n                query=query,\n            )\n        elif message[\"participant_role\"] == \"System\":\n            content = message[\"content\"].format(\n                context=\"\\n\".join(context),\n            )\n        else:\n            content = message[\"content\"]\n\n        role = message[\"participant_role\"].lower()\n\n        prompt_messages.append(dict(role=role, content=content))\n\n    # don't add here at the end the user query msg because Mistral and\n    # Anthropic doesn't work if the user message shows after the tools\n    messages = prompt_messages + custom_prompt_messages\n\n    await _start_chat(\n        protocol=protocol,\n        request=request,\n        response=response,\n        provider=chat_provider,\n        http_client=http_client,\n        model_name=model_name,\n        messages=messages,\n        stream=stream,\n        temperature=body.get(\"temperature\"),\n        top_p=body.get(\"top_p\"),\n        max_tokens=body.get(\"max_tokens\"),\n        seed=body.get(\"seed\"),\n        safe_prompt=body.get(\"safe_prompt\"),\n        top_k=body.get(\"top_k\"),\n        logit_bias=body.get(\"logit_bias\"),\n        logprobs=body.get(\"logprobs\"),\n        user=body.get(\"user\"),\n        tools=body.get(\"tools\"),\n    )\n\n\nasync def _handle_embeddings_request(\n    request: protocol.HttpRequest,\n    response: protocol.HttpResponse,\n    db: dbview.Database,\n    role_name: str,\n    tenant: srv_tenant.Tenant,\n) -> None:\n    try:\n        body = json.loads(request.body)\n        if not isinstance(body, dict):\n            raise TypeError(\n                'the body of the request must be a JSON object')\n\n        inputs = body.get(\"inputs\")\n        input = body.get(\"input\")\n\n        if inputs is not None and input is not None:\n            raise TypeError(\n                \"You cannot provide both 'inputs' and 'input'. \"\n                \"Please provide 'inputs'; 'input' has been deprecated.\"\n            )\n\n        if input is not None:\n            logger.warning(\"'input' is deprecated, use 'inputs' instead\")\n            inputs = input\n\n        if not inputs:\n            raise TypeError(\n                'missing or empty required \"inputs\" value in request'\n            )\n\n        model_name = body.get(\"model\")\n        if not model_name:\n            raise TypeError(\n                'missing or empty required \"model\" value in request')\n\n        shortening = body.get(\"dimensions\")\n        user = body.get(\"user\")\n\n    except Exception as ex:\n        raise BadRequestError(str(ex)) from None\n\n    provider_name = await _get_model_provider(\n        db,\n        base_model_type=EmbeddingModel.gel_type,\n        model_name=model_name,\n    )\n    if provider_name is None:\n        # Error\n        return\n\n    provider = _get_provider_config(db, provider_name)\n\n    if not isinstance(inputs, list):\n        inputs = [inputs]\n\n    result = await _generate_embeddings(\n        provider,\n        model_name,\n        inputs,\n        shortening,\n        user,\n        http_client=tenant.get_http_client(originator=\"ai/embeddings\"),\n    )\n    if isinstance(result.data, rs.Error):\n        raise AIProviderError(result.data.message)\n\n    result_data: bytes\n    if provider.api_style == ApiStyle.Ollama:\n        # Ollama produces embeddings differently.\n        # Repackage it to look like OpenAI.\n        decoded_result = json.loads(\n            result.data.embeddings.decode(\"utf-8\")\n        )\n        embeddings = cast(list[list[float]], decoded_result[\"embeddings\"])\n        prompt_eval_count = cast(int, decoded_result['prompt_eval_count'])\n        result_data = json.dumps({\n            \"object\": \"list\",\n            \"data\": [\n                {\n                    \"object\": \"embedding\",\n                    \"index\": index,\n                    \"embedding\": embedding\n                }\n                for index, embedding in enumerate(embeddings)\n            ],\n            \"model\": model_name,\n            \"usage\": {\n                \"prompt_tokens\": prompt_eval_count,\n                \"total_tokens\": prompt_eval_count\n            }\n        }).encode()\n\n    else:\n        result_data = result.data.embeddings\n\n    response.status = http.HTTPStatus.OK\n    response.content_type = b'application/json'\n    response.body = result_data\n\n\nasync def _edgeql_query_json(\n    *,\n    db: dbview.Database,\n    query: str,\n    role_name: str | None,\n    variables: Optional[dict[str, Any]] = None,\n    globals_: Optional[dict[str, Any]] = None,\n) -> list[Any]:\n    try:\n        result = await execute.parse_execute_json(\n            db,\n            query,\n            variables=variables or {},\n            globals_=globals_,\n            query_tag='gel/ai',\n        )\n\n        content = json.loads(result)\n    except Exception as ex:\n        try:\n            await _db_error(db, ex)\n        except Exception as iex:\n            raise iex from None\n    else:\n        return cast(list[Any], content)\n\n\nasync def _db_error(\n    db: dbview.Database,\n    ex: Exception,\n    *,\n    errcls: Optional[type[AIExtError]] = None,\n    context: Optional[str] = None,\n) -> NoReturn:\n    if debug.flags.server:\n        markup.dump(ex)\n\n    iex = await execute.interpret_error(ex, db)\n\n    if context:\n        msg = f'{context}: {iex}'\n    else:\n        msg = str(iex)\n\n    err_dct = {\n        'message': msg,\n        'type': str(type(iex).__name__),\n        'code': iex.get_code(),\n    }\n\n    if errcls is None:\n        if isinstance(iex, errors.QueryError):\n            errcls = BadRequestError\n        else:\n            errcls = InternalError\n\n    raise errcls(json=err_dct) from iex\n\n\ndef _get_provider_config(\n    db: dbview.Database,\n    provider_name: str,\n    try_builtin: bool = False,\n) -> ProviderConfig:\n    \"\"\"Try to return a provider config with a matching name.\n    Otherwise, raise an error.\n\n    Checks if there is a builtin provider with a matching name.\n\n    eg. \"openai\" -> ProviderConfig(name=\"builtin::openai\", ...)\n    \"\"\"\n\n    cfg = db.lookup_config(\"ext::ai::Config::providers\")\n\n    def _create_provider_config(db_cfg: Any) -> ProviderConfig:\n        cfg = cast(ProviderConfig, db_cfg)\n        return ProviderConfig(\n            name=cfg.name,\n            display_name=cfg.display_name,\n            api_url=cfg.api_url,\n            client_id=cfg.client_id,\n            secret=cfg.secret,\n            api_style=cfg.api_style,\n        )\n\n    # try builtin prefix\n    builtin_prefix = \"builtin::\"\n    if try_builtin and not provider_name.startswith(builtin_prefix):\n        builtin_name = builtin_prefix + provider_name\n\n        for provider in cfg:\n            if provider.name == builtin_name:\n                return _create_provider_config(provider)\n\n    # try unmodified name\n    for provider in cfg:\n        if provider.name == provider_name:\n            return _create_provider_config(provider)\n\n    raise ConfigurationError(\n        f\"provider {provider_name!r} has not been configured\"\n    )\n\n\nasync def _get_embedding_models(\n    db: dbview.Database,\n    model_names: list[str],\n) -> dict[str, EmbeddingModel]:\n\n    model_annotations = await _get_model_annotations(\n        db,\n        base_model_type=EmbeddingModel.gel_type,\n        model_names=model_names,\n        annotation_names=[\n            EmbeddingModel.provider_annotation,\n            EmbeddingModel.max_model_input_tokens_annotation,\n            EmbeddingModel.max_batch_tokens_annotation,\n            EmbeddingModel.max_batch_size_annotation,\n            EmbeddingModel.max_output_dimensions_annotation,\n            EmbeddingModel.supports_shortening_annotation,\n        ],\n    )\n\n    def _get_ann(\n        model: str,\n        anns: dict[str, str | None],\n        name: str,\n    ) -> str:\n        val = anns.get(name)\n        if val is None:\n            raise InternalError(f\"Could not read annotation '{name}'\")\n        if val == \"<must override>\":\n            raise InternalError(\n                f\"Model '{model}' is missing value for annotation '{name}'\"\n            )\n        return val\n\n    def _get_bool_ann(\n        model: str,\n        anns: dict[str, str | None],\n        name: str,\n    ) -> bool:\n        val = _get_ann(model, anns, name)\n        try:\n            return bool(val)\n        except ValueError:\n            raise InternalError(\n                f\"Model '{model}' annotation '{name}' \"\n                f\"has non boolean value {val}\"\n            )\n\n    def _get_int_ann(\n        model: str,\n        anns: dict[str, str | None],\n        name: str,\n    ) -> int:\n        val = _get_ann(model, anns, name)\n        try:\n            return int(val)\n        except ValueError:\n            raise InternalError(\n                f\"Model '{model}' annotation '{name}' \"\n                f\"has non integer value {val}\"\n            )\n\n    def _get_int_or_none_ann(\n        model: str,\n        anns: dict[str, str | None],\n        name: str,\n    ) -> int | None:\n        val = _get_ann(model, anns, name)\n        if val == \"<optional>\":\n            return None\n        try:\n            return int(val)\n        except ValueError:\n            raise InternalError(\n                f\"Model '{model}' annotation '{name}' \"\n                f\"has non integer value {val}\"\n            )\n\n    result: dict[str, EmbeddingModel] = {}\n    for model, anns in model_annotations.items():\n        result[model] = EmbeddingModel(\n            name=model,\n            provider=_get_ann(model, anns, EmbeddingModel.provider_annotation),\n            max_input_tokens=_get_int_ann(\n                model, anns, EmbeddingModel.max_model_input_tokens_annotation\n            ),\n            max_batch_tokens=_get_int_ann(\n                model, anns, EmbeddingModel.max_batch_tokens_annotation\n            ),\n            max_batch_size=_get_int_or_none_ann(\n                model, anns, EmbeddingModel.max_batch_size_annotation\n            ),\n            max_output_dimensions=_get_int_ann(\n                model, anns, EmbeddingModel.max_output_dimensions_annotation\n            ),\n            supports_shortening=_get_bool_ann(\n                model, anns, EmbeddingModel.supports_shortening_annotation\n            ),\n        )\n\n    return result\n\n\nasync def _get_model_annotations(\n    db: dbview.Database,\n    base_model_type: str,\n    model_names: list[str],\n    annotation_names: list[str],\n) -> dict[str, dict[str, str | None]]:\n\n    models = await _edgeql_query_json(\n        db=db,\n        role_name=None,\n        query=\"\"\"\n        WITH\n            Parent := (\n                SELECT\n                    schema::ObjectType\n                FILTER\n                    .name = <str>$base_model_type\n            ),\n            Models := Parent.<ancestors[IS schema::ObjectType],\n        SELECT\n            Models {\n                model_name := (\n                    SELECT\n                        (FOR ann IN .annotations SELECT (ann.name, ann@value))\n                    FILTER\n                        .0 = \"ext::ai::model_name\"\n                    limit 1\n                ).1,\n                values := (\n                    SELECT\n                        (FOR ann IN .annotations SELECT (ann.name, ann@value))\n                    FILTER\n                        .0 in array_unpack(<array<str>>$annotation_names)\n                ),\n            }\n        FILTER\n            .model_name in array_unpack(<array<str>>$model_names)\n        \"\"\",\n        variables={\n            \"base_model_type\": base_model_type,\n            \"model_names\": model_names,\n            \"annotation_names\": annotation_names,\n        },\n    )\n    if len(models) == 0:\n        raise BadRequestError(\"invalid model name\")\n\n    result: dict[str, dict[str, str | None]] = {}\n    for model in models:\n        model_name = model['model_name']\n        if model_name in result:\n            raise InternalError(f\"models with duplicate name: {model_name}\")\n\n        model_anns = {\n            ann_name: ann_value\n            for ann_name, ann_value in model['values']\n        }\n\n        result[model_name] = {\n            ann_name: model_anns.get(ann_name)\n            for ann_name in annotation_names\n        }\n\n    return result\n\n\nasync def _get_model_provider(\n    db: dbview.Database,\n    base_model_type: str,\n    model_name: str,\n) -> str:\n    model_annotations = await _get_model_annotations(\n        db, base_model_type, [model_name], [BaseModel.provider_annotation]\n    )\n    return not_none(\n        model_annotations[model_name][BaseModel.provider_annotation]\n    )\n\n\nasync def _generate_embeddings_for_type(\n    db: dbview.Database,\n    http_client: http.HttpClient,\n    type_query: str,\n    content: str,\n    role_name: str,\n) -> tuple[ProviderConfig, bytes]:\n    type_desc = await execute.describe(\n        db,\n        f\"SELECT ({type_query})\",\n        allow_capabilities=compiler.Capability.NONE,\n        query_tag='gel/ai',\n        role_name=role_name,\n    )\n    if (\n        not isinstance(type_desc, sertypes.ShapeDesc)\n        or not isinstance(type_desc.type, sertypes.ObjectDesc)\n    ):\n        raise errors.InvalidReferenceError(\n            'context query does not return an '\n            'object type indexed with an `ext::ai::index`'\n        )\n\n    return await generate_embeddings_for_text(\n        db, http_client, type_desc.type.tid, content\n    )\n\n\nasync def generate_embeddings_for_text(\n    db: dbview.Database,\n    http_client: http.HttpClient,\n    type_id: str | uuid.UUID,\n    content: str,\n) -> tuple[ProviderConfig, bytes]:\n\n    index = await get_ai_index_for_type(db, type_id)\n    provider = _get_provider_config(db=db, provider_name=index.provider)\n    if (\n        index.index_embedding_dimensions\n        < index.model_embedding_dimensions\n    ):\n        shortening = index.index_embedding_dimensions\n    else:\n        shortening = None\n    result = await _generate_embeddings(\n        provider,\n        index.model,\n        [content],\n        shortening,\n        None,\n        http_client,\n    )\n    if isinstance(result.data, rs.Error):\n        raise AIProviderError(result.data.message)\n    return provider, result.data.embeddings\n\n\n@dataclass(frozen=True, kw_only=True)\nclass TextEmbeddingsResult:\n    success: Optional[list[list[float]]] = None\n\n    too_long: Optional[list[int]] = None\n\n\nasync def generate_embeddings_for_texts(\n    db: dbview.Database,\n    http_client: http.HttpClient,\n    inputs: list[tuple[str | uuid.UUID, str]],\n) -> TextEmbeddingsResult:\n    \"\"\"Generate embeddings for strings to search for ai indexed objects.\n\n    Each input string may have a different object. The object is specified\n    by the object type id as either a string or uuid.\n\n    Produces embeddings for the input strings by:\n    - grouping string by their index model and shortening\n    - batching those groups\n    - then doing embeddings requests in batches\n\n    Input strings are truncated if allowed by their index.\n\n    If any string is too long and truncating is not allowed, a \"too_long\"\n    result is returned.\n\n    If all embeddings requests are successful, the embeddings are returned\n    as a \"success\" result in the same order as the inputs.\n    \"\"\"\n    # Gather information about the indexes and embeddings\n    # For each type, we will need:\n    # - model name\n    # - max input tokens\n    # - max batch tokens\n    # - provider config\n    # - allowed to truncate\n    # - shortening, if any\n    type_ai_indexes: dict[str, AIIndex] = {}\n    for type_id, _ in inputs:\n        type_id = str(type_id)\n        if type_id not in type_ai_indexes:\n            type_ai_indexes[type_id] = await get_ai_index_for_type(db, type_id)\n\n    model_providers = {\n        ai_index.model: ai_index.provider\n        for ai_index in type_ai_indexes.values()\n    }\n    embedding_models = await _get_embedding_models(\n        db, list(model_providers.keys())\n    )\n\n    provider_configs = {\n        provider: _get_provider_config(db=db, provider_name=provider)\n        for provider in set(model_providers.values())\n    }\n\n    # Group the inputs by model and shortening\n    group_input_indexes: dict[tuple[str, Optional[int]], list[int]] = {}\n\n    for input_index, (type_id, _) in enumerate(inputs):\n        ai_index = type_ai_indexes[str(type_id)]\n\n        model_name = ai_index.model\n        shortening = (\n            ai_index.index_embedding_dimensions\n            if (\n                ai_index.index_embedding_dimensions\n                < ai_index.model_embedding_dimensions\n            ) else\n            None\n        )\n\n        group_key = (model_name, shortening)\n\n        if group_key not in group_input_indexes:\n            group_input_indexes[group_key] = []\n\n        group_input_indexes[group_key].append(input_index)\n\n    # Batch each group separately\n    group_batch_texts_and_indexes: dict[\n        tuple[str, Optional[int]],\n        list[tuple[\n            # texts, truncated if needed\n            list[str],\n            # the associated input index\n            list[int],\n        ]]\n    ] = {}\n    too_long: list[int] = []\n\n    for group_key, input_indexes in group_input_indexes.items():\n        model_name, shortening = group_key\n        provider = model_providers[model_name]\n        embedding_model = embedding_models[model_name]\n\n        tokenizer = get_model_tokenizer(provider, model_name)\n\n        texts = [\n            (\n                inputs[input_index][1],\n                type_ai_indexes[str(inputs[input_index][0])].truncate_to_max,\n            )\n            for input_index in input_indexes\n        ]\n\n        text_batches, excluded_indexes = batch_texts(\n            texts,\n            tokenizer,\n            max_input_tokens=embedding_model.max_input_tokens,\n            max_batch_tokens=embedding_model.max_batch_tokens,\n            max_batch_size=embedding_model.max_batch_size,\n        )\n\n        if excluded_indexes or too_long:\n            # If any input is too long, collect all inputs that are too long\n            # and return them as a failure\n            too_long.extend(\n                input_indexes[excluded_index]\n                for excluded_index in excluded_indexes\n            )\n            continue\n\n        group_batch_texts_and_indexes[group_key] = []\n\n        for text_batch in text_batches:\n            batched_texts: list[str] = []\n            batched_input_indexes: list[int] = []\n\n            for entry in text_batch.entries:\n                batched_texts.append(entry.input_text)\n                batched_input_indexes.append(\n                    input_indexes[entry.input_index]\n                )\n\n            group_batch_texts_and_indexes[group_key].append(\n                (batched_texts, batched_input_indexes)\n            )\n\n    if too_long:\n        return TextEmbeddingsResult(too_long=too_long)\n\n    # Do the embeddings\n\n    # We have been tracking the input indexes of the batch texts this whole\n    # time. Use these indexes to fill in a result embeddings list\n    embeddings: list[Optional[list[float]]] = [None] * len(inputs)\n\n    for group_key, batched_texts_and_indexes in (\n        group_batch_texts_and_indexes.items()\n    ):\n        model_name, shortening = group_key\n        provider = model_providers[model_name]\n\n        provider_config = provider_configs[provider]\n\n        for batched_texts, batched_input_indexes in batched_texts_and_indexes:\n            embeddings_result = await _generate_embeddings(\n                provider_config,\n                model_name,\n                batched_texts,\n                shortening,\n                None,\n                http_client,\n            )\n            if isinstance(embeddings_result.data, rs.Error):\n                raise AIProviderError(embeddings_result.data.message)\n            result_entries = provider_config.get_embeddings_from_result(\n                embeddings_result.data.embeddings\n            )\n            for entry_index, result_entry in enumerate(result_entries):\n                input_index = batched_input_indexes[entry_index]\n                embeddings[input_index] = result_entry\n\n    assert all(e is not None for e in embeddings)\n\n    return TextEmbeddingsResult(\n        success=cast(list[list[float]], embeddings),\n    )\n\n\n@dataclass(frozen=True, kw_only=True)\nclass AIIndex:\n    model: str\n    provider: str\n    model_embedding_dimensions: int\n    index_embedding_dimensions: int\n    truncate_to_max: bool\n\n\nasync def get_ai_index_for_type(\n    db: dbview.Database,\n    type_id: str | uuid.UUID,\n) -> AIIndex:\n    try:\n        indexes = await _edgeql_query_json(\n            db=db,\n            query=\"\"\"\n            WITH\n                ObjectType := (\n                    SELECT\n                        schema::ObjectType\n                    FILTER\n                        .id = <uuid>$type_id\n                ),\n            SELECT\n                ObjectType.indexes {\n                    model := (\n                        SELECT\n                            (FOR a IN .annotations SELECT (a@value, a.name))\n                        FILTER\n                            .1 = \"ext::ai::model_name\"\n                        LIMIT\n                            1\n                    ).0,\n                    provider := (\n                        SELECT\n                            (FOR a IN .annotations SELECT (a@value, a.name))\n                        FILTER\n                            .1 = \"ext::ai::model_provider\"\n                        LIMIT\n                            1\n                    ).0,\n                    model_embedding_dimensions := <int64>(\n                        SELECT\n                            (FOR a IN .annotations SELECT (a@value, a.name))\n                        FILTER\n                            .1 =\n                            \"ext::ai::embedding_model_max_output_dimensions\"\n                        LIMIT\n                            1\n                    ).0,\n                    index_embedding_dimensions := <int64>(\n                        SELECT\n                            (FOR a IN .annotations SELECT (a@value, a.name))\n                        FILTER\n                            .1 = \"ext::ai::embedding_dimensions\"\n                        LIMIT\n                            1\n                    ).0,\n                    truncate_to_max := any((\n                        for kwarg in array_unpack(.kwargs) select (\n                            kwarg.name = 'truncate_to_max'\n                            and str_lower(kwarg.expr) = 'true'\n                        )\n                    ))\n                }\n            FILTER\n                .ancestors.name = 'ext::ai::index'\n            \"\"\",\n            variables={\"type_id\": str(type_id)},\n            role_name=None,\n        )\n        if len(indexes) == 0:\n            raise errors.InvalidReferenceError(\n                'context query does not return an '\n                'object type indexed with an `ext::ai::index`'\n            )\n        elif len(indexes) > 1:\n            raise errors.InvalidReferenceError(\n                'context query returns an object '\n                'indexed with multiple `ext::ai::index` indexes'\n            )\n\n    except Exception as ex:\n        await _db_error(db, ex, context=\"context.query\")\n\n    index = indexes[0]\n    return AIIndex(\n        model=index[\"model\"],\n        provider=index[\"provider\"],\n        model_embedding_dimensions=index[\"model_embedding_dimensions\"],\n        index_embedding_dimensions=index[\"index_embedding_dimensions\"],\n        truncate_to_max=index[\"truncate_to_max\"],\n    )\n"
  },
  {
    "path": "edb/server/protocol/args_ser.pxd",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ncimport cython\n\nfrom edb.server.dbview cimport dbview\nfrom edb.server.pgproto.pgproto cimport WriteBuffer\n\n\ncdef WriteBuffer recode_bind_args(\n    dbview.DatabaseConnectionView dbv,\n    dbview.CompiledQuery compiled,\n    bytes bind_args,\n    list converted_args,\n    list positions = ?,\n    list data_types = ?,\n)\n\n\ncdef recode_bind_args_for_script(\n    dbview.DatabaseConnectionView dbv,\n    dbview.CompiledQuery compiled,\n    bytes bind_args,\n    object converted_args,\n    ssize_t start,\n    ssize_t end,\n)\n\ncdef bytes recode_global(\n    dbview.DatabaseConnectionView dbv,\n    bytes glob,\n    object glob_descriptor,\n)\n\ncdef WriteBuffer combine_raw_args(object args = ?)\n\n@cython.final\ncdef class ParamConversion:\n    cdef:\n        str param_name\n        str conversion_name\n        tuple additional_info\n        bytes encoded_data\n        object constant_value\n\ncdef list[ParamConversion] get_param_conversions(\n    dbview.DatabaseConnectionView dbv,\n    list server_param_conversions,\n    bytes bind_args,\n    list[bytes] extra_blobs,\n)\n\ncdef dict[int, bytes] get_args_data_for_indexes(\n    bytes bind_args,\n    list[bytes] extra_blobs,\n    list[int] target_indexes,\n)\n\ncdef class ConvertedArg:\n    cdef:\n        int bind_format_code\n\n@cython.final\ncdef class ConvertedArgStr(ConvertedArg):\n    cdef:\n        str data\n\n    @staticmethod\n    cdef ConvertedArgStr new(str data)\n\n@cython.final\ncdef class ConvertedArgFloat64(ConvertedArg):\n    cdef:\n        float data\n\n    @staticmethod\n    cdef ConvertedArgFloat64 new(float data)\n\n@cython.final\ncdef class ConvertedArgListFloat32(ConvertedArg):\n    cdef:\n        list data\n\n    @staticmethod\n    cdef ConvertedArgListFloat32 new(list data)\n"
  },
  {
    "path": "edb/server/protocol/args_ser.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\ncimport cython\ncimport cpython\n\nfrom libc.stdint cimport int8_t, uint8_t, int16_t, uint16_t, \\\n                         int32_t, uint32_t, int64_t, uint64_t\n\nfrom edb import errors\nfrom edb.server.compiler import sertypes\nfrom edb.server.compiler import enums\nfrom edb.server.compiler import dbstate\nfrom edb.server.dbview cimport dbview\n\nfrom edb.server.pgproto cimport hton\nfrom edb.server.pgproto.pgproto cimport (\n    WriteBuffer,\n\n    FRBuffer,\n    frb_init,\n    frb_read,\n    frb_get_len,\n    frb_slice_from,\n)\n\ncdef uint32_t SCALAR_TAG = int(enums.TypeTag.SCALAR)\ncdef uint32_t TUPLE_TAG = int(enums.TypeTag.TUPLE)\ncdef uint32_t ARRAY_TAG = int(enums.TypeTag.ARRAY)\n\n\ncdef recode_bind_args_for_script(\n    dbview.DatabaseConnectionView dbv,\n    dbview.CompiledQuery compiled,\n    bytes bind_args,\n    object converted_args,\n    ssize_t start,\n    ssize_t end,\n):\n    cdef:\n        WriteBuffer bind_data\n        ssize_t i\n        ssize_t oidx\n        ssize_t iidx\n\n    unit_group = compiled.query_unit_group\n\n    # TODO: just do the simple thing if it is only one!\n\n    positions = []\n    recoded_buf = recode_bind_args(\n        dbv, compiled, bind_args, None, positions\n    )\n    # TODO: something with less copies\n    recoded = bytes(memoryview(recoded_buf))\n\n    bind_array = []\n    for i in range(start, end):\n        query_unit = unit_group[i]\n        bind_data = WriteBuffer.new()\n        bind_data.write_int32(0x00010001)\n\n        num_args = query_unit.in_type_args_real_count\n        num_args += _count_globals(query_unit)\n\n        if compiled.first_extra is not None:\n            num_args += compiled.extra_counts[i]\n\n        if query_unit.server_param_conversions is not None:\n            num_args += len(query_unit.server_param_conversions)\n\n        bind_data.write_int16(<int16_t>num_args)\n\n        if query_unit.in_type_args:\n            for iidx, arg in enumerate(query_unit.in_type_args):\n                oidx = arg.outer_idx if arg.outer_idx is not None else iidx\n                barg = recoded[positions[oidx]:positions[oidx+1]]\n                bind_data.write_bytes(barg)\n\n        if compiled.first_extra is not None:\n            bind_data.write_bytes(compiled.extra_blobs[i])\n\n        _inject_globals(dbv, query_unit, bind_data)\n\n        if converted_args and i in converted_args:\n            for arg in converted_args[i]:\n                assert isinstance(arg, ConvertedArg)\n                arg.encode(bind_data)\n\n        bind_data.write_int32(0x00010001)\n\n        bind_array.append(bind_data)\n\n    return bind_array\n\n\ncdef WriteBuffer recode_bind_args(\n    dbview.DatabaseConnectionView dbv,\n    dbview.CompiledQuery compiled,\n    bytes bind_args,\n    list converted_args,\n    # XXX do something better?!?\n    list positions = None,\n    list data_types = None,\n):\n    cdef:\n        FRBuffer in_buf\n        FRBuffer peek_buf\n        WriteBuffer out_buf = WriteBuffer.new()\n        int32_t recv_args\n        int32_t decl_args\n        ssize_t in_len\n        ssize_t i\n        int32_t array_tid\n        const char *data\n        bint live = positions is None\n\n    assert cpython.PyBytes_CheckExact(bind_args)\n    frb_init(\n        &in_buf,\n        cpython.PyBytes_AS_STRING(bind_args),\n        cpython.Py_SIZE(bind_args))\n\n    # number of elements in the tuple\n    # for empty tuple it's okay to send zero-length arguments\n    qug = compiled.query_unit_group\n    is_null_type = qug.in_type_id == sertypes.NULL_TYPE_ID.bytes\n    if frb_get_len(&in_buf) == 0:\n        if not is_null_type:\n            raise errors.InputDataError(\n                f\"insufficient data for type-id {qug.in_type_id}\")\n        recv_args = 0\n    else:\n        if is_null_type:\n            raise errors.InputDataError(\n                \"absence of query arguments must be encoded with a \"\n                \"'zero' type \"\n                \"(id: 00000000-0000-0000-0000-000000000000, \"\n                \"encoded with zero bytes)\")\n        recv_args = hton.unpack_int32(frb_read(&in_buf, 4))\n    decl_args = len(qug.in_type_args or ())\n\n    if recv_args != decl_args:\n        raise errors.InputDataError(\n            f\"invalid argument count, \"\n            f\"expected: {decl_args}, got: {recv_args}\")\n\n    num_args = qug.in_type_args_real_count\n    if compiled.first_extra is not None:\n        assert recv_args == compiled.first_extra, \\\n            f\"argument count mismatch {recv_args} != {compiled.first_extra}\"\n        num_args += compiled.extra_counts[0]\n\n    num_globals = _count_globals(qug)\n    num_args += num_globals\n\n    if converted_args is not None:\n        num_args += len(converted_args)\n\n    if live:\n        if not compiled.extra_formatted_as_text:\n            # all parameter values are in binary\n            out_buf.write_int32(0x00010001)\n        elif not recv_args and not num_globals:\n            # all parameter values are in text (i.e extracted SQL constants)\n            out_buf.write_int16(0x0000)\n        else:\n            # got a mix of binary and text, spell them out explicitly\n            out_buf.write_int16(<int16_t>num_args)\n            # explicit args are in binary\n            for _ in range(recv_args):\n                out_buf.write_int16(0x0001)\n            # and extracted SQL constants are in text\n            if compiled.extra_counts:\n                for _ in range(compiled.extra_counts[0]):\n                    out_buf.write_int16(0x0000)\n            # and injected globals are binary again\n            for _ in range(num_globals):\n                out_buf.write_int16(0x0001)\n            # and converted args depend on the conversion\n            if converted_args:\n                for arg in converted_args:\n                    out_buf.write_int16(arg.bind_format_code)\n\n        out_buf.write_int16(<int16_t>num_args)\n\n    if data_types is not None and compiled.extra_type_oids:\n        data_types.extend([0] * recv_args)\n        data_types.extend(compiled.extra_type_oids)\n        data_types.extend([0] * num_globals)\n\n    if qug.in_type_args:\n        for param in qug.in_type_args:\n            if positions is not None:\n                positions.append(out_buf._length)\n\n            frb_read(&in_buf, 4)  # reserved\n            # Some of the logic paths below need the length are cleaner if\n            # the length is still present in the input buf, so we just\n            # *peek* at the length here, and need to consume it later.\n            peek_buf = in_buf\n            in_len = hton.unpack_int32(frb_read(&peek_buf, 4))\n            if in_len < 0:\n                # This means argument value is NULL\n                if param.required:\n                    raise errors.QueryError(\n                        f\"parameter ${param.name} is required\")\n\n            # If the param has encoded tuples, we need to decode them\n            # and reencode them as arrays of scalars.\n            if param.sub_params:\n                tids, trans_typ = param.sub_params\n                _decode_tuple_args(\n                    dbv, &in_buf, out_buf, in_len, tids, trans_typ)\n                continue\n\n            frb_read(&in_buf, 4)\n            out_buf.write_int32(in_len)\n\n            if in_len > 0:\n                if param.array_type_id is not None:\n                    array_tid = dbv.resolve_backend_type_id(\n                        param.array_type_id)\n                    recode_array(dbv, &in_buf, out_buf, in_len, array_tid, None)\n                else:\n                    data = frb_read(&in_buf, in_len)\n                    out_buf.write_cstr(data, in_len)\n\n    if positions is not None:\n        positions.append(out_buf._length)\n\n    if live:\n        if compiled.first_extra is not None:\n            out_buf.write_bytes(compiled.extra_blobs[0])\n\n        # Inject any globals variables into the argument stream.\n        _inject_globals(dbv, qug, out_buf)\n\n        if converted_args:\n            for arg in converted_args:\n                assert isinstance(arg, ConvertedArg)\n                arg.encode(out_buf)\n\n        # All columns are in binary format\n        out_buf.write_int32(0x00010001)\n\n    if frb_get_len(&in_buf):\n        raise errors.InputDataError('unexpected trailing data in buffer')\n\n    return out_buf\n\n\ncdef bytes recode_global(\n    dbv: dbview.DatabaseConnectionView,\n    glob: bytes,\n    glob_descriptor: object,\n):\n    cdef:\n        WriteBuffer out_buf\n        FRBuffer in_buf\n\n    if glob_descriptor is None:\n        return glob\n\n    out_buf = WriteBuffer.new()\n\n    assert cpython.PyBytes_CheckExact(glob)\n    frb_init(\n        &in_buf,\n        cpython.PyBytes_AS_STRING(glob),\n        cpython.Py_SIZE(glob))\n\n    _recode_global(dbv, &in_buf, out_buf, in_buf.len, glob_descriptor)\n\n    if frb_get_len(&in_buf):\n        raise errors.InputDataError('unexpected trailing data in buffer')\n\n    return bytes(memoryview(out_buf))\n\n\ncdef _recode_global(\n    dbv: dbview.DatabaseConnectionView,\n    FRBuffer* in_buf,\n    out_buf: WriteBuffer,\n    in_len: ssize_t,\n    glob_descriptor: object,\n):\n    if glob_descriptor is None:\n        data = frb_read(in_buf, in_len)\n        out_buf.write_cstr(data, in_len)\n    elif glob_descriptor[0] == TUPLE_TAG:\n        _, el_tids, el_infos = glob_descriptor\n        recode_global_tuple(dbv, in_buf, out_buf, in_len, el_tids, el_infos)\n    elif glob_descriptor[0] == ARRAY_TAG:\n        _, el_tid, tuple_info = glob_descriptor\n        btid = dbv.resolve_backend_type_id(el_tid)\n        recode_array(dbv, in_buf, out_buf, in_len, btid, tuple_info)\n\n\ncdef recode_global_tuple(\n    dbv: dbview.DatabaseConnectionView,\n    FRBuffer* in_buf,\n    out_buf: WriteBuffer,\n    in_len: ssize_t,\n    el_tids: tuple,\n    el_infos: tuple,\n):\n    \"\"\"\n    Tuples in globals need to have NULLs checked and oids injected,\n    like arrays do.\n\n    Annoyingly this is a *totally separate* code path than tuple query\n    parameters go through. This is because global tuples actually can\n    get passed as postgres composite types, since they are declared in\n    the schema.\n    \"\"\"\n    cdef:\n        WriteBuffer buf\n        ssize_t cnt\n        ssize_t idx\n        ssize_t num\n        ssize_t tag\n        FRBuffer sub_buf\n\n    frb_slice_from(&sub_buf, in_buf, in_len)\n\n    cnt = <uint32_t>hton.unpack_int32(frb_read(&sub_buf, 4))\n    out_buf.write_int32(cnt)\n    num = len(el_tids)\n    if cnt != num:\n        raise errors.InputDataError(\n            f\"tuple length mismatch: {cnt} vs {num}\")\n    for idx in range(num):\n        frb_read(&sub_buf, 4)\n        el_btid = dbv.resolve_backend_type_id(el_tids[idx])\n        out_buf.write_int32(<int32_t>el_btid)\n\n        in_len = hton.unpack_int32(frb_read(&sub_buf, 4))\n        if in_len < 0:\n            raise errors.InputDataError(\"invalid NULL inside type\")\n        out_buf.write_int32(in_len)\n        _recode_global(dbv, &sub_buf, out_buf, in_len, el_infos[idx])\n\n    if frb_get_len(&sub_buf):\n        raise errors.InputDataError('unexpected trailing data in buffer')\n\n\ncdef recode_array(\n    dbv: dbview.DatabaseConnectionView,\n    FRBuffer* in_buf,\n    out_buf: WriteBuffer,\n    in_len: ssize_t,\n    array_tid: int32_t,\n    tuple_info: object,\n):\n    # For a standalone array, we still need to inject oids and reject\n    # NULL elements.\n    cdef:\n        ssize_t cnt\n        ssize_t idx\n        ssize_t num\n        ssize_t tag\n        FRBuffer sub_buf\n\n    frb_slice_from(&sub_buf, in_buf, in_len)\n\n    ndims = hton.unpack_int32(frb_read(&sub_buf, 4)) # ndims\n    if ndims != 1 and ndims != 0:\n        raise errors.InputDataError(\"unsupported array dimensions\")\n    out_buf.write_int32(ndims)\n\n    data = frb_read(&sub_buf, 8)  # flags + reserved (oid)\n    out_buf.write_cstr(data, 4)  # just write flags\n    out_buf.write_int32(<int32_t>array_tid)\n\n    if ndims != 0:\n        cnt = hton.unpack_int32(frb_read(&sub_buf, 4))\n        out_buf.write_int32(cnt)\n\n        val = hton.unpack_int32(frb_read(&sub_buf, 4)) # bound\n        if val != 1:\n            raise errors.InputDataError(\"unsupported array bound\")\n        out_buf.write_int32(val)\n\n        # We have to actually scan the array to make sure it\n        # doesn't have any NULLs in it.\n        for idx in range(cnt):\n            in_len = hton.unpack_int32(frb_read(&sub_buf, 4))\n            if in_len < 0:\n                raise errors.InputDataError(\"invalid NULL inside type\")\n            out_buf.write_int32(in_len)\n            if tuple_info is None:\n                data = frb_read(&sub_buf, in_len)\n                out_buf.write_cstr(data, in_len)\n            else:\n                _recode_global(dbv, &sub_buf, out_buf, in_len, tuple_info)\n        if frb_get_len(&sub_buf):\n            raise errors.InputDataError('unexpected trailing data in buffer')\n\n\ncdef _decode_tuple_args_core(\n    FRBuffer* in_buf,\n    out_bufs: tuple[WriteBuffer],\n    counts: list[int],\n    acounts: list[int],\n    trans_typ: tuple,\n    in_array: bool,\n):\n    # Recurse over the types and the input data, collecting the\n    # arguments into the various out_bufs. See\n    # edb.edgeql.compiler.tuple_args for more discussion.\n\n    cdef:\n        ssize_t in_len\n        WriteBuffer buf\n        ssize_t cnt\n        ssize_t idx\n        ssize_t num\n        ssize_t tag\n        int32_t val\n        FRBuffer sub_buf\n\n    tag = trans_typ[0]\n    idx = trans_typ[1]\n\n    in_len = hton.unpack_int32(frb_read(in_buf, 4))\n    buf = out_bufs[idx]\n\n    if in_len < 0:\n        raise errors.InputDataError(\"invalid NULL inside type\")\n\n    frb_slice_from(&sub_buf, in_buf, in_len)\n\n    if tag == SCALAR_TAG:\n        buf.write_int32(in_len)\n        data = frb_read(&sub_buf, in_len)\n        buf.write_cstr(data, in_len)\n        if in_array:\n            counts[idx] += 1\n\n    elif tag == TUPLE_TAG:\n        cnt = <uint32_t>hton.unpack_int32(frb_read(&sub_buf, 4))\n        num = len(trans_typ) - 2\n        if cnt != num:\n            raise errors.InputDataError(\n                f\"tuple length mismatch: {cnt} vs {num}\")\n        for idx in range(num):\n            typ = trans_typ[idx + 2]\n            frb_read(&sub_buf, 4)\n            _decode_tuple_args_core(\n                &sub_buf, out_bufs, counts, acounts, typ, in_array)\n\n    elif tag == ARRAY_TAG:\n        val = hton.unpack_int32(frb_read(&sub_buf, 4)) # ndims\n        if val != 1 and val != 0:\n            raise errors.InputDataError(\"unsupported array dimensions\")\n        frb_read(&sub_buf, 4)  # flags\n        frb_read(&sub_buf, 4)  # reserved\n        if val == 0:\n            cnt = 0\n        else:\n            cnt = <uint32_t>hton.unpack_int32(frb_read(&sub_buf, 4))\n            val = hton.unpack_int32(frb_read(&sub_buf, 4)) # bound\n            if val != 1:\n                raise errors.InputDataError(\"unsupported array bound\")\n\n        # For nested arrays, we need to produce an array containing\n        # the start/end indexes in the flattened array.\n        if in_array:\n            # If this is the first element, put in the 0\n            if acounts[idx] == -1:\n                counts[idx] += 1\n                acounts[idx] = 0\n                buf.write_int32(4)\n                buf.write_int32(0)\n            counts[idx] += 1\n            acounts[idx] += cnt\n            buf.write_int32(4)\n            buf.write_int32(acounts[idx])\n\n        styp = trans_typ[2]\n        for _ in range(cnt):\n            _decode_tuple_args_core(\n                &sub_buf, out_bufs, counts, acounts, styp, True)\n\n    if frb_get_len(&sub_buf):\n        raise errors.InputDataError('unexpected trailing data in buffer')\n\n\ncdef WriteBuffer _decode_tuple_args(\n    dbv: dbview.DatabaseConnectionView,\n    FRBuffer* in_buf,\n    out_buf: WriteBuffer,\n    in_len: ssize_t,\n    tids: list,\n    trans_typ: object,\n):\n    # PERF: Can we use real arrays, instead of python lists?\n    cdef:\n        const char *data\n        list buffers\n        list counts\n        list acounts\n        WriteBuffer buf\n\n    # N.B: We have peeked at in_len, but the size is still in the buffer, for\n    # more convenient processing by _decode_tuple_args_core\n\n    if in_len < 0:\n        # For a NULL argument, fill out *every* one of our args with NULL\n        for _ in tids:\n            out_buf.write_int32(in_len)\n        # We only peeked at in_len before, so consume it now\n        frb_read(in_buf, 4)\n        return\n\n    buffers = []\n    counts = []\n    acounts = []\n    for maybe_tid in tids:\n        buf = WriteBuffer.new()\n        counts.append(0 if maybe_tid else -1)\n        acounts.append(-1)\n        buffers.append(buf)\n\n    _decode_tuple_args_core(\n        in_buf, tuple(buffers), counts, acounts, trans_typ, False)\n\n    # zip all of the buffers we have collected into up\n    # PERF: or should we just index?\n    for maybe_tid, count, buf in zip(tids, counts, buffers):\n        if maybe_tid:\n            ndims = 1\n            out_buf.write_int32(12 + 8 * ndims + buf.len())\n            # ndimensions + flags\n            array_tid = dbv.resolve_backend_type_id(maybe_tid)\n            out_buf.write_int32(1)\n            out_buf.write_int32(0)\n            out_buf.write_int32(<int32_t>array_tid)\n\n            out_buf.write_int32(<int32_t>count)\n            out_buf.write_int32(1)\n\n        out_buf.write_buffer(buf)\n\n\ncdef _inject_globals(\n    dbv: dbview.DatabaseConnectionView,\n    query_unit_or_group: object,\n    out_buf: WriteBuffer,\n):\n    if globals := query_unit_or_group.globals:\n        for (name, has_present_arg) in globals:\n            val, is_present = dbv.get_global_value(name)\n            if val is not None:\n                out_buf.write_int32(len(val))\n                out_buf.write_bytes(val)\n            else:\n                out_buf.write_int32(-1)\n            if has_present_arg:\n                out_buf.write_int32(1)\n                present = b'\\x01' if is_present else b'\\x00'\n                out_buf.write_bytes(present)\n\n    if permissions := query_unit_or_group.permissions:\n        superuser, available_permissions = dbv.get_permissions()\n        for permission in permissions:\n            out_buf.write_int32(1)\n            out_buf.write_byte(\n                superuser or permission in available_permissions\n            )\n\n\ncdef uint64_t _count_globals(\n    query_unit: object,\n):\n    cdef:\n        uint64_t num_args\n\n    num_args = 0\n    if query_unit.globals:\n        num_args += len(query_unit.globals)\n        for _, has_present_arg in query_unit.globals:\n            if has_present_arg:\n                num_args += 1\n    if query_unit.permissions:\n        num_args += len(query_unit.permissions)\n\n    return num_args\n\n\ncdef WriteBuffer combine_raw_args(\n    args: tuple[bytes, ...] | list[bytes] = (),\n):\n    cdef:\n        int arg_len\n        WriteBuffer bind_data = WriteBuffer.new()\n\n    if len(args) > 32767:\n        raise AssertionError(\n            'the number of query arguments cannot exceed 32767')\n\n    bind_data.write_int32(0x00010001)\n    bind_data.write_int16(<int16_t> len(args))\n    for arg in args:\n        if arg is None:\n            bind_data.write_int32(-1)\n        else:\n            arg_len = len(arg)\n            if arg_len > 0x7fffffff:\n                raise ValueError(\"argument too long\")\n            bind_data.write_int32(<int32_t> arg_len)\n            bind_data.write_bytes(arg)\n    bind_data.write_int32(0x00010001)\n\n    return bind_data\n\n\n@cython.final\ncdef class ParamConversion:\n    def __init__(\n        self,\n        *,\n        param_name,\n        conversion_name,\n        additional_info,\n        encoded_data,\n        constant_value,\n    ):\n        self.param_name = param_name\n        self.conversion_name = conversion_name\n        self.additional_info = additional_info\n        self.encoded_data = encoded_data\n        self.constant_value = constant_value\n\n    def get_param_name(self):\n        return self.param_name\n\n    def get_conversion_name(self):\n        return self.conversion_name\n\n    def get_additional_info(self):\n        return self.additional_info\n\n    def get_encoded_data(self):\n        return self.encoded_data\n\n    def get_constant_value(self):\n        return self.constant_value\n\n    def param_as_int(self) -> int:\n        return self._decode_int() if self.constant_value is None else self.constant_value\n\n    def param_as_str(self) -> str:\n        return self._decode_str() if self.constant_value is None else self.constant_value\n\n    def param_as_array_of_str(self) -> list[str]:\n        return self._decode_array_of_str() if self.constant_value is None else self.constant_value\n\n    def _decode_int(self) -> int:\n        return int.from_bytes(self.encoded_data)\n\n    def _decode_str(self) -> str:\n        return self.encoded_data.decode(\"utf-8\")\n\n    def _decode_array_of_str(self) -> list[str]:\n        # See gel-python for more details on array encoding\n        texts = []\n        text_count = int.from_bytes(self.encoded_data[12:16])\n        data = self.encoded_data[20:]\n        for _ in range(text_count):\n            text_length = int.from_bytes(data[:4])\n            data = data[4:]\n            texts.append(data[:(text_length)].decode(\"utf-8\"))\n            data = data[text_length:]\n        return texts\n\n\ncdef list[ParamConversion] get_param_conversions(\n    dbview.DatabaseConnectionView dbv,\n    list server_param_conversions,\n    bytes bind_args,\n    list[bytes] extra_blobs,\n):\n    # Get encoded data from bind args and extra blobs\n    bind_args_datas: dict[int, bytes] = get_args_data_for_indexes(\n        bind_args,\n        extra_blobs,\n        [\n            param_conversion.script_param_index\n            for param_conversion in server_param_conversions\n            if param_conversion.script_param_index is not None\n        ],\n    )\n\n    # Construct the ParamConversions\n    result: list[ParamConversion] = []\n    for param_conversion in server_param_conversions:\n        assert isinstance(param_conversion, dbstate.ServerParamConversion)\n        param_name = param_conversion.param_name\n\n        if (\n            param_conversion.script_param_index is not None\n            and param_conversion.constant_value is not None\n        ):\n            raise RuntimeError(\n                f\"Parameter '{param_name}' has both \"\n                f\"a constant and a query arg value\"\n            )\n\n        elif param_conversion.script_param_index is not None:\n            # using data from the bind args\n            result.append(ParamConversion(\n                param_name=param_name,\n                conversion_name=param_conversion.conversion_name,\n                additional_info=param_conversion.additional_info,\n                encoded_data=bind_args_datas[\n                    param_conversion.script_param_index\n                ],\n                constant_value=None,\n            ))\n\n        elif param_conversion.constant_value is not None:\n            # using a constant from the query\n            result.append(ParamConversion(\n                param_name=param_name,\n                conversion_name=param_conversion.conversion_name,\n                additional_info=param_conversion.additional_info,\n                encoded_data=None,\n                constant_value=param_conversion.constant_value,\n            ))\n\n        else:\n            raise RuntimeError(\n                f\"Parameter '{param_name}' has no value\"\n            )\n\n    return result\n\n\ncdef dict[int, bytes] get_args_data_for_indexes(\n    bytes bind_args,\n    list[bytes] extra_blobs,\n    list[int] target_indexes,\n):\n    \"\"\"Extract bytes from the bind args and extra blobs by reading the length of\n    each variable and skipping forward by that amount.\n\n    When reaching the end of a blob, continue reading data from the next blob.\n    \"\"\"\n\n    cdef:\n        FRBuffer in_buf\n        ssize_t in_len\n        const char *data_str\n\n    all_blobs = [bind_args, *extra_blobs]\n    curr_blob_index = 0\n    # The first blob is the bind_args, which is has additional data which should\n    # be skipped when extracting the arg data.\n    args_needs_recoding = True\n\n    def setup_blob_buffer():\n        nonlocal curr_blob_index\n        nonlocal args_needs_recoding\n\n        if curr_blob_index >= len(all_blobs):\n            raise RuntimeError('insufficient args data')\n\n        blob = all_blobs[curr_blob_index]\n        assert cpython.PyBytes_CheckExact(blob)\n        frb_init(\n            &in_buf,\n            cpython.PyBytes_AS_STRING(blob),\n            cpython.Py_SIZE(blob)\n        )\n        args_needs_recoding = curr_blob_index == 0\n\n        if args_needs_recoding:\n            # Skip prefixed argument count\n            if frb_get_len(&in_buf) == 0:\n                pass\n            else:\n                frb_read(&in_buf, 4)\n\n    setup_blob_buffer()\n\n    curr_arg_index = 0\n    target_indexes.sort()\n\n    result: dict[int, bytes] = {}\n    for target_index in target_indexes:\n        # Read up to the end of the target variable\n        for arg_index in range(curr_arg_index, target_index + 1):\n            if frb_get_len(&in_buf) == 0:\n                # We've reached the end of the previous blob.\n                # Set up the next one and keep scanning.\n                curr_blob_index += 1\n                setup_blob_buffer()\n\n            if args_needs_recoding:\n                # Skip reserved\n                frb_read(&in_buf, 4)  # reserved\n\n            in_len = hton.unpack_int32(frb_read(&in_buf, 4))\n            data_str = frb_read(&in_buf, in_len)\n\n            if arg_index == target_index:\n                # Store the target variable data\n                data = cpython.PyBytes_FromStringAndSize(data_str, in_len)\n                result[target_index] = data\n\n        curr_arg_index = target_index + 1\n\n    return result\n\n\n# After param conversions, we need to re-encode the converted\n# arg before putting it into the recoded bind args\ncdef class ConvertedArg:\n\n    def encode(self, buffer: WriteBuffer):\n        raise NotImplementedError\n\n\ncdef class ConvertedArgStr(ConvertedArg):\n\n    @staticmethod\n    cdef ConvertedArgStr new(str data):\n        cdef ConvertedArgStr result\n        result = ConvertedArgStr.__new__(ConvertedArgStr)\n        result.bind_format_code = 0x0000\n        result.data = data\n        return result\n\n    def encode(self, buffer: WriteBuffer):\n        encoded = self.data.encode()\n        buffer.write_int32(len(encoded))\n        buffer.write_bytes(encoded)\n\n\ncdef class ConvertedArgFloat64(ConvertedArg):\n\n    @staticmethod\n    cdef ConvertedArgFloat64 new(float data):\n        cdef ConvertedArgFloat64 result\n        result = ConvertedArgFloat64.__new__(ConvertedArgFloat64)\n        result.bind_format_code = 0x0001\n        result.data = data\n        return result\n\n    def encode(self, buffer: WriteBuffer):\n        buffer.write_int32(8) # elem size\n        buffer.write_double(self.data)\n\n\ncdef class ConvertedArgListFloat32(ConvertedArg):\n\n    @staticmethod\n    cdef ConvertedArgListFloat32 new(list data):\n        cdef ConvertedArgListFloat32 result\n        result = ConvertedArgListFloat32.__new__(ConvertedArgListFloat32)\n        result.bind_format_code = 0x0001\n        result.data = data\n        return result\n\n    def encode(self, buffer: WriteBuffer):\n        elem_count = len(self.data)\n        buffer.write_int32(12 + 8 + elem_count * 8)  # buffer length\n        buffer.write_int32(1)  # number of dimensions\n        buffer.write_int32(0)  # flags\n        buffer.write_int32(700)  # array_tid for \"real\"\n\n        buffer.write_int32(elem_count) # count\n        buffer.write_int32(1) # bound\n\n        for elem in self.data:\n            buffer.write_int32(4) # elem size\n            buffer.write_float(elem)\n"
  },
  {
    "path": "edb/server/protocol/auth/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import TYPE_CHECKING\nimport http\nimport json\n\nfrom edb import errors\nfrom edb.common import debug\nfrom edb.common import markup\n\nfrom . import scram\n\nif TYPE_CHECKING:\n    from edb.server import tenant as edbtenant\n    from edb.server.protocol import protocol\n\n\nasync def handle_request(\n    request: protocol.HttpRequest,\n    response: protocol.HttpResponse,\n    path_parts: list[str],\n    tenant: edbtenant.Tenant,\n) -> None:\n    try:\n        if path_parts == [\"token\"]:\n            if not request.authorization:\n                response.status = http.HTTPStatus.UNAUTHORIZED\n                response.custom_headers[\"WWW-Authenticate\"] = \"SCRAM-SHA-256\"\n                return\n\n            scheme, _, auth_str = request.authorization.decode(\n                \"ascii\"\n            ).partition(\" \")\n\n            if scheme.lower().startswith(\"scram\"):\n                scram.handle_request(scheme, auth_str, response, tenant)\n            else:\n                response.body = b\"Unsupported authentication scheme\"\n                response.status = http.HTTPStatus.UNAUTHORIZED\n                response.custom_headers[\"WWW-Authenticate\"] = \"SCRAM-SHA-256\"\n                response.close_connection = True\n        else:\n            response.body = b\"Unknown path\"\n            response.status = http.HTTPStatus.NOT_FOUND\n            response.close_connection = True\n    except errors.EdgeDBError as ex:\n        if debug.flags.server:\n            markup.dump(ex)\n        _response_error(\n            response, http.HTTPStatus.INTERNAL_SERVER_ERROR, str(ex), type(ex)\n        )\n    except Exception as ex:\n        if debug.flags.server:\n            markup.dump(ex)\n\n        # XXX Fix this when LSP \"location\" objects are implemented\n        ex_type = errors.InternalServerError\n\n        _response_error(\n            response, http.HTTPStatus.INTERNAL_SERVER_ERROR, str(ex), ex_type\n        )\n\n\ndef _response_error(\n    response: protocol.HttpResponse,\n    status: http.HTTPStatus,\n    message: str,\n    ex_type: type[errors.EdgeDBError],\n) -> None:\n    err_dct = {\n        \"message\": message,\n        \"type\": str(ex_type.__name__),\n        \"code\": ex_type.get_code(),\n    }\n\n    response.body = json.dumps({\"error\": err_dct}).encode()\n    response.status = status\n    response.close_connection = True\n"
  },
  {
    "path": "edb/server/protocol/auth/scram.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import NamedTuple, Optional, TYPE_CHECKING\nimport base64\nimport collections\nimport hashlib\nimport http\nimport os\nimport time\n\nfrom edgedb import scram\n\nfrom edb.common import debug\nfrom edb.common import markup\nfrom edb.server import auth\n\nif TYPE_CHECKING:\n    from edb.server import tenant as edbtenant\n    from edb.server.protocol import protocol\n\n\nSESSION_TIMEOUT: float = 30\nSESSION_HIGH_WATER_MARK: float = SESSION_TIMEOUT * 10\n\n\nclass Session(NamedTuple):\n    time: float\n    client_nonce: str\n    server_nonce: str\n    client_first_bare: bytes\n    cb_flag: bool\n    server_first: bytes\n    verifier: scram.SCRAMVerifier\n    mock_auth: bool\n    username: str\n\n\nsessions: collections.OrderedDict[str, Session] = collections.OrderedDict()\n\n\ndef handle_request(\n    scheme: str,\n    auth_str: str,\n    response: protocol.HttpResponse,\n    tenant: edbtenant.Tenant,\n) -> None:\n    server = tenant.server\n    if scheme != \"SCRAM-SHA-256\":\n        response.body = (\n            b\"Client selected an invalid SASL authentication mechanism\"\n        )\n        response.status = http.HTTPStatus.UNAUTHORIZED\n        response.custom_headers[\"WWW-Authenticate\"] = \"SCRAM-SHA-256\"\n        return\n\n    data = None\n    sid = None\n    try:\n        for kv_str in auth_str.split():\n            key, _, value = kv_str.rstrip(\",\").partition(\"=\")\n            if key == \"data\":\n                data = base64.b64decode(value.strip('\"')).strip()\n            elif key == \"sid\":\n                sid = value.strip('\"')\n        if data is None:\n            raise ValueError(\"Malformed SCRAM message: data is missing\")\n    except Exception as ex:\n        if debug.flags.server:\n            markup.dump(ex)\n        response.body = str(ex).encode(\"ascii\")\n        response.status = http.HTTPStatus.BAD_REQUEST\n        response.close_connection = True\n        return\n\n    jws = server.get_jws_key()\n    if jws is None or not jws.has_private_keys():\n        response.body = b\"Server doesn't support HTTP SCRAM authentication\"\n        response.status = http.HTTPStatus.FORBIDDEN\n        response.close_connection = True\n        return\n\n    if sid is None:\n        try:\n            bare_offset: int\n            cb_flag: bool\n            authzid: Optional[bytes]\n            username_bytes: bytes\n            client_nonce: str\n            (\n                bare_offset,\n                cb_flag,\n                authzid,\n                username_bytes,\n                client_nonce,\n            ) = scram.parse_client_first_message(data)\n        except ValueError as ex:\n            if debug.flags.server:\n                markup.dump(ex)\n            response.body = f\"Bad client first message: {ex!s}\".encode(\"ascii\")\n            response.status = http.HTTPStatus.BAD_REQUEST\n            response.close_connection = True\n            return\n\n        username = username_bytes.decode(\"utf-8\")\n        client_first_bare = data[bare_offset:]\n\n        if isinstance(cb_flag, str):\n            response.body = (\n                b\"Malformed SCRAM message: \"\n                b\"The client selected SCRAM-SHA-256 without \"\n                b\"channel binding, but the SCRAM message \"\n                b\"includes channel binding data.\"\n            )\n            response.status = http.HTTPStatus.BAD_REQUEST\n            response.close_connection = True\n            return\n\n        if authzid:\n            response.body = (\n                b\"Client uses SASL authorization identity, \"\n                b\"which is not supported\"\n            )\n            response.status = http.HTTPStatus.BAD_REQUEST\n            response.close_connection = True\n            return\n\n        try:\n            verifier, mock_auth = get_scram_verifier(username, tenant)\n        except ValueError as ex:\n            if debug.flags.server:\n                markup.dump(ex)\n            response.body = b\"Authentication failed\"\n            response.status = http.HTTPStatus.UNAUTHORIZED\n            response.custom_headers[\"WWW-Authenticate\"] = \"SCRAM-SHA-256\"\n            return\n\n        server_nonce: str = scram.generate_nonce()\n        server_first: bytes = scram.build_server_first_message(\n            server_nonce, client_nonce, verifier.salt, verifier.iterations\n        ).encode(\"utf-8\")\n\n        if len(sessions) > SESSION_HIGH_WATER_MARK:\n            while sessions:\n                key, session = sessions.popitem(last=False)\n                if session.time + SESSION_TIMEOUT > time.monotonic():\n                    sessions[key] = session\n                    sessions.move_to_end(key, last=False)\n                    break\n\n        sid = (\n            base64.urlsafe_b64encode(os.urandom(16))\n            .decode(\"ascii\")\n            .rstrip(\"=\")\n        )\n        assert sid not in sessions\n        sessions[sid] = Session(\n            time.monotonic(),\n            client_nonce,\n            server_nonce,\n            client_first_bare,\n            cb_flag,\n            server_first,\n            verifier,\n            mock_auth,\n            username,\n        )\n\n        server_first_str = base64.b64encode(server_first).decode(\"ascii\")\n        response.status = http.HTTPStatus.UNAUTHORIZED\n        response.custom_headers[\n            \"WWW-Authenticate\"\n        ] = f\"SCRAM-SHA-256 sid={sid}, data={server_first_str}\"\n\n    else:\n        session = sessions.pop(sid)\n        if session is None:\n            response.body = b\"Bad session ID\"\n            response.status = http.HTTPStatus.UNAUTHORIZED\n            response.custom_headers[\"WWW-Authenticate\"] = \"SCRAM-SHA-256\"\n            return\n\n        (\n            ts,\n            client_nonce,\n            server_nonce,\n            client_first_bare,\n            cb_flag,\n            server_first,\n            verifier,\n            mock_auth,\n            username,\n        ) = session\n        if ts + SESSION_TIMEOUT < time.monotonic():\n            response.body = b\"Session timed out\"\n            response.status = http.HTTPStatus.UNAUTHORIZED\n            response.custom_headers[\"WWW-Authenticate\"] = \"SCRAM-SHA-256\"\n            return\n\n        try:\n            (\n                cb_data,\n                client_proof,\n                proof_len,\n            ) = scram.parse_client_final_message(\n                data, client_nonce, server_nonce\n            )\n        except ValueError as ex:\n            if debug.flags.server:\n                markup.dump(ex)\n            response.body = f\"Bad client final message: {ex!s}\".encode(\"ascii\")\n            response.status = http.HTTPStatus.BAD_REQUEST\n            response.close_connection = True\n            return\n\n        client_final_without_proof = data[:-proof_len]\n\n        cb_data_ok = (cb_flag is False and cb_data == b\"biws\") or (\n            cb_flag is True and cb_data == b\"eSws\"\n        )\n        if not cb_data_ok:\n            response.body = (\n                b\"Malformed SCRAM message: \"\n                b\"Unexpected SCRAM channel-binding attribute \"\n                b\"in client-final-message.\"\n            )\n            response.status = http.HTTPStatus.BAD_REQUEST\n            response.close_connection = True\n            return\n\n        if (\n            not scram.verify_client_proof(\n                client_first_bare,\n                server_first,\n                client_final_without_proof,\n                verifier.stored_key,\n                client_proof,\n            )\n            or mock_auth\n        ):\n            response.body = b\"Authentication failed\"\n            response.status = http.HTTPStatus.UNAUTHORIZED\n            response.custom_headers[\"WWW-Authenticate\"] = \"SCRAM-SHA-256\"\n            return\n\n        server_final = base64.b64encode(\n            scram.build_server_final_message(\n                client_first_bare,\n                server_first,\n                client_final_without_proof,\n                verifier.server_key,\n            ).encode(\"utf-8\")\n        ).decode(\"ascii\")\n\n        try:\n            response.body = auth.generate_gel_token(\n                jws, roles=[username],\n            ).encode(\"ascii\")\n        except ValueError as ex:\n            if debug.flags.server:\n                markup.dump(ex)\n            response.body = b\"Authentication failed\"\n            response.status = http.HTTPStatus.UNAUTHORIZED\n            response.custom_headers[\"WWW-Authenticate\"] = \"SCRAM-SHA-256\"\n            return\n\n        response.custom_headers[\n            \"Authentication-Info\"\n        ] = f\"sid={sid}, data={server_final}\"\n\n\ndef get_scram_verifier(\n    user: str,\n    tenant: edbtenant.Tenant,\n) -> tuple[scram.SCRAMVerifier, bool]:\n    roles = tenant.get_roles()\n\n    rolerec = roles.get(user)\n    if rolerec is not None:\n        verifier_string = rolerec[\"password\"]\n        if verifier_string is not None:\n            verifier = scram.parse_verifier(verifier_string)\n            is_mock = False\n            return verifier, is_mock\n\n    # To avoid revealing the validity of the submitted user name,\n    # generate a mock verifier using a salt derived from the\n    # received user name and the cluster mock auth nonce.\n    # The same approach is taken by Postgres.\n    nonce = tenant.get_instance_data(\"mock_auth_nonce\")\n    salt = hashlib.sha256(nonce.encode() + user.encode()).digest()\n\n    verifier = scram.SCRAMVerifier(\n        mechanism=\"SCRAM-SHA-256\",\n        iterations=scram.DEFAULT_ITERATIONS,\n        salt=salt[: scram.DEFAULT_SALT_LENGTH],\n        stored_key=b\"\",\n        server_key=b\"\",\n    )\n    is_mock = True\n    return verifier, is_mock\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/__init__.py",
    "content": "from . import http\n\n__all__ = ('http', )\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/_static/interactions.js",
    "content": "document.addEventListener(\"DOMContentLoaded\", () => {\n  /** @type {HTMLElement | null} */\n  const sliderContainer = /** @type {HTMLElement | null} */ (\n    document.getElementById(\"slider-container\")\n  );\n\n  if (!sliderContainer) {\n    return;\n  }\n\n  /** @type {HTMLElement | null} */\n  const tabsContainer = /** @type {HTMLElement | null} */ (\n    document.getElementById(\"email-provider-tabs\")\n  );\n  if (tabsContainer) {\n    /** @type {HTMLElement[]} */\n    const tabButtons = /** @type {HTMLElement[]} */ (\n      Array.from(tabsContainer.children)\n    );\n    for (let i = 0; i < tabButtons.length; i++) {\n      const tab = tabButtons[i];\n      tab.addEventListener(\"click\", () => {\n        activateTab(i);\n      });\n      /** @param {KeyboardEvent} e */\n      tab.addEventListener(\"keydown\", (e) => {\n        switch (e.key) {\n          case \"ArrowLeft\":\n            e.preventDefault();\n            focusTab((i - 1 + tabButtons.length) % tabButtons.length);\n            break;\n          case \"ArrowRight\":\n            e.preventDefault();\n            focusTab((i + 1) % tabButtons.length);\n            break;\n          case \"Home\":\n            e.preventDefault();\n            focusTab(0);\n            break;\n          case \"End\":\n            e.preventDefault();\n            focusTab(tabButtons.length - 1);\n            break;\n          case \"Enter\":\n          case \" \":\n            e.preventDefault();\n            activateTab(i);\n            break;\n        }\n      });\n    }\n\n    /** @param {number} index */\n    function focusTab(index) {\n      for (let j = 0; j < tabButtons.length; j++) {\n        const t = tabButtons[j];\n        const isActive = j === index;\n        t.setAttribute(\"tabindex\", isActive ? \"0\" : \"-1\");\n      }\n      tabButtons[index].focus();\n    }\n\n    /** @param {number} index */\n    function activateTab(index) {\n      const tabChildren = /** @type {HTMLCollection} */ (\n        /** @type {HTMLElement} */ (tabsContainer).children\n      );\n      setActiveClass(tabChildren, index);\n      syncAriaState(tabChildren, /** @type {HTMLElement} */ (sliderContainer), index);\n      moveSliderToIndex(/** @type {HTMLElement} */ (sliderContainer), index);\n    }\n  } else {\n    /** @type {HTMLFormElement | null} */\n    const form = /** @type {HTMLFormElement | null} */ (\n      document.getElementById(\"email-factor\")\n    );\n    if (!form) {\n      return;\n    }\n    let mainFormAction = form.action;\n\n    /** @type {HTMLInputElement[]} */\n    const hiddenInputs = /** @type {HTMLInputElement[]} */ (\n      Array.from(form.querySelectorAll(\"input[type=hidden]\"))\n    ).filter((input) => !!input.dataset.secondaryValue);\n    /** @type {string[]} */\n    const hiddenInputValues = hiddenInputs.map((input) => input.value);\n\n    if (!sliderContainer.children[0].classList.contains(\"active\")) {\n      form.action = /** @type {any} */ (form.dataset).secondaryAction;\n      setInputValues(hiddenInputs);\n    }\n\n    /** @type {HTMLElement | null} */\n    const showBtn = /** @type {HTMLElement | null} */ (\n      document.getElementById(\"show-password-form\")\n    );\n    showBtn?.setAttribute(\"aria-controls\", \"panel-password\");\n    showBtn?.setAttribute(\"aria-expanded\", \"false\");\n    document\n      .getElementById(\"password-email\")\n      ?.setAttribute(\"aria-describedby\", \"show-password-form\");\n\n    document\n      .getElementById(\"hide-password-form\")\n      ?.setAttribute(\"aria-controls\", \"panel-password\");\n\n    showBtn?.addEventListener(\"click\", () => {\n        moveSliderToIndex(/** @type {HTMLElement} */ (sliderContainer), 1);\n\n        form.action = /** @type {any} */ (form.dataset).secondaryAction;\n        setInputValues(hiddenInputs);\n\n        document.getElementById(\"password\")?.focus({ preventScroll: true });\n        showBtn.setAttribute(\"aria-expanded\", \"true\");\n      });\n\n    document.getElementById(\"hide-password-form\")?.addEventListener(\"click\", () => {\n        moveSliderToIndex(/** @type {HTMLElement} */ (sliderContainer), 0);\n\n        form.action = mainFormAction;\n        setInputValues(hiddenInputs, hiddenInputValues);\n        showBtn?.setAttribute(\"aria-expanded\", \"false\");\n      });\n  }\n});\n\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n  /** @type {HTMLAnchorElement | null} */\n  const forgotLink = /** @type {HTMLAnchorElement | null} */ (\n    document.getElementById(\"forgot-password-link\")\n  );\n  // Find the email input near the forgot link; fall back to a known id.\n  /** @type {HTMLInputElement | null} */\n  const emailInput =\n    /** @type {HTMLInputElement | null} */ (\n      forgotLink?.closest(\"form\")?.querySelector('input[name=\"email\"]')\n    ) || /** @type {HTMLInputElement | null} */ (\n      document.getElementById(\"password-email\")\n    );\n  if (forgotLink && emailInput) {\n    const href = forgotLink.href;\n    emailInput.addEventListener(\"input\", (e) => {\n      const target = /** @type {HTMLInputElement} */ (e.target);\n      forgotLink.href = `${href}&email=${encodeURIComponent(target.value)}`;\n    });\n    forgotLink.href = `${href}&email=${encodeURIComponent(emailInput.value)}`;\n  }\n\n  /** @type {NodeListOf<HTMLInputElement>} */\n  const emailInputs = /** @type {NodeListOf<HTMLInputElement>} */ (\n    document.querySelectorAll(\"input[name=email]\")\n  );\n  for (const input of emailInputs) {\n    input.addEventListener(\"input\", (e) => {\n      const target = /** @type {HTMLInputElement} */ (e.target);\n      const val = target.value;\n      for (const _input of emailInputs) {\n        if (_input !== input) {\n          _input.value = val;\n        }\n      }\n    });\n  }\n});\n\n/**\n * @param {HTMLInputElement[]} inputs\n * @param {string[]} [values]\n */\nfunction setInputValues(inputs, values) {\n  for (let i = 0; i < inputs.length; i++) {\n    const input = inputs[i];\n    const secondary = /** @type {any} */ (input.dataset).secondaryValue;\n    input.value = values ? values[i] : secondary || \"\";\n  }\n}\n\nlet firstInteraction = true;\n\n/**\n * @param {HTMLElement} sliderContainer\n * @param {number} index\n */\nfunction moveSliderToIndex(sliderContainer, index) {\n  if (firstInteraction) {\n    firstInteraction = false;\n    // Fix the height of the main form card wrapper so the layout doesn't shift\n    // when tabs are clicked\n    const containerWrapper = document.getElementById(\"container-wrapper\");\n    if (containerWrapper) {\n      containerWrapper.style.height =\n        containerWrapper.getElementsByClassName(\"container\")[0].clientHeight +\n        \"px\";\n    }\n\n    // Set the height for the first time as transition from 'auto' doesn't work\n    sliderContainer.style.height = `${\n      sliderContainer.getElementsByClassName(\"active\")[0].scrollHeight\n    }px`;\n  }\n\n  setActiveClass(sliderContainer.children, index);\n  sliderContainer.style.transform = `translateX(${-100 * index}%)`;\n  sliderContainer.style.height = `${sliderContainer.children[index].scrollHeight}px`;\n}\n\n/**\n * @param {HTMLCollection} tabButtons\n * @param {HTMLElement} sliderContainer\n * @param {number} index\n */\nfunction syncAriaState(tabButtons, sliderContainer, index) {\n  for (let i = 0; i < tabButtons.length; i++) {\n    const tab = /** @type {HTMLElement} */ (tabButtons[i]);\n    const isActive = i === index;\n    tab.setAttribute(\"aria-selected\", isActive ? \"true\" : \"false\");\n    tab.setAttribute(\"tabindex\", isActive ? \"0\" : \"-1\");\n  }\n\n  const panels = /** @type {HTMLCollectionOf<HTMLElement>} */ (sliderContainer.children);\n  for (let i = 0; i < panels.length; i++) {\n    const isActive = i === index;\n    if (isActive) {\n      panels[i].removeAttribute(\"hidden\");\n      panels[i].setAttribute(\"aria-hidden\", \"false\");\n    } else {\n      panels[i].setAttribute(\"hidden\", \"\");\n      panels[i].setAttribute(\"aria-hidden\", \"true\");\n    }\n  }\n}\n\n/**\n * @param {HTMLCollection} items\n * @param {number} index\n */\nfunction setActiveClass(items, index) {\n  for (let i = 0; i < items.length; i++) {\n    if (i === index) {\n      items[i].classList.add(\"active\");\n    } else {\n      items[i].classList.remove(\"active\");\n    }\n  }\n}\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/_static/styles.css",
    "content": "@font-face {\n  font-family: \"Roboto Flex\";\n  font-style: normal;\n  font-display: swap;\n  font-weight: 100 1000;\n  src: url(roboto-flex-latin-wght-normal.woff2) format(\"woff2-variations\");\n  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,\n    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,\n    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n\nbody {\n  background: #f3f4f6;\n  margin: 0;\n  padding: 0;\n  min-height: 100vh;\n  height: max-content;\n  display: grid;\n  grid-template-rows: minmax(120px, 1fr) auto minmax(120px, 1fr);\n  justify-content: center;\n  justify-items: center;\n  font-family: \"Roboto Flex\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n.brand-logo {\n  margin-bottom: 16px;\n  margin-top: 32px;\n  align-self: end;\n}\n.brand-logo img {\n  max-width: 300px;\n  max-height: 100px;\n}\n\n.container-wrapper {\n  grid-row: 2;\n}\n.container {\n  background: #fff;\n  padding: 24px;\n  padding-bottom: 16px;\n  width: 326px;\n  border-radius: 16px;\n  box-shadow: 0px 2px 2px rgba(3, 7, 18, 0.02), 0px 7px 7px rgba(3, 7, 18, 0.03),\n    0px 16px 16px rgba(3, 7, 18, 0.05);\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n\n.container form {\n  display: contents;\n}\n\n.container h1 {\n  margin: 0;\n  color: #495057;\n  font-size: 22px;\n  font-style: normal;\n  font-weight: 550;\n  margin-bottom: 20px;\n}\n.container h1 span {\n  opacity: 0.7;\n}\n\n.container input {\n  border-radius: 8px;\n  border: 1px solid #dee2e6;\n  background: #f8f9fa;\n  line-height: 40px;\n  padding: 0 14px;\n  color: #495057;\n  font-family: inherit;\n  font-size: 16px;\n  font-weight: 400;\n  outline: none;\n  margin-bottom: 16px;\n}\n\n.container input:focus-visible {\n  outline: 3px solid var(--accent-focus-color);\n}\n\n.container label {\n  color: #495057;\n  font-size: 16px;\n  font-weight: 450;\n  line-height: 18px;\n  margin-bottom: 8px;\n}\n\n.container button {\n  display: grid;\n  align-items: center;\n  grid-template-columns: 1fr auto 1fr;\n  padding: 0 12px;\n  height: 46px;\n  border-radius: 8px;\n  background: var(--accent-bg-color);\n  border: none;\n  color: var(--accent-bg-text-color);\n  font-family: inherit;\n  font-size: 17px;\n  font-weight: 550;\n  cursor: pointer;\n  margin: 8px 0;\n}\n.container button span {\n  grid-column: 2;\n  margin: 0 12px;\n}\n.container button svg {\n  margin-left: 8px;\n  justify-self: end;\n}\n.container button:hover {\n  background: var(--accent-bg-hover-color);\n}\n.container button:focus-visible {\n  outline: 3px solid var(--accent-focus-color);\n  outline-offset: 2px;\n}\n.container button:disabled {\n  opacity: 0.6;\n  pointer-events: none;\n}\n.container button.secondary {\n  background: none;\n  border: 1px solid #ced4da;\n  color: #6c757d;\n  font-weight: 500;\n}\n.container button.secondary svg {\n  color: #adb5bd;\n}\n.container button.secondary:hover {\n  background: #f5f6f8;\n}\n.container button.icon-only {\n  display: flex;\n  width: 46px;\n  padding: 0;\n  justify-content: center;\n  margin-right: 12px;\n  flex-shrink: 0;\n}\n.container button.icon-only svg {\n  margin-left: 0;\n  transform: scaleX(-1);\n}\n\n.button-group {\n  display: flex;\n}\n.button-group button:not(.icon-only) {\n  flex-grow: 1;\n}\n\n.slider-container {\n  width: calc(100% + 48px);\n  display: flex;\n  align-items: start;\n  margin: 0 -24px;\n  transition: transform 0.3s, height 0.3s;\n}\n\n.slider-section {\n  width: calc(100% - 48px);\n  margin: 0 24px;\n  flex-shrink: 0;\n  display: flex;\n  flex-direction: column;\n  height: 0;\n  visibility: hidden;\n  opacity: 0;\n  transition: opacity 0.15s 0s linear, visibility 0s 0.3s linear;\n}\n.slider-section > *,\n.slider-section > form > * {\n  flex-shrink: 0;\n}\n.slider-section.active {\n  height: auto;\n  visibility: visible;\n  opacity: 1;\n  transition-delay: 0s;\n}\n\n.tabs {\n  display: flex;\n  justify-content: center;\n  gap: 12px;\n  margin-bottom: 20px;\n}\n.tab {\n  position: relative;\n  display: flex;\n  height: 38px;\n  align-items: center;\n  padding: 0 12px;\n  color: #6c757d;\n  font-size: 15px;\n  font-weight: 550;\n  cursor: pointer;\n}\n.tab svg {\n  position: absolute;\n  bottom: -1px;\n  left: 0;\n  width: 100%;\n  fill: var(--accent-text-color);\n  opacity: 0;\n  transition: opacity 0.3s;\n}\n.tab.active {\n  color: #495057;\n}\n.tab.active svg {\n  opacity: 1;\n}\n\na {\n  outline: 0;\n  text-decoration: none;\n}\na:focus-visible {\n  text-decoration: underline solid 2px var(--accent-focus-color);\n  text-underline-offset: 4px;\n}\n\n.field-header {\n  display: flex;\n  justify-content: space-between;\n}\n.field-note {\n  color: #97a1ab;\n  font-size: 14px;\n  font-weight: 400;\n}\na.field-note:hover {\n  color: var(--accent-text-color);\n}\n\n.oauth-buttons {\n  display: flex;\n  flex-direction: column;\n  margin-bottom: 8px;\n  gap: 16px;\n}\n.oauth-buttons a {\n  display: flex;\n  align-items: center;\n  justify-content: start;\n  height: 46px;\n  border-radius: 8px;\n  padding: 0 12px;\n  border: 1px solid #dee2e6;\n  text-decoration: none;\n  color: #495057;\n  font-size: 16px;\n  font-weight: 450;\n}\n.oauth-buttons a:hover {\n  background: #f5f6f8;\n}\n.oauth-buttons a:focus-visible {\n  outline: 3px solid var(--accent-focus-color);\n}\n.oauth-buttons a span {\n  margin-left: 12px;\n}\n.oauth-buttons a img {\n  width: 32px;\n  height: 32px;\n  object-fit: contain;\n}\n.oauth-buttons.collapsed {\n  flex-direction: row;\n  flex-wrap: wrap;\n}\n.oauth-buttons.collapsed a {\n  padding: 0;\n  width: 46px;\n  justify-content: center;\n  flex-shrink: 0;\n}\n.oauth-buttons.collapsed a span {\n  display: none;\n}\n\n.divider {\n  display: flex;\n  align-items: center;\n  color: #6c757d;\n  font-size: 16px;\n  font-weight: 450;\n  line-height: 19px;\n  margin-top: 12px;\n  margin-bottom: 16px;\n}\n.divider span {\n  margin: 0 16px;\n}\n.divider:before,\n.divider:after {\n  content: \"\";\n  height: 0;\n  border-bottom: 1px solid #dee2e6;\n  flex-grow: 1;\n}\n\n.bottom-note {\n  color: #6c757d;\n  font-size: 16px;\n  font-weight: 400;\n  line-height: 19px;\n  margin-top: 4px;\n}\n.bottom-note a {\n  color: var(--accent-text-color);\n}\n\n.error-message,\n.success-message {\n  display: flex;\n  padding: 10px 12px;\n  align-items: center;\n  gap: 12px;\n  border-radius: 8px;\n  border: 1px solid #f9827b;\n  background: #fee6e5;\n  color: #eb4b42;\n  font-size: 14px;\n  font-weight: 400;\n  line-height: 19px;\n  margin-bottom: 16px;\n}\n.error-message svg,\n.success-message svg {\n  flex-shrink: 0;\n}\n.error-message a,\n.success-message a {\n  color: var(--accent-text-color);\n}\n.error-message b,\n.success-message b {\n  font-weight: 600;\n}\n\n.success-message {\n  color: #1f8aed;\n  border-color: #1f8aed;\n  background: #e4f1fc;\n}\n\n.no-webauthn-error {\n  border: 1px solid #f9827b;\n  background: #fee6e5;\n  color: #eb4b42;\n  margin: 8px 0;\n  border-radius: 8px;\n  padding: 10px 12px;\n}\n\n@media (prefers-color-scheme: dark) {\n  body {\n    background: #191c1f;\n    color: #dee2e6;\n  }\n\n  .container {\n    background: #2a2f34;\n  }\n  .container h1 {\n    color: #dee2e6;\n  }\n\n  .container button.secondary {\n    border-color: #495057;\n    color: #ced4da;\n  }\n  .container button.secondary svg {\n    color: #6c757d;\n  }\n  .container button.secondary:hover {\n    background: #363c42;\n  }\n\n  .container input {\n    border-color: #495057;\n    background: #31373d;\n    color: #dee2e6;\n  }\n  .container input:focus-visible {\n    outline-color: var(--accent-focus-dark-color);\n  }\n\n  .container label {\n    color: #dee2e6;\n  }\n\n  a:focus-visible {\n    text-decoration-color: var(--accent-focus-dark-color);\n  }\n\n  .field-note {\n    color: #adb5bd;\n  }\n  a.field-note:hover {\n    color: var(--accent-text-dark-color);\n  }\n\n  .oauth-buttons a {\n    border-color: #495057;\n    color: #dee2e6;\n  }\n  .oauth-buttons a:hover {\n    background: #363c42;\n  }\n  .oauth-buttons a:focus-visible {\n    outline-color: var(--accent-focus-dark-color);\n  }\n\n  .divider {\n    color: #6c757d;\n  }\n  .divider:before,\n  .divider:after {\n    border-bottom-color: #495057;\n  }\n\n  .tab {\n    color: #adb5bd;\n  }\n  .tab.active {\n    color: #dee2e6;\n  }\n\n  .bottom-note {\n    color: #ced4da;\n  }\n  .bottom-note a {\n    color: var(--accent-text-dark-color);\n  }\n\n  .error-message a,\n  .success-message a {\n    color: var(--accent-text-dark-color);\n  }\n  .error-message {\n    background: #423336;\n    border-color: #a1433d;\n  }\n  .success-message {\n    background: #293a4a;\n  }\n\n  .no-webauthn-error {\n    background: #423336;\n    border-color: #a1433d;\n  }\n}\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/_static/utils.js",
    "content": "/**\n * @param {(form: HTMLFormElement) => void} handler\n * @returns Uint8Array\n */\nexport function addWebAuthnSubmitHandler(handler) {\n  document.addEventListener(\"DOMContentLoaded\", () => {\n    if (!window.PublicKeyCredential) {\n      console.error(\"WebAuthn is not supported in this browser.\");\n\n      for (const button of [\n        document.getElementById(\"webauthn-signin\"),\n        document.getElementById(\"webauthn-signup\"),\n      ]) {\n        if (button) {\n          const newEl = document.createElement(\"div\");\n          newEl.classList.add(\"no-webauthn-error\");\n          newEl.appendChild(\n            document.createTextNode(\n              `Your browser does not support the WebAuthn API. ` +\n                `Use another login method, or upgrade your browser.`\n            )\n          );\n          button.parentNode.replaceChild(newEl, button);\n        }\n      }\n      return;\n    }\n\n    const emailFactorForm = document.getElementById(\"email-factor\");\n\n    if (emailFactorForm === null) {\n      return;\n    }\n\n    emailFactorForm.addEventListener(\"submit\", (event) => {\n      if (new URL(emailFactorForm.action).pathname == location.pathname) {\n        event.preventDefault();\n        handler(emailFactorForm);\n      }\n    });\n  });\n}\n\n/**\n * Decode a base64url encoded string\n * @param {string} base64UrlString\n * @returns Uint8Array\n */\nexport function decodeBase64Url(base64UrlString) {\n  return Uint8Array.from(\n    atob(base64UrlString.replace(/-/g, \"+\").replace(/_/g, \"/\")),\n    (c) => c.charCodeAt(0)\n  );\n}\n\n/**\n * Encode a Uint8Array to a base64url encoded string\n * @param {Uint8Array} bytes\n * @returns string\n */\nexport function encodeBase64Url(bytes) {\n  return btoa(String.fromCharCode(...bytes))\n    .replace(/\\+/g, \"-\")\n    .replace(/\\//g, \"_\")\n    .replace(/=/g, \"\");\n}\n\n/**\n * Parse an HTTP Response object. Allows passing in custom handlers for\n * different status codes and error.type values\n *\n * @param {Response} response\n * @param {Function[]=} handlers\n */\nexport async function parseResponseAsJSON(response, handlers = []) {\n  const bodyText = await response.text();\n\n  if (!response.ok) {\n    let error;\n    try {\n      error = JSON.parse(bodyText)?.error;\n    } catch (e) {\n      throw new Error(\n        `Failed to parse body as JSON. Status: ${response.status} ${response.statusText}. Body: ${bodyText}`\n      );\n    }\n\n    for (const handler of handlers) {\n      handler(response, error);\n    }\n\n    throw new Error(\n      `Response was not OK. Status: ${response.status} ${response.statusText}. Body: ${bodyText}`\n    );\n  }\n\n  return JSON.parse(bodyText);\n}\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/_static/webauthn-authenticate.js",
    "content": "import {\n  addWebAuthnSubmitHandler,\n  decodeBase64Url,\n  encodeBase64Url,\n  parseResponseAsJSON,\n} from \"./utils.js\";\n\naddWebAuthnSubmitHandler(onAuthenticateSubmit);\n\nlet authenticating = false;\n\n/**\n * Handle the form submission for WebAuthn authentication\n * @param {HTMLFormElement} form\n * @returns void\n */\nasync function onAuthenticateSubmit(form) {\n  if (authenticating) {\n    return;\n  }\n\n  authenticating = true;\n  const signinButton = document.getElementById(\"webauthn-signin\");\n  signinButton.disabled = true;\n\n  const formData = new FormData(form);\n  const email = formData.get(\"email\");\n  const provider = \"builtin::local_webauthn\";\n  const challenge = formData.get(\"challenge\");\n  const redirectOnFailure = formData.get(\"redirect_on_failure\");\n  const redirectTo = formData.get(\"redirect_to\");\n\n  const missingFields = Object.entries({\n    email,\n    challenge,\n    redirectTo,\n  }).filter(([k, v]) => !v);\n  if (missingFields.length > 0) {\n    throw new Error(\n      \"Missing required parameters: \" + missingFields.map(([k]) => k).join(\", \")\n    );\n  }\n\n  try {\n    const response = await authenticate({\n      email,\n      provider,\n      challenge,\n    });\n\n    const redirectUrl = new URL(redirectTo);\n    if (\"code\" in response) {\n      redirectUrl.searchParams.append(\"code\", response.code);\n    } else if (\"verification_email_sent_at\" in response) {\n      redirectUrl.searchParams.append(\n        \"verification_email_sent_at\",\n        response.verification_email_sent_at\n      );\n    }\n\n    window.location.href = redirectUrl.href;\n  } catch (error) {\n    console.error(\"Failed to authenticate WebAuthn credentials:\", error);\n    const url = new URL(redirectOnFailure ?? redirectTo);\n    url.searchParams.append(\"error\", error.message);\n    window.location.href = url.href;\n  } finally {\n    authenticating = false;\n    signinButton.disabled = false;\n  }\n}\n\nconst WEBAUTHN_OPTIONS_URL = new URL(\n  \"../webauthn/authenticate/options\",\n  window.location\n);\nconst WEBAUTHN_AUTHENTICATE_URL = new URL(\n  \"../webauthn/authenticate\",\n  window.location\n);\n/**\n * Authenticate an existing WebAuthn credential for the given email address\n * @param {Object} props - The properties for registration\n * @param {string} props.email - Email address to register\n * @param {string} props.provider - WebAuthn provider\n * @param {string} props.challenge - PKCE challenge\n * @returns {Promise<object>} - The server response\n */\nexport async function authenticate({ email, provider, challenge }) {\n  // Check if WebAuthn is supported\n  if (!window.PublicKeyCredential) {\n    console.error(\"WebAuthn is not supported in this browser.\");\n    return;\n  }\n\n  // Fetch WebAuthn options from the server\n  const options = await getAuthenticateOptions(email);\n\n  // Get the existing credentials assertion\n  const assertion = await navigator.credentials.get({\n    publicKey: {\n      ...options,\n      challenge: decodeBase64Url(options.challenge),\n      allowCredentials: options.allowCredentials.map((credential) => ({\n        ...credential,\n        id: decodeBase64Url(credential.id),\n      })),\n    },\n  });\n\n  // Register the credentials on the server\n  return await authenticateAssertion({\n    email,\n    assertion,\n    challenge,\n  });\n}\n\n/**\n * Fetch WebAuthn options from the server\n * @param {string} email - Email address to register\n * @returns {Promise<globalThis.PublicKeyCredentialCreationOptions>}\n */\nasync function getAuthenticateOptions(email) {\n  const url = new URL(WEBAUTHN_OPTIONS_URL);\n  url.searchParams.set(\"email\", email);\n\n  const optionsResponse = await fetch(url, {\n    method: \"GET\",\n  });\n\n  return parseResponseAsJSON(optionsResponse, [\n    (response, error) => {\n      if (response.status === 400 && error?.type === \"InvalidData\") {\n        throw new Error(error?.message ?? \"Email is invalid\");\n      }\n      if (!response.ok) {\n        console.error(\n          \"Failed to fetch WebAuthn options:\",\n          optionsResponse.statusText\n        );\n        console.error(error);\n        throw new Error(\"Failed to fetch WebAuthn options\");\n      }\n    },\n  ]);\n}\n\n/**\n * Authenticate the credentials on the server\n * @param {Object} props\n * @param {string} props.email\n * @param {Object} props.assertion\n * @param {string} props.provider\n * @param {string} props.challenge\n * @returns {Promise<Object>}\n */\nasync function authenticateAssertion(props) {\n  // Assertion includes raw bytes, so need to be encoded as base64url\n  // for transmission\n  const encodedAssertion = {\n    type: props.assertion.type,\n    id: props.assertion.id,\n    authenticatorAttachment: props.assertion.authenticatorAttachment,\n    clientExtensionResults: props.assertion.getClientExtensionResults(),\n    rawId: encodeBase64Url(new Uint8Array(props.assertion.rawId)),\n    response: {\n      authenticatorData: encodeBase64Url(\n        new Uint8Array(props.assertion.response.authenticatorData)\n      ),\n      clientDataJSON: encodeBase64Url(\n        new Uint8Array(props.assertion.response.clientDataJSON)\n      ),\n      signature: encodeBase64Url(\n        new Uint8Array(props.assertion.response.signature)\n      ),\n      userHandle: props.assertion.response.userHandle\n        ? encodeBase64Url(new Uint8Array(props.assertion.response.userHandle))\n        : null,\n    },\n  };\n\n  const authenticateResponse = await fetch(WEBAUTHN_AUTHENTICATE_URL, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      email: props.email,\n      assertion: encodedAssertion,\n      provider: props.provider,\n      challenge: props.challenge,\n    }),\n  });\n\n  return await parseResponseAsJSON(authenticateResponse, [\n    (response, error) => {\n      if (response.status === 401 && error?.type === \"VerificationRequired\") {\n        console.error(\n          \"User's email is not verified\",\n          response.statusText,\n          JSON.stringify(error)\n        );\n        throw new Error(\n          \"Please verify your email before attempting to sign in.\"\n        );\n      }\n    },\n    (response, error) => {\n      console.error(\n        \"Failed to authenticate WebAuthn credentials:\",\n        response.statusText,\n        JSON.stringify(error)\n      );\n      throw new Error(\"Failed to authenticate WebAuthn credentials\");\n    },\n  ]);\n}\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/_static/webauthn-register.js",
    "content": "import {\n  addWebAuthnSubmitHandler,\n  decodeBase64Url,\n  encodeBase64Url,\n} from \"./utils.js\";\n\naddWebAuthnSubmitHandler(onRegisterSubmit);\n\nlet registering = false;\n\n/**\n * Handle the form submission for WebAuthn registration\n * @param {HTMLFormElement} form\n * @returns void\n */\nexport async function onRegisterSubmit(form) {\n  if (registering) {\n    return;\n  }\n\n  registering = true;\n  const registerButton = document.getElementById(\"webauthn-signup\");\n  registerButton.disabled = true;\n\n  const formData = new FormData(form);\n  const email = formData.get(\"email\");\n  const provider = \"builtin::local_webauthn\";\n  const challenge = formData.get(\"challenge\");\n  const redirectOnFailure = formData.get(\"redirect_on_failure\");\n  const redirectTo = formData.get(\"redirect_to\");\n  const verifyUrl = formData.get(\"verify_url\");\n\n  try {\n    const missingFields = Object.entries({\n      email,\n      provider,\n      challenge,\n      redirectTo,\n      verifyUrl,\n    }).filter(([k, v]) => !v);\n    if (missingFields.length > 0) {\n      throw new Error(\n        \"Missing required parameters: \" +\n          missingFields.map(([k]) => k).join(\", \")\n      );\n    }\n\n    const response = await register({\n      email,\n      provider,\n      challenge,\n      verifyUrl,\n    });\n\n    const redirectUrl = new URL(redirectTo);\n    redirectUrl.searchParams.append(\"isSignUp\", \"true\");\n    if (\"code\" in response) {\n      redirectUrl.searchParams.append(\"code\", response.code);\n    } else if (\"verification_email_sent_at\" in response) {\n      redirectUrl.searchParams.append(\n        \"verification_email_sent_at\",\n        response.verification_email_sent_at\n      );\n    }\n    if (\"email\" in response) {\n      redirectUrl.searchParams.append(\"email\", response.email);\n    }\n\n    window.location.href = redirectUrl.href;\n  } catch (error) {\n    console.error(\"Failed to register WebAuthn credentials:\", error);\n    const url = new URL(redirectOnFailure ?? redirectTo);\n    url.searchParams.append(\"error\", error.message);\n    window.location.href = url.href;\n  } finally {\n    registering = false;\n    registerButton.disabled = false;\n  }\n}\n\nconst WEBAUTHN_OPTIONS_URL = new URL(\n  \"../webauthn/register/options\",\n  window.location\n);\nconst WEBAUTHN_REGISTER_URL = new URL(\"../webauthn/register\", window.location);\n\n/**\n * Register a new WebAuthn credential for the given email address\n * @param {Object} props - The properties for registration\n * @param {string} props.email - Email address to register\n * @param {string} props.provider - WebAuthn provider\n * @param {string} props.challenge - PKCE challenge\n * @param {string} props.verifyUrl - URL to verify email after registration\n * @returns {Promise<object>} - The server response\n */\nexport async function register({ email, provider, challenge, verifyUrl }) {\n  // Check if WebAuthn is supported\n  if (!window.PublicKeyCredential) {\n    console.error(\"WebAuthn is not supported in this browser.\");\n    return;\n  }\n\n  // Fetch WebAuthn options from the server\n  const options = await getCreateOptions(email);\n\n  // Register the new credential\n  const credentials = await navigator.credentials.create({\n    publicKey: {\n      ...options,\n      challenge: decodeBase64Url(options.challenge),\n      user: {\n        ...options.user,\n        id: decodeBase64Url(options.user.id),\n      },\n    },\n  });\n\n  // Register the credentials on the server\n  return await registerCredentials({\n    email,\n    credentials,\n    provider,\n    challenge,\n    verifyUrl,\n  });\n}\n\n/**\n * Fetch WebAuthn options from the server\n * @param {string} email - Email address to register\n * @returns {Promise<globalThis.PublicKeyCredentialCreationOptions>}\n */\nasync function getCreateOptions(email) {\n  const url = new URL(WEBAUTHN_OPTIONS_URL);\n  url.searchParams.set(\"email\", email);\n\n  const optionsResponse = await fetch(url, {\n    method: \"GET\",\n  });\n\n  if (!optionsResponse.ok) {\n    console.error(\n      \"Failed to fetch WebAuthn options:\",\n      optionsResponse.statusText\n    );\n    console.error(await optionsResponse.text());\n    throw new Error(\"Failed to fetch WebAuthn options\");\n  }\n\n  try {\n    return await optionsResponse.json();\n  } catch (e) {\n    console.error(\"Failed to parse WebAuthn options:\", e);\n    throw new Error(\"Failed to parse WebAuthn options\");\n  }\n}\n\n/**\n * Register the credentials on the server\n * @param {Object} props\n * @param {string} props.email\n * @param {Object} props.credentials\n * @param {string} props.provider\n * @param {string} props.challenge\n * @param {string} props.verifyUrl\n * @returns {Promise<Object>}\n */\nasync function registerCredentials(props) {\n  // Credentials include raw bytes, so need to be encoded as base64url\n  // for transmission\n  const encodedCredentials = {\n    type: props.credentials.type,\n    authenticatorAttachment: props.credentials.authenticatorAttachment,\n    clientExtensionResults: props.credentials.getClientExtensionResults(),\n    id: props.credentials.id,\n    rawId: encodeBase64Url(new Uint8Array(props.credentials.rawId)),\n    response: {\n      attestationObject: encodeBase64Url(\n        new Uint8Array(props.credentials.response.attestationObject)\n      ),\n      clientDataJSON: encodeBase64Url(\n        new Uint8Array(props.credentials.response.clientDataJSON)\n      ),\n    },\n  };\n\n  const registerResponse = await fetch(WEBAUTHN_REGISTER_URL, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      email: props.email,\n      credentials: encodedCredentials,\n      provider: props.provider,\n      challenge: props.challenge,\n      verify_url: props.verifyUrl,\n    }),\n  });\n\n  if (!registerResponse.ok) {\n    console.error(\n      \"Failed to register WebAuthn credentials:\",\n      registerResponse.statusText\n    );\n    console.error(await registerResponse.text());\n    throw new Error(\"Failed to register WebAuthn credentials\");\n  }\n\n  try {\n    return await registerResponse.json();\n  } catch (e) {\n    console.error(\"Failed to parse WebAuthn registration result:\", e);\n    throw new Error(\"Failed to parse WebAuthn registration result\");\n  }\n}\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/apple.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import Any\nimport uuid\nimport urllib.parse\n\n\nfrom . import base\n\n\nclass AppleProvider(base.OpenIDConnectProvider):\n    def __init__(self, *args: Any, **kwargs: Any):\n        super().__init__(\n            \"apple\",\n            \"https://appleid.apple.com\",\n            *args,\n            **kwargs,\n        )\n\n    async def get_code_url(\n        self, state: str, redirect_uri: str, additional_scope: str\n    ) -> str:\n        oidc_config = await self._get_oidc_config()\n        params = {\n            \"client_id\": self.client_id,\n            # Non-standard \"name\" scope\n            \"scope\": f\"openid email name {additional_scope}\",\n            \"state\": state,\n            \"redirect_uri\": redirect_uri,\n            \"nonce\": str(uuid.uuid4()),\n            \"response_type\": \"code id_token\",\n            \"response_mode\": \"form_post\",\n        }\n        encoded = urllib.parse.urlencode(params)\n        return f\"{oidc_config.authorization_endpoint}?{encoded}\"\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/azure.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom typing import Any\n\nfrom . import base\n\n\nclass AzureProvider(base.OpenIDConnectProvider):\n    def __init__(self, *args: Any, **kwargs: Any):\n        super().__init__(\n            \"azure\",\n            \"https://login.microsoftonline.com/common/v2.0\",\n            *args,\n            **kwargs,\n        )\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/base.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport uuid\nimport urllib.parse\nimport enum\nimport logging\n\nfrom typing import Any, Callable\nfrom datetime import datetime\n\nfrom . import data, errors\nfrom edb.server.http import HttpClient\nfrom edb.server import auth as jwt_auth\nfrom edb.server.protocol.auth_ext import util as auth_util\nfrom edb.server import metrics\n\nlogger = logging.getLogger(\"edb.server.ext.auth\")\n\n\nclass BaseProvider:\n    def __init__(\n        self,\n        name: str,\n        issuer_url: str,\n        client_id: str,\n        client_secret: str,\n        *,\n        additional_scope: str | None,\n        http_factory: Callable[..., HttpClient],\n    ):\n        self.name = name\n        self.issuer_url = issuer_url\n        self.client_id = client_id\n        self.client_secret = client_secret\n        self.http_factory = http_factory\n        self.additional_scope = additional_scope\n\n    async def get_code_url(\n        self, state: str, redirect_uri: str, additional_scope: str\n    ) -> str:\n        raise NotImplementedError\n\n    async def exchange_code(\n        self, code: str, redirect_uri: str\n    ) -> data.OAuthAccessTokenResponse:\n        raise NotImplementedError\n\n    async def fetch_user_info(\n        self, token_response: data.OAuthAccessTokenResponse\n    ) -> data.UserInfo:\n        raise NotImplementedError\n\n    def _maybe_isoformat_to_timestamp(self, value: str | None) -> float | None:\n        return datetime.fromisoformat(value).timestamp() if value else None\n\n\nclass ContentType(enum.StrEnum):\n    JSON = \"application/json\"\n    FORM_ENCODED = \"application/x-www-form-urlencoded\"\n\n\nclass OpenIDConnectProvider(BaseProvider):\n    def __init__(\n        self,\n        name: str,\n        issuer_url: str,\n        *args: Any,\n        **kwargs: Any,\n    ):\n        super().__init__(name, issuer_url, *args, **kwargs)\n\n    async def get_code_url(\n        self, state: str, redirect_uri: str, additional_scope: str\n    ) -> str:\n        oidc_config = await self._get_oidc_config()\n        params = {\n            \"client_id\": self.client_id,\n            \"scope\": f\"openid profile email {additional_scope}\",\n            \"state\": state,\n            \"redirect_uri\": redirect_uri,\n            \"nonce\": str(uuid.uuid4()),\n            \"response_type\": \"code\",\n        }\n        encoded = urllib.parse.urlencode(params)\n        return f\"{oidc_config.authorization_endpoint}?{encoded}\"\n\n    async def exchange_code(\n        self, code: str, redirect_uri: str\n    ) -> data.OpenIDConnectAccessTokenResponse:\n        oidc_config = await self._get_oidc_config()\n\n        token_endpoint = urllib.parse.urlparse(oidc_config.token_endpoint)\n        async with self.http_factory(\n            base_url=f\"{token_endpoint.scheme}://{token_endpoint.netloc}\"\n        ) as client:\n            request_body = {\n                \"grant_type\": \"authorization_code\",\n                \"code\": code,\n                \"client_id\": self.client_id,\n                \"client_secret\": self.client_secret,\n                \"redirect_uri\": redirect_uri,\n            }\n            headers = {\"Accept\": ContentType.JSON.value}\n            resp = await client.post(\n                token_endpoint.path,\n                data=request_body,\n                headers=headers,\n            )\n            if resp.status_code >= 400:\n                raise errors.OAuthProviderFailure(\n                    f\"Failed to exchange code: {resp.text}\"\n                )\n            content_type = resp.headers.get('Content-Type')\n            if content_type.startswith(str(ContentType.JSON)):\n                response_body = resp.json()\n            else:\n                response_body = {\n                    k: v[0] if len(v) == 1 else v\n                    for k, v in urllib.parse.parse_qs(resp.text).items()\n                }\n\n            return data.OpenIDConnectAccessTokenResponse(**response_body)\n\n    async def fetch_user_info(\n        self, token_response: data.OAuthAccessTokenResponse\n    ) -> data.UserInfo:\n        if not isinstance(\n            token_response, data.OpenIDConnectAccessTokenResponse\n        ):\n            raise TypeError(\n                \"token_response must be of type \"\n                \"OpenIDConnectAccessTokenResponse\"\n            )\n        id_token = token_response.id_token\n\n        # Retrieve JWK Set, potentially from the cache\n        oidc_config = await self._get_oidc_config()\n        try:\n            async def fetcher(url: str) -> jwt_auth.JWKSet:\n                jwks_uri = urllib.parse.urlparse(url)\n                async with self.http_factory(\n                    base_url=f\"{jwks_uri.scheme}://{jwks_uri.netloc}\"\n                ) as client:\n                    r = await client.get(jwks_uri.path, cache=True)\n                    jwk_set = jwt_auth.JWKSet()\n                    jwk_set.load_json(r.text)\n                    jwk_set.default_validation_context.allow(\n                        \"aud\", [self.client_id]\n                    )\n                    jwk_set.default_validation_context.require_expiry()\n                    metrics.auth_provider_jwkset_fetch_success.inc(\n                        1.0, self.name\n                    )\n                    return jwk_set\n\n            jwk_set = await auth_util.get_remote_jwtset(\n                oidc_config.jwks_uri, fetcher\n            )\n        except Exception as e:\n            metrics.auth_provider_jwkset_fetch_errors.inc(1.0, self.name)\n            logger.exception(\n                f\"Failed to fetch JWK Set from provider {oidc_config.jwks_uri}\"\n            )\n            raise errors.MisconfiguredProvider(\n                f\"Failed to fetch JWK Set from provider {oidc_config.jwks_uri}\"\n            ) from e\n\n        # Load the token as a JWT object and verify it directly. This will\n        # validate the audience and expiry.\n        try:\n            payload = jwk_set.validate(id_token)\n        except Exception as e:\n            metrics.auth_provider_token_validation_errors.inc(1.0, self.name)\n            raise errors.MisconfiguredProvider(\n                \"Failed to parse ID token with provider keyset\"\n            ) from e\n\n        metrics.auth_provider_token_validation_success.inc(1.0, self.name)\n\n        return data.UserInfo(\n            sub=str(payload[\"sub\"]),\n            name=payload.get(\"name\"),\n            email=payload.get(\"email\"),\n            picture=payload.get(\"picture\"),\n            source_id_token=id_token,\n        )\n\n    async def _get_oidc_config(self) -> data.OpenIDConfig:\n        client = self.http_factory(base_url=self.issuer_url)\n        response = await client.get(\n            '/.well-known/openid-configuration',\n            cache=True\n        )\n        config = response.json()\n        return data.OpenIDConfig(**config)\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/config.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom typing import Literal, Optional\nfrom dataclasses import dataclass\nimport urllib.parse\n\n\nfrom edb.ir import statypes\n\n\nVerificationMethod = Literal['Link', 'Code']\n\n\nclass UIConfig:\n    app_name: Optional[str]\n    logo_url: Optional[str]\n    dark_logo_url: Optional[str]\n    brand_color: Optional[str]\n    redirect_to: str\n    redirect_to_on_signup: Optional[str]\n\n\n@dataclass\nclass AppDetailsConfig:\n    app_name: Optional[str]\n    logo_url: Optional[str]\n    dark_logo_url: Optional[str]\n    brand_color: Optional[str]\n\n\n@dataclass\nclass ProviderConfig:\n    name: str\n\n\n@dataclass\nclass OAuthProviderConfig(ProviderConfig):\n    display_name: str\n    client_id: str\n    secret: str\n    additional_scope: Optional[str]\n    issuer_url: Optional[str]\n    logo_url: Optional[str]\n\n\n@dataclass\nclass DiscordOAuthProviderConfig(OAuthProviderConfig):\n    prompt: str\n\n\nclass WebAuthnProviderConfig(ProviderConfig):\n    relying_party_origin: str\n    require_verification: bool\n    verification_method: VerificationMethod\n\n\n@dataclass\nclass WebAuthnProvider:\n    name: str\n    relying_party_origin: str\n    require_verification: bool\n    verification_method: VerificationMethod\n\n    def __init__(\n        self,\n        name: str,\n        relying_party_origin: str,\n        require_verification: bool,\n        verification_method: VerificationMethod,\n    ):\n        self.name = name\n        self.relying_party_origin = relying_party_origin\n        self.require_verification = require_verification\n        self.verification_method = verification_method\n        parsed_url = urllib.parse.urlparse(self.relying_party_origin)\n        if parsed_url.hostname is None:\n            raise ValueError(\n                \"Invalid relying_party_origin, hostname cannot be None\"\n            )\n        self.relying_party_id = parsed_url.hostname\n\n\n@dataclass\nclass EmailPasswordProviderConfig(ProviderConfig):\n    name: Literal[\"builtin::local_emailpassword\"]\n    require_verification: bool\n    verification_method: VerificationMethod\n\n\n@dataclass\nclass MagicLinkProviderConfig(ProviderConfig):\n    name: Literal[\"builtin::local_magic_link\"]\n    token_time_to_live: statypes.Duration\n    verification_method: VerificationMethod\n    auto_signup: bool\n\n\n@dataclass\nclass WebhookConfig:\n    events: list[str]\n    url: str\n    signing_secret_key: Optional[str]\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/data.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport dataclasses\nimport datetime\nimport base64\n\nfrom typing import Any, Optional\n\n\n@dataclasses.dataclass\nclass UserInfo:\n    \"\"\"\n    OpenID Connect compatible user info.\n    See: https://openid.net/specs/openid-connect-core-1_0.html\n    \"\"\"\n\n    sub: str\n    name: Optional[str] = None\n    given_name: Optional[str] = None\n    family_name: Optional[str] = None\n    middle_name: Optional[str] = None\n    nickname: Optional[str] = None\n    preferred_username: Optional[str] = None\n    profile: Optional[str] = None\n    picture: Optional[str] = None\n    website: Optional[str] = None\n    email: Optional[str] = None\n    email_verified: Optional[bool] = None\n    gender: Optional[str] = None\n    birthdate: Optional[str] = None\n    zoneinfo: Optional[str] = None\n    locale: Optional[str] = None\n    phone_number: Optional[str] = None\n    phone_number_verified: Optional[bool] = None\n    address: Optional[dict[str, str]] = None\n    updated_at: Optional[float] = None\n    source_id_token: Optional[str] = None\n\n    def __str__(self) -> str:\n        return self.sub\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"sub={self.sub!r} \"\n            f\"name={self.name!r} \"\n            f\"email={self.email!r} \"\n            f\"preferred_username={self.preferred_username!r})\"\n        )\n\n\n@dataclasses.dataclass\nclass Identity:\n    id: str\n    subject: str\n    issuer: str\n    created_at: datetime.datetime\n    modified_at: datetime.datetime\n\n    def __str__(self) -> str:\n        return self.id\n\n\n@dataclasses.dataclass\nclass LocalIdentity(Identity):\n    pass\n\n\n@dataclasses.dataclass\nclass OpenIDConfig:\n    \"\"\"\n    OpenID Connect configuration. Only includes fields actually in use.\n    See:\n    - https://openid.net/specs/openid-connect-discovery-1_0.html\n    - https://accounts.google.com/.well-known/openid-configuration\n    \"\"\"\n\n    issuer: str\n    authorization_endpoint: str\n    token_endpoint: str\n    jwks_uri: str\n\n    def __init__(self, **kwargs: Any):\n        for field in dataclasses.fields(self):\n            setattr(self, field.name, kwargs.get(field.name))\n\n    def __str__(self) -> str:\n        return self.issuer\n\n\n@dataclasses.dataclass(repr=False)\nclass OAuthAccessTokenResponse:\n    \"\"\"\n    Access Token Response.\n    https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4\n    \"\"\"\n\n    access_token: str\n    token_type: str\n    expires_in: int\n    refresh_token: str | None\n\n    def __init__(self, **kwargs: Any):\n        for field in dataclasses.fields(self):\n            if field.name in kwargs:\n                setattr(self, field.name, kwargs.pop(field.name))\n            else:\n                setattr(self, field.name, None)\n        self._extra_fields = kwargs\n\n\n@dataclasses.dataclass(repr=False)\nclass OpenIDConnectAccessTokenResponse(OAuthAccessTokenResponse):\n    \"\"\"\n    OpenID Connect Access Token Response.\n    https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse\n    \"\"\"\n\n    id_token: str\n\n    def __init__(self, **kwargs: Any):\n        super().__init__(**kwargs)\n\n\n@dataclasses.dataclass\nclass EmailFactor:\n    id: str\n    created_at: datetime.datetime\n    modified_at: datetime.datetime\n    identity: LocalIdentity\n    email: str\n    verified_at: Optional[datetime.datetime]\n\n    def __init__(\n        self,\n        *,\n        id: str,\n        created_at: datetime.datetime,\n        modified_at: datetime.datetime,\n        identity: LocalIdentity,\n        email: str,\n        verified_at: Optional[datetime.datetime],\n    ):\n        self.id = id\n        self.created_at = created_at\n        self.modified_at = modified_at\n        self.identity = (\n            LocalIdentity(**identity)\n            if isinstance(identity, dict)\n            else identity\n        )\n        self.email = email\n        self.verified_at = verified_at\n\n\n@dataclasses.dataclass\nclass WebAuthnFactor(EmailFactor):\n    user_handle: bytes\n    credential_id: bytes\n    public_key: bytes\n\n    def __init__(\n        self,\n        *,\n        id: str,\n        created_at: datetime.datetime,\n        modified_at: datetime.datetime,\n        identity: LocalIdentity,\n        email: str,\n        verified_at: Optional[datetime.datetime],\n        user_handle: bytes,\n        credential_id: bytes,\n        public_key: bytes,\n    ):\n        self.id = id\n        self.created_at = created_at\n        self.modified_at = modified_at\n        self.identity = (\n            LocalIdentity(**identity)\n            if isinstance(identity, dict)\n            else identity\n        )\n        self.email = email\n        self.verified_at = verified_at\n        self.user_handle = base64.b64decode(user_handle)\n        self.credential_id = base64.b64decode(credential_id)\n        self.public_key = base64.b64decode(public_key)\n\n\n@dataclasses.dataclass\nclass WebAuthnAuthenticationChallenge:\n    id: str\n    created_at: datetime.datetime\n    modified_at: datetime.datetime\n    challenge: bytes\n    factors: list[WebAuthnFactor]\n\n    def __init__(\n        self,\n        *,\n        id: str,\n        created_at: datetime.datetime,\n        modified_at: datetime.datetime,\n        challenge: bytes,\n        factors: list[WebAuthnFactor],\n    ):\n        self.id = id\n        self.created_at = created_at\n        self.modified_at = modified_at\n        self.challenge = base64.b64decode(challenge)\n        self.factors = [\n            WebAuthnFactor(**factor) if isinstance(factor, dict) else factor\n            for factor in factors\n        ]\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/discord.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom typing import Any\nimport urllib.parse\nimport functools\n\nfrom . import base, data, errors\n\n\nclass DiscordProvider(base.BaseProvider):\n    def __init__(\n            self,\n            prompt: str,\n            *args: Any,\n            **kwargs: Any\n    ):\n        super().__init__(\"discord\", \"https://discord.com\", *args, **kwargs)\n        self.auth_domain = self.issuer_url\n        self.api_domain = f\"{self.issuer_url}/api/v10\"\n        self.auth_client = functools.partial(\n            self.http_factory, base_url=self.auth_domain\n        )\n        self.api_client = functools.partial(\n            self.http_factory, base_url=self.api_domain\n        )\n        self.prompt = prompt\n\n    async def get_code_url(\n        self, state: str, redirect_uri: str, additional_scope: str\n    ) -> str:\n        params = {\n            \"client_id\": self.client_id,\n            \"scope\": f\"email identify {additional_scope}\",\n            \"state\": state,\n            \"redirect_uri\": redirect_uri,\n            \"response_type\": \"code\",\n            \"prompt\": self.prompt,\n        }\n        encoded = urllib.parse.urlencode(params)\n        return f\"{self.auth_domain}/oauth2/authorize?{encoded}\"\n\n    async def exchange_code(\n        self, code: str, redirect_uri: str\n    ) -> data.OAuthAccessTokenResponse:\n        async with self.auth_client() as client:\n            resp = await client.post(\n                \"/api/oauth2/token\",\n                data={\n                    \"grant_type\": \"authorization_code\",\n                    \"code\": code,\n                    \"client_id\": self.client_id,\n                    \"client_secret\": self.client_secret,\n                    \"redirect_uri\": redirect_uri,\n                },\n                headers={\n                    \"accept\": \"application/json\",\n                },\n            )\n            if resp.status_code >= 400:\n                raise errors.OAuthProviderFailure(\n                    f\"Failed to exchange code: {resp.text}\"\n                )\n            json = resp.json()\n\n            return data.OAuthAccessTokenResponse(**json)\n\n    async def fetch_user_info(\n        self, token_response: data.OAuthAccessTokenResponse\n    ) -> data.UserInfo:\n        async with self.api_client() as client:\n            resp = await client.get(\n                \"/users/@me\",\n                headers={\n                    \"Authorization\": f\"Bearer {token_response.access_token}\",\n                    \"Accept\": \"application/json\",\n                    \"Cache-Control\": \"no-store\",\n                },\n            )\n            payload = resp.json()\n            return data.UserInfo(\n                sub=str(payload[\"id\"]),\n                preferred_username=payload.get(\"username\"),\n                name=payload.get(\"global_name\"),\n                email=payload.get(\"email\"),\n                picture=payload.get(\"avatar\"),\n            )\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/email.py",
    "content": "import asyncio\nimport urllib.parse\nimport random\nimport logging\n\nfrom email.message import EmailMessage\nfrom typing import Any, Coroutine\nfrom edb.server import tenant, smtp\nfrom edb import errors\n\nfrom . import util, ui\n\n\nlogger = logging.getLogger(\"edb.server.ext.auth\")\n\n\nasync def send_password_reset_email(\n    db: Any,\n    tenant: tenant.Tenant,\n    to_addr: str,\n    reset_url: str,\n    test_mode: bool,\n) -> None:\n    app_details_config = util.get_app_details_config(db)\n    if app_details_config is None:\n        email_args = {}\n    else:\n        email_args = dict(\n            app_name=app_details_config.app_name,\n            logo_url=app_details_config.logo_url,\n            dark_logo_url=app_details_config.dark_logo_url,\n            brand_color=app_details_config.brand_color,\n        )\n    msg = ui.render_password_reset_email(\n        to_addr=to_addr,\n        reset_url=reset_url,\n        **email_args,\n    )\n    await _maybe_send_message(msg, tenant, db, test_mode)\n\n\nasync def send_verification_email(\n    db: Any,\n    tenant: tenant.Tenant,\n    to_addr: str,\n    verify_url: str,\n    verification_token: str,\n    provider: str,\n    test_mode: bool,\n) -> None:\n    app_details_config = util.get_app_details_config(db)\n    verification_token_params = urllib.parse.urlencode(\n        {\n            \"verification_token\": verification_token,\n            \"provider\": provider,\n            \"email\": to_addr,\n        }\n    )\n    verify_url = f\"{verify_url}?{verification_token_params}\"\n    if app_details_config is None:\n        email_args = {}\n    else:\n        email_args = dict(\n            app_name=app_details_config.app_name,\n            logo_url=app_details_config.logo_url,\n            dark_logo_url=app_details_config.dark_logo_url,\n            brand_color=app_details_config.brand_color,\n        )\n    msg = ui.render_verification_email(\n        to_addr=to_addr,\n        verify_url=verify_url,\n        **email_args,\n    )\n    await _maybe_send_message(msg, tenant, db, test_mode)\n\n\nasync def send_magic_link_email(\n    db: Any,\n    tenant: tenant.Tenant,\n    to_addr: str,\n    link: str,\n    test_mode: bool,\n) -> None:\n    app_details_config = util.get_app_details_config(db)\n    if app_details_config is None:\n        email_args = {}\n    else:\n        email_args = dict(\n            app_name=app_details_config.app_name,\n            logo_url=app_details_config.logo_url,\n            dark_logo_url=app_details_config.dark_logo_url,\n            brand_color=app_details_config.brand_color,\n        )\n    msg = ui.render_magic_link_email(\n        to_addr=to_addr,\n        link=link,\n        **email_args,\n    )\n    await _maybe_send_message(msg, tenant, db, test_mode)\n\n\nasync def send_one_time_code_email(\n    db: Any,\n    tenant: tenant.Tenant,\n    to_addr: str,\n    code: str,\n    test_mode: bool,\n) -> None:\n    app_details_config = util.get_app_details_config(db)\n    if app_details_config is None:\n        email_args = {}\n    else:\n        email_args = dict(\n            app_name=app_details_config.app_name,\n            logo_url=app_details_config.logo_url,\n            dark_logo_url=app_details_config.dark_logo_url,\n            brand_color=app_details_config.brand_color,\n        )\n    msg = ui.render_one_time_code_email(\n        to_addr=to_addr,\n        code=code,\n        **email_args,\n    )\n    await _maybe_send_message(msg, tenant, db, test_mode)\n\n\nasync def send_password_reset_code_email(\n    db: Any,\n    tenant: tenant.Tenant,\n    to_addr: str,\n    code: str,\n    test_mode: bool,\n) -> None:\n    \"\"\"Send a password reset email with a one-time code.\"\"\"\n    app_details_config = util.get_app_details_config(db)\n    if app_details_config is None:\n        email_args = {}\n    else:\n        email_args = dict(\n            app_name=app_details_config.app_name,\n            logo_url=app_details_config.logo_url,\n            dark_logo_url=app_details_config.dark_logo_url,\n            brand_color=app_details_config.brand_color,\n        )\n    msg = ui.render_password_reset_code_email(\n        to_addr=to_addr,\n        code=code,\n        **email_args,\n    )\n    await _maybe_send_message(msg, tenant, db, test_mode)\n\n\nasync def send_fake_email(tenant: tenant.Tenant) -> None:\n    async def noop_coroutine() -> None:\n        pass\n\n    coro = noop_coroutine()\n    await _protected_send(coro, tenant)\n\n\nasync def _maybe_send_message(\n    msg: EmailMessage,\n    tenant: tenant.Tenant,\n    db: Any,\n    test_mode: bool,\n) -> None:\n    try:\n        smtp_provider = smtp.SMTP(db)\n    except errors.ConfigurationError as e:\n        logger.debug(\n            \"ConfigurationError while instantiating SMTP provider, \"\n            f\"sending fake email instead: {e}\"\n        )\n        smtp_provider = None\n    if smtp_provider is None:\n        coro = send_fake_email(tenant)\n    else:\n        coro = smtp_provider.send(\n            msg,\n            test_mode=test_mode,\n        )\n    await _protected_send(coro, tenant)\n\n\nasync def _protected_send(\n    coro: Coroutine[Any, Any, None], tenant: tenant.Tenant\n) -> None:\n    task = tenant.create_task(coro, interruptable=True)\n    # Prevent timing attack\n    await asyncio.sleep(random.random() * 0.5)\n    # Expose e.g. configuration errors\n    if task.done():\n        await task\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/email_password.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport argon2\nimport json\nimport hashlib\nimport base64\nimport dataclasses\n\nfrom typing import Any, Optional\nfrom edb.errors import ConstraintViolationError\n\nfrom . import errors, util, data, local\n\nph = argon2.PasswordHasher()\n\n\n@dataclasses.dataclass\nclass EmailPasswordProviderConfig:\n    name: str\n    require_verification: bool\n    verification_method: str\n\n\nclass Client(local.Client):\n    def __init__(self, db: Any):\n        super().__init__(db)\n        self.config = self._get_provider_config(\"builtin::local_emailpassword\")\n\n    async def register(self, input: dict[str, Any]) -> data.EmailFactor:\n        match (input.get(\"email\"), input.get(\"password\")):\n            case (str(e), str(p)):\n                email = e\n                password = p\n            case _:\n                raise errors.InvalidData(\n                    \"Missing 'email' or 'password' in data\"\n                )\n\n        try:\n            r = await util.json_query(\n                db=self.db,\n                query=\"\"\"\\\n    with\n      email := <optional str>$email,\n      password_hash := <str>$password_hash,\n      identity := (insert ext::auth::LocalIdentity {\n        issuer := \"local\",\n        subject := \"\",\n      }),\n      factor := (insert ext::auth::EmailPasswordFactor {\n        password_hash := password_hash,\n        email := email,\n        identity := identity,\n      }),\n\n    select factor {\n        id,\n        email,\n        verified_at,\n        created_at,\n        modified_at,\n        identity: { * },\n    };\"\"\",\n                variables={\n                    \"email\": email,\n                    \"password_hash\": ph.hash(password),\n                },\n            )\n        except ConstraintViolationError:\n            raise errors.UserAlreadyRegistered()\n\n        result_json = json.loads(r.decode())\n        assert len(result_json) == 1\n        return data.EmailFactor(**result_json[0])\n\n    async def authenticate(\n        self, email: str, password: str\n    ) -> data.LocalIdentity:\n        r = await util.json_query(\n            db=self.db,\n            query=\"\"\"\\\nwith\n  email := <str>$email,\nselect ext::auth::EmailPasswordFactor { password_hash, identity: { * } }\nfilter .email = email;\"\"\",\n            variables={\n                \"email\": email,\n            },\n        )\n\n        password_credential_dicts = json.loads(r.decode())\n        if len(password_credential_dicts) != 1:\n            raise errors.NoIdentityFound()\n        password_credential_dict = password_credential_dicts[0]\n\n        password_hash = password_credential_dict[\"password_hash\"]\n        try:\n            ph.verify(password_hash, password)\n        except argon2.exceptions.VerifyMismatchError:\n            raise errors.NoIdentityFound()\n\n        local_identity = data.LocalIdentity(\n            **password_credential_dict[\"identity\"]\n        )\n\n        if ph.check_needs_rehash(password_hash):\n            new_hash = ph.hash(password)\n            await util.json_query(\n                db=self.db,\n                query=\"\"\"\\\nwith\n  email := <str>$email,\n  new_hash := <str>$new_hash,\n\nupdate ext::auth::EmailPasswordFactor\nfilter .email = email\nset { password_hash := new_hash };\"\"\",\n                variables={\n                    \"email\": email,\n                    \"new_hash\": new_hash,\n                },\n            )\n\n        return local_identity\n\n    async def get_email_factor_and_secret(\n        self,\n        email: str,\n    ) -> tuple[data.EmailFactor, str]:\n        r = await util.json_query(\n            db=self.db,\n            query=\"\"\"\nwith\n  email := <str>$email,\nselect ext::auth::EmailPasswordFactor { ** } filter .email = email\"\"\",\n            variables={\n                \"email\": email,\n            },\n        )\n\n        result_json = json.loads(r.decode())\n        if len(result_json) != 1:\n            raise errors.NoIdentityFound()\n        password_cred = result_json[0]\n\n        secret = base64.b64encode(\n            hashlib.sha256(password_cred['password_hash'].encode()).digest()\n        ).decode()\n        email_factor = data.EmailFactor(\n            **{k: v for k, v in password_cred.items() if k != \"password_hash\"}\n        )\n\n        return (email_factor, secret)\n\n    async def validate_reset_secret(\n        self,\n        identity_id: str,\n        secret: str,\n    ) -> Optional[data.LocalIdentity]:\n        r = await util.json_query(\n            db=self.db,\n            query=\"\"\"\\\nwith\n  identity_id := <uuid><str>$identity_id,\nselect ext::auth::EmailPasswordFactor { password_hash, identity: { * } }\nfilter .identity.id = identity_id;\"\"\",\n            variables={\n                \"identity_id\": identity_id,\n            },\n        )\n\n        result_json = json.loads(r.decode())\n        if len(result_json) != 1:\n            raise errors.NoIdentityFound()\n        password_cred = result_json[0]\n\n        local_identity = data.LocalIdentity(**password_cred[\"identity\"])\n\n        current_secret = base64.b64encode(\n            hashlib.sha256(password_cred['password_hash'].encode()).digest()\n        ).decode()\n\n        return local_identity if secret == current_secret else None\n\n    async def update_password(\n        self, identity_id: str, secret: str, password: str\n    ) -> data.LocalIdentity:\n        local_identity = await self.validate_reset_secret(identity_id, secret)\n\n        if local_identity is None:\n            raise errors.InvalidData(\"Invalid 'reset_token'\")\n\n        # TODO: check if race between validating secret and updating password\n        #       is a problem\n        await util.json_query(\n            db=self.db,\n            query=\"\"\"\\\nwith\n  identity_id := <uuid><str>$identity_id,\n  new_hash := <str>$new_hash,\nupdate ext::auth::EmailPasswordFactor\nfilter .identity.id = identity_id\nset {\n    password_hash := new_hash,\n    verified_at := .verified_at ?? datetime_current()\n};\"\"\",\n            variables={\n                'identity_id': identity_id,\n                'new_hash': ph.hash(password),\n            },\n        )\n\n        return local_identity\n\n    def _get_provider_config(\n        self, provider_name: str\n    ) -> EmailPasswordProviderConfig:\n        provider_client_config = util.get_config(\n            self.db, \"ext::auth::AuthConfig::providers\", frozenset\n        )\n        for cfg in provider_client_config:\n            if cfg.name == provider_name:\n                return EmailPasswordProviderConfig(\n                    name=cfg.name,\n                    require_verification=cfg.require_verification,\n                    verification_method=cfg.verification_method,\n                )\n\n        raise errors.MissingConfiguration(\n            provider_name, f\"Provider is not configured\"\n        )\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/errors.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nclass AuthExtError(Exception):\n    \"\"\"Base class for all exceptions raised by the auth extension.\"\"\"\n\n    pass\n\n\nclass NotFound(AuthExtError):\n    \"\"\"Required resource could not be found.\"\"\"\n\n    def __init__(self, description: str):\n        self.description = description\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"description={self.description!r}\"\n            \")\"\n        )\n\n    def __str__(self) -> str:\n        return self.description\n\n\nclass MissingConfiguration(AuthExtError):\n    \"\"\"Required configuration is missing.\"\"\"\n\n    def __init__(self, key: str, description: str):\n        self.key = key\n        self.description = description\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"key={self.key!r} \"\n            f\"description={self.description!r}\"\n            \")\"\n        )\n\n    def __str__(self) -> str:\n        return f\"{self.description}: {self.key}\"\n\n\nclass InvalidData(AuthExtError):\n    \"\"\"Data received from the client is invalid.\"\"\"\n\n    def __init__(self, description: str):\n        self.description = description\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"description={self.description!r}\"\n            \")\"\n        )\n\n    def __str__(self) -> str:\n        return self.description\n\n\nclass MisconfiguredProvider(AuthExtError):\n    \"\"\"Data received from the auth provider is invalid.\"\"\"\n\n    def __init__(self, description: str):\n        self.description = description\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"description={self.description!r}\"\n            \")\"\n        )\n\n    def __str__(self) -> str:\n        return self.description\n\n\nclass NoIdentityFound(AuthExtError):\n    \"\"\"Could not find a matching identity.\"\"\"\n\n    def __init__(\n        self,\n        description: str = (\n            \"Could not find an Identity matching the provided credentials\"\n        ),\n    ):\n        self.description = description\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"description={self.description!r}\"\n            \")\"\n        )\n\n    def __str__(self) -> str:\n        return self.description\n\n\nclass UserAlreadyRegistered(AuthExtError):\n    \"\"\"Attempt to register an already registered handle.\"\"\"\n\n    def __init__(\n        self,\n        description: str = (\"This user has already been registered\"),\n    ):\n        self.description = description\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"description={self.description!r}\"\n            \")\"\n        )\n\n    def __str__(self) -> str:\n        return self.description\n\n\nclass OAuthProviderFailure(AuthExtError):\n    \"\"\"OAuth Provider returned a non-success for some part of the flow\"\"\"\n\n    def __init__(\n        self,\n        description: str,\n    ):\n        self.description = description\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"description={self.description!r}\"\n            \")\"\n        )\n\n    def __str__(self) -> str:\n        return self.description\n\n\nclass VerificationTokenExpired(AuthExtError):\n    \"\"\"Email verification token has expired\"\"\"\n\n    def __init__(\n        self,\n        description: str = \"Email verification token has expired\",\n    ):\n        self.description = description\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"description={self.description!r}\"\n            \")\"\n        )\n\n    def __str__(self) -> str:\n        return self.description\n\n\nclass VerificationRequired(AuthExtError):\n    \"\"\"Email verification is required\"\"\"\n\n    def __init__(\n        self,\n        description: str = \"Email verification is required\",\n    ):\n        self.description = description\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"description={self.description!r}\"\n            \")\"\n        )\n\n    def __str__(self) -> str:\n        return self.description\n\n\nclass PKCECreationFailed(AuthExtError):\n    \"\"\"Failed to create a valid PKCEChallenge object\"\"\"\n\n    def __init__(\n        self, description: str = \"Failed to create a valid PKCEChallenge object\"\n    ):\n        self.description = description\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"description={self.description!r}\"\n            \")\"\n        )\n\n    def __str__(self) -> str:\n        return self.description\n\n\nclass PKCEVerificationFailed(AuthExtError):\n    \"\"\"Verifier and challenge do not match\"\"\"\n\n    def __init__(\n        self, description: str = \"Verifier and challenge do not match\"\n    ):\n        self.description = description\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"description={self.description!r}\"\n            \")\"\n        )\n\n    def __str__(self) -> str:\n        return self.description\n\n\nclass WebAuthnAuthenticationFailed(AuthExtError):\n    \"\"\"WebAuthn authentication failed\"\"\"\n\n    def __init__(self, description: str = \"WebAuthn authentication failed\"):\n        self.description = description\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"description={self.description!r}\"\n            \")\"\n        )\n\n    def __str__(self) -> str:\n        return self.description\n\n\nclass WebAuthnRegistrationFailed(AuthExtError):\n    \"\"\"WebAuthn registration failed\"\"\"\n\n    def __init__(self, description: str = \"WebAuthn registration failed\"):\n        self.description = description\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"description={self.description!r}\"\n            \")\"\n        )\n\n    def __str__(self) -> str:\n        return self.description\n\n\nclass OTCVerificationError(AuthExtError):\n    \"\"\"Base class for one-time code verification failures.\"\"\"\n\n    def __init__(self, description: str):\n        self.description = description\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"description={self.description!r}\"\n            \")\"\n        )\n\n    def __str__(self) -> str:\n        return self.description\n\n\nclass OTCRateLimited(OTCVerificationError):\n    \"\"\"Maximum verification attempts exceeded for the factor.\"\"\"\n\n    def __init__(\n        self, description: str = \"Maximum verification attempts exceeded\"\n    ):\n        super().__init__(description)\n\n\nclass OTCInvalidCode(OTCVerificationError):\n    \"\"\"The provided one-time code is invalid or not found.\"\"\"\n\n    def __init__(self, description: str = \"Invalid code\"):\n        super().__init__(description)\n\n\nclass OTCExpired(OTCVerificationError):\n    \"\"\"The one-time code has expired.\"\"\"\n\n    def __init__(self, description: str = \"Code has expired\"):\n        super().__init__(description)\n\n\nclass OTCVerificationFailed(OTCVerificationError):\n    \"\"\"General verification failure that doesn't fall into other categories.\"\"\"\n\n    def __init__(self, description: str = \"Verification failed\"):\n        super().__init__(description)\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/github.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom typing import Any\nimport urllib.parse\nimport functools\n\nfrom . import base, data, errors\n\n\nclass GitHubProvider(base.BaseProvider):\n    def __init__(self, *args: Any, **kwargs: Any):\n        super().__init__(\"github\", \"https://github.com\", *args, **kwargs)\n        self.auth_domain = self.issuer_url\n        self.api_domain = \"https://api.github.com\"\n        self.auth_client = functools.partial(\n            self.http_factory, base_url=self.auth_domain\n        )\n        self.api_client = functools.partial(\n            self.http_factory, base_url=self.api_domain\n        )\n\n    async def get_code_url(\n        self, state: str, redirect_uri: str, additional_scope: str\n    ) -> str:\n        params = {\n            \"client_id\": self.client_id,\n            \"scope\": f\"read:user user:email {additional_scope}\",\n            \"state\": state,\n            \"redirect_uri\": redirect_uri,\n        }\n        encoded = urllib.parse.urlencode(params)\n        return f\"{self.auth_domain}/login/oauth/authorize?{encoded}\"\n\n    async def exchange_code(\n        self, code: str, redirect_uri: str\n    ) -> data.OAuthAccessTokenResponse:\n        async with self.auth_client() as client:\n            resp = await client.post(\n                \"/login/oauth/access_token\",\n                json={\n                    \"grant_type\": \"authorization_code\",\n                    \"code\": code,\n                    \"client_id\": self.client_id,\n                    \"client_secret\": self.client_secret,\n                    \"redirect_uri\": redirect_uri,\n                },\n                headers={\n                    \"accept\": \"application/json\",\n                },\n            )\n            if resp.status_code >= 400:\n                raise errors.OAuthProviderFailure(\n                    f\"Failed to exchange code: {resp.text}\"\n                )\n            json = resp.json()\n\n            return data.OAuthAccessTokenResponse(**json)\n\n    async def fetch_user_info(\n        self, token_response: data.OAuthAccessTokenResponse\n    ) -> data.UserInfo:\n        async with self.api_client() as client:\n            resp = await client.get(\n                \"/user\",\n                headers={\n                    \"Authorization\": f\"Bearer {token_response.access_token}\",\n                    \"Accept\": \"application/vnd.github+json\",\n                    \"X-GitHub-Api-Version\": \"2022-11-28\",\n                    \"Cache-Control\": \"no-store\",\n                },\n            )\n            payload = resp.json()\n            return data.UserInfo(\n                sub=str(payload[\"id\"]),\n                preferred_username=payload.get(\"login\"),\n                name=payload.get(\"name\"),\n                email=payload.get(\"email\"),\n                picture=payload.get(\"avatar_url\"),\n                updated_at=self._maybe_isoformat_to_timestamp(\n                    payload.get(\"updated_at\")\n                ),\n            )\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/google.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom typing import Any\n\nfrom . import base\n\n\nclass GoogleProvider(base.OpenIDConnectProvider):\n    def __init__(self, *args: Any, **kwargs: Any):\n        super().__init__(\n            \"google\", \"https://accounts.google.com\", *args, **kwargs\n        )\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/http.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport datetime\nimport http\nimport http.cookies\nimport json\nimport logging\nimport urllib.parse\nimport base64\nimport hashlib\nimport os\nimport mimetypes\nimport uuid\nimport dataclasses\n\nfrom typing import (\n    Any,\n    Optional,\n    cast,\n    TYPE_CHECKING,\n    Callable,\n)\n\nimport aiosmtplib\n\nfrom edb import errors as edb_errors\nfrom edb.common import debug\nfrom edb.common import markup\nfrom edb.server import tenant as edbtenant, metrics\nfrom edb.server.config.types import CompositeConfigType\nfrom edb.ir import statypes\n\nfrom . import (\n    email_password,\n    oauth,\n    errors,\n    util,\n    pkce,\n    ui,\n    config,\n    email as auth_emails,\n    webauthn,\n    magic_link,\n    webhook,\n    jwt,\n    otc,\n    local,\n)\nfrom .data import EmailFactor\n\nif TYPE_CHECKING:\n    from edb.server.protocol import protocol\n\n\nlogger = logging.getLogger('edb.server.ext.auth')\n\n\nclass Router:\n    test_url: Optional[str]\n\n    def __init__(\n        self,\n        *,\n        db: edbtenant.dbview.Database,\n        base_path: str,\n        tenant: edbtenant.Tenant,\n    ):\n        self.db = db\n        self.base_path = base_path\n        self.tenant = tenant\n        self.test_mode = tenant.server.in_test_mode()\n        self.signing_key = jwt.SigningKey(\n            lambda: util.get_config(\n                self.db, \"ext::auth::AuthConfig::auth_signing_key\"\n            ),\n            self.base_path,\n        )\n\n    def _get_url_munger(\n        self, request: protocol.HttpRequest\n    ) -> Callable[[str], str] | None:\n        \"\"\"\n        Returns a callable that can be used to modify the base URL\n        when making requests to the OAuth provider.\n\n        This is used to redirect requests to the test OAuth provider\n        when running in test mode.\n        \"\"\"\n        if not self.test_mode:\n            return None\n        test_url = (\n            request.params[b'oauth-test-server'].decode()\n            if (request.params and b'oauth-test-server' in request.params)\n            else None\n        )\n        if test_url:\n            return lambda path: f\"{test_url}{urllib.parse.quote(path)}\"\n        return None\n\n    async def handle_request(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n        args: list[str],\n    ) -> None:\n        if self.db.db_config is None:\n            await self.db.introspection()\n\n        self.test_url = (\n            request.params[b'oauth-test-server'].decode()\n            if (\n                self.test_mode\n                and request.params\n                and b'oauth-test-server' in request.params\n            )\n            else None\n        )\n\n        logger.info(\n            f\"Handling incoming HTTP request: /ext/auth/{'/'.join(args)}\"\n        )\n\n        try:\n            match args:\n                # PKCE token exchange route\n                case (\"token\",):\n                    await self.handle_token(request, response)\n\n                # OAuth routes\n                case (\"authorize\",):\n                    await self.handle_authorize(request, response)\n                case (\"callback\",):\n                    await self.handle_callback(request, response)\n\n                # Email/password routes\n                case (\"register\",):\n                    await self.handle_register(request, response)\n                case (\"authenticate\",):\n                    await self.handle_authenticate(request, response)\n                case ('send-reset-email',):\n                    await self.handle_send_reset_email(request, response)\n                case ('reset-password',):\n                    await self.handle_reset_password(request, response)\n\n                # Magic link routes\n                case ('magic-link', 'register'):\n                    await self.handle_magic_link_register(request, response)\n                case ('magic-link', 'email'):\n                    await self.handle_magic_link_email(request, response)\n                case ('magic-link', 'authenticate'):\n                    await self.handle_magic_link_authenticate(request, response)\n\n                # WebAuthn routes\n                case ('webauthn', 'register'):\n                    await self.handle_webauthn_register(request, response)\n                case ('webauthn', 'register', 'options'):\n                    await self.handle_webauthn_register_options(\n                        request, response\n                    )\n                case ('webauthn', 'authenticate'):\n                    await self.handle_webauthn_authenticate(request, response)\n                case ('webauthn', 'authenticate', 'options'):\n                    await self.handle_webauthn_authenticate_options(\n                        request, response\n                    )\n\n                # Email verification routes\n                case (\"verify\",):\n                    await self.handle_verify(request, response)\n                case (\"resend-verification-email\",):\n                    await self.handle_resend_verification_email(\n                        request, response\n                    )\n\n                # UI routes\n                case ('ui', 'signin'):\n                    await self.handle_ui_signin(request, response)\n                case ('ui', 'signup'):\n                    await self.handle_ui_signup(request, response)\n                case ('ui', 'forgot-password'):\n                    await self.handle_ui_forgot_password(request, response)\n                case ('ui', 'reset-password'):\n                    await self.handle_ui_reset_password(request, response)\n                case (\"ui\", \"verify\"):\n                    await self.handle_ui_verify(request, response)\n                case (\"ui\", \"resend-verification\"):\n                    await self.handle_ui_resend_verification(request, response)\n                case (\"ui\", \"magic-link-sent\"):\n                    await self.handle_ui_magic_link_sent(request, response)\n                case ('ui', '_static', filename):\n                    filepath = os.path.join(\n                        os.path.dirname(__file__), '_static', filename\n                    )\n                    try:\n                        with open(filepath, 'rb') as f:\n                            response.status = http.HTTPStatus.OK\n                            response.content_type = (\n                                mimetypes.guess_type(filename)[0]\n                                or 'application/octet-stream'\n                            ).encode()\n                            response.body = f.read()\n                    except FileNotFoundError:\n                        response.status = http.HTTPStatus.NOT_FOUND\n\n                case _:\n                    raise errors.NotFound(\"Unknown auth endpoint\")\n\n        # User-facing errors\n        except errors.NotFound as ex:\n            _fail_with_error(\n                response=response,\n                status=http.HTTPStatus.NOT_FOUND,\n                ex=ex,\n            )\n\n        except errors.InvalidData as ex:\n            _fail_with_error(\n                response=response,\n                status=http.HTTPStatus.BAD_REQUEST,\n                ex=ex,\n            )\n\n        except errors.PKCEVerificationFailed as ex:\n            _fail_with_error(\n                response=response,\n                status=http.HTTPStatus.FORBIDDEN,\n                ex=ex,\n            )\n\n        except errors.NoIdentityFound as ex:\n            _fail_with_error(\n                response=response,\n                status=http.HTTPStatus.FORBIDDEN,\n                ex=ex,\n            )\n\n        except errors.UserAlreadyRegistered as ex:\n            _fail_with_error(\n                response=response,\n                status=http.HTTPStatus.CONFLICT,\n                ex=ex,\n            )\n\n        except errors.VerificationRequired as ex:\n            _fail_with_error(\n                response=response,\n                status=http.HTTPStatus.UNAUTHORIZED,\n                ex=ex,\n            )\n\n        # Server errors\n        except errors.MissingConfiguration as ex:\n            _fail_with_error(\n                response=response,\n                status=http.HTTPStatus.INTERNAL_SERVER_ERROR,\n                ex=ex,\n            )\n\n        except errors.WebAuthnRegistrationFailed as ex:\n            _fail_with_error(\n                response=response,\n                status=http.HTTPStatus.BAD_REQUEST,\n                ex=ex,\n                exc_info=True,\n            )\n\n        except errors.WebAuthnAuthenticationFailed as ex:\n            _fail_with_error(\n                response=response,\n                status=http.HTTPStatus.UNAUTHORIZED,\n                ex=ex,\n                exc_info=True,\n            )\n\n        except Exception as ex:\n            if debug.flags.server:\n                markup.dump(ex)\n            _fail_with_error(\n                response=response,\n                status=http.HTTPStatus.INTERNAL_SERVER_ERROR,\n                ex=edb_errors.InternalServerError(str(ex)),\n                exc_info=True,\n            )\n\n    async def handle_authorize(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        query = urllib.parse.parse_qs(\n            request.url.query.decode(\"ascii\") if request.url.query else \"\"\n        )\n        provider_name = _get_search_param(query, \"provider\")\n        allowed_redirect_to = self._make_allowed_url(\n            _get_search_param(query, \"redirect_to\")\n        )\n        allowed_redirect_to_on_signup = self._maybe_make_allowed_url(\n            _maybe_get_search_param(query, \"redirect_to_on_signup\")\n        )\n        allowed_callback_url = self._maybe_make_allowed_url(\n            _maybe_get_search_param(query, \"callback_url\")\n        )\n        challenge = _get_search_param(\n            query, \"challenge\", fallback_keys=[\"code_challenge\"]\n        )\n        oauth_client = oauth.Client(\n            db=self.db,\n            provider_name=provider_name,\n            url_munger=self._get_url_munger(request),\n            http_client=self.tenant.get_http_client(originator=\"auth\"),\n        )\n        await pkce.create(self.db, challenge)\n        redirect_uri = (\n            allowed_callback_url.url\n            if allowed_callback_url\n            else self._get_callback_url()\n        )\n        authorize_url = await oauth_client.get_authorize_url(\n            redirect_uri=redirect_uri,\n            state=jwt.OAuthStateToken(\n                provider=provider_name,\n                redirect_to=allowed_redirect_to.url,\n                redirect_to_on_signup=(\n                    allowed_redirect_to_on_signup.url\n                    if allowed_redirect_to_on_signup\n                    else None\n                ),\n                challenge=challenge,\n                redirect_uri=redirect_uri,\n            ).sign(self.signing_key),\n        )\n        # n.b. Explicitly allow authorization URL to be outside of allowed\n        # URLs because it is a trusted URL from the identity provider.\n        self._do_redirect(response, AllowedUrl(authorize_url))\n\n    async def handle_callback(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        if request.method == b\"POST\" and (\n            request.content_type == b\"application/x-www-form-urlencoded\"\n        ):\n            form_data = urllib.parse.parse_qs(request.body.decode())\n            state = _maybe_get_form_field(form_data, \"state\")\n            code = _maybe_get_form_field(form_data, \"code\")\n\n            error = _maybe_get_form_field(form_data, \"error\")\n            error_description = _maybe_get_form_field(\n                form_data, \"error_description\"\n            )\n        elif request.url.query is not None:\n            query = urllib.parse.parse_qs(\n                request.url.query.decode(\"ascii\") if request.url.query else \"\"\n            )\n            state = _maybe_get_search_param(query, \"state\")\n            code = _maybe_get_search_param(query, \"code\")\n            error = _maybe_get_search_param(query, \"error\")\n            error_description = _maybe_get_search_param(\n                query, \"error_description\"\n            )\n        else:\n            raise errors.OAuthProviderFailure(\n                \"Provider did not respond with expected data\"\n            )\n\n        if state is None:\n            raise errors.InvalidData(\n                \"Provider did not include the 'state' parameter in callback\"\n            )\n\n        if error is not None:\n            try:\n                claims = jwt.OAuthStateToken.verify(state, self.signing_key)\n                redirect_to = claims.redirect_to\n            except Exception:\n                raise errors.InvalidData(\"Invalid state token\")\n\n            params = {\n                \"error\": error,\n            }\n            error_str = error\n            if error_description is not None:\n                params[\"error_description\"] = error_description\n                error_str += f\": {error_description}\"\n\n            logger.debug(f\"OAuth provider returned an error: {error_str}\")\n            return self._try_redirect(\n                response,\n                util.join_url_params(redirect_to, params),\n            )\n\n        if code is None:\n            raise errors.InvalidData(\n                \"Provider did not include the 'code' parameter in callback\"\n            )\n\n        try:\n            claims = jwt.OAuthStateToken.verify(state, self.signing_key)\n            provider_name = claims.provider\n            allowed_redirect_to = self._make_allowed_url(claims.redirect_to)\n            allowed_redirect_to_on_signup = self._maybe_make_allowed_url(\n                claims.redirect_to_on_signup\n            )\n            challenge = claims.challenge\n            redirect_uri = claims.redirect_uri\n        except Exception:\n            raise errors.InvalidData(\"Invalid state token\")\n        oauth_client = oauth.Client(\n            db=self.db,\n            provider_name=provider_name,\n            url_munger=self._get_url_munger(request),\n            http_client=self.tenant.get_http_client(originator=\"auth\"),\n        )\n        (\n            identity,\n            new_identity,\n            auth_token,\n            refresh_token,\n            id_token,\n        ) = await oauth_client.handle_callback(code, redirect_uri)\n        if new_identity:\n            await self._maybe_send_webhook(\n                webhook.IdentityCreated(\n                    event_id=str(uuid.uuid4()),\n                    timestamp=datetime.datetime.now(datetime.timezone.utc),\n                    identity_id=identity.id,\n                )\n            )\n        pkce_code = await pkce.link_identity_challenge(\n            self.db, identity.id, challenge\n        )\n        if auth_token or refresh_token:\n            await pkce.add_provider_tokens(\n                self.db,\n                id=pkce_code,\n                auth_token=auth_token,\n                refresh_token=refresh_token,\n                id_token=id_token,\n            )\n        new_url = (\n            (allowed_redirect_to_on_signup or allowed_redirect_to)\n            if new_identity\n            else allowed_redirect_to\n        ).map(\n            lambda u: util.join_url_params(\n                u, {\"code\": pkce_code, \"provider\": provider_name}\n            )\n        )\n        logger.info(\n            \"OAuth callback successful: \"\n            f\"identity_id={identity.id}, new_identity={new_identity}\"\n        )\n        self._do_redirect(response, new_url)\n\n    async def handle_token(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        query = urllib.parse.parse_qs(\n            request.url.query.decode(\"ascii\") if request.url.query else \"\"\n        )\n        code = _get_search_param(query, \"code\")\n        verifier = _get_search_param(\n            query, \"verifier\", fallback_keys=[\"code_verifier\"]\n        )\n\n        verifier_size = len(verifier)\n\n        if verifier_size < 43:\n            raise errors.InvalidData(\n                \"Verifier must be at least 43 characters long\"\n            )\n        if verifier_size > 128:\n            raise errors.InvalidData(\n                \"Verifier must be shorter than 128 characters long\"\n            )\n        try:\n            pkce_object = await pkce.get_by_id(self.db, code)\n        except Exception:\n            raise errors.NoIdentityFound(\"Could not find a matching PKCE code\")\n\n        if pkce_object.identity_id is None:\n            raise errors.InvalidData(\"Code is not associated with an Identity\")\n\n        hashed_verifier = hashlib.sha256(verifier.encode()).digest()\n        base64_url_encoded_verifier = base64.urlsafe_b64encode(\n            hashed_verifier\n        ).rstrip(b'=')\n\n        if base64_url_encoded_verifier.decode() == pkce_object.challenge:\n            await pkce.delete(self.db, code)\n\n            identity_id = pkce_object.identity_id\n            await self._maybe_send_webhook(\n                webhook.IdentityAuthenticated(\n                    event_id=str(uuid.uuid4()),\n                    timestamp=datetime.datetime.now(datetime.timezone.utc),\n                    identity_id=identity_id,\n                )\n            )\n            auth_expiration_time = util.get_config(\n                self.db,\n                \"ext::auth::AuthConfig::token_time_to_live\",\n                statypes.Duration,\n            )\n            session_token = jwt.SessionToken(\n                subject=identity_id,\n            ).sign(\n                self.signing_key,\n                expires_in=auth_expiration_time.to_timedelta(),\n            )\n            metrics.auth_successful_logins.inc(\n                1.0, self.tenant.get_instance_name()\n            )\n            logger.info(f\"Token exchange successful: identity_id={identity_id}\")\n            response.status = http.HTTPStatus.OK\n            response.content_type = b\"application/json\"\n            response.body = json.dumps(\n                {\n                    \"auth_token\": session_token,\n                    \"identity_id\": identity_id,\n                    \"provider_token\": pkce_object.auth_token,\n                    \"provider_refresh_token\": pkce_object.refresh_token,\n                    \"provider_id_token\": pkce_object.id_token,\n                }\n            ).encode()\n        else:\n            raise errors.PKCEVerificationFailed\n\n    async def handle_register(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        data = self._get_data_from_request(request)\n\n        allowed_redirect_to = self._maybe_make_allowed_url(\n            cast(Optional[str], data.get(\"redirect_to\"))\n        )\n\n        maybe_challenge = cast(Optional[str], data.get(\"challenge\"))\n        register_provider_name = cast(Optional[str], data.get(\"provider\"))\n        if register_provider_name is None:\n            raise errors.InvalidData('Missing \"provider\" in register request')\n\n        email_password_client = email_password.Client(db=self.db)\n        require_verification = email_password_client.config.require_verification\n        if not require_verification and maybe_challenge is None:\n            raise errors.InvalidData('Missing \"challenge\" in register request')\n        pkce_code: Optional[str] = None\n\n        try:\n            email_factor = await email_password_client.register(data)\n            identity = email_factor.identity\n\n            verify_url = data.get(\"verify_url\", f\"{self.base_path}/ui/verify\")\n            verification_token = self._make_verification_token(\n                identity_id=identity.id,\n                verify_url=verify_url,\n                maybe_challenge=maybe_challenge,\n                maybe_redirect_to=(\n                    allowed_redirect_to.url if allowed_redirect_to else None\n                ),\n            )\n\n            await self._maybe_send_webhook(\n                webhook.IdentityCreated(\n                    event_id=str(uuid.uuid4()),\n                    timestamp=datetime.datetime.now(datetime.timezone.utc),\n                    identity_id=identity.id,\n                )\n            )\n            await self._maybe_send_webhook(\n                webhook.EmailFactorCreated(\n                    event_id=str(uuid.uuid4()),\n                    timestamp=datetime.datetime.now(datetime.timezone.utc),\n                    identity_id=identity.id,\n                    email_factor_id=email_factor.id,\n                )\n            )\n            await self._maybe_send_webhook(\n                webhook.EmailVerificationRequested(\n                    event_id=str(uuid.uuid4()),\n                    timestamp=datetime.datetime.now(datetime.timezone.utc),\n                    identity_id=identity.id,\n                    email_factor_id=email_factor.id,\n                    verification_token=verification_token,\n                )\n            )\n\n            if require_verification:\n                response_dict = {\n                    \"identity_id\": identity.id,\n                    \"email\": email_factor.email,\n                    \"verification_email_sent_at\": datetime.datetime.now(\n                        datetime.timezone.utc\n                    ).isoformat(),\n                }\n            else:\n                # Checked at the beginning of the route handler\n                assert maybe_challenge is not None\n                await pkce.create(self.db, maybe_challenge)\n                pkce_code = await pkce.link_identity_challenge(\n                    self.db, identity.id, maybe_challenge\n                )\n                response_dict = {\n                    \"code\": pkce_code,\n                    \"email\": email_factor.email,\n                    \"provider\": register_provider_name,\n                }\n\n            await self._send_verification_email(\n                provider=register_provider_name,\n                verification_token=verification_token,\n                to_addr=data[\"email\"],\n                verify_url=verify_url,\n            )\n\n            logger.info(\n                f\"Identity created: identity_id={identity.id}, \"\n                f\"pkce_id={pkce_code!r}\"\n            )\n\n            if allowed_redirect_to is not None:\n                self._do_redirect(\n                    response,\n                    allowed_redirect_to.map(\n                        lambda u: util.join_url_params(u, response_dict)\n                    ),\n                )\n            else:\n                response.status = http.HTTPStatus.CREATED\n                response.content_type = b\"application/json\"\n                response.body = json.dumps(response_dict).encode()\n        except Exception as ex:\n            redirect_on_failure = data.get(\n                \"redirect_on_failure\", data.get(\"redirect_to\")\n            )\n            if redirect_on_failure is not None:\n                error_message = str(ex)\n                email = data.get(\"email\", \"\")\n                logger.error(\n                    f\"Error creating identity: error={error_message}, \"\n                    f\"email={email}\"\n                )\n                error_redirect_url = util.join_url_params(\n                    redirect_on_failure,\n                    {\n                        \"error\": error_message,\n                        \"email\": email,\n                    },\n                )\n                return self._try_redirect(response, error_redirect_url)\n            else:\n                raise ex\n\n    async def handle_authenticate(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        data = self._get_data_from_request(request)\n\n        _check_keyset(data, {\"provider\", \"challenge\", \"email\", \"password\"})\n        challenge = data[\"challenge\"]\n        email = data[\"email\"]\n        password = data[\"password\"]\n\n        await pkce.create(self.db, challenge)\n\n        allowed_redirect_to = self._maybe_make_allowed_url(\n            cast(Optional[str], data.get(\"redirect_to\"))\n        )\n\n        email_password_client = email_password.Client(db=self.db)\n        try:\n            local_identity = await email_password_client.authenticate(\n                email, password\n            )\n            verified_at = (\n                await email_password_client.get_verified_by_identity_id(\n                    identity_id=local_identity.id\n                )\n            )\n            if (\n                email_password_client.config.require_verification\n                and verified_at is None\n            ):\n                raise errors.VerificationRequired()\n\n            pkce_code = await pkce.link_identity_challenge(\n                self.db, local_identity.id, challenge\n            )\n            response_dict = {\"code\": pkce_code}\n            logger.info(\n                f\"Authentication successful: identity_id={local_identity.id}, \"\n                f\"pkce_id={pkce_code}\"\n            )\n            if allowed_redirect_to:\n                self._do_redirect(\n                    response,\n                    allowed_redirect_to.map(\n                        lambda u: util.join_url_params(u, response_dict)\n                    ),\n                )\n            else:\n                response.status = http.HTTPStatus.OK\n                response.content_type = b\"application/json\"\n                response.body = json.dumps(response_dict).encode()\n        except Exception as ex:\n            redirect_on_failure = data.get(\n                \"redirect_on_failure\", data.get(\"redirect_to\")\n            )\n            if redirect_on_failure is not None:\n                error_message = str(ex)\n                email = data.get(\"email\", \"\")\n                logger.error(\n                    f\"Error authenticating: error={error_message}, \"\n                    f\"email={email}\"\n                )\n                error_redirect_url = util.join_url_params(\n                    redirect_on_failure,\n                    {\n                        \"error\": error_message,\n                        \"email\": email,\n                    },\n                )\n                return self._try_redirect(response, error_redirect_url)\n            else:\n                raise ex\n\n    async def handle_verify(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        data = self._get_data_from_request(request)\n\n        verification_token = data.get(\"verification_token\")\n        email = data.get(\"email\")\n        code = data.get(\"code\")\n        provider = data.get(\"provider\")\n\n        if not provider:\n            raise errors.InvalidData('Missing \"provider\" in verify request')\n\n        if verification_token:\n            try:\n                token = jwt.VerificationToken.verify(\n                    verification_token,\n                    self.signing_key,\n                )\n\n                email_factor = await self._try_verify_email(\n                    provider=provider,\n                    identity_id=token.subject,\n                )\n                await self._maybe_send_webhook(\n                    webhook.EmailVerified(\n                        event_id=str(uuid.uuid4()),\n                        timestamp=datetime.datetime.now(datetime.timezone.utc),\n                        identity_id=token.subject,\n                        email_factor_id=email_factor.id,\n                    )\n                )\n            except errors.VerificationTokenExpired:\n                response.status = http.HTTPStatus.FORBIDDEN\n                response.content_type = b\"application/json\"\n                error_message = \"The verification token is older than 24 hours\"\n                logger.error(f\"Verification token expired: {error_message}\")\n                response.body = json.dumps({\"message\": error_message}).encode()\n                return\n\n            logger.info(\n                f\"Email verified via token: identity_id={token.subject}, \"\n                f\"email_factor_id={email_factor.id}, \"\n                f\"email={email_factor.email}\"\n            )\n            identity_id = token.subject\n            challenge = token.maybe_challenge\n            redirect_to = token.maybe_redirect_to\n\n        elif email and code:\n            _check_keyset(\n                data,\n                {\n                    \"email\",\n                    \"code\",\n                    \"provider\",\n                },\n            )\n            email_client: local.Client\n            if provider == \"builtin::local_emailpassword\":\n                email_client = email_password.Client(db=self.db)\n            elif provider == \"builtin::local_webauthn\":\n                email_client = webauthn.Client(db=self.db)\n            else:\n                raise errors.InvalidData(f\"Unsupported provider: {provider}\")\n\n            try:\n                maybe_email_factor = (\n                    await email_client.get_email_factor_by_email(email)\n                )\n                if maybe_email_factor is None:\n                    raise errors.NoIdentityFound(\"Invalid email\")\n\n                email_factor = maybe_email_factor\n\n                otc_id = await otc.verify(self.db, str(email_factor.id), code)\n\n                await self._handle_otc_verified(\n                    identity_id=str(email_factor.identity.id),\n                    email_factor_id=str(email_factor.id),\n                    otc_id=str(otc_id),\n                )\n\n                await self._try_verify_email(\n                    provider=provider,\n                    identity_id=email_factor.identity.id,\n                )\n\n                await self._maybe_send_webhook(\n                    webhook.EmailVerified(\n                        event_id=str(uuid.uuid4()),\n                        timestamp=datetime.datetime.now(datetime.timezone.utc),\n                        identity_id=email_factor.identity.id,\n                        email_factor_id=email_factor.id,\n                    )\n                )\n\n                logger.info(\n                    f\"Email verified via OTC: \"\n                    f\"identity_id={email_factor.identity.id}, \"\n                    f\"email_factor_id={email_factor.id}, \"\n                    f\"email={email}\"\n                )\n\n                identity_id = email_factor.identity.id\n                challenge = data.get(\"challenge\")\n                redirect_to = data.get(\"redirect_to\")\n\n            except Exception as ex:\n                self._handle_otc_failed(ex)\n                response.status = http.HTTPStatus.BAD_REQUEST\n                response.content_type = b\"application/json\"\n                response.body = json.dumps(\n                    {\"error\": str(ex), \"error_code\": \"verification_failed\"}\n                ).encode()\n                return\n\n        else:\n            raise errors.InvalidData(\n                'Must provide either \"verification_token\" (Link mode) '\n                'or \"email\" + \"code\" (OTC mode)'\n            )\n\n        logger.info(f\"Challenge: {challenge}, Redirect to: {redirect_to}\")\n        match (challenge, redirect_to):\n            case (str(), str()):\n                await pkce.create(self.db, challenge)\n                code = await pkce.link_identity_challenge(\n                    self.db, identity_id, challenge\n                )\n                return self._try_redirect(\n                    response,\n                    util.join_url_params(redirect_to, {\"code\": code}),\n                )\n            case (str(), None):\n                await pkce.create(self.db, challenge)\n                code = await pkce.link_identity_challenge(\n                    self.db, identity_id, challenge\n                )\n                response.status = http.HTTPStatus.OK\n                response.content_type = b\"application/json\"\n                response.body = json.dumps({\"code\": code}).encode()\n                return\n            case (None, str()):\n                return self._try_redirect(response, redirect_to)\n            case (None, None):\n                response.status = http.HTTPStatus.NO_CONTENT\n                return\n\n    async def handle_resend_verification_email(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        request_data = self._get_data_from_request(request)\n\n        _check_keyset(request_data, {\"provider\"})\n        provider_name = request_data[\"provider\"]\n        local_client: email_password.Client | webauthn.Client\n        match provider_name:\n            case \"builtin::local_emailpassword\":\n                local_client = email_password.Client(db=self.db)\n            case \"builtin::local_webauthn\":\n                local_client = webauthn.Client(db=self.db)\n            case _:\n                raise errors.InvalidData(\n                    f\"Unsupported provider: {request_data['provider']}\"\n                )\n\n        verify_url = request_data.get(\n            \"verify_url\", f\"{self.base_path}/ui/verify\"\n        )\n        email_factor: Optional[EmailFactor] = None\n        if \"verification_token\" in request_data:\n            token = jwt.VerificationToken.verify(\n                request_data[\"verification_token\"],\n                self.signing_key,\n                skip_expiration_check=True,\n            )\n            identity_id = token.subject\n            verify_url = token.verify_url\n            maybe_challenge = token.maybe_challenge\n            maybe_redirect_to = token.maybe_redirect_to\n            email_factor = await local_client.get_email_factor_by_identity_id(\n                identity_id\n            )\n        else:\n            maybe_challenge = request_data.get(\n                \"challenge\", request_data.get(\"code_challenge\")\n            )\n            maybe_redirect_to = request_data.get(\"redirect_to\")\n            if maybe_redirect_to and not self._is_url_allowed(\n                maybe_redirect_to\n            ):\n                raise errors.InvalidData(\n                    \"Redirect URL does not match any allowed URLs.\",\n                )\n            match local_client:\n                case webauthn.Client():\n                    _check_keyset(request_data, {\"credential_id\"})\n                    credential_id = base64.b64decode(\n                        request_data[\"credential_id\"]\n                    )\n                    email_factor = (\n                        await local_client.get_email_factor_by_credential_id(\n                            credential_id\n                        )\n                    )\n                case email_password.Client():\n                    _check_keyset(request_data, {\"email\"})\n                    email_factor = await local_client.get_email_factor_by_email(\n                        request_data[\"email\"]\n                    )\n\n        if email_factor is None:\n            match local_client:\n                case webauthn.Client():\n                    logger.debug(\n                        f\"Failed to find email factor for resend verification \"\n                        f\"email: provider={provider_name}, \"\n                        f\"webauthn_credential_id={request_data.get('credential_id')}\"\n                    )\n                case email_password.Client():\n                    logger.debug(\n                        f\"Failed to find email factor for resend verification \"\n                        f\"email: provider={provider_name}, \"\n                        f\"email={request_data.get('email')}\"\n                    )\n            await auth_emails.send_fake_email(self.tenant)\n        else:\n            logger.info(\n                f\"Resending verification email: provider={provider_name}, \"\n                f\"identity_id={email_factor.identity.id}, \"\n                f\"email_factor_id={email_factor.id}, \"\n                f\"email={email_factor.email}\"\n            )\n            verification_token = self._make_verification_token(\n                identity_id=email_factor.identity.id,\n                verify_url=verify_url,\n                maybe_challenge=maybe_challenge,\n                maybe_redirect_to=maybe_redirect_to,\n            )\n            await self._maybe_send_webhook(\n                webhook.EmailVerificationRequested(\n                    event_id=str(uuid.uuid4()),\n                    timestamp=datetime.datetime.now(datetime.timezone.utc),\n                    identity_id=email_factor.identity.id,\n                    email_factor_id=email_factor.id,\n                    verification_token=verification_token,\n                )\n            )\n            await self._send_verification_email(\n                provider=request_data[\"provider\"],\n                verification_token=verification_token,\n                to_addr=email_factor.email,\n                verify_url=verify_url,\n            )\n\n        response.status = http.HTTPStatus.OK\n\n    async def handle_send_reset_email(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        data = self._get_data_from_request(request)\n\n        _check_keyset(data, {\"provider\", \"email\", \"reset_url\", \"challenge\"})\n        email = data[\"email\"]\n        email_password_client = email_password.Client(db=self.db)\n        if not self._is_url_allowed(data[\"reset_url\"]):\n            raise errors.InvalidData(\n                \"Redirect URL does not match any allowed URLs.\",\n            )\n        allowed_redirect_to = self._maybe_make_allowed_url(\n            data.get(\"redirect_to\")\n        )\n\n        try:\n            try:\n                (\n                    email_factor,\n                    secret,\n                ) = await email_password_client.get_email_factor_and_secret(\n                    email\n                )\n                identity_id = email_factor.identity.id\n\n                if email_password_client.config.verification_method == \"Code\":\n                    code, otc_id = await otc.create(\n                        self.db,\n                        str(email_factor.id),\n                        datetime.timedelta(minutes=10),\n                    )\n                    await auth_emails.send_password_reset_code_email(\n                        db=self.db,\n                        tenant=self.tenant,\n                        to_addr=email,\n                        code=code,\n                        test_mode=self.test_mode,\n                    )\n\n                    await self._handle_otc_initiated(\n                        identity_id=identity_id,\n                        email_factor_id=str(email_factor.id),\n                        otc_id=str(otc_id),\n                        one_time_code=code,\n                    )\n\n                    logger.info(\n                        \"Sent OTC password reset email: \"\n                        f\"email={email}, otc_id={otc_id}\"\n                    )\n                else:\n                    new_reset_token = jwt.ResetToken(\n                        subject=identity_id,\n                        secret=secret,\n                        challenge=data[\"challenge\"],\n                    ).sign(self.signing_key)\n\n                    reset_token_params = {\"reset_token\": new_reset_token}\n                    reset_url = util.join_url_params(\n                        data['reset_url'], reset_token_params\n                    )\n                    await self._maybe_send_webhook(\n                        webhook.PasswordResetRequested(\n                            event_id=str(uuid.uuid4()),\n                            timestamp=datetime.datetime.now(\n                                datetime.timezone.utc\n                            ),\n                            identity_id=identity_id,\n                            reset_token=new_reset_token,\n                            email_factor_id=email_factor.id,\n                        )\n                    )\n\n                    await auth_emails.send_password_reset_email(\n                        db=self.db,\n                        tenant=self.tenant,\n                        to_addr=email,\n                        reset_url=reset_url,\n                        test_mode=self.test_mode,\n                    )\n\n            except errors.NoIdentityFound:\n                logger.debug(\n                    f\"Failed to find identity for send reset email: \"\n                    f\"email={email}\"\n                )\n                await auth_emails.send_fake_email(self.tenant)\n\n            return_data = {\n                \"email_sent\": email,\n            }\n\n            if allowed_redirect_to:\n                return self._do_redirect(\n                    response,\n                    allowed_redirect_to.map(\n                        lambda u: util.join_url_params(u, return_data)\n                    ),\n                )\n            else:\n                response.status = http.HTTPStatus.OK\n                response.content_type = b\"application/json\"\n                response.body = json.dumps(return_data).encode()\n        except aiosmtplib.SMTPException as ex:\n            if not debug.flags.server:\n                logger.warning(\"Failed to send emails via SMTP\", exc_info=True)\n            raise edb_errors.InternalServerError(\n                \"Failed to send the email, please try again later.\"\n            ) from ex\n\n        except Exception as ex:\n            redirect_on_failure = data.get(\n                \"redirect_on_failure\", data.get(\"redirect_to\")\n            )\n            if redirect_on_failure is not None:\n                error_message = str(ex)\n                logger.error(\n                    f\"Error sending reset email: error={error_message}, \"\n                    f\"email={email}\"\n                )\n                redirect_url = util.join_url_params(\n                    redirect_on_failure,\n                    {\n                        \"error\": error_message,\n                        \"email\": email,\n                    },\n                )\n                return self._try_redirect(\n                    response,\n                    redirect_url,\n                )\n            else:\n                raise ex\n\n    async def handle_reset_password(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        data = self._get_data_from_request(request)\n\n        try:\n            _check_keyset(data, {\"password\", \"provider\"})\n            password = data['password']\n\n            reset_token = data.get('reset_token')\n            email = data.get('email')\n            code = data.get('code')\n\n            allowed_redirect_to = self._maybe_make_allowed_url(\n                data.get(\"redirect_to\")\n            )\n\n            email_password_client = email_password.Client(db=self.db)\n\n            if reset_token:\n                token = jwt.ResetToken.verify(\n                    reset_token,\n                    self.signing_key,\n                )\n\n                await email_password_client.update_password(\n                    token.subject, token.secret, password\n                )\n                await pkce.create(self.db, token.challenge)\n                code = await pkce.link_identity_challenge(\n                    self.db, token.subject, token.challenge\n                )\n                response_dict = {\"code\": code}\n                logger.info(\n                    \"Reset password via token: \"\n                    f\"identity_id={token.subject}, pkce_id={code}\"\n                )\n\n            elif email and code:\n                try:\n                    (\n                        email_factor,\n                        secret,\n                    ) = await email_password_client.get_email_factor_and_secret(\n                        email\n                    )\n\n                    otc_id = await otc.verify(\n                        self.db, str(email_factor.id), code\n                    )\n                    logger.info(\n                        \"OTC verified for password reset: \"\n                        f\"otc_id={otc_id}, email={email}\"\n                    )\n\n                    await self._handle_otc_verified(\n                        identity_id=email_factor.identity.id,\n                        email_factor_id=str(email_factor.id),\n                        otc_id=str(otc_id),\n                    )\n                except Exception as ex:\n                    self._handle_otc_failed(ex)\n                    raise\n\n                try:\n                    await email_password_client.update_password(\n                        email_factor.identity.id, secret, password\n                    )\n                except Exception as ex:\n                    raise errors.InvalidData(\n                        f\"Failed to reset password: {str(ex)}\"\n                    )\n\n                challenge = data.get('challenge')\n                if challenge:\n                    await pkce.create(self.db, challenge)\n                    auth_code = await pkce.link_identity_challenge(\n                        self.db, email_factor.identity.id, challenge\n                    )\n                    response_dict = {\"code\": auth_code}\n                    logger.info(\n                        \"Reset password via OTC: \"\n                        f\"identity_id={email_factor.identity.id}, email={email}\"\n                    )\n                else:\n                    response_dict = {\"status\": \"password_reset\"}\n\n            else:\n                raise errors.InvalidData(\n                    'Must provide either \"reset_token\" (Token mode) '\n                    'or \"email\" + \"code\" (OTC mode)'\n                )\n\n            if allowed_redirect_to:\n                return self._do_redirect(\n                    response,\n                    allowed_redirect_to.map(\n                        lambda u: util.join_url_params(u, response_dict)\n                    ),\n                )\n            else:\n                response.status = http.HTTPStatus.OK\n                response.content_type = b\"application/json\"\n                response.body = json.dumps(response_dict).encode()\n\n        except Exception as ex:\n            redirect_on_failure = data.get(\n                \"redirect_on_failure\", data.get(\"redirect_to\")\n            )\n            if redirect_on_failure is not None:\n                error_message = str(ex)\n                logger.error(\n                    f\"Error resetting password: error={error_message}, \"\n                    f\"reset_token={reset_token}, email={email}\"\n                )\n                error_params = {\n                    \"error\": error_message,\n                }\n                if reset_token:\n                    error_params[\"reset_token\"] = reset_token\n                if email:\n                    error_params[\"email\"] = email\n                redirect_url = util.join_url_params(\n                    redirect_on_failure,\n                    error_params,\n                )\n                return self._try_redirect(response, redirect_url)\n            else:\n                raise ex\n\n    async def handle_magic_link_register(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        data = self._get_data_from_request(request)\n        email: str | None = None\n\n        try:\n            _check_keyset(data, {\"email\"})\n\n            email = cast(str, data[\"email\"])\n            allowed_redirect_to = self._maybe_make_allowed_url(\n                data.get(\"redirect_to\")\n            )\n\n            magic_link_client = magic_link.Client(\n                db=self.db,\n                issuer=self.base_path,\n                tenant=self.tenant,\n                test_mode=self.test_mode,\n                signing_key=self.signing_key,\n            )\n\n            if not _accepts_json(request) and not allowed_redirect_to:\n                raise errors.InvalidData(\n                    \"Request must accept JSON or provide a redirect URL.\"\n                )\n            email_factor = await magic_link_client.register(\n                email=email,\n            )\n            await self._maybe_send_webhook(\n                webhook.IdentityCreated(\n                    event_id=str(uuid.uuid4()),\n                    timestamp=datetime.datetime.now(datetime.timezone.utc),\n                    identity_id=email_factor.identity.id,\n                )\n            )\n            await self._maybe_send_webhook(\n                webhook.EmailFactorCreated(\n                    event_id=str(uuid.uuid4()),\n                    timestamp=datetime.datetime.now(datetime.timezone.utc),\n                    identity_id=email_factor.identity.id,\n                    email_factor_id=email_factor.id,\n                )\n            )\n            if magic_link_client.provider.verification_method == \"Code\":\n                code, otc_id = await otc.create(\n                    self.db,\n                    str(email_factor.id),\n                    datetime.timedelta(minutes=10),\n                )\n                await auth_emails.send_one_time_code_email(\n                    db=self.db,\n                    tenant=self.tenant,\n                    to_addr=email,\n                    code=code,\n                    test_mode=self.test_mode,\n                )\n\n                await self._handle_otc_initiated(\n                    identity_id=str(email_factor.identity.id),\n                    email_factor_id=str(email_factor.id),\n                    otc_id=str(otc_id),\n                    one_time_code=code,\n                )\n\n                logger.info(\n                    \"Sent OTC email: \"\n                    f\"identity_id={email_factor.identity.id}, \"\n                    f\"email={email}, otc_id={otc_id}\"\n                )\n\n                return_data = {\n                    \"code\": \"true\",\n                    \"signup\": \"true\",\n                    \"identity_id\": str(email_factor.identity.id),\n                    \"email\": email,\n                }\n\n            else:\n                _check_keyset(\n                    data, {\"challenge\", \"callback_url\", \"redirect_on_failure\"}\n                )\n\n                challenge = data[\"challenge\"]\n                callback_url = data[\"callback_url\"]\n                if not self._is_url_allowed(callback_url):\n                    raise errors.InvalidData(\n                        \"Callback URL does not match any allowed URLs.\",\n                    )\n\n                allowed_redirect_on_failure = self._make_allowed_url(\n                    data[\"redirect_on_failure\"]\n                )\n                allowed_link_url = self._maybe_make_allowed_url(\n                    data.get(\"link_url\")\n                )\n                link_url = (\n                    allowed_link_url.url\n                    if allowed_link_url\n                    else f\"{self.base_path}/magic-link/authenticate\"\n                )\n\n                magic_link_token = magic_link_client.make_magic_link_token(\n                    identity_id=email_factor.identity.id,\n                    callback_url=callback_url,\n                    challenge=challenge,\n                )\n                await self._maybe_send_webhook(\n                    webhook.MagicLinkRequested(\n                        event_id=str(uuid.uuid4()),\n                        timestamp=datetime.datetime.now(datetime.timezone.utc),\n                        identity_id=email_factor.identity.id,\n                        email_factor_id=email_factor.id,\n                        magic_link_token=magic_link_token,\n                        magic_link_url=link_url,\n                    )\n                )\n                logger.info(\n                    \"Sending magic link: \"\n                    f\"identity_id={email_factor.identity.id}, email={email}\"\n                )\n\n                await magic_link_client.send_magic_link(\n                    email=email,\n                    link_url=link_url,\n                    redirect_on_failure=allowed_redirect_on_failure.url,\n                    token=magic_link_token,\n                )\n\n                return_data = {\n                    \"email_sent\": email,\n                }\n\n            if _accepts_json(request):\n                response.status = http.HTTPStatus.OK\n                response.content_type = b\"application/json\"\n                response.body = json.dumps(return_data).encode()\n            elif allowed_redirect_to:\n                return self._do_redirect(\n                    response,\n                    allowed_redirect_to.map(\n                        lambda u: util.join_url_params(u, return_data)\n                    ),\n                )\n            else:\n                # This should not happen since we check earlier for this case\n                # but this seems safer than a cast\n                raise errors.InvalidData(\n                    \"Request must accept JSON or provide a redirect URL.\"\n                )\n        except Exception as ex:\n            if _accepts_json(request):\n                raise ex\n\n            redirect_on_failure = data.get(\n                \"redirect_on_failure\", data.get(\"redirect_to\")\n            )\n            error_message = str(ex)\n            email = email or \"\"\n            logger.error(\n                f\"Error sending magic link email: error={error_message}, \"\n                f\"email={email}\"\n            )\n\n            if redirect_on_failure is None:\n                raise ex\n            error_redirect_url = util.join_url_params(\n                redirect_on_failure,\n                {\n                    \"error\": error_message,\n                    \"email\": email,\n                },\n            )\n            self._try_redirect(response, error_redirect_url)\n\n    async def handle_magic_link_email(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        data = self._get_data_from_request(request)\n        email: str | None = None\n        return_data: dict[str, Any] = {}\n\n        try:\n            _check_keyset(data, {\"email\"})\n            email = cast(str, data[\"email\"])\n\n            allowed_redirect_to = self._maybe_make_allowed_url(\n                data.get(\"redirect_to\")\n            )\n\n            magic_link_client = magic_link.Client(\n                db=self.db,\n                issuer=self.base_path,\n                tenant=self.tenant,\n                test_mode=self.test_mode,\n                signing_key=self.signing_key,\n            )\n            email_factor = await magic_link_client.get_email_factor_by_email(\n                email\n            )\n            is_signup = False\n            if email_factor is None:\n                if magic_link_client.provider.auto_signup:\n                    # Auto-signup is enabled, create a new user\n                    is_signup = True\n                    email_factor = await magic_link_client.register(\n                        email=email,\n                    )\n                    await self._maybe_send_webhook(\n                        webhook.IdentityCreated(\n                            event_id=str(uuid.uuid4()),\n                            timestamp=datetime.datetime.now(datetime.timezone.utc),\n                            identity_id=email_factor.identity.id,\n                        )\n                    )\n                    await self._maybe_send_webhook(\n                        webhook.EmailFactorCreated(\n                            event_id=str(uuid.uuid4()),\n                            timestamp=datetime.datetime.now(datetime.timezone.utc),\n                            identity_id=email_factor.identity.id,\n                            email_factor_id=email_factor.id,\n                        )\n                    )\n                else:\n                    logger.error(\n                        \"Cannot send magic link email: no email factor found \"\n                        f\"for email={email}\"\n                    )\n                    await auth_emails.send_fake_email(self.tenant)\n                    if magic_link_client.provider.verification_method == \"Code\":\n                        return_data = {\n                            \"code\": \"true\",\n                            \"email\": email,\n                        }\n                    else:\n                        return_data = {\n                            \"email_sent\": email,\n                        }\n            if email_factor is not None:\n                identity_id = email_factor.identity.id\n                if magic_link_client.provider.verification_method == \"Code\":\n                    code, otc_id = await otc.create(\n                        self.db,\n                        str(email_factor.id),\n                        datetime.timedelta(minutes=10),\n                    )\n                    await auth_emails.send_one_time_code_email(\n                        db=self.db,\n                        tenant=self.tenant,\n                        to_addr=email,\n                        code=code,\n                        test_mode=self.test_mode,\n                    )\n\n                    await self._handle_otc_initiated(\n                        identity_id=str(identity_id),\n                        email_factor_id=str(email_factor.id),\n                        otc_id=str(otc_id),\n                        one_time_code=code,\n                    )\n\n                    logger.info(\n                        f\"Sent OTC email: identity_id={identity_id}, \"\n                        f\"email={email}, otc_id={otc_id}\"\n                    )\n\n                    return_data = {\n                        \"code\": \"true\",\n                        \"email\": email,\n                    }\n                    if is_signup:\n                        return_data[\"identity_id\"] = str(identity_id)\n                        return_data[\"signup\"] = \"true\"\n                else:\n                    _check_keyset(\n                        data,\n                        {\"challenge\", \"callback_url\", \"redirect_on_failure\"},\n                    )\n                    challenge = data[\"challenge\"]\n                    callback_url = data[\"callback_url\"]\n                    if not self._is_url_allowed(callback_url):\n                        raise errors.InvalidData(\n                            \"callback_url does not match any allowed URLs.\",\n                        )\n                    redirect_on_failure = data[\"redirect_on_failure\"]\n                    if not self._is_url_allowed(redirect_on_failure):\n                        raise errors.InvalidData(\n                            \"redirect_on_failure\"\n                            \" does not match any allowed URLs.\",\n                        )\n\n                    allowed_link_url = self._maybe_make_allowed_url(\n                        data.get(\"link_url\")\n                    )\n                    link_url = (\n                        allowed_link_url.url\n                        if allowed_link_url\n                        else f\"{self.base_path}/magic-link/authenticate\"\n                    )\n                    magic_link_token = magic_link_client.make_magic_link_token(\n                        identity_id=identity_id,\n                        callback_url=callback_url,\n                        challenge=challenge,\n                    )\n                    await self._maybe_send_webhook(\n                        webhook.MagicLinkRequested(\n                            event_id=str(uuid.uuid4()),\n                            timestamp=datetime.datetime.now(\n                                datetime.timezone.utc\n                            ),\n                            identity_id=identity_id,\n                            email_factor_id=email_factor.id,\n                            magic_link_token=magic_link_token,\n                            magic_link_url=link_url,\n                        )\n                    )\n                    await magic_link_client.send_magic_link(\n                        email=email,\n                        token=magic_link_token,\n                        link_url=link_url,\n                        redirect_on_failure=redirect_on_failure,\n                    )\n                    logger.info(\n                        \"Sent magic link email: \"\n                        f\"identity_id={identity_id}, email={email}\"\n                    )\n\n                    return_data = {\n                        \"email_sent\": email,\n                    }\n                    if is_signup:\n                        return_data[\"signup\"] = \"true\"\n\n            if allowed_redirect_to:\n                return self._do_redirect(\n                    response,\n                    allowed_redirect_to.map(\n                        lambda u: util.join_url_params(u, return_data)\n                    ),\n                )\n            else:\n                response.status = http.HTTPStatus.OK\n                response.content_type = b\"application/json\"\n                response.body = json.dumps(return_data).encode()\n        except Exception as ex:\n            if _accepts_json(request):\n                raise ex\n\n            redirect_on_failure = data.get(\n                \"redirect_on_failure\", data.get(\"redirect_to\")\n            )\n            error_message = str(ex)\n            email = email or \"\"\n            logger.error(\n                f\"Error sending magic link email: error={error_message}, \"\n                f\"email={email}\"\n            )\n\n            if redirect_on_failure is None:\n                raise ex\n            error_redirect_url = util.join_url_params(\n                redirect_on_failure,\n                {\n                    \"error\": error_message,\n                    \"email\": email,\n                },\n            )\n            self._try_redirect(response, error_redirect_url)\n\n    async def handle_magic_link_authenticate(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        query = urllib.parse.parse_qs(\n            request.url.query.decode(\"ascii\") if request.url.query else \"\"\n        )\n\n        token_str = _maybe_get_search_param(query, \"token\")\n\n        if token_str:\n            try:\n                token = jwt.MagicLinkToken.verify(token_str, self.signing_key)\n                await pkce.create(self.db, token.challenge)\n                code = await pkce.link_identity_challenge(\n                    self.db, token.subject, token.challenge\n                )\n                local_client = magic_link.Client(\n                    db=self.db,\n                    tenant=self.tenant,\n                    test_mode=self.test_mode,\n                    issuer=self.base_path,\n                    signing_key=self.signing_key,\n                )\n                await local_client.verify_email(\n                    token.subject, datetime.datetime.now(datetime.timezone.utc)\n                )\n\n                return self._try_redirect(\n                    response,\n                    util.join_url_params(token.callback_url, {\"code\": code}),\n                )\n\n            except Exception as ex:\n                redirect_on_failure = _maybe_get_search_param(\n                    query, \"redirect_on_failure\"\n                )\n                if redirect_on_failure is None:\n                    raise ex\n                else:\n                    error_message = str(ex)\n                    logger.error(\n                        \"Error authenticating magic link: \"\n                        f\"error={error_message}, token={token_str}\"\n                    )\n                    redirect_url = util.join_url_params(\n                        redirect_on_failure,\n                        {\n                            \"error\": error_message,\n                        },\n                    )\n                    return self._try_redirect(response, redirect_url)\n        else:\n            try:\n                data = self._get_data_from_request(request)\n\n                _check_keyset(\n                    data, {\"email\", \"code\", \"challenge\"}\n                )\n\n                email = data[\"email\"]\n                code_str = data[\"code\"]\n                challenge = data[\"challenge\"]\n\n                maybe_callback_url = cast(\n                    Optional[str], data.get(\"callback_url\")\n                )\n\n                if (\n                    maybe_callback_url and\n                    not self._is_url_allowed(maybe_callback_url)\n                ):\n                    raise errors.InvalidData(\n                        \"Callback URL does not match any allowed URLs.\",\n                    )\n\n                magic_link_client = magic_link.Client(\n                    db=self.db,\n                    tenant=self.tenant,\n                    test_mode=self.test_mode,\n                    issuer=self.base_path,\n                    signing_key=self.signing_key,\n                )\n\n                email_factor = (\n                    await magic_link_client.get_email_factor_by_email(email)\n                )\n                if email_factor is None:\n                    raise errors.NoIdentityFound(\"Invalid email\")\n\n                try:\n                    otc_id = await otc.verify(\n                        self.db, str(email_factor.id), code_str\n                    )\n\n                    await self._handle_otc_verified(\n                        identity_id=str(email_factor.identity.id),\n                        email_factor_id=str(email_factor.id),\n                        otc_id=str(otc_id),\n                    )\n                except Exception as ex:\n                    self._handle_otc_failed(ex)\n                    raise\n\n                await pkce.create(self.db, challenge)\n                auth_code = await pkce.link_identity_challenge(\n                    self.db, email_factor.identity.id, challenge\n                )\n\n                await magic_link_client.verify_email(\n                    email_factor.identity.id,\n                    datetime.datetime.now(datetime.timezone.utc),\n                )\n\n                response_dict = {\"code\": auth_code}\n\n                if maybe_callback_url:\n                    return self._try_redirect(\n                        response,\n                        util.join_url_params(maybe_callback_url, response_dict),\n                    )\n                else:\n                    response.status = http.HTTPStatus.OK\n                    response.content_type = b\"application/json\"\n                    response.body = json.dumps(response_dict).encode()\n\n            except Exception as ex:\n                redirect_on_failure = _maybe_get_search_param(\n                    query, \"redirect_on_failure\"\n                )\n                if redirect_on_failure is None:\n                    response.status = http.HTTPStatus.BAD_REQUEST\n                    response.content_type = b\"application/json\"\n                    response.body = json.dumps(\n                        {\"error\": str(ex), \"error_code\": \"verification_failed\"}\n                    ).encode()\n                    return\n                else:\n                    error_message = str(ex)\n                    logger.error(\n                        f\"Error authenticating OTC: error={error_message}, \"\n                        f\"email={_maybe_get_search_param(query, 'email')}\"\n                    )\n                    redirect_url = util.join_url_params(\n                        redirect_on_failure,\n                        {\n                            \"error\": error_message,\n                        },\n                    )\n                    return self._try_redirect(response, redirect_url)\n\n    async def handle_webauthn_register_options(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        query = urllib.parse.parse_qs(\n            request.url.query.decode(\"ascii\") if request.url.query else \"\"\n        )\n        email = _get_search_param(query, \"email\")\n        webauthn_client = webauthn.Client(self.db)\n\n        try:\n            (\n                user_handle,\n                registration_options,\n            ) = await webauthn_client.create_registration_options_for_email(\n                email=email,\n            )\n        except Exception as e:\n            raise errors.WebAuthnRegistrationFailed(\n                \"Failed to create registration options\"\n            ) from e\n\n        response.status = http.HTTPStatus.OK\n        response.content_type = b\"application/json\"\n        _set_cookie(\n            response,\n            \"edgedb-webauthn-registration-user-handle\",\n            user_handle,\n            path=\"/\",\n        )\n        response.body = registration_options\n\n    async def handle_webauthn_register(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        data = self._get_data_from_request(request)\n\n        _check_keyset(\n            data,\n            {\"provider\", \"challenge\", \"email\", \"credentials\", \"verify_url\"},\n        )\n        webauthn_client = webauthn.Client(self.db)\n\n        provider_name: str = data[\"provider\"]\n        email: str = data[\"email\"]\n        verify_url: str = data[\"verify_url\"]\n        credentials: str = data[\"credentials\"]\n        pkce_challenge: str = data[\"challenge\"]\n\n        user_handle_cookie = request.cookies.get(\n            \"edgedb-webauthn-registration-user-handle\"\n        )\n        user_handle_base64url: Optional[str] = (\n            user_handle_cookie.value\n            if user_handle_cookie\n            else data.get(\"user_handle\")\n        )\n        if user_handle_base64url is None:\n            raise errors.InvalidData(\n                \"Missing user_handle from cookie or request body\"\n            )\n        try:\n            user_handle = base64.urlsafe_b64decode(\n                f\"{user_handle_base64url}===\"\n            )\n        except Exception as e:\n            raise errors.InvalidData(\"Failed to decode user_handle\") from e\n\n        require_verification = webauthn_client.provider.require_verification\n        pkce_code: Optional[str] = None\n\n        try:\n            email_factor = await webauthn_client.register(\n                credentials=credentials,\n                email=email,\n                user_handle=user_handle,\n            )\n        except Exception as e:\n            raise errors.WebAuthnRegistrationFailed(\n                \"Failed to register WebAuthn\"\n            ) from e\n\n        identity_id = email_factor.identity.id\n\n        await self._maybe_send_webhook(\n            webhook.IdentityCreated(\n                event_id=str(uuid.uuid4()),\n                timestamp=datetime.datetime.now(datetime.timezone.utc),\n                identity_id=identity_id,\n            )\n        )\n        await self._maybe_send_webhook(\n            webhook.EmailFactorCreated(\n                event_id=str(uuid.uuid4()),\n                timestamp=datetime.datetime.now(datetime.timezone.utc),\n                identity_id=identity_id,\n                email_factor_id=email_factor.id,\n            )\n        )\n\n        verification_token = self._make_verification_token(\n            identity_id=identity_id,\n            verify_url=verify_url,\n            maybe_challenge=pkce_challenge,\n            maybe_redirect_to=None,\n        )\n\n        await self._maybe_send_webhook(\n            webhook.EmailVerificationRequested(\n                event_id=str(uuid.uuid4()),\n                timestamp=datetime.datetime.now(datetime.timezone.utc),\n                identity_id=identity_id,\n                email_factor_id=email_factor.id,\n                verification_token=verification_token,\n            )\n        )\n        await self._send_verification_email(\n            provider=provider_name,\n            verification_token=verification_token,\n            to_addr=email_factor.email,\n            verify_url=verify_url,\n        )\n\n        if not require_verification:\n            await pkce.create(self.db, pkce_challenge)\n            pkce_code = await pkce.link_identity_challenge(\n                self.db, identity_id, pkce_challenge\n            )\n\n        _set_cookie(\n            response,\n            \"edgedb-webauthn-registration-user-handle\",\n            \"\",\n            path=\"/\",\n        )\n        response.status = http.HTTPStatus.CREATED\n        response.content_type = b\"application/json\"\n        if require_verification:\n            now_iso8601 = datetime.datetime.now(\n                datetime.timezone.utc\n            ).isoformat()\n            response.body = json.dumps(\n                {\n                    \"identity_id\": identity_id,\n                    \"email\": email_factor.email,\n                    \"verification_email_sent_at\": now_iso8601,\n                }\n            ).encode()\n            logger.info(\n                f\"Sent verification email: identity_id={identity_id}, \"\n                f\"email={email}\"\n            )\n        else:\n            if pkce_code is None:\n                raise errors.PKCECreationFailed\n            response.body = json.dumps(\n                {\n                    \"code\": pkce_code,\n                    \"provider\": provider_name,\n                    \"email\": email_factor.email,\n                }\n            ).encode()\n            logger.info(\n                f\"WebAuthn registration successful: identity_id={identity_id}, \"\n                f\"email={email}, \"\n                f\"pkce_id={pkce_code}\"\n            )\n\n    async def handle_webauthn_authenticate_options(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        query = urllib.parse.parse_qs(\n            request.url.query.decode(\"ascii\") if request.url.query else \"\"\n        )\n        email = _get_search_param(query, \"email\")\n        webauthn_provider = self._get_webauthn_provider()\n        if webauthn_provider is None:\n            raise errors.MissingConfiguration(\n                \"ext::auth::AuthConfig::providers\",\n                \"WebAuthn provider is not configured\",\n            )\n        webauthn_client = webauthn.Client(self.db)\n\n        try:\n            (\n                _,\n                registration_options,\n            ) = await webauthn_client.create_authentication_options_for_email(\n                email=email, webauthn_provider=webauthn_provider\n            )\n        except Exception as e:\n            raise errors.WebAuthnAuthenticationFailed(\n                \"Failed to create authentication options\"\n            ) from e\n\n        response.status = http.HTTPStatus.OK\n        response.content_type = b\"application/json\"\n        response.body = registration_options\n\n    async def handle_webauthn_authenticate(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        data = self._get_data_from_request(request)\n\n        _check_keyset(\n            data,\n            {\"challenge\", \"email\", \"assertion\"},\n        )\n        webauthn_client = webauthn.Client(self.db)\n\n        email: str = data[\"email\"]\n        assertion: str = data[\"assertion\"]\n        pkce_challenge: str = data[\"challenge\"]\n\n        try:\n            identity = await webauthn_client.authenticate(\n                assertion=assertion,\n                email=email,\n            )\n        except Exception as e:\n            raise errors.WebAuthnAuthenticationFailed(\n                \"Failed to authenticate WebAuthn\"\n            ) from e\n\n        require_verification = webauthn_client.provider.require_verification\n        if require_verification:\n            email_is_verified = await webauthn_client.is_email_verified(\n                email, assertion\n            )\n            if not email_is_verified:\n                raise errors.VerificationRequired()\n\n        await pkce.create(self.db, pkce_challenge)\n        code = await pkce.link_identity_challenge(\n            self.db, identity.id, pkce_challenge\n        )\n\n        logger.info(\n            f\"WebAuthn authentication successful: identity_id={identity.id}, \"\n            f\"email={email}, \"\n            f\"pkce_id={code}\"\n        )\n\n        response.status = http.HTTPStatus.OK\n        response.content_type = b\"application/json\"\n        response.body = json.dumps(\n            {\n                \"code\": code,\n            }\n        ).encode()\n\n    async def handle_ui_signin(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        ui_config = self._get_ui_config()\n\n        if ui_config is None:\n            response.status = http.HTTPStatus.NOT_FOUND\n            response.body = b'Auth UI not enabled'\n        else:\n            providers = util.maybe_get_config(\n                self.db,\n                \"ext::auth::AuthConfig::providers\",\n                frozenset,\n            )\n            if providers is None or len(providers) == 0:\n                raise errors.MissingConfiguration(\n                    'ext::auth::AuthConfig::providers',\n                    'No providers are configured',\n                )\n\n            app_details_config = self._get_app_details_config()\n            query = urllib.parse.parse_qs(\n                request.url.query.decode(\"ascii\") if request.url.query else \"\"\n            )\n\n            maybe_challenge = _get_pkce_challenge(\n                response=response,\n                cookies=request.cookies,\n                query_dict=query,\n            )\n            if maybe_challenge is None:\n                raise errors.InvalidData(\n                    'Missing \"challenge\" in register request'\n                )\n\n            response.status = http.HTTPStatus.OK\n            response.content_type = b'text/html'\n            response.body = ui.render_signin_page(\n                base_path=self.base_path,\n                providers=providers,\n                redirect_to=ui_config.redirect_to,\n                redirect_to_on_signup=ui_config.redirect_to_on_signup,\n                error_message=_maybe_get_search_param(query, 'error'),\n                email=_maybe_get_search_param(query, 'email'),\n                challenge=maybe_challenge,\n                selected_tab=_maybe_get_search_param(query, 'selected_tab'),\n                app_name=app_details_config.app_name,\n                logo_url=app_details_config.logo_url,\n                dark_logo_url=app_details_config.dark_logo_url,\n                brand_color=app_details_config.brand_color,\n            )\n\n    async def handle_ui_signup(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        ui_config = self._get_ui_config()\n        if ui_config is None:\n            response.status = http.HTTPStatus.NOT_FOUND\n            response.body = b'Auth UI not enabled'\n        else:\n            providers = util.maybe_get_config(\n                self.db,\n                \"ext::auth::AuthConfig::providers\",\n                frozenset,\n            )\n            if providers is None or len(providers) == 0:\n                raise errors.MissingConfiguration(\n                    'ext::auth::AuthConfig::providers',\n                    'No providers are configured',\n                )\n\n            query = urllib.parse.parse_qs(\n                request.url.query.decode(\"ascii\") if request.url.query else \"\"\n            )\n\n            maybe_challenge = _get_pkce_challenge(\n                response=response,\n                cookies=request.cookies,\n                query_dict=query,\n            )\n            if maybe_challenge is None:\n                raise errors.InvalidData(\n                    'Missing \"challenge\" in register request'\n                )\n            app_details_config = self._get_app_details_config()\n\n            response.status = http.HTTPStatus.OK\n            response.content_type = b'text/html'\n            response.body = ui.render_signup_page(\n                base_path=self.base_path,\n                providers=providers,\n                redirect_to=ui_config.redirect_to,\n                redirect_to_on_signup=ui_config.redirect_to_on_signup,\n                error_message=_maybe_get_search_param(query, 'error'),\n                email=_maybe_get_search_param(query, 'email'),\n                challenge=maybe_challenge,\n                selected_tab=_maybe_get_search_param(query, 'selected_tab'),\n                app_name=app_details_config.app_name,\n                logo_url=app_details_config.logo_url,\n                dark_logo_url=app_details_config.dark_logo_url,\n                brand_color=app_details_config.brand_color,\n            )\n\n    async def handle_ui_forgot_password(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        ui_config = self._get_ui_config()\n        password_provider = (\n            self._get_password_provider() if ui_config is not None else None\n        )\n\n        if ui_config is None or password_provider is None:\n            response.status = http.HTTPStatus.NOT_FOUND\n            response.body = (\n                b'Password provider not configured'\n                if ui_config\n                else b'Auth UI not enabled'\n            )\n            return\n\n        query = urllib.parse.parse_qs(\n            request.url.query.decode(\"ascii\") if request.url.query else \"\"\n        )\n        challenge = _get_search_param(\n            query, \"challenge\", fallback_keys=[\"code_challenge\"]\n        )\n        app_details_config = self._get_app_details_config()\n\n        redirect_on_failure = (\n            f\"{self.base_path}/ui/forgot-password?challenge={challenge}\"\n        )\n        reset_url = f\"{self.base_path}/ui/reset-password\"\n        if password_provider.verification_method == \"Code\":\n            redirect_to = (\n                f\"{self.base_path}/ui/reset-password?\"\n                f\"code=true&challenge={challenge}\"\n            )\n        else:\n            redirect_to = (\n                f\"{self.base_path}/ui/forgot-password?challenge={challenge}\"\n            )\n\n        response.status = http.HTTPStatus.OK\n        response.content_type = b'text/html'\n        response.body = ui.render_forgot_password_page(\n            redirect_to=redirect_to,\n            redirect_on_failure=redirect_on_failure,\n            reset_url=reset_url,\n            provider_name=password_provider.name,\n            error_message=_maybe_get_search_param(query, 'error'),\n            email=_maybe_get_search_param(query, 'email'),\n            email_sent=_maybe_get_search_param(query, 'email_sent'),\n            challenge=challenge,\n            app_name=app_details_config.app_name,\n            logo_url=app_details_config.logo_url,\n            dark_logo_url=app_details_config.dark_logo_url,\n            brand_color=app_details_config.brand_color,\n        )\n\n    async def handle_ui_reset_password(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        ui_config = self._get_ui_config()\n        password_provider = (\n            self._get_password_provider() if ui_config is not None else None\n        )\n\n        if ui_config is None or password_provider is None:\n            raise errors.NotFound(\n                'Password provider not configured'\n                if ui_config\n                else 'Auth UI not enabled'\n            )\n\n        query = urllib.parse.parse_qs(\n            request.url.query.decode(\"ascii\") if request.url.query else \"\"\n        )\n\n        challenge = _get_pkce_challenge(\n            response=response,\n            cookies=request.cookies,\n            query_dict=query,\n        )\n\n        if challenge is None:\n            raise errors.InvalidData(\n                'Missing \"challenge\" in reset password request'\n            )\n\n        error_message = _maybe_get_search_param(query, \"error\")\n\n        if password_provider.verification_method == \"Code\":\n            maybe_email = _maybe_get_search_param(\n                query, \"email\", fallback_keys=[\"email_sent\"]\n            )\n            if maybe_email is None:\n                raise errors.InvalidData('Missing \"email\" for reset code flow')\n            app_details_config = self._get_app_details_config()\n            response.status = http.HTTPStatus.OK\n            response.content_type = b'text/html'\n            response.body = ui.render_reset_password_page(\n                base_path=self.base_path,\n                provider_name=password_provider.name,\n                is_valid=True,  # Code flow is always valid to show the form\n                redirect_to=ui_config.redirect_to,\n                challenge=challenge,\n                reset_token=None,\n                error_message=error_message,\n                is_code_flow=True,\n                email=maybe_email,\n                app_name=app_details_config.app_name,\n                logo_url=app_details_config.logo_url,\n                dark_logo_url=app_details_config.dark_logo_url,\n                brand_color=app_details_config.brand_color,\n            )\n            return\n\n        try:\n            reset_token = _get_search_param(query, 'reset_token')\n            token = jwt.ResetToken.verify(reset_token, self.signing_key)\n\n            email_password_client = email_password.Client(\n                db=self.db,\n            )\n\n            is_valid = (\n                await email_password_client.validate_reset_secret(\n                    token.subject, token.secret\n                )\n                is not None\n            )\n        except Exception:\n            is_valid = False\n\n        app_details_config = self._get_app_details_config()\n        response.status = http.HTTPStatus.OK\n        response.content_type = b'text/html'\n        response.body = ui.render_reset_password_page(\n            base_path=self.base_path,\n            provider_name=password_provider.name,\n            is_valid=is_valid,\n            redirect_to=ui_config.redirect_to,\n            reset_token=reset_token,\n            challenge=challenge,\n            error_message=error_message,\n            is_code_flow=False,\n            email=None,\n            app_name=app_details_config.app_name,\n            logo_url=app_details_config.logo_url,\n            dark_logo_url=app_details_config.dark_logo_url,\n            brand_color=app_details_config.brand_color,\n        )\n\n    async def handle_ui_verify(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        error_messages: list[str] = []\n        is_valid = True\n        is_code_method: bool = False\n\n        ui_config = self._get_ui_config()\n        if ui_config is None:\n            response.status = http.HTTPStatus.NOT_FOUND\n            response.body = b'Auth UI not enabled'\n            return\n\n        query = urllib.parse.parse_qs(\n            request.url.query.decode(\"ascii\") if request.url.query else \"\"\n        )\n\n        maybe_provider_name = _maybe_get_search_param(query, \"provider\")\n        provider: (\n            config.WebAuthnProvider | config.EmailPasswordProviderConfig | None\n        ) = None\n\n        # Decide flow by provider config\n        match maybe_provider_name:\n            case \"builtin::local_emailpassword\":\n                provider = self._get_password_provider()\n                if provider is None:\n                    raise errors.MissingConfiguration(\n                        \"ext::auth::AuthConfig::providers\",\n                        \"EmailPassword provider is not configured\",\n                    )\n                is_code_method = provider.verification_method == \"Code\"\n            case \"builtin::local_webauthn\":\n                provider = self._get_webauthn_provider()\n                if provider is None:\n                    raise errors.MissingConfiguration(\n                        \"ext::auth::AuthConfig::providers\",\n                        \"WebAuthn provider is not configured\",\n                    )\n                is_code_method = provider.verification_method == \"Code\"\n            case _:\n                raise errors.InvalidData(\n                    f\"Unknown provider: {maybe_provider_name}\"\n                )\n\n        if is_code_method:\n            email = _get_search_param(query, \"email\")\n            error_message = _maybe_get_search_param(query, \"error\")\n            challenge = _get_pkce_challenge(\n                cookies=request.cookies,\n                response=response,\n                query_dict=query,\n            )\n            if challenge is None:\n                raise errors.InvalidData(\n                    'Missing \"challenge\" in email verification request'\n                )\n\n            app_details_config = self._get_app_details_config()\n            response.status = http.HTTPStatus.OK\n            response.content_type = b'text/html'\n            response.body = ui.render_email_verification_page_code_flow(\n                challenge=challenge,\n                email=email,\n                provider=maybe_provider_name,\n                base_path=self.base_path,\n                callback_url=(\n                    ui_config.redirect_to_on_signup or ui_config.redirect_to\n                ),\n                error_message=error_message,\n                app_name=app_details_config.app_name,\n                logo_url=app_details_config.logo_url,\n                dark_logo_url=app_details_config.dark_logo_url,\n                brand_color=app_details_config.brand_color,\n            )\n            return\n\n        maybe_pkce_code: str | None = None\n        redirect_to = ui_config.redirect_to_on_signup or ui_config.redirect_to\n\n        maybe_verification_token = _maybe_get_search_param(\n            query, \"verification_token\"\n        )\n\n        match (maybe_provider_name, maybe_verification_token):\n            case (None, None):\n                error_messages.append(\n                    \"Missing provider and email verification token.\"\n                )\n                is_valid = False\n            case (None, _):\n                error_messages.append(\"Missing provider.\")\n                is_valid = False\n            case (_, None):\n                error_messages.append(\"Missing email verification token.\")\n                is_valid = False\n            case (str(provider_name), str(verification_token)):\n                try:\n                    token = jwt.VerificationToken.verify(\n                        verification_token,\n                        self.signing_key,\n                    )\n                    await self._try_verify_email(\n                        provider=provider_name,\n                        identity_id=token.subject,\n                    )\n\n                    match token.maybe_challenge:\n                        case str(ch):\n                            await pkce.create(self.db, ch)\n                            maybe_pkce_code = (\n                                await pkce.link_identity_challenge(\n                                    self.db,\n                                    token.subject,\n                                    ch,\n                                )\n                            )\n                        case _:\n                            maybe_pkce_code = None\n\n                    redirect_to = token.maybe_redirect_to or redirect_to\n                    redirect_to = (\n                        util.join_url_params(\n                            redirect_to,\n                            {\n                                \"code\": maybe_pkce_code,\n                            },\n                        )\n                        if maybe_pkce_code\n                        else redirect_to\n                    )\n\n                except errors.VerificationTokenExpired:\n                    app_details_config = self._get_app_details_config()\n                    response.status = http.HTTPStatus.OK\n                    response.content_type = b\"text/html\"\n                    response.body = ui.render_email_verification_expired_page(\n                        verification_token=verification_token,\n                        app_name=app_details_config.app_name,\n                        logo_url=app_details_config.logo_url,\n                        dark_logo_url=app_details_config.dark_logo_url,\n                        brand_color=app_details_config.brand_color,\n                    )\n                    return\n\n                except Exception as ex:\n                    error_messages.append(repr(ex))\n                    is_valid = False\n\n        # Only redirect back if verification succeeds\n        if is_valid:\n            return self._try_redirect(response, redirect_to)\n\n        app_details_config = self._get_app_details_config()\n        response.status = http.HTTPStatus.OK\n        response.content_type = b'text/html'\n        response.body = ui.render_email_verification_page_link_flow(\n            verification_token=maybe_verification_token,\n            is_valid=is_valid,\n            error_messages=error_messages,\n            app_name=app_details_config.app_name,\n            logo_url=app_details_config.logo_url,\n            dark_logo_url=app_details_config.dark_logo_url,\n            brand_color=app_details_config.brand_color,\n        )\n\n    async def handle_ui_resend_verification(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        query = urllib.parse.parse_qs(\n            request.url.query.decode(\"ascii\") if request.url.query else \"\"\n        )\n        ui_config = self._get_ui_config()\n        password_provider = (\n            self._get_password_provider() if ui_config is not None else None\n        )\n        is_valid = True\n\n        if password_provider is None:\n            response.status = http.HTTPStatus.NOT_FOUND\n            response.body = b'Password provider not configured'\n            return\n        try:\n            _check_keyset(query, {\"verification_token\"})\n            verification_token = query[\"verification_token\"][0]\n            token = jwt.VerificationToken.verify(\n                verification_token,\n                self.signing_key,\n                skip_expiration_check=True,\n            )\n            email_password_client = email_password.Client(self.db)\n            email_factor = (\n                await email_password_client.get_email_factor_by_identity_id(\n                    token.subject\n                )\n            )\n            if email_factor is None:\n                raise errors.NoIdentityFound(\n                    \"Could not find email for provided identity\"\n                )\n\n            verify_url = f\"{self.base_path}/ui/verify\"\n            verification_token = self._make_verification_token(\n                identity_id=token.subject,\n                verify_url=verify_url,\n                maybe_challenge=token.maybe_challenge,\n                maybe_redirect_to=token.maybe_redirect_to,\n            )\n\n            await self._send_verification_email(\n                provider=password_provider.name,\n                verification_token=verification_token,\n                to_addr=email_factor.email,\n                verify_url=verify_url,\n            )\n        except Exception:\n            is_valid = False\n\n        app_details_config = self._get_app_details_config()\n        response.status = http.HTTPStatus.OK\n        response.content_type = b\"text/html\"\n        response.body = ui.render_resend_verification_done_page(\n            is_valid=is_valid,\n            verification_token=_maybe_get_search_param(\n                query, \"verification_token\"\n            ),\n            app_name=app_details_config.app_name,\n            logo_url=app_details_config.logo_url,\n            dark_logo_url=app_details_config.dark_logo_url,\n            brand_color=app_details_config.brand_color,\n        )\n\n    async def handle_ui_magic_link_sent(\n        self,\n        request: protocol.HttpRequest,\n        response: protocol.HttpResponse,\n    ) -> None:\n        ui_config = self._get_ui_config()\n        if ui_config is None:\n            response.status = http.HTTPStatus.NOT_FOUND\n            response.body = b'Auth UI not enabled'\n            return\n        app_details = self._get_app_details_config()\n\n        query = urllib.parse.parse_qs(\n            request.url.query.decode(\"ascii\") if request.url.query else \"\"\n        )\n\n        # Use magic link provider to decide display mode\n        magic_link_provider = None\n        providers = util.maybe_get_config(\n            self.db,\n            \"ext::auth::AuthConfig::providers\",\n            frozenset,\n        )\n        if providers is not None:\n            for p in providers:\n                if getattr(p, 'name', None) == 'builtin::local_magic_link':\n                    magic_link_provider = p\n                    break\n\n        is_code_method = (\n            getattr(magic_link_provider, 'verification_method', 'Link')\n            == 'Code'\n        )\n\n        if is_code_method:\n            is_signup = _maybe_get_search_param(query, \"signup\") == \"true\"\n            email = _get_search_param(query, \"email\")\n            challenge = _get_pkce_challenge(\n                cookies=request.cookies, response=response, query_dict=query\n            )\n            if challenge is None:\n                response.status = http.HTTPStatus.BAD_REQUEST\n                response.body = b'Missing challenge from cookie or query params'\n                return\n\n            error_message = _maybe_get_search_param(query, \"error\")\n            callback_url = ui_config.redirect_to\n            if is_signup and ui_config.redirect_to_on_signup:\n                callback_url = ui_config.redirect_to_on_signup\n\n            content = ui.render_magic_link_sent_page_code_flow(\n                app_name=app_details.app_name,\n                logo_url=app_details.logo_url,\n                dark_logo_url=app_details.dark_logo_url,\n                brand_color=app_details.brand_color,\n                email=email,\n                challenge=challenge,\n                callback_url=callback_url,\n                error_message=error_message,\n            )\n        else:\n            content = ui.render_magic_link_sent_page_link_flow(\n                app_name=app_details.app_name,\n                logo_url=app_details.logo_url,\n                dark_logo_url=app_details.dark_logo_url,\n                brand_color=app_details.brand_color,\n            )\n\n        response.status = http.HTTPStatus.OK\n        response.content_type = b\"text/html\"\n        response.body = content\n\n    def _get_webhook_config(self) -> list[config.WebhookConfig]:\n        raw_webhook_configs = util.get_config(\n            self.db,\n            \"ext::auth::AuthConfig::webhooks\",\n            frozenset,\n        )\n        return [\n            config.WebhookConfig(\n                events=raw_config.events,\n                url=raw_config.url,\n                signing_secret_key=raw_config.signing_secret_key,\n            )\n            for raw_config in raw_webhook_configs\n        ]\n\n    async def _maybe_send_webhook(self, event: webhook.Event) -> None:\n        webhook_configs = self._get_webhook_config()\n        for webhook_config in webhook_configs:\n            if event.event_type in webhook_config.events:\n                request_id = await webhook.send(\n                    db=self.db,\n                    url=webhook_config.url,\n                    secret=webhook_config.signing_secret_key,\n                    event=event,\n                )\n                logger.info(\n                    f\"Sent webhook request {request_id} \"\n                    f\"to {webhook_config.url} for event {event!r}\"\n                )\n\n    async def _handle_otc_initiated(\n        self,\n        identity_id: str,\n        email_factor_id: str,\n        otc_id: str,\n        one_time_code: str,\n    ) -> None:\n        metrics.otc_initiated_total.inc(1.0, self.tenant.get_instance_name())\n\n        await self._maybe_send_webhook(\n            webhook.OneTimeCodeRequested(\n                event_id=str(uuid.uuid4()),\n                timestamp=datetime.datetime.now(datetime.timezone.utc),\n                identity_id=identity_id,\n                email_factor_id=email_factor_id,\n                otc_id=str(otc_id),\n                one_time_code=one_time_code,\n            )\n        )\n\n        logger.info(\n            f\"OTC initiated: identity_id={identity_id}, otc_id={otc_id}\"\n        )\n\n    async def _handle_otc_verified(\n        self, identity_id: str, email_factor_id: str, otc_id: str\n    ) -> None:\n        metrics.otc_verified_total.inc(1.0, self.tenant.get_instance_name())\n\n        await self._maybe_send_webhook(\n            webhook.OneTimeCodeVerified(\n                event_id=str(uuid.uuid4()),\n                timestamp=datetime.datetime.now(datetime.timezone.utc),\n                identity_id=identity_id,\n                email_factor_id=email_factor_id,\n                otc_id=str(otc_id),\n            )\n        )\n\n        logger.info(f\"OTC verified: identity_id={identity_id}, otc_id={otc_id}\")\n\n    def _handle_otc_failed(self, ex: Exception) -> None:\n        match ex:\n            case errors.OTCRateLimited():\n                failure_reason = \"rate_limited\"\n            case errors.OTCInvalidCode():\n                failure_reason = \"invalid_code\"\n            case errors.OTCExpired():\n                failure_reason = \"expired\"\n            case errors.OTCVerificationFailed():\n                failure_reason = \"verification_failed\"\n            case _:\n                failure_reason = \"unknown\"\n\n        metrics.otc_failed_total.inc(\n            1.0, self.tenant.get_instance_name(), failure_reason\n        )\n\n        logger.info(f\"OTC verification failed: reason={failure_reason}\")\n\n    def _get_callback_url(self) -> str:\n        return f\"{self.base_path}/callback\"\n\n    def _get_data_from_request(\n        self,\n        request: protocol.HttpRequest,\n    ) -> dict[Any, Any]:\n        content_type = request.content_type\n        match content_type:\n            case b\"application/x-www-form-urlencoded\":\n                return {\n                    k: v[0]\n                    for k, v in urllib.parse.parse_qs(\n                        request.body.decode('ascii')\n                    ).items()\n                }\n            case b\"application/json\":\n                data = json.loads(request.body)\n                if not isinstance(data, dict):\n                    raise errors.InvalidData(\n                        f\"Invalid json data, expected an object\"\n                    )\n                return data\n            case _:\n                raise errors.InvalidData(\n                    f\"Unsupported Content-Type: {content_type!r}\"\n                )\n\n    def _get_ui_config(self) -> config.UIConfig:\n        return cast(\n            config.UIConfig,\n            util.maybe_get_config(\n                self.db, \"ext::auth::AuthConfig::ui\", CompositeConfigType\n            ),\n        )\n\n    def _get_app_details_config(self) -> config.AppDetailsConfig:\n        return util.get_app_details_config(self.db)\n\n    def _get_password_provider(\n        self,\n    ) -> Optional[config.EmailPasswordProviderConfig]:\n        providers = cast(\n            list[config.ProviderConfig],\n            util.get_config(\n                self.db,\n                \"ext::auth::AuthConfig::providers\",\n                frozenset,\n            ),\n        )\n        password_providers = [\n            cast(config.EmailPasswordProviderConfig, p)\n            for p in providers\n            if (p.name == 'builtin::local_emailpassword')\n        ]\n\n        return password_providers[0] if len(password_providers) == 1 else None\n\n    def _get_webauthn_provider(self) -> config.WebAuthnProvider | None:\n        providers = cast(\n            list[config.ProviderConfig],\n            util.get_config(\n                self.db,\n                \"ext::auth::AuthConfig::providers\",\n                frozenset,\n            ),\n        )\n        webauthn_providers = cast(\n            list[config.WebAuthnProviderConfig],\n            [p for p in providers if (p.name == 'builtin::local_webauthn')],\n        )\n\n        if len(webauthn_providers) == 1:\n            provider = webauthn_providers[0]\n            return config.WebAuthnProvider(\n                name=provider.name,\n                relying_party_origin=provider.relying_party_origin,\n                require_verification=provider.require_verification,\n                verification_method=provider.verification_method,\n            )\n        else:\n            return None\n\n    def _make_verification_token(\n        self,\n        identity_id: str,\n        verify_url: str,\n        maybe_challenge: str | None,\n        maybe_redirect_to: str | None,\n        *,\n        maybe_provider: str | None = None,\n    ) -> str:\n        if not self._is_url_allowed(verify_url):\n            raise errors.InvalidData(\n                \"Verify URL does not match any allowed URLs.\",\n            )\n\n        return jwt.VerificationToken(\n            subject=identity_id,\n            verify_url=verify_url,\n            maybe_challenge=maybe_challenge,\n            maybe_redirect_to=maybe_redirect_to,\n        ).sign(self.signing_key)\n\n    async def _send_verification_email(\n        self,\n        *,\n        verification_token: str,\n        verify_url: str,\n        provider: str,\n        to_addr: str,\n    ) -> None:\n        client: email_password.Client | webauthn.Client | None = None\n        match provider:\n            case \"builtin::local_emailpassword\":\n                client = email_password.Client(db=self.db)\n            case \"builtin::local_webauthn\":\n                client = webauthn.Client(self.db)\n        if client is not None:\n            if client.config.verification_method == \"Code\":\n                email_factor = await client.get_email_factor_by_email(to_addr)\n                if email_factor is not None:\n                    code, otc_id = await otc.create(\n                        self.db,\n                        str(email_factor.id),\n                        datetime.timedelta(minutes=10),\n                    )\n                    await auth_emails.send_one_time_code_email(\n                        db=self.db,\n                        tenant=self.tenant,\n                        to_addr=to_addr,\n                        code=code,\n                        test_mode=self.test_mode,\n                    )\n\n                    await self._handle_otc_initiated(\n                        identity_id=email_factor.identity.id,\n                        email_factor_id=str(email_factor.id),\n                        otc_id=str(otc_id),\n                        one_time_code=code,\n                    )\n\n                    logger.info(\n                        \"Sent OTC verification email: \"\n                        f\"email={to_addr}, otc_id={otc_id}\"\n                    )\n                    return\n\n        await auth_emails.send_verification_email(\n            db=self.db,\n            tenant=self.tenant,\n            to_addr=to_addr,\n            verification_token=verification_token,\n            provider=provider,\n            verify_url=verify_url,\n            test_mode=self.test_mode,\n        )\n\n    async def _try_verify_email(\n        self, provider: str, identity_id: str\n    ) -> EmailFactor:\n        current_time = datetime.datetime.now(datetime.timezone.utc)\n\n        client: email_password.Client | webauthn.Client\n        match provider:\n            case \"builtin::local_emailpassword\":\n                client = email_password.Client(db=self.db)\n            case \"builtin::local_webauthn\":\n                client = webauthn.Client(self.db)\n            case _:\n                raise errors.InvalidData(\n                    f\"Unknown provider: {provider}\",\n                )\n\n        updated = await client.verify_email(identity_id, current_time)\n        if updated is None:\n            raise errors.NoIdentityFound(\n                \"Could not verify email for identity\"\n                f\" {identity_id}. This email address may not exist\"\n                \" in our system, or it might already be verified.\"\n            )\n        return updated\n\n    def _is_url_allowed(self, url: str) -> bool:\n        allowed_urls = util.get_config(\n            self.db,\n            \"ext::auth::AuthConfig::allowed_redirect_urls\",\n            frozenset,\n        )\n        allowed_urls = cast(frozenset[str], allowed_urls).union(\n            {self.base_path}\n        )\n\n        ui_config = self._get_ui_config()\n        if ui_config:\n            allowed_urls = allowed_urls.union({ui_config.redirect_to})\n            if ui_config.redirect_to_on_signup:\n                allowed_urls = allowed_urls.union(\n                    {ui_config.redirect_to_on_signup}\n                )\n\n        lower_url = url.lower()\n\n        for allowed_url in allowed_urls:\n            lower_allowed_url = allowed_url.lower()\n            if lower_url.startswith(lower_allowed_url):\n                return True\n\n            parsed_allowed_url = urllib.parse.urlparse(lower_allowed_url)\n            allowed_domain = parsed_allowed_url.netloc\n            allowed_path = parsed_allowed_url.path\n\n            parsed_lower_url = urllib.parse.urlparse(lower_url)\n            lower_domain = parsed_lower_url.netloc\n            lower_path = parsed_lower_url.path\n\n            if (\n                lower_domain == allowed_domain\n                or lower_domain.endswith('.' + allowed_domain)\n            ) and lower_path.startswith(allowed_path):\n                return True\n\n        return False\n\n    def _do_redirect(\n        self, response: protocol.HttpResponse, allowed_url: AllowedUrl\n    ) -> None:\n        response.status = http.HTTPStatus.FOUND\n        response.custom_headers[\"Location\"] = allowed_url.url\n\n    def _try_redirect(self, response: protocol.HttpResponse, url: str) -> None:\n        allowed_url = self._make_allowed_url(url)\n        self._do_redirect(response, allowed_url)\n\n    def _make_allowed_url(self, url: str) -> AllowedUrl:\n        if not self._is_url_allowed(url):\n            raise errors.InvalidData(\n                \"Redirect URL does not match any allowed URLs.\",\n            )\n        return AllowedUrl(url)\n\n    def _maybe_make_allowed_url(\n        self, url: Optional[str]\n    ) -> Optional[AllowedUrl]:\n        return self._make_allowed_url(url) if url else None\n\n\n@dataclasses.dataclass\nclass AllowedUrl:\n    url: str\n\n    def map(self, f: Callable[[str], str]) -> \"AllowedUrl\":\n        return AllowedUrl(f(self.url))\n\n\ndef _fail_with_error(\n    *,\n    response: protocol.HttpResponse,\n    status: http.HTTPStatus,\n    ex: Exception,\n    exc_info: bool = False,\n) -> None:\n    err_dct = {\n        \"message\": str(ex),\n        \"type\": str(ex.__class__.__name__),\n    }\n\n    logger.error(\n        f\"Failed to handle HTTP request: {err_dct!r}\", exc_info=exc_info\n    )\n    response.body = json.dumps({\"error\": err_dct}).encode()\n    response.status = status\n\n\ndef _maybe_get_search_param(\n    query_dict: dict[str, list[str]],\n    key: str,\n    *,\n    fallback_keys: Optional[list[str]] = None,\n) -> str | None:\n    params = query_dict.get(key)\n    if params is None and fallback_keys is not None:\n        for fallback_key in fallback_keys:\n            params = query_dict.get(fallback_key)\n            if params is not None:\n                break\n    return params[0] if params else None\n\n\ndef _get_search_param(\n    query_dict: dict[str, list[str]],\n    key: str,\n    *,\n    fallback_keys: Optional[list[str]] = None,\n) -> str:\n    val = _maybe_get_search_param(query_dict, key, fallback_keys=fallback_keys)\n    if val is None:\n        raise errors.InvalidData(f\"Missing query parameter: {key}\")\n    return val\n\n\ndef _maybe_get_form_field(\n    form_dict: dict[str, list[str]], key: str\n) -> str | None:\n    maybe_val = form_dict.get(key)\n    if maybe_val is None:\n        return None\n    return maybe_val[0]\n\n\ndef _get_pkce_challenge(\n    *,\n    response: protocol.HttpResponse,\n    cookies: http.cookies.SimpleCookie,\n    query_dict: dict[str, list[str]],\n) -> str | None:\n    cookie_name = 'edgedb-pkce-challenge'\n    challenge: str | None = _maybe_get_search_param(query_dict, 'challenge')\n    if challenge is not None:\n        logger.info(\n            f\"PKCE challenge found in query param 'challenge': {challenge!r}\"\n        )\n    else:\n        challenge = _maybe_get_search_param(query_dict, \"code_challenge\")\n        if challenge is not None:\n            logger.info(\n                \"PKCE challenge found in query param 'code_challenge':\"\n                f\" {challenge!r}\"\n            )\n    if challenge is not None:\n        _set_cookie(response, cookie_name, challenge)\n    else:\n        if 'edgedb-pkce-challenge' in cookies:\n            challenge = cookies['edgedb-pkce-challenge'].value\n            logger.info(f\"PKCE challenge found in cookie: {challenge!r}\")\n        else:\n            logger.info(\"No PKCE challenge found in query params or cookies.\")\n            logger.info(f\"Query params: {query_dict}\")\n            logger.info(f\"Cookies: {cookies}\")\n    return challenge\n\n\ndef _set_cookie(\n    response: protocol.HttpResponse,\n    name: str,\n    value: str,\n    *,\n    http_only: bool = True,\n    secure: bool = True,\n    same_site: str = \"Strict\",\n    path: Optional[str] = None,\n) -> None:\n    val: http.cookies.Morsel[str] = http.cookies.SimpleCookie({name: value})[\n        name\n    ]\n    val[\"httponly\"] = http_only\n    val[\"secure\"] = secure\n    val[\"samesite\"] = same_site\n    if path is not None:\n        val[\"path\"] = path\n    response.custom_headers[\"Set-Cookie\"] = val.OutputString()\n\n\ndef _check_keyset(candidate: dict[str, Any], keyset: set[str]) -> None:\n    missing_fields = [field for field in keyset if field not in candidate]\n    if missing_fields:\n        raise errors.InvalidData(\n            f\"Missing required fields: {', '.join(missing_fields)}\"\n        )\n\n\ndef _accepts_json(request: protocol.HttpRequest) -> bool:\n    return request.accept == b\"application/json\"\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/jwt.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport dataclasses\nimport datetime\n\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.kdf.hkdf import HKDFExpand\nfrom cryptography.hazmat.backends import default_backend\n\nfrom typing import Any, Callable\n\nfrom edb.server import auth as jwt_auth\nfrom edb.ir import statypes as statypes\n\nfrom . import errors\n\n\nVALIDATION_TOKEN_DEFAULT_EXPIRATION = datetime.timedelta(seconds=24 * 60 * 60)\nRESET_TOKEN_DEFAULT_EXPIRATION = datetime.timedelta(minutes=10)\nOAUTH_STATE_TOKEN_DEFAULT_EXPIRATION = datetime.timedelta(minutes=5)\n\n\nclass SigningKey:\n    subkeys: dict[str | None, jwt_auth.JWKSet]\n\n    def __init__(\n        self,\n        key_fetch: Callable[[], str],\n        issuer: str,\n        *,\n        is_key_for_testing: bool = False,\n    ):\n        self.key = \"\"\n        self.key_fetch = key_fetch\n        self.issuer = issuer\n        self.subkeys = {}\n        self.__is_key_for_testing = is_key_for_testing\n\n    def subkey(self, info: str | None = None) -> jwt_auth.JWKSet:\n        # Clear keycache if the key has changed\n        current_key = self.key_fetch()\n        if current_key != self.key:\n            self.key = current_key\n            self.subkeys = {}\n\n        if info in self.subkeys:\n            return self.subkeys[info]\n        if info is None:\n            key = jwt_auth.JWKSet.from_hs256_key(self.key.encode())\n        else:\n            key = jwt_auth.JWKSet.from_hs256_key(derive_key_raw(self.key, info))\n        key.default_validation_context.require_expiry()\n        key.default_validation_context.allow(\"iss\", [self.issuer])\n        self.subkeys[info] = key\n        return key\n\n    def sign(\n        self,\n        info: str | None,\n        claims: dict[str, str | None],\n        *,\n        ctx: jwt_auth.SigningCtx,\n    ) -> str:\n        # Remove any None values from the claims\n        claims = {k: v for k, v in claims.items() if v is not None}\n        if self.__is_key_for_testing:\n            claims[\"__test__\"] = str(info)\n        return self.subkey(info).sign(claims, ctx=ctx)\n\n    def validate(\n        self,\n        token: str,\n        info: str | None = None,\n        skip_expiration_check: bool = False,\n    ) -> dict[str, Any]:\n        key = self.subkey(info)\n\n        try:\n            ctx = None\n            if skip_expiration_check:\n                ctx = jwt_auth.ValidationCtx()\n                ctx.ignore_expiry()\n                ctx.allow(\"iss\", [self.issuer])\n            return key.validate(token, ctx=ctx)\n        except Exception as e:\n            raise errors.InvalidData(f\"Invalid token: {e}\") from e\n\n\ndef verify_str(cls: Any, claims: dict[str, Any], key: str) -> str:\n    value = claims.get(key, None)\n    if isinstance(value, str):\n        return value\n    raise errors.InvalidData(f\"Invalid '{cls.__name__}'\")\n\n\ndef verify_str_opt(cls: Any, claims: dict[str, Any], key: str) -> str | None:\n    value = claims.get(key, None)\n    if isinstance(value, str):\n        return value\n    if value is None:\n        return None\n    raise errors.InvalidData(f\"Invalid '{cls.__name__}'\")\n\n\n@dataclasses.dataclass\nclass MagicLinkToken:\n    \"\"\"\n    A token that can be used to verify a magic link sent to a user via email.\n\n    Expiration is controlled by the provider parameter `token_time_to_live`.\n    \"\"\"\n    subject: str\n    callback_url: str\n    challenge: str\n\n    def sign(\n        self, signing_key: SigningKey, expires_in: datetime.timedelta\n    ) -> str:\n        signing_ctx = jwt_auth.SigningCtx()\n        signing_ctx.set_expiry(int(expires_in.total_seconds()))\n        signing_ctx.set_not_before(30)\n        signing_ctx.set_issuer(signing_key.issuer)\n        return signing_key.sign(\n            \"magic_link\",\n            {\n                \"sub\": self.subject,\n                \"callback_url\": self.callback_url,\n                \"challenge\": self.challenge,\n            },\n            ctx=signing_ctx,\n        )\n\n    @classmethod\n    def verify(cls, token: str, signing_key: SigningKey) -> 'MagicLinkToken':\n        claims = signing_key.validate(token, \"magic_link\")\n\n        identity_id = verify_str(cls, claims, 'sub')\n        challenge = verify_str(cls, claims, 'challenge')\n        callback_url = verify_str(cls, claims, 'callback_url')\n\n        return MagicLinkToken(\n            subject=identity_id,\n            callback_url=callback_url,\n            challenge=challenge,\n        )\n\n\n@dataclasses.dataclass\nclass ResetToken:\n    \"\"\"\n    A token that can be used to verify a password reset request.\n    \"\"\"\n    subject: str\n    secret: str\n    challenge: str\n\n    def sign(\n        self,\n        signing_key: SigningKey,\n        expires_in: datetime.timedelta = RESET_TOKEN_DEFAULT_EXPIRATION,\n    ) -> str:\n        signing_ctx = jwt_auth.SigningCtx()\n        signing_ctx.set_expiry(int(expires_in.total_seconds()))\n        signing_ctx.set_not_before(30)\n        signing_ctx.set_issuer(signing_key.issuer)\n        return signing_key.sign(\n            \"reset\",\n            {\n                \"sub\": self.subject,\n                \"secret\": self.secret,\n                \"challenge\": self.challenge,\n            },\n            ctx=signing_ctx,\n        )\n\n    @classmethod\n    def verify(cls, token: str, signing_key: SigningKey) -> 'ResetToken':\n        claims = signing_key.validate(token, \"reset\")\n\n        return ResetToken(\n            subject=verify_str(cls, claims, 'sub'),\n            secret=verify_str(cls, claims, 'secret'),\n            challenge=verify_str(cls, claims, 'challenge'),\n        )\n\n\n@dataclasses.dataclass\nclass VerificationToken:\n    \"\"\"\n    A token that can be used to verify a user's email address. Note that we\n    allow expired tokens to trigger a resend of the verification email, but not\n    to verify the email address.\n    \"\"\"\n    subject: str\n    verify_url: str\n    maybe_challenge: str | None\n    maybe_redirect_to: str | None\n\n    def sign(\n        self,\n        signing_key: SigningKey,\n        expires_in: datetime.timedelta = VALIDATION_TOKEN_DEFAULT_EXPIRATION,\n    ) -> str:\n        signing_ctx = jwt_auth.SigningCtx()\n        signing_ctx.set_expiry(int(expires_in.total_seconds()))\n        signing_ctx.set_not_before(30)\n        signing_ctx.set_issuer(signing_key.issuer)\n        return signing_key.sign(\n            \"verification\",\n            {\n                \"sub\": self.subject,\n                \"verify_url\": self.verify_url,\n                \"challenge\": self.maybe_challenge,\n                \"redirect_to\": self.maybe_redirect_to,\n            },\n            ctx=signing_ctx,\n        )\n\n    @classmethod\n    def verify(\n        cls,\n        token: str,\n        signing_key: SigningKey,\n        skip_expiration_check: bool = False,\n    ) -> 'VerificationToken':\n        claims = signing_key.validate(\n            token,\n            \"verification\",\n            skip_expiration_check=skip_expiration_check,\n        )\n\n        return VerificationToken(\n            subject=verify_str(cls, claims, 'sub'),\n            verify_url=verify_str(cls, claims, 'verify_url'),\n            maybe_challenge=verify_str_opt(cls, claims, 'challenge'),\n            maybe_redirect_to=verify_str_opt(cls, claims, 'redirect_to'),\n        )\n\n\n@dataclasses.dataclass\nclass SessionToken:\n    \"\"\"\n    The token representing an auth session for a user. Expiration is controlled\n    by the database parameter `ext::auth::AuthConfig::token_time_to_live`.\n    \"\"\"\n    subject: str\n\n    def sign(\n        self,\n        signing_key: SigningKey,\n        expires_in: datetime.timedelta,\n    ) -> str:\n        signing_ctx = jwt_auth.SigningCtx()\n        signing_ctx.set_expiry(int(expires_in.total_seconds()))\n        signing_ctx.set_not_before(30)\n        signing_ctx.set_issuer(signing_key.issuer)\n        return signing_key.sign(\n            None,\n            {\n                \"sub\": self.subject,\n            },\n            ctx=signing_ctx,\n        )\n\n    @classmethod\n    def verify(cls, token: str, signing_key: SigningKey) -> 'SessionToken':\n        claims = signing_key.validate(token, None)\n\n        return SessionToken(\n            subject=verify_str(cls, claims, 'sub'),\n        )\n\n\n@dataclasses.dataclass\nclass OAuthStateToken:\n    \"\"\"\n    The token representing an OAuth state passed to the identity provider. It\n    allows the auth extension server to reference data from the original\n    authorize request, such as the provider, application redirect URLs, PKCE\n    challenge, and OAuth callback URL.\n    \"\"\"\n    provider: str\n    redirect_to: str\n    challenge: str\n    redirect_uri: str\n    redirect_to_on_signup: str | None = None\n\n    def sign(\n        self,\n        signing_key: SigningKey,\n        expires_in: datetime.timedelta = OAUTH_STATE_TOKEN_DEFAULT_EXPIRATION,\n    ) -> str:\n        signing_ctx = jwt_auth.SigningCtx()\n        signing_ctx.set_expiry(int(expires_in.total_seconds()))\n        signing_ctx.set_not_before(30)\n        signing_ctx.set_issuer(signing_key.issuer)\n        return signing_key.sign(\n            \"state\",\n            {\n                \"provider\": self.provider,\n                \"redirect_to\": self.redirect_to,\n                \"redirect_to_on_signup\": self.redirect_to_on_signup,\n                \"challenge\": self.challenge,\n                \"redirect_uri\": self.redirect_uri,\n            },\n            ctx=signing_ctx,\n        )\n\n    @classmethod\n    def verify(cls, token: str, signing_key: SigningKey) -> 'OAuthStateToken':\n        claims = signing_key.validate(token, \"state\")\n\n        return OAuthStateToken(\n            provider=verify_str(cls, claims, 'provider'),\n            redirect_to=verify_str(cls, claims, 'redirect_to'),\n            redirect_to_on_signup=verify_str_opt(\n                cls, claims, 'redirect_to_on_signup'\n            ),\n            challenge=verify_str(cls, claims, 'challenge'),\n            redirect_uri=verify_str(cls, claims, 'redirect_uri'),\n        )\n\n\ndef derive_key_raw(key: str, info: str) -> bytes:\n    \"\"\"Derive a new key from the given symmetric key using HKDF.\"\"\"\n    input_key_material = key.encode()\n\n    backend = default_backend()\n    hkdf = HKDFExpand(\n        algorithm=hashes.SHA256(),\n        length=32,\n        info=info.encode(\"utf-8\"),\n        backend=backend,\n    )\n    new_key_bytes = hkdf.derive(input_key_material)\n    return new_key_bytes\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/local.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport datetime\nimport json\n\nfrom typing import Any, cast\nfrom . import data, util\n\n\nclass Client:\n    def __init__(self, db: Any):\n        self.db = db\n\n    async def verify_email(\n        self, identity_id: str, verified_at: datetime.datetime\n    ) -> data.EmailFactor | None:\n        result_bytes = await util.json_query(\n            db=self.db,\n            query=\"\"\"\\\nwith\n    LOCAL_IDENTITY := <ext::auth::LocalIdentity><uuid>$identity_id,\n    verified_at := <datetime>$verified_at,\n    UPDATED := (\n        update ext::auth::EmailFactor\n        filter .identity = LOCAL_IDENTITY\n            and not exists .verified_at ?? false\n        set { verified_at := verified_at }\n    ),\nselect UPDATED {**};\n\"\"\",\n            variables={\n                \"identity_id\": identity_id,\n                \"verified_at\": verified_at.isoformat(),\n            },\n        )\n        result_json = json.loads(result_bytes.decode())\n        if len(result_json) == 0:\n            return None\n\n        factor = result_json[0]\n        return data.EmailFactor(**factor)\n\n    async def get_email_factor_by_identity_id(\n        self, identity_id: str\n    ) -> data.EmailFactor | None:\n        r = await util.json_query(\n            self.db,\n            \"\"\"\nselect ext::auth::EmailFactor { ** } filter .identity.id = <uuid>$identity_id;\n            \"\"\",\n            variables={\"identity_id\": identity_id},\n        )\n\n        result_json = json.loads(r.decode())\n        if len(result_json) == 0:\n            return None\n\n        assert len(result_json) == 1\n\n        factor = result_json[0]\n\n        return data.EmailFactor(**factor)\n\n    async def get_verified_by_identity_id(self, identity_id: str) -> str | None:\n        r = await util.json_query(\n            self.db,\n            \"\"\"\nselect ext::auth::EmailFactor {\n    verified_at,\n} filter .identity.id = <uuid>$identity_id;\n            \"\"\",\n            variables={\"identity_id\": identity_id},\n        )\n\n        result_json = json.loads(r.decode())\n        if len(result_json) == 0:\n            return None\n\n        assert len(result_json) == 1\n\n        return cast(str, result_json[0][\"verified_at\"])\n\n    async def get_identity_id_by_email(\n        self, email: str, *, factor_type: str = 'EmailFactor'\n    ) -> str | None:\n        r = await util.json_query(\n            self.db,\n            f\"\"\"\nwith\n    email := <str>$email,\n    identity := (\n        select ext::auth::LocalIdentity\n        filter .<identity[is ext::auth::{factor_type}].email = email\n    ),\nselect identity.id;\"\"\",\n            variables={\"email\": email},\n        )\n\n        result_json = json.loads(r.decode())\n        if len(result_json) == 0:\n            return None\n\n        assert len(result_json) == 1\n\n        return cast(str, result_json[0])\n\n    async def get_email_factor_by_email(\n        self, email: str\n    ) -> data.EmailFactor | None:\n        r = await util.json_query(\n            self.db,\n            \"\"\"\nselect ext::auth::EmailFactor { ** } filter .email = <str>$email;\n            \"\"\",\n            variables={\"email\": email},\n        )\n\n        result_json = json.loads(r.decode())\n        if len(result_json) == 0:\n            return None\n\n        assert len(result_json) == 1\n\n        factor = result_json[0]\n        return data.EmailFactor(**factor)\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/magic_link.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport logging\nimport aiosmtplib\nimport json\n\nfrom typing import Any, cast\n\nfrom edb import errors as edb_errors\nfrom edb.common import debug\n\nfrom . import config, data, errors, jwt, util, local, email as auth_emails\n\n\nlogger = logging.getLogger('edb.server')\n\n\nclass Client(local.Client):\n    def __init__(\n        self,\n        db: Any,\n        tenant: Any,\n        test_mode: bool,\n        issuer: str,\n        signing_key: jwt.SigningKey,\n    ):\n        super().__init__(db)\n        self.tenant = tenant\n        self.test_mode = test_mode\n        self.issuer = issuer\n        self.provider = self._get_provider()\n        self.signing_key = signing_key\n\n    def _get_provider(self) -> config.MagicLinkProviderConfig:\n        provider_name = \"builtin::local_magic_link\"\n        provider_client_config = cast(\n            list[config.ProviderConfig],\n            util.get_config(\n                self.db, \"ext::auth::AuthConfig::providers\", frozenset\n            ),\n        )\n        for cfg in provider_client_config:\n            if cfg.name == provider_name:\n                cfg = cast(config.MagicLinkProviderConfig, cfg)\n                return config.MagicLinkProviderConfig(\n                    name=cfg.name,\n                    token_time_to_live=cfg.token_time_to_live,\n                    verification_method=cfg.verification_method,\n                    auto_signup=cfg.auto_signup,\n                )\n\n        raise errors.MissingConfiguration(\n            provider_name, f\"Provider is not configured\"\n        )\n\n    async def register(self, email: str) -> data.EmailFactor:\n        try:\n            result = await util.json_query(\n                self.db,\n                \"\"\"\nwith\n    email := <str>$email,\n    identity := (insert ext::auth::LocalIdentity {\n        issuer := \"local\",\n        subject := \"\",\n    }),\n    email_factor := (insert ext::auth::MagicLinkFactor {\n        email := email,\n        identity := identity,\n    })\nselect email_factor { ** };\"\"\",\n                variables={\n                    \"email\": email,\n                },\n            )\n\n        except edb_errors.ConstraintViolationError:\n            raise errors.UserAlreadyRegistered()\n\n        result_json = json.loads(result.decode())\n        assert len(result_json) == 1\n        factor_dict = result_json[0]\n        return data.EmailFactor(**factor_dict)\n\n    def make_magic_link_token(\n        self,\n        *,\n        identity_id: str,\n        callback_url: str,\n        challenge: str,\n    ) -> str:\n        return jwt.MagicLinkToken(\n            subject=identity_id,\n            callback_url=callback_url,\n            challenge=challenge,\n        ).sign(\n            self.signing_key,\n            expires_in=self.provider.token_time_to_live.to_timedelta(),\n        )\n\n    async def send_magic_link(\n        self,\n        *,\n        email: str,\n        link_url: str,\n        token: str,\n        redirect_on_failure: str,\n    ) -> None:\n        link = util.join_url_params(\n            link_url,\n            {\n                \"token\": token,\n                \"redirect_on_failure\": redirect_on_failure,\n            },\n        )\n        try:\n            await auth_emails.send_magic_link_email(\n                db=self.db,\n                tenant=self.tenant,\n                to_addr=email,\n                link=link,\n                test_mode=self.test_mode,\n            )\n        except aiosmtplib.SMTPException as ex:\n            if not debug.flags.server:\n                logger.warning(\n                    \"Failed to send magic link via SMTP\", exc_info=True\n                )\n            raise edb_errors.InternalServerError(\n                \"Failed to send magic link email, please try again later.\"\n            ) from ex\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/oauth.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport json\n\nfrom typing import cast, Any, Callable\nfrom edb.server.http import HttpClient\n\nfrom . import github, google, azure, apple, discord, slack\nfrom . import config, errors, util, data, base\n\n\nclass Client:\n    provider: base.BaseProvider\n\n    def __init__(\n        self,\n        *,\n        db: Any,\n        provider_name: str,\n        http_client: HttpClient,\n        url_munger: Callable[[str], str] | None = None,\n    ):\n        self.db = db\n\n        http_factory = lambda *args, **kwargs: http_client.with_context(\n            *args, url_munger=url_munger, **kwargs\n        )\n\n        provider_config = self._get_provider_config(provider_name)\n        provider_args: tuple[str, str] | tuple[str, str, str, str] = (\n            provider_config.client_id,\n            provider_config.secret,\n        )\n        provider_kwargs = {\n            \"http_factory\": http_factory,\n            \"additional_scope\": provider_config.additional_scope,\n        }\n\n        match (provider_name, provider_config.issuer_url):\n            case (\"builtin::oauth_github\", _):\n                self.provider = github.GitHubProvider(\n                    *provider_args,\n                    **provider_kwargs,\n                )\n            case (\"builtin::oauth_google\", _):\n                self.provider = google.GoogleProvider(\n                    *provider_args,\n                    **provider_kwargs,\n                )\n            case (\"builtin::oauth_azure\", _):\n                self.provider = azure.AzureProvider(\n                    *provider_args,\n                    **provider_kwargs,\n                )\n            case (\"builtin::oauth_apple\", _):\n                self.provider = apple.AppleProvider(\n                    *provider_args,\n                    **provider_kwargs,\n                )\n            case (\"builtin::oauth_discord\", _) if isinstance(\n                provider_config, config.DiscordOAuthProviderConfig\n            ):\n                self.provider = discord.DiscordProvider(\n                    provider_config.prompt,\n                    *provider_args,\n                    **provider_kwargs,\n                )\n            case (\"builtin::oauth_slack\", _):\n                self.provider = slack.SlackProvider(\n                    *provider_args,\n                    **provider_kwargs,\n                )\n            case (provider_name, str(issuer_url)):\n                self.provider = base.OpenIDConnectProvider(\n                    provider_name,\n                    issuer_url,\n                    *provider_args,\n                    **provider_kwargs,\n                )\n            case _:\n                raise errors.InvalidData(f\"Invalid provider: {provider_name}\")\n\n    async def get_authorize_url(self, state: str, redirect_uri: str) -> str:\n        return await self.provider.get_code_url(\n            state=state,\n            redirect_uri=redirect_uri,\n            additional_scope=self.provider.additional_scope or \"\",\n        )\n\n    async def handle_callback(\n        self, code: str, redirect_uri: str\n    ) -> tuple[data.Identity, bool, str | None, str | None, str | None]:\n        response = await self.provider.exchange_code(code, redirect_uri)\n        user_info = await self.provider.fetch_user_info(response)\n        auth_token = response.access_token\n        refresh_token = response.refresh_token\n        source_id_token = user_info.source_id_token\n\n        return (\n            *(await self._handle_identity(user_info)),\n            auth_token,\n            refresh_token,\n            source_id_token,\n        )\n\n    async def _handle_identity(\n        self, user_info: data.UserInfo\n    ) -> tuple[data.Identity, bool]:\n        \"\"\"Update or create an identity\"\"\"\n\n        r = await util.json_query(\n            db=self.db,\n            query=\"\"\"\\\nwith\n  iss := <str>$issuer_url,\n  sub := <str>$subject,\n  identity := (\n    insert ext::auth::Identity {\n      issuer := iss,\n      subject := sub,\n    } unless conflict on ((.issuer, .subject))\n      else ext::auth::Identity\n  )\nselect {\n  identity := (select identity {*}),\n  new := (identity not in ext::auth::Identity)\n};\"\"\",\n            variables={\n                \"issuer_url\": self.provider.issuer_url,\n                \"subject\": user_info.sub,\n            },\n        )\n        result_json = json.loads(r.decode())\n        assert len(result_json) == 1\n\n        return (\n            data.Identity(**result_json[0]['identity']),\n            result_json[0]['new'],\n        )\n\n    def _get_provider_config(\n        self, provider_name: str\n    ) -> config.OAuthProviderConfig | config.DiscordOAuthProviderConfig:\n        provider_client_config = util.get_config(\n            self.db, \"ext::auth::AuthConfig::providers\", frozenset\n        )\n        for cfg in provider_client_config:\n            if cfg.name == provider_name:\n                cfg = cast(config.OAuthProviderConfig, cfg)\n                if provider_name == \"builtin::oauth_discord\":\n                    return config.DiscordOAuthProviderConfig(\n                        name=cfg.name,\n                        display_name=cfg.display_name,\n                        client_id=cfg.client_id,\n                        secret=cfg.secret,\n                        additional_scope=getattr(cfg, \"additional_scope\", None),\n                        issuer_url=getattr(cfg, \"issuer_url\", None),\n                        logo_url=getattr(cfg, \"logo_url\", None),\n                        prompt=getattr(cfg, \"prompt\", \"consent\"),\n                    )\n                return config.OAuthProviderConfig(\n                    name=cfg.name,\n                    display_name=cfg.display_name,\n                    client_id=cfg.client_id,\n                    secret=cfg.secret,\n                    additional_scope=getattr(cfg, \"additional_scope\", None),\n                    issuer_url=getattr(cfg, \"issuer_url\", None),\n                    logo_url=getattr(cfg, \"logo_url\", None),\n                )\n        raise errors.MissingConfiguration(\n            provider_name, \"Provider is not configured\"\n        )\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/otc.py",
    "content": "#\n# This source file is part of the Gel open source project.\n#\n# Copyright 2025-present MagicStack Inc. and the Gel authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport hashlib\nimport json\nimport logging\nimport secrets\nimport uuid\nimport datetime\nfrom typing import Any\n\nfrom edb.server import defines\n\nfrom . import errors, util\n\nlogger = logging.getLogger(__name__)\n\nMAX_ATTEMPTS = 5\n\n\ndef generate_code() -> str:\n    return f\"{secrets.randbelow(1000000):06d}\"\n\n\ndef hash_code(code: str) -> bytes:\n    return hashlib.sha256(code.encode('utf-8')).digest()\n\n\nasync def create(\n    db: Any, factor_id: str, ttl: datetime.timedelta\n) -> tuple[str, uuid.UUID]:\n    \"\"\"\n    Create a new OneTimeCode object in the database.\n\n    Args:\n        db: Database connection\n        factor_id: The ID of the factor to associate with this code\n        ttl: Time to live for the code\n\n    Returns:\n        Tuple of (code, otc_id) where code is the plain text code and otc_id is\n        the database ID\n    \"\"\"\n    code = generate_code()\n    code_hash = hash_code(code)\n    expires_at = datetime.datetime.now(datetime.timezone.utc) + ttl\n\n    r = await util.json_query(\n        db=db,\n        query=\"\"\"\\\nwith\n    ONE_TIME_CODE := (insert ext::auth::OneTimeCode {\n        factor := <ext::auth::Factor><uuid>$factor_id,\n        code_hash := <bytes>$code_hash,\n        expires_at := <datetime>$expires_at,\n    })\nselect ONE_TIME_CODE { id };\"\"\",\n        variables={\n            \"factor_id\": factor_id,\n            \"code_hash\": code_hash,\n            \"expires_at\": expires_at.isoformat(),\n        },\n    )\n\n    result_json = json.loads(r.decode())\n    if len(result_json) != 1:\n        raise errors.InvalidData(\"Failed to create OneTimeCode\")\n\n    return code, uuid.UUID(result_json[0][\"id\"])\n\n\nasync def verify(db: Any, factor_id: str, code: str) -> str:\n    \"\"\"\n    Verify a one-time code for a given factor.\n\n    This function performs all verification operations in a single atomic query:\n    - Cleanup expired codes\n    - Check rate limits\n    - Find and validate the code\n    - Delete the code if valid\n    - Record the authentication attempt\n\n    Args:\n        db: Database connection\n        factor_id: The ID of the factor\n        code: The code to verify\n\n    Returns:\n        The OneTimeCode ID if verification succeeds\n\n    Raises:\n        OTCRateLimited: If maximum verification attempts exceeded\n        OTCInvalidCode: If the code is invalid or not found\n        OTCExpired: If the code has expired\n        OTCVerificationFailed: If verification fails for other reasons\n    \"\"\"\n    code_hash = hash_code(code)\n\n    # N.B: I (sully) don't want to make this RepeatableRead because I\n    # worry that it would allow bypassing MAX_ATTEMPTS by performing\n    # many requests \"at the same time\". We need to analyze this before\n    # we change it.\n    r = await util.json_query(\n        db=db,\n        query=\"\"\"\\\nwith\n    factor_id := <uuid>$factor_id,\n    FACTOR := (select ext::auth::Factor filter .id = factor_id),\n    code_hash := <bytes>$code_hash,\n    MAX_ATTEMPTS := <int16>$max_attempts,\n    now := datetime_current(),\n    window_start := now - <duration>'10 minutes',\n\n    # Check rate limits\n    failed_attempts := (\n        select count(\n            select ext::auth::AuthenticationAttempt\n            filter .factor = FACTOR\n               and .attempt_type =\n                   ext::auth::AuthenticationAttemptType.OneTimeCode\n               and .successful = false\n               and .created_at > window_start\n        )\n    ),\n\n    # Find the OTC\n    otc := (\n        select ext::auth::OneTimeCode\n        filter .factor = FACTOR and .code_hash = code_hash\n        limit 1\n    ),\n\n    is_rate_limited := failed_attempts >= MAX_ATTEMPTS,\n    is_code_found := exists otc,\n    is_code_expired := (otc.expires_at < now) ?? false,\n    is_code_valid := (\n        is_code_found and not is_code_expired and not is_rate_limited\n    ),\n\n    # Delete OTC if valid (side effect)\n    deleted_otc := (\n        delete ext::auth::OneTimeCode\n        filter .id = otc.id and is_code_valid\n    ),\n\n    # Record attempt (side effect)\n    recorded_attempt := (\n        if (exists FACTOR)\n        then (\n            insert ext::auth::AuthenticationAttempt {\n                factor := FACTOR,\n                attempt_type :=\n                    ext::auth::AuthenticationAttemptType.OneTimeCode,\n                successful := is_code_valid,\n            }\n        )\n        else {}\n    ),\n\nselect {\n    failed_attempts := failed_attempts,\n    success := is_code_valid,\n    rate_limited := is_rate_limited,\n    code_found := is_code_found,\n    code_expired := is_code_expired,\n    otc_id := otc.id,\n};\"\"\",\n        variables={\n            \"factor_id\": factor_id,\n            \"code_hash\": code_hash,\n            \"max_attempts\": MAX_ATTEMPTS,\n        },\n    )\n\n    result_json = json.loads(r.decode())\n    result = result_json[0]\n\n    # Run an OTC GC. We don't really mind if it fails due to\n    # serialization problems.\n    try:\n        await util.json_query(\n            db=db,\n            query=\"\"\"\\\nwith\n    now := datetime_current(),\n\n# Cleanup expired codes\nselect count(\n    delete ext::auth::OneTimeCode filter .expires_at < now\n)\n            \"\"\",\n            tx_isolation=defines.TxIsolationLevel.RepeatableRead,\n        )\n    except Exception:\n        pass\n\n    if result[\"rate_limited\"]:\n        raise errors.OTCRateLimited()\n    elif not result[\"code_found\"]:\n        raise errors.OTCInvalidCode()\n    elif result[\"code_expired\"]:\n        raise errors.OTCExpired()\n    elif not result[\"success\"]:\n        raise errors.OTCVerificationFailed()\n\n    return str(result[\"otc_id\"])\n\n\nasync def cleanup_old_attempts(db: Any, retention_hours: int = 24) -> int:\n    \"\"\"\n    Remove authentication attempts older than the retention window.\n\n    This is intended for scheduled maintenance jobs to prevent unbounded\n    growth of the authentication attempts table.\n\n    Args:\n        db: Database connection\n        retention_hours: Hours to retain attempts (defaults to 24)\n\n    Returns:\n        Number of old attempts that were deleted\n    \"\"\"\n    r = await util.json_query(\n        db=db,\n        query=\"\"\"\\\nwith\n    cutoff_time := datetime_current() - <duration>$retention_duration,\n    old_attempts := (\n        delete ext::auth::AuthenticationAttempt\n        filter .created_at < cutoff_time\n    )\nselect count(old_attempts);\"\"\",\n        variables={\n            \"retention_duration\": f\"{retention_hours} hours\",\n        },\n    )\n\n    result_json = json.loads(r.decode())\n    return result_json[0] if result_json else 0\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/pkce.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nimport typing\n\nimport asyncio\nimport json\nimport logging\nimport dataclasses\n\nfrom edb.ir import statypes\nfrom edb.server import defines\n\nfrom . import util, errors\n\nif typing.TYPE_CHECKING:\n    from edb.server import server as edbserver\n    from edb.server import tenant as edbtenant\n\n\nlogger = logging.getLogger(\"edb.server\")\nVALIDITY = statypes.Duration.from_microseconds(10 * 60_000_000)  # 10 minutes\n\n\n@dataclasses.dataclass(repr=False)\nclass PKCEChallenge:\n    \"\"\"\n    Object that represents the ext::auth::PKCEChallenge type\n    \"\"\"\n\n    id: str\n    challenge: str\n    auth_token: str | None\n    refresh_token: str | None\n    id_token: str | None\n    identity_id: str | None\n\n\nasync def create(db: edbtenant.dbview.Database, challenge: str) -> None:\n    await util.json_query(\n        db,\n        \"\"\"\n        insert ext::auth::PKCEChallenge {\n            challenge := <str>$challenge,\n        } unless conflict on .challenge\n        else (select ext::auth::PKCEChallenge)\n        \"\"\",\n        variables={\n            \"challenge\": challenge,\n        },\n    )\n\n\nasync def link_identity_challenge(\n    db: edbtenant.dbview.Database,\n    identity_id: str,\n    challenge: str,\n) -> str:\n    r = await util.json_query(\n        db,\n        \"\"\"\n        update ext::auth::PKCEChallenge\n        filter .challenge = <str>$challenge\n        set { identity := <ext::auth::Identity><uuid>$identity_id }\n        \"\"\",\n        variables={\n            \"challenge\": challenge,\n            \"identity_id\": identity_id,\n        },\n    )\n\n    result_json = json.loads(r.decode())\n    if len(result_json) != 1:\n        raise errors.PKCEVerificationFailed(\n            f\"No linked PKCE session found for challenge '{challenge}'\"\n        )\n\n    return typing.cast(str, result_json[0][\"id\"])\n\n\nasync def add_provider_tokens(\n    db: edbtenant.dbview.Database,\n    id: str,\n    auth_token: str | None,\n    refresh_token: str | None,\n    id_token: str | None,\n) -> str:\n    r = await util.json_query(\n        db,\n        \"\"\"\n        update ext::auth::PKCEChallenge\n        filter .id = <uuid>$id\n        set {\n            auth_token := <optional str>$auth_token,\n            refresh_token := <optional str>$refresh_token,\n            id_token := <optional str>$id_token,\n        }\n        \"\"\",\n        variables={\n            \"id\": id,\n            \"auth_token\": auth_token,\n            \"refresh_token\": refresh_token,\n            \"id_token\": id_token,\n        },\n    )\n\n    result_json = json.loads(r.decode())\n    if len(result_json) != 1:\n        raise errors.PKCEVerificationFailed(\n            f\"No PKCE session found with id '{id}'\"\n        )\n\n    return typing.cast(str, result_json[0][\"id\"])\n\n\nasync def get_by_id(db: edbtenant.dbview.Database, id: str) -> PKCEChallenge:\n    r = await util.json_query(\n        db,\n        \"\"\"\n        select ext::auth::PKCEChallenge {\n            id,\n            challenge,\n            auth_token,\n            refresh_token,\n            id_token,\n            identity_id := .identity.id\n        }\n        filter .id = <uuid>$id\n        and (datetime_current() - .created_at) < <duration>$validity;\n        \"\"\",\n        variables={\"id\": id, \"validity\": VALIDITY.to_backend_str()},\n    )\n\n    result_json = json.loads(r.decode())\n    if len(result_json) != 1:\n        raise errors.PKCEVerificationFailed(\n            f\"No current PKCE session found with id '{id}'\"\n        )\n\n    return PKCEChallenge(**result_json[0])\n\n\nasync def delete(db: edbtenant.dbview.Database, id: str) -> None:\n    r = await util.json_query(\n        db,\n        \"\"\"\n        delete ext::auth::PKCEChallenge filter .id = <uuid>$id\n        \"\"\",\n        variables={\"id\": id},\n    )\n\n    result_json = json.loads(r.decode())\n    if len(result_json) != 1:\n        raise errors.PKCEVerificationFailed(\n            f\"No PKCE session found with id '{id}'\"\n        )\n\n\nasync def _delete_challenge(db: edbtenant.dbview.Database) -> None:\n    if not db.tenant.is_database_connectable(db.name):\n        # Don't run gc if the database is not connectable, e.g. being dropped\n        return\n\n    await util.json_query(\n        db,\n        \"\"\"\n        delete ext::auth::PKCEChallenge filter\n            (datetime_of_statement() - .created_at) >\n            <duration>$validity\n        \"\"\",\n        variables={\"validity\": VALIDITY.to_backend_str()},\n        tx_isolation=defines.TxIsolationLevel.RepeatableRead,\n    )\n\n\nasync def _gc(tenant: edbtenant.Tenant) -> None:\n    try:\n        async with asyncio.TaskGroup() as g:\n            for db in tenant.iter_dbs():\n                if \"auth\" in db.extensions:\n                    g.create_task(_delete_challenge(db))\n    except Exception as ex:\n        logger.debug(\n            \"GC of ext::auth::PKCEChallenge failed (instance: %s)\",\n            tenant.get_instance_name(),\n            exc_info=ex,\n        )\n\n\nasync def gc(server: edbserver.BaseServer) -> None:\n    while True:\n        try:\n            tasks = [\n                tenant.create_task(_gc(tenant), interruptable=True)\n                for tenant in server.iter_tenants()\n                if tenant.accept_new_tasks\n            ]\n            if tasks:\n                await asyncio.wait(tasks)\n        except Exception as ex:\n            logger.debug(\"GC of ext::auth::PKCEChallenge failed\", exc_info=ex)\n        finally:\n            await asyncio.sleep(VALIDITY.to_microseconds() / 1_000_000.0)\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/slack.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom typing import Any\n\nfrom . import base\n\n\nclass SlackProvider(base.OpenIDConnectProvider):\n    def __init__(self, *args: Any, **kwargs: Any):\n        super().__init__(\n            \"slack\",\n            \"https://slack.com\",\n            *args,\n            **kwargs,\n        )\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/ui/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import cast, Optional\n\nimport html\nimport email.message\n\nfrom edb.server.protocol.auth_ext import config as auth_config\n\nfrom . import components as render\n\n\ndef render_signin_page(\n    *,\n    base_path: str,\n    providers: frozenset[auth_config.ProviderConfig],\n    error_message: Optional[str] = None,\n    email: Optional[str] = None,\n    challenge: str,\n    selected_tab: Optional[str] = None,\n    # config\n    redirect_to: str,\n    redirect_to_on_signup: Optional[str] = None,\n    app_name: Optional[str] = None,\n    logo_url: Optional[str] = None,\n    dark_logo_url: Optional[str] = None,\n    brand_color: Optional[str] = None,\n) -> bytes:\n    password_provider = None\n    webauthn_provider = None\n    magic_link_provider = None\n    oauth_providers = []\n    for p in providers:\n        if p.name == 'builtin::local_emailpassword':\n            password_provider = cast(auth_config.EmailPasswordProviderConfig, p)\n        elif p.name == 'builtin::local_webauthn':\n            webauthn_provider = cast(auth_config.WebAuthnProviderConfig, p)\n        elif p.name == 'builtin::local_magic_link':\n            magic_link_provider = cast(auth_config.MagicLinkProviderConfig, p)\n        elif p.name.startswith('builtin::oauth_') or hasattr(p, \"issuer_url\"):\n            oauth_providers.append(cast(auth_config.OAuthProviderConfig, p))\n\n    email_factor_form = render_email_factor_form(\n        challenge=challenge,\n        email=email,\n        selected_tab=selected_tab,\n        single_form_fields=f'''\n            {\n            render.hidden_input(\n                name='redirect_to',\n                value=(\n                    redirect_to\n                    if webauthn_provider\n                    else (base_path + '/ui/magic-link-sent')\n                ),\n                secondary_value=redirect_to,\n            )\n        }\n            {\n            render.hidden_input(\n                name='redirect_on_failure',\n                value=f'{base_path}/ui/signin',\n                secondary_value=f'{base_path}/ui/signin?selected_tab=password',\n            )\n        }\n            {\n            render.hidden_input(\n                name='provider',\n                value=magic_link_provider.name if magic_link_provider else '',\n                secondary_value=(\n                    password_provider.name if password_provider else ''\n                ),\n            )\n        }\n            {\n            render.hidden_input(name='callback_url', value=redirect_to)\n            if magic_link_provider\n            else ''\n        }\n        ''',\n        password_form=(\n            render.render_password_form(\n                challenge=challenge,\n                email=email,\n                redirect_to=redirect_to,\n                base_path=base_path,\n                provider_name=password_provider.name,\n            )\n            if password_provider\n            else None\n        ),\n        webauthn_form=(\n            render.render_webauthn_form(\n                challenge=challenge,\n                email=email,\n                redirect_to=redirect_to,\n                base_path=base_path,\n                provider_name=webauthn_provider.name,\n            )\n            if webauthn_provider\n            else None\n        ),\n        magic_link_form=(\n            render.render_magic_link_form(\n                challenge=challenge,\n                email=email,\n                callback_url=(\n                    redirect_to\n                    if magic_link_provider.verification_method == \"Link\"\n                    else None\n                ),\n                base_path=base_path,\n                provider_name=magic_link_provider.name,\n                verification_method=magic_link_provider.verification_method,\n            )\n            if magic_link_provider\n            else None\n        ),\n        magic_link_verification_method=(\n            magic_link_provider.verification_method\n            if magic_link_provider\n            else 'Link'\n        ),\n    )\n\n    if email_factor_form:\n        email_factor_form += render.bottom_note(\n            \"Don't have an account?\", link='Sign up', href='signup'\n        )\n\n    oauth_buttons = render.oauth_buttons(\n        oauth_providers=oauth_providers,\n        label_prefix=('Sign in with' if email_factor_form else 'Continue with'),\n        challenge=challenge,\n        redirect_to=redirect_to,\n        redirect_to_on_signup=redirect_to_on_signup,\n        collapsed=email_factor_form is not None and len(oauth_providers) >= 3,\n    )\n\n    return render.base_page(\n        title=f'Sign in{f\" to {app_name}\" if app_name else \"\"}',\n        logo_url=logo_url,\n        dark_logo_url=dark_logo_url,\n        brand_color=brand_color,\n        cleanup_search_params=['error', 'email', 'selected_tab'],\n        content=f'''\n          {render.title('Sign in', app_name=app_name)}\n          {render.error_message(error_message)}\n          {oauth_buttons}\n          {\n            render.divider\n            if email_factor_form and len(oauth_providers) > 0\n            else ''\n        }\n          {email_factor_form or ''}\n          {render.script('webauthn-authenticate') if webauthn_provider else ''}\n        ''',\n    )\n\n\ndef render_email_factor_form(\n    *,\n    selected_tab: Optional[str] = None,\n    single_form_fields: str = '',\n    password_form: Optional[str],\n    webauthn_form: Optional[str],\n    magic_link_form: Optional[str],\n    magic_link_verification_method: str = \"Link\",\n    # used only for slider mode\n    challenge: Optional[str] = None,\n    email: Optional[str] = None,\n) -> Optional[str]:\n    match (password_form, webauthn_form, magic_link_form):\n        case (None, None, None):\n            return None\n        case (_, None, None):\n            return password_form\n        case (None, _, None):\n            return webauthn_form\n        case (None, None, _):\n            return magic_link_form\n\n    magic_link_tab_label = render.get_magic_link_tab_label(\n        magic_link_verification_method\n    )\n    magic_link_button_text = render.get_magic_link_button_text(\n        magic_link_verification_method\n    )\n\n    # Determine whether to render tabs (multiple distinct forms) or the\n    # single-form slider (quick factor + password) UI.\n    # Slider is shown only when there is exactly one of webauthn/magic-link\n    # available AND a password form, since it relies on a shared email input.\n    has_password = password_form is not None\n    has_webauthn = webauthn_form is not None\n    has_magic_link = magic_link_form is not None\n    has_single_quick_factor = has_webauthn ^ has_magic_link\n\n    should_render_slider = (\n        has_password and has_single_quick_factor and challenge is not None\n    )\n\n    if not should_render_slider or (has_webauthn and has_magic_link):\n        tabs = [\n            (\n                ('Passkey', webauthn_form, selected_tab == 'webauthn')\n                if webauthn_form\n                else None\n            ),\n            (\n                ('Password', password_form, selected_tab == 'password')\n                if password_form\n                else None\n            ),\n            (\n                (\n                    magic_link_tab_label,\n                    magic_link_form,\n                    selected_tab == 'magic_link',\n                )\n                if magic_link_form\n                else None\n            ),\n        ]\n\n        selected_tabs = [t[2] for t in tabs if t is not None]\n        selected_index = (\n            selected_tabs.index(True) if True in selected_tabs else 0\n        )\n\n        labels = [t[0] for t in tabs if t is not None]\n        sections = [t[1] for t in tabs if t is not None]\n        return render.tabs_buttons(\n            labels, selected_index\n        ) + render.tabs_content(sections, selected_index, labels)\n\n    # Build slider content for the single-form flow.\n    base_email_factor_form = render.render_base_email_form(\n        id=\"email\", challenge=challenge or \"\", email=email\n    )\n    password_input = render.render_password_input(\n        challenge=challenge or \"\", should_show_forgot_password=True\n    )\n\n    slider_content = [\n        f'''\n            {\n            render.button(\"Sign In\", id=\"webauthn-signin\")\n            if webauthn_form\n            else render.button(magic_link_button_text, id=\"magic-link-signin\")\n        }\n            {\n            render.button(\n                \"Sign in with password\",\n                id=\"show-password-form\",\n                secondary=True,\n                type=\"button\",\n            )\n        }\n        ''',\n        f'''\n            {password_input}\n            <div class=\"button-group\">\n                {\n            render.button(\n                None, id=\"hide-password-form\", secondary=True, type=\"button\"\n            )\n        }\n                {render.button(\"Sign in with password\", id=\"password-signin\")}\n            </div>\n        ''',\n    ]\n\n    return f\"\"\"\n    <form id=\"email-factor\" method=\"post\" {\n        'action=\"../magic-link/email\"' if magic_link_form else ''\n    } data-secondary-action=\"../authenticate\" novalidate>\n        {single_form_fields}\n        {base_email_factor_form}\n        {\n        render.tabs_content(\n            slider_content,\n            selected_tab=(1 if selected_tab == 'password' else 0),\n        )\n    }\n    </form>\n    \"\"\"\n\n\ndef render_signup_page(\n    *,\n    base_path: str,\n    providers: frozenset[auth_config.ProviderConfig],\n    error_message: Optional[str] = None,\n    email: Optional[str] = None,\n    challenge: str,\n    selected_tab: Optional[str] = None,\n    # config\n    redirect_to: str,\n    redirect_to_on_signup: Optional[str] = None,\n    app_name: Optional[str] = None,\n    logo_url: Optional[str] = None,\n    dark_logo_url: Optional[str] = None,\n    brand_color: Optional[str] = None,\n) -> bytes:\n    password_provider = None\n    webauthn_provider = None\n    magic_link_provider = None\n    oauth_providers = []\n    for p in providers:\n        if p.name == 'builtin::local_emailpassword':\n            password_provider = cast(auth_config.EmailPasswordProviderConfig, p)\n        elif p.name == 'builtin::local_webauthn':\n            webauthn_provider = cast(auth_config.WebAuthnProviderConfig, p)\n        elif p.name == 'builtin::local_magic_link':\n            magic_link_provider = cast(auth_config.MagicLinkProviderConfig, p)\n        elif p.name.startswith('builtin::oauth_') or hasattr(p, \"issuer_url\"):\n            oauth_providers.append(cast(auth_config.OAuthProviderConfig, p))\n\n    email_factor_form = render_email_factor_form(\n        selected_tab=selected_tab,\n        password_form=(\n            render.render_password_signup_form(\n                challenge=challenge,\n                email=email,\n                redirect_to=render.get_email_password_signup_redirect_url(\n                    password_provider.verification_method,\n                    base_path,\n                    redirect_to_on_signup or redirect_to,\n                ),\n                base_path=base_path,\n                provider_name=password_provider.name,\n            )\n            if password_provider\n            else None\n        ),\n        webauthn_form=(\n            render.render_webauthn_signup_form(\n                challenge=challenge,\n                email=email,\n                redirect_to=render.get_webauthn_signup_redirect_url(\n                    webauthn_provider.verification_method,\n                    base_path,\n                    redirect_to_on_signup or redirect_to,\n                ),\n                base_path=base_path,\n                provider_name=webauthn_provider.name,\n            )\n            if webauthn_provider\n            else None\n        ),\n        magic_link_form=(\n            render.render_magic_link_signup_form(\n                challenge=challenge,\n                email=email,\n                callback_url=redirect_to_on_signup or redirect_to,\n                base_path=base_path,\n                provider_name=magic_link_provider.name,\n                verification_method=magic_link_provider.verification_method,\n            )\n            if magic_link_provider\n            else None\n        ),\n        magic_link_verification_method=(\n            magic_link_provider.verification_method\n            if magic_link_provider\n            else 'Link'\n        ),\n    )\n\n    if email_factor_form:\n        email_factor_form += render.bottom_note(\n            'Already have an account?', link='Sign in', href='signin'\n        )\n\n    oauth_buttons = render.oauth_buttons(\n        oauth_providers=oauth_providers,\n        label_prefix=('Sign up with' if email_factor_form else 'Continue with'),\n        challenge=challenge,\n        redirect_to=redirect_to,\n        redirect_to_on_signup=redirect_to_on_signup,\n        collapsed=email_factor_form is not None and len(oauth_providers) >= 3,\n    )\n\n    return render.base_page(\n        title=f'Sign up{f\" to {app_name}\" if app_name else \"\"}',\n        logo_url=logo_url,\n        dark_logo_url=dark_logo_url,\n        brand_color=brand_color,\n        cleanup_search_params=['error', 'email', 'selected_tab'],\n        content=f'''\n            {render.title('Sign up', app_name=app_name)}\n            {render.error_message(error_message)}\n            {oauth_buttons}\n            {\n            render.divider\n            if email_factor_form and len(oauth_providers) > 0\n            else ''\n        }\n            {email_factor_form or ''}\n            {render.script('webauthn-register') if webauthn_provider else ''}\n        ''',\n    )\n\n\ndef render_forgot_password_page(\n    *,\n    redirect_to: str,\n    redirect_on_failure: str,\n    reset_url: str,\n    provider_name: str,\n    challenge: str,\n    error_message: Optional[str] = None,\n    email: Optional[str] = None,\n    email_sent: Optional[str] = None,\n    # config\n    app_name: Optional[str] = None,\n    logo_url: Optional[str] = None,\n    dark_logo_url: Optional[str] = None,\n    brand_color: Optional[str] = None,\n) -> bytes:\n    if email_sent is not None:\n        content = render.success_message(\n            f'Password reset email has been sent to <b>{email_sent}</b>'\n        )\n    else:\n        content = f'''\n        {render.error_message(error_message)}\n\n        <form method=\"POST\" action=\"../send-reset-email\">\n          <input type=\"hidden\" name=\"provider\" value=\"{provider_name}\" />\n          <input type=\"hidden\" name=\"challenge\" value=\"{challenge}\" />\n          <input\n            type=\"hidden\"\n            name=\"redirect_on_failure\"\n            value=\"{redirect_on_failure}\"\n          />\n          <input type=\"hidden\" name=\"redirect_to\" value=\"{redirect_to}\" />\n          <input type=\"hidden\" name=\"reset_url\" value=\"{reset_url}\" />\n\n          <label for=\"email\">Email</label>\n          <input id=\"email\" name=\"email\" type=\"email\" value=\"{email or ''}\" />\n\n          {render.button('Send Reset Email')}\n        </form>\n        '''\n\n    return render.base_page(\n        title=f'Reset password{f\" for {app_name}\" if app_name else \"\"}',\n        logo_url=logo_url,\n        dark_logo_url=dark_logo_url,\n        brand_color=brand_color,\n        cleanup_search_params=['error', 'email', 'email_sent'],\n        content=f'''\n            {render.title('Reset password', join='for', app_name=app_name)}\n            {content}\n            {render.bottom_note(\"Back to\", link=\"Sign In\", href=\"signin\")}\n        ''',\n    )\n\n\ndef render_reset_password_page(\n    *,\n    base_path: str,\n    provider_name: str,\n    is_valid: bool,\n    redirect_to: str,\n    challenge: str,\n    reset_token: Optional[str] = None,\n    error_message: Optional[str] = None,\n    is_code_flow: bool = False,\n    email: Optional[str] = None,\n    # config\n    app_name: Optional[str] = None,\n    logo_url: Optional[str] = None,\n    dark_logo_url: Optional[str] = None,\n    brand_color: Optional[str] = None,\n) -> bytes:\n    if not is_valid:\n        content = render.error_message(\n            f'''Reset token is invalid, it may have expired.\n            <a href=\"forgot-password?challenge={challenge}\">\n              Try sending another reset email\n            </a>''',\n            False,\n        )\n    elif is_code_flow and email:\n        content = f'''\n            {render.error_message(error_message)}\n            <p>We've sent a 6-digit reset code to <strong>{\n            html.escape(email)\n        }</strong></p>\n            {\n            render.code_input_form(\n                action=\"../reset-password\",\n                email=email,\n                provider=provider_name,\n                label=\"Enter reset code\",\n                button_text=\"Reset Password\",\n                additional_fields=f'''\n                    <input\n                        type=\"hidden\"\n                        name=\"redirect_to\"\n                        value=\"{redirect_to}\"\n                    />\n                    <input\n                        type=\"hidden\"\n                        name=\"redirect_on_failure\"\n                        value=\"{base_path}/ui/reset-password\"\n                    />\n                    <input type=\"hidden\" name=\"challenge\" value=\"{challenge}\" />\n                    <label for=\"password\">New Password</label>\n                    <input\n                        id=\"password\"\n                        name=\"password\"\n                        type=\"password\"\n                        required\n                    />\n                ''',\n            )\n        }\n        '''\n    else:\n        content = f'''\n        {render.error_message(error_message)}\n\n        <form method=\"POST\" action=\"../reset-password\">\n          <input type=\"hidden\" name=\"provider\" value=\"{provider_name}\" />\n          <input type=\"hidden\" name=\"reset_token\" value=\"{reset_token}\" />\n          <input type=\"hidden\" name=\"redirect_on_failure\" value=\"{\n            base_path\n        }/ui/reset-password\" />\n          <input type=\"hidden\" name=\"redirect_to\" value=\"{redirect_to}\" />\n\n          <label for=\"password\">New Password</label>\n          <input id=\"password\" name=\"password\" type=\"password\" />\n\n          {render.button('Reset Password')}\n        </form>'''\n\n    return render.base_page(\n        title=f'Reset password{f\" for {app_name}\" if app_name else \"\"}',\n        logo_url=logo_url,\n        dark_logo_url=dark_logo_url,\n        brand_color=brand_color,\n        cleanup_search_params=['error'],\n        content=f'''\n            {render.title('Reset password', join='for', app_name=app_name)}\n            {content}\n        ''',\n    )\n\n\ndef render_email_verification_page_code_flow(\n    *,\n    email: str,\n    provider: str,\n    callback_url: str,\n    base_path: str,\n    challenge: str,\n    error_message: Optional[str] = None,\n    # config\n    app_name: Optional[str] = None,\n    logo_url: Optional[str] = None,\n    dark_logo_url: Optional[str] = None,\n    brand_color: Optional[str] = None,\n) -> bytes:\n    \"\"\"Renders verification page that handles both link and code flows.\"\"\"\n\n    content = f'''\n        {render.error_message(error_message)}\n        <p>We've sent a 6-digit verification code to <strong>{\n        html.escape(email)\n    }</strong></p>\n        {\n        render.code_input_form(\n            action=\"../verify\",\n            email=email,\n            provider=provider,\n            label=\"Enter verification code\",\n            button_text=\"Verify Email\",\n            additional_fields=f'''\n                <input\n                    type=\"hidden\"\n                    name=\"redirect_to\"\n                    value=\"{callback_url}\"\n                />\n                <input\n                    type=\"hidden\"\n                    name=\"redirect_on_failure\"\n                    value=\"{base_path}/ui/verify\"\n                />\n                <input\n                    type=\"hidden\"\n                    name=\"challenge\"\n                    value=\"{challenge}\"\n                />\n            '''\n        )\n    }\n    '''\n\n    return render.base_page(\n        title=f'Verify email{f\" for {app_name}\" if app_name else \"\"}',\n        logo_url=logo_url,\n        dark_logo_url=dark_logo_url,\n        brand_color=brand_color,\n        cleanup_search_params=['error'],\n        content=f'''\n            {render.title('Verify email', join='for', app_name=app_name)}\n            {content}\n        ''',\n    )\n\n\ndef render_email_verification_page_link_flow(\n    *,\n    is_valid: bool,\n    error_messages: list[str],\n    verification_token: Optional[str] = None,\n    # config\n    app_name: Optional[str] = None,\n    logo_url: Optional[str] = None,\n    dark_logo_url: Optional[str] = None,\n    brand_color: Optional[str] = None,\n) -> bytes:\n    resend_url = None\n    if verification_token:\n        verification_token = html.escape(verification_token)\n        resend_url = (\n            f\"resend-verification?verification_token={verification_token}\"\n        )\n    if not is_valid:\n        messages = ''.join(\n            [render.error_message(error) for error in error_messages]\n        )\n        content = f'''\n            {messages}\n            {\n            (\n                f'<a href=\"{resend_url}\">Try sending another verification'\n                'email</a>'\n            )\n            if resend_url\n            else ''\n        }\n            '''\n    else:\n        content = '''\n        Email has been successfully verified. You may now\n        <a href=\"signin\">sign in</a>\n        '''\n\n    return render.base_page(\n        title=f'Verify email{f\" for {app_name}\" if app_name else \"\"}',\n        logo_url=logo_url,\n        dark_logo_url=dark_logo_url,\n        brand_color=brand_color,\n        cleanup_search_params=['error'],\n        content=f'''\n            {render.title('Verify email', join='for', app_name=app_name)}\n            {content}\n        ''',\n    )\n\n\ndef render_email_verification_expired_page(\n    verification_token: str,\n    # config\n    app_name: Optional[str] = None,\n    logo_url: Optional[str] = None,\n    dark_logo_url: Optional[str] = None,\n    brand_color: Optional[str] = None,\n) -> bytes:\n    verification_token = html.escape(verification_token)\n    content = render.error_message(\n        f'''\n        Your verification token has expired.\n        <a href=\"resend-verification?verification_token={verification_token}\">\n            Click here to resend the verification email\n        </a>\n        ''',\n        False,\n    )\n\n    return render.base_page(\n        title=f'Verification expired{f\" for {app_name}\" if app_name else \"\"}',\n        logo_url=logo_url,\n        dark_logo_url=dark_logo_url,\n        brand_color=brand_color,\n        cleanup_search_params=['error'],\n        content=f'''\n            {\n            render.title('Verification expired', join='for', app_name=app_name)\n        }\n            {content}\n        ''',\n    )\n\n\ndef render_resend_verification_done_page(\n    *,\n    is_valid: bool,\n    verification_token: Optional[str] = None,\n    # config\n    app_name: Optional[str] = None,\n    logo_url: Optional[str] = None,\n    dark_logo_url: Optional[str] = None,\n    brand_color: Optional[str] = None,\n) -> bytes:\n    if verification_token is None:\n        content = render.error_message(\n            f\"\"\"\n            Missing verification token, please follow the link provided in the\n            original email, or on the signin page.\n            \"\"\",\n            False,\n        )\n    else:\n        verification_token = html.escape(verification_token)\n        if is_valid:\n            content = f'''\n            Your verification email has been resent. Please check your email.\n            '''\n        else:\n            content = f'''\n            Unable to resend verification email. Please try again.\n            '''\n\n    return render.base_page(\n        title=(\n            f'Email verification resent{f\" for {app_name}\" if app_name else \"\"}'\n        ),\n        logo_url=logo_url,\n        dark_logo_url=dark_logo_url,\n        brand_color=brand_color,\n        cleanup_search_params=['error'],\n        content=f'''\n            {\n            render.title(\n                'Email verification resent', join='for', app_name=app_name\n            )\n        }\n            {content}\n        ''',\n    )\n\n\ndef render_magic_link_sent_page_code_flow(\n    *,\n    email: str,\n    challenge: str,\n    callback_url: str,\n    error_message: Optional[str] = None,\n    app_name: Optional[str] = None,\n    logo_url: Optional[str] = None,\n    dark_logo_url: Optional[str] = None,\n    brand_color: Optional[str] = None,\n) -> bytes:\n    content = f'''\n        {render.error_message(error_message)}\n        <p>We've sent a 6-digit sign-in code to <strong>{\n        html.escape(email)\n    }</strong></p>\n        {\n        render.code_input_form(\n            action=\"../magic-link/authenticate\",\n            email=email,\n            provider=\"builtin::local_magic_link\",\n            label=\"Enter sign-in code\",\n            button_text=\"Sign In\",\n            additional_fields=f'''\n                <input\n                    type=\"hidden\"\n                    name=\"callback_url\"\n                    value=\"{callback_url}\"\n                />\n                <input\n                    type=\"hidden\"\n                    name=\"challenge\"\n                    value=\"{challenge}\"\n                />\n            ''',\n        )\n    }\n    '''\n    title = f'Sign in code sent{f\" for {app_name}\" if app_name else \"\"}'\n    page_title = 'Sign in code sent'\n\n    return render.base_page(\n        title=title,\n        logo_url=logo_url,\n        dark_logo_url=dark_logo_url,\n        brand_color=brand_color,\n        cleanup_search_params=['error'],\n        content=f'''\n            {render.title(page_title, join='for', app_name=app_name)}\n            {content}\n        ''',\n    )\n\n\ndef render_magic_link_sent_page_link_flow(\n    *,\n    app_name: Optional[str] = None,\n    logo_url: Optional[str] = None,\n    dark_logo_url: Optional[str] = None,\n    brand_color: Optional[str] = None,\n) -> bytes:\n    content = render.success_message(\n        \"A sign in link has been sent to your email. Please check your \"\n        \"email.\"\n    )\n    title = f'Sign in link sent{f\" for {app_name}\" if app_name else \"\"}'\n    page_title = 'Sign in link sent'\n\n    return render.base_page(\n        title=title,\n        logo_url=logo_url,\n        dark_logo_url=dark_logo_url,\n        brand_color=brand_color,\n        cleanup_search_params=['error'],\n        content=f'''\n            {render.title(page_title, join='for', app_name=app_name)}\n            {content}\n        ''',\n    )\n\n\n# emails\n\n\ndef render_password_reset_email(\n    *,\n    to_addr: str,\n    reset_url: str,\n    app_name: Optional[str] = None,\n    logo_url: Optional[str] = None,\n    dark_logo_url: Optional[str] = None,\n    brand_color: Optional[str] = render.DEFAULT_BRAND_COLOR,\n) -> email.message.EmailMessage:\n    brand_color = brand_color or render.DEFAULT_BRAND_COLOR\n    msg = email.message.EmailMessage()\n    msg[\"To\"] = to_addr\n    msg[\"Subject\"] = \"Reset password\"\n    plain_text_content = f\"\"\"\nSomebody requested a new password for the {app_name or ''} account associated\nwith {to_addr}.\n\nPlease paste the following URL into your browser address bar to verify your\nemail address:\n\n{reset_url}\n        \"\"\"\n    html_content = f\"\"\"\n<tr>\n  <td\n    style=\"\n      direction: ltr;\n      font-size: 0px;\n      padding: 20px 0;\n      padding-bottom: 20px;\n      padding-top: 20px;\n      text-align: center;\n    \"\n  >\n    <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:middle;width:600px;\" ><![endif]-->\n    <div\n      class=\"mj-column-per-100 mj-outlook-group-fix\"\n      style=\"\n        font-size: 0px;\n        text-align: left;\n        direction: ltr;\n        display: inline-block;\n        vertical-align: middle;\n        width: 100%;\n      \"\n    >\n      <table\n        border=\"0\"\n        cellpadding=\"0\"\n        cellspacing=\"0\"\n        role=\"presentation\"\n        style=\"vertical-align: middle\"\n        width=\"100%\"\n      >\n        <tbody>\n          <tr>\n            <td\n              align=\"left\"\n              style=\"\n                font-size: 0px;\n                padding: 10px 25px;\n                padding-top: 50px;\n                word-break: break-word;\n              \"\n            >\n              <div\n                style=\"\n                  font-family: open Sans Helvetica, Arial, sans-serif;\n                  font-size: 16px;\n                  line-height: 1;\n                  text-align: left;\n                  color: #000000;\n                \"\n              >\n                Somebody requested a new password for the {app_name or ''}\n                account associated with {to_addr}.\n              </div>\n            </td>\n          </tr>\n          <tr>\n            <td\n              align=\"left\"\n              style=\"\n                font-size: 0px;\n                padding: 10px 25px;\n                word-break: break-word;\n              \"\n            >\n              <div\n                style=\"\n                  font-family: open Sans Helvetica, Arial, sans-serif;\n                  font-size: 16px;\n                  line-height: 1;\n                  text-align: left;\n                  color: #000000;\n                \"\n              >\n                No changes have been made to your account yet.\n              </div>\n            </td>\n          </tr>\n          <tr>\n            <td\n              align=\"left\"\n              style=\"\n                font-size: 0px;\n                padding: 10px 25px;\n                word-break: break-word;\n              \"\n            >\n              <div\n                style=\"\n                  font-family: open Sans Helvetica, Arial, sans-serif;\n                  font-size: 16px;\n                  line-height: 1;\n                  text-align: left;\n                  color: #000000;\n                \"\n              >\n                You can reset your password by clicking the button below:\n              </div>\n            </td>\n          </tr>\n          <tr>\n            <td\n              align=\"center\"\n              vertical-align=\"middle\"\n              style=\"\n                font-size: 0px;\n                padding: 10px 25px;\n                word-break: break-word;\n              \"\n            >\n              <table\n                border=\"0\"\n                cellpadding=\"0\"\n                cellspacing=\"0\"\n                role=\"presentation\"\n                style=\"border-collapse: separate; line-height: 100%\"\n              >\n                <tr>\n                  <td\n                    align=\"center\"\n                    bgcolor=\"#{brand_color}\"\n                    role=\"presentation\"\n                    style=\"\n                      border: none;\n                      border-radius: 4px;\n                      cursor: auto;\n                      mso-padding-alt: 10px 25px;\n                      background: #{brand_color};\n                    \"\n                    valign=\"middle\"\n                  >\n                    <a\n                      href=\"{reset_url}\"\n                      style=\"\n                        display: inline-block;\n                        background: #{brand_color};\n                        color: #ffffff;\n                        font-family: open Sans Helvetica, Arial, sans-serif;\n                        font-size: 18px;\n                        font-weight: bold;\n                        line-height: 120%;\n                        margin: 0;\n                        text-decoration: none;\n                        text-transform: none;\n                        padding: 10px 25px;\n                        mso-padding-alt: 0px;\n                        border-radius: 4px;\n                      \"\n                      target=\"_blank\"\n                    >\n                      Reset your password\n                    </a>\n                  </td>\n                </tr>\n              </table>\n            </td>\n          </tr>\n          <tr>\n            <td\n              align=\"left\"\n              style=\"\n                font-size: 0px;\n                padding: 10px 25px;\n                word-break: break-word;\n              \"\n            >\n              <div\n                style=\"\n                  font-family: open Sans Helvetica, Arial, sans-serif;\n                  font-size: 16px;\n                  line-height: 1;\n                  text-align: left;\n                  color: #000000;\n                \"\n              >\n                In case the button didn't work, please paste the following URL\n                into your browser address bar:\n                <p style=\"word-break: break-all\">{reset_url}</p>\n              </div>\n            </td>\n          </tr>\n          <tr>\n            <td\n              align=\"left\"\n              style=\"\n                font-size: 0px;\n                padding: 10px 25px;\n                word-break: break-word;\n              \"\n            >\n              <div\n                style=\"\n                  font-family: open Sans Helvetica, Arial, sans-serif;\n                  font-size: 16px;\n                  line-height: 1;\n                  text-align: left;\n                  color: #000000;\n                \"\n              >\n                If you did not request a new password, please let us know\n                immediately by replying to this email.\n              </div>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    </div>\n  </td>\n</tr>\n    \"\"\"  # noqa: E501\n\n    msg[\"X-gel-password-reset-url\"] = reset_url\n    msg.set_content(plain_text_content, subtype=\"plain\")\n    msg.add_alternative(\n        render.base_default_email(\n            content=html_content,\n            app_name=app_name,\n            logo_url=logo_url,\n        ),\n        subtype=\"html\",\n    )\n    return msg\n\n\ndef render_verification_email(\n    *,\n    to_addr: str,\n    verify_url: str,\n    app_name: Optional[str] = None,\n    logo_url: Optional[str] = None,\n    dark_logo_url: Optional[str] = None,\n    brand_color: Optional[str] = render.DEFAULT_BRAND_COLOR,\n) -> email.message.EmailMessage:\n    brand_color = brand_color or render.DEFAULT_BRAND_COLOR\n    msg = email.message.EmailMessage()\n    msg[\"To\"] = to_addr\n    msg[\"Subject\"] = (\n        f\"Verify your email{f' for {app_name}' if app_name else ''}\"\n    )\n    plain_text_content = f\"\"\"\nCongratulations, you're registered{f' at {app_name}' if app_name else ''}!\n\nPlease paste the following URL into your browser address bar to verify your\nemail address:\n\n{verify_url}\n        \"\"\"\n    html_content = f\"\"\"\n<tr>\n  <td\n    align=\"left\"\n    style=\"\n      font-size: 0px;\n      padding: 10px 25px;\n      padding-top: 50px;\n      word-break: break-word;\n    \"\n  >\n    <div\n      style=\"\n        font-family:\n          open Sans Helvetica,\n          Arial,\n          sans-serif;\n        font-size: 16px;\n        line-height: 1;\n        text-align: left;\n        color: #000000;\n      \"\n    >\n      Congratulations, you're registered\n      {f'at {app_name}' if app_name else ''}!\n    </div>\n  </td>\n</tr>\n<tr>\n  <td\n    align=\"left\"\n    style=\"font-size: 0px; padding: 10px 25px; word-break: break-word\"\n  >\n    <div\n      style=\"\n        font-family:\n          open Sans Helvetica,\n          Arial,\n          sans-serif;\n        font-size: 16px;\n        line-height: 1;\n        text-align: left;\n        color: #000000;\n      \"\n    >\n      Please press the button below to verify your email address:\n    </div>\n  </td>\n</tr>\n<tr>\n  <td\n    align=\"center\"\n    vertical-align=\"middle\"\n    style=\"font-size: 0px; padding: 10px 25px; word-break: break-word\"\n  >\n    <table\n      border=\"0\"\n      cellpadding=\"0\"\n      cellspacing=\"0\"\n      role=\"presentation\"\n      style=\"border-collapse: separate; line-height: 100%\"\n    >\n      <tr>\n        <td\n          align=\"center\"\n          bgcolor=\"#{brand_color}\"\n          role=\"presentation\"\n          style=\"\n            border: none;\n            border-radius: 4px;\n            cursor: auto;\n            mso-padding-alt: 10px 25px;\n            background: #{brand_color};\n          \"\n          valign=\"middle\"\n        >\n          <a\n            href=\"{verify_url}\"\n            style=\"\n              display: inline-block;\n              background: #{brand_color};\n              color: #ffffff;\n              font-family:\n                open Sans Helvetica,\n                Arial,\n                sans-serif;\n              font-size: 18px;\n              font-weight: bold;\n              line-height: 120%;\n              margin: 0;\n              text-decoration: none;\n              text-transform: none;\n              padding: 10px 25px;\n              mso-padding-alt: 0px;\n              border-radius: 4px;\n            \"\n            target=\"_blank\"\n          >\n            Verify email address\n          </a>\n        </td>\n      </tr>\n    </table>\n  </td>\n</tr>\n<tr>\n  <td\n    align=\"left\"\n    style=\"font-size: 0px; padding: 10px 25px; word-break: break-word\"\n  >\n    <div\n      style=\"\n        font-family:\n          open Sans Helvetica,\n          Arial,\n          sans-serif;\n        font-size: 16px;\n        line-height: 1;\n        text-align: left;\n        color: #000000;\n      \"\n    >\n      In case the button didn't work, please paste the following URL into\n      your browser address bar:\n      <p style=\"word-break: break-all\">{verify_url}</p>\n    </div>\n  </td>\n</tr>\n    \"\"\"\n    msg[\"X-gel-email-verify-url\"] = verify_url\n    msg.set_content(plain_text_content, subtype=\"plain\")\n    msg.add_alternative(\n        render.base_default_email(\n            content=html_content,\n            app_name=app_name,\n            logo_url=logo_url,\n        ),\n        subtype=\"html\",\n    )\n    return msg\n\n\ndef render_magic_link_email(\n    *,\n    to_addr: str,\n    link: str,\n    app_name: Optional[str] = None,\n    logo_url: Optional[str] = None,\n    dark_logo_url: Optional[str] = None,\n    brand_color: Optional[str] = render.DEFAULT_BRAND_COLOR,\n) -> email.message.EmailMessage:\n    brand_color = brand_color or render.DEFAULT_BRAND_COLOR\n    msg = email.message.EmailMessage()\n    msg[\"To\"] = to_addr\n    msg[\"Subject\"] = f\"Sign in{f' to {app_name}' if app_name else ''}\"\n    plain_text_content = f\"\"\"\nPlease paste the following URL into your browser address bar to be signed into\nyour account:\n\n{link}\n        \"\"\"\n    html_content = f\"\"\"\n<tr>\n  <td\n    align=\"left\"\n    style=\"\n      font-size: 0px;\n      padding: 10px 25px;\n      padding-top: 50px;\n      word-break: break-word;\n    \"\n  >\n    <div\n      style=\"\n        font-family:\n          open Sans Helvetica,\n          Arial,\n          sans-serif;\n        font-size: 16px;\n        line-height: 1;\n        text-align: left;\n        color: #000000;\n      \"\n    >\n      Click the button below to sign in{f' to {app_name}' if app_name else ''}:\n    </div>\n  </td>\n</tr>\n<tr>\n  <td\n    align=\"center\"\n    vertical-align=\"middle\"\n    style=\"\n      font-size: 0px;\n      padding: 10px 25px;\n      word-break: break-word;\n    \"\n  >\n    <table\n      border=\"0\"\n      cellpadding=\"0\"\n      cellspacing=\"0\"\n      role=\"presentation\"\n      style=\"border-collapse: separate; line-height: 100%\"\n    >\n      <tr>\n        <td\n          align=\"center\"\n          bgcolor=\"#{brand_color}\"\n          role=\"presentation\"\n          style=\"\n            border: none;\n            border-radius: 4px;\n            cursor: auto;\n            mso-padding-alt: 10px 25px;\n            background: #{brand_color};\n          \"\n          valign=\"middle\"\n        >\n          <a\n            href=\"{link}\"\n            style=\"\n              display: inline-block;\n              background: #{brand_color};\n              color: #ffffff;\n              font-family: open Sans Helvetica, Arial, sans-serif;\n              font-size: 18px;\n              font-weight: bold;\n              line-height: 120%;\n              margin: 0;\n              text-decoration: none;\n              text-transform: none;\n              padding: 10px 25px;\n              mso-padding-alt: 0px;\n              border-radius: 4px;\n            \"\n            target=\"_blank\"\n          >\n            Sign in\n          </a>\n        </td>\n      </tr>\n    </table>\n  </td>\n</tr>\n<tr>\n  <td\n    align=\"left\"\n    style=\"\n      font-size: 0px;\n      padding: 10px 25px;\n      word-break: break-word;\n    \"\n  >\n    <div\n      style=\"\n        font-family: open Sans Helvetica, Arial, sans-serif;\n        font-size: 16px;\n        line-height: 1;\n        text-align: left;\n        color: #000000;\n      \"\n    >\n      In case the button didn't work, please paste the following URL\n      into your browser address bar:\n      <p style=\"word-break: break-all\">{link}</p>\n    </div>\n  </td>\n</tr>\n    \"\"\"  # noqa: E501\n\n    msg[\"X-gel-magic-link\"] = link\n    msg.set_content(plain_text_content, subtype=\"plain\")\n    msg.add_alternative(\n        render.base_default_email(\n            content=html_content,\n            app_name=app_name,\n            logo_url=logo_url,\n        ),\n        subtype=\"html\",\n    )\n    return msg\n\n\ndef render_one_time_code_email(\n    *,\n    to_addr: str,\n    code: str,\n    app_name: Optional[str] = None,\n    logo_url: Optional[str] = None,\n    dark_logo_url: Optional[str] = None,\n    brand_color: Optional[str] = render.DEFAULT_BRAND_COLOR,\n) -> email.message.EmailMessage:\n    \"\"\"Renders an email containing a one-time verification code.\"\"\"\n    brand_color = brand_color or render.DEFAULT_BRAND_COLOR\n    msg = email.message.EmailMessage()\n    msg[\"To\"] = to_addr\n    msg[\"Subject\"] = (\n        f\"Your verification code{f' for {app_name}' if app_name else ''}\"\n    )\n    plain_text_content = f\"\"\"\nYour verification code{f' for {app_name}' if app_name else ''} is:\n\n{code}\n\nThis code will expire in 10 minutes.\n        \"\"\"\n    html_content = f\"\"\"\n<tr>\n  <td\n    align=\"left\"\n    style=\"\n      font-size: 0px;\n      padding: 10px 25px;\n      padding-top: 50px;\n      word-break: break-word;\n    \"\n  >\n    <div\n      style=\"\n        font-family:\n          open Sans Helvetica,\n          Arial,\n          sans-serif;\n        font-size: 16px;\n        line-height: 1;\n        text-align: left;\n        color: #000000;\n      \"\n    >\n      Your verification code{f' for {app_name}' if app_name else ''} is:\n    </div>\n  </td>\n</tr>\n<tr>\n  <td\n    align=\"center\"\n    vertical-align=\"middle\"\n    style=\"\n      font-size: 0px;\n      padding: 20px 25px;\n      word-break: break-word;\n    \"\n  >\n    <div\n      style=\"\n        font-family: open Sans Helvetica, Arial, sans-serif;\n        font-size: 32px;\n        font-weight: bold;\n        line-height: 1;\n        text-align: center;\n        color: #{brand_color};\n        letter-spacing: 8px;\n        padding: 20px;\n        border: 2px solid #{brand_color};\n        border-radius: 8px;\n        background: #f8f9fa;\n      \"\n    >\n      {code}\n    </div>\n  </td>\n</tr>\n<tr>\n  <td\n    align=\"left\"\n    style=\"\n      font-size: 0px;\n      padding: 10px 25px;\n      word-break: break-word;\n    \"\n  >\n    <div\n      style=\"\n        font-family: open Sans Helvetica, Arial, sans-serif;\n        font-size: 16px;\n        line-height: 1;\n        text-align: left;\n        color: #000000;\n      \"\n    >\n      This code will expire in 10 minutes for your security.\n    </div>\n  </td>\n</tr>\n    \"\"\"  # noqa: E501\n\n    msg[\"X-gel-email-verify-code\"] = code\n    msg.set_content(plain_text_content, subtype=\"plain\")\n    msg.add_alternative(\n        render.base_default_email(\n            content=html_content,\n            app_name=app_name,\n            logo_url=logo_url,\n        ),\n        subtype=\"html\",\n    )\n    return msg\n\n\ndef render_password_reset_code_email(\n    *,\n    to_addr: str,\n    code: str,\n    app_name: Optional[str] = None,\n    logo_url: Optional[str] = None,\n    dark_logo_url: Optional[str] = None,\n    brand_color: Optional[str] = render.DEFAULT_BRAND_COLOR,\n) -> email.message.EmailMessage:\n    \"\"\"Renders an email containing a one-time code for password reset.\"\"\"\n    brand_color = brand_color or render.DEFAULT_BRAND_COLOR\n    msg = email.message.EmailMessage()\n    msg[\"To\"] = to_addr\n    msg[\"Subject\"] = (\n        f\"Password reset code{f' for {app_name}' if app_name else ''}\"\n    )\n    plain_text_content = f\"\"\"\nYour password reset code{f' for {app_name}' if app_name else ''} is:\n\n{code}\n\nThis code will expire in 10 minutes. If you didn't request a password reset, you can safely ignore this email.\n        \"\"\"  # noqa: E501\n    html_content = f\"\"\"\n<tr>\n  <td\n    align=\"left\"\n    style=\"\n      font-size: 0px;\n      padding: 10px 25px;\n      padding-top: 50px;\n      word-break: break-word;\n    \"\n  >\n    <div\n      style=\"\n        font-family:\n          open Sans Helvetica,\n          Arial,\n          sans-serif;\n        font-size: 16px;\n        line-height: 1;\n        text-align: left;\n        color: #000000;\n      \"\n    >\n      Your password reset code{f' for {app_name}' if app_name else ''} is:\n    </div>\n  </td>\n</tr>\n<tr>\n  <td\n    align=\"center\"\n    vertical-align=\"middle\"\n    style=\"\n      font-size: 0px;\n      padding: 20px 25px;\n      word-break: break-word;\n    \"\n  >\n    <div\n      style=\"\n        font-family: open Sans Helvetica, Arial, sans-serif;\n        font-size: 32px;\n        font-weight: bold;\n        line-height: 1;\n        text-align: center;\n        color: #{brand_color};\n        letter-spacing: 8px;\n        padding: 20px;\n        border: 2px solid #{brand_color};\n        border-radius: 8px;\n        background: #f8f9fa;\n      \"\n    >\n      {code}\n    </div>\n  </td>\n</tr>\n<tr>\n  <td\n    align=\"left\"\n    style=\"\n      font-size: 0px;\n      padding: 10px 25px;\n      word-break: break-word;\n    \"\n  >\n    <div\n      style=\"\n        font-family: open Sans Helvetica, Arial, sans-serif;\n        font-size: 16px;\n        line-height: 1;\n        text-align: left;\n        color: #000000;\n      \"\n    >\n      This code will expire in 10 minutes for your security. If you didn't request a password reset, you can safely ignore this email.\n    </div>\n  </td>\n</tr>\n    \"\"\"  # noqa: E501\n\n    msg[\"X-gel-password-reset-code\"] = code\n    msg.set_content(plain_text_content, subtype=\"plain\")\n    msg.add_alternative(\n        render.base_default_email(\n            content=html_content,\n            app_name=app_name,\n            logo_url=logo_url,\n        ),\n        subtype=\"html\",\n    )\n    return msg\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/ui/components.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Optional, TYPE_CHECKING\n\nimport html\nimport re\nimport urllib.parse\n\nfrom . import util\n\nif TYPE_CHECKING:\n    from edb.server.protocol.auth_ext import config as auth_config\n\nknown_oauth_provider_names = [\n    'builtin::oauth_github',\n    'builtin::oauth_google',\n    'builtin::oauth_apple',\n    'builtin::oauth_azure',\n    'builtin::oauth_discord',\n    'builtin::oauth_slack',\n]\n\n\nDEFAULT_BRAND_COLOR = \"1f8aed\"\n\n\ndef base_page(\n    *,\n    content: str,\n    title: str,\n    cleanup_search_params: list[str],\n    logo_url: Optional[str] = None,\n    dark_logo_url: Optional[str] = None,\n    brand_color: Optional[str] = DEFAULT_BRAND_COLOR,\n) -> bytes:\n    logo = ''\n    if logo_url:\n        logo = '<picture class=\"brand-logo\">'\n        if dark_logo_url:\n            logo += f'''<source srcset=\"{html.escape(dark_logo_url)}\"\n                media=\"(prefers-color-scheme: dark)\" />'''\n        logo += f'<img src=\"{html.escape(logo_url)}\" /></picture>'\n\n    cleanup_script = (\n        f'''<script>\n      const params = [\"{'\", \"'.join(cleanup_search_params)}\"];\n      const url = new URL(location);\n      if (params.some((p) => url.searchParams.has(p))) {{\n        for (const p of params) {{\n          url.searchParams.delete(p);\n        }}\n        history.replaceState(null, '', url);\n      }}\n    </script>'''\n        if len(cleanup_search_params) > 0\n        else ''\n    )\n\n    if (\n        brand_color is None\n        or util.hex_color_regexp.fullmatch(brand_color) is None\n    ):\n        brand_color = DEFAULT_BRAND_COLOR\n\n    return f'''\n<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width\" />\n    <link rel=\"stylesheet\" href=\"_static/styles.css\" />\n    <title>{html.escape(title)}</title>\n    {cleanup_script}\n    <script type=\"module\" src=\"_static/interactions.js\"></script>\n  </head>\n  <body style=\"{util.get_colour_vars(brand_color)}\">\n    {logo}\n    <div id=\"container-wrapper\" class=\"container-wrapper\">\n      <main class=\"container\">\n        {content}\n      </main>\n    </div>\n  </body>\n</html>\n'''.encode()\n\n\ndef script(name: str) -> str:\n    return f'<script type=\"module\" src=\"_static/{name}.js\"></script>'\n\n\ndef title(title: str, *, app_name: Optional[str], join: str = 'to') -> str:\n    if app_name is None:\n        return f'''<h1><span>{title}</span></h1>'''\n\n    return f'''<h1><span>{title} {join}</span> {html.escape(app_name)}</h1>'''\n\n\ndef oauth_buttons(\n    *,\n    redirect_to: str,\n    challenge: str,\n    redirect_to_on_signup: Optional[str],\n    oauth_providers: list[auth_config.OAuthProviderConfig],\n    label_prefix: str,\n    collapsed: bool,\n) -> str:\n    if len(oauth_providers) == 0:\n        return ''\n\n    oauth_params = {\n        'redirect_to': redirect_to,\n        'challenge': challenge,\n    }\n    if redirect_to_on_signup:\n        oauth_params['redirect_to_on_signup'] = redirect_to_on_signup\n\n    buttons = '\\n'.join(\n        [\n            _oauth_button(p, oauth_params, label_prefix=label_prefix)\n            for p in sorted(oauth_providers, key=lambda p: p.name)\n        ]\n    )\n\n    return f'''\n      <div class=\"oauth-buttons{' collapsed' if collapsed else ''}\">\n        {buttons}\n      </div>\n    '''\n\n\ndef _oauth_button(\n    provider: auth_config.OAuthProviderConfig,\n    params: dict[str, str],\n    *,\n    label_prefix: str,\n) -> str:\n    href = '../authorize?' + urllib.parse.urlencode(\n        {'provider': provider.name, **params}\n    )\n    if (\n        provider.name.startswith('builtin::')\n        and provider.name in known_oauth_provider_names\n    ):\n        img = f'''<img src=\"_static/icon_{provider.name[15:]}.svg\"\n            alt=\"{provider.display_name} Icon\" />'''\n    elif provider.logo_url is not None:\n        img = f'''<img src=\"{provider.logo_url}\"\n            alt=\"{provider.display_name} Icon\" />'''\n    else:\n        img = ''\n\n    label = f'{label_prefix} {provider.display_name}'\n    return f'''\n        <a href={href} title=\"{label}\">\n          {img}\n          <span>{label}</span>\n        </a>\n    '''\n\n\ndef button(\n    text: Optional[str],\n    *,\n    id: Optional[str] = None,\n    secondary: Optional[bool] = False,\n    type: Optional[str] = 'submit',\n) -> str:\n    classes = []\n    if secondary:\n        classes.append('secondary')\n    if text is None:\n        classes.append('icon-only')\n\n    attrs = f'type=\"{type}\"'\n    if id:\n        attrs += f' id=\"{id}\"'\n    if len(classes):\n        attrs += f' class=\"{\" \".join(classes)}\"'\n\n    return f'''\n      <button {attrs}>\n        {f'<span>{text}</span>' if text else ''}\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          width=\"24\"\n          height=\"25\"\n          viewBox=\"0 0 24 25\"\n          fill=\"none\"\n        >\n          <path\n            d=\"M5 12.5H19\"\n            stroke=\"currentColor\"\n            stroke-width=\"1.75\"\n            stroke-linecap=\"round\"\n            stroke-linejoin=\"round\"\n          />\n          <path\n            d=\"M12 5.5L19 12.5L12 19.5\"\n            stroke=\"currentColor\"\n            stroke-width=\"1.75\"\n            stroke-linecap=\"round\"\n            stroke-linejoin=\"round\"\n          />\n        </svg>\n      </button>'''\n\n\ndivider = '''\n    <div class=\"divider\">\n      <span>or</span>\n    </div>'''\n\n\ndef _slugify_label(label: str) -> str:\n    slug = label.lower().strip()\n    slug = re.sub(r\"[^a-z0-9]+\", \"-\", slug)\n    slug = re.sub(r\"(^-|-$)\", \"\", slug)\n    return slug or \"section\"\n\n\ndef tabs_content(\n    sections: list[str], selected_tab: int, labels: Optional[list[str]] = None\n) -> str:\n    content = ''\n\n    for i, section in enumerate(sections):\n        active = selected_tab == i\n        aria_attrs = ''\n        if labels is not None and i < len(labels):\n            slug = _slugify_label(labels[i])\n            aria_attrs = (\n                ' role=\"tabpanel\" '\n                f'id=\"panel-{slug}\" aria-labelledby=\"tab-{slug}\"'\n            )\n            hidden_attrs = ' aria-hidden=\"true\" hidden' if not active else ''\n        else:\n            hidden_attrs = '' if active else ''\n\n        content += f'''\n            <div\n                class=\"slider-section{' active' if active else ''}\"\n                {aria_attrs}\n                {hidden_attrs}\n            >\n                {section}\n            </div>\n        '''\n\n    style = (\n        f'style=\"transform: translateX({-100 * selected_tab}%)\"'\n        if selected_tab > 0\n        else ''\n    )\n    return f'''\n        <div id=\"slider-container\" class=\"slider-container\" {style}>\n          {content}\n        </div>\n    '''\n\n\n_tab_underline = '''\n    <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"2\" fill=\"none\">\n      <rect height=\"2\" width=\"100%\" rx=\"1\" />\n    </svg>'''\n\n\ndef tabs_buttons(labels: list[str], selected_tab: int) -> str:\n    content = ''\n\n    for i, label in enumerate(labels):\n        active = selected_tab == i\n        slug = _slugify_label(label)\n        aria_selected = 'true' if active else 'false'\n        tabindex = '0' if active else '-1'\n        content += f'''\n            <div\n              id=\"tab-{slug}\"\n              class=\"tab{' active' if active else ''}\"\n              role=\"tab\"\n              aria-selected=\"{aria_selected}\"\n              aria-controls=\"panel-{slug}\"\n              tabindex=\"{tabindex}\"\n            >\n              {label}\n              {_tab_underline}\n            </div>\n        '''\n\n    return f'''\n        <div id=\"email-provider-tabs\" class=\"tabs\" role=\"tablist\">\n          {content}\n        </div>\n    '''\n\n\ndef hidden_input(\n    *, name: str, value: str, secondary_value: Optional[str] = None\n) -> str:\n    return f'''<input type=\"hidden\" name=\"{name}\" value=\"{value}\" {\n        f'data-secondary-value=\"{secondary_value}\"' if secondary_value else ''\n    } />'''\n\n\ndef bottom_note(message: str, *, link: str, href: str) -> str:\n    return f\"\"\"\n        <div class=\"bottom-note\">\n            {message}\n            <a href=\"{href}\">{link}</a>\n        </div>\n        \"\"\"\n\n\ndef error_message(message: Optional[str], escape: bool = True) -> str:\n    if message is None:\n        return ''\n\n    return f'''\n        <div class=\"error-message\">\n        <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"20\"\n          viewBox=\"0 0 24 20\" fill=\"none\">\n            <path d=\"M12 15H12.01M12 7.00002V11M10.29 1.86002L1.82002\n              16C1.64539 16.3024 1.55299 16.6453 1.55201 16.9945C1.55103\n              17.3438 1.64151 17.6872 1.81445 17.9905C1.98738 18.2939 2.23675\n              18.5468 2.53773 18.7239C2.83871 18.901 3.18082 18.9962 3.53002\n              19H20.47C20.8192 18.9962 21.1613 18.901 21.4623 18.7239C21.7633\n              18.5468 22.0127 18.2939 22.1856 17.9905C22.3585 17.6872 22.449\n              17.3438 22.448 16.9945C22.4471 16.6453 22.3547 16.3024 22.18\n              16L13.71 1.86002C13.5318 1.56613 13.2807 1.32314 12.9812\n              1.15451C12.6817 0.98587 12.3438 0.897278 12 0.897278C11.6563\n              0.897278 11.3184 0.98587 11.0188 1.15451C10.7193 1.32314 10.4683\n              1.56613 10.29 1.86002Z\"\n              stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"/>\n        </svg>\n        <span>{html.escape(message) if escape else message}</span>\n        </div>'''\n\n\ndef success_message(message: str) -> str:\n    return f'''\n        <div class=\"success-message\">\n        <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\"\n          viewBox=\"0 0 24 24\" fill=\"none\">\n          <path d=\"M22 2L11 13\" stroke=\"currentColor\" stroke-width=\"1.5\"\n            stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n          <path d=\"M22 2L15 22L11 13L2 9L22 2Z\" stroke=\"currentColor\"\n            stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n        </svg>\n        <span>{message}</span>\n        </div>\n    '''\n\n\ndef code_input_form(\n    *,\n    action: str,\n    email: str,\n    provider: str,\n    label: str = \"Enter verification code\",\n    button_text: str = \"Verify Code\",\n    additional_fields: str = \"\",\n) -> str:\n    \"\"\"Renders a code input form with auto-formatting and mobile\n    keyboard support.\"\"\"\n\n    return f'''\n    <form id=\"code-form\" method=\"POST\" action=\"{action}\">\n        <input type=\"hidden\" name=\"email\" value=\"{html.escape(email)}\" />\n        <input type=\"hidden\" name=\"provider\" value=\"{provider}\" />\n        <label for=\"code\">{label}</label>\n        <input\n            id=\"code\"\n            name=\"code\"\n            type=\"text\"\n            inputmode=\"numeric\"\n            autocomplete=\"one-time-code\"\n            enterkeyhint=\"done\"\n            pattern=\"[0-9]{{6}}\"\n            maxlength=\"6\"\n            required\n            spellcheck=\"false\"\n            autocapitalize=\"off\"\n            placeholder=\"123456\"\n        />\n\n        {additional_fields}\n        {button(button_text)}\n    </form>\n    '''\n\n\ndef base_default_email(\n    *,\n    content: str,\n    app_name: Optional[str],\n    logo_url: Optional[str],\n) -> str:\n    logo_html = (\n        f\"\"\"\n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" style=\"width:600px;\" width=\"600\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n      <div style=\"margin: 0px auto; max-width: 600px\">\n        <table\n          align=\"center\"\n          border=\"0\"\n          cellpadding=\"0\"\n          cellspacing=\"0\"\n          role=\"presentation\"\n          style=\"width: 100%\"\n        >\n          <tbody>\n            <tr>\n              <td\n                style=\"\n                  direction: ltr;\n                  font-size: 0px;\n                  padding: 20px 0;\n                  padding-bottom: 0px;\n                  padding-top: 20px;\n                  text-align: center;\n                \"\n              >\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n                <div\n                  class=\"mj-column-per-100 mj-outlook-group-fix\"\n                  style=\"\n                    font-size: 0px;\n                    text-align: left;\n                    direction: ltr;\n                    display: inline-block;\n                    vertical-align: top;\n                    width: 100%;\n                  \"\n                >\n                  <table\n                    border=\"0\"\n                    cellpadding=\"0\"\n                    cellspacing=\"0\"\n                    role=\"presentation\"\n                    style=\"vertical-align: top\"\n                    width=\"100%\"\n                  >\n                    <tbody>\n                      <tr>\n                        <td\n                          align=\"center\"\n                          style=\"\n                            font-size: 0px;\n                            padding: 10px 25px;\n                            padding-top: 0;\n                            padding-right: 0px;\n                            padding-bottom: 0px;\n                            padding-left: 0px;\n                            word-break: break-word;\n                          \"\n                        >\n                          <table\n                            border=\"0\"\n                            cellpadding=\"0\"\n                            cellspacing=\"0\"\n                            role=\"presentation\"\n                            style=\"border-collapse: collapse; border-spacing: 0px\"\n                          >\n                            <tbody>\n                              <tr>\n                                <td style=\"width: 150px\">\n                                  <img\n                                    alt=\"\n                                      {f'{app_name} logo' if app_name else ''}\n                                    \"\n                                    height=\"150\"\n                                    src=\"{logo_url}\"\n                                    style=\"\n                                      border: none;\n                                      display: block;\n                                      outline: none;\n                                      text-decoration: none;\n                                      height: 150px;\n                                      width: 100%;\n                                      font-size: 13px;\n                                    \"\n                                    width=\"150\"\n                                  />\n                                </td>\n                              </tr>\n                            </tbody>\n                          </table>\n                        </td>\n                      </tr>\n                    </tbody>\n                  </table>\n                </div>\n                <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n      <!--[if mso | IE]></td></tr></table><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" style=\"width:600px;\" width=\"600\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n\"\"\"  # noqa: E501\n        if logo_url\n        else \"\"\n    )\n\n    return f\"\"\"\n<!doctype html>\n<html\n  xmlns=\"http://www.w3.org/1999/xhtml\"\n  xmlns:v=\"urn:schemas-microsoft-com:vml\"\n  xmlns:o=\"urn:schemas-microsoft-com:office:office\"\n>\n<head>\n  <title>\n  </title>\n  <!--[if !mso]><!-->\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <!--<![endif]-->\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <style type=\"text/css\">\n    #outlook a {{\n      padding: 0;\n    }}\n\n    body {{\n      margin: 0;\n      padding: 0;\n      -webkit-text-size-adjust: 100%;\n      -ms-text-size-adjust: 100%;\n    }}\n\n    table,\n    td {{\n      border-collapse: collapse;\n      mso-table-lspace: 0pt;\n      mso-table-rspace: 0pt;\n    }}\n\n    img {{\n      border: 0;\n      height: auto;\n      line-height: 100%;\n      outline: none;\n      text-decoration: none;\n      -ms-interpolation-mode: bicubic;\n    }}\n\n    p {{\n      display: block;\n      margin: 13px 0;\n    }}\n  </style>\n  <!--[if mso]>\n        <noscript>\n        <xml>\n        <o:OfficeDocumentSettings>\n          <o:AllowPNG/>\n          <o:PixelsPerInch>96</o:PixelsPerInch>\n        </o:OfficeDocumentSettings>\n        </xml>\n        </noscript>\n        <![endif]-->\n  <!--[if lte mso 11]>\n        <style type=\"text/css\">\n          .mj-outlook-group-fix {{ width:100% !important; }}\n        </style>\n        <![endif]-->\n  <!--[if !mso]><!-->\n  <link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,700\" rel=\"stylesheet\" type=\"text/css\">\n  <style type=\"text/css\">\n    @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,700);\n  </style>\n  <!--<![endif]-->\n  <style type=\"text/css\">\n    @media only screen and (min-width:480px) {{\n      .mj-column-per-100 {{\n        width: 100% !important;\n        max-width: 100%;\n      }}\n    }}\n  </style>\n  <style media=\"screen and (min-width:480px)\">\n    .moz-text-html .mj-column-per-100 {{\n      width: 100% !important;\n      max-width: 100%;\n    }}\n  </style>\n  <style type=\"text/css\">\n    @media only screen and (max-width:480px) {{\n      table.mj-full-width-mobile {{\n        width: 100% !important;\n      }}\n\n      td.mj-full-width-mobile {{\n        width: auto !important;\n      }}\n    }}\n  </style>\n</head>\n\n  <body style=\"word-spacing: normal; background-color: #ffffff\">\n    <div style=\"background-color: #ffffff\">\n{logo_html}\n      <div style=\"margin: 0px auto; max-width: 600px\">\n        <table\n          align=\"center\"\n          border=\"0\"\n          cellpadding=\"0\"\n          cellspacing=\"0\"\n          role=\"presentation\"\n          style=\"width: 100%\"\n        >\n          <tbody>\n            <tr>\n              <td\n                style=\"\n                  direction: ltr;\n                  font-size: 0px;\n                  padding: 20px 0;\n                  padding-bottom: 20px;\n                  padding-top: 20px;\n                  text-align: center;\n                \"\n              >\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:middle;width:600px;\" ><![endif]-->\n                <div\n                  class=\"mj-column-per-100 mj-outlook-group-fix\"\n                  style=\"\n                    font-size: 0px;\n                    text-align: left;\n                    direction: ltr;\n                    display: inline-block;\n                    vertical-align: middle;\n                    width: 100%;\n                  \"\n                >\n                  <table\n                    border=\"0\"\n                    cellpadding=\"0\"\n                    cellspacing=\"0\"\n                    role=\"presentation\"\n                    style=\"vertical-align: middle\"\n                    width=\"100%\"\n                  >\n                    <tbody>\n{content}\n                    </tbody>\n                  </table>\n                </div>\n                <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    </div>\n  </body>\n</html>\n\"\"\"  # noqa: E501\n\n\n# Form Component Helpers\n# =====================\n\n\ndef get_magic_link_tab_label(verification_method: str) -> str:\n    return \"Email Code\" if verification_method == \"Code\" else \"Email Link\"\n\n\ndef get_magic_link_button_text(verification_method: str) -> str:\n    return (\n        \"Email sign in code\"\n        if verification_method == \"Code\"\n        else \"Email sign in link\"\n    )\n\n\ndef get_email_password_signup_redirect_url(\n    verification_method: str, base_path: str, fallback_redirect: str\n) -> str:\n    if verification_method == \"Code\":\n        return f\"{base_path}/ui/verify?provider=builtin::local_emailpassword\"\n    else:\n        return fallback_redirect\n\n\ndef get_webauthn_signup_redirect_url(\n    verification_method: str, base_path: str, fallback_redirect: str\n) -> str:\n    if verification_method == \"Code\":\n        return f\"{base_path}/ui/verify?provider=builtin::local_webauthn\"\n    else:\n        return fallback_redirect\n\n\ndef get_password_reset_redirect_url(\n    verification_method: str, base_path: str, challenge: str\n) -> str:\n    if verification_method == \"Code\":\n        return f\"{base_path}/ui/reset-password\"\n    else:\n        return f\"{base_path}/ui/forgot-password?challenge={challenge}\"\n\n\ndef get_send_button_text(verification_method: str) -> str:\n    return \"Send Code\" if verification_method == \"Code\" else \"Send Link\"\n\n\ndef get_verification_method_label(verification_method: str) -> str:\n    return \"Email Code\" if verification_method == \"Code\" else \"Email Link\"\n\n\ndef render_base_email_form(\n    *, id: str, challenge: str, email: str | None = None\n) -> str:\n    return f\"\"\"\n        <input type=\"hidden\" name=\"challenge\" value=\"{challenge}\" />\n        <label for=\"email\">Email</label>\n        <input id=\"{id}\" name=\"email\" type=\"email\" value=\"{email or ''}\" />\n    \"\"\"\n\n\ndef render_password_input(\n    *, challenge: str, should_show_forgot_password: bool\n) -> str:\n    forgot_password_link = (\n        f\"\"\"\n        <a\n          id=\"forgot-password-link\"\n          class=\"field-note\"\n          href=\"forgot-password?challenge={challenge}\"\n          tabindex=\"-1\"\n        >\n            Forgot password?\n        </a>\n        \"\"\"\n        if should_show_forgot_password\n        else ''\n    )\n\n    return f\"\"\"\n        <div class=\"field-header\">\n            <label for=\"password\">Password</label>\n            {forgot_password_link}\n        </div>\n        <input id=\"password\" name=\"password\" type=\"password\" />\n    \"\"\"\n\n\ndef render_password_form(\n    *,\n    challenge: str,\n    email: str | None = None,\n    redirect_to: str,\n    base_path: str,\n    provider_name: str,\n) -> str:\n    return f\"\"\"\n        <form\n            method=\"post\"\n            action=\"../authenticate\"\n            novalidate\n        >\n            <input type=\"hidden\" name=\"redirect_to\" value=\"{redirect_to}\" />\n            <input\n                type=\"hidden\"\n                name=\"redirect_on_failure\"\n                value=\"{base_path}/ui/signin?selected_tab=password\"\n            />\n            <input type=\"hidden\" name=\"provider\" value=\"{provider_name}\" />\n            {render_base_email_form(\n                id=\"password-email\", challenge=challenge, email=email\n            )}\n            {render_password_input(\n                challenge=challenge,\n                should_show_forgot_password=True,\n            )}\n            {button(\"Sign In\", id=\"password-signin\")}\n        </form>\n    \"\"\"\n\n\ndef render_webauthn_form(\n    *,\n    challenge: str,\n    email: str | None = None,\n    redirect_to: str,\n    base_path: str,\n    provider_name: str,\n) -> str:\n    \"\"\"Render a complete WebAuthn authentication form.\"\"\"\n    return f\"\"\"\n        <form\n            id=\"email-factor\"\n            novalidate\n        >\n            <input type=\"hidden\" name=\"redirect_to\" value=\"{redirect_to}\" />\n            <input\n                type=\"hidden\"\n                name=\"redirect_on_failure\"\n                value=\"{base_path}/ui/signin?selected_tab=webauthn\"\n            />\n            <input type=\"hidden\" name=\"provider\" value=\"{provider_name}\" />\n            <input type=\"hidden\" name=\"callback_url\" value=\"{redirect_to}\" />\n            {render_base_email_form(\n                id=\"webauthn-email\", challenge=challenge, email=email\n            )}\n            {button(\"Sign In\", id=\"webauthn-signin\")}\n        </form>\n    \"\"\"\n\n\ndef render_magic_link_form(\n    *,\n    challenge: str,\n    email: str | None = None,\n    base_path: str,\n    provider_name: str,\n    callback_url: str | None = None,\n    verification_method: str = \"Link\",\n) -> str:\n    button_text = get_magic_link_button_text(verification_method)\n    callback_field = (\n        f'''\n            <input type=\"hidden\" name=\"callback_url\" value=\"{callback_url}\" />\n        '''\n        if verification_method == \"Link\"\n        else \"\"\n    )\n\n    return f\"\"\"\n        <form\n            method=\"post\"\n            action=\"../magic-link/email\"\n            novalidate\n        >\n            <input\n                type=\"hidden\"\n                name=\"redirect_to\"\n                value=\"{base_path}/ui/magic-link-sent\"\n            />\n            <input\n                type=\"hidden\"\n                name=\"redirect_on_failure\"\n                value=\"{base_path}/ui/signin?selected_tab=magic_link\"\n            />\n            <input type=\"hidden\" name=\"provider\" value=\"{provider_name}\" />\n            {callback_field}\n            {render_base_email_form(\n                id=\"magic-link-email\", challenge=challenge, email=email\n            )}\n            {button(button_text, id=\"magic-link-signin\")}\n        </form>\n    \"\"\"\n\n\n# Signup-specific form helpers\n# ===========================\n\n\ndef render_password_signup_form(\n    *,\n    challenge: str,\n    email: str | None = None,\n    redirect_to: str,\n    base_path: str,\n    provider_name: str,\n) -> str:\n    return f\"\"\"\n        <form\n            method=\"post\"\n            action=\"../register\"\n            novalidate\n        >\n            <input type=\"hidden\" name=\"redirect_to\" value=\"{redirect_to}\" />\n            <input\n                type=\"hidden\"\n                name=\"redirect_on_failure\"\n                value=\"{base_path}/ui/signup?selected_tab=password\"\n            />\n            <input type=\"hidden\" name=\"provider\" value=\"{provider_name}\" />\n            <input\n                type=\"hidden\"\n                name=\"verify_url\"\n                value=\"{base_path}/ui/verify\"\n            />\n            {render_base_email_form(\n                id=\"password-email\", challenge=challenge, email=email\n            )}\n            {render_password_input(\n                challenge=challenge,\n                should_show_forgot_password=False,\n            )}\n            {button(\"Sign Up\", id=\"password-signup\")}\n        </form>\n    \"\"\"\n\n\ndef render_webauthn_signup_form(\n    *,\n    challenge: str,\n    email: str | None = None,\n    redirect_to: str,\n    base_path: str,\n    provider_name: str,\n) -> str:\n    \"\"\"Render a complete WebAuthn signup form.\"\"\"\n    return f\"\"\"\n        <form\n            id=\"email-factor\"\n            novalidate\n        >\n            <input type=\"hidden\" name=\"redirect_to\" value=\"{redirect_to}\" />\n            <input\n                type=\"hidden\"\n                name=\"redirect_on_failure\"\n                value=\"{base_path}/ui/signup?selected_tab=webauthn\"\n            />\n            <input type=\"hidden\" name=\"provider\" value=\"{provider_name}\" />\n            <input type=\"hidden\" name=\"callback_url\" value=\"{redirect_to}\" />\n            <input\n                type=\"hidden\"\n                name=\"verify_url\"\n                value=\"{base_path}/ui/verify\"\n            />\n            {render_base_email_form(\n                id=\"webauthn-email\", challenge=challenge, email=email\n            )}\n            {button(\"Sign Up\", id=\"webauthn-signup\")}\n        </form>\n    \"\"\"\n\n\ndef render_magic_link_signup_form(\n    *,\n    challenge: str,\n    email: str | None = None,\n    base_path: str,\n    provider_name: str,\n    callback_url: str | None = None,\n    verification_method: str = \"Link\",\n) -> str:\n    \"\"\"Render a complete magic link/OTC signup form.\"\"\"\n    tab_label = get_magic_link_tab_label(verification_method)\n    callback_field = (\n        f\"\"\"\n            <input type=\"hidden\" name=\"callback_url\" value=\"{callback_url}\" />\n        \"\"\"\n        if verification_method == \"Link\"\n        else \"\"\n    )\n\n    return f\"\"\"\n        <form\n            method=\"post\"\n            action=\"../magic-link/register\"\n            novalidate\n        >\n            <input type=\"hidden\" name=\"provider\" value=\"{provider_name}\" />\n            <input\n                type=\"hidden\"\n                name=\"redirect_to\"\n                value=\"{base_path}/ui/magic-link-sent\"\n            />\n            <input\n                type=\"hidden\"\n                name=\"redirect_on_failure\"\n                value=\"{base_path}/ui/signup?selected_tab=magic_link\"\n            />\n            {callback_field}\n            {render_base_email_form(\n                id=\"magic-link-email\", challenge=challenge, email=email\n            )}\n            {button(f\"Sign Up with {tab_label}\", id=\"magic-link-signup\")}\n        </form>\n    \"\"\"\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/ui/util.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport re\n\n# Colour utils\n\nhex_color_regexp = re.compile(r'[0-9a-fA-F]{6}')\n\n\ndef get_colour_vars(bg_hex: str) -> str:\n    bg_rgb = hex_to_rgb(bg_hex)\n    bg_hsl = rgb_to_hsl(*bg_rgb)\n    luma = rgb_to_luma(*bg_rgb)\n    luma_dark = luma < 0.6\n\n    text_color = hsl_to_rgb(\n        bg_hsl[0], bg_hsl[1], min(90 if luma_dark else 35, bg_hsl[2])\n    )\n    dark_text_color = hsl_to_rgb(bg_hsl[0], bg_hsl[1], max(60, bg_hsl[2]))\n\n    return f'''--accent-bg-color: #{bg_hex};\n        --accent-bg-text-color: #{rgb_to_hex(\n            *hsl_to_rgb(\n                bg_hsl[0],\n                bg_hsl[1],\n                95 if luma_dark else max(10, min(25, luma * 100 - 60))\n            )\n        )};\n        --accent-bg-hover-color: #{rgb_to_hex(\n            *hsl_to_rgb(\n                bg_hsl[0], bg_hsl[1], bg_hsl[2] + (5 if luma_dark else -5)\n            )\n        )};\n        --accent-text-color: #{rgb_to_hex(*text_color)};\n        --accent-text-dark-color: #{rgb_to_hex(*dark_text_color)};\n        --accent-focus-color: rgba({','.join(\n            str(c) for c in text_color)},0.6);\n        --accent-focus-dark-color: rgba({','.join(\n            str(c) for c in dark_text_color)},0.6);'''\n\n\ndef hex_to_rgb(hex: str) -> tuple[float, float, float]:\n    return (\n        int(hex[0:2], base=16),\n        int(hex[2:4], base=16),\n        int(hex[4:6], base=16),\n    )\n\n\ndef rgb_to_hex(r: float, g: float, b: float) -> str:\n    return '%02x%02x%02x' % (int(r), int(g), int(b))\n\n\ndef rgb_to_luma(r: float, g: float, b: float) -> float:\n    return (r * 0.299 + g * 0.587 + b * 0.114) / 255\n\n\ndef rgb_to_hsl(r: float, g: float, b: float) -> tuple[float, float, float]:\n    r /= 255\n    g /= 255\n    b /= 255\n    l = max(r, g, b)\n    s = l - min(r, g, b)\n    h = (\n        (\n            ((g - b) / s)\n            if l == r\n            else (2 + (b - r) / s) if l == g else (4 + (r - g) / s)\n        )\n        if s != 0\n        else 0\n    )\n    return (\n        60 * h + 360 if 60 * h < 0 else 60 * h,\n        100\n        * (\n            (s / (2 * l - s) if l <= 0.5 else s / (2 - (2 * l - s)))\n            if s != 0\n            else 0\n        ),\n        (100 * (2 * l - s)) / 2,\n    )\n\n\ndef hsl_to_rgb(h: float, s: float, l: float) -> tuple[float, float, float]:\n    s /= 100\n    l /= 100\n    k = lambda n: (n + h / 30) % 12\n    a = s * min(l, 1 - l)\n    f = lambda n: l - a * max(-1, min(k(n) - 3, min(9 - k(n), 1)))\n    return (\n        round(255 * f(0)),\n        round(255 * f(8)),\n        round(255 * f(4)),\n    )\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/util.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport urllib.parse\nimport html\nimport logging\nimport asyncio\n\nfrom typing import (\n    overload,\n    Any,\n    cast,\n    Optional,\n    TYPE_CHECKING,\n    Callable,\n    Awaitable,\n    Mapping,\n)\n\nimport immutables\n\nfrom edb.common import retryloop\nfrom edb import errors as edb_errors\n\nfrom edb.server import config as edb_config, auth as jwt_auth\nfrom edb.server.config.types import CompositeConfigType\nfrom edb.server.protocol import execute\n\nfrom . import errors, config\n\nif TYPE_CHECKING:\n    from edb.server import tenant as edbtenant\n    from edb.server.dbview import dbview\n    from edb.server import defines as edbdef\n\n\nlogger = logging.getLogger('edb.server.ext.auth')\n\n# Cache JWKSets for 10 minutes\njwtset_cache = jwt_auth.JWKSetCache(60 * 10)\n\n\nasync def json_query_no_retry(\n    db: dbview.Database,\n    query: str,\n    *,\n    variables: Mapping[str, Any] = immutables.Map(),\n    tx_isolation: edbdef.TxIsolationLevel | None = None,\n    role_name: str | None = None,\n) -> bytes:\n    try:\n        return await execute.parse_execute_json(\n            db,\n            query,\n            variables=variables,\n            tx_isolation=tx_isolation,\n            role_name=role_name,\n            cached_globally=True,\n            query_tag='gel/auth',\n        )\n    except Exception as e:\n        raise (await execute.interpret_error(e, db)) from None\n\n\nasync def json_query(\n    db: dbview.Database,\n    query: str,\n    *,\n    variables: Mapping[str, Any] = immutables.Map(),\n    tx_isolation: edbdef.TxIsolationLevel | None = None,\n    role_name: str | None = None,\n    retry_timeout: float = 5.0,\n) -> bytes:\n    # TODO: Should we move the retry into a function in execute instead?\n    rloop = retryloop.RetryLoop(\n        backoff=retryloop.exp_backoff(),\n        timeout=retry_timeout,\n        ignore=(edb_errors.TransactionConflictError,),\n    )\n    async for iteration in rloop:\n        async with iteration:\n            return await json_query_no_retry(\n                db,\n                query,\n                variables=variables,\n                tx_isolation=tx_isolation,\n                role_name=role_name,\n            )\n    raise AssertionError('retryloop is broken')\n\n\ndef maybe_get_config_unchecked(db: edbtenant.dbview.Database, key: str) -> Any:\n    return edb_config.lookup(key, db.db_config, spec=db.user_config_spec)\n\n\n@overload\ndef maybe_get_config[T](\n    db: Any, key: str, expected_type: type[T]\n) -> T | None: ...\n\n\n@overload\ndef maybe_get_config(db: Any, key: str) -> str | None: ...\n\n\ndef maybe_get_config(\n    db: Any, key: str, expected_type: type[object] = str\n) -> object:\n    value = maybe_get_config_unchecked(db, key)\n\n    if value is None:\n        return None\n\n    if not isinstance(value, expected_type):\n        raise TypeError(\n            f\"Config value `{key}` must be {expected_type.__name__}, got \"\n            f\"{type(value).__name__}\"\n        )\n\n    return value\n\n\n@overload\ndef get_config[T](db: Any, key: str, expected_type: type[T]) -> T: ...\n\n\n@overload\ndef get_config(db: Any, key: str) -> str: ...\n\n\ndef get_config(db: Any, key: str, expected_type: type[object] = str) -> object:\n    value = maybe_get_config(db, key, expected_type)\n    if value is None:\n        raise errors.MissingConfiguration(\n            key=key,\n            description=\"Missing configuration value\",\n        )\n    return value\n\n\ndef get_config_unchecked(db: Any, key: str) -> Any:\n    value = maybe_get_config_unchecked(db, key)\n    if value is None:\n        raise errors.MissingConfiguration(\n            key=key,\n            description=\"Missing configuration value\",\n        )\n    return value\n\n\ndef get_config_typename(config_value: edb_config.SettingValue) -> str:\n    return config_value._tspec.name  # type: ignore\n\n\ndef escape_and_truncate(input_str: str | None, max_len: int) -> str | None:\n    if input_str is None:\n        return None\n    trunc = (\n        f\"{input_str[:max_len]}...\" if len(input_str) > max_len else input_str\n    )\n    return html.escape(trunc)\n\n\ndef get_app_details_config(db: Any) -> config.AppDetailsConfig:\n    ui_config = cast(\n        Optional[config.UIConfig],\n        maybe_get_config(db, \"ext::auth::AuthConfig::ui\", CompositeConfigType),\n    )\n\n    return config.AppDetailsConfig(\n        app_name=escape_and_truncate(\n            maybe_get_config(db, \"ext::auth::AuthConfig::app_name\")\n            or (ui_config.app_name if ui_config else None),\n            100,\n        ),\n        logo_url=escape_and_truncate(\n            maybe_get_config(db, \"ext::auth::AuthConfig::logo_url\")\n            or (ui_config.logo_url if ui_config else None),\n            2000,\n        ),\n        dark_logo_url=escape_and_truncate(\n            maybe_get_config(db, \"ext::auth::AuthConfig::dark_logo_url\")\n            or (ui_config.dark_logo_url if ui_config else None),\n            2000,\n        ),\n        brand_color=escape_and_truncate(\n            maybe_get_config(db, \"ext::auth::AuthConfig::brand_color\")\n            or (ui_config.brand_color if ui_config else None),\n            8,\n        ),\n    )\n\n\ndef join_url_params(url: str, params: dict[str, str]) -> str:\n    parsed_url = urllib.parse.urlparse(url)\n    query_params = {\n        **urllib.parse.parse_qs(parsed_url.query),\n        **{key: [val] for key, val in params.items()},\n    }\n    new_query_params = urllib.parse.urlencode(query_params, doseq=True)\n    return parsed_url._replace(query=new_query_params).geturl()\n\n\nasync def get_remote_jwtset(\n    url: str,\n    fetch_lambda: Callable[[str], Awaitable[jwt_auth.JWKSet]],\n) -> jwt_auth.JWKSet:\n    \"\"\"\n    Get a JWKSet from the cache, or fetch it from the given URL if it's not in\n    the cache.\n    \"\"\"\n    is_fresh, jwtset = jwtset_cache.get(url)\n    match (is_fresh, jwtset):\n        case (_, None):\n            jwtset = await fetch_lambda(url)\n            jwtset_cache.set(url, jwtset)\n        case (True, jwtset):\n            pass\n        case _:\n            # Run fetch in background to refresh cache\n            async def refresh_cache(url: str) -> None:\n                try:\n                    new_jwtset = await fetch_lambda(url)\n                    jwtset_cache.set(url, new_jwtset)\n                except Exception:\n                    logger.exception(\n                        f\"Failed to refresh JWKSet cache for {url}\"\n                    )\n\n            asyncio.create_task(refresh_cache(url))\n\n    assert jwtset is not None\n    return jwtset\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/webauthn.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nimport dataclasses\nimport base64\nimport json\nimport webauthn\n\nfrom typing import Optional, TYPE_CHECKING\nfrom webauthn.helpers import (\n    parse_authentication_credential_json,\n    structs as webauthn_structs,\n    exceptions as webauthn_exceptions,\n)\n\nfrom edb.errors import ConstraintViolationError\n\nfrom . import config, data, errors, util, local\n\nif TYPE_CHECKING:\n    from edb.server import tenant as edbtenant\n\n\n@dataclasses.dataclass(repr=False)\nclass WebAuthnRegistrationChallenge:\n    \"\"\"\n    Object that represents the ext::auth::WebAuthnRegistrationChallenge type\n    \"\"\"\n\n    id: str\n    challenge: bytes\n    user_handle: bytes\n    email: str\n\n\nclass Client(local.Client):\n    def __init__(self, db: edbtenant.dbview.Database):\n        self.db = db\n        self.provider = self._get_provider()\n        self.app_name = self._get_app_name()\n        self.config = self._get_provider_config(\"builtin::local_webauthn\")\n\n    def _get_provider(self) -> config.WebAuthnProvider:\n        provider_name = \"builtin::local_webauthn\"\n        provider_client_config = util.get_config(\n            self.db, \"ext::auth::AuthConfig::providers\", frozenset\n        )\n        for cfg in provider_client_config:\n            if cfg.name == provider_name:\n                return config.WebAuthnProvider(\n                    name=cfg.name,\n                    relying_party_origin=cfg.relying_party_origin,\n                    require_verification=cfg.require_verification,\n                    verification_method=cfg.verification_method,\n                )\n\n        raise errors.MissingConfiguration(\n            provider_name, f\"Provider is not configured\"\n        )\n\n    def _get_app_name(self) -> Optional[str]:\n        app_config = util.get_app_details_config(self.db)\n        return app_config.app_name\n\n    async def create_registration_options_for_email(\n        self,\n        email: str,\n    ) -> tuple[str, bytes]:\n        maybe_user_handle = await self._maybe_get_existing_user_handle(\n            email=email\n        )\n        registration_options = webauthn.generate_registration_options(\n            rp_id=self.provider.relying_party_id,\n            rp_name=(self.app_name or self.provider.relying_party_origin),\n            user_name=email,\n            user_display_name=email,\n            user_id=maybe_user_handle,\n        )\n\n        await self._create_registration_challenge(\n            email=email,\n            challenge=registration_options.challenge,\n            user_handle=registration_options.user.id,\n        )\n\n        return (\n            base64.urlsafe_b64encode(registration_options.user.id).decode(),\n            webauthn.options_to_json(registration_options).encode(),\n        )\n\n    async def _maybe_get_existing_user_handle(\n        self,\n        email: str,\n    ) -> Optional[bytes]:\n        result = await util.json_query(\n            self.db,\n            \"\"\"\nwith\n    email := <str>$email,\n    factors := (\n        select ext::auth::WebAuthnFactor\n        filter .email = email\n    ),\nselect assert_single((select distinct factors.user_handle));\"\"\",\n            variables={\n                \"email\": email,\n            },\n        )\n\n        result_json = json.loads(result.decode())\n        if len(result_json) == 0:\n            return None\n        else:\n            return base64.b64decode(result_json[0])\n\n    async def _create_registration_challenge(\n        self,\n        email: str,\n        challenge: bytes,\n        user_handle: bytes,\n    ) -> None:\n        await util.json_query(\n            self.db,\n            \"\"\"\nwith\n    challenge := <bytes>$challenge,\n    user_handle := <bytes>$user_handle,\n    email := <str>$email,\ninsert ext::auth::WebAuthnRegistrationChallenge {\n    challenge := challenge,\n    user_handle := user_handle,\n    email := email,\n}\"\"\",\n            variables={\n                \"challenge\": challenge,\n                \"user_handle\": user_handle,\n                \"email\": email,\n            },\n        )\n\n    async def register(\n        self,\n        credentials: str,\n        email: str,\n        user_handle: bytes,\n    ) -> data.EmailFactor:\n        registration_challenge = await self._get_registration_challenge(\n            email=email,\n            user_handle=user_handle,\n        )\n        await self._delete_registration_challenges(\n            email=email,\n            user_handle=user_handle,\n        )\n\n        registration_verification = webauthn.verify_registration_response(\n            credential=credentials,\n            expected_challenge=registration_challenge.challenge,\n            expected_rp_id=self.provider.relying_party_id,\n            expected_origin=self.provider.relying_party_origin,\n        )\n\n        try:\n            result = await util.json_query(\n                self.db,\n                \"\"\"\nwith\n    email := <str>$email,\n    user_handle := <bytes>$user_handle,\n    credential_id := <bytes>$credential_id,\n    public_key := <bytes>$public_key,\n    identity := (insert ext::auth::LocalIdentity {\n        issuer := \"local\",\n        subject := \"\",\n    }),\n    factor := (insert ext::auth::WebAuthnFactor {\n        email := email,\n        user_handle := user_handle,\n        credential_id := credential_id,\n        public_key := public_key,\n        identity := identity,\n    }),\nselect factor { ** };\"\"\",\n                variables={\n                    \"email\": email,\n                    \"user_handle\": user_handle,\n                    \"credential_id\": registration_verification.credential_id,\n                    \"public_key\": (\n                        registration_verification.credential_public_key\n                    ),\n                },\n            )\n        except ConstraintViolationError:\n            raise errors.UserAlreadyRegistered()\n\n        result_json = json.loads(result.decode())\n        assert len(result_json) == 1\n\n        factor_dict = result_json[0]\n        local_identity = data.LocalIdentity(**factor_dict.pop(\"identity\"))\n        return data.WebAuthnFactor(**factor_dict, identity=local_identity)\n\n    async def _get_registration_challenge(\n        self,\n        email: str,\n        user_handle: bytes,\n    ) -> WebAuthnRegistrationChallenge:\n        result = await util.json_query(\n            self.db,\n            \"\"\"\nwith\n    email := <str>$email,\n    user_handle := <bytes>$user_handle,\nselect ext::auth::WebAuthnRegistrationChallenge {\n    id,\n    challenge,\n    user_handle,\n    email,\n}\nfilter .email = email and .user_handle = user_handle;\"\"\",\n            variables={\n                \"email\": email,\n                \"user_handle\": user_handle,\n            },\n        )\n        result_json = json.loads(result.decode())\n        assert len(result_json) == 1\n        challenge_dict = result_json[0]\n\n        return WebAuthnRegistrationChallenge(\n            id=challenge_dict[\"id\"],\n            challenge=base64.b64decode(challenge_dict[\"challenge\"]),\n            user_handle=base64.b64decode(challenge_dict[\"user_handle\"]),\n            email=challenge_dict[\"email\"],\n        )\n\n    async def _delete_registration_challenges(\n        self,\n        email: str,\n        user_handle: bytes,\n    ) -> None:\n        await util.json_query(\n            self.db,\n            \"\"\"\nwith\n    email := <str>$email,\n    user_handle := <bytes>$user_handle,\ndelete ext::auth::WebAuthnRegistrationChallenge\nfilter .email = email and .user_handle = user_handle;\"\"\",\n            variables={\n                \"email\": email,\n                \"user_handle\": user_handle,\n            },\n        )\n\n    async def create_authentication_options_for_email(\n        self,\n        *,\n        webauthn_provider: config.WebAuthnProvider,\n        email: str,\n    ) -> tuple[str, bytes]:\n        # Find credential IDs by email\n        result = await util.json_query(\n            self.db,\n            \"\"\"\nselect ext::auth::WebAuthnFactor {\n    user_handle,\n    credential_id,\n}\nfilter .email = <str>$email;\"\"\",\n            variables={\n                \"email\": email,\n            },\n        )\n        result_json = json.loads(result.decode())\n        if len(result_json) == 0:\n            raise errors.WebAuthnAuthenticationFailed(\n                \"No WebAuthn credentials found for this email.\"\n            )\n\n        user_handles: set[str] = {x[\"user_handle\"] for x in result_json}\n        assert (\n            len(user_handles) == 1\n        ), \"Found WebAuthn multiple user handles for the same email.\"\n\n        user_handle = base64.b64decode(result_json[0][\"user_handle\"])\n\n        credential_ids = [\n            webauthn_structs.PublicKeyCredentialDescriptor(\n                base64.b64decode(x[\"credential_id\"])\n            )\n            for x in result_json\n        ]\n\n        registration_options = webauthn.generate_authentication_options(\n            rp_id=webauthn_provider.relying_party_id,\n            allow_credentials=credential_ids,\n        )\n\n        await util.json_query(\n            self.db,\n            \"\"\"\nwith\n    challenge := <bytes>$challenge,\n    user_handle := <bytes>$user_handle,\n    email := <str>$email,\n    factors := (\n        assert_exists((\n            select ext::auth::WebAuthnFactor\n            filter .user_handle = user_handle\n            and .email = email\n        ))\n    )\ninsert ext::auth::WebAuthnAuthenticationChallenge {\n    challenge := challenge,\n    factors := factors,\n}\nunless conflict on .factors\nelse (\n    update ext::auth::WebAuthnAuthenticationChallenge\n    set {\n        challenge := challenge\n    }\n);\"\"\",\n            variables={\n                \"challenge\": registration_options.challenge,\n                \"user_handle\": user_handle,\n                \"email\": email,\n            },\n        )\n\n        return (\n            base64.urlsafe_b64encode(user_handle).decode(),\n            webauthn.options_to_json(registration_options).encode(),\n        )\n\n    async def is_email_verified(\n        self,\n        email: str,\n        assertion: str,\n    ) -> bool:\n        credential = parse_authentication_credential_json(assertion)\n\n        result = await util.json_query(\n            self.db,\n            \"\"\"\nwith\n    email := <str>$email,\n    credential_id := <bytes>$credential_id,\n    factor := assert_single((\n        select ext::auth::WebAuthnFactor\n        filter .email = email\n        and credential_id = credential_id\n    )),\nselect (factor.verified_at <= std::datetime_current()) ?? false;\"\"\",\n            variables={\n                \"email\": email,\n                \"credential_id\": credential.raw_id,\n            },\n        )\n        result_json = json.loads(result.decode())\n        return bool(result_json[0])\n\n    async def _get_authentication_challenge(\n        self,\n        email: str,\n        credential_id: bytes,\n    ) -> data.WebAuthnAuthenticationChallenge:\n        result = await util.json_query(\n            self.db,\n            \"\"\"\nwith\n    email := <str>$email,\n    credential_id := <bytes>$credential_id,\nselect ext::auth::WebAuthnAuthenticationChallenge {\n    id,\n    created_at,\n    modified_at,\n    challenge,\n    factors: {\n        id,\n        created_at,\n        modified_at,\n        email,\n        verified_at,\n        user_handle,\n        credential_id,\n        public_key,\n        identity: {\n            created_at,\n            modified_at,\n            id,\n            issuer,\n            subject,\n        }\n    },\n}\nfilter .factors.email = email and .factors.credential_id = credential_id;\"\"\",\n            variables={\n                \"email\": email,\n                \"credential_id\": credential_id,\n            },\n        )\n        result_json = json.loads(result.decode())\n        if len(result_json) == 0:\n            raise errors.WebAuthnAuthenticationFailed(\n                \"Could not find a challenge. Please retry authentication.\"\n            )\n        elif len(result_json) > 1:\n            raise errors.WebAuthnAuthenticationFailed(\n                \"Multiple challenges found. Please retry authentication.\"\n            )\n        return data.WebAuthnAuthenticationChallenge(**result_json[0])\n\n    async def _delete_authentication_challenges(\n        self,\n        email: str,\n        credential_id: bytes,\n    ) -> None:\n        await util.json_query(\n            self.db,\n            \"\"\"\nwith\n    email := <str>$email,\n    credential_id := <bytes>$credential_id,\ndelete ext::auth::WebAuthnAuthenticationChallenge\nfilter .factors.email = email and .factors.credential_id = credential_id;\"\"\",\n            variables={\n                \"email\": email,\n                \"credential_id\": credential_id,\n            },\n        )\n\n    async def authenticate(\n        self,\n        *,\n        email: str,\n        assertion: str,\n    ) -> data.LocalIdentity:\n        credential = parse_authentication_credential_json(assertion)\n\n        authentication_challenge = await self._get_authentication_challenge(\n            email=email,\n            credential_id=credential.raw_id,\n        )\n        await self._delete_authentication_challenges(\n            email=email,\n            credential_id=credential.raw_id,\n        )\n\n        factor = next(\n            (\n                f\n                for f in authentication_challenge.factors\n                if f.credential_id == credential.raw_id\n            ),\n            None,\n        )\n        assert factor is not None, \"Missing factor for the given credential.\"\n\n        try:\n            webauthn.verify_authentication_response(\n                credential=credential,\n                expected_challenge=authentication_challenge.challenge,\n                credential_public_key=factor.public_key,\n                credential_current_sign_count=0,\n                expected_rp_id=self.provider.relying_party_id,\n                expected_origin=self.provider.relying_party_origin,\n            )\n        except webauthn_exceptions.InvalidAuthenticationResponse:\n            raise errors.WebAuthnAuthenticationFailed(\n                \"Invalid authentication response. Please retry authentication.\"\n            )\n\n        return factor.identity\n\n    async def get_email_factor_by_credential_id(\n        self,\n        credential_id: bytes,\n    ) -> Optional[data.EmailFactor]:\n        result = await util.json_query(\n            self.db,\n            \"\"\"\nwith\n    credential_id := <bytes>$credential_id,\nselect ext::auth::WebAuthnFactor {\n    id,\n    created_at,\n    modified_at,\n    email,\n    verified_at,\n    identity: {*},\n} filter .credential_id = credential_id;\"\"\",\n            variables={\n                \"credential_id\": credential_id,\n            },\n        )\n        result_json = json.loads(result.decode())\n        if len(result_json) == 0:\n            return None\n        elif len(result_json) > 1:\n            # This should never happen given the exclusive constraint\n            raise errors.WebAuthnAuthenticationFailed(\n                \"Multiple WebAuthn factors found for the same credential ID.\"\n            )\n        return data.EmailFactor(**result_json[0])\n\n    def _get_provider_config(\n        self, provider_name: str\n    ) -> config.WebAuthnProvider:\n        provider_client_config = util.get_config(\n            self.db, \"ext::auth::AuthConfig::providers\", frozenset\n        )\n        for cfg in provider_client_config:\n            if cfg.name == provider_name:\n                return config.WebAuthnProvider(\n                    name=cfg.name,\n                    relying_party_origin=cfg.relying_party_origin,\n                    require_verification=cfg.require_verification,\n                    verification_method=cfg.verification_method,\n                )\n\n        raise errors.MissingConfiguration(\n            provider_name, f\"Provider is not configured\"\n        )\n"
  },
  {
    "path": "edb/server/protocol/auth_ext/webhook.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport dataclasses\nimport typing\nimport abc\nimport datetime\nimport json\nimport hmac\nimport hashlib\nimport uuid\n\n\nfrom . import util\n\nif typing.TYPE_CHECKING:\n    from edb.server import tenant as edbtenant\n\n\n@dataclasses.dataclass\nclass Event(abc.ABC):\n    event_type: str\n    event_id: str\n    timestamp: datetime.datetime\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"timestamp={self.timestamp!r}, \"\n            f\"event_id={self.event_id!r}\"\n            \")\"\n        )\n\n\n@dataclasses.dataclass\nclass HasIdentity(abc.ABC):\n    identity_id: str\n\n\n@dataclasses.dataclass\nclass HasEmailFactor(abc.ABC):\n    email_factor_id: str\n\n\n@dataclasses.dataclass\nclass IdentityCreated(Event, HasIdentity):\n    event_type: typing.Literal[\"IdentityCreated\"] = dataclasses.field(\n        default=\"IdentityCreated\",\n        init=False,\n    )\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"timestamp={self.timestamp}, \"\n            f\"event_id={self.event_id}, \"\n            f\"identity_id={self.identity_id}\"\n            \")\"\n        )\n\n\n@dataclasses.dataclass\nclass IdentityAuthenticated(Event, HasIdentity):\n    event_type: typing.Literal[\"IdentityAuthenticated\"] = dataclasses.field(\n        default=\"IdentityAuthenticated\",\n        init=False,\n    )\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"timestamp={self.timestamp}, \"\n            f\"event_id={self.event_id}, \"\n            f\"identity_id={self.identity_id}\"\n            \")\"\n        )\n\n\n@dataclasses.dataclass\nclass EmailFactorCreated(Event, HasIdentity, HasEmailFactor):\n    event_type: typing.Literal[\"EmailFactorCreated\"] = dataclasses.field(\n        default=\"EmailFactorCreated\",\n        init=False,\n    )\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"timestamp={self.timestamp}, \"\n            f\"event_id={self.event_id}, \"\n            f\"identity_id={self.identity_id}, \"\n            f\"email_factor_id={self.email_factor_id}\"\n            \")\"\n        )\n\n\n@dataclasses.dataclass\nclass EmailVerificationRequested(Event, HasIdentity, HasEmailFactor):\n    event_type: typing.Literal[\"EmailVerificationRequested\"] = (\n        dataclasses.field(\n            default=\"EmailVerificationRequested\",\n            init=False,\n        )\n    )\n    verification_token: str\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"timestamp={self.timestamp}, \"\n            f\"event_id={self.event_id}, \"\n            f\"identity_id={self.identity_id}, \"\n            f\"email_factor_id={self.email_factor_id}\"\n            \")\"\n        )\n\n\n@dataclasses.dataclass\nclass EmailVerified(Event, HasIdentity, HasEmailFactor):\n    event_type: typing.Literal[\"EmailVerified\"] = dataclasses.field(\n        default=\"EmailVerified\",\n        init=False,\n    )\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"timestamp={self.timestamp}, \"\n            f\"event_id={self.event_id}, \"\n            f\"identity_id={self.identity_id}, \"\n            f\"email_factor_id={self.email_factor_id}\"\n            \")\"\n        )\n\n\n@dataclasses.dataclass\nclass PasswordResetRequested(Event, HasIdentity, HasEmailFactor):\n    event_type: typing.Literal[\"PasswordResetRequested\"] = dataclasses.field(\n        default=\"PasswordResetRequested\",\n        init=False,\n    )\n    reset_token: str\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"timestamp={self.timestamp}, \"\n            f\"event_id={self.event_id}, \"\n            f\"identity_id={self.identity_id}, \"\n            f\"email_factor_id={self.email_factor_id}\"\n            \")\"\n        )\n\n\n@dataclasses.dataclass\nclass MagicLinkRequested(Event, HasIdentity, HasEmailFactor):\n    event_type: typing.Literal[\"MagicLinkRequested\"] = dataclasses.field(\n        default=\"MagicLinkRequested\",\n        init=False,\n    )\n    magic_link_token: str\n    magic_link_url: str\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"timestamp={self.timestamp}, \"\n            f\"event_id={self.event_id}, \"\n            f\"identity_id={self.identity_id}, \"\n            f\"email_factor_id={self.email_factor_id}\"\n            \")\"\n        )\n\n\n@dataclasses.dataclass\nclass OneTimeCodeRequested(Event, HasIdentity, HasEmailFactor):\n    event_type: typing.Literal[\"OneTimeCodeRequested\"] = dataclasses.field(\n        default=\"OneTimeCodeRequested\",\n        init=False,\n    )\n    otc_id: str\n    one_time_code: str\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"timestamp={self.timestamp}, \"\n            f\"event_id={self.event_id}, \"\n            f\"identity_id={self.identity_id}, \"\n            f\"email_factor_id={self.email_factor_id}\"\n            \")\"\n        )\n\n\n@dataclasses.dataclass\nclass OneTimeCodeVerified(Event, HasIdentity, HasEmailFactor):\n    event_type: typing.Literal[\"OneTimeCodeVerified\"] = dataclasses.field(\n        default=\"OneTimeCodeVerified\",\n        init=False,\n    )\n    otc_id: str\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"timestamp={self.timestamp}, \"\n            f\"event_id={self.event_id}, \"\n            f\"identity_id={self.identity_id}, \"\n            f\"email_factor_id={self.email_factor_id}\"\n            \")\"\n        )\n\n\nclass WebhookEncoder(json.JSONEncoder):\n    def default(self, obj: typing.Any) -> typing.Any:\n        if isinstance(obj, datetime.datetime):\n            return obj.isoformat()\n        if isinstance(obj, uuid.UUID):\n            return str(obj)\n        return super().default(obj)\n\n\nasync def send(\n    db: edbtenant.dbview.Database,\n    url: str,\n    secret: typing.Optional[str],\n    event: Event,\n) -> str:\n    body = json.dumps(\n        dataclasses.asdict(event), cls=WebhookEncoder\n    ).encode()\n    headers = [(\"Content-Type\", \"application/json\")]\n    if secret:\n        signature = hmac.new(\n            secret.encode(), body, hashlib.sha256\n        ).hexdigest()\n        headers.append((\"x-ext-auth-signature-sha256\", signature))\n\n    result_json = await util.json_query(\n        db,\n        \"\"\"\nwith\n    nh as module std::net::http,\n    net as module std::net,\n\n    # n.b. workaround for bug in parse_execute_json\n    url := <required str>$url,\n    headers := <array<tuple<str, str>>>$headers,\n    body := <bytes>$body,\n\n    REQUEST := (\n        nh::schedule_request(\n            url,\n            method := nh::Method.POST,\n            headers := headers,\n            body := body,\n        )\n    ),\nselect REQUEST;\n\"\"\",\n        variables={\n            \"url\": url,\n            \"body\": body,\n            \"headers\": headers,\n        },\n    )\n    result = json.loads(result_json)\n\n    match result[0][\"id\"]:\n        case str(id):\n            return id\n        case _:\n            raise ValueError(\n                \"Expected single result with 'id' string property, got \"\n                f\"{result[0]!r}\"\n            )\n"
  },
  {
    "path": "edb/server/protocol/auth_helpers.pxd",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ncdef extract_token_from_auth_data(bytes auth_data)\ncdef auth_jwt(tenant, prefixed_token, str user, str dbname)\ncdef scram_get_verifier(tenant, str user)\ncdef parse_basic_auth(str auth_payload)\ncdef extract_http_user(scheme, auth_payload, params)\ncdef auth_basic(tenant, str username, str password)\ncdef auth_mtls(transport)\ncdef auth_mtls_with_user(transport, str username)\n"
  },
  {
    "path": "edb/server/protocol/auth_helpers.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Authentication code that is shared between HTTP and binary protocols\"\"\"\n\nfrom edgedb import scram\n\nimport base64\nimport hashlib\nimport json\nimport logging\n\nfrom edb import errors\nfrom edb.server.auth import validate_gel_token\n\ncdef object logger = logging.getLogger('edb.server')\n\n\ncdef extract_token_from_auth_data(auth_data: bytes):\n    header_value = auth_data.decode(\"ascii\")\n    scheme, _, payload = header_value.partition(\" \")\n    return scheme.lower(), payload.strip()\n\n\ncdef auth_jwt(tenant, prefixed_token: str | None, user: str, dbname: str):\n    if not prefixed_token:\n        raise errors.AuthenticationError(\n            'authentication failed: no authorization data provided')\n\n    key = tenant.server.get_jws_key()\n    if err := validate_gel_token(key, prefixed_token, user, dbname, tenant.get_instance_name()):\n        raise errors.AuthenticationError(str(err))\n\n    # Ensure it's a valid role, but check after the JWT is validated\n    role = tenant.get_roles().get(user)\n    if role is None:\n        raise errors.AuthenticationError('authentication failed')\n\n\ncdef scram_get_verifier(tenant, user: str):\n    roles = tenant.get_roles()\n\n    rolerec = roles.get(user)\n    if rolerec is not None:\n        verifier_string = rolerec['password']\n        if verifier_string is not None:\n            try:\n                verifier = scram.parse_verifier(verifier_string)\n            except ValueError:\n                raise errors.AuthenticationError(\n                    f'invalid SCRAM verifier for user {user!r}') from None\n            is_mock = False\n            return verifier, is_mock\n\n    # To avoid revealing the validity of the submitted user name,\n    # generate a mock verifier using a salt derived from the\n    # received user name and the cluster mock auth nonce.\n    # The same approach is taken by Postgres.\n    nonce = tenant.get_instance_data('mock_auth_nonce')\n    salt = hashlib.sha256(nonce.encode() + user.encode()).digest()\n\n    verifier = scram.SCRAMVerifier(\n        mechanism='SCRAM-SHA-256',\n        iterations=scram.DEFAULT_ITERATIONS,\n        salt=salt[:scram.DEFAULT_SALT_LENGTH],\n        stored_key=b'',\n        server_key=b'',\n    )\n    is_mock = True\n    return verifier, is_mock\n\n\ndef scram_verify_password(password: str, verifier: object) -> bool:\n    \"\"\"Check the given password against a verifier.\n\n    Returns True if the password is OK, False otherwise.\n    \"\"\"\n\n    # adapted from edgedb-python's scram.verify_password but made to\n    # take a verifier object instead of a string\n\n    bpassword = scram.saslprep(password).encode('utf-8')\n    salted_password = scram.get_salted_password(\n        bpassword, verifier.salt, verifier.iterations)\n    computed_key = scram.get_server_key(salted_password)\n    return verifier.server_key == computed_key\n\n\ncdef parse_basic_auth(auth_payload: str):\n    try:\n        decoded = base64.b64decode(auth_payload).decode('utf-8')\n    except ValueError:\n        raise errors.AuthenticationError(\n            'authentication failed: malformed authentication') from None\n    username, colon, password = decoded.partition(':')\n    if colon != ':':\n        raise errors.AuthenticationError(\n            'authentication failed: malformed authentication')\n    return username, password\n\n\ncdef extract_http_user(scheme, auth_payload, params):\n    \"\"\"Extract the username from an HTTP request.\n\n    Raises an AuthenticationError if something is too malformed.\n\n    Returns the username, along with the password, if appropriate.\n    (To avoid needing to parse the packet twice.)\n    \"\"\"\n\n    if scheme == 'basic':\n        return parse_basic_auth(auth_payload)\n    else:\n        # Respect X-EdgeDB-User if present, but otherwise default to 'edgedb'\n        if params and b'user' in params:\n            username = params[b'user'].decode('ascii')\n        else:\n            username = 'edgedb'\n        return username, None\n\n\ncdef auth_basic(tenant, username: str, password: str):\n    verifier, mock_auth = scram_get_verifier(tenant, username)\n    if not scram_verify_password(password, verifier) or mock_auth:\n        raise errors.AuthenticationError('authentication failed')\n\n\ncdef auth_mtls(transport):\n    sslobj = transport.get_extra_info('ssl_object')\n    if sslobj is None:\n        raise errors.AuthenticationError(\n            \"mTLS authentication is not supported over plaintext transport\")\n    cert_data = sslobj.getpeercert()\n    if not cert_data:  # None or empty dict\n        # If --tls-client-ca-file is specified, the SSLContext used here would\n        # have done load_verify_locations() in `server/server.py`, and we will\n        # have a valid client certificate (non-empty dict) now if one was\n        # provided by the client and passed validation; empty dict otherwise.\n        # `None` just means the peer didn't send a client certificate.\n        raise errors.AuthenticationError(\n            \"valid client certificate required\")\n    return cert_data\n\n\ncdef auth_mtls_with_user(transport, str username):\n    cert_data = auth_mtls(transport)\n    try:\n        for rdn in cert_data[\"subject\"]:\n            if rdn[0][0] == 'commonName':\n                if rdn[0][1] == username:\n                    return\n    except Exception as ex:\n        raise errors.AuthenticationError(\n            \"bad client certificate\") from ex\n\n    raise errors.AuthenticationError(\n        f\"Common Name of client certificate doesn't match {username!r}\",\n    )\n"
  },
  {
    "path": "edb/server/protocol/binary.pxd",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ncimport cython\ncimport cpython\n\nfrom libc.stdint cimport int8_t, uint8_t, int16_t, uint16_t, \\\n                         int32_t, uint32_t, int64_t, uint64_t\n\nfrom edb.server.pgproto.pgproto cimport (\n    WriteBuffer,\n)\n\nfrom edb.server.dbview cimport dbview\nfrom edb.server.pgproto.debug cimport PG_DEBUG\nfrom edb.server.protocol cimport frontend\n\n\ncdef enum EdgeSeverity:\n    EDGE_SEVERITY_DEBUG = 20\n    EDGE_SEVERITY_INFO = 40\n    EDGE_SEVERITY_NOTICE = 60\n    EDGE_SEVERITY_WARNING = 80\n    EDGE_SEVERITY_ERROR = 120\n    EDGE_SEVERITY_FATAL = 200\n    EDGE_SEVERITY_PANIC = 255\n\n\ncdef enum EdgeConnectionStatus:\n    EDGECON_NEW = 0\n    EDGECON_STARTED = 1\n    EDGECON_OK = 2\n    EDGECON_BAD = 3\n\n\ncdef class EdgeConnection(frontend.FrontendConnection):\n\n    cdef:\n        EdgeConnectionStatus _con_status\n\n        readonly dbview.DatabaseConnectionView _dbview\n\n        object _startup_msg_waiter\n\n        dbview.CompiledQuery _last_anon_compiled\n        int64_t _last_anon_compiled_hash\n\n        bint query_cache_enabled\n\n        tuple protocol_version\n        tuple max_protocol\n        tuple min_protocol\n\n        object last_state\n        int last_state_id\n\n        bint _in_dump_restore\n\n        bytes _auth_data\n        dict  _conn_params\n\n    cdef inline dbview.DatabaseConnectionView get_dbview(self)\n\n    cdef parse_execute_request(self)\n    cdef parse_cardinality(self, bytes card)\n    cdef char render_cardinality(self, query_unit) except -1\n\n    cdef fallthrough(self)\n\n    cdef sync_status(self)\n\n    cdef WriteBuffer make_negotiate_protocol_version_msg(\n        self, tuple target_proto\n    )\n    cdef WriteBuffer make_command_data_description_msg(\n        self, dbview.CompiledQuery query\n    )\n    cdef WriteBuffer make_state_data_description_msg(self)\n    cdef WriteBuffer make_command_complete_msg(self, capabilities, status)\n\n    cdef inline ignore_headers(self)\n    cdef dict parse_headers(self)\n    cdef dict parse_annotations(self)\n    cdef inline ignore_annotations(self)\n    cdef get_checked_tag(self, dict annotations)\n\n    cdef write_status(self, bytes name, bytes value)\n    cdef write_edgedb_error(self, exc)\n\n    cdef write_log(self, EdgeSeverity severity, uint32_t code, str message)\n\n\n@cython.final\ncdef class VirtualTransport:\n    cdef:\n        WriteBuffer buf\n        bint closed\n        object transport\n"
  },
  {
    "path": "edb/server/protocol/binary.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport asyncio\nimport base64\nimport collections\nimport contextlib\nimport json\nimport logging\nimport time\nimport statistics\nimport traceback\nimport sys\n\ncimport cython\ncimport cpython\n\nfrom typing import Dict, List, Optional, Sequence, Tuple\nfrom edb.server.protocol cimport cpythonx\n\nfrom libc.stdint cimport int8_t, uint8_t, int16_t, uint16_t, \\\n                         int32_t, uint32_t, int64_t, uint64_t, \\\n                         UINT32_MAX\n\nimport immutables\n\nfrom edb import buildmeta\nfrom edb import edgeql\nfrom edb.edgeql import qltypes\nfrom edb.graphql import tokenizer as gql_tokenizer\n\nfrom edb.pgsql import parser as pgparser\nfrom edb.graphql import tokenizer as gql_tokenizer\n\nfrom edb.server.pgproto cimport hton\nfrom edb.server.pgproto.pgproto cimport (\n    WriteBuffer,\n    ReadBuffer,\n\n    FRBuffer,\n    frb_init,\n    frb_read,\n    frb_read_all,\n    frb_get_len,\n)\nfrom edb.server.pgproto.pgproto import UUID as pg_UUID\n\nfrom edb.server.dbview cimport dbview\n\nfrom edb.server import config\n\nfrom edb.server import args as srvargs\nfrom edb.server import compiler\nfrom edb.server import defines as edbdef\nfrom edb.server.compiler import errormech\nfrom edb.server.compiler import enums\nfrom edb.server.compiler import sertypes\nfrom edb.server.compiler cimport rpc\n\nfrom edb.server.protocol cimport auth_helpers\nfrom edb.server.protocol import execute\nfrom edb.server.protocol cimport frontend\nfrom edb.server.pgcon cimport pgcon\nfrom edb.server.pgcon import errors as pgerror\nfrom edb.server import metrics\n\nfrom edb.schema import objects as s_obj\n\nfrom edb import errors\nfrom edb.errors import base as base_errors, EdgeQLSyntaxError\nfrom edb.common import debug\n\nfrom edb.protocol import messages\n\n\nfrom edb import _graphql_rewrite\n\n\ninclude \"./consts.pxi\"\n\n\ncdef bytes EMPTY_TUPLE_UUID = s_obj.get_known_type_id('empty-tuple').bytes\n\ncdef uint64_t PROTO_CAPS = enums.Capability.PROTO_CAPS\n\ncdef object CARD_NO_RESULT = compiler.Cardinality.NO_RESULT\ncdef object CARD_AT_MOST_ONE = compiler.Cardinality.AT_MOST_ONE\ncdef object CARD_MANY = compiler.Cardinality.MANY\n\ncdef object FMT_NONE = compiler.OutputFormat.NONE\ncdef object FMT_BINARY = compiler.OutputFormat.BINARY\n\ncdef object LANG_EDGEQL = compiler.InputLanguage.EDGEQL\ncdef object LANG_SQL = compiler.InputLanguage.SQL\ncdef object LANG_GRAPHQL = compiler.InputLanguage.GRAPHQL\n\ncdef tuple DUMP_VER_MIN = (0, 7)\ncdef tuple DUMP_VER_MAX = edbdef.CURRENT_PROTOCOL\n\ncdef tuple MIN_PROTOCOL = edbdef.MIN_PROTOCOL\ncdef tuple CURRENT_PROTOCOL = edbdef.CURRENT_PROTOCOL\n\ncdef object logger = logging.getLogger('edb.server')\ncdef object log_metrics = logging.getLogger('edb.server.metrics')\n\nDEF QUERY_HEADER_DUMP_SECRETS = 0xFF10\n\n\ndef parse_catalog_version_header(value: bytes) -> uint64_t:\n    if len(value) != 8:\n        raise errors.BinaryProtocolError(\n            f'catalog version value must be exactly 8 bytes (got {len(value)})'\n        )\n    cdef uint64_t catver = hton.unpack_uint64(cpython.PyBytes_AS_STRING(value))\n    return catver\n\n\ncdef class EdgeConnection(frontend.FrontendConnection):\n    interface = \"edgeql\"\n\n    def __init__(\n        self,\n        server,\n        tenant,\n        *,\n        auth_data: bytes,\n        conn_params: dict[str, str] | None,\n        protocol_version: edbdef.ProtocolVersion = CURRENT_PROTOCOL,\n        **kwargs,\n    ):\n        super().__init__(server, tenant, **kwargs)\n        self._con_status = EDGECON_NEW\n\n        self._dbview = None\n\n        self._last_anon_compiled = None\n\n        self.query_cache_enabled = not (debug.flags.disable_qcache or\n                                        debug.flags.edgeql_compile)\n\n        self.protocol_version = protocol_version\n        self.min_protocol = MIN_PROTOCOL\n        self.max_protocol = CURRENT_PROTOCOL\n\n        self._conn_params = conn_params\n\n        self._in_dump_restore = False\n\n        # Authentication data supplied by the transport (e.g. the content\n        # of an HTTP Authorization header).\n        self._auth_data = auth_data\n\n    cdef is_in_tx(self):\n        return self.get_dbview().in_tx()\n\n    cdef inline dbview.DatabaseConnectionView get_dbview(self):\n        if self._dbview is None:\n            raise RuntimeError('Cannot access dbview while it is None')\n        return self._dbview\n\n    def debug_print(self, *args):\n        if self._dbview is None:\n            # This may happen before dbview is initialized, e.g. sending errors\n            # to non-TLS clients due to mandatory TLS.\n            print(\n                '::EDGEPROTO::',\n                f'id:{self._id}',\n                f'in_tx:{0}',\n                f'tx_error:{0}',\n                *args,\n                file=sys.stderr,\n            )\n        else:\n            print(\n                '::EDGEPROTO::',\n                f'id:{self._id}',\n                f'in_tx:{int(self._dbview.in_tx())}',\n                f'tx_error:{int(self._dbview.in_tx_error())}',\n                *args,\n                file=sys.stderr,\n            )\n\n    def is_idle(self, expiry_time: float):\n        # A connection is idle if it awaits for the next message for\n        # client for too long (even if it is in an open transaction!)\n        return (\n            self._con_status != EDGECON_BAD and\n            super().is_idle(expiry_time) and\n            not self._in_dump_restore\n        )\n\n    def is_alive(self):\n        return self._con_status == EDGECON_STARTED and super().is_alive()\n\n    def close_for_idling(self):\n        try:\n            self.write_edgedb_error(\n                errors.IdleSessionTimeoutError(\n                    'closing the connection due to idling')\n            )\n        finally:\n            self.close()  # will flush\n\n    cdef _after_idling(self):\n        self.server.on_binary_client_after_idling(self)\n\n    async def do_handshake(self):\n        cdef:\n            char mtype\n\n        if self._transport_proto is srvargs.ServerConnTransport.HTTP:\n            if self._conn_params is None:\n                params = {}\n            else:\n                params = self._conn_params\n        else:\n            await self.wait_for_message(report_idling=True)\n            mtype = self.buffer.get_message_type()\n            if mtype != b'V':\n                raise errors.BinaryProtocolError(\n                    f'unexpected initial message: \"{chr(mtype)}\", '\n                    f'expected \"V\"')\n\n            params = await self._do_handshake()\n            if self._conn_params is not None:\n                params = self._conn_params + params\n\n        return params\n\n    async def auth(self, params):\n        cdef:\n            WriteBuffer msg_buf\n            WriteBuffer buf\n\n        user = params.get('user')\n        if not user:\n            raise errors.BinaryProtocolError(\n                f'missing required connection parameter in ClientHandshake '\n                f'message: \"user\"'\n            )\n        user = self.tenant.resolve_user_name(user)\n\n        database = params.get('database')\n        branch = params.get('branch')\n        if not database and not branch:\n            raise errors.BinaryProtocolError(\n                f'missing required connection parameter in ClientHandshake '\n                f'message: \"branch\" (or \"database\")'\n            )\n        database = self.tenant.resolve_branch_name(database, branch)\n\n        logger.debug('received connection request by %s to database %s',\n                     user, database)\n\n        await self._authenticate(user, database, params)\n\n        logger.debug('successfully authenticated %s in database %s',\n                     user, database)\n\n        if not self.tenant.is_database_connectable(database):\n            raise errors.AccessError(\n                f'database {database!r} does not accept connections'\n            )\n\n        self.dbname = database\n        self.username = user\n        # In the tunneled HTTP endpoint, auth gets done after we have\n        # set up a dbview, so we need to update it..\n        if self._dbview:\n            self._dbview._role_name = user\n\n        await self._start_connection(database)\n\n        if self._transport_proto is srvargs.ServerConnTransport.HTTP:\n            return\n\n        buf = WriteBuffer()\n\n        msg_buf = WriteBuffer.new_message(b'R')\n        msg_buf.write_int32(0)\n        msg_buf.end_message()\n        buf.write_buffer(msg_buf)\n\n        msg_buf = WriteBuffer.new_message(b'K')\n        # TODO: should send ID of this connection\n        msg_buf.write_bytes(b'\\x00' * 32)\n        msg_buf.end_message()\n        buf.write_buffer(msg_buf)\n\n        if self.get_dbview().get_state_serializer() is None:\n            await self.get_dbview().reload_state_serializer()\n        buf.write_buffer(self.make_state_data_description_msg())\n\n        self.write(buf)\n\n        # In dev mode we expose the backend postgres DSN\n        if self.server.in_dev_mode():\n            params = self.tenant.get_pgaddr()\n            params.update(database=self.tenant.get_pg_dbname(\n                self.get_dbview().dbname\n            ))\n            params.clear_server_settings()\n            self.write_status(b'pgdsn', params.to_dsn().encode())\n\n        self.write_status(\n            b'suggested_pool_concurrency',\n            str(self.tenant.suggested_client_pool_size).encode()\n        )\n        self.write_status(\n            b'system_config',\n            self.tenant.get_report_config_data(self.protocol_version),\n        )\n\n        self.write(self.sync_status())\n\n        self.flush()\n\n    async def _do_handshake(self):\n        cdef:\n            uint16_t major\n            uint16_t minor\n            int i\n            uint16_t reserved\n            dict params = {}\n\n        major = <uint16_t>self.buffer.read_int16()\n        minor = <uint16_t>self.buffer.read_int16()\n\n        self.protocol_version = major, minor\n\n        nparams = <uint16_t>self.buffer.read_int16()\n        for i in range(nparams):\n            k = self.buffer.read_len_prefixed_utf8()\n            v = self.buffer.read_len_prefixed_utf8()\n            params[k] = v\n\n        reserved = <uint16_t>self.buffer.read_int16()\n        if reserved != 0:\n            raise errors.BinaryProtocolError(\n                f'unexpected value in reserved field of ClientHandshake')\n\n        self.buffer.finish_message()\n\n        negotiate = False\n        if self.protocol_version < self.min_protocol:\n            target_proto = self.min_protocol\n            negotiate = True\n        elif self.protocol_version > self.max_protocol:\n            target_proto = self.max_protocol\n            negotiate = True\n        else:\n            target_proto = self.protocol_version\n\n        if negotiate:\n            self.write(self.make_negotiate_protocol_version_msg(target_proto))\n            self.flush()\n\n        return params\n\n    async def _start_connection(self, database: str) -> None:\n        dbv = await self.tenant.new_dbview(\n            dbname=database,\n            query_cache=self.query_cache_enabled,\n            protocol_version=self.protocol_version,\n            role_name=self.username,\n        )\n        assert type(dbv) is dbview.DatabaseConnectionView\n        self._dbview = <dbview.DatabaseConnectionView>dbv\n        self.dbname = database\n\n        self._con_status = EDGECON_STARTED\n\n    cdef stop_connection(self):\n        self._con_status = EDGECON_BAD\n\n        if self._dbview is not None:\n            self.tenant.remove_dbview(self._dbview)\n            self._dbview = None\n\n    def _auth_jwt(self, user, database, params):\n        # token in the HTTP header has higher priority than\n        # the ClientHandshake message, under the scenario of\n        # binary protocol over HTTP\n        if self._auth_data:\n            scheme, prefixed_token = auth_helpers.extract_token_from_auth_data(\n                self._auth_data)\n            if scheme != 'bearer':\n                raise errors.AuthenticationError(\n                    'authentication failed: unrecognized authentication scheme')\n        else:\n            prefixed_token = params.get('secret_key')\n\n        return auth_helpers.auth_jwt(\n            self.tenant, prefixed_token, user, database)\n\n    cdef WriteBuffer _make_authentication_sasl_initial(self, list methods):\n        cdef WriteBuffer msg_buf\n        msg_buf = WriteBuffer.new_message(b'R')\n        msg_buf.write_int32(10)\n        # Number of auth methods followed by a series\n        # of zero-terminated strings identifying each method,\n        # sorted in the order of server preference.\n        msg_buf.write_int32(len(methods))\n        for method in methods:\n            msg_buf.write_len_prefixed_bytes(method)\n        return msg_buf.end_message()\n\n    cdef _expect_sasl_initial_response(self):\n        mtype = self.buffer.get_message_type()\n        if mtype != b'p':\n            raise errors.BinaryProtocolError(\n                f'expected SASL response, got message type {mtype}')\n        selected_mech = self.buffer.read_len_prefixed_bytes()\n        client_first = self.buffer.read_len_prefixed_bytes()\n        self.buffer.finish_message()\n        if not client_first:\n            # The client didn't send the Client Initial Response\n            # in SASLInitialResponse, this is an error.\n            raise errors.BinaryProtocolError(\n                f'client did not send the Client Initial Response '\n                f'data in SASLInitialResponse')\n        return selected_mech, client_first\n\n    cdef WriteBuffer _make_authentication_sasl_msg(\n        self, bytes data, bint final\n    ):\n        cdef WriteBuffer msg_buf\n        msg_buf = WriteBuffer.new_message(b'R')\n        if final:\n            msg_buf.write_int32(12)\n        else:\n            msg_buf.write_int32(11)\n        msg_buf.write_len_prefixed_bytes(data)\n        return msg_buf.end_message()\n\n    cdef bytes _expect_sasl_response(self):\n        mtype = self.buffer.get_message_type()\n        if mtype != b'r':\n            raise errors.BinaryProtocolError(\n                f'expected SASL response, got message type {mtype}')\n        client_final = self.buffer.read_len_prefixed_bytes()\n        self.buffer.finish_message()\n        return client_final\n\n    async def _execute_script(\n        self,\n        compiled: object,\n        bind_args: bytes,\n        *,\n        query_req: Optional[rpc.CompilationRequest] = None,\n    ):\n        cdef:\n            pgcon.PGConnection conn\n            dbview.DatabaseConnectionView dbv\n\n        if self._cancelled:\n            raise ConnectionAbortedError\n\n        dbv = self.get_dbview()\n        async with self.with_pgcon() as conn:\n            await execute.execute_script(\n                conn,\n                dbv,\n                compiled,\n                bind_args,\n                fe_conn=self,\n                query_req=query_req,\n            )\n\n    def _tokenize(\n        self,\n        eql: bytes,\n        lang: enums.InputLanguage,\n    ) -> edgeql.Source:\n        text = eql.decode('utf-8')\n        if lang is LANG_EDGEQL:\n            if debug.flags.edgeql_disable_normalization:\n                return edgeql.Source.from_string(text)\n            else:\n                return edgeql.NormalizedSource.from_string(text)\n        elif lang is LANG_SQL:\n            if debug.flags.edgeql_disable_normalization:\n                return pgparser.Source.from_string(text)\n            else:\n                return pgparser.NormalizedSource.from_string(text)\n        elif lang is LANG_GRAPHQL:\n            if debug.flags.edgeql_disable_normalization:\n                return gql_tokenizer.Source.from_string(text)\n            else:\n                try:\n                    return gql_tokenizer.NormalizedSource.from_string(text)\n                except Exception:\n                    return gql_tokenizer.Source.from_string(text)\n        else:\n            raise errors.UnsupportedFeatureError(\n                f\"unsupported input language: {lang}\")\n\n    async def _suppress_tx_timeout(self):\n        async with self.with_pgcon() as conn:\n            await conn.sql_execute(b'''\n                select pg_catalog.set_config(\n                    'idle_in_transaction_session_timeout', '0', true)\n            ''')\n\n    async def _restore_tx_timeout(self, dbview.DatabaseConnectionView dbv):\n        old_timeout = dbv.get_session_config().get(\n            'session_idle_transaction_timeout',\n        )\n        timeout = (\n            'NULL' if not old_timeout\n            else repr(old_timeout.value.to_backend_str())\n        )\n        async with self.with_pgcon() as conn:\n            await conn.sql_execute(f'''\n                select pg_catalog.set_config(\n                    'idle_in_transaction_session_timeout', {timeout}, true)\n            '''.encode('utf-8'))\n\n    async def _parse(\n        self,\n        rpc.CompilationRequest query_req,\n        uint64_t allow_capabilities,\n        tag=None,\n    ) -> dbview.CompiledQuery:\n        cdef dbview.DatabaseConnectionView dbv\n        dbv = self.get_dbview()\n        if self.debug:\n            source = query_req.source\n            text = source.text()\n            self.debug_print('PARSE', text)\n            self.debug_print(\n                'Cache key',\n                source.cache_key(),\n                f\"protocol_version={query_req.protocol_version}\",\n                f\"input_language={query_req.input_language}\",\n                f\"output_format={query_req.output_format}\",\n                f\"expect_one={query_req.expect_one}\",\n                f\"implicit_limit={query_req.implicit_limit}\",\n                f\"inline_typeids={query_req.inline_typeids}\",\n                f\"inline_typenames={query_req.inline_typenames}\",\n                f\"inline_objectids={query_req.inline_objectids}\",\n                f\"allow_capabilities={allow_capabilities}\",\n                f\"modaliazes={dbv.get_modaliases()}\",\n                f\"session_config={dbv.get_session_config()}\",\n            )\n            self.debug_print('Extra variables', source.variables(),\n                             'after', source.first_extra())\n\n        query_unit_group = dbv.lookup_compiled_query(query_req)\n        if query_unit_group is None:\n            # If we have to do a compile within a transaction, suppress\n            # the idle_in_transaction_session_timeout.\n            suppress_timeout = dbv.in_tx() and not dbv.in_tx_error()\n            if suppress_timeout:\n                await self._suppress_tx_timeout()\n            try:\n                if query_req.input_language is LANG_SQL:\n                    async with self.with_pgcon() as pg_conn:\n                        return await dbv.parse(\n                            query_req,\n                            allow_capabilities=allow_capabilities,\n                            pgcon=pg_conn,\n                            tag=tag,\n                        )\n                else:\n                    return await dbv.parse(\n                        query_req,\n                        allow_capabilities=allow_capabilities,\n                        send_log_message=(\n                            lambda code, s: self.write_log(\n                                EdgeSeverity.EDGE_SEVERITY_NOTICE,\n                                code,\n                                s,\n                            )\n                        )\n                    )\n            finally:\n                if suppress_timeout:\n                    try:\n                        await self._restore_tx_timeout(dbv)\n                    except pgerror.BackendError as ex:\n                        # dbv.parse() for LANG_SQL can send a SQL\n                        # query, which can put the transaction in a\n                        # bad state if it fails. If we fail because of\n                        # that, swallow it.\n                        if (\n                            query_req.input_language is not LANG_SQL\n                            or not ex.code_is(\n                                pgerror.ERRCODE_IN_FAILED_SQL_TRANSACTION\n                            )\n                        ):\n                            raise\n        else:\n            return dbv.as_compiled(query_req, query_unit_group)\n\n    cdef parse_cardinality(self, bytes card):\n        if card[0] == CARD_MANY.value:\n            return CARD_MANY\n        elif card[0] == CARD_AT_MOST_ONE.value:\n            return CARD_AT_MOST_ONE\n        else:\n            try:\n                card_name = compiler.Cardinality(card[0]).name\n            except ValueError:\n                raise errors.BinaryProtocolError(\n                    f'unknown expected cardinality \"{repr(card)[2:-1]}\"')\n            else:\n                raise errors.BinaryProtocolError(\n                    f'cardinality {card_name} cannot be requested')\n\n    cdef char render_cardinality(self, query_unit_group) except -1:\n        return query_unit_group.cardinality.value\n\n    cdef dict parse_headers(self):\n        cdef:\n            dict attrs\n            uint16_t num_fields\n            uint16_t key\n            bytes value\n\n        attrs = {}\n        num_fields = <uint16_t>self.buffer.read_int16()\n        while num_fields:\n            key = <uint16_t>self.buffer.read_int16()\n            value = self.buffer.read_len_prefixed_bytes()\n            attrs[key] = value\n            num_fields -= 1\n        return attrs\n\n    cdef inline ignore_headers(self):\n        cdef:\n            uint16_t num_fields\n\n        num_fields = <uint16_t>self.buffer.read_int16()\n        while num_fields:\n            self.buffer.read_int16()\n            self.buffer.read_len_prefixed_bytes()\n            num_fields -= 1\n\n    cdef dict parse_annotations(self):\n        cdef:\n            dict annos\n            uint16_t num_annos\n            str name, value\n\n        annos = {}\n        num_annos = <uint16_t>self.buffer.read_int16()\n        while num_annos:\n            name = self.buffer.read_len_prefixed_utf8()\n            value = self.buffer.read_len_prefixed_utf8()\n            annos[name] = value\n            num_annos -= 1\n        return annos\n\n    cdef inline ignore_annotations(self):\n        cdef:\n            uint16_t num_annos\n\n        num_annos = <uint16_t>self.buffer.read_int16()\n        while num_annos:\n            self.buffer.read_len_prefixed_bytes()\n            self.buffer.read_len_prefixed_bytes()\n            num_annos -= 1\n\n    #############\n\n    cdef WriteBuffer make_negotiate_protocol_version_msg(\n        self,\n        tuple target_proto,\n    ):\n        cdef:\n            WriteBuffer msg\n\n        # NegotiateProtocolVersion\n        msg = WriteBuffer.new_message(b'v')\n        # Highest supported major version of the protocol.\n        msg.write_int16(target_proto[0])\n        # Highest supported minor version of the protocol.\n        msg.write_int16(target_proto[1])\n        # No extensions are currently supported.\n        msg.write_int16(0)\n\n        msg.end_message()\n        return msg\n\n    cdef WriteBuffer make_command_data_description_msg(\n        self, dbview.CompiledQuery query\n    ):\n        cdef:\n            WriteBuffer msg\n            int16_t ann_count\n\n        msg = WriteBuffer.new_message(b'T')\n        ann_count = 0\n        if query.query_unit_group.warnings:\n            ann_count += 1\n        if query.query_unit_group.unsafe_isolation_dangers:\n            ann_count += 1\n\n        msg.write_int16(ann_count)\n\n        if query.query_unit_group.warnings:\n            warnings = json.dumps(\n                [w.to_json() for w in query.query_unit_group.warnings]\n            ).encode('utf-8')\n            msg.write_len_prefixed_bytes(b'warnings')\n            msg.write_len_prefixed_bytes(warnings)\n        if query.query_unit_group.unsafe_isolation_dangers:\n            dangers = json.dumps([\n                w.to_json() for\n                w in query.query_unit_group.unsafe_isolation_dangers\n            ]).encode('utf-8')\n            msg.write_len_prefixed_bytes(b'unsafe_isolation_dangers')\n            msg.write_len_prefixed_bytes(dangers)\n\n        msg.write_int64(\n            <int64_t><uint64_t>query.query_unit_group.capabilities & PROTO_CAPS\n        )\n        msg.write_byte(self.render_cardinality(query.query_unit_group))\n\n        in_data = query.query_unit_group.in_type_data\n        msg.write_bytes(query.query_unit_group.in_type_id)\n        msg.write_len_prefixed_bytes(in_data)\n\n        out_data = query.query_unit_group.out_type_data\n        msg.write_bytes(query.query_unit_group.out_type_id)\n        msg.write_len_prefixed_bytes(out_data)\n\n        msg.end_message()\n        return msg\n\n    cdef WriteBuffer make_state_data_description_msg(self):\n        cdef WriteBuffer msg\n\n        type_id, type_data = self.get_dbview().describe_state()\n\n        msg = WriteBuffer.new_message(b's')\n        msg.write_bytes(type_id.bytes)\n        msg.write_len_prefixed_bytes(type_data)\n        msg.end_message()\n\n        return msg\n\n    cdef WriteBuffer make_command_complete_msg(self, capabilities, status):\n        cdef:\n            WriteBuffer msg\n\n        state_tid, state_data = self.get_dbview().encode_state()\n\n        msg = WriteBuffer.new_message(b'C')\n        msg.write_int16(0)  # no annotations\n        msg.write_int64(<int64_t><uint64_t>capabilities & PROTO_CAPS)\n        msg.write_len_prefixed_bytes(status)\n\n        msg.write_bytes(state_tid.bytes)\n        msg.write_len_prefixed_bytes(state_data)\n\n        return msg.end_message()\n\n    async def _execute_rollback(self, compiled: dbview.CompiledQuery):\n        cdef:\n            dbview.DatabaseConnectionView _dbview\n            pgcon.PGConnection conn\n\n        query_unit = compiled.query_unit_group[0]\n        _dbview = self.get_dbview()\n        if not (\n            query_unit.tx_savepoint_rollback or\n            query_unit.tx_rollback or\n            query_unit.tx_abort_migration\n        ):\n            _dbview.raise_in_tx_error()\n\n        async with self.with_pgcon() as conn:\n            if query_unit.sql:\n                await conn.sql_execute(query_unit.sql)\n\n            if query_unit.tx_abort_migration:\n                _dbview.clear_tx_error()\n            elif query_unit.tx_savepoint_rollback:\n                _dbview.rollback_tx_to_savepoint(query_unit.sp_name)\n            else:\n                assert query_unit.tx_rollback\n                _dbview.abort_tx()\n\n    async def _execute(\n        self,\n        compiled: dbview.CompiledQuery,\n        bind_args: bytes,\n        use_prep_stmt: bint,\n        *,\n        query_req: Optional[rpc.CompilationRequest] = None,\n    ):\n        cdef:\n            dbview.DatabaseConnectionView dbv\n            pgcon.PGConnection conn\n\n        dbv = self.get_dbview()\n        async with self.with_pgcon() as conn:\n            await execute.execute(\n                conn,\n                dbv,\n                compiled,\n                bind_args,\n                fe_conn=self,\n                use_prep_stmt=use_prep_stmt,\n                query_req=query_req,\n            )\n\n        query_unit = compiled.query_unit_group[0]\n        if query_unit.config_requires_restart:\n            self.write_log(\n                EdgeSeverity.EDGE_SEVERITY_NOTICE,\n                errors.LogMessage.get_code(),\n                'server restart is required for the configuration '\n                'change to take effect')\n\n    cdef parse_execute_request(self):\n        cdef:\n            uint64_t allow_capabilities = 0\n            uint64_t compilation_flags = 0\n            int64_t implicit_limit = 0\n            bint inline_typenames = False\n            bint inline_typeids = False\n            bint inline_objectids = False\n            object cardinality\n            object output_format\n            bint expect_one = False\n            bytes query\n            dbview.DatabaseConnectionView _dbview\n\n        allow_capabilities = PROTO_CAPS & <uint64_t>self.buffer.read_int64()\n        compilation_flags = <uint64_t>self.buffer.read_int64()\n        implicit_limit = self.buffer.read_int64()\n\n        if implicit_limit < 0:\n            raise errors.BinaryProtocolError(\n                f'implicit limit cannot be negative'\n            )\n\n        inline_typenames = (\n            compilation_flags\n            & messages.CompilationFlag.INJECT_OUTPUT_TYPE_NAMES\n        )\n        inline_typeids = (\n            compilation_flags\n            & messages.CompilationFlag.INJECT_OUTPUT_TYPE_IDS\n        )\n        inline_objectids = (\n            compilation_flags\n            & messages.CompilationFlag.INJECT_OUTPUT_OBJECT_IDS\n        )\n\n        if self.protocol_version >= (3, 0):\n            lang = rpc.deserialize_input_language(self.buffer.read_byte())\n        else:\n            lang = LANG_EDGEQL\n\n        output_format = rpc.deserialize_output_format(self.buffer.read_byte())\n        if (\n            lang is LANG_SQL\n            and output_format is not FMT_NONE\n            and output_format is not FMT_BINARY\n        ):\n            raise errors.UnsupportedFeatureError(\n                \"non-binary output format is not supported with \"\n                \"SQL as the input language\"\n            )\n\n        cardinality = self.parse_cardinality(self.buffer.read_byte())\n        expect_one = cardinality is CARD_AT_MOST_ONE\n        if lang is LANG_SQL and cardinality is not CARD_MANY:\n            raise errors.UnsupportedFeatureError(\n                \"output cardinality assertions are not supported with \"\n                \"SQL as the input language\"\n            )\n\n        query = self.buffer.read_len_prefixed_bytes()\n        if not query:\n            raise errors.BinaryProtocolError('empty query')\n\n        metrics.query_size.observe(\n            len(query), self.get_tenant_label(), 'edgeql'\n        )\n\n        _dbview = self.get_dbview()\n        state_tid = self.buffer.read_bytes(16)\n        state_data = self.buffer.read_len_prefixed_bytes()\n        try:\n            _dbview.decode_state(state_tid, state_data)\n        except errors.StateMismatchError:\n            self.write(self.make_state_data_description_msg())\n            raise\n\n        cfg_ser = self.server.compilation_config_serializer\n        rv = rpc.CompilationRequest(\n            source=self._tokenize(query, lang),\n            protocol_version=self.protocol_version,\n            schema_version=_dbview.schema_version,\n            compilation_config_serializer=cfg_ser,\n            input_language=lang,\n            output_format=output_format,\n            expect_one=expect_one,\n            implicit_limit=implicit_limit,\n            inline_typeids=inline_typeids,\n            inline_typenames=inline_typenames,\n            inline_objectids=inline_objectids,\n            modaliases=_dbview.get_modaliases(),\n            session_config=_dbview.get_session_config(),\n            database_config=_dbview.get_database_config(),\n            system_config=_dbview.get_compilation_system_config(),\n            role_name=self.username,\n            branch_name=self.dbname,\n        )\n        return rv, allow_capabilities\n\n    cdef get_checked_tag(self, dict annotations):\n        tag = annotations.get(\"tag\")\n        if not tag:\n            return None\n        if len(tag) > 128:\n            raise errors.BinaryProtocolError(\n                'bad annotation: tag too long (> 128 bytes)')\n        return tag\n\n    async def parse(self):\n        cdef:\n            bytes eql\n            rpc.CompilationRequest query_req\n            dbview.DatabaseConnectionView _dbview\n            WriteBuffer parse_complete\n            WriteBuffer buf\n            uint64_t allow_capabilities\n\n        self._last_anon_compiled = None\n\n        if self.protocol_version >= (3, 0):\n            self.ignore_annotations()\n        else:\n            self.ignore_headers()\n\n        _dbview = self.get_dbview()\n        if _dbview.get_state_serializer() is None:\n            await _dbview.reload_state_serializer()\n        query_req, allow_capabilities = self.parse_execute_request()\n        compiled = await self._parse(query_req, allow_capabilities)\n\n        buf = self.make_command_data_description_msg(compiled)\n\n        # Cache compilation result in anticipation that the client\n        # will follow up with an Execute immediately.\n        #\n        # N.B.: we cannot rely on query cache because not all units\n        # are cacheable.\n        self._last_anon_compiled = compiled\n        self._last_anon_compiled_hash = hash(query_req)\n\n        self.write(buf)\n        self.flush()\n\n    async def execute(self):\n        cdef:\n            rpc.CompilationRequest query_req\n            dbview.DatabaseConnectionView _dbview\n            bytes in_tid\n            bytes out_tid\n            bytes args\n            uint64_t allow_capabilities\n\n        if self.protocol_version >= (3, 0):\n            tag = self.get_checked_tag(self.parse_annotations())\n        else:\n            self.ignore_headers()\n            tag = None\n\n        _dbview = self.get_dbview()\n        if _dbview.get_state_serializer() is None:\n            await _dbview.reload_state_serializer()\n        query_req, allow_capabilities = self.parse_execute_request()\n        in_tid = self.buffer.read_bytes(16)\n        out_tid = self.buffer.read_bytes(16)\n        args = self.buffer.read_len_prefixed_bytes()\n        self.buffer.finish_message()\n\n        compiled = None\n        if (\n            self._last_anon_compiled is not None and\n            hash(query_req) == self._last_anon_compiled_hash and\n            in_tid == self._last_anon_compiled.query_unit_group.in_type_id and\n            out_tid == self._last_anon_compiled.query_unit_group.out_type_id\n        ):\n            compiled = self._last_anon_compiled\n            query_unit_group = compiled.query_unit_group\n        else:\n            query_unit_group = _dbview.lookup_compiled_query(query_req)\n\n        if query_unit_group is None:\n            if self.debug:\n                self.debug_print('EXECUTE /CACHE MISS', query_req.source.text())\n\n            compiled = await self._parse(query_req, allow_capabilities, tag)\n            query_unit_group = compiled.query_unit_group\n\n        # If this is a graphql request, and the compilation of it\n        # depends on reading the value of some variables (because they\n        # are used in @include or as params to type introspection, for\n        # example) we need to reflect those variables into the\n        # query_req and look it up again, and then maybe compile again.\n        #\n        # What a pain!\n        if query_unit_group.graphql_key_variables:\n            key_vars = _extract_key_vars(query_unit_group, query_req, args)\n            query_req = query_req.__copy__()\n            query_req.set_key_params(key_vars)\n\n            compiled = None\n            query_unit_group = _dbview.lookup_compiled_query(query_req)\n\n        # If we had to do a graphql_key_variables lookup, we might need\n        # to compile again.\n        if query_unit_group is None:\n            if self.debug:\n                self.debug_print(\n                    'EXECUTE /CACHE MISS (graphql nonsense)',\n                    query_req.source.text(),\n                )\n\n            compiled = await self._parse(query_req, allow_capabilities, tag)\n            query_unit_group = compiled.query_unit_group\n        else:\n            if not compiled:\n                compiled = _dbview.as_compiled(query_req, query_unit_group)\n\n        compiled.tag = tag\n\n        if self._cancelled:\n            raise ConnectionAbortedError\n\n        self._query_count += 1\n\n        # Clear the _last_anon_compiled so that the next Execute - if\n        # identical - will always lookup in the cache and honor the\n        # `cacheable` flag to compile the query again.\n        self._last_anon_compiled = None\n\n        _dbview.check_capabilities(\n            query_unit_group,\n            allow_capabilities,\n            errors.DisabledCapabilityError,\n            \"disabled by the client\",\n            unsafe_isolation_dangers=query_unit_group.unsafe_isolation_dangers,\n        )\n\n        if query_unit_group.in_type_id != in_tid:\n            self.write(self.make_command_data_description_msg(compiled))\n            raise errors.ParameterTypeMismatchError(\n                \"specified parameter type(s) do not match the parameter \"\n                \"types inferred from specified command(s)\"\n            )\n\n        if (\n            query_unit_group.out_type_id != out_tid\n            or query_unit_group.warnings\n        ):\n            # The client has no up-to-date information about the output,\n            # so provide one.\n            self.write(self.make_command_data_description_msg(compiled))\n\n        if self.debug:\n            self.debug_print('EXECUTE', query_req.source.text())\n\n        force_script = any(x.needs_readback for x in query_unit_group)\n        if (\n            _dbview.in_tx_error()\n            or query_unit_group[0].tx_savepoint_rollback\n            or query_unit_group[0].tx_abort_migration\n        ):\n            assert len(query_unit_group) == 1\n            await self._execute_rollback(compiled)\n        elif len(query_unit_group) > 1 or force_script:\n            await self._execute_script(compiled, args, query_req=query_req)\n        else:\n            use_prep = (\n                len(query_unit_group) == 1\n                and bool(query_unit_group[0].sql_hash)\n            )\n            await self._execute(compiled, args, use_prep, query_req=query_req)\n\n        if self._cancelled:\n            raise ConnectionAbortedError\n\n        if _dbview.is_state_desc_changed():\n            self.write(self.make_state_data_description_msg())\n        self.write(\n            self.make_command_complete_msg(\n                compiled.query_unit_group.capabilities,\n                compiled.query_unit_group[-1].status,\n            )\n        )\n        self.flush()\n\n    async def sync(self):\n        self.buffer.consume_message()\n        self.write(self.sync_status())\n\n        if self.debug:\n            self.debug_print('SYNC')\n\n        self.flush()\n\n    def check_readiness(self):\n        if self.tenant.is_blocked():\n            readiness_reason = self.tenant.get_readiness_reason()\n            msg = \"the server is not accepting requests\"\n            if readiness_reason:\n                msg = f\"{msg}: {readiness_reason}\"\n            raise errors.ServerBlockedError(msg)\n        elif not self.tenant.is_online():\n            readiness_reason = self.tenant.get_readiness_reason()\n            msg = \"the server is going offline\"\n            if readiness_reason:\n                msg = f\"{msg}: {readiness_reason}\"\n            raise errors.ServerOfflineError(msg)\n\n    async def authenticate(self):\n        self.check_readiness()\n        params = await self.do_handshake()\n        await self.auth(params)\n        self.server.on_binary_client_authed(self)\n\n    async def main_step(self, char mtype):\n        try:\n            self.check_readiness()\n\n            if mtype == b'O':\n                await self.execute()\n\n            elif mtype == b'P':\n                await self.parse()\n\n            elif mtype == b'S':\n                await self.sync()\n\n            elif mtype == b'X':\n                self.close()\n                return True\n\n            elif mtype == b'>':\n                await self.dump()\n\n            elif mtype == b'<':\n                # The restore protocol cannot send SYNC beforehand,\n                # so if an error occurs the server should send an\n                # ERROR message immediately.\n                await self.restore()\n\n            elif mtype == b'D':\n                raise errors.BinaryProtocolError(\n                    \"Describe message (D) is not supported in \"\n                    \"protocol versions greater than 0.13\")\n\n            elif mtype == b'E':\n                raise errors.BinaryProtocolError(\n                    \"Legacy Execute message (E) is not supported in \"\n                    \"protocol versions greater than 0.13\")\n\n            elif mtype == b'Q':\n                raise errors.BinaryProtocolError(\n                    \"ExecuteScript message (Q) is not supported in \"\n                    \"protocol versions greater then 0.13\")\n\n            else:\n                self.fallthrough()\n\n        except ConnectionError:\n            raise\n\n        except asyncio.CancelledError:\n            raise\n\n        except Exception as ex:\n            if self._cancelled and \\\n                    isinstance(ex, pgerror.BackendQueryCancelledError):\n                # If we are cancelling the protocol (means that the\n                # client side of the connection has dropped and we\n                # need to gracefull cleanup and abort) we want to\n                # propagate the BackendQueryCancelledError exception.\n                #\n                # If we're not cancelling, we'll treat it just like\n                # any other error coming from Postgres (a query\n                # might get cancelled due to a variety of reasons.)\n                raise\n\n            # The connection has been aborted; there's nothing\n            # we can do except shutting this down.\n            if self._con_status == EDGECON_BAD:\n                return True\n\n            self.get_dbview().tx_error()\n            self.buffer.finish_message()\n\n            ex = await self.interpret_error(ex)\n\n            self.write_edgedb_error(ex)\n\n            if isinstance(\n                ex,\n                (errors.ServerOfflineError, errors.ServerBlockedError),\n            ):\n                # This server is going into \"offline\" or \"blocked\" mode,\n                # close the connection.\n                self.write(self.sync_status())\n                self.flush()\n                self.close()\n                return\n\n            self.flush()\n\n            # The connection was aborted while we were\n            # interpreting the error (via compiler/errmech.py).\n            if self._con_status == EDGECON_BAD:\n                return True\n\n            await self.recover_from_error()\n\n        else:\n            self.buffer.finish_message()\n\n    cdef _main_task_stopped_normally(self):\n        self.write_log(\n            EdgeSeverity.EDGE_SEVERITY_NOTICE,\n            errors.LogMessage.get_code(),\n            'requested to stop; disconnecting now')\n\n    async def recover_from_error(self):\n        # Consume all messages until sync.\n\n        while True:\n\n            if not self.buffer.take_message():\n                await self.wait_for_message(report_idling=True)\n            mtype = self.buffer.get_message_type()\n\n            if mtype == b'S':\n                await self.sync()\n                return\n            else:\n                self.buffer.discard_message()\n\n    cdef write_error(self, exc):\n        self.write_edgedb_error(execute.interpret_simple_error(exc))\n\n    cdef write_edgedb_error(self, exc):\n        cdef:\n            WriteBuffer buf\n            int16_t fields_len\n\n        if self.debug and not isinstance(exc, errors.BackendUnavailableError):\n            self.debug_print('EXCEPTION', type(exc).__name__, exc)\n            from edb.common.markup import dump\n            dump(exc)\n\n        if debug.flags.server and not isinstance(\n            exc, errors.BackendUnavailableError\n        ):\n            self.loop.call_exception_handler({\n                'message': (\n                    'an error in edgedb protocol'\n                ),\n                'exception': exc,\n                'protocol': self,\n                'transport': self._transport,\n        })\n\n        fields = {}\n        if isinstance(exc, errors.EdgeDBError):\n            fields.update(exc._attrs)\n            if isinstance(exc, errors.TransactionSerializationError):\n                metrics.transaction_serialization_errors.inc(\n                    1.0, self.get_tenant_label()\n                )\n\n        try:\n            formatted_error = exc.__formatted_error__\n        except AttributeError:\n            try:\n                formatted_error = ''.join(\n                    traceback.format_exception(\n                        type(exc), exc, exc.__traceback__,\n                        limit=50))\n            except Exception:\n                formatted_error = 'could not serialize error traceback'\n\n        fields[base_errors.FIELD_SERVER_TRACEBACK] = formatted_error\n\n        buf = WriteBuffer.new_message(b'E')\n        buf.write_byte(<char><uint8_t>EdgeSeverity.EDGE_SEVERITY_ERROR)\n        buf.write_int32(<int32_t><uint32_t>exc.get_code())\n        buf.write_len_prefixed_utf8(str(exc))\n        buf.write_int16(len(fields))\n        for k, v in fields.items():\n            buf.write_int16(<int16_t><uint16_t>k)\n            buf.write_len_prefixed_utf8(str(v))\n        buf.end_message()\n\n        self.write(buf)\n\n    async def interpret_error(self, exc):\n        dbv = self.get_dbview()\n        return await execute.interpret_error(\n            exc,\n            dbv._db,\n            global_schema_pickle=dbv.get_global_schema_pickle(),\n            user_schema_pickle=dbv.get_user_schema_pickle(),\n        )\n\n    cdef write_status(self, bytes name, bytes value):\n        cdef:\n            WriteBuffer buf\n\n        buf = WriteBuffer.new_message(b'S')\n        buf.write_len_prefixed_bytes(name)\n        buf.write_len_prefixed_bytes(value)\n        buf.end_message()\n\n        self.write(buf)\n\n    cdef write_log(self, EdgeSeverity severity, uint32_t code, str message):\n        cdef:\n            WriteBuffer buf\n\n        if severity >= EdgeSeverity.EDGE_SEVERITY_ERROR:\n            # This is an assertion.\n            raise errors.InternalServerError(\n                'emitting a log message with severity=ERROR')\n\n        buf = WriteBuffer.new_message(b'L')\n        buf.write_byte(<char><uint8_t>severity)\n        buf.write_int32(<int32_t><uint32_t>code)\n        buf.write_len_prefixed_utf8(message)\n        buf.write_int16(0)  # number of annotations\n        buf.end_message()\n\n        self.write(buf)\n\n    cdef sync_status(self):\n        cdef:\n            WriteBuffer buf\n            dbview.DatabaseConnectionView _dbview\n\n        buf = WriteBuffer.new_message(b'Z')\n        buf.write_int16(0)  # no annotations\n\n        # NOTE: EdgeDB and PostgreSQL current statuses can disagree.\n        # For example, Postres can be \"PQTRANS_INTRANS\" whereas EdgeDB\n        # would be \"PQTRANS_INERROR\". This can happen becuase some of\n        # EdgeDB errors can happen at the compile stage, not even\n        # reaching Postgres.\n\n        _dbview = self.get_dbview()\n        if _dbview.in_tx_error():\n            buf.write_byte(b'E')\n        elif _dbview.in_tx():\n            buf.write_byte(b'T')\n        else:\n            buf.write_byte(b'I')\n\n        return buf.end_message()\n\n    cdef fallthrough(self):\n        cdef:\n            char mtype = self.buffer.get_message_type()\n\n        if mtype == b'H':\n            # Flush\n            self.buffer.discard_message()\n            self.flush()\n\n        elif mtype == b'X':\n            # Terminate\n            self.buffer.discard_message()\n            self.close()\n\n        else:\n            raise errors.BinaryProtocolError(\n                f'unexpected message type {chr(mtype)!r}')\n\n    def connection_made(self, transport):\n        if self._con_status != EDGECON_NEW:\n            raise errors.BinaryProtocolError(\n                'invalid connection status while establishing the connection')\n        super().connection_made(transport)\n\n    cdef _main_task_created(self):\n        self.server.on_binary_client_connected(self)\n\n    def connection_lost(self, exc):\n        self.server.on_binary_client_disconnected(self)\n        super().connection_lost(exc)\n\n    @contextlib.asynccontextmanager\n    async def _with_dump_restore_pgcon(self):\n        self._in_dump_restore = True\n        try:\n            async with self.with_pgcon() as conn:\n                yield conn\n        finally:\n            self._in_dump_restore = False\n            # If backpressure was being applied during the operation, release it.\n            # `resume_reading` is idempotent.\n            self._transport.resume_reading()\n\n    async def dump(self):\n        cdef:\n            WriteBuffer msg_buf\n            dbview.DatabaseConnectionView _dbview\n            uint64_t flags\n\n        # Parse the \"Dump\" message\n        if self.protocol_version >= (3, 0):\n            self.ignore_annotations()\n            flags = <uint64_t>self.buffer.read_int64()\n            include_secrets = flags & messages.DumpFlag.DUMP_SECRETS\n        else:\n            headers = self.parse_headers()\n            include_secrets = headers.get(QUERY_HEADER_DUMP_SECRETS) == b'\\x01'\n\n        self.buffer.finish_message()\n\n        _dbview = self.get_dbview()\n        if _dbview.txid:\n            raise errors.ProtocolError(\n                'DUMP must not be executed while in transaction'\n            )\n\n        is_superuser, _ = _dbview.get_permissions()\n        if not is_superuser:\n            raise errors.DisabledCapabilityError(\n                f'role {_dbview._role_name} does not have permission to '\n                f'perform dump'\n            )\n\n        server = self.server\n        compiler_pool = server.get_compiler_pool()\n\n        dbname = _dbview.dbname\n        async with self._with_dump_restore_pgcon() as pgcon:\n            # To avoid having races, we want to:\n            #\n            #   1. start a transaction;\n            #\n            #   2. in the compiler process we connect to that transaction\n            #      and re-introspect the schema in it.\n            #\n            #   3. all dump worker pg connection would work on the same\n            #      connection.\n            #\n            # This guarantees that every pg connection and the compiler work\n            # with the same DB state.\n\n            await pgcon.sql_execute(\n                b'''START TRANSACTION\n                        ISOLATION LEVEL SERIALIZABLE\n                        READ ONLY\n                        DEFERRABLE;\n\n                    -- Disable transaction or query execution timeout\n                    -- limits. Both clients and the server can be slow\n                    -- during the dump/restore process.\n                    SET LOCAL idle_in_transaction_session_timeout = 0;\n                    SET LOCAL statement_timeout = 0;\n                ''',\n            )\n\n            user_schema_json = await server.introspect_user_schema_json(pgcon)\n            global_schema_json = (\n                await server.introspect_global_schema_json(pgcon)\n            )\n            db_config_json = await server.introspect_db_config(pgcon)\n            dump_protocol = self.max_protocol\n\n            schema_ddl, schema_dynamic_ddl, schema_ids, blocks = (\n                await compiler_pool.describe_database_dump(\n                    user_schema_json,\n                    global_schema_json,\n                    db_config_json,\n                    dump_protocol,\n                    include_secrets,\n                )\n            )\n\n            if schema_dynamic_ddl:\n                for query in schema_dynamic_ddl:\n                    result = await pgcon.sql_fetch_val(query.encode('utf-8'))\n                    if result:\n                        schema_ddl += '\\n' + result.decode('utf-8')\n\n            msg_buf = WriteBuffer.new_message(b'@')  # DumpHeader\n\n            msg_buf.write_int16(4)  # number of key-value pairs\n            msg_buf.write_int16(DUMP_HEADER_BLOCK_TYPE)\n            msg_buf.write_len_prefixed_bytes(DUMP_HEADER_BLOCK_TYPE_INFO)\n            msg_buf.write_int16(DUMP_HEADER_SERVER_VER)\n            msg_buf.write_len_prefixed_utf8(str(buildmeta.get_version()))\n            msg_buf.write_int16(DUMP_HEADER_SERVER_CATALOG_VERSION)\n            msg_buf.write_int32(8)\n            msg_buf.write_int64(buildmeta.EDGEDB_CATALOG_VERSION)\n            msg_buf.write_int16(DUMP_HEADER_SERVER_TIME)\n            msg_buf.write_len_prefixed_utf8(str(int(time.time())))\n\n            msg_buf.write_int16(dump_protocol[0])\n            msg_buf.write_int16(dump_protocol[1])\n            msg_buf.write_len_prefixed_utf8(schema_ddl)\n\n            msg_buf.write_int32(len(schema_ids))\n            for (tn, td, tid) in schema_ids:\n                msg_buf.write_len_prefixed_utf8(tn)\n                msg_buf.write_len_prefixed_utf8(td)\n                assert len(tid) == 16\n                msg_buf.write_bytes(tid)  # uuid\n\n            msg_buf.write_int32(len(blocks))\n            for block in blocks:\n                assert len(block.schema_object_id.bytes) == 16\n                msg_buf.write_bytes(block.schema_object_id.bytes)  # uuid\n                msg_buf.write_len_prefixed_bytes(block.type_desc)\n\n                msg_buf.write_int16(len(block.schema_deps))\n                for depid in block.schema_deps:\n                    assert len(depid.bytes) == 16\n                    msg_buf.write_bytes(depid.bytes)  # uuid\n\n            self._transport.write(memoryview(msg_buf.end_message()))\n            self.flush()\n\n            blocks_queue = collections.deque(blocks)\n            output_queue = asyncio.Queue(maxsize=2)\n\n            async with asyncio.TaskGroup() as g:\n                g.create_task(pgcon.dump(\n                    blocks_queue,\n                    output_queue,\n                    DUMP_BLOCK_SIZE,\n                ))\n\n                nstops = 0\n                while True:\n                    if self._cancelled:\n                        raise ConnectionAbortedError\n\n                    out = await output_queue.get()\n                    if out is None:\n                        nstops += 1\n                        if nstops == 1:\n                            # we only have one worker right now\n                            break\n                    else:\n                        block, block_num, data = out\n\n                        msg_buf = WriteBuffer.new_message(b'=')  # DumpBlock\n                        msg_buf.write_int16(4)  # number of key-value pairs\n\n                        msg_buf.write_int16(DUMP_HEADER_BLOCK_TYPE)\n                        msg_buf.write_len_prefixed_bytes(\n                            DUMP_HEADER_BLOCK_TYPE_DATA)\n                        msg_buf.write_int16(DUMP_HEADER_BLOCK_ID)\n                        msg_buf.write_len_prefixed_bytes(\n                            block.schema_object_id.bytes)\n                        msg_buf.write_int16(DUMP_HEADER_BLOCK_NUM)\n                        msg_buf.write_len_prefixed_bytes(\n                            str(block_num).encode())\n                        msg_buf.write_int16(DUMP_HEADER_BLOCK_DATA)\n                        msg_buf.write_len_prefixed_buffer(data)\n\n                        self._transport.write(memoryview(msg_buf.end_message()))\n                        if self._write_waiter:\n                            await self._write_waiter\n\n            await pgcon.sql_execute(b\"ROLLBACK;\")\n\n        msg_buf = WriteBuffer.new_message(b'C')  # CommandComplete\n        msg_buf.write_int16(0)  # no annotations\n        msg_buf.write_int64(0)  # capabilities\n        msg_buf.write_len_prefixed_bytes(b'DUMP')\n        msg_buf.write_bytes(sertypes.NULL_TYPE_ID.bytes)\n        msg_buf.write_len_prefixed_bytes(b'')\n        self.write(msg_buf.end_message())\n        self.flush()\n\n    async def _execute_utility_stmt(self, eql: str, pgcon):\n        cdef dbview.DatabaseConnectionView _dbview = self.get_dbview()\n\n        cfg_ser = self.server.compilation_config_serializer\n        query_req = rpc.CompilationRequest(\n            source=edgeql.Source.from_string(eql),\n            protocol_version=self.protocol_version,\n            schema_version=_dbview.schema_version,\n            compilation_config_serializer=cfg_ser,\n            role_name=self.username,\n            branch_name=self.dbname,\n        )\n\n        compiled = await _dbview.parse(query_req)\n        query_unit_group = compiled.query_unit_group\n        assert len(query_unit_group) == 1\n        query_unit = query_unit_group[0]\n\n        try:\n            _dbview.start(query_unit)\n            await pgcon.sql_execute(query_unit.sql)\n        except Exception:\n            _dbview.on_error()\n            if (\n                query_unit.tx_commit and\n                not pgcon.in_tx() and\n                _dbview.in_tx()\n            ):\n                # The COMMIT command has failed. Our Postgres connection\n                # isn't in a transaction anymore. Abort the transaction\n                # in dbview.\n                _dbview.abort_tx()\n            raise\n        else:\n            _dbview.on_success(query_unit, {})\n            # _execute_utility_stmt is only used in restore(), where the state\n            # serializer is not coming with the COMMIT command. However, we try\n            # to keep the state serializer here anyways in case of future use\n            if query_unit_group.state_serializer is not None:\n                _dbview.set_state_serializer(query_unit_group.state_serializer)\n\n    async def restore(self):\n        cdef:\n            WriteBuffer msg_buf\n            char mtype\n            dbview.DatabaseConnectionView _dbview\n\n        _dbview = self.get_dbview()\n        if _dbview.txid:\n            raise errors.ProtocolError(\n                'RESTORE must not be executed while in transaction'\n            )\n        is_superuser, _ = _dbview.get_permissions()\n        if not is_superuser:\n            raise errors.DisabledCapabilityError(\n                f'role {_dbview._role_name} does not have permission to '\n                f'perform restore'\n            )\n\n        if _dbview.get_state_serializer() is None:\n            await _dbview.reload_state_serializer()\n\n        # Parse the \"Restore\" message\n        if self.buffer.read_int16() != 0:  # number of attributes\n            raise errors.BinaryProtocolError('unexpected attributes')\n        self.buffer.read_int16()  # discard -j level\n\n        # Now parse the embedded \"DumpHeader\" message:\n\n        server = self.server\n        compiler_pool = server.get_compiler_pool()\n\n        global_schema_pickle = _dbview.get_global_schema_pickle()\n        user_schema_pickle = _dbview.get_user_schema_pickle()\n\n        dump_server_ver_str = None\n        cat_ver = None\n        headers_num = self.buffer.read_int16()\n        for _ in range(headers_num):\n            hdrname = self.buffer.read_int16()\n            hdrval = self.buffer.read_len_prefixed_bytes()\n            if hdrname == DUMP_HEADER_SERVER_VER:\n                dump_server_ver_str = hdrval.decode('utf-8')\n            if hdrname == DUMP_HEADER_SERVER_CATALOG_VERSION:\n                cat_ver = parse_catalog_version_header(hdrval)\n\n        proto_major = self.buffer.read_int16()\n        proto_minor = self.buffer.read_int16()\n        proto = (proto_major, proto_minor)\n        if proto > DUMP_VER_MAX or proto < DUMP_VER_MIN:\n            raise errors.ProtocolError(\n                f'unsupported dump version {proto_major}.{proto_minor}')\n\n        schema_ddl = self.buffer.read_len_prefixed_bytes()\n\n        ids_num = self.buffer.read_int32()\n        schema_ids = []\n        for _ in range(ids_num):\n            schema_ids.append((\n                self.buffer.read_len_prefixed_utf8(),\n                self.buffer.read_len_prefixed_utf8(),\n                self.buffer.read_bytes(16),\n            ))\n\n        block_num = <uint32_t>self.buffer.read_int32()\n        blocks = []\n        for _ in range(block_num):\n            blocks.append((\n                self.buffer.read_bytes(16),\n                self.buffer.read_len_prefixed_bytes(),\n            ))\n\n            # Ignore deps info\n            for _ in range(self.buffer.read_int16()):\n                self.buffer.read_bytes(16)\n\n        self.buffer.finish_message()\n        dbname = _dbview.dbname\n\n        async with self._with_dump_restore_pgcon() as pgcon:\n            _dbview.decode_state(sertypes.NULL_TYPE_ID.bytes, b'')\n            await self._execute_utility_stmt(\n                'START TRANSACTION',\n                pgcon,\n            )\n\n            try:\n                await pgcon.sql_execute(\n                    b'''\n                        -- Drop isolation level.\n                        SET TRANSACTION ISOLATION LEVEL READ COMMITTED;\n                        -- Disable transaction or query execution timeout\n                        -- limits. Both clients and the server can be slow\n                        -- during the dump/restore process.\n                        SET LOCAL idle_in_transaction_session_timeout = 0;\n                        SET LOCAL statement_timeout = 0;\n                    ''',\n                )\n\n                schema_sql_units, restore_blocks, tables, repopulate_units = \\\n                    await compiler_pool.describe_database_restore(\n                        user_schema_pickle,\n                        global_schema_pickle,\n                        dump_server_ver_str,\n                        cat_ver,\n                        schema_ddl,\n                        schema_ids,\n                        blocks,\n                        proto,\n                    )\n\n                for query_unit in schema_sql_units:\n                    new_types = None\n                    _dbview.start(query_unit)\n\n                    try:\n                        if query_unit.config_ops:\n                            for op in query_unit.config_ops:\n                                if op.scope is config.ConfigScope.INSTANCE:\n                                    raise errors.ProtocolError(\n                                        'CONFIGURE INSTANCE cannot be executed'\n                                        ' in dump restore'\n                                    )\n\n                        if query_unit.sql:\n                            if query_unit.ddl_stmt_id:\n                                await pgcon.parse_execute(query=query_unit)\n                                ddl_ret = pgcon.load_last_ddl_return(query_unit)\n                                if ddl_ret and ddl_ret['new_types']:\n                                    new_types = ddl_ret['new_types']\n                            else:\n                                await pgcon.sql_execute(query_unit.sql)\n                    except Exception:\n                        _dbview.on_error()\n                        raise\n                    else:\n                        _dbview.on_success(query_unit, new_types)\n\n                restore_blocks = {\n                    b.schema_object_id: b\n                    for b in restore_blocks\n                }\n\n                disable_trigger_q = ''\n                enable_trigger_q = ''\n                for table in tables:\n                    disable_trigger_q += (\n                        f'ALTER TABLE {table} DISABLE TRIGGER ALL;'\n                    )\n                    enable_trigger_q += (\n                        f'ALTER TABLE {table} ENABLE TRIGGER ALL;'\n                    )\n\n                await pgcon.sql_execute(disable_trigger_q.encode())\n\n                # Send \"RestoreReady\" message\n                msg = WriteBuffer.new_message(b'+')\n                msg.write_int16(0)  # no annotations\n                msg.write_int16(1)  # -j1\n                self.write(msg.end_message())\n                self.flush()\n\n                while True:\n                    if not self.buffer.take_message():\n                        # Don't report idling when restoring a dump.\n                        # This is an edge case and the client might be\n                        # legitimately slow.\n                        await self.wait_for_message(report_idling=False)\n                    mtype = self.buffer.get_message_type()\n\n                    if mtype == b'=':  # RestoreBlock\n                        block_type = None\n                        block_id = None\n                        block_num = None\n                        block_data = None\n\n                        num_headers = self.buffer.read_int16()\n                        for _ in range(num_headers):\n                            header = self.buffer.read_int16()\n                            if header == DUMP_HEADER_BLOCK_TYPE:\n                                block_type = self.buffer.read_len_prefixed_bytes()\n                            elif header == DUMP_HEADER_BLOCK_ID:\n                                block_id = self.buffer.read_len_prefixed_bytes()\n                                block_id = pg_UUID(block_id)\n                            elif header == DUMP_HEADER_BLOCK_NUM:\n                                block_num = self.buffer.read_len_prefixed_bytes()\n                            elif header == DUMP_HEADER_BLOCK_DATA:\n                                block_data = self.buffer.read_len_prefixed_bytes()\n\n                        self.buffer.finish_message()\n\n                        if (block_type is None or block_id is None\n                                or block_num is None or block_data is None):\n                            raise errors.ProtocolError('incomplete data block')\n\n                        restore_block = restore_blocks[block_id]\n                        type_id_map = self._build_type_id_map_for_restore_mending(\n                            restore_block)\n                        self._transport.pause_reading()\n                        await pgcon.restore(restore_block, block_data, type_id_map)\n                        self._transport.resume_reading()\n\n                    elif mtype == b'.':  # RestoreEof\n                        self.buffer.finish_message()\n                        break\n\n                    else:\n                        self.fallthrough()\n\n                for repopulate_unit in repopulate_units:\n                    await pgcon.sql_execute(repopulate_unit.encode())\n\n                await pgcon.sql_execute(enable_trigger_q.encode())\n\n            except Exception:\n                await pgcon.sql_execute(b'ROLLBACK')\n                _dbview.abort_tx()\n                raise\n\n            else:\n                await self._execute_utility_stmt('COMMIT', pgcon)\n\n        execute.signal_side_effects(_dbview, dbview.SideEffects.SchemaChanges)\n        await self.tenant.introspect_db(dbname)\n\n        if _dbview.is_state_desc_changed():\n            self.write(self.make_state_data_description_msg())\n\n        state_tid, state_data = _dbview.encode_state()\n\n        msg = WriteBuffer.new_message(b'C')  # CommandComplete\n        msg.write_int16(0)  # no annotations\n        msg.write_int64(0)  # capabilities\n        msg.write_len_prefixed_bytes(b'RESTORE')\n        msg.write_bytes(state_tid.bytes)\n        msg.write_len_prefixed_bytes(state_data)\n        self.write(msg.end_message())\n        self.flush()\n\n    def _build_type_id_map_for_restore_mending(self, restore_block):\n        type_map = {}\n        descriptor_stack = []\n\n        if not restore_block.data_mending_desc:\n            return type_map\n\n        descriptor_stack.append(restore_block.data_mending_desc)\n        while descriptor_stack:\n            desc_tuple = descriptor_stack.pop()\n            for desc in desc_tuple:\n                if desc is not None:\n                    type_map[desc.schema_type_id] = (\n                        self.get_dbview().resolve_backend_type_id(\n                            desc.schema_type_id,\n                        )\n                    )\n\n                    descriptor_stack.append(desc.elements)\n\n        return type_map\n\n\n@cython.final\ncdef class VirtualTransport:\n    def __init__(self, transport):\n        self.buf = WriteBuffer.new()\n        self.closed = False\n        self.transport = transport\n\n    def write(self, data):\n        self.buf.write_bytes(bytes(data))\n\n    def _get_data(self):\n        return bytes(self.buf)\n\n    def is_closing(self):\n        return self.closed\n\n    def close(self):\n        self.closed = True\n\n    def abort(self):\n        self.closed = True\n\n    def get_extra_info(self, name, default=None):\n        return self.transport.get_extra_info(name, default)\n\n\nasync def eval_buffer(\n    server,\n    tenant,\n    database: str,\n    data: bytes,\n    conn_params: dict[str, str],\n    protocol_version: edbdef.ProtocolVersion,\n    auth_data: bytes,\n    transport: srvargs.ServerConnTransport,\n    tcp_transport: asyncio.Transport,\n):\n    cdef:\n        VirtualTransport vtr\n        EdgeConnection proto\n\n    vtr = VirtualTransport(tcp_transport)\n\n    proto = new_edge_connection(\n        server,\n        tenant,\n        passive=True,\n        auth_data=auth_data,\n        transport=transport,\n        conn_params=conn_params,\n        protocol_version=protocol_version,\n    )\n\n    proto.connection_made(vtr)\n    if vtr.is_closing() or proto._main_task is None:\n        raise RuntimeError(\n            'cannot process the request, the server is shutting down')\n\n    # HACK: In the tunneled protocol we don't have the username when\n    # we create the dbview, so put in an empty username. It will be\n    # filled in once auth is called.\n    proto.username = ''\n\n    try:\n        await proto._start_connection(database)\n        proto.data_received(data)\n        await proto._main_task\n    except Exception as ex:\n        proto.connection_lost(ex)\n    else:\n        proto.connection_lost(None)\n\n    data = vtr._get_data()\n    return data\n\n\ndef new_edge_connection(\n    server,\n    tenant,\n    *,\n    external_auth: bool = False,\n    passive: bool = False,\n    transport: srvargs.ServerConnTransport = (\n        srvargs.ServerConnTransport.TCP),\n    auth_data: bytes = b'',\n    protocol_version: edbdef.ProtocolVersion = edbdef.CURRENT_PROTOCOL,\n    conn_params: dict[str, str] | None = None,\n    connection_made_at: float | None = None,\n):\n    return EdgeConnection(\n        server,\n        tenant,\n        external_auth=external_auth,\n        passive=passive,\n        transport=transport,\n        auth_data=auth_data,\n        protocol_version=protocol_version,\n        conn_params=conn_params,\n        connection_made_at=connection_made_at,\n    )\n\n\nasync def run_script(\n    server,\n    tenant,\n    database: str,\n    user: str,\n    script: str,\n) -> None:\n    cdef:\n        EdgeConnection conn\n        dbview.CompiledQuery compiled\n        dbview.DatabaseConnectionView _dbview\n    conn = new_edge_connection(server, tenant)\n    conn.username = user\n    await conn._start_connection(database)\n    try:\n        _dbview = conn.get_dbview()\n        cfg_ser = server.compilation_config_serializer\n        compiled = await _dbview.parse(\n            rpc.CompilationRequest(\n                source=edgeql.Source.from_string(script),\n                protocol_version=conn.protocol_version,\n                schema_version=_dbview.schema_version,\n                compilation_config_serializer=cfg_ser,\n                output_format=FMT_NONE,\n                role_name=user,\n                branch_name=database,\n            ),\n        )\n        compiled.tag = \"gel/startup-script\"\n        if len(compiled.query_unit_group) > 1:\n            await conn._execute_script(compiled, b'')\n        else:\n            await conn._execute(compiled, b'', use_prep_stmt=0)\n    except Exception as e:\n        exc = await conn.interpret_error(e)\n        if isinstance(exc, errors.EdgeDBError):\n            raise exc from None\n        else:\n            raise exc\n    finally:\n        conn.close()\n\n\ncdef _extract_key_vars(\n    qug: dbstate.QueryUnitGroup,\n    query_req: rpc.CompilationRequest,\n    args: bytes\n):\n    cdef:\n        FRBuffer in_buf\n        char *p\n        int32_t recv_args\n        int32_t decl_args\n        ssize_t in_len\n\n    frb_init(\n        &in_buf,\n        cpython.PyBytes_AS_STRING(args),\n        cpython.Py_SIZE(args))\n\n    keys = qug.graphql_key_variables\n\n    in_type_args = qug.in_type_args or ()\n    decl_args = len(in_type_args)\n    if args:\n        recv_args = hton.unpack_int32(frb_read(&in_buf, 4))\n    else:\n        recv_args = 0\n    if recv_args != decl_args:\n        raise errors.InputDataError(\n            f\"invalid argument count, \"\n            f\"expected: {decl_args}, got: {recv_args}\")\n\n    vals = {}\n\n    for param in in_type_args:\n        frb_read(&in_buf, 4)  # reserved\n        needed = param.name in keys\n\n        in_len = hton.unpack_int32(frb_read(&in_buf, 4))\n        if not needed:\n            if in_len > 0:\n                frb_read(&in_buf, in_len)\n            continue\n\n        if in_len < 0:\n            val = None\n        else:\n            p = frb_read(&in_buf, in_len)\n\n            # Very hacky and minimal decoding support.\n            if param.typename == 'std::str':\n                val = cpython.PyUnicode_DecodeUTF8(p, in_len, NULL)\n            elif param.typename == 'std::bool':\n                val = p[0] != 0\n            else:\n                raise AssertionError(\n                    f'unsupported type for graphql introspection: '\n                    f'{param.typename}'\n                )\n\n        vals[param.name] = val\n\n    # Extracted arguments come from the NormalizedSource.\n    query_vars = query_req.source.variables()\n    for name in keys:\n        if name.startswith('__edb_arg_'):\n            vals[name] = query_vars[name]\n\n    return vals\n"
  },
  {
    "path": "edb/server/protocol/consts.pxi",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nDEF DUMP_BLOCK_SIZE = 1024 * 1024 * 10\n\nDEF DUMP_HEADER_BLOCK_TYPE = 101\nDEF DUMP_HEADER_BLOCK_TYPE_INFO = b'I'\nDEF DUMP_HEADER_BLOCK_TYPE_DATA = b'D'\n\nDEF DUMP_HEADER_SERVER_TIME = 102\nDEF DUMP_HEADER_SERVER_VER = 103\nDEF DUMP_HEADER_BLOCKS_INFO = 104\nDEF DUMP_HEADER_SERVER_CATALOG_VERSION = 105\n\nDEF DUMP_HEADER_BLOCK_ID = 110\nDEF DUMP_HEADER_BLOCK_NUM = 111\nDEF DUMP_HEADER_BLOCK_DATA = 112\n"
  },
  {
    "path": "edb/server/protocol/cpythonx.pxd",
    "content": "# Copyright (C) 2016-present the asyncpg authors and contributors\n# <see AUTHORS file>\n#\n# This module is part of asyncpg and is released under\n# the Apache 2.0 License: http://www.apache.org/licenses/LICENSE-2.0\n\n\ncdef extern from \"Python.h\":\n    object PyLong_FromUnicodeObject(\n        object u, int base)\n"
  },
  {
    "path": "edb/server/protocol/edgeql_ext.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport decimal\nimport http\nimport json\nimport urllib.parse\n\nimport immutables\n\nfrom edb import errors\nfrom edb import edgeql\nfrom edb.server import defines as edbdef\nfrom edb.server.protocol import execute\n\nfrom edb.schema import schema as s_schema\n\nfrom edb.common import debug\nfrom edb.common import markup\n\nfrom edb.edgeql import qltypes\n\nfrom edb.server import compiler\nfrom edb.server import config\nfrom edb.server.compiler import enums\nfrom edb.server.dbview cimport dbview\nfrom edb.server.pgproto.pgproto cimport WriteBuffer\n\n\nasync def handle_request(\n    object request,\n    object response,\n    dbview.Database db,\n    str role_name,\n    list args,\n    object tenant,\n):\n    if args != []:\n        response.body = b'Unknown path'\n        response.status = http.HTTPStatus.NOT_FOUND\n        response.close_connection = True\n        return\n\n    variables = None\n    globals_ = None\n    query = None\n    config = None\n\n    try:\n        if request.method == b'POST':\n            if request.content_type and b'json' in request.content_type:\n                body = json.loads(request.body, parse_float=decimal.Decimal)\n                if not isinstance(body, dict):\n                    raise TypeError(\n                        'the body of the request must be a JSON object')\n                query = body.get('query')\n                variables = body.get('variables')\n                globals_ = body.get('globals')\n                config = body.get('config')\n            else:\n                raise TypeError(\n                    'unable to interpret EdgeQL POST request')\n\n        elif request.method == b'GET':\n            if request.url.query:\n                url_query = request.url.query.decode('ascii')\n                qs = urllib.parse.parse_qs(url_query)\n\n                query = qs.get('query')\n                if query is not None:\n                    query = query[0]\n\n                variables = qs.get('variables')\n                if variables is not None:\n                    try:\n                        variables = json.loads(variables[0])\n                    except Exception:\n                        raise TypeError(\n                            '\"variables\" must be a JSON object')\n\n                globals_ = qs.get('globals')\n                if globals_ is not None:\n                    try:\n                        globals_ = json.loads(globals_[0])\n                    except Exception:\n                        raise TypeError(\n                            '\"globals\" must be a JSON object')\n                config = qs.get('config')\n                if config is not None:\n                    try:\n                        config = json.loads(config[0])\n                    except Exception:\n                        raise TypeError(\n                            '\"config\" must be a JSON object')\n\n        else:\n            raise TypeError('expected a GET or a POST request')\n\n        if not query:\n            raise TypeError('invalid EdgeQL request: query is missing')\n\n        if variables is not None and not isinstance(variables, dict):\n            raise TypeError('\"variables\" must be a JSON object')\n\n        if globals_ is not None and not isinstance(globals_, dict):\n            raise TypeError('\"globals\" must be a JSON object')\n\n        if config is not None and not isinstance(config, dict):\n            raise TypeError('\"config\" must be a JSON object')\n\n    except Exception as ex:\n        if debug.flags.server:\n            markup.dump(ex)\n\n        response.body = str(ex).encode()\n        response.status = http.HTTPStatus.BAD_REQUEST\n        response.close_connection = True\n        return\n\n    response.status = http.HTTPStatus.OK\n    response.content_type = b'application/json'\n    try:\n        result = await execute.parse_execute_json(\n            db,\n            query,\n            role_name=role_name,\n            variables=variables or {},\n            globals_=globals_,\n            session_config=config,\n        )\n    except Exception as ex:\n        if debug.flags.server:\n            markup.dump(ex)\n\n        ex = await execute.interpret_error(ex, db)\n\n        response.body = json.dumps({'error': ex.to_json()}).encode()\n    else:\n        response.body = b'{\"data\":' + result + b'}'\n"
  },
  {
    "path": "edb/server/protocol/execute.pxd",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom edb.server.pgproto.pgproto cimport WriteBuffer\n\n\ncdef class ExecutionGroup:\n    cdef:\n        object group\n        list bind_datas\n\n    cdef append(self, object query_unit, WriteBuffer bind_data=?)\n"
  },
  {
    "path": "edb/server/protocol/execute.pyi",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import (\n    Any,\n    Mapping,\n    Optional,\n)\nimport immutables\n\nfrom edb import errors\nfrom edb.server import compiler\nfrom edb.server import defines as edbdef\nfrom edb.server.compiler import sertypes\nfrom edb.server.dbview import dbview\n\nasync def describe(\n    db: dbview.Database,\n    query: str,\n    *,\n    query_cache_enabled: Optional[bool] = None,\n    allow_capabilities: compiler.Capability = (\n        compiler.Capability.MODIFICATIONS),\n    query_tag: str | None = None,\n    role_name: str,\n) -> sertypes.TypeDesc:\n    ...\n\nasync def parse_execute_json(\n    db: dbview.Database,\n    query: str,\n    *,\n    variables: Mapping[str, Any] = immutables.Map(),\n    globals_: Optional[Mapping[str, Any]] = None,\n    output_format: compiler.OutputFormat = compiler.OutputFormat.JSON,\n    query_cache_enabled: Optional[bool] = None,\n    cached_globally: bool = False,\n    use_metrics: bool = True,\n    tx_isolation: edbdef.TxIsolationLevel | None = None,\n    query_tag: str | None = None,\n    role_name: str | None = None,\n) -> bytes:\n    ...\n\nasync def interpret_error(\n    exc: Exception,\n    db: dbview.Database,\n    *,\n    global_schema_pickle: object=None,\n    user_schema_pickle: object=None,\n    from_graphql: bool=False,\n) -> errors.EdgeDBError:\n    ...\n"
  },
  {
    "path": "edb/server/protocol/execute.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom typing import (\n    Any,\n    Mapping,\n    Optional,\n)\n\nfrom edgedb import scram\n\nimport asyncio\nimport base64\nimport decimal\nimport hashlib\nimport json\nimport logging\n\nimport immutables\n\nfrom edb import errors\nfrom edb.common import debug\n\nfrom edb import edgeql\nfrom edb.edgeql import qltypes\n\nfrom edb.pgsql.parser import exceptions as parser_errors\n\nfrom edb.server import compiler\nfrom edb.server import config\nfrom edb.server import defines as edbdef\nfrom edb.server import metrics\nfrom edb.server.compiler import dbstate\nfrom edb.server.compiler import errormech\nfrom edb.server.compiler cimport rpc\nfrom edb.server.compiler import sertypes\nfrom edb.server.dbview cimport dbview\nfrom edb.server.protocol cimport args_ser\nfrom edb.server.protocol cimport frontend\nfrom edb.server.protocol import ai_ext\nfrom edb.server.pgcon cimport pgcon\nfrom edb.server.pgcon import errors as pgerror\n\n\ncdef object logger = logging.getLogger('edb.server')\n\ncdef object FMT_NONE = compiler.OutputFormat.NONE\ncdef WriteBuffer NO_ARGS = args_ser.combine_raw_args()\n\n\ncdef class ExecutionGroup:\n    def __cinit__(self):\n        self.group = compiler.QueryUnitGroup()\n        self.bind_datas = []\n\n    cdef append(self, object query_unit, WriteBuffer bind_data=NO_ARGS):\n        self.group.append(query_unit, serialize=False)\n        self.bind_datas.append(bind_data)\n\n    async def execute(\n        self,\n        pgcon.PGConnection be_conn,\n        object dbv,  # can be DatabaseConnectionView or Database\n        fe_conn: frontend.AbstractFrontendConnection = None,\n        bytes state = None,\n        bint needs_commit_state = False,\n    ):\n        cdef int dbver\n\n        rv = None\n        async with be_conn.parse_execute_script_context():\n            dbver = dbv.dbver\n            parse_array = [False] * len(self.group)\n            be_conn.send_query_unit_group(\n                self.group,\n                True,  # sync\n                self.bind_datas,\n                state,\n                0,  # start\n                len(self.group),  # end\n                dbver,\n                parse_array,\n                None,  # query_prefix\n                needs_commit_state,\n            )\n            if state is not None:\n                await be_conn.wait_for_state_resp(\n                    state,\n                    state_sync=needs_commit_state,\n                    needs_commit_state=needs_commit_state,\n                )\n            for i, unit in enumerate(self.group):\n                ignore_data = unit.output_format == FMT_NONE\n                rv = await be_conn.wait_for_command(\n                    unit,\n                    parse_array[i],\n                    dbver,\n                    ignore_data=ignore_data,\n                    fe_conn=None if ignore_data else fe_conn,\n                )\n        return rv\n\n\ncpdef ExecutionGroup build_cache_persistence_units(\n    pairs: list[tuple[rpc.CompilationRequest, compiler.QueryUnitGroup]],\n    ExecutionGroup group = None,\n):\n    if group is None:\n        group = ExecutionGroup()\n    insert_sql = b'''\n        INSERT INTO \"edgedb\".\"_query_cache\"\n        (\"key\", \"schema_version\", \"input\", \"output\", \"evict\")\n        VALUES ($1, $2, $3, $4, $5)\n        ON CONFLICT (key) DO NOTHING\n    '''\n    sql_hash = hashlib.sha1(insert_sql).hexdigest().encode('latin1')\n    for request, units in pairs:\n        # FIXME: this is temporary; drop this assertion when we support scripts\n        assert len(units) == 1\n        query_unit = units[0]\n\n        assert query_unit.cache_sql is not None\n        persist, evict = query_unit.cache_sql\n\n        serialized_result = units.maybe_get_serialized(0)\n        assert serialized_result is not None\n\n        if evict:\n            group.append(compiler.QueryUnit(sql=evict, status=b''))\n        if persist:\n            group.append(compiler.QueryUnit(sql=persist, status=b''))\n        group.append(\n            compiler.QueryUnit(sql=insert_sql, sql_hash=sql_hash, status=b''),\n            args_ser.combine_raw_args((\n                query_unit.cache_key.bytes,\n                query_unit.user_schema_version.bytes,\n                request.serialize(),\n                serialized_result,\n                evict,\n            )),\n        )\n    return group\n\n\nasync def describe(\n    db: dbview.Database,\n    query: str,\n    *,\n    query_cache_enabled: Optional[bool] = None,\n    allow_capabilities: compiler.Capability = compiler.Capability.MODIFICATIONS,\n    query_tag: str | None = None,\n    role_name: str,\n) -> sertypes.TypeDesc:\n    dbv = await _get_transient_dbv(db, role_name=role_name)\n    _, compiled = await _parse(\n        dbv,\n        query,\n        query_cache_enabled=query_cache_enabled,\n        allow_capabilities=allow_capabilities,\n    )\n    if query_tag:\n        compiled.tag = query_tag\n\n    try:\n        desc = sertypes.parse(\n            compiled.query_unit_group.out_type_data,\n            edbdef.CURRENT_PROTOCOL,\n        )\n    finally:\n        db.tenant.remove_dbview(dbv)\n\n    return desc\n\n\nasync def _get_transient_dbv(\n    db: dbview.Database,\n    *,\n    query_cache_enabled: Optional[bool] = None,\n    role_name: str,\n) -> dbview.DatabaseConnectionView:\n    if query_cache_enabled is None:\n        query_cache_enabled = not (\n            debug.flags.disable_qcache or debug.flags.edgeql_compile)\n\n    tenant = db.tenant\n    dbv = await tenant.new_dbview(\n        dbname=db.name,\n        query_cache=query_cache_enabled,\n        protocol_version=edbdef.CURRENT_PROTOCOL,\n        role_name=role_name,\n    )\n    dbv.is_transient = True\n\n    return dbv\n\n\nasync def _parse(\n    dbv: dbview.DatabaseConnectionView,\n    query: str,\n    *,\n    input_format: compiler.InputFormat = compiler.InputFormat.BINARY,\n    output_format: compiler.OutputFormat = compiler.OutputFormat.BINARY,\n    allow_capabilities: compiler.Capability = compiler.Capability.MODIFICATIONS,\n    use_metrics: bool = True,\n    cached_globally: bool = False,\n    query_cache_enabled: Optional[bool] = None,\n) -> tuple[\n    rpc.CompilationRequest,\n    dbview.CompiledQuery,\n]:\n    db = dbv._db\n    tenant = db.tenant\n\n    if use_metrics:\n        metrics.query_size.observe(\n            len(query.encode('utf-8')), tenant.get_instance_name(), 'edgeql'\n        )\n\n    query_req = rpc.CompilationRequest(\n        source=edgeql.Source.from_string(query),\n        protocol_version=edbdef.CURRENT_PROTOCOL,\n        schema_version=dbv.schema_version,\n        compilation_config_serializer=db.server.compilation_config_serializer,\n        input_format=input_format,\n        output_format=output_format,\n        session_config=dbv.get_session_config(),\n        database_config=dbv.get_database_config(),\n        system_config=dbv.get_compilation_system_config(),\n        role_name=dbv._role_name,\n    )\n\n    compiled = await dbv.parse(\n        query_req,\n        cached_globally=cached_globally,\n        use_metrics=use_metrics,\n        allow_capabilities=allow_capabilities,\n    )\n\n    return query_req, compiled\n\n\n# TODO: can we merge execute and execute_script?\nasync def execute(\n    be_conn: pgcon.PGConnection,\n    dbv: dbview.DatabaseConnectionView,\n    compiled: dbview.CompiledQuery,\n    bind_args: bytes,\n    *,\n    fe_conn: frontend.AbstractFrontendConnection = None,\n    use_prep_stmt: bint = False,\n    tx_isolation: edbdef.TxIsolationLevel | None = None,\n    query_req: Optional[rpc.CompilationRequest] = None,\n):\n    cdef:\n        bytes state = None, orig_state = None\n        WriteBuffer bound_args_buf\n        bint needs_commit_state = False\n\n    query_unit = compiled.query_unit_group[0]\n\n    if not dbv.in_tx():\n        orig_state = state = dbv.serialize_state()\n        needs_commit_state = dbv.needs_commit_after_state_sync()\n\n    new_types = None\n    server = dbv.server\n    tenant = dbv.tenant\n\n    data = None\n\n    try:\n        if be_conn.last_state == state:\n            # the current status in be_conn is in sync with dbview, skip the\n            # state restoring\n            state = None\n        dbv.start(query_unit)\n        if query_unit.create_db_template:\n            await tenant.on_before_create_db_from_template(\n                query_unit.create_db_template,\n                dbv.dbname,\n                query_unit.create_db_mode,\n            )\n        if query_unit.drop_db:\n            await tenant.on_before_drop_db(\n                query_unit.drop_db,\n                dbv.dbname,\n                close_frontend_conns=query_unit.drop_db_reset_connections,\n            )\n\n        if query_unit.early_non_tx_sql:\n            # Sync state non transactionally\n            await be_conn.sql_fetch(b'select 1', state=state)\n            for sql in query_unit.early_non_tx_sql:\n                await be_conn.sql_execute(sql)\n\n        if query_unit.system_config:\n            # execute_system_config() always sync state in a separate tx,\n            # so we don't need to pass down the needs_commit_state here\n            await execute_system_config(be_conn, dbv, query_unit, state)\n        else:\n            config_ops = query_unit.config_ops\n\n            if query_unit.sql:\n                if query_unit.user_schema:\n                    await be_conn.parse_execute(\n                        query=query_unit,\n                        state=state,\n                        needs_commit_state=needs_commit_state,\n                    )\n                    if query_unit.ddl_stmt_id is not None:\n                        ddl_ret = be_conn.load_last_ddl_return(query_unit)\n                        if ddl_ret and ddl_ret['new_types']:\n                            new_types = ddl_ret['new_types']\n                else:\n                    converted_args: Optional[list[args_ser.ConvertedArg]] = None\n                    if query_unit.server_param_conversions:\n                        converted_args = (await _convert_parameters(\n                            dbv,\n                            compiled,\n                            query_unit.server_param_conversions,\n                            bind_args,\n                        )).get(0, None)\n\n                    data_types = []\n                    bound_args_buf = args_ser.recode_bind_args(\n                        dbv,\n                        compiled,\n                        bind_args,\n                        converted_args,\n                        None,\n                        data_types,\n                    )\n\n                    assert not (query_unit.database_config\n                                and query_unit.needs_readback), (\n                        \"needs_readback+database_config must use execute_script\"\n                    )\n                    read_data = (\n                        query_unit.needs_readback or query_unit.is_explain)\n\n                    data = await be_conn.parse_execute(\n                        query=query_unit,\n                        fe_conn=fe_conn if not read_data else None,\n                        bind_data=bound_args_buf,\n                        param_data_types=data_types,\n                        use_prep_stmt=use_prep_stmt,\n                        state=state,\n                        needs_commit_state=needs_commit_state,\n                        dbver=dbv.dbver,\n                        use_pending_func_cache=compiled.use_pending_func_cache,\n                        tx_isolation=tx_isolation,\n                        query_prefix=compiled.make_query_prefix(),\n                    )\n\n                    if query_unit.needs_readback and data:\n                        config_ops = [\n                            config.Operation.from_json(r[0][1:])\n                            for r in data\n                        ]\n\n                    if query_unit.is_explain:\n                        # Go back to the compiler pool to analyze\n                        # the explain output.\n                        compiler_pool = server.get_compiler_pool()\n                        r = await compiler_pool.analyze_explain_output(\n                            query_unit.query_asts, data\n                        )\n                        buf = WriteBuffer.new_message(b'D')\n                        buf.write_int16(1)  # 1 column\n                        buf.write_len_prefixed_bytes(r)\n                        fe_conn.write(buf.end_message())\n\n                if state is not None:\n                    # state is restored, clear orig_state so that we can\n                    # set be_conn.last_state correctly later\n                    orig_state = None\n\n            if query_unit.tx_savepoint_rollback:\n                dbv.rollback_tx_to_savepoint(query_unit.sp_name)\n\n            if query_unit.tx_savepoint_declare:\n                dbv.declare_savepoint(\n                    query_unit.sp_name, query_unit.sp_id)\n\n            if query_unit.create_db_template:\n                try:\n                    await tenant.on_after_create_db_from_template(\n                        query_unit.create_db,\n                        query_unit.create_db_template,\n                        query_unit.create_db_mode,\n                    )\n                except Exception:\n                    # Clean up the database if we failed to restore into it.\n                    # TODO: Is it worth having 'ready' flag that we set after\n                    # the database is fully set up, and use that to clean up\n                    # databases where a crash prevented doing this cleanup?\n                    db_name = f'{tenant.tenant_id}_{query_unit.create_db}'\n                    await be_conn.sql_execute(\n                        b'drop database \"%s\"' % db_name.encode('utf-8')\n                    )\n                    raise\n\n            if query_unit.create_db:\n                await tenant.introspect_db(query_unit.create_db)\n\n            if query_unit.drop_db:\n                tenant.on_after_drop_db(query_unit.drop_db)\n\n            if config_ops:\n                await dbv.apply_config_ops(be_conn, config_ops)\n\n            if query_unit.user_schema and debug.flags.delta_validate_reflection:\n                global_schema = (\n                    query_unit.global_schema or dbv.get_global_schema_pickle())\n                new_user_schema = await dbv.tenant._debug_introspect(\n                    be_conn, global_schema)\n                compiler_pool = dbv.server.get_compiler_pool()\n                await compiler_pool.validate_schema_equivalence(\n                    query_unit.user_schema,\n                    new_user_schema,\n                    global_schema,\n                    dbv._last_comp_state,\n                )\n                query_unit.user_schema = new_user_schema\n\n    except Exception as ex:\n        if isinstance(ex, pgerror.BackendError):\n            # If we made schema changes, include the new schema in the\n            # exception so that it can be used when interpreting.\n            if query_unit.user_schema:\n                ex._user_schema = query_unit.user_schema\n\n            # If we get an undefined function error, this is probably\n            # because of a pgfunc cache invalidation race condition,\n            # where another frontend dropped the function but we\n            # haven't processed the message yet. We are going to\n            # trigger a client retry (via errormech), but we also want\n            # to invalidate the cache entry, in cache we haven't\n            # processed the message by the retry.\n            if (\n                query_req\n                and ex.code_is(pgerror.ERROR_UNDEFINED_FUNCTION)\n            ):\n                dbv._db.invalidate_cache_entry_object(query_req)\n\n        if query_unit.source_map:\n            ex._from_sql = True\n\n        dbv.on_error()\n\n        if query_unit.tx_commit and not be_conn.in_tx() and dbv.in_tx():\n            # The COMMIT command has failed. Our Postgres connection\n            # isn't in a transaction anymore. Abort the transaction\n            # in dbview.\n            dbv.abort_tx()\n        raise\n    else:\n        side_effects = dbv.on_success(query_unit, new_types)\n        state_serializer = compiled.query_unit_group.state_serializer\n        if state_serializer is not None:\n            dbv.set_state_serializer(state_serializer)\n        if side_effects:\n            await process_side_effects(dbv, side_effects, be_conn)\n        if not dbv.in_tx() and not query_unit.tx_rollback and query_unit.sql:\n            state = dbv.serialize_state()\n            if state is not orig_state:\n                # In 3 cases the state is changed:\n                #   1. The non-tx query changed the state\n                #   2. The state is synced with dbview (orig_state is None)\n                #   3. We came out from a transaction (orig_state is None)\n                # Excluding two special case when the state is NOT changed:\n                #   1. An orphan ROLLBACK command without a paring start tx\n                #   2. There was no SQL, so the state can't have been synced.\n                be_conn.last_state = state\n                be_conn.state_reset_needs_commit = (\n                    dbv.needs_commit_after_state_sync())\n        if compiled.recompiled_cache:\n            for req, qu_group in compiled.recompiled_cache:\n                dbv.cache_compiled_query(req, qu_group)\n    finally:\n        if query_unit.drop_db:\n            tenant.allow_database_connections(query_unit.drop_db)\n\n    return data\n\n\nasync def _convert_parameters(\n    dbv: dbview.DatabaseConnectionView,\n    compiled: dbview.CompiledQuery,\n    server_param_conversions: list[dbstate.ServerParamConversion],\n    bind_args: bytes,\n) -> dict[int, list[args_ser.ConvertedArg]]:\n    \"\"\"\n    If there are server param conversions, compute them now so that they are\n    injected into the recoded bind args later.\n    \"\"\"\n\n    param_conversions: list[args_ser.ParamConversion] = (\n        args_ser.get_param_conversions(\n            dbv,\n            server_param_conversions,\n            bind_args,\n            compiled.extra_blobs,\n        )\n    )\n\n    # Cache converted args which may be used in multiple units\n    converted_args_cache: list[Optional[args_ser.ConvertedArg]] = (\n        [None] * len(param_conversions)\n    )\n\n    unit_group = compiled.query_unit_group\n\n    # First check for conversions which should be done in batches\n    ai_text_embedding_conversion_indexes: list[int] = []\n    ai_text_embedding_conversions: list[args_ser.ParamConversion] = []\n    for unit_index, converted_params_indexes in (\n        unit_group.unit_converted_param_indexes.items()\n    ):\n        for conversion_index in converted_params_indexes:\n            conversion = param_conversions[conversion_index]\n\n            conversion_name: str = conversion.get_conversion_name()\n\n            if conversion_name == 'ai_text_embedding':\n                ai_text_embedding_conversion_indexes.append(conversion_index)\n                ai_text_embedding_conversions.append(conversion)\n\n    # Compute batched conversions and store them in cache\n    if ai_text_embedding_conversions:\n        converted_args = (\n            await _batch_convert_ai_text_embedding(\n                dbv, ai_text_embedding_conversions\n            )\n        )\n        for conversion_index, converted_arg in zip(\n            ai_text_embedding_conversion_indexes,\n            converted_args,\n        ):\n            converted_args_cache[conversion_index] = converted_arg\n\n    # Do the remaining conversions\n    converted_args: dict[int, list[args_ser.ConvertedArg]] = {}\n    for unit_index, converted_params_indexes in (\n        unit_group.unit_converted_param_indexes.items()\n    ):\n        unit_converted_args: list[args_ser.ParamConversion] = []\n\n        for conversion_index in converted_params_indexes:\n            # Check for a cached conversion arg\n            if converted_arg := converted_args_cache[conversion_index]:\n                unit_converted_args.append(converted_arg)\n                continue\n\n            # Do the conversion\n            converted_arg = await _convert_parameter(\n                param_conversions[conversion_index]\n            )\n\n            unit_converted_args.append(converted_arg)\n            converted_args_cache[conversion_index] = converted_arg\n\n        if unit_converted_args:\n            converted_args[unit_index] = unit_converted_args\n\n    return converted_args\n\n\nasync def _convert_parameter(\n    conversion: args_ser.ParamConversion,\n) -> args_ser.ConvertedArg:\n\n    conversion_name = conversion.get_conversion_name()\n\n    # We receive the encoded param data from the bind_args or extra blobs\n    # and decode it manually.\n    if (\n        conversion_name == 'cast_int64_to_str'\n        or conversion_name == 'cast_int64_to_str_volatile'\n    ):\n        decoded_param_data = conversion.param_as_int()\n        return args_ser.ConvertedArgStr.new(\n            str(decoded_param_data)\n        )\n\n    elif conversion_name == 'cast_int64_to_float64':\n        decoded_param_data = conversion.param_as_int()\n\n        return args_ser.ConvertedArgFloat64.new(\n            float(decoded_param_data)\n        )\n\n    elif conversion_name == 'join_str_array':\n        decoded_param_data = conversion.param_as_array_of_str()\n\n        separator = conversion.get_additional_info()[0]\n        return args_ser.ConvertedArgStr.new(\n            separator.join(decoded_param_data)\n        )\n\n    elif conversion_name == 'ai_text_embedding':\n        raise RuntimeError(f'conversion should be batched: {conversion_name}')\n\n    else:\n        raise errors.QueryError(\n            f'unknown param conversion: {conversion_name}'\n        )\n\n\nasync def _batch_convert_ai_text_embedding(\n    dbv: dbview.DatabaseConnectionView,\n    conversions: list[args_ser.ParamConversion],\n) -> list[args_ser.ConvertedArg]:\n    embeddings_inputs: list[tuple[str, str]] = [\n        (\n            conversion_data.get_additional_info()[0],\n            conversion_data.param_as_str(),\n        )\n        for conversion_data in conversions\n    ]\n\n    tenant = dbv.tenant\n    db = tenant.maybe_get_db(dbname=dbv.dbname)\n    assert db is not None\n\n    embeddings_result = await ai_ext.generate_embeddings_for_texts(\n        db,\n        tenant.get_http_client(originator=\"ai/index\"),\n        embeddings_inputs,\n    )\n    if embeddings_result.too_long:\n        long_input = embeddings_inputs[embeddings_result.too_long[0]][1][:100]\n        raise errors.QueryError(\n            f'Search text exceeds maximum input token length: {long_input}...'\n        )\n    if not embeddings_result.success:\n        raise RuntimeError('failed to get embeddings')\n\n    return [\n        args_ser.ConvertedArgListFloat32.new(\n            embeddings\n        )\n        for embeddings in embeddings_result.success\n    ]\n\n\nasync def execute_script(\n    conn: pgcon.PGConnection,\n    dbv: dbview.DatabaseConnectionView,\n    compiled: dbview.CompiledQuery,\n    bind_args: bytes,\n    *,\n    query_req: Optional[rpc.CompilationRequest] = None,\n    fe_conn: Optional[frontend.AbstractFrontendConnection],\n):\n    cdef:\n        bytes state = None, orig_state = None\n        ssize_t sent = 0\n        bint in_tx, sync, no_sync\n        object user_schema, extensions, ext_config_settings, cached_reflection\n        object global_schema, roles\n        WriteBuffer bind_data\n        int dbver = dbv.dbver\n        bint parse, needs_commit_state = False\n\n    user_schema = extensions = ext_config_settings = cached_reflection = None\n    feature_used_metrics = None\n    global_schema = roles = None\n    unit_group = compiled.query_unit_group\n    query_prefix = compiled.make_query_prefix()\n    query_unit = None\n\n    sync = False\n    no_sync = False\n    in_tx = dbv.in_tx()\n    if not in_tx:\n        orig_state = state = dbv.serialize_state()\n        needs_commit_state = dbv.needs_commit_after_state_sync()\n\n    data = None\n\n    try:\n        if conn.last_state == state:\n            # the current status in be_conn is in sync with dbview, skip the\n            # state restoring\n            state = None\n        async with conn.parse_execute_script_context():\n\n            converted_args: Optional[dict[int, list[args_ser.ConvertedArg]]] = None\n            if unit_group.server_param_conversions:\n                converted_args = await _convert_parameters(\n                    dbv,\n                    compiled,\n                    unit_group.server_param_conversions,\n                    bind_args,\n                )\n\n            parse_array = [False] * len(unit_group)\n            for idx, query_unit in enumerate(unit_group):\n                if fe_conn is not None and fe_conn.cancelled:\n                    raise ConnectionAbortedError\n\n                assert not query_unit.is_explain\n\n                # XXX: pull out?\n                # We want to minimize the round trips we need to make, so\n                # ideally we buffer up everything, send it once, and then issue\n                # one SYNC. This gets messed up if there are commands where\n                # we need to read back information, though, such as SET GLOBAL.\n                #\n                # Because of that, we look for the next command that\n                # needs read back (probably there won't be one!), and\n                # execute everything up to that point at once,\n                # finished by a FLUSH.\n                if idx >= sent:\n                    no_sync = False\n                    for n in range(idx, len(unit_group)):\n                        ng = unit_group[n]\n                        if ng.ddl_stmt_id or ng.needs_readback:\n                            sent = n + 1\n                            if ng.needs_readback:\n                                no_sync = True\n                            break\n                    else:\n                        sent = len(unit_group)\n\n                    sync = sent == len(unit_group) and not no_sync\n\n                    bind_array = args_ser.recode_bind_args_for_script(\n                        dbv,\n                        compiled,\n                        bind_args,\n                        converted_args,\n                        idx,\n                        sent,\n                    )\n\n                    dbver = dbv.dbver\n                    conn.send_query_unit_group(\n                        unit_group,\n                        sync,\n                        bind_array,\n                        state,\n                        idx,\n                        sent,\n                        dbver,\n                        parse_array,\n                        query_prefix,\n                        needs_commit_state,\n                    )\n\n                if idx == 0 and state is not None:\n                    await conn.wait_for_state_resp(\n                        state,\n                        state_sync=needs_commit_state,\n                        needs_commit_state=needs_commit_state,\n                    )\n                    conn.state_reset_needs_commit = needs_commit_state\n                    # state is restored, clear orig_state so that we can\n                    # set conn.last_state correctly later\n                    orig_state = None\n\n                new_types = None\n                dbv.start_implicit(query_unit)\n                config_ops = query_unit.config_ops\n\n                if query_unit.user_schema:\n                    user_schema = query_unit.user_schema\n                    extensions = query_unit.extensions\n                    ext_config_settings = query_unit.ext_config_settings\n                    cached_reflection = query_unit.cached_reflection\n                    feature_used_metrics = query_unit.feature_used_metrics\n\n                if query_unit.global_schema:\n                    global_schema = query_unit.global_schema\n                    roles = query_unit.roles\n\n                if query_unit.sql:\n                    parse = parse_array[idx]\n                    fe_output = query_unit.output_format != FMT_NONE\n                    ignore_data = (\n                        not fe_output\n                        and not query_unit.needs_readback\n                    )\n                    data = await conn.wait_for_command(\n                        query_unit,\n                        parse,\n                        dbver,\n                        ignore_data=ignore_data,\n                        fe_conn=fe_conn if fe_output else None,\n                    )\n\n                    if query_unit.ddl_stmt_id:\n                        ddl_ret = conn.load_last_ddl_return(query_unit)\n                        if ddl_ret and ddl_ret['new_types']:\n                            new_types = ddl_ret['new_types']\n\n                    if query_unit.needs_readback and data:\n                        config_ops = [\n                            config.Operation.from_json(r[0][1:])\n                            for r in data\n                        ]\n\n                if config_ops:\n                    await dbv.apply_config_ops(conn, config_ops)\n\n                side_effects = dbv.on_success(query_unit, new_types)\n                if side_effects:\n                    raise errors.InternalServerError(\n                        \"Side-effects in implicit transaction!\"\n                    )\n\n        # Need to sync before calling process_side_effects, which will\n        # look at the database. Also, want to sync before we record success,\n        # since sync could fail.\n        if sent and not sync:\n            sync = True\n            await conn.sync()\n\n    except Exception as e:\n        dbv.on_error()\n\n        if isinstance(e, pgerror.BackendError):\n            # Include the new schema in the exception so that it can be\n            # used when interpreting.\n            e._user_schema = dbv.get_user_schema_pickle()\n\n            # If we get an undefined function error, this is probably\n            # because of a pgfunc cache invalidation race condition,\n            # where another frontend dropped the function but we\n            # haven't processed the message yet. We are going to\n            # trigger a client retry (via errormech), but we also want\n            # to invalidate the cache entry, in cache we haven't\n            # processed the message by the retry.\n            if (\n                query_req\n                and e.code_is(pgerror.ERROR_UNDEFINED_FUNCTION)\n            ):\n                dbv._db.invalidate_cache_entry_object(query_req)\n\n        if query_unit and query_unit.source_map:\n            e._from_sql = True\n\n        if not in_tx and dbv.in_tx():\n            # Abort the implicit transaction\n            dbv.abort_tx()\n\n        # If something went wrong that is *not* on the backend side, force\n        # an error to occur on the SQL side.\n        if not isinstance(e, pgerror.BackendError):\n            await conn.force_error()\n\n        raise\n\n    else:\n        updated_user_schema = False\n        if user_schema and debug.flags.delta_validate_reflection:\n            cur_global_schema = (\n                global_schema or dbv.get_global_schema_pickle())\n            new_user_schema = await dbv.tenant._debug_introspect(\n                conn, cur_global_schema)\n            compiler_pool = dbv.server.get_compiler_pool()\n            await compiler_pool.validate_schema_equivalence(\n                user_schema,\n                new_user_schema,\n                cur_global_schema,\n                dbv._last_comp_state,\n            )\n            user_schema = new_user_schema\n            updated_user_schema = True\n\n        if not in_tx:\n            side_effects = dbv.commit_implicit_tx(\n                user_schema,\n                extensions,\n                ext_config_settings,\n                global_schema,\n                roles,\n                cached_reflection,\n                feature_used_metrics,\n            )\n            if side_effects:\n                await process_side_effects(dbv, side_effects, conn)\n            state = dbv.serialize_state()\n            if state is not orig_state:\n                conn.last_state = state\n                conn.state_reset_needs_commit = (\n                    dbv.needs_commit_after_state_sync())\n        elif updated_user_schema:\n            dbv._in_tx_user_schema_pickle = user_schema\n\n        if unit_group.state_serializer is not None:\n            dbv.set_state_serializer(unit_group.state_serializer)\n\n    finally:\n        if sent and not sync:\n            await conn.sync()\n\n    return data\n\n\nasync def execute_system_config(\n    conn: pgcon.PGConnection,\n    dbv: dbview.DatabaseConnectionView,\n    query_unit: compiler.QueryUnit,\n    state: bytes | None,\n):\n    if query_unit.is_system_config:\n        dbv.server.before_alter_system_config()\n\n    # Sync state\n    await conn.sql_fetch(b'select 1', state=state)\n\n    if query_unit.sql:\n        data = await conn.sql_fetch_col(query_unit.sql)\n    else:\n        data = None\n\n    if data:\n        # Prefer encoded op produced by the SQL command.\n        if data[0][0] != 0x01:\n            raise errors.InternalServerError(\n                f\"unexpected JSONB version produced by SQL statement for \"\n                f\"CONFIGURE INSTANCE: {data[0][0]}\"\n            )\n        config_ops = [config.Operation.from_json(r[1:]) for r in data]\n    else:\n        # Otherwise, fall back to staticly evaluated op.\n        config_ops = query_unit.config_ops\n    await dbv.apply_config_ops(conn, config_ops)\n\n    await conn.sql_execute(b'delete from _config_cache')\n\n    # If this is a backend configuration setting we also\n    # need to make sure it has been loaded.\n    if query_unit.backend_config:\n        await conn.sql_execute(b'SELECT pg_reload_conf()')\n\n\nasync def process_side_effects(dbv, side_effects, conn):\n    signal_side_effects(dbv, side_effects)\n\n    if side_effects & dbview.SideEffects.DatabaseConfigChanges:\n        tenant = dbv.tenant\n        await tenant.process_local_database_config_change(conn, dbv.dbname)\n\n\ndef signal_side_effects(dbv, side_effects):\n    tenant = dbv.tenant\n    if not tenant.accept_new_tasks:\n        return\n\n    if side_effects & dbview.SideEffects.SchemaChanges:\n        tenant.create_task(\n            tenant.signal_sysevent(\n                'schema-changes',\n                dbname=dbv.dbname,\n            ),\n            interruptable=False,\n        )\n\n    if side_effects & dbview.SideEffects.GlobalSchemaChanges:\n        tenant.create_task(\n            tenant.signal_sysevent(\n                'global-schema-changes',\n            ),\n            interruptable=False,\n        )\n\n    if side_effects & dbview.SideEffects.DatabaseConfigChanges:\n        tenant.create_task(\n            tenant.signal_sysevent(\n                'database-config-changes',\n                dbname=dbv.dbname,\n            ),\n            interruptable=False,\n        )\n\n    if side_effects & dbview.SideEffects.DatabaseChanges:\n        tenant.create_task(\n            tenant.signal_sysevent(\n                'database-changes',\n            ),\n            interruptable=False,\n        )\n\n    if side_effects & dbview.SideEffects.InstanceConfigChanges:\n        tenant.create_task(\n            tenant.signal_sysevent(\n                'system-config-changes',\n            ),\n            interruptable=False,\n        )\n\n\nasync def parse_execute_json(\n    db: dbview.Database,\n    query: str,\n    *,\n    variables: Mapping[str, Any] = immutables.Map(),\n    globals_: Optional[Mapping[str, Any]] = None,\n    session_config: Optional[Mapping[str, Any]] = None,\n    output_format: compiler.OutputFormat = compiler.OutputFormat.JSON,\n    query_cache_enabled: Optional[bool] = None,\n    # WARNING: only set cached_globally to True when the query is\n    # strictly referring to only shared stable objects in user schema\n    # or anything from std schema, for example:\n    #     YES:  select ext::auth::UIConfig { ... }\n    #     NO:   select default::User { ... }\n    cached_globally: bool = False,\n    use_metrics: bool = True,\n    tx_isolation: edbdef.TxIsolationLevel | None = None,\n    query_tag: str | None = None,\n    role_name: str | None = None,\n) -> bytes:\n    if role_name is None:\n        role_name = edbdef.EDGEDB_SUPERUSER\n\n    dbv: dbview.DatabaseConnectionView = await _get_transient_dbv(\n        db,\n        query_cache_enabled=query_cache_enabled,\n        role_name=role_name,\n    )\n    dbv.decode_json_session_config(session_config)\n    query_req, compiled = await _parse(\n        dbv,\n        query,\n        input_format=compiler.InputFormat.JSON,\n        output_format=output_format,\n        allow_capabilities=compiler.Capability.MODIFICATIONS,\n        use_metrics=use_metrics,\n        cached_globally=cached_globally,\n    )\n    if query_tag:\n        compiled.tag = query_tag\n\n    tenant = db.tenant\n    async with tenant.with_pgcon(db.name) as pgcon:\n        try:\n            return await execute_json(\n                pgcon,\n                dbv,\n                compiled,\n                variables=variables,\n                globals_=globals_,\n                tx_isolation=tx_isolation,\n                query_req=query_req,\n            )\n        finally:\n            tenant.remove_dbview(dbv)\n\n\nasync def execute_json(\n    be_conn: pgcon.PGConnection,\n    dbv: dbview.DatabaseConnectionView,\n    compiled: dbview.CompiledQuery,\n    variables: Mapping[str, Any] = immutables.Map(),\n    globals_: Optional[Mapping[str, Any]] = None,\n    *,\n    fe_conn: Optional[frontend.AbstractFrontendConnection] = None,\n    use_prep_stmt: bint = False,\n    tx_isolation: edbdef.TxIsolationLevel | None = None,\n    query_req: Optional[rpc.CompilationRequest] = None,\n) -> bytes:\n    if globals_ is None:\n        globals_ = {}\n\n    if compiled.query_unit_group.json_permissions:\n        # Inject any required permissions into the globals json.\n\n        superuser, available_permissions = dbv.get_permissions()\n\n        for permission in compiled.query_unit_group.json_permissions:\n            if permission in globals_:\n                raise RuntimeError(\n                    f\"Permission cannot be passed as globals: '{permission}'\"\n                )\n\n            globals_[permission] = (\n                superuser or permission in available_permissions\n            )\n\n    # TODO: only when needed? in a less dodgy way??\n    for k, v in dbv._sys_globals.items():\n        if k in globals_:\n            raise RuntimeError(\n                f\"System global '{k}' cannot be explicitly specified\"\n            )\n        globals_[k] = v\n\n    dbv.set_globals(immutables.Map({\n        \"__::__edb_json_globals__\": config.SettingValue(\n            name=\"__::__edb_json_globals__\",\n            value=_encode_json_value(globals_),\n            source='global',\n            scope=qltypes.ConfigScope.GLOBAL,\n        )\n    }))\n\n    qug = compiled.query_unit_group\n\n    args = []\n    if qug.in_type_args:\n        for param in qug.in_type_args:\n            value = variables.get(param.name)\n            args.append(value)\n\n    bind_args = _encode_args(args)\n\n    force_script = any(x.needs_readback for x in qug)\n    if len(qug) > 1 or force_script:\n        if tx_isolation is not None:\n            raise errors.InternalServerError(\n                \"execute_script does not support \"\n                \"modified transaction isolation\"\n            )\n        data = await execute_script(\n            be_conn,\n            dbv,\n            compiled,\n            bind_args,\n            fe_conn=fe_conn,\n            query_req=query_req,\n        )\n    else:\n        if tx_isolation is not None:\n            if dbv.in_tx():\n                raise errors.InternalServerError(\n                    \"cannot run statement with alternate transaction \"\n                    \"isolation: already in a transaction\"\n                )\n\n            query_unit = compiled.query_unit_group[0]\n            if not query_unit.is_transactional:\n                raise errors.InternalServerError(\n                    \"cannot run statement with alternate transaction \"\n                    \"isolation: statement is not transactional\"\n                )\n\n        data = await execute(\n            be_conn,\n            dbv,\n            compiled,\n            bind_args,\n            fe_conn=fe_conn,\n            tx_isolation=tx_isolation,\n            query_req=query_req,\n        )\n\n    if fe_conn is None:\n        if not data or len(data) > 1 or len(data[0]) != 1:\n            raise errors.InternalServerError(\n                f'received incorrect response data for a JSON query')\n\n        return data[0][0]\n    else:\n        return None\n\n\nclass DecimalEncoder(json.JSONEncoder):\n    def encode(self, obj):\n        if isinstance(obj, dict):\n            return '{' + ', '.join(\n                    f'{self.encode(k)}: {self.encode(v)}'\n                    for (k, v) in obj.items()\n                ) + '}'\n        if isinstance(obj, list):\n            return '[' + ', '.join(map(self.encode, obj)) + ']'\n        if isinstance(obj, bytes):\n            return self.encode(base64.b64encode(obj).decode())\n        if isinstance(obj, decimal.Decimal):\n            return f'{obj:f}'\n        return super().encode(obj)\n\n\ncdef bytes _encode_json_value(object val):\n    jarg = json.dumps(val, cls=DecimalEncoder)\n\n    return b'\\x01' + jarg.encode('utf-8')\n\n\ncdef bytes _encode_args(list args):\n    cdef:\n        WriteBuffer out_buf = WriteBuffer.new()\n\n    if args:\n        out_buf.write_int32(len(args))\n        for arg in args:\n            out_buf.write_int32(0)  # reserved\n            if arg is None:\n                out_buf.write_int32(-1)\n            else:\n                jval = _encode_json_value(arg)\n                out_buf.write_int32(len(jval))\n                out_buf.write_bytes(jval)\n\n    return bytes(out_buf)\n\n\ncdef _check_for_ise(exc):\n    # Unwrap ExceptionGroup that has only one Exception\n    if isinstance(exc, BaseExceptionGroup) and len(exc.exceptions) == 1:\n        exc = exc.exceptions[0]\n\n    if not isinstance(exc, errors.EdgeDBError):\n        # TODO(rename): change URL once we can\n        nexc = errors.InternalServerError(\n            f'{type(exc).__name__}: {exc}',\n            hint=(\n                f'This is most likely a bug in Gel. '\n                f'Please consider opening an issue ticket '\n                f'at https://github.com/edgedb/edgedb/issues/new'\n                f'?template=bug_report.md'\n            ),\n        ).with_traceback(exc.__traceback__)\n        formatted = getattr(exc, '__formatted_error__', None)\n        if formatted:\n            nexc.__formatted_error__ = formatted\n        if isinstance(exc, BaseExceptionGroup):\n            nexc.__cause__ = exc.with_traceback(None)\n        exc = nexc\n\n    return exc\n\n\nasync def interpret_error(\n    exc: Exception,\n    db: dbview.Database,\n    *,\n    global_schema_pickle: object=None,\n    user_schema_pickle: object=None,\n    from_graphql: bool=False,\n) -> Exception:\n\n    if isinstance(exc, RecursionError):\n        exc = errors.UnsupportedFeatureError(\n            \"The query caused the compiler \"\n            \"stack to overflow. It is likely too deeply nested.\",\n            hint=(\n                \"If the query does not contain deep nesting, \"\n                \"this may be a bug.\"\n            ),\n        )\n\n    elif isinstance(exc, pgerror.BackendError):\n        try:\n            from_sql = getattr(exc, '_from_sql', False)\n            source_map = getattr(exc, '_source_map', None)\n            fields = exc.fields\n\n            static_exc = errormech.static_interpret_backend_error(\n                fields, from_graphql=from_graphql\n            )\n\n            # only use the backend if schema is required\n            if static_exc is errormech.SchemaRequired:\n                # Grab the schema from the exception first, if it is present.\n                user_schema_pickle = (\n                    getattr(exc, '_user_schema', None)\n                    or user_schema_pickle\n                    or db.user_schema_pickle\n                )\n                global_schema_pickle = (\n                    global_schema_pickle or db._index._global_schema_pickle\n                )\n                compiler_pool = db._index._server.get_compiler_pool()\n                exc = await compiler_pool.interpret_backend_error(\n                    user_schema_pickle,\n                    global_schema_pickle,\n                    fields,\n                    from_graphql,\n                )\n\n            elif isinstance(static_exc, (\n                    errors.DuplicateDatabaseDefinitionError,\n                    errors.UnknownDatabaseError)):\n                tenant_id = db.tenant.tenant_id\n                message = static_exc.args[0].replace(f'{tenant_id}_', '')\n                exc = type(static_exc)(message)\n            else:\n                exc = static_exc\n\n            if from_sql and isinstance(exc, errors.InternalServerError):\n                exc = errors.ExecutionError(*exc.args)\n\n            # Translate error position for SQL queries if we can\n            if source_map and isinstance(exc, errors.EdgeDBError):\n                if 'P' in fields:\n                    errors.EdgeDBError.set_position(\n                        exc,\n                        source_map.translate(int(fields['P'])),\n                        None,\n                    )\n\n            # Include hint/detail from SQL queries also, if we haven't\n            # produced our own.\n            if from_sql and isinstance(exc, errors.EdgeDBError):\n                if 'H' in fields or 'D' in fields:\n                    hint = exc.hint or fields.get('H')\n                    details = exc.details or fields.get('D')\n                    # ... there is some sort of cython bug/\"feature\"\n                    # involving the type annotation above which causes\n                    # exc.set_hint_and_details to fail, so we copy it\n                    # to a new variable.\n                    exc2: object = exc\n                    exc2.set_hint_and_details(hint, details)\n\n        except Exception as e:\n            from edb.common import debug\n            if debug.flags.server:\n                debug.dump(e)\n\n            exc = RuntimeError(\n                'unhandled error while calling interpret_backend_error(); '\n                'run with EDGEDB_DEBUG_SERVER to debug.')\n\n    elif isinstance(exc, parser_errors.PSqlParseError):\n        exc = errormech.static_interpret_psql_parse_error(exc)\n\n    return _check_for_ise(exc)\n\n\ndef interpret_simple_error(\n    exc: Exception,\n) -> Exception:\n    \"\"\"Intepret a protocol error not associated with a query or schema\"\"\"\n\n    if isinstance(exc, pgerror.BackendError):\n        static_exc = errormech.static_interpret_backend_error(exc.fields)\n        if static_exc is not errormech.SchemaRequired:\n            exc = static_exc\n\n    return _check_for_ise(exc)\n"
  },
  {
    "path": "edb/server/protocol/frontend.pxd",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom edb.server.dbview cimport dbview\nfrom edb.server.pgcon cimport pgcon\nfrom edb.server.pgproto.pgproto cimport ReadBuffer, WriteBuffer\n\n\ncdef class AbstractFrontendConnection:\n\n    cdef write(self, WriteBuffer buf)\n    cdef flush(self)\n\n\ncdef class FrontendConnection(AbstractFrontendConnection):\n\n    cdef:\n        str _id\n        object server\n        readonly object tenant\n        object loop\n        readonly str dbname\n        str username\n        dbview.Database database\n\n        pgcon.PGConnection _pinned_pgcon\n        bint _pinned_pgcon_in_tx\n        int _get_pgcon_cc\n\n        object _transport\n        WriteBuffer _write_buf\n        object _write_waiter\n        object connection_made_at\n        int _query_count\n\n        ReadBuffer buffer\n        object _msg_take_waiter\n\n        object started_idling_at\n        bint idling\n\n        bint _passive_mode\n\n        bint authed\n        object _main_task\n        bint _cancelled\n        bint _stop_requested\n        bint _pgcon_released_in_connection_lost\n\n        bint debug\n\n        object _transport_proto\n        bint _external_auth\n\n    cdef _after_idling(self)\n    cdef _main_task_created(self)\n    cdef _main_task_stopped_normally(self)\n    cdef write_error(self, exc)\n    cdef stop_connection(self)\n    cdef abort_pinned_pgcon(self)\n    cdef is_in_tx(self)\n\n    cdef WriteBuffer _make_authentication_sasl_initial(self, list methods)\n    cdef _expect_sasl_initial_response(self)\n    cdef WriteBuffer _make_authentication_sasl_msg(\n        self, bytes data, bint final)\n    cdef bytes _expect_sasl_response(self)\n"
  },
  {
    "path": "edb/server/protocol/frontend.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport asyncio\nimport contextlib\nimport logging\nimport time\n\nfrom edgedb import scram\n\nfrom edb import errors\nfrom edb.common import debug\nfrom edb.server import defines\nfrom edb.server import args as srvargs, metrics\nfrom edb.server.pgcon import errors as pgerror\n\nfrom . cimport auth_helpers\n\n\nDEF FLUSH_BUFFER_AFTER = 100_000\ncdef object logger = logging.getLogger('edb.server')\n\n\ncdef class AbstractFrontendConnection:\n\n    cdef write(self, WriteBuffer buf):\n        raise NotImplementedError\n\n    cdef flush(self):\n        raise NotImplementedError\n\n\ncdef class FrontendConnection(AbstractFrontendConnection):\n    interface = \"frontend\"\n\n    def __init__(\n        self,\n        server,\n        tenant,\n        *,\n        passive: bool,\n        transport: srvargs.ServerConnTransport,\n        external_auth: bool,\n        connection_made_at: float | None = None,\n    ):\n        self._id = server.on_binary_client_created()\n        self.server = server\n        self.tenant = tenant\n        self.loop = server.get_loop()\n        self.dbname = None\n\n        self._pinned_pgcon = None\n        self._pinned_pgcon_in_tx = False\n        self._get_pgcon_cc = 0\n\n        self.connection_made_at = connection_made_at\n        self._query_count = 0\n        self._transport = None\n        self._write_buf = None\n        self._write_waiter = None\n\n        self.buffer = ReadBuffer()\n        self._msg_take_waiter = None\n\n        self.idling = False\n        self.started_idling_at = 0.0\n\n        # In \"passive\" mode the protocol is instantiated to parse and execute\n        # just what's in the buffer. It cannot \"wait for message\". This\n        # is used to implement binary protocol over http+fetch.\n        self._passive_mode = passive\n\n        self.authed = False\n        self._main_task = None\n        self._cancelled = False\n        self._stop_requested = False\n        self._pgcon_released_in_connection_lost = False\n\n        self.debug = debug.flags.server_proto\n\n        self._transport_proto = transport\n        self._external_auth = external_auth\n\n    def get_id(self):\n        return self._id\n\n    cdef is_in_tx(self):\n        return False\n\n    # backend connection\n\n    def __del__(self):\n        # Should not ever happen, there's a strong ref to\n        # every client connection until it hits connection_lost().\n        if self._pinned_pgcon is not None:\n            # XXX/TODO: add test diagnostics for this and\n            # fail all tests if this ever happens.\n            self.abort_pinned_pgcon()\n\n    async def get_pgcon(self) -> pgcon.PGConnection:\n        if self._cancelled or self._pgcon_released_in_connection_lost:\n            raise RuntimeError(\n                'cannot acquire a pgconn; the connection is closed')\n        self._get_pgcon_cc += 1\n        try:\n            if self._get_pgcon_cc > 1:\n                raise RuntimeError('nested get_pgcon() calls are prohibited')\n            if self.is_in_tx():\n                #  In transaction. We must have a working pinned connection.\n                if not self._pinned_pgcon_in_tx or self._pinned_pgcon is None:\n                    raise RuntimeError(\n                        'get_pgcon(): in dbview transaction, '\n                        'but `_pinned_pgcon` is None')\n                return self._pinned_pgcon\n            if self._pinned_pgcon is not None:\n                raise RuntimeError('there is already a pinned pgcon')\n            conn = await self.tenant.acquire_pgcon(self.dbname)\n            self._pinned_pgcon = conn\n            conn.pinned_by = self\n            return conn\n        except Exception:\n            self._get_pgcon_cc -= 1\n            raise\n\n    def maybe_release_pgcon(self, pgcon.PGConnection conn):\n        self._get_pgcon_cc -= 1\n        if self._get_pgcon_cc < 0:\n            raise RuntimeError(\n                'maybe_release_pgcon() called more times than get_pgcon()')\n        if self._pinned_pgcon is not conn:\n            raise RuntimeError('mismatched released connection')\n\n        if self.is_in_tx():\n            if self._cancelled:\n                # There could be a situation where we cancel the protocol while\n                # it's in a transaction. In which case we want to immediately\n                # return the connection to the pool (where it would be\n                # discarded and re-opened.)\n                conn.pinned_by = None\n                self._pinned_pgcon = None\n                if not self._pgcon_released_in_connection_lost:\n                    self.tenant.release_pgcon(\n                        self.dbname,\n                        conn,\n                        discard=debug.flags.server_clobber_pg_conns,\n                    )\n            else:\n                self._pinned_pgcon_in_tx = True\n        else:\n            conn.pinned_by = None\n            self._pinned_pgcon_in_tx = False\n            self._pinned_pgcon = None\n            if not self._pgcon_released_in_connection_lost:\n                self.tenant.release_pgcon(\n                    self.dbname,\n                    conn,\n                    discard=debug.flags.server_clobber_pg_conns,\n                )\n\n    @contextlib.asynccontextmanager\n    async def with_pgcon(self):\n        con = await self.get_pgcon()\n        try:\n            yield con\n        finally:\n            self.maybe_release_pgcon(con)\n\n    def on_aborted_pgcon(self, pgcon.PGConnection conn):\n        try:\n            self._pinned_pgcon = None\n\n            if not self._pgcon_released_in_connection_lost:\n                self.tenant.release_pgcon(self.dbname, conn, discard=True)\n\n            if conn.aborted_with_error is not None:\n                self.write_error(conn.aborted_with_error)\n        finally:\n            self.close()  # will flush\n\n    cdef abort_pinned_pgcon(self):\n        if self._pinned_pgcon is not None:\n            self._pinned_pgcon.pinned_by = None\n            self._pinned_pgcon.abort()\n            self.tenant.release_pgcon(\n                self.dbname, self._pinned_pgcon, discard=True)\n            self._pinned_pgcon = None\n\n    # I/O write methods, implements AbstractFrontendConnection\n\n    cdef write(self, WriteBuffer buf):\n        # One rule for this method: don't write partial messages.\n        if self._write_buf is not None:\n            self._write_buf.write_buffer(buf)\n            if self._write_buf.len() >= FLUSH_BUFFER_AFTER:\n                self.flush()\n        else:\n            self._write_buf = buf\n\n    cdef flush(self):\n        if self._transport is None:\n            # could be if the connection is lost and a coroutine\n            # method is finalizing.\n            raise ConnectionAbortedError\n        if self._write_buf is not None and self._write_buf.len():\n            buf = self._write_buf\n            self._write_buf = None\n            self._transport.write(memoryview(buf))\n\n    def pause_writing(self):\n        if self._write_waiter and not self._write_waiter.done():\n            return\n        self._write_waiter = self.loop.create_future()\n\n    def resume_writing(self):\n        if not self._write_waiter or self._write_waiter.done():\n            return\n        self._write_waiter.set_result(True)\n\n    # I/O read methods\n\n    def data_received(self, data):\n        self.buffer.feed_data(data)\n        if self._msg_take_waiter is not None and self.buffer.take_message():\n            self._msg_take_waiter.set_result(True)\n            self._msg_take_waiter = None\n\n    def eof_received(self):\n        pass\n\n    cdef _after_idling(self):\n        # Hook for EdgeConnection\n        pass\n\n    async def wait_for_message(self, *, bint report_idling):\n        if self.buffer.take_message():\n            return\n        if self._passive_mode:\n            raise RuntimeError('cannot wait for more messages in passive mode')\n        if self._transport is None:\n            # could be if the connection is lost and a coroutine\n            # method is finalizing.\n            raise ConnectionAbortedError\n\n        self._msg_take_waiter = self.loop.create_future()\n        if report_idling:\n            self.idling = True\n            self.started_idling_at = time.monotonic()\n\n        try:\n            await self._msg_take_waiter\n        finally:\n            self.idling = False\n\n        self._after_idling()\n\n    def is_idle(self, expiry_time: float):\n        # A connection is idle if it awaits for the next message for\n        # client for too long (even if it is in an open transaction!)\n        return self.idling and self.started_idling_at < expiry_time\n\n    # establishing a new connection\n\n    cdef _main_task_created(self):\n        pass\n\n    cdef _main_task_stopped_normally(self):\n        pass\n\n    def get_tenant_label(self):\n        if self.tenant is None:\n            return \"unknown\"\n        else:\n            return self.tenant.get_instance_name()\n\n    def connection_made(self, transport):\n        if self.tenant is None:\n            self._transport = transport\n            self._main_task = self.loop.create_task(self.handshake())\n            self._main_task_created()\n        elif self.tenant.is_accepting_connections():\n            self._transport = transport\n            self._main_task = self.tenant.create_task(\n                self.main(), interruptable=False\n            )\n            self._main_task_created()\n        else:\n            transport.abort()\n\n    async def handshake(self):\n        try:\n            await self._handshake()\n        except Exception as ex:\n            if self._transport is not None:\n                # If there's no transport it means that the connection\n                # was aborted, in which case we don't really care about\n                # reporting the exception.\n                self.write_error(ex)\n                self.close()\n\n            if not isinstance(ex, (errors.ProtocolError,\n                                   errors.AuthenticationError)):\n                self.loop.call_exception_handler({\n                    'message': (\n                        f'unhandled error in {self.__class__.__name__} while '\n                        'accepting new connection'\n                    ),\n                    'exception': ex,\n                    'protocol': self,\n                    'transport': self._transport,\n                    'task': self._main_task,\n                })\n\n    async def _handshake(self):\n        if self.tenant is None:\n            self.tenant = self.server.get_default_tenant()\n        if self.tenant.is_accepting_connections():\n            self._main_task = self.tenant.create_task(\n                self.main(), interruptable=False\n            )\n        else:\n            if self._transport is not None:\n                self._transport.abort()\n\n    # main skeleton\n\n    async def main_step(self, char mtype):\n        raise NotImplementedError\n\n    cdef write_error(self, exc):\n        raise NotImplementedError\n\n    async def main(self):\n        cdef char mtype\n\n        try:\n            await self.authenticate()\n        except Exception as ex:\n            if self._transport is not None:\n                # If there's no transport it means that the connection\n                # was aborted, in which case we don't really care about\n                # reporting the exception.\n                self.write_error(ex)\n                self.close()\n\n            if not isinstance(ex, (errors.ProtocolError,\n                                   errors.AuthenticationError)):\n                self.loop.call_exception_handler({\n                    'message': (\n                        f'unhandled error in {self.__class__.__name__} while '\n                        'accepting new connection'\n                    ),\n                    'exception': ex,\n                    'protocol': self,\n                    'transport': self._transport,\n                    'task': self._main_task,\n                })\n\n            return\n\n        self.authed = True\n\n        try:\n            while True:\n                if self._cancelled:\n                    self.abort()\n                    return\n\n                if self._stop_requested:\n                    break\n\n                if not self.buffer.take_message():\n                    if self._passive_mode:\n                        # In \"passive\" mode we only parse what's in the buffer\n                        # and return. If there's any unparsed (incomplete) data\n                        # in the buffer it's an error.\n                        if self.buffer._length:\n                            raise RuntimeError(\n                                'unparsed data in the read buffer')\n                        # Flush whatever data is in the internal buffer before\n                        # returning.\n                        self.flush()\n                        return\n                    await self.wait_for_message(report_idling=True)\n\n                mtype = self.buffer.get_message_type()\n                if await self.main_step(mtype):\n                    break\n\n        except asyncio.CancelledError:\n            # Happens when the connection is aborted, the backend is\n            # being closed and propagates CancelledError to all\n            # EdgeCon methods that await on, say, the compiler process.\n            # We shouldn't have CancelledErrors otherwise, therefore,\n            # in this situation we just silently exit.\n            pass\n\n        except ConnectionError:\n            metrics.connection_errors.inc(\n                1.0, self.get_tenant_label(),\n            )\n\n        except pgerror.BackendQueryCancelledError:\n            pass\n\n        except Exception as ex:\n            # We can only be here if an exception occurred during\n            # handling another exception, in which case, the only\n            # sane option is to abort the connection.\n\n            self.loop.call_exception_handler({\n                'message': (\n                    'unhandled error in edgedb protocol while '\n                    'handling an error'\n                ),\n                'exception': ex,\n                'protocol': self,\n                'transport': self._transport,\n                'task': self._main_task,\n            })\n\n        finally:\n            if self._stop_requested:\n                self._main_task_stopped_normally()\n                self.close()\n            else:\n                # Abort the connection.\n                # It might have already been cleaned up, but abort() is\n                # safe to be called on a closed connection.\n                self.abort()\n\n    # shutting down the connection\n\n    cdef stop_connection(self):\n        pass\n\n    def close(self):\n        self.abort_pinned_pgcon()\n        self.stop_connection()\n        if self._transport is not None:\n            self.flush()\n            self._transport.close()\n            self._transport = None\n\n    def abort(self):\n        self.abort_pinned_pgcon()\n        self.stop_connection()\n        if self._transport is not None:\n            self._transport.abort()\n            self._transport = None\n\n    def request_stop(self):\n        # Actively stop a frontend connection - this is used by the server\n        # when it's stopping.\n        self._stop_requested = True\n        if self._msg_take_waiter is not None:\n            if not self._msg_take_waiter.done():\n                self._msg_take_waiter.cancel()\n\n    @property\n    def cancelled(self) -> bool:\n        return self._cancelled\n\n    def is_alive(self):\n        return self._transport is not None and not self._cancelled\n\n    def connection_lost(self, exc):\n        # Let's talk about cancellation.\n        #\n        # 1. Since we need to synchronize the state between Postgres and\n        #    EdgeDB, we need to make sure we never do straight asyncio\n        #    cancellation while some operation in pgcon is in flight.\n        #\n        #    Doing that can lead to the following few bad scenarios:\n        #\n        #       * pgcon connction being wrecked by asyncio.CancelledError;\n        #\n        #       * pgcon completing its operation and then, a rogue\n        #         CancelledError preventing us to apply the new state\n        #         to dbview/server config/etc.\n        #\n        # 2. It is safe to cancel `_msg_take_waiter` though. Cancelling it\n        #    would abort protocol parsing, but there's no global state that\n        #    needs syncing in protocol messages.\n        #\n        # 3. We can interrupt some operations like auth with a CancelledError.\n        #    Again, those operations don't mutate global state.\n\n        if self.connection_made_at is not None:\n            tenant_label = self.get_tenant_label()\n            metrics.client_connection_duration.observe(\n                time.monotonic() - self.connection_made_at,\n                tenant_label,\n                self.interface,\n            )\n            if self.authed:\n                metrics.queries_per_connection.observe(\n                    self._query_count, tenant_label, self.interface\n                )\n            if isinstance(exc, ConnectionError):\n                metrics.connection_errors.inc(1.0, tenant_label)\n\n        if (self._msg_take_waiter is not None and\n            not self._msg_take_waiter.done()):\n            # We're parsing the protocol. We can abort that.\n            self._msg_take_waiter.cancel()\n\n        if (\n            self._main_task is not None\n            and not self._main_task.done()\n            and not self._cancelled\n        ):\n\n            # The main connection handling task is up and running.\n\n            # First, let's set a flag to signal that we should cancel soon;\n            # after all the client has already disconnected.\n            self._cancelled = True\n\n            # Make sure nothing is blocked on flow control.\n            # (Currently only dump uses this.)\n            self.resume_writing()\n\n            if not self.authed:\n                # We must be still authenticating. We can abort that.\n                self._main_task.cancel()\n            else:\n                if (\n                    self._pinned_pgcon is not None\n                    and not self._pinned_pgcon.idle\n                ):\n                    # Looks like we have a Postgres connection acquired and\n                    # it's actively running some command for us.  To make\n                    # sure we're not leaving behind a heavy query, perform\n                    # an explicit Postgres cancellation because a mere\n                    # connection drop wouldn't necessarily abort the query\n                    # right away). Additionally, we must discard the connection\n                    # as we cannot be completely sure about its state. Postgres\n                    # cancellation is signal-based and is addressed to a whole\n                    # connection and not a concrete operation. The result is\n                    # that we might be racing with the currently running query\n                    # and if that completes before the cancellation signal\n                    # reaches the backend, we'll be setting a trap for the\n                    # _next_ query that is unlucky enough to pick up this\n                    # Postgres backend from the connection pool.\n                    # TODO(fantix): hold server shutdown to complete this task\n                    if self.tenant.accept_new_tasks:\n                        self.tenant.create_task(\n                            self.tenant.cancel_and_discard_pgcon(\n                                self._pinned_pgcon, self.dbname\n                            ),\n                            interruptable=False,\n                        )\n                    # Prevent the main task from releasing the same connection\n                    # twice. This flag is for now only used in this case.\n                    self._pgcon_released_in_connection_lost = True\n\n                # In all other cases, we can just wait until the `main()`\n                # coroutine notices that `self._cancelled` was set.\n                # It would be a mistake to cancel the main task here, as it\n                # could be unpacking results from pgcon and applying them\n                # to the global state.\n                #\n                # Ultimately, the main() coroutine will be aborted, eventually,\n                # and will call `self.abort()` to shut all things down.\n        else:\n            # The `main()` coroutine isn't running, it means that the\n            # connection is already pretty much dead.  Nonetheless, call\n            # abort() to make sure we've cleaned everything up properly.\n            self.abort()\n\n    # Authentication\n\n    async def authenticate(self):\n        raise NotImplementedError\n\n    def _auth_jwt(self, user, database, params):\n        raise NotImplementedError\n\n    def _auth_trust(self, user):\n        roles = self.tenant.get_roles()\n        if user not in roles:\n            raise errors.AuthenticationError('authentication failed')\n\n    async def _authenticate(self, user, database, params):\n        # The user has already been authenticated by other means\n        # (such as the ability to write to a protected socket).\n        if self._external_auth:\n            authmethods = [\n                self.server.config_settings.get_type_by_name('cfg::Trust')()\n            ]\n        else:\n            authmethods = await self.tenant.get_auth_methods(\n                user, self._transport_proto)\n\n        auth_errors = {}\n\n        for authmethod in authmethods:\n            authmethod_name = authmethod._tspec.name.split('::')[1]\n\n            try:\n                if authmethod_name == 'SCRAM':\n                    await self._auth_scram(user)\n                elif authmethod_name == 'JWT':\n                    self._auth_jwt(user, database, params)\n                elif authmethod_name == 'Trust':\n                    self._auth_trust(user)\n                elif authmethod_name == 'Password':\n                    raise errors.AuthenticationError(\n                        'authentication failed: '\n                        'Simple password authentication required but it is '\n                        'only supported for HTTP endpoints'\n                    )\n                elif authmethod_name == 'mTLS':\n                    auth_helpers.auth_mtls_with_user(self._transport, user)\n                else:\n                    raise errors.InternalServerError(\n                        f'unimplemented auth method: {authmethod_name}')\n            except errors.AuthenticationError as e:\n                auth_errors[authmethod_name] = e\n            else:\n                break\n\n        if len(auth_errors) == len(authmethods):\n            if len(auth_errors) > 1:\n                desc = \"; \".join(\n                    f\"{k}: {e.args[0]}\" for k, e in auth_errors.items())\n                raise errors.AuthenticationError(\n                    f\"all authentication methods failed: {desc}\")\n            else:\n                raise next(iter(auth_errors.values()))\n\n        role = self.tenant.get_roles().get(user)\n        if not role:\n            raise errors.AuthenticationError('authentication failed')\n        branches = role['branches']\n        if (\n            '*' not in branches\n            and database not in branches\n            and database != defines.EDGEDB_SYSTEM_DB\n        ):\n            raise errors.AuthenticationError(\n                f\"authentication failed: user does not have permission for \"\n                f\"database branch '{database}'\"\n            )\n\n    cdef WriteBuffer _make_authentication_sasl_initial(self, list methods):\n        raise NotImplementedError\n\n    cdef _expect_sasl_initial_response(self):\n        raise NotImplementedError\n\n    cdef WriteBuffer _make_authentication_sasl_msg(\n        self, bytes data, bint final\n    ):\n        raise NotImplementedError\n\n    cdef bytes _expect_sasl_response(self):\n        raise NotImplementedError\n\n    async def _auth_scram(self, user):\n        cdef WriteBuffer msg_buf\n\n        # Tell the client that we require SASL SCRAM auth.\n        msg_buf = self._make_authentication_sasl_initial([b'SCRAM-SHA-256'])\n        self.write(msg_buf)\n        self.flush()\n\n        selected_mech = None\n        verifier = None\n        mock_auth = False\n        client_nonce = None\n        cb_flag = None\n        done = False\n\n        while not done:\n            if not self.buffer.take_message():\n                await self.wait_for_message(report_idling=True)\n            mtype = self.buffer.get_message_type()\n\n            if selected_mech is None:\n                # Initial response.\n                (\n                    selected_mech, client_first\n                ) = self._expect_sasl_initial_response()\n                if selected_mech != b'SCRAM-SHA-256':\n                    raise errors.BinaryProtocolError(\n                        f'client selected an invalid SASL authentication '\n                        f'mechanism')\n                verifier, mock_auth = auth_helpers.scram_get_verifier(\n                    self.tenant, user)\n\n                try:\n                    bare_offset, cb_flag, authzid, username, client_nonce = (\n                        scram.parse_client_first_message(client_first))\n                except ValueError as e:\n                    raise errors.BinaryProtocolError(str(e))\n\n                client_first_bare = client_first[bare_offset:]\n\n                if isinstance(cb_flag, str):\n                    raise errors.BinaryProtocolError(\n                        'malformed SCRAM message',\n                        details='The client selected SCRAM-SHA-256 without '\n                                'channel binding, but the SCRAM message '\n                                'includes channel binding data.')\n\n                if authzid:\n                    raise errors.UnsupportedFeatureError(\n                        'client uses SASL authorization identity, '\n                        'which is not supported')\n\n                server_nonce = scram.generate_nonce()\n                server_first = scram.build_server_first_message(\n                    server_nonce, client_nonce,\n                    verifier.salt, verifier.iterations).encode('utf-8')\n\n                msg_buf = self._make_authentication_sasl_msg(server_first, 0)\n                self.write(msg_buf)\n                self.flush()\n\n            else:\n                # client final message\n                client_final = self._expect_sasl_response()\n                try:\n                    cb_data, client_proof, proof_len = (\n                        scram.parse_client_final_message(\n                            client_final, client_nonce, server_nonce))\n                except ValueError as e:\n                    raise errors.BinaryProtocolError(str(e)) from None\n\n                client_final_without_proof = client_final[:-proof_len]\n\n                cb_data_ok = (\n                    (cb_flag is False and cb_data == b'biws')\n                    or (cb_flag is True and cb_data == b'eSws')\n                )\n                if not cb_data_ok:\n                    raise errors.BinaryProtocolError(\n                        'malformed SCRAM message',\n                        details='Unexpected SCRAM channel-binding attribute '\n                                'in client-final-message.')\n\n                if not scram.verify_client_proof(\n                    client_first_bare, server_first,\n                    client_final_without_proof,\n                    verifier.stored_key, client_proof):\n                    raise errors.AuthenticationError(\n                        'authentication failed')\n\n                if mock_auth:\n                    # This user actually does not exist, so fail here.\n                    raise errors.AuthenticationError(\n                        'authentication failed')\n\n                server_final = scram.build_server_final_message(\n                    client_first_bare,\n                    server_first,\n                    client_final_without_proof,\n                    verifier.server_key,\n                ).encode('utf-8')\n\n                # AuthenticationSASLFinal\n                msg_buf = self._make_authentication_sasl_msg(server_final, 1)\n                self.write(msg_buf)\n                self.flush()\n\n                done = True\n"
  },
  {
    "path": "edb/server/protocol/metrics.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import TYPE_CHECKING\nimport http\n\nfrom edb import errors\nfrom edb.server import metrics\nfrom edb.server import server\n\nfrom edb.common import debug\nfrom edb.common import markup\n\nif TYPE_CHECKING:\n    from edb.server import tenant as edbtenant\n    from edb.server.protocol import protocol\n\n\nasync def handle_request(\n    request: protocol.HttpRequest,\n    response: protocol.HttpResponse,\n    tenant: edbtenant.Tenant,\n) -> None:\n    try:\n        if tenant is None or isinstance(tenant.server, server.Server):\n            output = metrics.registry.generate()\n        else:\n            output = metrics.registry.generate(\n                tenant=tenant.get_instance_name()\n            )\n        response.status = http.HTTPStatus.OK\n        response.content_type = b'text/plain; version=0.0.4; charset=utf-8'\n        response.body = output.encode()\n        response.close_connection = True\n\n    except Exception as ex:\n        if debug.flags.server:\n            markup.dump(ex)\n\n        # XXX Fix this when LSP \"location\" objects are implemented\n        ex_type = errors.InternalServerError\n\n        _response_error(\n            response, http.HTTPStatus.INTERNAL_SERVER_ERROR, str(ex), ex_type\n        )\n\n\ndef _response_error(\n    response: protocol.HttpResponse,\n    status: http.HTTPStatus,\n    message: str,\n    ex_type: type[errors.EdgeDBError],\n) -> None:\n    response.body = (\n        f'Unexpected error in /metrics.\\n\\n'\n        f'{ex_type.__name__}: {message}'\n    ).encode()\n    response.status = status\n    response.close_connection = True\n"
  },
  {
    "path": "edb/server/protocol/notebook_ext.pxd",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom edb.server.protocol cimport frontend\nfrom edb.server.pgproto.pgproto cimport WriteBuffer\n\n\ncdef class NotebookConnection(frontend.AbstractFrontendConnection):\n    cdef:\n        WriteBuffer buf\n\n    cdef bytes _get_data(self)\n"
  },
  {
    "path": "edb/server/protocol/notebook_ext.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport base64\nimport http\nimport json\nimport urllib.parse\n\nimport immutables\n\nfrom edb import errors\nfrom edb.server.pgcon import errors as pgerrors\n\nfrom edb.common import debug\nfrom edb.common import markup\n\nfrom edb.server import compiler\nfrom edb.server import defines as edbdef\nfrom edb.server.compiler import OutputFormat\nfrom edb.server.compiler import dbstate\nfrom edb.server.compiler import enums\nfrom edb.server.protocol import execute as p_execute\nfrom edb.server.dbview cimport dbview\nfrom edb.server.protocol cimport frontend\n\nfrom edb.server.pgproto.pgproto cimport (\n    WriteBuffer,\n)\n\n\ninclude \"./consts.pxi\"\n\ncdef tuple CURRENT_PROTOCOL = edbdef.CURRENT_PROTOCOL\n\nALLOWED_CAPABILITIES = (\n    enums.Capability.MODIFICATIONS |\n    enums.Capability.DDL\n)\n\n\ncdef handle_error(\n    object request,\n    object response,\n    error\n):\n    if debug.flags.server:\n        markup.dump(error)\n\n    er_type = type(error)\n    if not issubclass(er_type, errors.EdgeDBError):\n        er_type = errors.InternalServerError\n\n    response.body = json.dumps({\n        'kind': 'error',\n        'error': {\n            'message': str(error),\n            'type': er_type.__name__,\n        }\n    }).encode()\n    response.status = http.HTTPStatus.BAD_REQUEST\n    response.close_connection = True\n\n\nasync def handle_request(\n    object request,\n    object response,\n    object db,\n    str role_name,\n    list args,\n    object tenant,\n):\n    response.content_type = b'application/json'\n\n    if args == ['status'] and request.method == b'GET':\n        try:\n            await heartbeat_check(db, tenant)\n        except Exception as ex:\n            return handle_error(request, response, ex)\n        else:\n            response.status = http.HTTPStatus.OK\n            response.body = b'{\"kind\": \"status\", \"status\": \"OK\"}'\n            return\n\n    if args != []:\n        ex = Exception(f'Unknown path')\n        return handle_error(request, response, ex)\n\n    queries = None\n\n    try:\n        if request.method == b'POST':\n            body = json.loads(request.body)\n            if not isinstance(body, dict):\n                raise TypeError(\n                    'the body of the request must be a JSON object')\n            queries = body.get('queries')\n\n        else:\n            raise TypeError('expected a POST request')\n\n        if not queries:\n            raise TypeError(\n                'invalid notebook request: \"queries\" is missing')\n\n    except Exception as ex:\n        return handle_error(request, response, ex)\n\n    response.status = http.HTTPStatus.OK\n    try:\n        result = await execute(db, role_name, tenant, queries)\n    except Exception as ex:\n        return handle_error(request, response, ex)\n    else:\n        response.custom_headers['EdgeDB-Protocol-Version'] = \\\n            f'{CURRENT_PROTOCOL[0]}.{CURRENT_PROTOCOL[1]}'\n        response.custom_headers['Gel-Protocol-Version'] = \\\n            f'{CURRENT_PROTOCOL[0]}.{CURRENT_PROTOCOL[1]}'\n        response.body = b'{\"kind\": \"results\", \"results\":' + result + b'}'\n\n\nasync def heartbeat_check(db, tenant):\n    async with tenant.with_pgcon(db.name) as pgcon:\n        await pgcon.sql_execute(b\"SELECT 'OK';\")\n\n\ncdef class NotebookConnection(frontend.AbstractFrontendConnection):\n    def __cinit__(self):\n        self.buf = WriteBuffer.new()\n\n    cdef write(self, WriteBuffer data):\n        self.buf.write_bytes(bytes(data))\n\n    cdef bytes _get_data(self):\n        return bytes(self.buf)\n\n    cdef flush(self):\n        pass\n\n\nasync def execute(db, role_name, tenant, queries: list):\n    dbv: dbview.DatabaseConnectionView = await tenant.new_dbview(\n        dbname=db.name,\n        query_cache=False,\n        protocol_version=edbdef.CURRENT_PROTOCOL,\n        role_name=role_name,\n    )\n    compiler_pool = tenant.server.get_compiler_pool()\n    units = await compiler_pool.compile_notebook(\n        dbv.dbname,\n        dbv.get_user_schema_pickle(),\n        dbv.get_global_schema_pickle(),\n        dbv.reflection_cache,\n        dbv.get_database_config(),\n        dbv.get_compilation_system_config(),\n        queries,\n        CURRENT_PROTOCOL,\n        50,  # implicit limit\n        client_id=tenant.client_id,\n        client_name=tenant.get_instance_name(),\n    )\n    result = []\n    bind_data = None\n    async with tenant.with_pgcon(db.name) as pgcon:\n        try:\n            await pgcon.sql_execute(b'START TRANSACTION;')\n            dbv.start_tx()\n\n            for is_error, unit_or_error in units:\n                if is_error:\n                    result.append({\n                        'kind': 'error',\n                        'error': unit_or_error,\n                    })\n                else:\n                    query_unit = unit_or_error\n                    query_unit_group = dbstate.QueryUnitGroup()\n                    query_unit_group.append(query_unit)\n\n                    dbv.check_capabilities(\n                        query_unit,\n                        ALLOWED_CAPABILITIES,\n                        errors.UnsupportedCapabilityError,\n                        \"disallowed in notebook\",\n                        query_unit_group.unsafe_isolation_dangers,\n                    )\n                    try:\n                        if query_unit.in_type_args:\n                            raise errors.QueryError(\n                                'cannot use query parameters in tutorial')\n\n                        fe_conn = NotebookConnection()\n\n                        compiled = dbview.CompiledQuery(\n                            query_unit_group=query_unit_group)\n                        await p_execute.execute(\n                            pgcon, dbv, compiled, b'', fe_conn=fe_conn,\n                        )\n\n                    except Exception as ex:\n                        if debug.flags.server:\n                            markup.dump(ex)\n\n                        ex = await p_execute.interpret_error(\n                            ex,\n                            dbv._db,\n                            global_schema_pickle=dbv.get_global_schema_pickle(),\n                            user_schema_pickle=dbv.get_user_schema_pickle(),\n                        )\n\n                        result.append({\n                            'kind': 'error',\n                            'error': [type(ex).__name__, str(ex), {}],\n                        })\n\n                        break\n                    else:\n                        result.append({\n                            'kind': 'data',\n                            'data': (\n                                base64.b64encode(\n                                    query_unit.out_type_id).decode(),\n                                base64.b64encode(\n                                    query_unit.out_type_data).decode(),\n                                base64.b64encode(\n                                    fe_conn._get_data()).decode(),\n                                base64.b64encode(\n                                    query_unit.status).decode(),\n                            ),\n                        })\n\n        finally:\n            try:\n                await pgcon.sql_execute(b'ROLLBACK;')\n            finally:\n                tenant.remove_dbview(dbv)\n\n    return json.dumps(result).encode()\n"
  },
  {
    "path": "edb/server/protocol/pg_ext.pxd",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom edb.server.pgproto.pgproto cimport WriteBuffer\nfrom edb.server.protocol cimport frontend\nfrom edb.server.pgcon.pgcon cimport PGMessage\ncimport edb.pgsql.parser.parser as pg_parser\n\n\ncdef class PreparedStmt:\n    cdef:\n        PGMessage parse_action\n        pg_parser.Source source\n\n\ncdef class ConnectionView:\n\n    cdef:\n        object _local_fe_defaults\n        object _settings\n        object _fe_settings\n\n        bint _in_tx_explicit\n        bint _in_tx_implicit\n        object _in_tx_settings\n        object _in_tx_fe_settings\n        object _in_tx_fe_local_settings\n        dict _in_tx_portals\n        object _in_tx_new_portals\n        object _in_tx_savepoints\n        bint _tx_error\n\n        tuple _session_state_db_cache\n\n    cdef _init_user_configs(self, username, tenant)\n    cpdef inline current_fe_settings(self)\n    cdef inline fe_transaction_state(self)\n    cpdef inline bint in_tx(self)\n    cdef inline _reset_tx_state(\n        self, bint chain_implicit, bint chain_explicit\n    )\n    cdef bint needs_commit_after_state_sync(self)\n    cpdef inline close_portal_if_exists(self, str name)\n    cpdef inline close_portal(self, str name)\n    cdef inline find_portal(self, str name)\n    cdef inline portal_exists(self, str name)\n\n\ncdef class PgConnection(frontend.FrontendConnection):\n\n    cdef:\n        ConnectionView _dbview\n\n        bytes secret\n        dict prepared_stmts\n        dict sql_prepared_stmts\n        dict sql_prepared_stmts_map\n        dict wrapping_prepared_stmts\n        bint ignore_till_sync\n\n        object sslctx\n        object endpoint_security\n        bint is_tls\n        bint _disable_cache\n        bint _disable_normalization\n\n    cdef inline WriteBuffer ready_for_query(self)\n"
  },
  {
    "path": "edb/server/protocol/pg_ext.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport codecs\nimport collections\nimport contextlib\nimport copy\nimport encodings.aliases\nimport logging\nimport hashlib\nimport json\nimport os\nimport sys\nimport time\nimport uuid\nfrom collections import deque\nfrom typing import Sequence\n\ncimport cython\nimport immutables\nfrom libc.stdint cimport int32_t, int16_t, uint32_t\n\nfrom edb import errors\nfrom edb.common import debug\nfrom edb.common.log import current_tenant\nfrom edb.pgsql.common import setting_to_sql\nfrom edb.pgsql.parser import exceptions as parser_errors\nimport edb.pgsql.parser.parser as pg_parser\ncimport edb.pgsql.parser.parser as pg_parser\nfrom edb.server import args as srvargs\nfrom edb.server import defines, metrics\nfrom edb.server import tenant as edbtenant\nfrom edb.server.compiler import dbstate, enums\nfrom edb.server.pgcon import errors as pgerror\nfrom edb.server.pgcon.pgcon cimport PGAction, PGMessage\nfrom edb.server.protocol cimport frontend\n\nDEFAULT_SETTINGS = dbstate.DEFAULT_SQL_SETTINGS\nDEFAULT_FE_SETTINGS = dbstate.DEFAULT_SQL_FE_SETTINGS\n\ncdef object logger = logging.getLogger('edb.server')\ncdef object DEFAULT_STATE = None\n\nencodings.aliases.aliases[\"sql_ascii\"] = \"ascii\"\n\n\nclass ExtendedQueryError(Exception):\n    pass\n\n\n@contextlib.contextmanager\ndef managed_error():\n    try:\n        yield\n    except Exception as e:\n        raise ExtendedQueryError(e)\n\n\n@cython.final\ncdef class PreparedStmt:\n\n    def __init__(self, PGMessage parse_action, pg_parser.Source source):\n        self.parse_action = parse_action\n        self.source = source\n\n\n\n@cython.final\ncdef class ConnectionView:\n    def __init__(self):\n        self._in_tx_explicit = False\n        self._in_tx_implicit = False\n\n        # Kepp track of backend settings so that we can sync to use different\n        # backend connections (pgcon) within the same frontend connection,\n        # see serialize_state() below and its usages in pgcon.pyx.\n        self._settings = DEFAULT_SETTINGS\n        self._in_tx_settings = None\n\n        # Frontend-only settings are defined by the high-level compiler, and\n        # tracked only here, syncing between the compiler process,\n        # see current_fe_settings(), fe_transaction_state() and usages below.\n        self._local_fe_defaults = DEFAULT_FE_SETTINGS\n        self._fe_settings = self._local_fe_defaults\n        self._in_tx_fe_settings = None\n        self._in_tx_fe_local_settings = None\n\n        self._in_tx_portals = {}\n        self._in_tx_new_portals = set()\n        self._in_tx_savepoints = collections.deque()\n        self._tx_error = False\n        global DEFAULT_STATE\n        if DEFAULT_STATE is None:\n            DEFAULT_STATE = json.dumps(\n                [\n                    {\n                        \"type\": \"P\",\n                        \"name\": key,\n                        \"value\": setting_to_sql(key, val),\n                    }\n                    for key, val in DEFAULT_SETTINGS.items()\n                ] + [\n                    {\n                        \"type\": \"S\",\n                        \"name\": key,\n                        \"value\": setting_to_sql(key, val),\n                    }\n                    for key, val in DEFAULT_FE_SETTINGS.items()\n                ]\n            ).encode(\"utf-8\")\n\n        self._session_state_db_cache = (\n            DEFAULT_SETTINGS, DEFAULT_FE_SETTINGS, DEFAULT_STATE\n        )\n\n    cdef _init_user_configs(self, username, tenant):\n        assert self._fe_settings is DEFAULT_FE_SETTINGS\n        assert self._in_tx_fe_local_settings is None\n        role = tenant.get_roles()[username]\n        apply_default = role['apply_access_policies_pg_default']\n        if apply_default is not None:\n            self._local_fe_defaults = self._local_fe_defaults.set(\n                'apply_access_policies_pg',\n                (str(apply_default).lower(),),\n            )\n            self._fe_settings = self._local_fe_defaults\n\n    cpdef inline current_fe_settings(self):\n        if self.in_tx():\n            # For easier access, _in_tx_fe_local_settings is always a superset\n            # of _in_tx_fe_settings; _in_tx_fe_settings only keeps track of\n            # non-local settings, so that the local settings don't go across\n            # transaction boundaries; this must be consistent with dbstate.py.\n            return self._in_tx_fe_local_settings or self._local_fe_defaults\n        else:\n            return self._fe_settings\n\n    cdef inline fe_transaction_state(self):\n        return dbstate.SQLTransactionState(\n            in_tx=self.in_tx(),\n            settings=self._fe_settings,\n            in_tx_settings=self._in_tx_fe_settings,\n            in_tx_local_settings=self._in_tx_fe_local_settings,\n            savepoints=[sp[:3] for sp in self._in_tx_savepoints],\n        )\n\n    cpdef inline bint in_tx(self):\n        return self._in_tx_explicit or self._in_tx_implicit\n\n    cdef inline _reset_tx_state(\n        self, bint chain_implicit, bint chain_explicit\n    ):\n        # This method is a part of ending a transaction. COMMIT must be handled\n        # before calling this method. If any of the chain_* flag is set, a new\n        # transaction will be opened immediately after clean-up.\n        self._in_tx_implicit = chain_implicit\n        self._in_tx_explicit = chain_explicit\n        self._in_tx_settings = self._settings if self.in_tx() else None\n        self._in_tx_fe_settings = self._fe_settings if self.in_tx() else None\n        self._in_tx_fe_local_settings = self._in_tx_fe_settings\n        self._in_tx_portals.clear()\n        self._in_tx_new_portals.clear()\n        self._in_tx_savepoints.clear()\n        self._tx_error = False\n\n    def start_implicit(self):\n        if self._in_tx_implicit:\n            raise RuntimeError(\"already in implicit transaction\")\n        else:\n            if not self.in_tx():\n                self._in_tx_settings = self._settings\n                self._in_tx_fe_settings = self._fe_settings\n                self._in_tx_fe_local_settings = self._fe_settings\n            self._in_tx_implicit = True\n\n    def end_implicit(self):\n        if not self._in_tx_implicit:\n            raise RuntimeError(\"not in implicit transaction\")\n        if self._in_tx_explicit:\n            # There is an explicit transaction, nothing to do other than\n            # turning off the implicit flag so that we can start_implicit again\n            self._in_tx_implicit = False\n        else:\n            # Commit or rollback the implicit transaction\n            if not self._tx_error:\n                self._settings = self._in_tx_settings\n                self._fe_settings = self._in_tx_fe_settings\n            self._reset_tx_state(False, False)\n\n    def on_success(self, unit: dbstate.SQLQueryUnit):\n        # Handle ROLLBACK first before self._tx_error\n        if unit.tx_action == dbstate.TxAction.ROLLBACK:\n            if not self._in_tx_explicit:\n                # TODO: warn about \"no tx\" but still rollback implicit\n                pass\n            self._reset_tx_state(self._in_tx_implicit, unit.tx_chain)\n\n        elif unit.tx_action == dbstate.TxAction.ROLLBACK_TO_SAVEPOINT:\n            if not self._in_tx_explicit:\n                if self._in_tx_implicit:\n                    self._tx_error = True\n                raise errors.TransactionError(\n                    \"ROLLBACK TO SAVEPOINT can only be used \"\n                    \"in transaction blocks\"\n                )\n            while self._in_tx_savepoints:\n                (\n                    sp_name,\n                    fe_settings,\n                    fe_local_settings,\n                    settings,\n                    new_portals,\n                ) = self._in_tx_savepoints[-1]\n                for name in new_portals:\n                    self._in_tx_portals.pop(name, None)\n                if sp_name == unit.sp_name:\n                    new_portals.clear()\n                    self._in_tx_settings = settings\n                    self._in_tx_fe_settings = fe_settings\n                    self._in_tx_fe_local_settings = fe_local_settings\n                    self._in_tx_new_portals = new_portals\n                    break\n                else:\n                    self._in_tx_savepoints.pop()\n            else:\n                self._tx_error = True\n                raise errors.TransactionError(\n                    f'savepoint \"{unit.sp_name}\" does not exist'\n                )\n\n        elif self._tx_error:\n            raise errors.TransactionError(\n                \"current transaction is aborted, \"\n                \"commands ignored until end of transaction block\"\n            )\n\n        elif unit.tx_action == dbstate.TxAction.START:\n            if self._in_tx_explicit:\n                # TODO: warning: there is already a transaction in progress\n                pass\n            else:\n                if not self.in_tx():\n                    self._in_tx_settings = self._settings\n                    self._in_tx_fe_settings = self._fe_settings\n                    self._in_tx_fe_local_settings = self._fe_settings\n                self._in_tx_explicit = True\n\n        elif unit.tx_action == dbstate.TxAction.COMMIT:\n            if not self._in_tx_explicit:\n                # TODO: warning: there is no transaction in progress\n                # but we still commit implicit transactions if any\n                pass\n            if self.in_tx():\n                self._settings = self._in_tx_settings\n                self._fe_settings = self._in_tx_fe_settings\n            self._reset_tx_state(self._in_tx_implicit, unit.tx_chain)\n\n        elif unit.tx_action == dbstate.TxAction.DECLARE_SAVEPOINT:\n            if not self._in_tx_explicit:\n                raise errors.TransactionError(\n                    \"SAVEPOINT can only be used in transaction blocks\"\n                )\n            self._in_tx_new_portals = set()\n            self._in_tx_savepoints.append((\n                unit.sp_name,\n                self._in_tx_fe_settings,\n                self._in_tx_fe_local_settings,\n                self._in_tx_settings,\n                self._in_tx_new_portals,\n            ))\n\n        elif unit.tx_action == dbstate.TxAction.RELEASE_SAVEPOINT:\n            pass\n\n        if unit.set_vars:\n            # only session settings here\n            if unit.set_vars == {None: None}:  # RESET ALL\n                if self.in_tx():\n                    self._in_tx_settings = DEFAULT_SETTINGS\n                    self._in_tx_fe_settings = self._local_fe_defaults\n                    self._in_tx_fe_local_settings = self._local_fe_defaults\n                else:\n                    self._settings = DEFAULT_SETTINGS\n                    self._fe_settings = self._local_fe_defaults\n            else:\n                if self.in_tx():\n                    if unit.frontend_only:\n                        if not unit.is_local:\n                            settings = self._in_tx_fe_settings.mutate()\n                            for k, v in unit.set_vars.items():\n                                if v is None:\n                                    if k in self._local_fe_defaults:\n                                        settings[k] = self._local_fe_defaults[k]\n                                    else:\n                                        settings.pop(k, None)\n                                else:\n                                    settings[k] = v\n                            self._in_tx_fe_settings = settings.finish()\n                        settings = self._in_tx_fe_local_settings.mutate()\n                    else:\n                        settings = self._in_tx_settings.mutate()\n                elif not unit.is_local:\n                    if unit.frontend_only:\n                        settings = self._fe_settings.mutate()\n                    else:\n                        settings = self._settings.mutate()\n                else:\n                    return\n                for k, v in unit.set_vars.items():\n                    if v is None:\n                        if unit.frontend_only and k in self._local_fe_defaults:\n                            settings[k] = self._local_fe_defaults[k]\n                        else:\n                            settings.pop(k, None)\n                    else:\n                        settings[k] = v\n                if self.in_tx():\n                    if unit.frontend_only:\n                        self._in_tx_fe_local_settings = settings.finish()\n                    else:\n                        self._in_tx_settings = settings.finish()\n                else:\n                    if unit.frontend_only:\n                        self._fe_settings = settings.finish()\n                    else:\n                        self._settings = settings.finish()\n\n    def on_error(self):\n        self._tx_error = True\n\n    cpdef inline close_portal(self, str name):\n        try:\n            return self._in_tx_portals.pop(name)\n        except KeyError:\n            raise pgerror.new(\n                pgerror.ERROR_INVALID_CURSOR_NAME,\n                f\"cursor \\\"{name}\\\" does not exist\",\n            ) from None\n\n    cpdef inline close_portal_if_exists(self, str name):\n        return self._in_tx_portals.pop(name, None)\n\n    def create_portal(self, str name, query_unit):\n        if not self.in_tx():\n            raise RuntimeError(\n                \"portals cannot be created outside a transaction\"\n            )\n        if name and name in self._in_tx_portals:\n            raise pgerror.new(\n                pgerror.ERROR_DUPLICATE_CURSOR,\n                f\"cursor \\\"{name}\\\" already exists\",\n            )\n        self._in_tx_portals[name] = query_unit\n\n    cdef inline find_portal(self, str name):\n        try:\n            return self._in_tx_portals[name]\n        except KeyError:\n            raise pgerror.new(\n                pgerror.ERROR_INVALID_CURSOR_NAME,\n                f\"cursor \\\"{name}\\\" does not exist\",\n            ) from None\n\n    cdef inline portal_exists(self, str name):\n        return name in self._in_tx_portals\n\n    def serialize_state(self):\n        if self.in_tx():\n            raise errors.InternalServerError(\n                'no need to serialize state while in transaction')\n        if (\n            self._settings == DEFAULT_SETTINGS\n            and self._fe_settings == DEFAULT_FE_SETTINGS\n        ):\n            return DEFAULT_STATE\n\n        if self._session_state_db_cache is not None:\n            if self._session_state_db_cache[:2] == (\n                self._settings, self._fe_settings\n            ):\n                return self._session_state_db_cache[-1]\n\n        rv = json.dumps(\n            [\n                {\"type\": \"P\", \"name\": key, \"value\": setting_to_sql(key, val)}\n                for key, val in self._settings.items()\n            ] + [\n                {\"type\": \"S\", \"name\": key, \"value\": setting_to_sql(key, val)}\n                for key, val in self._fe_settings.items()\n            ]\n        ).encode(\"utf-8\")\n        self._session_state_db_cache = (self._settings, self._fe_settings, rv)\n        return rv\n\n    cdef bint needs_commit_after_state_sync(self):\n        return any(\n            tx_conf in self._settings\n            for tx_conf in [\n                \"default_transaction_isolation\",\n                \"default_transaction_deferrable\",\n                \"default_transaction_read_only\",\n            ]\n        )\n\n\ncdef class PgConnection(frontend.FrontendConnection):\n    interface = \"sql\"\n\n    def __init__(self, server, sslctx, endpoint_security, **kwargs):\n        super().__init__(server, None, **kwargs)\n        self._dbview = ConnectionView()\n        self._id = str(<int32_t><uint32_t>(int(self._id) % (2 ** 32)))\n        self.prepared_stmts = {}  # via extended query Parse\n        self.sql_prepared_stmts = {}  # via a PREPARE statement\n        self.sql_prepared_stmts_map = {}\n        # Tracks prepared statements of operations\n        # on *other* prepared statements.\n        self.wrapping_prepared_stmts = {}\n        self.ignore_till_sync = False\n\n        self.sslctx = sslctx\n        self.endpoint_security = endpoint_security\n        self.is_tls = False\n\n        self._disable_cache = debug.flags.disable_qcache\n        self._disable_normalization = debug.flags.edgeql_disable_normalization\n\n    cdef _main_task_created(self):\n        self.server.on_pgext_client_connected(self)\n        # complete the client initial message with a mocked type\n        self.buffer.feed_data(b'\\xff')\n\n    def connection_lost(self, exc):\n        self.server.on_pgext_client_disconnected(self)\n        super().connection_lost(exc)\n\n    cdef is_in_tx(self):\n        return self._dbview.in_tx()\n\n    cdef write_error(self, exc):\n        cdef WriteBuffer buf\n\n        if self.debug and not isinstance(exc, errors.BackendUnavailableError):\n            self.debug_print('EXCEPTION', type(exc).__name__, exc)\n            from edb.common.markup import dump\n            dump(exc)\n\n        if debug.flags.server and not isinstance(\n            exc, errors.BackendUnavailableError\n        ):\n            self.loop.call_exception_handler({\n                'message': (\n                    'an error in edgedb protocol'\n                ),\n                'exception': exc,\n                'protocol': self,\n                'transport': self._transport,\n            })\n\n        message = str(exc)\n\n        buf = WriteBuffer.new_message(b'E')\n\n        if isinstance(exc, pgerror.BackendError):\n            if exc.code_is(pgerror.ERROR_SERIALIZATION_FAILURE):\n                metrics.transaction_serialization_errors.inc(\n                    1.0, self.get_tenant_label()\n                )\n        elif isinstance(exc, parser_errors.PSqlUnsupportedError):\n            exc = pgerror.FeatureNotSupported(str(exc))\n        elif isinstance(exc, parser_errors.PSqlSyntaxError):\n            exc = pgerror.new(\n                pgerror.ERROR_SYNTAX_ERROR,\n                str(exc),\n                P=str(exc.cursor_pos),\n            )\n        elif isinstance(exc, errors.AuthenticationError):\n            exc = pgerror.InvalidAuthSpec(str(exc), severity=\"FATAL\")\n        elif isinstance(exc, errors.BinaryProtocolError):\n            exc = pgerror.ProtocolViolation(\n                str(exc), detail=exc.details, severity=\"FATAL\"\n            )\n        elif isinstance(exc, errors.UnsupportedFeatureError):\n            args = {}\n            if exc.line >= 0:\n                args['L'] = str(exc.line)\n            if exc.position >= 0:\n                args['P'] = str(exc.position + 1)\n            exc = pgerror.new(\n                pgerror.ERROR_FEATURE_NOT_SUPPORTED,\n                str(exc),\n                **args,\n            )\n        elif isinstance(exc, errors.EdgeDBError):\n            args = dict(hint=exc.hint, detail=exc.details)\n            if exc.line >= 0:\n                args['L'] = str(exc.line)\n            if exc.position >= 0:\n                # pg uses 1 based indexes for showing errors.\n                args['P'] = str(exc.position + 1)\n            exc = pgerror.new(\n                exc.pgext_code or pgerror.ERROR_INTERNAL_ERROR,\n                str(exc),\n                **args,\n            )\n            if isinstance(exc, errors.TransactionSerializationError):\n                metrics.transaction_serialization_errors.inc(\n                    1.0, self.get_tenant_label()\n                )\n        else:\n            exc = pgerror.new(\n                pgerror.ERROR_INTERNAL_ERROR,\n                str(exc),\n                severity=\"FATAL\",\n            )\n\n        for k, v in exc.fields.items():\n            buf.write_byte(ord(k))\n            buf.write_str(v, \"utf-8\")\n        buf.write_byte(b'\\0')\n\n        self.write(buf.end_message())\n\n    async def _handshake(self):\n        cdef int16_t proto_ver_major, proto_ver_minor\n\n        for first in (True, False):\n            if not self.buffer.take_message():\n                await self.wait_for_message(report_idling=True)\n\n            proto_ver_major = self.buffer.read_int16()\n            proto_ver_minor = self.buffer.read_int16()\n            if proto_ver_major == 1234:\n                if proto_ver_minor == 5678:  # CancelRequest\n                    pid = str(self.buffer.read_int32())\n                    secret = self.buffer.read_bytes(4)\n                    self.buffer.finish_message()\n\n                    if self.debug:\n                        self.debug_print(\"CancelRequest\", pid, secret)\n                    self.server.cancel_pgext_connection(pid, secret)\n                    self.request_stop()\n                    break\n\n                elif proto_ver_minor == 5679:  # SSLRequest\n                    if self.debug:\n                        self.debug_print(\"SSLRequest\")\n                    if not first:\n                        raise pgerror.ProtocolViolation(\n                            \"found multiple SSLRequest\", severity=\"FATAL\"\n                        )\n\n                    self.buffer.finish_message()\n                    if self._transport is None:\n                        raise ConnectionAbortedError\n                    if self.debug:\n                        self.debug_print(\"S for SSLRequest\")\n                    self._transport.write(b'S')\n                    # complete the next client message with a mocked type\n                    self.buffer.feed_data(b'\\xff')\n                    self._transport = await self.loop.start_tls(\n                        self._transport,\n                        self,\n                        self.sslctx,\n                        server_side=True,\n                    )\n                    tenant = self.server.retrieve_tenant(\n                        self._transport.get_extra_info(\"ssl_object\")\n                    )\n                    if tenant is edbtenant.host_tenant:\n                        tenant = None\n                    self.tenant = tenant\n                    if self.tenant is not None:\n                        current_tenant.set(self.tenant.get_instance_name())\n                    self.is_tls = True\n\n                elif proto_ver_minor == 5680:  # GSSENCRequest\n                    raise pgerror.FeatureNotSupported(\n                        \"GSSENCRequest is not supported\", severity=\"FATAL\"\n                    )\n\n                else:\n                    raise pgerror.FeatureNotSupported(severity=\"FATAL\")\n\n            elif proto_ver_major == 3 and proto_ver_minor == 0:\n                # StartupMessage with 3.0 protocol\n                if self.debug:\n                    self.debug_print(\"StartupMessage\")\n                if (\n                    not self.is_tls and self.endpoint_security ==\n                    srvargs.ServerEndpointSecurityMode.Tls\n                ):\n                    raise pgerror.InvalidAuthSpec(\n                        \"TLS required due to server endpoint security\",\n                        severity=\"FATAL\",\n                    )\n\n                await super()._handshake()\n                break\n\n            else:\n                raise pgerror.ProtocolViolation(\n                    \"invalid protocol version\", severity=\"FATAL\"\n                )\n\n    def cancel(self, secret):\n        if (\n            self.secret == secret and\n            self._pinned_pgcon is not None and\n            not self._pinned_pgcon.idle and\n            self.tenant.accept_new_tasks\n        ):\n            self.tenant.create_task(\n                self.tenant.cancel_pgcon_operation(self._pinned_pgcon),\n                interruptable=False,\n            )\n\n    def debug_print(self, *args):\n        print(\"::PGEXT::\", f\"id:{self._id}\", *args, file=sys.stderr)\n\n    cdef WriteBuffer _make_authentication_sasl_initial(self, list methods):\n        cdef WriteBuffer msg_buf\n        msg_buf = WriteBuffer.new_message(b'R')\n        msg_buf.write_int32(10)\n        for method in methods:\n            msg_buf.write_bytestring(method)\n        msg_buf.write_byte(b'\\0')\n        msg_buf.end_message()\n        if self.debug:\n            self.debug_print(\"AuthenticationSASL:\", *methods)\n        return msg_buf\n\n    cdef _expect_sasl_initial_response(self):\n        mtype = self.buffer.get_message_type()\n        if mtype != b'p':\n            raise pgerror.ProtocolViolation(\n                f'expected SASL response, got message type {mtype}')\n        selected_mech = self.buffer.read_null_str()\n        try:\n            client_first = self.buffer.read_len_prefixed_bytes()\n        except BufferError:\n            client_first = None\n        self.buffer.finish_message()\n        if self.debug:\n            self.debug_print(\n                \"SASLInitialResponse:\",\n                selected_mech,\n                len(client_first) if client_first else client_first,\n            )\n        if not client_first:\n            # The client didn't send the Client Initial Response\n            # in SASLInitialResponse, this is an error.\n            raise pgerror.ProtocolViolation(\n                'client did not send the Client Initial Response '\n                'data in SASLInitialResponse')\n        return selected_mech, client_first\n\n    cdef WriteBuffer _make_authentication_sasl_msg(\n        self, bytes data, bint final\n    ):\n        cdef WriteBuffer msg_buf\n        msg_buf = WriteBuffer.new_message(b'R')\n        if final:\n            msg_buf.write_int32(12)\n        else:\n            msg_buf.write_int32(11)\n        msg_buf.write_bytes(data)\n        msg_buf.end_message()\n        if self.debug:\n            self.debug_print(\n                \"AuthenticationSASLFinal\" if final\n                else \"AuthenticationSASLContinue\",\n                len(data),\n            )\n        return msg_buf\n\n    cdef bytes _expect_sasl_response(self):\n        mtype = self.buffer.get_message_type()\n        if mtype != b'p':\n            raise pgerror.ProtocolViolation(\n                f'expected SASL response, got message type {mtype}')\n        client_final = self.buffer.consume_message()\n        if self.debug:\n            self.debug_print(\"SASLResponse\", len(client_final))\n        return client_final\n\n    def check_readiness(self):\n        if self.tenant.is_blocked():\n            readiness_reason = self.tenant.get_readiness_reason()\n            msg = \"the server is not accepting requests\"\n            if readiness_reason:\n                msg = f\"{msg}: {readiness_reason}\"\n            raise pgerror.CannotConnectNowError(msg)\n        elif not self.tenant.is_online():\n            readiness_reason = self.tenant.get_readiness_reason()\n            msg = \"the server is going offline\"\n            if readiness_reason:\n                msg = f\"{msg}: {readiness_reason}\"\n            raise pgerror.CannotConnectNowError(msg)\n\n    async def authenticate(self):\n        cdef:\n            WriteBuffer msg_buf\n            WriteBuffer buf\n\n        self.check_readiness()\n\n        params = {}\n        while True:\n            name = self.buffer.read_null_str()\n            if not name:\n                break\n            value = self.buffer.read_null_str()\n            params[name.decode(\"utf-8\")] = value.decode(\"utf-8\")\n        if self.debug:\n            self.debug_print(\"StartupMessage params:\", params)\n        if \"user\" not in params:\n            raise pgerror.ProtocolViolation(\n                \"StartupMessage must have a \\\"user\\\"\", severity=\"FATAL\"\n            )\n        self.buffer.finish_message()\n\n        user = params[\"user\"]\n        database = params.get(\"database\", user)\n        if \"client_encoding\" in params:\n            encoding = params[\"client_encoding\"]\n            client_encoding = encodings.normalize_encoding(encoding).upper()\n            try:\n                codecs.lookup(client_encoding)\n            except LookupError:\n                raise pgerror.new(\n                    pgerror.ERROR_INVALID_PARAMETER_VALUE,\n                    f'invalid value for parameter \"client_encoding\": \"{encoding}\"',\n                )\n            self._dbview._settings = self._dbview._settings.set(\n                \"client_encoding\", (client_encoding,)\n            )\n        else:\n            client_encoding = \"UTF8\"\n\n        logger.debug('received pg connection request by %s to database %s',\n                     user, database)\n\n        if database == '__default__':\n            database = self.tenant.default_database\n        elif (\n            database == defines.EDGEDB_OLD_DEFAULT_DB\n            and self.tenant.maybe_get_db(\n                dbname=defines.EDGEDB_OLD_DEFAULT_DB\n            ) is None\n        ):\n            database = self.tenant.default_database\n\n        user = self.tenant.resolve_user_name(user)\n\n        await self._authenticate(user, database, params)\n\n        logger.debug('successfully authenticated %s in database %s',\n                     user, database)\n\n        if not self.tenant.is_database_connectable(database):\n            raise pgerror.InvalidAuthSpec(\n                f'database {database!r} does not accept connections',\n                severity=\"FATAL\",\n            )\n\n        self.database = self.tenant.get_db(dbname=database)\n        await self.database.introspection()\n\n        self.dbname = database\n        self.username = user\n\n        self._dbview._init_user_configs(user, self.tenant)\n\n        buf = WriteBuffer()\n\n        msg_buf = WriteBuffer.new_message(b'R')\n        msg_buf.write_int32(0)\n        msg_buf.end_message()\n        buf.write_buffer(msg_buf)\n        if self.debug:\n            self.debug_print(\"AuthenticationOk\")\n\n        self.secret = os.urandom(4)\n        msg_buf = WriteBuffer.new_message(b'K')\n        msg_buf.write_int32(int(self._id))\n        msg_buf.write_bytes(self.secret)\n        msg_buf.end_message()\n        buf.write_buffer(msg_buf)\n        if self.debug:\n            self.debug_print(\"BackendKeyData\")\n\n        async with self.with_pgcon() as conn:\n            for name, value in conn.parameter_status.items():\n                msg_buf = WriteBuffer.new_message(b'S')\n                msg_buf.write_str(name, \"utf-8\")\n                if name == \"client_encoding\":\n                    value = client_encoding\n                elif name == \"server_version\":\n                    value = str(defines.PGEXT_POSTGRES_VERSION)\n                elif name == \"session_authorization\":\n                    value = user\n                elif name == \"application_name\":\n                    value = self.tenant.get_instance_name()\n                msg_buf.write_str(value, \"utf-8\")\n                msg_buf.end_message()\n                buf.write_buffer(msg_buf)\n                if self.debug:\n                    self.debug_print(f\"ParameterStatus: {name}={value}\")\n            self.write(buf)\n            # Try to sync the settings, especially client_encoding.\n            await conn.sql_apply_state(self._dbview)\n\n        self.write(self.ready_for_query())\n        self.flush()\n\n    cdef inline WriteBuffer ready_for_query(self):\n        cdef WriteBuffer msg_buf\n        self.ignore_till_sync = False\n        msg_buf = WriteBuffer.new_message(b'Z')\n        if self._dbview.in_tx():\n            if self._dbview._tx_error:\n                msg_buf.write_byte(b'E')\n            else:\n                msg_buf.write_byte(b'T')\n        else:\n            msg_buf.write_byte(b'I')\n        return msg_buf.end_message()\n\n    def on_success(self, query_unit):\n        cdef:\n            PreparedStmt stmt\n\n        if query_unit.deallocate is not None:\n            stmt_name = query_unit.deallocate.stmt_name\n            self.sql_prepared_stmts.pop(stmt_name, None)\n            self.sql_prepared_stmts_map.pop(stmt_name, None)\n            self.prepared_stmts.pop(stmt_name, None)\n            # If any wrapping prepared statements referred to this\n            # prepared statement, invalidate them.\n            for wrapping_ps in self.wrapping_prepared_stmts.pop(stmt_name, []):\n                stmt = self.prepared_stmts.get(wrapping_ps)\n                if stmt is not None:\n                    stmt.parse_action.invalidate()\n\n    def on_error(self, query_unit):\n        cdef:\n            PreparedStmt stmt\n\n        if query_unit.prepare is not None:\n            stmt_name = query_unit.prepare.stmt_name\n            self.sql_prepared_stmts.pop(stmt_name, None)\n            self.sql_prepared_stmts_map.pop(stmt_name, None)\n            self.prepared_stmts.pop(stmt_name, None)\n            # If any wrapping prepared statements referred to this\n            # prepared statement, invalidate them.\n            for wrapping_ps in self.wrapping_prepared_stmts.pop(stmt_name, []):\n                stmt = self.prepared_stmts.get(wrapping_ps)\n                if stmt is not None:\n                    stmt.parse_action.invalidate()\n\n    async def main_step(self, char mtype):\n        try:\n            await self._main_step(mtype)\n        except pgerror.BackendError as ex:\n            self.write_error(ex)\n            self.write(self.ready_for_query())\n            self.flush()\n            self.request_stop()\n\n    async def _main_step(self, char mtype):\n        cdef:\n            WriteBuffer buf\n            ConnectionView dbv\n\n        dbv = self._dbview\n\n        self.check_readiness()\n\n        if self.debug:\n            self.debug_print(\"main_step\", repr(chr(mtype)))\n            if self.ignore_till_sync:\n                self.debug_print(\"ignoring\")\n\n        if mtype == b'S':  # Sync\n            self.buffer.finish_message()\n            if self.debug:\n                self.debug_print(\"Sync\")\n            if dbv._in_tx_implicit:\n                actions = [PGMessage(PGAction.SYNC)]\n                async with self.with_pgcon() as conn:\n                    success, _ = await conn.sql_extended_query(\n                        actions, self, self.database.dbver, dbv)\n                    self.ignore_till_sync = not success\n            else:\n                self.ignore_till_sync = False\n                self.write(self.ready_for_query())\n                self.flush()\n\n        elif mtype == b'X':  # Terminate\n            self.buffer.finish_message()\n            if self.debug:\n                self.debug_print(\"Terminate\")\n            self.close()\n            return True\n\n        elif self.ignore_till_sync:\n            self.buffer.discard_message()\n\n        elif mtype == b'Q':  # Query\n            try:\n                query = self.buffer.read_null_str()\n                metrics.query_size.observe(\n                    len(query), self.get_tenant_label(), 'sql'\n                )\n                query_str = query.decode(\"utf8\")\n                self.buffer.finish_message()\n                if self.debug:\n                    self.debug_print(\"Query\", query_str)\n                actions = await self.simple_query(query_str)\n                del query_str, query\n            except Exception as ex:\n                self.write_error(ex)\n                self.write(self.ready_for_query())\n                self.flush()\n\n            else:\n                async with self.with_pgcon() as conn:\n                    try:\n                        _, rq_sent = await conn.sql_extended_query(\n                            actions,\n                            self,\n                            self.database.dbver,\n                            dbv,\n                        )\n                    except Exception as ex:\n                        self.write_error(ex)\n                        self.write(self.ready_for_query())\n                    else:\n                        if not rq_sent:\n                            self.write(self.ready_for_query())\n\n                self.flush()\n\n        elif (\n            mtype == b'P' or mtype == b'B' or mtype == b'D' or mtype == b'E' or\n            # One of Parse, Bind, Describe or Execute starts an extended query\n            mtype == b'C'  # or Close\n        ):\n            try:\n                actions, exception = await self.extended_query()\n            except ExtendedQueryError as ex:\n                actions = ()\n                exception = ex\n            else:\n                async with self.with_pgcon() as conn:\n                    try:\n                        success, _ = await conn.sql_extended_query(\n                            actions, self, self.database.dbver, dbv)\n                        self.ignore_till_sync = not success\n                    except Exception as ex:\n                        self.write_error(ex)\n                        self.flush()\n                        self.ignore_till_sync = True\n            if exception:\n                self.write_error(exception.args[0])\n                self.flush()\n                self.ignore_till_sync = True\n\n        elif mtype == b'H':  # Flush\n            self.buffer.finish_message()\n            if self.debug:\n                self.debug_print(\"Flush\")\n            self.flush()\n\n        else:\n            if self.debug:\n                self.debug_print(\n                    \"MESSAGE\", chr(mtype), self.buffer.consume_message()\n                )\n            raise pgerror.FeatureNotSupported()\n\n    async def simple_query(self, query_str: str) -> list[PGMessage]:\n        cdef:\n            PreparedStmt stmt\n\n        actions = []\n        dbv = self._dbview\n\n        if self._disable_normalization:\n            source = pg_parser.Source.from_string(query_str)\n        else:\n            source = pg_parser.NormalizedSource.from_string(query_str)\n        query_units = await self.compile(source, dbv)\n\n        # TODO: currently, normalization does not work with multiple queries\n        # so we must re-run the compilation with non-normalized query.\n        # Ideally we could detect this before compilation.\n        if len(query_units) > 1:\n            source = pg_parser.Source.from_string(query_str)\n            query_units = await self.compile(source, dbv)\n\n        for qu in query_units:\n            self.check_capabilities(qu)\n\n        already_in_implicit_tx = dbv._in_tx_implicit\n        metrics.sql_queries.inc(\n            len(query_units), self.tenant.get_instance_name()\n        )\n        self._query_count += len(query_units)\n\n        if not already_in_implicit_tx:\n            actions.append(PGMessage(PGAction.START_IMPLICIT_TX))\n\n        for qu in query_units:\n            if qu.execute is not None:\n                fe_settings = dbv.current_fe_settings()\n                known_be_name = (\n                    self.sql_prepared_stmts_map.get(qu.execute.stmt_name))\n                recompile = (\n                    qu.fe_settings != fe_settings\n                    or qu.execute.be_stmt_name != known_be_name.encode(\"utf-8\")\n                )\n                actions.extend(await self._ensure_nested_ps_exists(\n                    dbv, qu, force_recompilation=recompile))\n            else:\n                recompile = False\n            if recompile:\n                stmt, new_stmts = await self._parse_statement(\n                    stmt_name=None,\n                    query_str=qu.orig_query,\n                    parse_data=b\"\\x00\\x00\",\n                    dbv=dbv,\n                    force_recompilation=True,\n                    injected_action=True,\n                )\n            else:\n                stmt, new_stmts = await self._parse_unit(\n                    stmt_name=None,\n                    unit=qu,\n                    source=source,\n                    parse_data=b\"\\x00\\x00\",\n                    dbv=dbv,\n                    injected_action=True,\n                )\n            parse_unit = stmt.parse_action.query_unit\n            if parse_unit.set_vars:\n                actions.extend(self._build_sql_settings_actions(parse_unit))\n            actions.append(stmt.parse_action)\n\n            # 2 bytes: number of format codes (1)\n            # 2 bytes: first format code (1) is binary\n            #          (this implies that all args are binary)\n            # 2 bytes: number of arguments (0)\n            # 2 bytes: number of result format codes (0)\n            #          (this implies that )\n            bind_data = b\"\\x00\\x01\\x00\\x01\\x00\\x00\\x00\\x00\"\n            # remap argumnets, which will inject globals\n            bind_data = remap_arguments(\n                bind_data,\n                parse_unit.params,\n                dbv.current_fe_settings(),\n                source,\n                self.get_permissions(),\n                self.username,\n            )\n            actions.append(\n                PGMessage(\n                    PGAction.BIND,\n                    portal_name=\"\",\n                    stmt_name=parse_unit.stmt_name,\n                    args=bind_data,\n                    query_unit=parse_unit,\n                    injected=True,\n                )\n            )\n\n            actions.append(\n                PGMessage(\n                    PGAction.DESCRIBE_STMT_ROWS,\n                    stmt_name=parse_unit.stmt_name,\n                    query_unit=parse_unit,\n                )\n            )\n            actions.append(\n                PGMessage(\n                    PGAction.EXECUTE,\n                    args=0,\n                    portal_name=\"\",\n                    query_unit=parse_unit,\n                    injected=False,\n                )\n            )\n            actions.append(\n                PGMessage(\n                    PGAction.CLOSE_PORTAL,\n                    portal_name=\"\",\n                    query_unit=parse_unit,\n                    injected=True,\n                )\n            )\n\n        actions.append(PGMessage(PGAction.SYNC))\n\n        return actions\n\n    async def extended_query(self):\n        cdef:\n            WriteBuffer buf\n            int16_t i\n            bytes data\n            bint in_implicit\n            PreparedStmt stmt\n            ConnectionView dbv\n\n        # Extended-query pre-plays on a deeply-cloned temporary dbview so as to\n        # compose the actions list with correct states; the actual changes to\n        # dbview is applied in pgcon.pyx when the actions are actually executed\n        dbv = copy.deepcopy(self._dbview)\n\n        actions = deque()\n        fresh_stmts = set()\n        in_implicit = self._dbview._in_tx_implicit\n\n        # Here we will exhaust the buffer and queue up actions for the backend.\n        # Any error in this step will be handled in the outer main_step() -\n        # the error will be returned, any remaining messages in the buffer will\n        # be discarded until a Sync message is found (ignore_till_sync).\n        # This also means no partial action is executed in the backend for now.\n        while self.buffer.take_message():\n            if not in_implicit:\n                actions.append(PGMessage(PGAction.START_IMPLICIT_TX))\n                in_implicit = True\n                with managed_error():\n                    dbv.start_implicit()\n            mtype = self.buffer.get_message_type()\n\n            if mtype == b'P':  # Parse\n                stmt_name = self.buffer.read_null_str().decode(\"utf8\")\n                query_bytes = self.buffer.read_null_str()\n                query_str = query_bytes.decode(\"utf8\")\n                data = self.buffer.consume_message()\n                if self.debug:\n                    self.debug_print(\"Parse\", repr(stmt_name), query_str, data)\n                metrics.query_size.observe(\n                    len(query_bytes), self.get_tenant_label(), 'sql'\n                )\n\n                with managed_error():\n                    if (\n                        stmt_name and (\n                            stmt_name in self.prepared_stmts\n                            or stmt_name in self.sql_prepared_stmts\n                        )\n                    ):\n                        raise pgerror.new(\n                            pgerror.ERROR_DUPLICATE_PREPARED_STATEMENT,\n                            f\"prepared statement \\\"{stmt_name}\\\" already \"\n                            f\"exists\",\n                        )\n\n                    stmt, new_stmts = await self._parse_statement(\n                        stmt_name, query_str, data, dbv\n                    )\n                    if stmt.parse_action.query_unit.execute is not None:\n                        actions.extend(\n                            await self._ensure_nested_ps_exists(\n                                dbv,\n                                stmt.parse_action.query_unit,\n                            )\n                        )\n                    fresh_stmts.update(new_stmts)\n                    actions.append(stmt.parse_action)\n\n            elif mtype == b'B':  # Bind\n                portal_name = self.buffer.read_null_str().decode(\"utf8\")\n                stmt_name = self.buffer.read_null_str().decode(\"utf8\")\n                data = self.buffer.consume_message()\n                if self.debug:\n                    self.debug_print(\n                        \"Bind\", repr(portal_name), repr(stmt_name), data\n                    )\n\n                with managed_error():\n                    stmt = await self._ensure_ps_locality(\n                        dbv,\n                        stmt_name,\n                        fresh_stmts,\n                        actions,\n                    )\n\n                    try:\n                        params = stmt.parse_action.query_unit.params\n                        fe_settings = dbv.current_fe_settings()\n                        data = remap_arguments(\n                            data,\n                            params,\n                            fe_settings,\n                            stmt.source,\n                            self.get_permissions(),\n                            self.username,\n                        )\n                    except Exception as e:\n                        # we return here instead of raising the exception\n                        # because we want to also return the previous actions\n                        return actions, ExtendedQueryError(e)\n\n                    actions.append(\n                        PGMessage(\n                            PGAction.BIND,\n                            stmt_name=stmt.parse_action.stmt_name,\n                            portal_name=portal_name,\n                            args=data,\n                            query_unit=stmt.parse_action.query_unit,\n                        )\n                    )\n                    dbv.create_portal(portal_name, stmt.parse_action.query_unit)\n\n            elif mtype == b'D':  # Describe\n                kind = self.buffer.read_byte()\n                name = self.buffer.read_null_str().decode(\"utf8\")\n                self.buffer.finish_message()\n                if self.debug:\n                    self.debug_print(\"Describe\", kind, repr(name))\n\n                with managed_error():\n                    if kind == b'S':  # prepared statement\n                        stmt = await self._ensure_ps_locality(\n                            dbv,\n                            name,\n                            fresh_stmts,\n                            actions,\n                        )\n                        actions.append(\n                            PGMessage(\n                                PGAction.DESCRIBE_STMT,\n                                stmt_name=stmt.parse_action.stmt_name,\n                                query_unit=stmt.parse_action.query_unit,\n                            )\n                        )\n\n                    elif kind == b'P':  # portal\n                        actions.append(\n                            PGMessage(\n                                PGAction.DESCRIBE_PORTAL,\n                                portal_name=name,\n                                query_unit=dbv.find_portal(name),\n                            )\n                        )\n\n                    else:\n                        raise pgerror.ProtocolViolation(\n                            \"invalid Describe kind\"\n                        )\n\n            elif mtype == b'E':  # Execute\n                portal_name = self.buffer.read_null_str().decode(\"utf8\")\n                max_rows = self.buffer.read_int32()\n                self.buffer.finish_message()\n                if self.debug:\n                    self.debug_print(\"Execute\", repr(portal_name), max_rows)\n\n                metrics.sql_queries.inc(1.0, self.tenant.get_instance_name())\n                self._query_count += 1\n                with managed_error():\n                    unit = dbv.find_portal(portal_name)\n                    if unit.set_vars:\n                        actions.extend(self._build_sql_settings_actions(unit))\n                    actions.append(\n                        PGMessage(\n                            PGAction.EXECUTE,\n                            portal_name=portal_name,\n                            args=max_rows,\n                            query_unit=unit,\n                        )\n                    )\n                    dbv.on_success(unit)\n\n            elif mtype == b'C':  # Close\n                kind = self.buffer.read_byte()\n                name_bytes = self.buffer.read_null_str()\n                name = name_bytes.decode(\"utf8\")\n                self.buffer.finish_message()\n                if self.debug:\n                    self.debug_print(\"Close\", kind, repr(name))\n\n                with managed_error():\n                    if kind == b'S':  # prepared statement\n                        if name not in self.prepared_stmts:\n                            raise pgerror.new(\n                                pgerror.ERROR_INVALID_SQL_STATEMENT_NAME,\n                                f\"prepared statement \\\"{name}\\\" does not \"\n                                f\"exist\",\n                            )\n                        # The prepared statement in the backend is managed by\n                        # the LRU cache in pgcon.pyx, we don't close it here\n                        fresh_stmts.discard(name)\n                        self.prepared_stmts.pop(name)\n                        actions.append(\n                            PGMessage(\n                                PGAction.CLOSE_STMT,\n                                stmt_name=name_bytes,\n                            ),\n                        )\n\n                    elif kind == b'P':  # portal\n                        actions.append(\n                            PGMessage(\n                                PGAction.CLOSE_PORTAL,\n                                portal_name=name,\n                                query_unit=dbv.close_portal(name),\n                            ),\n                        )\n\n                    else:\n                        raise pgerror.ProtocolViolation(\"invalid Close kind\")\n\n            elif mtype == b'H':  # Flush\n                self.buffer.finish_message()\n                if self.debug:\n                    self.debug_print(\"Flush\")\n                actions.append(PGMessage(PGAction.FLUSH))\n\n            elif mtype == b'S':  # Sync\n                in_implicit = False\n                self.buffer.finish_message()\n                if self.debug:\n                    self.debug_print(\"Sync\")\n                with managed_error():\n                    actions.append(PGMessage(PGAction.SYNC))\n                    dbv.end_implicit()\n                break\n\n            else:\n                # Other messages would cut off the current extended_query()\n                break\n\n        if self.debug:\n            self.debug_print(\"extended_query\", actions)\n        return actions, None\n\n    def check_capabilities(\n        self,\n        query_unit,\n    ):\n        query_capabilities = query_unit.capabilities\n\n        role_capability = self.get_role_capability()\n        if query_capabilities & ~role_capability:\n            raise query_capabilities.make_error(\n                role_capability,\n                errors.DisabledCapabilityError,\n                f\"role {self.username} does not have permission\",\n            )\n\n    def get_role_capability(self) -> enums.Capability:\n        if capability := self.tenant.get_role_capabilities().get(\n            self.username\n        ):\n            return capability\n        return enums.Capability.NONE\n\n    def get_permissions(self) -> tuple[bool, Sequence[str]]:\n        if role_desc := self.tenant.get_roles().get(self.username):\n            return (\n                bool(role_desc.get('superuser')),\n                (role_desc.get('all_permissions') or ())\n            )\n        return False, ()\n\n    async def _ensure_ps_locality(\n        self,\n        dbv: ConnectionView,\n        stmt_name: str,\n        local_stmts: set[str],\n        actions\n    ) -> PreparedStmt:\n        \"\"\"Make sure given *stmt_name* is known by Postgres\n\n        Frontend SQL connections do not normally own Postgres connections,\n        so there is no affinity between them.  Thus, whenever we receive\n        a message operating on some prepared statement, we must ensure\n        that this statement has been prepared in the currently active\n        Postgres connection.  We rely on pgcon LRU to actually make a\n        decision on whether to issue the injected Parse messages.\n\n        NB: this method mutates *local_stmts* and *actions*.\n        \"\"\"\n        cdef:\n            PreparedStmt stmt\n\n        stmt = self.prepared_stmts.get(stmt_name)\n        if stmt is None:\n            raise pgerror.new(\n                pgerror.ERROR_INVALID_SQL_STATEMENT_NAME,\n                f\"prepared statement \\\"{stmt_name}\\\" does not \"\n                f\"exist\",\n            )\n\n        if stmt_name not in local_stmts:\n            # Non-local statement, so inject its Parse.\n            fe_settings = dbv.current_fe_settings()\n            qu = stmt.parse_action.query_unit\n            assert qu is not None\n            if stmt.parse_action.fe_settings != fe_settings:\n                # Some of the statically compiler-evaluated\n                # queries like `current_schema` depend on the\n                # fe_settings, we need to re-compile if the\n                # fe_settings have changed.\n                stmt.parse_action.invalidate()\n\n            if (\n                qu.execute is not None\n                and (\n                    qu.execute.be_stmt_name\n                    != self.sql_prepared_stmts_map.get(\n                        qu.execute.stmt_name).encode(\"utf-8\")\n                )\n            ):\n                # Likewise, re-compile if this is an EXECUTE query\n                # and the translated name of the prepared statement\n                # has changed (e.g. due to it having been deallocated\n                # and prepared with a different query).\n                stmt.parse_action.invalidate()\n\n            if not stmt.parse_action.is_valid():\n                parse_actions, new_stmts = await self._reparse(\n                    stmt_name,\n                    stmt.parse_action,\n                    dbv,\n                )\n                local_stmts.update(new_stmts)\n                actions.extend(parse_actions)\n                stmt = self.prepared_stmts[stmt_name]\n            else:\n                actions.append(stmt.parse_action.as_injected())\n                local_stmts.add(stmt_name)\n\n        return stmt\n\n    async def _reparse(\n        self,\n        str stmt_name,\n        PGMessage parse_action,\n        ConnectionView dbv,\n    ):\n        cdef:\n            PreparedStmt outer_stmt\n        actions = []\n        qu = parse_action.query_unit\n        assert qu is not None\n\n        if self.debug:\n            self.debug_print(\"reparsing\", stmt_name, parse_action)\n\n        if (\n            qu.prepare is not None\n            or qu.execute is not None\n        ):\n            actions.extend(\n                await self._ensure_nested_ps_exists(\n                    dbv,\n                    qu,\n                    force_recompilation=True,\n                ),\n            )\n\n        outer_stmt, new_stmts = await self._parse_statement(\n            stmt_name,\n            qu.orig_query,\n            parse_action.args[2],\n            dbv,\n            force_recompilation=True,\n            injected_action=True,\n        )\n\n        actions.append(outer_stmt.parse_action)\n\n        return actions, new_stmts\n\n    async def _ensure_nested_ps_exists(\n        self,\n        dbv: ConnectionView,\n        execute_unit: dbstate.SQLQueryUnit,\n        force_recompilation: bool = False,\n    ) -> list[PGMessage]:\n        cdef:\n            PreparedStmt sql_stmt\n\n        exec_data = execute_unit.execute\n        prep_qu = self.sql_prepared_stmts.pop(exec_data.stmt_name, None)\n        actions = []\n\n        if prep_qu is None:\n            raise pgerror.new(\n                pgerror.ERROR_INVALID_SQL_STATEMENT_NAME,\n                f\"prepared statement \"\n                f\"\\\"{exec_data.stmt_name}\\\" does not \"\n                f\"exist\",\n            )\n\n        sql_stmt, _ = await self._parse_statement(\n            prep_qu.stmt_name.decode(\"utf-8\"),\n            prep_qu.orig_query,\n            b\"\\x00\\x00\",\n            dbv,\n            injected_action=True,\n            force_recompilation=force_recompilation,\n        )\n        actions.append(sql_stmt.parse_action)\n        parse_stmt_name = sql_stmt.parse_action.stmt_name\n        portal_name = parse_stmt_name.decode(\"utf-8\")\n        parse_query_unit = sql_stmt.parse_action.query_unit\n        actions.append(\n            PGMessage(\n                PGAction.BIND,\n                portal_name=portal_name,\n                stmt_name=parse_stmt_name,\n                args=b\"\\x00\\x01\\x00\\x01\\x00\\x00\\x00\\x00\",\n                query_unit=parse_query_unit,\n                injected=True,\n            )\n        )\n        actions.append(\n            PGMessage(\n                PGAction.EXECUTE,\n                args=0,\n                portal_name=portal_name,\n                query_unit=parse_query_unit,\n                injected=True,\n            )\n        )\n        actions.append(\n            PGMessage(\n                PGAction.CLOSE_PORTAL,\n                portal_name=portal_name,\n                query_unit=parse_query_unit,\n                injected=True,\n            )\n        )\n        return actions\n\n    def _build_sql_settings_actions(self, qu):\n        actions = []\n        if qu.set_vars == {None: None}:  # RESET ALL\n            actions.append(\n                PGMessage(\n                    PGAction.BIND,\n                    force_portal_name=b\"injected\",\n                    stmt_name=b\"_reset_sql_state_all\",\n                    # 2 bytes: number of format codes (0)\n                    # 2 bytes: number of parameters (0)\n                    # 2 bytes: number of result format codes (0)\n                    args=b\"\\x00\\x00\\x00\\x00\\x00\\x00\",\n                    injected=True,\n                )\n            )\n            actions.append(\n                PGMessage(\n                    PGAction.EXECUTE,\n                    args=0,\n                    force_portal_name=b\"injected\",\n                    injected=True,\n                )\n            )\n            actions.append(\n                PGMessage(\n                    PGAction.CLOSE_PORTAL,\n                    force_portal_name=b\"injected\",\n                    injected=True,\n                )\n            )\n        elif qu.frontend_only:\n            for k, v in qu.set_vars.items():\n                buf = WriteBuffer.new()\n\n                buf.write_int16(0)  # number of format codes\n\n                # number of parameters:\n                if v is None:\n                    buf.write_int16(2)\n                else:\n                    buf.write_int16(3)\n\n                buf.write_len_prefixed_utf8(k)  # 1st param: name\n                buf.write_len_prefixed_utf8(  # 2nd param: type\n                    \"L\" if qu.is_local else \"S\")\n                if v is not None:\n                    buf.write_len_prefixed_utf8(  # 3rd param: value\n                        setting_to_sql(k, v))\n\n                buf.write_int16(0)  # number of result format codes\n\n                if v is None:\n                    actions.append(\n                        PGMessage(\n                            PGAction.BIND,\n                            force_portal_name=b\"injected\",\n                            stmt_name=b\"_reset_sql_state\",\n                            args=bytes(buf),\n                            injected=True,\n                        )\n                    )\n                else:\n                    actions.append(\n                        PGMessage(\n                            PGAction.BIND,\n                            force_portal_name=b\"injected\",\n                            stmt_name=b\"_set_sql_state\",\n                            args=bytes(buf),\n                            injected=True,\n                        )\n                    )\n                actions.append(\n                    PGMessage(\n                        PGAction.EXECUTE,\n                        args=0,\n                        force_portal_name=b\"injected\",\n                        injected=True,\n                    )\n                )\n                actions.append(\n                    PGMessage(\n                        PGAction.CLOSE_PORTAL,\n                        force_portal_name=b\"injected\",\n                        injected=True,\n                    )\n                )\n        return actions\n\n    async def _parse_statement(\n        self,\n        stmt_name: str | None,\n        query_str: str,\n        parse_data: bytes,\n        dbv: ConnectionView,\n        force_recompilation: bool = False,\n        injected_action: bool = False,\n    ) -> Tuple[PreparedStmt, set[str]]:\n        \"\"\"Generate a PARSE action for *query_str*.\n\n        The *query_str* string must contain exactly one SQL statement.\n        \"\"\"\n        stmts = set()\n\n        if self._disable_normalization:\n            source = pg_parser.Source.from_string(query_str)\n        else:\n            source = pg_parser.NormalizedSource.from_string(query_str)\n\n        query_units = await self.compile(\n            source, dbv, ignore_cache=force_recompilation\n        )\n        if len(query_units) > 1:\n            raise pgerror.new(\n                pgerror.ERROR_SYNTAX_ERROR,\n                \"cannot insert multiple commands into a prepared \"\n                \"statement\",\n            )\n\n        for qu in query_units:\n            self.check_capabilities(qu)\n\n        return await self._parse_unit(\n            stmt_name,\n            query_units[0],\n            source,\n            parse_data,\n            dbv,\n            injected_action=injected_action,\n        )\n\n    async def _parse_unit(\n        self,\n        stmt_name: str | None,\n        unit: dbstate.SQLQueryUnit,\n        source: pg_parser.Source,\n        parse_data: bytes,\n        dbv: ConnectionView,\n        injected_action: bool = False,\n    ) -> Tuple[PreparedStmt, set[str]]:\n        stmts = set()\n\n        fe_settings = dbv.current_fe_settings()\n        nested_ps_name = None\n        if unit.prepare is not None:\n            # Statement-level PREPARE\n            nested_ps_name = unit.prepare.stmt_name\n            unit = self._validate_prepare_stmt(unit)\n            stmts.add(nested_ps_name)\n            self.sql_prepared_stmts[nested_ps_name] = unit\n            self.sql_prepared_stmts_map[nested_ps_name] = (\n                unit.prepare.be_stmt_name.decode(\"utf-8\"))\n        elif unit.execute is not None:\n            # Statement-level EXECUTE\n            nested_ps_name = unit.execute.stmt_name\n            unit = self._validate_execute_stmt(unit)\n        elif unit.deallocate is not None:\n            # Statement-level DEALLOCATE\n            nested_ps_name = unit.deallocate.stmt_name\n            unit = self._validate_deallocate_stmt(unit)\n\n        remapped_parse_data = remap_parameters(parse_data, unit.params)\n\n        action = PGMessage(\n            PGAction.PARSE,\n            stmt_name=unit.stmt_name,\n            args=(unit.query.encode(\"utf-8\"), remapped_parse_data, parse_data),\n            query_unit=unit,\n            fe_settings=fe_settings,\n            injected=injected_action,\n        )\n\n        if stmt_name is not None and nested_ps_name is not None:\n            # This is a prepared statement of an operation on *another*\n            # prepared statement, and so we must track this relationship\n            # in case the nested prepared statement gets deallocated.\n            try:\n                self.wrapping_prepared_stmts[nested_ps_name].add(stmt_name)\n            except KeyError:\n                self.wrapping_prepared_stmts[nested_ps_name] = set([stmt_name])\n\n        stmt = PreparedStmt(\n            parse_action=action,\n            source=source,\n        )\n\n        if stmt_name is not None:\n            self.prepared_stmts[stmt_name] = stmt\n            stmts.add(stmt_name)\n\n        return stmt, stmts\n\n    async def compile(\n        self, source: pg_parser.Source, ConnectionView dbv, ignore_cache=False\n    ) -> List[dbstate.SQLQueryUnit]:\n        if self.debug:\n            self.debug_print(\"Compile\", source.text())\n\n        fe_settings = dbv.current_fe_settings()\n        key = compute_cache_key(source, fe_settings)\n\n        ignore_cache |= self._disable_cache\n\n        result: List[dbstate.SQLQueryUnit]\n        if not ignore_cache:\n            result = self.database.lookup_compiled_sql(key)\n            if result is not None:\n                return result\n        # Remember the schema version we are compiling on, so that we can\n        # cache the result with the matching version. In case of concurrent\n        # schema update, we're only storing an outdated cache entry, and\n        # the next identical query could get recompiled on the new schema.\n        schema_version = self.database.schema_version\n        compiler_pool = self.server.get_compiler_pool()\n        started_at = time.monotonic()\n        try:\n            result = await compiler_pool.compile_sql(\n                self.dbname,\n                self.database.user_schema_pickle,\n                self.database._index._global_schema_pickle,\n                self.database.reflection_cache,\n                self.database.db_config,\n                self.database._index.get_compilation_system_config(),\n                source,\n                dbv.fe_transaction_state(),\n                self.sql_prepared_stmts_map,\n                self.dbname,\n                self.username,\n                client_id=self.tenant.client_id,\n                client_name=self.tenant.get_instance_name(),\n            )\n        finally:\n            metrics.query_compilation_duration.observe(\n                time.monotonic() - started_at,\n                self.tenant.get_instance_name(),\n                \"sql\",\n                )\n        self.database.cache_compiled_sql(key, result, schema_version)\n        metrics.sql_compilations.inc(\n            len(result), self.tenant.get_instance_name()\n        )\n        if self.debug:\n            self.debug_print(\"Compile result\", result)\n        return result\n\n    def _validate_prepare_stmt(self, qu):\n        assert qu.prepare is not None\n        stmt_name = qu.prepare.stmt_name\n        if (\n            stmt_name in self.prepared_stmts\n            or stmt_name in self.sql_prepared_stmts\n        ):\n            raise pgerror.new(\n                pgerror.ERROR_DUPLICATE_PREPARED_STATEMENT,\n                f\"prepared statement \\\"{stmt_name}\\\" \"\n                f\"already exists\",\n            )\n        return qu\n\n    def _validate_execute_stmt(self, qu):\n        assert qu.execute is not None\n        stmt_name = qu.execute.stmt_name\n        sql_ps = self.sql_prepared_stmts.get(stmt_name)\n        if sql_ps is None:\n            raise pgerror.new(\n                pgerror.ERROR_INVALID_SQL_STATEMENT_NAME,\n                f\"prepared statement \\\"{stmt_name}\\\" does \"\n                f\"not exist\",\n            )\n        return qu\n\n    def _validate_deallocate_stmt(self, qu):\n        assert qu.deallocate is not None\n        stmt_name = qu.deallocate.stmt_name\n        sql_ps = self.sql_prepared_stmts.get(stmt_name)\n        if sql_ps is None:\n            raise pgerror.new(\n                pgerror.ERROR_INVALID_SQL_STATEMENT_NAME,\n                f\"prepared statement \\\"{stmt_name}\\\" does \"\n                f\"not exist\",\n            )\n        return qu\n\n\ndef compute_cache_key(\n    source: pg_parser.Source, fe_settings: dbstate.SQLSettings\n) -> bytes:\n    h = hashlib.blake2b(source.cache_key())\n    for key, value in fe_settings.items():\n        if key.startswith('global '):\n            continue\n        h.update(hash(value).to_bytes(8, signed=True))\n    return h.digest()\n\n\ncdef bytes remap_arguments(\n    data: bytes,\n    params: list[dbstate.SQLParam] | None,\n    fe_settings: dbstate.SQLSettings,\n    source: pg_parser.Source,\n    permission_info: tuple[bool, Sequence[str]],\n    username: str,  # HACK\n):\n    cdef:\n        int16_t param_format_count\n        int32_t offset\n        int32_t arg_offset_external\n        int16_t param_count_external\n        int32_t size\n\n    is_superuser, permissions = permission_info\n\n    # The \"external\" parameters (that are visible to the user)\n    # don't include the internal params for globals and extracted constants.\n\n    # So when we send external params to postgres, we remap them\n    # to correct positions and add the globals.\n\n    buf = WriteBuffer.new()\n\n    # remap param format codes\n    param_format_count = read_int16(data[0:2])\n    offset = 2\n    if params:\n        buf.write_int16(len(params))\n        for i, param in enumerate(params):\n            if isinstance(param, dbstate.SQLParamExternal):\n                if param_format_count == 0:\n                    buf.write_int16(0) # text\n                elif param_format_count == 1:\n                    buf.write_bytes(\n                        data[offset:offset+2]\n                    )\n                else:\n                    o = offset + i * 2\n                    buf.write_bytes(data[o:o+2])\n            elif isinstance(param, dbstate.SQLParamExtractedConst):\n                buf.write_int16(0) # text\n            else:\n                # this is for globals\n                buf.write_int16(1) # binary\n    else:\n        buf.write_int16(0)\n    offset += param_format_count * 2\n\n    # find positions of external args\n    arg_count_external = read_int16(data[offset:offset+2])\n    offset += 2\n    arg_offset_external = offset\n    for p in range(arg_count_external):\n        size = read_int32(data[offset:offset+4])\n        if size == -1:  # special case: NULL\n            size = 0\n        size += 4 # for size which is int32\n        offset += size\n\n    # write remapped args\n    if params:\n        buf.write_int16(len(params))\n\n        param_count_external = 0\n        for i, param in enumerate(params):\n            if not isinstance(param, dbstate.SQLParamExternal):\n                break\n            param_count_external = i + 1\n        if param_count_external != arg_count_external:\n            raise pgerror.new(\n                pgerror.ERROR_PROTOCOL_VIOLATION,\n                f'bind message supplies {arg_count_external} '\n                f'parameters, but prepared statement \"\" requires '\n                f'{param_count_external}',\n            )\n\n        # write external args\n        if arg_offset_external < offset:\n            buf.write_bytes(data[arg_offset_external:offset])\n\n        # write non-external args\n        extracted_consts = list(source.variables().values())\n        for (e, param) in enumerate(params[param_count_external:]):\n            if isinstance(param, dbstate.SQLParamExtractedConst):\n                buf.write_len_prefixed_bytes(extracted_consts[e])\n            elif isinstance(param, dbstate.SQLParamGlobal):\n                name = param.global_name\n                if param.is_permission:\n                    buf.write_int32(1)\n                    buf.write_byte(is_superuser or str(name) in permissions)\n                elif name.module == 'sys' and name.name == 'current_role':\n                    write_arg(buf, param.pg_type, (username,))\n                else:\n                    setting_name = f'global {name.module}::{name.name}'\n                    values = fe_settings.get(setting_name, None)\n\n                    if values == None:\n                        buf.write_int32(-1) # NULL\n                    else:\n                        write_arg(buf, param.pg_type, values)\n    else:\n        buf.write_int16(0)\n\n    # result format codes\n    buf.write_bytes(data[offset:])\n    return bytes(buf)\n\n\ncdef bytes remap_parameters(\n    data: bytes,\n    params: list[dbstate.SQLParam] | None\n):\n    # Inject parameter type descriptions in parse message for parameters for\n    # globals and extracted constants.\n\n    if not params:\n        return b\"\\x00\\x00\"\n\n    buf = WriteBuffer.new()\n    buf.write_int16(len(params))\n\n    # copy the params specified by user\n    specified_ext = read_int16(data[0:2])\n    buf.write_bytes(data[2:2 + specified_ext*4])\n\n    for index, param in enumerate(params):\n\n        # already written\n        if index < specified_ext:\n            continue\n\n        if isinstance(param, dbstate.SQLParamExternal):\n            buf.write_int32(0)  # unspecified\n        elif isinstance(param, dbstate.SQLParamExtractedConst):\n            buf.write_int32(param.type_oid)\n        elif isinstance(param, dbstate.SQLParamGlobal):\n            buf.write_int32(0)  # unspecified\n    assert len(bytes(buf)) == 2 + 4 * len(params)\n    return bytes(buf)\n\n\ncdef write_arg(\n    buf: WriteBuffer, pg_type: tuple, values: dbstate.SQLSetting\n):\n    value = values[0]\n\n    if pg_type == ('text',) and isinstance(value, str):\n        val = str(value).encode('UTF-8')\n        buf.write_len_prefixed_bytes(val)\n    elif pg_type == ('uuid',) and isinstance(value, str):\n        try:\n            id = uuid.UUID(value)\n            buf.write_len_prefixed_bytes(id.bytes)\n        except ValueError:\n            buf.write_int32(-1)  # NULL\n    elif pg_type == ('int8',) and isinstance(value, int):\n        buf.write_int32(8)\n        buf.write_int64(value)\n    elif pg_type == ('int4',) and isinstance(value, int):\n        buf.write_int32(4)\n        buf.write_int32(value)\n    elif pg_type == ('int2',) and isinstance(value, int):\n        buf.write_int32(2)\n        buf.write_int16(value)\n    elif pg_type == ('bool',):\n        is_truthy = is_setting_truthy(value)\n        if is_truthy == None:\n            buf.write_int32(-1)\n        else:\n            buf.write_int32(1)\n            buf.write_byte(1 if is_truthy else 0)\n    elif pg_type == ('float8',) and isinstance(value, float):\n        buf.write_int32(8)\n        buf.write_double(value)\n    elif pg_type == ('float4',) and isinstance(value, float):\n        buf.write_int32(4)\n        buf.write_float(value)\n    else:\n        buf.write_int32(-1)  # NULL\n        raise RuntimeError(\n            f\"unimplemented glob type={pg_type}, value={type(value)}\"\n        )\n\n\ndef is_setting_truthy(value: str | int | float) -> bool | None:\n    if isinstance(value, int):\n        return value != 0\n    if isinstance(value, str):\n        value = value.lower()\n        if value == 'o':\n            # ambigious\n            return None\n\n        truthy_values = ('on', 'true', 'yes', '1')\n        if any(t.startswith(value) for t in truthy_values):\n            return True\n\n        falsy_values = ('off', 'false', 'no', '0')\n        if any(t.startswith(value) for t in falsy_values):\n            return False\n    return None\n\n\ncdef inline int16_t read_int16(data: bytes):\n    return int.from_bytes(data[0:2], \"big\", signed=True)\n\n\ncdef inline int32_t read_int32(data: bytes):\n    return int.from_bytes(data[0:4], \"big\", signed=True)\n\n\ndef new_pg_connection(server, sslctx, endpoint_security, connection_made_at):\n    return PgConnection(\n        server,\n        sslctx,\n        endpoint_security,\n        passive=False,\n        transport=srvargs.ServerConnTransport.TCP_PG,\n        external_auth=False,\n        connection_made_at=connection_made_at,\n    )\n"
  },
  {
    "path": "edb/server/protocol/protocol.pxd",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom edb.server.protocol cimport binary\n\n\ncdef class HttpRequest:\n\n    cdef:\n        public object url\n        public bytes version\n        public bint should_keep_alive\n        public bytes content_type\n        public bytes method\n        public bytes accept\n        public bytes body\n        public bytes host\n        public bytes origin\n        public bytes authorization\n        public object params\n        public object forwarded\n        public object cookies\n\n\ncdef class HttpResponse:\n\n    cdef:\n        public object status\n        public bint close_connection\n        public bytes content_type\n        public dict custom_headers\n        public bytes body\n        public bint sent\n\n\ncdef class HttpProtocol:\n\n    cdef public object server\n\n    cdef:\n        object loop\n        object parser\n        object transport\n        object unprocessed\n        object sslctx\n        object sslctx_pgext\n        bint in_response\n        bint first_data_call\n        bint external_auth\n        bint respond_hsts\n        bint is_tls\n        object binary_endpoint_security\n        object http_endpoint_security\n        object tenant\n        bint is_tenant_host\n        object connection_made_at\n\n        HttpRequest current_request\n\n    cdef _not_found(self, HttpRequest request, HttpResponse response,\n                    str message = ?)\n    cdef _bad_request(self, HttpRequest request, HttpResponse response,\n                      str message)\n    cdef _unauthorized(self, HttpRequest request, HttpResponse response,\n                       str message)\n    cdef _return_binary_error(self, binary.EdgeConnection proto)\n    cdef _write(self, bytes req_version, bytes resp_status,\n                bytes content_type, dict custom_headers, bytes body,\n                bint close_connection)\n\n    cpdef write(self, HttpRequest request, HttpResponse response)\n\n    cdef unhandled_exception(self, bytes status, ex)\n    cdef resume(self)\n    cpdef close(self)\n    cdef inline _schedule_handle_request(self, request)\n    cdef inline _close_with_error(self, bytes status, bytes message)\n"
  },
  {
    "path": "edb/server/protocol/protocol.pyi",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport asyncio\nimport http\nimport http.cookies\nimport httptools\nimport ssl\n\nfrom edb.server import args as srvargs\nfrom edb.server import server\n\nclass HttpRequest:\n    url: httptools.URL\n    version: bytes\n    should_keep_alive: bool\n    content_type: bytes\n    method: bytes\n    accept: bytes\n    body: bytes\n    host: bytes\n    origin: bytes\n    authorization: bytes\n    params: dict[bytes, bytes]\n    forwarded: dict[bytes, bytes]\n    cookies: http.cookies.SimpleCookie\n\nclass HttpResponse:\n    status: http.HTTPStatus\n    close_connection: bool\n    content_type: bytes\n    custom_headers: dict[str, str]\n    body: bytes\n    sent: bool\n\nclass HttpProtocol(asyncio.Protocol):\n    def __init__(\n        self,\n        server: server.BaseServer,\n        sslctx: ssl.SSLContext,\n        sslctx_pgext: ssl.SSLContext,\n        *,\n        external_auth: bool = False,\n        binary_endpoint_security: srvargs.ServerEndpointSecurityMode,\n        http_endpoint_security: srvargs.ServerEndpointSecurityMode,\n    ) -> None:\n        ...\n\n    def write_raw(self, data: bytes) -> None:\n        ...\n\n    def write(self, request: HttpRequest, response: HttpResponse) -> None:\n        ...\n\n    def close(self) -> None:\n        ...\n"
  },
  {
    "path": "edb/server/protocol/protocol.pyx",
    "content": "\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ninclude \"./consts.pxi\"\n\n\nimport asyncio\nimport collections\nimport http\nimport http.cookies\nimport re\nimport ssl\nimport time\nimport urllib.parse\n\nimport httptools\n\nfrom edb import errors\nfrom edb.common import debug\nfrom edb.common import markup\nfrom edb.common.log import current_tenant\n\nfrom edb.graphql import extension as graphql_ext\n\nfrom edb.server import args as srvargs\nfrom edb.server import config, metrics as srv_metrics\nfrom edb.server import tenant as edbtenant\nfrom edb.server.protocol cimport binary\nfrom edb.server.protocol import binary\nfrom edb.server.protocol import pg_ext\nfrom edb.server import defines as edbdef\nfrom edb.server.dbview cimport dbview\n# Without an explicit cimport of `pgproto.debug`, we\n# can't cimport `protocol.binary` for some reason.\nfrom edb.server.pgproto.debug cimport PG_DEBUG\n\nfrom . import auth\nfrom . cimport auth_helpers\nfrom . import edgeql_ext\nfrom . import metrics\nfrom . import server_info\nfrom . import notebook_ext\nfrom . import system_api\nfrom . import ui_ext\nfrom . import auth_ext\nfrom . import ai_ext\n\n\nHTTPStatus = http.HTTPStatus\n\nPROTO_MIME = (\n    f'application/x.edgedb.'\n    f'v_{edbdef.CURRENT_PROTOCOL[0]}_{edbdef.CURRENT_PROTOCOL[1]}'\n    f'.binary'\n).encode()\n\nPROTO_MIME_RE = re.compile(br'application/x\\.edgedb\\.v_(\\d+)_(\\d+)\\.binary')\n\n\ncdef class HttpRequest:\n\n    def __cinit__(self):\n        self.body = b''\n        self.authorization = b''\n        self.content_type = b''\n        self.forwarded = {}\n        self.cookies = http.cookies.SimpleCookie()\n\n\ncdef class HttpResponse:\n\n    def __cinit__(self):\n        self.status = HTTPStatus.OK\n        self.content_type = b'text/plain'\n        self.custom_headers = {}\n        self.body = b''\n        self.close_connection = False\n        self.sent = False\n\n\ncdef class HttpProtocol:\n\n    def __init__(\n        self,\n        server,\n        sslctx,\n        sslctx_pgext,\n        *,\n        external_auth: bool=False,\n        binary_endpoint_security = None,\n        http_endpoint_security = None,\n    ):\n        self.loop = server.get_loop()\n        self.server = server\n        self.tenant = None\n        self.transport = None\n        self.external_auth = external_auth\n        self.sslctx = sslctx\n        self.sslctx_pgext = sslctx_pgext\n\n        self.parser = None\n        self.current_request = None\n        self.in_response = False\n        self.unprocessed = None\n        self.first_data_call = True\n\n        self.binary_endpoint_security = binary_endpoint_security\n        self.http_endpoint_security = http_endpoint_security\n        self.respond_hsts = False  # redirect non-TLS HTTP clients to TLS URL\n\n        self.is_tls = False\n        self.is_tenant_host = False\n\n    def connection_made(self, transport):\n        self.connection_made_at = time.monotonic()\n        self.transport = transport\n\n    def connection_lost(self, exc):\n        srv_metrics.client_connection_duration.observe(\n            time.monotonic() - self.connection_made_at,\n            self.get_tenant_label(),\n            \"http\",\n        )\n        self.transport = None\n        self.unprocessed = None\n        self.server.maybe_auto_shutdown()\n\n    def get_tenant_label(self):\n        if self.tenant is None:\n            return \"unknown\"\n        else:\n            return self.tenant.get_instance_name()\n\n    def pause_writing(self):\n        pass\n\n    def resume_writing(self):\n        pass\n\n    def eof_received(self):\n        pass\n\n    def data_received(self, data):\n        if self.first_data_call:\n            self.first_data_call = False\n\n            # Detect if the client is speaking TLS in the \"first\" data using\n            # the SSL library. This is not the official handshake as we only\n            # need to know \"is_tls\"; the first data is used again for the true\n            # handshake if is_tls = True. This is for further responding a nice\n            # error message to non-TLS clients.\n            is_tls = True\n            try:\n                outgoing = ssl.MemoryBIO()\n                incoming = ssl.MemoryBIO()\n                incoming.write(data)\n                sslobj = self.sslctx.wrap_bio(\n                    incoming, outgoing, server_side=True\n                )\n                sslobj.do_handshake()\n            except ssl.SSLWantReadError:\n                pass\n            except ssl.SSLError:\n                is_tls = False\n\n            self.is_tls = is_tls\n\n            if is_tls:\n                # Most clients should arrive here to continue with TLS\n                self.transport.pause_reading()\n                self.loop.create_task(self._forward_first_data(data))\n                self.loop.create_task(self._start_tls())\n                return\n\n            # In case when we're talking to a non-TLS client, keep using the\n            # legacy magic byte check to choose the HTTP or binary protocol.\n            if data[0:2] == b'V\\x00':\n                # This is, most likely, our binary protocol,\n                # as its first message kind is `V`.\n                #\n                # Switch protocols now (for compatibility).\n                if (\n                    self.binary_endpoint_security\n                    is srvargs.ServerEndpointSecurityMode.Optional\n                ):\n                    self._switch_to_binary_protocol(data)\n                else:\n                    self._return_binary_error(\n                        self._switch_to_binary_protocol()\n                    )\n                return\n            elif data[0:1] == b'\\x00':\n                # Postgres protocol, assuming the 1st message is less than 16MB\n                pg_ext_conn = pg_ext.new_pg_connection(\n                    self.server,\n                    self.sslctx_pgext,\n                    self.binary_endpoint_security,\n                    connection_made_at=self.connection_made_at,\n                )\n                self.transport.set_protocol(pg_ext_conn)\n                pg_ext_conn.connection_made(self.transport)\n                pg_ext_conn.data_received(data)\n                return\n            else:\n                # HTTP.\n                self._init_http_parser()\n                self.respond_hsts = (\n                    self.http_endpoint_security\n                    is srvargs.ServerEndpointSecurityMode.Tls\n                )\n\n        try:\n            self.parser.feed_data(data)\n        except Exception as ex:\n            self.unhandled_exception(b'400 Bad Request', ex)\n\n    def on_url(self, url: bytes):\n        self.current_request.url = httptools.parse_url(url)\n\n    def on_header(self, name: bytes, value: bytes):\n        name = name.lower()\n        if name == b'content-type':\n            self.current_request.content_type = value\n        elif name == b'host':\n            self.current_request.host = value\n        elif name == b'origin':\n            self.current_request.origin = value\n        elif name == b'accept':\n            if self.current_request.accept:\n                self.current_request.accept += b',' + value\n            else:\n                self.current_request.accept = value\n        elif name == b'authorization':\n            self.current_request.authorization = value\n        elif name.startswith(b'x-edgedb-'):\n            if self.current_request.params is None:\n                self.current_request.params = {}\n            param = name[len(b'x-edgedb-'):]\n            self.current_request.params[param] = value\n        elif name.startswith(b'x-gel-'):\n            if self.current_request.params is None:\n                self.current_request.params = {}\n            param = name[len(b'x-gel-'):]\n            self.current_request.params[param] = value\n        elif name.startswith(b'x-forwarded-'):\n            if self.current_request.forwarded is None:\n                self.current_request.forwarded = {}\n            forwarded_key = name[len(b'x-forwarded-'):]\n            self.current_request.forwarded[forwarded_key] = value\n        elif name == b'cookie':\n            self.current_request.cookies.load(value.decode('ascii'))\n\n    def on_body(self, body: bytes):\n        self.current_request.body += body\n\n    def on_message_begin(self):\n        self.current_request = HttpRequest()\n\n    def on_message_complete(self):\n        self.transport.pause_reading()\n\n        req = self.current_request\n\n        req.version = self.parser.get_http_version().encode()\n        req.should_keep_alive = self.parser.should_keep_alive()\n        req.method = self.parser.get_method().upper()\n\n        if self.in_response:\n            # pipelining support\n            if self.unprocessed is None:\n                self.unprocessed = collections.deque()\n            self.unprocessed.append(req)\n        else:\n            self.in_response = True\n            self._schedule_handle_request(req)\n\n        self.server._http_last_minute_requests += 1\n\n    cdef inline _schedule_handle_request(self, request):\n        if self.tenant is None:\n            self.loop.create_task(self._handle_request(request))\n        elif self.tenant.is_accepting_connections():\n            self.tenant.create_task(\n                self._handle_request(request), interruptable=False\n            )\n        else:\n            self._close_with_error(\n                b'503 Service Unavailable',\n                b'The server is closing.',\n            )\n\n    cpdef close(self):\n        if self.transport is not None:\n            self.transport.close()\n            self.transport = None\n        self.unprocessed = None\n\n    cdef unhandled_exception(self, bytes status, ex):\n        if debug.flags.server:\n            markup.dump(ex)\n\n        self._close_with_error(\n            status,\n            f'{type(ex).__name__}: {ex}'.encode(),\n        )\n\n    cdef inline _close_with_error(self, bytes status, bytes message):\n        self._write(\n            b'1.0',\n            status,\n            b'text/plain',\n            {},\n            message,\n            True)\n\n        self.close()\n\n    cdef resume(self):\n        if self.transport is None:\n            return\n\n        if self.unprocessed:\n            req = self.unprocessed.popleft()\n            self._schedule_handle_request(req)\n        else:\n            self.transport.resume_reading()\n\n    cdef _write(self, bytes req_version, bytes resp_status,\n                bytes content_type, dict custom_headers, bytes body,\n                bint close_connection):\n        if self.transport is None:\n            return\n        data = [\n            b'HTTP/', req_version, b' ', resp_status, b'\\r\\n',\n            b'Content-Type: ', content_type, b'\\r\\n',\n        ]\n        if content_type != b\"text/event-stream\":\n            data.extend(\n                (b'Content-Length: ', f'{len(body)}'.encode(), b'\\r\\n'),\n            )\n\n        for key, value in custom_headers.items():\n            data.append(f'{key}: {value}\\r\\n'.encode())\n\n        if close_connection:\n            data.append(b'Connection: close\\r\\n')\n        data.append(b'\\r\\n')\n        if body:\n            data.append(body)\n        self.transport.write(b''.join(data))\n\n    cpdef write(self, HttpRequest request, HttpResponse response):\n        assert type(response.status) is HTTPStatus\n        self._write(\n            request.version,\n            f'{response.status.value} {response.status.phrase}'.encode(),\n            response.content_type,\n            response.custom_headers,\n            response.body,\n            response.close_connection or not request.should_keep_alive)\n        response.sent = True\n\n    def write_raw(self, bytes data):\n        self.transport.write(data)\n\n    def _switch_to_binary_protocol(self, data=None):\n        binproto = binary.new_edge_connection(\n            self.server,\n            self.tenant,\n            external_auth=self.external_auth,\n            connection_made_at=self.connection_made_at,\n        )\n        self.transport.set_protocol(binproto)\n        binproto.connection_made(self.transport)\n        if data:\n            binproto.data_received(data)\n        return binproto\n\n    def _init_http_parser(self):\n        self.parser = httptools.HttpRequestParser(self)\n        self.current_request = HttpRequest()\n\n    async def _forward_first_data(self, data):\n        # As we stole the \"first data\", we need to manually send it back to\n        # the SSLProtocol. The hack here is highly-coupled with uvloop impl.\n        transport = self.transport  # The TCP transport\n        for i in range(3):\n            await asyncio.sleep(0)\n            ssl_proto = self.transport.get_protocol()\n            if ssl_proto is not self:\n                break\n        else:\n            raise RuntimeError(\"start_tls() hasn't run in 3 loop iterations\")\n\n        # Delay for one more iteration to make sure the first data is fed after\n        # SSLProtocol.connection_made() is called.\n        await asyncio.sleep(0)\n\n        data_len = len(data)\n        buf = ssl_proto.get_buffer(data_len)\n        buf[:data_len] = data\n        ssl_proto.buffer_updated(data_len)\n\n    async def _start_tls(self):\n        self.transport = await self.loop.start_tls(\n            self.transport, self, self.sslctx, server_side=True\n        )\n        sslobj = self.transport.get_extra_info('ssl_object')\n        tenant = self.server.retrieve_tenant(sslobj)\n        if tenant is edbtenant.host_tenant:\n            tenant = None\n            self.is_tenant_host = True\n        self.tenant = tenant\n        if self.tenant is not None:\n            current_tenant.set(self.get_tenant_label())\n        if sslobj.selected_alpn_protocol() == 'edgedb-binary':\n            self._switch_to_binary_protocol()\n        else:\n            # It's either HTTP as the negotiated protocol, or the negotiation\n            # failed and we have no idea what ALPN the client has set. Here we\n            # just start talking in HTTP, and let the client bindings decide if\n            # this is an error based on the ALPN result.\n            self._init_http_parser()\n\n    cdef _return_binary_error(self, binary.EdgeConnection proto):\n        proto.write_error(errors.BinaryProtocolError(\n            'TLS Required',\n            details='The server requires Transport Layer Security (TLS)',\n            hint='Upgrade the client to a newer version that supports TLS'\n        ))\n        proto.close()\n\n    async def _handle_request(self, HttpRequest request):\n        cdef:\n            HttpResponse response = HttpResponse()\n\n        if self.transport is None:\n            return\n\n        if self.respond_hsts:\n            if request.host:\n                path = request.url.path.lstrip(b'/')\n                loc = b'https://' + request.host + b'/' + path\n                self.transport.write(\n                    b'HTTP/1.1 301 Moved Permanently\\r\\n'\n                    b'Strict-Transport-Security: max-age=31536000\\r\\n'\n                    b'Location: ' + loc + b'\\r\\n'\n                    b'\\r\\n'\n                )\n            else:\n                msg = b'Request is missing a header: Host\\r\\n'\n                self.transport.write(\n                    b'HTTP/1.1 400 Bad Request\\r\\n'\n                    b'Content-Length: ' + str(len(msg)).encode() + b'\\r\\n'\n                    b'\\r\\n' + msg\n                )\n\n            self.close()\n            return\n\n        if self.is_tls:\n            if (\n                self.http_endpoint_security\n                is srvargs.ServerEndpointSecurityMode.Optional\n            ):\n                response.custom_headers['Strict-Transport-Security'] = \\\n                    'max-age=0'\n            elif (\n                self.http_endpoint_security\n                is srvargs.ServerEndpointSecurityMode.Tls\n            ):\n                response.custom_headers['Strict-Transport-Security'] = \\\n                    'max-age=31536000'\n            else:\n                raise AssertionError(\n                    f\"unexpected http_endpoint_security \"\n                    f\"value: {self.http_endpoint_security}\"\n                )\n\n        try:\n            await self.handle_request(request, response)\n        except errors.AvailabilityError as ex:\n            self._close_with_error(\n                b\"503 Service Unavailable\",\n                f'{type(ex).__name__}: {ex}'.encode(),\n            )\n            return\n        except Exception as ex:\n            self.unhandled_exception(b\"500 Internal Server Error\", ex)\n            return\n\n        if not response.sent:\n            self.write(request, response)\n        self.in_response = False\n\n        if response.close_connection or not request.should_keep_alive:\n            self.close()\n        else:\n            self.resume()\n\n    def check_readiness(self):\n        if self.tenant.is_blocked():\n            readiness_reason = self.tenant.get_readiness_reason()\n            msg = \"the server is not accepting requests\"\n            if readiness_reason:\n                msg = f\"{msg}: {readiness_reason}\"\n            raise errors.ServerBlockedError(msg)\n        elif not self.tenant.is_online():\n            readiness_reason = self.tenant.get_readiness_reason()\n            msg = \"the server is going offline\"\n            if readiness_reason:\n                msg = f\"{msg}: {readiness_reason}\"\n            raise errors.ServerOfflineError(msg)\n\n    async def handle_request(self, HttpRequest request, HttpResponse response):\n        request_url = get_request_url(request, self.is_tls)\n        path = request_url.path.decode('ascii')\n        path = path.strip('/')\n        path_parts = path.split('/')\n        path_parts_len = len(path_parts)\n        route = path_parts[0]\n\n        if self.tenant is None and route in ['db', 'auth', 'branch']:\n            self.tenant = self.server.get_default_tenant()\n            self.check_readiness()\n            if self.tenant.is_accepting_connections():\n                return await self.tenant.create_task(\n                    self.handle_request(request, response),\n                    interruptable=False,\n                )\n            else:\n                return self._close_with_error(\n                    b'503 Service Unavailable',\n                    b'The server is closing.',\n                )\n\n        if route in ['db', 'branch']:\n            if path_parts_len < 2:\n                return self._not_found(request, response)\n\n            dbname = urllib.parse.unquote(path_parts[1])\n            dbname = self.tenant.resolve_branch_name(\n                database=dbname if route == 'db' else None,\n                branch=dbname if route == 'branch' else None,\n            )\n            extname = path_parts[2] if path_parts_len > 2 else None\n\n            # Binary proto tunnelled through HTTP\n            if extname is None:\n                if await self._handle_cors(\n                    request, response,\n                    dbname=dbname,\n                    allow_methods=['POST'],\n                    allow_headers=[\n                        'Authorization', 'X-EdgeDB-User', 'X-Gel-User'\n                    ],\n                ):\n                    return\n\n                if request.method == b'POST':\n                    if not request.content_type:\n                        return self._bad_request(\n                            request,\n                            response,\n                            message=\"missing or malformed Content-Type header\",\n                        )\n\n                    ver_m = PROTO_MIME_RE.match(request.content_type)\n                    if not ver_m:\n                        return self._bad_request(\n                            request,\n                            response,\n                            message=\"missing or malformed Content-Type header\",\n                        )\n\n                    proto_ver = (\n                        int(ver_m.group(1).decode()),\n                        int(ver_m.group(2).decode()),\n                    )\n\n                    if proto_ver < edbdef.MIN_PROTOCOL:\n                        return self._bad_request(\n                            request,\n                            response,\n                            message=\"requested protocol version is too old and \"\n                                \"no longer supported\",\n                        )\n                    if proto_ver > edbdef.CURRENT_PROTOCOL:\n                        proto_ver = edbdef.CURRENT_PROTOCOL\n\n                    params = request.params\n                    if params is None:\n                        conn_params = {}\n                    else:\n                        conn_params = {\n                            n.decode(\"utf-8\"): v.decode(\"utf-8\")\n                            for n, v in request.params.items()\n                        }\n\n                    conn_params[\"database\"] = dbname\n\n                    response.body = await binary.eval_buffer(\n                        self.server,\n                        self.tenant,\n                        database=dbname,\n                        data=self.current_request.body,\n                        conn_params=conn_params,\n                        protocol_version=proto_ver,\n                        auth_data=self.current_request.authorization,\n                        transport=srvargs.ServerConnTransport.HTTP,\n                        tcp_transport=self.transport,\n                    )\n                    response.status = http.HTTPStatus.OK\n                    response.content_type = (\n                        f'application/x.edgedb.v_'\n                        f'{proto_ver[0]}_{proto_ver[1]}.binary'\n                    ).encode()\n                    response.close_connection = True\n\n            else:\n                if await self._handle_cors(\n                    request, response,\n                    dbname=dbname,\n                    allow_methods=['GET', 'POST'],\n                    allow_headers=[\n                        'Authorization', 'X-EdgeDB-User', 'X-Gel-User'\n                    ],\n                    expose_headers=(\n                        ['EdgeDB-Protocol-Version', 'Gel-Protocol-Version']\n                        if extname == 'notebook'\n                        else ['WWW-Authenticate'] if extname != 'auth'\n                        else None\n                    ),\n                    allow_credentials=True\n                ):\n                    return\n\n                # Check if this is a request to a registered extension\n                if extname == 'edgeql':\n                    extname = 'edgeql_http'\n                if extname == 'ext':\n                    if path_parts_len < 4:\n                        return self._not_found(request, response)\n                    extname = path_parts[3]\n                    args = path_parts[4:]\n                else:\n                    args = path_parts[3:]\n\n                role_name = None\n                if extname != 'auth':\n                    role_name = await self._check_http_auth(\n                        request, response, dbname\n                    )\n                    if not role_name:\n                        return\n\n                db = self.tenant.maybe_get_db(dbname=dbname)\n                if db is None:\n                    return self._not_found(request, response)\n\n                if extname not in db.extensions:\n                    return self._not_found(request, response)\n\n                if extname == 'graphql':\n                    await graphql_ext.handle_request(\n                        request, response, db, role_name, args, self.tenant\n                    )\n                elif extname == 'notebook':\n                    await notebook_ext.handle_request(\n                        request, response, db, role_name, args, self.tenant\n                    )\n                elif extname == 'edgeql_http':\n                    await edgeql_ext.handle_request(\n                        request, response, db, role_name, args, self.tenant\n                    )\n                elif extname == 'ai':\n                    await ai_ext.handle_request(\n                        self,\n                        request, response, db, role_name, args, self.tenant\n                    )\n                elif extname == 'auth':\n                    netloc = (\n                        f\"{request_url.host.decode()}:{request_url.port}\"\n                            if request_url.port\n                            else request_url.host.decode()\n                    )\n                    ext_base_path = f\"{request_url.schema.decode()}://\" \\\n                                    f\"{netloc}/{route}/\" \\\n                                    f\"{urllib.parse.quote(dbname)}/ext/auth\"\n                    handler = auth_ext.http.Router(\n                        db=db,\n                        base_path=ext_base_path,\n                        tenant=self.tenant,\n                    )\n                    await handler.handle_request(request, response, args)\n                    if args:\n                        if args[0] == 'ui':\n                            if not (len(args) > 1 and args[1] == \"_static\"):\n                                srv_metrics.auth_ui_renders.inc(\n                                    1.0, self.get_tenant_label()\n                                )\n                        else:\n                            srv_metrics.auth_api_calls.inc(\n                                1.0, self.get_tenant_label()\n                            )\n                else:\n                    return self._not_found(request, response)\n\n        elif route == 'auth':\n            if await self._handle_cors(\n                request, response,\n                allow_methods=['GET'],\n                allow_headers=['Authorization'],\n                expose_headers=['WWW-Authenticate', 'Authentication-Info']\n            ):\n                return\n\n            # Authentication request\n            await auth.handle_request(\n                request,\n                response,\n                path_parts[1:],\n                self.tenant,\n            )\n        elif route == 'server':\n            if not await self._authenticate_for_default_conn_transport(\n                request,\n                response,\n                srvargs.ServerConnTransport.HTTP_HEALTH,\n            ):\n                return\n\n            # System API request\n            await system_api.handle_request(\n                request,\n                response,\n                path_parts[1:],\n                self.server,\n                self.tenant,\n                is_tenant_host=self.is_tenant_host,\n            )\n        elif path_parts == ['metrics'] and request.method == b'GET':\n            if not await self._authenticate_for_default_conn_transport(\n                request,\n                response,\n                srvargs.ServerConnTransport.HTTP_METRICS,\n            ):\n                return\n\n            self.server.get_compiler_pool().refresh_metrics()\n            # Quoting the Open Metrics spec:\n            #    Implementers MUST expose metrics in the OpenMetrics\n            #    text format in response to a simple HTTP GET request\n            #    to a documented URL for a given process or device.\n            #    This endpoint SHOULD be called \"/metrics\".\n            await metrics.handle_request(\n                request,\n                response,\n                self.tenant,\n            )\n        elif (path_parts == ['server-info'] and\n            request.method == b'GET' and\n            (self.server.in_dev_mode() or self.server.in_test_mode())\n        ):\n            await server_info.handle_request(\n                request,\n                response,\n                self.server,\n            )\n        elif path_parts[0] == 'ui':\n            if not self.server.is_admin_ui_enabled():\n                return self._not_found(\n                    request,\n                    response,\n                    \"Admin UI is not enabled on this EdgeDB instance. \"\n                    \"Run the server with --admin-ui=enabled \"\n                    \"(or EDGEDB_SERVER_ADMIN_UI=enabled) to enable.\"\n                )\n            else:\n                await ui_ext.handle_request(\n                    request,\n                    response,\n                    path_parts[1:],\n                    self.server,\n                )\n        else:\n            return self._not_found(request, response)\n\n    cdef _not_found(\n        self,\n        HttpRequest request,\n        HttpResponse response,\n        str message = \"Unknown path\",\n    ):\n        response.body = message.encode(\"utf-8\")\n        response.status = http.HTTPStatus.NOT_FOUND\n        response.close_connection = True\n\n    cdef _bad_request(\n        self,\n        HttpRequest request,\n        HttpResponse response,\n        str message,\n    ):\n        response.body = message.encode(\"utf-8\")\n        response.status = http.HTTPStatus.BAD_REQUEST\n        response.close_connection = True\n\n    async def _handle_cors(\n        self,\n        HttpRequest request,\n        HttpResponse response,\n        *,\n        str dbname = None,\n        list allow_methods = None,\n        list allow_headers = [],\n        list expose_headers = None,\n        bint allow_credentials = False\n    ):\n        db = self.tenant.maybe_get_db(dbname=dbname) if dbname else None\n\n        config = None\n        if db is not None:\n            if db.db_config is None:\n                await db.introspection()\n\n            config = db.db_config.get('cors_allow_origins')\n        if config is None:\n            config = self.tenant.get_sys_config().get('cors_allow_origins')\n\n        allowed_origins = config.value if config else None\n        overrides = self.server.get_cors_always_allowed_origins()\n\n        if allowed_origins is None and overrides == []:\n            return False\n\n        origin = request.origin.decode() if request.origin else None\n\n        origin_allowed = origin is not None and (\n                any(\n                    override.match(origin) if isinstance(override, re.Pattern)\n                    else origin == override\n                    for override in overrides\n                )\n                or (origin in allowed_origins or '*' in allowed_origins)\n            )\n\n        if origin_allowed:\n            response.custom_headers['Access-Control-Allow-Origin'] = origin\n            if expose_headers is not None:\n                response.custom_headers['Access-Control-Expose-Headers'] = (\n                    ', '.join(expose_headers))\n\n        if request.method == b'OPTIONS':\n            response.status = http.HTTPStatus.NO_CONTENT\n            if origin_allowed:\n                if allow_methods is not None:\n                    response.custom_headers['Access-Control-Allow-Methods'] = (\n                        ', '.join(allow_methods))\n                response.custom_headers['Access-Control-Allow-Headers'] = (\n                    ', '.join(['Content-Type'] + allow_headers))\n                if allow_credentials:\n                    response.custom_headers['Access-Control-Allow-Credentials'] = (\n                        'true')\n\n            return True\n\n        return False\n\n    cdef _unauthorized(\n        self,\n        HttpRequest request,\n        HttpResponse response,\n        str message,\n    ):\n        response.body = message.encode(\"utf-8\")\n        response.status = http.HTTPStatus.UNAUTHORIZED\n        response.close_connection = True\n\n    async def _check_http_auth(\n        self,\n        HttpRequest request,\n        HttpResponse response,\n        str dbname,\n    ):\n        dbindex: dbview.DatabaseIndex = self.tenant._dbindex\n\n        scheme = None\n        try:\n            # Extract the username from the relevant request headers\n            scheme, auth_payload = auth_helpers.extract_token_from_auth_data(\n                request.authorization)\n            username, opt_password = auth_helpers.extract_http_user(\n                scheme, auth_payload, request.params)\n            username = self.tenant.resolve_user_name(username)\n\n            # Fetch the configured auth methods\n            authmethods = await self.tenant.get_auth_methods(\n                username, srvargs.ServerConnTransport.SIMPLE_HTTP)\n\n            auth_errors = {}\n\n            for authmethod in authmethods:\n                authmethod_name = authmethod._tspec.name.split('::')[1]\n                try:\n                    # If the auth method and the provided auth information\n                    # match, try to resolve the authentication.\n                    if authmethod_name == 'JWT' and scheme == 'bearer':\n                        auth_helpers.auth_jwt(\n                            self.tenant, auth_payload, username, dbname)\n                    elif authmethod_name == 'Password' and scheme == 'basic':\n                        auth_helpers.auth_basic(\n                            self.tenant, username, opt_password)\n                    elif authmethod_name == 'Trust':\n                        pass\n                    elif authmethod_name == 'SCRAM':\n                        raise errors.AuthenticationError(\n                            'authentication failed: '\n                            'SCRAM authentication required but not '\n                            'supported for HTTP'\n                        )\n                    elif authmethod_name == 'mTLS':\n                        if (\n                            self.http_endpoint_security\n                            is srvargs.ServerEndpointSecurityMode.Tls\n                            or self.is_tls\n                        ):\n                            auth_helpers.auth_mtls_with_user(\n                                self.transport, username)\n                    else:\n                        raise errors.AuthenticationError(\n                            'authentication failed: wrong method used')\n\n                except errors.AuthenticationError as e:\n                    auth_errors[authmethod_name] = e\n\n                else:\n                    break\n\n            if len(auth_errors) == len(authmethods):\n                if len(auth_errors) > 1:\n                    desc = \"; \".join(\n                        f\"{k}: {e.args[0]}\" for k, e in auth_errors.items())\n                    raise errors.AuthenticationError(\n                        f\"all authentication methods failed: {desc}\")\n                else:\n                    raise next(iter(auth_errors.values()))\n\n            role = self.tenant.get_roles().get(username)\n            if not role:\n                raise errors.AuthenticationError('authentication failed')\n            branches = role['branches']\n            if '*' not in branches and dbname not in branches:\n                raise errors.AuthenticationError(\n                    f\"authentication failed: user does not have permission for \"\n                    f\"database branch '{dbname}'\"\n                )\n\n        except Exception as ex:\n            if debug.flags.server:\n                markup.dump(ex)\n\n            self._unauthorized(request, response, str(ex))\n\n            # If no scheme was specified, add a WWW-Authenticate header\n            if scheme == '':\n                response.custom_headers['WWW-Authenticate'] = (\n                    'Basic realm=\"edgedb\", Bearer'\n                )\n\n            return None\n\n        return username\n\n    async def _authenticate_for_default_conn_transport(\n        self,\n        HttpRequest request,\n        HttpResponse response,\n        transport: srvargs.ServerConnTransport,\n    ):\n        try:\n            auth_methods = self.server.get_default_auth_methods(transport)\n            auth_errors = {}\n\n            for auth_method in auth_methods:\n                authmethod_name = auth_method._tspec.name.split('::')[1]\n                try:\n                    # If the auth method and the provided auth information\n                    # match, try to resolve the authentication.\n                    if authmethod_name == 'Trust':\n                        pass\n                    elif authmethod_name == 'mTLS':\n                        if (\n                            self.http_endpoint_security\n                            is srvargs.ServerEndpointSecurityMode.Tls\n                            or self.is_tls\n                        ):\n                            auth_helpers.auth_mtls(self.transport)\n                    else:\n                        raise errors.AuthenticationError(\n                            'authentication failed: wrong method used')\n                except errors.AuthenticationError as e:\n                    auth_errors[authmethod_name] = e\n                else:\n                    break\n\n            if len(auth_errors) == len(auth_methods):\n                if len(auth_errors) > 1:\n                    desc = \"; \".join(\n                        f\"{k}: {e.args[0]}\" for k, e in auth_errors.items())\n                    raise errors.AuthenticationError(\n                        f\"all authentication methods failed: {desc}\")\n                else:\n                    raise next(iter(auth_errors.values()))\n\n        except Exception as ex:\n            if debug.flags.server:\n                markup.dump(ex)\n\n            self._unauthorized(request, response, str(ex))\n\n            return False\n\n        return True\n\ndef get_request_url(request, is_tls):\n    request_url = request.url\n    default_schema = b\"https\" if is_tls else b\"http\"\n    if all(\n        getattr(request_url, attr) is None\n        for attr in ('schema', 'host', 'port')\n    ):\n        forwarded = request.forwarded if hasattr(request, 'forwarded') else {}\n        schema = forwarded.get(b'proto', default_schema).decode()\n        host_header = forwarded.get(b'host', request.host).decode()\n\n        host, _, port = host_header.partition(':')\n        path = request_url.path.decode()\n        new_url = f\"{schema}://\"\\\n                  f\"{host}{port and ':' + port}\"\\\n                  f\"{path}\"\n        request_url = httptools.parse_url(new_url.encode())\n\n    return request_url\n"
  },
  {
    "path": "edb/server/protocol/request_scheduler.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom dataclasses import dataclass, field\nfrom typing import (\n    Final,\n    Iterable,\n    Literal,\n    Optional,\n    Sequence,\n)\n\nimport abc\nimport asyncio\nimport copy\nimport random\n\n\n@dataclass\nclass Timer:\n    \"\"\"Represents a time after which an action should be taken.\n\n    Examples:\n    (None, True) = execute immediately\n    (None, False) = execute any time\n    (10, True) = execute immediately after 10s\n    (10, False) = execute any time after 10s\n    \"\"\"\n\n    time: Optional[float] = None\n\n    # Whether the action should be taken as soon as possible after the time.\n    urgent: bool = True\n\n    @staticmethod\n    def create_delay(delay: Optional[float], urgent: bool) -> Timer:\n        now = asyncio.get_running_loop().time()\n        if delay is None:\n            time = None\n        else:\n            now = asyncio.get_running_loop().time()\n            time = now + delay\n\n        return Timer(time, urgent)\n\n    def is_ready(self) -> bool:\n        now = asyncio.get_running_loop().time()\n        return self.time is None or self.time <= now\n\n    def is_ready_and_urgent(self) -> bool:\n        return self.is_ready() and self.urgent\n\n    def remaining_time(self, max_delay: float) -> float:\n        \"\"\"How long before this timer is ready in seconds.\"\"\"\n        if self.urgent:\n            if self.time is None:\n                return 0\n\n            else:\n                # 1ms extra, just in case\n                now = asyncio.get_running_loop().time()\n                delay = self.time - now + 0.001\n                return min(max(0, delay), max_delay)\n\n        else:\n            # If not urgent, wait as long as possible\n            return max_delay\n\n    @staticmethod\n    def combine(timers: Iterable[Timer]) -> Optional[Timer]:\n        \"\"\"Combine the timers to determine the when to take the next action.\n\n        If the timers are (1, False), (2, False), (3, True), it may be wasteful\n        to act at times [1, 2, 3].\n\n        Instead, we would prefer to act only once, at time 3, since only the\n        third action was urgent.\n        \"\"\"\n        for target_urgency in [True, False]:\n            if any(\n                timer.time is None and timer.urgent == target_urgency\n                for timer in timers\n            ):\n                # An action should be taken right away.\n                return Timer(None, target_urgency)\n\n            urgent_times = [\n                timer.time\n                for timer in timers\n                if timer.time is not None and timer.urgent == target_urgency\n            ]\n            if len(urgent_times) > 0:\n                # An action should be taken after some delay\n                return Timer(min(urgent_times), target_urgency)\n\n        # Nothing to do\n        return None\n\n\ndef _default_delay_time() -> Timer:\n    return Timer()\n\n\n@dataclass\nclass Scheduler[_T](abc.ABC):\n    \"\"\"A scheduler for requests to an asynchronous service.\n\n    A Scheduler both generates requests and tracks when the service can be\n    accessed.\n    \"\"\"\n\n    service: Service\n\n    # The next time to process requests\n    timer: Timer = field(default_factory=_default_delay_time)\n\n    @abc.abstractmethod\n    async def get_params(\n        self, context: Context,\n    ) -> Optional[Sequence[Params[_T]]]:\n        \"\"\"Get parameters for the requests to run.\"\"\"\n        raise NotImplementedError\n\n    async def process(self, context: Context) -> bool:\n        if not self.timer.is_ready():\n            return False\n\n        request_params: Optional[Sequence[Params[_T]]]\n        try:\n            request_params = await self.get_params(context)\n        except Exception:\n            request_params = None\n\n        error_count = 0\n        deferred_costs: dict[str, int] = {\n            limit_name: 0\n            for limit_name in self.service.limits\n        }\n        success_count = 0\n\n        if request_params is None:\n            error_count = 1\n\n        elif len(request_params) > 0:\n            try:\n                execution_report = await execute_no_sleep(\n                    request_params, service=self.service,\n                )\n\n            except Exception:\n                execution_report = ExecutionReport(unknown_error_count=1)\n\n            assert isinstance(execution_report, ExecutionReport)\n\n            self.finalize(execution_report)\n\n            # Cache limits for next time\n            if execution_report.updated_limits is not None:\n                for limit_name, service_limit in self.service.limits.items():\n                    if limit_name not in execution_report.updated_limits:\n                        continue\n\n                    updated_limit = execution_report.updated_limits[limit_name]\n\n                    if service_limit is not None:\n                        service_limit.update_total(updated_limit)\n                        service_limit.delay_factor = updated_limit.delay_factor\n\n                    else:\n                        self.service.limits[limit_name] = updated_limit\n\n            # Update counts\n            error_count = (\n                len(execution_report.known_error_messages)\n                + execution_report.unknown_error_count\n            )\n\n            deferred_costs = execution_report.deferred_costs\n            success_count = execution_report.success_count\n\n        # Update when this service should be processed again\n        self.timer = self.service.next_delay(\n            success_count, deferred_costs, error_count, context.naptime\n        )\n\n        return True\n\n    @abc.abstractmethod\n    def finalize(self, execution_report: ExecutionReport) -> None:\n        \"\"\"An optional final step after executing requests\"\"\"\n        pass\n\n\n@dataclass\nclass Context:\n    \"\"\"Information passed to a Scheduler to process requests.\"\"\"\n\n    # If there is no work, the scheduler should take a nap.\n    naptime: float\n\n\n@dataclass\nclass ExecutionReport:\n    \"\"\"Information about the requests after they are complete\"\"\"\n\n    success_count: int = 0\n    unknown_error_count: int = 0\n    known_error_messages: list[str] = field(default_factory=list)\n    deferred_costs: dict[str, int] = field(default_factory=dict)\n\n    # Some requests may report an update to the service's rate limits.\n    updated_limits: dict[str, Limits] = field(default_factory=dict)\n\n\n@dataclass\nclass Service:\n    \"\"\"Information on how to access to a given service.\"\"\"\n\n    # Information about the service's rate limits\n    # Even if no Limit is available, the presence of a key indicates that a\n    # limit is used at least sometimes.\n    limits: dict[str, Optional[Limits]] = field(default_factory=dict)\n\n    # The maximum number of times to retry requests\n    max_retry_count: Final[int] = 4\n    # Whether to jitter the delay time if a retry error is produced\n    jitter: Final[bool] = True\n    # Initial guess for the delay\n    guess_delay: Final[float] = 1.0\n    # The upper bound for delays\n    delay_max: Final[float] = 60.0\n\n    def next_delay(\n        self,\n        success_count: int,\n        deferred_costs: dict[str, int],\n        error_count: int,\n        naptime: float\n    ) -> Timer:\n        \"\"\"When should the service should be processed again.\"\"\"\n\n        if self.limits is not None:\n            # Find the limit with the largest delay\n            limit_delays: dict[str, Optional[float]] = {}\n            for limit_names, service_limit in self.limits.items():\n                if service_limit is None:\n                    # If no information is available, assume no limits\n                    limit_delays[limit_names] = None\n\n                else:\n                    base_delay = service_limit.base_delay(\n                        deferred_costs[limit_names],\n                        guess=self.guess_delay,\n                    )\n                    if base_delay is None:\n                        limit_delays[limit_names] = None\n\n                    else:\n                        # If delay_factor is very high, it may take quite a long\n                        # time for it to return to 1. A maximum delay prevents\n                        # this service from never getting checked.\n                        limit_delays[limit_names] = (\n                            min(\n                                base_delay * service_limit.delay_factor,\n                                self.delay_max,\n                            )\n                        )\n            delay = _get_maximum_delay(limit_delays)\n\n        else:\n            # We have absolutely no information about the delay, assume naptime.\n            delay = naptime\n\n        if error_count > 0:\n            # There was an error, wait before trying again.\n            # Use the larger of delay or naptime.\n            delay = max(delay, naptime) if delay is not None else naptime\n            urgent = False\n\n        elif any(\n            deferred_cost > 0\n            for deferred_cost in deferred_costs.values()\n        ):\n            # There is some deferred work, apply the delay and run immediately.\n            urgent = True\n\n        elif success_count > 0:\n            # Some work was done successfully. Run again to ensure no more work\n            # needs to be done.\n            delay = None\n            urgent = True\n\n        else:\n            # No work left to do, wait before trying again.\n            # Use the larger of delay or naptime.\n            delay = max(delay, naptime) if delay is not None else naptime\n            urgent = False\n\n        return Timer.create_delay(delay, urgent)\n\n\n@dataclass\nclass Limits:\n    \"\"\"Information about a service's rate limits.\"\"\"\n\n    # Total limit of a resource per minute for a service.\n    total: Optional[int | Literal['unlimited']] = None\n\n    # Remaining resources before the limit is hit.\n    # It is assumed to be decreasing during a call to execute_no_sleep.\n    #\n    # This can be set by users before a call to Scheduler.process.\n    # It will also be updated during execution if a responseincludes an updated\n    # value.\n    #\n    # Finally, it is reset after requests are executed since we don't know when\n    # the next call will be.\n    remaining: Optional[int] = None\n\n    # A delay factor to implement exponential backoff\n    delay_factor: float = 1\n\n    def base_delay(\n        self,\n        request_cost: int,\n        *,\n        guess: float,\n    ) -> Optional[float]:\n        if self.total == 'unlimited':\n            return None\n\n        if self.remaining is not None and request_cost <= self.remaining:\n            return None\n\n        if self.total is not None:\n            assert isinstance(self.total, int)\n            return 60.0 / self.total * 1.1  # 10% buffer just in case\n\n        # guess the delay\n        return guess\n\n    def update_total(self, latest: Limits) -> Limits:\n        \"\"\"Update total based on the latest information.\n\n        The total will change rarely. Always take the latest value if\n        it exists\n        \"\"\"\n        if latest.total is not None:\n            self.total = latest.total\n\n        return self\n\n    def update_remaining(self, latest: Limits) -> Limits:\n        \"\"\"Update remaining based on the latest information.\n\n        The remaining amount is assumed to decreasing during a call to\n        execute_no_sleep.\n        \"\"\"\n        if self.remaining is None:\n            self.remaining = latest.remaining\n        elif latest.remaining is not None:\n            self.remaining = min(self.remaining, latest.remaining)\n\n        if self.total == 'unlimited' and self.remaining:\n            # If there is a remaining value, the total is not actually\n            # unlimited.\n            self.total = None\n\n        return self\n\n\nclass Request[_T](abc.ABC):\n    \"\"\"Represents an async request\"\"\"\n\n    params: Params[_T]\n    _inner: asyncio.Task[Optional[Result[_T]]]\n\n    def __init__(self, params: Params[_T]):\n        self.params = params\n        self._inner = asyncio.create_task(self.run())\n\n    @abc.abstractmethod\n    async def run(self) -> Optional[Result[_T]]:\n        \"\"\"Run the request and return a result.\"\"\"\n        raise NotImplementedError\n\n    async def wait_result(self) -> None:\n        \"\"\"Wait for the request to complete.\"\"\"\n        await self._inner\n\n    def get_result(self) -> Optional[Result[_T]]:\n        \"\"\"Get the result of the request.\"\"\"\n        result = self._inner.result()\n        return result\n\n\nclass Params[_T](abc.ABC):\n    \"\"\"The parameters of an async request.\n\n    These are used to generate requests.\n\n    A single Params instance may be used to generate multiple Request\n    instances if it fails, but can be retried right away.\n    \"\"\"\n\n    @abc.abstractmethod\n    def costs(self) -> dict[str, int]:\n        \"\"\"Expected cost to execute the request.\n\n        Keys must match service rate limits.\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def create_request(self) -> Request[_T]:\n        \"\"\"Create a request with the parameters.\"\"\"\n        raise NotImplementedError\n\n\n@dataclass(frozen=True)\nclass Result[_T](abc.ABC):\n    \"\"\"The result of an async request.\"\"\"\n\n    data: _T | Error\n\n    # Some services can return request limits along with their usual results.\n    # Keys should be a subset of service limits.\n    limits: dict[str, Limits] = field(default_factory=dict)\n\n    @abc.abstractmethod\n    async def finalize(self) -> None:\n        \"\"\"An optional finalize to be run sequentially.\"\"\"\n        pass\n\n\n@dataclass(frozen=True)\nclass Error:\n    \"\"\"Represents an error from an async request.\"\"\"\n\n    message: str\n\n    # If there was an error, it may be possible to retry the request\n    # Eg. 429 too many requests\n    retry: bool\n\n\nasync def execute_no_sleep[_T](\n    params: Sequence[Params[_T]],\n    *,\n    service: Service,\n) -> ExecutionReport:\n    \"\"\"Attempt to execute as many requests as possible without sleeping.\"\"\"\n    report = ExecutionReport()\n\n    # Set up limits\n    execute_limits: dict[str, Limits] = {\n        limit_name: (\n            # If no other information is available, for the first attempt assume\n            # there is no limit.\n            Limits(total='unlimited')\n            if service_limit is None else\n            copy.copy(service_limit)\n        )\n        for limit_name, service_limit in service.limits.items()\n    }\n\n    # If any requests fail and can be retried, retry them up to a maximum number\n    # of times.\n    retry_count: int = 0\n\n    # If the costs are larger than a total limit, set aside the excess to be\n    # processed later.\n    # This prevents wasting resources, and allows the delays to increase\n    # specifically when an unexpected deferral happens.\n    pending_request_indexes: list[int]\n    excess_request_indexes: list[int]\n\n    initial_pending_cost = {\n        limit_name: 0\n        for limit_name in service.limits.keys()\n    }\n    for request_index in range(len(params)):\n        for limit_name, cost in params[request_index].costs().items():\n            initial_pending_cost[limit_name] += cost\n\n        if (\n            # If the pending cost exceeds a known limit, set aside some\n            # requests.\n            any(\n                limit.total < initial_pending_cost[limit_name]\n                for limit_name, limit in service.limits.items()\n                if limit is not None and isinstance(limit.total, int)\n            )\n\n            # Always include at least 1 request\n            and request_index != 0\n        ):\n            pending_request_indexes = (\n                list(range(request_index))\n            )\n            excess_request_indexes = (\n                list(range(request_index, len(params)))\n            )\n            break\n\n    else:\n        # All inputs can be processed\n        pending_request_indexes = list(range(len(params)))\n        excess_request_indexes = []\n\n    while pending_request_indexes and retry_count < service.max_retry_count:\n        # Find the highest delay required by any of the service's limits\n        limit_base_delays = _get_limit_base_delays(\n            params, execute_limits, pending_request_indexes, service.guess_delay\n        )\n        base_delay = _get_maximum_delay(limit_base_delays)\n\n        active_request_indexes: list[int]\n        inactive_request_indexes: list[int]\n        if base_delay is None:\n            # Try to execute all requests.\n            active_request_indexes = pending_request_indexes\n            inactive_request_indexes = []\n\n        elif retry_count == 0:\n            # If there is any delay, only execute one request.\n            # This may update the remaining limit, allowing the remaining\n            # requests to run.\n            active_request_indexes = pending_request_indexes[:1]\n            inactive_request_indexes = pending_request_indexes[1:]\n\n        else:\n            break\n\n        results = await _execute_specified(\n            params, active_request_indexes,\n        )\n\n        # Check results\n        retry_request_indexes: list[int] = []\n\n        for request_index in active_request_indexes:\n            if request_index not in results:\n                report.unknown_error_count += 1\n                continue\n\n            result = results[request_index]\n\n            if isinstance(result.data, Error):\n                if result.data.retry:\n                    # requests can be retried\n                    retry_request_indexes.append(request_index)\n\n                else:\n                    # error with message\n                    report.known_error_messages.append(result.data.message)\n            else:\n                report.success_count += 1\n\n            await result.finalize()\n\n            if result.limits is not None:\n                for limit_name, execute_limit in execute_limits.items():\n                    if limit_name not in result.limits:\n                        continue\n                    result_limit = result.limits[limit_name]\n                    execute_limit.update_total(result_limit)\n                    execute_limit.update_remaining(result_limit)\n\n        retry_count += 1\n        pending_request_indexes = (\n            retry_request_indexes + inactive_request_indexes\n        )\n\n    # Determine which limits cause unexpected deferrals and require additional\n    # delays.\n    limit_base_delays = _get_limit_base_delays(\n        params, execute_limits, pending_request_indexes, service.guess_delay\n    )\n    expected_pending_cost = {\n        limit_name\n        for limit_name in service.limits.keys()\n        if limit_base_delays[limit_name] is not None\n    }\n    if len(expected_pending_cost) == 0:\n        # If requests were deferred, but no limit appears to be the cause, delay\n        # them all just in case.\n        expected_pending_cost = set(service.limits.keys())\n\n    # Update deferred costs and any resulting limits.\n    report.deferred_costs = {\n        limit_name: 0\n        for limit_name in service.limits\n    }\n    for limit_name in service.limits.keys():\n        unexpected_deferred_costs = sum(\n            params[i].costs()[limit_name]\n            for i in pending_request_indexes\n        )\n        excess_deferred_costs = sum(\n            params[i].costs()[limit_name]\n            for i in excess_request_indexes\n        )\n        report.deferred_costs[limit_name] = (\n            unexpected_deferred_costs + excess_deferred_costs\n        )\n\n        if (\n            unexpected_deferred_costs != 0\n\n            # If the limit was not a cause of delays, don't increase the delay\n            # factor.\n            and limit_name in expected_pending_cost\n        ):\n            # If there are deferred requests, gradually increase the delay\n            # factor\n            execute_limits[limit_name].delay_factor *= (\n                1 + random.random() if service.jitter else 2\n            )\n\n        elif (\n            len(report.known_error_messages) == 0\n            and report.unknown_error_count == 0\n            and excess_deferred_costs == 0\n        ):\n            # If there are no errors, gradually decrease the delay factor over\n            # time.\n            execute_limits[limit_name].delay_factor = max(\n                0.95 * execute_limits[limit_name].delay_factor,\n                1,\n            )\n\n    # We don't know when the service will be called again, so just clear the\n    # remaining values\n    for execute_limit in execute_limits.values():\n        execute_limit.remaining = None\n\n    # Return the updated request limits\n    report.updated_limits = execute_limits\n\n    return report\n\n\nasync def _execute_specified[_T](\n    params: Sequence[Params[_T]],\n    indexes: Iterable[int],\n) -> dict[int, Result[_T]]:\n    # Send all requests at once.\n    # We are confident that all requests can be handled right away.\n\n    requests: dict[int, Request[_T]] = {}\n\n    for request_index in indexes:\n        requests[request_index] = params[request_index].create_request()\n\n    results: dict[int, Result[_T]] = {}\n\n    for request_index, request in requests.items():\n        await request.wait_result()\n\n        result = request.get_result()\n        if result is not None:\n            results[request_index] = result\n\n    return results\n\n\ndef _get_limit_base_delays[_T](\n    params: Sequence[Params[_T]],\n    limits: dict[str, Limits],\n    request_indexes: Sequence[int],\n    guess_delay: float,\n) -> dict[str, Optional[float]]:\n    base_delays = {}\n    for limit_name, limit in limits.items():\n        pending_limit_cost = sum(\n            params[request_index].costs()[limit_name]\n            for request_index in request_indexes\n        )\n\n        base_delays[limit_name] = (limit.base_delay(\n            pending_limit_cost, guess=guess_delay,\n        ))\n\n    return base_delays\n\n\ndef _get_maximum_delay(\n    delays: dict[str, Optional[float]]\n) -> Optional[float]:\n    result: Optional[float] = None\n    for delay in delays.values():\n        if result is None:\n            result = delay\n        elif delay is not None:\n            result = max(result, delay)\n\n    return result\n"
  },
  {
    "path": "edb/server/protocol/server_info.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, TYPE_CHECKING\nimport dataclasses\nimport http\nimport json\n\nimport immutables\n\nfrom edb import errors\n\nfrom edb.ir import statypes\n\nfrom edb.common import debug\nfrom edb.common import markup\n\nif TYPE_CHECKING:\n    from edb.server import server as edbserver\n    from edb.server.protocol import protocol\n\n\nclass ImmutableEncoder(json.JSONEncoder):\n\n    def default(self, obj: Any) -> Any:\n        if isinstance(obj, (set, frozenset)):\n            return list(obj)\n        if isinstance(obj, immutables.Map):\n            return dict(obj.items())\n        if dataclasses.is_dataclass(obj) and not isinstance(obj, type):\n            return dataclasses.asdict(obj)\n        if isinstance(obj, statypes.ScalarType):\n            return obj.to_json()\n        if isinstance(obj, statypes.CompositeType):\n            return obj.to_json_value()\n        return super().default(obj)\n\n\nasync def handle_request(\n    request: protocol.HttpRequest,\n    response: protocol.HttpResponse,\n    server: edbserver.Server,\n) -> None:\n    try:\n        output = ImmutableEncoder().encode(server.get_debug_info())\n        response.status = http.HTTPStatus.OK\n        response.content_type = b'application/json'\n        response.body = output.encode()\n        response.close_connection = True\n\n    except Exception as ex:\n        if debug.flags.server:\n            markup.dump(ex)\n\n        # XXX Fix this when LSP \"location\" objects are implemented\n        ex_type = errors.InternalServerError\n\n        _response_error(\n            response, http.HTTPStatus.INTERNAL_SERVER_ERROR, str(ex), ex_type\n        )\n\n\ndef _response_error(\n    response: protocol.HttpResponse,\n    status: http.HTTPStatus,\n    message: str,\n    ex_type: type[errors.EdgeDBError],\n) -> None:\n    response.body = (\n        f'Unexpected error in /server-info.\\n\\n'\n        f'{ex_type.__name__}: {message}'\n    ).encode()\n    response.status = status\n    response.close_connection = True\n"
  },
  {
    "path": "edb/server/protocol/system_api.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import TYPE_CHECKING\n\nimport asyncio\nimport http\nimport json\n\nfrom edb import errors\n\nfrom edb.common import debug\nfrom edb.common import markup\n\n\nif TYPE_CHECKING:\n    from edb.server import tenant as edbtenant, server as edbserver\n    from edb.server.protocol import protocol\n\n\nasync def handle_request(\n    request: protocol.HttpRequest,\n    response: protocol.HttpResponse,\n    path_parts: list[str],\n    server: edbserver.BaseServer,\n    tenant: edbtenant.Tenant,\n    is_tenant_host: bool,\n) -> None:\n    try:\n        if tenant is None:\n            try:\n                tenant = server.get_default_tenant()\n            except Exception:\n                # Multi-tenant server doesn't have default tenant\n                pass\n        if tenant is None and not is_tenant_host:\n            _response(\n                response,\n                http.HTTPStatus.NOT_FOUND,\n                b'\"No such tenant configured\"',\n                True,\n            )\n        elif path_parts == ['status', 'ready'] and request.method == b'GET':\n            if tenant is None:\n                await handle_compiler_query(server, response)\n            else:\n                await tenant.create_task(\n                    handle_readiness_query(request, response, tenant),\n                    interruptable=False,\n                )\n        elif path_parts == ['status', 'alive'] and request.method == b'GET':\n            if tenant is None:\n                await handle_compiler_query(server, response)\n            else:\n                await tenant.create_task(\n                    handle_liveness_query(request, response, tenant),\n                    interruptable=False,\n                )\n        else:\n            _response(\n                response,\n                http.HTTPStatus.NOT_FOUND,\n                b'\"Unknown path\"',\n                True,\n            )\n    except errors.BackendUnavailableError as ex:\n        _response_error(\n            response, http.HTTPStatus.SERVICE_UNAVAILABLE, str(ex), type(ex)\n        )\n    except errors.EdgeDBError as ex:\n        if debug.flags.server:\n            markup.dump(ex)\n        _response_error(\n            response, http.HTTPStatus.INTERNAL_SERVER_ERROR, str(ex), type(ex)\n        )\n    except Exception as ex:\n        if debug.flags.server:\n            markup.dump(ex)\n\n        # XXX Fix this when LSP \"location\" objects are implemented\n        ex_type = errors.InternalServerError\n\n        _response_error(\n            response, http.HTTPStatus.INTERNAL_SERVER_ERROR, str(ex), ex_type\n        )\n\n\ndef _response_error(\n    response: protocol.HttpResponse,\n    status: http.HTTPStatus,\n    message: str,\n    ex_type: type[errors.EdgeDBError],\n) -> None:\n    err_dct = {\n        'message': message,\n        'type': str(ex_type.__name__),\n        'code': ex_type.get_code(),\n    }\n    _response(response, status, json.dumps({'error': err_dct}).encode(), True)\n\n\ndef _response(\n    response: protocol.HttpResponse,\n    status: http.HTTPStatus,\n    message: bytes,\n    close_connection: bool,\n) -> None:\n    response.body = message\n    response.status = status\n    response.content_type = b'application/json'\n    response.close_connection = close_connection\n\n\ndef _response_ok(response: protocol.HttpResponse, message: bytes) -> None:\n    _response(response, http.HTTPStatus.OK, message, False)\n\n\nasync def _ping(\n    response: protocol.HttpResponse, tenant: edbtenant.Tenant\n) -> None:\n    try:\n        async with asyncio.TaskGroup() as tg:\n            ping_backend = tg.create_task(tenant.ping_backend())\n            ping_compiler = tg.create_task(\n                tenant.server.get_compiler_pool().health_check()\n            )\n    except *TimeoutError:\n        if isinstance(ping_backend.exception(), TimeoutError):\n            who = \"the backend\"\n        else:\n            who = \"the compiler pool\"\n        _response_error(\n            response,\n            http.HTTPStatus.SERVICE_UNAVAILABLE,\n            f\"{who} health check timed out\",\n            errors.AvailabilityError,\n        )\n    else:\n        if not ping_backend.result():\n            _response_error(\n                response,\n                http.HTTPStatus.SERVICE_UNAVAILABLE,\n                \"this server is not ready to accept connections\",\n                errors.BackendUnavailableError,\n            )\n        elif not ping_compiler.result():\n            _response_error(\n                response,\n                http.HTTPStatus.SERVICE_UNAVAILABLE,\n                \"The compiler pool is not ready\",\n                errors.AvailabilityError,\n            )\n        else:\n            _response_ok(response, b'\"OK\"')\n\n\nasync def handle_compiler_query(\n    server: edbserver.BaseServer,\n    response: protocol.HttpResponse,\n) -> None:\n    if await server.get_compiler_pool().health_check():\n        _response_ok(response, b'\"OK\"')\n    else:\n        _response_error(\n            response,\n            http.HTTPStatus.SERVICE_UNAVAILABLE,\n            \"The compiler pool is not ready\",\n            errors.AvailabilityError,\n        )\n\n\nasync def handle_liveness_query(\n    request: protocol.HttpRequest,\n    response: protocol.HttpResponse,\n    tenant: edbtenant.Tenant,\n) -> None:\n    await _ping(response, tenant)\n\n\nasync def handle_readiness_query(\n    request: protocol.HttpRequest,\n    response: protocol.HttpResponse,\n    tenant: edbtenant.Tenant,\n) -> None:\n    if not tenant.is_ready():\n        _response_error(\n            response,\n            http.HTTPStatus.SERVICE_UNAVAILABLE,\n            \"this server is not ready to accept connections\",\n            errors.AccessError,\n        )\n    else:\n        await _ping(response, tenant)\n"
  },
  {
    "path": "edb/server/protocol/ui_ext.pyx",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport base64\nimport http\nimport json\nimport urllib.parse\nimport os\nimport mimetypes\n\nimport immutables\n\nfrom edb import buildmeta\nfrom edb import errors\n\nfrom edb.common import debug\nfrom edb.common import markup\n\n\nSTATIC_FILES_DIR = str(buildmeta.get_shared_data_dir_path() / 'ui')\n\nstatic_files = dict()\n\ndef cache_assets():\n    for dirpath, _, filenames in os.walk(STATIC_FILES_DIR):\n        for filename in filenames:\n            fullpath = os.path.join(dirpath, filename)\n\n            mimetype = mimetypes.guess_type(filename)[0]\n            if mimetype is None:\n                mimetype = 'application/octet-stream'\n\n            with open(fullpath, 'rb') as f:\n                static_files[os.path.relpath(fullpath, STATIC_FILES_DIR)] = (\n                    f.read(),\n                    mimetype.encode()\n                )\n\nasync def handle_request(\n    request,\n    response,\n    path_parts,\n    server,\n):\n    try:\n        if path_parts == []:\n            path_parts = ['index.html']\n\n        data, content_type = static_files.get(\n                os.path.join(*path_parts),\n                static_files['index.html']\n            )\n        response.status = http.HTTPStatus.OK\n        response.content_type = content_type\n        response.body = data\n        return\n    except Exception as ex:\n        return handle_error(request, response, ex)\n\n\ndef handle_error(\n    request,\n    response,\n    error\n):\n    if debug.flags.server:\n        markup.dump(error)\n\n    response.body = b'Internal Server Error'\n    response.status = http.HTTPStatus.INTERNAL_SERVER_ERROR\n    response.close_connection = True\n"
  },
  {
    "path": "edb/server/rust_async_channel.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport asyncio\nimport io\nimport logging\n\n\nfrom typing import Protocol, Optional, Any, Callable\n\nlogger = logging.getLogger(\"edb.server\")\n\nMAX_BATCH_SIZE = 16\n\n\nclass RustPipeProtocol(Protocol):\n    def _read(self) -> tuple[Any, ...]: ...\n\n    def _try_read(self) -> Optional[tuple[Any, ...]]: ...\n\n    def _close_pipe(self) -> None: ...\n\n    _fd: int\n\n\nclass RustAsyncChannel:\n    _buffered_reader: io.BufferedReader\n    _skip_reads: int\n    _closed: asyncio.Event\n\n    def __init__(\n        self,\n        pipe: RustPipeProtocol,\n        callback: Callable[[tuple[Any, ...]], None],\n    ) -> None:\n        self._closed = asyncio.Event()\n        fd = pipe._fd\n        self._buffered_reader = io.BufferedReader(\n            io.FileIO(fd), buffer_size=MAX_BATCH_SIZE\n        )\n        self._fd = fd\n        self._pipe = pipe\n        self._callback = callback\n        self._skip_reads = 0\n\n    def __del__(self):\n        if not self._closed.is_set():\n            logger.error(f\"RustAsyncChannel {id(self)} was not closed\")\n\n    async def run(self):\n        loop = asyncio.get_running_loop()\n        loop.add_reader(self._fd, self._channel_read)\n        try:\n            await self._closed.wait()\n        finally:\n            loop.remove_reader(self._fd)\n\n    def close(self):\n        if not self._closed.is_set():\n            self._pipe._close_pipe()\n            self._buffered_reader.close()\n            self._closed.set()\n\n    def read_hint(self):\n        while msg := self._pipe._try_read():\n            self._skip_reads += 1\n            self._callback(msg)\n\n    def _channel_read(self) -> None:\n        try:\n            n = len(self._buffered_reader.read1(MAX_BATCH_SIZE))\n            if n == 0:\n                return\n            if self._skip_reads > n:\n                self._skip_reads -= n\n                return\n            n -= self._skip_reads\n            self._skip_reads = 0\n            for _ in range(n):\n                msg = self._pipe._read()\n                if msg is None:\n                    self.close()\n                    return\n                self._callback(msg)\n        except Exception:\n            logger.error(\n                f\"Error reading from Rust async channel\", exc_info=True\n            )\n            self.close()\n"
  },
  {
    "path": "edb/server/server.py",
    "content": "# mypy: check-untyped-defs\n\n#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nimport re\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n    Hashable,\n    Iterator,\n    Mapping,\n    Sequence,\n    TYPE_CHECKING,\n)\n\nimport asyncio\nimport collections\nimport ipaddress\nimport itertools\nimport json\nimport logging\nimport os\nimport pathlib\nimport pickle\nimport socket\nimport ssl\nimport stat\nimport time\nimport uuid\n\nimport immutables\n\nfrom edb import buildmeta\nfrom edb import errors\n\nfrom edb.common import devmode\nfrom edb.common import lru\nfrom edb.common import secretkey\nfrom edb.common import windowedsum\nfrom edb.common.log import current_tenant\n\nfrom edb.schema import reflection as s_refl\nfrom edb.schema import schema as s_schema\n\nfrom edb.server import auth\nfrom edb.server import args as srvargs\nfrom edb.server import cache\nfrom edb.server import config\nfrom edb.server import compiler_pool\nfrom edb.server import daemon\nfrom edb.server import defines\nfrom edb.server import instdata\nfrom edb.server import protocol\nfrom edb.server import net_worker\nfrom edb.server import tenant as edbtenant\nfrom edb.server.protocol import binary  # type: ignore\nfrom edb.server.protocol import pg_ext  # type: ignore\nfrom edb.server.protocol import ui_ext  # type: ignore\nfrom edb.server.protocol.auth_ext import pkce\nfrom edb.server import metrics\nfrom edb.server import pgcon\n\nfrom edb.pgsql import patches as pg_patches\n\nfrom . import compiler as edbcompiler\nfrom .compiler import sertypes\n\nif TYPE_CHECKING:\n    import asyncio.base_events\n\n    from edb.pgsql import params as pgparams\n\n    from . import bootstrap\n\n\nADMIN_PLACEHOLDER = \"<edgedb:admin>\"\nlogger = logging.getLogger('edb.server')\nlog_metrics = logging.getLogger('edb.server.metrics')\n\n\nclass StartupError(Exception):\n    pass\n\n\nclass BaseServer:\n    _sys_queries: Mapping[str, bytes]\n    _local_intro_query: bytes\n    _global_intro_query: bytes\n    _report_config_typedesc: dict[defines.ProtocolVersion, bytes]\n    _use_monitor_fs: bool\n    _file_watch_handles: list[asyncio.Handle]\n\n    _std_schema: s_schema.Schema\n    _refl_schema: s_schema.Schema\n    _schema_class_layout: s_refl.SchemaClassLayout\n\n    _servers: Mapping[str, asyncio.AbstractServer]\n\n    _testmode: bool\n\n    # We maintain an OrderedDict of all active client connections.\n    # We use an OrderedDict because it allows to move keys to either\n    # end of the dict. That's used to keep all active client connections\n    # grouped at the right end of the dict. The idea is that we can then\n    # have a periodically run coroutine to GC all inactive connections.\n    # This should be more economical than maintaining a TimerHandle for\n    # every open connection. Also, this way, we can react to the\n    # `session_idle_timeout` config setting changed mid-flight.\n    _binary_conns: collections.OrderedDict[binary.EdgeConnection, bool]\n    _pgext_conns: dict[str, pg_ext.PgConnection]\n    _idle_gc_handler: asyncio.TimerHandle | None = None\n    _stmt_cache_size: int | None = None\n\n    _compiler_pool: compiler_pool.AbstractPool | None\n    compilation_config_serializer: sertypes.CompilationConfigSerializer\n    _http_request_logger: asyncio.Task | None\n    _auth_gc: asyncio.Task | None\n    _net_worker_http: asyncio.Task | None\n    _net_worker_http_gc: asyncio.Task | None\n\n    def __init__(\n        self,\n        *,\n        runstate_dir: pathlib.Path,\n        internal_runstate_dir: pathlib.Path,\n        compiler_pool_size: int,\n        compiler_worker_branch_limit,\n        compiler_pool_mode: srvargs.CompilerPoolMode,\n        compiler_pool_addr: tuple[str, int],\n        nethosts: Sequence[str],\n        netport: int,\n        compiler_worker_max_rss: Optional[int] = None,\n        listen_sockets: tuple[socket.socket, ...] = (),\n        testmode: bool = False,\n        daemonized: bool = False,\n        pidfile_dir: Optional[pathlib.Path] = None,\n        binary_endpoint_security: srvargs.ServerEndpointSecurityMode = (\n            srvargs.ServerEndpointSecurityMode.Tls),\n        http_endpoint_security: srvargs.ServerEndpointSecurityMode = (\n            srvargs.ServerEndpointSecurityMode.Tls),\n        auto_shutdown_after: float = -1,\n        echo_runtime_info: bool = False,\n        status_sinks: Sequence[Callable[[str], None]] = (),\n        default_auth_method: srvargs.ServerAuthMethods = (\n            srvargs.DEFAULT_AUTH_METHODS),\n        admin_ui: bool = False,\n        cors_always_allowed_origins: Optional[str] = None,\n        disable_dynamic_system_config: bool = False,\n        compiler_state: edbcompiler.CompilerState,\n        use_monitor_fs: bool = False,\n        net_worker_mode: srvargs.NetWorkerMode = srvargs.NetWorkerMode.Default,\n    ):\n        self.__loop = asyncio.get_running_loop()\n        self._use_monitor_fs = use_monitor_fs\n\n        self._schema_class_layout = compiler_state.schema_class_layout\n        self._config_settings = compiler_state.config_spec\n        self._refl_schema = compiler_state.refl_schema\n        self._std_schema = compiler_state.std_schema\n        assert compiler_state.global_intro_query is not None\n        self._global_intro_query = (\n            compiler_state.global_intro_query.encode(\"utf-8\"))\n        assert compiler_state.local_intro_query is not None\n        self._local_intro_query = (\n            compiler_state.local_intro_query.encode(\"utf-8\"))\n\n        # Used to tag PG notifications to later disambiguate them.\n        self._server_id = str(uuid.uuid4())\n\n        self._daemonized = daemonized\n        self._pidfile_dir = pidfile_dir\n        self._runstate_dir = runstate_dir\n        self._internal_runstate_dir = internal_runstate_dir\n        self._compiler_pool = None\n        self._compiler_pool_size = compiler_pool_size\n        self._compiler_worker_branch_limit = compiler_worker_branch_limit\n        self._compiler_pool_mode = compiler_pool_mode\n        self._compiler_pool_addr = compiler_pool_addr\n        self._compiler_worker_max_rss = compiler_worker_max_rss\n        self._system_compile_cache = lru.LRUMapping(\n            maxsize=defines._MAX_QUERIES_CACHE_SYSTEM\n        )\n        self._system_compile_cache_locks: dict[Any, Any] = {}\n\n        self._listen_sockets = listen_sockets\n        if listen_sockets:\n            nethosts = tuple(s.getsockname()[0] for s in listen_sockets)\n            netport = listen_sockets[0].getsockname()[1]\n\n        self._listen_hosts = nethosts\n        self._listen_port = netport\n\n        # Shutdown the server after the last management\n        # connection has disconnected\n        # and there have been no new connections for n seconds\n        self._auto_shutdown_after = auto_shutdown_after\n        self._auto_shutdown_handler: Any = None\n        self._keepalive_tokens: set = set()\n\n        self._echo_runtime_info = echo_runtime_info\n        self._status_sinks = status_sinks\n\n        self._sys_queries = immutables.Map()\n\n        self._devmode = devmode.is_in_dev_mode()\n        self._testmode = testmode\n\n        self._binary_proto_id_counter = 0\n        self._binary_conns = collections.OrderedDict()\n        self._pgext_conns = {}\n\n        self._servers = {}\n\n        self._http_query_cache = cache.StatementsCache(\n            maxsize=defines.HTTP_PORT_QUERY_CACHE_SIZE)\n\n        self._http_last_minute_requests = windowedsum.WindowedSum()\n        self._http_request_logger = None\n        self._auth_gc = None\n        self._net_worker_http = None\n        self._net_worker_http_gc = None\n        self._net_worker_mode = net_worker_mode\n\n        self._stop_evt = asyncio.Event()\n        self._tls_cert_file: str | Any = None\n        self._tls_cert_newly_generated = False\n        self._sslctx: ssl.SSLContext | Any = None\n        self._sslctx_pgext: ssl.SSLContext | Any = None\n\n        self._jws_key: auth.JWKSet | None = None\n        self._jws_keys_newly_generated = False\n\n        self._default_auth_method_spec = default_auth_method\n        self._default_auth_methods = self._get_auth_method_types(\n            default_auth_method)\n        self._binary_endpoint_security = binary_endpoint_security\n        self._http_endpoint_security = http_endpoint_security\n\n        self._idle_gc_handler = None\n\n        self._admin_ui = admin_ui\n\n        self._cors_always_allowed_origins = [\n            re.compile(\n                '^' + origin\n                    .replace('.', '\\\\.')\n                    .replace('*', '.*') + '$'\n            ) if '*' in origin else origin\n            for origin in cors_always_allowed_origins.split(',')\n        ] if cors_always_allowed_origins else []\n\n        self._file_watch_handles = []\n        self._tls_certs_reload_retry_handle: Any | asyncio.TimerHandle = None\n\n        self._disable_dynamic_system_config = disable_dynamic_system_config\n        self._report_config_typedesc = {}\n\n    def _get_auth_method_types(\n        self,\n        auth_methods_spec: srvargs.ServerAuthMethods,\n    ) -> dict[srvargs.ServerConnTransport, list[config.CompositeConfigType]]:\n        mapping = {}\n        for transport, methods in auth_methods_spec.items():\n            result = []\n            for method in methods:\n                auth_type = self.config_settings.get_type_by_name(\n                    f'cfg::{method.value}'\n                )\n                result.append(auth_type())\n            mapping[transport] = result\n\n        return mapping\n\n    async def _request_stats_logger(self):\n        last_seen = -1\n        while True:\n            current = int(self._http_last_minute_requests)\n            if current != last_seen:\n                log_metrics.info(\n                    \"HTTP requests in last minute: %d\",\n                    current,\n                )\n                last_seen = current\n\n            await asyncio.sleep(30)\n\n    def get_server_id(self):\n        return self._server_id\n\n    def get_listen_hosts(self):\n        return self._listen_hosts\n\n    def get_listen_port(self):\n        return self._listen_port\n\n    def get_loop(self):\n        return self.__loop\n\n    def in_dev_mode(self):\n        return self._devmode\n\n    def in_test_mode(self):\n        return self._testmode\n\n    def is_admin_ui_enabled(self):\n        return self._admin_ui\n\n    def get_cors_always_allowed_origins(self):\n        return self._cors_always_allowed_origins\n\n    def on_binary_client_created(self) -> str:\n        self._binary_proto_id_counter += 1\n        return str(self._binary_proto_id_counter)\n\n    def on_binary_client_connected(self, conn):\n        self._binary_conns[conn] = True\n        metrics.current_client_connections.inc(\n            1.0, conn.get_tenant_label()\n        )\n\n    def on_binary_client_authed(self, conn):\n        self._report_connections(event='opened')\n        metrics.total_client_connections.inc(\n            1.0, conn.get_tenant_label()\n        )\n\n    def on_binary_client_after_idling(self, conn):\n        try:\n            self._binary_conns.move_to_end(conn, last=True)\n        except KeyError:\n            # Shouldn't happen, but just in case some weird async twist\n            # gets us here we don't want to crash the connection with\n            # this error.\n            metrics.background_errors.inc(\n                1.0, conn.get_tenant_label(), 'client_after_idling'\n            )\n\n    def on_binary_client_disconnected(self, conn):\n        self._binary_conns.pop(conn, None)\n        self._report_connections(event=\"closed\")\n        metrics.current_client_connections.dec(\n            1.0, conn.get_tenant_label()\n        )\n        self.maybe_auto_shutdown()\n\n    def maybe_delay_auto_shutdown(self):\n        if self._auto_shutdown_handler:\n            self._auto_shutdown_handler.cancel()\n            self._auto_shutdown_handler = None\n\n    def maybe_auto_shutdown(self):\n        if (\n            not self._binary_conns\n            and not self._keepalive_tokens\n            and self._auto_shutdown_after >= 0\n            and self._auto_shutdown_handler is None\n        ):\n            self._auto_shutdown_handler = self.__loop.call_later(\n                self._auto_shutdown_after, self.request_auto_shutdown)\n\n    def _report_connections(self, *, event: str) -> None:\n        log_metrics.info(\n            \"%s a connection; open_count=%d\",\n            event,\n            len(self._binary_conns),\n        )\n\n    def add_keepalive_token(self, token: Hashable) -> None:\n        self.maybe_delay_auto_shutdown()\n        self._keepalive_tokens.add(token)\n\n    def remove_keepalive_token(self, token: Hashable) -> None:\n        self._keepalive_tokens.discard(token)\n        self.maybe_auto_shutdown()\n\n    def on_pgext_client_connected(self, conn):\n        self._pgext_conns[conn.get_id()] = conn\n\n    def on_pgext_client_disconnected(self, conn):\n        self._pgext_conns.pop(conn.get_id(), None)\n        self.maybe_auto_shutdown()\n\n    def cancel_pgext_connection(self, pid, secret):\n        conn = self._pgext_conns.get(pid)\n        if conn is not None:\n            conn.cancel(secret)\n\n    def monitor_fs(\n        self,\n        file_path: str | pathlib.Path,\n        cb: Callable[[], None],\n    ) -> Callable[[], None]:\n        if not self._use_monitor_fs:\n            return lambda: None\n\n        if isinstance(file_path, str):\n            path = pathlib.Path(file_path)\n            path_str = file_path\n        else:\n            path = file_path\n            path_str = str(file_path)\n        handle = None\n        parent_dir = path.parent\n\n        def watch_dir(file_modified, _event):\n            nonlocal handle\n            if parent_dir / os.fsdecode(file_modified) == path:\n                try:\n                    new_handle = self.__loop._monitor_fs(  # type: ignore\n                        path_str, callback)\n                except FileNotFoundError:\n                    pass\n                else:\n                    finalizer()\n                    handle = new_handle\n                    self._file_watch_handles.append(handle)\n                    cb()\n\n        def callback(_file_modified, _event):\n            nonlocal handle\n            # First, cancel the existing watcher and call cb() regardless of\n            # what event it is. This is because macOS issues RENAME while Linux\n            # issues CHANGE, and we don't have enough knowledge about renaming.\n            # The idea here is to re-watch the file path after every event, so\n            # that even if the file is recreated, we still watch the right one.\n            finalizer()\n            try:\n                cb()\n            finally:\n                try:\n                    # Then, see if we can directly re-watch the target path\n                    handle = self.__loop._monitor_fs(  # type: ignore\n                        path_str, callback)\n                except FileNotFoundError:\n                    # If not, watch the parent directory to wait for recreation\n                    handle = self.__loop._monitor_fs(  # type: ignore\n                        str(parent_dir), watch_dir)\n                self._file_watch_handles.append(handle)\n\n        # ... we depend on an event loop internal _monitor_fs\n        handle = self.__loop._monitor_fs(path_str, callback)  # type: ignore\n\n        def finalizer():\n            try:\n                self._file_watch_handles.remove(handle)\n            except ValueError:\n                # The server may have cleared _file_watch_handles before the\n                # tenants do, so we can skip the double cancel here.\n                pass\n            else:\n                handle.cancel()\n\n        self._file_watch_handles.append(handle)\n\n        return finalizer\n\n    def _get_sys_config(self) -> Mapping[str, config.SettingValue]:\n        raise NotImplementedError\n\n    def config_lookup(\n        self,\n        name: str,\n        *configs: Mapping[str, config.SettingValue],\n    ) -> Any:\n        return config.lookup(name, *configs, spec=self._config_settings)\n\n    @property\n    def config_settings(self) -> config.Spec:\n        return self._config_settings\n\n    async def init(self):\n        if self.is_admin_ui_enabled():\n            ui_ext.cache_assets()\n\n        sys_config = self._get_sys_config()\n        if not self._listen_hosts:\n            self._listen_hosts = (\n                self.config_lookup('listen_addresses', sys_config)\n                or ('localhost',)\n            )\n\n        if self._listen_port is None:\n            self._listen_port = (\n                self.config_lookup('listen_port', sys_config)\n                or defines.EDGEDB_PORT\n            )\n\n        self._stmt_cache_size = self.config_lookup(\n            '_pg_prepared_statement_cache_size', sys_config\n        )\n\n        self.reinit_idle_gc_collector()\n\n    def reinit_idle_gc_collector(self) -> float:\n        if self._auto_shutdown_after >= 0:\n            return -1\n\n        if self._idle_gc_handler is not None:\n            self._idle_gc_handler.cancel()\n            self._idle_gc_handler = None\n\n        session_idle_timeout = self.config_lookup(\n            'session_idle_timeout', self._get_sys_config())\n\n        timeout = session_idle_timeout.to_microseconds()\n        timeout /= 1_000_000.0  # convert to seconds\n\n        if timeout > 0:\n            self._idle_gc_handler = self.__loop.call_later(\n                timeout, self._idle_gc_collector)\n\n        return timeout\n\n    @property\n    def stmt_cache_size(self) -> int | None:\n        return self._stmt_cache_size\n\n    @property\n    def system_compile_cache(self):\n        return self._system_compile_cache\n\n    def request_stop_fe_conns(self, dbname: str) -> None:\n        for conn in itertools.chain(\n            self._binary_conns.keys(), self._pgext_conns.values()\n        ):\n            if conn.dbname == dbname:\n                conn.request_stop()\n\n    @property\n    def system_compile_cache_locks(self):\n        return self._system_compile_cache_locks\n\n    def _idle_gc_collector(self):\n        try:\n            self._idle_gc_handler = None\n            idle_timeout = self.reinit_idle_gc_collector()\n\n            if idle_timeout <= 0:\n                return\n\n            now = time.monotonic()\n            expiry_time = now - idle_timeout\n            for conn in self._binary_conns:\n                try:\n                    if conn.is_idle(expiry_time):\n                        label = conn.get_tenant_label()\n                        metrics.idle_client_connections.inc(1.0, label)\n                        current_tenant.set(label)\n                        conn.close_for_idling()\n                    elif conn.is_alive():\n                        # We are sorting connections in\n                        # 'on_binary_client_after_idling' to specifically\n                        # enable this optimization. As soon as we find first\n                        # non-idle active connection we're guaranteed\n                        # to have traversed all of the potentially idling\n                        # connections.\n                        break\n                except Exception:\n                    metrics.background_errors.inc(\n                        1.0, conn.get_tenant_label(), 'close_for_idling'\n                    )\n                    conn.abort()\n        except Exception:\n            metrics.background_errors.inc(\n                1.0, 'system', 'idle_clients_collector'\n            )\n            raise\n\n    def _get_backend_runtime_params(self) -> pgparams.BackendRuntimeParams:\n        raise NotImplementedError\n\n    def _get_compiler_args(self) -> dict[str, Any]:\n        # Force Postgres version in BackendRuntimeParams to be the\n        # minimal supported, because the compiler does not rely on\n        # the version, and not pinning it would make the remote compiler\n        # pool refuse connections from clients that have differing versions\n        # of Postgres backing them.\n        runtime_params = self._get_backend_runtime_params()\n        min_ver = '.'.join(str(v) for v in defines.MIN_POSTGRES_VERSION)\n        runtime_params = runtime_params._replace(\n            instance_params=runtime_params.instance_params._replace(\n                version=buildmeta.parse_pg_version(min_ver),\n            ),\n        )\n\n        args = dict(\n            pool_size=self._compiler_pool_size,\n            worker_branch_limit=self._compiler_worker_branch_limit,\n            pool_class=self._compiler_pool_mode.pool_class,\n            runstate_dir=self._internal_runstate_dir,\n            backend_runtime_params=runtime_params,\n            std_schema=self._std_schema,\n            refl_schema=self._refl_schema,\n            schema_class_layout=self._schema_class_layout,\n        )\n        if self._compiler_pool_mode == srvargs.CompilerPoolMode.Remote:\n            args['address'] = self._compiler_pool_addr\n        else:\n            if self._compiler_worker_max_rss is not None:\n                args['worker_max_rss'] = self._compiler_worker_max_rss\n        return args\n\n    async def _destroy_compiler_pool(self):\n        if self._compiler_pool is not None:\n            await self._compiler_pool.stop()\n            self._compiler_pool = None\n\n    def get_compiler_pool(self):\n        return self._compiler_pool\n\n    async def introspect_global_schema_json(\n        self, conn: pgcon.PGConnection\n    ) -> bytes:\n        return await conn.sql_fetch_val(self._global_intro_query)\n\n    def _parse_global_schema(\n        self, json_data: Any\n    ) -> s_schema.Schema:\n        return s_refl.parse_schema(\n            base_schema=self._std_schema,\n            data=json_data,\n            schema_class_layout=self._schema_class_layout,\n        )\n\n    async def introspect_global_schema(\n        self, conn: pgcon.PGConnection\n    ) -> s_schema.Schema:\n        json_data = await self.introspect_global_schema_json(conn)\n        return self._parse_global_schema(json_data)\n\n    async def introspect_user_schema_json(\n        self,\n        conn: pgcon.PGConnection,\n    ) -> bytes:\n        return await conn.sql_fetch_val(self._local_intro_query)\n\n    def _parse_user_schema(\n        self,\n        json_data: Any,\n        global_schema: s_schema.Schema,\n    ) -> s_schema.Schema:\n        base_schema = s_schema.ChainedSchema(\n            self._std_schema,\n            s_schema.EMPTY_SCHEMA,\n            global_schema,\n        )\n\n        return s_refl.parse_schema(\n            base_schema=base_schema,\n            data=json_data,\n            schema_class_layout=self._schema_class_layout,\n        )\n\n    async def _introspect_user_schema(\n        self,\n        conn: pgcon.PGConnection,\n        global_schema: s_schema.Schema,\n    ) -> s_schema.Schema:\n        json_data = await self.introspect_user_schema_json(conn)\n        return self._parse_user_schema(json_data, global_schema)\n\n    async def introspect_db_config(self, conn: pgcon.PGConnection) -> bytes:\n        return await conn.sql_fetch_val(self.get_sys_query(\"dbconfig\"))\n\n    def _parse_db_config(\n        self, db_config_json: bytes, user_schema: s_schema.Schema\n    ) -> Mapping[str, config.SettingValue]:\n        spec = config.ChainedSpec(\n            self._config_settings,\n            config.load_ext_spec_from_schema(\n                user_schema,\n                self.get_std_schema(),\n            ),\n        )\n\n        return config.from_json(spec, db_config_json)\n\n    async def get_dbnames(self, syscon):\n        dbs_query = self.get_sys_query('listdbs')\n        json_data = await syscon.sql_fetch_val(dbs_query)\n        return json.loads(json_data)\n\n    async def _on_system_config_add(self, setting_name, value):\n        # CONFIGURE INSTANCE INSERT ConfigObject;\n        pass\n\n    async def _on_system_config_rem(self, setting_name, value):\n        # CONFIGURE INSTANCE RESET ConfigObject;\n        pass\n\n    async def _on_system_config_set(self, setting_name, value):\n        # CONFIGURE INSTANCE SET setting_name := value;\n        pass\n\n    async def _on_system_config_reset(self, setting_name):\n        # CONFIGURE INSTANCE RESET setting_name;\n        pass\n\n    def before_alter_system_config(self):\n        if self._disable_dynamic_system_config:\n            raise errors.ConfigurationError(\n                \"cannot change this configuration value in this instance\"\n            )\n\n    async def _after_system_config_add(self, setting_name, value):\n        # CONFIGURE INSTANCE INSERT ConfigObject;\n        pass\n\n    async def _after_system_config_rem(self, setting_name, value):\n        # CONFIGURE INSTANCE RESET ConfigObject;\n        pass\n\n    async def _after_system_config_set(self, setting_name, value):\n        # CONFIGURE INSTANCE SET setting_name := value;\n        pass\n\n    async def _after_system_config_reset(self, setting_name):\n        # CONFIGURE INSTANCE RESET setting_name;\n        pass\n\n    def _make_protocol(self):\n        self.maybe_delay_auto_shutdown()\n        return protocol.HttpProtocol(\n            self,\n            self._sslctx,\n            self._sslctx_pgext,\n            binary_endpoint_security=self._binary_endpoint_security,\n            http_endpoint_security=self._http_endpoint_security,\n        )\n\n    async def _start_server(\n        self,\n        host: str,\n        port: int,\n        sock: Optional[socket.socket] = None,\n    ) -> Optional[asyncio.base_events.Server]:\n        try:\n            kwargs: dict[str, Any]\n            if sock is not None:\n                kwargs = {\"sock\": sock}\n            else:\n                kwargs = {\"host\": host, \"port\": port}\n            return await self.__loop.create_server(\n                self._make_protocol, **kwargs\n            )\n        except Exception as e:\n            logger.warning(\n                f\"could not create listen socket for '{host}:{port}': {e}\"\n            )\n            return None\n\n    async def _start_admin_server(\n        self,\n        port: int,\n    ) -> asyncio.base_events.Server:\n        admin_unix_sock_path = os.path.join(\n            self._runstate_dir, f'.s.GEL.admin.{port}')\n        symlink = os.path.join(\n            self._runstate_dir, f'.s.EDGEDB.admin.{port}')\n\n        exists = False\n        try:\n            mode = os.lstat(symlink).st_mode\n            if stat.S_ISSOCK(mode):\n                os.unlink(symlink)\n            else:\n                exists = True\n        except FileNotFoundError:\n            pass\n        if not exists:\n            os.symlink(admin_unix_sock_path, symlink)\n\n        assert len(admin_unix_sock_path) <= (\n            defines.MAX_RUNSTATE_DIR_PATH\n            + defines.MAX_UNIX_SOCKET_PATH_LENGTH\n            + 1\n        ), \"admin Unix socket length exceeds maximum allowed\"\n        admin_unix_srv = await self.__loop.create_unix_server(\n            lambda: binary.new_edge_connection(\n                self, self._get_admin_tenant(), external_auth=True\n            ),\n            admin_unix_sock_path\n        )\n        os.chmod(admin_unix_sock_path, stat.S_IRUSR | stat.S_IWUSR)\n        logger.info('Serving admin on %s', admin_unix_sock_path)\n        return admin_unix_srv\n\n    def _get_admin_tenant(self) -> edbtenant.Tenant:\n        return self.get_default_tenant()\n\n    async def _start_servers(\n        self,\n        hosts: tuple[str, ...],\n        port: int,\n        *,\n        admin: bool = True,\n        sockets: tuple[socket.socket, ...] = (),\n    ):\n        servers = {}\n        if port == 0:\n            # Automatic port selection requires us to start servers\n            # sequentially until we get a working bound socket to ensure\n            # consistent port value across all requested listen addresses.\n            try:\n                for host in hosts:\n                    server = await self._start_server(host, port)\n                    if server is not None:\n                        if port == 0:\n                            port = server.sockets[0].getsockname()[1]\n                        servers[host] = server\n            except Exception:\n                await self._stop_servers(servers.values())\n                raise\n        else:\n            start_tasks = {}\n            try:\n                async with asyncio.TaskGroup() as g:\n                    if sockets:\n                        for host, sock in zip(hosts, sockets):\n                            start_tasks[host] = g.create_task(\n                                self._start_server(host, port, sock=sock)\n                            )\n                    else:\n                        for host in hosts:\n                            start_tasks[host] = g.create_task(\n                                self._start_server(host, port)\n                            )\n            except Exception:\n                await self._stop_servers([\n                    fut.result() for fut in start_tasks.values()\n                    if (\n                        fut.done()\n                        and fut.exception() is None\n                        and fut.result() is not None\n                    )\n                ])\n                raise\n\n            servers.update({\n                host: srv\n                for host, fut in start_tasks.items()\n                if (srv := fut.result()) is not None\n            })\n\n        # Fail if none of the servers can be started, except when the admin\n        # server on a UNIX domain socket will be started.\n        if not servers and (not admin or port == 0):\n            raise StartupError(\"could not create any listen sockets\")\n\n        addrs = []\n        for tcp_srv in servers.values():\n            for s in tcp_srv.sockets:\n                addrs.append(s.getsockname())\n\n        if len(addrs) > 1:\n            if port:\n                addr_str = f\"{{{', '.join(addr[0] for addr in addrs)}}}:{port}\"\n            else:\n                addr_str = f\"\"\"{{{', '.join(\n                    f'{addr[0]}:{addr[1]}' for addr in addrs)}}}\"\"\"\n        elif addrs:\n            addr_str = f'{addrs[0][0]}:{addrs[0][1]}'\n            port = addrs[0][1]\n        else:\n            addr_str = None\n\n        if addr_str:\n            logger.info('Serving on %s', addr_str)\n\n        if admin and port:\n            try:\n                admin_unix_srv = await self._start_admin_server(port)\n            except Exception:\n                await self._stop_servers(servers.values())\n                raise\n            servers[ADMIN_PLACEHOLDER] = admin_unix_srv\n\n        return servers, port, addrs\n\n    def _sni_callback(self, sslobj, server_name, sslctx):\n        # Match the given SNI for a pre-registered Tenant instance,\n        # and temporarily store in memory indexed by sslobj for future\n        # retrieval, see also retrieve_tenant() below.\n        #\n        # Used in multi-tenant server only. This method must not fail.\n        pass\n\n    def reload_tls(self, tls_cert_file, tls_key_file, client_ca_file):\n        logger.info(\"loading TLS certificates\")\n        tls_password_needed = False\n        if self._tls_certs_reload_retry_handle is not None:\n            self._tls_certs_reload_retry_handle.cancel()\n            self._tls_certs_reload_retry_handle = None\n\n        def _tls_private_key_password():\n            nonlocal tls_password_needed\n            tls_password_needed = True\n            return (\n                os.environ.get('GEL_SERVER_TLS_PRIVATE_KEY_PASSWORD', '')\n                or os.environ.get('EDGEDB_SERVER_TLS_PRIVATE_KEY_PASSWORD', '')\n            )\n\n        sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)\n        sslctx_pgext = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)\n        try:\n            sslctx.load_cert_chain(\n                tls_cert_file,\n                tls_key_file,\n                password=_tls_private_key_password,\n            )\n            sslctx_pgext.load_cert_chain(\n                tls_cert_file,\n                tls_key_file,\n                password=_tls_private_key_password,\n            )\n        except ssl.SSLError as e:\n            if e.library == \"SSL\" and e.errno == 9:  # ERR_LIB_PEM\n                if tls_password_needed:\n                    if _tls_private_key_password():\n                        raise StartupError(\n                            \"Cannot load TLS certificates - it's likely that \"\n                            \"the private key password is wrong.\"\n                        ) from e\n                    else:\n                        raise StartupError(\n                            \"Cannot load TLS certificates - the private key \"\n                            \"file is likely protected by a password. Specify \"\n                            \"the password using environment variable: \"\n                            \"GEL_SERVER_TLS_PRIVATE_KEY_PASSWORD\"\n                        ) from e\n                elif tls_key_file is None:\n                    raise StartupError(\n                        \"Cannot load TLS certificates - have you specified \"\n                        \"the private key file using the `--tls-key-file` \"\n                        \"command-line argument?\"\n                    ) from e\n                else:\n                    raise StartupError(\n                        \"Cannot load TLS certificates - please double check \"\n                        \"if the specified certificate files are valid.\"\n                    )\n            elif e.library == \"X509\" and e.errno == 116:\n                # X509 Error 116: X509_R_KEY_VALUES_MISMATCH\n                raise StartupError(\n                    \"Cannot load TLS certificates - the private key doesn't \"\n                    \"match the certificate.\"\n                )\n\n            raise StartupError(f\"Cannot load TLS certificates - {e}\") from e\n\n        if client_ca_file is not None:\n            try:\n                sslctx.load_verify_locations(client_ca_file)\n                sslctx_pgext.load_verify_locations(client_ca_file)\n            except ssl.SSLError as e:\n                raise StartupError(\n                    f\"Cannot load client CA certificates - {e}\") from e\n            sslctx.verify_mode = ssl.CERT_OPTIONAL\n            sslctx_pgext.verify_mode = ssl.CERT_OPTIONAL\n\n        sslctx.set_alpn_protocols(['edgedb-binary', 'http/1.1'])\n        sslctx.sni_callback = self._sni_callback\n        sslctx_pgext.sni_callback = self._sni_callback\n        self._sslctx = sslctx\n        self._sslctx_pgext = sslctx_pgext\n\n    def init_tls(\n        self,\n        tls_cert_file,\n        tls_key_file,\n        tls_cert_newly_generated,\n        client_ca_file,\n    ):\n        assert self._sslctx is self._sslctx_pgext is None\n        self.reload_tls(tls_cert_file, tls_key_file, client_ca_file)\n\n        self._tls_cert_file = str(tls_cert_file)\n        self._tls_cert_newly_generated = tls_cert_newly_generated\n\n        def reload_tls(retry=0):\n            try:\n                self.reload_tls(tls_cert_file, tls_key_file, client_ca_file)\n            except (StartupError, FileNotFoundError) as e:\n                if retry > defines._TLS_CERT_RELOAD_MAX_RETRIES:\n                    logger.critical(str(e))\n                    self.request_shutdown()\n                else:\n                    delay = defines._TLS_CERT_RELOAD_EXP_INTERVAL * 2 ** retry\n                    logger.warning(\"%s; retrying in %.1f seconds.\", e, delay)\n                    self._tls_certs_reload_retry_handle = (\n                        self.__loop.call_later(\n                            delay,\n                            reload_tls,\n                            retry + 1,\n                        )\n                    )\n            except Exception:\n                logger.critical(\n                    \"error while reloading TLS certificate and/or key, \"\n                    \"shutting down.\",\n                    exc_info=True,\n                )\n                self.request_shutdown()\n\n        self.monitor_fs(tls_cert_file, reload_tls)\n        if tls_cert_file != tls_key_file:\n            self.monitor_fs(tls_key_file, reload_tls)\n        if client_ca_file is not None:\n            self.monitor_fs(client_ca_file, reload_tls)\n\n    def start_watching_files(self):\n        # TODO(fantix): include the monitor_fs() lines above\n        pass\n\n    def load_jwcrypto(self, jws_key_file: pathlib.Path) -> auth.JWKSet:\n        try:\n            jws_key = auth.load_secret_key(jws_key_file)\n            self._jws_key = jws_key\n            return jws_key\n        except auth.SecretKeyReadError as e:\n            raise StartupError(e.args[0]) from e\n\n    def init_jwcrypto(\n        self,\n        jws_key_file: pathlib.Path,\n        jws_keys_newly_generated: bool,\n    ) -> None:\n        self.load_jwcrypto(jws_key_file)\n        self._jws_keys_newly_generated = jws_keys_newly_generated\n\n    def get_jws_key(self) -> auth.JWKSet | None:\n        return self._jws_key\n\n    async def _stop_servers(self, servers):\n        async with asyncio.TaskGroup() as g:\n            for srv in servers:\n                srv.close()\n                g.create_task(srv.wait_closed())\n\n    async def _before_start_servers(self) -> None:\n        pass\n\n    async def _after_start_servers(self) -> None:\n        pass\n\n    async def start(self):\n        self._stop_evt.clear()\n\n        self._http_request_logger = self.__loop.create_task(\n            self._request_stats_logger()\n        )\n\n        pool = await compiler_pool.create_compiler_pool(\n            **self._get_compiler_args()\n        )\n        self.compilation_config_serializer = (\n            await pool.make_compilation_config_serializer()\n        )\n        self._compiler_pool = pool\n\n        await self._before_start_servers()\n        self._servers, actual_port, listen_addrs = await self._start_servers(\n            tuple((await _resolve_interfaces(self._listen_hosts))[0]),\n            self._listen_port,\n            sockets=self._listen_sockets,\n        )\n        self._listen_hosts = [addr[0] for addr in listen_addrs]\n        self._listen_port = actual_port\n\n        if self._daemonized:\n            pidfile_dir = self._pidfile_dir\n            if pidfile_dir is None:\n                pidfile_dir = self._runstate_dir\n            pidfile_path = pidfile_dir / f\".s.EDGEDB.{actual_port}.lock\"\n            pidfile = daemon.PidFile(pidfile_path)\n            pidfile.acquire()\n\n        await self._after_start_servers()\n        self._auth_gc = self.__loop.create_task(pkce.gc(self))\n        if self._net_worker_mode is srvargs.NetWorkerMode.Default:\n            self._net_worker_http = self.__loop.create_task(\n                net_worker.http(self)\n            )\n            self._net_worker_http_gc = self.__loop.create_task(\n                net_worker.gc(self)\n            )\n\n        if self._echo_runtime_info:\n            ri = {\n                \"port\": self._listen_port,\n                \"runstate_dir\": str(self._runstate_dir),\n                \"tls_cert_file\": self._tls_cert_file,\n            }\n            print(f'\\nEDGEDB_SERVER_DATA:{json.dumps(ri)}\\n', flush=True)\n\n        status = self._get_status()\n        status[\"listen_addrs\"] = listen_addrs\n        status_str = f'READY={json.dumps(status)}'\n        for status_sink in self._status_sinks:\n            status_sink(status_str)\n\n        if self._auto_shutdown_after > 0:\n            self._auto_shutdown_handler = self.__loop.call_later(\n                self._auto_shutdown_after, self.request_auto_shutdown)\n\n    def _get_status(self) -> dict[str, Any]:\n        return {\n            \"port\": self._listen_port,\n            \"socket_dir\": str(self._runstate_dir),\n            \"main_pid\": os.getpid(),\n            \"tls_cert_file\": self._tls_cert_file,\n            \"tls_cert_newly_generated\": self._tls_cert_newly_generated,\n            \"jws_keys_newly_generated\": self._jws_keys_newly_generated,\n        }\n\n    def request_auto_shutdown(self):\n        if self._auto_shutdown_after == 0:\n            logger.info(\"shutting down server: all clients disconnected\")\n        else:\n            logger.info(\n                f\"shutting down server: no clients connected in last\"\n                f\" {self._auto_shutdown_after} seconds\"\n            )\n        self.request_shutdown()\n\n    def request_shutdown(self):\n        self._stop_evt.set()\n\n    async def stop(self):\n        if self._idle_gc_handler is not None:\n            self._idle_gc_handler.cancel()\n            self._idle_gc_handler = None\n\n        if self._http_request_logger is not None:\n            self._http_request_logger.cancel()\n        if self._auth_gc is not None:\n            self._auth_gc.cancel()\n        if self._net_worker_http is not None:\n            self._net_worker_http.cancel()\n        if self._net_worker_http_gc is not None:\n            self._net_worker_http_gc.cancel()\n\n        for handle in self._file_watch_handles:\n            handle.cancel()\n        self._file_watch_handles.clear()\n\n        await self._stop_servers(self._servers.values())\n        self._servers = {}\n\n        # This should be done by tenant.stop(), but let's still do it again\n        for conn in self._binary_conns:\n            conn.request_stop()\n        self._binary_conns.clear()\n\n        for conn in self._pgext_conns.values():\n            conn.request_stop()\n        self._pgext_conns.clear()\n\n    def request_frontend_stop(self, tenant: edbtenant.Tenant):\n        dropped = []\n        for conn in self._binary_conns:\n            if conn.tenant is tenant:\n                conn.request_stop()\n                dropped.append(conn)\n        for conn in dropped:\n            self._binary_conns.pop(conn, None)\n\n        dropped.clear()\n        for conn in self._pgext_conns.values():\n            if conn.tenant is tenant:\n                conn.request_stop()\n                dropped.append(conn)\n        for conn in dropped:\n            self._pgext_conns.pop(conn, None)\n\n    async def serve_forever(self):\n        await self._stop_evt.wait()\n\n    def get_sys_query(self, key):\n        return self._sys_queries[key]\n\n    def get_debug_info(self):\n        \"\"\"Used to render the /server-info endpoint in dev/test modes.\n\n        Some tests depend on the exact layout of the returned structure.\n        \"\"\"\n\n        return dict(\n            params=dict(\n                dev_mode=self._devmode,\n                test_mode=self._testmode,\n                default_auth_methods=str(self._default_auth_method_spec),\n                listen_hosts=self._listen_hosts,\n                listen_port=self._listen_port,\n            ),\n            instance_config=config.debug_serialize_config(\n                self._get_sys_config()),\n            compiler_pool=(\n                self._compiler_pool.get_debug_info()\n                if self._compiler_pool\n                else None\n            ),\n        )\n\n    def get_report_config_typedesc(\n        self,\n    ) -> dict[defines.ProtocolVersion, bytes]:\n        return self._report_config_typedesc\n\n    def get_default_auth_methods(\n        self, transport: srvargs.ServerConnTransport\n    ) -> list[config.CompositeConfigType]:\n        return self._default_auth_methods.get(transport, [])\n\n    def get_std_schema(self) -> s_schema.Schema:\n        return self._std_schema\n\n    def retrieve_tenant(self, sslobj) -> edbtenant.Tenant | None:\n        # After TLS handshake, the client connection would use this method to\n        # retrieve the Tenant instance associated with the given SSLObject.\n        #\n        # This method must not fail. See also _sni_callback() above.\n        return self.get_default_tenant()\n\n    def get_default_tenant(self) -> edbtenant.Tenant:\n        # The client connection must proceed on a Tenant instance. In cases:\n        #   1. plain-text connection without TLS handshake\n        #   2. TLS handshake didn't provide SNI\n        #   3. SNI didn't match any Tenant (retrieve_tenant() returned None)\n        # this method will be called for a \"default\" tenant to use.\n        #\n        # The caller must be ready to handle errors raised in this method, and\n        # provide a decent error.\n        raise NotImplementedError\n\n    def iter_tenants(self) -> Iterator[edbtenant.Tenant]:\n        raise NotImplementedError\n\n    async def maybe_generate_pki(\n        self, args: srvargs.ServerConfig, ss: BaseServer\n    ) -> tuple[bool, bool]:\n        tls_cert_newly_generated = False\n        if args.tls_cert_mode is srvargs.ServerTlsCertMode.SelfSigned:\n            assert args.tls_cert_file is not None\n            if not args.tls_cert_file.exists():\n                assert args.tls_key_file is not None\n                logger.info(\n                    f'generating self-signed TLS certificate '\n                    f'in \"{args.tls_cert_file}\"'\n                )\n                secretkey.generate_tls_cert(\n                    args.tls_cert_file,\n                    args.tls_key_file,\n                    ss.get_listen_hosts(),\n                )\n                tls_cert_newly_generated = True\n        jws_keys_newly_generated = False\n        if args.jose_key_mode is srvargs.JOSEKeyMode.Generate:\n            assert args.jws_key_file is not None\n            if not args.jws_key_file.exists():\n                logger.info(\n                    f'generating JOSE key pair in \"{args.jws_key_file}\"'\n                )\n                auth.generate_jwk(args.jws_key_file)\n                jws_keys_newly_generated = True\n        return tls_cert_newly_generated, jws_keys_newly_generated\n\n\nclass Server(BaseServer):\n    _tenant: edbtenant.Tenant\n    _startup_script: srvargs.StartupScript | None\n    _new_instance: bool\n\n    def __init__(\n        self,\n        *,\n        tenant: edbtenant.Tenant,\n        startup_script: srvargs.StartupScript | None = None,\n        new_instance: bool,\n        **kwargs,\n    ):\n        super().__init__(**kwargs)\n        self._tenant = tenant\n        self._startup_script = startup_script\n        self._new_instance = new_instance\n\n        tenant.set_server(self)\n\n    def _get_sys_config(self) -> Mapping[str, config.SettingValue]:\n        return self._tenant.get_sys_config()\n\n    async def init(self) -> None:\n        logger.debug(\"starting server init\")\n        await self._tenant.init_sys_pgcon()\n        await self._load_instance_data()\n        await self._maybe_patch()\n        await self._tenant.init()\n        await super().init()\n\n    def get_default_tenant(self) -> edbtenant.Tenant:\n        return self._tenant\n\n    def iter_tenants(self) -> Iterator[edbtenant.Tenant]:\n        yield self._tenant\n\n    async def _get_patch_log(\n        self, conn: pgcon.PGConnection, idx: int\n    ) -> Optional[bootstrap.PatchEntry]:\n        # We need to maintain a log in the system database of\n        # patches that have been applied. This is so that if a\n        # patch creates a new object, and then we succesfully\n        # apply the patch to a user db but crash *before* applying\n        # it to the system db, when we start up again and try\n        # applying it to the system db, it is important that we\n        # apply the same compiled version of the patch. If we\n        # instead recompiled it, and it created new objects, those\n        # objects might have a different id in the std schema and\n        # in the actual user db.\n        result = await instdata.get_instdata(\n            conn, f'patch_log_{idx}', 'bin')\n        if result:\n            return pickle.loads(result)\n        else:\n            return None\n\n    async def _prepare_patches(\n        self, conn: pgcon.PGConnection\n    ) -> dict[int, bootstrap.PatchEntry]:\n        \"\"\"Prepare all the patches\"\"\"\n        num_patches = await self._tenant.get_patch_count(conn)\n\n        if num_patches < len(pg_patches.PATCHES):\n            logger.info(\"preparing patches for database upgrade\")\n\n        patches = {}\n        patch_list = list(enumerate(pg_patches.PATCHES))\n        for num, (kind, patch) in patch_list[num_patches:]:\n            from . import bootstrap  # noqa: F402\n\n            idx = num_patches + num\n            if not (entry := await self._get_patch_log(conn, idx)):\n                patch_info = await bootstrap.gather_patch_info(\n                    num, kind, patch, conn\n                )\n\n                entry = bootstrap.prepare_patch(\n                    num, kind, patch, self._std_schema, self._refl_schema,\n                    self._schema_class_layout,\n                    self._tenant.get_backend_runtime_params(),\n                    patch_info=patch_info,\n                )\n\n                await bootstrap._store_static_bin_cache_conn(\n                    conn, f'patch_log_{idx}', pickle.dumps(entry))\n\n            patches[num] = entry\n            _, _, updates = entry\n            if 'std_and_reflection_schema' in updates:\n                self._std_schema, self._refl_schema = updates[\n                    'std_and_reflection_schema']\n                # +config patches might modify config_spec, which requires\n                # a reload of it from the schema.\n                if '+config' in kind:\n                    config_spec = config.load_spec_from_schema(self._std_schema)\n                    self._config_settings = config_spec\n\n            if 'local_intro_query' in updates:\n                self._local_intro_query = updates['local_intro_query']\n            if 'global_intro_query' in updates:\n                self._global_intro_query = updates['global_intro_query']\n            if 'classlayout' in updates:\n                self._schema_class_layout = updates['classlayout']\n            if 'sysqueries' in updates:\n                queries = json.loads(updates['sysqueries'])\n                self._sys_queries = immutables.Map(\n                    {k: q.encode() for k, q in queries.items()})\n            if 'report_configs_typedesc' in updates:\n                self._report_config_typedesc = (\n                    updates['report_configs_typedesc'])\n\n        return patches\n\n    async def _maybe_apply_patches(\n        self,\n        dbname: str,\n        conn: pgcon.PGConnection,\n        patches: dict[int, bootstrap.PatchEntry],\n        sys: bool=False,\n    ) -> None:\n        \"\"\"Apply any un-applied patches to the database.\"\"\"\n        num_patches = await self._tenant.get_patch_count(conn)\n        for num, (sql_b, syssql, keys) in patches.items():\n            if num_patches <= num:\n                if sys:\n                    sql_b += syssql\n                logger.info(\"applying patch %d to database '%s'\", num, dbname)\n                sql = tuple(x.encode('utf-8') for x in sql_b)\n\n                # For certain things, we need to actually run it\n                # against each user database.\n                if keys.get('is_user_update'):\n                    from . import bootstrap\n\n                    kind, patch = pg_patches.PATCHES[num]\n                    patch_info = await bootstrap.gather_patch_info(\n                        num, kind, patch, conn\n                    )\n\n                    # Reload the compiler state from this database in\n                    # particular, so we can compiler from exactly the\n                    # right state. (Since self._std_schema and the like might\n                    # be further advanced.)\n                    state = (await edbcompiler.new_compiler_from_pg(conn)).state\n\n                    assert state.global_intro_query and state.local_intro_query\n                    global_schema = self._parse_global_schema(\n                        await conn.sql_fetch_val(\n                            state.global_intro_query.encode('utf-8')),\n                    )\n                    user_schema = self._parse_user_schema(\n                        await conn.sql_fetch_val(\n                            state.local_intro_query.encode('utf-8')),\n                        global_schema,\n                    )\n\n                    entry = bootstrap.prepare_patch(\n                        num, kind, patch,\n                        state.std_schema,\n                        state.refl_schema,\n                        state.schema_class_layout,\n                        self._tenant.get_backend_runtime_params(),\n                        patch_info=patch_info,\n                        user_schema=user_schema,\n                        global_schema=global_schema,\n                        dbname=dbname,\n                    )\n\n                    sql += tuple(x.encode('utf-8') for x in entry[0])\n\n                if sql:\n                    await conn.sql_execute(sql)\n                logger.info(\n                    \"finished applying patch %d to database '%s'\", num, dbname)\n\n    async def _maybe_patch_db(\n        self, dbname: str, patches: dict[int, bootstrap.PatchEntry], sem: Any\n    ) -> None:\n        logger.info(\"applying patches to database '%s'\", dbname)\n\n        try:\n            async with sem:\n                async with self._tenant.direct_pgcon(dbname) as conn:\n                    await self._maybe_apply_patches(dbname, conn, patches)\n        except Exception as e:\n            if (\n                isinstance(e, errors.EdgeDBError)\n                and not isinstance(e, errors.InternalServerError)\n            ):\n                raise\n            raise errors.InternalServerError(\n                f'Could not apply patches for minor version upgrade to '\n                f'database {dbname}'\n            ) from e\n\n    async def _maybe_patch(self) -> None:\n        \"\"\"Apply patches to all the databases\"\"\"\n\n        async with self._tenant.use_sys_pgcon() as syscon:\n            patches = await self._prepare_patches(syscon)\n            if not patches:\n                return\n\n            dbnames = await self.get_dbnames(syscon)\n\n        async with asyncio.TaskGroup() as g:\n            # Cap the parallelism used when applying patches, to avoid\n            # having huge numbers of in flight patches that make\n            # little visible progress in the logs.\n            sem = asyncio.Semaphore(16)\n\n            # Patch all the databases\n            for dbname in dbnames:\n                if dbname != defines.EDGEDB_SYSTEM_DB:\n                    g.create_task(\n                        self._maybe_patch_db(dbname, patches, sem))\n\n            # Patch the template db, so that any newly created databases\n            # will have the patches.\n            g.create_task(self._maybe_patch_db(\n                defines.EDGEDB_TEMPLATE_DB, patches, sem))\n\n        await self._tenant.ensure_database_not_connected(\n            defines.EDGEDB_TEMPLATE_DB\n        )\n\n        # Patch the system db last. The system db needs to go last so\n        # that it only gets updated if all of the other databases have\n        # been succesfully patched. This is important, since we don't check\n        # other databases for patches unless the system db is patched.\n        #\n        # Driving everything from the system db like this lets us\n        # always use the correct schema when compiling patches.\n        async with self._tenant.use_sys_pgcon() as syscon:\n            await self._maybe_apply_patches(\n                defines.EDGEDB_SYSTEM_DB, syscon, patches, sys=True)\n\n    def _load_schema(self, result, version_key) -> s_schema.Schema:\n        res = pickle.loads(result[2:])\n        if version_key != pg_patches.get_version_key(len(pg_patches.PATCHES)):\n            res = s_schema.upgrade_schema(res)\n        return res\n\n    async def _load_instance_data(self):\n        logger.info(\"loading instance data\")\n        async with self._tenant.use_sys_pgcon() as syscon:\n            patch_count = await self._tenant.get_patch_count(syscon)\n            version_key = pg_patches.get_version_key(patch_count)\n\n            result = await instdata.get_instdata(\n                syscon, f'sysqueries{version_key}', 'json')\n            queries = json.loads(result)\n            self._sys_queries = immutables.Map(\n                {k: q.encode() for k, q in queries.items()})\n\n            self._report_config_typedesc[(1, 0)] = (\n                await instdata.get_instdata(\n                    syscon,\n                    f'report_configs_typedesc_1_0{version_key}',\n                    'bin',\n                )\n            )\n\n            self._report_config_typedesc[(2, 0)] = (\n                await instdata.get_instdata(\n                    syscon,\n                    f'report_configs_typedesc_2_0{version_key}',\n                    'bin',\n                )\n            )\n\n    def _reload_stmt_cache_size(self):\n        size = self.config_lookup(\n            '_pg_prepared_statement_cache_size', self._get_sys_config()\n        )\n        self._stmt_cache_size = size\n        self._tenant.set_stmt_cache_size(size)\n\n    async def _restart_servers_new_addr(self, nethosts, netport):\n        if not netport:\n            raise RuntimeError('cannot restart without network port specified')\n        nethosts, has_ipv4_wc, has_ipv6_wc = await _resolve_interfaces(\n            nethosts\n        )\n        servers_to_stop = []\n        servers_to_stop_early = []\n        servers = {}\n        if self._listen_port == netport:\n            hosts_to_start = [\n                host for host in nethosts if host not in self._servers\n            ]\n            for host, srv in self._servers.items():\n                if host == ADMIN_PLACEHOLDER or host in nethosts:\n                    servers[host] = srv\n                elif host in ['::', '0.0.0.0']:\n                    servers_to_stop_early.append(srv)\n                else:\n                    if has_ipv4_wc:\n                        try:\n                            ipaddress.IPv4Address(host)\n                        except ValueError:\n                            pass\n                        else:\n                            servers_to_stop_early.append(srv)\n                            continue\n                    if has_ipv6_wc:\n                        try:\n                            ipaddress.IPv6Address(host)\n                        except ValueError:\n                            pass\n                        else:\n                            servers_to_stop_early.append(srv)\n                            continue\n                    servers_to_stop.append(srv)\n            admin = False\n        else:\n            hosts_to_start = nethosts\n            servers_to_stop = list(self._servers.values())\n            admin = True\n\n        if servers_to_stop_early:\n            await self._stop_servers_with_logging(servers_to_stop_early)\n\n        if hosts_to_start:\n            try:\n                new_servers, *_ = await self._start_servers(\n                    tuple(hosts_to_start),\n                    netport,\n                    admin=admin,\n                )\n                servers.update(new_servers)\n            except StartupError:\n                raise errors.ConfigurationError(\n                    'Server updated its config but cannot serve on requested '\n                    'address/port, please see server log for more information.'\n                )\n        self._servers = servers\n        self._listen_hosts = [\n            s.getsockname()[0]\n            for host, tcp_srv in servers.items()\n            if host != ADMIN_PLACEHOLDER\n            for s in tcp_srv.sockets  # type: ignore\n        ]\n        self._listen_port = netport\n\n        await self._stop_servers_with_logging(servers_to_stop)\n\n    async def _stop_servers_with_logging(self, servers_to_stop):\n        addrs = []\n        unix_addr = None\n        port = None\n        for srv in servers_to_stop:\n            for s in srv.sockets:\n                addr = s.getsockname()\n                if isinstance(addr, tuple):\n                    addrs.append(addr[:2])\n                    if port is None:\n                        port = addr[1]\n                    elif port != addr[1]:\n                        port = 0\n                else:\n                    unix_addr = addr\n        if len(addrs) > 1:\n            if port:\n                addr_str = f\"{{{', '.join(addr[0] for addr in addrs)}}}:{port}\"\n            else:\n                addr_str = f\"{{{', '.join('%s:%d' % addr for addr in addrs)}}}\"\n        elif addrs:\n            addr_str = \"%s:%d\" % addrs[0]\n        else:\n            addr_str = None\n        if addr_str:\n            logger.info('Stopping to serve on %s', addr_str)\n        if unix_addr:\n            logger.info('Stopping to serve admin on %s', unix_addr)\n\n        await self._stop_servers(servers_to_stop)\n\n    async def _on_system_config_set(self, setting_name, value):\n        try:\n            if setting_name == 'listen_addresses':\n                await self._restart_servers_new_addr(value, self._listen_port)\n\n            elif setting_name == 'listen_port':\n                await self._restart_servers_new_addr(self._listen_hosts, value)\n\n            elif setting_name == 'session_idle_timeout':\n                self.reinit_idle_gc_collector()\n\n            elif setting_name == '_pg_prepared_statement_cache_size':\n                self._reload_stmt_cache_size()\n\n            self._tenant.schedule_reported_config_if_needed(setting_name)\n        except Exception:\n            metrics.background_errors.inc(\n                1.0, self._tenant.get_instance_name(), 'on_system_config_set'\n            )\n            raise\n\n    async def _on_system_config_reset(self, setting_name):\n        try:\n            if setting_name == 'listen_addresses':\n                cfg = self._get_sys_config()\n                await self._restart_servers_new_addr(\n                    self.config_lookup('listen_addresses', cfg)\n                    or ('localhost',),\n                    self._listen_port,\n                )\n\n            elif setting_name == 'listen_port':\n                cfg = self._get_sys_config()\n                await self._restart_servers_new_addr(\n                    self._listen_hosts,\n                    self.config_lookup('listen_port', cfg)\n                    or defines.EDGEDB_PORT,\n                )\n\n            elif setting_name == 'session_idle_timeout':\n                self.reinit_idle_gc_collector()\n\n            elif setting_name == '_pg_prepared_statement_cache_size':\n                self._reload_stmt_cache_size()\n\n            self._tenant.schedule_reported_config_if_needed(setting_name)\n        except Exception:\n            metrics.background_errors.inc(\n                1.0, self._tenant.get_instance_name(), 'on_system_config_reset'\n            )\n            raise\n\n    async def _after_system_config_add(self, setting_name, value):\n        try:\n            if setting_name == 'auth':\n                self._tenant.populate_sys_auth()\n        except Exception:\n            metrics.background_errors.inc(\n                1.0,\n                self._tenant.get_instance_name(),\n                'after_system_config_add',\n            )\n            raise\n\n    async def _after_system_config_rem(self, setting_name, value):\n        try:\n            if setting_name == 'auth':\n                self._tenant.populate_sys_auth()\n        except Exception:\n            metrics.background_errors.inc(\n                1.0,\n                self._tenant.get_instance_name(),\n                'after_system_config_rem',\n            )\n            raise\n\n    async def run_startup_script_and_exit(self):\n        \"\"\"Run the script specified in *startup_script* and exit immediately\"\"\"\n        if self._startup_script is None:\n            raise AssertionError('startup script is not defined')\n        pool = await compiler_pool.create_compiler_pool(\n            **self._get_compiler_args()\n        )\n        self.compilation_config_serializer = (\n            await pool.make_compilation_config_serializer()\n        )\n        self._compiler_pool = pool\n        try:\n            await binary.run_script(\n                server=self,\n                tenant=self._tenant,\n                database=self._startup_script.database,\n                user=self._startup_script.user,\n                script=self._startup_script.text,\n            )\n        finally:\n            await self._destroy_compiler_pool()\n\n    async def _before_start_servers(self) -> None:\n        await self._tenant.start_accepting_new_tasks()\n        if self._startup_script and self._new_instance:\n            await binary.run_script(\n                server=self,\n                tenant=self._tenant,\n                database=self._startup_script.database,\n                user=self._startup_script.user,\n                script=self._startup_script.text,\n            )\n\n    async def _after_start_servers(self) -> None:\n        self._tenant.start_running()\n\n    def _get_status(self) -> dict[str, Any]:\n        status = super()._get_status()\n        status[\"tenant_id\"] = self._tenant.tenant_id\n        return status\n\n    def load_jwcrypto(self, jws_key_file: pathlib.Path) -> auth.JWKSet:\n        jws_key = super().load_jwcrypto(jws_key_file)\n        self._tenant.load_jwcrypto(jws_key)\n        return jws_key\n\n    def request_shutdown(self):\n        self._tenant.stop_accepting_connections()\n        super().request_shutdown()\n\n    async def stop(self):\n        try:\n            self._tenant.stop()\n\n            await super().stop()\n\n            await self._tenant.wait_stopped()\n            await self._destroy_compiler_pool()\n        finally:\n            self._tenant.terminate_sys_pgcon()\n\n    def get_debug_info(self):\n        parent = super().get_debug_info()\n        child = self._tenant.get_debug_info()\n        parent[\"params\"].update(child[\"params\"])\n        child[\"params\"] = parent[\"params\"]\n        parent.update(child)\n        return parent\n\n    def _get_backend_runtime_params(self) -> pgparams.BackendRuntimeParams:\n        return self._tenant.get_backend_runtime_params()\n\n    def _get_compiler_args(self) -> dict[str, Any]:\n        rv = super()._get_compiler_args()\n        rv.update(self._tenant.get_compiler_args())\n        return rv\n\n    def start_watching_files(self):\n        super().start_watching_files()\n        self._tenant.start_watching_files()\n\n\ndef _cleanup_wildcard_addrs(\n    hosts: Sequence[str],\n) -> tuple[list[str], list[str], bool, bool]:\n    \"\"\"Filter out conflicting addresses in presence of INADDR_ANY wildcards.\n\n    Attempting to bind to 0.0.0.0 (or ::) _and_ a non-wildcard address will\n    usually result in EADDRINUSE.  To avoid this, filter out all specific\n    addresses if a wildcard is present in the *hosts* sequence.\n\n    Returns a tuple: first element is the new list of hosts, second\n    element is a list of rejected host addrs/names.\n    \"\"\"\n\n    ipv4_hosts = set()\n    ipv6_hosts = set()\n    named_hosts = set()\n\n    ipv4_wc = ipaddress.ip_address('0.0.0.0')\n    ipv6_wc = ipaddress.ip_address('::')\n\n    for host in hosts:\n        if host == \"*\":\n            ipv4_hosts.add(ipv4_wc)\n            ipv6_hosts.add(ipv6_wc)\n            continue\n\n        try:\n            ip = ipaddress.IPv4Address(host)\n        except ValueError:\n            pass\n        else:\n            ipv4_hosts.add(ip)\n            continue\n\n        try:\n            ip6 = ipaddress.IPv6Address(host)\n        except ValueError:\n            pass\n        else:\n            ipv6_hosts.add(ip6)\n            continue\n\n        named_hosts.add(host)\n\n    if not ipv4_hosts and not ipv6_hosts:\n        return (list(hosts), [], False, False)\n\n    if ipv4_wc not in ipv4_hosts and ipv6_wc not in ipv6_hosts:\n        return (list(hosts), [], False, False)\n\n    if ipv4_wc in ipv4_hosts and ipv6_wc in ipv6_hosts:\n        return (\n            ['0.0.0.0', '::'],\n            [\n                str(a) for a in\n                ((named_hosts | ipv4_hosts | ipv6_hosts) - {ipv4_wc, ipv6_wc})\n            ],\n            True,\n            True,\n        )\n\n    if ipv4_wc in ipv4_hosts:\n        return (\n            [str(a) for a in ({ipv4_wc} | ipv6_hosts)],\n            [str(a) for a in ((named_hosts | ipv4_hosts) - {ipv4_wc})],\n            True,\n            False,\n        )\n\n    if ipv6_wc in ipv6_hosts:\n        return (\n            [str(a) for a in ({ipv6_wc} | ipv4_hosts)],\n            [str(a) for a in ((named_hosts | ipv6_hosts) - {ipv6_wc})],\n            False,\n            True,\n        )\n\n    raise AssertionError('unreachable')\n\n\nasync def _resolve_host(host: str) -> list[str] | Exception:\n    loop = asyncio.get_running_loop()\n    try:\n        addrinfo = await loop.getaddrinfo(\n            None if host == '*' else host,\n            0,\n            family=socket.AF_UNSPEC,\n            type=socket.SOCK_STREAM,\n            flags=socket.AI_PASSIVE,\n        )\n    except Exception as e:\n        return e\n    else:\n        return [addr[4][0] for addr in addrinfo]\n\n\nasync def _resolve_interfaces(\n    hosts: Sequence[str],\n) -> tuple[Sequence[str], bool, bool]:\n\n    async with asyncio.TaskGroup() as g:\n        resolve_tasks = {\n            host: g.create_task(_resolve_host(host))\n            for host in hosts\n        }\n\n    addrs = []\n    for host, fut in resolve_tasks.items():\n        result = fut.result()\n        if isinstance(result, Exception):\n            logger.warning(\n                f\"could not translate host name {host!r} to address: {result}\")\n        else:\n            addrs.extend(result)\n\n    (\n        clean_addrs, rejected_addrs, has_ipv4_wc, has_ipv6_wc\n    ) = _cleanup_wildcard_addrs(addrs)\n\n    if rejected_addrs:\n        logger.warning(\n            \"wildcard addresses found in listen_addresses; \" +\n            \"discarding the other addresses: \" +\n            \", \".join(repr(h) for h in rejected_addrs)\n        )\n\n    return clean_addrs, has_ipv4_wc, has_ipv6_wc\n"
  },
  {
    "path": "edb/server/service_manager.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Optional\n\nimport errno\nimport logging\nimport os\nimport socket\nimport sys\n\n\nSD_LISTEN_FDS_START = 3\n\n\nlogger = logging.getLogger('edb.server')\n\n\ndef _stream_socket_from_fd(fd: int) -> Optional[socket.socket]:\n    try:\n        sock = socket.socket(fileno=fd)\n    except OSError:\n        logger.warning(\n            f\"activation file descriptor {fd} is not a socket \"\n            f\", ignoring\"\n        )\n        return None\n\n    if sock.family not in {socket.AF_INET, socket.AF_INET6}:\n        logger.warning(\n            f\"activation file descriptor {fd} is not an AF_INET[6] socket \"\n            f\", ignoring\"\n        )\n        return None\n\n    if sock.type != socket.SOCK_STREAM:\n        logger.warning(\n            f\"activation file descriptor {fd} is not an SOCK_STREAM \"\n            f\"socket, ignoring\"\n        )\n        return None\n\n    return sock\n\n\ndef sd_notify(message: str) -> None:\n    notify_socket = os.environ.get('NOTIFY_SOCKET')\n    if not notify_socket:\n        return\n\n    if notify_socket[0] == '@':\n        notify_socket = '\\0' + notify_socket[1:]\n\n    with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as sd_sock:\n        try:\n            sd_sock.connect(notify_socket)\n            sd_sock.sendall(message.encode())\n        except Exception as e:\n            logger.info('Could not send systemd notification: %s', e)\n\n\ndef sd_get_activation_listen_sockets() -> dict[str, list[socket.socket]]:\n    # Prevent socket activation variables from being inherited by\n    # child processes (regardless of success below).\n    listen_pid = os.environ.pop(\"LISTEN_PID\", \"\")\n    listen_fds = os.environ.pop(\"LISTEN_FDS\", \"\")\n    listen_fdnames = os.environ.pop(\"LISTEN_FDNAMES\", \"\")\n\n    if not listen_pid or not listen_fds:\n        return {}\n\n    try:\n        expected_pid = int(listen_pid)\n    except ValueError:\n        logger.warning(\n            \"the value of LISTEN_PID environment variable \"\n            \"is not a valid integer, ignoring socket activation data\"\n        )\n        return {}\n\n    if expected_pid != os.getpid():\n        logger.warning(\n            \"the value of LISTEN_PID does not match the PID of this \"\n            \"process, ignoring socket activation data\"\n        )\n        return {}\n\n    try:\n        num_fds = int(listen_fds)\n    except ValueError:\n        logger.warning(\n            \"the value of LISTEN_FDS environment variable \"\n            \"is not a valid integer, ignoring socket activation data\"\n        )\n        return {}\n\n    fd_names = listen_fdnames.split(\":\")\n    fd_range = range(SD_LISTEN_FDS_START, SD_LISTEN_FDS_START + num_fds)\n    sockets: dict[str, list[socket.socket]] = {}\n\n    for i, fd in enumerate(fd_range):\n        os.set_inheritable(fd, False)\n\n        try:\n            name = fd_names[i]\n        except IndexError:\n            name = f\"LISTEN_FD_{fd}\"\n\n        sock = _stream_socket_from_fd(fd)\n        if sock is not None:\n            sockets.setdefault(name, []).append(sock)\n\n    return sockets\n\n\nif sys.platform == \"darwin\":\n    import ctypes\n    import ctypes.util\n\n    syslib = ctypes.CDLL(ctypes.util.find_library('System'))\n    syslib.launch_activate_socket.argypes = [  # type: ignore[attr-defined]\n        ctypes.c_char_p,\n        ctypes.POINTER(ctypes.POINTER(ctypes.c_int)),\n        ctypes.POINTER(ctypes.c_size_t),\n    ]\n\n    class LaunchActivateSocketError(Exception):\n        def __init__(self, errno: int) -> None:\n            self.errno = errno\n\n    def _launch_activate_socket(name) -> list[int]:\n        fds = ctypes.POINTER(ctypes.c_int)()\n        num_fds = ctypes.c_size_t()\n        result = syslib.launch_activate_socket(\n            ctypes.c_char_p(name.encode(\"utf-8\")),\n            ctypes.byref(fds),\n            ctypes.byref(num_fds),\n        )\n        if result == 0:\n            return [fds[i] for i in range(num_fds.value)]\n        elif result == errno.ESRCH:\n            # Not running under launchd\n            return []\n        else:\n            raise LaunchActivateSocketError(result)\n\n    def launchd_get_activation_listen_sockets() -> (\n        dict[str, list[socket.socket]]\n    ):\n        names = [\"edgedb-server\"]\n        sockets: dict[str, list[socket.socket]] = {}\n\n        for name in names:\n            try:\n                fds = _launch_activate_socket(name)\n            except LaunchActivateSocketError as e:\n                logger.warning(\n                    f\"could not activate socket {name}: \"\n                    f\"launch_activate_socket() returned {e.errno}\")\n                continue\n\n            for fd in fds:\n                os.set_inheritable(fd, False)\n                sock = _stream_socket_from_fd(fd)\n                if sock is not None:\n                    sockets.setdefault(name, []).append(sock)\n\n        return sockets\n\nelse:\n\n    def launchd_get_activation_listen_sockets() -> (\n        dict[str, list[socket.socket]]\n    ):\n        return {}\n\n\ndef get_activation_listen_sockets() -> dict[str, list[socket.socket]]:\n    if sys.platform == \"darwin\":\n        sockets = launchd_get_activation_listen_sockets()\n    else:\n        sockets = sd_get_activation_listen_sockets()\n\n    return sockets\n"
  },
  {
    "path": "edb/server/smtp.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nimport dataclasses\nimport email.message\nimport asyncio\nimport logging\nimport os\nimport hashlib\nimport pickle\nimport aiosmtplib\n\nfrom typing import Optional\n\nfrom edb.common import retryloop\nfrom edb.ir import statypes\nfrom edb import errors\nfrom . import dbview\n\n\n_semaphore: asyncio.BoundedSemaphore | None = None\n\nlogger = logging.getLogger('edb.server.smtp')\n\n\n@dataclasses.dataclass\nclass SMTPProviderConfig:\n    name: str\n    sender: Optional[str]\n    host: Optional[str]\n    port: Optional[int]\n    username: Optional[str]\n    password: Optional[str]\n    security: str\n    validate_certs: bool\n    timeout_per_email: statypes.Duration\n    timeout_per_attempt: statypes.Duration\n\n\nclass SMTP:\n    def __init__(self, db: dbview.Database):\n        current_provider = get_current_email_provider(db)\n        self.sender = current_provider.sender or \"noreply@example.com\"\n        default_port = (\n            465\n            if current_provider.security == \"TLS\"\n            else 587 if current_provider.security == \"STARTTLS\" else 25\n        )\n        use_tls: bool\n        start_tls: bool | None\n        match current_provider.security:\n            case \"PlainText\":\n                use_tls = False\n                start_tls = False\n\n            case \"TLS\":\n                use_tls = True\n                start_tls = False\n\n            case \"STARTTLS\":\n                use_tls = False\n                start_tls = True\n\n            case \"STARTTLSOrPlainText\":\n                use_tls = False\n                start_tls = None\n\n            case _:\n                raise NotImplementedError\n\n        host = current_provider.host or \"localhost\"\n        port = current_provider.port or default_port\n        username = current_provider.username\n        password = current_provider.password\n        validate_certs = current_provider.validate_certs\n        timeout_per_attempt = current_provider.timeout_per_attempt\n\n        req_timeout = timeout_per_attempt.to_microseconds() / 1_000_000.0\n        self.timeout_per_email = (\n            current_provider.timeout_per_email.to_microseconds() / 1_000_000.0\n        )\n        self.client = aiosmtplib.SMTP(\n            hostname=host,\n            port=port,\n            username=username,\n            password=password,\n            timeout=req_timeout,\n            use_tls=use_tls,\n            start_tls=start_tls,\n            validate_certs=validate_certs,\n        )\n\n    async def send(\n        self,\n        message: email.message.Message,\n        *,\n        test_mode: bool = False,\n    ) -> None:\n        global _semaphore\n        if _semaphore is None:\n            _semaphore = asyncio.BoundedSemaphore(\n                int(\n                    os.environ.get(\n                        \"EDGEDB_SERVER_AUTH_SMTP_CONCURRENCY\",\n                        os.environ.get(\"EDGEDB_SERVER_SMTP_CONCURRENCY\", 5),\n                    )\n                )\n            )\n\n        # n.b. When constructing EmailMessage objects, we don't set the \"From\"\n        # header since that is configured in the SmtpProviderConfig. However,\n        # the EmailMessage will have the correct \"To\" header.\n        message[\"From\"] = self.sender\n        rloop = retryloop.RetryLoop(\n            timeout=self.timeout_per_email,\n            backoff=retryloop.exp_backoff(),\n            ignore=(\n                aiosmtplib.SMTPConnectError,\n                aiosmtplib.SMTPHeloError,\n                aiosmtplib.SMTPServerDisconnected,\n                aiosmtplib.SMTPConnectTimeoutError,\n                aiosmtplib.SMTPConnectResponseError,\n            ),\n        )\n        async for iteration in rloop:\n            async with iteration:\n                async with _semaphore:\n                    # Currently we are not reusing SMTP connections, but\n                    # ideally we should replace this with a pool of\n                    # connections, and drop idle connections after configured\n                    # time.\n                    if test_mode:\n                        self._send_test_mode_email(message)\n                    else:\n                        logger.info(\n                            \"Sending SMTP message to \"\n                            f\"{self.client.hostname}:{self.client.port}\"\n                        )\n\n                        async with self.client:\n                            errors, response = await self.client.send_message(\n                                message\n                            )\n                        if errors:\n                            logger.error(\n                                f\"SMTP server returned errors: {errors}\"\n                            )\n                        else:\n                            logger.info(\n                                f\"SMTP message sent successfully: {response}\"\n                            )\n\n    def _send_test_mode_email(self, message: email.message.Message):\n        sender = message[\"From\"]\n        recipients = message[\"To\"]\n        recipients_list: list[str]\n        if isinstance(recipients, str):\n            recipients_list = [recipients]\n        elif recipients is None:\n            recipients_list = []\n        else:\n            recipients_list = list(recipients)\n\n        hash_input = f\"{sender}{','.join(recipients_list)}\"\n        file_name_hash = hashlib.sha256(hash_input.encode()).hexdigest()\n        file_name = f\"/tmp/edb-test-email-{file_name_hash}.pickle\"\n        test_file = os.environ.get(\n            \"EDGEDB_TEST_EMAIL_FILE\",\n            file_name,\n        )\n        if os.path.exists(test_file):\n            os.unlink(test_file)\n        with open(test_file, \"wb\") as f:\n            logger.info(f\"Dumping SMTP message to {test_file}\")\n            args = dict(\n                message=message,\n                sender=sender,\n                recipients=recipients,\n                hostname=self.client.hostname,\n                port=self.client.port,\n                username=self.client._login_username,\n                password=self.client._login_password,\n                timeout=self.client.timeout,\n                use_tls=self.client.use_tls,\n                start_tls=self.client._start_tls_on_connect,\n                validate_certs=self.client.validate_certs,\n            )\n            pickle.dump(args, f)\n\n\ndef get_current_email_provider(\n    db: dbview.Database,\n) -> SMTPProviderConfig:\n    current_provider_name = db.lookup_config(\"current_email_provider_name\")\n    if current_provider_name is None:\n        raise errors.ConfigurationError(\"No email provider configured\")\n\n    found = None\n    objs = (\n        list(db.lookup_config(\"email_providers\"))\n        + db.tenant._sidechannel_email_configs\n    )\n    for obj in objs:\n        if obj.name == current_provider_name:\n            values = {}\n            for field in dataclasses.fields(SMTPProviderConfig):\n                key = field.name\n                values[key] = getattr(obj, key)\n            found = SMTPProviderConfig(**values)\n            break\n\n    if found is None:\n        raise errors.ConfigurationError(\n            f\"No email provider named {current_provider_name!r}\"\n        )\n    return found\n"
  },
  {
    "path": "edb/server/tenant.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    Iterator,\n    Iterable,\n    Mapping,\n    Coroutine,\n    AsyncGenerator,\n    Optional,\n    TypedDict,\n    TYPE_CHECKING,\n)\n\nimport asyncio\nimport contextlib\nimport dataclasses\nimport json\nimport logging\nimport os\nimport pathlib\nimport pickle\nimport struct\nimport sys\nimport textwrap\nimport time\nimport tomllib\nimport uuid\nimport weakref\n\nimport immutables\n\nfrom edb import buildmeta\nfrom edb import errors\nfrom edb.common import asyncutil\nfrom edb.common import lru\nfrom edb.common import retryloop\nfrom edb.common import verutils\nfrom edb.common.log import current_tenant\n\nfrom . import auth\nfrom . import args as srvargs\nfrom . import config\nfrom . import connpool\nfrom . import dbview\nfrom . import defines\nfrom . import instdata\nfrom . import metrics\nfrom . import pgcon\nfrom . import compiler as edbcompiler\nfrom . import pgconnparams\n\nfrom .ha import adaptive as adaptive_ha\nfrom .ha import base as ha_base\nfrom .http import HttpClient\nfrom .pgcon import errors as pgcon_errors\nfrom .compiler import enums as compiler_enums\n\nif TYPE_CHECKING:\n    from edb.pgsql import params as pgparams\n\n    from . import pgcluster\n    from . import server as edbserver\n    from . import compiler_pool as edbcompiler_pool\n\n\nlogger = logging.getLogger(\"edb.server\")\n\n\nHTTP_MAX_CONNECTIONS = 100\nHEALTH_CHECK_MIN_INTERVAL: float = float(\n    os.getenv(\"GEL_BACKEND_HEALTH_CHECK_MIN_INTERVAL\", 10)\n)\nHEALTH_CHECK_TIMEOUT: float = float(\n    os.getenv(\"GEL_BACKEND_HEALTH_CHECK_TIMEOUT\", 10)\n)\n\n\nclass RoleDescriptor(TypedDict):\n    superuser: bool\n    name: str\n    password: str | None\n    all_permissions: list[str] | None\n    branches: list[str]\n    apply_access_policies_pg_default: bool | None\n\n\nclass Tenant(ha_base.ClusterProtocol):\n    _server: edbserver.BaseServer\n    _cluster: pgcluster.BaseCluster\n    _tenant_id: str\n    _instance_name: str\n    _instance_data: Mapping[str, str]\n    _dbindex: dbview.DatabaseIndex | None\n    _initing: bool\n    _running: bool\n    _accepting_connections: bool\n    _introspection_locks: weakref.WeakValueDictionary[str, asyncio.Lock]\n\n    __loop: asyncio.AbstractEventLoop\n    _task_group: asyncio.TaskGroup | None\n    _tasks: set[asyncio.Task]\n    _accept_new_tasks: bool\n    _file_watch_finalizers: list[Callable[[], None]]\n\n    __sys_pgcon: pgcon.PGConnection | None\n    _sys_pgcon_waiter: asyncio.Lock\n    _sys_pgcon_ready_evt: asyncio.Event\n    _sys_pgcon_reconnect_evt: asyncio.Event\n    _sys_pgcon_last_active_time: float\n    _max_backend_connections: int\n    _suggested_client_pool_size: int\n    _pg_pool: connpool.Pool\n    _pg_unavailable_msg: str | None\n    _init_con_data: list[config.ConState]\n    _init_con_sql: bytes | None\n\n    _ha_master_serial: int\n    _backend_adaptive_ha: adaptive_ha.AdaptiveHASupport | None\n    _readiness_state_file: pathlib.Path | None\n    _readiness: srvargs.ReadinessState\n    _readiness_reason: str\n    _config_file: pathlib.Path | None\n\n    _extensions_dirs: tuple[pathlib.Path, ...]\n\n    # A set of databases that should not accept new connections.\n    _block_new_connections: set[str]\n    _report_config_data: dict[defines.ProtocolVersion, bytes]\n\n    _roles: Mapping[str, RoleDescriptor]\n    _role_capabilities: Mapping[str, compiler_enums.Capability]\n    _sys_auth: tuple[Any, ...]\n    _jwt_sub_allowlist_file: pathlib.Path | None\n    _jwt_sub_allowlist: frozenset[str] | None\n    _jwt_revocation_list_file: pathlib.Path | None\n    _jwt_revocation_list: frozenset[str] | None\n\n    _http_client: HttpClient | None\n\n    _sidechannel_email_configs: list[Any]\n\n    def __init__(\n        self,\n        cluster: pgcluster.BaseCluster,\n        *,\n        instance_name: str,\n        max_backend_connections: int,\n        backend_adaptive_ha: bool = False,\n        extensions_dir: tuple[pathlib.Path, ...] = (),\n    ):\n        self._cluster = cluster\n        self._tenant_id = self.get_backend_runtime_params().tenant_id\n        self._instance_name = instance_name\n        self._instance_data = immutables.Map()\n        self._initing = True\n        self._running = False\n        self._accepting_connections = False\n\n        self._task_group = None\n        self._tasks = set()\n        self._named_tasks: dict[str, asyncio.Task] = dict()\n        self._accept_new_tasks = False\n        self._file_watch_finalizers = []\n        self._introspection_locks = weakref.WeakValueDictionary()\n        self._sidechannel_email_configs = []\n\n        self._extensions_dirs = extensions_dir\n\n        # Never use `self.__sys_pgcon` directly; get it via\n        # `async with self.use_sys_pgcon()`.\n        self.__sys_pgcon = None\n        self._sys_pgcon_last_active_time = 0\n\n        # Increase-only counter to reject outdated attempts to connect\n        self._ha_master_serial = 0\n        if backend_adaptive_ha:\n            self._backend_adaptive_ha = adaptive_ha.AdaptiveHASupport(\n                self, self._instance_name\n            )\n        else:\n            self._backend_adaptive_ha = None\n        self._readiness_state_file = None\n        self._readiness = srvargs.ReadinessState.Default\n        self._readiness_reason = \"\"\n        self._config_file = None\n\n        self._max_backend_connections = max_backend_connections\n        self._suggested_client_pool_size = max(\n            min(\n                max_backend_connections, defines.MAX_SUGGESTED_CLIENT_POOL_SIZE\n            ),\n            defines.MIN_SUGGESTED_CLIENT_POOL_SIZE,\n        )\n        self._pg_pool = connpool.Pool(\n            connect=self._pg_connect,\n            disconnect=self._pg_disconnect,\n            # 1 connection is reserved for the system DB\n            max_capacity=max_backend_connections - 1,\n        )\n        self._pg_unavailable_msg = None\n        self._block_new_connections = set()\n        self._report_config_data = {}\n        self._init_con_data = []\n        self._init_con_sql = None\n\n        # DB state will be initialized in init().\n        self._dbindex = None\n\n        self._branch_sem = asyncio.Semaphore(value=1)\n\n        self._roles = immutables.Map()\n        self._role_capabilities = immutables.Map()\n        self._sys_auth = tuple()\n        self._jwt_sub_allowlist_file = None\n        self._jwt_sub_allowlist = None\n        self._jwt_revocation_list_file = None\n        self._jwt_revocation_list = None\n\n        self._http_client = None\n\n        # If it isn't stored in instdata, it is the old default.\n        self.default_database = defines.EDGEDB_OLD_DEFAULT_DB\n\n    def set_reloadable_files(\n        self,\n        readiness_state_file: str | pathlib.Path | None = None,\n        jwt_sub_allowlist_file: str | pathlib.Path | None = None,\n        jwt_revocation_list_file: str | pathlib.Path | None = None,\n        config_file: str | pathlib.Path | None = None,\n    ) -> bool:\n        rv = False\n\n        if isinstance(readiness_state_file, str):\n            readiness_state_file = pathlib.Path(readiness_state_file)\n        if self._readiness_state_file != readiness_state_file:\n            self._readiness_state_file = readiness_state_file\n            rv = True\n\n        if isinstance(jwt_sub_allowlist_file, str):\n            jwt_sub_allowlist_file = pathlib.Path(jwt_sub_allowlist_file)\n        if self._jwt_sub_allowlist_file != jwt_sub_allowlist_file:\n            self._jwt_sub_allowlist_file = jwt_sub_allowlist_file\n            rv = True\n\n        if isinstance(jwt_revocation_list_file, str):\n            jwt_revocation_list_file = pathlib.Path(jwt_revocation_list_file)\n        if self._jwt_revocation_list_file != jwt_revocation_list_file:\n            self._jwt_revocation_list_file = jwt_revocation_list_file\n            rv = True\n\n        if isinstance(config_file, str):\n            config_file = pathlib.Path(config_file)\n        if self._config_file != config_file:\n            self._config_file = config_file\n            rv = True\n\n        return rv\n\n    def set_server(self, server: edbserver.BaseServer) -> None:\n        self._server = server\n        self.__loop = server.get_loop()\n\n    async def load_sidechannel_configs(\n        self,\n        value: Any,\n        *,\n        compiler: (\n            edbcompiler.Compiler | edbcompiler_pool.AbstractPool | None\n        ) = None,\n    ) -> None:\n        if compiler is None:\n            compiler = self._server.get_compiler_pool()\n        objects = {\"cfg::Config\": {\"email_providers\": value}}\n        if isinstance(compiler, edbcompiler.Compiler):\n            result = compiler.compile_structured_config(\n                objects, source=\"magic\", allow_nested=True\n            )\n        else:\n            result = await compiler.compile_structured_config(\n                objects, source=\"magic\", allow_nested=True\n            )\n        email_providers = result[\"cfg::Config\"][\"email_providers\"]\n        self._sidechannel_email_configs = list(email_providers.value)\n\n    def get_http_client(self, *, originator: str) -> HttpClient:\n        if self._http_client is None:\n            http_max_connections = self._server.config_lookup(\n                'http_max_connections', self.get_sys_config()\n            )\n            self._http_client = HttpClient(\n                http_max_connections,\n                user_agent=f\"EdgeDB {buildmeta.get_version_string(short=True)}\",\n                stat_callback=lambda stat: logger.debug(\n                    f\"HTTP stat: {originator} {stat}\"\n                )\n            )\n        return self._http_client\n\n    def on_switch_over(self):\n        # Bumping this serial counter will \"cancel\" all pending connections\n        # to the old master.\n        self._ha_master_serial += 1\n\n        if self._accept_new_tasks:\n            self.create_task(\n                self._pg_pool.prune_all_connections(),\n                interruptable=True,\n            )\n\n        if self.__sys_pgcon is None:\n            # Assume a reconnect task is already running, now that we know the\n            # new master is likely ready, let's just give the task a push.\n            self._sys_pgcon_reconnect_evt.set()\n        else:\n            # Brutally close the sys_pgcon to the old master - this should\n            # trigger a reconnect task.\n            self.__sys_pgcon.abort()\n\n        if self._backend_adaptive_ha is not None:\n            # Switch to FAILOVER if adaptive HA is enabled\n            self._backend_adaptive_ha.set_state_failover(\n                call_on_switch_over=False\n            )\n\n    def get_active_pgcon_num(self) -> int:\n        return self._pg_pool.active_conns\n\n    @property\n    def client_id(self) -> int:\n        return self._cluster.get_client_id()\n\n    @property\n    def server(self) -> edbserver.BaseServer:\n        return self._server\n\n    @property\n    def tenant_id(self) -> str:\n        return self._tenant_id\n\n    @property\n    def suggested_client_pool_size(self) -> int:\n        return self._suggested_client_pool_size\n\n    def get_pg_dbname(self, dbname: str) -> str:\n        return self._cluster.get_db_name(dbname)\n\n    def get_pgaddr(self) -> pgconnparams.ConnectionParams:\n        return self._cluster.get_pgaddr()\n\n    @lru.method_cache\n    def get_backend_runtime_params(self) -> pgparams.BackendRuntimeParams:\n        return self._cluster.get_runtime_params()\n\n    def get_instance_name(self) -> str:\n        return self._instance_name\n\n    def get_instance_data(self, key: str) -> str:\n        return self._instance_data[key]\n\n    def is_online(self) -> bool:\n        return self._readiness is not srvargs.ReadinessState.Offline\n\n    def is_blocked(self) -> bool:\n        return self._readiness is srvargs.ReadinessState.Blocked\n\n    def is_ready(self) -> bool:\n        return (\n            self._readiness is srvargs.ReadinessState.Default\n            or self._readiness is srvargs.ReadinessState.ReadOnly\n        )\n\n    def is_readonly(self) -> bool:\n        return self._readiness is srvargs.ReadinessState.ReadOnly\n\n    def get_readiness_reason(self) -> str:\n        return self._readiness_reason\n\n    def get_sys_config(self) -> Mapping[str, config.SettingValue]:\n        assert self._dbindex is not None\n        return self._dbindex.get_sys_config()\n\n    def get_report_config_data(\n        self,\n        protocol_version: defines.ProtocolVersion,\n    ) -> bytes:\n        if protocol_version >= (2, 0):\n            return self._report_config_data[(2, 0)]\n        else:\n            return self._report_config_data[(1, 0)]\n\n    def get_global_schema_pickle(self) -> bytes:\n        assert self._dbindex is not None\n        return self._dbindex.get_global_schema_pickle()\n\n    def get_db(self, *, dbname: str) -> dbview.Database:\n        assert self._dbindex is not None\n        return self._dbindex.get_db(dbname)\n\n    def maybe_get_db(self, *, dbname: str) -> dbview.Database | None:\n        assert self._dbindex is not None\n        return self._dbindex.maybe_get_db(dbname)\n\n    def is_accepting_connections(self) -> bool:\n        return self._accepting_connections and self._accept_new_tasks\n\n    def get_roles(self) -> Mapping[str, RoleDescriptor]:\n        return self._roles\n\n    def set_roles(self, roles: Mapping[str, RoleDescriptor]) -> None:\n        self._roles = roles\n        self._refresh_role_capabilities()\n\n    def get_role_capabilities(self) -> Mapping[str, compiler_enums.Capability]:\n        return self._role_capabilities\n\n    def _refresh_role_capabilities(self) -> None:\n        role_capabilities: dict[str, compiler_enums.Capability] = {}\n\n        for name, role_desc in self._roles.items():\n            superuser = bool(role_desc.get('superuser'))\n            available_permissions = (role_desc.get('all_permissions') or ())\n\n            if superuser:\n                capability = compiler_enums.Capability.ALL\n            else:\n                capability = (\n                    compiler_enums.Capability.TRANSACTION\n                    | compiler_enums.Capability.SESSION_CONFIG\n                    | compiler_enums.Capability.PERSISTENT_CONFIG\n                )\n\n                # Non-superuser can be given capabilities via\n                # the permissions\n                if 'sys::perm::data_modification' in available_permissions:\n                    capability |= compiler_enums.Capability.MODIFICATIONS\n                if 'sys::perm::ddl' in available_permissions:\n                    capability |= compiler_enums.Capability.DDL\n                if 'sys::perm::branch_config' in available_permissions:\n                    capability |= compiler_enums.Capability.BRANCH_CONFIG\n                if 'sys::perm::sql_session_config' in available_permissions:\n                    capability |= compiler_enums.Capability.SQL_SESSION_CONFIG\n                if 'sys::perm::analyze' in available_permissions:\n                    capability |= compiler_enums.Capability.ANALYZE\n\n            role_capabilities[name] = capability\n\n        self._role_capabilities = immutables.Map(role_capabilities)\n\n    async def _fetch_roles(self, syscon: pgcon.PGConnection) -> None:\n        role_query = self._server.get_sys_query(\"roles\")\n        json_data = await syscon.sql_fetch_val(role_query, use_prep_stmt=True)\n        roles = json.loads(json_data)\n        self._roles = immutables.Map([(r[\"name\"], r) for r in roles])\n        self._refresh_role_capabilities()\n\n    async def init_sys_pgcon(self) -> None:\n        self._sys_pgcon_waiter = asyncio.Lock()\n        self.__sys_pgcon = await self._pg_connect(\n            defines.EDGEDB_SYSTEM_DB,\n            source_description=\"init_sys_pgcon\",\n        )\n        self._sys_pgcon_last_active_time = time.monotonic()\n        self._sys_pgcon_ready_evt = asyncio.Event()\n        self._sys_pgcon_reconnect_evt = asyncio.Event()\n\n    async def get_patch_count(self, conn: pgcon.PGConnection) -> int:\n        \"\"\"Get the number of applied patches.\"\"\"\n        num_patches = await instdata.get_instdata(\n            conn, 'num_patches', 'json')\n        res: int = json.loads(num_patches) if num_patches else 0\n        return res\n\n    async def _check_metaschema_compatibility(\n        self, con: pgcon.PGConnection\n    ) -> None:\n        from edb.pgsql import patches as pg_patches\n\n        # Check catalog version\n        result = await instdata.get_instdata(\n            con, 'instancedata', 'json', versioned=False\n        )\n        catver = json.loads(result).get('catver')\n        if catver != defines.EDGEDB_CATALOG_VERSION:\n            raise errors.ConfigurationError(\n                'database instance incompatible with this version of Gel',\n                details=(\n                    f'The database instance was initialized with '\n                    f'Gel format version {catver}, but this version '\n                    f'of the server expects format version '\n                    f'{defines.EDGEDB_CATALOG_VERSION}.'\n                ),\n                hint=(\n                    'You need to either recreate the instance and upgrade '\n                    'using dump/restore, or do an inplace upgrade.'\n                )\n            )\n\n        # Check patch count\n        num_patches = await self.get_patch_count(con)\n        if num_patches < len(pg_patches.PATCHES):\n            raise errors.ConfigurationError(\n                'database instance incompatible with this version of Gel',\n                details=f\"expected {len(pg_patches.PATCHES)} patches, \"\n                        f\"but only {num_patches} applied\",\n                hint=\"if you are adding an old backend to a multi-tenant \"\n                     \"server, firstly run a new single-tenant server on \"\n                     \"that backend to apply the patches.\",\n            )\n\n    async def init(self, compat_check: bool = False) -> None:\n        logger.debug(\"starting database introspection\")\n        async with self.use_sys_pgcon() as syscon:\n            if compat_check:\n                await self._check_metaschema_compatibility(syscon)\n            result = await instdata.get_instdata(\n                syscon, 'instancedata', 'json')\n            self._instance_data = immutables.Map(json.loads(result))\n            await self._fetch_roles(syscon)\n            if self._server.get_compiler_pool() is None:\n                # Parse global schema in I/O process if this is done only once\n                logger.debug(\"parsing global schema locally\")\n                global_schema_pickle = pickle.dumps(\n                    await self._server.introspect_global_schema(syscon), -1\n                )\n                data = None\n            else:\n                # Multi-tenant server defers the parsing into the compiler\n                data = await self._server.introspect_global_schema_json(syscon)\n                compiler_pool = self._server.get_compiler_pool()\n\n            default_database = await instdata.get_instdata(\n                syscon, 'default_branch', 'text')\n            if default_database:\n                self.default_database = default_database.decode('utf-8')\n\n        if data is not None:\n            logger.debug(\"parsing global schema\")\n            global_schema_pickle = (\n                await compiler_pool.parse_global_schema(data)\n            )\n\n        logger.info(\"loading system config\")\n        sys_config = await self._load_sys_config()\n        default_sysconfig = await self._load_sys_config(\"sysconfig_default\")\n        await self._load_reported_config()\n\n        # To make in-place upgrade failures more testable, check\n        # 'force_database_error' with a 'startup' scope.\n        force_error = self._server.config_lookup(\n            'force_database_error', sys_config)\n        edbcompiler.maybe_force_database_error(force_error, scope='startup')\n\n        self._dbindex = dbview.DatabaseIndex(\n            self,\n            std_schema=self._server.get_std_schema(),\n            global_schema_pickle=global_schema_pickle,\n            sys_config=sys_config,\n            default_sysconfig=default_sysconfig,\n            sys_config_spec=self._server.config_settings,\n        )\n\n        await self._introspect_dbs()\n\n        await self.load_extension_packages(buildmeta.get_extension_dir_path())\n        # Allow user-specified too.\n        for dir in self._extensions_dirs:\n            await self.load_extension_packages(dir)\n\n        # Now, once all DBs have been introspected, start listening on\n        # any notifications about schema/roles/etc changes.\n        assert self.__sys_pgcon is not None\n        await self.__sys_pgcon.listen_for_sysevent()\n        self.__sys_pgcon.mark_as_system_db()\n        self._sys_pgcon_ready_evt.set()\n\n        self.populate_sys_auth()\n        self.reload_readiness_state()\n        self._initing = False\n\n    async def load_extension_packages(self, path: pathlib.Path) -> None:\n        exts = []\n        if self._is_extension_package(path):\n            exts.append(path)\n        else:\n            try:\n                with os.scandir(path) as it:\n                    for entry in it:\n                        if (\n                            entry.is_dir()\n                            and self._is_extension_package(entry)\n                        ):\n                            exts.append(pathlib.Path(entry))\n            except FileNotFoundError:\n                pass\n\n        if not exts:\n            return\n\n        async with self.use_sys_pgcon() as syscon:\n            from edb.pgsql import trampoline\n            ext_packages_json = await syscon.sql_fetch_val(\n                trampoline.fixup_query(\"\"\"\n                    SELECT json_agg(o.c)\n                    FROM (\n                        SELECT\n                            json_build_array(p.name, p.version) AS c\n                        FROM\n                            edgedb_VER.\"_SysExtensionPackage\" AS p\n                    ) AS o;\n                \"\"\").encode('utf-8')\n            )\n        ext_packages = {\n            (name, verutils.from_json(version))\n            for name, version in json.loads(ext_packages_json)\n        }\n\n        for ext in exts:\n            await self._load_extension_package(ext, ext_packages)\n\n    def _is_extension_package(self, path: pathlib.Path | os.DirEntry) -> bool:\n        return (pathlib.Path(path) / 'MANIFEST.toml').exists()\n\n    async def _load_extension_package(\n        self,\n        path: pathlib.Path,\n        ext_packages: set[tuple[str, verutils.Version]],\n    ) -> None:\n        with open(path / 'MANIFEST.toml', 'rb') as m:\n            manifest = tomllib.load(m)\n\n        name = manifest['name']\n        version = verutils.parse_version(manifest['version'])\n        if (name, version) in ext_packages:\n            logger.info(\n                f\"Extension package '{manifest['name']}' {version} \"\n                f\"already installed\"\n            )\n\n            return\n\n        scripts = []\n        for file in manifest['files']:\n            with open(path / file, 'rb') as f:\n                scripts.append(f.read().decode('utf-8'))\n\n        from edb.schema import schema as s_schema\n\n        async with self.use_sys_pgcon() as syscon:\n            global_schema = await self._server.introspect_global_schema(syscon)\n        compiler = edbcompiler.new_compiler(\n            std_schema=self._server._std_schema,\n            reflection_schema=self._server._refl_schema,\n            schema_class_layout=self._server._schema_class_layout,\n        )\n        compilerctx = edbcompiler.new_compiler_context(\n            compiler_state=compiler.state,\n            global_schema=global_schema,\n            user_schema=s_schema.EMPTY_SCHEMA,\n            internal_schema_mode=True,\n            # Extension installation only works if stdmode or testmode is\n            # set.  Force testmode to be set, since we don't want to set\n            # stdmode, because we want any externally loaded extensions to\n            # be marked as *not* builtin.\n            force_testmode=True,\n        )\n\n        script = '\\n'.join(scripts)\n        _, sql_script = edbcompiler.compile_edgeql_script(compilerctx, script)\n        logger.info(\n            f\"Installing extension package '{manifest['name']}'\")\n        async with self.use_sys_pgcon() as syscon:\n            await syscon.sql_execute(sql_script.encode('utf-8'))\n            global_schema = await self._server.introspect_global_schema(syscon)\n\n        assert self._dbindex\n        self._dbindex.update_global_schema(pickle.dumps(global_schema))\n\n    def start_watching_files(self):\n        if self._readiness_state_file is not None:\n\n            def reload_state_file():\n                self.reload_readiness_state()\n\n            self._file_watch_finalizers.append(\n                self._server.monitor_fs(\n                    self._readiness_state_file, reload_state_file\n                )\n            )\n\n        if self._jwt_sub_allowlist_file is not None:\n\n            def reload_jwt_sub_allowlist_file():\n                self.load_jwt_sub_allowlist()\n\n            self._file_watch_finalizers.append(\n                self._server.monitor_fs(\n                    self._jwt_sub_allowlist_file, reload_jwt_sub_allowlist_file\n                )\n            )\n\n        if self._jwt_revocation_list_file is not None:\n\n            def reload_jwt_revocation_list_file():\n                self.load_jwt_revocation_list()\n\n            self._file_watch_finalizers.append(\n                self._server.monitor_fs(\n                    self._jwt_revocation_list_file,\n                    reload_jwt_revocation_list_file,\n                )\n            )\n\n        if self._config_file is not None:\n\n            def reload_config_file():\n                self.reload_config_file.schedule()\n\n            self._file_watch_finalizers.append(\n                self.server.monitor_fs(self._config_file, reload_config_file)\n            )\n\n    async def start_accepting_new_tasks(self) -> None:\n        assert self._task_group is None\n        self._task_group = asyncio.TaskGroup()\n        await self._task_group.__aenter__()\n        self._accept_new_tasks = True\n        await self._cluster.start_watching(self.on_switch_over)\n\n    def start_running(self) -> None:\n        self._running = True\n        self._accepting_connections = True\n        assert self._dbindex is not None\n        for db in self._dbindex.iter_dbs():\n            db.start_stop_extensions()\n\n    def stop_accepting_connections(self) -> None:\n        self._accepting_connections = False\n\n    @property\n    def accept_new_tasks(self):\n        return self._accept_new_tasks\n\n    def is_db_ready(self, dbname: str) -> bool:\n        if not self._accept_new_tasks:\n            return False\n\n        if (\n            not (db := self.maybe_get_db(dbname=dbname))\n            or not db.is_introspected()\n        ):\n            return False\n\n        return True\n\n    def create_task(\n        self,\n        coro: Coroutine,\n        *,\n        interruptable: bool,\n        name: Optional[str] = None,\n    ) -> asyncio.Task:\n        # Interruptable tasks are regular asyncio tasks that may be interrupted\n        # randomly in the middle when the event loop stops; while tasks with\n        # interruptable=False are always awaited before the server stops, so\n        # that e.g. all finally blocks get a chance to execute in those tasks.\n        # Therefore, it is an error trying to create a task while the server is\n        # not expecting one, so always couple the call with an additional check\n        if self._accept_new_tasks and self._task_group is not None:\n            current_tenant.set(self.get_instance_name())\n            if interruptable:\n                rv = self.__loop.create_task(coro, name=name)\n            else:\n                rv = self._task_group.create_task(coro, name=name)\n\n            # Keep a strong reference of the created Task\n            if name is not None:\n                if name in self._named_tasks:\n                    raise RuntimeError(\n                        f\"task {name!r} already exists on on this server\")\n                self._named_tasks[name] = rv\n                rv.add_done_callback(\n                    lambda task: self._named_tasks.pop(task.get_name(), None))\n            else:\n                self._tasks.add(rv)\n                rv.add_done_callback(self._tasks.discard)\n\n            return rv\n        else:\n            # Hint: add `if tenant.accept_new_tasks` before `.create_task()`\n            raise RuntimeError(\"task cannot be created at this time\")\n\n    def get_task(self, name: str) -> Optional[asyncio.Task]:\n        return self._named_tasks.get(name)\n\n    def stop(self) -> None:\n        self._running = False\n        self._accept_new_tasks = False\n        self._cluster.stop_watching()\n        self._stop_watching_files()\n        self._server.request_frontend_stop(self)\n\n    def _stop_watching_files(self):\n        while self._file_watch_finalizers:\n            self._file_watch_finalizers.pop()()\n\n    async def wait_stopped(self) -> None:\n        if self._task_group is not None:\n            tg = self._task_group\n            self._task_group = None\n            await tg.__aexit__(*sys.exc_info())\n        await self._pg_pool.close()\n\n    def terminate_sys_pgcon(self) -> None:\n        if self.__sys_pgcon is not None:\n            self.__sys_pgcon.terminate()\n            self.__sys_pgcon = None\n        del self._sys_pgcon_waiter\n\n    def set_init_con_data(self, data: list[config.ConState]) -> None:\n        self._init_con_data = data\n        self._init_con_sql = None\n        if data:\n            self._init_con_sql = self._make_init_con_sql(data)\n\n    def _make_init_con_sql(self, data: list[config.ConState]) -> bytes:\n        if not data:\n            return b\"\"\n\n        from edb.pgsql import common\n\n        quoted_json = common.quote_literal(json.dumps(data))\n        return textwrap.dedent(\n            f'''\n                INSERT INTO _edgecon_state\n                    SELECT * FROM jsonb_to_recordset({quoted_json}::jsonb)\n                        AS cfg(name text, value jsonb, type text);\n            '''\n        ).strip().encode()\n\n    async def _pg_connect(\n        self,\n        dbname: str,\n        source_description: str=\"pool connection\"\n    ) -> pgcon.PGConnection:\n        ha_serial = self._ha_master_serial\n        if self.get_backend_runtime_params().has_create_database:\n            pg_dbname = self.get_pg_dbname(dbname)\n        else:\n            pg_dbname = self.get_pg_dbname(defines.EDGEDB_SUPERUSER_DB)\n        started_at = time.monotonic()\n        try:\n            rv = await self._cluster.connect(\n                source_description=source_description,\n                database=pg_dbname,\n                apply_init_script=True\n            )\n            if self._server.stmt_cache_size is not None:\n                rv.set_stmt_cache_size(self._server.stmt_cache_size)\n\n            if self._init_con_sql:\n                await rv.sql_execute(self._init_con_sql)\n            rv.last_init_con_data = self._init_con_data\n\n        except Exception:\n            metrics.backend_connection_establishment_errors.inc(\n                1.0, self._instance_name\n            )\n            raise\n        finally:\n            metrics.backend_connection_establishment_latency.observe(\n                time.monotonic() - started_at, self._instance_name\n            )\n        if ha_serial == self._ha_master_serial:\n            rv.set_tenant(self)\n            if self._backend_adaptive_ha is not None:\n                self._backend_adaptive_ha.on_pgcon_made(\n                    dbname == defines.EDGEDB_SYSTEM_DB\n                )\n            metrics.total_backend_connections.inc(1.0, self._instance_name)\n            metrics.current_backend_connections.inc(1.0, self._instance_name)\n            return rv\n        else:\n            rv.terminate()\n            raise ConnectionError(\"connected to outdated Postgres master\")\n\n    async def _pg_disconnect(self, conn: pgcon.PGConnection) -> None:\n        metrics.current_backend_connections.dec(1.0, self._instance_name)\n        conn.terminate()\n\n    def get_introspection_lock(\n        self,\n        dbname: str,\n    ) -> asyncio.Lock:\n        lock = self._introspection_locks.get(dbname)\n        if not lock:\n            self._introspection_locks[dbname] = lock = asyncio.Lock()\n        return lock\n\n    @contextlib.asynccontextmanager\n    async def direct_pgcon(\n        self,\n        dbname: str,\n    ) -> AsyncGenerator[pgcon.PGConnection, None]:\n        conn = None\n        try:\n            conn = await self._pg_connect(\n                dbname,\n                source_description=\"direct_pgcon\"\n            )\n            yield conn\n        finally:\n            if conn is not None:\n                await self._pg_disconnect(conn)\n\n    @contextlib.asynccontextmanager\n    async def use_sys_pgcon(self) -> AsyncGenerator[pgcon.PGConnection, None]:\n        if not self._initing and not self._running:\n            raise RuntimeError(\"Gel server is not running.\")\n\n        await self._sys_pgcon_waiter.acquire()\n\n        if not self._initing and not self._running:\n            self._sys_pgcon_waiter.release()\n            raise RuntimeError(\"Gel server is not running.\")\n\n        if self.__sys_pgcon is None or not self.__sys_pgcon.is_healthy():\n            conn, self.__sys_pgcon = self.__sys_pgcon, None\n            if conn is not None:\n                self._sys_pgcon_ready_evt.clear()\n                conn.abort()\n            # We depend on the reconnect on connection_lost() of __sys_pgcon\n            await self._sys_pgcon_ready_evt.wait()\n            if self.__sys_pgcon is None:\n                self._sys_pgcon_waiter.release()\n                raise RuntimeError(\"Cannot acquire pgcon to the system DB.\")\n\n        try:\n            yield self.__sys_pgcon\n        finally:\n            if self.__sys_pgcon is not None and self.__sys_pgcon.is_healthy():\n                self._sys_pgcon_last_active_time = time.monotonic()\n            self._sys_pgcon_waiter.release()\n\n    def set_stmt_cache_size(self, size: int) -> None:\n        for conn in self._pg_pool.iterate_connections():\n            conn.set_stmt_cache_size(size)\n\n    def on_sys_pgcon_parameter_status_updated(\n        self,\n        name: str,\n        value: str,\n    ) -> None:\n        try:\n            if name == \"in_hot_standby\" and value == \"on\":\n                # It is a strong evidence of failover if the sys_pgcon receives\n                # a notification that in_hot_standby is turned on.\n                self.on_sys_pgcon_failover_signal()\n        except Exception:\n            metrics.background_errors.inc(\n                1.0,\n                self._instance_name,\n                \"on_sys_pgcon_parameter_status_updated\"\n            )\n            raise\n\n    def on_sys_pgcon_failover_signal(self) -> None:\n        if not self._running:\n            return\n        try:\n            if self._backend_adaptive_ha is not None:\n                # Switch to FAILOVER if adaptive HA is enabled\n                self._backend_adaptive_ha.set_state_failover()\n            elif getattr(self._cluster, \"_ha_backend\", None) is None:\n                # If the server is not using an HA backend, nor has enabled the\n                # adaptive HA monitoring, we still try to \"switch over\" by\n                # disconnecting all pgcons if failover signal is received,\n                # allowing reconnection to happen sooner.\n                self.on_switch_over()\n            # Else, the HA backend should take care of calling on_switch_over()\n        except Exception:\n            metrics.background_errors.inc(\n                1.0, self._instance_name, \"on_sys_pgcon_failover_signal\"\n            )\n            raise\n\n    def on_sys_pgcon_connection_lost(self, exc: Exception | None) -> None:\n        try:\n            if not self._running:\n                # The tenant is shutting down, release all events so that\n                # the waiters if any could continue and exit\n                self._sys_pgcon_ready_evt.set()\n                self._sys_pgcon_reconnect_evt.set()\n                return\n\n            logger.error(\n                \"Connection to the system database is \"\n                + (\"closed.\" if exc is None else f\"broken! Reason: {exc}\")\n            )\n            self.set_pg_unavailable_msg(\n                \"Connection is lost, please check server log for the reason.\"\n            )\n            self.__sys_pgcon = None\n            self._sys_pgcon_ready_evt.clear()\n            if self._accept_new_tasks:\n                self.create_task(\n                    self._reconnect_sys_pgcon(), interruptable=True\n                )\n            self.on_pgcon_broken(True)\n        except Exception:\n            metrics.background_errors.inc(\n                1.0, self._instance_name, \"on_sys_pgcon_connection_lost\"\n            )\n            raise\n\n    async def _reconnect_sys_pgcon(self) -> None:\n        try:\n            conn = None\n            while self._running:\n                # Keep retrying as far as:\n                #   1. This tenant is still running\n                #   2. We still cannot connect to the Postgres cluster\n                try:\n                    conn = await self._pg_connect(\n                        defines.EDGEDB_SYSTEM_DB,\n                        source_description=\"_reconnect_sys_pgcon\"\n                    )\n                    break\n                except OSError:\n                    pass\n                except pgcon_errors.BackendError as e:\n                    # Be quiet if the Postgres cluster is still starting up,\n                    # or the HA failover is still in progress.\n                    # TODO: ERROR_FEATURE_NOT_SUPPORTED should be removed\n                    # once PostgreSQL supports SERIALIZABLE in hot standbys\n                    if not (\n                        e.code_is(pgcon_errors.ERROR_FEATURE_NOT_SUPPORTED)\n                        or e.code_is(pgcon_errors.ERROR_CANNOT_CONNECT_NOW)\n                        or e.code_is(\n                            pgcon_errors.ERROR_READ_ONLY_SQL_TRANSACTION\n                        )\n                    ):\n                        logger.error(\"Failed connecting to the backend: %s\", e)\n\n                if self._running:\n                    logger.info(\"Waiting for the backend to recover\")\n                    try:\n                        # Retry after INTERVAL seconds, unless the event is set\n                        # and we can retry immediately after the event.\n                        await asyncio.wait_for(\n                            self._sys_pgcon_reconnect_evt.wait(),\n                            defines.SYSTEM_DB_RECONNECT_INTERVAL,\n                        )\n                        # But the event can only skip one INTERVAL.\n                        self._sys_pgcon_reconnect_evt.clear()\n                    except asyncio.TimeoutError:\n                        pass\n\n            if not self._running:\n                if conn is not None:\n                    conn.abort()\n                return\n\n            assert conn is not None\n            logger.info(\"Successfully reconnected to the system database.\")\n            self.__sys_pgcon = conn\n            self.__sys_pgcon.mark_as_system_db()\n            self._sys_pgcon_last_active_time = time.monotonic()\n            # This await is meant to be after mark_as_system_db() because we\n            # need the pgcon to be able to trigger another reconnect if its\n            # connection is lost during this await.\n            await self.__sys_pgcon.listen_for_sysevent()\n            self.set_pg_unavailable_msg(None)\n        finally:\n            self._sys_pgcon_ready_evt.set()\n\n    def on_pgcon_broken(self, is_sys_pgcon: bool = False) -> None:\n        try:\n            if self._backend_adaptive_ha:\n                self._backend_adaptive_ha.on_pgcon_broken(is_sys_pgcon)\n        except Exception:\n            metrics.background_errors.inc(\n                1.0, self._instance_name, \"on_pgcon_broken\"\n            )\n            raise\n\n    def on_pgcon_lost(self) -> None:\n        try:\n            if self._backend_adaptive_ha:\n                self._backend_adaptive_ha.on_pgcon_lost()\n        except Exception:\n            metrics.background_errors.inc(\n                1.0, self._instance_name, \"on_pgcon_lost\")\n            raise\n\n    def set_pg_unavailable_msg(self, msg: str | None) -> None:\n        if msg is None or self._pg_unavailable_msg is None:\n            self._pg_unavailable_msg = msg\n\n    @contextlib.asynccontextmanager\n    async def with_pgcon(\n        self, dbname: str, *,\n        discard: bool=False\n    ) -> AsyncGenerator[pgcon.PGConnection, None]:\n        conn = await self.acquire_pgcon(dbname=dbname)\n        try:\n            yield conn\n        finally:\n            self.release_pgcon(dbname, conn, discard=discard)\n\n    async def acquire_pgcon(self, dbname: str) -> pgcon.PGConnection:\n        if self._pg_unavailable_msg is not None:\n            raise errors.BackendUnavailableError(\n                \"Postgres is not available: \" + self._pg_unavailable_msg\n            )\n\n        for _ in range(self._pg_pool.max_capacity):\n            conn = await self._pg_pool.acquire(dbname)\n            if not conn.is_healthy():\n                logger.warning(\"acquired an unhealthy pgcon; discard now\")\n            elif conn.last_init_con_data is not self._init_con_data:\n                try:\n                    await conn.sql_execute(\n                        pgcon.RESET_STATIC_CFG_SCRIPT +\n                        (self._init_con_sql or b'')\n                    )\n                except Exception as e:\n                    logger.warning(\n                        \"failed to update pgcon; discard now: %s\", e\n                    )\n                else:\n                    conn.last_init_con_data = self._init_con_data\n                    return conn\n            else:\n                return conn\n            self._pg_pool.release(dbname, conn, discard=True)\n        else:\n            # This is unlikely to happen, but we defer to the caller to retry\n            # when it does happen\n            raise errors.BackendUnavailableError(\n                \"No healthy backend connection available at the moment, \"\n                \"please try again.\"\n            )\n\n    def release_pgcon(\n        self,\n        dbname: str,\n        conn: pgcon.PGConnection,\n        *,\n        discard: bool = False,\n    ) -> None:\n        if not conn.is_healthy():\n            if not discard:\n                logger.warning(\"Released an unhealthy pgcon; discard now.\")\n            discard = True\n        try:\n            self._pg_pool.release(dbname, conn, discard=discard)\n        except Exception:\n            metrics.background_errors.inc(\n                1.0, self._instance_name, \"release_pgcon\"\n            )\n            raise\n\n    def allow_database_connections(self, dbname: str) -> None:\n        self._block_new_connections.discard(dbname)\n\n    def is_database_connectable(self, dbname: str) -> bool:\n        return (\n            self._running\n            and dbname != defines.EDGEDB_TEMPLATE_DB\n            and dbname not in self._block_new_connections\n        )\n\n    async def ensure_database_not_connected(\n        self, dbname: str, close_frontend_conns: bool = False\n    ) -> None:\n        if self._dbindex and self._dbindex.count_connections(dbname):\n            if close_frontend_conns:\n                self._server.request_stop_fe_conns(dbname)\n            else:\n                # If there are open Gel connections to the `dbname` DB\n                # just raise the error Postgres would have raised itself.\n                raise errors.ExecutionError(\n                    f\"database branch {dbname!r} is being accessed by \"\n                    f\"other users\"\n                )\n\n        self._block_new_connections.add(dbname)\n\n        rloop = retryloop.RetryLoop(\n            timeout=30.0,\n            ignore=errors.ExecutionError,\n        )\n\n        async for iteration in rloop:\n            async with iteration:\n                # Signal adjacent servers to prune their connections to this\n                # database.\n                await self.signal_sysevent(\n                    \"ensure-database-not-used\", dbname=dbname\n                )\n\n                # Prune our inactive connections.  (Do it in the loop\n                # to help in the close_frontend_conns situation.)\n                await self._pg_pool.prune_inactive_connections(dbname)\n\n                await self._pg_ensure_database_not_connected(dbname)\n\n    async def _pg_ensure_database_not_connected(self, dbname: str) -> None:\n        async with self.use_sys_pgcon() as pgcon:\n            conns = await pgcon.sql_fetch_col(\n                b\"\"\"\n                SELECT\n                    row_to_json(pg_stat_activity)\n                FROM\n                    pg_stat_activity\n                WHERE\n                    datname = $1\n                \"\"\",\n                args=[self.get_pg_dbname(dbname).encode(\"utf-8\")],\n            )\n\n        if conns:\n            debug_info = \"\"\n            if self.server.in_dev_mode() or self.server.in_test_mode():\n                jconns = [json.loads(conn) for conn in conns]\n                debug_info = \": \" + json.dumps(jconns)\n\n            raise errors.ExecutionError(\n                f\"database branch {dbname!r} is being accessed by \"\n                f\"other users{debug_info}\"\n            )\n\n    @contextlib.asynccontextmanager\n    async def _with_intro_pgcon(\n        self, dbname: str\n    ) -> AsyncGenerator[pgcon.PGConnection | None, None]:\n        conn = None\n        try:\n            conn = await self.acquire_pgcon(dbname)\n            yield conn\n        except pgcon_errors.BackendError as e:\n            if e.code_is(pgcon_errors.ERROR_INVALID_CATALOG_NAME):\n                # database does not exist (anymore)\n                logger.warning(\n                    \"Detected concurrently-dropped database branch %s; \"\n                    \"skipping.\",\n                    dbname,\n                )\n                if self._dbindex is not None and self._dbindex.has_db(dbname):\n                    self._dbindex.unregister_db(dbname)\n                yield None\n            else:\n                raise\n        finally:\n            if conn:\n                self.release_pgcon(dbname, conn)\n\n    async def _introspect_extensions(\n        self, conn: pgcon.PGConnection\n    ) -> set[str]:\n        from edb.pgsql import trampoline\n        extension_names_json = await conn.sql_fetch_val(\n            trampoline.fixup_query(\"\"\"\n                SELECT json_agg(name) FROM edgedb_VER.\"_SchemaExtension\";\n            \"\"\").encode('utf-8'),\n        )\n        if extension_names_json:\n            extensions = set(json.loads(extension_names_json))\n        else:\n            extensions = set()\n\n        return extensions\n\n    async def _debug_introspect(\n        self,\n        conn: pgcon.PGConnection,\n        global_schema_pickle,\n    ) -> Any:\n        user_schema_json = (\n            await self._server.introspect_user_schema_json(conn)\n        )\n        db_config_json = await self._server.introspect_db_config(conn)\n\n        compiler_pool = self._server.get_compiler_pool()\n        return (await compiler_pool.parse_user_schema_db_config(\n            user_schema_json, db_config_json, global_schema_pickle,\n        )).user_schema_pickle\n\n    async def introspect_db(\n        self,\n        dbname: str,\n        *,\n        conn: Optional[pgcon.PGConnection]=None,\n        reintrospection: bool=False,\n    ) -> None:\n        \"\"\"Use this method to (re-)introspect a DB.\n\n        If the DB is already registered in self._dbindex, its\n        schema, config, etc. would simply be updated. If it's missing\n        an entry for it would be created.\n\n        All remote notifications of remote events should use this method\n        to refresh the state. Even if the remote event was a simple config\n        change, a lot of other events could happen before it was sent to us\n        by a remote server and us receiving it. E.g. a DB could have been\n        dropped and recreated again. It's safer to refresh the entire state\n        than refreshing individual components of it. Besides, DDL and\n        database-level config modifications are supposed to be rare events.\n\n        This supports passing in a connection to use as well, so that\n        we can synchronously introspect on config changes without\n        risking deadlock by acquiring two connections at once.\n\n        Returns True if the query cache mode changed.\n\n        \"\"\"\n        cm = (\n            contextlib.nullcontext(conn) if conn\n            else self._with_intro_pgcon(dbname)\n        )\n\n        async with cm as conn:\n            if not conn:\n                return\n            # Acquire a per-db lock for doing the introspection, to avoid\n            # race conditions where an older introspection might overwrite\n            # a newer one.\n            async with self.get_introspection_lock(dbname):\n                await self._introspect_db(\n                    dbname, conn=conn, reintrospection=reintrospection\n                )\n\n    async def _introspect_db(\n        self,\n        dbname: str,\n        conn: pgcon.PGConnection,\n        reintrospection: bool,\n    ) -> None:\n        from edb.pgsql import trampoline\n        logger.info(\"introspecting database '%s'\", dbname)\n\n        assert self._dbindex is not None\n        if db := self._dbindex.maybe_get_db(dbname):\n            cache_mode_val = db.lookup_config('query_cache_mode')\n        else:\n            cache_mode_val = self._dbindex.lookup_config('query_cache_mode')\n        old_cache_mode = config.QueryCacheMode.effective(cache_mode_val)\n\n        # Introspection\n        user_schema_json = (\n            await self._server.introspect_user_schema_json(conn)\n        )\n\n        reflection_cache_json = await conn.sql_fetch_val(\n            trampoline.fixup_query(\"\"\"\n                SELECT json_agg(o.c)\n                FROM (\n                    SELECT\n                        json_build_object(\n                            'eql_hash', t.eql_hash,\n                            'argnames', array_to_json(t.argnames)\n                        ) AS c\n                    FROM\n                        ROWS FROM(edgedb_VER._get_cached_reflection())\n                            AS t(eql_hash text, argnames text[])\n                ) AS o;\n            \"\"\").encode('utf-8'),\n        )\n\n        reflection_cache = immutables.Map(\n            {\n                r[\"eql_hash\"]: tuple(r[\"argnames\"])\n                for r in json.loads(reflection_cache_json)\n            }\n        )\n\n        backend_ids_json = await conn.sql_fetch_val(\n            trampoline.fixup_query(\"\"\"\n            SELECT\n                json_object_agg(\n                    \"id\"::text,\n                    json_build_array(\"backend_id\", \"name\")\n                )::text\n            FROM\n                edgedb_VER.\"_SchemaType\"\n            \"\"\").encode('utf-8'),\n        )\n        backend_ids = json.loads(backend_ids_json)\n\n        db_config_json = await self._server.introspect_db_config(conn)\n\n        extensions = await self._introspect_extensions(conn)\n\n        query_cache: list[tuple[bytes, ...]] | None = None\n        if (\n            not reintrospection\n            and old_cache_mode is not config.QueryCacheMode.InMemory\n        ):\n            query_cache = await self._load_query_cache(conn)\n\n        # Analysis\n        compiler_pool = self._server.get_compiler_pool()\n        parsed_db = await compiler_pool.parse_user_schema_db_config(\n            user_schema_json, db_config_json, self.get_global_schema_pickle()\n        )\n        db = self._dbindex.register_db(\n            dbname,\n            user_schema_pickle=parsed_db.user_schema_pickle,\n            schema_version=parsed_db.schema_version,\n            db_config=parsed_db.database_config,\n            reflection_cache=reflection_cache,\n            backend_ids=backend_ids,\n            extensions=extensions,\n            ext_config_settings=parsed_db.ext_config_settings,\n            feature_used_metrics=parsed_db.feature_used_metrics,\n        )\n        db.set_state_serializer(\n            parsed_db.protocol_version,\n            parsed_db.state_serializer,\n        )\n        cache_mode = config.QueryCacheMode.effective(\n            db.lookup_config('query_cache_mode')\n        )\n\n        if query_cache and cache_mode is not config.QueryCacheMode.InMemory:\n            db.hydrate_cache(query_cache)\n        elif old_cache_mode is not cache_mode:\n            logger.info(\n                \"clearing query cache for database '%s'\", dbname)\n            await conn.sql_execute(\n                b'SELECT edgedb._clear_query_cache()')\n            assert self._dbindex\n            self._dbindex.get_db(dbname).clear_query_cache()\n\n    async def _early_introspect_db(self, dbname: str) -> None:\n        \"\"\"We need to always introspect the extensions for each database.\n\n        Otherwise, we won't know to accept connections for graphql or\n        http, for example, until a native connection is made.\n        \"\"\"\n        current_tenant.set(self.get_instance_name())\n        logger.info(\"introspecting extensions for database '%s'\", dbname)\n\n        async with self._with_intro_pgcon(dbname) as conn:\n            if not conn:\n                return\n            assert self._dbindex is not None\n            if not self._dbindex.has_db(dbname):\n                extensions = await self._introspect_extensions(conn)\n                # Re-check in case we have a concurrent introspection task.\n                if not self._dbindex.has_db(dbname):\n                    self._dbindex.register_db(\n                        dbname,\n                        user_schema_pickle=None,\n                        schema_version=None,\n                        db_config=None,\n                        reflection_cache=None,\n                        backend_ids=None,\n                        extensions=extensions,\n                        ext_config_settings=None,\n                        early=True,\n                    )\n\n        # Early introspection runs *before* we start accepting tasks.\n        # This means that if we are one of multiple frontends, and we\n        # get a ensure-database-not-used message, we aren't able to\n        # handle it. This can result in us hanging onto a connection\n        # that another frontend wants to get rid of.\n        #\n        # We still want to use the pool, though, since it limits our\n        # connections in the way we want.\n        #\n        # Hack around this by pruning the connection ourself.\n        await self._pg_pool.prune_inactive_connections(dbname)\n\n    async def _introspect_dbs(self) -> None:\n        async with self.use_sys_pgcon() as syscon:\n            dbnames = await self._server.get_dbnames(syscon)\n\n        async with asyncio.TaskGroup() as g:\n            for dbname in dbnames:\n                # There's a risk of the DB being dropped by another server\n                # between us building the list of databases and loading\n                # information about them.\n                g.create_task(self._early_introspect_db(dbname))\n\n    async def _load_reported_config(self) -> None:\n        async with self.use_sys_pgcon() as syscon:\n            try:\n                data = await syscon.sql_fetch_val(\n                    self._server.get_sys_query(\"report_configs\"),\n                    use_prep_stmt=True,\n                    state=b'[]',  # clear _config_cache\n                )\n\n                for (\n                    protocol_ver,\n                    typedesc,\n                ) in self._server.get_report_config_typedesc().items():\n                    self._report_config_data[protocol_ver] = (\n                        struct.pack(\"!L\", len(typedesc))\n                        + typedesc\n                        + struct.pack(\"!L\", len(data))\n                        + data\n                    )\n            except Exception:\n                metrics.background_errors.inc(\n                    1.0, self._instance_name, \"load_reported_config\"\n                )\n                raise\n\n    async def _load_sys_config(\n        self,\n        query_name: str = \"sysconfig\",\n        syscon: pgcon.PGConnection | None = None,\n    ) -> Mapping[str, config.SettingValue]:\n        query = self._server.get_sys_query(query_name)\n        if syscon is None:\n            async with self.use_sys_pgcon() as syscon:\n                sys_config_json = await syscon.sql_fetch_val(query)\n        else:\n            sys_config_json = await syscon.sql_fetch_val(query)\n\n        return config.from_json(self._server.config_settings, sys_config_json)\n\n    async def _reintrospect_global_schema(self) -> None:\n        if not self._initing and not self._running:\n            logger.warning(\n                \"global-schema-changes event received during shutdown; \"\n                \"ignoring.\"\n            )\n            return\n        async with self.use_sys_pgcon() as syscon:\n            data = await self._server.introspect_global_schema_json(syscon)\n            await self._fetch_roles(syscon)\n        compiler_pool = self._server.get_compiler_pool()\n        global_schema_pickle = await compiler_pool.parse_global_schema(data)\n        assert self._dbindex is not None\n        self._dbindex.update_global_schema(global_schema_pickle)\n\n    def populate_sys_auth(self) -> None:\n        assert self._dbindex is not None\n        cfg = self._dbindex.get_sys_config()\n        auth = self._server.config_lookup(\"auth\", cfg) or ()\n        self._sys_auth = tuple(sorted(auth, key=lambda a: a.priority))\n\n    def resolve_branch_name(\n        self, database: str | None, branch: str | None\n    ) -> str:\n        default = self.default_database\n        if branch == '__default__':\n            return default\n        elif branch is not None:\n            return branch\n        elif (\n            database == defines.EDGEDB_OLD_DEFAULT_DB\n            and self.maybe_get_db(dbname=defines.EDGEDB_OLD_DEFAULT_DB) is None\n        ):\n            return default\n        else:\n            assert database is not None\n            return database\n\n    def resolve_user_name(self, user: str) -> str:\n        if (\n            user == defines.EDGEDB_OLD_SUPERUSER\n            and user not in self.get_roles()\n        ):\n            return defines.EDGEDB_SUPERUSER\n        else:\n            return user\n\n    async def get_auth_methods(\n        self,\n        user: str,\n        transport: srvargs.ServerConnTransport,\n    ) -> list[config.CompositeConfigType]:\n        authlist = self._sys_auth\n        methods = []\n\n        if authlist:\n            for auth in authlist:\n                match = (user in auth.user or \"*\" in auth.user) and (\n                    not auth.method.transports\n                    or transport in auth.method.transports\n                )\n\n                if match:\n                    methods.append(auth.method)\n                    break\n\n        if not methods:\n            methods = self._server.get_default_auth_methods(transport)\n\n        return methods\n\n    async def new_dbview(\n        self,\n        *,\n        dbname: str,\n        query_cache: bool,\n        protocol_version: defines.ProtocolVersion,\n        role_name: str,\n    ) -> dbview.DatabaseConnectionView:\n        db = self.get_db(dbname=dbname)\n        await db.introspection()\n        assert self._dbindex is not None\n        return self._dbindex.new_view(\n            dbname,\n            query_cache=query_cache,\n            protocol_version=protocol_version,\n            role_name=role_name,\n        )\n\n    def remove_dbview(self, dbview_: dbview.DatabaseConnectionView) -> None:\n        assert self._dbindex is not None\n        return self._dbindex.remove_view(dbview_)\n\n    def schedule_reported_config_if_needed(self, setting_name: str) -> None:\n        setting = self._server.config_settings.get(setting_name)\n        if setting and setting.report and self._accept_new_tasks:\n            self.create_task(self._load_reported_config(), interruptable=True)\n\n    def load_jwcrypto(self, jwk_key: auth.JWKSet) -> None:\n        self._jws_key = jwk_key\n        self.load_jwt_sub_allowlist()\n        self.load_jwt_revocation_list()\n\n    def load_jwt_sub_allowlist(self) -> None:\n        if self._jwt_sub_allowlist_file is not None:\n            logger.info(\n                \"(re-)loading JWT subject allowlist from \"\n                f'\"{self._jwt_sub_allowlist_file}\"'\n            )\n            try:\n                self._jwt_sub_allowlist = frozenset(\n                    self._jwt_sub_allowlist_file.read_text().splitlines(),\n                )\n                if self._jws_key is not None:\n                    self._jws_key.default_validation_context.allow(\n                        \"sub\", self._jwt_sub_allowlist\n                    )\n                else:\n                    from . import server as edbserver\n                    raise edbserver.StartupError(\n                        \"cannot load JWT sub allowlist: no secret key\"\n                    )\n            except Exception as e:\n                from . import server as edbserver\n\n                raise edbserver.StartupError(\n                    f\"cannot load JWT sub allowlist: {e}\"\n                ) from e\n\n    def load_jwt_revocation_list(self) -> None:\n        if self._jwt_revocation_list_file is not None:\n            logger.info(\n                \"(re-)loading JWT revocation list from \"\n                f'\"{self._jwt_revocation_list_file}\"'\n            )\n            try:\n                self._jwt_revocation_list = frozenset(\n                    self._jwt_revocation_list_file.read_text().splitlines(),\n                )\n                if self._jws_key is not None:\n                    self._jws_key.default_validation_context.deny(\n                        \"jti\", self._jwt_revocation_list\n                    )\n                else:\n                    from . import server as edbserver\n                    raise edbserver.StartupError(\n                        \"cannot load JWT revocation list: no secret key\"\n                    )\n            except Exception as e:\n                from . import server as edbserver\n\n                raise edbserver.StartupError(\n                    f\"cannot load JWT revocation list: {e}\"\n                ) from e\n\n    def reload_readiness_state(self) -> None:\n        if self._readiness_state_file is None:\n            return\n        try:\n            with self._readiness_state_file.open(\"rt\") as rt:\n                line = rt.readline().strip()\n                try:\n                    state, _, reason = line.partition(\":\")\n                    self._readiness = srvargs.ReadinessState(state)\n                    self._readiness_reason = reason\n                    logger.info(\n                        \"readiness state file changed, \"\n                        \"setting server readiness to %r%s\",\n                        state,\n                        f\" ({reason})\" if reason else \"\",\n                    )\n                except ValueError:\n                    logger.warning(\n                        \"invalid state in readiness state file (%r): %r, \"\n                        \"resetting server readiness to 'default'\",\n                        self._readiness_state_file,\n                        state,\n                    )\n                    self._readiness = srvargs.ReadinessState.Default\n\n        except FileNotFoundError:\n            logger.info(\n                \"readiness state file (%s) removed, resetting \"\n                \"server readiness to 'default'\",\n                self._readiness_state_file,\n            )\n            self._readiness = srvargs.ReadinessState.Default\n\n        except Exception as e:\n            logger.warning(\n                \"cannot read readiness state file (%s): %s, \"\n                \"resetting server readiness to 'default'\",\n                self._readiness_state_file,\n                e,\n            )\n            self._readiness = srvargs.ReadinessState.Default\n\n        self._accepting_connections = self.is_online()\n\n    def set_readiness_state(self, state: srvargs.ReadinessState, reason: str):\n        self._readiness = state\n        self._readiness_reason = reason\n\n    @asyncutil.exclusive_task\n    async def reload_config_file(self):\n        if self._config_file is None:\n            return\n\n        try:\n            await self._reload_config_file()\n        except Exception:\n            logger.error(\"failed to reload config file\", exc_info=True)\n            metrics.background_errors.inc(\n                1.0, self._instance_name, \"reload_config_file\"\n            )\n\n    async def load_config_file(self, compiler):\n        logger.info(\"loading config file\")\n\n        # Read the TOML file\n        with self._config_file.open('rb') as f:\n            toml_data = tomllib.load(f)\n\n        # Handle special case for `magic_smtp_config`\n        magic_smtp_config = toml_data.pop(\"magic_smtp_config\", None)\n        if magic_smtp_config:\n            await self.load_sidechannel_configs(\n                magic_smtp_config, compiler=compiler\n            )\n\n        # Parse TOML config file content into JSON\n        if toml_data and toml_data.get(\"cfg::Config\"):\n            result = compiler.compile_structured_config(\n                toml_data, \"configuration file\"\n            )\n            if asyncio.iscoroutine(result):\n                result = await result\n\n            def setting_filter(value: config.SettingValue) -> bool:\n                if self._server.config_settings[value.name].backend_setting:\n                    raise errors.ConfigurationError(\n                        f\"backend config {value.name!r} cannot be set \"\n                        f\"via config file\"\n                    )\n                return True\n\n            json_obj = config.to_json_obj(\n                self._server.config_settings,\n                result[\"cfg::Config\"],\n                include_source=False,\n                setting_filter=setting_filter,\n            )\n            config_file_data = [\n                {\n                    \"name\": name,\n                    \"value\": value,\n                    \"type\": config.ConStateType.config_file,\n                }\n                for name, value in json_obj.items()\n            ]\n        else:\n            config_file_data = []\n\n        # Update init_con_data and SQL\n        self.set_init_con_data(\n            [\n                cs\n                for cs in self._init_con_data\n                if cs[\"type\"] != config.ConStateType.config_file\n            ]\n            + config_file_data\n        )\n\n    async def _reload_config_file(self):\n        # Load TOML config file\n        compiler = self._server.get_compiler_pool()\n        await self.load_config_file(compiler)\n\n        # Update sys pgcon and reload system config\n        async with self.use_sys_pgcon() as syscon:\n            if syscon.last_init_con_data is not self._init_con_data:\n                await syscon.sql_execute(\n                    pgcon.RESET_STATIC_CFG_SCRIPT + (self._init_con_sql or b'')\n                )\n                syscon.last_init_con_data = self._init_con_data\n            sys_config = await self._load_sys_config(syscon=syscon)\n            # GOTCHA: no need to notify other EdgeDBs on the same backend about\n            # such change to sysconfig, because static config is instance-local\n        self._dbindex.update_sys_config(sys_config)\n\n    def reload(self):\n        # In multi-tenant mode, the file paths for the following states may be\n        # unset in a reload, while it's impossible in a regular server.\n        # Therefore, we are clearing the states here first, rather than doing\n        # so in reload_readiness_state() or load_jwcrypto().\n        self._readiness = srvargs.ReadinessState.Default\n        self._jwt_sub_allowlist = None\n        self._jwt_revocation_list = None\n\n        # Re-add the fs watchers in case the path changed\n        self._stop_watching_files()\n\n        self.reload_readiness_state()\n        self.load_jwcrypto()\n        self.reload_config_file.schedule()\n\n        self.start_watching_files()\n\n    async def on_before_drop_db(\n        self,\n        dbname: str,\n        current_dbname: str,\n        close_frontend_conns: bool = False,\n    ) -> None:\n        if current_dbname == dbname:\n            raise errors.ExecutionError(\n                f\"cannot drop the currently open database branch {dbname!r}\"\n            )\n\n        await self.ensure_database_not_connected(\n            dbname, close_frontend_conns=close_frontend_conns\n        )\n\n    async def on_before_create_db_from_template(\n        self, dbname: str, current_dbname: str, mode: str\n    ) -> None:\n        # Make sure the database exists.\n        # TODO: Is it worth producing a nicer error message if it\n        # fails on the backside? (Because of a race?)\n        self.get_db(dbname=dbname)\n\n        if mode == 'TEMPLATE':\n            await self.ensure_database_not_connected(dbname)\n\n    async def on_after_create_db_from_template(\n        self, tgt_dbname: str, src_dbname: str, mode: str\n    ) -> None:\n        if mode == 'TEMPLATE':\n            self.allow_database_connections(tgt_dbname)\n            return\n\n        logger.info('Starting copy from %s to %s', src_dbname, tgt_dbname)\n        from edb.pgsql import common\n        from . import bootstrap  # noqa: F402\n\n        real_tgt_dbname = common.get_database_backend_name(\n            tgt_dbname, tenant_id=self._tenant_id)\n        real_src_dbname = common.get_database_backend_name(\n            src_dbname, tenant_id=self._tenant_id)\n\n        # HACK: Limit the maximum number of in-flight branch\n        # creations. This is because branches use up to 3 concurrent\n        # connections (one direct, two via pg_dump/pg_restore), and so\n        # it can substantially blow our budget if many are in flight.\n        # The right way to handle this issue would probably be to use\n        # the connection pool to reserve the connections, but we would\n        # need to carefully consider deadlock concerns if we want to\n        # allow tasks to acquire multiple pool connections.\n        async with self._branch_sem:\n            async with self.direct_pgcon(tgt_dbname) as con:\n                await bootstrap.create_branch(\n                    self._cluster,\n                    self._server._refl_schema,\n                    con,\n                    real_src_dbname,\n                    real_tgt_dbname,\n                    mode,\n                    self._server._sys_queries['backend_id_fixup'],\n                )\n\n        logger.info('Finished copy from %s to %s', src_dbname, tgt_dbname)\n\n    def on_after_drop_db(self, dbname: str) -> None:\n        try:\n            assert self._dbindex is not None\n            if self._dbindex.has_db(dbname):\n                self._dbindex.unregister_db(dbname)\n            self._block_new_connections.discard(dbname)\n        except Exception:\n            metrics.background_errors.inc(\n                1.0, self._instance_name, \"on_after_drop_db\"\n            )\n            raise\n\n    async def ping_backend(self) -> bool:\n        if not self._running:\n            return False\n        elapsed = time.monotonic() - self._sys_pgcon_last_active_time\n        if elapsed > HEALTH_CHECK_MIN_INTERVAL:\n            async with asyncio.timeout(HEALTH_CHECK_TIMEOUT):\n                async with self.use_sys_pgcon() as syscon:\n                    await syscon.sql_fetch_val(b\"select 'OK'\")\n        return True\n\n    async def cancel_pgcon_operation(self, con: pgcon.PGConnection) -> bool:\n        async with self.use_sys_pgcon() as syscon:\n            if con.idle:\n                # con could have received the query results while we\n                # were acquiring a system connection to cancel it.\n                return False\n\n            if con.is_cancelling():\n                # Somehow the connection is already being cancelled and\n                # we don't want to have to cancellations go in parallel.\n                return False\n\n            con.start_pg_cancellation()\n            try:\n                # Returns True if the `pid` exists and it was able to send it a\n                # SIGINT.  Will throw an exception if the privileges aren't\n                # sufficient.\n                result = await syscon.sql_fetch_val(\n                    f\"SELECT pg_cancel_backend({con.backend_pid});\".encode(),\n                )\n            finally:\n                con.finish_pg_cancellation()\n\n            return result == b\"\\x01\"\n\n    async def cancel_and_discard_pgcon(\n        self,\n        con: pgcon.PGConnection,\n        dbname: str,\n    ) -> None:\n        try:\n            if self._running:\n                await self.cancel_pgcon_operation(con)\n        finally:\n            self.release_pgcon(dbname, con, discard=True)\n\n    async def signal_sysevent(self, event: str, **kwargs) -> None:\n        try:\n            if not self._initing and not self._running:\n                # This is very likely if we are doing\n                # \"run_startup_script_and_exit()\", but is also possible if the\n                # tenant was shut down with this coroutine as a background task\n                # in flight.\n                return\n\n            async with self.use_sys_pgcon() as con:\n                await con.signal_sysevent(event, **kwargs)\n        except Exception:\n            metrics.background_errors.inc(\n                1.0, self._instance_name, \"signal_sysevent\"\n            )\n            raise\n\n    def on_remote_database_quarantine(self, dbname: str) -> None:\n        if not self._accept_new_tasks:\n            return\n\n        # Block new connections to the database.\n        self._block_new_connections.add(dbname)\n\n        async def task():\n            try:\n                await self._pg_pool.prune_inactive_connections(dbname)\n            except Exception:\n                metrics.background_errors.inc(\n                    1.0, self._instance_name, \"remote_db_quarantine\"\n                )\n                raise\n\n        self.create_task(task(), interruptable=True)\n\n    def on_remote_ddl(self, dbname: str) -> None:\n        if not self.is_db_ready(dbname):\n            return\n\n        # Triggered by a postgres notification event 'schema-changes'\n        # on the __edgedb_sysevent__ channel\n        async def task():\n            try:\n                await self.introspect_db(dbname)\n            except Exception:\n                metrics.background_errors.inc(\n                    1.0, self._instance_name, \"on_remote_ddl\"\n                )\n                raise\n\n        self.create_task(task(), interruptable=True)\n\n    def on_remote_database_changes(self) -> None:\n        if not self._accept_new_tasks:\n            return\n\n        # Triggered by a postgres notification event 'database-changes'\n        # on the __edgedb_sysevent__ channel\n        async def task():\n            async with self.use_sys_pgcon() as syscon:\n                dbnames = set(await self._server.get_dbnames(syscon))\n\n            tg = asyncio.TaskGroup()\n            async with tg as g:\n                for dbname in dbnames:\n                    if not self._dbindex.has_db(dbname):\n                        g.create_task(self._early_introspect_db(dbname))\n\n            dropped = []\n            for db in self._dbindex.iter_dbs():\n                if db.name not in dbnames:\n                    dropped.append(db.name)\n            for dbname in dropped:\n                self.on_after_drop_db(dbname)\n\n        self.create_task(task(), interruptable=True)\n\n    def on_remote_database_config_change(self, dbname: str) -> None:\n        if not self._accept_new_tasks:\n            return\n\n        # Triggered by a postgres notification event 'database-config-changes'\n        # on the __edgedb_sysevent__ channel\n        async def task():\n            try:\n                await self.introspect_db(dbname, reintrospection=True)\n            except Exception:\n                metrics.background_errors.inc(\n                    1.0,\n                    self._instance_name,\n                    \"on_remote_database_config_change\",\n                )\n                raise\n\n        self.create_task(task(), interruptable=True)\n\n    async def process_local_database_config_change(\n        self,\n        conn: pgcon.PGConnection,\n        dbname: str,\n    ) -> None:\n        # It's easier and safer to just do full re-introspection\n        # of the DB and update all components of it.\n        # TODO: Can we just do config?\n        await self.introspect_db(dbname, conn=conn, reintrospection=True)\n\n    def on_remote_system_config_change(self) -> None:\n        if not self._accept_new_tasks:\n            return\n\n        # Triggered by a postgres notification event 'system-config-changes'\n        # on the __edgedb_sysevent__ channel\n\n        async def task():\n            try:\n                cfg = await self._load_sys_config()\n                self._dbindex.update_sys_config(cfg)\n                self._server.reinit_idle_gc_collector()\n            except Exception:\n                metrics.background_errors.inc(\n                    1.0, self._instance_name, \"on_remote_system_config_change\"\n                )\n                raise\n\n        self.create_task(task(), interruptable=True)\n\n    def on_global_schema_change(self) -> None:\n        if not self._accept_new_tasks:\n            return\n\n        async def task():\n            try:\n                await self._reintrospect_global_schema()\n            except Exception:\n                metrics.background_errors.inc(\n                    1.0, self._instance_name, \"on_global_schema_change\"\n                )\n                raise\n\n        self.create_task(task(), interruptable=True)\n\n    async def _load_query_cache(\n        self,\n        conn: pgcon.PGConnection,\n        keys: Optional[Iterable[uuid.UUID]] = None,\n    ) -> list[tuple[bytes, ...]] | None:\n        if keys is None:\n            return await conn.sql_fetch(\n                b'''\n                SELECT \"schema_version\", \"input\", \"output\"\n                FROM \"edgedb\".\"_query_cache\"\n                ''',\n                use_prep_stmt=True,\n            )\n        else:\n            # If keys were specified, just load those keys.\n            # TODO: Or should we do something time based?\n            return await conn.sql_fetch(\n                b'''\n                SELECT \"schema_version\", \"input\", \"output\"\n                ROWS FROM json_array_elements($1) j(ikey)\n                INNER JOIN \"edgedb\".\"_query_cache\"\n                ON (to_jsonb(ARRAY[ikey])->>0)::uuid = key\n                ''',\n                args=(json.dumps(keys).encode('utf-8'),),\n                use_prep_stmt=True,\n            )\n\n    async def evict_query_cache(\n        self,\n        dbname: str,\n        keys: Iterable[uuid.UUID],\n    ) -> None:\n        try:\n            async with self._with_intro_pgcon(dbname) as conn:\n                if not conn:\n                    return\n                for key in keys:\n                    await conn.sql_fetch(\n                        b'SELECT \"edgedb\".\"_evict_query_cache\"($1)',\n                        args=(key.bytes,),\n                        use_prep_stmt=True,\n                    )\n\n        except Exception:\n            logger.exception(\"error in evict_query_cache():\")\n            metrics.background_errors.inc(\n                1.0, self._instance_name, \"evict_query_cache\"\n            )\n\n    def on_remote_query_cache_change(\n        self,\n        dbname: str,\n        to_add: Optional[list[str]],\n        to_invalidate: Optional[list[str]],\n    ) -> None:\n        if not self.is_db_ready(dbname):\n            return\n\n        if to_invalidate:\n            if db := self.maybe_get_db(dbname=dbname):\n                db.invalidate_cache_entries(\n                    [uuid.UUID(s) for s in to_invalidate]\n                )\n\n        async def task():\n            try:\n                async with self._with_intro_pgcon(dbname) as conn:\n                    if not conn:\n                        return\n                    query_cache = await self._load_query_cache(\n                        conn, keys=to_add\n                    )\n\n                if query_cache and (db := self.maybe_get_db(dbname=dbname)):\n                    db.hydrate_cache(query_cache)\n\n            except Exception:\n                logger.exception(\"error in on_remote_query_cache_change():\")\n                metrics.background_errors.inc(\n                    1.0, self._instance_name, \"on_remote_query_cache_change\"\n                )\n                raise\n\n        # If neither to_add nor to_invalidate are specified, then we do\n        # a full introspection.\n        if to_add or not to_invalidate:\n            self.create_task(task(), interruptable=True)\n\n    def get_debug_info(self) -> dict[str, Any]:\n        from . import smtp\n\n        pgaddr = self.get_pgaddr()\n        pgaddr.clear_server_settings()\n        pgdict = pgaddr.__dict__\n        del pgdict['database']\n        pgaddr.__dict__ = pgdict\n\n        obj = dict(\n            params=dict(\n                max_backend_connections=self._max_backend_connections,\n                suggested_client_pool_size=self._suggested_client_pool_size,\n                tenant_id=self._tenant_id,\n            ),\n            instance_config=config.debug_serialize_config(\n                self.get_sys_config()),\n            user_roles=self._roles,\n            pg_addr=dict(\n                server_settings=vars(self._cluster.get_connection_params()),\n                dsn=pgaddr.to_dsn(),\n            ),\n            pg_pool=self._pg_pool._build_snapshot(now=time.monotonic()),\n        )\n\n        dbs = {}\n        if self._dbindex is not None:\n            for db in self._dbindex.iter_dbs():\n                if db.name in defines.EDGEDB_SPECIAL_DBS:\n                    continue\n\n                try:\n                    email_provider = dataclasses.asdict(\n                        smtp.get_current_email_provider(db)\n                    )\n                except errors.ConfigurationError:\n                    email_provider = None\n                dbs[db.name] = dict(\n                    name=db.name,\n                    dbver=db.dbver,\n                    config=(\n                        None\n                        if db.db_config is None\n                        else config.debug_serialize_config(db.db_config)\n                    ),\n                    extensions=sorted(db.extensions),\n                    query_cache_size=db.get_query_cache_size(),\n                    connections=[\n                        dict(\n                            in_tx=view.in_tx(),\n                            in_tx_error=view.in_tx_error(),\n                            config=config.debug_serialize_config(\n                                view.get_session_config()),\n                            module_aliases=view.get_modaliases(),\n                        )\n                        for view in db.iter_views()\n                    ],\n                    current_email_provider=email_provider,\n                )\n\n        obj[\"databases\"] = dbs\n\n        return obj\n\n    def get_compiler_args(self) -> dict[str, Any]:\n        assert self._dbindex is not None\n        return {\"dbindex\": self._dbindex}\n\n    def iter_dbs(self) -> Iterator[dbview.Database]:\n        if self._dbindex is not None:\n            yield from self._dbindex.iter_dbs()\n\n\n# sentinel Tenant object to indicate an empty SNI\nhost_tenant = Tenant.__new__(Tenant)\n"
  },
  {
    "path": "edb/testbase/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n"
  },
  {
    "path": "edb/testbase/asyncutils.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport unittest\n\ntry:\n    import async_solipsism\nexcept ImportError:\n    async_solipsism = None  # type: ignore\n\n\ndef with_fake_event_loop(f):\n    # async_solpsism creates an event loop with, among other things,\n    # a totally fake clock which starts at 0.\n    def new(*args, **kwargs):\n        if not async_solipsism:\n            raise unittest.SkipTest('async_solipsism is missing')\n\n        loop = async_solipsism.EventLoop()\n        try:\n            loop.run_until_complete(f(*args, **kwargs))\n        finally:\n            loop.close()\n\n    return new\n"
  },
  {
    "path": "edb/testbase/cluster.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional, Mapping, TYPE_CHECKING\n\nimport asyncio\nimport json\nimport os\nimport pathlib\nimport socket\nimport subprocess\nimport sys\nimport tempfile\nimport time\n\nfrom edb import buildmeta\nfrom edb.common import devmode\nfrom edb.edgeql import quote\n\nfrom edb.server import auth\nfrom edb.server import args as edgedb_args\nfrom edb.server import defines as edgedb_defines\nfrom edb.server import pgcluster\nfrom edb.server import pgconnparams\n\n\nif TYPE_CHECKING:\n    from edb.server import pgcon\n\n\nclass ClusterError(Exception):\n    pass\n\n\nclass BaseCluster:\n    def __init__(\n        self,\n        runstate_dir: pathlib.Path,\n        *,\n        port: int = edgedb_defines.EDGEDB_PORT,\n        env: Optional[Mapping[str, str]] = None,\n        testmode: bool = False,\n        log_level: Optional[str] = None,\n        security: Optional[\n            edgedb_args.ServerSecurityMode\n        ] = None,\n        http_endpoint_security: Optional[\n            edgedb_args.ServerEndpointSecurityMode\n        ] = None,\n        compiler_pool_mode: Optional[\n            edgedb_args.CompilerPoolMode\n        ] = None,\n        net_worker_mode: Optional[\n            edgedb_args.NetWorkerMode\n        ] = None,\n    ):\n        self._edgedb_cmd = [sys.executable, '-I', '-m', 'edb.server.main']\n\n        if \"EDGEDB_SERVER_MULTITENANT_CONFIG_FILE\" not in os.environ:\n            self._edgedb_cmd.append('--instance-name=localtest')\n\n        self._edgedb_cmd.append('--tls-cert-mode=generate_self_signed')\n        self._edgedb_cmd.append('--jose-key-mode=generate')\n\n        if log_level:\n            self._edgedb_cmd.extend(['--log-level', log_level])\n\n        compiler_addr = os.getenv('EDGEDB_TEST_REMOTE_COMPILER')\n        if compiler_addr:\n            compiler_pool_mode = edgedb_args.CompilerPoolMode.Remote\n            self._edgedb_cmd.extend(\n                [\n                    '--compiler-pool-addr',\n                    compiler_addr,\n                ]\n            )\n\n        if devmode.is_in_dev_mode():\n            self._edgedb_cmd.append('--devmode')\n\n        if testmode:\n            self._edgedb_cmd.append('--testmode')\n\n        if security:\n            self._edgedb_cmd.extend((\n                '--security',\n                str(security),\n            ))\n\n        if http_endpoint_security:\n            self._edgedb_cmd.extend((\n                '--http-endpoint-security',\n                str(http_endpoint_security),\n            ))\n\n        if compiler_pool_mode is not None:\n            self._edgedb_cmd.extend((\n                '--compiler-pool-mode',\n                str(compiler_pool_mode),\n            ))\n\n        if net_worker_mode is not None:\n            self._edgedb_cmd.extend((\n                '--net-worker-mode',\n                str(net_worker_mode),\n            ))\n\n        self._log_level = log_level\n        self._runstate_dir = runstate_dir\n        self._edgedb_cmd.extend(['--runstate-dir', str(runstate_dir)])\n        self._pg_cluster: Optional[pgcluster.BaseCluster] = None\n        self._pg_connect_args: pgconnparams.CreateParamsKwargs = {}\n        self._daemon_process: Optional[subprocess.Popen[str]] = None\n        self._port = port\n        self._effective_port = None\n        self._tls_cert_file = None\n        self._env = env\n\n    async def _get_pg_cluster(self) -> pgcluster.BaseCluster:\n        if self._pg_cluster is None:\n            self._pg_cluster = await self._new_pg_cluster()\n        return self._pg_cluster\n\n    async def _new_pg_cluster(self) -> pgcluster.BaseCluster:\n        raise NotImplementedError()\n\n    async def get_status(self) -> str:\n        pg_cluster = await self._get_pg_cluster()\n        pg_status = await pg_cluster.get_status()\n        initially_stopped = pg_status == 'stopped'\n\n        if initially_stopped:\n            await pg_cluster.start()\n        elif pg_status == 'not-initialized':\n            return 'not-initialized'\n\n        conn = None\n        try:\n            conn = await pg_cluster.connect(\n                source_description=f\"{self.__class__.__name__}.get_status\",\n                **self._pg_connect_args,\n            )\n\n            db_exists = await self._edgedb_template_exists(conn)\n        finally:\n            if conn is not None:\n                conn.terminate()\n            await asyncio.sleep(0)\n            if initially_stopped:\n                await pg_cluster.stop()\n\n        if initially_stopped:\n            return 'stopped' if db_exists else 'not-initialized,stopped'\n        else:\n            return 'running' if db_exists else 'not-initialized,running'\n\n    def get_connect_args(self) -> dict[str, Any]:\n        return {\n            'host': 'localhost',\n            'port': self._effective_port,\n            'tls_ca_file': self._tls_cert_file,\n        }\n\n    async def init(\n        self,\n        *,\n        server_settings: Optional[Mapping[str, str]] = None,\n    ) -> None:\n        cluster_status = await self.get_status()\n\n        if not cluster_status.startswith('not-initialized'):\n            raise ClusterError('cluster has already been initialized')\n\n        self._init()\n\n    async def start(\n        self,\n        wait: int=60,\n        *,\n        port: Optional[int] = None,\n        **settings: Any,\n    ) -> None:\n        if port is None:\n            port = self._port\n\n        if port == 0:\n            cmd_port = 'auto'\n        else:\n            cmd_port = str(port)\n\n        extra_args = ['--{}={}'.format(k.replace('_', '-'), v)\n                      for k, v in settings.items()]\n        extra_args.append(f'--port={cmd_port}')\n        status_r = status_w = None\n        if port == 0:\n            status_r, status_w = socket.socketpair()\n            extra_args.append(f'--emit-server-status=fd://{status_w.fileno()}')\n\n        env: Optional[dict[str, str]]\n        if self._env:\n            env = os.environ.copy()\n            env.update(self._env)\n        else:\n            env = None\n\n        self._daemon_process = subprocess.Popen(\n            self._edgedb_cmd + extra_args,\n            env=env,\n            text=True,\n            pass_fds=(status_w.fileno(),) if status_w is not None else (),\n        )\n\n        if status_w is not None:\n            status_w.close()\n\n        try:\n            await self._wait_for_server(timeout=wait, status_sock=status_r)\n        except Exception:\n            self.stop()\n            raise\n\n    def stop(self, wait: int = 60) -> None:\n        if (self._daemon_process is not None and\n                self._daemon_process.returncode is None):\n            self._daemon_process.terminate()\n            self._daemon_process.wait(wait)\n\n    def destroy(self) -> None:\n        if self._pg_cluster is not None:\n            self._pg_cluster.destroy()\n\n    def _init(self) -> None:\n        env: Optional[dict[str, str]]\n        if self._env:\n            env = os.environ.copy()\n            env.update(self._env)\n        else:\n            env = None\n\n        init = subprocess.run(\n            self._edgedb_cmd + ['--bootstrap-only'],\n            stdout=sys.stdout, stderr=sys.stderr,\n            env=env)\n\n        if init.returncode != 0:\n            raise ClusterError(\n                f'edgedb-server --bootstrap-only failed with '\n                f'exit code {init.returncode}')\n\n    async def _edgedb_template_exists(\n        self,\n        conn: pgcon.PGConnection,\n    ) -> bool:\n        return await conn.sql_fetch_val(\n            b\"SELECT True FROM pg_catalog.pg_database WHERE datname = $1\",\n            args=[edgedb_defines.EDGEDB_TEMPLATE_DB.encode(\"utf-8\")],\n        ) is not None\n\n    async def _wait_for_server(\n        self,\n        timeout: float = 30.0,\n        status_sock: Optional[socket.socket] = None,\n    ) -> None:\n\n        async def _read_server_status(\n            stream: asyncio.StreamReader,\n        ) -> dict[str, Any]:\n            while True:\n                line = await stream.readline()\n                if not line:\n                    raise ClusterError(\"Gel server terminated\")\n                if line.startswith(b'READY='):\n                    break\n\n            _, _, dataline = line.decode().partition('=')\n            try:\n                return json.loads(dataline)  # type: ignore\n            except Exception as e:\n                raise ClusterError(\n                    f\"Gel server returned invalid status line: \"\n                    f\"{dataline!r} ({e})\"\n                )\n\n        async def test() -> None:\n            stat_reader, stat_writer = await asyncio.open_connection(\n                sock=status_sock,\n            )\n            try:\n                data = await asyncio.wait_for(\n                    _read_server_status(stat_reader),\n                    timeout=timeout\n                )\n            except asyncio.TimeoutError:\n                raise ClusterError(\n                    f'Gel server did not initialize '\n                    f'within {timeout} seconds'\n                ) from None\n\n            self._effective_port = data['port']\n            self._tls_cert_file = data['tls_cert_file']\n            stat_writer.close()\n\n        left = timeout\n        if status_sock is not None:\n            started = time.monotonic()\n            await test()\n            left -= (time.monotonic() - started)\n        if res := self._admin_query(\n            \"SELECT ();\",\n            f\"{max(1, int(left))}s\",\n            check=False,\n        ):\n            raise ClusterError(\n                f'could not connect to edgedb-server '\n                f'within {timeout} seconds (exit code = {res})') from None\n\n    def _admin_query(\n        self,\n        query: str,\n        wait_until_available: str = \"0s\",\n        check: bool=True,\n    ) -> int:\n        args = [\n            \"gel\",\n            \"query\",\n            \"--unix-path\",\n            str(os.path.abspath(self._runstate_dir)),\n            \"--port\",\n            str(self._effective_port),\n            \"--admin\",\n            \"--user\",\n            edgedb_defines.EDGEDB_SUPERUSER,\n            \"--branch\",\n            edgedb_defines.EDGEDB_SUPERUSER_DB,\n            \"--wait-until-available\",\n            wait_until_available,\n            query,\n        ]\n        res = subprocess.run(\n            args=args,\n            check=check,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.STDOUT,\n        )\n        return res.returncode\n\n    async def set_test_config(self) -> None:\n        # Set session_idle_transaction_timeout to 5 minutes.\n        self._admin_query('''\n            CONFIGURE INSTANCE SET session_idle_transaction_timeout :=\n                <duration>'5 minutes';\n        ''')\n        # And disable session_idle_timeout\n        self._admin_query('''\n            CONFIGURE INSTANCE SET session_idle_timeout :=\n                <duration>'0 seconds';\n        ''')\n\n    async def set_superuser_password(self, password: str) -> None:\n        self._admin_query(f'''\n            ALTER ROLE {edgedb_defines.EDGEDB_SUPERUSER}\n            SET password := {quote.quote_literal(password)}\n        ''')\n\n    async def trust_local_connections(self) -> None:\n        self._admin_query('''\n            CONFIGURE INSTANCE INSERT Auth {\n                priority := 0,\n                method := (INSERT Trust),\n            }\n        ''')\n\n    def has_create_database(self) -> bool:\n        return True\n\n    def has_create_role(self) -> bool:\n        return True\n\n\nclass Cluster(BaseCluster):\n    def __init__(\n        self,\n        data_dir: pathlib.Path,\n        *,\n        pg_superuser: str = 'postgres',\n        port: int = edgedb_defines.EDGEDB_PORT,\n        runstate_dir: Optional[pathlib.Path] = None,\n        env: Optional[Mapping[str, str]] = None,\n        testmode: bool = False,\n        log_level: Optional[str] = None,\n        security: Optional[\n            edgedb_args.ServerSecurityMode\n        ] = None,\n        http_endpoint_security: Optional[\n            edgedb_args.ServerEndpointSecurityMode\n        ] = None,\n        compiler_pool_mode: Optional[\n            edgedb_args.CompilerPoolMode\n        ] = None,\n    ) -> None:\n        self._data_dir = data_dir\n        if runstate_dir is None:\n            runstate_dir = buildmeta.get_runstate_path(self._data_dir)\n        super().__init__(\n            runstate_dir,\n            port=port,\n            env=env,\n            testmode=testmode,\n            log_level=log_level,\n            security=security,\n            http_endpoint_security=http_endpoint_security,\n            compiler_pool_mode=compiler_pool_mode,\n        )\n        self._edgedb_cmd.extend(['-D', str(self._data_dir)])\n        self._pg_connect_args['user'] = pg_superuser\n        self._pg_connect_args['database'] = 'template1'\n        self._jws_key: Optional[auth.JWKSet] = None\n\n    async def _new_pg_cluster(self) -> pgcluster.Cluster:\n        return await pgcluster.get_local_pg_cluster(\n            self._data_dir,\n            runstate_dir=self._runstate_dir,\n            log_level=self._log_level,\n        )\n\n    def get_data_dir(self) -> pathlib.Path:\n        return self._data_dir\n\n    async def init(\n        self,\n        *,\n        server_settings: Optional[Mapping[str, str]] = None,\n    ) -> None:\n        cluster_status = await self.get_status()\n\n        if not cluster_status.startswith('not-initialized'):\n            raise ClusterError(\n                'cluster in {!r} has already been initialized'.format(\n                    self._data_dir))\n\n        self._init()\n\n\nclass TempCluster(Cluster):\n    def __init__(\n        self,\n        *,\n        data_dir_suffix: Optional[str] = None,\n        data_dir_prefix: Optional[str] = None,\n        data_dir_parent: Optional[str] = None,\n        env: Optional[Mapping[str, str]] = None,\n        testmode: bool = False,\n        log_level: Optional[str] = None,\n        security: Optional[\n            edgedb_args.ServerSecurityMode\n        ] = None,\n        http_endpoint_security: Optional[\n            edgedb_args.ServerEndpointSecurityMode\n        ] = None,\n        compiler_pool_mode: Optional[\n            edgedb_args.CompilerPoolMode\n        ] = None,\n    ) -> None:\n        tempdir = pathlib.Path(\n            tempfile.mkdtemp(\n                suffix=data_dir_suffix,\n                prefix=data_dir_prefix,\n                dir=data_dir_parent,\n            ),\n        )\n        super().__init__(\n            data_dir=tempdir,\n            runstate_dir=tempdir,\n            env=env,\n            testmode=testmode,\n            log_level=log_level,\n            security=security,\n            http_endpoint_security=http_endpoint_security,\n            compiler_pool_mode=compiler_pool_mode,\n        )\n\n\nclass RunningCluster(BaseCluster):\n    def __init__(self, **conn_args: Any) -> None:\n        self.conn_args = conn_args\n\n    def is_managed(self) -> bool:\n        return False\n\n    def ensure_initialized(self) -> bool:\n        return False\n\n    def get_connect_args(self) -> dict[str, Any]:\n        return dict(self.conn_args)\n\n    async def get_status(self) -> str:\n        return 'running'\n\n    async def init(\n        self,\n        *,\n        server_settings: Optional[Mapping[str, str]] = None,\n    ) -> None:\n        pass\n\n    async def start(\n        self,\n        wait: int=60,\n        *,\n        port: Optional[int] = None,\n        **settings: Any,\n    ) -> None:\n        pass\n\n    def stop(self, wait: int = 60) -> None:\n        pass\n\n    def destroy(self) -> None:\n        pass\n\n    def has_create_database(self) -> bool:\n        return os.environ.get('EDGEDB_TEST_CASES_SET_UP') != 'inplace'\n\n    def has_create_role(self) -> bool:\n        return os.environ.get('EDGEDB_TEST_HAS_CREATE_ROLE') == 'True'\n\n\nclass TempClusterWithRemotePg(BaseCluster):\n    def __init__(\n        self,\n        backend_dsn: str,\n        *,\n        data_dir_suffix: Optional[str] = None,\n        data_dir_prefix: Optional[str] = None,\n        data_dir_parent: Optional[str] = None,\n        env: Optional[Mapping[str, str]] = None,\n        testmode: bool = False,\n        log_level: Optional[str] = None,\n        security: Optional[\n            edgedb_args.ServerSecurityMode\n        ] = None,\n        http_endpoint_security: Optional[\n            edgedb_args.ServerEndpointSecurityMode\n        ] = None,\n        compiler_pool_mode: Optional[\n            edgedb_args.CompilerPoolMode\n        ] = None,\n    ) -> None:\n        runstate_dir = pathlib.Path(\n            tempfile.mkdtemp(\n                suffix=data_dir_suffix,\n                prefix=data_dir_prefix,\n                dir=data_dir_parent,\n            ),\n        )\n        self._backend_dsn = backend_dsn\n        mt = \"EDGEDB_SERVER_MULTITENANT_CONFIG_FILE\" in os.environ\n        if mt:\n            compiler_pool_mode = edgedb_args.CompilerPoolMode.MultiTenant\n\n        super().__init__(\n            runstate_dir,\n            env=env,\n            testmode=testmode,\n            log_level=log_level,\n            security=security,\n            http_endpoint_security=http_endpoint_security,\n            compiler_pool_mode=compiler_pool_mode,\n        )\n        if not mt:\n            self._edgedb_cmd.extend(['--backend-dsn', backend_dsn])\n\n    async def _new_pg_cluster(self) -> pgcluster.BaseCluster:\n        return await pgcluster.get_remote_pg_cluster(self._backend_dsn)\n\n    def has_create_database(self) -> bool:\n        if self._pg_cluster:\n            return self._pg_cluster.get_runtime_params().has_create_database\n        else:\n            return super().has_create_database()\n\n    def has_create_role(self) -> bool:\n        if self._pg_cluster:\n            return self._pg_cluster.get_runtime_params().has_create_role\n        else:\n            return super().has_create_role()\n"
  },
  {
    "path": "edb/testbase/connection.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"A specialized client API for Gel tests.\n\nHistorically Gel tests relied on a very specific client API that\nis no longer supported by edgedb-python. Here we implement that API\n(for example, transactions can be nested and are non-retrying).\n\"\"\"\n\nfrom __future__ import annotations\nimport typing\n\nimport abc\nimport asyncio\nimport contextlib\nimport enum\nimport functools\nimport random\nimport socket\nimport ssl\nimport time\n\nfrom gel import abstract\nfrom gel import errors\nfrom gel import con_utils\nfrom gel import enums as edgedb_enums\nfrom gel import options\nfrom gel.protocol import protocol  # type: ignore\n\nfrom edb.protocol import protocol as edb_protocol  # type: ignore\n\n\nclass TransactionState(enum.Enum):\n    NEW = 0\n    STARTED = 1\n    COMMITTED = 2\n    ROLLEDBACK = 3\n    FAILED = 4\n\n\nInputLanguage = protocol.InputLanguage\n\n\nclass BaseTransaction(abc.ABC):\n\n    ID_COUNTER = 0\n\n    def __init__(self, owner):\n        self._connection = owner\n        self._state = TransactionState.NEW\n        self._managed = False\n        self._nested = False\n\n        type(self).ID_COUNTER += 1\n        self._id = f'raw_tx_{self.ID_COUNTER}'\n\n    def is_active(self) -> bool:\n        return self._state is TransactionState.STARTED\n\n    def __check_state_base(self, opname):\n        if self._state is TransactionState.COMMITTED:\n            raise errors.InterfaceError(\n                f'cannot {opname}; the transaction is already committed')\n        if self._state is TransactionState.ROLLEDBACK:\n            raise errors.InterfaceError(\n                f'cannot {opname}; the transaction is already rolled back')\n        if self._state is TransactionState.FAILED:\n            raise errors.InterfaceError(\n                f'cannot {opname}; the transaction is in error state')\n\n    def __check_state(self, opname):\n        if self._state is not TransactionState.STARTED:\n            if self._state is TransactionState.NEW:\n                raise errors.InterfaceError(\n                    f'cannot {opname}; the transaction is not yet started')\n            self.__check_state_base(opname)\n\n    def _make_start_query(self):\n        self.__check_state_base('start')\n        if self._state is TransactionState.STARTED:\n            raise errors.InterfaceError(\n                'cannot start; the transaction is already started')\n        qry = self._make_start_query_inner()\n        if self._connection._top_xact is None:\n            self._connection._top_xact = self\n        return qry\n\n    @abc.abstractmethod\n    def _make_start_query_inner(self):\n        ...\n\n    def _make_commit_query(self):\n        self.__check_state('commit')\n\n        if self._connection._top_xact is self:\n            self._connection._top_xact = None\n\n        return 'COMMIT;'\n\n    def _make_rollback_query(self):\n        self.__check_state('rollback')\n\n        if self._connection._top_xact is self:\n            self._connection._top_xact = None\n\n        if self._nested:\n            query = f'ROLLBACK TO SAVEPOINT {self._id};'\n        else:\n            query = 'ROLLBACK;'\n\n        return query\n\n    async def start(self) -> None:\n        query = self._make_start_query()\n        try:\n            await self._connection.execute(query)\n        except BaseException:\n            self._state = TransactionState.FAILED\n            raise\n        else:\n            self._state = TransactionState.STARTED\n\n    async def commit(self) -> None:\n        if self._managed:\n            raise errors.InterfaceError(\n                'cannot manually commit from within an `async with` block')\n        await self._commit()\n\n    async def _commit(self) -> None:\n        query = self._make_commit_query()\n        try:\n            # Use _fetchall to ensure there is no retry performed.\n            # The protocol level apparently thinks the transaction is\n            # over if COMMIT fails, and since we use that to decide\n            # whether to retry in query/execute, it would want to\n            # retry a COMMIT.\n            await self._connection._fetchall(query)\n        except BaseException:\n            self._state = TransactionState.FAILED\n            raise\n        else:\n            self._state = TransactionState.COMMITTED\n\n    async def rollback(self) -> None:\n        if self._managed:\n            raise errors.InterfaceError(\n                'cannot manually rollback from within an `async with` block')\n        await self._rollback()\n\n    async def _rollback(self) -> None:\n        query = self._make_rollback_query()\n        try:\n            await self._connection.execute(query)\n        except BaseException:\n            self._state = TransactionState.FAILED\n            raise\n        else:\n            self._state = TransactionState.ROLLEDBACK\n\n\nclass RawTransaction(BaseTransaction):\n    def _make_start_query_inner(self):\n        con = self._connection\n\n        if con._top_xact is not None:\n            # Nested transaction block\n            self._nested = True\n\n        if self._nested:\n            query = f'DECLARE SAVEPOINT {self._id};'\n        else:\n            query = 'START TRANSACTION;'\n\n        return query\n\n    def _make_commit_query(self):\n        query = super()._make_commit_query()\n\n        if self._nested:\n            query = f'RELEASE SAVEPOINT {self._id};'\n\n        return query\n\n    def _make_rollback_query(self):\n        query = super()._make_rollback_query()\n\n        if self._nested:\n            query = f'ROLLBACK TO SAVEPOINT {self._id};'\n\n        return query\n\n    async def __aenter__(self):\n        if self._managed:\n            raise errors.InterfaceError(\n                'cannot enter context: already in an `async with` block')\n        self._managed = True\n        await self.start()\n        return self\n\n    async def __aexit__(self, extype, ex, tb):\n        try:\n            if extype is not None:\n                await self._rollback()\n            else:\n                await self._commit()\n        finally:\n            self._managed = False\n\n\nclass _Executor(abstract.AsyncIOExecutor):\n    # TODO: Remove this, once we land this in gel-python and update\n    # our bindings.\n    async def query_graphql_json(\n        self, query, *args: typing.Any, **kwargs: typing.Any\n    ) -> str:\n        return await self._query(\n            abstract.QueryContext(\n                query=abstract.QueryWithArgs(\n                    query,\n                    # None,\n                    args,\n                    kwargs,\n                    input_language=ord('G'),\n                ),\n                cache=self._get_query_cache(),\n                query_options=abstract._query_single_json_opts,\n                retry_options=self._get_retry_options(),\n                state=self._get_state(),\n                # transaction_options=self._get_active_tx_options(),\n                warning_handler=self._get_warning_handler(),\n                annotations=self._get_annotations(),\n            )\n        )\n\n\nclass Iteration(BaseTransaction, _Executor):\n    def __init__(self, retry, connection, iteration):\n        super().__init__(connection)\n        self._options = retry._options.transaction_options\n        self.__retry = retry\n        self.__iteration = iteration\n        self.__started = False\n\n    async def __aenter__(self):\n        if self._managed:\n            raise errors.InterfaceError(\n                'cannot enter context: already in an `async with` block')\n        self._managed = True\n        return self\n\n    async def __aexit__(self, extype, ex, tb):\n        self._managed = False\n        if not self.__started:\n            return False\n\n        try:\n            if extype is None:\n                await self._commit()\n            else:\n                await self._rollback()\n        except errors.EdgeDBError as err:\n            if ex is None:\n                # On commit we don't know if commit is succeeded before the\n                # database have received it or after it have been done but\n                # network is dropped before we were able to receive a response\n                raise err\n            # If we were going to rollback, look at original error\n            # to find out whether we want to retry, regardless of\n            # the rollback error.\n            # In this case we ignore rollback issue as original error is more\n            # important, e.g. in case `CancelledError` it's important\n            # to propagate it to cancel the whole task.\n            # NOTE: rollback error is always swallowed, should we use\n            # on_log_message for it?\n\n        if (\n            extype is not None and\n            issubclass(extype, errors.EdgeDBError) and\n            ex.has_tag(errors.SHOULD_RETRY)\n        ):\n            return self.__retry._retry(ex)\n\n    def _make_start_query_inner(self):\n        return self._options.start_transaction_query()\n\n    def _get_query_cache(self) -> abstract.QueryCache:\n        return self._connection._query_cache\n\n    async def _query(self, query_context: abstract.QueryContext):\n        await self._ensure_transaction()\n        return await self._connection.raw_query(query_context)\n\n    async def _execute(self, query: abstract.ExecuteContext) -> None:\n        await self._ensure_transaction()\n        await self._connection._execute(query)\n\n    async def _ensure_transaction(self):\n        if not self._managed:\n            raise errors.InterfaceError(\n                \"Only managed retriable transactions are supported. \"\n                \"Use `async with transaction:`\"\n            )\n        if not self.__started:\n            self.__started = True\n            if self._connection.is_closed():\n                await self._connection.connect(\n                    single_attempt=self.__iteration != 0\n                )\n            await self.start()\n\n    def _get_retry_options(self) -> options.RetryOptions:\n        return options.RetryOptions.defaults()\n\n    def _get_state(self) -> options.State:\n        return self._connection._get_state()\n\n    def _get_warning_handler(self) -> options.WarningHandler:\n        return self._connection._get_warning_handler()\n\n    def _get_annotations(self) -> dict[str, str]:\n        return self._connection._get_annotations()\n\n\nclass Retry:\n    def __init__(self, connection, raw=False):\n        self._connection = connection\n        self._iteration = 0\n        self._done = False\n        self._next_backoff = 0\n        self._options = connection._options\n\n    def _retry(self, exc):\n        self._last_exception = exc\n        rule = self._options.retry_options.get_rule_for_exception(exc)\n        if self._iteration >= rule.attempts:\n            return False\n        self._done = False\n        self._next_backoff = rule.backoff(self._iteration)\n        return True\n\n    def __aiter__(self):\n        return self\n\n    async def __anext__(self):\n        # Note: when changing this code consider also\n        # updating Retry.__next__.\n        if self._done:\n            raise StopAsyncIteration\n        if self._next_backoff:\n            await asyncio.sleep(self._next_backoff)\n        self._done = True\n        iteration = Iteration(self, self._connection, self._iteration)\n        self._iteration += 1\n        return iteration\n\n\nclass Connection(options._OptionsMixin, _Executor):\n\n    _top_xact: RawTransaction | None = None\n\n    def __init__(\n        self, connect_args, *, test_no_tls=False, server_hostname=None\n    ):\n        super().__init__()\n        self._connect_args = connect_args\n        self._protocol = None\n        self._transport = None\n        self._query_cache = abstract.QueryCache(\n            codecs_registry=protocol.CodecsRegistry(),\n            query_cache=protocol.LRUMapping(maxsize=1000),\n        )\n        self._test_no_tls = test_no_tls\n        self._params = None\n        self._server_hostname = server_hostname\n        self._log_listeners = set()\n        self._capture_warnings = None\n\n    def add_log_listener(self, callback):\n        self._log_listeners.add(callback)\n\n    def remove_log_listener(self, callback):\n        self._log_listeners.discard(callback)\n\n    def _get_retry_options(self) -> options.RetryOptions:\n        return self._options.retry_options\n\n    def _get_state(self):\n        return self._options.state\n\n    def _get_annotations(self) -> dict[str, str]:\n        return self._options.annotations\n\n    @contextlib.contextmanager\n    def capture_warnings(self) -> typing.Iterator[list[errors.EdgeDBError]]:\n        old = self._capture_warnings\n        warnings: list[errors.EdgeDBError] = []\n        self._capture_warnings = warnings\n        try:\n            yield warnings\n        finally:\n            self._capture_warnings = old\n\n    def _warning_handler(self, warnings, res):\n        if self._capture_warnings is not None:\n            self._capture_warnings.extend(warnings)\n            return res\n        else:\n            raise warnings[0]\n\n    def _get_warning_handler(self) -> options.WarningHandler:\n        return self._warning_handler\n\n    def _on_log_message(self, msg):\n        if self._log_listeners:\n            loop = asyncio.get_running_loop()\n            for cb in self._log_listeners:\n                loop.call_soon(cb, self, msg)\n\n    def _shallow_clone(self):\n        con = self.__class__.__new__(self.__class__)\n        con._connect_args = self._connect_args\n        con._protocol = self._protocol\n        con._query_cache = self._query_cache\n        con._test_no_tls = self._test_no_tls\n        con._params = self._params\n        con._server_hostname = self._server_hostname\n        return con\n\n    def _get_query_cache(self) -> abstract.QueryCache:\n        return self._query_cache\n\n    async def ensure_connected(self):\n        if self.is_closed():\n            await self.connect()\n        return self\n\n    async def _query(self, query_context: abstract.QueryContext):\n        await self.ensure_connected()\n        return await self.raw_query(query_context)\n\n    async def _retry_operation(self, func):\n        i = 0\n        while True:\n            i += 1\n            try:\n                return await func()\n            # Retry transaction conflict errors, up to a maximum of 5\n            # times.  We don't do this if we are in a transaction,\n            # since that *ought* to be done at the transaction level.\n            # Though in reality in the test suite it is usually done at the\n            # test runner level.\n            except errors.TransactionConflictError:\n                if i >= 10 or self.is_in_transaction():\n                    raise\n                await asyncio.sleep(\n                    min((2 ** i) * 0.1, 10)\n                    + random.randrange(100) * 0.001\n                )\n\n    def _prohibit_state(self, state) -> None:\n        # The testbase connection uses our own subclass of\n        # gel-python's AsyncIOProtocol,\n        # edb.protocol.protocol.Protocol, that overrides encode_state\n        # to ignore any user specified state and to always just use\n        # whatever the server suggests.\n        #\n        # It is probably possible to make CONFIGURE .../SET GLOBAL\n        # play nicely with with_globals/etc, by decoding what the server\n        # has sent and then overlaying the user configured stuff.\n        #\n        # Since it doesn't work, disable it.\n        if state.as_dict():\n            raise AssertionError(\n                f'test suite client cannot use with_XXX config methods; '\n                f'use SET ... in the protocol instead '\n                f'or use the stock python client '\n                f'(or go make it work; that would be nice too)\\n'\n                f'config was: {state.as_dict()}'\n            )\n\n    async def _execute(self, script: abstract.ExecuteContext) -> None:\n        await self.ensure_connected()\n        self._prohibit_state(script.state)\n\n        async def _inner():\n            ctx = script.lower(allow_capabilities=edgedb_enums.Capability.ALL)\n            res = await self._protocol.execute(ctx)\n            if ctx.warnings:\n                script.warning_handler(ctx.warnings, res)\n\n        await self._retry_operation(_inner)\n\n    async def raw_query(self, query_context: abstract.QueryContext):\n        self._prohibit_state(query_context.state)\n\n        async def _inner():\n            ctx = query_context.lower(\n                allow_capabilities=edgedb_enums.Capability.ALL)\n            res = await self._protocol.query(ctx)\n            if ctx.warnings:\n                res = query_context.warning_handler(ctx.warnings, res)\n            return res\n\n        return await self._retry_operation(_inner)\n\n    async def _fetchall_generic(self, ctx):\n        await self.ensure_connected()\n        res = await self._protocol.query(ctx)\n        if ctx.warnings:\n            res = self._get_warning_handler()(ctx.warnings, res)\n        return res\n\n    async def _fetchall(\n        self,\n        query: str,\n        *args,\n        __language__: protocol.InputLanguage = protocol.InputLanguage.EDGEQL,\n        __limit__: int = 0,\n        __typeids__: bool = False,\n        __typenames__: bool = False,\n        __allow_capabilities__: edgedb_enums.Capability = (\n            edgedb_enums.Capability.ALL),\n        **kwargs,\n    ):\n        return await self._fetchall_generic(\n            protocol.ExecuteContext(\n                query=query,\n                args=args,\n                kwargs=kwargs,\n                reg=self._query_cache.codecs_registry,\n                qc=self._query_cache.query_cache,\n                implicit_limit=__limit__,\n                inline_typeids=__typeids__,\n                inline_typenames=__typenames__,\n                input_language=__language__,\n                output_format=protocol.OutputFormat.BINARY,\n                allow_capabilities=__allow_capabilities__,\n            )\n        )\n\n    async def _fetchall_json(\n        self,\n        query: str,\n        *args,\n        __limit__: int = 0,\n        **kwargs,\n    ):\n        return await self._fetchall_generic(\n            protocol.ExecuteContext(\n                query=query,\n                args=args,\n                kwargs=kwargs,\n                reg=self._query_cache.codecs_registry,\n                qc=self._query_cache.query_cache,\n                implicit_limit=__limit__,\n                inline_typenames=False,\n                input_language=protocol.InputLanguage.EDGEQL,\n                output_format=protocol.OutputFormat.JSON,\n            )\n        )\n\n    async def _fetchall_json_elements(self, query: str, *args, **kwargs):\n        return await self._fetchall_generic(\n            protocol.ExecuteContext(\n                query=query,\n                args=args,\n                kwargs=kwargs,\n                reg=self._query_cache.codecs_registry,\n                qc=self._query_cache.query_cache,\n                input_language=protocol.InputLanguage.EDGEQL,\n                output_format=protocol.OutputFormat.JSON_ELEMENTS,\n                allow_capabilities=edgedb_enums.Capability.EXECUTE,  # type: ignore\n            )\n        )\n\n    def _clear_codecs_cache(self):\n        self._query_cache.codecs_registry.clear_cache()\n\n    def _get_last_status(self) -> typing.Optional[str]:\n        if self._protocol is None:\n            return None\n        status = self._protocol.last_status\n        if status is not None:\n            status = status.decode()\n        return status\n\n    def _get_last_capabilities(\n        self,\n    ) -> typing.Optional[edgedb_enums.Capability]:\n        if self._protocol is None:\n            return None\n        else:\n            return self._protocol.last_capabilities\n\n    def is_closed(self):\n        return self._protocol is None or not self._protocol.connected\n\n    async def connect(self, single_attempt=False):\n        self._params, client_config = con_utils.parse_connect_arguments(\n            **self._connect_args,\n            tls_server_name=None,\n            command_timeout=None,\n            server_settings=None,\n        )\n        start = time.monotonic()\n        if single_attempt:\n            max_time = 0\n        else:\n            max_time = start + client_config.wait_until_available\n        iteration = 1\n\n        while True:\n            addr = self._params.address\n            try:\n                await asyncio.wait_for(\n                    self.connect_addr(),\n                    client_config.connect_timeout,\n                )\n            except TimeoutError as e:\n                if iteration > 1 and time.monotonic() >= max_time:\n                    raise errors.ClientConnectionTimeoutError(\n                        f\"connecting to {addr} failed in\"\n                        f\" {client_config.connect_timeout} sec\"\n                    ) from e\n            except errors.ClientConnectionError as e:\n                if (\n                    not e.has_tag(errors.SHOULD_RECONNECT) or\n                    (iteration > 1 and time.monotonic() >= max_time)\n                ):\n                    nice_err = e.__class__(\n                        con_utils.render_client_no_connection_error(\n                            e,\n                            addr,\n                            attempts=iteration,\n                            duration=time.monotonic() - start,\n                        ))\n                    raise nice_err from e.__cause__\n            else:\n                return\n\n            iteration += 1\n            await asyncio.sleep(0.01 + random.random() * 0.2)\n\n    async def connect_addr(self):\n        tr = None\n        loop = asyncio.get_running_loop()\n        addr = self._params.address\n        protocol_factory = functools.partial(\n            edb_protocol.Protocol, self._params, loop\n        )\n\n        try:\n            if isinstance(addr, str):\n                # UNIX socket\n                tr, pr = await loop.create_unix_connection(\n                    protocol_factory, addr\n                )\n            elif self._test_no_tls:\n                tr, pr = await loop.create_connection(protocol_factory, *addr)\n            else:\n                try:\n                    tr, pr = await loop.create_connection(\n                        protocol_factory,\n                        *addr,\n                        server_hostname=self._server_hostname,\n                        ssl=self._params.ssl_ctx,\n                    )\n                except ssl.CertificateError as e:\n                    raise con_utils.wrap_error(e) from e\n                except ssl.SSLError as e:\n                    if e.reason == 'CERTIFICATE_VERIFY_FAILED':\n                        raise con_utils.wrap_error(e) from e\n                    tr, pr = await loop.create_connection(\n                        protocol_factory,\n                        *addr,\n                    )\n                else:\n                    con_utils.check_alpn_protocol(\n                        tr.get_extra_info('ssl_object')\n                    )\n        except socket.gaierror as e:\n            # All name resolution errors are considered temporary\n            raise errors.ClientConnectionFailedTemporarilyError(str(e)) from e\n        except OSError as e:\n            raise con_utils.wrap_error(e) from e\n        except Exception:\n            if tr is not None:\n                tr.close()\n            raise\n\n        pr.set_connection(self)\n\n        try:\n            await pr.connect()\n        except OSError as e:\n            if tr is not None:\n                tr.close()\n            raise con_utils.wrap_error(e) from e\n        except BaseException:\n            if tr is not None:\n                tr.close()\n            raise\n\n        self._protocol = pr\n        self._transport = tr\n\n    def retrying_transaction(self) -> Retry:\n        return Retry(self)\n\n    def transaction(self) -> RawTransaction:\n        return RawTransaction(self)\n\n    def is_in_transaction(self):\n        return self._protocol.is_in_transaction()\n\n    def get_settings(self) -> dict[str, typing.Any]:\n        return self._protocol.get_settings()\n\n    @property\n    def dbname(self) -> str:\n        return self._params.database\n\n    def connected_addr(self):\n        return self._params.address\n\n    async def aclose(self):\n        if not self.is_closed():\n            try:\n                self._protocol.terminate()\n                await self._protocol.wait_for_disconnect()\n            except (Exception, asyncio.CancelledError):\n                self.terminate()\n                raise\n\n    def terminate(self):\n        if not self.is_closed():\n            self._protocol.abort()\n\n    def get_transport(self):\n        return self._transport\n\n\nasync def async_connect_test_client(\n    dsn: typing.Optional[str] = None,\n    host: typing.Optional[str] = None,\n    port: typing.Optional[int] = None,\n    credentials: typing.Optional[str] = None,\n    credentials_file: typing.Optional[str] = None,\n    user: typing.Optional[str] = None,\n    password: typing.Optional[str] = None,\n    secret_key: typing.Optional[str] = None,\n    branch: typing.Optional[str] = None,\n    database: typing.Optional[str] = None,\n    tls_ca: typing.Optional[str] = None,\n    tls_ca_file: typing.Optional[str] = None,\n    tls_security: typing.Optional[str] = None,\n    test_no_tls: bool = False,\n    wait_until_available: int = 30,\n    timeout: int = 10,\n    server_hostname: str | None = None,\n) -> Connection:\n    return await Connection(\n        {\n            \"dsn\": dsn,\n            \"host\": host,\n            \"port\": port,\n            \"credentials\": credentials,\n            \"credentials_file\": credentials_file,\n            \"user\": user,\n            \"password\": password,\n            \"secret_key\": secret_key,\n            \"branch\": branch,\n            \"database\": database,\n            \"timeout\": timeout,\n            \"tls_ca\": tls_ca,\n            \"tls_ca_file\": tls_ca_file,\n            \"tls_security\": tls_security,\n            \"wait_until_available\": wait_until_available,\n        },\n        test_no_tls=test_no_tls,\n        server_hostname=server_hostname,\n    ).ensure_connected()\n"
  },
  {
    "path": "edb/testbase/experimental_interpreter.py",
    "content": "\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n)\nimport unittest\n\n\nfrom edb.common import assert_data_shape\nfrom edb.tools.experimental_interpreter.new_interpreter import EdgeQLInterpreter\n\nbag = assert_data_shape.bag\n\n\nclass ExperimentalInterpreterTestCase(unittest.TestCase):\n    SCHEMA: Optional[str] = None\n    SETUP: Optional[str] = None\n    INTERPRETER_USE_SQLITE = False\n\n    client: EdgeQLInterpreter\n    initial_state: object\n\n    @classmethod\n    def setUpClass(cls):\n        if cls.SCHEMA is not None:\n            with open(cls.SCHEMA) as f:\n                schema_content = f.read()\n        else:\n            schema_content = \"\"\n\n        sqlite_filename = None\n        if cls.INTERPRETER_USE_SQLITE:\n            sqlite_filename = \":memory:\"\n\n            try:\n                import sqlite3\n            except ModuleNotFoundError:\n                raise unittest.SkipTest(\"sqlite is not installed\")\n\n            if sqlite3.sqlite_version_info < (3, 37):\n                raise unittest.SkipTest(\"sqlite version is too old (need 3.37)\")\n\n        cls.client = EdgeQLInterpreter(schema_content, sqlite_filename)\n\n        if cls.SETUP is not None:\n            with open(cls.SETUP) as f:\n                setup_content = f.read()\n            cls.client.query_str(setup_content)\n\n        cls.initial_state = cls.client.db.dump_state()\n\n    def setUp(self):\n        self.client.db.restore_state(self.initial_state)\n\n    def execute(self, query: str, *, variables=None) -> Any:\n        return self.client.run_single_str_get_json_with_cache(\n            query, variables=variables)\n\n    def execute_single(self, query: str, *, variables=None) -> Any:\n        return self.client.query_single_json(query, variables=variables)\n\n    def assert_query_result(self, query,\n                                  exp_result_json,\n                                  exp_result_binary=...,\n                                  *,\n                                  msg: Optional[str] = None,\n                                  sort: Optional[bool] = None,\n                                  variables=None,\n                                  ):\n        if (hasattr(self, \"use_experimental_interpreter\") and\n                self.use_experimental_interpreter):\n            result = self.client.run_single_str_get_json_with_cache(\n                query, variables=variables)\n            res = result\n            if sort is not None:\n                assert_data_shape.sort_results(res, sort)\n            if exp_result_binary is not ...:\n                assert_data_shape.assert_data_shape(\n                    res, exp_result_binary, self.fail, message=msg)\n            else:\n                assert_data_shape.assert_data_shape(\n                    res, exp_result_json, self.fail, message=msg)\n"
  },
  {
    "path": "edb/testbase/http.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n)\n\nimport asyncio\nimport http.server\nimport json\nimport threading\nimport urllib.parse\nimport urllib.request\nimport dataclasses\nimport time\nimport random\n\nimport gel\n\nfrom edb.errors import base as base_errors\n\nfrom edb.common import assert_data_shape\n\nfrom . import server\n\n\nbag = assert_data_shape.bag\n\n\nclass BaseHttpTest(server.QueryTestCase):\n    @classmethod\n    async def _wait_for_db_config(\n        cls,\n        config_key,\n        *,\n        server=None,\n        instance_config=False,\n        value=None,\n        is_reset=False,\n    ):\n        dbname = cls.get_database_name()\n        # Wait for the database config changes to propagate to the\n        # server by watching a debug endpoint\n        async for tr in cls.try_until_succeeds(\n            ignore=AssertionError,\n            timeout=120,\n        ):\n            async with tr:\n                with cls.http_con(server) as http_con:\n                    (\n                        rdata,\n                        _headers,\n                        _status,\n                    ) = cls.http_con_request(\n                        http_con,\n                        prefix=\"\",\n                        path=\"server-info\",\n                    )\n                    data = json.loads(rdata)\n                    if \"databases\" not in data:\n                        # multi-tenant instance - use the first tenant\n                        data = next(iter(data[\"tenants\"].values()))\n                    if instance_config:\n                        config = data[\"instance_config\"]\n                    else:\n                        config = data[\"databases\"][dbname][\"config\"]\n                    if is_reset:\n                        if config_key in config:\n                            raise AssertionError(\"database config not ready\")\n                    else:\n                        if config_key not in config:\n                            raise AssertionError(\"database config not ready\")\n                        if value and config[config_key] != value:\n                            raise AssertionError(\"database config not ready\")\n\n\nclass BaseHttpExtensionTest(BaseHttpTest):\n    @classmethod\n    def get_extension_path(cls):\n        raise NotImplementedError\n\n    @classmethod\n    def get_api_prefix(cls):\n        extpath = cls.get_extension_path()\n        dbname = cls.get_database_name()\n        return f\"/branch/{dbname}/{extpath}\"\n\n\nclass ExtAuthTestCase(BaseHttpExtensionTest):\n    EXTENSIONS = [\"pgcrypto\", \"auth\"]\n\n    @classmethod\n    def get_extension_path(cls):\n        return \"ext/auth\"\n\n    def generate_pkce_pair(self) -> tuple[str, str]:\n        \"\"\"Generate a PKCE verifier and its corresponding challenge.\n\n        Returns:\n            (verifier, challenge): tuple of str\n        \"\"\"\n        import os\n        import base64\n        import hashlib\n\n        verifier = base64.urlsafe_b64encode(os.urandom(43)).rstrip(b'=')\n        challenge = base64.urlsafe_b64encode(\n            hashlib.sha256(verifier).digest()\n        ).rstrip(b'=')\n        return verifier.decode(), challenge.decode()\n\n\nclass EdgeQLTestCase(BaseHttpExtensionTest):\n    EXTENSIONS = [\"edgeql_http\"]\n\n    @classmethod\n    def get_extension_path(cls):\n        return \"edgeql\"\n\n    def edgeql_query(\n        self,\n        query,\n        *,\n        use_http_post=True,\n        variables=None,\n        globals=None,\n        config=None,\n        origin=None,\n\n        user=None,\n        password=None,\n    ):\n        req_data = {\"query\": query}\n\n        if use_http_post:\n            if variables is not None:\n                req_data[\"variables\"] = variables\n            if globals is not None:\n                req_data[\"globals\"] = globals\n            if config is not None:\n                req_data[\"config\"] = config\n            req = urllib.request.Request(self.http_addr, method=\"POST\")\n            req.add_header(\"Content-Type\", \"application/json\")\n            req.add_header(\n                \"Authorization\", self.make_auth_header(user, password)\n            )\n            if origin:\n                req.add_header(\"Origin\", origin)\n            response = urllib.request.urlopen(\n                req, json.dumps(req_data).encode(), context=self.tls_context\n            )\n            resp_data = json.loads(response.read())\n        else:\n            if variables is not None:\n                req_data[\"variables\"] = json.dumps(variables)\n            if globals is not None:\n                req_data[\"globals\"] = json.dumps(globals)\n            if config is not None:\n                req_data[\"config\"] = json.dumps(config)\n            req = urllib.request.Request(\n                f\"{self.http_addr}/?{urllib.parse.urlencode(req_data)}\",\n            )\n            req.add_header(\n                \"Authorization\", self.make_auth_header(user, password)\n            )\n            response = urllib.request.urlopen(\n                req,\n                context=self.tls_context,\n            )\n            resp_data = json.loads(response.read())\n\n        if \"data\" in resp_data:\n            return (resp_data[\"data\"], response)\n\n        err = resp_data[\"error\"]\n\n        ex_msg = err[\"message\"].strip()\n        ex_code = err[\"code\"]\n\n        raise gel.EdgeDBError._from_code(ex_code, ex_msg)\n\n    def assert_edgeql_query_result(\n        self,\n        query,\n        result,\n        *,\n        msg=None,\n        sort=None,\n        use_http_post=True,\n        variables=None,\n        globals=None,\n        config=None,\n    ):\n        res, _ = self.edgeql_query(\n            query,\n            use_http_post=use_http_post,\n            variables=variables,\n            globals=globals,\n            config=config,\n        )\n\n        if sort is not None:\n            # GQL will always have a single object returned. The data is\n            # in the top-level fields, so that's what needs to be sorted.\n            for r in res.values():\n                assert_data_shape.sort_results(r, sort)\n\n        assert_data_shape.assert_data_shape(res, result, self.fail, message=msg)\n        return res\n\n\nclass GraphQLTestCase(BaseHttpExtensionTest):\n    EXTENSIONS = [\"graphql\"]\n\n    @classmethod\n    def get_extension_path(cls):\n        return \"graphql\"\n\n    def graphql_query(\n        self,\n        query,\n        *,\n        operation_name=None,\n        use_http_post=True,\n        variables=None,\n        globals=None,\n        deprecated_globals=None,\n        config=None,\n\n        user=None,\n        password=None,\n    ):\n        def inner():\n            return self._graphql_query(\n                query,\n                operation_name=operation_name,\n                use_http_post=use_http_post,\n                variables=variables,\n                globals=globals,\n                deprecated_globals=deprecated_globals,\n                config=config,\n                user=user,\n                password=password,\n            )\n        return self._retry_operation(inner)\n\n    def _retry_operation(self, func):\n        i = 0\n        while True:\n            i += 1\n            try:\n                return func()\n\n            # Retry transaction conflict errors\n            except gel.errors.TransactionConflictError:\n                if i >= 10:\n                    raise\n                time.sleep(\n                    min((2 ** i) * 0.1, 10)\n                    + random.randrange(100) * 0.001\n                )\n\n    def _graphql_query(\n        self,\n        query,\n        *,\n        operation_name=None,\n        use_http_post=True,\n        variables=None,\n        globals=None,\n        deprecated_globals=None,\n        config=None,\n        user=None,\n        password=None,\n    ):\n        req_data = {\"query\": query}\n\n        if operation_name is not None:\n            req_data[\"operationName\"] = operation_name\n\n        if use_http_post:\n            if variables is not None:\n                req_data[\"variables\"] = variables\n            if globals is not None:\n                if variables is None:\n                    req_data[\"variables\"] = dict()\n                req_data[\"variables\"][\"__globals__\"] = globals\n            if config is not None:\n                if variables is None:\n                    req_data[\"variables\"] = dict()\n                req_data[\"variables\"][\"__config__\"] = config\n            # Support testing the old way of sending globals.\n            if deprecated_globals is not None:\n                req_data[\"globals\"] = deprecated_globals\n\n            req = urllib.request.Request(self.http_addr, method=\"POST\")\n            req.add_header(\"Content-Type\", \"application/json\")\n            req.add_header(\n                \"Authorization\", self.make_auth_header(user, password)\n            )\n            response = urllib.request.urlopen(\n                req, json.dumps(req_data).encode(), context=self.tls_context\n            )\n            resp_data = json.loads(response.read())\n        else:\n            if globals is not None:\n                if variables is None:\n                    variables = dict()\n                variables[\"__globals__\"] = globals\n            if config is not None:\n                if variables is None:\n                    variables = dict()\n                variables[\"__config__\"] = config\n            # Support testing the old way of sending globals.\n            if deprecated_globals is not None:\n                req_data[\"globals\"] = json.dumps(deprecated_globals)\n            if variables is not None:\n                req_data[\"variables\"] = json.dumps(variables)\n            req = urllib.request.Request(\n                f\"{self.http_addr}/?{urllib.parse.urlencode(req_data)}\",\n            )\n            req.add_header(\n                \"Authorization\", self.make_auth_header(user, password)\n            )\n            response = urllib.request.urlopen(\n                req,\n                context=self.tls_context,\n            )\n            resp_data = json.loads(response.read())\n\n        if \"data\" in resp_data:\n            return resp_data[\"data\"]\n\n        err = resp_data[\"errors\"][0]\n\n        typename, msg = err[\"message\"].split(\":\", 1)\n        msg = msg.strip()\n\n        try:\n            ex_type = getattr(gel, typename)\n        except AttributeError:\n            raise AssertionError(\n                f\"server returned an invalid exception typename: {typename!r}\"\n                f\"\\n  Message: {msg}\"\n            )\n\n        ex = ex_type(msg)\n\n        if \"locations\" in err:\n            # XXX Fix this when LSP \"location\" objects are implemented\n            ex._attrs[base_errors.FIELD_LINE_START] = str(\n                err[\"locations\"][0][\"line\"]\n            ).encode()\n            ex._attrs[base_errors.FIELD_COLUMN_START] = str(\n                err[\"locations\"][0][\"column\"]\n            ).encode()\n\n        raise ex\n\n    async def _native_graphql_query(\n        self,\n        query,\n        *,\n        # Can/should we support operation_name somehow...\n        variables=None,\n        globals=None,\n    ):\n        # The graphql tests are all synchronous, and our gel\n        # connections need to be async... so we spin up a new\n        # connection and asyncio.run the coro.\n        con = await self.connect()\n        try:\n            # Ahhhhhh. We don't support with_globals on testbase\n            # connections, so....\n            if globals:\n                glob_defs = {\n                    obj.name: obj\n                    for obj in\n                    await con.query('''\n                        select schema::Global {\n                            name, required, tname := .target.name\n                        }\n                    ''')\n                }\n\n                for k, v in globals.items():\n                    glob = glob_defs[k]\n                    # Why do we allow this for the HTTP proto??\n                    # We don't for binary proto stuff.\n                    if v is None and glob.required:\n                        continue\n                    mod = 'required ' if glob.required else ''\n                    await con.execute(\n                        f'set global {k} := <{mod}{glob.tname}><json>$0',\n                        json.dumps(v),\n                    )\n\n            async with server.RollbackChanges(con):\n                return json.loads(await con.query_graphql_json(\n                    query,\n                    **(variables or {}),\n                ))\n        finally:\n            await con.aclose()\n\n    def assert_graphql_query_result(\n        self,\n        query,\n        result,\n        *,\n        msg=None,\n        sort=None,\n        operation_name=None,\n        use_http_post=True,\n        native_variables=None,\n        variables=None,\n        globals=None,\n        deprecated_globals=None,\n        config=None,\n    ):\n        # Try to use the native protocol first!\n        if operation_name is None and config is None:\n            try:\n                res = asyncio.run(self._native_graphql_query(\n                    query,\n                    variables=(\n                        native_variables if native_variables is not None\n                        else variables\n                    ),\n                    globals=globals or deprecated_globals,\n                ))\n\n                if sort is not None:\n                    # GQL will always have a single object\n                    # returned. The data is in the top-level fields,\n                    # so that's what needs to be sorted.\n                    for r in res.values():\n                        assert_data_shape.sort_results(r, sort)\n\n                assert_data_shape.assert_data_shape(\n                    res, result, self.fail, message=msg)\n            except gel.UnsupportedFeatureError as e:\n                if 'Default variables are not supported' in str(e):\n                    # Whatever.\n                    pass\n                else:\n                    raise\n\n        res = self.graphql_query(\n            query,\n            operation_name=operation_name,\n            use_http_post=use_http_post,\n            variables=variables,\n            globals=globals,\n            deprecated_globals=deprecated_globals,\n            config=config,\n        )\n\n        if sort is not None:\n            # GQL will always have a single object returned. The data is\n            # in the top-level fields, so that's what needs to be sorted.\n            for r in res.values():\n                assert_data_shape.sort_results(r, sort)\n\n        assert_data_shape.assert_data_shape(res, result, self.fail, message=msg)\n        return res\n\n\nclass MockHttpServerHandler(http.server.BaseHTTPRequestHandler):\n    def get_server_and_path(self) -> tuple[str, str]:\n        server = f'http://{self.headers.get(\"Host\")}'\n        return server, self.path\n\n    def do_GET(self):\n        self.close_connection = False\n        server, path = self.get_server_and_path()\n        self.server.owner.handle_request(\"GET\", server, path, self)\n\n    def do_POST(self):\n        self.close_connection = False\n        server, path = self.get_server_and_path()\n        self.server.owner.handle_request(\"POST\", server, path, self)\n\n    def log_message(self, *args):\n        pass\n\n\nclass MultiHostMockHttpServerHandler(MockHttpServerHandler):\n    def get_server_and_path(self) -> tuple[str, str]:\n        # Path looks like:\n        # http://127.0.0.1:32881/https%3A//slack.com/.well-known/openid-configuration\n        raw_url = urllib.parse.unquote(self.path.lstrip(\"/\"))\n        url = urllib.parse.urlparse(raw_url)\n        return (f\"{url.scheme}://{url.netloc}\", url.path.lstrip(\"/\"))\n\n\nResponseType = tuple[str, int] | tuple[str, int, dict[str, str]]\n\n\n@dataclasses.dataclass\nclass RequestDetails:\n    headers: dict[str, str | Any]\n    query_params: dict[str, list[str]]\n    body: Optional[str]\n\n\nclass MockHttpServer:\n    def __init__(\n        self,\n        handler_type: type[MockHttpServerHandler] = MockHttpServerHandler,\n        port: int = 0,\n    ) -> None:\n        self._port = port\n        self.has_started = threading.Event()\n        self.routes: dict[\n            tuple[str, str, str],\n            (\n                ResponseType\n                | Callable[\n                    [MockHttpServerHandler, RequestDetails], ResponseType\n                ]\n            ),\n        ] = {}\n        self.requests: dict[tuple[str, str, str], list[RequestDetails]] = {}\n        self.url: Optional[str] = None\n        self.handler_type = handler_type\n\n    def get_base_url(self) -> str:\n        if self.url is None:\n            raise RuntimeError(\"mock server is not running\")\n        return self.url\n\n    def register_route_handler(\n        self,\n        method: str,\n        server: str,\n        path: str,\n    ):\n        def wrapper(\n            handler: (\n                ResponseType\n                | Callable[\n                    [MockHttpServerHandler, RequestDetails], ResponseType\n                ]\n            ),\n        ):\n            self.routes[(method, server, path)] = handler\n            return handler\n\n        return wrapper\n\n    def handle_request(\n        self,\n        method: str,\n        server: str,\n        path: str,\n        handler: MockHttpServerHandler,\n    ):\n        # `handler` is documented here:\n        # https://docs.python.org/3/library/http.server.html#http.server.BaseHTTPRequestHandler\n        key = (method, server, path)\n        if key not in self.requests:\n            self.requests[key] = []\n\n        # Parse and save the request details\n        parsed_path = urllib.parse.urlparse(path)\n        headers = {k.lower(): v for k, v in dict(handler.headers).items()}\n        query_params = urllib.parse.parse_qs(parsed_path.query)\n        if \"content-length\" in headers:\n            body = handler.rfile.read(int(headers[\"content-length\"])).decode()\n        else:\n            body = None\n\n        request_details = RequestDetails(\n            headers=headers,\n            query_params=query_params,\n            body=body,\n        )\n        self.requests[key].append(request_details)\n        if key not in self.routes:\n            error_message = (\n                f\"No route handler for {key}\\n\\n\"\n                f\"Available routes:\\n{self.routes}\"\n            )\n            handler.send_error(404, message=error_message)\n            return\n\n        registered_handler = self.routes[key]\n\n        if callable(registered_handler):\n            try:\n                handler_result = registered_handler(handler, request_details)\n                if len(handler_result) == 2:\n                    response, status = handler_result\n                    additional_headers = None\n                elif len(handler_result) == 3:\n                    response, status, additional_headers = handler_result\n            except Exception:\n                handler.send_error(500)\n                raise\n        else:\n            if len(registered_handler) == 2:\n                response, status = registered_handler\n                additional_headers = None\n            elif len(registered_handler) == 3:\n                response, status, additional_headers = registered_handler\n\n        accept_header = request_details.headers.get(\n            \"accept\", \"application/json\"\n        )\n\n        if (\n            accept_header.startswith(\"application/json\")\n            or (\n                accept_header.startswith(\"application/\")\n                and \"vnd.\" in accept_header\n                and \"+json\" in accept_header\n            )\n            or accept_header == \"*/*\"\n        ):\n            content_type = \"application/json\"\n        elif accept_header.startswith(\"application/x-www-form-urlencoded\"):\n            content_type = \"application/x-www-form-urlencoded\"\n        else:\n            handler.send_error(\n                415, f\"Unsupported accept header: {accept_header}\"\n            )\n            return\n\n        data = response.encode()\n\n        handler.send_response(status)\n        handler.send_header(\"Content-Type\", content_type)\n        handler.send_header(\"Content-Length\", str(len(data)))\n        if additional_headers is not None:\n            for header, value in additional_headers.items():\n                handler.send_header(header, value)\n        handler.end_headers()\n        handler.wfile.write(data)\n\n    def start(self):\n        assert not hasattr(self, \"_http_runner\")\n        self._http_runner = threading.Thread(target=self._http_worker)\n        self._http_runner.start()\n        self.has_started.wait()\n        self.url = f\"http://{self._address[0]}:{self._address[1]}/\"\n\n    def __enter__(self):\n        self.start()\n        return self\n\n    def _http_worker(self):\n        self._http_server = http.server.HTTPServer(\n            (\"localhost\", self._port), self.handler_type\n        )\n        self._http_server.owner = self\n        self._address = self._http_server.server_address\n        self.has_started.set()\n        self._http_server.serve_forever(poll_interval=0.01)\n        self._http_server.server_close()\n\n    def stop(self):\n        self._http_server.shutdown()\n        if self._http_runner is not None:\n            self._http_runner.join(timeout=60)\n            if self._http_runner.is_alive():\n                raise RuntimeError(\"Mock HTTP server failed to stop\")\n            self._http_runner = None\n\n    def __exit__(self, *exc):\n        self.stop()\n        self.url = None\n"
  },
  {
    "path": "edb/testbase/lang.py",
    "content": "# mypy: ignore-errors\n\n#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Optional\n\nimport typing\nimport functools\nimport os\nimport re\nimport unittest\n\nfrom edb.common import span\nfrom edb.common import debug\nfrom edb.common import devmode\nfrom edb.common import markup\n\nfrom edb import buildmeta\nfrom edb import errors\nfrom edb import edgeql\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import parser as qlparser\nfrom edb.edgeql.parser import grammar as qlgrammar\nfrom edb.edgeql import qltypes\n\nfrom edb.server import compiler as edbcompiler\n\nfrom edb.schema import ddl as s_ddl\nfrom edb.schema import delta as sd\nfrom edb.schema import migrations as s_migrations  # noqa\nfrom edb.schema import reflection as s_refl\nfrom edb.schema import schema as s_schema\nfrom edb.schema import std as s_std\nfrom edb.schema import utils as s_utils\nfrom edb.schema import modules as s_mod\n\n\ndef must_fail(exc_type, exc_msg_re=None, **kwargs):\n    \"\"\"A decorator to ensure that the test fails with a specific exception.\n\n    If exc_msg_re is passed, assertRaisesRegex will be used to match the\n    exception message.\n\n    Example:\n\n        @must_fail(EdgeQLSyntaxError,\n                   'non-default argument follows', line=2, col=61)\n        def test_edgeql_syntax_1(self):\n            ...\n    \"\"\"\n    def wrap(func):\n        args = (exc_type,)\n        if exc_msg_re is not None:\n            args += (exc_msg_re,)\n\n        _set_spec(func, 'must_fail', (args, kwargs))\n        return func\n    return wrap\n\n\ndef _set_spec(func, name, attrs):\n    try:\n        spec = func.test_spec\n    except AttributeError:\n        spec = func.test_spec = {}\n\n    assert name not in spec\n    spec[name] = attrs\n\n\nclass DocTestMeta(type(unittest.TestCase)):\n    def __new__(mcls, name, bases, dct):\n        for attr, meth in tuple(dct.items()):\n            if attr.startswith('test_') and meth.__doc__:\n\n                @functools.wraps(meth)\n                def wrapper(self, meth=meth, doc=meth.__doc__):\n                    spec = getattr(meth, 'test_spec', {})\n                    spec['test_name'] = meth.__name__\n\n                    if doc:\n                        output = error = None\n\n                        source, _, output = doc.partition('\\n% OK %')\n\n                        if not output:\n                            source, _, error = doc.partition('\\n% ERROR %')\n\n                            if not error:\n                                output = None\n                            else:\n                                output = error\n                    else:\n                        source = output = None\n\n                    self._run_test(source=source, spec=spec, expected=output)\n\n                dct[attr] = wrapper\n\n        return super().__new__(mcls, name, bases, dct)\n\n\nclass BaseDocTest(unittest.TestCase, metaclass=DocTestMeta):\n    parser_debug_flag = ''\n    re_filter: Optional[typing.Pattern[str]] = None\n\n    def _run_test(self, *, source, spec=None, expected=None):\n        if spec and 'must_fail' in spec:\n            spec_args, spec_kwargs = spec['must_fail']\n\n            if len(spec_args) == 1:\n                assertRaises = self.assertRaises\n            else:\n                assertRaises = self.assertRaisesRegex\n\n            with assertRaises(*spec_args) as cm:\n                return self.run_test(source=source, spec=spec,\n                                     expected=expected)\n\n            if cm.exception:\n                exc = cm.exception\n                for attr_name, expected_val in spec_kwargs.items():\n                    val = getattr(exc, attr_name)\n                    if val != expected_val:\n                        raise AssertionError(\n                            f'must_fail: attribute {attr_name!r} is '\n                            f'{val} (expected is {expected_val!r})') from exc\n        else:\n            return self.run_test(source=source, spec=spec, expected=expected)\n\n    def run_test(self, *, source, spec, expected=None):\n        raise NotImplementedError\n\n    def assert_equal(\n        self,\n        expected,\n        result,\n        *,\n        re_filter: Optional[str] = None,\n        message: Optional[str] = None\n    ) -> None:\n        if re_filter is None:\n            re_filter = self.re_filter\n\n        if re_filter is not None:\n            expected_stripped = re_filter.sub('', expected).lower()\n            result_stripped = re_filter.sub('', result).lower()\n        else:\n            expected_stripped = expected.lower()\n            result_stripped = result.lower()\n\n        self.assertEqual(\n            expected_stripped,\n            result_stripped,\n            (f'{message if message else \"\"}' +\n                f'\\nexpected:\\n{expected}\\nreturned:\\n{result}')\n        )\n\n\nclass BaseSyntaxTest(BaseDocTest):\n    ast_to_source: Optional[Any] = None\n    markup_dump_lexer: Optional[str] = None\n\n    @classmethod\n    def get_grammar_token(cls) -> type[qlgrammar.tokens.GrammarToken]:\n        raise NotImplementedError\n\n    def run_test(self, *, source, spec, expected=None):\n        debug = bool(os.environ.get(self.parser_debug_flag))\n        if debug:\n            markup.dump_code(source, lexer=self.markup_dump_lexer)\n\n        inast = qlparser.parse(self.get_grammar_token(), source)\n\n        if debug:\n            markup.dump(inast)\n\n        # make sure that the AST has context\n        span.SpanValidator().visit(inast)\n\n        processed_src = self.ast_to_source(inast)\n\n        if debug:\n            markup.dump_code(processed_src, lexer=self.markup_dump_lexer)\n\n        expected_src = source if expected is None else expected\n\n        self.assert_equal(expected_src, processed_src)\n\n\n_std_schema = None\n_refl_schema = None\n_schema_class_layout = None\n\n\ndef _load_std_schema():\n    global _std_schema\n    if _std_schema is None:\n        std_dirs_hash = buildmeta.hash_dirs(s_std.CACHE_SRC_DIRS)\n        schema = None\n\n        if devmode.is_in_dev_mode():\n            schema = buildmeta.read_data_cache(\n                std_dirs_hash, 'transient-stdschema.pickle')\n\n        if schema is None:\n            schema = s_schema.EMPTY_SCHEMA\n            for modname in [*s_schema.STD_SOURCES, *s_schema.TESTMODE_SOURCES]:\n                schema = s_std.load_std_module(schema, modname)\n            schema, _ = s_std.make_schema_version(schema)\n            schema, _ = s_std.make_global_schema_version(schema)\n\n        if devmode.is_in_dev_mode():\n            buildmeta.write_data_cache(\n                schema, std_dirs_hash, 'transient-stdschema.pickle')\n\n        _std_schema = schema\n\n    return _std_schema\n\n\ndef _load_reflection_schema():\n    global _refl_schema\n    global _schema_class_layout\n\n    if _refl_schema is None:\n        std_dirs_hash = buildmeta.hash_dirs(s_std.CACHE_SRC_DIRS)\n\n        cache = None\n        if devmode.is_in_dev_mode():\n            cache = buildmeta.read_data_cache(\n                std_dirs_hash, 'transient-reflschema.pickle')\n\n        if cache is not None:\n            reflschema, classlayout = cache\n        else:\n            std_schema = _load_std_schema()\n            reflection = s_refl.generate_structure(std_schema)\n            classlayout = reflection.class_layout\n            context = sd.CommandContext(stdmode=True)\n            reflschema = reflection.intro_schema_delta.apply(\n                std_schema, context)\n\n            if devmode.is_in_dev_mode():\n                buildmeta.write_data_cache(\n                    (reflschema, classlayout),\n                    std_dirs_hash,\n                    'transient-reflschema.pickle',\n                )\n\n        _refl_schema = reflschema\n        _schema_class_layout = classlayout\n\n    return _refl_schema, _schema_class_layout\n\n\ndef new_compiler():\n    std_schema = _load_std_schema()\n    refl_schema, layout = _load_reflection_schema()\n\n    return edbcompiler.new_compiler(\n        std_schema=std_schema,\n        reflection_schema=refl_schema,\n        schema_class_layout=layout,\n    )\n\n\nclass BaseSchemaTest(BaseDocTest):\n    DEFAULT_MODULE = 'default'\n    SCHEMA: Optional[str] = None\n\n    schema: s_schema.Schema\n\n    @classmethod\n    def setUpClass(cls):\n        script = cls.get_schema_script()\n        if script is not None:\n            cls.schema = cls.run_ddl(_load_std_schema(), script)\n        else:\n            cls.schema = _load_std_schema()\n\n    @classmethod\n    def run_ddl(cls, schema, ddl, default_module=s_mod.DEFAULT_MODULE_ALIAS):\n        statements = edgeql.parse_block(ddl)\n\n        current_schema = schema\n        target_schema = None\n        migration_schema = None\n        migration_target = None\n        migration_script = []\n\n        for stmt in statements:\n            if isinstance(stmt, qlast.StartMigration):\n                # START MIGRATION\n                if target_schema is None:\n                    target_schema = _load_std_schema()\n\n                migration_target, _ = s_ddl.apply_sdl(\n                    stmt.target,\n                    base_schema=target_schema,\n                    testmode=True,\n                )\n\n                migration_schema = current_schema\n\n                ddl_plan = None\n\n            elif isinstance(stmt, qlast.PopulateMigration):\n                # POPULATE MIGRATION\n                if migration_target is None:\n                    raise errors.QueryError(\n                        'unexpected POPULATE MIGRATION:'\n                        ' not currently in a migration block',\n                        span=stmt.span,\n                    )\n\n                migration_diff = s_ddl.delta_schemas(\n                    migration_schema,\n                    migration_target,\n                )\n\n                if debug.flags.delta_plan:\n                    debug.header('Populate Migration Diff')\n                    debug.dump(migration_diff, schema=schema)\n\n                new_ddl = s_ddl.ddlast_from_delta(\n                    migration_schema,\n                    migration_target,\n                    migration_diff,\n                )\n\n                migration_script.extend(new_ddl)\n\n                if debug.flags.delta_plan:\n                    debug.header('Populate Migration DDL AST')\n                    text = []\n                    for cmd in new_ddl:\n                        debug.dump(cmd)\n                        text.append(edgeql.generate_source(cmd, pretty=True))\n                    debug.header('Populate Migration DDL Text')\n                    debug.dump_code(';\\n'.join(text) + ';')\n\n            elif isinstance(stmt, qlast.DescribeCurrentMigration):\n                # This is silly, and we don't bother doing all the work,\n                # but try to catch when doing the JSON thing wouldn't work.\n                if stmt.language is qltypes.DescribeLanguage.JSON:\n                    guided_diff = s_ddl.delta_schemas(\n                        migration_schema,\n                        migration_target,\n                        generate_prompts=True,\n                    )\n                    s_ddl.statements_from_delta(\n                        schema,\n                        migration_target,\n                        guided_diff,\n                    )\n\n            elif isinstance(stmt, qlast.CommitMigration):\n                if migration_target is None:\n                    raise errors.QueryError(\n                        'unexpected COMMIT MIGRATION:'\n                        ' not currently in a migration block',\n                        span=stmt.span,\n                    )\n\n                last_migration = current_schema.get_last_migration()\n                if last_migration:\n                    last_migration_ref = s_utils.name_to_ast_ref(\n                        last_migration.get_name(current_schema),\n                    )\n                else:\n                    last_migration_ref = None\n\n                create_migration = qlast.CreateMigration(\n                    body=qlast.NestedQLBlock(commands=migration_script),\n                    parent=last_migration_ref,\n                )\n\n                ddl_plan = s_ddl.delta_from_ddl(\n                    create_migration,\n                    schema=migration_schema,\n                    modaliases={None: default_module},\n                    testmode=True,\n                )\n\n                if debug.flags.delta_plan:\n                    debug.header('Delta Plan')\n                    debug.dump(ddl_plan, schema=schema)\n\n                migration_schema = None\n                migration_target = None\n                migration_script = []\n\n            elif isinstance(stmt, qlast.DDLCommand):\n                if migration_target is not None:\n                    migration_script.append(stmt)\n                    ddl_plan = None\n                else:\n                    ddl_plan = s_ddl.delta_from_ddl(\n                        stmt,\n                        schema=current_schema,\n                        modaliases={None: default_module},\n                        testmode=True,\n                    )\n\n                    if debug.flags.delta_plan:\n                        debug.header('Delta Plan')\n                        debug.dump(ddl_plan, schema=schema)\n            else:\n                raise ValueError(\n                    f'unexpected {stmt!r} in compiler setup script')\n\n            if ddl_plan is not None:\n                context = sd.CommandContext()\n                context.testmode = True\n                current_schema = ddl_plan.apply(current_schema, context)\n\n        return current_schema\n\n    @classmethod\n    def load_schema(\n        cls, source: str, modname: Optional[str] = None\n    ) -> s_schema.Schema:\n        if not modname:\n            modname = cls.DEFAULT_MODULE\n        sdl_schema = qlparser.parse_sdl(f'module {modname} {{ {source} }}')\n        schema = _load_std_schema()\n        return s_ddl.apply_sdl(\n            sdl_schema,\n            base_schema=schema,\n        )[0]\n\n    @classmethod\n    def get_schema_script(cls):\n        script = ''\n\n        # look at all SCHEMA entries and potentially create multiple modules\n        schema = []\n        for name in dir(cls):\n            val = getattr(cls, name)\n            m = re.match(r'^SCHEMA(?:_(\\w+))?', name)\n            if m and val:\n                module_name = (m.group(1)\n                               or 'default').lower().replace('_', '::')\n\n                if '\\n' in val:\n                    # Inline schema source\n                    module = val\n                else:\n                    with open(val, 'r') as sf:\n                        module = sf.read()\n\n                schema.append(f'\\nmodule {module_name} {{ {module} }}')\n\n        if schema:\n            script += f'\\nSTART MIGRATION'\n            script += f' TO {{ {\"\".join(schema)} }};'\n            script += f'\\nPOPULATE MIGRATION;'\n            script += f'\\nCOMMIT MIGRATION;'\n\n        return script.strip(' \\n')\n\n\nclass BaseSchemaLoadTest(BaseSchemaTest):\n    def run_test(self, *, source, spec, expected=None):\n        self.load_schema(source)\n\n\nclass BaseEdgeQLCompilerTest(BaseSchemaTest):\n    @classmethod\n    def get_schema_script(cls):\n        script = super().get_schema_script()\n        if not script:\n            raise ValueError(\n                'compiler test cases must define at least one '\n                'schema in the SCHEMA[_MODNAME] class attribute.')\n        return script\n"
  },
  {
    "path": "edb/testbase/proc.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport asyncio\nimport socket\nimport sys\nimport unittest\n\nfrom edb.common import devmode\nfrom . import server\n\nexec(sys.argv[1], globals(), locals())\n\n\nclass ProcTest(server.TestCase):\n    def notify_parent(self, mark):\n        self.parent_writer.write(str(mark).encode() + b\"\\n\")\n\n    async def wait_for_parent(self, mark):\n        self.assertEqual(\n            (await self.parent_reader.readline()).strip(),\n            str(mark).encode(),\n        )\n\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n\n        async def _setup():\n            sock = socket.fromfd(\n                int(sys.argv[3]), socket.AF_UNIX, socket.SOCK_STREAM\n            )\n            cls.parent_reader, cls.parent_writer = (\n                await asyncio.open_connection(sock=sock)\n            )\n        cls.loop.run_until_complete(_setup())\n\n    exec(sys.argv[2], globals(), locals())\n\n\ndef main():\n    cov_config = devmode.CoverageConfig.from_environ()\n    if cov_config:\n        cov = cov_config.new_coverage_object()\n        cov.start()\n        try:\n            unittest.main(argv=sys.argv[:1], verbosity=2)\n        finally:\n            cov.stop()\n            cov.save()\n    else:\n        unittest.main(argv=sys.argv[:1], verbosity=2)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "edb/testbase/protocol/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n"
  },
  {
    "path": "edb/testbase/protocol/test.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom edb.testbase import server\n\nfrom edb.protocol import protocol  # type: ignore\nfrom edb.protocol.protocol import Connection\n\n\nclass ProtocolTestCase(server.DatabaseTestCase):\n\n    PARALLELISM_GRANULARITY = 'database'\n    BASE_TEST_CLASS = True\n\n    con: Connection\n\n    def setUp(self):\n        self.con = self.loop.run_until_complete(\n            protocol.new_connection(\n                **self.get_connect_args(database=self.get_database_name())\n            )\n        )\n\n    def tearDown(self):\n        try:\n            self.loop.run_until_complete(\n                self.con.aclose()\n            )\n        finally:\n            self.con = None\n"
  },
  {
    "path": "edb/testbase/serutils.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport dataclasses\nimport datetime\nimport decimal\nimport functools\nimport uuid\n\nimport edgedb\n\n\n@functools.singledispatch\ndef serialize(o):\n    raise TypeError(f'cannot serialize type {type(o)}')\n\n\n@serialize.register\ndef _tuple(o: edgedb.Tuple):\n    return [serialize(el) for el in o]\n\n\n@serialize.register\ndef _namedtuple(o: edgedb.NamedTuple):\n    return {attr: serialize(getattr(o, attr)) for attr in dir(o)}\n\n\n@serialize.register\ndef _object(o: edgedb.Object):\n    # We iterate over dataclasses.fields(o) (instead of dir(o))\n    # because it contains both regular pointers and link properties,\n    # and is I think the only current way to extract the names of all\n    # the link properties\n    attrs = [field.name for field in dataclasses.fields(o)]\n    return {attr: serialize(getattr(o, attr)) for attr in attrs}\n\n\n@serialize.register(edgedb.Set)\n@serialize.register(edgedb.Array)\ndef _set(o):\n    return [serialize(el) for el in o]\n\n\n@serialize.register(uuid.UUID)\ndef _stringify(o):\n    return str(o)\n\n\n@serialize.register(int)\n@serialize.register(float)\n@serialize.register(str)\n@serialize.register(bytes)\n@serialize.register(bool)\n@serialize.register(type(None))\n@serialize.register(decimal.Decimal)\n@serialize.register(datetime.timedelta)\n@serialize.register(edgedb.RelativeDuration)\n@serialize.register(edgedb.DateDuration)\ndef _scalar(o):\n    return o\n\n\n@serialize.register\ndef _datetime(o: datetime.datetime):\n    return o.isoformat()\n\n\n@serialize.register\ndef _date(o: datetime.date):\n    return o.isoformat()\n\n\n@serialize.register\ndef _time(o: datetime.time):\n    return o.isoformat()\n\n\n@serialize.register\ndef _enum(o: edgedb.EnumValue):\n    return str(o)\n\n\n@serialize.register\ndef _record(o: edgedb.Record):\n    return {k: serialize(v) for k, v in o.as_dict().items()}\n\n\n@serialize.register\ndef _range(o: edgedb.Range):\n    return {\n        'lower': serialize(o.lower),\n        'inc_lower': o.inc_lower,\n        'upper': serialize(o.upper),\n        'inc_upper': o.inc_upper,\n        'empty': o.is_empty(),\n    }\n\n\n@serialize.register\ndef _multirane(o: edgedb.MultiRange):\n    return [serialize(el) for el in o]\n\n\n@serialize.register\ndef _cfg_memory(o: edgedb.ConfigMemory):\n    return str(o)\n"
  },
  {
    "path": "edb/testbase/server.py",
    "content": "# mypy: ignore-errors\n\n#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Optional,\n    Iterable,\n    Literal,\n    Sequence,\n    NamedTuple,\n    TYPE_CHECKING,\n)\nimport typing\n\nimport asyncio\nimport atexit\nimport base64\nimport contextlib\nimport dataclasses\nimport functools\nimport heapq\nimport http\nimport http.client\nimport inspect\nimport json\nimport os\nimport pathlib\nimport random\nimport re\nimport secrets\nimport shlex\nimport socket\nimport ssl\nimport struct\nimport subprocess\nimport sys\nimport tempfile\nimport time\nimport unittest\nimport urllib\n\nimport edgedb\n\nfrom edb.edgeql import quote as qlquote\nfrom edb.server import args as edgedb_args\nfrom edb.testbase import cluster as edgedb_cluster\nfrom edb.server import pgcluster\nfrom edb.server import defines as edgedb_defines\nfrom edb.server import auth\nfrom edb.server.pgconnparams import ConnectionParams\n\nfrom edb.common import assert_data_shape\nfrom edb.common import devmode\nfrom edb.common import debug\nfrom edb.common import retryloop\nfrom edb.common import secretkey\n\nfrom edb import buildmeta\nfrom edb import protocol\nfrom edb.protocol import protocol as test_protocol\nfrom edb.testbase import serutils\n\nfrom edb.testbase import connection as tconn\n\n\nif TYPE_CHECKING:\n    import asyncpg\n    DatabaseName = str\n    SetupScript = str\n\n\ndef _add_test(result, test):\n    # test is a tuple of the same test method that may zREPEAT\n    cls = type(test[0])\n    try:\n        methods, repeat_methods = result[cls]\n    except KeyError:\n        # put zREPEAT tests in a separate list\n        methods = []\n        repeat_methods = []\n        result[cls] = methods, repeat_methods\n\n    methods.append(test[0])\n    if len(test) > 1:\n        repeat_methods.extend(test[1:])\n\n\ndef _merge_results(result):\n    # make sure all the zREPEAT tests comes in the end\n    return {k: v[0] + v[1] for k, v in result.items()}\n\n\ndef _get_test_cases(tests):\n    result = {}\n\n    for test in tests:\n        if isinstance(test, unittest.TestSuite):\n            result.update(_get_test_cases(test._tests))\n        elif not getattr(test, '__unittest_skip__', False):\n            _add_test(result, (test,))\n\n    return result\n\n\ndef get_test_cases(tests):\n    return _merge_results(_get_test_cases(tests))\n\n\nbag = assert_data_shape.bag\n\ngenerate_jwk = auth.generate_jwk\ngenerate_tls_cert = secretkey.generate_tls_cert\n\n\nclass CustomSNI_HTTPSConnection(http.client.HTTPSConnection):\n    def __init__(self, *args, server_hostname=..., **kwargs):\n        super().__init__(*args, **kwargs)\n        self.server_hostname = server_hostname\n\n    def connect(self):\n        super(http.client.HTTPSConnection, self).connect()\n\n        if self._tunnel_host:\n            server_hostname = self._tunnel_host\n        elif self.server_hostname is not ...:\n            server_hostname = self.server_hostname\n        else:\n            server_hostname = self.host\n\n        self.sock = self._context.wrap_socket(self.sock,\n                                              server_hostname=server_hostname)\n\n    def true_close(self):\n        self.close()\n\n\nclass StubbornHttpConnection(CustomSNI_HTTPSConnection):\n\n    def close(self):\n        # Don't actually close the connection.  This allows us to\n        # test keep-alive and \"Connection: close\" headers.\n        pass\n\n    def true_close(self):\n        http.client.HTTPConnection.close(self)\n\n\nclass TestCaseMeta(type(unittest.TestCase)):\n    _database_names = set()\n\n    @staticmethod\n    def _iter_methods(bases, ns):\n        for base in bases:\n            for methname in dir(base):\n                if not methname.startswith('test_'):\n                    continue\n\n                meth = getattr(base, methname)\n                if not inspect.iscoroutinefunction(meth):\n                    continue\n\n                yield methname, meth\n\n        for methname, meth in ns.items():\n            if not methname.startswith('test_'):\n                continue\n\n            if not inspect.iscoroutinefunction(meth):\n                continue\n\n            yield methname, meth\n\n    @classmethod\n    def wrap(mcls, meth, is_repeat=False):\n        @functools.wraps(meth)\n        def wrapper(self, *args, __meth__=meth, **kwargs):\n            try_no = 1\n\n            if is_repeat and not getattr(self, 'TRANSACTION_ISOLATION', False):\n                raise unittest.SkipTest()\n\n            self.is_repeat = is_repeat\n            while True:\n                try:\n                    # There might be unobvious serializability\n                    # anomalies across the test suite, so, rather\n                    # than hunting them down every time, simply\n                    # retry the test.\n                    self.loop.run_until_complete(\n                        __meth__(self, *args, **kwargs))\n                except (edgedb.TransactionSerializationError,\n                        edgedb.TransactionDeadlockError):\n                    if (\n                        try_no == 10\n                        # Only do a retry loop when we have a transaction\n                        or not getattr(self, 'TRANSACTION_ISOLATION', False)\n                    ):\n                        raise\n                    else:\n                        self.loop.run_until_complete(self.xact.rollback())\n                        self.loop.run_until_complete(asyncio.sleep(\n                            min((2 ** try_no) * 0.1, 10)\n                            + random.randrange(100) * 0.001\n                        ))\n                        self.xact = self.con.transaction()\n                        self.loop.run_until_complete(self.xact.start())\n\n                        try_no += 1\n                else:\n                    break\n\n        return wrapper\n\n    @classmethod\n    def add_method(mcls, methname, ns, meth):\n        ns[methname] = mcls.wrap(meth)\n\n        # If EDGEDB_TEST_REPEATS is set, duplicate all the tests.\n        # This is valuable because it should exercise the function\n        # cache.\n        if (\n            os.environ.get('EDGEDB_TEST_REPEATS', None)\n            and methname.startswith('test_')\n        ):\n            new = methname.replace('test_', 'test_zREPEAT_', 1)\n            ns[new] = mcls.wrap(meth, is_repeat=True)\n\n    def __new__(mcls, name, bases, ns):\n        for methname, meth in mcls._iter_methods(bases, ns.copy()):\n            if methname in ns:\n                del ns[methname]\n            mcls.add_method(methname, ns, meth)\n\n        cls = super().__new__(mcls, name, bases, ns)\n        if not ns.get('BASE_TEST_CLASS') and hasattr(cls, 'get_database_name'):\n            dbname = cls.get_database_name()\n\n            if name in mcls._database_names:\n                raise TypeError(\n                    f'{name} wants duplicate database name: {dbname}')\n\n            mcls._database_names.add(name)\n\n        return cls\n\n\nclass TestCase(unittest.TestCase, metaclass=TestCaseMeta):\n    is_repeat: bool = False\n\n    @classmethod\n    def setUpClass(cls):\n        loop = asyncio.new_event_loop()\n        asyncio.set_event_loop(loop)\n        cls.loop = loop\n\n    @classmethod\n    def tearDownClass(cls):\n        cls.loop.close()\n        asyncio.set_event_loop(None)\n\n    @classmethod\n    def uses_server(cls) -> bool:\n        return True\n\n    def add_fail_notes(self, **kwargs):\n        if getattr(self, 'fail_notes', None) is None:\n            self.fail_notes = {}\n        self.fail_notes.update(kwargs)\n\n    @contextlib.contextmanager\n    def annotate(self, **kwargs):\n        # Annotate the test in case the nested block of code fails.\n        try:\n            yield\n        except Exception:\n            self.add_fail_notes(**kwargs)\n            raise\n\n    @contextlib.contextmanager\n    def assertRaisesRegex(self, exception, regex, msg=None, **kwargs):\n        with super().assertRaisesRegex(exception, regex, msg=msg):\n            try:\n                yield\n            except BaseException as e:\n                if isinstance(e, exception):\n                    for attr_name, expected_val in kwargs.items():\n                        val = getattr(e, attr_name)\n                        if val != expected_val:\n                            raise self.failureException(\n                                f'{exception.__name__} context attribute '\n                                f'{attr_name!r} is {val} (expected '\n                                f'{expected_val!r})') from e\n                raise\n\n    @staticmethod\n    def try_until_succeeds(\n        *,\n        ignore: type[Exception] | tuple[type[Exception]] | None = None,\n        ignore_regexp: str | None = None,\n        delay: float=0.5,\n        timeout: float=5\n    ):\n        \"\"\"Retry a block of code a few times ignoring the specified errors.\n\n        Example:\n\n            async for tr in self.try_until_succeeds(\n                    ignore=edgedb.AuthenticationError):\n                async with tr:\n                    await edgedb.connect(...)\n\n        \"\"\"\n        if ignore is None and ignore_regexp is None:\n            raise ValueError('Expect at least one of ignore or ignore_regexp')\n        return retryloop.RetryLoop(\n            backoff=retryloop.const_backoff(delay),\n            timeout=timeout,\n            ignore=ignore,\n            ignore_regexp=ignore_regexp,\n        )\n\n    @staticmethod\n    def try_until_fails(\n        *,\n        wait_for: type[Exception] | tuple[type[Exception]] | None = None,\n        wait_for_regexp: str | None = None,\n        delay: float=0.5,\n        timeout: float=5\n    ):\n        \"\"\"Retry a block of code a few times until the specified error happens.\n\n        Example:\n\n            async for tr in self.try_until_fails(\n                    wait_for=edgedb.AuthenticationError):\n                async with tr:\n                    await edgedb.connect(...)\n\n        \"\"\"\n        if wait_for is None and wait_for_regexp is None:\n            raise ValueError(\n                'Expect at least one of wait_for or wait_for_regexp'\n            )\n        return retryloop.RetryLoop(\n            backoff=retryloop.const_backoff(delay),\n            timeout=timeout,\n            wait_for=wait_for,\n            wait_for_regexp=wait_for_regexp,\n        )\n\n    def addCleanup(self, func, *args, **kwargs):\n        @functools.wraps(func)\n        def cleanup():\n            res = func(*args, **kwargs)\n            if inspect.isawaitable(res):\n                self.loop.run_until_complete(res)\n        super().addCleanup(cleanup)\n\n    def __getstate__(self):\n        # TestCases get pickled when run in in separate OS processes\n        # via `edb test -jN`. If they reference any unpickleable objects,\n        # the test engine crashes with no indication why and on what test.\n        # That said, most of the TestCases' guts are not needed for the\n        # test results renderer, so we only keep the essential attributes\n        # here.\n\n        outcome = self._outcome\n        if outcome is not None and getattr(outcome, \"errors\", []):\n            # We don't use `test._outcome` to render errors in\n            # our renderers.\n            outcome.errors = []\n\n        return {\n            '_testMethodName': self._testMethodName,\n            '_outcome': outcome,\n            '_testMethodDoc': self._testMethodDoc,\n            '_subtest': self._subtest,\n            '_cleanups': [],\n            '_type_equality_funcs': self._type_equality_funcs,\n            'fail_notes': getattr(self, 'fail_notes', None),\n        }\n\n    @contextlib.contextmanager\n    def assertChange(\n        self, measure_fn: typing.Callable[[], int | float],\n        expected_change: int | float\n    ):\n        before = measure_fn()\n        try:\n            yield\n        finally:\n            after = measure_fn()\n            change = after - before\n            self.assertEqual(expected_change, change)\n\n\nclass RollbackException(Exception):\n    pass\n\n\nclass RollbackChanges:\n    def __init__(self, con):\n        self._conn = con\n\n    async def __aenter__(self):\n        self._tx = self._conn.transaction()\n        await self._tx.start()\n\n    async def __aexit__(self, exc_type, exc, tb):\n        await self._tx.rollback()\n\n\nclass TestCaseWithHttpClient(TestCase):\n    @classmethod\n    def get_api_prefix(cls):\n        return ''\n\n    @classmethod\n    @contextlib.contextmanager\n    def http_con(\n        cls,\n        server,\n        keep_alive=True,\n        client_cert_file=None,\n        client_key_file=None,\n        **kwargs,\n    ):\n        conn_args = server.get_connect_args()\n        tls_context = ssl.create_default_context(\n            ssl.Purpose.SERVER_AUTH,\n            cafile=conn_args[\"tls_ca_file\"],\n        )\n        tls_context.check_hostname = False\n        if any((client_cert_file, client_key_file)):\n            tls_context.load_cert_chain(client_cert_file, client_key_file)\n        if keep_alive:\n            ConCls = StubbornHttpConnection\n        else:\n            ConCls = CustomSNI_HTTPSConnection\n\n        con = ConCls(\n            conn_args[\"host\"],\n            conn_args[\"port\"],\n            context=tls_context,\n            **kwargs,\n        )\n        con.connect()\n        try:\n            yield con\n        finally:\n            con.true_close()\n\n    @classmethod\n    def http_con_send_request(\n        cls,\n        http_con: http.client.HTTPConnection,\n        params: Optional[dict[str, str]] = None,\n        *,\n        prefix: Optional[str] = None,\n        headers: Optional[dict[str, str]] = None,\n        method: str = \"GET\",\n        body: bytes = b\"\",\n        path: str = \"\",\n    ):\n        url = f'https://{http_con.host}:{http_con.port}'\n        if prefix is None:\n            prefix = cls.get_api_prefix()\n        if prefix:\n            url = f'{url}{prefix}'\n        if path:\n            url = f'{url}/{path}'\n        if params is not None:\n            url = f'{url}?{urllib.parse.urlencode(params)}'\n        if headers is None:\n            headers = {}\n        http_con.request(method, url, body=body, headers=headers)\n\n    @classmethod\n    def http_con_read_response(\n        cls,\n        http_con: http.client.HTTPConnection,\n    ) -> tuple[bytes, dict[str, str], int]:\n        resp = http_con.getresponse()\n        resp_body = resp.read()\n        resp_headers = {k.lower(): v for k, v in resp.getheaders()}\n        return resp_body, resp_headers, resp.status\n\n    @classmethod\n    def http_con_request(\n        cls,\n        http_con: http.client.HTTPConnection,\n        params: Optional[dict[str, str]] = None,\n        *,\n        prefix: Optional[str] = None,\n        headers: Optional[dict[str, str]] = None,\n        method: str = \"GET\",\n        body: bytes = b\"\",\n        path: str = \"\",\n    ) -> tuple[bytes, dict[str, str], int]:\n        cls.http_con_send_request(\n            http_con,\n            params,\n            prefix=prefix,\n            headers=headers,\n            method=method,\n            body=body,\n            path=path,\n        )\n        return cls.http_con_read_response(http_con)\n\n    @classmethod\n    def http_con_json_request(\n        cls,\n        http_con: http.client.HTTPConnection,\n        params: Optional[dict[str, str]] = None,\n        *,\n        prefix: Optional[str] = None,\n        headers: Optional[dict[str, str]] = None,\n        body: Any,\n        path: str = \"\",\n    ):\n        response, headers, status = cls.http_con_request(\n            http_con,\n            params,\n            method=\"POST\",\n            body=json.dumps(body).encode(),\n            prefix=prefix,\n            headers={\n                \"Content-Type\": \"application/json\",\n                **(headers or {}),\n            },\n            path=path,\n        )\n\n        if status == http.HTTPStatus.OK:\n            result = json.loads(response)\n        else:\n            result = None\n\n        return result, headers, status\n\n    @classmethod\n    def http_con_binary_request(\n        cls,\n        http_con: http.client.HTTPConnection,\n        query: str,\n        proto_ver=edgedb_defines.CURRENT_PROTOCOL,\n        bearer_token: Optional[str] = None,\n        user: str = \"edgedb\",\n        database: str = \"main\",\n    ):\n        proto_ver_str = f\"v_{proto_ver[0]}_{proto_ver[1]}\"\n        mime_type = f\"application/x.edgedb.{proto_ver_str}.binary\"\n        headers = {\"Content-Type\": mime_type, \"X-EdgeDB-User\": user}\n        if bearer_token:\n            headers[\"Authorization\"] = f\"Bearer {bearer_token}\"\n        content, headers, status = cls.http_con_request(\n            http_con,\n            method=\"POST\",\n            path=f\"db/{database}\",\n            prefix=\"\",\n            body=protocol.Execute(\n                annotations=[],\n                allowed_capabilities=protocol.Capability.ALL,\n                compilation_flags=protocol.CompilationFlag(0),\n                implicit_limit=0,\n                command_text=query,\n                input_language=protocol.InputLanguage.EDGEQL,\n                output_format=protocol.OutputFormat.JSON,\n                expected_cardinality=protocol.Cardinality.AT_MOST_ONE,\n                input_typedesc_id=b\"\\0\" * 16,\n                output_typedesc_id=b\"\\0\" * 16,\n                state_typedesc_id=b\"\\0\" * 16,\n                arguments=b\"\",\n                state_data=b\"\",\n            ).dump() + protocol.Sync().dump(),\n            headers=headers,\n        )\n        content = memoryview(content)\n        uint32_unpack = struct.Struct(\"!L\").unpack\n        msgs = []\n        while content:\n            mtype = content[0]\n            (msize,) = uint32_unpack(content[1:5])\n            msg = protocol.ServerMessage.parse(mtype, content[5: msize + 1])\n            msgs.append(msg)\n            content = content[msize + 1:]\n        return msgs, headers, status\n\n\n_default_cluster = None\n\n\nasync def init_cluster(\n    data_dir=None,\n    backend_dsn=None,\n    *,\n    cleanup_atexit=True,\n    init_settings=None,\n    security=edgedb_args.ServerSecurityMode.Strict,\n    http_endpoint_security=edgedb_args.ServerEndpointSecurityMode.Optional,\n    compiler_pool_mode=edgedb_args.CompilerPoolMode.Fixed,\n) -> edgedb_cluster.BaseCluster:\n    if data_dir is not None and backend_dsn is not None:\n        raise ValueError(\n            \"data_dir and backend_dsn cannot be set at the same time\")\n    if init_settings is None:\n        init_settings = {}\n\n    log_level = 's' if not debug.flags.server else 'd'\n\n    if backend_dsn:\n        cluster = edgedb_cluster.TempClusterWithRemotePg(\n            backend_dsn,\n            testmode=True,\n            log_level=log_level,\n            data_dir_prefix='edb-test-',\n            security=security,\n            http_endpoint_security=http_endpoint_security,\n            compiler_pool_mode=compiler_pool_mode,\n        )\n        destroy = True\n    elif data_dir is None:\n        cluster = edgedb_cluster.TempCluster(\n            testmode=True,\n            log_level=log_level,\n            data_dir_prefix='edb-test-',\n            security=security,\n            http_endpoint_security=http_endpoint_security,\n            compiler_pool_mode=compiler_pool_mode,\n        )\n        destroy = True\n    else:\n        cluster = edgedb_cluster.Cluster(\n            testmode=True,\n            data_dir=data_dir,\n            log_level=log_level,\n            security=security,\n            http_endpoint_security=http_endpoint_security,\n            compiler_pool_mode=compiler_pool_mode,\n        )\n        destroy = False\n\n    pg_cluster = await cluster._get_pg_cluster()\n    if await pg_cluster.get_status() == 'not-initialized':\n        await cluster.init(server_settings=init_settings)\n\n    await cluster.start(port=0)\n    await cluster.set_test_config()\n    await cluster.set_superuser_password('test')\n\n    if cleanup_atexit:\n        atexit.register(_shutdown_cluster, cluster, destroy=destroy)\n\n    return cluster\n\n\ndef _start_cluster(\n    *,\n    loop: asyncio.AbstractEventLoop,\n    cleanup_atexit=True,\n    http_endpoint_security=None,\n):\n    global _default_cluster\n\n    if _default_cluster is None:\n        cluster_addr = os.environ.get('EDGEDB_TEST_CLUSTER_ADDR')\n        if cluster_addr:\n            conn_spec = json.loads(cluster_addr)\n            _default_cluster = edgedb_cluster.RunningCluster(**conn_spec)\n        else:\n            # This branch is not usually used - `edb test` will call\n            # init_cluster() separately and set EDGEDB_TEST_CLUSTER_ADDR\n            data_dir = os.environ.get('EDGEDB_TEST_DATA_DIR')\n            backend_dsn = os.environ.get('EDGEDB_TEST_BACKEND_DSN')\n            _default_cluster = loop.run_until_complete(\n                init_cluster(\n                    data_dir=data_dir,\n                    backend_dsn=backend_dsn,\n                    cleanup_atexit=cleanup_atexit,\n                    http_endpoint_security=http_endpoint_security,\n                )\n            )\n\n    return _default_cluster\n\n\ndef _shutdown_cluster(cluster, *, destroy=True):\n    global _default_cluster\n    _default_cluster = None\n    if cluster is not None:\n        cluster.stop()\n        if destroy:\n            cluster.destroy()\n\n\ndef _fetch_metrics(host: str, port: int, sslctx=None) -> str:\n    return _call_system_api(\n        host, port, '/metrics', return_json=False, sslctx=sslctx\n    )\n\n\ndef _fetch_server_info(host: str, port: int) -> dict[str, Any]:\n    return _call_system_api(host, port, '/server-info')\n\n\ndef _call_system_api(\n    host: str,\n    port: int,\n    path: str,\n    return_json=True,\n    sslctx=None,\n    **kwargs,\n):\n    if sslctx is None:\n        con = http.client.HTTPConnection(host, port, **kwargs)\n    else:\n        con = CustomSNI_HTTPSConnection(host, port, context=sslctx, **kwargs)\n    con.connect()\n    try:\n        con.request(\n            'GET',\n            f'http://{host}:{port}{path}'\n        )\n        resp = con.getresponse()\n        if resp.status != 200:\n            err = resp.read().decode()\n            raise AssertionError(\n                f'{path} returned non 200 HTTP status: {resp.status}\\n\\t{err}'\n            )\n        rv = resp.read().decode()\n        if return_json:\n            rv = json.loads(rv)\n        return rv\n    finally:\n        con.close()\n\n\ndef parse_metrics(metrics: str) -> dict[str, float]:\n    res = {}\n    for line in metrics.splitlines():\n        if line.startswith('#') or ' ' not in line:\n            continue\n        key, _, val = line.partition(' ')\n        res[key] = float(val)\n    return res\n\n\ndef _extract_background_errors(metrics: str) -> str | None:\n    non_zero = []\n\n    for label, total in parse_metrics(metrics).items():\n        if label.startswith('edgedb_server_background_errors_total'):\n            if total:\n                non_zero.append(\n                    f'non-zero {label!r} metric: {total}'\n                )\n\n    if non_zero:\n        return '\\n'.join(non_zero)\n    else:\n        return None\n\n\nasync def drop_db(conn, dbname):\n    await conn.execute(f'DROP BRANCH {dbname}')\n\n\nclass ClusterTestCase(TestCaseWithHttpClient):\n\n    BASE_TEST_CLASS = True\n    backend_dsn: Optional[str] = None\n\n    # Some tests may want to manage transactions manually,\n    # or affect non-transactional state, in which case\n    # TRANSACTION_ISOLATION must be set to False\n    TRANSACTION_ISOLATION = True\n\n    # By default, tests from the same testsuite may be ran in parallel in\n    # several test worker processes.  However, certain cases might exhibit\n    # pathological locking behavior, or are parallel-unsafe altogether, in\n    # which case PARALLELISM_GRANULARITY must be set to 'database', 'suite',\n    # or 'system'.  The 'database' granularity signals that no two runners\n    # may execute tests on the same database in parallel, although the tests\n    # may still run on copies of the test database.  The 'suite' granularity\n    # means that only one test worker is allowed to execute tests from this\n    # suite.  Finally, the 'system' granularity means that the test suite\n    # is not parallelizable at all and must run sequentially with respect\n    # to *all other* suites with 'system' granularity.\n    PARALLELISM_GRANULARITY = 'default'\n\n    # Turns on \"Gel developer\" mode which allows using restricted\n    # syntax like USING SQL and similar. It allows modifying standard\n    # library (e.g. declaring casts).\n    INTERNAL_TESTMODE = True\n\n    # Turns off query cache recompilation on DDL\n    ENABLE_RECOMPILATION = False\n\n    # Setup and teardown commands that run per test\n    PER_TEST_SETUP: Sequence[str] = ()\n    PER_TEST_TEARDOWN: Sequence[str] = ()\n\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        cls.cluster = _start_cluster(\n            loop=cls.loop,\n            cleanup_atexit=True,\n            http_endpoint_security=(\n                edgedb_args.ServerEndpointSecurityMode.Optional),\n        )\n        cls.has_create_database = cls.cluster.has_create_database()\n        cls.has_create_role = cls.cluster.has_create_role()\n        cls.is_superuser = cls.has_create_database and cls.has_create_role\n        cls.backend_dsn = os.environ.get('EDGEDB_TEST_BACKEND_DSN')\n        if getattr(cls, 'BACKEND_SUPERUSER', False):\n            if not cls.is_superuser:\n                raise unittest.SkipTest('skipped due to lack of superuser')\n\n    @classmethod\n    async def tearDownSingleDB(cls):\n        await cls.con.execute(\"RESET SCHEMA TO initial;\")\n\n    @classmethod\n    def fetch_metrics(cls) -> str:\n        assert cls.cluster is not None\n        conargs = cls.cluster.get_connect_args()\n        host, port = conargs['host'], conargs['port']\n        ctx = ssl.create_default_context()\n        ctx.load_verify_locations(conargs['tls_ca_file'])\n        return _fetch_metrics(host, port, sslctx=ctx)\n\n    @classmethod\n    def get_connect_args(\n        cls,\n        *,\n        cluster=None,\n        database=None,\n        user=None,\n        password=None,\n        secret_key=None,\n    ):\n        if password is None and secret_key is None:\n            password = \"test\"\n        if cluster is None:\n            cluster = cls.cluster\n        if database is None:\n            database = edgedb_defines.EDGEDB_SUPERUSER_DB\n        if user is None:\n            user = edgedb_defines.EDGEDB_SUPERUSER\n        conargs = cluster.get_connect_args().copy()\n        conargs.update(dict(user=user,\n                            password=password,\n                            database=database,\n                            secret_key=secret_key))\n        return conargs\n\n    @classmethod\n    def make_auth_header(cls, user=None, password=None):\n        # urllib *does* have actual support for basic auth but it is so much\n        # more annoying than just doing it yourself...\n        conargs = cls.get_connect_args(user=user, password=password)\n        username = conargs.get('user')\n        password = conargs.get('password')\n        key = f'{username}:{password}'.encode('ascii')\n        basic_header = f'Basic {base64.b64encode(key).decode(\"ascii\")}'\n\n        return basic_header\n\n    @classmethod\n    def get_parallelism_granularity(cls):\n        if cls.PARALLELISM_GRANULARITY == 'default':\n            if cls.TRANSACTION_ISOLATION:\n                return 'default'\n            else:\n                return 'database'\n        else:\n            return cls.PARALLELISM_GRANULARITY\n\n    @classmethod\n    def uses_database_copies(cls):\n        return (\n            os.environ.get('EDGEDB_TEST_PARALLEL')\n            and cls.get_parallelism_granularity() == 'database'\n        )\n\n    def ensure_no_background_server_errors(self):\n        metrics = self.fetch_metrics()\n        errors = _extract_background_errors(metrics)\n        if errors:\n            raise AssertionError(\n                f'{self._testMethodName!r}:\\n\\n{errors}'\n            )\n\n    @contextlib.asynccontextmanager\n    async def assertRaisesRegexTx(self, exception, regex, msg=None, **kwargs):\n        \"\"\"A version of assertRaisesRegex with automatic transaction recovery\n        \"\"\"\n\n        with super().assertRaisesRegex(exception, regex, msg=msg, **kwargs):\n            try:\n                tx = self.con.transaction()\n                await tx.start()\n                yield\n            finally:\n                await tx.rollback()\n\n    @classmethod\n    @contextlib.contextmanager\n    def http_con(\n        cls,\n        server=None,\n        keep_alive=True,\n        client_cert_file=None,\n        client_key_file=None,\n        **kwargs,\n    ):\n        if server is None:\n            server = cls\n        with super().http_con(\n            server,\n            keep_alive=keep_alive,\n            client_cert_file=client_cert_file,\n            client_key_file=client_key_file,\n            **kwargs,\n        ) as http_con:\n            yield http_con\n\n    @property\n    def http_addr(self) -> str:\n        conn_args = self.get_connect_args()\n        url = f'https://{conn_args[\"host\"]}:{conn_args[\"port\"]}'\n        prefix = self.get_api_prefix()\n        if prefix:\n            url = f'{url}{prefix}'\n        return url\n\n    @property\n    def tls_context(self) -> ssl.SSLContext:\n        conn_args = self.get_connect_args()\n        tls_context = ssl.create_default_context(\n            ssl.Purpose.SERVER_AUTH,\n            cafile=conn_args[\"tls_ca_file\"],\n        )\n        tls_context.check_hostname = False\n        return tls_context\n\n\ndef ignore_warnings(warning_message=None):\n    def w(f):\n        async def wf(self, *args, **kwargs):\n            with self.ignore_warnings(warning_message):\n                return await f(self, *args, **kwargs)\n\n        return wf\n\n    return w\n\n\nclass ConnectedTestCase(ClusterTestCase):\n\n    BASE_TEST_CLASS = True\n    NO_FACTOR = True\n    WARN_FACTOR = False\n\n    con: tconn.Connection\n\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        cls.loop.run_until_complete(cls.setup_and_connect())\n\n    @classmethod\n    def tearDownClass(cls):\n        try:\n            cls.loop.run_until_complete(cls.teardown_and_disconnect())\n        finally:\n            super().tearDownClass()\n\n    @contextlib.contextmanager\n    def ignore_warnings(self, warning_message=None):\n        with self.con.capture_warnings() as warnings:\n            yield\n\n        if warning_message is not None:\n            for warning in warnings:\n                # If it doesn't match the re, send it back to the con.\n                # It might get raised or it might get captured by an\n                # enclosing call to capture_warnings/ignore_warnings.\n                if not re.search(warning_message, str(warning)):\n                    self.con._get_warning_handler()([warning], None)\n\n    @classmethod\n    async def setup_and_connect(cls):\n        cls.con = await cls.connect()\n\n    @classmethod\n    async def teardown_and_disconnect(cls):\n        await cls.con.aclose()\n        # Give event loop another iteration so that connection\n        # transport has a chance to properly close.\n        await asyncio.sleep(0)\n        cls.con = None\n\n    def setUp(self):\n        if self.INTERNAL_TESTMODE:\n            self.loop.run_until_complete(\n                self.con.execute(\n                    'CONFIGURE SESSION SET __internal_testmode := true;'))\n\n        if not self.ENABLE_RECOMPILATION:\n            self.loop.run_until_complete(\n                self.con.execute(\n                    'CONFIGURE SESSION SET auto_rebuild_query_cache := false;'\n                )\n            )\n\n        if self.NO_FACTOR:\n            self.loop.run_until_complete(\n                self.con.execute(\n                    'CONFIGURE SESSION SET simple_scoping := true;'))\n\n        if self.WARN_FACTOR:\n            self.loop.run_until_complete(\n                self.con.execute(\n                    'CONFIGURE SESSION SET warn_old_scoping := true;'))\n\n        if self.TRANSACTION_ISOLATION:\n            self.xact = self.con.transaction()\n            self.loop.run_until_complete(self.xact.start())\n\n        for cmd in self.PER_TEST_SETUP:\n            self.loop.run_until_complete(self.con.execute(cmd))\n\n        super().setUp()\n\n    def tearDown(self):\n        try:\n            self.ensure_no_background_server_errors()\n\n            for cmd in self.PER_TEST_TEARDOWN:\n                self.loop.run_until_complete(self.con.execute(cmd))\n        finally:\n            try:\n                if self.TRANSACTION_ISOLATION:\n                    self.loop.run_until_complete(self.xact.rollback())\n                    del self.xact\n\n                if self.con.is_in_transaction():\n                    self.loop.run_until_complete(\n                        self.con.query('ROLLBACK'))\n                    raise AssertionError(\n                        'test connection is still in transaction '\n                        '*after* the test')\n\n                if not self.TRANSACTION_ISOLATION:\n                    self.loop.run_until_complete(\n                        self.con.execute('RESET ALIAS *;'))\n\n            finally:\n                super().tearDown()\n\n    @classmethod\n    async def connect(\n        cls,\n        *,\n        cluster=None,\n        database=None,\n        user=None,\n        password=None,\n        secret_key=None,\n    ) -> tconn.Connection:\n        conargs = cls.get_connect_args(\n            cluster=cluster,\n            database=database,\n            user=user,\n            password=password,\n            secret_key=secret_key,\n        )\n        return await tconn.async_connect_test_client(**conargs)\n\n    def repl(self):\n        \"\"\"Open interactive EdgeQL REPL right in the test.\n\n        This is obviously only for debugging purposes.  Just add\n        `self.repl()` at any point in your test.\n        \"\"\"\n\n        conargs = self.get_connect_args()\n\n        cmd = [\n            'python', '-m', 'edb.cli',\n            '--database', self.con.dbname,\n            '--user', conargs['user'],\n            '--tls-ca-file', conargs['tls_ca_file'],\n        ]\n\n        env = os.environ.copy()\n        env['EDGEDB_HOST'] = conargs['host']\n        env['EDGEDB_PORT'] = str(conargs['port'])\n        if password := conargs.get('password'):\n            env['EDGEDB_PASSWORD'] = password\n        if secret_key := conargs.get('secret_key'):\n            env['EDGEDB_SECRET_KEY'] = secret_key\n\n        proc = subprocess.Popen(\n            cmd, stdin=sys.stdin, stdout=sys.stdout, env=env)\n        while proc.returncode is None:\n            try:\n                proc.wait()\n            except KeyboardInterrupt:\n                pass\n\n    def _run_and_rollback(self):\n        return RollbackChanges(self.con)\n\n    async def _run_and_rollback_retrying(self):\n        @contextlib.asynccontextmanager\n        async def cm(tx):\n            try:\n                async with tx:\n                    await tx._ensure_transaction()\n                    yield tx\n                    raise RollbackException\n            except RollbackException:\n                pass\n\n        async for tx in self.con.retrying_transaction():\n            yield cm(tx)\n\n    def assert_data_shape(self, data, shape,\n                          message=None, rel_tol=None, abs_tol=None):\n        assert_data_shape.assert_data_shape(\n            data, shape, self.fail,\n            message=message, rel_tol=rel_tol, abs_tol=abs_tol,\n        )\n\n    async def assert_query_result(\n        self,\n        query,\n        exp_result_json,\n        exp_result_binary=...,\n        *,\n        always_typenames=False,\n        always_typeids=False,\n        msg=None,\n        sort=None,\n        implicit_limit=0,\n        variables=None,\n        json_only=False,\n        binary_only=False,\n        rel_tol=None,\n        abs_tol=None,\n        language: Literal[\"sql\", \"edgeql\"] = \"edgeql\",\n    ):\n        fetch_args = variables if isinstance(variables, tuple) else ()\n        fetch_kw = variables if isinstance(variables, dict) else {}\n\n        if not binary_only and language != \"sql\":\n            try:\n                tx = self.con.transaction()\n                await tx.start()\n                try:\n                    res = await self.con._fetchall_json(\n                        query,\n                        *fetch_args,\n                        __limit__=implicit_limit,\n                        **fetch_kw)\n                finally:\n                    await tx.rollback()\n\n                res = json.loads(res)\n                if sort is not None:\n                    assert_data_shape.sort_results(res, sort)\n                assert_data_shape.assert_data_shape(\n                    res, exp_result_json, self.fail,\n                    message=msg, rel_tol=rel_tol, abs_tol=abs_tol,\n                )\n            except Exception:\n                self.add_fail_notes(serialization='json')\n                if msg:\n                    self.add_fail_notes(msg=msg)\n                raise\n\n        if json_only:\n            return\n\n        if exp_result_binary is ...:\n            # The expected result is the same\n            exp_result_binary = exp_result_json\n\n        typenames = random.choice([True, False]) or always_typenames\n        typeids = random.choice([True, False]) or always_typeids\n\n        try:\n            res = await self.con._fetchall(\n                query,\n                *fetch_args,\n                __typenames__=typenames,\n                __typeids__=typeids,\n                __limit__=implicit_limit,\n                __language__=(\n                    tconn.InputLanguage.SQL if language == \"sql\"\n                    else tconn.InputLanguage.EDGEQL\n                ),\n                **fetch_kw\n            )\n            res = serutils.serialize(res)\n            if sort is not None:\n                assert_data_shape.sort_results(res, sort)\n            assert_data_shape.assert_data_shape(\n                res,\n                exp_result_binary,\n                self.fail,\n                message=msg,\n                rel_tol=rel_tol,\n                abs_tol=abs_tol,\n            )\n        except Exception:\n            self.add_fail_notes(\n                serialization='binary',\n                __typenames__=typenames,\n                __typeids__=typeids)\n            if msg:\n                self.add_fail_notes(msg=msg)\n            raise\n\n    async def assert_sql_query_result(\n        self,\n        query,\n        exp_result,\n        *,\n        implicit_limit=0,\n        msg=None,\n        sort=None,\n        variables=None,\n        rel_tol=None,\n        abs_tol=None,\n        apply_access_policies=True,\n    ):\n        if not apply_access_policies:\n            ctx = self.without_access_policies()\n        else:\n            ctx = contextlib.nullcontext()\n        async with ctx:\n            await self.assert_query_result(\n                query,\n                exp_result,\n                implicit_limit=implicit_limit,\n                msg=msg,\n                sort=sort,\n                variables=variables,\n                rel_tol=rel_tol,\n                abs_tol=abs_tol,\n                language=\"sql\",\n            )\n\n    async def assert_index_use(self, query, *args, index_type):\n        def look(obj):\n            if (\n                isinstance(obj, dict)\n                and \"IndexScan\" in obj.get('plan_type', '')\n            ):\n                return any(\n                    prop['title'] == 'index_name'\n                    and index_type in prop['value']\n                    for prop in obj.get('properties', [])\n                )\n\n            if isinstance(obj, dict):\n                return any([look(v) for v in obj.values()])\n            elif isinstance(obj, list):\n                return any(look(v) for v in obj)\n            else:\n                return False\n\n        plan = await self.con.query_json(f'analyze {query}', *args)\n        if not look(json.loads(plan)):\n            raise AssertionError(f\"query did not use the {index_type!r} index\")\n\n    @classmethod\n    def get_backend_sql_dsn(cls, dbname=None):\n        settings = cls.con.get_settings()\n        pgdsn = settings.get('pgdsn')\n        if pgdsn is None:\n            raise unittest.SkipTest('raw SQL test skipped: not in devmode')\n        params = ConnectionParams(dsn=pgdsn.decode('utf8'))\n        if dbname:\n            params.update(database=dbname)\n        params.clear_server_settings()\n        return params.to_dsn()\n\n    @classmethod\n    async def get_backend_sql_connection(cls, dbname=None):\n        \"\"\"Get a raw connection to the underlying SQL server, if possible\n\n        This is useful when we want to do things like querying the pg_catalog\n        of the underlying database.\n        \"\"\"\n        try:\n            import asyncpg\n        except ImportError:\n            raise unittest.SkipTest(\n                'SQL test skipped: asyncpg not installed')\n\n        pgdsn = cls.get_backend_sql_dsn(dbname=dbname)\n        return await asyncpg.connect(pgdsn)\n\n    @classmethod\n    @contextlib.asynccontextmanager\n    async def with_backend_sql_connection(cls, dbname=None):\n        con = await cls.get_backend_sql_connection(dbname=dbname)\n        try:\n            yield con\n        finally:\n            await con.close()\n\n    @contextlib.asynccontextmanager\n    async def without_access_policies(self):\n        await self.con.execute(\n            'CONFIGURE SESSION SET apply_access_policies := false'\n        )\n        raised_an_execption = False\n        try:\n            yield\n        except BaseException:\n            raised_an_execption = True\n            raise\n        finally:\n            if not (raised_an_execption and self.con.is_in_transaction()):\n                await self.con.execute(\n                    'CONFIGURE SESSION RESET apply_access_policies'\n                )\n\n    @classmethod\n    def get_sql_proto_dsn(cls, dbname=None):\n        dbname = dbname or cls.con.dbname\n        conargs = cls.get_connect_args()\n        return (\n            f\"postgres://{conargs['user']}:{conargs['password']}@\"\n            f\"{conargs['host']}:{conargs['port']}/{cls.con.dbname}?\"\n            f\"sslrootcert={conargs['tls_ca_file']}\"\n        )\n\n\nclass DatabaseTestCase(ConnectedTestCase):\n\n    SETUP: Optional[str | pathlib.Path | list[str] | list[pathlib.Path]] = None\n    TEARDOWN: Optional[str] = None\n    SCHEMA: Optional[str | pathlib.Path] = None\n    DEFAULT_MODULE: str = 'default'\n    EXTENSIONS: list[str] = []\n\n    BASE_TEST_CLASS = True\n\n    con: Any  # XXX: the real type?\n\n    @classmethod\n    async def setup_and_connect(cls):\n        dbname = cls.get_database_name()\n\n        cls.con = None\n\n        class_set_up = os.environ.get('EDGEDB_TEST_CASES_SET_UP', 'run')\n\n        # Only open an extra admin connection if necessary.\n        if class_set_up == 'run':\n            script = f'CREATE DATABASE {dbname};'\n            admin_conn = await cls.connect(\n                database=edgedb_defines.EDGEDB_SUPERUSER_DB\n            )\n            await admin_conn.execute(script)\n            await admin_conn.aclose()\n\n        elif class_set_up == 'inplace':\n            dbname = edgedb_defines.EDGEDB_SUPERUSER_DB\n\n        elif cls.uses_database_copies():\n            admin_conn = await cls.connect(\n                database=edgedb_defines.EDGEDB_SUPERUSER_DB\n            )\n\n            base_db_name, _, _ = dbname.rpartition('_')\n\n            if cls.get_setup_script():\n                await admin_conn.execute('''\n                    configure session set __internal_testmode := true;\n                ''')\n\n                create_command = (\n                    f'CREATE TEMPLATE BRANCH {qlquote.quote_ident(dbname)}'\n                    f' FROM {qlquote.quote_ident(base_db_name)};'\n                )\n            else:\n                create_command = (\n                    f'CREATE EMPTY BRANCH {qlquote.quote_ident(dbname)}')\n\n            # The retry here allows the test to survive a concurrent testing\n            # Gel server (e.g. async with tb.start_edgedb_server()) whose\n            # introspection holds a lock on the base_db here\n            async for tr in cls.try_until_succeeds(\n                ignore=edgedb.ExecutionError,\n                timeout=30,\n            ):\n                async with tr:\n                    await admin_conn.execute(create_command)\n\n            await admin_conn.aclose()\n\n        cls.con = await cls.connect(database=dbname)\n\n        if class_set_up != 'skip':\n            script = cls.get_setup_script()\n            if script:\n                with cls.con.ignore_warnings():\n                    await cls.con.execute(script)\n\n    @staticmethod\n    def get_set_up():\n        return os.environ.get('EDGEDB_TEST_CASES_SET_UP', 'run')\n\n    @classmethod\n    async def teardown_and_disconnect(cls):\n        script = ''\n\n        class_set_up = cls.get_set_up()\n\n        if cls.TEARDOWN and class_set_up != 'skip':\n            script = cls.TEARDOWN.strip()\n\n        try:\n            if script:\n                await cls.con.execute(script)\n            if class_set_up == 'inplace':\n                await cls.tearDownSingleDB()\n        finally:\n            await cls.con.aclose()\n\n            if class_set_up == 'inplace':\n                pass\n\n            elif class_set_up == 'run' or cls.uses_database_copies():\n                dbname = qlquote.quote_ident(cls.get_database_name())\n                admin_conn = await cls.connect(\n                    database=edgedb_defines.EDGEDB_SUPERUSER_DB\n                )\n                try:\n                    await drop_db(admin_conn, dbname)\n                finally:\n                    await admin_conn.aclose()\n\n    @classmethod\n    def get_connect_args(\n        cls,\n        *,\n        database=None,\n        **kwargs,\n    ):\n        return super().get_connect_args(\n            database=database or cls.get_database_name(),\n            **kwargs,\n        )\n\n    @classmethod\n    def get_database_name(cls):\n        if not getattr(cls, 'has_create_database', True):\n            return edgedb_defines.EDGEDB_SUPERUSER_DB\n\n        if cls.__name__.startswith('TestEdgeQL'):\n            dbname = cls.__name__[len('TestEdgeQL'):]\n        elif cls.__name__.startswith('Test'):\n            dbname = cls.__name__[len('Test'):]\n        else:\n            dbname = cls.__name__\n\n        if cls.uses_database_copies():\n            return f'{dbname.lower()}_{os.getpid()}'\n        else:\n            return dbname.lower()\n\n    @classmethod\n    def get_api_prefix(cls):\n        return f'/db/{cls.get_database_name()}'\n\n    @classmethod\n    def get_setup_script(cls):\n        script = ''\n        has_nontrivial_script = False\n\n        # allow the setup script to also run in test mode and no recompilation\n        if cls.INTERNAL_TESTMODE:\n            script += '\\nCONFIGURE SESSION SET __internal_testmode := true;'\n        if not cls.ENABLE_RECOMPILATION:\n            script += (\n                '\\nCONFIGURE SESSION SET auto_rebuild_query_cache := false;'\n            )\n\n        if getattr(cls, 'BACKEND_SUPERUSER', False):\n            is_superuser = getattr(cls, 'is_superuser', True)\n            if not is_superuser:\n                raise unittest.SkipTest('skipped due to lack of superuser')\n\n        schema = []\n        # Incude the extensions before adding schemas.\n        for ext in cls.EXTENSIONS:\n            schema.append(f'using extension {ext};')\n\n        # Look at all SCHEMA entries and potentially create multiple\n        # modules, but always create the test module, if not `default`.\n        if cls.DEFAULT_MODULE != 'default':\n            schema.append(f'\\nmodule {cls.DEFAULT_MODULE} {{}}')\n        for name in dir(cls):\n            m = re.match(r'^SCHEMA(?:_(\\w+))?', name)\n            if m:\n                module_name = (\n                    (m.group(1) or cls.DEFAULT_MODULE)\n                    .lower().replace('_', '::')\n                )\n\n                schema_fn = getattr(cls, name)\n                if schema_fn is not None:\n                    with open(schema_fn, 'r') as sf:\n                        module = sf.read()\n\n                    schema.append(f'\\nmodule {module_name} {{ {module} }}')\n\n        full_schema_fn = getattr(cls, 'FULL_SCHEMA', None)\n        if full_schema_fn:\n            with open(full_schema_fn, 'r') as sf:\n                schema.append(sf.read())\n\n        if schema:\n            has_nontrivial_script = True\n\n            script += f'\\nSTART MIGRATION'\n            script += f' TO {{ {\"\".join(schema)} }};'\n            script += f'\\nPOPULATE MIGRATION;'\n            script += f'\\nCOMMIT MIGRATION;'\n\n        if cls.SETUP:\n            if not isinstance(cls.SETUP, (list, tuple)):\n                scripts = [cls.SETUP]\n            else:\n                scripts = cls.SETUP\n\n            for scr in scripts:\n                has_nontrivial_script = True\n\n                is_path = (\n                    isinstance(scr, pathlib.Path)\n                    or '\\n' not in scr and os.path.exists(scr)\n                )\n\n                if is_path:\n                    with open(scr, 'rt') as f:\n                        setup_text = f.read()\n                else:\n                    assert isinstance(scr, str)\n                    setup_text = scr\n\n                script += '\\n' + setup_text\n\n            # If the SETUP script did a SET MODULE, make sure it is cleared\n            # (since in some modes we keep using the same connection)\n            script += '\\nRESET MODULE;'\n\n        # allow the setup script to also run in test mode\n        if cls.INTERNAL_TESTMODE:\n            script += '\\nCONFIGURE SESSION SET __internal_testmode := false;'\n        if not cls.ENABLE_RECOMPILATION:\n            script += '\\nCONFIGURE SESSION RESET auto_rebuild_query_cache;'\n\n        return script.strip(' \\n') if has_nontrivial_script else ''\n\n    async def migrate(self, migration, *, module: Optional[str] = 'default'):\n        if module:\n            migration = f\"\"\"\n                module {module} {{\n                    {migration}\n                }}\n            \"\"\"\n\n        with self.ignore_warnings('Non-simple_scoping will be removed'):\n            await self.con.execute(f\"\"\"\n                START MIGRATION TO {{\n                    {migration}\n                }};\n                POPULATE MIGRATION;\n                COMMIT MIGRATION;\n            \"\"\")\n\n\nclass Error:\n    def __init__(self, cls, message, shape):\n        self._message = message\n        self._class = cls\n        self._shape = shape\n\n    @property\n    def message(self):\n        return self._message\n\n    @property\n    def cls(self):\n        return self._class\n\n    @property\n    def shape(self):\n        return self._shape\n\n\nclass BaseQueryTestCase(DatabaseTestCase):\n\n    BASE_TEST_CLASS = True\n\n\nclass DDLTestCase(BaseQueryTestCase):\n    # DDL test cases generally need to be serialized\n    # to avoid deadlocks in parallel execution.\n    PARALLELISM_GRANULARITY = 'database'\n    BASE_TEST_CLASS = True\n\n\nclass QueryTestCase(BaseQueryTestCase):\n\n    BASE_TEST_CLASS = True\n\n\nclass SQLQueryTestCase(BaseQueryTestCase):\n\n    BASE_TEST_CLASS = True\n\n    scon: asyncpg.Connection\n\n    @classmethod\n    def setUpClass(cls):\n        try:\n            import asyncpg  # noqa: F401\n        except ImportError:\n            raise unittest.SkipTest('SQL tests skipped: asyncpg not installed')\n\n        super().setUpClass()\n        cls.scon = cls.loop.run_until_complete(\n            cls.create_sql_connection()\n        )\n\n    @classmethod\n    def create_sql_connection(\n        cls,\n        *,\n        user: str = None,\n        password: str = None,\n    ) -> asyncio.Future[asyncpg.Connection]:\n        import asyncpg\n        conargs = cls.get_connect_args()\n\n        tls_context = ssl.create_default_context(\n            ssl.Purpose.SERVER_AUTH,\n            cafile=conargs[\"tls_ca_file\"],\n        )\n        tls_context.check_hostname = False\n\n        return asyncpg.connect(\n            host=conargs['host'],\n            port=conargs['port'],\n            user=conargs['user'] if user is None else user,\n            password=conargs['password'] if password is None else password,\n            database=cls.con.dbname,\n            ssl=tls_context,\n        )\n\n    @classmethod\n    def tearDownClass(cls):\n        try:\n            cls.loop.run_until_complete(cls.scon.close())\n            # Give event loop another iteration so that connection\n            # transport has a chance to properly close.\n            cls.loop.run_until_complete(asyncio.sleep(0))\n            cls.scon = None\n        finally:\n            super().tearDownClass()\n\n    def setUp(self):\n        if self.TRANSACTION_ISOLATION:\n            self.stran = self.scon.transaction()\n            self.loop.run_until_complete(self.stran.start())\n        super().setUp()\n\n    def tearDown(self):\n        try:\n            if self.TRANSACTION_ISOLATION:\n                self.loop.run_until_complete(self.stran.rollback())\n            self.loop.run_until_complete(self.scon.execute('RESET ALL'))\n        finally:\n            super().tearDown()\n\n    async def squery_values(self, query, *args):\n        res = await self.scon.fetch(query, *args)\n        return [list(r.values()) for r in res]\n\n    def assert_shape(self, res: Any, rows: int, columns: int | list[str]):\n        \"\"\"\n        Fail if query result does not confront the specified shape, defined in\n        terms of:\n        - number of rows,\n        - number of columns (not checked if there are not rows)\n        - column names.\n        \"\"\"\n\n        self.assertEqual(len(res), rows)\n\n        if isinstance(columns, int):\n            if rows > 0:\n                self.assertEqual(len(res[0]), columns)\n        elif isinstance(columns, list):\n            self.assertListEqual(columns, list(res[0].keys()))\n\n\nclass CLITestCaseMixin:\n\n    def run_cli(self, *args, input: Optional[str] = None) -> None:\n        conn_args = self.get_connect_args()\n        self.run_cli_on_connection(conn_args, *args, input=input)\n\n    @classmethod\n    def run_cli_on_connection(\n        cls, conn_args: dict[str, Any], *args, input: Optional[str] = None\n    ) -> None:\n        cmd_args = [\n            '--host', conn_args['host'],\n            '--port', str(conn_args['port']),\n            '--tls-ca-file', conn_args['tls_ca_file']\n        ]\n        if conn_args.get('user'):\n            cmd_args += ['--user', conn_args['user']]\n        if conn_args.get('password'):\n            cmd_args += ['--password-from-stdin']\n            if input is not None:\n                input = f\"{conn_args['password']}\\n{input}\"\n            else:\n                input = f\"{conn_args['password']}\\n\"\n        cmd_args += args\n        cmd = ['gel'] + cmd_args\n        try:\n            subprocess.run(\n                cmd,\n                input=input.encode() if input else None,\n                check=True,\n                capture_output=True,\n            )\n        except subprocess.CalledProcessError as e:\n            output = '\\n'.join(getattr(out, 'decode', out.__str__)()\n                               for out in [e.output, e.stderr] if out)\n            raise AssertionError(\n                f'command {cmd} returned non-zero exit status {e.returncode}'\n                f'\\n{output}'\n            ) from e\n\n\nclass DumpCompatTestCaseMeta(TestCaseMeta):\n\n    def __new__(\n        mcls,\n        name,\n        bases,\n        ns,\n        *,\n        dump_subdir=None,\n        check_method=None,\n    ):\n        if not name.startswith('Test'):\n            return super().__new__(mcls, name, bases, ns)\n\n        if dump_subdir is None:\n            raise TypeError(\n                f'{name}: missing required \"dump_subdir\" class argument')\n\n        if check_method is None:\n            raise TypeError(\n                f'{name}: missing required \"check_method\" class argument')\n\n        mod = sys.modules[ns['__module__']]\n        dumps_dir = pathlib.Path(mod.__file__).parent / 'dumps' / dump_subdir\n\n        async def check_dump_restore_compat_single_db(self, *, dumpfn):\n            dbname = edgedb_defines.EDGEDB_SUPERUSER_DB\n            self.run_cli('-d', dbname, 'restore', str(dumpfn))\n            try:\n                await check_method(self)\n            finally:\n                await self.tearDownSingleDB()\n\n        async def check_dump_restore_compat(self, *, dumpfn: pathlib.Path):\n            if not self.has_create_database:\n                return await check_dump_restore_compat_single_db(\n                    self, dumpfn=dumpfn\n                )\n\n            dbname = f\"{type(self).__name__}_{dumpfn.stem}\"\n            qdbname = qlquote.quote_ident(dbname)\n            await self.con.execute(f'CREATE DATABASE {qdbname}')\n            try:\n                self.run_cli('-d', dbname, 'restore', str(dumpfn))\n                con2 = await self.connect(database=dbname)\n            except Exception:\n                await drop_db(self.con, qdbname)\n                raise\n\n            oldcon = self.__class__.con\n            self.__class__.con = con2\n            try:\n                await check_method(self)\n            finally:\n                self.__class__.con = oldcon\n                await con2.aclose()\n\n                await drop_db(self.con, qdbname)\n\n        for entry in dumps_dir.iterdir():\n            if not entry.is_file() or not entry.name.endswith(\".dump\"):\n                continue\n\n            mcls.add_method(\n                f'test_{dump_subdir}_restore_compatibility_{entry.stem}',\n                ns,\n                functools.partial(check_dump_restore_compat, dumpfn=entry),\n            )\n\n        return super().__new__(mcls, name, bases, ns)\n\n\nclass DumpCompatTestCase(\n    ConnectedTestCase,\n    CLITestCaseMixin,\n    metaclass=DumpCompatTestCaseMeta,\n):\n    BASE_TEST_CLASS = True\n    TRANSACTION_ISOLATION = False\n\n\nclass StableDumpTestCase(QueryTestCase, CLITestCaseMixin):\n\n    BASE_TEST_CLASS = True\n    STABLE_DUMP = True\n    TRANSACTION_ISOLATION = False\n    PARALLELISM_GRANULARITY = 'suite'\n\n    async def check_dump_restore_single_db(self, check_method):\n        with tempfile.TemporaryDirectory() as f:\n            fname = os.path.join(f, 'dump')\n            dbname = edgedb_defines.EDGEDB_SUPERUSER_DB\n            await asyncio.to_thread(self.run_cli, '-d', dbname, 'dump', fname)\n            await self.tearDownSingleDB()\n            await asyncio.to_thread(\n                self.run_cli, '-d', dbname, 'restore', fname\n            )\n\n        # Cycle the connection to avoid state mismatches\n        await self.con.aclose()\n        self.con = await self.connect(database=dbname)\n\n        await check_method(self)\n\n    async def check_dump_restore(\n        self, check_method, include_secrets: bool=False\n    ):\n        if not self.has_create_database:\n            return await self.check_dump_restore_single_db(check_method)\n\n        src_dbname = self.get_database_name()\n        tgt_dbname = f'{src_dbname}_restored'\n        q_tgt_dbname = qlquote.quote_ident(tgt_dbname)\n        with tempfile.TemporaryDirectory() as f:\n            fname = os.path.join(f, 'dump')\n            extra = ['--include-secrets'] if include_secrets else []\n            await asyncio.to_thread(\n                self.run_cli, '-d', src_dbname, 'dump', fname, *extra\n            )\n\n            await self.con.execute(f'CREATE DATABASE {q_tgt_dbname}')\n            try:\n                await asyncio.to_thread(\n                    self.run_cli, '-d', tgt_dbname, 'restore', fname\n                )\n                con2 = await self.connect(database=tgt_dbname)\n            except Exception:\n                await drop_db(self.con, q_tgt_dbname)\n                raise\n\n        oldcon = self.con\n        self.__class__.con = con2\n        try:\n            await check_method(self)\n        finally:\n            self.__class__.con = oldcon\n            await con2.aclose()\n            await drop_db(self.con, q_tgt_dbname)\n\n    async def check_branching(self, include_data=False, *, check_method):\n        if not self.has_create_database:\n            self.skipTest(\"create branch is not supported by the backend\")\n\n        orig_branch = self.get_database_name()\n        new_branch = f'new_{orig_branch}'\n        # record the original schema\n        orig_schema = await self.con.query_single('describe schema as sdl')\n\n        # connect to a default branch so we can create a new branch\n        branch_type = 'data' if include_data else 'schema'\n        await self.con.execute(\n            f'create {branch_type} branch {new_branch} '\n            f'from {orig_branch}'\n        )\n\n        try:\n            con2 = await self.connect(database=new_branch)\n        except Exception:\n            await drop_db(self.con, new_branch)\n            raise\n\n        oldcon = self.con\n        self.__class__.con = con2\n        try:\n            # We cannot compare the SDL text of the new branch schema to the\n            # original because the order in which it renders all the\n            # components is not guaranteed. Instead we will use migrations to\n            # compare the new branch schema to the original. We expect there\n            # to be no difference and therefore a new migration to the\n            # original schema should have the \"complete\" status right away.\n            with self.ignore_warnings():\n                await self.con.execute(\n                    f'start migration to {{ {orig_schema} }}'\n                )\n            mig_status = json.loads(\n                await self.con.query_single_json(\n                    'describe current migration as json'\n                )\n            )\n            self.assertTrue(mig_status.get('complete'))\n            await self.con.execute('abort migration')\n\n            # run the check_method on the copied branch\n            if include_data:\n                await check_method(self)\n            else:\n                await check_method(self, include_data=include_data)\n        finally:\n            self.__class__.con = oldcon\n            await con2.aclose()\n            await drop_db(self.con, new_branch)\n\n\nclass StablePGDumpTestCase(BaseQueryTestCase):\n\n    BASE_TEST_CLASS = True\n    TRANSACTION_ISOLATION = False\n\n    def run_pg_dump(self, *args, input: Optional[str] = None) -> None:\n        conargs = self.get_connect_args()\n        self.run_pg_dump_on_connection(conargs, *args, input=input)\n\n    @classmethod\n    def run_pg_dump_on_connection(\n        cls, dsn: str, *args, input: Optional[str] = None\n    ) -> None:\n        cmd = [cls._pg_bin_dir / 'pg_dump', '--dbname', dsn]\n        cmd += args\n        try:\n            subprocess.run(\n                cmd,\n                input=input.encode() if input else None,\n                check=True,\n                capture_output=True,\n            )\n        except subprocess.CalledProcessError as e:\n            output = '\\n'.join(getattr(out, 'decode', out.__str__)()\n                               for out in [e.output, e.stderr] if out)\n            raise AssertionError(\n                f'command {cmd} returned non-zero exit status {e.returncode}'\n                f'\\n{output}'\n            ) from e\n\n    @classmethod\n    def setUpClass(cls):\n        try:\n            import asyncpg\n        except ImportError:\n            raise unittest.SkipTest('SQL tests skipped: asyncpg not installed')\n\n        if cls.get_set_up() == 'inplace':\n            raise unittest.SkipTest('SQL dump tests skipped in single db mode')\n\n        super().setUpClass()\n        frontend_dsn = cls.get_sql_proto_dsn()\n        src_dbname = cls.con.dbname\n        tgt_dbname = f'restored_{src_dbname}'\n        try:\n            newdsn = cls.get_backend_sql_dsn(dbname=tgt_dbname)\n        except Exception:\n            super().tearDownClass()\n            raise\n\n        cls._pg_bin_dir = cls.loop.run_until_complete(\n            pgcluster.get_pg_bin_dir())\n\n        cls.backend = cls.loop.run_until_complete(\n            cls.get_backend_sql_connection())\n\n        # Run pg_dump to create the dump data for an existing Gel database.\n        with tempfile.NamedTemporaryFile() as f:\n            cls.run_pg_dump_on_connection(frontend_dsn, '-f', f.name)\n\n            # Skip the restore part of the test if the database\n            # backend is older than our pg_dump, since it won't work.\n            pg_ver_str = cls.loop.run_until_complete(\n                cls.backend.fetch('select version()')\n            )[0][0]\n            pg_ver = buildmeta.parse_pg_version(pg_ver_str)\n            bundled_ver = buildmeta.get_pg_version()\n            if pg_ver.major < bundled_ver.major:\n                raise unittest.SkipTest('pg_dump newer than backend')\n\n            # Create a new Postgres database to be used for dump tests.\n            db_exists = cls.loop.run_until_complete(\n                cls.backend.fetch(f'''\n                    SELECT oid\n                    FROM pg_database\n                    WHERE datname = {tgt_dbname!r}\n                ''')\n            )\n            if list(db_exists):\n                cls.loop.run_until_complete(\n                    cls.backend.execute(f'drop database {tgt_dbname}')\n                )\n            cls.loop.run_until_complete(\n                cls.backend.execute(f'create database {tgt_dbname}')\n            )\n\n            # Populate the new database using the dump\n            cmd = [\n                cls._pg_bin_dir / 'psql',\n                '-a',\n                '--dbname', newdsn,\n                '-f', f.name,\n                '-v', 'ON_ERROR_STOP=on',\n            ]\n            try:\n                subprocess.run(\n                    cmd,\n                    input=None,\n                    check=True,\n                    capture_output=True,\n                )\n            except subprocess.CalledProcessError as e:\n                output = '\\n'.join(getattr(out, 'decode', out.__str__)()\n                                   for out in [e.output, e.stderr] if out)\n                raise AssertionError(\n                    f'command {cmd} returned non-zero exit status '\n                    f'{e.returncode}\\n{output}'\n                ) from e\n\n        # Connect to the newly created database.\n        cls.scon = cls.loop.run_until_complete(\n            asyncpg.connect(newdsn))\n\n    @classmethod\n    def tearDownClass(cls):\n        try:\n            cls.loop.run_until_complete(cls.scon.close())\n            # Give event loop another iteration so that connection\n            # transport has a chance to properly close.\n            cls.loop.run_until_complete(asyncio.sleep(0))\n            cls.scon = None\n\n            tgt_dbname = f'restored_{cls.con.dbname}'\n            cls.loop.run_until_complete(\n                cls.backend.execute(f'drop database {tgt_dbname}')\n            )\n            cls.loop.run_until_complete(cls.backend.close())\n            cls.loop.run_until_complete(asyncio.sleep(0))\n        finally:\n            super().tearDownClass()\n\n    def assert_shape(\n        self,\n        sqlres: Iterable[Any],\n        eqlres: Iterable[asyncpg.Record],\n    ) -> None:\n        \"\"\"\n        Compare the shape of results produced by a SQL query and an EdgeQL\n        query.\n        \"\"\"\n\n        assert_data_shape.assert_data_shape(\n            list(sqlres),\n            [dataclasses.asdict(r) for r in eqlres],\n            self.fail,\n            from_sql=True,\n        )\n\n    def multi_prop_subquery(self, source: str, prop: str) -> str:\n        \"Propduce a subquery fetching a multi prop as an array.\"\n\n        return (\n            f'(SELECT array_agg(target) FROM \"{source}.{prop}\"'\n            f' WHERE source = \"{source}\".id) AS {prop}'\n        )\n\n    def single_link_subquery(\n        self,\n        source: str,\n        link: str,\n        target: str,\n        link_props: Optional[Iterable[str]] = None\n    ) -> str:\n        \"\"\"Propduce a subquery fetching a single link as a record.\n\n        If no link properties are specified then the array of records will be\n        made up of target types.\n\n        If the link properties are specified then the array of records will be\n        made up of link records.\n        \"\"\"\n\n        if link_props:\n            return (\n                f'(SELECT x FROM \"{target}\"'\n                f' JOIN \"{source}.{link}\" x ON x.target = \"{target}\".id'\n                f' WHERE x.source = \"{source}\".id) AS _{link}'\n            )\n\n        else:\n            return (\n                f'(SELECT \"{target}\" FROM \"{target}\"'\n                f' WHERE \"{target}\".id = \"{source}\".{link}_id) AS {link}'\n            )\n\n    def multi_link_subquery(\n        self,\n        source: str,\n        link: str,\n        target: str,\n        link_props: Optional[Iterable[str]] = None\n    ) -> str:\n        \"\"\"Propduce a subquery fetching a multi link as an array or records.\n\n        If no link properties are specified then the array of records will be\n        made up of target types.\n\n        If the link properties are specified then the array of records will be\n        made up of link records.\n        \"\"\"\n\n        if link_props:\n            return (\n                f'(SELECT array_agg(x) FROM \"{target}\"'\n                f' JOIN \"{source}.{link}\" x ON x.target = \"{target}\".id'\n                f' WHERE x.source = \"{source}\".id) AS _{link}'\n            )\n\n        else:\n            return (\n                f'(SELECT array_agg(\"{target}\") FROM \"{target}\"'\n                f' JOIN \"{source}.{link}\" x ON x.target = \"{target}\".id'\n                f' WHERE x.source = \"{source}\".id) AS {link}'\n            )\n\n\ndef get_test_cases_setup(\n    cases: Iterable[unittest.TestCase]\n) -> list[tuple[unittest.TestCase, DatabaseName, SetupScript]]:\n    result: list[tuple[unittest.TestCase, DatabaseName, SetupScript]] = []\n\n    for case in cases:\n        if not hasattr(case, 'get_setup_script'):\n            continue\n\n        try:\n            setup_script = case.get_setup_script()\n        except unittest.SkipTest:\n            continue\n\n        dbname = case.get_database_name()\n        result.append((case, dbname, setup_script))\n\n    return result\n\n\ndef test_cases_use_server(cases: Iterable[unittest.TestCase]) -> bool:\n    for case in cases:\n        if not hasattr(case, 'uses_server'):\n            continue\n\n        if case.uses_server():\n            return True\n\n\nasync def setup_test_cases(\n    cases,\n    conn,\n    num_jobs,\n    try_cached_db=False,\n    skip_empty_databases=False,\n    verbose=False,\n):\n    setup = get_test_cases_setup(cases)\n\n    stats = []\n    if num_jobs == 1:\n        # Special case for --jobs=1\n        for _case, dbname, setup_script in setup:\n            if skip_empty_databases and not setup_script:\n                continue\n            await _setup_database(\n                dbname, setup_script, conn, stats, try_cached_db)\n            if verbose:\n                print(f' -> {dbname}: OK', flush=True)\n    else:\n        async with asyncio.TaskGroup() as g:\n            # Use a semaphore to limit the concurrency of bootstrap\n            # tasks to the number of jobs (bootstrap is heavy, having\n            # more tasks than `--jobs` won't necessarily make\n            # things faster.)\n            sem = asyncio.BoundedSemaphore(num_jobs)\n\n            async def controller(coro, dbname, *args):\n                async with sem:\n                    await coro(dbname, *args)\n                    if verbose:\n                        print(f' -> {dbname}: OK', flush=True)\n\n            for _case, dbname, setup_script in setup:\n                if skip_empty_databases and not setup_script:\n                    continue\n\n                g.create_task(controller(\n                    _setup_database, dbname, setup_script, conn, stats,\n                    try_cached_db))\n    return stats\n\n\nasync def _setup_database(\n        dbname, setup_script, conn_args, stats, try_cached_db):\n    start_time = time.monotonic()\n    default_args = {\n        'user': edgedb_defines.EDGEDB_SUPERUSER,\n        'password': 'test',\n    }\n\n    default_args.update(conn_args)\n\n    try:\n        admin_conn = await tconn.async_connect_test_client(\n            database=edgedb_defines.EDGEDB_SUPERUSER_DB,\n            **default_args)\n    except Exception as ex:\n        raise RuntimeError(\n            f'exception during creation of {dbname!r} test DB; '\n            f'could not connect to the {edgedb_defines.EDGEDB_SUPERUSER_DB} '\n            f'db; {type(ex).__name__}({ex})'\n        ) from ex\n\n    try:\n        await admin_conn.execute(\n            f'CREATE DATABASE {qlquote.quote_ident(dbname)};'\n        )\n    except edgedb.DuplicateDatabaseDefinitionError:\n        # Eh, that's fine\n        # And, if we are trying to use a cache of the database, assume\n        # the db is populated and return.\n        if try_cached_db:\n            elapsed = time.monotonic() - start_time\n            stats.append(\n                ('setup::' + dbname,\n                 {'running-time': elapsed, 'cached': True}))\n            return\n    except Exception as ex:\n        raise RuntimeError(\n            f'exception during creation of {dbname!r} test DB: '\n            f'{type(ex).__name__}({ex})'\n        ) from ex\n    finally:\n        await admin_conn.aclose()\n\n    dbconn = await tconn.async_connect_test_client(\n        database=dbname, **default_args\n    )\n    try:\n        if setup_script:\n            async for tx in dbconn.retrying_transaction():\n                async with tx:\n                    with dbconn.capture_warnings():\n                        await dbconn.execute(setup_script)\n    except Exception as ex:\n        raise RuntimeError(\n            f'exception during initialization of {dbname!r} test DB: '\n            f'{type(ex).__name__}({ex})'\n        ) from ex\n    finally:\n        await dbconn.aclose()\n\n    elapsed = time.monotonic() - start_time\n    stats.append(\n        ('setup::' + dbname, {'running-time': elapsed, 'cached': False}))\n\n\n_lock_cnt = 0\n\n\ndef gen_lock_key():\n    global _lock_cnt\n    _lock_cnt += 1\n    return os.getpid() * 1000 + _lock_cnt\n\n\nclass _EdgeDBServerData(NamedTuple):\n\n    host: str\n    port: int\n    password: str\n    server_data: Any\n    tls_cert_file: str\n    pid: int\n\n    def get_connect_args(self, **kwargs) -> dict[str, str | int]:\n        conn_args = dict(\n            user='edgedb',\n            password=self.password,\n            host=self.host,\n            port=self.port,\n            tls_ca_file=self.tls_cert_file,\n        )\n\n        conn_args.update(kwargs)\n        return conn_args\n\n    def fetch_metrics(self) -> str:\n        ctx = ssl.create_default_context()\n        ctx.load_verify_locations(self.tls_cert_file)\n        return _fetch_metrics(self.host, self.port, sslctx=ctx)\n\n    def fetch_server_info(self) -> dict[str, Any]:\n        return _fetch_server_info(self.host, self.port)\n\n    def call_system_api(self, path: str, **kwargs):\n        args = dict(host=self.host, port=self.port, path=path)\n        args.update(kwargs)\n        return _call_system_api(**args)\n\n    async def connect(self, **kwargs: Any) -> tconn.Connection:\n        conn_args = self.get_connect_args(**kwargs)\n        return await tconn.async_connect_test_client(**conn_args)\n\n    async def connect_pg(self, **kwargs: Any) -> asyncpg.Connection:\n        import asyncpg\n\n        conn_args = self.get_connect_args(**kwargs)\n        return await asyncpg.connect(\n            host=conn_args['host'],\n            port=conn_args['port'],\n            user=conn_args['user'],\n            password=conn_args['password'],\n            ssl='require'\n        )\n\n    async def connect_test_protocol(self, **kwargs):\n        conn_args = self.get_connect_args(**kwargs)\n        conn = await test_protocol.new_connection(**conn_args)\n        await conn.connect()\n        return conn\n\n\nclass _EdgeDBServer:\n\n    proc: Optional[asyncio.Process]\n\n    def __init__(\n        self,\n        *,\n        bind_addrs: tuple[str, ...] = ('localhost',),\n        bootstrap_command: Optional[str],\n        auto_shutdown_after: Optional[int],\n        adjacent_to: Optional[tconn.Connection],\n        max_allowed_connections: Optional[int],\n        compiler_pool_size: int,\n        compiler_pool_mode: Optional[edgedb_args.CompilerPoolMode] = None,\n        debug: bool,\n        backend_dsn: Optional[str] = None,\n        data_dir: Optional[str] = None,\n        runstate_dir: Optional[str] = None,\n        reset_auth: Optional[bool] = None,\n        tenant_id: Optional[str] = None,\n        security: edgedb_args.ServerSecurityMode,\n        default_auth_method: Optional[\n            edgedb_args.ServerAuthMethod | edgedb_args.ServerAuthMethods\n        ] = None,\n        binary_endpoint_security: Optional[\n            edgedb_args.ServerEndpointSecurityMode] = None,\n        http_endpoint_security: Optional[\n            edgedb_args.ServerEndpointSecurityMode] = None,  # see __aexit__\n        enable_backend_adaptive_ha: bool = False,\n        ignore_other_tenants: bool = False,\n        readiness_state_file: Optional[str] = None,\n        tls_cert_file: Optional[os.PathLike] = None,\n        tls_key_file: Optional[os.PathLike] = None,\n        tls_cert_mode: edgedb_args.ServerTlsCertMode = (\n            edgedb_args.ServerTlsCertMode.SelfSigned),\n        tls_client_ca_file: Optional[os.PathLike] = None,\n        jws_key_file: Optional[os.PathLike] = None,\n        jwt_sub_allowlist_file: Optional[os.PathLike] = None,\n        jwt_revocation_list_file: Optional[os.PathLike] = None,\n        multitenant_config: Optional[str] = None,\n        config_file: Optional[os.PathLike] = None,\n        default_branch: Optional[str] = None,\n        env: Optional[dict[str, str]] = None,\n        extra_args: Optional[list[str]] = None,\n        net_worker_mode: Optional[str] = None,\n        password: Optional[str] = None,\n    ) -> None:\n        self.bind_addrs = bind_addrs\n        self.auto_shutdown_after = auto_shutdown_after\n        self.bootstrap_command = bootstrap_command\n        self.adjacent_to = adjacent_to\n        self.max_allowed_connections = max_allowed_connections\n        self.compiler_pool_size = compiler_pool_size\n        self.compiler_pool_mode = compiler_pool_mode\n        self.debug = debug\n        self.backend_dsn = backend_dsn\n        self.data_dir = data_dir\n        self.runstate_dir = runstate_dir\n        self.reset_auth = reset_auth\n        self.tenant_id = tenant_id\n        self.proc = None\n        self.data = None\n        self.security = security\n        self.default_auth_method = default_auth_method\n        self.binary_endpoint_security = binary_endpoint_security\n        self.http_endpoint_security = http_endpoint_security\n        self.enable_backend_adaptive_ha = enable_backend_adaptive_ha\n        self.ignore_other_tenants = ignore_other_tenants\n        self.readiness_state_file = readiness_state_file\n        self.tls_cert_file = tls_cert_file\n        self.tls_key_file = tls_key_file\n        self.tls_cert_mode = tls_cert_mode\n        self.tls_client_ca_file = tls_client_ca_file\n        self.jws_key_file = jws_key_file\n        self.jwt_sub_allowlist_file = jwt_sub_allowlist_file\n        self.jwt_revocation_list_file = jwt_revocation_list_file\n        self.multitenant_config = multitenant_config\n        self.config_file = config_file\n        self.default_branch = default_branch\n        self.env = env\n        self.extra_args = extra_args\n        self.net_worker_mode = net_worker_mode\n        self.password = password\n\n    async def wait_for_server_readiness(self, stream: asyncio.StreamReader):\n        while True:\n            line = await stream.readline()\n            if self.debug:\n                print(line.decode())\n            if not line:\n                raise RuntimeError(\"Gel server terminated\")\n            if line.startswith(b'READY='):\n                break\n\n        _, _, dataline = line.decode().partition('=')\n        return json.loads(dataline)\n\n    async def kill_process(self, proc: asyncio.Process):\n        proc.terminate()\n        try:\n            await asyncio.wait_for(proc.wait(), timeout=60)\n        except TimeoutError:\n            proc.kill()\n\n    async def _shutdown(self, exc: Optional[Exception] = None):\n        if self.proc is None:\n            return\n\n        if self.proc.returncode is None:\n            if self.auto_shutdown_after is not None and exc is None:\n                try:\n                    await asyncio.wait_for(self.proc.wait(), timeout=60 * 5)\n                except TimeoutError:\n                    self.proc.kill()\n                    raise AssertionError(\n                        'server did not auto-shutdown in 5 minutes')\n            else:\n                await self.kill_process(self.proc)\n\n        # asyncio, hello?\n        # Workaround SubprocessProtocol.__del__ weirdly\n        # complaining that loop is closed.\n        self.proc._transport.close()\n\n        self.proc = None\n\n    async def __aenter__(self):\n        status_r, status_w = socket.socketpair()\n\n        cmd = [\n            sys.executable, '-I', '-m', 'edb.server.main',\n            '--port', 'auto',\n            '--testmode',\n            '--emit-server-status', f'fd://{status_w.fileno()}',\n            '--compiler-pool-size', str(self.compiler_pool_size),\n            '--tls-cert-mode', str(self.tls_cert_mode),\n            '--jose-key-mode', 'generate',\n        ]\n\n        if self.compiler_pool_mode is not None:\n            cmd.extend(('--compiler-pool-mode', self.compiler_pool_mode.value))\n\n        for addr in self.bind_addrs:\n            cmd.extend(('--bind-address', addr))\n\n        reset_auth = self.reset_auth\n\n        cmd.extend(['--log-level', 'd' if self.debug else 's'])\n        if self.max_allowed_connections is not None:\n            cmd.extend([\n                '--max-backend-connections', str(self.max_allowed_connections),\n            ])\n        if self.backend_dsn is not None:\n            cmd.extend([\n                '--backend-dsn', self.backend_dsn,\n            ])\n        elif self.adjacent_to is not None:\n            settings = self.adjacent_to.get_settings()\n            pgdsn = settings.get('pgdsn')\n            if pgdsn is None:\n                raise RuntimeError('test requires devmode to access pgdsn')\n            cmd += [\n                '--backend-dsn', pgdsn.decode('utf-8')\n            ]\n        elif self.multitenant_config:\n            cmd += ['--multitenant-config-file', self.multitenant_config]\n        elif self.data_dir:\n            cmd += ['--data-dir', self.data_dir]\n        else:\n            cmd += ['--temp-dir']\n\n            if reset_auth is None:\n                reset_auth = True\n\n        if not reset_auth:\n            password = self.password\n            bootstrap_command = ''\n        else:\n            password = secrets.token_urlsafe()\n            bootstrap_command = f\"\"\"\\\n                ALTER ROLE admin {{\n                    SET password := '{password}';\n                }};\n                \"\"\"\n\n        if self.bootstrap_command is not None:\n            bootstrap_command += self.bootstrap_command\n\n        if bootstrap_command:\n            cmd += ['--bootstrap-command', bootstrap_command]\n\n        if self.default_branch is not None:\n            cmd += ['--default-branch', self.default_branch]\n\n        if self.auto_shutdown_after is not None:\n            cmd += ['--auto-shutdown-after', str(self.auto_shutdown_after)]\n\n        if self.runstate_dir:\n            cmd += ['--runstate-dir', self.runstate_dir]\n\n        if self.tenant_id:\n            cmd += ['--tenant-id', self.tenant_id]\n\n        if self.security:\n            cmd += ['--security', str(self.security)]\n\n        if self.default_auth_method:\n            cmd += ['--default-auth-method', str(self.default_auth_method)]\n\n        if self.binary_endpoint_security:\n            cmd += ['--binary-endpoint-security',\n                    str(self.binary_endpoint_security)]\n\n        if self.http_endpoint_security:\n            cmd += ['--http-endpoint-security',\n                    str(self.http_endpoint_security)]\n\n        if self.enable_backend_adaptive_ha:\n            cmd += ['--enable-backend-adaptive-ha']\n\n        if self.ignore_other_tenants:\n            cmd += ['--ignore-other-tenants']\n\n        if self.tls_cert_file:\n            cmd += ['--tls-cert-file', self.tls_cert_file]\n\n        if self.tls_key_file:\n            cmd += ['--tls-key-file', self.tls_key_file]\n\n        if self.tls_client_ca_file:\n            cmd += ['--tls-client-ca-file', str(self.tls_client_ca_file)]\n\n        if self.readiness_state_file:\n            cmd += ['--readiness-state-file', self.readiness_state_file]\n\n        if self.jws_key_file:\n            cmd += ['--jws-key-file', str(self.jws_key_file)]\n\n        if self.jwt_sub_allowlist_file:\n            cmd += ['--jwt-sub-allowlist-file', self.jwt_sub_allowlist_file]\n\n        if self.jwt_revocation_list_file:\n            cmd += ['--jwt-revocation-list-file',\n                    self.jwt_revocation_list_file]\n\n        if self.config_file:\n            cmd += ['--config-file', self.config_file]\n\n        if not self.multitenant_config:\n            cmd += ['--instance-name=localtest']\n\n        if self.net_worker_mode:\n            cmd += ['--net-worker-mode', self.net_worker_mode]\n\n        if self.extra_args:\n            cmd.extend(self.extra_args)\n\n        if self.debug:\n            print(\n                f'Starting Gel cluster with the following params:\\n'\n                f'{\" \".join(shlex.quote(c) for c in cmd)}'\n            )\n\n        env = os.environ.copy()\n        if self.env:\n            env.update(self.env)\n        env.pop(\"EDGEDB_SERVER_MULTITENANT_CONFIG_FILE\", None)\n\n        stat_reader, stat_writer = await asyncio.open_connection(sock=status_r)\n\n        self.proc: asyncio.Process = await asyncio.create_subprocess_exec(\n            *cmd,\n            env=env,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.STDOUT,\n            pass_fds=(status_w.fileno(),),\n        )\n\n        status_task = asyncio.create_task(\n            asyncio.wait_for(\n                self.wait_for_server_readiness(stat_reader),\n                timeout=240,\n            ),\n        )\n\n        output = b''\n\n        async def read_stdout():\n            nonlocal output\n            # Tee the log temporarily to a tempfile that exists as long as the\n            # test is running. This helps debug hanging tests.\n            with tempfile.NamedTemporaryFile(\n                mode='w+t',\n                prefix='edgedb-test-log-') as temp_file:\n                if self.debug:\n                    print(f\"Logging to {temp_file.name}\")\n                while True:\n                    line = await self.proc.stdout.readline()\n                    if not line:\n                        break\n                    output += line\n                    temp_file.write(line.decode(errors='ignore'))\n                    if self.debug:\n                        print(line.decode(errors='ignore'), end='')\n\n        stdout_task = asyncio.create_task(read_stdout())\n\n        try:\n            _, pending = await asyncio.wait(\n                [\n                    status_task,\n                    asyncio.create_task(self.proc.wait()),\n                ],\n                return_when=asyncio.FIRST_COMPLETED,\n            )\n        except (Exception, asyncio.CancelledError):\n            try:\n                await self._shutdown()\n            finally:\n                raise\n        finally:\n            stat_writer.close()\n            status_w.close()\n\n        if pending:\n            for task in pending:\n                if not task.done():\n                    task.cancel()\n\n            await asyncio.wait(pending, timeout=10)\n\n        if self.proc.returncode is not None:\n            await stdout_task\n            raise edgedb_cluster.ClusterError(output.decode(errors='ignore'))\n        else:\n            assert status_task.done()\n            data = status_task.result()\n\n        return _EdgeDBServerData(\n            host='localhost',\n            port=data['port'],\n            password=password,\n            server_data=data,\n            tls_cert_file=data['tls_cert_file'],\n            pid=self.proc.pid,\n        )\n\n    async def __aexit__(self, exc_type, exc, tb):\n        try:\n            if (\n                (\n                    self.http_endpoint_security\n                    is edgedb_args.ServerEndpointSecurityMode.Optional\n                )\n                and self.data is not None\n                and self.auto_shutdown_after is None\n            ):\n                # It's a good idea to test most of the ad-hoc test clusters\n                # for any errors in background tasks, as such tests usually\n                # test the functionality that involves notifications and\n                # other async events.\n                metrics = _fetch_metrics('127.0.0.1', self.data['port'])\n                errors = _extract_background_errors(metrics)\n                if errors:\n                    raise AssertionError(\n                        'server terminated with unexpected ' +\n                        'background errors\\n\\n' +\n                        errors\n                    )\n        finally:\n            await self._shutdown(exc)\n\n\ndef start_edgedb_server(\n    *,\n    bind_addrs: tuple[str, ...] = ('localhost',),\n    auto_shutdown_after: Optional[int]=None,\n    bootstrap_command: Optional[str]=None,\n    max_allowed_connections: Optional[int]=5,\n    compiler_pool_size: int=2,\n    compiler_pool_mode: Optional[edgedb_args.CompilerPoolMode] = None,\n    adjacent_to: Optional[tconn.Connection]=None,\n    debug: bool=debug.flags.server,\n    backend_dsn: Optional[str] = None,\n    runstate_dir: Optional[str] = None,\n    data_dir: Optional[str] = None,\n    reset_auth: Optional[bool] = None,\n    tenant_id: Optional[str] = None,\n    security: edgedb_args.ServerSecurityMode = (\n        edgedb_args.ServerSecurityMode.Strict),\n    default_auth_method: Optional[\n        edgedb_args.ServerAuthMethod | edgedb_args.ServerAuthMethods\n    ] = None,\n    binary_endpoint_security: Optional[\n        edgedb_args.ServerEndpointSecurityMode] = None,\n    http_endpoint_security: Optional[\n        edgedb_args.ServerEndpointSecurityMode] = None,\n    enable_backend_adaptive_ha: bool = False,\n    ignore_other_tenants: bool = False,\n    readiness_state_file: Optional[str] = None,\n    tls_cert_file: Optional[os.PathLike] = None,\n    tls_key_file: Optional[os.PathLike] = None,\n    tls_cert_mode: edgedb_args.ServerTlsCertMode = (\n        edgedb_args.ServerTlsCertMode.SelfSigned),\n    tls_client_ca_file: Optional[os.PathLike] = None,\n    jws_key_file: Optional[os.PathLike] = None,\n    jwt_sub_allowlist_file: Optional[os.PathLike] = None,\n    jwt_revocation_list_file: Optional[os.PathLike] = None,\n    multitenant_config: Optional[str] = None,\n    config_file: Optional[os.PathLike] = None,\n    env: Optional[dict[str, str]] = None,\n    extra_args: Optional[list[str]] = None,\n    default_branch: Optional[str] = None,\n    net_worker_mode: Optional[str] = None,\n    force_new: bool = False,  # True for ignoring multitenant config env\n):\n    if (not devmode.is_in_dev_mode() or adjacent_to) and not runstate_dir:\n        if backend_dsn or adjacent_to:\n            import traceback\n            # We don't want to implicitly \"fix the issue\" for the test author\n            print('WARNING: starting a Gel server with the default '\n                  'runstate_dir; the test is likely to fail or hang. '\n                  'Consider specifying the runstate_dir parameter.')\n            print('\\n'.join(traceback.format_stack(limit=5)))\n\n    password = None\n    if mt_conf := os.environ.get(\"EDGEDB_SERVER_MULTITENANT_CONFIG_FILE\"):\n        if multitenant_config is None and max_allowed_connections == 10:\n            if not any(\n                (\n                    adjacent_to,\n                    data_dir,\n                    backend_dsn,\n                    compiler_pool_mode,\n                    default_branch,\n                    force_new,\n                )\n            ):\n                multitenant_config = mt_conf\n                max_allowed_connections = None\n                password = 'test'  # set in init_cluster() by test/runner.py\n\n    params = locals()\n    exclusives = [\n        name\n        for name in [\n            \"adjacent_to\",\n            \"data_dir\",\n            \"backend_dsn\",\n            \"multitenant_config\",\n        ]\n        if params[name]\n    ]\n    if len(exclusives) > 1:\n        raise RuntimeError(\n            \" and \".join(exclusives) + \" options are mutually exclusive\"\n        )\n\n    if not runstate_dir and data_dir:\n        runstate_dir = data_dir\n\n    return _EdgeDBServer(\n        bind_addrs=bind_addrs,\n        auto_shutdown_after=auto_shutdown_after,\n        bootstrap_command=bootstrap_command,\n        max_allowed_connections=max_allowed_connections,\n        adjacent_to=adjacent_to,\n        compiler_pool_size=compiler_pool_size,\n        compiler_pool_mode=compiler_pool_mode,\n        debug=debug,\n        backend_dsn=backend_dsn,\n        tenant_id=tenant_id,\n        data_dir=data_dir,\n        runstate_dir=runstate_dir,\n        reset_auth=reset_auth,\n        security=security,\n        default_auth_method=default_auth_method,\n        binary_endpoint_security=binary_endpoint_security,\n        http_endpoint_security=http_endpoint_security,\n        enable_backend_adaptive_ha=enable_backend_adaptive_ha,\n        ignore_other_tenants=ignore_other_tenants,\n        readiness_state_file=readiness_state_file,\n        tls_cert_file=tls_cert_file,\n        tls_key_file=tls_key_file,\n        tls_cert_mode=tls_cert_mode,\n        tls_client_ca_file=tls_client_ca_file,\n        jws_key_file=jws_key_file,\n        jwt_sub_allowlist_file=jwt_sub_allowlist_file,\n        jwt_revocation_list_file=jwt_revocation_list_file,\n        multitenant_config=multitenant_config,\n        config_file=config_file,\n        env=env,\n        extra_args=extra_args,\n        default_branch=default_branch,\n        net_worker_mode=net_worker_mode,\n        password=password,\n    )\n\n\ndef get_cases_by_shard(cases, selected_shard, total_shards, verbosity, stats):\n    if total_shards <= 1:\n        return cases\n\n    selected_shard -= 1  # starting from 0\n    new_test_est = 0.1  # default estimate if test is not found in stats\n    new_setup_est = 1  # default estimate if setup is not found in stats\n\n    # For logging\n    total_tests = 0\n    selected_tests = 0\n    total_est = 0\n    selected_est = 0\n\n    # Priority queue of tests grouped by setup script ordered by estimated\n    # running time of the groups. Order of tests within cases is preserved.\n    tests_by_setup = []\n\n    # Priority queue of individual tests ordered by estimated running time.\n    tests_with_est = []\n\n    # Prepare the source heaps\n    setup_count = 0\n    for case, tests in cases.items():\n        # Extract zREPEAT tests and attach them to their first runs\n        combined = {}\n        for test in tests:\n            test_name = str(test)\n            orig_name = test_name.replace('test_zREPEAT', 'test')\n            if orig_name == test_name:\n                if test_name in combined:\n                    combined[test_name] = (test, *combined[test_name])\n                else:\n                    combined[test_name] = (test,)\n            else:\n                if orig_name in combined:\n                    combined[orig_name] = (*combined[orig_name], test)\n                else:\n                    combined[orig_name] = (test,)\n\n        setup_script_getter = getattr(case, 'get_setup_script', None)\n        if setup_script_getter and combined:\n            tests_per_setup = []\n            est_per_setup = setup_est = stats.get(\n                'setup::' + case.get_database_name(), (new_setup_est, 0),\n            )[0]\n            for test_name, test in combined.items():\n                total_tests += len(test)\n                est = stats.get(test_name, (new_test_est, 0))[0] * len(test)\n                est_per_setup += est\n                tests_per_setup.append((est, test))\n            heapq.heappush(\n                tests_by_setup,\n                (-est_per_setup, setup_count, setup_est, tests_per_setup),\n            )\n            setup_count += 1\n            total_est += est_per_setup\n        else:\n            for test_name, test in combined.items():\n                total_tests += len(test)\n                est = stats.get(test_name, (new_test_est, 0))[0] * len(test)\n                total_est += est\n                heapq.heappush(tests_with_est, (-est, total_tests, test))\n\n    target_est = total_est / total_shards  # target running time of one shard\n    shards_est = [(0, shard, set()) for shard in range(total_shards)]\n    cases = {}  # output\n    setup_to_alloc = set(range(setup_count))  # tracks first run of each setup\n\n    # Assign per-setup tests first\n    while tests_by_setup:\n        remaining_est, setup_id, setup_est, tests = heapq.heappop(\n            tests_by_setup,\n        )\n        est_acc, current, setups = heapq.heappop(shards_est)\n\n        # Add setup time\n        if setup_id not in setups:\n            setups.add(setup_id)\n            est_acc += setup_est\n            if current == selected_shard:\n                selected_est += setup_est\n            if setup_id in setup_to_alloc:\n                setup_to_alloc.remove(setup_id)\n            else:\n                # This means one more setup for the overall test run\n                target_est += setup_est / total_shards\n\n        # Add as much tests from this group to current shard as possible\n        while tests:\n            est, test = tests.pop(0)\n            est_acc += est  # est is a positive number\n            remaining_est += est  # remaining_est is a negative number\n\n            if current == selected_shard:\n                # Add the test to the result\n                _add_test(cases, test)\n                selected_tests += len(test)\n                selected_est += est\n\n            if est_acc >= target_est and -remaining_est > setup_est * 2:\n                # Current shard is full and the remaining tests would take more\n                # time than their setup, then add the tests back to the heap so\n                # that we could add them to another shard\n                heapq.heappush(\n                    tests_by_setup,\n                    (remaining_est, setup_id, setup_est, tests),\n                )\n                break\n\n        heapq.heappush(shards_est, (est_acc, current, setups))\n\n    # Assign all non-setup tests, but leave the last shard for everything else\n    setups = set()\n    while tests_with_est and len(shards_est) > 1:\n        est, _, test = heapq.heappop(tests_with_est)  # est is negative\n        est_acc, current, setups = heapq.heappop(shards_est)\n        est_acc -= est\n\n        if current == selected_shard:\n            # Add the test to the result\n            _add_test(cases, test)\n            selected_tests += len(test)\n            selected_est -= est\n\n        if est_acc >= target_est:\n            # The current shard is full\n            if current == selected_shard:\n                # End early if the selected shard is full\n                break\n        else:\n            # Only add the current shard back to the heap if it's not full\n            heapq.heappush(shards_est, (est_acc, current, setups))\n\n    else:\n        # Add all the remaining tests to the first remaining shard if any\n        while shards_est:\n            est_acc, current, setups = heapq.heappop(shards_est)\n            if current == selected_shard:\n                for est, _, test in tests_with_est:\n                    _add_test(cases, test)\n                    selected_tests += len(test)\n                    selected_est -= est\n                break\n            tests_with_est.clear()  # should always be empty already here\n\n    if verbosity >= 1:\n        print(f'Running {selected_tests}/{total_tests} tests for shard '\n              f'#{selected_shard + 1} out of {total_shards} shards, '\n              f'estimate: {int(selected_est / 60)}m {int(selected_est % 60)}s'\n              f' / {int(total_est / 60)}m {int(total_est % 60)}s, '\n              f'{len(setups)}/{setup_count} databases to setup.')\n    return _merge_results(cases)\n\n\ndef find_available_port() -> int:\n    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:\n        sock.bind((\"localhost\", 0))\n        return sock.getsockname()[1]\n\n\ndef _needs_factoring(weakly):\n    def decorator(f):\n        async def g(self, *args, **kwargs):\n            if self.NO_FACTOR and not weakly:\n                with self.assertRaisesRegex(Exception, ''):\n                    await f(self, *args, **kwargs)\n            elif self.WARN_FACTOR:\n                with self.assertRaisesRegex(\n                    edgedb.InvalidReferenceError, 'attempting to factor out'\n                ):\n                    await f(self, *args, **kwargs)\n\n            else:\n                await f(self, *args, **kwargs)\n\n        return g\n    return decorator\n\n\n@contextlib.asynccontextmanager\nasync def temp_file_with(data: bytes):\n    with tempfile.NamedTemporaryFile() as f:\n        f.write(data)\n        f.flush()\n        yield f\n\n\nneeds_factoring = _needs_factoring(weakly=False)\nneeds_factoring_weakly = _needs_factoring(weakly=True)\n"
  },
  {
    "path": "edb/tools/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n# DO NOT ADD ANYTHING TO THIS FILE:\n# This package contains tools like mypy plugins that\n# when loaded should not trigger any imports.\n"
  },
  {
    "path": "edb/tools/__main__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\"\"\"Stub to allow invoking `edb` as `python -m edb.tools`.\"\"\"\n\nfrom __future__ import annotations\n\nimport sys\n\nfrom edb.tools import edb\n\n\nif __name__ == '__main__':\n    sys.exit(edb.edbcommands(prog_name='edb'))\n"
  },
  {
    "path": "edb/tools/ast_inheritance_graph.py",
    "content": "# Generates an inheritance graph of Python classes.\n#\n# Usage:\n# $ edb ast-inheritance-graph | fdp -T svg -o ast-fdp.svg\n#\n# Requirements:\n# - graphviz\n\nimport typing\nimport dataclasses\nimport enum\n\nimport click\n\nfrom edb.edgeql import ast as qlast\nfrom edb.ir import ast as irast\nfrom edb.pgsql import ast as pgast\nfrom edb.tools.edb import edbcommands\n\n\nclass ASTModule(str, enum.Enum):\n    ql = \"ql\"\n    ir = \"ir\"\n    pg = \"pg\"\n\n\n@dataclasses.dataclass()\nclass ASTClass:\n    name: str\n    typ: typing.Any\n    bases: set[str]\n    children: set[str]\n\n\n@edbcommands.command(\"ast-inheritance-graph\")\n@click.argument('ast', type=click.Choice(ASTModule))  # type: ignore\ndef main(ast: ASTModule) -> None:\n    ast_mod: typing.Any\n    if ast == ASTModule.ql:\n        ast_mod = qlast\n    elif ast == ASTModule.ir:\n        ast_mod = irast\n    elif ast == ASTModule.pg:\n        ast_mod = pgast\n    else:\n        raise AssertionError()\n\n    # discover all nodes\n    ast_classes: dict[str, ASTClass] = {}\n    for name, typ in ast_mod.__dict__.items():\n        if not isinstance(typ, type):\n            continue\n\n        if not issubclass(typ, ast_mod.Base) or name in {\n            'Base',\n            'ImmutableBase',\n        }:\n            continue\n\n        if typ.__rust_ignore__:  # type: ignore\n            continue\n\n        # re-run field collection to correctly handle forward-references\n        typ = typ._collect_direct_fields()  # type: ignore\n\n        ast_classes[typ.__name__] = ASTClass(\n            name=name,\n            typ=typ,\n            children=set(),\n            bases=set(),\n        )\n\n    for ast_class in ast_classes.values():\n        for base in ast_class.typ.__bases__:\n            if base.__name__ not in ast_classes:\n                continue\n            ast_class.bases.add(base.__name__)\n            ast_classes[base.__name__].children.add(ast_class.name)\n\n    inheritance_graph(ast_classes)\n    enum_graph(ast_classes)\n\n\ndef inheritance_graph(ast_classes: dict[str, ASTClass]):\n    print('digraph I {')\n    for ast_class in ast_classes.values():\n        if ast_class.typ.__abstract_node__:\n            print(f'  {ast_class.name} [color = red];')\n        for base in ast_class.bases:\n            print(f'  {ast_class.name} -> {base};')\n\n    print('}')\n\n\ndef enum_graph(ast_classes: dict[str, ASTClass]):\n    print('digraph M {')\n\n    def dfs(node, start):\n        ast_class = ast_classes[node]\n        if ast_class.typ.__abstract_node__:\n            print(f'  {node}_{start} [color = red];')\n\n        for child in ast_class.children:\n            print(f'  {node}_{start} -> {child}_{start};')\n            dfs(child, start)\n\n    for ast_class in ast_classes.values():\n        if len(ast_class.bases) != 0 or len(ast_class.children) == 0:\n            continue\n        dfs(ast_class.name, ast_class.name)\n\n    print('}')\n"
  },
  {
    "path": "edb/tools/cli.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport subprocess\nimport sys\n\nimport click\n\nfrom edb import cli as rustcli\nfrom edb.tools.edb import edbcommands\n\n\n@edbcommands.command('cli',\n                     add_help_option=False,\n                     context_settings=dict(ignore_unknown_options=True))\n@click.argument('args', nargs=-1, type=click.UNPROCESSED)\ndef cli(args: tuple[str, ...]):\n    \"\"\"Run edgedb CLI with `-H localhost`.\"\"\"\n\n    args_list = _ensure_linked(args)\n\n    if (\n        '--wait-until-available' not in args_list and\n        not any('--wait-until-available=' in a for a in args_list)\n    ):\n        args_list += ['--wait-until-available', '60s']\n\n    sys.exit(rustcli.rustcli(args=[sys.argv[0], *args_list]))\n\n\n@edbcommands.command('ui',\n                     add_help_option=False,\n                     context_settings=dict(ignore_unknown_options=True))\n@click.argument('args', nargs=-1, type=click.UNPROCESSED)\ndef ui(args: tuple[str, ...]):\n    \"\"\"Run edgedb GUI with `-H localhost`.\"\"\"\n\n    _ensure_linked(args)\n    subprocess.check_call(\n        [\n            sys.executable,\n            \"-I\",\n            \"-m\",\n            \"edb.cli\",\n            \"ui\",\n            \"--instance=_localdev\",\n        ],\n    )\n\n\ndef _ensure_linked(args: tuple[str, ...]) -> list[str]:\n\n    if (\n        '--host' not in args and\n        not any(a.startswith('-H') for a in args) and\n        not any(a.startswith('--host=') for a in args) and\n        '--port' not in args and\n        not any(a.startswith('-P') for a in args) and\n        not any(a.startswith('--port=') for a in args) and\n        '--instance' not in args and\n        not any(a.startswith('-I') for a in args) and\n        not any(a.startswith('--instance=') for a in args)\n    ):\n        subprocess.check_call([\n            sys.executable,\n            \"-I\",\n            \"-m\",\n            \"edb.cli\",\n            \"instance\",\n            \"link\",\n            \"--host=localhost\",\n            \"--non-interactive\",\n            \"--trust-tls-cert\",\n            \"--overwrite\",\n            \"--quiet\",\n            \"_localdev\",\n        ])\n\n        return list(args) + ['-I', '_localdev']\n    else:\n        return list(args)\n"
  },
  {
    "path": "edb/tools/config.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\n\nimport click\n\nfrom edb import buildmeta\nfrom edb.common import devmode\nfrom edb.tools.edb import edbcommands\n\n\n@edbcommands.command(\"config\")\n@click.option(\n    \"--make-include\",\n    is_flag=True,\n    help='Print path to the include file for extension Makefiles',\n)\n@click.option(\n    \"--pg-config\",\n    is_flag=True,\n    help='Print path to bundled pg_config',\n)\ndef config(make_include: bool, pg_config: bool) -> None:\n    '''Query certain parameters about an edgedb environment'''\n    if make_include:\n        share = buildmeta.get_extension_dir_path()\n        base = share.parent.parent.parent\n        # XXX: It should not be here.\n        if not devmode.is_in_dev_mode():\n            base = base / 'share'\n        mk = (\n            base / 'tests' / 'extension-testing' / 'exts.mk'\n        )\n        print(mk)\n    if pg_config:\n        print(buildmeta.get_pg_config_path())\n"
  },
  {
    "path": "edb/tools/dflags.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom edb.common import debug\nfrom edb.tools.edb import edbcommands\n\n\n@edbcommands.command('dflags')\ndef gen_types():\n    \"\"\"Print available debug flags.\"\"\"\n\n    for flag in debug.flags:\n        print(f'env EDGEDB_DEBUG_{flag.name.upper()}=1')\n        print(f'    {flag.doc}\\n')\n"
  },
  {
    "path": "edb/tools/docs/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom docutils import nodes as d_nodes\nfrom docutils.parsers import rst as d_rst\nfrom sphinx import addnodes as s_nodes\nfrom sphinx import transforms as s_transforms\nfrom sphinx.domains.index import IndexDirective\n\nfrom . import edb\nfrom . import cli\nfrom . import eql\nfrom . import js\nfrom . import sdl\nfrom . import graphql\nfrom . import go\nfrom . import shared\n\n\nclass ProhibitedNodeTransform(s_transforms.SphinxTransform):\n\n    default_priority = 1  # before ReferencesResolver\n\n    def apply(self):\n        for bq in list(self.document.traverse(d_nodes.block_quote)):\n            if not bq['classes'] or 'pull-quote' not in bq['classes']:\n                raise shared.EdgeSphinxExtensionError(\n                    f'blockquote found: {bq.asdom().toxml()!r} in {bq.source};'\n                    f' Try using the \"pull-quote\" directive')\n            else:\n                bq.get('classes').remove('pull-quote')\n\n        trs = list(self.document.traverse(d_nodes.title_reference))\n        if trs:\n            raise shared.EdgeSphinxExtensionError(\n                f'title reference (single backticks quote) found: '\n                f'{trs[0].asdom().toxml()!r} in {trs[0].source}; '\n                f'perhaps you wanted to use double backticks?')\n\n\nclass VersionAdded(d_rst.Directive):\n\n    has_content = True\n    optional_arguments = 0\n    required_arguments = 1\n\n    def run(self):\n        node = s_nodes.versionmodified()\n        node['type'] = 'versionadded'\n        node['version'] = self.arguments[0]\n        self.state.nested_parse(self.content, self.content_offset, node)\n        return [node]\n\n\nclass VersionChanged(d_rst.Directive):\n\n    has_content = True\n    optional_arguments = 0\n    required_arguments = 1\n\n    def run(self):\n        node = s_nodes.versionmodified()\n        node['type'] = 'versionchanged'\n        node['version'] = self.arguments[0]\n        self.state.nested_parse(self.content, self.content_offset, node)\n        return [node]\n\n\nclass VersionedSection(d_rst.Directive):\n\n    has_content = False\n    optional_arguments = 0\n    required_arguments = 0\n\n    def run(self):\n        node = d_nodes.container()\n        node['versioned-section'] = True\n        return [node]\n\n\nclass VersionedReplaceRole:\n\n    def __call__(\n        self, role, rawtext, text, lineno, inliner, options=None, content=None\n    ):\n        nodes = []\n        if not text.startswith('_default:'):\n            text = '_default:' + text\n        for section in text.split(';'):\n            parts = section.split(':', maxsplit=1)\n            node = s_nodes.versionmodified()\n            node['type'] = 'versionchanged'\n            node['version'] = parts[0].strip()\n            node += d_nodes.Text(parts[1].strip())\n            nodes.append(node)\n        return nodes, []\n\n\nclass APIIndex(IndexDirective):\n\n    def run(self):\n        nodes = super().run()\n        nodes[0]['api-index'] = True\n        return nodes\n\n\ndef setup(app):\n    edb.setup_domain(app)\n    cli.setup_domain(app)\n    eql.setup_domain(app)\n    js.setup_domain(app)\n    sdl.setup_domain(app)\n    graphql.setup_domain(app)\n    go.setup_domain(app)\n\n    app.add_directive('versionadded', VersionAdded, True)\n    app.add_directive('versionchanged', VersionChanged, True)\n    app.add_directive('code-block', shared.CodeBlock, True)\n    app.add_directive('versioned-section', VersionedSection)\n    app.add_directive('api-index', APIIndex)\n    app.add_role('versionreplace', VersionedReplaceRole())\n\n    app.add_transform(ProhibitedNodeTransform)\n"
  },
  {
    "path": "edb/tools/docs/cli.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom edb.tools.pygments.edgeql import EdgeQLLexer\n\nimport pygments.lexers\n\nfrom sphinx import domains as s_domains\nfrom docutils.parsers.rst import directives as d_directives  # type: ignore\n\nfrom . import shared\n\n\nclass CLISynopsisDirective(shared.CodeBlock):\n\n    has_content = True\n    optional_arguments = 0\n    required_arguments = 0\n    option_spec: dict[str, Any] = {\n        'version-lt': d_directives.unchanged_required\n    }\n\n    def run(self):\n        self.arguments = ['cli-synopsis']\n        return super().run()\n\n\nclass CLIDomain(s_domains.Domain):\n\n    name = \"cli\"\n    label = \"Command Line Interface\"\n\n    directives = {\n        'synopsis': CLISynopsisDirective,\n    }\n\n\ndef setup_domain(app):\n    # Dummy lexers; the actual highlighting is implemented\n    # in the edgedb.com website code.\n    app.add_lexer(\"txt\", pygments.lexers.TextLexer)\n    app.add_lexer(\"bash\", pygments.lexers.TextLexer)\n\n    app.add_lexer(\"cli\", EdgeQLLexer)\n    app.add_lexer(\"cli-synopsis\", EdgeQLLexer)\n\n    app.add_role(\n        'cli:synopsis',\n        shared.InlineCodeRole('cli-synopsis'))\n\n    app.add_domain(CLIDomain)\n"
  },
  {
    "path": "edb/tools/docs/edb.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport re\n\nfrom sphinx import domains as s_domains\nfrom docutils import nodes as d_nodes\nfrom docutils.parsers import rst as d_rst\nfrom docutils.parsers.rst import directives as d_directives  # type: ignore\n\nfrom sphinx import transforms\n\n\nclass EDBYoutubeEmbed(d_rst.Directive):\n\n    has_content = True\n    optional_arguments = 0\n    required_arguments = 1\n\n    def run(self):\n        node = d_nodes.container()\n        node['youtube-video-id'] = self.arguments[0]\n        self.state.nested_parse(self.content, self.content_offset, node)\n        return [node]\n\n\nclass EDBCollapsed(d_rst.Directive):\n\n    has_content = True\n    optional_arguments = 0\n    required_arguments = 0\n    option_spec = {\n        'summary': d_directives.unchanged_required,\n    }\n\n    def run(self):\n        node = d_nodes.container()\n        node['collapsed_block'] = True\n        if 'summary' in self.options:\n            node['summary'] = self.options['summary']\n        self.state.nested_parse(self.content, self.content_offset, node)\n        return [node]\n\n\nclass EDBEnvironmentSwitcher(d_rst.Directive):\n\n    has_content = False\n    optional_arguments = 0\n    required_arguments = 0\n\n    def run(self):\n        node = d_nodes.container()\n        node['env-switcher'] = True\n        return [node]\n\n\nclass EDBSplitSection(d_rst.Directive):\n\n    has_content = True\n    optional_arguments = 0\n    required_arguments = 0\n\n    def run(self):\n        node = d_nodes.container()\n        node['split-section'] = True\n        self.state.nested_parse(self.content, self.content_offset, node)\n\n        split_indexes = [\n            index for index, child in enumerate(node.children)\n            if isinstance(child, d_nodes.container) and child.get('split-point')\n        ]\n        if len(split_indexes) > 1:\n            raise Exception(\n                f'cannot have multiple edb:split-point\\'s in edb:split-section'\n            )\n        blocks = (\n            node.children[:split_indexes[0]] if\n            node.children[split_indexes[0]].get('code-above')\n            else node.children[split_indexes[0] + 1:]\n        ) if len(split_indexes) == 1 else [node.children[-1]]\n        if len(blocks) < 1:\n            raise Exception(\n                f'no content found at end of edb:split-section block, '\n                f'or before/after the edb:split-point in the edb:split-section'\n            )\n        return [node]\n\n\nclass EDBSplitPoint(d_rst.Directive):\n\n    has_content = False\n    optional_arguments = 1\n    required_arguments = 0\n\n    def run(self):\n        node = d_nodes.container()\n        node['split-point'] = True\n        if len(self.arguments) > 0:\n            if self.arguments[0] not in ['above', 'below']:\n                raise Exception(\n                    f\"expected edb:split-point arg to be 'above', 'below' \"\n                    f\"or empty (defaults to 'below')\"\n                )\n            if self.arguments[0] == 'above':\n                node['code-above'] = True\n        return [node]\n\n\nclass GelDomain(s_domains.Domain):\n    name = \"edb\"\n    label = \"Gel\"\n\n    directives = {\n        'collapsed': EDBCollapsed,\n        'youtube-embed': EDBYoutubeEmbed,\n        'env-switcher': EDBEnvironmentSwitcher,\n        'split-section': EDBSplitSection,\n        'split-point': EDBSplitPoint\n    }\n\n\nclass GelSubstitutionTransform(transforms.SphinxTransform):\n    default_priority = 0\n\n    def apply(self):\n        builder_name = \"html\"\n        if hasattr(self.document.settings, 'env'):\n            env = self.document.settings.env\n            if env and hasattr(env, \"app\"):\n                builder_name = env.app.builder.name\n\n        # Traverse all substitution_reference nodes.\n        for node in self.document.traverse(d_nodes.substitution_reference):\n            nt = node.astext()\n            if nt.lower() in {\n                \"gel\", \"gel's\", \"edgedb\", \"gelcmd\", \".gel\", \"gel.toml\",\n                \"gel-server\", \"geluri\", \"admin\", \"main\",\n                \"branch\", \"branches\"\n            }:\n                if builder_name in {\"xml\", \"edge-xml\"}:\n                    if nt == \"gelcmd\":\n                        sub = d_nodes.literal(\n                            'gel', 'gel',\n                            **{\n                                \"edb-gelcmd\": \"true\",\n                                \"edb-gelcmd-top\": \"true\",\n                                \"edb-substitution\": \"true\",\n                            }\n                        )\n                    elif nt == \"geluri\":\n                        sub = d_nodes.literal(\n                            'gel', 'gel://',\n                            **{\n                                \"edb-geluri\": \"true\",\n                                \"edb-geluri-top\": \"true\",\n                                \"edb-substitution\": \"true\",\n                            }\n                        )\n                    else:\n                        sub = d_nodes.inline(\n                            nt, nt, **{\"edb-substitution\": \"true\"}\n                        )\n                    node.replace_self(sub)\n                else:\n                    node.replace_self(d_nodes.Text(nt))\n\n\nclass GelCmdRole:\n\n    def __call__(\n        self, role, rawtext, text, lineno, inliner, options=None, content=None\n    ):\n        text = text.strip()\n        text = re.sub(r'(\\n\\s*)+', \" \", text)\n        if text.startswith(\"edgedb\"):\n            fn = inliner.document.current_source\n            raise Exception(\n                f\"{fn}:{lineno} - :gelcmd:`{text}` - can't start with 'edgedb'\"\n            )\n        if text.startswith(\"gel \") or text == \"gel\":\n            fn = inliner.document.current_source\n            raise Exception(\n                f\"{fn}:{lineno} - :gelcmd:`{text}` - can't start with 'gel'\"\n            )\n        text = f'gel {text}'\n        node = d_nodes.literal(text, text)\n        node[\"edb-gelcmd\"] = \"true\"\n        node[\"edb-gelcmd-top\"] = \"false\"\n        node[\"edb-substitution\"] = \"true\"\n        return [node], []\n\n\nclass GelUriRole:\n\n    def __call__(\n        self, role, rawtext, text, lineno, inliner, options=None, content=None\n    ):\n        if text.startswith(\"edgedb://\"):\n            fn = inliner.document.current_source\n            raise Exception(\n                f\"{fn}:{lineno} - :geluri:`{text}`\"\n                f\" - can't start with 'edgedb://'\"\n            )\n        if text.startswith(\"gel://\"):\n            fn = inliner.document.current_source\n            raise Exception(\n                f\"{fn}:{lineno} - :geluri:`{text}` - can't start with 'gel://'\"\n            )\n        text = f'gel://{text}'\n        node = d_nodes.literal(text, text)\n        node[\"edb-geluri\"] = \"true\"\n        node[\"edb-geluri-top\"] = \"false\"\n        node[\"edb-substitution\"] = \"true\"\n        return [node], []\n\n\nclass DotGelRole:\n\n    def __call__(\n        self, role, rawtext, text, lineno, inliner, options=None, content=None\n    ):\n        if text.endswith(\".gel\") or text.endswith(\".esdl\"):\n            fn = inliner.document.current_source\n            raise Exception(\n                f\"{fn}:{lineno} - :dotgel:`{text}`\"\n                f\" - can't end with '.esdl' or '.gel'\"\n            )\n        text = f'{text}.gel'\n        node = d_nodes.literal(text, text)\n        node[\"edb-dotgel\"] = \"true\"\n        node[\"edb-substitution\"] = \"true\"\n        return [node], []\n\n\nclass GelEnvRole:\n\n    def __call__(\n        self, role, rawtext, text, lineno, inliner, options=None, content=None\n    ):\n        if (\n            text.startswith(\"EDGEDB_\") or\n            text.startswith(\"GEL_\") or\n            text.startswith(\"_\")\n        ):\n            fn = inliner.document.current_source\n            raise Exception(\n                f\"{fn}:{lineno} - :gelenv:`{text}`\"\n                f\" - can't start with 'EDGEDB_', 'GEL_', or '_'\"\n            )\n        text = f'GEL_{text}'\n        node = d_nodes.literal(text, text)\n        node[\"edb-gelenv\"] = \"true\"\n        node[\"edb-substitution\"] = \"true\"\n        return [node], []\n\n\ndef setup_domain(app):\n\n    app.add_role('gelcmd', GelCmdRole())\n    app.add_role('geluri', GelUriRole())\n    app.add_role('dotgel', DotGelRole())\n    app.add_role('gelenv', GelEnvRole())\n    app.add_domain(GelDomain)\n    app.add_transform(GelSubstitutionTransform)\n\n\ndef setup(app):\n    setup_domain(app)\n"
  },
  {
    "path": "edb/tools/docs/eql.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nr\"\"\"\n=====================================\n:eql: domain for EdgeQL documentation\n=====================================\n\n\nFunctions\n---------\n\nTo declare a function use a \".. eql:function::\" directive.  A few\nthings must be defined:\n\n* Full function signature with a fully qualified name must be specified.\n\n* \":param $name: description:\" a short description of the $name parameter.\n  $name must match the the name of the parameter in function's signature.\n  If a parameter is anonymous, its number should be used instead (e.g. $1).\n\n* \":paramtype $name: type\": for every :param: there must be a\n  corresponding :paramtype field.  For example: \":paramtype $name: int64\"\n  declares that the type of the $name parameter is `int64`.  If a parameter\n  has more than one valid types list them separated by \"or\":\n  \":paramtype $name: int64 or str\".\n\n* :return: and :returntype: are similar to :param: and\n  :paramtype: but lack parameter names.  They must be used to document\n  the return value of the function.\n\n* A few paragraphs and code samples.  The first paragraph must\n  be a single sentence no longer than 79 characters describing the\n  function.\n\nExample:\n\n    .. eql:function:: std::array_agg(SET OF any, $a: any) -> array<any>\n\n        :param $1: input set\n        :paramtype $1: SET OF any\n\n        :param $a: description of this param\n        :paramtype $a: int64 or str\n\n        :return: array made of input set elements\n        :returntype: array<any>\n\n        Return the array made from all of the input set elements.\n\n        The ordering of the input set will be preserved if specified.\n\n\nA function can be referenced from anywhere in the documentation by using\na \":eql:func:\" role.  For instance:\n\n* \":eql:func:`array_agg`\";\n* \":eql:func:`std::array_agg`\";\n* or, \"look at this :eql:func:`fancy function <array_agg>`\".\n\n\nOperators\n---------\n\nUse \".. eql:operator::\" directive to declare an operator.  Supported fields:\n\n* \":optype NAME: TYPE\" -- operand type.\n\nThe first argument of the directive must be a string in the following\nformat: \"OPERATOR_ID: OPERATOR SIGNATURE\".  For instance, for a \"+\"\noperator it would be \"PLUS: A + B\":\n\n    .. eql:operator:: PLUS: A + B\n\n        :optype A: int64 or str or bytes\n        :optype B: any\n        :resulttype: any\n\n        Arithmetic addition.\n\nTo reference an operator use the :eql:op: role along with OPERATOR_ID:\n\":eql:op:`plus`\" or \":eql:op:`+ <plus>`\".  Operator ID is case-insensitive.\n\n\nStatements\n----------\n\nUse \":eql-statement:\" field for sections that describe a statement.\n\nA :eql-haswith: field should be used if the statement supports a WITH block.\n\nExample:\n\n    SELECT\n    ------\n\n    :eql-statement:\n    :eql-haswith:\n\n    SELECT is used to select stuff.\n\n\n    .. eql:synopsis::\n\n        [WITH [MODULE name]]\n        SELECT expr\n        FILTER expr\n\n\n    .. eql:clause:: FILTER: A FILTER B\n\n        :paramtype A: any\n        :paramtype B: SET OF any\n        :returntype: any\n\n        FILTER should be used to filter stuff.\n\n\n    More paragraphs describing intricacies of SELECT go here...\n\n    More paragraphs describing intricacies of SELECT go here...\n\n    More paragraphs describing intricacies of SELECT go here...\n\nNotes:\n\n* To reference a statement use the \":eql:stmt:\" role.  For instance:\n\n  - :eql:stmt:`SELECT`\n  - :eql:stmt:`my fav statement <SELECT>`\n  - :eql:stmt:`select`\n  - :eql:stmt:`CREATE FUNCTION`\n  - :eql:stmt:`create function <CREATE FUNCTION>`\n\n* Synopsis section, denoted with \".. eql:synopsis::\" should follow the\n  format used in PostgreSQL documentation:\n  https://www.postgresql.org/docs/10/static/sql-select.html\n\n* An \"inline-synopsis\" role can be used as an inline highlighted code block:\n\n  - :eql:inline-synopsis:`ADD ATTRIBUTE <attribute_name>`.\n\n\nTypes\n-----\n\nTo declare a type use a \".. eql:type::\" directive.  It doesn't have any\nfields at the moment, just description.  Example:\n\n    .. eql:type:: std::bytes\n\n        A sequence of bytes.\n\nTo reference a type use a \":eql:type:\" role, e.g.:\n\n- :eql:type:`bytes`\n- :eql:type:`std::bytes`\n- :eql:type:`SET OF any`\n- :eql:type:`SET OF array\\<any\\>`\n- :eql:type:`array of \\<int64\\> <array<int64>>`\n- :eql:type:`array\\<int64\\>`\n\nKeywords\n--------\n\nTo describe a keyword use a \".. eql:keyword::\" directive.  Example:\n\n    .. eql:keyword:: WITH\n\n        The ``WITH`` block in EdgeQL is used to define aliases.\n\nIf a keyword is compound use dash to separate keywords:\n\n\n    .. eql:keyword:: SET-OF\n\nTo reference a keyword use a \":eql:kw:\" role.  For instance:\n\n* :eql:kw:`WITH block <with>`\n* :eql:kw:`SET OF <SET-OF>`\n\n\"\"\"\n\n\nfrom __future__ import annotations\n\nimport io\nimport importlib\nimport re\n\nimport lxml.etree\nimport pygments.lexers\nimport pygments.lexers.special\n\nfrom typing import Any\n\nfrom edb.common import debug\n\nfrom edb.tools.pygments.edgeql import EdgeQLLexer\n\nfrom edb import protocol\n\nfrom docutils import nodes as d_nodes\nfrom docutils.parsers import rst as d_rst\nfrom docutils.parsers.rst import directives as d_directives  # type: ignore\nfrom docutils import utils as d_utils\n\nfrom sphinx import addnodes as s_nodes\nfrom sphinx import directives as s_directives\nfrom sphinx import domains as s_domains\nfrom sphinx import roles as s_roles\nfrom sphinx import transforms as s_transforms\nfrom sphinx.util import docfields as s_docfields\nfrom sphinx.util import nodes as s_nodes_utils\nfrom sphinx.ext.intersphinx import InventoryAdapter\n\nfrom . import shared\n\n\nclass EQLField(s_docfields.Field):\n\n    def __init__(\n        self,\n        name,\n        names=(),\n        label=None,\n        has_arg=False,\n        rolename=None,\n        bodyrolename=None,\n    ):\n        super().__init__(name, names, label, has_arg, rolename, bodyrolename)\n\n    def make_field(self, *args, **kwargs):\n        node = super().make_field(*args, **kwargs)\n        node['eql-name'] = self.name\n        return node\n\n    def make_xref(\n        self,\n        rolename,\n        domain,\n        target,\n        innernode=d_nodes.emphasis,\n        contnode=None,\n        env=None,\n        inliner=None,\n        location=None,\n    ):\n\n        if not rolename:\n            return contnode or innernode(target, target)\n\n        title = target\n        if domain == 'eql' and rolename == 'type':\n            target = EQLTypeXRef.filter_target(target)\n            if target in EQLTypedField.ignored_types:\n                return d_nodes.Text(title)\n\n        refnode = s_nodes.pending_xref('', refdomain=domain,\n                                       refexplicit=title != target,\n                                       reftype=rolename, reftarget=target,\n                                       location=location)\n        refnode += contnode or innernode(title, title)\n        if env:\n            env.domains[domain].process_field_xref(refnode)\n\n        refnode['eql-auto-link'] = True\n        return refnode\n\n    def make_xrefs(\n        self,\n        rolename,\n        domain,\n        target,\n        innernode=d_nodes.emphasis,\n        contnode=None,\n        env=None,\n        inliner=None,\n        location=None,\n    ):\n        delims = r'''(?x)\n        (\n            \\s* [\\[\\]\\(\\)<>,] \\s* | \\s+or\\s+ |\n            \\s*\\bSET\\s+OF\\s+ |\n            \\s*\\bOPTIONAL\\s+\n        )\n        '''\n\n        delims_re = re.compile(delims)\n        sub_targets = re.split(delims, target)\n\n        split_contnode = bool(contnode and contnode.astext() == target)\n\n        results = []\n        for sub_target in filter(None, sub_targets):\n            if split_contnode:\n                contnode = d_nodes.Text(sub_target)\n\n            if delims_re.match(sub_target):\n                results.append(contnode or innernode(sub_target, sub_target))\n            else:\n                results.append(self.make_xref(rolename, domain, sub_target,\n                                              innernode, contnode, env,\n                                              inliner=inliner,\n                                              location=location))\n\n        return results\n\n\nINDEX_FIELD = EQLField(\n    'index',\n    label='Index Keywords',\n    names=('index',),\n    has_arg=False)\n\n\nclass EQLTypedField(EQLField):\n\n    ignored_types = {\n        'type'\n    }\n\n    def __init__(\n        self,\n        name,\n        names=(),\n        label=None,\n        rolename=None,\n        *,\n        typerolename,\n        has_arg=True,\n    ):\n        super().__init__(name, names, label, has_arg, rolename, None)\n        self.typerolename = typerolename\n\n    def make_field(\n        self, types, domain, item, env=None, inliner=None, location=None\n    ):\n        fieldarg, fieldtype = item\n\n        body = d_nodes.paragraph()\n        if fieldarg:\n            body.extend(self.make_xrefs(self.rolename, domain, fieldarg,\n                                        s_nodes.literal_strong, env=env,\n                                        inliner=inliner, location=location))\n\n            body += d_nodes.Text('--')\n\n        typename = u''.join(n.astext() for n in fieldtype)\n        body.extend(\n            self.make_xrefs(self.typerolename, domain, typename,\n                            s_nodes.literal_emphasis, env=env,\n                            inliner=inliner, location=location))\n\n        fieldname = d_nodes.field_name('', self.label)\n        fieldbody = d_nodes.field_body('', body)\n\n        node = d_nodes.field('', fieldname, fieldbody)\n        node['eql-name'] = self.name\n        node['eql-opname'] = fieldarg\n        if typename:\n            node['eql-optype'] = typename\n        return node\n\n\nclass EQLTypedParamField(EQLField):\n\n    is_typed = True\n\n    def __init__(\n        self,\n        name,\n        names=(),\n        label=None,\n        rolename=None,\n        *,\n        has_arg=True,\n        typerolename,\n        typenames,\n    ):\n        super().__init__(name, names, label, has_arg, rolename)\n        self.typenames = typenames\n        self.typerolename = typerolename\n\n    def make_field(self, types, domain, item, env=None, inliner=None):\n        fieldname = d_nodes.field_name('', self.label)\n\n        fieldarg, content = item\n        body = d_nodes.paragraph()\n        body.extend(self.make_xrefs(self.rolename, domain, fieldarg,\n                                    s_nodes.literal_strong, env=env,\n                                    inliner=inliner))\n\n        typename = None\n        if fieldarg in types:\n            body += d_nodes.Text(' (')\n\n            # NOTE: using .pop() here to prevent a single type node to be\n            # inserted twice into the doctree, which leads to\n            # inconsistencies later when references are resolved\n            fieldtype = types.pop(fieldarg)\n            if len(fieldtype) == 1 and isinstance(fieldtype[0], d_nodes.Text):\n                typename = u''.join(n.astext() for n in fieldtype)\n                body.extend(\n                    self.make_xrefs(self.typerolename, domain, typename,\n                                    s_nodes.literal_emphasis, env=env,\n                                    inliner=inliner))\n            else:\n                body += fieldtype\n            body += d_nodes.Text(')')\n\n        body += d_nodes.Text(' -- ')\n        body += content\n\n        fieldbody = d_nodes.field_body('', body)\n\n        node = d_nodes.field('', fieldname, fieldbody)\n        node['eql-name'] = self.name\n        node['eql-paramname'] = fieldarg\n        if typename:\n            node['eql-paramtype'] = typename\n        return node\n\n\nclass BaseEQLDirective(s_directives.ObjectDescription):\n\n    @staticmethod\n    def strip_ws(text):\n        text = text.strip()\n        text = ' '.join(\n            line.strip() for line in text.split() if line.strip())\n        return text\n\n    def _validate_and_extract_summary(self, node):\n        desc_cnt = None\n        for child in node.children:\n            if isinstance(child, s_nodes.desc_content):\n                desc_cnt = child\n                break\n        if desc_cnt is None or not desc_cnt.children:\n            raise self.error('the directive must include a description')\n\n        first_node_index = 0\n        first_node = desc_cnt.children[first_node_index]\n\n        if isinstance(first_node, d_nodes.field_list):\n            if len(desc_cnt.children) < 2:\n                raise self.error('the directive must include a description')\n\n            first_node_index += 1\n            first_node = desc_cnt.children[first_node_index]\n\n        while (\n            isinstance(first_node, s_nodes.versionmodified)\n            or isinstance(first_node, s_nodes.index)\n            or isinstance(first_node, d_nodes.target)\n        ):\n            first_node_index += 1\n            first_node = desc_cnt.children[first_node_index]\n\n        if not isinstance(first_node, d_nodes.paragraph):\n            raise self.error(\n                'there must be a short text paragraph after directive fields')\n\n        summary = self.strip_ws(first_node.astext())\n        if len(summary) > 79:\n            raise self.error(\n                f'First paragraph is expected to be shorter than 80 '\n                f'characters, got {len(summary)}: {summary!r}')\n\n        node['summary'] = summary\n\n    def _find_field_desc(self, field_node: d_nodes.field):\n        fieldname = field_node.children[0].astext()\n\n        if ' ' in fieldname:\n            fieldtype, fieldarg = fieldname.split(' ', 1)\n            fieldarg = fieldarg.strip()\n            if not fieldarg:\n                fieldarg = None\n        else:\n            fieldtype = fieldname\n            fieldarg = None\n\n        fieldtype = fieldtype.lower().strip()\n\n        for fielddesc in self.doc_field_types:\n            if fielddesc.name == fieldtype:\n                return fieldtype, fielddesc, fieldarg\n\n        return fieldtype, None, fieldarg\n\n    def _validate_fields(self, node):\n        desc_cnt = None\n        for child in node.children:\n            if isinstance(child, s_nodes.desc_content):\n                desc_cnt = child\n                break\n        if desc_cnt is None or not desc_cnt.children:\n            raise self.error('the directive must include a description')\n\n        fields = None\n        first_node = desc_cnt.children[0]\n        if isinstance(first_node, d_nodes.field_list):\n            fields = first_node\n\n        for child in desc_cnt.children[1:]:\n            if isinstance(child, d_nodes.field_list):\n                raise self.error(\n                    f'fields must be specified before all other content')\n\n        if fields:\n            for field in fields:\n                if 'eql-name' in field:\n                    continue\n\n                # Since there is *no* validation or sane error reporting\n                # in Sphinx, attempt to do it here.\n\n                fname, fdesc, farg = self._find_field_desc(field)\n                msg = f'found unknown field {fname!r}'\n\n                if fdesc is None:\n                    msg += (\n                        f'\\n\\nPossible reason: field {fname!r} '\n                        f'is not supported by the directive; '\n                        f'is there a typo?\\n\\n'\n                    )\n                else:\n                    if farg and not fdesc.has_arg:\n                        msg += (\n                            f'\\n\\nPossible reason: field {fname!r} '\n                            f'is specified with an argument {farg!r}, but '\n                            f'the directive expects it without one.\\n\\n'\n                        )\n                    elif not farg and fdesc.has_arg:\n                        msg += (\n                            f'\\n\\nPossible reason: field {fname!r} '\n                            f'expects an argument but did not receive it;'\n                            f'check your ReST source.\\n\\n'\n                        )\n\n                raise self.error(msg)\n\n    def run(self):\n        indexnode, node = super().run()\n        self._validate_fields(node)\n        self._validate_and_extract_summary(node)\n\n        objects = self.env.domaindata['eql']['objects']\n        objects[self.__eql_target] += (node['summary'],)\n\n        return [indexnode, node]\n\n    def add_target_and_index(self, name, sig, signode):\n        target = name.replace(' ', '-')\n\n        if target in self.state.document.ids:\n            raise self.error(\n                f'duplicate {self.objtype} {name} description')\n\n        signode['names'].append(target)\n        signode['ids'].append(target)\n        signode['first'] = (not self.names)\n        self.state.document.note_explicit_target(signode)\n\n        objects = self.env.domaindata['eql']['objects']\n\n        if target in objects:\n            raise self.error(\n                f'duplicate {self.objtype} {name} description')\n        objects[target] = (self.env.docname, self.objtype)\n\n        self.__eql_target = target\n\n\nclass EQLTypeDirective(BaseEQLDirective):\n\n    doc_field_types = [\n        INDEX_FIELD,\n    ]\n\n    def handle_signature(self, sig, signode):\n        if '::' in sig:\n            mod, name = sig.strip().rsplit('::', 1)\n        else:\n            name = sig.strip()\n            mod = 'std'\n\n        display = name.replace('-', ' ')\n        if mod != 'std':\n            display = f'{mod}::{display}'\n\n        signode['eql-module'] = mod\n        signode['eql-name'] = name\n        signode['eql-fullname'] = fullname = f'{mod}::{name}'\n\n        signode += s_nodes.desc_annotation('type', 'type')\n        signode += d_nodes.Text(' ')\n        signode += s_nodes.desc_name(display, display)\n        return fullname\n\n    def add_target_and_index(self, name, sig, signode):\n        return super().add_target_and_index(\n            f'type::{name}', sig, signode)\n\n\nclass EQLKeywordDirective(BaseEQLDirective):\n\n    def handle_signature(self, sig, signode):\n        signode['eql-name'] = sig\n        signode['eql-fullname'] = sig\n\n        display = sig.replace('-', ' ')\n        signode += s_nodes.desc_annotation('keyword', 'keyword')\n        signode += d_nodes.Text(' ')\n        signode += s_nodes.desc_name(display, display)\n\n        return sig\n\n    def add_target_and_index(self, name, sig, signode):\n        return super().add_target_and_index(\n            f'keyword::{name}', sig, signode)\n\n\nclass EQLSynopsisDirective(shared.CodeBlock):\n\n    has_content = True\n    optional_arguments = 0\n    required_arguments = 0\n    option_spec: dict[str, Any] = {\n        'version-lt': d_directives.unchanged_required\n    }\n\n    def run(self):\n        self.arguments = ['edgeql-synopsis']\n        return super().run()\n\n\nclass EQLReactElement(d_rst.Directive):\n\n    has_content = False\n    optional_arguments = 0\n    required_arguments = 1\n\n    def run(self):\n        node = d_nodes.container()\n        node['react-element'] = self.arguments[0]\n        return [node]\n\n\nclass EQLSectionIntroPage(d_rst.Directive):\n\n    has_content = False\n    optional_arguments = 0\n    required_arguments = 1\n\n    def run(self):\n        node = d_nodes.container()\n        node['section-intro-page'] = self.arguments[0]\n        return [node]\n\n\nclass EQLStructElement(d_rst.Directive):\n\n    has_content = False\n    optional_arguments = 0\n    required_arguments = 1\n\n    def run(self):\n        fullname = self.arguments[0]\n        modname, _, name = fullname.rpartition('.')\n        mod = importlib.import_module(modname)\n        cls = getattr(mod, name)\n        try:\n            code = protocol.render(cls)\n        except Exception:\n            raise RuntimeError(f'could not render {fullname} struct')\n        node = d_nodes.literal_block(code, code)\n        node['language'] = 'c'\n        return [node]\n\n\nclass EQLOperatorDirective(BaseEQLDirective):\n\n    doc_field_types = [\n        INDEX_FIELD,\n\n        EQLTypedField(\n            'operand',\n            label='Operand',\n            names=('optype',),\n            typerolename='type'),\n\n        EQLTypedField(\n            'resulttype',\n            label='Result',\n            has_arg=False,\n            names=('resulttype',),\n            typerolename='type'),\n    ]\n\n    def handle_signature(self, sig, signode):\n        if self.names:\n            name = self.names[0]\n        else:\n            try:\n                name, sig = sig.split(':', 1)\n            except Exception as ex:\n                raise self.error(\n                    f':eql:operator signature must match \"NAME: SIGNATURE\" '\n                    f'template'\n                ) from ex\n            name = name.strip()\n\n        sig = sig.strip()\n        if not name or not sig:\n            raise self.error(f'invalid :eql:operator: signature')\n\n        signode['eql-name'] = name\n        signode['eql-fullname'] = name\n        signode['eql-signature'] = sig\n\n        signode += s_nodes.desc_annotation('operator', 'operator')\n        signode += d_nodes.Text(' ')\n        signode += s_nodes.desc_name(sig, sig)\n\n        return name\n\n    def add_target_and_index(self, name, sig, signode):\n        return super().add_target_and_index(\n            f'operator::{name}', sig, signode)\n\n\nclass EQLFunctionDirective(BaseEQLDirective):\n\n    doc_field_types = [\n        INDEX_FIELD,\n    ]\n\n    def handle_signature(self, sig, signode):\n        if debug.flags.disable_docs_edgeql_validation:\n            signode['eql-fullname'] = fullname = sig.split('(')[0]\n            signode['eql-signature'] = sig\n            mod, name = fullname.rsplit('::', 1)\n            signode['eql-module'] = mod\n            signode['eql-name'] = name\n\n            return fullname\n\n        from edb.edgeql import parser as edgeql_parser\n        from edb.edgeql import ast as ql_ast\n        from edb.edgeql import codegen as ql_gen\n        from edb.edgeql import qltypes\n\n        try:\n            astnode = edgeql_parser.parse_block(\n                f'create function {sig} using SQL function \"xxx\";'\n            )[0]\n        except Exception as ex:\n            raise self.error(\n                f'could not parse function signature {sig!r}: '\n                f'{ex.__class__.__name__}({ex.args[0]!r})'\n            ) from ex\n\n        if (not isinstance(astnode, ql_ast.CreateFunction) or\n                not isinstance(astnode.name, ql_ast.ObjectRef)):\n            raise self.error(f'EdgeQL parser returned unsupported AST')\n\n        modname = astnode.name.module\n        funcname = astnode.name.name\n        if not modname:\n            raise self.error(\n                f'EdgeQL function declaration is missing namespace')\n\n        func_repr = ql_gen.generate_source(astnode)\n        m = re.match(r'''(?xs)\n            ^\n            create\\sfunction\\s\n            (?P<f>.*?)\n            \\susing\\ssql\\sfunction\n            .*$\n        ''', func_repr)\n        if not m or not m.group('f'):\n            raise self.error(f'could not recreate function signature from AST')\n        func_repr = m.group('f')\n\n        signode['eql-module'] = modname\n        signode['eql-name'] = funcname\n        signode['eql-fullname'] = fullname = f'{modname}::{funcname}'\n        signode['eql-signature'] = func_repr\n\n        signode += s_nodes.desc_annotation('function', 'function')\n        signode += d_nodes.Text(' ')\n        signode += s_nodes.desc_name(fullname, fullname)\n\n        ret_repr = ql_gen.generate_source(astnode.returning)\n        if astnode.returning_typemod is qltypes.TypeModifier.SetOfType:\n            ret_repr = f'set of {ret_repr}'\n        elif astnode.returning_typemod is qltypes.TypeModifier.OptionalType:\n            ret_repr = f'optional {ret_repr}'\n        signode += s_nodes.desc_returns(ret_repr, ret_repr)\n\n        return fullname\n\n    def add_target_and_index(self, name, sig, signode):\n        return super().add_target_and_index(\n            f'function::{name}', sig, signode)\n\n\nclass EQLConstraintDirective(BaseEQLDirective):\n\n    doc_field_types = [\n        INDEX_FIELD,\n    ]\n\n    def handle_signature(self, sig, signode):\n        if debug.flags.disable_docs_edgeql_validation:\n            signode['eql-fullname'] = fullname = re.split(r'\\(| ', sig)[0]\n            signode['eql-signature'] = sig\n            mod, name = fullname.split('::')\n            signode['eql-module'] = mod\n            signode['eql-name'] = name\n\n            return fullname\n\n        from edb.edgeql import parser as edgeql_parser\n        from edb.edgeql import ast as ql_ast\n        from edb.edgeql import codegen as ql_gen\n\n        try:\n            astnode = edgeql_parser.parse_block(\n                f'create abstract constraint {sig};'\n            )[0]\n        except Exception as ex:\n            raise self.error(\n                f'could not parse constraint signature {sig!r}') from ex\n\n        if (not isinstance(astnode, ql_ast.CreateConstraint) or\n                not isinstance(astnode.name, ql_ast.ObjectRef)):\n            raise self.error(f'EdgeQL parser returned unsupported AST')\n\n        modname = astnode.name.module\n        constr_name = astnode.name.name\n        if not modname:\n            raise self.error(\n                f'Missing module in EdgeQL constraint declaration')\n\n        constr_repr = ql_gen.generate_source(astnode)\n\n        m = re.match(r'''(?xs)\n            ^\n            create\\sabstract\\sconstraint\\s\n            (?P<f>.*?)(?:\\s*on(?P<subj>.*))?\n            $\n        ''', constr_repr)\n        if not m or not m.group('f'):\n            raise self.error(\n                f'could not recreate constraint signature from AST')\n        constr_repr = m.group('f')\n\n        signode['eql-module'] = modname\n        signode['eql-name'] = constr_name\n        signode['eql-fullname'] = fullname = f'{modname}::{constr_name}'\n        signode['eql-signature'] = constr_repr\n        subject = m.group('subj')\n        if subject:\n            subject = subject.strip()[1:-1]\n            signode['eql-subjexpr'] = subject\n            signode['eql-signature'] += f' on ({subject})'\n\n        signode += s_nodes.desc_annotation('constraint', 'constraint')\n        signode += d_nodes.Text(' ')\n        signode += s_nodes.desc_name(fullname, fullname)\n\n        return fullname\n\n    def add_target_and_index(self, name, sig, signode):\n        return super().add_target_and_index(\n            f'constraint::{name}', sig, signode)\n\n\nclass EQLPermissionDirective(BaseEQLDirective):\n\n    doc_field_types = [\n        INDEX_FIELD,\n    ]\n\n    def handle_signature(self, sig, signode):\n        if '::' in sig:\n            mod, name = sig.strip().rsplit('::', 1)\n        else:\n            name = sig.strip()\n            mod = 'std'\n\n        display = name.replace('-', ' ')\n        if mod != 'std':\n            display = f'{mod}::{display}'\n\n        signode['eql-module'] = mod\n        signode['eql-name'] = name\n        signode['eql-fullname'] = fullname = f'{mod}::{name}'\n\n        signode += s_nodes.desc_annotation('permission', 'permission')\n        signode += d_nodes.Text(' ')\n        signode += s_nodes.desc_name(display, display)\n        return fullname\n\n    def add_target_and_index(self, name, sig, signode):\n        return super().add_target_and_index(\n            f'permission::{name}', sig, signode)\n\n\nclass EQLTypeXRef(s_roles.XRefRole):\n\n    @staticmethod\n    def filter_target(target):\n        new_target = re.sub(r'''(?xi)\n            ^ \\s*\\bSET\\s+OF\\s+ | \\s*\\bOPTIONAL\\s+\n        ''', '', target)\n\n        if '<' in new_target:\n            new_target, _ = new_target.split('<', 1)\n\n        return new_target\n\n    def process_link(self, env, refnode, has_explicit_title, title, target):\n        new_target = self.filter_target(target)\n        if not has_explicit_title:\n            title = target.replace('-', ' ')\n        return super().process_link(\n            env, refnode, has_explicit_title, title, new_target)\n\n\nclass EQLFunctionXRef(s_roles.XRefRole):\n\n    def process_link(self, env, refnode, has_explicit_title, title, target):\n        if not has_explicit_title:\n            title += '()'\n        return super().process_link(\n            env, refnode, has_explicit_title, title, target)\n\n\nclass EQLFunctionDescXRef(s_roles.XRefRole):\n    pass\n\n\nclass EQLOperatorDescXRef(s_roles.XRefRole):\n    pass\n\n\nclass EQLConstraintXRef(s_roles.XRefRole):\n    pass\n\n\nclass EQLPermissionXRef(s_roles.XRefRole):\n    pass\n\n\nclass GitHubLinkRole:\n\n    DEFAULT_REPO = 'edgedb/edgedb'\n    BASE_URL = 'https://github.com/'\n\n    # \\x00 means the \"<\" was backslash-escaped\n    explicit_title_re = re.compile(r'^(.+?)\\s*(?<!\\x00)<(.*?)>$', re.DOTALL)\n\n    link_re = re.compile(\n        r'''\n            (?:\n                (?P<repo>(?:[\\w\\d\\-_]+)/(?:[\\w\\d\\-_]+))\n                /\n            )?\n            (?:\n                (?:\\#(?P<issue>\\d+))\n                |\n                (?P<commit>[A-Fa-f\\d]{8,40})\n            )\n        ''',\n        re.X)\n\n    def __call__(\n        self, role, rawtext, text, lineno, inliner, options=None, content=None\n    ):\n        if options is None:\n            options = {}\n        if content is None:\n            content = []\n\n        matched = self.explicit_title_re.match(text)\n        if matched:\n            has_explicit_title = True\n            title = d_utils.unescape(matched.group(1))\n            target = d_utils.unescape(matched.group(2))\n        else:\n            has_explicit_title = False\n            title = d_utils.unescape(text)\n            target = d_utils.unescape(text)\n\n        matched = self.link_re.match(target)\n        if not matched:\n            raise shared.EdgeSphinxExtensionError(f'cannot parse {rawtext}')\n\n        repo = matched.group('repo')\n        explicit_repo = True\n        if not repo:\n            repo = self.DEFAULT_REPO\n            explicit_repo = False\n\n        issue = matched.group('issue')\n        commit = matched.group('commit')\n        if issue:\n            postfix = f'issues/{issue}'\n        elif commit:\n            postfix = f'commit/{commit}'\n            if not has_explicit_title:\n                if explicit_repo:\n                    title = f'{repo}/{commit[:8]}'\n                else:\n                    title = f'{commit[:8]}'\n        else:\n            raise shared.EdgeSphinxExtensionError(f'cannot parse {rawtext}')\n\n        url = f'{self.BASE_URL}{repo}/{postfix}'\n\n        node = d_nodes.reference(refuri=url, name=title)\n        node['eql-github'] = True\n        node += d_nodes.Text(title)\n        return [node], []\n\n\nclass EdgeQLDomain(s_domains.Domain):\n\n    name = \"eql\"\n    label = \"EdgeQL\"\n\n    object_types = {\n        'function': s_domains.ObjType('function', 'func', 'func-desc'),\n        'constraint': s_domains.ObjType('constraint', 'constraint'),\n        'type': s_domains.ObjType('type', 'type'),\n        'keyword': s_domains.ObjType('keyword', 'kw'),\n        'operator': s_domains.ObjType('operator', 'op', 'op-desc'),\n        'statement': s_domains.ObjType('statement', 'stmt'),\n        'permission': s_domains.ObjType('permission', 'permission'),\n    }\n\n    _role_to_object_type = {\n        role: tn\n        for tn, td in object_types.items() for role in td.roles\n    }\n\n    directives = {\n        'function': EQLFunctionDirective,\n        'constraint': EQLConstraintDirective,\n        'type': EQLTypeDirective,\n        'keyword': EQLKeywordDirective,\n        'operator': EQLOperatorDirective,\n        'permission': EQLPermissionDirective,\n        'synopsis': EQLSynopsisDirective,\n        'struct': EQLStructElement,\n\n        # TODO: Move to edb domain\n        'react-element': EQLReactElement,\n        'section-intro-page': EQLSectionIntroPage,\n    }\n\n    roles = {\n        'func': EQLFunctionXRef(),\n        'func-desc': EQLFunctionDescXRef(),\n        'constraint': EQLConstraintXRef(),\n        'type': EQLTypeXRef(),\n        'kw': s_roles.XRefRole(),\n        'op': s_roles.XRefRole(),\n        'op-desc': EQLOperatorDescXRef(),\n        'permission': EQLPermissionXRef(),\n        'stmt': s_roles.XRefRole(),\n\n        # TODO: Move to edb domain\n        'gh': GitHubLinkRole(),\n    }\n\n    desc_roles = {\n        'func-desc',\n        'op-desc',\n    }\n\n    initial_data: dict[str, dict[str, Any]] = {\n        'objects': {}  # fullname -> docname, objtype, description\n    }\n\n    def resolve_xref(\n        self, env, fromdocname, builder, type, target, node, contnode\n    ):\n\n        objects = self.data['objects']\n        expected_type = self._role_to_object_type[type]\n\n        target = target.replace(' ', '-')\n        if expected_type == 'keyword':\n            targets = [f'keyword::{target}']\n        elif expected_type == 'operator':\n            targets = [f'operator::{target}']\n        elif expected_type == 'statement':\n            targets = [f'statement::{target}']\n        elif expected_type in {'type', 'function', 'constraint', 'permission'}:\n            targets = [f'{expected_type}::{target}']\n            if '::' not in target:\n                targets.append(f'{expected_type}::std::{target}')\n        else:\n            targets = [target]\n\n        docname = None\n        obj_type = None\n        obj_desc = None\n        for target in targets:\n            try:\n                docname, obj_type, obj_desc = objects[target]\n            except KeyError:\n                continue\n\n        if docname is None:\n            if not node.get('eql-auto-link'):\n                # if ref was not found, the :eql: xref may be being used\n                # outside of the docs, so try resolving ref from intersphinx\n                # inventories\n                inventories = InventoryAdapter(env)\n\n                for target in targets:\n                    obj_type, name = target.split('::', 1)\n                    if ':' not in name:\n                        continue\n                    docset_name, name = name.split(':', 1)\n\n                    docset = inventories.named_inventory.get(docset_name)\n                    if docset is None:\n                        continue\n                    refs = docset.get('eql:' + obj_type)\n                    if refs is None:\n                        continue\n                    ref = refs.get(obj_type + '::' + name)\n                    if ref is None:\n                        continue\n\n                    newnode = d_nodes.reference(\n                        '', '',\n                        internal=False, refuri=ref[2],\n                    )\n                    if node.get('refexplicit'):\n                        newnode.append(d_nodes.Text(contnode.astext()))\n                    else:\n                        title = contnode.astext()\n                        newnode.append(\n                            contnode.__class__(\n                                title[len(docset_name) + 1:],\n                                title[len(docset_name) + 1:]\n                            )\n                        )\n                    return newnode\n\n                raise shared.DomainError(\n                    f'cannot resolve :eql:{type}: targeting {target!r}')\n            else:\n                return\n\n        if obj_type != expected_type:\n            raise shared.DomainError(\n                f'cannot resolve :eql:{type}: targeting {target!r}: '\n                f'the type of referred object {expected_type!r} '\n                f'does not match the reftype')\n\n        if node['reftype'] in self.desc_roles:\n            node = d_nodes.Text(obj_desc)\n        else:\n            node = s_nodes_utils.make_refnode(\n                builder, fromdocname, docname, target, contnode, None)\n            node['eql-type'] = obj_type\n\n        return node\n\n    def resolve_any_xref(\n        self, env, fromdocname, builder, target, node, contnode\n    ):\n        # 'myst-parser' resolves all markdown links as :any: xrefs, so return\n        # empty list to prevent sphinx trying to resolve these as :eql: refs\n        return []\n\n    def clear_doc(self, docname):\n        for fullname, (fn, _l, _d) in list(self.data['objects'].items()):\n            if fn == docname:\n                del self.data['objects'][fullname]\n\n    def merge_domaindata(self, docnames, otherdata):\n        for fullname, (fn, objtype, desc) in otherdata['objects'].items():\n            if fn in docnames:\n                self.data['objects'][fullname] = (fn, objtype, desc)\n\n    def get_objects(self):\n        for refname, (docname, type, _desc) in self.data['objects'].items():\n            yield (refname, refname, type, docname, refname, 1)\n\n    def get_full_qualified_name(self, node):\n        fn = node.get('eql-fullname')\n        if not fn:\n            raise self.error('no eql-fullname attribute')\n        return fn\n\n\nclass StatementTransform(s_transforms.SphinxTransform):\n\n    default_priority = 5  # before ReferencesResolver\n\n    def apply(self):\n        for section in self.document.traverse(d_nodes.section):\n            xml_str = section.asdom().toxml(encoding=\"UTF-8\")\n            parser = lxml.etree.XMLParser(recover=True, encoding=\"UTF-8\")\n            x = lxml.etree.parse(io.BytesIO(xml_str), parser)\n\n            fields = set(x.xpath('field_list/field/field_name/text()'))\n            title = x.xpath('title/text()')[0]\n\n            page_title = None\n            if 'edb-alt-title' in fields:\n                page_titles = x.xpath(\n                    '''//field_list/field/field_name[text()=\"edb-alt-title\"]\n                        /parent::field/field_body/paragraph/text()\n                    ''')\n                if page_titles:\n                    page_title = page_titles[0]\n\n            if page_title:\n                if (not section.children or\n                        not isinstance(section.children[0], d_nodes.title)):\n                    raise shared.EdgeSphinxExtensionError(\n                        f'cannot apply :edb-alt-title: field to the {title!r} '\n                        f'section')\n\n                section.children[0]['edb-alt-title'] = page_title\n\n            if 'eql-statement' not in fields:\n                continue\n\n            nested_statements = x.xpath(\n                '//field_list/field/field_name[text()=\"eql-statement\"]')\n            if len(nested_statements) > 1:\n                raise shared.EdgeSphinxExtensionError(\n                    f'section {title!r} has a nested section with '\n                    f'a :eql-statement: field set')\n\n            first_para = x.xpath('paragraph[1]/descendant-or-self::*/text()')\n            if not len(first_para):\n                raise shared.EdgeSphinxExtensionError(\n                    f'section {title!r} is marked with an :eql-statement: '\n                    f'and is required to have at least one paragraph')\n            first_para = ''.join(first_para)\n            summary = BaseEQLDirective.strip_ws(first_para)\n            if len(summary) > 79:\n                raise shared.EdgeSphinxExtensionError(\n                    f'section {title!r} is marked with an :eql-statement: '\n                    f'and its first paragraph is longer than 79 characters')\n\n            section['eql-statement'] = 'true'\n            section['eql-haswith'] = ('true' if 'eql-haswith' in fields\n                                      else 'false')\n            section['summary'] = summary\n\n            objects = self.env.domaindata['eql']['objects']\n            # Make it so that the statement can be referenced by the\n            # lower-case version by default.\n            target = 'statement::' + title.lower().replace(' ', '-')\n\n            if target in objects:\n                raise shared.EdgeSphinxExtensionError(\n                    f'duplicate {title!r} statement')\n\n            objects[target] = (self.env.docname, 'statement', summary)\n\n            section['ids'].append(target)\n\n\ndef setup_domain(app):\n    # Dummy lexers; the actual highlighting is implemented\n    # in the edgedb.com website code.\n    app.add_lexer(\"sdl-diff\", pygments.lexers.TextLexer)\n    app.add_lexer(\"edgeql-diff\", pygments.lexers.TextLexer)\n\n    app.add_lexer(\"edgeql\", EdgeQLLexer)\n    app.add_lexer(\"edgeql-repl\", EdgeQLLexer)\n    app.add_lexer(\"edgeql-runnable\", EdgeQLLexer)\n    app.add_lexer(\"edgeql-synopsis\", EdgeQLLexer)\n    app.add_lexer(\"edgeql-result\", pygments.lexers.special.TextLexer)\n\n    app.add_role(\n        'eql:synopsis',\n        shared.InlineCodeRole('edgeql-synopsis'))\n\n    app.add_role(\n        'eql:code',\n        shared.InlineCodeRole('edgeql'))\n\n    app.add_domain(EdgeQLDomain)\n\n    app.add_transform(StatementTransform)\n\n\ndef setup(app):\n    setup_domain(app)\n"
  },
  {
    "path": "edb/tools/docs/go.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport re\n\nfrom typing import Any\n\nfrom sphinx import addnodes as s_nodes\nfrom sphinx import directives as s_directives\nfrom sphinx import domains as s_domains\n\n\nclass BaseGoDirective(s_directives.ObjectDescription):\n\n    def get_signatures(self):\n        return [re.compile(r'\\\\\\s*\\n').sub('\\n', self.arguments[0])]\n\n    def add_target_and_index(self, name, sig, signode):\n        target = name.replace(' ', '-')\n\n        if target in self.state.document.ids:\n            raise self.error(\n                f'duplicate {self.objtype} {name} description')\n\n        signode['names'].append(target)\n        signode['ids'].append(target)\n        self.state.document.note_explicit_target(signode)\n\n        objects = self.env.domaindata['go']['objects']\n\n        if target in objects:\n            raise self.error(\n                f'duplicate {self.objtype} {name} description')\n        objects[target] = (self.env.docname, self.objtype)\n\n\nclass GoTypeDirective(BaseGoDirective):\n\n    def handle_signature(self, sig, signode):\n        name = re.split(r'\\s+', sig)[1].strip()\n\n        signode['name'] = name\n        signode['fullname'] = name\n\n        signode['is_multiline'] = True\n        signode += [\n            s_nodes.desc_signature_line(sig, line)\n            for line in sig.split('\\n')\n          ]\n\n        return name\n\n    def add_target_and_index(self, name, sig, signode):\n        return super().add_target_and_index(name, sig, signode)\n\n\ngoFuncRegex = re.compile(\n    r\"func\\s+(?:\\(.+?\\s+\\*?(?P<receiver>.+?)\\)\\s+)?(?P<name>.+?)\\s*\\(\")\n\n\nclass GoFunctionDirective(BaseGoDirective):\n\n    def handle_signature(self, sig, signode):\n        match = goFuncRegex.match(sig)\n        if match is None:\n            raise self.error(f'could not parse go func signature: {sig!r}')\n\n        signode['fullname'] = fullname = (\n            f\"{match.group('receiver')}.{match.group('name')}\"\n            if match.group('receiver')\n            else match.group('name')\n        )\n        signode['name'] = match.group('name')\n\n        signode['is_multiline'] = True\n        signode += [\n            s_nodes.desc_signature_line(sig, line)\n            for line in sig.split('\\n')\n          ]\n\n        return fullname\n\n    def add_target_and_index(self, name, sig, signode):\n        return super().add_target_and_index(name, sig, signode)\n\n\nclass GoMethodDirective(GoFunctionDirective):\n    pass\n\n\nclass GolangDomain(s_domains.Domain):\n\n    name = \"go\"\n    label = \"Golang\"\n\n    object_types = {\n        'function': s_domains.ObjType('function'),\n        'type': s_domains.ObjType('type'),\n    }\n\n    directives = {\n        'function': GoFunctionDirective,\n        'type': GoTypeDirective,\n        'method': GoMethodDirective,\n    }\n\n    initial_data: dict[str, dict[str, Any]] = {\n        'objects': {}  # fullname -> docname, objtype\n    }\n\n    def clear_doc(self, docname):\n        for fullname, (fn, _l) in list(self.data['objects'].items()):\n            if fn == docname:\n                del self.data['objects'][fullname]\n\n    def merge_domaindata(self, docnames, otherdata):\n        for fullname, (fn, objtype) in otherdata['objects'].items():\n            if fn in docnames:\n                self.data['objects'][fullname] = (fn, objtype)\n\n    def get_objects(self):\n        for refname, (docname, type) in self.data['objects'].items():\n            yield (refname, refname, type, docname, refname, 1)\n\n    def get_full_qualified_name(self, node):\n        fn = node.get('fullname')\n        if not fn:\n            raise self.error('no fullname attribute')\n        return fn\n\n\ndef setup_domain(app):\n    app.add_domain(GolangDomain)\n\n\ndef setup(app):\n    setup_domain(app)\n"
  },
  {
    "path": "edb/tools/docs/graphql.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom edb.tools.pygments.graphql import GraphQLLexer\n\n\ndef setup_domain(app):\n    app.add_lexer(\"graphql\", GraphQLLexer)\n    app.add_lexer(\"graphql-schema\", GraphQLLexer)\n"
  },
  {
    "path": "edb/tools/docs/js.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nr\"\"\"\n===========================================\n:js: domain for Gel driver documentation\n===========================================\n\nThis module extends the default js domain by overriding class, method\nand function directives in the following ways:\n\n* The main change is that TypeScript-like specifications are now\n  supported in signatures, allowing a better way to specify what the\n  expected arguments and return types are supposed to be. The types\n  are optional.\n\n* A :staticmethod: option is added to the methods.\n\n* Class signatures support `extends` clause.\n\n* The :param: option supports type specification without trying to\n  link to the types (since mostly they are native JS types that don't\n  necessarily have a meaningful link).\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom docutils import nodes as d_nodes\nfrom docutils.parsers.rst import directives  # type: ignore\n\nimport pygments.lexers\n\nfrom sphinx import addnodes as s_nodes\nfrom sphinx.domains import javascript as js\nfrom sphinx.locale import _\nfrom sphinx.util import docfields\n\n\nclass JSFieldMixin:\n    def make_xref(self, rolename, domain, target, *args, **kwargs):\n        if rolename:\n            return d_nodes.literal(target, target)\n        return super().make_xref(rolename, domain, target, *args, **kwargs)\n\n\nclass JSTypedField(JSFieldMixin, docfields.TypedField):\n    pass\n\n\nclass JSCallableDirective(js.JSCallable):\n    doc_field_types = [  # type: ignore\n        JSTypedField('arguments', label=_('Arguments'),\n                     names=('argument', 'arg', 'parameter', 'param'),\n                     typerolename='func', typenames=('paramtype', 'type')),\n    ] + js.JSCallable.doc_field_types[1:]   # type: ignore\n\n    def handle_signature(self, sig, signode):\n        # if the function has a return type specified, clip it before\n        # processing the rest of signature\n        if sig[-1] != ')' and '):' in sig:\n            newsig, rettype = sig.rsplit(':', 1)\n            rettype = rettype.strip()\n        else:\n            newsig = sig\n            rettype = None\n\n        fullname, prefix = super().handle_signature(newsig, signode)\n\n        if rettype:\n            signode += s_nodes.desc_returns(rettype, rettype)\n\n        return fullname, prefix\n\n\nclass JSMethodDirective(JSCallableDirective):\n    option_spec = {\n        **js.JSCallable.option_spec,\n        **{'staticmethod': directives.flag},\n    }\n\n    def handle_signature(self, sig, signode):\n        fullname, prefix = super().handle_signature(sig, signode)\n\n        if 'staticmethod' in self.options:\n            signode.insert(\n                0, s_nodes.desc_annotation('static method', 'static method'))\n\n        return fullname, prefix\n\n\nclass JSClassDirective(JSCallableDirective):\n    \"\"\"Like a callable but with an optional \"extends\" clause.\"\"\"\n    display_prefix = 'class '\n    allow_nesting = True\n\n    def handle_signature(self, sig, signode):\n        # if the class has \"extends\" clause specified, clip it before\n        # processing the rest of signature\n        if ' extends ' in sig:\n            newsig, mro = sig.rsplit(' extends ', 1)\n            mro = mro.strip()\n            newsig = newsig.strip()\n        else:\n            newsig = sig\n            mro = None\n\n        fullname, prefix = super().handle_signature(newsig, signode)\n\n        if mro:\n            signode['mro'] = mro\n            mronode = s_nodes.desc_type('extends', '')\n            signode += mronode\n            for itype in mro.split(','):\n                itype = itype.strip()\n                mronode += s_nodes.desc_type(itype, itype)\n\n        return fullname, prefix\n\n\nclass JSDomain(js.JavaScriptDomain):\n    directives = {\n        **js.JavaScriptDomain.directives,\n        **{\n            'function': JSCallableDirective,\n            'method': JSMethodDirective,\n            'class': JSClassDirective,\n        }\n    }\n\n\ndef setup_domain(app):\n    # Dummy lexers; the actual highlighting is implemented\n    # in the edgedb.com website code.\n    app.add_lexer(\"tsx\", pygments.lexers.TextLexer)\n    app.add_lexer(\"tsx-diff\", pygments.lexers.TextLexer)\n    app.add_lexer(\"typescript-diff\", pygments.lexers.TextLexer)\n    app.add_lexer(\"javascript-diff\", pygments.lexers.TextLexer)\n\n    app.add_domain(JSDomain, override=True)\n"
  },
  {
    "path": "edb/tools/docs/sdl.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom edb.tools.pygments.edgeql import EdgeQLLexer\n\nfrom sphinx import domains as s_domains\nfrom docutils.parsers.rst import directives as d_directives  # type: ignore\n\nfrom . import shared\n\n\nclass SDLSynopsisDirective(shared.CodeBlock):\n\n    has_content = True\n    optional_arguments = 0\n    required_arguments = 0\n    option_spec: dict[str, Any] = {\n        'version-lt': d_directives.unchanged_required\n    }\n\n    def run(self):\n        self.arguments = ['sdl-synopsis']\n        return super().run()\n\n\nclass SDLDomain(s_domains.Domain):\n\n    name = \"sdl\"\n    label = \"Gel Schema Definition Language\"\n\n    directives = {\n        'synopsis': SDLSynopsisDirective,\n    }\n\n\ndef setup_domain(app):\n    app.add_lexer(\"sdl\", EdgeQLLexer)\n    app.add_lexer(\"sdl-synopsis\", EdgeQLLexer)\n\n    app.add_role(\n        'sdl:synopsis',\n        shared.InlineCodeRole('sdl-synopsis'))\n\n    app.add_domain(SDLDomain)\n"
  },
  {
    "path": "edb/tools/docs/shared.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom docutils import nodes as d_nodes\nfrom docutils import utils as d_utils\nfrom docutils.parsers.rst import roles as d_roles\nfrom docutils.parsers.rst import directives as d_directives  # type: ignore\nfrom sphinx.directives import code as s_code\n\nfrom sphinx import errors as s_errors\n\n\nclass EdgeSphinxExtensionError(s_errors.ExtensionError):\n    pass\n\n\nclass DomainError(EdgeSphinxExtensionError):\n    pass\n\n\nclass InlineCodeRole:\n\n    def __init__(self, lang):\n        self.lang = lang\n\n    def __call__(\n        self, role, rawtext, text, lineno, inliner, options=None, content=None\n    ):\n        if options is None:\n            options = {}\n        if content is None:\n            content = []\n        d_roles.set_classes(options)\n        node = d_nodes.literal(rawtext, d_utils.unescape(text), **options)\n        node['eql-lang'] = self.lang\n        return [node], []\n\n\ndef make_CodeBlock(parent):\n    class CodeBlock(parent):\n\n        option_spec = s_code.CodeBlock.option_spec.copy()\n        option_spec.update({\n            'version-lt': d_directives.unchanged_required\n        })\n\n        def run(self):\n            literal = super().run()\n            if 'version-lt' in self.options:\n                if len(self.options) > 1:\n                    raise EdgeSphinxExtensionError(\n                        'other options not allowed if :version-lt: option '\n                        'is provided, put other options on latest version '\n                        'code block'\n                    )\n                literal[0]['version_lt'] = self.options['version-lt']\n            return literal\n\n    return CodeBlock\n\n\nCodeBlock = make_CodeBlock(s_code.CodeBlock)\n"
  },
  {
    "path": "edb/tools/edb.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom edb.common.log import early_setup\n# ruff: noqa: E402\nearly_setup()\n\nimport os\nimport sys\n\nimport click\n\nfrom edb import buildmeta\nfrom edb.common import debug\nfrom edb.common import devmode as dm\nfrom edb.server import args as srv_args\nfrom edb.server import main as srv_main\nfrom edb.load_ext import main as load_ext_main\n\n\n@click.group(\n    context_settings=dict(help_option_names=['-h', '--help']))\n@click.option('--devmode/--no-devmode',\n              help='enable or disable the development mode',\n              default=True)\n@click.pass_context\ndef edbcommands(ctx, devmode: bool):\n    if devmode:\n        dm.enable_dev_mode()\n\n\n@edbcommands.command()\n@srv_args.server_options\ndef server(version=False, **kwargs):\n    if version:\n        print(f\"edb, version {buildmeta.get_version()}\")\n        sys.exit(0)\n\n    os.environ['EDGEDB_DEBUG_SERVER'] = '1'\n    debug.init_debug_flags()\n    kwargs['security'] = srv_args.ServerSecurityMode.InsecureDevMode\n    if kwargs['cors_always_allowed_origins'] is None:\n        kwargs['cors_always_allowed_origins'] = \"http://localhost:*\"\n    srv_main.server_main(**kwargs)\n\n\n@edbcommands.command(add_help_option=False,\n                     context_settings=dict(ignore_unknown_options=True))\n@click.argument('args', nargs=-1, type=click.UNPROCESSED)\ndef load_ext(args: tuple[str, ...]):\n    load_ext_main.main(args)\n\n\n# Import at the end of the file so that \"edb.tools.edb.edbcommands\"\n# is defined for all of the below modules when they try to import it.\nfrom . import cli  # noqa\nfrom . import config  # noqa\nfrom . import rm_data_dir  # noqa\nfrom . import dflags  # noqa\nfrom . import fake_ai_server  # noqa\nfrom . import gen_errors  # noqa\nfrom . import gen_types  # noqa\nfrom . import gen_meta_grammars  # noqa\nfrom . import gen_cast_table  # noqa\nfrom . import inittestdb  # noqa\nfrom . import test  # noqa\nfrom . import test_extension  # noqa\nfrom . import wipe  # noqa\nfrom . import gen_test_dumps  # noqa\nfrom . import gen_sql_introspection  # noqa\nfrom . import gen_rust_ast  # noqa\nfrom . import ast_inheritance_graph  # noqa\nfrom . import parser_demo  # noqa\nfrom . import ls_forbidden_functions  # noqa\nfrom . import redo_metaschema  # noqa\nfrom . import ls  # noqa\nfrom . import railroad_diagram  # noqa\nfrom .profiling import cli as prof_cli  # noqa\nfrom .experimental_interpreter import edb_entry # noqa\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/back_to_ql.py",
    "content": "from typing import Any, Optional, Sequence\n\nfrom edb.edgeql import ast as qlast\nfrom edb.schema.pointers import PointerDirection\n\nfrom .data.data_ops import (\n    ArrExpr,\n    BackLinkExpr,\n    DetachedExpr,\n    Expr,\n    FilterOrderExpr,\n    ForExpr,\n    FreeVarExpr,\n    FunAppExpr,\n    InsertExpr,\n    Label,\n    LinkPropLabel,\n    LinkPropProjExpr,\n    MultiSetExpr,\n    NamedTupleExpr,\n    ObjectProjExpr,\n    ObjectVal,\n    OffsetLimitExpr,\n    OptionalForExpr,\n    OrderAscending,\n    OrderDescending,\n    OrderLabelSep,\n    RefVal,\n    ShapedExprExpr,\n    ShapeExpr,\n    StrLabel,\n    SubqueryExpr,\n    Tp,\n    TpIntersectExpr,\n    TypeCastExpr,\n    UnionExpr,\n    UnnamedTupleExpr,\n    UpdateExpr,\n    Val,\n    WithExpr,\n)\nfrom .data import data_ops as e\nfrom .data.expr_ops import abstract_over_expr, instantiate_expr\nfrom .elaboration import DEFAULT_HEAD_NAME\n\n\ndef reverse_elab_error(msg: str, expr: Val | Expr | Sequence[Val]) -> Any:\n    raise ValueError(\"Reverse Elab Error\", msg, expr)\n\n\ndef reverse_elab_raw_name(name: e.RawName) -> qlast.ObjectRef:\n    match name:\n        case e.QualifiedName(names=names):\n            return qlast.ObjectRef(\n                name=names[-1], module=\"::\".join(names[:-1])\n            )\n        case e.UnqualifiedName(name=n):\n            return qlast.ObjectRef(name=n)\n        case e.UncheckedTypeName(name=n):\n            return reverse_elab_raw_name(n)\n        case _:\n            raise ValueError(name)\n\n\ndef show_raw_name(name: e.RawName) -> str:\n    match name:\n        case e.QualifiedName(names=names):\n            return \"::\".join(names)\n        case e.UnqualifiedName(name=n):\n            return n\n        case _:\n            raise ValueError(name)\n\n\ndef reverse_elab_label(lbl: Label) -> qlast.Path:\n    match lbl:\n        case StrLabel(l):\n            return qlast.Path(\n                steps=[qlast.Ptr(name=l, direction=PointerDirection.Outbound)]\n            )\n        case LinkPropLabel(l):\n            return qlast.Path(steps=[qlast.Ptr(name=l, type='property')])\n        case _:\n            raise ValueError(lbl)\n\n\ndef reverse_elab_shape(expr: ShapeExpr) -> list[qlast.ShapeElement]:\n    return [\n        qlast.ShapeElement(\n            expr=reverse_elab_label(lbl),\n            compexpr=reverse_elab(\n                instantiate_expr(FreeVarExpr(DEFAULT_HEAD_NAME), val)\n            ),\n            operation=qlast.ShapeOperation(op=qlast.ShapeOp.ASSIGN),\n        )\n        for (lbl, val) in expr.shape.items()\n    ]\n\n\ndef reverse_elab_type_name(tp: Tp | e.RawName) -> qlast.TypeName:\n    match tp:\n        case e.QualifiedName(_):\n            qname = tp\n            if len(qname.names) == 2:\n                return qlast.TypeName(\n                    maintype=qlast.ObjectRef(\n                        name=qname.names[-1],\n                        module=\"::\".join(qname.names[:-1]),\n                    )\n                )\n            elif len(qname.names) == 1:\n                return qlast.TypeName(\n                    maintype=qlast.ObjectRef(name=qname.names[0])\n                )\n            else:\n                raise ValueError(\"Unimplemented\")\n        case e.ScalarTp(qname) | e.UncheckedTypeName(qname):\n            if isinstance(qname, e.QualifiedName):\n                return reverse_elab_type_name(qname)\n            elif isinstance(qname, e.UnqualifiedName):\n                return qlast.TypeName(\n                    maintype=qlast.ObjectRef(name=qname.name)\n                )\n            else:\n                raise ValueError(\"Unimplemented\")\n        case e.CompositeTp(kind=kind, tps=tps, labels=_):\n            return qlast.TypeName(\n                maintype=qlast.ObjectRef(name=kind.name),\n                subtypes=[reverse_elab_type_name(tp) for tp in tps],\n            )\n        case e.AnyTp(specifier=specifier):\n            return qlast.TypeName(\n                maintype=reverse_elab_raw_name(\n                    e.QualifiedName(names=[\"std\", \"any\" + (specifier or \"\")])\n                )\n            )\n    raise ValueError(\"Unimplemented\")\n\n\ndef reverse_elab_order(\n    order: dict[str, Expr]\n) -> Optional[list[qlast.SortExpr]]:\n    keys = sorted(\n        [\n            (idx, spec, k)\n            for k in order.keys()\n            for [idx, spec, empty_spec] in [k.split(OrderLabelSep)]\n        ]\n    )\n    if len(keys) == 0:\n        return None\n    return [\n        qlast.SortExpr(\n            path=reverse_elab(order[k]),\n            direction=(\n                qlast.SortOrder.Asc\n                if spec == OrderAscending\n                else (\n                    qlast.SortOrder.Desc\n                    if spec == OrderDescending\n                    else reverse_elab_error(\n                        \"unknown direction \" + spec, order[k]\n                    )\n                )\n            ),\n        )\n        for (idx, spec, k) in keys\n    ]\n\n\ndef reverse_elab_object_val(val: ObjectVal) -> qlast.Expr:\n    return qlast.Shape(\n        expr=None,\n        elements=reverse_elab_shape(\n            ShapeExpr(\n                shape={\n                    lbl: abstract_over_expr(\n                        MultiSetExpr([e for e in mv.getVals()])\n                    )\n                    for (lbl, (u, mv)) in val.val.items()\n                }\n            )\n        ),\n    )\n\n\ndef append_path_element(\n    subject: qlast.Expr, to_add: qlast.PathElement\n) -> qlast.Path:\n    match subject:\n        case qlast.Path(steps=steps, partial=partial):\n            return qlast.Path(steps=[*steps, to_add], partial=partial)\n        case rsub:\n            return qlast.Path(steps=[rsub, to_add], partial=False)\n\n\ndef reverse_elab(ir_expr: Expr) -> qlast.Expr:\n    expr: Expr\n    match ir_expr:\n        case e.ScalarVal(tp, s):\n            match tp.name.names:\n                case [\"std\", \"str\"]:\n                    return qlast.Constant.string(value=s)\n                case [\"std\", \"int64\"]:\n                    i = s\n                    return qlast.Constant.integer(i)\n                case [\"std\", \"bool\"]:\n                    b = s\n                    return qlast.Constant.boolean(b)\n                case _:\n                    raise ValueError(\"Unimplemented\", tp.name)\n        case RefVal(_):\n            return qlast.Constant.string(\n                value=str(\"<REFVAL, TODO: UUID_CASTING>\")\n            )\n        case InsertExpr(name=tname, new=arg):\n            return qlast.InsertQuery(\n                subject=reverse_elab_raw_name(tname),\n                shape=reverse_elab_shape(\n                    e.ShapeExpr(\n                        shape={\n                            StrLabel(k): abstract_over_expr(v)\n                            for (k, v) in arg.items()\n                        }\n                    )\n                ),\n            )\n        case FilterOrderExpr(subject=subject, filter=filter, order=order):\n            result_name = filter.var\n            return qlast.SelectQuery(\n                result=reverse_elab(subject),\n                result_alias=result_name,\n                where=reverse_elab(\n                    instantiate_expr(FreeVarExpr(result_name), filter)\n                ),\n                orderby=reverse_elab_order(\n                    {\n                        l: instantiate_expr(FreeVarExpr(result_name), o)\n                        for (l, o) in order.items()\n                    }\n                ),\n            )\n        case OffsetLimitExpr(subject=subject, offset=offset, limit=limit):\n            return qlast.SelectQuery(\n                result=reverse_elab(subject),\n                offset=reverse_elab(offset),\n                limit=reverse_elab(limit),\n            )\n        case ShapedExprExpr(expr=subject, shape=shape):\n            return qlast.Shape(\n                expr=reverse_elab(subject), elements=reverse_elab_shape(shape)\n            )\n        case FreeVarExpr(var=name):\n            if name == DEFAULT_HEAD_NAME:\n                return qlast.Path(steps=[], partial=True)\n            else:\n                return qlast.Path(steps=[qlast.ObjectRef(name=name)])\n        case e.QualifiedName(names=_):\n            return qlast.Path(steps=[reverse_elab_raw_name(ir_expr)])\n        case e.UnqualifiedName(name=name):\n            return qlast.Path(steps=[reverse_elab_raw_name(ir_expr)])\n        case FunAppExpr(fun=fname, args=args, overloading_index=_):\n            return qlast.FunctionCall(\n                func=show_raw_name(fname),\n                args=[reverse_elab(arg) for arg in args],\n            )\n        case e.ConditionalDedupExpr(expr=inner):\n            return qlast.FunctionCall(\n                func=\"cond_dedup\", args=[reverse_elab(inner)]\n            )\n        case e.FreeObjectExpr():\n            return qlast.Path(steps=[qlast.ObjectRef(name=\"std::FreeObject\")])\n        case ObjectProjExpr(subject=subject, label=label) | e.TupleProjExpr(\n            subject=subject, label=label\n        ):\n            label_path_component = qlast.Ptr(\n                name=label, direction=PointerDirection.Outbound, type=None\n            )\n            return append_path_element(\n                reverse_elab(subject), label_path_component\n            )\n        case BackLinkExpr(subject=subject, label=label):\n            label_path_component = qlast.Ptr(\n                name=label, direction=PointerDirection.Inbound, type=None\n            )\n            return append_path_element(\n                reverse_elab(subject), label_path_component\n            )\n        case LinkPropProjExpr(subject=subject, linkprop=label):\n            label_path_component = qlast.Ptr(\n                name=label,\n                direction=PointerDirection.Outbound,\n                type=\"property\",\n            )\n            return append_path_element(\n                reverse_elab(subject), label_path_component\n            )\n        case e.IsTpExpr(subject=subject, tp=tp_name):\n            if isinstance(tp_name, e.AnyTp):\n                original_tp = qlast.TypeName(\n                    maintype=reverse_elab_raw_name(\n                        e.QualifiedName(\n                            [\"std\", \"any\" + (tp_name.specifier or \"\")]\n                        )\n                    )\n                )\n            else:\n                original_tp = reverse_elab_type_name(tp_name)\n            return qlast.IsOp(\n                left=reverse_elab(subject), op=\"IS\", right=original_tp\n            )\n        case TpIntersectExpr(subject=subject, tp=tp_name):\n            tp_path_component = qlast.TypeIntersection(\n                type=reverse_elab_type_name(tp_name)\n            )\n            return append_path_element(\n                reverse_elab(subject), tp_path_component\n            )\n        case TypeCastExpr(tp=tp, arg=arg):\n            return qlast.TypeCast(\n                type=reverse_elab_type_name(tp), expr=reverse_elab(arg)\n            )\n        case UnnamedTupleExpr(val=tuples) | e.UnnamedTupleVal(val=tuples):\n            return qlast.Tuple(elements=[reverse_elab(e) for e in tuples])\n        case NamedTupleExpr(val=tuples):\n            return qlast.NamedTuple(\n                elements=[\n                    qlast.TupleElement(\n                        name=qlast.Ptr(name=k), val=reverse_elab(v)\n                    )\n                    for (k, v) in tuples.items()\n                ]\n            )\n        case UnionExpr(left=l, right=r):\n            return qlast.BinOp(\n                op=\"UNION\", left=reverse_elab(l), right=reverse_elab(r)\n            )\n        case ArrExpr(elems=elems):\n            return qlast.Array(elements=[reverse_elab(e) for e in elems])\n        case UpdateExpr(subject=subject, shape=shape):\n            return qlast.UpdateQuery(\n                subject=reverse_elab(subject), shape=reverse_elab_shape(shape)\n            )\n        case e.DeleteExpr(subject=subject):\n            return qlast.DeleteQuery(subject=reverse_elab(subject))\n        case MultiSetExpr(expr=elems):\n            return qlast.Set(elements=[reverse_elab(e) for e in elems])\n        case WithExpr(bound=bound, next=next):\n            name = next.var\n            body = reverse_elab(instantiate_expr(FreeVarExpr(name), next))\n            if (\n                isinstance(body, qlast.SelectQuery)\n                or isinstance(body, qlast.InsertQuery)\n                or isinstance(body, qlast.UpdateQuery)\n                or isinstance(body, qlast.ForQuery)\n            ):\n                if body.aliases is None:\n                    body.aliases = []\n                body.aliases = [\n                    qlast.AliasedExpr(alias=name, expr=reverse_elab(bound)),\n                    *body.aliases,\n                ]\n                return body\n            else:\n                return qlast.SelectQuery(\n                    result=body,\n                    aliases=[\n                        qlast.AliasedExpr(alias=name, expr=reverse_elab(bound))\n                    ],\n                )\n        case ForExpr(bound=bound, next=next):\n            name = next.var\n            bound_v = reverse_elab(bound)\n            body = reverse_elab(instantiate_expr(FreeVarExpr(name), next))\n            return qlast.ForQuery(\n                iterator=bound_v, iterator_alias=name, result=body\n            )\n        case OptionalForExpr(bound=bound, next=next):\n            name = next.var\n            bound_v = reverse_elab(bound)\n            body = reverse_elab(instantiate_expr(FreeVarExpr(name), next))\n            return qlast.ForQuery(\n                iterator=bound_v,\n                iterator_alias=name,\n                optional=True,\n                result=body,\n            )\n        case DetachedExpr(expr=expr):\n            return qlast.DetachedExpr(expr=reverse_elab(expr))\n        case SubqueryExpr(expr=expr):\n            return reverse_elab(expr)\n        case e.IfElseExpr(\n            then_branch=then_branch,\n            condition=condition,\n            else_branch=else_branch,\n        ):\n            return qlast.IfElse(\n                if_expr=reverse_elab(then_branch),\n                condition=reverse_elab(condition),\n                else_expr=reverse_elab(else_branch),\n            )\n        case e.CheckedTypeCastExpr(\n            cast_tp=(_, target_tp), cast_spec=_, arg=arg\n        ):\n            return qlast.TypeCast(\n                type=reverse_elab_type_name(target_tp), expr=reverse_elab(arg)\n            )\n        case e.ParameterExpr(name=name, tp=tp, is_required=is_required):\n            return qlast.TypeCast(\n                type=reverse_elab_type_name(tp),\n                expr=qlast.QueryParameter(name=name),\n                cardinality_mod=(\n                    qlast.CardinalityModifier.Required\n                    if is_required\n                    else qlast.CardinalityModifier.Optional\n                ),\n            )\n        case e.QualifiedNameWithFilter(name=name, filter=filter):\n            return qlast.Path(steps=[reverse_elab_raw_name(name)])\n        case _:\n            raise ValueError(\"Unimplemented\", ir_expr)\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/basis/80-interpreter-internal.edgeql",
    "content": "\n\nCREATE FUNCTION\nstd::`_[_]`(array: array<anytype>, idx: std::int64) -> anytype\n{\n    USING SQL EXPRESSION;\n};\n\nCREATE FUNCTION\nstd::`_[_]`(array: json, idx: std::int64) -> json\n{\n    USING SQL EXPRESSION;\n};\n\nCREATE FUNCTION\nstd::`_[_:]`(array: array<anytype>, idx: std::int64) -> array<anytype>\n{\n    USING SQL EXPRESSION;\n};\n\nCREATE FUNCTION\nstd::`_[_:]`(array: json, idx: std::int64) -> json\n{\n    USING SQL EXPRESSION;\n};\n\nCREATE FUNCTION\nstd::`_[:_]`(array: array<anytype>, idx: std::int64) -> array<anytype>\n{\n    USING SQL EXPRESSION;\n};\n\nCREATE FUNCTION\nstd::`_[:_]`(array: json, idx: std::int64) -> json\n{\n    USING SQL EXPRESSION;\n};\n\nCREATE FUNCTION\nstd::`_[_:_]`(array: array<anytype>, idx_start: std::int64, idx_end: std::int64) -> array<anytype>\n{\n    USING SQL EXPRESSION;\n};\n\nCREATE FUNCTION\nstd::`_[_:_]`(array: json, idx_start: std::int64, idx_end: std::int64) -> json\n{\n    USING SQL EXPRESSION;\n};\n\nCREATE FUNCTION\nstd::`_[_]`(s: std::str, idx: std::int64) -> std::str\n{\n    USING SQL EXPRESSION;\n};\n\nCREATE FUNCTION\nstd::`_[_:]`(s: std::str, idx: std::int64) -> std::str\n{\n    USING SQL EXPRESSION;\n};\n\nCREATE FUNCTION\nstd::`_[:_]`(s: std::str , idx: std::int64) -> std::str\n{\n    USING SQL EXPRESSION;\n};\n\nCREATE FUNCTION\nstd::`_[_:_]`(s: std::str, idx_start: std::int64, idx_end: std::int64) -> std::str\n{\n    USING SQL EXPRESSION;\n};"
  },
  {
    "path": "edb/tools/experimental_interpreter/basis/built_ins.py",
    "content": "from typing import Callable, Any, Optional, Sequence\nfrom ..data import data_ops as e\n\n\ndef lift_binary_scalar_op(\n    f: Callable[[Any, Any], Any], override_ret_tp: Optional[e.ScalarTp] = None\n) -> Callable[[Sequence[Sequence[e.Val]]], Sequence[e.Val]]:\n\n    def op_impl(arg: Sequence[Sequence[e.Val]]) -> Sequence[e.Val]:\n        match arg:\n            case [[e.ScalarVal(t1, v1)], [e.ScalarVal(t2, v2)]]:\n                if t1 != t2 and override_ret_tp is None:\n                    raise ValueError(\"Scalar types do not match\", t1, t2)\n                ret_tp = t1\n                if override_ret_tp is not None:\n                    ret_tp = override_ret_tp\n                return [e.ScalarVal(ret_tp, f(v1, v2))]\n        raise ValueError(\"Expecing two scalar values\")\n\n    return op_impl\n\n\ndef lift_unary_scalar_op(\n    f: Callable[[Any], Any]\n) -> Callable[[Sequence[Sequence[e.Val]]], Sequence[e.Val]]:\n\n    def impl(arg: Sequence[Sequence[e.Val]]) -> Sequence[e.Val]:\n        match arg:\n            case [[e.ScalarVal(t1, v1)]]:\n                return [e.ScalarVal(t1, f(v1))]\n        raise ValueError(\"Expecing two scalar values\")\n\n    return impl\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/basis/builtin_bin_ops.py",
    "content": "from __future__ import annotations\nfrom collections import Counter as mset\n\nfrom typing import Sequence\n\nfrom ..data import data_ops as e\nfrom ..data import expr_ops as eops\n\nfrom ..data.data_ops import BoolVal, Val, RefVal, StrVal, ArrVal\nfrom .errors import FunCallErr\nfrom .built_ins import lift_unary_scalar_op, lift_binary_scalar_op\nimport fnmatch\nimport operator\n\n\ndef add_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[e.ScalarVal(t1, v1)], [e.ScalarVal(t2, v2)]]:\n            assert t1 == t2\n            return [e.ScalarVal(t1, v1 + v2)]\n    raise FunCallErr()\n\n\ndef subtract_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[e.ScalarVal(t1, v1)], [e.ScalarVal(t2, v2)]]:\n            assert t1 == t2\n            return [e.ScalarVal(t1, v1 - v2)]\n    raise FunCallErr()\n\n\ndef eq_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[v1], [v2]]:\n            return [BoolVal(eops.val_eq(v1, v2))]\n    raise FunCallErr(arg)\n\n\ndef not_eq_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[e.ScalarVal(t1, v1)], [e.ScalarVal(t2, v2)]]:\n            assert t1 == t2\n            return [BoolVal(v1 != v2)]\n        case [[RefVal(id1, v1)], [RefVal(id2, v2)]]:\n            return [BoolVal(id1 != id2)]\n    raise FunCallErr(arg)\n\n\ndef opt_eq_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[], []]:\n            return [BoolVal(True)]\n        case [l1, l2]:\n            if len(l1) == 0 or len(l2) == 0:\n                return [BoolVal(False)]\n            else:\n                return eq_impl(arg)\n    raise FunCallErr()\n\n\ndef opt_not_eq_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[], []]:\n            return [BoolVal(False)]\n        case [l1, l2]:\n            if len(l1) == 0 or len(l2) == 0:\n                return [BoolVal(True)]\n            else:\n                return not_eq_impl(arg)\n    raise FunCallErr()\n\n\ndef concatenate_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[e.ScalarVal(_, s1)], [e.ScalarVal(_, s2)]]:\n            return [StrVal(s1 + s2)]\n        case [[ArrVal(arr1)], [ArrVal(arr2)]]:\n            return [ArrVal([*arr1, *arr2])]\n    raise FunCallErr()\n\n\ndef coalescing_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[], default]:\n            return default\n        case [[non_empty], _]:\n            return [non_empty]\n    raise FunCallErr()\n\n\ndef in_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[singleton], l]:\n            if isinstance(singleton, RefVal):\n                assert all(isinstance(v, RefVal) for v in l)\n                return [\n                    BoolVal(singleton.refid in [v.refid   # type: ignore\n                                                for v in l])\n                ]\n            elif all(isinstance(v, e.ScalarVal) for v in l) and isinstance(\n                singleton, e.ScalarVal\n            ):\n                return [BoolVal(singleton.val in (v.val for v in l))]\n            else:\n                return [BoolVal(singleton in l)]\n    raise FunCallErr()\n\n\ndef exists_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[]]:\n            return [BoolVal(False)]\n        case [_]:\n            return [BoolVal(True)]\n    raise FunCallErr()\n\n\ndef or_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[e.ScalarVal(_, b1)], [e.ScalarVal(_, b2)]]:\n            return [e.BoolVal(b1 or b2)]\n        case [_]:\n            return [BoolVal(True)]\n    raise FunCallErr()\n\n\ndef like(value, pattern) -> bool:\n    fnmatch_pattern = (\n        pattern.replace('%', '*')\n        .replace('_', '?')\n        .replace(r'\\%', '%')\n        .replace(r'\\_', '_')\n    )\n    return fnmatch.fnmatch(value, fnmatch_pattern)\n\n\ndef ilike(value, pattern) -> bool:\n    fnmatch_pattern = (\n        pattern.replace('%', '*')\n        .replace('_', '?')\n        .replace(r'\\%', '%')\n        .replace(r'\\_', '_')\n    )\n    value = value.lower()\n    fnmatch_pattern = fnmatch_pattern.lower()\n    return fnmatch.fnmatch(value, fnmatch_pattern)\n\n\ndef like_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[e.ScalarVal(_, value)], [e.ScalarVal(_, pattern)]]:\n            return [e.BoolVal(like(value, pattern))]\n    raise FunCallErr()\n\n\ndef not_like_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[e.ScalarVal(_, value)], [e.ScalarVal(_, pattern)]]:\n            return [e.BoolVal(not like(value, pattern))]\n    raise FunCallErr()\n\n\ndef ilike_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[e.ScalarVal(_, value)], [e.ScalarVal(_, pattern)]]:\n            return [e.BoolVal(ilike(value, pattern))]\n    raise FunCallErr()\n\n\ndef not_ilike_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[e.ScalarVal(_, value)], [e.ScalarVal(_, pattern)]]:\n            return [e.BoolVal(not ilike(value, pattern))]\n    raise FunCallErr()\n\n\ndef distinct_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [vset]:\n            if all(isinstance(v, e.RefVal) for v in vset):\n                return {v.refid: v for v in vset}.values()  # type: ignore\n            elif all(\n                isinstance(v, e.ArrVal | e.UnnamedTupleVal | e.NamedTupleVal)\n                for v in vset\n            ):\n                return vset\n            else:\n                return list(set(vset))\n    raise FunCallErr()\n\n\ndef intersect_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    def list_interset(l1, l2):\n        return list((mset(l1) & mset(l2)).elements())\n\n    match arg:\n        case [arg1, arg2]:\n            if all(isinstance(v, e.RefVal) for v in arg1) and all(\n                isinstance(v, e.RefVal) for v in arg2\n            ):\n                id1 = [v.refid for v in arg1]  # type: ignore[union-attr]\n                id2 = [v.refid for v in arg2]  # type: ignore[union-attr]\n                id_common = list_interset(id1, id2)\n                return [v for v in arg1 if v.refid in id_common]  # type: ignore[union-attr]\n            else:\n                return list_interset(arg1, arg2)\n    raise FunCallErr()\n\n\ndef except_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [arg1, arg2]:\n            if all(isinstance(v, e.RefVal) for v in arg1) and all(\n                isinstance(v, e.RefVal) for v in arg2\n            ):\n                id1 = [v.refid for v in arg1]  # type: ignore\n                id2 = [v.refid for v in arg2]  # type: ignore\n                id_diff = list((mset(id1) - mset(id2)).elements())\n                return [v for v in arg1 if v.refid in id_diff]  # type: ignore\n            else:\n                return list((mset(arg1) - mset(arg2)).elements())\n    raise FunCallErr()\n\n\nmultiply_impl = lift_binary_scalar_op(operator.mul)\nfloor_divide_impl = lift_binary_scalar_op(operator.floordiv)\nmod_impl = lift_binary_scalar_op(operator.mod)\ngt_impl = lift_binary_scalar_op(operator.gt, override_ret_tp=e.BoolTp())\nnot_impl = lift_unary_scalar_op(operator.not_)\nand_impl = lift_binary_scalar_op(operator.and_)\npow_impl = lift_binary_scalar_op(operator.pow)\nless_than_impl = lift_binary_scalar_op(operator.lt, override_ret_tp=e.BoolTp())\nless_than_or_equal_to_impl = lift_binary_scalar_op(\n    operator.le, override_ret_tp=e.BoolTp()\n)\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/basis/errors.py",
    "content": "class FunCallErr(Exception):\n    pass\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/basis/reserved_ops.py",
    "content": "from __future__ import annotations\nfrom typing import Sequence\nimport edgedb\n\nfrom ..data import data_ops as e\nfrom .errors import FunCallErr\n\n\ndef indirection_slice_start_stop_impl(\n    arg: Sequence[Sequence[e.Val]],\n) -> Sequence[e.Val]:\n    match arg:\n        case [\n            [e.ScalarVal(t1, s)],\n            [e.ScalarVal(_, start)],\n            [e.ScalarVal(_, end)],\n        ]:\n            return [e.ScalarVal(t1, s[start:end])]\n        case [\n            [e.ArrVal(val=arr)],\n            [e.ScalarVal(_, start)],\n            [e.ScalarVal(_, end)],\n        ]:\n            return [e.ArrVal(val=arr[start:end])]\n    raise FunCallErr()\n\n\ndef indirection_slice_start_impl(\n    arg: Sequence[Sequence[e.Val]],\n) -> Sequence[e.Val]:\n    match arg:\n        case [[e.ScalarVal(t1, s)], [e.ScalarVal(_, start)]]:\n            return [e.ScalarVal(t1, s[start:])]\n        case [[e.ArrVal(val=arr)], [e.ScalarVal(_, start)]]:\n            return [e.ArrVal(val=arr[start:])]\n    raise FunCallErr()\n\n\ndef indirection_slice_stop_impl(\n    arg: Sequence[Sequence[e.Val]],\n) -> Sequence[e.Val]:\n    match arg:\n        case [[e.ScalarVal(t1, s)], [e.ScalarVal(_, end)]]:\n            return [e.ScalarVal(t1, s[:end])]\n        case [[e.ArrVal(val=arr)], [e.ScalarVal(_, end)]]:\n            return [e.ArrVal(val=arr[:end])]\n    raise FunCallErr()\n\n\ndef indirection_index_impl(arg: Sequence[Sequence[e.Val]]) -> Sequence[e.Val]:\n    match arg:\n        case [[e.ScalarVal(t1, s)], [e.ScalarVal(_, idx)]]:\n            if idx < -len(s) or idx >= len(s):\n                raise edgedb.InvalidValueError(f\"index {idx} is out of bounds\")\n            return [e.ScalarVal(t1, s[idx])]\n        case [[e.ArrVal(val=arr)], [e.ScalarVal(_, idx)]]:\n            if idx < -len(arr) or idx >= len(arr):\n                raise edgedb.InvalidValueError(\n                    f\"array index {idx} is out of bounds\"\n                )\n            return [arr[idx]]\n    raise FunCallErr()\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/basis/server_funcs.py",
    "content": "from __future__ import annotations\nfrom typing import Callable, Sequence\n\nfrom ..data import data_ops as e\nfrom . import std_funcs as stdf\nfrom . import builtin_bin_ops as bbo\nfrom .reserved_ops import (\n    indirection_index_impl,\n    indirection_slice_start_stop_impl,\n    indirection_slice_start_impl,\n    indirection_slice_stop_impl,\n)\nfrom ..data.casts import type_cast\n\n\nall_server_std_funcs: dict[\n    str, Callable[[Sequence[Sequence[e.Val]]], Sequence[e.Val]]\n] = {\n    \"+\": bbo.add_impl,\n    \"-\": bbo.subtract_impl,\n    \"*\": bbo.multiply_impl,\n    \"%\": bbo.mod_impl,\n    \"//\": bbo.floor_divide_impl,\n    \"=\": bbo.eq_impl,\n    \"!=\": bbo.not_eq_impl,\n    \"?=\": bbo.opt_eq_impl,\n    \"?!=\": bbo.opt_not_eq_impl,\n    \">\": bbo.gt_impl,\n    \"^\": bbo.pow_impl,\n    \"<=\": bbo.less_than_or_equal_to_impl,\n    \"++\": bbo.concatenate_impl,\n    \"??\": bbo.coalescing_impl,\n    \"IN\": bbo.in_impl,\n    \"EXISTS\": bbo.exists_impl,\n    \"OR\": bbo.or_impl,\n    \"AND\": bbo.and_impl,\n    \"NOT\": bbo.not_impl,\n    \"LIKE\": bbo.like_impl,\n    \"NOT LIKE\": bbo.not_like_impl,\n    \"ILIKE\": bbo.ilike_impl,\n    \"NOT ILIKE\": bbo.not_ilike_impl,\n    \"DISTINCT\": bbo.distinct_impl,\n    \"EXCEPT\": bbo.except_impl,\n    \"INTERSECT\": bbo.intersect_impl,\n    \"<\": bbo.less_than_impl,\n    \"all\": stdf.std_all_impl,\n    \"any\": stdf.std_any_impl,\n    \"array_agg\": stdf.std_array_agg_impl,\n    \"array_unpack\": stdf.std_array_unpack_impl,\n    \"count\": stdf.std_count_impl,\n    \"enumerate\": stdf.std_enumerate_impl,\n    \"len\": stdf.std_len_impl,\n    \"sum\": stdf.std_sum_impl,\n    \"assert_exists\": stdf.std_assert_exists,\n    \"assert_single\": stdf.std_assert_single,\n    \"assert_distinct\": stdf.std_assert_distinct,\n    \"str_split\": stdf.str_split_impl,\n    \"str_upper\": stdf.str_upper_impl,\n    \"str_lower\": stdf.str_lower_impl,\n    \"to_json\": stdf.to_json_impl,\n    \"datetime_current\": stdf.std_datetime_current,\n    \"contains\": stdf.std_contains_impl,\n    \"random\": stdf.random_impl,\n    \"re_test\": stdf.std_re_test_impl,\n    e.IndirectionIndexOp: indirection_index_impl,\n    e.IndirectionSliceStartStopOp: indirection_slice_start_stop_impl,\n    e.IndirectionSliceStartOp: indirection_slice_start_impl,\n    e.IndirectionSliceStopOp: indirection_slice_stop_impl,\n}\nall_server_cal_funcs: dict[\n    str, Callable[[Sequence[Sequence[e.Val]]], Sequence[e.Val]]\n] = {\n    \"to_local_datetime\": stdf.cal_to_local_datetime_impl,\n}\nall_server_math_funcs: dict[\n    str, Callable[[Sequence[Sequence[e.Val]]], Sequence[e.Val]]\n] = {\n    \"mean\": stdf.math_mean_impl,\n}\n\n\ndef get_default_func_impl_for_function(\n    name: e.QualifiedName,\n) -> Callable[[Sequence[Sequence[e.Val]]], Sequence[e.Val]]:\n    if len(name.names) == 2:\n\n        def default_impl(arg: Sequence[Sequence[e.Val]]) -> Sequence[e.Val]:\n            raise ValueError(\"Not implemented: \", name)\n\n        match name.names[0]:\n            case 'std':\n                if name.names[1] in all_server_std_funcs:\n                    return all_server_std_funcs[name.names[1]]\n                else:\n                    return default_impl\n            case \"std::cal\":\n                if name.names[1] in all_server_cal_funcs:\n                    return all_server_cal_funcs[name.names[1]]\n                else:\n                    return default_impl\n            case \"std::math\":\n                if name.names[1] in all_server_math_funcs:\n                    return all_server_math_funcs[name.names[1]]\n                else:\n                    return default_impl\n            case _:\n                raise ValueError(\n                    \"Cannot get a default implementaiton\"\n                    \" for a non-std function\",\n                    name,\n                )\n    else:\n        raise ValueError(\n            \"Cannot get a default implementaiton for a non-std function\", name\n        )\n\n\ndef get_default_func_impl_for_cast(\n    from_tp: e.Tp, to_tp: e.Tp\n) -> Callable[[e.Val], e.Val]:\n    def default_impl(arg: e.Val) -> e.Val:\n        return type_cast(to_tp, arg)\n\n    return default_impl\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/basis/std_funcs.py",
    "content": "from __future__ import annotations\n\nfrom typing import Sequence\n\n\nimport random\nimport json\nimport re\n\nfrom datetime import datetime\n\nfrom ..data import data_ops as e\nfrom ..data.data_ops import ArrVal, BoolVal, IntVal, Val, UnnamedTupleVal\nfrom .errors import FunCallErr\nfrom .. import interpreter_logging\n\n\ndef val_is_true(v: Val) -> bool:\n    match v:\n        case e.ScalarVal(_, v):\n            assert isinstance(v, bool)\n            return v\n        case _:\n            raise ValueError(\"not a boolean\")\n\n\ndef std_all_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [l1]:\n            return [BoolVal(all(val_is_true(v) for v in l1))]\n    raise FunCallErr()\n\n\ndef std_any_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n\n    match arg:\n        case [l1]:\n            return [BoolVal(any(val_is_true(v) for v in l1))]\n    raise FunCallErr()\n\n\ndef std_array_agg_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [l1]:\n            return [ArrVal(val=l1)]\n    raise FunCallErr()\n\n\ndef std_array_unpack_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[ArrVal(val=arr)]]:\n            return arr\n    raise FunCallErr()\n\n\ndef std_count_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [l1]:\n            return [IntVal(val=len(l1))]\n    raise FunCallErr()\n\n\ndef std_enumerate_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [l1]:\n            return [\n                UnnamedTupleVal(val=[IntVal(i), v]) for (i, v) in enumerate(l1)\n            ]\n    raise FunCallErr()\n\n\ndef std_len_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[e.ScalarVal(_, v)]]:\n            return [IntVal(len(v))]\n        case [[ArrVal(arr)]]:\n            return [IntVal(len(arr))]\n    raise FunCallErr()\n\n\ndef std_sum_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [l]:\n            if all(\n                isinstance(elem, e.ScalarVal) and isinstance(elem.val, int)\n                for elem in l\n            ):\n                return [IntVal(sum(elem.val for elem in l))]  # type: ignore\n            else:\n                raise ValueError(\"not implemented: std::sum\")\n    raise FunCallErr()\n\n\ndef std_assert_single(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [l, [msg]]:\n            if len(l) > 1:\n                raise ValueError(msg)\n            else:\n                return l\n        case [l, []]:\n            if len(l) > 1:\n                raise ValueError(\"Expected a single value, got more than one.\")\n            else:\n                return l\n    raise FunCallErr()\n\n\ndef std_assert_distinct(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [vset, pmsg]:\n            msg = None\n\n            match pmsg:\n                case [e.ScalarVal(_, errmsg)]:\n                    msg = errmsg\n                case []:\n                    msg = \"Expected distinct values, got duplicates.\"\n\n            if all(isinstance(v, e.RefVal) for v in vset):\n                ids = {v.refid: v for v in vset}.values()  # type: ignore\n                if len(ids) != len(vset):\n                    raise ValueError(msg)\n                else:\n                    return vset\n            elif all(\n                isinstance(v, e.ArrVal | e.UnnamedTupleVal | e.NamedTupleVal)\n                for v in vset\n            ):\n                if len(set(vset)) != len(vset):\n                    raise ValueError(msg)\n                else:\n                    return vset\n            else:\n                raise ValueError(\"Not implemented: assert_distinct\")\n    raise FunCallErr()\n\n\ndef std_assert_exists(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [l, [msg]]:\n            if len(l) == 0:\n                raise ValueError(\"std::assert_exists failed\", msg)\n            else:\n                return l\n        case [l, []]:\n            if len(l) == 0:\n                raise ValueError(\"Expected a value, got none.\")\n            else:\n                return l\n    raise FunCallErr()\n\n\ndef std_datetime_current(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case []:\n            current_datetime = datetime.now()\n            val = current_datetime.strftime(\"%Y-%m-%dT%H:%M:%S%z\")\n            return [\n                e.ScalarVal(\n                    e.ScalarTp(e.QualifiedName([\"std\", \"datetime\"])), val\n                )\n            ]\n    raise FunCallErr()\n\n\ndef str_split(s, delimiter):\n    return [part for part in s.split(delimiter)]\n\n\ndef str_split_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[e.ScalarVal(_, s)], [e.ScalarVal(_, delimiter)]]:\n            return [\n                ArrVal(\n                    val=[\n                        e.ScalarVal(\n                            e.ScalarTp(e.QualifiedName([\"std\", \"str\"])), part\n                        )\n                        for part in str_split(s, delimiter)\n                    ]\n                )\n            ]\n    raise FunCallErr()\n\n\ndef str_upper_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[e.ScalarVal(_, s)]]:\n            return [\n                e.ScalarVal(\n                    e.ScalarTp(e.QualifiedName([\"std\", \"str\"])), s.upper()\n                )\n            ]\n    raise FunCallErr()\n\n\ndef str_lower_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[e.ScalarVal(_, s)]]:\n            return [\n                e.ScalarVal(\n                    e.ScalarTp(e.QualifiedName([\"std\", \"str\"])), s.lower()\n                )\n            ]\n    raise FunCallErr()\n\n\ndef to_json_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[e.ScalarVal(_, s)]]:\n            return [\n                e.ScalarVal(\n                    e.ScalarTp(e.QualifiedName([\"std\", \"json\"])), json.loads(s)\n                )\n            ]\n    raise FunCallErr()\n\n\ndef random_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case []:\n            return [\n                e.ScalarVal(\n                    e.ScalarTp(e.QualifiedName([\"std\", \"float64\"])),\n                    random.random(),\n                )\n            ]\n    raise FunCallErr()\n\n\ndef cal_to_local_datetime_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [[e.ScalarVal(_, s)], [e.ScalarVal(_, _)]]:\n            interpreter_logging.print_warning(\n                \"Warning: cal::to_local_datetime is implemented\"\n                \" properly. It is a no-op.\"\n            )\n            return [\n                e.ScalarVal(\n                    e.ScalarTp(e.QualifiedName(\n                        [\"std::cal\", \"local_datetime\"]\n                    )),\n                    s\n                )\n            ]\n    raise FunCallErr()\n\n\ndef math_mean_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [l]:\n            return [\n                e.ScalarVal(\n                    e.ScalarTp(e.QualifiedName([\"std\", \"float64\"])),\n                    sum(elem.val for elem in l) / len(l),  # type: ignore\n                )\n            ]\n    raise FunCallErr()\n\n\ndef std_contains_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [\n            [\n                e.ScalarVal(\n                    e.ScalarTp(e.QualifiedName([\"std\", \"str\"])), haystack\n                )\n            ],\n            [e.ScalarVal(e.ScalarTp(e.QualifiedName([\"std\", \"str\"])), needle)],\n        ]:\n            return [e.BoolVal(needle in haystack)]\n    raise FunCallErr()\n\n\ndef std_re_test_impl(arg: Sequence[Sequence[Val]]) -> Sequence[Val]:\n    match arg:\n        case [\n            [\n                e.ScalarVal(\n                    e.ScalarTp(e.QualifiedName([\"std\", \"str\"])), pattern\n                )\n            ],\n            [e.ScalarVal(e.ScalarTp(e.QualifiedName([\"std\", \"str\"])), string)],\n        ]:\n            return [e.BoolVal(bool(re.search(pattern, string)))]\n    raise FunCallErr()\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/data/casts.py",
    "content": "from . import data_ops as e\nfrom . import val_to_json as v2j\n\n\ndef type_cast(tp: e.Tp, arg: e.Val) -> e.Val:\n    match (tp, arg):\n        case e.ScalarTp(qname), e.ScalarVal(_, v):\n            match qname.names:\n                case [\"std\", \"str\"]:\n                    return e.StrVal(str(v))\n                case [\"std\", \"datetime\"]:\n                    assert isinstance(v, str)\n                    return e.ScalarVal(tp=e.ScalarTp(qname), val=v)\n                case [\"std\", \"int64\"]:\n                    return e.IntVal(int(v))\n                case _:\n                    raise ValueError(\"cannot cast\", tp, arg)\n        case _:\n            raise ValueError(\"cannot cast\", tp, arg)\n\n\ndef get_json_cast(source_tp: e.Tp, schema: e.DBSchema) -> e.TpCast:\n\n    def cast_to_json(arg: e.Val) -> e.Val:\n        return e.ScalarVal(\n            tp=e.ScalarTp(e.QualifiedName([\"std\", \"json\"])),\n            val=v2j.typed_val_to_json_like(arg, source_tp, schema),\n        )\n\n    return e.TpCast(e.TpCastKind.Explicit, cast_to_json)\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/data/data_ops.py",
    "content": "from __future__ import annotations\nfrom typing import (\n    NamedTuple,\n    Sequence,\n    Optional,\n    Callable,\n    Any,\n)\n\nfrom dataclasses import dataclass\n\nfrom enum import Enum\n\n\n# LABELS\n\n\n@dataclass(frozen=True)\nclass StrLabel:\n    label: str\n\n\n@dataclass(frozen=True)\nclass LinkPropLabel:\n    label: str\n\n\nLabel = StrLabel | LinkPropLabel\n\n# DEFINE TYPES\n\n\n@dataclass(frozen=True)\nclass ObjectTp:\n    \"\"\"Object Type encapsulating val: Dict[str, ResultTp]\"\"\"\n\n    val: dict[str, ResultTp]\n\n    def __hash__(self):\n        return hash(tuple(self.val.items()))\n\n\n@dataclass(frozen=True)\nclass ScalarTp:\n    name: QualifiedName\n\n\ndef BoolTp():\n    return ScalarTp(QualifiedName([\"std\", \"bool\"]))\n\n\ndef StrTp():\n    return ScalarTp(QualifiedName([\"std\", \"str\"]))\n\n\ndef IntTp():\n    return ScalarTp(QualifiedName([\"std\", \"int64\"]))\n\n\ndef UuidTp():\n    return ScalarTp(QualifiedName([\"std\", \"uuid\"]))\n\n\nclass TpCastKind(Enum):\n    Implicit = \"implicit\"  # implicit includes assignment\n    Assignment = \"assignment\"\n    Explicit = \"explicit\"\n\n\n@dataclass(frozen=True)\nclass TpCast:\n    kind: TpCastKind\n    cast_fun: Callable[[Val], Val]\n\n\nclass CompositeTpKind(Enum):\n    Array = \"array\"\n    Tuple = \"tuple\"\n    Enum = \"enum\"\n    Range = \"range\"\n    MultiRange = \"multirange\"\n\n\n@dataclass(frozen=True)\nclass CompositeTp:\n    kind: CompositeTpKind\n    tps: list[Tp]\n    labels: list[str]\n\n    def __hash__(self):\n        return hash((self.kind, tuple(self.tps), tuple(self.labels)))\n\n    def __post_init__(self):\n        if not isinstance(self.labels, list):\n            raise ValueError(\"labels must be a list\")\n\n\ndef ArrTp(tp: Tp):\n    return CompositeTp(CompositeTpKind.Array, [tp], [])\n\n\ndef UnnamedTupleTp(tps: list[Tp]):\n    return CompositeTp(CompositeTpKind.Tuple, tps, [])\n\n\ndef NamedTupleTp(val: dict[str, Tp]):\n    lbls = [*val.keys()]\n    tps = [*val.values()]\n    return CompositeTp(CompositeTpKind.Tuple, tps, lbls)\n\n\n@dataclass(frozen=True)\nclass UnionTp:\n    left: Tp\n    right: Tp\n\n    def __post_init__(self):\n        if self.left == self.right:\n            raise ValueError(\"Do not use union tp for identical types\")\n\n\n@dataclass(frozen=True)\nclass IntersectTp:\n    left: Tp\n    right: Tp\n\n    def __post_init__(self):\n        if self.left == self.right:\n            raise ValueError(\"Do not use intersect tp for identical types\")\n\n\n@dataclass(frozen=True)\nclass NamedNominalLinkTp:\n    name: RawName\n    linkprop: ObjectTp\n\n    def __post_init__(self):\n        if not isinstance(self.linkprop, ObjectTp):\n            raise ValueError(\"linkprop must be an object type\")\n\n\n@dataclass(frozen=True)\nclass UncheckedTypeName:\n    name: RawName\n\n\n@dataclass(frozen=True)\nclass NominalLinkTp:\n    subject: ObjectTp\n    name: QualifiedName\n    linkprop: ObjectTp\n\n    def __post_init__(self):\n        if not isinstance(self.linkprop, ObjectTp):\n            raise ValueError(\"linkprop must be an object type\")\n\n\n@dataclass(frozen=True)\nclass ComputableTp:\n    expr: BindingExpr\n    tp: Tp\n\n\n# Computable Tp Pending Type Inference\n@dataclass(frozen=True)\nclass UncheckedComputableTp:\n    expr: BindingExpr\n\n\n@dataclass(frozen=True)\nclass OverloadedTargetTp:\n    \"\"\"place holder for a overloaded type\"\"\"\n\n    linkprop: Optional[ObjectTp]  # overloaded or additional link props\n\n\n@dataclass(frozen=True)\nclass DefaultTp:\n    expr: BindingExpr\n    tp: Tp\n\n    def __hash__(self) -> int:\n        return hash(self.tp)\n\n\n@dataclass(frozen=True)\nclass AnyTp:\n    specifier: Optional[str] = None\n\n\n@dataclass(frozen=True)\nclass SomeTp:\n    index: int\n\n\nTp = (\n    ObjectTp\n    | NamedNominalLinkTp\n    | NominalLinkTp\n    | ScalarTp\n    | UncheckedTypeName\n    | CompositeTp\n    | AnyTp\n    | SomeTp\n    | UnionTp\n    | IntersectTp\n    | OverloadedTargetTp\n    | ComputableTp\n    | DefaultTp\n    | UncheckedComputableTp\n)\n\n\n@dataclass(frozen=True)\nclass Visible:\n    pass\n\n\n@dataclass(frozen=True)\nclass Invisible:\n    pass\n\n\nMarker = Visible | Invisible\n\n\n# DEFINE CARDINALITIES\n\n\n@dataclass(frozen=True)\nclass ZeroCardinal:\n    def __add__(self, other):\n        return other\n\n    def __mul__(self, other: Cardinal):\n        assert not isinstance(\n            other, InfiniteCardinal\n        ), \"Cannot multiply zero by inf\"\n        return self\n\n    def __le__(self, other: Cardinal):\n        return True\n\n\n@dataclass(frozen=True)\nclass OneCardinal:\n    def __add__(self, other: Cardinal):\n        match other:\n            case ZeroCardinal():\n                return OneCardinal()\n            case OneCardinal():\n                return InfiniteCardinal()\n            case InfiniteCardinal():\n                return InfiniteCardinal()\n        raise ValueError()\n\n    def __mul__(self, other: Cardinal):\n        return other\n\n    def __le__(self, other: Cardinal):\n        match other:\n            case ZeroCardinal():\n                return False\n            case OneCardinal():\n                return True\n            case InfiniteCardinal():\n                return True\n        raise ValueError()\n\n\n@dataclass(frozen=True)\nclass InfiniteCardinal:\n    def __add__(self, other: Cardinal):\n        return InfiniteCardinal()\n\n    def __mul__(self, other: Cardinal):\n        assert not isinstance(\n            other, ZeroCardinal\n        ), \"cannot multiply zero by inf\"\n        return InfiniteCardinal()\n\n    def __le__(self, other: Cardinal):\n        match other:\n            case InfiniteCardinal():\n                return True\n            case OneCardinal():\n                return False\n            case ZeroCardinal():\n                return False\n        raise ValueError()\n\n\nCardinal = ZeroCardinal | OneCardinal | InfiniteCardinal\nLowerCardinal = ZeroCardinal | OneCardinal\nUpperCardinal = OneCardinal | InfiniteCardinal\n\nCardNumZero = ZeroCardinal()\nCardNumOne = OneCardinal()\nCardNumInf = InfiniteCardinal()\n\n\ndef max_cardinal(a: Cardinal, b: Cardinal):\n    if a <= b:\n        return b\n    else:\n        return a\n\n\ndef min_cardinal(a: Cardinal, b: Cardinal):\n    if a <= b:\n        return a\n    else:\n        return b\n\n\n@dataclass(frozen=True)\nclass CMMode:\n    lower: LowerCardinal\n    upper: UpperCardinal\n\n    def __add__(self, other: CMMode):\n        new_lower = self.lower + other.lower\n        return CMMode(\n            new_lower if new_lower != CardNumInf else CardNumOne,\n            self.upper + other.upper,\n        )\n\n    def __mul__(self, other: CMMode):\n        return CMMode(self.lower * other.lower, self.upper * other.upper)\n\n\nCardOne = CMMode(CardNumOne, CardNumOne)\nCardAtMostOne = CMMode(CardNumZero, CardNumOne)\nCardAtLeastOne = CMMode(CardNumOne, CardNumInf)\nCardAny = CMMode(CardNumZero, CardNumInf)\n\n\nclass ResultTp(NamedTuple):\n    tp: Tp\n    mode: CMMode\n\n\n# DEFINE PARAMETER MODIFIERS\n\n\n@dataclass(frozen=True)\nclass ParamSingleton:\n    pass\n\n\n@dataclass(frozen=True)\nclass ParamOptional:\n    pass\n\n\n@dataclass(frozen=True)\nclass ParamSetOf:\n    pass\n\n\nParamModifier = ParamSingleton | ParamOptional | ParamSetOf\n\n\n@dataclass(frozen=True)\nclass FunArgRetType:\n    args_tp: Sequence[Tp]\n    args_mod: Sequence[ParamModifier]\n    args_label: Sequence[str]\n    ret_tp: ResultTp\n\n\n@dataclass(frozen=True, order=True)\nclass ScalarVal:\n    tp: ScalarTp\n    val: Any\n\n    def __post_init__(self):\n        if self.tp == ScalarTp(\n            name=QualifiedName([\"std\", \"int64\"])\n        ) and not isinstance(self.val, int):\n            raise ValueError(\"val must be an int\")\n        if self.val is None:\n            raise ValueError(\"val cannot be None\")\n\n\ndef IntVal(val: int):\n    return ScalarVal(IntTp(), val)\n\n\ndef StrVal(val: str):\n    return ScalarVal(StrTp(), val)\n\n\ndef BoolVal(val: bool):\n    return ScalarVal(BoolTp(), val)\n\n\nEdgeID = int\n\n\ndef UuidVal(val: EdgeID):\n    return ScalarVal(UuidTp(), val)\n\n\n# DEFINE EXPRESSIONS\n\n\n@dataclass(frozen=True)\nclass UnionExpr:\n    left: Expr\n    right: Expr\n\n\n@dataclass(frozen=True)\nclass MultiSetExpr:\n    expr: Sequence[Expr]\n\n\n@dataclass(frozen=True)\nclass TypeCastExpr:\n    tp: Tp\n    arg: Expr\n\n\n@dataclass(frozen=True)\nclass CheckedTypeCastExpr:\n    cast_tp: tuple[Tp, Tp]\n    cast_spec: TpCast\n    arg: Expr\n\n\n@dataclass(frozen=True)\nclass ParameterExpr:\n    name: str\n    tp: Tp\n    is_required: bool\n\n\n@dataclass(frozen=True)\nclass FunAppExpr:\n    fun: UnqualifiedName | QualifiedName\n    overloading_index: Optional[int]\n    args: Sequence[Expr]\n    kwargs: dict[str, Expr]\n\n\n@dataclass(frozen=True)\nclass FreeObjectExpr:\n    pass\n\n\n@dataclass(frozen=True)\nclass ConditionalDedupExpr:\n    expr: Expr\n\n\n@dataclass(frozen=True)\nclass FreeVarExpr:\n    var: str\n\n\n@dataclass(frozen=True)\nclass BoundVarExpr:\n    var: str\n\n\n@dataclass(frozen=True)\nclass QualifiedName:\n    names: list[str]\n\n    def __hash__(self):\n        return hash(tuple(self.names))\n\n    def __post_init__(self):\n        if not isinstance(self.names, list) or not all(\n            isinstance(name, str) for name in self.names\n        ):\n            raise ValueError(\n                \"The 'names' attribute must be a non-empty list of strings.\"\n            )\n\n\n@dataclass(frozen=True)\nclass UnqualifiedName:\n    name: str\n\n\nRawName = UnqualifiedName | QualifiedName\n\n\n@dataclass(frozen=True)\nclass ObjectProjExpr:\n    subject: Expr\n    label: str\n\n\n@dataclass(frozen=True)\nclass TupleProjExpr:\n    subject: Expr\n    label: str\n\n\n@dataclass(frozen=True)\nclass BackLinkExpr:\n    subject: Expr\n    label: str\n\n\n@dataclass(frozen=True)\nclass IsTpExpr:\n    subject: Expr\n    tp: Tp | RawName\n\n\n@dataclass(frozen=True)\nclass TpIntersectExpr:\n    subject: Expr\n    tp: Tp | RawName\n\n\n@dataclass(frozen=True)\nclass LinkPropProjExpr:\n    subject: Expr\n    linkprop: str\n\n\n@dataclass(frozen=True)\nclass SubqueryExpr:  # select e in formalism\n    expr: Expr\n\n\n@dataclass(frozen=True)\nclass DetachedExpr:\n    expr: Expr\n\n\n@dataclass(frozen=True)\nclass WithExpr:\n    bound: Expr\n    next: BindingExpr\n\n\n@dataclass(frozen=True)\nclass ForExpr:\n    bound: Expr\n    next: BindingExpr\n\n\n@dataclass(frozen=True)\nclass OptionalForExpr:\n    bound: Expr\n    next: BindingExpr\n\n\n@dataclass(frozen=True)\nclass IfElseExpr:\n    then_branch: Expr\n    condition: Expr\n    else_branch: Expr\n\n\n@dataclass\nclass EdgeDatabaseEqFilter:\n    propname: str\n    # arg is a disjunction,\n    # both .propname = <arg> and .propanme IN <arg>\n    # are treated equivalently\n    arg: Expr | MultiSetVal\n\n\n@dataclass\nclass EdgeDatabaseConjunctiveFilter:\n    conjuncts: Sequence[EdgeDatabaseSelectFilter]\n\n\n@dataclass\nclass EdgeDatabaseDisjunctiveFilter:\n    disjuncts: Sequence[EdgeDatabaseSelectFilter]\n\n\n@dataclass\nclass EdgeDatabaseTrueFilter:\n    pass\n\n\nEdgeDatabaseSelectFilter = (\n    EdgeDatabaseEqFilter\n    | EdgeDatabaseConjunctiveFilter\n    | EdgeDatabaseDisjunctiveFilter\n    | EdgeDatabaseTrueFilter\n)\n\n\n@dataclass(frozen=True)\nclass QualifiedNameWithFilter:\n    name: QualifiedName\n    filter: EdgeDatabaseSelectFilter\n\n\n@dataclass(frozen=True)\nclass FilterOrderExpr:\n    subject: Expr\n    filter: BindingExpr\n    order: dict[str, BindingExpr]  # keys are order-specifying list\n\n\n@dataclass(frozen=True)\nclass OffsetLimitExpr:\n    subject: Expr\n    offset: Expr\n    limit: Expr\n\n\n@dataclass(frozen=True)\nclass InsertExpr:\n    name: UnqualifiedName | QualifiedName\n    new: dict[str, Expr]\n\n\n@dataclass(frozen=True)\nclass UpdateExpr:\n    subject: Expr\n    shape: ShapeExpr\n\n\n@dataclass(frozen=True)\nclass DeleteExpr:\n    subject: Expr\n\n\n@dataclass(frozen=True)\nclass ShapedExprExpr:\n    expr: Expr\n    shape: ShapeExpr\n\n\n@dataclass(frozen=True)\nclass BindingExpr:\n    var: str\n    body: Expr\n\n\n@dataclass(frozen=True)\nclass ShapeExpr:\n    shape: dict[Label, BindingExpr]\n\n\n@dataclass(frozen=True)\nclass UnnamedTupleExpr:\n    val: Sequence[Expr]\n\n\n@dataclass(frozen=True)\nclass NamedTupleExpr:\n    val: dict[str, Expr]\n\n\n@dataclass(frozen=True)\nclass ArrExpr:\n    elems: Sequence[Expr]\n\n\n# VALUES\n\n\n@dataclass(frozen=True)\nclass ObjectVal:\n    val: dict[Label, tuple[Marker, MultiSetVal]]\n\n    def __post_init__(self):\n        for lbl, (marker, val) in self.val.items():\n            if not isinstance(val, MultiSetVal):\n                raise ValueError(\"val must be a MultiSetVal\")\n            if not isinstance(marker, Marker):  # type: ignore\n                raise ValueError(\"marker must be a Marker\")\n            if not isinstance(lbl, Label):  # type: ignore\n                raise ValueError(\"label must be a Label\")\n\n\n@dataclass(frozen=True)\nclass RefVal:\n    refid: int\n    tpname: QualifiedName\n    val: ObjectVal\n\n    def __post_init__(self):\n        if not isinstance(self.val, ObjectVal):\n            raise ValueError(\"val must be an ObjectVal\")\n        if not isinstance(self.tpname, QualifiedName):\n            raise ValueError(\"tpname must be a QualifiedName\")\n\n\n@dataclass(frozen=True, order=True)\nclass UnnamedTupleVal:\n    val: Sequence[Val]\n\n\n@dataclass(frozen=True)\nclass NamedTupleVal:\n    val: dict[str, Val]\n\n\n@dataclass(frozen=True)\nclass ArrVal:\n    val: Sequence[Val]\n\n\n@dataclass(frozen=True, order=True)\nclass ResultMultiSetVal:\n    _vals: Sequence[Val]\n    # singleton: bool = False\n\n    def getVals(self) -> Sequence[Val]:\n        return self._vals\n\n    def getRawVals(self) -> Sequence[Val]:\n        return self._vals\n\n    def __post_init__(self):\n        if not isinstance(self._vals, list) or not all(\n            isinstance(v, Val) for v in self._vals  # type: ignore\n        ):\n            raise ValueError(\"vals must be a list\")\n\n\nMultiSetVal = ResultMultiSetVal\n\n\nVal = ScalarVal | RefVal | UnnamedTupleVal | NamedTupleVal | ArrVal\n\n\nVarExpr = FreeVarExpr | BoundVarExpr\n\nExpr = (\n    ScalarVal\n    | TypeCastExpr\n    | FunAppExpr\n    | FreeVarExpr\n    | BoundVarExpr\n    | CheckedTypeCastExpr\n    | ObjectProjExpr\n    | LinkPropProjExpr\n    | WithExpr\n    | ForExpr\n    | OptionalForExpr\n    | TpIntersectExpr\n    | BackLinkExpr\n    | FilterOrderExpr\n    | OffsetLimitExpr\n    | QualifiedNameWithFilter\n    | InsertExpr\n    | UpdateExpr\n    | MultiSetExpr\n    | ShapedExprExpr\n    | ShapeExpr\n    | FreeObjectExpr\n    | ConditionalDedupExpr\n    | TupleProjExpr\n    | IsTpExpr\n    | BindingExpr\n    | Val\n    | UnnamedTupleExpr\n    | NamedTupleExpr\n    | ParameterExpr\n    | ArrExpr\n    | Tp\n    | UnionExpr\n    | DetachedExpr\n    | SubqueryExpr\n    | IfElseExpr\n    | DeleteExpr\n    | QualifiedName\n)\n\n\n@dataclass(frozen=True)\nclass DBEntry:\n    tp: QualifiedName\n    data: dict[str, MultiSetVal]\n\n\n@dataclass(frozen=True)\nclass DB:\n    dbdata: dict[int, DBEntry]\n\n\n@dataclass(frozen=True)\nclass BuiltinFuncDef:\n    tp: FunArgRetType\n    impl: Callable[[Sequence[Sequence[Val]]], Sequence[Val]]\n    defaults: dict[str, Expr]\n\n\n@dataclass(frozen=True)\nclass DefinedFuncDef:\n    tp: FunArgRetType\n    impl: Expr  # Has the same number of bindings as num_args = len(tp.args_tp)\n    defaults: dict[str, Expr]\n\n\nFuncDef = BuiltinFuncDef | DefinedFuncDef\n\n\n@dataclass(frozen=True)\nclass ExclusiveConstraint:\n    name: str\n    delegated: bool\n\n\n@dataclass(frozen=True)\nclass ExpressionConstraint:\n    expr: Expr\n\n\nConstraint = ExclusiveConstraint | ExpressionConstraint\n\n\n@dataclass(frozen=True)\nclass ModuleEntityTypeDef:\n    typedef: ObjectTp | ScalarTp\n    is_abstract: bool\n    constraints: Sequence[Constraint]\n    # Indexes are a list of indexed properties (as a tuple),\n    # e.g. if a type has (.a), (.a, .b) as indexes, we have [[.a], [.a, .b]]\n    indexes: Sequence[Sequence[str]]\n\n\n@dataclass(frozen=True)\nclass ModuleEntityFuncDef:\n    funcdefs: list[FuncDef]\n\n\nModuleEntity = ModuleEntityTypeDef | ModuleEntityFuncDef\n\n\n@dataclass(frozen=True)\nclass DBModule:\n    defs: dict[str, ModuleEntity]\n\n\nModuleName = tuple[str, ...]\n\n\n@dataclass(frozen=True)\nclass DBSchema:\n    modules: dict[tuple[str, ...], DBModule]\n\n    # modules that are currently under type checking\n    unchecked_modules: dict[tuple[str, ...], DBModule]\n\n    # subtyping_relations: indexed by subtypes,\n    # subtype -> immediate super types mapping\n    subtyping_relations: dict[QualifiedName, list[QualifiedName]]\n    unchecked_subtyping_relations: dict[\n        QualifiedName, list[tuple[tuple[str, ...], RawName]]\n    ]  # name -> current declared module and raw name\n    casts: dict[tuple[Tp, Tp], TpCast]\n\n\nclass RTExpr(NamedTuple):\n    cur_db: DB\n    expr: Expr\n\n\nclass RTVal(NamedTuple):\n    cur_db: DB\n    val: MultiSetVal\n\n\n@dataclass\nclass TcCtx:\n    schema: DBSchema\n    current_module: tuple[\n        str, ...\n    ]  # current module name, TODO: nested modules\n    varctx: dict[str, ResultTp]\n\n\nstarting_id = 0\n\n\ndef next_id():\n    global starting_id\n    starting_id += 1\n    return starting_id\n\n\ndef next_name(prefix: str = \"n\") -> str:\n    return prefix + str(next_id())\n\n\ndef ref(id):\n    return RefVal(id, {})\n\n\nOrderLabelSep = \"-\"  # separates components of an order object label\nOrderAscending = \"ascending\"\nOrderDescending = \"descending\"\nOrderEmptyFirst = \"emptyfirst\"\nOrderEmptyLast = \"emptylast\"\n\n\nIndirectionIndexOp = \"_[_]\"\nIndirectionSliceStartStopOp = \"_[_:_]\"\nIndirectionSliceStartOp = \"_[_:]\"\nIndirectionSliceStopOp = \"_[:_]\"\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/data/deduplication_insert.py",
    "content": "\"\"\"\nThis file inserts\nconditional deduplicaiton statements into the EdgeQL query language.\n\nA projection M.l is not deduplicated if\nthere are eventually a link property projection M.l.....@l\n, otherwise it is deduplicated.\n\n\"\"\"\n\nfrom . import data_ops as e\nfrom . import expr_ops as eops\nfrom typing import Optional\n\n\ndef insert_conditional_dedup(expr: e.Expr):\n\n    def do_recursive_insert(sub: e.Expr) -> e.Expr:\n        match sub:\n            case e.ObjectProjExpr(subject=subject, label=label):\n                return e.ConditionalDedupExpr(\n                    e.ObjectProjExpr(\n                        subject=do_recursive_insert(subject), label=label\n                    )\n                )\n            case _:\n                return insert_conditional_dedup(sub)\n\n    def do_not_recursive_insert(sub: e.Expr) -> e.Expr:\n        match sub:\n            case e.LinkPropProjExpr(subject=subject, linkprop=linkprop):\n                return e.LinkPropProjExpr(\n                    subject=do_not_recursive_insert(subject), linkprop=linkprop\n                )\n            case e.TpIntersectExpr(subject=subject, tp=tp):\n                return e.TpIntersectExpr(\n                    subject=do_not_recursive_insert(subject), tp=tp\n                )\n            case e.ObjectProjExpr(subject=subject, label=label):\n                return e.ObjectProjExpr(\n                    subject=insert_conditional_dedup(subject), label=label\n                )\n            case e.BackLinkExpr(subject=subject, label=label):\n                return e.BackLinkExpr(\n                    subject=insert_conditional_dedup(subject), label=label\n                )\n            case _:\n                return insert_conditional_dedup(sub)\n\n    def insert_function_top(sub: e.Expr) -> Optional[e.Expr]:\n        if isinstance(sub, e.LinkPropProjExpr):\n            return do_not_recursive_insert(sub)\n        elif isinstance(sub, e.ObjectProjExpr):\n            if sub.label == \"__edgedb_reserved_subject__\":\n                return do_not_recursive_insert(sub)\n            else:\n                return do_recursive_insert(sub)\n        else:\n            return None\n\n    result = eops.map_expr(insert_function_top, expr)\n    return result\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/data/expr_ops.py",
    "content": "from typing import Callable, Optional, Sequence, cast\nimport typing\n\nfrom .data_ops import (\n    ArrExpr,\n    ArrVal,\n    BackLinkExpr,\n    BindingExpr,\n    BoundVarExpr,\n    DetachedExpr,\n    Expr,\n    FilterOrderExpr,\n    ForExpr,\n    FreeVarExpr,\n    FunAppExpr,\n    InsertExpr,\n    LinkPropLabel,\n    LinkPropProjExpr,\n    MultiSetExpr,\n    MultiSetVal,\n    NamedTupleExpr,\n    ObjectProjExpr,\n    ObjectVal,\n    OffsetLimitExpr,\n    OptionalForExpr,\n    RefVal,\n    ShapedExprExpr,\n    ShapeExpr,\n    StrLabel,\n    SubqueryExpr,\n    Tp,\n    TpIntersectExpr,\n    TypeCastExpr,\n    UnionExpr,\n    UnnamedTupleExpr,\n    UnnamedTupleVal,\n    UpdateExpr,\n    Val,\n    VarExpr,\n    WithExpr,\n    next_name,\n)\nfrom . import data_ops as e\nfrom ..interpreter_logging import print_warning\n\n\ndef map_tp(f: Callable[[Tp], Optional[Tp]], tp: Tp) -> Tp:\n    \"\"\"maps a function over free variables and bound variables,\n    and does not modify other nodes\n\n    f : called with current expression and the current level, which refers to\n        the first binder outside of the entire expression expr\n    level : this value refers to the first binder OUTSIDE of the expression\n            being mapped, it should be called with initially = 1.\n            Increases as we encounter abstractions\n    \"\"\"\n\n    tentative = f(tp)\n    if tentative is not None:\n        return tentative\n    else:\n\n        def recur(expr):\n            return map_tp(f, expr)\n\n        match tp:\n            case e.ScalarTp() | e.AnyTp():\n                return tp\n            case e.ObjectTp(val=val):\n                return e.ObjectTp(\n                    val={\n                        k: e.ResultTp(recur(v), card)\n                        for k, (v, card) in val.items()\n                    }\n                )\n            case e.CompositeTp(kind=k, tps=tps, labels=labels):\n                return e.CompositeTp(\n                    kind=k, tps=[recur(v) for v in tps], labels=labels\n                )\n            case e.NamedNominalLinkTp(name=name, linkprop=linkprop):\n                return e.NamedNominalLinkTp(\n                    name=name, linkprop=recur(linkprop)\n                )\n            # case e.UncheckedNamedNominalLinkTp(name=name, linkprop=linkprop):\n            #     return e.UncheckedNamedNominalLinkTp(name=name,\n            #                         linkprop=recur(linkprop))\n            case e.NominalLinkTp(\n                name=name, subject=subject, linkprop=linkprop\n            ):\n                return e.NominalLinkTp(\n                    name=name, subject=recur(subject), linkprop=recur(linkprop)\n                )\n            case e.UncheckedComputableTp(_):\n                return tp\n            case e.ComputableTp(expr=expr, tp=tp):\n                return e.ComputableTp(expr=expr, tp=recur(tp))\n            case e.DefaultTp(expr=expr, tp=tp):\n                return e.DefaultTp(expr=expr, tp=recur(tp))\n            case e.UncheckedTypeName(name=name):\n                return tp\n            case e.UnionTp(l, r):\n                return e.UnionTp(recur(l), recur(r))\n            case _:\n                raise ValueError(\"Not Implemented\", tp)\n\n\ndef map_edge_select_filter(\n    f: Callable[[Expr], Optional[Expr]],\n    expr: e.Expr | e.EdgeDatabaseSelectFilter,\n) -> e.Expr | e.EdgeDatabaseSelectFilter:\n    tentative = f(expr)  # type: ignore[arg-type]\n    if tentative is not None:\n        return tentative\n    else:\n        match expr:\n            case e.EdgeDatabaseConjunctiveFilter(filters):\n                new_filters = [\n                    map_edge_select_filter(f, filter) for filter in filters\n                ]\n                return e.EdgeDatabaseConjunctiveFilter(new_filters)  # type: ignore[arg-type]\n            case e.EdgeDatabaseDisjunctiveFilter(filters):\n                new_filters = [\n                    map_edge_select_filter(f, filter) for filter in filters\n                ]\n                return e.EdgeDatabaseDisjunctiveFilter(new_filters)  # type: ignore[arg-type]\n            case e.EdgeDatabaseEqFilter(label, arg):\n                return e.EdgeDatabaseEqFilter(\n                    label, map_edge_select_filter(f, arg)  # type: ignore[arg-type]\n                )\n            case _:\n                assert not isinstance(expr, e.EdgeDatabaseSelectFilter)  # type: ignore[arg-type]\n                return expr\n\n\ndef map_expr(f: Callable[[Expr], Optional[Expr]], expr: Expr) -> Expr:\n    \"\"\"maps a function over free variables and bound variables,\n    and does not modify other nodes\n\n    f : called with current expression and the current level, which refers to\n        the first binder outside of the entire expression expr\n    level : this value refers to the first binder OUTSIDE of the expression\n            being mapped, it should be called with initially = 1.\n            Increases as we encounter abstractions\n    \"\"\"\n    tentative = f(expr)\n    if tentative is not None:\n        return tentative\n    else:\n\n        def recur_tp(expr):\n            def f_tp(tp: Tp) -> Optional[Tp]:\n                result = f(tp)\n                # if result is not None:\n                #     assert isinstance(result, Tp)\n                return cast(Optional[Tp], result)\n\n            return map_tp(f_tp, expr)\n\n        def recur(expr):\n            return map_expr(f, expr)\n\n        match expr:\n            case (\n                FreeVarExpr(_)\n                | BoundVarExpr(_)\n                | e.ScalarVal(_)\n                | RefVal(_)\n                | ArrVal(_)\n                | UnnamedTupleVal(_)\n                | e.QualifiedName(_)\n                | e.UnqualifiedName(_)\n                | e.QualifiedNameWithFilter(_, _)\n            ):\n                return expr\n            case BindingExpr(var=var, body=body):\n                return BindingExpr(var=var, body=map_expr(f, body))\n            case UnnamedTupleExpr(val=val):\n                return UnnamedTupleExpr(val=[recur(e) for e in val])\n            case NamedTupleExpr(val=val):\n                return NamedTupleExpr(\n                    val={k: recur(e) for (k, e) in val.items()}\n                )\n            case ObjectProjExpr(subject=subject, label=label):\n                return ObjectProjExpr(subject=recur(subject), label=label)\n            case e.TupleProjExpr(subject=subject, label=label):\n                return e.TupleProjExpr(subject=recur(subject), label=label)\n            case BackLinkExpr(subject=subject, label=label):\n                return BackLinkExpr(subject=recur(subject), label=label)\n            case e.IsTpExpr(subject=subject, tp=tp):\n                return e.IsTpExpr(subject=recur(subject), tp=tp)\n            case TpIntersectExpr(subject=subject, tp=tp_name):\n                return TpIntersectExpr(subject=recur(subject), tp=tp_name)\n            case LinkPropProjExpr(subject=subject, linkprop=label):\n                return LinkPropProjExpr(subject=recur(subject), linkprop=label)\n            case FunAppExpr(\n                fun=fname, args=args, overloading_index=idx, kwargs=kwargs\n            ):\n                return FunAppExpr(\n                    fun=fname,\n                    args=[recur(arg) for arg in args],\n                    overloading_index=idx,\n                    kwargs={k: recur(v) for (k, v) in kwargs.items()},\n                )\n            case FilterOrderExpr(subject=subject, filter=filter, order=order):\n                return FilterOrderExpr(\n                    subject=recur(subject),\n                    filter=recur(filter),\n                    order={l: recur(o) for (l, o) in order.items()},\n                )\n            case ShapedExprExpr(expr=expr, shape=shape):\n                return ShapedExprExpr(expr=recur(expr), shape=recur(shape))\n            case ShapeExpr(shape=shape):\n                return ShapeExpr(\n                    shape={k: recur(e_1) for (k, e_1) in shape.items()}\n                )\n            case TypeCastExpr(tp=tp, arg=arg):\n                return TypeCastExpr(tp=recur_tp(tp), arg=recur(arg))\n            case UnionExpr(left=left, right=right):\n                return UnionExpr(left=recur(left), right=recur(right))\n            case ArrExpr(elems=arr):\n                return ArrExpr(elems=[recur(e) for e in arr])\n            case MultiSetExpr(expr=arr):\n                return MultiSetExpr(expr=[recur(e) for e in arr])\n            case OffsetLimitExpr(subject=subject, offset=offset, limit=limit):\n                return OffsetLimitExpr(\n                    subject=recur(subject),\n                    offset=recur(offset),\n                    limit=recur(limit),\n                )\n            case WithExpr(bound=bound, next=next):\n                return WithExpr(bound=recur(bound), next=recur(next))\n            case InsertExpr(name=name, new=new):\n                return InsertExpr(\n                    name=name, new={k: recur(v) for (k, v) in new.items()}\n                )\n            case e.FreeObjectExpr():\n                return e.FreeObjectExpr()\n            case e.ConditionalDedupExpr(expr=sub):\n                return e.ConditionalDedupExpr(recur(sub))\n            # case ObjectExpr(val=val):\n            #     return ObjectExpr(\n            #         val={label: recur(item)\n            #              for (label, item) in val.items()})\n            case DetachedExpr(expr=expr):\n                return DetachedExpr(expr=recur(expr))\n            case SubqueryExpr(expr=expr):\n                return SubqueryExpr(expr=recur(expr))\n            case UpdateExpr(subject=subject, shape=shape):\n                return UpdateExpr(subject=recur(subject), shape=recur(shape))\n            case e.DeleteExpr(subject=subject):\n                return e.DeleteExpr(subject=recur(subject))\n            case ForExpr(bound=bound, next=next):\n                return ForExpr(bound=recur(bound), next=recur(next))\n            case OptionalForExpr(bound=bound, next=next):\n                return OptionalForExpr(bound=recur(bound), next=recur(next))\n            case e.IfElseExpr(\n                then_branch=then_branch,\n                condition=condition,\n                else_branch=else_branch,\n            ):\n                return e.IfElseExpr(\n                    then_branch=recur(then_branch),\n                    condition=recur(condition),\n                    else_branch=recur(else_branch),\n                )\n            case e.CheckedTypeCastExpr(\n                cast_tp=cast_tp,\n                cast_spec=cast_spec,\n                arg=arg,\n            ):\n                return e.CheckedTypeCastExpr(\n                    cast_tp=cast_tp, cast_spec=cast_spec, arg=recur(arg)\n                )\n            case e.ParameterExpr(name=name, tp=tp, is_required=is_required):\n                return e.ParameterExpr(\n                    name=name, tp=recur_tp(tp), is_required=is_required\n                )\n            case _:\n                return map_tp(f, expr)  # type: ignore[arg-type]\n    raise ValueError(\"Not Implemented: map_expr \", expr)\n\n\ndef map_var(f: Callable[[VarExpr], Optional[Expr]], expr: Expr) -> Expr:\n    \"\"\"maps a function over free variables and bound variables,\n    and does not modify other nodes\n\n    f : if not None, replace with the result\n    \"\"\"\n\n    def map_func(e: Expr) -> Optional[Expr]:\n        match e:\n            case FreeVarExpr(var=_):\n                return f(e)\n            case BoundVarExpr(var=_):\n                return f(e)\n        return None\n\n    return map_expr(map_func, expr)\n\n\ndef get_free_vars(e: Expr) -> Sequence[str]:\n    res: Sequence[str] = []\n\n    def map_var_func(ve: VarExpr) -> None:\n        nonlocal res\n        if isinstance(ve, FreeVarExpr):\n            if ve.var not in res:\n                res = [*res, ve.var]\n        return None\n\n    map_var(map_var_func, e)\n    return res\n\n\ndef ensure_no_capture(\n    avoid_list: Sequence[str], e: BindingExpr\n) -> BindingExpr:\n    assert isinstance(e, BindingExpr)\n    candidate_name = e.var\n    while candidate_name in avoid_list:\n        candidate_name = next_name(candidate_name)\n    if candidate_name != e.var:\n        return abstract_over_expr(\n            instantiate_expr(FreeVarExpr(candidate_name), e), candidate_name\n        )\n    else:\n        return e\n\n\ndef instantiate_expr(e2: Expr, bnd_expr: BindingExpr) -> Expr:\n    if not isinstance(bnd_expr, BindingExpr):\n        raise ValueError(bnd_expr)\n\n    result = subst_expr_for_expr(e2, BoundVarExpr(bnd_expr.var), bnd_expr.body)\n\n    return result\n\n\ndef subst_expr_for_expr(expr2: Expr, replace: Expr, subject: Expr):\n    assert not isinstance(replace, BindingExpr)\n\n    e2_free_vars = get_free_vars(expr2)\n\n    def map_func(candidate: Expr) -> Optional[Expr]:\n        if candidate == replace:\n            return expr2\n        elif isinstance(candidate, BindingExpr):\n            # shortcut : if we are substituting a free var and a\n            # binder binds that var, then we can early stop.\n            # The reason is that the var will not occur after alpha renaming\n            match replace:\n                case BoundVarExpr(v) | FreeVarExpr(v):\n                    if v == candidate.var:\n                        return candidate\n\n            # otherwise we need to ensure that no variable is captured\n            no_capture_cand = ensure_no_capture(e2_free_vars, candidate)\n\n            return BindingExpr(\n                var=no_capture_cand.var,\n                body=subst_expr_for_expr(expr2, replace, no_capture_cand.body),\n            )\n        else:\n            return None\n\n    return map_expr(map_func, subject)\n\n\ndef abstract_over_expr(expr: Expr, var: Optional[str] = None) -> BindingExpr:\n    \"\"\"Construct a BindingExpr that binds var\"\"\"\n\n    if var is None:\n        var = next_name()\n\n    new_body = subst_expr_for_expr(BoundVarExpr(var), FreeVarExpr(var), expr)\n\n    return BindingExpr(var=var, body=new_body)\n\n\ndef iterative_subst_expr_for_expr(\n    expr2: Sequence[Expr], replace: Sequence[Expr], subject: Expr\n):\n    \"\"\"Iteratively perform substitution from right to left,\n    comptues: [expr2[0]/replace[0]]...[expr[n-1]/replace[n-1]]subject\"\"\"\n\n    assert len(expr2) == len(replace)\n    result = subject\n    for i in reversed(list(range(len(replace)))):\n        result = subst_expr_for_expr(expr2[i], replace[i], result)\n    return result\n\n\ndef appears_in_expr_pred(\n    search_pred: Callable[[Expr], bool], subject: Expr\n) -> bool:\n\n    class ReturnTrue(Exception):\n        pass\n\n    def map_func(candidate: Expr) -> Optional[Expr]:\n        if search_pred(candidate):\n            raise ReturnTrue()\n        else:\n            return None\n\n    try:\n        map_expr(map_func, subject)\n    except ReturnTrue:\n        return True\n    return False\n\n\ndef count_appearances_in_expr(search: Expr, subject: Expr):\n\n    expr_is_var: Optional[str]\n    match search:\n        case FreeVarExpr(vname):\n            expr_is_var = vname\n        case BoundVarExpr(vname):\n            expr_is_var = vname\n        case _:\n            expr_is_var = None\n\n    appearances = 0\n\n    def map_func(candidate: Expr) -> Optional[Expr]:\n        nonlocal appearances\n        if (\n            expr_is_var is not None\n            and isinstance(candidate, BindingExpr)\n            and candidate.var == expr_is_var\n        ):\n\n            # terminate search here\n            return candidate\n        if candidate == search:\n            appearances += 1\n            return None\n        else:\n            return None\n\n    map_expr(map_func, subject)\n\n    return appearances\n\n\ndef appears_in_expr(search: Expr, subject: Expr):\n    class ReturnTrue(Exception):\n        pass\n\n    expr_is_var: Optional[str]\n    match search:\n        case FreeVarExpr(vname):\n            expr_is_var = vname\n        case BoundVarExpr(vname):\n            expr_is_var = vname\n        case _:\n            expr_is_var = None\n\n    def map_func(candidate: Expr) -> Optional[Expr]:\n        if (\n            expr_is_var is not None\n            and isinstance(candidate, BindingExpr)\n            and candidate.var == expr_is_var\n        ):\n\n            # terminate search here\n            return candidate\n        if candidate == search:\n            raise ReturnTrue()\n        else:\n            return None\n\n    try:\n        map_expr(map_func, subject)\n    except ReturnTrue:\n        return True\n    return False\n\n\ndef binding_is_unnamed(expr: BindingExpr) -> bool:\n    return not appears_in_expr(BoundVarExpr(expr.var), expr.body)\n\n\ndef operate_under_binding(e: BindingExpr, op: Callable[[Expr], Expr]):\n    name = next_name()\n    return abstract_over_expr(op(instantiate_expr(FreeVarExpr(name), e)), name)\n\n\ndef val_is_primitive(rt: Val) -> bool:\n    match rt:\n        case e.ScalarVal(_):\n            return True\n        case RefVal(_):\n            return False\n    raise ValueError(\"not implemented\")\n\n\ndef val_is_ref_val(rt: Val) -> bool:\n    match rt:\n        case RefVal(_):\n            return True\n    return False\n\n\ndef remove_unless_link_props(dic: ObjectVal) -> ObjectVal:\n    return ObjectVal(\n        val={\n            k: v for (k, v) in dic.val.items() if isinstance(k, LinkPropLabel)\n        }\n    )\n\n\ndef conversion_error():\n    class ConversionError(Exception):\n        pass\n\n    raise ConversionError\n\n\ndef obj_to_link_prop_obj(dic: ObjectVal) -> ObjectVal:\n    return ObjectVal(\n        val={\n            (\n                LinkPropLabel(k.label)\n                if isinstance(k, StrLabel)\n                else conversion_error()\n            ): v\n            for (k, v) in dic.val.items()\n        }\n    )\n\n\ndef link_prop_obj_to_obj(dic: ObjectVal) -> ObjectVal:\n    return ObjectVal(\n        val={\n            (\n                StrLabel(k.label)\n                if isinstance(k, LinkPropLabel)\n                else conversion_error()\n            ): v\n            for (k, v) in dic.val.items()\n        }\n    )\n\n\ndef combine_object_val(o1: ObjectVal, o2: ObjectVal) -> ObjectVal:\n    return ObjectVal({**o1.val, **o2.val})\n\n\ndef object_dedup(val: Sequence[Val]) -> Sequence[Val]:\n    temp: dict[int, RefVal] = {}\n    for v in val:\n        match v:\n            case RefVal(refid=id, tpname=tpname, val=_):\n                if id in temp:\n                    temp[id] = RefVal(\n                        refid=id,\n                        tpname=tpname,\n                        val=combine_object_val(temp[id].val, v.val),\n                    )\n                else:\n                    temp[id] = v\n            # case FreeVal(_):\n            #     # Should link dedup apply to free objects?\n            #     temp[next_id()] = v\n            case _:\n                raise ValueError(\"must pass in objects\")\n    return list(temp.values())\n\n\ndef map_expand_multiset_val(\n    sv: Sequence[MultiSetVal],\n) -> Sequence[Sequence[Val]]:\n    return [v.getVals() for v in sv]\n\n\ndef val_is_link_convertible(val: Val) -> bool:\n    match val:\n        case RefVal(refid=_, val=obj):\n            return all(\n                [isinstance(label, LinkPropLabel) for label in obj.val.keys()]\n            )\n        case _:\n            return False\n\n\n# def convert_to_link(val: Val) -> LinkPropVal:\n#     assert val_is_link_convertible(val)\n#     match val:\n#         case RefVal(refid=id, val=obj):\n#             return LinkPropVal(refid=id,\n#                 linkprop=obj)\n#         case _:\n#             raise ValueError(\"Val is not link convertible, check va\")\n\n\ndef is_path(e: Expr) -> bool:\n    match e:\n        case FreeVarExpr(_):\n            return True\n        case LinkPropProjExpr(subject=subject, linkprop=_):\n            return is_path(subject)\n        case ObjectProjExpr(subject=subject, label=_):\n            return is_path(subject)\n        case BackLinkExpr(subject=subject, label=_):\n            return is_path(subject)\n        case TpIntersectExpr(\n            subject=BackLinkExpr(subject=subject, label=_), tp=_\n        ):\n            return is_path(subject)\n        case _:\n            return False\n\n\ndef get_path_head(e: Expr) -> e.FreeVarExpr:\n    match e:\n        case FreeVarExpr(_):\n            return e\n        case LinkPropProjExpr(subject=subject, linkprop=_):\n            return get_path_head(subject)\n        case ObjectProjExpr(subject=subject, label=_):\n            return get_path_head(subject)\n        case BackLinkExpr(subject=subject, label=_):\n            return get_path_head(subject)\n        case TpIntersectExpr(\n            subject=BackLinkExpr(subject=subject, label=_), tp=_\n        ):\n            return get_path_head(subject)\n        case _:\n            raise ValueError(\"not a path\")\n\n\ndef get_first_path_component(e: Expr) -> e.Optional[e.Expr]:\n    match e:\n        case FreeVarExpr(_):\n            return None\n        case LinkPropProjExpr(subject=FreeVarExpr(_), linkprop=_):\n            return e\n        case ObjectProjExpr(subject=FreeVarExpr(_), label=_):\n            return e\n        case BackLinkExpr(subject=FreeVarExpr(_), label=_):\n            return e\n        case TpIntersectExpr(\n            subject=BackLinkExpr(subject=FreeVarExpr(_), label=_), tp=_\n        ):\n            return e\n        case _:\n            raise ValueError(\"not a path\")\n\n\ndef tcctx_add_binding(\n    ctx: e.TcCtx, bnd_e: BindingExpr, binder_tp: e.ResultTp\n) -> tuple[e.TcCtx, Expr, str]:\n    bnd_e = ensure_no_capture(\n        list(get_free_vars(bnd_e)) + list(ctx.varctx.keys()), bnd_e\n    )\n    new_ctx = e.TcCtx(\n        ctx.schema, ctx.current_module, {**ctx.varctx, bnd_e.var: binder_tp}\n    )\n    after_e = instantiate_expr(e.FreeVarExpr(bnd_e.var), bnd_e)\n    return new_ctx, after_e, bnd_e.var\n\n\ndef emtpy_tcctx_from_dbschema(\n    dbschema: e.DBSchema, current_module_name: tuple[str, ...] = (\"std\",)\n) -> e.TcCtx:\n    return e.TcCtx(\n        schema=dbschema, current_module=current_module_name, varctx={}\n    )\n\n\ndef is_effect_free(expr: Expr) -> bool:\n    def pred(expr: Expr) -> bool:\n        if (\n            isinstance(expr, e.InsertExpr)\n            or isinstance(expr, e.UpdateExpr)\n            or isinstance(expr, e.DeleteExpr)\n        ):\n            return True\n        else:\n            return False\n\n    return not appears_in_expr_pred(pred, expr)\n\n\ndef collect_names_in_select_filter(\n    filter: e.EdgeDatabaseSelectFilter,\n) -> list[str]:\n    res: list[str] = []\n\n    def map_func(candidate: Expr) -> Optional[Expr]:\n        if isinstance(candidate, e.EdgeDatabaseEqFilter):\n            res.append(candidate.propname)\n        return None\n\n    map_edge_select_filter(map_func, filter)\n    return res\n\n\n@typing.no_type_check\ndef val_eq(v1: e.Val, v2: e.Val) -> bool:\n    match v1, v2:\n        case e.ScalarVal(t1, v1), e.ScalarVal(t2, v2):\n            if t1 != t2:\n                print_warning(\n                    f\"Warning: comparing different types {t1} and {t2}\"\n                )\n            return v1 == v2\n        case e.RefVal(id1, v1), e.RefVal(id2, v2):\n            return id1 == id2\n        case e.UnnamedTupleVal(v1), e.UnnamedTupleVal(v2):\n            if len(v1) != len(v2):\n                return False\n            else:\n                return all(val_eq(v1[i], v2[i]) for i in range(len(v1)))\n        case e.NamedTupleVal(v1), e.NamedTupleVal(v2):\n            if len(v1) != len(v2) or v1.keys() != v2.keys():\n                return False\n            else:\n                return all(val_eq(v1[k], v2[k]) for k in v1.keys())\n        case _:\n            if v1.__class__ == v2.__class__:\n                raise ValueError(\"Not Implemented\", v1, v2)\n            else:\n                return False\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/data/expr_to_str.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any\n\nfrom . import data_ops as e\nfrom . import expr_ops as eops\n\n\ndef show_card(card: e.Cardinal) -> str:\n    match card:\n        case e.ZeroCardinal():\n            return \"0\"\n        case e.OneCardinal():\n            return \"1\"\n        case e.InfiniteCardinal():\n            return \"∞\"\n        case _:\n            raise ValueError('Unimplemented', card)\n\n\ndef show_cmmode(mode: e.CMMode) -> str:\n    return \"[\" + show_card(mode.lower) + \",\" + show_card(mode.upper) + \"]\"\n\n\ndef show_qname(name: e.QualifiedName) -> str:\n    return \"::\".join(name.names)\n\n\ndef show_raw_name(name: e.QualifiedName | e.UnqualifiedName) -> str:\n    if isinstance(name, e.QualifiedName):\n        return show_qname(name)\n    else:\n        return name.name\n\n\ndef show_tp(tp: e.Tp | e.RawName) -> str:\n    match tp:\n        case e.ObjectTp(val=tp_val):\n            return (\n                '{'\n                + ', '.join(\n                    lbl + \": \" + show_tp(md_tp.tp) + show_cmmode(md_tp.mode)\n                    for lbl, md_tp in tp_val.items()\n                )\n                + '}'\n            )\n        case e.ScalarTp(name):\n            return show_qname(name)\n        case e.UncheckedTypeName(name):\n            return \"unchecked_name(\" + show_raw_name(name) + \")\"\n        case e.CompositeTp(kind=kind, tps=tps, labels=labels):\n            if labels:\n                return (\n                    kind.value\n                    + '<'\n                    + \",\".join(\n                        label + \":\" + show_tp(tp)\n                        for (label, tp) in zip(labels, tps, strict=True)\n                    )\n                    + '>'\n                )\n            else:\n                return f'{kind.value}<{\",\".join(show_tp(tp) for tp in tps)}>'\n        case e.SomeTp(index=index):\n            return f'some_{{{index}}}'\n        case e.AnyTp(name):\n            return 'any' + (name or '')\n        case e.NamedNominalLinkTp(name=name, linkprop=lp_tp):\n            return f'{show_raw_name(name)}@{show_tp(lp_tp)}'\n        case e.NominalLinkTp(name=name, subject=s_tp, linkprop=lp_tp):\n            return f'{show_tp(s_tp)}_{show_raw_name(name)}@{show_tp(lp_tp)}'\n        case e.UnionTp(left=left, right=right):\n            return f'{show_tp(left)} | {show_tp(right)}'\n        case e.IntersectTp(left=left, right=right):\n            return f'{show_tp(left)} & {show_tp(right)}'\n        case e.UncheckedComputableTp(expr=expr):\n            return 'comp_unck(' + show_expr(expr) + ')'\n        case e.ComputableTp(expr=expr, tp=tp):\n            return 'comp(' + show_tp(tp) + \",\" + show_expr(expr) + \")\"\n        case e.DefaultTp(expr=expr, tp=tp):\n            return 'default(' + show_tp(tp) + \",\" + show_expr(expr) + \")\"\n        case e.OverloadedTargetTp(linkprop=linkprop):\n            if linkprop is None:\n                return 'overloaded()'\n            else:\n                return 'overloaded(linkprop=' + show_tp(linkprop) + \")\"\n        case e.QualifiedName(_) | e.UnqualifiedName(_):\n            return show_raw_name(tp)\n        case _:\n            raise ValueError('Unimplemented', tp)\n\n\ndef show_func_tps(tp: e.FunArgRetType) -> str:\n    return (\n        \", \".join(show_tp(arg_tp) for arg_tp in tp.args_tp)\n        + \" -> \"\n        + show_result_tp(tp.ret_tp)\n    )\n\n\ndef show_result_tp(tp: e.ResultTp) -> str:\n    return show_tp(tp.tp) + show_cmmode(tp.mode)\n\n\ndef show_label(lbl: e.Label) -> str:\n    match lbl:\n        case e.StrLabel(label=s_label):\n            return s_label\n        case e.LinkPropLabel(label=l_label):\n            return \"@\" + l_label\n        case _:\n            raise ValueError('Unimplemented', lbl)\n\n\ndef show_scalar_val(val: e.ScalarVal) -> str:\n    tp = val.tp\n    v = val.val\n    match tp.name:\n        case e.QualifiedName([\"std\", \"str\"]):\n            return '\"' + v + '\"'\n        case e.QualifiedName([\"std\", \"int64\"]):\n            return str(v)\n        case e.QualifiedName([\"std\", \"bool\"]):\n            return str(v)\n        case _:\n            return show_qname(tp.name) + \"(\" + str(v) + \")\"\n\n\ndef show_edge_database_select_filter(\n    filter: e.EdgeDatabaseSelectFilter,\n) -> str:\n    match filter:\n        case e.EdgeDatabaseEqFilter(propname=propname, arg=arg):\n            return f'({propname} = {show(arg)})'\n        case e.EdgeDatabaseConjunctiveFilter(conjuncts=conjuncts):\n            return (\n                '('\n                + \" AND \".join(\n                    show_edge_database_select_filter(f) for f in conjuncts\n                )\n                + ')'\n            )\n        case e.EdgeDatabaseDisjunctiveFilter(disjuncts=disjuncts):\n            return (\n                '('\n                + \" OR \".join(\n                    show_edge_database_select_filter(f) for f in disjuncts\n                )\n                + ')'\n            )\n        case e.EdgeDatabaseTrueFilter():\n            return 'TRUE'\n        case _:\n            raise ValueError('Unimplemented', filter)\n\n\ndef show_expr(expr: e.Expr) -> str:\n    match expr:\n        case e.QualifiedName(_):\n            return show_qname(expr)\n        case e.UnqualifiedName(_):\n            return show_raw_name(expr)\n        case e.ScalarVal(tp, _):\n            return show_scalar_val(expr)\n        case e.BindingExpr(var=var, body=_):\n            return (\n                \"λ\"\n                + var\n                + \". \"\n                + show_expr(eops.instantiate_expr(e.FreeVarExpr(var), expr))\n            )\n        case e.TypeCastExpr(tp=tp, arg=arg):\n            return \"<\" + show_tp(tp) + \">\" + show_expr(arg)\n        case e.CheckedTypeCastExpr(\n            cast_tp=(tp_from, tp_to), arg=arg, cast_spec=_\n        ):\n            return (\n                \"<\"\n                + show_tp(tp_from)\n                + \" -> \"\n                + show_tp(tp_to)\n                + \">\"\n                + show_expr(arg)\n            )\n        case e.MultiSetExpr(expr=arr):\n            return \"{\" + \", \".join(show_expr(el) for el in arr) + \"}\"\n        # case e.ObjectExpr(val=elems):\n        #     return \"{\" + \", \".join(f'{show_label(lbl)} := {show_expr(el)}'\n        #                            for lbl, el in elems.items()) + \"}\"\n        case e.ShapeExpr(shape=shape):\n            return (\n                \"{\"\n                + \", \".join(\n                    show_label(lbl) + \" := \" + show_expr(el)\n                    for lbl, el in shape.items()\n                )\n                + \"}\"\n            )\n        case e.UnionExpr(left=left, right=right):\n            return show_expr(left) + \" `UNION` \" + show_expr(right)\n        case e.FunAppExpr(fun=fname, args=args, overloading_index=_):\n            return (\n                show_raw_name(fname)\n                + \"(\"\n                + \", \".join(show_expr(el) for el in args)\n                + \")\"\n            )\n        case e.FreeVarExpr(var=var):\n            return var\n        case e.ObjectProjExpr(subject=subject, label=label):\n            return show_expr(subject) + \".\" + label\n        case e.TupleProjExpr(subject=subject, label=label):\n            return show_expr(subject) + \".→\" + label\n        case e.LinkPropProjExpr(subject=subject, linkprop=linkprop):\n            return show_expr(subject) + \"@\" + linkprop\n        case e.WithExpr(bound=bound, next=next):\n            return \"with \" + show_expr(bound) + \", \" + show_expr(next)\n        case e.DetachedExpr(expr=subject):\n            return \"detached \" + show_expr(subject)\n        case e.BackLinkExpr(subject=subject, label=label):\n            return show_expr(subject) + \".<\" + label\n        case e.TpIntersectExpr(subject=subject, tp=tp):\n            return show_expr(subject) + \" [is \" + show_tp(tp) + \"]\"\n        case e.IsTpExpr(subject=subject, tp=tp):\n            return show_expr(subject) + \" is \" + show_tp(tp)\n        case e.SubqueryExpr(expr=subject):\n            return \"select \" + show_expr(subject)\n        case e.FilterOrderExpr(subject=subject, filter=filter, order=order):\n            return (\n                \"(\"\n                + show_expr(subject)\n                + \" filter \"\n                + show_expr(filter)\n                + \" order by {\"\n                + \", \".join(\n                    [l + \" => \" + show_expr(o) for (l, o) in order.items()]\n                )\n                + \"})\"\n            )\n        case e.OffsetLimitExpr(subject=subject, offset=offset, limit=limit):\n            return (\n                \"(\"\n                + show_expr(subject)\n                + \" offset \"\n                + show_expr(offset)\n                + \" limit \"\n                + show_expr(limit)\n                + \")\"\n            )\n        case e.InsertExpr(name=name, new=new):\n            return (\n                \"insert \"\n                + show_raw_name(name)\n                + \" {\"\n                + \", \".join(\n                    [k + \" := \" + show_expr(n) for (k, n) in new.items()]\n                )\n                + \"}\"\n            )\n        case e.UpdateExpr(subject=subject, shape=shape):\n            return \"update \" + show_expr(subject) + \" \" + show_expr(shape)\n        case e.DeleteExpr(subject=subject):\n            return \"delete \" + show_expr(subject)\n        case e.ForExpr(bound=bound, next=next):\n            return \"for \" + show_expr(bound) + \" union \" + show_expr(next)\n        case e.OptionalForExpr(bound=bound, next=next):\n            return (\n                \"for optional \"\n                + show_expr(bound)\n                + \" union \"\n                + show_expr(next)\n            )\n        case e.ShapedExprExpr(expr=subject, shape=shape):\n            return show_expr(subject) + \" \" + show_expr(shape)\n        case e.UnnamedTupleExpr(val=elems):\n            return \"(\" + \", \".join(show_expr(el) for el in elems) + \")\"\n        case e.NamedTupleExpr(val=elems):\n            return (\n                \"(\"\n                + \", \".join(\n                    f'{lbl} := {show_expr(el)}' for lbl, el in elems.items()\n                )\n                + \")\"\n            )\n        case e.ArrExpr(elems=arr):\n            return \"[\" + \", \".join(show_expr(el) for el in arr) + \"]\"\n        case e.IfElseExpr(\n            then_branch=then_branch,\n            condition=condition,\n            else_branch=else_branch,\n        ):\n            return (\n                show_expr(then_branch)\n                + \" if \"\n                + show_expr(condition)\n                + \" else \"\n                + show_expr(else_branch)\n            )\n        case e.ConditionalDedupExpr(expr=inner):\n            return \"cond_dedup(\" + show_expr(inner) + \")\"\n        case e.FreeObjectExpr():\n            return \"{<free>}\"\n        case e.ParameterExpr(name=name):\n            return f\"${name}\"\n        case e.QualifiedNameWithFilter(name=name, filter=filter):\n            return (\n                \"with_filter(\"\n                + show_qname(name)\n                + \", \"\n                + show_edge_database_select_filter(filter)\n                + \")\"\n            )\n        case _:\n            raise ValueError('Unimplemented', expr)\n\n\ndef show_arg_mod(mod: e.ParamModifier) -> str:\n    match mod:\n        case e.ParamSingleton():\n            return \"1\"\n        case e.ParamOptional():\n            return \"?\"\n        case e.ParamSetOf():\n            return \"*\"\n        case _:\n            raise ValueError('Unimplemented', mod)\n\n\ndef show_arg_ret_type(tp: e.FunArgRetType) -> str:\n    return (\n        \"[\"\n        + (\n            \", \".join(\n                show_tp(arg_tp) + \"^\" + show_arg_mod(mod)\n                for arg_tp, mod in zip(tp.args_tp, tp.args_mod)\n            )\n        )\n        + \"]\"\n        + \" -> \"\n        + show_result_tp(tp.ret_tp)\n    )\n\n\ndef show_func_defs(funcdefs: list[e.FuncDef]) -> str:\n    if len(funcdefs) == 1:\n        return show_arg_ret_type(funcdefs[0].tp)\n    elif len(funcdefs) > 1:\n        return show_arg_ret_type(funcdefs[0].tp) + \" ...\"\n    else:\n        raise ValueError('Unimplemented', funcdefs)\n\n\ndef show_constraint(constraint: e.Constraint) -> str:\n    match constraint:\n        case e.ExclusiveConstraint(name=name, delegated=delegated):\n            return (\n                \"exclusive(\"\n                + name\n                + \")\"\n                + (\", delegated\" if delegated else \"\")\n            )\n        case _:\n            raise ValueError('Unimplemented', constraint)\n\n\ndef show_me(me: e.ModuleEntity) -> str:\n    match me:\n        case e.ModuleEntityTypeDef(\n            typedef=typedef, is_abstract=is_abstract, constraints=constraints\n        ):\n            auxiliary = \"\"\n            auxiliary += \"abstract, \" if is_abstract else \"\"\n            auxiliary += (\n                \"constraints = [\"\n                + \", \".join(show_constraint(c) for c in constraints)\n                + \"], \"\n                if constraints\n                else \"\"\n            )\n            auxiliary = \"\\n    \" + auxiliary if auxiliary else \"\"\n            base = show_tp(typedef)\n            return base + auxiliary\n        case e.ModuleEntityFuncDef(funcdefs=funcdefs):\n            return show_func_defs(funcdefs)\n        case _:\n            raise ValueError('Unimplemented', me)\n\n\ndef show_module(dbschema: e.DBModule) -> str:\n    return \"\\n\".join(\n        name + \" := \" + show_me(me) for name, me in dbschema.defs.items()\n    )\n\n\ndef show_module_name(name: tuple[str, ...]) -> str:\n    return \"::\".join(name)\n\n\ndef show_schema(dbschema: e.DBSchema) -> str:\n    return (\n        \"Checked Modules:\"\n        + \"\\n\".join(\n            show_module_name(name) + \" := { \" + show_module(module) + \" } \"\n            for name, module in dbschema.modules.items()\n        )\n        + \"\\nUnchecked Modules:\\n\"\n        + \"\\n\".join(\n            show_module_name(name) + \" := { \" + show_module(module) + \" } \"\n            for name, module in dbschema.unchecked_modules.items()\n        )\n    )\n\n\ndef show_tcctx(tcctx: e.TcCtx) -> str:\n    return (\n        show_schema(tcctx.schema)\n        + \"\\n\"\n        + (\n            \"\\n\".join(\n                name + \" := \" + show_result_tp(r_tp)\n                for name, r_tp in tcctx.varctx.items()\n            )\n        )\n    )\n\n\ndef show_visibility_marker(maker: e.Marker) -> str:\n    match maker:\n        case e.Visible():\n            return \"v\"\n        case e.Invisible():\n            return \"i\"\n        case _:\n            raise ValueError('Unimplemented', maker)\n\n\ndef show_val(val: e.Val | e.ObjectVal | e.MultiSetVal) -> str:\n    match val:\n        case e.ScalarVal(_, _):\n            return show_scalar_val(val)\n        case e.ObjectVal(val=elems):\n            return (\n                \"{\"\n                + \", \".join(\n                    show_label(lbl)\n                    + f' ({show_visibility_marker(m)}): {show_multiset_val(el)}'\n                    for (lbl, (m, el)) in elems.items()\n                )\n                + \"}\"\n            )\n        case e.RefVal(refid=id, val=v):\n            return f\"ref({id})\" + show_val(v)\n        case e.UnnamedTupleVal(val=elems):\n            return \"(\" + \", \".join(show_val(el) for el in elems) + \")\"\n        case e.NamedTupleVal(val=elems):\n            return (\n                \"(\"\n                + \", \".join(\n                    f'{lbl} := {show_val(el)}' for lbl, el in elems.items()\n                )\n                + \")\"\n            )\n        case e.ArrVal(val=arr):\n            return \"[\" + \", \".join(show_val(el) for el in arr) + \"]\"\n        case _:\n            raise ValueError('Unimplemented', val)\n\n\ndef show_multiset_val(val: e.MultiSetVal) -> str:\n    match val:\n        case e.ResultMultiSetVal(_vals=arr):\n            return (\n                \"(multiset val){\" + \", \".join(show_val(el) for el in arr) + \"}\"\n            )\n        case _:\n            raise ValueError('Unimplemented', val)\n\n\ndef show_ctx(ctx: e.TcCtx) -> str:\n    return (\n        \"Schema:\"\n        + \"\\n\"\n        + show_schema(ctx.schema)\n        + \"\\n\"\n        + \"Current Module: \"\n        + show_module_name(ctx.current_module)\n        + \"\\n\"\n        + \"VarCtx:\"\n        + \"\\n\"\n        + (\n            \"\\n\".join(\n                name + \" : \" + show_result_tp(r_tp)\n                for name, r_tp in ctx.varctx.items()\n            )\n        )\n        + \"\\n\"\n    )\n\n\ndef show(expr: Any) -> str:\n    if isinstance(expr, e.Tp):  # type: ignore\n        return show_tp(expr)\n    elif isinstance(expr, e.Expr):  # type: ignore\n        return show_expr(expr)\n    elif isinstance(expr, e.Val):  # type: ignore\n        return show_val(expr)\n    elif isinstance(expr, e.TcCtx):\n        return show_ctx(expr)\n    elif isinstance(expr, e.ObjectVal):\n        return show_val(expr)\n    elif isinstance(expr, e.ResultTp):\n        return show(expr.tp) + \"^\" + show_cmmode(expr.mode)\n    elif isinstance(expr, e.MultiSetVal):\n        return show_multiset_val(expr)\n    elif isinstance(expr, list):\n        return \"!!!LIST([\" + \", \".join(show(el) for el in expr) + \"])\"\n    else:\n        raise ValueError('Unimplemented', expr)\n\n\ndef p(expr: Any) -> None:\n    print(show(expr))\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/data/module_ops.py",
    "content": "from . import data_ops as e\nfrom typing import Optional\n\ndefault_open_scopes = [(\"std\",)]\n\n\ndef resolve_module_in_schema(\n    schema: e.DBSchema, name: tuple[str, ...]\n) -> e.DBModule:\n    if name in schema.unchecked_modules:\n        assert name not in schema.modules\n        return schema.unchecked_modules[name]\n    elif name in schema.modules:\n        return schema.modules[name]\n    else:\n        raise ValueError(f\"Module {name} not found\")\n\n\ndef try_resolve_module_entity(\n    ctx: e.TcCtx | e.DBSchema, name: e.QualifiedName\n) -> Optional[e.ModuleEntity]:\n    \"\"\"\n    Resolve a module entity using the ABS method.\n    https://github.com/edgedb/edgedb/discussions/4883\n    \"\"\"\n    assert len(name.names) >= 2\n    if name.names[0] == \"module\":\n        assert isinstance(ctx, e.TcCtx), (\n            \"qualified names beginning with\"\n            \" module cannot be resolved in a schema\"\n        )\n        name = e.QualifiedName([*ctx.current_module, *name.names[1:]])\n    module: e.DBModule\n    if isinstance(ctx, e.TcCtx):\n        module = resolve_module_in_schema(ctx.schema, tuple(name.names[:-1]))\n    else:\n        module = resolve_module_in_schema(ctx, tuple(name.names[:-1]))\n    if name.names[-1] in module.defs:\n        return module.defs[name.names[-1]]\n    else:\n        return None\n\n\ndef try_resolve_type_name(\n    ctx: e.TcCtx | e.DBSchema, name: e.QualifiedName\n) -> Optional[e.ObjectTp | e.ScalarTp]:\n    me = try_resolve_module_entity(ctx, name)\n    if me is not None:\n        if isinstance(me, e.ModuleEntityTypeDef):\n            return me.typedef\n        else:\n            raise ValueError(f\"{name} is not a type\")\n    else:\n        return None\n\n\ndef resolve_type_def(\n    ctx: e.TcCtx | e.DBSchema, name: e.QualifiedName\n) -> e.ModuleEntityTypeDef:\n    me = try_resolve_module_entity(ctx, name)\n    if me is not None:\n        if isinstance(me, e.ModuleEntityTypeDef):\n            return me\n        else:\n            raise ValueError(f\"{name} is not a type\")\n    else:\n        raise ValueError(f\"Type {name} not found\")\n\n\ndef resolve_type_name(\n    ctx: e.TcCtx | e.DBSchema, name: e.QualifiedName\n) -> e.ObjectTp | e.ScalarTp:\n    resolved = try_resolve_type_name(ctx, name)\n    if resolved is None:\n        raise ValueError(f\"Type {name} not found\")\n    else:\n        return resolved\n\n\ndef resolve_func_name(\n    ctx: e.TcCtx | e.DBSchema, name: e.QualifiedName\n) -> list[e.FuncDef]:\n    me = try_resolve_module_entity(ctx, name)\n    if me is not None:\n        if isinstance(me, e.ModuleEntityFuncDef):\n            return me.funcdefs\n        else:\n            raise ValueError(f\"{name} is not a function\")\n    else:\n        raise ValueError(f\"Function {name} not found\")\n\n\ndef try_resolve_simple_name(\n    ctx: e.TcCtx | e.DBSchema, unq_name: e.UnqualifiedName\n) -> Optional[e.QualifiedName]:\n    \"\"\"\n    Resolve the name (may refer to a type or a function) in this order:\n    1. Current module\n    2. The default `std` module\n    \"\"\"\n    name = unq_name.name\n\n    if isinstance(ctx, e.TcCtx):\n        current_module = resolve_module_in_schema(\n            ctx.schema, ctx.current_module\n        )\n        if name in current_module.defs:\n            return e.QualifiedName([*ctx.current_module, name])\n\n    if isinstance(ctx, e.TcCtx):\n        schema = ctx.schema\n    else:\n        schema = ctx\n\n    for default_scope in default_open_scopes:\n        std_module = resolve_module_in_schema(schema, default_scope)\n        if name in std_module.defs:\n            return e.QualifiedName([*default_scope, name])\n    return None\n\n\ndef resolve_simple_name(\n    ctx: e.TcCtx | e.DBSchema, unq_name: e.UnqualifiedName\n) -> e.QualifiedName:\n    name = try_resolve_simple_name(ctx, unq_name)\n    if name is not None:\n        return name\n    else:\n        raise ValueError(f\"Name {name} not found\")\n\n\ndef resolve_raw_name_and_type_def(\n    ctx: e.TcCtx | e.DBSchema, name: e.QualifiedName | e.UnqualifiedName\n) -> tuple[e.QualifiedName, e.ObjectTp | e.ScalarTp]:\n    if isinstance(name, e.UnqualifiedName):\n        name = resolve_simple_name(ctx, name)\n    return (name, resolve_type_name(ctx, name))\n\n\ndef resolve_raw_name_and_func_def(\n    ctx: e.TcCtx | e.DBSchema, name: e.QualifiedName | e.UnqualifiedName\n) -> tuple[e.QualifiedName, list[e.FuncDef]]:\n    if isinstance(name, e.UnqualifiedName):\n        name = resolve_simple_name(ctx, name)\n    return (name, resolve_func_name(ctx, name))\n\n\ndef enumerate_all_object_type_defs(\n    ctx: e.TcCtx,\n) -> list[tuple[e.QualifiedName, e.ObjectTp]]:\n    \"\"\"\n    Enumerate all type definitions in the current module\n    and the default `std` module.\n    \"\"\"\n    result: list[tuple[e.QualifiedName, e.ObjectTp]] = []\n    for module_name, module_def in [\n        *ctx.schema.modules.items(),\n        *ctx.schema.unchecked_modules.items(),\n    ]:\n        for tp_name, me in module_def.defs.items():\n            if isinstance(me, e.ModuleEntityTypeDef) and isinstance(\n                me.typedef, e.ObjectTp\n            ):\n                result.append(\n                    (e.QualifiedName([*module_name, tp_name]), me.typedef)\n                )\n\n    return result\n\n\ndef tp_name_is_abstract(name: e.QualifiedName, schema: e.DBSchema) -> bool:\n    me = try_resolve_module_entity(schema, name)\n    assert isinstance(me, e.ModuleEntityTypeDef)\n    return me.is_abstract\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/data/path_factor.py",
    "content": "from typing import Callable, Optional\n\nfrom .data_ops import (\n    BackLinkExpr,\n    BindingExpr,\n    BoolVal,\n    DetachedExpr,\n    Expr,\n    FilterOrderExpr,\n    FreeVarExpr,\n    LinkPropProjExpr,\n    ObjectProjExpr,\n    TpIntersectExpr,\n    OptionalForExpr,\n    next_name,\n    StrLabel,\n)\nfrom . import data_ops as e\nfrom . import expr_ops as eops\nfrom .expr_ops import (\n    abstract_over_expr,\n    appears_in_expr,\n    instantiate_expr,\n    is_path,\n    iterative_subst_expr_for_expr,\n    map_expr,\n    operate_under_binding,\n)\nfrom .query_ops import QueryLevel, map_query, map_sub_and_semisub_queries\n\n\ndef all_prefixes_of_a_path(expr: Expr) -> list[Expr]:\n    match expr:\n        case FreeVarExpr(_):\n            return [expr]\n        case LinkPropProjExpr(subject=subject, linkprop=_):\n            return [*all_prefixes_of_a_path(subject), expr]\n        case ObjectProjExpr(subject=subject, label=_):\n            return [*all_prefixes_of_a_path(subject), expr]\n        case e.TpIntersectExpr(subject=subject, tp=_):\n            return [*all_prefixes_of_a_path(subject), expr]\n        case BackLinkExpr(subject=subject, label=_):\n            return [*all_prefixes_of_a_path(subject), expr]\n        case _:\n            raise ValueError(\"not a path\", expr)\n\n\ndef path_lexicographic_key(e: Expr) -> str:\n    match e:\n        case FreeVarExpr(s):\n            return s\n        case LinkPropProjExpr(subject=subject, linkprop=linkprop):\n            return path_lexicographic_key(subject) + \"@\" + linkprop\n        case ObjectProjExpr(subject=subject, label=label):\n            return path_lexicographic_key(subject) + \".\" + label\n        case TpIntersectExpr(subject=subject, tp=_):\n            return path_lexicographic_key(subject) + \"[is _]\"\n        case BackLinkExpr(subject=subject, label=label):\n            return path_lexicographic_key(subject) + \".<\" + label\n        case _:\n            raise ValueError(\"not a path\")\n\n\ndef get_all_paths(e: Expr) -> list[Expr]:\n    all_paths: list[Expr] = []\n\n    def populate(sub: Expr) -> Optional[Expr]:\n        nonlocal all_paths\n        if isinstance(sub, DetachedExpr):  # skip detached\n            return sub\n        if is_path(sub):\n            all_paths = [*all_paths, sub]\n            return sub\n        else:\n            return None\n\n    map_expr(populate, e)\n    return all_paths\n\n\ndef get_all_pre_top_level_paths(e: Expr, dbschema: e.TcCtx) -> list[Expr]:\n    all_paths: list[Expr] = []\n\n    def populate(sub: Expr, level: QueryLevel) -> Optional[Expr]:\n        nonlocal all_paths\n        if isinstance(sub, DetachedExpr):  # skip detached\n            return sub\n        if is_path(sub) and (\n            level == QueryLevel.TOP_LEVEL or level == QueryLevel.SEMI_SUBQUERY\n        ):\n            all_paths = [*all_paths, sub]\n            return sub\n        else:\n            return None\n\n    map_query(populate, e, dbschema)\n    return all_paths\n\n\ndef get_all_proper_top_level_paths(e: Expr, dbschema: e.TcCtx) -> list[Expr]:\n    definite_top_paths: list[Expr] = []\n    semi_sub_paths: list[list[Expr]] = []\n    sub_paths: list[Expr] = []\n    sub_sub_paths: list[list[Expr]] = []\n\n    def populate(sub: Expr, level: QueryLevel) -> Optional[Expr]:\n        nonlocal definite_top_paths, semi_sub_paths, sub_paths, sub_sub_paths\n        if isinstance(sub, DetachedExpr):  # skip detached\n            return sub\n        if level == QueryLevel.TOP_LEVEL and is_path(sub):\n            definite_top_paths = [*definite_top_paths, sub]\n            return sub\n        elif level == QueryLevel.SEMI_SUBQUERY:\n            this_semi_sub_paths = get_all_pre_top_level_paths(sub, dbschema)\n            semi_sub_paths = [*semi_sub_paths, this_semi_sub_paths]\n            this_sub_paths = [\n                p for p in get_all_paths(sub) if p not in this_semi_sub_paths\n            ]\n            sub_sub_paths = [*sub_sub_paths, this_sub_paths]\n            return sub  # also cut off here\n        elif level == QueryLevel.SUBQUERY:\n            sub_paths = [*sub_paths, *get_all_paths(sub)]\n            return sub  # also cut off here as paths inside subqueries\n        else:\n            return None\n\n    map_query(populate, e, dbschema)\n\n    selected_semi_sub_paths = []\n    for i, cluster in enumerate(semi_sub_paths):\n        for candidate in cluster:\n            prefixes = all_prefixes_of_a_path(candidate)\n            to_check = (\n                definite_top_paths\n                + sub_paths\n                + [\n                    p\n                    for spl in (semi_sub_paths[:i] + semi_sub_paths[i + 1 :])\n                    for p in spl\n                ]\n                + [\n                    p\n                    for spl in (sub_sub_paths[:i] + sub_sub_paths[i + 1 :])\n                    for p in spl\n                ]\n            )\n            if any(\n                [\n                    appears_in_expr(prefix, ck)\n                    for prefix in prefixes\n                    for ck in to_check\n                ]\n            ):\n                selected_semi_sub_paths.append(candidate)\n\n    # all top_paths will show up finally,\n    # we need to filter out those paths in semi_sub\n    # whose prefixes (including itself) appears solely in the same subquery\n    return definite_top_paths + selected_semi_sub_paths\n\n\ndef common_longest_path_prefix(e1: Expr, e2: Expr) -> Optional[Expr]:\n    pending = None\n\n    def find_longest(pp1: list[Expr], pp2: list[Expr]) -> Optional[Expr]:\n        nonlocal pending\n        match (pp1, pp2):\n            case ([], []):\n                return pending\n            case ([], _):\n                return pending\n            case (_, []):\n                return pending\n            case ([p1this, *p1next], [p2this, *p2next]):\n                if p1this == p2this:\n                    pending = p1this\n                return find_longest([*p1next], [*p2next])\n        raise ValueError(\"should not happen\")\n\n    return find_longest(all_prefixes_of_a_path(e1), all_prefixes_of_a_path(e2))\n\n\ndef common_longest_path_prefix_in_set(test_set: list[Expr]) -> list[Expr]:\n    result: list[Expr] = []\n    for s in test_set:\n        for t in test_set:\n            optional = common_longest_path_prefix(s, t)\n            if optional:\n                result.append(optional)\n    return result\n\n\ndef separate_common_longest_path_prefix_in_set(\n    base_set: list[Expr], compare_set: list[Expr]\n) -> list[Expr]:\n    result: list[Expr] = []\n    for s in base_set:\n        for t in compare_set:\n            optional = common_longest_path_prefix(s, t)\n            if optional:\n                result.append(optional)\n    return result\n\n\ndef toppath_for_factoring(expr: Expr, dbschema: e.TcCtx) -> list[Expr]:\n    all_paths = get_all_paths(expr)\n    top_level_paths = get_all_proper_top_level_paths(expr, dbschema)\n    clpp_a = common_longest_path_prefix_in_set(top_level_paths)\n    c_i = [\n        separate_common_longest_path_prefix_in_set(top_level_paths, [b])\n        for b in all_paths\n    ]\n    c_all = [p for c in c_i for p in c]\n    d = []\n    for p in [*clpp_a, *c_all]:\n        match p:\n            case e.LinkPropProjExpr(subject=subject, linkprop=_):\n                match subject:\n                    case e.ObjectProjExpr(subject=subject, label=_):\n                        d.append(subject)\n                    case e.TpIntersectExpr(\n                        subject=(e.BackLinkExpr(subject=subject, label=_)),\n                        tp=_,\n                    ):\n                        d.append(subject)\n                    case e.BackLinkExpr(subject=subject, label=_):\n                        d.append(subject)\n                    case _:\n                        pass\n            case _:\n                pass\n    all_factoring_paths = c_all + clpp_a + d\n\n    # remove from all_factoring_paths those paths that only occurred once\n    excluding_paths = [\n        p\n        for p in top_level_paths\n        if eops.count_appearances_in_expr(p, expr) == 1\n    ]\n    all_factoring_paths = [\n        p for p in all_factoring_paths if p not in excluding_paths\n    ]\n\n    return sorted(list(set(all_factoring_paths)), key=path_lexicographic_key)\n\n\ndef trace_input_output(func):\n    def wrapper(e, s):\n        indent = \"| \" * wrapper.depth\n        print(f\"{indent}input: {e} \")\n        wrapper.depth += 1\n        result = func(e, s)\n        wrapper.depth -= 1\n        print(f\"{indent}output: {result}\")\n        return result\n\n    wrapper.depth = 0\n    return wrapper\n\n\ndef sub_select_hoist(top_e: Expr, dbschema: e.TcCtx) -> Expr:\n    def sub_select_hoist_map_func(e: Expr) -> Expr:\n        if isinstance(e, BindingExpr):\n            new_fresh_name = next_name()\n            return abstract_over_expr(\n                select_hoist(\n                    instantiate_expr(FreeVarExpr(new_fresh_name), e), dbschema\n                ),\n                new_fresh_name,\n            )\n        else:\n            return select_hoist(e, dbschema)\n\n    return map_sub_and_semisub_queries(\n        sub_select_hoist_map_func, top_e, dbschema\n    )\n\n\ndef select_hoist(expr: Expr, dbschema: e.TcCtx) -> Expr:\n\n    top_paths = toppath_for_factoring(expr, dbschema)\n    fresh_names: list[str] = [next_name() for p in top_paths]\n    fresh_vars: list[Expr] = [FreeVarExpr(n) for n in fresh_names]\n    for_paths = [\n        iterative_subst_expr_for_expr(fresh_vars[:i], top_paths[:i], p_i)\n        for (i, p_i) in enumerate(top_paths)\n    ]\n\n    inner_e: Expr\n    post_process_transform: Callable[[Expr], Expr]\n    match expr:\n        # only perform special factoring if there is an order\n        case FilterOrderExpr(\n            subject=subject, filter=filter, order=order\n        ) if order:\n            bindname = next_name()\n            inner_e = OptionalForExpr(\n                FilterOrderExpr(\n                    subject=sub_select_hoist(\n                        iterative_subst_expr_for_expr(\n                            fresh_vars, top_paths, subject\n                        ),\n                        dbschema,\n                    ),\n                    filter=operate_under_binding(\n                        filter,\n                        lambda filter: select_hoist(\n                            iterative_subst_expr_for_expr(\n                                fresh_vars, top_paths, filter\n                            ),\n                            dbschema,\n                        ),\n                    ),\n                    order={},\n                ),\n                abstract_over_expr(\n                    e.ShapedExprExpr(\n                        expr=e.FreeObjectExpr(),\n                        shape=e.ShapeExpr(\n                            shape={\n                                StrLabel(\n                                    \"__edgedb_reserved_subject__\"\n                                ): abstract_over_expr(FreeVarExpr(bindname)),\n                                **{\n                                    StrLabel(l): abstract_over_expr(\n                                        select_hoist(\n                                            iterative_subst_expr_for_expr(\n                                                fresh_vars,\n                                                top_paths,\n                                                instantiate_expr(\n                                                    FreeVarExpr(bindname), o\n                                                ),\n                                            ),\n                                            dbschema,\n                                        )\n                                    )\n                                    for (l, o) in order.items()\n                                },\n                            }\n                        ),\n                    ),\n                    bindname,\n                ),\n            )\n\n            def post_processing(expr: Expr) -> Expr:\n                bindname = next_name()\n                return ObjectProjExpr(\n                    subject=FilterOrderExpr(\n                        subject=expr,\n                        filter=abstract_over_expr(BoolVal(True)),\n                        order={\n                            l: abstract_over_expr(\n                                ObjectProjExpr(\n                                    subject=FreeVarExpr(bindname), label=l\n                                ),\n                                bindname,\n                            )\n                            for (l, o) in order.items()\n                        },\n                    ),\n                    label=\"__edgedb_reserved_subject__\",\n                )\n\n            post_process_transform = post_processing\n        case _:\n            after_e = iterative_subst_expr_for_expr(\n                fresh_vars, top_paths, expr\n            )\n            inner_e = sub_select_hoist(after_e, dbschema)\n\n            def id_transform(x):\n                return x\n\n            post_process_transform = id_transform\n\n    result = inner_e\n    for i in reversed(list(range(len(for_paths)))):\n        result = OptionalForExpr(\n            for_paths[i], abstract_over_expr(result, fresh_names[i])\n        )\n\n    result = post_process_transform(result)\n    return result\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/data/query_ops.py",
    "content": "from enum import Enum\nfrom typing import Callable, Optional, Sequence\n\nfrom . import data_ops as e\nfrom .data_ops import (\n    ArrExpr,\n    BackLinkExpr,\n    BindingExpr,\n    BoundVarExpr,\n    DetachedExpr,\n    Expr,\n    FilterOrderExpr,\n    ForExpr,\n    FreeVarExpr,\n    FunAppExpr,\n    InsertExpr,\n    LinkPropProjExpr,\n    MultiSetExpr,\n    NamedTupleExpr,\n    ObjectProjExpr,\n    OffsetLimitExpr,\n    OptionalForExpr,\n    ParamOptional,\n    ParamSetOf,\n    ParamSingleton,\n    ShapedExprExpr,\n    ShapeExpr,\n    SubqueryExpr,\n    TpIntersectExpr,\n    TypeCastExpr,\n    UnionExpr,\n    UnnamedTupleExpr,\n    UpdateExpr,\n    WithExpr,\n)\n\nfrom . import module_ops as mops\nfrom . import expr_ops as eops\nfrom ..interpreter_logging import print_warning\n\n\nclass QueryLevel(Enum):\n    TOP_LEVEL = 1\n    SEMI_SUBQUERY = 2\n    SUBQUERY = 3\n\n\ndef enter_semi_subquery(level: QueryLevel) -> QueryLevel:\n    match level:\n        case QueryLevel.TOP_LEVEL:\n            return QueryLevel.SEMI_SUBQUERY\n        case QueryLevel.SEMI_SUBQUERY:\n            return QueryLevel.SEMI_SUBQUERY\n        case QueryLevel.SUBQUERY:\n            return QueryLevel.SUBQUERY\n\n\ndef enter_sub_query(level: QueryLevel) -> QueryLevel:\n    return QueryLevel.SUBQUERY\n\n\ndef map_query(\n    f: Callable[[Expr, QueryLevel], Optional[Expr]],\n    expr: Expr,\n    schema: e.TcCtx,\n    level: QueryLevel = QueryLevel.TOP_LEVEL,\n) -> Expr:\n    \"\"\"maps a function over free variables and bound variables,\n    and does not modify other nodes\n\n    f : called with current expression and the current level,\n        return a non-null value to cut off further exploration\n    level : Indicates the current level, initially should be\n\n    \"\"\"\n    tentative = f(expr, level)\n    if tentative is not None:\n        return tentative\n    else:\n\n        def recur(expr):\n            return map_query(f, expr, schema, level)\n\n        def semisub_recur(expr):\n            return map_query(f, expr, schema, enter_semi_subquery(level))\n\n        def sub_recur(expr):\n            return map_query(f, expr, schema, enter_sub_query(level))\n\n        match expr:\n            case FreeVarExpr(_):\n                return expr\n            case e.QualifiedName(_):\n                return expr\n            case BoundVarExpr(_):\n                return expr\n            case e.ScalarVal(_):\n                return expr\n            case BindingExpr(var=var, body=body):\n                # type: ignore[has-type]\n                return BindingExpr(var=var, body=recur(body))\n            case UnnamedTupleExpr(val=val):\n                return UnnamedTupleExpr(val=[recur(e) for e in val])\n            case NamedTupleExpr(val=val):\n                return NamedTupleExpr(\n                    val={k: recur(e) for (k, e) in val.items()}\n                )\n            case ObjectProjExpr(subject=subject, label=label):\n                if eops.is_path(subject):\n                    return ObjectProjExpr(subject=recur(subject), label=label)\n                else:\n                    return ObjectProjExpr(\n                        subject=semisub_recur(subject), label=label\n                    )\n            case LinkPropProjExpr(subject=subject, linkprop=linkprop):\n                return LinkPropProjExpr(\n                    subject=recur(subject), linkprop=linkprop\n                )\n            case BackLinkExpr(subject=subject, label=label):\n                return BackLinkExpr(subject=recur(subject), label=label)\n            case e.IsTpExpr(subject=subject, tp=tp):\n                return e.IsTpExpr(subject=recur(subject), tp=tp)\n            case TpIntersectExpr(subject=subject, tp=tp_name):\n                return TpIntersectExpr(subject=recur(subject), tp=tp_name)\n            case FunAppExpr(\n                fun=fname, args=args, overloading_index=idx, kwargs=kwargs\n            ):\n                mapped_args: Sequence[Expr] = []\n                _, resolved_fun_defs = mops.resolve_raw_name_and_func_def(\n                    schema, fname\n                )\n\n                from ..type_checking_tools import function_checking as fun_ck\n\n                args_mods = [\n                    mods\n                    for mods in [\n                        fun_ck.try_match_and_get_arg_mods(\n                            expr, resolved_fun_defs[i]\n                        )\n                        for i in range(len(resolved_fun_defs))\n                    ]\n                    if mods is not None\n                ]\n                if len(args_mods) == 0:\n                    # This is due to named only arguments,\n                    # need some time to sort out\n                    raise ValueError(\"Expecting fun_defs [TODO named args]\")\n\n                if not all(\n                    [args_mods[0] == args_mod for args_mod in args_mods]\n                ):\n                    print_warning(\n                        \"Function call with different argument mods\",\n                        \"Choosing the first one\",\n                    )\n                params = resolved_fun_defs[0].tp.args_mod\n                for i in range(len(args)):\n                    match params[i]:\n                        case ParamSingleton():\n                            mapped_args = [*mapped_args, recur(args[i])]\n                        case ParamOptional():\n                            mapped_args = [\n                                *mapped_args,\n                                semisub_recur(args[i]),\n                            ]\n                        case ParamSetOf():\n                            mapped_args = [*mapped_args, sub_recur(args[i])]\n                        case _:\n                            raise ValueError\n                mapped_kwargs: dict[str, Expr] = {}\n                for i, (k, v) in enumerate(kwargs.items()):\n                    match params[i + len(args)]:\n                        case ParamSingleton():\n                            mapped_kwargs[k] = recur(v)\n                        case ParamOptional():\n                            mapped_kwargs[k] = semisub_recur(v)\n                        case ParamSetOf():\n                            mapped_kwargs[k] = sub_recur(v)\n                return FunAppExpr(\n                    fun=fname,\n                    args=mapped_args,\n                    overloading_index=idx,\n                    kwargs=mapped_kwargs,\n                )\n            case FilterOrderExpr(subject=subject, filter=filter, order=order):\n                return FilterOrderExpr(\n                    subject=recur(subject),\n                    filter=sub_recur(filter),\n                    order={l: sub_recur(o) for (l, o) in order.items()},\n                )\n            case ShapedExprExpr(expr=expr, shape=shape):\n                return ShapedExprExpr(expr=recur(expr), shape=sub_recur(shape))\n            case ShapeExpr(shape=shape):\n                return ShapeExpr(\n                    shape={k: sub_recur(e_1) for (k, e_1) in shape.items()}\n                )\n            case TypeCastExpr(tp=tp, arg=arg):\n                return TypeCastExpr(tp=tp, arg=recur(arg))\n            case UnionExpr(left=left, right=right):\n                return UnionExpr(left=sub_recur(left), right=sub_recur(right))\n            case ArrExpr(elems=arr):\n                return ArrExpr(elems=[recur(e) for e in arr])\n            case MultiSetExpr(expr=arr):\n                return MultiSetExpr(expr=[sub_recur(e) for e in arr])\n            case OffsetLimitExpr(subject=subject, offset=offset, limit=limit):\n                return OffsetLimitExpr(\n                    subject=sub_recur(subject),\n                    offset=sub_recur(offset),\n                    limit=sub_recur(limit),\n                )\n            case WithExpr(bound=bound, next=next):\n                return WithExpr(bound=sub_recur(bound), next=sub_recur(next))\n            case ForExpr(bound=bound, next=next):\n                return ForExpr(bound=sub_recur(bound), next=sub_recur(next))\n            case OptionalForExpr(bound=bound, next=next):\n                return OptionalForExpr(\n                    bound=sub_recur(bound), next=sub_recur(next)\n                )\n            case e.IfElseExpr(\n                then_branch=then_branch,\n                condition=condition,\n                else_branch=else_branch,\n            ):\n                return e.IfElseExpr(\n                    then_branch=sub_recur(then_branch),\n                    condition=recur(condition),\n                    else_branch=sub_recur(else_branch),\n                )\n            case InsertExpr(name=name, new=new):\n                return InsertExpr(\n                    name=name, new={k: sub_recur(e) for (k, e) in new.items()}\n                )\n            case DetachedExpr(expr=expr):\n                return DetachedExpr(expr=sub_recur(expr))\n            case SubqueryExpr(expr=expr):\n                return SubqueryExpr(expr=sub_recur(expr))\n            case UpdateExpr(subject=subject, shape=shape):\n                return UpdateExpr(\n                    subject=recur(subject), shape=sub_recur(shape)\n                )\n            case e.DeleteExpr(subject=subject):\n                return e.DeleteExpr(subject=recur(subject))\n            case e.FreeObjectExpr():\n                return e.FreeObjectExpr()\n            case e.ParameterExpr(_):\n                return expr\n\n    raise ValueError(\"Not Implemented: map_query \", expr)\n\n\ndef map_sub_and_semisub_queries(\n    f: Callable[[Expr], Optional[Expr]], expr: Expr, schema: e.TcCtx\n) -> Expr:\n    def map_fun(sub: Expr, level: QueryLevel) -> Optional[Expr]:\n        if level == QueryLevel.SEMI_SUBQUERY or level == QueryLevel.SUBQUERY:\n            return f(sub)\n        else:\n            return None\n\n    return map_query(map_fun, expr, schema)\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/data/type_ops.py",
    "content": "from . import data_ops as e\nfrom . import expr_ops as eops\nfrom typing import Optional, Callable\nfrom . import module_ops as mops\nfrom ..data import expr_to_str as pp\nfrom functools import reduce\nfrom ..schema import subtyping_resolution\n\nimport edgedb\n\n\ndef construct_tp_intersection(tp1: e.Tp, tp2: e.Tp) -> e.Tp:\n    # TODO: optimize so that if tp1 is a subtype of tp2, we return tp2\n    if tp1 == e.AnyTp():\n        return tp2\n    elif tp2 == e.AnyTp():\n        return tp1\n    else:\n        return e.IntersectTp(tp1, tp2)\n\n\ndef construct_tp_union(tp1: e.Tp, tp2: e.Tp) -> e.Tp:\n    # TODO: optimize so that if tp1 is a subtype of tp2, we return tp2\n    assert not isinstance(tp1, e.UncheckedTypeName) and not isinstance(\n        tp2, e.UncheckedTypeName\n    )\n    if tp1 == tp2:\n        return tp1\n    else:\n        return e.UnionTp(tp1, tp2)\n\n\ndef construct_tps_union(tps: list[e.Tp]) -> e.Tp:\n    assert len(tps) > 0\n    return reduce(construct_tp_union, tps)\n\n\ndef collect_tp_intersection(tp1: e.Tp) -> list[e.Tp]:\n    match tp1:\n        case e.IntersectTp(left=tp1, right=tp2):\n            return collect_tp_intersection(tp1) + collect_tp_intersection(tp2)\n        case _:\n            return [tp1]\n\n\ndef collect_tp_union(tp1: e.Tp) -> list[e.Tp]:\n    match tp1:\n        case e.UnionTp(left=tp1, right=tp2):\n            return collect_tp_union(tp1) + collect_tp_union(tp2)\n        case _:\n            return [tp1]\n\n\ndef is_nominal_subtype_in_schema(\n    ctx: e.TcCtx | e.DBSchema,\n    subtype: e.QualifiedName,\n    supertype: e.QualifiedName,\n):\n    if subtype == supertype:\n        return True\n    else:\n        schema = ctx.schema if isinstance(ctx, e.TcCtx) else ctx\n        if subtype in schema.subtyping_relations:\n            if supertype == e.QualifiedName([\"std\", \"anytype\"]):\n                return True\n            return (\n                supertype\n                in subtyping_resolution.find_all_supertypes_of_tp_in_schema(\n                    schema, subtype\n                )\n            )\n        else:\n            return False\n\n\ndef mode_is_optional(m: e.CMMode) -> bool:\n    return isinstance(m.lower, e.ZeroCardinal)\n\n\ndef object_tp_is_essentially_optional(tp: e.ObjectTp) -> bool:\n    return all(mode_is_optional(md_tp.mode) for md_tp in tp.val.values())\n\n\ndef dereference_var_tp(\n    dbschema: e.DBSchema, qn: e.QualifiedName\n) -> e.ObjectTp:\n    resolved = mops.resolve_type_name(dbschema, qn)\n    assert isinstance(resolved, e.ObjectTp)\n    return resolved\n\n\ndef assert_real_subtype(\n    ctx: e.TcCtx,\n    tp1: e.Tp,\n    tp2: e.Tp,\n) -> None:\n    if not check_is_subtype(ctx, tp1, tp2):\n        raise ValueError(\"not subtype\", tp1, tp2)\n    else:\n        pass\n\n\ndef resolve_named_nominal_link_tp(\n    ctx: e.TcCtx, tp: e.NamedNominalLinkTp\n) -> e.NominalLinkTp:\n    match tp:\n        case e.NamedNominalLinkTp(name=e.QualifiedName(n_1), linkprop=lp_1):\n            resolved_type_def = mops.resolve_type_name(\n                ctx, e.QualifiedName(n_1)\n            )\n            assert isinstance(resolved_type_def, e.ObjectTp)\n            return e.NominalLinkTp(\n                subject=resolved_type_def,\n                name=e.QualifiedName(n_1),\n                linkprop=lp_1,\n            )\n        case _:\n            raise ValueError(\"Type Error\")\n\n\ndef type_subtyping_walk(\n    recurse: Callable[[e.TcCtx, e.Tp, e.Tp], bool],\n    ctx: e.TcCtx,\n    tp1: e.Tp,\n    tp2: e.Tp,\n) -> bool:\n    \"\"\"\n    Walks the type equality tree.\n    Subtrees are checked for equality using the recurse function,\n    to account for possible unifications.\n    \"\"\"\n    if tp_is_primitive(tp1) and tp_is_primitive(tp2):\n        if isinstance(tp1, e.ScalarTp) and isinstance(tp2, e.ScalarTp):\n            return is_nominal_subtype_in_schema(ctx, tp1.name, tp2.name)\n        else:\n            match tp1, tp2:\n                case _, e.DefaultTp(tp=tp2_inner, expr=_):\n                    return recurse(ctx, tp1, tp2_inner)\n                case e.DefaultTp(tp=tp1_inner, expr=_), _:\n                    return recurse(ctx, tp1_inner, tp2)\n                case _, e.ComputableTp(tp=tp2_inner, expr=_):\n                    return recurse(ctx, tp1, tp2_inner)\n                case e.ComputableTp(tp=tp1_inner, expr=_), _:\n                    return recurse(ctx, tp1_inner, tp2)\n                case _:\n                    raise ValueError(\"TODO\")\n    else:\n        match tp1, tp2:\n            case _, e.AnyTp():\n                return True\n\n            # Union and intersections before expansion of names\n            case (e.UnionTp(left=tp1_left, right=tp1_right), _):\n                return recurse(ctx, tp1_left, tp2) and recurse(\n                    ctx, tp1_right, tp2\n                )\n\n            case (_, e.UnionTp(left=tp2_left, right=tp2_right)):\n                return recurse(ctx, tp1, tp2_left) or recurse(\n                    ctx, tp1, tp2_right\n                )\n\n            case (_, e.IntersectTp(left=tp2_left, right=tp2_right)):\n                return recurse(ctx, tp1, tp2_left) and recurse(\n                    ctx, tp1, tp2_right\n                )\n\n            case (e.IntersectTp(left=tp1_left, right=tp1_right), _):\n                return recurse(ctx, tp1_left, tp2) or recurse(\n                    ctx, tp1_right, tp2\n                )\n\n            case e.ObjectTp(val=tp1_val), e.ObjectTp(val=tp2_val):\n                if set(tp1_val.keys()) != set(tp2_val.keys()):\n                    return False\n                for k in tp1_val.keys():\n                    if not recurse(ctx, tp1_val[k].tp, tp2_val[k].tp):\n                        return False\n                    if not is_cardinal_subtype(\n                        tp1_val[k].mode, tp2_val[k].mode\n                    ):\n                        return False\n                return True\n            case (\n                e.NominalLinkTp(name=n_1, subject=s_1, linkprop=lp_1),\n                e.NominalLinkTp(name=n_2, subject=s_2, linkprop=lp_2),\n            ):\n\n                if is_nominal_subtype_in_schema(ctx, n_1, n_2):\n                    return recurse(ctx, s_1, s_2) and recurse(ctx, lp_1, lp_2)\n                else:\n                    return False\n            case (\n                e.NamedNominalLinkTp(name=n_1, linkprop=lp_1),\n                e.NamedNominalLinkTp(name=n_2, linkprop=lp_2),\n            ):\n                assert isinstance(n_1, e.QualifiedName)\n                assert isinstance(n_2, e.QualifiedName)\n                if is_nominal_subtype_in_schema(ctx, n_1, n_2):\n                    return recurse(ctx, lp_1, lp_2)\n                else:\n                    return False\n            case (\n                _,\n                e.NamedNominalLinkTp(name=e.QualifiedName(n_2), linkprop=lp_2),\n            ):\n                assert isinstance(tp2, e.NamedNominalLinkTp)\n                return recurse(\n                    ctx, tp1, resolve_named_nominal_link_tp(ctx, tp2)\n                )\n            case (\n                e.NamedNominalLinkTp(name=e.QualifiedName(n_1), linkprop=lp_1),\n                _,\n            ):\n                assert isinstance(tp1, e.NamedNominalLinkTp)\n                return recurse(\n                    ctx, resolve_named_nominal_link_tp(ctx, tp1), tp2\n                )\n\n            # Other structural typing\n            case (\n                e.CompositeTp(kind=kind1, tps=tps1),\n                e.CompositeTp(kind=kind2, tps=tps2),\n            ):\n                if kind1 != kind2 or len(tps1) != len(tps2):\n                    return False\n                else:\n                    return all(\n                        recurse(ctx, tp1, tp2) for tp1, tp2 in zip(tps1, tps2)\n                    )\n\n            case _:\n                return False\n        raise ValueError(\n            \"should not be reachable, check if returns are missing?\", tp1, tp2\n        )\n\n\ndef check_is_subtype(\n    ctx: e.TcCtx,\n    tp1: e.Tp,\n    tp2: e.Tp,\n) -> bool:\n    return type_subtyping_walk(check_is_subtype, ctx, tp1, tp2)\n\n\ndef collect_is_subtype_with_instantiation(\n    ctx: e.TcCtx,\n    syn_tp: e.Tp,\n    ck_tp: e.Tp,\n    some_tp_mapping: dict[int, list[e.Tp]],\n) -> bool:\n    \"\"\"\n    Here, tp2 may be a some type\n    (parametric morphism, and need to be instantiated)\n    \"\"\"\n    if isinstance(ck_tp, e.SomeTp):\n        if ck_tp.index in some_tp_mapping:\n            some_tp_mapping[ck_tp.index] = [\n                syn_tp,\n                *some_tp_mapping[ck_tp.index],\n            ]\n        else:\n            some_tp_mapping[ck_tp.index] = [syn_tp]\n        return True\n    else:\n\n        def recurse(ctx: e.TcCtx, tp1: e.Tp, tp2: e.Tp) -> bool:\n            return collect_is_subtype_with_instantiation(\n                ctx, tp1, tp2, some_tp_mapping\n            )\n\n        return type_subtyping_walk(recurse, ctx, syn_tp, ck_tp)\n\n\ndef check_is_subtype_with_instantiation(\n    ctx: e.TcCtx, syn_tp: e.Tp, ck_tp: e.Tp, some_tp_mapping: dict[int, e.Tp]\n) -> bool:\n    if isinstance(ck_tp, e.SomeTp):\n        if ck_tp.index in some_tp_mapping:\n            real_ck_tp = some_tp_mapping[ck_tp.index]\n            return check_is_subtype_with_instantiation(\n                ctx, syn_tp, real_ck_tp, some_tp_mapping\n            )\n        else:\n            some_tp_mapping[ck_tp.index] = syn_tp\n            return True\n    else:\n\n        def recurse(ctx: e.TcCtx, tp1: e.Tp, tp2: e.Tp) -> bool:\n            return check_is_subtype_with_instantiation(\n                ctx, tp1, tp2, some_tp_mapping\n            )\n\n        return type_subtyping_walk(recurse, ctx, syn_tp, ck_tp)\n\n\ndef recursive_instantiate_tp(\n    tp: e.Tp, some_tp_mapping: dict[int, e.Tp]\n) -> e.Tp:\n    \"\"\"\n    Instantiate all Some(i) sub term in a type.\n    Used to compute parametric function's return type.\n    \"\"\"\n\n    def inst_func(tp: e.Tp) -> Optional[e.Tp]:\n        if isinstance(tp, e.SomeTp):\n            if tp.index in some_tp_mapping:\n                return some_tp_mapping[tp.index]\n            else:\n                raise ValueError(\"some tp not found\")\n        else:\n            return None\n\n    return eops.map_tp(inst_func, tp)\n\n\ndef is_cardinal_subtype(cm: e.CMMode, cm2: e.CMMode) -> bool:\n    return cm2.lower <= cm.lower and cm.upper <= cm2.upper\n\n\ndef assert_cardinal_subtype(cm: e.CMMode, cm2: e.CMMode) -> None:\n    if not (is_cardinal_subtype(cm, cm2)):\n        raise ValueError(\"not subtype\", cm, cm2)\n\n\ndef get_runtime_tp(tp: e.Tp) -> e.Tp:\n    \"\"\"Drops defaults and computed\"\"\"\n\n    def map_func(candidate: e.Tp) -> Optional[e.Tp]:\n        match candidate:\n            case e.ComputableTp(expr=_, tp=c_tp):\n                return get_runtime_tp(c_tp)\n            case e.DefaultTp(expr=_, tp=c_tp):\n                return get_runtime_tp(c_tp)\n            case _:\n                return None\n\n    return eops.map_tp(map_func, tp)\n\n\ndef get_storage_tp(fmt: e.ObjectTp) -> e.ObjectTp:\n    \"\"\"\n    Returns the storage type of a given type.\n    In particular, computable attributes should be removed.\n\n    \"\"\"\n\n    def drop_computable(t: e.ObjectTp):\n        return e.ObjectTp(\n            {\n                k: tp\n                for (k, tp) in t.val.items()\n                if not isinstance(tp.tp, e.ComputableTp)\n            }\n        )\n\n    def get_lp_storage(t: e.ResultTp) -> e.ResultTp:\n        match t.tp:\n            case e.NamedNominalLinkTp(name=n, linkprop=lp):\n                return e.ResultTp(\n                    e.NamedNominalLinkTp(name=n, linkprop=drop_computable(lp)),\n                    t.mode,\n                )\n            case e.DefaultTp(tp=tp, expr=_):\n                return get_lp_storage(e.ResultTp(tp, t.mode))\n            case e.UnionTp(left=left_tp, right=right_tp):\n                return e.ResultTp(\n                    e.UnionTp(\n                        left=get_lp_storage(e.ResultTp(left_tp, t.mode)).tp,\n                        right=get_lp_storage(e.ResultTp(right_tp, t.mode)).tp,\n                    ),\n                    t.mode,\n                )\n            case e.CompositeTp(kind=kind, tps=tps, labels=labels):\n                return e.ResultTp(\n                    e.CompositeTp(\n                        kind=kind,\n                        tps=[\n                            get_lp_storage(e.ResultTp(tp, t.mode)).tp\n                            for tp in tps\n                        ],\n                        labels=labels,\n                    ),\n                    t.mode,\n                )\n            case _:\n                if tp_is_primitive(t.tp):\n                    return t\n                else:\n                    raise ValueError(\"Cannot get lp storage\", t.tp)\n\n    return e.ObjectTp(\n        {\n            k: get_lp_storage(tp)\n            for (k, tp) in fmt.val.items()\n            if not isinstance(tp.tp, e.ComputableTp)\n        }\n    )\n\n\ndef tp_is_primitive(tp: e.Tp) -> bool:\n    match tp:\n        case e.ScalarTp(_):\n            return True\n        case (\n            e.ObjectTp(_)\n            | e.SomeTp(_)\n            | e.NamedNominalLinkTp(_)\n            | e.NominalLinkTp(_)\n            | e.AnyTp()\n        ):\n            return False\n        case e.UnionTp(left=_, right=_) | e.IntersectTp(left=_, right=_):\n            return False  # this case is actually ambiguous\n        case e.ComputableTp(tp=under_tp, expr=_):\n            return tp_is_primitive(under_tp)\n        case e.DefaultTp(tp=under_tp, expr=_):\n            return tp_is_primitive(under_tp)\n        case e.CompositeTp(_):\n            return False\n        case _:\n            raise ValueError(\"Not implemented\", tp)\n\n\ndef match_param_modifier(p: e.ParamModifier, m: e.CMMode) -> e.CMMode:\n    match p:\n        case e.ParamSingleton():\n            return m\n        case e.ParamOptional():\n            return e.CMMode(\n                e.max_cardinal(e.CardNumOne, m.lower),\n                e.max_cardinal(e.CardNumOne, m.upper),\n            )\n        case e.ParamSetOf():\n            return e.CardOne\n        case _:\n            raise ValueError(\"Not possible\")\n\n\ndef is_order_spec(tp: e.ResultTp) -> bool:\n    print(\"WARNING: is_order_spec not implemented\")\n    return True\n\n\ndef is_tp_projection_tuple_proj(tp: e.Tp) -> bool:\n    match tp:\n        case e.CompositeTp(kind=e.CompositeTpKind.Tuple, tps=_):\n            return True\n        case _:\n            return False\n\n\ndef can_project_label_from_tp(\n    ctx: e.TcCtx | e.DBSchema, tp: e.Tp, label: e.Label\n) -> bool:\n    match tp:\n        case e.UnionTp(l, r):\n            return can_project_label_from_tp(\n                ctx, l, label\n            ) and can_project_label_from_tp(ctx, r, label)\n        case e.IntersectTp(l, r):\n            return can_project_label_from_tp(\n                ctx, l, label\n            ) or can_project_label_from_tp(ctx, r, label)\n        case _:\n            pass\n    match label:\n        case e.LinkPropLabel(label=lbl):\n            match tp:\n                case e.NominalLinkTp(name=_, subject=_, linkprop=tp_linkprop):\n                    return can_project_label_from_tp(\n                        ctx, tp_linkprop, e.StrLabel(lbl)\n                    )\n                case e.NamedNominalLinkTp(name=_, linkprop=tp_linkprop):\n                    return can_project_label_from_tp(\n                        ctx, tp_linkprop, e.StrLabel(lbl)\n                    )\n                case _:\n                    raise ValueError(\"LP Label Unimplemented\")\n        case e.StrLabel(label=lbl):\n            match tp:\n                case e.NominalLinkTp(name=_, subject=tp_subject, linkprop=_):\n                    return can_project_label_from_tp(ctx, tp_subject, label)\n                case e.NamedNominalLinkTp(name=name, linkprop=_):\n                    _, tp_def = mops.resolve_raw_name_and_type_def(ctx, name)\n                    assert isinstance(tp_def, e.ObjectTp)\n                    return can_project_label_from_tp(ctx, tp_def, label)\n                case e.ObjectTp(val=tp_obj):\n                    if lbl in tp_obj.keys():\n                        return True\n                    elif lbl == \"id\":\n                        return True\n                    else:\n                        return False\n                case _:\n                    raise ValueError(\"Unimplemented\")\n\n\ndef tp_project(\n    ctx: e.TcCtx | e.DBSchema, tp: e.ResultTp, label: e.Label\n) -> e.ResultTp:\n\n    def post_process_result_base_tp(\n        result_base_tp: e.Tp, result_mode: e.CMMode\n    ) -> e.ResultTp:\n\n        if isinstance(result_base_tp, e.UncheckedTypeName):\n            raise ValueError(\"Must not return UncheckedTypeName\")\n        return e.ResultTp(result_base_tp, result_mode)\n\n    # if isinstance(tp.tp, e.VarTp):\n    #     target_tp = dereference_var_tp(ctx.schema, tp.tp)\n    #     return tp_project(ctx, e.ResultTp(target_tp, tp.mode),\n    #                       label)\n    match tp.tp:\n        case e.UnionTp(_, _):\n            tps = collect_tp_union(tp.tp)\n            result_tps = [\n                tp_project(ctx, e.ResultTp(u_tp, tp.mode), label)\n                for u_tp in tps\n            ]\n            result = result_tps[0]\n            if all(r_tp.mode == result.mode for r_tp in result_tps):\n                return e.ResultTp(\n                    construct_tps_union([r_tp.tp for r_tp in result_tps]),\n                    result.mode,\n                )\n            else:\n                raise ValueError(\"Ambiguous union projection\", result_tps)\n        case e.IntersectTp(_, _):\n            tps = collect_tp_intersection(tp.tp)\n            projectable_tps = [\n                itp\n                for itp in tps\n                if can_project_label_from_tp(ctx, itp, label)\n            ]\n            if len(projectable_tps) == 0:\n                raise edgedb.InvalidReferenceError(\n                    f\"property '{(label.label)}' does not exist.\"\n                )\n            else:\n                projected_tps = [\n                    tp_project(ctx, e.ResultTp(itp, tp.mode), label)\n                    for itp in projectable_tps\n                ]\n                if all(\n                    r_tp.mode == projected_tps[0].mode\n                    for r_tp in projected_tps\n                ):\n                    return e.ResultTp(\n                        construct_tps_union(\n                            [r_tp.tp for r_tp in projected_tps]\n                        ),\n                        projected_tps[0].mode,\n                    )\n                else:\n                    raise ValueError(\n                        \"Ambiguous intersection projection\", projected_tps\n                    )\n        case _:\n            pass\n\n    match label:\n        case e.LinkPropLabel(label=lbl):\n            match tp.tp:\n                case e.NominalLinkTp(name=_, subject=_, linkprop=tp_linkprop):\n                    return tp_project(\n                        ctx, e.ResultTp(tp_linkprop, tp.mode), e.StrLabel(lbl)\n                    )\n                case e.NamedNominalLinkTp(name=_, linkprop=tp_linkprop):\n                    return tp_project(\n                        ctx, e.ResultTp(tp_linkprop, tp.mode), e.StrLabel(lbl)\n                    )\n                case _:\n                    raise ValueError(\n                        \"Cannot tp_project a linkprop \"\n                        \"from a non linkprop type\",\n                        pp.show(tp.tp),\n                    )\n        case e.StrLabel(label=lbl):\n            match tp.tp:\n                case e.NominalLinkTp(name=_, subject=tp_subject, linkprop=_):\n                    return tp_project(\n                        ctx, e.ResultTp(tp_subject, tp.mode), e.StrLabel(lbl)\n                    )\n                case e.NamedNominalLinkTp(name=name, linkprop=_):\n                    _, tp_def = mops.resolve_raw_name_and_type_def(ctx, name)\n                    assert isinstance(tp_def, e.ObjectTp)\n                    return tp_project(\n                        ctx, e.ResultTp(tp_def, tp.mode), e.StrLabel(lbl)\n                    )\n                case e.ObjectTp(val=tp_obj):\n                    if lbl in tp_obj.keys():\n                        result_base_tp = tp_obj[lbl].tp\n                        result_mode = tp.mode * tp_obj[lbl].mode\n                        return post_process_result_base_tp(\n                            result_base_tp, result_mode\n                        )\n                    elif lbl == \"id\":\n                        return post_process_result_base_tp(e.UuidTp(), tp.mode)\n                    elif lbl == \"__type__\":\n                        return post_process_result_base_tp(\n                            e.NamedNominalLinkTp(\n                                name=e.QualifiedName([\"schema\", \"ObjectType\"]),\n                                linkprop=e.ObjectTp({}),\n                            ),\n                            tp.mode,\n                        )\n                    else:\n                        raise ValueError(\"Label not found\", lbl, tp_obj.keys())\n                case e.CompositeTp(\n                    kind=e.CompositeTpKind.Tuple, tps=tp_tuple, labels=lbls\n                ):\n                    if lbl.isdigit():\n                        result_base_tp = tp_tuple[int(lbl)]\n                        result_mode = tp.mode\n                        return post_process_result_base_tp(\n                            result_base_tp, result_mode\n                        )\n                    if lbl in lbls:\n                        result_base_tp = tp_tuple[lbls.index(lbl)]\n                        result_mode = tp.mode\n                        return post_process_result_base_tp(\n                            result_base_tp, result_mode\n                        )\n                    else:\n                        raise ValueError(\"Label not found\")\n                case _:\n                    raise ValueError(\n                        \"Cannot tp_project a label \",\n                        label,\n                        \"from a non object type\",\n                        pp.show(tp.tp),\n                    )\n\n\ndef combine_object_tp(o1: e.ObjectTp, o2: e.ObjectTp) -> e.ObjectTp:\n    if isinstance(o1, e.ObjectTp) and isinstance(o2, e.ObjectTp):\n        return e.ObjectTp({**o1.val, **o2.val})\n    else:\n        raise ValueError(\n            \"not implemented combine object tp\", pp.show(o1), pp.show(o2)\n        )\n\n\ndef combine_tp_with_subject_tp(ctx: e.TcCtx, o1: e.Tp, o2: e.ObjectTp) -> e.Tp:\n    match o1:\n        case e.NominalLinkTp(\n            name=name, subject=subject_tp, linkprop=linkprop_tp\n        ):\n            return e.NominalLinkTp(\n                name=name,\n                subject=combine_object_tp(subject_tp, o2),\n                linkprop=linkprop_tp,\n            )\n        case e.NamedNominalLinkTp(name=name, linkprop=linkprop_tp):\n            return combine_tp_with_subject_tp(\n                ctx, resolve_named_nominal_link_tp(ctx, o1), o2\n            )\n        case e.UnionTp(l, r):\n            return create_union_tp(\n                combine_tp_with_subject_tp(ctx, l, o2),\n                combine_tp_with_subject_tp(ctx, r, o2),\n            )\n        case e.IntersectTp(l, r):\n            return create_intersect_tp(\n                combine_tp_with_subject_tp(ctx, l, o2),\n                combine_tp_with_subject_tp(ctx, r, o2),\n            )\n        case _:\n            raise ValueError(\n                \"not implemented combine tp\", pp.show(o1), pp.show(o2)\n            )\n\n\ndef combine_tp_with_linkprop_tp(\n    ctx: e.TcCtx, o1: e.Tp, o2: e.ObjectTp\n) -> e.Tp:\n    match o1:\n        case e.NominalLinkTp(\n            name=name, subject=subject_tp, linkprop=linkprop_tp\n        ):\n            return e.NominalLinkTp(\n                name=name,\n                subject=subject_tp,\n                linkprop=combine_object_tp(linkprop_tp, o2),\n            )\n        case e.NamedNominalLinkTp(name=name, linkprop=linkprop_tp):\n            return combine_tp_with_linkprop_tp(\n                ctx, resolve_named_nominal_link_tp(ctx, o1), o2\n            )\n        case e.UnionTp(l, r):\n            return e.UnionTp(\n                combine_tp_with_linkprop_tp(ctx, l, o2),\n                combine_tp_with_linkprop_tp(ctx, r, o2),\n            )\n        case e.IntersectTp(l, r):\n            return e.IntersectTp(\n                combine_tp_with_linkprop_tp(ctx, l, o2),\n                combine_tp_with_linkprop_tp(ctx, r, o2),\n            )\n        case _:\n            raise ValueError(\n                \"not implemented combine tp\", pp.show(o1), pp.show(o2)\n            )\n\n\ndef create_union_tp(tp1: e.Tp, tp2: e.Tp) -> e.Tp:\n    if tp1 == tp2:\n        return tp1\n    else:\n        return e.UnionTp(tp1, tp2)\n\n\ndef create_intersect_tp(tp1: e.Tp, tp2: e.Tp) -> e.Tp:\n    if tp1 == tp2:\n        return tp1\n    else:\n        return e.IntersectTp(tp1, tp2)\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/data/val_to_json.py",
    "content": "from typing import Any, Sequence\nimport json\nfrom . import data_ops as e\nfrom .data_ops import (\n    ArrVal,\n    Label,\n    LinkPropLabel,\n    MultiSetVal,\n    NamedTupleVal,\n    ObjectVal,\n    RefVal,\n    StrLabel,\n    UnnamedTupleVal,\n    Val,\n    Visible,\n)\nfrom . import type_ops as tops\nfrom . import expr_to_str as pp\n\njson_like = str | int | bool | dict[str, Any] | Sequence[Any]\n\n\ndef label_to_str(lbl: Label) -> str:\n    match lbl:\n        case StrLabel(s):\n            return s\n        case LinkPropLabel(s):\n            return \"@\" + s\n    raise ValueError(\"MATCH\")\n\n\ndef objectval_to_json_like(objv: ObjectVal) -> dict[str, json_like]:\n    return {\n        label_to_str(k): multi_set_val_to_json_like(v[1])\n        for (k, v) in objv.val.items()\n        if isinstance(v[0], Visible)\n    }\n\n\ndef val_to_json_like(v: Val) -> json_like:\n    match v:\n        case e.ScalarVal(_, v):\n            if isinstance(v, int) or isinstance(v, str) or isinstance(v, bool):\n                return v\n            else:\n                raise ValueError(\"not implemented\")\n        case RefVal(refid, _, object):\n            object_val_result = objectval_to_json_like(object)\n            if len(object_val_result) == 0:\n                object_val_result['id'] = refid\n            return object_val_result\n        case ArrVal(val=array):\n            return [val_to_json_like(v) for v in array]\n        case UnnamedTupleVal(val=array):\n            return [val_to_json_like(v) for v in array]\n        case NamedTupleVal(val=dic):\n            return {k: val_to_json_like(v) for (k, v) in dic.items()}\n    raise ValueError(\"MATCH\", v)\n\n\ndef multi_set_val_to_json_like(m: MultiSetVal) -> json_like:\n    # do not dedup when converting to json (see test_edgeql_shape_for_01)\n    result = [val_to_json_like(v) for v in m.getRawVals()]\n    return result\n\n\ndef typed_objectval_to_json_like(\n    objv: ObjectVal, obj_tp: e.Tp, dbschema: e.DBSchema\n) -> dict[str, json_like]:\n    result: dict[str, json_like] = {}\n    for k, v in objv.val.items():\n        if isinstance(v[0], Visible):\n            sub_tp = tops.tp_project(\n                dbschema, e.ResultTp(tp=obj_tp, mode=e.CardOne), k\n            )\n            result[label_to_str(k)] = typed_multi_set_val_to_json_like(\n                sub_tp, v[1], dbschema\n            )\n    return result\n\n\ndef typed_val_to_json_like(\n    v: Val, tp: e.Tp, dbschema: e.DBSchema\n) -> json_like:\n    match v:\n        case e.ScalarVal(s_tp, v):\n            if s_tp == e.ScalarTp(e.QualifiedName([\"std\", \"json\"])):\n                return json.dumps(v)\n            elif (\n                isinstance(v, int)\n                or isinstance(v, str)\n                or isinstance(v, bool)\n                or isinstance(v, float)\n            ):\n                return v\n            else:\n                raise ValueError(\"not implemented\")\n        case RefVal(refid, _, object):\n            if not isinstance(\n                tp,\n                e.ObjectTp\n                | e.NominalLinkTp\n                | e.NamedNominalLinkTp\n                | e.UnionTp\n                | e.IntersectTp,\n            ):\n                raise ValueError(\"Expecing objecttp\", tp)\n            object_val_result = typed_objectval_to_json_like(\n                object, tp, dbschema\n            )\n            if len(object_val_result) == 0:\n                object_val_result['id'] = refid\n            return object_val_result\n        case ArrVal(val=array):\n            match tp:\n                case e.CompositeTp(kind=e.CompositeTpKind.Array, tps=tps):\n                    return [\n                        typed_val_to_json_like(v, tps[0], dbschema)\n                        for v in array\n                    ]\n                case e.UnionTp(_, _):\n                    tps = tops.collect_tp_union(tp)\n                    if all(\n                        isinstance(tp, e.CompositeTp)\n                        and tp.kind == e.CompositeTpKind.Array\n                        for tp in tps\n                    ):\n                        return [\n                            typed_val_to_json_like(\n                                v,\n                                tops.construct_tps_union(\n                                    [tp.tps[0] for tp in tps]  # type: ignore\n                                ),\n                                dbschema,\n                            )\n                            for v in array\n                        ]\n                    else:\n                        raise ValueError(\"Expecing array tp\", pp.show(tp))\n                case _:\n                    raise ValueError(\"Expecing array tp\", pp.show(tp))\n        case UnnamedTupleVal(val=array):\n            if (\n                not isinstance(tp, e.CompositeTp)\n                or tp.kind != e.CompositeTpKind.Tuple\n            ):\n                match tp:\n                    case e.IntersectTp(_, _):\n                        all_i_tps = tops.collect_tp_intersection(tp)\n                        if all(\n                            isinstance(tp, e.CompositeTp)\n                            and tp.kind == e.CompositeTpKind.Tuple\n                            for tp in all_i_tps\n                        ):\n                            tps = all_i_tps[0].tps  # type: ignore\n                    case e.UnionTp(_, _):\n                        all_u_tps = tops.collect_tp_union(tp)\n                        if all(\n                            isinstance(tp, e.CompositeTp)\n                            and tp.kind == e.CompositeTpKind.Tuple\n                            for tp in all_u_tps\n                        ):\n                            if all(\n                                len(all_u_tps[0].tps) == len(tp.tps)  # type: ignore\n                                for tp in all_u_tps\n                            ):\n                                tps = [\n                                    tops.construct_tps_union(\n                                        [\n                                            tp.tps[i]  # type: ignore\n                                            for tp in all_u_tps\n                                        ]\n                                    )\n                                    for i in range(len(all_u_tps[0].tps))  # type: ignore\n                                ]\n                    case _:\n                        raise ValueError(\n                            \"Expecing unnamed tuple tp\", pp.show(tp)\n                        )\n            else:\n                tps = tp.tps\n            return [\n                typed_val_to_json_like(v, t, dbschema)\n                for (v, t) in zip(array, tps, strict=True)\n            ]\n        case NamedTupleVal(val=dic):\n            assert (\n                isinstance(tp, e.CompositeTp)\n                and tp.kind == e.CompositeTpKind.Tuple\n            )\n            return {\n                k: typed_val_to_json_like(\n                    v, tp.tps[tp.labels.index(k)], dbschema\n                )\n                for (k, v) in dic.items()\n            }\n    raise ValueError(\"MATCH\", v)\n\n\ndef typed_multi_set_val_to_json_like(\n    tp: e.ResultTp, m: MultiSetVal, dbschema: e.DBSchema, top_level=False\n) -> json_like:\n    \"\"\"\n    Convert a MultiSetVal to a JSON-like value.\n\n    param top_level: If True, the result is a list of values, even if\n                     the result's type is a singleton.\n    \"\"\"\n    if tp.mode.upper == e.CardNumOne:\n        if len(m.getVals()) > 1:\n            raise ValueError(\"Single Multiset must have cardinality at most 1\")\n        if len(m.getVals()) == 1:\n            result = typed_val_to_json_like(m.getVals()[0], tp.tp, dbschema)\n            if top_level:\n                result = [result]\n        else:\n            if top_level:\n                result = []\n            else:\n                result = []\n    else:\n        # do not dedup when converting to json (see test_edgeql_shape_for_01)\n        result = [\n            typed_val_to_json_like(v, tp.tp, dbschema) for v in m.getRawVals()\n        ]\n    return result\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/db_interface.py",
    "content": "from __future__ import annotations\nfrom typing import Sequence\nfrom .data import data_ops as e\nfrom .data import expr_ops as eops\nimport copy\n\n\nclass EdgeDatabaseStorageProviderInterface:\n\n    # filters have intersection semantics\n    def query_ids_for_a_type(\n        self, tp: e.QualifiedName, filters: e.EdgeDatabaseSelectFilter\n    ) -> list[e.EdgeID]:\n        raise NotImplementedError()\n\n    def get_schema(self) -> e.DBSchema:\n        raise NotImplementedError()\n\n    def next_id(self) -> e.EdgeID:\n        raise NotImplementedError()\n\n    def insert(\n        self,\n        id: e.EdgeID,\n        tp: e.QualifiedName,\n        props: dict[str, e.MultiSetVal],\n    ) -> None:\n        raise NotImplementedError()\n\n    def delete(self, id: e.EdgeID, tp: e.QualifiedName) -> None:\n        raise NotImplementedError()\n\n    def update(\n        self,\n        id: e.EdgeID,\n        tp: e.QualifiedName,\n        props: dict[str, e.MultiSetVal],\n    ) -> None:\n        raise NotImplementedError()\n\n    def check_id_present(self, id: e.EdgeID) -> bool:\n        raise NotImplementedError()\n\n    # project a property/link from an object\n    def project(\n        self, id: e.EdgeID, tp: e.QualifiedName, property: str\n    ) -> e.MultiSetVal:\n        raise NotImplementedError()\n\n    # get all reverse links for a given object in a set of\n    # objects, including its link properties\n    # That is, return a list of ids which has a link (via the given property)\n    # to any object in the given set\n    def reverse_project(\n        self, ids: Sequence[e.EdgeID], property: str\n    ) -> e.MultiSetVal:\n        raise NotImplementedError()\n\n    def dump_state(self) -> object:\n        raise NotImplementedError()\n\n    def restore_state(self, dumped_state) -> None:\n        raise NotImplementedError()\n\n    def commit(self) -> None:\n        raise NotImplementedError()\n\n\nclass InMemoryEdgeDatabaseStorageProvider(\n    EdgeDatabaseStorageProviderInterface\n):\n\n    def __init__(self, schema) -> None:\n        super().__init__()\n        self.schema = schema\n        self.db = e.DB({})\n        self.next_id_to_return = 1\n\n    def get_schema(self) -> e.DBSchema:\n        return self.schema\n\n    def query_ids_for_a_type(\n        self, tp: e.QualifiedName, filters: e.EdgeDatabaseSelectFilter\n    ) -> list[e.EdgeID]:\n        def check_filter(filter: e.EdgeDatabaseEqFilter, id: e.EdgeID) -> bool:\n            data_to_check = self.db.dbdata[id].data\n            target_vals = data_to_check[filter.propname]\n            assert isinstance(filter.arg, e.MultiSetVal)\n            return any(\n                eops.val_eq(v, vv)\n                for v in target_vals.getVals()\n                for vv in filter.arg.getVals()\n            )\n\n        def check_filter_top(\n            filter: e.EdgeDatabaseSelectFilter, id: e.EdgeID\n        ) -> bool:\n            match filter:\n                case e.EdgeDatabaseEqFilter(propname, arg):\n                    return check_filter(\n                        e.EdgeDatabaseEqFilter(propname, arg), id\n                    )\n                case e.EdgeDatabaseConjunctiveFilter(filters):\n                    return all(check_filter_top(f, id) for f in filters)\n                case e.EdgeDatabaseDisjunctiveFilter(filters):\n                    return any(check_filter_top(f, id) for f in filters)\n                case e.EdgeDatabaseTrueFilter():\n                    return True\n                case _:\n                    raise ValueError(\"Unsupported filter type\")\n\n        return [\n            id\n            for id in self.db.dbdata.keys()\n            if self.db.dbdata[id].tp == tp and check_filter_top(filters, id)\n        ]\n\n    def dump_state(self) -> object:\n        return {\n            \"db\": copy.deepcopy(self.db.dbdata),\n            \"next_id_to_return\": self.next_id_to_return,\n        }\n\n    def restore_state(self, dumped_state) -> None:\n        self.db = e.DB(copy.copy(dumped_state[\"db\"]))\n        self.next_id_to_return = dumped_state[\"next_id_to_return\"]\n\n    def project(\n        self, id: e.EdgeID, tp: e.QualifiedName, prop: str\n    ) -> e.MultiSetVal:\n        if id in self.db.dbdata.keys():\n            props = self.db.dbdata[id].data\n        else:\n            raise ValueError(f\"ID {id} not found in database\")\n        if prop in props:\n            return props[prop]\n        else:\n            raise ValueError(f\"Property {prop} not found in object {id}\")\n\n    def check_id_present(self, id: e.EdgeID) -> bool:\n        return id in self.db.dbdata.keys()\n\n    def reverse_project(\n        self, subject_ids: Sequence[e.EdgeID], prop: str\n    ) -> e.MultiSetVal:\n        results: list[e.Val] = []\n        for id, obj in self.db.dbdata.items():\n            if prop in obj.data.keys():\n                object_vals = obj.data[prop].getVals()\n                if all(\n                    isinstance(object_val, e.RefVal)\n                    for object_val in object_vals\n                ):\n                    object_id_mapping = {\n                        object_val.refid: object_val.val\n                        for object_val in object_vals\n                        if isinstance(object_val, e.RefVal)\n                    }\n                    for (\n                        object_id,\n                        obj_linkprop_val,\n                    ) in object_id_mapping.items():\n                        if not all(\n                            isinstance(lbl, e.LinkPropLabel)\n                            for lbl in obj_linkprop_val.val.keys()\n                        ):\n                            raise ValueError(\n                                \"Expecting only link prop vals in store\"\n                            )\n                        if object_id in subject_ids:\n                            results = [\n                                *results,\n                                e.RefVal(\n                                    refid=id,\n                                    tpname=obj.tp,\n                                    val=obj_linkprop_val,\n                                ),\n                            ]\n        return e.ResultMultiSetVal(results)\n\n    def next_id(self) -> e.EdgeID:\n        id = self.next_id_to_return\n        self.next_id_to_return += 1\n        return id\n\n    def insert(\n        self,\n        id: e.EdgeID,\n        tp: e.QualifiedName,\n        props: dict[str, e.MultiSetVal],\n    ) -> None:\n        self.db.dbdata[id] = e.DBEntry(tp, props)\n\n    def delete(self, id: e.EdgeID, tp: e.QualifiedName) -> None:\n        del self.db.dbdata[id]\n\n    def update(\n        self,\n        id: e.EdgeID,\n        tp: e.QualifiedName,\n        props: dict[str, e.MultiSetVal],\n    ) -> None:\n        if id not in self.db.dbdata.keys():\n            raise ValueError(f\"ID {id} not found in database\")\n        self.db.dbdata[id] = e.DBEntry(\n            tp=self.db.dbdata[id].tp, data={**self.db.dbdata[id].data, **props}\n        )\n\n    def commit(self) -> None:\n        pass\n\n\nclass EdgeDatabase:\n\n    def __init__(self, storage: EdgeDatabaseStorageProviderInterface) -> None:\n        super().__init__()\n        self.storage = storage\n        self.to_delete: list[tuple[e.QualifiedName, e.EdgeID]] = []\n        self.to_update: dict[\n            e.EdgeID, tuple[e.QualifiedName, dict[str, e.MultiSetVal]]\n        ] = {}\n        self.to_insert = e.DB({})\n\n    def dump_state(self) -> object:\n        return {\n            \"storage\": self.storage.dump_state(),\n            \"to_delete\": copy.deepcopy(self.to_delete),\n            \"to_update\": copy.deepcopy(self.to_update),\n            \"to_insert\": copy.deepcopy(self.to_insert),\n        }\n\n    def restore_state(self, dumped_state) -> None:\n        self.storage.restore_state(dumped_state[\"storage\"])\n        self.to_delete = copy.copy(dumped_state[\"to_delete\"])\n        self.to_update = copy.copy(dumped_state[\"to_update\"])\n        self.to_insert = copy.copy(dumped_state[\"to_insert\"])\n\n    def project(\n        self, id: e.EdgeID, tp: e.QualifiedName, prop: str\n    ) -> e.MultiSetVal:\n        if id in self.to_insert.dbdata.keys():\n            raise ValueError(\n                \"Semantic Change: Insert should carry \"\n                \"properties before storage coercion\"\n            )\n\n        result = self.storage.project(id, tp, prop)\n        assert isinstance(result, e.MultiSetVal)\n        return result\n\n    def delete(self, id: e.EdgeID, tp: e.QualifiedName) -> None:\n        self.to_delete.append((tp, id))\n\n    def insert(\n        self, tp: e.QualifiedName, props: dict[str, e.MultiSetVal]\n    ) -> e.EdgeID:\n        id = self.storage.next_id()\n        self.to_insert.dbdata[id] = e.DBEntry(tp, props)\n        return id\n\n    def update(\n        self,\n        id: e.EdgeID,\n        tp: e.QualifiedName,\n        props: dict[str, e.MultiSetVal],\n    ) -> None:\n        if id in self.to_insert.dbdata.keys():\n            self.to_insert.dbdata[id] = e.DBEntry(\n                tp=self.to_insert.dbdata[id].tp,\n                data={**self.to_insert.dbdata[id].data, **props},\n            )\n        else:\n            self.to_update[id] = (tp, props)\n\n    def commit_dml(self) -> None:\n        # updates must happen after insert because it may update inserted data\n        for id, insert_obj in self.to_insert.dbdata.items():\n            self.storage.insert(id, insert_obj.tp, insert_obj.data)\n        for id, (tpname, obj) in self.to_update.items():\n            self.storage.update(id, tpname, obj)\n        # delete happens last, you may also delete an inserted object\n        for tpname, id in self.to_delete:\n            self.storage.delete(id, tpname)\n        self.to_delete = []\n        self.to_update = {}\n        self.to_insert = e.DB({})\n        self.storage.commit()\n\n    def get_schema(self) -> e.DBSchema:\n        return self.storage.get_schema()\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/edb_entry.py",
    "content": "import click\nfrom edb.tools.edb import edbcommands\n\nfrom .new_interpreter import repl\n\nimport os\n\n\n@edbcommands.command(\n    'exp-interp', context_settings={\"help_option_names\": [\"-h\", \"--help\"]}\n)\n@click.option(\n    \"--init-ql-file\",\n    type=str,\n    required=False,\n    help=\"run this edgeql file on startup\",\n)\n@click.option(\n    \"--next-ql-file\",\n    type=str,\n    required=False,\n    help=\"run this edgeql file after startup\",\n)\n@click.option(\n    \"--init-sdl-file\",\n    type=str,\n    required=False,\n    help=\"initialize schema to be this file, schema\" \"should not be place\",\n)\n@click.option(\n    \"--library-ddl-files\",\n    '-l',\n    multiple=True,\n    type=str,\n    help=\"standard library files\",\n)\n@click.option(\"--trace-to-file\", type=str, required=False)\n@click.option(\"--sqlite-file\", type=str, required=False)\n@click.option(\n    \"--test\",\n    type=str,\n    required=False,\n    help=\"\"\"specify a single name.\nWill search the test schema directory for esdl file containing the case\ninsensitive specified esdl file.\nWill also load the corresponding ql file.\nWill turn on trace-to-file to a default html file.\nWill populate next ql file if it exists.\n              \"\"\",\n)\n@click.option(\n    \"--no-setup\",\n    is_flag=True,\n    default=False,\n    required=False,\n    help=\"\"\"--test only, do not include a setup file. \"\"\",\n)\n@click.option(\n    \"-y\", \"--skip-test-confirm\", default=False, required=False, is_flag=True\n)\n@click.option(\"-v\", \"--verbose\", default=False, required=False, is_flag=True)\ndef interperter_entry(\n    *,\n    init_sdl_file=None,\n    next_ql_file=None,\n    init_ql_file=None,\n    verbose=False,\n    trace_to_file=None,\n    sqlite_file=None,\n    library_ddl_files=None,\n    test=None,\n    no_setup=False,\n    skip_test_confirm=False,\n) -> None:\n\n    if test:\n        schemas_dir = os.path.join(\n            os.path.dirname(__file__), '..', '..', '..', 'tests', 'schemas'\n        )\n        print(\"Schemas are in \", schemas_dir)\n        # search root of test schemas for esdl file\n        candidate_files = []\n        failed_files = []\n        for root, _, files in os.walk(schemas_dir):\n            for file in files:\n                if file.endswith('.esdl'):\n                    if test.lower() in file.lower():\n                        candidate_files.append(os.path.join(root, file))\n                    else:\n                        failed_files.append(os.path.join(root, file))\n\n        if len(candidate_files) == 0:\n            print(f'Could not find any esdl files containing {test}')\n            print('Found these files:', failed_files)\n            return\n        if len(candidate_files) > 1:\n            print(f'Found multiple esdl files containing {test}:')\n            for file in candidate_files:\n                print(file)\n            return\n        if init_sdl_file is None:\n            init_sdl_file = candidate_files[0]\n\n        ql_file = candidate_files[0].replace('.esdl', '_setup.edgeql')\n        if init_ql_file is None:\n            init_ql_file = ql_file\n\n        if next_ql_file is None and os.path.exists(\n            \"temp_current_testing.edgeql\"\n        ):\n            next_ql_file = \"temp_current_testing.edgeql\"\n\n        if no_setup:\n            init_ql_file = None\n            next_ql_file = None\n\n        if trace_to_file is None:\n            trace_to_file = \"temp_debug.html\"\n\n        if verbose is False:\n            verbose = True\n\n        # print all options\n        print(f'Running test {test} with options:')\n        print(\n            f'init_sdl_file: '\n            + (\n                init_sdl_file\n                if not init_sdl_file.startswith(schemas_dir)\n                else \"<s_dir>\" + init_sdl_file[len(schemas_dir) :]\n            )\n        )\n        if init_ql_file:\n            print(\n                'init_ql_file: '\n                + (\n                    init_ql_file\n                    if not init_ql_file.startswith(schemas_dir)\n                    else \"<s_dir>\" + init_ql_file[len(schemas_dir) :]\n                )\n            )\n        else:\n            print(f'init_ql_file: None')\n        print(f'next_ql_file: {next_ql_file}')\n        print(f'trace_to_file: {trace_to_file}')\n        print(f'verbose: {verbose}')\n\n        if not skip_test_confirm:\n            if input('Continue? (Y/n)') == 'n':\n                return\n\n    \"\"\" Run the experimental interpreter for EdgeQL \"\"\"\n    repl(\n        init_sdl_file=init_sdl_file,\n        init_ql_file=init_ql_file,\n        next_ql_file=next_ql_file,\n        library_ddl_files=library_ddl_files,\n        debug_print=verbose,\n        trace_to_file_path=trace_to_file,\n        sqlite_file=sqlite_file,\n    )\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/elab_schema.py",
    "content": "from typing import Any, Optional, Sequence, cast\n\nfrom edb.edgeql import ast as qlast\n\nfrom .data.data_ops import CMMode, ObjectTp, ResultTp, Tp\nfrom .elaboration import (\n    elab_single_type_expr,\n    elab_expr_with_default_head,\n    elab,\n    DEFAULT_HEAD_NAME,\n)\nfrom .helper_funcs import parse_sdl\nfrom .data import data_ops as e\nfrom .data import expr_ops as eops\nfrom .type_checking_tools import schema_checking as tck\nfrom .interpreter_logging import print_warning\nfrom .schema import function_elaboration as fun_elab\n\n\ndef elab_schema_error(obj: Any) -> Any:\n    raise ValueError(obj)\n\n\ndef elab_schema_cardinality(\n    is_required: Optional[bool],\n    cardinality: Optional[qlast.qltypes.SchemaCardinality],\n) -> CMMode:\n    return CMMode(\n        e.CardNumOne if is_required else e.CardNumZero,\n        (\n            e.CardNumInf\n            if cardinality == qlast.qltypes.SchemaCardinality.Many\n            else e.CardNumOne\n        ),\n    )\n\n\ndef elab_schema_target_tp(\n    target: Optional[qlast.Expr | qlast.TypeExpr]\n) -> Tp:\n    return (\n        elab_single_type_expr(target)\n        if isinstance(target, qlast.TypeExpr)\n        else elab_schema_error(target)\n    )\n\n\ndef construct_final_schema_target_tp(\n    base: Tp, linkprops: dict[str, ResultTp]\n) -> Tp:\n    if linkprops:\n        match base:\n            case e.UnionTp(tp1, tp2):\n                return e.UnionTp(\n                    construct_final_schema_target_tp(tp1, linkprops),\n                    construct_final_schema_target_tp(tp2, linkprops),\n                )\n            case e.UncheckedTypeName(e.QualifiedName(name)):\n                return e.NamedNominalLinkTp(\n                    name=e.QualifiedName(name), linkprop=ObjectTp(linkprops)\n                )\n            case e.UncheckedTypeName(e.UnqualifiedName(name)):\n                return e.NamedNominalLinkTp(\n                    name=e.UnqualifiedName(name), linkprop=ObjectTp(linkprops)\n                )\n            case e.OverloadedTargetTp(linkprop=None):\n                return e.OverloadedTargetTp(linkprop=ObjectTp(linkprops))\n            case _:\n                if linkprops:\n                    raise ValueError(\n                        \"cannot construct schema target type\", base, linkprops\n                    )\n                else:\n                    return base\n    else:\n        return base\n\n\ndef elab_create_object_tp(\n    commands: list[qlast.DDLOperation],\n) -> tuple[ObjectTp, Sequence[e.Constraint], Sequence[Sequence[str]]]:\n    object_tp_content: dict[str, ResultTp] = {}\n    constrants: list[e.Constraint] = []\n    indexes: list[list[str]] = []\n    for cmd in commands:\n        match cmd:\n            case qlast.CreateConcretePointer(\n                bases=_,\n                name=qlast.ObjectRef(name=pname),\n                target=ptarget,\n                is_required=p_is_required,\n                cardinality=p_cardinality,\n                declared_overloaded=declared_overloaded,\n                commands=pcommands,\n            ):\n                base_target_type: Tp\n                if ptarget is None:\n                    if declared_overloaded:\n                        base_target_type = e.OverloadedTargetTp(linkprop=None)\n                    else:\n                        raise ValueError(\"expecting target\")\n                if isinstance(ptarget, qlast.TypeExpr):\n                    base_target_type = elab_schema_target_tp(ptarget)\n                elif isinstance(ptarget, qlast.Expr):\n                    base_target_type = e.UncheckedComputableTp(\n                        elab_expr_with_default_head(ptarget)\n                    )\n                else:\n                    print_warning(\"WARNING: not implemented ptarget\", ptarget)\n                link_property_tps: dict[str, ResultTp] = {}\n                p_has_set_default: Optional[e.BindingExpr] = None\n                for pcmd in pcommands:\n                    match pcmd:\n                        case qlast.CreateConcretePointer(\n                            bases=_,\n                            name=qlast.ObjectRef(name=plname),\n                            target=pltarget,\n                            is_required=pl_is_required,\n                            cardinality=pl_cardinality,\n                            commands=plcommands,\n                        ):\n                            pl_has_set_default: Optional[e.BindingExpr] = None\n                            if plcommands:\n                                for plcommand in plcommands:\n                                    match plcommand:\n                                        case qlast.SetField(\n                                            name=set_field_name,  # noqa: E501\n                                            value=set_field_value,\n                                        ):  # noqa: E501\n                                            match set_field_name:\n                                                case \"default\":\n                                                    assert isinstance(\n                                                        set_field_value,\n                                                        qlast.Expr,\n                                                    )  # noqa: E501\n                                                    pl_has_set_default = elab_expr_with_default_head(  # noqa: E501  # noqa: E501\n                                                        set_field_value\n                                                    )  # noqa: E501\n                                                case _:\n                                                    print(\n                                                        \"WARNING: \"\n                                                        \"not \"\n                                                        \"implemented \"  # noqa: E501\n                                                        \"set_field_name\",  # noqa: E501\n                                                        set_field_name,\n                                                    )  # noqa: E501\n                                        case _:\n                                            print(\n                                                \"WARNING: not \"\n                                                \"implemented plcmd\",  # noqa: E501\n                                                plcommand,\n                                            )\n                            if isinstance(pltarget, qlast.TypeExpr):\n                                lp_base_tp = elab_schema_target_tp(pltarget)\n                            elif isinstance(pltarget, qlast.Expr):\n                                lp_base_tp = e.UncheckedComputableTp(\n                                    elab_expr_with_default_head(pltarget)\n                                )\n                            else:\n                                print(\n                                    \"WARNING: \" \"not implemented pltarget\",\n                                    pltarget,\n                                )\n                            if pl_has_set_default is not None:\n                                assert not isinstance(\n                                    lp_base_tp, e.UncheckedComputableTp\n                                )\n                                assert not isinstance(\n                                    lp_base_tp, e.ComputableTp\n                                )\n                                lp_base_tp = e.DefaultTp(\n                                    pl_has_set_default, lp_base_tp\n                                )\n                            link_property_tps = {\n                                **link_property_tps,\n                                plname: ResultTp(\n                                    lp_base_tp,\n                                    elab_schema_cardinality(\n                                        pl_is_required, pl_cardinality\n                                    ),\n                                ),\n                            }\n                        case qlast.CreateConcreteConstraint(\n                            name=name,\n                            args=args,\n                            delegated=delegated,\n                        ):\n                            if args:\n                                print_warning(\n                                    \"WARNING: not implemented args\", args\n                                )\n                            else:\n                                match name:\n                                    case qlast.ObjectRef(\n                                        name='exclusive', module=('std' | None)\n                                    ):\n                                        constrants.append(\n                                            e.ExclusiveConstraint(\n                                                name=pname, delegated=delegated\n                                            )\n                                        )\n                                    case _:\n                                        print_warning(\n                                            \"WARNING: not implemented pcmd\"\n                                            \" (constraint)\",\n                                            pcmd,\n                                        )\n                        case qlast.SetField(\n                            name=set_field_name, value=set_field_value\n                        ):\n                            match set_field_name:\n                                case \"default\":\n                                    assert isinstance(\n                                        set_field_value, qlast.Expr\n                                    )  # noqa: E501\n                                    p_has_set_default = (\n                                        elab_expr_with_default_head(\n                                            set_field_value\n                                        )\n                                    )\n                                case _:\n                                    print_warning(\n                                        \"WARNING: not implemented \"\n                                        \"set_field_name\",\n                                        set_field_name,\n                                    )\n                        case _:\n                            print_warning(\n                                \"WARNING: not \" \"implemented pcmd\", pcmd\n                            )\n                final_target_type = construct_final_schema_target_tp(\n                    base_target_type, link_property_tps\n                )\n                if p_has_set_default is not None:\n                    assert not isinstance(\n                        final_target_type, e.UncheckedComputableTp\n                    )\n                    assert not isinstance(final_target_type, e.ComputableTp)\n                    final_target_type = e.DefaultTp(\n                        expr=p_has_set_default, tp=final_target_type\n                    )\n                object_tp_content = {\n                    **object_tp_content,\n                    pname: ResultTp(\n                        final_target_type,\n                        elab_schema_cardinality(\n                            is_required=p_is_required,\n                            cardinality=p_cardinality,\n                        ),\n                    ),\n                }\n            case qlast.CreateConcreteIndex(name=_, expr=index_expr):\n                index_expr_elab = elab(index_expr)\n\n                def elab_single_proj(proj_expr: e.Expr) -> str:\n                    match proj_expr:\n                        case e.ObjectProjExpr(\n                            subject=e.FreeVarExpr(subject_name), label=lbl\n                        ):\n                            if subject_name == DEFAULT_HEAD_NAME:\n                                return lbl\n                            else:\n                                raise ValueError(\n                                    \"Unsupported Index Expression\", proj_expr\n                                )\n                        case _:\n                            print_warning(\n                                \"WARNING: not implemented proj_expr\", proj_expr\n                            )\n                            raise ValueError(\"TODO\")\n\n                match index_expr_elab:\n                    case e.ObjectProjExpr(_, _):\n                        indexes.append([elab_single_proj(index_expr_elab)])\n                    case e.UnnamedTupleExpr(exprs):\n                        if all(\n                            isinstance(expr, e.ObjectProjExpr)\n                            for expr in exprs\n                        ):\n                            indexes.append(\n                                [elab_single_proj(expr) for expr in exprs]\n                            )\n                        else:\n                            print_warning(\n                                \"Unsupported Index Expression\", index_expr_elab\n                            )\n                            pass\n                    case _:\n                        print_warning(\n                            \"Unsupported Index Expression\", index_expr_elab\n                        )\n                        pass\n            case _:\n                print_warning(\"WARNING: not implemented cmd\", cmd)\n                # debug.dump(cmd)\n    return ObjectTp(val=object_tp_content), constrants, indexes\n\n\ndef add_bases_for_name(\n    schema: e.DBSchema,\n    current_module_name: tuple[str, ...],\n    current_type_name: str,\n    bases: list[qlast.TypeName],\n    add_object_type=False,\n) -> None:\n    base_tps = [elab_single_type_expr(base) for base in bases]\n    base_tps_ck: list[tuple[tuple[str, ...], e.RawName]] = []\n    this_type_name = e.QualifiedName([*current_module_name, current_type_name])\n    for base_tp in base_tps:\n        match base_tp:\n            case e.UncheckedTypeName(raw_name):\n                base_tps_ck.append((current_module_name, raw_name))\n            case _:\n                raise ValueError(\"TODO\", base_tp)\n    if add_object_type:\n        raise ValueError(\"TODO\")\n        # you cannot do this for std Object becuase the way id projection\n        # is treated is differnt in the interpreter,\n        # default id generation is not treated as properties in the interpter\n        # but rather a builtin concept\n    assert this_type_name not in schema.unchecked_subtyping_relations\n    schema.unchecked_subtyping_relations[this_type_name] = base_tps_ck\n\n\ndef elab_schema(existing: e.DBSchema, sdef: qlast.Schema) -> tuple[str, ...]:\n    if (\n        len(sdef.declarations) != 1\n        or sdef.declarations[0].name.name != \"default\"\n    ):\n        raise ValueError(\n            \"Expect single module declaration named default in schema\"\n        )\n    types_decls = cast(Sequence[qlast.ModuleDeclaration], sdef.declarations)[\n        0\n    ].declarations\n\n    current_module_name = (\"default\",)\n\n    type_defs: dict[str, e.ModuleEntityTypeDef | e.ModuleEntityFuncDef] = {}\n    existing.unchecked_modules[current_module_name] = e.DBModule(type_defs)\n    for t_decl in types_decls:\n        match t_decl:\n            case qlast.CreateObjectType(\n                bases=bases,\n                commands=commands,\n                name=qlast.ObjectRef(name=name),\n                abstract=abstract,\n            ):\n                obj_tp, constraints, indexes = elab_create_object_tp(commands)\n                add_bases_for_name(existing, (\"default\",), name, bases)\n                assert name not in type_defs\n                type_defs[name] = e.ModuleEntityTypeDef(\n                    obj_tp,\n                    is_abstract=abstract,\n                    constraints=constraints,\n                    indexes=indexes,\n                )\n            case qlast.CreateScalarType(\n                name=qlast.ObjectRef(name=name, module=None),\n                bases=bases,\n                abstract=abstract,\n            ):\n                this_name = e.QualifiedName([\"default\", name])\n                add_bases_for_name(existing, current_module_name, name, bases)\n                assert name not in type_defs\n                type_defs[name] = e.ModuleEntityTypeDef(\n                    e.ScalarTp(this_name),\n                    is_abstract=abstract,\n                    constraints=[],\n                    indexes=[],\n                )\n            case qlast.CreateConstraint():\n                print_warning(\"WARNING: not supported yet\", t_decl)\n            case qlast.CreateFunction(\n                name=qlast.ObjectRef(name=name, module=None),\n                params=params,\n                returning=returning,\n                returning_typemod=returning_typemod,\n                nativecode=nativecode,\n            ):\n                this_name = e.QualifiedName([\"default\", name])\n                assert isinstance(returning, qlast.TypeName), \"TODO\"\n                func_type = fun_elab.elaborate_fun_def_arg_type(\n                    params, returning, returning_typemod\n                )\n                assert nativecode is not None, \"TODO\"\n                func_body = elab(nativecode)\n                for label in reversed(func_type.args_label):\n                    func_body = eops.abstract_over_expr(func_body, label)\n                defaults = {\n                    p.name: elab(p.default) for p in params if p.default\n                }\n                this_def = e.DefinedFuncDef(\n                    tp=func_type, impl=func_body, defaults=defaults\n                )\n                if name in type_defs:\n                    current_def = type_defs[name]\n                    assert isinstance(current_def, e.ModuleEntityFuncDef)\n                    current_def.funcdefs.append(this_def)\n                else:\n                    type_defs[name] = e.ModuleEntityFuncDef([this_def])\n            case _:\n                raise ValueError(\"TODO\", t_decl)\n\n    return (\"default\",)\n\n\ndef add_module_from_sdl_defs(\n    schema: e.DBSchema,\n    module_defs: str,\n) -> e.DBSchema:\n    name = elab_schema(schema, parse_sdl(module_defs))\n    checked_schema = tck.check_module_validity(schema, name)\n    return checked_schema\n\n\ndef add_module_from_sdl_file(\n    schema: e.DBSchema,\n    init_sdl_file_path: str,\n) -> e.DBSchema:\n    with open(init_sdl_file_path) as f:\n        return add_module_from_sdl_defs(schema, f.read())\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/elaboration.py",
    "content": "from functools import singledispatch\nfrom typing import Any, Optional, Sequence, cast\n\nfrom edb import errors\n\n# import edb as edgedb\n# import edgedb\nfrom edb.common import debug\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes as qltypes\nfrom edb.schema import pointers as s_pointers\nfrom edb.schema.pointers import PointerDirection\nfrom . import interpreter_logging as i_logging\n\nfrom .data import data_ops as e\nfrom .data.data_ops import (\n    ArrExpr,\n    BackLinkExpr,\n    BindingExpr,\n    BoolVal,\n    BoundVarExpr,\n    DetachedExpr,\n    Expr,\n    FilterOrderExpr,\n    ForExpr,\n    FreeVarExpr,\n    FunAppExpr,\n    IndirectionIndexOp,\n    InsertExpr,\n    IntVal,\n    Label,\n    LinkPropLabel,\n    LinkPropProjExpr,\n    MultiSetExpr,\n    NamedTupleExpr,\n    ObjectProjExpr,\n    OffsetLimitExpr,\n    OptionalForExpr,\n    OrderAscending,\n    OrderDescending,\n    OrderLabelSep,\n    ShapedExprExpr,\n    ShapeExpr,\n    StrLabel,\n    SubqueryExpr,\n    Tp,\n    TpIntersectExpr,\n    TypeCastExpr,\n    UnionExpr,\n    UnionTp,\n    UnnamedTupleExpr,\n    UpdateExpr,\n    WithExpr,\n    next_name,\n)\nfrom .data.expr_ops import (\n    abstract_over_expr,\n    instantiate_expr,\n    is_path,\n    subst_expr_for_expr,\n)\nfrom .data import expr_ops as eops\n\nDEFAULT_HEAD_NAME = \"___nchsxx_\"\n# used as the name for the leading dot notation!\n# will always disappear when elaboration finishes\n\n\ndef elab_expr_with_default_head(node: qlast.Expr) -> BindingExpr:\n    return abstract_over_expr(elab(node), DEFAULT_HEAD_NAME)\n\n\ndef elab_error(msg: str, ctx: Optional[qlast.Span]) -> Any:\n    raise errors.QueryError(msg, span=ctx)\n\n\ndef elab_not_implemented(node: qlast.Base, msg: str = \"\") -> Any:\n    debug.dump(node)\n    debug.dump_edgeql(node)\n    raise ValueError(\"Not Implemented!\", msg, node)\n\n\ndef elab_Shape(elements: Sequence[qlast.ShapeElement]) -> ShapeExpr:\n    \"\"\"Convert a concrete syntax shape to object expressions\"\"\"\n    result: dict[e.Label, e.BindingExpr] = {}\n    for se in elements:\n        if path_contains_splat(se.expr):\n            i_logging.print_warning(\"Splat is not implemented\")\n            continue\n        match elab_ShapeElement(se):\n            case (name, elem):\n                if name not in result.keys():\n                    result = {**result, name: elem}\n                else:\n                    (elab_error(\"Duplicate Value in Shapes\", se.span))\n    return ShapeExpr(result)\n\n\n@singledispatch\ndef elab(node: qlast.Base) -> Expr:\n    return elab_not_implemented(node)\n\n\n@elab.register(qlast.QueryParameter)\ndef elab_QueryParameter(node: qlast.QueryParameter) -> None:\n    raise errors.QueryError(\"missing a type cast\", span=node.span)\n\n\n@elab.register(qlast.Introspect)\ndef elab_Introspect(node: qlast.Introspect) -> Expr:\n    i_logging.print_warning(\"Introspect is not implemented\")\n    return e.StrVal(\"Introspect is not implemented\")\n\n\n@elab.register(qlast.IsOp)\ndef elab_IsTp(oper: qlast.IsOp) -> e.IsTpExpr:\n    if oper.op != 'IS':\n        raise ValueError(\"Unknown Op Name for IsTp\", oper.op)\n    if isinstance(oper.right, qlast.TypeName):\n        right = elab_TypeName(oper.right)\n    else:\n        raise ValueError(\"Expecting a type name here\")\n    left = elab(oper.left)\n    return e.IsTpExpr(left, right)\n\n\n@elab.register(qlast.Path)\ndef elab_Path(p: qlast.Path) -> Expr:\n    result: Expr = None  # type: ignore[assignment]\n    if p.partial:\n        result = FreeVarExpr(DEFAULT_HEAD_NAME)\n    for step in p.steps:\n        match step:\n            case qlast.ObjectRef(name=name):\n                if result is None:\n                    if step.module:\n                        result = e.QualifiedName(\n                            [*step.module.split(\"::\"), name]\n                        )\n                    else:\n                        result = FreeVarExpr(var=name)\n                else:\n                    raise ValueError(\"Unexpected ObjectRef in Path\")\n            case qlast.Ptr(\n                name=path_name,\n                direction=PointerDirection.Outbound,\n                type=ptr_type,\n            ):\n                if result is None:\n                    raise ValueError(\"should not be\")\n                else:\n                    if ptr_type == 'property':\n                        result = LinkPropProjExpr(result, path_name)\n                    else:\n                        result = ObjectProjExpr(result, path_name)\n            case qlast.Ptr(\n                name=path_name, direction=PointerDirection.Inbound, type=None\n            ):\n                if result is None:\n                    raise ValueError(\"should not be\")\n                else:\n                    result = BackLinkExpr(result, path_name)\n            case qlast.TypeIntersection(type=tp):\n                if result is None:\n                    raise ValueError(\"should not be\")\n                else:\n                    match elab_single_type_expr(tp):\n                        case e.UncheckedTypeName(name=tp_name):\n                            result = TpIntersectExpr(result, tp_name)\n                        case _:\n                            raise ValueError(\"expecting single type name here\")\n            case _:\n                if result is None:\n                    result = elab(step)\n                else:\n                    elab_not_implemented(step, \"in path\")\n    return result\n\n\ndef path_contains_splat(p: qlast.Path) -> bool:\n    for step in p.steps:\n        if isinstance(step, qlast.Splat):\n            return True\n    return False\n\n\ndef elab_label(p: qlast.Path) -> Label:\n    \"\"\"Elaborates a single name e.g. in the left hand side of a shape\"\"\"\n    steps = [*p.steps]\n    while steps[0] is not None and isinstance(\n        steps[0], qlast.TypeIntersection\n    ):\n        steps = steps[1:]\n    match steps[0]:\n        case qlast.Ptr(\n            name=pname, direction=s_pointers.PointerDirection.Outbound\n        ):\n            return StrLabel(pname)\n        case qlast.Ptr(name=pname, type='property'):\n            return LinkPropLabel(pname)\n        case _:\n            return elab_not_implemented(p, \"label\")\n\n\n@elab.register(qlast.ShapeElement)\ndef elab_ShapeElement(s: qlast.ShapeElement) -> tuple[Label, BindingExpr]:\n    def default_post_processing(x):\n        return x\n\n    post_processing = default_post_processing\n\n    if s.orderby or s.where:\n\n        def process(e: BindingExpr) -> BindingExpr:\n            return abstract_over_expr(\n                FilterOrderExpr(\n                    subject=instantiate_expr(\n                        FreeVarExpr(DEFAULT_HEAD_NAME), e\n                    ),\n                    filter=elab_where(s.where),\n                    order=elab_orderby(s.orderby),\n                ),\n                DEFAULT_HEAD_NAME,\n            )\n\n        post_processing = process\n\n    if s.compexpr is not None:\n        # l := E -> l := ¶.e if E -> e\n        if s.operation.op != qlast.ShapeOp.ASSIGN:\n            return elab_not_implemented(s)\n        else:\n            name = elab_label(s.expr)\n            val = abstract_over_expr(elab(s.compexpr), DEFAULT_HEAD_NAME)\n            return (name, post_processing(val))\n    elif s.elements:\n        # l : S -> l := x. (x ⋅ l) s if S -> s\n        name = elab_label(s.expr)\n        match name:\n            case StrLabel(_):\n                var = next_name()\n                return (\n                    name,\n                    post_processing(\n                        BindingExpr(\n                            var=var,\n                            body=ShapedExprExpr(\n                                ObjectProjExpr(BoundVarExpr(var), name.label),\n                                elab_Shape(s.elements),\n                            ),\n                        )\n                    ),\n                )\n            case _:\n                return elab_not_implemented(s, \"link property with shapes\")\n    else:\n        # l -> l := x. (x ⋅ l)\n        name = elab_label(s.expr)\n        match name:\n            case StrLabel(_):\n                var = next_name()\n                return (\n                    name,\n                    post_processing(\n                        BindingExpr(\n                            var=var,\n                            body=ObjectProjExpr(BoundVarExpr(var), name.label),\n                        )\n                    ),\n                )\n            case LinkPropLabel(_):\n                var = next_name()\n                return (\n                    name,\n                    post_processing(\n                        BindingExpr(\n                            var=var,\n                            body=LinkPropProjExpr(\n                                BoundVarExpr(var), name.label\n                            ),\n                        )\n                    ),\n                )\n            case _:\n                return elab_not_implemented(s)\n\n\n@elab.register(qlast.Shape)\ndef elab_ShapedExpr(shape: qlast.Shape) -> ShapedExprExpr:\n    if shape.expr is None:\n        return ShapedExprExpr(\n            expr=e.FreeObjectExpr(), shape=elab_Shape(shape.elements)\n        )\n    else:\n        return ShapedExprExpr(\n            expr=elab(shape.expr), shape=elab_Shape(shape.elements)\n        )\n\n\n@elab.register(qlast.InsertQuery)\ndef elab_InsertQuery(expr: qlast.InsertQuery) -> InsertExpr:\n    subject_type = expr.subject.name\n    object_shape = elab_Shape(expr.shape)\n    unshaped = {}\n    for k, v in object_shape.shape.items():\n        if not isinstance(k, StrLabel):\n            raise ValueError(\"Expecting Plain Labels\")\n        assert eops.binding_is_unnamed(\n            v\n        ), \"Not expecting leading dot notaiton in Shapes\"\n        unshaped[k.label] = v.body\n\n    return cast(\n        InsertExpr,\n        elab_aliases(\n            expr.aliases,\n            InsertExpr(name=e.UnqualifiedName(subject_type), new=unshaped),\n        ),\n    )  # TODO: we should allow qualified names here\n\n\n@elab.register(qlast.Constant)\ndef elab_Constant(expr: qlast.Constant) -> e.ScalarVal:\n    match expr.kind:\n        case qlast.ConstantKind.STRING:\n            return e.StrVal(val=expr.value)\n        case qlast.ConstantKind.INTEGER:\n            return e.IntVal(val=int(expr.value))\n        case qlast.ConstantKind.FLOAT:\n            return e.ScalarVal(\n                tp=e.ScalarTp(e.QualifiedName([\"std\", \"float64\"])),\n                val=float(expr.value),\n            )\n        case qlast.ConstantKind.BOOLEAN:\n            match expr.value:\n                case \"True\" | \"true\":\n                    return BoolVal(val=True)\n                case \"False\" | \"false\":\n                    return BoolVal(val=False)\n                case _:\n                    raise ValueError(\"Unknown Bool Value\", expr)\n        case _:\n            raise ValueError(\"Unknown Constant Kind\", expr.kind)\n\n\ndef elab_where(where: Optional[qlast.Expr]) -> BindingExpr:\n    if where is None:\n        return abstract_over_expr(BoolVal(True))\n    else:\n        return abstract_over_expr(elab(where), DEFAULT_HEAD_NAME)\n\n\ndef elab_orderby(\n    qle: Optional[Sequence[qlast.SortExpr]],\n) -> dict[str, BindingExpr]:\n    if qle is None:\n        return {}\n    result: dict[str, Expr] = {}\n    for idx, sort_expr in enumerate(qle):\n\n        empty_label = (\n            e.OrderEmptyFirst\n            if sort_expr.nones_order == qlast.NonesOrder.First\n            or sort_expr.nones_order is None\n            else (\n                e.OrderEmptyLast\n                if sort_expr.nones_order == qlast.NonesOrder.Last\n                else elab_error(\"unknown nones order\", sort_expr.span)\n            )\n        )\n\n        direction_label = (\n            OrderAscending\n            if sort_expr.direction == qlast.SortOrder.Asc\n            else (\n                OrderDescending\n                if sort_expr.direction == qlast.SortOrder.Desc\n                else elab_error(\"unknown direction\", sort_expr.span)\n            )\n        )\n\n        key = (\n            str(idx)\n            + OrderLabelSep\n            + direction_label\n            + OrderLabelSep\n            + empty_label\n        )\n        elabed_expr = elab(sort_expr.path)\n        result = {**result, key: elabed_expr}\n\n    return {\n        l: abstract_over_expr(v, DEFAULT_HEAD_NAME)\n        for (l, v) in result.items()\n    }\n\n\n@elab.register(qlast.SelectQuery)\ndef elab_SelectExpr(qle: qlast.SelectQuery) -> Expr:\n    if qle.offset is not None or qle.limit is not None:\n        return elab_aliases(\n            qle.aliases,\n            SubqueryExpr(\n                OffsetLimitExpr(\n                    subject=elab(qle.result),\n                    offset=(\n                        elab(qle.offset)\n                        if qle.offset is not None\n                        else IntVal(0)\n                    ),\n                    limit=(\n                        elab(qle.limit)\n                        if qle.limit is not None\n                        else e.MultiSetExpr([])\n                    ),\n                )\n            ),\n        )\n    else:\n        subject_elab = elab(qle.result)\n        filter_elab = elab_where(qle.where)\n        order_elab = elab_orderby(qle.orderby)\n        if qle.result_alias is not None:\n            # apply and reabstract the result alias\n            subject_elab = SubqueryExpr(expr=subject_elab)\n            alias_var = FreeVarExpr(qle.result_alias)\n            filter_elab = abstract_over_expr(\n                instantiate_expr(alias_var, filter_elab), qle.result_alias\n            )\n            order_elab = {\n                l: abstract_over_expr(\n                    instantiate_expr(alias_var, o), qle.result_alias\n                )\n                for (l, o) in order_elab.items()\n            }\n        else:\n\n            def path_abstraction(subject: Expr) -> None:\n                nonlocal filter_elab, order_elab\n                match subject:\n                    case ShapedExprExpr(expr=e, shape=_):\n                        return path_abstraction(e)\n                    case _:\n                        if is_path(subject):\n                            name = next_name()\n                            filter_elab = abstract_over_expr(\n                                subst_expr_for_expr(\n                                    FreeVarExpr(name),\n                                    subject,\n                                    instantiate_expr(\n                                        FreeVarExpr(name), filter_elab\n                                    ),\n                                ),\n                                name,\n                            )\n                            order_elab = {\n                                l: abstract_over_expr(\n                                    subst_expr_for_expr(\n                                        FreeVarExpr(name),\n                                        subject,\n                                        instantiate_expr(FreeVarExpr(name), o),\n                                    ),\n                                    name,\n                                )\n                                for (l, o) in order_elab.items()\n                            }\n                        return\n\n            path_abstraction(subject_elab)\n\n        without_alias = SubqueryExpr(\n            FilterOrderExpr(\n                subject=subject_elab, filter=filter_elab, order=order_elab\n            )\n        )\n        return elab_aliases(qle.aliases, without_alias)\n\n\n@elab.register(qlast.FunctionCall)\ndef elab_FunctionCall(fcall: qlast.FunctionCall) -> FunAppExpr:\n    if fcall.window:\n        return elab_not_implemented(fcall)\n    fname: e.RawName\n    if isinstance(fcall.func, str):\n        fname = e.UnqualifiedName(fcall.func)\n    else:\n        assert isinstance(fcall.func, tuple)\n        fname = e.QualifiedName(list(fcall.func))\n    args = [elab(arg) for arg in fcall.args]\n    kwargs = {k: elab(v) for (k, v) in fcall.kwargs.items()}\n    return FunAppExpr(fname, None, args, kwargs)\n\n\n@elab.register\ndef elab_UnaryOp(uop: qlast.UnaryOp) -> FunAppExpr:\n    return FunAppExpr(\n        fun=e.UnqualifiedName(uop.op),\n        args=[elab(uop.operand)],\n        overloading_index=None,\n        kwargs={},\n    )\n\n\n@elab.register(qlast.BinOp)\ndef elab_BinOp(binop: qlast.BinOp) -> FunAppExpr | UnionExpr:\n    if binop.rebalanced:\n        return elab_not_implemented(binop)\n    left_expr = elab(binop.left)\n    right_expr = elab(binop.right)\n    if binop.op == \"UNION\":\n        return UnionExpr(left_expr, right_expr)\n    else:\n        return FunAppExpr(\n            fun=e.UnqualifiedName(binop.op),\n            args=[left_expr, right_expr],\n            overloading_index=None,\n            kwargs={},\n        )\n\n\ndef elab_param_modifier(mod: qltypes.TypeModifier) -> e.ParamModifier:\n    match mod:\n        case qltypes.TypeModifier.OptionalType:\n            return e.ParamOptional()\n        case qltypes.TypeModifier.SingletonType:\n            return e.ParamSingleton()\n        case qltypes.TypeModifier.SetOfType:\n            return e.ParamSetOf()\n        case _:\n            raise ValueError(\"Unknown Param Modifier\", mod)\n\n\ndef elab_single_type_str(name: str, module_name: Optional[str]) -> Tp:\n    if name.startswith(\"any\") and module_name is None:\n        return e.AnyTp(name[3:])\n    else:\n        if module_name:\n            return e.UncheckedTypeName(e.QualifiedName([module_name, name]))\n        else:\n            return e.UncheckedTypeName(e.UnqualifiedName(name))\n\n\ndef elab_CompositeTp(\n    basetp: qlast.ObjectRef,\n    sub_tps: list[Tp],\n    labels: Optional[list[str]] = None,\n) -> Tp:\n    if labels is None:\n        labels = []\n\n    if basetp.name in {k.value for k in e.CompositeTpKind}:\n        return e.CompositeTp(\n            kind=e.CompositeTpKind(basetp.name), tps=sub_tps, labels=labels\n        )\n    else:\n        raise ValueError(\"Unknown Composite Type\", basetp.name)\n\n\n@elab.register(qlast.TypeName)\ndef elab_TypeName(qle: qlast.TypeName) -> Tp:\n    # if qle.name:\n    #     return elab_not_implemented(qle)\n    if qle.dimensions:\n        return elab_not_implemented(qle)\n\n    basetp = qle.maintype\n    if isinstance(basetp, qlast.ObjectRef):\n        if basetp.itemclass:\n            return elab_not_implemented(qle)\n        if qle.subtypes:\n            if (\n                all(tp_name.name for tp_name in qle.subtypes)\n                and basetp.name == \"tuple\"\n            ):\n                sub_tps = [\n                    elab_single_type_expr(subtype) for subtype in qle.subtypes\n                ]\n                labels = [tp_name.name for tp_name in qle.subtypes]\n                return elab_CompositeTp(\n                    basetp, sub_tps, labels  # type: ignore\n                )\n            else:\n                sub_tps = [\n                    elab_single_type_expr(subtype) for subtype in qle.subtypes\n                ]\n                return elab_CompositeTp(basetp, sub_tps)\n        return elab_single_type_str(basetp.name, basetp.module)\n    elif isinstance(basetp, qlast.PseudoObjectRef):\n        if basetp.name.startswith(\"any\"):\n            return e.AnyTp(basetp.name[3:])\n        else:\n            return elab_not_implemented(qle)\n    else:\n        raise ValueError(\"Unknown Type Name\", qle)\n\n    # raise ValueError(\"Unrecognized conversion type\", basetp.name)\n    # return elab_not_implemented(basetp, \"unrecognized type \" + basetp.name)\n\n\ndef elab_single_type_expr(typedef: qlast.TypeExpr) -> Tp:\n    \"\"\"elaborates the target type of a\n    concrete unknown pointer, i.e. links or properties\"\"\"\n    if isinstance(typedef, qlast.TypeName):\n        return elab_TypeName(typedef)\n    else:\n        match typedef:\n            case qlast.TypeOp(left=left_type, op=op_name, right=right_type):\n                if op_name == \"|\":\n                    return UnionTp(\n                        left=elab_single_type_expr(left_type),\n                        right=elab_single_type_expr(right_type),\n                    )\n                else:\n                    raise ValueError(\"Unknown Type Op\")\n        raise ValueError(\"MATCH\")\n\n\n@elab.register(qlast.TypeCast)\ndef elab_TypeCast(qle: qlast.TypeCast) -> TypeCastExpr | e.ParameterExpr:\n    if isinstance(qle.expr, qlast.QueryParameter):\n        if qle.cardinality_mod == qlast.CardinalityModifier.Optional:\n            is_required = False\n        else:\n            is_required = True\n        return e.ParameterExpr(\n            name=qle.expr.name,\n            tp=elab_single_type_expr(qle.type),\n            is_required=is_required,\n        )\n    if isinstance(qle.type, qlast.TypeName):\n        tp = elab_TypeName(qle.type)\n        expr = elab(qle.expr)\n        return TypeCastExpr(tp=tp, arg=expr)\n    else:\n        return elab_not_implemented(qle)\n\n\n@elab.register(qlast.Array)\ndef elab_Array(qle: qlast.Array) -> ArrExpr:\n    return ArrExpr(elems=[elab(elem) for elem in qle.elements])\n\n\n@elab.register(qlast.UpdateQuery)\ndef elab_UpdateQuery(qle: qlast.UpdateQuery):\n    subject = FilterOrderExpr(\n        subject=elab(qle.subject),\n        filter=(\n            abstract_over_expr(elab(qle.where), DEFAULT_HEAD_NAME)\n            if qle.where\n            else abstract_over_expr(BoolVal(True))\n        ),\n        order={},\n    )\n    shape = elab_Shape(qle.shape)\n    return elab_aliases(qle.aliases, UpdateExpr(subject=subject, shape=shape))\n\n\n@elab.register(qlast.DeleteQuery)\ndef elab_DeleteQuery(qle: qlast.DeleteQuery):\n    subject = FilterOrderExpr(\n        subject=elab(qle.subject),\n        filter=(\n            abstract_over_expr(elab(qle.where), DEFAULT_HEAD_NAME)\n            if qle.where\n            else abstract_over_expr(BoolVal(True))\n        ),\n        order={},\n    )\n    return elab_aliases(qle.aliases, e.DeleteExpr(subject=subject))\n\n\n@elab.register(qlast.Set)\ndef elab_Set(qle: qlast.Set):\n    return MultiSetExpr(expr=[elab(e) for e in qle.elements])\n\n\ndef elab_aliases(\n    aliases: Optional[Sequence[qlast.Alias]],\n    tail_expr: Expr,\n) -> Expr:\n    if aliases is None:\n        return tail_expr\n    result = tail_expr\n    for i in reversed(range(len(aliases))):\n        cur_alias = aliases[i]\n        if isinstance(cur_alias, qlast.AliasedExpr):\n            result = WithExpr(\n                elab(cur_alias.expr),\n                abstract_over_expr(result, cur_alias.alias),\n            )\n        else:\n            raise ValueError(\"Module Aliases\")\n    return result\n\n\n@elab.register(qlast.DetachedExpr)\ndef elab_DetachedExpr(qle: qlast.DetachedExpr):\n    if qle.preserve_path_prefix:\n        return elab_not_implemented(qle)\n    return DetachedExpr(expr=elab(qle.expr))\n\n\n@elab.register(qlast.NamedTuple)\ndef elab_NamedTuple(qle: qlast.NamedTuple) -> NamedTupleExpr:\n    # raise ValueError(\"TODO : FIX MYPY below\")\n    result: dict[str, Expr] = {}\n\n    for element in qle.elements:\n        if element.name.name in result.keys():\n            raise elab_error(\"Duplicate Value in Named Tuple\", qle.span)\n        result[element.name.name] = elab(element.val)\n\n    return NamedTupleExpr(val=result)\n\n\n@elab.register(qlast.Tuple)\ndef elab_UnnamedTuple(qle: qlast.Tuple) -> UnnamedTupleExpr:\n    return UnnamedTupleExpr(val=[elab(e) for e in qle.elements])\n\n\n@elab.register(qlast.ForQuery)\ndef elab_ForQuery(qle: qlast.ForQuery) -> ForExpr | OptionalForExpr:\n    if qle.result_alias:\n        raise elab_not_implemented(qle)\n    return cast(\n        (ForExpr | OptionalForExpr),\n        elab_aliases(\n            qle.aliases,\n            cast(\n                Expr,\n                (OptionalForExpr if qle.optional else ForExpr)(\n                    bound=elab(qle.iterator),\n                    next=abstract_over_expr(\n                        elab(qle.result), qle.iterator_alias\n                    ),\n                ),\n            ),\n        ),\n    )\n\n\n@elab.register\ndef elab_Indirection(qle: qlast.Indirection) -> FunAppExpr:\n    subject = elab(qle.arg)\n    match qle.indirection:\n        case [qlast.Slice(start=None, stop=None)]:\n            raise ValueError(\"Slice cannot be both empty\")\n        case [qlast.Slice(start=None, stop=stop)]:\n            assert stop is not None  # required for mypy\n            return FunAppExpr(\n                fun=e.UnqualifiedName(e.IndirectionSliceStopOp),\n                args=[subject, elab(stop)],\n                overloading_index=None,\n                kwargs={},\n            )\n        case [qlast.Slice(start=start, stop=None)]:\n            assert start is not None  # required for mypy\n            return FunAppExpr(\n                fun=e.UnqualifiedName(e.IndirectionSliceStartOp),\n                args=[subject, elab(start)],\n                overloading_index=None,\n                kwargs={},\n            )\n        case [qlast.Slice(start=start, stop=stop)]:\n            assert start is not None  # required for mypy\n            assert stop is not None  # required for mypy\n            return FunAppExpr(\n                fun=e.UnqualifiedName(e.IndirectionSliceStartStopOp),\n                args=[subject, elab(start), elab(stop)],\n                overloading_index=None,\n                kwargs={},\n            )\n        case [qlast.Index(index=idx)]:\n            return FunAppExpr(\n                fun=e.UnqualifiedName(IndirectionIndexOp),\n                args=[subject, elab(idx)],\n                overloading_index=None,\n                kwargs={},\n            )\n    raise ValueError(\"Not yet implemented indirection\", qle)\n\n\n@elab.register\ndef elab_IfElse(qle: qlast.IfElse) -> e.IfElseExpr:\n    return e.IfElseExpr(\n        then_branch=elab(qle.if_expr),\n        condition=elab(qle.condition),\n        else_branch=elab(qle.else_expr),\n    )\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/errors.py",
    "content": "class ElaborationError(Exception):\n    pass\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/evaluation.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any, Callable, Optional, Sequence, cast\n\nimport itertools\n\nfrom .data.data_ops import (\n    ArrExpr,\n    ArrVal,\n    BackLinkExpr,\n    BoolVal,\n    DetachedExpr,\n    Expr,\n    FilterOrderExpr,\n    ForExpr,\n    FreeVarExpr,\n    FunAppExpr,\n    InsertExpr,\n    Invisible,\n    Label,\n    LinkPropLabel,\n    LinkPropProjExpr,\n    Marker,\n    MultiSetExpr,\n    MultiSetVal,\n    NamedTupleExpr,\n    NamedTupleVal,\n    ObjectProjExpr,\n    ObjectVal,\n    OffsetLimitExpr,\n    OptionalForExpr,\n    OrderAscending,\n    OrderDescending,\n    OrderLabelSep,\n    ParamOptional,\n    ParamSetOf,\n    ParamSingleton,\n    RefVal,\n    ShapedExprExpr,\n    ShapeExpr,\n    StrLabel,\n    StrVal,\n    SubqueryExpr,\n    TpIntersectExpr,\n    UnionExpr,\n    UnnamedTupleExpr,\n    UnnamedTupleVal,\n    UpdateExpr,\n    Val,\n    Visible,\n    WithExpr,\n    next_id,\n)\nfrom .data import data_ops as e\nfrom .data import expr_ops as eops\nfrom .data import type_ops as tops\nfrom .data import module_ops as mops\nfrom .data.expr_ops import (\n    instantiate_expr,\n    map_expand_multiset_val,\n    val_is_ref_val,\n)\nfrom .data.type_ops import is_nominal_subtype_in_schema\nfrom .db_interface import EdgeDatabase\nfrom .evaluation_tools.storage_coercion import coerce_to_storage\nfrom .interpreter_logging import print_warning\n\n\ndef get_param_reserved_name(param: str | int) -> str:\n    return f\"__edgedb_reserved_param_name_{param}_\"\n\n\ndef eval_error(expr: Val | Expr | Sequence[Val], msg: str = \"\") -> Any:\n    raise ValueError(\"Eval Error\", msg, expr)\n\n\ndef eval_order_by(\n    after_condition: Sequence[Val], orders: Sequence[dict[str, Val]]\n) -> Sequence[Val]:\n    if len(after_condition) == 0:\n        return after_condition\n    if len(orders) == 0:\n        return after_condition\n\n    keys = [k for k in orders[0].keys()]\n    if len(keys) == 0:\n        return after_condition\n    sort_specs = sorted(\n        [\n            (int(idx), spec, empty_order)\n            for k in keys\n            for [idx, spec, empty_order] in [k.split(OrderLabelSep)]\n        ]\n    )\n\n    result: Sequence[tuple[int, Val]] = list(enumerate(after_condition))\n    # use reversed to achieve the desired effect\n    for idx, spec, empty_order in reversed(sort_specs):\n\n        def key_extract(\n            elem: tuple[int, Val], idx=idx, spec=spec, empty_order=empty_order\n        ):\n            order_elem = orders[elem[0]][\n                (str(idx) + OrderLabelSep + spec + OrderLabelSep + empty_order)\n            ]\n            if empty_order == e.OrderEmptyLast:\n                assert isinstance(order_elem, MultiSetVal)\n                return (len(order_elem.getVals()) == 0, order_elem)\n            else:\n                return order_elem\n\n        result = sorted(\n            result,\n            key=key_extract,\n            reverse=(\n                False\n                if spec == OrderAscending\n                else (\n                    True\n                    if spec == OrderDescending\n                    else eval_error(\n                        cast(Sequence[Val], orders), \"unknown spec\"\n                    )\n                )\n            ),\n        )\n\n    return [elem for (_, elem) in result]\n\n\nEvalEnv = dict[str, MultiSetVal]\n\n\ndef ctx_extend(\n    ctx: EvalEnv, bnd: e.BindingExpr, val: MultiSetVal\n) -> tuple[EvalEnv, Expr]:\n    assert isinstance(val, MultiSetVal), \"Expecting MultiSetVal\"\n    bnd_no_capture = eops.ensure_no_capture(list(ctx.keys()), bnd)\n    return {**ctx, bnd_no_capture.var: val}, instantiate_expr(\n        e.FreeVarExpr(bnd_no_capture.var), bnd_no_capture\n    )\n\n\ndef apply_shape(\n    ctx: EvalEnv, db: EdgeDatabase, shape: ShapeExpr, value: Val\n) -> Val:\n    def apply_shape_to_prodval(\n        shape: ShapeExpr, objectval: ObjectVal\n    ) -> ObjectVal:\n        result: dict[Label, tuple[Marker, MultiSetVal]] = {}\n        for key, (_, pval) in objectval.val.items():\n            if key not in shape.shape.keys():\n                result = {**result, key: (Invisible(), (pval))}\n            else:\n                pass\n        for key, shape_elem in shape.shape.items():\n            new_ctx, shape_body = ctx_extend(\n                ctx, shape_elem, e.ResultMultiSetVal([value])\n            )\n            new_val: MultiSetVal = eval_expr(new_ctx, db, shape_body)\n            result = {**result, key: (Visible(), (new_val))}\n\n        return ObjectVal(result)\n\n    match value:\n        case RefVal(refid=id, tpname=tpname, val=dictval):\n            return RefVal(\n                refid=id,\n                tpname=tpname,\n                val=apply_shape_to_prodval(shape, dictval),\n            )\n        case _:\n            return eval_error(value, \"Cannot apply shape to value\")\n\n\ndef eval_expr_list(\n    ctx: EvalEnv, db: EdgeDatabase, exprs: Sequence[Expr]\n) -> Sequence[MultiSetVal]:\n    result: Sequence[MultiSetVal] = []\n    for expr in exprs:\n        val = eval_expr(ctx, db, expr)\n        result = [*result, val]\n    return result\n\n\n# not sure why the semantics says to produce empty set when label not present\n\n\ndef singular_proj(\n    ctx: EvalEnv, db: EdgeDatabase, subject: Val, label: Label\n) -> MultiSetVal:\n    match subject:\n        case RefVal(refid=id, tpname=tpname, val=objVal):\n            if label in objVal.val.keys():\n                return objVal.val[label][1]\n            elif isinstance(label, StrLabel):\n                label_str = label.label\n                if label_str == \"id\":\n                    return e.ResultMultiSetVal([e.UuidVal(id)])\n                elif label_str == \"__type__\":\n                    print_warning(\n                        \"Introspection is not properly supported yet\"\n                    )\n                    return e.ResultMultiSetVal(\n                        [\n                            e.RefVal(\n                                next_id(),\n                                e.QualifiedName([\"schema\", \"ObjectType\"]),\n                                e.ObjectVal(\n                                    {\n                                        e.StrLabel(\"name\"): (\n                                            e.Visible(),\n                                            e.MultiSetVal(\n                                                [\n                                                    e.StrVal(\n                                                        \"::\".join(tpname.names)\n                                                    )\n                                                ]\n                                            ),\n                                        ),\n                                    }\n                                ),\n                            )\n                        ]\n                    )\n                else:\n                    return db.project(id, tpname, label_str)\n            else:\n                raise ValueError(\"Label not found\", label)\n        case NamedTupleVal(val=dic):\n            match label:\n                case StrLabel(l):\n                    if l in dic.keys():\n                        return e.ResultMultiSetVal([dic[l]])\n                    else:\n                        if l.isdigit() and int(l) < len(dic.keys()):\n                            return e.ResultMultiSetVal(\n                                [dic[list(dic.keys())[int(l)]]]\n                            )\n                        else:\n                            raise ValueError(\"key DNE\")\n            raise ValueError(\"Label not Str\")\n        case UnnamedTupleVal(val=arr):\n            match label:\n                case StrLabel(l):\n                    if l.isdigit() and int(l) < len(arr):\n                        return e.ResultMultiSetVal([arr[int(l)]])\n                    else:\n                        raise ValueError(\"key DNE\")\n            raise ValueError(\"Label not Str\")\n    raise ValueError(\"Cannot project, unknown subject\", subject)\n\n\ndef offset_vals(val: Sequence[Val], offset: Val):\n    match offset:\n        case e.ScalarVal(_, v):\n            if v < 0:\n                raise ValueError(\"OFFSET must not be negative\")\n            return val[v:]\n        case _:\n            raise ValueError(\"offset must be an int\")\n\n\ndef limit_vals(val: Sequence[Val], limit: Val) -> Sequence[Val]:\n    match limit:\n        case e.ScalarVal(_, v):\n            if v < 0:\n                raise ValueError(\"LIMIT must not be negative\")\n            return val[:v]\n        case _:\n            raise ValueError(\"offset must be an int\")\n\n\ndef make_invisible(val: MultiSetVal) -> MultiSetVal:\n    result: list[Val] = []\n    for v in val.getVals():\n        match v:\n            case RefVal(refid=id, tpname=tpname, val=dictval):\n                result = [\n                    *result,\n                    RefVal(\n                        refid=id,\n                        tpname=tpname,\n                        val=ObjectVal(\n                            {\n                                k: (Invisible(), v)\n                                for k, (_, v) in dictval.val.items()\n                            }\n                        ),\n                    ),\n                ]\n            case _:\n                result = [*result, v]\n    return e.ResultMultiSetVal(result)\n\n\nclass EvaluationLogsWrapper:\n    def __init__(self):\n        self.original_eval_expr = None\n        self.reset_logs(None)\n\n    def reset_logs(self, logs: Optional[list[Any]]):\n        self.logs = logs\n        self.indexes: list[int] = []\n\n    def __call__(\n        self, eval_expr: Callable[[EvalEnv, EdgeDatabase, Expr], MultiSetVal]\n    ):\n        self.original_eval_expr = eval_expr\n\n        def wrapper(ctx: EvalEnv, db: EdgeDatabase, expr: Expr) -> MultiSetVal:\n            if self.logs is None:\n                return self.original_eval_expr(ctx, db, expr)\n            else:\n                parent = self.logs\n                [parent := parent[i] for i in self.indexes]\n                self.indexes.append(len(parent))\n                parent.append(\n                    [(expr, e.ResultMultiSetVal([StrVal(\"NOT AVAILABLE!!!\")]))]\n                )\n                rt_val = self.original_eval_expr(ctx, db, expr)\n                parent[self.indexes[-1]][0] = (\n                    parent[self.indexes[-1]][0][0],\n                    rt_val,\n                )\n                assert len(parent[self.indexes[-1]][0]) == 2\n                self.indexes.pop()\n                if not isinstance(rt_val, e.MultiSetVal):\n                    raise ValueError(\n                        \"Evaluation should always return MultiSetVal\"\n                    )\n                return rt_val\n\n        return wrapper\n\n\neval_logs_wrapper = EvaluationLogsWrapper()\n\n\ndef do_conditional_dedup(val: MultiSetVal) -> MultiSetVal:\n    if all(val_is_ref_val(v) for v in val.getVals()):\n        return e.ResultMultiSetVal(eops.object_dedup(val.getVals()))\n    return val\n\n\n# the database is a mutable reference that keeps track of a read snapshot inside\n@eval_logs_wrapper\ndef eval_expr(ctx: EvalEnv, db: EdgeDatabase, expr: Expr) -> MultiSetVal:\n    match expr:\n        case e.ScalarVal(_):\n            return e.ResultMultiSetVal([expr])\n        case e.FreeObjectExpr():\n            return e.ResultMultiSetVal(\n                [\n                    e.RefVal(\n                        next_id(),\n                        tpname=e.QualifiedName([\"std\", \"FreeObject\"]),\n                        val=e.ObjectVal(val={}),\n                    )\n                ]\n            )\n        case e.ConditionalDedupExpr(expr=inner):\n            inner_val = eval_expr(ctx, db, inner)\n            return do_conditional_dedup(inner_val)\n        case InsertExpr(tname, arg):\n            assert isinstance(\n                tname, e.QualifiedName\n            ), \"Should be updated during tcking\"\n            id = db.insert(tname, {})\n            argv = {k: eval_expr(ctx, db, v) for (k, v) in arg.items()}\n            arg_object = ObjectVal(\n                {StrLabel(k): (e.Visible(), v) for (k, v) in argv.items()}\n            )\n            type_def = mops.resolve_type_name(db.storage.get_schema(), tname)\n            if isinstance(type_def, e.ObjectTp):\n                new_object = coerce_to_storage(\n                    arg_object, tops.get_storage_tp(type_def)\n                )\n                db.update(id, tname, {k: v for k, v in new_object.items()})\n                return e.ResultMultiSetVal(\n                    [\n                        RefVal(\n                            id,\n                            tname,\n                            ObjectVal(\n                                {\n                                    k: (e.Invisible(), v)\n                                    for k, (_, v) in arg_object.val.items()\n                                }\n                            ),\n                        )\n                    ]\n                )\n            else:\n                raise ValueError(\"Cannot insert into scalar types\")\n            # inserts return empty dict\n        case FilterOrderExpr(subject=subject, filter=filter, order=order):\n            selected = eval_expr(ctx, db, subject)\n            # assume data unchaged throught the evaluation of conditions\n            conditions: Sequence[MultiSetVal] = [\n                eval_expr(new_ctx, db, filter_body)\n                for select_i in selected.getRawVals()\n                for new_ctx, filter_body in [\n                    ctx_extend(ctx, filter, e.ResultMultiSetVal([select_i]))\n                ]\n            ]\n            after_condition: Sequence[Val] = [\n                select_i\n                for (select_i, condition) in zip(\n                    selected.getRawVals(), conditions\n                )\n                if BoolVal(True) in condition.getVals()\n            ]\n            orders: Sequence[dict[str, Val]] = []\n            for after_condition_i in after_condition:\n                current: dict[str, Val] = {}\n                for l, o in order.items():\n                    new_ctx, o_body = ctx_extend(\n                        ctx, o, e.ResultMultiSetVal([after_condition_i])\n                    )\n                    current = {**current, l: eval_expr(new_ctx, db, o_body)}\n                orders = [*orders, current]\n            after_order = eval_order_by(after_condition, orders)\n            if isinstance(selected, e.ResultMultiSetVal):\n                return e.ResultMultiSetVal(after_order)\n            else:\n                raise ValueError(\"Not Implemented\", selected)\n        case ShapedExprExpr(expr=subject, shape=shape):\n            subjectv = eval_expr(ctx, db, subject)\n            after_shape: Sequence[Val] = [\n                apply_shape(ctx, db, shape, v) for v in subjectv.getVals()\n            ]\n            return e.ResultMultiSetVal(after_shape)\n        case FreeVarExpr(var=name):\n            if name in ctx.keys():\n                # binder needs to be invisible when selected\n                return ctx[name]\n            else:\n                raise ValueError(\"Variable not found\", name)\n        case e.QualifiedName(names=names):\n\n            all_ids: Sequence[Val] = [\n                RefVal(id, e.QualifiedName(names=names), ObjectVal({}))\n                for id in db.storage.query_ids_for_a_type(\n                    expr, e.EdgeDatabaseTrueFilter()\n                )\n            ]\n            return e.ResultMultiSetVal(all_ids)\n        case e.QualifiedNameWithFilter(name=name, filter=filter):\n\n            def filter_map(filter_expr: Expr) -> Optional[Expr]:\n                if isinstance(filter_expr, e.EdgeDatabaseSelectFilter):  # type: ignore\n                    return None\n                match filter_expr:\n                    case e.FreeVarExpr(var=var):\n                        return ctx[var]  # type: ignore\n                    case e.ScalarVal(_):\n                        return e.ResultMultiSetVal([filter_expr])  # type: ignore\n                    case _:\n                        raise ValueError(\n                            \"Unrecognized filter expression,\"\n                            \" check post processing: \",\n                            filter_expr,\n                        )\n\n            filter_val = eops.map_edge_select_filter(filter_map, filter)  # type: ignore\n            assert isinstance(filter_val, e.EdgeDatabaseSelectFilter)  # type: ignore\n            all_ids = [\n                RefVal(id, name, ObjectVal({}))\n                for id in db.storage.query_ids_for_a_type(\n                    name, filter_val  # type: ignore\n                )\n            ]\n            return e.ResultMultiSetVal(all_ids)\n\n        case FunAppExpr(fun=fname, args=args, overloading_index=idx):\n            assert (\n                idx is not None\n            ), \"overloading index must be set in type checking\"\n            argsv = eval_expr_list(ctx, db, args)\n            # argsv = map_assume_link_target(argsv)\n            assert isinstance(\n                fname, e.QualifiedName\n            ), \"Should resolve in type checking\"\n            looked_up_fun = mops.resolve_func_name(db.get_schema(), fname)[idx]\n            # db.get_schema().fun_defs[fname]\n            f_modifier = looked_up_fun.tp.args_mod\n            assert len(f_modifier) == len(argsv)\n            argv_final: Sequence[Sequence[Sequence[Val]]] = [[]]\n            for i in range(len(f_modifier)):\n                mod_i = f_modifier[i]\n                argv_i: Sequence[Val] = argsv[i].getVals()\n                match mod_i:\n                    case ParamSingleton():\n                        argv_final = [\n                            [*cur, [new]]\n                            for cur in argv_final\n                            for new in argv_i\n                        ]\n                    case ParamOptional():\n                        if len(argv_i) == 0:\n                            argv_final = [[*cur, []] for cur in argv_final]\n                        else:\n                            argv_final = [\n                                [*cur, [new]]\n                                for cur in argv_final\n                                for new in argv_i\n                            ]\n                    case ParamSetOf():\n                        argv_final = [[*cur, argv_i] for cur in argv_final]\n                    case _:\n                        raise ValueError()\n\n            after_fun_vals: Sequence[Val]\n            if isinstance(looked_up_fun, e.BuiltinFuncDef):\n                after_fun_vals = [\n                    v for arg in argv_final for v in looked_up_fun.impl(arg)\n                ]\n            elif isinstance(looked_up_fun, e.DefinedFuncDef):\n                after_fun_vals = []\n                for vset in argv_final:\n                    body = looked_up_fun.impl\n                    for farg in vset:\n                        assert isinstance(body, e.BindingExpr)\n                        ctx, body = ctx_extend(\n                            ctx, body, e.ResultMultiSetVal(farg)\n                        )\n                    after_fun_vals = [\n                        *after_fun_vals,\n                        *eval_expr(ctx, db, body).getVals(),\n                    ]\n            else:\n                raise ValueError(\"Not implemented yet\", looked_up_fun)\n            return e.ResultMultiSetVal(after_fun_vals)\n        case e.TupleProjExpr(subject=subject, label=label) | ObjectProjExpr(\n            subject=subject, label=label\n        ):\n            subjectv = eval_expr(ctx, db, subject)\n            projected = [\n                p\n                for v in subjectv.getVals()\n                for p in singular_proj(\n                    ctx, db, v, StrLabel(label)\n                ).getRawVals()\n            ]\n            return e.ResultMultiSetVal(projected)\n        case BackLinkExpr(subject=subject, label=label):\n            subjectv = eval_expr(ctx, db, subject)\n            # subjectv = assume_link_target(subjectv)\n            subject_ids = [\n                (\n                    v.refid\n                    if isinstance(v, RefVal)\n                    else eval_error(v, \"expecting references\")\n                )\n                for v in subjectv.getVals()\n            ]\n\n            return db.storage.reverse_project(subject_ids, label)\n        case e.IsTpExpr(subject=subject, tp=tp_name):\n            if not isinstance(tp_name, e.QualifiedName):\n                raise ValueError(\"Should be updated during tcking\")\n            subjectv = eval_expr(ctx, db, subject)\n            is_result: list[Val] = []\n            for v in subjectv.getVals():\n                match v:\n                    case RefVal(refid=_, tpname=val_tp, val=_):\n                        is_subtype = is_nominal_subtype_in_schema(\n                            db.get_schema(), val_tp, tp_name\n                        )\n                    case e.ScalarVal(tp=e.ScalarTp(s_name), val=_):\n                        is_subtype = is_nominal_subtype_in_schema(\n                            db.get_schema(), s_name, tp_name\n                        )\n                    case _:\n                        raise ValueError(\"UnExpected Value Type\")\n                is_result = [*is_result, e.BoolVal(is_subtype)]\n            return e.ResultMultiSetVal(is_result)\n        case TpIntersectExpr(subject=subject, tp=tp_name):\n            if not isinstance(tp_name, e.QualifiedName):\n                raise ValueError(\"Should be updated during tcking\")\n            subjectv = eval_expr(ctx, db, subject)\n            after_intersect: list[Val] = []\n            for v in subjectv.getVals():\n                match v:\n                    case RefVal(refid=_, tpname=val_tp, val=_):\n                        if is_nominal_subtype_in_schema(\n                            db.get_schema(), val_tp, tp_name\n                        ):\n                            after_intersect = [*after_intersect, v]\n                    case _:\n                        raise ValueError(\"Expecting References\")\n            return e.ResultMultiSetVal(after_intersect)\n        case e.CheckedTypeCastExpr(cast_tp=_, cast_spec=cast_spec, arg=arg):\n            argv2 = eval_expr(ctx, db, arg)\n            casted = [cast_spec.cast_fun(v) for v in argv2.getVals()]\n            return e.ResultMultiSetVal(casted)\n        case UnnamedTupleExpr(val=tuples):\n            tuplesv = eval_expr_list(ctx, db, tuples)\n            result_list: list[Val] = []\n            for prod in itertools.product(*map_expand_multiset_val(tuplesv)):\n                result_list.append(UnnamedTupleVal(list(prod)))\n            return e.ResultMultiSetVal(result_list)\n        case NamedTupleExpr(val=tuples):\n            tuplesv = eval_expr_list(ctx, db, list(tuples.values()))\n            result_list = []\n            for prod in itertools.product(*map_expand_multiset_val(tuplesv)):\n                result_list.append(\n                    NamedTupleVal(\n                        {\n                            k: p\n                            for (k, p) in zip(tuples.keys(), prod, strict=True)\n                        }\n                    )\n                )\n            return e.ResultMultiSetVal(result_list)\n        case UnionExpr(left=l, right=r):\n            lvals = eval_expr(ctx, db, l)\n            rvals = eval_expr(ctx, db, r)\n            return e.ResultMultiSetVal([*lvals.getVals(), *rvals.getVals()])\n        case ArrExpr(elems=elems):\n            elemsv = eval_expr_list(ctx, db, elems)\n            arr_result = [\n                ArrVal(list(el))\n                for el in itertools.product(*map_expand_multiset_val(elemsv))\n            ]\n            return e.ResultMultiSetVal(arr_result)\n        case e.DeleteExpr(subject=subject):\n            subjectv = eval_expr(ctx, db, subject)\n            if all([val_is_ref_val(v) for v in subjectv.getVals()]):\n                delete_ref_ids = [\n                    (v.refid, v.tpname) for v in subjectv.getVals()\n                ]\n                for delete_id, tpname in delete_ref_ids:\n                    db.delete(delete_id, tpname)\n                return subjectv\n            else:\n                return eval_error(expr, \"expecting all references\")\n        case UpdateExpr(subject=subject, shape=shape):\n            subjectv = eval_expr(ctx, db, subject)\n            if all([val_is_ref_val(v) for v in subjectv.getVals()]):\n                updated: Sequence[Val] = [\n                    apply_shape(ctx, db, shape, v) for v in subjectv.getVals()\n                ]  # type: ignore[misc]\n                for u in cast(Sequence[RefVal], updated):\n                    full_tp = tops.dereference_var_tp(\n                        db.get_schema(), u.tpname\n                    )\n                    cut_tp = {\n                        k: v\n                        for (k, v) in full_tp.val.items()\n                        if StrLabel(k) in u.val.val.keys()\n                    }\n                    db.update(\n                        u.refid,\n                        u.tpname,\n                        coerce_to_storage(u.val, e.ObjectTp(cut_tp)),\n                    )\n                return e.ResultMultiSetVal(updated)\n            else:\n                return eval_error(expr, \"expecting all references\")\n        case MultiSetExpr(expr=elems):\n            elemsv = eval_expr_list(ctx, db, elems)\n            result_list = [e for el in elemsv for e in el.getVals()]\n            return e.ResultMultiSetVal(result_list)\n        case WithExpr(bound=bound, next=next):\n            boundv = eval_expr(ctx, db, bound)\n            new_ctx, next_body = ctx_extend(ctx, next, boundv)\n            nextv = eval_expr(new_ctx, db, next_body)\n            return nextv\n        case OffsetLimitExpr(subject=subject, offset=offset, limit=limit):\n            subjectv = eval_expr(ctx, db, subject)\n            offsetv_m = eval_expr(ctx, db, offset)\n            assert len(offsetv_m.getVals()) <= 1\n            offsetv = (\n                offsetv_m.getVals()[0]\n                if len(offsetv_m.getVals()) == 1\n                else e.IntVal(0)\n            )\n            limitv_m = eval_expr(ctx, db, limit)\n            offseted_result = offset_vals(subjectv.getVals(), offsetv)\n            assert len(limitv_m.getVals()) <= 1\n            if len(limitv_m.getVals()) == 1:\n                limitv = limitv_m.getVals()[0]\n                result_list = list(limit_vals(offseted_result, limitv))\n            else:\n                result_list = offseted_result\n            return e.ResultMultiSetVal(result_list)\n        case SubqueryExpr(expr=expr):\n            exprv = eval_expr(ctx, db, expr)\n            return exprv\n        case DetachedExpr(expr=expr):\n            exprv = eval_expr(ctx, db, expr)\n            return exprv\n        case LinkPropProjExpr(subject=subject, linkprop=label):\n            subjectv = eval_expr(ctx, db, subject)\n            projected = [\n                p\n                for v in subjectv.getVals()\n                for p in singular_proj(\n                    ctx, db, v, LinkPropLabel(label)\n                ).getVals()\n            ]\n            return e.ResultMultiSetVal(projected)\n        case ForExpr(bound=bound, next=next):\n            boundv = eval_expr(ctx, db, bound)\n            vv = []\n            for v in boundv.getVals():\n                new_ctx, next_body = ctx_extend(\n                    ctx, next, e.ResultMultiSetVal([v])\n                )\n                nextv = eval_expr(new_ctx, db, next_body)\n                vv.append(nextv)\n            result_list = [p for v in vv for p in v.getVals()]\n            return e.ResultMultiSetVal(result_list)\n        case e.IfElseExpr(\n            then_branch=then_branch,\n            condition=condition,\n            else_branch=else_branch,\n        ):\n            conditionv = eval_expr(ctx, db, condition)\n            vv2 = eval_expr_list(\n                ctx,\n                db,\n                [\n                    (\n                        then_branch\n                        if v == e.BoolVal(True)\n                        else (\n                            else_branch\n                            if v == e.BoolVal(False)\n                            else eval_error(\n                                condition, \"condition must be a boolean\"\n                            )\n                        )\n                    )\n                    for v in conditionv.getVals()\n                ],\n            )\n            result_list = [p for v in vv2 for p in v.getVals()]\n            return e.ResultMultiSetVal(result_list)\n        case OptionalForExpr(bound=bound, next=next):\n            boundv = eval_expr(ctx, db, bound)\n            if boundv.getVals():\n                vv = []\n                for v in boundv.getVals():\n                    new_ctx, next_body = ctx_extend(\n                        ctx, next, e.ResultMultiSetVal([v])\n                    )\n                    nextv = eval_expr(new_ctx, db, next_body)\n                    vv.append(nextv)\n                result_list = [p for v in vv for p in v.getVals()]\n                return e.ResultMultiSetVal(result_list)\n            else:\n                new_ctx, next_body = ctx_extend(\n                    ctx, next, e.ResultMultiSetVal([])\n                )\n                return eval_expr(new_ctx, db, next_body)\n        case e.ParameterExpr(name=name, tp=_, is_required=_):\n            param_name = get_param_reserved_name(name)\n            if param_name in ctx.keys():\n                return ctx[param_name]\n            else:\n                raise ValueError(\"Parameter not found\", name, param_name)\n\n    raise ValueError(\"Not Implemented\", expr)\n\n\ndef eval_ctx_from_variables(variables) -> EvalEnv:\n    def get_prim_param_value(v) -> Val:\n        if isinstance(v, str):\n            return e.StrVal(v)\n        elif isinstance(v, int):\n            return e.IntVal(v)\n        elif isinstance(v, list):\n            return e.ArrVal([get_prim_param_value(vv) for vv in v])\n        else:\n            raise ValueError(\"Unimplemented\")\n\n    def get_prim_param_multiset_value(v) -> MultiSetVal:\n        if v is None:\n            return e.ResultMultiSetVal([])\n        elif isinstance(v, str) or isinstance(v, int):\n            return e.ResultMultiSetVal([get_prim_param_value(v)])\n        elif isinstance(v, list):\n            return e.ResultMultiSetVal([get_prim_param_value(v)])\n        else:\n            raise ValueError(\"Unimplemented\")\n\n    if isinstance(variables, dict):\n        return {\n            get_param_reserved_name(k): get_prim_param_multiset_value(v)\n            for k, v in variables.items()\n        }\n    elif isinstance(variables, tuple):\n        return {\n            (get_param_reserved_name(i)): get_prim_param_multiset_value(v)\n            for i, v in enumerate(variables)\n        }\n    else:\n        raise ValueError(\"\")\n\n\ndef eval_expr_toplevel(\n    db: EdgeDatabase,\n    expr: Expr,\n    variables: Optional[dict[str, Val] | tuple[Val, ...]] = None,\n    logs: Optional[Any] = None,\n) -> MultiSetVal:\n\n    # on exception, this is not none\n    # assert eval_logs_wrapper.logs is None\n    if logs is not None:\n        eval_logs_wrapper.reset_logs(logs)\n\n    initial_ctx = eval_ctx_from_variables(variables) if variables else {}\n\n    final_v = eval_expr(initial_ctx, db, expr)\n    # commit DML after evaluation\n    db.commit_dml()\n\n    # restore the decorator state\n    eval_logs_wrapper.reset_logs(None)\n\n    # Do not dedup (see one of the test cases)\n    # i.e. should not return assume_link_target(final_v)\n    return final_v\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/evaluation_tools/storage_coercion.py",
    "content": "from ..data import data_ops as e\nfrom ..data.data_ops import (\n    Val,\n    Tp,\n    ObjectTp,\n    RefVal,\n    ObjectVal,\n    LinkPropLabel,\n    Visible,\n    MultiSetVal,\n    StrLabel,\n)\nfrom ..data import expr_ops as eops\nfrom ..data import type_ops as tops\nfrom ..data import expr_to_str as pp\n\n\ndef make_storage_atomic(val: Val, tp: Tp) -> Val:\n    def do_coerce_value_to_linkprop_tp(tp_linkprop: ObjectTp) -> Val:\n        match val:\n            case RefVal(refid=id, tpname=tpname, val=obj):\n                obj_link_prop = eops.remove_unless_link_props(obj)\n                temp_obj = eops.link_prop_obj_to_obj(obj_link_prop)\n                after_obj = coerce_to_storage(temp_obj, tp_linkprop)\n                return RefVal(\n                    id,\n                    tpname,\n                    ObjectVal(\n                        {\n                            LinkPropLabel(k): (Visible(), v)\n                            for (k, v) in after_obj.items()\n                        }\n                    ),\n                )\n            case _:\n                raise ValueError(\"Cannot Coerce to LinkPropType\", val)\n\n    match tp:\n        case e.NamedNominalLinkTp(name=_, linkprop=tp_linkprop):\n            return do_coerce_value_to_linkprop_tp(tp_linkprop=tp_linkprop)\n        case e.NominalLinkTp(name=_, subject=_, linkprop=tp_linkprop):\n            return do_coerce_value_to_linkprop_tp(tp_linkprop=tp_linkprop)\n        case e.ScalarTp(_):\n            return val\n        case e.DefaultTp(expr=_, tp=d_tp):\n            return make_storage_atomic(val, d_tp)\n        case e.UnionTp(_, _):\n            all_tps = tops.collect_tp_union(tp)\n            assert len(all_tps) > 0\n            if all(\n                isinstance(tp, e.NamedNominalLinkTp | e.NominalLinkTp)\n                for tp in all_tps\n            ):\n                lp: ObjectTp = all_tps[0].linkprop  # type: ignore\n                if all(tp.linkprop == lp for tp in all_tps):  # type: ignore\n                    return do_coerce_value_to_linkprop_tp(tp_linkprop=lp)\n                else:\n                    raise ValueError(\"TODO\")\n            else:\n                raise ValueError(\"TODO\")\n        case e.CompositeTp(_, tps, _):\n            if all(tops.tp_is_primitive(tp) for tp in tps):\n                return val\n            else:\n                raise ValueError(\"TODO\")\n        case _:\n            raise ValueError(\"Coercion Not Implemented for\", tp)\n\n\n# we require fmt to be a storage tp -- No Computable Types should be present\ndef coerce_to_storage(val: ObjectVal, fmt: ObjectTp) -> dict[str, MultiSetVal]:\n    # ensure no redundant keys\n    extra_keys = [\n        k\n        for k in val.val.keys()\n        if k not in [StrLabel(k) for k in fmt.val.keys()]\n    ]\n    if extra_keys:\n        raise ValueError(\n            \"Coercion failed, object contains redundant keys:\",\n            extra_keys,\n            \"val_keys are\",\n            val.val.keys(),\n            \"fmt_keys are\",\n            fmt.val.keys(),\n            \"when coercing \",\n            pp.show_val(val),\n            \" to \",\n            pp.show_tp(fmt),\n        )\n    left_out_keys = [\n        k for k in fmt.val.keys() if StrLabel(k) not in val.val.keys()\n    ]\n    if left_out_keys:\n        raise ValueError(\n            \"Coercion failed, object missing keys:\",\n            left_out_keys,\n            \"when coercing \",\n            pp.show(val),\n            \" to \",\n            pp.show(fmt),\n        )\n    return {\n        k: (\n            e.ResultMultiSetVal(\n                [\n                    make_storage_atomic(v, tp[0])\n                    for v in val.val[StrLabel(k)][1].getVals()\n                ]\n            )\n            if StrLabel(k) in val.val.keys()\n            else e.ResultMultiSetVal([])\n        )\n        for (k, tp) in fmt.val.items()\n    }\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/helper_funcs.py",
    "content": "from __future__ import annotations\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import Source, parser\nfrom typing import Any, Sequence\nimport json\nimport uuid\nfrom edb.errors import EdgeQLSyntaxError\nimport re\n\n\nclass EdbJSONEncoder(json.JSONEncoder):\n    def default(self, x: Any) -> Any:\n        if isinstance(x, uuid.UUID):\n            return str(x)\n        return super().default(x)\n\n\ndef parse_ddl(ddlstr: str) -> list[qlast.DDLOperation]:\n    ddls = parser.parse_block(Source.from_string(ddlstr))\n    assert all(isinstance(ddl, qlast.DDLOperation) for ddl in ddls)\n    return ddls  # type: ignore[return-value]\n\n\ndef parse_ql(querystr: str) -> Sequence[qlast.Expr]:\n    def notExpr(expr: qlast.Base) -> Any:\n        raise EdgeQLSyntaxError(\"Not an Expression\", span=expr.span)\n\n    source = Source.from_string(querystr)\n    base_statements = parser.parse_block(source)  # type : Sequence[Expr]\n    statements: Sequence[qlast.Expr] = [\n        s if isinstance(s, qlast.Expr) else notExpr(s) for s in base_statements\n    ]\n    return statements\n\n\ndef parse_sdl(sdlstr: str) -> qlast.Schema:\n    # See test_docs.py if this doesn't work\n    contains_module = re.match(\n        r'''(?xm)\\s*\n            (\\bmodule\\s+\\w+\\s*{) |\n            (^.*\n                (type|annotation|link|property|constraint)\n                \\s+(\\w+::\\w+)\\s+\n                ({|extending)\n            )\n        ''',\n        sdlstr,\n    )\n    if contains_module:\n        return parser.parse_sdl(sdlstr)\n    else:\n        return parser.parse_sdl(f'module default {{{sdlstr}}}')\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/interpreter_logging.py",
    "content": "# SHOW_WARNINGS = True\nSHOW_WARNINGS = False\n\n\ndef print_warning(*args, **kwargs):\n    if SHOW_WARNINGS:\n        print(*args, **kwargs)\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/logs.py",
    "content": "import json\nfrom typing import Any\nfrom edb.edgeql import codegen\nfrom .back_to_ql import reverse_elab\nfrom .data.val_to_json import multi_set_val_to_json_like\nfrom .data import expr_to_str as pp\n\n\ndef to_html_str(s: str) -> str:\n    return s.replace(\"λ\", \"&lambda;\")\n\n\ndef do_write_logs(logs: list[Any], filename: str):\n\n    def format_entry(entry, index):\n        entry_id = '_'.join(map(str, index))\n        result = \"\"\"<a href='#/' onclick='toggle(\"{}\")'>\n                    Input/Output {}</a>\\n\"\"\".format(\n            entry_id, entry_id\n        )\n        result += \"\"\"<button onclick='foldEntry(\\\"{}\\\")'>Fold</button>\n                   <button onclick='unfoldEntry(\\\"{}\\\")'>\n                   Unfold</button>\"\"\".format(\n            entry_id, entry_id\n        )\n        result += \"<div class='entry' id='entry_{}'>\".format(entry_id)\n        result += \"\"\"<div class='input'><span style='color:blue;'>Input:\n                    </span> {}</div>\"\"\".format(\n            to_html_str(pp.show(entry[0]))\n        )\n        result += \"\"\"<div class='input'><span style='color:green;'>\n                     Human-friendly Input:</span> {}</div>\"\"\".format(\n            codegen.generate_source(reverse_elab(entry[0]))\n        )\n        if len(entry) > 1:\n            result += \"\"\"<div class='output'><span style='color:red;'>\n                         Output:</span> {}</div>\"\"\".format(\n                to_html_str(pp.show(entry[1]))\n            )\n            try:\n                json_text = json.dumps(\n                    multi_set_val_to_json_like(entry[1]), indent=4\n                )\n            except Exception as e:\n                json_text = \"EXCEPTION OCCURRED\" + str(e)\n            result += \"\"\"<div class='output'><span style='color:green;'>\n                        Human-friendly Output:</span> {}</div>\"\"\".format(\n                json_text\n            )\n        result += \"</div>\\n\"\n        return result\n\n    def format_log(log, index):\n        result = \"<ul {} id='entry_{}'>\\n\".format(\n            \"class='entry'\" if len(index) > 0 else '',\n            '_'.join(map(str, index)),\n        )\n        for i, entry in enumerate(log):\n            result += \"<li>\\n\"\n            if isinstance(entry, list):\n                sub_index = index + (i,)\n                result += \"\"\"<a href='#/' onclick='toggle(\"{}\")'>\n                             Log {}</a>\\n\"\"\".format(\n                    '_'.join(map(str, sub_index)),\n                    '_'.join(map(str, sub_index)),\n                )\n                result += format_log(entry, sub_index)\n            else:\n                sub_index = index + (i,)\n                result += format_entry(entry, sub_index)\n            result += \"</li>\\n\"\n        result += \"</ul>\\n\"\n        return result\n\n    with open(filename, \"w\") as f:\n        f.write(\"<html>\\n\")\n        f.write(\"<head>\\n\")\n        f.write(\"<title>Log</title>\\n\")\n        f.write(\"\"\"<meta charset=\"UTF-8\">\\n\"\"\")\n        f.write(\"<style>\\n\")\n        f.write(\".entry { margin-left: 20px; }\\n\")\n        f.write(\"</style>\\n\")\n        f.write(\"</head>\\n\")\n        f.write(\"<body>\\n\")\n        f.write(\"<h1>Log</h1>\\n\")\n        f.write(\"<button onclick='foldAll()'>Fold all</button>\\n\")\n        f.write(\"<button onclick='unfoldAll()'>Unfold all</button>\\n\")\n        f.write(format_log(logs, ()))\n        f.write(\"<script>\\n\")\n        f.write(\"function toggle(index) {\\n\")\n        f.write(\"  var entry = document.getElementById('entry_' + index);\\n\")\n        f.write(\n            \"  entry.style.display = entry.style.display === 'none' ?\"\n            \" 'block' : 'none';\\n\"\n            \"\"\n        )\n        f.write(\"return False;}\\n\")\n        f.write(\"function foldAll() {\\n\")\n        f.write(\"  var entries = document.getElementsByClassName('entry');\\n\")\n        f.write(\"  for (var i = 0; i < entries.length; i++) {\\n\")\n        f.write(\"    entries[i].style.display = 'none';\\n\")\n        f.write(\"  }\\n\")\n        f.write(\"}\\n\")\n        f.write(\"function unfoldAll() {\\n\")\n        f.write(\"  var entries = document.getElementsByClassName('entry');\\n\")\n        f.write(\"  for (var i = 0; i < entries.length; i++) {\\n\")\n        f.write(\"    entries[i].style.display = 'block';\\n\")\n        f.write(\"  }\\n\")\n        f.write(\"}\\n\")\n        f.write(\n            \"\"\"\n            function foldEntry(id) {\n                var entry = document.getElementById('entry_' + id);\n                entry.style.display = 'none';\n                var entries = entry.querySelectorAll('.entry');\n                for (var i = 0; i < entries.length; i++) {\n                    entries[i].style.display = 'none';\n                }\n            }\n\n            function unfoldEntry(id) {\n                var entry = document.getElementById('entry_' + id);\n                entry.style.display = 'block';\n                var entries = entry.querySelectorAll('.entry');\n                for (var i = 0; i < entries.length; i++) {\n                    entries[i].style.display = 'block';\n                }\n            }\n        \"\"\"\n        )\n        f.write(\"</script>\\n\")\n        f.write(\"</body>\\n\")\n        f.write(\"</html>\\n\")\n\n\ndef write_logs_to_file(logs: list[Any], filepath: str):\n    # the logs are structured as follows:\n    # Log ::= [(Input, Output), Log_1, ..., Log_n]\n    # where Log_1 ... Log_n are sub logs\n    do_write_logs(logs, filepath)\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/new_interpreter.py",
    "content": "from __future__ import annotations\n\n\nimport sys\nimport traceback\nimport os\ntry:\n    import readline\nexcept ImportError:\n    readline = None  # type: ignore[assignment]\n\nfrom edb.common import debug\nfrom edb.edgeql import ast as qlast\n\nfrom typing import Optional, Any, Sequence\nfrom .type_checking_tools import typechecking as tc\nfrom .back_to_ql import reverse_elab\nfrom .data import data_ops as e\nfrom .data.data_ops import DBSchema, MultiSetVal, ResultTp, Val, Expr\nfrom .data.expr_to_str import show_expr, show_result_tp\nfrom .data.path_factor import select_hoist\nfrom .post_processing_tools import post_processing\nfrom .data.val_to_json import json_like, typed_multi_set_val_to_json_like\nfrom .elab_schema import add_module_from_sdl_defs, add_module_from_sdl_file\nfrom .elaboration import elab\nfrom .evaluation import eval_expr_toplevel\nfrom .helper_funcs import parse_ql\nfrom .logs import write_logs_to_file\nfrom .sqlite import sqlite_adapter\nfrom .data import expr_to_str as pp\nfrom .db_interface import EdgeDatabase, InMemoryEdgeDatabaseStorageProvider\nfrom .schema.library_discovery import add_ddl_library\nfrom .type_checking_tools import schema_checking as sck\nfrom .type_checking_tools import name_resolution\n\n# CODE REVIEW: !!! CHECK IF THIS WILL BE SET ON EVERY RUN!!!\n# sys.setrecursionlimit(10000)\n\n\nVariablesTp = Optional[dict[str, Val] | tuple[Val, ...]]\n\n\ndef empty_db(schema: DBSchema) -> EdgeDatabase:\n    storage = InMemoryEdgeDatabaseStorageProvider(schema)\n    return EdgeDatabase(storage)\n\n\ndef empty_dbschema() -> DBSchema:\n    return DBSchema({}, {}, {}, {}, {})\n\n\ndef default_dbschema() -> DBSchema:\n    initial_db = empty_dbschema()\n    relative_path_to_std = os.path.join(\"..\", \"..\", \"lib\", \"std\")\n    relative_path_to_schema = os.path.join(\"..\", \"..\", \"lib\", \"schema.edgeql\")\n    relative_path_to_cal = os.path.join(\"..\", \"..\", \"lib\", \"cal.edgeql\")\n    relative_path_to_math = os.path.join(\"..\", \"..\", \"lib\", \"math.edgeql\")\n    relative_path_to_interpreter_internal = os.path.join(\n        \"basis\", \"80-interpreter-internal.edgeql\"\n    )\n    std_path = os.path.join(os.path.dirname(__file__), relative_path_to_std)\n    schema_path = os.path.join(\n        os.path.dirname(__file__), relative_path_to_schema\n    )\n    cal_path = os.path.join(os.path.dirname(__file__), relative_path_to_cal)\n    math_path = os.path.join(os.path.dirname(__file__), relative_path_to_math)\n    interpreter_internal_path = os.path.join(\n        os.path.dirname(__file__), relative_path_to_interpreter_internal\n    )\n    add_ddl_library(\n        initial_db,\n        [\n            std_path,\n            schema_path,\n            cal_path,\n            math_path,\n            interpreter_internal_path,\n        ],\n    )\n    name_resolution.checked_module_name_resolve(initial_db, (\"schema\",))\n    name_resolution.checked_module_name_resolve(initial_db, (\"std\",))\n    sck.re_populate_module_inheritance(initial_db, (\"std\",))\n    sck.re_populate_module_inheritance(initial_db, (\"schema\",))\n    return initial_db\n\n\ndef prepare_statement(\n    stmt: qlast.Expr,\n    dbschema: DBSchema,\n    should_print: bool,\n) -> tuple[e.Expr, e.ResultTp]:\n    dbschema_ctx = e.TcCtx(dbschema, (\"default\",), {})\n\n    if should_print:\n        print(\"vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv Starting\")\n        debug.dump_edgeql(stmt)\n        print(\">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Elaborating\")\n\n    elaborated = elab(stmt)\n\n    if should_print:\n        debug.print(show_expr(elaborated))\n        debug.dump_edgeql(reverse_elab(elaborated))\n        print(\">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Preprocessing\")\n\n    factored = select_hoist(elaborated, dbschema_ctx)\n\n    if should_print:\n        debug.print(show_expr(factored))\n        reverse_elabed = reverse_elab(factored)\n        debug.dump_edgeql(reverse_elabed)\n        print(\">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Type Checking\")\n\n    tp, type_checked = tc.synthesize_type(dbschema_ctx, factored)\n\n    if should_print:\n        debug.print(show_result_tp(tp))\n        reverse_elabed = reverse_elab(type_checked)\n        debug.dump_edgeql(reverse_elabed)\n        print(\">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Post Processing\")\n\n    deduped = post_processing.post_process(type_checked)\n\n    if should_print:\n        debug.print(pp.show(deduped))\n        reverse_elabed = reverse_elab(deduped)\n        debug.dump_edgeql(reverse_elabed)\n        print(\">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Running\")\n\n    return deduped, tp\n\n\ndef run_prepared_statement(\n    db: EdgeDatabase,\n    deduped: e.Expr,\n    tp: e.ResultTp,\n    dbschema: DBSchema,\n    should_print: bool,\n    logs: Optional[list[Any]],\n    variables: VariablesTp = None,\n) -> MultiSetVal:\n    result = eval_expr_toplevel(db, deduped, variables=variables, logs=logs)\n    if should_print:\n        print(\">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Result\")\n        debug.print(pp.show_multiset_val(result))\n        print(typed_multi_set_val_to_json_like(tp, result, dbschema))\n        print(\"^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Done \")\n    return result\n\n\ndef run_statement(\n    db: EdgeDatabase,\n    stmt: qlast.Expr,\n    dbschema: DBSchema,\n    should_print: bool,\n    logs: Optional[list[Any]],\n    variables: VariablesTp = None,\n) -> tuple[MultiSetVal, e.ResultTp]:\n\n    deduped, tp = prepare_statement(stmt, dbschema, should_print)\n    result = run_prepared_statement(\n        db, deduped, tp, dbschema, should_print, logs, variables\n    )\n    return result, tp\n\n\ndef run_stmts(\n    db: EdgeDatabase,\n    stmts: Sequence[qlast.Expr],\n    dbschema: DBSchema,\n    debug_print: bool,\n    logs: Optional[list[Any]],\n) -> Sequence[MultiSetVal]:\n    match stmts:\n        case []:\n            return []\n        case current, *rest:\n            (cur_val, _) = run_statement(\n                db,\n                current,\n                dbschema,\n                should_print=debug_print,\n                logs=logs,\n            )\n            rest_val = run_stmts(\n                db,\n                rest,\n                dbschema,\n                debug_print,\n                logs=logs,\n            )\n            return [cur_val, *rest_val]\n    raise ValueError(\"Not Possible\")\n\n\ndef run_meta_cmd(db: EdgeDatabase, dbschema: DBSchema, cmd: str) -> None:\n    if cmd == r\"\\ps\":\n        print(pp.show_module(dbschema.modules[(\"default\",)]) + \"\\n\")\n    elif cmd == r\"\\ps --all\":\n        print(pp.show_schema(dbschema) + \"\\n\")\n    else:\n        raise ValueError(\"Unknown meta command: \" + cmd)\n\n\ndef run_str(\n    db: EdgeDatabase,\n    dbschema: DBSchema,\n    s: str,\n    print_asts: bool = False,\n    logs: Optional[list[str]] = None,\n) -> Sequence[MultiSetVal]:\n\n    q = parse_ql(s)\n    res = run_stmts(db, q, dbschema, print_asts, logs)\n    return res\n\n\ndef run_single_str(\n    dbschema_and_db: tuple[DBSchema, EdgeDatabase],\n    s: str,\n    variables: VariablesTp = None,\n    print_asts: bool = False,\n) -> tuple[MultiSetVal, ResultTp]:\n    q = parse_ql(s)\n    if len(q) != 1:\n        raise ValueError(\"Not a single query\")\n    dbschema, db = dbschema_and_db\n    (res, tp) = run_statement(\n        db, q[0], dbschema, print_asts, variables=variables, logs=None\n    )\n    return (res, tp)\n\n\ndef run_single_str_get_json(\n    dbschema_and_db: tuple[DBSchema, EdgeDatabase],\n    s: str,\n    variables: VariablesTp = None,\n    print_asts: bool = False,\n) -> json_like:\n    (res, tp) = run_single_str(\n        dbschema_and_db, s, variables=variables, print_asts=print_asts\n    )\n    return typed_multi_set_val_to_json_like(\n        tp, res, dbschema_and_db[0], top_level=True\n    )\n\n\ndef interpreter_parser_init():\n    from edb.edgeql import parser as ql_parser\n\n    ql_parser.preload_spec()\n\n\ndef repl(\n    *,\n    init_sdl_file=None,\n    init_ql_file=None,\n    next_ql_file=None,\n    library_ddl_files=None,\n    debug_print=False,\n    trace_to_file_path=None,\n    sqlite_file=None,\n) -> None:\n    interpreter_parser_init()\n\n    dbschema: DBSchema\n    db: EdgeDatabase\n    logs: list[Any] = []  # type: ignore[var]\n\n    dbschema = default_dbschema()\n    if library_ddl_files:\n        add_ddl_library(dbschema, library_ddl_files)\n\n    if sqlite_file is not None:\n        if init_sdl_file is not None:\n            with open(init_sdl_file) as f:\n                init_sdl_file_content = f.read()\n        else:\n            init_sdl_file_content = None\n        (dbschema, db) = sqlite_adapter.schema_and_db_from_sqlite(\n            init_sdl_file_content, sqlite_file\n        )\n    else:\n        if init_sdl_file is not None:\n            dbschema = add_module_from_sdl_file(\n                dbschema, init_sdl_file_path=init_sdl_file\n            )\n        else:\n            dbschema = dbschema\n        db = empty_db(dbschema)\n\n    if debug_print:\n        print(\"=== ALL Schema Loaded ===\")\n        print(pp.show_module(dbschema.modules[(\"default\",)]))\n\n    if init_ql_file is not None:\n        initial_queries = open(init_ql_file).read()\n        run_str(\n            db, dbschema, initial_queries, print_asts=debug_print, logs=logs\n        )\n\n    try:\n        if next_ql_file is not None:\n            next_queries = open(next_ql_file).read()\n            run_str(\n                db, dbschema, next_queries, print_asts=debug_print, logs=logs\n            )\n    except Exception:\n        traceback.print_exception(*sys.exc_info())\n\n    history_file = \".edgeql_interpreter_history.temp.txt\"\n    try:\n        if readline:\n            readline.read_history_file(history_file)\n    except FileNotFoundError:\n        pass\n\n    while True:\n        if trace_to_file_path is not None:\n            write_logs_to_file(logs, trace_to_file_path)\n        s = \"\"\n\n        def reset_s():\n            nonlocal s\n            print(\"\\nKeyboard Interrupt\")\n            s = \"\"\n\n        while ';' not in s and not s.startswith(\"\\\\\"):\n            if s:\n                try:\n                    s += input(\"... \")\n                except KeyboardInterrupt:\n                    reset_s()\n                    continue\n            else:\n                try:\n                    s += input(\"> \")\n                except KeyboardInterrupt:\n                    reset_s()\n                    continue\n        try:\n            if readline:\n                readline.write_history_file(history_file)\n            if s.startswith(\"\\\\\"):\n                run_meta_cmd(db, dbschema, s)\n            else:\n                run_str(db, dbschema, s, print_asts=debug_print, logs=logs)\n        except Exception:\n            traceback.print_exception(*sys.exc_info())\n\n\ndef dbschema_and_db_with_initial_schema_and_queries(\n    initial_schema_defs: Optional[str],\n    initial_queries: str,\n    sqlite_file_name: Optional[str] = None,\n    debug_print=False,\n    logs: Optional[list[Any]] = None,\n) -> tuple[DBSchema, EdgeDatabase]:\n    if sqlite_file_name is not None:\n        dbschema, db = sqlite_adapter.schema_and_db_from_sqlite(\n            initial_schema_defs, sqlite_file_name\n        )\n    else:\n        dbschema = default_dbschema()\n        if initial_schema_defs is not None:\n            dbschema = add_module_from_sdl_defs(dbschema, initial_schema_defs)\n        db = empty_db(dbschema)\n    run_str(db, dbschema, initial_queries, print_asts=debug_print, logs=logs)\n    return dbschema, db\n\n\nclass EdgeQLInterpreter:\n\n    def __init__(\n        self,\n        initial_schema_defs: Optional[str] = None,\n        sqlite_file_name: Optional[str] = None,\n    ):\n        interpreter_parser_init()\n        dbschema, db = dbschema_and_db_with_initial_schema_and_queries(\n            initial_schema_defs, \"\", sqlite_file_name\n        )\n        self.dbschema: e.DBSchema = dbschema\n        self.db: EdgeDatabase = db\n        self.query_cache: dict[str, tuple[Expr, ResultTp]] = {}\n\n    def run_single_str_get_json_with_cache(\n        self,\n        s: str,\n        variables: VariablesTp = None,\n        disable_cache: bool = False,\n    ) -> json_like:\n        if not disable_cache and s in self.query_cache:\n            (query_expr, tp) = self.query_cache[s]\n        else:\n            q = parse_ql(s)\n            if len(q) != 1:\n                raise ValueError(\"Not a single query\")\n            query_expr, tp = prepare_statement(q[0], self.dbschema, False)\n            self.query_cache[s] = (query_expr, tp)\n\n        res = run_prepared_statement(\n            self.db,\n            query_expr,\n            tp,\n            self.dbschema,\n            should_print=False,\n            logs=None,\n            variables=variables,\n        )\n        result = typed_multi_set_val_to_json_like(\n            tp, res, self.dbschema, top_level=True\n        )\n\n        return result\n\n    def query_single_json(self, s: str, **kwargs) -> json_like:\n        result = self.run_single_str_get_json_with_cache(s, kwargs)\n        if isinstance(result, list) and len(result) == 1:\n            return result[0]\n        else:\n            raise ValueError(\"Expected a single result\")\n\n    def query_json(self, s: str, **kwargs) -> json_like:\n        result = self.run_single_str_get_json_with_cache(s, kwargs)\n        return result\n\n    def query_str(self, s: str) -> Sequence[MultiSetVal]:\n        q = parse_ql(s)\n        res = run_stmts(\n            self.db, q, self.dbschema, debug_print=False, logs=None\n        )\n        return res\n\n\nif __name__ == \"__main__\":\n    repl()\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/post_processing_tools/insert_select_optimization.py",
    "content": "from typing import Optional\nfrom ..data import data_ops as e\nfrom ..data import expr_ops as eops\n\n\ndef try_collect_constraints_from_filter(\n    expr: e.Expr,\n) -> Optional[e.EdgeDatabaseSelectFilter]:\n    if not isinstance(expr, e.BindingExpr):\n        return None\n    bnd_name = expr.var\n\n    def try_iterative_collection(\n        expr_body: e.Expr,\n    ) -> Optional[e.EdgeDatabaseSelectFilter]:\n        match expr_body:\n            case e.FunAppExpr(\n                fun=e.QualifiedName([\"std\", \"=\"]),\n                overloading_index=_,\n                args=[arg1, arg2],\n                kwargs={},\n            ):\n                match (arg1, arg2):\n                    case (\n                        e.ConditionalDedupExpr(\n                            e.ObjectProjExpr(\n                                subject=e.BoundVarExpr(subject_name),\n                                label=label,\n                            )\n                        ),\n                        _,\n                    ):\n                        if (\n                            subject_name == bnd_name\n                            and not eops.appears_in_expr(\n                                e.FreeVarExpr(bnd_name), arg2\n                            )\n                        ):\n                            return e.EdgeDatabaseEqFilter(label, arg2)\n                        else:\n                            return None\n                    case (\n                        _,\n                        e.ConditionalDedupExpr(\n                            e.ObjectProjExpr(\n                                subject=e.BoundVarExpr(subject_name),\n                                label=label,\n                            )\n                        ),\n                    ):\n                        if (\n                            subject_name == bnd_name\n                            and not eops.appears_in_expr(\n                                e.FreeVarExpr(bnd_name), arg1\n                            )\n                        ):\n                            return e.EdgeDatabaseEqFilter(label, arg1)\n                        else:\n                            return None\n                    case _:\n                        return None\n            case e.FunAppExpr(\n                fun=e.QualifiedName([\"std\", \"IN\"]),\n                overloading_index=_,\n                args=[\n                    e.ConditionalDedupExpr(\n                        e.ObjectProjExpr(\n                            subject=e.BoundVarExpr(subject_name), label=label\n                        )\n                    ),\n                    arg2,\n                ],\n                kwargs={},\n            ):\n                if subject_name == bnd_name and not eops.appears_in_expr(\n                    e.FreeVarExpr(bnd_name), arg2\n                ):\n                    return e.EdgeDatabaseEqFilter(label, arg2)\n                else:\n                    return None\n            case _:\n                return None\n\n    return try_iterative_collection(expr.body)\n\n\ndef is_trivial_shape_element(shape: e.ShapeExpr, label: str) -> bool:\n    shape_elem = shape.shape[e.StrLabel(label)]\n    bnd_name = shape_elem.var\n    match shape_elem.body:\n        case e.ConditionalDedupExpr(\n            e.ObjectProjExpr(\n                subject=e.BoundVarExpr(subject_name), label=proj_label\n            )\n        ):\n            if subject_name == bnd_name and proj_label == label:\n                return True\n            else:\n                return False\n        case _:\n            return False\n\n\ndef refine_subject_with_filter(\n    subject: e.Expr, filter: e.EdgeDatabaseSelectFilter\n) -> Optional[e.Expr]:\n    all_labels = eops.collect_names_in_select_filter(filter)\n    match subject:\n        case e.QualifiedName(name):\n            bindings = {}\n            binding_extraction_failed = False\n\n            def extract_expression_bindings(expr: e.Expr) -> Optional[e.Expr]:\n                nonlocal binding_extraction_failed\n                if isinstance(expr, e.EdgeDatabaseSelectFilter):  # type: ignore\n                    return None\n                elif isinstance(expr, e.FreeVarExpr | e.ScalarVal):\n                    return expr\n                elif eops.is_effect_free(expr):\n                    binder_name = e.next_name(\"filter_bnd\")\n                    bindings[binder_name] = expr\n                    return e.FreeVarExpr(binder_name)\n                else:\n                    binding_extraction_failed = True\n                    return None\n\n            after_filter = eops.map_edge_select_filter(\n                extract_expression_bindings, filter\n            )\n            if binding_extraction_failed:\n                return None\n            assert isinstance(after_filter, e.EdgeDatabaseSelectFilter)  # type: ignore\n            return_expr: e.Expr = e.QualifiedNameWithFilter(\n                subject, after_filter  # type: ignore\n            )\n            for bnd_name, bnd_expr in bindings.items():\n                return_expr = e.WithExpr(\n                    bnd_expr, eops.abstract_over_expr(return_expr, bnd_name)\n                )\n            return return_expr\n        case e.MultiSetExpr(expr=[e.QualifiedName(name)]):\n            return refine_subject_with_filter(e.QualifiedName(name), filter)\n        case e.ShapedExprExpr(expr=main, shape=shape):\n            if any(\n                l.label in all_labels\n                for l in shape.shape.keys()\n                if isinstance(l, e.StrLabel)\n                and not is_trivial_shape_element(shape, l.label)\n            ):\n                return None\n            else:\n                expr_refined = refine_subject_with_filter(main, filter)\n                if expr_refined:\n                    return e.ShapedExprExpr(expr=expr_refined, shape=shape)\n                else:\n                    return None\n        case _:\n            return None\n\n\ndef select_optimize(expr: e.Expr) -> e.Expr:\n    def sub_f(sub: e.Expr) -> Optional[e.Expr]:\n        match sub:\n            case e.FilterOrderExpr(\n                subject=subject, filter=filter, order=order\n            ):\n                if order:\n                    return None\n                else:\n                    subject_new = select_optimize(subject)\n                    filter_new = select_optimize(filter)\n                    assert isinstance(filter_new, e.BindingExpr)\n\n                    default_return_expr = e.FilterOrderExpr(\n                        subject=subject_new, filter=filter_new, order=order\n                    )\n                    possible_filter = try_collect_constraints_from_filter(\n                        filter_new\n                    )\n                    if possible_filter:\n                        possible_subject = refine_subject_with_filter(\n                            subject_new, possible_filter\n                        )\n                        if possible_subject:\n                            return e.FilterOrderExpr(\n                                subject=possible_subject,\n                                filter=filter_new,\n                                order=order,\n                            )\n                        else:\n                            return default_return_expr\n                    else:\n                        return default_return_expr\n            case _:\n                return None\n\n    return eops.map_expr(sub_f, expr)\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/post_processing_tools/post_processing.py",
    "content": "from ..data import deduplication_insert\nfrom ..data import data_ops as e\nfrom . import insert_select_optimization\n\n\ndef post_process(expr: e.Expr) -> e.Expr:\n    result = deduplication_insert.insert_conditional_dedup(expr)\n    result = insert_select_optimization.select_optimize(result)\n    return result\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/schema/ddl_processing.py",
    "content": "from ..data import data_ops as e\n\nfrom edb.edgeql import ast as qlast\nfrom edb.common import debug\nfrom .. import elaboration as elab\nfrom ..basis.server_funcs import get_default_func_impl_for_cast\nfrom edb.edgeql import qltypes as qltypes\nfrom .. import elab_schema as elab_schema\nfrom ..type_checking_tools import typechecking as tck\nfrom ..interpreter_logging import print_warning\n\nfrom .function_elaboration import process_builtin_fun_def\n\n\ndef process_ddl(schema: e.DBSchema, ddl: qlast.DDLOperation) -> None:\n    \"\"\"\n    Process a single DDL operation.\n    \"\"\"\n    # debug.dump_edgeql(ddl)\n    match ddl:\n        case qlast.CreateModule(\n            name=qlast.ObjectRef(name=module_name), commands=[]\n        ):\n            schema.modules[(module_name,)] = e.DBModule({})\n        case (\n            qlast.CreatePseudoType()\n            | qlast.CreateAnnotation()\n            | qlast.AlterAnnotation()\n        ):\n            print_warning(\"WARNING: not supported yet\", ddl)\n        case qlast.CreateScalarType(\n            name=qlast.ObjectRef(module=module_name, name=type_name),\n            commands=[],\n            bases=bases,\n            abstract=is_abstract,\n        ):\n            assert (\n                module_name is not None\n            ), \"Scalar types cannot be created in top level\"\n            schema.modules[(module_name,)].defs[type_name] = (\n                e.ModuleEntityTypeDef(\n                    e.ScalarTp(e.QualifiedName([module_name, type_name])),\n                    constraints=[],\n                    is_abstract=is_abstract,\n                    indexes=[],\n                )\n            )\n            # We require DDL to contain fully qualified names\n            schema.subtyping_relations[\n                e.QualifiedName([module_name, type_name])\n            ] = []\n            for base_tp in bases:\n                base_elabed = elab.elab_TypeName(base_tp)\n                match base_elabed:\n                    # for bare ddl, we assume qualified type name\n                    # is actually checked\n                    case e.UncheckedTypeName(name=e.QualifiedName(_)):\n                        assert isinstance(base_elabed.name, e.QualifiedName)\n                        schema.subtyping_relations[\n                            e.QualifiedName([module_name, type_name])\n                        ].append(base_elabed.name)\n                    case e.AnyTp(spec):\n                        # choice: make anytype live in std\n                        schema.subtyping_relations[\n                            e.QualifiedName([module_name, type_name])\n                        ].append(\n                            e.QualifiedName([\"std\", \"any\" + (spec or \"\")])\n                        )\n                    case e.CompositeTp(kind=e.CompositeTpKind.Enum, tps=_):\n                        print_warning(\n                            \"WARNING: behavior of extending\"\n                            \" enum types undefined\",\n                            base_elabed,\n                        )\n                    case _:\n                        raise ValueError(\n                            \"Must inherit from single name\", base_elabed\n                        )\n        case qlast.CreateOperator(\n            kind=_,\n            params=params,\n            name=name,\n            returning=ret_tp,\n            returning_typemod=ret_typemod,\n        ):\n            process_builtin_fun_def(schema, name, params, ret_tp, ret_typemod)\n        case qlast.CreateFunction(\n            commands=commands,\n            params=params,\n            name=name,\n            returning=ret_tp,\n            returning_typemod=ret_typemod,\n        ):\n            process_builtin_fun_def(schema, name, params, ret_tp, ret_typemod)\n\n        case qlast.CreateCast(\n            from_type=from_type,\n            to_type=to_type,\n            commands=commands,\n            allow_implicit=allow_implicit,\n            allow_assignment=allow_assignment,\n            code=cast_code,\n        ):\n            from_tp = elab.elab_TypeName(from_type)\n            to_tp = elab.elab_TypeName(to_type)\n            from_tp_ck = tck.check_type_valid(schema, from_tp)\n            to_tp_ck = tck.check_type_valid(schema, to_tp)\n            assert (\n                from_tp_ck,\n                to_tp_ck,\n            ) not in schema.casts, \"duplicate casts\"\n            match cast_code:\n                case qlast.CastCode(from_expr=from_expr, from_cast=from_cast):\n                    match from_expr, from_cast:\n                        case False, True:\n                            cast_impl = get_default_func_impl_for_cast(\n                                from_tp_ck, to_tp_ck\n                            )\n                        case True, False:\n                            cast_impl = get_default_func_impl_for_cast(\n                                from_tp_ck, to_tp_ck\n                            )\n                        case False, False:\n                            cast_impl = get_default_func_impl_for_cast(\n                                from_tp_ck, to_tp_ck\n                            )\n                        case _:\n                            raise ValueError(\n                                \"TODO\", cast_code, from_tp_ck, to_tp_ck\n                            )\n                case _:\n                    raise ValueError(\"TODO\", cast_code)\n            schema.casts[(from_tp_ck, to_tp_ck)] = e.TpCast(\n                (\n                    e.TpCastKind.Implicit\n                    if allow_implicit\n                    else (\n                        e.TpCastKind.Assignment\n                        if allow_assignment\n                        else e.TpCastKind.Explicit\n                    )\n                ),\n                cast_impl,\n            )  # TODO implicit cast\n        case qlast.CreateConstraint():\n            print_warning(\"WARNING: not supported yet\", ddl)\n        case qlast.CreateProperty():\n            print_warning(\"WARNING: not supported yet\", ddl)\n        case qlast.CreateLink():\n            print_warning(\"WARNING: not supported yet\", ddl)\n\n        case qlast.CreateObjectType(\n            bases=bases,\n            commands=commands,\n            name=qlast.ObjectRef(name=name, module=module_name),\n            abstract=abstract,\n        ):\n            assert (\n                module_name is not None\n            ), \"Object types cannot be created in top level\"\n            obj_tp, constraints, indexes = elab_schema.elab_create_object_tp(\n                commands\n            )\n            elab_schema.add_bases_for_name(schema, (module_name,), name, bases)\n            schema.modules[(module_name,)].defs[name] = e.ModuleEntityTypeDef(\n                obj_tp,\n                is_abstract=abstract,\n                constraints=constraints,\n                indexes=indexes,\n            )\n        case qlast.AlterObjectType():\n            print_warning(\"WARNING: not supported yet\", ddl)\n        case _:\n            debug.dump(ddl)\n            raise ValueError(\"DDL not yet supported\", ddl)\n\n\ndef process_ddls(schema: e.DBSchema, ddls: list[qlast.DDLOperation]) -> None:\n    for ddl in ddls:\n        process_ddl(schema, ddl)\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/schema/function_elaboration.py",
    "content": "from ..data import data_ops as e\nfrom ..data import expr_ops as eops\n\nfrom edb.edgeql import ast as qlast\nfrom .. import elaboration as elab\nfrom ..basis.server_funcs import get_default_func_impl_for_function\nfrom edb.edgeql import qltypes as qltypes\nfrom .. import elab_schema as elab_schema\nfrom ..type_checking_tools import name_resolution as name_res\nfrom typing import Optional\n\n\ndef fun_arg_type_polymorphism_post_processing(tp: e.Tp) -> e.Tp:\n    \"\"\"\n    replace any type with Some(0)\n    TODO: handling anyelem\n    This is how the current polymorphism status quo.\n    \"\"\"\n\n    def replace_any(tp: e.Tp) -> Optional[e.Tp]:\n        match tp:\n            case e.AnyTp(spec):\n                if spec == \"type\":\n                    return e.SomeTp(0)\n                else:\n                    return None\n            case _:\n                return None\n\n    return eops.map_tp(replace_any, tp)\n\n\ndef elaboarate_ret_typemod(ret_typemod: qltypes.TypeModifier) -> e.CMMode:\n    match ret_typemod:\n        case qltypes.TypeModifier.OptionalType:\n            return e.CardAtMostOne\n        case qltypes.TypeModifier.SingletonType:\n            return e.CardOne\n        case qltypes.TypeModifier.SetOfType:\n            return e.CardAny\n        case _:\n            raise ValueError(\"TODO\", ret_typemod)\n\n\ndef elaborate_fun_def_arg_type(\n    params: list[qlast.FuncParamDecl],\n    ret_tp: qlast.TypeExpr,\n    ret_typemod: qltypes.TypeModifier,\n) -> e.FunArgRetType:\n    return_cad = elaboarate_ret_typemod(ret_typemod)\n    return_tp = elab.elab_single_type_expr(ret_tp)\n    return_tp = fun_arg_type_polymorphism_post_processing(return_tp)\n    params_elab = []\n    params_mod_elab = []\n    params_label_elab = []\n    for param in params:\n        match param:\n            case qlast.FuncParamDecl(\n                name=param_name, type=param_type, typemod=modifier\n            ):\n                params_label_elab.append(param_name)\n                param_type_raw = elab.elab_single_type_expr(param_type)\n                param_type_ck = fun_arg_type_polymorphism_post_processing(\n                    param_type_raw\n                )\n                params_elab.append(param_type_ck)\n                params_mod_elab.append(elab.elab_param_modifier(modifier))\n            case _:\n                raise ValueError(\"TODO\", param)\n    return e.FunArgRetType(\n        args_tp=params_elab,\n        args_mod=params_mod_elab,\n        args_label=params_label_elab,\n        ret_tp=e.ResultTp(return_tp, return_cad),\n    )\n\n\ndef process_builtin_fun_def(\n    schema: e.DBSchema,\n    name: qlast.ObjectRef,\n    params: list[qlast.FuncParamDecl],\n    ret_tp: qlast.TypeExpr,\n    ret_typemod: qltypes.TypeModifier,\n) -> None:\n    match name:\n        case qlast.ObjectRef(name=fun_name, module=module_name):\n            assert (\n                module_name is not None\n            ), \"Functions cannot be created in top level\"\n            func_type = elaborate_fun_def_arg_type(params, ret_tp, ret_typemod)\n            func_type = name_res.fun_arg_ret_type_name_resolve(\n                eops.emtpy_tcctx_from_dbschema(schema), func_type\n            )\n            defaults = {\n                p.name: elab.elab(p.default) for p in params if p.default\n            }\n            this_def = e.BuiltinFuncDef(\n                tp=func_type,\n                impl=get_default_func_impl_for_function(\n                    e.QualifiedName([module_name, fun_name])\n                ),\n                defaults=defaults,\n            )\n            if fun_name in schema.modules[(module_name,)].defs:\n                current_def = schema.modules[(module_name,)].defs[fun_name]\n                assert isinstance(current_def, e.ModuleEntityFuncDef)\n                current_def.funcdefs.append(this_def)\n            else:\n                schema.modules[(module_name,)].defs[fun_name] = (\n                    e.ModuleEntityFuncDef([this_def])\n                )\n        case _:\n            raise ValueError(\"TODO\", name)\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/schema/library_discovery.py",
    "content": "from ..data import data_ops as e\nfrom ..helper_funcs import parse_ddl\nfrom .ddl_processing import process_ddls\nimport os\n\n\ndef process_edgeql_file(schema: e.DBSchema, path: str) -> None:\n    \"\"\"\n    Process an edgeql file as ddl.\n    \"\"\"\n\n    with open(path) as f:\n        content = f.read()\n        ddls = parse_ddl(content)\n        process_ddls(schema, ddls)\n\n\ndef add_ddl_library(schema: e.DBSchema, libpaths: list[str]) -> None:\n    \"\"\"\n    Add a library to the schema.\n\n    Given a list of library paths,\n    If library is a edgeql file, process the edgeql file as ddl.\n    If library is a directory,\n    process all edgeql files in the top level of the directory\n    in a lexicographical order.\n    \"\"\"\n\n    for libpath in libpaths:\n        if os.path.isdir(libpath):\n            for filename in sorted(os.listdir(libpath)):\n                if filename.startswith(\"_\"):\n                    continue\n                if filename.endswith(\".edgeql\"):\n                    process_edgeql_file(\n                        schema, os.path.join(libpath, filename)\n                    )\n        elif libpath.endswith(\".edgeql\"):\n            process_edgeql_file(schema, libpath)\n        else:\n            raise ValueError(f\"Invalid library path {libpath}\")\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/schema/subtyping_resolution.py",
    "content": "from __future__ import annotations\nfrom typing import Sequence\nfrom ..data import data_ops as e\n\n\ndef find_all_subtypes_of_tp_in_schema(\n    schema: e.DBSchema, tp: e.QualifiedName\n) -> Sequence[e.QualifiedName]:\n    checked_tps = []\n    frontier = [tp]\n\n    while len(frontier) > 0:\n        next_tp = frontier.pop()\n        if next_tp in checked_tps:\n            continue\n        checked_tps.append(next_tp)\n        frontier.extend(\n            [\n                subtype\n                for subtype in schema.subtyping_relations\n                if next_tp in schema.subtyping_relations[subtype]\n            ]\n        )\n\n    return checked_tps\n\n\ndef find_all_supertypes_of_tp_in_schema(\n    schema: e.DBSchema, tp: e.QualifiedName\n) -> Sequence[e.QualifiedName]:\n    checked_tps = []\n    frontier = [tp]\n\n    while len(frontier) > 0:\n        next_tp = frontier.pop()\n        if next_tp in checked_tps:\n            continue\n        checked_tps.append(next_tp)\n        frontier.extend(schema.subtyping_relations[next_tp])\n\n    return checked_tps\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/sqlite/sqlite_adapter.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any, Sequence, Optional, TYPE_CHECKING\nimport json\n\nfrom dataclasses import dataclass\nfrom ..db_interface import EdgeDatabaseStorageProviderInterface\nfrom .. import db_interface\n\nfrom ..data.data_ops import EdgeID, ObjectVal, Val, MultiSetVal, DBSchema\nfrom ..data import data_ops as e\nfrom ..elab_schema import add_module_from_sdl_defs\n\nif TYPE_CHECKING:\n    import sqlite3\n\n\n# SQLITE_PRINT_QUERIES = True\nSQLITE_PRINT_QUERIES = False\n\n\n@dataclass(frozen=True)\nclass PropertyTypeView:\n    is_primitive: bool\n    is_optional: bool\n    is_singular: bool\n    target_type_name: list[e.QualifiedName]  # a union (choice) of names\n    link_props: dict[\n        str, PropertyTypeView\n    ]  # link props must be empty when is_primitive is True\n\n    def has_lp_table(self) -> bool:\n        if self.link_props:\n            fetch_from_lp_table = True\n        else:\n            if self.is_singular:\n                fetch_from_lp_table = False\n            else:\n                fetch_from_lp_table = True\n        return fetch_from_lp_table\n\n    def get_storage_class(self) -> str:\n        if (\n            self.target_type_name == [e.QualifiedName([\"std\", \"int64\"])]\n            or not self.is_primitive\n        ):\n            return \"INTEGER\"\n        else:\n            return \"TEXT\"\n\n\n@dataclass(frozen=True)\nclass TableTypeView:\n    columns: dict[str, PropertyTypeView]  # property name -> property type\n    indexes: Sequence[Sequence[str]]\n\n\n@dataclass(frozen=True)\nclass ColumnSpec:\n    type: str  # \"INTEGER\" or \"TEXT\"\n    is_nullable: bool\n\n\n@dataclass(frozen=True)\nclass TableSpec:\n    columns: dict[str, ColumnSpec]\n    primary_key: Sequence[str]\n    indexes: Sequence[Sequence[str]]\n\n\ndef get_property_type_view(result_tp: e.ResultTp) -> PropertyTypeView:\n    tp = result_tp.tp\n    mode = result_tp.mode\n    is_optional = mode.lower == e.CardNumZero\n    is_singular = mode.upper == e.CardNumOne\n\n    match tp:\n        case e.NamedNominalLinkTp(name=name, linkprop=link_props):\n            assert isinstance(name, e.QualifiedName)\n            lp_view = {\n                lpname: get_property_type_view(t)\n                for (lpname, t) in link_props.val.items()\n                if not isinstance(t.tp, e.ComputableTp)\n            }\n            return PropertyTypeView(\n                is_primitive=False,\n                is_optional=is_optional,\n                is_singular=is_singular,\n                target_type_name=[name],\n                link_props=lp_view,\n            )\n        case e.ScalarTp(name=name):\n            return PropertyTypeView(\n                is_primitive=True,\n                is_optional=is_optional,\n                is_singular=is_singular,\n                target_type_name=[name],\n                link_props={},\n            )\n        case e.CompositeTp(kind=_, tps=_, labels=_):\n            # This is extremely TODO\n            return PropertyTypeView(\n                is_primitive=True,\n                is_optional=is_optional,\n                is_singular=is_singular,\n                target_type_name=[],\n                link_props={},\n            )\n        case e.DefaultTp(expr=_, tp=inner):\n            return get_property_type_view(e.ResultTp(inner, result_tp.mode))\n        case e.ComputableTp(_):\n            raise ValueError(\"Computable type is not supported yet\")\n        case e.UnionTp(left=l, right=r):\n            l_view = get_property_type_view(e.ResultTp(l, result_tp.mode))\n            r_view = get_property_type_view(e.ResultTp(r, result_tp.mode))\n            assert (\n                l_view.link_props == r_view.link_props\n            ), \"Link props mismatch\"\n            assert (\n                l_view.is_primitive == r_view.is_primitive\n            ), \"Primitive mismatch\"\n            return PropertyTypeView(\n                is_primitive=l_view.is_primitive,\n                is_optional=is_optional,\n                is_singular=is_singular,\n                target_type_name=l_view.target_type_name\n                + r_view.target_type_name,\n                link_props=l_view.link_props,\n            )\n        case _:\n            raise ValueError(f\"Unimplemented type {tp}\")\n\n\ndef get_schema_property_view(schema: e.DBSchema) -> dict[str, TableTypeView]:\n    if (\"default\",) not in schema.modules:\n        raise ValueError(\"Default module not found in schema\")\n    default_module = schema.modules[(\"default\",)]\n    result = {}\n    for name, mdef in default_module.defs.items():\n        match mdef:\n            case e.ModuleEntityTypeDef(\n                typedef=e.ObjectTp(_),\n                is_abstract=False,\n                constraints=_,\n                indexes=indexes,\n            ):\n                if isinstance(mdef.typedef, e.ScalarTp):\n                    raise ValueError(\n                        \"User defined scalar type is not supported yet\"\n                    )\n                type_def_view = TableTypeView(\n                    columns={\n                        pname: get_property_type_view(t)\n                        for (pname, t) in mdef.typedef.val.items()\n                        if not isinstance(t.tp, e.ComputableTp)\n                    },\n                    indexes=indexes,\n                )\n                result[name] = type_def_view\n            case _:\n                pass\n    return result\n\n\ndef get_table_view_from_property_view(\n    schema_property_view: dict[str, TableTypeView]\n) -> dict[str, TableSpec]:\n    result_table = {}\n    for tname, tview in schema_property_view.items():\n        assert tname not in result_table, \"Duplicate table name\"\n        tdef = tview.columns\n\n        all_single_prop_names = sorted(\n            [pname for (pname, pdef) in tdef.items() if pdef.is_singular]\n        )\n\n        # add all links to indexes\n        indexes = list(tview.indexes)\n        for pname, pdef in tdef.items():\n            if pname in all_single_prop_names and not pdef.is_primitive:\n                if [pname] not in indexes:\n                    indexes.append([pname])\n\n        result_table[tname] = TableSpec(\n            columns={\n                \"id\": ColumnSpec(type=\"INTEGER\", is_nullable=False),\n                **{\n                    pname: ColumnSpec(\n                        type=tdef[pname].get_storage_class(),\n                        is_nullable=tdef[pname].is_optional,\n                    )\n                    for pname in all_single_prop_names\n                },\n            },\n            primary_key=[\"id\"],\n            indexes=indexes,\n        )\n\n        for pname, pdef in tdef.items():\n            if pname in all_single_prop_names and len(pdef.link_props) == 0:\n                continue\n\n            tname_pname = f\"{tname}.{pname}\"\n            assert tname_pname not in result_table, \"Duplicate table name\"\n\n            target_type = pdef.get_storage_class()\n            assert \"source\" not in pdef.link_props, \"source is reserved\"\n            assert \"target\" not in pdef.link_props, \"target is reserved\"\n            result_table[tname_pname] = TableSpec(\n                columns={\n                    \"source\": ColumnSpec(type=\"INTEGER\", is_nullable=False),\n                    \"target\": ColumnSpec(type=target_type, is_nullable=False),\n                    **{\n                        lpname: ColumnSpec(\n                            type=pdef.link_props[lpname].get_storage_class(),\n                            is_nullable=pdef.link_props[lpname].is_optional,\n                        )\n                        for lpname in pdef.link_props\n                    },\n                },\n                primary_key=[\"source\", \"target\"],\n                indexes=[[\"target\"], [\"source\", \"target\"]],\n            )\n\n    if (\n        \"objects\" in result_table\n        or \"next_id_to_return_gen\" in result_table\n        or \"sdl_schema\" in result_table\n    ):\n        raise ValueError(\n            \"objects, next_id_to_return_gen, sdl_schema tables are reserved\"\n        )\n    else:\n        result_table[\"objects\"] = TableSpec(\n            columns={\n                \"id\": ColumnSpec(type=\"INTEGER\", is_nullable=False),\n                \"tp\": ColumnSpec(type=\"TEXT\", is_nullable=False),\n            },\n            primary_key=[\"id\"],\n            indexes=[],\n        )\n\n    return result_table\n\n\ndef convert_val_to_sqlite_val(val: e.ResultMultiSetVal) -> Any:\n    def sub_convert(v: e.Val) -> Any:\n        match v:\n            case e.ScalarVal(tp=tp, val=v):\n                match tp:\n                    case e.QualifiedName(name=[\"std\", \"int64\"]):\n                        return v\n                    case _:\n                        return str(v)\n            case e.RefVal(refid=refid, tpname=_, val=_):\n                return refid\n            case e.ArrVal(val=vals):\n                return json.dumps([sub_convert(v) for v in vals])\n            case _:\n                raise ValueError(f\"Unknown value {val}\")\n\n    assert isinstance(val, e.ResultMultiSetVal)\n    if len(val.getVals()) > 1:\n        raise ValueError(\"MultiSetVal is not supported yet\")\n    elif len(val.getVals()) == 0:\n        return None\n    else:\n        return sub_convert(val.getVals()[0])\n\n\nclass SQLiteEdgeDatabaseStorageProvider(EdgeDatabaseStorageProviderInterface):\n\n    def __init__(self, conn: sqlite3.Connection, schema: e.DBSchema) -> None:\n        super().__init__()\n        self.conn = conn\n        self.schema = schema\n        self.cursor = conn.cursor()\n        self.should_commit_to_disk = True\n\n        self.schema_property_view = get_schema_property_view(schema)\n        self.table_view = get_table_view_from_property_view(\n            self.schema_property_view\n        )\n        self.id_initialization()\n        self.create_or_populate_schema_table()\n\n    # result should be fetched using self.cursor.fetchall()\n    def do_execute_query(self, query: str, *args):\n        if SQLITE_PRINT_QUERIES:\n            print(query, *args)\n        self.cursor.execute(query, *args)\n        if SQLITE_PRINT_QUERIES:\n            print(\"[DONE]\", query, *args)\n\n    def get_tp_name(self, tp: e.QualifiedName) -> str:\n        assert (\n            len(tp.names) == 2 and tp.names[0] == \"default\"\n        ), \"Only default module is supported\"\n        return tp.names[1]\n\n    def to_tp_name(self, name: str) -> e.QualifiedName:\n        return e.QualifiedName([\"default\", name])\n\n    def get_type_for_an_id(self, id: EdgeID) -> e.QualifiedName:\n        self.do_execute_query(\"SELECT tp FROM objects WHERE id=?\", (id,))\n        tp_row = self.cursor.fetchone()\n        if tp_row is None:\n            raise ValueError(f\"ID {id} not found in database\")\n        else:\n            return e.QualifiedName(tp_row[0].split(\"::\"))\n\n    def to_type_for_an_id(self, tp: e.QualifiedName) -> str:\n        return \"::\".join(tp.names)\n\n    def convert_sqlite_link_props_to_object_val(\n        self,\n        link_props: dict[str, Any],\n        link_props_view: dict[str, PropertyTypeView],\n    ) -> ObjectVal:\n        assert (\n            link_props.keys() == link_props_view.keys()\n        ), \"Link props mismatch\"\n        return ObjectVal(\n            {\n                e.LinkPropLabel(lpname): (\n                    e.Invisible(),\n                    (\n                        e.ResultMultiSetVal(\n                            [\n                                self.convert_sqlite_result_to_val(\n                                    link_props[lpname],\n                                    link_props_view[lpname],\n                                    {},\n                                )\n                            ]\n                        )\n                        if link_props[lpname]\n                        else e.ResultMultiSetVal([])\n                    ),\n                )\n                for lpname in link_props\n            }\n        )\n\n    def convert_sqlite_result_to_val(\n        self,\n        result_data: Any,\n        result_tp: PropertyTypeView,\n        link_props: dict[str, Any],\n    ) -> Val:\n        if result_data is None:\n            raise ValueError(\"Unexpected sqlite value None (Internal Error)\")\n\n        if len(link_props) > 0:\n\n            if len(result_tp.target_type_name) == 1:\n                tp_name = result_tp.target_type_name[0]\n            else:\n                tp_name = self.get_type_for_an_id(result_data)\n            converted_link_props = (\n                self.convert_sqlite_link_props_to_object_val(\n                    link_props, result_tp.link_props\n                )\n            )\n            return e.RefVal(\n                refid=result_data, tpname=tp_name, val=converted_link_props\n            )\n        else:\n            if result_tp.is_primitive:\n                assert len(result_tp.target_type_name) == 1\n                tp_name = result_tp.target_type_name[0]\n                return e.ScalarVal(\n                    tp=e.ScalarTp(name=tp_name), val=result_data\n                )\n            else:\n                if len(result_tp.target_type_name) == 1:\n                    tp_name = result_tp.target_type_name[0]\n                else:\n                    tp_name = self.get_type_for_an_id(result_data)\n                return e.RefVal(\n                    refid=result_data, tpname=tp_name, val=e.ObjectVal({})\n                )\n\n    def create_or_populate_schema_table(self) -> None:\n\n        for tname, tspec in self.table_view.items():\n            column_spec = ', '.join(\n                [\n                    f\"{cname} {cspec.type}\"\n                    + (\"\" if cspec.is_nullable else \" NOT NULL\")\n                    for (cname, cspec) in tspec.columns.items()\n                ]\n            )\n            primary_key_spec = f\"PRIMARY KEY ({','.join(tspec.primary_key)})\"\n\n            self.do_execute_query(\n                f\"CREATE TABLE IF NOT EXISTS '{tname}'\"\n                + f\" ({column_spec}, {primary_key_spec}) STRICT, WITHOUT ROWID\"\n            )\n\n            for index in tspec.indexes:\n                index_name = f\"{tname}_{'_'.join(index)}_idx\"\n                index_spec = ','.join(index)\n                self.do_execute_query(\n                    f\"CREATE INDEX IF NOT EXISTS '{index_name}'\"\n                    + f\" ON '{tname}' ({index_spec})\"\n                )\n\n    def id_initialization(self):\n        self.do_execute_query(\n            \"CREATE TABLE IF NOT EXISTS\"\n            \" next_id_to_return_gen (id INTEGER PRIMARY KEY)\"\n        )\n        self.do_execute_query(\"SELECT id FROM next_id_to_return_gen LIMIT 1\")\n        next_id_row = self.cursor.fetchone()\n        if next_id_row is not None:\n            self.next_id_to_return = next_id_row[0]\n        else:\n            self.do_execute_query(\n                \"INSERT INTO next_id_to_return_gen (id) VALUES (?)\", (101,)\n            )\n        self.conn.commit()  # I am not sure whether this is needed\n\n    def get_schema(self) -> DBSchema:\n        return self.schema\n\n    def query_ids_for_a_type(\n        self, tp: e.QualifiedName, filters: e.EdgeDatabaseSelectFilter\n    ) -> list[EdgeID]:\n\n        query_args = []\n\n        def convert_select_filter_to_condition_text(\n            filter: e.EdgeDatabaseSelectFilter,\n        ) -> str:\n            match filter:\n                case e.EdgeDatabaseEqFilter(propname=propname, arg=arg):\n                    if not isinstance(arg, e.MultiSetVal):\n                        raise ValueError(\n                            \"Only MultiSetVal is supported,\"\n                            \" check evaluation implementation\"\n                        )\n                    if len(arg.getVals()) != 1:\n                        equivalent_disjuctive_filter = (\n                            e.EdgeDatabaseDisjunctiveFilter(\n                                disjuncts=[\n                                    e.EdgeDatabaseEqFilter(\n                                        propname=propname,\n                                        arg=e.ResultMultiSetVal([arg2]),\n                                    )\n                                    for arg2 in arg.getVals()\n                                ]\n                            )\n                        )\n                        return convert_select_filter_to_condition_text(\n                            equivalent_disjuctive_filter\n                        )\n                    else:\n                        query_args.append(convert_val_to_sqlite_val(arg))\n                        if propname == \"id\":\n                            return \"(id = ?)\"\n                        else:\n                            this_view = self.schema_property_view[\n                                tp_name\n                            ].columns[propname]\n                            if this_view.is_singular:\n                                return f\"({propname} = ?)\"\n                            else:\n                                return (\n                                    f\"(EXISTS (SELECT 1 FROM '\"\n                                    + f\"{tp_name}.{propname}\"\n                                    + \"' WHERE source = id AND target = ?))\"\n                                )\n                case e.EdgeDatabaseConjunctiveFilter(conjuncts=filters):\n                    if len(filters) == 0:\n                        return \"(1=1)\"\n                    return (\n                        \"(\"\n                        + \" AND \".join(\n                            [\n                                convert_select_filter_to_condition_text(f)\n                                for f in filters\n                            ]\n                        )\n                        + \")\"\n                    )\n                case e.EdgeDatabaseDisjunctiveFilter(disjuncts=filters):\n                    if len(filters) == 0:\n                        return \"(1=0)\"\n                    return (\n                        \"(\"\n                        + \" OR \".join(\n                            [\n                                convert_select_filter_to_condition_text(f)\n                                for f in filters\n                            ]\n                        )\n                        + \")\"\n                    )\n                case e.EdgeDatabaseTrueFilter():\n                    return \"(1=1)\"\n                case _:\n                    raise ValueError(f\"Unknown filter {filter}\")\n\n        tp_name = self.get_tp_name(tp)\n        filter_clause = \"WHERE \" + convert_select_filter_to_condition_text(\n            filters\n        )\n\n        sql_query = f\"\"\"SELECT id FROM \"{tp_name}\" {filter_clause} \"\"\"\n        self.do_execute_query(sql_query, (*query_args,))\n        return [row[0] for row in self.cursor.fetchall()]\n\n    def dump_state(self) -> object:\n        self.conn.commit()\n\n        dump_script = '\\n'.join(self.conn.iterdump())\n        return {\n            \"dump\": dump_script,\n        }\n\n    def restore_state(self, dumped_state) -> None:\n\n        # Drop all tables\n        self.do_execute_query(\n            \"SELECT name FROM sqlite_master WHERE type='table'\"\n        )\n        tables = self.cursor.fetchall()\n        drop_statements = [\n            f\"DROP TABLE IF EXISTS \\\"{table[0]}\\\";\" for table in tables\n        ]\n        drop_script = '\\n'.join(drop_statements)\n\n        # Execute the drop script and the dump script\n        self.conn.executescript(drop_script + '\\n' + dumped_state[\"dump\"])\n\n    def next_id(self) -> EdgeID:\n        # XXX: This is not thread safe\n        self.do_execute_query(\"SELECT id FROM next_id_to_return_gen LIMIT 1\")\n        id_row = self.cursor.fetchone()\n        if id_row is None:\n            raise ValueError(\"Cannot fetch next id, check initialization\")\n        id = id_row[0]\n        self.do_execute_query(\"UPDATE next_id_to_return_gen SET id = id + 1\")\n        return id\n\n    def project(\n        self, id: EdgeID, tp: e.QualifiedName, prop: str\n    ) -> MultiSetVal:\n        tp_name = self.get_tp_name(tp)\n        pview = self.schema_property_view[tp_name].columns[prop]\n\n        fetch_from_lp_table = pview.has_lp_table()\n\n        if fetch_from_lp_table:\n            lp_property_names = list(pview.link_props.keys())\n            lp_table_name = f\"{tp_name}.{prop}\"\n            if len(lp_property_names) > 0:\n                query = (\n                    f\"SELECT target, {','.join(lp_property_names)}\"\n                    + f\" FROM '{lp_table_name}' WHERE source=?\"\n                )\n            else:\n                query = f\"SELECT target FROM '{lp_table_name}' WHERE source=?\"\n            self.do_execute_query(query, (id,))\n            result = []\n            for row in self.cursor.fetchall():\n                target_id = row[0]\n                lp_vals = {}\n                for i, lp_prop_name in enumerate(lp_property_names):\n                    lp_vals[lp_prop_name] = row[i + 1]\n                result.append(\n                    self.convert_sqlite_result_to_val(\n                        target_id, pview, lp_vals\n                    )\n                )\n            return e.ResultMultiSetVal(result)\n        else:\n            query = f\"SELECT {prop} FROM {tp_name} WHERE id=?\"\n            self.do_execute_query(query, (id,))\n            result = []\n            for row in self.cursor.fetchall():\n                if row[0] is not None:\n                    result.append(\n                        self.convert_sqlite_result_to_val(row[0], pview, {})\n                    )\n            return e.ResultMultiSetVal(result)\n\n    def reverse_project(\n        self, subject_ids: Sequence[EdgeID], prop: str\n    ) -> MultiSetVal:\n        result: list[Val] = []\n        for tp_name, tdef in self.schema_property_view.items():\n            for prop_name, pview in tdef.columns.items():\n                if prop_name == prop:\n                    if pview.has_lp_table():\n                        lp_property_names = list(pview.link_props.keys())\n                        lp_table_name = f\"{tp_name}.{prop}\"\n                        if len(lp_property_names) > 0:\n                            query = (\n                                \"SELECT source, \"\n                                + ','.join(lp_property_names)\n                                + \" FROM '\"\n                                + lp_table_name\n                                + \"' WHERE target IN (\"\n                                + ','.join(['?'] * len(subject_ids))\n                                + \")\"\n                            )\n                        else:\n                            query = (\n                                f\"SELECT source FROM '{lp_table_name}'\"\n                                + \" WHERE target IN \"\n                                + f\"({','.join(['?'] * len(subject_ids))})\"\n                            )\n                    else:\n                        lp_property_names = []\n                        query = (\n                            f\"SELECT id FROM '{tp_name}' WHERE {prop} IN \"\n                            + f\"({','.join(['?'] * len(subject_ids))})\"\n                        )\n                    self.do_execute_query(\n                        query, [int(id) for id in subject_ids]\n                    )\n                    for row in self.cursor.fetchall():\n                        source_id = row[0]\n                        lp_vals = {}\n                        for i, lp_prop_name in enumerate(lp_property_names):\n                            lp_vals[lp_prop_name] = row[i + 1]\n                        converted_lp_vals = (\n                            self.convert_sqlite_link_props_to_object_val(\n                                lp_vals, pview.link_props\n                            )\n                        )\n                        result.append(\n                            e.RefVal(\n                                refid=source_id,\n                                tpname=self.to_tp_name(tp_name),\n                                val=converted_lp_vals,\n                            )\n                        )\n\n        return e.ResultMultiSetVal(result)\n\n    def insert(\n        self, id: EdgeID, tp: e.QualifiedName, props: dict[str, MultiSetVal]\n    ) -> None:\n        tp_name = self.get_tp_name(tp)\n        tdef = self.schema_property_view[tp_name].columns\n\n        single_props = [\n            pname for (pname, pview) in tdef.items() if pview.is_singular\n        ]\n        single_prop_vals = [\n            convert_val_to_sqlite_val(props[pname]) for pname in single_props\n        ]\n        self.do_execute_query(\n            f\"INSERT INTO {tp_name} (id, {','.join(single_props)})\"\n            + f\" VALUES (?, {','.join(['?'] * len(single_props))})\",\n            (id, *single_prop_vals),\n        )\n\n        for pname, pview in tdef.items():\n            if pview.has_lp_table():\n                lp_table_name = f\"{tp_name}.{pname}\"\n                lp_property_names = list(pview.link_props.keys())\n                for v in props[pname].getVals():\n                    if len(lp_property_names) > 0:\n                        assert isinstance(v, e.RefVal)\n                        lp_props = [\n                            convert_val_to_sqlite_val(\n                                v.val.val[e.LinkPropLabel(lp_prop_name)][1]\n                            )\n                            for lp_prop_name in lp_property_names\n                        ]\n                        self.do_execute_query(\n                            \"INSERT INTO '\"\n                            + lp_table_name\n                            + \"' (source, target, \"\n                            + ','.join(lp_property_names)\n                            + f\") VALUES (?, ?, \"\n                            + ','.join(['?'] * len(lp_property_names))\n                            + \")\",\n                            (\n                                id,\n                                convert_val_to_sqlite_val(\n                                    e.ResultMultiSetVal([v])\n                                ),\n                                *lp_props,\n                            ),\n                        )\n                    else:\n                        self.do_execute_query(\n                            f\"INSERT INTO '{lp_table_name}'\"\n                            + \" (source, target) VALUES (?, ?)\",\n                            (\n                                id,\n                                convert_val_to_sqlite_val(\n                                    e.ResultMultiSetVal([v])\n                                ),\n                            ),\n                        )\n\n        self.do_execute_query(\n            f\"INSERT INTO objects (id, tp) VALUES (?, ?)\",\n            (id, self.to_type_for_an_id(tp)),\n        )\n\n    def delete(self, id: EdgeID, tp: e.QualifiedName) -> None:\n        tp_name = self.get_tp_name(tp)\n\n        for pname, pview in self.schema_property_view[tp_name].columns.items():\n            if pview.has_lp_table():\n                lp_table_name = f\"{tp_name}.{pname}\"\n                self.do_execute_query(\n                    f\"DELETE FROM '{lp_table_name}' WHERE source=?\", (id,)\n                )\n\n        self.do_execute_query(f\"DELETE FROM '{tp_name}' WHERE id=?\", (id,))\n        self.do_execute_query(f\"DELETE FROM objects WHERE id=?\", (id,))\n\n    def update(\n        self, id: EdgeID, tp: e.QualifiedName, props: dict[str, MultiSetVal]\n    ) -> None:\n        tp_name = self.get_tp_name(tp)\n        tdef = self.schema_property_view[tp_name]\n\n        single_props = [\n            pname\n            for (pname, pview) in tdef.columns.items()\n            if pview.is_singular\n        ]\n        single_prop_vals = [\n            convert_val_to_sqlite_val(props[pname])\n            for pname in single_props\n            if pname in props\n        ]\n        if len(single_prop_vals) > 0:\n            self.do_execute_query(\n                f\"UPDATE '{tp_name}' SET \"\n                f\"{','.join([f'{pname}=?' for pname in props])} WHERE id=?\",\n                (*single_prop_vals, id),\n            )\n\n        for pname, pview in tdef.columns.items():\n            if pview.has_lp_table() and pname in props:\n                lp_table_name = f\"{tp_name}.{pname}\"\n                lp_property_names = list(pview.link_props.keys())\n                # Delete all existing links\n                self.do_execute_query(\n                    f\"DELETE FROM '{lp_table_name}' WHERE source=?\", (id,)\n                )\n                for v in props[pname].getVals():\n                    assert isinstance(v, e.RefVal)\n                    if len(lp_property_names) > 0:\n                        lp_props = [\n                            convert_val_to_sqlite_val(\n                                v.val.val[e.LinkPropLabel(lp_prop_name)][1]\n                            )\n                            for lp_prop_name in lp_property_names\n                        ]\n                        self.do_execute_query(\n                            \"INSERT INTO '\"\n                            + lp_table_name\n                            + \"' (source, target, \"\n                            + ','.join(lp_property_names)\n                            + \") VALUES (?, ?, \"\n                            + ','.join(['?'] * len(lp_property_names))\n                            + \")\",\n                            (\n                                id,\n                                convert_val_to_sqlite_val(\n                                    e.ResultMultiSetVal([v])\n                                ),\n                                *lp_props,\n                            ),\n                        )\n                    else:\n                        self.do_execute_query(\n                            f\"INSERT INTO '{lp_table_name}'\"\n                            \" (source, target) VALUES (?, ?)\",\n                            (\n                                id,\n                                convert_val_to_sqlite_val(\n                                    e.ResultMultiSetVal([v])\n                                ),\n                            ),\n                        )\n\n    # By default commit is called per query,\n    # doing a bunch of consecutive queries\n    # will cause significant delays.\n    # This function can be used to pause the commit.\n    def pause_disk_commit(self) -> None:\n        self.should_commit_to_disk = False\n\n    def resume_disk_commit(self) -> None:\n        self.should_commit_to_disk = True\n        self.commit()\n\n    def commit(self) -> None:\n        if self.should_commit_to_disk:\n            self.conn.commit()\n\n\ndef schema_and_db_from_sqlite(\n    sdl_file_content: Optional[str], sqlite_file_name: str\n):\n    import sqlite3\n\n    # Connect to the SQLite database\n    conn = sqlite3.connect(sqlite_file_name)\n    c = conn.cursor()\n\n    # Check if sdl_schema table exists\n    c.execute(\n        \"SELECT name FROM sqlite_master \"\n        \"WHERE type='table' AND name='sdl_schema'\"\n    )\n    table_exists = c.fetchone()\n    db_sdl = None\n    if table_exists:\n        # Retrieve the pickled object from the table\n        c.execute(\"SELECT content FROM sdl_schema LIMIT 1\")\n        db_sdl_row = c.fetchone()\n        if db_sdl_row:\n            db_sdl = db_sdl_row[0]\n    else:\n        # Create sdl_schema table\n        c.execute(\"CREATE TABLE sdl_schema (content TEXT)\")\n\n    from ..new_interpreter import default_dbschema\n\n    dbschema = default_dbschema()\n    if db_sdl is None:\n\n        if sdl_file_content is None:\n            content = \"\"  # empty schema\n        else:\n            content = sdl_file_content\n\n        # Read and store the content into sdl_schema table\n        c.execute(\"INSERT INTO sdl_schema (content) VALUES (?)\", (content,))\n        dbschema = add_module_from_sdl_defs(dbschema, content)\n\n        # Commit the changes\n        conn.commit()\n    else:\n\n        sdl_content = sdl_file_content\n        if sdl_content is None:\n            sdl_content = \"\"\n\n        # Compare content and abort if they differ\n        if db_sdl != sdl_content:\n            raise ValueError(\n                \"Passed in SDL file differs from SQLite Schema.\",\n                sdl_content,\n                db_sdl,\n            )\n\n        dbschema = add_module_from_sdl_defs(dbschema, db_sdl)\n\n    storage = SQLiteEdgeDatabaseStorageProvider(conn, dbschema)\n    db = db_interface.EdgeDatabase(storage)\n    return dbschema, db\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/type_checking_tools/cast_checking.py",
    "content": "from typing import Optional\n\nfrom ..data import data_ops as e\nfrom ..data import casts as casts\nfrom ..data import path_factor as path_factor\nfrom ..basis import server_funcs as server_funcs\n\n\ndef check_castable(\n    ctx: e.TcCtx, from_tp: e.Tp, to_tp: e.Tp\n) -> Optional[e.TpCast]:\n    if to_tp == e.ScalarTp(e.QualifiedName([\"std\", \"json\"])):\n        return casts.get_json_cast(from_tp, ctx.schema)\n    if (from_tp, to_tp) in ctx.schema.casts:\n        return ctx.schema.casts[(from_tp, to_tp)]\n    else:\n        match from_tp:\n            case e.ScalarTp(name=name):\n                for supertype in ctx.schema.subtyping_relations[name]:\n                    candidate = check_castable(\n                        ctx, e.ScalarTp(supertype), to_tp\n                    )\n                    if candidate is not None:\n                        return candidate\n                else:\n                    return None\n            case _:\n                return None\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/type_checking_tools/dml_checking.py",
    "content": "from ..data import data_ops as e\nfrom ..data import expr_ops as eops\nfrom ..data import type_ops as tops\nfrom ..data import path_factor as pops\nfrom ..data import module_ops as mops\nfrom ..data import expr_to_str as pp\n\n\ndef get_key_dependency(tp: e.DefaultTp) -> list[str]:\n    new_head_name = eops.next_name(\"head_name_test\")\n    instantiaed = eops.instantiate_expr(e.FreeVarExpr(new_head_name), tp.expr)\n    paths = pops.get_all_paths(instantiaed)\n    deps = []\n    for p in paths:\n        if eops.get_path_head(p) == new_head_name:\n            first_path_comp = eops.get_first_path_component(p)\n            match first_path_comp:\n                case e.FreeVarExpr(_):\n                    raise ValueError(\"Path head is FreeVar, cannot do insert\")\n                case e.ObjectProjExpr(subject=_, label=lbl):\n                    deps.append(lbl)\n                case e.LinkPropProjExpr(_, _):\n                    raise ValueError(\n                        \"default expr should not do link prop proj\"\n                    )\n                case e.BackLinkExpr(_) | e.TpIntersectExpr(_):\n                    continue  # this is fine\n    return deps\n\n\ndef type_elaborate_default_tp(ctx: e.TcCtx, default_expr: e.Expr) -> e.Expr:\n    from .typechecking import synthesize_type\n\n    elabed = synthesize_type(ctx, default_expr)[1]\n    return elabed\n\n\ndef insert_link_prop_checking(\n    ctx: e.TcCtx,\n    expr_ck: e.Expr,\n    synth_lp: dict[str, e.ResultTp],\n    ck_lp: dict[str, e.ResultTp],\n) -> e.Expr:\n    \"\"\"\n    Check link properties, return additional link props\n    that need to be inserted (default fields)\n    \"\"\"\n    # we do not support heterogenous link targets\n    for k in synth_lp.keys():\n        this_tp = ck_lp[k].tp\n        if k not in ck_lp.keys():\n            raise ValueError(\"Link prop not in type\", k, ck_lp.keys())\n        elif isinstance(synth_lp[k].tp, e.ComputableTp):\n            raise ValueError(\"Cannot define computable tp\")\n        elif isinstance(this_tp, e.DefaultTp):\n            tops.assert_real_subtype(ctx, synth_lp[k].tp, this_tp.tp)\n        else:\n            tops.assert_real_subtype(ctx, synth_lp[k].tp, ck_lp[k].tp)\n\n    additional_lps: dict[str, e.Expr] = {}\n\n    for k in ck_lp.keys():\n        this_tp = ck_lp[k].tp\n        if k in synth_lp.keys():\n            continue\n        elif isinstance(ck_lp[k].tp, e.ComputableTp):\n            continue\n        elif isinstance(this_tp, e.DefaultTp):\n            default_expr = this_tp.expr\n            if eops.binding_is_unnamed(default_expr):\n                additional_lps[k] = type_elaborate_default_tp(\n                    ctx,\n                    eops.instantiate_expr(\n                        e.FreeVarExpr(\"LP_DEFAULT_SHOULD_NOT_OCCUR\"),\n                        default_expr,\n                    ),\n                )\n            else:\n                raise ValueError(\"Default Tp is not unnamed\", this_tp.expr)\n        elif tops.mode_is_optional(ck_lp[k].mode):\n            additional_lps[k] = e.MultiSetExpr(expr=[])\n        else:\n            raise ValueError(\n                \"Missing link prop\",\n                k,\n                \"synthesized keys\",\n                synth_lp.keys(),\n                \"required keys\",\n                ck_lp.keys(),\n            )\n\n    if len(additional_lps.keys()) == 0:\n        return expr_ck\n    else:\n        return e.ShapedExprExpr(\n            expr_ck,\n            e.ShapeExpr(\n                shape={\n                    e.LinkPropLabel(k): eops.abstract_over_expr(v)\n                    for (k, v) in additional_lps.items()\n                }\n            ),\n        )\n\n\ndef insert_dynamic_cardinality_check(\n    ctx: e.TcCtx, attr_expr: e.Expr, target_mode: e.CMMode\n) -> e.Expr:\n    # TODO: after constraint system, this should be checked in the compile time\n    if target_mode.upper == e.OneCardinal():\n        attr_expr = e.FunAppExpr(\n            e.QualifiedName([\"std\", \"assert_single\"]),\n            None,\n            [attr_expr, e.StrVal(\"Single links turn our to be multiples\")],\n            {},\n        )\n    # this is a hack that insertions on link targets ignores\n    # required at compile time\n    if target_mode.lower == e.OneCardinal():\n        attr_expr = e.FunAppExpr(\n            e.QualifiedName([\"std\", \"assert_exists\"]),\n            None,\n            [attr_expr, e.StrVal(\"Required links turn our to be empty\")],\n            {},\n        )\n    return attr_expr\n\n\ndef insert_proprerty_checking(\n    ctx: e.TcCtx, attr_expr: e.Expr, attr_tp: e.ResultTp\n) -> e.Expr:\n    # for breaking circular dependency\n    from .typechecking import synthesize_type, check_type\n\n    target_tp = attr_tp.tp\n    target_mode = attr_tp.mode\n    if tops.tp_is_primitive(target_tp):\n        return check_type(\n            ctx, attr_expr, attr_tp, with_assignment_cast=True\n        )  # allow assignment cast for inserts\n    else:\n        match target_tp:\n            case e.CompositeTp(_):\n                return check_type(\n                    ctx, attr_expr, attr_tp, with_assignment_cast=True\n                )  # allow assignment cast for inserts\n            case e.DefaultTp(expr=_, tp=tp):\n                # since we're inserting an actual value, default is overridden\n                return insert_proprerty_checking(\n                    ctx, attr_expr, e.ResultTp(tp, target_mode)\n                )\n            case e.NamedNominalLinkTp(name=target_name, linkprop=lp):\n                attr_expr = insert_dynamic_cardinality_check(\n                    ctx, attr_expr, target_mode\n                )\n                (synthesized_tp, expr_ck) = synthesize_type(ctx, attr_expr)\n                tops.assert_cardinal_subtype(synthesized_tp.mode, target_mode)\n                ck_lp = lp.val\n\n                # get the synthesized link props\n                match synthesized_tp.tp:\n                    case e.NamedNominalLinkTp(name=synth_name, linkprop=lp2):\n                        if target_name != synth_name:\n                            raise ValueError(\n                                \"Unmatched target and synth name\",\n                                target_name,\n                                synth_name,\n                            )\n                        synth_lp = lp2.val\n                    case e.NominalLinkTp(\n                        subject=_, name=synth_name, linkprop=lp2\n                    ):\n                        if target_name != synth_name:\n                            raise ValueError(\n                                \"Unmatched target and synth name\",\n                                target_name,\n                                synth_name,\n                            )\n                        synth_lp = lp2.val\n                    case _:\n                        raise ValueError(\n                            \"Unrecognized synthesized type\", synthesized_tp\n                        )\n\n                return insert_link_prop_checking(ctx, expr_ck, synth_lp, ck_lp)\n\n            case e.UnionTp(_, _):\n                all_target_tps = tops.collect_tp_union(target_tp)\n                if any(tops.tp_is_primitive(tp) for tp in all_target_tps):\n                    assert all(\n                        tops.tp_is_primitive(tp) for tp in all_target_tps\n                    ), \"Union type should be all primitive\"\n                    raise ValueError(\"TODO\")\n                if not all(\n                    isinstance(tp, e.NamedNominalLinkTp)\n                    for tp in all_target_tps\n                ):\n                    raise ValueError(\"TODO\")\n                all_target_names: dict[e.QualifiedName, e.Tp] = {\n                    tp.name: tp for tp in all_target_tps  # type: ignore\n                }\n                # synthesize once to get the type\n                (synthesized_tp, expr_ck) = synthesize_type(ctx, attr_expr)\n                if isinstance(\n                    synthesized_tp.tp, e.NamedNominalLinkTp | e.NominalLinkTp\n                ):\n                    synthesized_name = synthesized_tp.tp.name\n                    if synthesized_name not in all_target_names:\n                        raise ValueError(\n                            \"Synthesized name not in union types\",\n                            synthesized_name,\n                            all_target_names,\n                        )\n                    assert isinstance(synthesized_name, e.QualifiedName)\n                    return insert_proprerty_checking(\n                        ctx,\n                        attr_expr,\n                        e.ResultTp(\n                            all_target_names[synthesized_name], target_mode\n                        ),\n                    )\n                elif isinstance(synthesized_tp.tp, e.UnionTp):\n                    all_synthesized_tps = tops.collect_tp_union(\n                        synthesized_tp.tp\n                    )\n                    assert all(\n                        isinstance(tp, e.NamedNominalLinkTp | e.NominalLinkTp)\n                        for tp in all_synthesized_tps\n                    )\n\n                    all_synthesized_names = {\n                        tp.name: tp  # type: ignore\n                        for tp in all_synthesized_tps\n                    }\n\n                    if not all(\n                        any(\n                            tops.is_nominal_subtype_in_schema(\n                                ctx, synth_name, ck_name  # type: ignore\n                            )\n                            for ck_name in all_target_names.keys()\n                        )\n                        for synth_name in all_synthesized_names.keys()\n                    ):\n                        raise ValueError(\n                            \"Synthesized names not in union types\",\n                            all_synthesized_names.keys(),\n                            all_target_names.keys(),\n                        )\n\n                    assert isinstance(\n                        all_synthesized_tps[0],\n                        e.NamedNominalLinkTp | e.NominalLinkTp,\n                    )\n                    synth_lp = all_synthesized_tps[0].linkprop.val\n                    assert all(\n                        synth_lp == tp.linkprop.val  # type: ignore\n                        for tp in all_synthesized_tps\n                    )\n                    assert isinstance(\n                        all_target_tps[0],\n                        e.NamedNominalLinkTp | e.NominalLinkTp,\n                    )\n                    ck_lp = all_target_tps[0].linkprop.val\n                    assert all(\n                        ck_lp == tp.linkprop.val  # type: ignore\n                        for tp in all_target_tps\n                    )\n                    return insert_link_prop_checking(\n                        ctx, expr_ck, synth_lp, ck_lp\n                    )\n                else:\n                    raise ValueError(\n                        \"TODO\", pp.show(synthesized_tp.tp), pp.show(attr_expr)\n                    )\n            case _:\n                raise ValueError(\n                    \"Unrecognized Attribute Target Type\", pp.show(target_tp)\n                )\n\n\ndef insert_checking(ctx: e.TcCtx, expr: e.InsertExpr) -> e.Expr:\n    # for breaking circular dependency\n    from .typechecking import synthesize_type\n\n    insert_tp_name, schema_tp = mops.resolve_raw_name_and_type_def(\n        ctx, expr.name\n    )\n    assert isinstance(schema_tp, e.ObjectTp), \"Cannot insert into Scalar Types\"\n    new_v: dict[str, e.Expr] = {}\n    for k, v in expr.new.items():\n        if k not in schema_tp.val:\n            raise ValueError(f\"Key {k} not in schema for {expr.name}\")\n        target_tp = schema_tp.val[k]\n        if isinstance(target_tp, e.ComputableTp):\n            raise ValueError(\n                f\"Key {k} is computable in {expr.name},\"\n                \" modification of computable types prohibited\"\n            )\n        vv = insert_proprerty_checking(ctx, v, schema_tp.val[k])\n        new_v = {**new_v, k: vv}\n\n    # add optional fields that do not have a default\n    for k, target_tp in schema_tp.val.items():\n        if (\n            tops.mode_is_optional(target_tp.mode)\n            and not isinstance(target_tp.tp, e.DefaultTp)\n            and not isinstance(target_tp.tp, e.ComputableTp)\n            and k not in new_v\n        ):\n            new_v = {**new_v, k: e.MultiSetExpr(expr=[])}\n\n    # check non-optional fields\n    missing_keys = []\n    for k, target_tp in schema_tp.val.items():\n        if isinstance(target_tp.tp, e.ComputableTp):\n            continue\n        if isinstance(target_tp.tp, e.DefaultTp):\n            continue\n        if k not in new_v:\n            missing_keys.append(k)\n    if len(missing_keys) > 0:\n        raise ValueError(\n            f\"Missing keys {missing_keys} in insert for {expr.name}\"\n        )\n\n    # a list of keys that are dependent,\n    # need to extract them in this order,\n    # second element provides the binder name\n    dependent_keys: dict[str, str] = {}\n\n    def add_deps_from_new_v(deps: list[str]) -> None:\n        for k in deps:\n            if k in new_v and k not in dependent_keys:\n                dependent_keys[k] = eops.next_name(f\"insert_{expr.name}_{k}\")\n\n    def get_shaped_from_deps(deps: list[str]) -> e.ShapedExprExpr:\n        return e.ShapedExprExpr(\n            expr=e.FreeObjectExpr(),\n            shape=e.ShapeExpr(\n                shape={\n                    e.StrLabel(k): eops.abstract_over_expr(\n                        e.FreeVarExpr(dependent_keys[k])\n                    )\n                    for k in deps\n                }\n            ),\n        )\n\n    pending_default: dict[str, list[str]] = {}  # key and its dependent keys\n    # topologically sort the default insertions.\n    for k, target_tp in schema_tp.val.items():\n        if isinstance(target_tp.tp, e.DefaultTp):\n            deps = get_key_dependency(target_tp.tp)\n            if len(deps) == 0:\n                actual_v = eops.instantiate_expr(\n                    e.FreeVarExpr(\"INSERT_SHOULD_NOT_OCCUR\"),\n                    target_tp.tp.expr,\n                )\n                new_v = {**new_v, k: type_elaborate_default_tp(ctx, actual_v)}\n            else:\n                # add to dependent_keys those in new_v\n                add_deps_from_new_v(deps)\n                # if all dependencies are currently in add new_v\n                if all(k in dependent_keys.keys() for k in deps):\n                    actual_v = e.WithExpr(\n                        get_shaped_from_deps(deps), target_tp.tp.expr\n                    )\n                    new_v = {\n                        **new_v,\n                        k: actual_v,\n                    }  # TODO THIS NEEDS ELABORATION\n                else:\n                    assert (\n                        k not in pending_default\n                    ), \"only iterating over schema once, no duplicate keys\"\n                    pending_default[k] = deps\n\n    # process pending keys until all are resolved\n    while len(pending_default) > 0:\n        # find a key that has all dependencies resolved\n        for k, deps in pending_default.items():\n            add_deps_from_new_v(deps)\n            if all(k in dependent_keys.keys() for k in deps):\n                target_tp = schema_tp.val[k]\n                assert isinstance(target_tp, e.DefaultTp)\n                actual_v = e.WithExpr(\n                    get_shaped_from_deps(deps), target_tp.expr\n                )\n                new_v = {\n                    **new_v,\n                    k: actual_v,\n                }\n                del pending_default[k]\n                break\n        else:\n            raise ValueError(\n                f\"Cannot resolve default value for\"\n                f\" because of circular dependencies: {pending_default}\"\n            )\n\n    # now abstract over the dependent keys in order\n    result_expr: e.Expr = e.InsertExpr(\n        name=insert_tp_name,\n        new={k: v for (k, v) in new_v.items() if k not in dependent_keys},\n    )\n    for k, binder_name in reversed(dependent_keys.items()):\n        result_expr = e.WithExpr(\n            new_v[k], eops.abstract_over_expr(result_expr, binder_name)\n        )\n\n    # if we are recursing, second time there will not be any dependent keys\n    if dependent_keys:\n        # This is a hack because we did not\n        # enforce the default expression's types.\n        # So we recheck everything at the end.\n        # TODO: we should optimize checking\n        result_expr = synthesize_type(ctx, result_expr)[1]\n    return result_expr\n\n\ndef update_checking(\n    ctx: e.TcCtx, update_shape: e.ShapeExpr, subject_tp: e.Tp\n) -> e.ShapeExpr:\n    # for breaking circular dependency\n    assert isinstance(subject_tp, e.NamedNominalLinkTp) or isinstance(\n        subject_tp, e.NominalLinkTp\n    ), \"Expecting link type\"\n    tp_name = subject_tp.name\n    _, full_tp = mops.resolve_raw_name_and_type_def(ctx, tp_name)\n    assert isinstance(full_tp, e.ObjectTp), \"Cannot update scalar types\"\n    assert all(\n        isinstance(k, e.StrLabel) for k in update_shape.shape.keys()\n    ), \"Expecting string labels\"\n    cut_tp = {\n        k: v\n        for (k, v) in full_tp.val.items()\n        if e.StrLabel(k) in update_shape.shape.keys()\n    }\n    shape_ck: dict[e.Label, e.BindingExpr] = {}\n    for k, v in cut_tp.items():\n        ctx_new, shape_body, bnd_var = eops.tcctx_add_binding(\n            ctx,\n            update_shape.shape[e.StrLabel(k)],\n            e.ResultTp(subject_tp, e.CardOne),\n        )\n        shape_body_ck = insert_proprerty_checking(ctx_new, shape_body, v)\n        shape_ck[e.StrLabel(k)] = eops.abstract_over_expr(\n            shape_body_ck, bnd_var\n        )\n\n    return e.ShapeExpr(shape=shape_ck)\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/type_checking_tools/function_checking.py",
    "content": "from ..data import data_ops as e\nfrom ..data import type_ops as tops\nfrom ..data import expr_to_str as pp\nfrom ..data import module_ops as mops\nfrom ..interpreter_logging import print_warning\nfrom typing import Optional, Sequence\nfrom functools import reduce\nimport operator\n\n\ndef refine_candidate_tp(tp: e.Tp) -> e.Tp:\n    match tp:\n        case e.NamedNominalLinkTp(name=name, linkprop=_):\n            return e.NamedNominalLinkTp(name=name, linkprop=e.ObjectTp({}))\n        case e.NominalLinkTp(subject=_, name=name, linkprop=_):\n            # refine also drops subject which may contain additional properties\n            return e.NamedNominalLinkTp(name=name, linkprop=e.ObjectTp({}))\n        case e.UnionTp(l, r):\n            return e.UnionTp(refine_candidate_tp(l), refine_candidate_tp(r))\n        case e.IntersectTp(l, r):\n            return e.IntersectTp(\n                refine_candidate_tp(l), refine_candidate_tp(r)\n            )\n        case _:\n            return tp\n\n\ndef try_match_and_get_arg_mods(\n    expr: e.FunAppExpr, fun_def: e.FuncDef\n) -> Optional[Sequence[e.ParamModifier]]:\n    \"\"\"\n    Returns None if the expr does not match the fun_def.\n    \"\"\"\n    match expr:\n        case e.FunAppExpr(\n            fun=_, args=args, overloading_index=_, kwargs=kwargs\n        ):\n            # positional\n            if len(args) == len(fun_def.tp.args_mod):\n                return fun_def.tp.args_mod\n            elif len(args) < len(fun_def.tp.args_mod):\n                part_one = fun_def.tp.args_mod[0 : len(args)]\n                assert all(\n                    [\n                        fun_def.tp.args_label.index(l) >= len(args)\n                        for l in kwargs.keys()\n                    ]\n                )\n                part_two = [\n                    fun_def.tp.args_mod[fun_def.tp.args_label.index(l)]\n                    for l in kwargs.keys()\n                ]\n                return [*part_one, *part_two]\n            elif len(args) > len(fun_def.tp.args_mod):\n                return None\n            else:\n                raise ValueError(\"impossible\", expr)\n        case _:\n            raise ValueError(\"impossible\", expr)\n\n\ndef check_args_ret_type_match(\n    ctx: e.TcCtx, tps_syn: list[e.Tp], tps_ck: e.FunArgRetType\n) -> Optional[e.Tp]:  # Returns the result Tp if matches\n    \"\"\"\n    If matches, return the result type.\n    Need to return result type because we have parametric ploymorphism.\n    \"\"\"\n\n    some_tp_mapping_candidates: dict[int, list[e.Tp]] = {}\n\n    args_ck_tps = tps_ck.args_tp\n    ret_tp = tps_ck.ret_tp.tp\n\n    if len(args_ck_tps) != len(tps_syn):\n        return None\n\n    for _, (syn_tp, ck_tp) in enumerate(zip(tps_syn, args_ck_tps)):\n        tops.collect_is_subtype_with_instantiation(\n            ctx, syn_tp, ck_tp, some_tp_mapping_candidates\n        )\n\n    some_tp_mapping: dict[int, e.Tp] = {}\n    for i, candidate_tps in some_tp_mapping_candidates.items():\n        if len(candidate_tps) == 1:\n            some_tp_mapping[i] = candidate_tps[0]\n            continue\n        else:\n            for candidate_tp in candidate_tps:\n                if all(\n                    tops.check_is_subtype(ctx, tp, candidate_tp)\n                    for tp in candidate_tps\n                ):\n                    some_tp_mapping[i] = candidate_tp\n                    break\n            else:\n                # the refined is used with the test case\n                # test_edgeql_select_subqueries_16\n                # where for std::IN, one argument has a link prop\n                # and the other do not\n                # I choose to remove the link prop uniformly in this case\n                refined_candidate_tps = [\n                    refine_candidate_tp(tp) for tp in candidate_tps\n                ]\n                for candidate_tp in refined_candidate_tps:\n                    if all(\n                        tops.check_is_subtype(ctx, tp, candidate_tp)\n                        for tp in refined_candidate_tps\n                    ):\n                        some_tp_mapping[i] = candidate_tp\n                        break\n                else:\n                    # cannot find a unique assignment for a candidate type\n                    return None\n\n    for _, (syn_tp, ck_tp) in enumerate(zip(tps_syn, args_ck_tps)):\n        if tops.check_is_subtype_with_instantiation(\n            ctx, syn_tp, ck_tp, some_tp_mapping\n        ):\n            continue\n        else:\n            syn_tp = refine_candidate_tp(\n                syn_tp\n            )  # use refinement if it fails on the first run\n            if tops.check_is_subtype_with_instantiation(\n                ctx, syn_tp, ck_tp, some_tp_mapping\n            ):\n                continue\n            else:\n                return None\n\n    final_ret_tp = tops.recursive_instantiate_tp(ret_tp, some_tp_mapping)\n    return final_ret_tp\n\n\ndef func_call_checking(\n    ctx: e.TcCtx, fun_call: e.FunAppExpr\n) -> tuple[e.ResultTp, e.FunAppExpr]:\n    # for breaking circular dependency\n    from . import typechecking as tc\n\n    match fun_call:\n        case e.FunAppExpr(\n            fun=fname, args=args, overloading_index=idx, kwargs=kwargs\n        ):\n            qualified_fname, fun_defs = mops.resolve_raw_name_and_func_def(\n                ctx, fname\n            )\n            if args:\n                [res_tps, args_cks_tuple] = zip(\n                    *[tc.synthesize_type(ctx, v) for v in args]\n                )\n                [tps_tuple, arg_cards_tuple] = zip(*res_tps)\n                tps = list(tps_tuple)\n                arg_cards = list(arg_cards_tuple)\n                args_cks = list(args_cks_tuple)\n            else:\n                tps = []\n                arg_cards = []\n                args_cks = []\n\n            kwargs_ck = {\n                k: tc.synthesize_type(ctx, v) for k, v in kwargs.items()\n            }\n\n            if idx is not None:\n                args_ret_type = fun_defs[idx].tp\n                assert len(kwargs) == 0, \"idx must be concurrent with kwargs\"\n                result_tp = check_args_ret_type_match(ctx, tps, args_ret_type)\n                if result_tp is None:\n                    raise ValueError(\n                        \"Overloading for function incorrectly calculated\",\n                        fun_call,\n                    )\n            else:\n                ok_candidates: list[tuple[int, e.Tp]] = []\n                for fun_idx, fun_def in enumerate(fun_defs):\n                    if len(tps) == len(fun_def.tp.args_tp):\n                        if len(kwargs) == 0:\n                            result_tp = check_args_ret_type_match(\n                                ctx, tps, fun_def.tp\n                            )\n                        else:\n                            pass\n                    elif len(tps) > len(fun_def.tp.args_tp):\n                        pass\n                    elif len(tps) < len(fun_def.tp.args_tp):\n                        new_tps = [*tps]\n                        for i in range(len(tps), len(fun_def.tp.args_tp)):\n                            label_i = fun_def.tp.args_label[i]\n                            if label_i in kwargs:\n                                new_tps = [*new_tps, kwargs_ck[label_i][0][0]]\n                            elif label_i in fun_def.defaults:\n                                default_expr = fun_def.defaults[label_i]\n                                if default_expr == e.MultiSetExpr([]):\n                                    new_tps = [*new_tps, fun_def.tp.args_tp[i]]\n                                else:\n                                    new_tps = [\n                                        *new_tps,\n                                        tc.synthesize_type(\n                                            ctx, fun_def.defaults[label_i]\n                                        )[0][0],\n                                    ]\n                            else:\n                                result_tp = None\n                                break\n                        else:\n                            result_tp = check_args_ret_type_match(\n                                ctx, new_tps, fun_def.tp\n                            )\n                    if result_tp is not None:\n                        ok_candidates.append((fun_idx, result_tp))\n\n                if len(ok_candidates) > 1:\n                    print_warning(\n                        \"WARNING: Ambiguous overloading\",\n                        pp.show_qname(qualified_fname),\n                        \"picking the first one\",\n                    )\n                    ok_candidates = ok_candidates[0:1]\n\n                if len(ok_candidates) == 0:\n                    for _, fun_def in enumerate(fun_defs):\n                        result_tp = check_args_ret_type_match(\n                            ctx, tps, fun_def.tp\n                        )\n                    raise ValueError(\n                        \"No overloading matches\",\n                        pp.show_qname(qualified_fname),\n                        \"args type\",\n                        [pp.show_tp(tp) for tp in tps],\n                        \"candidates\",\n                        [pp.show_func_tps(fun_def.tp) for fun_def in fun_defs],\n                        pp.show_expr(fun_call),\n                    )\n                elif len(ok_candidates) == 1:\n                    idx, result_tp = ok_candidates[0]\n                else:\n                    raise ValueError(\n                        \"Ambiguous overloading\",\n                        pp.show_qname(qualified_fname),\n                        \"args type\",\n                        [pp.show_tp(tp) for tp in tps],\n                        \"candidates\",\n                        [pp.show_func_tps(fun_def.tp) for fun_def in fun_defs],\n                        pp.show_expr(fun_call),\n                    )\n\n            if len(arg_cards) < len(fun_defs[idx].tp.args_mod):\n                for i in range(len(tps), len(fun_def.tp.args_tp)):\n                    label_i = fun_def.tp.args_label[i]\n                    if label_i in kwargs:\n                        arg_cards.append(kwargs_ck[label_i][0][1])\n                    elif label_i in fun_def.defaults:\n                        arg_cards.append(\n                            tc.synthesize_type(ctx, fun_def.defaults[label_i])[\n                                0\n                            ][1]\n                        )\n                    else:\n                        raise ValueError(\"impossible\", fun_call)\n\n            # take the product of argument cardinalities\n            arg_card_product = reduce(\n                operator.mul,\n                (\n                    tops.match_param_modifier(param_mod, arg_card)\n                    for param_mod, arg_card in zip(\n                        fun_defs[idx].tp.args_mod, arg_cards, strict=True\n                    )\n                ),\n                e.CardOne,\n            )\n            result_card = arg_card_product * fun_defs[idx].tp.ret_tp.mode\n            # special processing of cardinality inference for certain functions\n            match qualified_fname:\n                case e.QualifiedName([\"std\", \"??\"]):\n                    assert len(arg_cards) == 2\n                    result_card = e.CMMode(\n                        e.max_cardinal(arg_cards[0].lower, arg_cards[1].lower),\n                        e.max_cardinal(arg_cards[0].upper, arg_cards[1].upper),\n                    )\n                case e.QualifiedName([\"std\", \"assert_exists\"]):\n                    if result_card.lower == e.ZeroCardinal():\n                        result_card = e.CMMode(\n                            e.OneCardinal(), arg_cards[0].upper\n                        )\n                        # TODO preserve cardinality annotation\n                case _:\n                    pass\n\n            if len(args_cks) < len(fun_defs[idx].tp.args_mod):\n                for i in range(len(args_cks), len(fun_defs[idx].tp.args_tp)):\n                    label_i = fun_defs[idx].tp.args_label[i]\n                    if label_i in kwargs:\n                        args_cks.append(kwargs_ck[label_i][1])\n                    elif label_i in fun_defs[idx].defaults:\n                        args_cks.append(\n                            tc.synthesize_type(\n                                ctx, fun_defs[idx].defaults[label_i]\n                            )[1]\n                        )\n                    else:\n                        raise ValueError(\"impossible\", fun_call)\n\n            result_expr = e.FunAppExpr(\n                fun=qualified_fname,\n                args=args_cks,\n                overloading_index=idx,\n                kwargs={},\n            )\n            return (e.ResultTp(result_tp, result_card), result_expr)\n        case _:\n            raise ValueError(\"impossible\", fun_call)\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/type_checking_tools/inheritance_populate.py",
    "content": "from typing import Sequence\n\nfrom ..data import data_ops as e\nfrom ..data import expr_ops as eops\nfrom ..data import module_ops as mops\nfrom ..data import path_factor as path_factor\nfrom ..data import expr_to_str as pp\n\n\ndef merge_result_tp(ctx: e.TcCtx, l: e.ResultTp, r: e.ResultTp) -> e.ResultTp:\n    if l.mode != r.mode:\n        raise ValueError(\"Cardinality mismatch\", l, r)\n    match l.tp, r.tp:\n        case e.NamedNominalLinkTp(\n            name=l_name, linkprop=l_linkprop\n        ), e.NamedNominalLinkTp(name=r_name, linkprop=r_linkprop):\n            if l_name != r_name:\n                raise ValueError(\"Named nominal link tp name mismatch\", l, r)\n            new_link_prop: dict[str, e.ResultTp] = {}\n            for lbl, (l_comp_tp, l_comp_card) in l_linkprop.val.items():\n                new_link_prop[lbl] = e.ResultTp(l_comp_tp, l_comp_card)\n            for lbl, (r_comp_tp, r_comp_card) in r_linkprop.val.items():\n                if lbl not in new_link_prop:\n                    new_link_prop[lbl] = e.ResultTp(r_comp_tp, r_comp_card)\n                else:\n                    new_link_prop[lbl] = merge_result_tp(\n                        ctx,\n                        new_link_prop[lbl],\n                        e.ResultTp(r_comp_tp, r_comp_card),\n                    )\n            return e.ResultTp(\n                e.NamedNominalLinkTp(\n                    name=l_name, linkprop=e.ObjectTp(new_link_prop)\n                ),\n                l.mode,\n            )\n        case e.NamedNominalLinkTp(\n            name=l_name, linkprop=l_linkprop\n        ), e.OverloadedTargetTp(linkprop=r_linkprop):\n            assert r_linkprop is not None\n            return merge_result_tp(\n                ctx,\n                l,\n                e.ResultTp(\n                    e.NamedNominalLinkTp(name=l_name, linkprop=r_linkprop),\n                    r.mode,\n                ),\n            )\n\n        case _:\n            if l.tp != r.tp:\n                raise ValueError(\n                    \"Type mismatch\", pp.show_result_tp(l), pp.show_result_tp(r)\n                )\n            return l\n\n\ndef copy_construct_inheritance(\n    ctx: e.TcCtx,\n    typedef: e.ObjectTp,\n    super_types: list[e.QualifiedName],\n    constraints: Sequence[e.Constraint],\n    indexes: Sequence[Sequence[str]],\n) -> tuple[e.ObjectTp, Sequence[e.Constraint], Sequence[Sequence[str]]]:\n\n    definitions = [\n        mops.resolve_type_def(ctx, super_type) for super_type in super_types\n    ]\n    final_tp_dict: dict[str, e.ResultTp] = {}\n    final_constraints: list[e.Constraint] = [*constraints]\n    final_indexes: list[Sequence[str]] = [*indexes]\n    for i, mdef in enumerate(definitions):\n        definition = mdef.typedef\n        super_constraint = mdef.constraints\n        super_indexes = mdef.indexes\n        assert isinstance(definition, e.ObjectTp)\n        def_dep = ctx.schema.subtyping_relations[super_types[i]]\n        definition_ck, constraints_ck, indexes_ck = copy_construct_inheritance(\n            ctx, definition, def_dep, super_constraint, super_indexes\n        )\n\n        for lbl, (t_comp_tp, t_comp_card) in definition_ck.val.items():\n            if lbl not in final_tp_dict:\n                final_tp_dict[lbl] = e.ResultTp(t_comp_tp, t_comp_card)\n            else:\n                final_tp_dict[lbl] = merge_result_tp(\n                    ctx, final_tp_dict[lbl], e.ResultTp(t_comp_tp, t_comp_card)\n                )\n        final_constraints = [\n            *final_constraints,\n            *(\n                c\n                for c in constraints_ck\n                if isinstance(c, e.ExclusiveConstraint) and c.delegated\n            ),\n        ]\n        final_indexes = [*final_indexes, *indexes_ck]\n\n    for lbl, (t_comp_tp, t_comp_card) in typedef.val.items():\n        if lbl not in final_tp_dict:\n            final_tp_dict[lbl] = e.ResultTp(t_comp_tp, t_comp_card)\n        else:\n            final_tp_dict[lbl] = merge_result_tp(\n                ctx, final_tp_dict[lbl], e.ResultTp(t_comp_tp, t_comp_card)\n            )\n    return e.ObjectTp(final_tp_dict), final_constraints, final_indexes\n\n\ndef module_inheritance_populate(\n    dbschema: e.DBSchema, module_name: tuple[str, ...]\n) -> None:\n    \"\"\"\n    Modifies the db schema after checking\n    \"\"\"\n    result_vals: dict[str, e.ModuleEntity] = {}\n    dbmodule = dbschema.unchecked_modules[module_name]\n    for t_name, t_me in dbmodule.defs.items():\n        root_ctx = eops.emtpy_tcctx_from_dbschema(dbschema, module_name)\n        match t_me:\n            case e.ModuleEntityTypeDef(\n                typedef=typedef,\n                is_abstract=is_abstract,\n                constraints=constraints,\n                indexes=indexes,\n            ):\n                if isinstance(typedef, e.ObjectTp):\n                    if (\n                        e.QualifiedName([*module_name, t_name])\n                        in dbschema.subtyping_relations\n                    ):\n                        new_typedef, new_constraints, new_indexes = (\n                            copy_construct_inheritance(\n                                root_ctx,\n                                typedef,\n                                dbschema.subtyping_relations[\n                                    e.QualifiedName([*module_name, t_name])\n                                ],\n                                constraints,\n                                indexes,\n                            )\n                        )\n\n                        result_vals = {\n                            **result_vals,\n                            t_name: e.ModuleEntityTypeDef(\n                                typedef=new_typedef,\n                                is_abstract=is_abstract,\n                                constraints=new_constraints,\n                                indexes=new_indexes,\n                            ),\n                        }\n                    else:\n                        result_vals = {**result_vals, t_name: t_me}\n                elif isinstance(typedef, e.ScalarTp):\n                    # insert assignment casts\n                    assert isinstance(\n                        typedef.name, e.QualifiedName\n                    ), \"Name resolution should have been done\"\n                    assert typedef.name == e.QualifiedName(\n                        [*module_name, t_name]\n                    )\n                    assert (\n                        typedef.name\n                        not in dbschema.unchecked_subtyping_relations\n                    )\n                    for parent_name in dbschema.subtyping_relations[\n                        typedef.name\n                    ]:\n\n                        def default_cast_fun(v):\n                            return v\n\n                        cast_key = (\n                            e.ScalarTp(parent_name),\n                            e.ScalarTp(typedef.name),\n                        )\n                        assert cast_key not in dbschema.casts\n                        dbschema.casts[cast_key] = e.TpCast(\n                            e.TpCastKind.Assignment, default_cast_fun\n                        )\n                    result_vals = {**result_vals, t_name: t_me}\n                else:\n                    raise ValueError(\"Not Implemented\", typedef)\n            case e.ModuleEntityFuncDef(funcdefs=funcdefs):\n                result_vals = {\n                    **result_vals,\n                    t_name: e.ModuleEntityFuncDef(funcdefs=funcdefs),\n                }\n            case _:\n                raise ValueError(\"Unimplemented\", t_me)\n    dbschema.unchecked_modules[module_name] = e.DBModule(result_vals)\n\n\ndef module_subtyping_resolve(dbschema: e.DBSchema) -> None:\n    for qname, rname_list in dbschema.unchecked_subtyping_relations.items():\n        if qname in dbschema.subtyping_relations:\n            raise ValueError(\"Duplicate subtyping relation\", qname)\n        rname_ck_list = []\n        for cur_module, rname in rname_list:\n            resolved_name, _ = mops.resolve_raw_name_and_type_def(\n                e.TcCtx(dbschema, cur_module, {}), rname\n            )\n            rname_ck_list.append(resolved_name)\n        dbschema.subtyping_relations[qname] = rname_ck_list\n\n    # remove everything from unchecked_subtyping_relations\n    dbschema.unchecked_subtyping_relations.clear()\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/type_checking_tools/module_check_tools.py",
    "content": "from typing import Callable\n\nfrom ..data import data_ops as e\nfrom ..data import expr_ops as eops\nfrom ..data import path_factor as path_factor\n\n\ndef unchecked_module_map(\n    dbschema: e.DBSchema,\n    module_name: tuple[str, ...],\n    f: Callable[[e.TcCtx, e.Tp, e.Tp, e.CMMode], e.Tp],\n    g: Callable[[e.TcCtx, e.FuncDef], e.FuncDef],\n) -> None:\n    \"\"\"\n    Modifies the db schema after checking\n    \"\"\"\n    root_ctx = eops.emtpy_tcctx_from_dbschema(dbschema, module_name)\n\n    def unchecked_object_tp_map(\n        subject_tp: e.Tp, obj_tp: e.ObjectTp\n    ) -> e.ObjectTp:\n        result_vals: dict[str, e.ResultTp] = {}\n        for lbl, (t_comp_tp, t_comp_card) in obj_tp.val.items():\n            result_vals[lbl] = e.ResultTp(\n                f(root_ctx, subject_tp, t_comp_tp, t_comp_card), t_comp_card\n            )\n        return e.ObjectTp(result_vals)\n\n    result_vals: dict[str, e.ModuleEntity] = {}\n    dbmodule = dbschema.unchecked_modules[module_name]\n    for t_name, t_me in dbmodule.defs.items():\n        match t_me:\n            case e.ModuleEntityTypeDef(\n                typedef=typedef,\n                is_abstract=is_abstract,\n                constraints=constraints,\n                indexes=indexes,\n            ):\n                if isinstance(typedef, e.ObjectTp):\n                    result_vals = {\n                        **result_vals,\n                        t_name: e.ModuleEntityTypeDef(\n                            typedef=unchecked_object_tp_map(\n                                e.NamedNominalLinkTp(\n                                    name=e.QualifiedName(\n                                        [*module_name, t_name]\n                                    ),\n                                    linkprop=e.ObjectTp({}),\n                                ),\n                                typedef,\n                            ),\n                            is_abstract=is_abstract,\n                            constraints=constraints,\n                            indexes=indexes,\n                        ),\n                    }\n                else:\n                    assert isinstance(typedef, e.ScalarTp)\n                    result_vals = {**result_vals, t_name: t_me}\n            case e.ModuleEntityFuncDef(funcdefs=funcdefs):\n                result_vals = {\n                    **result_vals,\n                    t_name: e.ModuleEntityFuncDef(\n                        funcdefs=[g(root_ctx, funcdef) for funcdef in funcdefs]\n                    ),\n                }\n            case _:\n                raise ValueError(\"Unimplemented\", t_me)\n    dbschema.unchecked_modules[module_name] = e.DBModule(result_vals)\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/type_checking_tools/name_resolution.py",
    "content": "\nfrom ..data import data_ops as e\nfrom ..data import module_ops as mops\nfrom ..data import path_factor as path_factor\nfrom . import typechecking as tck\nfrom . import module_check_tools as mck\n\n\ndef object_tp_comp_name_resolve(\n    root_ctx: e.TcCtx,\n    tp_comp: e.Tp,\n) -> e.Tp:\n    match tp_comp:\n        case e.UncheckedTypeName(name):\n            return tck.check_type_valid(root_ctx, tp_comp)\n        case e.NamedNominalLinkTp(name=name, linkprop=l_prop):\n            if isinstance(name, e.UnqualifiedName):\n                name_ck = mops.resolve_simple_name(root_ctx, name)\n            else:\n                name_ck = name\n            resolved_tp = mops.try_resolve_type_name(root_ctx, name_ck)\n            if not isinstance(resolved_tp, e.ObjectTp):\n                raise ValueError(\n                    \"Scalar type cannot carry link props\", tp_comp\n                )\n\n            linkprop_ck: dict[str, e.ResultTp] = {}\n            for lbl, (t_comp_tp, t_comp_card) in l_prop.val.items():\n                linkprop_ck[lbl] = e.ResultTp(\n                    object_tp_comp_name_resolve(\n                        root_ctx=root_ctx,\n                        tp_comp=t_comp_tp,\n                    ),\n                    t_comp_card,\n                )\n\n            return e.NamedNominalLinkTp(\n                name=name_ck, linkprop=e.ObjectTp(linkprop_ck)\n            )\n        case e.NominalLinkTp(subject=_, name=name, linkprop=l_prop):\n            raise ValueError(\n                \"No nominal link tp should appear in name resolution\", tp_comp\n            )\n        case e.UncheckedComputableTp(expr=c_expr):\n            return tp_comp\n        case e.ComputableTp(expr=c_expr, tp=c_tp):\n            return e.ComputableTp(\n                expr=c_expr, tp=object_tp_comp_name_resolve(root_ctx, c_tp)\n            )\n        case e.DefaultTp(expr=c_expr, tp=c_tp):\n            return e.DefaultTp(\n                expr=c_expr, tp=object_tp_comp_name_resolve(root_ctx, c_tp)\n            )\n        case e.OverloadedTargetTp(linkprop=linkprop):\n            assert linkprop is not None\n            return e.OverloadedTargetTp(\n                linkprop=e.ObjectTp(\n                    {\n                        lbl: e.ResultTp(\n                            object_tp_comp_name_resolve(root_ctx, t_comp_tp),\n                            t_comp_card,\n                        )\n                        for lbl, (\n                            t_comp_tp,\n                            t_comp_card,\n                        ) in linkprop.val.items()\n                    }\n                )\n            )\n        case e.UnionTp(l, r):\n            return e.UnionTp(\n                object_tp_comp_name_resolve(root_ctx, l),\n                object_tp_comp_name_resolve(root_ctx, r),\n            )\n        case e.CompositeTp(kind=kind, tps=tps, labels=labels):\n            return e.CompositeTp(\n                kind=kind,\n                tps=[object_tp_comp_name_resolve(root_ctx, t) for t in tps],\n                labels=labels,\n            )\n        case e.SomeTp(_):\n            return tp_comp\n        case e.AnyTp(_):\n            return tp_comp\n        case _:\n            raise ValueError(\"Not Implemented\", tp_comp)\n\n\ndef fun_arg_ret_type_name_resolve(\n    root_ctx: e.TcCtx,\n    tp: e.FunArgRetType,\n) -> e.FunArgRetType:\n    return e.FunArgRetType(\n        args_tp=[object_tp_comp_name_resolve(root_ctx, t) for t in tp.args_tp],\n        args_mod=tp.args_mod,\n        args_label=tp.args_label,\n        ret_tp=e.ResultTp(\n            object_tp_comp_name_resolve(root_ctx, tp.ret_tp.tp), tp.ret_tp.mode\n        ),\n    )\n\n\ndef func_def_name_resolve(\n    root_ctx: e.TcCtx,\n    func_def: e.FuncDef,\n) -> e.FuncDef:\n    match func_def:\n        case e.DefinedFuncDef(tp=tp, impl=impl, defaults=defaults):\n            return e.DefinedFuncDef(\n                tp=fun_arg_ret_type_name_resolve(root_ctx, tp),\n                impl=impl,\n                defaults=defaults,\n            )\n        case e.BuiltinFuncDef(tp=tp, impl=impl, defaults=defaults):\n            # do not check validity for builtin funcs\n            return e.BuiltinFuncDef(tp=tp, impl=impl, defaults=defaults)\n        case _:\n            raise ValueError(\"Not Implemented\", func_def)\n\n\ndef module_name_resolve(\n    dbschema: e.DBSchema, module_name: tuple[str, ...]\n) -> None:\n    \"\"\"\n    Modifies the db schema after checking\n    \"\"\"\n\n    def f(\n        root_ctx: e.TcCtx,\n        subject_tp: e.Tp,\n        tp_comp: e.Tp,\n        tp_comp_card: e.CMMode,\n    ) -> e.Tp:\n        return object_tp_comp_name_resolve(root_ctx, tp_comp)\n\n    mck.unchecked_module_map(dbschema, module_name, f, func_def_name_resolve)\n\n\ndef checked_module_name_resolve(\n    dbschema: e.DBSchema, module_name: tuple[str, ...]\n) -> None:\n    \"\"\"\n    Modifies the db schema after checking\n    \"\"\"\n    assert module_name not in dbschema.unchecked_modules\n    dbschema.unchecked_modules[module_name] = dbschema.modules[module_name]\n    del dbschema.modules[module_name]\n    module_name_resolve(dbschema, module_name)\n    assert module_name not in dbschema.modules\n    dbschema.modules[module_name] = dbschema.unchecked_modules[module_name]\n    del dbschema.unchecked_modules[module_name]\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/type_checking_tools/schema_checking.py",
    "content": "\nfrom ..data import data_ops as e\nfrom ..data import expr_ops as eops\nfrom ..data import type_ops as tops\nfrom ..data import module_ops as mops\nfrom ..data import path_factor as path_factor\nfrom ..data import expr_to_str as pp\nfrom . import name_resolution as name_res\nfrom .typechecking import check_type_valid, synthesize_type, check_type\nfrom . import module_check_tools as mck\nfrom . import inheritance_populate as inheritance_populate\n\n\ndef check_object_tp_comp_validity(\n    root_ctx: e.TcCtx, subject_tp: e.Tp, tp_comp: e.Tp, tp_comp_card: e.CMMode\n) -> e.Tp:\n    match tp_comp:\n        case e.UncheckedTypeName(name):\n            return check_type_valid(root_ctx, tp_comp)\n        case e.NamedNominalLinkTp(name=name, linkprop=l_prop):\n            if isinstance(name, e.UnqualifiedName):\n                name_ck = mops.resolve_simple_name(root_ctx, name)\n            else:\n                name_ck = name\n            resolved_tp = mops.try_resolve_type_name(root_ctx, name_ck)\n            if not isinstance(resolved_tp, e.ObjectTp):\n                raise ValueError(\n                    \"Scalar type cannot carry link props\", tp_comp\n                )\n            return e.NamedNominalLinkTp(\n                name=name_ck,\n                linkprop=check_object_tp_validity(\n                    root_ctx=root_ctx,\n                    subject_tp=tops.get_runtime_tp(tp_comp),\n                    obj_tp=l_prop,\n                ),\n            )\n        case e.NominalLinkTp(subject=l_sub, name=name, linkprop=l_prop):\n            return e.NominalLinkTp(\n                subject=l_sub,\n                name=name,\n                linkprop=check_object_tp_validity(\n                    root_ctx=root_ctx,\n                    subject_tp=tops.get_runtime_tp(tp_comp),\n                    obj_tp=l_prop,\n                ),\n            )\n        case e.UncheckedComputableTp(expr=c_expr):\n            if not isinstance(c_expr, e.BindingExpr):  # type: ignore\n                raise ValueError(\n                    \"Computable type must be a binding expression\"\n                )\n            new_ctx, c_body, bnd_var = eops.tcctx_add_binding(\n                root_ctx,\n                c_expr,  # type: ignore\n                e.ResultTp(subject_tp, e.CardOne),\n            )\n            c_body = path_factor.select_hoist(c_body, new_ctx)\n            synth_tp, c_body_ck = synthesize_type(new_ctx, c_body)\n            tops.assert_cardinal_subtype(synth_tp.mode, tp_comp_card)\n            return e.ComputableTp(\n                expr=eops.abstract_over_expr(c_body_ck, bnd_var),\n                tp=synth_tp.tp,\n            )\n        case e.ComputableTp(expr=c_expr, tp=c_tp):\n            if not isinstance(c_expr, e.BindingExpr):  # type: ignore\n                raise ValueError(\n                    \"Computable type must be a binding expression\"\n                )\n            new_ctx, c_body, bnd_var = eops.tcctx_add_binding(\n                root_ctx,\n                c_expr,  # type: ignore\n                e.ResultTp(subject_tp, e.CardOne),\n            )\n            c_body = path_factor.select_hoist(c_body, new_ctx)\n            synth_tp, c_body_ck = synthesize_type(new_ctx, c_body)\n            tops.assert_cardinal_subtype(synth_tp.mode, tp_comp_card)\n            tops.assert_real_subtype(new_ctx, synth_tp.tp, c_tp)\n            return e.ComputableTp(\n                expr=eops.abstract_over_expr(c_body_ck, bnd_var), tp=c_tp\n            )\n        # This code is mostly copied from the above\n        # TODO: Can we not copy?\n        case e.DefaultTp(expr=c_expr, tp=c_tp):\n            if not isinstance(c_expr, e.BindingExpr):  # type: ignore\n                raise ValueError(\n                    \"Computable type must be a binding expression\"\n                )\n            c_tp_ck = c_tp\n            new_ctx, c_body, bnd_var = eops.tcctx_add_binding(\n                root_ctx,\n                c_expr,  # type: ignore\n                e.ResultTp(subject_tp, e.CardOne),\n            )\n            c_body = path_factor.select_hoist(c_body, new_ctx)\n            synth_tp, c_body_ck = synthesize_type(new_ctx, c_body)\n            tops.assert_cardinal_subtype(synth_tp.mode, tp_comp_card)\n            tops.assert_real_subtype(new_ctx, synth_tp.tp, c_tp_ck)\n            return e.DefaultTp(\n                expr=eops.abstract_over_expr(c_body_ck, bnd_var), tp=c_tp_ck\n            )\n        case e.ScalarTp(_):\n            return tp_comp\n        case e.UnionTp(l, r):\n            return e.UnionTp(\n                check_object_tp_comp_validity(\n                    root_ctx, subject_tp, l, tp_comp_card\n                ),\n                check_object_tp_comp_validity(\n                    root_ctx, subject_tp, r, tp_comp_card\n                ),\n            )\n        case e.CompositeTp(kind=kind, tps=tps, labels=labels):\n            return e.CompositeTp(\n                kind=kind,\n                tps=[\n                    check_object_tp_comp_validity(\n                        root_ctx, subject_tp, t_comp_tp, tp_comp_card\n                    )\n                    for t_comp_tp in tps\n                ],\n                labels=labels,\n            )\n        case e.OverloadedTargetTp(_):\n            raise ValueError(\n                \"Overloaded target tp should not appear in type checking, \"\n                \"check whether the inheritance processing is intact\",\n                tp_comp,\n            )\n        case _:\n            raise ValueError(\"Not Implemented\", pp.show(tp_comp))\n\n\ndef check_object_tp_validity(\n    root_ctx: e.TcCtx, subject_tp: e.Tp, obj_tp: e.ObjectTp\n) -> e.ObjectTp:\n    result_vals: dict[str, e.ResultTp] = {}\n    for lbl, (t_comp_tp, t_comp_card) in obj_tp.val.items():\n        result_vals[lbl] = e.ResultTp(\n            check_object_tp_comp_validity(\n                root_ctx=root_ctx,\n                subject_tp=subject_tp,\n                tp_comp=t_comp_tp,\n                tp_comp_card=t_comp_card,\n            ),\n            t_comp_card,\n        )\n    return e.ObjectTp(result_vals)\n\n\ndef param_modifier_to_paramter_cardinality(mod: e.ParamModifier) -> e.CMMode:\n    match mod:\n        case e.ParamSingleton():\n            return e.CardOne\n        case e.ParamSetOf():\n            return e.CardAny\n        case e.ParamOptional():\n            return e.CardAtMostOne\n        case _:\n            raise ValueError(\"Not Implemented\", mod)\n\n\ndef check_fun_def_validity(ctx: e.TcCtx, fun_def: e.FuncDef) -> e.FuncDef:\n    match fun_def:\n        case e.DefinedFuncDef(tp=tp, impl=impl, defaults=defaults):\n            binders = []\n            for i, arg_tp in enumerate(tp.args_tp):\n                assert isinstance(impl, e.BindingExpr)\n                arg_mod = param_modifier_to_paramter_cardinality(\n                    tp.args_mod[i]\n                )\n                ctx, impl, binder_name = eops.tcctx_add_binding(\n                    ctx, impl, e.ResultTp(arg_tp, arg_mod)\n                )\n                binders.append(binder_name)\n            impl_ck = check_type(ctx, impl, tp.ret_tp)\n            for binder in binders[::-1]:\n                impl_ck = eops.abstract_over_expr(impl_ck, binder)\n            return e.DefinedFuncDef(\n                tp=tp,\n                impl=impl_ck,\n                defaults={\n                    k: synthesize_type(ctx, v)[1] for k, v in defaults.items()\n                },\n            )\n        case e.BuiltinFuncDef(tp=tp, impl=impl, defaults=defaults):\n            # do not check validity for builtin funcs\n            return e.BuiltinFuncDef(tp=tp, impl=impl, defaults=defaults)\n        case _:\n            raise ValueError(\"Not Implemented\", fun_def)\n\n\ndef check_module_validity(\n    dbschema: e.DBSchema, module_name: tuple[str, ...]\n) -> e.DBSchema:\n    \"\"\"\n    Checks the validity of an unchecked module in dbschema.\n    Modifies the db schema after checking\n    \"\"\"\n    name_res.module_name_resolve(dbschema, module_name)\n    inheritance_populate.module_subtyping_resolve(dbschema)\n    inheritance_populate.module_inheritance_populate(dbschema, module_name)\n    mck.unchecked_module_map(\n        dbschema,\n        module_name,\n        check_object_tp_comp_validity,\n        check_fun_def_validity,\n    )\n    dbschema.modules[module_name] = dbschema.unchecked_modules[module_name]\n    del dbschema.unchecked_modules[module_name]\n    return dbschema\n\n\ndef re_populate_module_inheritance(\n    dbschema: e.DBSchema, module_name: tuple[str, ...]\n) -> None:\n    \"\"\"\n    Checks the validity of an unchecked module in dbschema.\n    Modifies the db schema after checking\n    \"\"\"\n    dbschema.unchecked_modules[module_name] = dbschema.modules[module_name]\n    del dbschema.modules[module_name]\n    inheritance_populate.module_subtyping_resolve(dbschema)\n    inheritance_populate.module_inheritance_populate(dbschema, module_name)\n    dbschema.modules[module_name] = dbschema.unchecked_modules[module_name]\n    del dbschema.unchecked_modules[module_name]\n"
  },
  {
    "path": "edb/tools/experimental_interpreter/type_checking_tools/typechecking.py",
    "content": "from functools import reduce\nimport operator\nfrom typing import Sequence\n\nfrom edb import errors\nfrom ..data import data_ops as e\nfrom ..data import expr_ops as eops\nfrom ..data import type_ops as tops\nfrom ..data import module_ops as mops\nfrom ..data import path_factor as path_factor\nfrom .dml_checking import insert_checking, update_checking\nfrom ..data import expr_to_str as pp\nfrom .function_checking import func_call_checking\nfrom .cast_checking import check_castable\nfrom ..schema import subtyping_resolution as subtp_resol\n\n\ndef synthesize_type_for_val(val: e.Val) -> e.Tp:\n    match val:\n        case e.ScalarVal(tp, _):\n            return tp\n        case _:\n            raise ValueError(\"Not implemented\", val)\n\n\ndef check_shape_transform(\n    ctx: e.TcCtx, s: e.ShapeExpr, tp: e.Tp\n) -> tuple[e.Tp, e.ShapeExpr]:\n\n    result_s_tp = e.ObjectTp({})\n    result_l_tp = e.ObjectTp({})\n    result_expr = e.ShapeExpr({})\n\n    for lbl, comp in s.shape.items():\n        match lbl:\n            case e.StrLabel(s_lbl):\n                new_ctx, body, bnd_var = eops.tcctx_add_binding(\n                    ctx, comp, e.ResultTp(tp, e.CardOne)\n                )\n                result_tp, checked_body = synthesize_type(new_ctx, body)\n                result_s_tp = e.ObjectTp({**result_s_tp.val, s_lbl: result_tp})\n                result_expr = e.ShapeExpr(\n                    {\n                        **result_expr.shape,\n                        lbl: eops.abstract_over_expr(checked_body, bnd_var),\n                    }\n                )\n            case e.LinkPropLabel(l_lbl):\n                new_ctx, body, bnd_var = eops.tcctx_add_binding(\n                    ctx, comp, e.ResultTp(tp, e.CardOne)\n                )\n                result_tp, checked_body = synthesize_type(new_ctx, body)\n                result_l_tp = e.ObjectTp({**result_l_tp.val, l_lbl: result_tp})\n                result_expr = e.ShapeExpr(\n                    {\n                        **result_expr.shape,\n                        lbl: eops.abstract_over_expr(checked_body, bnd_var),\n                    }\n                )\n\n    ret_tp = tops.combine_tp_with_subject_tp(ctx, tp, result_s_tp)\n    ret_tp = tops.combine_tp_with_linkprop_tp(ctx, ret_tp, result_l_tp)\n    return ret_tp, result_expr\n\n\ndef type_cast_tp(ctx: e.TcCtx, from_tp: e.ResultTp, to_tp: e.Tp) -> e.ResultTp:\n    if (from_tp.tp, to_tp) in ctx.schema.casts:\n        return e.ResultTp(to_tp, from_tp.mode)\n    else:\n        raise ValueError(\"Not Implemented\", from_tp, to_tp)\n\n\ndef check_filter_body_is_exclusive(ctx: e.TcCtx, filter_ck: e.Expr) -> bool:\n    match filter_ck:\n        case e.FunAppExpr(fun=e.QualifiedName([\"std\", \"=\"]), args=args):\n            if len(args) != 2:\n                return False\n\n            if (\n                isinstance(args[0], e.ObjectProjExpr)\n                and isinstance(args[1], e.ScalarVal)\n            ) or (\n                isinstance(args[1], e.ObjectProjExpr)\n                and isinstance(args[0], e.ScalarVal)\n            ):\n                proj = (\n                    args[0]\n                    if isinstance(args[0], e.ObjectProjExpr)\n                    else args[1]\n                )\n                match proj:\n                    case e.ObjectProjExpr(\n                        subject=e.FreeVarExpr(varname), label=label\n                    ):\n                        result_tp, _ = ctx.varctx[varname]\n                        match result_tp:\n                            case e.NominalLinkTp(\n                                subject=_, name=name, linkprop=_\n                            ):\n                                type_def = mops.resolve_type_def(\n                                    ctx.schema, name\n                                )\n                            case e.NamedNominalLinkTp(name=name, linkprop=_):\n                                assert isinstance(\n                                    name, e.QualifiedName\n                                ), \"should have been resolved\"\n                                type_def = mops.resolve_type_def(\n                                    ctx.schema, name\n                                )\n                            case _:\n                                return False\n                        assert isinstance(type_def.typedef, e.ObjectTp)\n                        if label in type_def.typedef.val:\n                            if [\n                                c\n                                for c in type_def.constraints\n                                if isinstance(c, e.ExclusiveConstraint)\n                                and c.name == label\n                            ]:\n                                return True\n                            else:\n                                return False\n                        else:\n                            return False\n                    case _:\n                        return False\n\n            else:\n                return False\n\n        case _:\n            return False\n\n\ndef synthesize_type(ctx: e.TcCtx, expr: e.Expr) -> tuple[e.ResultTp, e.Expr]:\n    result_tp: e.Tp\n    result_card: e.CMMode\n    result_expr: e.Expr = expr  # by default we don't change expr\n\n    match expr:\n        case e.ScalarVal(_):\n            result_tp = synthesize_type_for_val(expr)\n            result_card = e.CardOne\n        case e.FreeVarExpr(var=var):\n            if var in ctx.varctx.keys():\n                result_tp, result_card = ctx.varctx[var]\n            else:\n                possible_resolved_name = mops.try_resolve_simple_name(\n                    ctx, e.UnqualifiedName(var)\n                )\n                if possible_resolved_name is not None:\n                    return synthesize_type(ctx, possible_resolved_name)\n                else:\n                    raise ValueError(\n                        \"Unknown variable\",\n                        var,\n                        \"list of known vars\",\n                        list(ctx.varctx.keys()),\n                    )\n        case e.QualifiedName(_):\n            module_entity = mops.try_resolve_module_entity(ctx, expr)\n            match module_entity:\n                case e.ModuleEntityTypeDef(typedef=typedef):\n                    assert isinstance(\n                        typedef, e.ObjectTp\n                    ), \"Cannot select Scalar type\"\n                    result_tp = e.NominalLinkTp(\n                        subject=typedef, name=expr, linkprop=e.ObjectTp({})\n                    )\n                    result_expr = e.MultiSetExpr(\n                        expr=[\n                            name\n                            for name in subtp_resol.find_all_subtypes_of_tp_in_schema(  # NoQA\n                                ctx.schema, expr\n                            )\n                            if not mops.tp_name_is_abstract(name, ctx.schema)\n                        ]\n                    )\n                    result_card = e.CardAny\n                case _:\n                    raise ValueError(\n                        \"Unsupported Module Entity\", module_entity\n                    )\n\n        case e.TypeCastExpr(tp=tp, arg=arg):\n            tp_ck = check_type_valid(ctx, tp)\n            if expr_tp_is_not_synthesizable(arg):\n                result_card, result_expr = check_type_no_card(ctx, arg, tp_ck)\n                result_tp = tp_ck\n            else:\n                (arg_tp, arg_v) = synthesize_type(ctx, arg)\n                candidate_cast = check_castable(ctx, arg_tp.tp, tp_ck)\n                if candidate_cast is not None:\n                    (result_tp, result_card) = (tp_ck, arg_tp.mode)\n                    result_expr = e.CheckedTypeCastExpr(\n                        cast_tp=(arg_tp.tp, tp_ck),\n                        cast_spec=candidate_cast,\n                        arg=arg_v,\n                    )\n                else:\n                    raise ValueError(\"Cannot cast\", arg_tp, tp_ck)\n        case e.ParameterExpr(name=name, tp=tp, is_required=is_required):\n            result_tp = check_type_valid(ctx, tp)\n            result_card = e.CardOne if is_required else e.CardAtMostOne\n            result_expr = e.ParameterExpr(\n                name=name, tp=result_tp, is_required=is_required\n            )\n\n        case e.ShapedExprExpr(expr=subject, shape=shape):\n            (subject_tp, subject_ck) = synthesize_type(ctx, subject)\n            result_tp, shape_ck = check_shape_transform(\n                ctx, shape, subject_tp.tp\n            )\n            if not eops.is_effect_free(shape):\n                raise ValueError(\"Shape should be effect free\", shape)\n            result_card = subject_tp.mode\n            result_expr = e.ShapedExprExpr(subject_ck, shape_ck)\n        case e.UnionExpr(left=l, right=r):\n            (l_tp, l_ck) = synthesize_type(ctx, l)\n            (r_tp, r_ck) = synthesize_type(ctx, r)\n            result_tp = tops.construct_tp_union(l_tp.tp, r_tp.tp)\n            result_card = l_tp.mode + r_tp.mode\n            result_expr = e.UnionExpr(l_ck, r_ck)\n        case e.FunAppExpr(fun=_, args=_, overloading_index=_):\n            (e_result_tp, e_ck) = func_call_checking(ctx, expr)\n            result_tp = e_result_tp.tp\n            result_card = e_result_tp.mode\n            result_expr = e_ck\n        case e.FreeObjectExpr():\n            result_tp = e.NominalLinkTp(\n                subject=e.ObjectTp({}),\n                name=e.QualifiedName([\"std\", \"FreeObject\"]),\n                linkprop=e.ObjectTp({}),\n            )\n            result_card = e.CardOne\n            result_expr = expr\n        case e.ConditionalDedupExpr(expr=inner):\n            (inner_tp, inner_ck) = synthesize_type(ctx, inner)\n            result_tp = inner_tp.tp\n            result_card = inner_tp.mode\n            result_expr = e.ConditionalDedupExpr(inner_ck)\n        case e.ObjectProjExpr(subject=subject, label=label):\n            (subject_tp, subject_ck) = synthesize_type(ctx, subject)\n            result_tp, result_card = tops.tp_project(\n                ctx, subject_tp, e.StrLabel(label)\n            )\n            # If the projection is a computable expression,\n            # project from the subject\n            if isinstance(result_tp, e.ComputableTp):\n                comp_expr = e.WithExpr(subject_ck, result_tp.expr)\n                result_expr = check_type(\n                    ctx, comp_expr, e.ResultTp(result_tp.tp, result_card)\n                )\n                result_tp = result_tp.tp\n            else:\n                if tops.is_tp_projection_tuple_proj(subject_tp.tp):\n                    result_expr = e.TupleProjExpr(subject_ck, label)\n                else:\n                    result_expr = e.ObjectProjExpr(subject_ck, label)\n        case e.LinkPropProjExpr(subject=subject, linkprop=lp):\n            (subject_tp, subject_ck) = synthesize_type(ctx, subject)\n            result_tp, result_card = tops.tp_project(\n                ctx, subject_tp, e.LinkPropLabel(lp)\n            )\n            if isinstance(result_tp, e.ComputableTp):\n                comp_expr = e.WithExpr(subject_ck, result_tp.expr)\n                result_expr = check_type(\n                    ctx, comp_expr, e.ResultTp(result_tp.tp, result_card)\n                )\n                result_tp = result_tp.tp\n            else:\n                result_expr = e.LinkPropProjExpr(subject_ck, lp)\n        case e.BackLinkExpr(subject=subject, label=label):\n            (_, subject_ck) = synthesize_type(ctx, subject)\n            candidates: list[e.NamedNominalLinkTp] = []\n            for t_name, name_def in mops.enumerate_all_object_type_defs(ctx):\n                for name_label, comp_tp in name_def.val.items():\n                    if name_label == label:\n                        match comp_tp.tp:\n                            case e.NamedNominalLinkTp(_):\n                                candidates = [\n                                    *candidates,\n                                    e.NamedNominalLinkTp(\n                                        t_name, comp_tp.tp.linkprop\n                                    ),\n                                ]\n                            case _:\n                                candidates = [\n                                    *candidates,\n                                    e.NamedNominalLinkTp(\n                                        t_name, e.ObjectTp({})\n                                    ),\n                                ]\n            result_expr = e.BackLinkExpr(subject_ck, label)\n            if len(candidates) == 0:\n                result_tp = e.AnyTp()\n            else:\n                result_tp = reduce(\n                    tops.construct_tp_union,  # type: ignore[arg-type]\n                    candidates,\n                )\n            result_card = e.CardAny\n        case e.IsTpExpr(subject=subject, tp=intersect_tp):\n            # intersect_tp = check_type_valid(ctx, intersect_tp)\n            if isinstance(intersect_tp, e.UncheckedTypeName):\n                intersect_tp_name, _ = mops.resolve_raw_name_and_type_def(\n                    ctx, intersect_tp.name\n                )\n            if isinstance(intersect_tp, e.AnyTp):\n                intersect_tp_name = e.QualifiedName(\n                    [\"std\", \"any\" + (intersect_tp.specifier or \"\")]\n                )\n            else:\n                assert isinstance(intersect_tp, e.RawName)  # type: ignore\n                intersect_tp_name, _ = mops.resolve_raw_name_and_type_def(\n                    ctx, intersect_tp  # type: ignore\n                )\n            (subject_tp, subject_ck) = synthesize_type(ctx, subject)\n            result_expr = e.IsTpExpr(subject_ck, intersect_tp_name)\n            result_card = subject_tp.mode\n            result_tp = e.BoolTp()\n        case e.TpIntersectExpr(subject=subject, tp=intersect_tp):\n            if isinstance(intersect_tp, e.UncheckedTypeName):\n                intersect_tp = intersect_tp.name\n            assert isinstance(intersect_tp, e.RawName)  # type: ignore\n            intersect_tp_name, _ = mops.resolve_raw_name_and_type_def(\n                ctx, intersect_tp  # type: ignore\n            )\n            (subject_tp, subject_ck) = synthesize_type(ctx, subject)\n            result_expr = e.TpIntersectExpr(subject_ck, intersect_tp_name)\n            if all(\n                isinstance(t, e.NamedNominalLinkTp)\n                for t in tops.collect_tp_union(subject_tp.tp)\n            ):\n                candidates = []\n                for t in tops.collect_tp_union(subject_tp.tp):\n                    assert isinstance(t, e.NamedNominalLinkTp)\n                    if t.name == intersect_tp_name:\n                        candidates = [*candidates, t]\n                if len(candidates) == 0:\n                    result_tp = tops.construct_tp_intersection(\n                        subject_tp.tp,\n                        e.NamedNominalLinkTp(\n                            name=intersect_tp_name, linkprop=e.ObjectTp({})\n                        ),\n                    )\n                else:\n                    result_tp = reduce(\n                        tops.construct_tp_union,  # type: ignore[arg-type]\n                        candidates,\n                    )\n            else:\n                result_tp = tops.construct_tp_intersection(\n                    subject_tp.tp,\n                    e.NamedNominalLinkTp(\n                        name=intersect_tp_name, linkprop=e.ObjectTp({})\n                    ),\n                )  # TODO: get linkprop\n            result_card = e.CMMode(\n                e.CardNumZero,\n                subject_tp.mode.upper,\n            )\n        case e.SubqueryExpr(expr=sub_expr):\n            (sub_expr_tp, sub_expr_ck) = synthesize_type(ctx, sub_expr)\n            result_expr = e.SubqueryExpr(sub_expr_ck)\n            result_tp = sub_expr_tp.tp\n            result_card = sub_expr_tp.mode\n        case e.DetachedExpr(expr=sub_expr):\n            (sub_expr_tp, sub_expr_ck) = synthesize_type(ctx, sub_expr)\n            result_expr = e.SubqueryExpr(sub_expr_ck)\n            result_tp = sub_expr_tp.tp\n            result_card = sub_expr_tp.mode\n        case e.WithExpr(bound=bound_expr, next=next_expr):\n            (bound_tp, bound_ck) = synthesize_type(ctx, bound_expr)\n            new_ctx, body, bound_var = eops.tcctx_add_binding(\n                ctx, next_expr, bound_tp\n            )\n            (next_tp, next_ck) = synthesize_type(new_ctx, body)\n            result_expr = e.WithExpr(\n                bound_ck, eops.abstract_over_expr(next_ck, bound_var)\n            )\n            result_tp, result_card = next_tp\n        case e.FilterOrderExpr(subject=subject, filter=filter, order=order):\n            (subject_tp, subject_ck) = synthesize_type(ctx, subject)\n            filter_ctx, filter_body, filter_bound_var = eops.tcctx_add_binding(\n                ctx, filter, e.ResultTp(subject_tp.tp, e.CardOne)\n            )\n\n            order_ck: dict[str, e.BindingExpr] = {}\n            for order_label, o in order.items():\n                order_ctx, order_body, order_bound_var = (\n                    eops.tcctx_add_binding(\n                        ctx, o, e.ResultTp(subject_tp.tp, e.CardOne)\n                    )\n                )\n                (_, o_ck) = synthesize_type(order_ctx, order_body)\n                order_ck = {\n                    **order_ck,\n                    order_label: eops.abstract_over_expr(\n                        o_ck, order_bound_var\n                    ),\n                }\n\n            assert eops.is_effect_free(filter), \"Expecting effect-free filter\"\n            assert all(\n                eops.is_effect_free(o) for o in order.values()\n            ), \"Expecting effect-free order\"\n\n            (_, filter_ck) = check_type_no_card(\n                filter_ctx, filter_body, e.BoolTp()\n            )\n\n            result_expr = e.FilterOrderExpr(\n                subject_ck,\n                eops.abstract_over_expr(filter_ck, filter_bound_var),\n                order_ck,\n            )\n            result_tp = subject_tp.tp\n            # pass cardinality if filter body can be determined to be true\n            if filter_body == e.BoolVal(True):\n                result_card = subject_tp.mode\n            elif check_filter_body_is_exclusive(filter_ctx, filter_ck):\n                result_card = e.CardAtMostOne\n            else:\n                result_card = e.CMMode(e.CardNumZero, subject_tp.mode.upper)\n        case e.OffsetLimitExpr(subject=subject, offset=offset, limit=limit):\n            (subject_tp, subject_ck) = synthesize_type(ctx, subject)\n            offset_mode, offset_ck = check_type_no_card(ctx, offset, e.IntTp())\n            limit_mode, limit_ck = check_type_no_card(ctx, limit, e.IntTp())\n            if offset_mode.upper == e.CardNumInf:\n                raise errors.QueryError(\"Offset must have cardinality (<=1)\")\n            if limit_mode.upper == e.CardNumInf:\n                raise errors.QueryError(\"Limit must have cardinality (<=1)\")\n            result_expr = e.OffsetLimitExpr(subject_ck, offset_ck, limit_ck)\n            result_tp = subject_tp.tp\n            if isinstance(limit_ck, e.ScalarVal):\n                v = limit_ck.val\n                assert isinstance(limit_ck.val, int), \"Expecting int\"\n                if v > 1:\n                    upper_card_bound = subject_tp.mode.upper\n                else:\n                    upper_card_bound = e.CardNumOne\n            else:\n                upper_card_bound = subject_tp.mode.upper\n\n            result_card = e.CMMode(\n                e.CardNumZero,\n                upper_card_bound,\n            )\n\n        case e.InsertExpr(name=_, new=arg):\n            result_expr = insert_checking(ctx, expr)\n            assert isinstance(result_expr, e.InsertExpr) and isinstance(\n                result_expr.name, e.QualifiedName\n            )\n            result_tp = e.NamedNominalLinkTp(\n                name=result_expr.name, linkprop=e.ObjectTp({})\n            )\n            result_card = e.CardOne\n        case e.UpdateExpr(subject=subject, shape=shape_expr):\n            (subject_tp, subject_ck) = synthesize_type(ctx, subject)\n            (shape_ck) = update_checking(ctx, shape_expr, subject_tp.tp)\n            result_expr = e.UpdateExpr(subject_ck, shape_ck)\n            result_tp, result_card = subject_tp\n        case e.DeleteExpr(subject=subject):\n            (subject_tp, subject_ck) = synthesize_type(ctx, subject)\n            assert eops.is_effect_free(\n                subject\n            ), \"Expecting subject expr to be effect-free\"\n            result_expr = e.DeleteExpr(subject_ck)\n            result_tp, result_card = subject_tp\n        case e.IfElseExpr(\n            then_branch=then_branch,\n            condition=condition,\n            else_branch=else_branch,\n        ):\n            (_, condition_ck) = check_type_no_card(\n                ctx, condition, e.ScalarTp(e.QualifiedName([\"std\", \"bool\"]))\n            )\n            then_tp, then_ck = synthesize_type(ctx, then_branch)\n            else_tp, else_ck = synthesize_type(ctx, else_branch)\n            result_tp = tops.construct_tp_union(then_tp.tp, else_tp.tp)\n            result_card = e.CMMode(\n                e.min_cardinal(then_tp.mode.lower, else_tp.mode.lower),\n                e.max_cardinal(then_tp.mode.upper, else_tp.mode.upper),\n            )\n            result_expr = e.IfElseExpr(\n                then_branch=then_ck,\n                condition=condition_ck,\n                else_branch=else_ck,\n            )\n        case e.ForExpr(bound=bound, next=next):\n            (bound_tp, bound_ck) = synthesize_type(ctx, bound)\n            new_ctx, next_body, bound_var = eops.tcctx_add_binding(\n                ctx, next, e.ResultTp(bound_tp.tp, e.CardOne)\n            )\n            (next_tp, next_ck) = synthesize_type(new_ctx, next_body)\n            result_expr = e.ForExpr(\n                bound=bound_ck,\n                next=eops.abstract_over_expr(next_ck, bound_var),\n            )\n            result_tp = next_tp.tp\n            result_card = next_tp.mode * bound_tp.mode\n        case e.OptionalForExpr(bound=bound, next=next):\n            (bound_tp, bound_ck) = synthesize_type(ctx, bound)\n            if bound_tp.mode.lower == e.CardNumZero:\n                bound_card = e.CardAtMostOne\n            elif bound_tp.mode.lower == e.CardNumOne:\n                bound_card = e.CardOne\n            else:\n                raise ValueError(\"Cannot have inf as lower bound\")\n            new_ctx, next_body, bound_var = eops.tcctx_add_binding(\n                ctx, next, e.ResultTp(bound_tp.tp, bound_card)\n            )\n            (next_tp, next_ck) = synthesize_type(new_ctx, next_body)\n            result_expr = e.OptionalForExpr(\n                bound=bound_ck,\n                next=eops.abstract_over_expr(next_ck, bound_var),\n            )\n            result_tp = next_tp.tp\n            result_card = next_tp.mode * e.CMMode(\n                e.CardNumOne, bound_tp.mode.upper\n            )\n        case e.UnnamedTupleExpr(val=arr):\n            [res_tps, cks] = zip(*[synthesize_type(ctx, v) for v in arr])\n            result_expr = e.UnnamedTupleExpr(list(cks))\n            [tps, cards] = zip(*res_tps)\n            result_tp = e.UnnamedTupleTp(list(tps))\n            result_card = reduce(operator.mul, cards, e.CardOne)\n        case e.NamedTupleExpr(val=arr):\n            [res_tps, cks] = zip(\n                *[synthesize_type(ctx, v) for _, v in arr.items()]\n            )\n            result_expr = e.NamedTupleExpr(\n                {k: c for k, c in zip(arr.keys(), cks)}\n            )\n            [tps, cards] = zip(*res_tps)\n            result_tp = e.NamedTupleTp({k: t for k, t in zip(arr.keys(), tps)})\n            result_card = reduce(operator.mul, cards, e.CardOne)\n        case e.ArrExpr(elems=arr):\n            if len(arr) == 0:\n                raise ValueError(\"Empty array does not support type synthesis\")\n            (first_tp, first_ck) = synthesize_type(ctx, arr[0])\n            if len(arr[1:]) > 0:\n                rest_card: Sequence[e.CMMode]\n                (rest_card, rest_cks) = zip(\n                    *[\n                        check_type_no_card(ctx, arr_elem, first_tp.tp)\n                        for arr_elem in arr[1:]\n                    ]\n                )\n            else:\n                rest_card = []\n                rest_cks = ()\n            # TODO: change to use unions\n            result_expr = e.ArrExpr([first_ck] + list(rest_cks))\n            result_tp = e.ArrTp(first_tp.tp)\n            result_card = reduce(\n                operator.mul, rest_card, first_tp.mode\n            )  # type: ignore[arg-type]\n        case e.MultiSetExpr(expr=arr):\n            if len(arr) == 0:\n                raise ValueError(\n                    \"Empty multiset does not support type synthesis\"\n                )\n            (first_tp, first_ck) = synthesize_type(ctx, arr[0])\n            if len(arr[1:]) == 0:\n                result_expr = e.MultiSetExpr([first_ck])\n                result_tp = first_tp.tp\n                result_card = first_tp.mode\n            else:\n                (rest_res_tps, rest_cks) = zip(\n                    *[synthesize_type(ctx, arr_elem) for arr_elem in arr[1:]]\n                )\n                rest_tps, rest_cards = zip(*rest_res_tps)\n                result_expr = e.MultiSetExpr([first_ck] + list(rest_cks))\n                result_tp = reduce(\n                    tops.construct_tp_union, rest_tps, first_tp.tp\n                )\n                result_card = reduce(\n                    operator.add, rest_cards, first_tp.mode\n                )  # type: ignore[arg-type]\n        case _:\n            raise ValueError(\"Not Implemented\", expr)\n\n    if isinstance(result_tp, e.ObjectTp):\n        raise ValueError(\n            \"Must return NominalLinkTp instead of object tp\", expr, result_tp\n        )\n    if isinstance(result_tp, e.UncheckedTypeName):\n        raise ValueError(\"Must not return UncheckedTypeName\", expr, result_tp)\n\n    return (e.ResultTp(result_tp, result_card), result_expr)\n\n\ndef expr_tp_is_not_synthesizable(expr: e.Expr) -> bool:\n    match expr:\n        case e.MultiSetExpr(expr=[]):\n            return True\n        case e.ArrExpr(elems=[]):\n            return True\n        case _:\n            return False\n\n\ndef check_type_no_card(\n    ctx: e.TcCtx, expr: e.Expr, tp: e.Tp, with_assignment_cast: bool = False\n) -> tuple[e.CMMode, e.Expr]:\n    match expr:\n        case e.MultiSetExpr(expr=[]):\n            return (e.CardAtMostOne, expr)\n        case e.ArrExpr(elems=[]):\n            return (e.CardAtMostOne, expr)\n        case _:\n            expr_tp, expr_ck = synthesize_type(ctx, expr)\n            default_return = (expr_tp.mode, expr_ck)\n            if tops.check_is_subtype(ctx, expr_tp.tp, tp):\n                return default_return\n            else:\n                if (expr_tp.tp, tp) in ctx.schema.casts:\n                    cast_fun = ctx.schema.casts[(expr_tp.tp, tp)]\n                    match cast_fun.kind:\n                        case e.TpCastKind.Explicit:\n                            raise ValueError(\"Not a sub type\", expr_tp.tp, tp)\n                        case e.TpCastKind.Implicit:\n                            return (\n                                expr_tp.mode,\n                                e.CheckedTypeCastExpr(\n                                    (expr_tp.tp, tp), cast_fun, expr_ck\n                                ),\n                            )\n                        case e.TpCastKind.Assignment:\n                            if with_assignment_cast:\n                                return (\n                                    expr_tp.mode,\n                                    e.CheckedTypeCastExpr(\n                                        (expr_tp.tp, tp), cast_fun, expr_ck\n                                    ),\n                                )\n                            else:\n                                raise ValueError(\n                                    \"Not a sub type\", expr_tp.tp, tp\n                                )\n                        case _:\n                            raise ValueError(\"Not Implemented\", cast_fun.kind)\n                else:\n                    raise ValueError(\n                        \"Not a sub type\", pp.show(expr_tp.tp), pp.show(tp)\n                    )\n\n\ndef check_type(\n    ctx: e.TcCtx,\n    expr: e.Expr,\n    tp: e.ResultTp,\n    with_assignment_cast: bool = False,\n) -> e.Expr:\n    synth_mode, expr_ck = check_type_no_card(\n        ctx, expr, tp.tp, with_assignment_cast\n    )\n    tops.assert_cardinal_subtype(synth_mode, tp.mode)\n    return expr_ck\n\n\ndef check_type_valid(ctx: e.TcCtx | e.DBSchema, tp: e.Tp) -> e.Tp:\n    \"\"\"\n    Check that a raw schema type is a valid type.\n    Returns the checked valid type.\n    \"\"\"\n    match tp:\n        case e.UncheckedTypeName(name=name):\n            resolved_name, resolved_tp = mops.resolve_raw_name_and_type_def(\n                ctx, name\n            )\n            match resolved_tp:\n                case e.ScalarTp(_):\n                    return resolved_tp\n                case e.ObjectTp(_):\n                    return e.NamedNominalLinkTp(\n                        name=resolved_name, linkprop=e.ObjectTp({})\n                    )\n                case _:\n                    raise ValueError(\"Not Implemented\", resolved_tp)\n        case e.AnyTp(_):\n            return tp\n        case e.CompositeTp(kind=kind, tps=tps, labels=labels):\n            return e.CompositeTp(\n                kind=kind,\n                tps=[check_type_valid(ctx, t) for t in tps],\n                labels=labels,\n            )\n        case e.QualifiedName(_):\n            return tp\n        case _:\n            raise ValueError(\"Not Implemented\", tp)\n"
  },
  {
    "path": "edb/tools/fake_ai_server.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport pathlib\nimport sys\n\nimport click\n\nfrom edb.tools.edb import edbcommands\n\nfrom edb.testbase import http as tb\n\n\n@edbcommands.command('fake-ai-server')\n@click.option(\n    '--port', type=int, default=0)\ndef fake_ai_server(*, port):\n    \"\"\"Run a fake AI embedding server\"\"\"\n\n    # Hmmmmmm.\n    tests_dir = pathlib.Path(__file__).parent.parent.parent / 'tests'\n    sys.path.append(str(tests_dir))\n    import test_ext_ai  # type: ignore\n\n    mock_server = tb.MockHttpServer(port=port)\n    mock_server.start()\n\n    base_url = mock_server.get_base_url().rstrip(\"/\")\n\n    mock_server.register_route_handler(\n        \"POST\",\n        base_url,\n        \"/v1/embeddings\",\n    )(test_ext_ai.TestExtAI.mock_api_embeddings)\n\n    print(\"Running on\", base_url)\n    print(\"Consider this config:\\n\")\n    print(test_ext_ai.TestExtAI.get_ai_config(base_url))\n"
  },
  {
    "path": "edb/tools/gen_cast_table.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport json\nimport subprocess\nimport sys\n\nfrom edb.tools.edb import edbcommands\n\n\n# NOTE: The types are HARDCODED here to trim the cast table to relevant\n# built-in types and the order of their appearance in the table, because it's\n# hard to group them otherwise.\n#\n# Please update this if new types need to be included.\nTYPES = [\n    'std::json',\n    'std::str',\n    'std::float32',\n    'std::float64',\n    'std::int16',\n    'std::int32',\n    'std::int64',\n    'std::bigint',\n    'std::decimal',\n    'std::bool',\n    'std::bytes',\n    'std::uuid',\n    'std::datetime',\n    'std::duration',\n    'std::cal::local_date',\n    'std::cal::local_datetime',\n    'std::cal::local_time',\n    'std::cal::relative_duration',\n    'std::cal::date_duration',\n    'std::anyenum',\n    'std::BaseObject',\n]\nTYPES_SET = set(TYPES)\n\n\ndef die(msg):\n    print(f'FATAL: {msg}', file=sys.stderr)\n    sys.exit(1)\n\n\ndef get_casts_to_type(target, impl_cast):\n    results = []\n    for source in TYPES:\n        cast = (source, target)\n        if impl_cast.get(cast):\n            results.append(cast)\n\n    return results\n\n\ndef is_reachable(source, target, impl_cast):\n    if source == target:\n        return True\n\n    casts = get_casts_to_type(target, impl_cast)\n    if not casts:\n        return False\n\n    sources = {c[0] for c in casts}\n\n    if source in sources:\n        return True\n    else:\n        reachable = any(is_reachable(source, s, impl_cast) for s in sources)\n\n        if reachable:\n            impl_cast[(source, target)] = True\n\n        return reachable\n\n\ndef get_all_casts(casts):\n    # Calculate the explicit, assignment, and implicit cast tables.\n    expl_cast = {}\n    assn_cast = {}\n    impl_cast = {}\n    for cast in casts:\n        source = cast['source']\n        target = cast['target']\n        if source in TYPES_SET and target in TYPES_SET:\n            expl_cast[(source, target)] = True\n            if cast['allow_assignment']:\n                assn_cast[(source, target)] = True\n            if cast['allow_implicit']:\n                assn_cast[(source, target)] = True\n                impl_cast[(source, target)] = True\n\n    # Implicit cast table needs to be recursively expanded from the\n    # starting casts.\n    for source in TYPES:\n        for target in TYPES:\n            is_reachable(source, target, impl_cast)\n\n    # HACK: We add the `uuid` -> `BaseObject` cast manually because it's\n    # currently missing from the casting table.\n    expl_cast[('std::uuid', 'std::BaseObject')] = True\n\n    return (expl_cast, assn_cast, impl_cast)\n\n\ndef render_type(name):\n    match name:\n        case 'std::anyenum':\n            return ':eql:type:`enum`'\n        case 'std::BaseObject':\n            return 'object'\n        case _:\n            return f':eql:type:`{name.split(\"::\")[1]} <{name}>`'\n\n\ndef main(casts):\n    expl_cast, assn_cast, impl_cast = get_all_casts(casts)\n\n    # Top row with all the scalars listed\n    code = []\n    line = ['from \\\\\\\\ to']\n    for target in TYPES:\n        line.append(render_type(target))\n    code.append(','.join(line))\n\n    for source in TYPES:\n        line = [render_type(source)]\n        for target in TYPES:\n            val = ''\n            if impl_cast.get((source, target)):\n                val = 'impl'\n            elif assn_cast.get((source, target)):\n                val = '``:=``'\n            elif expl_cast.get((source, target)):\n                if source in {\n                    'std::float32', 'std::float64'\n                } and target in {\n                    'std::int16', 'std::int32', 'std::int64', 'std::bigint'\n                }:\n                    val = '``<>*``'\n                else:\n                    val = '``<>``'\n            line.append(val)\n\n        code.append(','.join(line))\n\n    code = '\\n'.join(code) + '\\n'\n\n    print(code, end='')\n\n\n@edbcommands.command('gen-cast-table')\ndef gen_cast_table():\n    \"\"\"Generate a table of scalar casts to be used in the documentation.\n\n    NAME - at the moment there's only one option 'edgeql'\n    \"\"\"\n\n    try:\n        res = subprocess.run([\n            'edb',\n            'cli',\n            'query',\n            '-Fjson',\n\n            r\"\"\"\n            WITH MODULE schema\n            SELECT Cast {\n                source := .from_type.name,\n                target := .to_type.name,\n                allow_assignment,\n                allow_implicit,\n            }\n            FILTER all({.from_type, .to_type} IS ScalarType | ObjectType)\n            \"\"\",\n        ], capture_output=True)\n\n        if res.returncode != 0:\n            die('Could not connect to the dev Gel instance')\n\n        main(json.loads(res.stdout))\n    except Exception as ex:\n        die(str(ex))\n"
  },
  {
    "path": "edb/tools/gen_errors.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport builtins\nimport json\nimport pathlib\nimport re\nimport sys\nfrom dataclasses import dataclass\n\nimport click\n\nimport edb\n\nfrom edb.tools.edb import edbcommands\nfrom edb.errors import base as edb_base_errors\n\n\n@dataclass(frozen=True)\nclass ErrorCode:\n    b1: int\n    b2: int\n    b3: int\n    b4: int\n\n    def __iter__(self):\n        return iter((self.b1, self.b2, self.b3, self.b4))\n\n\n@dataclass(frozen=True)\nclass ErrorDescription:\n    name: str\n    code: ErrorCode\n    tags: frozenset\n    base_name: str = ''\n\n\nclass ErrorsTree:\n\n    python_errors = frozenset(\n        name for name in dir(builtins) if re.match(r'^\\w+Error$', name)\n    )\n\n    js_errors = frozenset({\n        'EvalError', 'InternalError', 'RangeError', 'ReferenceError',\n        'SyntaxError', 'TypeError', 'URIError',\n    })\n\n    ruby_errors = frozenset({\n        'NoMemoryError', 'ScriptError', 'LoadError', 'NotImplementedError',\n        'SyntaxError', 'SecurityError', 'SignalException', 'Interrupt',\n        'StandardError', 'ArgumentError', 'UncaughtThrowError',\n        'EncodingError', 'FiberError', 'IOError', 'EOFError', 'IndexError',\n        'KeyError', 'StopIteration', 'LocalJumpError', 'NameError',\n        'NoMethodError', 'RangeError', 'FloatDomainError', 'RegexpError',\n        'RuntimeError', 'SystemCallError', 'ThreadError', 'TypeError',\n        'ZeroDivisionError', 'SystemExit', 'SystemStackError',\n    })\n\n    # Normally in Java application code it is unlikely to throw/catch any of\n    # the below (application exceptions have \"Exception\" suffix), but\n    # let's try to avoid using these names anyways.\n    java_errors = frozenset({\n        'AssertionError', 'LinkageError', 'BootstrapMethodError',\n        'ClassCircularityError', 'ClassFormatError',\n        'UnsupportedClassVersionError', 'ExceptionInInitializerError',\n        'IncompatibleClassChangeError', 'AbstractMethodError',\n        'IllegalAccessError', 'InstantiationError', 'NoSuchFieldError',\n        'NoSuchMethodError', 'NoClassDefFoundError', 'UnsatisfiedLinkError',\n        'VerifyError', 'ThreadDeath', 'VirtualMachineError', 'InternalError',\n        'OutOfMemoryError', 'StackOverflowError', 'UnknownError',\n    })\n\n    scala_errors = frozenset({\n        'MatchError', 'NotImplementedError', 'UninitializedError',\n        'UninitializedFieldError', 'AbstractMethodError',\n    })\n\n    edgedb_base_errors = frozenset(\n        name for name in edb_base_errors.__all__\n        if re.match(r'^\\w+Error$', name)\n    )\n\n    # more on exceptions:\n    # * In C# and PHP built-in exceptions have an \"Exception\" suffix;\n    # * In C++, std::exceptions use snake_case.\n\n    errors_names = {\n        'Python': python_errors,\n        'JavaScript': js_errors,\n        'Ruby': ruby_errors,\n        'Java': java_errors,\n        'Scala': scala_errors,\n        'EdgeDB Base': edgedb_base_errors,\n    }\n\n    DEFAULT_BASE_IMPORT = 'from edb.errors.base import *'\n    DEFAULT_BASE_CLASS = 'EdgeDBError'\n    DEFAULT_MESSAGE_BASE_CLASS = 'EdgeDBMessage'\n    DEFAULT_EXTRA_ALL = 'base.__all__'\n\n    def __init__(self):\n        self._tree = {}\n        self._all_names = set()\n\n    def add(self, desc):\n        if desc.name in self._all_names:\n            raise ValueError(f'duplicate error name {desc.name!r}')\n\n        # Let's try to avoid name clashes with built-in exception\n        # names in some popular languages\n        for lang, names in self.errors_names.items():\n            if desc.name in names:\n                raise ValueError(f'error name {desc.name!r} conflicts with '\n                                 f'{lang} exception')\n\n        if desc.code in self._tree:\n            raise ValueError(f'duplicate error code for error {desc.name!r}')\n\n        self._all_names.add(desc.name)\n        self._tree[desc.code] = desc\n\n    def load(self, ep):\n        with open(ep, 'rt') as f:\n            self._load(f)\n\n    def _load(self, f):\n        for line in f.readlines():\n            if re.match(r'(?x)^ (\\s*\\#[^\\n]*) | (\\s*) $', line):\n                continue\n\n            # For consistency we require a very particular format\n            # for error codes (hex numbers) and error names\n            # (camel case, only words, ends with \"Error\").\n            m = re.match(\n                r'''(?x)^\n                    0x_(?P<b1>[0-9A-F]{2})_\n                       (?P<b2>[0-9A-F]{2})_\n                       (?P<b3>[0-9A-F]{2})_\n                       (?P<b4>[0-9A-F]{2})\n\n                    \\s+\n                    (?P<name>[A-Z][a-zA-Z]+(?:Error|Message))\n                    (?P<tags>(?:\\s+\\#[A-Z_]+)*)\n                    \\s*\n                $''',\n                line\n            )\n\n            if not m:\n                die(f'Unable to parse {line!r} line')\n\n            code = ErrorCode(\n                int(m.group('b1'), 16),\n                int(m.group('b2'), 16),\n                int(m.group('b3'), 16),\n                int(m.group('b4'), 16),\n            )\n            name = m.group('name')\n            tags = m.group('tags').split()\n            tags = frozenset(t.strip().lstrip('#') for t in tags)\n            desc = ErrorDescription(name=name, code=code, tags=tags)\n\n            self.add(desc)\n\n    def get_parent(self, code):\n        b1, b2, b3, b4 = code\n\n        if b4 == 0 and b3 == 0 and b2 == 0:\n            return None\n\n        if b4 == 0 and b3 == 0:\n            parent_code = ErrorCode(b1, 0, 0, 0)\n        elif b4 == 0:\n            parent_code = ErrorCode(b1, b2, 0, 0)\n        else:\n            parent_code = ErrorCode(b1, b2, b3, 0)\n\n        try:\n            return self._tree[parent_code]\n        except KeyError:\n            raise ValueError(f'No base class for code '\n                             f'0x_{b1:0>2X}_{b2:0>2X}_{b3:0>2X}_{b4:0>2X}')\n\n    def generate_classes(self, *, message_base_class, base_class, client):\n        classes = []\n\n        for desc in self._tree.values():\n            if desc.code.b1 == 0xFF and not client:\n                continue\n\n            parent = self.get_parent(desc.code)\n\n            if parent:\n                base_name = parent.name\n            elif desc.name.endswith('Error'):\n                base_name = base_class\n            else:\n                base_name = message_base_class\n\n            tags = desc.tags\n            while parent:\n                tags |= parent.tags\n                parent = self.get_parent(parent.code)\n\n            # make tag list order stable\n            tags = sorted(tags)\n            b1, b2, b3, b4 = desc.code\n\n            classes.append((desc.name, base_name, b1, b2, b3, b4, tags))\n\n        return classes\n\n    def generate_pycode(\n        self, *, message_base_class, base_class, base_import, extra_all, client\n    ):\n        classes = self.generate_classes(\n            message_base_class=message_base_class,\n            base_class=base_class,\n            client=client)\n\n        lines = []\n        all_lines = []\n        for name, base, i1, i2, i3, i4, tags in classes:\n            all_lines.append(name)\n            klass = (\n                f'class {name}({base}):\\n'\n                f'    _code = 0x_{i1:0>2X}_{i2:0>2X}_{i3:0>2X}_{i4:0>2X}'\n            )\n            if client and tags:\n                tag_list = \", \".join(sorted(tags))\n                klass += f'\\n    tags = frozenset({{{tag_list}}})'\n            lines.append(klass)\n\n        lines = '\\n\\n\\n'.join(lines)\n\n        all_lines = '    ' + ',\\n    '.join(repr(ln) for ln in all_lines) + ','\n        all_lines = (\n            f'__all__ = {extra_all} + (  # type: ignore\\n'\n            f'{all_lines}\\n)'\n        )\n\n        code = (\n            f'{base_import}'\n            f'\\n\\n\\n'\n            f'{all_lines}'\n            f'\\n\\n\\n'\n            f'{lines}'\n        )\n\n        return code\n\n\ndef die(msg):\n    print(f'FATAL: {msg}', file=sys.stderr)\n    sys.exit(1)\n\n\ndef main(\n    *,\n    base_class,\n    message_base_class,\n    base_import,\n    stdout,\n    extra_all,\n    client,\n    language,\n):\n\n    for p in edb.__path__:\n        ep = pathlib.Path(p) / 'api' / 'errors.txt'\n        if ep.exists():\n            out_fn = pathlib.Path(p) / 'errors' / '__init__.py'\n            break\n    else:\n        die('Unable to find the \"edb/api/errors.txt\" file')\n\n    tree = ErrorsTree()\n    tree.load(ep)\n\n    code = tree.generate_pycode(base_class=base_class,\n                                message_base_class=message_base_class,\n                                base_import=base_import,\n                                extra_all=extra_all,\n                                client=client)\n\n    cmd_line = '#    $ edb gen-errors'\n    if base_class != ErrorsTree.DEFAULT_BASE_CLASS:\n        cmd_line += f' \\\\\\n#        --base-class \"{base_class}\"'\n    if message_base_class != ErrorsTree.DEFAULT_MESSAGE_BASE_CLASS:\n        cmd_line += \\\n            f' \\\\\\n#        --message-base-class \"{message_base_class}\"'\n    if base_import != ErrorsTree.DEFAULT_BASE_IMPORT:\n        cmd_line += f' \\\\\\n#        --import {repr(base_import)}'\n    if extra_all != ErrorsTree.DEFAULT_EXTRA_ALL:\n        cmd_line += f' \\\\\\n#        --extra-all \"{extra_all}\"'\n    if stdout:\n        cmd_line += f' \\\\\\n#        --stdout'\n    if client:\n        cmd_line += f' \\\\\\n#        --client'\n\n    code = (\n        f'# AUTOGENERATED FROM \"edb/api/errors.txt\" WITH\\n'\n        f'{cmd_line}'\n        f'\\n\\n\\n'\n        f'# flake8: noqa'\n        f'\\n\\n\\n'\n        f'{code}'\n        f'\\n'\n    )\n\n    if stdout:\n        print(code)\n    else:\n        with open(out_fn, 'wt') as f:\n            f.write(code)\n\n\n@edbcommands.command('gen-errors')\n@click.option(\n    '--base-class', type=str, default=ErrorsTree.DEFAULT_BASE_CLASS)\n@click.option(\n    '--message-base-class', type=str,\n    default=ErrorsTree.DEFAULT_MESSAGE_BASE_CLASS)\n@click.option(\n    '--import', 'base_import', type=str,\n    default=ErrorsTree.DEFAULT_BASE_IMPORT)\n@click.option(\n    '--extra-all', type=str, default=ErrorsTree.DEFAULT_EXTRA_ALL)\n@click.option(\n    '--stdout', type=bool, default=False, is_flag=True)\n@click.option(\n    '--client', type=bool, default=False, is_flag=True)\ndef gen_errors(*, base_class, message_base_class, base_import,\n               stdout, extra_all, client):\n    \"\"\"Generate edb/errors.py from edb/api/errors.txt\"\"\"\n    try:\n        main(base_class=base_class,\n             message_base_class=message_base_class,\n             base_import=base_import,\n             stdout=stdout,\n             extra_all=extra_all,\n             client=client,\n             language='python')\n    except Exception as ex:\n        die(str(ex))\n\n\n@edbcommands.command('gen-errors-json')\n@click.option(\n    '--client', type=bool, default=False, is_flag=True)\ndef gen_errors_json(*, client):\n    \"\"\"Generate JSON from edb/api/errors.txt\"\"\"\n    for p in edb.__path__:\n        ep = pathlib.Path(p) / 'api' / 'errors.txt'\n        if ep.exists():\n            break\n    else:\n        die('Unable to find the \"edb/api/errors.txt\" file')\n\n    try:\n        tree = ErrorsTree()\n        tree.load(ep)\n\n        clss = tree.generate_classes(\n            message_base_class=None, base_class=None, client=client)\n        print(json.dumps(clss))\n    except Exception as ex:\n        die(str(ex))\n"
  },
  {
    "path": "edb/tools/gen_meta_grammars.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport click\nimport collections\nimport json\nimport subprocess\nimport sys\n\nfrom edb.edgeql.parser.grammar import keywords as eql_keywords\nfrom edb.schema import schema as s_schema\nfrom edb.tools.edb import edbcommands\n\n\nBOOL_LITERALS = {'true', 'false'}\nSPECIAL_TYPES = {'array', 'tuple', 'enum', 'range', 'multirange'}\nNAMES = {'edgeql'}\nNAVIGATION = ('.<', '.>', '@', '.')\n\n\ndef die(msg):\n    print(f'FATAL: {msg}', file=sys.stderr)\n    sys.exit(1)\n\n\ndef gen_grammar_class(name, components):\n    code = [f'class {name}:\\n']\n    for varname, values in components.items():\n        code.append(f'    {varname} = (\\n')\n        code.extend([f'        \"{val}\",\\n' for val in values])\n        code.append(f'    )\\n')\n\n    return code\n\n\ndef main(names, data):\n    code = [\n        f'# AUTOGENERATED BY Gel WITH\\n'\n        f'#     $ edb gen-meta-grammars {\" \".join(names)}\\n'\n        f'\\n\\n'\n        f'from __future__ import annotations\\n'\n        f'\\n\\n'\n    ]\n\n    # add builtins\n    types = set(data['t_names'])\n    types |= SPECIAL_TYPES\n    types = sorted(types)\n\n    modules = set()\n    for m in s_schema.STD_MODULES:\n        mod = str(m)\n        if not (mod.startswith('__') or 'test' in mod):\n            # we want each individual module name component separately\n            modules |= set(mod.split('::'))\n    constraints = sorted(set(data['c_names']))\n    fn_builtins = sorted(set(data['f_names']))\n    index_builtins = sorted(set(data['i_names']))\n    # add non-word operators\n    operators = sorted((set(data['o_names']) - {'[]'}) | {':='})\n\n    for gname in names:\n        if gname == 'edgeql':\n            code.extend(gen_grammar_class(\n                'EdgeQL',\n                collections.OrderedDict(\n                    reserved_keywords=sorted(\n                        eql_keywords.reserved_keywords.union(\n                            eql_keywords.partial_reserved_keywords\n                        ) - BOOL_LITERALS),\n                    unreserved_keywords=sorted(\n                        eql_keywords.unreserved_keywords - BOOL_LITERALS),\n                    bool_literals=sorted(BOOL_LITERALS),\n                    type_builtins=types,\n                    module_builtins=(sorted(modules)),\n                    constraint_builtins=constraints,\n                    fn_builtins=fn_builtins,\n                    index_builtins=index_builtins,\n                    operators=operators,\n                    navigation=NAVIGATION,\n                )\n            ))\n\n        code.append('\\n\\n')\n\n    code = ''.join(code).strip() + '\\n'\n\n    print(code, end='')\n\n\n@edbcommands.command('gen-meta-grammars')\n@click.argument('names', required=True, nargs=-1, metavar='NAME...')\ndef gen_meta_grammars(names):\n    \"\"\"Generate keywords, builtins, operators, etc. which can be used\n    for EdgeQL and SDL grammars.\n\n    NAME - at the moment there's only one option 'edgeql'\n    \"\"\"\n\n    if names:\n        for name in names:\n            if name not in NAMES:\n                die(f'{name} is not a valid NAME')\n\n        if len(names) > 2:\n            die(f'too many NAMES')\n\n    try:\n        res = subprocess.run([\n            'edb',\n            'cli',\n            'query',\n            '-Fjson',\n\n            r\"\"\"\n            WITH\n                MODULE schema,\n                T := (SELECT Type\n                      FILTER Type IS (PseudoType | ScalarType | ObjectType)),\n                t_names := (\n                    SELECT re_match(r'(?:.*::)?(.+)', T.name)[0]\n                    FILTER re_test(r\"^(?:std|math|cal|fts|pg)::\", T.name)\n                ),\n                c_names := re_match(\n                    r\"(?:std|sys|math)::([a-zA-Z]\\w+$)\",\n                    DISTINCT `Constraint`.name\n                )[0],\n                f_names := re_match(\n                    r\"(?:std|sys|math|cal|fts|pg)::([a-zA-Z]\\w+$)\",\n                    DISTINCT `Function`.name\n                ),\n                o_names := (\n                    SELECT _ := DISTINCT Operator.name[5:]\n                    FILTER not re_test(r\"^[a-zA-Z ]+$\", _)\n                    ORDER BY _\n                ),\n                i_names := re_match(\n                    r\"(?:std|sys|math|cal|fts|pg)::([a-zA-Z]\\w+$)\",\n                    DISTINCT `Index`.name\n                )[0],\n            SELECT {\n                t_names := t_names,\n                c_names := c_names,\n                f_names := f_names[0] if len(f_names) = 1 else '',\n                o_names := o_names,\n                i_names := i_names,\n            }\n            \"\"\",\n        ], capture_output=True)\n\n        if res.returncode != 0:\n            die('Could not connect to the dev Gel instance')\n\n        main(names, (json.loads(res.stdout))[0])\n    except Exception as ex:\n        die(str(ex))\n"
  },
  {
    "path": "edb/tools/gen_rust_ast.py",
    "content": "import itertools\nimport typing\nimport dataclasses\nimport textwrap\nfrom itertools import chain\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes\nfrom edb.common.ast import base as ast\nfrom edb.common import enum as s_enum\nfrom edb.common import typing_inspect\nfrom edb.tools.edb import edbcommands\n\n\n@dataclasses.dataclass()\nclass ASTClass:\n    name: str\n    typ: typing.Any\n    children: list[type] = dataclasses.field(default_factory=list)\n\n\n@dataclasses.dataclass()\nclass ASTUnion:\n    name: str\n    variants: typing.Sequence[type | str]\n    for_composition: bool\n\n\n# a queue for union types that are to be generated\nunion_types: list[ASTUnion] = []\n\n# all discovered AST classes\nast_classes: dict[str, ASTClass] = {}\n\n\n@edbcommands.command(\"gen-rust-ast\")\ndef main() -> None:\n    f = open('edb/edgeql-parser/src/ast.rs', 'w')\n\n    f.write(\n        textwrap.dedent(\n            '''\\\n            // DO NOT EDIT. This file was generated with:\n            //\n            // $ edb gen-rust-ast\n\n            //! Abstract Syntax Tree for EdgeQL\n            #![allow(non_camel_case_types)]\n\n            use indexmap::IndexMap;\n\n            #[cfg(feature = \"python\")]\n            use edgeql_parser_derive::IntoPython;\n            '''\n        )\n    )\n\n    # discover all nodes\n    for name, typ in qlast.__dict__.items():\n        if not isinstance(typ, type) or not hasattr(typ, '_direct_fields'):\n            continue\n\n        if not issubclass(typ, qlast.Base):\n            continue\n\n        # re-run field collection to correctly handle forward-references\n        typ = typ._collect_direct_fields()  # type: ignore\n\n        ast_classes[typ.__name__] = ASTClass(name=name, typ=typ)\n\n    # build inheritance graph\n    for ast_class in ast_classes.values():\n        for base in ast_class.typ.__bases__:\n            if base.__name__ not in ast_classes:\n                continue\n            ast_classes[base.__name__].children.append(ast_class.typ)\n\n    # generate structs\n    for ast_class in ast_classes.values():\n        f.write(codegen_struct(ast_class))\n\n        while len(union_types) > 0:\n            f.write(codegen_union(union_types.pop(0)))\n\n    # generate enums\n    for name, typ in chain(qlast.__dict__.items(), qltypes.__dict__.items()):\n\n        if not isinstance(typ, type) or not issubclass(typ, s_enum.StrEnum):\n            continue\n\n        f.write(codegen_enum(name, typ))\n\n\ndef codegen_struct(cls: ASTClass) -> str:\n    field_names = set()\n    fields = ''\n    doc_comment = ''\n\n    for f in typing.cast(list[ast._Field], cls.typ._direct_fields):\n\n        if f.hidden:\n            continue\n\n        union_name = f'{cls.name}{title_case(f.name)}'\n\n        typ = translate_type(f.type, union_name, False)\n        if hasattr(cls.typ, '__rust_box__') and f.name in cls.typ.__rust_box__:\n            typ = f'Box<{typ}>'\n\n        f_name = quote_rust_ident(f.name)\n        field_names.add(f_name)\n\n        fields += f'    pub {f_name}: {typ},\\n'\n\n    if len(cls.children) > 0:\n\n        # find an unused name for the py_child field\n        for i in itertools.count(0, 1):\n            kind_name = 'kind' if i == 0 else f'kind{i}'\n            if kind_name not in field_names:\n                break\n\n        name = f'{cls.name}Kind'\n        variants: typing.Sequence[type | str] = cls.children\n\n        union_types.append(\n            ASTUnion(name=name, variants=variants, for_composition=True)\n        )\n\n        if cls.typ.__abstract_node__:\n            field_type = name\n        else:\n            field_type = f'Option<{name}>'\n\n        fields += (\n            f'    #[cfg_attr(feature = \"python\", py_child)]\\n'\n            f'    pub {kind_name}: {field_type},\\n'\n        )\n\n    return (\n        f'\\n{doc_comment}'\n        + f'#[derive(Debug, Clone)]\\n'\n        + f'#[cfg_attr(feature = \"python\", derive(IntoPython))]\\n'\n        + f'pub struct {cls.name} {\"{\"}\\n'\n        + fields\n        + '}\\n'\n    ).replace('{\\n}', r'{}')\n\n\ndef codegen_enum(name: str, cls: typing.Any) -> str:\n    fields = ''\n    for member in cls._member_names_:\n        fields += f'    {member},\\n'\n\n    if cls.__module__ == 'edb.edgeql.ast':\n        cls_path = f'qlast.{cls.__name__}'\n    elif cls.__module__ == 'edb.edgeql.qltypes':\n        cls_path = f'qltypes.{cls.__name__}'\n    else:\n        raise LookupError(\n            'we only support generating AST from qlast and qltypes modules'\n        )\n\n    return (\n        '\\n#[derive(Debug, Clone)]\\n'\n        + f'#[cfg_attr(feature = \"python\", derive(IntoPython))]\\n'\n        + f'#[cfg_attr(feature = \"python\", py_enum({cls_path}))]\\n'\n        + f'pub enum {name} {\"{\"}\\n'\n        + fields\n        + '}\\n'\n    )\n\n\ndef quote_rust_ident(name: str) -> str:\n    if name in {'type', 'where', 'ref', 'final', 'abstract'}:\n        return 'r#' + name\n    return name\n\n\ndef title_case(name: str) -> str:\n    return name[0].upper() + name[1:]\n\n\ndef codegen_union(union: ASTUnion) -> str:\n    fields = ''\n    for arg in union.variants:\n        if isinstance(arg, str):\n            fields += f'    {arg},\\n'\n        else:\n            typ = translate_type(arg, '???', union.for_composition)\n            fields += f'    {arg.__name__}({typ}),\\n'\n\n    attr = 'py_child' if union.for_composition else 'py_union'\n\n    return (\n        '\\n#[derive(Debug, Clone)]\\n'\n        f'#[cfg_attr(feature = \"python\", derive(IntoPython))]\\n'\n        f'#[cfg_attr(feature = \"python\", {attr})]\\n'\n        f'pub enum {union.name} {\"{\"}\\n{fields}{\"}\"}\\n'\n    )\n\n\ndef translate_type(\n    typ: typing.Any, union_name: str, for_composition: bool\n) -> str:\n    params = [\n        translate_type(param, union_name, for_composition)\n        for param in typing_inspect.get_args(typ)\n    ]\n\n    if typing_inspect.is_union_type(typ):\n\n        if hasattr(typ, '_name') and typ._name == 'Optional':\n            return f'Option<{params[0]}>'\n\n        union_types.append(\n            ASTUnion(\n                name=union_name,\n                variants=typing_inspect.get_args(typ),\n                for_composition=for_composition,\n            )\n        )\n        return union_name\n\n    if typing_inspect.is_generic_type(typ) and hasattr(typ, '_name'):\n\n        if typ._name in ('List', 'Sequence'):\n            return f'Vec<{params[0]}>'\n\n        if typ._name == 'Dict':\n            return f'IndexMap<{params[0]}, {params[1]}>'\n\n    if not hasattr(typ, '__name__'):\n        return str(typ)\n\n    if typ.__name__ == 'Tuple' and typ.__module__ == 'typing':\n        if len(params) > 0 and params[1] == 'Ellipsis':\n            return f'Vec<{params[0]}>'\n        else:\n            return '(' + ', '.join(params) + ')'\n\n    mappings = {\n        'str': 'String',\n        'bool': 'bool',\n        'int': 'i64',\n        'float': 'f64',\n        'Expr': 'Box<Expr>',\n        'NoneType': '()',\n        'bytes': 'Vec<u8>',\n    }\n\n    if typ.__name__ in mappings:\n        return mappings[typ.__name__]\n\n    if typ.__module__ not in ('edb.edgeql.ast', 'edb.edgeql.qltypes'):\n        raise NotImplementedError(f'cannot translate: {typ}')\n\n    if for_composition or typ.__name__ not in ast_classes:\n        return typ.__name__\n\n    ancestor = find_covering_ancestor(\n        typ, set(f.name for f in typ._fields.values() if not f.hidden)\n    )\n    return ancestor.__name__\n\n\ndef find_covering_ancestor(typ: type, fields: set[str]):\n    # In Rust, a type will not inherit fields from parent types.\n    # This means that we need to omit some ancestor of this type, which\n    # would include all fields of the type.\n    # We lose a bit of type checking strictness here.\n    for parent in typ.__mro__:\n        if not hasattr(parent, '_direct_fields'):\n            continue\n\n        fields = fields.difference((f.name for f in parent._direct_fields))\n        if len(fields) == 0:\n            return parent\n    raise AssertionError('unreachable')\n"
  },
  {
    "path": "edb/tools/gen_sql_introspection.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport textwrap\nfrom typing import Mapping\n\nfrom edb.tools.edb import edbcommands\n\n\nasync def run():\n    import asyncpg\n\n    schemas_by_version = {}\n\n    # docker run -p 5413:5432 --rm -e POSTGRES_PASSWORD=pass postgres:13\n    # docker run -p 5414:5432 --rm -e POSTGRES_PASSWORD=pass postgres:14\n    # docker run -p 5415:5432 --rm -e POSTGRES_PASSWORD=pass postgres:15\n    # docker run -p 5416:5432 --rm -e POSTGRES_PASSWORD=pass postgres:16\n    # docker run -p 5417:5432 --rm -e POSTGRES_PASSWORD=pass postgres:17\n    for port in [5413, 5414, 5415, 5416, 5417]:\n        await query_instance(\n            await asyncpg.connect(\n                host='localhost',\n                database='postgres',\n                user='postgres',\n                password='pass',\n                port=str(port),\n            ),\n            schemas_by_version,\n        )\n\n    with open('./edb/pgsql/resolver/sql_introspection.py', 'w') as file:\n        print_header(file)\n        print_schema(file, \"information_schema\", schemas_by_version)\n        print_schema(file, \"pg_catalog\", schemas_by_version)\n\n    print('Done.')\n\n\nasync def query_instance(c, schemas_by_version):\n    import asyncpg\n\n    assert isinstance(c, asyncpg.Connection)\n\n    [[version_string]] = await c.fetch('SELECT version()')\n    assert isinstance(version_string, str)\n    version_string = version_string.removeprefix('PostgreSQL ')\n    version_major = int(version_string.split(' ')[0].split('.')[0])\n\n    print(f'querying PostgreSQL {version_major}')\n\n    [[res]] = await c.fetch(\n        r'''\n    WITH\n    columns AS (\n        SELECT\n            table_schema,\n            table_name,\n            TO_JSON(ARRAY [\n                column_name, COALESCE(domain_name, data_type)\n            ]) AS col\n        FROM information_schema.columns\n        WHERE (\n            table_schema = 'information_schema' AND table_name NOT LIKE '\\_pg%'\n        ) OR (\n            table_schema = 'pg_catalog'\n        )\n        ORDER BY table_schema, table_name, ordinal_position\n    ),\n    tables AS (\n        SELECT table_schema, table_name, JSON_AGG(col) as cols\n        FROM columns\n        GROUP BY table_schema, table_name\n    ),\n    schemas AS (\n        SELECT table_schema, JSON_OBJECT_AGG(table_name, cols) as tables\n        FROM tables\n        GROUP BY table_schema\n    )\n    SELECT JSON_OBJECT_AGG(table_schema, tables)\n    FROM schemas\n    '''\n    )\n\n    schemas_by_version[version_major] = json.loads(res)\n\n\ndef print_header(f):\n    print(\n        textwrap.dedent(\n            '''\n        # AUTOGENERATED FROM _localdev postgres instance WITH\n        #    $ edb gen-sql-introspection\n\n        \"\"\"Declarations of information schema and pg_catalog\"\"\"\n\n        from typing import Tuple, Dict, List\n\n        ColumnName = str\n        ColumnType = str | None\n        '''\n        )[1:],\n        file=f,\n    )\n\n\ndef print_schema(\n    f,\n    schema_name: str,\n    schemas_by_version: Mapping[\n        int, Mapping[str, Mapping[str, list[tuple[str, str]]]]\n    ],\n):\n    \"\"\"\n    Generates Python dict source for tables of a given PostgreSQL schema and\n    writes it into a file. Param `tables` can contain data for more than one\n    version of PostgreSQL. This function will generate code for the latest\n    PostgreSQL version and search previous versions to determine the first\n    version that contains each column.\n    \"\"\"\n    print(f'Code generation of schema \"{schema_name}\"')\n\n    version_latest = max(iter(schemas_by_version.keys()))\n    versions_desc = list(schemas_by_version.keys())\n    versions_desc.sort(reverse=True)\n    versions_desc = versions_desc[1:]\n\n    schemas_latest = schemas_by_version[version_latest]\n    tables_latest = schemas_latest[schema_name]\n\n    typ = ': Dict[str, List[Tuple[ColumnName, ColumnType, int]]]'\n    print(schema_name.upper() + typ + \" = {\", file=f)\n    for index, (table, columns) in enumerate(tables_latest.items()):\n        print(f'    \"{table}\": [', file=f)\n        for [col_name, col_typ] in columns:\n\n            ver_since = version_latest\n            for v in versions_desc:\n                schema = schemas_by_version.get(v)\n                assert schema\n                tbls = schema.get(schema_name)\n                assert tbls\n                tbl = tbls.get(table, None)\n                if tbl is None:\n                    break\n                c = next((True for c, _ in tbl if c == col_name), False)\n                if not c:\n                    break\n                ver_since = v\n\n            if col_typ == \"ARRAY\" or col_typ.startswith(\"any\"):\n                col_typ = \"None\"\n            else:\n                col_typ = col_typ.replace('\"', '\\\\\"')\n                col_typ = f'\"{col_typ}\"'\n            print(f'        (\"{col_name}\", {col_typ}, {ver_since}),', file=f)\n\n        last = index == len(tables_latest) - 1\n        comma = ',' if not last else ''\n        print(f'    ]{comma}', file=f)\n    print('}', file=f)\n\n\n@edbcommands.command(\"gen-sql-introspection\")\ndef main():\n    asyncio.run(run())\n"
  },
  {
    "path": "edb/tools/gen_test_dumps.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any\n\nimport asyncio\nimport os\nimport pathlib\nimport shutil\nimport sys\nimport tempfile\nimport unittest\n\nimport click\n\nfrom edb import buildmeta\nfrom edb.testbase import cluster as edgedb_cluster\nfrom edb.testbase import server as tb\nfrom edb.tools.edb import edbcommands\n\n\nclass TestResult:\n    def wasSuccessful(self):\n        return True\n\n\nclass TestRunner:\n    def __init__(self):\n        self.cases = set()\n\n    def run(self, test):\n        self.cases.update(tb.get_test_cases([test]))\n        return TestResult()\n\n\nasync def execute(\n    tests_dir: str,\n    conn: dict[str, Any],\n    num_workers: int,\n    version: str,\n) -> None:\n    runner = TestRunner()\n    unittest.main(\n        module=None,\n        argv=[\"unittest\", \"discover\", \"-s\", tests_dir],\n        testRunner=runner,\n        exit=False,\n    )\n\n    setup_scripts = tb.get_test_cases_setup(runner.cases)\n    dump_cases = {\n        db_name: case\n        for case, db_name, _ss in setup_scripts\n        if getattr(case, \"STABLE_DUMP\", False)\n    }\n    await tb.setup_test_cases(list(dump_cases.values()), conn, num_workers)\n\n    dumps_dir = pathlib.Path(tests_dir) / \"dumps\"\n    db_friendly_version = version.split(\"+\", 1)[0]\n    db_friendly_version = db_friendly_version.replace(\"-alpha.\", \"a\")\n    db_friendly_version = db_friendly_version.replace(\"-beta.\", \"b\")\n    db_friendly_version = db_friendly_version.replace(\"-rc.\", \"rc\")\n    db_friendly_version = db_friendly_version.replace(\"-\", \"_\")\n    db_friendly_version = db_friendly_version.replace(\".\", \"_\")\n    for db_name in dump_cases:\n        with tempfile.NamedTemporaryFile() as f:\n            tb.CLITestCaseMixin.run_cli_on_connection(\n                conn, \"-d\", db_name, \"dump\", f.name\n            )\n            db_dumps_dir = dumps_dir / db_name\n            db_dumps_dir.mkdir(exist_ok=True)\n            dump_p = (db_dumps_dir / db_friendly_version).with_suffix(\".dump\")\n            shutil.copy(f.name, dump_p)\n            print(f\"Dumped {dump_p}\")\n\n\ndef die(msg):\n    print(f\"FATAL: {msg}\", file=sys.stderr)\n    sys.exit(1)\n\n\n@edbcommands.command(\"gen-test-dumps\")\n@click.option(\n    \"-t\",\n    \"--tests-dir\",\n    type=str,\n    default=str(\n        pathlib.Path(__file__).parent.parent.parent.resolve() / \"tests\"\n    ),\n    help=\"directory to start dump test discovery from\",\n)\n@click.option(\n    \"-j\",\n    \"--jobs\",\n    type=int,\n    default=lambda: round((os.cpu_count() or 1) * 0.75),\n    help=\"number of parallel processes to use\",\n)\ndef gen_test_dumps(*, jobs, tests_dir):\n    if not jobs:\n        jobs = os.cpu_count()\n\n    with tempfile.TemporaryDirectory(\n        dir=\"/tmp/\", prefix=\"edb_gen-test-dumps_\"\n    ) as data_dir:\n        asyncio.run(\n            _gen_test_dumps(\n                tests_dir=tests_dir,\n                data_dir=data_dir,\n                jobs=jobs,\n            ),\n        )\n\n\nasync def _gen_test_dumps(*, jobs: int, tests_dir: str, data_dir: str) -> None:\n    version = str(buildmeta.get_version())\n    cluster = edgedb_cluster.Cluster(pathlib.Path(data_dir), testmode=True)\n    print(\n        f\"Generating test dumps for version {version}\"\n        f\" with a temporary Gel instance in {data_dir}...\"\n    )\n\n    try:\n        await cluster.init()\n        await cluster.start(port=0)\n        await cluster.trust_local_connections()\n    except BaseException:\n        raise\n\n    conn = cluster.get_connect_args()\n    try:\n        await execute(tests_dir, conn, num_workers=jobs, version=version)\n    except BaseException:\n        raise\n    finally:\n        cluster.stop()\n        cluster.destroy()\n"
  },
  {
    "path": "edb/tools/gen_types.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport pathlib\nimport re\nimport sys\nimport uuid\n\nimport click\n\nfrom edb.tools.edb import edbcommands\n\n\ndef die(msg):\n    print(f'FATAL: {msg}', file=sys.stderr)\n    sys.exit(1)\n\n\ndef main(*, stdout: bool):\n    import edb\n    for p in edb.__path__:  # type: ignore[attr-defined]\n        ep = pathlib.Path(p) / 'api' / 'types.txt'\n        if ep.exists():\n            out_fn = pathlib.Path(p) / 'schema' / '_types.py'\n            break\n    else:\n        die('Unable to find the \"edb/api/types.txt\" file')\n\n    items_code = []\n    with open(ep, 'rt') as f:\n        for line in f.readlines():\n            if re.match(r'(?x)^ (\\s*\\#[^\\n]*) | (\\s*) $', line):\n                continue\n\n            parts = re.split(r'\\s+', line.strip())\n            tid, name = parts\n\n            items_code.append(\n                f'    sn.name_from_string({name!r}):\\n'\n                f'        {uuid.UUID(tid)!r},')\n\n    code = (\n        f'# AUTOGENERATED FROM \"edb/api/types.txt\" WITH\\n'\n        f'#    $ edb gen-types'\n        f'\\n\\n\\n'\n        f'from __future__ import annotations'\n        f'\\n'\n        f'from typing import Type'\n        f'\\n\\n\\n'\n        f'import uuid'\n        f'\\n\\n'\n        f'from edb.common import uuidgen'\n        f'\\n'\n        f'from edb.schema import name as sn'\n        f'\\n\\n\\n'\n        f'UUID: Type[uuid.UUID] = uuidgen.UUID'\n        f'\\n\\n\\n'\n        f'TYPE_IDS = {{'\n        f'\\n' +\n        \"\\n\".join(items_code) +\n        f'\\n'\n        f'}}'\n        f'\\n'\n    )\n\n    if stdout:\n        print(code, end='')\n    else:\n        with open(out_fn, 'wt') as f:\n            f.write(code)\n\n\n@edbcommands.command('gen-types')\n@click.option(\n    '--stdout', type=bool, default=False, is_flag=True)\ndef gen_types(*, stdout):\n    \"\"\"Generate edb/schema/_types.py from edb/api/types.txt\"\"\"\n    try:\n        main(stdout=stdout)\n    except Exception as ex:\n        die(str(ex))\n"
  },
  {
    "path": "edb/tools/inittestdb.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport asyncio\nimport os.path\nimport pathlib\nimport shutil\nimport sys\nimport unittest\n\nimport click\n\nfrom edb.common import devmode\nfrom edb.testbase import cluster as edgedb_cluster\nfrom edb.testbase import server as tb\nfrom edb.tools.edb import edbcommands\n\n\nclass TestResult:\n    def wasSuccessful(self):\n        return True\n\n\nclass TestRunner:\n    def __init__(self):\n        self.cases = set()\n\n    def run(self, test):\n        self.cases.update(tb.get_test_cases([test]))\n        return TestResult()\n\n\nasync def execute(tests_dir, conn, num_workers, include):\n    runner = TestRunner()\n    include = [x for pat in include for x in ['-k', pat]]\n    unittest.main(\n        module=None,\n        argv=['unittest', 'discover', '-s', tests_dir, *include],\n        testRunner=runner, exit=False)\n\n    await tb.setup_test_cases(\n        runner.cases, conn, num_workers, skip_empty_databases=True\n    )\n\n\ndef die(msg):\n    print(f'FATAL: {msg}', file=sys.stderr)\n    sys.exit(1)\n\n\n@edbcommands.command()\n@click.option(\n    '-D', '--data-dir',\n    type=str,\n    default=str(devmode.get_dev_mode_data_dir()),\n    help='database cluster directory',\n)\n@click.option(\n    '-t', '--tests-dir', type=str,\n    default=str(pathlib.Path(__file__).parent.parent.parent.resolve() /\n                'tests'),\n    help='directory to start test discovery from')\n@click.option('-j', '--jobs', type=int,\n              default=lambda: round((os.cpu_count() or 1) * 0.75),\n              help='number of parallel processes to use')\n@click.option('-k', '--include', type=str, multiple=True, metavar='REGEXP',\n              help='only use tests which match the given regular expression')\n@click.option('-u', '--update', is_flag=True,\n              help='add the tests to the existing db')\ndef inittestdb(*, data_dir, jobs, tests_dir, include, update):\n    if os.path.exists(data_dir):\n        if not os.path.isdir(data_dir):\n            die(f'{data_dir!r} exists and is not a directory')\n        if os.listdir(data_dir) and not update:\n            die(f'{data_dir!r} exists and is not empty')\n\n    if not jobs:\n        jobs = os.cpu_count()\n\n    asyncio.run(\n        _inittestdb(\n            jobs=jobs,\n            data_dir=data_dir,\n            tests_dir=tests_dir,\n            include=include,\n            update=update,\n        ),\n    )\n\n\nasync def _inittestdb(\n    *,\n    jobs: int,\n    data_dir: str,\n    tests_dir: str,\n    include: list[str],\n    update: bool,\n) -> None:\n    cluster = edgedb_cluster.Cluster(pathlib.Path(data_dir), testmode=True)\n\n    try:\n        if not update:\n            print(f'Bootstrapping test Gel instance in {data_dir}...')\n            await cluster.init()\n        await cluster.start(port=0)\n    except BaseException:\n        if not update:\n            if os.path.exists(data_dir):\n                shutil.rmtree(data_dir)\n        raise\n\n    conn = cluster.get_connect_args()\n    destroy_cluster = False\n\n    try:\n        await execute(tests_dir, conn, num_workers=jobs, include=include)\n        print(f'Initialized and populated test Gel instance in {data_dir}')\n    except BaseException:\n        destroy_cluster = True\n        raise\n    finally:\n        cluster.stop()\n        if destroy_cluster:\n            cluster.destroy()\n"
  },
  {
    "path": "edb/tools/ls.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nfrom typing import Optional\nimport sys\nimport click\n\nfrom edb import buildmeta\nfrom edb.tools.edb import edbcommands\n\n\n@edbcommands.command(\"ls\")\n@click.option('--version', is_flag=True, help=\"Show the version and exit.\")\n@click.option(\n    '--stdio',\n    is_flag=True,\n    help=\"Use stdio for LSP. This is currently the only transport.\",\n)\n@click.argument(\"options\", type=str, default='{}')\ndef main(options: Optional[str], *, version: bool, stdio: bool):\n    # import language_server only if we are using this command\n    # otherwise this breaks when pygls is not installed\n    from edb.language_server import main as ls_main\n\n    if version:\n        print(f\"gel-ls, version {buildmeta.get_version()}\")\n        sys.exit(0)\n\n    ls = ls_main.init(options)\n\n    if stdio:\n        ls.start_io()\n    else:\n        print(\"Error: no LSP transport enabled. Use --stdio.\")\n"
  },
  {
    "path": "edb/tools/ls_forbidden_functions.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\n\nimport asyncio\n\nfrom edb.tools.edb import edbcommands\n\n\nasync def run():\n    import asyncpg\n\n    # import os\n    # localdev = os.path.expanduser('~/.local/share/edgedb/_localdev')\n    # c = await asyncpg.connect(\n    #     host=localdev, database='postgres', user='postgres'\n    # )\n\n    # docker run -it -p 5433:5432 --rm -e POSTGRES_PASSWORD=pass postgres:13\n    c = await asyncpg.connect(\n        host='localhost',\n        database='postgres',\n        user='postgres',\n        password='pass',\n        port='5433',\n    )\n\n    res = await c.fetch(\n        r'''\n        SELECT DISTINCT proname\n        FROM pg_proc p\n        INNER JOIN pg_namespace n ON pronamespace = n.oid\n        WHERE n.nspname = 'pg_catalog' AND proname like 'pg_%'\n        ORDER BY proname;\n        '''\n    )\n\n    import edb.pgsql.resolver.static as pg_r_static\n\n    print('Forbidden pg_* functions:')\n    for row in res:\n        [func_name] = row\n        if func_name in pg_r_static.ALLOWED_ADMIN_FUNCTIONS:\n            continue\n        print('  ', func_name)\n\n\n@edbcommands.command(\"ls-forbidden-functions\")\ndef main():\n    asyncio.run(run())\n"
  },
  {
    "path": "edb/tools/mypy/__init__.py",
    "content": ""
  },
  {
    "path": "edb/tools/mypy/plugin.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Mypy plugin to provide support for schema objects.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Optional, AbstractSet, NamedTuple\n\nfrom mypy import exprtotype\nimport mypy.plugin as mypy_plugin\nfrom mypy import mro\nfrom mypy import nodes\nfrom mypy import options as mypy_options\nfrom mypy import types\nfrom mypy import typevars as mypy_typevars\nfrom mypy import semanal_shared as mypy_semanal\nfrom mypy.plugins import common as mypy_helpers\nfrom mypy.server import trigger as mypy_trigger\n\n\nMETADATA_KEY = 'edbplugin'\n\nAST_BASE_CLASSES = {\n    'edb.common.ast.base.AST',\n}\n\nSTRUCT_BASE_METACLASSES = {\n    'edb.common.struct.StructMeta',\n}\n\nSCHEMA_BASE_METACLASSES = {\n    'edb.schema.objects.ObjectMeta',\n    'edb.schema.types.SchemaCollectionMeta',\n}\n\nADAPTER_METACLASS = 'edb.common.adapter.Adapter'\n\n\ndef plugin(version: str):\n    return EDBPlugin\n\n\nclass EDBPlugin(mypy_plugin.Plugin):\n\n    def get_base_class_hook(self, fullname: str):\n        if fullname.startswith('edb.'):\n            return self.handle_schema_class\n\n    def handle_schema_class(self, ctx: mypy_plugin.ClassDefContext):\n        mro = ctx.cls.info.mro\n        mcls = ctx.cls.info.metaclass_type\n        mcls_mro = mcls.type.mro if mcls else []\n\n        transformers: list[BaseTransformer] = []\n\n        if any(c.fullname in SCHEMA_BASE_METACLASSES for c in mcls_mro):\n            transformers.append(\n                SchemaClassTransformer(\n                    ctx,\n                    self.options,\n                    field_makers={'edb.schema.objects.SchemaField'},\n                )\n            )\n            transformers.append(\n                StructTransformer(\n                    ctx,\n                    self.options,\n                    field_makers={'edb.schema.objects.Field'},\n                )\n            )\n\n        elif any(c.fullname in STRUCT_BASE_METACLASSES for c in mcls_mro):\n            transformers.append(\n                StructTransformer(\n                    ctx,\n                    self.options,\n                    field_makers={'edb.common.struct.Field'},\n                )\n            )\n\n        elif any(c.fullname in AST_BASE_CLASSES for c in mro):\n            transformers.append(\n                ASTClassTransformer(\n                    ctx,\n                    self.options,\n                )\n            )\n\n        for transformer in transformers:\n            transformer.transform()\n\n    def get_customize_class_mro_hook(self, fullname: str):\n        if fullname.startswith('edb.'):\n            return self.maybe_update_mro\n\n    def maybe_update_mro(self, ctx: mypy_plugin.ClassDefContext):\n        info = ctx.cls.info\n        mcls = info.metaclass_type\n        if not mcls:\n            # This is a deep hack. The MRO gets computed *before* we\n            # know the metaclass, which is kind of weird, since I\n            # think metaclass shenanigans are the whole point of\n            # get_customize_class_mro_hook. If we defer it, though,\n            # then when we get called again it will still be sitting\n            # there.\n            if ctx.cls.metaclass:\n                ctx.api.defer()\n            return\n\n        # If the adapter class is in our metaclass MRO and we have an\n        # adapts argument, add it to our bases and recompute the MRO.\n        # This mirrors what the actual metaclass does.\n        if (\n            any(c.fullname == ADAPTER_METACLASS for c in mcls.type.mro)\n            and (adapts := ctx.cls.keywords.get('adapts'))\n            and isinstance(adapts, nodes.RefExpr)\n        ):\n            if not (\n                isinstance(adapts, nodes.RefExpr)\n                and isinstance(adapts.node, nodes.TypeInfo)\n            ):\n                ctx.api.fail('Invalid argument to adapts', ctx.cls)\n                return\n            typ = types.Instance(adapts.node, ())\n            if typ not in info.bases:\n                info.bases.append(typ)\n\n            old_mro = info.mro\n            info.mro = []\n            try:\n                mro.calculate_mro(\n                    info, lambda: ctx.api.named_type('builtins.object', []))\n            except mro.MroError:\n                ctx.api.fail(\n                    \"Cannot determine consistent method resolution \"\n                    'order (MRO) for \"%s\"' % ctx.cls.name,\n                    ctx.cls,\n                )\n                info.mro = old_mro\n\n\nclass DeferException(Exception):\n    pass\n\n\nclass Field(NamedTuple):\n\n    name: str\n    has_explicit_accessor: bool\n    has_default: bool\n    line: int\n    column: int\n    type: types.Type\n\n    def to_argument(self) -> nodes.Argument:\n        result = nodes.Argument(\n            variable=self.to_var(),\n            type_annotation=self.type,\n            initializer=None,\n            kind=nodes.ARG_NAMED_OPT if self.has_default else nodes.ARG_NAMED,\n        )\n\n        return result\n\n    def to_var(self) -> nodes.Var:\n        return nodes.Var(self.name, self.type)\n\n    def serialize(self) -> nodes.JsonDict:\n        return {\n            'name': self.name,\n            'has_explicit_accessor': self.has_explicit_accessor,\n            'has_default': self.has_default,\n            'line': self.line,\n            'column': self.column,\n            'type': self.type.serialize(),\n        }\n\n    @classmethod\n    def deserialize(\n        cls,\n        api,\n        data: nodes.JsonDict,\n    ) -> Field:\n        return cls(\n            name=data['name'],\n            has_explicit_accessor=data['has_explicit_accessor'],\n            has_default=data['has_default'],\n            line=data['line'],\n            column=data['column'],\n            type=mypy_helpers.deserialize_and_fixup_type(data['type'], api),\n        )\n\n\nclass BaseTransformer:\n\n    def __init__(\n        self,\n        ctx: mypy_plugin.ClassDefContext,\n        options: mypy_options.Options,\n    ) -> None:\n        self._ctx = ctx\n        self._options = options\n\n    def transform(self):\n        ctx = self._ctx\n        metadata_key = self._get_metadata_key()\n        metadata = ctx.cls.info.metadata.get(metadata_key)\n        if not metadata:\n            ctx.cls.info.metadata[metadata_key] = metadata = {}\n\n        metadata['processing'] = True\n\n        if metadata.get('processed'):\n            return\n\n        try:\n            fields = self._transform()\n        except DeferException:\n            ctx.api.defer()\n            return None\n\n        metadata['fields'] = {f.name: f.serialize() for f in fields}\n        metadata['processed'] = True\n\n    def _transform(self) -> list[Field]:\n        raise NotImplementedError\n\n    def _field_from_field_def(\n        self,\n        stmt: nodes.AssignmentStmt,\n        name: nodes.NameExpr,\n        sym: nodes.SymbolTableNode,\n    ) -> Optional[Field]:\n        raise NotImplementedError\n\n    def _collect_fields(self) -> list[Field]:\n        \"\"\"Collect all fields declared in a class and its ancestors.\"\"\"\n\n        cls = self._ctx.cls\n\n        fields: list[Field] = []\n\n        known_fields: set[str] = set()\n\n        for stmt in cls.defs.body:\n            if not isinstance(stmt, nodes.AssignmentStmt):\n                continue\n\n            lhs = stmt.lvalues[0]\n            if not isinstance(lhs, nodes.NameExpr):\n                continue\n\n            sym = cls.info.names.get(lhs.name)\n            if sym is None or isinstance(sym.node, nodes.PlaceholderNode):\n                # Not resolved yet?\n                continue\n\n            node = sym.node\n            assert isinstance(node, nodes.Var)\n\n            if node.is_classvar:\n                # Disregard ClassVar stuff\n                continue\n\n            field = self._field_from_field_def(stmt, lhs, sym)\n            if field is not None:\n                fields.append(field)\n                known_fields.add(field.name)\n\n        return self._get_inherited_fields(known_fields) + fields\n\n    def _lookup_type(self, fullname: str) -> types.Type:\n        ctx = self._ctx\n\n        type_sym = ctx.api.lookup_fully_qualified_or_none(fullname)\n\n        if type_sym is None:\n            raise DeferException\n\n        t: types.Type\n\n        if isinstance(type_sym.node, nodes.TypeInfo):\n            from mypy.typevars import fill_typevars\n            t = fill_typevars(type_sym.node)\n        elif type_sym.type:\n            t = type_sym.type\n        else:\n            ctx.api.fail(f'cannot find {fullname}', ctx.cls)\n\n        return t\n\n    def _get_metadata_key(self) -> str:\n        return f'{METADATA_KEY}%%{type(self).__name__}'\n\n    def _has_explicit_field_accessor(self, fieldname: str) -> bool:\n        cls = self._ctx.cls\n        accessor = cls.info.names.get(f'get_{fieldname}')\n        return accessor is not None and not accessor.plugin_generated\n\n    def _get_inherited_fields(self, self_fields: set[str]) -> list[Field]:\n        ctx = self._ctx\n        cls = ctx.cls\n        all_fields: list[Field] = []\n        known_fields = set(self_fields)\n\n        for ancestor_info in cls.info.mro[1:-1]:\n            metadata = ancestor_info.metadata.get(self._get_metadata_key())\n            if metadata is None:\n                continue\n            elif not metadata.get('processed'):\n                raise DeferException\n\n            ancestor_fields = []\n\n            ctx.api.add_plugin_dependency(\n                mypy_trigger.make_wildcard_trigger(ancestor_info.fullname))\n\n            for name, data in metadata['fields'].items():\n                if name not in known_fields:\n                    if self._has_explicit_field_accessor(name):\n                        data = dict(data)\n                        data['has_explicit_accessor'] = True\n                    field = Field.deserialize(ctx.api, data)\n\n                    known_fields.add(name)\n                    ancestor_fields.append(field)\n            all_fields = ancestor_fields + all_fields\n\n        return all_fields\n\n    def _synthesize_init(self, fields: list[Field]) -> None:\n        ctx = self._ctx\n        cls_info = ctx.cls.info\n\n        # If our self type has placeholders (probably because of type\n        # var bounds), defer. If we skip deferring and stick something\n        # in our symbol table anyway, we'll get in trouble.  (Arguably\n        # plugins.common ought to help us with this, but oh well.)\n        self_type = mypy_typevars.fill_typevars(cls_info)\n        if mypy_semanal.has_placeholder(self_type):\n            raise DeferException\n\n        if (\n            (\n                '__init__' not in cls_info.names\n                or cls_info.names['__init__'].plugin_generated\n            ) and fields\n        ):\n            mypy_helpers.add_method(\n                ctx,\n                '__init__',\n                self_type=self_type,\n                args=[field.to_argument() for field in fields],\n                return_type=types.NoneType(),\n            )\n\n\nclass BaseStructTransformer(BaseTransformer):\n\n    def __init__(\n        self,\n        ctx: mypy_plugin.ClassDefContext,\n        options: mypy_options.Options,\n        field_makers: AbstractSet[str],\n    ) -> None:\n        super().__init__(ctx, options)\n        self._field_makers = field_makers\n\n    def _field_from_field_def(\n        self,\n        stmt: nodes.AssignmentStmt,\n        name: nodes.NameExpr,\n        sym: nodes.SymbolTableNode,\n    ) -> Optional[Field]:\n        ctx = self._ctx\n\n        rhs = stmt.rvalue\n\n        if isinstance(rhs, nodes.CastExpr):\n            rhs = rhs.expr\n\n        if not isinstance(rhs, nodes.CallExpr):\n            return None\n\n        fdef = rhs.callee\n\n        ftype = None\n        if (\n            isinstance(fdef, nodes.IndexExpr)\n            and isinstance(fdef.analyzed, nodes.TypeApplication)\n        ):\n            # Explicitly typed Field declaration\n            ctor = fdef.analyzed.expr\n            if len(fdef.analyzed.types) > 1:\n                ctx.api.fail('too many type arguments to Field', fdef)\n            ftype = fdef.analyzed.types[0]\n        else:\n            ctor = fdef\n            ftype = None\n\n        if (\n            not isinstance(ctor, nodes.RefExpr)\n            or ctor.fullname not in self._field_makers\n        ):\n            return None\n\n        type_arg = rhs.args[0]\n\n        deflt = self._get_default(rhs)\n\n        if ftype is None:\n            try:\n                un_type = exprtotype.expr_to_unanalyzed_type(\n                    type_arg,\n                    options=self._options,\n                )\n            except exprtotype.TypeTranslationError:\n                ctx.api.fail('Cannot resolve schema field type', type_arg)\n            else:\n                ftype = ctx.api.anal_type(un_type)\n            if ftype is None:\n                raise DeferException\n\n            is_optional = (\n                isinstance(deflt, nodes.NameExpr)\n                and deflt.fullname == 'builtins.None'\n            )\n            if is_optional:\n                ftype = types.UnionType.make_union(\n                    [ftype, types.NoneType()],\n                    line=ftype.line,\n                    column=ftype.column,\n                )\n\n        assert isinstance(name.node, nodes.Var)\n        name.node.type = ftype\n\n        return Field(\n            name=name.name,\n            has_explicit_accessor=self._has_explicit_field_accessor(name.name),\n            has_default=deflt is not None,\n            line=stmt.line,\n            column=stmt.column,\n            type=ftype,\n        )\n\n    def _get_default(self, call) -> Optional[nodes.Expression]:\n        for (n, v) in zip(call.arg_names, call.args):\n            if n == 'default':\n                return v\n        else:\n            return None\n\n\nclass StructTransformer(BaseStructTransformer):\n\n    def _transform(self) -> list[Field]:\n        fields = self._collect_fields()\n        self._synthesize_init(fields)\n        return fields\n\n    def _field_from_field_def(\n        self,\n        stmt: nodes.AssignmentStmt,\n        name: nodes.NameExpr,\n        sym: nodes.SymbolTableNode,\n    ):\n        field = super()._field_from_field_def(stmt, name, sym)\n        if field is None:\n            return None\n        else:\n            assert isinstance(sym.node, nodes.Var)\n            sym.node.is_initialized_in_class = False\n\n            name.is_inferred_def = False\n\n            rhs = stmt.rvalue\n            if not isinstance(rhs, nodes.CastExpr):\n                stmt.rvalue = nodes.CastExpr(\n                    typ=field.type,\n                    expr=rhs,\n                )\n                stmt.rvalue.line = rhs.line\n                stmt.rvalue.column = rhs.column\n\n            return field\n\n\nclass SchemaClassTransformer(BaseStructTransformer):\n\n    def _transform(self) -> list[Field]:\n        ctx = self._ctx\n        fields = self._collect_fields()\n        schema_t = self._lookup_type('edb.schema.schema.Schema')\n\n        for f in fields:\n            if f.has_explicit_accessor:\n                continue\n\n            mypy_helpers.add_method(\n                ctx,\n                name=f'get_{f.name}',\n                args=[\n                    nodes.Argument(\n                        variable=nodes.Var(\n                            name='schema',\n                            type=schema_t,\n                        ),\n                        type_annotation=schema_t,\n                        initializer=None,\n                        kind=nodes.ARG_POS,\n                    ),\n                ],\n                return_type=f.type,\n            )\n\n        return fields\n\n\nclass ASTClassTransformer(BaseTransformer):\n\n    def _transform(self) -> list[Field]:\n        fields = self._collect_fields()\n        self._synthesize_init(fields)\n        return fields\n\n    def _field_from_field_def(\n        self,\n        stmt: nodes.AssignmentStmt,\n        name: nodes.NameExpr,\n        sym: nodes.SymbolTableNode,\n    ) -> Optional[Field]:\n\n        if sym.type is None:\n            # If the assignment has a type annotation but the symbol\n            # doesn't yet, we need to defer\n            if stmt.type:\n                raise DeferException\n            # No type annotation?\n            return None\n        else:\n            has_default = not isinstance(stmt.rvalue, nodes.TempNode)\n\n            if not has_default:\n                sym.implicit = True\n\n            return Field(\n                name=name.name,\n                has_default=has_default,\n                line=stmt.line,\n                column=stmt.column,\n                type=sym.type,\n                has_explicit_accessor=False,\n            )\n"
  },
  {
    "path": "edb/tools/parser_demo.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import tokenizer\nfrom edb.edgeql import parser as qlparser\nfrom edb.edgeql.parser.grammar import tokens as qltokens\n\nimport edb._edgeql_parser as rust_parser\n\nfrom edb.tools.edb import edbcommands\n\n\n@edbcommands.command(\"parser-demo\")\ndef main():\n    qlparser.preload_spec()\n\n    for q in QUERIES[-8:]:\n        sdl = q.startswith('sdl')\n        if sdl:\n            q = q[3:]\n\n        try:\n            source = tokenizer.NormalizedSource.from_string(q)\n            # source = tokenizer.Source.from_string(q)\n        except Exception as e:\n            print('Error during tokenization:')\n            print(e)\n            continue\n\n        start_t = qltokens.T_STARTSDLDOCUMENT if sdl else qltokens.T_STARTBLOCK\n        start_t_name = start_t.__name__[2:]\n        tokens = source.tokens()\n        result, productions = rust_parser.parse(start_t_name, tokens)\n\n        print('-' * 30)\n        print()\n\n        for index, error in enumerate(result.errors):\n            message, span, hint, details = error\n            (start, end) = tokenizer.inflate_span(source.text(), span)\n\n            print(f'Error [{index + 1}/{len(result.errors)}]:')\n            print(\n                '\\n'.join(\n                    source.text().splitlines()[(start.line - 1) : end.line]\n                )\n            )\n            print(\n                ' ' * (start.column - 1)\n                + '^' * (max(1, end.column - start.column))\n                + ' '\n                + message\n            )\n            if details:\n                print(f'  Details: {details}')\n            if hint:\n                print(f'  Hint: {hint}')\n            print()\n\n        if result.out:\n            try:\n                ast = qlparser._cst_to_ast(\n                    result.out, productions, source=source, filename=''\n                ).val\n            except Exception as e:\n                print(e)\n                ast = None\n            if ast:\n                print('Recovered AST:')\n                if isinstance(ast, list):\n                    for x in ast:\n                        assert isinstance(x, qlast.Base)\n                        x.dump_edgeql()\n                        x.dump()\n                        print(x.span.start, x.span.end)\n                elif isinstance(ast, qlast.Base):\n                    ast.dump_edgeql()\n                    ast.dump()\n                    print(ast.span.start, ast.span.end)\n                else:\n                    print(ast)\n\n\nQUERIES = [\n    '''sdl\n    module y {\n        type X {\n            property z:\n        }\n    }\n    ''',\n    '''\n    CREATE MIGRATION\n    {\n        set global foo := \"test\";\n        alter type Foo {\n            create required property name -> str {\n            set default := (global foo);\n        }\n    }; }\n    '''\n]\n"
  },
  {
    "path": "edb/tools/profiling/README.md",
    "content": "HOWTO profile EdgeDB code\n=========================\n\nThis package provides tooling built around cProfile that lets us perform\nquick time-based profiling.  Then we can aggregate the results into\nregular pstats output as well as a zoomable and searchable SVG flame\ngraph.\n\n\n## Running the profiler\n\nThe profiler is not sample based and supports multiprocessing so it\nshould provide a full view into what is going on.  That being said, it's\ntime-based and done in-process so it's bound to have some jitter.\n\nRunning it is as simple as this:\n\n```py3\nfrom edb.tools.profiling import profile\n\n...\n\n@profile()\ndef some_function_we_want_to_profile() -> None:\n    ...\n```\n\nThis will generate a series of `.prof` files in a temporary directory\n(note: on macOS this is *not* /tmp/ by default).  Only invocations of\nthis function will be profiled.  The profiler reuses the same file name\nper process to avoid generating hundreds of thousands of files if the\nprofiled function is called many times.\n\n\n## Aggregating data\n\nAfter the program is profiled, we can aggregate the results by running:\n\n```\n$ edb perfviz\n```\n\nThis should produce a few files in the EdgeDB project directory by\ndefault:\n\n* `profile_analysis.prof` which is an aggregation of possibly hundreds\n  of smaller `.prof` files produced by subprocesses of the profiled\n  program.  You can use this as input for further aggregation or\n  analysis.  This is the marshalled format accepted directly by the\n  `pstats` module.\n\n* `profile_analysis.pstats` is a text file with \"Top calls\" sorted and\n  formatted by the `pstats.Stats` class.\n\n* `profile_analysis.call_stack.svg` is a zoomable and searchable SVG\n  flame graph of the call stack.\n\n* `profile_analysis.usage.svg` is a zoomable and searchable SVG\n  reverse flame graph of most used functions.\n\n* `profile_analysis.singledispatch` is a single dispatch sidecar file,\n  explained further down in this document.\n\n## Customizing the profiler\n\nThe `profile()` decorator accepts a number of arguments.  I don't want\nto risk this file going out of date, so your best course of action is\nlooking at the decorator's docstring.\n\nSimilarly, `edb perfviz --help` lists all possible options of the\naggregator.\n\nHere let me just show you an example customization:\n\n```py3\n@profile(dir='/tmp/', prefix='specific_function_')\ndef specific_function() -> None:\n    ...\n\n@profile(dir='/tmp/', prefix='another_function_')\ndef another_function() -> None:\n    ...\n```\n\nTo aggregate this data, use for example:\n\n```\n$ edb perfviz --prefix=another_function_ --out=/Users/ambv/Desktop/another_function.pstats /tmp/\n```\n\nAs you see, we filtered by the prefix, put the output on the desktop\nwith a custom file name, and we're looking for the input files in /tmp/\nwhich is where they were configured to be saved by the decorators.\n\n## SVG flame graphs\n\nFlame graphs are a nice way to visualize call patterns in a program.\nIn the graphs generated here, the upper one is the traditional call\nstack graph where the lower bars are functions calling other functions\nin the upper bars.  The width demonstrates relative time spent in each\nfunction.  The lower graph is a reverse flame graph, showing functions\nthat are called very often and their callers.\n\nThe generated flame graph can be big by default if a large part of the\nprogram is analyzed.  If the resulting file is over 10MB big and your\nWeb browser doesn't like it, adjust the threshold and/or width of the\ngenerated file, like so:\n\n```\n$ edb perfviz --threshold=0.001 --width=1280\n```\n\n## Singledispatch and the fog of war\n\nSingle-dispatch generic functions available in Python with the use of\nthe `functools.singledispatch` decorator are handy but have one downside\nwhen it comes to profiling.\n\nLet's say we have a decorated single-dispatch generic function S.  That\nfunction has a few implementations registered for different types, let's\nsay F1, F2, and F3.\n\nNow, if functions A, B, C all call function S, we would expect to see\nthose calls nicely separated and showing us which concrete\nregistered implementations were chosen when function A was calling S,\nor when function C was calling S.  Sadly, this is not what's happening.\n\nThe generic function S is replaced with a `wrapper` by the\n`@singledispatch` decorator.  This wrapper dispatches calls to\nregistered concrete implementations based on the type of the first\nargument in the call.  Even though the closures are different for every\ndecorated single-dispatch generic function, the code for the `wrapper`\nis shared between every one.  In consequence, in the SVG flame graph\nyou'll see functions A, B, C low in the graph, then a thick bar called\n`wrapper` and concrete implementations F1, F2, and F3 above.  But we\nlost knowledge about whether A is responsible for 90% of F3 calls or\nwhether B is the biggest user of S.\n\nTo fix this problem, do this *very early in your program*:\n\n```py3\nfrom edb.tools.profiling import tracing_singledispatch\ntracing_singledispatch.patch_functools()\n```\n\nYou have to do it very early because single-dispatch generic functions\nare configured and registered at import time.  Patch before any\nsingledispatch import and usage.\n\nAnyway, now that singledispatch is patched, the `@profile` decorator\nwill discover this and will save sidecar `.singledispatch` files that\nstore which function called the single-dispatch wrapper and which\nimplementation ended up being used.  It also stores the call count to\nenable proportional aggregation later.\n\nWhen `.singledispatch` sidecar files were present, aggregating the\nprofiler data will generate an SVG flame graph that replaces the\n`wrapper` fog of war with concrete relationships between A, B, C\nand F1, F2, and F3.\n\nPro tip: to profile tests, make sure to cover at least the following\nentry points with the functools patch:\n\n* edb/cli/__init__.py (for the `edgedb` CLI command)\n\n* edb/server/main.py (for the `edgedb-server` CLI command)\n\n* edb/tools/edb.py (for the `edb` CLI command)\n\n* edb/server/compiler_pool/worker.py (for worker subprocesses like the EdgeQL\n  compiler)\n\n## Profiling caveats\n\nThe decorators are not fully reentrant: if more than one function is\nprofiled at the same time and they share some call paths, the resulting\ndata will be corrupt (i.e. each result will be missing parts of the\ninformation).\n\nThis is a wall clock time-based profiler, it's not well suited for\nbenchmarking.\n\nThis profiler does not store call history so the flame graph does not\nrepresent a timeline.\n\nThis profiler is using `atexit` to ensure all data is written to disk\nbefore the program is done.  However, terminating a process with\na signal may circumvent `atexit` from executing.  Similarly, an\nexception in an `atexit` handler called sooner may prevent our handler\nfrom running.  If data is visibly incomplete, use `save_every_n_calls=1`\n(which is the default).\n\nThis profiler will not be able to trace calls that don't trigger\n`sys.settrace()` callbacks.\n"
  },
  {
    "path": "edb/tools/profiling/__init__.py",
    "content": "# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Profiling tools for Gel and beyond.\n\nSee README.md in this package for more details.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom . import profiler\n\n__all__ = ['profile']\n\nprofile = profiler.profile\n"
  },
  {
    "path": "edb/tools/profiling/cli.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"An entry point to the aggregation capabilitities of the profiler.\n\nSee README.md in this package for more details.\n\"\"\"\n\nfrom __future__ import annotations\nfrom typing import Optional\n\nimport pathlib\n\nimport click\n\nfrom edb.tools.edb import edbcommands\n\nfrom . import profiler\n\n\n@edbcommands.command()\n@click.option(\n    \"--prefix\",\n    default=profiler.PREFIX,\n    show_default=True,\n    help=\"Input file prefix to match\",\n)\n@click.option(\n    \"--suffix\",\n    default=profiler.PROF_SUFFIX,\n    show_default=True,\n    help=\"Input file suffix to match\",\n)\n@click.option(\n    \"--sort-by\",\n    default=\"cumulative\",\n    show_default=True,\n    help=\"Default sort, same values as pstats\",\n)\n@click.option(\n    \"--out\",\n    default=profiler.EDGEDB_DIR,\n    show_default=True,\n    help=\"Output file or directory for the aggregations\",\n)\n@click.option(\n    \"--width\",\n    default=1920,\n    show_default=True,\n    help=\"Width of the SVG flame graph in pixels\",\n)\n@click.option(\n    \"--threshold\",\n    default=0.0001,\n    show_default=True,\n    help=(\n        \"Percentage of time spent in a function in relation to the total\"\n        \" time spent below which the function is not going to appear on the\"\n        \" SVG flame graph. 1.0 is 100%.\"\n    ),\n)\n@click.argument(\"dirs\", nargs=-1)  # one or zero\ndef perfviz(\n    dirs: list[str],\n    prefix: str,\n    suffix: str,\n    sort_by: str,\n    out: pathlib.Path | str,\n    width: int,\n    threshold: float,\n) -> None:\n    \"\"\"Aggregate raw profiling traces into textual and graphical formats.\n\n    Generates aggregate .prof and .singledispatch files, an aggregate textual\n    .pstats file, as well as two SVG flame graphs.\n\n    For more comprehensive documentation read edb/tools/profiling/README.md.\n    \"\"\"\n    if len(dirs) > 1:\n        raise click.UsageError(\"Specify at most one directory\")\n\n    dir: Optional[str] = dirs[0] if dirs else None\n    prof = profiler.profile(\n        dir=dir, prefix=prefix, suffix=suffix, save_every_n_calls=1\n    )\n    prof.aggregate(\n        pathlib.Path(out), sort_by=sort_by, width=width, threshold=threshold\n    )\n"
  },
  {
    "path": "edb/tools/profiling/profiler.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Provides a `profile()` decorator with aggregation capabilities.\n\nSee README.md in this package for more details.\n\"\"\"\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n    TypeVar,\n    AbstractSet,\n    Iterator,\n    Sequence,\n    Counter,\n    NamedTuple,\n    cast,\n    TYPE_CHECKING,\n)\n\nimport ast\nimport atexit\nimport builtins\nimport cProfile\nimport dataclasses\nimport functools\nimport hashlib\nimport linecache\nimport os\nimport pathlib\nimport pickle\nimport pstats\nimport re\nimport sys\nimport tempfile\nfrom xml.sax import saxutils\n\nfrom edb.tools.profiling import tracing_singledispatch\n\n\nCURRENT_DIR = pathlib.Path(__file__).resolve().parent\nEDGEDB_DIR = CURRENT_DIR.parent.parent.parent\nPROFILING_JS = CURRENT_DIR / \"svg_helpers.js\"\nPREFIX = \"edgedb_\"\nSTAT_SUFFIX = \".pstats\"\nPROF_SUFFIX = \".prof\"\nSVG_SUFFIX = \".svg\"\nSINGLEDISPATCH_SUFFIX = \".singledispatch\"\n\n\nT = TypeVar(\"T\", bound=Callable[..., Any])\n\n\nif TYPE_CHECKING:\n    ModulePath = str\n    LineNo = int\n    FunctionName = str\n    FunctionID = tuple[ModulePath, LineNo, FunctionName]\n    LineID = tuple[ModulePath, LineNo]\n    # cc, nc, tt, ct, callers\n    PrimitiveCallCount = int  # without recursion\n    CallCount = int  # with recursion\n    TotalTime = float\n    CumulativeTime = float\n    Stat = tuple[PrimitiveCallCount, CallCount, TotalTime, CumulativeTime]\n    StatWithCallers = tuple[\n        PrimitiveCallCount,\n        CallCount,\n        TotalTime,\n        CumulativeTime,\n        dict[FunctionID, Stat],  # callers\n    ]\n    Stats = dict[FunctionID, StatWithCallers]\n    Caller = FunctionID\n    Callee = FunctionID\n    Call = tuple[Caller, Optional[Callee]]\n    CallCounts = dict[Caller, dict[Callee, CallCount]]\n\n\nclass profile:\n    \"\"\"A decorator for CPU profiling.\"\"\"\n\n    def __init__(\n        self,\n        *,\n        prefix: str = PREFIX,\n        suffix: str = PROF_SUFFIX,\n        dir: Optional[str] = None,\n        save_every_n_calls: int = 1,\n    ):\n        \"\"\"Create the decorator.\n\n        If `save_every_n_calls` is greater than 1, the profiler will not\n        dump data to files on every call to the profiled function.  This speeds\n        up the running program but risks incomplete data if the process is\n        terminated non-gracefully.\n\n        `dir`, `prefix`, and `suffix` after `tempfile.mkstemp`.\n        \"\"\"\n        self.prefix = prefix\n        self.suffix = suffix\n        self.save_every_n_calls = save_every_n_calls\n        self.n_calls = 0\n        self._dir: str | pathlib.Path | None = dir\n        self._profiler: Optional[cProfile.Profile] = None\n        self._dump_file_path: Optional[str] = None\n        self._profiler_enabled = False\n\n    def __call__(self, func: T) -> T:\n        \"\"\"Apply decorator to a function.\"\"\"\n\n        @functools.wraps(func)\n        def wrapper(*args, **kwargs):\n            tracing_singledispatch.profiling_in_progress.set()\n            self.n_calls += 1\n            profiler_was_enabled_here = False\n            if not self._profiler_enabled:\n                self.profiler.enable()\n                self._profiler_enabled = True\n                profiler_was_enabled_here = True\n            try:\n                return func(*args, **kwargs)\n            finally:\n                if profiler_was_enabled_here:\n                    self.profiler.disable()\n                if self.n_calls % self.save_every_n_calls == 0:\n                    self.dump_stats()\n                tracing_singledispatch.profiling_in_progress.clear()\n\n        return cast(T, wrapper)\n\n    @property\n    def dir(self) -> pathlib.Path:\n        if self._dir is None:\n            with tempfile.NamedTemporaryFile() as tmp:\n                self._dir = pathlib.Path(tmp.name).parent\n        return pathlib.Path(self._dir)\n\n    @property\n    def profiler(self) -> cProfile.Profile:\n        if self._profiler is None:\n            self._profiler = cProfile.Profile()\n            if self.save_every_n_calls > 1:\n                # This is attached here so the registration is in the right\n                # process (relevant for multiprocessing workers).  This is\n                # still sadly flimsy, hence the `save every n calls`.\n                atexit.register(self.dump_stats)\n        return self._profiler\n\n    @property\n    def dump_file(self) -> str:\n        \"\"\"Return a path to a new, empty, existing named temporary file.\"\"\"\n        if self._dump_file_path is None:\n            file = tempfile.NamedTemporaryFile(\n                dir=self.dir,\n                prefix=self.prefix,\n                suffix=self.suffix,\n                delete=False,\n            )\n            file.close()\n            self._dump_file_path = file.name\n\n        return self._dump_file_path\n\n    def dump_stats(self) -> None:\n        self.profiler.dump_stats(self.dump_file)\n        try:\n            done_dispatches = tracing_singledispatch.done_dispatches\n        except AttributeError:\n            return  # we're at program exit; `tracing_singledispatch` went away\n        if done_dispatches:\n            with open(self.dump_file + \".singledispatch\", \"wb\") as sd_file:\n                pickle.dump(done_dispatches, sd_file, pickle.HIGHEST_PROTOCOL)\n\n    def aggregate(\n        self,\n        out_path: pathlib.Path,\n        *,\n        sort_by: str = \"\",\n        width: int = 1920,\n        threshold: float = 0.0001,  # 1.0 is 100%\n        quiet: bool = False,\n    ) -> tuple[int, int]:\n        \"\"\"Read all pstats in `self.dir` and write a summary to `out_path`.\n\n        `sort_by` after `pstats.sort_stats()`.  Files identified by `self.dir`,\n        `self.prefix`, and `self.suffix`.\n\n        `width` selects the width of the generated SVG.\n\n        Functions whose runtime is below `threshold` percentage are not\n        included.\n\n        Returns a tuple with number of successfully and unsucessfully\n        aggregated files.\n        \"\"\"\n        print = builtins.print\n        if quiet:\n            print = lambda *args, **kwargs: None\n\n        if out_path.is_dir():\n            out_path = out_path / \"profile_analysis\"\n        prof_path = out_path.with_suffix(PROF_SUFFIX)\n        pstats_path = out_path.with_suffix(STAT_SUFFIX)\n        call_svg_path = out_path.with_suffix(\".call_stack\" + SVG_SUFFIX)\n        usage_svg_path = out_path.with_suffix(\".usage\" + SVG_SUFFIX)\n        files = list(\n            str(f) for f in self.dir.glob(self.prefix + \"*\" + self.suffix)\n        )\n        if not files:\n            print(f\"warning: no files to process\", file=sys.stderr)\n            return 0, 0\n\n        success = 0\n        failure = 0\n        with open(pstats_path, \"w\") as out:\n            ps = pstats.Stats(stream=out)\n            for file in files:\n                try:\n                    ps.add(file)\n                except TypeError as te:\n                    # Probably the profile file is empty.\n                    print(te, file=sys.stderr)\n                    failure += 1\n                else:\n                    success += 1\n            ps.dump_stats(str(prof_path))\n            if sort_by:\n                ps.sort_stats(sort_by)\n            ps.print_stats()\n        singledispatch_traces = self.accumulate_singledispatch_traces()\n        if singledispatch_traces:\n            singledispatch_path = out_path.with_suffix(SINGLEDISPATCH_SUFFIX)\n            with singledispatch_path.open(\"wb\") as sd_file:\n                pickle.dump(\n                    singledispatch_traces, sd_file, pickle.HIGHEST_PROTOCOL\n                )\n\n        # Mypy is wrong below, `stats` is there on all pstats.Stats objects\n        stats = ps.stats  # type: ignore\n        filter_singledispatch_in_place(stats, singledispatch_traces)\n        try:\n            render_svg(\n                stats,\n                call_svg_path,\n                usage_svg_path,\n                width=width,\n                threshold=threshold,\n            )\n        except ValueError as ve:\n            print(f\"Cannot display flame graph: {ve}\", file=sys.stderr)\n        print(\n            f\"Processed {success + failure} files, {failure} failed.\",\n            file=sys.stderr,\n        )\n        return success, failure\n\n    def accumulate_singledispatch_traces(self) -> dict[FunctionID, CallCounts]:\n        result: dict[FunctionID, CallCounts] = {}\n        d = self.dir.glob(\n            self.prefix + \"*\" + self.suffix + SINGLEDISPATCH_SUFFIX\n        )\n        for f in d:\n            with open(str(f), \"rb\") as file:\n                dispatches = pickle.load(file)\n            for singledispatch_funcid, call_counts in dispatches.items():\n                for caller, calls in call_counts.items():\n                    for impl, call_count in calls.items():\n                        r_d = result.setdefault(singledispatch_funcid, {})\n                        c_d = r_d.setdefault(caller, {})\n                        c_d[impl] = c_d.get(impl, 0) + call_count\n        return result\n\n\ndef profile_memory(func: Callable[[], Any]) -> MemoryFrame:\n    \"\"\"Profile memory and return a tree of statistics.\n\n    Feed those to `render_memory_svg()` to write an SVG.\n    \"\"\"\n    import tracemalloc\n\n    tracemalloc.start(1024)\n    try:\n        func()\n    finally:\n        snap = tracemalloc.take_snapshot()\n        tracemalloc.stop()\n    stats = snap.statistics(\"traceback\")\n    root = MemoryFrame(blocks=0, size=0)\n    for stat in stats:\n        blocks = stat.count\n        size = stat.size\n        callee = root\n        callee.blocks += blocks\n        callee.size += size\n        for frame in stat.traceback:\n            lineid = (frame.filename, frame.lineno)\n            callee = callee.callers.setdefault(\n                lineid, MemoryFrame(blocks=0, size=0)\n            )\n            callee.blocks += blocks\n            callee.size += size\n    while len(root.callers) == 1:\n        root = next(iter(root.callers.values()))\n    return root\n\n\n@dataclasses.dataclass\nclass Function:\n    id: FunctionID\n    calls: list[FunctionID]\n    calledby: list[FunctionID]\n    stat: Stat\n\n\nROOT_ID: FunctionID = (\"<root>\", 0, \"<root>\")\n\n\nclass RGB(NamedTuple):\n    r: int\n    g: int\n    b: int\n\n\ndef gen_colors(s: RGB, e: RGB, size: int) -> Iterator[RGB]:\n    \"\"\"Generate a gradient of `size` colors between `s` and `e`.\"\"\"\n    for i in range(size):\n        yield RGB(\n            s.r + (e.r - s.r) * i // size,\n            s.g + (e.g - s.g) * i // size,\n            s.b + (e.b - s.b) * i // size,\n        )\n\n\nCOLORS = list(gen_colors(RGB(255, 240, 141), RGB(255, 65, 34), 7))\nCCOLORS = list(gen_colors(RGB(44, 255, 210), RGB(113, 194, 0), 5))\nECOLORS = list(gen_colors(RGB(230, 230, 255), RGB(150, 150, 255), 5))\nDCOLORS = list(gen_colors(RGB(190, 190, 190), RGB(240, 240, 240), 7))\n\n\ndef gradient_from_name(name: str) -> float:\n    v = int(hashlib.sha1(name.encode(\"utf8\")).hexdigest()[:8], base=16)\n    return v / (0xFFFFFFFF + 1.0)\n\n\ndef calc_callers(\n    stats: Stats,\n    threshold: float,\n) -> tuple[dict[FunctionID, Function], dict[Call, Stat]]:\n    \"\"\"Calculate flattened stats of calls between functions.\"\"\"\n    roots: list[FunctionID] = []\n    funcs: dict[FunctionID, Function] = {}\n    calls: dict[Call, Stat] = {}\n    for func, (cc, nc, tt, ct, callers) in stats.items():\n        funcs[func] = Function(\n            id=func, calls=[], calledby=[], stat=(cc, nc, tt, ct)\n        )\n        if not callers:\n            roots.append(func)\n            calls[ROOT_ID, func] = funcs[func].stat\n\n    for func, (_, _, _, _, callers) in stats.items():\n        for caller, t in callers.items():\n            assert (caller, func) not in calls\n            funcs[caller].calls.append(func)\n            funcs[func].calledby.append(caller)\n            calls[caller, func] = t\n\n    total = sum(funcs[r].stat[3] for r in roots)\n    ttotal = sum(funcs[r].stat[2] for r in funcs)\n\n    # Try to find suitable root\n    newroot = max(\n        (r for r in funcs if r not in roots), key=lambda r: funcs[r].stat[3]\n    )\n    nstat = funcs[newroot].stat\n    ntotal = total + nstat[3]\n    if 0.8 < ntotal / ttotal < 1.2:\n        roots.append(newroot)\n        calls[ROOT_ID, newroot] = nstat\n        total = ntotal\n    else:\n        total = ttotal\n\n    funcs[ROOT_ID] = Function(\n        id=ROOT_ID, calls=roots, calledby=[], stat=(1, 1, 0, total),\n    )\n    return funcs, calls\n\n\n@dataclasses.dataclass\nclass Block:\n    func: FunctionID\n    call_stack: tuple[FunctionID, ...]\n    color: int\n    level: int\n    tooltip: str\n    w: float\n    x: float\n\n    @property\n    def id(self) -> str:\n        return repr(self.func)\n\n    @property\n    def name(self) -> FunctionName:\n        result = self.func[2]\n        if result.startswith(\"<built-in method builtins.\"):\n            result = result[len(\"<built-in method \") : -1]\n        return result\n\n    @property\n    def module(self) -> str:\n        result = self.func[0]\n        edgedb = str(EDGEDB_DIR) + os.sep\n        if result.startswith(edgedb):\n            return result[len(edgedb):]\n\n        parts = []\n        maybe_stdlib = False\n        for part in pathlib.Path(result).parts[::-1]:\n            parts.append(part)\n            if part in {\"python3.6\", \"python3.7\", \"python3.8\", \"python3.9\"}:\n                maybe_stdlib = True\n            elif maybe_stdlib:\n                if part == \"lib\":\n                    parts.pop()\n                    return os.sep.join(parts[::-1])\n\n                break\n\n        return result\n\n    @property\n    def full_name(self) -> str:\n        result = \"\"\n        if self.func[0] not in {\"~\", \"\", None}:\n            result += self.module\n            result += \":\"\n        if self.func[1] not in (0, None):\n            result += str(self.func[1])\n            result += \":\"\n        result += self.name\n        return f\"{result} {self.tooltip}\"\n\n\n@dataclasses.dataclass\nclass MemoryFrame:\n    \"\"\"A node of a tree of calls.\n\n    Leaves are were memory allocations actually happened.\n    \"\"\"\n    blocks: int\n    size: int  # in bytes\n    callers: dict[LineID, MemoryFrame] = dataclasses.field(\n        default_factory=dict\n    )\n\n\nclass ScopeRecorder(ast.NodeVisitor):\n    \"\"\"A nifty AST visitor that records all scope changes in the file.\"\"\"\n\n    # the value is a qualified name (without the module), e.g. \"Class.method\"\n    scopes: dict[LineNo, str]\n\n    # internal stack for correct naming in the scope\n    stack: list[FunctionName]\n\n    def __init__(self):\n        self.reset()\n        self.visit_Functiondef = self.handle_scopes\n        self.visit_AsyncFunctiondef = self.handle_scopes\n        self.visit_Classdef = self.handle_scopes\n        super().__init__()\n\n    def reset(self) -> None:\n        self.stack = []\n        self.scopes = {}\n\n    def handle_scopes(\n        self, node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef\n    ) -> None:\n        self.stack.append(node.name)\n        self.scopes[node.lineno] = \".\".join(self.stack)\n        self.generic_visit(node)\n        self.stack.pop()\n\n\nclass ScopeCache:\n    \"\"\"Returns qualified names of scopes for a given module path and lineno.\n\n    Caches both scopes from ScopeRecorder and queries about a given line number\n    (which is likely *inside* the function body).\n    \"\"\"\n\n    def __init__(self) -> None:\n        self.recorder = ScopeRecorder()\n        self.scopes: dict[ModulePath, dict[LineNo, str]] = {}\n        self.cache: dict[tuple[ModulePath, LineNo], str] = {}\n\n    def __getitem__(self, key: tuple[ModulePath, LineNo]) -> str:\n        if key not in self.cache:\n            try:\n                self.cache[key] = self._get_scope(key[0], key[1])\n            except FileNotFoundError:\n                self.cache[key] = \"\"\n        return self.cache[key]\n\n    def _get_scope(self, mod_path: ModulePath, wanted_lineno: LineNo) -> str:\n        if mod_path not in self.scopes:\n            with open(mod_path) as mod:\n                self.recorder.visit(ast.parse(mod.read(), mod_path))\n                self.scopes[mod_path] = self.recorder.scopes\n                self.recorder.reset()\n        last_scope = \"\"\n        for lineno, scope in sorted(self.scopes[mod_path].items()):\n            if lineno > wanted_lineno:\n                return last_scope\n\n            last_scope = scope\n\n        return last_scope\n\n\ndef count_calls(funcs: dict[FunctionID, Function]) -> Counter[Call]:\n    call_counter: Counter[Call] = Counter()\n\n    def _counts(caller: FunctionID, visited: set[Call], level: int = 0) -> None:\n        for callee in funcs[caller].calls:\n            call = caller, callee\n            call_counter[call] += 1\n            if call_counter[call] < 2 and call not in visited:\n                _counts(callee, visited | {call}, level + 1)\n\n    _counts(ROOT_ID, set())\n    return call_counter\n\n\ndef find_singledispatch_wrapper(\n    stats: Stats, *, regular_location: bool = False\n) -> FunctionID:\n    \"\"\"Returns the singledispatch wrapper function ID tuple.\n\n    Raises LookupError if not found.\n    \"\"\"\n    if regular_location:\n        functools_path = re.compile(r\"python3.\\d+/functools.py$\")\n        dispatch_name = \"dispatch\"\n        wrapper_name = \"wrapper\"\n    else:\n        functools_path = re.compile(r\"profiling/tracing_singledispatch.py$\")\n        dispatch_name = \"dispatch\"\n        wrapper_name = \"sd_wrapper\"\n\n    for (modpath, _lineno, funcname), (_, _, _, _, callers) in stats.items():\n        if funcname != dispatch_name:\n            continue\n\n        m = functools_path.search(modpath)\n        if not m:\n            continue\n\n        # Using this opportunity, we're figuring out which `wrapper` from\n        # functools in the trace is the singledispatch `wrapper` (there\n        # are three more others in functools.py).\n        for caller_modpath, caller_lineno, caller_funcname in callers:\n            if caller_funcname == wrapper_name:\n                m = functools_path.search(modpath)\n                if not m:\n                    continue\n\n                return (caller_modpath, caller_lineno, caller_funcname)\n\n        raise LookupError(\"singledispatch.dispatch without wrapper?\")\n\n    raise LookupError(\"No singledispatch use in provided stats\")\n\n\ndef filter_singledispatch_in_place(\n    stats: Stats,\n    dispatches: dict[FunctionID, CallCounts],\n    regular_location: bool = False,\n) -> None:\n    \"\"\"Removes singledispatch `wrapper` from the `stats.`\n\n    Given that:\n    - W is a wrapper function hiding original function O;\n    - D is the internal dispatching function of singledispatch;\n    - W calls D first to select which function to call;\n    - then, W calls the concrete registered implementations F1, F2, F3, and\n      rather rarely, O.\n\n    This filter changes this ( -> means \"calls\"):\n\n    A -> W -> F1\n    A -> W -> D\n\n    into this:\n\n    A -> F1\n    A -> D\n    \"\"\"\n\n    try:\n        wrapper = find_singledispatch_wrapper(\n            stats, regular_location=regular_location\n        )\n    except LookupError:\n        return\n\n    # Delete the function from stats\n    del stats[wrapper]\n\n    # Fix up all \"callers\" stats\n    singledispatch_functions = {d: (0, 0, 0, 0) for d in dispatches}\n    for funcid, (_, _, _, _, callers) in stats.items():\n        if wrapper not in callers:\n            continue\n\n        new_direct_calls = {}\n        for call_counts in dispatches.values():\n            for caller, calls in call_counts.items():\n                if funcid not in calls:\n                    continue\n\n                new_direct_calls[caller] = calls[funcid]\n\n        pcc, cc, tottime, cumtime = callers.pop(wrapper)\n        all_calls = sum(new_direct_calls.values())\n        if all_calls == 0:\n            count = len(singledispatch_functions)\n            for sdfid, old_stats in singledispatch_functions.items():\n                cur_stats = (\n                    round(pcc / count),\n                    round(cc / count),\n                    tottime / count,\n                    cumtime / count,\n                )\n                callers[sdfid] = cur_stats\n                new_stats = tuple(\n                    old_stats[i] + cur_stats[i] for i in range(4)\n                )\n                singledispatch_functions[sdfid] = new_stats  # type: ignore\n\n            continue\n\n        factor = all_calls / cc\n        pcc_fl = pcc * factor\n        cc_fl = cc * factor\n        tottime *= factor\n        cumtime *= factor\n\n        for caller, count in new_direct_calls.items():\n            factor = count / cc_fl\n            callers[caller] = (\n                round(pcc_fl * factor),\n                count,\n                tottime * factor,\n                cumtime * factor,\n            )\n\n    # Insert original single dispatch generic functions back\n    for sdfid, sd_stats in singledispatch_functions.items():\n        o_pcc, o_cc, o_tottime, o_cumtime, callers = stats.get(\n            sdfid, (0, 0, 0, 0, {})\n        )\n        stats[sdfid] = (\n            sd_stats[0] + o_pcc,\n            sd_stats[1] + o_cc,\n            sd_stats[2] + o_tottime,\n            sd_stats[3] + o_cumtime,\n            callers,\n        )\n\n\ndef build_svg_blocks(\n    funcs: dict[FunctionID, Function],\n    calls: dict[Call, Stat],\n    threshold: float,\n) -> tuple[list[Block], list[Block], float]:\n    call_stack_blocks: list[Block] = []\n    usage_blocks: list[Block] = []\n    counts: Counter[Call] = count_calls(funcs)\n    maxw = float(funcs[ROOT_ID].stat[3])\n\n    def _build_blocks_by_call_stack(\n        func: FunctionID,\n        scaled_timings: Stat,\n        *,\n        visited: AbstractSet[Call] = frozenset(),\n        level: int = 0,\n        origin: float = 0,\n        call_stack: tuple[FunctionID, ...] = (),\n        parent_call_count: int = 1,\n        parent_block: Optional[Block] = None,\n    ) -> None:\n        _, _, func_tt, func_tc = scaled_timings\n        pcc = parent_call_count\n        fchildren = [\n            (f, funcs[f], calls[func, f], max(counts[func, f], pcc))\n            for f in funcs[func].calls\n        ]\n        fchildren.sort(key=lambda elem: elem[0])\n        gchildren = [elem for elem in fchildren if elem[3] == 1]\n        bchildren = [elem for elem in fchildren if elem[3] > 1]\n        if bchildren:\n            gchildren_tc_sum = sum(r[2][3] for r in gchildren)\n            bchildren_tc_sum = sum(r[2][3] for r in bchildren)\n            rest = func_tc - func_tt - gchildren_tc_sum\n            if bchildren_tc_sum > 0:\n                factor = rest / bchildren_tc_sum\n            else:\n                factor = 1\n            # Round up and scale times and call counts.\n            bchildren = [\n                (\n                    f,\n                    ff,\n                    (\n                        round(cc * factor),\n                        round(nc * factor),\n                        tt * factor,\n                        tc * factor,\n                    ),\n                    ccnt,\n                )\n                for f, ff, (cc, nc, tt, tc), ccnt in bchildren\n            ]\n\n        for child, _, (cc, nc, tt, tc), call_count in gchildren + bchildren:\n            if tc / maxw < threshold:\n                origin += tc\n                continue\n\n            child_call_stack = call_stack + (child,)\n            tooltip = TOOLTIP.format(tc / maxw, cc, nc, tt, tc)\n            block = Block(\n                func=child,\n                call_stack=child_call_stack,\n                color=(parent_call_count == 1 and call_count > 1),\n                level=level,\n                tooltip=tooltip,\n                w=tc,\n                x=origin,\n            )\n            call_stack_blocks.append(block)\n            call = func, child\n            if call not in visited:\n                _build_blocks_by_call_stack(\n                    child,\n                    (cc, nc, tt, tc),\n                    level=level + 1,\n                    origin=origin,\n                    visited=visited | {call},\n                    call_stack=child_call_stack,\n                    parent_call_count=call_count,\n                    parent_block=block,\n                )\n            origin += tc\n\n    def _build_blocks_by_usage(\n        ids: Sequence[FunctionID],\n        *,\n        level: int = 0,\n        to: Optional[FunctionID] = None,\n        origin: float = 0,\n        visited: AbstractSet[Call] = frozenset(),\n        parent_width: float = 0,\n    ) -> None:\n        factor = 1.0\n        if ids and to is not None:\n            calls_tottime = sum(calls[fid, to][3] for fid in ids)\n            if calls_tottime:\n                factor = parent_width / calls_tottime\n\n        for fid in sorted(ids):\n            call = fid, to\n            if to is not None:\n                cc, nc, tt, tc = calls[call]  # type: ignore\n                ttt = tc * factor\n            else:\n                cc, nc, tt, tc = funcs[fid].stat\n                ttt = tt * factor\n\n            if ttt / maxw < threshold:\n                origin += ttt\n                continue\n\n            tooltip = TOOLTIP.format(tt / maxw, cc, nc, tt, tc)\n            block = Block(\n                func=fid,\n                call_stack=(),\n                color=2 if level > 0 else not funcs[fid].calls,\n                level=level,\n                tooltip=tooltip,\n                w=ttt,\n                x=origin,\n            )\n            usage_blocks.append(block)\n            if call not in visited:\n                _build_blocks_by_usage(\n                    funcs[fid].calledby,\n                    level=level + 1,\n                    to=fid,\n                    origin=origin,\n                    visited=visited | {call},\n                    parent_width=ttt,\n                )\n            origin += ttt\n\n    _build_blocks_by_call_stack(ROOT_ID, scaled_timings=(1, 1, maxw, maxw))\n    _build_blocks_by_usage([fid for fid in funcs if fid != ROOT_ID])\n    return call_stack_blocks, usage_blocks, maxw\n\n\ndef build_svg_blocks_by_memory(\n    root: MemoryFrame,\n    *,\n    maxw: int,\n    level: int = 0,\n    x: int = 0,\n    scope_cache: Optional[ScopeCache] = None,\n) -> Iterator[Block]:\n    if scope_cache is None:\n        scope_cache = ScopeCache()\n    for (mod_path, lineno), caller in root.callers.items():\n        func_name = scope_cache[mod_path, lineno]\n        line = linecache.getline(mod_path, lineno).strip()\n        if len(caller.callers) == 0 or level == 0:\n            color = 0\n        elif len(caller.callers) >= 2:\n            color = 1\n        else:\n            color = 2\n        yield Block(\n            func=(mod_path, lineno, func_name),\n            call_stack=(),\n            color=color,\n            level=level,\n            tooltip=(\n                f\"{caller.size / 1024:.2f} KiB / {caller.blocks}\"\n                f\" blocks \\N{RIGHTWARDS DOUBLE ARROW} {line}\"\n            ),\n            w=caller.size,\n            x=x,\n        )\n        yield from build_svg_blocks_by_memory(\n            caller, maxw=maxw, level=level + 1, x=x, scope_cache=scope_cache,\n        )\n        x += caller.size\n\n\ndef render_svg_section(\n    blocks: list[Block],\n    maxw: float,\n    colors: list[list[RGB]],\n    block_height: int,\n    font_size: int,\n    width: int,\n    javascript: str = \"\",\n    invert: bool = False,\n) -> str:\n    maxlevel = max(r.level for r in blocks) + 1\n    height = (maxlevel + 1) * block_height\n    top = 0 if not invert else 3 * block_height\n    content = []\n    for b in blocks:\n        x = b.x * width / maxw\n        tx = block_height / 6\n        y = b.level\n        if invert:\n            y = maxlevel - y\n        y = top + height - y * block_height - block_height\n        ty = block_height / 2\n        w = max(1, b.w * width / maxw - 1)\n        bcolors = colors[b.color]\n        fill = bcolors[int(len(bcolors) * gradient_from_name(b.id))]\n        content.append(\n            ELEM.format(\n                w=w,\n                x=x,\n                y=y,\n                tx=tx,\n                ty=ty,\n                name=saxutils.escape(b.name),\n                full_name=saxutils.escape(b.full_name),\n                font_size=font_size,\n                h=block_height - 1,\n                fill=fill,\n                upsidedown=\"true\" if invert else \"false\",\n            )\n        )\n    height += block_height\n    content.append(\n        DETAILS.format(\n            font_size=font_size, y=2 * block_height if invert else height\n        )\n    )\n    result = SVG.format(\n        \"\\n\".join(content),\n        javascript=javascript,\n        width=width,\n        height=top + height + block_height,\n        unzoom_button_x=width - 100,\n        ui_font_size=1.33 * font_size,\n    )\n    return result\n\n\ndef render_svg(\n    stats: Stats,\n    call_out: pathlib.Path | str,\n    usage_out: pathlib.Path | str,\n    *,\n    threshold: float = 0.00001,  # 1.0 is 100%\n    width: int = 1920,  # in pixels\n    block_height: int = 24,  # in pixels\n    font_size: int = 12,\n    raw: bool = False,\n) -> None:\n    \"\"\"Render an SVG file to `call_out` and `usage_out`.\n\n    Raises ValueError if rendering cannot be done with the given `stats`.\n\n    Functions whose runtime is below `threshold` percentage are not included.\n    Unless `raw` is True, functions are filtered to exclude common wrappers\n    that make the resulting SVG too busy but are themselves harmless.\n    \"\"\"\n    funcs, calls = calc_callers(stats, threshold)\n    call_blocks, usage_blocks, maxw = build_svg_blocks(\n        funcs, calls, threshold=threshold\n    )\n    with PROFILING_JS.open() as js_file:\n        javascript = js_file.read()\n    if call_blocks:\n        call_svg = render_svg_section(\n            call_blocks,\n            maxw,\n            [COLORS, CCOLORS],\n            block_height=block_height,\n            font_size=font_size,\n            width=width,\n            javascript=javascript,\n        )\n        with open(call_out, \"w\") as outf:\n            outf.write(call_svg)\n    if usage_blocks:\n        usage_svg = render_svg_section(\n            usage_blocks,\n            maxw,\n            [COLORS, ECOLORS, DCOLORS],\n            block_height=block_height,\n            font_size=font_size,\n            width=width,\n            javascript=javascript,\n        )\n        with open(usage_out, \"w\") as outf:\n            outf.write(usage_svg)\n\n\ndef render_memory_svg(\n    stats: MemoryFrame,\n    out: pathlib.Path | str,\n    *,\n    width: int = 1920,  # in pixels\n    block_height: int = 24,  # in pixels\n    font_size: int = 12,\n):\n    with PROFILING_JS.open() as js_file:\n        javascript = js_file.read()\n    maxw = stats.size\n    mem_blocks = list(build_svg_blocks_by_memory(stats, maxw=maxw))\n    mem_svg = render_svg_section(\n        mem_blocks,\n        maxw,\n        [COLORS, CCOLORS, DCOLORS],\n        block_height=block_height,\n        font_size=font_size,\n        width=width,\n        javascript=javascript,\n        invert=True,\n    )\n    with open(out, \"w\") as outf:\n        outf.write(mem_svg)\n\n\nSVG = \"\"\"\\\n<?xml version=\"1.0\" standalone=\"no\"?>\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \\\n\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<svg version=\"1.1\" width=\"{width}\" height=\"{height}\"\n xmlns=\"http://www.w3.org/2000/svg\"\n xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n onload=\"init(evt)\"\n class=\"default\">\n<style type=\"text/css\">\n .func_g {{ font-family: arial }}\n .func_g:hover {{ stroke:black; stroke-width:0.5; cursor:pointer; }}\n</style>\n<script type=\"text/ecmascript\">\n<![CDATA[\n{javascript}\n]]>\n</script>\n<text id=\"unzoom\" onclick=\"unzoom()\"\n text-anchor=\"\" x=\"{unzoom_button_x}\" y=\"24\"\n font-size=\"{ui_font_size}\" font-family=\"arial\"\n fill=\"rgb(0,0,0)\" style=\"opacity:0.0;cursor:pointer\" >Reset Zoom</text>\n<text id=\"search\"\n onmouseover=\"searchover()\" onmouseout=\"searchout()\" onclick=\"search_prompt()\"\n text-anchor=\"\" x=\"10\" y=\"24\"\n font-size=\"{ui_font_size}\" font-family=\"arial\"\n fill=\"rgb(0,0,0)\" style=\"opacity:0.1;cursor:pointer\" >Search</text>\n{}\n</svg>\"\"\"\n\nELEM = \"\"\"\\\n<svg class=\"func_g\" x=\"{x}\" y=\"{y}\" width=\"{w}\" height=\"{h}\"\n onclick=\"zoom(this, {upsidedown})\" onmouseover=\"s(this)\" onmouseout=\"s()\">\n    <title>{full_name}</title>\n    <rect height=\"100%\" width=\"100%\" fill=\"rgb({fill.r},{fill.g},{fill.b})\"\n     rx=\"2\" ry=\"2\" />\n    <text text-anchor=\"\" x=\"{tx}\" y=\"{ty}\"\n     font-size=\"{font_size}px\" fill=\"rgb(0,0,0)\">{name}</text>\n</svg>\"\"\"\n\nDETAILS = \"\"\"\n<text id=\"details\" text-anchor=\"\" x=\"10.00\" y=\"{y}\"\n font-family=\"arial\" font-size=\"{font_size}\" font-weight=\"bold\"\n fill=\"rgb(0,0,0)\"> </text>\n\"\"\"\n\nTOOLTIP = \"{0:.2%} (calls={1} pcalls={2} tottime={3:.2f} cumtime={4:.2f})\"\n"
  },
  {
    "path": "edb/tools/profiling/svg_helpers.js",
    "content": "/*\nThe contents of this file are subject to the terms of the\nCommon Development and Distribution License (the \"License\").\nYou may not use this file except in compliance with the License.\n\nYou can obtain a copy of the license at docs/cddl1.txt or\nhttp://opensource.org/licenses/CDDL-1.0.\nSee the License for the specific language governing permissions\nand limitations under the License.\n\nWhen distributing Covered Code, include this CDDL HEADER in each\nfile and include the License file at docs/cddl1.txt.\n\nPortions Copyright 2019 EdgeDB Inc.\nCopyright 2016 Netflix, Inc.\nCopyright 2011 Joyent, Inc.  All rights reserved.\nCopyright 2011 Brendan Gregg.  All rights reserved.\n*/\n\nvar details, searchbtn, svg, searching;\nfunction init(evt) {\n    details = document.getElementById(\"details\").firstChild;\n    searchbtn = document.getElementById(\"search\");\n    svg = document.getElementsByTagName(\"svg\")[0];\n    searching = 0;\n}\n\n// mouse-over for info\nfunction s(node) {\n    value = \"\";\n    if (node !== undefined) {\n        title = node.getElementsByTagName(\"title\");\n        if (title.length == 1) {\n            value = \"Function: \" + title[0].textContent;\n        }\n    }\n    details.nodeValue = value;\n}\n\n// ctrl-F for search\nwindow.addEventListener(\"keydown\",function (e) {\n    if (e.keyCode === 114 || (e.ctrlKey && e.keyCode === 70)) {\n        e.preventDefault();\n        search_prompt();\n    }\n})\n\n// functions\nfunction find_child(parent, name, attr) {\n    var children = parent.childNodes;\n    for (var i=0; i<children.length;i++) {\n        if (children[i].tagName == name)\n            return (attr != undefined) ? children[i].attributes[attr].value : children[i];\n    }\n    return;\n}\nfunction orig_save(e, attr, val) {\n    if (e.attributes[\"_orig_\"+attr] != undefined) return;\n    if (e.attributes[attr] == undefined) return;\n    if (val == undefined) val = e.attributes[attr].value;\n    e.setAttribute(\"_orig_\"+attr, val);\n}\nfunction orig_load(e, attr) {\n    if (e.attributes[\"_orig_\"+attr] == undefined) return;\n    e.attributes[attr].value = e.attributes[\"_orig_\"+attr].value;\n    e.removeAttribute(\"_orig_\"+attr);\n}\nfunction g_to_text(e) {\n    var text = find_child(e, \"title\").firstChild.nodeValue;\n    return (text)\n}\nfunction g_to_func(e) {\n    var func = g_to_text(e);\n    if (func != null)\n        func = func.replace(/ .*/, \"\");\n    return (func);\n}\n\n// zoom\nfunction zoom_reset(e) {\n    if (e.attributes != undefined) {\n        orig_load(e, \"x\");\n        orig_load(e, \"width\");\n    }\n    if (e.childNodes == undefined) return;\n    for(var i=0, c=e.childNodes; i<c.length; i++) {\n        zoom_reset(c[i]);\n    }\n}\nfunction zoom_child(e, x, ratio) {\n    if(e.tagName != \"svg\")  {\n        return;\n    }\n    if (e.attributes != undefined) {\n        if (e.attributes[\"x\"] != undefined) {\n            orig_save(e, \"x\");\n            e.attributes[\"x\"].value = (parseFloat(e.attributes[\"x\"].value) - x) * ratio;\n        }\n        if (e.attributes[\"width\"] != undefined) {\n            orig_save(e, \"width\");\n            e.attributes[\"width\"].value = parseFloat(e.attributes[\"width\"].value) * ratio;\n        }\n    }\n\n    if (e.childNodes == undefined) return;\n    for(var i=0, c=e.childNodes; i<c.length; i++) {\n        zoom_child(c[i], x, ratio);\n    }\n}\nfunction zoom_parent(e) {\n    if(e.tagName != \"svg\")  {\n        return;\n    }\n    if (e.attributes) {\n        if (e.attributes[\"x\"] != undefined) {\n            orig_save(e, \"x\");\n            e.attributes[\"x\"].value = 0;\n        }\n        if (e.attributes[\"width\"] != undefined) {\n            orig_save(e, \"width\");\n            e.attributes[\"width\"].value = parseInt(svg.width.baseVal.value);\n        }\n    }\n    if (e.childNodes == undefined) return;\n    for(var i=0, c=e.childNodes; i<c.length; i++) {\n        zoom_parent(c[i]);\n    }\n}\nfunction zoom(node, upsidedown) {\n    var attr = node.attributes;\n    var width = parseFloat(attr[\"width\"].value);\n    var xmin = parseFloat(attr[\"x\"].value);\n    var xmax = parseFloat(xmin + width);\n    var ymin = parseFloat(attr[\"y\"].value);\n    var ratio = (svg.width.baseVal.value) / width;\n\n    // XXX: Workaround for JavaScript float issues (fix me)\n    var fudge = 0.0001;\n\n    var unzoombtn = document.getElementById(\"unzoom\");\n    unzoombtn.style[\"opacity\"] = \"1.0\";\n\n    var el = document.getElementsByTagName(\"svg\");\n    for(var i=0;i<el.length;i++){\n        var e = el[i];\n        var a = e.attributes;\n        if (a[\"class\"].value !== \"func_g\")\n            continue;\n\n        var ex = parseFloat(a[\"x\"].value);\n        var ew = parseFloat(a[\"width\"].value);\n\n        // Is it an ancestor\n        if (upsidedown === true) {\n            var upstack = parseFloat(a[\"y\"].value) < ymin;\n        } else {\n            var upstack = parseFloat(a[\"y\"].value) > ymin;\n        }\n        if (upstack) {\n            // Direct ancestor\n            if (ex <= xmin && (ex+ew+fudge) >= xmax) {\n                e.style[\"opacity\"] = \"0.5\";\n                zoom_parent(e);\n                e.onclick = function(e){unzoom(); zoom(this, upsidedown);};\n                //#update_text(e);\n            }\n            // not in current path\n            else\n                e.style[\"display\"] = \"none\";\n        }\n        // Children maybe\n        else {\n            // no common path\n            if (ex < xmin || ex + fudge >= xmax) {\n                e.style[\"display\"] = \"none\";\n            }\n            else {\n                zoom_child(e, xmin, ratio);\n                e.onclick = function(e){zoom(this, upsidedown);};\n                //#update_text(e);\n            }\n        }\n    }\n}\nfunction unzoom() {\n    var unzoombtn = document.getElementById(\"unzoom\");\n    unzoombtn.style[\"opacity\"] = \"0.0\";\n\n    var el = document.getElementsByTagName(\"svg\");\n    for(i=0;i<el.length;i++) {\n        var e = el[i];\n        if (e.attributes[\"class\"].value !== \"func_g\")\n            continue;\n        e.style[\"display\"] = \"block\";\n        e.style[\"opacity\"] = \"1\";\n        zoom_reset(e);\n        //#update_text(e);\n    }\n}\n\n// search\nfunction reset_search() {\n    var el = document.getElementsByTagName(\"rect\");\n    for (var i=0; i < el.length; i++){\n        orig_load(el[i], \"fill\")\n    }\n}\nfunction search_prompt() {\n    if (!searching) {\n        var term = prompt(\"Enter a search term (regexp \" +\n            \"allowed, eg: ^compile_)\", \"\");\n        if (term != null) {\n            search(term)\n        }\n    } else {\n        reset_search();\n        searching = 0;\n        searchbtn.style[\"opacity\"] = \"0.1\";\n        searchbtn.firstChild.nodeValue = \"Search\"\n    }\n}\nfunction search(term) {\n    var re = new RegExp(term);\n    var el = document.getElementsByTagName(\"svg\");\n    for (var i = 0; i < el.length; i++) {\n        var e = el[i];\n        if (e.attributes[\"class\"].value != \"func_g\")\n            continue;\n        var func = g_to_func(e);\n        var rect = find_child(e, \"rect\");\n        if (func == null || rect == null)\n            continue;\n\n        if (func.match(re)) {\n            // highlight\n            orig_save(rect, \"fill\");\n            rect.attributes[\"fill\"].value = \"rgb(230,0,230)\";\n            searching = 1;\n        }\n    }\n    if (!searching)\n        return;\n\n    searchbtn.style[\"opacity\"] = \"1.0\";\n    searchbtn.firstChild.nodeValue = \"Reset Search\"\n}\nfunction searchover(e) {\n    searchbtn.style[\"opacity\"] = \"1.0\";\n}\nfunction searchout(e) {\n    if (searching) {\n        searchbtn.style[\"opacity\"] = \"1.0\";\n    } else {\n        searchbtn.style[\"opacity\"] = \"0.1\";\n    }\n}\n"
  },
  {
    "path": "edb/tools/profiling/tracing_singledispatch.py",
    "content": "# mypy: ignore-errors\n\n# Portions copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n# Portions copyright 2001-2019 Python Software Foundation.\n# License: PSFL.\n\n\"\"\"A replacement for functools.singledispatch() that records usage.\n\nSee README.md in this package for more details.\n\"\"\"\n\n\nfrom __future__ import annotations\nfrom typing import (\n    Any,\n    Callable,\n    TYPE_CHECKING,\n)\n\nimport functools\nimport sys\nimport threading\n\n\n# Aliases to minimize code changes in the vendored singledispatch below.\nget_cache_token = functools.get_cache_token\nupdate_wrapper = functools.update_wrapper\n_find_impl = functools._find_impl\n\n\nprofiling_in_progress = threading.Event()\n\n\nif TYPE_CHECKING:\n    ModulePath = str\n    LineNo = int\n    FunctionName = str\n    FunctionID = tuple[ModulePath, LineNo, FunctionName]\n    Caller = FunctionID\n    Callee = FunctionID\n    CallCount = int\n    CallCounts = dict[Caller, dict[Callee, CallCount]]\n\n\ndone_dispatches: dict[FunctionID, CallCounts] = {}\n\n\n# Taken from Python 3.8.0+ (051ff526b5dc2c40c4a53d87089740358822edfa)\n# The only change is the `wrapper` function implementation, replaced with\n# `sd_wrapper` for clarity.\ndef tracing_singledispatch[T: Callable[..., Any]](func: T) -> T:\n    \"\"\"Single-dispatch generic function decorator.\n\n    Transforms a function into a generic function, which can have different\n    behaviours depending upon the type of its first argument. The decorated\n    function acts as the default implementation, and additional\n    implementations can be registered using the register() attribute of the\n    generic function.\n    \"\"\"\n    # There are many programs that use functools without singledispatch, so we\n    # trade-off making singledispatch marginally slower for the benefit of\n    # making start-up of such applications slightly faster.\n    import types\n    import weakref\n\n    registry = {}\n    dispatch_cache = weakref.WeakKeyDictionary()\n    cache_token = None\n\n    def dispatch(cls):\n        \"\"\"generic_func.dispatch(cls) -> <function implementation>\n\n        Runs the dispatch algorithm to return the best available implementation\n        for the given *cls* registered on *generic_func*.\n\n        \"\"\"\n        nonlocal cache_token\n        if cache_token is not None:\n            current_token = get_cache_token()\n            if cache_token != current_token:\n                dispatch_cache.clear()\n                cache_token = current_token\n        try:\n            impl = dispatch_cache[cls]\n        except KeyError:\n            try:\n                impl = registry[cls]\n            except KeyError:\n                impl = _find_impl(cls, registry)\n            dispatch_cache[cls] = impl\n        return impl\n\n    def register(cls, func=None):\n        \"\"\"generic_func.register(cls, func) -> func\n\n        Registers a new implementation for the given *cls* on a *generic_func*.\n\n        \"\"\"\n        nonlocal cache_token\n        if func is None:\n            if isinstance(cls, type):\n                return lambda f: register(cls, f)\n            ann = getattr(cls, '__annotations__', {})\n            if not ann:\n                raise TypeError(\n                    f\"Invalid first argument to `register()`: {cls!r}. \"\n                    f\"Use either `@register(some_class)` or plain `@register` \"\n                    f\"on an annotated function.\"\n                )\n            func = cls\n\n            # only import typing if annotation parsing is necessary\n            from typing import get_type_hints\n            argname, cls = next(iter(get_type_hints(func).items()))\n            if not isinstance(cls, type):\n                raise TypeError(\n                    f\"Invalid annotation for {argname!r}. \"\n                    f\"{cls!r} is not a class.\"\n                )\n        registry[cls] = func\n        if cache_token is None and hasattr(cls, '__abstractmethods__'):\n            cache_token = get_cache_token()\n        dispatch_cache.clear()\n        return func\n\n    def sd_wrapper(*args, **kw):\n        if not args:\n            raise TypeError(f'{funcname} requires at least '\n                            '1 positional argument')\n\n        impl = dispatch(args[0].__class__)\n        if profiling_in_progress.is_set():\n            caller = sys._getframe().f_back.f_code\n            caller_id = (\n                caller.co_filename,\n                caller.co_firstlineno,\n                caller.co_name,\n            )\n            impl_id = (\n                impl.__code__.co_filename,\n                impl.__code__.co_firstlineno,\n                impl.__code__.co_name,\n            )\n            our_dispatches = done_dispatches.setdefault(func_id, {})\n            caller_dispatches = our_dispatches.setdefault(caller_id, {})\n            caller_dispatches[impl_id] = caller_dispatches.get(impl_id, 0) + 1\n        return impl(*args, **kw)\n\n    funcname = getattr(func, '__name__', 'singledispatch function')\n    registry[object] = func\n    _fcode = func.__code__\n    func_id = (_fcode.co_filename, _fcode.co_firstlineno, _fcode.co_name)\n    wrapper = sd_wrapper\n    wrapper.register = register\n    wrapper.dispatch = dispatch\n    wrapper.registry = types.MappingProxyType(registry)\n    wrapper._clear_cache = dispatch_cache.clear\n    update_wrapper(wrapper, func)\n    return wrapper\n\n\ndef patch_functools() -> None:\n    functools.singledispatch = tracing_singledispatch\n"
  },
  {
    "path": "edb/tools/pygments/__init__.py",
    "content": ""
  },
  {
    "path": "edb/tools/pygments/edgeql/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom pygments.lexer import RegexLexer, include\nfrom pygments import token\n\nfrom . import meta\n\n\n__all__ = ['EdgeQLLexer']\n\n\nunreserved_keywords = meta.EdgeQL.unreserved_keywords\nreserved_keywords = meta.EdgeQL.reserved_keywords\nbuiltins = sorted(set(\n    meta.EdgeQL.type_builtins + meta.EdgeQL.constraint_builtins +\n    meta.EdgeQL.fn_builtins))\nstdmodules = meta.EdgeQL.module_builtins\n# Operators need to be sorted from longest to shortest to match\n# correctly. Lexicographical sort is added on top of that for\n# stability, but is not itself important.\noperators = sorted(meta.EdgeQL.operators,\n                   key=lambda x: (len(x), x), reverse=True)\n# the operator symbols need to be escaped\noperators = ['\\\\' + '\\\\'.join(op) for op in operators]\n\n# navigation punctuation needs to be processed similar to operators\nnavigation = sorted(meta.EdgeQL.navigation,\n                    key=lambda x: (len(x), x), reverse=True)\n# the operator symbols need to be escaped\nnavigation = ['\\\\' + '\\\\'.join(nav) for nav in navigation\n              # exclude '.' for the moment\n              if nav != '.']\n\n\nclass EdgeQLLexer(RegexLexer):\n    name = 'EdgeQL'\n    aliases = ['edgeql', 'esdl']\n    filenames = ['*.edgeql', '*.esdl']\n\n    tokens = {\n        'root': [\n            include('comments'),\n            (fr\"(?x)({' | '.join(operators)})\", token.Operator),\n            (fr\"(?x)({' | '.join(navigation)})\", token.Punctuation.Navigation),\n            include('keywords'),\n            (r'@\\w+', token.Name.Decorator),\n            (r'\\$[\\w\\d]+', token.Name.Variable),\n            include('numbers'),\n            include('strings'),\n            (r'(?i)\\b(true|false)\\b', token.Keyword.Constant),\n            (r'\\s+', token.Text),\n            (r'.', token.Text),\n        ],\n        'comments': [\n            (r'#.*?\\n', token.Comment.Singleline),\n        ],\n        'keywords': [\n            (r'(?i)\\b(?<![:\\.<>@])(__source__|__subject__)\\b',\n             token.Name.Builtin.Pseudo),\n\n            (r'\\b(__type__)\\b', token.Name.Builtin.Pseudo),\n\n            (fr'''(?ix)\n                \\b(?<![:\\.<>@])(\n                    {' | '.join(reserved_keywords)}\n                )\\b''', token.Keyword.Reserved),\n\n            # Unreserved keywords should lose their special meaning\n            # when part of a path or fully-qualified name.\n            (fr'''(?ix)\n                \\b(?<![:\\.<>@])(\n                    {' | '.join(unreserved_keywords)}\n                )\\b(?![:\\.<>@])''', token.Keyword.Reserved),\n\n            (fr'''(?x)\n                \\b(?<!\\.)(\n                    {' | '.join(stdmodules)}\n                )\\b(?=::)''', token.Name.Builtin),\n\n            (fr'''(?x)\n                \\b(?<!\\.)(\n                    {' | '.join(builtins)}\n                )\\b''', token.Name.Builtin),\n        ],\n        'strings': [\n            (r'''(?x)\n                (?P<Q>(r?)['\"])\n                (?:\n                    (\\\\['\"] | \\n | .)*?\n                )\n                (?P=Q)\n            ''', token.String),\n            (r'''(?x)\n                (?P<Q>\n                    # capture the opening quote in group Q\n                    (\n                        \\$(?:[A-Za-z_][A-Za-z_0-9]*)?\\$\n                    )\n                )\n                (?:\n                    (\\\\['\"] | \\n | .)*?\n                )\n                (?P=Q)\n            ''', token.String.Other),\n            (r'`.*?`', token.String.Backtick)\n        ],\n        'numbers': [\n            (r'''(?x)\n                (?<!\\w)\n                    (?:\n                        (?: \\d+ (?:\\.\\d+)?\n                            (?:[eE](?:[+\\-])?[0-9]+)\n                        )\n                        |\n                        (?: \\d+\\.\\d+)\n                    )\n                    n?\n            ''', token.Number),\n            (r'(?<!\\w)\\d+n?', token.Number),\n        ],\n    }\n"
  },
  {
    "path": "edb/tools/pygments/edgeql/meta.py",
    "content": "# AUTOGENERATED BY Gel WITH\n#     $ edb gen-meta-grammars edgeql\n\n\nfrom __future__ import annotations\n\n\nclass EdgeQL:\n    reserved_keywords = (\n        \"__default__\",\n        \"__edgedbsys__\",\n        \"__edgedbtpl__\",\n        \"__new__\",\n        \"__old__\",\n        \"__source__\",\n        \"__specified__\",\n        \"__std__\",\n        \"__subject__\",\n        \"__type__\",\n        \"administer\",\n        \"alter\",\n        \"analyze\",\n        \"and\",\n        \"anyarray\",\n        \"anyobject\",\n        \"anytuple\",\n        \"anytype\",\n        \"begin\",\n        \"by\",\n        \"case\",\n        \"check\",\n        \"commit\",\n        \"configure\",\n        \"create\",\n        \"deallocate\",\n        \"delete\",\n        \"describe\",\n        \"detached\",\n        \"discard\",\n        \"distinct\",\n        \"do\",\n        \"drop\",\n        \"else\",\n        \"end\",\n        \"except\",\n        \"exists\",\n        \"explain\",\n        \"extending\",\n        \"fetch\",\n        \"filter\",\n        \"for\",\n        \"get\",\n        \"global\",\n        \"grant\",\n        \"group\",\n        \"if\",\n        \"ilike\",\n        \"import\",\n        \"in\",\n        \"insert\",\n        \"intersect\",\n        \"introspect\",\n        \"is\",\n        \"like\",\n        \"limit\",\n        \"listen\",\n        \"load\",\n        \"lock\",\n        \"match\",\n        \"module\",\n        \"move\",\n        \"never\",\n        \"not\",\n        \"notify\",\n        \"offset\",\n        \"on\",\n        \"optional\",\n        \"or\",\n        \"over\",\n        \"partition\",\n        \"prepare\",\n        \"raise\",\n        \"refresh\",\n        \"revoke\",\n        \"rollback\",\n        \"select\",\n        \"set\",\n        \"single\",\n        \"start\",\n        \"typeof\",\n        \"union\",\n        \"update\",\n        \"variadic\",\n        \"when\",\n        \"window\",\n        \"with\",\n    )\n    unreserved_keywords = (\n        \"abort\",\n        \"abstract\",\n        \"access\",\n        \"after\",\n        \"alias\",\n        \"all\",\n        \"allow\",\n        \"annotation\",\n        \"applied\",\n        \"as\",\n        \"asc\",\n        \"assignment\",\n        \"before\",\n        \"blobal\",\n        \"branch\",\n        \"cardinality\",\n        \"cast\",\n        \"committed\",\n        \"config\",\n        \"conflict\",\n        \"constraint\",\n        \"cube\",\n        \"current\",\n        \"data\",\n        \"database\",\n        \"ddl\",\n        \"declare\",\n        \"default\",\n        \"deferrable\",\n        \"deferred\",\n        \"delegated\",\n        \"deny\",\n        \"desc\",\n        \"each\",\n        \"empty\",\n        \"expression\",\n        \"extension\",\n        \"final\",\n        \"first\",\n        \"force\",\n        \"from\",\n        \"function\",\n        \"future\",\n        \"implicit\",\n        \"index\",\n        \"infix\",\n        \"inheritable\",\n        \"instance\",\n        \"into\",\n        \"isolation\",\n        \"json\",\n        \"last\",\n        \"link\",\n        \"migration\",\n        \"multi\",\n        \"named\",\n        \"object\",\n        \"of\",\n        \"only\",\n        \"onto\",\n        \"operator\",\n        \"optionality\",\n        \"order\",\n        \"orphan\",\n        \"overloaded\",\n        \"owned\",\n        \"package\",\n        \"policy\",\n        \"populate\",\n        \"postfix\",\n        \"prefix\",\n        \"property\",\n        \"proposed\",\n        \"pseudo\",\n        \"read\",\n        \"reject\",\n        \"release\",\n        \"rename\",\n        \"repeatable\",\n        \"required\",\n        \"reset\",\n        \"restrict\",\n        \"rewrite\",\n        \"role\",\n        \"roles\",\n        \"rollup\",\n        \"savepoint\",\n        \"scalar\",\n        \"schema\",\n        \"sdl\",\n        \"serializable\",\n        \"session\",\n        \"source\",\n        \"superuser\",\n        \"system\",\n        \"target\",\n        \"template\",\n        \"ternary\",\n        \"text\",\n        \"then\",\n        \"to\",\n        \"transaction\",\n        \"trigger\",\n        \"type\",\n        \"unless\",\n        \"using\",\n        \"verbose\",\n        \"version\",\n        \"view\",\n        \"write\",\n    )\n    bool_literals = (\n        \"false\",\n        \"true\",\n    )\n    type_builtins = (\n        \"Base64Alphabet\",\n        \"BaseObject\",\n        \"ElasticLanguage\",\n        \"Endian\",\n        \"FreeObject\",\n        \"JsonEmpty\",\n        \"Language\",\n        \"LuceneLanguage\",\n        \"Method\",\n        \"Object\",\n        \"PGLanguage\",\n        \"RequestFailureKind\",\n        \"RequestState\",\n        \"Response\",\n        \"ScheduledRequest\",\n        \"Weight\",\n        \"anycontiguous\",\n        \"anydiscrete\",\n        \"anyenum\",\n        \"anyfloat\",\n        \"anyint\",\n        \"anynumeric\",\n        \"anypoint\",\n        \"anyreal\",\n        \"anyscalar\",\n        \"array\",\n        \"bigint\",\n        \"bool\",\n        \"bytes\",\n        \"date\",\n        \"date_duration\",\n        \"datetime\",\n        \"decimal\",\n        \"document\",\n        \"duration\",\n        \"enum\",\n        \"float32\",\n        \"float64\",\n        \"int16\",\n        \"int32\",\n        \"int64\",\n        \"interval\",\n        \"json\",\n        \"local_date\",\n        \"local_datetime\",\n        \"local_time\",\n        \"multirange\",\n        \"range\",\n        \"relative_duration\",\n        \"sequence\",\n        \"str\",\n        \"timestamp\",\n        \"timestamptz\",\n        \"tuple\",\n        \"uuid\",\n    )\n    module_builtins = (\n        \"cal\",\n        \"cfg\",\n        \"enc\",\n        \"ext\",\n        \"fts\",\n        \"http\",\n        \"math\",\n        \"net\",\n        \"pg\",\n        \"schema\",\n        \"std\",\n        \"sys\",\n    )\n    constraint_builtins = (\n        \"constraint\",\n        \"exclusive\",\n        \"expression\",\n        \"len_value\",\n        \"max_ex_value\",\n        \"max_len_value\",\n        \"max_value\",\n        \"min_ex_value\",\n        \"min_len_value\",\n        \"min_value\",\n        \"one_of\",\n        \"regexp\",\n    )\n    fn_builtins = (\n        \"abs\",\n        \"acos\",\n        \"adjacent\",\n        \"all\",\n        \"any\",\n        \"array_agg\",\n        \"array_fill\",\n        \"array_get\",\n        \"array_insert\",\n        \"array_join\",\n        \"array_replace\",\n        \"array_set\",\n        \"array_unpack\",\n        \"asin\",\n        \"assert\",\n        \"assert_distinct\",\n        \"assert_exists\",\n        \"assert_single\",\n        \"atan\",\n        \"atan2\",\n        \"bit_and\",\n        \"bit_count\",\n        \"bit_lshift\",\n        \"bit_not\",\n        \"bit_or\",\n        \"bit_rshift\",\n        \"bit_xor\",\n        \"bounded_above\",\n        \"bounded_below\",\n        \"bytes_get_bit\",\n        \"ceil\",\n        \"contains\",\n        \"cos\",\n        \"cot\",\n        \"count\",\n        \"date_get\",\n        \"datetime_current\",\n        \"datetime_get\",\n        \"datetime_of_statement\",\n        \"datetime_of_transaction\",\n        \"datetime_truncate\",\n        \"duration_get\",\n        \"duration_normalize_days\",\n        \"duration_normalize_hours\",\n        \"duration_to_seconds\",\n        \"duration_truncate\",\n        \"enumerate\",\n        \"find\",\n        \"floor\",\n        \"get_current_branch\",\n        \"get_current_database\",\n        \"get_instance_name\",\n        \"get_transaction_isolation\",\n        \"get_version\",\n        \"get_version_as_str\",\n        \"json_array_unpack\",\n        \"json_get\",\n        \"json_object_pack\",\n        \"json_object_unpack\",\n        \"json_set\",\n        \"json_typeof\",\n        \"len\",\n        \"lg\",\n        \"ln\",\n        \"log\",\n        \"materialized\",\n        \"max\",\n        \"mean\",\n        \"min\",\n        \"multirange\",\n        \"multirange_unpack\",\n        \"overlaps\",\n        \"pi\",\n        \"random\",\n        \"range\",\n        \"range_get_lower\",\n        \"range_get_upper\",\n        \"range_is_empty\",\n        \"range_is_inclusive_lower\",\n        \"range_is_inclusive_upper\",\n        \"range_unpack\",\n        \"re_match\",\n        \"re_match_all\",\n        \"re_replace\",\n        \"re_test\",\n        \"reset_query_stats\",\n        \"round\",\n        \"search\",\n        \"sequence_next\",\n        \"sequence_reset\",\n        \"sin\",\n        \"sqrt\",\n        \"stddev\",\n        \"stddev_pop\",\n        \"str_lower\",\n        \"str_lpad\",\n        \"str_ltrim\",\n        \"str_pad_end\",\n        \"str_pad_start\",\n        \"str_repeat\",\n        \"str_replace\",\n        \"str_reverse\",\n        \"str_rpad\",\n        \"str_rtrim\",\n        \"str_split\",\n        \"str_title\",\n        \"str_trim\",\n        \"str_trim_end\",\n        \"str_trim_start\",\n        \"str_upper\",\n        \"strictly_above\",\n        \"strictly_below\",\n        \"sum\",\n        \"tan\",\n        \"time_get\",\n        \"to_bigint\",\n        \"to_bytes\",\n        \"to_date_duration\",\n        \"to_datetime\",\n        \"to_decimal\",\n        \"to_duration\",\n        \"to_float32\",\n        \"to_float64\",\n        \"to_int16\",\n        \"to_int32\",\n        \"to_int64\",\n        \"to_json\",\n        \"to_local_date\",\n        \"to_local_datetime\",\n        \"to_local_time\",\n        \"to_relative_duration\",\n        \"to_str\",\n        \"to_uuid\",\n        \"uuid_generate_v1mc\",\n        \"uuid_generate_v4\",\n        \"var\",\n        \"var_pop\",\n        \"with_options\",\n    )\n    index_builtins = (\n        \"brin\",\n        \"btree\",\n        \"gin\",\n        \"gist\",\n        \"hash\",\n        \"index\",\n        \"spgist\",\n    )\n    operators = (\n        \"!=\",\n        \"%\",\n        \"*\",\n        \"+\",\n        \"++\",\n        \"-\",\n        \"/\",\n        \"//\",\n        \":=\",\n        \"<\",\n        \"<=\",\n        \"=\",\n        \">\",\n        \">=\",\n        \"?!=\",\n        \"?=\",\n        \"??\",\n        \"^\",\n    )\n    navigation = (\n        \".<\",\n        \".>\",\n        \"@\",\n        \".\",\n    )\n"
  },
  {
    "path": "edb/tools/pygments/graphql/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nfrom pygments.lexer import RegexLexer, include\nfrom pygments import token\n\n\n__all__ = ['GraphQLLexer']\n\n\nclass GraphQLLexer(RegexLexer):\n    name = 'GraphQL'\n    aliases = ['graphql']\n    filenames = ['*.gql', '*.graphql']\n\n    tokens = {\n        'root': [\n            include('comments'),\n            (r'@\\w+', token.Name.Decorator),\n            (r'\\$\\w+', token.Name.Variable),\n            include('keywords'),\n            include('numbers'),\n            include('strings'),\n            (r'\\b(true|false|null)\\b', token.Keyword.Constant),\n            (r'\\s+', token.Text),\n            (r'\\w+', token.Text),\n            (r'.', token.Text),\n        ],\n        'comments': [\n            (r'#.*?\\n', token.Comment.Singleline),\n        ],\n        'keywords': [\n            (r'''(?x)\n                \\b(\n                    query | mutation\n                )\\b\n            ''', token.Keyword.Reserved),\n\n            (r'\\b(__schema|__type|__typename)\\b',\n             token.Name.Builtin.Pseudo),\n        ],\n        'strings': [\n            (r'''(?x)\n                \" [^\\n]*? (?<!\\\\)\"\n            ''', token.String.Double),\n        ],\n        'numbers': [\n            (r'''(?x)\n                (?<!\\w)\n                    (?: \\d+ (?:\\.\\d*)?\n                        |\n                        \\. \\d+\n                    ) (?:[eE](?:[+\\-])?[0-9]+)\n            ''', token.Number.Float),\n            (r'''(?x)\n                (?<!\\w)\n                    (?: \\d+\\.(?!\\.)\\d*\n                        |\n                        \\.\\d+)\n            ''', token.Number.Float),\n            (r'(?<!\\w)\\d+', token.Number.Integer),\n        ],\n    }\n"
  },
  {
    "path": "edb/tools/railroad_diagram.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom dataclasses import dataclass\nimport os\nimport parsing\nimport pathlib\nfrom typing import Sequence, Callable\n\nfrom edb.common import parsing as edb_parsing, debug\nfrom edb.edgeql.parser import grammar as qlgrammar\nfrom edb.tools.edb import edbcommands\n\n\nROOT_PATH = pathlib.Path(__file__).parent.resolve()\n\n\n@edbcommands.command()\ndef railroad_diagram():\n    # ensure parsing spec is not skinny, we need Spec._tokens\n    debug.flags.edgeql_parser = True\n\n    # load grammar definitions and build the parser\n    spec = edb_parsing.load_parser_spec(qlgrammar.start)\n    spec_json = edb_parsing.spec_to_json(spec)\n\n    # prepare destination\n    dst = str(ROOT_PATH / 'railroad_diagram' / 'grammar.bc')\n    os.makedirs(os.path.dirname(dst), exist_ok=True)\n\n    # serialize\n    import edb._edgeql_parser as rust_parser\n    rust_parser.save_spec(spec_json, str(dst))\n\n    productions = to_ebnf(spec)\n    write_ebnf_productions(productions, ROOT_PATH / 'railroad_diagram')\n\n\n# EBNF datatypes\n\nclass ebnf:\n    class Item:\n        pass\n\n    @dataclass(eq=False, match_args=False)\n    class Literal(Item):\n        token: str\n\n    @dataclass(eq=False, match_args=False)\n    class Reference(Item):\n        name: str\n\n    @dataclass(eq=False, match_args=False)\n    class Single(Item):\n        inner: 'ebnf.Item'\n\n    @dataclass(eq=False, match_args=False)\n    class Optional(Single):\n        pass\n\n    @dataclass(eq=False, match_args=False)\n    class Multiple(Item):\n        inner: Sequence['ebnf.Item']\n\n    @dataclass(eq=False, match_args=False)\n    class Sequence(Multiple):\n        pass\n\n    @dataclass(eq=False, match_args=False)\n    class Choice(Multiple):\n        pass\n\n    @dataclass(eq=False, match_args=False)\n    class Production:\n        name: str\n        item: 'ebnf.Item'\n\n\n# Conversion functions\n\ndef ebnf_single_or_sequence(items: Sequence[ebnf.Item]) -> ebnf.Item:\n    if len(items) == 1:\n        return items[0]\n    else:\n        return ebnf.Sequence(items)\n\n\ndef ebnf_single_or_choice(items: Sequence[ebnf.Item]) -> ebnf.Item:\n    if len(items) == 1:\n        return items[0]\n    else:\n        return ebnf.Choice(items)\n\n\ndef expand_iso_ebnf(item: ebnf.Item) -> str:\n    match item:\n        case ebnf.Literal():\n            return '\"' + item.token + '\"'\n        case ebnf.Reference():\n            return item.name\n        case ebnf.Optional():\n            return '[' + expand_iso_ebnf(item.inner) + ']'\n        case ebnf.Sequence():\n            return (\n                '('\n                + ', '.join(expand_iso_ebnf(inner) for inner in item.inner)\n                + ')'\n            )\n        case ebnf.Choice():\n            return (\n                '('\n                + ' | '.join(\n                    expand_iso_ebnf(inner_item) for inner_item in item.inner\n                )\n                + ')'\n            )\n        case _:\n            raise NotImplementedError\n\n\ndef to_iso_ebnf(productions: list[ebnf.Production]) -> list[str]:\n    return [\n        production.name + ' = ' + expand_iso_ebnf(production.item) + ';'\n        for production in productions\n    ]\n\n\ndef expand_w3c_ebnf(item: ebnf.Item) -> str:\n    match item:\n        case ebnf.Literal():\n            return '\"' + item.token + '\"'\n        case ebnf.Reference():\n            return item.name\n        case ebnf.Optional():\n            return expand_w3c_ebnf(item.inner) + '?'\n        case ebnf.Sequence():\n            return (\n                '('\n                + ' '.join(\n                    expand_w3c_ebnf(inner_item) for inner_item in item.inner\n                )\n                + ')'\n            )\n        case ebnf.Choice():\n            return (\n                '('\n                + ' | '.join(\n                    expand_w3c_ebnf(inner_item) for inner_item in item.inner\n                )\n                + ')'\n            )\n        case _:\n            raise NotImplementedError\n\n\ndef to_w3c_ebnf(productions: list[ebnf.Production]) -> list[str]:\n    return [\n        production.name + ' ::= ' + expand_w3c_ebnf(production.item)\n        for production in productions\n    ]\n\n\ndef simplify_productions(\n    productions: list[ebnf.Production],\n) -> list[ebnf.Production]:\n\n    productions_by_name: dict[str, ebnf.Production] = {\n        production.name: production for production in productions\n    }\n\n    def referenced_item(reference: ebnf.Reference) -> ebnf.Item:\n        return productions_by_name[reference.name].item\n\n    def is_reference_to(item: ebnf.Item, name: str) -> bool:\n        return isinstance(item, ebnf.Reference) and item.name == name\n\n    def is_literal_reference(item: ebnf.Item) -> bool:\n        return isinstance(item, ebnf.Reference) and isinstance(\n            referenced_item(item), ebnf.Literal\n        )\n\n    def is_non_literal_reference(item: ebnf.Item) -> bool:\n        return isinstance(item, ebnf.Reference) and not isinstance(\n            referenced_item(item), ebnf.Literal\n        )\n\n    # remove unused productions\n\n    reference_counts: dict[str, int] = {\n        production.name: 0 for production in productions\n    }\n\n    def update_reference_count(prod_name: str, item: ebnf.Item):\n        match item:\n            case ebnf.Reference():\n                if prod_name != item.name:\n                    # don't count recursive references\n                    reference_counts[item.name] += 1\n            case ebnf.Single():\n                update_reference_count(prod_name, item.inner)\n            case ebnf.Multiple():\n                for inner_item in item.inner:\n                    update_reference_count(prod_name, inner_item)\n\n    for production in productions:\n        update_reference_count(production.name, production.item)\n\n    productions = [\n        production\n        for production in productions\n        if reference_counts[production.name] > 0\n    ]\n\n    # utilities to help inlining\n\n    def separate_references_to_inline(\n        productions: Sequence[ebnf.Production],\n        is_inlined: Callable[[ebnf.Item], bool],\n    ):\n        inlined_references: dict[str, ebnf.Item] = {\n            production.name: production.item\n            for production in productions\n            if is_inlined(production.item)\n        }\n        productions = [\n            production\n            for production in productions\n            if not is_inlined(production.item)\n        ]\n\n        return productions, inlined_references\n\n    def inline_references(\n        item: ebnf.Item, references: dict[str, ebnf.Item]\n    ) -> tuple[ebnf.Item, bool]:\n\n        if isinstance(item, ebnf.Reference):\n            if item.name in references:\n                return references[item.name], True\n\n        return item, False\n\n    # inline direct references\n\n    productions, direct_references = separate_references_to_inline(\n        productions, lambda item: isinstance(item, ebnf.Reference)\n    )\n\n    def inline_direct_references(item: ebnf.Item) -> tuple[ebnf.Item, bool]:\n        return inline_references(item, direct_references)\n\n    # inline direct optionals\n\n    productions, direct_optionals = separate_references_to_inline(\n        productions, lambda item: isinstance(item, ebnf.Optional)\n    )\n\n    def inline_direct_optionals(item: ebnf.Item) -> tuple[ebnf.Item, bool]:\n        return inline_references(item, direct_optionals)\n\n    # inline choice of 3 or fewer references to literals\n\n    def is_short_choice_of_literals(item: ebnf.Item) -> bool:\n        return (\n            isinstance(item, ebnf.Choice)\n            and len(item.inner) <= 3\n            and all(\n                is_literal_reference(inner_item) for inner_item in item.inner\n            )\n        )\n\n    productions, short_choice_of_literals = separate_references_to_inline(\n        productions, is_short_choice_of_literals\n    )\n\n    def inline_short_choice_of_literals(\n        item: ebnf.Item,\n    ) -> tuple[ebnf.Item, bool]:\n        return inline_references(item, short_choice_of_literals)\n\n    # inline sequence of 2 with at least 1 literal\n\n    def is_short_sequence_with_literal(item: ebnf.Item) -> bool:\n        return (\n            isinstance(item, ebnf.Sequence)\n            and len(item.inner) <= 2\n            and sum(\n                is_literal_reference(inner_item) for inner_item in item.inner\n            )\n            > 0\n        )\n\n    productions, short_sequence_with_literal = separate_references_to_inline(\n        productions, is_short_sequence_with_literal\n    )\n\n    def inline_short_sequence_with_literal(\n        item: ebnf.Item,\n    ) -> tuple[ebnf.Item, bool]:\n        return inline_references(item, short_sequence_with_literal)\n\n    # inline paren sequences\n\n    def is_paren_reference(item: ebnf.Item) -> bool:\n        return (\n            isinstance(item, ebnf.Sequence)\n            and len(item.inner) <= 4\n            and (\n                (\n                    is_reference_to(item.inner[0], 'LBRACE')\n                    and is_reference_to(item.inner[-1], 'RBRACE')\n                )\n                or (\n                    is_reference_to(item.inner[0], 'LBRACKET')\n                    and is_reference_to(item.inner[-1], 'RBRACKET')\n                )\n                or (\n                    is_reference_to(item.inner[0], 'LPAREN')\n                    and is_reference_to(item.inner[-1], 'RPAREN')\n                )\n            )\n        )\n\n    productions, paren_references = separate_references_to_inline(\n        productions, is_paren_reference\n    )\n\n    def inline_paren_references(item: ebnf.Item) -> tuple[ebnf.Item, bool]:\n        return inline_references(item, paren_references)\n\n    # substitute multi-items with a single item\n\n    def substitute_multi_item_single(item: ebnf.Item) -> tuple[ebnf.Item, bool]:\n\n        if isinstance(item, ebnf.Multiple) and len(item.inner) == 1:\n            return item.inner[0], True\n\n        return item, False\n\n    # substitute choices of optionals with optional of choice\n\n    def substitute_choice_of_options(item: ebnf.Item) -> tuple[ebnf.Item, bool]:\n\n        if isinstance(item, ebnf.Choice):\n            inner_options = [\n                inner_item\n                for inner_item in item.inner\n                if isinstance(inner_item, ebnf.Optional)\n            ]\n\n            if len(inner_options) == len(item.inner):\n                return (\n                    ebnf.Optional(\n                        ebnf.Choice(\n                            [\n                                inner_option.inner\n                                for inner_option in inner_options\n                            ]\n                        )\n                    ),\n                    True,\n                )\n\n        return item, False\n\n    # apply replacements until no changes are made\n\n    def replace_repeatedly_helper(\n        item: ebnf.Item,\n        funcs: list[Callable[[ebnf.Item], tuple[ebnf.Item, bool]]],\n    ) -> ebnf.Item:\n\n        changed = True\n        while changed:\n            changed = False\n            for func in funcs:\n                item, curr_changed = func(item)\n                changed = changed or curr_changed\n        return item\n\n    def replace_repeatedly(item: ebnf.Item) -> ebnf.Item:\n        return replace_repeatedly_helper(\n            item,\n            [\n                inline_direct_references,\n                inline_direct_optionals,\n                inline_short_choice_of_literals,\n                inline_short_sequence_with_literal,\n                inline_paren_references,\n                substitute_multi_item_single,\n                substitute_choice_of_options,\n            ],\n        )\n\n    def replace_recursively(item: ebnf.Item) -> ebnf.Item:\n        # replace parent before and after children\n        # before children handles recursive inlining\n        # after children handles recursive substitution\n        item = replace_repeatedly(item)\n\n        if isinstance(item, ebnf.Single):\n            item.inner = replace_recursively(item.inner)\n            item = replace_repeatedly(item)\n        elif isinstance(item, ebnf.Multiple):\n            item.inner = [\n                replace_recursively(inner_item) for inner_item in item.inner\n            ]\n            item = replace_repeatedly(item)\n\n        return item\n\n    return [\n        ebnf.Production(production.name, replace_recursively(production.item))\n        for production in productions\n    ]\n\n\ndef to_ebnf(spec: parsing.Spec) -> list[ebnf.Production]:\n    ebnf_productions: list[ebnf.Production] = []\n\n    # add token productions\n    for token in spec._tokens:\n        if token in ['<e>', '<$>']:\n            continue\n        ebnf_productions.append(ebnf.Production(token, ebnf.Literal(token)))\n\n    # add nonterm productions\n    nonterm_productions: dict[str, list[list[ebnf.Reference]]] = {}\n\n    has_production = {\n        **{token: True for token in spec._tokens},\n        **{nonterm: False for nonterm in spec._nonterms},\n    }\n\n    for production in spec._productions:\n        prod_name = str(production.lhs)\n        if prod_name == '<S>':\n            continue\n\n        item_list: list[ebnf.Reference] = [\n            ebnf.Reference(item.name) for item in production.rhs\n        ]\n\n        has_production[prod_name] = True\n\n        if prod_name not in nonterm_productions:\n            nonterm_productions[prod_name] = []\n        nonterm_productions[prod_name].append(item_list)\n\n    for prod_name, item_lists in nonterm_productions.items():\n        # if a production only refers to nonterminals with no productions\n        # remove it, since it does nothing\n\n        nonterm_productions[prod_name] = [\n            item_list\n            for item_list in item_lists\n            if\n            # refers to token or nonterm with productions\n            any(has_production[item.name] for item in item_list) or\n            # keep empty reductions, handled later as an optional\n            item_list == []\n        ]\n\n    for prod_name, item_lists in nonterm_productions.items():\n        if [] in item_lists:\n            # optional nonterm\n            item_lists = [\n                item_list for item_list in item_lists if item_list != []\n            ]\n            ebnf_productions.append(\n                ebnf.Production(\n                    prod_name,\n                    ebnf_single_or_choice(\n                        [\n                            ebnf.Optional(ebnf_single_or_sequence(item_list))\n                            for item_list in item_lists\n                        ]\n                    ),\n                )\n            )\n        else:\n            ebnf_productions.append(\n                ebnf.Production(\n                    prod_name,\n                    ebnf_single_or_choice(\n                        [\n                            ebnf_single_or_sequence(item_list)\n                            for item_list in item_lists\n                        ]\n                    ),\n                )\n            )\n\n    # repeatedly simplify until no simplifications are made\n    while True:\n        count = len(ebnf_productions)\n        ebnf_productions = simplify_productions(ebnf_productions)\n        if count == len(ebnf_productions):\n            break\n\n    return ebnf_productions\n\n\ndef write_ebnf_productions(\n    ebnf_productions: list[ebnf.Production],\n    path: pathlib.Path\n) -> None:\n    with open(path / 'grammar.iso.ebnf', 'w') as file:\n        file.write('\\n'.join(to_iso_ebnf(ebnf_productions)))\n    with open(path / 'grammar.w3c.ebnf', 'w') as file:\n        file.write('\\n'.join(to_w3c_ebnf(ebnf_productions)))\n"
  },
  {
    "path": "edb/tools/redo_metaschema.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom edb.tools.edb import edbcommands\n\n\n@edbcommands.command(\"redo-metaschema-sql\")\ndef run():\n    \"\"\"\n    Generates DDL to recreate metaschema for sql introspection.\n    Can be used to apply changes to metaschema to an existing database.\n\n    edb redo-metaschema-sql | ./build/postgres/install/bin/psql \\\n        \"postgresql://postgres@/E_main?host=$(pwd)/tmp/devdatadir&port=5432\" \\\n        -v ON_ERROR_STOP=ON\n    \"\"\"\n\n    from edb.common import devmode\n    devmode.enable_dev_mode()\n\n    from edb.pgsql import dbops, metaschema\n    from edb import buildmeta\n\n    version = buildmeta.get_pg_version()\n    commands = metaschema._generate_sql_information_schema(version)\n\n    for command in commands:\n        block = dbops.PLTopBlock()\n\n        if isinstance(command, dbops.CreateFunction):\n            command.or_replace = True\n        if isinstance(command, dbops.CreateView):\n            command.or_replace = True\n\n        command.generate(block)\n\n        print(block.to_string())\n"
  },
  {
    "path": "edb/tools/rm_data_dir.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport shutil\n\nfrom edb.common import devmode\nfrom edb.tools.edb import edbcommands\n\n\n@edbcommands.command(\"rm-data-dir\")\ndef rm_data_dir():\n    \"\"\"Remove the local development data directory if present\"\"\"\n    data_dir = devmode.get_dev_mode_data_dir()\n    if data_dir.exists():\n        shutil.rmtree(data_dir)\n        print(\"Removed the following local dev data directory.\")\n        print(data_dir)\n    else:\n        print(\"The local dev data directory does not exist.\")\n"
  },
  {
    "path": "edb/tools/test/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport contextlib\nimport os\nimport pathlib\nimport shutil\nimport sys\nimport tempfile\nimport unittest\nimport typing\n\nimport click\nimport psutil\n\nimport edb\nfrom edb.common import devmode\nfrom edb.testbase.server import get_test_cases\nfrom edb.tools.edb import edbcommands\n\nfrom .decorators import async_timeout\nfrom .decorators import not_implemented\nfrom .decorators import _xfail\nfrom .decorators import xfail\nfrom .decorators import xerror\nfrom .decorators import skip\n\nfrom . import loader\nfrom . import mproc_fixes\nfrom . import runner\nfrom . import styles\nfrom . import results\n\n\n__all__ = ('async_timeout', 'not_implemented', 'xerror', 'xfail', '_xfail',\n           'skip')\n\n\n@edbcommands.command()\n@click.argument('files', nargs=-1, metavar='[file or directory]...')\n@click.option('-v', '--verbose', is_flag=True,\n              help='increase verbosity')\n@click.option('-q', '--quiet', is_flag=True,\n              help='decrease verbosity')\n@click.option('--debug', is_flag=True,\n              help='output internal debug logs')\n@click.option('--output-format',\n              type=click.Choice(runner.OutputFormat),  # type: ignore\n              help='test progress output style',\n              default=runner.OutputFormat.auto)\n@click.option('--warnings/--no-warnings',\n              help='enable or disable warnings (enabled by default)',\n              default=True)\n@click.option('-j', '--jobs', type=int,\n              default=0,\n              help='number of parallel processes to use, default is 0, which '\n                   'means choose automatically based on the number of '\n                   'available CPU cores')\n@click.option('-s', '--shard', type=str,\n              default='1/1',\n              help='run tests in shards (current/total)')\n@click.option('-k', '--include', type=str, multiple=True, metavar='REGEXP',\n              help='only run tests which match the given regular expression')\n@click.option('-e', '--exclude', type=str, multiple=True, metavar='REGEXP',\n              help='do not run tests which match the given regular expression')\n@click.option('-x', '--failfast', is_flag=True,\n              help='stop tests after a first failure/error')\n@click.option('--shuffle', is_flag=True,\n              help='shuffle the order in which tests are run')\n@click.option('--repeat', type=int, default=1,\n              help='repeat tests N times or until first unsuccessful run')\n@click.option('--cov', type=str, multiple=True,\n              help='package name to measure code coverage for, '\n                   'can be specified multiple times '\n                   '(e.g --cov edb.common --cov edb.server)')\n@click.option('--running-times-log', 'running_times_log_file',\n              type=click.File('a+'), metavar='FILEPATH',\n              help='maintain a running time log file at FILEPATH')\n@click.option('--result-log', type=str, metavar='FILEPATH',\n              help='write the test result to a log file. '\n                'If the path contains %TIMESTAMP%, it will be replaced by '\n                'ISO8601 date and time. '\n                'Empty string means not to write the log at all.',\n              default='build/test-results/%TIMESTAMP%.json')\n@click.option('--include-unsuccessful', is_flag=True,\n              help='include the tests that were not successful in the last run')\n@click.option('--list', 'list_tests', is_flag=True,\n              help='list all the tests and exit')\n@click.option('--backend-dsn', type=str,\n              help='use the specified backend cluster instead of starting a '\n                   'temporary local one.')\n@click.option('--use-db-cache', is_flag=True,\n              help='attempt to use a cache of the test databases (unsound!)')\n@click.option('--data-dir', type=str,\n              help='use a specified data dir')\n@click.option('--use-data-dir-dbs', is_flag=True,\n              help='attempt to use setup databases in the data-dir')\ndef test(\n    *,\n    files: typing.Sequence[str],\n    jobs: int,\n    shard: str,\n    include: typing.Sequence[str],\n    exclude: typing.Sequence[str],\n    verbose: bool,\n    quiet: bool,\n    debug: bool,\n    output_format: runner.OutputFormat,\n    warnings: bool,\n    failfast: bool,\n    shuffle: bool,\n    cov: typing.Sequence[str],\n    repeat: int,\n    running_times_log_file: typing.Optional[typing.TextIO],\n    list_tests: bool,\n    backend_dsn: typing.Optional[str],\n    use_db_cache: bool,\n    data_dir: typing.Optional[str],\n    use_data_dir_dbs: bool,\n    result_log: str,\n    include_unsuccessful: bool,\n):\n    \"\"\"Run Gel test suite.\n\n    Discovers and runs tests in the specified files or directories.\n    If no files or directories are specified, current directory is assumed.\n    \"\"\"\n    if quiet:\n        if verbose:\n            click.secho(\n                'Warning: both --quiet and --verbose are '\n                'specified, assuming --quiet.', fg='yellow')\n        verbosity = 0\n    elif verbose:\n        verbosity = 2\n    else:\n        verbosity = 1\n\n    if jobs == 0:\n        jobs = psutil.cpu_count(logical=False)\n\n    mproc_fixes.patch_multiprocessing(debug=debug)\n\n    if verbosity > 1 and output_format is runner.OutputFormat.stacked:\n        click.secho(\n            'Error: cannot use stacked output format in verbose mode.',\n            fg='red')\n        sys.exit(1)\n\n    if repeat < 1:\n        click.secho(\n            'Error: --repeat must be a positive non-zero number.', fg='red')\n        sys.exit(1)\n\n    if not files:\n        cwd = os.path.abspath(os.getcwd())\n        if os.path.exists(os.path.join(cwd, 'tests')):\n            files = ('tests',)\n        else:\n            click.secho(\n                'Error: no test path specified and no \"tests\" directory found',\n                fg='red')\n            sys.exit(1)\n\n    for file in files:\n        if not os.path.exists(file):\n            click.secho(\n                f'Error: test path {file!r} does not exist', fg='red')\n            sys.exit(1)\n\n    try:\n        selected_shard, total_shards = map(int, shard.split('/'))\n    except Exception:\n        click.secho(f'Error: --shard {shard} must match format e.g. 2/5')\n        sys.exit(1)\n\n    if selected_shard < 1 or selected_shard > total_shards:\n        click.secho(f'Error: --shard {shard} is out of bound')\n        sys.exit(1)\n\n    run = lambda: _run(\n        include=include,\n        exclude=exclude,\n        verbosity=verbosity,\n        files=files,\n        jobs=jobs,\n        output_format=output_format,\n        warnings=warnings,\n        failfast=failfast,\n        shuffle=shuffle,\n        repeat=repeat,\n        selected_shard=selected_shard,\n        total_shards=total_shards,\n        running_times_log_file=running_times_log_file,\n        list_tests=list_tests,\n        backend_dsn=backend_dsn,\n        try_cached_db=use_db_cache,\n        data_dir=data_dir,\n        use_data_dir_dbs=use_data_dir_dbs,\n        result_log=result_log,\n        include_unsuccessful=include_unsuccessful,\n    )\n\n    if cov:\n        for pkg in cov:\n            if '\\\\' in pkg or '/' in pkg or pkg.endswith('.py'):\n                click.secho(\n                    f'Error: --cov argument {pkg!r} looks like a path, '\n                    f'expected a Python package name', fg='red')\n                sys.exit(1)\n\n        with _coverage_wrapper(cov):\n            result = run()\n    else:\n        result = run()\n\n    sys.exit(result)\n\n\n@contextlib.contextmanager\ndef _coverage_wrapper(paths):\n    try:\n        import coverage  # NoQA\n    except ImportError:\n        click.secho(\n            'Error: \"coverage\" package is missing, cannot run tests '\n            'with --cov')\n        sys.exit(1)\n\n    for path in edb.__path__:\n        cov_rc = pathlib.Path(path).parent / 'pyproject.toml'\n        if cov_rc.exists():\n            break\n    else:\n        raise RuntimeError('cannot locate the pyproject.toml file')\n\n    with tempfile.TemporaryDirectory() as td:\n        cov_config = devmode.CoverageConfig(\n            paths=paths,\n            config=str(cov_rc),\n            datadir=td)\n        cov_config.save_to_environ()\n\n        main_cov = cov_config.new_coverage_object()\n        main_cov.start()\n\n        try:\n            yield\n        finally:\n            main_cov.stop()\n            main_cov.save()\n\n            covfile = str(pathlib.Path(td) / '.coverage')\n            data = coverage.CoverageData(covfile)\n\n            with os.scandir(td) as it:\n                for entry in it:\n                    new_data = coverage.CoverageData(entry.path)\n                    new_data.read()\n                    data.update(new_data)\n\n            data.write()\n            report_cov = cov_config.new_custom_coverage_object(\n                config_file=str(cov_rc),\n                data_file=covfile,\n            )\n            report_cov.load()\n            click.secho('Coverage:')\n            report_cov.report()\n            # store the coverage file in cwd, so it can be used to produce\n            # additional reports with coverage cli\n            shutil.copy(covfile, '.')\n\n\ndef _run(\n    *,\n    include: typing.Sequence[str],\n    exclude: typing.Sequence[str],\n    verbosity: int,\n    files: typing.Sequence[str],\n    jobs: int,\n    output_format: str,\n    warnings: bool,\n    failfast: bool,\n    shuffle: bool,\n    repeat: int,\n    selected_shard: int,\n    total_shards: int,\n    running_times_log_file: typing.Optional[typing.TextIO],\n    list_tests: bool,\n    backend_dsn: typing.Optional[str],\n    try_cached_db: bool,\n    data_dir: typing.Optional[str],\n    use_data_dir_dbs: bool,\n    result_log: str,\n    include_unsuccessful: bool,\n):\n    suite = unittest.TestSuite()\n\n    total = 0\n    total_unfiltered = 0\n\n    _update_progress: typing.Callable[[int, int], None] | None\n    if verbosity > 0:\n\n        def _update_progress(n: int, unfiltered_n: int):\n            nonlocal total, total_unfiltered\n            total += n\n            total_unfiltered += unfiltered_n\n            click.echo(styles.status(\n                f'Collected {total}/{total_unfiltered} tests.\\r'),\n                nl=False, err=list_tests)\n    else:\n        _update_progress = None\n\n    if include_unsuccessful and result_log:\n        unsuccessful = results.read_unsuccessful(result_log)\n        include = list(include) + unsuccessful + ['a_non_existing_test']\n\n    test_loader = loader.TestLoader(\n        verbosity=verbosity,\n        exclude=exclude,\n        include=include,\n        progress_cb=_update_progress,\n    )\n\n    for file in files:\n        if not os.path.exists(file) and verbosity > 0:\n            click.echo(styles.warning(\n                f'Warning: {file}: no such file or directory.'))\n\n        if os.path.isdir(file):\n            tests = test_loader.discover(file)\n        else:\n            tests = test_loader.discover(\n                os.path.dirname(file),\n                pattern=os.path.basename(file))\n\n        suite.addTest(tests)\n\n    if list_tests:\n        click.echo(err=True)\n        cases = get_test_cases([suite])\n        for tests in cases.values():\n            for test in tests:\n                click.echo(str(test))\n        return 0\n\n    jobs = max(min(total, jobs), 1)\n\n    if verbosity > 0:\n        click.echo()\n        if jobs > 1:\n            click.echo(styles.status(\n                f'Using up to {jobs} processes to run tests.'))\n\n    for rnum in range(repeat):\n        if repeat > 1:\n            click.echo(styles.status(\n                f'Repeat #{rnum + 1} out of {repeat}.'))\n\n        test_runner = runner.ParallelTextTestRunner(\n            verbosity=verbosity, output_format=output_format,\n            warnings=warnings, num_workers=jobs,\n            failfast=failfast, shuffle=shuffle, backend_dsn=backend_dsn,\n            try_cached_db=try_cached_db,\n            data_dir=data_dir,\n            use_data_dir_dbs=use_data_dir_dbs,\n        )\n\n        result = test_runner.run(\n            suite, selected_shard, total_shards, running_times_log_file,\n        )\n\n        if verbosity > 0:\n            results.render_result(test_runner.stream, result)\n\n        if not result.was_successful:\n            break\n\n    if result_log:\n        results.write_result(result_log, result)\n\n    return 0 if result.was_successful else 1\n"
  },
  {
    "path": "edb/tools/test/cpython_state.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport ctypes\n\n\nclass _Py_HashSecret_t(ctypes.Union):\n    _fields_ = [\n        ('uc', ctypes.c_ubyte * 24),\n    ]\n\n\ndef get_py_hash_secret() -> bytes:\n    hashsecret = _Py_HashSecret_t.in_dll(ctypes.pythonapi, '_Py_HashSecret')\n    return bytes(hashsecret.uc)\n"
  },
  {
    "path": "edb/tools/test/decorators.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport asyncio\nimport functools\nimport unittest\nimport logging\n\n\nlogger = logging.getLogger(\"edb.server\")\nskip = unittest.skip\n\n\ndef _xfail(reason, *, unless=False, allow_failure, allow_error):\n    def decorator(test_item):\n        if unless:\n            return test_item\n        else:\n            test_item.__et_xfail_reason__ = reason\n            test_item.__et_xfail_allow_failure__ = allow_failure\n            test_item.__et_xfail_allow_error__ = allow_error\n            return unittest.expectedFailure(test_item)\n\n    return decorator\n\n\ndef xfail(reason, *, unless=False):\n    return _xfail(reason, unless=unless, allow_failure=True, allow_error=False)\n\n\ndef xerror(reason, *, unless=False):\n    return _xfail(reason, unless=unless, allow_failure=False, allow_error=True)\n\n\ndef not_implemented(reason):\n    def decorator(test_item):\n        test_item.__et_xfail_reason__ = reason\n        test_item.__et_xfail_not_implemented__ = True\n        test_item.__et_xfail_allow_failure__ = True\n        test_item.__et_xfail_allow_error__ = True\n        return unittest.expectedFailure(test_item)\n\n    return decorator\n\n\ndef async_timeout(timeout: int):\n    def decorator(test_func):\n        @functools.wraps(test_func)\n        async def wrapper(*args, **kwargs):\n            try:\n                await asyncio.wait_for(test_func(*args, **kwargs), timeout)\n            except asyncio.TimeoutError:\n                logger.error(\n                    f\"Test {test_func} failed due to timeout after {timeout}\"\n                    \"seconds\")\n                raise AssertionError(\n                    f\"Test failed due to timeout after {timeout} seconds\")\n            except asyncio.CancelledError as e:\n                logger.error(\n                    f\"Test {test_func} failed due to timeout after {timeout}\"\n                    \"seconds\", e)\n                raise AssertionError(\n                    f\"Test failed due to timeout after {timeout} seconds\", e)\n        return wrapper\n    return decorator\n"
  },
  {
    "path": "edb/tools/test/loader.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport re\nimport unittest\nfrom typing import Callable, Optional, Sequence\n\n\nclass TestLoader(unittest.TestLoader):\n    include: Optional[Sequence[re.Pattern]]\n    exclude: Optional[Sequence[re.Pattern]]\n\n    def __init__(\n        self,\n        *,\n        verbosity: int = 1,\n        exclude: Sequence[str] = (),\n        include: Sequence[str] = (),\n        progress_cb: Optional[Callable[[int, int], None]] = None,\n    ):\n        super().__init__()\n        self.verbosity = verbosity\n\n        if include:\n            self.include = [re.compile(r) for r in include]\n        else:\n            self.include = None\n\n        if exclude:\n            self.exclude = [re.compile(r) for r in exclude]\n        else:\n            self.exclude = None\n\n        self.progress_cb = progress_cb\n\n    def getTestCaseNames(self, caseclass):\n        names = super().getTestCaseNames(caseclass)\n        unfiltered_len = len(names)\n        cname = caseclass.__name__\n\n        if self.include or self.exclude:\n            if self.include:\n                names = filter(\n                    lambda n: (\n                        any(r.search(n) for r in self.include)\n                        or any(r.search(f'{cname}.{n}') for r in self.include)\n                    ),\n                    names,\n                )\n\n            if self.exclude:\n                names = filter(\n                    lambda n: (\n                        not any(r.search(n) for r in self.exclude)\n                        and not any(\n                            r.search(f'{cname}.{n}') for r in self.exclude\n                        )\n                    ),\n                    names,\n                )\n\n            names = list(names)\n\n        if self.progress_cb:\n            self.progress_cb(len(names), unfiltered_len)\n\n        return names\n"
  },
  {
    "path": "edb/tools/test/mproc_fixes.py",
    "content": "# mypy: ignore-errors\n\n#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport logging\nimport multiprocessing.pool\nimport multiprocessing.process\nimport multiprocessing.util\n\n\n_orig_pool_worker_handler = None\n_orig_pool_join_exited_workers = None\n\nlogger = logging.getLogger(__name__)\n\n\nclass WorkerScope:\n\n    def __init__(self, initializer, destructor):\n        self.initializer = initializer\n        self.destructor = destructor\n\n    def __call__(self, *args, **kwargs):\n        # Make multiprocessing.Pool happy\n        return self.initializer(*args, **kwargs)\n\n\ndef multiprocessing_pool_worker(\n    inqueue, outqueue, initializer=None, *args, **kwargs\n):\n    destructor = None\n    if isinstance(initializer, WorkerScope):\n        destructor = initializer.destructor\n\n    # This function is executed in the context of a spawned\n    # worker process, so the pool.worker() function is the\n    # original unpatched version.\n    try:\n        multiprocessing.pool.worker(\n            inqueue, outqueue, initializer, *args, **kwargs)\n    except KeyboardInterrupt:\n        # Try to exit with less noise when ctrl+c is pressed\n        return\n\n    if destructor is not None:\n        destructor()\n\n\ndef multiprocessing_worker_handler(*args):\n    _orig_pool_worker_handler(*args)\n\n    if len(args) == 1:\n        # In some pythons this is a static method with\n        # a single argument...\n        workers = args[0]._pool\n    else:\n        # ... and in others it's a staticmethod or a classmethod taking\n        # 12-14 positional arguments.\n        for arg in args:\n            if (isinstance(arg, list) and arg\n                    and isinstance(\n                        arg[0],\n                        multiprocessing.process.BaseProcess)):\n                workers = arg\n                break\n        else:\n            logger.error(\n                'unable to patch multiprocessing.Pool._handle_workers')\n            return\n\n    for worker_process in workers:\n        # Give workers ample time to shutdown, and\n        # if they don't, the pool will terminate them.\n        worker_process.join(timeout=10)\n\n\ndef join_exited_workers(pool):\n    # Our use case shouldn't have workers exiting really, so we skip\n    # doing the joins so that we can detect crashes ourselves in the\n    # test runner.x\n    pass\n\n\ndef patch_multiprocessing(debug: bool):\n    global _orig_pool_worker\n    global _orig_pool_worker_handler\n    global _orig_pool_join_exited_workers\n\n    if debug:\n        multiprocessing.util.log_to_stderr(logging.DEBUG)\n\n    # A \"fork\" without \"exec\" is broken on macOS since 10.14:\n    # https://www.wefearchange.org/2018/11/forkmacos.rst.html\n    # Since there is no apparent benefit of using fork for\n    # the test workers, use the \"spawn\" method on all platforms.\n    multiprocessing.set_start_method('spawn')\n\n    # Add the ability to do clean shutdown of the worker.\n    multiprocessing.pool.worker = multiprocessing_pool_worker\n\n    # Allow workers some time to shut down gracefully.\n    _orig_pool_worker_handler = multiprocessing.pool.Pool._handle_workers\n    multiprocessing.pool.Pool._handle_workers = multiprocessing_worker_handler\n\n    _orig_pool_join_exited_workers = (\n        multiprocessing.pool.Pool._join_exited_workers)\n    multiprocessing.pool.Pool._join_exited_workers = join_exited_workers\n"
  },
  {
    "path": "edb/tools/test/results.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# Warning: this file is ran in GHA tests/test-conclusion, with (almost) no\n# dependencies installed.\n\nfrom __future__ import annotations\n\nimport binascii\nimport dataclasses\nimport datetime\nimport functools\nimport json\nimport pathlib\nimport sys\nimport traceback\nimport typing\nimport unittest\nimport glob\nimport shutil\nfrom unittest.result import STDERR_LINE, STDOUT_LINE\n\nimport click\n\nif typing.TYPE_CHECKING:\n    from . import runner\n\n\n@dataclasses.dataclass()\nclass TestCase:\n    id: str\n    description: str\n\n    py_HashSecret: typing.Optional[str]\n    py_random_seed: typing.Optional[str]\n\n    error_message: typing.Optional[str]\n    server_traceback: typing.Optional[str]\n\n\ndef _collect_case_data(\n    result: runner.ParallelTextTestResult,\n    test: unittest.TestCase,\n    err: typing.Any,\n) -> TestCase:\n    from . import runner\n    import edgedb\n\n    py_HashSecret = None\n    py_random_seed = None\n    if annos := result.get_test_annotations(test):\n        if phs := annos.get('py-hash-secret'):\n            py_HashSecret = binascii.hexlify(phs).decode()\n        if prs := annos.get('py-random-seed'):\n            py_random_seed = binascii.hexlify(prs).decode()\n\n    error_message = None\n    server_traceback = None\n    if runner._is_exc_info(err):\n        if isinstance(err[1], edgedb.EdgeDBError):\n            server_traceback = err[1].get_server_context()\n        error_message = _exc_info_to_string(result, err, test)\n    elif isinstance(err, runner.SerializedServerError):\n        error_message, server_traceback = err.test_error, err.server_error\n    elif isinstance(err, str):\n        error_message = err\n\n    return TestCase(\n        id=test.id(),\n        description=result.getDescription(test),\n        py_HashSecret=py_HashSecret,\n        py_random_seed=py_random_seed,\n        error_message=error_message,\n        server_traceback=server_traceback,\n    )\n\n\ndef _exc_info_to_string(\n    result: runner.ParallelTextTestResult,\n    err: typing.Any,\n    test: unittest.TestCase,\n):\n    \"\"\"Converts a sys.exc_info()-style tuple of values into a string.\"\"\"\n    from edb.common import traceback as edb_traceback\n\n    # Copied from unittest.TestResult._exc_info_to_string\n\n    exctype, value, tb = err\n    tb = result._clean_tracebacks(exctype, value, tb, test)  # type: ignore\n    tb_e = traceback.TracebackException(\n        exctype, value, tb, capture_locals=result.tb_locals, compact=True\n    )\n    tb_e.stack = edb_traceback.StandardStackSummary(tb_e.stack)\n    msgLines = list(tb_e.format())\n\n    if result.buffer:\n        output = sys.stdout.getvalue()  # type: ignore\n        error = sys.stderr.getvalue()  # type: ignore\n        if output:\n            if not output.endswith('\\n'):\n                output += '\\n'\n            msgLines.append(STDOUT_LINE % output)\n        if error:\n            if not error.endswith('\\n'):\n                error += '\\n'\n            msgLines.append(STDERR_LINE % error)\n    return ''.join(msgLines)\n\n\n@dataclasses.dataclass()\nclass TestResult:\n    was_successful: bool\n\n    testsRun: int\n    boot_time_taken: float\n    tests_time_taken: float\n\n    # negative\n    failures: list[TestCase]\n    errors: list[TestCase]\n    unexpected_successes: list[TestCase]\n\n    # positive\n    warnings: list[TestCase]\n    skipped: list[TestCase]\n    not_implemented: list[TestCase]\n    expected_failures: list[TestCase]\n\n\ndef _combine_test_results(a: TestResult, b: TestResult) -> TestResult:\n    return TestResult(\n        was_successful=a.was_successful and b.was_successful,\n        testsRun=a.testsRun + b.testsRun,\n        # this assumes each result comes from a parallel run\n        boot_time_taken=max(a.boot_time_taken, b.boot_time_taken),\n        # this assumes each result comes from a parallel run\n        tests_time_taken=max(a.tests_time_taken, b.tests_time_taken),\n        # negative\n        failures=a.failures + b.failures,\n        errors=a.errors + b.errors,\n        unexpected_successes=a.unexpected_successes + b.unexpected_successes,\n        # positive\n        warnings=a.warnings + b.warnings,\n        skipped=a.skipped + b.skipped,\n        not_implemented=a.not_implemented + b.not_implemented,\n        expected_failures=a.expected_failures + b.expected_failures,\n    )\n\n\ndef collect_result_data(\n    r: runner.ParallelTextTestResult,\n    boot_time_taken: float,\n    tests_time_taken: float,\n):\n    return TestResult(\n        was_successful=r.wasSuccessful(),\n        testsRun=r.testsRun,\n        boot_time_taken=boot_time_taken,\n        tests_time_taken=tests_time_taken,\n        failures=[_collect_case_data(r, t, e) for t, e in r.failures],\n        errors=[_collect_case_data(r, t, e) for t, e in r.errors],\n        unexpected_successes=[\n            _collect_case_data(r, t, None) for t in r.unexpectedSuccesses\n        ],\n        warnings=[_collect_case_data(r, t, e) for t, e in r.warnings],\n        skipped=[_collect_case_data(r, t, e) for t, e in r.skipped],\n        not_implemented=[\n            _collect_case_data(r, t, e) for t, e in r.notImplemented\n        ],\n        expected_failures=[\n            _collect_case_data(r, t, e) for t, e in r.expectedFailures\n        ],\n    )\n\n\nclass EnhancedJSONEncoder(json.JSONEncoder):\n    def default(self, o):\n        if dataclasses.is_dataclass(o):\n            return dataclasses.asdict(o)\n        return super().default(o)\n\n\ndef _get_term_width():\n    return shutil.get_terminal_size()[0] or 70\n\n\ndef _echo(file: typing.IO, s: str = '', **kwargs):\n    click.secho(s, file=file, **kwargs)\n\n\ndef _fill(file: typing.IO, char, **kwargs):\n    _echo(file, char * _get_term_width(), **kwargs)\n\n\ndef _format_time(seconds: float):\n    hours = int(seconds // 3600)\n    seconds %= 3600\n    minutes = int(seconds // 60)\n    seconds %= 60\n\n    return f'{hours:02d}:{minutes:02d}:{seconds:04.1f}'\n\n\ndef _print_case_result(file: typing.IO, case: TestCase, kind: str, fg: str):\n    _fill(file, '=', fg=fg)\n    _echo(file, f'{kind}: {case.description}', fg=fg, bold=True)\n    _fill(file, '-', fg=fg)\n\n    if case.py_HashSecret or case.py_random_seed:\n        if case.py_HashSecret:\n            _echo(file, f'Py_HashSecret: {case.py_HashSecret}')\n        if case.py_random_seed:\n            _echo(file, f'random.seed(): {case.py_random_seed}')\n        _fill(file, '-', fg=fg)\n\n    if case.server_traceback:\n        _echo(file, 'Server Traceback:', fg='red', bold=True)\n        _echo(file, case.server_traceback)\n    if case.error_message:\n        if case.server_traceback:\n            _echo(file, 'Test Traceback:', fg='red', bold=True)\n        _echo(file, case.error_message)\n\n\ndef render_result(\n    file: typing.IO,\n    result: TestResult,\n) -> None:\n    _echo(file)\n\n    # cases\n    for case in sorted(result.warnings, key=lambda c: c.id):\n        _print_case_result(file, case, 'WARNING', 'yellow')\n    for case in sorted(result.errors, key=lambda c: c.id):\n        _print_case_result(file, case, 'ERROR', 'red')\n    for case in sorted(result.failures, key=lambda c: c.id):\n        _print_case_result(file, case, 'FAIL', 'red')\n    for case in sorted(result.unexpected_successes, key=lambda c: c.id):\n        _print_case_result(file, case, 'UNEXPECTED SUCCESS', 'red')\n\n    # outcome\n    if result.was_successful:\n        _echo(file, 'SUCCESS', fg='green', bold=True)\n    else:\n        _echo(file, 'FAILURE', fg='red', bold=True)\n\n    # counts\n    counts = [\n        ('tests ran', result.testsRun, None),\n        ('failures', len(result.failures), 'red'),\n        ('errors', len(result.errors), 'red'),\n        ('unexpected successes', len(result.unexpected_successes), 'red'),\n        ('not implemented', len(result.not_implemented), 'yellow'),\n        ('skipped', len(result.skipped), 'yellow'),\n        ('expected failures', len(result.expected_failures), None),\n    ]\n    for name, count, fg in counts:\n        if not count:\n            continue\n        _echo(file, f'  {name}: ', nl=False, fg=fg)\n        _echo(file, f'{count}', bold=True)\n\n    # running times\n    _echo(file)\n    _echo(file, f'Running times: ')\n    if result.boot_time_taken > 0.0:\n        _echo(file, '  bootstrap: ', nl=False)\n        _echo(file, _format_time(result.boot_time_taken), bold=True)\n\n    _echo(file, '  tests: ', nl=False)\n    _echo(file, _format_time(result.tests_time_taken), bold=True)\n\n    if result.boot_time_taken > 0.0:\n        _echo(file, '  total: ', nl=False)\n        _echo(\n            file,\n            _format_time(result.boot_time_taken + result.tests_time_taken),\n            bold=True,\n        )\n\n    _echo(file)\n\n\ndef _result_log_path(path_template: str) -> typing.Optional[pathlib.Path]:\n    now = str(datetime.datetime.now()).replace(' ', '_')\n    path = pathlib.Path(path_template.replace('%TIMESTAMP%', now))\n\n    dir = path.parent\n    try:\n        dir.mkdir(parents=True, exist_ok=True)\n        return path\n    except OSError:\n        # this might happen when the process is running in readonly mode\n        return None\n\n\ndef write_result(path_template: str, res: TestResult):\n    path = _result_log_path(path_template)\n    if not path:\n        return None\n    log_file = open(path, 'w')\n\n    json.dump(dataclasses.asdict(res), log_file, indent=4)\n\n\ndef read_unsuccessful(path_template: str) -> list[str]:\n    log_path = _result_log_path(path_template)\n    if not log_path:\n        return []\n    results = list(log_path.parent.iterdir())\n    if not results:\n        return []\n\n    results.sort()\n    last = results[-1]\n\n    try:\n        result_dict = json.load(open(last, 'r'))\n    except Exception:\n        return []\n    result: TestResult = _dataclass_from_dict(TestResult, result_dict)\n    return [\n        case.id.split('.')[-1]\n        for case in result.failures\n        + result.errors\n        + result.unexpected_successes\n    ]\n\n\ndef _dataclass_from_dict(cls: typing.Any, data: typing.Any):\n    if not cls:\n        return data\n\n    if hasattr(cls, '__origin__') and cls.__origin__ is list:\n        args = cls.__args__\n        return [_dataclass_from_dict(args[0], e) for e in data]\n\n    if not dataclasses.is_dataclass(cls):\n        return data\n    if not isinstance(data, dict):\n        raise ValueError(f'expected a dict of a dataclass, found {type(data)}')\n\n    field_types: typing.Mapping[str, type] = typing.get_type_hints(cls)\n    return cls(  # type: ignore\n        **{\n            k: _dataclass_from_dict(field_types.get(k), v)\n            for k, v in data.items()\n        }\n    )\n\n\n# if this file is invoked directly\nif __name__ == '__main__':\n    # read result JSON files, concat them into a single result and render\n    result_path_glob = sys.argv[1]\n\n    results: list[TestResult] = []\n    for new_file in glob.glob(result_path_glob):\n        with open(new_file) as f:\n            result_dict = json.load(f)\n            results.append(_dataclass_from_dict(TestResult, result_dict))\n\n    result = functools.reduce(\n        lambda acc, r: _combine_test_results(acc, r) if acc else r,\n        results,\n        typing.cast(typing.Optional[TestResult], None),\n    )\n    if not result:\n        raise ValueError(\n            f'no result files were found at glob {result_path_glob}'\n        )\n\n    render_result(sys.stdout, result)\n    sys.exit(0 if result.was_successful else 1)\n"
  },
  {
    "path": "edb/tools/test/runner.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2017-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import Any, Callable, Optional, TYPE_CHECKING\n\nimport asyncio\nimport collections\nimport collections.abc\nimport csv\nimport dataclasses\nimport enum\nimport faulthandler\nimport io\nimport itertools\nimport json\nimport multiprocessing\nimport multiprocessing.reduction\nimport multiprocessing.util\nimport os\nimport pathlib\nimport random\nimport re\nimport shutil\nimport subprocess\nimport sys\nimport tempfile\nimport threading\nimport time\nimport types\nimport unittest.case\nimport unittest.result\nimport unittest.runner\nimport unittest.signals\nimport warnings\n\nimport click\n\nimport edgedb\n\nfrom edb.common import devmode\nfrom edb.testbase import server as tb\n\nfrom . import cpython_state\nfrom . import mproc_fixes\nfrom . import styles\nfrom . import results\n\nif TYPE_CHECKING:\n    import edb.testbase.cluster as edb_cluster\n\nresult: Optional[unittest.result.TestResult] = None\ncoverage_run: Optional[Any] = None\npy_hash_secret: bytes = cpython_state.get_py_hash_secret()\npy_random_seed: bytes = random.SystemRandom().randbytes(8)\n\nfaulthandler.enable(file=sys.stderr, all_threads=True)\n\n\ndef teardown_suite() -> None:\n    # The TestSuite methods are mutating the *result* object,\n    # and the suite itself does not hold any state whatsoever,\n    # and, in our case specifically, it doesn't even hold\n    # references to tests being run, so we can think of\n    # its methods as static.\n    suite = StreamingTestSuite()\n    suite._tearDownPreviousClass(None, result)  # type: ignore[attr-defined]\n    suite._handleModuleTearDown(result)  # type: ignore[attr-defined]\n\n\ndef init_worker(\n    status_queue: multiprocessing.SimpleQueue,\n    param_queue: multiprocessing.SimpleQueue,\n    result_queue: multiprocessing.SimpleQueue,\n    additional_init: Optional[Callable]\n) -> None:\n    global result\n    global coverage_run\n    global py_hash_secret\n    global py_random_seed\n\n    faulthandler.enable(file=sys.stderr, all_threads=True)\n\n    if additional_init:\n        additional_init()\n\n    # Make sure the generator is re-seeded, as we have inherited\n    # the seed from the parent process.\n    py_random_seed = random.SystemRandom().randbytes(8)\n    random.seed(py_random_seed)\n\n    result = ChannelingTestResult(result_queue)\n    if not param_queue.empty():\n        server_addr, backend_dsn = param_queue.get()\n\n        if server_addr is not None:\n            os.environ['EDGEDB_TEST_CLUSTER_ADDR'] = json.dumps(server_addr)\n        if backend_dsn:\n            os.environ['EDGEDB_TEST_BACKEND_DSN'] = backend_dsn\n\n    os.environ['EDGEDB_TEST_PARALLEL'] = '1'\n    coverage_run = devmode.CoverageConfig.start_coverage_if_requested()\n    py_hash_secret = cpython_state.get_py_hash_secret()\n    status_queue.put(True)\n\n\ndef shutdown_worker() -> None:\n    global coverage_run\n\n    teardown_suite()\n    if coverage_run is not None:\n        coverage_run.stop()\n        coverage_run.save()\n\n\nclass StreamingTestSuite(unittest.TestSuite):\n    _cleanup = False\n\n    def run(self, test, result):\n        with warnings.catch_warnings(record=True) as ww:\n            warnings.resetwarnings()\n            warnings.simplefilter('default')\n\n            # This is temporary, until we implement `subtransaction`\n            # functionality of RFC1004\n            warnings.filterwarnings(\n                'ignore',\n                message=r'The \"transaction\\(\\)\" method is deprecated'\n                        r' and is scheduled to be removed',\n                category=DeprecationWarning)\n\n            self._run(test, result)\n\n            if ww:\n                for wmsg in ww:\n                    if wmsg.source is not None:\n                        wmsg.source = str(wmsg.source)\n                    result.addWarning(test, wmsg)\n\n    def _run(self, test, result):\n        result._testRunEntered = True\n        self._tearDownPreviousClass(test, result)\n        self._handleModuleFixture(test, result)\n        self._handleClassSetUp(test, result)\n        result._previousTestClass = test.__class__\n\n        if (getattr(test.__class__, '_classSetupFailed', False) or\n                getattr(result, '_moduleSetUpFailed', False)):\n            return\n\n        result.annotate_test(test, {\n            'py-hash-secret': py_hash_secret,\n            'py-random-seed': py_random_seed,\n            'runner-pid': os.getpid(),\n        })\n\n        start = time.monotonic()\n        test.run(result)\n        elapsed = time.monotonic() - start\n\n        result.record_test_stats(test, {'running-time': elapsed})\n\n        result._testRunEntered = False\n        return result\n\n\ndef _run_test(workload):\n    suite = StreamingTestSuite()\n\n    if isinstance(workload, collections.abc.Iterable):\n        # Got a test suite\n        for test in workload:\n            suite.run(test, result)\n    else:\n        suite.run(workload, result)\n\n\ndef _is_exc_info(args):\n    return (\n        isinstance(args, tuple) and\n        len(args) == 3 and\n        issubclass(args[0], BaseException)\n    )\n\n\ndef _is_assert_failure(args):\n    if _is_exc_info(args):\n        return issubclass(args[0], AssertionError)\n    elif isinstance(args, str):\n        # HACK: If we serialized the error on the client side... just\n        # detect it in the string.\n        return \"\\nAssertionError\" in args\n    else:\n        return False\n\n\n@dataclasses.dataclass\nclass SerializedServerError:\n    test_error: str\n    server_error: str\n\n\nclass ChannelingTestResultMeta(type):\n    @staticmethod\n    def get_wrapper(meth):\n        def _wrapper(self, *args, **kwargs):\n            args = list(args)\n\n            if args and _is_exc_info(args[-1]):\n                exc_info = args[-1]\n                err = self._exc_info_to_string(exc_info, args[0])\n                if isinstance(exc_info[1], edgedb.EdgeDBError):\n                    srv_tb = exc_info[1].get_server_context()\n                    if srv_tb:\n                        err = SerializedServerError(err, srv_tb)\n                args[-1] = err\n\n            try:\n                self._queue.put((meth, args, kwargs))\n            except Exception:\n                print(\n                    f'!!! Test worker child process: '\n                    f'failed to serialize arguments for {meth}: '\n                    f'*args={args} **kwargs={kwargs} !!!')\n                raise\n        return _wrapper\n\n    def __new__(mcls, name, bases, dct):\n        for meth in {'startTest', 'addSuccess', 'addError', 'addFailure',\n                     'addSkip', 'addExpectedFailure', 'addUnexpectedSuccess',\n                     'addSubTest', 'addWarning', 'record_test_stats',\n                     'annotate_test'}:\n            dct[meth] = mcls.get_wrapper(meth)\n\n        return super().__new__(mcls, name, bases, dct)\n\n\nclass ChannelingTestResult(unittest.result.TestResult,\n                           metaclass=ChannelingTestResultMeta):\n    def __init__(self, queue):\n        super().__init__(io.StringIO(), False, 1)\n        self._queue = queue\n\n    def _setupStdout(self):\n        pass\n\n    def _restoreStdout(self):\n        pass\n\n    def printErrors(self):\n        pass\n\n    def printErrorList(self, flavour, errors):\n        pass\n\n    def __getstate__(self):\n        state = self.__dict__.copy()\n        state.pop('_queue')\n        state.pop('_original_stdout')\n        state.pop('_original_stderr')\n        return state\n\n\ndef monitor_thread(queue, result):\n    while True:\n        methname, args, kwargs = queue.get()\n        if methname is None and args is None and kwargs is None:\n            # This must be the last message in the queue, injected\n            # when all tests are completed and the pool is about\n            # to be closed.\n            break\n\n        method = result\n        for part in methname.split('.'):\n            method = getattr(method, part)\n        method(*args, **kwargs)\n\n\ndef status_thread_func(\n    result: ParallelTextTestResult,\n    stop_event: threading.Event,\n) -> None:\n    while True:\n        result.report_still_running()\n        time.sleep(1)\n        if stop_event.is_set():\n            break\n\n\nclass ParallelTestSuite(unittest.TestSuite):\n    def __init__(\n        self, tests, server_conn, num_workers, backend_dsn, init_worker\n    ):\n        self.tests = tests\n        self.server_conn = server_conn\n        self.num_workers = num_workers\n        self.stop_requested = False\n        self.backend_dsn = backend_dsn\n        self.init_worker = init_worker\n\n    def run(self, result):\n        # We use SimpleQueues because they are more predictable.\n        # They do the necessary IO directly, without using a\n        # helper thread.\n        result_queue = multiprocessing.SimpleQueue()\n        status_queue = multiprocessing.SimpleQueue()\n        worker_param_queue = multiprocessing.SimpleQueue()\n\n        # Prepopulate the worker param queue with server connection\n        # information.\n        for _ in range(self.num_workers):\n            worker_param_queue.put((self.server_conn, self.backend_dsn))\n\n        result_thread = threading.Thread(\n            name='test-monitor',\n            target=monitor_thread,\n            args=(result_queue, result),\n            daemon=True,\n        )\n        result_thread.start()\n\n        status_thread_stop_event = threading.Event()\n        status_thread = threading.Thread(\n            name='test-status',\n            target=status_thread_func,\n            args=(result, status_thread_stop_event),\n            daemon=True,\n        )\n        status_thread.start()\n\n        initargs = (\n            status_queue, worker_param_queue, result_queue, self.init_worker\n        )\n\n        pool = multiprocessing.Pool(\n            self.num_workers,\n            initializer=mproc_fixes.WorkerScope(init_worker, shutdown_worker),\n            initargs=initargs)\n\n        # Wait for all workers to initialize.\n        for _ in range(self.num_workers):\n            status_queue.get()\n\n        with pool:\n            for is_repeat in (False, True):\n                if self.stop_requested:\n                    break\n                ar = pool.map_async(\n                    _run_test,\n                    filter(\n                        lambda t: ('test_zREPEAT' in str(t)) == is_repeat,\n                        self.tests,\n                    ),\n                    chunksize=1,\n                )\n\n                while True:\n                    try:\n                        ar.get(timeout=0.1)\n                    except multiprocessing.TimeoutError:\n                        # multiprocessing doesn't handle processes\n                        # crashing very well, so we check ourselves\n                        # (having disabled its own child pruning in\n                        # mproc_fixes)\n                        #\n                        # TODO: Should we look into using\n                        # concurrent.futures.ProcessPoolExecutor\n                        # instead?\n                        for p in pool._pool:\n                            if p.exitcode:\n                                tmsg = ''\n                                if isinstance(result, ParallelTextTestResult):\n                                    test = result.current_pids.get(p.pid)\n                                    tmsg = f' while running {test}'\n                                print(\n                                    f\"ERROR: Test worker {p.pid} crashed with \"\n                                    f\"exit code {p.exitcode}{tmsg}\",\n                                    file=sys.stderr,\n                                )\n                                sys.stderr.flush()\n                                os._exit(1)\n\n                        if self.stop_requested:\n                            break\n                        else:\n                            continue\n                    else:\n                        break\n\n        # Wait for pool to shutdown, this includes test teardowns.\n        pool.join()\n\n        # Post the terminal message to the queue so that\n        # test-monitor can stop.\n        result_queue.put((None, None, None))\n        status_thread_stop_event.set()\n\n        # Give the test-monitor and test-status threads some time to process the\n        # queue messages.  If something goes wrong, the thread will be forcibly\n        # joined by a timeout.\n        result_thread.join(timeout=3)\n        status_thread.join(timeout=3)\n\n        return result\n\n\nclass SequentialTestSuite(unittest.TestSuite):\n\n    def __init__(self, tests, server_conn, backend_dsn, worker_init):\n        self.tests = tests\n        self.server_conn = server_conn\n        self.stop_requested = False\n        self.backend_dsn = backend_dsn\n        self.worker_init = worker_init\n\n    def run(self, result_):\n        global result\n        result = result_\n\n        if self.server_conn:\n            os.environ['EDGEDB_TEST_CLUSTER_ADDR'] = \\\n                json.dumps(self.server_conn)\n        if self.backend_dsn:\n            os.environ['EDGEDB_TEST_BACKEND_DSN'] = self.backend_dsn\n\n        if self.worker_init:\n            self.worker_init()\n\n        random.seed(py_random_seed)\n\n        for test in self.tests:\n            _run_test(test)\n            if self.stop_requested:\n                break\n\n        # Make sure the class and the module teardown methods are\n        # executed for the trailing test, _run_test() does not do\n        # this for us.\n        teardown_suite()\n\n        return result\n\n\nclass Markers(enum.Enum):\n    passed = '.'\n    errored = 'E'\n    skipped = 's'\n    failed = 'F'\n    xfailed = 'x'  # expected fail\n    not_implemented = '-'\n    upassed = 'U'  # unexpected success\n\n\nclass OutputFormat(str, enum.Enum):\n    auto = 'auto'\n    simple = 'simple'\n    stacked = 'stacked'\n    verbose = 'verbose'\n\n\nclass BaseRenderer:\n    def __init__(self, *, tests, stream):\n        self.stream = stream\n        self.styles_map = {\n            marker.value: getattr(styles, f'marker_{marker.name}')\n            for marker in Markers}\n\n    def format_test(self, test):\n        if isinstance(test, unittest.case._SubTest):\n            if test.params:\n                params = ', '.join(\n                    f'{k}={v!r}' for k, v in test.params.items())\n            else:\n                params = '<subtest>'\n            return f'{test.test_case} {{{params}}}'\n        else:\n            if hasattr(test, 'fail_notes') and test.fail_notes:\n                fail_notes = ', '.join(\n                    f'{k}={v!r}' for k, v in test.fail_notes.items())\n                return f'{test} {{{fail_notes}}}'\n            else:\n                return str(test)\n\n    def report(self, test, marker, description=None, *, currently_running):\n        raise NotImplementedError\n\n    def report_start(self, test, *, currently_running):\n        return\n\n    def report_still_running(self, still_running: dict[str, float]):\n        return\n\n\nclass SimpleRenderer(BaseRenderer):\n    def report(self, test, marker, description=None, *, currently_running):\n        click.echo(self.styles_map[marker.value](marker.value),\n                   nl=False, file=self.stream)\n\n\nclass VerboseRenderer(BaseRenderer):\n    fullnames = {\n        Markers.passed: 'OK',\n        Markers.errored: 'ERROR',\n        Markers.skipped: 'SKIPPED',\n        Markers.failed: 'FAILED',\n        Markers.xfailed: 'expected failure',\n        Markers.not_implemented: 'not implemented',\n        Markers.upassed: 'unexpected success',\n    }\n\n    def _render_test(self, test, marker, description):\n        test_title = self.format_test(test)\n        if description:\n            return f'{test_title}: {self.fullnames[marker]}: {description}'\n        else:\n            return f'{test_title}: {self.fullnames[marker]}'\n\n    def report(self, test, marker, description=None, *, currently_running):\n        style = self.styles_map[marker.value]\n        click.echo(style(self._render_test(test, marker, description)),\n                   file=self.stream)\n\n    def report_still_running(self, still_running: dict[str, float]) -> None:\n        items = [f\"{t} for {d:.02f}s\" for t, d in still_running.items()]\n        click.echo(f\"still running:\\n  {'\\n   '.join(items)}\")\n\n\nclass MultiLineRenderer(BaseRenderer):\n\n    FT_LABEL = 'First few failed: '\n    FT_MAX_LINES = 6\n\n    R_LABEL = 'Running: '\n    R_MAX_LINES = 6\n\n    def __init__(self, *, tests, stream):\n        super().__init__(tests=tests, stream=stream)\n\n        self.total_tests = len(tests)\n        self.completed_tests = 0\n\n        test_modules = {test.__class__.__module__ for test in tests}\n        max_test_module_len = max((len(self._render_modname(name))\n                                   for name in test_modules), default=0)\n        self.first_col_width = max_test_module_len + 1  # 1 == len(' ')\n\n        self.failed_tests = set()\n\n        self.buffer = collections.defaultdict(str)\n        self.last_lines = -1\n        self.max_lines = 0\n        self.max_label_lines_rendered = collections.defaultdict(int)\n\n    def report(self, test, marker, description=None, *, currently_running):\n        if marker in {Markers.failed, Markers.errored}:\n            test_name = test.id().rpartition('.')[2]\n            if ' ' in test_name:\n                test_name = test_name.split(' ')[0]\n            self.failed_tests.add(test_name)\n\n        self.buffer[test.__class__.__module__] += marker.value\n        self.completed_tests += 1\n        self._render(currently_running)\n\n    def report_start(self, test, *, currently_running):\n        self._render(currently_running)\n\n    def report_still_running(self, still_running: dict[str, float]):\n        # Still-running tests are already reported in normal repert\n        return\n\n    def _render_modname(self, name):\n        return name.replace('.', '/') + '.py'\n\n    def _color_second_column(self, line, style):\n        return line[:self.first_col_width] + style(line[self.first_col_width:])\n\n    def _render(self, currently_running):\n\n        def print_line(line):\n            if len(line) < cols:\n                line += ' ' * (cols - len(line))\n            lines.append(line)\n\n        def print_empty_line():\n            print_line(' ')\n\n        last_render = self.completed_tests == self.total_tests\n        cols, rows = shutil.get_terminal_size()\n        second_col_width = cols - self.first_col_width\n\n        def _render_test_list(label, max_lines, tests, style):\n\n            if (\n                len(label) > self.first_col_width\n                or cols - self.first_col_width <= 40\n            ):\n                return\n\n            print_empty_line()\n\n            line = f'{label}{\" \" * (self.first_col_width - len(label))}'\n            tests_lines = 1\n            for testi, test in enumerate(tests, 1):\n                last = testi == len(tests)\n\n                if not last:\n                    test += ', '\n\n                test_name_len = len(test)\n\n                if len(line) + test_name_len < cols:\n                    line += test\n\n                else:\n                    if tests_lines == max_lines:\n                        if len(line) + 3 < cols:\n                            line += '...'\n                        break\n\n                    else:\n                        line += (cols - len(line)) * ' '\n                        line = self._color_second_column(line, style)\n                        lines.append(line)\n\n                        tests_lines += 1\n                        line = self.first_col_width * ' '\n\n                        if len(line) + test_name_len > cols:\n                            continue\n\n                        line += test\n\n            line += (cols - len(line)) * ' '\n            line = self._color_second_column(line, style)\n            lines.append(line)\n\n            # Prevent the rendered output from \"jumping\" up/down when we\n            # render 2 lines worth of running tests just after we rendered\n            # 3 lines.\n            lkey = label.split(':')[0]\n            # ^- We can't just use `label`, as we append extra information\n            # to the \"Running: (..)\" label, so strip that\n            for _ in range(self.max_label_lines_rendered[lkey] - tests_lines):\n                lines.append(' ' * cols)\n            self.max_label_lines_rendered[lkey] = max(\n                self.max_label_lines_rendered[lkey],\n                tests_lines\n            )\n\n        clear_cmd = ''\n        if self.last_lines > 0:\n            # Move cursor up `last_lines` times.\n            clear_cmd = f'\\r\\033[{self.last_lines}A'\n\n        lines = []\n        for mod, progress in self.buffer.items():\n            line = self._render_modname(mod).ljust(self.first_col_width, ' ')\n            while progress:\n                second_col = progress[:second_col_width]\n                second_col = second_col.ljust(second_col_width, ' ')\n\n                progress = progress[second_col_width:]\n\n                # Apply styles *after* slicing and padding the string\n                # (otherwise ANSI codes could be sliced in half).\n                second_col = re.sub(\n                    r'\\S',\n                    lambda x: self.styles_map[x[0]](x[0]),\n                    second_col)\n\n                lines.append(f'{line}{second_col}')\n\n                if line[0] != ' ':\n                    line = ' ' * self.first_col_width\n\n        if not last_render:\n            if self.failed_tests:\n                _render_test_list(\n                    self.FT_LABEL,\n                    self.FT_MAX_LINES,\n                    self.failed_tests,\n                    styles.marker_errored,\n                )\n\n            running_tests = []\n            for test in currently_running:\n                test_name = test.id().rpartition('.')[2]\n                if ' ' in test_name:\n                    test_name = test_name.split(' ')[0]\n                running_tests.append(test_name)\n\n            if not running_tests:\n                running_tests.append('...')\n\n            _render_test_list(\n                self.R_LABEL + f'({len(currently_running)})',\n                self.R_MAX_LINES,\n                running_tests,\n                styles.marker_passed\n            )\n\n        print_empty_line()\n        print_line(\n            f'Progress: {self.completed_tests}/{self.total_tests} tests.'\n        )\n\n        if self.max_lines > len(lines):\n            for _ in range(self.max_lines - len(lines)):\n                lines.insert(0, ' ' * cols)\n\n        if not last_render:\n            # If it's not the last test, check if our render buffer\n            # requires more rows than currently visible.\n            if len(lines) + 1 > rows:\n                # Scroll the render buffer to the bottom and\n                # cut the lines from the beginning, so that it\n                # will fit the screen.\n                #\n                # We need to do this because we can't move the\n                # cursor past the visible screen area, so if we\n                # render more data than the screen can fit, we\n                # will have lot's of garbage output.\n                lines = lines[len(lines) + 1 - rows:]\n                lines[0] = '^' * cols\n\n        # Hide cursor.\n        print('\\033[?25l', end='', flush=True, file=self.stream)\n        try:\n            # Use `print` (not `click.echo`) because we want to\n            # precisely control when the output is flushed.\n            print(clear_cmd + '\\n'.join(lines), flush=False, file=self.stream)\n        finally:\n            # Show cursor.\n            print('\\033[?25h', end='', flush=True, file=self.stream)\n\n        self.last_lines = len(lines)\n        self.max_lines = max(self.last_lines, self.max_lines)\n\n\nclass ParallelTextTestResult(unittest.result.TestResult):\n    def __init__(self, *, stream, verbosity, warnings, tests,\n                 output_format=OutputFormat.auto, failfast=False, suite):\n        super().__init__(stream, False, verbosity)\n        self.verbosity = verbosity\n        self.catch_warnings = warnings\n        self.failfast = failfast\n        self.test_stats = []\n        self.test_annotations = collections.defaultdict(dict)\n        self.warnings = []\n        self.notImplemented = []\n        self.currently_running = {}\n        self.current_pids = {}\n        # An index of all seen warnings to keep track\n        # of repeated warnings.\n        self._warnings = {}\n        self.suite = suite\n\n        if (output_format is OutputFormat.verbose or\n                (output_format is OutputFormat.auto and self.verbosity > 1)):\n            self.ren = VerboseRenderer(tests=tests, stream=stream)\n        elif (output_format is OutputFormat.stacked or\n                (output_format is OutputFormat.auto and stream.isatty() and\n                 shutil.get_terminal_size()[0] > 60 and\n                 os.name != 'nt')):\n            self.ren = MultiLineRenderer(tests=tests, stream=stream)\n        else:\n            self.ren = SimpleRenderer(tests=tests, stream=stream)\n\n    def report_progress(self, test, marker, description=None):\n        self.currently_running.pop(test, None)\n        self.ren.report(\n            test,\n            marker,\n            description,\n            currently_running=list(self.currently_running),\n        )\n\n    def report_still_running(self):\n        now = time.monotonic()\n        still_running = {}\n        for test, start in self.currently_running.items():\n            running_for = now - start\n            if running_for > 5.0:\n                key = str(test)\n                if (\n                    test in self.test_annotations\n                    and (pid := self.test_annotations[test].get('runner-pid'))\n                ):\n                    key = f'{key} (pid={pid})'\n\n                still_running[key] = running_for\n        if still_running:\n            self.ren.report_still_running(still_running)\n\n    def record_test_stats(self, test, stats):\n        self.test_stats.append((test, stats))\n\n    def annotate_test(self, test, annotations: dict[str, Any]) -> None:\n        self.test_annotations[test].update(annotations)\n\n    def get_test_annotations(self, test) -> Optional[dict[str, Any]]:\n        return self.test_annotations.get(test)\n\n    def _exc_info_to_string(self, err, test):\n        # Errors are serialized in the worker.\n        return err\n\n    def getDescription(self, test):\n        return self.ren.format_test(test)\n\n    def startTest(self, test):\n        super().startTest(test)\n        self.currently_running[test] = time.monotonic()\n        self.ren.report_start(\n            test, currently_running=list(self.currently_running))\n        if (\n            test in self.test_annotations\n            and (pid := self.test_annotations[test].get('runner-pid'))\n        ):\n            self.current_pids[pid] = test\n\n    def addSuccess(self, test):\n        super().addSuccess(test)\n        self.report_progress(test, Markers.passed)\n\n    def addError(self, test, err):\n        super().addError(test, err)\n        self.report_progress(test, Markers.errored)\n        if self.failfast:\n            self.suite.stop_requested = True\n\n    def addFailure(self, test, err):\n        super().addFailure(test, err)\n        self.report_progress(test, Markers.failed)\n        if self.failfast:\n            self.suite.stop_requested = True\n\n    def addSubTest(self, test, subtest, err):\n        if err is not None:\n            self.errors.append((subtest, self._exc_info_to_string(err, test)))\n            self._mirrorOutput = True\n\n            self.ren.report(\n                subtest,\n                Markers.errored,\n                currently_running=list(self.currently_running))\n            if self.failfast:\n                self.suite.stop_requested = True\n\n    def addSkip(self, test, reason):\n        super().addSkip(test, reason)\n        self.report_progress(test, Markers.skipped)\n\n    def addExpectedFailure(self, test, err):\n        method = getattr(test, test._testMethodName)\n        try:\n            reason = method.__et_xfail_reason__\n            not_impl = getattr(method, '__et_xfail_not_implemented__', False)\n            allow_fail = getattr(method, '__et_xfail_allow_failure__', False)\n            allow_error = getattr(method, '__et_xfail_allow_error__', False)\n        except AttributeError:\n            # Maybe the whole test case class is decorated?\n            reason = getattr(test, '__et_xfail_reason__', None)\n            not_impl = getattr(test, '__et_xfail_not_implemented__', False)\n            allow_fail = getattr(test, '__et_xfail_allow_failure__', False)\n            allow_error = getattr(test, '__et_xfail_allow_error__', False)\n\n        marker = Markers.not_implemented if not_impl else Markers.xfailed\n        if not_impl:\n            self.notImplemented.append(\n                (test, self._exc_info_to_string(err, test)))\n        else:\n            is_fail = _is_assert_failure(err)\n            if (allow_fail and is_fail) or (allow_error and not is_fail):\n                super().addExpectedFailure(test, err)\n            else:\n                if is_fail:\n                    super().addFailure(test, err)\n                else:\n                    super().addError(test, err)\n\n        self.report_progress(test, marker, reason)\n\n    def addUnexpectedSuccess(self, test):\n        super().addUnexpectedSuccess(test)\n        self.report_progress(test, Markers.upassed)\n\n    def addWarning(self, test, wmsg):\n        if not self.catch_warnings:\n            return\n\n        key = str(wmsg.message), wmsg.filename, wmsg.lineno\n\n        if key not in self._warnings:\n            self._warnings[key] = wmsg\n            self.warnings.append((test, warnings.formatwarning(\n                wmsg.message, wmsg.category, wmsg.filename, wmsg.lineno,\n                wmsg.line\n            )))\n\n    def wasSuccessful(self):\n        # Overload TestResult.wasSuccessful to ignore unexpected successes\n        return (len(self.failures) == len(self.errors) == 0)\n\n\nclass ParallelTextTestRunner:\n\n    def __init__(self, *, stream=None, num_workers=1, verbosity=1,\n                 output_format=OutputFormat.auto, warnings=True,\n                 failfast=False, shuffle=False, backend_dsn=None,\n                 data_dir=None, try_cached_db=False, use_data_dir_dbs=False):\n        self.stream = stream if stream is not None else sys.stderr\n        self.num_workers = num_workers\n        self.verbosity = verbosity\n        self.warnings = warnings\n        self.failfast = failfast\n        self.shuffle = shuffle\n        self.output_format = output_format\n        self.backend_dsn = backend_dsn\n        self.data_dir = data_dir\n        self.use_data_dir_dbs = use_data_dir_dbs\n        self.try_cached_db = try_cached_db\n\n    def run(\n        self,\n        test: Any,\n        selected_shard: int,\n        total_shards: int,\n        running_times_log_file: Optional[Any],\n    ) -> results.TestResult:\n        session_start = time.monotonic()\n        cases = tb.get_test_cases([test])\n        stats = {}\n        if running_times_log_file:\n            running_times_log_file.seek(0)\n            stats = {\n                k: (float(v), int(c))\n                for k, v, c in csv.reader(running_times_log_file)\n            }\n        cases = tb.get_cases_by_shard(\n            cases, selected_shard, total_shards, self.verbosity, stats,\n        )\n        setup = tb.get_test_cases_setup(cases)\n        server_used = tb.test_cases_use_server(cases)\n        worker_init = None\n        bootstrap_time_taken = 0.0\n        tests_time_taken = 0.0\n        result: Optional[ParallelTextTestResult] = None\n        cluster: Optional[edb_cluster.BaseCluster] = None\n        conn = None\n        tempdir = None\n        setup_stats = []\n\n        if server_used:\n            tempdir = tempfile.TemporaryDirectory(prefix=\"edb-test-\")\n\n            if (\n                not os.environ.get(\"EDGEDB_SERVER_TLS_CERT_FILE\")\n                and not os.environ.get(\"EDGEDB_SERVER_TLS_KEY_FILE\")\n                and not os.environ.get(\"GEL_SERVER_TLS_CERT_FILE\")\n                and not os.environ.get(\"GEL_SERVER_TLS_KEY_FILE\")\n            ):\n                if self.verbosity >= 1:\n                    self._echo(\n                        'Generating TLS key and certificate...',\n                        fg='white',\n                    )\n                cert_file = pathlib.Path(tempdir.name) / \"tlscert.pem\"\n                key_file = pathlib.Path(tempdir.name) / \"tlskey.pem\"\n                tb.generate_tls_cert(cert_file, key_file, [\"localhost\"])\n\n                os.environ[\"GEL_SERVER_TLS_CERT_FILE\"] = str(cert_file)\n                os.environ[\"GEL_SERVER_TLS_KEY_FILE\"] = str(key_file)\n\n            if (\n                not os.environ.get(\"EDGEDB_SERVER_JWS_KEY_FILE\")\n                and not os.environ.get(\"GEL_SERVER_JWS_KEY_FILE\")\n            ):\n                jwk_file = pathlib.Path(tempdir.name) / \"jwk.json\"\n                if self.verbosity >= 1:\n                    self._echo(\n                        'Generating JSON Web Key...',\n                        fg='white',\n                    )\n                tb.generate_jwk(jwk_file)\n\n                os.environ[\"GEL_SERVER_JWS_KEY_FILE\"] = str(jwk_file)\n\n        try:\n            if setup:\n                if self.verbosity >= 1:\n                    self._echo(\n                        'Populating test databases... ',\n                        fg='white',\n                        nl=False,\n                    )\n\n                if self.verbosity > 1:\n                    self._echo(\n                        '\\n -> Bootstrapping Gel instance...',\n                        fg='white',\n                        nl=False,\n                    )\n\n                async def _setup():\n                    nonlocal cluster\n                    nonlocal conn\n\n                    data_dir = self.data_dir\n\n                    if (\n                        self.try_cached_db\n                        and (cache_file := (\n                            devmode.get_dev_mode_cache_dir() / 'test_dbs.tar')\n                        ).is_file()\n                    ):\n                        if self.verbosity >= 1:\n                            self._echo(\n                                f'(using DB cache from {cache_file}) ',\n                                fg='white',\n                                nl=False,\n                            )\n\n                        data_dir = tempfile.mkdtemp(prefix=\"edb-test-c-\")\n\n                        # We shell out to tar with subprocess instead of using\n                        # tarfile because it is quite a bit faster.\n                        subprocess.check_call(\n                            ('tar', 'xf', cache_file, '--strip-components=1'),\n                            cwd=data_dir,\n                        )\n\n                    cluster = await tb.init_cluster(\n                        backend_dsn=self.backend_dsn,\n                        cleanup_atexit=False,\n                        data_dir=data_dir,\n                    )\n\n                    if self.verbosity > 1:\n                        self._echo(' OK')\n\n                    conn = cluster.get_connect_args()\n\n                    if not cluster.has_create_database():\n                        return []\n\n                    if not cluster.has_create_role():\n                        for case in cases:\n                            case.is_superuser = False\n\n                    stats = await tb.setup_test_cases(\n                        cases,\n                        conn,\n                        self.num_workers,\n                        verbose=self.verbosity > 1,\n                        try_cached_db=(\n                            self.try_cached_db or self.use_data_dir_dbs\n                        ),\n                    )\n                    if self.try_cached_db and any(\n                        not x[1]['cached'] for x in stats\n                    ):\n                        # We stop the cluster before making a cache of\n                        # the data directory. This isn't strictly\n                        # necessary, but it speeds up startup when\n                        # restoring a cached directory, since postgres\n                        # needs to go through recovery if the shutdown\n                        # wasn't clean.\n                        cluster.stop()\n                        if self.verbosity > 1:\n                            self._echo(\n                                f'\\n -> Writing DB cache to {cache_file} ...',\n                                fg='white',\n                                nl=False,\n                            )\n                        subprocess.check_output(\n                            ('tar', 'cf', cache_file, '.'),\n                            cwd=cluster._data_dir,\n                            stderr=subprocess.STDOUT,\n                        )\n                        await cluster.start(port=conn['port'])\n\n                    return stats\n\n                setup_stats = asyncio.run(_setup())\n\n                assert cluster\n                if cluster.has_create_database():\n                    os.environ.update({\n                        'EDGEDB_TEST_CASES_SET_UP': \"skip\"\n                    })\n                else:\n                    os.environ.update({\n                        'EDGEDB_TEST_CASES_SET_UP': \"inplace\"\n                    })\n                os.environ.update({\n                    'EDGEDB_TEST_HAS_CREATE_ROLE': str(\n                        cluster.has_create_role()\n                    )\n                })\n\n                bootstrap_time_taken = time.monotonic() - session_start\n\n                if self.verbosity >= 1:\n                    self._echo('OK')\n\n            start = time.monotonic()\n\n            all_tests = list(itertools.chain.from_iterable(\n                tests for tests in cases.values()))\n\n            suite: unittest.TestSuite\n            if self.num_workers > 1:\n                suite = ParallelTestSuite(\n                    self._sort_tests(cases),\n                    conn,\n                    self.num_workers,\n                    self.backend_dsn,\n                    worker_init,\n                )\n            else:\n                suite = SequentialTestSuite(\n                    self._sort_tests(cases),\n                    conn,\n                    self.backend_dsn,\n                    worker_init,\n                )\n\n            result = ParallelTextTestResult(\n                stream=self.stream, verbosity=self.verbosity,\n                warnings=self.warnings, failfast=self.failfast,\n                output_format=self.output_format,\n                tests=all_tests, suite=suite)\n            unittest.signals.registerResult(result)\n\n            self._echo()\n            suite.run(result)\n\n            if running_times_log_file:\n                for test, stat in result.test_stats + setup_stats:\n                    name = str(test)\n                    t = stat['running-time']\n                    at, c = stats.get(name, (0, 0))\n                    stats[name] = (at + (t - at) / (c + 1), c + 1)\n                running_times_log_file.seek(0)\n                running_times_log_file.truncate()\n                writer = csv.writer(running_times_log_file)\n                for k, v in stats.items():\n                    writer.writerow((k, ) + v)\n            tests_time_taken = time.monotonic() - start\n\n        except KeyboardInterrupt:\n            raise\n\n        finally:\n            if self.verbosity == 1:\n                self._echo()\n\n            if tempdir is not None:\n                tempdir.cleanup()\n\n            if setup:\n                self._echo()\n                self._echo('Shutting down test cluster... ', nl=False)\n                tb._shutdown_cluster(cluster, destroy=self.data_dir is None)\n                self._echo('OK.')\n\n        if result is not None:\n            return results.collect_result_data(\n                result, bootstrap_time_taken, tests_time_taken\n            )\n        else:\n            return None\n\n    def _echo(self, s: str = '', **kwargs):\n        if self.verbosity > 0:\n            click.secho(s, file=self.stream, **kwargs)\n\n    def _sort_tests(self, cases):\n        serialized_suites = {}\n        exclusive_suites = set()\n        exclusive_tests = []\n\n        for casecls, tests in cases.items():\n            gg = getattr(casecls, 'get_parallelism_granularity', None)\n            granularity = gg() if gg is not None else 'default'\n\n            if granularity == 'suite':\n                serialized_suites[casecls] = unittest.TestSuite(tests)\n            elif granularity == 'system':\n                exclusive_tests.extend(tests)\n                exclusive_suites.add(casecls)\n\n        tests = itertools.chain(\n            serialized_suites.values(),\n            itertools.chain.from_iterable(\n                tests for casecls, tests in cases.items()\n                if (\n                    casecls not in serialized_suites\n                    and casecls not in exclusive_suites\n                )\n            ),\n            [unittest.TestSuite(exclusive_tests)],\n        )\n\n        test_list = list(tests)\n        if self.shuffle:\n            random.shuffle(test_list)\n\n        return test_list\n\n\n# Disable pickling of traceback objects in multiprocessing.\n# Test errors' tracebacks are serialized manually by\n# `TestReesult._exc_info_to_string()`.  Therefore we need\n# to make sure that some random __traceback__ attribute\n# doesn't crash the test results queue.\nmultiprocessing.reduction.ForkingPickler.register(\n    types.TracebackType,\n    lambda o: (_restore_Traceback, ()))\n\n\ndef _restore_Traceback():\n    return None\n"
  },
  {
    "path": "edb/tools/test/styles.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2017-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport click\n\n\n__all__ = ()\n\n\nmarker_passed = lambda t: t\nmarker_errored = lambda t: click.style(t, fg='red', bold=True)\nmarker_skipped = lambda t: click.style(t, fg='yellow')\nmarker_failed = lambda t: click.style(t, fg='red', bold=True)\nmarker_xfailed = lambda t: t\nmarker_not_implemented = lambda t: t\nmarker_upassed = lambda t: click.style(t, fg='yellow')\n\nstatus = lambda t: click.style(t, fg='white', bold=True)\nwarning = lambda t: click.style(t, fg='yellow')\n"
  },
  {
    "path": "edb/tools/test_extension.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\n\nimport pathlib\nimport sys\nimport typing\n\nimport click\nimport edgedb\n\nfrom edb import edgeql\nfrom edb.edgeql import ast as qlast\nfrom edb.tools.edb import edbcommands\n\n\n@edbcommands.command(\"test-extension-package\")\n@click.argument(\n    \"script_path\",\n    type=pathlib.Path,\n)\n@click.option('--localdev/--no-localdev',\n              help='whether to connect to _localdev instance by default',\n              default=True)\ndef test_extension(\n    script_path: pathlib.Path,\n    localdev: bool\n) -> None:\n    '''Installs an extension package into a dev environment and creates it.\n\n    Removes the extension and package first if it already exists.'''\n\n    with open(script_path) as f:\n        script = f.read()\n\n    statements = edgeql.parse_block(script)\n    if not statements or not isinstance(\n        statements[0], qlast.CreateExtensionPackage\n    ):\n        print(\"Script does not begin with CREATE EXTENSION PACKAGE\")\n        sys.exit(1)\n    extension_name = statements[0].name.name\n\n    conn_params: dict[str, typing.Any] = {}\n    if localdev:\n        conn_params = dict(\n            dsn='_localdev',\n            tls_security='insecure',\n        )\n    db = edgedb.create_client(**conn_params)\n\n    db.execute(f'''\n        configure current database set __internal_testmode := true;\n    ''')\n\n    # Delete the extension and the package if it already exists\n    ext = db.query('''\n        select schema::Extension filter .name = <str>$0;\n    ''', extension_name)\n    if ext:\n        print(f\"Dropping existing extension {extension_name}\")\n        db.execute(f'''\n            drop extension {extension_name};\n        ''')\n    ext_package = db.query('''\n        select sys::ExtensionPackage {version} filter .name = <str>$0;\n    ''', extension_name)\n    if ext_package:\n        v = ext_package[0].version\n        version = f'{v.major}.{v.minor}.{v.stage_no}'\n        print(\n            f\"Dropping existing extension package {extension_name} \"\n            f\"version {version}\"\n        )\n        db.execute(f'''\n            drop extension package {extension_name} VERSION '{version}';\n        ''')\n\n    # Run the script; should create the package\n    print(f\"Creating extension package {extension_name}\")\n    db.execute(script)\n\n    # Create the extension\n    print(f\"Creating extension {extension_name}\")\n    db.execute(f'''\n        create extension {extension_name};\n    ''')\n"
  },
  {
    "path": "edb/tools/toy_eval_model.py",
    "content": "# mypy: no-ignore-errors, strict-optional, disallow-any-generics\n\n\"\"\"Toy evaluator model for an edgeql subset.\n\nThe idea here is to be able to test queries against a simple semantics\ndriven evaluator model. The core of this is basically a transcription\nof the \"EdgeQL - Overview\" section of the docs.\n\nThis version does not have any understanding of schemas and has the\nfunction signatures and behaviors of a bunch of basic functions\nhardcoded in.\n\nThe data model is a super simple in-memory one hardcoded into this\nfile, though it shouldn't be too hard to populate it from a real DB or\nvice versa to do testing.\n\nIt is a goal that this can usefully be pointed at different corners of\nthe language for testing. It is a non-goal that it can be scaled up to\nbe a full evaluator model; if it can serve as the basis of one, great,\nbut I'm not worrying about that yet. If we have to start from scratch,\nalso fine, because this one is pretty simple so not much will be lost\nin throwing it away.\n\nAlso a non-goal: performance.\n\nRight now we support a lot of the core SELECT fragment of the language,\nbut are missing:\n * Any DML at all\n * Most casts (we support int and str casts)\n * Most of the standard library, except for some basic functions\n * Any understanding of modules\n * Any understanding of the schema or the type hierarchy\n * Cardinality inference for shape elements; everything is reported as as list\n\nCardinality inference is one of the big open design questions that I have:\nis there a reasonable way that we could implement it as a mostly-dynamic\nanalysis, without needing a full separate cardinality checker?\n\nWe don't really understand name shadowing at all.\n\nThere is no type or error checking.\n\nRun this with `python3 -m edb.tools.toy_eval_model` for a bad REPL\nthat can be noodled around in. I've tested out a bunch of queries\nplaying around and have a small test suite but this hasn't gotten any\nparticular rigorous testing against the real DB.\n\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n    Protocol,\n    Iterable,\n    Iterator,\n    Sequence,\n    Collection,\n    NamedTuple,\n)\n\nfrom edb.common import debug\nfrom edb.common.compiler import SimpleCounter\nfrom edb import edgeql\n\nfrom edb.common.ast import NodeVisitor\nfrom edb.common.compiler import AliasGenerator\nfrom edb.edgeql import ast as qlast\nfrom edb.edgeql import qltypes as ft\nfrom edb.edgeql import desugar_group\n\nfrom dataclasses import dataclass, replace, field\nfrom collections import defaultdict\n\nimport argparse\nimport contextlib\nimport functools\nimport itertools\nimport json\nimport operator\nimport pprint\nimport random\nimport statistics\nimport sys\nimport traceback\nimport uuid\n\n\nDESUGARING_GROUP = True\n\n\ndef bsid(n: int) -> uuid.UUID:\n    return uuid.UUID(f'ffffffff-ffff-ffff-ffff-{n:012x}')\n\n\n# ############# Data model\n\nData = Any\nResult = list[Data]\nRow = tuple[Data, ...]\n\n\nclass DB(NamedTuple):\n    data: dict[uuid.UUID, dict[str, Data]]\n    # We have a bad hacky mechanism for specifying schema computables,\n    # but it's good enough to let us have these things in test queries.\n    schema_computables: dict[str, dict[str, qlast.Expr]]\n\n\nclass Obj:\n    def __init__(\n        self,\n        id: uuid.UUID,\n        shape: Optional[dict[str, Data]]=None,\n        data: Optional[dict[str, Data]]=None,\n    ) -> None:\n        self.id = id\n        if shape is None:\n            shape = {\"id\": id}\n        self.shape = shape\n        self.data = data or {}\n\n    def __eq__(self, other: Any) -> bool:\n        return isinstance(other, Obj) and other.id == self.id\n\n    def __repr__(self) -> str:\n        return f'Obj{self.shape!r}'\n\n    def get(self, name: str, db: DB) -> Optional[Data]:\n        if name in self.data:\n            return self.data[name]\n        else:\n            return db.data[self.id].get(name)\n\n    def get_required(self, name: str, db: DB) -> Data:\n        x = self.get(name, db)\n        assert x\n        return x\n\n\ndef mk_db(\n    data: Iterable[dict[str, Data]],\n    schema_computables: dict[str, dict[str, str]],\n) -> DB:\n    return DB(\n        {x[\"id\"]: x for x in data},\n        {\n            typ: {ptr: parse_fragment(ql) for ptr, ql in d.items()}\n            for typ, d in schema_computables.items()\n        }\n    )\n\n\ndef bslink(n: int, **kwargs: Data) -> Data:\n    lprops = {'@' + k: v for k, v in kwargs.items()}\n    return Obj(bsid(n), data=lprops)\n\n\ndef mk_free_object(\n    shape: Optional[dict[str, Data]]=None,\n    data: Optional[dict[str, Data]]=None,\n) -> Obj:\n    id = uuid.uuid4()\n    base_data = {'id': id, '__type__': 'FreeObject'}\n    if data:\n        base_data.update(data)\n    return Obj(id, shape, base_data)\n\n\n# # Toy basis stuff\n\nSET_OF, OPTIONAL, SINGLETON = (\n    ft.TypeModifier.SetOfType, ft.TypeModifier.OptionalType,\n    ft.TypeModifier.SingletonType)\n\n# We just list things with weird behavior\nBASIS = {\n    'count': [SET_OF],\n    'sum': [SET_OF],\n    'min': [SET_OF],\n    'max': [SET_OF],\n    'all': [SET_OF],\n    'any': [SET_OF],\n    'array_agg': [SET_OF],\n    'enumerate': [SET_OF],\n    'IN': [SINGLETON, SET_OF],\n    'NOT IN': [SINGLETON, SET_OF],\n    '??': [OPTIONAL, SET_OF],\n    'EXISTS': [SET_OF],\n    'DISTINCT': [SET_OF],\n    'IF': [SET_OF, SINGLETON, SET_OF],\n    'UNION': [SET_OF, SET_OF],\n    '?=': [OPTIONAL, OPTIONAL],\n    '?!=': [OPTIONAL, OPTIONAL],\n    'math::mean': [SET_OF],\n}\n\n\n# #############\n@dataclass(frozen=True, order=True)\nclass IPathElement:\n    def is_alias_ref(self) -> bool:\n        return False\n\n\n@dataclass(frozen=True, order=True)\nclass IPartial(IPathElement):\n    pass\n\n\n@dataclass(frozen=True, order=True)\nclass IExpr(IPathElement):\n    expr: qlast.Expr\n\n\n@dataclass(frozen=True, order=True)\nclass IORef(IPathElement):\n    name: str\n    is_alias: bool = field(default=False, compare=False)\n\n    def is_alias_ref(self) -> bool:\n        return self.is_alias\n\n\n@dataclass(frozen=True, order=True)\nclass ITypeIntersection(IPathElement):\n    typ: str\n\n\n@dataclass(frozen=True, order=True)\nclass IPtr(IPathElement):\n    name: str\n    direction: Optional[str] = None\n    is_link_property: bool = False\n\n\n# Wrapper to indicate that an entry in the input tuple is really\n# a *set* bound by an alias.\nclass Alias(NamedTuple):\n    contents: list[Data]\n\n\nIPath = tuple[IPathElement, ...]\n\n# Implementation of built in functions and operators\n\n\nclass LiftedFunc(Protocol):\n    def __call__(self, *args: Result) -> Result:\n        pass\n\n\ndef lift(f: Callable[..., Data]) -> LiftedFunc:\n    \"\"\"Lifts a function operating on base data to operator on sets.\n\n    The result is the usual cartesian product.\"\"\"\n    def inner(*args: Result) -> Result:\n        out = []\n        for args1 in itertools.product(*args):\n            val = f(*args1)\n            out.append(val)\n        return out\n    return inner\n\n\ndef lift_set_of(f: Callable[..., Data]) -> LiftedFunc:\n    def inner(*args: Result) -> Result:\n        return [f(*args)]\n    return inner\n\n\ndef opt_eq(x: Result, y: Result) -> Result:\n    if not x or not y:\n        return [len(x) == len(y)]\n    return lift(operator.eq)(x, y)\n\n\ndef opt_ne(x: Result, y: Result) -> Result:\n    if not x or not y:\n        return [len(x) != len(y)]\n    return lift(operator.ne)(x, y)\n\n\ndef contains(es: Result, s: Result) -> Result:\n    return [e in s for e in es]\n\n\ndef not_contains(es: Result, s: Result) -> Result:\n    return [e not in s for e in es]\n\n\ndef coalesce(x: Result, y: Result) -> Result:\n    return strip_shapes(x or y)\n\n\ndef distinct(x: Result) -> Result:\n    return dedup(x)\n\n\ndef union(x: Result, y: Result) -> Result:\n    return strip_shapes(x + y)\n\n\ndef enumerate_(x: Result) -> Result:\n    return list(enumerate(x))\n\n\ndef array_agg(x: Result) -> Result:\n    return [x]\n\n\ndef array_unpack(x: Result) -> Result:\n    return [y for array in x for y in array]\n\n\ndef if_(x: Result, bs: Result, y: Result) -> Result:\n    out = []\n    for b in bs:\n        if b:\n            out.extend(x)\n        else:\n            out.extend(y)\n    return out\n\n\ndef bad_array_cast(x: Data) -> Result:\n    if x != []:\n        raise ValueError(\"We only know how to cast empty arrays\")\n    return x\n\n\n# For implementing a next() testing function\nNextCounter = SimpleCounter()\n\n\n_BASIS_BINOP_IMPLS: Any = {\n    '+': lift(operator.add),\n    '-': lift(operator.sub),\n    '*': lift(operator.mul),\n    '/': lift(operator.truediv),\n    '//': lift(operator.floordiv),\n    '%': lift(operator.mod),\n    '++': lift(operator.add),\n    '=': lift(operator.eq),\n    '!=': lift(operator.ne),\n    '<': lift(operator.lt),\n    '<=': lift(operator.le),\n    '>': lift(operator.gt),\n    '>=': lift(operator.ge),\n    '^': lift(operator.pow),\n    'OR': lift(operator.or_),\n    'AND': lift(operator.and_),\n    '?=': opt_eq,\n    '?!=': opt_ne,\n    'IN': contains,\n    'NOT IN': not_contains,\n    '??': coalesce,\n    'UNION': union,\n    # ... not really a binop\n    'IF': if_,\n}\n_BASIS_UNOP_IMPLS: Any = {\n    '-': lift(operator.neg),\n    '+': lift(operator.pos),\n    'NOT': lift(operator.not_),\n    'EXISTS': lift_set_of(bool),\n    'DISTINCT': distinct,\n}\n_BASIS_CAST_IMPLS: Any = {\n    'str': lift(str),\n    'int32': lift(int),\n    'int64': lift(int),\n    'uuid': lift(uuid.UUID),\n    'array': lift(bad_array_cast),\n}\n_BASIS_FUNC_IMPLS: Any = {\n    'enumerate': enumerate_,\n    'count': lift_set_of(len),\n    'sum': lift_set_of(sum),\n    'min': lift_set_of(min),\n    'max': lift_set_of(max),\n    'all': lift_set_of(all),\n    'any': lift_set_of(any),\n    'len': lift(len),\n    'math::mean': lift_set_of(statistics.mean),\n    'array_agg': array_agg,\n    'array_unpack': array_unpack,\n    'random': lift(random.random),\n    'contains': lift(operator.contains),\n    'round': lift(round),\n    'next': lift(NextCounter.nextval),  # testing func, not really in std\n    'uuid_generate_v1mc': lift(uuid.uuid4),\n}\n\nBASIS_IMPLS: dict[tuple[str, str], LiftedFunc] = {\n    (typ, key): impl\n    for typ, impls in [\n        ('binop', _BASIS_BINOP_IMPLS),\n        ('unop', _BASIS_UNOP_IMPLS),\n        ('cast', _BASIS_CAST_IMPLS),\n        ('func', _BASIS_FUNC_IMPLS),\n    ]\n    for key, impl in impls.items()\n}\n\n\n# ############### The actual evaluator\n\n@dataclass\nclass EvalContext:\n    query_input_list: list[IPath]\n    input_tuple: tuple[Data, ...]\n    cur_path: Optional[qlast.Path]\n    db: DB\n\n\n@functools.singledispatch\ndef _eval(\n    node: qlast.Base,\n    ctx: EvalContext,\n) -> Result:\n    raise NotImplementedError(\n        f'no EdgeQL eval handler for {node.__class__}')\n\n\ndef graft(\n    prefix: Optional[qlast.Path], new: qlast.Path, always_partial: bool = False\n) -> qlast.Path:\n    if new.partial or always_partial:\n        assert prefix is not None\n        return qlast.Path(\n            steps=prefix.steps + new.steps, partial=prefix.partial)\n    else:\n        return new\n\n\ndef update_path(\n    prefix: Optional[qlast.Path],\n    query: Optional[qlast.Expr],\n    subject: bool = False,\n) -> Optional[qlast.Path]:\n    if query is None:\n        return None\n    elif subject and isinstance(\n        query, (qlast.DeleteQuery, qlast.UpdateQuery, qlast.GroupQuery)\n    ):\n        if (\n            isinstance(query, qlast.GroupQuery)\n            and query.subject_alias is not None\n        ):\n            return qlast.Path(\n                steps=[qlast.ObjectRef(name=query.subject_alias)])\n        else:\n            query = query.subject\n    elif isinstance(\n        query, (qlast.SelectQuery, qlast.ForQuery, qlast.InternalGroupQuery)\n    ):\n        if query.result_alias is not None:\n            return qlast.Path(steps=[\n                qlast.ObjectRef(name=query.result_alias)\n            ])\n        else:\n            query = query.result\n\n    while isinstance(query, qlast.Shape):\n        query = query.expr\n\n    if isinstance(query, qlast.Path):\n        return graft(prefix, query)\n    else:\n        # XXX: synthesize a real prefix name?\n        return qlast.Path(partial=True, steps=[])\n\n\ndef ptr_name(ptr: qlast.Ptr) -> str:\n    name = ptr.name\n    if ptr.type == 'property':\n        name = '@' + name\n    return name\n\n\ndef ensure_ql_query(expr: qlast.Expr) -> qlast.Query:\n    if not isinstance(expr, qlast.Query):\n        expr = qlast.SelectQuery(\n            result=expr,\n            implicit=True,\n        )\n    return expr\n\n\ndef eval_filter(\n    where: Optional[qlast.Expr],\n    qil: list[IPath],\n    out: list[Row],\n    ctx: EvalContext\n) -> list[Row]:\n    if not where:\n        return out\n\n    new = []\n    for row in out:\n        subctx = replace(ctx, query_input_list=qil, input_tuple=row)\n\n        if any(subquery(where, ctx=subctx)):\n            new.append(row)\n    return new\n\n\ndef eval_orderby(\n    orderby: list[qlast.SortExpr],\n    qil: list[IPath],\n    out: list[Row],\n    ctx: EvalContext\n) -> list[Row]:\n    # Go through the sort specifiers in reverse order, which takes\n    # advantage of sort being stable to do the right thing. (We can't\n    # just build one composite key because they might have different\n    # sort orders, so we'd have to use cmp_to_key if we wanted to do\n    # it in one go...)\n    for sort in reversed(orderby):\n        nones_bigger = (\n            (sort.direction == 'ASC' and sort.nones_order == 'last')\n            or (sort.direction == 'DESC' and sort.nones_order == 'first')\n        )\n\n        # Decorate\n        new = []\n        for row in out:\n            subctx = replace(ctx, query_input_list=qil, input_tuple=row)\n            vals = subquery(sort.path, ctx=subctx)\n            assert len(vals) <= 1\n            # We wrap the result in a tuple with an emptiness tag at the start\n            # to handle sorting empty values\n            val = (not nones_bigger, vals[0]) if vals else (nones_bigger,)\n            new.append((row, (val,)))\n\n        # Sort\n        new.sort(key=lambda x: x[1], reverse=sort.direction == 'DESC')\n\n        # Undecorate\n        out = [row for row, _ in new]\n\n    return out\n\n\ndef eval_offset(\n    offset: Optional[qlast.Expr], out: list[Row], ctx: EvalContext\n) -> list[Row]:\n    if offset:\n        res = subquery(offset, ctx=ctx)\n        assert len(res) == 1\n        out = out[int(res[0]):]\n    return out\n\n\ndef eval_limit(\n    limit: Optional[qlast.Expr], out: list[Row], ctx: EvalContext\n) -> list[Row]:\n    if limit:\n        res = subquery(limit, ctx=ctx)\n        assert len(res) == 1\n        out = out[:int(res[0])]\n    return out\n\n\ndef add_alias(name: str, vals: Data, ctx: EvalContext) -> EvalContext:\n    return replace(\n        ctx,\n        query_input_list=ctx.query_input_list + [\n            (IORef(name, is_alias=True),)],\n        input_tuple=ctx.input_tuple + (Alias(vals),),\n    )\n\n\ndef eval_aliases(node: qlast.Statement, ctx: EvalContext) -> EvalContext:\n    if node.aliases:\n        for alias in node.aliases:\n            assert isinstance(alias, qlast.AliasedExpr)\n            ctx = add_alias(\n                alias.alias, subquery(alias.expr, ctx=ctx), ctx=ctx)\n\n    return ctx\n\n\n@_eval.register\ndef eval_Select(node: qlast.SelectQuery, ctx: EvalContext) -> Result:\n    ctx = eval_aliases(node, ctx)\n\n    # XXX: I believe this is right, but:\n    # WHERE and ORDER BY are treated as subqueries of the result query,\n    # and LIMIT and OFFSET are not.\n    subq_path = update_path(ctx.cur_path, node)\n\n    orderby = node.orderby or []\n    # XXX: wait, why do we need extra_subqs? All the queries I've\n    # thought of where it has an effect\n    # (SELECT (User.name, User.deck.cost) FILTER User.deck.name != \"Dragon\")\n    # get rejected by the real compiler for \"changing the interpretation\".\n    subqs = [node.where] + [x.path for x in orderby]\n    extra_subqs = [(subq_path, subq) for subq in subqs]\n    new_qil, out = subquery_full(node.result, extra_subqs=extra_subqs, ctx=ctx)\n    if node.result_alias:\n        # If there is a result alias, the body is treated as being a subquery,\n        # so it doesn't become visible.\n        for i in range(len(ctx.query_input_list), len(new_qil)):\n            new_qil[i] = ()\n    new_qil += [simplify_path(subq_path) if subq_path else (IPartial(),)]\n\n    subq_ctx = replace(ctx, cur_path=subq_path)\n    out = eval_filter(node.where, new_qil, out, ctx=subq_ctx)\n    out = eval_orderby(orderby, new_qil, out, ctx=subq_ctx)\n\n    limoff_ctx = replace(ctx, cur_path=None)\n    out = eval_offset(node.offset, out, ctx=limoff_ctx)\n    out = eval_limit(node.limit, out, ctx=limoff_ctx)\n\n    return [row[-1] for row in out]\n\n\n# From the itertools docs\ndef powerset[T](iterable: Iterable[T]) -> Iterable[tuple[T, ...]]:\n    \"powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)\"\n    s = list(iterable)\n    return itertools.chain.from_iterable(\n        itertools.combinations(s, r) for r in range(len(s) + 1))\n\n\ndef simplify_grouping_sets(\n    gset: qlast.GroupingElement,\n) -> list[qlast.GroupingAtom]:\n    if isinstance(gset, qlast.GroupingSimple):\n        return [gset.element]\n    elif isinstance(gset, qlast.GroupingSets):\n        return [x for s in gset.sets for x in simplify_grouping_sets(s)]\n    elif isinstance(gset, qlast.GroupingOperation):\n        if gset.oper == 'cube':\n            return [\n                qlast.GroupingIdentList(elements=x)\n                for x in powerset(gset.elements)\n            ]\n        elif gset.oper == 'rollup':\n            return [\n                qlast.GroupingIdentList(elements=tuple(gset.elements[:i]))\n                for i in range(len(gset.elements) + 1)\n            ]\n\n    raise ValueError\n\n\nByElement = IORef | IPtr\n\n\ndef get_by_element(atom: qlast.ObjectRef | qlast.Path) -> ByElement:\n    if isinstance(atom, qlast.ObjectRef):\n        return IORef(atom.name)\n    else:\n        assert isinstance(atom.steps[0], qlast.Ptr)\n        return IPtr(atom.steps[0].name)\n\n\ndef flatten_grouping_atom(atom: qlast.GroupingAtom) -> tuple[ByElement, ...]:\n    if isinstance(atom, (qlast.ObjectRef, qlast.Path)):\n        return (get_by_element(atom),)\n    else:\n        assert isinstance(atom, qlast.GroupingIdentList)\n        return tuple(\n            x for g in atom.elements\n            for x in flatten_grouping_atom(g)\n        )\n\n\ndef get_grouping_sets(\n    node: qlast.GroupQuery | qlast.InternalGroupQuery\n) -> list[tuple[ByElement, ...]]:\n    toplevel_gsets = []\n    for col in node.by:\n        gsets = simplify_grouping_sets(col)\n        simp_gsets = [flatten_grouping_atom(x) for x in gsets]\n        toplevel_gsets.append(simp_gsets)\n\n    return [\n        tuple(x for y in g for x in y)\n        for g in itertools.product(*toplevel_gsets)\n    ]\n\n\ndef _keyify(v: Data) -> Data:\n    if isinstance(v, list):\n        v = tuple(v)\n    if isinstance(v, tuple):\n        v = tuple(_keyify(x) for x in v)\n    if isinstance(v, dict):\n        return tuple(sorted((k, _keyify(v)) for k, v in v.items()))\n    return v\n\n\ndef get_groups(\n    node: qlast.GroupQuery | qlast.InternalGroupQuery, ctx: EvalContext\n) -> list[tuple[tuple[Data, ...], tuple[dict[ByElement, Data], list[Data]]]]:\n    ctx = eval_aliases(node, ctx)\n\n    # Actually evaluate the subject\n    subject_vals = subquery(node.subject, ctx=ctx)\n\n    subq_path = update_path(ctx.cur_path, node, subject=True)\n    new_qil = ctx.query_input_list + [\n        simplify_path(subq_path) if subq_path else (IPartial(),)]\n\n    # Collect all the grouping sets from the node\n    grouping_sets = get_grouping_sets(node)\n    all_keys = tuple(dedup([x for g in grouping_sets for x in g]))\n\n    # For every subject value, evaluate all of the expressions in the\n    # USING clause and all the path prefixes used in BY and record them.\n    vals_and_keys = []\n    for val in subject_vals:\n        subctx = replace(ctx, query_input_list=new_qil,\n                         cur_path=subq_path,\n                         input_tuple=ctx.input_tuple + (val,))\n\n        keys: dict[ByElement, Data] = {}\n        # Collect all the USING bindings\n        for using in (node.using or ()):\n            using_val = eval(using.expr, ctx=subctx)\n            assert len(using_val) <= 1\n            subctx = add_alias(using.alias, using_val, subctx)\n            keys[IORef(using.alias)] = using_val\n        # And collect all the partial path references\n        for key_el in all_keys:\n            if isinstance(key_el, IPtr):\n                assert not isinstance(node, qlast.InternalGroupQuery)\n                key_val = eval_ptr(val, key_el, ctx=subctx)\n                assert len(key_val) <= 1\n                keys[key_el] = key_val\n\n        vals_and_keys.append((val, keys))\n\n    # With the keys computed, run through every grouping set and\n    # produce our groups.\n    all_groups = []\n    # Rebuild the set tuples from all_keys to both deduplicate\n    # and ensure a canonical order.\n    grouping_sets = [\n        tuple(k for k in all_keys if k in grouping_set)\n        for grouping_set in grouping_sets\n    ]\n    for grouping_set in grouping_sets:\n        groups: dict[\n            tuple[Data, ...],\n            tuple[dict[ByElement, Data], list[Data]]\n        ] = {}\n        for val, keys in vals_and_keys:\n            # Prune the keys down to just this grouping set\n            keys = {k: v if k in grouping_set else [] for k, v in keys.items()}\n            key = _keyify([\n                None if not keys[k] else keys[k][0]\n                for k in grouping_set])\n            groups.setdefault(key, (keys, []))[1].append(val)\n\n        # We need to always output a group for the empty grouping set, if\n        # it exists.\n        if grouping_set == () and () not in groups:\n            groups[()] = ({k: [] for k in all_keys}, [])\n\n        all_groups.extend([(grouping_set, v) for v in groups.values()])\n\n    return all_groups\n\n\ndef direct_eval_group(node: qlast.GroupQuery, ctx: EvalContext) -> Result:\n    all_groups = get_groups(node, ctx)\n\n    # Now we can produce our output.\n    out = []\n    for grouping, (bindings, elements) in all_groups:\n        key_dict = {k.name: v for k, v in bindings.items()}\n        key_obj = mk_free_object(key_dict, key_dict)\n        group_dict = {\n            'key': [key_obj],\n            'elements': elements,\n            'grouping': [g.name for g in grouping],\n        }\n        group_obj = mk_free_object(group_dict, group_dict)\n        out.append(group_obj)\n\n    return out\n\n\n@_eval.register\ndef eval_InternalGroup(\n    node: qlast.InternalGroupQuery, ctx: EvalContext\n) -> Result:\n    all_groups = get_groups(node, ctx)\n\n    out = []\n    for grouping, (bindings, elements) in all_groups:\n        key_dict = {k.name: v for k, v in bindings.items()}\n        subctx = ctx\n        for k, v in key_dict.items():\n            subctx = add_alias(k, v, subctx)\n        subctx = add_alias(node.group_alias, elements, subctx)\n        if node.grouping_alias:\n            subctx = add_alias(\n                node.grouping_alias,\n                [[g.name.split('~')[0] for g in grouping]],\n                subctx)\n\n        new_qil, new = subquery_full(node.result, ctx=subctx)\n        out += new\n\n    if not out:\n        return []\n\n    # XXX: There is some duplication with SELECT here\n    subq_path = update_path(ctx.cur_path, node)\n    if node.result_alias:\n        # If there is a result alias, the body is treated as being a subquery,\n        # so it doesn't become visible.\n        for i in range(len(ctx.query_input_list), len(new_qil)):\n            new_qil[i] = ()\n    new_qil += [simplify_path(subq_path) if subq_path else (IPartial(),)]\n\n    subq_ctx = replace(ctx, cur_path=subq_path)\n    out = eval_filter(node.where, new_qil, out, ctx=subq_ctx)\n    out = eval_orderby(node.orderby or [], new_qil, out, ctx=subq_ctx)\n\n    return [row[-1] for row in out]\n\n\n@_eval.register\ndef eval_Group(node: qlast.GroupQuery, ctx: EvalContext) -> Result:\n    if DESUGARING_GROUP:\n        return eval(\n            desugar_group.desugar_group(node, AliasGenerator()), ctx=ctx)\n    else:\n        return direct_eval_group(node, ctx=ctx)\n\n\n@_eval.register\ndef eval_ShapeElement(el: qlast.ShapeElement, ctx: EvalContext) -> Result:\n    if el.compexpr:\n        result = el.compexpr\n    else:\n        result = qlast.Path(partial=True, steps=el.expr.steps)\n\n    if el.elements:\n        result = qlast.Shape(expr=result, elements=el.elements)\n\n    fake_select = qlast.SelectQuery(\n        result=result,\n        orderby=el.orderby,\n        where=el.where,\n        limit=el.limit,\n        offset=el.offset,\n    )\n\n    return eval(fake_select, ctx=ctx)\n\n\nFREE_SHAPE_EXPR = qlast.DetachedExpr(\n    expr=qlast.Path(\n        steps=[qlast.ObjectRef(name='FreeObject')],\n    ),\n)\n\n\n@_eval.register\ndef eval_Shape(node: qlast.Shape, ctx: EvalContext) -> Result:\n\n    expr = node.expr or FREE_SHAPE_EXPR\n\n    subq_path = update_path(ctx.cur_path, expr)\n    subq_ipath = simplify_path(subq_path) if subq_path else (IPartial(),)\n    qil = ctx.query_input_list + [subq_ipath]\n\n    # XXX: do we need to do extra_subqs??\n    shape_vals = eval(expr, ctx=ctx)\n\n    out = []\n    for val in shape_vals:\n        subctx = replace(ctx, query_input_list=qil,\n                         cur_path=subq_path,\n                         input_tuple=ctx.input_tuple + (val,))\n\n        vals = {}\n        for el in node.elements:\n            ptr = el.expr.steps[0]\n            assert isinstance(ptr, qlast.Ptr)\n            name = ptr_name(ptr)\n\n            el_val = eval(el, ctx=subctx)\n            vals[name] = el_val\n\n        # Merge any data already on the object with any new shape info\n        data = {**val.data, **vals}\n\n        val = Obj(val.id, shape=vals, data=data)\n        out.append(val)\n\n    return out\n\n\n@_eval.register\ndef eval_For(node: qlast.ForQuery, ctx: EvalContext) -> Result:\n    ctx = eval_aliases(node, ctx)\n    iter_vals = strip_shapes(subquery(node.iterator, ctx=ctx))\n    if node.optional and not iter_vals:\n        iter_vals = [None]\n    qil = ctx.query_input_list + [(IORef(node.iterator_alias),)]\n    out = []\n    for val in iter_vals:\n        subctx = replace(ctx, query_input_list=qil,\n                         input_tuple=ctx.input_tuple + (val,))\n        out.extend(subquery(node.result, ctx=subctx))\n\n    return out\n\n\n@_eval.register\ndef eval_DetachedExpr(node: qlast.DetachedExpr, ctx: EvalContext) -> Result:\n    return toplevel_query(node.expr, db=ctx.db)\n\n\ndef eval_func_or_op(\n    op: str, args: list[qlast.Expr], typ: str, ctx: EvalContext\n) -> Result:\n    arg_specs = BASIS.get(op)\n\n    results = []\n    for i, arg in enumerate(args):\n        if arg_specs and arg_specs[i] in (SET_OF, OPTIONAL):\n            # SET OF is a subquery\n            results.append(subquery(arg, ctx=ctx))\n        else:\n            results.append(eval(arg, ctx=ctx))\n\n    f = BASIS_IMPLS[typ, op]\n    return f(*results)\n\n\n@_eval.register\ndef eval_BinOp(node: qlast.BinOp, ctx: EvalContext) -> Result:\n    return eval_func_or_op(\n        node.op.upper(), [node.left, node.right], 'binop', ctx)\n\n\n@_eval.register\ndef eval_UnaryOp(node: qlast.UnaryOp, ctx: EvalContext) -> Result:\n    return eval_func_or_op(node.op.upper(), [node.operand], 'unop', ctx)\n\n\n@_eval.register\ndef eval_Call(node: qlast.FunctionCall, ctx: EvalContext) -> Result:\n    func = node.func\n    if isinstance(func, tuple):\n        func = '::'.join(func)  # sure, for now.\n    return eval_func_or_op(func, node.args or [], 'func', ctx)\n\n\n@_eval.register\ndef visit_IfElse(query: qlast.IfElse, ctx: EvalContext) -> Result:\n    return eval_func_or_op(\n        'IF', [query.if_expr, query.condition, query.else_expr], 'binop', ctx)\n\n\n@_eval.register\ndef eval_Indirection(node: qlast.Indirection, ctx: EvalContext) -> Result:\n    base = eval(node.arg, ctx)\n    for index in node.indirection:\n        index_out = (\n            lift(slice)(eval(index.start, ctx) if index.start else [None],\n                        eval(index.stop, ctx) if index.stop else [None])\n            if isinstance(index, qlast.Slice)\n            else eval(index.index, ctx)\n        )\n        base = lift(operator.getitem)(base, index_out)\n    return base\n\n\n@_eval.register\ndef eval_Constant(node: qlast.Constant, ctx: EvalContext) -> Result:\n    if node.kind == qlast.ConstantKind.STRING:\n        return [node.value]\n    elif node.kind == qlast.ConstantKind.INTEGER:\n        return [int(node.value)]\n    elif node.kind == qlast.ConstantKind.BOOLEAN:\n        return [node.value == 'true']\n    elif node.kind == qlast.ConstantKind.FLOAT:\n        return [float(node.value)]\n    raise AssertionError('unimplemented')\n\n\n@_eval.register\ndef eval_Set(node: qlast.Set, ctx: EvalContext) -> Result:\n    out = []\n    for elem in node.elements:\n        out.extend(eval(elem, ctx))\n    return out\n\n\n@_eval.register\ndef eval_Tuple(node: qlast.Tuple, ctx: EvalContext) -> Result:\n    args = [eval(arg, ctx) for arg in node.elements]\n    return lift(lambda *va: va)(*args)\n\n\n@_eval.register\ndef eval_Array(node: qlast.Array, ctx: EvalContext) -> Result:\n    args = [eval(arg, ctx) for arg in node.elements]\n    return lift(lambda *va: list(va))(*args)\n\n\n@_eval.register\ndef eval_NamedTuple(node: qlast.NamedTuple, ctx: EvalContext) -> Result:\n    names = [elem.name.name for elem in node.elements]\n    args = [eval(arg.val, ctx) for arg in node.elements]\n    return lift(lambda *va: dict(zip(names, va)))(*args)\n\n\n@_eval.register\ndef eval_TypeCast(node: qlast.TypeCast, ctx: EvalContext) -> Result:\n    typ = node.type.maintype.name  # type: ignore  # our types are hinky.\n    f = BASIS_IMPLS['cast', typ]\n    return f(eval(node.expr, ctx))\n\n\n@_eval.register\ndef eval_Path(node: qlast.Path, ctx: EvalContext) -> Result:\n    return eval_path(simplify_path(graft(ctx.cur_path, node)), ctx)\n\n\ndef eval(node: qlast.Base, ctx: EvalContext) -> Result:\n    return _eval(node, ctx)\n\n# Query setup\n\n\ndef fix_links(links: Optional[Data]) -> Result:\n    if links is None:\n        links = []\n    if not isinstance(links, list):\n        links = [links]\n    return links\n\n\ndef lookup_computed(\n    obj: Obj, name: str, ctx: EvalContext\n) -> Optional[tuple[qlast.Expr, str, Obj]]:\n    \"\"\"Lookup a schema-computed property\n\n    Return (code, source type name, source object).\n    \"\"\"\n    if not (typ := obj.get('__type__', ctx.db)):\n        return None\n\n    typ_computed = ctx.db.schema_computables.get(typ)\n    if name[0] != '@' and typ_computed and name in typ_computed:\n        return typ_computed[name], typ, obj\n\n    elif (\n        name[0] == '@'\n        and (src := obj.get('@source', ctx.db))\n        and (src_ptr := obj.get('@__source_link', ctx.db))\n        and (src_type := src.get('__type__', ctx.db))\n        and (src_computed := ctx.db.schema_computables.get(src_type))\n        and f'{src_ptr}{name}' in src_computed\n    ):\n        return src_computed[f'{src_ptr}{name}'], src_type, src\n    else:\n        return None\n\n\ndef eval_computed(\n    obj: Obj,\n    name: str,\n    query: qlast.Expr,\n    typ: str,\n    src: Obj,\n    *,\n    ctx: EvalContext,\n) -> Result:\n    paths = [qlast.Path(steps=[qlast.ObjectRef(name=typ)])]\n\n    if name[0] != '@':\n        input_tuple: tuple[Data, ...] = (obj,)\n    else:\n        # For linkprops, we want both the source and the target in the\n        # query input.\n        paths.append(qlast.Path(\n            steps=paths[0].steps + [qlast.Ptr(name=name)]\n        ))\n        input_tuple = (src, obj)\n\n    subctx = EvalContext(\n        query_input_list=[simplify_path(p) for p in paths],\n        input_tuple=input_tuple,\n        cur_path=paths[-1],\n        db=ctx.db,\n    )\n    return subquery(query, ctx=subctx)\n\n\ndef eval_fwd_ptr(base: Data, ptr: IPtr, ctx: EvalContext) -> Result:\n    if isinstance(base, tuple):\n        return [base[int(ptr.name)]]\n    elif isinstance(base, Obj):\n        name = '@' + ptr.name if ptr.is_link_property else ptr.name\n        data = base.get(name, ctx.db)\n        # could be computed\n        if data is None and (computed := lookup_computed(base, name, ctx)):\n            data = eval_computed(base, name, *computed, ctx=ctx)\n        return fix_links(data)\n    else:\n        return [base[ptr.name]]\n\n\ndef eval_bwd_ptr(base: Data, ptr: IPtr, ctx: EvalContext) -> Result:\n    # XXX: This is slow even by the standards of this terribly slow model\n    res = []\n    for obj in ctx.db.data.values():\n        for tgt in fix_links(obj.get(ptr.name)):\n            if base == tgt:\n                # Extract any lprops and put them on the backlink\n                data = {k: v for k, v in tgt.data.items() if k[0] == '@'}\n                res.append(Obj(obj['id'], data=data))\n                break\n    return res\n\n\ndef eval_ptr(base: Data, ptr: IPtr, ctx: EvalContext) -> Result:\n    return (\n        eval_bwd_ptr(base, ptr, ctx) if ptr.direction == '<'\n        else eval_fwd_ptr(base, ptr, ctx))\n\n\ndef eval_intersect(\n    base: Data, ptr: ITypeIntersection, ctx: EvalContext\n) -> Result:\n    # TODO: we want actual types but for now we just match directly\n    typ = ctx.db.data[base.id][\"__type__\"]\n    return [base] if typ == ptr.typ else []\n\n\n# This should only get called during input tuple building\ndef eval_objref(name: str, ctx: EvalContext) -> Result:\n    if name == 'FreeObject':\n        return [mk_free_object()]\n\n    return [\n        Obj(obj[\"id\"]) for obj in ctx.db.data.values()\n        if obj[\"__type__\"] == name\n    ]\n\n\ndef last_index[T](vs: Sequence[T], x: T) -> int:\n    for i in range(len(vs) - 1, -1, -1):\n        if vs[i] == x:\n            return i\n    raise ValueError(f\"{x} not in list\")\n\n\ndef eval_path(path: IPath, ctx: EvalContext) -> Result:\n    # Base case for stuff in the input list\n    if path in ctx.query_input_list:\n        # need last index, since there could be multiple IPrefixes or the like\n        i = last_index(ctx.query_input_list, path)\n        obj = ctx.input_tuple[i]\n        if isinstance(obj, Alias):\n            return obj.contents\n        return [obj] if obj is not None else []\n\n    if len(path) == 1:\n        if isinstance(path[0], IORef):\n            return eval_objref(path[0].name, ctx)\n        elif isinstance(path[0], IExpr):\n            return eval(path[0].expr, ctx)\n        else:\n            raise AssertionError(f\"Bogus path base: {path[0]}\")\n\n    base = eval_path(path[:-1], ctx)\n    out = []\n    ptr = path[-1]\n    assert isinstance(ptr, (IPtr, ITypeIntersection))\n    for obj in base:\n        if isinstance(ptr, IPtr):\n            out.extend(eval_ptr(obj, ptr, ctx))\n        elif isinstance(ptr, ITypeIntersection):\n            out.extend(eval_intersect(obj, ptr, ctx))\n    # We need to deduplicate links.\n    if base and isinstance(base[0], Obj) and out and isinstance(out[0], Obj):\n        out = dedup(out)\n\n    return out\n\n\ndef build_input_tuples(\n    qil: list[IPath], always_optional: dict[IPath, bool], ctx: EvalContext\n) -> list[tuple[Data, ...]]:\n    data: list[tuple[Data, ...]] = [ctx.input_tuple]\n    for i, in_path in enumerate(qil):\n        new_data: list[tuple[Data, ...]] = []\n        new_qil = ctx.query_input_list + qil[:i]\n        for row in data:\n            subctx = replace(ctx, query_input_list=new_qil, input_tuple=row)\n            out = eval_path(in_path, subctx)\n            for val in out:\n                new_data.append(row + (val,))\n            if not out and always_optional[in_path]:\n                new_data.append(row + (None,))\n        data = new_data\n\n    return data\n\n# ############### Preparation\n\n\nclass PathFinder(NodeVisitor):\n    def __init__(self, cur_path: Optional[qlast.Path]) -> None:\n        super().__init__()\n        self.in_optional = False\n        self.optional_counter = 0\n        self.in_subquery = False\n        self.paths: list[tuple[qlast.Path, Optional[int], bool]] = []\n        self.current_path = cur_path\n\n    def _update(self, **kwargs: Any) -> Iterator[None]:\n        old = {k: getattr(self, k) for k in kwargs}\n        for k, v in kwargs.items():\n            setattr(self, k, v)\n        try:\n            yield\n        finally:\n            for k, v in old.items():\n                setattr(self, k, v)\n\n    @contextlib.contextmanager\n    def subquery(self) -> Iterator[None]:\n        yield from self._update(in_subquery=True)\n\n    @contextlib.contextmanager\n    def update_path(\n        self,\n        query: Optional[qlast.Expr],\n        subject: bool = False,\n    ) -> Iterator[None]:\n        yield from self._update(\n            current_path=update_path(self.current_path, query, subject))\n\n    def visit_Path(\n        self, path: qlast.Path, always_partial: bool = False\n    ) -> None:\n        self.paths.append((\n            graft(self.current_path, path, always_partial=always_partial),\n            self.optional_counter if self.in_optional else None,\n            self.in_subquery,\n        ))\n        self.generic_visit(path)\n\n    def visit_SelectQuery(self, query: qlast.SelectQuery) -> None:\n        with self.subquery():\n            # XXX: shadowing?\n            self.visit(query.aliases)\n\n            if query.result_alias:\n                with self.subquery():\n                    self.visit(query.result)\n            else:\n                self.visit(query.result)\n\n            with self.update_path(query):\n                self.visit(query.orderby)\n                self.visit(query.where)\n\n            with self.update_path(None):\n                self.visit(query.limit)\n                self.visit(query.offset)\n\n    def visit_GroupQuery(self, query: qlast.GroupQuery) -> None:\n        with self.subquery():\n            self.visit(query.aliases)\n\n            if query.subject_alias:\n                with self.subquery():\n                    self.visit(query.subject)\n            else:\n                self.visit(query.subject)\n\n            with self.update_path(query, subject=True):\n                # deal with shadowing?\n                self.visit(query.using)\n\n    def visit_Shape(self, shape: qlast.Shape) -> None:\n        expr = shape.expr or FREE_SHAPE_EXPR\n        self.visit(expr)\n        with self.subquery(), self.update_path(expr):\n            self.visit(shape.elements)\n\n    def visit_ShapeElement(self, el: qlast.ShapeElement) -> None:\n        if not el.compexpr:\n            self.visit_Path(el.expr, always_partial=True)\n        self.visit(el.compexpr)\n        with self.subquery(), self.update_path(el.expr):\n            self.visit(el.elements)\n\n    def visit_ForQuery(self, query: qlast.ForQuery) -> None:\n        with self.subquery():\n            self.generic_visit(query)\n\n    def visit_Set(self, expr: qlast.Set) -> None:\n        with self.subquery():\n            self.visit(expr.elements)\n\n    def visit_func_or_op(self, op: str, args: list[qlast.Expr]) -> None:\n        # Totally ignoring that polymorphic whatever is needed\n        arg_specs = BASIS.get(op)\n        old = self.in_optional, self.in_subquery\n        for i, arg in enumerate(args):\n            if arg_specs:\n                # SET OF is a subquery so we skip it\n                if arg_specs[i] == SET_OF:\n                    self.in_subquery = True\n                elif arg_specs[i] == OPTIONAL:\n                    if not self.in_subquery and not self.in_optional:\n                        self.optional_counter += 1\n                    self.in_optional = True\n\n            self.visit(arg)\n            self.in_optional, self.in_subquery = old\n\n    def visit_BinOp(self, query: qlast.BinOp) -> None:\n        self.visit_func_or_op(query.op.upper(), [query.left, query.right])\n\n    def visit_UnaryOp(self, query: qlast.UnaryOp) -> None:\n        self.visit_func_or_op(query.op.upper(), [query.operand])\n\n    def visit_FunctionCall(self, query: qlast.FunctionCall) -> None:\n        assert not query.kwargs\n        func = query.func\n        if isinstance(func, tuple):\n            func = '::'.join(func)  # sure, for now.\n        self.visit_func_or_op(func, query.args)\n        assert not query.window  # done last or we get dced.\n\n    def visit_IfElse(self, query: qlast.IfElse) -> None:\n        self.visit_func_or_op(\n            'IF', [query.if_expr, query.condition, query.else_expr])\n\n    def visit_DetachedExpr(self, query: qlast.DetachedExpr) -> None:\n        pass\n\n\ndef find_paths(\n    e: qlast.Expr,\n    cur_path: Optional[qlast.Path],\n    extra_subqs: Iterable[\n        tuple[Optional[qlast.Path], Optional[qlast.Base]]] = (),\n) -> list[tuple[qlast.Path, Optional[int], bool]]:\n    pf = PathFinder(cur_path)\n    pf.visit(e)\n    pf.in_subquery = True\n    for path, subq in extra_subqs:\n        pf.current_path = path\n        pf.visit(subq)\n    return pf.paths\n\n\ndef longest_common_prefix(p1: IPath, p2: IPath) -> IPath:\n    common = []\n    for a, b in zip(p1, p2):\n        if a == b:\n            common.append(a)\n        else:\n            break\n    return tuple(common)\n\n\ndef dedup[T](old: Collection[T]) -> list[T]:\n    new: list[T] = []\n    for x in old:\n        if x not in new:\n            new.append(x)\n    return new\n\n\ndef find_common_prefixes(\n    direct_refs: list[tuple[IPath, Optional[int]]],\n    subquery_refs: list[tuple[IPath, Optional[int]]],\n) -> set[IPath]:\n    prefixes = set()\n    for i, (x, ox) in enumerate(direct_refs):\n        # We start from only the refs directly in the query, but we\n        # look for common prefixes with anything in subqueries also.\n        # XXX: The docs are wrong and don't suggest this.\n        for (y, oy) in direct_refs[i:] + subquery_refs:\n            # XXX: Also optional stuff that the docs don't suggest\n            if ox is not None and ox == oy:\n                continue\n            pfx = longest_common_prefix(x, y)\n            if pfx:\n                prefixes.add(pfx)\n    return prefixes\n\n\ndef _contains_non_alias_path(paths: Sequence[IPath], new: IPath) -> bool:\n    return any(\n        new == y and not (len(y) == 1 and y[0].is_alias_ref())\n        for y in paths\n    )\n\n\ndef make_query_input_list(\n    direct_refs: list[tuple[IPath, Optional[int]]],\n    subquery_refs: list[tuple[IPath, Optional[int]]],\n    old: list[IPath],\n) -> list[IPath]:\n    direct_refs = [x for x in direct_refs if isinstance(x[0][0], IORef)]\n    qil = find_common_prefixes(direct_refs, subquery_refs)\n\n    return sorted(x for x in qil if not _contains_non_alias_path(old, x))\n\n\ndef simplify_path(path: qlast.Path) -> IPath:\n    spath: list[IPathElement] = []\n    # XXX: Could there still be some scoping issues with partial?\n    # Probably yes? Maybe we need to make up fake path ids...\n    if path.partial:\n        spath.append(IPartial())\n    for step in path.steps:\n        if isinstance(step, qlast.ObjectRef):\n            assert not spath\n            spath.append(IORef(step.name))\n        elif isinstance(step, qlast.Ptr):\n            is_property = step.type == 'property'\n            spath.append(IPtr(step.name, step.direction, is_property))\n        elif isinstance(step, qlast.TypeIntersection):\n            spath.append(ITypeIntersection(\n                step.type.maintype.name))  # type: ignore\n        elif isinstance(step, qlast.Splat):\n            raise AssertionError(\"splats are not supported yet\")\n        else:\n            assert not spath\n            spath.append(IExpr(step))\n\n    return tuple(spath)\n\n\ndef parse(querystr: str) -> qlast.Expr:\n    source = edgeql.Source.from_string(querystr)\n    statements = edgeql.parse_block(source)\n    assert len(statements) == 1\n    assert isinstance(statements[0], qlast.Expr)\n    return statements[0]\n\n\ndef parse_fragment(querystr: str) -> qlast.Expr:\n    source = edgeql.Source.from_string(querystr)\n    return edgeql.parse_fragment(source)\n\n\ndef analyze_paths(\n    q: qlast.Expr,\n    extra_subqs: Iterable[\n        tuple[Optional[qlast.Path], Optional[qlast.Base]]],\n    cur_path: Optional[qlast.Path],\n) -> tuple[\n    list[tuple[IPath, Optional[int]]],\n    list[tuple[IPath, Optional[int]]],\n    dict[IPath, bool],\n]:\n    paths_opt = [(simplify_path(p), optional, subq)\n                 for p, optional, subq in find_paths(q, cur_path, extra_subqs)]\n\n    # For any link property reference we see, strip off the\n    # link and the link prop, and add that. So for Person.notes@name,\n    # add Person to our input set too. This prevents us from\n    # deduplicating the link before we access the prop.\n    for path, optional, subquery in list(paths_opt):\n        if isinstance(path[-1], IPtr) and path[-1].is_link_property:\n            i = -2\n            while isinstance(path[i], ITypeIntersection):\n                i -= 1\n            paths_opt.append((path[:i], optional, subquery))\n\n    always_optional = defaultdict(lambda: True)\n\n    direct_paths = []\n    subquery_paths = []\n    for path, optional, subquery in paths_opt:\n        if subquery:\n            subquery_paths.append((path, optional))\n        else:\n            direct_paths.append((path, optional))\n            if not optional:\n                # Mark all path prefixes as not being optional\n                for i in range(1, len(path) + 1):\n                    always_optional[path[:i]] = False\n\n    return direct_paths, subquery_paths, always_optional\n\n\ndef subquery_full(\n    q: qlast.Expr,\n    *,\n    extra_subqs: Iterable[\n        tuple[Optional[qlast.Path], Optional[qlast.Base]]] = (),\n    ctx: EvalContext\n) -> tuple[list[IPath], list[Row]]:\n    direct_paths, subquery_paths, always_optional = analyze_paths(\n        q, extra_subqs, ctx.cur_path)\n\n    qil = make_query_input_list(\n        direct_paths, subquery_paths, ctx.query_input_list)\n\n    in_tuples = build_input_tuples(qil, always_optional, ctx)\n\n    # Actually eval it\n    out = []\n    new_qil = ctx.query_input_list + qil\n    for row in in_tuples:\n        subctx = replace(ctx, query_input_list=new_qil, input_tuple=row)\n        for val in eval(q, subctx):\n            out.append(row + (val,))\n\n    return new_qil, out\n\n\ndef subquery(q: qlast.Expr, *, ctx: EvalContext) -> Result:\n    return [row[-1] for row in subquery_full(q, ctx=ctx)[1]]\n\n\ndef toplevel_query(q: qlast.Expr, db: DB) -> Data:\n    ctx = EvalContext(\n        query_input_list=[],\n        input_tuple=(),\n        cur_path=None,\n        db=db,\n    )\n    return subquery(q, ctx=ctx)\n\n\ndef strip_shapes(x: Data) -> Data:\n    if isinstance(x, Obj):\n        return Obj(x.id, data=x.data, shape=None)\n    elif isinstance(x, dict):\n        return {k: strip_shapes(v) for k, v in x.items()}\n    elif isinstance(x, tuple):\n        return tuple(strip_shapes(v) for v in x)\n    elif isinstance(x, list):\n        return [strip_shapes(v) for v in x]\n    else:\n        return x\n\n\ndef clean_data(x: Data, cheat: bool, *, is_el: bool = False) -> Data:\n    if isinstance(x, Obj):\n        return clean_data(x.shape, cheat)\n    elif isinstance(x, dict):\n        return {k: clean_data(v, cheat, is_el=True) for k, v in x.items()}\n    elif isinstance(x, tuple):\n        return tuple(clean_data(v, cheat) for v in x)\n    elif isinstance(x, list):\n        res = [clean_data(v, cheat) for v in x]\n        if cheat and is_el and len(res) == 1:\n            return res[0]\n        return res\n    else:\n        return x\n\n\ndef go(q: qlast.Expr, db: DB, cheat: bool) -> Data:\n    return clean_data(toplevel_query(q, db), cheat)\n\n\nclass EdbJSONEncoder(json.JSONEncoder):\n    def default(self, x: Any) -> Any:\n        if isinstance(x, uuid.UUID):\n            return str(x)\n        return super().default(x)\n\n\ndef run(\n    db: DB,\n    s: str,\n    print_asts: bool, output_mode: str, singleton_cheating: bool,\n) -> None:\n    q = parse(s)\n    if print_asts:\n        debug.dump(q)\n    res = go(q, db, singleton_cheating)\n    if output_mode == 'pprint':\n        pprint.pprint(res)\n    elif output_mode == 'json':\n        print(EdbJSONEncoder().encode(res))\n    else:\n        debug.dump(res)\n\n\ndef repl(\n    db: DB,\n    print_asts: bool=False,\n    output_mode: str='debug',\n    singleton_cheating: bool=False,\n) -> None:\n    # for now users should just invoke this script with rlwrap since I\n    # don't want to fiddle with history or anything\n    while True:\n        print(\"> \", end=\"\", flush=True)\n        s = \"\"\n        while ';' not in s:\n            s += sys.stdin.readline()\n            if not s:\n                return\n        try:\n            run(db, s, print_asts, output_mode, singleton_cheating)\n        except Exception:\n            traceback.print_exception(*sys.exc_info())\n\n\n# Our toy DB\n# Make this UUIDs?\n# This is just documentation I guess.\nSCHEMA = '''\ntype Note {\n    required single property name -> str;\n    optional single property note -> str;\n}\ntype Person {\n    required single property name -> str;\n    optional multi property multi_prop -> str;\n    multi link notes -> Note {\n        property metanote -> str;\n    }\n    optional single property tag -> str;\n}\ntype Foo {\n    required single property val -> str;\n    optional single property opt -> int64;\n}\n'''\n\n\ndef load_json_obj(obj: Any) -> Any:\n    new_obj = {}\n    for k, v in obj.items():\n        if k == 'id':\n            v = uuid.UUID(v)\n        elif k == 'typ':\n            k = '__type__'\n            v = v.replace('test::', '')\n\n        vs = v if isinstance(v, list) else [v]\n        nvs = []\n        for v1 in vs:\n            if isinstance(v1, dict):\n                lprops = {lk: lv for lk, lv in v1.items() if lk[0] == '@'}\n                lprops['@source'] = Obj(uuid.UUID(obj['id']))\n                lprops['@__source_link'] = k\n                v1 = Obj(uuid.UUID(v1['id']), data=lprops)\n            nvs.append(v1)\n        nv = nvs if isinstance(v, list) else nvs[0]\n\n        new_obj[k] = nv\n    return new_obj\n\n\ndef load_json_db(data: Any) -> Any:\n    return [load_json_obj(obj) for obj in data]\n\n\nnull: None = None\nCARDS_DB = [\n    {\n        \"avatar\": {\n            \"@text\": \"Best\",\n            \"id\": \"81537667-c308-11eb-98b8-e7ee6a203949\"\n        },\n        \"awards\": [\n            {\"id\": \"81537661-c308-11eb-98b8-d7ab026ed715\"},\n            {\"id\": \"81537663-c308-11eb-98b8-47f340f064e1\"}\n        ],\n        \"deck\": [\n            {\"@count\": 2, \"id\": \"81537666-c308-11eb-98b8-67d1235c4527\"},\n            {\"@count\": 2, \"id\": \"81537667-c308-11eb-98b8-e7ee6a203949\"},\n            {\"@count\": 3, \"id\": \"81537668-c308-11eb-98b8-2b363efb8a80\"},\n            {\"@count\": 3, \"id\": \"81537669-c308-11eb-98b8-c37076e778d2\"}\n        ],\n        \"friends\": [\n            {\n                \"@nickname\": \"Swampy\",\n                \"id\": \"81537670-c308-11eb-98b8-d3d2e939fbfc\"\n            },\n            {\n                \"@nickname\": \"Firefighter\",\n                \"id\": \"81537671-c308-11eb-98b8-6b6a92e0be3e\"\n            },\n            {\n                \"@nickname\": \"Grumpy\",\n                \"id\": \"81537672-c308-11eb-98b8-53b70c263a56\"\n            }\n        ],\n        \"id\": \"8153766f-c308-11eb-98b8-af7e8ffd99f3\",\n        \"name\": \"Alice\",\n        \"typ\": \"test::User\"\n    },\n    {\n        \"avatar\": null,\n        \"awards\": [{\"id\": \"81537665-c308-11eb-98b8-a7fee63c63ca\"}],\n        \"deck\": [\n            {\"@count\": 3, \"id\": \"81537668-c308-11eb-98b8-2b363efb8a80\"},\n            {\"@count\": 3, \"id\": \"81537669-c308-11eb-98b8-c37076e778d2\"},\n            {\"@count\": 3, \"id\": \"8153766a-c308-11eb-98b8-330dce42eb46\"},\n            {\"@count\": 3, \"id\": \"8153766b-c308-11eb-98b8-430d489d7125\"}\n        ],\n        \"friends\": [],\n        \"id\": \"81537670-c308-11eb-98b8-d3d2e939fbfc\",\n        \"name\": \"Bob\",\n        \"typ\": \"test::User\"\n    },\n    {\n        \"avatar\": null,\n        \"awards\": [],\n        \"deck\": [\n            {\"@count\": 3, \"id\": \"81537668-c308-11eb-98b8-2b363efb8a80\"},\n            {\"@count\": 2, \"id\": \"81537669-c308-11eb-98b8-c37076e778d2\"},\n            {\"@count\": 4, \"id\": \"8153766a-c308-11eb-98b8-330dce42eb46\"},\n            {\"@count\": 2, \"id\": \"8153766b-c308-11eb-98b8-430d489d7125\"},\n            {\"@count\": 4, \"id\": \"8153766c-c308-11eb-98b8-5bd98eec95bd\"},\n            {\"@count\": 3, \"id\": \"8153766d-c308-11eb-98b8-8b072b1a5f69\"},\n            {\"@count\": 1, \"id\": \"8153766e-c308-11eb-98b8-1b59432eef87\"}\n        ],\n        \"friends\": [],\n        \"id\": \"81537671-c308-11eb-98b8-6b6a92e0be3e\",\n        \"name\": \"Carol\",\n        \"typ\": \"test::User\"\n    },\n    {\n        \"avatar\": {\n            \"@text\": \"Wow\",\n            \"id\": \"8153766e-c308-11eb-98b8-1b59432eef87\"\n        },\n        \"awards\": [],\n        \"deck\": [\n            {\"@count\": 1, \"id\": \"81537667-c308-11eb-98b8-e7ee6a203949\"},\n            {\"@count\": 1, \"id\": \"81537668-c308-11eb-98b8-2b363efb8a80\"},\n            {\"@count\": 1, \"id\": \"81537669-c308-11eb-98b8-c37076e778d2\"},\n            {\"@count\": 1, \"id\": \"8153766b-c308-11eb-98b8-430d489d7125\"},\n            {\"@count\": 4, \"id\": \"8153766c-c308-11eb-98b8-5bd98eec95bd\"},\n            {\"@count\": 1, \"id\": \"8153766d-c308-11eb-98b8-8b072b1a5f69\"},\n            {\"@count\": 1, \"id\": \"8153766e-c308-11eb-98b8-1b59432eef87\"}\n        ],\n        \"friends\": [\n            {\"@nickname\": null, \"id\": \"81537670-c308-11eb-98b8-d3d2e939fbfc\"}\n        ],\n        \"id\": \"81537672-c308-11eb-98b8-53b70c263a56\",\n        \"name\": \"Dave\",\n        \"typ\": \"test::User\"\n    },\n    {\n        \"awards\": [{\"id\": \"81537663-c308-11eb-98b8-47f340f064e1\"}],\n        \"cost\": 1,\n        \"element\": \"Fire\",\n        \"id\": \"81537666-c308-11eb-98b8-67d1235c4527\",\n        \"name\": \"Imp\",\n        \"typ\": \"test::Card\"\n    },\n    {\n        \"awards\": [{\"id\": \"81537661-c308-11eb-98b8-d7ab026ed715\"}],\n        \"cost\": 5,\n        \"element\": \"Fire\",\n        \"id\": \"81537667-c308-11eb-98b8-e7ee6a203949\",\n        \"name\": \"Dragon\",\n        \"typ\": \"test::Card\"\n    },\n    {\n        \"awards\": [],\n        \"cost\": 2,\n        \"element\": \"Water\",\n        \"id\": \"81537668-c308-11eb-98b8-2b363efb8a80\",\n        \"name\": \"Bog monster\",\n        \"typ\": \"test::Card\"\n    },\n    {\n        \"awards\": [],\n        \"cost\": 3,\n        \"element\": \"Water\",\n        \"id\": \"81537669-c308-11eb-98b8-c37076e778d2\",\n        \"name\": \"Giant turtle\",\n        \"typ\": \"test::Card\"\n    },\n    {\n        \"awards\": [],\n        \"cost\": 1,\n        \"element\": \"Earth\",\n        \"id\": \"8153766a-c308-11eb-98b8-330dce42eb46\",\n        \"name\": \"Dwarf\",\n        \"typ\": \"test::Card\"\n    },\n    {\n        \"awards\": [],\n        \"cost\": 3,\n        \"element\": \"Earth\",\n        \"id\": \"8153766b-c308-11eb-98b8-430d489d7125\",\n        \"name\": \"Golem\",\n        \"typ\": \"test::Card\"\n    },\n    {\n        \"awards\": [],\n        \"cost\": 1,\n        \"element\": \"Air\",\n        \"id\": \"8153766c-c308-11eb-98b8-5bd98eec95bd\",\n        \"name\": \"Sprite\",\n        \"typ\": \"test::Card\"\n    },\n    {\n        \"awards\": [],\n        \"cost\": 2,\n        \"element\": \"Air\",\n        \"id\": \"8153766d-c308-11eb-98b8-8b072b1a5f69\",\n        \"name\": \"Giant eagle\",\n        \"typ\": \"test::Card\"\n    },\n    {\n        \"awards\": [{\"id\": \"81537665-c308-11eb-98b8-a7fee63c63ca\"}],\n        \"cost\": 4,\n        \"element\": \"Air\",\n        \"id\": \"8153766e-c308-11eb-98b8-1b59432eef87\",\n        \"name\": \"Djinn\",\n        \"typ\": \"test::Card\"\n        # \"typ\": \"test::SpecialCard\"\n    },\n    {\n        \"id\": \"81537661-c308-11eb-98b8-d7ab026ed715\",\n        \"name\": \"1st\",\n        \"typ\": \"test::Award\"\n    },\n    {\n        \"id\": \"81537663-c308-11eb-98b8-47f340f064e1\",\n        \"name\": \"2nd\",\n        \"typ\": \"test::Award\"\n    },\n    {\n        \"id\": \"81537665-c308-11eb-98b8-a7fee63c63ca\",\n        \"name\": \"3rd\",\n        \"typ\": \"test::Award\"\n    }\n]\n\n\nPersonT = \"Person\"\nNoteT = \"Note\"\nFooT = \"Foo\"\n\nSCHEMA_COMPUTABLES = {\n    'Card': {\n        'owners': '.<deck[IS User]',\n        'elemental_cost': \"<str>.cost ++ ' ' ++ .element\",\n        'good_awards': \"(SELECT .awards FILTER .name != '3rd')\",\n    },\n    'User': {\n        'deck_cost': 'sum(.deck.cost)',\n        'deck@total_cost': '@count * .cost',\n        'avatar@tag': '.name ++ ((\"-\" ++ @text) ?? \"\")',\n    },\n}\n\nDB1_stub = [\n    # Person\n    {\"id\": bsid(0x10), \"__type__\": PersonT,\n     \"name\": \"Phil Emarg\",\n     \"notes\": [bslink(0x20), bslink(0x21, metanote=\"arg!\")]},\n    {\"id\": bsid(0x11), \"__type__\": PersonT,\n     \"name\": \"Madeline Hatch\", \"notes\": [bslink(0x21, metanote=\"sigh\")]},\n    {\"id\": bsid(0x12), \"__type__\": PersonT,\n     \"name\": \"Emmanuel Villip\"},\n    # Note\n    {\"id\": bsid(0x20), \"__type__\": NoteT, \"name\": \"boxing\"},\n    {\"id\": bsid(0x21), \"__type__\": NoteT, \"name\": \"unboxing\", \"note\": \"lolol\"},\n    {\"id\": bsid(0x22), \"__type__\": NoteT, \"name\": \"dynamic\", \"note\": \"blarg\"},\n\n    # Foo\n    {\"id\": bsid(0x30), \"__type__\": FooT, \"val\": \"a\"},\n    {\"id\": bsid(0x31), \"__type__\": FooT, \"val\": \"b\", \"opt\": 111},\n\n    # volatility\n    {\"id\": bsid(0x81), \"__type__\": \"Tgt\", \"n\": 1},\n    {\"id\": bsid(0x82), \"__type__\": \"Tgt\", \"n\": 2},\n    {\"id\": bsid(0x83), \"__type__\": \"Tgt\", \"n\": 3},\n    {\"id\": bsid(0x84), \"__type__\": \"Tgt\", \"n\": 4},\n\n    {\"id\": bsid(0x91), \"__type__\": \"Obj\", \"n\": 1,\n     \"tgt\": [bslink(0x81), bslink(0x82)]},\n    {\"id\": bsid(0x92), \"__type__\": \"Obj\", \"n\": 2,\n     \"tgt\": [bslink(0x82), bslink(0x83)]},\n    {\"id\": bsid(0x93), \"__type__\": \"Obj\", \"n\": 3,\n     \"tgt\": [bslink(0x83), bslink(0x84)]},\n]\n\n\ndef mk_DB1():\n    return mk_db(\n        DB1_stub + load_json_db(CARDS_DB),\n        SCHEMA_COMPUTABLES,\n    )\n\n\nparser = argparse.ArgumentParser(description='Toy EdgeQL eval model')\nparser.add_argument('--debug', '-d', action='store_true',\n                    help='Dump ASTs after parsing')\nparser.add_argument('--pprint', '-p', action='store_true',\n                    help='Use pprint instead of debug.dump')\nparser.add_argument('--json', '-j', action='store_true',\n                    help='Use json.dump instead of debug.dump')\n\n# The toy model currently doesn't understand cardinality inference,\n# but reading shape output where everything is a list is just awful.\n# So as a hacky workaround for now, add a flag to just print size one\n# sets as if they were singletons.\nparser.add_argument('--singleton-cheating', '-s', action='store_true',\n                    help='Print length one shape elements as singletons')\n\nparser.add_argument('commands', metavar='cmd', type=str, nargs='*',\n                    help='commands to run')\n\n\ndef main() -> None:\n    db = mk_DB1()\n\n    args = parser.parse_args()\n\n    output_mode = 'json' if args.json else 'pprint' if args.pprint else 'debug'\n\n    if args.commands:\n        for arg in args.commands:\n            run(db, arg, args.debug, output_mode, args.singleton_cheating)\n    else:\n        return repl(db, args.debug, output_mode, args.singleton_cheating)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "edb/tools/wipe.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfrom __future__ import annotations\nfrom typing import TYPE_CHECKING\n\nimport asyncio\nimport json\nimport pathlib\nimport sys\n\nimport click\n\nfrom edb.schema import schema as s_schema\n\nfrom edb.common import topological\nfrom edb.tools.edb import edbcommands\n\nfrom edb.server import compiler as edbcompiler\nfrom edb.server import defines as edbdef\nfrom edb.server import pgcluster\n\nfrom edb.pgsql import common as pgcommon\nfrom edb.pgsql.common import quote_ident as qi\n\nif TYPE_CHECKING:\n    from edb.server import pgcon\n\n\nclass AbsPath(click.Path):\n    name = 'path'\n\n    def convert(self, value, param, ctx):\n        return pathlib.Path(super().convert(value, param, ctx)).absolute()\n\n\n@edbcommands.command('wipe')\n@click.option(\n    '--backend-dsn',\n    type=str,\n    help='DSN of the remote Postgres instance to wipe Gel from')\n@click.option(\n    '-D',\n    '--data-dir',\n    type=AbsPath(),\n    help='database cluster directory')\n@click.option(\n    '--tenant-id',\n    type=str,\n    multiple=True,\n    help='The tenant ID of a Gel server to wipe.  May be specified'\n         ' multiple times.  If not specified, all tenants are wiped.')\n@click.option(\n    '-y',\n    'yes',\n    is_flag=True,\n    help='assume Yes response to all questions')\n@click.option(\n    '--dry-run',\n    is_flag=True,\n    help='give a summary of wipe operations without performing them')\n@click.option(\n    '--list-tenants',\n    is_flag=True,\n    help='list cluster tenants instead of performing a wipe')\ndef wipe(\n    *,\n    backend_dsn,\n    data_dir,\n    tenant_id,\n    yes,\n    dry_run,\n    list_tenants,\n):\n    asyncio.run(do_wipe(\n        backend_dsn=backend_dsn,\n        data_dir=data_dir,\n        tenant_id=tenant_id,\n        yes=yes,\n        dry_run=dry_run,\n        list_tenants=list_tenants,\n    ))\n\n\nasync def do_wipe(\n    *,\n    backend_dsn,\n    data_dir,\n    tenant_id,\n    yes,\n    dry_run,\n    list_tenants,\n):\n    if backend_dsn:\n        cluster = await pgcluster.get_remote_pg_cluster(\n            backend_dsn,\n            tenant_id='<unknown>',\n        )\n    elif data_dir:\n        cluster = await pgcluster.get_local_pg_cluster(\n            data_dir,\n            tenant_id='<unknown>',\n        )\n        cluster.update_connection_params(\n            user='postgres',\n            database='template1',\n        )\n    else:\n        raise click.UsageError(\n            'either --postgres-dsn or --data-dir is required'\n        )\n\n    if not yes and not dry_run and not list_tenants and not click.confirm(\n            'This will DELETE all Gel data from the target '\n            'PostgreSQL instance.  ARE YOU SURE?'):\n        click.echo('OK. Not proceeding.')\n        return\n\n    status = await cluster.get_status()\n    cluster_started_by_us = False\n    if status != 'running':\n        if isinstance(cluster, pgcluster.RemoteCluster):\n            click.secho(f'Remote cluster is not running', fg='red')\n            sys.exit(1)\n        else:\n            await cluster.start()\n            cluster_started_by_us = True\n\n    try:\n        conn = await cluster.connect(source_description=\"Wipe\")\n        if tenant_id:\n            tenants = list(tenant_id)\n        else:\n            tenants = await _get_all_tenants(conn)\n\n        if list_tenants:\n            print('\\n'.join(t if t else '(none)' for t in tenants))\n            return\n        else:\n            for tenant in tenants:\n                await wipe_tenant(cluster, conn, tenant, dry_run)\n    finally:\n        await conn.close()\n        if cluster_started_by_us:\n            await cluster.stop()\n\n\ndef get_database_backend_name(name: str, tenant_id: str) -> str:\n    if not tenant_id:\n        return name\n    else:\n        return pgcommon.get_database_backend_name(name, tenant_id=tenant_id)\n\n\ndef get_role_backend_name(name: str, tenant_id: str) -> str:\n    if not tenant_id:\n        return name\n    else:\n        return pgcommon.get_role_backend_name(name, tenant_id=tenant_id)\n\n\nasync def wipe_tenant(\n    cluster: pgcluster.BaseCluster,\n    pgconn: pgcon.PGConnection,\n    tenant: str,\n    dry_run: bool,\n) -> None:\n    from edb.server import pgcon\n\n    tpl_db = get_database_backend_name(\n        edbdef.EDGEDB_TEMPLATE_DB,\n        tenant_id=tenant,\n    )\n\n    sup_role = get_role_backend_name(\n        edbdef.EDGEDB_SUPERUSER,\n        tenant_id=tenant,\n    )\n\n    try:\n        tpl_conn = await cluster.connect(\n            database=tpl_db,\n            source_description=\"wipe_tenant\",\n        )\n    except pgcon.BackendCatalogNameError:\n        click.secho(\n            f'Instance tenant {tenant!r} does not have the '\n            f'{edbdef.EDGEDB_TEMPLATE_DB!r} database. Is it already clean?'\n        )\n        return\n\n    try:\n        databases, roles = await _get_dbs_and_roles(tpl_conn)\n    finally:\n        tpl_conn.terminate()\n\n    stmts = [\n        f'SET ROLE {qi(sup_role)}',\n    ]\n\n    for db in databases:\n        pg_db = get_database_backend_name(db, tenant_id=tenant)\n        owner = await pgconn.sql_fetch_val(\n            b\"\"\"\n            SELECT\n                rolname\n            FROM\n                pg_database d\n                INNER JOIN pg_roles r\n                    ON (d.datdba = r.oid)\n            WHERE\n                d.datname = $1\n            \"\"\",\n            args=[pg_db.encode(\"utf-8\")],\n        )\n\n        if owner:\n            stmts.append(f'SET ROLE {qi(owner.decode(\"utf-8\"))}')\n\n        if pg_db == tpl_db:\n            stmts.append(f'ALTER DATABASE {qi(pg_db)} IS_TEMPLATE = false')\n\n        stmts.append(f'DROP DATABASE {qi(pg_db)}')\n\n    stmts.append('RESET ROLE;')\n\n    for role in roles:\n        pg_role = get_role_backend_name(role, tenant_id=tenant)\n\n        members = json.loads(await pgconn.sql_fetch_val(\n            b\"\"\"\n            SELECT\n                json_agg(member::regrole::text)\n            FROM\n                pg_auth_members\n            WHERE\n                roleid = (SELECT oid FROM pg_roles WHERE rolname = $1)\n            \"\"\",\n            args=[pg_role.encode(\"utf-8\")],\n        ))\n\n        for member in members:\n            stmts.append(f'REVOKE {qi(pg_role)} FROM {qi(member)}')\n\n        stmts.append(f'DROP ROLE {qi(pg_role)}')\n\n    super_group = get_role_backend_name(\n        edbdef.EDGEDB_SUPERGROUP, tenant_id=tenant)\n    stmts.append(f'DROP ROLE {qi(super_group)}')\n\n    for stmt in stmts:\n        click.echo(stmt + (';' if not stmt.endswith(';') else ''))\n        if not dry_run:\n            await pgconn.sql_execute(stmt.encode(\"utf-8\"))\n\n\nasync def _get_all_tenants(\n    conn: pgcon.PGConnection,\n) -> list[str]:\n    dbs = await conn.sql_fetch_col(\n        b\"\"\"\n        SELECT datname\n        FROM pg_database\n        WHERE datname LIKE $1\n        \"\"\",\n        args=[f\"%{edbdef.EDGEDB_TEMPLATE_DB}\".encode(\"utf-8\")],\n    )\n\n    tenants = []\n    for dbname in (db.decode(\"utf-8\") for db in dbs):\n        if dbname == edbdef.EDGEDB_TEMPLATE_DB:\n            t = \"\"\n        else:\n            t, _, _ = dbname.partition('_')\n        tenants.append(t)\n\n    return tenants\n\n\nasync def _get_dbs_and_roles(\n    pgconn: pgcon.PGConnection,\n) -> tuple[list[str], list[str]]:\n    compiler = await edbcompiler.new_compiler_from_pg(pgconn)\n    compilerctx = edbcompiler.new_compiler_context(\n        compiler_state=compiler.state,\n        user_schema=s_schema.EMPTY_SCHEMA,\n        global_schema=s_schema.EMPTY_SCHEMA,\n        expected_cardinality_one=False,\n        output_format=edbcompiler.OutputFormat.JSON,\n        bootstrap_mode=True,\n    )\n\n    _, get_databases_sql = edbcompiler.compile_edgeql_script(\n        compilerctx,\n        'SELECT sys::Branch.name',\n    )\n\n    databases = list(sorted(\n        json.loads(\n            await pgconn.sql_fetch_val(get_databases_sql.encode(\"utf-8\")),\n        ),\n        key=lambda dname: edbdef.EDGEDB_TEMPLATE_DB in dname,\n    ))\n\n    _, get_roles_sql = edbcompiler.compile_edgeql_script(\n        compilerctx,\n        '''SELECT sys::Role {\n            name,\n            parents := .member_of.name,\n        }''',\n    )\n\n    roles = json.loads(\n        await pgconn.sql_fetch_val(get_roles_sql.encode(\"utf-8\")),\n    )\n    sorted_roles = list(topological.sort({\n        r['name']: topological.DepGraphEntry(\n            item=r['name'],\n            deps=r['parents'],\n            extra=False,\n        ) for r in roles\n    }))\n\n    return databases, sorted_roles\n"
  },
  {
    "path": "edb_stat_statements/.gitignore",
    "content": "# Generated subdirectories\n/log/\n/results/\n/tmp_check/\n/expected/dml.out\n/expected/level_tracking.out\n/expected/parallel.out\n/expected/utility.out\n/expected/wal.out\n"
  },
  {
    "path": "edb_stat_statements/Makefile",
    "content": "MODULE_big = edb_stat_statements\nOBJS = \\\n\t$(WIN32RES) \\\n\tedb_stat_statements.o\n\nEXTENSION = edb_stat_statements\nDATA = edb_stat_statements--1.0.sql\nPGFILEDESC = \"edb_stat_statements - execution statistics of EdgeDB queries\"\n\nLDFLAGS_SL += $(filter -lm, $(LIBS))\n\nREGRESS = select dml cursors utility level_tracking planning \\\n\tuser_activity wal entry_timestamp privileges \\\n\tparallel cleanup oldextversions\n\nPG_CONFIG ?= pg_config\n\nTAP_TESTS = 1\nPG_MAJOR = $(shell $(PG_CONFIG) --version | grep -oE '[0-9]+' | head -1)\nPG_MAJOR_SPLIT_17 = $(shell test $(PG_MAJOR) -ge 17 && echo 17 || echo 16)\nPG_MAJOR_SPLIT_18 = $(shell test $(PG_MAJOR) -ge 18 && echo 18 || echo 17)\n\nifeq ($(PG_MAJOR_SPLIT_18), 18)\n\tREGRESS += extended\nendif\n\nall:\n\nexpected/dml.out: expected/dml.out.$(PG_MAJOR_SPLIT_18)\n\tcp $< $@\n\nexpected/level_tracking.out: expected/level_tracking.out.$(PG_MAJOR_SPLIT_18)\n\tcp $< $@\n\nexpected/parallel.out: expected/parallel.out.$(PG_MAJOR_SPLIT_18)\n\tcp $< $@\n\nexpected/utility.out: expected/utility.out.$(PG_MAJOR_SPLIT_17)\n\tcp $< $@\n\nexpected/wal.out: expected/wal.out.$(PG_MAJOR_SPLIT_18)\n\tcp $< $@\n\ninstallcheck: \\\n\texpected/dml.out \\\n\texpected/level_tracking.out \\\n\texpected/parallel.out \\\n\texpected/utility.out \\\n\texpected/wal.out\n\nPGXS := $(shell $(PG_CONFIG) --pgxs)\ninclude $(PGXS)\n"
  },
  {
    "path": "edb_stat_statements/edb_stat_statements--1.0.sql",
    "content": "-- complain if script is sourced in psql, rather than via CREATE EXTENSION\n\\echo Use \"CREATE EXTENSION edb_stat_statements\" to load this file. \\quit\n\n-- Register functions.\nCREATE FUNCTION edb_stat_statements_reset(IN userid Oid DEFAULT 0,\n    IN dbids Oid[] DEFAULT '{}',\n    IN queryid bigint DEFAULT 0,\n    IN minmax_only boolean DEFAULT false\n)\nRETURNS timestamp with time zone\nAS 'MODULE_PATHNAME'\nLANGUAGE C STRICT PARALLEL SAFE;\n\nCREATE FUNCTION edb_stat_queryid(IN id uuid)\nRETURNS bigint\nAS 'MODULE_PATHNAME'\nLANGUAGE C STRICT PARALLEL SAFE;\n\nCREATE FUNCTION edb_stat_statements(IN showtext boolean,\n    OUT userid oid,\n    OUT dbid oid,\n    OUT toplevel bool,\n    OUT queryid bigint,\n    OUT query text,\n    OUT extras jsonb,\n    OUT tag text,\n    OUT id uuid,\n    OUT stmt_type int2,\n    OUT plans int8,\n    OUT total_plan_time float8,\n    OUT min_plan_time float8,\n    OUT max_plan_time float8,\n    OUT mean_plan_time float8,\n    OUT stddev_plan_time float8,\n    OUT calls int8,\n    OUT total_exec_time float8,\n    OUT min_exec_time float8,\n    OUT max_exec_time float8,\n    OUT mean_exec_time float8,\n    OUT stddev_exec_time float8,\n    OUT rows int8,\n    OUT shared_blks_hit int8,\n    OUT shared_blks_read int8,\n    OUT shared_blks_dirtied int8,\n    OUT shared_blks_written int8,\n    OUT local_blks_hit int8,\n    OUT local_blks_read int8,\n    OUT local_blks_dirtied int8,\n    OUT local_blks_written int8,\n    OUT temp_blks_read int8,\n    OUT temp_blks_written int8,\n    OUT shared_blk_read_time float8,\n    OUT shared_blk_write_time float8,\n    OUT local_blk_read_time float8,\n    OUT local_blk_write_time float8,\n    OUT temp_blk_read_time float8,\n    OUT temp_blk_write_time float8,\n    OUT wal_records int8,\n    OUT wal_fpi int8,\n    OUT wal_bytes numeric,\n    OUT jit_functions int8,\n    OUT jit_generation_time float8,\n    OUT jit_inlining_count int8,\n    OUT jit_inlining_time float8,\n    OUT jit_optimization_count int8,\n    OUT jit_optimization_time float8,\n    OUT jit_emission_count int8,\n    OUT jit_emission_time float8,\n    OUT jit_deform_count int8,\n    OUT jit_deform_time float8,\n    OUT parallel_workers_to_launch int8,\n    OUT parallel_workers_launched int8,\n    OUT stats_since timestamp with time zone,\n    OUT minmax_stats_since timestamp with time zone\n)\nRETURNS SETOF record\nAS 'MODULE_PATHNAME'\nLANGUAGE C STRICT VOLATILE PARALLEL SAFE;\n\nCREATE FUNCTION edb_stat_statements_info(\n    OUT dealloc bigint,\n    OUT stats_reset timestamp with time zone\n)\nRETURNS record\nAS 'MODULE_PATHNAME'\nLANGUAGE C STRICT VOLATILE PARALLEL SAFE;\n\n-- Register views on the functions for ease of use.\nCREATE VIEW edb_stat_statements AS\n  SELECT * FROM edb_stat_statements(true);\n\nGRANT SELECT ON edb_stat_statements TO PUBLIC;\n\nCREATE VIEW edb_stat_statements_info AS\n  SELECT * FROM edb_stat_statements_info();\n\nGRANT SELECT ON edb_stat_statements_info TO PUBLIC;\n\n-- Don't want this to be available to non-superusers.\nREVOKE ALL ON FUNCTION edb_stat_statements_reset(Oid, Oid[], bigint, boolean) FROM PUBLIC;\n"
  },
  {
    "path": "edb_stat_statements/edb_stat_statements.c",
    "content": "/*-------------------------------------------------------------------------\n *\n * edb_stat_statements.c\n *\t\tTrack statement planning and execution times as well as resource\n *\t\tusage across a whole database cluster.\n *\n * Execution costs are totaled for each distinct source query, and kept in\n * a shared hashtable.  (We track only as many distinct queries as will fit\n * in the designated amount of shared memory.)\n *\n * Starting in Postgres 9.2, this module normalized query entries.  As of\n * Postgres 14, the normalization is done by the core if compute_query_id is\n * enabled, or optionally by third-party modules.\n *\n * To facilitate presenting entries to users, we create \"representative\" query\n * strings in which constants are replaced with parameter symbols ($n), to\n * make it clearer what a normalized entry can represent.  To save on shared\n * memory, and to avoid having to truncate oversized query strings, we store\n * these strings in a temporary external query-texts file.  Offsets into this\n * file are kept in shared memory.\n *\n * Note about locking issues: to create or delete an entry in the shared\n * hashtable, one must hold pgss->lock exclusively.  Modifying any field\n * in an entry except the counters requires the same.  To look up an entry,\n * one must hold the lock shared.  To read or update the counters within\n * an entry, one must hold the lock shared or exclusive (so the entry doesn't\n * disappear!) and also take the entry's mutex spinlock.\n * The shared state variable pgss->extent (the next free spot in the external\n * query-text file) should be accessed only while holding either the\n * pgss->mutex spinlock, or exclusive lock on pgss->lock.  We use the mutex to\n * allow reserving file space while holding only shared lock on pgss->lock.\n * Rewriting the entire external query-text file, eg for garbage collection,\n * requires holding pgss->lock exclusively; this allows individual entries\n * in the file to be read or written while holding only shared lock.\n *\n *\n * Copyright (c) 2008-2024, PostgreSQL Global Development Group\n * Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n *\n *-------------------------------------------------------------------------\n */\n#include \"postgres.h\"\n\n#include <math.h>\n#include <sys/stat.h>\n#include <unistd.h>\n\n#include \"access/parallel.h\"\n#include \"catalog/pg_authid.h\"\n#include \"common/hashfn.h\"\n#include \"common/int.h\"\n#include \"common/jsonapi.h\"\n#include \"executor/instrument.h\"\n#include \"funcapi.h\"\n#include \"jit/jit.h\"\n#include \"mb/pg_wchar.h\"\n#include \"miscadmin.h\"\n#include \"nodes/queryjumble.h\"\n#include \"optimizer/planner.h\"\n#include \"parser/analyze.h\"\n#include \"parser/parsetree.h\"\n#include \"parser/scanner.h\"\n#include \"parser/scansup.h\"\n#include \"pgstat.h\"\n#include \"storage/fd.h\"\n#include \"storage/ipc.h\"\n#include \"storage/lwlock.h\"\n#include \"storage/shmem.h\"\n#include \"storage/spin.h\"\n#include \"tcop/utility.h\"\n#include \"utils/acl.h\"\n#include \"utils/builtins.h\"\n#include \"utils/jsonb.h\"\n#include \"utils/memutils.h\"\n#include \"utils/timestamp.h\"\n#include \"utils/uuid.h\"\n\nPG_MODULE_MAGIC;\n\n#define EDB_STMT_MAGIC_PREFIX \"-- {\"\n\n/* Location of permanent stats file (valid when database is shut down) */\n#define PGSS_DUMP_FILE\tPGSTAT_STAT_PERMANENT_DIRECTORY \"/edb_stat_statements.stat\"\n\n/*\n * Location of external query text file.\n */\n#define PGSS_TEXT_FILE\tPG_STAT_TMP_DIR \"/edbss_query_texts.stat\"\n\n/* Magic number identifying the stats file format */\nstatic const uint32 PGSS_FILE_HEADER = 0x20241125;\n\n/* PostgreSQL major version number, changes in which invalidate all entries */\nstatic const uint32 PGSS_PG_MAJOR_VERSION = PG_VERSION_NUM / 100;\n\n/* XXX: Should USAGE_EXEC reflect execution time and/or buffer usage? */\n#define USAGE_EXEC(duration)\t(1.0)\n#define USAGE_INIT\t\t\t\t(1.0)\t/* including initial planning */\n#define ASSUMED_MEDIAN_INIT\t\t(10.0)\t/* initial assumed median usage */\n#define ASSUMED_LENGTH_INIT\t\t1024\t/* initial assumed mean query length */\n#define USAGE_DECREASE_FACTOR\t(0.99)\t/* decreased every entry_dealloc */\n#define STICKY_DECREASE_FACTOR\t(0.50)\t/* factor for sticky entries */\n#define USAGE_DEALLOC_PERCENT\t5\t/* free this % of entries at once */\n#define IS_STICKY(c)\t((c.calls[PGSS_PLAN] + c.calls[PGSS_EXEC]) == 0)\n\n/*\n * Extension version number, for supporting older extension versions' objects\n */\ntypedef enum pgssVersion\n{\n\tPGSS_V1_0 = 0,\n} pgssVersion;\n\ntypedef enum pgssStoreKind\n{\n\tPGSS_INVALID = -1,\n\n\t/*\n\t * PGSS_PLAN and PGSS_EXEC must be respectively 0 and 1 as they're used to\n\t * reference the underlying values in the arrays in the Counters struct,\n\t * and this order is required in edb_stat_statements_internal().\n\t */\n\tPGSS_PLAN = 0,\n\tPGSS_EXEC,\n} pgssStoreKind;\n\n#define PGSS_NUMKIND (PGSS_EXEC + 1)\n\ntypedef enum EdbStmtType {\n\tEDB_EDGEQL\t= 1,\n\tEDB_SQL\t\t= 2,\n} EdbStmtType;\n\n/*\n * Internal states parsing the info JSON.\n */\ntypedef enum EdbStmtInfoParseState {\n\tEDB_STMT_INFO_PARSE_NOOP\t\t= 0,\n\tEDB_STMT_INFO_PARSE_QUERY\t\t= 1 << 0,\n\tEDB_STMT_INFO_PARSE_ID\t\t\t= 1 << 1,\n\tEDB_STMT_INFO_PARSE_TYPE\t\t= 1 << 2,\n\tEDB_STMT_INFO_PARSE_EXTRAS\t\t= 1 << 3,\n\tEDB_STMT_INFO_PARSE_TAG\t\t\t= 1 << 4,\n} EdbStmtInfoParseState;\n\n/*\n * The info JSON parsing is only considered a success\n * if all the fields listed below are found.\n */\n#define EDB_STMT_INFO_PARSE_REQUIRED \\\n\t(EDB_STMT_INFO_PARSE_QUERY \\\n\t | EDB_STMT_INFO_PARSE_ID \\\n\t | EDB_STMT_INFO_PARSE_TYPE \\\n\t)\n\n/*\n * The result of parsing the info JSON by edbss_extract_stmt_info().\n */\ntypedef struct EdbStmtInfo {\n\tunion {\n\t\tpg_uuid_t uuid;\n\t\tuint64 query_id;\n\t} id;\n\tconst char *query;\n\tint query_len;\n\tconst char *tag;\n\tint tag_len;\n\tEdbStmtType stmt_type;\n\tJsonb *extras;\n} EdbStmtInfo;\n\n/*\n * The custom \"semantic state\" structure for info JSON parsing.\n * This is used internally as the `semstate` pointer of the parser,\n * keeping track of:\n *   - level of nested JSON objects\n *   - known object keys we've found\n *   - current key/state we're parsing\n *   - pointer to the parse result struct\n */\ntypedef struct EdbStmtInfoSemState {\n\tint nested_level;\n\tuint found;\n\tEdbStmtInfoParseState state;\n\tEdbStmtInfo *info;\n} EdbStmtInfoSemState;\n\n/*\n * Hashtable key that defines the identity of a hashtable entry.  We separate\n * queries by user and by database even if they are otherwise identical.\n *\n * If you add a new key to this struct, make sure to teach pgss_store() to\n * zero the padding bytes.  Otherwise, things will break, because pgss_hash is\n * created using HASH_BLOBS, and thus tag_hash is used to hash this.\n\n */\ntypedef struct pgssHashKey\n{\n\tOid\t\t\tuserid;\t\t\t/* user OID */\n\tOid\t\t\tdbid;\t\t\t/* database OID */\n\tuint64\t\tqueryid;\t\t/* query identifier */\n\tbool\t\ttoplevel;\t\t/* query executed at top level */\n} pgssHashKey;\n\n/*\n * The actual stats counters kept within pgssEntry.\n */\ntypedef struct Counters\n{\n\tint64\t\tcalls[PGSS_NUMKIND];\t/* # of times planned/executed */\n\tdouble\t\ttotal_time[PGSS_NUMKIND];\t/* total planning/execution time,\n\t\t\t\t\t\t\t\t\t\t\t * in msec */\n\tdouble\t\tmin_time[PGSS_NUMKIND]; /* minimum planning/execution time in\n\t\t\t\t\t\t\t\t\t\t * msec since min/max reset */\n\tdouble\t\tmax_time[PGSS_NUMKIND]; /* maximum planning/execution time in\n\t\t\t\t\t\t\t\t\t\t * msec since min/max reset */\n\tdouble\t\tmean_time[PGSS_NUMKIND];\t/* mean planning/execution time in\n\t\t\t\t\t\t\t\t\t\t\t * msec */\n\tdouble\t\tsum_var_time[PGSS_NUMKIND]; /* sum of variances in\n\t\t\t\t\t\t\t\t\t\t\t * planning/execution time in msec */\n\tint64\t\trows;\t\t\t/* total # of retrieved or affected rows */\n\tint64\t\tshared_blks_hit;\t/* # of shared buffer hits */\n\tint64\t\tshared_blks_read;\t/* # of shared disk blocks read */\n\tint64\t\tshared_blks_dirtied;\t/* # of shared disk blocks dirtied */\n\tint64\t\tshared_blks_written;\t/* # of shared disk blocks written */\n\tint64\t\tlocal_blks_hit; /* # of local buffer hits */\n\tint64\t\tlocal_blks_read;\t/* # of local disk blocks read */\n\tint64\t\tlocal_blks_dirtied; /* # of local disk blocks dirtied */\n\tint64\t\tlocal_blks_written; /* # of local disk blocks written */\n\tint64\t\ttemp_blks_read; /* # of temp blocks read */\n\tint64\t\ttemp_blks_written;\t/* # of temp blocks written */\n\tdouble\t\tshared_blk_read_time;\t/* time spent reading shared blocks,\n\t\t\t\t\t\t\t\t\t\t * in msec */\n\tdouble\t\tshared_blk_write_time;\t/* time spent writing shared blocks,\n\t\t\t\t\t\t\t\t\t\t * in msec */\n\tdouble\t\tlocal_blk_read_time;\t/* time spent reading local blocks, in\n\t\t\t\t\t\t\t\t\t\t * msec */\n\tdouble\t\tlocal_blk_write_time;\t/* time spent writing local blocks, in\n\t\t\t\t\t\t\t\t\t\t * msec */\n\tdouble\t\ttemp_blk_read_time; /* time spent reading temp blocks, in msec */\n\tdouble\t\ttemp_blk_write_time;\t/* time spent writing temp blocks, in\n\t\t\t\t\t\t\t\t\t\t * msec */\n\tdouble\t\tusage;\t\t\t/* usage factor */\n\tint64\t\twal_records;\t/* # of WAL records generated */\n\tint64\t\twal_fpi;\t\t/* # of WAL full page images generated */\n\tuint64\t\twal_bytes;\t\t/* total amount of WAL generated in bytes */\n\tint64\t\tjit_functions;\t/* total number of JIT functions emitted */\n\tdouble\t\tjit_generation_time;\t/* total time to generate jit code */\n\tint64\t\tjit_inlining_count; /* number of times inlining time has been\n\t\t\t\t\t\t\t\t\t * > 0 */\n\tdouble\t\tjit_deform_time;\t/* total time to deform tuples in jit code */\n\tint64\t\tjit_deform_count;\t/* number of times deform time has been >\n\t\t\t\t\t\t\t\t\t * 0 */\n\n\tdouble\t\tjit_inlining_time;\t/* total time to inline jit code */\n\tint64\t\tjit_optimization_count; /* number of times optimization time\n\t\t\t\t\t\t\t\t\t\t * has been > 0 */\n\tdouble\t\tjit_optimization_time;\t/* total time to optimize jit code */\n\tint64\t\tjit_emission_count; /* number of times emission time has been\n\t\t\t\t\t\t\t\t\t * > 0 */\n\tdouble\t\tjit_emission_time;\t/* total time to emit jit code */\n\tint64\t\tparallel_workers_to_launch; /* # of parallel workers planned\n\t\t\t\t\t\t\t\t\t\t\t * to be launched */\n\tint64\t\tparallel_workers_launched;\t/* # of parallel workers actually\n\t\t\t\t\t\t\t\t\t\t\t * launched */\n} Counters;\n\n/*\n * Global statistics for edb_stat_statements\n */\ntypedef struct pgssGlobalStats\n{\n\tint64\t\tdealloc;\t\t/* # of times entries were deallocated */\n\tTimestampTz stats_reset;\t/* timestamp with all stats reset */\n} pgssGlobalStats;\n\n/*\n * Statistics per statement\n *\n * Note: in event of a failure in garbage collection of the query text file,\n * we reset query_offset to zero and query_len to -1.  This will be seen as\n * an invalid state by qtext_fetch().\n */\ntypedef struct pgssEntry\n{\n\tpgssHashKey key;\t\t\t/* hash key of entry - MUST BE FIRST */\n\tCounters\tcounters;\t\t/* the statistics for this query */\n\tSize\t\tquery_offset;\t/* query text offset in external file */\n\tint\t\t\tquery_len;\t\t/* # of valid bytes in query string, or -1 */\n\tint\t\t\tencoding;\t\t/* query text encoding */\n\tTimestampTz stats_since;\t/* timestamp of entry allocation */\n\tTimestampTz minmax_stats_since; /* timestamp of last min/max values reset */\n\tslock_t\t\tmutex;\t\t\t/* protects the counters only */\n\n\tpg_uuid_t\tid;\t\t\t\t/* Full 16-bytes query ID as UUID */\n\tEdbStmtType\tstmt_type;\t\t/* Type of the EdgeDB query */\n\tint\t\t\textras_len;\t\t/* # of valid bytes in extras jsonb, or 0 */\n\tint\t\t\ttag_len;\t\t/* # of valid bytes in tag string, or 0 */\n} pgssEntry;\n\n/*\n * Global shared state\n */\ntypedef struct pgssSharedState\n{\n\tLWLock\t   *lock;\t\t\t/* protects hashtable search/modification */\n\tdouble\t\tcur_median_usage;\t/* current median usage in hashtable */\n\tSize\t\tmean_query_len; /* current mean entry text length */\n\tslock_t\t\tmutex;\t\t\t/* protects following fields only: */\n\tSize\t\textent;\t\t\t/* current extent of query file */\n\tint\t\t\tn_writers;\t\t/* number of active writers to query file */\n\tint\t\t\tgc_count;\t\t/* query file garbage collection cycle count */\n\tpgssGlobalStats stats;\t\t/* global statistics for pgss */\n} pgssSharedState;\n\n/*---- Local variables ----*/\n\nstatic pg_uuid_t zero_uuid = { 0 };\n\n/* Current nesting depth of planner/ExecutorRun/ProcessUtility calls */\nstatic int\tnesting_level = 0;\n\n/* Saved hook values in case of unload */\nstatic shmem_request_hook_type prev_shmem_request_hook = NULL;\nstatic shmem_startup_hook_type prev_shmem_startup_hook = NULL;\nstatic post_parse_analyze_hook_type prev_post_parse_analyze_hook = NULL;\nstatic planner_hook_type prev_planner_hook = NULL;\nstatic ExecutorStart_hook_type prev_ExecutorStart = NULL;\nstatic ExecutorRun_hook_type prev_ExecutorRun = NULL;\nstatic ExecutorFinish_hook_type prev_ExecutorFinish = NULL;\nstatic ExecutorEnd_hook_type prev_ExecutorEnd = NULL;\nstatic ProcessUtility_hook_type prev_ProcessUtility = NULL;\n\n/* Links to shared memory state */\nstatic pgssSharedState *pgss = NULL;\nstatic HTAB *pgss_hash = NULL;\n\n/*---- GUC variables ----*/\n\ntypedef enum\n{\n\tPGSS_TRACK_NONE,\t\t\t/* track no statements */\n\tPGSS_TRACK_ALL,\t\t\t\t/* all recognized top-level statements */\n\tPGSS_TRACK_DEV,\t\t\t\t/* all top-level statements, including unrecognized ones */\n\tPGSS_TRACK_NESTED,\t\t\t/* all statements, including unrecognized and nested ones */\n}\t\t\tPGSSTrackLevel;\n\nstatic const struct config_enum_entry track_options[] =\n{\n\t{\"None\", PGSS_TRACK_NONE, false},\n\t{\"All\", PGSS_TRACK_ALL, false},\n\t{\"Dev\", PGSS_TRACK_DEV, false},\n\t{\"Dev-Nested\", PGSS_TRACK_NESTED, false},\n\t{NULL, 0, false}\n};\n\nstatic int\tpgss_max = 5000;\t/* max # statements to track */\nstatic int\tpgss_track = PGSS_TRACK_ALL;\t/* tracking level */\nstatic bool pgss_track_utility = true;\t/* whether to track utility commands */\nstatic bool pgss_track_planning = false;\t/* whether to track planning\n\t\t\t\t\t\t\t\t\t\t\t * duration */\nstatic bool pgss_save = true;\t/* whether to save stats across shutdown */\n\n\n#define pgss_enabled(level) \\\n\t(!IsParallelWorker() && \\\n\t(pgss_track == PGSS_TRACK_NESTED || \\\n\t(pgss_track != PGSS_TRACK_NONE && (level) == 0)))\n\n#define edbss_track_unrecognized() \\\n\t(pgss_track == PGSS_TRACK_DEV || pgss_track == PGSS_TRACK_NESTED)\n\n#define record_gc_qtexts() \\\n\tdo { \\\n\t\tSpinLockAcquire(&pgss->mutex); \\\n\t\tpgss->gc_count++; \\\n\t\tSpinLockRelease(&pgss->mutex); \\\n\t} while(0)\n\n/*---- Function declarations ----*/\n\nPG_FUNCTION_INFO_V1(edb_stat_statements_reset);\nPG_FUNCTION_INFO_V1(edb_stat_statements);\nPG_FUNCTION_INFO_V1(edb_stat_statements_info);\nPG_FUNCTION_INFO_V1(edb_stat_queryid);\n\nconst char *\nedbss_extract_info_line(const char *s, int* len);\nEdbStmtInfo *\nedbss_extract_stmt_info(const char* query_str, int query_len);\nstatic inline void\nedbss_free_stmt_info(EdbStmtInfo *info);\nstatic JsonParseErrorType\nedbss_json_struct_start(void *semstate);\nstatic JsonParseErrorType\nedbss_json_struct_end(void *semstate);\nstatic JsonParseErrorType\nedbss_json_ofield_start(void *semstate, char *fname, bool isnull);\nstatic JsonParseErrorType\nedbss_json_scalar(void *semstate, char *token, JsonTokenType tokenType);\n\nstatic void pgss_shmem_request(void);\nstatic void pgss_shmem_startup(void);\nstatic void pgss_shmem_shutdown(int code, Datum arg);\nstatic void pgss_post_parse_analyze(ParseState *pstate, Query *query,\n\t\t\t\t\t\t\t\t\tJumbleState *jstate);\nstatic PlannedStmt *pgss_planner(Query *parse,\n\t\t\t\t\t\t\t\t const char *query_string,\n\t\t\t\t\t\t\t\t int cursorOptions,\n\t\t\t\t\t\t\t\t ParamListInfo boundParams);\nstatic void pgss_ExecutorStart(QueryDesc *queryDesc, int eflags);\nstatic void pgss_ExecutorRun(QueryDesc *queryDesc,\n\t\t\t\t\t\t\t ScanDirection direction,\n\t\t\t\t\t\t\t uint64 count, bool execute_once);\nstatic void pgss_ExecutorFinish(QueryDesc *queryDesc);\nstatic void pgss_ExecutorEnd(QueryDesc *queryDesc);\nstatic void pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,\n\t\t\t\t\t\t\t\tbool readOnlyTree,\n\t\t\t\t\t\t\t\tProcessUtilityContext context, ParamListInfo params,\n\t\t\t\t\t\t\t\tQueryEnvironment *queryEnv,\n\t\t\t\t\t\t\t\tDestReceiver *dest, QueryCompletion *qc);\nstatic void pgss_store(const char *query, uint64 queryId,\n\t\t\t\t\t   int query_location, int query_len,\n\t\t\t\t\t   pgssStoreKind kind,\n\t\t\t\t\t   double total_time, uint64 rows,\n\t\t\t\t\t   const BufferUsage *bufusage,\n\t\t\t\t\t   const WalUsage *walusage,\n\t\t\t\t\t   const struct JitInstrumentation *jitusage,\n\t\t\t\t\t   JumbleState *jstate,\n\t\t\t\t\t   bool edb_extracted,\n\t\t\t\t\t   pg_uuid_t *id,\n\t\t\t\t\t   EdbStmtType stmt_type,\n\t\t\t\t\t   const Jsonb *extras,\n\t\t\t\t\t   const char *tag, int tag_len,\n\t\t\t\t\t   int parallel_workers_to_launch,\n\t\t\t\t\t   int parallel_workers_launched);\nstatic void edb_stat_statements_internal(FunctionCallInfo fcinfo,\n\t\t\t\t\t\t\t\t\t\t pgssVersion api_version,\n\t\t\t\t\t\t\t\t\t\t bool showtext);\nstatic Size pgss_memsize(void);\nstatic pgssEntry *entry_alloc(pgssHashKey *key, Size query_offset, int query_len,\n\t\t\t\t\t\t\t  int encoding, bool sticky, pg_uuid_t *id,\n\t\t\t\t\t\t\t  EdbStmtType stmt_type, int extras_len, int tag_len);\nstatic void entry_dealloc(void);\nstatic bool qtext_store(const char *query, int query_len,\n\t\t\t\t\t\tconst Jsonb *extras, int extras_len,\n\t\t\t\t\t\tconst char *tag, int tag_len,\n\t\t\t\t\t\tSize *query_offset, int *gc_count);\nstatic char *qtext_load_file(Size *buffer_size);\nstatic char *qtext_fetch(Size query_offset, int query_len,\n\t\t\t\t\t\t char *buffer, Size buffer_size);\nstatic bool need_gc_qtexts(void);\nstatic void gc_qtexts(void);\nstatic TimestampTz entry_reset(Oid userid, const Datum *dbids, int dbids_len, uint64 queryid, bool minmax_only);\nstatic char *generate_normalized_query(JumbleState *jstate, const char *query,\n\t\t\t\t\t\t\t\t\t   int query_loc, int *query_len_p);\nstatic void fill_in_constant_lengths(JumbleState *jstate, const char *query,\n\t\t\t\t\t\t\t\t\t int query_loc);\nstatic int\tcomp_location(const void *a, const void *b);\n\n\n/*\n * Module load callback\n */\nvoid\n_PG_init(void)\n{\n\t/*\n\t * In order to create our shared memory area, we have to be loaded via\n\t * shared_preload_libraries.  If not, fall out without hooking into any of\n\t * the main system.  (We don't throw error here because it seems useful to\n\t * allow the edb_stat_statements functions to be created even when the\n\t * module isn't active.  The functions must protect themselves against\n\t * being called then, however.)\n\t */\n\tif (!process_shared_preload_libraries_in_progress)\n\t\treturn;\n\n\t/*\n\t * Inform the postmaster that we want to enable query_id calculation if\n\t * compute_query_id is set to auto.\n\t */\n\tEnableQueryId();\n\n\t/*\n\t * Define (or redefine) custom GUC variables.\n\t */\n\tDefineCustomIntVariable(\"edb_stat_statements.max\",\n\t\t\t\t\t\t\t\"Sets the maximum number of statements tracked by edb_stat_statements.\",\n\t\t\t\t\t\t\tNULL,\n\t\t\t\t\t\t\t&pgss_max,\n\t\t\t\t\t\t\t5000,\n\t\t\t\t\t\t\t100,\n\t\t\t\t\t\t\tINT_MAX / 2,\n\t\t\t\t\t\t\tPGC_POSTMASTER,\n\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\tNULL,\n\t\t\t\t\t\t\tNULL,\n\t\t\t\t\t\t\tNULL);\n\n\tDefineCustomEnumVariable(\"edb_stat_statements.track\",\n\t\t\t\t\t\t\t \"Selects which statements are tracked by edb_stat_statements.\",\n\t\t\t\t\t\t\t NULL,\n\t\t\t\t\t\t\t &pgss_track,\n\t\t\t\t\t\t\t PGSS_TRACK_ALL,\n\t\t\t\t\t\t\t track_options,\n\t\t\t\t\t\t\t PGC_SUSET,\n\t\t\t\t\t\t\t 0,\n\t\t\t\t\t\t\t NULL,\n\t\t\t\t\t\t\t NULL,\n\t\t\t\t\t\t\t NULL);\n\n\tDefineCustomBoolVariable(\"edb_stat_statements.track_utility\",\n\t\t\t\t\t\t\t \"Selects whether utility commands are tracked by edb_stat_statements.\",\n\t\t\t\t\t\t\t NULL,\n\t\t\t\t\t\t\t &pgss_track_utility,\n\t\t\t\t\t\t\t true,\n\t\t\t\t\t\t\t PGC_SUSET,\n\t\t\t\t\t\t\t 0,\n\t\t\t\t\t\t\t NULL,\n\t\t\t\t\t\t\t NULL,\n\t\t\t\t\t\t\t NULL);\n\n\tDefineCustomBoolVariable(\"edb_stat_statements.track_planning\",\n\t\t\t\t\t\t\t \"Selects whether planning duration is tracked by edb_stat_statements.\",\n\t\t\t\t\t\t\t NULL,\n\t\t\t\t\t\t\t &pgss_track_planning,\n\t\t\t\t\t\t\t false,\n\t\t\t\t\t\t\t PGC_SUSET,\n\t\t\t\t\t\t\t 0,\n\t\t\t\t\t\t\t NULL,\n\t\t\t\t\t\t\t NULL,\n\t\t\t\t\t\t\t NULL);\n\n\tDefineCustomBoolVariable(\"edb_stat_statements.save\",\n\t\t\t\t\t\t\t \"Save edb_stat_statements statistics across server shutdowns.\",\n\t\t\t\t\t\t\t NULL,\n\t\t\t\t\t\t\t &pgss_save,\n\t\t\t\t\t\t\t true,\n\t\t\t\t\t\t\t PGC_SIGHUP,\n\t\t\t\t\t\t\t 0,\n\t\t\t\t\t\t\t NULL,\n\t\t\t\t\t\t\t NULL,\n\t\t\t\t\t\t\t NULL);\n\n\tMarkGUCPrefixReserved(\"edb_stat_statements\");\n\n\t/*\n\t * Install hooks.\n\t */\n\tprev_shmem_request_hook = shmem_request_hook;\n\tshmem_request_hook = pgss_shmem_request;\n\tprev_shmem_startup_hook = shmem_startup_hook;\n\tshmem_startup_hook = pgss_shmem_startup;\n\tprev_post_parse_analyze_hook = post_parse_analyze_hook;\n\tpost_parse_analyze_hook = pgss_post_parse_analyze;\n\tprev_planner_hook = planner_hook;\n\tplanner_hook = pgss_planner;\n\tprev_ExecutorStart = ExecutorStart_hook;\n\tExecutorStart_hook = pgss_ExecutorStart;\n\tprev_ExecutorRun = ExecutorRun_hook;\n\tExecutorRun_hook = pgss_ExecutorRun;\n\tprev_ExecutorFinish = ExecutorFinish_hook;\n\tExecutorFinish_hook = pgss_ExecutorFinish;\n\tprev_ExecutorEnd = ExecutorEnd_hook;\n\tExecutorEnd_hook = pgss_ExecutorEnd;\n\tprev_ProcessUtility = ProcessUtility_hook;\n\tProcessUtility_hook = pgss_ProcessUtility;\n}\n\nconst char *\nedbss_extract_info_line(const char *s, int *len) {\n\tint prefix_len = strlen(EDB_STMT_MAGIC_PREFIX);\n\tif (*len > prefix_len && strncmp(s, EDB_STMT_MAGIC_PREFIX, prefix_len) == 0) {\n\t\tconst char *rv = s + 3;  // skip \"-- \"\n\t\tint remaining_len = *len - prefix_len;\n\t\tint rv_len = 0;\n\t\twhile (rv_len < remaining_len && rv[rv_len] != '\\n')\n\t\t\trv_len++;\n\t\tif (rv_len > 0) {\n\t\t\t*len = rv_len;\n\t\t\treturn rv;\n\t\t}\n\t}\n\treturn NULL;\n}\n\n/*\n * Extract EdgeDB query info from the JSON in the leading comments.\n * If success, returns a palloc-ed EdbStmtInfo which must be freed\n * after usage with edbss_free_stmt_info().\n *\n * The query info JSON comments must be at the beginning of the\n * query_str. Each line must start with `-- {` and end with `\\n`,\n * with a single valid JSON string. The JSON string itself must\n * not contain any `\\n`, or it'll be treated as a bad JSON.\n *\n * This function scans over all such lines and records known\n * values progressively. Malformed JSONs may be partially read,\n * this function won't bail just because of that; it'll continue\n * with the next line.  If the same key exists more than once,\n * only the first occurrence is effective, later ones are ignored.\n * This function returns successfully as soon as all required\n * fields (EDB_STMT_INFO_PARSE_REQUIRED) are found AND the current\n * JSON is in good form, ignoring remaining lines. For example:\n *\n *   -- {\"a\": 1}\n *   -- {\"a\": 11, \"d\": 4, \"nested\": {\"b\": 22}}\n *   -- {\"b\": 2, \"unknown\": \"skipped\",\n *   -- {\"c\": 3}\n *   -- {\"e\": 5}\n *   SELECT ....\n *\n * If the required fields are {a, b, c}, while {d, e} are known\n * but not required, the extracted info will be:\n *\n *   {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}\n *\n */\nEdbStmtInfo *\nedbss_extract_stmt_info(const char* query_str, int query_len) {\n\tint info_len = query_len;\n\tconst char *info_str = edbss_extract_info_line(query_str, &info_len);\n\n\tif (info_str) {\n\t\tEdbStmtInfo *info = (EdbStmtInfo *) palloc0(sizeof(EdbStmtInfo));\n\t\tEdbStmtInfoSemState state = {\n\t\t\t\t.info = info,\n\t\t\t\t.state = EDB_STMT_INFO_PARSE_NOOP,\n\t\t};\n\t\tJsonSemAction sem = {\n\t\t\t\t.semstate = (void *) &state,\n\t\t\t\t.object_start = edbss_json_struct_start,\n\t\t\t\t.object_end = edbss_json_struct_end,\n\t\t\t\t.array_start = edbss_json_struct_start,\n\t\t\t\t.array_end = edbss_json_struct_end,\n\t\t\t\t.object_field_start = edbss_json_ofield_start,\n\t\t\t\t.scalar = edbss_json_scalar,\n\t\t};\n\n\t\twhile (info_str) {\n\t\t\tJsonLexContext *lex = makeJsonLexContextCstringLen(\n#if PG_VERSION_NUM >= 170000\n\t\t\t\t\tNULL,\n\t\t\t\t\tinfo_str,\n#else\n\t\t\t\t\t(char *) info_str,  // not actually mutating\n#endif\n\t\t\t\t\tinfo_len,\n\t\t\t\t\tPG_UTF8,\n\t\t\t\t\ttrue);\n\t\t\tJsonParseErrorType parse_rv = pg_parse_json(lex, &sem);\n#if PG_VERSION_NUM >= 170000\n\t\t\tfreeJsonLexContext(lex);\n#else\n\t\t\tpfree(lex);\n#endif\n\n\t\t\tif (parse_rv == JSON_SUCCESS)\n\t\t\t\tif ((state.found & EDB_STMT_INFO_PARSE_REQUIRED) == EDB_STMT_INFO_PARSE_REQUIRED)\n\t\t\t\t\treturn info->id.query_id != UINT64CONST(0) ? info : NULL;\n\n\t\t\tinfo_str += info_len + 1;\n\t\t\tinfo_len = query_len - (int)(info_str - query_str);\n\t\t\tinfo_str = edbss_extract_info_line(info_str, &info_len);\n\t\t\tstate.nested_level = 0;\n\t\t\tstate.state = EDB_STMT_INFO_PARSE_NOOP;\n\t\t}\n\t\tedbss_free_stmt_info(info);\n\t}\n\n\treturn NULL;\n}\n\n/*\n * Frees the given EdbStmtInfo struct as well as\n * its owning sub-fields (query).\n */\nstatic inline void\nedbss_free_stmt_info(EdbStmtInfo *info) {\n\tAssert(info != NULL);\n\tif (info->query != NULL)\n\t\tpfree((void *) info->query);\n\tif (info->tag != NULL)\n\t\tpfree((void *) info->tag);\n\tpfree(info);\n}\n\nstatic JsonParseErrorType\nedbss_json_struct_start(void *semstate) {\n\tEdbStmtInfoSemState *state = (EdbStmtInfoSemState *) semstate;\n\tstate->nested_level += 1;\n\treturn JSON_SUCCESS;\n}\n\nstatic JsonParseErrorType\nedbss_json_struct_end(void *semstate) {\n\tEdbStmtInfoSemState *state = (EdbStmtInfoSemState *) semstate;\n\tstate->nested_level -= 1;\n\treturn JSON_SUCCESS;\n}\n\nstatic JsonParseErrorType\nedbss_json_ofield_start(void *semstate, char *fname, bool isnull) {\n\tEdbStmtInfoSemState *state = (EdbStmtInfoSemState *) semstate;\n\tAssert(fname != NULL);\n\tif (state->nested_level == 1) {\n\t\tif (strcmp(fname, \"query\") == 0) {\n\t\t\tstate->state = EDB_STMT_INFO_PARSE_QUERY;\n\t\t} else if (strcmp(fname, \"id\") == 0) {\n\t\t\tstate->state = EDB_STMT_INFO_PARSE_ID;\n\t\t} else if (strcmp(fname, \"type\") == 0) {\n\t\t\tstate->state = EDB_STMT_INFO_PARSE_TYPE;\n\t\t} else if (strcmp(fname, \"extras\") == 0) {\n\t\t\tstate->state = EDB_STMT_INFO_PARSE_EXTRAS;\n\t\t} else if (strcmp(fname, \"tag\") == 0) {\n\t\t\tstate->state = EDB_STMT_INFO_PARSE_TAG;\n\t\t}\n\t}\n\tpfree(fname);  /* must not use object_field_end */\n\treturn JSON_SUCCESS;\n}\n\nstatic JsonParseErrorType\nedbss_json_scalar(void *semstate, char *token, JsonTokenType tokenType) {\n\tEdbStmtInfoSemState *state = (EdbStmtInfoSemState *) semstate;\n\tAssert(token != NULL);\n\n\tif (state->found & state->state) {\n\t\tpfree(token);\n\t\tstate->state = EDB_STMT_INFO_PARSE_NOOP;\n\t\treturn JSON_SUCCESS;\n\t}\n\n\tswitch (state->state) {\n\t\tcase EDB_STMT_INFO_PARSE_QUERY:\n\t\t\tif (tokenType == JSON_TOKEN_STRING) {\n\t\t\t\tstate->info->query = token;\n\t\t\t\tstate->info->query_len = (int) strlen(token);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tgoto fail;\n\n\t\tcase EDB_STMT_INFO_PARSE_ID:\n\t\t\tif (tokenType == JSON_TOKEN_STRING) {\n\t\t\t\tDatum id_datum = DirectFunctionCall1(uuid_in, CStringGetDatum(token));\n\t\t\t\tpg_uuid_t *id_ptr = DatumGetUUIDP(id_datum);\n\t\t\t\tstate->info->id.uuid = *id_ptr;\n\t\t\t\tpfree(id_ptr);\n\t\t\t\tpfree(token);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tgoto fail;\n\n\t\tcase EDB_STMT_INFO_PARSE_TYPE:\n\t\t\tif (tokenType == JSON_TOKEN_NUMBER) {\n\t\t\t\tchar *endptr;\n\t\t\t\tlong type_val = strtol(token, &endptr, 10);\n\t\t\t\tif (*endptr == '\\0' && type_val != LONG_MAX) {\n\t\t\t\t\tif (type_val == EDB_EDGEQL || type_val == EDB_SQL) {\n\t\t\t\t\t\tstate->info->stmt_type = type_val;\n\t\t\t\t\t\tpfree(token);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tgoto fail;\n\n\t\tcase EDB_STMT_INFO_PARSE_EXTRAS:\n\t\t\tif (tokenType == JSON_TOKEN_STRING) {\n\t\t\t\tDatum extras_jsonb = DirectFunctionCall1(jsonb_in, CStringGetDatum(token));\n\t\t\t\tstate->info->extras = DatumGetJsonbP(extras_jsonb);\n\t\t\t\tpfree(token);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tgoto fail;\n\n\t\tcase EDB_STMT_INFO_PARSE_TAG:\n\t\t\tif (tokenType == JSON_TOKEN_STRING) {\n\t\t\t\tstate->info->tag = token;\n\t\t\t\tstate->info->tag_len = (int) strlen(token);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tgoto fail;\n\n\t\tcase EDB_STMT_INFO_PARSE_NOOP:\n\t\t\tpfree(token);\n\t\t\treturn JSON_SUCCESS;\n\t}\n\tstate->found |= state->state;\n\tstate->state = EDB_STMT_INFO_PARSE_NOOP;\n\treturn JSON_SUCCESS;\n\nfail:\n\tpfree(token);\n\treturn JSON_SEM_ACTION_FAILED;\n}\n\n/*\n * shmem_request hook: request additional shared resources.  We'll allocate or\n * attach to the shared resources in pgss_shmem_startup().\n */\nstatic void\npgss_shmem_request(void)\n{\n\tif (prev_shmem_request_hook)\n\t\tprev_shmem_request_hook();\n\n\tRequestAddinShmemSpace(pgss_memsize());\n\tRequestNamedLWLockTranche(\"edb_stat_statements\", 1);\n}\n\n/*\n * shmem_startup hook: allocate or attach to shared memory,\n * then load any pre-existing statistics from file.\n * Also create and load the query-texts file, which is expected to exist\n * (even if empty) while the module is enabled.\n */\nstatic void\npgss_shmem_startup(void)\n{\n\tbool\t\tfound;\n\tHASHCTL\t\tinfo;\n\tFILE\t   *file = NULL;\n\tFILE\t   *qfile = NULL;\n\tuint32\t\theader;\n\tint32\t\tnum;\n\tint32\t\tpgver;\n\tint32\t\ti;\n\tint\t\t\tbuffer_size;\n\tchar\t   *buffer = NULL;\n\n\tif (prev_shmem_startup_hook)\n\t\tprev_shmem_startup_hook();\n\n\t/* reset in case this is a restart within the postmaster */\n\tpgss = NULL;\n\tpgss_hash = NULL;\n\n\t/*\n\t * Create or attach to the shared memory state, including hash table\n\t */\n\tLWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);\n\n\tpgss = ShmemInitStruct(\"edb_stat_statements\",\n\t\t\t\t\t\t   sizeof(pgssSharedState),\n\t\t\t\t\t\t   &found);\n\n\tif (!found)\n\t{\n\t\t/* First time through ... */\n\t\tpgss->lock = &(GetNamedLWLockTranche(\"edb_stat_statements\"))->lock;\n\t\tpgss->cur_median_usage = ASSUMED_MEDIAN_INIT;\n\t\tpgss->mean_query_len = ASSUMED_LENGTH_INIT;\n\t\tSpinLockInit(&pgss->mutex);\n\t\tpgss->extent = 0;\n\t\tpgss->n_writers = 0;\n\t\tpgss->gc_count = 0;\n\t\tpgss->stats.dealloc = 0;\n\t\tpgss->stats.stats_reset = GetCurrentTimestamp();\n\t}\n\n\tinfo.keysize = sizeof(pgssHashKey);\n\tinfo.entrysize = sizeof(pgssEntry);\n\tpgss_hash = ShmemInitHash(\"edb_stat_statements hash\",\n\t\t\t\t\t\t\t  pgss_max, pgss_max,\n\t\t\t\t\t\t\t  &info,\n\t\t\t\t\t\t\t  HASH_ELEM | HASH_BLOBS);\n\n\tLWLockRelease(AddinShmemInitLock);\n\n\t/*\n\t * If we're in the postmaster (or a standalone backend...), set up a shmem\n\t * exit hook to dump the statistics to disk.\n\t */\n\tif (!IsUnderPostmaster)\n\t\ton_shmem_exit(pgss_shmem_shutdown, (Datum) 0);\n\n\t/*\n\t * Done if some other process already completed our initialization.\n\t */\n\tif (found)\n\t\treturn;\n\n\t/*\n\t * Note: we don't bother with locks here, because there should be no other\n\t * processes running when this code is reached.\n\t */\n\n\t/* Unlink query text file possibly left over from crash */\n\tunlink(PGSS_TEXT_FILE);\n\n\t/* Allocate new query text temp file */\n\tqfile = AllocateFile(PGSS_TEXT_FILE, PG_BINARY_W);\n\tif (qfile == NULL)\n\t\tgoto write_error;\n\n\t/*\n\t * If we were told not to load old statistics, we're done.  (Note we do\n\t * not try to unlink any old dump file in this case.  This seems a bit\n\t * questionable but it's the historical behavior.)\n\t */\n\tif (!pgss_save)\n\t{\n\t\tFreeFile(qfile);\n\t\treturn;\n\t}\n\n\t/*\n\t * Attempt to load old statistics from the dump file.\n\t */\n\tfile = AllocateFile(PGSS_DUMP_FILE, PG_BINARY_R);\n\tif (file == NULL)\n\t{\n\t\tif (errno != ENOENT)\n\t\t\tgoto read_error;\n\t\t/* No existing persisted stats file, so we're done */\n\t\tFreeFile(qfile);\n\t\treturn;\n\t}\n\n\tbuffer_size = 2048;\n\tbuffer = (char *) palloc(buffer_size);\n\n\tif (fread(&header, sizeof(uint32), 1, file) != 1 ||\n\t\tfread(&pgver, sizeof(uint32), 1, file) != 1 ||\n\t\tfread(&num, sizeof(int32), 1, file) != 1)\n\t\tgoto read_error;\n\n\tif (header != PGSS_FILE_HEADER ||\n\t\tpgver != PGSS_PG_MAJOR_VERSION)\n\t\tgoto data_error;\n\n\tfor (i = 0; i < num; i++)\n\t{\n\t\tpgssEntry\ttemp;\n\t\tpgssEntry  *entry;\n\t\tSize\t\tquery_offset;\n\t\tint\t\t\tlen;\n\n\t\tif (fread(&temp, sizeof(pgssEntry), 1, file) != 1)\n\t\t\tgoto read_error;\n\n\t\t/* Encoding is the only field we can easily sanity-check */\n\t\tif (!PG_VALID_BE_ENCODING(temp.encoding))\n\t\t\tgoto data_error;\n\n\t\tlen = temp.query_len + temp.extras_len + temp.tag_len;\n\n\t\t/* Resize buffer as needed */\n\t\tif (len >= buffer_size)\n\t\t{\n\t\t\tbuffer_size = Max(buffer_size * 2, len + 1);\n\t\t\tbuffer = repalloc(buffer, buffer_size);\n\t\t}\n\n\t\tif (fread(buffer, 1, len + 1, file) != len + 1)\n\t\t\tgoto read_error;\n\n\t\t/* Should have a trailing null, but let's make sure */\n\t\tbuffer[len] = '\\0';\n\n\t\t/* Skip loading \"sticky\" entries */\n\t\tif (IS_STICKY(temp.counters))\n\t\t\tcontinue;\n\n\t\t/* Store the query text */\n\t\tquery_offset = pgss->extent;\n\t\tif (fwrite(buffer, 1, len + 1, qfile) != len + 1)\n\t\t\tgoto write_error;\n\t\tpgss->extent += len + 1;\n\n\t\t/* make the hashtable entry (discards old entries if too many) */\n\t\tentry = entry_alloc(&temp.key, query_offset, temp.query_len,\n\t\t\t\t\t\t\ttemp.encoding,\n\t\t\t\t\t\t\tfalse, NULL, 0, temp.extras_len, temp.tag_len);\n\n\t\t/* copy in the actual stats */\n\t\tentry->counters = temp.counters;\n\t\tentry->stats_since = temp.stats_since;\n\t\tentry->minmax_stats_since = temp.minmax_stats_since;\n\t\tentry->id = temp.id;\n\t\tentry->stmt_type = temp.stmt_type;\n\t}\n\n\t/* Read global statistics for edb_stat_statements */\n\tif (fread(&pgss->stats, sizeof(pgssGlobalStats), 1, file) != 1)\n\t\tgoto read_error;\n\n\tpfree(buffer);\n\tFreeFile(file);\n\tFreeFile(qfile);\n\n\t/*\n\t * Remove the persisted stats file so it's not included in\n\t * backups/replication standbys, etc.  A new file will be written on next\n\t * shutdown.\n\t *\n\t * Note: it's okay if the PGSS_TEXT_FILE is included in a basebackup,\n\t * because we remove that file on startup; it acts inversely to\n\t * PGSS_DUMP_FILE, in that it is only supposed to be around when the\n\t * server is running, whereas PGSS_DUMP_FILE is only supposed to be around\n\t * when the server is not running.  Leaving the file creates no danger of\n\t * a newly restored database having a spurious record of execution costs,\n\t * which is what we're really concerned about here.\n\t */\n\tunlink(PGSS_DUMP_FILE);\n\n\treturn;\n\nread_error:\n\tereport(LOG,\n\t\t\t(errcode_for_file_access(),\n\t\t\t errmsg(\"could not read file \\\"%s\\\": %m\",\n\t\t\t\t\tPGSS_DUMP_FILE)));\n\tgoto fail;\ndata_error:\n\tereport(LOG,\n\t\t\t(errcode(ERRCODE_INVALID_PARAMETER_VALUE),\n\t\t\t errmsg(\"ignoring invalid data in file \\\"%s\\\"\",\n\t\t\t\t\tPGSS_DUMP_FILE)));\n\tgoto fail;\nwrite_error:\n\tereport(LOG,\n\t\t\t(errcode_for_file_access(),\n\t\t\t errmsg(\"could not write file \\\"%s\\\": %m\",\n\t\t\t\t\tPGSS_TEXT_FILE)));\nfail:\n\tif (buffer)\n\t\tpfree(buffer);\n\tif (file)\n\t\tFreeFile(file);\n\tif (qfile)\n\t\tFreeFile(qfile);\n\t/* If possible, throw away the bogus file; ignore any error */\n\tunlink(PGSS_DUMP_FILE);\n\n\t/*\n\t * Don't unlink PGSS_TEXT_FILE here; it should always be around while the\n\t * server is running with edb_stat_statements enabled\n\t */\n}\n\n/*\n * shmem_shutdown hook: Dump statistics into file.\n *\n * Note: we don't bother with acquiring lock, because there should be no\n * other processes running when this is called.\n */\nstatic void\npgss_shmem_shutdown(int code, Datum arg)\n{\n\tFILE\t   *file;\n\tchar\t   *qbuffer = NULL;\n\tSize\t\tqbuffer_size = 0;\n\tHASH_SEQ_STATUS hash_seq;\n\tint32\t\tnum_entries;\n\tpgssEntry  *entry;\n\n\t/* Don't try to dump during a crash. */\n\tif (code)\n\t\treturn;\n\n\t/* Safety check ... shouldn't get here unless shmem is set up. */\n\tif (!pgss || !pgss_hash)\n\t\treturn;\n\n\t/* Don't dump if told not to. */\n\tif (!pgss_save)\n\t\treturn;\n\n\tfile = AllocateFile(PGSS_DUMP_FILE \".tmp\", PG_BINARY_W);\n\tif (file == NULL)\n\t\tgoto error;\n\n\tif (fwrite(&PGSS_FILE_HEADER, sizeof(uint32), 1, file) != 1)\n\t\tgoto error;\n\tif (fwrite(&PGSS_PG_MAJOR_VERSION, sizeof(uint32), 1, file) != 1)\n\t\tgoto error;\n\tnum_entries = hash_get_num_entries(pgss_hash);\n\tif (fwrite(&num_entries, sizeof(int32), 1, file) != 1)\n\t\tgoto error;\n\n\tqbuffer = qtext_load_file(&qbuffer_size);\n\tif (qbuffer == NULL)\n\t\tgoto error;\n\n\t/*\n\t * When serializing to disk, we store query texts immediately after their\n\t * entry data.  Any orphaned query texts are thereby excluded.\n\t */\n\thash_seq_init(&hash_seq, pgss_hash);\n\twhile ((entry = hash_seq_search(&hash_seq)) != NULL)\n\t{\n\t\tint\t\t\tlen = entry->query_len + entry->extras_len + entry->tag_len;\n\t\tchar\t   *qstr = qtext_fetch(entry->query_offset, len,\n\t\t\t\t\t\t\t\t\t   qbuffer, qbuffer_size);\n\n\t\tif (qstr == NULL)\n\t\t\tcontinue;\t\t\t/* Ignore any entries with bogus texts */\n\n\t\tif (fwrite(entry, sizeof(pgssEntry), 1, file) != 1 ||\n\t\t\tfwrite(qstr, 1, len + 1, file) != len + 1)\n\t\t{\n\t\t\t/* note: we assume hash_seq_term won't change errno */\n\t\t\thash_seq_term(&hash_seq);\n\t\t\tgoto error;\n\t\t}\n\t}\n\n\t/* Dump global statistics for edb_stat_statements */\n\tif (fwrite(&pgss->stats, sizeof(pgssGlobalStats), 1, file) != 1)\n\t\tgoto error;\n\n\tfree(qbuffer);\n\tqbuffer = NULL;\n\n\tif (FreeFile(file))\n\t{\n\t\tfile = NULL;\n\t\tgoto error;\n\t}\n\n\t/*\n\t * Rename file into place, so we atomically replace any old one.\n\t */\n\t(void) durable_rename(PGSS_DUMP_FILE \".tmp\", PGSS_DUMP_FILE, LOG);\n\n\t/* Unlink query-texts file; it's not needed while shutdown */\n\tunlink(PGSS_TEXT_FILE);\n\n\treturn;\n\nerror:\n\tereport(LOG,\n\t\t\t(errcode_for_file_access(),\n\t\t\t errmsg(\"could not write file \\\"%s\\\": %m\",\n\t\t\t\t\tPGSS_DUMP_FILE \".tmp\")));\n\tfree(qbuffer);\n\tif (file)\n\t\tFreeFile(file);\n\tunlink(PGSS_DUMP_FILE \".tmp\");\n\tunlink(PGSS_TEXT_FILE);\n}\n\n/*\n * Post-parse-analysis hook: mark query with a queryId\n */\nstatic void\npgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate)\n{\n\tEdbStmtInfo *info;\n\tconst char *query_str;\n\tint query_location, query_len;\n\n\tif (prev_post_parse_analyze_hook)\n\t\tprev_post_parse_analyze_hook(pstate, query, jstate);\n\n\t/* Safety check... */\n\tif (!pgss || !pgss_hash || !pgss_enabled(nesting_level))\n\t\treturn;\n\n\t/*\n\t * If it's EXECUTE, clear the queryId so that stats will accumulate for\n\t * the underlying PREPARE.  But don't do this if we're not tracking\n\t * utility statements, to avoid messing up another extension that might be\n\t * tracking them.\n\t */\n\tif (query->utilityStmt)\n\t{\n\t\tif (pgss_track_utility && IsA(query->utilityStmt, ExecuteStmt))\n\t\t{\n\t\t\tquery->queryId = UINT64CONST(0);\n\t\t\treturn;\n\t\t}\n\t}\n\n\t/* Parse EdgeDB query info JSON and overwrite query->queryId */\n\tquery_location = query->stmt_location;\n\tquery_len = query->stmt_len;\n\tquery_str = CleanQuerytext(pstate->p_sourcetext, &query_location, &query_len);\n\tif ((info = edbss_extract_stmt_info(query_str, query_len)) != NULL) {\n\t\tquery->queryId = info->id.query_id;\n\n\t\t/* We immediately create a hash table entry for the query,\n\t\t * so that we don't need to parse the query info JSON later\n\t\t * again for the query with the same queryId.\n\t\t */\n\t\tpgss_store(info->query,\n\t\t\t\t   info->id.query_id,\n\t\t\t\t   0,\n\t\t\t\t   info->query_len,\n\t\t\t\t   PGSS_INVALID,\n\t\t\t\t   0,\n\t\t\t\t   0,\n\t\t\t\t   NULL,\n\t\t\t\t   NULL,\n\t\t\t\t   NULL,\n\t\t\t\t   NULL,\n\t\t\t\t   true,\n\t\t\t\t   &info->id.uuid,\n\t\t\t\t   info->stmt_type,\n\t\t\t\t   info->extras,\n\t\t\t\t   info->tag,\n\t\t\t\t   info->tag_len,\n\t\t\t\t   0,\n\t\t\t\t   0);\n\t\tedbss_free_stmt_info(info);\n\t} else if (!edbss_track_unrecognized()) {\n\t\tquery->queryId = UINT64CONST(0);\n\t} else if (jstate && jstate->clocations_count > 0)\n\t\t/*\n\t\t * If query jumbling were able to identify any ignorable constants, we\n\t\t * immediately create a hash table entry for the query, so that we can\n\t\t * record the normalized form of the query string.  If there were no such\n\t\t * constants, the normalized string would be the same as the query text\n\t\t * anyway, so there's no need for an early entry.\n\t\t */\n\t\tpgss_store(pstate->p_sourcetext,\n\t\t\t\t   query->queryId,\n\t\t\t\t   query->stmt_location,\n\t\t\t\t   query->stmt_len,\n\t\t\t\t   PGSS_INVALID,\n\t\t\t\t   0,\n\t\t\t\t   0,\n\t\t\t\t   NULL,\n\t\t\t\t   NULL,\n\t\t\t\t   NULL,\n\t\t\t\t   jstate,\n\t\t\t\t   true,\n\t\t\t\t   NULL,\n\t\t\t\t   0,\n\t\t\t\t   NULL,\n\t\t\t\t   NULL,\n\t\t\t\t   0,\n\t\t\t\t   0,\n\t\t\t\t   0);\n}\n\n/*\n * Planner hook: forward to regular planner, but measure planning time\n * if needed.\n */\nstatic PlannedStmt *\npgss_planner(Query *parse,\n\t\t\t const char *query_string,\n\t\t\t int cursorOptions,\n\t\t\t ParamListInfo boundParams)\n{\n\tPlannedStmt *result;\n\n\t/*\n\t * We can't process the query if no query_string is provided, as\n\t * pgss_store needs it.  We also ignore query without queryid, as it would\n\t * be treated as a utility statement, which may not be the case.\n\t */\n\tif (pgss_enabled(nesting_level)\n\t\t&& pgss_track_planning && query_string\n\t\t&& parse->queryId != UINT64CONST(0))\n\t{\n\t\tinstr_time\tstart;\n\t\tinstr_time\tduration;\n\t\tBufferUsage bufusage_start,\n\t\t\t\t\tbufusage;\n\t\tWalUsage\twalusage_start,\n\t\t\t\t\twalusage;\n\n\t\t/* We need to track buffer usage as the planner can access them. */\n\t\tbufusage_start = pgBufferUsage;\n\n\t\t/*\n\t\t * Similarly the planner could write some WAL records in some cases\n\t\t * (e.g. setting a hint bit with those being WAL-logged)\n\t\t */\n\t\twalusage_start = pgWalUsage;\n\t\tINSTR_TIME_SET_CURRENT(start);\n\n\t\tnesting_level++;\n\t\tPG_TRY();\n\t\t{\n\t\t\tif (prev_planner_hook)\n\t\t\t\tresult = prev_planner_hook(parse, query_string, cursorOptions,\n\t\t\t\t\t\t\t\t\t\t   boundParams);\n\t\t\telse\n\t\t\t\tresult = standard_planner(parse, query_string, cursorOptions,\n\t\t\t\t\t\t\t\t\t\t  boundParams);\n\t\t}\n\t\tPG_FINALLY();\n\t\t{\n\t\t\tnesting_level--;\n\t\t}\n\t\tPG_END_TRY();\n\n\t\tINSTR_TIME_SET_CURRENT(duration);\n\t\tINSTR_TIME_SUBTRACT(duration, start);\n\n\t\t/* calc differences of buffer counters. */\n\t\tmemset(&bufusage, 0, sizeof(BufferUsage));\n\t\tBufferUsageAccumDiff(&bufusage, &pgBufferUsage, &bufusage_start);\n\n\t\t/* calc differences of WAL counters. */\n\t\tmemset(&walusage, 0, sizeof(WalUsage));\n\t\tWalUsageAccumDiff(&walusage, &pgWalUsage, &walusage_start);\n\n\t\tpgss_store(query_string,\n\t\t\t\t   parse->queryId,\n\t\t\t\t   parse->stmt_location,\n\t\t\t\t   parse->stmt_len,\n\t\t\t\t   PGSS_PLAN,\n\t\t\t\t   INSTR_TIME_GET_MILLISEC(duration),\n\t\t\t\t   0,\n\t\t\t\t   &bufusage,\n\t\t\t\t   &walusage,\n\t\t\t\t   NULL,\n\t\t\t\t   NULL,\n\t\t\t\t   false,\n\t\t\t\t   NULL,\n\t\t\t\t   0,\n\t\t\t\t   NULL,\n\t\t\t\t   NULL,\n\t\t\t\t   0,\n\t\t\t\t   0,\n\t\t\t\t   0);\n\t}\n\telse\n\t{\n\t\t/*\n\t\t * Even though we're not tracking plan time for this statement, we\n\t\t * must still increment the nesting level, to ensure that functions\n\t\t * evaluated during planning are not seen as top-level calls.\n\t\t */\n\t\tnesting_level++;\n\t\tPG_TRY();\n\t\t{\n\t\t\tif (prev_planner_hook)\n\t\t\t\tresult = prev_planner_hook(parse, query_string, cursorOptions,\n\t\t\t\t\t\t\t\t\t\t   boundParams);\n\t\t\telse\n\t\t\t\tresult = standard_planner(parse, query_string, cursorOptions,\n\t\t\t\t\t\t\t\t\t\t  boundParams);\n\t\t}\n\t\tPG_FINALLY();\n\t\t{\n\t\t\tnesting_level--;\n\t\t}\n\t\tPG_END_TRY();\n\t}\n\n\treturn result;\n}\n\n/*\n * ExecutorStart hook: start up tracking if needed\n */\nstatic void\npgss_ExecutorStart(QueryDesc *queryDesc, int eflags)\n{\n\tif (prev_ExecutorStart)\n\t\tprev_ExecutorStart(queryDesc, eflags);\n\telse\n\t\tstandard_ExecutorStart(queryDesc, eflags);\n\n\t/*\n\t * If query has queryId zero, don't track it.  This prevents double\n\t * counting of optimizable statements that are directly contained in\n\t * utility statements.\n\t */\n\tif (pgss_enabled(nesting_level) && queryDesc->plannedstmt->queryId != UINT64CONST(0))\n\t{\n\t\t/*\n\t\t * Set up to track total elapsed time in ExecutorRun.  Make sure the\n\t\t * space is allocated in the per-query context so it will go away at\n\t\t * ExecutorEnd.\n\t\t */\n\t\tif (queryDesc->totaltime == NULL)\n\t\t{\n\t\t\tMemoryContext oldcxt;\n\n\t\t\toldcxt = MemoryContextSwitchTo(queryDesc->estate->es_query_cxt);\n\t\t\tqueryDesc->totaltime = InstrAlloc(1, INSTRUMENT_ALL, false);\n\t\t\tMemoryContextSwitchTo(oldcxt);\n\t\t}\n\t}\n}\n\n/*\n * ExecutorRun hook: all we need do is track nesting depth\n */\nstatic void\npgss_ExecutorRun(QueryDesc *queryDesc, ScanDirection direction, uint64 count,\n\t\t\t\t bool execute_once)\n{\n\tnesting_level++;\n\tPG_TRY();\n\t{\n\t\tif (prev_ExecutorRun)\n\t\t\tprev_ExecutorRun(queryDesc, direction, count, execute_once);\n\t\telse\n\t\t\tstandard_ExecutorRun(queryDesc, direction, count, execute_once);\n\t}\n\tPG_FINALLY();\n\t{\n\t\tnesting_level--;\n\t}\n\tPG_END_TRY();\n}\n\n/*\n * ExecutorFinish hook: all we need do is track nesting depth\n */\nstatic void\npgss_ExecutorFinish(QueryDesc *queryDesc)\n{\n\tnesting_level++;\n\tPG_TRY();\n\t{\n\t\tif (prev_ExecutorFinish)\n\t\t\tprev_ExecutorFinish(queryDesc);\n\t\telse\n\t\t\tstandard_ExecutorFinish(queryDesc);\n\t}\n\tPG_FINALLY();\n\t{\n\t\tnesting_level--;\n\t}\n\tPG_END_TRY();\n}\n\n/*\n * ExecutorEnd hook: store results if needed\n */\nstatic void\npgss_ExecutorEnd(QueryDesc *queryDesc)\n{\n\tuint64\t\tqueryId = queryDesc->plannedstmt->queryId;\n\n\tif (queryId != UINT64CONST(0) && queryDesc->totaltime &&\n\t\tpgss_enabled(nesting_level))\n\t{\n\t\t/*\n\t\t * Make sure stats accumulation is done.  (Note: it's okay if several\n\t\t * levels of hook all do this.)\n\t\t */\n\t\tInstrEndLoop(queryDesc->totaltime);\n\n\t\tpgss_store(queryDesc->sourceText,\n\t\t\t\t   queryId,\n\t\t\t\t   queryDesc->plannedstmt->stmt_location,\n\t\t\t\t   queryDesc->plannedstmt->stmt_len,\n\t\t\t\t   PGSS_EXEC,\n\t\t\t\t   queryDesc->totaltime->total * 1000.0,\t/* convert to msec */\n\t\t\t\t   queryDesc->estate->es_total_processed,\n\t\t\t\t   &queryDesc->totaltime->bufusage,\n\t\t\t\t   &queryDesc->totaltime->walusage,\n\t\t\t\t   queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL,\n\t\t\t\t   NULL,\n\t\t\t\t   false,\n\t\t\t\t   NULL,\n\t\t\t\t   0,\n\t\t\t\t   NULL,\n\t\t\t\t   NULL,\n\t\t\t\t   0,\n#if PG_VERSION_NUM >= 180000\n\t\t\t\t   queryDesc->estate->es_parallel_workers_to_launch,\n\t\t\t\t   queryDesc->estate->es_parallel_workers_launched\n#else\n\t\t\t\t   0,\n\t\t\t\t   0\n#endif\n\t\t);\n\t}\n\n\tif (prev_ExecutorEnd)\n\t\tprev_ExecutorEnd(queryDesc);\n\telse\n\t\tstandard_ExecutorEnd(queryDesc);\n}\n\n/*\n * ProcessUtility hook\n */\nstatic void\npgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString,\n\t\t\t\t\tbool readOnlyTree,\n\t\t\t\t\tProcessUtilityContext context,\n\t\t\t\t\tParamListInfo params, QueryEnvironment *queryEnv,\n\t\t\t\t\tDestReceiver *dest, QueryCompletion *qc)\n{\n\tNode\t   *parsetree = pstmt->utilityStmt;\n\tuint64\t\tsaved_queryId = pstmt->queryId;\n\tint\t\t\tsaved_stmt_location = pstmt->stmt_location;\n\tint\t\t\tsaved_stmt_len = pstmt->stmt_len;\n\tbool\t\tenabled = pgss_track_utility && pgss_enabled(nesting_level);\n\n\t/*\n\t * Force utility statements to get queryId zero.  We do this even in cases\n\t * where the statement contains an optimizable statement for which a\n\t * queryId could be derived (such as EXPLAIN or DECLARE CURSOR).  For such\n\t * cases, runtime control will first go through ProcessUtility and then\n\t * the executor, and we don't want the executor hooks to do anything,\n\t * since we are already measuring the statement's costs at the utility\n\t * level.\n\t *\n\t * Note that this is only done if edb_stat_statements is enabled and\n\t * configured to track utility statements, in the unlikely possibility\n\t * that user configured another extension to handle utility statements\n\t * only.\n\t */\n\tif (enabled)\n\t\tpstmt->queryId = UINT64CONST(0);\n\n\t/*\n\t * If it's an EXECUTE statement, we don't track it and don't increment the\n\t * nesting level.  This allows the cycles to be charged to the underlying\n\t * PREPARE instead (by the Executor hooks), which is much more useful.\n\t *\n\t * We also don't track execution of PREPARE.  If we did, we would get one\n\t * hash table entry for the PREPARE (with hash calculated from the query\n\t * string), and then a different one with the same query string (but hash\n\t * calculated from the query tree) would be used to accumulate costs of\n\t * ensuing EXECUTEs.  This would be confusing.  Since PREPARE doesn't\n\t * actually run the planner (only parse+rewrite), its costs are generally\n\t * pretty negligible and it seems okay to just ignore it.\n\t */\n\tif (enabled &&\n\t\t!IsA(parsetree, ExecuteStmt) &&\n\t\t!IsA(parsetree, PrepareStmt))\n\t{\n\t\tinstr_time\tstart;\n\t\tinstr_time\tduration;\n\t\tuint64\t\trows;\n\t\tBufferUsage bufusage_start,\n\t\t\t\t\tbufusage;\n\t\tWalUsage\twalusage_start,\n\t\t\t\t\twalusage;\n\n\t\tbufusage_start = pgBufferUsage;\n\t\twalusage_start = pgWalUsage;\n\t\tINSTR_TIME_SET_CURRENT(start);\n\n\t\tnesting_level++;\n\t\tPG_TRY();\n\t\t{\n\t\t\tif (prev_ProcessUtility)\n\t\t\t\tprev_ProcessUtility(pstmt, queryString, readOnlyTree,\n\t\t\t\t\t\t\t\t\tcontext, params, queryEnv,\n\t\t\t\t\t\t\t\t\tdest, qc);\n\t\t\telse\n\t\t\t\tstandard_ProcessUtility(pstmt, queryString, readOnlyTree,\n\t\t\t\t\t\t\t\t\t\tcontext, params, queryEnv,\n\t\t\t\t\t\t\t\t\t\tdest, qc);\n\t\t}\n\t\tPG_FINALLY();\n\t\t{\n\t\t\tnesting_level--;\n\t\t}\n\t\tPG_END_TRY();\n\n\t\t/*\n\t\t * CAUTION: do not access the *pstmt data structure again below here.\n\t\t * If it was a ROLLBACK or similar, that data structure may have been\n\t\t * freed.  We must copy everything we still need into local variables,\n\t\t * which we did above.\n\t\t *\n\t\t * For the same reason, we can't risk restoring pstmt->queryId to its\n\t\t * former value, which'd otherwise be a good idea.\n\t\t */\n\n\t\tINSTR_TIME_SET_CURRENT(duration);\n\t\tINSTR_TIME_SUBTRACT(duration, start);\n\n\t\t/*\n\t\t * Track the total number of rows retrieved or affected by the utility\n\t\t * statements of COPY, FETCH, CREATE TABLE AS, CREATE MATERIALIZED\n\t\t * VIEW, REFRESH MATERIALIZED VIEW and SELECT INTO.\n\t\t */\n\t\trows = (qc && (qc->commandTag == CMDTAG_COPY ||\n\t\t\t\t\t   qc->commandTag == CMDTAG_FETCH ||\n\t\t\t\t\t   qc->commandTag == CMDTAG_SELECT ||\n\t\t\t\t\t   qc->commandTag == CMDTAG_REFRESH_MATERIALIZED_VIEW)) ?\n\t\t\tqc->nprocessed : 0;\n\n\t\t/* calc differences of buffer counters. */\n\t\tmemset(&bufusage, 0, sizeof(BufferUsage));\n\t\tBufferUsageAccumDiff(&bufusage, &pgBufferUsage, &bufusage_start);\n\n\t\t/* calc differences of WAL counters. */\n\t\tmemset(&walusage, 0, sizeof(WalUsage));\n\t\tWalUsageAccumDiff(&walusage, &pgWalUsage, &walusage_start);\n\n\t\tpgss_store(queryString,\n\t\t\t\t   saved_queryId,\n\t\t\t\t   saved_stmt_location,\n\t\t\t\t   saved_stmt_len,\n\t\t\t\t   PGSS_EXEC,\n\t\t\t\t   INSTR_TIME_GET_MILLISEC(duration),\n\t\t\t\t   rows,\n\t\t\t\t   &bufusage,\n\t\t\t\t   &walusage,\n\t\t\t\t   NULL,\n\t\t\t\t   NULL,\n\t\t\t\t   false,\n\t\t\t\t   NULL,\n\t\t\t\t   0,\n\t\t\t\t   NULL,\n\t\t\t\t   NULL,\n\t\t\t\t   0,\n\t\t\t\t   0,\n\t\t\t\t   0);\n\t}\n\telse\n\t{\n\t\t/*\n\t\t * Even though we're not tracking execution time for this statement,\n\t\t * we must still increment the nesting level, to ensure that functions\n\t\t * evaluated within it are not seen as top-level calls.  But don't do\n\t\t * so for EXECUTE; that way, when control reaches pgss_planner or\n\t\t * pgss_ExecutorStart, we will treat the costs as top-level if\n\t\t * appropriate.  Likewise, don't bump for PREPARE, so that parse\n\t\t * analysis will treat the statement as top-level if appropriate.\n\t\t *\n\t\t * To be absolutely certain we don't mess up the nesting level,\n\t\t * evaluate the bump_level condition just once.\n\t\t */\n\t\tbool\t\tbump_level =\n\t\t\t!IsA(parsetree, ExecuteStmt) &&\n\t\t\t!IsA(parsetree, PrepareStmt);\n\n\t\tif (bump_level)\n\t\t\tnesting_level++;\n\t\tPG_TRY();\n\t\t{\n\t\t\tif (prev_ProcessUtility)\n\t\t\t\tprev_ProcessUtility(pstmt, queryString, readOnlyTree,\n\t\t\t\t\t\t\t\t\tcontext, params, queryEnv,\n\t\t\t\t\t\t\t\t\tdest, qc);\n\t\t\telse\n\t\t\t\tstandard_ProcessUtility(pstmt, queryString, readOnlyTree,\n\t\t\t\t\t\t\t\t\t\tcontext, params, queryEnv,\n\t\t\t\t\t\t\t\t\t\tdest, qc);\n\t\t}\n\t\tPG_FINALLY();\n\t\t{\n\t\t\tif (bump_level)\n\t\t\t\tnesting_level--;\n\t\t}\n\t\tPG_END_TRY();\n\t}\n}\n\n/*\n * Store some statistics for a statement.\n *\n * If jstate is not NULL then we're trying to create an entry for which\n * we have no statistics as yet; we just want to record the normalized\n * query string.  total_time, rows, bufusage and walusage are ignored in this\n * case.\n *\n * If kind is PGSS_PLAN or PGSS_EXEC, its value is used as the array position\n * for the arrays in the Counters field.\n */\nstatic void\npgss_store(const char *query, uint64 queryId,\n\t\t   int query_location, int query_len,\n\t\t   pgssStoreKind kind,\n\t\t   double total_time, uint64 rows,\n\t\t   const BufferUsage *bufusage,\n\t\t   const WalUsage *walusage,\n\t\t   const struct JitInstrumentation *jitusage,\n\t\t   JumbleState *jstate,\n\t\t   bool edb_extracted,\n\t\t   pg_uuid_t *id,\n\t\t   EdbStmtType stmt_type,\n\t\t   const Jsonb *extras,\n\t\t   const char *tag, int tag_len,\n\t\t   int parallel_workers_to_launch,\n\t\t   int parallel_workers_launched)\n{\n\tpgssHashKey key;\n\tpgssEntry  *entry;\n\tchar\t   *norm_query = NULL;\n\tint\t\t\tencoding = GetDatabaseEncoding();\n\tEdbStmtInfo *info = NULL;\n\n\tAssert(query != NULL);\n\n\t/* Safety check... */\n\tif (!pgss || !pgss_hash)\n\t\treturn;\n\n\t/*\n\t * Nothing to do if compute_query_id isn't enabled and no other module\n\t * computed a query identifier.\n\t */\n\tif (queryId == UINT64CONST(0))\n\t\treturn;\n\n\t/*\n\t * Confine our attention to the relevant part of the string, if the query\n\t * is a portion of a multi-statement source string, and update query\n\t * location and length if needed.\n\t */\n\tquery = CleanQuerytext(query, &query_location, &query_len);\n\n\t/* Set up key for hashtable search */\n\n\t/* clear padding */\n\tmemset(&key, 0, sizeof(pgssHashKey));\n\n\tkey.userid = GetUserId();\n\tkey.dbid = MyDatabaseId;\n\tkey.queryid = queryId;\n\tkey.toplevel = (nesting_level == 0);\n\n\t/* Lookup the hash table entry with shared lock. */\n\tLWLockAcquire(pgss->lock, LW_SHARED);\n\n\tentry = (pgssEntry *) hash_search(pgss_hash, &key, HASH_FIND, NULL);\n\n\t/* Create new entry, if not present */\n\tif (!entry)\n\t{\n\t\tSize\t\tquery_offset;\n\t\tint\t\t\tgc_count;\n\t\tbool\t\tstored;\n\t\tbool\t\tdo_gc;\n\t\tbool\t\tsticky = true;\n\t\tint\t\t\textras_len;\n\n\t\tif (!edb_extracted) {\n\t\t\t/* Try extract from the context of plan/execute.\n\t\t\t * This is usually happening after a stats reset.\n\t\t\t */\n\t\t\tif ((info = edbss_extract_stmt_info(query, query_len)) != NULL) {\n\t\t\t\t/* We should just get the same queryId again\n\t\t\t\t * as we extracted before the reset in post_parse.\n\t\t\t\t */\n\t\t\t\tif (info->id.query_id != queryId)\n\t\t\t\t\tgoto done;\n\t\t\t\tquery = info->query;\n\t\t\t\tquery_len = info->query_len;\n\t\t\t\tid = &info->id.uuid;\n\t\t\t\tstmt_type = info->stmt_type;\n\t\t\t\textras = info->extras;\n\t\t\t\ttag = info->tag;\n\t\t\t\ttag_len = info->tag_len;\n\t\t\t} else if (!edbss_track_unrecognized()) {\n\t\t\t\t/* skip unrecognized statements unless we're told not to */\n\t\t\t\tgoto done;\n\t\t\t} else {\n\t\t\t\tsticky = jstate != NULL;\n\t\t\t}\n\t\t}\n\n\t\t/*\n\t\t * Create a new, normalized query string if caller asked.  We don't\n\t\t * need to hold the lock while doing this work.  (Note: in any case,\n\t\t * it's possible that someone else creates a duplicate hashtable entry\n\t\t * in the interval where we don't hold the lock below.  That case is\n\t\t * handled by entry_alloc.)\n\t\t */\n\t\tif (jstate)\n\t\t{\n\t\t\tLWLockRelease(pgss->lock);\n\t\t\tnorm_query = generate_normalized_query(jstate, query,\n\t\t\t\t\t\t\t\t\t\t\t\t   query_location,\n\t\t\t\t\t\t\t\t\t\t\t\t   &query_len);\n\t\t\tLWLockAcquire(pgss->lock, LW_SHARED);\n\t\t}\n\n\t\textras_len = extras == NULL ? 0 : VARSIZE(JsonbPGetDatum(extras));\n\n\t\t/* Append new query text to file with only shared lock held */\n\t\tstored = qtext_store(norm_query ? norm_query : query, query_len,\n\t\t\t\t\t\t\t extras, extras_len, tag, tag_len,\n\t\t\t\t\t\t\t &query_offset, &gc_count);\n\n\t\t/*\n\t\t * Determine whether we need to garbage collect external query texts\n\t\t * while the shared lock is still held.  This micro-optimization\n\t\t * avoids taking the time to decide this while holding exclusive lock.\n\t\t */\n\t\tdo_gc = need_gc_qtexts();\n\n\t\t/* Need exclusive lock to make a new hashtable entry - promote */\n\t\tLWLockRelease(pgss->lock);\n\t\tLWLockAcquire(pgss->lock, LW_EXCLUSIVE);\n\n\t\t/*\n\t\t * A garbage collection may have occurred while we weren't holding the\n\t\t * lock.  In the unlikely event that this happens, the query text we\n\t\t * stored above will have been garbage collected, so write it again.\n\t\t * This should be infrequent enough that doing it while holding\n\t\t * exclusive lock isn't a performance problem.\n\t\t */\n\t\tif (!stored || pgss->gc_count != gc_count)\n\t\t\tstored = qtext_store(norm_query ? norm_query : query, query_len,\n\t\t\t\t\t\t\t\t extras, extras_len, tag, tag_len,\n\t\t\t\t\t\t\t\t &query_offset, NULL);\n\n\t\t/* If we failed to write to the text file, give up */\n\t\tif (!stored)\n\t\t\tgoto done;\n\n\t\t/* OK to create a new hashtable entry */\n\t\tentry = entry_alloc(&key, query_offset, query_len, encoding,\n\t\t\t\t\t\t\tsticky, id, stmt_type, extras_len, tag_len);\n\n\t\t/* If needed, perform garbage collection while exclusive lock held */\n\t\tif (do_gc)\n\t\t\tgc_qtexts();\n\t}\n\n\t/* Increment the counts, except when jstate is not NULL */\n\tif (!edb_extracted)\n\t{\n\t\tAssert(kind == PGSS_PLAN || kind == PGSS_EXEC);\n\n\t\t/*\n\t\t * Grab the spinlock while updating the counters (see comment about\n\t\t * locking rules at the head of the file)\n\t\t */\n\t\tSpinLockAcquire(&entry->mutex);\n\n\t\t/* \"Unstick\" entry if it was previously sticky */\n\t\tif (IS_STICKY(entry->counters))\n\t\t\tentry->counters.usage = USAGE_INIT;\n\n\t\tentry->counters.calls[kind] += 1;\n\t\tentry->counters.total_time[kind] += total_time;\n\n\t\tif (entry->counters.calls[kind] == 1)\n\t\t{\n\t\t\tentry->counters.min_time[kind] = total_time;\n\t\t\tentry->counters.max_time[kind] = total_time;\n\t\t\tentry->counters.mean_time[kind] = total_time;\n\t\t}\n\t\telse\n\t\t{\n\t\t\t/*\n\t\t\t * Welford's method for accurately computing variance. See\n\t\t\t * <http://www.johndcook.com/blog/standard_deviation/>\n\t\t\t */\n\t\t\tdouble\t\told_mean = entry->counters.mean_time[kind];\n\n\t\t\tentry->counters.mean_time[kind] +=\n\t\t\t\t(total_time - old_mean) / entry->counters.calls[kind];\n\t\t\tentry->counters.sum_var_time[kind] +=\n\t\t\t\t(total_time - old_mean) * (total_time - entry->counters.mean_time[kind]);\n\n\t\t\t/*\n\t\t\t * Calculate min and max time. min = 0 and max = 0 means that the\n\t\t\t * min/max statistics were reset\n\t\t\t */\n\t\t\tif (entry->counters.min_time[kind] == 0\n\t\t\t\t&& entry->counters.max_time[kind] == 0)\n\t\t\t{\n\t\t\t\tentry->counters.min_time[kind] = total_time;\n\t\t\t\tentry->counters.max_time[kind] = total_time;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tif (entry->counters.min_time[kind] > total_time)\n\t\t\t\t\tentry->counters.min_time[kind] = total_time;\n\t\t\t\tif (entry->counters.max_time[kind] < total_time)\n\t\t\t\t\tentry->counters.max_time[kind] = total_time;\n\t\t\t}\n\t\t}\n\t\tentry->counters.rows += rows;\n\t\tentry->counters.shared_blks_hit += bufusage->shared_blks_hit;\n\t\tentry->counters.shared_blks_read += bufusage->shared_blks_read;\n\t\tentry->counters.shared_blks_dirtied += bufusage->shared_blks_dirtied;\n\t\tentry->counters.shared_blks_written += bufusage->shared_blks_written;\n\t\tentry->counters.local_blks_hit += bufusage->local_blks_hit;\n\t\tentry->counters.local_blks_read += bufusage->local_blks_read;\n\t\tentry->counters.local_blks_dirtied += bufusage->local_blks_dirtied;\n\t\tentry->counters.local_blks_written += bufusage->local_blks_written;\n\t\tentry->counters.temp_blks_read += bufusage->temp_blks_read;\n\t\tentry->counters.temp_blks_written += bufusage->temp_blks_written;\n#if PG_VERSION_NUM >= 170000\n\t\tentry->counters.shared_blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage->shared_blk_read_time);\n\t\tentry->counters.shared_blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage->shared_blk_write_time);\n\t\tentry->counters.local_blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage->local_blk_read_time);\n\t\tentry->counters.local_blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage->local_blk_write_time);\n#else\n\t\tentry->counters.shared_blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage->blk_read_time);\n\t\tentry->counters.shared_blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage->blk_write_time);\n#endif\n\t\tentry->counters.temp_blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage->temp_blk_read_time);\n\t\tentry->counters.temp_blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage->temp_blk_write_time);\n\t\tentry->counters.usage += USAGE_EXEC(total_time);\n\t\tentry->counters.wal_records += walusage->wal_records;\n\t\tentry->counters.wal_fpi += walusage->wal_fpi;\n\t\tentry->counters.wal_bytes += walusage->wal_bytes;\n\t\tif (jitusage)\n\t\t{\n\t\t\tentry->counters.jit_functions += jitusage->created_functions;\n\t\t\tentry->counters.jit_generation_time += INSTR_TIME_GET_MILLISEC(jitusage->generation_counter);\n\n#if PG_VERSION_NUM >= 170000\n\t\t\tif (INSTR_TIME_GET_MILLISEC(jitusage->deform_counter))\n\t\t\t\tentry->counters.jit_deform_count++;\n\t\t\tentry->counters.jit_deform_time += INSTR_TIME_GET_MILLISEC(jitusage->deform_counter);\n#endif\n\n\t\t\tif (INSTR_TIME_GET_MILLISEC(jitusage->inlining_counter))\n\t\t\t\tentry->counters.jit_inlining_count++;\n\t\t\tentry->counters.jit_inlining_time += INSTR_TIME_GET_MILLISEC(jitusage->inlining_counter);\n\n\t\t\tif (INSTR_TIME_GET_MILLISEC(jitusage->optimization_counter))\n\t\t\t\tentry->counters.jit_optimization_count++;\n\t\t\tentry->counters.jit_optimization_time += INSTR_TIME_GET_MILLISEC(jitusage->optimization_counter);\n\n\t\t\tif (INSTR_TIME_GET_MILLISEC(jitusage->emission_counter))\n\t\t\t\tentry->counters.jit_emission_count++;\n\t\t\tentry->counters.jit_emission_time += INSTR_TIME_GET_MILLISEC(jitusage->emission_counter);\n\t\t}\n\n\t\t/* parallel worker counters */\n\t\tentry->counters.parallel_workers_to_launch += parallel_workers_to_launch;\n\t\tentry->counters.parallel_workers_launched += parallel_workers_launched;\n\n\t\tSpinLockRelease(&entry->mutex);\n\t}\n\ndone:\n\tLWLockRelease(pgss->lock);\n\n\t/* We postpone this clean-up until we're out of the lock */\n\tif (norm_query)\n\t\tpfree(norm_query);\n\n\tif (info)\n\t\tedbss_free_stmt_info(info);\n}\n\n/*\n * Reset statement statistics corresponding to userid, dbid, and queryid.\n */\n\nDatum\nedb_stat_statements_reset(PG_FUNCTION_ARGS)\n{\n\tOid\t\t\tuserid;\n\tArrayType\t*dbids_array;\n\tDatum\t\t*dbids;\n\tint\t\t\tdbids_len;\n\tuint64\t\tqueryid;\n\tbool\t\tminmax_only;\n\n\tuserid = PG_GETARG_OID(0);\n\tdbids_array = PG_GETARG_ARRAYTYPE_P(1);\n\tqueryid = (uint64) PG_GETARG_INT64(2);\n\tminmax_only = PG_GETARG_BOOL(3);\n\n\tdeconstruct_array_builtin(dbids_array, OIDOID, &dbids, NULL, &dbids_len);\n\n\tPG_RETURN_TIMESTAMPTZ(entry_reset(userid, dbids, dbids_len, queryid, minmax_only));\n}\n\n/* Number of output arguments (columns) for various API versions */\n#define PG_STAT_STATEMENTS_COLS_V1_0\t55\n#define PG_STAT_STATEMENTS_COLS\t\t\t55\t/* maximum of above */\n\n/*\n * Retrieve statement statistics.\n *\n * The SQL API of this function has changed multiple times, and will likely\n * do so again in future.  To support the case where a newer version of this\n * loadable module is being used with an old SQL declaration of the function,\n * we continue to support the older API versions.  For 1.2 and later, the\n * expected API version is identified by embedding it in the C name of the\n * function.  Unfortunately we weren't bright enough to do that for 1.1.\n */\nDatum\nedb_stat_statements(PG_FUNCTION_ARGS)\n{\n\tbool\t\tshowtext = PG_GETARG_BOOL(0);\n\n\tedb_stat_statements_internal(fcinfo, PGSS_V1_0, showtext);\n\n\treturn (Datum) 0;\n}\n\n/* Common code for all versions of edb_stat_statements() */\nstatic void\nedb_stat_statements_internal(FunctionCallInfo fcinfo,\n\t\t\t\t\t\t\t pgssVersion api_version,\n\t\t\t\t\t\t\t bool showtext)\n{\n\tReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;\n\tOid\t\t\tuserid = GetUserId();\n\tbool\t\tis_allowed_role = false;\n\tchar\t   *qbuffer = NULL;\n\tSize\t\tqbuffer_size = 0;\n\tSize\t\textent = 0;\n\tint\t\t\tgc_count = 0;\n\tHASH_SEQ_STATUS hash_seq;\n\tpgssEntry  *entry;\n\n\t/*\n\t * Superusers or roles with the privileges of pg_read_all_stats members\n\t * are allowed\n\t */\n\tis_allowed_role = has_privs_of_role(userid, ROLE_PG_READ_ALL_STATS);\n\n\t/* hash table must exist already */\n\tif (!pgss || !pgss_hash)\n\t\tereport(ERROR,\n\t\t\t\t(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),\n\t\t\t\t errmsg(\"edb_stat_statements must be loaded via \\\"shared_preload_libraries\\\"\")));\n\n\tInitMaterializedSRF(fcinfo, 0);\n\n\t/*\n\t * Check we have the expected number of output arguments.  Aside from\n\t * being a good safety check, we need a kluge here to detect API version\n\t * 1.1, which was wedged into the code in an ill-considered way.\n\t */\n\tswitch (rsinfo->setDesc->natts)\n\t{\n\t\tcase PG_STAT_STATEMENTS_COLS_V1_0:\n\t\t\tif (api_version != PGSS_V1_0)\n\t\t\t\telog(ERROR, \"incorrect number of output arguments\");\n\t\t\tbreak;\n\t\tdefault:\n\t\t\telog(ERROR, \"incorrect number of output arguments\");\n\t}\n\n\t/*\n\t * We'd like to load the query text file (if needed) while not holding any\n\t * lock on pgss->lock.  In the worst case we'll have to do this again\n\t * after we have the lock, but it's unlikely enough to make this a win\n\t * despite occasional duplicated work.  We need to reload if anybody\n\t * writes to the file (either a retail qtext_store(), or a garbage\n\t * collection) between this point and where we've gotten shared lock.  If\n\t * a qtext_store is actually in progress when we look, we might as well\n\t * skip the speculative load entirely.\n\t */\n\tif (showtext)\n\t{\n\t\tint\t\t\tn_writers;\n\n\t\t/* Take the mutex so we can examine variables */\n\t\tSpinLockAcquire(&pgss->mutex);\n\t\textent = pgss->extent;\n\t\tn_writers = pgss->n_writers;\n\t\tgc_count = pgss->gc_count;\n\t\tSpinLockRelease(&pgss->mutex);\n\n\t\t/* No point in loading file now if there are active writers */\n\t\tif (n_writers == 0)\n\t\t\tqbuffer = qtext_load_file(&qbuffer_size);\n\t}\n\n\t/*\n\t * Get shared lock, load or reload the query text file if we must, and\n\t * iterate over the hashtable entries.\n\t *\n\t * With a large hash table, we might be holding the lock rather longer\n\t * than one could wish.  However, this only blocks creation of new hash\n\t * table entries, and the larger the hash table the less likely that is to\n\t * be needed.  So we can hope this is okay.  Perhaps someday we'll decide\n\t * we need to partition the hash table to limit the time spent holding any\n\t * one lock.\n\t */\n\tLWLockAcquire(pgss->lock, LW_SHARED);\n\n\tif (showtext)\n\t{\n\t\t/*\n\t\t * Here it is safe to examine extent and gc_count without taking the\n\t\t * mutex.  Note that although other processes might change\n\t\t * pgss->extent just after we look at it, the strings they then write\n\t\t * into the file cannot yet be referenced in the hashtable, so we\n\t\t * don't care whether we see them or not.\n\t\t *\n\t\t * If qtext_load_file fails, we just press on; we'll return NULL for\n\t\t * every query text.\n\t\t */\n\t\tif (qbuffer == NULL ||\n\t\t\tpgss->extent != extent ||\n\t\t\tpgss->gc_count != gc_count)\n\t\t{\n\t\t\tfree(qbuffer);\n\t\t\tqbuffer = qtext_load_file(&qbuffer_size);\n\t\t}\n\t}\n\n\thash_seq_init(&hash_seq, pgss_hash);\n\twhile ((entry = hash_seq_search(&hash_seq)) != NULL)\n\t{\n\t\tDatum\t\tvalues[PG_STAT_STATEMENTS_COLS];\n\t\tbool\t\tnulls[PG_STAT_STATEMENTS_COLS];\n\t\tint\t\t\ti = 0;\n\t\tCounters\ttmp;\n\t\tdouble\t\tstddev;\n\t\tint64\t\tqueryid = entry->key.queryid;\n\t\tTimestampTz stats_since;\n\t\tTimestampTz minmax_stats_since;\n\n\t\tmemset(values, 0, sizeof(values));\n\t\tmemset(nulls, 0, sizeof(nulls));\n\n\t\tvalues[i++] = ObjectIdGetDatum(entry->key.userid);\n\t\tvalues[i++] = ObjectIdGetDatum(entry->key.dbid);\n\t\tvalues[i++] = BoolGetDatum(entry->key.toplevel);\n\n\t\tif (is_allowed_role || entry->key.userid == userid)\n\t\t{\n\t\t\tvalues[i++] = Int64GetDatumFast(queryid);\n\n\t\t\tif (showtext)\n\t\t\t{\n\t\t\t\tchar\t   *qstr = qtext_fetch(entry->query_offset,\n\t\t\t\t\t\t\t\t\t\t\t   entry->query_len + entry->extras_len + entry->tag_len,\n\t\t\t\t\t\t\t\t\t\t\t   qbuffer,\n\t\t\t\t\t\t\t\t\t\t\t   qbuffer_size);\n\n\t\t\t\tif (qstr)\n\t\t\t\t{\n\t\t\t\t\tchar\t   *enc;\n\n\t\t\t\t\tenc = pg_any_to_server(qstr + entry->extras_len + entry->tag_len,\n\t\t\t\t\t\t\t\t\t\t   entry->query_len,\n\t\t\t\t\t\t\t\t\t\t   entry->encoding);\n\n\t\t\t\t\tvalues[i++] = CStringGetTextDatum(enc);\n\n\t\t\t\t\t// The \"extras\" Jsonb varlena datum\n\t\t\t\t\tif (entry->extras_len > 0)\n\t\t\t\t\t\tvalues[i++] = PointerGetDatum(qstr + entry->tag_len);\n\t\t\t\t\telse\n\t\t\t\t\t\tnulls[i++] = true;\n\n\t\t\t\t\t// The \"tag\" text varlena datum\n\t\t\t\t\tif (entry->tag_len > 0)\n\t\t\t\t\t\tvalues[i++] = PointerGetDatum(cstring_to_text_with_len(qstr, entry->tag_len));\n\t\t\t\t\telse\n\t\t\t\t\t\tnulls[i++] = true;\n\n\t\t\t\t\tif (enc != qstr + entry->extras_len + entry->tag_len)\n\t\t\t\t\t\tpfree(enc);\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t/* Just return a null if we fail to find the text */\n\t\t\t\t\tnulls[i++] = true;\n\n\t\t\t\t\t/* null extras */\n\t\t\t\t\tnulls[i++] = true;\n\n\t\t\t\t\t/* null tag */\n\t\t\t\t\tnulls[i++] = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\t/* Query text not requested */\n\t\t\t\tnulls[i++] = true;\n\n\t\t\t\t/* null extras */\n\t\t\t\tnulls[i++] = true;\n\n\t\t\t\t/* always show tag */\n\t\t\t\tif (entry->tag_len > 0 && qbuffer != NULL && entry->query_offset + entry->tag_len < qbuffer_size) {\n\t\t\t\t\tvalues[i++] = PointerGetDatum(cstring_to_text_with_len(qbuffer + entry->query_offset, entry->tag_len));\n\t\t\t\t} else {\n\t\t\t\t\tnulls[i++] = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\t/* Don't show queryid */\n\t\t\tnulls[i++] = true;\n\n\t\t\t/*\n\t\t\t * Don't show query text, but hint as to the reason for not doing\n\t\t\t * so if it was requested\n\t\t\t */\n\t\t\tif (showtext)\n\t\t\t\tvalues[i++] = CStringGetTextDatum(\"<insufficient privilege>\");\n\t\t\telse\n\t\t\t\tnulls[i++] = true;\n\n\t\t\t/* null extras */\n\t\t\tnulls[i++] = true;\n\n\t\t\t/* always show tag */\n\t\t\tif (entry->tag_len > 0 && qbuffer != NULL && entry->query_offset + entry->tag_len < qbuffer_size) {\n\t\t\t\tvalues[i++] = PointerGetDatum(cstring_to_text_with_len(qbuffer + entry->query_offset, entry->tag_len));\n\t\t\t} else {\n\t\t\t\tnulls[i++] = true;\n\t\t\t}\n\t\t}\n\n\t\tif (memcmp(&entry->id, &zero_uuid, sizeof(zero_uuid)) == 0)\n\t\t\tnulls[i++] = true;\n\t\telse\n\t\t\tvalues[i++] = UUIDPGetDatum(&entry->id);\n\n\t\tif (entry->stmt_type == 0)\n\t\t\tnulls[i++] = true;\n\t\telse\n\t\t\tvalues[i++] = Int16GetDatum(entry->stmt_type);\n\n\t\t/* copy counters to a local variable to keep locking time short */\n\t\tSpinLockAcquire(&entry->mutex);\n\t\ttmp = entry->counters;\n\t\tstats_since = entry->stats_since;\n\t\tminmax_stats_since = entry->minmax_stats_since;\n\t\tSpinLockRelease(&entry->mutex);\n\n\t\t/* Skip entry if unexecuted (ie, it's a pending \"sticky\" entry) */\n\t\tif (IS_STICKY(tmp))\n\t\t\tcontinue;\n\n\t\t/* Note that we rely on PGSS_PLAN being 0 and PGSS_EXEC being 1. */\n\t\tfor (int kind = 0; kind < PGSS_NUMKIND; kind++)\n\t\t{\n\t\t\tvalues[i++] = Int64GetDatumFast(tmp.calls[kind]);\n\t\t\tvalues[i++] = Float8GetDatumFast(tmp.total_time[kind]);\n\t\t\tvalues[i++] = Float8GetDatumFast(tmp.min_time[kind]);\n\t\t\tvalues[i++] = Float8GetDatumFast(tmp.max_time[kind]);\n\t\t\tvalues[i++] = Float8GetDatumFast(tmp.mean_time[kind]);\n\n\t\t\t/*\n\t\t\t * Note we are calculating the population variance here, not\n\t\t\t * the sample variance, as we have data for the whole\n\t\t\t * population, so Bessel's correction is not used, and we\n\t\t\t * don't divide by tmp.calls - 1.\n\t\t\t */\n\t\t\tif (tmp.calls[kind] > 1)\n\t\t\t\tstddev = sqrt(tmp.sum_var_time[kind] / tmp.calls[kind]);\n\t\t\telse\n\t\t\t\tstddev = 0.0;\n\t\t\tvalues[i++] = Float8GetDatumFast(stddev);\n\t\t}\n\t\tvalues[i++] = Int64GetDatumFast(tmp.rows);\n\t\tvalues[i++] = Int64GetDatumFast(tmp.shared_blks_hit);\n\t\tvalues[i++] = Int64GetDatumFast(tmp.shared_blks_read);\n\t\tvalues[i++] = Int64GetDatumFast(tmp.shared_blks_dirtied);\n\t\tvalues[i++] = Int64GetDatumFast(tmp.shared_blks_written);\n\t\tvalues[i++] = Int64GetDatumFast(tmp.local_blks_hit);\n\t\tvalues[i++] = Int64GetDatumFast(tmp.local_blks_read);\n\t\tvalues[i++] = Int64GetDatumFast(tmp.local_blks_dirtied);\n\t\tvalues[i++] = Int64GetDatumFast(tmp.local_blks_written);\n\t\tvalues[i++] = Int64GetDatumFast(tmp.temp_blks_read);\n\t\tvalues[i++] = Int64GetDatumFast(tmp.temp_blks_written);\n\t\tvalues[i++] = Float8GetDatumFast(tmp.shared_blk_read_time);\n\t\tvalues[i++] = Float8GetDatumFast(tmp.shared_blk_write_time);\n\t\tvalues[i++] = Float8GetDatumFast(tmp.local_blk_read_time);\n\t\tvalues[i++] = Float8GetDatumFast(tmp.local_blk_write_time);\n\t\tvalues[i++] = Float8GetDatumFast(tmp.temp_blk_read_time);\n\t\tvalues[i++] = Float8GetDatumFast(tmp.temp_blk_write_time);\n\t\t{\n\t\t\tchar\t\tbuf[256];\n\t\t\tDatum\t\twal_bytes;\n\n\t\t\tvalues[i++] = Int64GetDatumFast(tmp.wal_records);\n\t\t\tvalues[i++] = Int64GetDatumFast(tmp.wal_fpi);\n\n\t\t\tsnprintf(buf, sizeof buf, UINT64_FORMAT, tmp.wal_bytes);\n\n\t\t\t/* Convert to numeric. */\n\t\t\twal_bytes = DirectFunctionCall3(numeric_in,\n\t\t\t\t\t\t\t\t\t\t\tCStringGetDatum(buf),\n\t\t\t\t\t\t\t\t\t\t\tObjectIdGetDatum(0),\n\t\t\t\t\t\t\t\t\t\t\tInt32GetDatum(-1));\n\t\t\tvalues[i++] = wal_bytes;\n\t\t}\n\t\tvalues[i++] = Int64GetDatumFast(tmp.jit_functions);\n\t\tvalues[i++] = Float8GetDatumFast(tmp.jit_generation_time);\n\t\tvalues[i++] = Int64GetDatumFast(tmp.jit_inlining_count);\n\t\tvalues[i++] = Float8GetDatumFast(tmp.jit_inlining_time);\n\t\tvalues[i++] = Int64GetDatumFast(tmp.jit_optimization_count);\n\t\tvalues[i++] = Float8GetDatumFast(tmp.jit_optimization_time);\n\t\tvalues[i++] = Int64GetDatumFast(tmp.jit_emission_count);\n\t\tvalues[i++] = Float8GetDatumFast(tmp.jit_emission_time);\n\t\tvalues[i++] = Int64GetDatumFast(tmp.jit_deform_count);\n\t\tvalues[i++] = Float8GetDatumFast(tmp.jit_deform_time);\n\t\tvalues[i++] = Int64GetDatumFast(tmp.parallel_workers_to_launch);\n\t\tvalues[i++] = Int64GetDatumFast(tmp.parallel_workers_launched);\n\t\tvalues[i++] = TimestampTzGetDatum(stats_since);\n\t\tvalues[i++] = TimestampTzGetDatum(minmax_stats_since);\n\n\t\tAssert(i == (api_version == PGSS_V1_0 ? PG_STAT_STATEMENTS_COLS_V1_0 :\n\t\t\t\t\t -1 /* fail if you forget to update this assert */ ));\n\n\t\ttuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);\n\t}\n\n\tLWLockRelease(pgss->lock);\n\n\tfree(qbuffer);\n}\n\n/* Number of output arguments (columns) for edb_stat_statements_info */\n#define PG_STAT_STATEMENTS_INFO_COLS\t2\n\n/*\n * Return statistics of edb_stat_statements.\n */\nDatum\nedb_stat_statements_info(PG_FUNCTION_ARGS)\n{\n\tpgssGlobalStats stats;\n\tTupleDesc\ttupdesc;\n\tDatum\t\tvalues[PG_STAT_STATEMENTS_INFO_COLS] = {0};\n\tbool\t\tnulls[PG_STAT_STATEMENTS_INFO_COLS] = {0};\n\n\tif (!pgss || !pgss_hash)\n\t\tereport(ERROR,\n\t\t\t\t(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),\n\t\t\t\t errmsg(\"edb_stat_statements must be loaded via \\\"shared_preload_libraries\\\"\")));\n\n\t/* Build a tuple descriptor for our result type */\n\tif (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)\n\t\telog(ERROR, \"return type must be a row type\");\n\n\t/* Read global statistics for edb_stat_statements */\n\tSpinLockAcquire(&pgss->mutex);\n\tstats = pgss->stats;\n\tSpinLockRelease(&pgss->mutex);\n\n\tvalues[0] = Int64GetDatum(stats.dealloc);\n\tvalues[1] = TimestampTzGetDatum(stats.stats_reset);\n\n\tPG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));\n}\n\n/*\n * Convert uuid to bigint as queryid.\n */\nDatum\nedb_stat_queryid(PG_FUNCTION_ARGS) {\n\tunion {\n\t\tpg_uuid_t uuid;\n\t\tuint64 id;\n\t} id;\n\tid.uuid = *PG_GETARG_UUID_P(0);\n\treturn UInt64GetDatum(id.id);\n}\n\n/*\n * Estimate shared memory space needed.\n */\nstatic Size\npgss_memsize(void)\n{\n\tSize\t\tsize;\n\n\tsize = MAXALIGN(sizeof(pgssSharedState));\n\tsize = add_size(size, hash_estimate_size(pgss_max, sizeof(pgssEntry)));\n\n\treturn size;\n}\n\n/*\n * Allocate a new hashtable entry.\n * caller must hold an exclusive lock on pgss->lock\n *\n * \"query\" need not be null-terminated; we rely on query_len instead\n *\n * If \"sticky\" is true, make the new entry artificially sticky so that it will\n * probably still be there when the query finishes execution.  We do this by\n * giving it a median usage value rather than the normal value.  (Strictly\n * speaking, query strings are normalized on a best effort basis, though it\n * would be difficult to demonstrate this even under artificial conditions.)\n *\n * Note: despite needing exclusive lock, it's not an error for the target\n * entry to already exist.  This is because pgss_store releases and\n * reacquires lock after failing to find a match; so someone else could\n * have made the entry while we waited to get exclusive lock.\n */\nstatic pgssEntry *\nentry_alloc(pgssHashKey *key, Size query_offset, int query_len, int encoding,\n\t\t\tbool sticky, pg_uuid_t *id, EdbStmtType stmt_type, int extras_len, int tag_len)\n{\n\tpgssEntry  *entry;\n\tbool\t\tfound;\n\n\t/* Make space if needed */\n\twhile (hash_get_num_entries(pgss_hash) >= pgss_max)\n\t\tentry_dealloc();\n\n\t/* Find or create an entry with desired hash code */\n\tentry = (pgssEntry *) hash_search(pgss_hash, key, HASH_ENTER, &found);\n\n\tif (!found)\n\t{\n\t\t/* New entry, initialize it */\n\n\t\t/* reset the statistics */\n\t\tmemset(&entry->counters, 0, sizeof(Counters));\n\t\t/* set the appropriate initial usage count */\n\t\tentry->counters.usage = sticky ? pgss->cur_median_usage : USAGE_INIT;\n\t\t/* re-initialize the mutex each time ... we assume no one using it */\n\t\tSpinLockInit(&entry->mutex);\n\t\t/* ... and don't forget the query text metadata */\n\t\tAssert(query_len >= 0);\n\t\tentry->query_offset = query_offset;\n\t\tentry->query_len = query_len;\n\t\tentry->encoding = encoding;\n\t\tentry->stats_since = GetCurrentTimestamp();\n\t\tentry->minmax_stats_since = entry->stats_since;\n\t\tif (id != NULL)\n\t\t\tentry->id = *id;\n\t\tentry->stmt_type = stmt_type;\n\t\tentry->extras_len = extras_len;\n\t\tentry->tag_len = tag_len;\n\t}\n\n\treturn entry;\n}\n\n/*\n * qsort comparator for sorting into increasing usage order\n */\nstatic int\nentry_cmp(const void *lhs, const void *rhs)\n{\n\tdouble\t\tl_usage = (*(pgssEntry *const *) lhs)->counters.usage;\n\tdouble\t\tr_usage = (*(pgssEntry *const *) rhs)->counters.usage;\n\n\tif (l_usage < r_usage)\n\t\treturn -1;\n\telse if (l_usage > r_usage)\n\t\treturn +1;\n\telse\n\t\treturn 0;\n}\n\n/*\n * Deallocate least-used entries.\n *\n * Caller must hold an exclusive lock on pgss->lock.\n */\nstatic void\nentry_dealloc(void)\n{\n\tHASH_SEQ_STATUS hash_seq;\n\tpgssEntry **entries;\n\tpgssEntry  *entry;\n\tint\t\t\tnvictims;\n\tint\t\t\ti;\n\tSize\t\ttottextlen;\n\tint\t\t\tnvalidtexts;\n\n\t/*\n\t * Sort entries by usage and deallocate USAGE_DEALLOC_PERCENT of them.\n\t * While we're scanning the table, apply the decay factor to the usage\n\t * values, and update the mean query length.\n\t *\n\t * Note that the mean query length is almost immediately obsolete, since\n\t * we compute it before not after discarding the least-used entries.\n\t * Hopefully, that doesn't affect the mean too much; it doesn't seem worth\n\t * making two passes to get a more current result.  Likewise, the new\n\t * cur_median_usage includes the entries we're about to zap.\n\t */\n\n\tentries = palloc(hash_get_num_entries(pgss_hash) * sizeof(pgssEntry *));\n\n\ti = 0;\n\ttottextlen = 0;\n\tnvalidtexts = 0;\n\n\thash_seq_init(&hash_seq, pgss_hash);\n\twhile ((entry = hash_seq_search(&hash_seq)) != NULL)\n\t{\n\t\tentries[i++] = entry;\n\t\t/* \"Sticky\" entries get a different usage decay rate. */\n\t\tif (IS_STICKY(entry->counters))\n\t\t\tentry->counters.usage *= STICKY_DECREASE_FACTOR;\n\t\telse\n\t\t\tentry->counters.usage *= USAGE_DECREASE_FACTOR;\n\t\t/* In the mean length computation, ignore dropped texts. */\n\t\tif (entry->query_len >= 0)\n\t\t{\n\t\t\ttottextlen += entry->query_len + 1;\n\t\t\tnvalidtexts++;\n\t\t}\n\t}\n\n\t/* Sort into increasing order by usage */\n\tqsort(entries, i, sizeof(pgssEntry *), entry_cmp);\n\n\t/* Record the (approximate) median usage */\n\tif (i > 0)\n\t\tpgss->cur_median_usage = entries[i / 2]->counters.usage;\n\t/* Record the mean query length */\n\tif (nvalidtexts > 0)\n\t\tpgss->mean_query_len = tottextlen / nvalidtexts;\n\telse\n\t\tpgss->mean_query_len = ASSUMED_LENGTH_INIT;\n\n\t/* Now zap an appropriate fraction of lowest-usage entries */\n\tnvictims = Max(10, i * USAGE_DEALLOC_PERCENT / 100);\n\tnvictims = Min(nvictims, i);\n\n\tfor (i = 0; i < nvictims; i++)\n\t{\n\t\thash_search(pgss_hash, &entries[i]->key, HASH_REMOVE, NULL);\n\t}\n\n\tpfree(entries);\n\n\t/* Increment the number of times entries are deallocated */\n\tSpinLockAcquire(&pgss->mutex);\n\tpgss->stats.dealloc += 1;\n\tSpinLockRelease(&pgss->mutex);\n}\n\n/*\n * Given a query string (not necessarily null-terminated), allocate a new\n * entry in the external query text file and store the string there.\n *\n * If successful, returns true, and stores the new entry's offset in the file\n * into *query_offset.  Also, if gc_count isn't NULL, *gc_count is set to the\n * number of garbage collections that have occurred so far.\n *\n * On failure, returns false.\n *\n * At least a shared lock on pgss->lock must be held by the caller, so as\n * to prevent a concurrent garbage collection.  Share-lock-holding callers\n * should pass a gc_count pointer to obtain the number of garbage collections,\n * so that they can recheck the count after obtaining exclusive lock to\n * detect whether a garbage collection occurred (and removed this entry).\n */\nstatic bool\nqtext_store(const char *query, int query_len,\n\t\t\tconst Jsonb *extras, int extras_len,\n\t\t\tconst char *tag, int tag_len,\n\t\t\tSize *query_offset, int *gc_count)\n{\n\tSize\t\toff;\n\tint\t\t\tfd;\n\n\t/*\n\t * We use a spinlock to protect extent/n_writers/gc_count, so that\n\t * multiple processes may execute this function concurrently.\n\t */\n\tSpinLockAcquire(&pgss->mutex);\n\toff = pgss->extent;\n\tpgss->extent += query_len + extras_len + tag_len + 1;\n\tpgss->n_writers++;\n\tif (gc_count)\n\t\t*gc_count = pgss->gc_count;\n\tSpinLockRelease(&pgss->mutex);\n\n\t*query_offset = off;\n\n\t/*\n\t * Don't allow the file to grow larger than what qtext_load_file can\n\t * (theoretically) handle.  This has been seen to be reachable on 32-bit\n\t * platforms.\n\t */\n\tif (unlikely(query_len + extras_len + tag_len >= MaxAllocHugeSize - off))\n\t{\n\t\terrno = EFBIG;\t\t\t/* not quite right, but it'll do */\n\t\tfd = -1;\n\t\tgoto error;\n\t}\n\n\t/* Now write the data into the successfully-reserved part of the file */\n\tfd = OpenTransientFile(PGSS_TEXT_FILE, O_RDWR | O_CREAT | PG_BINARY);\n\tif (fd < 0)\n\t\tgoto error;\n\n\t/*\n\t * The format of the stored string is:\n\t *  - tag_len bytes of query tag (maybe empty)\n\t *  - extras_len bytes of extras JSONB (maybe empty)\n\t *  - query_len bytes of query string\n\t *  - NUL\n\t */\n\tif (tag_len > 0 && pg_pwrite(fd, tag, tag_len, off) != tag_len)\n\t\tgoto error;\n\toff += tag_len;\n\tif (extras_len > 0 && pg_pwrite(fd, extras, extras_len, off) != extras_len)\n\t\tgoto error;\n\toff += extras_len;\n\tif (pg_pwrite(fd, query, query_len, off) != query_len)\n\t\tgoto error;\n\toff += query_len;\n\tif (pg_pwrite(fd, \"\\0\", 1, off) != 1)\n\t\tgoto error;\n\n\tCloseTransientFile(fd);\n\n\t/* Mark our write complete */\n\tSpinLockAcquire(&pgss->mutex);\n\tpgss->n_writers--;\n\tSpinLockRelease(&pgss->mutex);\n\n\treturn true;\n\nerror:\n\tereport(LOG,\n\t\t\t(errcode_for_file_access(),\n\t\t\t errmsg(\"could not write file \\\"%s\\\": %m\",\n\t\t\t\t\tPGSS_TEXT_FILE)));\n\n\tif (fd >= 0)\n\t\tCloseTransientFile(fd);\n\n\t/* Mark our write complete */\n\tSpinLockAcquire(&pgss->mutex);\n\tpgss->n_writers--;\n\tSpinLockRelease(&pgss->mutex);\n\n\treturn false;\n}\n\n/*\n * Read the external query text file into a malloc'd buffer.\n *\n * Returns NULL (without throwing an error) if unable to read, eg\n * file not there or insufficient memory.\n *\n * On success, the buffer size is also returned into *buffer_size.\n *\n * This can be called without any lock on pgss->lock, but in that case\n * the caller is responsible for verifying that the result is sane.\n */\nstatic char *\nqtext_load_file(Size *buffer_size)\n{\n\tchar\t   *buf;\n\tint\t\t\tfd;\n\tstruct stat stat;\n\tSize\t\tnread;\n\n\tfd = OpenTransientFile(PGSS_TEXT_FILE, O_RDONLY | PG_BINARY);\n\tif (fd < 0)\n\t{\n\t\tif (errno != ENOENT)\n\t\t\tereport(LOG,\n\t\t\t\t\t(errcode_for_file_access(),\n\t\t\t\t\t errmsg(\"could not read file \\\"%s\\\": %m\",\n\t\t\t\t\t\t\tPGSS_TEXT_FILE)));\n\t\treturn NULL;\n\t}\n\n\t/* Get file length */\n\tif (fstat(fd, &stat))\n\t{\n\t\tereport(LOG,\n\t\t\t\t(errcode_for_file_access(),\n\t\t\t\t errmsg(\"could not stat file \\\"%s\\\": %m\",\n\t\t\t\t\t\tPGSS_TEXT_FILE)));\n\t\tCloseTransientFile(fd);\n\t\treturn NULL;\n\t}\n\n\t/* Allocate buffer; beware that off_t might be wider than size_t */\n\tif (stat.st_size <= MaxAllocHugeSize)\n\t\tbuf = (char *) malloc(stat.st_size);\n\telse\n\t\tbuf = NULL;\n\tif (buf == NULL)\n\t{\n\t\tereport(LOG,\n\t\t\t\t(errcode(ERRCODE_OUT_OF_MEMORY),\n\t\t\t\t errmsg(\"out of memory\"),\n\t\t\t\t errdetail(\"Could not allocate enough memory to read file \\\"%s\\\".\",\n\t\t\t\t\t\t   PGSS_TEXT_FILE)));\n\t\tCloseTransientFile(fd);\n\t\treturn NULL;\n\t}\n\n\t/*\n\t * OK, slurp in the file.  Windows fails if we try to read more than\n\t * INT_MAX bytes at once, and other platforms might not like that either,\n\t * so read a very large file in 1GB segments.\n\t */\n\tnread = 0;\n\twhile (nread < stat.st_size)\n\t{\n\t\tint\t\t\ttoread = Min(1024 * 1024 * 1024, stat.st_size - nread);\n\n\t\t/*\n\t\t * If we get a short read and errno doesn't get set, the reason is\n\t\t * probably that garbage collection truncated the file since we did\n\t\t * the fstat(), so we don't log a complaint --- but we don't return\n\t\t * the data, either, since it's most likely corrupt due to concurrent\n\t\t * writes from garbage collection.\n\t\t */\n\t\terrno = 0;\n\t\tif (read(fd, buf + nread, toread) != toread)\n\t\t{\n\t\t\tif (errno)\n\t\t\t\tereport(LOG,\n\t\t\t\t\t\t(errcode_for_file_access(),\n\t\t\t\t\t\t errmsg(\"could not read file \\\"%s\\\": %m\",\n\t\t\t\t\t\t\t\tPGSS_TEXT_FILE)));\n\t\t\tfree(buf);\n\t\t\tCloseTransientFile(fd);\n\t\t\treturn NULL;\n\t\t}\n\t\tnread += toread;\n\t}\n\n\tif (CloseTransientFile(fd) != 0)\n\t\tereport(LOG,\n\t\t\t\t(errcode_for_file_access(),\n\t\t\t\t errmsg(\"could not close file \\\"%s\\\": %m\", PGSS_TEXT_FILE)));\n\n\t*buffer_size = nread;\n\treturn buf;\n}\n\n/*\n * Locate a query text in the file image previously read by qtext_load_file().\n *\n * We validate the given offset/length, and return NULL if bogus.  Otherwise,\n * the result points to a null-terminated string within the buffer.\n */\nstatic char *\nqtext_fetch(Size query_offset, int query_len,\n\t\t\tchar *buffer, Size buffer_size)\n{\n\t/* File read failed? */\n\tif (buffer == NULL)\n\t\treturn NULL;\n\t/* Bogus offset/length? */\n\tif (query_len < 0 ||\n\t\tquery_offset + query_len >= buffer_size)\n\t\treturn NULL;\n\t/* As a further sanity check, make sure there's a trailing null */\n\tif (buffer[query_offset + query_len] != '\\0')\n\t\treturn NULL;\n\t/* Looks OK */\n\treturn buffer + query_offset;\n}\n\n/*\n * Do we need to garbage-collect the external query text file?\n *\n * Caller should hold at least a shared lock on pgss->lock.\n */\nstatic bool\nneed_gc_qtexts(void)\n{\n\tSize\t\textent;\n\n\t/* Read shared extent pointer */\n\tSpinLockAcquire(&pgss->mutex);\n\textent = pgss->extent;\n\tSpinLockRelease(&pgss->mutex);\n\n\t/*\n\t * Don't proceed if file does not exceed 512 bytes per possible entry.\n\t *\n\t * Here and in the next test, 32-bit machines have overflow hazards if\n\t * pgss_max and/or mean_query_len are large.  Force the multiplications\n\t * and comparisons to be done in uint64 arithmetic to forestall trouble.\n\t */\n\tif ((uint64) extent < (uint64) 512 * pgss_max)\n\t\treturn false;\n\n\t/*\n\t * Don't proceed if file is less than about 50% bloat.  Nothing can or\n\t * should be done in the event of unusually large query texts accounting\n\t * for file's large size.  We go to the trouble of maintaining the mean\n\t * query length in order to prevent garbage collection from thrashing\n\t * uselessly.\n\t */\n\tif ((uint64) extent < (uint64) pgss->mean_query_len * pgss_max * 2)\n\t\treturn false;\n\n\treturn true;\n}\n\n/*\n * Garbage-collect orphaned query texts in external file.\n *\n * This won't be called often in the typical case, since it's likely that\n * there won't be too much churn, and besides, a similar compaction process\n * occurs when serializing to disk at shutdown or as part of resetting.\n * Despite this, it seems prudent to plan for the edge case where the file\n * becomes unreasonably large, with no other method of compaction likely to\n * occur in the foreseeable future.\n *\n * The caller must hold an exclusive lock on pgss->lock.\n *\n * At the first sign of trouble we unlink the query text file to get a clean\n * slate (although existing statistics are retained), rather than risk\n * thrashing by allowing the same problem case to recur indefinitely.\n */\nstatic void\ngc_qtexts(void)\n{\n\tchar\t   *qbuffer;\n\tSize\t\tqbuffer_size;\n\tFILE\t   *qfile = NULL;\n\tHASH_SEQ_STATUS hash_seq;\n\tpgssEntry  *entry;\n\tSize\t\textent;\n\tint\t\t\tnentries;\n\n\t/*\n\t * When called from pgss_store, some other session might have proceeded\n\t * with garbage collection in the no-lock-held interim of lock strength\n\t * escalation.  Check once more that this is actually necessary.\n\t */\n\tif (!need_gc_qtexts())\n\t\treturn;\n\n\t/*\n\t * Load the old texts file.  If we fail (out of memory, for instance),\n\t * invalidate query texts.  Hopefully this is rare.  It might seem better\n\t * to leave things alone on an OOM failure, but the problem is that the\n\t * file is only going to get bigger; hoping for a future non-OOM result is\n\t * risky and can easily lead to complete denial of service.\n\t */\n\tqbuffer = qtext_load_file(&qbuffer_size);\n\tif (qbuffer == NULL)\n\t\tgoto gc_fail;\n\n\t/*\n\t * We overwrite the query texts file in place, so as to reduce the risk of\n\t * an out-of-disk-space failure.  Since the file is guaranteed not to get\n\t * larger, this should always work on traditional filesystems; though we\n\t * could still lose on copy-on-write filesystems.\n\t */\n\tqfile = AllocateFile(PGSS_TEXT_FILE, PG_BINARY_W);\n\tif (qfile == NULL)\n\t{\n\t\tereport(LOG,\n\t\t\t\t(errcode_for_file_access(),\n\t\t\t\t errmsg(\"could not write file \\\"%s\\\": %m\",\n\t\t\t\t\t\tPGSS_TEXT_FILE)));\n\t\tgoto gc_fail;\n\t}\n\n\textent = 0;\n\tnentries = 0;\n\n\thash_seq_init(&hash_seq, pgss_hash);\n\twhile ((entry = hash_seq_search(&hash_seq)) != NULL)\n\t{\n\t\tint\t\t\tquery_len = entry->query_len + entry->extras_len + entry->tag_len;\n\t\tchar\t   *qry = qtext_fetch(entry->query_offset,\n\t\t\t\t\t\t\t\t\t  query_len,\n\t\t\t\t\t\t\t\t\t  qbuffer,\n\t\t\t\t\t\t\t\t\t  qbuffer_size);\n\n\t\tif (qry == NULL)\n\t\t{\n\t\t\t/* Trouble ... drop the text */\n\t\t\tentry->query_offset = 0;\n\t\t\tentry->query_len = -1;\n\t\t\tentry->extras_len = 0;\n\t\t\tentry->tag_len = 0;\n\t\t\t/* entry will not be counted in mean query length computation */\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (fwrite(qry, 1, query_len + 1, qfile) != query_len + 1)\n\t\t{\n\t\t\tereport(LOG,\n\t\t\t\t\t(errcode_for_file_access(),\n\t\t\t\t\t errmsg(\"could not write file \\\"%s\\\": %m\",\n\t\t\t\t\t\t\tPGSS_TEXT_FILE)));\n\t\t\thash_seq_term(&hash_seq);\n\t\t\tgoto gc_fail;\n\t\t}\n\n\t\tentry->query_offset = extent;\n\t\textent += query_len + 1;\n\t\tnentries++;\n\t}\n\n\t/*\n\t * Truncate away any now-unused space.  If this fails for some odd reason,\n\t * we log it, but there's no need to fail.\n\t */\n\tif (ftruncate(fileno(qfile), extent) != 0)\n\t\tereport(LOG,\n\t\t\t\t(errcode_for_file_access(),\n\t\t\t\t errmsg(\"could not truncate file \\\"%s\\\": %m\",\n\t\t\t\t\t\tPGSS_TEXT_FILE)));\n\n\tif (FreeFile(qfile))\n\t{\n\t\tereport(LOG,\n\t\t\t\t(errcode_for_file_access(),\n\t\t\t\t errmsg(\"could not write file \\\"%s\\\": %m\",\n\t\t\t\t\t\tPGSS_TEXT_FILE)));\n\t\tqfile = NULL;\n\t\tgoto gc_fail;\n\t}\n\n\telog(DEBUG1, \"pgss gc of queries file shrunk size from %zu to %zu\",\n\t\t pgss->extent, extent);\n\n\t/* Reset the shared extent pointer */\n\tpgss->extent = extent;\n\n\t/*\n\t * Also update the mean query length, to be sure that need_gc_qtexts()\n\t * won't still think we have a problem.\n\t */\n\tif (nentries > 0)\n\t\tpgss->mean_query_len = extent / nentries;\n\telse\n\t\tpgss->mean_query_len = ASSUMED_LENGTH_INIT;\n\n\tfree(qbuffer);\n\n\t/*\n\t * OK, count a garbage collection cycle.  (Note: even though we have\n\t * exclusive lock on pgss->lock, we must take pgss->mutex for this, since\n\t * other processes may examine gc_count while holding only the mutex.\n\t * Also, we have to advance the count *after* we've rewritten the file,\n\t * else other processes might not realize they read a stale file.)\n\t */\n\trecord_gc_qtexts();\n\n\treturn;\n\ngc_fail:\n\t/* clean up resources */\n\tif (qfile)\n\t\tFreeFile(qfile);\n\tfree(qbuffer);\n\n\t/*\n\t * Since the contents of the external file are now uncertain, mark all\n\t * hashtable entries as having invalid texts.\n\t */\n\thash_seq_init(&hash_seq, pgss_hash);\n\twhile ((entry = hash_seq_search(&hash_seq)) != NULL)\n\t{\n\t\tentry->query_offset = 0;\n\t\tentry->query_len = -1;\n\t\tentry->extras_len = 0;\n\t\tentry->tag_len = 0;\n\t}\n\n\t/*\n\t * Destroy the query text file and create a new, empty one\n\t */\n\t(void) unlink(PGSS_TEXT_FILE);\n\tqfile = AllocateFile(PGSS_TEXT_FILE, PG_BINARY_W);\n\tif (qfile == NULL)\n\t\tereport(LOG,\n\t\t\t\t(errcode_for_file_access(),\n\t\t\t\t errmsg(\"could not recreate file \\\"%s\\\": %m\",\n\t\t\t\t\t\tPGSS_TEXT_FILE)));\n\telse\n\t\tFreeFile(qfile);\n\n\t/* Reset the shared extent pointer */\n\tpgss->extent = 0;\n\n\t/* Reset mean_query_len to match the new state */\n\tpgss->mean_query_len = ASSUMED_LENGTH_INIT;\n\n\t/*\n\t * Bump the GC count even though we failed.\n\t *\n\t * This is needed to make concurrent readers of file without any lock on\n\t * pgss->lock notice existence of new version of file.  Once readers\n\t * subsequently observe a change in GC count with pgss->lock held, that\n\t * forces a safe reopen of file.  Writers also require that we bump here,\n\t * of course.  (As required by locking protocol, readers and writers don't\n\t * trust earlier file contents until gc_count is found unchanged after\n\t * pgss->lock acquired in shared or exclusive mode respectively.)\n\t */\n\trecord_gc_qtexts();\n}\n\n#define SINGLE_ENTRY_RESET(e) \\\nif (e) { \\\n\tif (minmax_only) { \\\n\t\t/* When requested reset only min/max statistics of an entry */ \\\n\t\tfor (int kind = 0; kind < PGSS_NUMKIND; kind++) \\\n\t\t{ \\\n\t\t\te->counters.max_time[kind] = 0; \\\n\t\t\te->counters.min_time[kind] = 0; \\\n\t\t} \\\n\t\te->minmax_stats_since = stats_reset; \\\n\t} \\\n\telse \\\n\t{ \\\n\t\t/* Remove the key otherwise  */ \\\n\t\thash_search(pgss_hash, &e->key, HASH_REMOVE, NULL); \\\n\t\tnum_remove++; \\\n\t} \\\n}\n\n/*\n * Reset entries corresponding to parameters passed.\n */\nstatic TimestampTz\nentry_reset(Oid userid, const Datum *dbids, int dbids_len, uint64 queryid, bool minmax_only)\n{\n\tHASH_SEQ_STATUS hash_seq;\n\tpgssEntry  *entry;\n\tFILE\t   *qfile;\n\tlong\t\tnum_entries;\n\tlong\t\tnum_remove = 0;\n\tpgssHashKey key;\n\tTimestampTz stats_reset;\n\n\tif (!pgss || !pgss_hash)\n\t\tereport(ERROR,\n\t\t\t\t(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),\n\t\t\t\t errmsg(\"edb_stat_statements must be loaded via \\\"shared_preload_libraries\\\"\")));\n\n\tLWLockAcquire(pgss->lock, LW_EXCLUSIVE);\n\tnum_entries = hash_get_num_entries(pgss_hash);\n\n\tstats_reset = GetCurrentTimestamp();\n\n\tif (userid != 0 && dbids_len == 1 && queryid != UINT64CONST(0))\n\t{\n\t\t/* If all the parameters are available, use the fast path. */\n\t\tmemset(&key, 0, sizeof(pgssHashKey));\n\t\tkey.userid = userid;\n\t\tkey.dbid = DatumGetObjectId(dbids[0]);\n\t\tkey.queryid = queryid;\n\n\t\t/*\n\t\t * Reset the entry if it exists, starting with the non-top-level\n\t\t * entry.\n\t\t */\n\t\tkey.toplevel = false;\n\t\tentry = (pgssEntry *) hash_search(pgss_hash, &key, HASH_FIND, NULL);\n\n\t\tSINGLE_ENTRY_RESET(entry);\n\n\t\t/* Also reset the top-level entry if it exists. */\n\t\tkey.toplevel = true;\n\t\tentry = (pgssEntry *) hash_search(pgss_hash, &key, HASH_FIND, NULL);\n\n\t\tSINGLE_ENTRY_RESET(entry);\n\t}\n\telse if (userid != 0 || dbids_len > 0 || queryid != UINT64CONST(0))\n\t{\n\t\t/* Reset entries corresponding to valid parameters. */\n\t\thash_seq_init(&hash_seq, pgss_hash);\n\t\tif (dbids_len > 0) {\n\t\t\twhile ((entry = hash_seq_search(&hash_seq)) != NULL) {\n\t\t\t\tfor (int i = 0; i < dbids_len; i++) {\n\t\t\t\t\tOid dbid = DatumGetObjectId(dbids[i]);\n\t\t\t\t\tif ((!userid || entry->key.userid == userid) &&\n\t\t\t\t\t\t(entry->key.dbid == dbid) &&\n\t\t\t\t\t\t(!queryid || entry->key.queryid == queryid)) {\n\t\t\t\t\t\tSINGLE_ENTRY_RESET(entry);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\twhile ((entry = hash_seq_search(&hash_seq)) != NULL) {\n\t\t\t\tif ((!userid || entry->key.userid == userid) &&\n\t\t\t\t\t(!queryid || entry->key.queryid == queryid)) {\n\t\t\t\t\tSINGLE_ENTRY_RESET(entry);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\telse\n\t{\n\t\t/* Reset all entries. */\n\t\thash_seq_init(&hash_seq, pgss_hash);\n\t\twhile ((entry = hash_seq_search(&hash_seq)) != NULL)\n\t\t{\n\t\t\tSINGLE_ENTRY_RESET(entry);\n\t\t}\n\t}\n\n\t/* All entries are removed? */\n\tif (num_entries != num_remove)\n\t\tgoto release_lock;\n\n\t/*\n\t * Reset global statistics for edb_stat_statements since all entries are\n\t * removed.\n\t */\n\tSpinLockAcquire(&pgss->mutex);\n\tpgss->stats.dealloc = 0;\n\tpgss->stats.stats_reset = stats_reset;\n\tSpinLockRelease(&pgss->mutex);\n\n\t/*\n\t * Write new empty query file, perhaps even creating a new one to recover\n\t * if the file was missing.\n\t */\n\tqfile = AllocateFile(PGSS_TEXT_FILE, PG_BINARY_W);\n\tif (qfile == NULL)\n\t{\n\t\tereport(LOG,\n\t\t\t\t(errcode_for_file_access(),\n\t\t\t\t errmsg(\"could not create file \\\"%s\\\": %m\",\n\t\t\t\t\t\tPGSS_TEXT_FILE)));\n\t\tgoto done;\n\t}\n\n\t/* If ftruncate fails, log it, but it's not a fatal problem */\n\tif (ftruncate(fileno(qfile), 0) != 0)\n\t\tereport(LOG,\n\t\t\t\t(errcode_for_file_access(),\n\t\t\t\t errmsg(\"could not truncate file \\\"%s\\\": %m\",\n\t\t\t\t\t\tPGSS_TEXT_FILE)));\n\n\tFreeFile(qfile);\n\ndone:\n\tpgss->extent = 0;\n\t/* This counts as a query text garbage collection for our purposes */\n\trecord_gc_qtexts();\n\nrelease_lock:\n\tLWLockRelease(pgss->lock);\n\n\treturn stats_reset;\n}\n\n/*\n * Generate a normalized version of the query string that will be used to\n * represent all similar queries.\n *\n * Note that the normalized representation may well vary depending on\n * just which \"equivalent\" query is used to create the hashtable entry.\n * We assume this is OK.\n *\n * If query_loc > 0, then \"query\" has been advanced by that much compared to\n * the original string start, so we need to translate the provided locations\n * to compensate.  (This lets us avoid re-scanning statements before the one\n * of interest, so it's worth doing.)\n *\n * *query_len_p contains the input string length, and is updated with\n * the result string length on exit.  The resulting string might be longer\n * or shorter depending on what happens with replacement of constants.\n *\n * Returns a palloc'd string.\n */\nstatic char *\ngenerate_normalized_query(JumbleState *jstate, const char *query,\n\t\t\t\t\t\t  int query_loc, int *query_len_p)\n{\n\tchar\t   *norm_query;\n\tint\t\t\tquery_len = *query_len_p;\n\tint\t\t\ti,\n\t\t\t\tnorm_query_buflen,\t/* Space allowed for norm_query */\n\t\t\t\tlen_to_wrt,\t\t/* Length (in bytes) to write */\n\t\t\t\tquer_loc = 0,\t/* Source query byte location */\n\t\t\t\tn_quer_loc = 0, /* Normalized query byte location */\n\t\t\t\tlast_off = 0,\t/* Offset from start for previous tok */\n\t\t\t\tlast_tok_len = 0;\t/* Length (in bytes) of that tok */\n\n\t/*\n\t * Get constants' lengths (core system only gives us locations).  Note\n\t * this also ensures the items are sorted by location.\n\t */\n\tfill_in_constant_lengths(jstate, query, query_loc);\n\n\t/*\n\t * Allow for $n symbols to be longer than the constants they replace.\n\t * Constants must take at least one byte in text form, while a $n symbol\n\t * certainly isn't more than 11 bytes, even if n reaches INT_MAX.  We\n\t * could refine that limit based on the max value of n for the current\n\t * query, but it hardly seems worth any extra effort to do so.\n\t */\n\tnorm_query_buflen = query_len + jstate->clocations_count * 10;\n\n\t/* Allocate result buffer */\n\tnorm_query = palloc(norm_query_buflen + 1);\n\n\tfor (i = 0; i < jstate->clocations_count; i++)\n\t{\n\t\tint\t\t\toff,\t\t/* Offset from start for cur tok */\n\t\t\t\t\ttok_len;\t/* Length (in bytes) of that tok */\n\n\t\toff = jstate->clocations[i].location;\n\t\t/* Adjust recorded location if we're dealing with partial string */\n\t\toff -= query_loc;\n\n\t\ttok_len = jstate->clocations[i].length;\n\n\t\tif (tok_len < 0)\n\t\t\tcontinue;\t\t\t/* ignore any duplicates */\n\n\t\t/* Copy next chunk (what precedes the next constant) */\n\t\tlen_to_wrt = off - last_off;\n\t\tlen_to_wrt -= last_tok_len;\n\n\t\tAssert(len_to_wrt >= 0);\n\t\tmemcpy(norm_query + n_quer_loc, query + quer_loc, len_to_wrt);\n\t\tn_quer_loc += len_to_wrt;\n\n\t\t/* And insert a param symbol in place of the constant token */\n\t\tn_quer_loc += sprintf(norm_query + n_quer_loc, \"$%d\",\n\t\t\t\t\t\t\t  i + 1 + jstate->highest_extern_param_id);\n\n\t\tquer_loc = off + tok_len;\n\t\tlast_off = off;\n\t\tlast_tok_len = tok_len;\n\t}\n\n\t/*\n\t * We've copied up until the last ignorable constant.  Copy over the\n\t * remaining bytes of the original query string.\n\t */\n\tlen_to_wrt = query_len - quer_loc;\n\n\tAssert(len_to_wrt >= 0);\n\tmemcpy(norm_query + n_quer_loc, query + quer_loc, len_to_wrt);\n\tn_quer_loc += len_to_wrt;\n\n\tAssert(n_quer_loc <= norm_query_buflen);\n\tnorm_query[n_quer_loc] = '\\0';\n\n\t*query_len_p = n_quer_loc;\n\treturn norm_query;\n}\n\n/*\n * Given a valid SQL string and an array of constant-location records,\n * fill in the textual lengths of those constants.\n *\n * The constants may use any allowed constant syntax, such as float literals,\n * bit-strings, single-quoted strings and dollar-quoted strings.  This is\n * accomplished by using the public API for the core scanner.\n *\n * It is the caller's job to ensure that the string is a valid SQL statement\n * with constants at the indicated locations.  Since in practice the string\n * has already been parsed, and the locations that the caller provides will\n * have originated from within the authoritative parser, this should not be\n * a problem.\n *\n * Duplicate constant pointers are possible, and will have their lengths\n * marked as '-1', so that they are later ignored.  (Actually, we assume the\n * lengths were initialized as -1 to start with, and don't change them here.)\n *\n * If query_loc > 0, then \"query\" has been advanced by that much compared to\n * the original string start, so we need to translate the provided locations\n * to compensate.  (This lets us avoid re-scanning statements before the one\n * of interest, so it's worth doing.)\n *\n * N.B. There is an assumption that a '-' character at a Const location begins\n * a negative numeric constant.  This precludes there ever being another\n * reason for a constant to start with a '-'.\n */\nstatic void\nfill_in_constant_lengths(JumbleState *jstate, const char *query,\n\t\t\t\t\t\t int query_loc)\n{\n\tLocationLen *locs;\n\tcore_yyscan_t yyscanner;\n\tcore_yy_extra_type yyextra;\n\tcore_YYSTYPE yylval;\n\tYYLTYPE\t\tyylloc;\n\tint\t\t\tlast_loc = -1;\n\tint\t\t\ti;\n\n\t/*\n\t * Sort the records by location so that we can process them in order while\n\t * scanning the query text.\n\t */\n\tif (jstate->clocations_count > 1)\n\t\tqsort(jstate->clocations, jstate->clocations_count,\n\t\t\t  sizeof(LocationLen), comp_location);\n\tlocs = jstate->clocations;\n\n\t/* initialize the flex scanner --- should match raw_parser() */\n\tyyscanner = scanner_init(query,\n\t\t\t\t\t\t\t &yyextra,\n\t\t\t\t\t\t\t &ScanKeywords,\n\t\t\t\t\t\t\t ScanKeywordTokens);\n\n\t/* we don't want to re-emit any escape string warnings */\n\tyyextra.escape_string_warning = false;\n\n\t/* Search for each constant, in sequence */\n\tfor (i = 0; i < jstate->clocations_count; i++)\n\t{\n\t\tint\t\t\tloc = locs[i].location;\n\t\tint\t\t\ttok;\n\n\t\t/* Adjust recorded location if we're dealing with partial string */\n\t\tloc -= query_loc;\n\n\t\tAssert(loc >= 0);\n\n\t\tif (loc <= last_loc)\n\t\t\tcontinue;\t\t\t/* Duplicate constant, ignore */\n\n\t\t/* Lex tokens until we find the desired constant */\n\t\tfor (;;)\n\t\t{\n\t\t\ttok = core_yylex(&yylval, &yylloc, yyscanner);\n\n\t\t\t/* We should not hit end-of-string, but if we do, behave sanely */\n\t\t\tif (tok == 0)\n\t\t\t\tbreak;\t\t\t/* out of inner for-loop */\n\n\t\t\t/*\n\t\t\t * We should find the token position exactly, but if we somehow\n\t\t\t * run past it, work with that.\n\t\t\t */\n\t\t\tif (yylloc >= loc)\n\t\t\t{\n\t\t\t\tif (query[loc] == '-')\n\t\t\t\t{\n\t\t\t\t\t/*\n\t\t\t\t\t * It's a negative value - this is the one and only case\n\t\t\t\t\t * where we replace more than a single token.\n\t\t\t\t\t *\n\t\t\t\t\t * Do not compensate for the core system's special-case\n\t\t\t\t\t * adjustment of location to that of the leading '-'\n\t\t\t\t\t * operator in the event of a negative constant.  It is\n\t\t\t\t\t * also useful for our purposes to start from the minus\n\t\t\t\t\t * symbol.  In this way, queries like \"select * from foo\n\t\t\t\t\t * where bar = 1\" and \"select * from foo where bar = -2\"\n\t\t\t\t\t * will have identical normalized query strings.\n\t\t\t\t\t */\n\t\t\t\t\ttok = core_yylex(&yylval, &yylloc, yyscanner);\n\t\t\t\t\tif (tok == 0)\n\t\t\t\t\t\tbreak;\t/* out of inner for-loop */\n\t\t\t\t}\n\n\t\t\t\t/*\n\t\t\t\t * We now rely on the assumption that flex has placed a zero\n\t\t\t\t * byte after the text of the current token in scanbuf.\n\t\t\t\t */\n\t\t\t\tlocs[i].length = strlen(yyextra.scanbuf + loc);\n\t\t\t\tbreak;\t\t\t/* out of inner for-loop */\n\t\t\t}\n\t\t}\n\n\t\t/* If we hit end-of-string, give up, leaving remaining lengths -1 */\n\t\tif (tok == 0)\n\t\t\tbreak;\n\n\t\tlast_loc = loc;\n\t}\n\n\tscanner_finish(yyscanner);\n}\n\n/*\n * comp_location: comparator for qsorting LocationLen structs by location\n */\nstatic int\ncomp_location(const void *a, const void *b)\n{\n\tint\t\t\tl = ((const LocationLen *) a)->location;\n\tint\t\t\tr = ((const LocationLen *) b)->location;\n\n#if PG_VERSION_NUM >= 170000\n\treturn pg_cmp_s32(l, r);\n#else\n\tif (l < r)\n\t\treturn -1;\n\telse if (l > r)\n\t\treturn +1;\n\telse\n\t\treturn 0;\n#endif\n}\n"
  },
  {
    "path": "edb_stat_statements/edb_stat_statements.control",
    "content": "# edb_stat_statements extension\ncomment = 'track planning and execution statistics of all EdgeDB queries executed'\ndefault_version = '1.0'\nmodule_pathname = '$libdir/edb_stat_statements'\nrelocatable = true\n"
  },
  {
    "path": "edb_stat_statements/expected/cleanup.out",
    "content": "DROP EXTENSION edb_stat_statements;\n"
  },
  {
    "path": "edb_stat_statements/expected/cursors.out",
    "content": "--\n-- Cursors\n--\n-- These tests require track_utility to be enabled.\nSET edb_stat_statements.track_utility = TRUE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- DECLARE\n-- SELECT is normalized.\nDECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT 1;\nCLOSE cursor_stats_1;\nDECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT 2;\nCLOSE cursor_stats_1;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                         query                         \n-------+------+-------------------------------------------------------\n     2 |    0 | CLOSE cursor_stats_1\n     2 |    0 | DECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT $1\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(3 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- FETCH\nBEGIN;\nDECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT 2;\nDECLARE cursor_stats_2 CURSOR WITH HOLD FOR SELECT 3;\nFETCH 1 IN cursor_stats_1;\n ?column? \n----------\n        2\n(1 row)\n\nFETCH 1 IN cursor_stats_2;\n ?column? \n----------\n        3\n(1 row)\n\nCLOSE cursor_stats_1;\nCLOSE cursor_stats_2;\nCOMMIT;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                         query                         \n-------+------+-------------------------------------------------------\n     1 |    0 | BEGIN\n     1 |    0 | CLOSE cursor_stats_1\n     1 |    0 | CLOSE cursor_stats_2\n     1 |    0 | COMMIT\n     1 |    0 | DECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT $1\n     1 |    0 | DECLARE cursor_stats_2 CURSOR WITH HOLD FOR SELECT $1\n     1 |    1 | FETCH 1 IN cursor_stats_1\n     1 |    1 | FETCH 1 IN cursor_stats_2\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(9 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n"
  },
  {
    "path": "edb_stat_statements/expected/dml.out.17",
    "content": "--\n-- DMLs on test table\n--\nSET edb_stat_statements.track_utility = FALSE;\nCREATE TEMP TABLE pgss_dml_tab (a int, b char(20));\nINSERT INTO pgss_dml_tab VALUES(generate_series(1, 10), 'aaa');\nUPDATE pgss_dml_tab SET b = 'bbb' WHERE a > 7;\nDELETE FROM pgss_dml_tab WHERE a > 9;\n-- explicit transaction\nBEGIN;\nUPDATE pgss_dml_tab SET b = '111' WHERE a = 1 ;\nCOMMIT;\nBEGIN \\;\nUPDATE pgss_dml_tab SET b = '222' WHERE a = 2 \\;\nCOMMIT ;\nUPDATE pgss_dml_tab SET b = '333' WHERE a = 3 \\;\nUPDATE pgss_dml_tab SET b = '444' WHERE a = 4 ;\nBEGIN \\;\nUPDATE pgss_dml_tab SET b = '555' WHERE a = 5 \\;\nUPDATE pgss_dml_tab SET b = '666' WHERE a = 6 \\;\nCOMMIT ;\n-- many INSERT values\nINSERT INTO pgss_dml_tab (a, b) VALUES (1, 'a'), (2, 'b'), (3, 'c');\n-- SELECT with constants\nSELECT * FROM pgss_dml_tab WHERE a > 5 ORDER BY a ;\n a |          b           \n---+----------------------\n 6 | 666                 \n 7 | aaa                 \n 8 | bbb                 \n 9 | bbb                 \n(4 rows)\n\nSELECT *\n  FROM pgss_dml_tab\n  WHERE a > 9\n  ORDER BY a ;\n a | b \n---+---\n(0 rows)\n\n-- these two need to be done on a different table\n-- SELECT without constants\nSELECT * FROM pgss_dml_tab ORDER BY a;\n a |          b           \n---+----------------------\n 1 | a                   \n 1 | 111                 \n 2 | b                   \n 2 | 222                 \n 3 | c                   \n 3 | 333                 \n 4 | 444                 \n 5 | 555                 \n 6 | 666                 \n 7 | aaa                 \n 8 | bbb                 \n 9 | bbb                 \n(12 rows)\n\n-- SELECT with IN clause\nSELECT * FROM pgss_dml_tab WHERE a IN (1, 2, 3, 4, 5);\n a |          b           \n---+----------------------\n 1 | 111                 \n 2 | 222                 \n 3 | 333                 \n 4 | 444                 \n 5 | 555                 \n 1 | a                   \n 2 | b                   \n 3 | c                   \n(8 rows)\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                                query                                \n-------+------+---------------------------------------------------------------------\n     1 |    1 | DELETE FROM pgss_dml_tab WHERE a > $1\n     1 |    3 | INSERT INTO pgss_dml_tab (a, b) VALUES ($1, $2), ($3, $4), ($5, $6)\n     1 |   10 | INSERT INTO pgss_dml_tab VALUES(generate_series($1, $2), $3)\n     1 |   12 | SELECT * FROM pgss_dml_tab ORDER BY a\n     2 |    4 | SELECT * FROM pgss_dml_tab WHERE a > $1 ORDER BY a\n     1 |    8 | SELECT * FROM pgss_dml_tab WHERE a IN ($1, $2, $3, $4, $5)\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n     1 |    0 | SET edb_stat_statements.track_utility = FALSE\n     6 |    6 | UPDATE pgss_dml_tab SET b = $1 WHERE a = $2\n     1 |    3 | UPDATE pgss_dml_tab SET b = $1 WHERE a > $2\n(10 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- MERGE\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN MATCHED THEN UPDATE SET b = st.b || st.a::text;\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN MATCHED THEN UPDATE SET b = pgss_dml_tab.b || st.a::text;\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN MATCHED AND length(st.b) > 1 THEN UPDATE SET b = pgss_dml_tab.b || st.a::text;\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)\n WHEN NOT MATCHED THEN INSERT (a, b) VALUES (0, NULL);\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)\n WHEN NOT MATCHED THEN INSERT VALUES (0, NULL);\t-- same as above\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)\n WHEN NOT MATCHED THEN INSERT (b, a) VALUES (NULL, 0);\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)\n WHEN NOT MATCHED THEN INSERT (a) VALUES (0);\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN MATCHED THEN DELETE;\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN MATCHED THEN DO NOTHING;\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN NOT MATCHED THEN DO NOTHING;\nDROP TABLE pgss_dml_tab;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                                          query                                          \n-------+------+-----------------------------------------------------------------------------------------\n     1 |    6 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+\n       |      |  WHEN MATCHED AND length(st.b) > $2 THEN UPDATE SET b = pgss_dml_tab.b || st.a::text\n     1 |    6 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+\n       |      |  WHEN MATCHED THEN DELETE\n     1 |    0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+\n       |      |  WHEN MATCHED THEN DO NOTHING\n     1 |    6 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+\n       |      |  WHEN MATCHED THEN UPDATE SET b = pgss_dml_tab.b || st.a::text\n     1 |    6 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+\n       |      |  WHEN MATCHED THEN UPDATE SET b = st.b || st.a::text\n     1 |    0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+\n       |      |  WHEN NOT MATCHED THEN DO NOTHING\n     1 |    0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)               +\n       |      |  WHEN NOT MATCHED THEN INSERT (a) VALUES ($1)\n     2 |    0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)               +\n       |      |  WHEN NOT MATCHED THEN INSERT (a, b) VALUES ($1, $2)\n     1 |    0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)               +\n       |      |  WHEN NOT MATCHED THEN INSERT (b, a) VALUES ($1, $2)\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(10 rows)\n\n-- check that [temp] table relation extensions are tracked as writes\nCREATE TABLE pgss_extend_tab (a int, b text);\nCREATE TEMP TABLE pgss_extend_temp_tab (a int, b text);\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nINSERT INTO pgss_extend_tab (a, b) SELECT generate_series(1, 1000), 'something';\nINSERT INTO pgss_extend_temp_tab (a, b) SELECT generate_series(1, 1000), 'something';\nWITH sizes AS (\n  SELECT\n    pg_relation_size('pgss_extend_tab') / current_setting('block_size')::int8 AS rel_size,\n    pg_relation_size('pgss_extend_temp_tab') / current_setting('block_size')::int8 AS temp_rel_size\n)\nSELECT\n    SUM(local_blks_written) >= (SELECT temp_rel_size FROM sizes) AS temp_written_ok,\n    SUM(local_blks_dirtied) >= (SELECT temp_rel_size FROM sizes) AS temp_dirtied_ok,\n    SUM(shared_blks_written) >= (SELECT rel_size FROM sizes) AS written_ok,\n    SUM(shared_blks_dirtied) >= (SELECT rel_size FROM sizes) AS dirtied_ok\nFROM edb_stat_statements;\n temp_written_ok | temp_dirtied_ok | written_ok | dirtied_ok \n-----------------+-----------------+------------+------------\n t               | t               | t          | t\n(1 row)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n"
  },
  {
    "path": "edb_stat_statements/expected/dml.out.18",
    "content": "--\n-- DMLs on test table\n--\nSET edb_stat_statements.track_utility = FALSE;\nCREATE TEMP TABLE pgss_dml_tab (a int, b char(20));\nINSERT INTO pgss_dml_tab VALUES(generate_series(1, 10), 'aaa');\nUPDATE pgss_dml_tab SET b = 'bbb' WHERE a > 7;\nDELETE FROM pgss_dml_tab WHERE a > 9;\n-- explicit transaction\nBEGIN;\nUPDATE pgss_dml_tab SET b = '111' WHERE a = 1 ;\nCOMMIT;\nBEGIN \\;\nUPDATE pgss_dml_tab SET b = '222' WHERE a = 2 \\;\nCOMMIT ;\nUPDATE pgss_dml_tab SET b = '333' WHERE a = 3 \\;\nUPDATE pgss_dml_tab SET b = '444' WHERE a = 4 ;\nBEGIN \\;\nUPDATE pgss_dml_tab SET b = '555' WHERE a = 5 \\;\nUPDATE pgss_dml_tab SET b = '666' WHERE a = 6 \\;\nCOMMIT ;\n-- many INSERT values\nINSERT INTO pgss_dml_tab (a, b) VALUES (1, 'a'), (2, 'b'), (3, 'c');\n-- SELECT with constants\nSELECT * FROM pgss_dml_tab WHERE a > 5 ORDER BY a ;\n a |          b           \n---+----------------------\n 6 | 666                 \n 7 | aaa                 \n 8 | bbb                 \n 9 | bbb                 \n(4 rows)\n\nSELECT *\n  FROM pgss_dml_tab\n  WHERE a > 9\n  ORDER BY a ;\n a | b \n---+---\n(0 rows)\n\n-- these two need to be done on a different table\n-- SELECT without constants\nSELECT * FROM pgss_dml_tab ORDER BY a;\n a |          b           \n---+----------------------\n 1 | a                   \n 1 | 111                 \n 2 | b                   \n 2 | 222                 \n 3 | c                   \n 3 | 333                 \n 4 | 444                 \n 5 | 555                 \n 6 | 666                 \n 7 | aaa                 \n 8 | bbb                 \n 9 | bbb                 \n(12 rows)\n\n-- SELECT with IN clause\nSELECT * FROM pgss_dml_tab WHERE a IN (1, 2, 3, 4, 5);\n a |          b           \n---+----------------------\n 1 | 111                 \n 2 | 222                 \n 3 | 333                 \n 4 | 444                 \n 5 | 555                 \n 1 | a                   \n 2 | b                   \n 3 | c                   \n(8 rows)\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                                query                                \n-------+------+---------------------------------------------------------------------\n     1 |    1 | DELETE FROM pgss_dml_tab WHERE a > $1\n     1 |    3 | INSERT INTO pgss_dml_tab (a, b) VALUES ($1, $2), ($3, $4), ($5, $6)\n     1 |   10 | INSERT INTO pgss_dml_tab VALUES(generate_series($1, $2), $3)\n     1 |   12 | SELECT * FROM pgss_dml_tab ORDER BY a\n     2 |    4 | SELECT * FROM pgss_dml_tab WHERE a > $1 ORDER BY a\n     1 |    8 | SELECT * FROM pgss_dml_tab WHERE a IN ($1, $2, $3, $4, $5)\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n     1 |    0 | SET edb_stat_statements.track_utility = $1\n     6 |    6 | UPDATE pgss_dml_tab SET b = $1 WHERE a = $2\n     1 |    3 | UPDATE pgss_dml_tab SET b = $1 WHERE a > $2\n(10 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- MERGE\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN MATCHED THEN UPDATE SET b = st.b || st.a::text;\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN MATCHED THEN UPDATE SET b = pgss_dml_tab.b || st.a::text;\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN MATCHED AND length(st.b) > 1 THEN UPDATE SET b = pgss_dml_tab.b || st.a::text;\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)\n WHEN NOT MATCHED THEN INSERT (a, b) VALUES (0, NULL);\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)\n WHEN NOT MATCHED THEN INSERT VALUES (0, NULL);\t-- same as above\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)\n WHEN NOT MATCHED THEN INSERT (b, a) VALUES (NULL, 0);\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)\n WHEN NOT MATCHED THEN INSERT (a) VALUES (0);\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN MATCHED THEN DELETE;\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN MATCHED THEN DO NOTHING;\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN NOT MATCHED THEN DO NOTHING;\nDROP TABLE pgss_dml_tab;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                                          query                                          \n-------+------+-----------------------------------------------------------------------------------------\n     1 |    6 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+\n       |      |  WHEN MATCHED AND length(st.b) > $2 THEN UPDATE SET b = pgss_dml_tab.b || st.a::text\n     1 |    6 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+\n       |      |  WHEN MATCHED THEN DELETE\n     1 |    0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+\n       |      |  WHEN MATCHED THEN DO NOTHING\n     1 |    6 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+\n       |      |  WHEN MATCHED THEN UPDATE SET b = pgss_dml_tab.b || st.a::text\n     1 |    6 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+\n       |      |  WHEN MATCHED THEN UPDATE SET b = st.b || st.a::text\n     1 |    0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= $1)+\n       |      |  WHEN NOT MATCHED THEN DO NOTHING\n     1 |    0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)               +\n       |      |  WHEN NOT MATCHED THEN INSERT (a) VALUES ($1)\n     2 |    0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)               +\n       |      |  WHEN NOT MATCHED THEN INSERT (a, b) VALUES ($1, $2)\n     1 |    0 | MERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)               +\n       |      |  WHEN NOT MATCHED THEN INSERT (b, a) VALUES ($1, $2)\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(10 rows)\n\n-- check that [temp] table relation extensions are tracked as writes\nCREATE TABLE pgss_extend_tab (a int, b text);\nCREATE TEMP TABLE pgss_extend_temp_tab (a int, b text);\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nINSERT INTO pgss_extend_tab (a, b) SELECT generate_series(1, 1000), 'something';\nINSERT INTO pgss_extend_temp_tab (a, b) SELECT generate_series(1, 1000), 'something';\nWITH sizes AS (\n  SELECT\n    pg_relation_size('pgss_extend_tab') / current_setting('block_size')::int8 AS rel_size,\n    pg_relation_size('pgss_extend_temp_tab') / current_setting('block_size')::int8 AS temp_rel_size\n)\nSELECT\n    SUM(local_blks_written) >= (SELECT temp_rel_size FROM sizes) AS temp_written_ok,\n    SUM(local_blks_dirtied) >= (SELECT temp_rel_size FROM sizes) AS temp_dirtied_ok,\n    SUM(shared_blks_written) >= (SELECT rel_size FROM sizes) AS written_ok,\n    SUM(shared_blks_dirtied) >= (SELECT rel_size FROM sizes) AS dirtied_ok\nFROM edb_stat_statements;\n temp_written_ok | temp_dirtied_ok | written_ok | dirtied_ok \n-----------------+-----------------+------------+------------\n t               | t               | t          | t\n(1 row)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n"
  },
  {
    "path": "edb_stat_statements/expected/entry_timestamp.out",
    "content": "--\n-- statement timestamps\n--\n-- planning time is needed during tests\nSET edb_stat_statements.track_planning = TRUE;\nSELECT 1 AS \"STMTTS1\";\n STMTTS1 \n---------\n       1\n(1 row)\n\nSELECT now() AS ref_ts \\gset\nSELECT 1,2 AS \"STMTTS2\";\n ?column? | STMTTS2 \n----------+---------\n        1 |       2\n(1 row)\n\nSELECT stats_since >= :'ref_ts', count(*) FROM edb_stat_statements\nWHERE query LIKE '%STMTTS%'\nGROUP BY stats_since >= :'ref_ts'\nORDER BY stats_since >= :'ref_ts';\n ?column? | count \n----------+-------\n f        |     1\n t        |     1\n(2 rows)\n\nSELECT now() AS ref_ts \\gset\nSELECT\n  count(*) as total,\n  count(*) FILTER (\n    WHERE min_plan_time + max_plan_time = 0\n  ) as minmax_plan_zero,\n  count(*) FILTER (\n    WHERE min_exec_time + max_exec_time = 0\n  ) as minmax_exec_zero,\n  count(*) FILTER (\n    WHERE minmax_stats_since >= :'ref_ts'\n  ) as minmax_stats_since_after_ref,\n  count(*) FILTER (\n    WHERE stats_since >= :'ref_ts'\n  ) as stats_since_after_ref\nFROM edb_stat_statements\nWHERE query LIKE '%STMTTS%';\n total | minmax_plan_zero | minmax_exec_zero | minmax_stats_since_after_ref | stats_since_after_ref \n-------+------------------+------------------+------------------------------+-----------------------\n     2 |                0 |                0 |                            0 |                     0\n(1 row)\n\n-- Perform single min/max reset\nSELECT edb_stat_statements_reset(0, '{}', queryid, true) AS minmax_reset_ts\nFROM edb_stat_statements\nWHERE query LIKE '%STMTTS1%' \\gset\n-- check\nSELECT\n  count(*) as total,\n  count(*) FILTER (\n    WHERE min_plan_time + max_plan_time = 0\n  ) as minmax_plan_zero,\n  count(*) FILTER (\n    WHERE min_exec_time + max_exec_time = 0\n  ) as minmax_exec_zero,\n  count(*) FILTER (\n    WHERE minmax_stats_since >= :'ref_ts'\n  ) as minmax_stats_since_after_ref,\n  count(*) FILTER (\n    WHERE stats_since >= :'ref_ts'\n  ) as stats_since_after_ref\nFROM edb_stat_statements\nWHERE query LIKE '%STMTTS%';\n total | minmax_plan_zero | minmax_exec_zero | minmax_stats_since_after_ref | stats_since_after_ref \n-------+------------------+------------------+------------------------------+-----------------------\n     2 |                1 |                1 |                            1 |                     0\n(1 row)\n\n-- check minmax reset timestamps\nSELECT\nquery, minmax_stats_since = :'minmax_reset_ts' AS reset_ts_match\nFROM edb_stat_statements\nWHERE query LIKE '%STMTTS%'\nORDER BY query COLLATE \"C\";\n           query           | reset_ts_match \n---------------------------+----------------\n SELECT $1 AS \"STMTTS1\"    | t\n SELECT $1,$2 AS \"STMTTS2\" | f\n(2 rows)\n\n-- check that minmax reset does not set stats_reset\nSELECT\nstats_reset = :'minmax_reset_ts' AS stats_reset_ts_match\nFROM edb_stat_statements_info;\n stats_reset_ts_match \n----------------------\n f\n(1 row)\n\n-- Perform common min/max reset\nSELECT edb_stat_statements_reset(0, '{}', 0, true) AS minmax_reset_ts \\gset\n-- check again\nSELECT\n  count(*) as total,\n  count(*) FILTER (\n    WHERE min_plan_time + max_plan_time = 0\n  ) as minmax_plan_zero,\n  count(*) FILTER (\n    WHERE min_exec_time + max_exec_time = 0\n  ) as minmax_exec_zero,\n  count(*) FILTER (\n    WHERE minmax_stats_since >= :'ref_ts'\n  ) as minmax_ts_after_ref,\n  count(*) FILTER (\n    WHERE minmax_stats_since = :'minmax_reset_ts'\n  ) as minmax_ts_match,\n  count(*) FILTER (\n    WHERE stats_since >= :'ref_ts'\n  ) as stats_since_after_ref\nFROM edb_stat_statements\nWHERE query LIKE '%STMTTS%';\n total | minmax_plan_zero | minmax_exec_zero | minmax_ts_after_ref | minmax_ts_match | stats_since_after_ref \n-------+------------------+------------------+---------------------+-----------------+-----------------------\n     2 |                2 |                2 |                   2 |               2 |                     0\n(1 row)\n\n-- Execute first query once more to check stats update\nSELECT 1 AS \"STMTTS1\";\n STMTTS1 \n---------\n       1\n(1 row)\n\n-- check\n-- we don't check planing times here to be independent of\n-- plan caching approach\nSELECT\n  count(*) as total,\n  count(*) FILTER (\n    WHERE min_exec_time + max_exec_time = 0\n  ) as minmax_exec_zero,\n  count(*) FILTER (\n    WHERE minmax_stats_since >= :'ref_ts'\n  ) as minmax_ts_after_ref,\n  count(*) FILTER (\n    WHERE stats_since >= :'ref_ts'\n  ) as stats_since_after_ref\nFROM edb_stat_statements\nWHERE query LIKE '%STMTTS%';\n total | minmax_exec_zero | minmax_ts_after_ref | stats_since_after_ref \n-------+------------------+---------------------+-----------------------\n     2 |                1 |                   2 |                     0\n(1 row)\n\n-- Cleanup\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n"
  },
  {
    "path": "edb_stat_statements/expected/extended.out",
    "content": "-- Tests with extended query protocol\nSET edb_stat_statements.track_utility = FALSE;\n-- This test checks that an execute message sets a query ID.\nSELECT query_id IS NOT NULL AS query_id_set\n  FROM pg_stat_activity WHERE pid = pg_backend_pid() \\bind \\g\n query_id_set \n--------------\n t\n(1 row)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nSELECT $1 \\parse stmt1\nSELECT $1, $2 \\parse stmt2\nSELECT $1, $2, $3 \\parse stmt3\nSELECT $1 \\bind 'unnamed_val1' \\g\n   ?column?   \n--------------\n unnamed_val1\n(1 row)\n\n\\bind_named stmt1 'stmt1_val1' \\g\n  ?column?  \n------------\n stmt1_val1\n(1 row)\n\n\\bind_named stmt2 'stmt2_val1' 'stmt2_val2' \\g\n  ?column?  |  ?column?  \n------------+------------\n stmt2_val1 | stmt2_val2\n(1 row)\n\n\\bind_named stmt3 'stmt3_val1' 'stmt3_val2' 'stmt3_val3' \\g\n  ?column?  |  ?column?  |  ?column?  \n------------+------------+------------\n stmt3_val1 | stmt3_val2 | stmt3_val3\n(1 row)\n\n\\bind_named stmt3 'stmt3_val4' 'stmt3_val5' 'stmt3_val6' \\g\n  ?column?  |  ?column?  |  ?column?  \n------------+------------+------------\n stmt3_val4 | stmt3_val5 | stmt3_val6\n(1 row)\n\n\\bind_named stmt2 'stmt2_val3' 'stmt2_val4' \\g\n  ?column?  |  ?column?  \n------------+------------\n stmt2_val3 | stmt2_val4\n(1 row)\n\n\\bind_named stmt1 'stmt1_val1' \\g\n  ?column?  \n------------\n stmt1_val1\n(1 row)\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                        query                        \n-------+------+-----------------------------------------------------\n     3 |    3 | SELECT $1\n     2 |    2 | SELECT $1, $2\n     2 |    2 | SELECT $1, $2, $3\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(4 rows)\n\n"
  },
  {
    "path": "edb_stat_statements/expected/level_tracking.out.17",
    "content": "--\n-- Statement level tracking\n--\nSET edb_stat_statements.track_utility = TRUE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- DO block - top-level tracking.\nCREATE TABLE stats_track_tab (x int);\nSET edb_stat_statements.track = 'dev';\nDELETE FROM stats_track_tab;\nDO $$\nBEGIN\n  DELETE FROM stats_track_tab;\nEND;\n$$ LANGUAGE plpgsql;\nSELECT toplevel, calls, query FROM edb_stat_statements\n  WHERE query LIKE '%DELETE%' ORDER BY query COLLATE \"C\", toplevel;\n toplevel | calls |             query              \n----------+-------+--------------------------------\n t        |     1 | DELETE FROM stats_track_tab\n t        |     1 | DO $$                         +\n          |       | BEGIN                         +\n          |       |   DELETE FROM stats_track_tab;+\n          |       | END;                          +\n          |       | $$ LANGUAGE plpgsql\n(2 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- DO block - all-level tracking.\nSET edb_stat_statements.track = 'dev-nested';\nDELETE FROM stats_track_tab;\nDO $$\nBEGIN\n  DELETE FROM stats_track_tab;\nEND; $$;\nDO LANGUAGE plpgsql $$\nBEGIN\n  -- this is a SELECT\n  PERFORM 'hello world'::TEXT;\nEND; $$;\nSELECT toplevel, calls, query FROM edb_stat_statements\n  ORDER BY query COLLATE \"C\", toplevel;\n toplevel | calls |                        query                        \n----------+-------+-----------------------------------------------------\n f        |     1 | DELETE FROM stats_track_tab\n t        |     1 | DELETE FROM stats_track_tab\n t        |     1 | DO $$                                              +\n          |       | BEGIN                                              +\n          |       |   DELETE FROM stats_track_tab;                     +\n          |       | END; $$\n t        |     1 | DO LANGUAGE plpgsql $$                             +\n          |       | BEGIN                                              +\n          |       |   -- this is a SELECT                              +\n          |       |   PERFORM 'hello world'::TEXT;                     +\n          |       | END; $$\n f        |     1 | SELECT $1::TEXT\n t        |     1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n t        |     1 | SET edb_stat_statements.track = 'dev-nested'\n(7 rows)\n\n-- Procedure with multiple utility statements.\nCREATE OR REPLACE PROCEDURE proc_with_utility_stmt()\nLANGUAGE SQL\nAS $$\n  SHOW edb_stat_statements.track;\n  show edb_stat_statements.track;\n  SHOW edb_stat_statements.track_utility;\n$$;\nSET edb_stat_statements.track_utility = TRUE;\n-- all-level tracking.\nSET edb_stat_statements.track = 'dev-nested';\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nCALL proc_with_utility_stmt();\nSELECT toplevel, calls, query FROM edb_stat_statements\n  ORDER BY query COLLATE \"C\", toplevel;\n toplevel | calls |                        query                        \n----------+-------+-----------------------------------------------------\n t        |     1 | CALL proc_with_utility_stmt()\n t        |     1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n f        |     2 | SHOW edb_stat_statements.track\n f        |     1 | SHOW edb_stat_statements.track_utility\n(4 rows)\n\n-- top-level tracking.\nSET edb_stat_statements.track = 'dev';\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nCALL proc_with_utility_stmt();\nSELECT toplevel, calls, query FROM edb_stat_statements\n  ORDER BY query COLLATE \"C\", toplevel;\n toplevel | calls |                        query                        \n----------+-------+-----------------------------------------------------\n t        |     1 | CALL proc_with_utility_stmt()\n t        |     1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(2 rows)\n\n-- DO block - top-level tracking without utility.\nSET edb_stat_statements.track = 'dev';\nSET edb_stat_statements.track_utility = FALSE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nDELETE FROM stats_track_tab;\nDO $$\nBEGIN\n  DELETE FROM stats_track_tab;\nEND; $$;\nDO LANGUAGE plpgsql $$\nBEGIN\n  -- this is a SELECT\n  PERFORM 'hello world'::TEXT;\nEND; $$;\nSELECT toplevel, calls, query FROM edb_stat_statements\n  ORDER BY query COLLATE \"C\", toplevel;\n toplevel | calls |                        query                        \n----------+-------+-----------------------------------------------------\n t        |     1 | DELETE FROM stats_track_tab\n t        |     1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(2 rows)\n\n-- DO block - all-level tracking without utility.\nSET edb_stat_statements.track = 'dev-nested';\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nDELETE FROM stats_track_tab;\nDO $$\nBEGIN\n  DELETE FROM stats_track_tab;\nEND; $$;\nDO LANGUAGE plpgsql $$\nBEGIN\n  -- this is a SELECT\n  PERFORM 'hello world'::TEXT;\nEND; $$;\nSELECT toplevel, calls, query FROM edb_stat_statements\n  ORDER BY query COLLATE \"C\", toplevel;\n toplevel | calls |                        query                        \n----------+-------+-----------------------------------------------------\n f        |     1 | DELETE FROM stats_track_tab\n t        |     1 | DELETE FROM stats_track_tab\n f        |     1 | SELECT $1::TEXT\n t        |     1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(4 rows)\n\n-- PL/pgSQL function - top-level tracking.\nSET edb_stat_statements.track = 'dev';\nSET edb_stat_statements.track_utility = FALSE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nCREATE FUNCTION PLUS_TWO(i INTEGER) RETURNS INTEGER AS $$\nDECLARE\n  r INTEGER;\nBEGIN\n  SELECT (i + 1 + 1.0)::INTEGER INTO r;\n  RETURN r;\nEND; $$ LANGUAGE plpgsql;\nSELECT PLUS_TWO(3);\n plus_two \n----------\n        5\n(1 row)\n\nSELECT PLUS_TWO(7);\n plus_two \n----------\n        9\n(1 row)\n\n-- SQL function --- use LIMIT to keep it from being inlined\nCREATE FUNCTION PLUS_ONE(i INTEGER) RETURNS INTEGER AS\n$$ SELECT (i + 1.0)::INTEGER LIMIT 1 $$ LANGUAGE SQL;\nSELECT PLUS_ONE(8);\n plus_one \n----------\n        9\n(1 row)\n\nSELECT PLUS_ONE(10);\n plus_one \n----------\n       11\n(1 row)\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                        query                        \n-------+------+-----------------------------------------------------\n     2 |    2 | SELECT PLUS_ONE($1)\n     2 |    2 | SELECT PLUS_TWO($1)\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(3 rows)\n\n-- immutable SQL function --- can be executed at plan time\nCREATE FUNCTION PLUS_THREE(i INTEGER) RETURNS INTEGER AS\n$$ SELECT i + 3 LIMIT 1 $$ IMMUTABLE LANGUAGE SQL;\nSELECT PLUS_THREE(8);\n plus_three \n------------\n         11\n(1 row)\n\nSELECT PLUS_THREE(10);\n plus_three \n------------\n         13\n(1 row)\n\nSELECT toplevel, calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n toplevel | calls | rows |                                     query                                     \n----------+-------+------+-------------------------------------------------------------------------------\n t        |     2 |    2 | SELECT PLUS_ONE($1)\n t        |     2 |    2 | SELECT PLUS_THREE($1)\n t        |     2 |    2 | SELECT PLUS_TWO($1)\n t        |     1 |    3 | SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\"\n t        |     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(5 rows)\n\n-- PL/pgSQL function - all-level tracking.\nSET edb_stat_statements.track = 'dev-nested';\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- we drop and recreate the functions to avoid any caching funnies\nDROP FUNCTION PLUS_ONE(INTEGER);\nDROP FUNCTION PLUS_TWO(INTEGER);\nDROP FUNCTION PLUS_THREE(INTEGER);\n-- PL/pgSQL function\nCREATE FUNCTION PLUS_TWO(i INTEGER) RETURNS INTEGER AS $$\nDECLARE\n  r INTEGER;\nBEGIN\n  SELECT (i + 1 + 1.0)::INTEGER INTO r;\n  RETURN r;\nEND; $$ LANGUAGE plpgsql;\nSELECT PLUS_TWO(-1);\n plus_two \n----------\n        1\n(1 row)\n\nSELECT PLUS_TWO(2);\n plus_two \n----------\n        4\n(1 row)\n\n-- SQL function --- use LIMIT to keep it from being inlined\nCREATE FUNCTION PLUS_ONE(i INTEGER) RETURNS INTEGER AS\n$$ SELECT (i + 1.0)::INTEGER LIMIT 1 $$ LANGUAGE SQL;\nSELECT PLUS_ONE(3);\n plus_one \n----------\n        4\n(1 row)\n\nSELECT PLUS_ONE(1);\n plus_one \n----------\n        2\n(1 row)\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                        query                        \n-------+------+-----------------------------------------------------\n     2 |    2 | SELECT (i + $2 + $3)::INTEGER\n     2 |    2 | SELECT (i + $2)::INTEGER LIMIT $3\n     2 |    2 | SELECT PLUS_ONE($1)\n     2 |    2 | SELECT PLUS_TWO($1)\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(5 rows)\n\n-- immutable SQL function --- can be executed at plan time\nCREATE FUNCTION PLUS_THREE(i INTEGER) RETURNS INTEGER AS\n$$ SELECT i + 3 LIMIT 1 $$ IMMUTABLE LANGUAGE SQL;\nSELECT PLUS_THREE(8);\n plus_three \n------------\n         11\n(1 row)\n\nSELECT PLUS_THREE(10);\n plus_three \n------------\n         13\n(1 row)\n\nSELECT toplevel, calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n toplevel | calls | rows |                                     query                                     \n----------+-------+------+-------------------------------------------------------------------------------\n f        |     2 |    2 | SELECT (i + $2 + $3)::INTEGER\n f        |     2 |    2 | SELECT (i + $2)::INTEGER LIMIT $3\n t        |     2 |    2 | SELECT PLUS_ONE($1)\n t        |     2 |    2 | SELECT PLUS_THREE($1)\n t        |     2 |    2 | SELECT PLUS_TWO($1)\n t        |     1 |    5 | SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\"\n t        |     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n f        |     2 |    2 | SELECT i + $2 LIMIT $3\n(8 rows)\n\n--\n-- edb_stat_statements.track = none\n--\nSET edb_stat_statements.track = 'none';\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nSELECT 1 AS \"one\";\n one \n-----\n   1\n(1 row)\n\nSELECT 1 + 1 AS \"two\";\n two \n-----\n   2\n(1 row)\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows | query \n-------+------+-------\n(0 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n"
  },
  {
    "path": "edb_stat_statements/expected/level_tracking.out.18",
    "content": "--\n-- Statement level tracking\n--\nSET edb_stat_statements.track_utility = TRUE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- DO block - top-level tracking.\nCREATE TABLE stats_track_tab (x int);\nSET edb_stat_statements.track = 'dev';\nDELETE FROM stats_track_tab;\nDO $$\nBEGIN\n  DELETE FROM stats_track_tab;\nEND;\n$$ LANGUAGE plpgsql;\nSELECT toplevel, calls, query FROM edb_stat_statements\n  WHERE query LIKE '%DELETE%' ORDER BY query COLLATE \"C\", toplevel;\n toplevel | calls |             query              \n----------+-------+--------------------------------\n t        |     1 | DELETE FROM stats_track_tab\n t        |     1 | DO $$                         +\n          |       | BEGIN                         +\n          |       |   DELETE FROM stats_track_tab;+\n          |       | END;                          +\n          |       | $$ LANGUAGE plpgsql\n(2 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- DO block - all-level tracking.\nSET edb_stat_statements.track = 'dev-nested';\nDELETE FROM stats_track_tab;\nDO $$\nBEGIN\n  DELETE FROM stats_track_tab;\nEND; $$;\nDO LANGUAGE plpgsql $$\nBEGIN\n  -- this is a SELECT\n  PERFORM 'hello world'::TEXT;\nEND; $$;\nSELECT toplevel, calls, query FROM edb_stat_statements\n  ORDER BY query COLLATE \"C\", toplevel;\n toplevel | calls |                        query                        \n----------+-------+-----------------------------------------------------\n f        |     1 | DELETE FROM stats_track_tab\n t        |     1 | DELETE FROM stats_track_tab\n t        |     1 | DO $$                                              +\n          |       | BEGIN                                              +\n          |       |   DELETE FROM stats_track_tab;                     +\n          |       | END; $$\n t        |     1 | DO LANGUAGE plpgsql $$                             +\n          |       | BEGIN                                              +\n          |       |   -- this is a SELECT                              +\n          |       |   PERFORM 'hello world'::TEXT;                     +\n          |       | END; $$\n f        |     1 | SELECT $1::TEXT\n t        |     1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n t        |     1 | SET edb_stat_statements.track = $1\n(7 rows)\n\n-- Procedure with multiple utility statements.\nCREATE OR REPLACE PROCEDURE proc_with_utility_stmt()\nLANGUAGE SQL\nAS $$\n  SHOW edb_stat_statements.track;\n  show edb_stat_statements.track;\n  SHOW edb_stat_statements.track_utility;\n$$;\nSET edb_stat_statements.track_utility = TRUE;\n-- all-level tracking.\nSET edb_stat_statements.track = 'dev-nested';\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nCALL proc_with_utility_stmt();\nSELECT toplevel, calls, query FROM edb_stat_statements\n  ORDER BY query COLLATE \"C\", toplevel;\n toplevel | calls |                        query                        \n----------+-------+-----------------------------------------------------\n t        |     1 | CALL proc_with_utility_stmt()\n t        |     1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n f        |     2 | SHOW edb_stat_statements.track\n f        |     1 | SHOW edb_stat_statements.track_utility\n(4 rows)\n\n-- top-level tracking.\nSET edb_stat_statements.track = 'dev';\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nCALL proc_with_utility_stmt();\nSELECT toplevel, calls, query FROM edb_stat_statements\n  ORDER BY query COLLATE \"C\", toplevel;\n toplevel | calls |                        query                        \n----------+-------+-----------------------------------------------------\n t        |     1 | CALL proc_with_utility_stmt()\n t        |     1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(2 rows)\n\n-- DO block - top-level tracking without utility.\nSET edb_stat_statements.track = 'dev';\nSET edb_stat_statements.track_utility = FALSE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nDELETE FROM stats_track_tab;\nDO $$\nBEGIN\n  DELETE FROM stats_track_tab;\nEND; $$;\nDO LANGUAGE plpgsql $$\nBEGIN\n  -- this is a SELECT\n  PERFORM 'hello world'::TEXT;\nEND; $$;\nSELECT toplevel, calls, query FROM edb_stat_statements\n  ORDER BY query COLLATE \"C\", toplevel;\n toplevel | calls |                        query                        \n----------+-------+-----------------------------------------------------\n t        |     1 | DELETE FROM stats_track_tab\n t        |     1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(2 rows)\n\n-- DO block - all-level tracking without utility.\nSET edb_stat_statements.track = 'dev-nested';\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nDELETE FROM stats_track_tab;\nDO $$\nBEGIN\n  DELETE FROM stats_track_tab;\nEND; $$;\nDO LANGUAGE plpgsql $$\nBEGIN\n  -- this is a SELECT\n  PERFORM 'hello world'::TEXT;\nEND; $$;\nSELECT toplevel, calls, query FROM edb_stat_statements\n  ORDER BY query COLLATE \"C\", toplevel;\n toplevel | calls |                        query                        \n----------+-------+-----------------------------------------------------\n f        |     1 | DELETE FROM stats_track_tab\n t        |     1 | DELETE FROM stats_track_tab\n f        |     1 | SELECT $1::TEXT\n t        |     1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(4 rows)\n\n-- PL/pgSQL function - top-level tracking.\nSET edb_stat_statements.track = 'dev';\nSET edb_stat_statements.track_utility = FALSE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nCREATE FUNCTION PLUS_TWO(i INTEGER) RETURNS INTEGER AS $$\nDECLARE\n  r INTEGER;\nBEGIN\n  SELECT (i + 1 + 1.0)::INTEGER INTO r;\n  RETURN r;\nEND; $$ LANGUAGE plpgsql;\nSELECT PLUS_TWO(3);\n plus_two \n----------\n        5\n(1 row)\n\nSELECT PLUS_TWO(7);\n plus_two \n----------\n        9\n(1 row)\n\n-- SQL function --- use LIMIT to keep it from being inlined\nCREATE FUNCTION PLUS_ONE(i INTEGER) RETURNS INTEGER AS\n$$ SELECT (i + 1.0)::INTEGER LIMIT 1 $$ LANGUAGE SQL;\nSELECT PLUS_ONE(8);\n plus_one \n----------\n        9\n(1 row)\n\nSELECT PLUS_ONE(10);\n plus_one \n----------\n       11\n(1 row)\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                        query                        \n-------+------+-----------------------------------------------------\n     2 |    2 | SELECT PLUS_ONE($1)\n     2 |    2 | SELECT PLUS_TWO($1)\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(3 rows)\n\n-- immutable SQL function --- can be executed at plan time\nCREATE FUNCTION PLUS_THREE(i INTEGER) RETURNS INTEGER AS\n$$ SELECT i + 3 LIMIT 1 $$ IMMUTABLE LANGUAGE SQL;\nSELECT PLUS_THREE(8);\n plus_three \n------------\n         11\n(1 row)\n\nSELECT PLUS_THREE(10);\n plus_three \n------------\n         13\n(1 row)\n\nSELECT toplevel, calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n toplevel | calls | rows |                                     query                                     \n----------+-------+------+-------------------------------------------------------------------------------\n t        |     2 |    2 | SELECT PLUS_ONE($1)\n t        |     2 |    2 | SELECT PLUS_THREE($1)\n t        |     2 |    2 | SELECT PLUS_TWO($1)\n t        |     1 |    3 | SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\"\n t        |     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(5 rows)\n\n-- PL/pgSQL function - all-level tracking.\nSET edb_stat_statements.track = 'dev-nested';\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- we drop and recreate the functions to avoid any caching funnies\nDROP FUNCTION PLUS_ONE(INTEGER);\nDROP FUNCTION PLUS_TWO(INTEGER);\nDROP FUNCTION PLUS_THREE(INTEGER);\n-- PL/pgSQL function\nCREATE FUNCTION PLUS_TWO(i INTEGER) RETURNS INTEGER AS $$\nDECLARE\n  r INTEGER;\nBEGIN\n  SELECT (i + 1 + 1.0)::INTEGER INTO r;\n  RETURN r;\nEND; $$ LANGUAGE plpgsql;\nSELECT PLUS_TWO(-1);\n plus_two \n----------\n        1\n(1 row)\n\nSELECT PLUS_TWO(2);\n plus_two \n----------\n        4\n(1 row)\n\n-- SQL function --- use LIMIT to keep it from being inlined\nCREATE FUNCTION PLUS_ONE(i INTEGER) RETURNS INTEGER AS\n$$ SELECT (i + 1.0)::INTEGER LIMIT 1 $$ LANGUAGE SQL;\nSELECT PLUS_ONE(3);\n plus_one \n----------\n        4\n(1 row)\n\nSELECT PLUS_ONE(1);\n plus_one \n----------\n        2\n(1 row)\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                        query                        \n-------+------+-----------------------------------------------------\n     2 |    2 | SELECT (i + $2 + $3)::INTEGER\n     2 |    2 | SELECT (i + $2)::INTEGER LIMIT $3\n     2 |    2 | SELECT PLUS_ONE($1)\n     2 |    2 | SELECT PLUS_TWO($1)\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(5 rows)\n\n-- immutable SQL function --- can be executed at plan time\nCREATE FUNCTION PLUS_THREE(i INTEGER) RETURNS INTEGER AS\n$$ SELECT i + 3 LIMIT 1 $$ IMMUTABLE LANGUAGE SQL;\nSELECT PLUS_THREE(8);\n plus_three \n------------\n         11\n(1 row)\n\nSELECT PLUS_THREE(10);\n plus_three \n------------\n         13\n(1 row)\n\nSELECT toplevel, calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n toplevel | calls | rows |                                     query                                     \n----------+-------+------+-------------------------------------------------------------------------------\n f        |     2 |    2 | SELECT (i + $2 + $3)::INTEGER\n f        |     2 |    2 | SELECT (i + $2)::INTEGER LIMIT $3\n t        |     2 |    2 | SELECT PLUS_ONE($1)\n t        |     2 |    2 | SELECT PLUS_THREE($1)\n t        |     2 |    2 | SELECT PLUS_TWO($1)\n t        |     1 |    5 | SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\"\n t        |     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n f        |     2 |    2 | SELECT i + $2 LIMIT $3\n(8 rows)\n\n--\n-- edb_stat_statements.track = none\n--\nSET edb_stat_statements.track = 'none';\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nSELECT 1 AS \"one\";\n one \n-----\n   1\n(1 row)\n\nSELECT 1 + 1 AS \"two\";\n two \n-----\n   2\n(1 row)\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows | query \n-------+------+-------\n(0 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n"
  },
  {
    "path": "edb_stat_statements/expected/oldextversions.out",
    "content": "-- test old extension version entry points\nCREATE EXTENSION edb_stat_statements WITH VERSION '1.0';\nSELECT pg_get_functiondef('edb_stat_statements_info'::regproc);\n                                                    pg_get_functiondef                                                    \n--------------------------------------------------------------------------------------------------------------------------\n CREATE OR REPLACE FUNCTION public.edb_stat_statements_info(OUT dealloc bigint, OUT stats_reset timestamp with time zone)+\n  RETURNS record                                                                                                         +\n  LANGUAGE c                                                                                                             +\n  PARALLEL SAFE STRICT                                                                                                   +\n AS '$libdir/edb_stat_statements', $function$edb_stat_statements_info$function$                                          +\n \n(1 row)\n\nSELECT pg_get_functiondef('edb_stat_statements_reset'::regproc);\n                                                                               pg_get_functiondef                                                                                \n---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n CREATE OR REPLACE FUNCTION public.edb_stat_statements_reset(userid oid DEFAULT 0, dbids oid[] DEFAULT '{}'::oid[], queryid bigint DEFAULT 0, minmax_only boolean DEFAULT false)+\n  RETURNS timestamp with time zone                                                                                                                                              +\n  LANGUAGE c                                                                                                                                                                    +\n  PARALLEL SAFE STRICT                                                                                                                                                          +\n AS '$libdir/edb_stat_statements', $function$edb_stat_statements_reset$function$                                                                                                +\n \n(1 row)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n\\d edb_stat_statements\n                           View \"public.edb_stat_statements\"\n           Column           |           Type           | Collation | Nullable | Default \n----------------------------+--------------------------+-----------+----------+---------\n userid                     | oid                      |           |          | \n dbid                       | oid                      |           |          | \n toplevel                   | boolean                  |           |          | \n queryid                    | bigint                   |           |          | \n query                      | text                     |           |          | \n extras                     | jsonb                    |           |          | \n tag                        | text                     |           |          | \n id                         | uuid                     |           |          | \n stmt_type                  | smallint                 |           |          | \n plans                      | bigint                   |           |          | \n total_plan_time            | double precision         |           |          | \n min_plan_time              | double precision         |           |          | \n max_plan_time              | double precision         |           |          | \n mean_plan_time             | double precision         |           |          | \n stddev_plan_time           | double precision         |           |          | \n calls                      | bigint                   |           |          | \n total_exec_time            | double precision         |           |          | \n min_exec_time              | double precision         |           |          | \n max_exec_time              | double precision         |           |          | \n mean_exec_time             | double precision         |           |          | \n stddev_exec_time           | double precision         |           |          | \n rows                       | bigint                   |           |          | \n shared_blks_hit            | bigint                   |           |          | \n shared_blks_read           | bigint                   |           |          | \n shared_blks_dirtied        | bigint                   |           |          | \n shared_blks_written        | bigint                   |           |          | \n local_blks_hit             | bigint                   |           |          | \n local_blks_read            | bigint                   |           |          | \n local_blks_dirtied         | bigint                   |           |          | \n local_blks_written         | bigint                   |           |          | \n temp_blks_read             | bigint                   |           |          | \n temp_blks_written          | bigint                   |           |          | \n shared_blk_read_time       | double precision         |           |          | \n shared_blk_write_time      | double precision         |           |          | \n local_blk_read_time        | double precision         |           |          | \n local_blk_write_time       | double precision         |           |          | \n temp_blk_read_time         | double precision         |           |          | \n temp_blk_write_time        | double precision         |           |          | \n wal_records                | bigint                   |           |          | \n wal_fpi                    | bigint                   |           |          | \n wal_bytes                  | numeric                  |           |          | \n jit_functions              | bigint                   |           |          | \n jit_generation_time        | double precision         |           |          | \n jit_inlining_count         | bigint                   |           |          | \n jit_inlining_time          | double precision         |           |          | \n jit_optimization_count     | bigint                   |           |          | \n jit_optimization_time      | double precision         |           |          | \n jit_emission_count         | bigint                   |           |          | \n jit_emission_time          | double precision         |           |          | \n jit_deform_count           | bigint                   |           |          | \n jit_deform_time            | double precision         |           |          | \n parallel_workers_to_launch | bigint                   |           |          | \n parallel_workers_launched  | bigint                   |           |          | \n stats_since                | timestamp with time zone |           |          | \n minmax_stats_since         | timestamp with time zone |           |          | \n\nSELECT count(*) > 0 AS has_data FROM edb_stat_statements;\n has_data \n----------\n t\n(1 row)\n\nDROP EXTENSION edb_stat_statements;\n"
  },
  {
    "path": "edb_stat_statements/expected/parallel.out.17",
    "content": "--\n-- Tests for parallel statistics\n--\nSET edb_stat_statements.track_utility = FALSE;\n-- encourage use of parallel plans\nSET parallel_setup_cost = 0;\nSET parallel_tuple_cost = 0;\nSET min_parallel_table_scan_size = 0;\nSET max_parallel_workers_per_gather = 2;\nCREATE TABLE pgss_parallel_tab (a int);\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nSELECT count(*) FROM pgss_parallel_tab;\n count \n-------\n     0\n(1 row)\n\nSELECT query,\n  parallel_workers_to_launch > 0 AS has_workers_to_launch,\n  parallel_workers_launched > 0 AS has_workers_launched\n  FROM edb_stat_statements\n  WHERE query ~ 'SELECT count'\n  ORDER BY query COLLATE \"C\";\n                 query                  | has_workers_to_launch | has_workers_launched \n----------------------------------------+-----------------------+----------------------\n SELECT count(*) FROM pgss_parallel_tab | f                     | f\n(1 row)\n\nDROP TABLE pgss_parallel_tab;\n"
  },
  {
    "path": "edb_stat_statements/expected/parallel.out.18",
    "content": "--\n-- Tests for parallel statistics\n--\nSET edb_stat_statements.track_utility = FALSE;\n-- encourage use of parallel plans\nSET parallel_setup_cost = 0;\nSET parallel_tuple_cost = 0;\nSET min_parallel_table_scan_size = 0;\nSET max_parallel_workers_per_gather = 2;\nCREATE TABLE pgss_parallel_tab (a int);\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nSELECT count(*) FROM pgss_parallel_tab;\n count \n-------\n     0\n(1 row)\n\nSELECT query,\n  parallel_workers_to_launch > 0 AS has_workers_to_launch,\n  parallel_workers_launched > 0 AS has_workers_launched\n  FROM edb_stat_statements\n  WHERE query ~ 'SELECT count'\n  ORDER BY query COLLATE \"C\";\n                 query                  | has_workers_to_launch | has_workers_launched \n----------------------------------------+-----------------------+----------------------\n SELECT count(*) FROM pgss_parallel_tab | t                     | t\n(1 row)\n\nDROP TABLE pgss_parallel_tab;\n"
  },
  {
    "path": "edb_stat_statements/expected/planning.out",
    "content": "--\n-- Information related to planning\n--\n-- These tests require track_planning to be enabled.\nSET edb_stat_statements.track_planning = TRUE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n--\n-- [re]plan counting\n--\nCREATE TABLE stats_plan_test ();\nPREPARE prep1 AS SELECT COUNT(*) FROM stats_plan_test;\nEXECUTE prep1;\n count \n-------\n     0\n(1 row)\n\nEXECUTE prep1;\n count \n-------\n     0\n(1 row)\n\nEXECUTE prep1;\n count \n-------\n     0\n(1 row)\n\nALTER TABLE stats_plan_test ADD COLUMN x int;\nEXECUTE prep1;\n count \n-------\n     0\n(1 row)\n\nSELECT 42;\n ?column? \n----------\n       42\n(1 row)\n\nSELECT 42;\n ?column? \n----------\n       42\n(1 row)\n\nSELECT 42;\n ?column? \n----------\n       42\n(1 row)\n\nSELECT plans, calls, rows, query FROM edb_stat_statements\n  WHERE query NOT LIKE 'PREPARE%' ORDER BY query COLLATE \"C\";\n plans | calls | rows |                           query                           \n-------+-------+------+-----------------------------------------------------------\n     0 |     1 |    0 | ALTER TABLE stats_plan_test ADD COLUMN x int\n     0 |     1 |    0 | CREATE TABLE stats_plan_test ()\n     3 |     3 |    3 | SELECT $1\n     0 |     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n     1 |     0 |    0 | SELECT plans, calls, rows, query FROM edb_stat_statements+\n       |       |      |   WHERE query NOT LIKE $1 ORDER BY query COLLATE \"C\"\n(5 rows)\n\n-- for the prepared statement we expect at least one replan, but cache\n-- invalidations could force more\nSELECT plans >= 2 AND plans <= calls AS plans_ok, calls, rows, query FROM edb_stat_statements\n  WHERE query LIKE 'PREPARE%' ORDER BY query COLLATE \"C\";\n plans_ok | calls | rows |                         query                         \n----------+-------+------+-------------------------------------------------------\n t        |     4 |    4 | PREPARE prep1 AS SELECT COUNT(*) FROM stats_plan_test\n(1 row)\n\n-- Cleanup\nDROP TABLE stats_plan_test;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n"
  },
  {
    "path": "edb_stat_statements/expected/privileges.out",
    "content": "--\n-- Only superusers and roles with privileges of the pg_read_all_stats role\n-- are allowed to see the SQL text and queryid of queries executed by\n-- other users. Other users can see the statistics.\n--\nSET edb_stat_statements.track_utility = FALSE;\nCREATE ROLE regress_stats_superuser SUPERUSER;\nCREATE ROLE regress_stats_user1;\nCREATE ROLE regress_stats_user2;\nGRANT pg_read_all_stats TO regress_stats_user2;\nSET ROLE regress_stats_superuser;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nSELECT 1 AS \"ONE\";\n ONE \n-----\n   1\n(1 row)\n\nSET ROLE regress_stats_user1;\nSELECT 1+1 AS \"TWO\";\n TWO \n-----\n   2\n(1 row)\n\n--\n-- A superuser can read all columns of queries executed by others,\n-- including query text and queryid.\n--\nSET ROLE regress_stats_superuser;\nSELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.query, ss.calls, ss.rows\n  FROM edb_stat_statements ss JOIN pg_roles r ON ss.userid = r.oid\n  ORDER BY r.rolname, ss.query COLLATE \"C\", ss.calls, ss.rows;\n         rolname         | queryid_bool |                        query                        | calls | rows \n-------------------------+--------------+-----------------------------------------------------+-------+------\n regress_stats_superuser | t            | SELECT $1 AS \"ONE\"                                  |     1 |    1\n regress_stats_superuser | t            | SELECT edb_stat_statements_reset() IS NOT NULL AS t |     1 |    1\n regress_stats_user1     | t            | SELECT $1+$2 AS \"TWO\"                               |     1 |    1\n(3 rows)\n\n--\n-- regress_stats_user1 has no privileges to read the query text or\n-- queryid of queries executed by others but can see statistics\n-- like calls and rows.\n--\nSET ROLE regress_stats_user1;\nSELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.query, ss.calls, ss.rows\n  FROM edb_stat_statements ss JOIN pg_roles r ON ss.userid = r.oid\n  ORDER BY r.rolname, ss.query COLLATE \"C\", ss.calls, ss.rows;\n         rolname         | queryid_bool |          query           | calls | rows \n-------------------------+--------------+--------------------------+-------+------\n regress_stats_superuser |              | <insufficient privilege> |     1 |    1\n regress_stats_superuser |              | <insufficient privilege> |     1 |    1\n regress_stats_superuser |              | <insufficient privilege> |     1 |    3\n regress_stats_user1     | t            | SELECT $1+$2 AS \"TWO\"    |     1 |    1\n(4 rows)\n\n--\n-- regress_stats_user2, with pg_read_all_stats role privileges, can\n-- read all columns, including query text and queryid, of queries\n-- executed by others.\n--\nSET ROLE regress_stats_user2;\nSELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.query, ss.calls, ss.rows\n  FROM edb_stat_statements ss JOIN pg_roles r ON ss.userid = r.oid\n  ORDER BY r.rolname, ss.query COLLATE \"C\", ss.calls, ss.rows;\n         rolname         | queryid_bool |                                      query                                      | calls | rows \n-------------------------+--------------+---------------------------------------------------------------------------------+-------+------\n regress_stats_superuser | t            | SELECT $1 AS \"ONE\"                                                              |     1 |    1\n regress_stats_superuser | t            | SELECT edb_stat_statements_reset() IS NOT NULL AS t                             |     1 |    1\n regress_stats_superuser | t            | SELECT r.rolname, ss.queryid <> $1 AS queryid_bool, ss.query, ss.calls, ss.rows+|     1 |    3\n                         |              |   FROM edb_stat_statements ss JOIN pg_roles r ON ss.userid = r.oid             +|       | \n                         |              |   ORDER BY r.rolname, ss.query COLLATE \"C\", ss.calls, ss.rows                   |       | \n regress_stats_user1     | t            | SELECT $1+$2 AS \"TWO\"                                                           |     1 |    1\n regress_stats_user1     | t            | SELECT r.rolname, ss.queryid <> $1 AS queryid_bool, ss.query, ss.calls, ss.rows+|     1 |    4\n                         |              |   FROM edb_stat_statements ss JOIN pg_roles r ON ss.userid = r.oid             +|       | \n                         |              |   ORDER BY r.rolname, ss.query COLLATE \"C\", ss.calls, ss.rows                   |       | \n(5 rows)\n\n--\n-- cleanup\n--\nRESET ROLE;\nDROP ROLE regress_stats_superuser;\nDROP ROLE regress_stats_user1;\nDROP ROLE regress_stats_user2;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n"
  },
  {
    "path": "edb_stat_statements/expected/select.out",
    "content": "--\n-- SELECT statements\n--\nCREATE EXTENSION edb_stat_statements;\nSET edb_stat_statements.track_utility = FALSE;\nSET edb_stat_statements.track_planning = TRUE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n--\n-- simple and compound statements\n--\nSELECT 1 AS \"int\";\n int \n-----\n   1\n(1 row)\n\nSELECT 'hello'\n  -- multiline\n  AS \"text\";\n text  \n-------\n hello\n(1 row)\n\nSELECT 'world' AS \"text\";\n text  \n-------\n world\n(1 row)\n\n-- transaction\nBEGIN;\nSELECT 1 AS \"int\";\n int \n-----\n   1\n(1 row)\n\nSELECT 'hello' AS \"text\";\n text  \n-------\n hello\n(1 row)\n\nCOMMIT;\n-- compound transaction\nBEGIN \\;\nSELECT 2.0 AS \"float\" \\;\nSELECT 'world' AS \"text\" \\;\nCOMMIT;\n float \n-------\n   2.0\n(1 row)\n\n text  \n-------\n world\n(1 row)\n\n-- compound with empty statements and spurious leading spacing\n\\;\\;   SELECT 3 + 3 \\;\\;\\;   SELECT ' ' || ' !' \\;\\;   SELECT 1 + 4 \\;;\n ?column? \n----------\n        6\n(1 row)\n\n ?column? \n----------\n   !\n(1 row)\n\n ?column? \n----------\n        5\n(1 row)\n\n-- non ;-terminated statements\nSELECT 1 + 1 + 1 AS \"add\" \\gset\nSELECT :add + 1 + 1 AS \"add\" \\;\nSELECT :add + 1 + 1 AS \"add\" \\gset\n add \n-----\n   5\n(1 row)\n\n-- set operator\nSELECT 1 AS i UNION SELECT 2 ORDER BY i;\n i \n---\n 1\n 2\n(2 rows)\n\n-- ? operator\nselect '{\"a\":1, \"b\":2}'::jsonb ? 'b';\n ?column? \n----------\n t\n(1 row)\n\n-- cte\nWITH t(f) AS (\n  VALUES (1.0), (2.0)\n)\n  SELECT f FROM t ORDER BY f;\n  f  \n-----\n 1.0\n 2.0\n(2 rows)\n\n-- prepared statement with parameter\nPREPARE pgss_test (int) AS SELECT $1, 'test' LIMIT 1;\nEXECUTE pgss_test(1);\n ?column? | ?column? \n----------+----------\n        1 | test\n(1 row)\n\nDEALLOCATE pgss_test;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                                     query                                     \n-------+------+-------------------------------------------------------------------------------\n     1 |    1 | PREPARE pgss_test (int) AS SELECT $1, $2 LIMIT $3\n     4 |    4 | SELECT $1                                                                    +\n       |      |   -- multiline                                                               +\n       |      |   AS \"text\"\n     2 |    2 | SELECT $1 + $2\n     3 |    3 | SELECT $1 + $2 + $3 AS \"add\"\n     1 |    1 | SELECT $1 AS \"float\"\n     2 |    2 | SELECT $1 AS \"int\"\n     1 |    2 | SELECT $1 AS i UNION SELECT $2 ORDER BY i\n     1 |    1 | SELECT $1 || $2\n     0 |    0 | SELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\"\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n     1 |    2 | WITH t(f) AS (                                                               +\n       |      |   VALUES ($1), ($2)                                                          +\n       |      | )                                                                            +\n       |      |   SELECT f FROM t ORDER BY f\n     1 |    1 | select $1::jsonb ? $2\n(12 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n--\n-- queries with locking clauses\n--\nCREATE TABLE pgss_a (id integer PRIMARY KEY);\nCREATE TABLE pgss_b (id integer PRIMARY KEY, a_id integer REFERENCES pgss_a);\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- control query\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id;\n id | id | a_id \n----+----+------\n(0 rows)\n\n-- test range tables\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE;\n id | id | a_id \n----+----+------\n(0 rows)\n\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_a;\n id | id | a_id \n----+----+------\n(0 rows)\n\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_b;\n id | id | a_id \n----+----+------\n(0 rows)\n\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_a, pgss_b; -- matches plain \"FOR UPDATE\"\n id | id | a_id \n----+----+------\n(0 rows)\n\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_b, pgss_a;\n id | id | a_id \n----+----+------\n(0 rows)\n\n-- test strengths\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR NO KEY UPDATE;\n id | id | a_id \n----+----+------\n(0 rows)\n\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR SHARE;\n id | id | a_id \n----+----+------\n(0 rows)\n\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR KEY SHARE;\n id | id | a_id \n----+----+------\n(0 rows)\n\n-- test wait policies\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE NOWAIT;\n id | id | a_id \n----+----+------\n(0 rows)\n\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE SKIP LOCKED;\n id | id | a_id \n----+----+------\n(0 rows)\n\nSELECT calls, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls |                                          query                                           \n-------+------------------------------------------------------------------------------------------\n     1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id\n     1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR KEY SHARE\n     1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR NO KEY UPDATE\n     1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR SHARE\n     2 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE\n     1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE NOWAIT\n     1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_a\n     1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_b\n     1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_b, pgss_a\n     1 | SELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE SKIP LOCKED\n     0 | SELECT calls, query FROM edb_stat_statements ORDER BY query COLLATE \"C\"\n     1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(12 rows)\n\nDROP TABLE pgss_a, pgss_b CASCADE;\n--\n-- access to edb_stat_statements_info view\n--\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nSELECT dealloc FROM edb_stat_statements_info;\n dealloc \n---------\n       0\n(1 row)\n\n-- FROM [ONLY]\nCREATE TABLE tbl_inh(id integer);\nCREATE TABLE tbl_inh_1() INHERITS (tbl_inh);\nINSERT INTO tbl_inh_1 SELECT 1;\nSELECT * FROM tbl_inh;\n id \n----\n  1\n(1 row)\n\nSELECT * FROM ONLY tbl_inh;\n id \n----\n(0 rows)\n\nSELECT COUNT(*) FROM edb_stat_statements WHERE query LIKE '%FROM%tbl_inh%';\n count \n-------\n     2\n(1 row)\n\n-- WITH TIES\nCREATE TABLE limitoption AS SELECT 0 AS val FROM generate_series(1, 10);\nSELECT *\nFROM limitoption\nWHERE val < 2\nORDER BY val\nFETCH FIRST 2 ROWS WITH TIES;\n val \n-----\n   0\n   0\n   0\n   0\n   0\n   0\n   0\n   0\n   0\n   0\n(10 rows)\n\nSELECT *\nFROM limitoption\nWHERE val < 2\nORDER BY val\nFETCH FIRST 2 ROW ONLY;\n val \n-----\n   0\n   0\n(2 rows)\n\nSELECT COUNT(*) FROM edb_stat_statements WHERE query LIKE '%FETCH FIRST%';\n count \n-------\n     2\n(1 row)\n\n-- GROUP BY [DISTINCT]\nSELECT a, b, c\nFROM (VALUES (1, 2, 3), (4, NULL, 6), (7, 8, 9)) AS t (a, b, c)\nGROUP BY ROLLUP(a, b), rollup(a, c)\nORDER BY a, b, c;\n a | b | c \n---+---+---\n 1 | 2 | 3\n 1 | 2 |  \n 1 | 2 |  \n 1 |   | 3\n 1 |   | 3\n 1 |   |  \n 1 |   |  \n 1 |   |  \n 4 |   | 6\n 4 |   | 6\n 4 |   | 6\n 4 |   |  \n 4 |   |  \n 4 |   |  \n 4 |   |  \n 4 |   |  \n 7 | 8 | 9\n 7 | 8 |  \n 7 | 8 |  \n 7 |   | 9\n 7 |   | 9\n 7 |   |  \n 7 |   |  \n 7 |   |  \n   |   |  \n(25 rows)\n\nSELECT a, b, c\nFROM (VALUES (1, 2, 3), (4, NULL, 6), (7, 8, 9)) AS t (a, b, c)\nGROUP BY DISTINCT ROLLUP(a, b), rollup(a, c)\nORDER BY a, b, c;\n a | b | c \n---+---+---\n 1 | 2 | 3\n 1 | 2 |  \n 1 |   | 3\n 1 |   |  \n 4 |   | 6\n 4 |   | 6\n 4 |   |  \n 4 |   |  \n 7 | 8 | 9\n 7 | 8 |  \n 7 |   | 9\n 7 |   |  \n   |   |  \n(13 rows)\n\nSELECT COUNT(*) FROM edb_stat_statements WHERE query LIKE '%GROUP BY%ROLLUP%';\n count \n-------\n     2\n(1 row)\n\n-- GROUPING SET agglevelsup\nSELECT (\n  SELECT (\n    SELECT GROUPING(a,b) FROM (VALUES (1)) v2(c)\n  ) FROM (VALUES (1,2)) v1(a,b) GROUP BY (a,b)\n) FROM (VALUES(6,7)) v3(e,f) GROUP BY ROLLUP(e,f);\n grouping \n----------\n        0\n        0\n        0\n(3 rows)\n\nSELECT (\n  SELECT (\n    SELECT GROUPING(e,f) FROM (VALUES (1)) v2(c)\n  ) FROM (VALUES (1,2)) v1(a,b) GROUP BY (a,b)\n) FROM (VALUES(6,7)) v3(e,f) GROUP BY ROLLUP(e,f);\n grouping \n----------\n        3\n        0\n        1\n(3 rows)\n\nSELECT COUNT(*) FROM edb_stat_statements WHERE query LIKE '%SELECT GROUPING%';\n count \n-------\n     2\n(1 row)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n"
  },
  {
    "path": "edb_stat_statements/expected/user_activity.out",
    "content": "--\n-- Track user activity and reset them\n--\nSET edb_stat_statements.track_utility = TRUE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nCREATE ROLE regress_stats_user1;\nCREATE ROLE regress_stats_user2;\nSET ROLE regress_stats_user1;\nSELECT 1 AS \"ONE\";\n ONE \n-----\n   1\n(1 row)\n\nSELECT 1+1 AS \"TWO\";\n TWO \n-----\n   2\n(1 row)\n\nRESET ROLE;\nSET ROLE regress_stats_user2;\nSELECT 1 AS \"ONE\";\n ONE \n-----\n   1\n(1 row)\n\nSELECT 1+1 AS \"TWO\";\n TWO \n-----\n   2\n(1 row)\n\nRESET ROLE;\nSELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n                        query                        | calls | rows \n-----------------------------------------------------+-------+------\n CREATE ROLE regress_stats_user1                     |     1 |    0\n CREATE ROLE regress_stats_user2                     |     1 |    0\n RESET ROLE                                          |     2 |    0\n SELECT $1 AS \"ONE\"                                  |     1 |    1\n SELECT $1 AS \"ONE\"                                  |     1 |    1\n SELECT $1+$2 AS \"TWO\"                               |     1 |    1\n SELECT $1+$2 AS \"TWO\"                               |     1 |    1\n SELECT edb_stat_statements_reset() IS NOT NULL AS t |     1 |    1\n SET ROLE regress_stats_user1                        |     1 |    0\n SET ROLE regress_stats_user2                        |     1 |    0\n(10 rows)\n\n--\n-- Don't reset anything if any of the parameter is NULL\n--\nSELECT edb_stat_statements_reset(NULL) IS NOT NULL AS t;\n t \n---\n f\n(1 row)\n\nSELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n                                     query                                     | calls | rows \n-------------------------------------------------------------------------------+-------+------\n CREATE ROLE regress_stats_user1                                               |     1 |    0\n CREATE ROLE regress_stats_user2                                               |     1 |    0\n RESET ROLE                                                                    |     2 |    0\n SELECT $1 AS \"ONE\"                                                            |     1 |    1\n SELECT $1 AS \"ONE\"                                                            |     1 |    1\n SELECT $1+$2 AS \"TWO\"                                                         |     1 |    1\n SELECT $1+$2 AS \"TWO\"                                                         |     1 |    1\n SELECT edb_stat_statements_reset($1) IS NOT NULL AS t                         |     1 |    1\n SELECT edb_stat_statements_reset() IS NOT NULL AS t                           |     1 |    1\n SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE \"C\" |     1 |   10\n SET ROLE regress_stats_user1                                                  |     1 |    0\n SET ROLE regress_stats_user2                                                  |     1 |    0\n(12 rows)\n\n--\n-- remove query ('SELECT $1+$2 AS \"TWO\"') executed by regress_stats_user2\n-- in the current_database\n--\nSELECT edb_stat_statements_reset(\n\t(SELECT r.oid FROM pg_roles AS r WHERE r.rolname = 'regress_stats_user2'),\n\tARRAY(SELECT d.oid FROM pg_database As d where datname = current_database()),\n\t(SELECT s.queryid FROM edb_stat_statements AS s\n\t\t\t\tWHERE s.query = 'SELECT $1+$2 AS \"TWO\"' LIMIT 1))\n\tIS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nSELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n                                         query                                         | calls | rows \n---------------------------------------------------------------------------------------+-------+------\n CREATE ROLE regress_stats_user1                                                       |     1 |    0\n CREATE ROLE regress_stats_user2                                                       |     1 |    0\n RESET ROLE                                                                            |     2 |    0\n SELECT $1 AS \"ONE\"                                                                    |     1 |    1\n SELECT $1 AS \"ONE\"                                                                    |     1 |    1\n SELECT $1+$2 AS \"TWO\"                                                                 |     1 |    1\n SELECT edb_stat_statements_reset(                                                    +|     1 |    1\n         (SELECT r.oid FROM pg_roles AS r WHERE r.rolname = $1),                      +|       | \n         ARRAY(SELECT d.oid FROM pg_database As d where datname = current_database()),+|       | \n         (SELECT s.queryid FROM edb_stat_statements AS s                              +|       | \n                                 WHERE s.query = $2 LIMIT $3))                        +|       | \n         IS NOT NULL AS t                                                              |       | \n SELECT edb_stat_statements_reset($1) IS NOT NULL AS t                                 |     1 |    1\n SELECT edb_stat_statements_reset() IS NOT NULL AS t                                   |     1 |    1\n SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE \"C\"         |     2 |   22\n SET ROLE regress_stats_user1                                                          |     1 |    0\n SET ROLE regress_stats_user2                                                          |     1 |    0\n(12 rows)\n\n--\n-- remove query ('SELECT $1 AS \"ONE\"') executed by two users\n--\nSELECT edb_stat_statements_reset(0,'{}',s.queryid) IS NOT NULL AS t\n\tFROM edb_stat_statements AS s WHERE s.query = 'SELECT $1 AS \"ONE\"';\n t \n---\n t\n t\n(2 rows)\n\nSELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n                                         query                                         | calls | rows \n---------------------------------------------------------------------------------------+-------+------\n CREATE ROLE regress_stats_user1                                                       |     1 |    0\n CREATE ROLE regress_stats_user2                                                       |     1 |    0\n RESET ROLE                                                                            |     2 |    0\n SELECT $1+$2 AS \"TWO\"                                                                 |     1 |    1\n SELECT edb_stat_statements_reset(                                                    +|     1 |    1\n         (SELECT r.oid FROM pg_roles AS r WHERE r.rolname = $1),                      +|       | \n         ARRAY(SELECT d.oid FROM pg_database As d where datname = current_database()),+|       | \n         (SELECT s.queryid FROM edb_stat_statements AS s                              +|       | \n                                 WHERE s.query = $2 LIMIT $3))                        +|       | \n         IS NOT NULL AS t                                                              |       | \n SELECT edb_stat_statements_reset($1) IS NOT NULL AS t                                 |     1 |    1\n SELECT edb_stat_statements_reset($1,$2,s.queryid) IS NOT NULL AS t                   +|     1 |    2\n         FROM edb_stat_statements AS s WHERE s.query = $3                              |       | \n SELECT edb_stat_statements_reset() IS NOT NULL AS t                                   |     1 |    1\n SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE \"C\"         |     3 |   34\n SET ROLE regress_stats_user1                                                          |     1 |    0\n SET ROLE regress_stats_user2                                                          |     1 |    0\n(11 rows)\n\n--\n-- remove query of a user (regress_stats_user1)\n--\nSELECT edb_stat_statements_reset(r.oid) IS NOT NULL AS t\n\t\tFROM pg_roles AS r WHERE r.rolname = 'regress_stats_user1';\n t \n---\n t\n(1 row)\n\nSELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n                                         query                                         | calls | rows \n---------------------------------------------------------------------------------------+-------+------\n CREATE ROLE regress_stats_user1                                                       |     1 |    0\n CREATE ROLE regress_stats_user2                                                       |     1 |    0\n RESET ROLE                                                                            |     2 |    0\n SELECT edb_stat_statements_reset(                                                    +|     1 |    1\n         (SELECT r.oid FROM pg_roles AS r WHERE r.rolname = $1),                      +|       | \n         ARRAY(SELECT d.oid FROM pg_database As d where datname = current_database()),+|       | \n         (SELECT s.queryid FROM edb_stat_statements AS s                              +|       | \n                                 WHERE s.query = $2 LIMIT $3))                        +|       | \n         IS NOT NULL AS t                                                              |       | \n SELECT edb_stat_statements_reset($1) IS NOT NULL AS t                                 |     1 |    1\n SELECT edb_stat_statements_reset($1,$2,s.queryid) IS NOT NULL AS t                   +|     1 |    2\n         FROM edb_stat_statements AS s WHERE s.query = $3                              |       | \n SELECT edb_stat_statements_reset() IS NOT NULL AS t                                   |     1 |    1\n SELECT edb_stat_statements_reset(r.oid) IS NOT NULL AS t                             +|     1 |    1\n                 FROM pg_roles AS r WHERE r.rolname = $1                               |       | \n SELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE \"C\"         |     4 |   45\n SET ROLE regress_stats_user2                                                          |     1 |    0\n(10 rows)\n\n--\n-- reset all\n--\nSELECT edb_stat_statements_reset(0,'{}',0) IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nSELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n                            query                            | calls | rows \n-------------------------------------------------------------+-------+------\n SELECT edb_stat_statements_reset(0,'{}',0) IS NOT NULL AS t |     1 |    1\n(1 row)\n\n--\n-- cleanup\n--\nDROP ROLE regress_stats_user1;\nDROP ROLE regress_stats_user2;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n"
  },
  {
    "path": "edb_stat_statements/expected/utility.out.16",
    "content": "--\n-- Utility commands\n--\n-- These tests require track_utility to be enabled.\nSET edb_stat_statements.track_utility = TRUE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Tables, indexes, triggers\nCREATE TEMP TABLE tab_stats (a int, b char(20));\nCREATE INDEX index_stats ON tab_stats(b, (b || 'data1'), (b || 'data2')) WHERE a > 0;\nALTER TABLE tab_stats ALTER COLUMN b set default 'a';\nALTER TABLE tab_stats ALTER COLUMN b TYPE text USING 'data' || b;\nALTER TABLE tab_stats ADD CONSTRAINT a_nonzero CHECK (a <> 0);\nDROP TABLE tab_stats \\;\nDROP TABLE IF EXISTS tab_stats \\;\n-- This DROP query uses two different strings, still they count as one entry.\nDROP TABLE IF EXISTS tab_stats \\;\nDrop Table If Exists tab_stats \\;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nNOTICE:  table \"tab_stats\" does not exist, skipping\nNOTICE:  table \"tab_stats\" does not exist, skipping\nNOTICE:  table \"tab_stats\" does not exist, skipping\n calls | rows |                                        query                                         \n-------+------+--------------------------------------------------------------------------------------\n     1 |    0 | ALTER TABLE tab_stats ADD CONSTRAINT a_nonzero CHECK (a <> 0)\n     1 |    0 | ALTER TABLE tab_stats ALTER COLUMN b TYPE text USING 'data' || b\n     1 |    0 | ALTER TABLE tab_stats ALTER COLUMN b set default 'a'\n     1 |    0 | CREATE INDEX index_stats ON tab_stats(b, (b || 'data1'), (b || 'data2')) WHERE a > 0\n     1 |    0 | CREATE TEMP TABLE tab_stats (a int, b char(20))\n     3 |    0 | DROP TABLE IF EXISTS tab_stats\n     1 |    0 | DROP TABLE tab_stats\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(8 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Partitions\nCREATE TABLE pt_stats (a int, b int) PARTITION BY range (a);\nCREATE TABLE pt_stats1 (a int, b int);\nALTER TABLE pt_stats ATTACH PARTITION pt_stats1 FOR VALUES FROM (0) TO (100);\nCREATE TABLE pt_stats2 PARTITION OF pt_stats FOR VALUES FROM (100) TO (200);\nCREATE INDEX pt_stats_index ON ONLY pt_stats (a);\nCREATE INDEX pt_stats2_index ON ONLY pt_stats2 (a);\nALTER INDEX pt_stats_index ATTACH PARTITION pt_stats2_index;\nDROP TABLE pt_stats;\n-- Views\nCREATE VIEW view_stats AS SELECT 1::int AS a, 2::int AS b;\nALTER VIEW view_stats ALTER COLUMN a SET DEFAULT 2;\nDROP VIEW view_stats;\n-- Foreign tables\nCREATE FOREIGN DATA WRAPPER wrapper_stats;\nCREATE SERVER server_stats FOREIGN DATA WRAPPER wrapper_stats;\nCREATE FOREIGN TABLE foreign_stats (a int) SERVER server_stats;\nALTER FOREIGN TABLE foreign_stats ADD COLUMN b integer DEFAULT 1;\nALTER FOREIGN TABLE foreign_stats ADD CONSTRAINT b_nonzero CHECK (b <> 0);\nDROP FOREIGN TABLE foreign_stats;\nDROP SERVER server_stats;\nDROP FOREIGN DATA WRAPPER wrapper_stats;\n-- Functions\nCREATE FUNCTION func_stats(a text DEFAULT 'a_data', b text DEFAULT lower('b_data'))\n  RETURNS text AS $$ SELECT $1::text || '_' || $2::text; $$ LANGUAGE SQL\n  SET work_mem = '256kB';\nDROP FUNCTION func_stats;\n-- Rules\nCREATE TABLE tab_rule_stats (a int, b int);\nCREATE TABLE tab_rule_stats_2 (a int, b int, c int, d int);\nCREATE RULE rules_stats AS ON INSERT TO tab_rule_stats DO INSTEAD\n  INSERT INTO tab_rule_stats_2 VALUES(new.*, 1, 2);\nDROP RULE rules_stats ON tab_rule_stats;\nDROP TABLE tab_rule_stats, tab_rule_stats_2;\n-- Types\nCREATE TYPE stats_type as (f1 numeric(35, 6), f2 numeric(35, 2));\nDROP TYPE stats_type;\n-- Triggers\nCREATE TABLE trigger_tab_stats (a int, b int);\nCREATE FUNCTION trigger_func_stats () RETURNS trigger LANGUAGE plpgsql\n  AS $$ BEGIN return OLD; end; $$;\nCREATE TRIGGER trigger_tab_stats\n    AFTER UPDATE ON trigger_tab_stats\n    FOR EACH ROW WHEN (OLD.a < 0 AND OLD.b < 1 AND true)\n    EXECUTE FUNCTION trigger_func_stats();\nDROP TABLE trigger_tab_stats;\n-- Policies\nCREATE TABLE tab_policy_stats (a int, b int);\nCREATE POLICY policy_stats ON tab_policy_stats USING (a = 5) WITH CHECK (b < 5);\nDROP TABLE tab_policy_stats;\n-- Statistics\nCREATE TABLE tab_expr_stats (a int, b int);\nCREATE STATISTICS tab_expr_stats_1 (mcv) ON a, (2*a), (3*b) FROM tab_expr_stats;\nDROP TABLE tab_expr_stats;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                                        query                                        \n-------+------+-------------------------------------------------------------------------------------\n     1 |    0 | ALTER FOREIGN TABLE foreign_stats ADD COLUMN b integer DEFAULT 1\n     1 |    0 | ALTER FOREIGN TABLE foreign_stats ADD CONSTRAINT b_nonzero CHECK (b <> 0)\n     1 |    0 | ALTER INDEX pt_stats_index ATTACH PARTITION pt_stats2_index\n     1 |    0 | ALTER TABLE pt_stats ATTACH PARTITION pt_stats1 FOR VALUES FROM (0) TO (100)\n     1 |    0 | ALTER VIEW view_stats ALTER COLUMN a SET DEFAULT 2\n     1 |    0 | CREATE FOREIGN DATA WRAPPER wrapper_stats\n     1 |    0 | CREATE FOREIGN TABLE foreign_stats (a int) SERVER server_stats\n     1 |    0 | CREATE FUNCTION func_stats(a text DEFAULT 'a_data', b text DEFAULT lower('b_data'))+\n       |      |   RETURNS text AS $$ SELECT $1::text || '_' || $2::text; $$ LANGUAGE SQL           +\n       |      |   SET work_mem = '256kB'\n     1 |    0 | CREATE FUNCTION trigger_func_stats () RETURNS trigger LANGUAGE plpgsql             +\n       |      |   AS $$ BEGIN return OLD; end; $$\n     1 |    0 | CREATE INDEX pt_stats2_index ON ONLY pt_stats2 (a)\n     1 |    0 | CREATE INDEX pt_stats_index ON ONLY pt_stats (a)\n     1 |    0 | CREATE POLICY policy_stats ON tab_policy_stats USING (a = 5) WITH CHECK (b < 5)\n     1 |    0 | CREATE RULE rules_stats AS ON INSERT TO tab_rule_stats DO INSTEAD                  +\n       |      |   INSERT INTO tab_rule_stats_2 VALUES(new.*, 1, 2)\n     1 |    0 | CREATE SERVER server_stats FOREIGN DATA WRAPPER wrapper_stats\n     1 |    0 | CREATE STATISTICS tab_expr_stats_1 (mcv) ON a, (2*a), (3*b) FROM tab_expr_stats\n     1 |    0 | CREATE TABLE pt_stats (a int, b int) PARTITION BY range (a)\n     1 |    0 | CREATE TABLE pt_stats1 (a int, b int)\n     1 |    0 | CREATE TABLE pt_stats2 PARTITION OF pt_stats FOR VALUES FROM (100) TO (200)\n     1 |    0 | CREATE TABLE tab_expr_stats (a int, b int)\n     1 |    0 | CREATE TABLE tab_policy_stats (a int, b int)\n     1 |    0 | CREATE TABLE tab_rule_stats (a int, b int)\n     1 |    0 | CREATE TABLE tab_rule_stats_2 (a int, b int, c int, d int)\n     1 |    0 | CREATE TABLE trigger_tab_stats (a int, b int)\n     1 |    0 | CREATE TRIGGER trigger_tab_stats                                                   +\n       |      |     AFTER UPDATE ON trigger_tab_stats                                              +\n       |      |     FOR EACH ROW WHEN (OLD.a < 0 AND OLD.b < 1 AND true)                           +\n       |      |     EXECUTE FUNCTION trigger_func_stats()\n     1 |    0 | CREATE TYPE stats_type as (f1 numeric(35, 6), f2 numeric(35, 2))\n     1 |    0 | CREATE VIEW view_stats AS SELECT 1::int AS a, 2::int AS b\n     1 |    0 | DROP FOREIGN DATA WRAPPER wrapper_stats\n     1 |    0 | DROP FOREIGN TABLE foreign_stats\n     1 |    0 | DROP FUNCTION func_stats\n     1 |    0 | DROP RULE rules_stats ON tab_rule_stats\n     1 |    0 | DROP SERVER server_stats\n     1 |    0 | DROP TABLE pt_stats\n     1 |    0 | DROP TABLE tab_expr_stats\n     1 |    0 | DROP TABLE tab_policy_stats\n     1 |    0 | DROP TABLE tab_rule_stats, tab_rule_stats_2\n     1 |    0 | DROP TABLE trigger_tab_stats\n     1 |    0 | DROP TYPE stats_type\n     1 |    0 | DROP VIEW view_stats\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(39 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Transaction statements\nBEGIN;\nABORT;\nBEGIN;\nROLLBACK;\n-- WORK\nBEGIN WORK;\nCOMMIT WORK;\nBEGIN WORK;\nABORT WORK;\n-- TRANSACTION\nBEGIN TRANSACTION;\nCOMMIT TRANSACTION;\nBEGIN TRANSACTION;\nABORT TRANSACTION;\n-- More isolation levels\nBEGIN TRANSACTION DEFERRABLE;\nCOMMIT TRANSACTION AND NO CHAIN;\nBEGIN ISOLATION LEVEL SERIALIZABLE;\nCOMMIT;\nBEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;\nCOMMIT;\n-- List of A_Const nodes, same lists.\nBEGIN TRANSACTION READ ONLY, READ WRITE, DEFERRABLE, NOT DEFERRABLE;\nCOMMIT;\nBEGIN TRANSACTION NOT DEFERRABLE, READ ONLY, READ WRITE, DEFERRABLE;\nCOMMIT;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                                query                                \n-------+------+---------------------------------------------------------------------\n     4 |    0 | ABORT\n     6 |    0 | BEGIN\n     2 |    0 | BEGIN ISOLATION LEVEL SERIALIZABLE\n     1 |    0 | BEGIN TRANSACTION DEFERRABLE\n     1 |    0 | BEGIN TRANSACTION NOT DEFERRABLE, READ ONLY, READ WRITE, DEFERRABLE\n     1 |    0 | BEGIN TRANSACTION READ ONLY, READ WRITE, DEFERRABLE, NOT DEFERRABLE\n     7 |    0 | COMMIT WORK\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(8 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Two-phase transactions\nBEGIN;\nPREPARE TRANSACTION 'stat_trans1';\nCOMMIT PREPARED 'stat_trans1';\nBEGIN;\nPREPARE TRANSACTION 'stat_trans2';\nROLLBACK PREPARED 'stat_trans2';\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                        query                        \n-------+------+-----------------------------------------------------\n     2 |    0 | BEGIN\n     1 |    0 | COMMIT PREPARED 'stat_trans1'\n     1 |    0 | PREPARE TRANSACTION 'stat_trans1'\n     1 |    0 | PREPARE TRANSACTION 'stat_trans2'\n     1 |    0 | ROLLBACK PREPARED 'stat_trans2'\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(6 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Savepoints\nBEGIN;\nSAVEPOINT sp1;\nSAVEPOINT sp2;\nSAVEPOINT sp3;\nSAVEPOINT sp4;\nROLLBACK TO sp4;\nROLLBACK TO SAVEPOINT sp4;\nROLLBACK TRANSACTION TO SAVEPOINT sp3;\nRELEASE sp3;\nRELEASE SAVEPOINT sp2;\nROLLBACK TO sp1;\nRELEASE SAVEPOINT sp1;\nCOMMIT;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                        query                        \n-------+------+-----------------------------------------------------\n     1 |    0 | BEGIN\n     1 |    0 | COMMIT\n     1 |    0 | RELEASE SAVEPOINT sp1\n     1 |    0 | RELEASE SAVEPOINT sp2\n     1 |    0 | RELEASE sp3\n     1 |    0 | ROLLBACK TO sp1\n     2 |    0 | ROLLBACK TO sp4\n     1 |    0 | ROLLBACK TRANSACTION TO SAVEPOINT sp3\n     1 |    0 | SAVEPOINT sp1\n     1 |    0 | SAVEPOINT sp2\n     1 |    0 | SAVEPOINT sp3\n     1 |    0 | SAVEPOINT sp4\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(13 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- EXPLAIN statements\n-- A Query is used, normalized by the query jumbling.\nEXPLAIN (costs off) SELECT 1;\n QUERY PLAN \n------------\n Result\n(1 row)\n\nEXPLAIN (costs off) SELECT 2;\n QUERY PLAN \n------------\n Result\n(1 row)\n\nEXPLAIN (costs off) SELECT a FROM generate_series(1,10) AS tab(a) WHERE a = 3;\n              QUERY PLAN              \n--------------------------------------\n Function Scan on generate_series tab\n   Filter: (a = 3)\n(2 rows)\n\nEXPLAIN (costs off) SELECT a FROM generate_series(1,10) AS tab(a) WHERE a = 7;\n              QUERY PLAN              \n--------------------------------------\n Function Scan on generate_series tab\n   Filter: (a = 7)\n(2 rows)\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                                      query                                      \n-------+------+---------------------------------------------------------------------------------\n     2 |    0 | EXPLAIN (costs off) SELECT $1\n     2 |    0 | EXPLAIN (costs off) SELECT a FROM generate_series($1,$2) AS tab(a) WHERE a = $3\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(3 rows)\n\n-- CALL\nCREATE OR REPLACE PROCEDURE sum_one(i int) AS $$\nDECLARE\n  r int;\nBEGIN\n  SELECT (i + i)::int INTO r;\nEND; $$ LANGUAGE plpgsql;\nCREATE OR REPLACE PROCEDURE sum_two(i int, j int) AS $$\nDECLARE\n  r int;\nBEGIN\n  SELECT (i + j)::int INTO r;\nEND; $$ LANGUAGE plpgsql;\n-- Overloaded functions.\nCREATE OR REPLACE PROCEDURE overload(i int) AS $$\nDECLARE\n  r int;\nBEGIN\n  SELECT (i + i)::int INTO r;\nEND; $$ LANGUAGE plpgsql;\nCREATE OR REPLACE PROCEDURE overload(i text) AS $$\nDECLARE\n  r text;\nBEGIN\n  SELECT i::text INTO r;\nEND; $$ LANGUAGE plpgsql;\n-- Mix of IN/OUT parameters.\nCREATE OR REPLACE PROCEDURE in_out(i int, i2 OUT int, i3 INOUT int) AS $$\nDECLARE\n  r int;\nBEGIN\n  i2 := i;\n  i3 := i3 + i;\nEND; $$ LANGUAGE plpgsql;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nCALL sum_one(3);\nCALL sum_one(199);\nCALL sum_two(1,1);\nCALL sum_two(1,2);\nCALL overload(1);\nCALL overload('A');\nCALL in_out(1, NULL, 1);\n i2 | i3 \n----+----\n  1 |  2\n(1 row)\n\nCALL in_out(2, 1, 2);\n i2 | i3 \n----+----\n  2 |  4\n(1 row)\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                        query                        \n-------+------+-----------------------------------------------------\n     1 |    0 | CALL in_out(1, NULL, 1)\n     1 |    0 | CALL in_out(2, 1, 2)\n     1 |    0 | CALL overload('A')\n     1 |    0 | CALL overload(1)\n     1 |    0 | CALL sum_one(199)\n     1 |    0 | CALL sum_one(3)\n     1 |    0 | CALL sum_two(1,1)\n     1 |    0 | CALL sum_two(1,2)\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(9 rows)\n\n-- COPY\nCREATE TABLE copy_stats (a int, b int);\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Some queries with A_Const nodes.\nCOPY (SELECT 1) TO STDOUT;\n1\nCOPY (SELECT 2) TO STDOUT;\n2\nCOPY (INSERT INTO copy_stats VALUES (1, 1) RETURNING *) TO STDOUT;\n1\t1\nCOPY (INSERT INTO copy_stats VALUES (2, 2) RETURNING *) TO STDOUT;\n2\t2\nCOPY (UPDATE copy_stats SET b = b + 1 RETURNING *) TO STDOUT;\n1\t2\n2\t3\nCOPY (UPDATE copy_stats SET b = b + 2 RETURNING *) TO STDOUT;\n1\t4\n2\t5\nCOPY (DELETE FROM copy_stats WHERE a = 1 RETURNING *) TO STDOUT;\n1\t4\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                               query                               \n-------+------+-------------------------------------------------------------------\n     1 |    1 | COPY (DELETE FROM copy_stats WHERE a = 1 RETURNING *) TO STDOUT\n     1 |    1 | COPY (INSERT INTO copy_stats VALUES (1, 1) RETURNING *) TO STDOUT\n     1 |    1 | COPY (INSERT INTO copy_stats VALUES (2, 2) RETURNING *) TO STDOUT\n     1 |    1 | COPY (SELECT 1) TO STDOUT\n     1 |    1 | COPY (SELECT 2) TO STDOUT\n     1 |    2 | COPY (UPDATE copy_stats SET b = b + 1 RETURNING *) TO STDOUT\n     1 |    2 | COPY (UPDATE copy_stats SET b = b + 2 RETURNING *) TO STDOUT\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(8 rows)\n\nDROP TABLE copy_stats;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- CREATE TABLE AS\n-- SELECT queries are normalized, creating matching query IDs.\nCREATE TABLE ctas_stats_1 AS SELECT 1 AS a;\nDROP TABLE ctas_stats_1;\nCREATE TABLE ctas_stats_1 AS SELECT 2 AS a;\nDROP TABLE ctas_stats_1;\nCREATE TABLE ctas_stats_2 AS\n  SELECT a AS col1, 2::int AS col2\n    FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2;\nDROP TABLE ctas_stats_2;\nCREATE TABLE ctas_stats_2 AS\n  SELECT a AS col1, 4::int AS col2\n    FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 1;\nDROP TABLE ctas_stats_2;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                               query                                \n-------+------+--------------------------------------------------------------------\n     2 |    2 | CREATE TABLE ctas_stats_1 AS SELECT $1 AS a\n     2 |    4 | CREATE TABLE ctas_stats_2 AS                                      +\n       |      |   SELECT a AS col1, $1::int AS col2                               +\n       |      |     FROM generate_series($2, $3) AS tab(a) WHERE a < $4 AND a > $5\n     2 |    0 | DROP TABLE ctas_stats_1\n     2 |    0 | DROP TABLE ctas_stats_2\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(5 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- CREATE MATERIALIZED VIEW\n-- SELECT queries are normalized, creating matching query IDs.\nCREATE MATERIALIZED VIEW matview_stats_1 AS\n  SELECT a AS col1, 2::int AS col2\n    FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2;\nDROP MATERIALIZED VIEW matview_stats_1;\nCREATE MATERIALIZED VIEW matview_stats_1 AS\n  SELECT a AS col1, 4::int AS col2\n    FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 3;\nDROP MATERIALIZED VIEW matview_stats_1;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                               query                                \n-------+------+--------------------------------------------------------------------\n     2 |    2 | CREATE MATERIALIZED VIEW matview_stats_1 AS                       +\n       |      |   SELECT a AS col1, $1::int AS col2                               +\n       |      |     FROM generate_series($2, $3) AS tab(a) WHERE a < $4 AND a > $5\n     2 |    0 | DROP MATERIALIZED VIEW matview_stats_1\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(3 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- CREATE VIEW\nCREATE VIEW view_stats_1 AS\n  SELECT a AS col1, 2::int AS col2\n    FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2;\nDROP VIEW view_stats_1;\nCREATE VIEW view_stats_1 AS\n  SELECT a AS col1, 4::int AS col2\n    FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 3;\nDROP VIEW view_stats_1;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                              query                              \n-------+------+-----------------------------------------------------------------\n     1 |    0 | CREATE VIEW view_stats_1 AS                                    +\n       |      |   SELECT a AS col1, 2::int AS col2                             +\n       |      |     FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2\n     1 |    0 | CREATE VIEW view_stats_1 AS                                    +\n       |      |   SELECT a AS col1, 4::int AS col2                             +\n       |      |     FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 3\n     2 |    0 | DROP VIEW view_stats_1\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(4 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Domains\nCREATE DOMAIN domain_stats AS int CHECK (VALUE > 0);\nALTER DOMAIN domain_stats SET DEFAULT '3';\nALTER DOMAIN domain_stats ADD CONSTRAINT higher_than_one CHECK (VALUE > 1);\nDROP DOMAIN domain_stats;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                                   query                                    \n-------+------+----------------------------------------------------------------------------\n     1 |    0 | ALTER DOMAIN domain_stats ADD CONSTRAINT higher_than_one CHECK (VALUE > 1)\n     1 |    0 | ALTER DOMAIN domain_stats SET DEFAULT '3'\n     1 |    0 | CREATE DOMAIN domain_stats AS int CHECK (VALUE > 0)\n     1 |    0 | DROP DOMAIN domain_stats\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(5 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Execution statements\nSELECT 1 as a;\n a \n---\n 1\n(1 row)\n\nPREPARE stat_select AS SELECT $1 AS a;\nEXECUTE stat_select (1);\n a \n---\n 1\n(1 row)\n\nDEALLOCATE stat_select;\nPREPARE stat_select AS SELECT $1 AS a;\nEXECUTE stat_select (2);\n a \n---\n 2\n(1 row)\n\nDEALLOCATE PREPARE stat_select;\nDEALLOCATE ALL;\nDEALLOCATE PREPARE ALL;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                        query                        \n-------+------+-----------------------------------------------------\n     2 |    0 | DEALLOCATE ALL\n     2 |    0 | DEALLOCATE stat_select\n     2 |    2 | PREPARE stat_select AS SELECT $1 AS a\n     1 |    1 | SELECT $1 as a\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(5 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- SET statements.\n-- These use two different strings, still they count as one entry.\nCREATE ROLE regress_stat_set_1;\nCREATE ROLE regress_stat_set_2;\nSET work_mem = '1MB';\nSet work_mem = '1MB';\nSET work_mem = '2MB';\nSET work_mem = DEFAULT;\nSET work_mem TO DEFAULT;\nSET work_mem FROM CURRENT;\nBEGIN;\nSET LOCAL work_mem = '128kB';\nSET LOCAL work_mem = '256kB';\nSET LOCAL work_mem = DEFAULT;\nSET LOCAL work_mem TO DEFAULT;\nSET LOCAL work_mem FROM CURRENT;\nCOMMIT;\nRESET work_mem;\nSET enable_seqscan = off;\nSET enable_seqscan = on;\nSET SESSION work_mem = '300kB';\nSET SESSION work_mem = '400kB';\nRESET enable_seqscan;\n-- SET TRANSACTION ISOLATION\nBEGIN;\nSET TRANSACTION ISOLATION LEVEL READ COMMITTED;\nSET TRANSACTION ISOLATION LEVEL REPEATABLE READ;\nSET TRANSACTION ISOLATION LEVEL SERIALIZABLE;\nCOMMIT;\n-- SET SESSION AUTHORIZATION\nSET SESSION SESSION AUTHORIZATION DEFAULT;\nSET SESSION AUTHORIZATION 'regress_stat_set_1';\nSET SESSION AUTHORIZATION 'regress_stat_set_2';\nRESET SESSION AUTHORIZATION;\nBEGIN;\nSET LOCAL SESSION AUTHORIZATION DEFAULT;\nSET LOCAL SESSION AUTHORIZATION 'regress_stat_set_1';\nSET LOCAL SESSION AUTHORIZATION 'regress_stat_set_2';\nRESET SESSION AUTHORIZATION;\nCOMMIT;\n-- SET SESSION CHARACTERISTICS\nSET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;\nSET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ ONLY;\nSET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ WRITE;\n-- SET XML OPTION\nSET XML OPTION DOCUMENT;\nSET XML OPTION CONTENT;\n-- SET TIME ZONE\nSET TIME ZONE 'America/New_York';\nSET TIME ZONE 'Asia/Tokyo';\nSET TIME ZONE DEFAULT;\nSET TIME ZONE LOCAL;\nSET TIME ZONE 'CST7CDT,M4.1.0,M10.5.0';\nRESET TIME ZONE;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                              query                               \n-------+------+------------------------------------------------------------------\n     3 |    0 | BEGIN\n     3 |    0 | COMMIT\n     1 |    0 | CREATE ROLE regress_stat_set_1\n     1 |    0 | CREATE ROLE regress_stat_set_2\n     2 |    0 | RESET SESSION AUTHORIZATION\n     1 |    0 | RESET TIME ZONE\n     1 |    0 | RESET enable_seqscan\n     1 |    0 | RESET work_mem\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n     1 |    0 | SET LOCAL SESSION AUTHORIZATION 'regress_stat_set_1'\n     1 |    0 | SET LOCAL SESSION AUTHORIZATION 'regress_stat_set_2'\n     1 |    0 | SET LOCAL SESSION AUTHORIZATION DEFAULT\n     1 |    0 | SET LOCAL work_mem = '128kB'\n     1 |    0 | SET LOCAL work_mem = '256kB'\n     2 |    0 | SET LOCAL work_mem = DEFAULT\n     1 |    0 | SET LOCAL work_mem FROM CURRENT\n     1 |    0 | SET SESSION AUTHORIZATION 'regress_stat_set_1'\n     1 |    0 | SET SESSION AUTHORIZATION 'regress_stat_set_2'\n     1 |    0 | SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY\n     1 |    0 | SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ ONLY\n     1 |    0 | SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ WRITE\n     1 |    0 | SET SESSION SESSION AUTHORIZATION DEFAULT\n     1 |    0 | SET SESSION work_mem = '300kB'\n     1 |    0 | SET SESSION work_mem = '400kB'\n     1 |    0 | SET TIME ZONE 'America/New_York'\n     1 |    0 | SET TIME ZONE 'Asia/Tokyo'\n     1 |    0 | SET TIME ZONE 'CST7CDT,M4.1.0,M10.5.0'\n     2 |    0 | SET TIME ZONE DEFAULT\n     1 |    0 | SET TRANSACTION ISOLATION LEVEL READ COMMITTED\n     1 |    0 | SET TRANSACTION ISOLATION LEVEL REPEATABLE READ\n     1 |    0 | SET TRANSACTION ISOLATION LEVEL SERIALIZABLE\n     1 |    0 | SET XML OPTION CONTENT\n     1 |    0 | SET XML OPTION DOCUMENT\n     1 |    0 | SET enable_seqscan = off\n     1 |    0 | SET enable_seqscan = on\n     2 |    0 | SET work_mem = '1MB'\n     1 |    0 | SET work_mem = '2MB'\n     2 |    0 | SET work_mem = DEFAULT\n     1 |    0 | SET work_mem FROM CURRENT\n(39 rows)\n\nDROP ROLE regress_stat_set_1;\nDROP ROLE regress_stat_set_2;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n--\n-- Track the total number of rows retrieved or affected by the utility\n-- commands of COPY, FETCH, CREATE TABLE AS, CREATE MATERIALIZED VIEW,\n-- REFRESH MATERIALIZED VIEW and SELECT INTO\n--\nCREATE TABLE pgss_ctas AS SELECT a, 'ctas' b FROM generate_series(1, 10) a;\nSELECT generate_series(1, 10) c INTO pgss_select_into;\nCOPY pgss_ctas (a, b) FROM STDIN;\nCREATE MATERIALIZED VIEW pgss_matv AS SELECT * FROM pgss_ctas;\nREFRESH MATERIALIZED VIEW pgss_matv;\nBEGIN;\nDECLARE pgss_cursor CURSOR FOR SELECT * FROM pgss_matv;\nFETCH NEXT pgss_cursor;\n a |  b   \n---+------\n 1 | ctas\n(1 row)\n\nFETCH FORWARD 5 pgss_cursor;\n a |  b   \n---+------\n 2 | ctas\n 3 | ctas\n 4 | ctas\n 5 | ctas\n 6 | ctas\n(5 rows)\n\nFETCH FORWARD ALL pgss_cursor;\n a  |  b   \n----+------\n  7 | ctas\n  8 | ctas\n  9 | ctas\n 10 | ctas\n 11 | copy\n 12 | copy\n 13 | copy\n(7 rows)\n\nCOMMIT;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                                  query                                  \n-------+------+-------------------------------------------------------------------------\n     1 |    0 | BEGIN\n     1 |    0 | COMMIT\n     1 |    3 | COPY pgss_ctas (a, b) FROM STDIN\n     1 |   13 | CREATE MATERIALIZED VIEW pgss_matv AS SELECT * FROM pgss_ctas\n     1 |   10 | CREATE TABLE pgss_ctas AS SELECT a, $1 b FROM generate_series($2, $3) a\n     1 |    0 | DECLARE pgss_cursor CURSOR FOR SELECT * FROM pgss_matv\n     1 |    5 | FETCH FORWARD 5 pgss_cursor\n     1 |    7 | FETCH FORWARD ALL pgss_cursor\n     1 |    1 | FETCH NEXT pgss_cursor\n     1 |   13 | REFRESH MATERIALIZED VIEW pgss_matv\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n     1 |   10 | SELECT generate_series($1, $2) c INTO pgss_select_into\n(12 rows)\n\nDROP MATERIALIZED VIEW pgss_matv;\nDROP TABLE pgss_ctas;\nDROP TABLE pgss_select_into;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Special cases.  Keep these ones at the end to avoid conflicts.\nSET SCHEMA 'foo';\nSET SCHEMA 'public';\nRESET ALL;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                        query                        \n-------+------+-----------------------------------------------------\n     1 |    0 | RESET ALL\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n     1 |    0 | SET SCHEMA 'foo'\n     1 |    0 | SET SCHEMA 'public'\n(4 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n"
  },
  {
    "path": "edb_stat_statements/expected/utility.out.17",
    "content": "--\n-- Utility commands\n--\n-- These tests require track_utility to be enabled.\nSET edb_stat_statements.track_utility = TRUE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Tables, indexes, triggers\nCREATE TEMP TABLE tab_stats (a int, b char(20));\nCREATE INDEX index_stats ON tab_stats(b, (b || 'data1'), (b || 'data2')) WHERE a > 0;\nALTER TABLE tab_stats ALTER COLUMN b set default 'a';\nALTER TABLE tab_stats ALTER COLUMN b TYPE text USING 'data' || b;\nALTER TABLE tab_stats ADD CONSTRAINT a_nonzero CHECK (a <> 0);\nDROP TABLE tab_stats \\;\nDROP TABLE IF EXISTS tab_stats \\;\n-- This DROP query uses two different strings, still they count as one entry.\nDROP TABLE IF EXISTS tab_stats \\;\nDrop Table If Exists tab_stats \\;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nNOTICE:  table \"tab_stats\" does not exist, skipping\nNOTICE:  table \"tab_stats\" does not exist, skipping\nNOTICE:  table \"tab_stats\" does not exist, skipping\n calls | rows |                                        query                                         \n-------+------+--------------------------------------------------------------------------------------\n     1 |    0 | ALTER TABLE tab_stats ADD CONSTRAINT a_nonzero CHECK (a <> 0)\n     1 |    0 | ALTER TABLE tab_stats ALTER COLUMN b TYPE text USING 'data' || b\n     1 |    0 | ALTER TABLE tab_stats ALTER COLUMN b set default 'a'\n     1 |    0 | CREATE INDEX index_stats ON tab_stats(b, (b || 'data1'), (b || 'data2')) WHERE a > 0\n     1 |    0 | CREATE TEMP TABLE tab_stats (a int, b char(20))\n     3 |    0 | DROP TABLE IF EXISTS tab_stats\n     1 |    0 | DROP TABLE tab_stats\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(8 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Partitions\nCREATE TABLE pt_stats (a int, b int) PARTITION BY range (a);\nCREATE TABLE pt_stats1 (a int, b int);\nALTER TABLE pt_stats ATTACH PARTITION pt_stats1 FOR VALUES FROM (0) TO (100);\nCREATE TABLE pt_stats2 PARTITION OF pt_stats FOR VALUES FROM (100) TO (200);\nCREATE INDEX pt_stats_index ON ONLY pt_stats (a);\nCREATE INDEX pt_stats2_index ON ONLY pt_stats2 (a);\nALTER INDEX pt_stats_index ATTACH PARTITION pt_stats2_index;\nDROP TABLE pt_stats;\n-- Views\nCREATE VIEW view_stats AS SELECT 1::int AS a, 2::int AS b;\nALTER VIEW view_stats ALTER COLUMN a SET DEFAULT 2;\nDROP VIEW view_stats;\n-- Foreign tables\nCREATE FOREIGN DATA WRAPPER wrapper_stats;\nCREATE SERVER server_stats FOREIGN DATA WRAPPER wrapper_stats;\nCREATE FOREIGN TABLE foreign_stats (a int) SERVER server_stats;\nALTER FOREIGN TABLE foreign_stats ADD COLUMN b integer DEFAULT 1;\nALTER FOREIGN TABLE foreign_stats ADD CONSTRAINT b_nonzero CHECK (b <> 0);\nDROP FOREIGN TABLE foreign_stats;\nDROP SERVER server_stats;\nDROP FOREIGN DATA WRAPPER wrapper_stats;\n-- Functions\nCREATE FUNCTION func_stats(a text DEFAULT 'a_data', b text DEFAULT lower('b_data'))\n  RETURNS text AS $$ SELECT $1::text || '_' || $2::text; $$ LANGUAGE SQL\n  SET work_mem = '256kB';\nDROP FUNCTION func_stats;\n-- Rules\nCREATE TABLE tab_rule_stats (a int, b int);\nCREATE TABLE tab_rule_stats_2 (a int, b int, c int, d int);\nCREATE RULE rules_stats AS ON INSERT TO tab_rule_stats DO INSTEAD\n  INSERT INTO tab_rule_stats_2 VALUES(new.*, 1, 2);\nDROP RULE rules_stats ON tab_rule_stats;\nDROP TABLE tab_rule_stats, tab_rule_stats_2;\n-- Types\nCREATE TYPE stats_type as (f1 numeric(35, 6), f2 numeric(35, 2));\nDROP TYPE stats_type;\n-- Triggers\nCREATE TABLE trigger_tab_stats (a int, b int);\nCREATE FUNCTION trigger_func_stats () RETURNS trigger LANGUAGE plpgsql\n  AS $$ BEGIN return OLD; end; $$;\nCREATE TRIGGER trigger_tab_stats\n    AFTER UPDATE ON trigger_tab_stats\n    FOR EACH ROW WHEN (OLD.a < 0 AND OLD.b < 1 AND true)\n    EXECUTE FUNCTION trigger_func_stats();\nDROP TABLE trigger_tab_stats;\n-- Policies\nCREATE TABLE tab_policy_stats (a int, b int);\nCREATE POLICY policy_stats ON tab_policy_stats USING (a = 5) WITH CHECK (b < 5);\nDROP TABLE tab_policy_stats;\n-- Statistics\nCREATE TABLE tab_expr_stats (a int, b int);\nCREATE STATISTICS tab_expr_stats_1 (mcv) ON a, (2*a), (3*b) FROM tab_expr_stats;\nDROP TABLE tab_expr_stats;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                                        query                                        \n-------+------+-------------------------------------------------------------------------------------\n     1 |    0 | ALTER FOREIGN TABLE foreign_stats ADD COLUMN b integer DEFAULT 1\n     1 |    0 | ALTER FOREIGN TABLE foreign_stats ADD CONSTRAINT b_nonzero CHECK (b <> 0)\n     1 |    0 | ALTER INDEX pt_stats_index ATTACH PARTITION pt_stats2_index\n     1 |    0 | ALTER TABLE pt_stats ATTACH PARTITION pt_stats1 FOR VALUES FROM (0) TO (100)\n     1 |    0 | ALTER VIEW view_stats ALTER COLUMN a SET DEFAULT 2\n     1 |    0 | CREATE FOREIGN DATA WRAPPER wrapper_stats\n     1 |    0 | CREATE FOREIGN TABLE foreign_stats (a int) SERVER server_stats\n     1 |    0 | CREATE FUNCTION func_stats(a text DEFAULT 'a_data', b text DEFAULT lower('b_data'))+\n       |      |   RETURNS text AS $$ SELECT $1::text || '_' || $2::text; $$ LANGUAGE SQL           +\n       |      |   SET work_mem = '256kB'\n     1 |    0 | CREATE FUNCTION trigger_func_stats () RETURNS trigger LANGUAGE plpgsql             +\n       |      |   AS $$ BEGIN return OLD; end; $$\n     1 |    0 | CREATE INDEX pt_stats2_index ON ONLY pt_stats2 (a)\n     1 |    0 | CREATE INDEX pt_stats_index ON ONLY pt_stats (a)\n     1 |    0 | CREATE POLICY policy_stats ON tab_policy_stats USING (a = 5) WITH CHECK (b < 5)\n     1 |    0 | CREATE RULE rules_stats AS ON INSERT TO tab_rule_stats DO INSTEAD                  +\n       |      |   INSERT INTO tab_rule_stats_2 VALUES(new.*, 1, 2)\n     1 |    0 | CREATE SERVER server_stats FOREIGN DATA WRAPPER wrapper_stats\n     1 |    0 | CREATE STATISTICS tab_expr_stats_1 (mcv) ON a, (2*a), (3*b) FROM tab_expr_stats\n     1 |    0 | CREATE TABLE pt_stats (a int, b int) PARTITION BY range (a)\n     1 |    0 | CREATE TABLE pt_stats1 (a int, b int)\n     1 |    0 | CREATE TABLE pt_stats2 PARTITION OF pt_stats FOR VALUES FROM (100) TO (200)\n     1 |    0 | CREATE TABLE tab_expr_stats (a int, b int)\n     1 |    0 | CREATE TABLE tab_policy_stats (a int, b int)\n     1 |    0 | CREATE TABLE tab_rule_stats (a int, b int)\n     1 |    0 | CREATE TABLE tab_rule_stats_2 (a int, b int, c int, d int)\n     1 |    0 | CREATE TABLE trigger_tab_stats (a int, b int)\n     1 |    0 | CREATE TRIGGER trigger_tab_stats                                                   +\n       |      |     AFTER UPDATE ON trigger_tab_stats                                              +\n       |      |     FOR EACH ROW WHEN (OLD.a < 0 AND OLD.b < 1 AND true)                           +\n       |      |     EXECUTE FUNCTION trigger_func_stats()\n     1 |    0 | CREATE TYPE stats_type as (f1 numeric(35, 6), f2 numeric(35, 2))\n     1 |    0 | CREATE VIEW view_stats AS SELECT 1::int AS a, 2::int AS b\n     1 |    0 | DROP FOREIGN DATA WRAPPER wrapper_stats\n     1 |    0 | DROP FOREIGN TABLE foreign_stats\n     1 |    0 | DROP FUNCTION func_stats\n     1 |    0 | DROP RULE rules_stats ON tab_rule_stats\n     1 |    0 | DROP SERVER server_stats\n     1 |    0 | DROP TABLE pt_stats\n     1 |    0 | DROP TABLE tab_expr_stats\n     1 |    0 | DROP TABLE tab_policy_stats\n     1 |    0 | DROP TABLE tab_rule_stats, tab_rule_stats_2\n     1 |    0 | DROP TABLE trigger_tab_stats\n     1 |    0 | DROP TYPE stats_type\n     1 |    0 | DROP VIEW view_stats\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(39 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Transaction statements\nBEGIN;\nABORT;\nBEGIN;\nROLLBACK;\n-- WORK\nBEGIN WORK;\nCOMMIT WORK;\nBEGIN WORK;\nABORT WORK;\n-- TRANSACTION\nBEGIN TRANSACTION;\nCOMMIT TRANSACTION;\nBEGIN TRANSACTION;\nABORT TRANSACTION;\n-- More isolation levels\nBEGIN TRANSACTION DEFERRABLE;\nCOMMIT TRANSACTION AND NO CHAIN;\nBEGIN ISOLATION LEVEL SERIALIZABLE;\nCOMMIT;\nBEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;\nCOMMIT;\n-- List of A_Const nodes, same lists.\nBEGIN TRANSACTION READ ONLY, READ WRITE, DEFERRABLE, NOT DEFERRABLE;\nCOMMIT;\nBEGIN TRANSACTION NOT DEFERRABLE, READ ONLY, READ WRITE, DEFERRABLE;\nCOMMIT;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                                query                                \n-------+------+---------------------------------------------------------------------\n     4 |    0 | ABORT\n     6 |    0 | BEGIN\n     2 |    0 | BEGIN ISOLATION LEVEL SERIALIZABLE\n     1 |    0 | BEGIN TRANSACTION DEFERRABLE\n     1 |    0 | BEGIN TRANSACTION NOT DEFERRABLE, READ ONLY, READ WRITE, DEFERRABLE\n     1 |    0 | BEGIN TRANSACTION READ ONLY, READ WRITE, DEFERRABLE, NOT DEFERRABLE\n     7 |    0 | COMMIT WORK\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(8 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Two-phase transactions\nBEGIN;\nPREPARE TRANSACTION 'stat_trans1';\nCOMMIT PREPARED 'stat_trans1';\nBEGIN;\nPREPARE TRANSACTION 'stat_trans2';\nROLLBACK PREPARED 'stat_trans2';\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                        query                        \n-------+------+-----------------------------------------------------\n     2 |    0 | BEGIN\n     1 |    0 | COMMIT PREPARED $1\n     2 |    0 | PREPARE TRANSACTION $1\n     1 |    0 | ROLLBACK PREPARED $1\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(5 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Savepoints\nBEGIN;\nSAVEPOINT sp1;\nSAVEPOINT sp2;\nSAVEPOINT sp3;\nSAVEPOINT sp4;\nROLLBACK TO sp4;\nROLLBACK TO SAVEPOINT sp4;\nROLLBACK TRANSACTION TO SAVEPOINT sp3;\nRELEASE sp3;\nRELEASE SAVEPOINT sp2;\nROLLBACK TO sp1;\nRELEASE SAVEPOINT sp1;\nCOMMIT;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                        query                        \n-------+------+-----------------------------------------------------\n     1 |    0 | BEGIN\n     1 |    0 | COMMIT\n     3 |    0 | RELEASE $1\n     4 |    0 | ROLLBACK TO $1\n     4 |    0 | SAVEPOINT $1\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(6 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- EXPLAIN statements\n-- A Query is used, normalized by the query jumbling.\nEXPLAIN (costs off) SELECT 1;\n QUERY PLAN \n------------\n Result\n(1 row)\n\nEXPLAIN (costs off) SELECT 2;\n QUERY PLAN \n------------\n Result\n(1 row)\n\nEXPLAIN (costs off) SELECT a FROM generate_series(1,10) AS tab(a) WHERE a = 3;\n              QUERY PLAN              \n--------------------------------------\n Function Scan on generate_series tab\n   Filter: (a = 3)\n(2 rows)\n\nEXPLAIN (costs off) SELECT a FROM generate_series(1,10) AS tab(a) WHERE a = 7;\n              QUERY PLAN              \n--------------------------------------\n Function Scan on generate_series tab\n   Filter: (a = 7)\n(2 rows)\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                                      query                                      \n-------+------+---------------------------------------------------------------------------------\n     2 |    0 | EXPLAIN (costs off) SELECT $1\n     2 |    0 | EXPLAIN (costs off) SELECT a FROM generate_series($1,$2) AS tab(a) WHERE a = $3\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(3 rows)\n\n-- CALL\nCREATE OR REPLACE PROCEDURE sum_one(i int) AS $$\nDECLARE\n  r int;\nBEGIN\n  SELECT (i + i)::int INTO r;\nEND; $$ LANGUAGE plpgsql;\nCREATE OR REPLACE PROCEDURE sum_two(i int, j int) AS $$\nDECLARE\n  r int;\nBEGIN\n  SELECT (i + j)::int INTO r;\nEND; $$ LANGUAGE plpgsql;\n-- Overloaded functions.\nCREATE OR REPLACE PROCEDURE overload(i int) AS $$\nDECLARE\n  r int;\nBEGIN\n  SELECT (i + i)::int INTO r;\nEND; $$ LANGUAGE plpgsql;\nCREATE OR REPLACE PROCEDURE overload(i text) AS $$\nDECLARE\n  r text;\nBEGIN\n  SELECT i::text INTO r;\nEND; $$ LANGUAGE plpgsql;\n-- Mix of IN/OUT parameters.\nCREATE OR REPLACE PROCEDURE in_out(i int, i2 OUT int, i3 INOUT int) AS $$\nDECLARE\n  r int;\nBEGIN\n  i2 := i;\n  i3 := i3 + i;\nEND; $$ LANGUAGE plpgsql;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\nCALL sum_one(3);\nCALL sum_one(199);\nCALL sum_two(1,1);\nCALL sum_two(1,2);\nCALL overload(1);\nCALL overload('A');\nCALL in_out(1, NULL, 1);\n i2 | i3 \n----+----\n  1 |  2\n(1 row)\n\nCALL in_out(2, 1, 2);\n i2 | i3 \n----+----\n  2 |  4\n(1 row)\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                        query                        \n-------+------+-----------------------------------------------------\n     2 |    0 | CALL in_out($1, $2, $3)\n     1 |    0 | CALL overload($1)\n     1 |    0 | CALL overload($1)\n     2 |    0 | CALL sum_one($1)\n     2 |    0 | CALL sum_two($1,$2)\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(6 rows)\n\n-- COPY\nCREATE TABLE copy_stats (a int, b int);\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Some queries with A_Const nodes.\nCOPY (SELECT 1) TO STDOUT;\n1\nCOPY (SELECT 2) TO STDOUT;\n2\nCOPY (INSERT INTO copy_stats VALUES (1, 1) RETURNING *) TO STDOUT;\n1\t1\nCOPY (INSERT INTO copy_stats VALUES (2, 2) RETURNING *) TO STDOUT;\n2\t2\nCOPY (UPDATE copy_stats SET b = b + 1 RETURNING *) TO STDOUT;\n1\t2\n2\t3\nCOPY (UPDATE copy_stats SET b = b + 2 RETURNING *) TO STDOUT;\n1\t4\n2\t5\nCOPY (DELETE FROM copy_stats WHERE a = 1 RETURNING *) TO STDOUT;\n1\t4\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                               query                               \n-------+------+-------------------------------------------------------------------\n     1 |    1 | COPY (DELETE FROM copy_stats WHERE a = 1 RETURNING *) TO STDOUT\n     1 |    1 | COPY (INSERT INTO copy_stats VALUES (1, 1) RETURNING *) TO STDOUT\n     1 |    1 | COPY (INSERT INTO copy_stats VALUES (2, 2) RETURNING *) TO STDOUT\n     1 |    1 | COPY (SELECT 1) TO STDOUT\n     1 |    1 | COPY (SELECT 2) TO STDOUT\n     1 |    2 | COPY (UPDATE copy_stats SET b = b + 1 RETURNING *) TO STDOUT\n     1 |    2 | COPY (UPDATE copy_stats SET b = b + 2 RETURNING *) TO STDOUT\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(8 rows)\n\nDROP TABLE copy_stats;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- CREATE TABLE AS\n-- SELECT queries are normalized, creating matching query IDs.\nCREATE TABLE ctas_stats_1 AS SELECT 1 AS a;\nDROP TABLE ctas_stats_1;\nCREATE TABLE ctas_stats_1 AS SELECT 2 AS a;\nDROP TABLE ctas_stats_1;\nCREATE TABLE ctas_stats_2 AS\n  SELECT a AS col1, 2::int AS col2\n    FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2;\nDROP TABLE ctas_stats_2;\nCREATE TABLE ctas_stats_2 AS\n  SELECT a AS col1, 4::int AS col2\n    FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 1;\nDROP TABLE ctas_stats_2;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                               query                                \n-------+------+--------------------------------------------------------------------\n     2 |    2 | CREATE TABLE ctas_stats_1 AS SELECT $1 AS a\n     2 |    4 | CREATE TABLE ctas_stats_2 AS                                      +\n       |      |   SELECT a AS col1, $1::int AS col2                               +\n       |      |     FROM generate_series($2, $3) AS tab(a) WHERE a < $4 AND a > $5\n     2 |    0 | DROP TABLE ctas_stats_1\n     2 |    0 | DROP TABLE ctas_stats_2\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(5 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- CREATE MATERIALIZED VIEW\n-- SELECT queries are normalized, creating matching query IDs.\nCREATE MATERIALIZED VIEW matview_stats_1 AS\n  SELECT a AS col1, 2::int AS col2\n    FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2;\nDROP MATERIALIZED VIEW matview_stats_1;\nCREATE MATERIALIZED VIEW matview_stats_1 AS\n  SELECT a AS col1, 4::int AS col2\n    FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 3;\nDROP MATERIALIZED VIEW matview_stats_1;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                               query                                \n-------+------+--------------------------------------------------------------------\n     2 |    2 | CREATE MATERIALIZED VIEW matview_stats_1 AS                       +\n       |      |   SELECT a AS col1, $1::int AS col2                               +\n       |      |     FROM generate_series($2, $3) AS tab(a) WHERE a < $4 AND a > $5\n     2 |    0 | DROP MATERIALIZED VIEW matview_stats_1\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(3 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- CREATE VIEW\nCREATE VIEW view_stats_1 AS\n  SELECT a AS col1, 2::int AS col2\n    FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2;\nDROP VIEW view_stats_1;\nCREATE VIEW view_stats_1 AS\n  SELECT a AS col1, 4::int AS col2\n    FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 3;\nDROP VIEW view_stats_1;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                              query                              \n-------+------+-----------------------------------------------------------------\n     1 |    0 | CREATE VIEW view_stats_1 AS                                    +\n       |      |   SELECT a AS col1, 2::int AS col2                             +\n       |      |     FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2\n     1 |    0 | CREATE VIEW view_stats_1 AS                                    +\n       |      |   SELECT a AS col1, 4::int AS col2                             +\n       |      |     FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 3\n     2 |    0 | DROP VIEW view_stats_1\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(4 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Domains\nCREATE DOMAIN domain_stats AS int CHECK (VALUE > 0);\nALTER DOMAIN domain_stats SET DEFAULT '3';\nALTER DOMAIN domain_stats ADD CONSTRAINT higher_than_one CHECK (VALUE > 1);\nDROP DOMAIN domain_stats;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                                   query                                    \n-------+------+----------------------------------------------------------------------------\n     1 |    0 | ALTER DOMAIN domain_stats ADD CONSTRAINT higher_than_one CHECK (VALUE > 1)\n     1 |    0 | ALTER DOMAIN domain_stats SET DEFAULT '3'\n     1 |    0 | CREATE DOMAIN domain_stats AS int CHECK (VALUE > 0)\n     1 |    0 | DROP DOMAIN domain_stats\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(5 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Execution statements\nSELECT 1 as a;\n a \n---\n 1\n(1 row)\n\nPREPARE stat_select AS SELECT $1 AS a;\nEXECUTE stat_select (1);\n a \n---\n 1\n(1 row)\n\nDEALLOCATE stat_select;\nPREPARE stat_select AS SELECT $1 AS a;\nEXECUTE stat_select (2);\n a \n---\n 2\n(1 row)\n\nDEALLOCATE PREPARE stat_select;\nDEALLOCATE ALL;\nDEALLOCATE PREPARE ALL;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                        query                        \n-------+------+-----------------------------------------------------\n     2 |    0 | DEALLOCATE $1\n     2 |    0 | DEALLOCATE ALL\n     2 |    2 | PREPARE stat_select AS SELECT $1 AS a\n     1 |    1 | SELECT $1 as a\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n(5 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- SET statements.\n-- These use two different strings, still they count as one entry.\nCREATE ROLE regress_stat_set_1;\nCREATE ROLE regress_stat_set_2;\nSET work_mem = '1MB';\nSet work_mem = '1MB';\nSET work_mem = '2MB';\nSET work_mem = DEFAULT;\nSET work_mem TO DEFAULT;\nSET work_mem FROM CURRENT;\nBEGIN;\nSET LOCAL work_mem = '128kB';\nSET LOCAL work_mem = '256kB';\nSET LOCAL work_mem = DEFAULT;\nSET LOCAL work_mem TO DEFAULT;\nSET LOCAL work_mem FROM CURRENT;\nCOMMIT;\nRESET work_mem;\nSET enable_seqscan = off;\nSET enable_seqscan = on;\nSET SESSION work_mem = '300kB';\nSET SESSION work_mem = '400kB';\nRESET enable_seqscan;\n-- SET TRANSACTION ISOLATION\nBEGIN;\nSET TRANSACTION ISOLATION LEVEL READ COMMITTED;\nSET TRANSACTION ISOLATION LEVEL REPEATABLE READ;\nSET TRANSACTION ISOLATION LEVEL SERIALIZABLE;\nCOMMIT;\n-- SET SESSION AUTHORIZATION\nSET SESSION SESSION AUTHORIZATION DEFAULT;\nSET SESSION AUTHORIZATION 'regress_stat_set_1';\nSET SESSION AUTHORIZATION 'regress_stat_set_2';\nRESET SESSION AUTHORIZATION;\nBEGIN;\nSET LOCAL SESSION AUTHORIZATION DEFAULT;\nSET LOCAL SESSION AUTHORIZATION 'regress_stat_set_1';\nSET LOCAL SESSION AUTHORIZATION 'regress_stat_set_2';\nRESET SESSION AUTHORIZATION;\nCOMMIT;\n-- SET SESSION CHARACTERISTICS\nSET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;\nSET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ ONLY;\nSET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ WRITE;\n-- SET XML OPTION\nSET XML OPTION DOCUMENT;\nSET XML OPTION CONTENT;\n-- SET TIME ZONE\nSET TIME ZONE 'America/New_York';\nSET TIME ZONE 'Asia/Tokyo';\nSET TIME ZONE DEFAULT;\nSET TIME ZONE LOCAL;\nSET TIME ZONE 'CST7CDT,M4.1.0,M10.5.0';\nRESET TIME ZONE;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                              query                               \n-------+------+------------------------------------------------------------------\n     3 |    0 | BEGIN\n     3 |    0 | COMMIT\n     1 |    0 | CREATE ROLE regress_stat_set_1\n     1 |    0 | CREATE ROLE regress_stat_set_2\n     2 |    0 | RESET SESSION AUTHORIZATION\n     1 |    0 | RESET TIME ZONE\n     1 |    0 | RESET enable_seqscan\n     1 |    0 | RESET work_mem\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n     1 |    0 | SET LOCAL SESSION AUTHORIZATION 'regress_stat_set_1'\n     1 |    0 | SET LOCAL SESSION AUTHORIZATION 'regress_stat_set_2'\n     1 |    0 | SET LOCAL SESSION AUTHORIZATION DEFAULT\n     1 |    0 | SET LOCAL work_mem = '128kB'\n     1 |    0 | SET LOCAL work_mem = '256kB'\n     2 |    0 | SET LOCAL work_mem = DEFAULT\n     1 |    0 | SET LOCAL work_mem FROM CURRENT\n     1 |    0 | SET SESSION AUTHORIZATION 'regress_stat_set_1'\n     1 |    0 | SET SESSION AUTHORIZATION 'regress_stat_set_2'\n     1 |    0 | SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY\n     1 |    0 | SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ ONLY\n     1 |    0 | SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ WRITE\n     1 |    0 | SET SESSION SESSION AUTHORIZATION DEFAULT\n     1 |    0 | SET SESSION work_mem = '300kB'\n     1 |    0 | SET SESSION work_mem = '400kB'\n     1 |    0 | SET TIME ZONE 'America/New_York'\n     1 |    0 | SET TIME ZONE 'Asia/Tokyo'\n     1 |    0 | SET TIME ZONE 'CST7CDT,M4.1.0,M10.5.0'\n     2 |    0 | SET TIME ZONE DEFAULT\n     1 |    0 | SET TRANSACTION ISOLATION LEVEL READ COMMITTED\n     1 |    0 | SET TRANSACTION ISOLATION LEVEL REPEATABLE READ\n     1 |    0 | SET TRANSACTION ISOLATION LEVEL SERIALIZABLE\n     1 |    0 | SET XML OPTION CONTENT\n     1 |    0 | SET XML OPTION DOCUMENT\n     1 |    0 | SET enable_seqscan = off\n     1 |    0 | SET enable_seqscan = on\n     2 |    0 | SET work_mem = '1MB'\n     1 |    0 | SET work_mem = '2MB'\n     2 |    0 | SET work_mem = DEFAULT\n     1 |    0 | SET work_mem FROM CURRENT\n(39 rows)\n\nDROP ROLE regress_stat_set_1;\nDROP ROLE regress_stat_set_2;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n--\n-- Track the total number of rows retrieved or affected by the utility\n-- commands of COPY, FETCH, CREATE TABLE AS, CREATE MATERIALIZED VIEW,\n-- REFRESH MATERIALIZED VIEW and SELECT INTO\n--\nCREATE TABLE pgss_ctas AS SELECT a, 'ctas' b FROM generate_series(1, 10) a;\nSELECT generate_series(1, 10) c INTO pgss_select_into;\nCOPY pgss_ctas (a, b) FROM STDIN;\nCREATE MATERIALIZED VIEW pgss_matv AS SELECT * FROM pgss_ctas;\nREFRESH MATERIALIZED VIEW pgss_matv;\nBEGIN;\nDECLARE pgss_cursor CURSOR FOR SELECT * FROM pgss_matv;\nFETCH NEXT pgss_cursor;\n a |  b   \n---+------\n 1 | ctas\n(1 row)\n\nFETCH FORWARD 5 pgss_cursor;\n a |  b   \n---+------\n 2 | ctas\n 3 | ctas\n 4 | ctas\n 5 | ctas\n 6 | ctas\n(5 rows)\n\nFETCH FORWARD ALL pgss_cursor;\n a  |  b   \n----+------\n  7 | ctas\n  8 | ctas\n  9 | ctas\n 10 | ctas\n 11 | copy\n 12 | copy\n 13 | copy\n(7 rows)\n\nCOMMIT;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                                  query                                  \n-------+------+-------------------------------------------------------------------------\n     1 |    0 | BEGIN\n     1 |    0 | COMMIT\n     1 |    3 | COPY pgss_ctas (a, b) FROM STDIN\n     1 |   13 | CREATE MATERIALIZED VIEW pgss_matv AS SELECT * FROM pgss_ctas\n     1 |   10 | CREATE TABLE pgss_ctas AS SELECT a, $1 b FROM generate_series($2, $3) a\n     1 |    0 | DECLARE pgss_cursor CURSOR FOR SELECT * FROM pgss_matv\n     1 |    5 | FETCH FORWARD 5 pgss_cursor\n     1 |    7 | FETCH FORWARD ALL pgss_cursor\n     1 |    1 | FETCH NEXT pgss_cursor\n     1 |   13 | REFRESH MATERIALIZED VIEW pgss_matv\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n     1 |   10 | SELECT generate_series($1, $2) c INTO pgss_select_into\n(12 rows)\n\nDROP MATERIALIZED VIEW pgss_matv;\nDROP TABLE pgss_ctas;\nDROP TABLE pgss_select_into;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n-- Special cases.  Keep these ones at the end to avoid conflicts.\nSET SCHEMA 'foo';\nSET SCHEMA 'public';\nRESET ALL;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n calls | rows |                        query                        \n-------+------+-----------------------------------------------------\n     1 |    0 | RESET ALL\n     1 |    1 | SELECT edb_stat_statements_reset() IS NOT NULL AS t\n     1 |    0 | SET SCHEMA 'foo'\n     1 |    0 | SET SCHEMA 'public'\n(4 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n"
  },
  {
    "path": "edb_stat_statements/expected/wal.out.17",
    "content": "--\n-- Validate WAL generation metrics\n--\nSET edb_stat_statements.track_utility = FALSE;\nCREATE TABLE pgss_wal_tab (a int, b char(20));\nINSERT INTO pgss_wal_tab VALUES(generate_series(1, 10), 'aaa');\nUPDATE pgss_wal_tab SET b = 'bbb' WHERE a > 7;\nDELETE FROM pgss_wal_tab WHERE a > 9;\nDROP TABLE pgss_wal_tab;\n-- Check WAL is generated for the above statements\nSELECT query, calls, rows,\nwal_bytes > 0 as wal_bytes_generated,\nwal_records > 0 as wal_records_generated,\nwal_records >= rows as wal_records_ge_rows\nFROM edb_stat_statements ORDER BY query COLLATE \"C\";\n                            query                             | calls | rows | wal_bytes_generated | wal_records_generated | wal_records_ge_rows \n--------------------------------------------------------------+-------+------+---------------------+-----------------------+---------------------\n DELETE FROM pgss_wal_tab WHERE a > $1                        |     1 |    1 | t                   | t                     | t\n INSERT INTO pgss_wal_tab VALUES(generate_series($1, $2), $3) |     1 |   10 | t                   | t                     | t\n SELECT edb_stat_statements_reset() IS NOT NULL AS t          |     1 |    1 | f                   | f                     | f\n SET edb_stat_statements.track_utility = FALSE                |     1 |    0 | f                   | f                     | t\n UPDATE pgss_wal_tab SET b = $1 WHERE a > $2                  |     1 |    3 | t                   | t                     | t\n(5 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n"
  },
  {
    "path": "edb_stat_statements/expected/wal.out.18",
    "content": "--\n-- Validate WAL generation metrics\n--\nSET edb_stat_statements.track_utility = FALSE;\nCREATE TABLE pgss_wal_tab (a int, b char(20));\nINSERT INTO pgss_wal_tab VALUES(generate_series(1, 10), 'aaa');\nUPDATE pgss_wal_tab SET b = 'bbb' WHERE a > 7;\nDELETE FROM pgss_wal_tab WHERE a > 9;\nDROP TABLE pgss_wal_tab;\n-- Check WAL is generated for the above statements\nSELECT query, calls, rows,\nwal_bytes > 0 as wal_bytes_generated,\nwal_records > 0 as wal_records_generated,\nwal_records >= rows as wal_records_ge_rows\nFROM edb_stat_statements ORDER BY query COLLATE \"C\";\n                            query                             | calls | rows | wal_bytes_generated | wal_records_generated | wal_records_ge_rows \n--------------------------------------------------------------+-------+------+---------------------+-----------------------+---------------------\n DELETE FROM pgss_wal_tab WHERE a > $1                        |     1 |    1 | t                   | t                     | t\n INSERT INTO pgss_wal_tab VALUES(generate_series($1, $2), $3) |     1 |   10 | t                   | t                     | t\n SELECT edb_stat_statements_reset() IS NOT NULL AS t          |     1 |    1 | f                   | f                     | f\n SET edb_stat_statements.track_utility = $1                   |     1 |    0 | f                   | f                     | t\n UPDATE pgss_wal_tab SET b = $1 WHERE a > $2                  |     1 |    3 | t                   | t                     | t\n(5 rows)\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n t \n---\n t\n(1 row)\n\n"
  },
  {
    "path": "edb_stat_statements/sql/cleanup.sql",
    "content": "DROP EXTENSION edb_stat_statements;\n"
  },
  {
    "path": "edb_stat_statements/sql/cursors.sql",
    "content": "--\n-- Cursors\n--\n\n-- These tests require track_utility to be enabled.\nSET edb_stat_statements.track_utility = TRUE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- DECLARE\n-- SELECT is normalized.\nDECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT 1;\nCLOSE cursor_stats_1;\nDECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT 2;\nCLOSE cursor_stats_1;\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- FETCH\nBEGIN;\nDECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT 2;\nDECLARE cursor_stats_2 CURSOR WITH HOLD FOR SELECT 3;\nFETCH 1 IN cursor_stats_1;\nFETCH 1 IN cursor_stats_2;\nCLOSE cursor_stats_1;\nCLOSE cursor_stats_2;\nCOMMIT;\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n"
  },
  {
    "path": "edb_stat_statements/sql/dml.sql",
    "content": "--\n-- DMLs on test table\n--\n\nSET edb_stat_statements.track_utility = FALSE;\n\nCREATE TEMP TABLE pgss_dml_tab (a int, b char(20));\n\nINSERT INTO pgss_dml_tab VALUES(generate_series(1, 10), 'aaa');\nUPDATE pgss_dml_tab SET b = 'bbb' WHERE a > 7;\nDELETE FROM pgss_dml_tab WHERE a > 9;\n\n-- explicit transaction\nBEGIN;\nUPDATE pgss_dml_tab SET b = '111' WHERE a = 1 ;\nCOMMIT;\n\nBEGIN \\;\nUPDATE pgss_dml_tab SET b = '222' WHERE a = 2 \\;\nCOMMIT ;\n\nUPDATE pgss_dml_tab SET b = '333' WHERE a = 3 \\;\nUPDATE pgss_dml_tab SET b = '444' WHERE a = 4 ;\n\nBEGIN \\;\nUPDATE pgss_dml_tab SET b = '555' WHERE a = 5 \\;\nUPDATE pgss_dml_tab SET b = '666' WHERE a = 6 \\;\nCOMMIT ;\n\n-- many INSERT values\nINSERT INTO pgss_dml_tab (a, b) VALUES (1, 'a'), (2, 'b'), (3, 'c');\n\n-- SELECT with constants\nSELECT * FROM pgss_dml_tab WHERE a > 5 ORDER BY a ;\n\nSELECT *\n  FROM pgss_dml_tab\n  WHERE a > 9\n  ORDER BY a ;\n\n-- these two need to be done on a different table\n-- SELECT without constants\nSELECT * FROM pgss_dml_tab ORDER BY a;\n\n-- SELECT with IN clause\nSELECT * FROM pgss_dml_tab WHERE a IN (1, 2, 3, 4, 5);\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- MERGE\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN MATCHED THEN UPDATE SET b = st.b || st.a::text;\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN MATCHED THEN UPDATE SET b = pgss_dml_tab.b || st.a::text;\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN MATCHED AND length(st.b) > 1 THEN UPDATE SET b = pgss_dml_tab.b || st.a::text;\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)\n WHEN NOT MATCHED THEN INSERT (a, b) VALUES (0, NULL);\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)\n WHEN NOT MATCHED THEN INSERT VALUES (0, NULL);\t-- same as above\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)\n WHEN NOT MATCHED THEN INSERT (b, a) VALUES (NULL, 0);\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a)\n WHEN NOT MATCHED THEN INSERT (a) VALUES (0);\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN MATCHED THEN DELETE;\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN MATCHED THEN DO NOTHING;\nMERGE INTO pgss_dml_tab USING pgss_dml_tab st ON (st.a = pgss_dml_tab.a AND st.a >= 4)\n WHEN NOT MATCHED THEN DO NOTHING;\n\nDROP TABLE pgss_dml_tab;\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n\n-- check that [temp] table relation extensions are tracked as writes\nCREATE TABLE pgss_extend_tab (a int, b text);\nCREATE TEMP TABLE pgss_extend_temp_tab (a int, b text);\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\nINSERT INTO pgss_extend_tab (a, b) SELECT generate_series(1, 1000), 'something';\nINSERT INTO pgss_extend_temp_tab (a, b) SELECT generate_series(1, 1000), 'something';\nWITH sizes AS (\n  SELECT\n    pg_relation_size('pgss_extend_tab') / current_setting('block_size')::int8 AS rel_size,\n    pg_relation_size('pgss_extend_temp_tab') / current_setting('block_size')::int8 AS temp_rel_size\n)\nSELECT\n    SUM(local_blks_written) >= (SELECT temp_rel_size FROM sizes) AS temp_written_ok,\n    SUM(local_blks_dirtied) >= (SELECT temp_rel_size FROM sizes) AS temp_dirtied_ok,\n    SUM(shared_blks_written) >= (SELECT rel_size FROM sizes) AS written_ok,\n    SUM(shared_blks_dirtied) >= (SELECT rel_size FROM sizes) AS dirtied_ok\nFROM edb_stat_statements;\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n"
  },
  {
    "path": "edb_stat_statements/sql/entry_timestamp.sql",
    "content": "--\n-- statement timestamps\n--\n\n-- planning time is needed during tests\nSET edb_stat_statements.track_planning = TRUE;\n\nSELECT 1 AS \"STMTTS1\";\nSELECT now() AS ref_ts \\gset\nSELECT 1,2 AS \"STMTTS2\";\nSELECT stats_since >= :'ref_ts', count(*) FROM edb_stat_statements\nWHERE query LIKE '%STMTTS%'\nGROUP BY stats_since >= :'ref_ts'\nORDER BY stats_since >= :'ref_ts';\n\nSELECT now() AS ref_ts \\gset\nSELECT\n  count(*) as total,\n  count(*) FILTER (\n    WHERE min_plan_time + max_plan_time = 0\n  ) as minmax_plan_zero,\n  count(*) FILTER (\n    WHERE min_exec_time + max_exec_time = 0\n  ) as minmax_exec_zero,\n  count(*) FILTER (\n    WHERE minmax_stats_since >= :'ref_ts'\n  ) as minmax_stats_since_after_ref,\n  count(*) FILTER (\n    WHERE stats_since >= :'ref_ts'\n  ) as stats_since_after_ref\nFROM edb_stat_statements\nWHERE query LIKE '%STMTTS%';\n\n-- Perform single min/max reset\nSELECT edb_stat_statements_reset(0, '{}', queryid, true) AS minmax_reset_ts\nFROM edb_stat_statements\nWHERE query LIKE '%STMTTS1%' \\gset\n\n-- check\nSELECT\n  count(*) as total,\n  count(*) FILTER (\n    WHERE min_plan_time + max_plan_time = 0\n  ) as minmax_plan_zero,\n  count(*) FILTER (\n    WHERE min_exec_time + max_exec_time = 0\n  ) as minmax_exec_zero,\n  count(*) FILTER (\n    WHERE minmax_stats_since >= :'ref_ts'\n  ) as minmax_stats_since_after_ref,\n  count(*) FILTER (\n    WHERE stats_since >= :'ref_ts'\n  ) as stats_since_after_ref\nFROM edb_stat_statements\nWHERE query LIKE '%STMTTS%';\n\n-- check minmax reset timestamps\nSELECT\nquery, minmax_stats_since = :'minmax_reset_ts' AS reset_ts_match\nFROM edb_stat_statements\nWHERE query LIKE '%STMTTS%'\nORDER BY query COLLATE \"C\";\n\n-- check that minmax reset does not set stats_reset\nSELECT\nstats_reset = :'minmax_reset_ts' AS stats_reset_ts_match\nFROM edb_stat_statements_info;\n\n-- Perform common min/max reset\nSELECT edb_stat_statements_reset(0, '{}', 0, true) AS minmax_reset_ts \\gset\n\n-- check again\nSELECT\n  count(*) as total,\n  count(*) FILTER (\n    WHERE min_plan_time + max_plan_time = 0\n  ) as minmax_plan_zero,\n  count(*) FILTER (\n    WHERE min_exec_time + max_exec_time = 0\n  ) as minmax_exec_zero,\n  count(*) FILTER (\n    WHERE minmax_stats_since >= :'ref_ts'\n  ) as minmax_ts_after_ref,\n  count(*) FILTER (\n    WHERE minmax_stats_since = :'minmax_reset_ts'\n  ) as minmax_ts_match,\n  count(*) FILTER (\n    WHERE stats_since >= :'ref_ts'\n  ) as stats_since_after_ref\nFROM edb_stat_statements\nWHERE query LIKE '%STMTTS%';\n\n-- Execute first query once more to check stats update\nSELECT 1 AS \"STMTTS1\";\n\n-- check\n-- we don't check planing times here to be independent of\n-- plan caching approach\nSELECT\n  count(*) as total,\n  count(*) FILTER (\n    WHERE min_exec_time + max_exec_time = 0\n  ) as minmax_exec_zero,\n  count(*) FILTER (\n    WHERE minmax_stats_since >= :'ref_ts'\n  ) as minmax_ts_after_ref,\n  count(*) FILTER (\n    WHERE stats_since >= :'ref_ts'\n  ) as stats_since_after_ref\nFROM edb_stat_statements\nWHERE query LIKE '%STMTTS%';\n\n-- Cleanup\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n"
  },
  {
    "path": "edb_stat_statements/sql/extended.sql",
    "content": "-- Tests with extended query protocol\n\nSET edb_stat_statements.track_utility = FALSE;\n\n-- This test checks that an execute message sets a query ID.\nSELECT query_id IS NOT NULL AS query_id_set\n  FROM pg_stat_activity WHERE pid = pg_backend_pid() \\bind \\g\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\nSELECT $1 \\parse stmt1\nSELECT $1, $2 \\parse stmt2\nSELECT $1, $2, $3 \\parse stmt3\nSELECT $1 \\bind 'unnamed_val1' \\g\n\\bind_named stmt1 'stmt1_val1' \\g\n\\bind_named stmt2 'stmt2_val1' 'stmt2_val2' \\g\n\\bind_named stmt3 'stmt3_val1' 'stmt3_val2' 'stmt3_val3' \\g\n\\bind_named stmt3 'stmt3_val4' 'stmt3_val5' 'stmt3_val6' \\g\n\\bind_named stmt2 'stmt2_val3' 'stmt2_val4' \\g\n\\bind_named stmt1 'stmt1_val1' \\g\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n"
  },
  {
    "path": "edb_stat_statements/sql/level_tracking.sql",
    "content": "--\n-- Statement level tracking\n--\n\nSET edb_stat_statements.track_utility = TRUE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- DO block - top-level tracking.\nCREATE TABLE stats_track_tab (x int);\nSET edb_stat_statements.track = 'dev';\nDELETE FROM stats_track_tab;\nDO $$\nBEGIN\n  DELETE FROM stats_track_tab;\nEND;\n$$ LANGUAGE plpgsql;\nSELECT toplevel, calls, query FROM edb_stat_statements\n  WHERE query LIKE '%DELETE%' ORDER BY query COLLATE \"C\", toplevel;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- DO block - all-level tracking.\nSET edb_stat_statements.track = 'dev-nested';\nDELETE FROM stats_track_tab;\nDO $$\nBEGIN\n  DELETE FROM stats_track_tab;\nEND; $$;\nDO LANGUAGE plpgsql $$\nBEGIN\n  -- this is a SELECT\n  PERFORM 'hello world'::TEXT;\nEND; $$;\nSELECT toplevel, calls, query FROM edb_stat_statements\n  ORDER BY query COLLATE \"C\", toplevel;\n\n-- Procedure with multiple utility statements.\nCREATE OR REPLACE PROCEDURE proc_with_utility_stmt()\nLANGUAGE SQL\nAS $$\n  SHOW edb_stat_statements.track;\n  show edb_stat_statements.track;\n  SHOW edb_stat_statements.track_utility;\n$$;\nSET edb_stat_statements.track_utility = TRUE;\n-- all-level tracking.\nSET edb_stat_statements.track = 'dev-nested';\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\nCALL proc_with_utility_stmt();\nSELECT toplevel, calls, query FROM edb_stat_statements\n  ORDER BY query COLLATE \"C\", toplevel;\n-- top-level tracking.\nSET edb_stat_statements.track = 'dev';\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\nCALL proc_with_utility_stmt();\nSELECT toplevel, calls, query FROM edb_stat_statements\n  ORDER BY query COLLATE \"C\", toplevel;\n\n-- DO block - top-level tracking without utility.\nSET edb_stat_statements.track = 'dev';\nSET edb_stat_statements.track_utility = FALSE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\nDELETE FROM stats_track_tab;\nDO $$\nBEGIN\n  DELETE FROM stats_track_tab;\nEND; $$;\nDO LANGUAGE plpgsql $$\nBEGIN\n  -- this is a SELECT\n  PERFORM 'hello world'::TEXT;\nEND; $$;\nSELECT toplevel, calls, query FROM edb_stat_statements\n  ORDER BY query COLLATE \"C\", toplevel;\n\n-- DO block - all-level tracking without utility.\nSET edb_stat_statements.track = 'dev-nested';\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\nDELETE FROM stats_track_tab;\nDO $$\nBEGIN\n  DELETE FROM stats_track_tab;\nEND; $$;\nDO LANGUAGE plpgsql $$\nBEGIN\n  -- this is a SELECT\n  PERFORM 'hello world'::TEXT;\nEND; $$;\nSELECT toplevel, calls, query FROM edb_stat_statements\n  ORDER BY query COLLATE \"C\", toplevel;\n\n-- PL/pgSQL function - top-level tracking.\nSET edb_stat_statements.track = 'dev';\nSET edb_stat_statements.track_utility = FALSE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\nCREATE FUNCTION PLUS_TWO(i INTEGER) RETURNS INTEGER AS $$\nDECLARE\n  r INTEGER;\nBEGIN\n  SELECT (i + 1 + 1.0)::INTEGER INTO r;\n  RETURN r;\nEND; $$ LANGUAGE plpgsql;\n\nSELECT PLUS_TWO(3);\nSELECT PLUS_TWO(7);\n\n-- SQL function --- use LIMIT to keep it from being inlined\nCREATE FUNCTION PLUS_ONE(i INTEGER) RETURNS INTEGER AS\n$$ SELECT (i + 1.0)::INTEGER LIMIT 1 $$ LANGUAGE SQL;\n\nSELECT PLUS_ONE(8);\nSELECT PLUS_ONE(10);\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n\n-- immutable SQL function --- can be executed at plan time\nCREATE FUNCTION PLUS_THREE(i INTEGER) RETURNS INTEGER AS\n$$ SELECT i + 3 LIMIT 1 $$ IMMUTABLE LANGUAGE SQL;\n\nSELECT PLUS_THREE(8);\nSELECT PLUS_THREE(10);\n\nSELECT toplevel, calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n\n-- PL/pgSQL function - all-level tracking.\nSET edb_stat_statements.track = 'dev-nested';\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- we drop and recreate the functions to avoid any caching funnies\nDROP FUNCTION PLUS_ONE(INTEGER);\nDROP FUNCTION PLUS_TWO(INTEGER);\nDROP FUNCTION PLUS_THREE(INTEGER);\n\n-- PL/pgSQL function\nCREATE FUNCTION PLUS_TWO(i INTEGER) RETURNS INTEGER AS $$\nDECLARE\n  r INTEGER;\nBEGIN\n  SELECT (i + 1 + 1.0)::INTEGER INTO r;\n  RETURN r;\nEND; $$ LANGUAGE plpgsql;\n\nSELECT PLUS_TWO(-1);\nSELECT PLUS_TWO(2);\n\n-- SQL function --- use LIMIT to keep it from being inlined\nCREATE FUNCTION PLUS_ONE(i INTEGER) RETURNS INTEGER AS\n$$ SELECT (i + 1.0)::INTEGER LIMIT 1 $$ LANGUAGE SQL;\n\nSELECT PLUS_ONE(3);\nSELECT PLUS_ONE(1);\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n\n-- immutable SQL function --- can be executed at plan time\nCREATE FUNCTION PLUS_THREE(i INTEGER) RETURNS INTEGER AS\n$$ SELECT i + 3 LIMIT 1 $$ IMMUTABLE LANGUAGE SQL;\n\nSELECT PLUS_THREE(8);\nSELECT PLUS_THREE(10);\n\nSELECT toplevel, calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n\n--\n-- edb_stat_statements.track = none\n--\nSET edb_stat_statements.track = 'none';\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\nSELECT 1 AS \"one\";\nSELECT 1 + 1 AS \"two\";\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n"
  },
  {
    "path": "edb_stat_statements/sql/oldextversions.sql",
    "content": "-- test old extension version entry points\n\nCREATE EXTENSION edb_stat_statements WITH VERSION '1.0';\n\nSELECT pg_get_functiondef('edb_stat_statements_info'::regproc);\n\nSELECT pg_get_functiondef('edb_stat_statements_reset'::regproc);\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\\d edb_stat_statements\nSELECT count(*) > 0 AS has_data FROM edb_stat_statements;\n\nDROP EXTENSION edb_stat_statements;\n"
  },
  {
    "path": "edb_stat_statements/sql/parallel.sql",
    "content": "--\n-- Tests for parallel statistics\n--\n\nSET edb_stat_statements.track_utility = FALSE;\n\n-- encourage use of parallel plans\nSET parallel_setup_cost = 0;\nSET parallel_tuple_cost = 0;\nSET min_parallel_table_scan_size = 0;\nSET max_parallel_workers_per_gather = 2;\n\nCREATE TABLE pgss_parallel_tab (a int);\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\nSELECT count(*) FROM pgss_parallel_tab;\n\nSELECT query,\n  parallel_workers_to_launch > 0 AS has_workers_to_launch,\n  parallel_workers_launched > 0 AS has_workers_launched\n  FROM edb_stat_statements\n  WHERE query ~ 'SELECT count'\n  ORDER BY query COLLATE \"C\";\n\nDROP TABLE pgss_parallel_tab;\n"
  },
  {
    "path": "edb_stat_statements/sql/planning.sql",
    "content": "--\n-- Information related to planning\n--\n\n-- These tests require track_planning to be enabled.\nSET edb_stat_statements.track_planning = TRUE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n--\n-- [re]plan counting\n--\nCREATE TABLE stats_plan_test ();\nPREPARE prep1 AS SELECT COUNT(*) FROM stats_plan_test;\nEXECUTE prep1;\nEXECUTE prep1;\nEXECUTE prep1;\nALTER TABLE stats_plan_test ADD COLUMN x int;\nEXECUTE prep1;\nSELECT 42;\nSELECT 42;\nSELECT 42;\nSELECT plans, calls, rows, query FROM edb_stat_statements\n  WHERE query NOT LIKE 'PREPARE%' ORDER BY query COLLATE \"C\";\n-- for the prepared statement we expect at least one replan, but cache\n-- invalidations could force more\nSELECT plans >= 2 AND plans <= calls AS plans_ok, calls, rows, query FROM edb_stat_statements\n  WHERE query LIKE 'PREPARE%' ORDER BY query COLLATE \"C\";\n\n-- Cleanup\nDROP TABLE stats_plan_test;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n"
  },
  {
    "path": "edb_stat_statements/sql/privileges.sql",
    "content": "--\n-- Only superusers and roles with privileges of the pg_read_all_stats role\n-- are allowed to see the SQL text and queryid of queries executed by\n-- other users. Other users can see the statistics.\n--\n\nSET edb_stat_statements.track_utility = FALSE;\nCREATE ROLE regress_stats_superuser SUPERUSER;\nCREATE ROLE regress_stats_user1;\nCREATE ROLE regress_stats_user2;\nGRANT pg_read_all_stats TO regress_stats_user2;\n\nSET ROLE regress_stats_superuser;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\nSELECT 1 AS \"ONE\";\n\nSET ROLE regress_stats_user1;\nSELECT 1+1 AS \"TWO\";\n\n--\n-- A superuser can read all columns of queries executed by others,\n-- including query text and queryid.\n--\n\nSET ROLE regress_stats_superuser;\nSELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.query, ss.calls, ss.rows\n  FROM edb_stat_statements ss JOIN pg_roles r ON ss.userid = r.oid\n  ORDER BY r.rolname, ss.query COLLATE \"C\", ss.calls, ss.rows;\n\n--\n-- regress_stats_user1 has no privileges to read the query text or\n-- queryid of queries executed by others but can see statistics\n-- like calls and rows.\n--\n\nSET ROLE regress_stats_user1;\nSELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.query, ss.calls, ss.rows\n  FROM edb_stat_statements ss JOIN pg_roles r ON ss.userid = r.oid\n  ORDER BY r.rolname, ss.query COLLATE \"C\", ss.calls, ss.rows;\n\n--\n-- regress_stats_user2, with pg_read_all_stats role privileges, can\n-- read all columns, including query text and queryid, of queries\n-- executed by others.\n--\n\nSET ROLE regress_stats_user2;\nSELECT r.rolname, ss.queryid <> 0 AS queryid_bool, ss.query, ss.calls, ss.rows\n  FROM edb_stat_statements ss JOIN pg_roles r ON ss.userid = r.oid\n  ORDER BY r.rolname, ss.query COLLATE \"C\", ss.calls, ss.rows;\n\n--\n-- cleanup\n--\n\nRESET ROLE;\nDROP ROLE regress_stats_superuser;\nDROP ROLE regress_stats_user1;\nDROP ROLE regress_stats_user2;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n"
  },
  {
    "path": "edb_stat_statements/sql/select.sql",
    "content": "--\n-- SELECT statements\n--\n\nCREATE EXTENSION edb_stat_statements;\nSET edb_stat_statements.track_utility = FALSE;\nSET edb_stat_statements.track_planning = TRUE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n--\n-- simple and compound statements\n--\nSELECT 1 AS \"int\";\n\nSELECT 'hello'\n  -- multiline\n  AS \"text\";\n\nSELECT 'world' AS \"text\";\n\n-- transaction\nBEGIN;\nSELECT 1 AS \"int\";\nSELECT 'hello' AS \"text\";\nCOMMIT;\n\n-- compound transaction\nBEGIN \\;\nSELECT 2.0 AS \"float\" \\;\nSELECT 'world' AS \"text\" \\;\nCOMMIT;\n\n-- compound with empty statements and spurious leading spacing\n\\;\\;   SELECT 3 + 3 \\;\\;\\;   SELECT ' ' || ' !' \\;\\;   SELECT 1 + 4 \\;;\n\n-- non ;-terminated statements\nSELECT 1 + 1 + 1 AS \"add\" \\gset\nSELECT :add + 1 + 1 AS \"add\" \\;\nSELECT :add + 1 + 1 AS \"add\" \\gset\n\n-- set operator\nSELECT 1 AS i UNION SELECT 2 ORDER BY i;\n\n-- ? operator\nselect '{\"a\":1, \"b\":2}'::jsonb ? 'b';\n\n-- cte\nWITH t(f) AS (\n  VALUES (1.0), (2.0)\n)\n  SELECT f FROM t ORDER BY f;\n\n-- prepared statement with parameter\nPREPARE pgss_test (int) AS SELECT $1, 'test' LIMIT 1;\nEXECUTE pgss_test(1);\nDEALLOCATE pgss_test;\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n--\n-- queries with locking clauses\n--\nCREATE TABLE pgss_a (id integer PRIMARY KEY);\nCREATE TABLE pgss_b (id integer PRIMARY KEY, a_id integer REFERENCES pgss_a);\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- control query\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id;\n\n-- test range tables\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE;\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_a;\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_b;\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_a, pgss_b; -- matches plain \"FOR UPDATE\"\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE OF pgss_b, pgss_a;\n\n-- test strengths\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR NO KEY UPDATE;\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR SHARE;\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR KEY SHARE;\n\n-- test wait policies\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE NOWAIT;\nSELECT * FROM pgss_a JOIN pgss_b ON pgss_b.a_id = pgss_a.id FOR UPDATE SKIP LOCKED;\n\nSELECT calls, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n\nDROP TABLE pgss_a, pgss_b CASCADE;\n\n--\n-- access to edb_stat_statements_info view\n--\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\nSELECT dealloc FROM edb_stat_statements_info;\n\n-- FROM [ONLY]\nCREATE TABLE tbl_inh(id integer);\nCREATE TABLE tbl_inh_1() INHERITS (tbl_inh);\nINSERT INTO tbl_inh_1 SELECT 1;\n\nSELECT * FROM tbl_inh;\nSELECT * FROM ONLY tbl_inh;\n\nSELECT COUNT(*) FROM edb_stat_statements WHERE query LIKE '%FROM%tbl_inh%';\n\n-- WITH TIES\nCREATE TABLE limitoption AS SELECT 0 AS val FROM generate_series(1, 10);\nSELECT *\nFROM limitoption\nWHERE val < 2\nORDER BY val\nFETCH FIRST 2 ROWS WITH TIES;\n\nSELECT *\nFROM limitoption\nWHERE val < 2\nORDER BY val\nFETCH FIRST 2 ROW ONLY;\n\nSELECT COUNT(*) FROM edb_stat_statements WHERE query LIKE '%FETCH FIRST%';\n\n-- GROUP BY [DISTINCT]\nSELECT a, b, c\nFROM (VALUES (1, 2, 3), (4, NULL, 6), (7, 8, 9)) AS t (a, b, c)\nGROUP BY ROLLUP(a, b), rollup(a, c)\nORDER BY a, b, c;\nSELECT a, b, c\nFROM (VALUES (1, 2, 3), (4, NULL, 6), (7, 8, 9)) AS t (a, b, c)\nGROUP BY DISTINCT ROLLUP(a, b), rollup(a, c)\nORDER BY a, b, c;\n\nSELECT COUNT(*) FROM edb_stat_statements WHERE query LIKE '%GROUP BY%ROLLUP%';\n\n-- GROUPING SET agglevelsup\nSELECT (\n  SELECT (\n    SELECT GROUPING(a,b) FROM (VALUES (1)) v2(c)\n  ) FROM (VALUES (1,2)) v1(a,b) GROUP BY (a,b)\n) FROM (VALUES(6,7)) v3(e,f) GROUP BY ROLLUP(e,f);\nSELECT (\n  SELECT (\n    SELECT GROUPING(e,f) FROM (VALUES (1)) v2(c)\n  ) FROM (VALUES (1,2)) v1(a,b) GROUP BY (a,b)\n) FROM (VALUES(6,7)) v3(e,f) GROUP BY ROLLUP(e,f);\n\nSELECT COUNT(*) FROM edb_stat_statements WHERE query LIKE '%SELECT GROUPING%';\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n"
  },
  {
    "path": "edb_stat_statements/sql/user_activity.sql",
    "content": "--\n-- Track user activity and reset them\n--\n\nSET edb_stat_statements.track_utility = TRUE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\nCREATE ROLE regress_stats_user1;\nCREATE ROLE regress_stats_user2;\n\nSET ROLE regress_stats_user1;\n\nSELECT 1 AS \"ONE\";\nSELECT 1+1 AS \"TWO\";\n\nRESET ROLE;\nSET ROLE regress_stats_user2;\n\nSELECT 1 AS \"ONE\";\nSELECT 1+1 AS \"TWO\";\n\nRESET ROLE;\nSELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n\n--\n-- Don't reset anything if any of the parameter is NULL\n--\nSELECT edb_stat_statements_reset(NULL) IS NOT NULL AS t;\nSELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n\n--\n-- remove query ('SELECT $1+$2 AS \"TWO\"') executed by regress_stats_user2\n-- in the current_database\n--\nSELECT edb_stat_statements_reset(\n\t(SELECT r.oid FROM pg_roles AS r WHERE r.rolname = 'regress_stats_user2'),\n\tARRAY(SELECT d.oid FROM pg_database As d where datname = current_database()),\n\t(SELECT s.queryid FROM edb_stat_statements AS s\n\t\t\t\tWHERE s.query = 'SELECT $1+$2 AS \"TWO\"' LIMIT 1))\n\tIS NOT NULL AS t;\nSELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n\n--\n-- remove query ('SELECT $1 AS \"ONE\"') executed by two users\n--\nSELECT edb_stat_statements_reset(0,'{}',s.queryid) IS NOT NULL AS t\n\tFROM edb_stat_statements AS s WHERE s.query = 'SELECT $1 AS \"ONE\"';\nSELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n\n--\n-- remove query of a user (regress_stats_user1)\n--\nSELECT edb_stat_statements_reset(r.oid) IS NOT NULL AS t\n\t\tFROM pg_roles AS r WHERE r.rolname = 'regress_stats_user1';\nSELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n\n--\n-- reset all\n--\nSELECT edb_stat_statements_reset(0,'{}',0) IS NOT NULL AS t;\nSELECT query, calls, rows FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n\n--\n-- cleanup\n--\nDROP ROLE regress_stats_user1;\nDROP ROLE regress_stats_user2;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n"
  },
  {
    "path": "edb_stat_statements/sql/utility.sql",
    "content": "--\n-- Utility commands\n--\n\n-- These tests require track_utility to be enabled.\nSET edb_stat_statements.track_utility = TRUE;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- Tables, indexes, triggers\nCREATE TEMP TABLE tab_stats (a int, b char(20));\nCREATE INDEX index_stats ON tab_stats(b, (b || 'data1'), (b || 'data2')) WHERE a > 0;\nALTER TABLE tab_stats ALTER COLUMN b set default 'a';\nALTER TABLE tab_stats ALTER COLUMN b TYPE text USING 'data' || b;\nALTER TABLE tab_stats ADD CONSTRAINT a_nonzero CHECK (a <> 0);\nDROP TABLE tab_stats \\;\nDROP TABLE IF EXISTS tab_stats \\;\n-- This DROP query uses two different strings, still they count as one entry.\nDROP TABLE IF EXISTS tab_stats \\;\nDrop Table If Exists tab_stats \\;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- Partitions\nCREATE TABLE pt_stats (a int, b int) PARTITION BY range (a);\nCREATE TABLE pt_stats1 (a int, b int);\nALTER TABLE pt_stats ATTACH PARTITION pt_stats1 FOR VALUES FROM (0) TO (100);\nCREATE TABLE pt_stats2 PARTITION OF pt_stats FOR VALUES FROM (100) TO (200);\nCREATE INDEX pt_stats_index ON ONLY pt_stats (a);\nCREATE INDEX pt_stats2_index ON ONLY pt_stats2 (a);\nALTER INDEX pt_stats_index ATTACH PARTITION pt_stats2_index;\nDROP TABLE pt_stats;\n\n-- Views\nCREATE VIEW view_stats AS SELECT 1::int AS a, 2::int AS b;\nALTER VIEW view_stats ALTER COLUMN a SET DEFAULT 2;\nDROP VIEW view_stats;\n\n-- Foreign tables\nCREATE FOREIGN DATA WRAPPER wrapper_stats;\nCREATE SERVER server_stats FOREIGN DATA WRAPPER wrapper_stats;\nCREATE FOREIGN TABLE foreign_stats (a int) SERVER server_stats;\nALTER FOREIGN TABLE foreign_stats ADD COLUMN b integer DEFAULT 1;\nALTER FOREIGN TABLE foreign_stats ADD CONSTRAINT b_nonzero CHECK (b <> 0);\nDROP FOREIGN TABLE foreign_stats;\nDROP SERVER server_stats;\nDROP FOREIGN DATA WRAPPER wrapper_stats;\n\n-- Functions\nCREATE FUNCTION func_stats(a text DEFAULT 'a_data', b text DEFAULT lower('b_data'))\n  RETURNS text AS $$ SELECT $1::text || '_' || $2::text; $$ LANGUAGE SQL\n  SET work_mem = '256kB';\nDROP FUNCTION func_stats;\n\n-- Rules\nCREATE TABLE tab_rule_stats (a int, b int);\nCREATE TABLE tab_rule_stats_2 (a int, b int, c int, d int);\nCREATE RULE rules_stats AS ON INSERT TO tab_rule_stats DO INSTEAD\n  INSERT INTO tab_rule_stats_2 VALUES(new.*, 1, 2);\nDROP RULE rules_stats ON tab_rule_stats;\nDROP TABLE tab_rule_stats, tab_rule_stats_2;\n\n-- Types\nCREATE TYPE stats_type as (f1 numeric(35, 6), f2 numeric(35, 2));\nDROP TYPE stats_type;\n\n-- Triggers\nCREATE TABLE trigger_tab_stats (a int, b int);\nCREATE FUNCTION trigger_func_stats () RETURNS trigger LANGUAGE plpgsql\n  AS $$ BEGIN return OLD; end; $$;\nCREATE TRIGGER trigger_tab_stats\n    AFTER UPDATE ON trigger_tab_stats\n    FOR EACH ROW WHEN (OLD.a < 0 AND OLD.b < 1 AND true)\n    EXECUTE FUNCTION trigger_func_stats();\nDROP TABLE trigger_tab_stats;\n\n-- Policies\nCREATE TABLE tab_policy_stats (a int, b int);\nCREATE POLICY policy_stats ON tab_policy_stats USING (a = 5) WITH CHECK (b < 5);\nDROP TABLE tab_policy_stats;\n\n-- Statistics\nCREATE TABLE tab_expr_stats (a int, b int);\nCREATE STATISTICS tab_expr_stats_1 (mcv) ON a, (2*a), (3*b) FROM tab_expr_stats;\nDROP TABLE tab_expr_stats;\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- Transaction statements\nBEGIN;\nABORT;\nBEGIN;\nROLLBACK;\n-- WORK\nBEGIN WORK;\nCOMMIT WORK;\nBEGIN WORK;\nABORT WORK;\n-- TRANSACTION\nBEGIN TRANSACTION;\nCOMMIT TRANSACTION;\nBEGIN TRANSACTION;\nABORT TRANSACTION;\n-- More isolation levels\nBEGIN TRANSACTION DEFERRABLE;\nCOMMIT TRANSACTION AND NO CHAIN;\nBEGIN ISOLATION LEVEL SERIALIZABLE;\nCOMMIT;\nBEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;\nCOMMIT;\n-- List of A_Const nodes, same lists.\nBEGIN TRANSACTION READ ONLY, READ WRITE, DEFERRABLE, NOT DEFERRABLE;\nCOMMIT;\nBEGIN TRANSACTION NOT DEFERRABLE, READ ONLY, READ WRITE, DEFERRABLE;\nCOMMIT;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- Two-phase transactions\nBEGIN;\nPREPARE TRANSACTION 'stat_trans1';\nCOMMIT PREPARED 'stat_trans1';\nBEGIN;\nPREPARE TRANSACTION 'stat_trans2';\nROLLBACK PREPARED 'stat_trans2';\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- Savepoints\nBEGIN;\nSAVEPOINT sp1;\nSAVEPOINT sp2;\nSAVEPOINT sp3;\nSAVEPOINT sp4;\nROLLBACK TO sp4;\nROLLBACK TO SAVEPOINT sp4;\nROLLBACK TRANSACTION TO SAVEPOINT sp3;\nRELEASE sp3;\nRELEASE SAVEPOINT sp2;\nROLLBACK TO sp1;\nRELEASE SAVEPOINT sp1;\nCOMMIT;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- EXPLAIN statements\n-- A Query is used, normalized by the query jumbling.\nEXPLAIN (costs off) SELECT 1;\nEXPLAIN (costs off) SELECT 2;\nEXPLAIN (costs off) SELECT a FROM generate_series(1,10) AS tab(a) WHERE a = 3;\nEXPLAIN (costs off) SELECT a FROM generate_series(1,10) AS tab(a) WHERE a = 7;\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n\n-- CALL\nCREATE OR REPLACE PROCEDURE sum_one(i int) AS $$\nDECLARE\n  r int;\nBEGIN\n  SELECT (i + i)::int INTO r;\nEND; $$ LANGUAGE plpgsql;\nCREATE OR REPLACE PROCEDURE sum_two(i int, j int) AS $$\nDECLARE\n  r int;\nBEGIN\n  SELECT (i + j)::int INTO r;\nEND; $$ LANGUAGE plpgsql;\n-- Overloaded functions.\nCREATE OR REPLACE PROCEDURE overload(i int) AS $$\nDECLARE\n  r int;\nBEGIN\n  SELECT (i + i)::int INTO r;\nEND; $$ LANGUAGE plpgsql;\nCREATE OR REPLACE PROCEDURE overload(i text) AS $$\nDECLARE\n  r text;\nBEGIN\n  SELECT i::text INTO r;\nEND; $$ LANGUAGE plpgsql;\n-- Mix of IN/OUT parameters.\nCREATE OR REPLACE PROCEDURE in_out(i int, i2 OUT int, i3 INOUT int) AS $$\nDECLARE\n  r int;\nBEGIN\n  i2 := i;\n  i3 := i3 + i;\nEND; $$ LANGUAGE plpgsql;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\nCALL sum_one(3);\nCALL sum_one(199);\nCALL sum_two(1,1);\nCALL sum_two(1,2);\nCALL overload(1);\nCALL overload('A');\nCALL in_out(1, NULL, 1);\nCALL in_out(2, 1, 2);\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n\n-- COPY\nCREATE TABLE copy_stats (a int, b int);\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n-- Some queries with A_Const nodes.\nCOPY (SELECT 1) TO STDOUT;\nCOPY (SELECT 2) TO STDOUT;\nCOPY (INSERT INTO copy_stats VALUES (1, 1) RETURNING *) TO STDOUT;\nCOPY (INSERT INTO copy_stats VALUES (2, 2) RETURNING *) TO STDOUT;\nCOPY (UPDATE copy_stats SET b = b + 1 RETURNING *) TO STDOUT;\nCOPY (UPDATE copy_stats SET b = b + 2 RETURNING *) TO STDOUT;\nCOPY (DELETE FROM copy_stats WHERE a = 1 RETURNING *) TO STDOUT;\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nDROP TABLE copy_stats;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- CREATE TABLE AS\n-- SELECT queries are normalized, creating matching query IDs.\nCREATE TABLE ctas_stats_1 AS SELECT 1 AS a;\nDROP TABLE ctas_stats_1;\nCREATE TABLE ctas_stats_1 AS SELECT 2 AS a;\nDROP TABLE ctas_stats_1;\nCREATE TABLE ctas_stats_2 AS\n  SELECT a AS col1, 2::int AS col2\n    FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2;\nDROP TABLE ctas_stats_2;\nCREATE TABLE ctas_stats_2 AS\n  SELECT a AS col1, 4::int AS col2\n    FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 1;\nDROP TABLE ctas_stats_2;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- CREATE MATERIALIZED VIEW\n-- SELECT queries are normalized, creating matching query IDs.\nCREATE MATERIALIZED VIEW matview_stats_1 AS\n  SELECT a AS col1, 2::int AS col2\n    FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2;\nDROP MATERIALIZED VIEW matview_stats_1;\nCREATE MATERIALIZED VIEW matview_stats_1 AS\n  SELECT a AS col1, 4::int AS col2\n    FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 3;\nDROP MATERIALIZED VIEW matview_stats_1;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- CREATE VIEW\nCREATE VIEW view_stats_1 AS\n  SELECT a AS col1, 2::int AS col2\n    FROM generate_series(1, 10) AS tab(a) WHERE a < 5 AND a > 2;\nDROP VIEW view_stats_1;\nCREATE VIEW view_stats_1 AS\n  SELECT a AS col1, 4::int AS col2\n    FROM generate_series(1, 5) AS tab(a) WHERE a < 4 AND a > 3;\nDROP VIEW view_stats_1;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- Domains\nCREATE DOMAIN domain_stats AS int CHECK (VALUE > 0);\nALTER DOMAIN domain_stats SET DEFAULT '3';\nALTER DOMAIN domain_stats ADD CONSTRAINT higher_than_one CHECK (VALUE > 1);\nDROP DOMAIN domain_stats;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- Execution statements\nSELECT 1 as a;\nPREPARE stat_select AS SELECT $1 AS a;\nEXECUTE stat_select (1);\nDEALLOCATE stat_select;\nPREPARE stat_select AS SELECT $1 AS a;\nEXECUTE stat_select (2);\nDEALLOCATE PREPARE stat_select;\nDEALLOCATE ALL;\nDEALLOCATE PREPARE ALL;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- SET statements.\n-- These use two different strings, still they count as one entry.\nCREATE ROLE regress_stat_set_1;\nCREATE ROLE regress_stat_set_2;\nSET work_mem = '1MB';\nSet work_mem = '1MB';\nSET work_mem = '2MB';\nSET work_mem = DEFAULT;\nSET work_mem TO DEFAULT;\nSET work_mem FROM CURRENT;\nBEGIN;\nSET LOCAL work_mem = '128kB';\nSET LOCAL work_mem = '256kB';\nSET LOCAL work_mem = DEFAULT;\nSET LOCAL work_mem TO DEFAULT;\nSET LOCAL work_mem FROM CURRENT;\nCOMMIT;\nRESET work_mem;\nSET enable_seqscan = off;\nSET enable_seqscan = on;\nSET SESSION work_mem = '300kB';\nSET SESSION work_mem = '400kB';\nRESET enable_seqscan;\n-- SET TRANSACTION ISOLATION\nBEGIN;\nSET TRANSACTION ISOLATION LEVEL READ COMMITTED;\nSET TRANSACTION ISOLATION LEVEL REPEATABLE READ;\nSET TRANSACTION ISOLATION LEVEL SERIALIZABLE;\nCOMMIT;\n-- SET SESSION AUTHORIZATION\nSET SESSION SESSION AUTHORIZATION DEFAULT;\nSET SESSION AUTHORIZATION 'regress_stat_set_1';\nSET SESSION AUTHORIZATION 'regress_stat_set_2';\nRESET SESSION AUTHORIZATION;\nBEGIN;\nSET LOCAL SESSION AUTHORIZATION DEFAULT;\nSET LOCAL SESSION AUTHORIZATION 'regress_stat_set_1';\nSET LOCAL SESSION AUTHORIZATION 'regress_stat_set_2';\nRESET SESSION AUTHORIZATION;\nCOMMIT;\n-- SET SESSION CHARACTERISTICS\nSET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;\nSET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ ONLY;\nSET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, READ WRITE;\n-- SET XML OPTION\nSET XML OPTION DOCUMENT;\nSET XML OPTION CONTENT;\n-- SET TIME ZONE\nSET TIME ZONE 'America/New_York';\nSET TIME ZONE 'Asia/Tokyo';\nSET TIME ZONE DEFAULT;\nSET TIME ZONE LOCAL;\nSET TIME ZONE 'CST7CDT,M4.1.0,M10.5.0';\nRESET TIME ZONE;\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\nDROP ROLE regress_stat_set_1;\nDROP ROLE regress_stat_set_2;\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n--\n-- Track the total number of rows retrieved or affected by the utility\n-- commands of COPY, FETCH, CREATE TABLE AS, CREATE MATERIALIZED VIEW,\n-- REFRESH MATERIALIZED VIEW and SELECT INTO\n--\nCREATE TABLE pgss_ctas AS SELECT a, 'ctas' b FROM generate_series(1, 10) a;\nSELECT generate_series(1, 10) c INTO pgss_select_into;\nCOPY pgss_ctas (a, b) FROM STDIN;\n11\tcopy\n12\tcopy\n13\tcopy\n\\.\nCREATE MATERIALIZED VIEW pgss_matv AS SELECT * FROM pgss_ctas;\nREFRESH MATERIALIZED VIEW pgss_matv;\nBEGIN;\nDECLARE pgss_cursor CURSOR FOR SELECT * FROM pgss_matv;\nFETCH NEXT pgss_cursor;\nFETCH FORWARD 5 pgss_cursor;\nFETCH FORWARD ALL pgss_cursor;\nCOMMIT;\n\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n\nDROP MATERIALIZED VIEW pgss_matv;\nDROP TABLE pgss_ctas;\nDROP TABLE pgss_select_into;\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n\n-- Special cases.  Keep these ones at the end to avoid conflicts.\nSET SCHEMA 'foo';\nSET SCHEMA 'public';\nRESET ALL;\nSELECT calls, rows, query FROM edb_stat_statements ORDER BY query COLLATE \"C\";\n\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n"
  },
  {
    "path": "edb_stat_statements/sql/wal.sql",
    "content": "--\n-- Validate WAL generation metrics\n--\n\nSET edb_stat_statements.track_utility = FALSE;\n\nCREATE TABLE pgss_wal_tab (a int, b char(20));\n\nINSERT INTO pgss_wal_tab VALUES(generate_series(1, 10), 'aaa');\nUPDATE pgss_wal_tab SET b = 'bbb' WHERE a > 7;\nDELETE FROM pgss_wal_tab WHERE a > 9;\nDROP TABLE pgss_wal_tab;\n\n-- Check WAL is generated for the above statements\nSELECT query, calls, rows,\nwal_bytes > 0 as wal_bytes_generated,\nwal_records > 0 as wal_records_generated,\nwal_records >= rows as wal_records_ge_rows\nFROM edb_stat_statements ORDER BY query COLLATE \"C\";\nSELECT edb_stat_statements_reset() IS NOT NULL AS t;\n"
  },
  {
    "path": "edb_stat_statements/t/010_restart.pl",
    "content": "# Copyright (c) 2023-2024, PostgreSQL Global Development Group\n\n# Tests for checking that edb_stat_statements contents are preserved\n# across restarts.\n\nuse strict;\nuse warnings FATAL => 'all';\nuse PostgreSQL::Test::Cluster;\nuse PostgreSQL::Test::Utils;\nuse Test::More;\n\nmy $node = PostgreSQL::Test::Cluster->new('main');\n$node->init;\n$node->append_conf('postgresql.conf',\n\t\"shared_preload_libraries = 'edb_stat_statements'\");\n$node->append_conf('postgresql.conf',\n\t\"edb_stat_statements.track_unrecognized = true\");\n$node->start;\n\n$node->safe_psql('postgres', 'CREATE EXTENSION edb_stat_statements');\n\n$node->safe_psql('postgres', 'CREATE TABLE t1 (a int)');\n$node->safe_psql('postgres', 'SELECT a FROM t1');\n\nis( $node->safe_psql(\n\t\t'postgres',\n\t\t\"SELECT query FROM edb_stat_statements WHERE query NOT LIKE '%edb_stat_statements%' ORDER BY query\"\n\t),\n\t\"CREATE TABLE t1 (a int)\\nSELECT a FROM t1\",\n\t'edb_stat_statements populated');\n\n$node->restart;\n\nis( $node->safe_psql(\n\t\t'postgres',\n\t\t\"SELECT query FROM edb_stat_statements WHERE query NOT LIKE '%edb_stat_statements%' ORDER BY query\"\n\t),\n\t\"CREATE TABLE t1 (a int)\\nSELECT a FROM t1\",\n\t'edb_stat_statements data kept across restart');\n\n$node->append_conf('postgresql.conf', \"edb_stat_statements.save = false\");\n$node->reload;\n\n$node->restart;\n\nis( $node->safe_psql(\n\t\t'postgres',\n\t\t\"SELECT count(*) FROM edb_stat_statements WHERE query NOT LIKE '%edb_stat_statements%'\"\n\t),\n\t'0',\n\t'edb_stat_statements data not kept across restart with .save=false');\n\n$node->stop;\n\ndone_testing();\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"gel-server\"\ndescription = \"Gel Server\"\nrequires-python = '>=3.12.0'\ndynamic = [\"version\"]\ndependencies = [\n    'gel==3.1.0b1',\n\n    'httptools>=0.6.0',\n    'immutables>=0.18',\n    'parsing~=2.0',\n    'uvloop~=0.21.0',\n\n    'click~=8.1.0',\n    'cryptography~=42.0',\n    'graphql-core~=3.1.5',\n    'psutil~=5.8',\n    'setproctitle~=1.2',\n\n    'webauthn~=2.0.0',\n    'argon2-cffi~=23.1.0',\n    'aiosmtplib~=3.0',\n    'tiktoken~=0.7.0',\n    'mistral_common~=1.3.0',\n\n    # pin because newer versions are either broken or require\n    # us to update setuptools (in gel-pkg)\n    'calver==2025.03.31',\n]\n\n[project.scripts]\nedb = \"edb.tools.edb:edbcommands\"\n\nedgedb-server = \"edb.server.main:main\"\nedgedb-load-ext = \"edb.load_ext.main:main\"\nedgedb-ls = \"edb.language_server.main:main\"\n\ngel-server = \"edb.server.main:main\"\ngel-load-ext = \"edb.load_ext.main:main\"\ngel-ls = \"edb.language_server.main:main\"\n\n[project.optional-dependencies]\ntest = [\n    'black~=24.2.0',\n    'coverage~=7.4',\n    'ruff==0.11.2',\n    'asyncpg~=0.30.0',\n\n    # Needed for testing asyncutil\n    'async_solipsism==0.5.0',\n\n    # Needed for test_docs_sphinx_ext\n    'requests-xml~=0.2.3',\n\n    # For rebuilding GHA workflows\n    'Jinja2~=2.11',\n    'MarkupSafe~=1.1',\n    'PyYAML~=6.0',\n\n    'mypy[faster-cache] ~= 1.13.0',\n    # mypy stub packages; when updating, you can use mypy --install-types\n    # to install stub packages and then pip freeze to read out the specifier\n    'types-docutils~=0.17.0,<0.17.6', # incomplete nodes.document.__init__\n    'types-Jinja2~=2.11',\n    'types-MarkupSafe~=1.1',\n    'types-setuptools~=71.1.0',\n    'types-typed-ast~=1.4.2',\n    'types-requests~=2.25.6',\n    'types-PyYAML~=6.0',\n\n    'prometheus_client~=0.11.0',\n\n    'docutils~=0.17.0',\n    'lxml~=6.0.0',\n    'Pygments~=2.10.0',\n    'Sphinx~=4.2.0',\n    'sphinxcontrib-asyncio~=0.3.0',\n    'sphinxcontrib-applehelp<1.0.8',\n    'sphinxcontrib-devhelp<1.0.6',\n    'sphinxcontrib-htmlhelp<2.0.5',\n    'sphinxcontrib-serializinghtml<1.1.10',\n    'sphinxcontrib-qthelp<1.0.7',\n    'sphinx_code_tabs~=0.5.3',\n]\n\ndocs = [\n    'docutils~=0.17.0',\n    'lxml~=6.0.0',\n    'Pygments~=2.10.0',\n    'Sphinx~=4.2.0',\n    'sphinxcontrib-asyncio~=0.3.0',\n    'sphinx_code_tabs~=0.5.3',\n]\n\nlanguage-server = ['pygls~=1.3.1']\n\n[build-system]\nrequires = [\n    \"Cython(>=3.0.11,<3.1.0)\",\n    \"packaging >= 21.0\",\n    \"setuptools(>= 67,<80.0.9)\",\n    \"setuptools-rust ~= 1.8\",\n    \"wheel\",\n\n    \"parsing ~= 2.0\",\n    'gel==3.1.0b1',\n]\n# Custom backend needed to set up build-time sys.path because\n# setup.py needs to import `edb.buildmeta`.\nbuild-backend = \"build_backend\"\nbackend-path = [\".\"]\n\n[tool.setuptools]\npackages = { find = { include = [\"edb\", \"edb.*\"] } }\nzip-safe = false\n\n\n# ========================\n#          BLACK\n# ========================\n[tool.black]\nline-length = 80\ntarget-version = [\"py310\"]\nskip-string-normalization = true\n\n\n# ========================\n#          MYPY\n# ========================\n\n[tool.mypy]\nexclude = [\n    \"^.eggs\",\n    \"^.github\",\n    \"^.vscode\",\n    \"^build\",\n    \"^dist\",\n    \"^docs\",\n    \"^postgres\",\n    \"^target\",\n]\nfiles = \".\"\npython_version = \"3.12\"\nplugins = \"edb/tools/mypy/plugin.py\"\nfollow_imports = \"normal\"\nignore_missing_imports = true\nwarn_redundant_casts = true\nwarn_unused_configs = true\nshow_column_numbers = true\nshow_error_codes = true\nlocal_partial_types = true\n# This being an error seems super confused to me.\ndisable_error_code = \"type-abstract\"\n\n[[tool.mypy.overrides]]\nmodule = [\n    \"edb.common.adapter\",\n    \"edb.edgeql.compiler.*\",\n    \"edb.edgeql.codegen\",\n    \"edb.edgeql.declarative\",\n    \"edb.edgeql.tracer\",\n    \"edb.graphql.types\",\n    \"edb.ir.*\",\n    \"edb.pgsql.metaschema\",\n    \"edb.pgsql.codegen\",\n    \"edb.pgsql.types\",\n    \"edb.pgsql.compiler.*\",\n    \"edb.repl.*\",\n    \"edb.schema.*\",\n    \"edb.schema.reflection.*\",\n    \"edb.testbase.cluster\",\n    \"edb.server.compiler.*\",\n    \"edb.server.config\",\n    \"edb.server.connpool.*\",\n    \"edb.server.protocol.*\",\n    \"edb.server.pgcluster\",\n    \"edb.server.pgconnparams\",\n]\n# Equivalent of --strict on the command line,\n# but without disallow_untyped_calls:\ndisallow_subclassing_any = true\ndisallow_any_generics = true\n# disallow_untyped_calls = true\ndisallow_untyped_defs = true\ndisallow_incomplete_defs = true\ncheck_untyped_defs = true\ndisallow_untyped_decorators = true\nno_implicit_optional = true\nwarn_unused_ignores = true\nwarn_return_any = true\nno_implicit_reexport = true\nstrict_equality = true\n\n[[tool.mypy.overrides]]\nmodule = [\n    \"edb.common.checked\",\n    \"edb.common.compiler\",\n    \"edb.common.ordered\",\n    \"edb.common.parametric\",\n    \"edb.common.retryloop\",\n    \"edb.common.struct\",\n    \"edb.common.topological\",\n    \"edb.common.uuidgen\",\n    \"edb.common.value_dispatch\",\n]\n# Equivalent of --strict on the command line:\ndisallow_subclassing_any = true\ndisallow_any_generics = true\ndisallow_untyped_calls = true\ndisallow_untyped_defs = true\ndisallow_incomplete_defs = true\ncheck_untyped_defs = true\ndisallow_untyped_decorators = true\nno_implicit_optional = true\nwarn_unused_ignores = true\nwarn_return_any = true\nno_implicit_reexport = true\nstrict_equality = true\n\n\n# ========================\n#        COVERAGE\n# ========================\n[tool.coverage.run]\nbranch = false\nplugins = [\"Cython.Coverage\"]\nparallel = true\n\n[tool.coverage.report]\nexclude_lines = [\n    \"pragma: no cover\",\n    \"def __repr__\",\n    \"if debug\",\n    \"raise NotImplementedError\",\n    \"if __name__ == .__main__.\",\n]\nshow_missing = true\nignore_errors = true\n\n# Change the below to invalidate dependency cache in CI\n# CACHE-TAG: 2\n\n[tool.ruff]\nline-length = 80\nindent-width = 4\nexclude = [\"postgres\", \".github\", \"edb/server/pgproto\"]\n\n[tool.ruff.format]\nquote-style = \"preserve\"\n\n[tool.ruff.lint]\npreview = true\nselect = [\"E\", \"F\", \"W\", \"B\", \"UP006\", \"UP007\" , \"UP046\", \"UP047\"]\nignore = [\n    \"F541\", # f-string without any placeholders\n    \"B904\", # Within an except clause, raise exceptions with raise ... from err\n    # or raise ... from None to distinguish them from errors in\n    # exception handling\n    \"E731\", # Do not assign a lambda expression, use a def\n    \"E741\", # Ambiguous variable name: l or i or I\n    \"E252\", # Missing whitespace around parameter equals\n\n    # TODO: enable this\n    \"B905\", # zip() without an explicit strict= parameter\n]\nflake8-bugbear.extend-immutable-calls = [\"immutables.Map\"]\n\n[tool.pyright]\n# Pyright has no idea about metaclass-generated getters for schema fields.\nreportAttributeAccessIssue = false\ntypeCheckingMode = \"off\"\n"
  },
  {
    "path": "rust/conn_pool/Cargo.toml",
    "content": "[package]\nname = \"conn_pool\"\nversion = \"0.1.0\"\nlicense = \"MIT/Apache-2.0\"\nauthors = [\"MagicStack Inc. <hello@magic.io>\"]\nedition = \"2021\"\n\n[lints]\nworkspace = true\n\n[features]\npython_extension = [\"pyo3\"]\noptimizer = [\"genetic_algorithm\", \"lru\", \"rand\", \"statrs\", \"anyhow\", \"tokio/test-util\"]\n\n[dependencies]\npyo3 = { workspace = true, optional = true }\ntokio.workspace = true\npyo3_util.workspace = true\ntracing.workspace = true\n\nfutures = \"0\"\nscopeguard = \"1\"\nitertools = \"0\"\nthiserror = \"2\"\nstrum = { version = \"0.26\", features = [\"derive\"] }\nconsume_on_drop = \"0\"\nsmart-default = \"0\"\nserde = { version = \"1\", features = [\"derive\"] }\nserde-pickle = \"1\"\n\n# For the optimizer\ngenetic_algorithm = { version = \"0.9.0\", optional = true }\nlru = { version = \"0.12.4\", optional = true }\nrand = { version = \"0.8.5\", optional = true }\nstatrs = { version = \"0.17.1\", optional = true }\nanyhow = { version = \"1\", optional = true }\n\nderive_more = { version = \"2\", features = [\"full\"] }\n\n[dev-dependencies]\ntokio = { workspace = true, features = [\"test-util\"] }\n\npretty_assertions = \"1.2.0\"\ntest-log = { version = \"0\", features = [\"trace\"] }\nanyhow = \"1\"\nrstest = \"0\"\n\nstatrs = \"0.17.1\"\nrand = \"0.8.5\"\n\n[lib]\n\n[[bin]]\nname = \"optimizer\"\nrequired-features = [\"optimizer\"]\n"
  },
  {
    "path": "rust/conn_pool/README.md",
    "content": "# Connection Pool\n\n## Overview\n\nThe load-balancing algorithm is designed to optimize the allocation and\nmanagement of database connections in a way that maximizes Quality of Service\n(QoS). This involves minimizing the overall time spent on connecting and\nreconnecting (connection efficiency) while ensuring that latencies remain\nsimilar across different streams of connections (fairness).\n\n## Architecture\n\nThis library is split into four major components:\n\n 1. The low-level blocks/block, connections, and metrics code. This code\n    creates, destroys and transfers connections without understanding of\n    policies, quotas or any sort of algorithm. We ensure that the blocks and\n    metrics are reliable, and use this as a building block for our pool.\n 2. The algorithm. This performs planning operations for acquisition, release\n    and rebalancing of the pool. The algorithm does not perform operations, but\n    rather informs that caller what it should do.\n 3. The pool itself. This drives the blocks and the connector interface, and\n    polls the algorithm to plan next steps during acquisition, release and\n    during the timer-based planning callback.\n 4. The Python integration code. This is behind an optional feature, and exposes\n    PyO3-based interface that allows a connection factory to be implemented in\n    Python.\n\n## Details\n\nDemand for connections is measured in terms of “database time,” which is\ncalculated as the product of the number of connections and the average hold time\nof these connections. This metric provides a basis for determining how resources\nshould be distributed among different database blocks to meet their needs\neffectively.\n\nTo maximize QoS, the algorithm aims to minimize the time spent on managing\nconnections and keep the latencies low and uniform across various connection\nstreams. This involves allocation strategies that balance the immediate needs of\ndifferent database blocks with the overall system capacity and future demand\npredictions.\n\nWhen a connection is acquired, the system may be in a state where the pool is\nnot currently constrained by demand. In such cases, connections can be allocated\ngreedily without complex balancing, as there are sufficient resources to meet\nall demands. This allows for quick connection handling without additional\noverhead.\n\nWhen the pool is constrained, the “stealing” algorithm aims to transfer\nconnections from less utilized or idle database blocks (victims) to those\nexperiencing high demand (hunger) to ensure efficient resource use and maintain\nQoS. A victim block is chosen based on its idle state, characterized by holding\nconnections but having low or no immediate demand for them.\n\nUpon releasing a connection, the algorithm evaluates which backend (database\nblock) needs the connection the most (the hungriest). This decision is based on\ncurrent demand, wait times, and historical usage patterns. By reallocating\nconnections to the blocks that need them most, the algorithm ensures that\nresources are utilized efficiently and effectively.\n\nUnused connection capacity is eventually reclaimed to prevent wastage. The\nalgorithm includes mechanisms to identify and collect these idle connections,\nredistributing them to blocks with higher demand or returning them to the pool\nfor future use. This helps maintain an optimal number of active connections,\nreducing unnecessary resource consumption.\n\nTo avoid excessive thrashing, the algorithm ensures that connections are held\nfor a minimum period, which is longer than the time it takes to reconnect to a\ndatabase or a configured minimum threshold. This reduces the frequency of\nreallocation, preventing performance degradation due to constant connection\nchurn and ensuring that blocks can maintain stable and predictable access to\nresource\n\n## Detailed Algorithm\n\nThe algorithm is designed to 1) maximize time spent running queries in a\ndatabase and 2) minimize latency of queries waiting for their turn to run. These\ngoals may be in conflict at times. We do this by optimizing the time spent\nswitching between databases, which is considered \"dead time\" -- as the database\nis not actively performing operations.\n\nThe demand for a connection is based on estimated total sequential processing\ntime. We use the average time that a connection is held, times the number of\nconnections in demand as a rough idea of how much total sequential time a\ncertain block demands in the future.\n\nAt a regular interval, we compute two items for each block: a quota, and a\n\"hunger\" metric. The hunger metric may indicate that a block is \"hungry\"\n(wanting more connections), satisfied (having the expected number of\nconnections) or overfull (holding more connections than it should). The \"hungry\"\nscore is determined by the estimated total sequential time needed for a block.\nThe \"overfull\" score is determined by the number of extra connections held by\nthis block, in combination with how old the longest-held connection is. Quota is\ndetermined by the connection rate.\n\nWe then use the hunger metric and quota in an attempt to rebalance the pool\nproactively to ensure that the connection capacity of each block reflects its\nmost recent demand profile. Blocks are sorted into a list of hungry and overfull\nblocks, and we attempt to transfer from the most hungry to the most overfull\nuntil we run out of either list. We may not be able to perform the rebalance\nfully because of block activity that cannot be interrupted.\n\nIf a connection is requested for a block that is hungry, it is allowed to steal\na connection from the block that most overfull and has idle connections. As the\n\"overfull\" score is calculated in part by the longest-held connection's age, we\nminimize context switching.\n\nWhen a connection is released, we choose what happens based on its state. If\nmore connections are waiting on this block, we return the connection to the\nblock to be re-used immediately. If no connections are waiting but the block is\nhungry, we return it. If the block is satisfied or overfull and we have hungry\nblocks waiting, we transfer it to a hungry block that has waiters.\n\n## Error Handling\n\nThe pool will attempt to provide a connection where possible, but connection\noperations may not always be reliable. The error for a connection failure will\nbe routed through the acquire operation if the pool detects there are no other\npotential sources for a connection for the acquire. Sources for a connection may\nbe a currently-connecting connection, a reconnecting connection, a connection\nthat is actively held by someone else or a connection that is sitting idle.\n\nThe pool does not currently retry, and retry logic should be included in the\nconnect operation.\n"
  },
  {
    "path": "rust/conn_pool/src/algo.rs",
    "content": "use std::{\n    cell::{Cell, RefCell},\n    time::Duration,\n};\nuse tracing::trace;\n\nuse crate::{\n    block::Name,\n    drain::Drain,\n    metrics::{MetricVariant, RollingAverageU32},\n};\n\n/// The historical length of data we'll maintain for demand.\nconst DEMAND_HISTORY_LENGTH: usize = 16;\n\n#[cfg(not(feature = \"optimizer\"))]\n#[derive(Clone, Copy, derive_more::From)]\npub struct Knob<T: Copy>(&'static str, T);\n\n#[cfg(not(feature = \"optimizer\"))]\nimpl<T: Copy> Knob<T> {\n    pub const fn new(name: &'static str, value: T) -> Self {\n        Self(name, value)\n    }\n\n    pub fn get(&self) -> T {\n        self.1\n    }\n}\n\n#[cfg(feature = \"optimizer\")]\npub struct Knob<T: Copy + 'static>(\n    &'static str,\n    &'static std::thread::LocalKey<std::cell::RefCell<T>>,\n    Option<std::ops::RangeInclusive<T>>,\n);\n\nimpl<T: Copy + PartialOrd<T> + std::fmt::Display + std::fmt::Debug> std::fmt::Debug for Knob<T> {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.write_fmt(format_args!(\"{}={:?}\", self.0, self.get()))\n    }\n}\n\n#[cfg(feature = \"optimizer\")]\nimpl<T: Copy + PartialOrd<T> + std::fmt::Display + std::fmt::Debug> Knob<T> {\n    pub const fn new(\n        name: &'static str,\n        value: &'static std::thread::LocalKey<std::cell::RefCell<T>>,\n        bounds: &[std::ops::RangeInclusive<T>],\n    ) -> Self {\n        let copy = if !bounds.is_empty() {\n            Some(*bounds[0].start()..=*bounds[0].end())\n        } else {\n            None\n        };\n        Self(name, value, copy)\n    }\n\n    pub fn name(&self) -> &'static str {\n        self.0\n    }\n\n    pub fn get(&self) -> T {\n        self.1.with_borrow(|t| *t)\n    }\n\n    pub fn set(&self, value: T) -> Result<(), String> {\n        if let Some(range) = &self.2 {\n            if range.contains(&value) {\n                self.1.with_borrow_mut(|t| *t = value);\n                Ok(())\n            } else {\n                Err(format!(\"{value} is out of range of {range:?}\"))\n            }\n        } else {\n            self.1.with_borrow_mut(|t| *t = value);\n            Ok(())\n        }\n    }\n\n    pub fn clamp(&self, value: &mut T) {\n        if let Some(range) = &self.2 {\n            if !range.contains(value) {\n                if *value < *range.start() {\n                    *value = *range.start()\n                } else {\n                    *value = *range.end()\n                }\n            }\n        }\n    }\n}\n\n#[allow(unused)]\nmacro_rules! range {\n    () => {\n        &[]\n    };\n    (($n1:literal..)) => {\n        &[$n1..=isize::MAX]\n    };\n    (($n1:literal..=$n2:literal)) => {\n        &[$n1..=$n2]\n    };\n}\n\nmacro_rules! constants {\n    ($(\n        $( #[doc=$doc:literal] )*\n        $( #[range $range:tt] )?\n        const $name:ident: $type:ty = $value:literal;\n    )*) => {\n        #[cfg(feature=\"optimizer\")]\n        pub mod knobs {\n            pub use super::Knob;\n            mod locals {\n                $(\n                    thread_local! {\n                        pub static $name: std::cell::RefCell<$type> = std::cell::RefCell::new($value);\n                    }\n                )*\n            }\n\n            $(\n                $( #[doc=$doc] )*\n                pub static $name: Knob<$type> = Knob::new(stringify!($name), &locals::$name, range!($($range)?));\n            )*\n\n            pub const ALL_KNOB_COUNT: usize = [$(stringify!($name)),*].len();\n            pub static ALL_KNOBS: [&Knob<isize>; ALL_KNOB_COUNT] = [\n                $(&$name),*\n            ];\n        }\n        #[cfg(not(feature=\"optimizer\"))]\n        pub mod knobs {\n            pub use super::Knob;\n            $(\n                $( #[doc=$doc] )*\n                pub const $name: Knob<$type> = Knob::new(stringify!($name), $value);\n            )*\n        }\n        pub use knobs::*;\n    };\n}\n\n// Note: these constants are tuned via the generic algorithm optimizer.\nconstants! {\n    /// The maximum number of connections to create or destroy during a rebalance.\n    #[range(1..=10)]\n    const MAX_REBALANCE_OPS: isize = 5;\n    /// The maximum % of total connections to create or destroy during a\n    /// rebalance when full.\n    #[range(1..=10)]\n    const MAX_REBALANCE_OPS_PERCENT_WHEN_FULL: isize = 5;\n    /// The minimum headroom in a block between its current total and its target\n    /// for us to pre-create connections for it.\n    #[range(0..=10)]\n    const MIN_REBALANCE_HEADROOM_TO_CREATE: isize = 0;\n\n    /// The minimum amount of time we'll consider for an active connection.\n    #[range(1..=100)]\n    const MIN_TIME: isize = 1;\n\n    /// The weight we apply to waiting connections.\n    #[range(0..)]\n    const DEMAND_WEIGHT_WAITING: isize = 61;\n    /// The weight we apply to active connections.\n    #[range(0..)]\n    const DEMAND_WEIGHT_ACTIVE: isize = 31;\n    /// The minimum non-zero demand. This makes the demand calculations less noisy\n    /// when we are competing at lower levels of demand, allowing for more\n    /// reproducable results.\n    #[range(1..=256)]\n    const DEMAND_MINIMUM: isize = 1;\n\n    /// The maximum-minimum connection count we'll allocate to connections if there\n    /// is more capacity than backends.\n    #[range(1..)]\n    const MAXIMUM_SHARED_TARGET: isize = 1;\n\n    /// The boost we apply to our own apparent hunger when releasing a connection.\n    /// This prevents excessive swapping when hunger is similar across various\n    /// backends.\n    #[range(0..)]\n    const SELF_HUNGER_BOOST_FOR_RELEASE: isize = 46;\n    /// The weight we apply to the difference between the target and required\n    /// connections when determining overfullness.\n    const HUNGER_DIFF_WEIGHT: isize = -3;\n    /// The weight we apply to waiters when determining hunger.\n    const HUNGER_WAITER_WEIGHT: isize = 2;\n    const HUNGER_WAITER_ACTIVE_WEIGHT: isize = 0;\n    const HUNGER_ACTIVE_WEIGHT_DIVIDEND_ADD: isize = -611;\n    const HUNGER_ACTIVE_WEIGHT_DIVIDEND_SUB: isize = 0;\n    /// The weight we apply to the oldest waiter's age in milliseconds (as a divisor).\n    const HUNGER_AGE_DIVISOR_WEIGHT: isize = -35;\n    /// Penalize switching to a backend which has changed recently.\n    const HUNGER_CHANGE_WEIGHT_DIVIDEND: isize = -39;\n\n    /// The weight we apply to the difference between the target and required\n    /// connections when determining overfullness.\n    const OVERFULL_DIFF_WEIGHT: isize = -3;\n    /// The weight we apply to idle connections when determining overfullness.\n    const OVERFULL_IDLE_WEIGHT: isize = 423;\n    /// This is divided by the youngest connection metric to penalize switching from\n    /// a backend which has changed recently.\n    const OVERFULL_CHANGE_WEIGHT_DIVIDEND: isize = -59;\n    /// The weight we apply to waiters when determining overfullness.\n    const OVERFULL_WAITER_WEIGHT: isize = 194;\n    const OVERFULL_WAITER_ACTIVE_WEIGHT: isize = -98;\n\n    const OVERFULL_ACTIVE_WEIGHT_DIVIDEND_ADD: isize = -696;\n    const OVERFULL_ACTIVE_WEIGHT_DIVIDEND_SUB: isize = 60;\n}\n\n/// Determines the rebalance plan based on the current pool state.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum RebalanceOp {\n    /// Transfer from one block to another\n    Transfer { to: Name, from: Name },\n    /// Create a block\n    Create(Name),\n    /// Garbage collect a block.\n    Close(Name),\n}\n\n/// Determines the acquire plan based on the current pool state.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum AcquireOp {\n    /// Create a new connection.\n    Create,\n    /// Steal a connection from another block.\n    Steal(Name),\n    /// Wait for a connection.\n    Wait,\n    /// A connection cannot be established due to shutdown.\n    FailInShutdown,\n}\n\n/// Determines the release plan based on the current pool state.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum ReleaseOp {\n    /// Release this connection back to the same database.\n    Release,\n    /// Reopen this connection.\n    Reopen,\n    /// Discard this connection.\n    Discard,\n    /// Release this connection to a different database.\n    ReleaseTo(Name),\n}\n\n/// The type of release to perform.\n#[derive(Debug, Default, Clone, PartialEq, Eq)]\npub enum ReleaseType {\n    /// A normal release\n    #[default]\n    Normal,\n    /// A release of a poisoned connection.\n    Poison,\n}\n\n#[derive(Default, Clone, Copy)]\n#[repr(transparent)]\nstruct Score(isize);\n\n#[allow(clippy::comparison_chain)]\nimpl Score {\n    #[inline(always)]\n    pub fn add(&mut self, knob: &Knob<isize>, value: impl TryInto<isize>) {\n        let knob = knob.get();\n        let value = value.try_into().unwrap_or_default();\n        if knob < 0 {\n            self.0 += value / -knob\n        } else if knob > 0 {\n            self.0 += value * knob\n        }\n    }\n\n    #[inline(always)]\n    pub fn add_fraction(\n        &mut self,\n        knob: &Knob<isize>,\n        numerator: impl TryInto<isize>,\n        denominator: impl TryInto<isize>,\n    ) {\n        let knob = knob.get();\n        let numerator = numerator.try_into().unwrap_or_default();\n        let denominator = denominator.try_into().unwrap_or_default();\n        if knob < 0 {\n            self.0 += numerator / (denominator * -knob)\n        } else if knob > 0 {\n            self.0 += numerator * knob / denominator\n        }\n    }\n\n    #[inline(always)]\n    pub fn sub(&mut self, knob: &Knob<isize>, value: impl TryInto<isize>) {\n        let knob = knob.get();\n        if knob < 0 {\n            self.0 -= value.try_into().unwrap_or_default() / -knob\n        } else if knob > 0 {\n            self.0 -= value.try_into().unwrap_or_default() * knob\n        }\n    }\n\n    #[inline(always)]\n    pub fn sub_fraction(\n        &mut self,\n        knob: &Knob<isize>,\n        numerator: impl TryInto<isize>,\n        denominator: impl TryInto<isize>,\n    ) {\n        let knob = knob.get();\n        let numerator = numerator.try_into().unwrap_or_default();\n        let denominator = denominator.try_into().unwrap_or_default();\n        if knob < 0 {\n            self.0 -= numerator / (denominator * -knob)\n        } else if knob > 0 {\n            self.0 -= numerator * knob / denominator\n        }\n    }\n\n    #[inline(always)]\n    pub fn into(self) -> isize {\n        self.0\n    }\n}\n\n/// Generic trait to decouple the algorithm from the underlying pool blocks.\n/// This minimizes the interface between the algorithm and the blocks to keep\n/// coupling between the two at the right level.\npub trait VisitPoolAlgoData: PoolAlgorithmDataPool {\n    type Block: PoolAlgorithmDataBlock;\n\n    /// Ensure that the given block is available, inserting it with the default\n    /// demand if necessary.\n    fn ensure_block(&self, db: &str, default_demand: usize) -> bool;\n    /// Iterates all the blocks, garbage collecting any idle blocks with no demand.\n    fn with_all(&self, f: impl FnMut(&Name, &Self::Block));\n    /// Retreives a single block, returning `None` if the block doesn't exist.\n    fn with<T>(&self, db: &str, f: impl Fn(&Self::Block) -> T) -> Option<T>;\n\n    #[inline]\n    fn target(&self, db: &str) -> usize {\n        self.with(db, |data| data.target()).unwrap_or_default()\n    }\n}\n\npub trait PoolAlgorithmDataMetrics {\n    fn total(&self) -> usize;\n    fn count(&self, variant: MetricVariant) -> usize;\n    fn total_max(&self) -> usize;\n    fn max(&self, variant: MetricVariant) -> usize;\n    fn avg_ms(&self, variant: MetricVariant) -> usize;\n}\n\npub trait PoolAlgorithmDataBlock: PoolAlgorithmDataMetrics {\n    fn target(&self) -> usize;\n    fn set_target(&self, target: usize);\n    fn insert_demand(&self, demand: u32);\n    fn demand(&self) -> u32;\n\n    fn count_older(&self, variant: MetricVariant, age: Duration) -> usize;\n    fn oldest_ms(&self, variant: MetricVariant) -> usize;\n    fn youngest_ms(&self) -> usize;\n\n    /// Calculates the hunger score for the current state.\n    ///\n    /// The score is determined based on the difference between the target and current metrics,\n    /// and the number of waiting elements. It uses weights for each component to compute the final score.\n    /// If the current state exceeds the target, the function returns `None`.\n    ///\n    /// # Parameters\n    ///\n    /// - `will_release`: A boolean indicating whether an element will be released.\n    ///\n    /// # Returns\n    ///\n    /// Returns an `Option<NonZeroUsize>` containing the hunger score if the current state is below the target\n    /// and there are waiting elements; otherwise, returns `None`.\n    fn hunger_score(&self, will_release: bool) -> Option<isize> {\n        let waiting = self.count(MetricVariant::Waiting);\n        let connecting =\n            self.count(MetricVariant::Connecting) + self.count(MetricVariant::Reconnecting);\n        let waiters = waiting.saturating_sub(connecting);\n        let current = self.total() - if will_release { 1 } else { 0 };\n        let target = self.target();\n\n        // If we have more connections than our target, we are not hungry. We\n        // may still be hungry if current <= target if we have waiters, however.\n        if current > target || (target == current && waiters < 1) {\n            return None;\n        }\n\n        let active_ms = self.avg_ms(MetricVariant::Active).max(MIN_TIME.get() as _);\n        let reconnecting_ms = self\n            .avg_ms(MetricVariant::Reconnecting)\n            .max(self.avg_ms(MetricVariant::Connecting) + self.avg_ms(MetricVariant::Disconnecting))\n            .max(MIN_TIME.get() as _);\n        let youngest_ms = self.youngest_ms();\n\n        let mut score = Score::default();\n\n        // Waiters become more hungry as they age\n        score.add(\n            &HUNGER_AGE_DIVISOR_WEIGHT,\n            self.oldest_ms(MetricVariant::Waiting),\n        );\n        // The number of waiters increases hunger\n        score.add(&HUNGER_WAITER_WEIGHT, waiters);\n        // The expected number of waiters we could handle within a reconnection period increases hunger\n        score.add_fraction(\n            &HUNGER_WAITER_ACTIVE_WEIGHT,\n            waiters * active_ms,\n            reconnecting_ms,\n        );\n        // The amount of connections we owe this block increases hunger\n        score.add(&HUNGER_DIFF_WEIGHT, target - current);\n\n        // Allow for some non-linearity in scoring w/active time\n        score.add_fraction(\n            &HUNGER_ACTIVE_WEIGHT_DIVIDEND_ADD,\n            active_ms,\n            reconnecting_ms,\n        );\n        score.sub_fraction(\n            &HUNGER_ACTIVE_WEIGHT_DIVIDEND_SUB,\n            active_ms,\n            reconnecting_ms,\n        );\n\n        // We give an hunger \"negative\" penalty to blocks that have newly\n        // acquired a connection.\n        score.sub_fraction(&HUNGER_CHANGE_WEIGHT_DIVIDEND, youngest_ms, reconnecting_ms);\n\n        Some(score.into())\n    }\n\n    /// Calculates the overfull score for the current state.\n    ///\n    /// The score is determined based on the difference between the current and target metrics,\n    /// the idle count, and the age of the youngest element. It uses weights for each component to\n    /// compute the final score. If the current state is not overfull or there are no idle elements,\n    /// the function returns `None`.\n    ///\n    /// # Parameters\n    ///\n    /// - `will_release`: A boolean indicating whether an element will be released.\n    ///\n    /// # Returns\n    ///\n    /// Returns an `Option<NonZeroUsize>` containing the overfull score if the current state is overfull\n    /// and there are idle elements; otherwise, returns `None`.\n    fn overfull_score(&self, will_release: bool) -> Option<isize> {\n        let idle = self.count(MetricVariant::Idle) + if will_release { 1 } else { 0 };\n        let current = self.total();\n        let target = self.target();\n\n        // If we have no idle connections, or we don't have enough connections we're not overfull.\n        if target >= current || idle == 0 {\n            return None;\n        }\n\n        let connecting =\n            self.count(MetricVariant::Connecting) + self.count(MetricVariant::Reconnecting);\n        let waiting = self.count(MetricVariant::Waiting);\n        let waiters = waiting.saturating_sub(connecting);\n\n        let active_ms = self.avg_ms(MetricVariant::Active).max(MIN_TIME.get() as _);\n        let reconnecting_ms = self\n            .avg_ms(MetricVariant::Reconnecting)\n            .max(self.avg_ms(MetricVariant::Connecting) + self.avg_ms(MetricVariant::Disconnecting))\n            .max(MIN_TIME.get() as _);\n        let youngest_ms = self.youngest_ms();\n\n        let mut score = Score::default();\n\n        // The more idle connections we have, the more overfull this block is.\n        score.add(&OVERFULL_IDLE_WEIGHT, idle);\n        // If we've got more connections than we were allocated, we're overfull.\n        score.add(&OVERFULL_DIFF_WEIGHT, current - target);\n\n        // We take the ratio of youngest/connecting ato give an overfullness\n        // \"negative\" penalty to blocks that have newly acquired a connection.\n        score.sub_fraction(\n            &OVERFULL_CHANGE_WEIGHT_DIVIDEND,\n            youngest_ms,\n            reconnecting_ms,\n        );\n\n        // The number of waiters and the amount of time we expect to spend\n        // active on these waiters also acts as a \"negative\" penalty.\n        score.sub(&OVERFULL_WAITER_WEIGHT, waiters);\n        score.sub_fraction(\n            &OVERFULL_WAITER_ACTIVE_WEIGHT,\n            waiters * active_ms,\n            reconnecting_ms,\n        );\n\n        // Allow for some non-linearity in scoring w/active time\n        score.add_fraction(\n            &OVERFULL_ACTIVE_WEIGHT_DIVIDEND_ADD,\n            active_ms,\n            reconnecting_ms,\n        );\n        score.sub_fraction(\n            &OVERFULL_ACTIVE_WEIGHT_DIVIDEND_SUB,\n            active_ms,\n            reconnecting_ms,\n        );\n\n        Some(score.into())\n    }\n\n    /// We calculate demand based on the estimated connection active time\n    /// multiplied by the active + waiting counts. This gives us an\n    /// estimated database time statistic we can use for relative\n    /// weighting.\n    fn demand_score(&self) -> usize {\n        // This gives us an approximate count of incoming connection load during the last period\n        let active = self.max(MetricVariant::Active);\n        let active_ms = self.avg_ms(MetricVariant::Active).max(MIN_TIME.get() as _);\n        let waiting = self.max(MetricVariant::Waiting);\n        let idle = active == 0 && waiting == 0;\n\n        if idle {\n            0\n        } else {\n            let mut score = Score::default();\n            score.add(&DEMAND_WEIGHT_WAITING, waiting * active_ms);\n            score.add(&DEMAND_WEIGHT_ACTIVE, active * active_ms);\n            // Note that we clamp to DEMAND_MINIMUM to ensure the average is non-zero\n            score\n                .into()\n                .max(DEMAND_MINIMUM.get() * DEMAND_HISTORY_LENGTH as isize) as _\n        }\n    }\n}\n\npub trait PoolAlgorithmDataPool: PoolAlgorithmDataMetrics {\n    fn reset_max(&self);\n}\n\n#[derive(Default, Debug)]\npub struct PoolAlgoTargetData {\n    /// A numeric score representing hunger or overfullness.\n    target_size: Cell<usize>,\n    avg_demand: RefCell<RollingAverageU32<DEMAND_HISTORY_LENGTH>>,\n}\n\nimpl PoolAlgoTargetData {\n    pub fn set_target(&self, target: usize) {\n        self.target_size.set(target);\n    }\n    pub fn target(&self) -> usize {\n        self.target_size.get()\n    }\n    pub fn insert_demand(&self, demand: u32) {\n        self.avg_demand.borrow_mut().accum(demand)\n    }\n    pub fn demand(&self) -> u32 {\n        self.avg_demand.borrow().avg()\n    }\n}\n\n/// The pool algorithm constraints.\n#[derive(Debug)]\npub struct PoolConstraints {\n    /// Maximum pool size.\n    pub max: usize,\n    /// The minimum idle time before a connection can be GC'd.\n    pub min_idle_time_for_gc: Duration,\n}\n\n/// The algorithm runs against these data structures.\npub struct AlgoState<'a, V: VisitPoolAlgoData> {\n    pub drain: &'a Drain,\n    pub blocks: &'a V,\n    pub constraints: &'a PoolConstraints,\n}\n\nimpl<V: VisitPoolAlgoData> AlgoState<'_, V> {\n    /// Recalculate the quota targets for each block within the pool/\n    fn recalculate_shares(&self, update_demand: bool) {\n        // First, compute the overall request load and number of backend targets\n        let mut total_demand = 0;\n        let mut total_target = 0;\n        let mut s = \"\".to_owned();\n\n        self.blocks.with_all(|name, data| {\n            // Draining targets act like they have demand zero\n            if self.drain.is_draining(name) {\n                if tracing::enabled!(tracing::Level::TRACE) {\n                    s += &format!(\"{name}=drain \");\n                }\n                return;\n            }\n\n            if update_demand {\n                let demand = data.demand_score();\n                data.insert_demand(demand as _);\n            }\n            let demand = data.demand();\n\n            if tracing::enabled!(tracing::Level::TRACE) {\n                s += &format!(\"{name}={demand} \");\n            }\n\n            total_demand += demand as usize;\n            if demand > 0 {\n                total_target += 1;\n            } else {\n                data.set_target(0);\n            }\n        });\n\n        if tracing::enabled!(tracing::Level::TRACE) {\n            trace!(\"Demand: {total_target} {}\", s);\n        }\n\n        self.allocate_demand(total_target, total_demand);\n    }\n\n    /// Adjust the quota targets for each block within the pool.\n    pub fn adjust(&self) {\n        self.recalculate_shares(true);\n\n        // Once we've adjusted the constraints, reset the max settings\n        self.blocks.reset_max();\n    }\n\n    /// Allocate the calculated demand to target quotas.\n    fn allocate_demand(&self, total_target: usize, total_demand: usize) {\n        // Empty pool, no math\n        if total_target == 0 || total_demand == 0 {\n            self.blocks.with_all(|_name, data| {\n                data.set_target(0);\n            });\n            return;\n        }\n\n        let mut allocated = 0;\n        let max = self.constraints.max;\n        // This is the minimum number of connections we'll allocate to any particular\n        // backend regardless of demand if there are less backends than the capacity.\n        let min = (max / total_target).min(MAXIMUM_SHARED_TARGET.get() as usize);\n        // The remaining capacity after we allocated the `min` value above.\n        let capacity = max - min * total_target;\n\n        if min == 0 {\n            self.blocks.with_all(|_name, data| {\n                data.set_target(0);\n            });\n        } else {\n            self.blocks.with_all(|name, data| {\n                let demand = data.demand();\n                if demand == 0 || self.drain.is_draining(name) {\n                    data.set_target(0);\n                    return;\n                }\n\n                // Give everyone what they requested, plus a share of the spare\n                // capacity. If there is leftover spare capacity, that capacity\n                // may float between whoever needs it the most.\n                let target =\n                    (demand as f32 * capacity as f32 / total_demand as f32).floor() as usize + min;\n\n                data.set_target(target);\n                allocated += target;\n            });\n        }\n\n        if tracing::enabled!(tracing::Level::TRACE) {\n            let mut s = String::new();\n            self.blocks.with_all(|name, block| {\n                s += &format!(\"{name}={}/{} \", block.target(), block.total());\n            });\n            trace!(\"Targets: {s}\");\n        }\n\n        debug_assert!(\n            allocated <= max,\n            \"Attempted to allocate more than we were allowed: {allocated} > {max} \\\n                (req={total_demand}, target={total_target})\"\n        );\n    }\n\n    /// Plan a shutdown.\n    fn plan_shutdown(&self) -> Vec<RebalanceOp> {\n        let mut ops = vec![];\n        self.blocks.with_all(|name, block| {\n            let idle = block.count(MetricVariant::Idle);\n            let failed = block.count(MetricVariant::Failed);\n\n            for _ in 0..(idle + failed) {\n                ops.push(RebalanceOp::Close(name.clone()));\n            }\n        });\n        ops\n    }\n\n    /// Plan a rebalance to better match the target quotas of the blocks in the\n    /// pool.\n    pub fn plan_rebalance(&self, garbage_collect: bool) -> Vec<RebalanceOp> {\n        // In shutdown, we just want to close all idle connections where possible.\n        if self.drain.in_shutdown() {\n            return self.plan_shutdown();\n        }\n\n        let mut current_pool_size = self.blocks.total();\n        let max_pool_size = self.constraints.max;\n        let mut tasks = vec![];\n\n        // TODO: These could potentially be transferred instead, but\n        // drain/shutdown are pretty unlikely.\n        if self.drain.are_any_draining() {\n            self.blocks.with_all(|name, data| {\n                if self.drain.is_draining(name) {\n                    for _ in 0..data.count(MetricVariant::Idle) + data.count(MetricVariant::Failed)\n                    {\n                        tasks.push(RebalanceOp::Close(name.clone()))\n                    }\n                }\n            })\n        }\n\n        // If we're garbage collecting, we want to close any idle connections\n        // older than `min_idle_time_for_gc`. If we don't have any, just run a\n        // normal rebalance.\n        if garbage_collect {\n            self.blocks.with_all(|name, block| {\n                // Drains are handled at the start of this function\n                if self.drain.is_draining(name) {\n                    return;\n                }\n\n                let gc_able =\n                    block.count_older(MetricVariant::Idle, self.constraints.min_idle_time_for_gc);\n                if gc_able > 0 {\n                    let idle = block.count(MetricVariant::Idle);\n                    trace!(\"GC for {name}: {gc_able}/{idle} connection(s) to close\");\n                }\n                for _ in 0..gc_able {\n                    tasks.push(RebalanceOp::Close(name.clone()));\n                }\n            });\n            if !tasks.is_empty() {\n                return tasks;\n            }\n        }\n\n        // If there's room in the pool, we can be more aggressive in\n        // how we allocate.\n        if current_pool_size < max_pool_size {\n            let mut made_changes = false;\n            for i in 0..MAX_REBALANCE_OPS.get() as usize {\n                self.blocks.with_all(|name, block| {\n                    // Drains are handled at the start of this function\n                    if self.drain.is_draining(name) {\n                        return;\n                    }\n\n                    // If there's room in the block, and room in the pool, and\n                    // the block is bumping up against its current headroom, we'll grab\n                    // another one.\n                    if block.target() > block.total()\n                        && current_pool_size < max_pool_size\n                        && (block.max(MetricVariant::Active) + block.max(MetricVariant::Waiting))\n                            > (block.total() + i)\n                                .saturating_sub(MIN_REBALANCE_HEADROOM_TO_CREATE.get() as usize)\n                    {\n                        tasks.push(RebalanceOp::Create(name.clone()));\n                        current_pool_size += 1;\n                        made_changes = true;\n                    }\n                });\n                if !made_changes {\n                    break;\n                }\n            }\n\n            return tasks;\n        }\n\n        // For any block with less connections than its quota that has\n        // waiters, we want to transfer from the most overloaded block.\n        let mut overloaded = vec![];\n        let mut hungriest = vec![];\n        let mut idle = vec![];\n\n        let mut s1 = \"\".to_owned();\n        let mut s2 = \"\".to_owned();\n\n        self.blocks.with_all(|name, block| {\n            // Drains are handled at the start of this function\n            if self.drain.is_draining(name) {\n                return;\n            }\n\n            if let Some(value) = block.hunger_score(false) {\n                if tracing::enabled!(tracing::Level::TRACE) {\n                    s1 += &format!(\"{name}={value} \");\n                }\n                hungriest.push((value, name.clone()))\n            } else if let Some(value) = block.overfull_score(false) {\n                if tracing::enabled!(tracing::Level::TRACE) {\n                    s2 += &format!(\"{name}={value} \");\n                }\n                if block.demand() == 0 {\n                    idle.push(name.clone());\n                } else {\n                    overloaded.push((value, name.clone()))\n                }\n            }\n        });\n\n        if tracing::enabled!(tracing::Level::TRACE) {\n            trace!(\"Hunger: {s1}\");\n            trace!(\"Overfullness: {s2}\");\n        }\n        overloaded.sort();\n        hungriest.sort();\n\n        let ops_count =\n            ((MAX_REBALANCE_OPS_PERCENT_WHEN_FULL.get() as usize * max_pool_size) / 100).max(1);\n        for _ in 0..ops_count {\n            let Some((_, to)) = hungriest.pop() else {\n                break;\n            };\n\n            // Prefer rebalancing from idle connections, otherwise take from\n            // overloaded ones.\n            if let Some(from) = idle.pop() {\n                tasks.push(RebalanceOp::Transfer { to, from });\n            } else if let Some((_, from)) = overloaded.pop() {\n                tasks.push(RebalanceOp::Transfer { to, from });\n            } else {\n                break;\n            }\n        }\n\n        tasks\n    }\n\n    /// Plan a connection acquisition.\n    pub fn plan_acquire(&self, db: &str) -> AcquireOp {\n        if self.drain.in_shutdown() {\n            return AcquireOp::FailInShutdown;\n        }\n\n        // If the block is new, we need to perform an initial adjustment to\n        // ensure this block gets some capacity.\n        if self\n            .blocks\n            .ensure_block(db, DEMAND_MINIMUM.get() as usize * DEMAND_HISTORY_LENGTH)\n        {\n            self.recalculate_shares(false);\n        }\n\n        let target_block_size = self.blocks.target(db);\n        let current_block_size = self\n            .blocks\n            .with(db, |data| data.total())\n            .unwrap_or_default();\n        let current_pool_size = self.blocks.total();\n        let max_pool_size = self.constraints.max;\n\n        let pool_is_full = current_pool_size >= max_pool_size;\n        if !pool_is_full {\n            trace!(\"Pool has room, acquiring new connection for {db}\");\n            return AcquireOp::Create;\n        }\n\n        let block_has_room = current_block_size < target_block_size || target_block_size == 0;\n        trace!(\"Acquiring {db}: {current_pool_size}/{max_pool_size} {current_block_size}/{target_block_size}\");\n        if pool_is_full && block_has_room {\n            let mut max = isize::MIN;\n            let mut which = None;\n            self.blocks.with_all(|name, block| {\n                if let Some(overfullness) = block.overfull_score(false) {\n                    if overfullness > max {\n                        which = Some(name.clone());\n                        max = overfullness;\n                    }\n                }\n            });\n            match which {\n                Some(name) => AcquireOp::Steal(name),\n                None => AcquireOp::Wait,\n            }\n        } else if block_has_room {\n            AcquireOp::Create\n        } else {\n            AcquireOp::Wait\n        }\n    }\n\n    /// Plan a connection release.\n    pub fn plan_release(&self, db: &str, release_type: ReleaseType) -> ReleaseOp {\n        if self.drain.is_draining(db) {\n            return ReleaseOp::Discard;\n        }\n\n        if release_type == ReleaseType::Poison {\n            return ReleaseOp::Reopen;\n        }\n\n        let current_pool_size = self.blocks.total();\n        let max_pool_size = self.constraints.max;\n        if current_pool_size < max_pool_size {\n            trace!(\"Pool has room, keeping connection\");\n            return ReleaseOp::Release;\n        }\n\n        // We only want to consider a release elsewhere if this block is overfull\n        if let Some(Some(overfull)) = self.blocks.with(db, |block| block.overfull_score(true)) {\n            trace!(\"Block {db} is overfull ({overfull}), trying to release\");\n            let mut max = isize::MIN;\n            let mut which = None;\n            let mut s = \"\".to_owned();\n            self.blocks.with_all(|name, block| {\n                let is_self = &**name == db;\n                if let Some(mut hunger) = block.hunger_score(is_self) {\n                    // Penalize switching by boosting the current database's relative hunger here\n                    if is_self {\n                        hunger += SELF_HUNGER_BOOST_FOR_RELEASE.get();\n                    }\n\n                    if tracing::enabled!(tracing::Level::TRACE) {\n                        s += &format!(\"{name}={hunger} \");\n                    }\n\n                    if hunger > max {\n                        which = if is_self { None } else { Some(name.clone()) };\n                        max = hunger;\n                    }\n                }\n            });\n\n            if tracing::enabled!(tracing::Level::TRACE) {\n                trace!(\"Hunger: {s}\");\n            }\n\n            match which {\n                Some(name) => {\n                    trace!(\"Releasing to {name:?} with score {max}\");\n                    ReleaseOp::ReleaseTo(name)\n                }\n                None => {\n                    trace!(\"Keeping connection\");\n                    ReleaseOp::Release\n                }\n            }\n        } else {\n            trace!(\"Block {db} is not overfull, keeping\");\n            ReleaseOp::Release\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::{block::Blocks, test::BasicConnector, PoolConfig};\n    use anyhow::{Ok, Result};\n    use futures::{stream::FuturesUnordered, StreamExt};\n    use test_log::test;\n    use tokio::task::LocalSet;\n\n    #[test(tokio::test(flavor = \"current_thread\"))]\n    async fn test_pool_normal() -> Result<()> {\n        let future = async {\n            let connector = BasicConnector::no_delay();\n            let config = PoolConfig::suggested_default_for(10);\n            let blocks = Blocks::default();\n            let drain = Drain::default();\n            let algo = AlgoState {\n                constraints: &config.constraints,\n                drain: &drain,\n                blocks: &blocks,\n            };\n\n            let futures = FuturesUnordered::new();\n            for i in (0..algo.constraints.max).map(Name::from) {\n                assert_eq!(algo.plan_acquire(&i), AcquireOp::Create);\n                futures.push(blocks.create_if_needed(&connector, &i));\n            }\n            let conns: Vec<_> = futures.collect().await;\n            let futures = FuturesUnordered::new();\n            for i in (0..algo.constraints.max).map(Name::from) {\n                assert_eq!(algo.plan_acquire(&i), AcquireOp::Wait);\n                futures.push(blocks.queue(&i));\n            }\n            for conn in conns {\n                assert_eq!(\n                    algo.plan_release(&conn?.state.db_name, ReleaseType::Normal),\n                    ReleaseOp::Release\n                );\n            }\n            let conns: Vec<_> = futures.collect().await;\n            for conn in conns {\n                assert_eq!(\n                    algo.plan_release(&conn?.state.db_name, ReleaseType::Normal),\n                    ReleaseOp::Release\n                );\n            }\n            Ok(())\n        };\n        LocalSet::new().run_until(future).await\n    }\n\n    /// Ensures that when a pool is starved for connections because there are\n    /// more backends than connections, we release connections to other to\n    /// ensure fairness.\n    #[test(tokio::test(flavor = \"current_thread\", start_paused = true))]\n    async fn test_pool_starved() -> Result<()> {\n        let future = async {\n            let connector = BasicConnector::no_delay();\n            let config = PoolConfig::suggested_default_for(10);\n            let blocks = Blocks::default();\n            let drain = Drain::default();\n            let algo = AlgoState {\n                constraints: &config.constraints,\n                drain: &drain,\n                blocks: &blocks,\n            };\n\n            // Room for these\n            let futures = FuturesUnordered::new();\n            for db in (0..5).map(Name::from) {\n                assert_eq!(algo.plan_acquire(&db), AcquireOp::Create);\n                futures.push(blocks.create_if_needed(&connector, &db));\n            }\n            // ... and these\n            let futures2 = FuturesUnordered::new();\n            for db in (5..10).map(Name::from) {\n                assert_eq!(algo.plan_acquire(&db), AcquireOp::Create);\n                futures2.push(blocks.create_if_needed(&connector, &db));\n            }\n            // But not these (yet)\n            let futures3 = FuturesUnordered::new();\n            for db in (10..15).map(Name::from) {\n                assert_eq!(algo.plan_acquire(&db), AcquireOp::Wait);\n                futures3.push(blocks.queue(&db));\n            }\n            let conns: Vec<_> = futures.collect().await;\n            let conns2: Vec<_> = futures2.collect().await;\n            // These are released to 10..15\n            for conn in conns {\n                let conn = conn?;\n                let res = algo.plan_release(&conn.state.db_name, ReleaseType::Normal);\n                let ReleaseOp::ReleaseTo(to) = res else {\n                    panic!(\"Wrong release: {res:?}\");\n                };\n                blocks.task_move_to(&connector, conn, &to).await?;\n            }\n            // These don't have anywhere to go\n            for conn in conns2 {\n                let conn = conn?;\n                let res = algo.plan_release(&conn.state.db_name, ReleaseType::Normal);\n                let ReleaseOp::Release = res else {\n                    panic!(\"Wrong release: {res:?}\");\n                };\n            }\n            Ok(())\n        };\n        LocalSet::new().run_until(future).await\n    }\n}\n"
  },
  {
    "path": "rust/conn_pool/src/bin/optimizer.rs",
    "content": "use conn_pool::{knobs::*, test::spec::run_specs_tests_in_runtime};\nuse std::sync::{atomic::AtomicIsize, Mutex};\n\nuse genetic_algorithm::strategy::evolve::prelude::*;\nuse lru::LruCache;\nuse rand::Rng;\n\nstatic LOG_LOCK: Mutex<()> = Mutex::new(());\n\nfn main() {\n    // Enable tracing\n    // tracing_subscriber::fmt::init();\n\n    const PREDICATE: fn(&str) -> bool = |_name| true;\n\n    let qos = run_specs_tests_in_runtime(5, None, &PREDICATE).unwrap();\n    println!(\"{qos:?}\");\n\n    // the search goal to optimize towards (maximize or minimize)\n    #[derive(Clone, std::fmt::Debug, smart_default::SmartDefault)]\n    pub struct Optimizer {\n        #[default(std::sync::Arc::new(AtomicIsize::new(isize::MIN)))]\n        best: std::sync::Arc<AtomicIsize>,\n        #[default(LruCache::new(100_000_000.try_into().unwrap()))]\n        lru: LruCache<[isize; ALL_KNOB_COUNT], isize>,\n        #[default(std::time::Instant::now())]\n        now: std::time::Instant,\n    }\n\n    impl Fitness for Optimizer {\n        type Allele = isize;\n        fn calculate_for_chromosome(\n            &mut self,\n            chromosome: &Chromosome<Self::Allele>,\n        ) -> Option<FitnessValue> {\n            let mut knobs: [isize; ALL_KNOB_COUNT] = Default::default();\n            for (knob, gene) in knobs.iter_mut().zip(&chromosome.genes) {\n                *knob = *gene as _;\n            }\n            if let Some(res) = self.lru.get(&knobs) {\n                return Some(*res);\n            }\n\n            for (i, knob) in conn_pool::knobs::ALL_KNOBS.iter().enumerate() {\n                if knob.set(knobs[i]).is_err() {\n                    return None;\n                };\n            }\n\n            let weights = [(1.0, 1, None), (1.0, 5, Some(10.0))];\n            let outputs = weights\n                .map(|(_, count, scale)| run_specs_tests_in_runtime(count, scale, &PREDICATE));\n            let mut score = 0.0;\n            for ((weight, ..), output) in weights.iter().zip(&outputs) {\n                score += weight * output.as_ref().ok()?.qos_rms_error();\n            }\n            let qos_i = (score * 1_000_000.0) as isize;\n            if qos_i > self.best.load(std::sync::atomic::Ordering::SeqCst) {\n                let _lock = LOG_LOCK.lock();\n                println!(\"{:?} New best: {score:.02} {knobs:?}\", self.now.elapsed());\n                println!(\"{:?}\", conn_pool::knobs::ALL_KNOBS);\n                for (weight, output) in weights.iter().zip(outputs) {\n                    println!(\"{weight:?}: {:?}\", output.ok()?);\n                }\n                println!(\"*****************************\");\n                self.best.store(qos_i, std::sync::atomic::Ordering::SeqCst);\n            }\n            self.lru.push(knobs, qos_i);\n\n            Some(qos_i)\n        }\n    }\n\n    let mut seeds: Vec<Vec<isize>> = vec![];\n\n    // The current state\n    seeds.push(\n        conn_pool::knobs::ALL_KNOBS\n            .iter()\n            .map(|k| k.get() as _)\n            .collect(),\n    );\n\n    // Some randomness\n    for _ in 0..100 {\n        seeds.push(\n            conn_pool::knobs::ALL_KNOBS\n                .iter()\n                .map(|k| {\n                    let proposed: isize =\n                        (k.get() as f32 * rand::thread_rng().gen_range(-2.0..2.0_f32)) as _;\n                    proposed\n                })\n                .collect(),\n        );\n    }\n\n    let mut final_seeds = vec![];\n    for mut seed in seeds {\n        for (i, knob) in conn_pool::knobs::ALL_KNOBS.iter().enumerate() {\n            let mut value = seed[i] as _;\n            if knob.set(value).is_err() {\n                knob.clamp(&mut value);\n                seed[i] = value as _;\n            };\n        }\n        final_seeds.push(seed);\n    }\n\n    let genotype = RangeGenotype::builder()\n        .with_genes_size(conn_pool::knobs::ALL_KNOBS.len())\n        .with_allele_range(-100_000..=100_000)\n        .with_allele_mutation_range(-1000..=1000)\n        .with_seed_genes_list(final_seeds)\n        .build()\n        .unwrap();\n\n    let mut rng = rand::thread_rng(); // a randomness provider implementing Trait rand::Rng\n    let evolve = Evolve::builder()\n        .with_multithreading(true)\n        .with_genotype(genotype)\n        .with_target_population_size(1000)\n        .with_target_fitness_score(100 * 1_000_000)\n        .with_max_stale_generations(1000)\n        .with_mutate(MutateMultiGeneDynamic::new(3, 0.2, 500))\n        .with_fitness(Optimizer::default())\n        .with_crossover(CrossoverUniform::new(true))\n        .with_compete(CompeteTournament::new(200))\n        .with_extension(ExtensionMassInvasion::new(200, 0.8))\n        .with_reporter(EvolveReporterSimple::new(10))\n        .call(&mut rng)\n        .unwrap();\n    println!(\"{}\", evolve);\n}\n"
  },
  {
    "path": "rust/conn_pool/src/block.rs",
    "content": "use crate::{\n    algo::{\n        PoolAlgoTargetData, PoolAlgorithmDataBlock, PoolAlgorithmDataMetrics,\n        PoolAlgorithmDataPool, VisitPoolAlgoData,\n    },\n    conn::*,\n    metrics::{MetricVariant, MetricsAccum, PoolMetrics},\n    time::Instant,\n    waitqueue::WaitQueue,\n};\nuse futures::future::Either;\nuse serde::Serialize;\nuse std::{\n    cell::RefCell,\n    collections::HashMap,\n    future::{poll_fn, ready, Future},\n    rc::Rc,\n    time::Duration,\n};\nuse tracing::trace;\n\n/// Perform a consistency check on entry and exit for this function.\nmacro_rules! consistency_check {\n    ($self:ident) => {\n        // On entry\n        $self.check_consistency();\n        // On exit\n        scopeguard::defer!($self.check_consistency());\n    };\n}\n\n/// A cheaply cloneable name string.\n#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]\npub struct Name(Rc<String>);\n\nimpl Serialize for Name {\n    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n    where\n        S: serde::Serializer,\n    {\n        serializer.serialize_str(self.0.as_str())\n    }\n}\n\nimpl std::fmt::Display for Name {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        self.0.fmt(f)\n    }\n}\nimpl std::fmt::Debug for Name {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        self.0.fmt(f)\n    }\n}\n\nimpl PartialEq<str> for Name {\n    fn eq(&self, other: &str) -> bool {\n        self.0.as_str() == other\n    }\n}\n\nimpl From<&str> for Name {\n    fn from(value: &str) -> Self {\n        Name(Rc::new(String::from(value)))\n    }\n}\n\nimpl From<String> for Name {\n    fn from(value: String) -> Self {\n        Name(Rc::new(value))\n    }\n}\n\n#[cfg(test)]\nimpl From<usize> for Name {\n    fn from(value: usize) -> Self {\n        Name::from(format!(\"db-{value}\"))\n    }\n}\n\nimpl AsRef<str> for Name {\n    fn as_ref(&self) -> &str {\n        self.0.as_str()\n    }\n}\n\nimpl std::ops::Deref for Name {\n    type Target = str;\n    fn deref(&self) -> &Self::Target {\n        self.0.as_str()\n    }\n}\n\nimpl std::borrow::Borrow<str> for Name {\n    fn borrow(&self) -> &str {\n        self.0.as_str()\n    }\n}\n\n/// Manages the connection state for a single backend database.\n///\n/// This is only a set of connections, and does not understand policy, capacity,\n/// balancing, or anything outside of a request to make, transfer or discard a\n/// connection. It also manages connection statistics for higher layers of code\n/// to make decisions.\n///\n/// The block provides a number of tasks related to connections. The task\n/// methods provide futures, but run the accounting \"up-front\" to ensure that we\n/// keep a handle on quotas, even if running the task async.\n///\n/// The block has an associated data generic parameter that may be provided,\n/// where additional metadata for this block can live.\npub struct Block<C: Connector, D: Default = ()> {\n    pub db_name: Name,\n    conns: Conns<C>,\n    state: Rc<ConnState>,\n    /// Associated data for this block useful for statistics, quotas or other\n    /// information. This is provided by the algorithm in this crate.\n    data: D,\n}\n\nimpl<C: Connector, D: Default> Block<C, D> {\n    pub fn new(db_name: Name, parent_metrics: Option<Rc<MetricsAccum>>) -> Self {\n        let metrics = Rc::new(MetricsAccum::new(parent_metrics));\n        let state = ConnState {\n            db_name: db_name.clone(),\n            waiters: WaitQueue::new(),\n            metrics,\n        }\n        .into();\n        Self {\n            db_name,\n            conns: Conns::default(),\n            state,\n            data: Default::default(),\n        }\n    }\n\n    pub fn is_empty(&self) -> bool {\n        self.len() == 0\n    }\n\n    pub fn len(&self) -> usize {\n        self.state.metrics.total()\n    }\n\n    fn conn(&self, conn: Conn<C>) -> ConnHandle<C> {\n        ConnHandle::new(conn, self.state.clone())\n    }\n\n    #[track_caller]\n    pub fn check_consistency(&self) {\n        if cfg!(debug_assertions) {\n            // These should never be non-zero during the consistency check\n            assert_eq!(\n                self.state.metrics.count(MetricVariant::Closed),\n                0,\n                \"Should not have any closed connections\"\n            );\n\n            assert_eq!(\n                self.len(),\n                self.conns.len(),\n                \"Block {} failed consistency check. Total connection count was wrong.\",\n                self.db_name\n            );\n            let conn_metrics = MetricsAccum::default();\n            self.conns.walk(|conn| conn_metrics.insert(conn.variant()));\n            conn_metrics.set_value(MetricVariant::Waiting, self.state.waiters.lock.get());\n            assert_eq!(\n                self.metrics().summary().value,\n                conn_metrics.summary().value,\n                \"Connection metrics are incorrect. Left: actual, right: expected\"\n            );\n        }\n    }\n\n    #[inline]\n    pub fn metrics(&self) -> Rc<MetricsAccum> {\n        self.state.metrics.clone()\n    }\n\n    /// Creates a connection from this block.\n    #[cfg(test)]\n    fn create(\n        self: Rc<Self>,\n        connector: &C,\n    ) -> impl Future<Output = ConnResult<ConnHandle<C>, C::Error>> {\n        let conn = {\n            consistency_check!(self);\n            let conn = Conn::new(connector.connect(&self.db_name), &self.state.metrics);\n            self.conns.insert(conn.clone());\n            conn\n        };\n        async move {\n            consistency_check!(self);\n            let res =\n                poll_fn(|cx| conn.poll_ready(cx, &self.state.metrics, MetricVariant::Active)).await;\n            if res.is_ok() {\n                Ok(self.conn(conn))\n            } else {\n                let res = self.conns.remove(conn, &self.state.metrics);\n                Err(res.err().unwrap())\n            }\n        }\n    }\n\n    /// Awaits a connection from this block.\n    #[cfg(test)]\n    fn create_if_needed(\n        self: Rc<Self>,\n        connector: &C,\n    ) -> impl Future<Output = ConnResult<ConnHandle<C>, C::Error>> {\n        if let Some(res) = self.conns.try_acquire_idle_mru(&self.state.metrics) {\n            return Either::Left(ready(res.map(|c| self.conn(c))));\n        }\n        Either::Right(self.create(connector))\n    }\n\n    /// Awaits a connection from this block.\n    fn queue(self: Rc<Self>) -> impl Future<Output = ConnResult<ConnHandle<C>, C::Error>> {\n        // Note that we cannot skip the waiting queue because this makes it\n        // difficult to test and manage metrics -- if we skipped the queue then\n        // there would never be an apparent waiter.\n\n        // Update the metrics now before we actually queue\n        self.state.waiters.lock();\n        self.state.metrics.insert(MetricVariant::Waiting);\n        let state = self.state.clone();\n        let now = Instant::now();\n        let guard = scopeguard::guard((), move |_| {\n            state.waiters.unlock();\n            state\n                .metrics\n                .remove_time(MetricVariant::Waiting, now.elapsed());\n        });\n        async move {\n            loop {\n                consistency_check!(self);\n                if let Some(res) = self.conns.try_acquire_idle_mru(&self.state.metrics) {\n                    drop(guard);\n                    return res.map(|c| self.conn(c));\n                }\n                trace!(\"Queueing for a connection\");\n                self.state.waiters.queue().await;\n            }\n        }\n    }\n\n    /// Creates a connection from this block.\n    fn task_create(\n        self: Rc<Self>,\n        connector: &C,\n    ) -> impl Future<Output = ConnResult<(), C::Error>> {\n        let conn = {\n            consistency_check!(self);\n            let conn = Conn::new(connector.connect(&self.db_name), &self.state.metrics);\n            self.conns.insert(conn.clone());\n            conn\n        };\n        self.poll_to_idle(conn)\n    }\n\n    /// Close one of idle connections in this block\n    ///\n    /// ## Panics\n    ///\n    /// If there are no idle connections, this function will panic.\n    fn task_close_one(\n        self: Rc<Self>,\n        connector: &C,\n    ) -> impl Future<Output = ConnResult<(), C::Error>> {\n        let conn = {\n            consistency_check!(self);\n            let conn = self\n                .conns\n                .try_get_idle(&self.state.metrics, false)\n                .expect(\"Could not acquire a connection\");\n            let conn = match conn {\n                Ok(conn) => conn,\n                Err(err) => {\n                    return Either::Left(ready(Err(err)));\n                }\n            };\n            conn.close(connector, &self.state.metrics);\n            conn\n        };\n        Either::Right(self.poll_to_close(conn))\n    }\n\n    /// Steals a connection from one block to another.\n    ///\n    /// ## Panics\n    ///\n    /// If there are no idle connections, this function will panic.\n    fn task_reconnect(\n        from: Rc<Self>,\n        to: Rc<Self>,\n        connector: &C,\n    ) -> impl Future<Output = ConnResult<(), C::Error>> {\n        let conn = {\n            consistency_check!(from);\n            consistency_check!(to);\n\n            let conn = from\n                .conns\n                .try_take_idle_lru(&from.state.metrics)\n                .expect(\"Could not acquire a connection\");\n            let conn = Conn::new(connector.reconnect(conn, &to.db_name), &to.state.metrics);\n            to.conns.insert(conn.clone());\n            conn\n        };\n        to.poll_to_idle(conn)\n    }\n\n    /// Moves a connection to a different block than it was acquired from\n    /// without giving any wakers on the old block a chance to get it.\n    fn task_reconnect_conn(\n        from: Rc<Self>,\n        to: Rc<Self>,\n        conn: ConnHandle<C>,\n        connector: &C,\n    ) -> impl Future<Output = ConnResult<(), C::Error>> {\n        let conn = {\n            consistency_check!(from);\n            consistency_check!(to);\n\n            let conn = conn.into_inner();\n            let conn = from\n                .conns\n                .remove(conn, &from.state.metrics)\n                .unwrap()\n                .unwrap();\n            let conn = Conn::new(connector.reconnect(conn, &to.db_name), &to.state.metrics);\n            to.conns.insert(conn.clone());\n            conn\n        };\n        to.poll_to_idle(conn)\n    }\n\n    /// Marks a connection as requiring re-open.\n    fn task_reopen(\n        self: Rc<Self>,\n        conn: ConnHandle<C>,\n        connector: &C,\n    ) -> impl Future<Output = ConnResult<(), C::Error>> {\n        let conn = {\n            consistency_check!(self);\n            let conn = conn.into_inner();\n            conn.reopen(connector, &self.state.metrics, &self.db_name);\n            conn\n        };\n        self.poll_to_idle(conn)\n    }\n\n    /// Marks a connection as requiring a discard.\n    fn task_discard(\n        self: Rc<Self>,\n        conn: ConnHandle<C>,\n        connector: &C,\n    ) -> impl Future<Output = ConnResult<(), C::Error>> {\n        let conn = {\n            consistency_check!(self);\n            let conn = conn.into_inner();\n            conn.discard(connector, &self.state.metrics);\n            conn\n        };\n        self.poll_to_close(conn)\n    }\n\n    async fn poll_to_close(self: Rc<Self>, conn: Conn<C>) -> ConnResult<(), C::Error> {\n        consistency_check!(self);\n        // If the close task fails, we route the error to the task's result\n        // as there will never be a waiter who will pick it up\n        let res =\n            poll_fn(|cx| conn.poll_ready(cx, &self.state.metrics, MetricVariant::Closed)).await;\n        let res2 = self.conns.remove(conn, &self.state.metrics);\n        debug_assert_eq!(res.is_ok(), res2.is_ok());\n        let conn = res2?;\n        debug_assert!(conn.is_none());\n        Ok(())\n    }\n\n    /// Once a connection has completed the `Connecting` phase, we will route\n    /// the results to a waiter, assuming there is one.\n    async fn poll_to_idle(self: Rc<Self>, conn: Conn<C>) -> ConnResult<(), C::Error> {\n        let res = poll_fn(|cx| conn.poll_ready(cx, &self.state.metrics, MetricVariant::Idle)).await;\n        self.state.waiters.trigger();\n        res.map_err(|_| ConnError::TaskFailed)\n    }\n}\n\n/// Manages the connection state for a number of backend databases. See\n/// the notes on [`Block`] for the scope of responsibility of this struct.\npub struct Blocks<C: Connector, D: Default = ()> {\n    map: RefCell<HashMap<Name, Rc<Block<C, D>>>>,\n    metrics: Rc<MetricsAccum>,\n}\n\nimpl<C: Connector, D: Default> Default for Blocks<C, D> {\n    fn default() -> Self {\n        Self {\n            map: RefCell::new(HashMap::default()),\n            metrics: Rc::new(MetricsAccum::default()),\n        }\n    }\n}\n\nimpl<C: Connector> PoolAlgorithmDataMetrics for Block<C, PoolAlgoTargetData> {\n    #[inline(always)]\n    fn avg_ms(&self, variant: MetricVariant) -> usize {\n        self.state.metrics.avg_ms(variant)\n    }\n    #[inline(always)]\n    fn count(&self, variant: MetricVariant) -> usize {\n        self.state.metrics.count(variant)\n    }\n    #[inline(always)]\n    fn max(&self, variant: MetricVariant) -> usize {\n        self.state.metrics.max(variant)\n    }\n    #[inline(always)]\n    fn total(&self) -> usize {\n        self.state.metrics.total()\n    }\n    #[inline(always)]\n    fn total_max(&self) -> usize {\n        self.state.metrics.total_max()\n    }\n}\n\nimpl<C: Connector> PoolAlgorithmDataBlock for Block<C, PoolAlgoTargetData> {\n    #[inline(always)]\n    fn target(&self) -> usize {\n        self.data.target()\n    }\n    #[inline(always)]\n    fn set_target(&self, target: usize) {\n        self.data.set_target(target);\n    }\n    #[inline(always)]\n    fn insert_demand(&self, demand: u32) {\n        self.data.insert_demand(demand)\n    }\n    #[inline(always)]\n    fn demand(&self) -> u32 {\n        self.data.demand()\n    }\n    #[inline(always)]\n    fn count_older(&self, variant: MetricVariant, than: Duration) -> usize {\n        self.conns.count_older(variant, than)\n    }\n    #[inline(always)]\n    fn oldest_ms(&self, variant: MetricVariant) -> usize {\n        assert_eq!(variant, MetricVariant::Waiting);\n        self.state.waiters.oldest().as_millis() as _\n    }\n    #[inline(always)]\n    fn youngest_ms(&self) -> usize {\n        self.conns.youngest().as_millis() as _\n    }\n}\n\nimpl<C: Connector> PoolAlgorithmDataMetrics for Blocks<C, PoolAlgoTargetData> {\n    #[inline(always)]\n    fn avg_ms(&self, variant: MetricVariant) -> usize {\n        self.metrics.avg_ms(variant)\n    }\n    #[inline(always)]\n    fn count(&self, variant: MetricVariant) -> usize {\n        self.metrics.count(variant)\n    }\n    #[inline(always)]\n    fn max(&self, variant: MetricVariant) -> usize {\n        self.metrics.max(variant)\n    }\n    #[inline(always)]\n    fn total(&self) -> usize {\n        self.metrics.total()\n    }\n    #[inline(always)]\n    fn total_max(&self) -> usize {\n        self.metrics.total_max()\n    }\n}\n\nimpl<C: Connector> PoolAlgorithmDataPool for Blocks<C, PoolAlgoTargetData> {\n    #[inline(always)]\n    fn reset_max(&self) {\n        self.metrics.reset_max();\n        for block in self.map.borrow().values() {\n            block.metrics().reset_max()\n        }\n    }\n}\n\nimpl<C: Connector> VisitPoolAlgoData for Blocks<C, PoolAlgoTargetData> {\n    type Block = Block<C, PoolAlgoTargetData>;\n\n    fn ensure_block(&self, db: &str, default_demand: usize) -> bool {\n        if self.map.borrow().contains_key(db) {\n            false\n        } else {\n            let block: Rc<Block<C, PoolAlgoTargetData>> =\n                Rc::new(Block::new(db.into(), Some(self.metrics.clone())));\n            block.data.insert_demand(default_demand as _);\n            self.map.borrow_mut().insert(db.into(), block);\n            true\n        }\n    }\n\n    fn with<T>(&self, db: &str, f: impl Fn(&Block<C, PoolAlgoTargetData>) -> T) -> Option<T> {\n        self.map.borrow().get(db).map(|block| f(block))\n    }\n\n    fn with_all(&self, mut f: impl FnMut(&Name, &Block<C, PoolAlgoTargetData>)) {\n        self.map.borrow_mut().retain(|name, block| {\n            if block.is_empty() && block.data.demand() == 0 {\n                false\n            } else {\n                f(name, block);\n                true\n            }\n        });\n    }\n}\n\nimpl<C: Connector, D: Default> Blocks<C, D> {\n    /// To ensure that we can trust our summary statistics, we run a consistency check in\n    /// debug mode on most operations. This is cheap enough to run all the time, but we\n    /// assume confidence in this code and disable the checks in release mode.\n    ///\n    /// See [`consistency_check!`] for the macro that calls this on entry and exit.\n    #[track_caller]\n    pub fn check_consistency(&self) {\n        if cfg!(debug_assertions) {\n            let mut total = 0;\n            for block in self.map.borrow().values() {\n                block.check_consistency();\n                total += block.len();\n            }\n            if total != self.metrics.total() && tracing::enabled!(tracing::Level::TRACE) {\n                for block in self.map.borrow().values() {\n                    trace!(\n                        \"{}: {} {:?}\",\n                        block.db_name,\n                        block.len(),\n                        block.metrics().summary()\n                    );\n                }\n            }\n            assert_eq!(\n                total,\n                self.metrics.total(),\n                \"Blocks failed consistency check. Total connection count was wrong. {:?}\",\n                self.metrics\n            );\n        }\n    }\n\n    pub fn name(&self, db: &str) -> Option<Name> {\n        if let Some((name, _)) = self.map.borrow().get_key_value(db) {\n            Some(name.clone())\n        } else {\n            None\n        }\n    }\n\n    pub fn contains(&self, db: &str) -> bool {\n        self.map.borrow().contains_key(db)\n    }\n\n    pub fn block_count(&self) -> usize {\n        self.map.borrow().len()\n    }\n\n    pub fn conn_count(&self) -> usize {\n        self.metrics.total()\n    }\n\n    pub fn metrics(&self, db: &str) -> Rc<MetricsAccum> {\n        self.map\n            .borrow_mut()\n            .get(db)\n            .map(|b| b.metrics())\n            .unwrap_or_default()\n    }\n\n    fn block(&self, db: &str) -> Rc<Block<C, D>> {\n        let mut lock = self.map.borrow_mut();\n        if let Some(block) = lock.get(db) {\n            block.clone()\n        } else {\n            let db = Name(Rc::new(db.to_owned()));\n            let block = Rc::new(Block::new(db.clone(), Some(self.metrics.clone())));\n            lock.insert(db, block.clone());\n            block\n        }\n    }\n\n    /// Create and acquire a connection. Only used for tests.\n    #[cfg(test)]\n    pub fn create(\n        &self,\n        connector: &C,\n        db: &str,\n    ) -> impl Future<Output = ConnResult<ConnHandle<C>, C::Error>> {\n        consistency_check!(self);\n        let block = self.block(db);\n        block.create(connector)\n    }\n\n    /// Create and acquire a connection. If a connection is free, skips\n    /// creation. Only used for tests.\n    #[cfg(test)]\n    pub fn create_if_needed(\n        &self,\n        connector: &C,\n        db: &str,\n    ) -> impl Future<Output = ConnResult<ConnHandle<C>, C::Error>> {\n        consistency_check!(self);\n        let block = self.block(db);\n        block.create_if_needed(connector)\n    }\n\n    /// Queue for a connection.\n    pub fn queue(&self, db: &str) -> impl Future<Output = ConnResult<ConnHandle<C>, C::Error>> {\n        consistency_check!(self);\n        let block = self.block(db);\n        block.queue()\n    }\n\n    /// Creates one connection in a block.\n    pub fn task_create_one(\n        &self,\n        connector: &C,\n        db: &str,\n    ) -> impl Future<Output = ConnResult<(), C::Error>> {\n        consistency_check!(self);\n        let block = self.block(db);\n        block.task_create(connector)\n    }\n\n    /// Closes one connection in a block.\n    pub fn task_close_one(\n        &self,\n        connector: &C,\n        db: &str,\n    ) -> impl Future<Output = ConnResult<(), C::Error>> {\n        consistency_check!(self);\n        let block = self.block(db);\n        block.task_close_one(connector)\n    }\n\n    /// Steals a connection from one block to another.\n    pub fn task_steal(\n        &self,\n        connector: &C,\n        db: &str,\n        from: &str,\n    ) -> impl Future<Output = ConnResult<(), C::Error>> {\n        let from_block = self.block(from);\n        let to_block = self.block(db);\n        Block::task_reconnect(from_block, to_block, connector)\n    }\n\n    /// Moves a connection to a different block than it was acquired from\n    /// without giving any wakers on the old block a chance to get it.\n    pub fn task_move_to(\n        &self,\n        connector: &C,\n        conn: ConnHandle<C>,\n        db: &str,\n    ) -> impl Future<Output = ConnResult<(), C::Error>> {\n        let from_block = self.block(&conn.state.db_name);\n        let to_block = self.block(db);\n        Block::task_reconnect_conn(from_block, to_block, conn, connector)\n    }\n\n    /// Marks a connection as requiring a discard.\n    pub fn task_discard(\n        &self,\n        connector: &C,\n        conn: ConnHandle<C>,\n    ) -> impl Future<Output = ConnResult<(), C::Error>> {\n        let block = self.block(&conn.state.db_name);\n        block.task_discard(conn, connector)\n    }\n\n    /// Marks a connection as requiring re-open.\n    pub fn task_reopen(\n        &self,\n        connector: &C,\n        conn: ConnHandle<C>,\n    ) -> impl Future<Output = ConnResult<(), C::Error>> {\n        let block = self.block(&conn.state.db_name);\n        block.task_reopen(conn, connector)\n    }\n\n    /// Do we have any live blocks?\n    pub fn is_empty(&self) -> bool {\n        self.conn_count() == 0\n    }\n}\n\nimpl<C: Connector> Blocks<C, PoolAlgoTargetData> {\n    pub fn summary(&self) -> PoolMetrics {\n        let mut metrics = PoolMetrics::default();\n        metrics.pool = self.metrics.summary();\n        metrics.all_time = self.metrics.all_time();\n        for (name, block) in self.map.borrow().iter() {\n            let mut block_metrics = block.metrics().summary();\n            block_metrics.target = block.data.target();\n            metrics.blocks.insert(name.clone(), block_metrics);\n        }\n        metrics\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::metrics::{MetricVariant, VariantArray};\n    use crate::test::*;\n    use anyhow::{Ok, Result};\n    use pretty_assertions::assert_eq;\n    use test_log::test;\n    use tokio::task::LocalSet;\n\n    /// Tiny DSL to make the tests more readable.\n    macro_rules! assert_block {\n        ($block:ident has $($count:literal $type:ident),+) => {\n            assert_eq!(\n                $block.metrics().summary().value,\n                [$(VariantArray::with(MetricVariant::$type, $count)),+].into_iter().sum(),\n                stringify!(Expected block has $($count $type),+)\n            );\n        };\n        ($block:ident $db:literal is empty) => {\n            assert_eq!($block.metrics($db).summary().value, VariantArray::default(), stringify!(Expected block is empty));\n        };\n        ($block:ident $db:literal has $($count:literal $type:ident),+) => {\n            assert_eq!(\n                $block.metrics($db).summary().value,\n                [$(VariantArray::with(MetricVariant::$type, $count)),+].into_iter().sum(),\n                stringify!(Expected block $db has $($count $type),+)\n            );\n        };\n        ($block:ident $db:literal has max $($count:literal $type:ident),+) => {\n            assert_eq!(\n                $block.metrics($db).summary().max,\n                [$(VariantArray::with(MetricVariant::$type, $count)),+].into_iter().sum(),\n                stringify!(Expected block $db has max $($count $type),+)\n            );\n        };\n        ($block:ident $db:literal has all time $($count:literal $type:ident),+) => {\n            assert_eq!(\n                $block.metrics($db).summary().all_time,\n                [$(VariantArray::with(MetricVariant::$type, $count)),+].into_iter().sum(),\n                stringify!(Expected block $db has all time $($count $type),+)\n            );\n        };\n    }\n\n    #[test(tokio::test)]\n    async fn test_counts_updated() -> Result<()> {\n        let connector = BasicConnector::no_delay();\n        let block = Rc::new(Block::<BasicConnector>::new(Name::from(\"db\"), None));\n        let f = block.clone().create(&connector);\n        assert_block!(block has 1 Connecting);\n        let conn = f.await?;\n        assert_block!(block has 1 Active);\n        let f = block.clone().queue();\n        assert_block!(block has 1 Waiting, 1 Active);\n        drop(conn);\n        assert_block!(block has 1 Waiting, 1 Idle);\n        let conn = f.await?;\n        assert_block!(block has 1 Active);\n        drop(conn);\n        assert_block!(block has 1 Idle);\n\n        Ok(())\n    }\n\n    #[test(tokio::test)]\n    async fn test_block() -> Result<()> {\n        let connector = BasicConnector::no_delay();\n        let block = Rc::new(Block::<BasicConnector>::new(Name::from(\"db\"), None));\n        let conn = block.clone().create(&connector).await?;\n        assert_block!(block has 1 Active);\n        let local = LocalSet::new();\n        let block2 = block.clone();\n        local.spawn_local(async move {\n            assert_block!(block2 has 1 Active);\n            let conn = block2.clone().queue().await?;\n            assert_block!(block2 has 1 Active);\n            drop(conn);\n            Ok(())\n        });\n        local.spawn_local(async move {\n            tokio::task::yield_now().await;\n            drop(conn);\n        });\n        local.await;\n        Ok(())\n    }\n\n    #[test(tokio::test)]\n    async fn test_block_parallel_acquire() -> Result<()> {\n        let connector = BasicConnector::no_delay();\n        let block = Rc::new(Block::<BasicConnector>::new(Name::from(\"db\"), None));\n        block.clone().create(&connector).await?;\n        block.clone().create(&connector).await?;\n        block.clone().create(&connector).await?;\n        assert_block!(block has 3 Idle);\n\n        let local = LocalSet::new();\n        for i in 0..100 {\n            let block2 = block.clone();\n            local.spawn_local(async move {\n                for _ in 0..i % 10 {\n                    tokio::task::yield_now().await;\n                }\n                block2.clone().queue().await\n            });\n        }\n        local.await;\n        assert_block!(block has 3 Idle);\n        Ok(())\n    }\n\n    #[test(tokio::test)]\n    async fn test_block_fails_connect() -> Result<()> {\n        let connector = BasicConnector::no_delay();\n        let block = Rc::new(Block::<BasicConnector>::new(Name::from(\"db\"), None));\n        connector.fail_next_connect();\n        block\n            .clone()\n            .create(&connector)\n            .await\n            .expect_err(\"Expected this to fail\");\n        Ok(())\n    }\n\n    #[test(tokio::test)]\n    async fn test_block_fails_queue() -> Result<()> {\n        let connector = BasicConnector::no_delay();\n        let block = Rc::new(Block::<BasicConnector>::new(Name::from(\"db\"), None));\n        connector.fail_next_connect();\n        let queue = block.clone().queue();\n        let local = LocalSet::new();\n        local.spawn_local(block.clone().task_create(&connector));\n        local.await;\n        queue\n            .await\n            .expect_err(\"Expected this queueing operation to fail\");\n        Ok(())\n    }\n\n    #[test(tokio::test)]\n    async fn test_block_fails_queue_multiple() -> Result<()> {\n        let connector = BasicConnector::no_delay();\n        let block = Rc::new(Block::<BasicConnector>::new(Name::from(\"db\"), None));\n        connector.fail_next_connect();\n        let queue = block.clone().queue();\n        let local = LocalSet::new();\n        // The first will fail, but the second one will provide a valid\n        // connection to the waiter.\n        local.spawn_local(block.clone().task_create(&connector));\n        local.spawn_local(block.clone().task_create(&connector));\n        local.await;\n        queue.await?;\n        Ok(())\n    }\n\n    /// This test shows that we\n    #[test(tokio::test)]\n    async fn test_block_fails_queue_multiple_2() -> Result<()> {\n        let connector = BasicConnector::no_delay();\n        let block = Rc::new(Block::<BasicConnector>::new(Name::from(\"db\"), None));\n        let queue1 = block.clone().queue();\n        let queue2 = block.clone().queue();\n        let local = LocalSet::new();\n\n        connector.fail_next_connect();\n        local.spawn_local(block.clone().task_create(&connector));\n        connector.fail_next_connect();\n        local.spawn_local(block.clone().task_create(&connector));\n        local.await;\n        queue1.await.expect_err(\"Expected this to fail\");\n        queue2.await.expect_err(\"Expected this to fail\");\n        Ok(())\n    }\n\n    #[test(tokio::test)]\n    async fn test_steal() -> Result<()> {\n        let connector = BasicConnector::no_delay();\n        let blocks = Blocks::<_, ()>::default();\n        assert_eq!(0, blocks.block_count());\n        blocks.create(&connector, \"db\").await?;\n        blocks.create(&connector, \"db\").await?;\n        blocks.create(&connector, \"db\").await?;\n        blocks.metrics(\"db\").reset_max();\n        blocks.metrics(\"db2\").reset_max();\n        assert_eq!(1, blocks.block_count());\n        assert_block!(blocks \"db\" has 3 Idle);\n        assert_block!(blocks \"db2\" is empty);\n        blocks.task_steal(&connector, \"db2\", \"db\").await?;\n        blocks.task_steal(&connector, \"db2\", \"db\").await?;\n        blocks.task_steal(&connector, \"db2\", \"db\").await?;\n        // Block hasn't been GC'd yet\n        assert_eq!(2, blocks.block_count());\n        assert_block!(blocks \"db\" is empty);\n        assert_block!(blocks \"db2\" has 3 Idle);\n        // Should not activate a connection to steal it\n        assert_block!(blocks \"db\" has max 0 Active, 3 Idle);\n        assert_block!(blocks \"db2\" has max 0 Active, 1 Connecting, 3 Idle);\n        Ok(())\n    }\n\n    #[test(tokio::test)]\n    async fn test_steal_fails() -> Result<()> {\n        let connector = BasicConnector::no_delay();\n        let blocks = Blocks::<_, ()>::default();\n        assert_eq!(0, blocks.block_count());\n        blocks.create(&connector, \"db\").await?;\n        blocks.create(&connector, \"db\").await?;\n        blocks.metrics(\"db\").reset_max();\n        connector.fail_next_connect();\n        blocks\n            .task_steal(&connector, \"db2\", \"db\")\n            .await\n            .expect_err(\"Expected to fail\");\n        assert_block!(blocks \"db\" has 1 Idle);\n        assert_block!(blocks \"db2\" has 1 Failed);\n        let queue = blocks.queue(\"db2\");\n        assert_block!(blocks \"db2\" has 1 Waiting, 1 Failed);\n        connector.fail_next_connect();\n        blocks\n            .task_steal(&connector, \"db2\", \"db\")\n            .await\n            .expect_err(\"Expected to fail\");\n        queue.await.expect_err(\"Expected this to fail\");\n        Ok(())\n    }\n\n    #[test(tokio::test)]\n    async fn test_move() -> Result<()> {\n        let connector = BasicConnector::no_delay();\n        let blocks = Blocks::<_, ()>::default();\n        assert_eq!(0, blocks.block_count());\n        blocks.create(&connector, \"db\").await?;\n        blocks.create(&connector, \"db\").await?;\n        blocks.create(&connector, \"db\").await?;\n        blocks.metrics(\"db\").reset_max();\n        blocks.metrics(\"db2\").reset_max();\n        assert_eq!(1, blocks.block_count());\n        assert_block!(blocks \"db\" has 3 Idle);\n        assert_block!(blocks \"db2\" is empty);\n        let conn = blocks.queue(\"db\").await?;\n        blocks.task_move_to(&connector, conn, \"db2\").await?;\n        assert_eq!(2, blocks.block_count());\n        assert_block!(blocks \"db\" has 2 Idle);\n        assert_block!(blocks \"db2\" has 1 Idle);\n        // Should not activate a connection to move it\n        assert_block!(blocks \"db\" has max 1 Active, 3 Idle, 1 Waiting);\n        assert_block!(blocks \"db2\" has max 0 Active, 1 Connecting, 1 Idle);\n        Ok(())\n    }\n\n    #[test(tokio::test)]\n    async fn test_move_fail() -> Result<()> {\n        let connector = BasicConnector::no_delay();\n        let blocks = Blocks::<_, ()>::default();\n        assert_eq!(0, blocks.block_count());\n        blocks.create(&connector, \"db\").await?;\n        blocks.create(&connector, \"db\").await?;\n        blocks.create(&connector, \"db\").await?;\n        assert_block!(blocks \"db\" has 3 Idle);\n        let conn1 = blocks.queue(\"db\").await?;\n        let conn2 = blocks.queue(\"db\").await?;\n        blocks.metrics(\"db\").reset_max();\n        blocks.metrics(\"db2\").reset_max();\n        connector.fail_next_connect();\n        let queue = blocks.queue(\"db2\");\n        blocks\n            .task_move_to(&connector, conn1, \"db2\")\n            .await\n            .expect_err(\"Expected this to fail\");\n        blocks.task_move_to(&connector, conn2, \"db2\").await?;\n        queue.await?;\n        assert_eq!(2, blocks.block_count());\n        assert_block!(blocks \"db\" has 1 Idle);\n        assert_block!(blocks \"db2\" has 1 Idle, 1 Failed);\n        assert_block!(blocks \"db\" has max 2 Active, 1 Idle);\n        assert_block!(blocks \"db2\" has max 1 Active, 1 Connecting, 1 Idle, 1 Failed, 1 Waiting);\n        Ok(())\n    }\n\n    #[test(tokio::test)]\n    async fn test_close() -> Result<()> {\n        let connector = BasicConnector::no_delay();\n        let blocks = Blocks::<_, ()>::default();\n        assert_eq!(0, blocks.block_count());\n        blocks.create(&connector, \"db\").await?;\n        blocks.create(&connector, \"db\").await?;\n        blocks.metrics(\"db\").reset_max();\n        assert_eq!(1, blocks.block_count());\n        assert_block!(blocks \"db\" has 2 Idle);\n        blocks.task_close_one(&connector, \"db\").await?;\n        // This will only fail the task\n        connector.fail_next_disconnect();\n        blocks\n            .task_close_one(&connector, \"db\")\n            .await\n            .expect_err(\"Expected to fail\");\n        assert_block!(blocks \"db\" is empty);\n        // Hasn't GC'd yet\n        assert_eq!(1, blocks.block_count());\n        // Should not activate a connection to close it\n        assert_block!(blocks \"db\" has max 0 Active, 1 Disconnecting, 2 Idle, 1 Failed, 1 Closed);\n        Ok(())\n    }\n\n    #[test(tokio::test)]\n    async fn test_reopen() -> Result<()> {\n        let connector = BasicConnector::no_delay();\n        let blocks = Blocks::<_, ()>::default();\n        assert_eq!(0, blocks.block_count());\n        let conn = blocks.create(&connector, \"db\").await?;\n        blocks.task_reopen(&connector, conn).await?;\n        assert_block!(blocks \"db\" has 1 Idle);\n        assert_block!(blocks \"db\" has all time 2 Connecting, 1 Disconnecting, 1 Idle, 1 Active, 1 Closed);\n        Ok(())\n    }\n\n    #[test(tokio::test)]\n    async fn test_reopen_fails() -> Result<()> {\n        let connector = BasicConnector::no_delay();\n        let blocks = Blocks::<_, ()>::default();\n        assert_eq!(0, blocks.block_count());\n        let local = LocalSet::new();\n\n        // Success\n        let conn = blocks.create(&connector, \"db\").await?;\n        // This one gets the error\n        let acq1 = blocks.queue(\"db\");\n        // This one doesn't\n        let acq2 = local.spawn_local(blocks.queue(\"db\"));\n\n        assert_block!(blocks \"db\" has 1 Active, 2 Waiting);\n\n        connector.fail_next_connect();\n        blocks\n            .task_reopen(&connector, conn)\n            .await\n            .expect_err(\"Expected a failure\");\n        acq1.await.expect_err(\"Expected a failure\");\n\n        local.spawn_local(blocks.task_create_one(&connector, \"db\"));\n        local.await;\n        _ = acq2.await?;\n\n        assert_block!(blocks \"db\" has 1 Idle);\n        assert_block!(blocks \"db\" has all time \n            3 Connecting, 1 Disconnecting, 2 Idle, 2 Active, 1 Failed, 1 Closed, 2 Waiting);\n        Ok(())\n    }\n\n    #[test(tokio::test)]\n    async fn test_reopen_fails_2() -> Result<()> {\n        let connector = BasicConnector::no_delay();\n        let blocks = Blocks::<_, ()>::default();\n        assert_eq!(0, blocks.block_count());\n        let conn = blocks.create(&connector, \"db\").await?;\n        assert_block!(blocks \"db\" has all time 1 Connecting, 1 Active);\n        connector.fail_next_connect();\n        blocks\n            .task_reopen(&connector, conn)\n            .await\n            .expect_err(\"Expected a failure\");\n        assert_block!(blocks \"db\" has 1 Failed);\n        assert_block!(blocks \"db\" has all time 2 Connecting, 1 Disconnecting, 1 Failed, 1 Active, 1 Closed);\n        Ok(())\n    }\n\n    #[test(tokio::test)]\n    async fn test_discard() -> Result<()> {\n        let connector = BasicConnector::no_delay();\n        let blocks = Blocks::<_, ()>::default();\n        assert_eq!(0, blocks.block_count());\n        let conn = blocks.create(&connector, \"db\").await?;\n        blocks.task_discard(&connector, conn).await?;\n        assert_block!(blocks \"db\" is empty);\n        assert_block!(blocks \"db\" has all time 1 Connecting, 1 Disconnecting, 1 Active, 1 Closed);\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "rust/conn_pool/src/conn.rs",
    "content": "use crate::{\n    block::Name,\n    metrics::{MetricVariant, MetricsAccum},\n    time::Instant,\n    waitqueue::WaitQueue,\n};\nuse futures::FutureExt;\nuse std::{\n    borrow::Cow,\n    cell::{Cell, RefCell},\n    future::Future,\n    pin::Pin,\n    rc::Rc,\n    task::{ready, Poll},\n    time::Duration,\n};\nuse tracing::{error, trace};\n\npub struct ConnState {\n    pub db_name: Name,\n    pub waiters: WaitQueue,\n    pub metrics: Rc<MetricsAccum>,\n}\n\n#[derive(Debug, Clone, thiserror::Error)]\npub enum ConnError<E: Clone> {\n    #[error(\"Shutdown\")]\n    Shutdown,\n    #[error(\"{0}\")]\n    Underlying(E),\n    #[error(\"Task failed, but error was re-routed\")]\n    TaskFailed,\n    #[error(\"{0}\")]\n    Other(Cow<'static, str>),\n}\n\npub type ConnResult<T, E> = Result<T, ConnError<E>>;\n\npub trait Connector: std::fmt::Debug + 'static {\n    /// The type of connection associated with this [`Connector`].\n    type Conn;\n    /// The type of error returned from this [`Connector`]. The error must be\n    /// `Clone`able as it may be returned through multiple channels.\n    type Error: Into<Box<dyn std::error::Error + Send + Sync>> + Clone + std::fmt::Debug;\n\n    /// Perform a connect operation to the given database.\n    fn connect(\n        &self,\n        db: &str,\n    ) -> impl Future<Output = ConnResult<Self::Conn, Self::Error>> + 'static;\n\n    /// Perform a graceful reconnect operation from the existing connection to a new database.\n    fn reconnect(\n        &self,\n        conn: Self::Conn,\n        db: &str,\n    ) -> impl Future<Output = ConnResult<Self::Conn, Self::Error>> + 'static;\n\n    /// Perform a graceful disconnect operation on the given connection.\n    fn disconnect(\n        &self,\n        conn: Self::Conn,\n    ) -> impl Future<Output = ConnResult<(), Self::Error>> + 'static;\n}\n\n#[derive(Debug)]\npub struct Conn<C: Connector> {\n    inner: Rc<RefCell<ConnInner<C>>>,\n}\n\nimpl<C: Connector> PartialEq for Conn<C> {\n    fn eq(&self, other: &Self) -> bool {\n        Rc::ptr_eq(&self.inner, &other.inner)\n    }\n}\n\nimpl<C: Connector> Eq for Conn<C> {}\n\nimpl<C: Connector> Clone for Conn<C> {\n    fn clone(&self) -> Self {\n        Self {\n            inner: self.inner.clone(),\n        }\n    }\n}\n\nimpl<C: Connector> Conn<C> {\n    pub fn new(\n        f: impl Future<Output = ConnResult<C::Conn, C::Error>> + 'static,\n        metrics: &MetricsAccum,\n    ) -> Self {\n        metrics.insert(MetricVariant::Connecting);\n        Self {\n            inner: Rc::new(RefCell::new(ConnInner::Connecting(\n                Instant::now(),\n                f.boxed_local(),\n            ))),\n        }\n    }\n\n    pub fn new_transfer(\n        f: impl Future<Output = ConnResult<C::Conn, C::Error>> + 'static,\n        metrics: &MetricsAccum,\n    ) -> Self {\n        metrics.insert(MetricVariant::Reconnecting);\n        Self {\n            inner: Rc::new(RefCell::new(ConnInner::Reconnecting(\n                Instant::now(),\n                f.boxed_local(),\n            ))),\n        }\n    }\n\n    #[inline(always)]\n    pub fn with_handle<T>(&self, f: impl Fn(&C::Conn) -> T) -> Option<T> {\n        match &*self.inner.borrow() {\n            ConnInner::Active(_, conn, ..) => Some(f(conn)),\n            _ => None,\n        }\n    }\n\n    #[inline]\n    fn transition(&self, f: impl FnOnce(ConnInner<C>) -> ConnInner<C>) {\n        let mut lock = self.inner.borrow_mut();\n        let inner = std::mem::replace(&mut *lock, ConnInner::Transition);\n        *lock = f(inner);\n    }\n\n    pub fn close(&self, connector: &C, metrics: &MetricsAccum) {\n        self.transition(|inner| match inner {\n            ConnInner::Idle(t, conn, ..) => {\n                metrics.transition(\n                    MetricVariant::Idle,\n                    MetricVariant::Disconnecting,\n                    t.elapsed(),\n                );\n                let f = connector.disconnect(conn).boxed_local();\n                ConnInner::Disconnecting(Instant::now(), f)\n            }\n            _ => unreachable!(),\n        });\n    }\n\n    pub fn discard(&self, connector: &C, metrics: &MetricsAccum) {\n        self.transition(|inner| match inner {\n            ConnInner::Active(t, conn, ..) => {\n                metrics.transition(\n                    MetricVariant::Active,\n                    MetricVariant::Disconnecting,\n                    t.elapsed(),\n                );\n                let f = connector.disconnect(conn).boxed_local();\n                ConnInner::Disconnecting(Instant::now(), f)\n            }\n            _ => unreachable!(),\n        });\n    }\n\n    pub fn reopen(&self, connector: &C, metrics: &MetricsAccum, db: &str) {\n        self.transition(|inner| match inner {\n            ConnInner::Active(t, conn) => {\n                metrics.inc_all_time(MetricVariant::Disconnecting);\n                metrics.inc_all_time(MetricVariant::Closed);\n                metrics.transition(\n                    MetricVariant::Active,\n                    MetricVariant::Connecting,\n                    t.elapsed(),\n                );\n                let f = connector.reconnect(conn, db).boxed_local();\n                ConnInner::Connecting(Instant::now(), f)\n            }\n            _ => unreachable!(),\n        });\n    }\n\n    /// Polls a connection that is in a pollable state.\n    ///\n    /// ## Panics\n    ///\n    /// This function panics if the connection is not pollable.\n    pub fn poll_ready(\n        &self,\n        cx: &mut std::task::Context,\n        metrics: &MetricsAccum,\n        to: MetricVariant,\n    ) -> Poll<ConnResult<(), ()>> {\n        let mut lock = self.inner.borrow_mut();\n        debug_assert!(\n            to == MetricVariant::Active || to == MetricVariant::Idle || to == MetricVariant::Closed\n        );\n\n        let res = match &mut *lock {\n            ConnInner::Connecting(t, f) | ConnInner::Reconnecting(t, f) => {\n                match ready!(f.poll_unpin(cx)) {\n                    Ok(c) => {\n                        let elapsed = t.elapsed();\n                        debug_assert!(to == MetricVariant::Active || to == MetricVariant::Idle);\n                        let from = (&std::mem::replace(&mut *lock, ConnInner::Transition)).into();\n                        metrics.transition(from, to, elapsed);\n                        if to == MetricVariant::Active {\n                            *lock = ConnInner::Active(Instant::now(), c);\n                        } else {\n                            *lock = ConnInner::Idle(Instant::now(), c);\n                        }\n                        Ok(())\n                    }\n                    Err(err) => {\n                        let elapsed = t.elapsed();\n                        let from = (&std::mem::replace(&mut *lock, ConnInner::Transition)).into();\n                        metrics.transition(from, MetricVariant::Failed, elapsed);\n                        *lock = ConnInner::Failed(err);\n                        Err(ConnError::TaskFailed)\n                    }\n                }\n            }\n            ConnInner::Disconnecting(t, f) => match ready!(f.poll_unpin(cx)) {\n                Ok(_) => {\n                    debug_assert_eq!(to, MetricVariant::Closed);\n                    metrics.transition(MetricVariant::Disconnecting, to, t.elapsed());\n                    *lock = ConnInner::Closed;\n                    Ok(())\n                }\n                Err(err) => {\n                    metrics.transition(\n                        MetricVariant::Disconnecting,\n                        MetricVariant::Failed,\n                        t.elapsed(),\n                    );\n                    *lock = ConnInner::Failed(err);\n                    Err(ConnError::TaskFailed)\n                }\n            },\n            _ => unreachable!(),\n        };\n        Poll::Ready(res)\n    }\n\n    pub fn try_lock(&self, metrics: &MetricsAccum) -> bool {\n        let mut lock = self.inner.borrow_mut();\n\n        let res: bool;\n        let old = std::mem::replace(&mut *lock, ConnInner::Transition);\n        (*lock, res) = match old {\n            ConnInner::Idle(t, conn) => {\n                metrics.transition(MetricVariant::Idle, MetricVariant::Active, t.elapsed());\n                (ConnInner::Active(Instant::now(), conn), true)\n            }\n            other => (other, false),\n        };\n        res\n    }\n\n    pub fn variant(&self) -> MetricVariant {\n        (&*self.inner.borrow()).into()\n    }\n\n    fn untrack(&self, metrics: &MetricsAccum) {\n        match &*self.inner.borrow() {\n            ConnInner::Active(t, _) | ConnInner::Idle(t, _) => {\n                // If we're transferring, we need to emit virtual disconnecting/closed events\n                metrics.inc_all_time(MetricVariant::Disconnecting);\n                metrics.inc_all_time(MetricVariant::Closed);\n                metrics.remove_time(self.variant(), t.elapsed());\n            }\n            ConnInner::Closed | ConnInner::Failed(..) => {\n                metrics.remove(self.variant());\n            }\n            _ => unreachable!(),\n        }\n    }\n\n    fn age(&self) -> Duration {\n        match &*self.inner.borrow() {\n            ConnInner::Connecting(t, _)\n            | ConnInner::Reconnecting(t, _)\n            | ConnInner::Disconnecting(t, _) => t.elapsed(),\n            ConnInner::Idle(t, _) | ConnInner::Active(t, _) => t.elapsed(),\n            ConnInner::Closed | ConnInner::Failed(..) => Duration::ZERO,\n            ConnInner::Transition => unreachable!(),\n        }\n    }\n}\n\n#[allow(type_alias_bounds)]\ntype ConnFuture<C: Connector, T> = dyn Future<Output = ConnResult<T, <C as Connector>::Error>>;\n\n/// Connection state diagram:\n///\n/// ```text\n///                      v-------------+\n/// S -> Connecting -> Idle -> Active -+\n///                 -> Failed          +-> Disconnecting -> Closed\n/// ```\nenum ConnInner<C: Connector> {\n    /// Connecting connections hold a spot in the pool as they count towards quotas\n    Connecting(Instant, Pin<Box<ConnFuture<C, C::Conn>>>),\n    /// Reconnecting connections hold a spot in the pool as they count towards quotas\n    Reconnecting(Instant, Pin<Box<ConnFuture<C, C::Conn>>>),\n    /// Disconnecting connections hold a spot in the pool as they count towards quotas\n    Disconnecting(Instant, Pin<Box<ConnFuture<C, ()>>>),\n    /// The connection is alive, but it is not being held.\n    Idle(Instant, C::Conn),\n    /// The connection is alive, and is being held.\n    Active(Instant, C::Conn),\n    /// The connection is in a failed state.\n    Failed(ConnError<C::Error>),\n    /// The connection is in a closed state.\n    Closed,\n    /// Transitioning between states. Used internally, never escapes an internal\n    /// function.\n    Transition,\n}\n\nimpl<C: Connector> From<&ConnInner<C>> for MetricVariant {\n    fn from(val: &ConnInner<C>) -> Self {\n        match val {\n            ConnInner::Connecting(..) => MetricVariant::Connecting,\n            ConnInner::Reconnecting(..) => MetricVariant::Reconnecting,\n            ConnInner::Disconnecting(..) => MetricVariant::Disconnecting,\n            ConnInner::Idle(..) => MetricVariant::Idle,\n            ConnInner::Active(..) => MetricVariant::Active,\n            ConnInner::Failed(..) => MetricVariant::Failed,\n            ConnInner::Closed => MetricVariant::Closed,\n            ConnInner::Transition => unreachable!(),\n        }\n    }\n}\n\nimpl<C: Connector> std::fmt::Debug for ConnInner<C> {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.write_fmt(format_args!(\"ConnInner({:?})\", MetricVariant::from(self)))\n    }\n}\n\npub struct ConnHandle<C: Connector> {\n    pub(crate) conn: Conn<C>,\n    pub(crate) state: Rc<ConnState>,\n    pub(crate) dropped: Cell<bool>,\n}\n\nimpl<C: Connector> std::fmt::Debug for ConnHandle<C> {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.write_fmt(format_args!(\n            \"Connection({:?}, {:?})\",\n            self.state.db_name,\n            self.conn.variant()\n        ))\n    }\n}\n\nimpl<C: Connector> ConnHandle<C> {\n    pub fn new(conn: Conn<C>, state: Rc<ConnState>) -> Self {\n        Self {\n            conn,\n            state,\n            dropped: Cell::default(),\n        }\n    }\n\n    pub(crate) fn into_inner(mut self) -> Conn<C> {\n        self.dropped.set(true);\n        std::mem::replace(\n            &mut self.conn,\n            Conn {\n                inner: Rc::new(RefCell::new(ConnInner::Closed)),\n            },\n        )\n    }\n}\n\nimpl<C: Connector> Drop for ConnHandle<C> {\n    fn drop(&mut self) {\n        if self.dropped.get() {\n            return;\n        }\n        self.conn.transition(|inner| match inner {\n            ConnInner::Active(t, c) => {\n                self.state.metrics.transition(\n                    MetricVariant::Active,\n                    MetricVariant::Idle,\n                    t.elapsed(),\n                );\n                self.state.waiters.trigger();\n                ConnInner::Idle(Instant::now(), c)\n            }\n            _ => {\n                unreachable!(\"Impossible state: {:?}\", MetricVariant::from(&inner));\n            }\n        });\n    }\n}\n\n/// Maintains a list of connections. Tries to provide idle connections for use\n/// in a MRU mode, and for release in a LRU mode where possible.\n#[derive(Debug)]\npub struct Conns<C: Connector> {\n    conns: RefCell<Vec<Conn<C>>>,\n    youngest: Cell<Instant>,\n}\n\nimpl<C: Connector> Default for Conns<C> {\n    fn default() -> Self {\n        Self {\n            conns: vec![].into(),\n            youngest: Cell::new(Instant::now()),\n        }\n    }\n}\n\nimpl<C: Connector> Conns<C> {\n    #[inline]\n    pub fn len(&self) -> usize {\n        self.conns.borrow().len()\n    }\n\n    #[inline]\n    pub fn youngest(&self) -> Duration {\n        self.youngest.get().elapsed()\n    }\n\n    #[inline]\n    pub fn walk(&self, mut f: impl FnMut(&Conn<C>)) {\n        for conn in self.conns.borrow().iter() {\n            f(conn)\n        }\n    }\n\n    pub fn count_older(&self, metric: MetricVariant, than: Duration) -> usize {\n        let mut count = 0;\n        for conn in self.conns.borrow().iter() {\n            if conn.variant() == metric && conn.age() > than {\n                count += 1;\n            }\n        }\n        count\n    }\n\n    /// Insert a new connection, in the MRU spot.\n    pub fn insert(&self, conn: Conn<C>) {\n        self.conns.borrow_mut().push(conn);\n        self.youngest.set(Instant::now());\n    }\n\n    /// Remove a specific connection from the list. This may break MRU ordering\n    /// for performance reasons.\n    pub fn remove(\n        &self,\n        conn: Conn<C>,\n        metrics: &MetricsAccum,\n    ) -> ConnResult<Option<C::Conn>, C::Error> {\n        conn.untrack(metrics);\n\n        // Find the connection and remove it\n        let mut lock = self.conns.borrow_mut();\n        let index = lock\n            .iter()\n            .position(|other| &conn == other)\n            .expect(\"Connection unexpectedly could not be found\");\n        lock.swap_remove(index);\n\n        // We know that a removed connection cannot have a handle active, so the\n        // `Rc` unwrap will always succeed.\n        debug_assert_eq!(Rc::strong_count(&conn.inner), 1);\n\n        match Rc::into_inner(conn.inner).unwrap().into_inner() {\n            ConnInner::Active(_, conn) | ConnInner::Idle(_, conn) => Ok(Some(conn)),\n            ConnInner::Closed => Ok(None),\n            ConnInner::Failed(err) => Err(err),\n            _ => unreachable!(),\n        }\n    }\n\n    /// Acquires the most-recently-used idle connection, moving it to the end of\n    /// the internal vector. If no connections are available, polls a pending error.\n    pub fn try_acquire_idle_mru(\n        &self,\n        metrics: &MetricsAccum,\n    ) -> Option<ConnResult<Conn<C>, C::Error>> {\n        let res = self.try_get_idle(metrics, true);\n        if let Some(Ok(conn)) = &res {\n            if !conn.try_lock(metrics) {\n                panic!(\"Connection unexpectedly could not be locked\")\n            }\n        }\n        res\n    }\n\n    /// Gets the most-recently-used idle connection, moving it to the end of\n    /// the internal vector. If no connections are available, polls a pending error.\n    pub fn try_get_idle(\n        &self,\n        metrics: &MetricsAccum,\n        mru: bool,\n    ) -> Option<ConnResult<Conn<C>, C::Error>> {\n        let mut lock = self.conns.borrow_mut();\n        if lock.is_empty() {\n            None\n        } else {\n            let last_item = lock.len() - 1;\n            let range: &mut dyn Iterator<Item = usize> = if mru {\n                &mut (0..lock.len()).rev()\n            } else {\n                &mut (0..lock.len())\n            };\n            for pos in range {\n                return match lock[pos].variant() {\n                    MetricVariant::Idle => {\n                        trace!(\"Got a connection mru={mru}\");\n                        lock.swap(last_item, pos);\n                        let conn = lock[last_item].clone();\n                        Some(Ok(conn))\n                    }\n                    MetricVariant::Failed => {\n                        trace!(\"Got an error\");\n                        let conn = lock.swap_remove(pos);\n                        conn.untrack(metrics);\n\n                        // We know that a removed connection cannot have a handle active, so the\n                        // `Rc` unwrap will always succeed.\n                        debug_assert_eq!(Rc::strong_count(&conn.inner), 1);\n\n                        match Rc::into_inner(conn.inner).unwrap().into_inner() {\n                            ConnInner::Failed(err) => Some(Err(err)),\n                            _ => unreachable!(),\n                        }\n                    }\n                    _ => continue,\n                };\n            }\n            None\n        }\n    }\n\n    /// Takes the least-recently-used idle connection, does not re-order the\n    /// underlying list.\n    pub fn try_take_idle_lru(&self, metrics: &MetricsAccum) -> Option<C::Conn> {\n        let mut lock = self.conns.borrow_mut();\n        let index = lock\n            .iter()\n            .position(|conn| conn.variant() == MetricVariant::Idle)?;\n\n        let conn = lock.swap_remove(index);\n        conn.untrack(metrics);\n\n        // We know that a removed connection cannot have a handle active, so the\n        // `Rc` unwrap will always succeed.\n        debug_assert_eq!(Rc::strong_count(&conn.inner), 1);\n\n        match Rc::into_inner(conn.inner).unwrap().into_inner() {\n            ConnInner::Idle(_, conn) => Some(conn),\n            _ => unreachable!(),\n        }\n    }\n}\n"
  },
  {
    "path": "rust/conn_pool/src/drain.rs",
    "content": "use crate::block::Name;\nuse std::{\n    cell::{Cell, RefCell},\n    collections::HashMap,\n};\n\n/// Holds the current drainage and shutdown state for the `Pool`.\n#[derive(Default, Debug)]\npub struct Drain {\n    drain_all: Cell<usize>,\n    drain: RefCell<HashMap<Name, usize>>,\n    shutdown: Cell<bool>,\n}\n\nimpl Drain {\n    pub fn shutdown(&self) {\n        self.shutdown.set(true)\n    }\n\n    pub fn in_shutdown(&self) -> bool {\n        self.shutdown.get()\n    }\n\n    /// Lock all connections for draining.\n    pub fn lock_all<T: AsRef<Drain>>(this: T) -> DrainLock<T> {\n        let drain = this.as_ref();\n        drain.drain_all.set(drain.drain_all.get() + 1);\n        DrainLock {\n            db: None,\n            has_drain: this,\n        }\n    }\n\n    // Lock a specific connection for draining.\n    pub fn lock<T: AsRef<Drain>>(this: T, db: Name) -> DrainLock<T> {\n        {\n            let mut drain = this.as_ref().drain.borrow_mut();\n            drain.entry(db.clone()).and_modify(|v| *v += 1).or_default();\n        }\n        DrainLock {\n            db: Some(db),\n            has_drain: this,\n        }\n    }\n\n    /// Is this connection draining?\n    pub fn is_draining(&self, db: &str) -> bool {\n        self.drain_all.get() > 0 || self.drain.borrow().contains_key(db) || self.shutdown.get()\n    }\n\n    /// Are any connections draining?\n    pub fn are_any_draining(&self) -> bool {\n        !self.drain.borrow().is_empty() || self.shutdown.get() || self.drain_all.get() > 0\n    }\n}\n\n/// Provides a RAII lock for a db- or whole-pool drain operation.\npub struct DrainLock<T: AsRef<Drain>> {\n    db: Option<Name>,\n    has_drain: T,\n}\n\nimpl<T: AsRef<Drain>> Drop for DrainLock<T> {\n    fn drop(&mut self) {\n        if let Some(name) = self.db.take() {\n            let mut drain = self.has_drain.as_ref().drain.borrow_mut();\n            if let Some(count) = drain.get_mut(&name) {\n                if *count >= 1 {\n                    *count -= 1;\n                } else {\n                    drain.remove(&name);\n                }\n            } else {\n                unreachable!()\n            }\n        } else {\n            let this = self.has_drain.as_ref();\n            this.drain_all.set(this.drain_all.get() - 1);\n        }\n    }\n}\n\nimpl AsRef<Drain> for Drain {\n    fn as_ref(&self) -> &Drain {\n        self\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn drain_lock_all() {\n        let drain = Drain::default();\n        let l1 = Drain::lock_all(&drain);\n        assert!(drain.are_any_draining());\n        drop(l1);\n        assert!(!drain.are_any_draining());\n    }\n\n    #[test]\n    fn drain_lock_db_one() {\n        let drain = Drain::default();\n        assert!(!drain.are_any_draining());\n        let l1 = Drain::lock(&drain, \"db\".into());\n        assert!(drain.are_any_draining());\n        drop(l1);\n        assert!(!drain.are_any_draining());\n    }\n\n    #[test]\n    fn drain_lock_db_two() {\n        let drain = Drain::default();\n        let l1 = Drain::lock(&drain, \"db\".into());\n        assert!(drain.are_any_draining());\n        let l2 = Drain::lock(&drain, \"db\".into());\n        assert!(drain.are_any_draining());\n        drop((l1, l2));\n        assert!(!drain.are_any_draining());\n    }\n\n    #[test]\n    fn drain_lock_db_mixed_one() {\n        let drain = Drain::default();\n        let l1 = Drain::lock(&drain, \"db\".into());\n        let l2 = Drain::lock(&drain, \"db1\".into());\n        drop((l1, l2));\n    }\n\n    #[test]\n    fn drain_lock_db_mixed_two() {\n        let drain = Drain::default();\n        let l1 = Drain::lock(&drain, \"db\".into());\n        let l2 = Drain::lock(&drain, \"db1\".into());\n        let l3 = Drain::lock(&drain, \"db1\".into());\n        drop((l1, l2, l3));\n    }\n}\n"
  },
  {
    "path": "rust/conn_pool/src/lib.rs",
    "content": "pub(crate) mod algo;\npub(crate) mod block;\npub(crate) mod conn;\npub(crate) mod drain;\npub(crate) mod metrics;\npub(crate) mod pool;\npub(crate) mod waitqueue;\n\nmod time {\n    #[cfg(not(any(test, feature = \"optimizer\")))]\n    pub use std::time::Instant;\n    #[cfg(any(test, feature = \"optimizer\"))]\n    pub use tokio::time::Instant;\n}\n\n#[cfg(feature = \"optimizer\")]\npub use algo::knobs;\n\n// Public interface\n\npub use conn::Connector;\npub use pool::{Pool, PoolConfig, PoolHandle};\n\n#[cfg(any(test, feature = \"optimizer\"))]\npub mod test;\n\n#[cfg(feature = \"python_extension\")]\npub mod python;\n"
  },
  {
    "path": "rust/conn_pool/src/metrics.rs",
    "content": "use serde::{Serialize, Serializer};\nuse std::collections::BTreeMap;\nuse std::{cell::RefCell, rc::Rc, time::Duration};\nuse strum::EnumCount;\nuse strum::IntoEnumIterator;\n\nuse crate::algo::PoolAlgorithmDataMetrics;\nuse crate::block::Name;\n\n#[derive(\n    Clone, Copy, Debug, PartialEq, Eq, Hash, strum::EnumCount, strum::EnumIter, strum::AsRefStr,\n)]\npub enum MetricVariant {\n    Connecting,\n    Disconnecting,\n    Reconnecting,\n    Idle,\n    Active,\n    Failed,\n    Closed,\n    Waiting,\n}\n\n/// Maintains a rolling average of `u32` values. Note that this struct attempts\n/// to optimize `SIZE == 1`.\n#[derive(Debug, PartialEq, Eq)]\npub struct RollingAverageU32<const SIZE: usize> {\n    values: [u32; SIZE],\n    cumulative: u64,\n    ptr: u8,\n    /// If we've never rolled over, we cannot divide the entire array by `SIZE`\n    /// and have to use `ptr` instead.\n    rollover: bool,\n}\n\nimpl<const SIZE: usize> Default for RollingAverageU32<SIZE> {\n    fn default() -> Self {\n        assert!(SIZE <= u8::MAX as _);\n        Self {\n            values: [0; SIZE],\n            ptr: 0,\n            cumulative: 0,\n            rollover: false,\n        }\n    }\n}\n\nimpl<const SIZE: usize> RollingAverageU32<SIZE> {\n    pub fn accum(&mut self, new: u32) {\n        if SIZE == 1 {\n            self.values[0] = new;\n        } else {\n            let size = SIZE as u8;\n            let old = std::mem::replace(&mut self.values[self.ptr as usize], new);\n            self.cumulative -= old as u64;\n            self.cumulative += new as u64;\n            self.ptr = (self.ptr + 1) % size;\n            if self.ptr == 0 {\n                self.rollover = true;\n            }\n        }\n    }\n\n    #[inline]\n    pub fn avg(&self) -> u32 {\n        if SIZE == 1 {\n            self.values[0]\n        } else if self.rollover {\n            (self.cumulative / SIZE as u64) as u32\n        } else if self.ptr == 0 {\n            0\n        } else {\n            (self.cumulative / self.ptr as u64) as u32\n        }\n    }\n}\n\n#[derive(Debug, Default, Serialize)]\npub struct PoolMetrics {\n    pub pool: ConnMetrics,\n    pub all_time: VariantArray<usize>,\n    pub blocks: BTreeMap<Name, ConnMetrics>,\n}\n\n/// An array indexed by [`MetricVariant`].\n#[derive(Default, Clone, Copy, PartialEq, Eq)]\npub struct VariantArray<T>([T; MetricVariant::COUNT]);\n\nimpl<T> Serialize for VariantArray<T>\nwhere\n    T: Serialize,\n{\n    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n    where\n        S: Serializer,\n    {\n        self.0.serialize(serializer)\n    }\n}\n\nimpl<T> std::ops::Index<MetricVariant> for VariantArray<T> {\n    type Output = T;\n    fn index(&self, index: MetricVariant) -> &Self::Output {\n        &self.0[index as usize]\n    }\n}\n\nimpl<T> std::ops::IndexMut<MetricVariant> for VariantArray<T> {\n    fn index_mut(&mut self, index: MetricVariant) -> &mut Self::Output {\n        &mut self.0[index as usize]\n    }\n}\n\nimpl<T: Copy + std::ops::AddAssign> std::ops::Add for VariantArray<T> {\n    type Output = VariantArray<T>;\n    fn add(self, rhs: Self) -> Self::Output {\n        let mut out = self;\n        for i in MetricVariant::iter() {\n            out[i] += rhs[i];\n        }\n        out\n    }\n}\n\nimpl<T: Copy + std::ops::AddAssign> std::ops::AddAssign for VariantArray<T> {\n    fn add_assign(&mut self, rhs: Self) {\n        for i in MetricVariant::iter() {\n            self[i] += rhs[i];\n        }\n    }\n}\n\nimpl<T: Default + Copy + std::ops::AddAssign> std::iter::Sum for VariantArray<T> {\n    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {\n        let mut sum = Default::default();\n        for i in iter {\n            sum += i;\n        }\n        sum\n    }\n}\n\nimpl<T: std::fmt::Debug + std::cmp::Eq + Default> std::fmt::Debug for VariantArray<T> {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let mut d = f.debug_struct(\"\");\n        for variant in MetricVariant::iter() {\n            if self[variant] != T::default() {\n                d.field(variant.as_ref(), &self[variant]);\n            }\n        }\n        d.finish()\n    }\n}\n\nimpl<T: Copy + Default> VariantArray<T> {\n    #[cfg(test)]\n    pub fn with(variant: MetricVariant, count: T) -> Self {\n        let mut summary = Self::default();\n        summary[variant] = count;\n        summary\n    }\n}\n\n#[derive(Default, Serialize)]\n#[allow(unused)]\npub struct ConnMetrics {\n    pub(crate) value: VariantArray<usize>,\n    pub(crate) all_time: VariantArray<usize>,\n    pub(crate) max: VariantArray<usize>,\n    pub(crate) avg_time: VariantArray<u32>,\n    pub(crate) total: usize,\n    pub(crate) total_max: usize,\n    pub(crate) target: usize,\n}\n\nimpl std::fmt::Debug for ConnMetrics {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.write_str(\"ConnMetrics {\\n\")?;\n        for variant in MetricVariant::iter() {\n            f.write_fmt(format_args!(\n                \"    {variant:?}: {} (max={}, avg={}ms)\\n\",\n                self.value[variant], self.max[variant], self.avg_time[variant]\n            ))?;\n        }\n        f.write_str(\"}\")?;\n        Ok(())\n    }\n}\n\n#[derive(Debug, Default)]\nstruct RawMetrics {\n    /// The total number of non-waiting connections.\n    total: usize,\n    /// The max total number of non-waiting connections.\n    total_max: usize,\n    /// The number of connections per state.\n    counts: VariantArray<usize>,\n    /// The total number of transitions into this state for all time.\n    all_time: VariantArray<usize>,\n    /// The max number of connections per state.\n    max: VariantArray<usize>,\n    /// The time spent in each state.\n    times: VariantArray<RollingAverageU32<32>>,\n}\n\nimpl RawMetrics {\n    #[inline(always)]\n    fn reset_max(&mut self) {\n        self.max = self.counts;\n        self.total_max = self.total;\n    }\n\n    #[inline(always)]\n    fn inc_all_time(&mut self, to: MetricVariant) {\n        self.all_time[to] += 1;\n    }\n\n    #[inline(always)]\n    fn inc(&mut self, to: MetricVariant) {\n        self.counts[to] += 1;\n        self.max[to] = self.max[to].max(self.counts[to]);\n        self.inc_all_time(to)\n    }\n\n    #[inline(always)]\n    fn inc_total(&mut self, to: MetricVariant) {\n        if to != MetricVariant::Waiting {\n            self.total += 1;\n            self.total_max = self.total_max.max(self.total);\n        }\n    }\n\n    #[inline(always)]\n    fn dec(&mut self, from: MetricVariant) {\n        self.counts[from] -= 1;\n    }\n\n    #[inline(always)]\n    fn time(&mut self, from: MetricVariant, time: Duration) {\n        self.times[from].accum(time.as_millis() as _);\n    }\n\n    #[inline(always)]\n    fn dec_total(&mut self, from: MetricVariant) {\n        if from != MetricVariant::Waiting {\n            self.total -= 1;\n        }\n    }\n}\n\n/// Metrics accumulator. Designed to be updated without a lock.\n#[derive(Debug, Default)]\npub struct MetricsAccum {\n    raw: RefCell<RawMetrics>,\n    parent: Option<Rc<MetricsAccum>>,\n}\n\nimpl PoolAlgorithmDataMetrics for MetricsAccum {\n    #[inline(always)]\n    fn avg_ms(&self, variant: MetricVariant) -> usize {\n        self.raw.borrow().times[variant].avg() as _\n    }\n    #[inline(always)]\n    fn count(&self, variant: MetricVariant) -> usize {\n        self.raw.borrow().counts[variant]\n    }\n    #[inline(always)]\n    fn max(&self, variant: MetricVariant) -> usize {\n        self.raw.borrow().max[variant]\n    }\n    #[inline(always)]\n    fn total(&self) -> usize {\n        self.raw.borrow().total\n    }\n    #[inline(always)]\n    fn total_max(&self) -> usize {\n        self.raw.borrow().total_max\n    }\n}\n\nimpl MetricsAccum {\n    pub fn new(parent: Option<Rc<MetricsAccum>>) -> Self {\n        Self {\n            parent,\n            ..Default::default()\n        }\n    }\n\n    /// Get the current total\n    #[inline(always)]\n    pub fn total(&self) -> usize {\n        self.raw.borrow().total\n    }\n\n    /// Get the current value of a variant\n    #[inline(always)]\n    pub fn get(&self, variant: MetricVariant) -> usize {\n        self.raw.borrow().counts[variant]\n    }\n\n    /// Sums the values of all the given variants.\n    #[inline(always)]\n    pub fn sum_all(&self, variants: &[MetricVariant]) -> usize {\n        let mut sum = 0;\n        let lock = self.raw.borrow();\n        for variant in variants {\n            sum += lock.counts[*variant];\n        }\n        sum\n    }\n\n    /// Returns true if there is a non-zero count for any of the variants.\n    #[inline(always)]\n    pub fn has_any(&self, variants: &[MetricVariant]) -> bool {\n        let lock = self.raw.borrow();\n        for variant in variants {\n            if lock.counts[*variant] > 0 {\n                return true;\n            }\n        }\n        false\n    }\n\n    #[inline(always)]\n    pub fn reset_max(&self) {\n        self.raw.borrow_mut().reset_max();\n    }\n\n    pub fn summary(&self) -> ConnMetrics {\n        let lock = self.raw.borrow_mut();\n        let mut avg_time = VariantArray::default();\n        for i in MetricVariant::iter() {\n            avg_time[i] = lock.times[i].avg();\n        }\n        ConnMetrics {\n            value: lock.counts,\n            all_time: lock.all_time,\n            max: lock.max,\n            avg_time,\n            total: lock.total,\n            total_max: lock.total_max,\n            target: 0,\n        }\n    }\n\n    pub fn counts(&self) -> VariantArray<usize> {\n        self.raw.borrow().counts\n    }\n\n    pub fn all_time(&self) -> VariantArray<usize> {\n        self.raw.borrow().all_time\n    }\n\n    #[inline]\n    pub fn inc_all_time(&self, to: MetricVariant) {\n        let mut lock = self.raw.borrow_mut();\n        lock.inc_all_time(to);\n        if let Some(parent) = &self.parent {\n            parent.inc_all_time(to);\n        }\n    }\n\n    #[inline]\n    pub fn insert(&self, to: MetricVariant) {\n        let mut lock = self.raw.borrow_mut();\n        lock.inc(to);\n        lock.inc_total(to);\n        // trace!(\"None->{to:?} ({})\", lock[to ]);\n        if let Some(parent) = &self.parent {\n            parent.insert(to);\n        }\n    }\n\n    #[inline]\n    pub fn set_value(&self, to: MetricVariant, len: usize) {\n        let mut lock = self.raw.borrow_mut();\n        debug_assert_eq!(lock.counts[to], 0);\n        lock.counts[to] = len;\n        lock.total += len;\n        lock.max[to] = lock.max[to].max(lock.counts[to]);\n    }\n\n    #[inline]\n    pub fn transition(&self, from: MetricVariant, to: MetricVariant, time: Duration) {\n        // trace!(\"{from:?}->{to:?}: {time:?}\");\n        let mut lock = self.raw.borrow_mut();\n        lock.dec(from);\n        lock.time(from, time);\n        lock.inc(to);\n        if let Some(parent) = &self.parent {\n            parent.transition(from, to, time);\n        }\n    }\n\n    #[inline]\n    pub fn remove_time(&self, from: MetricVariant, time: Duration) {\n        let mut lock = self.raw.borrow_mut();\n        lock.dec(from);\n        lock.time(from, time);\n        lock.dec_total(from);\n        // trace!(\"{from:?}->None ({time:?})\");\n        if let Some(parent) = &self.parent {\n            parent.remove_time(from, time);\n        }\n    }\n\n    #[inline]\n    pub fn remove(&self, from: MetricVariant) {\n        let mut lock = self.raw.borrow_mut();\n        lock.dec(from);\n        lock.dec_total(from);\n        // trace!(\"{from:?}->None\");\n        if let Some(parent) = &self.parent {\n            parent.remove(from);\n        }\n    }\n}\n"
  },
  {
    "path": "rust/conn_pool/src/pool.rs",
    "content": "use crate::{\n    algo::{\n        AcquireOp, AlgoState, PoolAlgoTargetData, PoolAlgorithmDataMetrics, PoolConstraints,\n        RebalanceOp, ReleaseOp, ReleaseType,\n    },\n    block::Blocks,\n    conn::{ConnError, ConnHandle, ConnResult, Connector},\n    drain::Drain,\n    metrics::{MetricVariant, PoolMetrics},\n    time::Instant,\n};\nuse consume_on_drop::{Consume, ConsumeOnDrop};\nuse derive_more::Debug;\nuse std::{cell::Cell, rc::Rc, time::Duration};\nuse tracing::trace;\n\n#[derive(Debug)]\npub struct PoolConfig {\n    pub constraints: PoolConstraints,\n    pub adjustment_interval: Duration,\n    pub gc_interval: Duration,\n}\n\nimpl PoolConfig {\n    pub fn assert_valid(&self) {\n        assert!(self.constraints.max > 0);\n    }\n\n    /// Generate suggested default configurations for the expected number of connections with an\n    /// unknown number of databases.\n    pub fn suggested_default_for(connections: usize) -> Self {\n        Self::suggested_default_for_databases(connections, usize::MAX)\n    }\n\n    /// Generate suggested default configurations for the expected number of connections and databases.\n    pub fn suggested_default_for_databases(connections: usize, databases: usize) -> Self {\n        assert!(connections > 0);\n        assert!(databases > 0);\n        Self {\n            adjustment_interval: Duration::from_millis(10),\n            gc_interval: Duration::from_secs(1),\n            constraints: PoolConstraints {\n                max: connections,\n                min_idle_time_for_gc: Duration::from_secs(120),\n            },\n        }\n    }\n\n    pub fn with_min_idle_time_for_gc(mut self, min_idle_time_for_gc: Duration) -> Self {\n        self.constraints.min_idle_time_for_gc = min_idle_time_for_gc;\n        self.gc_interval = (min_idle_time_for_gc / 120).max(Duration::from_secs_f64(0.5));\n        self\n    }\n}\n\nstruct HandleAndPool<C: Connector>(ConnHandle<C>, Rc<Pool<C>>, Cell<bool>);\n\n/// An opaque handle representing a RAII lock on a connection in the pool. The\n/// underlying connection object may be\npub struct PoolHandle<C: Connector> {\n    conn: ConsumeOnDrop<HandleAndPool<C>>,\n}\n\nimpl<C: Connector> std::fmt::Debug for PoolHandle<C> {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        self.conn.0.fmt(f)\n    }\n}\n\nimpl<C: Connector> Consume for HandleAndPool<C> {\n    fn consume(self) {\n        self.1.release(self.0, self.2.get())\n    }\n}\n\nimpl<C: Connector> PoolHandle<C> {\n    /// Marks this handle as poisoned, which will not allow it to be reused in the pool. The\n    /// most likely case for this is that the underlying connection's stream has closed, or\n    /// the remote end is no longer valid for some reason.\n    pub fn poison(&self) {\n        self.conn.2.set(true)\n    }\n\n    /// Use this pool's handle temporarily.\n    #[inline(always)]\n    pub fn with_handle<T>(&self, f: impl Fn(&C::Conn) -> T) -> T {\n        self.conn.0.conn.with_handle(f).unwrap()\n    }\n\n    fn new(conn: ConnHandle<C>, pool: Rc<Pool<C>>) -> Self {\n        Self {\n            conn: ConsumeOnDrop::new(HandleAndPool(conn, pool, Cell::default())),\n        }\n    }\n}\n\nimpl<C: Connector> PoolHandle<C>\nwhere\n    C::Conn: Copy,\n{\n    /// If the handle is `Copy`, copies this handle.\n    #[inline(always)]\n    pub fn handle(&self) -> C::Conn {\n        self.conn.0.conn.with_handle(|c| *c).unwrap()\n    }\n}\n\nimpl<C: Connector> PoolHandle<C>\nwhere\n    C::Conn: Clone,\n{\n    /// If the handle is `Clone`, clones this handle.\n    #[inline(always)]\n    pub fn handle_clone(&self) -> C::Conn {\n        self.conn.0.conn.with_handle(|c| c.clone()).unwrap()\n    }\n}\n\n#[derive(derive_more::Debug)]\n/// A connection pool consists of a number of blocks, each with a target\n/// connection count (aka a quota). Generally, a block may take up to its quota,\n/// but no more, though the pool operating conditions may allow for this to vary\n/// for optimal allocation of the limited connections. If a block is over quota,\n/// one of its connections may be stolen to satisfy another block's needs.\npub struct Pool<C: Connector> {\n    connector: C,\n    pub(crate) config: PoolConfig,\n    blocks: Blocks<C, PoolAlgoTargetData>,\n    drain: Drain,\n    /// If the pool has been dirtied by acquiring or releasing a connection\n    dirty: Rc<Cell<bool>>,\n    last_gc: Cell<Instant>,\n}\n\nimpl<C: Connector> Pool<C> {\n    pub fn new(config: PoolConfig, connector: C) -> Rc<Self> {\n        config.assert_valid();\n        Rc::new(Self {\n            config,\n            blocks: Default::default(),\n            connector,\n            dirty: Default::default(),\n            drain: Drain::default(),\n            last_gc: Instant::now().into(),\n        })\n    }\n}\n\nimpl<C: Connector> Pool<C> {\n    fn algo(&self) -> AlgoState<'_, Blocks<C, PoolAlgoTargetData>> {\n        AlgoState {\n            drain: &self.drain,\n            blocks: &self.blocks,\n            constraints: &self.config.constraints,\n        }\n    }\n\n    /// Runs the required async task that takes care of quota management, garbage collection,\n    /// and other important async tasks. This should happen only if something has changed in\n    /// the pool.\n    pub async fn run(&self) {\n        loop {\n            tokio::time::sleep(self.config.adjustment_interval).await;\n            self.run_once();\n        }\n    }\n\n    /// Runs the required async task that takes care of quota management, garbage collection,\n    /// and other important async tasks. This should happen only if we have live blocks.\n    pub fn run_once(&self) {\n        // No need to run if we have no blocks\n        if self.blocks.is_empty() {\n            return;\n        }\n\n        self.algo().adjust();\n\n        // Run a garbage collection if we're due\n        let since_last_gc = self.last_gc.get().elapsed();\n        let gc = if since_last_gc > self.config.gc_interval {\n            trace!(\n                \"GC triggered: time since last GC = {since_last_gc:?} > {:?}\",\n                self.config.gc_interval\n            );\n            self.last_gc.set(Instant::now());\n            true\n        } else {\n            false\n        };\n\n        for op in self.algo().plan_rebalance(gc) {\n            trace!(\"Rebalance: {op:?}\");\n            match op {\n                RebalanceOp::Transfer { from, to } => {\n                    tokio::task::spawn_local(self.blocks.task_steal(&self.connector, &to, &from));\n                }\n                RebalanceOp::Create(name) => {\n                    tokio::task::spawn_local(self.blocks.task_create_one(&self.connector, &name));\n                }\n                RebalanceOp::Close(name) => {\n                    tokio::task::spawn_local(self.blocks.task_close_one(&self.connector, &name));\n                }\n            }\n        }\n    }\n\n    /// Acquire a handle from this connection pool. The returned [`PoolHandle`]\n    /// controls the lock for the connection and may be dropped to release it\n    /// back into the pool.\n    pub async fn acquire(self: &Rc<Self>, db: &str) -> ConnResult<PoolHandle<C>, C::Error> {\n        self.dirty.set(true);\n        let plan = self.algo().plan_acquire(db);\n        trace!(\"Acquire {db}: {plan:?}\");\n        match plan {\n            AcquireOp::Create => {\n                tokio::task::spawn_local(self.blocks.task_create_one(&self.connector, db));\n            }\n            AcquireOp::Steal(from) => {\n                tokio::task::spawn_local(self.blocks.task_steal(&self.connector, db, &from));\n            }\n            AcquireOp::Wait => {}\n            AcquireOp::FailInShutdown => {\n                return Err(ConnError::Shutdown);\n            }\n        };\n        let conn = self.blocks.queue(db).await?;\n\n        Ok(PoolHandle::new(conn, self.clone()))\n    }\n\n    /// Internal release method\n    fn release(self: Rc<Self>, conn: ConnHandle<C>, poison: bool) {\n        let db = &conn.state.db_name;\n        self.dirty.set(true);\n        let release_type = if poison {\n            ReleaseType::Poison\n        } else {\n            ReleaseType::Normal\n        };\n        let plan = self.algo().plan_release(db, release_type);\n        trace!(\"Release: {conn:?} {plan:?}\");\n        match plan {\n            ReleaseOp::Release => {}\n            ReleaseOp::Discard => {\n                tokio::task::spawn_local(self.blocks.task_discard(&self.connector, conn));\n            }\n            ReleaseOp::ReleaseTo(db) => {\n                tokio::task::spawn_local(self.blocks.task_move_to(&self.connector, conn, &db));\n            }\n            ReleaseOp::Reopen => {\n                tokio::task::spawn_local(self.blocks.task_reopen(&self.connector, conn));\n            }\n        }\n    }\n\n    /// Retrieve the current pool metrics snapshot.\n    pub fn metrics(&self) -> PoolMetrics {\n        self.blocks.summary()\n    }\n\n    /// Is this pool idle?\n    pub fn idle(&self) -> bool {\n        self.blocks.is_empty()\n    }\n\n    /// Drain all connections to the given database. All connections will be\n    /// poisoned on return and this method will return when the given database\n    /// is idle. Multiple calls to this method with the same database are valid,\n    /// and the drain operation will be kept alive as long as one future has not\n    /// been dropped.\n    ///\n    /// It is valid, though unadvisable, to request a connection during this\n    /// period. The connection will be poisoned on return as well.\n    ///\n    /// Dropping this future cancels the drain operation.\n    pub async fn drain(self: Rc<Self>, db: &str) {\n        // If the block doesn't exist, we can return\n        let Some(name) = self.blocks.name(db) else {\n            return;\n        };\n\n        let lock = Drain::lock(self.clone(), name);\n        while self.blocks.metrics(db).total() > 0 {\n            tokio::time::sleep(Duration::from_millis(10)).await;\n        }\n        drop(lock);\n    }\n\n    /// Drain all idle connections to the given database. All connections will be\n    /// poisoned on return and this method will return when the given database\n    /// is idle. Multiple calls to this method with the same database are valid,\n    /// and the drain operation will be kept alive as long as one future has not\n    /// been dropped.\n    ///\n    /// It is valid, though unadvisable, to request a connection during this\n    /// period. The connection will be poisoned on return as well.\n    ///\n    /// Dropping this future cancels the drain operation.\n    pub async fn drain_idle(self: Rc<Self>, db: &str) {\n        // If the block doesn't exist, we can return\n        let Some(name) = self.blocks.name(db) else {\n            return;\n        };\n\n        let lock = Drain::lock(self.clone(), name);\n        while self.blocks.metrics(db).get(MetricVariant::Idle) > 0 {\n            tokio::time::sleep(Duration::from_millis(10)).await;\n        }\n        drop(lock);\n    }\n\n    /// Drain all connections in the pool, returning when the pool is completely\n    /// empty. Multiple calls to this method with the same database are valid,\n    /// and the drain operation will be kept alive as long as one future has not\n    /// been dropped.\n    ///\n    /// It is valid, though unadvisable, to request a connection during this\n    /// period. The connection will be poisoned on return as well.\n    ///\n    /// Dropping this future cancels the drain operation.\n    pub async fn drain_all(self: Rc<Self>) {\n        let lock = Drain::lock_all(self.clone());\n        while self.blocks.total() > 0 {\n            tokio::time::sleep(Duration::from_millis(10)).await;\n        }\n        drop(lock);\n    }\n\n    /// Shuts this pool down safely. Dropping this future does not cancel\n    /// the shutdown operation.\n    pub async fn shutdown(mut self: Rc<Self>) {\n        self.drain.shutdown();\n        let pool = loop {\n            match Rc::try_unwrap(self) {\n                Ok(pool) => break pool,\n                Err(pool) => self = pool,\n            };\n            tokio::time::sleep(Duration::from_millis(10)).await;\n        };\n        while !pool.idle() {\n            pool.run_once();\n            tokio::time::sleep(Duration::from_millis(10)).await;\n        }\n        if cfg!(debug_assertions) {\n            let all_time = &pool.metrics().all_time;\n            if all_time[MetricVariant::Failed] == 0 {\n                assert_eq!(\n                    all_time[MetricVariant::Connecting] + all_time[MetricVariant::Reconnecting],\n                    all_time[MetricVariant::Disconnecting],\n                    \"Connecting + Reconnecting != Disconnecting\"\n                );\n                assert_eq!(\n                    all_time[MetricVariant::Disconnecting],\n                    all_time[MetricVariant::Closed],\n                    \"Disconnecting != Closed\"\n                );\n            }\n        }\n    }\n}\n\nimpl<C: Connector> AsRef<Drain> for Rc<Pool<C>> {\n    fn as_ref(&self) -> &Drain {\n        &self.drain\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::test::*;\n    use anyhow::{Ok, Result};\n    use itertools::Itertools;\n    use rstest::rstest;\n\n    use test_log::test;\n    use tokio::task::LocalSet;\n    use tracing::trace;\n\n    #[test(tokio::test(flavor = \"current_thread\", start_paused = true))]\n    async fn test_pool_basic() -> Result<()> {\n        LocalSet::new()\n            .run_until(async {\n                let config = PoolConfig::suggested_default_for(10);\n\n                let pool = Pool::new(config, BasicConnector::no_delay());\n                let conn1 = pool.acquire(\"1\").await?;\n                let conn2 = pool.acquire(\"1\").await?;\n\n                drop(conn1);\n                conn2.poison();\n                drop(conn2);\n\n                pool.shutdown().await;\n\n                Ok(())\n            })\n            .await\n    }\n\n    #[test(tokio::test(flavor = \"current_thread\", start_paused = true))]\n    async fn test_pool_eventually_idles() -> Result<()> {\n        let future = async {\n            let config = PoolConfig::suggested_default_for(10)\n                .with_min_idle_time_for_gc(Duration::from_secs(1));\n\n            let pool = Pool::new(config, BasicConnector::no_delay());\n            let conn = pool.acquire(\"1\").await?;\n            tokio::time::sleep(Duration::from_millis(10)).await;\n            drop(conn);\n\n            while !pool.idle() {\n                tokio::time::sleep(Duration::from_millis(10)).await;\n                pool.run_once();\n            }\n            trace!(\"Pool idle, shutting down\");\n\n            pool.shutdown().await;\n            Ok(())\n        };\n        tokio::time::timeout(Duration::from_secs(120), LocalSet::new().run_until(future)).await?\n    }\n\n    #[test(tokio::test(flavor = \"current_thread\", start_paused = true))]\n    #[rstest]\n    #[case(1)]\n    #[case(3)]\n    #[case(10)]\n    async fn test_pool_gc_from_max(\n        #[case] dbs: usize,\n        #[values(10, 100)] pool_size: usize,\n    ) -> Result<()> {\n        let future = async {\n            let config = PoolConfig::suggested_default_for(pool_size)\n                .with_min_idle_time_for_gc(Duration::from_secs(1));\n\n            let pool = Pool::new(config, BasicConnector::no_delay());\n            let mut conns = vec![];\n            for i in 0..10 {\n                conns.push(pool.acquire(&format!(\"{}\", i % dbs)).await?);\n            }\n            drop(conns);\n\n            while !pool.idle() {\n                tokio::time::sleep(Duration::from_millis(10)).await;\n                pool.run_once();\n            }\n            trace!(\"Pool idle, shutting down\");\n\n            pool.shutdown().await;\n            Ok(())\n        };\n        tokio::time::timeout(Duration::from_secs(10), LocalSet::new().run_until(future)).await?\n    }\n\n    #[test(tokio::test(flavor = \"current_thread\", start_paused = true))]\n    async fn test_pool_drains() -> Result<()> {\n        let future = async {\n            let config = PoolConfig::suggested_default_for(10);\n\n            let pool = Pool::new(config, BasicConnector::no_delay());\n            let conn = pool.acquire(\"1\").await?;\n            tokio::task::spawn_local(pool.clone().drain_all());\n            tokio::task::spawn_local(async {\n                tokio::time::sleep(Duration::from_millis(10)).await;\n                drop(conn);\n            });\n\n            while !pool.idle() {\n                tokio::time::sleep(Duration::from_millis(10)).await;\n            }\n            trace!(\"Pool idle, shutting down\");\n\n            pool.shutdown().await;\n            Ok(())\n        };\n        tokio::time::timeout(Duration::from_secs(120), LocalSet::new().run_until(future)).await?\n    }\n\n    #[test(tokio::test(flavor = \"current_thread\", start_paused = true))]\n    #[rstest]\n    #[case::one(1)]\n    #[case::small(10)]\n    #[case::medium(12)]\n    #[case::large(20)]\n    async fn test_pool(#[case] databases: usize) -> Result<()> {\n        let spec = Spec {\n            name: format!(\"test_pool_{databases}\").into(),\n            desc: \"\",\n            capacity: 10,\n            conn_cost: Triangle(0.05, 0.0),\n            score: vec![\n                Score::new(\n                    0.8,\n                    [2.0, 0.5, 0.25, 0.0],\n                    LatencyDistribution {\n                        group: 0..databases,\n                    },\n                ),\n                Score::new(0.2, [0.5, 0.2, 0.1, 0.0], ConnectionOverhead {}),\n            ],\n            dbs: (0..databases)\n                .map(|db| DBSpec {\n                    db,\n                    start_at: 0.0,\n                    end_at: 1.0,\n                    qps: 1200,\n                    query_cost: Triangle(0.001, 0.0),\n                })\n                .collect_vec(),\n            ..Default::default()\n        };\n\n        crate::test::spec::run(spec).await.map(drop)\n    }\n\n    #[test(tokio::test(flavor = \"current_thread\", start_paused = true))]\n    #[rstest]\n    #[case::small(1)]\n    #[case::medium(10)]\n    #[case::large(20)]\n    async fn test_pool_failures(#[case] databases: usize) -> Result<()> {\n        let spec = Spec {\n            name: format!(\"test_pool_fail50_{databases}\").into(),\n            desc: \"\",\n            capacity: 10,\n            conn_cost: Triangle(0.05, 0.0),\n            conn_failure_percentage: 50,\n            score: vec![\n                Score::new(\n                    0.8,\n                    [2.0, 0.5, 0.25, 0.0],\n                    LatencyDistribution {\n                        group: 0..databases,\n                    },\n                ),\n                Score::new(0.2, [0.5, 0.2, 0.1, 0.0], ConnectionOverhead {}),\n            ],\n            dbs: (0..databases)\n                .map(|db| DBSpec {\n                    db,\n                    start_at: 0.0,\n                    end_at: 1.0,\n                    qps: 1200,\n                    query_cost: Triangle(0.001, 0.0),\n                })\n                .collect_vec(),\n            ..Default::default()\n        };\n\n        crate::test::spec::run(spec).await.map(drop)\n    }\n}\n"
  },
  {
    "path": "rust/conn_pool/src/python.rs",
    "content": "use crate::{\n    conn::{ConnError, ConnResult, Connector},\n    metrics::MetricVariant,\n    pool::{Pool, PoolConfig},\n    PoolHandle,\n};\nuse derive_more::{Add, AddAssign};\nuse pyo3::{\n    exceptions::{PyException, PyValueError},\n    prelude::*,\n    types::PyByteArray,\n};\nuse pyo3_util::{\n    channel::{new_python_channel, PythonChannel, PythonChannelImpl, RustChannel},\n    logging::{get_python_logger_level, initialize_logging_in_thread},\n};\nuse serde_pickle::SerOptions;\nuse std::{\n    cell::{Cell, RefCell},\n    collections::BTreeMap,\n    rc::Rc,\n    sync::Arc,\n    thread,\n    time::{Duration, Instant},\n};\nuse strum::IntoEnumIterator;\nuse tokio::task::LocalSet;\nuse tracing::{error, info, trace};\n\npyo3::create_exception!(_conn_pool, InternalError, PyException);\n\n#[derive(Debug)]\nenum RustToPythonMessage {\n    Acquired(PythonConnId, ConnHandleId),\n    Pruned(PythonConnId),\n\n    PerformConnect(ConnHandleId, String),\n    PerformDisconnect(ConnHandleId),\n    PerformReconnect(ConnHandleId, String),\n\n    Failed(PythonConnId, ConnHandleId),\n    Metrics(Vec<u8>),\n}\n\nimpl<'py> IntoPyObject<'py> for RustToPythonMessage {\n    type Target = PyAny;\n    type Output = Bound<'py, PyAny>;\n    type Error = PyErr;\n\n    fn into_pyobject(self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {\n        use RustToPythonMessage::*;\n        let res = match self {\n            Acquired(a, b) => (0, a, b.0).into_pyobject(py),\n            PerformConnect(conn, s) => (1, conn.0, s).into_pyobject(py),\n            PerformDisconnect(conn) => (2, conn.0).into_pyobject(py),\n            PerformReconnect(conn, s) => (3, conn.0, s).into_pyobject(py),\n            Pruned(conn) => (4, conn).into_pyobject(py),\n            Failed(conn, error) => (5, conn, error.0).into_pyobject(py),\n            Metrics(metrics) => {\n                // This is not really fast but it should not be happening very often\n                (6, PyByteArray::new(py, &metrics)).into_pyobject(py)\n            }\n        }?;\n        Ok(res.into_any())\n    }\n}\n\n#[derive(Debug)]\nenum PythonToRustMessage {\n    /// Acquire a connection.\n    Acquire(PythonConnId, String),\n    /// Release a connection.\n    Release(PythonConnId),\n    /// Discard a connection.\n    Discard(PythonConnId),\n    /// Prune connections from a database.\n    Prune(PythonConnId, String),\n    /// Completed an async request made by Rust.\n    CompletedAsync(ConnHandleId),\n    /// Failed an async request made by Rust.\n    FailedAsync(ConnHandleId),\n}\n\nimpl<'py> FromPyObject<'py> for PythonToRustMessage {\n    fn extract_bound(_: &Bound<'py, PyAny>) -> PyResult<Self> {\n        // Unused for this class\n        Err(PyValueError::new_err(\"Not implemented\"))\n    }\n}\n\ntype PythonConnId = u64;\n#[derive(Debug, Default, Clone, Copy, Add, AddAssign, PartialEq, Eq, Hash, PartialOrd, Ord)]\nstruct ConnHandleId(u64);\n\nimpl From<ConnHandleId> for Box<(dyn std::error::Error + std::marker::Send + Sync + 'static)> {\n    fn from(val: ConnHandleId) -> Self {\n        Box::new(ConnError::Underlying(format!(\"{val:?}\")))\n    }\n}\n\nstruct RpcPipe {\n    channel: RustChannel<PythonToRustMessage, RustToPythonMessage>,\n    handles: RefCell<BTreeMap<PythonConnId, PoolHandle<Rc<RpcPipe>>>>,\n    next_id: Cell<ConnHandleId>,\n    async_ops: RefCell<BTreeMap<ConnHandleId, tokio::sync::oneshot::Sender<()>>>,\n}\n\nimpl std::fmt::Debug for RpcPipe {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.write_str(\"RpcPipe\")\n    }\n}\n\nimpl RpcPipe {\n    async fn call<T>(\n        self: Rc<Self>,\n        conn_id: ConnHandleId,\n        ok: T,\n        msg: RustToPythonMessage,\n    ) -> ConnResult<T, ConnHandleId> {\n        let (tx, rx) = tokio::sync::oneshot::channel();\n        self.async_ops.borrow_mut().insert(conn_id, tx);\n        self.channel\n            .write(msg)\n            .await\n            .map_err(|_| ConnError::Underlying(conn_id))?;\n        if rx.await.is_ok() {\n            Err(ConnError::Underlying(conn_id))\n        } else {\n            Ok(ok)\n        }\n    }\n}\n\nimpl Connector for Rc<RpcPipe> {\n    type Conn = ConnHandleId;\n    type Error = ConnHandleId;\n\n    fn connect(\n        &self,\n        db: &str,\n    ) -> impl futures::Future<\n        Output = ConnResult<<Self as Connector>::Conn, <Self as Connector>::Error>,\n    > + 'static {\n        let id = self.next_id.get();\n        self.next_id.set(id + ConnHandleId(1));\n        let msg = RustToPythonMessage::PerformConnect(id, db.to_owned());\n        self.clone().call(id, id, msg)\n    }\n\n    fn disconnect(\n        &self,\n        conn: Self::Conn,\n    ) -> impl futures::Future<Output = ConnResult<(), <Self as Connector>::Error>> + 'static {\n        self.clone()\n            .call(conn, (), RustToPythonMessage::PerformDisconnect(conn))\n    }\n\n    fn reconnect(\n        &self,\n        conn: Self::Conn,\n        db: &str,\n    ) -> impl futures::Future<\n        Output = ConnResult<<Self as Connector>::Conn, <Self as Connector>::Error>,\n    > + 'static {\n        self.clone().call(\n            conn,\n            conn,\n            RustToPythonMessage::PerformReconnect(conn, db.to_owned()),\n        )\n    }\n}\n\n#[pyclass]\nstruct ConnPool {\n    channel: Arc<PythonChannelImpl<PythonToRustMessage, RustToPythonMessage>>,\n}\n\nimpl Drop for ConnPool {\n    fn drop(&mut self) {\n        info!(\"ConnPool dropped\");\n    }\n}\n\nfn internal_error(message: &str) -> PyErr {\n    error!(\"{message}\");\n    InternalError::new_err(())\n}\n\nasync fn run_and_block(config: PoolConfig, rpc_pipe: RpcPipe, stats_interval: f64) {\n    let rpc_pipe = Rc::new(rpc_pipe);\n\n    let pool = Pool::new(config, rpc_pipe.clone());\n\n    let pool_task = {\n        let pool = pool.clone();\n        let rpc_pipe = rpc_pipe.clone();\n        tokio::task::spawn_local(async move {\n            let stats_interval = Duration::from_secs_f64(stats_interval);\n            let mut last_stats = Instant::now();\n            loop {\n                pool.run_once();\n                tokio::time::sleep(Duration::from_millis(10)).await;\n                if last_stats.elapsed() > stats_interval {\n                    last_stats = Instant::now();\n                    if rpc_pipe\n                        .channel\n                        .write(RustToPythonMessage::Metrics(\n                            serde_pickle::to_vec(&pool.metrics(), SerOptions::new())\n                                .unwrap_or_default(),\n                        ))\n                        .await\n                        .is_err()\n                    {\n                        break;\n                    }\n                }\n            }\n        })\n    };\n\n    loop {\n        let Some(rpc) = rpc_pipe.channel.recv().await else {\n            info!(\"ConnPool shutting down\");\n            pool_task.abort();\n            pool.shutdown().await;\n            break;\n        };\n        let pool = pool.clone();\n        trace!(\"Received RPC: {rpc:?}\");\n        let rpc_pipe = rpc_pipe.clone();\n        tokio::task::spawn_local(async move {\n            use PythonToRustMessage::*;\n            match rpc {\n                Acquire(conn_id, db) => {\n                    let conn = match pool.acquire(&db).await {\n                        Ok(conn) => conn,\n                        Err(ConnError::Underlying(err)) => {\n                            _ = rpc_pipe\n                                .channel\n                                .write(RustToPythonMessage::Failed(conn_id, err))\n                                .await;\n                            return;\n                        }\n                        Err(_) => {\n                            // TODO\n                            return;\n                        }\n                    };\n                    let handle = conn.handle();\n                    rpc_pipe.handles.borrow_mut().insert(conn_id, conn);\n                    _ = rpc_pipe\n                        .channel\n                        .write(RustToPythonMessage::Acquired(conn_id, handle))\n                        .await;\n                }\n                Release(conn_id) => {\n                    rpc_pipe.handles.borrow_mut().remove(&conn_id);\n                }\n                Discard(conn_id) => {\n                    rpc_pipe\n                        .handles\n                        .borrow_mut()\n                        .remove(&conn_id)\n                        .unwrap()\n                        .poison();\n                }\n                Prune(conn_id, db) => {\n                    pool.drain_idle(&db).await;\n                    _ = rpc_pipe\n                        .channel\n                        .write(RustToPythonMessage::Pruned(conn_id))\n                        .await;\n                }\n                CompletedAsync(handle_id) => {\n                    rpc_pipe.async_ops.borrow_mut().remove(&handle_id);\n                }\n                FailedAsync(handle_id) => {\n                    _ = rpc_pipe\n                        .async_ops\n                        .borrow_mut()\n                        .remove(&handle_id)\n                        .unwrap()\n                        .send(());\n                }\n            }\n        });\n    }\n}\n\n#[pymethods]\nimpl ConnPool {\n    /// Create the connection pool and automatically boot a tokio runtime on a\n    /// new thread. When this [`ConnPool`] is GC'd, the thread will be torn down.\n    #[new]\n    fn new(\n        py: Python,\n        max_capacity: usize,\n        min_idle_time_before_gc: f64,\n        stats_interval: f64,\n    ) -> PyResult<Self> {\n        let level = get_python_logger_level(py, \"edb.server.conn_pool\")?;\n        let min_idle_time_before_gc = min_idle_time_before_gc as usize;\n        let new = py.detach(|| {\n            let (txfd, rxfd) = std::sync::mpsc::channel();\n            thread::spawn(move || {\n                initialize_logging_in_thread(\"edb.server.conn_pool\", level);\n                info!(\"ConnPool::new(max_capacity={max_capacity}, min_idle_time_before_gc={min_idle_time_before_gc})\");\n                info!(\"Rust-side ConnPool thread booted\");\n                let rt = tokio::runtime::Builder::new_current_thread()\n                    .enable_time()\n                    .enable_io()\n                    .build()\n                    .unwrap();\n                let _guard = rt.enter();\n\n                let (rust, python) = new_python_channel();\n                txfd.send(python).unwrap();\n                let local = LocalSet::new();\n\n                let rpc_pipe = RpcPipe {\n                    channel: rust,\n                    next_id: Default::default(),\n                    handles: Default::default(),\n                    async_ops: Default::default(),\n                };\n\n                let config = PoolConfig::suggested_default_for(max_capacity)\n                    .with_min_idle_time_for_gc(Duration::from_secs(min_idle_time_before_gc as _));\n                local.block_on(&rt, run_and_block(config, rpc_pipe, stats_interval));\n            });\n\n            let channel = rxfd.recv().unwrap().into();\n            ConnPool {\n                channel,\n            }\n        });\n        Ok(new)\n    }\n\n    #[getter]\n    fn _channel(&self) -> PyResult<PythonChannel> {\n        Ok(PythonChannel::new(self.channel.clone()))\n    }\n\n    fn _acquire(&self, id: u64, db: &str) -> PyResult<()> {\n        self.channel\n            .send(PythonToRustMessage::Acquire(id, db.to_owned()))\n            .map_err(|_| internal_error(\"In shutdown\"))\n    }\n\n    fn _release(&self, id: u64) -> PyResult<()> {\n        self.channel.send_err(PythonToRustMessage::Release(id))\n    }\n\n    fn _discard(&self, id: u64) -> PyResult<()> {\n        self.channel.send_err(PythonToRustMessage::Discard(id))\n    }\n\n    fn _completed(&self, id: u64) -> PyResult<()> {\n        self.channel\n            .send_err(PythonToRustMessage::CompletedAsync(ConnHandleId(id)))\n    }\n\n    fn _failed(&self, id: u64, _error: Py<PyAny>) -> PyResult<()> {\n        self.channel\n            .send_err(PythonToRustMessage::FailedAsync(ConnHandleId(id)))\n    }\n\n    fn _prune(&self, id: u64, db: &str) -> PyResult<()> {\n        self.channel\n            .send_err(PythonToRustMessage::Prune(id, db.to_owned()))\n    }\n}\n\n#[pymodule]\npub fn _conn_pool(py: Python, m: &Bound<PyModule>) -> PyResult<()> {\n    m.add_class::<ConnPool>()?;\n    m.add(\"InternalError\", py.get_type::<InternalError>())?;\n\n    // Add each metric variant as a constant\n    for variant in MetricVariant::iter() {\n        m.add(\n            format!(\"METRIC_{}\", variant.as_ref().to_ascii_uppercase()).as_str(),\n            variant as u32,\n        )?;\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "rust/conn_pool/src/test/mod.rs",
    "content": "//! Test utilities.\nuse itertools::Itertools;\nuse rand::random;\nuse statrs::statistics::{Data, Distribution, OrderStatistics, Statistics};\nuse std::{\n    borrow::Cow,\n    cell::{Cell, RefCell},\n    collections::{BTreeMap, HashMap},\n    future::Future,\n    ops::Range,\n    rc::Rc,\n    time::Duration,\n};\n\nuse crate::{\n    conn::{ConnError, ConnResult, Connector},\n    metrics::{MetricVariant, PoolMetrics},\n    PoolConfig,\n};\n\npub mod spec;\n\n#[derive(derive_more::Debug)]\npub struct BasicConnector {\n    #[allow(clippy::type_complexity)]\n    #[debug(skip)]\n    delay: Option<Rc<dyn Fn(bool) -> Result<Duration, ()>>>,\n    fail_next_connect: Cell<bool>,\n    fail_next_disconnect: Cell<bool>,\n}\n\nimpl BasicConnector {\n    pub fn no_delay() -> Self {\n        BasicConnector {\n            delay: None,\n            fail_next_connect: Default::default(),\n            fail_next_disconnect: Default::default(),\n        }\n    }\n\n    pub fn delay(f: impl Fn(bool) -> Result<Duration, ()> + 'static) -> Self {\n        BasicConnector {\n            delay: Some(Rc::new(f)),\n            fail_next_connect: Default::default(),\n            fail_next_disconnect: Default::default(),\n        }\n    }\n\n    pub fn fail_next_connect(&self) {\n        self.fail_next_connect.set(true);\n    }\n\n    pub fn fail_next_disconnect(&self) {\n        self.fail_next_disconnect.set(true);\n    }\n\n    fn duration(&self, disconnect: bool) -> ConnResult<Option<Duration>, String> {\n        if disconnect && self.fail_next_disconnect.replace(false) {\n            return Err(ConnError::Underlying(\"failed\".to_string()));\n        }\n        if !disconnect && self.fail_next_connect.replace(false) {\n            return Err(ConnError::Underlying(\"failed\".to_string()));\n        }\n        if let Some(f) = &self.delay {\n            Ok(Some(f(disconnect).map_err(|_| {\n                ConnError::Underlying(\"failed\".to_string())\n            })?))\n        } else {\n            Ok(None)\n        }\n    }\n}\n\nimpl Connector for BasicConnector {\n    type Conn = ();\n    type Error = String;\n    fn connect(\n        &self,\n        _db: &str,\n    ) -> impl Future<Output = ConnResult<Self::Conn, Self::Error>> + 'static {\n        let connect = self.duration(false);\n        async move {\n            if let Some(f) = connect? {\n                tokio::time::sleep(f).await;\n            }\n            Ok(())\n        }\n    }\n    fn reconnect(\n        &self,\n        conn: Self::Conn,\n        _db: &str,\n    ) -> impl Future<Output = ConnResult<Self::Conn, Self::Error>> + 'static {\n        let connect = self.duration(false);\n        let disconnect = self.duration(true);\n        async move {\n            if let Some(f) = disconnect? {\n                tokio::time::sleep(f).await;\n            }\n            if let Some(f) = connect? {\n                tokio::time::sleep(f).await;\n            }\n            Ok(conn)\n        }\n    }\n    fn disconnect(\n        &self,\n        _conn: Self::Conn,\n    ) -> impl Future<Output = ConnResult<(), Self::Error>> + 'static {\n        let disconnect = self.duration(true);\n        async move {\n            if let Some(f) = disconnect? {\n                tokio::time::sleep(f).await;\n            }\n            Ok(())\n        }\n    }\n}\n\n#[derive(Clone, Default)]\npub struct Latencies {\n    data: Rc<RefCell<HashMap<String, Vec<f64>>>>,\n}\n\n/// Helper function for [`Stats`] [`Debug`] impl.\n#[allow(unused)]\nfn m(v: &f64) -> Duration {\n    if *v <= 0.000_001 {\n        Duration::ZERO\n    } else {\n        Duration::from_secs_f64(*v)\n    }\n}\n\n#[derive(derive_more::Debug)]\n#[allow(unused)]\n#[debug(\n    \"#{count} %{{1,25,50,75,99}}: {:?}/{:?}/{:?}/{:?}/{:?}, x̄: {:?} Πx: {:?}\",\n    m(p01),\n    m(p25),\n    m(p50),\n    m(p75),\n    m(p99),\n    m(mean),\n    m(geometric_mean)\n)]\nstruct Stats {\n    p01: f64,\n    p25: f64,\n    p50: f64,\n    p75: f64,\n    p99: f64,\n    geometric_mean: f64,\n    mean: f64,\n    count: usize,\n}\n\nimpl Latencies {\n    pub fn mark(&self, db: &str, latency: f64) {\n        self.data\n            .borrow_mut()\n            .entry(db.to_owned())\n            .or_default()\n            .push(latency.max(0.000_001));\n    }\n\n    fn len(&self) -> usize {\n        let mut len = 0;\n        for values in self.data.borrow().values() {\n            len += values.len()\n        }\n        len\n    }\n}\n\nimpl std::fmt::Debug for Latencies {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let mut s = f.debug_struct(\"Latencies\");\n        let mut data = self.data.borrow_mut();\n        let mut all = vec![];\n        for key in data.keys().cloned().sorted() {\n            let data = data.get_mut(&key).unwrap();\n            all.extend_from_slice(data);\n            let stats = stats(data);\n            s.field(&key, &stats);\n        }\n        let stats = stats(&mut all);\n        s.field(\"all\", &stats);\n        s.finish()\n    }\n}\n\nfn stats(data: &mut [f64]) -> Stats {\n    let geometric_mean = data.geometric_mean();\n    let mut data = Data::new(data);\n    let mean = data.mean().unwrap();\n\n    Stats {\n        p01: data.percentile(1),\n        p25: data.percentile(25),\n        p50: data.percentile(50),\n        p75: data.percentile(75),\n        p99: data.percentile(99),\n        geometric_mean,\n        mean,\n        count: data.len(),\n    }\n}\n\n#[derive(smart_default::SmartDefault)]\npub struct Spec {\n    pub name: Cow<'static, str>,\n    pub desc: &'static str,\n    #[default = 30]\n    pub timeout: usize,\n    #[default = 1.1]\n    pub duration: f64,\n    pub capacity: usize,\n    pub conn_cost: Triangle,\n    #[default = 0]\n    pub conn_failure_percentage: u8,\n    pub dbs: Vec<DBSpec>,\n    #[default(Triangle(0.006, 0.0015))]\n    pub disconn_cost: Triangle,\n    pub score: Vec<Score>,\n}\n\nimpl Spec {\n    pub fn scale(&mut self, time_scale: f64) {\n        self.duration *= time_scale;\n        for db in &mut self.dbs {\n            db.scale(time_scale);\n        }\n    }\n}\n\n#[derive(derive_more::Debug)]\npub struct Scored {\n    pub description: String,\n    #[debug(skip)]\n    pub detailed_calculation: Box<dyn Fn(usize) -> String + Send + Sync>,\n    pub raw_value: f64,\n}\n\n#[derive(Debug)]\npub struct WeightedScored {\n    pub weight: f64,\n    pub score: f64,\n    pub scored: Scored,\n}\n\n#[derive(Debug)]\npub struct QoS {\n    pub scores: Vec<WeightedScored>,\n    pub qos: f64,\n}\n\n#[derive(Default, derive_more::Deref, derive_more::DerefMut, derive_more::IntoIterator)]\npub struct SuiteQoS(#[into_iterator(owned, ref, ref_mut)] BTreeMap<Cow<'static, str>, QoS>);\n\nimpl std::fmt::Debug for SuiteQoS {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let mut s = f.debug_struct(\"SuiteQos\");\n        for (name, qos) in self {\n            s.field(name, &format!(\"QoS = {:.02}\", qos.qos));\n        }\n        s.field(\"qos\", &self.qos());\n        s.field(\"qos_rms\", &self.qos_rms_error());\n        s.field(\"qos_min\", &self.qos_min());\n        s.finish()\n    }\n}\n\nimpl SuiteQoS {\n    pub fn qos(&self) -> f64 {\n        let mut total = 0.0;\n        for qos in self.values() {\n            total += qos.qos;\n        }\n        total /= self.len() as f64;\n        if !total.is_normal() || total < 0.0 {\n            0.0\n        } else {\n            total\n        }\n    }\n\n    /// Return the root-mean-square error QoS. The error between the QoS and 100\n    /// is squared, averaged, and we subtract that from 100 for a final score.\n    pub fn qos_rms_error(&self) -> f64 {\n        let mut total = 0.0;\n        for qos in self.values() {\n            total += (100.0 - qos.qos).powf(2.0);\n        }\n        total /= self.len() as f64;\n        total = 100.0 - total.sqrt();\n        if !total.is_normal() || total < 0.0 {\n            0.0\n        } else {\n            total\n        }\n    }\n\n    /// Return the root-mean-square error QoS. The error between the QoS and 100\n    /// is squared, averaged, and we subtract that from 100 for a final score.\n    pub fn qos_min(&self) -> f64 {\n        let mut min: f64 = 100.0;\n        for qos in self.values() {\n            min = min.min(qos.qos);\n        }\n        if !min.is_normal() || min < 0.0 {\n            0.0\n        } else {\n            min\n        }\n    }\n}\n\npub trait ScoringMethod {\n    fn score(&self, latencies: &Latencies, metrics: &PoolMetrics, config: &PoolConfig) -> Scored;\n}\n\npub struct Score {\n    pub v100: f64,\n    pub v90: f64,\n    pub v60: f64,\n    pub v0: f64,\n    pub weight: f64,\n    pub method: Box<dyn ScoringMethod + Send + Sync + 'static>,\n}\n\nimpl Score {\n    pub fn new(\n        weight: f64,\n        scores: [f64; 4],\n        method: impl ScoringMethod + Send + Sync + 'static,\n    ) -> Self {\n        Self {\n            weight,\n            v0: scores[0],\n            v60: scores[1],\n            v90: scores[2],\n            v100: scores[3],\n            method: Box::new(method),\n        }\n    }\n\n    pub fn calculate(&self, value: f64) -> f64 {\n        if value.is_nan() || value.is_infinite() {\n            return 0.0;\n        }\n\n        let intervals = [\n            (self.v100, self.v90, 90.0, 10.0),\n            (self.v90, self.v60, 60.0, 30.0),\n            (self.v60, self.v0, 0.0, 60.0),\n        ];\n\n        for &(v1, v2, base, diff) in &intervals {\n            let v_min = v1.min(v2);\n            let v_max = v1.max(v2);\n            if v_min <= value && value < v_max {\n                return base + (value - v2).abs() / (v_max - v_min) * diff;\n            }\n        }\n\n        if self.v0 > self.v100 {\n            if value < self.v100 {\n                100.0\n            } else {\n                0.0\n            }\n        } else if value < self.v0 {\n            0.0\n        } else {\n            100.0\n        }\n    }\n}\n\npub struct LatencyDistribution {\n    pub group: Range<usize>,\n}\n\nimpl ScoringMethod for LatencyDistribution {\n    fn score(&self, latencies: &Latencies, _metrics: &PoolMetrics, _config: &PoolConfig) -> Scored {\n        let dbs = self.group.clone().map(|t| format!(\"t{t}\")).collect_vec();\n        let mut data = latencies.data.borrow_mut();\n        let fail = Cell::new(false);\n\n        // Calculates the average CV (coefficient of variation) of the given\n        // distributions. The result is a float ranging from zero indicating how\n        // different the given distributions are, where zero means no\n        // difference. Known defect: when the mean value is close to zero, the\n        // coefficient of variation will approach infinity and is therefore\n        // sensitive to small changes.\n        let values = (1..=9)\n            .map(move |n| {\n                let decile = Data::new(\n                    dbs.iter()\n                        .map(|db| {\n                            let Some(data) = data.get_mut(db) else {\n                                fail.set(true);\n                                return 0.0;\n                            };\n                            let mut data = Data::new(data.as_mut_slice());\n                            // This is equivalent to Python's statistics.quartile(n=10)\n                            data.percentile(n * 10)\n                        })\n                        .collect_vec(),\n                );\n                let cv = decile.std_dev().unwrap_or_default() / decile.mean().unwrap_or_default();\n                if cv.is_normal() {\n                    cv\n                } else {\n                    0.0\n                }\n            })\n            .collect_vec();\n        let mean = values.iter().geometric_mean();\n        Scored {\n            description: format!(\"Average CV for range {:?}\", self.group),\n            detailed_calculation: Box::new(move |precision| format!(\"{values:.precision$?}\")),\n            raw_value: mean,\n        }\n    }\n}\n\nimpl<T: ScoringMethod + 'static> From<T> for Box<dyn ScoringMethod> {\n    fn from(value: T) -> Self {\n        Box::new(value)\n    }\n}\n\npub struct ConnectionOverhead {}\n\nimpl ScoringMethod for ConnectionOverhead {\n    fn score(&self, latencies: &Latencies, metrics: &PoolMetrics, _config: &PoolConfig) -> Scored {\n        let reconnects = metrics.all_time[MetricVariant::Reconnecting];\n        let count = latencies.len();\n        let raw_value = reconnects as f64 / count as f64;\n        Scored {\n            description: \"Num of re-connects/query\".to_owned(),\n            detailed_calculation: Box::new(move |_precision| format!(\"{reconnects}/{count}\")),\n            raw_value,\n        }\n    }\n}\n\npub struct LatencyRatio {\n    pub percentile: u8,\n    pub dividend: Range<usize>,\n    pub divisor: Range<usize>,\n}\n\nimpl ScoringMethod for LatencyRatio {\n    fn score(&self, latencies: &Latencies, _metrics: &PoolMetrics, _config: &PoolConfig) -> Scored {\n        let mut data = latencies.data.borrow_mut();\n        let dbs = self.divisor.clone().map(|t| format!(\"t{t}\")).collect_vec();\n        let divisor = dbs\n            .iter()\n            .map(|db| {\n                let Some(data) = data.get_mut(db) else {\n                    return f64::NAN;\n                };\n                let mut data = Data::new(data.as_mut_slice());\n                data.percentile(self.percentile as _)\n            })\n            .mean();\n        let dbs = self.dividend.clone().map(|t| format!(\"t{t}\")).collect_vec();\n        let dividend = dbs\n            .iter()\n            .map(|db| {\n                let Some(data) = data.get_mut(db) else {\n                    return f64::NAN;\n                };\n                let mut data = Data::new(data.as_mut_slice());\n                data.percentile(self.percentile as _)\n            })\n            .mean();\n        let raw_value = dividend / divisor;\n        Scored {\n            description: format!(\n                \"P{} ratio {:?}/{:?}\",\n                self.percentile, self.dividend, self.divisor\n            ),\n            detailed_calculation: Box::new(move |precision| {\n                format!(\"{dividend:.precision$}/{divisor:.precision$}\")\n            }),\n            raw_value,\n        }\n    }\n}\n\npub struct EndingCapacity {}\n\nimpl ScoringMethod for EndingCapacity {\n    fn score(&self, _latencies: &Latencies, metrics: &PoolMetrics, _config: &PoolConfig) -> Scored {\n        let total = metrics.pool.total;\n        let raw_value = total as _;\n        Scored {\n            description: \"Ending capacity\".to_string(),\n            detailed_calculation: Box::new(move |_| format!(\"{total}\")),\n            raw_value,\n        }\n    }\n}\n\npub struct AbsoluteLatency {\n    pub percentile: u8,\n    pub group: Range<usize>,\n}\n\nimpl ScoringMethod for AbsoluteLatency {\n    fn score(&self, latencies: &Latencies, _metrics: &PoolMetrics, _config: &PoolConfig) -> Scored {\n        let mut data = latencies.data.borrow_mut();\n        let dbs = self.group.clone().map(|t| format!(\"t{t}\")).collect_vec();\n        let raw_value = dbs\n            .iter()\n            .map(|db| {\n                let Some(data) = data.get_mut(db) else {\n                    return f64::NAN;\n                };\n                let mut data = Data::new(data.as_mut_slice());\n                data.percentile(self.percentile as _)\n            })\n            .mean();\n\n        Scored {\n            description: format!(\"Absolute P{} value {:?}\", self.percentile, self.group),\n            detailed_calculation: Box::new(move |precision| format!(\"{raw_value:.precision$}\")),\n            raw_value,\n        }\n    }\n}\n\n#[derive(Debug)]\npub struct DBSpec {\n    pub db: usize,\n    pub start_at: f64,\n    pub end_at: f64,\n    pub qps: usize,\n    pub query_cost: Triangle,\n}\n\nimpl DBSpec {\n    pub fn scale(&mut self, time_scale: f64) {\n        self.start_at *= time_scale;\n        self.end_at *= time_scale;\n    }\n}\n\n#[derive(Default, derive_more::Debug, Clone, Copy)]\n#[debug(\"{0:?}±{1:?}\", Duration::from_secs_f64(self.0), Duration::from_secs_f64(self.1))]\npub struct Triangle(pub f64, pub f64);\n\nimpl Triangle {\n    pub fn random(&self) -> f64 {\n        self.0 + (random::<f64>() * 2.0 - 1.0) * self.1\n    }\n\n    pub fn random_duration(&self) -> Duration {\n        let r = self.random();\n        if r <= 0.001 {\n            Duration::from_millis(1)\n        } else {\n            Duration::from_secs_f64(r)\n        }\n    }\n}\n"
  },
  {
    "path": "rust/conn_pool/src/test/spec.rs",
    "content": "//! Test utilities.\nuse anyhow::{Error, Ok, Result};\nuse rand::{thread_rng, Rng};\nuse std::time::Duration;\nuse tokio::task::LocalSet;\nuse tracing::{error, info, trace};\n\nuse crate::{\n    test::{BasicConnector, Latencies, WeightedScored},\n    time::Instant,\n    Pool, PoolConfig,\n};\n\nuse super::{\n    AbsoluteLatency, ConnectionOverhead, DBSpec, EndingCapacity, LatencyDistribution, LatencyRatio,\n    QoS, Score, Spec, SuiteQoS, Triangle,\n};\n\npub async fn run(spec: Spec) -> Result<QoS> {\n    let local = LocalSet::new();\n    let res = local.run_until(run_local(spec)).await?;\n    local.await;\n    Ok(res)\n}\n\n/// This is the general spec-running function used by all spec paths.\nasync fn run_local(spec: Spec) -> std::result::Result<QoS, Error> {\n    let start = Instant::now();\n    let real_time = std::time::Instant::now();\n    let config = PoolConfig::suggested_default_for(spec.capacity)\n        .with_min_idle_time_for_gc(Duration::from_secs_f64(spec.duration / 10.0));\n    let disconnect_cost = spec.disconn_cost;\n    let connect_cost = spec.disconn_cost;\n    let conn_failure_percentage = spec.conn_failure_percentage;\n    let connector = BasicConnector::delay(move |disconnect| {\n        if conn_failure_percentage > 0 && thread_rng().gen_range(0..100) > conn_failure_percentage {\n            return std::result::Result::Err(());\n        }\n        std::result::Result::Ok(if disconnect {\n            disconnect_cost.random_duration()\n        } else {\n            connect_cost.random_duration()\n        })\n    });\n    let pool = Pool::new(config, connector);\n    let mut tasks = vec![];\n    let latencies = Latencies::default();\n\n    // Boot a task for each DBSpec in the Spec\n    for (i, db_spec) in spec.dbs.into_iter().enumerate() {\n        let interval = 1.0 / (db_spec.qps as f64);\n        info!(\"[{i:-2}] db {db_spec:?}\");\n        let db = format!(\"t{}\", db_spec.db);\n        let pool = pool.clone();\n        let latencies = latencies.clone();\n        let local = async move {\n            let now = Instant::now();\n            let count = ((db_spec.end_at - db_spec.start_at) * (db_spec.qps as f64)) as usize;\n            tokio::time::sleep(Duration::from_secs_f64(db_spec.start_at)).await;\n            info!(\n                \"+[{i:-2}] Starting db {db} at {}qps (approx {}q·s/s from {}..{})...\",\n                db_spec.qps,\n                db_spec.qps as f64 * db_spec.query_cost.0,\n                db_spec.start_at,\n                db_spec.end_at,\n            );\n            let start_time = now.elapsed().as_secs_f64();\n            // Boot one task for each expected query in a localset, with a\n            // sleep that schedules it for the appropriate time.\n            let local = LocalSet::new();\n            for i in 0..count {\n                let pool = pool.clone();\n                let latencies = latencies.clone();\n                let duration = db_spec.query_cost.random_duration();\n                let db = db.clone();\n                local.spawn_local(async move {\n                    tokio::time::sleep(Duration::from_secs_f64(i as f64 * interval)).await;\n                    let now = Instant::now();\n                    let conn = pool.acquire(&db).await?;\n                    let latency = now.elapsed();\n                    latencies.mark(&db, latency.as_secs_f64());\n                    tokio::time::sleep(duration).await;\n                    drop(conn);\n                    Ok(())\n                });\n            }\n            tokio::time::timeout(Duration::from_secs(120), local)\n                .await\n                .unwrap_or_else(move |_| error!(\"*[{i:-2}] DBSpec {i} for {db} timed out\"));\n            let end_time = now.elapsed().as_secs_f64();\n            info!(\"-[{i:-2}] Finished db t{} at {}qps. Load generated from {}..{}, processed from {}..{}\",\n                    db_spec.db, db_spec.qps, db_spec.start_at, db_spec.end_at, start_time, end_time);\n        };\n        tasks.push(tokio::task::spawn_local(local));\n    }\n\n    // Boot the monitor the runs the pool algorithm and prints the current\n    // block connection stats.\n    let monitor = {\n        let pool = pool.clone();\n        tokio::task::spawn_local(async move {\n            let mut orig = \"\".to_owned();\n            loop {\n                pool.run_once();\n                let mut s = \"\".to_owned();\n                for (name, block) in pool.metrics().blocks {\n                    s += &format!(\"{name}={} \", block.total);\n                }\n                if !s.is_empty() && s != orig {\n                    trace!(\n                        \"Blocks: {}/{} {s}\",\n                        pool.metrics().pool.total,\n                        pool.config.constraints.max\n                    );\n                    orig = s;\n                }\n                tokio::time::sleep(Duration::from_millis(10)).await;\n            }\n        })\n    };\n\n    info!(\"Starting...\");\n    tokio::time::sleep(Duration::from_secs_f64(spec.duration)).await;\n\n    for task in tasks {\n        _ = task.await;\n    }\n\n    info!(\n        \"Took {:?} of virtual time ({:?} real time)\",\n        start.elapsed(),\n        real_time.elapsed()\n    );\n\n    monitor.abort();\n    _ = monitor.await;\n    let metrics = pool.metrics();\n    info!(\"{metrics:#?}\");\n    info!(\"{latencies:#?}\");\n\n    let metrics = pool.metrics();\n    let mut qos = 0.0;\n    let mut scores = vec![];\n    for score in spec.score {\n        let scored = score.method.score(&latencies, &metrics, &pool.config);\n\n        let score_component = score.calculate(scored.raw_value);\n        info!(\n            \"[QoS: {}] {} = {:.2} -> {:.2} (weight {:.2})\",\n            spec.name, scored.description, scored.raw_value, score_component, score.weight\n        );\n        trace!(\n            \"[QoS: {}] {} [detail]: {} = {:.3}\",\n            spec.name,\n            scored.description,\n            (scored.detailed_calculation)(3),\n            scored.raw_value\n        );\n        scores.push(WeightedScored {\n            scored,\n            weight: score.weight,\n            score: score_component,\n        });\n        qos += score_component * score.weight;\n    }\n    info!(\"[QoS: {}] Score = {qos:0.02}\", spec.name);\n\n    info!(\"Shutting down...\");\n    pool.shutdown().await;\n\n    Ok(QoS { scores, qos })\n}\n\nfn test_connpool_1() -> Spec {\n    let mut dbs = vec![];\n    for i in 0..6 {\n        dbs.push(DBSpec {\n            db: i,\n            start_at: 0.0,\n            end_at: 0.5,\n            qps: 50,\n            query_cost: Triangle(0.03, 0.005),\n        })\n    }\n    for i in 6..12 {\n        dbs.push(DBSpec {\n            db: i,\n            start_at: 0.3,\n            end_at: 0.7,\n            qps: 50,\n            query_cost: Triangle(0.03, 0.005),\n        })\n    }\n    for i in 0..6 {\n        dbs.push(DBSpec {\n            db: i,\n            start_at: 0.6,\n            end_at: 0.8,\n            qps: 50,\n            query_cost: Triangle(0.03, 0.005),\n        })\n    }\n\n    Spec {\n        name: \"test_connpool_1\".into(),\n        desc: r#\"\n            This is a test for Mode D, where 2 groups of blocks race for connections\n            in the pool with max capacity set to 6. The first group (0-5) has more\n            dedicated time with the pool, so it should have relatively lower latency\n            than the second group (6-11). But the QoS is focusing on the latency\n            distribution similarity, as we don't want to starve only a few blocks\n            because of the lack of capacity. Therefore, reconnection is a necessary\n            cost for QoS.\n        \"#,\n        capacity: 6,\n        conn_cost: Triangle(0.05, 0.01),\n        score: vec![\n            Score::new(\n                0.18,\n                [2.0, 0.5, 0.25, 0.0],\n                LatencyDistribution { group: 0..6 },\n            ),\n            Score::new(\n                0.28,\n                [2.0, 0.3, 0.1, 0.0],\n                LatencyDistribution { group: 6..12 },\n            ),\n            Score::new(\n                0.48,\n                [2.0, 0.7, 0.45, 0.2],\n                LatencyDistribution { group: 0..12 },\n            ),\n            Score::new(0.06, [0.5, 0.2, 0.1, 0.0], ConnectionOverhead {}),\n        ],\n        dbs,\n        ..Default::default()\n    }\n}\n\nfn test_connpool_2() -> Spec {\n    let mut dbs = vec![];\n    for i in 0..6 {\n        dbs.push(DBSpec {\n            db: i,\n            start_at: 0.0,\n            end_at: 0.5,\n            qps: 1500,\n            query_cost: Triangle(0.001, 0.005),\n        })\n    }\n    for i in 6..12 {\n        dbs.push(DBSpec {\n            db: i,\n            start_at: 0.3,\n            end_at: 0.7,\n            qps: 700,\n            query_cost: Triangle(0.03, 0.001),\n        })\n    }\n    for i in 0..6 {\n        dbs.push(DBSpec {\n            db: i,\n            start_at: 0.6,\n            end_at: 0.8,\n            qps: 700,\n            query_cost: Triangle(0.06, 0.01),\n        })\n    }\n\n    Spec {\n        name: \"test_connpool_2\".into(),\n        desc: r#\"\n            In this test, we have 6x1500qps connections that simulate fast\n            queries (0.001..0.006s), and 6x700qps connections that simulate\n            slow queries (~0.03s). The algorithm allocates connections\n            fairly to both groups, essentially using the\n            \"demand = avg_query_time * avg_num_of_connection_waiters\"\n            formula. The QoS is at the same level for all DBs. (Mode B / C)\n        \"#,\n        capacity: 100,\n        conn_cost: Triangle(0.04, 0.011),\n        score: vec![\n            Score::new(\n                0.18,\n                [2.0, 0.5, 0.25, 0.0],\n                LatencyDistribution { group: 0..6 },\n            ),\n            Score::new(\n                0.28,\n                [2.0, 0.3, 0.1, 0.0],\n                LatencyDistribution { group: 6..12 },\n            ),\n            Score::new(\n                0.48,\n                [2.0, 0.7, 0.45, 0.2],\n                LatencyDistribution { group: 0..12 },\n            ),\n            Score::new(0.06, [0.5, 0.2, 0.1, 0.0], ConnectionOverhead {}),\n        ],\n        dbs,\n        ..Default::default()\n    }\n}\n\nfn test_connpool_3() -> Spec {\n    let mut dbs = vec![];\n    for i in 0..6 {\n        dbs.push(DBSpec {\n            db: i,\n            start_at: 0.0,\n            end_at: 0.8,\n            qps: 5000,\n            query_cost: Triangle(0.01, 0.005),\n        })\n    }\n\n    Spec {\n        name: \"test_connpool_3\".into(),\n        desc: r#\"\n            This test simply starts 6 same crazy requesters for 6 databases to\n            test the pool fairness in Mode C with max capacity of 100.\n        \"#,\n        capacity: 100,\n        conn_cost: Triangle(0.04, 0.011),\n        score: vec![\n            Score::new(\n                0.85,\n                [1.0, 0.2, 0.1, 0.0],\n                LatencyDistribution { group: 0..6 },\n            ),\n            Score::new(0.15, [0.5, 0.2, 0.1, 0.0], ConnectionOverhead {}),\n        ],\n        dbs,\n        ..Default::default()\n    }\n}\n\nfn test_connpool_4() -> Spec {\n    let mut dbs = vec![];\n    for i in 0..6 {\n        dbs.push(DBSpec {\n            db: i,\n            start_at: 0.0,\n            end_at: 0.8,\n            qps: 1000,\n            query_cost: Triangle(0.01 * (i as f64 + 1.0), 0.005 * (i as f64 + 1.0)),\n        })\n    }\n\n    Spec {\n        name: \"test_connpool_4\".into(),\n        desc: r#\"\n            Similar to test 3, this test also has 6 requesters for 6 databases,\n            they have the same Q/s but with different query cost. In Mode C,\n            we should observe equal connection acquisition latency, fair and\n            stable connection distribution and reasonable reconnection cost.\n        \"#,\n        capacity: 50,\n        conn_cost: Triangle(0.04, 0.011),\n        score: vec![\n            Score::new(\n                0.9,\n                [1.0, 0.2, 0.1, 0.0],\n                LatencyDistribution { group: 0..6 },\n            ),\n            Score::new(0.1, [0.5, 0.2, 0.1, 0.0], ConnectionOverhead {}),\n        ],\n        dbs,\n        ..Default::default()\n    }\n}\n\nfn test_connpool_5() -> Spec {\n    let mut dbs = vec![];\n\n    for i in 0..6 {\n        dbs.push(DBSpec {\n            db: i,\n            start_at: 0.0 + i as f64 / 10.0,\n            end_at: 0.5 + i as f64 / 10.0,\n            qps: 150,\n            query_cost: Triangle(0.020, 0.005),\n        });\n    }\n    for i in 6..12 {\n        dbs.push(DBSpec {\n            db: i,\n            start_at: 0.3,\n            end_at: 0.7,\n            qps: 50,\n            query_cost: Triangle(0.008, 0.003),\n        });\n    }\n    for i in 0..6 {\n        dbs.push(DBSpec {\n            db: i,\n            start_at: 0.6,\n            end_at: 0.8,\n            qps: 50,\n            query_cost: Triangle(0.003, 0.002),\n        });\n    }\n\n    Spec {\n        name: \"test_connpool_5\".into(),\n        desc: r#\"\n            This is a mixed test with pool max capacity set to 6. Requests in\n            the first group (0-5) come and go alternatively as time goes on,\n            even with different query cost, so its latency similarity doesn't\n            matter much, as far as the latency distribution is not too crazy\n            and unstable. However the second group (6-11) has a stable\n            environment - pressure from the first group is quite even at the\n            time the second group works. So we should observe a high similarity\n            in the second group. Also due to a low query cost, the second group\n            should have a higher priority in connection acquisition, therefore\n            a much lower latency distribution comparing to the first group.\n            Pool Mode wise, we should observe a transition from Mode A to C,\n            then D and eventually back to C. One regression to be aware of is\n            that, the last D->C transition should keep the pool running at\n            a full capacity.\n        \"#,\n        capacity: 6,\n        conn_cost: Triangle(0.15, 0.05),\n        score: vec![\n            Score::new(\n                0.05,\n                [2.0, 0.8, 0.4, 0.0],\n                LatencyDistribution { group: 0..6 },\n            ),\n            Score::new(\n                0.25,\n                [2.0, 0.8, 0.4, 0.0],\n                LatencyDistribution { group: 6..12 },\n            ),\n            Score::new(\n                0.45,\n                [1.0, 2.0, 5.0, 30.0],\n                LatencyRatio {\n                    percentile: 75,\n                    dividend: 0..6,\n                    divisor: 6..12,\n                },\n            ),\n            Score::new(0.15, [0.5, 0.2, 0.1, 0.0], ConnectionOverhead {}),\n            Score::new(0.10, [3.0, 4.0, 5.0, 6.0], EndingCapacity {}),\n        ],\n        dbs,\n        ..Default::default()\n    }\n}\n\nfn test_connpool_6() -> Spec {\n    let mut dbs = vec![];\n\n    for i in 0..6 {\n        dbs.push(DBSpec {\n            db: 0,\n            start_at: 0.0 + i as f64 / 10.0,\n            end_at: 0.5 + i as f64 / 10.0,\n            qps: 150,\n            query_cost: Triangle(0.020, 0.005),\n        });\n    }\n\n    Spec {\n        name: \"test_connpool_6\".into(),\n        desc: r#\"\n            This is a simple test for Mode A. In this case, we don't want to\n            have lots of reconnection overhead.\n        \"#,\n        capacity: 6,\n        conn_cost: Triangle(0.15, 0.05),\n        score: vec![Score::new(1.0, [0.5, 0.2, 0.1, 0.0], ConnectionOverhead {})],\n        dbs,\n        ..Default::default()\n    }\n}\n\nfn test_connpool_7() -> Spec {\n    Spec {\n        name: \"test_connpool_7\".into(),\n        desc: r#\"\n            The point of this test is to have one connection \"t1\" that\n            just has crazy demand for connections.  Then the \"t2\" connections\n            are infrequent -- so they have a miniscule quota.\n\n            Our goal is to make sure that \"t2\" has good QoS and gets\n            its queries processed as soon as they're submitted. Therefore,\n            \"t2\" should have way lower connection acquisition cost than \"t1\".\n        \"#,\n        capacity: 6,\n        conn_cost: Triangle(0.15, 0.05),\n        score: vec![\n            Score::new(\n                0.2,\n                [1.0, 10.0, 50.0, 100.0],\n                LatencyRatio {\n                    percentile: 99,\n                    dividend: 1..2,\n                    divisor: 2..3,\n                },\n            ),\n            Score::new(\n                0.4,\n                [1.0, 20.0, 100.0, 200.0],\n                LatencyRatio {\n                    percentile: 75,\n                    dividend: 1..2,\n                    divisor: 2..3,\n                },\n            ),\n            Score::new(0.4, [0.5, 0.2, 0.1, 0.0], ConnectionOverhead {}),\n        ],\n        dbs: vec![\n            DBSpec {\n                db: 1,\n                start_at: 0.0,\n                end_at: 1.0,\n                qps: 500,\n                query_cost: Triangle(0.040, 0.005),\n            },\n            DBSpec {\n                db: 2,\n                start_at: 0.1,\n                end_at: 0.3,\n                qps: 30,\n                query_cost: Triangle(0.030, 0.005),\n            },\n            DBSpec {\n                db: 2,\n                start_at: 0.6,\n                end_at: 0.9,\n                qps: 30,\n                query_cost: Triangle(0.010, 0.005),\n            },\n        ],\n        ..Default::default()\n    }\n}\n\nfn test_connpool_8() -> Spec {\n    let base_load = 200;\n\n    Spec {\n        name: \"test_connpool_8\".into(),\n        desc: r#\"\n            This test spec is to check the pool connection reusability with a\n            single block before the pool reaches its full capacity in Mode A.\n            We should observe just enough number of connects to serve the load,\n            while there can be very few disconnects because of GC.\n        \"#,\n        capacity: 100,\n        conn_cost: Triangle(0.0, 0.0),\n        score: vec![Score::new(1.0, [0.5, 0.2, 0.1, 0.0], ConnectionOverhead {})],\n        dbs: vec![\n            DBSpec {\n                db: 1,\n                start_at: 0.0,\n                end_at: 0.1,\n                qps: base_load / 4,\n                query_cost: Triangle(0.01, 0.0),\n            },\n            DBSpec {\n                db: 1,\n                start_at: 0.1,\n                end_at: 0.2,\n                qps: base_load / 2,\n                query_cost: Triangle(0.01, 0.0),\n            },\n            DBSpec {\n                db: 1,\n                start_at: 0.2,\n                end_at: 0.6,\n                qps: base_load,\n                query_cost: Triangle(0.01, 0.0),\n            },\n        ],\n        ..Default::default()\n    }\n}\n\nfn test_connpool_9() -> Spec {\n    let full_qps = 20000;\n\n    Spec {\n        name: \"test_connpool_9\".into(),\n        desc: r#\"\n            This test spec is to check the pool performance with low traffic\n            between 3 pre-heated blocks in Mode B. t1 is a reference block,\n            t2 has the same qps as t1, but t3 with doubled qps came in while t2\n            is active. As the total throughput is low enough, we shouldn't have\n            a lot of connects and disconnects, nor a high acquire waiting time.\n        \"#,\n        capacity: 100,\n        conn_cost: Triangle(0.01, 0.005),\n        score: vec![\n            Score::new(\n                0.1,\n                [2.0, 1.0, 0.5, 0.2],\n                LatencyDistribution { group: 1..4 },\n            ),\n            Score::new(\n                0.1,\n                [0.05, 0.004, 0.002, 0.001],\n                AbsoluteLatency {\n                    group: 1..4,\n                    percentile: 99,\n                },\n            ),\n            Score::new(\n                0.2,\n                [0.005, 0.0004, 0.0002, 0.0001],\n                AbsoluteLatency {\n                    group: 1..4,\n                    percentile: 75,\n                },\n            ),\n            Score::new(0.6, [0.5, 0.2, 0.1, 0.0], ConnectionOverhead {}),\n        ],\n        dbs: vec![\n            DBSpec {\n                db: 1,\n                start_at: 0.0,\n                end_at: 0.1,\n                qps: (full_qps / 32),\n                query_cost: Triangle(0.01, 0.005),\n            },\n            DBSpec {\n                db: 1,\n                start_at: 0.1,\n                end_at: 0.4,\n                qps: (full_qps / 16),\n                query_cost: Triangle(0.01, 0.005),\n            },\n            DBSpec {\n                db: 2,\n                start_at: 0.5,\n                end_at: 0.6,\n                qps: (full_qps / 32),\n                query_cost: Triangle(0.01, 0.005),\n            },\n            DBSpec {\n                db: 2,\n                start_at: 0.6,\n                end_at: 1.0,\n                qps: (full_qps / 16),\n                query_cost: Triangle(0.01, 0.005),\n            },\n            DBSpec {\n                db: 3,\n                start_at: 0.7,\n                end_at: 0.8,\n                qps: (full_qps / 16),\n                query_cost: Triangle(0.01, 0.005),\n            },\n            DBSpec {\n                db: 3,\n                start_at: 0.8,\n                end_at: 0.9,\n                qps: (full_qps / 8),\n                query_cost: Triangle(0.01, 0.005),\n            },\n        ],\n        ..Default::default()\n    }\n}\n\nfn test_connpool_10() -> Spec {\n    let full_qps = 2000;\n\n    Spec {\n        name: \"test_connpool_10\".into(),\n        desc: r#\"\n            This test spec is to check the pool garbage collection feature.\n            t1 is a constantly-running reference block, t2 starts in the middle\n            with a full qps and ends early to leave enough time for the pool to\n            execute garbage collection.\n        \"#,\n        timeout: 10,\n        duration: 2.0,\n        capacity: 100,\n        conn_cost: Triangle(0.01, 0.005),\n        score: vec![Score::new(\n            1.0,\n            [100.0, 40.0, 20.0, 10.0],\n            EndingCapacity {},\n        )],\n        dbs: vec![\n            DBSpec {\n                db: 1,\n                start_at: 0.0,\n                end_at: 1.0,\n                qps: (full_qps / 32),\n                query_cost: Triangle(0.01, 0.005),\n            },\n            DBSpec {\n                db: 2,\n                start_at: 0.4,\n                end_at: 0.6,\n                qps: ((full_qps / 32) * 31),\n                query_cost: Triangle(0.01, 0.005),\n            },\n        ],\n        ..Default::default()\n    }\n}\n\n#[cfg(test)]\n#[test_log::test(tokio::test(flavor = \"current_thread\", start_paused = true))]\nasync fn run_spec_tests() -> Result<()> {\n    let qos = spec_tests(None, &|_| true).await?;\n    eprintln!(\"QoS = {qos:?}\");\n    // assert!(qos.qos() > 85.0, \"Avg QoS failed: {}\", qos.qos());\n    // assert!(qos.qos_min() > 70.0, \"Min QoS failed: {}\", qos.qos_min());\n    Ok(())\n}\n\n#[cfg(test)]\nasync fn spec_tests(\n    scale: Option<f64>,\n    spec_predicate: &impl Fn(&'static str) -> bool,\n) -> Result<SuiteQoS> {\n    let mut results = SuiteQoS::default();\n    for (name, spec) in SPEC_FUNCTIONS {\n        if !spec_predicate(name) {\n            continue;\n        }\n        let mut spec = spec();\n        if let Some(scale) = scale {\n            spec.scale(scale);\n        }\n        let name = spec.name.clone();\n        let res = run(spec).await?;\n        results.insert(name, res);\n    }\n    for (name, QoS { qos, .. }) in &results {\n        info!(\"QoS[{name}] = [{qos:.02}]\");\n    }\n    info!(\n        \"QoS = [{:.02}] (rms={:.02})\",\n        results.qos(),\n        results.qos_rms_error()\n    );\n    Ok(results)\n}\n\n/// Runs the specs `count` times, returning the median run.\npub fn run_specs_tests_in_runtime(\n    count: usize,\n    scale: Option<f64>,\n    spec_predicate: &impl Fn(&'static str) -> bool,\n) -> Result<SuiteQoS> {\n    let mut handles = vec![];\n    for _ in 0..count {\n        let mut suite_handles = vec![];\n        for (name, spec) in SPEC_FUNCTIONS {\n            if !spec_predicate(name) {\n                continue;\n            }\n            let mut spec = spec();\n            if let Some(scale) = scale {\n                spec.scale(scale);\n            }\n            let h = std::thread::spawn(move || {\n                let runtime = tokio::runtime::Builder::new_current_thread()\n                    .enable_time()\n                    .build()\n                    .unwrap();\n                let _guard = runtime.enter();\n                tokio::time::pause();\n                let qos = runtime.block_on(run(spec))?;\n                Ok((name, qos))\n            });\n            suite_handles.push(h);\n        }\n        handles.push(suite_handles);\n    }\n    let mut runs = vec![];\n    for suite_handles in handles {\n        let mut suite = SuiteQoS::default();\n        for handle in suite_handles {\n            let (name, qos) = handle\n                .join()\n                .map_err(|e| anyhow::anyhow!(\"Thread failed: {e:?}\"))??;\n            suite.insert(name.into(), qos);\n        }\n        runs.push(suite)\n    }\n    runs.sort_by_cached_key(|run| (run.qos_rms_error() * 1_000_000.0) as usize);\n    let ret = runs.drain(count / 2..).next().unwrap();\n    Ok(ret)\n}\n\nmacro_rules! run_spec {\n    ($($spec:ident),* $(,)?) => {\n        const SPEC_FUNCTIONS: [(&'static str, fn() -> Spec); [$( $spec ),*].len()] = [\n            $(\n                (stringify!($spec), $spec),\n            )*\n        ];\n\n        #[cfg(test)]\n        mod spec {\n            use super::*;\n            $(\n                #[::test_log::test]\n                fn $spec() -> Result<()> {\n                    let runtime = tokio::runtime::Builder::new_current_thread()\n                        .enable_time()\n                        .build()\n                        .unwrap();\n                    let _guard = runtime.enter();\n                    tokio::time::pause();\n                    let qos = runtime.block_on(run(super::$spec()))?;\n                    eprintln!(\"QoS = {qos:?}\");\n                    // assert!(qos.qos > 80.0, \"QoS failed: {}\", qos.qos);\n                    Ok(())\n                }\n            )*\n        }\n    };\n}\n\nrun_spec!(\n    test_connpool_1,\n    test_connpool_2,\n    test_connpool_3,\n    test_connpool_4,\n    test_connpool_5,\n    test_connpool_6,\n    test_connpool_7,\n    test_connpool_8,\n    test_connpool_9,\n    test_connpool_10,\n);\n"
  },
  {
    "path": "rust/conn_pool/src/waitqueue.rs",
    "content": "use crate::time::Instant;\nuse scopeguard::defer;\nuse std::{\n    cell::{Cell, RefCell},\n    collections::VecDeque,\n    future::poll_fn,\n    rc::Rc,\n    task::{Poll, Waker},\n    time::Duration,\n};\nuse tracing::trace;\n\nstruct WaitObject {\n    waker: Waker,\n    woke: Cell<bool>,\n    gc: Cell<bool>,\n    when: Instant,\n}\n\n/// Maintains a list of waiters for a given object. Similar to tokio's `Notify`\n/// but explicitly not thread-safe.\npub struct WaitQueue {\n    waiters: RefCell<VecDeque<Rc<WaitObject>>>,\n    pub(crate) lock: Cell<usize>,\n}\n\nimpl Default for WaitQueue {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl WaitQueue {\n    pub fn new() -> Self {\n        Self {\n            waiters: RefCell::default(),\n            lock: Cell::default(),\n        }\n    }\n\n    pub fn trigger(&self) {\n        loop {\n            if let Some(front) = self.waiters.borrow_mut().pop_front() {\n                if front.gc.get() {\n                    trace!(\"Tossing away a GC'd entry\");\n                    continue;\n                }\n                trace!(\"Triggered a waiter\");\n                front.woke.set(true);\n                front.waker.wake_by_ref();\n            } else {\n                trace!(\"No waiters to trigger\");\n            }\n            break;\n        }\n    }\n\n    pub async fn queue(&self) {\n        trace!(\"Queueing\");\n        let waker = poll_fn(|cx| Poll::Ready(cx.waker().clone())).await;\n\n        let entry = Rc::new(WaitObject {\n            waker,\n            gc: Cell::default(),\n            woke: Cell::default(),\n            when: Instant::now(),\n        });\n\n        self.waiters.borrow_mut().push_back(entry.clone());\n\n        defer! {\n            entry.gc.set(true);\n        }\n\n        poll_fn(|_cx| {\n            if entry.woke.get() {\n                Poll::Ready(())\n            } else {\n                Poll::Pending\n            }\n        })\n        .await;\n    }\n\n    #[inline]\n    pub fn len(&self) -> usize {\n        self.lock.get()\n    }\n\n    #[inline]\n    pub fn is_empty(&self) -> bool {\n        self.len() == 0\n    }\n\n    pub(crate) fn lock(&self) {\n        self.lock.set(self.lock.get() + 1);\n    }\n\n    pub(crate) fn unlock(&self) {\n        self.lock.set(self.lock.get() - 1);\n    }\n\n    pub(crate) fn oldest(&self) -> Duration {\n        if let Some(entry) = self.waiters.borrow().front() {\n            entry.when.elapsed()\n        } else {\n            Duration::default()\n        }\n    }\n}\n"
  },
  {
    "path": "rust/gel-http/Cargo.toml",
    "content": "[package]\nname = \"gel-http\"\nversion = \"0.1.0\"\nlicense = \"MIT/Apache-2.0\"\nauthors = [\"MagicStack Inc. <hello@magic.io>\"]\nedition = \"2021\"\n\n[lints]\nworkspace = true\n\n[features]\npython_extension = [\"pyo3\"]\n\n[dependencies]\npyo3 = { workspace = true, optional = true }\ntokio.workspace = true\npyo3_util.workspace = true\ntracing.workspace = true\n\nscopeguard = \"1\"\neventsource-stream = \"0.2.3\"\nhttp-cache-semantics = { version = \"2\", features = [] }\nhttp = \"1\"\nhttp-body-util = \"0.1.2\"\nlru = \"0.16\"\nbytes = \"1\"\n\n# We want to use rustls to avoid setenv issues w/ OpenSSL and the system certs. As long\n# as we don't call `openssl_probe::*init*env*()` functions (functions that call setenv\n# in a thread-unsafe way), we should be fine.\n#\n# More details: https://github.com/edgedb/edgedb/pull/8201\n#\n# We add these features:\n# - http2: to use HTTP/2\n# - charset: to support charset encoding\n# - gzip/deflate/brotli: to support compression\n# - stream: to support streaming responses\n# - rustls-tls-native-roots: to use the native root certificates (rather than WebPKI)\nreqwest = { version = \"0.12\", default-features = false, features = [\"http2\", \"charset\", \"gzip\", \"deflate\", \"brotli\", \"stream\", \"rustls-tls-native-roots\"] }\nfutures = \"0.3\"\n\n[dev-dependencies]\ntokio = { workspace = true, features = [\"test-util\"] }\nrstest = \"0.26\"\n\n[lib]\n"
  },
  {
    "path": "rust/gel-http/src/cache.rs",
    "content": "use bytes::Bytes;\nuse http::{HeaderMap, Method, Request, Response, Uri};\nuse http_cache_semantics::{AfterResponse, BeforeRequest, CacheOptions, CachePolicy};\nuse lru::LruCache;\nuse std::{\n    num::NonZero,\n    sync::{Arc, Mutex},\n    time::{Duration, SystemTime},\n};\n\n#[derive(Debug)]\npub enum CacheBefore {\n    /// The cache is stale or missing, so we need to make a request. Note that CachedItem\n    /// may contain a stale cache item and must be returned in `after_request`.\n    Request(http::Request<Vec<u8>>, CachedItem),\n    /// The cache is fresh, so we can return the response\n    Response(http::Response<Bytes>),\n}\n\n#[derive(Debug)]\npub struct CachedItem {\n    maybe_item: Option<(Arc<CachePolicy>, Bytes)>,\n}\n\nimpl CachedItem {\n    fn none() -> Self {\n        Self { maybe_item: None }\n    }\n\n    fn some(policy: Arc<CachePolicy>, body: Bytes) -> Self {\n        Self {\n            maybe_item: Some((policy, body)),\n        }\n    }\n}\n\nstruct CacheItems<T> {\n    items: LruCache<Uri, (T, Bytes)>,\n    byte_size: usize,\n    max_byte_size: usize,\n}\n\nimpl<T> CacheItems<T> {\n    fn new(capacity: NonZero<usize>, max_byte_size: usize) -> Self {\n        Self {\n            items: LruCache::new(capacity),\n            byte_size: 0,\n            max_byte_size,\n        }\n    }\n\n    fn insert(&mut self, uri: Uri, policy: T, body: Bytes) {\n        if self.items.is_empty() {\n            debug_assert!(self.byte_size == 0);\n        }\n        let body_len = body.len();\n        if let Some((_, old_body)) = self.items.push(uri, (policy, body)) {\n            self.byte_size = self.byte_size.saturating_sub(old_body.1.len());\n        }\n        self.byte_size = self.byte_size.saturating_add(body_len);\n        while self.byte_size > self.max_byte_size {\n            if let Some((_, old_body)) = self.items.pop_lru() {\n                self.byte_size = self.byte_size.saturating_sub(old_body.1.len());\n            } else {\n                return;\n            }\n        }\n    }\n\n    fn get(&mut self, uri: &Uri) -> Option<&(T, Bytes)> {\n        self.items.get(uri)\n    }\n\n    fn get_mut(&mut self, uri: &Uri) -> Option<(&mut T, &Bytes)> {\n        let entry = self.items.get_mut(uri)?;\n        Some((&mut entry.0, &entry.1))\n    }\n}\n\n#[derive(Clone)]\npub struct Cache {\n    cache_options: CacheOptions,\n    cache: Arc<Mutex<CacheItems<Arc<CachePolicy>>>>,\n}\n\nimpl Cache {\n    pub fn new() -> Self {\n        Self {\n            cache_options: CacheOptions {\n                shared: false,\n                // Immutable objects should be cached for 24 hours\n                immutable_min_time_to_live: Duration::from_secs(86_400),\n                ..Default::default()\n            },\n            cache: Arc::new(Mutex::new(CacheItems::new(\n                NonZero::new(100).unwrap(),\n                10 * 1024 * 1024,\n            ))),\n        }\n    }\n\n    #[cfg(test)]\n    pub fn get_cache_body(&self, url: &Uri) -> Option<(bool, Vec<u8>)> {\n        let mut cache = self.cache.lock().unwrap();\n        let entry = cache.get(url);\n        if let Some((policy, body)) = entry {\n            let state = policy.is_stale(SystemTime::now());\n            // NOTE: inefficient, for testing only\n            return Some((state, body.to_vec()));\n        }\n        None\n    }\n\n    #[cfg(test)]\n    pub fn clear(&self) {\n        let mut cache = self.cache.lock().unwrap();\n        cache.items.clear();\n        cache.byte_size = 0;\n    }\n\n    pub fn before_request(\n        &self,\n        allow_cache: bool,\n        method: &Method,\n        url: &Uri,\n        headers: &HeaderMap,\n        body: Vec<u8>,\n    ) -> CacheBefore {\n        let mut req = Request::new(body);\n        *req.method_mut() = method.clone();\n        *req.uri_mut() = url.clone();\n        *req.headers_mut() = headers.clone();\n\n        // Only cache GET requests\n        if !allow_cache || method != Method::GET {\n            return CacheBefore::Request(req, CachedItem::none());\n        }\n\n        let now = SystemTime::now();\n        let mut cache = self.cache.lock().unwrap();\n        if let Some((policy, body)) = cache.get(url) {\n            match policy.before_request(&req, now) {\n                BeforeRequest::Fresh(parts) => {\n                    // Fresh response from cache\n                    CacheBefore::Response(Response::from_parts(parts, body.clone()))\n                }\n                BeforeRequest::Stale { request, .. } => {\n                    // Stale response, return the cached item that we need to revalidate\n                    *req.uri_mut() = request.uri;\n                    *req.headers_mut() = request.headers;\n                    *req.method_mut() = request.method;\n                    CacheBefore::Request(req, CachedItem::some(policy.clone(), body.clone()))\n                }\n            }\n        } else {\n            CacheBefore::Request(req, CachedItem::none())\n        }\n    }\n\n    pub fn after_request(\n        &self,\n        allow_cache: bool,\n        method: Method,\n        uri: Uri,\n        headers: HeaderMap,\n        res: &mut http::Response<Bytes>,\n        cached_item: CachedItem,\n    ) {\n        // Only cache GET requests\n        if !allow_cache || method != Method::GET {\n            return;\n        }\n\n        let now = SystemTime::now();\n        let mut cache = self.cache.lock().unwrap();\n\n        let mut req = Request::new(());\n        *req.method_mut() = method;\n        *req.uri_mut() = uri.clone();\n        *req.headers_mut() = headers;\n\n        if let Some((policy, body)) = cached_item.maybe_item {\n            let entry = cache.get_mut(&uri);\n            let parts = match policy.after_response(&req, res, now) {\n                AfterResponse::NotModified(new_policy, parts) => {\n                    // Not modified, return the cached response and update the\n                    // policy and response body\n                    body.clone_into(res.body_mut());\n\n                    // TODO: Can new_policy.is_storable() be false?\n                    if let Some((old_policy, _)) = entry {\n                        *old_policy = Arc::new(new_policy);\n                    } else {\n                        cache.insert(uri, Arc::new(new_policy), body);\n                    }\n                    parts\n                }\n                AfterResponse::Modified(new_policy, parts) => {\n                    // Modified, update the cache, but not the response body\n\n                    // TODO: Can new_policy.is_storable() be false?\n                    cache.insert(uri, Arc::new(new_policy), res.body().clone());\n                    parts\n                }\n            };\n            *res.headers_mut() = parts.headers;\n            *res.status_mut() = parts.status;\n            *res.version_mut() = parts.version;\n            *res.extensions_mut() = parts.extensions;\n        } else {\n            // We had no cached item at the start, but we might have raced another\n            let policy = CachePolicy::new_options(&req, res, now, self.cache_options);\n            if policy.is_storable() {\n                cache.insert(uri, Arc::new(policy), res.body().clone());\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use http::*;\n    use std::str::FromStr;\n\n    use super::*;\n\n    fn get_google() -> (Method, Uri, HeaderMap, Vec<u8>) {\n        let method = Method::GET;\n        let uri = Uri::from_str(\"https://www.google.com\").unwrap();\n        let headers = HeaderMap::new();\n        let body = vec![];\n        (method, uri, headers, body)\n    }\n\n    fn cache_control<T>(resp: &mut Response<T>, value: &str) {\n        resp.headers_mut().insert(\n            HeaderName::from_static(\"cache-control\"),\n            HeaderValue::from_str(value).unwrap(),\n        );\n    }\n\n    fn etag<T>(resp: &mut Response<T>, value: &str) {\n        resp.headers_mut().insert(\n            HeaderName::from_static(\"etag\"),\n            HeaderValue::from_str(value).unwrap(),\n        );\n    }\n\n    fn response(status: StatusCode, body: &str) -> Response<Bytes> {\n        let mut resp = Response::new(body.as_bytes().to_vec().into());\n        *resp.status_mut() = status;\n        resp\n    }\n\n    #[test]\n    fn test_cache_byte_size_eviction() {\n        let mut cache_items = CacheItems::<()>::new(NonZero::new(100).unwrap(), 1024 * 1024);\n        cache_items.insert(\n            Uri::from_str(\"https://www.google.com\").unwrap(),\n            (),\n            vec![0; 1024 * 1024].into(),\n        );\n        assert_eq!(cache_items.byte_size, 1024 * 1024);\n        assert_eq!(cache_items.items.len(), 1);\n        cache_items.insert(\n            Uri::from_str(\"https://www.example.com\").unwrap(),\n            (),\n            vec![0; 1].into(),\n        );\n        assert_eq!(cache_items.byte_size, 1);\n        assert_eq!(cache_items.items.len(), 1);\n        cache_items.insert(\n            Uri::from_str(\"https://www.google.com\").unwrap(),\n            (),\n            vec![0; 1024 * 1024].into(),\n        );\n        assert_eq!(cache_items.byte_size, 1024 * 1024);\n        assert_eq!(cache_items.items.len(), 1);\n    }\n\n    #[test]\n    fn test_cache_capacity_eviction() {\n        let mut cache_items = CacheItems::<()>::new(NonZero::new(100).unwrap(), 1024 * 1024);\n        for i in 0..120 {\n            cache_items.insert(\n                Uri::from_str(&format!(\"https://www.example.com/{i}\")).unwrap(),\n                (),\n                vec![0; 10].into(),\n            );\n        }\n        assert_eq!(cache_items.byte_size, 1000);\n        assert_eq!(cache_items.items.len(), 100);\n    }\n\n    #[test]\n    fn test_cache() {\n        let cache = Cache::new();\n        let (method, uri, headers, body) = get_google();\n        let before = cache.before_request(true, &method, &uri, &headers, body);\n        let CacheBefore::Request(req, cached_item) = before else {\n            panic!(\"Expected a request {before:?}\");\n        };\n\n        assert_eq!(req.method(), &method);\n        assert_eq!(req.uri(), &uri);\n        assert_eq!(req.headers(), &headers);\n        assert!(cached_item.maybe_item.is_none());\n\n        let mut resp = response(StatusCode::OK, \"this is a response\");\n        cache_control(&mut resp, \"max-age=3600\");\n        etag(&mut resp, \"\\\"1234567890\\\"\");\n        cache.after_request(\n            true,\n            method.clone(),\n            uri.clone(),\n            headers.clone(),\n            &mut resp,\n            cached_item,\n        );\n\n        let (method, uri, headers, body) = get_google();\n        let after = cache.before_request(true, &method, &uri, &headers, body);\n\n        let CacheBefore::Response(resp) = after else {\n            panic!(\"Expected a response {after:?}\");\n        };\n        assert_eq!(resp.status(), StatusCode::OK);\n        assert_eq!(resp.body(), \"this is a response\".as_bytes());\n        assert_eq!(\n            resp.headers().get(\"etag\"),\n            Some(&HeaderValue::from_str(\"\\\"1234567890\\\"\").unwrap())\n        );\n        assert_eq!(\n            resp.headers().get(\"cache-control\"),\n            Some(&HeaderValue::from_str(\"max-age=3600\").unwrap())\n        );\n    }\n\n    #[test]\n    fn test_cache_not_modified() {\n        let cache = Cache::new();\n\n        // First request, clean cache\n\n        let (method, uri, headers, body) = get_google();\n        let before = cache.before_request(true, &method, &uri, &headers, body);\n        let CacheBefore::Request(req, cached_item) = before else {\n            panic!(\"Expected a request {before:?}\");\n        };\n\n        assert_eq!(req.method(), &method);\n        assert_eq!(req.uri(), &uri);\n        assert_eq!(req.headers(), &headers);\n        assert!(cached_item.maybe_item.is_none());\n\n        let mut resp = response(StatusCode::OK, \"contents!\");\n        cache_control(\n            &mut resp,\n            \"max-age=0, must-revalidate, stale-while-revalidate=86400\",\n        );\n        etag(&mut resp, \"\\\"1234567890\\\"\");\n        cache.after_request(\n            true,\n            method.clone(),\n            uri.clone(),\n            headers.clone(),\n            &mut resp,\n            cached_item,\n        );\n\n        let (state, body) = cache.get_cache_body(&uri).unwrap();\n        assert!(state);\n        assert_eq!(body, \"contents!\".as_bytes());\n\n        // Must-revalidate, so we need to revalidate the cache\n\n        let (method, uri, headers, body) = get_google();\n        let after = cache.before_request(true, &method, &uri, &headers, body);\n        let CacheBefore::Request(req, cached_item) = after else {\n            panic!(\"Expected a request {after:?}\");\n        };\n        assert_eq!(req.method(), &Method::GET);\n        assert_eq!(req.uri(), &uri);\n        assert_eq!(\n            req.headers().get(\"if-none-match\"),\n            Some(&HeaderValue::from_str(\"\\\"1234567890\\\"\").unwrap())\n        );\n\n        let mut resp = response(StatusCode::NOT_MODIFIED, \"\");\n        cache_control(\n            &mut resp,\n            \"max-age=0, must-revalidate, stale-while-revalidate=86400\",\n        );\n        etag(&mut resp, \"\\\"1234567890\\\"\");\n        cache.after_request(\n            true,\n            method.clone(),\n            uri.clone(),\n            headers.clone(),\n            &mut resp,\n            cached_item,\n        );\n\n        let (state, body) = cache.get_cache_body(&uri).unwrap();\n        assert!(state);\n        assert_eq!(body, \"contents!\".as_bytes());\n\n        // Test what happens when the cache evicts during the request\n\n        let (method, uri, headers, body) = get_google();\n        let after = cache.before_request(true, &method, &uri, &headers, body);\n        let CacheBefore::Request(req, cached_item) = after else {\n            panic!(\"Expected a request {after:?}\");\n        };\n        assert_eq!(req.method(), &Method::GET);\n        assert_eq!(req.uri(), &uri);\n        assert_eq!(\n            req.headers().get(\"if-none-match\"),\n            Some(&HeaderValue::from_str(\"\\\"1234567890\\\"\").unwrap())\n        );\n\n        cache.clear();\n\n        let mut resp = response(StatusCode::NOT_MODIFIED, \"\");\n        cache_control(\n            &mut resp,\n            \"max-age=0, must-revalidate, stale-while-revalidate=86400\",\n        );\n        etag(&mut resp, \"\\\"1234567890\\\"\");\n        cache.after_request(\n            true,\n            method.clone(),\n            uri.clone(),\n            headers.clone(),\n            &mut resp,\n            cached_item,\n        );\n\n        assert_eq!(resp.body(), \"contents!\".as_bytes());\n\n        let (state, body) = cache.get_cache_body(&uri).unwrap();\n        assert!(state);\n        assert_eq!(body, \"contents!\".as_bytes());\n    }\n}\n"
  },
  {
    "path": "rust/gel-http/src/lib.rs",
    "content": "mod cache;\n#[cfg(feature = \"python_extension\")]\npub mod python;\n"
  },
  {
    "path": "rust/gel-http/src/python.rs",
    "content": "use bytes::Bytes;\nuse eventsource_stream::Eventsource;\nuse futures::TryStreamExt;\nuse http::{HeaderMap, HeaderName, HeaderValue, Uri};\nuse http_body_util::BodyExt;\nuse pyo3::{\n    exceptions::{PyException, PyValueError},\n    prelude::*,\n    types::PyByteArray,\n};\nuse pyo3_util::{\n    channel::{new_python_channel, PythonChannel, PythonChannelImpl, RustChannel},\n    logging::{get_python_logger_level, initialize_logging_in_thread},\n};\nuse reqwest::Method;\nuse scopeguard::{defer, guard, ScopeGuard};\nuse std::{\n    collections::HashMap,\n    rc::Rc,\n    str::FromStr,\n    sync::{Arc, Mutex},\n    thread,\n    time::Duration,\n};\nuse tokio::{\n    sync::{AcquireError, Semaphore, SemaphorePermit},\n    task::{JoinHandle, LocalSet},\n};\nuse tracing::{info, trace};\n\nuse crate::cache::{Cache, CacheBefore};\n\npyo3::create_exception!(_http, InternalError, PyException);\n\n/// The backlog for SSE message\nconst SSE_QUEUE_SIZE: usize = 100;\n\ntype PythonConnId = u64;\n\ntype RpcPipe = RustChannel<PythonToRustMessage, RustToPythonMessage>;\n\n#[derive(Debug)]\nenum RustToPythonMessage {\n    Response(PythonConnId, (u16, Bytes, HashMap<String, String>)),\n    SSEStart(PythonConnId, (u16, HashMap<String, String>)),\n    SSEEvent(PythonConnId, eventsource_stream::Event),\n    SSEEnd(PythonConnId),\n    Error(PythonConnId, String),\n}\n\nimpl<'py> IntoPyObject<'py> for RustToPythonMessage {\n    type Target = PyAny;\n    type Output = Bound<'py, PyAny>;\n    type Error = PyErr;\n\n    fn into_pyobject(self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {\n        use RustToPythonMessage::*;\n        let res = match self {\n            Error(conn, error) => (0, conn, error).into_pyobject(py),\n            Response(conn, (status, body, headers)) => {\n                (1, conn, (status, PyByteArray::new(py, &body), headers)).into_pyobject(py)\n            }\n            SSEStart(conn, (status, headers)) => (2, conn, (status, headers)).into_pyobject(py),\n            SSEEvent(conn, message) => {\n                (3, conn, (&message.id, &message.data, &message.event)).into_pyobject(py)\n            }\n            SSEEnd(conn) => (4, conn, ()).into_pyobject(py),\n        }?;\n        Ok(res.into_any())\n    }\n}\n\n#[derive(Debug)]\nenum PythonToRustMessage {\n    /// Update the inflight limit\n    UpdateLimit(usize),\n    /// Perform a request\n    Request(\n        PythonConnId,\n        String,\n        String,\n        Vec<u8>,\n        Vec<(String, String)>,\n        bool,\n    ),\n    /// Perform a request with SSE\n    RequestSse(PythonConnId, String, String, Vec<u8>, Vec<(String, String)>),\n    /// Close an SSE connection\n    Close(PythonConnId),\n    /// Acknowledge an SSE message\n    Ack(PythonConnId),\n}\n\nimpl<'py> FromPyObject<'py> for PythonToRustMessage {\n    fn extract_bound(_: &Bound<'py, PyAny>) -> PyResult<Self> {\n        // Unused for this class\n        Err(PyValueError::new_err(\"Not implemented\"))\n    }\n}\n\n/// If this is likely a stream, returns the `Stream` variant.\n/// Otherwise, returns the `Bytes` variant.\nenum MaybeResponse {\n    Bytes(Bytes),\n    Stream(reqwest::Body),\n}\n\nimpl MaybeResponse {\n    async fn try_into_bytes(self) -> Result<Bytes, String> {\n        match self {\n            MaybeResponse::Bytes(bytes) => Ok(bytes),\n            MaybeResponse::Stream(body) => Ok(http_body_util::BodyExt::collect(body)\n                .await\n                .map_err(|e| format!(\"Failed to read response body: {e:?}\"))?\n                .to_bytes()\n                .to_vec()\n                .into()),\n        }\n    }\n}\n\nasync fn request(\n    client: reqwest::Client,\n    url: String,\n    method: String,\n    body: Vec<u8>,\n    headers: Vec<(String, String)>,\n    allow_cache: bool,\n    cache: Cache,\n) -> Result<http::Response<MaybeResponse>, String> {\n    let headers = parse_headers(headers)?;\n    let method =\n        Method::from_bytes(method.as_bytes()).map_err(|e| format!(\"Invalid HTTP method: {e:?}\"))?;\n    let uri = Uri::from_str(&url).map_err(|e| format!(\"Invalid URL: {e:?}\"))?;\n\n    let (req, cached_item) = match cache.before_request(allow_cache, &method, &uri, &headers, body)\n    {\n        CacheBefore::Request(req, cached_item) => (req, cached_item),\n        CacheBefore::Response(resp) => {\n            return Ok(resp.map(MaybeResponse::Bytes));\n        }\n    };\n\n    let resp = client\n        .execute(\n            req.try_into()\n                .map_err(|e| format!(\"Invalid request: {e:?}\"))?,\n        )\n        .await\n        .map_err(|e| format!(\"Request failed: {e:?}\"))?;\n    let resp: http::Response<_> = resp.into();\n\n    let content_type = resp.headers().get(\"content-type\");\n    let is_event_stream = content_type\n        .and_then(|v| v.to_str().ok())\n        .map(|s| s.starts_with(\"text/event-stream\"))\n        .unwrap_or(false);\n\n    let mut resp = if is_event_stream {\n        return Ok(resp.map(MaybeResponse::Stream));\n    } else {\n        let (parts, body) = resp.into_parts();\n        let bytes = http_body_util::BodyExt::collect(body)\n            .await\n            .map_err(|e| format!(\"Failed to read response body: {e:?}\"))?\n            .to_bytes();\n        http::Response::from_parts(parts, bytes)\n    };\n\n    cache.after_request(allow_cache, method, uri, headers, &mut resp, cached_item);\n\n    Ok(resp.map(MaybeResponse::Bytes))\n}\n\nfn parse_headers(headers: Vec<(String, String)>) -> Result<HeaderMap, String> {\n    let mut header_map = HeaderMap::new();\n    for (key, value) in headers {\n        header_map.insert(\n            HeaderName::from_str(&key).map_err(|e| format!(\"Invalid header name: {e:?}\"))?,\n            HeaderValue::from_str(&value).map_err(|e| format!(\"Invalid header value: {e:?}\"))?,\n        );\n    }\n    Ok(header_map)\n}\n\nasync fn request_bytes(\n    client: reqwest::Client,\n    url: String,\n    method: String,\n    body: Vec<u8>,\n    headers: Vec<(String, String)>,\n    allow_cache: bool,\n    cache: Cache,\n) -> Result<(http::StatusCode, Bytes, HashMap<String, String>), String> {\n    let (parts, body) = request(client, url, method, body, headers, allow_cache, cache)\n        .await?\n        .into_parts();\n    let status = parts.status;\n    let headers = process_headers(&parts.headers);\n    let body = body.try_into_bytes().await?;\n\n    Ok((status, body, headers))\n}\n\n#[allow(clippy::too_many_arguments)]\nasync fn request_sse(\n    client: reqwest::Client,\n    id: PythonConnId,\n    backpressure: Arc<Semaphore>,\n    url: String,\n    method: String,\n    body: Vec<u8>,\n    headers: Vec<(String, String)>,\n    rpc_pipe: Rc<RpcPipe>,\n    cache: Cache,\n) -> Result<(), String> {\n    trace!(\"Entering SSE\");\n    let guard = guard((), |_| trace!(\"Exiting SSE due to cancellation\"));\n    let (parts, body) = request(client, url, method, body, headers, false, cache)\n        .await?\n        .into_parts();\n\n    let mut stream = match body {\n        MaybeResponse::Bytes(bytes) => {\n            let headers = process_headers(&parts.headers);\n            let status = parts.status;\n            let body = bytes;\n            _ = rpc_pipe\n                .write(RustToPythonMessage::Response(\n                    id,\n                    (status.as_u16(), body, headers),\n                ))\n                .await;\n\n            trace!(\"Exiting SSE due to non-SSE response\");\n            ScopeGuard::into_inner(guard);\n            return Ok(());\n        }\n        MaybeResponse::Stream(body) => body.into_data_stream().eventsource(),\n    };\n\n    let headers = process_headers(&parts.headers);\n    let status = parts.status;\n    _ = rpc_pipe\n        .write(RustToPythonMessage::SSEStart(\n            id,\n            (status.as_u16(), headers.clone()),\n        ))\n        .await;\n\n    loop {\n        let chunk = match stream.try_next().await {\n            Ok(None) => break,\n            Ok(Some(chunk)) => chunk,\n            Err(e) => {\n                return Err(format!(\"Failed to read response body: {e:?}\"));\n            }\n        };\n\n        // Note that we use semaphores here in a strange way, but basically we\n        // want to have per-stream backpressure to avoid buffering messages\n        // indefinitely.\n        let Ok(permit) = backpressure.acquire().await else {\n            break;\n        };\n        permit.forget();\n\n        if rpc_pipe\n            .write(RustToPythonMessage::SSEEvent(id, chunk))\n            .await\n            .is_err()\n        {\n            break;\n        }\n    }\n\n    trace!(\"Exiting SSE\");\n    ScopeGuard::into_inner(guard);\n    Ok(())\n}\n\nfn process_headers(headers: &HeaderMap) -> HashMap<String, String> {\n    headers\n        .iter()\n        .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or(\"\").to_string()))\n        .collect()\n}\n\n#[derive(Debug, Clone, Copy)]\nstruct PermitCount {\n    active: usize,\n    capacity: usize,\n    #[cfg(debug_assertions)]\n    waiting: usize,\n}\n\n/// By default, the [`Semaphore`] does not allow for releasing permits that are currently\n/// outstanding. This allows us to keep a count of what the limit _should_ be, which we'll\n/// target if we've taken too many permits out by forgetting outstanding permits.\nstruct PermitManager {\n    counts: Mutex<PermitCount>,\n    semaphore: Semaphore,\n}\n\nimpl PermitManager {\n    fn new(capacity: usize) -> Self {\n        Self {\n            counts: Mutex::new(PermitCount {\n                active: 0,\n                capacity,\n                #[cfg(debug_assertions)]\n                waiting: 0,\n            }),\n            semaphore: Semaphore::new(capacity),\n        }\n    }\n\n    async fn acquire<'a>(\n        &'a self,\n    ) -> Result<ScopeGuard<SemaphorePermit<'a>, impl FnOnce(SemaphorePermit<'a>) + 'a>, AcquireError>\n    {\n        #[cfg(debug_assertions)]\n        {\n            let mut counts = self.counts.lock().unwrap();\n            counts.waiting += 1;\n            drop(counts);\n        }\n        let permit = self.semaphore.acquire().await?;\n        let mut counts = self.counts.lock().unwrap();\n        counts.active += 1;\n        #[cfg(debug_assertions)]\n        {\n            counts.waiting -= 1;\n        }\n        drop(counts);\n        self.assert_valid();\n        Ok(scopeguard::guard(permit, |permit| self.release(permit)))\n    }\n\n    fn release(&self, permit: SemaphorePermit<'_>) {\n        let mut counts = self.counts.lock().unwrap();\n        if counts.active > counts.capacity {\n            // We have too many permits, so forget this one instead of releasing\n            permit.forget();\n        } else {\n            // Normal release\n            drop(permit);\n        }\n        counts.active -= 1;\n        drop(counts);\n        self.assert_valid();\n    }\n\n    #[allow(clippy::comparison_chain)]\n    fn update_limit(&self, new_limit: usize) {\n        let mut counts = self.counts.lock().unwrap();\n        let old_capacity = counts.capacity;\n        counts.capacity = new_limit;\n\n        if new_limit > old_capacity {\n            // We may be oversubscribed on permits right now. If so, we want to add\n            // enough permits to cover the current deficit only.\n            let added = new_limit.saturating_sub(counts.active.max(old_capacity));\n            self.semaphore.add_permits(added);\n        } else if new_limit < old_capacity {\n            // This may not be able to forget all of the permits, as some may be\n            // active. That's OK, because we'll fix it in `release`.\n            let removed = old_capacity - new_limit;\n            self.semaphore.forget_permits(removed);\n        }\n\n        drop(counts);\n        self.assert_valid();\n    }\n\n    #[cfg(test)]\n    fn active(&self) -> usize {\n        let counts = self.counts.lock().unwrap();\n        counts.active\n    }\n\n    #[cfg(test)]\n    fn capacity(&self) -> usize {\n        let counts = self.counts.lock().unwrap();\n        counts.capacity\n    }\n\n    fn assert_valid(&self) {\n        #[cfg(debug_assertions)]\n        {\n            let count_lock = self.counts.lock().unwrap();\n            let available = self.semaphore.available_permits();\n            let counts = *count_lock;\n            drop(count_lock);\n\n            if counts.active > counts.capacity {\n                // Oversubscribed\n                assert!(available == 0);\n            } else {\n                // Not oversubscribed, but we can only validate these if nothing is waiting\n                if counts.waiting == 0 {\n                    assert!(\n                        available + counts.waiting == counts.capacity - counts.active,\n                        \"{} + {} == {} - {}\",\n                        available,\n                        counts.waiting,\n                        counts.capacity,\n                        counts.active\n                    );\n                    assert!(counts.active <= counts.capacity);\n                }\n            }\n        }\n    }\n}\n\nstruct HttpTask {\n    task: JoinHandle<()>,\n    backpressure: Arc<Semaphore>,\n}\n\nasync fn run_and_block(capacity: usize, rpc_pipe: RpcPipe) {\n    let rpc_pipe = Rc::new(rpc_pipe);\n\n    const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);\n    const POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(30);\n    const STANDARD_READ_TIMEOUT: Duration = Duration::from_secs(10);\n    const STANDARD_TOTAL_TIMEOUT: Duration = Duration::from_secs(120);\n    const SSE_READ_TIMEOUT: Duration = Duration::from_secs(60 * 60); // 1 hour\n\n    // Set some reasonable defaults for timeouts\n    let client = reqwest::Client::builder()\n        .connect_timeout(CONNECT_TIMEOUT)\n        .timeout(STANDARD_TOTAL_TIMEOUT)\n        .read_timeout(STANDARD_READ_TIMEOUT)\n        .pool_idle_timeout(POOL_IDLE_TIMEOUT);\n    let client = client.build().unwrap();\n\n    // SSE requests should have a very long read timeout and no general timeout\n    let client_sse = reqwest::Client::builder()\n        .connect_timeout(CONNECT_TIMEOUT)\n        .read_timeout(SSE_READ_TIMEOUT)\n        .pool_idle_timeout(POOL_IDLE_TIMEOUT);\n    let client_sse = client_sse.build().unwrap();\n\n    let cache = Cache::new();\n\n    let permit_manager = Rc::new(PermitManager::new(capacity));\n    let tasks = Arc::new(Mutex::new(HashMap::<PythonConnId, HttpTask>::new()));\n\n    loop {\n        let Some(rpc) = rpc_pipe.recv().await else {\n            info!(\"Http shutting down\");\n            break;\n        };\n        let client = client.clone();\n        let client_sse = client_sse.clone();\n        trace!(\"Received RPC: {rpc:?}\");\n        let rpc_pipe = rpc_pipe.clone();\n        // Allocate a task ID and backpressure object if we're initiating a\n        // request. This would be less awkward if we allocated in the Rust side\n        // of the code rather than the Python side.\n        let (id, backpressure) = match rpc {\n            PythonToRustMessage::Request(id, ..) | PythonToRustMessage::RequestSse(id, ..) => {\n                (Some(id), Some(Semaphore::new(SSE_QUEUE_SIZE).into()))\n            }\n            _ => (None, None),\n        };\n        let task = tokio::task::spawn_local(execute(\n            id,\n            backpressure.clone(),\n            tasks.clone(),\n            rpc,\n            permit_manager.clone(),\n            client,\n            client_sse,\n            rpc_pipe,\n            cache.clone(),\n        ));\n        if let (Some(id), Some(backpressure)) = (id, backpressure) {\n            tasks\n                .lock()\n                .unwrap()\n                .insert(id, HttpTask { task, backpressure });\n        }\n    }\n}\n\n#[allow(clippy::too_many_arguments)]\nasync fn execute(\n    id: Option<u64>,\n    backpressure: Option<Arc<Semaphore>>,\n    tasks_clone: Arc<Mutex<HashMap<u64, HttpTask>>>,\n    rpc: PythonToRustMessage,\n    permit_manager: Rc<PermitManager>,\n    client: reqwest::Client,\n    client_sse: reqwest::Client,\n    rpc_pipe: Rc<RpcPipe>,\n    cache: Cache,\n) {\n    // If a request task was booted by this request, remove it from the list of\n    // tasks when we exit.\n    if let Some(id) = id {\n        defer!(_ = tasks_clone.lock().unwrap().remove(&id));\n    }\n\n    use PythonToRustMessage::*;\n    match rpc {\n        UpdateLimit(limit) => {\n            permit_manager.update_limit(limit);\n        }\n        Request(id, url, method, body, headers, allow_cache) => {\n            let Ok(permit) = permit_manager.acquire().await else {\n                return;\n            };\n            match request_bytes(client, url, method, body, headers, allow_cache, cache).await {\n                Ok((status, body, headers)) => {\n                    _ = rpc_pipe\n                        .write(RustToPythonMessage::Response(\n                            id,\n                            (status.as_u16(), body, headers),\n                        ))\n                        .await;\n                }\n                Err(err) => {\n                    _ = rpc_pipe.write(RustToPythonMessage::Error(id, err)).await;\n                }\n            }\n            drop(permit);\n        }\n        RequestSse(id, url, method, body, headers) => {\n            // Ensure we send the end message whenever this block exits (though\n            // we need to spawn a task to do so)\n            defer!({\n                let rpc_pipe = rpc_pipe.clone();\n                let future = async move { rpc_pipe.write(RustToPythonMessage::SSEEnd(id)).await };\n                tokio::task::spawn_local(future);\n            });\n            let Ok(permit) = permit_manager.acquire().await else {\n                return;\n            };\n            match request_sse(\n                client_sse,\n                id,\n                backpressure.unwrap(),\n                url,\n                method,\n                body,\n                headers,\n                rpc_pipe.clone(),\n                cache,\n            )\n            .await\n            {\n                Ok(..) => {}\n                Err(err) => {\n                    _ = rpc_pipe\n                        .write(RustToPythonMessage::Error(id, format!(\"SSE error: {err}\")))\n                        .await;\n                }\n            }\n            drop(permit);\n        }\n        Ack(id) => {\n            let lock = tasks_clone.lock().unwrap();\n            if let Some(task) = lock.get(&id) {\n                task.backpressure.add_permits(1);\n            }\n        }\n        Close(id) => {\n            let Some(task) = tasks_clone.lock().unwrap().remove(&id) else {\n                return;\n            };\n            task.task.abort();\n        }\n    }\n}\n\n#[pyclass]\nstruct Http {\n    python: Arc<PythonChannelImpl<PythonToRustMessage, RustToPythonMessage>>,\n}\n\n#[pymethods]\nimpl Http {\n    /// Create the HTTP pool and automatically boot a tokio runtime on a\n    /// new thread. When this class is GC'd, the thread will be torn down.\n    #[new]\n    fn new(py: Python, max_capacity: usize) -> PyResult<Self> {\n        let level = get_python_logger_level(py, \"edgedb.server.http\")?;\n\n        info!(\"Http::new(max_capacity={max_capacity})\");\n        let (txfd, rxfd) = std::sync::mpsc::channel();\n\n        thread::Builder::new()\n            .name(\"edgedb-http\".to_string())\n            .spawn(move || {\n                initialize_logging_in_thread(\"edgedb.server.http\", level);\n                defer!(info!(\"Rust-side Http thread exiting\"));\n                info!(\"Rust-side Http thread booted\");\n                let rt = tokio::runtime::Builder::new_current_thread()\n                    .enable_time()\n                    .enable_io()\n                    .build()\n                    .unwrap();\n                let _guard = rt.enter();\n\n                let (rust, python) = new_python_channel();\n                txfd.send(python).unwrap();\n                let local = LocalSet::new();\n\n                local.block_on(&rt, run_and_block(max_capacity, rust));\n            })\n            .expect(\"Failed to create HTTP thread\");\n\n        Ok(Http {\n            python: Arc::new(rxfd.recv().unwrap()),\n        })\n    }\n\n    #[getter]\n    fn _channel(&self) -> PyResult<PythonChannel> {\n        Ok(PythonChannel::new(self.python.clone()))\n    }\n\n    fn _request(\n        &self,\n        id: PythonConnId,\n        url: String,\n        method: String,\n        body: Vec<u8>,\n        headers: Vec<(String, String)>,\n        cache: bool,\n    ) -> PyResult<()> {\n        self.python.send_err(PythonToRustMessage::Request(\n            id, url, method, body, headers, cache,\n        ))\n    }\n\n    fn _request_sse(\n        &self,\n        id: PythonConnId,\n        url: String,\n        method: String,\n        body: Vec<u8>,\n        headers: Vec<(String, String)>,\n    ) -> PyResult<()> {\n        self.python.send_err(PythonToRustMessage::RequestSse(\n            id, url, method, body, headers,\n        ))\n    }\n\n    fn _close(&self, id: PythonConnId) -> PyResult<()> {\n        self.python.send_err(PythonToRustMessage::Close(id))\n    }\n\n    fn _ack_sse(&self, id: PythonConnId) -> PyResult<()> {\n        self.python.send_err(PythonToRustMessage::Ack(id))\n    }\n\n    fn _update_limit(&self, limit: usize) -> PyResult<()> {\n        self.python\n            .send_err(PythonToRustMessage::UpdateLimit(limit))\n    }\n}\n\n#[pymodule]\npub fn _gel_http(py: Python, m: &Bound<PyModule>) -> PyResult<()> {\n    m.add_class::<Http>()?;\n    m.add(\"InternalError\", py.get_type::<InternalError>())?;\n\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use rstest::rstest;\n    use std::sync::{\n        atomic::{AtomicBool, AtomicUsize, Ordering},\n        Arc,\n    };\n\n    #[tokio::test]\n    async fn test_permit_manager() {\n        let manager = Arc::new(PermitManager::new(5));\n\n        // Shared done flag\n        let done = Arc::new(AtomicBool::new(false));\n\n        // Create 100 tasks\n        let tasks = (0..100)\n            .map(|_| {\n                let manager = manager.clone();\n                let done = done.clone();\n                tokio::spawn(async move {\n                    loop {\n                        if done.load(Ordering::Relaxed) {\n                            break;\n                        }\n                        let permit = manager.acquire().await.unwrap();\n                        tokio::time::sleep(std::time::Duration::from_millis(2)).await;\n                        drop(permit);\n                    }\n                })\n            })\n            .collect::<Vec<_>>();\n\n        // Create a task that changes the limit between 1..10 every 1ms\n        {\n            let manager = manager.clone();\n            tokio::spawn(async move {\n                for i in 1..100 {\n                    manager.update_limit(i % 10 + 1);\n                    tokio::time::sleep(std::time::Duration::from_millis(1)).await;\n                }\n            })\n            .await\n            .unwrap();\n        }\n\n        done.store(true, Ordering::Relaxed);\n\n        for task in tasks {\n            task.await.unwrap();\n        }\n\n        assert_eq!(manager.active(), 0);\n        assert_eq!(manager.capacity(), 10);\n    }\n\n    #[tokio::test]\n    async fn test_permit_manager_zero_to_five() {\n        let manager = Arc::new(PermitManager::new(0));\n        let permit_count = Arc::new(AtomicUsize::new(0));\n\n        // Create 5 tasks that try to acquire permits\n        let tasks: Vec<_> = (0..5)\n            .map(|_| {\n                let manager = manager.clone();\n                let permit_count = permit_count.clone();\n                tokio::spawn(async move {\n                    let _ = manager.acquire().await.unwrap();\n                    permit_count.fetch_add(1, Ordering::SeqCst);\n                })\n            })\n            .collect();\n\n        // Wait a bit to ensure no permits are issued\n        tokio::time::sleep(std::time::Duration::from_millis(100)).await;\n        assert_eq!(permit_count.load(Ordering::SeqCst), 0);\n\n        // Update the permit count to 5\n        manager.update_limit(5);\n\n        // Wait for all tasks to complete\n        for task in tasks {\n            task.await.unwrap();\n        }\n\n        // Check that all 5 permits were issued\n        assert_eq!(permit_count.load(Ordering::SeqCst), 5);\n        assert_eq!(manager.active(), 0);\n        assert_eq!(manager.capacity(), 5);\n    }\n\n    #[rstest]\n    // Up-down-up\n    #[case(20, 5, 20, 8)]\n    #[case(10, 3, 15, 5)]\n    #[case(5, 2, 4, 3)]\n    #[case(8, 3, 6, 4)]\n    #[case(12, 4, 9, 6)]\n    #[case(7, 2, 5, 3)]\n    #[case(9, 4, 7, 5)]\n    // Down-up-down\n    #[case(5, 10, 5, 4)]\n    #[case(8, 15, 6, 4)]\n    // Down-up-up\n    #[case(10, 15, 20, 4)]\n    #[case(10, 15, 20, 10)]\n    #[tokio::test]\n    async fn test_overrelease(\n        #[case] initial_capacity: usize,\n        #[case] new_capacity: usize,\n        #[case] final_capacity: usize,\n        #[case] acquire_count: usize,\n    ) {\n        let manager = Arc::new(PermitManager::new(initial_capacity));\n        let mut permits = vec![];\n\n        for _ in 0..acquire_count {\n            permits.push(manager.acquire().await.unwrap());\n        }\n\n        assert_eq!(manager.active(), acquire_count);\n        assert_eq!(manager.capacity(), initial_capacity);\n        assert_eq!(\n            manager.semaphore.available_permits(),\n            initial_capacity - acquire_count\n        );\n\n        manager.update_limit(new_capacity);\n\n        assert_eq!(manager.active(), acquire_count);\n        assert_eq!(manager.capacity(), new_capacity);\n        assert_eq!(\n            manager.semaphore.available_permits(),\n            new_capacity.saturating_sub(acquire_count)\n        );\n\n        manager.update_limit(final_capacity);\n\n        assert_eq!(manager.active(), acquire_count);\n        assert_eq!(manager.capacity(), final_capacity);\n        assert_eq!(\n            manager.semaphore.available_permits(),\n            final_capacity.saturating_sub(acquire_count)\n        );\n\n        for permit in permits {\n            drop(permit);\n            assert_eq!(\n                manager.semaphore.available_permits(),\n                final_capacity.saturating_sub(manager.active())\n            );\n            eprintln!(\n                \"{} {} {}\",\n                manager.active(),\n                manager.capacity(),\n                manager.semaphore.available_permits()\n            );\n        }\n\n        assert_eq!(manager.active(), 0);\n        assert_eq!(manager.capacity(), final_capacity);\n        assert_eq!(manager.semaphore.available_permits(), final_capacity);\n    }\n}\n"
  },
  {
    "path": "rust/pgrust/Cargo.toml",
    "content": "[package]\nname = \"pgrust\"\nversion = \"0.1.0\"\nlicense = \"MIT/Apache-2.0\"\nauthors = [\"MagicStack Inc. <hello@magic.io>\"]\nedition = \"2021\"\n\n[lints]\nworkspace = true\n\n[features]\npython_extension = [\"pyo3/serde\"]\noptimizer = []\n\n[dependencies]\ngel-auth = { workspace = true, features = [\"postgres\"] }\ngel-stream = { workspace = true, features = [\"client\"] }\ngel-pg-protocol.workspace = true\ngel-db-protocol.workspace = true\ngel-dsn = { workspace = true, features = [\"postgres\"] }\n\npyo3.workspace = true\ntracing.workspace = true\n\npaste = \"1\"\nderive_more = { version = \"2\", features = [\"full\"] }\n\n[dev-dependencies]\ntracing-subscriber.workspace = true\ngel-pg-captive.workspace = true\ngel-stream = { workspace = true, features = [\"rustls\", \"client\", \"tokio\"] }\n\nscopeguard = \"1\"\npretty_assertions = \"1.2.0\"\ntest-log = { version = \"0\", features = [\"trace\"] }\nrstest = \"0\"\nclap = \"4\"\nclap_derive = \"4\"\nlibc = \"0.2.158\"\n\n[lib]\n"
  },
  {
    "path": "rust/pgrust/src/errors/edgedb.rs",
    "content": "#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display)]\n#[repr(i32)]\npub enum EdbError {\n    InternalServerError = 0x_01_00_00_00,\n    UnsupportedFeatureError = 0x_02_00_00_00,\n    ProtocolError = 0x_03_00_00_00,\n    BinaryProtocolError = 0x_03_01_00_00,\n    UnsupportedProtocolVersionError = 0x_03_01_00_01,\n    TypeSpecNotFoundError = 0x_03_01_00_02,\n    UnexpectedMessageError = 0x_03_01_00_03,\n    InputDataError = 0x_03_02_00_00,\n    ParameterTypeMismatchError = 0x_03_02_01_00,\n    StateMismatchError = 0x_03_02_02_00,\n    ResultCardinalityMismatchError = 0x_03_03_00_00,\n    CapabilityError = 0x_03_04_00_00,\n    UnsupportedCapabilityError = 0x_03_04_01_00,\n    DisabledCapabilityError = 0x_03_04_02_00,\n    QueryError = 0x_04_00_00_00,\n    InvalidSyntaxError = 0x_04_01_00_00,\n    EdgeQLSyntaxError = 0x_04_01_01_00,\n    SchemaSyntaxError = 0x_04_01_02_00,\n    GraphQLSyntaxError = 0x_04_01_03_00,\n    InvalidTypeError = 0x_04_02_00_00,\n    InvalidTargetError = 0x_04_02_01_00,\n    InvalidLinkTargetError = 0x_04_02_01_01,\n    InvalidPropertyTargetError = 0x_04_02_01_02,\n    InvalidReferenceError = 0x_04_03_00_00,\n    UnknownModuleError = 0x_04_03_00_01,\n    UnknownLinkError = 0x_04_03_00_02,\n    UnknownPropertyError = 0x_04_03_00_03,\n    UnknownUserError = 0x_04_03_00_04,\n    UnknownDatabaseError = 0x_04_03_00_05,\n    UnknownParameterError = 0x_04_03_00_06,\n    SchemaError = 0x_04_04_00_00,\n    SchemaDefinitionError = 0x_04_05_00_00,\n    InvalidDefinitionError = 0x_04_05_01_00,\n    InvalidModuleDefinitionError = 0x_04_05_01_01,\n    InvalidLinkDefinitionError = 0x_04_05_01_02,\n    InvalidPropertyDefinitionError = 0x_04_05_01_03,\n    InvalidUserDefinitionError = 0x_04_05_01_04,\n    InvalidDatabaseDefinitionError = 0x_04_05_01_05,\n    InvalidOperatorDefinitionError = 0x_04_05_01_06,\n    InvalidAliasDefinitionError = 0x_04_05_01_07,\n    InvalidFunctionDefinitionError = 0x_04_05_01_08,\n    InvalidConstraintDefinitionError = 0x_04_05_01_09,\n    InvalidCastDefinitionError = 0x_04_05_01_0A,\n    DuplicateDefinitionError = 0x_04_05_02_00,\n    DuplicateModuleDefinitionError = 0x_04_05_02_01,\n    DuplicateLinkDefinitionError = 0x_04_05_02_02,\n    DuplicatePropertyDefinitionError = 0x_04_05_02_03,\n    DuplicateUserDefinitionError = 0x_04_05_02_04,\n    DuplicateDatabaseDefinitionError = 0x_04_05_02_05,\n    DuplicateOperatorDefinitionError = 0x_04_05_02_06,\n    DuplicateViewDefinitionError = 0x_04_05_02_07,\n    DuplicateFunctionDefinitionError = 0x_04_05_02_08,\n    DuplicateConstraintDefinitionError = 0x_04_05_02_09,\n    DuplicateCastDefinitionError = 0x_04_05_02_0A,\n    DuplicateMigrationError = 0x_04_05_02_0B,\n    SessionTimeoutError = 0x_04_06_00_00,\n    IdleSessionTimeoutError = 0x_04_06_01_00,\n    QueryTimeoutError = 0x_04_06_02_00,\n    TransactionTimeoutError = 0x_04_06_0A_00,\n    IdleTransactionTimeoutError = 0x_04_06_0A_01,\n    ExecutionError = 0x_05_00_00_00,\n    InvalidValueError = 0x_05_01_00_00,\n    DivisionByZeroError = 0x_05_01_00_01,\n    NumericOutOfRangeError = 0x_05_01_00_02,\n    AccessPolicyError = 0x_05_01_00_03,\n    QueryAssertionError = 0x_05_01_00_04,\n    IntegrityError = 0x_05_02_00_00,\n    ConstraintViolationError = 0x_05_02_00_01,\n    CardinalityViolationError = 0x_05_02_00_02,\n    MissingRequiredError = 0x_05_02_00_03,\n    TransactionError = 0x_05_03_00_00,\n    TransactionConflictError = 0x_05_03_01_00,\n    TransactionSerializationError = 0x_05_03_01_01,\n    TransactionDeadlockError = 0x_05_03_01_02,\n    WatchError = 0x_05_04_00_00,\n    ConfigurationError = 0x_06_00_00_00,\n    AccessError = 0x_07_00_00_00,\n    AuthenticationError = 0x_07_01_00_00,\n    AvailabilityError = 0x_08_00_00_00,\n    BackendUnavailableError = 0x_08_00_00_01,\n    ServerOfflineError = 0x_08_00_00_02,\n    UnknownTenantError = 0x_08_00_00_03,\n    ServerBlockedError = 0x_08_00_00_04,\n    BackendError = 0x_09_00_00_00,\n    UnsupportedBackendFeatureError = 0x_09_00_01_00,\n    LogMessage = 0x_F0_00_00_00_u32 as i32,\n    WarningMessage = 0x_F0_01_00_00_u32 as i32,\n    ClientError = 0x_FF_00_00_00_u32 as i32,\n    ClientConnectionError = 0x_FF_01_00_00_u32 as i32,\n    ClientConnectionFailedError = 0x_FF_01_01_00_u32 as i32,\n    ClientConnectionFailedTemporarilyError = 0x_FF_01_01_01_u32 as i32,\n    ClientConnectionTimeoutError = 0x_FF_01_02_00_u32 as i32,\n    ClientConnectionClosedError = 0x_FF_01_03_00_u32 as i32,\n    InterfaceError = 0x_FF_02_00_00_u32 as i32,\n    QueryArgumentError = 0x_FF_02_01_00_u32 as i32,\n    MissingArgumentError = 0x_FF_02_01_01_u32 as i32,\n    UnknownArgumentError = 0x_FF_02_01_02_u32 as i32,\n    InvalidArgumentError = 0x_FF_02_01_03_u32 as i32,\n    NoDataError = 0x_FF_03_00_00_u32 as i32,\n    InternalClientError = 0x_FF_04_00_00_u32 as i32,\n}\n\nimpl std::error::Error for EdbError {}\n"
  },
  {
    "path": "rust/pgrust/src/errors/mod.rs",
    "content": "use core::str;\nuse paste::paste;\nuse std::{collections::HashMap, str::FromStr};\n\npub mod edgedb;\n\nuse gel_pg_protocol::protocol::{ErrorResponse, NoticeResponse};\n\n#[macro_export]\nmacro_rules! pg_error_class {\n    ($(\n        #[doc=$doc:literal]\n        $code:expr => $name:ident\n    ),* $(,)?) => {\n\n        paste!(\n            /// Postgres error classes. See https://www.postgresql.org/docs/current/errcodes-appendix.html.\n            #[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]\n            enum PgErrorClass {\n                $(\n                    #[doc=$doc]\n                    [<$name:camel>],\n                )*\n                /// Unknown error class\n                Other([u8; 2])\n            }\n        );\n\n        impl PgErrorClass {\n            paste!(\n                pub const fn from_code(code: [u8; 2]) -> Self {\n                    $(\n                        const [<$name:upper>]: [u8; 2] = [stringify!($code).as_bytes()[0], stringify!($code).as_bytes()[1]];\n                    )*\n\n                    match code {\n                        $(\n                            [<$name:upper>] => Self::[<$name:camel>],\n                        )*\n                        _ => Self::Other(code)\n                    }\n                }\n            );\n\n            pub const fn to_code(self) -> [u8; 2] {\n                match self {\n                    $(\n                        paste!(Self::[<$name:camel>]) => {\n                            let s = stringify!($code).as_bytes();\n                            [s[0], s[1]]\n                        }\n                    )*\n                    Self::Other(code) => code,\n                }\n            }\n\n            pub const fn get_class_string(&self) -> &'static str {\n                match self {\n                    $(\n                        paste!(Self::[<$name:camel>]) => stringify!($name),\n                    )*\n                    Self::Other(..) => \"other\",\n                }\n            }\n        }\n    };\n}\n\nmacro_rules! pg_error {\n    ($(\n        $class:ident {\n            $(\n                $($code_l:literal)? $($code_i:ident)? => $name:ident\n            ),* $(,)?\n        }\n    ),* $(,)?) => {\n        $(\n            paste!(\n                #[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]\n                pub enum [<PgError $class:camel>] {\n                    $(\n                        [<$name:camel>],\n                    )*\n                }\n\n                impl [<PgError $class:camel>] {\n                    pub const fn from_code(code: [u8; 3]) -> Option<Self> {\n                        $(\n                            const [<$name:upper>]: [u8; 3] = [stringify!($($code_i)? $($code_l)?).as_bytes()[0], stringify!($($code_i)? $($code_l)?).as_bytes()[1], stringify!($($code_i)? $($code_l)?).as_bytes()[2]];\n                        )*\n\n                        match code {\n                            $(\n                                [<$name:upper>] => Some(Self::[<$name:camel>]),\n                            )*\n                            _ => None\n                        }\n                    }\n\n                    pub const fn to_code(self) -> [u8; 5] {\n                        match self {\n                            $(\n                                Self::[<$name:camel>] => {\n                                    let s = stringify!($($code_i)? $($code_l)?).as_bytes();\n                                    let c = paste!(PgErrorClass::[<$class:camel>].to_code());\n                                    [c[0], c[1], s[0], s[1], s[2]]\n                                }\n                            )*\n                        }\n                    }\n\n                    pub const fn get_code_string(&self) -> &'static str {\n                        match self {\n                            $(\n                                Self::[<$name:camel>] => {\n                                    const CODE: [u8; 5] = [<PgError $class:camel>]::[<$name:camel>].to_code();\n                                    match str::from_utf8(&CODE) {\n                                        Ok(s) => s,\n                                        _ => panic!()\n                                    }\n                                }\n                            )*\n                        }\n                    }\n\n                    pub const fn get_error_string(&self) -> &'static str {\n                        match self {\n                            $(\n                                paste!(Self::[<$name:camel>]) => stringify!($name),\n                            )*\n                        }\n                    }\n                }\n            );\n        )*\n\n        paste!(\n            /// Postgres error codes. See <https://www.postgresql.org/docs/current/errcodes-appendix.html>.\n            #[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]\n            pub enum PgError {\n                $(\n                    [<$class:camel>]([<PgError $class:camel>]),\n                )*\n                Other([u8; 5])\n            }\n\n            impl PgError {\n                pub const fn from_code(code: [u8; 5]) -> Self {\n                    match PgErrorClass::from_code([code[0], code[1]]) {\n                        $(\n                            PgErrorClass::[<$class:camel>] => {\n                                if let Some(code) = [<PgError $class:camel>]::from_code([code[2], code[3], code[4]]) {\n                                    Self::[<$class:camel>](code)\n                                } else {\n                                    Self::Other(code)\n                                }\n                            }\n                        )*,\n                        PgErrorClass::Other(..) => Self::Other(code)\n                    }\n                }\n\n                pub const fn to_code(self) -> [u8; 5] {\n                    match self {\n                        $(\n                            Self::[<$class:camel>](error) => {\n                                error.to_code()\n                            }\n                        )*\n                        Self::Other(code) => code,\n                    }\n                }\n\n                pub const fn get_code_string(&self) -> &str {\n                    match self {\n                        $(\n                            Self::[<$class:camel>](error) => error.get_code_string(),\n                        )*\n                        Self::Other(code) => match str::from_utf8(code) {\n                            Ok(s) => s,\n                            _ => \"\"\n                        }\n                    }\n                }\n\n                pub const fn get_error_string(self) -> &'static str {\n                    match self {\n                        $(\n                            Self::[<$class:camel>](error) => error.get_error_string(),\n                        )*\n                        Self::Other(..) => \"other\",\n                    }\n                }\n            }\n        );\n    };\n}\n\nimpl std::fmt::Display for PgErrorClass {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let code = self.to_code();\n        for &byte in &code {\n            if byte.is_ascii() {\n                write!(f, \"{}\", byte as char)?;\n            } else {\n                write!(f, \"{{{byte:02X}}}\")?;\n            }\n        }\n        Ok(())\n    }\n}\n\nimpl std::fmt::Debug for PgErrorClass {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let clazz = self.get_class_string();\n        write!(f, \"{clazz}({self})\")?;\n        Ok(())\n    }\n}\n\nimpl std::fmt::Display for PgError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let code = self.to_code();\n        for &byte in &code {\n            if byte.is_ascii() {\n                write!(f, \"{}\", byte as char)?;\n            } else {\n                write!(f, \"{{{byte:02X}}}\")?;\n            }\n        }\n        Ok(())\n    }\n}\n\nimpl std::fmt::Debug for PgError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let clazz = self.get_error_string();\n        write!(f, \"{self}: {clazz}\")?;\n        Ok(())\n    }\n}\n\nimpl From<[u8; 5]> for PgError {\n    fn from(code: [u8; 5]) -> Self {\n        Self::from_code(code)\n    }\n}\n\nimpl From<PgError> for [u8; 5] {\n    fn from(error: PgError) -> Self {\n        error.to_code()\n    }\n}\n\n#[derive(Debug)]\npub struct PgErrorParseError {\n    kind: PgErrorParseErrorKind,\n}\n\n#[derive(Debug)]\npub enum PgErrorParseErrorKind {\n    InvalidLength,\n    InvalidCharacter,\n}\n\nimpl FromStr for PgError {\n    type Err = PgErrorParseError;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        if s.len() != 5 {\n            return Err(PgErrorParseError {\n                kind: PgErrorParseErrorKind::InvalidLength,\n            });\n        }\n\n        let code = s.as_bytes();\n        if !code.is_ascii() {\n            return Err(PgErrorParseError {\n                kind: PgErrorParseErrorKind::InvalidCharacter,\n            });\n        }\n\n        Ok(PgError::from_code([\n            code[0], code[1], code[2], code[3], code[4],\n        ]))\n    }\n}\n\nimpl std::fmt::Display for PgErrorParseError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self.kind {\n            PgErrorParseErrorKind::InvalidLength => write!(f, \"Invalid PgError code length\"),\n            PgErrorParseErrorKind::InvalidCharacter => {\n                write!(f, \"Invalid character in PgError code\")\n            }\n        }\n    }\n}\n\nimpl std::error::Error for PgErrorParseError {}\n\nimpl std::error::Error for PgError {}\n\n/// A fully-qualified Postgres wire error message.\n#[derive(Debug)]\npub struct PgServerError {\n    pub code: PgError,\n    pub severity: PgErrorSeverity,\n    pub message: String,\n    pub extra: HashMap<PgServerErrorField, String>,\n}\n\nimpl PgServerError {\n    pub fn new(\n        code: PgError,\n        arg: impl AsRef<str>,\n        extra: HashMap<PgServerErrorField, String>,\n    ) -> Self {\n        Self {\n            code,\n            severity: PgErrorSeverity::Error,\n            message: arg.as_ref().to_owned(),\n            extra,\n        }\n    }\n\n    /// Iterate all the fields of this error.\n    pub fn fields(&self) -> impl Iterator<Item = (PgServerErrorField, &str)> {\n        PgServerErrorFieldIterator::new(self)\n    }\n}\n\nstruct PgServerErrorBasicFieldIterator<'a> {\n    error: &'a PgServerError,\n    index: usize,\n}\n\nimpl<'a> PgServerErrorBasicFieldIterator<'a> {\n    fn new(error: &'a PgServerError) -> Self {\n        Self { error, index: 0 }\n    }\n}\n\nimpl<'a> Iterator for PgServerErrorBasicFieldIterator<'a> {\n    type Item = (PgServerErrorField, &'a str);\n\n    fn next(&mut self) -> Option<Self::Item> {\n        let result = match self.index {\n            0 => Some((PgServerErrorField::Code, self.error.code.get_code_string())),\n            1 => Some((PgServerErrorField::Message, self.error.message.as_str())),\n            2 => Some((\n                PgServerErrorField::SeverityNonLocalized,\n                self.error.severity.as_ref(),\n            )),\n            _ => None,\n        };\n        self.index += 1;\n        result\n    }\n}\n\n#[allow(clippy::type_complexity)]\nstruct PgServerErrorFieldIterator<'a> {\n    iter: std::iter::Chain<\n        PgServerErrorBasicFieldIterator<'a>,\n        std::iter::Map<\n            std::collections::hash_map::Iter<'a, PgServerErrorField, String>,\n            fn((&'a PgServerErrorField, &'a String)) -> (PgServerErrorField, &'a str),\n        >,\n    >,\n}\n\nimpl<'a> PgServerErrorFieldIterator<'a> {\n    pub fn new(error: &'a PgServerError) -> Self {\n        let f: fn((&'a PgServerErrorField, &'a String)) -> (PgServerErrorField, &'a str) =\n            |(f, e)| (*f, e.as_str());\n        let a = PgServerErrorBasicFieldIterator::new(error);\n        let b = error.extra.iter().map(f);\n        let iter = Iterator::chain(a, b);\n        Self { iter }\n    }\n}\n\nimpl<'a> Iterator for PgServerErrorFieldIterator<'a> {\n    type Item = (PgServerErrorField, &'a str);\n\n    fn next(&mut self) -> Option<Self::Item> {\n        self.iter.next()\n    }\n}\n\nimpl std::fmt::Display for PgServerError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"Server error: {}: {}\", self.code, self.message)\n    }\n}\n\nimpl std::error::Error for PgServerError {\n    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {\n        Some(&self.code)\n    }\n}\n\nimpl From<ErrorResponse<'_>> for PgServerError {\n    fn from(error: ErrorResponse) -> Self {\n        let mut code = String::new();\n        let mut message = String::new();\n        let mut extra = HashMap::new();\n        let mut severity = PgErrorSeverity::Error;\n\n        for field in error.fields() {\n            let value = field.value().to_string_lossy().into_owned();\n            match PgServerErrorField::try_from(field.etype()) {\n                Ok(PgServerErrorField::Code) => code = value,\n                Ok(PgServerErrorField::Message) => message = value,\n                Ok(PgServerErrorField::SeverityNonLocalized) => {\n                    severity = PgErrorSeverity::from_str(&value).unwrap_or_default()\n                }\n                Ok(field_type) => {\n                    extra.insert(field_type, value);\n                }\n                Err(_) => {}\n            }\n        }\n\n        // It's very unlikely the server will give us a non-five-character code\n        let code = match PgError::from_str(&code) {\n            Ok(code) => code,\n            Err(_) => PgError::Other(*b\"?????\"),\n        };\n\n        PgServerError {\n            code,\n            severity,\n            message,\n            extra,\n        }\n    }\n}\n\nimpl From<NoticeResponse<'_>> for PgServerError {\n    fn from(error: NoticeResponse) -> Self {\n        let mut code = String::new();\n        let mut message = String::new();\n        let mut extra = HashMap::new();\n        let mut severity = PgErrorSeverity::Error;\n\n        for field in error.fields() {\n            let value = field.value().to_string_lossy().into_owned();\n            match PgServerErrorField::try_from(field.ntype()) {\n                Ok(PgServerErrorField::Code) => code = value,\n                Ok(PgServerErrorField::Message) => message = value,\n                Ok(PgServerErrorField::SeverityNonLocalized) => {\n                    severity = PgErrorSeverity::from_str(&value).unwrap_or_default()\n                }\n                Ok(field_type) => {\n                    extra.insert(field_type, value);\n                }\n                Err(_) => {}\n            }\n        }\n\n        // It's very unlikely the server will give us a non-five-character code\n        let code = match PgError::from_str(&code) {\n            Ok(code) => code,\n            Err(_) => PgError::Other(*b\"?????\"),\n        };\n\n        PgServerError {\n            code,\n            severity,\n            message,\n            extra,\n        }\n    }\n}\n\n/// Enum representing the field types in ErrorResponse and NoticeResponse messages.\n///\n/// See <https://www.postgresql.org/docs/current/protocol-error-fields.html>\n#[repr(u8)]\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, derive_more::TryFrom)]\n#[try_from(repr)]\npub enum PgServerErrorField {\n    /// Severity: ERROR, FATAL, PANIC, WARNING, NOTICE, DEBUG, INFO, or LOG\n    Severity = b'S',\n    /// Severity (non-localized): ERROR, FATAL, PANIC, WARNING, NOTICE, DEBUG, INFO, or LOG\n    SeverityNonLocalized = b'V',\n    /// SQLSTATE code for the error\n    Code = b'C',\n    /// Primary human-readable error message\n    Message = b'M',\n    /// Optional secondary error message with more detail\n    Detail = b'D',\n    /// Optional suggestion on how to resolve the problem\n    Hint = b'H',\n    /// Error cursor position as an index into the original query string\n    Position = b'P',\n    /// Internal position for internally generated commands\n    InternalPosition = b'p',\n    /// Text of a failed internally-generated command\n    InternalQuery = b'q',\n    /// Context in which the error occurred (e.g., call stack traceback)\n    Where = b'W',\n    /// Schema name associated with the error\n    SchemaName = b's',\n    /// Table name associated with the error\n    TableName = b't',\n    /// Column name associated with the error\n    ColumnName = b'c',\n    /// Data type name associated with the error\n    DataTypeName = b'd',\n    /// Constraint name associated with the error\n    ConstraintName = b'n',\n    /// Source-code file name where the error was reported\n    File = b'F',\n    /// Source-code line number where the error was reported\n    Line = b'L',\n    /// Source-code routine name reporting the error\n    Routine = b'R',\n}\n\n/// Enum representing the severity levels of PostgreSQL errors and notices.\n#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]\npub enum PgErrorSeverity {\n    #[default]\n    Error,\n    Fatal,\n    Panic,\n    Warning,\n    Notice,\n    Debug,\n    Info,\n    Log,\n}\n\nimpl std::fmt::Display for PgErrorSeverity {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            PgErrorSeverity::Error => write!(f, \"ERROR\"),\n            PgErrorSeverity::Fatal => write!(f, \"FATAL\"),\n            PgErrorSeverity::Panic => write!(f, \"PANIC\"),\n            PgErrorSeverity::Warning => write!(f, \"WARNING\"),\n            PgErrorSeverity::Notice => write!(f, \"NOTICE\"),\n            PgErrorSeverity::Debug => write!(f, \"DEBUG\"),\n            PgErrorSeverity::Info => write!(f, \"INFO\"),\n            PgErrorSeverity::Log => write!(f, \"LOG\"),\n        }\n    }\n}\nimpl std::str::FromStr for PgErrorSeverity {\n    type Err = ();\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        match s.to_uppercase().as_str() {\n            \"ERROR\" => Ok(PgErrorSeverity::Error),\n            \"FATAL\" => Ok(PgErrorSeverity::Fatal),\n            \"PANIC\" => Ok(PgErrorSeverity::Panic),\n            \"WARNING\" => Ok(PgErrorSeverity::Warning),\n            \"NOTICE\" => Ok(PgErrorSeverity::Notice),\n            \"DEBUG\" => Ok(PgErrorSeverity::Debug),\n            \"INFO\" => Ok(PgErrorSeverity::Info),\n            \"LOG\" => Ok(PgErrorSeverity::Log),\n            _ => Err(()),\n        }\n    }\n}\n\nimpl std::ops::Deref for PgErrorSeverity {\n    type Target = str;\n\n    fn deref(&self) -> &Self::Target {\n        match self {\n            PgErrorSeverity::Error => \"ERROR\",\n            PgErrorSeverity::Fatal => \"FATAL\",\n            PgErrorSeverity::Panic => \"PANIC\",\n            PgErrorSeverity::Warning => \"WARNING\",\n            PgErrorSeverity::Notice => \"NOTICE\",\n            PgErrorSeverity::Debug => \"DEBUG\",\n            PgErrorSeverity::Info => \"INFO\",\n            PgErrorSeverity::Log => \"LOG\",\n        }\n    }\n}\n\npg_error_class!(\n    /// Successful Completion\n    00 => successful_completion,\n    /// Warning\n    01 => warning,\n    /// No Data (this is also a warning class per the SQL standard)\n    02 => no_data,\n    /// SQL Statement Not Yet Complete\n    03 => sql_statement_not_yet_complete,\n    /// Connection Exception\n    08 => connection_exception,\n    /// Triggered Action Exception\n    09 => triggered_action_exception,\n    /// Feature Not Supported\n    0A => feature_not_supported,\n    /// Invalid Transaction Initiation\n    0B => invalid_transaction_initiation,\n    /// Locator Exception\n    0F => locator_exception,\n    /// Invalid Grantor\n    0L => invalid_grantor,\n    /// Invalid Role Specification\n    0P => invalid_role_specification,\n    /// Diagnostics Exception\n    0Z => diagnostics_exception,\n    /// Case Not Found\n    20 => case_not_found,\n    /// Cardinality Violation\n    21 => cardinality_violation,\n    /// Data Exception\n    22 => data_exception,\n    /// Integrity Constraint Violation\n    23 => integrity_constraint_violation,\n    /// Invalid Cursor State\n    24 => invalid_cursor_state,\n    /// Invalid Transaction State\n    25 => invalid_transaction_state,\n    /// Invalid SQL Statement Name\n    26 => invalid_sql_statement_name,\n    /// Triggered Data Change Violation\n    27 => triggered_data_change_violation,\n    /// Invalid Authorization Specification\n    28 => invalid_authorization_specification,\n    /// Dependent Privilege Descriptors Still Exist\n    2B => dependent_privilege_descriptors_still_exist,\n    /// Invalid Transaction Termination\n    2D => invalid_transaction_termination,\n    /// SQL Routine Exception\n    2F => sql_routine_exception,\n    /// Invalid Cursor Name\n    34 => invalid_cursor_name,\n    /// External Routine Exception\n    38 => external_routine_exception,\n    /// External Routine Invocation Exception\n    39 => external_routine_invocation_exception,\n    /// Savepoint Exception\n    3B => savepoint_exception,\n    /// Invalid Catalog Name\n    3D => invalid_catalog_name,\n    /// Invalid Schema Name\n    3F => invalid_schema_name,\n    /// Transaction Rollback\n    40 => transaction_rollback,\n    /// Syntax Error or Access Rule Violation\n    42 => syntax_error_or_access_rule_violation,\n    /// WITH CHECK OPTION Violation\n    44 => with_check_option_violation,\n    /// Insufficient Resources\n    53 => insufficient_resources,\n    /// Program Limit Exceeded\n    54 => program_limit_exceeded,\n    /// Object Not In Prerequisite State\n    55 => object_not_in_prerequisite_state,\n    /// Operator Intervention\n    57 => operator_intervention,\n    /// System Error (errors external to PostgreSQL itself)\n    58 => system_error,\n    /// Configuration File Error\n    F0 => config_file_error,\n    /// Foreign Data Wrapper Error (SQL/MED)\n    HV => fdw_error,\n    /// PL/pgSQL Error\n    P0 => plpgsql_error,\n    /// Internal Error\n    XX => internal_error\n);\n\npg_error!(\n    successful_completion {\n        000 => successful_completion,\n    },\n    warning {\n        000 => warning,\n        00C => dynamic_result_sets_returned,\n        008 => implicit_zero_bit_padding,\n        003 => null_value_eliminated_in_set_function,\n        007 => privilege_not_granted,\n        006 => privilege_not_revoked,\n        004 => string_data_right_truncation,\n        P01 => deprecated_feature,\n    },\n    no_data {\n        000 => no_data,\n        001 => no_additional_dynamic_result_sets_returned,\n    },\n    sql_statement_not_yet_complete {\n        000 => sql_statement_not_yet_complete,\n    },\n    connection_exception {\n        000 => connection_exception,\n        003 => connection_does_not_exist,\n        006 => connection_failure,\n        001 => sqlclient_unable_to_establish_sqlconnection,\n        004 => sqlserver_rejected_establishment_of_sqlconnection,\n        007 => transaction_resolution_unknown,\n        P01 => protocol_violation,\n    },\n    triggered_action_exception {\n        000 => triggered_action_exception,\n    },\n    feature_not_supported {\n        000 => feature_not_supported,\n    },\n    invalid_transaction_initiation {\n        000 => invalid_transaction_initiation,\n    },\n    locator_exception {\n        000 => locator_exception,\n        001 => invalid_locator_specification,\n    },\n    invalid_grantor {\n        000 => invalid_grantor,\n        P01 => invalid_grant_operation,\n    },\n    invalid_role_specification {\n        000 => invalid_role_specification,\n    },\n    diagnostics_exception {\n        000 => diagnostics_exception,\n        002 => stacked_diagnostics_accessed_without_active_handler,\n    },\n    case_not_found {\n        000 => case_not_found,\n    },\n    cardinality_violation {\n        000 => cardinality_violation,\n    },\n    data_exception {\n        000 => data_exception,\n        \"02E\" => array_subscript_error,\n        021 => character_not_in_repertoire,\n        008 => datetime_field_overflow,\n        012 => division_by_zero,\n        005 => error_in_assignment,\n        00B => escape_character_conflict,\n        022 => indicator_overflow,\n        015 => interval_field_overflow,\n        \"01E\" => invalid_argument_for_logarithm,\n        014 => invalid_argument_for_ntile_function,\n        016 => invalid_argument_for_nth_value_function,\n        01F => invalid_argument_for_power_function,\n        01G => invalid_argument_for_width_bucket_function,\n        018 => invalid_character_value_for_cast,\n        007 => invalid_datetime_format,\n        019 => invalid_escape_character,\n        00D => invalid_escape_octet,\n        025 => invalid_escape_sequence,\n        P06 => nonstandard_use_of_escape_character,\n        010 => invalid_indicator_parameter_value,\n        023 => invalid_parameter_value,\n        013 => invalid_preceding_or_following_size,\n        01B => invalid_regular_expression,\n        01W => invalid_row_count_in_limit_clause,\n        01X => invalid_row_count_in_result_offset_clause,\n        02H => invalid_tablesample_argument,\n        02G => invalid_tablesample_repeat,\n        009 => invalid_time_zone_displacement_value,\n        00C => invalid_use_of_escape_character,\n        00G => most_specific_type_mismatch,\n        004 => null_value_not_allowed,\n        002 => null_value_no_indicator_parameter,\n        003 => numeric_value_out_of_range,\n        00H => sequence_generator_limit_exceeded,\n        026 => string_data_length_mismatch,\n        001 => string_data_right_truncation,\n        011 => substring_error,\n        027 => trim_error,\n        024 => unterminated_c_string,\n        00F => zero_length_character_string,\n        P01 => floating_point_exception,\n        P02 => invalid_text_representation,\n        P03 => invalid_binary_representation,\n        P04 => bad_copy_file_format,\n        P05 => untranslatable_character,\n        00L => not_an_xml_document,\n        00M => invalid_xml_document,\n        00N => invalid_xml_content,\n        00S => invalid_xml_comment,\n        00T => invalid_xml_processing_instruction,\n        030 => duplicate_json_object_key_value,\n        031 => invalid_argument_for_sql_json_datetime_function,\n        032 => invalid_json_text,\n        033 => invalid_sql_json_subscript,\n        034 => more_than_one_sql_json_item,\n        035 => no_sql_json_item,\n        036 => non_numeric_sql_json_item,\n        037 => non_unique_keys_in_a_json_object,\n        038 => singleton_sql_json_item_required,\n        039 => sql_json_array_not_found,\n        03A => sql_json_member_not_found,\n        03B => sql_json_number_not_found,\n        03C => sql_json_object_not_found,\n        03D => too_many_json_array_elements,\n        \"03E\" => too_many_json_object_members,\n        03F => sql_json_scalar_required,\n        03G => sql_json_item_cannot_be_cast_to_target_type,\n    },\n    integrity_constraint_violation {\n        000 => integrity_constraint_violation,\n        001 => restrict_violation,\n        502 => not_null_violation,\n        503 => foreign_key_violation,\n        505 => unique_violation,\n        514 => check_violation,\n        P01 => exclusion_violation,\n    },\n    invalid_cursor_state {\n        000 => invalid_cursor_state,\n    },\n    invalid_transaction_state {\n        000 => invalid_transaction_state,\n        001 => active_sql_transaction,\n        002 => branch_transaction_already_active,\n        008 => held_cursor_requires_same_isolation_level,\n        003 => inappropriate_access_mode_for_branch_transaction,\n        004 => inappropriate_isolation_level_for_branch_transaction,\n        005 => no_active_sql_transaction_for_branch_transaction,\n        006 => read_only_sql_transaction,\n        007 => schema_and_data_statement_mixing_not_supported,\n        P01 => no_active_sql_transaction,\n        P02 => in_failed_sql_transaction,\n        P03 => idle_in_transaction_session_timeout,\n        P04 => transaction_timeout,\n    },\n    invalid_sql_statement_name {\n        000 => invalid_sql_statement_name,\n    },\n    triggered_data_change_violation {\n        000 => triggered_data_change_violation,\n    },\n    invalid_authorization_specification {\n        000 => invalid_authorization_specification,\n        P01 => invalid_password,\n    },\n    dependent_privilege_descriptors_still_exist {\n        000 => dependent_privilege_descriptors_still_exist,\n        P01 => dependent_objects_still_exist,\n    },\n    invalid_transaction_termination {\n        000 => invalid_transaction_termination,\n    },\n    sql_routine_exception {\n        000 => sql_routine_exception,\n        005 => function_executed_no_return_statement,\n        002 => modifying_sql_data_not_permitted,\n        003 => prohibited_sql_statement_attempted,\n        004 => reading_sql_data_not_permitted,\n    },\n    invalid_cursor_name {\n        000 => invalid_cursor_name,\n    },\n    external_routine_exception {\n        000 => external_routine_exception,\n        001 => containing_sql_not_permitted,\n        002 => modifying_sql_data_not_permitted,\n        003 => prohibited_sql_statement_attempted,\n        004 => reading_sql_data_not_permitted,\n    },\n    external_routine_invocation_exception {\n        000 => external_routine_invocation_exception,\n        001 => invalid_sqlstate_returned,\n        004 => null_value_not_allowed,\n        P01 => trigger_protocol_violated,\n        P02 => srf_protocol_violated,\n        P03 => event_trigger_protocol_violated,\n    },\n    savepoint_exception {\n        000 => savepoint_exception,\n        001 => invalid_savepoint_specification,\n    },\n    invalid_catalog_name {\n        000 => invalid_catalog_name,\n    },\n    invalid_schema_name {\n        000 => invalid_schema_name,\n    },\n    transaction_rollback {\n        000 => transaction_rollback,\n        002 => transaction_integrity_constraint_violation,\n        001 => serialization_failure,\n        003 => statement_completion_unknown,\n        P01 => deadlock_detected,\n    },\n    syntax_error_or_access_rule_violation {\n        000 => syntax_error_or_access_rule_violation,\n        601 => syntax_error,\n        501 => insufficient_privilege,\n        846 => cannot_coerce,\n        803 => grouping_error,\n        P20 => windowing_error,\n        P19 => invalid_recursion,\n        830 => invalid_foreign_key,\n        602 => invalid_name,\n        622 => name_too_long,\n        939 => reserved_name,\n        804 => datatype_mismatch,\n        P18 => indeterminate_datatype,\n        P21 => collation_mismatch,\n        P22 => indeterminate_collation,\n        809 => wrong_object_type,\n        8C9 => generated_always,\n        703 => undefined_column,\n        883 => undefined_function,\n        P01 => undefined_table,\n        P02 => undefined_parameter,\n        704 => undefined_object,\n        701 => duplicate_column,\n        P03 => duplicate_cursor,\n        P04 => duplicate_database,\n        723 => duplicate_function,\n        P05 => duplicate_prepared_statement,\n        P06 => duplicate_schema,\n        P07 => duplicate_table,\n        712 => duplicate_alias,\n        710 => duplicate_object,\n        702 => ambiguous_column,\n        725 => ambiguous_function,\n        P08 => ambiguous_parameter,\n        P09 => ambiguous_alias,\n        P10 => invalid_column_reference,\n        611 => invalid_column_definition,\n        P11 => invalid_cursor_definition,\n        P12 => invalid_database_definition,\n        P13 => invalid_function_definition,\n        P14 => invalid_prepared_statement_definition,\n        P15 => invalid_schema_definition,\n        P16 => invalid_table_definition,\n        P17 => invalid_object_definition,\n    },\n    with_check_option_violation {\n        000 => with_check_option_violation,\n    },\n    insufficient_resources {\n        000 => insufficient_resources,\n        100 => disk_full,\n        200 => out_of_memory,\n        300 => too_many_connections,\n        400 => configuration_limit_exceeded,\n    },\n    program_limit_exceeded {\n        000 => program_limit_exceeded,\n        001 => statement_too_complex,\n        011 => too_many_columns,\n        023 => too_many_arguments,\n    },\n    object_not_in_prerequisite_state {\n        000 => object_not_in_prerequisite_state,\n        006 => object_in_use,\n        P02 => cant_change_runtime_param,\n        P03 => lock_not_available,\n        P04 => unsafe_new_enum_value_usage,\n    },\n    operator_intervention {\n        000 => operator_intervention,\n        014 => query_canceled,\n        P01 => admin_shutdown,\n        P02 => crash_shutdown,\n        P03 => cannot_connect_now,\n        P04 => database_dropped,\n        P05 => idle_session_timeout,\n    },\n    system_error {\n        000 => system_error,\n        030 => io_error,\n        P01 => undefined_file,\n        P02 => duplicate_file,\n    },\n    config_file_error {\n        000 => config_file_error,\n        001 => lock_file_exists,\n    },\n    fdw_error {\n        000 => fdw_error,\n        005 => fdw_column_name_not_found,\n        002 => fdw_dynamic_parameter_value_needed,\n        010 => fdw_function_sequence_error,\n        021 => fdw_inconsistent_descriptor_information,\n        024 => fdw_invalid_attribute_value,\n        007 => fdw_invalid_column_name,\n        008 => fdw_invalid_column_number,\n        004 => fdw_invalid_data_type,\n        006 => fdw_invalid_data_type_descriptors,\n        091 => fdw_invalid_descriptor_field_identifier,\n        00B => fdw_invalid_handle,\n        00C => fdw_invalid_option_index,\n        00D => fdw_invalid_option_name,\n        090 => fdw_invalid_string_length_or_buffer_length,\n        00A => fdw_invalid_string_format,\n        009 => fdw_invalid_use_of_null_pointer,\n        014 => fdw_too_many_handles,\n        001 => fdw_out_of_memory,\n        00P => fdw_no_schemas,\n        00J => fdw_option_name_not_found,\n        00K => fdw_reply_handle,\n        00Q => fdw_schema_not_found,\n        00R => fdw_table_not_found,\n        00L => fdw_unable_to_create_execution,\n        00M => fdw_unable_to_create_reply,\n        00N => fdw_unable_to_establish_connection,\n    },\n    plpgsql_error {\n        000 => plpgsql_error,\n        001 => raise_exception,\n        002 => no_data_found,\n        003 => too_many_rows,\n        004 => assert_failure,\n    },\n    internal_error {\n        000 => internal_error,\n        001 => data_corrupted,\n        002 => index_corrupted,\n    }\n);\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    pub fn test_codes() {\n        assert_eq!(PgError::from_code(*b\"badco\"), PgError::Other(*b\"badco\"));\n        assert_eq!(\n            PgError::from_code(*b\"00000\"),\n            PgError::SuccessfulCompletion(PgErrorSuccessfulCompletion::SuccessfulCompletion)\n        );\n        assert_eq!(\n            PgError::from_code(*b\"22P04\"),\n            PgError::DataException(PgErrorDataException::BadCopyFileFormat)\n        );\n        assert_eq!(\n            PgError::from_code(*b\"2F003\"),\n            PgError::SqlRoutineException(\n                PgErrorSqlRoutineException::ProhibitedSqlStatementAttempted\n            )\n        );\n        assert_eq!(\n            PgError::from_code(*b\"XX002\"),\n            PgError::InternalError(PgErrorInternalError::IndexCorrupted)\n        );\n        assert_eq!(PgError::from_code(*b\"XXXXX\"), PgError::Other(*b\"XXXXX\"));\n\n        assert_eq!(format!(\"{}\", PgError::from_code(*b\"badco\")), \"badco\");\n        assert_eq!(format!(\"{}\", PgError::from_code(*b\"00000\")), \"00000\");\n        assert_eq!(format!(\"{}\", PgError::from_code(*b\"22P04\")), \"22P04\");\n        assert_eq!(format!(\"{}\", PgError::from_code(*b\"2F003\")), \"2F003\");\n        assert_eq!(format!(\"{}\", PgError::from_code(*b\"XX002\")), \"XX002\");\n        assert_eq!(format!(\"{}\", PgError::from_code(*b\"XXXXX\")), \"XXXXX\");\n\n        assert_eq!(\n            format!(\"{:?}\", PgError::from_code(*b\"badco\")),\n            \"badco: other\"\n        );\n        assert_eq!(\n            format!(\"{:?}\", PgError::from_code(*b\"00000\")),\n            \"00000: successful_completion\"\n        );\n        assert_eq!(\n            format!(\"{:?}\", PgError::from_code(*b\"22P04\")),\n            \"22P04: bad_copy_file_format\"\n        );\n        assert_eq!(\n            format!(\"{:?}\", PgError::from_code(*b\"2F003\")),\n            \"2F003: prohibited_sql_statement_attempted\"\n        );\n        assert_eq!(\n            format!(\"{:?}\", PgError::from_code(*b\"XX002\")),\n            \"XX002: index_corrupted\"\n        );\n        assert_eq!(\n            format!(\"{:?}\", PgError::from_code(*b\"XXXXX\")),\n            \"XXXXX: other\"\n        );\n    }\n\n    #[test]\n    fn test_pg_server_error() {\n        let error = PgServerError::new(\n            PgError::from_code(\"28000\".as_bytes().try_into().unwrap()),\n            \"message!\",\n            Default::default(),\n        );\n        let map: HashMap<_, _> = error.fields().collect();\n        assert_eq!(\n            map,\n            HashMap::from([\n                (PgServerErrorField::SeverityNonLocalized, \"ERROR\"),\n                (PgServerErrorField::Message, \"message!\"),\n                (PgServerErrorField::Code, \"28000\"),\n            ])\n        );\n    }\n}\n"
  },
  {
    "path": "rust/pgrust/src/lib.rs",
    "content": "pub mod errors;\n#[cfg(feature = \"python_extension\")]\npub mod python;\n"
  },
  {
    "path": "rust/pgrust/src/python/mod.rs",
    "content": "use gel_auth::postgres::client::{\n    ConnectionDrive, ConnectionState, ConnectionStateSend, ConnectionStateType,\n    ConnectionStateUpdate,\n};\nuse gel_auth::postgres::{ConnectionSslRequirement, Credentials};\nuse gel_db_protocol::prelude::StructBuffer;\nuse gel_dsn::postgres::*;\nuse gel_dsn::*;\nuse gel_pg_protocol::errors::PgServerError;\nuse gel_pg_protocol::protocol::{FrontendBuilder, InitialBuilder, Message, SSLResponse};\nuse gel_stream::ResolvedTarget;\nuse pyo3::{\n    buffer::PyBuffer,\n    exceptions::PyException,\n    prelude::*,\n    pymodule,\n    types::{PyAnyMethods, PyByteArray, PyBytes, PyMemoryView, PyModule, PyModuleMethods},\n    Bound, PyAny, PyResult, Python,\n};\nuse std::collections::HashMap;\nuse std::{borrow::Cow, path::Path};\nuse tracing::warn;\n\n#[derive(Clone, Copy, PartialEq, Eq)]\n#[pyclass(eq, eq_int)]\npub enum SSLMode {\n    Disable,\n    Allow,\n    Prefer,\n    Require,\n    VerifyCa,\n    VerifyFull,\n}\n\nstruct PyEnvVar<'a>(String, Bound<'a, PyAny>);\n\nimpl EnvVar for PyEnvVar<'_> {\n    fn read(&self, name: &str) -> Result<std::borrow::Cow<str>, std::env::VarError> {\n        // os.environ[name], or the default user if not\n        let py_str = self.1.get_item(name).ok();\n        if name == \"PGUSER\" && py_str.is_none() {\n            Ok((&self.0).into())\n        } else {\n            py_str\n                .map(|s| s.to_string().into())\n                .ok_or(std::env::VarError::NotPresent)\n        }\n    }\n}\n\n#[pyclass]\nstruct PyConnectionParams {\n    inner: RawConnectionParameters<'static>,\n}\n\n#[pymethods]\nimpl PyConnectionParams {\n    #[new]\n    #[pyo3(signature = (dsn=None))]\n    fn new(dsn: Option<String>) -> PyResult<Self> {\n        if let Some(dsn_str) = dsn {\n            match parse_postgres_dsn(&dsn_str) {\n                Ok(params) => Ok(PyConnectionParams {\n                    inner: params.to_static(),\n                }),\n                Err(err) => Err(PyException::new_err(err.to_string())),\n            }\n        } else {\n            Ok(PyConnectionParams {\n                inner: RawConnectionParameters::default(),\n            })\n        }\n    }\n\n    #[getter]\n    #[allow(clippy::type_complexity)]\n    pub fn host_candidates(\n        &self,\n        py: Python,\n    ) -> PyResult<Vec<(&'static str, Py<PyAny>, String, u16)>> {\n        // As this might be blocking, drop the GIL while we allow for\n        // resolution to take place.\n        let hosts = self\n            .inner\n            .hosts()\n            .map_err(|e| PyException::new_err(e.to_string()))?;\n        let hosts = py.detach(|| hosts.to_addrs_sync());\n        let mut errors = Vec::new();\n        let mut resolved_hosts = Vec::new();\n\n        for (host, resolved) in hosts {\n            let hostname = host.0.to_string();\n            let port = host.1;\n            match resolved {\n                Ok(addrs) => {\n                    for addr in addrs {\n                        match addr {\n                            ResolvedTarget::SocketAddr(addr) => {\n                                resolved_hosts.push((\n                                    if addr.ip().is_ipv4() { \"v4\" } else { \"v6\" },\n                                    addr.ip().to_string().into_pyobject(py)?.into(),\n                                    hostname.clone(),\n                                    addr.port(),\n                                ));\n                            }\n                            #[cfg(unix)]\n                            ResolvedTarget::UnixSocketAddr(path) => {\n                                if let Some(path) = path.as_pathname() {\n                                    resolved_hosts.push((\n                                        \"unix\",\n                                        path.to_string_lossy().into_pyobject(py)?.into(),\n                                        hostname.clone(),\n                                        port,\n                                    ));\n                                    continue;\n                                }\n\n                                #[cfg(target_os = \"linux\")]\n                                {\n                                    use std::os::linux::net::SocketAddrExt;\n                                    if let Some(name) = path.as_abstract_name() {\n                                        let mut name = name.to_vec();\n                                        name.insert(0, 0);\n                                        resolved_hosts.push((\n                                            \"unix\",\n                                            PyBytes::new(py, &name).as_any().clone().unbind(),\n                                            hostname.clone(),\n                                            port,\n                                        ));\n                                        continue;\n                                    }\n                                }\n\n                                unreachable!()\n                            }\n                        }\n                    }\n                }\n                Err(err) => errors.push(err),\n            }\n        }\n\n        if resolved_hosts.is_empty() {\n            return Err(PyException::new_err(format!(\n                \"Could not resolve addresses: {errors:?}\"\n            )));\n        }\n\n        Ok(resolved_hosts)\n    }\n\n    #[getter]\n    pub fn keys(&self) -> Vec<&str> {\n        RawConnectionParameters::field_names()\n    }\n\n    pub fn to_dict(&self) -> HashMap<String, String> {\n        self.inner.clone().into()\n    }\n\n    pub fn update_server_settings(&mut self, key: &str, value: &str) -> PyResult<()> {\n        self.inner\n            .server_settings\n            .get_or_insert_with(HashMap::new)\n            .insert(key.to_string().into(), value.to_string().into());\n        Ok(())\n    }\n\n    pub fn clear_server_settings(&mut self) -> PyResult<()> {\n        if let Some(server_settings) = &mut self.inner.server_settings {\n            server_settings.clear();\n        }\n        Ok(())\n    }\n\n    pub fn clone(&self) -> Self {\n        Self {\n            inner: self.inner.clone(),\n        }\n    }\n\n    #[pyo3(signature = (username, home_dir))]\n    pub fn resolve(\n        &self,\n        py: Python,\n        username: String,\n        home_dir: Option<String>,\n    ) -> PyResult<Self> {\n        let os = py.import(\"os\")?;\n        let environ = os.getattr(\"environ\")?;\n\n        let mut params = self.inner.clone();\n        params\n            .apply_env(PyEnvVar(username.clone(), environ))\n            .map_err(|err| PyException::new_err(err.to_string()))?;\n        let mut params = ConnectionParameters::try_from(params)\n            .map_err(|err| PyException::new_err(err.to_string()))?;\n        if let Some(warning) = params.password.resolve(\n            home_dir.as_deref().map(Path::new),\n            &params.hosts,\n            &params.database,\n            &params.user,\n        )? {\n            let warnings = py.import(\"warnings\")?;\n            warnings.call_method1(\"warn\", (warning.to_string(),))?;\n        }\n\n        params\n            .ssl\n            .resolve(home_dir.as_deref().map(Path::new))\n            .map_err(|err| PyException::new_err(err.to_string()))?;\n        Ok(Self {\n            inner: params.into(),\n        })\n    }\n\n    pub fn to_dsn(&self) -> String {\n        self.inner.to_url()\n    }\n\n    fn __repr__(&self) -> String {\n        let field_names = RawConnectionParameters::field_names();\n        let mut repr = \"<ConnectionParams\".to_owned();\n        for field_name in field_names {\n            let value = self.inner.get_by_name(field_name);\n            let Some(value) = value else {\n                continue;\n            };\n            repr.push_str(&format!(\" {field_name}={value}\"));\n        }\n        if let Some(server_settings) = &self.inner.server_settings {\n            repr.push_str(&format!(\" server_settings={server_settings:?}\"));\n        }\n        repr.push('>');\n        repr\n    }\n\n    pub fn __getitem__(&self, name: &str) -> Option<Cow<'_, str>> {\n        self.inner.get_by_name(name)\n    }\n\n    pub fn __setitem__(&mut self, name: &str, value: &str) -> PyResult<()> {\n        self.inner\n            .set_by_name(name, value.to_string().into())\n            .map_err(|e| PyException::new_err(e.to_string()))?;\n        Ok(())\n    }\n}\n\n#[pymodule]\npub fn _pg_rust(_py: Python, m: &Bound<PyModule>) -> PyResult<()> {\n    m.add_class::<PyConnectionParams>()?;\n    m.add_class::<PyConnectionState>()?;\n    m.add_class::<SSLMode>()?;\n    Ok(())\n}\n\n#[pyclass]\nstruct PyConnectionState {\n    inner: ConnectionState,\n    parsed_dsn: Py<PyConnectionParams>,\n    update: PyConnectionStateUpdate,\n    message_buffer: StructBuffer<Message<'static>>,\n}\n\n#[pymethods]\nimpl PyConnectionState {\n    #[new]\n    #[pyo3(signature = (dsn, username, home_dir))]\n    fn new(\n        py: Python,\n        dsn: &PyConnectionParams,\n        username: String,\n        home_dir: Option<String>,\n    ) -> PyResult<Self> {\n        let os = py.import(\"os\")?;\n        let environ = os.getattr(\"environ\")?;\n\n        let mut params = dsn.inner.clone();\n        params\n            .apply_env(PyEnvVar(username.clone(), environ))\n            .map_err(|err| PyException::new_err(err.to_string()))?;\n        let mut params = ConnectionParameters::try_from(params)\n            .map_err(|err| PyException::new_err(err.to_string()))?;\n        if let Some(warning) = params.password.resolve(\n            home_dir.as_deref().map(Path::new),\n            &params.hosts,\n            &params.database,\n            &params.user,\n        )? {\n            let warnings = py.import(\"warnings\")?;\n            warnings.call_method1(\"warn\", (warning.to_string(),))?;\n        }\n\n        params\n            .ssl\n            .resolve(home_dir.as_deref().map(Path::new))\n            .map_err(|err| PyException::new_err(err.to_string()))?;\n        let credentials = Credentials {\n            username: params.user.clone(),\n            password: params.password.password().unwrap_or_default().to_string(),\n            database: params.database.clone(),\n            server_settings: params.server_settings.clone(),\n        };\n        let ssl_mode = match params.ssl {\n            Ssl::Disable => ConnectionSslRequirement::Disable,\n            Ssl::Enable(SslMode::Allow | SslMode::Prefer, ..) => ConnectionSslRequirement::Optional,\n            _ => ConnectionSslRequirement::Required,\n        };\n        let params = params.into();\n        Ok(PyConnectionState {\n            inner: ConnectionState::new(credentials, ssl_mode),\n            parsed_dsn: Py::new(py, PyConnectionParams { inner: params })?,\n            update: PyConnectionStateUpdate {\n                py_update: py.None(),\n            },\n            message_buffer: Default::default(),\n        })\n    }\n\n    #[setter]\n    fn update(&mut self, update: &Bound<PyAny>) {\n        self.update.py_update = update.clone().unbind();\n    }\n\n    fn is_ready(&self) -> bool {\n        self.inner.is_ready()\n    }\n\n    fn read_ssl_response(&self) -> bool {\n        self.inner.read_ssl_response()\n    }\n\n    fn drive_initial(&mut self) -> PyResult<()> {\n        self.inner\n            .drive(ConnectionDrive::Initial, &mut self.update)\n            .map_err(|e| PyException::new_err(e.to_string()))?;\n        Ok(())\n    }\n\n    fn drive_message(&mut self, py: Python, data: &Bound<PyMemoryView>) -> PyResult<()> {\n        let buffer = PyBuffer::<u8>::get(data)?;\n        if self.inner.read_ssl_response() {\n            // SSL responses are always one character\n            let response = [buffer.as_slice(py).unwrap().first().unwrap().get()];\n            let response =\n                SSLResponse::new(&response).map_err(|e| PyException::new_err(e.to_string()))?;\n            self.inner\n                .drive(ConnectionDrive::SslResponse(response), &mut self.update)\n                .map_err(|e| PyException::new_err(e.to_string()))?;\n        } else {\n            with_python_buffer(py, buffer, |buf| {\n                self.message_buffer.push_fallible(buf, |message| {\n                    self.inner\n                        .drive(ConnectionDrive::Message(message), &mut self.update)\n                })\n            })\n            .map_err(|e| PyException::new_err(e.to_string()))?;\n        }\n        Ok(())\n    }\n\n    fn drive_ssl_ready(&mut self) -> PyResult<()> {\n        self.inner\n            .drive(ConnectionDrive::SslReady, &mut self.update)\n            .map_err(|e| PyException::new_err(e.to_string()))?;\n        Ok(())\n    }\n\n    #[getter]\n    fn config(&self, py: Python) -> PyResult<Py<PyConnectionParams>> {\n        Ok(self.parsed_dsn.clone_ref(py))\n    }\n}\n\n/// Attempt to stack-copy the data from a `PyBuffer`.\n#[inline(always)]\nfn with_python_buffer<T>(py: Python, data: PyBuffer<u8>, mut f: impl FnMut(&[u8]) -> T) -> T {\n    let len = data.item_count();\n    if len <= 128 {\n        let mut slice = [0; 128];\n        data.copy_to_slice(py, &mut slice[..len]).unwrap();\n        f(&slice[..len])\n    } else if len <= 1024 {\n        let mut slice = [0; 1024];\n        data.copy_to_slice(py, &mut slice[..len]).unwrap();\n        f(&slice[..len])\n    } else {\n        f(&data.to_vec(py).unwrap())\n    }\n}\n\nstruct PyConnectionStateUpdate {\n    py_update: Py<PyAny>,\n}\n\nimpl ConnectionStateSend for PyConnectionStateUpdate {\n    fn send_initial(&mut self, message: InitialBuilder) -> Result<(), std::io::Error> {\n        Python::attach(|py| {\n            let bytes = PyByteArray::new(py, &message.to_vec());\n            if let Err(e) = self.py_update.call_method1(py, \"send\", (bytes,)) {\n                eprintln!(\"Error in send_initial: {e:?}\");\n                e.print(py);\n            }\n        });\n        Ok(())\n    }\n\n    fn send(&mut self, message: FrontendBuilder) -> Result<(), std::io::Error> {\n        Python::attach(|py| {\n            let bytes = PyBytes::new(py, &message.to_vec());\n            if let Err(e) = self.py_update.call_method1(py, \"send\", (bytes,)) {\n                eprintln!(\"Error in send: {e:?}\");\n                e.print(py);\n            }\n        });\n        Ok(())\n    }\n\n    fn upgrade(&mut self) -> Result<(), std::io::Error> {\n        Python::attach(|py| {\n            if let Err(e) = self.py_update.call_method0(py, \"upgrade\") {\n                eprintln!(\"Error in upgrade: {e:?}\");\n                e.print(py);\n            }\n        });\n        Ok(())\n    }\n}\n\nimpl ConnectionStateUpdate for PyConnectionStateUpdate {\n    fn parameter(&mut self, name: &str, value: &str) {\n        Python::attach(|py| {\n            if let Err(e) = self.py_update.call_method1(py, \"parameter\", (name, value)) {\n                eprintln!(\"Error in parameter: {e:?}\");\n                e.print(py);\n            }\n        });\n    }\n\n    fn cancellation_key(&mut self, pid: i32, key: i32) {\n        Python::attach(|py| {\n            if let Err(e) = self\n                .py_update\n                .call_method1(py, \"cancellation_key\", (pid, key))\n            {\n                eprintln!(\"Error in cancellation_key: {e:?}\");\n                e.print(py);\n            }\n        });\n    }\n\n    fn state_changed(&mut self, state: ConnectionStateType) {\n        Python::attach(|py| {\n            if let Err(e) = self\n                .py_update\n                .call_method1(py, \"state_changed\", (state as u8,))\n            {\n                eprintln!(\"Error in state_changed: {e:?}\");\n                e.print(py);\n            }\n        });\n    }\n\n    fn auth(&mut self, auth: gel_auth::AuthType) {\n        Python::attach(|py| {\n            if let Err(e) = self.py_update.call_method1(py, \"auth\", (auth as u8,)) {\n                eprintln!(\"Error in auth: {e:?}\");\n                e.print(py);\n            }\n        });\n    }\n\n    fn server_notice(&mut self, notice: &PgServerError) {\n        warn!(\"Unexpected server notice during handshake: {:?}\", notice);\n    }\n\n    fn server_error(&mut self, error: &PgServerError) {\n        Python::attach(|py| {\n            let mut fields = vec![];\n            for (field, value) in error.fields() {\n                let etype = field as u8 as char;\n                let message = value.to_string();\n                fields.push((etype, message));\n            }\n            if let Err(e) = self.py_update.call_method1(py, \"server_error\", (fields,)) {\n                eprintln!(\"Error in server_error: {e:?}\");\n                e.print(py);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "rust/pyo3_util/Cargo.toml",
    "content": "[package]\nname = \"pyo3_util\"\nversion = \"0.1.0\"\nlicense = \"MIT/Apache-2.0\"\nauthors = [\"MagicStack Inc. <hello@magic.io>\"]\nedition = \"2021\"\n\n[lints]\nworkspace = true\n\n[dependencies]\npyo3.workspace = true\ntracing.workspace = true\ntracing-subscriber.workspace = true\ntokio.workspace = true\n\nscopeguard = \"1\"\n\n[lib]\n"
  },
  {
    "path": "rust/pyo3_util/src/channel.rs",
    "content": "use std::{\n    cell::RefCell,\n    future::poll_fn,\n    os::fd::IntoRawFd,\n    pin::Pin,\n    sync::{Arc, Mutex},\n};\n\nuse pyo3::{\n    exceptions::PyException, prelude::*, BoundObject, FromPyObject, IntoPyObject, PyAny, PyResult,\n};\nuse tokio::io::AsyncWrite;\nuse tracing::{error, trace};\n\npyo3::create_exception!(_channel, InternalError, PyException);\n\nfn internal_error(message: &str) -> PyErr {\n    error!(\"{message}\");\n    InternalError::new_err(())\n}\n\npub trait RustToPython: for<'py> IntoPyObject<'py> + Send + std::fmt::Debug {}\npub trait PythonToRust: for<'py> FromPyObject<'py> + Send + std::fmt::Debug {}\n\nimpl<T> RustToPython for T where T: for<'py> IntoPyObject<'py> + Send + std::fmt::Debug {}\nimpl<T> PythonToRust for T where T: for<'py> FromPyObject<'py> + Send + std::fmt::Debug {}\n\n/// A channel that can be used to send and receive messages between Rust and Python.\npub struct RustChannel<RX: for<'py> FromPyObject<'py>, TX: for<'py> IntoPyObject<'py> + Send> {\n    rust_to_python_notify: RefCell<tokio::net::unix::pipe::Sender>,\n    rust_to_python: std::sync::mpsc::Sender<TX>,\n    python_to_rust: RefCell<tokio::sync::mpsc::UnboundedReceiver<RX>>,\n}\n\nimpl<RX: PythonToRust, TX: RustToPython> RustChannel<RX, TX> {\n    pub async fn recv(&self) -> Option<RX> {\n        // Don't hold the lock across the await point\n        poll_fn(|cx| {\n            let pipe = &mut *self.python_to_rust.borrow_mut();\n            let mut this = Pin::new(pipe);\n            this.poll_recv(cx)\n        })\n        .await\n    }\n\n    pub async fn write(&self, msg: TX) -> Result<(), String> {\n        trace!(\"Rust -> Python: {msg:?}\");\n        self.rust_to_python.send(msg).map_err(|_| \"Shutdown\")?;\n        // If we're shutting down, this may fail (but that's OK)\n        poll_fn(|cx| {\n            let pipe = &mut *self.rust_to_python_notify.borrow_mut();\n            let this = Pin::new(pipe);\n            this.poll_write(cx, &[0])\n        })\n        .await\n        .map_err(|_| \"Shutdown\")?;\n        Ok(())\n    }\n}\n\npub struct PythonChannelImpl<RX: PythonToRust, TX: RustToPython> {\n    python_to_rust: tokio::sync::mpsc::UnboundedSender<RX>,\n    rust_to_python: Mutex<std::sync::mpsc::Receiver<TX>>,\n    notify_fd: u64,\n}\n\nimpl<RX: PythonToRust, TX: RustToPython> PythonChannelImpl<RX, TX> {\n    pub fn send(&self, msg: RX) -> Result<(), RX> {\n        self.python_to_rust.send(msg).map_err(|e| e.0)\n    }\n\n    pub fn send_err(&self, msg: RX) -> PyResult<()> {\n        self.python_to_rust\n            .send(msg)\n            .map_err(|_| internal_error(\"In shutdown\"))\n    }\n}\n\npub trait PythonChannelProtocol: Send + Sync {\n    fn _write(&self, py: Python<'_>, msg: Py<PyAny>) -> PyResult<()>;\n    fn _read<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>>;\n    fn _try_read<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>>;\n    fn _close_pipe(&mut self);\n    fn _fd(&self) -> u64;\n}\n\nimpl<RX: PythonToRust, TX: RustToPython> PythonChannelProtocol for Arc<PythonChannelImpl<RX, TX>> {\n    fn _write(&self, py: Python<'_>, msg: Py<PyAny>) -> PyResult<()> {\n        let msg = msg.extract(py)?;\n        trace!(\"Python -> Rust: {msg:?}\");\n        self.python_to_rust\n            .send(msg)\n            .map_err(|_| internal_error(\"In shutdown\"))\n    }\n\n    fn _read<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {\n        let Ok(msg) = self\n            .rust_to_python\n            .try_lock()\n            .expect(\"Unsafe thread access\")\n            .try_recv()\n        else {\n            return Ok(py.None().into_bound(py));\n        };\n        Ok(msg\n            .into_pyobject(py)\n            .map_err(|e| e.into())?\n            .into_bound()\n            .into_any())\n    }\n\n    fn _try_read<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {\n        let Ok(msg) = self\n            .rust_to_python\n            .try_lock()\n            .expect(\"Unsafe thread access\")\n            .try_recv()\n        else {\n            return Ok(py.None().into_bound(py));\n        };\n\n        Ok(msg\n            .into_pyobject(py)\n            .map_err(|e| e.into())?\n            .into_bound()\n            .into_any())\n    }\n\n    fn _close_pipe(&mut self) {\n        *self\n            .rust_to_python\n            .try_lock()\n            .expect(\"Unsafe thread access\") = std::sync::mpsc::channel().1;\n    }\n\n    fn _fd(&self) -> u64 {\n        self.notify_fd\n    }\n}\n\n#[pyclass]\npub struct PythonChannel {\n    _impl: Box<dyn PythonChannelProtocol>,\n}\n\nimpl PythonChannel {\n    pub fn new<T: PythonChannelProtocol + 'static>(imp: T) -> Self {\n        Self {\n            _impl: Box::new(imp),\n        }\n    }\n}\n\n#[pymethods]\nimpl PythonChannel {\n    fn _write(&self, py: Python<'_>, msg: Py<PyAny>) -> PyResult<()> {\n        self._impl._write(py, msg)\n    }\n\n    fn _read<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {\n        self._impl._read(py)\n    }\n\n    fn _try_read<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {\n        self._impl._try_read(py)\n    }\n\n    fn _close_pipe(&mut self) {\n        // Replace the channel with a dummy, closed one which will also\n        // signal the other side to exit.\n        self._impl._close_pipe()\n    }\n\n    #[getter]\n    fn _fd(&self) -> u64 {\n        self._impl._fd()\n    }\n}\n\n/// Create a new Python <-> Rust channel from within a tokio runtime.\npub fn new_python_channel<RX: PythonToRust, TX: RustToPython>(\n) -> (RustChannel<RX, TX>, PythonChannelImpl<RX, TX>) {\n    let (tx_sync, rx_sync) = std::sync::mpsc::channel();\n    let (tx_async, rx_async) = tokio::sync::mpsc::unbounded_channel();\n    let (tx_pipe, rx_pipe) = tokio::net::unix::pipe::pipe().unwrap();\n    let notify_fd = rx_pipe.into_nonblocking_fd().unwrap().into_raw_fd() as u64;\n    let rust = RustChannel {\n        rust_to_python_notify: RefCell::new(tx_pipe),\n        rust_to_python: tx_sync,\n        python_to_rust: RefCell::new(rx_async),\n    };\n    let python = PythonChannelImpl {\n        python_to_rust: tx_async,\n        rust_to_python: Mutex::new(rx_sync),\n        notify_fd,\n    };\n    (rust, python)\n}\n"
  },
  {
    "path": "rust/pyo3_util/src/lib.rs",
    "content": "pub mod channel;\npub mod logging;\n"
  },
  {
    "path": "rust/pyo3_util/src/logging.rs",
    "content": "use std::cell::RefCell;\nuse std::os::fd::IntoRawFd;\nuse std::sync::{Mutex, OnceLock};\nuse std::time::Duration;\n\nuse pyo3::{\n    prelude::*,\n    types::{PyAnyMethods, PyDict},\n    PyResult, Python,\n};\nuse scopeguard::defer;\nuse tracing::{subscriber::DefaultGuard, Dispatch, Level};\nuse tracing_subscriber::{filter::LevelFilter, layer::SubscriberExt};\n\n/// A useful tool for debugging logging.\n#[macro_export]\nmacro_rules! debug_log_method {\n    ($method_name:expr, $($arg:tt)*) => {\n        if is_debug_enabled() {\n            debug_log!($($arg)*);\n            defer! {\n                debug_log!(\"{} exited\", $method_name);\n            }\n        }\n    };\n}\n\n/// A simple debug logging macro that prints to stderr if debug logging is enabled.\n#[macro_export]\nmacro_rules! debug_log {\n    ($($arg:tt)*) => {\n        if is_debug_enabled() {\n            eprint!(\"LOGGING [{}]: \", std::process::id());\n            eprintln!($($arg)*);\n        }\n    };\n}\n\n/// Initializes logging for the current thread. This function should be called\n/// at the start of any new thread that needs to use logging.\n///\n/// Important: logging from threads involves a write to a socket and taking the GIL,\n/// any may have performance impacts when logging is enabled. Disabled logging is\n/// nearly free, however.\npub fn initialize_logging_in_thread(python_package: &'static str, level: LevelFilter) {\n    debug_log_method!(\n        \"initialize_logging_in_thread\",\n        \"Initializing logging in thread {python_package:?}\"\n    );\n    thread_local! {\n        static GUARD: RefCell<Option<DefaultGuard>> = const { RefCell::new(None) };\n    }\n    GUARD.with(|g| {\n        debug_log_method!(\"initialize_logging_in_thread\", \"Initializing logger bridge\");\n        let unix_socket = get_logging_socket();\n\n        debug_log!(\"Got logging socket\");\n\n        let logger_bridge = LoggerBridge {\n            unix_socket,\n            buffer: Mutex::new([0; 65536]),\n            python_package,\n        };\n\n        let dispatch = Dispatch::new(\n            tracing_subscriber::registry()\n                .with(level)\n                .with(logger_bridge),\n        );\n        *g.borrow_mut() = Some(tracing::dispatcher::set_default(&dispatch));\n    });\n}\n\nstatic EDGEDB_RUST_PYTHON_LOGGER_DEBUG: OnceLock<bool> = OnceLock::new();\n\nfn is_debug_enabled() -> bool {\n    *EDGEDB_RUST_PYTHON_LOGGER_DEBUG.get_or_init(|| {\n        std::env::var(\"EDGEDB_RUST_PYTHON_LOGGER_DEBUG\")\n            .map(|v| v == \"1\")\n            .unwrap_or(false)\n    })\n}\n\n/// The Python script that spawns the log reader thread. This runs within a blocking I/O thread\n/// to avoid interaction with the asyncio event loop.\nconst THREAD_SCRIPT: &std::ffi::CStr = cr#\"\ndef spawn_log_reader(fd):\n    import socket\n    import threading\n\n    def _log_reader(sock):\n        import socket\n        import struct\n        import logging\n\n        log_cache = {}\n\n        while True:\n            try:\n                # Receive entire datagram at once\n                data, _ = sock.recvfrom(65536) # Max UDP datagram size\n                if not data:\n                    break\n\n                # Parse level (first 4 bytes) in big-endian\n                level = struct.unpack('>I', data[:4])[0]\n\n                # Parse first string length and data in big-endian\n                str1_len = struct.unpack('>I', data[4:8])[0]\n                logger_name = data[8:8+str1_len].decode('utf-8')\n\n                # Parse second string length and data in big-endian\n                str2_start = 8 + str1_len\n                str2_len = struct.unpack('>I', data[str2_start:str2_start+4])[0]\n                msg = data[str2_start+4:str2_start+4+str2_len].decode('utf-8')\n\n                logger = log_cache.get(logger_name, None);\n                if logger is None:\n                    log_cache[logger_name] = logger = logging.getLogger(logger_name)\n                logger.log(level, msg)\n\n            except socket.error:\n                break\n        sock.close()\n\n    sock = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_DGRAM)\n    thread = threading.Thread(target=_log_reader, args=(sock,), daemon=True, name=\"Python/Rust logging bridge\")\n    thread.start()\n\ntry:\n    spawn_log_reader(fd)\nexcept Exception as e:\n    import traceback\n    traceback.print_exc()\n    raise\n\"#;\n\nfn python_to_rust_level(level: i32) -> LevelFilter {\n    match level {\n        ..10 => LevelFilter::TRACE,\n        10 => LevelFilter::DEBUG,\n        11..=20 => LevelFilter::INFO,\n        21..=30 => LevelFilter::WARN,\n        31..=40 => LevelFilter::ERROR,\n        _ => LevelFilter::OFF,\n    }\n}\n\n/// Call this from the thread that is running the Python interpreter.\npub fn get_python_logger_level(py: Python, python_package: &'static str) -> PyResult<LevelFilter> {\n    let logging = py.import(\"logging\")?;\n    let logger = logging.call_method(\"getLogger\", (python_package,), None)?;\n    let level = logger.call_method(\"getEffectiveLevel\", (), None)?;\n    debug_log!(\"Python logger '{python_package}' level = {level:?}\");\n    Ok(python_to_rust_level(level.extract::<i32>()?))\n}\n\nstruct LoggerBridge {\n    unix_socket: std::os::unix::net::UnixDatagram,\n    buffer: Mutex<[u8; 65536]>,\n    python_package: &'static str,\n}\n\nimpl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for LoggerBridge {\n    fn on_event(\n        &self,\n        event: &tracing::Event<'_>,\n        _ctx: tracing_subscriber::layer::Context<'_, S>,\n    ) {\n        debug_log_method!(\"on_event\", \"LoggerBridge on_event called: {event:?}\");\n        let mut message = format!(\"[{}] \", event.metadata().target());\n        #[derive(Default)]\n        struct Visitor(String);\n        impl tracing::field::Visit for Visitor {\n            fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {\n                if field.name() == \"message\" {\n                    self.0 += &format!(\"{value:?} \");\n                } else {\n                    self.0 += &format!(\"{}={:?} \", field.name(), value)\n                }\n            }\n        }\n\n        let mut visitor = Visitor::default();\n        event.record(&mut visitor);\n        message += &visitor.0;\n\n        // Clamp the length of message to 4kB\n        let message = &message[..message.len().min(4096)];\n\n        let level = match *event.metadata().level() {\n            Level::TRACE => 5,  // NOTSET\n            Level::DEBUG => 10, // DEBUG\n            Level::INFO => 20,  // INFO\n            Level::WARN => 30,  // WARNING\n            Level::ERROR => 40, // ERROR\n        };\n\n        let logger = self.python_package;\n        let mut lock = self.buffer.lock().unwrap();\n\n        // Write the level, logger, and message to the buffer\n        let buf = lock.as_mut_slice();\n        buf[..4].copy_from_slice(&(level as u32).to_be_bytes());\n        buf[4..8].copy_from_slice(&(logger.len() as u32).to_be_bytes());\n        buf[8..8 + logger.len()].copy_from_slice(logger.as_bytes());\n        let str2_start = 8 + logger.len();\n        buf[str2_start..str2_start + 4].copy_from_slice(&(message.len() as u32).to_be_bytes());\n        buf[str2_start + 4..str2_start + 4 + message.len()].copy_from_slice(message.as_bytes());\n\n        let total_len = str2_start + 4 + message.len();\n\n        _ = self.unix_socket.send(&buf[..total_len]);\n    }\n}\n\nstatic LOGGING_WRITER: OnceLock<std::os::unix::net::UnixDatagram> = OnceLock::new();\n\nfn get_logging_socket() -> std::os::unix::net::UnixDatagram {\n    debug_log_method!(\"get_logging_socket\", \"Getting logging socket\");\n    let tx = LOGGING_WRITER\n        .get_or_init(|| {\n            debug_log_method!(\"get_logging_socket\", \"Creating logging socket\");\n            let (tx, rx) =\n                std::os::unix::net::UnixDatagram::pair().expect(\"Failed to create logging socket\");\n            let rx = rx.into_raw_fd();\n            Python::attach(|py| {\n                debug_log_method!(\"get_logging_socket\", \"Running thread script\");\n                let locals = PyDict::new(py);\n                locals\n                    .set_item(\"fd\", rx)\n                    .expect(\"Failed to set fd in locals\");\n                py.run(THREAD_SCRIPT, None, Some(&locals))\n                    .expect(\"Failed to run thread script\");\n            });\n            tx\n        })\n        .try_clone()\n        .expect(\"Failed to clone logging socket\");\n\n    tx.set_write_timeout(Some(Duration::from_secs(10)))\n        .expect(\"Failed to set write timeout\");\n    tx\n}\n"
  },
  {
    "path": "rust-toolchain.toml",
    "content": "[toolchain]\nchannel = \"1.88\"\ncomponents = [ \"rustfmt\", \"clippy\", \"rust-analyzer\" ]\n"
  },
  {
    "path": "setup.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport binascii\nimport os\nimport os.path\nimport pathlib\nimport platform\nimport shlex\nimport shutil\nimport subprocess\nimport textwrap\n\nimport setuptools\nfrom setuptools import extension as setuptools_extension\nfrom setuptools.command import build as setuptools_build\nfrom setuptools.command import build_ext as setuptools_build_ext\n\nimport distutils\n\nimport Cython.Build\nimport setuptools_rust\n\n\nEDGEDBGUI_REPO = 'https://github.com/edgedb/edgedb-studio.git'\n# This can be a branch, tag, or commit\nEDGEDBGUI_COMMIT = 'main'\n\nPGVECTOR_REPO = 'https://github.com/pgvector/pgvector.git'\n# This can be a branch, tag, or commit\nPGVECTOR_COMMIT = 'v0.7.4'\n\nSAFE_EXT_CFLAGS: list[str] = []\nif flag := os.environ.get('EDGEDB_OPT_CFLAG'):\n    SAFE_EXT_CFLAGS += [flag]\nelse:\n    SAFE_EXT_CFLAGS += ['-O2']\n\nEXT_CFLAGS: list[str] = list(SAFE_EXT_CFLAGS)\n# See also: https://github.com/cython/cython/issues/5240\nEXT_CFLAGS += ['-Wno-error=incompatible-pointer-types']\nEXT_LDFLAGS: list[str] = []\n\nROOT_PATH = pathlib.Path(__file__).parent.resolve()\n\nEXT_INC_DIRS = [\n    (ROOT_PATH / 'edb' / 'server' / 'pgproto').as_posix(),\n    (ROOT_PATH / 'edb' / 'pgsql' / 'parser' / 'libpg_query').as_posix()\n]\n\nEXT_LIB_DIRS = [\n    (ROOT_PATH / 'edb' / 'pgsql' / 'parser' / 'libpg_query').as_posix()\n]\nEDBSS_DIR = ROOT_PATH / 'edb_stat_statements'\n\n\nif platform.uname().system != 'Windows':\n    EXT_CFLAGS.extend([\n        '-std=c99', '-fsigned-char', '-Wall', '-Wsign-compare', '-Wconversion'\n    ])\n\n\ndef _is_langserver_build() -> bool:\n    return os.environ.get(\"EDGEDB_BUILD_PACKAGE\", \"\") == \"language-server\"\n\n\ndef _compile_build_meta(build_lib, version, pg_config, runstate_dir,\n                        shared_dir, version_suffix):\n    from edb.common import verutils\n\n    parsed_version = verutils.parse_version(version)\n    vertuple = list(parsed_version._asdict().values())\n    vertuple[2] = int(vertuple[2])\n    if version_suffix:\n        vertuple[4] = tuple(version_suffix.split('.'))\n    vertuple = tuple(vertuple)\n\n    pg_config_path = pathlib.Path(pg_config)\n    if not pg_config_path.is_absolute():\n        pg_config_path = f\"_ROOT / {str(pg_config_path)!r}\"\n    else:\n        pg_config_path = repr(str(pg_config_path))\n\n    if runstate_dir:\n        runstate_dir_path = pathlib.Path(runstate_dir)\n        if not runstate_dir_path.is_absolute():\n            runstate_dir_path = f\"_ROOT / {str(runstate_dir_path)!r}\"\n        else:\n            runstate_dir_path = repr(str(runstate_dir_path))\n    else:\n        runstate_dir_path = \"None  # default to <data-dir>\"\n\n    shared_dir_path = pathlib.Path(shared_dir)\n    if not shared_dir_path.is_absolute():\n        shared_dir_path = f\"_ROOT / {str(shared_dir_path)!r}\"\n    else:\n        shared_dir_path = repr(str(shared_dir_path))\n\n    content = textwrap.dedent('''\\\n        #\n        # This source file is part of the EdgeDB open source project.\n        #\n        # Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n        #\n        # Licensed under the Apache License, Version 2.0 (the \"License\");\n        #\n        # THIS FILE HAS BEEN AUTOMATICALLY GENERATED.\n        #\n\n        import pathlib\n\n        _ROOT = pathlib.Path(__file__).parent\n\n        PG_CONFIG_PATH = {pg_config_path}\n        RUNSTATE_DIR = {runstate_dir_path}\n        SHARED_DATA_DIR = {shared_dir_path}\n        VERSION = {version!r}\n    ''').format(\n        version=vertuple,\n        pg_config_path=pg_config_path,\n        runstate_dir_path=runstate_dir_path,\n        shared_dir_path=shared_dir_path,\n    )\n\n    directory = pathlib.Path(build_lib) / 'edb'\n    if not directory.exists():\n        directory.mkdir(parents=True)\n\n    with open(directory / '_buildmeta.py', 'w+t') as f:\n        f.write(content)\n\n\ndef _get_env_with_openssl_flags():\n    env = dict(os.environ)\n    cflags = env.get('EDGEDB_BUILD_OPENSSL_CFLAGS')\n    ldflags = env.get('EDGEDB_BUILD_OPENSSL_LDFLAGS')\n\n    if not (cflags or ldflags) and platform.system() == 'Darwin':\n        try:\n            openssl_prefix = pathlib.Path(subprocess.check_output(\n                ['brew', '--prefix', 'openssl'], text=True\n            ).strip())\n        except (FileNotFoundError, subprocess.CalledProcessError):\n            openssl_prefix = None\n        else:\n            pc_path = str(openssl_prefix / 'lib' / 'pkgconfig')\n            if 'PKG_CONFIG_PATH' in env:\n                env['PKG_CONFIG_PATH'] += f':{pc_path}'\n            else:\n                env['PKG_CONFIG_PATH'] = pc_path\n        try:\n            cflags = subprocess.check_output(\n                ['pkg-config', '--cflags', 'openssl'], text=True, env=env\n            ).strip()\n            ldflags = subprocess.check_output(\n                ['pkg-config', '--libs', 'openssl'], text=True, env=env\n            ).strip()\n        except FileNotFoundError:\n            # pkg-config is not installed\n            if openssl_prefix:\n                cflags = f'-I{openssl_prefix / \"include\"!s}'\n                ldflags = f'-L{openssl_prefix / \"lib\"!s}'\n            else:\n                return env\n        except subprocess.CalledProcessError:\n            # Cannot find flags with pkg-config\n            return env\n\n    if cflags:\n        if 'CPPFLAGS' in env:\n            env['CPPFLAGS'] += f' {cflags}'\n        elif 'CFLAGS' in env:\n            env['CFLAGS'] += f' {cflags}'\n        else:\n            env['CPPFLAGS'] = cflags\n    if ldflags:\n        if 'LDFLAGS' in env:\n            env['LDFLAGS'] += f' {ldflags}'\n        else:\n            env['LDFLAGS'] = ldflags\n    return env\n\n\ndef _compile_postgres(build_base, build_temp, *,\n                      force_build=False, fresh_build=True,\n                      run_configure=True, build_contrib=True,\n                      produce_compile_commands_json=False,\n                      run_tests=False):\n\n    proc = subprocess.run(\n        ['git', 'submodule', 'status', 'postgres'],\n        stdout=subprocess.PIPE,\n        universal_newlines=True,\n        check=True,\n        cwd=ROOT_PATH,\n    )\n    status = proc.stdout\n    if status[0] == '-':\n        print('postgres submodule not initialized, '\n              'run `git submodule init; git submodule update`')\n        exit(1)\n\n    source_stamp = _get_pg_source_stamp()\n\n    postgres_build = (build_base / 'postgres').resolve()\n    postgres_src = ROOT_PATH / 'postgres'\n    postgres_build_stamp = postgres_build / 'stamp'\n\n    if postgres_build_stamp.exists():\n        with open(postgres_build_stamp, 'r') as f:\n            build_stamp = f.read()\n    else:\n        build_stamp = None\n\n    is_outdated = source_stamp != build_stamp\n\n    if is_outdated or force_build:\n        system = platform.system()\n        if system == 'Darwin':\n            uuidlib = 'e2fs'\n        elif system == 'Linux':\n            uuidlib = 'e2fs'\n        else:\n            raise NotImplementedError('unsupported system: {}'.format(system))\n\n        if fresh_build and postgres_build.exists():\n            shutil.rmtree(postgres_build)\n        build_dir = postgres_build / 'build'\n        if not build_dir.exists():\n            build_dir.mkdir(parents=True)\n        if not run_configure:\n            run_configure = not (build_dir / 'Makefile').exists()\n\n        if run_configure or fresh_build or is_outdated:\n            env = _get_env_with_openssl_flags()\n            cmd = [\n                str(postgres_src / 'configure'),\n                '--prefix=' + str(postgres_build / 'install'),\n                '--with-openssl',\n                '--with-uuid=' + uuidlib,\n            ]\n            if os.environ.get('EDGEDB_DEBUG'):\n                cmd += [\n                    '--enable-tap-tests',\n                    '--enable-debug',\n                ]\n                cflags = os.environ.get(\"CFLAGS\", \"\")\n                cflags = f\"{cflags} -O0\"\n                env['CFLAGS'] = cflags\n            subprocess.run(cmd, check=True, cwd=str(build_dir), env=env)\n\n        if produce_compile_commands_json:\n            make = ['bear', '--', 'make']\n        else:\n            make = ['make']\n\n        make_args = ['MAKELEVEL=0', '-j', str(max(os.cpu_count() - 1, 1))]\n\n        subprocess.run(\n            make + make_args,\n            cwd=str(build_dir), check=True)\n\n        if build_contrib or fresh_build or is_outdated:\n            subprocess.run(\n                make + ['-C', 'contrib'] + make_args,\n                cwd=str(build_dir), check=True)\n\n        if run_tests:\n            subprocess.run(\n                make + [\"check-world\"],\n                cwd=str(build_dir),\n                check=True,\n                env=os.environ | {\"MAKELEVEL\": \"0\"},\n            )\n\n        subprocess.run(\n            ['make', 'MAKELEVEL=0', 'install'],\n            cwd=str(build_dir), check=True)\n\n        if build_contrib or fresh_build or is_outdated:\n            subprocess.run(\n                ['make', '-C', 'contrib', 'MAKELEVEL=0', 'install'],\n                cwd=str(build_dir), check=True)\n\n        pg_config = (\n            build_base / 'postgres' / 'install' / 'bin' / 'pg_config'\n        ).resolve()\n        _compile_pgvector(pg_config, build_temp)\n        _compile_edb_stat_statements(pg_config, build_temp)\n\n        with open(postgres_build_stamp, 'w') as f:\n            f.write(source_stamp)\n\n        if produce_compile_commands_json:\n            shutil.copy(\n                build_dir / \"compile_commands.json\",\n                postgres_src / \"compile_commands.json\",\n            )\n\n\ndef _compile_pgvector(pg_config, build_temp):\n    git_rev = _get_git_rev(PGVECTOR_REPO, PGVECTOR_COMMIT)\n\n    pgv_root = (build_temp / 'pgvector').resolve()\n    if not pgv_root.exists():\n        subprocess.run(\n            [\n                'git',\n                'clone',\n                '--recursive',\n                PGVECTOR_REPO,\n                pgv_root,\n            ],\n            check=True\n        )\n    else:\n        subprocess.run(\n            ['git', 'fetch', '--all'],\n            check=True,\n            cwd=pgv_root,\n        )\n\n    subprocess.run(\n        ['git', 'reset', '--hard', git_rev],\n        check=True,\n        cwd=pgv_root,\n    )\n\n    cflags = os.environ.get(\"CFLAGS\", \"\")\n    cflags = f\"{cflags} {' '.join(SAFE_EXT_CFLAGS)} -std=gnu99\"\n\n    args = [\n        f'PG_CONFIG={pg_config}',\n        # By default pgvector tries -march=native, which causes grief\n        'OPT_FLAGS=',\n    ]\n\n    subprocess.run(\n        [\n            'make',\n            *args,\n        ],\n        cwd=pgv_root,\n        check=True,\n    )\n\n    subprocess.run(\n        [\n            'make',\n            'install',\n            *args,\n        ],\n        cwd=pgv_root,\n        check=True,\n    )\n\n\ndef _compile_edb_stat_statements(pg_config, build_temp):\n    subprocess.run(\n        [\n            'make',\n            f'PG_CONFIG={pg_config}',\n        ],\n        cwd=EDBSS_DIR,\n        check=True,\n    )\n\n    subprocess.run(\n        [\n            'make',\n            'install',\n            f'PG_CONFIG={pg_config}',\n        ],\n        cwd=EDBSS_DIR,\n        check=True,\n    )\n\n\ndef _get_env_with_protobuf_c_flags():\n    env = dict(os.environ)\n    cflags = env.get('EDGEDB_BUILD_PROTOBUFC_CFLAGS')\n    ldflags = env.get('EDGEDB_BUILD_PROTOBUFC_LDFLAGS')\n\n    if not (cflags or ldflags) and platform.system() == 'Darwin':\n        try:\n            prefix = pathlib.Path(subprocess.check_output(\n                ['brew', '--prefix', 'protobuf-c'], text=True\n            ).strip())\n        except (FileNotFoundError, subprocess.CalledProcessError):\n            prefix = None\n        else:\n            pc_path = str(prefix / 'lib' / 'pkgconfig')\n            if 'PKG_CONFIG_PATH' in env:\n                env['PKG_CONFIG_PATH'] += f':{pc_path}'\n            else:\n                env['PKG_CONFIG_PATH'] = pc_path\n        try:\n            cflags = subprocess.check_output(\n                ['pkg-config', '--cflags', 'protobuf-c'], text=True, env=env\n            ).strip()\n            ldflags = subprocess.check_output(\n                ['pkg-config', '--libs', 'protobuf-c'], text=True, env=env\n            ).strip()\n        except (FileNotFoundError, subprocess.CalledProcessError):\n            # pkg-config is not installed or cannot find flags with pkg-config\n            if not prefix:\n                prefix = pathlib.Path(\"/opt/local\")\n            cflags = f'-I{prefix / \"include\"!s}'\n            ldflags = f'-L{prefix / \"lib\"!s}'\n\n    if cflags:\n        if 'CPPFLAGS' in env:\n            env['CPPFLAGS'] += f' {cflags}'\n        elif 'CFLAGS' in env:\n            env['CFLAGS'] += f' {cflags}'\n        else:\n            env['CPPFLAGS'] = cflags\n    if ldflags:\n        if 'LDFLAGS' in env:\n            env['LDFLAGS'] += f' {ldflags}'\n        else:\n            env['LDFLAGS'] = ldflags\n    return env\n\n\ndef _compile_libpg_query():\n    dir = (ROOT_PATH / 'edb' / 'pgsql' / 'parser' / 'libpg_query').resolve()\n\n    if not (dir / 'README.md').exists():\n        print('libpg_query submodule has not been initialized, '\n              'run `git submodule update --init --recursive`')\n        exit(1)\n\n    cflags = os.environ.get(\"CFLAGS\", \"\")\n    cflags = f\"{cflags} {' '.join(SAFE_EXT_CFLAGS)} -std=gnu99\"\n\n    env = _get_env_with_protobuf_c_flags()\n    if \"CFLAGS\" in env:\n        env[\"CFLAGS\"] += f' {cflags}'\n    else:\n        env[\"CFLAGS\"] = cflags\n\n    subprocess.run(\n        [\n            'make',\n            'build',\n            '-j',\n            str(max(os.cpu_count() - 1, 1)),\n        ],\n        cwd=str(dir),\n        env=env,\n        check=True,\n    )\n\n\ndef _get_git_rev(repo, ref):\n    output = subprocess.check_output(\n        ['git', 'ls-remote', repo, ref],\n        universal_newlines=True,\n    ).strip()\n\n    if output:\n        rev, _ = output.split()\n        rev = rev.strip()\n    else:\n        rev = ''\n\n    # The name can be a branch or tag, so we attempt to look it up\n    # with ls-remote. If we don't find anything, we assume it's a\n    # commit hash.\n    return rev if rev else ref\n\n\ndef _get_pg_source_stamp():\n    from edb.buildmeta import hash_dirs\n\n    output = subprocess.check_output(\n        ['git', 'submodule', 'status', '--cached', 'postgres'],\n        universal_newlines=True,\n        cwd=ROOT_PATH,\n    )\n    revision, _, _ = output[1:].partition(' ')\n    edbss_dir = EDBSS_DIR.as_posix()\n    edbss_hash = hash_dirs(\n        [(edbss_dir, '.c'), (edbss_dir, '.sql')],\n        extra_files=[\n            EDBSS_DIR / 'Makefile',\n            EDBSS_DIR / 'edb_stat_statements.control',\n        ],\n    )\n    edbss = binascii.hexlify(edbss_hash).decode()\n    stamp_list = [revision, PGVECTOR_COMMIT + '_v3', edbss]\n    if os.environ.get('EDGEDB_DEBUG'):\n        stamp_list += ['debug']\n    source_stamp = '+'.join(stamp_list)\n    return source_stamp.strip()\n\n\ndef _get_libpg_query_source_stamp():\n    output = subprocess.check_output(\n        ['git', 'submodule', 'status', '--cached',\n         'edb/pgsql/parser/libpg_query'],\n        universal_newlines=True,\n        cwd=ROOT_PATH,\n    )\n    revision, _, _ = output[1:].partition(' ')\n    return revision.strip()\n\n\n_PYTHON_ONLY = os.environ.get(\"BUILD_EXT_MODE\", \"both\") == \"skip\"\n\n\nclass build(setuptools_build.build):\n\n    user_options = setuptools_build.build.user_options\n\n    sub_commands = setuptools_build.build.sub_commands if _PYTHON_ONLY else [\n        (\"build_libpg_query\", lambda self: True),\n        *setuptools_build.build.sub_commands,\n        (\"build_metadata\", lambda self: True),\n        (\"build_parsers\", lambda self: True),\n        (\"build_postgres\", lambda self: True),\n        (\"build_ui\", lambda self: True),\n    ]\n\n\nclass build_metadata(setuptools.Command):\n\n    build_lib: str\n\n    user_options = [\n        ('pg-config=', None, 'path to pg_config to use with this build'),\n        ('runstatedir=', None, 'directory to use for the runtime state'),\n        ('shared-dir=', None, 'directory to use for shared data'),\n        ('version-suffix=', None, 'dot-separated local version suffix'),\n    ]\n\n    def initialize_options(self):\n        self.build_lib = None\n        self.editable_mode = False\n        self.pg_config = None\n        self.runstatedir = None\n        self.shared_dir = None\n        self.version_suffix = None\n\n    def finalize_options(self):\n        self.set_undefined_options(\"build_py\", (\"build_lib\", \"build_lib\"))\n        if self.pg_config is None:\n            self.pg_config = os.environ.get(\"EDGEDB_BUILD_PG_CONFIG\")\n        if self.runstatedir is None:\n            self.runstatedir = os.environ.get(\"EDGEDB_BUILD_RUNSTATEDIR\")\n        if self.shared_dir is None:\n            self.shared_dir = os.environ.get(\"EDGEDB_BUILD_SHARED_DIR\")\n        if self.version_suffix is None:\n            self.version_suffix = os.environ.get(\"EDGEDB_BUILD_VERSION_SUFFIX\")\n\n    def has_build_metadata(self) -> bool:\n        return bool(\n            self.pg_config\n            or self.runstatedir\n            or self.shared_dir\n            or self.version_suffix\n        )\n\n    def get_outputs(self) -> list[str]:\n        if self.has_build_metadata():\n            return [\n                str(pathlib.Path(self.build_lib) / 'edb' / '_buildmeta.py'),\n            ]\n        else:\n            return []\n\n    def run(self, *args, **kwargs):\n        if self.has_build_metadata():\n            _compile_build_meta(\n                self.build_lib,\n                self.distribution.metadata.version,\n                self.pg_config,\n                self.runstatedir,\n                self.shared_dir,\n                self.version_suffix,\n            )\n\n\nclass ci_helper(setuptools.Command):\n\n    description = \"echo specified hash or build info for CI\"\n    user_options = [\n        ('type=', None,\n         'one of: cli, rust, ext, parsers, postgres, libpg_query, bootstrap, '\n         'build_temp, build_lib'),\n    ]\n\n    def run(self):\n        import edb as _edb\n        from edb.buildmeta import hash_dirs, get_cache_src_dirs\n\n        build = self.get_finalized_command('build')\n        pkg_dir = pathlib.Path(_edb.__path__[0])\n\n        if self.type == 'parsers':\n            parser_hash = hash_dirs(\n                [(pkg_dir / 'edgeql/parser/grammar', '.py')],\n                extra_files=[\n                    pkg_dir / 'edgeql-parser/src/keywords.rs',\n                    pkg_dir / 'edgeql-parser/edgeql-parser-python/src/parser.rs'\n                ],\n            )\n            print(binascii.hexlify(parser_hash).decode())\n\n        elif self.type == 'postgres':\n            print(_get_pg_source_stamp())\n\n        elif self.type == 'libpg_query':\n            print(_get_libpg_query_source_stamp())\n\n        elif self.type == 'bootstrap':\n            bootstrap_hash = hash_dirs(\n                get_cache_src_dirs(),\n                extra_files=[\n                    pkg_dir / 'server/bootstrap.py',\n                    pkg_dir / 'buildmeta.py',\n                ],\n            )\n            print(binascii.hexlify(bootstrap_hash).decode())\n\n        elif self.type == 'rust':\n            dirs = []\n            # HACK: For annoying reasons, metapkg invokes setup.py\n            # with an ancient version of Python, and that doesn't have\n            # tomllib.  It doesn't invoke *this* code path, though, so\n            # import it here.\n            import tomllib\n\n            # Read the list of Rust projects from Cargo.toml\n            with open(pkg_dir.parent / 'Cargo.toml', 'rb') as f:\n                root = tomllib.load(f)\n                for member in root['workspace']['members']:\n                    dirs.append(pkg_dir.parent / member)\n            rust_hash = hash_dirs(\n                [(dir, '.rs') for dir in dirs],\n                extra_files=[dir / 'Cargo.toml' for dir in dirs] +\n                  [pkg_dir.parent / 'Cargo.lock'])\n            print(binascii.hexlify(rust_hash).decode())\n\n        elif self.type == 'ext':\n            import gel\n\n            ext_hash = hash_dirs(\n                [\n                    (pkg_dir, '.pyx'),\n                    (pkg_dir, '.pyi'),\n                    (pkg_dir, '.pxd'),\n                    (pkg_dir, '.pxi'),\n                ],\n                # protocol.pyx for tests links to edgedb-python binary\n                extra_data=gel.__version__.encode(),\n            )\n            print(\n                binascii.hexlify(ext_hash).decode() + '-'\n                + _get_libpg_query_source_stamp()\n            )\n\n        elif self.type == 'ui':\n            print(_get_git_rev(EDGEDBGUI_REPO, EDGEDBGUI_COMMIT))\n\n        elif self.type == 'build_temp':\n            print(pathlib.Path(build.build_temp).resolve())\n\n        elif self.type == 'build_lib':\n            print(pathlib.Path(build.build_lib).resolve())\n\n        else:\n            raise RuntimeError(\n                f'Illegal --type={self.type}; can only be: '\n                'cli, rust, ext, postgres, libpg_query, bootstrap, parsers,'\n                'build_temp or build_lib'\n            )\n\n    def initialize_options(self):\n        self.type = None\n\n    def finalize_options(self):\n        pass\n\n\nclass build_postgres(setuptools.Command):\n\n    description = \"build postgres\"\n\n    user_options = [\n        ('configure', None, 'run ./configure'),\n        ('build-contrib', None, 'build contrib'),\n        ('fresh-build', None, 'rebuild from scratch'),\n        ('compile-commands', None, 'produce compile-commands.json using bear'),\n        ('run-tests', None, 'run Postgres test suite after building'),\n    ]\n\n    editable_mode: bool\n\n    def initialize_options(self):\n        self.editable_mode = False\n        self.configure = False\n        self.build_contrib = False\n        self.fresh_build = False\n        self.compile_commands = False\n        self.run_tests = False\n\n    def finalize_options(self):\n        pass\n\n    def run(self, *args, **kwargs):\n        if os.environ.get(\"EDGEDB_BUILD_PACKAGE\"):\n            return\n        build = self.get_finalized_command('build')\n        _compile_postgres(\n            pathlib.Path(build.build_base).resolve(),\n            pathlib.Path(build.build_temp).resolve(),\n            force_build=True,\n            fresh_build=self.fresh_build,\n            run_configure=self.configure,\n            build_contrib=self.build_contrib,\n            produce_compile_commands_json=self.compile_commands,\n            run_tests=self.run_tests,\n        )\n\n\nclass build_libpg_query(setuptools.Command):\n\n    description = \"build libpg_query\"\n\n    user_options: list[str] = []\n\n    editable_mode: bool\n\n    def initialize_options(self):\n        self.editable_mode = False\n\n    def finalize_options(self):\n        pass\n\n    def run(self):\n        _compile_libpg_query()\n\n\nclass build_ext(setuptools_build_ext.build_ext):\n\n    user_options = setuptools_build_ext.build_ext.user_options + [\n        ('cython-annotate', None,\n            'Produce a colorized HTML version of the Cython source.'),\n        ('cython-extra-directives=', None,\n            'Extra Cython compiler directives'),\n    ]\n\n    def initialize_options(self):\n        # initialize_options() may be called multiple times on the\n        # same command object, so make sure not to override previously\n        # set options.\n        if getattr(self, '_initialized', False):\n            return\n\n        super(build_ext, self).initialize_options()\n\n        if os.environ.get('EDGEDB_DEBUG'):\n            self.cython_always = True\n            self.cython_annotate = True\n            self.cython_extra_directives = \"linetrace=True\"\n            self.define = 'PG_DEBUG,CYTHON_TRACE,CYTHON_TRACE_NOGIL'\n            self.debug = True\n        else:\n            self.cython_always = False\n            self.cython_annotate = None\n            self.cython_extra_directives = None\n            self.debug = False\n        self.build_mode = os.environ.get('BUILD_EXT_MODE', 'both')\n\n    def finalize_options(self) -> None:\n        # finalize_options() may be called multiple times on the\n        # same command object, so make sure not to override previously\n        # set options.\n        if getattr(self, '_initialized', False):\n            return\n\n        if self.build_mode not in {'both', 'py-only', 'rust-only', 'skip'}:\n            raise RuntimeError(f'Illegal BUILD_EXT_MODE={self.build_mode}; '\n                               f'can only be \"both\", \"py-only\" or \"skip\".')\n        if self.build_mode not in {'both', 'py-only'}:\n            super(build_ext, self).finalize_options()\n            return\n\n        directives: dict[str, str | bool] = {\n            'language_level': '3'\n        }\n\n        if self.cython_extra_directives:\n            for directive in self.cython_extra_directives.split(','):\n                k, _, v = directive.partition('=')\n                if v.lower() == 'false':\n                    v = False\n                if v.lower() == 'true':\n                    v = True\n\n                directives[k] = v\n\n        self.distribution.ext_modules[:] = Cython.Build.cythonize(\n            self.distribution.ext_modules,\n            compiler_directives=directives,\n            annotate=self.cython_annotate,\n            include_path=[\"edb/server/pgproto/\"])\n\n        super(build_ext, self).finalize_options()\n\n    def run(self):\n        if self.build_mode != 'skip':\n            super().run()\n        else:\n            distutils.log.info(f'Skipping build_ext because '\n                               f'BUILD_EXT_MODE={self.build_mode}')\n\n\nclass build_ui(setuptools.Command):\n\n    description = \"build EdgeDB UI\"\n    user_options: list[str] = []\n    editable_mode: bool\n    build_lib: str\n\n    def initialize_options(self):\n        self.editable_mode = False\n        self.build_lib = None\n\n    def finalize_options(self):\n        self.set_undefined_options(\"build_py\", (\"build_lib\", \"build_lib\"))\n\n    def run(self, *args, **kwargs):\n        if _is_langserver_build():\n            return\n\n        from edb import buildmeta\n        from edb.common import devmode\n\n        try:\n            buildmeta.get_build_metadata_value(\"SHARED_DATA_DIR\")\n        except buildmeta.MetadataError:\n            # buildmeta path resolution needs this\n            devmode.enable_dev_mode()\n\n        build = self.get_finalized_command('build')\n        self._build_ui(pathlib.Path(build.build_base).resolve())\n\n    def _build_ui(self, build_base: pathlib.Path) -> None:\n        from edb import buildmeta\n\n        git_ref = os.environ.get(\"EDGEDB_UI_GIT_REV\") or EDGEDBGUI_COMMIT\n        git_rev = _get_git_rev(EDGEDBGUI_REPO, git_ref)\n\n        ui_root = build_base / 'edgedb-studio'\n        if not ui_root.exists():\n            subprocess.run(\n                [\n                    'git',\n                    'clone',\n                    '--recursive',\n                    EDGEDBGUI_REPO,\n                    ui_root,\n                ],\n                check=True\n            )\n        else:\n            subprocess.run(\n                ['git', 'fetch', '--all'],\n                check=True,\n                cwd=ui_root,\n            )\n\n        subprocess.run(\n            ['git', 'reset', '--hard', git_rev],\n            check=True,\n            cwd=ui_root,\n        )\n\n        dest = buildmeta.get_shared_data_dir_path() / 'ui'\n        if dest.exists():\n            shutil.rmtree(dest)\n\n        # install deps\n        subprocess.run(['yarn'], check=True, cwd=ui_root)\n\n        # run build\n        env = dict(os.environ)\n        # With CI=true (set in GH CI) `yarn build` fails if there are any\n        # warnings. We don't need this check in our build so we're disabling\n        # this behavior.\n        env['CI'] = ''\n        subprocess.run(\n            ['yarn', 'build'],\n            check=True,\n            cwd=ui_root / 'web',\n            env=env\n        )\n\n        shutil.copytree(ui_root / 'web' / 'build', dest)\n\n\nclass build_parsers(setuptools.Command):\n\n    description = \"build and serialize the parser grammar spec\"\n\n    build_lib: str\n    target_root: pathlib.Path\n    editable_mode: bool\n    inplace: bool\n\n    user_options = [\n        ('inplace', None,\n         'ignore build-lib and put compiled parsers into the source directory '\n         'alongside your pure Python modules')]\n\n    def initialize_options(self):\n        self.editable_mode = False\n        self.inplace = None\n        self.build_lib = None\n        self.target_root = None\n\n    def finalize_options(self):\n        self.set_undefined_options(\"build_py\", (\"build_lib\", \"build_lib\"))\n        if self.editable_mode:\n            self.inplace = True\n        if self.inplace:\n            self.target_root = ROOT_PATH\n        else:\n            self.target_root = pathlib.Path(self.build_lib)\n\n    def run(self, *args, **kwargs):\n        # load grammar definitions and build the parser\n        from edb.common import parsing\n        from edb.edgeql.parser import grammar as qlgrammar\n        spec = parsing.load_parser_spec(qlgrammar.start)\n        spec_json = parsing.spec_to_json(spec)\n\n        # prepare destination\n        dst = str(self.target_root / 'edb' / 'edgeql' / 'grammar.bc')\n        os.makedirs(os.path.dirname(dst), exist_ok=True)\n\n        # serialize\n        import edb._edgeql_parser as rust_parser\n        rust_parser.save_spec(spec_json, str(dst))\n\n\nclass build_rust(setuptools_rust.build.build_rust):\n    def run(self):\n        build_ext = self.get_finalized_command(\"build_ext\")\n        if build_ext.build_mode not in {'both', 'rust-only'}:\n            distutils.log.info(\n                f'Skipping build_rust because '\n                f'BUILD_EXT_MODE={build_ext.build_mode}'\n            )\n            return\n        self.plat_name = build_ext.plat_name\n        copy_list = []\n        if not build_ext.inplace:\n            for ext in self.distribution.rust_extensions:\n                # Always build in-place because later stages of the build\n                # may depend on the modules having been built\n                dylib_path = pathlib.Path(\n                    build_ext.get_ext_fullpath(ext.name))\n                build_ext.inplace = True\n                target_path = pathlib.Path(\n                    build_ext.get_ext_fullpath(ext.name))\n                build_ext.inplace = False\n                copy_list.append((dylib_path, target_path))\n\n                # Workaround a bug in setuptools-rust: it uses\n                # shutil.copyfile(), which is not safe w.r.t mmap,\n                # so if the target module has been previously loaded\n                # bad things will happen.\n                if target_path.exists():\n                    target_path.unlink()\n\n                target_path.parent.mkdir(parents=True, exist_ok=True)\n\n        os.environ['CARGO_TARGET_DIR'] = str(\n            pathlib.Path(build_ext.build_temp) / 'rust' / 'extensions',\n        )\n        super().run()\n\n        for src, dst in copy_list:\n            shutil.copyfile(src, dst)\n\n\ndef _version():\n    from edb import buildmeta\n    return buildmeta.get_version_from_scm(ROOT_PATH)\n\n\n_protobuf_c_flags = _get_env_with_protobuf_c_flags()\n_protobuf_c_cflags = shlex.split(_protobuf_c_flags.get(\"CPPFLAGS\", \"\"))\n\n\nsetuptools.setup(\n    version=_version(),\n    cmdclass={\n        'build': build,\n        'build_metadata': build_metadata,\n        'build_ext': build_ext,\n        'build_rust': build_rust,\n        'build_postgres': build_postgres,\n        'build_parsers': build_parsers,\n        'build_ui': build_ui,\n        'build_libpg_query': build_libpg_query,\n        'ci_helper': ci_helper,\n    },\n    ext_modules=[\n        setuptools_extension.Extension(\n            \"edb.common.turbo_uuid\",\n            [\"edb/server/pgproto/uuid.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.server.cache.stmt_cache\",\n            [\"edb/server/cache/stmt_cache.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.protocol.protocol\",\n            [\"edb/protocol/protocol.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.server.pgproto.pgproto\",\n            [\"edb/server/pgproto/pgproto.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.server.dbview.dbview\",\n            [\"edb/server/dbview/dbview.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.server.protocol.binary\",\n            [\"edb/server/protocol/binary.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.server.protocol.pg_ext\",\n            [\"edb/server/protocol/pg_ext.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.server.protocol.args_ser\",\n            [\"edb/server/protocol/args_ser.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.server.protocol.execute\",\n            [\"edb/server/protocol/execute.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.server.protocol.auth_helpers\",\n            [\"edb/server/protocol/auth_helpers.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.server.protocol.notebook_ext\",\n            [\"edb/server/protocol/notebook_ext.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.server.protocol.ui_ext\",\n            [\"edb/server/protocol/ui_ext.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.server.protocol.edgeql_ext\",\n            [\"edb/server/protocol/edgeql_ext.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.server.protocol.frontend\",\n            [\"edb/server/protocol/frontend.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.server.protocol.protocol\",\n            [\"edb/server/protocol/protocol.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n        setuptools_extension.Extension(\n            \"edb.server.pgcon.pgcon\",\n            [\"edb/server/pgcon/pgcon.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.graphql.extension\",\n            [\"edb/graphql/extension.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.pgsql.parser.parser\",\n            [\"edb/pgsql/parser/parser.pyx\"],\n            extra_compile_args=EXT_CFLAGS + _protobuf_c_cflags,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n            library_dirs=EXT_LIB_DIRS,\n            libraries=['pg_query']\n        ),\n\n        setuptools_extension.Extension(\n            \"edb.server.compiler.rpc\",\n            [\"edb/server/compiler/rpc.pyx\"],\n            extra_compile_args=EXT_CFLAGS,\n            extra_link_args=EXT_LDFLAGS,\n            include_dirs=EXT_INC_DIRS,\n        ),\n    ],\n    rust_extensions=[\n        setuptools_rust.RustExtension(\n            \"edb._edgeql_parser\",\n            path=\"edb/edgeql-parser/edgeql-parser-python/Cargo.toml\",\n            features=[\"python_extension\"],\n            binding=setuptools_rust.Binding.PyO3,\n        ),\n        setuptools_rust.RustExtension(\n            \"edb._graphql_rewrite\",\n            path=\"edb/graphql-rewrite/Cargo.toml\",\n            features=[\"python_extension\"],\n            binding=setuptools_rust.Binding.PyO3,\n        ),\n        setuptools_rust.RustExtension(\n            \"edb.server._rust_native\",\n            path=\"edb/server/_rust_native/Cargo.toml\",\n            features=[\"python_extension\"],\n            binding=setuptools_rust.Binding.PyO3,\n        ),\n    ],\n)\n"
  },
  {
    "path": "tests/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport unittest\n\n\ndef suite():\n    test_loader = unittest.TestLoader()\n    test_suite = test_loader.discover('.', pattern='test_*.py')\n    return test_suite\n"
  },
  {
    "path": "tests/certs/.gitignore",
    "content": "*.txt*\n*.csr*\n*.srl\n"
  },
  {
    "path": "tests/certs/ca.cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIG+zCCBOOgAwIBAgIURGhmXq3lrQDD650rd+ZqQ4PBajMwDQYJKoZIhvcNAQEL\nBQAwgaYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH\nDA1TYW4gRnJhbmNpc2NvMRQwEgYDVQQKDAtFZGdlREIgSW5jLjEVMBMGA1UECwwM\nRWRnZURCIHRlc3RzMRwwGgYDVQQDDBNFZGdlREIgdGVzdCByb290IGNhMR8wHQYJ\nKoZIhvcNAQkBFhBoZWxsb0BlZGdlZGIuY29tMB4XDTI1MDEyOTIwNDAyN1oXDTQ1\nMDEyNDIwNDAyN1owgaYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh\nMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRQwEgYDVQQKDAtFZGdlREIgSW5jLjEV\nMBMGA1UECwwMRWRnZURCIHRlc3RzMRwwGgYDVQQDDBNFZGdlREIgdGVzdCByb290\nIGNhMR8wHQYJKoZIhvcNAQkBFhBoZWxsb0BlZGdlZGIuY29tMIICIjANBgkqhkiG\n9w0BAQEFAAOCAg8AMIICCgKCAgEAyrVFGhnSZL/EY0h450+4mWERRUD2I8v8sIjv\nZd5qTf5UEDk1Wg6HOIJHtNmgmVOv0b6jQ9duHuktT4BhU6MFVRel4f1rbX77i6r6\nM6O151uu/iDxxYJJ1MiPQVTfNPH1x/DG8zAHc8XZLKp7S9Lz45k3RQE6RC6cdV2S\nCIrCKE/bTm6o3/0ceqrlcOgFuPncdcRBzc0qNl2ecjySzhgcXZZzjoVb7lJ0n4NS\n6v8YdB26SceJZJfIhSIFwEkmpGbSYlZpL+2BLkxRg29w7uVoxWtjzQ0DdULrqkWu\nWOwQ4w06SIvNQxyYiGWY3bMGELkWUpxQaBBPfTYpbP3quosKpa+LiFAmjIEpKj/R\nRZTEQDAogadJoT5rvBYDffdtpK2s95wBfZbZhTumNJEOtD86CjAmBbyHvgM3HW2S\na2aEdq7K3k17Ogk1vB3LwMARgYCbdSBNovayCHdsBsn5tchwNvMalSrLMV+mmm2f\nJ/yZsDtVeXYcIfZA00Kz26jdBct1a7MBXbQdOqxoIC3aPDV5S3VLqk+lLQolbil9\nwelUj/mgW9gfafyfD83fTiHCQwgFC0pLKojYGfWLt67yqs9wvXHNdbv/CMr3Ub5v\n6G1NooEP5BtRImOwtMIhDnrBRprsi5xHa1QJtLp91P8cXU7wzwUnA+594qwiNHeH\nU0cqHLUCAwEAAaOCAR0wggEZMB0GA1UdDgQWBBQUcTQtXJpgHoIH5Zuy5WaiCGUk\nyjCB5gYDVR0jBIHeMIHbgBQUcTQtXJpgHoIH5Zuy5WaiCGUkyqGBrKSBqTCBpjEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBG\ncmFuY2lzY28xFDASBgNVBAoMC0VkZ2VEQiBJbmMuMRUwEwYDVQQLDAxFZGdlREIg\ndGVzdHMxHDAaBgNVBAMME0VkZ2VEQiB0ZXN0IHJvb3QgY2ExHzAdBgkqhkiG9w0B\nCQEWEGhlbGxvQGVkZ2VkYi5jb22CFERoZl6t5a0Aw+udK3fmakODwWozMA8GA1Ud\nEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAEFPR/3ga+tJenOAlNwykYma\n4rz1ryxizrww2TgFKmA7VdPK+NqyoTuK+OhikJ0r2GCk6jEQvru4e7pBxsUoBpTP\nd6wv/xNkMnDgS/J9VGziQF3ZarbayyQdLlt+J/kmvxiF9/RUyB0x3f3hfx/yqrRD\nb07/0qRgW0pIk/D5Hs+D3FnPpr3i0pg1iauLwnWptagobFEM27eHAuqOmmc8EMUP\nhHmwhEI7vQ1NZ7X/+gdVx/CyIiDYBw88/x4aEH/6rVPZU3Ifg5GINGyMyndJ+WtV\n21CSFz8SqG+K2pU3k9wjorJcSZr2ChLEi9gyHH4Rk9ADHYxiKg+qb11Mysyx4Qoo\npyfGeJq1qhb2ScJrdTuj2NctQhUVTQoeIWh36zdf6sxRLXcj8M6gQz5joed1/7Y7\nYgOXq3gmI4e9you/F4Vqhdl3ojhstkpW8fWQiXYvWIvOctgjGtZoLP4gZujgGs4h\n+fdX5pMmOoSPrriUINo+0W55hhIFIDisUNkxC1PKkjdU4XjckFC9iJeT0VsKLPzK\nHMN0LA7BxEjLnV0TG/vXaoaM+rlz11nMTwt7BNSWqhH+6lxwgOSG3Ft7u8svt7x/\n6b706AHItpYRegu+NWzVfrbntZsJSdGSYi8mZwvIjSrA8o/zFpWepINlWph14mxU\nbmAtF79u44d/tAoBn/gH\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/certs/ca.conf",
    "content": "[ ca ]\ndefault_ca = CA_default\n\n[ CA_default ]\ndatabase = index.txt\nserial = serial.txt\ncrlnumber = crlnumber.txt\ncrl_extensions = crlext\ndefault_crl_days = 3650\ndefault_days = 3650\n\n[ req ]\ndistinguished_name = req_distinguished_name\nx509_extensions = v3_ca\n\n[ req_distinguished_name ]\nC = US\nST = California \nL = San Francisco\nO = EdgeDB Inc.\nOU = EdgeDB tests\nCN = EdgeDB test root ca\nemailAddress = hello@edgedb.com\n\n[ v3_ca ]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid:always,issuer:always\nbasicConstraints = critical,CA:true\n\n[ v3_req ]\nbasicConstraints = CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment\nsubjectAltName = @alt_names\n\n[ alt_names ]\nDNS.1 = localhost\n\n[ crlext ]\nauthorityKeyIdentifier = keyid:always,issuer:always\n"
  },
  {
    "path": "tests/certs/ca.crl.pem",
    "content": "-----BEGIN X509 CRL-----\nMIIEFTCCAf0CAQEwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAlVTMRMwEQYD\nVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRQwEgYDVQQK\nDAtFZGdlREIgSW5jLjEVMBMGA1UECwwMRWRnZURCIHRlc3RzMRwwGgYDVQQDDBNF\nZGdlREIgdGVzdCByb290IGNhMR8wHQYJKoZIhvcNAQkBFhBoZWxsb0BlZGdlZGIu\nY29tFw0yNTAxMjkyMDQwMjlaFw0zNTAxMjcyMDQwMjlaMCcwJQIUZqAmIZS9Cgjj\n65btVW6Z6rl4/LMXDTI1MDEyOTIwNDAyOVqggfgwgfUwgeYGA1UdIwSB3jCB24AU\nFHE0LVyaYB6CB+WbsuVmoghlJMqhgaykgakwgaYxCzAJBgNVBAYTAlVTMRMwEQYD\nVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRQwEgYDVQQK\nDAtFZGdlREIgSW5jLjEVMBMGA1UECwwMRWRnZURCIHRlc3RzMRwwGgYDVQQDDBNF\nZGdlREIgdGVzdCByb290IGNhMR8wHQYJKoZIhvcNAQkBFhBoZWxsb0BlZGdlZGIu\nY29tghREaGZereWtAMPrnSt35mpDg8FqMzAKBgNVHRQEAwIBATANBgkqhkiG9w0B\nAQsFAAOCAgEAXAp/4mtAufO/c9qJnRstYZOfP4yAxxDK5YiVVWYN6FuuYO5mxsEY\nQEylw7dByDxP4rxuQ3xFUOxGzcyjnGnZI9wIQbsh78b5nM0xH8ndXoQ5AdB+lBse\nvuLqAbzPggDgfjewc4TndWaEXzCQMk24pDiYWHTFy/Hr80AotDMmsYxs0V7/1dSJ\nZHkHxkEVepnU3/uHZ866uLbo8TFYrzkLfjfO9ykKPJK9FsL2xaz2S+cET0oHx5Hx\nCQDU8mjbtjYxClyj9/MSSa9Xvo5SkmCwm4WOzSnNnuUZSopVYlpwCWP0HXOKS++b\n5FF9Nz93rZ7cNMZiCUP2obj1wny8PeXRAcM/gP2W49BFKwmFFFRPWoddjAhf0IYL\nkmN2nxW/HWXgDBKou7CsQQXFpL6H8vcEjXjveHYe9pThE89vuMT+WS6GeWTreilV\nHUSv67m1TlBVxO2Yv32PZdoDj9t0cFZBRkGy0IZb7D+legS69vZ7TOz3SS9G9nUp\nn6Kb5B9i+naOLsSI2hQlosWlVhPY1AFUGxUnEV5GJqU0gc2xODwRG20MV9fRkBRJ\nMO2sl16rIDz/R5C1yXBAayiy6q90s4lubSksIxgQrGX84U9XwBNxHbJE2vp5qOan\n9Eyzx9/+Pio4ZT8ndULPntU51KktKE1bNGl8u3ID2sVEiq9Ln4FX4jY=\n-----END X509 CRL-----\n"
  },
  {
    "path": "tests/certs/ca.key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDKtUUaGdJkv8Rj\nSHjnT7iZYRFFQPYjy/ywiO9l3mpN/lQQOTVaDoc4gke02aCZU6/RvqND124e6S1P\ngGFTowVVF6Xh/WttfvuLqvozo7XnW67+IPHFgknUyI9BVN808fXH8MbzMAdzxdks\nqntL0vPjmTdFATpELpx1XZIIisIoT9tObqjf/Rx6quVw6AW4+dx1xEHNzSo2XZ5y\nPJLOGBxdlnOOhVvuUnSfg1Lq/xh0HbpJx4lkl8iFIgXASSakZtJiVmkv7YEuTFGD\nb3Du5WjFa2PNDQN1QuuqRa5Y7BDjDTpIi81DHJiIZZjdswYQuRZSnFBoEE99Nils\n/eq6iwqlr4uIUCaMgSkqP9FFlMRAMCiBp0mhPmu8FgN9922kraz3nAF9ltmFO6Y0\nkQ60PzoKMCYFvIe+AzcdbZJrZoR2rsreTXs6CTW8HcvAwBGBgJt1IE2i9rIId2wG\nyfm1yHA28xqVKssxX6aabZ8n/JmwO1V5dhwh9kDTQrPbqN0Fy3VrswFdtB06rGgg\nLdo8NXlLdUuqT6UtCiVuKX3B6VSP+aBb2B9p/J8Pzd9OIcJDCAULSksqiNgZ9Yu3\nrvKqz3C9cc11u/8IyvdRvm/obU2igQ/kG1EiY7C0wiEOesFGmuyLnEdrVAm0un3U\n/xxdTvDPBScD7n3irCI0d4dTRyoctQIDAQABAoICAA7qRUI1Odq0P4jcnsTKWWIJ\nFze6HsJQh/ByCvIQalBV0S1RFaC25p0yUitiQ05fhCrR36UmF1IKmeJlJwQmlRsH\nPpXryyPx5CQJOcK4njGpTdggvW6+dkdEAJfFMm1CC8chQMpJCOtXA0f65/qSyY5A\nH6XMqtMJ05+mebsg1pttEM0VwwhEJqch/E2RTPEo4AoBnaPnyjODh505NsYXq71O\nd4SPlyLoifCTaEj76c/gFhfH/k8wBI7t4MV0EctRTilH2WfaORdFLKRCpr51Pn8e\n4TzdDcRANHiWtaGRBnG7xJjs7HauhndWyRIwUixt1KYnHprMRG+z9LAH2OXOmu2n\ne18e5askP+UlhVRDyIVr7lroEEWB8VFkye4XTG8M3LPoC1B8HqsGOcg/lhjcjDh9\ncmvx9CdOzjFz9rcvUdqGK1fltHOTsMvDmjM7nOUiGjtVhDtYk4VShMi1dYf1D6ff\n4BYS/X/A3E8s65JO0wt3NT/H7848UvponNwOp1v+M7f2+ICgNxRkZn5ApFAezzM0\nxZ6WeCxe6WY+Vw9iqWeStRAZa56Ai9Ez5Ou8i0BYzLF2A5d/RMbnhJWK1L7Hx+06\np+9fAdAr/ITiI0m2QKKyc28tm8DAWIPbC+XB8MAqaikNfxUW6lh5/Nq3DVpBWNyo\nlFlP6SfYDoxeNvj/QMPrAoIBAQDr1nDaP39wvJbJXPwPhUJJAXf98orDfZKVZRkn\nwUB/6aYWY4pGk/XD1afHpf5rXxiK1onVEO8LA47NoDLjbvCnc9iTqsw3QKbILNzV\nV7KK2c0H1icCjjd9yvW3xbgjuCPFPsbC6NhfFEeQiD9veOy2JZoAeby6drqu0Ae1\nkB4FqDkOP2MNVaPQYk+A3TitNRdMxjIkjAcMYwn/vQ7ch7GxJ1JstHDS1vwfY1/7\niaWt19giejCMPRijdsH4Fygo1Kv9D4k8Yd06tcrf58qVlZFGjS0pVWkw83kPd0hs\nlxoW3r7mv98egsjQz2PuLT7gqdUEjq30r/Mstnliih5oKYtjAoIBAQDcCcDDHN8c\noeiaV9fjXXQbNVzLA+LSqK/vBmHYBk3ds5BElMPAA6KMzbhO7xjNgcD5AjhLTM1F\n3Ei+W1PS9AP4ZNAarntZn0BC+tI7E6owDfBYH/L4R20YsqK5KVea9COFER5KY8pM\nwjURqQM7zgKsRCTxKnpSA1/n2jN7WiSgStlCdTejY4e0L6Y7wbNM/D3tkT6mxV22\nLF1dK5JV/lM9PwxZZEVGJimWQGubfBYv48PaodVoVVgjuARV8otxwhr2mH7G9GS0\nsx/ui+BI5vkvEm0h+olQ97U9cJ3wKR/K3XSr92ij385DWPM5wVlyA4TXAAXVks7v\nP3fU9JzZmY8HAoIBAAMMR3MapPwBA/XgRMWylDO7WCCpFNAH/G//2X5hCgNdMq9R\nZAUbfm6kgUGcTJh4pymMMkXVrTE4P406x82WrneLkL3/1BnWtREbO1Nqib0vqW2z\nf9eRnPf8OobAgGu9woCXGhyEw98etPoSOLepGW4VOFNPP3gtdqYxvBfFoA20qeAc\nQ5x1geN8kch5k3TxnbZ5TUaZpLGtSgDLIbkJ6+r9Nhx/jIG9E48YSrJGiiSgCIQR\njjURyRK7wzAApJ06emqP29cy8JgEp3WTWlPqlfESfAXvu6dNTkA762yz2zt2b4Mt\n8aVETXIdbA40+X4P09f2PBtQdtUaGqGCZXg1KT8CggEBAMbu3mLIUIK/ct9PufRq\nglUzCoDVM5XHQsB0YbOAB5gABteqM8v+vVBVkWNz0VXDEKdQNXsGpbOac/397awU\nRx6kbm0hAI5HZz7nK3iTz9MAVyIlSHLliKHCp1GGKhkCzrY4gs04qSZ6kqYzyqOg\nHlSGi2uqPsq1GFkyska8ec6dvQzTkwjaLE9goQb3mdZpWsfU//KhD5drRsG8aeHr\nPHBr9ws+l07To9eeyGrbZefIIUMh+yIHvtcUQH8/+IhRuDToK/5N6Fpic+UkexMO\nF41SOG525vzX5vj0PyZo18B+NURgOy+lYQMMgWHfB7IHsmr7L0snHoW5OOrEeKZW\nqbMCggEAKP4hxxL5E9wAOkcZ/20KyUVX5oj7GecJi8gEBzLS1wHCV00tOBT2qhmj\n9rnhYH5m+GGkJON6UyYdK/mKvv2Smlca6dQMnT7J0bAIXX3YTDt2KktiWshvdPCH\nWcfG1DNyDuA2QX28iBJwbHtd0ddWAgjoAPs0w7fos52ogvvOfRiTBObgHbP0HSjc\nY50UWCFg4LwYthhVFotDPfAnRDWYEKiE8lQcrPoAbzY6Ugvo2PC0xKp7UCIly2cV\nHJlXwoiZprG9pLEJ2SDxQLdhGahxefIBSP1xP0Nt9Z8P6/qsWotOxd608kOFdph/\nHEQwFKprD1VR7j6l2kChJIMHc5RJxQ==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/certs/client.cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIGQzCCBCugAwIBAgIUPfej8IQ/5bCrihqWImrq2vKPOr4wDQYJKoZIhvcNAQEL\nBQAwgagxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH\nDA1TYW4gRnJhbmNpc2NvMRQwEgYDVQQKDAtFZGdlREIgSW5jLjEVMBMGA1UECwwM\nRWRnZURCIHRlc3RzMR4wHAYDVQQDDBVFZGdlREIgdGVzdCBjbGllbnQgQ0ExHzAd\nBgkqhkiG9w0BCQEWEGhlbGxvQGVkZ2VkYi5jb20wHhcNMjUwMTI5MjA0MDI5WhcN\nNDUwMTI0MjA0MDI5WjCBmzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3Ju\naWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFDASBgNVBAoMC0VkZ2VEQiBJbmMu\nMRUwEwYDVQQLDAxFZGdlREIgdGVzdHMxETAPBgNVBAMMCHNzbF91c2VyMR8wHQYJ\nKoZIhvcNAQkBFhBoZWxsb0BlZGdlZGIuY29tMIICIjANBgkqhkiG9w0BAQEFAAOC\nAg8AMIICCgKCAgEAxTM5bI1hyl3t8Iy2sFcDZgjhSinUDTF9m0CDUailyDjpbOI+\n2tVGVW2rILNSiZ7zsIgnqCCotuVLpKeulafDpiw2/cdz1m3c6icXpKswX/KCCXEz\nNcXpy6hJ69BXJaWBChtZ303SAfNAaP3mIXund/j4J0IrtHq1OEbqs+D9BSVNI5b5\nkE4GMmxsZQwhs0NebZQFQOoQao1f4DhaIbdNKWizu4EmcGYMtFGqy4LUwXgVFecT\n9eisqxzcITeT0NWKMdBeWSGYC7Hfcjwa1KtCPzJSYM9HC5QwU2VPqVBGPnmk5tMj\nPende8mUbcYHxUzCVbk//LkaeMN0co2NYEoqEa9gpS1u2Q/cqVEuVs45OpyXA7qj\nK0h7u+hmoofps3Xi8KaU2RR3eRxAaRXauzC2Uv8ObHhz9l6ZREc0jy04yJe2TL9o\nEW20CTwQRviosZG9jlErUYD4zehG4h3BIk5gSSrsPfjkcnfGjOcIL8ld18K1QscH\nTxvfpS0N4h3HtZ4czn/Gpbdotq/EzdCmxGzB841d6oN4GZcX8kHluvsuzdNCk8Ww\nQuEEosF6Ju5YYE89WWfxGdVnDojXtr1j1YBcsBgmHWf5/CcxHSrGeZCWm2ggt42u\nIJGzIs61I/6j7m/RLaTF8zKzizewEye1BrCCQUAEefSLKYw/Cbun3U7zm3UCAwEA\nAaNwMG4wCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwFAYDVR0RBA0wC4IJbG9jYWxo\nb3N0MB0GA1UdDgQWBBSLmPfPnfxJ512zj4FZw09PSsK09zAfBgNVHSMEGDAWgBQy\nJOoeR8Y5TkgQ9vmRjskM3yS1qTANBgkqhkiG9w0BAQsFAAOCAgEAcaU5/Sl/ooHI\nCS1ed3RP7TNYlrWWEB6fiPS0XQtzXmNizTuqS9Z3mDagZ1YZmj96G9vTftgGFiKw\nu/ULQsEJmmkaNpaPup48pr6KFwh1mSp3KkCrxd14Qm4LBny5b83Z98eMrsQLjWWJ\nwjHE9FLnyaTAoLTLJ8AgGXCDOzxDitrrplWC/Y58RWvnDiHTcAsO1OO1QLHYLT5y\nqhXjwh48BQl/Ww2sIa8tfHKWDEG9bIP2oxFT83j4ZeVnxX97Hvuh/co4aK4k+MJp\nkSYUzo9rj333E/dPoMMZ9E0CvkR7twQ48TKYCMIK7JrG+e1mcEU+zeT6RwOf4tQx\nICfiwHcxUGMtAwlpFYQ8WMnM8xLLPhElGRmVSXBnrbBXRFJ5yp1zVB0nQ4EZPNFz\nKlWrc7txEQADM51p0YRpxI559zeTEm0ZvTIr5z/5VuzAvIE9mvchAmc8QG8rTLMu\nhf9B+XhfzCACRTfFTPlLXO45J7YzhPKl8ys2Lw+kPaeoPkta07oyiZJTAvoU728Z\nvSmHa6xsFVaB2QWjwf2iUMmVcHM5hoja8SCo5p+3sudh9C4ol+bEKfRJZfMh7svc\ntcNOWC+fVIVAERr0pYdND8CKplrNFUth7jtLZ07jhYTgLwiBLuADq6XAA+pm3eQt\nvR6+GZsfuWGM8iVlCZPSFrZCngjEZKg=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/certs/client.key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDFMzlsjWHKXe3w\njLawVwNmCOFKKdQNMX2bQINRqKXIOOls4j7a1UZVbasgs1KJnvOwiCeoIKi25Uuk\np66Vp8OmLDb9x3PWbdzqJxekqzBf8oIJcTM1xenLqEnr0FclpYEKG1nfTdIB80Bo\n/eYhe6d3+PgnQiu0erU4Ruqz4P0FJU0jlvmQTgYybGxlDCGzQ15tlAVA6hBqjV/g\nOFoht00paLO7gSZwZgy0UarLgtTBeBUV5xP16KyrHNwhN5PQ1Yox0F5ZIZgLsd9y\nPBrUq0I/MlJgz0cLlDBTZU+pUEY+eaTm0yM96d17yZRtxgfFTMJVuT/8uRp4w3Ry\njY1gSioRr2ClLW7ZD9ypUS5Wzjk6nJcDuqMrSHu76Gaih+mzdeLwppTZFHd5HEBp\nFdq7MLZS/w5seHP2XplERzSPLTjIl7ZMv2gRbbQJPBBG+Kixkb2OUStRgPjN6Ebi\nHcEiTmBJKuw9+ORyd8aM5wgvyV3XwrVCxwdPG9+lLQ3iHce1nhzOf8alt2i2r8TN\n0KbEbMHzjV3qg3gZlxfyQeW6+y7N00KTxbBC4QSiwXom7lhgTz1ZZ/EZ1WcOiNe2\nvWPVgFywGCYdZ/n8JzEdKsZ5kJabaCC3ja4gkbMizrUj/qPub9EtpMXzMrOLN7AT\nJ7UGsIJBQAR59IspjD8Ju6fdTvObdQIDAQABAoICAAaWkwUO5iT6JWBjexkCFzF5\n+3jcU+LK8/zTV2/LeBpr2FSUbHAoLuzcuJpjk07YuiB8NAL4cFqH55KNLZZ/X9h1\n4rJwzuxriDVkb/RG6dtSwUhqeUEDJy/wI+QWdkriMCDzz/lXYrxA8bZwQRd+R5aJ\nAVVibw5dCR/jsqSm0B0zZVPaj+Tjzm4x1B8+HAQerJFxoAlOqJS+u2TEzTISKyhG\ngLazdmLL7gG6NsAM02nRRrcQHMka6GXhFXOgpTYVZEQMx5InvqopW6M34deYDyMI\nXdsxIZj/7utiV5p6/ZXJv5oclLFVVlpz3hsawhiz20w836fkUT+WnBXVp9L/N0+H\n7J/MmbbDgsLIMpPUMC45kEYfFuYjZYvcnhxHfnfG7IQm1HGOq5pAVUiZ022JD9Y8\nVvYJLOUQJEce320ZaJrBnzcH7ApxcrcJdcwargGqPl1QVNWcjqb0tzQgtVvgRpC9\naynqifAWIx7UHVfECZw33k5kZOz62PgJ1OjYw2QXYiJO7ROICOnJR3YxYNtYyV39\nO01kuVtVYZ680J6VacIYVRe88DoV7BBacoHFaNIeE4eBKvO7RGcG+IdkRSviEZpF\nJzh9m8q8qTt0lUf9pExH8RdmSfTYW2bnoj2GZrMT4XwiPsjELb6Klz3CI51LTcwq\nZUlqx5ehq2KiKl3Y3pHjAoIBAQDn/BfHcadRPS4Yua1y7iSmHFV1O5VGS+lAWH2l\nRhsAakuk7MfeyRAe5/t6t9N+oO4L6JTIE5oNHdKOxaL8fpzb58R/6OJI/BSISJq5\nyN7qPvBDDfQVygdITjtNSskq+1FQMMx1lmKhjOyx+DAI6MwDW8YqdkFyfMy2GcZD\nYr/11m26zLF3KCKrYD3Vq08nHhDc0jAXXw+xYf/+iGOvWk26MW59UJp4smJ4PUL8\nS+KNsFQrd7nU+jWGtcXaPCql6qk35nG4rNy3uBnLNSuKA7oPOffN2dvPpKTg8NgU\n/kXu+LUdU5aHFXdPGl8mKnOKD+JJd4FDF1tcUwokn2ug/yk3AoIBAQDZnUqrFpUL\nFj1Qu78wnaSFLqj3l0NtYeTCmYbae+1qRV2V1/W4Q5yASypOBey5du5A76n05o8O\n2v88jJ++LbqG5y9QJlbl6cQox4xjG2mhHnFPR/1SXU8leikiQCUmndlIAbuPIY76\n+QfUsvGAVxWDTrMS2quXRO1MzzL7v2pfHTL80enU+6x9YLUQMp5pZKg3nV5m3ZPH\nUQdu/jyxVipiPOywYSzNyrTqBYChXLJEE6V0Ty2VMT3eOydWfaU2hd0p2ht7WuEQ\nL2W53Kb0H/SNh/GwWpiN4XJKd/yQlcJkXitGgZ5xcp2aY7ydHtbbfT+MCmRP0ljT\nYFvG2pv6QYazAoIBAQDmx/4r9l/aTL/H7yejF1A0RCmr56t39FrGHYJZYeXIwvYf\ny0KG2oUECgo+qhNnfNdz11vzsrRlag6m4+xhvd90URxFlztOGiCe94OdYTyJ0jUY\nsA/rgUE/aDxMhyKbdMsOuI0eSY4zYsuxvNKkeltC2BDK+zvellLcscVwEhQfj8M3\nuxytCqt6y9KX9sVWh+2EkEExbutgrrqJz8tDjdWXbkeZuQ8DFYsqTN+PuDpYdFs3\npvKi8os/SSPcGFDhIBJZvxHRA37L8gUPCAUZVt3I+gUQrzOiXQt9j3uXXuHZe2hK\nFXbBSdSYSAbyI/cvGOAn8BAS23CS5zVG+6WJ021/AoIBAQC3ZL6rpvFekZSE+GSt\nFP20m9kcJ6dUhH6knXwvnuc7e0/eW00iyCAZYr85V/bjal5p7VCfKrr/ewJFRgHN\n5X3f+O8/rb/oLPT6pQkj8NM5TI2TkgjkI+zymZwW2FY20Cpwa71kZ5S2366Ay3mJ\nflqL+YQi7JRVfGo8JBZEYVHE7Leupz1YF+2LEDgneXFVQtYdYItRR4UmIZyRJsOB\ndCtt9QhHsO1wVVfYLWD1HEjD5Ia4mY3BwOjx44pIcsUMSQ5VGhG1CKbJ3Bfv5gvx\niwivShUeWYtdbtTB+5KnSv6zVUVFOzGLTFuT5F/tTjMmcMxgOdXGC3B+WyOdV1jC\nM/zPAoIBAGsuz6FfKGBGL8C8B6vcfjWCsEtzT0l7HzYIhAiSqoacIWuXwlSlvM/Q\nGiCd1A09QXNH78KVwmnz0s87qz4D3jV4udhM1TuR7WSZEnw0pUlbV69LV3S/1enF\nh3WW2yX/6pA+BHB9YlJc+2Ef1D6skl3NsKQieLcRoqKwAniGK78cTZe66H0tMNIW\n9VcGQFJmJkejgxez5MJpXBZJh2lEXjKe3iz1n5g+NL3M/pzzFuqX8YXpUDA07XxZ\nrD1+sICtSKUHXJYFxZ7XORP1sEH1dGiWdX1ZzO/mKuj0rAASCm39I6aJg5FfLVT8\nTRkcU50kVlx/0XcGTR0F9Cp2fhO8NHo=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/certs/client.key.protected.pem",
    "content": "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIJrTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQI8pk14cPNvL4CAggA\nMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBB19/mX8iRRmlPlXkActtl1BIIJ\nULcY2iuRm2B5jDGEqbchfcFlUun2U5umvf7QNbbGXawb1+pLrbWhOmrIvMbIszCg\nLYxzfSgMx9MbQLMMbA3hCnULu6p26sVYzMJtHj2BNJwUCesQ2IiyPxowfaRMDjHK\nN2BAjUhTURjxTvzuu6/8kqeeTE36vhKJcV9bgpnot/44GLVAyscJQqr5J06POqGQ\nf1W2oBoVnyM0S2KBrUu+MXp3HwW62UWP5PSwEcrnMqRuTjBZaZO+fKXpKaAjLJom\n4BLo39FrOCep0hRGjdqD/QmoVs2QDosSxCui4iWQbcitCNTYYV/VigO/krQp9yQJ\nhZ03frSEp3Ws9Z1RNDKXdko3+/6axnW+REXcTiVngt8lMN+i+G5JrFoZKVUJM9re\nW9TKaaZaTcdSeKoPgyUyaollZtW6qTDqRd5eh2sErFg6CCWU0smDS+p3U2DN1plg\n/6F9k6PiGYpKTd35Jfn8mBpqZtNIw5iEa6QiT771AjSOAo+v6gwv+U8RSqG9V4qZ\nhyLSc7OVaUleZJwNncyFYhX/uuMEeN14NOIBqWyRwq0UXKXWwkQlYu6uscg7C5NV\nTE2TR5my+0ga2btaTrnw2CuYo9l3HVGteDv8WgoRgsrj/QT/GAqCAHfDunzsY7nH\n3wNSHk2blJ6i8pJhAFDv2itscIaKhYqQb9gPXf4zEsopq6hQ0O2rYoyOG8tgqQhE\nQoxxGwxeInepESS+iiF0gRWJdYhhEHstPdvPBhx+Jt/ElmFXipfSR4QbPwyBsGmg\nUTh/ImD+t1/S1yg/e4eKqlkF6dVwuX3ZNY0sFzgzcSSNOhcgfGIr8adJrb9XclJQ\nWwiajdnO3G0jxs033qgqLbQdrLyX/U8BnW/uWM0M9Ov/+rm5iLSnu1HG8wcaY/FB\nIid15vyyJWpVaVcCwiQd+cSVPzDW4igHJicUAUdQdJqPdSUdz6zcHrh7J7amj/nE\nOFANvXiXCm14OmCKpoxU42jJsnn4nrRKEgBo0JZUebqRhz+pw30n3VYXOcwMu/E3\n1HF/f3AFhPxG2qmcLJJ8E2Pk8SaNE+xjoopULW6NyJhamBOcbyr1oPMjeC0ZD8Ll\n0y9RGMBHk6vLjemSwDrV0n3zrDjHQQ/0x0mhO6bHlGBz/wvrmdy7hbq1gsBq8QL5\n89810NuFrAvxo9Z1oL0pBZMKfcwnWyaKRy2DZ526v5zc5LUNn6F7NtnijG1fiNcz\naApnzZ33KN+SJxRDhslBwDqRMQwhQW/1jdPJ60xFfB7jzKLTv9zZkpryriHu2ihc\nmGu47LQZbf09azyvDYhdu6a0SMYEuDTLrfzxhVOCXxFJviYSUb2pdDSlRFMqTrJD\nUHD59drsJghx/qWJXXmd/BMGSFZATHocWgqv5N11zkaKOpsoR+6GRBreMflK30dy\nO+WIb0lSo8DosRq3FJz7H3ABvsnGVctO+UsNhOhK5gnAUp1+MxJ17zyuQv8DYKZX\nWNtxFl6RZUdLER6m9z2Vs+uQ/JelZa9ssI+Mj1mvZyDgi6bVR1TIW0/XeG1XOch2\nJP0vZHh4KvjAfPjGPj+YA4EOBVEUtTcJAFYGAGsO8LeFFegB4Cs//GT43xFoGYwS\n0XQnzU+feHgv7voHwZOFm991fpTGnpzzEJyFtFQC19v6MG2DsxanPPXNMTNZJJkg\nF27tcvyxl825RaPqIA5x7INiPWahMRlagrDOB3PzbCRLsunrAY/bbnbhUippgqm8\n62mE42tuQxV8e+pBs0ACvyINtbRur7VYKIhFm1wyj8OawHK/Mkc8p7bKYJ0NAIn8\nBk+YghGzpWwVAjVFZlGr7bNBPwe1rR7Y9WUKJsezR3opXTpi1uJVx6pomTWf0t3S\nOdI4v82ltDh9kFdRoCLG+WO1tUtz1DlAZPyUuVsgnlk0bVVFaxoTyqKlQUkbIIYe\niHrgrls9/z3DOo60g1ryGQ0g1suqg1M1WUKjzMf3C1ave3WoGMKhvjJEEcuXuhv0\n2IvbqFGyzwRrQY3B2YAlGqsucLCFZq/KGH8B5B1hyeAokjpwYzxnezPKaf5EBd4Q\nbY4q36YyUNVUwNYYxnSNbqXd5oPlYrYgYGNrIWeHrB8fgpCWRVRW8NgpPwFa5fpy\nlqCwduyhiYHytJHFHYWYvUhoujSZA6HLXYxndVsHCHWuNCGPRg0IplYqvj01+WMm\nhSN9N5BE86N6jrmA/lX+N4XxXWKZ4WVcNB0SDxTx+B7yx+fszGSsSindBYXJknmh\nJv5ef0QOFAnF4GH+9+tglgSMRoPMudGoCypisxc1Is3vqthB+pwwcsl/3gdBpVsh\nR4BzswBn8AmKOnGNbSxRFsKACg96jIcXHesDp9aF/6o0mbX/uj2EJrmM8EO47l9b\nmkgTX00cRwFM4E+7r6ugGJILub7fjFeNPHxDJ4+XXYcjDoyG1VW07VH/UCJYjaR6\n/Zs0JmH2WOHHgXmOgN6Tofp18K0Az0hxzW70vnv1ecUrmLuKLJdwktZ2n9j9QuZ5\nY88voeTDW/qdl+C4FLqJ8o+ARk6pkI8v3JZdSKfo85sSZUbKmT8qqmtIRmcUZzaq\niiTTylUBwzoCjOOj/ewiKY9RMLuUMU5yKZyVcDu1HDmIrY322TZ/HBch7HoX2ziQ\nj9i4pX3Eeu11/2t1+78t666Lpw7VM6sdXOq1UJeBxJ3vn6uYvaNfUxc8Ki9Dhfbt\nK9Ct1gBIOPMTAMC4Q3YogLoq+jLYzxUejQ46GQFZd9M/nWPcAeR4VI5PwEIxa/8Q\nDAskoPJgFZytA1BF8vAsRUtmoWId5tQkE8oHK9bIHk6gIj3JVs/COrzpiIeYToF/\n1XM/5Ex5sfnQKorIdZjnGM7HVCnEAofLHKheR8Gcake5txe28gaTFqLW7Nv9sCv6\niU3n8Atcnnls32ToH6kdVBxXaEoMJrQo2/7pDNzfzAtGF7zVMLQFp1AYqKSOYRqF\nNcg/jhiKP+GBtIMrcdNMCQHy+Kyau2V+vla7F9z5uLf5CQ21iZYKgl5Lv0iQmio1\nAuAKgp6Hkt5dFNx0gF72izkmeQRZZQg7a3x7qWNrIfAcKMktIl4yXXOYwEobq3l1\n4LNl4yI4kax41KgXdpma9HrhiTENZyNWdmkgophD0gCOK9Ald70J+OZkCvx0OQuH\nb2YbLpG6R62LPLXDXNZSNWvznJOLOJyLtxe+oNb5aEpq\n-----END ENCRYPTED PRIVATE KEY-----\n"
  },
  {
    "path": "tests/certs/client_ca.cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIGMzCCBBugAwIBAgIUHbVsfHTBUgD8chRAUzPcWl8rGSUwDQYJKoZIhvcNAQEL\nBQAwgagxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH\nDA1TYW4gRnJhbmNpc2NvMRQwEgYDVQQKDAtFZGdlREIgSW5jLjEVMBMGA1UECwwM\nRWRnZURCIHRlc3RzMR4wHAYDVQQDDBVFZGdlREIgdGVzdCBjbGllbnQgQ0ExHzAd\nBgkqhkiG9w0BCQEWEGhlbGxvQGVkZ2VkYi5jb20wHhcNMjUwMTI5MjA0MDI4WhcN\nNDUwMTI0MjA0MDI4WjCBqDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3Ju\naWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFDASBgNVBAoMC0VkZ2VEQiBJbmMu\nMRUwEwYDVQQLDAxFZGdlREIgdGVzdHMxHjAcBgNVBAMMFUVkZ2VEQiB0ZXN0IGNs\naWVudCBDQTEfMB0GCSqGSIb3DQEJARYQaGVsbG9AZWRnZWRiLmNvbTCCAiIwDQYJ\nKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKybpkWhPpMncwyrbUmPWyjYh0v42XS+\n+Mr+G45OAzt6H+FCnOKLTaHV3dOAAztmYcc/XjBJ51Tv/bRqn8ou2xhCAX5eky2i\n8xBq04dYLz1iatT2Gvla0JpxozNxKEQhGN/AaC6BsGomSf05sjgrTI/b5tEZxzv3\niqdyewPq9bYPiEjU7r7lQBOGUfmItpUQq1NQXhBO6ezovFAZ52iPxfyIdatl9CWY\nbZWc/VgEIc3ojjsfspeXvkbDawQlIkJgBFD+O5BPDltF5bZG0MLcSU1fnWpwFlf5\nCqf+H7F7dk001zD90Uq1evBFmYbQMD2IIeDP05kK5aeWcRX9ibbEydUaRKh6L1Ag\nJrfmBoneoMcjxbdiK43b8lsPnkEeuvCwu5VN0upHLGJW7Fy60assL0hCvSMbmKTJ\nqIHQd+sINifXq2vAjVZdUJompHrnypJxIrxdl+sTSYP/veZwvxuLdTUXQceiofEc\naR/y7OcLOIgywQkbqkrO/Cx1fcoPopr++dAEWQ2VhoSPuoC9f54dxgeMXj1/Kmxy\nu4z89Xz6HfWqNDYbAEwSaWFOOWewUVfnsJ7gcptE2Arn+NlllGQN10DKdwHKUiKG\nYfBhd3tdMlMkkNtowUGHhny32eoEnQv3Xqrd4rqe8cE58bKacM+OhhPkfWA2UT9w\nbYA2mUVcg+izAgMBAAGjUzBRMB0GA1UdDgQWBBQyJOoeR8Y5TkgQ9vmRjskM3yS1\nqTAfBgNVHSMEGDAWgBQyJOoeR8Y5TkgQ9vmRjskM3yS1qTAPBgNVHRMBAf8EBTAD\nAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQAmq6ca42dFmuRrIzMTKQgAAD70QC1dgGAh\nN8miJlbTfzsqXKtiMezZCQmeBJHVb3ljYtf3HkddqxQG8YGHOYTbxjvpoMhUbQe7\n/r2iBtYtMcempJPpflT3GqNPZcESMQKJP7Cq8uZjSbHAE3kgXxH6/89J9DP2V0mF\nfOs3pnenvsEQPnPub/QZ4XGRtUJH1+0ALlBfvULiG8Sm3pMS0wvOeNB7GkXwUl1V\nZvEhCTJUX1lrgYVmw/wnypAffABIXl1PIeXP2GVxANPjFhFgoSKJI1iAvMoCeCAQ\nJLuH7Vxk1nA9BaQavwvWJdDlfdtDMTrMegM9U2dSHeqw1Ai8lyxsINnCRtmLSRui\n7DztPojiUanBJSPNJPzcAjJvFJRJ/xafHL3uhK31cMo15yUskxPPJvSfVzDvRGBX\nGQjN7BIcFVx9CZYwjlVkXjGXCbUs+LAo5ecN6MmQ/BU4AKJ8GHcH2VBQ34W7H1TU\nZlnQxlzus+eMlhAjkoEKQn7SLjHdZT0SqYBPKXDymj2/VtvTqJgwcrf2E0pKBDh3\nTlmL+OuSLjwmhoQo2YASw0wb1NKJuALvYUfULl7en+WQPpfYvcHdShqWe8cid3T9\nBSksp7qKiKm67n4okeOqhUJu68yJ1ffdAnZCoy8DQmTouIWSAf2/5Z4En9rfT8+a\nnuI9Lyjq7Q==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/certs/client_ca.key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCsm6ZFoT6TJ3MM\nq21Jj1so2IdL+Nl0vvjK/huOTgM7eh/hQpzii02h1d3TgAM7ZmHHP14wSedU7/20\nap/KLtsYQgF+XpMtovMQatOHWC89YmrU9hr5WtCacaMzcShEIRjfwGgugbBqJkn9\nObI4K0yP2+bRGcc794qncnsD6vW2D4hI1O6+5UAThlH5iLaVEKtTUF4QTuns6LxQ\nGedoj8X8iHWrZfQlmG2VnP1YBCHN6I47H7KXl75Gw2sEJSJCYARQ/juQTw5bReW2\nRtDC3ElNX51qcBZX+Qqn/h+xe3ZNNNcw/dFKtXrwRZmG0DA9iCHgz9OZCuWnlnEV\n/Ym2xMnVGkSoei9QICa35gaJ3qDHI8W3YiuN2/JbD55BHrrwsLuVTdLqRyxiVuxc\nutGrLC9IQr0jG5ikyaiB0HfrCDYn16trwI1WXVCaJqR658qScSK8XZfrE0mD/73m\ncL8bi3U1F0HHoqHxHGkf8uznCziIMsEJG6pKzvwsdX3KD6Ka/vnQBFkNlYaEj7qA\nvX+eHcYHjF49fypscruM/PV8+h31qjQ2GwBMEmlhTjlnsFFX57Ce4HKbRNgK5/jZ\nZZRkDddAyncBylIihmHwYXd7XTJTJJDbaMFBh4Z8t9nqBJ0L916q3eK6nvHBOfGy\nmnDPjoYT5H1gNlE/cG2ANplFXIPoswIDAQABAoICAFCtpg+ouQ9eIpP68qet5o2W\ne9LiW34Kn05+bJHc2/zqbclD8gGf3Cb3SGqJzLjwd2aCs3s9p++XB05TgiGmOglC\nHOGcwg1UO2lijUGXUelOvaGR8PS5YoS19fLfAtOpZq97BxcpzjQndnDyjV9cuboK\nLn5xHqNkZn4y79XadoPlCa4FPRykGgmmQF2y7aiKNJJKH9VelU+DTzXfjb7daMzL\nWbjF/FKwRxRl3zLLJ6PPfd+bxsW9ixYXVEeJNdSxfYL/+gZPNWYrKx5CRmS2Q1rz\nhKgSAReYk8cG7HhwPVEEEEtZcACMCA0TcxnE6K9yGGe0rHI92i81jeEZ80sm+zEo\nP+HE0Q0ZzaH4SnoYQODJtNdP23NEg7zZ9eKhsxR00L6OznkTwYsql7BxW99GmYzn\nfJ1f9IZgKPkhbqHiqKEWseMEeisymG0NzGmiOAIB6OSN2jjRY+wtEuwRxRc06YJh\nKJYrqbzGMsdJl7LEo36XfYusy7TP6GLhent6+R9qO48ePmPc1inj65skMjf9sNzn\n1L1b1lT7BsgSQ8O46gi3iDyZHIYoUdg/ZIoxzkXzwSTKDlz+D52aGXg4Ri1HngZ8\nWIQzKYAGshrVJ5ONuLRs78kAnQeYpy8qFe0wvHF/gSz0D2+paqITBOHL7f74iBsV\no03/M00DmHSSP6+RqZtxAoIBAQDVkADzrij5/CmnZpZ3qpEAgyIWmLnGluHR0/Gc\nXFCU93ERP0kKdQif3YMRKaD091LRwJUdcuvpjkNpcfz/fRmV2nRdy+WkJMNMHHsa\na9YQF1DEq6N3oUJs5DPtzwY1y/qvvOVISRje7fYagrTWtWlXzgOu9A87OIMM4+ho\nHnzxa4OpANYUfAA1R6bT/9ubI8xlPi82SG279w6EUAeENRrrAcPncHkrpQ7YdDgN\n+5/U77GWR0yX5vBKr5IFpWCoIVdYEKmWuuLdt85Aik19uhc66XlntsL0+RfuxQVX\nP2oqKuFr2yhQ06dG9Ow9KfiO/4Jk9BHPHZ6QBG5ixJ/AR7RjAoIBAQDO6Ea6KmgS\nne+pBRxXha2YDMz5FQ1wUg5kyoKojTcUvYYU4UkK2jNKwC8o9lQUmtLa41V/eIBD\nOk/VADpD9Da7cBnm4WJPKgMRwnmExky+wQeQhqJ2MtRXee1wsnD4eRWcVRDqs12U\nLXefRNL8OpZGUyf4X4W4hHAGg7f7W7L0CZz+B1s/9JpKQ9vIFY9XHXolti6L+fow\nkhU84bJ6jdPTnBiPUnjKuood2Lp/YrQw7jFnJF/qve+fnPTGYniJbj1/9qFP/JgW\nl+B9T6ulNbwtRy0Mx0BLJpknOVhEgOJ5RSYx9NIGfpSVrzwVpkEMy9CK2lPcPJkX\nwX2txSVc6WNxAoIBAG7gMa5R6FJJObMAjvQX6OpUKpmCt0jEQv92QwVD5E5C3T/w\nFFLKiiy9i3OYokksMqJVktVUOejrBFK1bH2UjHkBjtK3rkT3FTHpw3vnKp72C+ff\nmKHeZic0n0VC6114xnEA6CUMVk4/SzteStcCHmwIuF7XtSSw5VEG7j1IPuP0Rsmy\ndnLyIgWHarS8LF6ySkbx7v8GwXoJ/U5yYkSgcZY7N2NsQGyJaFi2lfekgMnDm/aC\nk+B1dKHB31TxFGqVzMwa8oEgC/LCn+FgLHUu7SqX1oEjILqgrG05etleQhccZiZi\nBN9Z04oCVLg4lyRewWr6UFiwbwckVc8PeEUStTMCggEAb5GqibpSMi+9yqs51Cv+\nJm5InMtwWq+0mT7l45N6LxHfWiT86QAuBlHJXFIvlojByEwrjzfgGeA4qgecY4Yt\neTcCkI/aHgvuacYvFpyDR5z4wkMHGatg+uaBVXKzHhjUwV7RZ21euYcm6NgI2P+S\nhstSU0jW519qtOiT7dNNlPAWGpjG6J6yD/e1bJfLmlMHyYwKX2plMYmkMBcX0aPm\npEWYrLfw8IhT77ItJoGH3paiRxbDLeZLbwsIpmz0yE6MlRLdey8ep5gv8gJi8Qwf\ns16c+TX8AkoG7bKrWQ0Skgfqh6eXFO9umaRLRvVGQGsqwaTm8WwvtTKd3XTgJ9Cl\n0QKCAQAt6X6IrTppS/KkgdpjCMgHhAlh8ZTk1Sx9havcR87/3+lBE0eiZa2LYU/Q\ncxpWmuBApf9/0gnZbLAcerG66AJVml4uuPkU7dCuzcFAI92TRe1fvOrIB70tQyUq\nxlhdRPFAIGYr9P3njHq5bSSo8ZZjicnDye5vaQ06RH8M3Lq21sbM8+b9M0hjzDKG\nXCCyRGXoiXme4bSJ7pDOFPDcymMcIJ02nl7m3tR49f+qrDqERVBLIiSK5R8VoUEx\nOiWQVFRxHvRJlw63ZtIpml3bUhfSQmiFNNtUT3OwFrb+fNJkXZyCH3ZBJ9X2cA8l\ngrZhpKVB22PtQI9T68oHoj4yU7BC\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/certs/gen.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport datetime\nimport os\n\nfrom cryptography import x509\nfrom cryptography.hazmat import backends\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography.x509 import oid\n\n\ndef _new_cert(issuer=None, is_issuer=False, serial_number=None, **subject):\n    backend = backends.default_backend()\n    private_key = rsa.generate_private_key(\n        public_exponent=65537, key_size=4096, backend=backend\n    )\n    public_key = private_key.public_key()\n    subject = x509.Name(\n        [\n            x509.NameAttribute(getattr(oid.NameOID, key.upper()), value)\n            for key, value in subject.items()\n        ]\n    )\n    builder = (\n        x509.CertificateBuilder()\n        .subject_name(subject)\n        .public_key(public_key)\n        .serial_number(serial_number or int.from_bytes(os.urandom(8), \"big\"))\n    )\n    if issuer:\n        issuer_cert, signing_key = issuer\n        builder = (\n            builder.issuer_name(issuer_cert.subject)\n            .not_valid_before(issuer_cert.not_valid_before)\n            .not_valid_after(issuer_cert.not_valid_after)\n        )\n        aki_ext = x509.AuthorityKeyIdentifier(\n            key_identifier=issuer_cert.extensions.get_extension_for_class(\n                x509.SubjectKeyIdentifier\n            ).value.digest,\n            authority_cert_issuer=[x509.DirectoryName(issuer_cert.subject)],\n            authority_cert_serial_number=issuer_cert.serial_number,\n        )\n    else:\n        signing_key = private_key\n        builder = (\n            builder.issuer_name(subject)\n            .not_valid_before(\n                datetime.datetime.today() - datetime.timedelta(days=1)\n            )\n            .not_valid_after(\n                datetime.datetime.today() + datetime.timedelta(weeks=1000)\n            )\n        )\n        aki_ext = x509.AuthorityKeyIdentifier.from_issuer_public_key(\n            public_key\n        )\n    if is_issuer:\n        builder = (\n            builder.add_extension(\n                x509.BasicConstraints(ca=True, path_length=None),\n                critical=False,\n            )\n            .add_extension(\n                x509.SubjectKeyIdentifier.from_public_key(public_key),\n                critical=False,\n            )\n            .add_extension(\n                aki_ext,\n                critical=False,\n            )\n        )\n    else:\n        builder = (\n            builder.add_extension(\n                x509.KeyUsage(\n                    digital_signature=True,\n                    content_commitment=False,\n                    key_encipherment=True,\n                    data_encipherment=False,\n                    key_agreement=False,\n                    key_cert_sign=False,\n                    crl_sign=False,\n                    encipher_only=False,\n                    decipher_only=False,\n                ),\n                critical=False,\n            )\n            .add_extension(\n                x509.BasicConstraints(ca=False, path_length=None),\n                critical=False,\n            )\n            .add_extension(\n                x509.ExtendedKeyUsage([oid.ExtendedKeyUsageOID.SERVER_AUTH]),\n                critical=False,\n            )\n            .add_extension(\n                x509.SubjectAlternativeName([x509.DNSName(\"localhost\")]),\n                critical=False,\n            )\n            .add_extension(\n                x509.SubjectKeyIdentifier.from_public_key(public_key),\n                critical=False,\n            )\n            .add_extension(\n                aki_ext,\n                critical=False,\n            )\n        )\n    certificate = builder.sign(\n        private_key=signing_key,\n        algorithm=hashes.SHA256(),\n        backend=backend,\n    )\n    return certificate, private_key\n\n\ndef _write_cert(path, cert_key_pair, password=None):\n    certificate, private_key = cert_key_pair\n    if password:\n        encryption = serialization.BestAvailableEncryption(password)\n    else:\n        encryption = serialization.NoEncryption()\n    with open(path + \".key.pem\", \"wb\") as f:\n        f.write(\n            private_key.private_bytes(\n                encoding=serialization.Encoding.PEM,\n                format=serialization.PrivateFormat.TraditionalOpenSSL,\n                encryption_algorithm=encryption,\n            )\n        )\n    with open(path + \".cert.pem\", \"wb\") as f:\n        f.write(\n            certificate.public_bytes(\n                encoding=serialization.Encoding.PEM,\n            )\n        )\n\n\ndef new_ca(path, **subject):\n    cert_key_pair = _new_cert(is_issuer=True, **subject)\n    _write_cert(path, cert_key_pair)\n    return cert_key_pair\n\n\ndef new_cert(\n    path, ca_cert_key_pair, password=None, is_issuer=False, **subject\n):\n    cert_key_pair = _new_cert(\n        issuer=ca_cert_key_pair, is_issuer=is_issuer, **subject\n    )\n    _write_cert(path, cert_key_pair, password)\n    return cert_key_pair\n\n\ndef new_crl(path, issuer, cert):\n    issuer_cert, signing_key = issuer\n    revoked_cert = (\n        x509.RevokedCertificateBuilder()\n        .serial_number(cert[0].serial_number)\n        .revocation_date(datetime.datetime.today())\n        .build()\n    )\n    builder = (\n        x509.CertificateRevocationListBuilder()\n        .issuer_name(issuer_cert.subject)\n        .last_update(datetime.datetime.today())\n        .next_update(datetime.datetime.today() + datetime.timedelta(days=1))\n        .add_revoked_certificate(revoked_cert)\n        .add_extension(x509.CRLNumber(1), critical=True)\n        .add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(\n            issuer_cert.public_key()), critical=True)\n    )\n    crl = builder.sign(private_key=signing_key, algorithm=hashes.SHA256())\n    with open(path + \".crl.pem\", \"wb\") as f:\n        f.write(crl.public_bytes(encoding=serialization.Encoding.PEM))\n\n\ndef main():\n    ca = new_ca(\n        \"ca\",\n        country_name=\"US\",\n        state_or_province_name=\"California\",\n        locality_name=\"San Francisco\",\n        organization_name=\"EdgeDB Inc.\",\n        organizational_unit_name=\"EdgeDB tests\",\n        common_name=\"EdgeDB test root ca\",\n        email_address=\"hello@edgedb.com\",\n    )\n    server = new_cert(\n        \"server\",\n        ca,\n        country_name=\"US\",\n        state_or_province_name=\"California\",\n        organization_name=\"EdgeDB Inc.\",\n        organizational_unit_name=\"EdgeDB tests\",\n        common_name=\"localhost\",\n        email_address=\"hello@edgedb.com\",\n        serial_number=4096,\n    )\n    new_crl('server', ca, server)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tests/certs/gen.sh",
    "content": "#!/bin/sh\nset -e -x\n\n# Save original directory\nORIG_DIR=$(pwd)\n\n# Ensure we return to original directory even on error\ntrap 'cd \"$ORIG_DIR\"' EXIT\n\ncd tests/certs\n\nrm index.txt*\nrm serial.txt\nrm crlnumber.txt\n\n# Root CA\nopenssl genrsa -out ca.key.pem 4096\n\n# Create empty database files required by ca.conf\ntouch index.txt\necho \"01\" > serial.txt\necho \"01\" > crlnumber.txt\n\nopenssl req -new -x509 -key ca.key.pem -out ca.cert.pem -days 7300 -config ca.conf -batch -subj \"/C=US/ST=California/L=San Francisco/O=EdgeDB Inc./OU=EdgeDB tests/CN=EdgeDB test root ca/emailAddress=hello@edgedb.com\"\n\n# Server cert\nopenssl genrsa -out server.key.pem 4096\nopenssl req -new -key server.key.pem -out server.csr.pem -subj \"/C=US/ST=California/L=San Francisco/O=EdgeDB Inc./OU=EdgeDB tests/CN=localhost/emailAddress=hello@edgedb.com\" -batch\nopenssl x509 -req -in server.csr.pem -CA ca.cert.pem -CAkey ca.key.pem -CAcreateserial -out server.cert.pem -days 7300 -extensions v3_req -extfile ca.conf\n\n# Client CA\nopenssl genrsa -out client_ca.key.pem 4096\nopenssl req -new -x509 -key client_ca.key.pem -out client_ca.cert.pem -days 7300 -subj \"/C=US/ST=California/L=San Francisco/O=EdgeDB Inc./OU=EdgeDB tests/CN=EdgeDB test client CA/emailAddress=hello@edgedb.com\" -batch\n\n# Client cert\nopenssl genrsa -out client.key.pem 4096\nopenssl req -new -key client.key.pem -out client.csr.pem -subj \"/C=US/ST=California/L=San Francisco/O=EdgeDB Inc./OU=EdgeDB tests/CN=ssl_user/emailAddress=hello@edgedb.com\" -batch\nopenssl x509 -req -in client.csr.pem -CA client_ca.cert.pem -CAkey client_ca.key.pem -CAcreateserial -out client.cert.pem -days 7300 -extensions v3_req -extfile ca.conf\n\n# Password protected client key\nopenssl rsa -aes256 -in client.key.pem -out client.key.protected.pem -passout pass:secret1234\n\n# Revoke server cert and generate CRL\nopenssl ca -config ca.conf -revoke server.cert.pem -keyfile ca.key.pem -cert ca.cert.pem -batch -md sha256\nopenssl ca -config ca.conf -gencrl -keyfile ca.key.pem -cert ca.cert.pem -out ca.crl.pem -batch -md sha256\n"
  },
  {
    "path": "tests/certs/server.cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIGQjCCBCqgAwIBAgIUZqAmIZS9Cgjj65btVW6Z6rl4/LMwDQYJKoZIhvcNAQEL\nBQAwgaYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH\nDA1TYW4gRnJhbmNpc2NvMRQwEgYDVQQKDAtFZGdlREIgSW5jLjEVMBMGA1UECwwM\nRWRnZURCIHRlc3RzMRwwGgYDVQQDDBNFZGdlREIgdGVzdCByb290IGNhMR8wHQYJ\nKoZIhvcNAQkBFhBoZWxsb0BlZGdlZGIuY29tMB4XDTI1MDEyOTIwNDAyOFoXDTQ1\nMDEyNDIwNDAyOFowgZwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh\nMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRQwEgYDVQQKDAtFZGdlREIgSW5jLjEV\nMBMGA1UECwwMRWRnZURCIHRlc3RzMRIwEAYDVQQDDAlsb2NhbGhvc3QxHzAdBgkq\nhkiG9w0BCQEWEGhlbGxvQGVkZ2VkYi5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4IC\nDwAwggIKAoICAQC0se06E5EF/GwL/y1CqNwPJK04qpDmt6kL9feZsegdh0KGR062\nRP7RPieJxYYCVSGSokNU5LJXo1t3XbS18xjnLVFfZXZNE1zbF1xRWQKJE7ZV5BpG\nO1ed1cAQGR6tsNsbUjQ3N+kLkph3dxLSho4Bqu9RQjTtrdGnu/lDlC3Tt2qmAl4M\nFFLmyUGPQiBOHWKDU+VG/i09cbZQVS7tFnyfL6F0SS3N+fOCa/gt7uEdNdVDRz81\nOoQ/xFAJFbj2kvCMgNfigyaUQc+CJVXHgWvw+hLdgiGqIbnVB6nUpVOXIS9xXSY8\ni+ZURWdoz82HJJxuPUPoVsmlGqKTZy6DXIlLz0kPXCNpIAMatlBfWO3VV+Zc/ylC\nEjfN5uoGellr8WVlu793WuSZMpyy50ULeGTV7/sNmQtdsYbl5wmv/mFG4EQm8De2\ngw9G09Ek7JdJbKLX3+lg2hC7AGecQNLbnPhtvgMTp7dY0jy9xh7+k88Qha9EGAiF\n21ZzhqxhWrNk0u3CKJtD1QxtVDyHq8FoeYpxLyDdW7h2YKykCXialZQlqHIk3wjQ\nGlUTgNUtQn8z/el8a2pPWeU0+KsKMDIizvFHMFbB5YZo/7vQ3k6rCq4tP5/UapTN\neud6X6nlztGZL+Df+hONULJmL7SuNIablyqACmBykFpqvH1kF0xtklENsQIDAQAB\no3AwbjAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DAUBgNVHREEDTALgglsb2NhbGhv\nc3QwHQYDVR0OBBYEFJfIRd55lBPNoJqNtVl+a7OOsIOvMB8GA1UdIwQYMBaAFBRx\nNC1cmmAeggflm7LlZqIIZSTKMA0GCSqGSIb3DQEBCwUAA4ICAQBYe/i6zkxTwJXH\nZhv+0kAgdUIBiTKBImzwPwnwShbKbWNLloKtZ6aF+Spy1uO0w/NnCZ5S8M/E7HZr\nELnLJR+tU//sfmDb/y0UGNyJptj2z78R8k7yZCqA2MpA+t8nf9TT9cYu2wmCORt+\njNZ0qYiRD4JichkVprkswF8gzZkhy8tM8TzO1HJu5TTZKQqcT4O1BCh9LkSgkhFD\nOYqVqwdq/bo8tCnJzICl6ZdRObKUkQ3MrEyGde68suvG5mlfxylgknF9BUDvQKPA\neQISbIy+KuUSgD5q8f12qL6O1gPbnydYaFdS3aY8nTkfZCKUFcqBroyO57vK9rr3\nWdUZoT3eU011JrAXG7w8MBaoghTHor8mx3tjsNmnnJztngvm2nE2f34q261SA/uT\njfp1avecmzRFudUtT4NaAHw2aa6KqTZ4y/nHfoFozJxMtYVyuFGsIeToAAtZIpdb\nzp8cDL7kW6MTIwFy7d8RAI+msDB5dUgT45iJUseAGklvnn/kibLzsbgfkeZXLxTg\nl7oPm8ybtrM0RU/s3Pkknq5zoYp4PTQukcWWKO6rKBnu4UHDaF7ATtj9/7mQ/7rK\nMuqt+iFhb3RMD7lj0ihZokvGZSe1AQxyS48llbLZoXOts20giTZGwVkvR125jF70\nbLtP4MYoBxHJxYGo4r0S6pYJ/nnTEA==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/certs/server.key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQC0se06E5EF/GwL\n/y1CqNwPJK04qpDmt6kL9feZsegdh0KGR062RP7RPieJxYYCVSGSokNU5LJXo1t3\nXbS18xjnLVFfZXZNE1zbF1xRWQKJE7ZV5BpGO1ed1cAQGR6tsNsbUjQ3N+kLkph3\ndxLSho4Bqu9RQjTtrdGnu/lDlC3Tt2qmAl4MFFLmyUGPQiBOHWKDU+VG/i09cbZQ\nVS7tFnyfL6F0SS3N+fOCa/gt7uEdNdVDRz81OoQ/xFAJFbj2kvCMgNfigyaUQc+C\nJVXHgWvw+hLdgiGqIbnVB6nUpVOXIS9xXSY8i+ZURWdoz82HJJxuPUPoVsmlGqKT\nZy6DXIlLz0kPXCNpIAMatlBfWO3VV+Zc/ylCEjfN5uoGellr8WVlu793WuSZMpyy\n50ULeGTV7/sNmQtdsYbl5wmv/mFG4EQm8De2gw9G09Ek7JdJbKLX3+lg2hC7AGec\nQNLbnPhtvgMTp7dY0jy9xh7+k88Qha9EGAiF21ZzhqxhWrNk0u3CKJtD1QxtVDyH\nq8FoeYpxLyDdW7h2YKykCXialZQlqHIk3wjQGlUTgNUtQn8z/el8a2pPWeU0+KsK\nMDIizvFHMFbB5YZo/7vQ3k6rCq4tP5/UapTNeud6X6nlztGZL+Df+hONULJmL7Su\nNIablyqACmBykFpqvH1kF0xtklENsQIDAQABAoICAA3YrJMMuMo6o5Tvwuoewm4u\no096Uow7eqq9+HFAnsbUfJaJlFCHTPd/ycvW5QP8vgvcf0hcFgZ9MB8fgR+IN1pP\nsLKctcoGN9FaIurg5T7X0dsXFaRYG8iufn89TYqFyOR5EiNBF4yZeTF+YGTdhrg9\n/wS9DA1CipRN5TX2fuSAY53BBK/sRsYEmg9+Q7d4rPnfTex4wcK5mfzh6iyk0nvo\nTHj6upXUF4Lg/y2V5o40d9kl9oP/re6s/m0Tyw6qB+DcZq6m2if2Ow6ACei11C2t\nHBD5TdcZqoHWin8PBC7KjjYjqzBskAPZJal0cw1uEsanzzJYpC8QsXCWMYxDZQnW\n/JnOaEI8fbTalIjdprNNUWTH3bYuUjhdzlGj0XG8fgJmVWcI0YxvZK/7XkYZoO3T\n0YsPlNfT92v1gUFy9LmdbzwVR+X3szQbKW00Mi4UfI9PGvVnNFWIWgQzZSn0unHL\nx6bqFoL+7lRndJYhw7XEfgmkvWAC72VVW44v/gfhbPreInayrm6sjCU3iW2ck5oA\n2yUQ5tkRIUAKRzrFFUeFlPifQYyl+XBqdZxmm4dBS8+wD5EidDWR+SSTrdQtHRQe\nZMnlLrQ2pselkJfWT0kN69Ht/+MlM8XFXC4pnRwzBPqzZe3SvaqSFXTV6iZVkr87\nbrXjQHqpDR2Iz/mLeuohAoIBAQDvJUBhO/jFaNL07zMz2PVPvp02kw4XkAf9Ioj1\ncvMopPt5dJl/Ib3YzFSspgHt1fyQhXOMcyge+eUppfdvyeQD0NL5pq/snDUjTs8/\nrkHkEeqi0uEE3mIBSZJMyPj/M/0PIkP6aTS1YOD1wUnrmiKPAO5XxhxoDkOKvjua\nn9G8Atn9p419V57JYgUOQKXMJ54dSgG57brBpCztiCjrv+ryCKJN3RDy4YBi/wIR\n+60UIfUfp0LDtqXGOzUoFW+gF7BtmoI/bMS+JRt2tUhANQ0agLm09FQolQY9cvWt\nm3jqSCT0G2F9EASk5z6dQQnM4Vm0Xa1nopCcGXxshj7Fw3SlAoIBAQDBbhc2VqKT\n5vss3hdWWJ/NMR+n1dF+DN+0oKzRTv6mThUClTivliNh6a8S2N7uNspzKwfLJYtF\nt7lttBypmX+AWFXfuvdQwckU2mbsqUu0eX0B1yleo/xjxDEH2X3KtR7oGd5fCSqq\ngPSud7luTnD1+i4NYn0CWZuBIqz1/2UJAiMJ9C35pkK3kYZnVXl51ra0MHoqAN7l\nxWxto7kdipbvRqHaxWRxDlEovnjdh2h9WcdT9L2Sv7yoddG6M1csvzj+BlpCdozX\nGc2Msn8WXqaOstbrxbrSH1ONrIMnd7C6/AulzBDgm6/0oG2vloGYEg4P+R6Shmj8\nAUm/fsxxlssdAoIBAAIGc7wfEOVDFP5EBE/9F168g4JRzn2+l4K6RySk+5AtjX+i\n0CX2eDAc0t6/bSbBeCkVKDxZQU652Qn4QNcug4LQwuigU78SN5T79t9YL3CqAi2s\n0YEEDRprsBR8YzduDkeRh7fYKj0a++y43VJkdMR3Qo11vQnYjRPgtoqBrfoEoCn4\nwEb+dfbIoLhVLdJDx6AB32/epoU7SbIdRBNzBZ9VGWLFa35TEI5GEJNsaTPRccz5\n0qPrqQ27tKCJRe+6I6FZ/J5i4mulzsy7jkgU6u59hpUUuJ27XVkp3xDAT07Um5OB\no9q2RwPKfYpBYb7dbbAMVwqYotbflGEq5d8w340CggEADIUlt3ywFUa0J1lQxWQD\nI/L0C5cJclE/AaAMz5d/YCBZt8sU2jirnaDUljG3bE/blszIOlv5wc98jx9DY67t\n087j8BFYBMAmVdU1KEhlAA+FCeN1aAzRP1vpmIp5W++RSpCyFhCv1E14iPpy5DLr\nmOBSrscbNFW8fQVTkLCxR3396p0FhM1AMEWZH4Mm074UIWGp5qIby1+V/xrD1qer\n0V0PCOwR7kdw9WQuypgDKWnwPvzucFs8yOKnapf8IodWFwsuOHMX9qxS47KCC16h\nBUeKJcSsrQEeFWN/McTLia7ayiaFSSSKpRjlQKJLTR6ODnafhfhxPy6OKXj5nriV\n+QKCAQBRgUoJr+ByP1fqmbz0M/0zvo5567bFPIhX1SQ+VTCnMkSFKDonrh2k0MSX\n4pY8jQCcRlhQqXNrDoGrJ1brkmLSIVHbdzquI+2YemffgPiOs8n7cEkHvpoD6eMm\nqlcDH/QvbVWMWeJ3rPMmbzh+9mrcOn86qPLLNzy6gU8J03rBi7tcO1aRvqx9vq44\n0WNNnTHMI53bNDZOt4J326N0sVVPpJFzx7GXTbhHI6yMVQ87touZ+GH1KQZu5Q8V\nXm54TyqZ76Pl7okuTq92yhdBuHCq0W+aUTW2GDMykp887m3PmWfub1/Dmj1BY0eY\nOVaGdduWlABHp+A2Q9+iJdYrF5iN\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/common/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport unittest\n\n\ndef suite():\n    test_loader = unittest.TestLoader()\n    test_suite = test_loader.discover('.', pattern='test_*.py')\n    return test_suite\n"
  },
  {
    "path": "tests/common/test_ast.py",
    "content": "# mypy: ignore-errors\n\n#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2010-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport copy\nimport typing\nimport unittest\nimport unittest.mock\n\nfrom edb.common import ast\nfrom edb.common.ast import visitor\n\n\nclass tast:\n\n    class Base(ast.AST):\n        pass\n\n    class BinOp(Base):\n        op: typing.Any = None\n        left: typing.Any = None\n        right: typing.Any = None\n\n    class UnaryOp(Base):\n        op: typing.Any = None\n        operand: typing.Any = None\n\n    class FunctionCall(Base):\n        name: typing.Any = None\n        args: list[int]\n\n    class Constant(Base):\n        value: typing.Any = None\n\n\nclass ASTBaseTests(unittest.TestCase):\n\n    def test_common_ast_copy(self):\n        lconst = tast.Constant(value='foo')\n        tree1 = tast.BinOp(left=lconst)\n        ctree11 = copy.copy(tree1)\n\n        assert ctree11 is not tree1\n        assert ctree11.left is lconst\n\n        ctree12 = copy.deepcopy(tree1)\n        assert ctree12 is not tree1\n        assert ctree12.left is not lconst\n        assert ctree12.left.value == lconst.value\n\n        class Dict(tast.Base):\n            node: dict\n\n        tree2 = tast.BinOp(\n            left=tast.FunctionCall(args=[Dict(node={'lconst': lconst})]))\n\n        ctree21 = copy.copy(tree2)\n        assert ctree21 is not tree2\n        assert ctree21.left.args[0].node['lconst'] is lconst\n\n        ctree22 = copy.deepcopy(tree2)\n        assert ctree22 is not tree2\n        assert ctree22.left.args[0].node['lconst'] is not lconst\n        assert ctree22.left.args[0].node['lconst'].value == lconst.value\n\n    @unittest.mock.patch(\n        'edb.common.ast.base._check_type',\n        ast.base._check_type_real,\n    )\n    @unittest.mock.patch(\n        'edb.common.ast.base.AST.__setattr__',\n        ast.base.AST._checked_setattr,\n    )\n    def test_common_ast_typing(self):\n        class Base(ast.AST):\n            pass\n\n        class Node(Base):\n            field_list: list = ast.field(factory=list)\n            field_typing_list: list[Base] = ast.field(factory=list)\n            field_typing_tuple: tuple[Base, ...] = ()\n            field_typing_union: str | bytes\n            field_typing_union_list: list[\n                str | bytes] = ast.field(factory=list)\n            field_typing_str: str\n            field_typing_optional_str: typing.Optional[str]\n            field_typing_mapping: dict[\n                int, str] = ast.field(factory=dict)\n            field_typing_mapping_opt_key: \\\n                dict[\n                    typing.Optional[int], str] = ast.field(factory=dict)\n\n        self.assertEqual(Node().field_list, [])\n        self.assertEqual(Node().field_typing_list, [])\n        self.assertEqual(Node().field_typing_tuple, ())\n\n        Node().field_list = []\n        Node().field_list = [12, 2]\n        with self.assertRaises(TypeError):\n            Node().field_list = 'abc'\n\n        Node().field_typing_list = []\n        Node().field_typing_list = [Base()]\n        with self.assertRaises(TypeError):\n            Node().field_typing_list = 'abc'\n        with self.assertRaises(TypeError):\n            Node().field_typing_list = ['abc']\n\n        Node().field_typing_tuple = ()\n        Node().field_typing_tuple = (Base(),)\n        with self.assertRaises(TypeError):\n            Node().field_typing_tuple = 'abc'\n        with self.assertRaises(TypeError):\n            Node().field_typing_tuple = ('abc',)\n\n        Node().field_typing_union = 'abc'\n        Node().field_typing_union = b'abc'\n        with self.assertRaises(TypeError):\n            Node().field_typing_union = 1\n\n        self.assertEqual(Node().field_typing_union_list, [])\n        Node().field_typing_union_list = ['abc', b'abc']\n        Node().field_typing_union_list = [b'abc', 'abc']\n        with self.assertRaises(TypeError):\n            Node().field_typing_union_list = [1]\n        with self.assertRaises(TypeError):\n            Node().field_typing_union_list = 'abc'\n\n        Node().field_typing_str = 'aaa'\n        # All fields in AST are optional\n        Node().field_typing_str = None\n\n        Node().field_typing_optional_str = None\n        Node().field_typing_optional_str = 'aaa'\n\n        Node().field_typing_mapping = {1: 'a'}\n        with self.assertRaises(TypeError):\n            Node().field_typing_mapping = {'a': 1}\n        with self.assertRaisesRegex(RuntimeError, 'empty key'):\n            Node().field_typing_mapping = {None: 1}\n        with self.assertRaisesRegex(TypeError, 'expected str but got int'):\n            Node().field_typing_mapping_opt_key = {None: 1}\n        Node().field_typing_mapping_opt_key = {None: '1'}\n\n        class Node(ast.AST):\n            field1: str\n            field2: object\n            field3: object = 123\n\n        Node().field1 = '123'\n        Node().field2 = 'aaa'\n        Node().field3 = 'aaa'\n        self.assertEqual(Node().field1, None)\n        self.assertEqual(Node().field3, 123)\n\n\nclass ASTFindChildrenTests(unittest.TestCase):\n\n    def test_common_ast_find_children(self):\n        node = tast.UnaryOp(\n            op='NamedTuple',\n            operand=[\n                ('foo', tast.Constant(value=2)),\n                ('bar', [\n                    tast.UnaryOp(op='-', operand=tast.Constant(value=3))]),\n            ],\n        )\n        children = visitor.find_children(node, tast.Constant)\n        assert {x.value for x in children} == {2, 3}\n"
  },
  {
    "path": "tests/common/test_asyncutil.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport asyncio\nimport unittest\n\nfrom edb.common import asyncutil\nfrom edb.testbase.asyncutils import with_fake_event_loop\n\n\nclass TestDebounce(unittest.TestCase):\n\n    @with_fake_event_loop\n    async def test_debounce_01(self):\n        loop = asyncio.get_running_loop()\n        outs = []\n        ins = asyncio.Queue()\n\n        async def output(vs):\n            assert loop.time() == int(loop.time())\n            outs.append((int(loop.time()), vs))\n\n        async def sleep_until(t):\n            await asyncio.sleep(t - loop.time())\n\n        task = asyncio.create_task(asyncutil.debounce(\n            ins.get,\n            output,\n            # Use integers for delays to avoid any possibility of\n            # floating point nonsense\n            max_wait=500,\n            delay_amt=200,\n            max_batch_size=4,\n        ))\n\n        ins.put_nowait(1)\n        await sleep_until(10)\n        ins.put_nowait(2)\n        ins.put_nowait(3)\n        await sleep_until(300)\n        ins.put_nowait(4)\n        ins.put_nowait(5)\n        ins.put_nowait(6)\n        await sleep_until(1000)\n\n        # Time 1000 now\n        ins.put_nowait(7)\n        await sleep_until(1150)\n        ins.put_nowait(8)\n        ins.put_nowait(9)\n        ins.put_nowait(10)\n        await sleep_until(1250)\n        ins.put_nowait(11)\n\n        ins.put_nowait(12)\n        await asyncio.sleep(190)\n        ins.put_nowait(13)\n        await asyncio.sleep(190)\n        ins.put_nowait(14)\n        await asyncio.sleep(190)\n        self.assertEqual(loop.time(), 1820)\n        ins.put_nowait(15)\n\n        # Make sure everything clears out and stop it\n        await asyncio.sleep(10000)\n        task.cancel()\n\n        self.assertEqual(\n            outs,\n            [\n                # First one right away\n                (0, [1]),\n                # Next two added at 10 + 200 tick\n                (210, [2, 3]),\n                # Next three added at 300 + 200 tick\n                (500, [4, 5, 6]),\n                # First at 1000\n                (1000, [7]),\n                # Next group at 1250 when the batch fills up\n                (1250, [8, 9, 10, 11]),\n                # And more at 1750 when time expires on that batch\n                (1750, [12, 13, 14]),\n                # And the next one (queued at 1820) at 200 after it was queued,\n                # since there had been a recent signal when it was queued.\n                (2020, [15]),\n            ],\n        )\n\n\nclass TestExclusiveTask(unittest.TestCase):\n    async def _test(self, task: asyncutil.ExclusiveTask, get_counter):\n        # double-schedule is effective only once\n        task.schedule()\n        self.assertTrue(task.scheduled)\n        task.schedule()\n        self.assertTrue(task.scheduled)\n        self.assertEqual(get_counter(), 0)\n\n        # an exclusive task is running, schedule another one with a double shot\n        await asyncio.sleep(4)\n        self.assertFalse(task.scheduled)\n        await asyncio.sleep(1)\n        task.schedule()\n        self.assertTrue(task.scheduled)\n        task.schedule()\n        self.assertTrue(task.scheduled)\n        self.assertEqual(get_counter(), 1)\n\n        # first task done, second follows immediately\n        await asyncio.sleep(5)\n        self.assertFalse(task.scheduled)\n        self.assertEqual(get_counter(), 3)\n\n        # all done\n        await asyncio.sleep(9)\n        self.assertFalse(task.scheduled)\n        self.assertEqual(get_counter(), 4)\n\n        # works repeatedly\n        await asyncio.sleep(1)\n        task.schedule()\n        self.assertTrue(task.scheduled)\n        await asyncio.sleep(3)\n        self.assertFalse(task.scheduled)\n        await asyncio.sleep(1)\n        task.schedule()\n        self.assertTrue(task.scheduled)\n        self.assertEqual(get_counter(), 5)\n\n        # now stop the scheduled task and wait for the running one to finish\n        await asyncio.sleep(1)\n        await task.stop()\n        self.assertFalse(task.scheduled)\n        self.assertEqual(get_counter(), 6)\n\n        # no further schedule allowed\n        task.schedule()\n        self.assertFalse(task.scheduled)\n        await asyncio.sleep(10)\n        self.assertEqual(get_counter(), 6)\n\n    @with_fake_event_loop\n    async def test_exclusive_task_01(self):\n        counter = 0\n\n        @asyncutil.exclusive_task\n        async def task():\n            nonlocal counter\n            counter += 1\n            await asyncio.sleep(8)\n            counter += 1\n\n        await self._test(task, lambda: counter)\n\n    @with_fake_event_loop\n    async def test_exclusive_task_02(self):\n        counter = 0\n\n        @asyncutil.exclusive_task()\n        async def task():\n            nonlocal counter\n            counter += 1\n            await asyncio.sleep(8)\n            counter += 1\n\n        await self._test(task, lambda: counter)\n\n    @with_fake_event_loop\n    async def test_exclusive_task_03(self):\n        class MyClass:\n            def __init__(self):\n                self.counter = 0\n\n            @asyncutil.exclusive_task\n            async def task(self):\n                self.counter += 1\n                await asyncio.sleep(8)\n                self.counter += 1\n\n        obj = MyClass()\n        await self._test(obj.task, lambda: obj.counter)\n\n    @with_fake_event_loop\n    async def test_exclusive_task_04(self):\n        class MyClass:\n            def __init__(self):\n                self.counter = 0\n\n            @asyncutil.exclusive_task(slot=\"another\")\n            async def task(self):\n                self.counter += 1\n                await asyncio.sleep(8)\n                self.counter += 1\n\n        obj = MyClass()\n        await self._test(obj.task, lambda: obj.counter)\n\n    @with_fake_event_loop\n    async def test_exclusive_task_05(self):\n        class MyClass:\n            __slots__ = (\"counter\", \"another\",)\n\n            def __init__(self):\n                self.counter = 0\n\n            @asyncutil.exclusive_task(slot=\"another\")\n            async def task(self):\n                self.counter += 1\n                await asyncio.sleep(8)\n                self.counter += 1\n\n        obj = MyClass()\n        await self._test(obj.task, lambda: obj.counter)\n\n    @with_fake_event_loop\n    async def test_exclusive_task_06(self):\n        class MyClass:\n            def __init__(self, factor: int):\n                self.counter = 0\n                self.factor = factor\n\n            @asyncutil.exclusive_task\n            async def task(self):\n                self.counter += self.factor\n                await asyncio.sleep(8)\n                self.counter += self.factor\n\n        obj1 = MyClass(1)\n        obj2 = MyClass(2)\n        async with asyncio.TaskGroup() as g:\n            g.create_task(\n                self._test(obj1.task, lambda: obj1.counter // obj1.factor)\n            )\n            await asyncio.sleep(3)\n            g.create_task(\n                self._test(obj2.task, lambda: obj2.counter // obj2.factor)\n            )\n\n    def test_exclusive_task_07(self):\n        with self.assertRaises(TypeError):\n            class MyClass:\n                __slots__ = ()\n\n                @asyncutil.exclusive_task\n                async def task(self):\n                    pass\n\n    def test_exclusive_task_08(self):\n        with self.assertRaises(TypeError):\n            class MyClass:\n                __slots__ = ()\n\n                @asyncutil.exclusive_task(slot=\"missing\")\n                async def task(self):\n                    pass\n\n    def test_exclusive_task_09(self):\n        with self.assertRaises(TypeError):\n            @asyncutil.exclusive_task\n            async def task(*args, **kwargs):\n                pass\n\n    def test_exclusive_task_10(self):\n        with self.assertRaises(TypeError):\n            @asyncutil.exclusive_task\n            async def task(*, p):\n                pass\n\n    def test_exclusive_task_11(self):\n        with self.assertRaises(TypeError):\n            class MyClass:\n                @asyncutil.exclusive_task\n                async def task(self, p):\n                    pass\n\n    def test_exclusive_task_12(self):\n        with self.assertRaises(TypeError):\n            class MyClass:\n                @asyncutil.exclusive_task\n                @classmethod\n                async def task(cls):\n                    pass\n\n    @with_fake_event_loop\n    async def test_exclusive_task_13(self):\n        counter = 0\n\n        class MyClass:\n            @asyncutil.exclusive_task\n            @staticmethod\n            async def task():\n                nonlocal counter\n                counter += 1\n                await asyncio.sleep(8)\n                counter += 1\n\n        obj1 = MyClass()\n        obj2 = MyClass()\n\n        async with asyncio.TaskGroup() as g:\n            g.create_task(\n                self._test(obj1.task, lambda: counter)\n            )\n            g.create_task(\n                self._test(obj2.task, lambda: counter)\n            )\n"
  },
  {
    "path": "tests/common/test_checked.py",
    "content": "# mypy: ignore-errors\n\n#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import TypeVar\n\nimport pickle\nimport sys\nimport unittest\n\nfrom edb.common.checked import CheckedDict\nfrom edb.common.checked import CheckedList\nfrom edb.common.checked import CheckedSet\nfrom edb.common.checked import FrozenCheckedList\nfrom edb.common.checked import FrozenCheckedSet\nfrom edb.common.checked import enable_typechecks, disable_typechecks\nfrom edb.common import debug\n\n\nclass EnsureTypeChecking:\n    def setUp(self):\n        if not debug.flags.typecheck:\n            enable_typechecks()\n\n    def tearDown(self):\n        if not debug.flags.typecheck:\n            disable_typechecks()\n\n\nclass CheckedDictTests(EnsureTypeChecking, unittest.TestCase):\n    def test_common_checked_checkeddict_basics(self) -> None:\n        StrDict = CheckedDict[str, int]\n        assert StrDict({\"1\": 2})[\"1\"] == 2\n        assert StrDict(foo=1, initdict=2)[\"initdict\"] == 2\n\n        sd = StrDict(**{\"1\": 2})\n        assert sd[\"1\"] == 2\n\n        assert dict(sd) == {\"1\": 2}\n\n        sd[\"foo\"] = 42\n\n        with self.assertRaises(KeyError):\n            sd[0] = 0\n        with self.assertRaises(ValueError):\n            sd[\"foo\"] = \"bar\"\n        assert sd[\"foo\"] == 42\n\n        with self.assertRaises(ValueError):\n            sd.update({\"spam\": \"ham\"})\n\n        sd.update({\"spam\": 12})\n        assert sd[\"spam\"] == 12\n\n        with self.assertRaises(ValueError):\n            StrDict(**{\"foo\": \"bar\"})\n\n        with self.assertRaisesRegex(TypeError, \"expects 2 type parameters\"):\n            # no value type given\n            CheckedDict[int]\n\n        class Foo:\n            def __repr__(self):\n                return self.__class__.__name__\n\n        class Bar(Foo):\n            pass\n\n        FooDict = CheckedDict[str, Foo]\n\n        td = FooDict(bar=Bar(), foo=Foo())\n        module_path = self.__module__\n        expected = (\n            f\"edb.common.checked.CheckedDict[str, {module_path}.\"\n            \"CheckedDictTests.test_common_checked_checkeddict_basics.\"\n            \"<locals>.Foo]({'bar': Bar, 'foo': Foo})\"\n        )\n        assert repr(td) == expected\n        expected = \"{'bar': Bar, 'foo': Foo}\"\n        assert str(td) == expected\n\n        with self.assertRaisesRegex(ValueError, \"expected at most 1\"):\n            FooDict(Foo(), Bar())\n\n        td = FooDict.fromkeys(\"abc\", value=Bar())\n        assert len(td) == 3\n        del td[\"b\"]\n        assert \"b\" not in td\n        assert len(td) == 2\n        assert str(td) == \"{'a': Bar, 'c': Bar}\"\n\n    def test_common_checked_checkeddict_pickling(self) -> None:\n        StrDict = CheckedDict[str, int]\n        sd = StrDict()\n        sd[\"foo\"] = 123\n        sd[\"bar\"] = 456\n\n        assert sd.keytype is str and sd.valuetype is int\n        assert type(sd) is StrDict\n        assert sd[\"foo\"] == 123\n        assert sd[\"bar\"] == 456\n\n        sd2 = pickle.loads(pickle.dumps(sd))\n\n        assert sd2.keytype is str and sd2.valuetype is int\n        assert type(sd2) is StrDict\n        assert sd2[\"foo\"] == 123\n        assert sd2[\"bar\"] == 456\n        assert sd is not sd2\n        assert sd == sd2\n\n\nclass CheckedListTestBase(EnsureTypeChecking):\n    BaseList = FrozenCheckedList\n\n    def test_common_checked_shared_list_basics(self) -> None:\n        IntList = self.BaseList[int]\n        StrList = self.BaseList[str]\n\n        with self.assertRaises(ValueError):\n            IntList((\"1\", \"2\"))\n\n        with self.assertRaises(ValueError):\n            StrList([1])\n\n        with self.assertRaises(ValueError):\n            StrList([None])\n\n        sl = StrList([\"Some\", \"strings\", \"here\"])\n        assert sl == [\"Some\", \"strings\", \"here\"]\n        assert list(sl) == [\"Some\", \"strings\", \"here\"]\n        assert sl > [\"Some\", \"strings\"]\n        assert sl < [\"Some\", \"strings\", \"here\", \"too\"]\n        assert sl >= [\"Some\", \"strings\"]\n        assert sl <= [\"Some\", \"strings\", \"here\", \"too\"]\n        assert sl >= [\"Some\", \"strings\", \"here\"]\n        assert sl <= StrList([\"Some\", \"strings\", \"here\"])\n        assert sl + [\"too\"] == [\"Some\", \"strings\", \"here\", \"too\"]\n        assert [\"Hey\"] + sl == [\"Hey\", \"Some\", \"strings\", \"here\"]\n        assert type(sl + [\"too\"]) is StrList\n        assert type([\"Hey\"] + sl) is StrList\n        assert sl[0] == \"Some\"\n        assert type(sl[:2]) is StrList\n        assert sl[:2] == StrList([\"Some\", \"strings\"])\n        assert len(sl) == 3\n        assert sl[1:2] * 3 == [\"strings\", \"strings\", \"strings\"]\n        assert 3 * sl[1:2] == [\"strings\", \"strings\", \"strings\"]\n        assert type(3 * sl[1:2]) is StrList\n\n        class Foo:\n            def __repr__(self):\n                return self.__class__.__name__\n\n        class Bar(Foo):\n            pass\n\n        FooList = self.BaseList[Foo]\n\n        tl = FooList([Bar(), Foo()])\n        cls_name = self.BaseList.__name__\n        module_path = self.__module__\n        expected = (\n            f\"edb.common.checked.{cls_name}[{module_path}.\"\n            \"CheckedListTestBase.test_common_checked_shared_list_basics.\"\n            \"<locals>.Foo]([Bar, Foo])\"\n        )\n        assert repr(tl) == expected, repr(tl)\n        expected = \"[Bar, Foo]\"\n        assert str(tl) == expected\n\n    def test_common_checked_shared_list_pickling(self):\n        StrList = self.BaseList[str]\n        sd = StrList([\"123\", \"456\"])\n\n        assert sd.type is str\n        assert type(sd) is StrList\n        assert sd[0] == \"123\"\n        assert sd[1] == \"456\"\n\n        sd = pickle.loads(pickle.dumps(sd))\n\n        assert sd.type is str\n        assert type(sd) is StrList\n        assert sd[0] == \"123\"\n        assert sd[1] == \"456\"\n\n    def test_common_checked_shared_list_invalid_parameters(self):\n        with self.assertRaisesRegex(TypeError, \"must be parametrized\"):\n            self.BaseList()\n\n        with self.assertRaisesRegex(TypeError, \"expects 1 type parameter\"):\n            self.BaseList[int, int]()\n\n        with self.assertRaisesRegex(TypeError, \"already parametrized\"):\n            self.BaseList[int][int]\n\n    @unittest.skipUnless(sys.version_info >= (3, 7, 3), \"BPO-35992\")\n    def test_common_checked_shared_list_non_type_parameter(self):\n        with self.assertRaisesRegex(TypeError, \"expects types\"):\n            self.BaseList[1]()\n\n\nclass FrozenCheckedListTests(CheckedListTestBase, unittest.TestCase):\n    BaseList = FrozenCheckedList\n\n    def test_common_checked_frozenlist_basics(self) -> None:\n        StrList = self.BaseList[str]\n        sl = StrList([\"1\", \"2\"])\n        with self.assertRaises(AttributeError):\n            sl.append(\"3\")\n\n    def test_common_checked_frozenlist_hashable(self) -> None:\n        StrList = self.BaseList[str]\n        s1 = StrList([\"1\", \"2\"])\n        s2 = StrList([\"1\", \"2\"])\n        self.assertEqual(hash(s1), hash(tuple(s1)))\n        self.assertEqual(hash(s1), hash(s2))\n\n\nclass CheckedListTests(CheckedListTestBase, unittest.TestCase):\n    BaseList = CheckedList\n\n    def test_common_checked_checkedlist_basics(self) -> None:\n        StrList = self.BaseList[str]\n        tl = StrList()\n        tl.append(\"1\")\n        tl.extend((\"2\", \"3\"))\n        tl += [\"4\"]\n        tl += (\"5\",)\n        tl = tl + (\"6\",)\n        tl = (\"0\",) + tl\n        tl.insert(0, \"-1\")\n        assert tl == [\"-1\", \"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\"]\n        del tl[1]\n        assert tl == [\"-1\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\"]\n        del tl[1:3]\n        assert tl == [\"-1\", \"3\", \"4\", \"5\", \"6\"]\n        tl[2] = \"X\"\n        assert tl == [\"-1\", \"3\", \"X\", \"5\", \"6\"]\n        tl[1:4] = (\"A\", \"B\", \"C\")\n        assert tl == [\"-1\", \"A\", \"B\", \"C\", \"6\"]\n        tl *= 2\n        assert tl == [\"-1\", \"A\", \"B\", \"C\", \"6\", \"-1\", \"A\", \"B\", \"C\", \"6\"]\n        tl.sort()\n        assert tl == [\"-1\", \"-1\", \"6\", \"6\", \"A\", \"A\", \"B\", \"B\", \"C\", \"C\"]\n\n        with self.assertRaises(ValueError):\n            tl.append(42)\n\n        with self.assertRaises(ValueError):\n            tl.extend((42,))\n\n        with self.assertRaises(ValueError):\n            tl.insert(0, 42)\n\n        with self.assertRaises(ValueError):\n            tl += (42,)\n\n        with self.assertRaises(ValueError):\n            tl = tl + (42,)\n\n        with self.assertRaises(ValueError):\n            tl = (42,) + tl\n\n\nclass CheckedSetTestBase(EnsureTypeChecking):\n    BaseSet = FrozenCheckedSet\n\n    def test_common_checked_shared_set_basics(self) -> None:\n        StrSet = self.BaseSet[str]\n        s1 = StrSet(\"sphinx of black quartz judge my vow\")\n        assert s1 == set(\"abcdefghijklmnopqrstuvwxyz \")\n        s2 = StrSet(\"hunter2\")\n        assert (s1 & s2) == StrSet(\"hunter\")\n        assert type(s1 & s2) is StrSet\n        assert (s1 | s2) == set(\"abcdefghijklmnopqrstuvwxyz 2\")\n        assert type(s1 | s2) is StrSet\n        assert (s1 - s2) == set(\"abcdfgijklmopqsvwxyz \")\n        assert type(s1 - s2) is StrSet\n        assert (set(\"hunter2\") - s1) == StrSet(\"2\")\n        assert type(set(\"hunter2\") - s1) is StrSet\n\n        class Foo:\n            def __repr__(self):\n                return self.__class__.__name__\n\n        class Bar(Foo):\n            def __eq__(self, other):\n                return isinstance(other, Bar)\n\n            def __hash__(self):\n                return 1\n\n        FooSet = self.BaseSet[Foo]\n\n        tl = FooSet([Bar(), Foo(), Bar()])\n        tl2 = FooSet(tl | {Foo()})\n        assert len(tl) == 2\n        assert len(tl ^ tl2) == 1\n        assert tl.issuperset({Bar()})\n        assert tl.issubset(tl2)\n        # We have to do some gymnastics due to sets being unordered.\n        expected = {\"{Bar, Foo}\", \"{Foo, Bar}\"}\n        assert str(tl) in expected\n        cls_name = self.BaseSet.__name__\n        module_path = self.__module__\n        expected_template = (\n            f\"edb.common.checked.{cls_name}[{module_path}.\"\n            \"CheckedSetTestBase.test_common_checked_shared_set_basics.\"\n            \"<locals>.Foo]({})\"\n        )\n        assert repr(tl) in {expected_template.format(e) for e in expected}\n\n    def test_common_checkedset_pickling(self):\n        StrSet = self.BaseSet[str]\n        sd = StrSet({\"123\", \"456\"})\n\n        self.assertIs(sd.type, str)\n        self.assertIs(type(sd), StrSet)\n        self.assertIn(\"123\", sd)\n        self.assertIn(\"456\", sd)\n\n        sd = pickle.loads(pickle.dumps(sd))\n\n        self.assertIs(sd.type, str)\n        self.assertIs(type(sd), StrSet)\n        self.assertIn(\"123\", sd)\n        self.assertIn(\"456\", sd)\n\n\nclass FrozenCheckedSetTests(CheckedSetTestBase, unittest.TestCase):\n    BaseSet = FrozenCheckedSet\n\n    def test_common_checked_frozenset_hashable(self) -> None:\n        StrSet = self.BaseSet[str]\n        s1 = StrSet([\"1\", \"2\"])\n        s2 = StrSet([\"2\", \"1\"])\n        self.assertEqual(hash(s1), hash(frozenset((\"1\", \"2\"))))\n        self.assertEqual(hash(s1), hash(s2))\n\n\nclass CheckedSetTests(CheckedSetTestBase, unittest.TestCase):\n    BaseSet = CheckedSet\n\n    def test_common_checked_checkedset_basics(self) -> None:\n        StrSet = self.BaseSet[str]\n        tl = StrSet()\n        tl.add(\"1\")\n        tl.update((\"2\", \"3\"))\n        tl |= [\"4\"]\n        tl |= (\"5\",)\n        tl = tl | StrSet([\"6\"])\n        tl = {\"0\"} | tl\n        assert set(tl) == {\"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\"}\n\n        tl = \"67896789\" - tl  # sic, TypedSet used to coerce arguments, too.\n        assert tl == {\"7\", \"8\", \"9\"}\n        assert set(tl - {\"8\", \"9\"}) == {\"7\"}\n\n        assert set(tl ^ {\"8\", \"9\", \"10\"}) == {\"7\", \"10\"}\n        assert set({\"8\", \"9\", \"10\"} ^ tl) == {\"7\", \"10\"}\n        tl -= {\"8\"}\n        assert tl == StrSet(\"79\")\n\n        with self.assertRaises(ValueError):\n            tl.add(42)\n\n        with self.assertRaises(ValueError):\n            tl.update((42,))\n\n        with self.assertRaises(ValueError):\n            tl |= {42}\n\n        with self.assertRaises(ValueError):\n            tl = tl | {42}\n\n        with self.assertRaises(ValueError):\n            tl = {42} | tl\n\n        with self.assertRaises(ValueError):\n            tl = {42} ^ tl\n\n        with self.assertRaises(ValueError):\n            tl &= {42}\n\n        with self.assertRaises(ValueError):\n            tl ^= {42}\n\n\nT = TypeVar(\"T\")\n\n\nclass ConcreteFrozenCheckedSetSubclass1(FrozenCheckedSet[int]):\n    def sum(self) -> int:\n        return sum(elem for elem in self)\n\n\nclass GenericFrozenCheckedSetSubclass(FrozenCheckedSet[T]):\n    def sum(self) -> T:\n        return sum(elem for elem in self)\n\n\nConcreteFrozenCheckedSetSubclass2 = GenericFrozenCheckedSetSubclass[int]\n\n\nclass CheckedSubclassingTestBase(EnsureTypeChecking):\n    BaseSet = GenericFrozenCheckedSetSubclass\n\n    def test_common_checked_checkedset_subclass_pickling(self):\n        cfcss = self.BaseSet([0, 2, 4, 6, 8])\n        self.assertIs(cfcss.type, int)\n        self.assertIs(type(cfcss), self.BaseSet)\n        self.assertEqual(cfcss, {0, 2, 4, 6, 8})\n        self.assertEqual(cfcss.sum(), 20)\n\n        pickled = pickle.dumps(cfcss)\n        cfcss2 = pickle.loads(pickled)\n\n        self.assertTrue(cfcss2.type, int)\n        self.assertIs(type(cfcss2), self.BaseSet)\n        self.assertIsNot(cfcss, cfcss2)\n        self.assertEqual(cfcss, cfcss2)\n        self.assertEqual(cfcss.sum(), 20)\n\n\nclass CheckedSubclass1Tests(CheckedSubclassingTestBase, unittest.TestCase):\n    BaseSet = ConcreteFrozenCheckedSetSubclass1\n\n\nclass CheckedSubclass2Tests(CheckedSubclassingTestBase, unittest.TestCase):\n    BaseSet = ConcreteFrozenCheckedSetSubclass2\n"
  },
  {
    "path": "tests/common/test_debug.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 20017-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport unittest\n\nfrom edb.common import debug\n\n\nclass DebugTests(unittest.TestCase):\n\n    def test_common_debug_flags(self):\n        flags = {flag.name: flag for flag in debug.flags}\n        self.assertIn('edgeql_compile', flags)\n        self.assertIn('EdgeQL', flags['edgeql_compile'].doc)\n        self.assertIsInstance(debug.flags.edgeql_compile, bool)\n"
  },
  {
    "path": "tests/common/test_lru.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport dataclasses\nimport unittest\n\nfrom edb.common import lru\n\n\n@dataclasses.dataclass(frozen=True)\nclass Key:\n\n    name: str\n\n\nclass TestLRU(unittest.TestCase):\n\n    def test_lru_1(self):\n        l = lru.LRUMapping(maxsize=3)  # noqa\n\n        k1 = Key('1')\n        k2 = Key('2')\n        k3 = Key('3')\n        k4 = Key('4')\n        k5 = Key('5')\n\n        l[k1] = '1'\n        l[k2] = '2'\n        l[k3] = '3'\n        l[k4] = '4'\n        l[k5] = '5'\n\n        self.assertEqual(len(l), 3)\n        self.assertNotIn(k1, l)\n        self.assertNotIn(k2, l)\n\n        self.assertEqual(list(l), [k3, k4, k5])\n        self.assertEqual(list(l), [k3, k4, k5])\n\n        self.assertEqual(l[k4], '4')\n        self.assertEqual(list(l), [k3, k5, k4])\n        self.assertEqual(list(l), [k3, k5, k4])\n\n        l[k1] = '10'\n        self.assertEqual(list(l), [k5, k4, k1])\n\n        self.assertEqual(l[k4], '4')\n        self.assertEqual(l[k5], '5')\n        self.assertEqual(l[k1], '10')\n\n        self.assertEqual(list(l), [k4, k5, k1])\n\n        l[k5] = '50'\n        self.assertEqual(list(l), [k4, k1, k5])\n\n        l[k4] = l[k4]\n        self.assertEqual(list(l), [k1, k5, k4])\n"
  },
  {
    "path": "tests/common/test_markup.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport collections\nimport unittest\n\nfrom edb.common import markup\nfrom edb.common.markup.format import xrepr\nfrom edb.common.struct import Field\n\n\nclass SpecialList(list):\n    pass\n\n\nclass _SpecialListNode(markup.elements.base.Markup):\n    pass\n\n\nclass SpecialListNode(_SpecialListNode):\n    node = Field(_SpecialListNode, default=None)\n\n\n@markup.serializer.serializer.register(SpecialList)\ndef serialize_special(obj, *, ctx):\n    if obj and isinstance(obj[0], SpecialList):\n        child = markup.serialize(obj[0], ctx=ctx)\n        return SpecialListNode(node=child)\n    else:\n        return SpecialListNode()\n\n\nclass MarkupTests(unittest.TestCase):\n    def _get_test_markup(self):\n        def foobar():\n            raise ValueError('foobar: spam ham!')\n\n        exc = None\n\n        try:\n            foobar()\n        except Exception as ex:\n            exc = ex\n\n        return markup.serialize(exc, ctx=markup.Context())\n\n    def test_utils_markup_dumps(self):\n        assert markup.dumps('123') == \"'123'\"\n\n        expected = \\\n            \"[\\n    '123',\\n    1,\\n    1.1,\\n    {\\n        foo: ()\\n    }\\n]\"\n        expected = expected.replace(' ', '')\n        assert markup.dumps(['123', 1, 1.1, {'foo': ()}]).replace(\n            ' ', '') == expected\n\n    def test_utils_markup_overflow(self):\n        obj = a = []\n        for _ in range(200):\n            a.append([])\n            a = a[0]\n\n        result = markup.dumps(obj).replace(' ', '').replace('\\n', '')\n\n        # current limit is 100, so 2 chars per list - 200 + some space reserved\n        # for the OverflowBarier markup element\n        #\n        assert len(result) < 220\n\n    def test_utils_markup_overflow_deep_1(self):\n        obj = a = []\n        for _ in range(200):\n            a.append([])\n            a = a[0]\n\n        result = markup.dumps(obj).replace(' ', '').replace('\\n', '')\n\n        # current limit is 100, so 2 chars per list - 200 + some space reserved\n        # for the OverflowBarier markup element\n        #\n        assert len(result) < 220\n\n    def test_utils_markup_overflow_deep_2(self):\n        assert isinstance(\n            markup.elements.base.OverflowBarier(),\n            markup.elements.lang.TreeNode)\n        assert issubclass(\n            markup.elements.base.OverflowBarier, markup.elements.lang.TreeNode)\n        assert isinstance(\n            markup.elements.base.SerializationError(text='1', cls='1'),\n            markup.elements.lang.TreeNode)\n        assert issubclass(\n            markup.elements.base.SerializationError,\n            markup.elements.lang.TreeNode)\n        assert not isinstance(\n            markup.elements.base.Markup(), markup.elements.lang.TreeNode)\n        assert not issubclass(\n            markup.elements.base.Markup, markup.elements.lang.TreeNode)\n\n        from edb.common.markup.serializer.base \\\n            import OVERFLOW_BARIER, Context\n\n        def gen(deep):\n            if deep > 0:\n                return SpecialList([gen(deep - 1)])\n\n        assert not str(\n            markup.serialize(gen(OVERFLOW_BARIER - 1), ctx=Context())).count(\n                'Overflow')\n        assert str(markup.serialize(gen(OVERFLOW_BARIER + 10), ctx=Context(\n        ))).count('Overflow') == 1\n        assert not str(\n            markup.serialize(gen(OVERFLOW_BARIER + 10), ctx=Context())).count(\n                'SerializationError')\n\n    def test_utils_markup_overflow_wide(self):\n        obj3 = []\n        for _ in range(10):\n            obj2 = []\n            for _ in range(10):\n                obj1 = []\n                for _ in range(10):\n                    obj = []\n                    for _ in range(20):\n                        obj.append(list(1 for _ in range(10)))\n                    obj1.append(obj)\n                obj2.append(obj1)\n            obj3.append(obj2)\n\n        result = markup.dumps(obj3).replace(' ', '').replace('\\n', '')\n        assert len(result) < 13000\n\n    def test_utils_markup_format_xrepr(self):\n        a = '1234567890'\n\n        assert xrepr(a) == repr(a)\n\n        assert xrepr(a, max_len=5) == \"''...\"\n        assert xrepr(a, max_len=7) == \"'12'...\"\n        assert xrepr(a, max_len=12) == repr(a)\n\n        assert repr(repr) == '<built-in function repr>'\n\n        assert xrepr(repr) == repr(repr)\n        assert xrepr(repr, max_len=10) == '<built>...'\n        assert xrepr(repr, max_len=100) == repr(repr)\n\n        assert len(xrepr(repr, max_len=10)) == 10\n\n    def test_utils_markup_dump_ordereddict(self):\n        obj = collections.OrderedDict([[1, 2], [2, 3], [3, 4], [5, 6]])\n        result = ''.join(markup.dumps(obj).split())\n        assert result == '{1:2,2:3,3:4,5:6}'\n"
  },
  {
    "path": "tests/common/test_parametric.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import ClassVar, Generic, TypeVar\n\n# I'm not really sure why this is needed here\n# These are things needed for ParametricType?\nfrom typing import Optional, Tuple, Dict, Any  # NoQA\n\nimport unittest\n\nfrom edb.common.parametric import ParametricType\n\nT = TypeVar(\"T\")\nK = TypeVar(\"K\")\nZ = TypeVar(\"Z\")\nA = TypeVar(\"A\")\nB = TypeVar(\"B\")\n\n\nclass ParametricTypeTests(unittest.TestCase):\n\n    def test_common_parametric_basics(self) -> None:\n        with self.assertRaisesRegex(\n            TypeError,\n            'must be declared as Generic'\n        ):\n            class Foo(ParametricType):\n                pass\n\n        with self.assertRaisesRegex(\n            TypeError,\n            'P1: missing ClassVar for generic parameter ~T'\n        ):\n            class P1(ParametricType, Generic[T]):\n                pass\n\n        class P2(ParametricType, Generic[T]):\n            random_class_var: ClassVar[int]\n            another_class_var: ClassVar[type[str]]\n            not_class_var: int\n            t: ClassVar[type[T]]  # type: ignore\n\n        self.assertTrue(issubclass(P2[int].t, int))  # type: ignore\n\n        with self.assertRaisesRegex(\n            TypeError,\n            \"type 'P2' expects 1 type parameter, got 2\"\n        ):\n            P2[int, str]  # type: ignore\n\n        with self.assertRaisesRegex(\n            TypeError,\n            \"P3: missing one or more type arguments for base 'P2'\"\n        ):\n            class P3(P2, Generic[K]):\n                pass\n\n        class P4(P2[Z], Generic[K, Z]):\n            pass\n\n        self.assertTrue(issubclass(P4[str, int].t, int))  # type: ignore\n\n        class Bar:\n            pass\n\n        class P5(P4[A, B], Bar):\n            pass\n\n        self.assertTrue(issubclass(P5[float, str].t, str))  # type: ignore\n\n        # Note the origin class error below is imperfect because we\n        # rely on __orig_bases__ in the subclass check and that elides\n        # P5.\n        with self.assertRaisesRegex(\n            TypeError,\n            \"P6: missing one or more type arguments for base 'P4'\"\n        ):\n            class P6(P5):\n                pass\n\n        class P7(P5[int, float]):\n            pass\n\n        self.assertTrue(issubclass(P7.t, float))  # type: ignore\n"
  },
  {
    "path": "tests/common/test_prometheus.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport unittest\n\nfrom edb.common import prometheus as prom\n\ntry:\n    import prometheus_client as pmc_root\n    import prometheus_client.registry as pmc_reg\nexcept ImportError:\n    PMC = None  # type: ignore\nelse:\n    class PMC:  # type: ignore\n\n        class Registry(pmc_reg.CollectorRegistry):\n            def __init__(self):\n                super().__init__(auto_describe=True)\n\n        def generate(registry):\n            return pmc_root.generate_latest(registry).decode()\n\n        class _RequiredRegistryMixin:\n            def __init__(self, *args, registry, **kwargs):\n                super().__init__(*args, registry=registry, **kwargs)\n                self._kwargs['registry'] = registry\n\n            def _metric_init(self):\n                super()._metric_init()\n                self._created = CREATED_AT\n\n        class Counter(_RequiredRegistryMixin, pmc_root.Counter):\n            pass\n\n        class Gauge(_RequiredRegistryMixin, pmc_root.Gauge):\n            pass\n\n        class Histogram(_RequiredRegistryMixin, pmc_root.Histogram):\n            pass\n\n        class Info(_RequiredRegistryMixin, pmc_root.Info):\n            pass\n\n\nclass EP:\n\n    class Registry(prom.Registry):\n\n        def now(self):\n            return CREATED_AT\n\n\nCREATED_AT = -1142.11\n\n\n@unittest.skipIf(PMC is None, 'prometheus_client package is not installed')\nclass TestPrometheusClient(unittest.TestCase):\n\n    def test_prometheus_01(self):\n\n        def run_pmc():\n            registry = PMC.Registry()\n\n            test_counter = PMC.Counter(\n                'test_counter', 'A test counter\"',\n                registry=registry)\n            test_labeled_counter = PMC.Counter(\n                'test_labeled_counter', 'A test labeled counter',\n                labelnames=['t1', 't2'], registry=registry)\n            test_labeled_gauge = PMC.Gauge(\n                'test_labeled_gauge', 'test labeled gauge\\'\"\\n',\n                labelnames=['g1'], registry=registry)\n            test_gauge = PMC.Gauge(\n                'test_gauge', 'A test      gauge',\n                registry=registry)\n\n            r0 = PMC.generate(registry)\n\n            test_counter.inc()\n            test_counter.inc(1.2)\n\n            test_gauge.inc()\n            test_gauge.dec()\n            test_gauge.set(1.2)\n\n            test_labeled_counter.labels('aaa', 'bbb\"').inc()\n            test_labeled_counter.labels('aaa', 'zzz').inc(1.3)\n\n            test_labeled_gauge.labels('l1').inc(1.3)\n\n            r1 = PMC.generate(registry)\n\n            test_labeled_counter.labels('aaa', 'ezi').inc(1.4)\n            test_gauge.set(111.2)\n\n            test_labeled_gauge.labels('l1').dec(0.1)\n            test_labeled_gauge.labels('l2').set(42)\n\n            r2 = PMC.generate(registry)\n\n            return [r0, r1, r2]\n\n        def run_emc():\n            r = EP.Registry()\n\n            test_counter = r.new_counter(\n                'test_counter_total', 'A test counter\"'\n            )\n\n            test_labeled_counter = r.new_labeled_counter(\n                'test_labeled_counter_total', 'A test labeled counter',\n                labels=('t1', 't2')\n            )\n\n            test_labeled_gauge = r.new_labeled_gauge(\n                'test_labeled_gauge', 'test labeled gauge\\'\"\\n',\n                labels=('g1',)\n            )\n\n            test_gauge = r.new_gauge(\n                'test_gauge', 'A test      gauge',\n            )\n\n            r0 = r.generate()\n\n            test_counter.inc()\n            test_counter.inc(1.2)\n\n            test_gauge.inc()\n            test_gauge.dec()\n            test_gauge.set(1.2)\n\n            test_labeled_counter.inc(1.0, 'aaa', 'bbb\"')\n            test_labeled_counter.inc(1.3, 'aaa', 'zzz')\n\n            test_labeled_gauge.inc(1.3, 'l1')\n\n            r1 = r.generate()\n\n            test_labeled_counter.inc(1.4, 'aaa', 'ezi')\n            test_gauge.set(111.2)\n\n            test_labeled_gauge.dec(0.1, 'l1')\n            test_labeled_gauge.set(42, 'l2')\n\n            r2 = r.generate()\n\n            return [r0, r1, r2]\n\n        pmc_r = run_pmc()\n        emc_r = run_emc()\n        self.assertEqual(pmc_r, emc_r)\n\n    def test_prometheus_02(self):\n\n        def run_pmc():\n            registry = PMC.Registry()\n\n            test_info = PMC.Info(\n                'test', 'A  test info',\n                registry=registry)\n\n            test_info.info(dict(blah='blahaaah', spam='ha\"\\nm'))\n            r1 = PMC.generate(registry)\n            return [r1]\n\n        def run_emc():\n            r = EP.Registry()\n\n            r.set_info(\n                'test', 'A  test info',\n                blah='blahaaah', spam='ha\"\\nm'\n            )\n\n            r1 = r.generate()\n\n            return [r1]\n\n        pmc_r = run_pmc()\n        emc_r = run_emc()\n        self.assertEqual(pmc_r, emc_r)\n\n    def test_prometheus_03(self):\n\n        def run_pmc():\n            registry = PMC.Registry()\n\n            test_hist = PMC.Histogram(\n                'test_hist', 'A  test info',\n                registry=registry)\n\n            r0 = PMC.generate(registry)\n\n            test_hist.observe(0.22)\n            test_hist.observe(0.44)\n            test_hist.observe(0.66)\n            test_hist.observe(0.43)\n            test_hist.observe(2.0)\n\n            r1 = PMC.generate(registry)\n\n            test_hist.observe(-1)\n            test_hist.observe(0.0001)\n            test_hist.observe(0.43)\n\n            r2 = PMC.generate(registry)\n\n            return [r0, r1, r2]\n\n        def run_emc():\n            r = EP.Registry()\n\n            test_hist = r.new_histogram(\n                'test_hist', 'A  test info',\n            )\n\n            r0 = r.generate()\n\n            test_hist.observe(0.22)\n            test_hist.observe(0.44)\n            test_hist.observe(0.66)\n            test_hist.observe(0.43)\n            test_hist.observe(2.0)\n\n            r1 = r.generate()\n\n            test_hist.observe(-1)\n            test_hist.observe(0.0001)\n            test_hist.observe(0.43)\n\n            r2 = r.generate()\n\n            return [r0, r1, r2]\n\n        pmc_r = run_pmc()\n        emc_r = run_emc()\n        self.assertEqual(pmc_r, emc_r)\n\n    def test_prometheus_04(self):\n\n        def run_pmc():\n            registry = PMC.Registry()\n\n            test_hist = PMC.Histogram(\n                'test_hist_seconds', 'A  test info',\n                registry=registry)\n\n            test_hist.observe(0.22)\n\n            r1 = PMC.generate(registry)\n            return [r1]\n\n        def run_emc():\n            r = EP.Registry()\n\n            test_hist = r.new_histogram(\n                'test_hist', 'A  test info',\n                unit=prom.Unit.SECONDS\n            )\n\n            test_hist.observe(0.22)\n            r1 = r.generate()\n            return [r1]\n\n        pmc_r = run_pmc()\n        emc_r = run_emc()\n        self.assertEqual(pmc_r, emc_r)\n\n    def test_prometheus_05(self):\n        # basic sanity checks\n        bs = prom.calc_buckets(0.05, 10.0, increment_ratio=1.20)\n        self.assertEqual(len(bs), 30)\n        self.assertEqual(len(set(bs)), len(bs))\n        self.assertEqual(bs, tuple(sorted(bs)))\n\n    def test_prometheus_06(self):\n        r = EP.Registry(prefix='edgedb')\n\n        test = r.new_counter(\n            'test_total', 'A  test info'\n        )\n        test.inc()\n        r1 = r.generate()\n\n        self.assertIn('\\nedgedb_test_total ', r1)\n        self.assertIn('\\nedgedb_test_created ', r1)\n\n        with self.assertRaisesRegex(\n                ValueError, \"metric with a name 'edgedb_test'\"):\n            r.new_counter(\n                'test_total', 'A  test info'\n            )\n\n    def test_prometheus_07(self):\n\n        def run_pmc():\n            registry = PMC.Registry()\n\n            test_labeled_counter = PMC.Counter(\n                'test_labeled_counter', 'A test labeled counter',\n                labelnames=['t1', 't2'], registry=registry)\n            test_labeled_gauge = PMC.Gauge(  # NoQA\n                'test_labeled_gauge', 'test labeled gauge\\'\"\\n',\n                labelnames=['g1'], registry=registry)\n\n            r1 = PMC.generate(registry)\n\n            test_labeled_counter.labels('blah', 'spam').inc()\n\n            r2 = PMC.generate(registry)\n\n            test_labeled_counter.labels('blah', 'ham').inc()\n\n            r3 = PMC.generate(registry)\n\n            return [r1, r2, r3]\n\n        def run_emc():\n            r = EP.Registry()\n\n            test_labeled_counter = r.new_labeled_counter(\n                'test_labeled_counter_total', 'A test labeled counter',\n                labels=('t1', 't2')\n            )\n\n            test_labeled_gauge = r.new_labeled_gauge(  # NoQA\n                'test_labeled_gauge', 'test labeled gauge\\'\"\\n',\n                labels=('g1',)\n            )\n\n            r1 = r.generate()\n\n            test_labeled_counter.inc(1.0, 'blah', 'spam')\n\n            r2 = r.generate()\n\n            test_labeled_counter.inc(1.0, 'blah', 'ham')\n\n            r3 = r.generate()\n\n            return [r1, r2, r3]\n\n        pmc_r = run_pmc()\n        emc_r = run_emc()\n        self.assertEqual(pmc_r, emc_r)\n\n    def test_prometheus_08(self):\n\n        def run_pmc():\n            registry = PMC.Registry()\n\n            test_hist = PMC.Histogram(\n                'test_hist', 'A  test info',\n                labelnames=['tenant'], registry=registry)\n\n            r0 = PMC.generate(registry)\n\n            test_hist.labels('1').observe(0.22)\n            test_hist.labels('2').observe(0.44)\n            test_hist.labels('1').observe(0.66)\n            test_hist.labels('2').observe(0.43)\n            test_hist.labels('1').observe(2.0)\n\n            r1 = PMC.generate(registry)\n\n            test_hist.labels('2').observe(-1)\n            test_hist.labels('1').observe(0.0001)\n            test_hist.labels('2').observe(0.43)\n\n            r2 = PMC.generate(registry)\n\n            return [r0, r1, r2]\n\n        def run_emc():\n            r = EP.Registry()\n\n            test_hist = r.new_labeled_histogram(\n                'test_hist', 'A  test info', labels=('tenant',)\n            )\n\n            r0 = r.generate()\n\n            test_hist.observe(0.22, '1')\n            test_hist.observe(0.44, '2')\n            test_hist.observe(0.66, '1')\n            test_hist.observe(0.43, '2')\n            test_hist.observe(2.0, '1')\n\n            r1 = r.generate()\n\n            test_hist.observe(-1, '2')\n            test_hist.observe(0.0001, '1')\n            test_hist.observe(0.43, '2')\n\n            r2 = r.generate()\n\n            return [r0, r1, r2]\n\n        pmc_r = run_pmc()\n        emc_r = run_emc()\n        self.assertEqual(pmc_r, emc_r)\n"
  },
  {
    "path": "tests/common/test_signalctl.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport asyncio\nimport pickle\nimport signal\nimport socket\nimport subprocess\nimport sys\nimport textwrap\n\nfrom edb.testbase import server as tb\n\n\nTIMEOUT = 30\n\n\nclass ChildProcess:\n    def __init__(self, test_prog, global_prog=\"\"):\n        self._args = (\n            sys.executable,\n            \"-I\",\n            \"-m\",\n            \"edb.testbase.proc\",\n            textwrap.dedent(global_prog)\n            + \"\\n\"\n            + textwrap.dedent(\n                \"\"\"\\\n                import signal\n                from edb.common import signalctl\n                \"\"\"\n            ),\n            textwrap.dedent(test_prog),\n        )\n\n    async def __aenter__(self):\n        sock, child_sock = socket.socketpair(socket.AF_UNIX)\n        fd = child_sock.detach()\n        self.child_reader, self.child_writer = await asyncio.open_connection(\n            sock=sock\n        )\n        self.proc = await asyncio.create_subprocess_exec(\n            *self._args,\n            str(fd),\n            stderr=subprocess.PIPE,\n            pass_fds=[fd],\n        )\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        try:\n            _, stderr = await asyncio.wait_for(\n                self.proc.communicate(), TIMEOUT\n            )\n            if self.proc.returncode > 0:\n                raise ChildProcessError(\"\\n\\n\" + stderr.decode())\n        finally:\n            self.child_writer.close()\n            try:\n                self.proc.kill()\n                await asyncio.wait_for(self.proc.wait(), TIMEOUT)\n            except OSError:\n                pass\n            finally:\n                await self.child_writer.wait_closed()\n\n    def __getattr__(self, item):\n        return getattr(self.proc, item)\n\n\ndef spawn(test_prog, global_prog=\"\"):\n    return ChildProcess(test_prog, global_prog)\n\n\nclass TestSignalctl(tb.TestCase):\n    def notify_child(self, p, mark):\n        p.child_writer.write(str(mark).encode() + b\"\\n\")\n\n    async def wait_for_child(self, p, mark):\n        line = await asyncio.wait_for(p.child_reader.readline(), TIMEOUT)\n        if not line:\n            self.fail(\"Child process exited unexpectedly.\")\n        elif len(line) <= 3:\n            self.assertEqual(line.strip(), str(mark).encode())\n        else:\n            line += await asyncio.wait_for(p.child_reader.read(), TIMEOUT)\n            ex, traceback = pickle.loads(line)\n            raise ex from ChildProcessError(\"\\n\\n\" + traceback.strip())\n\n    async def test_signalctl_wait_for_01(self):\n        test_prog = \"\"\"\\\n        async def test_signalctl_wait_for_01_child(self):\n            async def task():\n                self.notify_parent(1)\n                await asyncio.sleep(1)\n\n            with signalctl.SignalController(signal.SIGTERM) as sc:\n                with self.assertRaisesRegex(signalctl.SignalError, \"SIGTERM\"):\n                    await sc.wait_for(task(), cancel_on={signal.SIGTERM})\n\n            self.notify_parent(2)\n            await self.wait_for_parent(3)\n        \"\"\"\n        async with spawn(test_prog) as p:\n            await self.wait_for_child(p, 1)\n            p.terminate()\n\n            await self.wait_for_child(p, 2)\n            p.terminate()\n            self.assertEqual(\n                await asyncio.wait_for(p.wait(), TIMEOUT), -signal.SIGTERM\n            )\n\n    async def test_signalctl_wait_for_02(self):\n        test_prog = \"\"\"\\\n        async def test_signalctl_wait_for_02_child(self):\n            async def task():\n                self.notify_parent(1)\n                await asyncio.sleep(1)\n\n            with signalctl.SignalController(signal.SIGINT) as sc:\n                with self.assertRaisesRegex(signalctl.SignalError, \"SIGINT\"):\n                    await sc.wait_for(task(), cancel_on={signal.SIGINT})\n\n            self.notify_parent(2)\n            await self.wait_for_parent(3)\n        \"\"\"\n        end_reached = False\n        async with spawn(test_prog) as p:\n            await self.wait_for_child(p, 1)\n            p.send_signal(signal.SIGINT)\n\n            await self.wait_for_child(p, 2)\n            p.send_signal(signal.SIGINT)\n            self.assertEqual(\n                await asyncio.wait_for(p.wait(), TIMEOUT), -signal.SIGINT\n            )\n            end_reached = True\n\n        self.assertTrue(end_reached)\n\n    async def test_signalctl_wait_for_03(self):\n        test_prog = \"\"\"\\\n        async def test_signalctl_wait_for_03_child(self):\n            async def task():\n                self.notify_parent(1)\n                await asyncio.sleep(1)\n\n            with signalctl.SignalController(\n                signal.SIGTERM, signal.SIGINT\n            ) as sc:\n                with self.assertRaisesRegex(signalctl.SignalError, \"SIGTERM\"):\n                    await sc.wait_for(task(), cancel_on={signal.SIGTERM})\n\n            self.notify_parent(2)\n            await self.wait_for_parent(3)\n        \"\"\"\n\n        async with spawn(test_prog) as p:\n            await self.wait_for_child(p, 1)\n\n            p.send_signal(signal.SIGINT)\n            with self.assertRaises(asyncio.TimeoutError):\n                await asyncio.wait_for(\n                    asyncio.gather(p.wait(), p.child_reader.read(1)), 0.2\n                )\n\n            p.terminate()\n            await self.wait_for_child(p, 2)\n\n            p.terminate()\n            self.assertEqual(\n                await asyncio.wait_for(p.wait(), TIMEOUT), -signal.SIGTERM\n            )\n\n    async def test_signalctl_wait_for_04(self):\n        test_prog = \"\"\"\\\n        async def test_signalctl_wait_for_04_child(self):\n            async def task():\n                self.notify_parent(1)\n                await asyncio.sleep(1)\n\n            with signalctl.SignalController(\n                signal.SIGTERM, signal.SIGINT\n            ) as sc:\n                with self.assertRaisesRegex(signalctl.SignalError, \"SIGINT\"):\n                    await sc.wait_for(task(), cancel_on={signal.SIGINT})\n\n            self.notify_parent(2)\n        \"\"\"\n\n        async with spawn(test_prog) as p:\n            await self.wait_for_child(p, 1)\n\n            p.terminate()\n            with self.assertRaises(asyncio.TimeoutError):\n                await asyncio.wait_for(\n                    asyncio.gather(p.wait(), p.child_reader.read(1)), 0.2\n                )\n\n            p.send_signal(signal.SIGINT)\n            await self.wait_for_child(p, 2)\n\n    async def test_signalctl_wait_for_05(self):\n        test_prog = \"\"\"\\\n        async def test_signalctl_wait_for_05_child(self):\n            async def task():\n                self.notify_parent(2)\n                await self.wait_for_parent(3)\n\n            with signalctl.SignalController(signal.SIGTERM) as sc:\n                with self.assertRaises(asyncio.TimeoutError):\n                    await asyncio.wait_for(\n                        sc.wait_for(self.wait_for_parent(1)), 0.1\n                    )\n\n                with self.assertRaisesRegex(signalctl.SignalError, \"SIGTERM\"):\n                    await sc.wait_for(task(), cancel_on={signal.SIGTERM})\n\n            self.notify_parent(4)\n        \"\"\"\n\n        async with spawn(test_prog) as p:\n            await self.wait_for_child(p, 2)\n            p.terminate()\n            await self.wait_for_child(p, 4)\n\n    async def test_signalctl_wait_for_06(self):\n        test_prog = \"\"\"\\\n        async def test_signalctl_wait_for_06_child(self):\n            fut = self.loop.create_future()\n            waiter = self.loop.create_future()\n\n            async def _task():\n                waiter.set_result(None)\n                return await fut\n\n            with signalctl.SignalController(signal.SIGTERM) as sc:\n                task = self.loop.create_task(\n                    sc.wait_for(_task(), cancel_on={signal.SIGTERM})\n                )\n                await waiter\n\n                # The task is cancelled (not by signal) at the moment the\n                # result is ready - return the result instead of the error\n                fut.set_result(123)\n                task.cancel()\n                self.assertEqual(await task, 123)\n        \"\"\"\n\n        async with spawn(test_prog):\n            pass\n\n    async def test_signalctl_wait_for_07(self):\n        test_prog = \"\"\"\\\n        async def test_signalctl_wait_for_07_child(self):\n            fut = self.loop.create_future()\n\n            class Waiter:\n                def done(self):\n                    return False\n\n                def set_result(self, result):\n                    # Simulates a completed task at the moment signal arrives\n                    fut.set_result(123)\n\n            with signalctl.SignalController(signal.SIGTERM) as sc:\n                task = self.loop.create_task(\n                    sc.wait_for(fut, cancel_on={signal.SIGTERM})\n                )\n                waiter = Waiter()\n                sc._register_waiter(signal.SIGTERM, waiter)\n                try:\n                    os.kill(os.getpid(), signal.SIGTERM)\n                    self.assertEqual(await task, 123)\n                finally:\n                    sc._discard_waiter(signal.SIGTERM, waiter)\n        \"\"\"\n\n        async with spawn(test_prog, global_prog=\"import os\"):\n            pass\n\n    async def test_signalctl_wait_for_08(self):\n        test_prog = \"\"\"\\\n        async def test_signalctl_wait_for_08_child(self):\n            async def task():\n                self.notify_parent(1)\n                try:\n                    await self.wait_for_parent(2)\n                except asyncio.CancelledError:\n                    # In case the task cancellation is intercepted, ..\n                    return 123\n\n            with signalctl.SignalController(signal.SIGTERM) as sc:\n                # .. the SignalError is not raised\n                await sc.wait_for(task(), cancel_on={signal.SIGTERM})\n                self.notify_parent(3)\n        \"\"\"\n\n        async with spawn(test_prog) as p:\n            await self.wait_for_child(p, 1)\n            p.terminate()\n            await self.wait_for_child(p, 3)\n\n    async def test_signalctl_wait_for_09(self):\n        test_prog = \"\"\"\\\n        async def test_signalctl_wait_for_09_child(self):\n            fut = self.loop.create_future()\n\n            async def _task():\n                self.notify_parent(1)\n                try:\n                    await self.wait_for_parent(2)  # cancelled by signal\n                except asyncio.CancelledError:\n                    # In case the task cancellation is hanging, ..\n                    fut.set_result(None)\n                    await self.wait_for_parent(2)\n\n            with signalctl.SignalController(signal.SIGTERM) as sc:\n                task = self.loop.create_task(\n                    sc.wait_for(_task(), cancel_on={signal.SIGTERM})\n                )\n                await fut\n\n                # .. we should still be able to reliably cancel the task\n                with self.assertRaises(asyncio.TimeoutError):\n                    await asyncio.wait_for(task, 0.2)\n                self.assertTrue(task.done())\n                self.assertTrue(task.cancelled())\n        \"\"\"\n\n        async with spawn(test_prog) as p:\n            await self.wait_for_child(p, 1)\n            p.terminate()\n\n    async def test_signalctl_wait_for_10(self):\n        test_prog = \"\"\"\\\n        async def test_signalctl_wait_for_10_child(self):\n            async def _subtask1():\n                with signalctl.SignalController(signal.SIGTERM) as sc2:\n                    self.notify_parent(1)\n                    try:\n                        with self.assertRaisesRegex(\n                            signalctl.SignalError, \"SIGTERM\"\n                        ):  # not actually caught; task cancelled\n                            await sc2.wait_for(self.wait_for_parent(2))\n                    finally:\n                        self.notify_parent(3)\n\n            async def _subtask2():\n                self.notify_parent(1)\n                try:\n                    await asyncio.sleep(10)\n                finally:\n                    self.notify_parent(3)\n\n            async def _task():\n                async with asyncio.TaskGroup() as tg:\n                    tg.create_task(_subtask1())\n                    tg.create_task(_subtask2())\n\n            with signalctl.SignalController(signal.SIGTERM) as sc:\n                with self.assertRaisesRegex(signalctl.SignalError, \"SIGTERM\"):\n                    await sc.wait_for(_task())\n        \"\"\"\n\n        async with spawn(test_prog) as p:\n            await self.wait_for_child(p, 1)\n            await self.wait_for_child(p, 1)\n            p.terminate()\n            await self.wait_for_child(p, 3)\n            await self.wait_for_child(p, 3)\n\n    async def test_signalctl_wait_for_11(self):\n        test_prog = \"\"\"\\\n        async def test_signalctl_wait_for_11_child(self):\n            async def task():\n                self.notify_parent(1)\n                try:\n                    await self.wait_for_parent(2)\n                finally:\n                    self.notify_parent(3)\n\n            with signalctl.SignalController(signal.SIGTERM) as sc:\n                with self.assertRaisesRegex(signalctl.SignalError, \"SIGTERM\"):\n                    await sc.wait_for(task())\n            self.notify_parent(4)\n        \"\"\"\n\n        async with spawn(test_prog) as p:\n            await self.wait_for_child(p, 1)\n            p.terminate()\n            await self.wait_for_child(p, 3)\n            await self.wait_for_child(p, 4)\n\n    async def test_signalctl_wait_for_12(self):\n        test_prog = \"\"\"\\\n        async def test_signalctl_wait_for_12_child(self):\n            async def task():\n                self.notify_parent(1)\n                try:\n                    await self.wait_for_parent(2)\n                finally:\n                    self.notify_parent(3)\n                    try:\n                        await self.wait_for_parent(4)\n                    finally:\n                        self.notify_parent(5)\n                        await self.wait_for_parent(6)\n\n            with signalctl.SignalController(\n                signal.SIGTERM, signal.SIGINT, signal.SIGUSR1\n            ) as sc:\n                with self.assertRaises(signalctl.SignalError) as ctx:\n                    await sc.wait_for(task())\n            ex = ctx.exception\n            self.assertEqual(ex.signo, signal.SIGUSR1)\n            self.assertEqual(ex.__context__.signo, signal.SIGINT)\n            self.assertEqual(ex.__context__.__context__.signo, signal.SIGTERM)\n            self.notify_parent(7)\n        \"\"\"\n\n        async with spawn(test_prog) as p:\n            await self.wait_for_child(p, 1)\n            p.terminate()\n            await self.wait_for_child(p, 3)\n            p.send_signal(signal.SIGINT)\n            await self.wait_for_child(p, 5)\n            p.send_signal(signal.SIGUSR1)\n            await self.wait_for_child(p, 7)\n\n    async def test_signalctl_wait_for_13(self):\n        test_prog = \"\"\"\\\n        async def test_signalctl_wait_for_13_child(self):\n            fut = self.loop.create_future()\n            async def _task():\n                self.notify_parent(1)\n                try:\n                    await self.wait_for_parent(2)\n                finally:\n                    try:\n                        self.notify_parent(3)\n                        await self.wait_for_parent(4)\n                    finally:\n                        fut.set_result(None)\n                        await asyncio.sleep(10)\n\n            with signalctl.SignalController(\n                signal.SIGTERM, signal.SIGINT\n            ) as sc:\n                with self.assertRaises(asyncio.TimeoutError) as ctx:\n                    task = self.loop.create_task(sc.wait_for(_task()))\n                    await fut\n                    await asyncio.wait_for(task, 0.1)\n            ex = ctx.exception\n            while not isinstance(ex, signalctl.SignalError):\n                ex = ex.__context__\n            self.assertEqual(ex.signo, signal.SIGINT)\n            self.assertEqual(ex.__context__.signo, signal.SIGTERM)\n            self.notify_parent(5)\n        \"\"\"\n\n        async with spawn(test_prog) as p:\n            await self.wait_for_child(p, 1)\n            p.terminate()\n            await self.wait_for_child(p, 3)\n            p.send_signal(signal.SIGINT)\n            await self.wait_for_child(p, 5)\n\n    async def test_signalctl_wait_for_14(self):\n        test_prog = \"\"\"\\\n        async def test_signalctl_wait_for_14_child(self):\n            fut = self.loop.create_future()\n            async def _task():\n                self.notify_parent(1)\n                try:\n                    await self.wait_for_parent(2)\n                finally:\n                    fut.set_result(None)\n                    try:\n                        await asyncio.sleep(10)\n                    finally:\n                        self.notify_parent(3)\n                        await self.wait_for_parent(4)\n\n            with signalctl.SignalController(\n                signal.SIGTERM, signal.SIGINT\n            ) as sc:\n                with self.assertRaises(signalctl.SignalError) as ctx:\n                    task = self.loop.create_task(sc.wait_for(_task()))\n                    await fut\n                    await asyncio.wait_for(task, 0.1)\n            ex = ctx.exception\n            self.assertEqual(ex.signo, signal.SIGINT)\n            self.assertEqual(ex.__context__.signo, signal.SIGTERM)\n            self.notify_parent(5)\n        \"\"\"\n\n        async with spawn(test_prog) as p:\n            await self.wait_for_child(p, 1)\n            p.terminate()\n            await self.wait_for_child(p, 3)\n            p.send_signal(signal.SIGINT)\n            await self.wait_for_child(p, 5)\n\n    async def test_signalctl_add_handler(self):\n        test_prog = \"\"\"\\\n        async def test_signalctl_wait_for_signal_child(self):\n            done = asyncio.Future()\n            expectation = [\n                signal.SIGTERM,\n                signal.SIGTERM,\n                signal.SIGUSR1,\n                signal.SIGINT,\n                signal.SIGINT,\n            ]\n            n_len = len(expectation) + 1\n\n            def handler(s):\n                try:\n                    self.assertEqual(s, expectation.pop(0))\n                    self.notify_parent(n_len - len(expectation))\n                    if not expectation:\n                        done.set_result(None)\n                except Exception as e:\n                    done.set_exception(e)\n\n            with signalctl.SignalController(*set(expectation)) as sc:\n                sc.add_handler(handler)\n                self.notify_parent(1)\n                await done\n        \"\"\"\n\n        async with spawn(test_prog) as p:\n            await self.wait_for_child(p, 1)\n            p.send_signal(signal.SIGTERM)\n            await self.wait_for_child(p, 2)\n            p.send_signal(signal.SIGTERM)\n            await self.wait_for_child(p, 3)\n            p.send_signal(signal.SIGUSR1)\n            await self.wait_for_child(p, 4)\n            p.send_signal(signal.SIGINT)\n            await self.wait_for_child(p, 5)\n            p.send_signal(signal.SIGINT)\n            await self.wait_for_child(p, 6)\n"
  },
  {
    "path": "tests/common/test_struct.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport pickle\nimport unittest\n\nfrom edb.common.struct import (\n    Struct,\n    RTStruct,\n    MixedStruct,\n    MixedRTStruct,\n    Field,\n)\n\n\nclass PickleTest(Struct):\n    a = Field(str, default='42')\n    b = Field(int)\n\n\nclass PickleTestMixed(MixedStruct):\n    a = Field(str, default='42')\n    b = Field(int)\n\n\nclass StructTests(unittest.TestCase):\n\n    def test_common_struct_basics(self):\n        class Test(Struct):\n            field1 = Field(str, default='42')\n            field2 = Field(bool)\n\n        with self.assertRaisesRegex(TypeError, 'field2 is required'):\n            Test()\n\n        t = Test(field2=False)\n        assert t.field1 == '42'\n        assert t.field2 is False\n\n        assert set(t) == {'field1', 'field2'}\n\n    def test_common_struct_coercion(self):\n        class Test(RTStruct):\n            field = Field(int, coerce=True)\n\n        assert Test(field=1).field == 1\n        assert Test(field='42').field == 42\n        with self.assertRaisesRegex(TypeError, 'cannot coerce'):\n            Test(field='42.2')\n\n        class Test(RTStruct):\n            field = Field(int)\n\n        assert Test(field=1).field == 1\n        with self.assertRaisesRegex(TypeError, 'expected int'):\n            Test(field='42')\n\n    def test_common_struct_strictness(self):\n        class Test(Struct):\n            field = Field(str, None)\n\n        assert Test.__slots__ == ('field', )\n\n        t = Test()\n        t.field = 'foo'\n        assert t.field == 'foo'\n        with self.assertRaisesRegex(AttributeError, 'has no attribute'):\n            t.foo = 'bar'\n\n        class DTest(Test):\n            field2 = Field(int, None)\n\n        t = DTest()\n        t.field = '1'\n        t.field2 = 2\n        assert t.field == '1'\n        assert t.field2 == 2\n        with self.assertRaisesRegex(AttributeError, 'has no attribute'):\n            t.foo = 'bar'\n\n        with self.assertRaisesRegex(\n                TypeError, 'field3 is an invalid argument'):\n            DTest(field='1', field2=2, field3='aaa')\n\n        t = DTest()\n\n        with self.assertRaisesRegex(TypeError,\n                                    'field3 is an invalid argument'):\n            t.update(field='1', field2=2, field3='aaa')\n\n    def test_common_struct_mixed(self):\n        class Test(MixedStruct):\n            field1 = Field(str, default='42')\n            field2 = Field(bool)\n\n        t1 = Test(field1='field1', field2=True, spam='ham')\n        t1.update(ham='spam')\n\n        t1.monty = 'python'\n        assert t1.monty == 'python'\n\n    def test_common_struct_pickle(self):\n        s1 = PickleTest(b=41)\n        s2 = pickle.loads(pickle.dumps(s1))\n        assert s2.b == 41 and s2.a == '42'\n        assert s2.__class__.__name__ == 'PickleTest'\n\n        s1 = PickleTestMixed(b=41)\n        s2 = pickle.loads(pickle.dumps(s1))\n        assert s2.b == 41 and s2.a == '42'\n        assert s2.__class__.__name__ == 'PickleTestMixed'\n\n    def test_common_struct_frozen(self):\n        class Test(MixedRTStruct):\n            field1 = Field(str, default='42', frozen=True)\n            field2 = Field(bool)\n\n        t1 = Test(field1='field1', field2=True, spam='ham')\n\n        with self.assertRaisesRegex(ValueError, 'cannot assign'):\n            t1.update(field1='spam')\n\n        with self.assertRaisesRegex(ValueError, 'cannot assign'):\n            t1.field1 = 'aaa'\n\n        self.assertEqual(t1.field1, 'field1')\n        self.assertEqual(t1.field2, True)\n\n        t1.field2 = False\n        self.assertEqual(t1.field2, False)\n"
  },
  {
    "path": "tests/common/test_supervisor.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport asyncio\n\nfrom edb.common import supervisor\nfrom edb.testbase import server as tb\n\n\nclass TestSupervisor(tb.TestCase):\n\n    async def test_supervisor_01(self):\n\n        async def foo1():\n            await asyncio.sleep(0.1)\n            return 42\n\n        async def foo2():\n            await asyncio.sleep(0.2)\n            return 11\n\n        g = await supervisor.Supervisor.create()\n        t1 = g.create_task(foo1())\n        t2 = g.create_task(foo2())\n        await g.wait()\n\n        self.assertEqual(t1.result(), 42)\n        self.assertEqual(t2.result(), 11)\n\n    async def test_supervisor_02(self):\n\n        async def foo1():\n            await asyncio.sleep(0.1)\n            return 42\n\n        async def foo2():\n            await asyncio.sleep(0.2)\n            return 11\n\n        g = await supervisor.Supervisor.create()\n        t1 = g.create_task(foo1())\n        await asyncio.sleep(0.15)\n        t2 = g.create_task(foo2())\n        await g.wait()\n\n        self.assertEqual(t1.result(), 42)\n        self.assertEqual(t2.result(), 11)\n\n    async def test_supervisor_03(self):\n\n        async def foo1():\n            await asyncio.sleep(1)\n            return 42\n\n        async def foo2():\n            await asyncio.sleep(0.2)\n            return 11\n\n        g = await supervisor.Supervisor.create()\n\n        t1 = g.create_task(foo1())\n        await asyncio.sleep(0.15)\n        # cancel t1 explicitly, i.e. everything should continue\n        # working as expected.\n        t1.cancel()\n\n        t2 = g.create_task(foo2())\n        await g.wait()\n\n        self.assertTrue(t1.cancelled())\n        self.assertEqual(t2.result(), 11)\n\n    async def test_supervisor_04(self):\n\n        NUM = 0\n        t2_cancel = False\n        t2 = None\n\n        async def foo1():\n            await asyncio.sleep(0.1)\n            return 1 / 0\n\n        async def foo2():\n            nonlocal NUM, t2_cancel\n            try:\n                await asyncio.sleep(1)\n            except asyncio.CancelledError:\n                t2_cancel = True\n                raise\n            NUM += 1\n\n        async def runner():\n            nonlocal NUM, t2\n\n            g = await supervisor.Supervisor.create()\n\n            g.create_task(foo1())\n            t2 = g.create_task(foo2())\n\n            await g.wait()\n\n            NUM += 10\n\n        with self.assertRaisesRegex(ExceptionGroup, r'1 sub-exception'):\n            await self.loop.create_task(runner())\n\n        self.assertEqual(NUM, 0)\n        self.assertTrue(t2_cancel)\n        self.assertTrue(t2.cancelled())\n\n    async def test_supervisor_05(self):\n\n        NUM = 0\n\n        async def foo():\n            nonlocal NUM\n            try:\n                await asyncio.sleep(5)\n            except asyncio.CancelledError:\n                NUM += 1\n                raise\n\n        async def runner():\n            g = await supervisor.Supervisor.create()\n\n            for _ in range(5):\n                g.create_task(foo())\n\n            await g.wait()\n\n        r = self.loop.create_task(runner())\n        await asyncio.sleep(0.1)\n\n        self.assertFalse(r.done())\n        r.cancel()\n        with self.assertRaises(asyncio.CancelledError):\n            await r\n\n        self.assertEqual(NUM, 5)\n\n    async def test_supervisor_06(self):\n\n        async def foo1():\n            await asyncio.sleep(1)\n            return 42\n\n        async def foo2():\n            await asyncio.sleep(2)\n            return 11\n\n        async def runner():\n            g = await supervisor.Supervisor.create()\n\n            g.create_task(foo1())\n            g.create_task(foo2())\n\n            await g.wait()\n\n        r = self.loop.create_task(runner())\n        await asyncio.sleep(0.05)\n        r.cancel()\n\n        with self.assertRaises(asyncio.CancelledError):\n            await r\n\n    async def test_supervisor_07(self):\n        NUM = 0\n\n        async def foo1():\n            nonlocal NUM\n            NUM += 1\n            try:\n                await asyncio.sleep(1)\n            except asyncio.CancelledError:\n                NUM += 10\n                await asyncio.sleep(10)\n                NUM += 1000\n                raise\n            return 42\n\n        async def foo2():\n            nonlocal NUM\n            NUM += 1\n            await asyncio.sleep(2)\n            NUM += 1000\n            return 11\n\n        async def runner():\n            g = await supervisor.Supervisor.create()\n\n            g.create_task(foo1())\n            g.create_task(foo2())\n\n            await asyncio.sleep(0.1)\n\n            await g.cancel()\n\n        r = self.loop.create_task(runner())\n        await asyncio.sleep(0.5)\n        r.cancel()\n\n        with self.assertRaises(asyncio.CancelledError):\n            await r\n\n        self.assertEqual(NUM, 12)\n\n    async def test_supervisor_08(self):\n        NUM = 0\n\n        async def foo1():\n            nonlocal NUM\n            NUM += 1\n            await asyncio.sleep(1)\n            NUM += 1000\n            return 42\n\n        async def foo2():\n            nonlocal NUM\n            NUM += 1\n            await asyncio.sleep(2)\n            NUM += 1000\n            return 11\n\n        async def runner():\n            g = await supervisor.Supervisor.create()\n\n            g.create_task(foo1())\n            g.create_task(foo2())\n\n            await asyncio.sleep(0.1)\n\n            await g.cancel()\n\n        await runner()\n        self.assertEqual(NUM, 2)\n\n    async def test_supervisor_09(self):\n        NUM = 0\n\n        async def foo1():\n            nonlocal NUM\n            NUM += 1\n            try:\n                await asyncio.sleep(1)\n            except asyncio.CancelledError:\n                await asyncio.sleep(0.2)\n                NUM += 10\n                raise\n            NUM += 1000\n            return 42\n\n        async def foo2():\n            nonlocal NUM\n            NUM += 1\n            await asyncio.sleep(2)\n            NUM += 1000\n            return 11\n\n        async def runner():\n            g = await supervisor.Supervisor.create()\n\n            g.create_task(foo1())\n            g.create_task(foo2())\n\n            await asyncio.sleep(0.1)\n\n            await g.cancel()\n\n        await runner()\n        self.assertEqual(NUM, 12)\n"
  },
  {
    "path": "tests/common/test_term.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport unittest\n\nfrom edb.common.term import Style16, Style256\n\n\nclass TermStyleTests(unittest.TestCase):\n\n    def test_common_term_style16(self):\n        s = Style16(color='red', bgcolor='green', bold=True)\n\n        assert s.color == 'red'\n        assert s.bgcolor == 'green'\n\n        assert s.bold\n        s.bold = False\n        assert not s.bold\n        s.underline = True\n        assert s.underline\n\n        s.color = 'yellow'\n        assert s.color == 'yellow'\n\n        with self.assertRaisesRegex(ValueError, 'unknown color'):\n            s.color = '#FFF'\n\n        assert not s.empty\n        assert Style16().empty\n\n    def test_common_term_style256(self):\n        assert Style256(color='red')._color == 196\n        assert Style256(color='#FF0000')._color == 196\n        assert Style256(color='#FE0000')._color == 196\n        assert Style256(color='darkmagenta')._color == 90\n\n        with self.assertRaisesRegex(ValueError, 'Unknown color'):\n            Style256(color='foooocolor')\n"
  },
  {
    "path": "tests/common/test_token_bucket.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport unittest\nimport unittest.mock\n\nfrom edb.common.token_bucket import TokenBucket\n\n\nclass ManualClock:\n    def __init__(self, value: float) -> None:\n        self.value = value\n\n    def __call__(self) -> float:\n        return self.value\n\n\nclass WindowedSumTests(unittest.TestCase):\n    def test_common_token_bucket(self) -> None:\n        monotonic = ManualClock(0)\n        with unittest.mock.patch(\"time.monotonic\", monotonic):\n            tb = TokenBucket(10, 0.1)\n            self.assertEqual(tb.consume(5), 0)\n\n            monotonic.value += 12\n            self.assertEqual(tb.consume(6), 0)\n            self.assertGreater(tb.consume(1), 0)\n            self.assertGreater(tb.consume(2), tb.consume(1))\n\n            monotonic.value += 30\n            self.assertEqual(tb.consume(2), 0)\n            self.assertEqual(tb.consume(1), 0)\n            self.assertGreater(tb.consume(1), 0)\n"
  },
  {
    "path": "tests/common/test_value_dispatch.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport unittest\n\nfrom edb.common import value_dispatch\n\n\nclass TestValueDispatch(unittest.TestCase):\n\n    def test_common_value_dispatch_01(self):\n\n        @value_dispatch.value_dispatch\n        def eat(fruit):\n            return f\"I don't want a {fruit}...\"\n\n        @eat.register('apple')\n        def _eat_apple(fruit):\n            return \"I love apples!\"\n\n        @eat.register('eggplant')\n        @eat.register('squash')\n        def _eat_what(fruit):\n            return f\"I didn't know {fruit} is a fruit!\"\n\n        self.assertEqual(eat('apple'), \"I love apples!\")\n        self.assertEqual(eat('squash'), \"I didn't know squash is a fruit!\")\n        self.assertEqual(eat('eggplant'), \"I didn't know eggplant is a fruit!\")\n        self.assertEqual(eat('banana'), \"I don't want a banana...\")\n\n    def test_common_value_dispatch_02(self):\n\n        @value_dispatch.value_dispatch\n        def eat(fruit):\n            return f\"I don't want a {fruit}...\"\n\n        @eat.register_for_all({'eggplant', 'squash'})\n        def _eat_what(fruit):\n            return f\"I didn't know {fruit} is a fruit!\"\n\n        self.assertEqual(eat('squash'), \"I didn't know squash is a fruit!\")\n        self.assertEqual(eat('eggplant'), \"I didn't know eggplant is a fruit!\")\n        self.assertEqual(eat('banana'), \"I don't want a banana...\")\n\n    def test_common_value_dispatch_03(self):\n        @value_dispatch.value_dispatch\n        def eat(fruit):\n            return f\"I don't want a {fruit}...\"\n\n        @eat.register('apple')\n        def _eat_apple(fruit):\n            return \"I love apples!\"\n\n        with self.assertRaisesRegex(\n                ValueError,\n                \"there is already a handler registered for 'apple'\"):\n            @eat.register('apple')\n            def _eat_apple_bogus(fruit):\n                return \"I love apples, do I?\"\n\n        with self.assertRaisesRegex(\n                ValueError,\n                \"there is already a handler registered for 'apple'\"):\n            @eat.register_for_all({'apple'})\n            def _eat_apple_bogus2(fruit):\n                return \"I love apples, do I?\"\n"
  },
  {
    "path": "tests/common/test_windowedsum.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nfrom __future__ import annotations\n\nimport unittest\nimport unittest.mock\n\nfrom edb.common.windowedsum import WindowedSum\n\n\nclass ManualClock:\n    def __init__(self, value: float) -> None:\n        self.value = value\n\n    def __call__(self) -> float:\n        return self.value\n\n\nclass WindowedSumTests(unittest.TestCase):\n    def test_common_windowedsum(self) -> None:\n        monotonic = ManualClock(0)\n        with unittest.mock.patch(\"time.monotonic\", monotonic):\n            s = WindowedSum()\n            s += 1\n            self.assertEqual(1, int(s))\n            monotonic.value += 60\n            self.assertEqual(0, int(s))\n            s += 1\n            s += 2\n            s += 3\n            self.assertEqual(6, int(s))\n            monotonic.value += 59\n            self.assertEqual(6, int(s))\n            s += 4\n            self.assertEqual(10, int(s))\n            monotonic.value += 1.5\n            self.assertEqual(4, int(s))\n            monotonic.value += 1.5\n            s += 5\n            self.assertEqual(9, int(s))\n            monotonic.value += 1.5\n            s += 6\n            self.assertEqual(15, int(s))\n"
  },
  {
    "path": "tests/common/test_xdedent.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2011-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport textwrap\nimport unittest\n\nfrom edb.common import xdedent\n\nX = xdedent.escape\n\n\nEXPECTED_1 = '''\ncall_something()\nfoo = 10\nwhile True:\n    if bar:\n        do_something(\n            foo,\n            bar,\n            [1, 2, 3,\n             4, 5, 6],\n            reify(\n                spam\n            ),\n            reify(\n                eggs\n            ),\n            reify(\n                ham\n            )\n        )\n        another\nmore\n'''.strip('\\n')\n\nEXPECTED_2 = '''\ncall_something()\nfoo = 10\nwhile True:\n    if bar:\n        another\nmore\n'''.strip('\\n')\n\n\nclass XDedentTests(unittest.TestCase):\n    def _test1(self, do_something=True):\n        foo = 'foo'\n        bar = 'bar'\n\n        left = '''\n            [1, 2, 3,\n             4, 5, 6]\n        '''\n\n        things = []\n        for thing in ['spam', 'eggs', 'ham']:\n            things.append(f'''\n                reify(\n                    {thing}\n                )\n            ''')\n\n        sep = \",\\n\"\n        if do_something:\n            orig = f'''\n                do_something(\n                    {foo},\n                    {bar},\n                    {X(left)},\n                    {X(sep.join(X(x) for x in things))}\n                )\n            '''\n        else:\n            orig = xdedent.LINE_BLANK\n\n        return xdedent.xdedent(f'''\n            call_something()\n            {X(foo)} = 10\n            while True:\n                if {bar}:\n                    {X(orig)}\n                    another\n            more\n        ''')\n\n    def test_xdedent_1(self):\n        self.assertEqual(self._test1(True), EXPECTED_1)\n        self.assertEqual(self._test1(False), EXPECTED_2)\n\n    def test_xdedent_2(self):\n        EXPECTED = textwrap.dedent('''\\\n        LATERAL (SELECT\n                    ARRAY[(q0.val)->>'name'] AS key\n                ) AS k0,\n        LATERAL (SELECT\n                    ARRAY[(q0.val)->>'name'] AS key\n                ) AS k0''')\n\n        q = '''\n        (SELECT\n            ARRAY[(q0.val)->>'name'] AS key\n        ) AS k0\n        '''\n        sources = [q, q]\n        fromlist = ',\\n'.join(f'LATERAL {X(s)}' for s in sources)\n        res = xdedent.xdedent(fromlist)\n        self.assertEqual(res, EXPECTED)\n"
  },
  {
    "path": "tests/dumps/dumpv3/.gitignore",
    "content": ""
  },
  {
    "path": "tests/dumps/dumpv4/.gitignore",
    "content": ""
  },
  {
    "path": "tests/dumps/dumpv5/.gitignore",
    "content": ""
  },
  {
    "path": "tests/dumps/dumpv6/.gitignore",
    "content": ""
  },
  {
    "path": "tests/dumps/dumpv7/.gitignore",
    "content": ""
  },
  {
    "path": "tests/edgeql/__init__.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport unittest\n\n\ndef suite():\n    test_loader = unittest.TestLoader()\n    test_suite = test_loader.discover('.', pattern='test_*.py')\n    return test_suite\n"
  },
  {
    "path": "tests/edgeql/test_quote.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport unittest\n\nimport edb.edgeql.quote as qlquote\n\n\nclass QuoteTests(unittest.TestCase):\n\n    def test_quote_string(self):\n        self.assertEqual(qlquote.quote_literal(\"\"), \"''\"),\n        self.assertEqual(qlquote.quote_literal(\"abc\"), \"'abc'\")\n        self.assertEqual(qlquote.quote_literal(\"\\\"\"), \"'\\\"'\")\n        self.assertEqual(qlquote.quote_literal(\"\\b\"), \"'\\\\b'\")\n        self.assertEqual(qlquote.quote_literal(\"\\f\"), \"'\\\\f'\")\n        self.assertEqual(qlquote.quote_literal(\"\\n\"), \"'\\\\n'\")\n        self.assertEqual(qlquote.quote_literal(\"\\r\"), \"'\\\\r'\")\n        self.assertEqual(qlquote.quote_literal(\"\\t\"), \"'\\\\t'\")\n        self.assertEqual(qlquote.quote_literal(\"\\'\"), \"'\\\\\\''\")\n        self.assertEqual(qlquote.quote_literal(\"\\\\\"), \"'\\\\\\\\'\")\n        self.assertEqual(qlquote.quote_literal(\"\\\\b\"), \"'\\\\\\\\b'\")\n        self.assertEqual(qlquote.quote_literal(\"\\\\f\"), \"'\\\\\\\\f'\")\n        self.assertEqual(qlquote.quote_literal(\"\\\\n\"), \"'\\\\\\\\n'\")\n        self.assertEqual(qlquote.quote_literal(\"\\\\r\"), \"'\\\\\\\\r'\")\n        self.assertEqual(qlquote.quote_literal(\"\\\\t\"), \"'\\\\\\\\t'\")\n        self.assertEqual(qlquote.quote_literal(\"\\\\\\'\"), \"'\\\\\\\\\\\\\\''\")\n        self.assertEqual(qlquote.quote_literal(\"\\\\\\\\\"), \"'\\\\\\\\\\\\\\\\'\")\n        self.assertEqual(qlquote.quote_literal(\n            \"abc\\\"efg\\nhij\\'klm\\\\nop\"),\n            \"'abc\\\"efg\\\\nhij\\\\\\'klm\\\\\\\\nop'\")\n"
  },
  {
    "path": "tests/extension-testing/.gitignore",
    "content": "build/\next_test*.zip\n"
  },
  {
    "path": "tests/extension-testing/ext_test/MANIFEST.toml",
    "content": "name = \"ext_test\"\nversion = \"0.1\"\nfiles = [\"get_sum.edgeql\"]\n"
  },
  {
    "path": "tests/extension-testing/ext_test/Makefile",
    "content": "# Configurable parts\nSQL_MODULE := sql\n\n### Boilerplate\nPYTHON := python3\nEDB := $(PYTHON) -m edb.tools $(EDBFLAGS)\nMKS := $(shell $(EDB) config --make-include)\ninclude $(MKS)\n### End Boilerplate\n"
  },
  {
    "path": "tests/extension-testing/ext_test/get_sum.edgeql",
    "content": "create extension package ext_test VERSION '0.1' {\n  set ext_module := \"ext::ext_test\";\n  set sql_extensions := [\"get_sum\"];\n\n  create module ext::ext_test;\n  create function ext::ext_test::get_sum(x: std::int32, y: std::int32) -> std::int32 {\n    USING SQL $$\n      select get_sum(x, y)\n    $$\n  };\n};\n"
  },
  {
    "path": "tests/extension-testing/ext_test/sql/Makefile",
    "content": "MODULES = get_sum\nEXTENSION = get_sum\nDATA = get_sum--0.0.1.sql\nREGRESS = get_sum_test\n\nPG_CONFIG = pg_config\nPGXS := $(shell $(PG_CONFIG) --pgxs)\ninclude $(PGXS)\n"
  },
  {
    "path": "tests/extension-testing/ext_test/sql/get_sum--0.0.1.sql",
    "content": "-- Via http://web.archive.org/web/20240216072136/https://www.highgo.ca/2020/01/10/how-to-create-test-and-debug-an-extension-written-in-c-for-postgresql/\n\n--complain if script is sourced in psql, rather than via CREATE EXTENSION\n\\echo Use \"CREATE EXTENSION get_sum\" to load this file. \\quit\n\nCREATE OR REPLACE FUNCTION get_sum(int, int) RETURNS int\nAS '$libdir/get_sum'\nLANGUAGE C IMMUTABLE STRICT;\n"
  },
  {
    "path": "tests/extension-testing/ext_test/sql/get_sum.c",
    "content": "// Via http://web.archive.org/web/20240216072136/https://www.highgo.ca/2020/01/10/how-to-create-test-and-debug-an-extension-written-in-c-for-postgresql/\n#include \"postgres.h\"\n#include \"fmgr.h\"\n\nPG_MODULE_MAGIC;\n\nPG_FUNCTION_INFO_V1(get_sum);\n\nDatum\nget_sum(PG_FUNCTION_ARGS)\n{\n    bool isnull, isnull2;\n\n    isnull = PG_ARGISNULL(0);\n    isnull2 = PG_ARGISNULL(1);\n    if (isnull || isnull2)\n      ereport( ERROR,\n               ( errcode(ERRCODE_FEATURE_NOT_SUPPORTED),\n               errmsg(\"two and only two integer values are required as input\")));\n\n    int32 a = PG_GETARG_INT32(0);\n    int32 b = PG_GETARG_INT32(1);\n    int32 sum;\n\n    sum = a + b;\n\n    PG_RETURN_INT32(sum);\n}\n"
  },
  {
    "path": "tests/extension-testing/ext_test/sql/get_sum.control",
    "content": "# Via http://web.archive.org/web/20240216072136/https://www.highgo.ca/2020/01/10/how-to-create-test-and-debug-an-extension-written-in-c-for-postgresql/\n# get_sum postgresql extension\ncomment = 'simple sum of two integers for postgres extension using c'\ndefault_version = '0.0.1'\nmodule_pathname = '$libdir/get_sum'\nrelocatable = false\n"
  },
  {
    "path": "tests/extension-testing/exts.mk",
    "content": "PYTHON ?= python3\nEXT_NAME := $(shell $(PYTHON) -c 'import tomllib; f = tomllib.load(open(\"MANIFEST.toml\", \"rb\")); print(f[\"name\"])')\nEXT_VERSION := $(shell $(PYTHON) -c 'import tomllib; f = tomllib.load(open(\"MANIFEST.toml\", \"rb\")); print(f[\"version\"])')\nEDGEQL_SRCS := $(shell $(PYTHON) -c 'import tomllib; f = tomllib.load(open(\"MANIFEST.toml\", \"rb\")); print(\" \".join(f[\"files\"]))')\nEXT_FNAME := $(EXT_NAME)--$(EXT_VERSION)\n\n.DEFAULT_GOAL := build\n.PHONY: build clean install zip\n\n#\n\nWITH_SQL ?= yes\nWITH_EDGEQL ?= yes\n\nrwildcard=$(foreach d,$(wildcard $(1:=/*)),$(call rwildcard,$d,$2) $(filter $(subst *,%,$2),$d))\nSQL_DEPS := $(call rwildcard,$(SQL_MODULE),*.c *.h *.sql *.control Makefile)\n\nSQL_BUILD_STAMP := build/.sql_build_stamp\nSQL_INSTALL_STAMP := build/.sql_install_stamp\nEDGEQL_INSTALL_STAMP := build/.edgeql_install_stamp\nINSTALLABLES := MANIFEST.toml\n\nifeq ($(strip $(WITH_SQL)),yes)\n\nINSTALLABLES += lib/ share/\n\nifeq ($(origin PG_CONFIG), undefined)\nEDB := $(PYTHON) -m edb.tools $(EDBFLAGS)\nPG_CONFIG := $(shell $(EDB) config --pg-config)\n\nifeq ($(PG_CONFIG),)\n$(error cannot find pg_config, please set PG_CONFIG explicitly)\nendif\nendif\n\nifneq ($(strip $(CUSTOM_SQL_BUILD)),1)\n\n$(SQL_BUILD_STAMP): MANIFEST.toml $(SQL_DEPS) $(EXTRA_DEPS) Makefile\n\tenv PG_CONFIG=$(PG_CONFIG) $(MAKE) -C $(SQL_MODULE) DESTDIR=$(PWD)/build/out PG_CONFIG=$(PG_CONFIG) install\n\ttouch $(SQL_BUILD_STAMP)\nendif\n\n$(SQL_INSTALL_STAMP): $(SQL_BUILD_STAMP)\n\trm -rf build/$(EXT_FNAME)/lib\n\tmkdir -p build/$(EXT_FNAME)/lib\n\trm -rf build/$(EXT_FNAME)/share/postgresql\n\tmkdir -p build/$(EXT_FNAME)/share/postgresql\n\tmkdir -p $(PWD)/build/out/$(shell $(PG_CONFIG) --pkglibdir)\n\tcp -r $(PWD)/build/out/$(shell $(PG_CONFIG) --pkglibdir) build/$(EXT_FNAME)/lib/postgresql\n\tmkdir -p $(PWD)/build/out/$(shell $(PG_CONFIG) --sharedir)/contrib\n\tmkdir -p $(PWD)/build/out/$(shell $(PG_CONFIG) --sharedir)/extension\n\tcp -r $(PWD)/build/out/$(shell $(PG_CONFIG) --sharedir)/contrib build/$(EXT_FNAME)/share/postgresql/contrib\n\tcp -r $(PWD)/build/out/$(shell $(PG_CONFIG) --sharedir)/extension build/$(EXT_FNAME)/share/postgresql/extension\n\ttouch $(SQL_INSTALL_STAMP)\nelse\n$(SQL_INSTALL_STAMP):\n\tmkdir -p build\n\ttouch $(SQL_INSTALL_STAMP)\nendif\nifeq ($(strip $(WITH_EDGEQL)),yes)\nINSTALLABLES += $(EDGEQL_SRCS) $(EXTRA_FILES)\n\n$(EDGEQL_INSTALL_STAMP): MANIFEST.toml $(EDGEQL_SRCS) $(EXTRA_FILES) Makefile\n\tmkdir -p build/$(EXT_FNAME)\n\tcp $(EDGEQL_SRCS) build/$(EXT_FNAME)\n\tif [ -n \"$(EXTRA_FILES)\" ]; then cp $(EXTRA_FILES) build/$(EXT_FNAME); fi\n\tcp MANIFEST.toml build/$(EXT_FNAME)\n\ttouch $(EDGEQL_INSTALL_STAMP)\nelse\nEDGEQL:\n\tmkdir -p build\n\ttouch $(EDGEQL_INSTALL_STAMP)\nendif\n\nbuild: $(EDGEQL_INSTALL_STAMP) $(SQL_INSTALL_STAMP)\n\ninstall: build\n\tif [ -z \"$(DESTDIR)\" ]; then echo \"DESTDIR must be set\" >&2; exit 1; fi\n\tmkdir -p \"$(DESTDIR)\"\n\tcd \"build/$(EXT_FNAME)\" && cp -r $(INSTALLABLES) \"${DESTDIR}/\"\n\n$(EXT_FNAME).zip: build\n\trm -f $(EXT_FNAME).zip\n\tcd build/ && zip -r ../$(EXT_FNAME).zip $(EXT_FNAME)/\n\nzip: $(EXT_FNAME).zip\n\nclean:\n\trm -rf build $(EXT_FNAME).zip\n"
  },
  {
    "path": "tests/inplace-testing/prep-upgrades.py",
    "content": "#!/usr/bin/env python3\n\nimport edgedb\nimport json\nimport sys\n\n\ndef main(argv):\n    con = edgedb.create_client()\n\n    dbs = con.query('''\n        select sys::Database.name\n    ''')\n\n    con.close()\n\n    datas = {}\n    for db in dbs:\n        con = edgedb.create_client(database=db)\n        output = json.loads(con.query_single('''\n            administer prepare_upgrade()\n        '''))\n        datas[db] = output\n\n    print(json.dumps(datas))\n\n\nif __name__ == '__main__':\n    sys.exit(main(sys.argv))\n"
  },
  {
    "path": "tests/inplace-testing/test-old.sh",
    "content": "#!/bin/bash -ex\n\nwhile [[ $# -gt 0 ]]; do\n  case $1 in\n    --rollback-and-test)\n        ROLLBACK=1\n        shift\n        ;;\n    --rollback-and-reapply)\n        REAPPLY=1\n        shift\n        ;;\n    --save-tarballs)\n        SAVE_TARBALLS=1\n        shift\n        ;;\n    *)\n        break\n        ;;\n  esac\ndone\n\n\nDIR=\"$1\"\nRDIR=$(realpath \"$1\")\nSERVER_INSTALL=$(realpath \"$2\")\nshift 2\n\n# Force bootstrapping of the server\nTMPDIR=$(mktemp -d)\nedb server --bootstrap-only --data-dir $TMPDIR/bootstrap || (rm -rf $TMPDIR && false)\n\n# Setup the test database\n(cd / && GEL_SERVER_SECURITY=insecure_dev_mode $SERVER_INSTALL/bin/python3  -m edb.tools --no-devmode inittestdb --tests-dir $SERVER_INSTALL/share/tests -k test_dump --data-dir \"$RDIR\")\n\n\nif [ \"$SAVE_TARBALLS\" = 1 ]; then\n    tar cf \"$DIR\".tar \"$DIR\"\nfi\n\n\nPORT=$(( $RANDOM + 2000 ))\nGEL_SERVER_SECURITY=insecure_dev_mode $SERVER_INSTALL/bin/gel-server --testmode -D \"$RDIR\" -P $PORT &\nSPID=$!\nstop_server() {\n    kill $SPID\n    wait $SPID\n    SPID=\n}\ncleanup() {\n    if [ -n \"$SPID\" ]; then\n        stop_server\n    fi\n}\ntrap cleanup EXIT\n\nGEL=\"gel -H localhost -P $PORT --tls-security insecure --wait-until-available 120sec\"\n\n# Wait for the server to come up and see it is working\n$GEL -b dump01 query 'select count(Z)' | grep 2\n\n# Block DDL\n$GEL query 'configure instance set force_database_error := $${\"type\": \"AvailabilityError\", \"message\": \"DDL is disabled due to in-place upgrade.\", \"_scopes\": [\"ddl\"]}$$;'\n\nif $GEL query 'create empty branch asdf'; then\n    echo Unexpected DDL success despite blocking it\n    exit 4\nfi\n\n# Prepare the upgrades\nEDGEDB_PORT=$PORT EDGEDB_CLIENT_TLS_SECURITY=insecure python3 tests/inplace-testing/prep-upgrades.py > \"${DIR}/upgrade.json\"\n\nif [ \"$SAVE_TARBALLS\" = 1 ]; then\n    tar cf \"$DIR\"-ready.tar \"$DIR\"\nfi\n\n# Get the DSN from the debug endpoint\nDSN=$(curl -s http://localhost:$PORT/server-info | jq -r '.pg_addr.dsn')\n\n# Prepare the upgrade, operating against the postgres that the old\n# version server is managing\nedb server --inplace-upgrade-prepare \"$DIR\"/upgrade.json --backend-dsn=\"$DSN\"\n\n# Check the server is still working\n$GEL -b dump01 query 'select count(Z)' | grep 2\n\nif [ \"$ROLLBACK\" = 1 ]; then\n    # Inject a failure into our first attempt to rollback\n    if EDGEDB_UPGRADE_ROLLBACK_ERROR_INJECTION=dumpbasics edb server --inplace-upgrade-rollback --backend-dsn=\"$DSN\"; then\n        echo Unexpected rollback success despite failure injection\n        exit 4\n    fi\n\n    # Second try should work\n    edb server --inplace-upgrade-rollback --backend-dsn=\"$DSN\"\n    $GEL query 'configure instance reset force_database_error'\n\n    # XXX: what can we do here???\n    stop_server\n    # patch -R -f -p1 < tests/inplace-testing/upgrade.patch\n    # make parsers\n    # edb test --data-dir \"$DIR\" --use-data-dir-dbs -v \"$@\"\n    exit 0\nfi\n\nif [ \"$REAPPLY\" = 1 ]; then\n    # Rollback and then reapply\n    edb server --inplace-upgrade-rollback --backend-dsn=\"$DSN\"\n\n    edb server --inplace-upgrade-prepare \"$DIR\"/upgrade.json --backend-dsn=\"$DSN\"\nfi\n\n# Check the server is still working\n$GEL -b dump01 query 'select count(Z)' | grep 2\n\n# Kill the old version so we can finalize the upgrade\nstop_server\n\nif [ \"$SAVE_TARBALLS\" = 1 ]; then\n    tar cf \"$DIR\"-prepped.tar \"$DIR\"\nfi\n\n# Try to finalize the upgrade, but inject a failure\nif EDGEDB_UPGRADE_FINALIZE_ERROR_INJECTION=dumpbasics edb server --inplace-upgrade-finalize --data-dir \"$DIR\"; then\n    echo Unexpected upgrade success despite failure injection\n    exit 4\nfi\n\n# Try doing a rollback. It should fail, because of the partially\n# succesful finalization.\nif edb server --inplace-upgrade-rollback --data-dir \"$DIR\"; then\n    echo Unexpected upgrade success\n    exit 5\nfi\n\n# Finalize the upgrade\nedb server --inplace-upgrade-finalize --data-dir \"$DIR\"\nif [ \"$SAVE_TARBALLS\" = 1 ]; then\n    tar cf \"$DIR\"-cooked.tar \"$DIR\"\nfi\n\n# Start the server again so we can reenable DDL\nedb server -D \"$DIR\" -P $PORT &\nSPID=$!\nif $GEL query 'create empty branch asdf'; then\n    echo Unexpected DDL success despite blocking it\n    exit 6\nfi\n$GEL query 'configure instance reset force_database_error'\nstop_server\nif [ \"$SAVE_TARBALLS\" = 1 ]; then\n    tar cf \"$DIR\"-cooked2.tar \"$DIR\"\nfi\n\n\n# Test!\nedb test --data-dir \"$DIR\" --use-data-dir-dbs -v \"$@\" -k test_dump\n"
  },
  {
    "path": "tests/inplace-testing/test.sh",
    "content": "#!/bin/bash -ex\n\nwhile [[ $# -gt 0 ]]; do\n  case $1 in\n    --rollback-and-test)\n        ROLLBACK=1\n        shift\n        ;;\n    --rollback-and-reapply)\n        REAPPLY=1\n        shift\n        ;;\n    --save-tarballs)\n        SAVE_TARBALLS=1\n        shift\n        ;;\n    *)\n        break\n        ;;\n  esac\ndone\n\n\nDIR=\"$1\"\nshift\n\nif ! git diff-index --quiet HEAD --; then\n    set +x\n    echo Refusing to run in-place upgrade test with dirty git state.\n    echo \"(The test makes local modifications.)\"\n    exit 1\nfi\n\nmake parsers\n\n# Setup the test database\nedb inittestdb -D \"$DIR\" \"$@\"\n\n\nif [ \"$SAVE_TARBALLS\" = 1 ]; then\n    tar cf \"$DIR\".tar \"$DIR\"\nfi\n\n\nPORT=$(( $RANDOM + 2000 ))\nedb server -D \"$DIR\" -P $PORT &\nSPID=$!\nstop_server() {\n    kill $SPID\n    wait $SPID\n    SPID=\n}\ncleanup() {\n    if [ -n \"$SPID\" ]; then\n        stop_server\n    fi\n}\ntrap cleanup EXIT\n\nGEL=\"gel -H localhost -P $PORT --tls-security insecure --wait-until-available 120sec\"\n\n# Wait for the server to come up and see it is working\n$GEL -b select query 'select count(User)' | grep 2\n\n# Block DDL\n$GEL query 'configure instance set force_database_error := $${\"type\": \"AvailabilityError\", \"message\": \"DDL is disabled due to in-place upgrade.\", \"_scopes\": [\"ddl\"]}$$;'\n\nif $GEL query 'create empty branch asdf'; then\n    echo Unexpected DDL success despite blocking it\n    exit 4\nfi\n\n# Prepare the upgrades\nEDGEDB_PORT=$PORT EDGEDB_CLIENT_TLS_SECURITY=insecure python3 tests/inplace-testing/prep-upgrades.py > \"${DIR}/upgrade.json\"\n\n# Upgrade to the new version\npatch -f -p1 < tests/inplace-testing/upgrade.patch\nmake parsers\n# Force bootstrapping of the server\nTMPDIR=$(mktemp -d)\nedb server --bootstrap-only --data-dir $TMPDIR/bootstrap || (rm -rf $TMPDIR && false)\n\n# Get the DSN from the debug endpoint\nDSN=$(curl -s http://localhost:$PORT/server-info | jq -r '.pg_addr.dsn')\n\n# Prepare the upgrade, operating against the postgres that the old\n# version server is managing\nedb server --inplace-upgrade-prepare \"$DIR\"/upgrade.json --backend-dsn=\"$DSN\"\n\n# Check the server is still working\n$GEL -b select query 'select count(User)' | grep 2\n\nif [ \"$ROLLBACK\" = 1 ]; then\n    # Inject a failure into our first attempt to rollback\n    if EDGEDB_UPGRADE_ROLLBACK_ERROR_INJECTION=main edb server --inplace-upgrade-rollback --backend-dsn=\"$DSN\"; then\n        echo Unexpected rollback success despite failure injection\n        exit 4\n    fi\n\n    # Second try should work\n    edb server --inplace-upgrade-rollback --backend-dsn=\"$DSN\"\n    $GEL query 'configure instance reset force_database_error'\n\n    # Rollback and then run the tests on the old database\n    stop_server\n    patch -R -f -p1 < tests/inplace-testing/upgrade.patch\n    make parsers\n    TMPDIR=$(mktemp -d)\n    edb server --bootstrap-only --data-dir $TMPDIR/bootstrap || (rm -rf $TMPDIR && false)\n\n    edb test --data-dir \"$DIR\" --use-data-dir-dbs -v \"$@\"\n    exit 0\nfi\n\nif [ \"$REAPPLY\" = 1 ]; then\n    # Rollback and then reapply\n    edb server --inplace-upgrade-rollback --backend-dsn=\"$DSN\"\n\n    edb server --inplace-upgrade-prepare \"$DIR\"/upgrade.json --backend-dsn=\"$DSN\"\nfi\n\n# Check the server is still working\n$GEL -b select query 'select count(User)' | grep 2\n\n# Kill the old version so we can finalize the upgrade\nstop_server\n\nif [ \"$SAVE_TARBALLS\" = 1 ]; then\n    tar cf \"$DIR\"-prepped.tar \"$DIR\"\nfi\n\n# Try to finalize the upgrade, but inject a failure\nif EDGEDB_UPGRADE_FINALIZE_ERROR_INJECTION=main edb server --inplace-upgrade-finalize --data-dir \"$DIR\"; then\n    echo Unexpected upgrade success despite failure injection\n    exit 4\nfi\n\n# Try doing a rollback. It should fail, because of the partially\n# succesful finalization.\nif edb server --inplace-upgrade-rollback --data-dir \"$DIR\"; then\n    echo Unexpected upgrade success\n    exit 5\nfi\n\n# Finalize the upgrade\nedb server --inplace-upgrade-finalize --data-dir \"$DIR\"\nif [ \"$SAVE_TARBALLS\" = 1 ]; then\n    tar cf \"$DIR\"-cooked.tar \"$DIR\"\nfi\n\n# Start the server again so we can reenable DDL\nedb server -D \"$DIR\" -P $PORT &\nSPID=$!\nif $GEL query 'create empty branch asdf'; then\n    echo Unexpected DDL success despite blocking it\n    exit 6\nfi\n$GEL query 'configure instance reset force_database_error'\nstop_server\nif [ \"$SAVE_TARBALLS\" = 1 ]; then\n    tar cf \"$DIR\"-cooked2.tar \"$DIR\"\nfi\n\n\n# Test!\nedb test --data-dir \"$DIR\" --use-data-dir-dbs -v \"$@\"\n"
  },
  {
    "path": "tests/inplace-testing/upgrade.patch",
    "content": "diff --git a/edb/buildmeta.py b/edb/buildmeta.py\nindex b6222e4a2..5f1b366fb 100644\n--- a/edb/buildmeta.py\n+++ b/edb/buildmeta.py\n@@ -65,6 +65,12 @@ class MetadataError(Exception):\n     pass\n \n \n+# HACK: Put this down here so it overrides the above version without\n+# merge conflicting with them.\n+EDGEDB_CATALOG_VERSION = 2030_01_01_00_00\n+EDGEDB_MAJOR_VERSION = 1000\n+\n+\n class BackendVersion(NamedTuple):\n     major: int\n     minor: int\ndiff --git a/edb/edgeql/ast.py b/edb/edgeql/ast.py\nindex 59973c0ec..003700c76 100644\n--- a/edb/edgeql/ast.py\n+++ b/edb/edgeql/ast.py\n@@ -1183,6 +1183,23 @@ class DropPermission(DropObject, PermissionCommand):\n     pass\n \n \n+class BlobalCommand(ObjectDDL):\n+\n+    __abstract_node__ = True\n+\n+\n+class CreateBlobal(CreateObject, BlobalCommand):\n+    pass\n+\n+\n+class AlterBlobal(AlterObject, BlobalCommand):\n+    pass\n+\n+\n+class DropBlobal(DropObject, BlobalCommand):\n+    pass\n+\n+\n class LinkCommand(ObjectDDL):\n \n     __abstract_node__ = True\ndiff --git a/edb/edgeql/codegen.py b/edb/edgeql/codegen.py\nindex 2dffdd519..e05a4108d 100644\n--- a/edb/edgeql/codegen.py\n+++ b/edb/edgeql/codegen.py\n@@ -2474,6 +2474,12 @@ class EdgeQLSourceGenerator(codegen.SourceGenerator):\n     def visit_DropPermission(self, node: qlast.DropPermission) -> None:\n         self._visit_DropObject(node, 'PERMISSION')\n \n+    def visit_CreateBlobal(self, node: qlast.CreateGlobal) -> None:\n+        self._visit_CreateObject(node, 'BLOBAL')\n+\n+    def visit_DropBlobal(self, node: qlast.DropGlobal) -> None:\n+        self._visit_DropObject(node, 'BLOBAL')\n+\n     def visit_ConfigSet(self, node: qlast.ConfigSet) -> None:\n         if node.scope == qltypes.ConfigScope.GLOBAL:\n             self._write_keywords('SET GLOBAL ')\ndiff --git a/edb/edgeql/parser/grammar/ddl.py b/edb/edgeql/parser/grammar/ddl.py\nindex bd55c93af..22fc58bf6 100644\n--- a/edb/edgeql/parser/grammar/ddl.py\n+++ b/edb/edgeql/parser/grammar/ddl.py\n@@ -260,6 +260,14 @@ class InnerDDLStmt(Nonterm):\n     def reduce_DropPermissionStmt(self, *_):\n         pass\n \n+    @parsing.inline(0)\n+    def reduce_CreateBlobalStmt(self, *_):\n+        pass\n+\n+    @parsing.inline(0)\n+    def reduce_DropBlobalStmt(self, *_):\n+        pass\n+\n     @parsing.inline(0)\n     def reduce_DropCastStmt(self, *_):\n         pass\n@@ -3613,6 +3621,38 @@ class DropGlobalStmt(Nonterm):\n         )\n \n \n+#\n+# CREATE BLOBAL\n+#\n+\n+\n+commands_block(\n+    'CreateBlobal',\n+    SetFieldStmt,\n+    CreateAnnotationValueStmt,\n+)\n+\n+\n+class CreateBlobalStmt(Nonterm):\n+    def reduce_CreateBlobal(self, *kids):\n+        \"\"\"%reduce\n+            CREATE BLOBAL NodeName\n+            OptCreateBlobalCommandsBlock\n+        \"\"\"\n+        self.val = qlast.CreateBlobal(\n+            name=kids[2].val,\n+            commands=kids[3].val,\n+        )\n+\n+\n+class DropBlobalStmt(Nonterm):\n+    def reduce_DropBlobal(self, *kids):\n+        r\"\"\"%reduce DROP BLOBAL NodeName\"\"\"\n+        self.val = qlast.DropBlobal(\n+            name=kids[2].val\n+        )\n+\n+\n #\n # CREATE PERMISSION\n #\ndiff --git a/edb/lib/_testmode.edgeql b/edb/lib/_testmode.edgeql\nindex 761a5dc53..d9a70f541 100644\n--- a/edb/lib/_testmode.edgeql\n+++ b/edb/lib/_testmode.edgeql\n@@ -232,6 +232,15 @@ create extension package _conf VERSION '1.0' {\n \n # std::_gen_series\n \n+CREATE FUNCTION\n+std::_upgrade_test(\n+) -> std::str\n+{\n+    SET volatility := 'Immutable';\n+    USING ('asdf');\n+};\n+\n+\n CREATE FUNCTION\n std::_gen_series(\n     `start`: std::int64,\ndiff --git a/edb/lib/schema.edgeql b/edb/lib/schema.edgeql\nindex a41bbba6e..f8f1b1ccb 100644\n--- a/edb/lib/schema.edgeql\n+++ b/edb/lib/schema.edgeql\n@@ -531,9 +531,17 @@ CREATE TYPE schema::Permission\n         schema::AnnotationSubject;\n \n \n+CREATE TYPE schema::Blobal EXTENDING schema::AnnotationSubject {\n+    CREATE PROPERTY required -> std::bool;\n+};\n+\n+\n CREATE TYPE schema::Function\n     EXTENDING schema::CallableObject, schema::VolatilitySubject\n {\n+    CREATE PROPERTY test_field_a -> std::str;\n+    CREATE PROPERTY test_nativecode_size -> std::int64;\n+\n     CREATE PROPERTY preserves_optionality -> std::bool {\n         SET default := false;\n     };\ndiff --git a/edb/pgsql/delta.py b/edb/pgsql/delta.py\nindex 3a9effaa5..145a472cd 100644\n--- a/edb/pgsql/delta.py\n+++ b/edb/pgsql/delta.py\n@@ -777,6 +777,38 @@ class RenamePermission(\n     pass\n \n \n+class BlobalCommand(MetaCommand):\n+    pass\n+\n+\n+class CreateBlobal(\n+    BlobalCommand,\n+    adapts=s_globals.CreateBlobal,\n+):\n+    pass\n+\n+\n+class RenameBlobal(\n+    BlobalCommand,\n+    adapts=s_globals.RenameBlobal,\n+):\n+    pass\n+\n+\n+class AlterBlobal(\n+    BlobalCommand,\n+    adapts=s_globals.AlterBlobal,\n+):\n+    pass\n+\n+\n+class DeleteBlobal(\n+    BlobalCommand,\n+    adapts=s_globals.DeleteBlobal,\n+):\n+    pass\n+\n+\n class AccessPolicyCommand(MetaCommand):\n     pass\n \ndiff --git a/edb/schema/functions.py b/edb/schema/functions.py\nindex 48baa30be..48fb55b89 100644\n--- a/edb/schema/functions.py\n+++ b/edb/schema/functions.py\n@@ -1252,6 +1252,27 @@ class Function(\n     data_safe=True,\n ):\n \n+    ##\n+    test_field_a = so.SchemaField(\n+        str,\n+        default=None,\n+        compcoef=0.4,\n+        allow_ddl_set=True,\n+    )\n+\n+    test_field_b = so.SchemaField(\n+        str,\n+        default=None,\n+        compcoef=0.4,\n+        allow_ddl_set=True,\n+    )\n+\n+    test_nativecode_size = so.SchemaField(\n+        int,\n+        default=None,\n+    )\n+    ##\n+\n     used_globals = so.SchemaField(\n         so.ObjectSet[s_globals.Global],\n         coerce=True, default=so.DEFAULT_CONSTRUCTOR,\n@@ -1655,6 +1676,10 @@ class FunctionCommand(\n                 nativecode.not_compiled()\n             )\n \n+        if self.has_attribute_value('nativecode'):\n+            code = self.get_attribute_value('nativecode')\n+            self.set_attribute_value('test_nativecode_size', len(code.text))\n+\n         # Resolving 'nativecode' has side effects on has_dml and\n         # volatility, so force it to happen as part of\n         # canonicalization of attributes.\ndiff --git a/edb/schema/globals.py b/edb/schema/globals.py\nindex 0bac4f113..9754274cd 100644\n--- a/edb/schema/globals.py\n+++ b/edb/schema/globals.py\n@@ -637,3 +637,60 @@ class DeleteGlobal(\n                 self.add_caused(op)\n \n         return schema\n+\n+\n+class Blobal(\n+    so.QualifiedObject,\n+    s_anno.AnnotationSubject,\n+    qlkind=qltypes.SchemaObjectClass.GLOBAL,\n+    data_safe=True,\n+):\n+\n+    required = so.SchemaField(\n+        bool,\n+        default=False,\n+        compcoef=0.909,\n+        allow_ddl_set=True,\n+    )\n+\n+\n+class BlobalCommandContext(\n+    sd.ObjectCommandContext[so.Object],\n+    s_anno.AnnotationSubjectCommandContext\n+):\n+    pass\n+\n+\n+class BlobalCommand(\n+    sd.QualifiedObjectCommand[Blobal],\n+    context_class=BlobalCommandContext,\n+):\n+    pass\n+\n+\n+class CreateBlobal(\n+    sd.CreateObject[Blobal],\n+    BlobalCommand,\n+):\n+    astnode = qlast.CreateBlobal\n+\n+\n+class RenameBlobal(\n+    sd.RenameObject[Blobal],\n+    BlobalCommand,\n+):\n+    pass\n+\n+\n+class AlterBlobal(\n+    sd.AlterObject[Blobal],\n+    BlobalCommand,\n+):\n+    astnode = qlast.AlterBlobal\n+\n+\n+class DeleteBlobal(\n+    sd.DeleteObject[Blobal],\n+    BlobalCommand,\n+):\n+    astnode = qlast.DropBlobal\ndiff --git a/edb/schema/operators.py b/edb/schema/operators.py\nindex baba8c1dd..371d59240 100644\n--- a/edb/schema/operators.py\n+++ b/edb/schema/operators.py\n@@ -68,12 +68,6 @@ class Operator(\n     code = so.SchemaField(\n         str, default=None, compcoef=0.4)\n \n-    # An unused dummy field. We have this here to make it easier to\n-    # test the *removal* of internal schema fields during in-place\n-    # upgrades.\n-    _dummy_field = so.SchemaField(\n-        str, default=None)\n-\n     # If this is a derivative operator, *derivative_of* would\n     # contain the name of the origin operator.\n     # For example, the `std::IN` operator has `std::=`\ndiff --git a/edb/server/compiler/status.py b/edb/server/compiler/status.py\nindex c66a6274e..10fddead8 100644\n--- a/edb/server/compiler/status.py\n+++ b/edb/server/compiler/status.py\n@@ -68,6 +68,8 @@ def get_schema_class(ql: qlast.ObjectDDL) -> qltypes.SchemaObjectClass:\n             return osc.ALIAS\n         case qlast.GlobalCommand():\n             return osc.GLOBAL\n+        case qlast.BlobalCommand():\n+            return osc.GLOBAL\n         case qlast.PermissionCommand():\n             return osc.PERMISSION\n         case qlast.LinkCommand():\ndiff --git a/tests/test_edgeql_select.py b/tests/test_edgeql_select.py\nindex 17f302373..b812e04f6 100644\n--- a/tests/test_edgeql_select.py\n+++ b/tests/test_edgeql_select.py\n@@ -2060,6 +2060,18 @@ class TestEdgeQLSelect(tb.QueryTestCase):\n             [],\n         )\n \n+    async def test_edgeql_select_baseobject_function_01(self):\n+        # HACK: special inplace-upgrade test\n+        await self.con.execute('''\n+            CREATE BLOBAL asdf { set required := true; };\n+        ''')\n+        await self.assert_query_result(\n+            r'''\n+            select all_objects()[is schema::Blobal] { name };\n+            ''',\n+            [{\"name\": \"default::asdf\"}],\n+        )\n+\n     async def test_edgeql_select_empty_intersection_property(self):\n         with self.assertRaisesRegex(\n             edgedb.InvalidReferenceError,\ndiff --git a/tests/test_link_target_delete.py b/tests/test_link_target_delete.py\nindex 8982b3113..f50b28c92 100644\n--- a/tests/test_link_target_delete.py\n+++ b/tests/test_link_target_delete.py\n@@ -307,6 +307,48 @@ class TestLinkTargetDeleteDeclarative(stb.QueryTestCase):\n                     DELETE (SELECT Target1 FILTER .name = 'Target1.1');\n                 \"\"\")\n \n+    async def test_link_on_target_delete_restrict_schema_01(self):\n+        # HACK: special inplace-upgrade test\n+        async with self._run_and_rollback():\n+            await self.con.execute(\"\"\"\n+                CREATE BLOBAL asdf2 { set required := true; };\n+\n+                INSERT SchemaSource {\n+                    name := 'Source1.1',\n+                    schema_restrict := (\n+                        SELECT schema::Blobal LIMIT 1\n+                    )\n+                };\n+            \"\"\")\n+\n+            with self.assertRaisesRegex(\n+                    edgedb.ConstraintViolationError,\n+                    'prohibited by link'):\n+                await self.con.execute(\"\"\"\n+                    DROP BLOBAL asdf2;\n+                \"\"\")\n+\n+    async def test_link_on_target_delete_restrict_schema_02(self):\n+        # HACK: special inplace-upgrade test\n+        async with self._run_and_rollback():\n+            await self.con.execute(\"\"\"\n+                CREATE BLOBAL asdf2 { set required := true; };\n+\n+                INSERT SchemaSource {\n+                    name := 'Source1.1',\n+                    schema_m_restrict := (\n+                        SELECT schema::Blobal LIMIT 1\n+                    )\n+                };\n+            \"\"\")\n+\n+            with self.assertRaisesRegex(\n+                    edgedb.ConstraintViolationError,\n+                    'prohibited by link'):\n+                await self.con.execute(\"\"\"\n+                    DROP BLOBAL asdf2;\n+                \"\"\")\n+\n     async def test_link_on_target_delete_deferred_restrict_01(self):\n         exception_is_deferred = False\n \n"
  },
  {
    "path": "tests/patch-testing/test.sh",
    "content": "#!/bin/bash -ex\n\nwhile [[ $# -gt 0 ]]; do\n  case $1 in\n    --save-tarballs)\n        SAVE_TARBALLS=1\n        shift\n        ;;\n    *)\n        break\n        ;;\n  esac\ndone\n\n\nDIR=\"$1\"\nshift\n\nif ! git diff-index --quiet HEAD --; then\n    set +x\n    echo Refusing to run patching upgrade test with dirty git state.\n    echo \"(The test makes local modifications.)\"\n    exit 1\nfi\n\nmake parsers\n\n# Setup the test database\nedb inittestdb -D \"$DIR\" \"$@\"\n\n\nif [ \"$SAVE_TARBALLS\" = 1 ]; then\n    tar cf \"$DIR\".tar \"$DIR\"\nfi\n\n\n# Upgrade to the new version\npatch -f -p1 < tests/patch-testing/upgrade.patch\nmake parsers\n\nedb server --bootstrap-only --data-dir \"$DIR\"\n\nif [ \"$SAVE_TARBALLS\" = 1 ]; then\n    tar cf \"$DIR\"-upgraded.tar \"$DIR\"\nfi\n\n# Test!\nedb test --data-dir \"$DIR\" --use-data-dir-dbs -v \"$@\"\n"
  },
  {
    "path": "tests/patch-testing/upgrade.patch",
    "content": "diff --git a/edb/edgeql/ast.py b/edb/edgeql/ast.py\nindex 59973c0ec..003700c76 100644\n--- a/edb/edgeql/ast.py\n+++ b/edb/edgeql/ast.py\n@@ -1183,6 +1183,23 @@ class DropPermission(DropObject, PermissionCommand):\n     pass\n \n \n+class BlobalCommand(ObjectDDL):\n+\n+    __abstract_node__ = True\n+\n+\n+class CreateBlobal(CreateObject, BlobalCommand):\n+    pass\n+\n+\n+class AlterBlobal(AlterObject, BlobalCommand):\n+    pass\n+\n+\n+class DropBlobal(DropObject, BlobalCommand):\n+    pass\n+\n+\n class LinkCommand(ObjectDDL):\n \n     __abstract_node__ = True\ndiff --git a/edb/edgeql/codegen.py b/edb/edgeql/codegen.py\nindex 2dffdd519..e05a4108d 100644\n--- a/edb/edgeql/codegen.py\n+++ b/edb/edgeql/codegen.py\n@@ -2474,6 +2474,12 @@ class EdgeQLSourceGenerator(codegen.SourceGenerator):\n     def visit_DropPermission(self, node: qlast.DropPermission) -> None:\n         self._visit_DropObject(node, 'PERMISSION')\n \n+    def visit_CreateBlobal(self, node: qlast.CreateGlobal) -> None:\n+        self._visit_CreateObject(node, 'BLOBAL')\n+\n+    def visit_DropBlobal(self, node: qlast.DropGlobal) -> None:\n+        self._visit_DropObject(node, 'BLOBAL')\n+\n     def visit_ConfigSet(self, node: qlast.ConfigSet) -> None:\n         if node.scope == qltypes.ConfigScope.GLOBAL:\n             self._write_keywords('SET GLOBAL ')\ndiff --git a/edb/edgeql/parser/grammar/ddl.py b/edb/edgeql/parser/grammar/ddl.py\nindex bd55c93af..22fc58bf6 100644\n--- a/edb/edgeql/parser/grammar/ddl.py\n+++ b/edb/edgeql/parser/grammar/ddl.py\n@@ -260,6 +260,14 @@ class InnerDDLStmt(Nonterm):\n     def reduce_DropPermissionStmt(self, *_):\n         pass\n \n+    @parsing.inline(0)\n+    def reduce_CreateBlobalStmt(self, *_):\n+        pass\n+\n+    @parsing.inline(0)\n+    def reduce_DropBlobalStmt(self, *_):\n+        pass\n+\n     @parsing.inline(0)\n     def reduce_DropCastStmt(self, *_):\n         pass\n@@ -3613,6 +3621,38 @@ class DropGlobalStmt(Nonterm):\n         )\n \n \n+#\n+# CREATE BLOBAL\n+#\n+\n+\n+commands_block(\n+    'CreateBlobal',\n+    SetFieldStmt,\n+    CreateAnnotationValueStmt,\n+)\n+\n+\n+class CreateBlobalStmt(Nonterm):\n+    def reduce_CreateBlobal(self, *kids):\n+        \"\"\"%reduce\n+            CREATE BLOBAL NodeName\n+            OptCreateBlobalCommandsBlock\n+        \"\"\"\n+        self.val = qlast.CreateBlobal(\n+            name=kids[2].val,\n+            commands=kids[3].val,\n+        )\n+\n+\n+class DropBlobalStmt(Nonterm):\n+    def reduce_DropBlobal(self, *kids):\n+        r\"\"\"%reduce DROP BLOBAL NodeName\"\"\"\n+        self.val = qlast.DropBlobal(\n+            name=kids[2].val\n+        )\n+\n+\n #\n # CREATE PERMISSION\n #\ndiff --git a/edb/lib/_testmode.edgeql b/edb/lib/_testmode.edgeql\nindex 761a5dc53..d9a70f541 100644\n--- a/edb/lib/_testmode.edgeql\n+++ b/edb/lib/_testmode.edgeql\n@@ -232,6 +232,15 @@ create extension package _conf VERSION '1.0' {\n \n # std::_gen_series\n \n+CREATE FUNCTION\n+std::_upgrade_test(\n+) -> std::str\n+{\n+    SET volatility := 'Immutable';\n+    USING ('asdf');\n+};\n+\n+\n CREATE FUNCTION\n std::_gen_series(\n     `start`: std::int64,\ndiff --git a/edb/lib/schema.edgeql b/edb/lib/schema.edgeql\nindex a41bbba6e..f8f1b1ccb 100644\n--- a/edb/lib/schema.edgeql\n+++ b/edb/lib/schema.edgeql\n@@ -531,9 +531,17 @@ CREATE TYPE schema::Permission\n         schema::AnnotationSubject;\n \n \n+CREATE TYPE schema::Blobal EXTENDING schema::AnnotationSubject {\n+    CREATE PROPERTY required -> std::bool;\n+};\n+\n+\n CREATE TYPE schema::Function\n     EXTENDING schema::CallableObject, schema::VolatilitySubject\n {\n+    CREATE PROPERTY test_field_a -> std::str;\n+    CREATE PROPERTY test_nativecode_size -> std::int64;\n+\n     CREATE PROPERTY preserves_optionality -> std::bool {\n         SET default := false;\n     };\ndiff --git a/edb/pgsql/delta.py b/edb/pgsql/delta.py\nindex 3a9effaa5..145a472cd 100644\n--- a/edb/pgsql/delta.py\n+++ b/edb/pgsql/delta.py\n@@ -777,6 +777,38 @@ class RenamePermission(\n     pass\n \n \n+class BlobalCommand(MetaCommand):\n+    pass\n+\n+\n+class CreateBlobal(\n+    BlobalCommand,\n+    adapts=s_globals.CreateBlobal,\n+):\n+    pass\n+\n+\n+class RenameBlobal(\n+    BlobalCommand,\n+    adapts=s_globals.RenameBlobal,\n+):\n+    pass\n+\n+\n+class AlterBlobal(\n+    BlobalCommand,\n+    adapts=s_globals.AlterBlobal,\n+):\n+    pass\n+\n+\n+class DeleteBlobal(\n+    BlobalCommand,\n+    adapts=s_globals.DeleteBlobal,\n+):\n+    pass\n+\n+\n class AccessPolicyCommand(MetaCommand):\n     pass\n \ndiff --git a/edb/pgsql/patches.py b/edb/pgsql/patches.py\nindex 12c627df8..df6773aeb 100644\n--- a/edb/pgsql/patches.py\n+++ b/edb/pgsql/patches.py\n@@ -66,4 +66,41 @@ The current kinds are:\n  * ...+testmode - only run the patch in testmode. Works with any patch kind.\n \"\"\"\n PATCHES: list[tuple[str, str]] = [\n+    ('edgeql', '''\n+CREATE FUNCTION\n+std::_upgrade_test(\n+) -> std::str\n+{\n+    SET volatility := 'Immutable';\n+    USING ('asdf');\n+};\n+'''),\n+    ('edgeql+schema', '''\n+# Empty edgeql+schema patch to make sure that non-initial\n+# edgeql+schema patches can add things publically. (They didn't\n+# use to be able to.)\n+    '''),\n+    ('edgeql+schema', '''\n+CREATE TYPE schema::Blobal EXTENDING schema::AnnotationSubject {\n+    CREATE PROPERTY required -> std::bool;\n+};\n+'''),\n+    ('edgeql+schema', '''\n+ALTER TYPE schema::Function\n+{\n+    CREATE PROPERTY test_field_a -> std::str;\n+    CREATE PROPERTY test_nativecode_size -> std::int64;\n+};\n+'''),\n+    ('repair', ''),\n+    ('edgeql+schema+config+testmode', '''\n+ALTER TYPE cfg::AbstractConfig {\n+    CREATE PROPERTY __internal_sess_testvalue2 -> std::str {\n+        CREATE ANNOTATION cfg::internal := 'true';\n+        SET default := '!';\n+    };\n+};\n+'''),\n+    ('sql-introspection', ''),\n+    ('metaschema-sql', 'SysConfigFullFunction'),\n ]\ndiff --git a/edb/schema/functions.py b/edb/schema/functions.py\nindex 48baa30be..a3c238a7c 100644\n--- a/edb/schema/functions.py\n+++ b/edb/schema/functions.py\n@@ -1252,6 +1252,31 @@ class Function(\n     data_safe=True,\n ):\n \n+    ##\n+    test_field_a = so.SchemaField(\n+        str,\n+        default=None,\n+        compcoef=0.4,\n+        allow_ddl_set=True,\n+        patch_level=2,\n+    )\n+\n+    test_field_b = so.SchemaField(\n+        str,\n+        default=None,\n+        compcoef=0.4,\n+        allow_ddl_set=True,\n+        patch_level=2,\n+    )\n+\n+    test_nativecode_size = so.SchemaField(\n+        int,\n+        default=None,\n+        compcoef=0.99,\n+        patch_level=2,\n+    )\n+    ##\n+\n     used_globals = so.SchemaField(\n         so.ObjectSet[s_globals.Global],\n         coerce=True, default=so.DEFAULT_CONSTRUCTOR,\n@@ -1655,6 +1680,10 @@ class FunctionCommand(\n                 nativecode.not_compiled()\n             )\n \n+        if self.has_attribute_value('nativecode'):\n+            code = self.get_attribute_value('nativecode')\n+            self.set_attribute_value('test_nativecode_size', len(code.text))\n+\n         # Resolving 'nativecode' has side effects on has_dml and\n         # volatility, so force it to happen as part of\n         # canonicalization of attributes.\ndiff --git a/edb/schema/globals.py b/edb/schema/globals.py\nindex 0bac4f113..7ff961aa6 100644\n--- a/edb/schema/globals.py\n+++ b/edb/schema/globals.py\n@@ -637,3 +637,61 @@ class DeleteGlobal(\n                 self.add_caused(op)\n \n         return schema\n+\n+\n+class Blobal(\n+    so.QualifiedObject,\n+    s_anno.AnnotationSubject,\n+    qlkind=qltypes.SchemaObjectClass.GLOBAL,\n+    data_safe=True,\n+    patch_level=1,\n+):\n+\n+    required = so.SchemaField(\n+        bool,\n+        default=False,\n+        compcoef=0.909,\n+        allow_ddl_set=True,\n+    )\n+\n+\n+class BlobalCommandContext(\n+    sd.ObjectCommandContext[so.Object],\n+    s_anno.AnnotationSubjectCommandContext\n+):\n+    pass\n+\n+\n+class BlobalCommand(\n+    sd.QualifiedObjectCommand[Blobal],\n+    context_class=BlobalCommandContext,\n+):\n+    pass\n+\n+\n+class CreateBlobal(\n+    sd.CreateObject[Blobal],\n+    BlobalCommand,\n+):\n+    astnode = qlast.CreateBlobal\n+\n+\n+class RenameBlobal(\n+    sd.RenameObject[Blobal],\n+    BlobalCommand,\n+):\n+    pass\n+\n+\n+class AlterBlobal(\n+    sd.AlterObject[Blobal],\n+    BlobalCommand,\n+):\n+    astnode = qlast.AlterBlobal\n+\n+\n+class DeleteBlobal(\n+    sd.DeleteObject[Blobal],\n+    BlobalCommand,\n+):\n+    astnode = qlast.DropBlobal\ndiff --git a/edb/server/compiler/status.py b/edb/server/compiler/status.py\nindex c66a6274e..10fddead8 100644\n--- a/edb/server/compiler/status.py\n+++ b/edb/server/compiler/status.py\n@@ -68,6 +68,8 @@ def get_schema_class(ql: qlast.ObjectDDL) -> qltypes.SchemaObjectClass:\n             return osc.ALIAS\n         case qlast.GlobalCommand():\n             return osc.GLOBAL\n+        case qlast.BlobalCommand():\n+            return osc.GLOBAL\n         case qlast.PermissionCommand():\n             return osc.PERMISSION\n         case qlast.LinkCommand():\ndiff --git a/tests/test_edgeql_select.py b/tests/test_edgeql_select.py\nindex 17f302373..38e43ddbe 100644\n--- a/tests/test_edgeql_select.py\n+++ b/tests/test_edgeql_select.py\n@@ -2060,6 +2060,56 @@ class TestEdgeQLSelect(tb.QueryTestCase):\n             [],\n         )\n \n+    @test.xfail('Not fixed for patches; see #5844')\n+    async def test_edgeql_select_baseobject_function_01(self):\n+        # HACK: special inplace-upgrade test\n+        await self.con.execute('''\n+            CREATE BLOBAL asdf { set required := true; };\n+        ''')\n+        await self.assert_query_result(\n+            r'''\n+            select all_objects()[is schema::Blobal] { name };\n+            ''',\n+            [{\"name\": \"default::asdf\"}],\n+        )\n+\n+    async def test_edgeql_select_nativecode_size_01(self):\n+        # HACK: special inplace-upgrade test\n+        await self.assert_query_result(\n+            r'''\n+            select schema::Function { test_nativecode_size }\n+            filter .name = 'default::ident'\n+            ''',\n+            [{\"test_nativecode_size\": 8}],\n+        )\n+\n+    async def test_edgeql_select_config_hack_01(self):\n+        # HACK: special inplace-upgrade test\n+        await self.assert_query_result(\n+            r'''\n+            select cfg::Config.__internal_sess_testvalue2\n+            ''',\n+            ['!']\n+        )\n+        await self.con.execute(\n+            r'''\n+            configure session set __internal_sess_testvalue2 := 'asdf';\n+            '''\n+        )\n+        await self.assert_query_result(\n+            r'''\n+            select cfg::Config.__internal_sess_testvalue2\n+            ''',\n+            ['asdf']\n+        )\n+\n+        await self.assert_query_result(\n+            r'''\n+            select _upgrade_test()\n+            ''',\n+            ['asdf']\n+        )\n+\n     async def test_edgeql_select_empty_intersection_property(self):\n         with self.assertRaisesRegex(\n             edgedb.InvalidReferenceError,\ndiff --git a/tests/test_link_target_delete.py b/tests/test_link_target_delete.py\nindex 8982b3113..de1632b0a 100644\n--- a/tests/test_link_target_delete.py\n+++ b/tests/test_link_target_delete.py\n@@ -29,6 +29,7 @@ from edb.schema import links as s_links\n from edb.schema import name as s_name\n \n from edb.testbase import server as stb\n+from edb.tools import test\n \n \n class TestLinkTargetDeleteSchema(tb.BaseSchemaLoadTest):\n@@ -307,6 +308,50 @@ class TestLinkTargetDeleteDeclarative(stb.QueryTestCase):\n                     DELETE (SELECT Target1 FILTER .name = 'Target1.1');\n                 \"\"\")\n \n+    @test.xfail('Not fixed for patches; see #5844')\n+    async def test_link_on_target_delete_restrict_schema_01(self):\n+        # HACK: special inplace-upgrade test\n+        async with self._run_and_rollback():\n+            await self.con.execute(\"\"\"\n+                CREATE BLOBAL asdf2 { set required := true; };\n+\n+                INSERT SchemaSource {\n+                    name := 'Source1.1',\n+                    schema_restrict := (\n+                        SELECT schema::Blobal LIMIT 1\n+                    )\n+                };\n+            \"\"\")\n+\n+            with self.assertRaisesRegex(\n+                    edgedb.ConstraintViolationError,\n+                    'prohibited by link'):\n+                await self.con.execute(\"\"\"\n+                    DROP BLOBAL asdf2;\n+                \"\"\")\n+\n+    @test.xfail('Not fixed for patches; see #5844')\n+    async def test_link_on_target_delete_restrict_schema_02(self):\n+        # HACK: special inplace-upgrade test\n+        async with self._run_and_rollback():\n+            await self.con.execute(\"\"\"\n+                CREATE BLOBAL asdf2 { set required := true; };\n+\n+                INSERT SchemaSource {\n+                    name := 'Source1.1',\n+                    schema_m_restrict := (\n+                        SELECT schema::Blobal LIMIT 1\n+                    )\n+                };\n+            \"\"\")\n+\n+            with self.assertRaisesRegex(\n+                    edgedb.ConstraintViolationError,\n+                    'prohibited by link'):\n+                await self.con.execute(\"\"\"\n+                    DROP BLOBAL asdf2;\n+                \"\"\")\n+\n     async def test_link_on_target_delete_deferred_restrict_01(self):\n         exception_is_deferred = False\n \n"
  },
  {
    "path": "tests/schemas/advtypes.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nabstract type R {\n    required property name -> str {\n        delegated constraint exclusive;\n    }\n}\n\ntype A extending R;\n\ntype S extending R {\n    required property s -> str;\n    multi link l_a -> A;\n}\n\ntype T extending R {\n    required property t -> str;\n    multi link l_a -> A;\n}\n\nabstract type U {\n    required property u -> str;\n}\n\ntype V extending U, S, T;\n\ntype W {\n    required property name -> str {\n        constraint exclusive;\n    }\n    link w -> W;\n}\n\ntype X extending W, U;\n\ntype Z {\n    required property name -> str {\n        constraint exclusive;\n    };\n\n    # have 'name' in common\n    multi link stw0 -> S | T | W;\n}\n\n# 3 abstract base types and their concrete permutations\nabstract type Ba {\n    required property ba -> str;\n}\n\nabstract type Bb {\n    required property bb -> int64;\n}\n\nabstract type Bc {\n    required property bc -> float64;\n}\n\ntype CBa extending Ba;\n\ntype CBb extending Bb;\n\ntype CBc extending Bc;\n\ntype CBaBb extending Ba, Bb;\n\ntype CBaBc extending Ba, Bc;\n\ntype CBbBc extending Bb, Bc;\n\ntype CBaBbBc extending Ba, Bb, Bc;\n\n# 3 types which resemble the base types\n\ntype XBa {\n    required property ba -> str;\n}\n\ntype XBb {\n    required property bb -> int64;\n}\n\ntype XBc {\n    required property bc -> float64;\n}\n\n# Objects which all have a `numbers` property and `siblings` link\n\n# non-computed single\n\ntype SoloNonCompSinglePropA {\n    single property numbers -> int64;\n}\ntype SoloNonCompSinglePropB {\n    single property numbers -> int64;\n}\ntype SoloNonCompSingleLinkA {\n    single link siblings -> SoloNonCompSingleLinkA;\n}\ntype SoloNonCompSingleLinkB {\n    single link siblings -> SoloNonCompSingleLinkB;\n}\n\n# non-computed multi\n\ntype SoloNonCompMultiPropA {\n    multi property numbers -> int64;\n}\ntype SoloNonCompMultiPropB {\n    multi property numbers -> int64;\n}\ntype SoloNonCompMultiLinkA {\n    multi link siblings -> SoloNonCompMultiLinkA;\n}\ntype SoloNonCompMultiLinkB {\n    multi link siblings -> SoloNonCompMultiLinkB;\n}\n\n# computed single\n\ntype SoloCompSinglePropA {\n    single property numbers := 1;\n}\ntype SoloCompSinglePropB {\n    single property numbers := 1;\n}\ntype SoloCompSingleLinkA {\n    single link siblings := (select detached SoloCompSingleLinkA limit 1);\n}\ntype SoloCompSingleLinkB {\n    single link siblings := (select detached SoloCompSingleLinkB limit 1);\n}\n\n# computed multi\n\ntype SoloCompMultiPropA {\n    multi property numbers := {1, 2, 3};\n}\ntype SoloCompMultiPropB {\n    multi property numbers := {1, 2, 3};\n}\ntype SoloCompMultiLinkA {\n    multi link siblings := (select detached SoloCompMultiLinkA);\n}\ntype SoloCompMultiLinkB {\n    multi link siblings := (select detached SoloCompMultiLinkB);\n}\n\n# non-computed single from base class\n\nabstract type BaseNonCompSingleProp {\n    single property numbers -> int64;\n}\ntype DerivedNonCompSinglePropA extending BaseNonCompSingleProp;\ntype DerivedNonCompSinglePropB extending BaseNonCompSingleProp;\n\nabstract type BaseNonCompSingleLink {\n    single link siblings -> BaseNonCompSingleLink;\n}\ntype DerivedNonCompSingleLinkA extending BaseNonCompSingleLink;\ntype DerivedNonCompSingleLinkB extending BaseNonCompSingleLink;\n\n# non-computed multi from base class\n\nabstract type BaseNonCompMultiProp {\n    multi property numbers -> int64;\n}\ntype DerivedNonCompMultiPropA extending BaseNonCompMultiProp;\ntype DerivedNonCompMultiPropB extending BaseNonCompMultiProp;\n\nabstract type BaseNonCompMultiLink {\n    multi link siblings -> BaseNonCompMultiLink;\n}\ntype DerivedNonCompMultiLinkA extending BaseNonCompMultiLink;\ntype DerivedNonCompMultiLinkB extending BaseNonCompMultiLink;\n\n# computed single from base class\n\nabstract type BaseCompSingleProp {\n    single property numbers := 1;\n}\ntype DerivedCompSinglePropA extending BaseCompSingleProp;\ntype DerivedCompSinglePropB extending BaseCompSingleProp;\n\nabstract type BaseCompSingleLink {\n    single link siblings := (select detached BaseCompSingleLink limit 1);\n}\ntype DerivedCompSingleLinkA extending BaseCompSingleLink;\ntype DerivedCompSingleLinkB extending BaseCompSingleLink;\n\n# computed multi from base class\n\nabstract type BaseCompMultiProp {\n    multi property numbers := {1, 2, 3};\n}\ntype DerivedCompMultiPropA extending BaseCompMultiProp;\ntype DerivedCompMultiPropB extending BaseCompMultiProp;\n\nabstract type BaseCompMultiLink {\n    multi link siblings := (select detached BaseCompMultiLink);\n}\ntype DerivedCompMultiLinkA extending BaseCompMultiLink;\ntype DerivedCompMultiLinkB extending BaseCompMultiLink;\n\n# Objects with links to a target type\n\ntype Destination {\n    required property name -> str;\n}\n\n# independent types with compatible pointers\n\ntype SoloOriginA {\n    single link dest -> Destination;\n}\ntype SoloOriginB {\n    single link dest -> Destination;\n}\n\n# independent types with compatible pointers and common derived type\n\ntype BaseOriginA {\n    single link dest -> Destination;\n}\ntype BaseOriginB {\n    single link dest -> Destination;\n}\ntype DerivedOriginC extending BaseOriginA, BaseOriginB;\n"
  },
  {
    "path": "tests/schemas/cards.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nabstract type Named {\n    required name: str {\n        delegated constraint exclusive;\n    }\n}\n\ntype User extending Named {\n    multi deck: Card {\n        count: int64 {\n            default := 1;\n        };\n        property total_cost := @count * .cost;\n    }\n\n    property deck_cost := sum(.deck.cost);\n\n    multi friends: User {\n        nickname: str;\n        # how the friend responded to requests for a favor\n        #favor: array<bool>\n    }\n\n    multi awards: Award {\n        constraint exclusive;\n    }\n\n    avatar: Card {\n        text: str;\n        property tag := .name ++ ((\"-\" ++ @text) ?? \"\");\n    }\n    constraint exclusive on (.avatar);\n}\n\ntype Bot extending User;\n\ntype Card extending Named {\n    required element: str;\n    required cost: int64;\n    optional text: str;\n    multi owners := .<deck[IS User];\n    # computable property\n    elemental_cost := <str>.cost ++ ' ' ++ .element;\n    multi awards: Award;\n    multi good_awards := (SELECT .awards FILTER .name != '3rd');\n    single best_award := (select .awards order by .name limit 1);\n}\n\ntype SpecialCard extending Card;\n\ntype Award extending Named {\n    link winner := .<awards[is User];\n};\n\nalias AirCard := (\n    SELECT Card\n    FILTER Card.element = 'Air'\n);\n\nalias WaterCard := (\n    SELECT Card\n    FILTER Card.element = 'Water'\n);\n\nalias EarthCard := (\n    SELECT Card\n    FILTER Card.element = 'Earth'\n);\n\nalias FireCard := (\n    SELECT Card\n    FILTER Card.element = 'Fire'\n);\n\nalias WaterOrEarthCard := (\n    SELECT Card {\n        owned_by_alice := EXISTS (SELECT Card.<deck[IS User].name = 'Alice')\n    }\n    FILTER .element = 'Water' OR .element = 'Earth'\n);\n\nalias EarthOrFireCard {\n    using (SELECT Card FILTER .element = 'Fire' OR .element = 'Earth')\n};\n\nalias AliceCard := (\n    SELECT Card\n    FILTER 'Alice' IN Card.<deck[IS User].name\n);\n\nalias BobCard := (\n    SELECT Card\n    FILTER 'Bob' IN Card.<deck[IS User].name\n);\n\nalias CarolCard := (\n    SELECT Card\n    FILTER 'Carol' IN Card.<deck[IS User].name\n);\n\nalias DaveCard := (\n    SELECT Card\n    FILTER 'Dave' IN Card.<deck[IS User].name\n);\n\nalias AliasedFriends := (\n    SELECT User { my_friends := User.friends, my_name := User.name }\n);\n\nalias AwardAlias := (\n    Award {\n        # this should be a single link, because awards are exclusive\n        winner := Award.<awards[IS User] {\n            name_upper := str_upper(.name)\n        }\n    }\n);\n\n# This expression is unnecessarily deep, but that shouldn't have\n# any impact as compared to AwardAlias.\nalias AwardAlias2 := (\n    SELECT Award {\n        winner := Award.<awards[IS User] {\n            deck: {\n                id\n            }\n        }\n    }\n);\n\n# This alias includes ordering\nalias UserAlias := (\n    SELECT User {\n        deck: {\n            id\n        } ORDER BY User.deck.cost DESC\n          LIMIT 1,\n    }\n);\n\nalias SpecialCardAlias := SpecialCard {\n    el_cost := (.element, .cost)\n};\n\nalias AliasOne := 1;\nglobal GlobalOne := 1;\n\nglobal HighestCost := (\n    SELECT max(Card.cost)\n);\n\nglobal CardsWithText := (\n    SELECT Card FILTER exists(.text)\n);\n\nalias AliasArrayOfArrayOfScalar := [[1, 2, 3], [4, 5, 6]];\nglobal GlobalArrayOfArrayOfScalar := [[1, 2, 3], [4, 5, 6]];\n\nalias AliasCardsByCost := array_agg((\n    for cost in range_unpack(range(0, max(Card.cost) + 1))\n        select array_agg(\n            (select Card filter .cost = cost)\n        )\n));\nglobal GlobalCardsByCost := array_agg((\n    for cost in range_unpack(range(0, max(Card.cost) + 1))\n        select array_agg(\n            (select Card filter .cost = cost)\n        )\n));\n\npermission GameAdmin;\n"
  },
  {
    "path": "tests/schemas/cards_ir_inference.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nabstract type Named {\n    required property name -> str {\n        delegated constraint exclusive;\n    }\n}\n\ntype User extending Named {\n    multi link deck -> Card {\n        property count -> int64;\n    }\n\n    property deck_cost := sum(.deck.cost);\n\n    multi link friends -> User {\n        property nickname -> str;\n        # how the friend responded to requests for a favor\n        #property favor -> array<bool>\n    }\n\n    multi link awards -> Award {\n        constraint exclusive;\n    }\n\n    link avatar -> Card {\n        property text -> str;\n    }\n\n    link unique_avatar -> Card {\n        constraint exclusive;\n    }\n}\n\ntype Card extending Named {\n    required property element -> str;\n    required property cost -> int64;\n    multi link owners := .<deck[IS User];\n    # computable property\n    property elemental_cost := <str>.cost ++ ' ' ++ .element;\n\n    required multi link req_awards -> Award;\n    required multi property req_tags -> str;\n}\n\ntype SpecialCard extending Card;\n\ntype Award extending Named {\n    link rec := .<awards[IS User]\n}\n\n\nalias AwardAlias := Award {\n    recipient := .<awards[IS User]\n};\n\n\nalias WaterOrEarthCard := (\n    SELECT Card {\n        owned_by_alice := EXISTS (SELECT Card.<deck[IS User].name = 'Alice')\n    }\n    FILTER .element = 'Water' OR .element = 'Earth'\n);\n\n\nalias EarthOrFireCard {\n    using (SELECT Card FILTER .element = 'Fire' OR .element = 'Earth')\n};\n\n\nalias SpecialCardAlias := SpecialCard {\n    el_cost := (.element, .cost)\n};\n\n\ntype Eert {\n    required property val -> str {\n        constraint exclusive;\n    }\n\n    link parent := .<children[IS Eert];\n    multi link children -> Eert {\n        constraint exclusive;\n    }\n}\n\ntype Asdf {\n    link children -> Eert;\n}\n\n\ntype Report extending Named {\n    property subtitle -> str;\n\n    required link user -> User {\n        property note -> str;\n    }\n}\n\n\nabstract type BadlyNamed {\n    property first -> str;\n    property last -> str;\n    delegated constraint exclusive on ((.first, .last));\n}\n\n\ntype Person extending BadlyNamed {\n    # these constraints don't really make sense but that's fine.\n    property email -> str;\n    constraint exclusive on (.email);\n    property p -> int64;\n    property q -> int64;\n    constraint exclusive on (.p * .q);\n    constraint exclusive on (((.p, __subject__.q), __subject__.first));\n\n    link card -> Card;\n    constraint exclusive on ((.p, .card));\n}\n\n\nfunction taking_opt_returning_non_opt(a: optional str) -> str {\n    using (\n        a ?? \"\"\n    );\n};\n\nfunction taking_non_opt_returning_opt(a: str) -> optional str {\n    using (\n        a\n    );\n};\n\n\ntype Tgt;\nabstract type Src {\n    required lnk: Tgt { delegated constraint exclusive }\n};\ntype SrcSub1 extending Src;\ntype SrcSub2 extending Src;\n\nabstract type Named2 {\n    required property name -> str;\n    delegated constraint exclusive on (.name);\n}\ntype Named2Sub extending Named2;\n\nglobal Alice := (select User filter .name = 'Alice');\n\npermission GameAdmin;\n\ntype TypeExprA {\n    val: str;\n};\ntype TypeExprB {\n    required val: str;\n};\ntype TypeExprC {\n    multi val: str;\n};\ntype TypeExprD {\n    required multi val: str;\n};"
  },
  {
    "path": "tests/schemas/cards_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nFOR award in {'1st', '2nd', '3rd'} UNION (\n    INSERT Award { name := award }\n);\n\nINSERT Card {\n    name := 'Imp',\n    element := 'Fire',\n    cost := 1,\n    awards := (SELECT Award FILTER .name = '2nd'),\n};\n\nINSERT Card {\n    name := 'Dragon',\n    element := 'Fire',\n    cost := 5,\n    awards := (SELECT Award FILTER .name IN {'1st', '3rd'}),\n    text := '\"Watch your back, shoot straight, conserve ammo, and never, ever, cut a deal with a dragon.\" -Street Proverb',\n};\n\nINSERT Card {\n    name := 'Bog monster',\n    element := 'Water',\n    cost := 2,\n};\n\nINSERT Card {\n    name := 'Giant turtle',\n    element := 'Water',\n    cost := 3,\n    text := '\"The world rides through space on the back of a turtle. This is one of the great ancient world myths, found wherever men and turtles were gathered together;\"',\n};\n\nINSERT Card {\n    name := 'Dwarf',\n    element := 'Earth',\n    cost := 1,\n    text := '\"The dwarves of yore made mighty spells / While hammers fell like ringing bells\"',\n};\n\nINSERT Card {\n    name := 'Golem',\n    element := 'Earth',\n    cost := 3,\n    # text := '\"The only smell Josef could detect arising from the swarthy flesh of the Golem was one too faint to name, acrid and green, that he was only later to identify as the sweet stench, on a summer afternoon in the dog days, of the Moldau.\"',\n    text := '\"Every golem in the history of the world, from Rabbi Hanina’s delectable goat to the river-clay Frankenstein of Rabbi Judah Loew ben Bezalel, was summoned into existence through language, through murmuring, recital, and kabbalistic chitchat—was, literally, talked into life.\"',\n};\n\nINSERT Card {\n    name := 'Sprite',\n    element := 'Air',\n    cost := 1,\n};\n\nINSERT Card {\n    name := 'Giant eagle',\n    element := 'Air',\n    cost := 2,\n    text := '\"The North Wind blows, but we shall outfly it\"',\n};\n\nINSERT SpecialCard {\n    name := 'Djinn',\n    element := 'Air',\n    cost := 4,\n    awards := (SELECT Award FILTER .name = '3rd'),\n    text := '\"Phenomenal cosmic powers! ... Itty bitty living space!\"',\n};\n\n\n# create players & decks\nINSERT User {\n    name := 'Alice',\n    deck := (\n        SELECT Card {@count := len(Card.element) - 2}\n        FILTER .element IN {'Fire', 'Water'}\n    ),\n    awards := (SELECT Award FILTER .name IN {'1st', '2nd'}),\n    avatar := (\n        SELECT Card {@text := 'Best'} FILTER .name = 'Dragon'\n    ),\n};\n\nINSERT User {\n    name := 'Bob',\n    deck := (\n        SELECT Card {@count := 3} FILTER .element IN {'Earth', 'Water'}\n    ),\n    awards := (SELECT Award FILTER .name = '3rd'),\n};\n\nINSERT User {\n    name := 'Carol',\n    deck := (\n        SELECT Card {@count := 5 - Card.cost} FILTER .element != 'Fire'\n    )\n};\n\nINSERT Bot {\n    name := 'Dave',\n    deck := (\n        SELECT Card {@count := 4 IF Card.cost = 1 ELSE 1}\n        FILTER .element = 'Air' OR .cost != 1\n    ),\n    avatar := (\n        SELECT Card {@text := 'Wow'} FILTER .name = 'Djinn'\n    ),\n};\n\n# update friends list\nWITH\n    U2 := DETACHED User\nUPDATE User\nFILTER User.name = 'Alice'\nSET {\n    friends := (\n        SELECT U2 {\n            @nickname :=\n                'Swampy'        IF U2.name = 'Bob' ELSE\n                'Firefighter'   IF U2.name = 'Carol' ELSE\n                'Grumpy'\n        } FILTER U2.name IN {'Bob', 'Carol', 'Dave'}\n    )\n};\n\nWITH\n    U2 := DETACHED User\nUPDATE User\nFILTER User.name = 'Dave'\nSET {\n    friends := (\n        SELECT U2 FILTER U2.name = 'Bob'\n    )\n};\n"
  },
  {
    "path": "tests/schemas/casts.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nscalar type custom_str_t extending str {\n    constraint regexp('[A-Z]+');\n}\n\nscalar type foo extending str;\nscalar type bar extending str;\n\ntype Test {\n    property p_bool -> bool;\n    property p_str -> str;\n    property p_datetime -> datetime;\n    property p_local_datetime -> cal::local_datetime;\n    property p_local_date -> cal::local_date;\n    property p_local_time -> cal::local_time;\n    property p_duration -> duration;\n    property p_int16 -> int16;\n    property p_int32 -> int32;\n    property p_int64 -> int64;\n    property p_float32 -> float32;\n    property p_float64 -> float64;\n    property p_bigint -> bigint;\n    property p_decimal -> decimal;\n    property p_tup -> tuple<test: str>;\n}\n\ntype JSONTest {\n    property j_bool -> json;\n    property j_str -> json;\n    property j_datetime -> json;\n    property j_local_datetime -> json;\n    property j_local_date -> json;\n    property j_local_time -> json;\n    property j_duration -> json;\n    property j_int16 -> json;\n    property j_int32 -> json;\n    property j_int64 -> json;\n    property j_float32 -> json;\n    property j_float64 -> json;\n    property j_bigint -> json;\n    property j_decimal -> json;\n}\n\ntype ScalarTest {\n    property p_bool -> bool;\n    property p_uuid -> uuid;\n    property p_str -> str;\n    property p_datetime -> datetime;\n    property p_local_datetime -> cal::local_datetime;\n    property p_local_date -> cal::local_date;\n    property p_local_time -> cal::local_time;\n    property p_duration -> duration;\n    property p_int16 -> int16;\n    property p_int32 -> int32;\n    property p_int64 -> int64;\n    property p_float32 -> float32;\n    property p_float64 -> float64;\n    property p_bigint -> bigint;\n    property p_decimal -> decimal;\n    property p_json -> json;\n}\n\ntype Person {\n    property name -> str;\n}\n"
  },
  {
    "path": "tests/schemas/casts_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nINSERT Test {\n    p_bool := True,\n    p_str := 'Hello',\n    p_datetime := <datetime>'2018-05-07T20:01:22.306916+00:00',\n    p_local_datetime := <cal::local_datetime>'2018-05-07T20:01:22.306916',\n    p_local_date := <cal::local_date>'2018-05-07',\n    p_local_time := <cal::local_time>'20:01:22.306916',\n    p_duration := <duration>'20 hrs',\n    p_int16 := 12345,\n    p_int32 := 1234567890,\n    p_int64 := 1234567890123,\n    p_float32 := 2.5,\n    p_float64 := 2.5,\n    p_bigint := 123456789123456789123456789n,\n    p_decimal := 123456789123456789123456789.123456789123456789123456789n,\n};\n\n\nINSERT JSONTest {\n    j_bool := <json>True,\n    j_str := <json>'Hello',\n    j_datetime := <json><datetime>'2018-05-07T20:01:22.306916+00:00',\n    j_local_datetime := <json><cal::local_datetime>'2018-05-07T20:01:22.306916',\n    j_local_date := <json><cal::local_date>'2018-05-07',\n    j_local_time := <json><cal::local_time>'20:01:22.306916',\n    j_duration := <json><duration>'20 hrs',\n    j_int16 := <json>12345,\n    j_int32 := <json>1234567890,\n    j_int64 := <json>1234567890123,\n    j_float32 := <json>2.5,\n    j_float64 := <json>2.5,\n    j_bigint := <json>123456789123456789123456789n,\n    j_decimal := <json>123456789123456789123456789.123456789123456789123456789n\n};\n\nINSERT Person {\n    name := 'tom',\n};\n\nINSERT Person {\n    name := 'kelly',\n};"
  },
  {
    "path": "tests/schemas/constraints.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nscalar type constraint_length extending str {\n    constraint max_len_value(16);\n    constraint max_len_value(10);\n    constraint min_len_value(5);\n    constraint min_len_value(8);\n}\n\nscalar type constraint_length_2 extending constraint_length {\n    constraint min_len_value(9);\n}\n\nscalar type constraint_minmax extending str {\n    constraint min_value(\"99900000\");\n    constraint min_value(\"99990000\");\n    constraint max_value(\"9999999989\");\n}\n\nscalar type constraint_minmax_2 extending float64 {\n    constraint min_ex_value(13);\n    constraint max_ex_value(100);\n}\n\nscalar type constraint_strvalue extending str {\n    constraint expression on (__subject__[-1:] = '9');\n\n    constraint regexp(r\"^\\d+$\");\n\n    constraint expression on (__subject__[0] = '9');\n\n    constraint regexp(r\"^\\d+9{3,}.*$\");\n}\n\n# A variant of one_of that uses an array argument instead of\n# a variadic.\nabstract constraint my_one_of(one_of: array<anytype>) {\n    using (contains(one_of, __subject__));\n}\n\n\nscalar type constraint_enum extending str {\n   constraint one_of('foo', 'bar');\n}\n\nscalar type constraint_enum2 extending str {\n   constraint one_of('notfoo', 'notbar');\n}\n\nscalar type constraint_my_enum extending str {\n   constraint my_one_of(['fuz', 'buz']);\n}\n\n\nabstract link translated_label {\n    lang: str;\n    prop1: str;\n}\n\nabstract link link_with_unique_property {\n    property unique_property -> str {\n        # TODO: Move the constraint back here once linkprop constraints\n        # supported in conflict selects.\n        # constraint exclusive;\n    }\n    constraint exclusive on (@unique_property);\n}\n\nabstract link link_with_unique_property_inherited\n    extending link_with_unique_property;\n\nabstract link another_link_with_unique_property {\n    property unique_property -> str {\n        constraint exclusive;\n    }\n}\n\nabstract link another_link_with_unique_property_inherited\n    extending another_link_with_unique_property;\n\n\ntype Label {\n    property text -> str;\n}\n\ntype Object {\n    property name -> str;\n    property c_length -> constraint_length;\n    property c_length_2 -> constraint_length_2;\n    property c_length_3 -> constraint_length_2 {\n        constraint min_len_value(10);\n    }\n    property c_one_of -> str {\n        constraint one_of('foo', 'bar');\n    }\n\n    property c_minmax -> constraint_minmax;\n    property c_ex_minmax -> constraint_minmax_2;\n    property c_strvalue -> constraint_strvalue;\n    property c_enum -> constraint_enum;\n    property c_enum2 -> constraint_enum2 {\n        default := 'notfoo';\n    }\n    property c_my_enum -> constraint_my_enum;\n}\n\ntype ObjCnstr {\n    required property first_name -> str;\n    required property last_name -> str;\n    link label -> Label;\n    constraint exclusive on (__subject__.first_name);\n    constraint exclusive on (__subject__.label);\n}\n\ntype UniqueName {\n    property name -> str {\n        constraint exclusive;\n    }\n\n    link link_with_unique_property\n        extending link_with_unique_property -> Object;\n\n    link link_with_unique_property_inherited\n        extending link_with_unique_property_inherited -> Object;\n\n    link translated_label extending translated_label -> Label {\n        constraint exclusive on ((__subject__@source, __subject__@lang));\n        constraint exclusive on (__subject__@prop1);\n    }\n\n    multi link translated_labels extending translated_label -> Label {\n        constraint exclusive on ((@source, @lang));\n        constraint exclusive on (__subject__@prop1);\n    }\n\n    link translated_label_tgt extending translated_label -> Label {\n        constraint exclusive on ((__subject__@target, __subject__@lang));\n    }\n\n    multi link translated_labels_tgt extending translated_label -> Label {\n        constraint exclusive on ((@target, @lang));\n    }\n}\n\ntype UniqueNameInherited extending UniqueName {\n    overloaded property name -> str;\n}\n\ntype UniqueDescription {\n    property description -> str {\n        constraint exclusive;\n    }\n\n    link another_link_with_unique_property\n        extending another_link_with_unique_property -> Object;\n\n    link another_link_with_unique_property_inherited\n        extending another_link_with_unique_property  -> Object;\n}\n\ntype UniqueDescriptionInherited extending UniqueDescription;\n\n\ntype UniqueName_2 {\n    property name -> str {\n        constraint exclusive;\n    }\n}\n\ntype UniqueName_2_Inherited extending UniqueName_2;\n\n\ntype UniqueName_3 extending UniqueName_2 {\n    overloaded property name -> str {\n        constraint exclusive on (str_lower(__subject__));\n    }\n}\n\ntype UniqueName_4 extending UniqueName_2_Inherited;\n\ntype MultiConstraint {\n    property name -> str {\n        constraint exclusive;\n        constraint exclusive on (str_lower(__subject__));\n    }\n\n    property m1 -> str;\n}\n\ntype ParentUniqueName {\n    property name -> str {\n        constraint exclusive;\n    }\n}\n\ntype ReceivingParent {\n    property name -> str;\n}\n\ntype LosingParent extending ParentUniqueName {\n    overloaded property name -> str;\n    property lp -> str;\n}\n\ntype AbstractConstraintParent {\n    property name -> str {\n        delegated constraint exclusive;\n    }\n}\n\ntype AbstractConstraintParent2 {\n    property name -> str {\n        delegated constraint exclusive on (str_lower(__subject__));\n    }\n}\n\ntype AbstractConstraintPureChild extending AbstractConstraintParent;\n\ntype AbstractConstraintMixedChild extending AbstractConstraintParent {\n    overloaded property name -> str {\n        constraint exclusive on (str_lower(__subject__));\n    }\n}\n\ntype AbstractConstraintPropagated extending AbstractConstraintParent {\n    overloaded property name -> str {\n        delegated constraint exclusive;\n    }\n}\n\ntype AbstractConstraintParent3 {\n    property name -> str {\n        delegated constraint exclusive;\n        delegated constraint exclusive on (str_lower(__subject__));\n    }\n}\n\ntype AbstractConstraintMultipleParentsFlattening\n        extending AbstractConstraintParent, AbstractConstraintParent2 {\n    property flat -> str;\n}\n\ntype LosingAbstractConstraintParent extending AbstractConstraintParent;\n\ntype LosingAbstractConstraintParent2 extending AbstractConstraintParent;\n\ntype BecomingAbstractConstraint {\n    property name -> str {\n        constraint exclusive;\n    }\n}\n\ntype BecomingAbstractConstraintChild extending BecomingAbstractConstraint;\n\ntype BecomingConcreteConstraint {\n    property name -> str {\n        delegated constraint exclusive;\n    }\n}\n\ntype BecomingConcreteConstraintChild extending BecomingConcreteConstraint;\n\ntype PropertyContainer {\n    multi property tags -> str {\n        constraint exclusive\n    }\n}\ntype PropertyContainerChild extending PropertyContainer;\n\ntype Pair {\n    required property x -> str;\n    required property y -> str;\n    constraint exclusive on (( .x, .y ));\n}\n\ntype Indexing {\n    required property x -> str;\n    required property y -> array<int16>;\n    required property z -> json;\n    required property u -> bytes;\n    constraint exclusive on ((.x[0]));\n    constraint exclusive on ((.y[0]));\n    constraint exclusive on ((.z[0]));\n    constraint exclusive on ((.u[0]));\n}\n\ntype Slicing {\n    required property x -> str;\n    required property y -> array<int16>;\n    required property z -> json;\n    required property u -> bytes;\n    constraint exclusive on ((.x[1:3]));\n    constraint exclusive on ((.y[1:3]));\n    constraint exclusive on ((.z[1:3]));\n    constraint exclusive on ((.u[1:3]));\n}\n\nscalar type OrderStatus extending enum<open, processing, complete>;\n\ntype Order {\n    required property status -> OrderStatus;\n    constraint exclusive on ((OrderStatus.open in .status));\n}\n"
  },
  {
    "path": "tests/schemas/constraints_migration/schema.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nscalar type constraint_length extending str {\n    constraint max_len_value(16);\n    constraint max_len_value(10);\n    constraint min_len_value(5);\n    constraint min_len_value(8);\n}\n\nscalar type constraint_length_2 extending constraint_length {\n    constraint min_len_value(9);\n}\n\nscalar type constraint_minmax extending str {\n    constraint min_value(\"99900000\");\n    constraint min_value(\"99990000\");\n    constraint max_value(\"9999999989\");\n}\n\nscalar type constraint_strvalue extending str {\n    constraint expression on (__subject__[-1:] = '9');\n\n    constraint regexp(r\"^\\d+$\");\n\n    constraint expression on (__subject__[0] = '9');\n\n    constraint regexp(r\"^\\d+9{3,}.*$\");\n}\n\nscalar type constraint_enum extending str {\n   constraint one_of('foo', 'bar');\n}\n\nabstract link translated_label {\n    property lang -> str;\n    property prop1 -> str;\n}\n\nabstract link link_with_unique_property {\n    property unique_property -> str {\n        constraint exclusive;\n    }\n}\n\nabstract link link_with_unique_property_inherited\n    extending link_with_unique_property;\n\nabstract link another_link_with_unique_property {\n    property unique_property -> str {\n        constraint exclusive;\n    }\n}\n\nabstract link another_link_with_unique_property_inherited\n    extending another_link_with_unique_property;\n\ntype Label {\n    property text -> str;\n}\n\ntype Object {\n    property name -> str;\n    property c_length -> constraint_length;\n    property c_length_2 -> constraint_length_2;\n    property c_length_3 -> constraint_length_2 {\n        constraint min_len_value(10);\n    }\n\n    property c_minmax -> constraint_minmax;\n    property c_strvalue -> constraint_strvalue;\n    property c_enum -> constraint_enum;\n}\n\ntype ObjCnstr {\n    required property first_name -> str;\n    required property last_name -> str;\n    constraint exclusive on ((__subject__.first_name, __subject__.last_name));\n}\n\ntype UniqueName {\n    property name -> str {\n        constraint exclusive;\n    }\n\n    link link_with_unique_property\n        extending link_with_unique_property -> Object;\n\n    link link_with_unique_property_inherited\n        extending link_with_unique_property_inherited -> Object;\n\n    link translated_label extending translated_label -> Label {\n        constraint exclusive on ((__subject__@source, __subject__@lang));\n        constraint exclusive on (__subject__@prop1);\n    }\n}\n\ntype UniqueNameInherited extending UniqueName {\n    overloaded property name -> str;\n}\n\ntype UniqueDescription {\n    property description -> str {\n        constraint exclusive;\n    }\n\n    link another_link_with_unique_property\n        extending another_link_with_unique_property -> Object;\n\n    link another_link_with_unique_property_inherited\n        extending another_link_with_unique_property  -> Object;\n}\n\ntype UniqueDescriptionInherited extending UniqueDescription;\n\n\ntype UniqueName_2 {\n    property name -> str {\n        constraint exclusive;\n    }\n}\n\ntype UniqueName_2_Inherited extending UniqueName_2;\n\n\ntype UniqueName_3 extending UniqueName_2 {\n    overloaded property name -> str {\n        constraint exclusive on (str_lower(__subject__));\n    }\n}\n\ntype UniqueName_4 extending UniqueName_2_Inherited;\n\ntype MultiConstraint {\n    property name -> str {\n        constraint exclusive;\n        constraint exclusive on (str_lower(__subject__));\n    }\n\n    property m1 -> str;\n}\n\ntype ParentUniqueName {\n    property name -> str {\n        constraint exclusive;\n    }\n}\n\ntype ReceivingParent {\n    property name -> str;\n}\n\ntype LosingParent extending ParentUniqueName {\n    overloaded property name -> str;\n    property lp -> str;\n}\n\ntype AbstractConstraintParent {\n    property name -> str {\n        delegated constraint exclusive;\n    }\n}\n\ntype AbstractConstraintParent2 {\n    property name -> str {\n        delegated constraint exclusive on (str_lower(__subject__));\n    }\n}\n\ntype AbstractConstraintPureChild extending AbstractConstraintParent;\n\ntype AbstractConstraintMixedChild extending AbstractConstraintParent {\n    overloaded property name -> str {\n        constraint exclusive on (str_lower(__subject__));\n    }\n}\n\ntype AbstractConstraintPropagated extending AbstractConstraintParent {\n    overloaded property name -> str {\n        delegated constraint exclusive;\n    }\n}\n\ntype AbstractConstraintParent3 {\n    property name -> str {\n        delegated constraint exclusive;\n        delegated constraint exclusive on (str_lower(__subject__));\n    }\n}\n\ntype AbstractConstraintMultipleParentsFlattening\n        extending AbstractConstraintParent, AbstractConstraintParent2 {\n    property flat -> str;\n}\n\ntype LosingAbstractConstraintParent extending AbstractConstraintParent;\n\ntype LosingAbstractConstraintParent2 extending AbstractConstraintParent;\n\ntype BecomingAbstractConstraint {\n    property name -> str {\n        constraint exclusive;\n    }\n}\n\ntype BecomingAbstractConstraintChild extending BecomingAbstractConstraint;\n\ntype BecomingConcreteConstraint {\n    property name -> str {\n        delegated constraint exclusive;\n    }\n}\n\ntype BecomingConcreteConstraintChild extending BecomingConcreteConstraint;\n"
  },
  {
    "path": "tests/schemas/constraints_migration/updated_schema.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nscalar type constraint_length extending str {\n    constraint max_len_value(16);\n    constraint max_len_value(10);\n    constraint min_len_value(5);\n    constraint min_len_value(8);\n}\n\nscalar type constraint_length_2 extending constraint_length {\n    constraint min_len_value(9);\n}\n\nscalar type constraint_minmax extending str {\n    constraint min_value(\"99900000\");\n    constraint min_value(\"99990000\");\n    constraint max_value(\"9999999989\");\n}\n\nscalar type constraint_strvalue extending str {\n    constraint expression on (__subject__[-1:] = '9');\n\n    constraint regexp(r\"^\\d+$\");\n\n    constraint expression on (__subject__[0] = '9');\n\n    constraint regexp(r\"^\\d+9{3,}.*$\");\n}\n\nscalar type constraint_enum extending str {\n   constraint one_of('foo', 'bar');\n}\n\nabstract link translated_label {\n    property lang -> str;\n    property prop1 -> str;\n}\n\nabstract link link_with_unique_property {\n    property unique_property -> str {\n        constraint exclusive;\n    }\n\n    property unique_property2 -> str {\n        constraint exclusive;\n    }\n}\n\nabstract link link_with_unique_property_inherited\n    extending link_with_unique_property;\n\nabstract link another_link_with_unique_property {\n    property unique_property -> str {\n        constraint exclusive;\n    }\n}\n\nabstract link another_link_with_unique_property_inherited\n    extending another_link_with_unique_property;\n\ntype Label {\n    property text -> str;\n}\n\ntype Object {\n    property name -> str {\n        constraint exclusive;\n        constraint exclusive on (std::str_lower(__subject__));\n    }\n\n    property c_length -> constraint_length;\n    property c_length_2 -> constraint_length_2;\n    property c_length_3 -> constraint_length_2 {\n        constraint min_len_value(10);\n    }\n\n    property c_minmax -> constraint_minmax;\n    property c_strvalue -> constraint_strvalue;\n    property c_enum -> constraint_enum;\n\n    link translated_label extending translated_label -> Label {\n        constraint exclusive on ((__subject__@source, __subject__@lang));\n        constraint exclusive on (__subject__@prop1);\n    }\n}\n\ntype ObjCnstr {\n    required property first_name -> str;\n    required property last_name -> str;\n    constraint exclusive on (__subject__.first_name) {\n        errmessage := \"nope!\";\n    }\n}\n\ntype UniqueName {\n    property name -> str {\n        constraint exclusive;\n    }\n    property name2 -> str {\n        constraint exclusive;\n    }\n\n    link link_with_unique_property\n        extending link_with_unique_property -> Object;\n\n    link link_with_unique_property_inherited\n        extending link_with_unique_property_inherited -> Object;\n\n    link translated_label extending translated_label -> Label {\n        constraint exclusive on ((__subject__@source, __subject__@lang));\n        constraint exclusive on (__subject__@prop1);\n    }\n}\n\ntype UniqueNameInherited extending UniqueName {\n    overloaded property name -> str;\n}\n\ntype UniqueNameGrandchild extending UniqueNameInherited;\n\ntype UniqueDescription {\n    property description -> str;\n\n    link another_link_with_unique_property\n        extending another_link_with_unique_property -> Object;\n\n    link another_link_with_unique_property_inherited\n        extending another_link_with_unique_property_inherited -> Object;\n}\n\ntype UniqueDescriptionInherited extending UniqueDescription;\n\ntype UniqueName_2_Renamed {\n    property name -> str {\n        constraint exclusive;\n    }\n}\n\ntype UniqueName_2_Inherited extending UniqueName_2_Renamed;\n\ntype UniqueName_3 extending UniqueName_2_Renamed;\n\ntype UniqueName_4 extending UniqueName_2_Inherited;\n\ntype MultiConstraint_Renamed {\n    property name -> str {\n        constraint exclusive;\n        constraint exclusive on (str_lower(__subject__));\n    }\n\n    property m1 -> str;\n}\n\ntype ParentUniqueName {\n    property name -> str {\n        constraint exclusive;\n    }\n}\n\ntype ReceivingParent extending ParentUniqueName {\n    overloaded property name -> str;\n}\n\ntype LosingParent {\n    property name -> str;\n    property lp -> str;\n}\n\ntype AbstractConstraintParent {\n    property name -> str {\n        delegated constraint exclusive;\n    }\n}\n\ntype AbstractConstraintParent2 {\n    property name -> str {\n        delegated constraint exclusive on (str_lower(__subject__));\n    }\n}\n\ntype AbstractConstraintPureChild extending AbstractConstraintParent;\n\ntype AbstractConstraintMixedChild extending AbstractConstraintParent {\n    overloaded property name -> str {\n        constraint exclusive on (str_lower(__subject__));\n    }\n}\n\ntype AbstractConstraintPropagated extending AbstractConstraintParent {\n    overloaded property name -> str {\n        delegated constraint exclusive;\n    }\n}\n\ntype AbstractConstraintParent3 {\n    property name -> str {\n        delegated constraint exclusive;\n        delegated constraint exclusive on (str_lower(__subject__));\n    }\n}\n\ntype AbstractConstraintMultipleParentsFlattening\n        extending AbstractConstraintParent, AbstractConstraintParent2 {\n    property flat -> str;\n}\n\ntype LosingAbstractConstraintParent {\n    property name -> str {\n        constraint exclusive;\n    }\n}\n\ntype LosingAbstractConstraintParent2 {\n    property name -> str;\n}\n\ntype BecomingAbstractConstraint {\n    property name -> str {\n        delegated constraint exclusive;\n    }\n}\n\ntype BecomingAbstractConstraintChild extending BecomingAbstractConstraint;\n\ntype BecomingConcreteConstraint {\n    property name -> str {\n        constraint exclusive;\n    }\n}\n\ntype BecomingConcreteConstraintChild extending BecomingConcreteConstraint;\n"
  },
  {
    "path": "tests/schemas/dump01_default.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nabstract annotation user_anno;\nabstract inheritable annotation heritable_user_anno;\n\nfunction user_func_0(x: int64) -> str {\n    using (\n        SELECT 'func' ++ <str>x\n    );\n    annotation title := 'user_func(int64) -> str';\n    volatility := 'Immutable';\n};\n\nfunction user_func_1(x: array<int64>, y: str) -> str {\n    using (\n        SELECT array_join(<array<str>>x, y)\n    );\n    volatility := 'Stable';\n};\n\nfunction user_func_2(x: OPTIONAL int64, y: str = 'x') -> SET OF str {\n    using (\n        SELECT {<str>x, y}\n    );\n    volatility := 'Immutable';\n};\n\ntype A {\n    annotation title := 'A';\n\n    property p_bool -> bool {\n        annotation title := 'single bool';\n    }\n    property p_str -> str;\n    property p_datetime -> datetime;\n    property p_local_datetime -> cal::local_datetime;\n    property p_local_date -> cal::local_date;\n    property p_local_time -> cal::local_time;\n    property p_duration -> duration;\n    property p_int16 -> int16;\n    property p_int32 -> int32;\n    property p_int64 -> int64;\n    property p_float32 -> float32;\n    property p_float64 -> float64;\n    property p_bigint -> bigint;\n    property p_decimal -> decimal;\n    property p_json -> json;\n    property p_bytes -> bytes;\n}\n\n\ntype B {\n    annotation title := 'B';\n\n    required multi property p_bool -> bool {\n        annotation title := 'multi bool';\n    }\n    required multi property p_str -> str;\n    required multi property p_datetime -> datetime;\n    required multi property p_local_datetime -> cal::local_datetime;\n    required multi property p_local_date -> cal::local_date;\n    required multi property p_local_time -> cal::local_time;\n    required multi property p_duration -> duration;\n    required multi property p_int16 -> int16;\n    required multi property p_int32 -> int32;\n    required multi property p_int64 -> int64;\n    required multi property p_float32 -> float32;\n    required multi property p_float64 -> float64;\n    required multi property p_bigint -> bigint;\n    required multi property p_decimal -> decimal;\n    required multi property p_json -> json;\n    required multi property p_bytes -> bytes;\n}\n\n\ntype C {\n    annotation title := 'C';\n    required property val -> str {\n        annotation title := 'val';\n        constraint exclusive {\n            annotation title := 'exclusive C val';\n        }\n    }\n}\n\n\ntype D {\n    annotation title := 'D';\n    annotation user_anno := 'D only';\n    annotation heritable_user_anno := 'all D';\n\n    required property num -> int64;\n\n    link single_link -> C {\n        annotation title := 'single link to C';\n    }\n    multi link multi_link -> C {\n        annotation title := 'multi link to C';\n    }\n}\n\n\ntype E extending D {\n    annotation title := 'E';\n\n    overloaded link single_link -> C {\n        property lp0 -> str {\n            annotation title := 'single lp0';\n        }\n    }\n    overloaded multi link multi_link -> C {\n        property lp1 -> str {\n            annotation title := 'single lp1';\n        }\n    }\n}\n\n\ntype F extending D {\n    annotation title := 'F';\n\n    overloaded required link single_link -> C;\n    overloaded required multi link multi_link -> C;\n}\n\n\ntype G {\n    required property g0 -> str {\n        default := 'fixed';\n    };\n    required property g1 -> str {\n        default := user_func_0(1);\n    };\n    required property g2 -> str {\n        default := to_str(2);\n    };\n}\n\n\ntype H {\n    property h0 := 'fixed';\n    property h1 := user_func_0(1);\n    property h2 := to_str(2);\n}\n\n\ntype I {\n    required link i0 -> C {\n        default := (SELECT C FILTER .val = 'D00' LIMIT 1);\n    };\n    required link i1 -> C {\n        default := (\n            SELECT C FILTER .val = 'D0' ++ user_func_0(1)[-1] LIMIT 1\n        );\n    };\n    required link i2 -> C {\n        default := (\n            SELECT C FILTER .val = array_join(['D', '0', '2'], '') LIMIT 1\n        );\n    };\n}\n\n\ntype J {\n    link j0 := (SELECT C FILTER .val = 'D00' LIMIT 1);\n    link j1 := (\n        SELECT C FILTER .val = 'D0' ++ user_func_0(1)[-1] LIMIT 1\n    );\n    link j2 := (\n        SELECT C FILTER .val = array_join(['D', '0', '2'], '') LIMIT 1\n    );\n}\n\n\n# indexes\ntype K {\n    required property k -> str;\n    index on (.k);\n}\n\n\ntype L {\n    required property l0 -> str;\n    required property l1 -> str;\n    index on (.l0 ++ .l1);\n}\n\n\n# constraints\nabstract constraint user_int_constr(x: int64) {\n    using (__subject__ > x);\n    errmessage := '{__subject__} must be greater than {x}';\n    annotation title := 'user_int_constraint constraint'\n}\n\nscalar type UserInt extending int64 {\n    annotation title := 'UserInt scalar';\n    constraint user_int_constr(5);\n}\n\n\nscalar type UserStr extending str {\n    annotation title := 'UserStr scalar';\n    constraint max_len_value(5);\n}\n\n\ntype M {\n    required property m0 -> int64 {\n        constraint user_int_constr(3);\n    }\n    required property m1 -> str {\n        constraint max_len_value(3);\n    }\n}\n\n\ntype N {\n    required property n0 -> UserInt;\n    required property n1 -> UserStr;\n}\n\n\n# enum\nscalar type UserEnum extending enum<'Lorem', 'ipsum', 'dolor', 'sit', 'amet'>;\n\n\ntype O {\n    required property o0 -> UserEnum;\n    required property o1 -> UserEnum {\n        default := <UserEnum>'Lorem';\n    };\n    property o2 := <UserEnum>'dolor';\n}\n\n\n# collection props\ntype P {\n    required link plink0 -> C {\n        property p0 -> array<str>;\n    };\n    required link plink1 -> C {\n        property p1 -> array<float64>;\n    };\n    required property p2 -> array<str>;\n    required property p3 -> array<float64>;\n}\n\n\ntype Q {\n    required property q0 -> tuple<int64, bool>;\n    required property q1 -> tuple<str, decimal>;\n    required property q2 -> tuple<x: int64, y: bool>;\n    required property q3 -> tuple<x: str, y: decimal>;\n}\n\n\n# inheritance and delegated constraint\nabstract type R {\n    required property name -> str {\n        delegated constraint exclusive;\n    }\n}\n\n\ntype S extending R {\n    required property s -> str;\n}\n\n\ntype T extending R {\n    required property t -> str;\n}\n\n\nabstract type U {\n    required property u -> str;\n}\n\n\ntype V extending U, S, T;\n\n\n# aliases\nalias AliasP := P {\n    name := 'alias P',\n    p2 := .p2 ++ ['!'],\n    f := F {\n        k := (SELECT K LIMIT 1)\n    }\n};\n\n\nalias Primes := {2, 3, 5, 7};\n\n\n# self-referential and mutually-referential types\ntype W {\n    required property name -> str {\n        constraint exclusive;\n    }\n    link w -> W;\n}\n\n\ntype X {\n    required property name -> str;\n    link y -> Y;\n}\n\n\ntype Y {\n    required property name -> str;\n    link x -> X;\n}\n\n\n# link target as a union type\ntype Z {\n    # have only 'id' in common\n    link ck -> C | K;\n    # have 'name' in common\n    multi link stw -> S | T | W;\n}\n\n\n# cross-module references\ntype DefA extending test::TestA;\n\n\ntype DefB {\n    required property name -> str {\n        default := test::user_func_3(0);\n    }\n    link other -> test::TestB;\n}\n\n\ntype DefC {\n    required property name -> str {\n        default := test::user_func_3(1);\n    }\n    link other -> test::TestC;\n}\n\n\n# on-target delete restrictions\ntype TargetA {\n    required property name -> str {\n        constraint exclusive;\n    }\n}\n\ntype SourceA {\n    required property name -> str {\n        constraint exclusive;\n    }\n\n    link link0 -> TargetA {\n        on target delete restrict;\n    };\n    link link1 -> TargetA {\n        on target delete delete source;\n    };\n    link link2 -> TargetA {\n        on target delete allow;\n    };\n    link link3 -> TargetA {\n        on target delete deferred restrict;\n    };\n};\n\n\n# read-only links and props\ntype ROPropsA {\n    required property name -> str {\n        constraint exclusive;\n    }\n\n    property rop0 -> int64 {\n        readonly := True;\n    }\n    required property rop1 -> int64 {\n        readonly := True;\n        default := <int64>round(10 * random());\n    }\n}\n\n\ntype ROLinksA {\n    required property name -> str {\n        constraint exclusive;\n    }\n\n    link rol0 -> C {\n        readonly := True;\n    }\n    required link rol1 -> C {\n        readonly := True;\n        default := (SELECT C FILTER .val = 'D00');\n    }\n    required multi link rol2 -> C {\n        readonly := True;\n        default := (SELECT C FILTER .val IN {'D01', 'D02'});\n    }\n}\n\n\ntype ROLinksB {\n    required property name -> str {\n        constraint exclusive;\n    }\n\n    link rol0 -> C {\n        property rolp00 -> int64 {\n            readonly := True;\n        }\n        property rolp01 -> int64 {\n            readonly := True;\n            default := <int64>round(10 * random());\n        }\n    }\n    multi link rol1 -> C {\n        property rolp10 -> int64 {\n            readonly := True;\n        }\n        property rolp11 -> int64 {\n            readonly := True;\n            default := <int64>round(10 * random());\n        }\n    }\n}\n"
  },
  {
    "path": "tests/schemas/dump01_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nSET MODULE default;\n\nINSERT A {\n    p_bool := True,\n    p_str := 'Hello',\n    p_datetime := <datetime>'2018-05-07T20:01:22.306916+00:00',\n    p_local_datetime := <cal::local_datetime>'2018-05-07T20:01:22.306916',\n    p_local_date := <cal::local_date>'2018-05-07',\n    p_local_time := <cal::local_time>'20:01:22.306916',\n    p_duration := <duration>'20 hrs',\n    p_int16 := 12345,\n    p_int32 := 1234567890,\n    p_int64 := 1234567890123,\n    p_float32 := 2.5,\n    p_float64 := 2.5,\n    p_bigint := 123456789123456789123456789n,\n    p_decimal := 123456789123456789123456789.123456789123456789123456789n,\n    p_json := to_json('[{\"a\": null, \"b\": true}, 1, 2.5, \"foo\"]'),\n    p_bytes := b'Hello',\n};\n\n\nINSERT B {\n    p_bool := {True, False},\n    p_str := {'Hello', 'world'},\n    p_datetime := {\n        <datetime>'2018-05-07T20:01:22.306916+00:00',\n        <datetime>'2019-05-07T20:01:22.306916+00:00',\n    },\n    p_local_datetime := {\n        <cal::local_datetime>'2018-05-07T20:01:22.306916',\n        <cal::local_datetime>'2019-05-07T20:01:22.306916',\n    },\n    p_local_date := {\n        <cal::local_date>'2018-05-07',\n        <cal::local_date>'2019-05-07',\n    },\n    p_local_time := {\n        <cal::local_time>'20:01:22.306916',\n        <cal::local_time>'20:02:22.306916',\n    },\n    p_duration := {<duration>'20 hrs', <duration>'20 sec'},\n    p_int16 := {12345, -42},\n    p_int32 := {1234567890, -42},\n    p_int64 := {1234567890123, -42},\n    p_float32 := {2.5, -42},\n    p_float64 := {2.5, -42},\n    p_bigint := {\n        123456789123456789123456789n,\n        -42n,\n    },\n    p_decimal := {\n        123456789123456789123456789.123456789123456789123456789n,\n        -42n,\n    },\n    p_json := {\n        to_json('[{\"a\": null, \"b\": true}, 1, 2.5, \"foo\"]'),\n        <json>'bar',\n        <json>False,\n    },\n    p_bytes := {b'Hello', b'world'},\n};\n\n\nFOR x IN {{'D', 'E', 'F'} ++ {'00', '01', '02', '03'}}\nUNION (\n    INSERT C {\n        val := x,\n    }\n);\n\n\nINSERT D {\n    num := 0,\n};\nINSERT D {\n    num := 1,\n    single_link := (SELECT C FILTER .val = 'D00'),\n};\nINSERT D {\n    num := 2,\n    multi_link := (SELECT C FILTER .val IN DISTINCT {'D01', 'D02'}),\n};\nINSERT D {\n    num := 3,\n    single_link := (SELECT C FILTER .val = 'D00'),\n    multi_link := (SELECT C FILTER .val IN DISTINCT {'D01', 'D02', 'D03'}),\n};\n\n\nINSERT E {\n    num := 4,\n};\nINSERT E {\n    num := 5,\n    single_link := (SELECT C FILTER .val = 'E00'),\n};\nINSERT E {\n    num := 6,\n    multi_link := (SELECT C FILTER .val IN DISTINCT {'E01', 'E02'}),\n};\nINSERT E {\n    num := 7,\n    single_link := (\n        WITH val := 'E00'\n        SELECT C {@lp0 := val}\n        FILTER .val = val\n    ),\n    multi_link := (\n        FOR val IN (DISTINCT {'E01', 'E02', 'E03'})\n        UNION (\n            SELECT C {@lp1 := val}\n            FILTER .val = val\n        )\n    ),\n};\n\n\nINSERT F {\n    num := 8,\n    single_link := (SELECT C FILTER .val = 'F00'),\n    multi_link := (SELECT C FILTER .val IN DISTINCT {'F01', 'F02', 'F03'}),\n};\n\n\nINSERT G;\nINSERT H;\nINSERT I;\nINSERT J;\n\nINSERT K {k := 'k0'};\nINSERT L {l0 := 'l0_0', l1 := 'l1_0'};\n\nINSERT M {m0 := 10, m1 := 'm1'};\nINSERT N {n0 := 10, n1 := 'n1'};\n\nINSERT O {o0 := 'ipsum'};\n\nINSERT P {\n    plink0 := (\n        SELECT C{@p0 := ['hello', 'world']} FILTER .val = 'E00'\n    ),\n    plink1 := (\n        SELECT C{@p1 := [2.5, -4.25]} FILTER .val = 'E00'\n    ),\n    p2 := ['hello', 'world'],\n    p3 := [2.5, -4.25],\n};\n\nINSERT Q {\n    q0 := (2, False),\n    q1 := ('p3', 3.33n),\n    q2 := (x := 2, y := False),\n    q3 := ('p11', 3.33n),\n};\n\nINSERT S {name:= 'name0', s := 's0'};\nINSERT T {name:= 'name0', t := 't0'};\nINSERT V {name:= 'name1', s := 's1', t := 't1', u := 'u1'};\n\nINSERT W {name := 'w0'};\nINSERT W {name := 'w2'};\nINSERT W {name := 'w1', w := (SELECT DETACHED W FILTER .name = 'w2')};\nINSERT W {name := 'w3'};\nINSERT W {name := 'w4', w := (SELECT DETACHED W FILTER .name = 'w3')};\nUPDATE W\nFILTER .name = 'w3'\nSET {\n    w := (SELECT DETACHED W FILTER .name = 'w4')\n};\n\nINSERT X {name := 'x0'};\nINSERT Y {name := 'y0', x := (SELECT X LIMIT 1)};\nUPDATE X SET {y := (SELECT Y LIMIT 1)};\n\nINSERT Z {\n    ck := (SELECT C FILTER .val = 'F00'),\n    stw := (SELECT S FILTER .name = 'name0'),\n};\nINSERT Z {\n    ck := (SELECT K LIMIT 1),\n    stw := {\n        (SELECT S FILTER .name = 'name0'),\n        (SELECT W FILTER .name = 'w1' LIMIT 1),\n        (SELECT T FILTER .name = 'name0'),\n    }\n};\n\n# cross-module data\nINSERT DefA {a := 'DefA'};\nINSERT test::TestB {b := 'TestB', blink := (SELECT DefA LIMIT 1)};\nINSERT DefB {other := (SELECT test::TestB LIMIT 1)};\nINSERT test::TestC {c := 'TestC'};\nINSERT DefC {other := (SELECT test::TestC LIMIT 1)};\nUPDATE test::TestC\nSET {clink := (SELECT DefC LIMIT 1)};\n\n# on delete\nINSERT TargetA {name := 't0'};\nINSERT TargetA {name := 't1'};\nINSERT TargetA {name := 't2'};\nINSERT TargetA {name := 't3'};\nINSERT SourceA {name := 's0', link0 := (SELECT TargetA FILTER .name = 't0')};\nINSERT SourceA {name := 's1', link1 := (SELECT TargetA FILTER .name = 't1')};\nINSERT SourceA {name := 's2', link2 := (SELECT TargetA FILTER .name = 't2')};\nINSERT SourceA {name := 's3', link3 := (SELECT TargetA FILTER .name = 't3')};\n\n# read-only\nINSERT ROPropsA {name := 'ro0'};\nINSERT ROPropsA {name := 'ro1', rop0 := 100};\nINSERT ROPropsA {name := 'ro2', rop1 := -2};\n\nINSERT ROLinksA {name := 'ro0'};\nINSERT ROLinksA {name := 'ro1', rol0 := (SELECT C FILTER .val = 'F00')};\nINSERT ROLinksA {name := 'ro2', rol1 := (SELECT C FILTER .val = 'F00')};\nINSERT ROLinksA {\n    name := 'ro3', rol2 := (SELECT C FILTER .val IN {'F01', 'F02'})\n};\n\nINSERT ROLinksB {\n    name := 'ro0',\n    rol0 := (SELECT C FILTER .val = 'D00'),\n    rol1 := (SELECT C FILTER .val IN {'D01', 'D02'}),\n};\nINSERT ROLinksB {\n    name := 'ro1',\n    rol0 := (SELECT C{@rolp00 := 99} FILTER .val = 'D00'),\n    rol1 := (\n        SELECT C{@rolp10 := 100 - <int64>.val[-1]}\n        FILTER .val IN {'D01', 'D02'}\n    ),\n};\nINSERT ROLinksB {\n    name := 'ro2',\n    rol0 := (SELECT C{@rolp01 := -10} FILTER .val = 'E00'),\n    rol1 := (\n        SELECT C{@rolp11 := -<int64>.val[-1]}\n        FILTER .val IN {'E01', 'E02'}\n    ),\n};\n"
  },
  {
    "path": "tests/schemas/dump01_test.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfunction user_func_3(x: int64) -> str {\n    using (\n        SELECT 'test' ++ <str>x\n    );\n    volatility := 'Immutable';\n};\n\n# cross-module references\ntype TestA {\n    required property a -> str;\n}\n\n\ntype TestB {\n    required property b -> str;\n    link blink -> TestA;\n}\n\n\ntype TestC {\n    required property c -> str;\n    link clink -> default::DefC;\n}\n"
  },
  {
    "path": "tests/schemas/dump02_default.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nabstract annotation `🍿`;\n\nabstract constraint `🚀🍿`(max: int64) extending max_len_value;\n\nfunction `💯`(NAMED ONLY `🙀`: int64) -> int64 {\n    using (\n        SELECT 100 - `🙀`\n    );\n\n    annotation `🍿` := 'fun!🚀';\n    volatility := 'Immutable';\n}\n\ntype `S p a M` {\n    required property `🚀` -> int32;\n    property c100 := (SELECT `💯`(`🙀` := .`🚀`));\n}\n\ntype A {\n    required link `s p A m 🤞` -> `S p a M`;\n}\n\nscalar type 你好 extending str;\n\nscalar type مرحبا extending 你好 {\n    constraint `🚀🍿`(100);\n};\n\nscalar type `🚀🚀🚀` extending مرحبا;\n\ntype Łukasz {\n    required property `Ł🤞` -> `🚀🚀🚀` {\n        default := <`🚀🚀🚀`>'你好🤞'\n    }\n    index on (.`Ł🤞`);\n\n    link `Ł💯` -> A {\n        property `🙀🚀🚀🚀🙀` -> `🚀🚀🚀`;\n        property `🙀مرحبا🙀` -> مرحبا {\n            constraint `🚀🍿`(200);\n        }\n    };\n}\n\ntype Tree {\n    required property val -> str {\n        constraint exclusive;\n    };\n\n    link parent -> Tree;\n    link children := .<parent[IS Tree];\n    property child_vals := .children.val;\n}\n\n# DML containing functions are prohibited in b1+, but we still\n# allow them in historical dumps to preserve continuity until\n# we figure out a migration path to mutation callables.\nfunction insert_tree() -> Tree using (\n    INSERT Tree { val := 'foo' }\n);\n"
  },
  {
    "path": "tests/schemas/dump02_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nSET MODULE default;\n\nCREATE MIGRATION m1t2phsw6j2rgl4ieihm6mnvoln3ssayxncjzl2kwkxmunn2f6aqha\nONTO m1iej6dr3hk33wykqwqgg4xxo3tivpiznpb2mto7qsw2zgipsbfihq {\n    CREATE TYPE default::Migrated;\n    create type default::Migrated2 {};\n};\n\n# Not sure if the esdl filename will handle this on all systems, so\n# I'm adding stuff here.\nCREATE MODULE `💯💯💯`;\n\nCREATE FUNCTION `💯💯💯`::`🚀🙀🚀`(`🤞`: default::`🚀🚀🚀`) -> `🚀🚀🚀`\nUSING (\n    SELECT <`🚀🚀🚀`>(`🤞` ++ 'Ł🙀')\n);\n# end of DDL\n\nINSERT `S p a M` {\n    `🚀` := 42\n};\n\nINSERT A {\n    `s p A m 🤞` := assert_single((SELECT `S p a M` FILTER .`🚀` = 42))\n};\n\nINSERT Łukasz;\n\nINSERT Łukasz {\n    `Ł🤞` := 'simple 🚀',\n    `Ł💯` := (\n        SELECT A\n        # {\n        #     `🙀🚀🚀🚀🙀`:= 'Łink prop 🙀🚀🚀🚀🙀',\n        #     `🙀مرحبا🙀`:=\n        #         `💯💯💯`::`🚀🙀🚀`('Łink prop 🙀مرحبا🙀'),\n        # }\n        FILTER .`s p A m 🤞`.`🚀` = 42\n        LIMIT 1\n    )\n};\n"
  },
  {
    "path": "tests/schemas/dump03_default.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nscalar type MyStr extending str;\nscalar type MySeq extending sequence;\nscalar type MyPristineSeq extending sequence;\n\ntype Test {\n    required property name -> str {\n        constraint exclusive;\n    };\n    property array_of_tuples -> array<tuple<int64, MyStr, int64>>;\n    property tuple_of_arrays ->\n        tuple<\n            MyStr,\n            array<MyStr>,\n            tuple<int64, int64, array<MyStr>>,\n        >;\n    property seq -> MySeq;\n};\n"
  },
  {
    "path": "tests/schemas/dump03_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nSET MODULE default;\n\nINSERT Test {\n    name := 'test01',\n    array_of_tuples := [(1, '2', 3), (4, '5', 6)],\n    tuple_of_arrays := ('1', ['2', '3'], (4, 5, ['6'])),\n};\n"
  },
  {
    "path": "tests/schemas/dump_v2_default.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ntype Test1 {\n    property t1 -> array<tuple<name: str, severity: int16>> {\n        # https://github.com/edgedb/edgedb/issues/2606\n        default := <array<tuple<name: str, severity: int16>>>[]\n    };\n};\n\ntype Test2 {\n    property range_of_int -> range<int64>;\n    property range_of_date -> range<datetime>;\n    property date_duration -> cal::date_duration;\n\n    access policy test allow all using (true);\n};\n\nglobal foo -> str;\nrequired global bar -> int64 {\n    default := -1;\n};\nglobal baz := (select TargetA filter .name = global foo);\n\ntype TargetA {\n    required property name -> str {\n        constraint exclusive;\n    }\n}\n\ntype SourceA {\n    required property name -> str {\n        constraint exclusive;\n    }\n\n    link link1 -> TargetA {\n        on source delete delete target;\n    };\n    link link2 -> TargetA {\n        on source delete delete target if orphan;\n    };\n};\n"
  },
  {
    "path": "tests/schemas/dump_v2_setup.edgeql",
    "content": "INSERT Test1;  # https://github.com/edgedb/edgedb/issues/2606\n\nINSERT Test2 {\n    range_of_int := range(-1, 10),\n    range_of_date := range(\n        <datetime>'2010-12-27T23:59:59-07:00',\n        <datetime>'2012-12-27T23:59:59-07:00',\n    ),\n    date_duration := <cal::date_duration>'1month 3days',\n};\n\nINSERT TargetA {name := 't0'};\nINSERT TargetA {name := 't1'};\nINSERT TargetA {name := 't2'};\n\nINSERT SourceA {name := 's0', link1 := (SELECT TargetA FILTER .name = 't0')};\nINSERT SourceA {name := 's1', link1 := (SELECT TargetA FILTER .name = 't1')};\nINSERT SourceA {name := 's2', link1 := (SELECT TargetA FILTER .name = 't1')};\nINSERT SourceA {name := 's3', link2 := (SELECT TargetA FILTER .name = 't2')};\nINSERT SourceA {name := 's4', link2 := (SELECT TargetA FILTER .name = 't2')};\n"
  },
  {
    "path": "tests/schemas/dump_v3_default.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nmodule nested { type T }\nmodule `back``ticked` { type T }\n"
  },
  {
    "path": "tests/schemas/dump_v3_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# If the hashes break, it is fine to change them, I think?\nCREATE MIGRATION m1xpafeaeinvq562zlqkqgcjgdpqds45jr6eybmxm5kzmpzadvvamq\nONTO m1nnh3uhlwn5vfe7dfhyyxxjafsxniljyuzov6avzqeyddw2qpkw7q {\n    SET message := \"test\";\n    CREATE TYPE default::Test1;\n};\n\nCREATE TYPE default::Test2;\n\ncreate type Log {\n    create property message -> str;\n    create property timestamp -> float64 {\n        create rewrite insert, update using (random())\n    };\n    create access policy whatever allow all;\n    create access policy whatever_no deny insert using (false) {\n        set errmessage := \"aaaaaa\";\n    };\n};\n\ncreate type Foo {\n    create property name -> str;\n    create trigger log after insert for each do (\n        insert Log {\n            message := __new__.name\n        }\n    );\n};\n\nconfigure current database set allow_user_specified_id := true;\nconfigure current database set query_execution_timeout :=\n  <std::duration>'1 hour 20 minutes 13 seconds';\n"
  },
  {
    "path": "tests/schemas/dump_v4_default.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nscalar type v3 extending ext::pgvector::vector<3>;\n\ntype L2 {\n    required vec: v3;\n    index ext::pgvector::ivfflat_euclidean(lists := 100) on (.vec);\n}\n\ntype L3 {\n    property x: str;\n\n    index fts::index on (fts::with_options(.x, language := fts::Language.eng));\n}\n"
  },
  {
    "path": "tests/schemas/dump_v4_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nset module default;\n\nfor x in range_unpack(range(1, 1000))\nunion (\n    # Large, varied, but deterministic dataset.\n    insert L2 {vec := [x % 10, math::ln(x), x / 7 % 13]}\n);\n\n\nCONFIGURE CURRENT DATABASE SET ext::_conf::Config::config_name := 'ready';\nCONFIGURE CURRENT DATABASE SET ext::_conf::Config::secret := 'secret';\n\nCONFIGURE CURRENT DATABASE INSERT ext::_conf::Obj {\n    name := '1',\n    value := 'foo',\n};\nCONFIGURE CURRENT DATABASE INSERT ext::_conf::Obj {\n    name := '2',\n    value := 'bar',\n};\nCONFIGURE CURRENT DATABASE INSERT ext::_conf::SubObj {\n    extra := 42,\n    name := '3',\n    value := 'baz',\n};\nCONFIGURE CURRENT DATABASE INSERT ext::_conf::SecretObj {\n    name := '4',\n    value := 'spam',\n    secret := '123456',\n};\n\n# Lots of ext::auth config\nCONFIGURE CURRENT DATABASE SET\next::auth::AuthConfig::auth_signing_key := 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';\n\nCONFIGURE CURRENT DATABASE SET\next::auth::AuthConfig::token_time_to_live := <duration>'24 hours';\n\n# N.B: This CONFIGURE command was the original one, but then we\n# removed that flag.  We kept it working in dumps, though, so old\n# dumps still work and behave as if they had the next two statements\n# instead.\n#\n# CONFIGURE CURRENT DATABASE SET\n# ext::auth::SMTPConfig::sender := 'noreply@example.com';\n\nCONFIGURE CURRENT DATABASE INSERT cfg::SMTPProviderConfig {\n    name := \"_default\",\n    sender := 'noreply@example.com',\n};\n\nCONFIGURE CURRENT DATABASE SET\ncfg::current_email_provider_name := \"_default\";\n\n\nCONFIGURE CURRENT DATABASE SET\next::auth::AuthConfig::allowed_redirect_urls := {\n    'https://example.com'\n};\n\nCONFIGURE CURRENT DATABASE\nINSERT ext::auth::GitHubOAuthProvider {\n    secret := 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',\n    client_id := '12f25e85-8659-41a5-87f5-9024e8b057d0',\n};\n\nCONFIGURE CURRENT DATABASE\nINSERT ext::auth::GoogleOAuthProvider {\n    secret := 'cccccccccccccccccccccccccccccccc',\n    client_id := '798dcc1b-ab29-4aa1-9d8c-dae9f01444f2',\n};\n\nCONFIGURE CURRENT DATABASE\nINSERT ext::auth::AzureOAuthProvider {\n    secret := 'cccccccccccccccccccccccccccccccc',\n    client_id := '1597b3fc-b67d-4d2b-b38f-acc256341dbc',\n    additional_scope := 'offline_access',\n};\n\nCONFIGURE CURRENT DATABASE\nINSERT ext::auth::AppleOAuthProvider {\n    secret := 'cccccccccccccccccccccccccccccccc',\n    client_id := 'aaf279c6-6c6e-4815-9849-d7a912d26e3b',\n};\n\nCONFIGURE CURRENT DATABASE\nINSERT ext::auth::EmailPasswordProviderConfig {\n    require_verification := false,\n};\n\nCONFIGURE CURRENT DATABASE INSERT ext::auth::UIConfig {\n    redirect_to := 'http://example.edgedb.com'\n};\n\nINSERT L3 { x := 'satisfied customer' };\n"
  },
  {
    "path": "tests/schemas/dump_v5_default.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nscalar type v3 extending ext::pgvector::vector<3>;\n\ntype L2 {\n    required vec: v3;\n    index ext::pgvector::hnsw_cosine(m := 2, ef_construction := 4) on (.vec);\n}\n\ntype TestEmbeddingModel\n    extending ext::ai::EmbeddingModel\n{\n    annotation ext::ai::model_name := \"text-embedding-test\";\n    annotation ext::ai::model_provider := \"custom::test\";\n    annotation ext::ai::embedding_model_max_input_tokens := \"8191\";\n    annotation ext::ai::embedding_model_max_batch_tokens := \"16384\";\n    annotation ext::ai::embedding_model_max_output_dimensions := \"10\";\n    annotation ext::ai::embedding_model_supports_shortening := \"true\";\n};\n\ntype Astronomy {\n    content: str;\n    deferred index ext::ai::index(embedding_model := 'text-embedding-test')\n        on (.content);\n\n    # N.B: This was added late, for 5.5, so won't appear in 5.0 dumps.\n    # Test that having both AI and FTS indexes works.\n    index fts::index on (\n      fts::with_options(.content, language := fts::Language.eng));\n};\n"
  },
  {
    "path": "tests/schemas/dump_v5_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nset module default;\n\nfor x in range_unpack(range(1, 1000))\n# Large, varied, but deterministic dataset.\ninsert L2 {vec := [x % 10, math::ln(x), x / 7 % 13]};\n\n# set the ef_search extension config value\nconfigure current database set\next::pgvector::Config::ef_search := 5;\n\ninsert Astronomy {\n    content := 'Skies on Mars are red'\n};\ninsert Astronomy {\n    content := 'Skies on Earth are blue'\n};\n"
  },
  {
    "path": "tests/schemas/dump_v6_default.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ntype Foo {\n  required name: str;\n}\n\nfunction insert_foo(name: str) -> Foo using (\n  insert Foo { name := name }\n);\n"
  },
  {
    "path": "tests/schemas/dump_v6_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n"
  },
  {
    "path": "tests/schemas/dump_v7_default.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\npermission foo;\npermission bar;\npermission baz;\n\ntype T {\n  access policy foo allow all using (global foo);\n};\n\nfunction f() -> int64 {\n  using (count(T));\n  required_permissions := {bar, baz};\n}\n\nfunction g() -> bool {\n  using (global foo);\n  required_permissions := bar;\n}\n\nalias Nested := [[1]];\n"
  },
  {
    "path": "tests/schemas/dump_v7_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n"
  },
  {
    "path": "tests/schemas/enums.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nscalar type color_enum_t extending enum<'RED', 'GREEN', 'BLUE'>;\n\ntype Foo {\n    required property color -> color_enum_t;\n}\n\n\ntype Bar {\n    required property color -> color_enum_t {\n        default := 'RED';\n    }\n}\n"
  },
  {
    "path": "tests/schemas/explain.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nabstract type Text {\n    # This is an abstract object containing text.\n    required body: str {\n        # Maximum length of text is 10000 characters.\n        constraint max_len_value(10000);\n    }\n}\n\nabstract type Named {\n    required name: str;\n}\n\n# Dictionary is a NamedObject variant, that enforces\n# name uniqueness across all instances if its subclass.\nabstract type Dictionary extending Named {\n    overloaded required name: str {\n        delegated constraint exclusive;\n    }\n    index on (__subject__.name);\n}\n\ntype User extending Dictionary {\n    multi todo: Issue {\n        rank: int64 {\n            default := 42;\n        }\n    }\n    multi link owned_issues := .<owner[is Issue];\n    rank: int64 {\n        default := 0;\n    }\n}\n\nabstract type Owned {\n    # By default links are optional.\n    required owner: User {\n        note: str;\n    }\n}\n\ntype Status extending Dictionary;\n\ntype LogEntry extending Owned, Text {\n    # LogEntry is an Owned and a Text, so it\n    # will have all of their links and attributes,\n    # in particular, owner and text links.\n    required spent_time: int64;\n}\n\nscalar type issue_num_t extending std::str;\n\ntype Comment extending Text, Owned {\n    required issue: Issue;\n    optional parent: Comment;\n}\n\nfunction frob(s: str) -> str using (s ++ '!');\n\ntype Issue extending Named, Owned, Text {\n    overloaded required link owner {\n        property since: datetime;\n    }\n\n    required number: issue_num_t {\n        readonly := true;\n        constraint exclusive;\n    }\n    required status: Status;\n\n    optional multi watchers: User;\n\n    optional time_estimate: int64;\n\n    multi time_spent_log: LogEntry;\n\n    start_date: datetime {\n        default := (SELECT datetime_current());\n        # The default value of start_date will be a\n        # result of the EdgeQL expression above.\n    }\n    due_date: datetime;\n\n    multi related_to: Issue;\n\n    multi references: File | URL {\n        list_order: int64;\n    };\n\n    # Pure index testing stuff\n    number2 := frob(.number);\n    index on (.number2);\n}\n\ntype File extending Named;\n\ntype URL extending Named {\n    required address: str;\n}\n\ntype RangeTest {\n    required rval: range<int64>;\n    required mval: multirange<int64>;\n    required rdate: range<cal::local_date>;\n    required mdate: multirange<cal::local_date>;\n\n    index pg::gist on (.rval);\n    index pg::gist on (.mval);\n    index pg::gist on (.rdate);\n    index pg::gist on (.mdate);\n}\n\ntype JSONTest {\n    required val: json;\n\n    index pg::gin on (.val);\n}\n"
  },
  {
    "path": "tests/schemas/explain_bug5758.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n# Schema for bug #5758\ntype User {\n    required name: str;\n};\n\ntype Album extending TimeTrackedEntity {\n    required name: str;\n\n    multi link tracks: Track {\n        position: int16;\n    };\n};\n\ntype Track extending TimeTrackedEntity {\n    required name: str;\n    multi link artists: Artist;\n    multi link liked_by: User;\n};\n\ntype Artist extending TimeTrackedEntity {\n    required name: str;\n    required stream_count: int64;\n    required follower_count: int64;\n};\n\nabstract type TimeTrackedEntity {\n    created: datetime {\n        rewrite insert using (datetime_of_statement())\n    };\n\n    modified: datetime {\n        rewrite update using (datetime_of_statement())\n    };\n};"
  },
  {
    "path": "tests/schemas/explain_bug5791.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n# Schema for bug #5791\n\nscalar type autoIncrementFileLegacyId extending sequence;\nscalar type autoIncrementCollectionLegacyId extending sequence;\nscalar type autoIncrementCommentMappingId extending sequence;\n\ntype File {\n    required property legacyId -> int32{\n        default := (\n            select sequence_next(introspect autoIncrementFileLegacyId)+1000000)\n    };\n\n    required property name -> str {\n        constraint expression on (\n            __subject__ = str_trim(__subject__)\n        );\n    }\n\n    required property slug -> str;\n\n    required property hash -> str {\n        readonly := true;\n        constraint exclusive;\n    }\n\n    property description -> str;\n\n    property workflowId -> uuid {\n        readonly := true;\n    }\n\n    required property userTags -> json;\n\n    required property createdAt -> datetime{\n        default := std::datetime_current();\n        readonly := true;\n    }\n    property updatedAt -> datetime;\n    property publishedAt -> datetime;\n\n    required property userId -> uuid {\n        readonly := true;\n    }\n\n    required property status -> str {\n        constraint one_of('PENDING', 'PUBLISHED', 'REJECTED', 'DELETED');\n        default := 'PENDING';\n    }\n\n    property bgColor -> str;\n\n    required property isSticker -> bool {\n        default := false;\n    }\n    required property isPremium -> bool {\n        default := false;\n    }\n\n    property downloadCount -> int32{\n        default := 0;\n    };\n\n    multi link fileVariations := .<file[is FileVariation];\n\n    property lottieSource := (\n        select .fileVariations\n        filter .type = \"LOTTIE\" and .isOptimized = false\n        order by .createdAt desc\n        limit 1\n    ).path;\n\n    property jsonSource := (\n        select .fileVariations\n        filter .type = \"JSON\" and .isOptimized = false\n        order by .createdAt desc\n        limit 1\n    ).path;\n\n    property imageSource := (\n        select .fileVariations\n        filter .type = \"PNG\"\n        order by .createdAt desc\n        limit 1\n    ).path;\n\n    index on ((.hash, .status, .isPremium));\n    index on ((.slug, .status));\n    index on ((.id, .isSticker, .isPremium, .status));\n    index on ((.isSticker, .isPremium, .status));\n    index on ((.userId, .isPremium, .status));\n    index on ((.userId, .status));\n    index on ((.hash, .status));\n    index on ((.userId, .isPremium, .status, .publishedAt));\n    index on (.hash);\n    index on (.isSticker);\n    index on (.isPremium);\n    index on (.status);\n    index on (.userId);\n    index on (.publishedAt);\n    index on (.downloadCount);\n}\n\ntype FileVariation {\n    required property path -> str;\n\n    required property createdAt -> datetime {\n        default := std::datetime_current();\n        readonly := true;\n    }\n\n    property updatedAt -> datetime;\n\n    required property type -> str {\n        constraint one_of(\n            'LOTTIE', 'JSON', 'MP4', 'GIF', 'PNG', 'ZIP', 'WEBP', 'WEBM',\n            'MOV', 'AEP'\n        );\n        default := 'LOTTIE';\n    }\n\n    property size -> int32 {\n        default := 0;\n    }\n\n    link file -> File;\n\n    link fileDimension -> FileDimension;\n\n    property isOptimized -> bool {\n        default := false;\n    }\n\n    property isTransparent -> bool {\n        default := false;\n    }\n\n    index on ((.type, .isOptimized));\n    index on ((.type, .isTransparent));\n    index on (.type);\n    index on (.isOptimized);\n    index on (.isTransparent);\n}\n\ntype UserPreference {\n    required property userId -> uuid{\n        constraint exclusive;\n    };\n\n    property isHireable -> bool {\n        default := false;\n    }\n    index on (.isHireable);\n}\n\ntype FileDimension {\n    required property name -> str;\n    required property width -> int32 {\n        default := 0;\n    }\n    required property height -> int32 {\n        default := 0;\n    }\n}\n"
  },
  {
    "path": "tests/schemas/explain_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nINSERT Status {\n    name := 'Open'\n};\n\nINSERT Status {\n    name := 'Closed'\n};\n\n\nINSERT User {\n    name := 'Elvis'\n};\n\nINSERT User {\n    name := 'Yury'\n};\n\nINSERT URL {\n    name := 'edgedb.com',\n    address := 'https://edgedb.com'\n};\n\nINSERT File {\n    name := 'screenshot.png'\n};\n\nINSERT LogEntry {\n    owner := (\n        SELECT\n            User {\n                @note := 'reassigned'\n            }\n        FILTER\n            User.name = 'Elvis'\n    ),\n    spent_time := 50000,\n    body := 'Rewriting everything.'\n};\n\nINSERT Issue {\n    number := '1',\n    name := 'Release EdgeDB',\n    body := 'Initial public release of EdgeDB.',\n    owner := (SELECT User {\n                @since := <datetime>'2018-01-01T00:00+00',\n                @note := 'automatic assignment',\n              }\n              FILTER User.name = 'Elvis'),\n    watchers := (SELECT User FILTER User.name = 'Yury'),\n    status := (SELECT Status FILTER Status.name = 'Open'),\n    time_spent_log := (SELECT LogEntry),\n    time_estimate := 3000\n};\n\nINSERT Comment {\n    body := 'EdgeDB needs to happen soon.',\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    issue := (SELECT Issue FILTER Issue.number = '1')\n};\n\n\nINSERT Issue {\n    number := '2',\n    name := 'Improve EdgeDB repl output rendering.',\n    body := 'We need to be able to render data in tabular format.',\n    owner := (SELECT User FILTER User.name = 'Yury'),\n    watchers := (SELECT User FILTER User.name = 'Elvis'),\n    status := (SELECT Status FILTER Status.name = 'Open'),\n    references :=\n        (SELECT URL FILTER URL.address = 'https://edgedb.com')\n        UNION\n        (SELECT File FILTER File.name = 'screenshot.png')\n};\n\nWITH\n    I := DETACHED Issue\nINSERT Issue {\n    number := '3',\n    name := 'Repl tweak.',\n    body := 'Minor lexer tweaks.',\n    owner := (SELECT User FILTER User.name = 'Yury'),\n    watchers := (SELECT User FILTER User.name = 'Elvis'),\n    status := (SELECT Status FILTER Status.name = 'Closed'),\n    related_to := (\n        SELECT I FILTER I.number = '2'\n    ),\n};\n\nWITH\n    I := DETACHED Issue\nINSERT Issue {\n    number := '4',\n    name := 'Regression.',\n    body := 'Fix regression introduced by lexer tweak.',\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    status := (SELECT Status FILTER Status.name = 'Closed'),\n    related_to := (\n        SELECT I FILTER I.number = '3'\n    ),\n};\n\n# NOTE: UPDATE Users for testing the link properties\n#\nUPDATE User\nFILTER User.name = 'Elvis'\nSET {\n    todo := (SELECT Issue FILTER Issue.number IN {'1', '2'})\n};\n\nUPDATE User\nFILTER User.name = 'Yury'\nSET {\n    todo := (SELECT Issue FILTER Issue.number IN {'3', '4'})\n};\n\n\n# Generate some data ...\nfor i in range_unpack(range(1, 10)) union (\n  with u := (insert User { name := <str>i }),\n  for j in range_unpack(range(0, 3)) union (\n    insert Issue {\n      owner := u,\n      number := <str>(i*100 + j),\n      name := \"issue \" ++ <str>i ++ \"/\" ++ <str>j,\n      status := (select Status filter .name = 'Open'),\n      body := \"BUG\",\n    }\n));\nupdate User set {\n  todo += (select .owned_issues filter <int64>.number % 3 = 0)\n};\n\n\nfor x in range_unpack(range(0, 100)) union (\n  insert RangeTest {\n    rval := range(-(x * 101419 % 307), x * 201881 % 307),\n    mval := multirange([\n      range(-155 - (x * 100267 % 151), -(x * 201791 % 151)),\n      range(-(x * 100003 % 307), x * 202001 % 307),\n    ]),\n    rdate := range(\n      <cal::local_date>'2000-01-01' +\n        cal::to_date_duration(days:=x * 300439 % 4999),\n      <cal::local_date>'2014-01-01' +\n        cal::to_date_duration(days:=x * 300277 % 4999),\n    ),\n    mdate := multirange([\n      range(\n        <cal::local_date>'2000-01-01' +\n          cal::to_date_duration(days:=x * 302507 % 4999),\n        <cal::local_date>'2014-01-01' +\n          cal::to_date_duration(days:=x * 301907 % 4999),\n      ),\n      range(\n        <cal::local_date>'2003-01-01' +\n          cal::to_date_duration(days:=x * 305933 % 4999),\n        <cal::local_date>'2017-01-01' +\n          cal::to_date_duration(days:=x * 305999 % 4999),\n      ),\n    ]),\n  }\n);\n\n\nfor x in range_unpack(range(0, 100)) union (\n    insert JSONTest{val := <json>(a:=x, b:=x * 40123 % 10007)}\n);\n\n\nadminister statistics_update();\n"
  },
  {
    "path": "tests/schemas/ext_ai.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ntype TestEmbeddingModel\n    extending ext::ai::EmbeddingModel\n{\n    annotation ext::ai::model_name := \"text-embedding-test\";\n    annotation ext::ai::model_provider := \"custom::test\";\n    annotation ext::ai::embedding_model_max_input_tokens := \"100\";\n    annotation ext::ai::embedding_model_max_batch_tokens := \"500\";\n    annotation ext::ai::embedding_model_max_output_dimensions := \"10\";\n    annotation ext::ai::embedding_model_supports_shortening := \"true\";\n};\n\ntype Astronomy {\n    content: str;\n    deferred index ext::ai::index(embedding_model := 'text-embedding-test')\n        on (.content);\n};\n\ntype OnExpression extending Astronomy {\n    content2: str;\n    deferred index ext::ai::index(embedding_model := 'text-embedding-test')\n        on ({.content} ++ {.content2});\n};\n\ntype OnComputed extending Astronomy {\n    content2: str;\n    combined := .content ++ .content2;\n    deferred index ext::ai::index(embedding_model := 'text-embedding-test')\n        on (.combined);\n};\n\ntype Truncated {\n    content: str;\n    deferred index ext::ai::index(\n        embedding_model := 'text-embedding-test',\n        truncate_to_max := true,\n    )\n        on (.content);\n};\n\ntype Star extending Astronomy;\n\ntype Supernova extending Star;\n\ntype CustomDimensions {\n    content: str;\n    deferred index ext::ai::index(\n        embedding_model := 'text-embedding-test',\n        dimensions := 9,\n    ) on (.content);\n};\n"
  },
  {
    "path": "tests/schemas/fts.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nabstract type Ordered {\n    required property number -> int64;\n    index fts::index on (());\n}\n\ntype Chapter extending Ordered {\n    required property title -> str;\n\n    multi link paragraphs := .<chapter[is Paragraph];\n\n    index fts::index on (\n        fts::with_options(.title, language := fts::Language.eng)\n    );\n}\n\ntype Paragraph extending Ordered {\n    required link chapter -> Chapter;\n\n    required property text -> str;\n\n    index fts::index on (\n        fts::with_options(.text, language := fts::Language.eng)\n    );\n}\n\ntype Sentence extending Ordered {\n    required property text -> str;\n    # index not overridden\n}\n\n# This is intended to test the various FTS schema features.\ntype Text {\n    required text: str;\n    index fts::index on (fts::with_options(.text,\n        language := fts::Language.eng,\n        weight_category := fts::Weight.A\n    ));\n}\n\ntype FancyText extending Text {\n    required style: int64;\n}\n\ntype QuotedText extending Text {\n    required author: str;\n}\n\ntype FancyQuotedText extending FancyText, QuotedText;\n\ntype TitledText extending Text {\n    required title: str;\n    index fts::index on ((\n        fts::with_options(\n            .title,\n            language := fts::Language.eng\n        ),\n        fts::with_options(\n            .text,\n            language := fts::Language.eng\n        )\n    ));\n}\n\n\ntype Post {\n    required title: str;\n    body: str;\n    # 2 properties are subject to FTS with different weights\n    index fts::index on ((\n        fts::with_options(.title,\n            language := fts::Language.eng,\n            weight_category := fts::Weight.A\n        ),\n        fts::with_options(.body,\n            language := fts::Language.eng,\n            weight_category := fts::Weight.B\n        )\n    ));\n\n    note: str;\n    weight_a: float64;\n}\n\ntype Description {\n    required num: int64;\n    required raw: str;\n    required property text := 'Item #' ++ to_str(.num) ++ ': ' ++ .raw;\n    # FTS on a computed property\n    index fts::index on (\n        fts::with_options(.text,\n            language := fts::Language.eng,\n            weight_category := fts::Weight.C\n        )\n    );\n}\n\ntype MultiLang {\n    required eng: str;\n    required fra: str;\n    required ita: str;\n    index fts::index on ((\n        fts::with_options(.eng,\n            language := fts::Language.eng,\n            weight_category := fts::Weight.A\n        ),\n        fts::with_options(.fra,\n            language := fts::Language.fra,\n            weight_category := fts::Weight.B\n        ),\n        fts::with_options(.ita,\n            language := fts::Language.ita,\n            weight_category := fts::Weight.C\n        ),\n    ));\n}\n\ntype DynamicLang {\n    required text: str;\n    required lang: fts::Language;\n    index fts::index on (\n        fts::with_options(.text,\n            language := .lang,\n            weight_category := fts::Weight.A\n        )\n    );\n}\n\ntype TouristVocab {\n    required text: str;\n    index fts::index on ((\n        fts::with_options(str_split(.text, '--')[0],\n            language := fts::Language.eng,\n            weight_category := fts::Weight.A\n        ),\n        fts::with_options(str_split(.text, '--')[1],\n            language := fts::Language.ita,\n            weight_category := fts::Weight.B\n        ),\n    ));\n}\n"
  },
  {
    "path": "tests/schemas/fts_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n# First two chapters of Alice in Wonderland, broken into paragraphs.\n\ninsert Chapter {title := 'Down the Rabbit-Hole', number := 1};\ninsert Chapter {title := 'The Pool of Tears', number := 2};\n\nwith\n    ch1 := assert_single((select Chapter filter .number = 1)),\n    paragraphs := str_split($$\\\nAlice was beginning to get very tired of sitting by her sister on the\nbank, and of having nothing to do: once or twice she had peeped into\nthe book her sister was reading, but it had no pictures or\nconversations in it, “and what is the use of a book,” thought Alice\n“without pictures or conversations?”\n\nSo she was considering in her own mind (as well as she could, for the\nhot day made her feel very sleepy and stupid), whether the pleasure of\nmaking a daisy-chain would be worth the trouble of getting up and\npicking the daisies, when suddenly a White Rabbit with pink eyes ran\nclose by her.\n\nThere was nothing so _very_ remarkable in that; nor did Alice think it\nso _very_ much out of the way to hear the Rabbit say to itself, “Oh\ndear! Oh dear! I shall be late!” (when she thought it over afterwards,\nit occurred to her that she ought to have wondered at this, but at the\ntime it all seemed quite natural); but when the Rabbit actually _took a\nwatch out of its waistcoat-pocket_, and looked at it, and then hurried\non, Alice started to her feet, for it flashed across her mind that she\nhad never before seen a rabbit with either a waistcoat-pocket, or a\nwatch to take out of it, and burning with curiosity, she ran across the\nfield after it, and fortunately was just in time to see it pop down a\nlarge rabbit-hole under the hedge.\n\nIn another moment down went Alice after it, never once considering how\nin the world she was to get out again.\n\nThe rabbit-hole went straight on like a tunnel for some way, and then\ndipped suddenly down, so suddenly that Alice had not a moment to think\nabout stopping herself before she found herself falling down a very\ndeep well.\n\nEither the well was very deep, or she fell very slowly, for she had\nplenty of time as she went down to look about her and to wonder what\nwas going to happen next. First, she tried to look down and make out\nwhat she was coming to, but it was too dark to see anything; then she\nlooked at the sides of the well, and noticed that they were filled with\ncupboards and book-shelves; here and there she saw maps and pictures\nhung upon pegs. She took down a jar from one of the shelves as she\npassed; it was labelled “ORANGE MARMALADE”, but to her great\ndisappointment it was empty: she did not like to drop the jar for fear\nof killing somebody underneath, so managed to put it into one of the\ncupboards as she fell past it.\n\n“Well!” thought Alice to herself, “after such a fall as this, I shall\nthink nothing of tumbling down stairs! How brave they’ll all think me\nat home! Why, I wouldn’t say anything about it, even if I fell off the\ntop of the house!” (Which was very likely true.)\n\nDown, down, down. Would the fall _never_ come to an end? “I wonder how\nmany miles I’ve fallen by this time?” she said aloud. “I must be\ngetting somewhere near the centre of the earth. Let me see: that would\nbe four thousand miles down, I think—” (for, you see, Alice had learnt\nseveral things of this sort in her lessons in the schoolroom, and\nthough this was not a _very_ good opportunity for showing off her\nknowledge, as there was no one to listen to her, still it was good\npractice to say it over) “—yes, that’s about the right distance—but\nthen I wonder what Latitude or Longitude I’ve got to?” (Alice had no\nidea what Latitude was, or Longitude either, but thought they were nice\ngrand words to say.)\n\nPresently she began again. “I wonder if I shall fall right _through_\nthe earth! How funny it’ll seem to come out among the people that walk\nwith their heads downward! The Antipathies, I think—” (she was rather\nglad there _was_ no one listening, this time, as it didn’t sound at all\nthe right word) “—but I shall have to ask them what the name of the\ncountry is, you know. Please, Ma’am, is this New Zealand or Australia?”\n(and she tried to curtsey as she spoke—fancy _curtseying_ as you’re\nfalling through the air! Do you think you could manage it?) “And what\nan ignorant little girl she’ll think me for asking! No, it’ll never do\nto ask: perhaps I shall see it written up somewhere.”\n\nDown, down, down. There was nothing else to do, so Alice soon began\ntalking again. “Dinah’ll miss me very much to-night, I should think!”\n(Dinah was the cat.) “I hope they’ll remember her saucer of milk at\ntea-time. Dinah my dear! I wish you were down here with me! There are\nno mice in the air, I’m afraid, but you might catch a bat, and that’s\nvery like a mouse, you know. But do cats eat bats, I wonder?” And here\nAlice began to get rather sleepy, and went on saying to herself, in a\ndreamy sort of way, “Do cats eat bats? Do cats eat bats?” and\nsometimes, “Do bats eat cats?” for, you see, as she couldn’t answer\neither question, it didn’t much matter which way she put it. She felt\nthat she was dozing off, and had just begun to dream that she was\nwalking hand in hand with Dinah, and saying to her very earnestly,\n“Now, Dinah, tell me the truth: did you ever eat a bat?” when suddenly,\nthump! thump! down she came upon a heap of sticks and dry leaves, and\nthe fall was over.\n\nAlice was not a bit hurt, and she jumped up on to her feet in a moment:\nshe looked up, but it was all dark overhead; before her was another\nlong passage, and the White Rabbit was still in sight, hurrying down\nit. There was not a moment to be lost: away went Alice like the wind,\nand was just in time to hear it say, as it turned a corner, “Oh my ears\nand whiskers, how late it’s getting!” She was close behind it when she\nturned the corner, but the Rabbit was no longer to be seen: she found\nherself in a long, low hall, which was lit up by a row of lamps hanging\nfrom the roof.\n\nThere were doors all round the hall, but they were all locked; and when\nAlice had been all the way down one side and up the other, trying every\ndoor, she walked sadly down the middle, wondering how she was ever to\nget out again.\n\nSuddenly she came upon a little three-legged table, all made of solid\nglass; there was nothing on it except a tiny golden key, and Alice’s\nfirst thought was that it might belong to one of the doors of the hall;\nbut, alas! either the locks were too large, or the key was too small,\nbut at any rate it would not open any of them. However, on the second\ntime round, she came upon a low curtain she had not noticed before, and\nbehind it was a little door about fifteen inches high: she tried the\nlittle golden key in the lock, and to her great delight it fitted!\n\nAlice opened the door and found that it led into a small passage, not\nmuch larger than a rat-hole: she knelt down and looked along the\npassage into the loveliest garden you ever saw. How she longed to get\nout of that dark hall, and wander about among those beds of bright\nflowers and those cool fountains, but she could not even get her head\nthrough the doorway; “and even if my head would go through,” thought\npoor Alice, “it would be of very little use without my shoulders. Oh,\nhow I wish I could shut up like a telescope! I think I could, if I only\nknew how to begin.” For, you see, so many out-of-the-way things had\nhappened lately, that Alice had begun to think that very few things\nindeed were really impossible.\n\nThere seemed to be no use in waiting by the little door, so she went\nback to the table, half hoping she might find another key on it, or at\nany rate a book of rules for shutting people up like telescopes: this\ntime she found a little bottle on it, (“which certainly was not here\nbefore,” said Alice,) and round the neck of the bottle was a paper\nlabel, with the words “DRINK ME,” beautifully printed on it in large\nletters.\n\nIt was all very well to say “Drink me,” but the wise little Alice was\nnot going to do _that_ in a hurry. “No, I’ll look first,” she said,\n“and see whether it’s marked ‘_poison_’ or not”; for she had read\nseveral nice little histories about children who had got burnt, and\neaten up by wild beasts and other unpleasant things, all because they\n_would_ not remember the simple rules their friends had taught them:\nsuch as, that a red-hot poker will burn you if you hold it too long;\nand that if you cut your finger _very_ deeply with a knife, it usually\nbleeds; and she had never forgotten that, if you drink much from a\nbottle marked “poison,” it is almost certain to disagree with you,\nsooner or later.\n\nHowever, this bottle was _not_ marked “poison,” so Alice ventured to\ntaste it, and finding it very nice, (it had, in fact, a sort of mixed\nflavour of cherry-tart, custard, pine-apple, roast turkey, toffee, and\nhot buttered toast,) she very soon finished it off.\n\n“What a curious feeling!” said Alice; “I must be shutting up like a\ntelescope.”\n\nAnd so it was indeed: she was now only ten inches high, and her face\nbrightened up at the thought that she was now the right size for going\nthrough the little door into that lovely garden. First, however, she\nwaited for a few minutes to see if she was going to shrink any further:\nshe felt a little nervous about this; “for it might end, you know,”\nsaid Alice to herself, “in my going out altogether, like a candle. I\nwonder what I should be like then?” And she tried to fancy what the\nflame of a candle is like after the candle is blown out, for she could\nnot remember ever having seen such a thing.\n\nAfter a while, finding that nothing more happened, she decided on going\ninto the garden at once; but, alas for poor Alice! when she got to the\ndoor, she found she had forgotten the little golden key, and when she\nwent back to the table for it, she found she could not possibly reach\nit: she could see it quite plainly through the glass, and she tried her\nbest to climb up one of the legs of the table, but it was too slippery;\nand when she had tired herself out with trying, the poor little thing\nsat down and cried.\n\n“Come, there’s no use in crying like that!” said Alice to herself,\nrather sharply; “I advise you to leave off this minute!” She generally\ngave herself very good advice, (though she very seldom followed it),\nand sometimes she scolded herself so severely as to bring tears into\nher eyes; and once she remembered trying to box her own ears for having\ncheated herself in a game of croquet she was playing against herself,\nfor this curious child was very fond of pretending to be two people.\n“But it’s no use now,” thought poor Alice, “to pretend to be two\npeople! Why, there’s hardly enough of me left to make _one_ respectable\nperson!”\n\nSoon her eye fell on a little glass box that was lying under the table:\nshe opened it, and found in it a very small cake, on which the words\n“EAT ME” were beautifully marked in currants. “Well, I’ll eat it,” said\nAlice, “and if it makes me grow larger, I can reach the key; and if it\nmakes me grow smaller, I can creep under the door; so either way I’ll\nget into the garden, and I don’t care which happens!”\n\nShe ate a little bit, and said anxiously to herself, “Which way? Which\nway?”, holding her hand on the top of her head to feel which way it was\ngrowing, and she was quite surprised to find that she remained the same\nsize: to be sure, this generally happens when one eats cake, but Alice\nhad got so much into the way of expecting nothing but out-of-the-way\nthings to happen, that it seemed quite dull and stupid for life to go\non in the common way.\n\nSo she set to work, and very soon finished off the cake.$$, '\\n\\n')\nfor p in enumerate(array_unpack(paragraphs))\nunion (\n    insert Paragraph {\n        chapter := ch1,\n        number := p.0 + 1,\n        text := str_replace(p.1, '\\n', ' ')\n    }\n);\n\nwith\n    ch2 := assert_single((select Chapter filter .number = 2)),\n    paragraphs := str_split($$\\\n“Curiouser and curiouser!” cried Alice (she was so much surprised, that\nfor the moment she quite forgot how to speak good English); “now I’m\nopening out like the largest telescope that ever was! Good-bye, feet!”\n(for when she looked down at her feet, they seemed to be almost out of\nsight, they were getting so far off). “Oh, my poor little feet, I\nwonder who will put on your shoes and stockings for you now, dears? I’m\nsure _I_ shan’t be able! I shall be a great deal too far off to trouble\nmyself about you: you must manage the best way you can;—but I must be\nkind to them,” thought Alice, “or perhaps they won’t walk the way I\nwant to go! Let me see: I’ll give them a new pair of boots every\nChristmas.”\n\nAnd she went on planning to herself how she would manage it. “They must\ngo by the carrier,” she thought; “and how funny it’ll seem, sending\npresents to one’s own feet! And how odd the directions will look!\n\n     _Alice’s Right Foot, Esq., Hearthrug, near the Fender,_ (_with\n     Alice’s love_).\n\nOh dear, what nonsense I’m talking!”\n\nJust then her head struck against the roof of the hall: in fact she was\nnow more than nine feet high, and she at once took up the little golden\nkey and hurried off to the garden door.\n\nPoor Alice! It was as much as she could do, lying down on one side, to\nlook through into the garden with one eye; but to get through was more\nhopeless than ever: she sat down and began to cry again.\n\n“You ought to be ashamed of yourself,” said Alice, “a great girl like\nyou,” (she might well say this), “to go on crying in this way! Stop\nthis moment, I tell you!” But she went on all the same, shedding\ngallons of tears, until there was a large pool all round her, about\nfour inches deep and reaching half down the hall.\n\nAfter a time she heard a little pattering of feet in the distance, and\nshe hastily dried her eyes to see what was coming. It was the White\nRabbit returning, splendidly dressed, with a pair of white kid gloves\nin one hand and a large fan in the other: he came trotting along in a\ngreat hurry, muttering to himself as he came, “Oh! the Duchess, the\nDuchess! Oh! won’t she be savage if I’ve kept her waiting!” Alice felt\nso desperate that she was ready to ask help of any one; so, when the\nRabbit came near her, she began, in a low, timid voice, “If you please,\nsir—” The Rabbit started violently, dropped the white kid gloves and\nthe fan, and skurried away into the darkness as hard as he could go.\n\nAlice took up the fan and gloves, and, as the hall was very hot, she\nkept fanning herself all the time she went on talking: “Dear, dear! How\nqueer everything is to-day! And yesterday things went on just as usual.\nI wonder if I’ve been changed in the night? Let me think: was I the\nsame when I got up this morning? I almost think I can remember feeling\na little different. But if I’m not the same, the next question is, Who\nin the world am I? Ah, _that’s_ the great puzzle!” And she began\nthinking over all the children she knew that were of the same age as\nherself, to see if she could have been changed for any of them.\n\n“I’m sure I’m not Ada,” she said, “for her hair goes in such long\nringlets, and mine doesn’t go in ringlets at all; and I’m sure I can’t\nbe Mabel, for I know all sorts of things, and she, oh! she knows such a\nvery little! Besides, _she’s_ she, and _I’m_ I, and—oh dear, how\npuzzling it all is! I’ll try if I know all the things I used to know.\nLet me see: four times five is twelve, and four times six is thirteen,\nand four times seven is—oh dear! I shall never get to twenty at that\nrate! However, the Multiplication Table doesn’t signify: let’s try\nGeography. London is the capital of Paris, and Paris is the capital of\nRome, and Rome—no, _that’s_ all wrong, I’m certain! I must have been\nchanged for Mabel! I’ll try and say ‘_How doth the little_—’” and she\ncrossed her hands on her lap as if she were saying lessons, and began\nto repeat it, but her voice sounded hoarse and strange, and the words\ndid not come the same as they used to do:—\n\n“How doth the little crocodile\n    Improve his shining tail,\nAnd pour the waters of the Nile\n    On every golden scale!\n\n“How cheerfully he seems to grin,\n    How neatly spread his claws,\nAnd welcome little fishes in\n    With gently smiling jaws!”\n\n“I’m sure those are not the right words,” said poor Alice, and her eyes\nfilled with tears again as she went on, “I must be Mabel after all, and\nI shall have to go and live in that poky little house, and have next to\nno toys to play with, and oh! ever so many lessons to learn! No, I’ve\nmade up my mind about it; if I’m Mabel, I’ll stay down here! It’ll be\nno use their putting their heads down and saying ‘Come up again, dear!’\nI shall only look up and say ‘Who am I then? Tell me that first, and\nthen, if I like being that person, I’ll come up: if not, I’ll stay down\nhere till I’m somebody else’—but, oh dear!” cried Alice, with a sudden\nburst of tears, “I do wish they _would_ put their heads down! I am so\n_very_ tired of being all alone here!”\n\nAs she said this she looked down at her hands, and was surprised to see\nthat she had put on one of the Rabbit’s little white kid gloves while\nshe was talking. “How _can_ I have done that?” she thought. “I must be\ngrowing small again.” She got up and went to the table to measure\nherself by it, and found that, as nearly as she could guess, she was\nnow about two feet high, and was going on shrinking rapidly: she soon\nfound out that the cause of this was the fan she was holding, and she\ndropped it hastily, just in time to avoid shrinking away altogether.\n\n“That _was_ a narrow escape!” said Alice, a good deal frightened at the\nsudden change, but very glad to find herself still in existence; “and\nnow for the garden!” and she ran with all speed back to the little\ndoor: but, alas! the little door was shut again, and the little golden\nkey was lying on the glass table as before, “and things are worse than\never,” thought the poor child, “for I never was so small as this\nbefore, never! And I declare it’s too bad, that it is!”\n\nAs she said these words her foot slipped, and in another moment,\nsplash! she was up to her chin in salt water. Her first idea was that\nshe had somehow fallen into the sea, “and in that case I can go back by\nrailway,” she said to herself. (Alice had been to the seaside once in\nher life, and had come to the general conclusion, that wherever you go\nto on the English coast you find a number of bathing machines in the\nsea, some children digging in the sand with wooden spades, then a row\nof lodging houses, and behind them a railway station.) However, she\nsoon made out that she was in the pool of tears which she had wept when\nshe was nine feet high.\n\n“I wish I hadn’t cried so much!” said Alice, as she swam about, trying\nto find her way out. “I shall be punished for it now, I suppose, by\nbeing drowned in my own tears! That _will_ be a queer thing, to be\nsure! However, everything is queer to-day.”\n\nJust then she heard something splashing about in the pool a little way\noff, and she swam nearer to make out what it was: at first she thought\nit must be a walrus or hippopotamus, but then she remembered how small\nshe was now, and she soon made out that it was only a mouse that had\nslipped in like herself.\n\n“Would it be of any use, now,” thought Alice, “to speak to this mouse?\nEverything is so out-of-the-way down here, that I should think very\nlikely it can talk: at any rate, there’s no harm in trying.” So she\nbegan: “O Mouse, do you know the way out of this pool? I am very tired\nof swimming about here, O Mouse!” (Alice thought this must be the right\nway of speaking to a mouse: she had never done such a thing before, but\nshe remembered having seen in her brother’s Latin Grammar, “A mouse—of\na mouse—to a mouse—a mouse—O mouse!”) The Mouse looked at her rather\ninquisitively, and seemed to her to wink with one of its little eyes,\nbut it said nothing.\n\n“Perhaps it doesn’t understand English,” thought Alice; “I daresay it’s\na French mouse, come over with William the Conqueror.” (For, with all\nher knowledge of history, Alice had no very clear notion how long ago\nanything had happened.) So she began again: “Où est ma chatte?” which\nwas the first sentence in her French lesson-book. The Mouse gave a\nsudden leap out of the water, and seemed to quiver all over with\nfright. “Oh, I beg your pardon!” cried Alice hastily, afraid that she\nhad hurt the poor animal’s feelings. “I quite forgot you didn’t like\ncats.”\n\n“Not like cats!” cried the Mouse, in a shrill, passionate voice. “Would\n_you_ like cats if you were me?”\n\n“Well, perhaps not,” said Alice in a soothing tone: “don’t be angry\nabout it. And yet I wish I could show you our cat Dinah: I think you’d\ntake a fancy to cats if you could only see her. She is such a dear\nquiet thing,” Alice went on, half to herself, as she swam lazily about\nin the pool, “and she sits purring so nicely by the fire, licking her\npaws and washing her face—and she is such a nice soft thing to\nnurse—and she’s such a capital one for catching mice—oh, I beg your\npardon!” cried Alice again, for this time the Mouse was bristling all\nover, and she felt certain it must be really offended. “We won’t talk\nabout her any more if you’d rather not.”\n\n“We indeed!” cried the Mouse, who was trembling down to the end of his\ntail. “As if _I_ would talk on such a subject! Our family always\n_hated_ cats: nasty, low, vulgar things! Don’t let me hear the name\nagain!”\n\n“I won’t indeed!” said Alice, in a great hurry to change the subject of\nconversation. “Are you—are you fond—of—of dogs?” The Mouse did not\nanswer, so Alice went on eagerly: “There is such a nice little dog near\nour house I should like to show you! A little bright-eyed terrier, you\nknow, with oh, such long curly brown hair! And it’ll fetch things when\nyou throw them, and it’ll sit up and beg for its dinner, and all sorts\nof things—I can’t remember half of them—and it belongs to a farmer, you\nknow, and he says it’s so useful, it’s worth a hundred pounds! He says\nit kills all the rats and—oh dear!” cried Alice in a sorrowful tone,\n“I’m afraid I’ve offended it again!” For the Mouse was swimming away\nfrom her as hard as it could go, and making quite a commotion in the\npool as it went.\n\nSo she called softly after it, “Mouse dear! Do come back again, and we\nwon’t talk about cats or dogs either, if you don’t like them!” When the\nMouse heard this, it turned round and swam slowly back to her: its face\nwas quite pale (with passion, Alice thought), and it said in a low\ntrembling voice, “Let us get to the shore, and then I’ll tell you my\nhistory, and you’ll understand why it is I hate cats and dogs.”\n\nIt was high time to go, for the pool was getting quite crowded with the\nbirds and animals that had fallen into it: there were a Duck and a\nDodo, a Lory and an Eaglet, and several other curious creatures. Alice\nled the way, and the whole party swam to the shore.$$, '\\n\\n')\nfor p in enumerate(array_unpack(paragraphs))\nunion (\n    insert Paragraph {\n        chapter := ch2,\n        number := p.0 + 1,\n        text := str_replace(p.1, '\\n', ' ')\n    }\n);\ninsert Sentence {number := 0, text := \"This will not be indexed\"};\n\ninsert Text {text := 'hello world'};\ninsert Text {text := 'running and jumping fox'};\ninsert FancyText {text := 'fancy hello', style := 0};\ninsert FancyText {text := 'elaborate and foxy', style := 10};\ninsert QuotedText {text := 'the world is big', author := 'Alice'};\ninsert QuotedText {text := 'this is simple', author := 'Bob'};\ninsert FancyQuotedText {\n    text := 'the fox chases the rabbit', author := 'Cameron', style := 1,\n};\ninsert FancyQuotedText {\n    text := 'the rabbit is fast', author := 'Cameron', style := 2,\n};\ninsert TitledText {\n    title := 'big',\n    text := 'important'\n};\n\ninsert Post {\n    title := 'first post',\n    body := 'The sky is so red.',\n};\ninsert Post {\n    title := 'angry reply',\n    body := \"No! Wrong! It's blue!\",\n    note := 'blue reply',\n};\ninsert Post {\n    title := 'helpful reply',\n    body := \"That's Rayleigh scattering for you\",\n    weight_a := 0,\n};\ninsert Post {\n    title := 'random stuff',\n    body := 'angry giraffes',\n    note := 'random angry stuff',\n    weight_a := 0.1,\n};\ninsert Post {\n    title := 'no body',\n    note := 'no body',\n};\n\ninsert Description {\n    num := 1,\n    raw := 'red umbrella',\n};\ninsert Description {\n    num := 2,\n    raw := 'red and white candy cane',\n};\ninsert Description {\n    num := 3,\n    raw := 'fancy pants',\n};\n\n# Let's use some words that have the same spelling, but different meaning\n# based on the language.\ninsert MultiLang {\n    eng := \"The window pane is clear\",\n    fra := \"La vitre est claire\",\n    ita := \"Il vetro della finestra è chiaro\",\n};\ninsert MultiLang {\n    eng := \"No pain no gain\",\n    fra := \"Qui ne tente rien n'a rien\",\n    ita := \"Chi la dura la vince\",\n};\ninsert MultiLang {\n    eng := \"This delicious bread\",\n    fra := \"Ce délicieux pain\",\n    ita := \"Questo pane buonissimo\",\n};\ninsert MultiLang {\n    eng := \"The breaded pork is nice\",\n    fra := \"Le porc pané est bon\",\n    ita := \"Il maiale impanato è buono\",\n};\ninsert DynamicLang {\n    text := \"The window pane is clear\",\n    lang := fts::Language.eng,\n};\ninsert DynamicLang {\n    text := \"No pain no gain\",\n    lang := fts::Language.eng,\n};\ninsert DynamicLang {\n    text := \"Ce délicieux pain\",\n    lang := fts::Language.fra,\n};\ninsert DynamicLang {\n    text := \"Questo pane buonissimo\",\n    lang := fts::Language.ita,\n};\ninsert TouristVocab {\n    text := \"The window pane is clear -- Il vetro della finestra è chiaro\",\n};\ninsert TouristVocab {\n    text := \"This delicious bread -- Questo pane buonissimo\",\n};"
  },
  {
    "path": "tests/schemas/graphql.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nabstract type NamedObject {\n    annotation description := 'An object with a name';\n\n    required property name -> str;\n}\n\ntype UserGroup extending NamedObject {\n    multi link settings -> Setting {\n        constraint exclusive;\n    }\n}\n\ntype Setting extending NamedObject {\n    required property value -> str;\n}\n\ntype Profile extending NamedObject {\n    required property value -> str;\n    property tags -> array<str>;\n    multi property odd -> array<int64>;\n\n    # computed link and property test\n    link owner_user := .<profile[IS User];\n    property owner_name := .<profile[IS User].name;\n}\n\ntype User extending NamedObject {\n    required property active -> bool {\n        default := false;\n    }\n    multi link groups -> UserGroup;\n    required property age -> int64;\n    required property score -> float64 {\n        default := 0;\n    }\n    link profile -> Profile;\n    # a link pointing to an abstract type\n    multi link favorites -> NamedObject;\n    multi link fav_users := .favorites[is User];\n}\n\nalias SettingAlias := Setting {\n    of_group := .<settings[IS UserGroup]\n};\n\nalias SettingAliasAugmented := Setting {\n    of_group := .<settings[IS UserGroup] {\n        name_upper := str_upper(.name)\n    }\n};\n\nalias ProfileAlias := Profile {\n    # although this will point to an actual user, but the type system\n    # will only resolve an Object here\n    owner := .<profile\n};\n\nalias TwoUsers := (\n    select User {\n        initial := .name[0],\n    } order by .name limit 2\n);\n\ntype Person extending User;\n\nscalar type positive_int_t extending int64 {\n    constraint min_value(0);\n}\n\ntype ScalarTest {\n    property p_bool -> bool;\n    property p_str -> str;\n    property p_datetime -> datetime;\n    property p_local_datetime -> cal::local_datetime;\n    property p_local_date -> cal::local_date;\n    property p_local_time -> cal::local_time;\n    property p_duration -> duration;\n    property p_int16 -> int16;\n    property p_int32 -> int32;\n    property p_int64 -> int64;\n    property p_bigint -> bigint;\n    property p_float32 -> float32;\n    property p_float64 -> float64;\n    property p_decimal -> decimal;\n    property p_decimal_str := <str>.p_decimal;\n    property p_json -> json;\n    property p_bytes -> bytes;\n\n    property p_posint -> positive_int_t;\n    property p_array_str -> array<str>;\n    property p_array_int64 -> array<int64>;\n    property p_array_json -> array<json>;\n    property p_array_bytes -> array<bytes>;\n\n    property p_tuple -> tuple<int64, str>;\n    property p_array_tuple -> array<tuple<str, bool>>;\n\n    property p_short_str -> str {\n        constraint max_len_value(5);\n    }\n}\ntype BigIntTest {\n    property value -> bigint;\n}\n\n# Inheritance tests\ntype Bar {\n    property q -> str\n}\n\ntype Bar2 extending Bar {\n    property w -> str\n}\n\ntype Rab {\n    # target type will be overridden\n    link blah -> Bar\n}\n\ntype Rab2 extending Rab {\n    overloaded link blah -> Bar2;\n}\n\ntype Genre extending NamedObject {\n    multi link games -> Game;\n}\n\ntype Game extending NamedObject {\n    multi link players -> User\n}\n\n# Recursive structure\ntype LinkedList extending NamedObject {\n    link next -> LinkedList\n}\n\nglobal test_global_str -> str;\nglobal test_global_array -> array<str>;\nglobal test_global_id -> uuid;\nrequired global test_global_def -> str {\n    default := \"\"\n};\noptional global test_global_def2 -> str {\n    default := \"\"\n};\nfunction get_glob() -> optional str using (global test_global_str);\nfunction get_glob_inline() -> optional str {\n  using (global test_global_str);\n  volatility := 'Modifying';\n};\n\ntype Missing {\n    access policy no allow all using (false);\n    access policy innsert allow insert;\n};\n\nalias GlobalTest := {\n    gstr := global test_global_str,\n    garray := global test_global_array,\n    gid := global test_global_id,\n    gdef := global test_global_def,\n    gdef2 := global test_global_def2,\n    transaction_isolation := <str>sys::get_transaction_isolation(),\n    access_policies := not (exists Missing),\n};\n\nfunction id_func(s: str) -> str using (s);\n\nalias FuncTest := {\n    fstr := id_func('test'),\n};\n\ntype Combo {\n    required property name -> str;\n    # Test type union, Setting and Profile share some inherited fields as well\n    # as non-inherited ones.\n    link data -> Setting | Profile;\n};\n\nabstract constraint error_test_constraint extending expression {\n    errmessage :=\n        '{__subject__} cannot have val equal to the length of text field.';\n}\n\n# Used to test run-time errors.\ntype ErrorTest {\n    required property text -> str {\n        # rewrite empty string as empty set\n        rewrite update using (\n            .text if len(.text) > 0 else <str>{}\n        );\n    }\n    required property val -> int64 {\n        # force values of 10 or more cause an error\n        rewrite update using (\n            .val if .val < 10 else .val // 0\n        );\n    }\n\n    property div_by_val := 1 / .val;\n    property re_text := re_match(.text, 'test');\n\n    access policy access_all\n        allow all\n        using (true);\n    access policy deny_negative_val\n        deny update write\n        using (.val < 0);\n\n    constraint error_test_constraint on (len(.text) != .val);\n}\n\ntype RangeTest {\n    required name: str;\n    rval: range<float64>;\n    mval: multirange<float64>;\n    rdate: range<cal::local_date>;\n    mdate: multirange<cal::local_date>;\n}\n\ntype Fixed {\n    property computed := 123;\n}\n\ntype NotEditable {\n    property computed := 'a computed value';\n    required once: str {\n        readonly := true;\n    }\n}\n\nfunction modifying_noop(x: str) -> str {\n    using (x);\n    volatility := 'Modifying';\n}\n\nglobal current_user_id: uuid;\nglobal current_user := (select User filter .id = global current_user_id);\n"
  },
  {
    "path": "tests/schemas/graphql_other.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nscalar type ColorEnum extending enum<'RED', 'GREEN', 'BLUE'> {\n    annotation description := 'RGB color enum';\n}\n\ntype Foo {\n    annotation description := 'Test type \"Foo\"';\n\n    property `select` -> str;\n    property after -> str;\n    required property color -> ColorEnum;\n    multi property multi_color -> ColorEnum;\n    property color_array -> array<ColorEnum>;\n\n    # Testing linking to the same type\n    multi link foos -> Foo {\n        on target delete deferred restrict;\n    }\n}\n"
  },
  {
    "path": "tests/schemas/graphql_schema.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nabstract type NamedObject {\n    annotation description := 'An object with a name';\n\n    required property name -> str;\n}\n\ntype UserGroup extending NamedObject {\n    multi link settings -> Setting {\n        constraint exclusive;\n    }\n}\n\ntype Setting extending NamedObject {\n    required property value -> str;\n}\n\ntype Profile extending NamedObject {\n    required property value -> str;\n    property tags -> array<str>;\n    multi property odd -> array<int64>;\n\n    # computed link and property test\n    link owner_user := .<profile[IS User];\n    property owner_name := .<profile[IS User].name;\n}\n\ntype User extending NamedObject {\n    required property active -> bool;\n    multi link groups -> UserGroup;\n    required property age -> int64;\n    required property score -> float64;\n    link profile -> Profile;\n}\n\nalias SettingAlias := Setting {\n    of_group := .<settings[IS UserGroup]\n};\n\nalias SettingAliasAugmented := Setting {\n    of_group := .<settings[IS UserGroup] {\n        name_upper := str_upper(.name)\n    }\n};\n\ntype Person extending User;\n\ntype Combo {\n    # Test type union, Setting and Profile share some inherited fields as well\n    # as non-inherited ones.\n    link data -> Setting | Profile;\n}\n\nscalar type positive_int_t extending int64 {\n    constraint min_value(0);\n}\n\ntype ScalarTest {\n    property p_bool -> bool;\n    property p_str -> str;\n    property p_datetime -> datetime;\n    property p_local_datetime -> cal::local_datetime;\n    property p_local_date -> cal::local_date;\n    property p_local_time -> cal::local_time;\n    property p_duration -> duration;\n    property p_int16 -> int16;\n    property p_int32 -> int32;\n    property p_int64 -> int64;\n    property p_bigint -> bigint;\n    property p_float32 -> float32;\n    property p_float64 -> float64;\n    property p_decimal -> decimal;\n    property p_json -> json;\n    property p_bytes -> bytes;\n\n    property p_posint -> positive_int_t;\n    property p_array_str -> array<str>;\n    property p_array_int64 -> array<int64>;\n    property p_array_json -> array<json>;\n    property p_array_bytes -> array<bytes>;\n}\n\ntype Fixed {\n    property computed := 123;\n}\n\ntype NotEditable {\n    property computed := 'a computed value';\n    required once: str {\n        readonly := true;\n    }\n}"
  },
  {
    "path": "tests/schemas/graphql_schema_other.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nscalar type ColorEnum extending enum<'RED', 'GREEN', 'BLUE'> {\n    annotation description := 'RGB color enum';\n}\n\ntype Foo {\n    annotation description := 'Test type \"Foo\"';\n\n    property `select` -> str;\n    property after -> str;\n    required property color -> ColorEnum;\n}\n"
  },
  {
    "path": "tests/schemas/graphql_schema_other_deep.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ntype NestedMod {\n    required val: str;\n}\n"
  },
  {
    "path": "tests/schemas/graphql_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ninsert Setting {\n    name := 'template',\n    value := 'blue'\n};\n\ninsert Setting {\n    name := 'perks',\n    value := 'full'\n};\n\ninsert UserGroup {\n    name := 'basic'\n};\n\ninsert UserGroup {\n    name := 'upgraded',\n    settings := (select Setting)\n};\n\ninsert UserGroup {\n    name := 'unused',\n    settings := (\n        insert Setting {\n            name := 'template',\n            value := 'none'\n        }\n    )\n};\n\ninsert User {\n    name := 'John',\n    age := 25,\n    active := True,\n    score := 3.14,\n    groups := (select UserGroup filter UserGroup.name = 'basic')\n};\n\ninsert User {\n    name := 'Jane',\n    age := 25,\n    active := True,\n    score := 1.23,\n    groups := (select UserGroup filter UserGroup.name = 'upgraded')\n};\n\ninsert User {\n    name := 'Alice',\n    age := 27,\n    active := True,\n    score := 5.0,\n    profile := (insert Profile {\n        name := 'Alice profile',\n        value := 'special',\n        tags := ['1st', '2nd'],\n    }),\n    favorites := {Setting, UserGroup}\n};\n\ninsert Person {\n    name := 'Bob',\n    age := 21,\n    active := True,\n    score := 4.2,\n    profile := (insert Profile {\n        name := 'Bob profile',\n        value := 'special',\n    }),\n    favorites := {UserGroup, User}\n};\n\nWITH MODULE other\ninsert Foo {\n    `select` := 'a',\n    color := 'RED',\n};\n\nWITH MODULE other\ninsert Foo {\n    `select` := 'b',\n    after := 'w',\n    color := 'GREEN',\n};\n\nWITH MODULE other\ninsert Foo {\n    after := 'q',\n    color := 'BLUE',\n};\n\ninsert ScalarTest {\n    p_bool := True,\n    p_str := 'Hello world',\n    p_datetime := <datetime>'2018-05-07T20:01:22.306916+00:00',\n    p_local_datetime := <cal::local_datetime>'2018-05-07T20:01:22.306916',\n    p_local_date := <cal::local_date>'2018-05-07',\n    p_local_time := <cal::local_time>'20:01:22.306916',\n    p_duration := <duration>'20 hrs',\n    p_int16 := 12345,\n    p_int32 := 1234567890,\n    p_int64 := 1234567890123,\n    p_bigint := 123456789123456789123456789n,\n    p_float32 := 2.5,\n    p_float64 := 2.5,\n    p_decimal :=\n        123456789123456789123456789.123456789123456789123456789n,\n    p_json := to_json('{\"foo\": [1, null, \"bar\"]}'),\n    p_bytes := b'Hello World',\n\n    p_posint := 42,\n    p_array_str := ['hello', 'world'],\n    p_array_json := [<json>'hello', <json>'world'],\n    p_array_bytes := [b'hello', b'world'],\n\n    p_tuple := (123, 'test'),\n    p_array_tuple := [('hello', true), ('world', false)],\n\n    p_short_str := 'hello',\n};\n\n# Inheritance tests\ninsert Bar {\n    q := 'bar'\n};\n\n\ninsert Bar2 {\n    q := 'bar2',\n    w := 'special'\n};\n\n\ninsert Rab {\n    blah := (select Bar limit 1)\n};\n\n\ninsert Rab2 {\n    blah := (select Bar2 limit 1)\n};\n\n\ninsert LinkedList {\n    name := '4th',\n};\n\ninsert LinkedList {\n    name := '3rd',\n    next := (\n        select DETACHED LinkedList\n        filter .name = '4th'\n        limit 1\n    )\n};\n\ninsert LinkedList {\n    name := '2nd',\n    next := (\n        select DETACHED LinkedList\n        filter .name = '3rd'\n        limit 1\n    )\n};\n\ninsert LinkedList {\n    name := '1st',\n    next := (\n        select DETACHED LinkedList\n        filter .name = '2nd'\n        limit 1\n    )\n};\n\ninsert Combo {\n    name := 'combo 0',\n};\n\ninsert Combo {\n    name := 'combo 1',\n    data := assert_single((\n        select Setting filter .name = 'template' and .value = 'blue'\n    )),\n};\n\ninsert Combo {\n    name := 'combo 2',\n    data := assert_single((select Profile filter .name = 'Alice profile')),\n};\n\ninsert ErrorTest {\n    text := ')error(',\n    val := 0,\n};\n\ninsert RangeTest {\n    name := 'test01',\n    rval := range(-1.3, 1.2),\n    mval := multirange([\n        range(<float64>{}, -10.0),\n        range(-1.3, 1.2),\n        range(10.0),\n    ]),\n    rdate := range(\n        <cal::local_date>'2018-01-23',\n        <cal::local_date>'2023-07-25',\n    ),\n    mdate := multirange([\n        range(\n            <cal::local_date>'2018-01-23',\n            <cal::local_date>'2023-07-25',\n        ),\n        range(\n            <cal::local_date>'2025-11-22',\n        ),\n    ]),\n};\n\ninsert RangeTest {\n    name := 'missing boundaries',\n    # empty\n    rval := range(<float64>{}, empty := true),\n    # unbounded = everything\n    mval := multirange([range(<float64>{})]),\n    # empty\n    rdate := range(<cal::local_date>{}, empty := true),\n    # unbounded = everything\n    mdate := multirange([range(<cal::local_date>{})]),\n};\n\ninsert Fixed;\n\ninsert NotEditable {\n    once := 'init',\n};\n\ninsert other::deep::NestedMod {\n    val := 'in nested module'\n};\n\ninsert Missing;\n"
  },
  {
    "path": "tests/schemas/insert.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ntype Subordinate {\n    required property name -> str;\n    property val -> int64;\n}\n\ntype InsertTest {\n    property name -> str;\n    property l1 -> int64;\n    required property l2 -> int64;\n    property l3 -> str {\n        default := \"test\";\n    }\n    multi link subordinates -> Subordinate {\n        property comment -> str;\n    }\n    link sub -> Subordinate {\n        property note -> str;\n    }\n    link sub_ex -> Subordinate {\n        constraint exclusive;\n    }\n}\n\ntype DerivedTest extending InsertTest;\n\ntype Note {\n    required property name -> str;\n    property note -> str;\n    link subject -> Object;\n}\ntype DerivedNote extending Note;\n\ntype Person {\n    required single property name -> std::str {\n        constraint std::exclusive;\n        default := \"Nemo\";\n    };\n    optional single property tag -> std::str;\n    required single property tag2 -> std::str {\n        default := \"<n/a>\";\n    };\n    optional multi link notes -> Note;\n    optional multi property multi_prop -> std::str {\n        constraint std::exclusive;\n    };\n    optional single link note -> Note;\n    property case_name -> str {\n        constraint exclusive on (str_lower(__subject__));\n    }\n}\ntype DerivedPerson extending Person {\n    property sub_key -> str {\n        constraint exclusive;\n    }\n};\n\ntype Person2 {\n    required single property first -> std::str;\n    optional single link note -> Note;\n    optional multi link notes -> Note;\n}\n\ntype Person2a extending Person2 {\n    required single property last -> std::str;\n    constraint exclusive on ((__subject__.first, __subject__.last));\n    single link bff -> Person;\n    constraint exclusive on ((.first, .bff));\n}\ntype DerivedPerson2a extending Person2a;\n\ntype Person2b extending Person2 {\n    optional single property last -> std::str;\n    property namespace := .first ++ \" \"; # har, har\n    property name {\n        using (.namespace ++ .last);\n        constraint exclusive;\n    }\n}\ntype DerivedPerson2b extending Person2b;\n\ntype PersonWrapper {\n    required single link person -> Person;\n}\n\ntype DefaultTest1 {\n    property num -> int64 {\n        default := 42;\n    }\n    property foo -> str;\n}\n\ntype DefaultTest2 {\n    property foo -> str;\n    required property num -> int64 {\n        # XXX: circumventing sequence deficiency\n        default := (\n          (SELECT DefaultTest1\n            ORDER BY .num DESC\n           LIMIT 1).num + 1\n        );\n    }\n}\n\ntype DefaultTest3 {\n    required property foo -> float64 {\n        # non-deterministic dynamic value\n        default := random();\n    }\n}\n\ntype DefaultTest4 {\n    required property bar -> int64 {\n        # deterministic dynamic value\n        default := (SELECT count(DefaultTest4));\n    }\n}\n\ntype DefaultTest5 {\n    required property name -> str;\n    link other -> Subordinate {\n        # statically defined value\n        default := (\n            SELECT Subordinate\n            FILTER .name = 'DefaultTest5/Sub'\n            LIMIT 1\n        );\n    }\n}\n\ntype DefaultTest6 {\n    required property name -> str;\n    link other -> DefaultTest5 {\n        # staticly defined insert\n        default := (\n            INSERT DefaultTest5 {\n                name := 'DefaultTest6/5'\n            }\n        );\n    }\n}\n\ntype DefaultTest7 {\n    required property name -> str;\n    link other -> DefaultTest6 {\n        # staticly defined insert that creates an implicit insert chain\n        default := (\n            INSERT DefaultTest6 {\n                name := 'DefaultTest7/6'\n            }\n        );\n    }\n}\n\n# self incrementing integer sequence\nscalar type int_seq8_t extending sequence;\n\ntype DefaultTest8 {\n    required property number -> int_seq8_t {\n        # The number values are automatically\n        # generated, and are not supposed to be\n        # directly writable.\n        readonly := true;\n    }\n}\n\ntype DunderDefaultTest01 {\n    required a: int64;\n    required b: int64 {\n        default := __source__.a+1\n    };\n    required c: int64 {\n        default := 1\n    }\n}\n\ntype DunderDefaultTest02_A {\n    required a: int64 {\n        default := 1\n    };\n}\ntype DunderDefaultTest02_B {\n    multi default_with_insert: DunderDefaultTest02_A {\n        default := (\n            insert DunderDefaultTest02_A {\n                a := 1\n            }\n        )\n    };\n    multi default_with_update: DunderDefaultTest02_A {\n        default := (\n            update DunderDefaultTest02_A\n            filter DunderDefaultTest02_A.a = 2\n            set {\n                a := 22\n            }\n        )\n    };\n    multi default_with_delete: DunderDefaultTest02_A {\n        default := (\n            delete DunderDefaultTest02_A\n            filter DunderDefaultTest02_A.a = 3\n        )\n    };\n    multi default_with_select: DunderDefaultTest02_A {\n        default := (\n            select DunderDefaultTest02_A\n            filter DunderDefaultTest02_A.a = 4\n        )\n    };\n}\n\ntype DunderDefaultTest03_A {\n    required x: int64;\n}\ntype DunderDefaultTest03_B {\n    required x: int64 {\n        default := 1\n    };\n}\ntype DunderDefaultTest03_C {\n    required x: int64 {\n        default := 2\n    };\n}\n\ntype DunderDefaultTest04_A {\n    required x: int64;\n};\ntype DunderDefaultTest04_B {\n    required x: int64;\n    l: DunderDefaultTest04_A {\n        default := (\n            select DunderDefaultTest04_A\n            limit 1\n        )\n    };\n};\n\n# types to test some inheritance issues\ntype InputValue {\n    property val -> str;\n}\n\nabstract type Callable {\n    multi link args -> InputValue;\n}\n\ntype Field extending Callable {\n    # This link 'args' appears to be overriding the overloaded 'args'\n    # from Callable.\n    overloaded multi link args -> InputValue;\n}\n\ntype Directive extending Callable;\n\n\ntype SelfRef {\n    required property name -> str;\n    multi link ref -> SelfRef;\n}\n\ntype CollectionTest {\n    property some_tuple -> tuple<str, int64>;\n    multi property some_multi_tuple -> tuple<str, int64>;\n    property str_array -> array<str>;\n    property float_array -> array<float32>;\n}\n\ntype ExceptTest {\n    required property name -> str;\n    property deleted -> bool;\n    constraint exclusive on (.name) except (.deleted);\n};\ntype ExceptTestSub extending ExceptTest;\n\ntype ConflictA {\n  name: str;\n}\ntype ConflictB {\n  name: str { constraint exclusive };\n  whatever: str;\n}\ntype ConflictAB extending ConflictA, ConflictB;\n"
  },
  {
    "path": "tests/schemas/interpreter_disambiguation.esdl",
    "content": "\n\ntype A {\n    name : str;\n    b : B {\n        b_lp : str\n    }\n}\n\ntype B {\n    name : str\n}"
  },
  {
    "path": "tests/schemas/interpreter_disambiguation_setup.edgeql",
    "content": "insert B {name := \"b1\"};\n\ninsert A {name := \"a1\", b := (select B {@b_lp := \"a1_b1_lp\"} limit 1)};"
  },
  {
    "path": "tests/schemas/inventory.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nabstract type Named {\n    required property name -> str {\n        delegated constraint exclusive;\n    }\n}\n\ntype Item extending Named {\n    multi property tag_set1 -> str;\n    multi property tag_set2 -> str;\n    property tag_array -> array<str>;\n}\n"
  },
  {
    "path": "tests/schemas/inventory_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nINSERT Item {\n    name := 'table',\n\n    tag_set1 := {'wood', 'rectangle'},\n    tag_set2 := {'wood', 'rectangle'},\n    tag_array := ['wood', 'rectangle'],\n};\n\nINSERT Item {\n    name := 'floor lamp',\n\n    tag_set1 := {'metal', 'plastic'},\n    tag_set2 := {'metal', 'plastic'},\n    tag_array := ['metal', 'plastic'],\n};\n\n# some items with incomplete data\nINSERT Item {\n    name := 'chair',\n\n    tag_set1 := {'wood', 'rectangle'},\n    tag_array := ['wood', 'rectangle'],\n};\n\n\nINSERT Item {\n    name := 'tv',\n\n    tag_set2 := {'plastic', 'rectangle'},\n    tag_array := ['plastic', 'rectangle'],\n};\n\n\nINSERT Item {\n    name := 'ball',\n\n    tag_set1 := {'plastic', 'round'},\n    tag_set2 := {'plastic', 'round'},\n};\n\n\nINSERT Item {\n    name := 'teapot',\n\n    tag_array := ['ceramic', 'round'],\n};\n\nINSERT Item {\n    name := 'mystery toy',\n};\n\n# no known properties\nINSERT Item {\n    name := 'ectoplasm',\n};\n"
  },
  {
    "path": "tests/schemas/issues.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nabstract type Text {\n    # This is an abstract object containing text.\n    required body: str {\n        # Maximum length of text is 10000 characters.\n        constraint max_len_value(10000);\n    }\n}\n\nabstract type Named {\n    required name: str;\n}\n\n# Dictionary is a NamedObject variant, that enforces\n# name uniqueness across all instances if its subclass.\nabstract type Dictionary extending Named {\n    overloaded required name: str {\n        delegated constraint exclusive;\n    }\n    index on (.name);\n}\n\ntype User extending Dictionary {\n    multi todo: Issue {\n        rank: int64 {\n            default := 42;\n        }\n    }\n}\n\nabstract type Owned {\n    # By default links are optional.\n    required owner: User {\n        note: str;\n    }\n}\n\ntype Status extending Dictionary;\n\ntype Priority extending Dictionary;\n\ntype LogEntry extending Owned, Text {\n    # LogEntry is an Owned and a Text, so it\n    # will have all of their links and attributes,\n    # in particular, owner and text links.\n    required spent_time: int64;\n}\n\nscalar type issue_num_t extending std::str;\n\ntype Comment extending Text, Owned {\n    required issue: Issue;\n    optional parent: Comment;\n}\n\ntype Issue extending Named, Owned, Text {\n    overloaded required link owner {\n        property since: datetime;\n    }\n\n    required number: issue_num_t {\n        readonly := true;\n        constraint exclusive;\n    }\n    required status: Status;\n\n    priority: Priority {\n      splat_strategy := 'Implicit';\n    }\n\n    optional multi watchers: User;\n    num_watchers {\n      using (count(.watchers));\n    }\n\n    optional time_estimate: int64;\n\n    multi time_spent_log: LogEntry;\n\n    start_date: datetime {\n        default := (SELECT datetime_current());\n        # The default value of start_date will be a\n        # result of the EdgeQL expression above.\n    }\n    due_date: datetime;\n\n    multi related_to: Issue {\n        splat_strategy := 'Explicit';\n    }\n\n    multi references: File | URL | Publication {\n        list_order: int64;\n    };\n\n    tags: array<str> {\n        splat_strategy := 'Explicit';\n    }\n\n    index fts::index on ((\n        fts::with_options(.name, language := fts::Language.eng),\n        fts::with_options(.body, language := fts::Language.eng),\n    ));\n}\n\n# This is used to test correct behavior of boolean operators: NOT,\n# AND, OR. It targets especially interactions of properties and {}.\n#\n# Issue can be used to test similar interaction for links.\ntype BooleanTest extending Named {\n    val: int64;\n    multi tags: str;\n}\n\ntype File extending Named;\n\ntype URL extending Named {\n    required address: str;\n}\n\ntype Publication {\n    required title: str;\n\n    title1 := (SELECT ident(.title));\n    required title2 := (SELECT ident(.title));\n    required single title3 := (SELECT ident(.title));\n    optional single title4 := (SELECT ident(.title));\n    optional multi title5 := (SELECT ident(.title));\n    required multi title6 := (SELECT ident(.title));\n\n    multi authors: User {\n        list_order: int64;\n    };\n}\n\nabstract constraint my_one_of(one_of: array<anytype>) {\n    using (contains(one_of, __subject__));\n}\n\nscalar type EmulatedEnum extending str {\n    constraint one_of('v1', 'v2');\n}\n\nfunction ident(a: str) -> str {\n    USING (SELECT a)\n}\n\n\nfunction opt_test(tag: int64, x: str) -> str using (x ?? '');\nfunction opt_test(tag: bool, x: optional str) -> str using (x ?? '');\nfunction opt_test(tag: int64, x: int64) -> int64 using (x ?? -1);\nfunction opt_test(tag: bool, x: optional int64) -> int64 using (x ?? -1);\n\nfunction opt_test(tag: int64, x: int64, y: optional int64) -> int64 using (y ?? -1);\nfunction opt_test(tag: bool, x: optional int64, y: optional int64) -> int64 using (y ?? -1);\n"
  },
  {
    "path": "tests/schemas/issues_coalesce_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nINSERT Priority {\n    name := 'High'\n};\n\nINSERT Priority {\n    name := 'Low'\n};\n\nINSERT Status {\n    name := 'Open'\n};\n\nINSERT Status {\n    name := 'Closed'\n};\n\n\nINSERT User {\n    name := 'Elvis'\n};\n\nINSERT URL {\n    name := 'edgedb.com',\n    address := 'https://edgedb.com'\n};\n\nINSERT File {\n    name := 'screenshot.png'\n};\n\nINSERT LogEntry {\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    spent_time := -1,\n    body := 'Dummy'\n};\n\nINSERT LogEntry {\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    spent_time := 60,\n    body := 'Log1'\n};\n\nINSERT LogEntry {\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    spent_time := 90,\n    body := 'Log2'\n};\n\nINSERT LogEntry {\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    spent_time := 60,\n    body := 'Log3'\n};\n\nINSERT LogEntry {\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    spent_time := 30,\n    body := 'Log4'\n};\n\nINSERT Issue {\n    number := '1',\n    name := 'Issue 1',\n    body := 'Body 1',\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    status := (SELECT Status FILTER Status.name = 'Closed'),\n    time_estimate := 60,\n    time_spent_log := (SELECT LogEntry FILTER LogEntry.body = 'Log1'),\n};\n\nINSERT Issue {\n    number := '2',\n    name := 'Issue 2',\n    body := 'Body 2',\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    status := (SELECT Status FILTER Status.name = 'Closed'),\n    time_estimate := 90,\n    time_spent_log := (SELECT LogEntry FILTER LogEntry.body = 'Log2'),\n};\n\nINSERT Issue {\n    number := '3',\n    name := 'Issue 3',\n    body := 'Body 3',\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    status := (SELECT Status FILTER Status.name = 'Closed'),\n    time_estimate := 90,\n    time_spent_log := (\n        SELECT LogEntry FILTER LogEntry.body IN {'Log3','Log4'}),\n};\n\nINSERT Issue {\n    number := '4',\n    name := 'Issue 4',\n    body := 'Body 4',\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    status := (SELECT Status FILTER Status.name = 'Open'),\n};\n\nWITH\n    I := DETACHED Issue\nINSERT Issue {\n    number := '5',\n    name := 'Issue 5',\n    body := 'Body 5',\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    status := (SELECT Status FILTER Status.name = 'Open'),\n    related_to := (SELECT I FILTER I.number = '1'),\n};\n\nWITH\n    I := DETACHED Issue\nINSERT Issue {\n    number := '6',\n    name := 'Issue 6',\n    body := 'Body 6',\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    status := (SELECT Status FILTER Status.name = 'Open'),\n    related_to := (SELECT I FILTER I.number = '2'),\n};\n"
  },
  {
    "path": "tests/schemas/issues_filter_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nINSERT Status {\n    name := 'Open'\n};\n\nINSERT Status {\n    name := 'Closed'\n};\n\n\nINSERT User {\n    name := 'Elvis'\n};\n\nINSERT User {\n    name := 'Yury'\n};\n\nINSERT User {\n    name := 'Victor'\n};\n\n\nINSERT Issue {\n    number := '1',\n    name := 'Implicit path existence',\n    body := 'Any expression involving paths also implies paths exist.',\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    status := (SELECT Status FILTER Status.name = 'Closed'),\n    time_estimate := 9001,\n};\n\nINSERT Issue {\n    number := '2',\n    name := 'NOT EXISTS problem',\n    body := 'Implicit path existence does not apply to NOT EXISTS.',\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    status := (SELECT Status FILTER Status.name = 'Open'),\n    due_date := <datetime>'2020-01-15T00:00:00+00:00',\n};\n\nINSERT Issue {\n    number := '3',\n    name := 'EdgeQL to SQL translator',\n    body := 'Rewrite and refactor translation to SQL.',\n    owner := (SELECT User FILTER User.name = 'Yury'),\n    status := (SELECT Status FILTER Status.name = 'Open'),\n    time_estimate := 9999,\n    due_date := <datetime>'2020-01-15T00:00:00+00:00',\n};\n\nINSERT Issue {\n    number := '4',\n    name := 'Translator optimization',\n    body := 'At some point SQL translations should be optimized.',\n    owner := (SELECT User FILTER User.name = 'Yury'),\n    status := (SELECT Status FILTER Status.name = 'Open'),\n};\n"
  },
  {
    "path": "tests/schemas/issues_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nINSERT Priority {\n    name := 'High'\n};\n\nINSERT Priority {\n    name := 'Low'\n};\n\nINSERT Status {\n    name := 'Open'\n};\n\nINSERT Status {\n    name := 'Closed'\n};\n\n\nINSERT User {\n    name := 'Elvis'\n};\n\nINSERT User {\n    name := 'Yury'\n};\n\nINSERT URL {\n    name := 'edgedb.com',\n    address := 'https://edgedb.com'\n};\n\nINSERT File {\n    name := 'screenshot.png'\n};\n\nINSERT LogEntry {\n    owner := (\n        SELECT\n            User {\n                @note := 'reassigned'\n            }\n        FILTER\n            User.name = 'Elvis'\n    ),\n    spent_time := 50000,\n    body := 'Rewriting everything.'\n};\n\nINSERT Issue {\n    number := '1',\n    name := 'Release EdgeDB',\n    body := 'Initial public release of EdgeDB.',\n    owner := (SELECT User {\n                @since := <datetime>'2018-01-01T00:00+00',\n                @note := 'automatic assignment',\n              }\n              FILTER User.name = 'Elvis'),\n    watchers := (SELECT User FILTER User.name = 'Yury'),\n    status := (SELECT Status FILTER Status.name = 'Open'),\n    time_spent_log := (SELECT LogEntry),\n    time_estimate := 3000\n};\n\nINSERT Comment {\n    body := 'EdgeDB needs to happen soon.',\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    issue := (SELECT Issue FILTER Issue.number = '1')\n};\n\n\nINSERT Issue {\n    number := '2',\n    name := 'Improve EdgeDB repl output rendering.',\n    body := 'We need to be able to render data in tabular format.',\n    owner := (SELECT User FILTER User.name = 'Yury'),\n    watchers := (SELECT User FILTER User.name = 'Elvis'),\n    status := (SELECT Status FILTER Status.name = 'Open'),\n    priority := (SELECT Priority FILTER Priority.name = 'High'),\n    references :=\n        (SELECT URL FILTER URL.address = 'https://edgedb.com')\n        UNION\n        (SELECT File FILTER File.name = 'screenshot.png')\n};\n\nWITH\n    I := DETACHED Issue\nINSERT Issue {\n    number := '3',\n    name := 'Repl tweak.',\n    body := 'Minor lexer tweaks.',\n    owner := (SELECT User FILTER User.name = 'Yury'),\n    watchers := (SELECT User FILTER User.name = 'Elvis'),\n    status := (SELECT Status FILTER Status.name = 'Closed'),\n    related_to := (\n        SELECT I FILTER I.number = '2'\n    ),\n    priority := (SELECT Priority FILTER Priority.name = 'Low')\n};\n\nWITH\n    I := DETACHED Issue\nINSERT Issue {\n    number := '4',\n    name := 'Regression.',\n    body := 'Fix regression introduced by lexer tweak.',\n    owner := (SELECT User FILTER User.name = 'Elvis'),\n    status := (SELECT Status FILTER Status.name = 'Closed'),\n    related_to := (\n        SELECT I FILTER I.number = '3'\n    ),\n    tags := ['regression', 'lexer']\n};\n\n# NOTE: UPDATE Users for testing the link properties\n#\nUPDATE User\nFILTER User.name = 'Elvis'\nSET {\n    todo := (SELECT Issue FILTER Issue.number IN {'1', '2'})\n};\n\nUPDATE User\nFILTER User.name = 'Yury'\nSET {\n    todo := (SELECT Issue FILTER Issue.number IN {'3', '4'})\n};\n\nINSERT BooleanTest {\n    name := 'circle',\n    val := 2,\n    tags := {'red', 'black'},\n};\n\nINSERT BooleanTest {\n    name := 'triangle',\n    val := 10,\n    tags := {'red', 'green'},\n};\n\nINSERT BooleanTest {\n    name := 'square',\n    tags := {'red'},\n};\n\nINSERT BooleanTest {\n    name := 'pentagon',\n};\n\nINSERT BooleanTest {\n    name := 'hexagon',\n    val := 4,\n};\n"
  },
  {
    "path": "tests/schemas/json.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ntype JSONTest {\n    required property number -> int64 {\n        constraint exclusive;\n    }\n\n    # these properties are intended to have specific JSON types in them\n    property j_string -> json;\n    property j_number -> json;\n    property j_boolean -> json;\n    property j_array -> json;\n    property j_object -> json;\n\n    # this property is for more generic JSON handling\n    property data -> json;\n\n    # these properties are used for testing casting\n    property edb_string -> str;\n}\n"
  },
  {
    "path": "tests/schemas/json_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\nINSERT JSONTest {\n    number := 0,\n    j_string := <json>'the',\n    j_number := <json>2,\n    j_boolean := <json>true,\n    j_array := to_json('[1, 1, 1]'),\n    j_object := to_json('{\n        \"a\": 1,\n        \"b\": 2\n    }'),\n    data := to_json('null'),\n    edb_string := 'jumps'\n};\n\nINSERT JSONTest {\n    number := 1,\n    j_string := <json>'quick',\n    j_number := <json>2.7,\n    j_boolean := <json>false,\n    j_array := to_json('[]'),\n    j_object := to_json('{\n        \"b\": 1,\n        \"c\": 2\n    }'),\n    data := to_json('{}'),\n    edb_string := 'over'\n};\n\nINSERT JSONTest {\n    number := 2,\n    j_string := <json>'brown',\n    j_number := <json>2.71,\n    j_boolean := <json>true,\n    j_array := to_json('[2, \"q\", [3], {}, null]'),\n};\n\nINSERT JSONTest {\n    number := 3,\n    data := to_json('[\n        1.61,\n        null,\n        \"Fraka\",\n        8033,\n        {\n            \"a\": \"apple\",\n            \"b\": {\n                \"foo\": 988,\n                \"bar\": [null, null, {\"bingo\": \"42!\"}]\n            },\n            \"c\": \"corn\"\n        },\n        75,\n        true\n    ]'),\n};\n"
  },
  {
    "path": "tests/schemas/link_tgt_del.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-2016 MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nabstract type Named {\n    required property name -> str {\n        delegated constraint exclusive;\n    }\n}\n\ntype Target1 extending Named {\n     multi link extra_tgt -> Target1;\n};\ntype Target1Child extending Target1;\ntype Target2 extending Named;\n\ntype Source1 extending Named {\n    link tgt1_restrict -> Target1 {\n        on target delete restrict;\n    }\n    link tgt_union_restrict -> Target1 | Target2 {\n        on target delete restrict;\n    }\n    link tgt1_allow -> Target1 {\n        on target delete allow;\n    }\n    link tgt1_del_source -> Target1 {\n        on target delete delete source;\n    }\n    link tgt1_deferred_restrict -> Target1 {\n        on target delete deferred restrict;\n    }\n    multi link tgt1_m2m_restrict -> Target1 {\n        on target delete restrict;\n    }\n    multi link tgt1_m2m_allow -> Target1 {\n        on target delete allow;\n    }\n    multi link tgt1_m2m_del_source -> Target1 {\n        on target delete delete source;\n    }\n    multi link tgt_union_m2m_del_source -> Target1 | Target2 {\n        on target delete delete source;\n    }\n\n    link tgt1_del_target -> Target1 {\n        on source delete delete target;\n    }\n    multi link tgt1_m2m_del_target -> Target1 {\n        on source delete delete target;\n    }\n    link self_del_target -> Named {\n        on source delete delete target;\n    }\n    link self_del_source -> Named {\n        on target delete delete source;\n    }\n\n    link tgt1_del_target_orphan -> Target1 {\n        on source delete delete target if orphan;\n    }\n    multi link tgt1_m2m_del_target_orphan -> Target1 {\n        on source delete delete target if orphan;\n    }\n    link self_del_target_orphan -> Named {\n        on source delete delete target;\n    }\n\n}\n\n# Make sure the existence of aliases doen't cause trouble\nalias ASource1 := Source1;\nalias ATarget1 := Target1;\n\ntype Source2 extending Named {\n    link src1_del_source -> Source1 {\n        on target delete delete source;\n    }\n    multi link tgt_m2m -> Target1;\n}\n\ntype Source3 extending Source1;\n\ntype ObjectType4 {\n    link foo -> Target1;\n}\n\ntype ObjectType5 {\n    link foo -> Target1;\n}\n\nabstract type AbsSource1 extending Named {\n    link tgt1_del_source -> Target1 {\n        on target delete delete source;\n    }\n}\n\ntype ChildSource1 extending AbsSource1;\n\ntype SchemaSource extending Named {\n    link schema_restrict -> schema::Object {\n        on target delete restrict;\n    }\n    link schema_m_restrict -> schema::Object {\n        on target delete restrict;\n    }\n}\n"
  },
  {
    "path": "tests/schemas/link_tgt_del_migrated.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-2016 MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nabstract type Named {\n    required property name -> str {\n        delegated constraint exclusive;\n    }\n}\n\ntype Target1 extending Named;\ntype Target1Child extending Target1;\n\ntype Source1 extending Named {\n    link tgt1_restrict -> Target1 {\n        on target delete restrict;\n    }\n    link tgt1_allow -> Target1 {\n        on target delete allow;\n    }\n    link tgt1_del_source -> Target1 {\n        on target delete delete source;\n    }\n    link tgt1_deferred_restrict -> Target1 {\n        on target delete restrict;\n    }\n    multi link tgt1_m2m_restrict -> Target1 {\n        on target delete restrict;\n    }\n    multi link tgt1_m2m_allow -> Target1 {\n        on target delete allow;\n    }\n    multi link tgt1_m2m_del_source -> Target1 {\n        on target delete delete source;\n    }\n}\n\ntype Source2 extending Named {\n    link src1_del_source -> Source1 {\n        on target delete delete source;\n    }\n}\n\ntype Source3 extending Source1;\n\nabstract type AbstractObjectType {\n    link foo -> Target1;\n}\n\ntype ObjectType4 extending AbstractObjectType;\n\ntype ObjectType5 extending AbstractObjectType;\n"
  },
  {
    "path": "tests/schemas/links_1.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-2016 MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ntype Target0 {\n    property name -> str;\n}\n\ntype Target1 extending Target0 {\n    overloaded property name -> str;\n}\n\ntype ObjectType0 {\n    link target -> Target0;\n}\n\ntype ObjectType1 {\n    link target -> Target1;\n}\n\ntype ObjectType01 extending ObjectType0, ObjectType1;\n\ntype ObjectType2 {\n    required link target -> Target0;\n}\n\ntype ObjectType3 {\n    required link target -> Target0;\n}\n\ntype ObjectType23 extending ObjectType2, ObjectType3;\n"
  },
  {
    "path": "tests/schemas/links_1_migrated.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2008-2016 MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ntype Target0 {\n    property name -> str;\n}\n\ntype Target1 extending Target0 {\n    overloaded property name -> str;\n}\n\ntype ObjectType0 {\n    link target -> Target0;\n}\n\ntype ObjectType1 {\n    link target -> Target0;\n}\n\ntype ObjectType01 extending ObjectType0;\n\ntype ObjectType2 {\n    required link target -> Target0;\n}\n\ntype ObjectType3 {\n    required link target -> Target0;\n}\n\ntype ObjectType23 extending ObjectType2, ObjectType3;\n"
  },
  {
    "path": "tests/schemas/movies.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nglobal username_prefix: str;\n\ntype Person {\n    required first_name: str;\n    last_name: str;\n\n    full_name := __source__.first_name ++ ((' ' ++ .last_name) ?? '');\n    favorite_genre := (select Genre filter .name = 'Drama' limit 1);\n    directed_movie := (select .<director[is Movie] limit 1);\n    username := (global username_prefix ?? 'u_') ++ str_lower(.first_name);\n}\n\ntype Genre {\n    required name: str;\n}\n\nglobal filter_title: str;\n\ntype Content {\n    required title: str;\n    genre: Genre;\n\n    access policy filter_title\n        allow select\n        using (global filter_title ?= .title);\n    access policy dml allow insert, update, delete;\n}\n\ntype Movie extending Content {\n    release_year: int64;\n    multi actors: Person {\n        property role: str;\n        property role_lower := str_lower(@role);\n    };\n    director: Person {\n        bar: str;\n    };\n\n    multi actor_names := __source__.actors.first_name;\n    multi similar_to := (select Content);\n}\n\ntype Book extending Content {\n    required pages: int16;\n    multi chapters: str;\n}\n\ntype novel extending Book {\n    foo: str;\n}\n\nmodule nested {\n    type Hello {\n        property hello -> str;\n    };\n\n    module deep {\n        type Rolling {\n            property rolling -> str;\n        };\n    };\n}\n\ntype ContentSummary {\n    property x := std::count((select Content));\n\n    access policy select_always allow select;\n    access policy dml allow insert, update, delete\n        using (global filter_title ?= 'summary');\n}\n\nmodule links {\n    type A;\n\n    type B {\n        multi link a: A;\n        link prop: A {\n            property lp: str;\n        };\n        multi property vals: str;\n    }\n\n    type C extending B;\n}"
  },
  {
    "path": "tests/schemas/movies_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\ninsert Genre { name:= 'Fiction' };\ninsert Genre { name:= 'Drama' };\ninsert Genre { name:= '武侠' };\n\ninsert Person { first_name:= 'Tom', last_name:= 'Hanks' };\ninsert Person { first_name:= 'Robin' };\ninsert Person { first_name:= 'Steven', last_name:= 'Spielberg' };\n\ninsert Movie {\n    title := 'Forrest Gump',\n    release_year := 1994,\n    actors := (select Person\n        filter .first_name = 'Tom' or .first_name = 'Robin'\n    ),\n    genre := (select Genre filter .name = 'Drama' limit 1),\n};\n\ninsert Movie {\n    title := 'Saving Private Ryan',\n    release_year := 1998,\n    actors := (\n        select Person { @role := 'Captain Miller' } filter .first_name = 'Tom'\n    ),\n    director := (\n        select Person { @bar := 'bar' } filter .last_name = 'Spielberg' limit 1\n    ),\n    genre := (select Genre filter .name = 'Drama' limit 1),\n};\n\ninsert novel {\n    title :='Hunger Games',\n    pages := 374,\n    genre := (select Genre filter .name = 'Fiction' limit 1),\n    chapters := {\n        'Part 1',\n        'Part 2',\n        'Part 3',\n    },\n};\n\ninsert Book {\n    title:='Chronicles of Narnia',\n    pages := 206,\n    chapters := {\n        'Lucy looks into a wardrobe',\n        'What Lucy found there',\n        'Edmund and the wardrobe',\n        'Turkish delight',\n    },\n    genre:= (select Genre filter .name = 'Fiction' limit 1)\n};\n\ninsert Content {\n    title := 'Halo 3',\n    genre := (select Genre filter .name = 'Fiction' limit 1)\n};\n\nset global filter_title := 'summary';\ninsert ContentSummary;\nreset global filter_title;\n\ninsert default::links::C {\n    a := {(insert default::links::A), (insert default::links::A)},\n    prop := (insert default::links::A),\n    vals := {\"1\", \"2\", \"3\", \"4\"},\n};\n"
  },
  {
    "path": "tests/schemas/pg_dump01_default.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ntype A {\n    annotation title := 'A';\n\n    property p_bool -> bool {\n        annotation title := 'single bool';\n    }\n    property p_str -> str;\n    property p_datetime -> datetime;\n    property p_local_datetime -> cal::local_datetime;\n    property p_local_date -> cal::local_date;\n    property p_local_time -> cal::local_time;\n    property p_duration -> duration;\n    property p_relative_duration -> cal::relative_duration;\n    property p_date_duration -> cal::date_duration;\n    property p_int16 -> int16;\n    property p_int32 -> int32;\n    property p_int64 -> int64;\n    property p_float32 -> float32;\n    property p_float64 -> float64;\n    property p_bigint -> bigint;\n    property p_decimal -> decimal;\n    property p_json -> json;\n    property p_bytes -> bytes;\n}\n\ntype B {\n    annotation title := 'B';\n\n    property arr_bool -> array<bool>;\n    property arr_str -> array<str>;\n    property arr_datetime -> array<datetime>;\n    property arr_local_datetime -> array<cal::local_datetime>;\n    property arr_local_date -> array<cal::local_date>;\n    property arr_local_time -> array<cal::local_time>;\n    property arr_duration -> array<duration>;\n    property arr_relative_duration -> array<cal::relative_duration>;\n    property arr_date_duration -> array<cal::date_duration>;\n    property arr_int16 -> array<int16>;\n    property arr_int32 -> array<int32>;\n    property arr_int64 -> array<int64>;\n    property arr_float32 -> array<float32>;\n    property arr_float64 -> array<float64>;\n    property arr_bigint -> array<bigint>;\n    property arr_decimal -> array<decimal>;\n    property arr_json -> array<json>;\n    property arr_bytes -> array<bytes>;\n}\n\ntype C {\n    annotation title := 'C';\n\n    property tup0 -> tuple<bool, str, datetime>;\n    property tup1 -> tuple<\n                        cal::local_datetime,\n                        cal::local_date,\n                        cal::local_time,\n                        duration\n                     >;\n    property tup2 -> tuple<\n                        cal::relative_duration,\n                        cal::date_duration,\n                        json,\n                        bytes\n                     >;\n    property tup3 -> tuple<\n                        int16,\n                        int32,\n                        int64,\n                        float32,\n                        float64,\n                        bigint,\n                        decimal\n                     >;\n\n    property nested0 -> array<tuple<array<tuple<array<int64>>>>>;\n    property nested1 -> tuple<\n                            Positive,\n                            str,\n                            tuple<\n                                tuple<\n                                    float64,\n                                    bool,\n                                >,\n                                tuple<\n                                    cal::local_date,\n                                    cal::local_time,\n                                >,\n                            >,\n                        >;\n\n    property r_int32 -> range<std::int32>;\n    property r_int64 -> range<std::int64>;\n    property r_float32 -> range<std::float32>;\n    property r_float64 -> range<std::float64>;\n    property r_decimal -> range<std::decimal>;\n    property r_datetime -> range<std::datetime>;\n    property r_local_datetime -> range<cal::local_datetime>;\n    property r_local_date -> range<cal::local_date>;\n\n    property pos -> Positive;\n}\n\nscalar type Positive extending int64 {\n  constraint min_value(1);\n};\n"
  },
  {
    "path": "tests/schemas/pg_dump01_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nSET MODULE default;\n\nINSERT A {\n    p_bool := True,\n    p_str := 'Hello',\n    p_datetime := <datetime>'2018-05-07T20:01:22.306916+00:00',\n    p_local_datetime := <cal::local_datetime>'2018-05-07T20:01:22.306916',\n    p_local_date := <cal::local_date>'2018-05-07',\n    p_local_time := <cal::local_time>'20:01:22.306916',\n    p_duration := <duration>'20 hrs',\n    p_relative_duration := <cal::relative_duration>'3 days 4 hrs',\n    p_date_duration := <cal::date_duration>'2 months 5 days',\n    p_int16 := 12345,\n    p_int32 := 1234567890,\n    p_int64 := 1234567890123,\n    p_float32 := 2.5,\n    p_float64 := 2.5,\n    p_bigint := 123456789123456789123456789n,\n    p_decimal := 123456789123456789123456789.123456789123456789123456789n,\n    p_json := to_json('[{\"a\": null, \"b\": true}, 1, 2.5, \"foo\"]'),\n    p_bytes := b'Hello',\n};\n\nINSERT B {\n    arr_bool := [True, False],\n    arr_str := ['Hello', 'world'],\n    arr_datetime := [<datetime>'2018-05-07T20:01:22.306916+00:00'],\n    arr_local_datetime := [<cal::local_datetime>'2018-05-07T20:01:22.306916'],\n    arr_local_date := [<cal::local_date>'2018-05-07'],\n    arr_local_time := [<cal::local_time>'20:01:22.306916'],\n    arr_duration := [<duration>'20 hrs'],\n    arr_relative_duration := [<cal::relative_duration>'3 days 4 hrs'],\n    arr_date_duration := [<cal::date_duration>'2 months 5 days'],\n    arr_int16 := [12345],\n    arr_int32 := [1234567890],\n    arr_int64 := [1234567890123],\n    arr_float32 := [2.5],\n    arr_float64 := [2.5],\n    arr_bigint := [123456789123456789123456789n],\n    arr_decimal := [123456789123456789123456789.123456789123456789123456789n],\n    arr_json := [to_json('[{\"a\": null, \"b\": true}, 1, 2.5, \"foo\"]')],\n    arr_bytes := [b'Hello', b'world'],\n};\n\nINSERT C {\n    tup0 := (\n      True,\n      'Hello',\n      <datetime>'2018-05-07T20:01:22.306916+00:00',\n    ),\n    tup1 := (\n      <cal::local_datetime>'2018-05-07T20:01:22.306916',\n      <cal::local_date>'2018-05-07',\n      <cal::local_time>'20:01:22.306916',\n      <duration>'20 hrs',\n    ),\n    tup2 := (\n      <cal::relative_duration>'3 days 4 hrs',\n      <cal::date_duration>'2 months 5 days',\n      to_json('[{\"a\": null, \"b\": true}, 1, 2.5, \"foo\"]'),\n      b'Hello',\n    ),\n    tup3 := (\n      12345,\n      1234567890,\n      1234567890123,\n      2.5,\n      2.5,\n      123456789123456789123456789n,\n      123456789123456789123456789.123456789123456789123456789n,\n    ),\n\n    nested0 := [\n        ([([0, 1],), ([2, 3, 4],)],),\n        ([([5, 6],), (<array<int64>>[],)],),\n    ],\n    nested1 := (\n        <Positive>2,\n        'some string',\n        (\n            (-1.2, False),\n            (\n                <cal::local_date>'2023-05-17',\n                <cal::local_time>'21:43:56',\n            ),\n        ),\n    ),\n\n    r_int32 := range(<int32>1, <int32>20),\n    r_int64 := range(2, 123456789012),\n    r_float32 := range(<float32>1.1, <float32>2.2),\n    r_float64 := range(0.1, 2.3),\n    r_decimal := range(1.2n, 3.4n),\n    r_datetime := range(<datetime>'2022-01-31T11:22:33Z',\n                        <datetime>'2024-03-31T17:28:04Z'),\n    r_local_datetime := range(<cal::local_datetime>'2022-02-15T11:22:33',\n                              <cal::local_datetime>'2024-04-25T17:28:04'),\n    r_local_date := range(<cal::local_date>'2022-03-17',\n                          <cal::local_date>'2024-05-31'),\n    pos := <Positive>1234,\n};\n"
  },
  {
    "path": "tests/schemas/pg_dump02_default.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nabstract annotation `🍿`;\n\nabstract constraint `🚀🍿`(max: int64) extending max_len_value;\n\nfunction `💯`(NAMED ONLY `🙀`: int64) -> int64 {\n    using (\n        SELECT 100 - `🙀`\n    );\n\n    annotation `🍿` := 'fun!🚀';\n    volatility := 'Immutable';\n}\n\ntype `S p a M` {\n    required property `🚀` -> int32;\n    property c100 := (SELECT `💯`(`🙀` := .`🚀`));\n}\n\ntype A {\n    required link `s p A m 🤞` -> `S p a M`;\n}\n\nscalar type 你好 extending str;\n\nscalar type مرحبا extending 你好 {\n    constraint `🚀🍿`(100);\n};\n\nscalar type `🚀🚀🚀` extending مرحبا;\n\ntype Łukasz {\n    required property `Ł🤞` -> `🚀🚀🚀` {\n        default := <`🚀🚀🚀`>'你好🤞'\n    }\n    index on (.`Ł🤞`);\n\n    link `Ł💯` -> A {\n        property `🙀🚀🚀🚀🙀` -> `🚀🚀🚀`;\n        property `🙀مرحبا🙀` -> مرحبا {\n            constraint `🚀🍿`(200);\n        }\n    };\n}\n\ntype Tree {\n    required property val -> str {\n        constraint exclusive;\n    };\n\n    link parent -> Tree;\n    link children := .<parent[IS Tree];\n    property child_vals := .children.val;\n\n    index fts::index on (\n        fts::with_options(.val, language := fts::Language.eng)\n    );\n}\n"
  },
  {
    "path": "tests/schemas/pg_dump02_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nSET MODULE default;\n\n# Not sure if the esdl filename will handle this on all systems, so\n# I'm adding stuff here.\nCREATE MODULE `💯💯💯`;\n\nCREATE MODULE `back``ticked`;\n\nCREATE FUNCTION `💯💯💯`::`🚀🙀🚀`(`🤞`: default::`🚀🚀🚀`) -> `🚀🚀🚀`\nUSING (\n    SELECT <`🚀🚀🚀`>(`🤞` ++ 'Ł🙀')\n);\n\nCREATE TYPE `💯💯💯`::`🚀🙀🚀Type` {\n  CREATE PROPERTY p_你好 -> 你好;\n};\n\nCREATE TYPE `back``ticked`::`Ticked``Type` {\n    CREATE PROPERTY `p_``ticked` -> str;\n};\n# end of DDL\n\nINSERT `S p a M` {\n    `🚀` := 42\n};\n\nINSERT A {\n    `s p A m 🤞` := assert_single((SELECT `S p a M` FILTER .`🚀` = 42))\n};\n\nINSERT Łukasz;\n\nINSERT Łukasz {\n    `Ł🤞` := 'simple 🚀',\n    `Ł💯` := (\n        SELECT A\n        {\n            `🙀🚀🚀🚀🙀`:= 'Łink prop 🙀🚀🚀🚀🙀',\n            `🙀مرحبا🙀`:=\n                `💯💯💯`::`🚀🙀🚀`('Łink prop 🙀مرحبا🙀'),\n        }\n        FILTER .`s p A m 🤞`.`🚀` = 42\n        LIMIT 1\n    )\n};\n\nINSERT `💯💯💯`::`🚀🙀🚀Type` {\n    p_你好 := 'plain text',\n};\n\nINSERT `back``ticked`::`Ticked``Type` {\n    `p_``ticked` := 'no backticks here',\n};"
  },
  {
    "path": "tests/schemas/pg_trgm.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\nabstract type Base {\n    required p_str: str;\n}\n\ntype Gin extending Base {\n    index ext::pg_trgm::gin on (.p_str);\n}\n\ntype Gin2 extending Base {\n    p_str_2: str;\n    index ext::pg_trgm::gist on ((.p_str, .p_str_2));\n}\n\ntype Gist extending Base {\n    index ext::pg_trgm::gist on (.p_str);\n}\n\ntype Gist2 extending Base {\n    p_str_2: str;\n    index ext::pg_trgm::gist on ((.p_str, .p_str_2));\n}\n"
  },
  {
    "path": "tests/schemas/pg_trgm_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Portions Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfor x in range_unpack(range(1, 1001)) union (\n    insert Gist {\n        p_str := \"qwertyu\" ++ str_pad_start(<str>x, 4, \"0\"),\n    }\n);\n\nfor x in range_unpack(range(1, 1001)) union (\n    insert Gist2 {\n        p_str := \"qwertyu\" ++ str_pad_start(<str>x, 4, \"0\"),\n        p_str_2 := \"iopasdf\" ++ str_pad_start(<str>x, 4, \"0\"),\n    }\n);\n\n# Dataset excerpt from pg_trgm tests in Postgres\nfor x in {\n    \"Tandobai Algad\",\n    \"Daikalay\",\n    \"Stubaital\",\n    \"Neustift im Stubaital\",\n    \"Anonyme Appartments Stubaital\",\n    \"Barkaladja Pool\",\n    \"Awabakal Nature Reserve\",\n    \"Awabakal Field Studies Centre\",\n    \"Barkala\",\n    \"Bailallie\",\n    \"Barkala Park\",\n    \"Purba Kalaujan\",\n    \"Nabakalas\",\n    \"Barkal\",\n    \"Baikanthapur\",\n    \"Baikarjhuti\",\n    \"Baika\",\n    \"Baikari\",\n    \"Purba Kalmegha\",\n    \"Baskaltsi\",\n    \"Bajkal\",\n    \"Riacho do Sambaibal\",\n    \"Sambaibal\",\n    \"Barkalabava\",\n    \"Kaikalahun Indian Reserve 25\",\n    \"Tumba-Kalamba\",\n    \"Kamba-Kalele\",\n    \"Boyagbakala\",\n    \"Bombakalo\",\n    \"Batikalengbe\",\n    \"Matamba-Kalenge\",\n    \"Kambakala\",\n    \"Abakalu\",\n    \"Bonagbakala\",\n    \"Bikala Madila\",\n    \"Bikala\",\n    \"Bumba-Kaloki\",\n    \"Tumba-Kalunga\",\n    \"Kabankala\",\n    \"Mambakala\",\n    \"Tumba-Kalumba\",\n    \"Kabakala\",\n    \"Bikalabwa\",\n    \"Bomba-Kalende\",\n    \"Mwalaba-Kalamba\",\n    \"Matamba-Kalenga\",\n    \"Bumba-Kalumba\",\n    \"Bikalange\",\n    \"Kabikala\",\n    \"Mubikale\",\n    \"Kanampumba-Kalawa\",\n    \"Tshiabakale\",\n    \"Bambakala\",\n    \"Tsibakala\",\n    \"Kimbakala\",\n    \"Dabakalakoro\",\n    \"Dabakala\",\n    \"Mobaika\",\n    \"Baimalou\",\n    \"Xibaitaling\",\n    \"Baikai\",\n    \"Baikang\",\n    \"Baitaling\",\n    \"Baikan\",\n    \"Baimaling Linchang\",\n    \"Baimalong\",\n    \"Baikanzui\",\n    \"Baiyali\",\n    \"Baimaling\",\n    \"Baimalang Donggang\",\n    \"Baikangshuoma\",\n    \"Baitaliao\",\n    \"Taikale\",\n    \"Babainale\",\n    \"Bailale\",\n    \"Baibale\",\n    \"Baiwale\",\n    \"Baikangnei\",\n    \"Baitali\",\n    \"Xiabaikan\",\n    \"Bailalong\",\n    \"Baimaluo\",\n    \"Baikacun\",\n    \"Baisala\",\n    \"Bailalin\",\n    \"Baimala\",\n    \"Baidalong\",\n    \"Dabaika\",\n    \"Caikalong\",\n    \"Cuobaikacun\",\n    \"Baikadangcun\",\n    \"Baimalin\",\n    \"Subaika\",\n    \"Gabakkale\",\n    \"Barkallou\",\n    \"Embatkala\",\n    \"Bodega Tabaibal\",\n    \"Golba Kalo\",\n    \"Haikala\",\n    \"Kaikale\",\n    \"Waikaloulevu\",\n    \"Waikalou Creek\",\n    \"Waikalou\",\n    \"Ndelaikalou\",\n    \"Ndelaikalokalo\",\n    \"Bay of Backaland\",\n    \"Bankali\",\n    \"Ker Samba Kalla\",\n    \"Demba Kali\",\n    \"Baipal\",\n    \"Kalibakalako\",\n    \"Dalabakala\",\n    \"Bikal\",\n    \"Sembaikan\",\n    \"Praikalogu\",\n    \"Tanjung Ompaikalio\",\n    \"Bonebabakal\",\n    \"Tanjung Batikala\",\n    \"Buku Baikole\",\n    \"Pulau Baika\",\n    \"Kebakkalang\",\n    \"Ngambakalang\",\n    \"Mota Sabakal\",\n    \"Babakalo\",\n    \"Buyu Rapanbakalai\",\n    \"Kalimundubakalan\",\n    \"Tanabakal\",\n    \"Tanjung Aikaluin\",\n    \"Desa Kebakalan\",\n    \"Kebakalan\",\n    \"Kalibakal\",\n    \"Trobakal\",\n    \"Alue Bakkala\",\n    \"Moncong Baika\",\n    \"Sampangbakalan\",\n    \"Lebakkalapa Tonggoh\",\n    \"Trembakal\",\n    \"Desa Cemengbakalan\",\n    \"Desa Tambakkalisogo\",\n    \"Tambakkalisogo\",\n    \"Laikalanda\",\n    \"Tanjung Mbakalang\",\n    \"Kali Purbakala\",\n    \"Tukad Kubakal\",\n    \"Praikalangga\",\n    \"Banjar Kubakal\",\n    \"Kombakalada\",\n    \"Sori Rabakalo\",\n    \"Kahambikalela\",\n    \"Baikarara\",\n    \"Baikapaka\",\n    \"Teluk Haludubakal\",\n    \"Yabakalewa\",\n    \"Praikalumbang\",\n    \"Waikalowo\",\n    \"Praikalubu\",\n    \"Loko Praikalubu\",\n    \"Ramuk Ombakalada\",\n    \"Praikalebung\",\n    \"Praikaleka\",\n    \"Andabakal\",\n    \"Praikalau\",\n    \"Praikalokat\",\n    \"Praikalimbung\",\n    \"Bambakalo\",\n    \"Leubakkalian\",\n    \"Pematang Baitalimbangan\",\n    \"Lebakalil\",\n    \"Gereba Kaler\",\n    \"Muarabakal\",\n    \"Umbulan Maharobakal\",\n    \"Baidaloen\",\n    \"Jatibakal\",\n    \"Dola Peimambakal\",\n    \"Salu Baidale\",\n    \"Parbakalan\",\n    \"Praikalembu\",\n    \"Palindi Laikali\",\n    \"Praikalu\",\n    \"Sori Labakalate\",\n    \"Sungaikalung\",\n    \"Sungaikalong\",\n    \"Payabakal\",\n    \"Waikala\",\n    \"Sungaikali\",\n    \"Sungai Pebakalan\",\n    \"Parit Membakal\",\n    \"Baikat Abu Jaraban\",\n    \"Maikalganj\",\n    \"Maikala Range\",\n    \"Baitalpur\",\n    \"Baikanthpur\",\n    \"Baihal\",\n    \"Barkala Reserved Forest\",\n    \"Babaipalli\",\n    \"Kaikalapettai\",\n    \"Kambainallur\",\n    \"Bakkalale\",\n    \"Kaikalui\",\n    \"Baijalpur\",\n    \"Nehalla Bankalah Reserved Forest\",\n    \"Barkala Rao\",\n    \"Barkali\",\n    \"Baidal\",\n    \"Barkaleh\",\n    \"Darreh Pumba Kal\",\n    \"Bahkalleh\",\n    \"Wibakale\",\n    \"Gaikali\",\n    \"Gagaba Kalo\",\n    \"Barkalare\",\n    \"Bakkalmal\",\n    \"Bugor Arba-Kalgan\",\n    \"Kolodets Tabakkalgan\",\n    \"Walangivattu Vaikal\",\n    \"Vattevaikal Anicut\",\n    \"Vaikali Tevar Kulam\",\n    \"Vaikalitevan Kulam\",\n    \"Vaikaladichchenai\",\n    \"Uchchodaikallu\",\n    \"Sellapattu Vaikal\",\n    \"Savata Vaikal\",\n    \"Puttadivali Vaikal\",\n    \"Palukadu Vaikal\",\n    \"Mulaikallu Kulam\",\n    \"Koraikallimadu\",\n    \"Koraikalapu Kulam\",\n    \"Karaiyamullivaikal\",\n    \"Karaivaikal Kulam\",\n    \"Kanawali Vaikal\",\n    \"Habakkala\",\n    \"Chalam Vaikal Aru\",\n    \"Ambakala Wewa\",\n    \"Alaikallupoddakulam\",\n    \"Alaikallupodda Alankulam\",\n    \"Akamadi Vaikal\",\n    \"Alaikalluppodda Kulam\",\n    \"Vaikaliththevakulam\",\n    \"Baikole\",\n    \"Sidi Mohammed Bakkal\",\n    \"Oulad el Bakkal\",\n    \"Azib el Bakkali\",\n    \"Tombakala\",\n    \"Malaikaly\",\n    \"Ambadikala\",\n    \"Abankala\",\n    \"Kombakala\",\n    \"Bawkalut\",\n    \"Bawkalut Chaung\",\n    \"Baukala\",\n    \"Cerro Bainaltzin\",\n    \"Bukit Ubaibalih\",\n    \"Kampong Sombakal\",\n    \"Kampung Lebai Ali\",\n    \"Batikal\",\n    \"Maikali\",\n    \"Abakaliki\",\n    \"Tsaunin Maikalaji\",\n    \"Baikaha\",\n    \"Llano Limbaika\",\n    \"Barkald\",\n    \"Barkald stasjon\",\n    \"Barkaleitet\",\n    \"Barkaldfossen\",\n    \"Barkaldvola\",\n    \"Bakkalegskardet\",\n    \"Baikajavri\",\n    \"Barkalden\",\n    \"Bakkalia\",\n    \"Siljabaika\",\n    \"Aikaluokta\",\n    \"Blombakkali\",\n    \"Bavkalasis\",\n    \"Baikajohka\",\n    \"Bakkalykkja\",\n    \"Baiyaldi\",\n    \"Naikala\",\n    \"Baikanda\",\n    \"Barkalne\",\n    \"Raikal\",\n    \"Baikatte\",\n    \"Maikal\",\n    \"Waikalabubu Bay\",\n    \"Baikai Island\",\n    \"Abikal\",\n    \"Boikalakalawa Bay\",\n    \"Maikal River\",\n    \"Bankal\",\n    \"Bankal School\",\n    \"Kabankalan City Public Plaza\",\n    \"Ranra Tabai Algad\",\n    \"Bairkal Jabal\",\n    \"Bairkal Dhora\",\n    \"Bairkal\",\n    \"Zaibai Algad\",\n    \"Gulba Kalle\",\n    \"Dabbarkal Sar\",\n    \"Tabai Algad\",\n    \"Haikalzai\",\n    \"Wuchobai Algad\",\n    \"Jabba Kalai\",\n    \"Goth Soba Kaloi\",\n    \"Baikar Tsarai\",\n    \"Dudgaikal\",\n    \"Baixale Kamar\",\n    \"Zebai Algad\",\n    \"Goth Haikal\",\n    \"Haikal\",\n    \"Jaba Kalle\",\n    \"Salabaikasy\",\n    \"Guba Kalita\",\n    \"Guba Kalgalaksha\",\n    \"Guba Kaldo\",\n    \"Baskalino\",\n    \"Sopka Barkaleptskaya\",\n    \"Zabaykalovskiy\",\n    \"Barkalova\",\n    \"Barkalovka\",\n    \"Gora Barkalova\",\n    \"Gora Barkalyu\",\n    \"Bikalamakhi\",\n    \"Zabaykalka\",\n    \"Kambaika\",\n    \"Bikalikha\",\n    \"Kordon Barkalo\",\n    \"Ramada Makkah Shubaika\",\n    \"Mount Tohebakala\",\n    \"Tambakale Island\",\n    \"Mbanitambaika Island\",\n    \"Mbakalaka Island\",\n    \"Kumbakale\",\n    \"Kaikaloka\",\n    \"Kelesaikal\",\n    \"Nasb Gabakallah\",\n    \"Jabal Barkal\",\n    \"Jabal Abakallah\",\n    \"Al Barkali\",\n    \"Shabakal Abbass\",\n    \"Mabaikuli\",\n    \"Bambakalema\",\n    \"Bambakalia\",\n    \"Baiwala\",\n    \"Babakalia\",\n    \"Baikama\",\n    \"Bankalol\",\n    \"Kundebakali\",\n    \"Yumbaikamadu\",\n    \"Tabakali\",\n    \"Daba Kalharereh\",\n    \"Barkale\",\n    \"Jabal Mobakali\",\n    \"Korombaital\",\n    \"Ambakali\",\n    \"Ba Kaliin\",\n    \"Tagobikala\",\n    \"Fayzabadkala\",\n    \"Aghbai Allazy\",\n    \"Aghbai Alikagar\",\n    \"Gora Fayzabadkala\",\n    \"Daraikalot\",\n    \"Aghbai Alakisirak\",\n    \"Beikala\",\n    \"Foho Berbakalau\",\n    \"Mota Caicabaisala\",\n    \"Sungai Utabailale\",\n    \"Urochishche Batkali\",\n    \"Khrebet Batkali\",\n    \"Ras Barkallah\",\n    \"Babakale\",\n    \"Fabrikalar\",\n    \"Laikala\",\n    \"Waikalakaka\",\n    \"Bakkala Cemetery\",\n    \"Clifton T Barkalow Elementary School\",\n    \"Barkalow Hollow\",\n    \"Kailuapuhi Waikalua Homesteads\",\n    \"Kawaikalia Gulch\",\n    \"Waikalae\",\n    \"Waikaloa Stream\",\n    \"Waikalua-Loko Fish Pond\",\n    \"Halekou Waikaluakai Homesteads\",\n    \"East Waikalua\",\n    \"Omar Haikal Islamic Academy\",\n    \"Koshbakaly\",\n    \"Bagkalen\",\n    \"Gora Baikara\",\n    \"Mfumbaika\",\n    \"Mbakalungu\",\n    \"Chumbaika\",\n    \"Ntombankala School\",\n    \"Khobai al Janhra\",\n    \"Holiday Inn Dubai Al Barsha\",\n    \"Novotel Dubai Al Barsha\",\n    \"Doubletree Res.Dubai-Al Barsha\",\n    \"Doubletree By Hilton Hotel and Apartments Dubai Al Barsha\",\n    \"Doubletree By Hilton Dubai Al Barsha Hotel and Res\",\n    \"Park Inn By Radisson Dubai Al Barsha\",\n    \"Ramee Rose Hotel Dubai Al Barsha\",\n    \"Aparthotel Adagio Premium Dubai Al Barsha\",\n    \"Ataikala\",\n    \"Selman Marrakech\",\n    \"Riad Ain Marrakech\",\n    \"Taj Palace Marrakech\",\n    \"Delano Marrakech\",\n    \"Pullman Marrakech Palmeraie Resort And Spa\",\n    \"Lalla Calipau Marrakech\",\n    \"Hotel Fashion Marrakech\",\n    \"Four Seasons Resort Marrakech\",\n    \"Adama Resort Marrakech\",\n    \"Pullman Marrakech Palmeraie Re\",\n    \"Ramada Resort Marrakech Douar Al Hana\",\n    \"Hotel Zahia Marrakech\",\n    \"Hotel Marrakech Le Tichka\",\n    \"Le Chems Marrakech\",\n    \"Beachcomber Royal Palm Marrakech\",\n    \"Residence Marrakech\",\n    \"Riad Hermes Marrakech\",\n    \"Riad La Lune De Marrakech\",\n    \"Hotel Marrakech Le Sangho Privilege\",\n    \"Tempoo Hotel Marrakech\",\n    \"Ag Hotel & Spa Marrakech\",\n    \"Palm Appart Club Marrakech\",\n    \"Hotel Ibis Moussafir Marrakech Palmeraie\",\n    \"Ibis Marrakech Gare Voyageurs\",\n    \"Marrakech Ryads Parc And Spa\",\n    \"Terra Mia Marrakech Riad\",\n    \"Residence Dar Lamia Marrakech\",\n    \"Pullman Marrakech Palmeraie Rs\",\n    \"Moussaf Marrakech Centre Gare\",\n    \"Tempoo Hotel Marrakech Adults Only\",\n    \"Sahara Palace Marrakech\",\n    \"Moroccan House Marrakech\",\n    \"El Andalouss And Spa Marrakech\",\n    \"Suite Novotel Marrakech Rs\",\n    \"Dar Catalina Marrakech Hotel Non Refundable Room\",\n    \"Marrakech Hotel\",\n    \"Oued Tammarrakech\",\n    \"Tammarrakech\",\n    \"Cercle de Marrakech-Banlieue\",\n    \"Marrakech-Tensift-Al Haouz\",\n    \"Koudia Marrakech\",\n    \"Hotel Tichka Salam Marrakech\",\n    \"L'Atlas Marrakech\",\n    \"Royal Mirage Deluxe Marrakech\",\n    \"Golden Tulip Farah Marrakech\",\n    \"Ryad Mogador Marrakech\",\n    \"Coralia Club Marrakech Palmariva\",\n    \"La Sultana Marrakech\",\n    \"Marrakech-Medina\",\n    \"Marrakech\",\n    \"Museum of Marrakech\",\n    \"Douar Marrakechiyinc\",\n    \"Ibis Marrakech Centre Gare\",\n    \"Golden Tulip Rawabi Marrakech\",\n    \"Murano Resort Marrakech\",\n    \"Marrakech Garden Hotel\",\n    \"Pullman Marrakech Palmerai Resort & Spa\",\n    \"The Pearl Marrakech\",\n    \"Palais Calipau Marrakech\",\n    \"Hostal Equity Point Marrakech\",\n    \"Sofitel Marrakech Lounge And Spa\",\n    \"Pullman Marrakech Hotel And Spa\",\n    \"Sofitel Marrakech Palais Imperial\",\n    \"Hotel Ibis Moussafir Marrakech Centre Gare\",\n    \"Red Hotel Marrakech\",\n    \"Riad Zenith Marrakech\",\n    \"Ksar Catalina Marrakech Hotel\",\n    \"Blue Sea Hotel Marrakech Ryads Parc & Spa\",\n    \"Bluebay Marrakech\",\n    \"Pullman Marrakech Palmeraie Resort & Spa Hotel\",\n    \"Riad Litzy Marrakech\",\n    \"Sultana Hotel & Spa Marrakech\",\n    \"Albatros Club Marrakech\",\n    \"Hotel Sangho Club Marrakech\",\n    \"Suite Novotel Marrakech Hotel\",\n    \"Riad Utopia Suites & Spa Marrakech\",\n    \"Riad Fatinat Marrakech\",\n    \"Riad Dar El Aila Marrakech\",\n    \"Es Saadi And Casino De Marrakech\",\n    \"Dar Catalina Marrakech Hotel\",\n    \"Grace Marrakech\",\n    \"Marrakesh Apartments\",\n    \"Marrakesh Country Club\",\n    \"Koudiat Lmerrakechiyine\",\n    \"Sidi Mohammed el Marrakchi\",\n    \"Marrakesh\",\n    \"Marrakchien\",\n    \"Marrakchia\",\n    \"Marrakesh Menara Airport\",\n    \"Marrakesh Hua Hin Resort & Spa\",\n    \"Marrakesh Hua Hin Resort And Spa\",\n    \"Marrakesh Resort And Spa (Pool Suite)\",\n    \"Marrakesh Huahin Resort & Spa\",\n    \"Ibis Moussafir Marrakesh Centre Gare Hotel\",\n    \"Maerak-chi\",\n    \"Dar Hammou Ben Merrakchi\",\n    \"Lalla el Marakchia\",\n    \"Khrebet Marrakh\",\n    \"Sungai Maru Kechil\",\n    \"Marrache\",\n    \"Goth Marracha\",\n    \"Maramech Hill\",\n    \"Maramech Woods Nature Preserve\",\n    \"Oued Karakech\",\n    \"Samarra School\",\n    \"Jangal-e Marakeh Sar\",\n} union (\n    (insert Gist {\n        p_str := x\n    })\n    union\n    (insert Gist2 {\n        p_str := x,\n        p_str_2 := x,\n    })\n);\n"
  },
  {
    "path": "tests/schemas/pg_unaccent.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\ntype Post {\n    title: str;\n    body: str;\n\n    index fts::index on ((\n        fts::with_options(\n            .title,\n            language := fts::Language.eng\n        ),\n        fts::with_options(\n            ext::pg_unaccent::unaccent(.body),\n            language := fts::Language.eng,\n        ),\n    ));\n};\n"
  },
  {
    "path": "tests/schemas/pgvector.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nscalar type v3 extending ext::pgvector::vector<3>;\nscalar type hv3 extending ext::pgvector::halfvec<3>;\nscalar type sv3 extending ext::pgvector::sparsevec<3>;\n\nscalar type myf64 extending float64 {\n    constraint max_value(100);\n}\nscalar type deepf64 extending myf64;\n\n\ntype Basic {\n    required p_str: str;\n    p_json: json {rewrite insert using (to_json(__subject__.p_str))};\n}\n\ntype IVFFlat_vec_L2 {\n    required vec: v3;\n    index ext::pgvector::ivfflat_euclidean(lists := 100) on (.vec);\n}\n\ntype IVFFlat_vec_IP {\n    required vec: v3;\n    index ext::pgvector::ivfflat_ip(lists := 100) on (.vec);\n}\n\ntype IVFFlat_vec_Cosine {\n    required vec: v3;\n    index ext::pgvector::ivfflat_cosine(lists := 100) on (.vec);\n}\n\ntype HNSW_vec_L2 {\n    required vec: v3;\n    index ext::pgvector::hnsw_euclidean() on (.vec);\n}\n\ntype HNSW_vec_IP {\n    required vec: v3;\n    index ext::pgvector::hnsw_ip(m := 4) on (.vec);\n}\n\ntype HNSW_vec_Cosine {\n    required vec: v3;\n    index ext::pgvector::hnsw_cosine(m := 2, ef_construction := 4) on (.vec);\n}\n\ntype HNSW_vec_L1 {\n    required vec: v3;\n    index ext::pgvector::hnsw_taxicab() on (.vec);\n}\n\ntype IVFFlat_hv_L2 {\n    required vec: hv3;\n    index ext::pgvector::ivfflat_hv_euclidean(lists := 100) on (.vec);\n}\n\ntype IVFFlat_hv_IP {\n    required vec: hv3;\n    index ext::pgvector::ivfflat_hv_ip(lists := 100) on (.vec);\n}\n\ntype IVFFlat_hv_Cosine {\n    required vec: hv3;\n    index ext::pgvector::ivfflat_hv_cosine(lists := 100) on (.vec);\n}\n\ntype HNSW_hv_L2 {\n    required vec: hv3;\n    index ext::pgvector::hnsw_hv_euclidean() on (.vec);\n}\n\ntype HNSW_hv_IP {\n    required vec: hv3;\n    index ext::pgvector::hnsw_hv_ip(m := 4) on (.vec);\n}\n\ntype HNSW_hv_Cosine {\n    required vec: hv3;\n    index ext::pgvector::hnsw_hv_cosine(m := 2, ef_construction := 4) on (.vec);\n}\n\ntype HNSW_hv_L1 {\n    required vec: hv3;\n    index ext::pgvector::hnsw_hv_taxicab() on (.vec);\n}\n\ntype HNSW_sv_L2 {\n    required vec: sv3;\n    index ext::pgvector::hnsw_sv_euclidean() on (.vec);\n}\n\ntype HNSW_sv_IP {\n    required vec: sv3;\n    index ext::pgvector::hnsw_sv_ip(m := 4) on (.vec);\n}\n\ntype HNSW_sv_Cosine {\n    required vec: sv3;\n    index ext::pgvector::hnsw_sv_cosine(m := 2, ef_construction := 4) on (.vec);\n}\n\ntype HNSW_sv_L1 {\n    required vec: sv3;\n    index ext::pgvector::hnsw_sv_taxicab() on (.vec);\n}\n\ntype Con {\n    required vec: v3 {\n        constraint expression on (\n            ext::pgvector::cosine_distance(\n                __subject__, <ext::pgvector::vector>[1, 1, 1]\n            ) < 0.2\n        )\n    }\n}\n\n\ntype Raw {\n    required val: float64;\n\n    p_int16: int16 {rewrite insert using (<int16>__subject__.val)};\n    p_int32: int32 {rewrite insert using (<int32>__subject__.val)};\n    p_int64: int64 {rewrite insert using (<int64>__subject__.val)};\n    p_bigint: bigint {rewrite insert using (<bigint>__subject__.val)};\n    p_float32: float32 {rewrite insert using (<float32>__subject__.val)};\n    p_decimal: decimal {rewrite insert using (<decimal>__subject__.val)};\n\n    p_myf64: myf64 {rewrite insert using (<myf64>__subject__.val)};\n    p_deepf64: deepf64 {rewrite insert using (<deepf64>__subject__.val)};\n}\n"
  },
  {
    "path": "tests/schemas/pgvector_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2023-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nfor x in {0, 3, 4.25, 6.75}\nunion (\n    insert Raw {val := x}\n);\n\n\nfor x in {[0, 1, 2.3], [1, 1, 10.11], [4.5, 6.7, 8.9]}\nunion (\n    (insert Basic {p_str := to_str(<json>x)}),\n    (insert IVFFlat_vec_L2 {vec := <v3>x}),\n    (insert IVFFlat_vec_IP {vec := <v3>x}),\n    (insert IVFFlat_vec_Cosine {vec := <v3>x}),\n    (insert HNSW_vec_L2 {vec := <v3>x}),\n    (insert HNSW_vec_IP {vec := <v3>x}),\n    (insert HNSW_vec_Cosine {vec := <v3>x}),\n    (insert HNSW_vec_L1 {vec := <v3>x}),\n\n    (insert IVFFlat_hv_L2 {vec := <hv3>x}),\n    (insert IVFFlat_hv_IP {vec := <hv3>x}),\n    (insert IVFFlat_hv_Cosine {vec := <hv3>x}),\n    (insert HNSW_hv_L2 {vec := <hv3>x}),\n    (insert HNSW_hv_IP {vec := <hv3>x}),\n    (insert HNSW_hv_Cosine {vec := <hv3>x}),\n    (insert HNSW_hv_L1 {vec := <hv3>x}),\n\n    (insert HNSW_sv_L2 {vec := <sv3><v3>x}),\n    (insert HNSW_sv_IP {vec := <sv3><v3>x}),\n    (insert HNSW_sv_Cosine {vec := <sv3><v3>x}),\n    (insert HNSW_sv_L1 {vec := <sv3><v3>x}),\n);\n"
  },
  {
    "path": "tests/schemas/smoke_test_interp.esdl",
    "content": "type Note {\n    required single property name -> str;\n    optional single property note -> str;\n}\ntype Person {\n    required single property name -> str;\n    optional multi property multi_prop -> str;\n    multi link notes -> Note {\n        property metanote -> str;\n    }\n    optional single property tag -> str;\n}\ntype Foo {\n    required single property val -> str;\n    optional single property opt -> int64;\n}\ntype Award {\n    name : str;\n}\ntype Card {\n    single name : str;\n    multi awards : Award;\n    element : str;\n    cost : int64;\n}\n\n\ntype User {\n    required name: str {\n        delegated constraint exclusive;\n    }\n\n    multi deck: Card {\n        count: int64 {\n            default := 1;\n        };\n        property total_cost := @count * .cost;\n    }\n\n    property deck_cost := sum(.deck.cost);\n\n    multi friends: User {\n        nickname: str;\n        # how the friend responded to requests for a favor\n        #favor: array<bool>\n    }\n\n    multi awards: Award {\n        constraint exclusive;\n    }\n\n    avatar: Card {\n        text: str;\n        property tag := .name ++ ((\"-\" ++ @text) ?? \"\");\n    }\n}\n\ntype Publication {\n    required title: str;\n\n    multi authors: User {\n        list_order: int64;\n    };\n}"
  },
  {
    "path": "tests/schemas/smoke_test_interp_setup.edgeql",
    "content": "# # I really hope this query could work, but due to multiple bugs, it cannot work at the momoent.\n\n# with n0 := (insert Note {name := \"boxing\", note := {}}),\n#      n1 := (insert Note {name := \"unboxing\", note := \"lolol\"}),\n#      n2 := (insert Note {name := \"dynamic\", note := \"blarg\"}),\n#      p0 := (insert Person {name := \"Phil Emarg\", notes := {n0, n1 {@metanote := \"arg!\"}}}),\n#      p1 := (insert Person {name := \"Madeline Hatch\", notes:={n1 {@metanote := \"sigh\"}}}),\n#      p2 := (insert Person {name := \"Emmanuel Villip\"}),\n#      a_15 := (insert Award {name := \"1st\"}), \n#      a_e1 := (insert Award {name := \"2nd\"}),\n#      a_ca := (insert Award {name := \"3rd\"}),\n#      c_27 := (insert Card {name := \"Imp\", element := \"Fire\", cost := 1, awards := {a_e1}}),\n#      c_49 := (insert Card {name := \"Dragon\", element := \"Fire\",  cost := 5, awards := {a_15}}),\n#      c_80 := (insert Card {name := \"Bog monster\", element := \"Water\", cost := 2}),\n#      c_d2 := (insert Card {name := \"Giant turtle\", element := \"Water\", cost := 3}),\n#      c_46 := (insert Card {name := 'Dwarf', element := 'Earth', cost := 1}),\n#      c_25 := (insert Card {name := 'Golem', element := 'Earth', cost := 3}),\n#      c_bd := (insert Card {name := 'Sprite', element := 'Air', cost := 1}),\n#      c_69 := (insert Card {name := 'Giant eagle', element := 'Air', cost := 2}),\n#      c_87 := (insert Card {name := 'Djinn', element := 'Air', cost := 4, awards := {a_ca}}),\n#      u_3e := (insert User {name := \"Carol\", deck := {c_80 { @count := 3}, \n#             c_d2 {@count := 2}, c_46 {@count := 4}, c_25 {@count := 2},\n#             c_bd {@count := 4}, c_69 {@count := 3}, c_87 {@count := 1}\n#         }}),\n#     u_fc := (insert User {name := \"Bob\", deck := {\n#             c_80 {@count := 3},\n#             c_d2 {@count := 3},\n#             c_46 {@count := 3},\n#             c_25 {@count := 3}\n#         }}), \n#     u_56 := (insert User {name := \"Dave\", deck := {\n#            c_49  {@count:= 1},\n#            c_80  {@count:= 1},\n#            c_d2  {@count:= 1},\n#            c_25  {@count:= 1},\n#            c_bd  {@count:= 4},\n#            c_69  {@count:= 1},\n#            c_87  {@count:= 1}\n#         }, friends := {u_fc}, avatar := c_87 {@text := \"Wow\"}}),\n#     u_f3 := (insert User {name := \"Alice\", deck := {\n#             c_27 {@count:= 2},\n#             c_49 {@count:= 2},\n#             c_80 {@count:= 3},\n#             c_d2 {@count:= 3}\n#         }, friends := {\n#             u_fc {@nickname := \"Swampy\"},\n#             u_3e {@nickname := \"Firefighter\"},\n#             u_56 {@nickname := \"Grumpy\"}\n#         }, awards := {a_15, a_31}, \n#             avatar := {c_49 {@text := \"Best\"}}\n#         }),\n    \n\n# select 0;\n\n\n# COPY of CARDS_SETUP\n\n\ninsert Note {name := \"boxing\", note := {}};\ninsert Note {name := \"unboxing\", note := \"lolol\"};\ninsert Note {name := \"dynamic\", note := \"blarg\"};\n# This obviously should work but it doesn't\n# insert Person {name := \"Phil Emarg\", notes := {(select Note filter .name = \"boxing\"), \n#                                                 (select Note {@metanote := \"arg!\"} filter .name = \"unboxing\")}};\ninsert Person {name := \"Phil Emarg\", notes := (select Note {@metanote := <str>{} if .name != \"unboxing\" else \"arg!\"}\n                                                    filter .name = \"boxing\" or .name = \"unboxing\")};\ninsert Person {name := \"Madeline Hatch\", notes:=(select Note {@metanote := \"sigh\"} filter .name = \"unboxing\")};\ninsert Person {name := \"Emmanuel Villip\"};\n\nFOR award in {'1st', '2nd', '3rd'} UNION (\n    INSERT Award { name := award }\n);\n\nINSERT Card {\n    name := 'Imp',\n    element := 'Fire',\n    cost := 1,\n    awards := (SELECT Award FILTER .name = '2nd'),\n};\n\nINSERT Card {\n    name := 'Dragon',\n    element := 'Fire',\n    cost := 5,\n    awards := (SELECT Award FILTER .name IN {'1st', '3rd'}),\n};\n\nINSERT Card {\n    name := 'Bog monster',\n    element := 'Water',\n    cost := 2\n};\n\nINSERT Card {\n    name := 'Giant turtle',\n    element := 'Water',\n    cost := 3\n};\n\nINSERT Card {\n    name := 'Dwarf',\n    element := 'Earth',\n    cost := 1\n};\n\nINSERT Card {\n    name := 'Golem',\n    element := 'Earth',\n    cost := 3\n};\n\nINSERT Card {\n    name := 'Sprite',\n    element := 'Air',\n    cost := 1\n};\n\nINSERT Card {\n    name := 'Giant eagle',\n    element := 'Air',\n    cost := 2\n};\n\ninsert Card {\n    name := 'Djinn', \n    element := 'Air', \n    cost := 4, \n    awards := (select Award filter .name = \"3rd\"),\n};\n\n# INSERT SpecialCard {\n#     name := 'Djinn',\n#     element := 'Air',\n#     cost := 4,\n#     awards := (SELECT Award FILTER .name = '3rd'),\n# };\n\n\n# create players & decks\nINSERT User {\n    name := 'Alice',\n    deck := (\n        SELECT Card {@count := len(Card.element) - 2}\n        FILTER .element IN {'Fire', 'Water'}\n    ),\n    awards := (SELECT Award FILTER .name IN {'1st', '2nd'}),\n    avatar := (\n        SELECT Card {@text := 'Best'} FILTER .name = 'Dragon' LIMIT 1\n    ),\n};\n\nINSERT User {\n    name := 'Bob',\n    deck := (\n        SELECT Card {@count := 3} FILTER .element IN {'Earth', 'Water'}\n    ),\n    awards := (SELECT Award FILTER .name = '3rd'),\n};\n\nINSERT User {\n    name := 'Carol',\n    deck := (\n        SELECT Card {@count := 5 - Card.cost} FILTER .element != 'Fire'\n    )\n};\n\nINSERT User {\n    name := 'Dave',\n    deck := (\n        SELECT Card {@count := 4 IF Card.cost = 1 ELSE 1}\n        FILTER .element = 'Air' OR .cost != 1\n    ),\n    avatar := (\n        SELECT Card {@text := 'Wow'} FILTER .name = 'Djinn' LIMIT 1\n    ),\n};\n\n# update friends list\nWITH\n    U2 := DETACHED User\nUPDATE User\nFILTER User.name = 'Alice'\nSET {\n    friends := (\n        SELECT U2 {\n            @nickname :=\n                'Swampy'        IF U2.name = 'Bob' ELSE\n                'Firefighter'   IF U2.name = 'Carol' ELSE\n                'Grumpy'\n        } FILTER U2.name IN {'Bob', 'Carol', 'Dave'}\n    )\n};\n\nWITH\n    U2 := DETACHED User\nUPDATE User\nFILTER User.name = 'Dave'\nSET {\n    friends := (\n        SELECT U2 FILTER U2.name = 'Bob'\n    )\n};\n"
  },
  {
    "path": "tests/schemas/tree.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n# A tree setup in a simple way with a parent link and computable children.\ntype Tree {\n    required val: str {\n        constraint exclusive;\n    }\n\n    parent: Tree;\n    multi link children := .<parent[IS Tree];\n}\n\n\n# A tree setup in a reverse way compared to Tree: children links are\n# real and parent is computable.\ntype Eert {\n    required val: str {\n        constraint exclusive;\n    }\n\n    # We need the limit 1 because children isn't exclusive and we can't\n    # have it be exclusive if we want to do an atomic swap...\n    link parent := (SELECT .<children[IS Eert] LIMIT 1);\n    multi children: Eert;\n}\n"
  },
  {
    "path": "tests/schemas/tree_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\n\nINSERT Tree {val := '0'};\nINSERT Tree {\n    val := '00', parent := (SELECT DETACHED Tree FILTER .val = '0')};\nINSERT Tree {\n    val := '01', parent := (SELECT DETACHED Tree FILTER .val = '0')};\nINSERT Tree {\n    val := '02', parent := (SELECT DETACHED Tree FILTER .val = '0')};\nINSERT Tree {\n    val := '000', parent := (SELECT DETACHED Tree FILTER .val = '00')};\nINSERT Tree {\n    val := '010', parent := (SELECT DETACHED Tree FILTER .val = '01')};\n\nINSERT Tree {val := '1'};\nINSERT Tree {\n    val := '10', parent := (SELECT DETACHED Tree FILTER .val = '1')};\nINSERT Tree {\n    val := '11', parent := (SELECT DETACHED Tree FILTER .val = '1')};\nINSERT Tree {\n    val := '12', parent := (SELECT DETACHED Tree FILTER .val = '1')};\nINSERT Tree {\n    val := '13', parent := (SELECT DETACHED Tree FILTER .val = '1')};\n\n\n# same structure using a different tree type\nINSERT Eert {val := '000'};\nINSERT Eert {val := '010'};\n\nINSERT Eert {\n    val := '00', children := (SELECT DETACHED Eert FILTER .val = '000')};\nINSERT Eert {\n    val := '01', children := (SELECT DETACHED Eert FILTER .val = '010')};\nINSERT Eert {val := '02'};\n\nINSERT Eert {\n    val := '0',\n    children := (SELECT DETACHED Eert FILTER .val IN {'00', '01', '02'}),\n};\n\nINSERT Eert {val := '10'};\nINSERT Eert {val := '11'};\nINSERT Eert {val := '12'};\nINSERT Eert {val := '13'};\n\nINSERT Eert {\n    val := '1',\n    children := (SELECT DETACHED Eert FILTER .val IN {'10', '11', '12', '13'}),\n};\n"
  },
  {
    "path": "tests/schemas/updates.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nINSERT Status {\n    name := 'Open'\n};\n\nINSERT Status {\n    name := 'Closed'\n};\n\nINSERT MajorLifeEvent {\n    name := 'Broke a Type System'\n};\n\nINSERT MajorLifeEvent {\n    name := 'Downloaded a Car'\n};\n\nINSERT Tag {\n    name := 'fun'\n};\n\nINSERT Tag {\n    name := 'boring'\n};\n\nINSERT Tag {\n    name := 'wow'\n};\n\nINSERT UpdateTest {\n    name := 'update-test1',\n    status := (SELECT Status FILTER Status.name = 'Open'),\n    readonly_tag := (SELECT Tag FILTER .name = 'wow'),\n    readonly_note := 'this is read-only',\n};\n\nINSERT UpdateTest {\n    name := 'update-test2',\n    comment := 'second',\n    status := (SELECT Status FILTER Status.name = 'Open')\n};\n\nINSERT UpdateTest {\n    name := 'update-test3',\n    comment := 'third',\n    status := (SELECT Status FILTER Status.name = 'Closed')\n};\n\nINSERT CollectionTest {\n    name := 'collection-test1'\n};\n"
  },
  {
    "path": "tests/schemas/updates.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ntype Status {\n    required property name -> str {\n        constraint exclusive;\n    }\n}\ntype MajorLifeEvent extending Status;\n\ntype Tag {\n    required property name -> str {\n        constraint exclusive;\n    }\n    required property flag -> int64 {\n        default := 0;\n    }\n}\n\ntype UpdateTest {\n    required property name -> str {\n        constraint exclusive;\n    }\n    property comment -> str;\n\n    # for testing singleton links\n    link status -> Status;\n    link annotated_status -> Status {\n        property note -> str;\n    }\n    link annotated_status2 -> Status {\n        property note -> str;\n    }\n\n    # for testing links to sets\n    multi link tags -> Tag;\n    multi link weighted_tags -> Tag {\n        property weight -> int64;\n        property note -> str;\n        property readonly_note -> str {\n            readonly := true;\n        }\n    }\n    multi link statuses -> Status;\n\n    # for testing links to sets of the same type as originator\n    multi link related -> UpdateTest;\n    multi link annotated_tests -> UpdateTest {\n        property note -> str;\n    }\n\n    link readonly_tag -> Tag {\n        readonly := true;\n    }\n\n    property readonly_note -> str {\n        readonly := true;\n    }\n\n    multi property str_tags -> str;\n}\n\ntype UpdateTestSubType extending UpdateTest;\n\ntype UpdateTestSubSubType extending UpdateTestSubType {\n    overloaded link status -> MajorLifeEvent;\n    overloaded multi link statuses -> MajorLifeEvent;\n};\n\ntype CollectionTest {\n    required property name -> str;\n    property some_tuple -> tuple<str, int64>;\n    property str_array -> array<str>;\n}\n\ntype MultiRequiredTest {\n    required property name -> str {\n        constraint exclusive;\n    };\n    required multi property prop -> str;\n    required multi link tags -> Tag;\n}\n\ntype DunderDefaultTest01 {\n    required a: int64;\n    required b: int64 {\n        default := __source__.a+1\n    };\n    required c: int64 {\n        default := 1\n    }\n}\n\ntype DunderDefaultTest02_A {\n    required a: int64 {\n        default := 1\n    };\n}\ntype DunderDefaultTest02_B {\n    multi default_with_insert: DunderDefaultTest02_A {\n        default := (\n            insert DunderDefaultTest02_A {\n                a := 1\n            }\n        )\n    };\n    multi default_with_update: DunderDefaultTest02_A {\n        default := (\n            update DunderDefaultTest02_A\n            filter DunderDefaultTest02_A.a = 2\n            set {\n                a := 22\n            }\n        )\n    };\n    multi default_with_delete: DunderDefaultTest02_A {\n        default := (\n            delete DunderDefaultTest02_A\n            filter DunderDefaultTest02_A.a = 3\n        )\n    };\n    multi default_with_select: DunderDefaultTest02_A {\n        default := (\n            select DunderDefaultTest02_A\n            filter DunderDefaultTest02_A.a = 4\n        )\n    };\n}\n\ntype DunderDefaultTest03_A {\n    required x: int64;\n}\ntype DunderDefaultTest03_B {\n    required x: int64 {\n        default := 1\n    };\n}\ntype DunderDefaultTest03_C {\n    required x: int64 {\n        default := 2\n    };\n}\n\ntype DunderDefaultTest04_A {\n    required x: int64;\n};\ntype DunderDefaultTest04_B {\n    required x: int64;\n    l: DunderDefaultTest04_A {\n        default := (\n            select DunderDefaultTest04_A\n            limit 1\n        )\n    };\n};\n"
  },
  {
    "path": "tests/schemas/volatility.esdl",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ntype Obj {\n    required property n -> int64;\n    multi link tgt -> Tgt;\n}\n\ntype Tgt {\n    required property n -> int64;\n}\n"
  },
  {
    "path": "tests/schemas/volatility_setup.edgeql",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nCREATE FUNCTION vol_immutable() -> float64 {\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT random();\n    $$;\n};\n\nCREATE FUNCTION vol_stable() -> float64 {\n    SET volatility := 'Stable';\n    USING SQL $$\n        SELECT random();\n    $$;\n};\n\nCREATE FUNCTION vol_volatile() -> float64 {\n    SET volatility := 'Volatile';\n    USING SQL $$\n        SELECT random();\n    $$;\n};\n\nCREATE FUNCTION err_immutable() -> float64 {\n    SET volatility := 'Immutable';\n    USING SQL $$\n        SELECT random()/0;\n    $$;\n};\n\nCREATE FUNCTION err_stable() -> float64 {\n    SET volatility := 'Stable';\n    USING SQL $$\n        SELECT random()/0;\n    $$;\n};\n\nCREATE FUNCTION err_volatile() -> float64 {\n    SET volatility := 'Volatile';\n    USING SQL $$\n        SELECT random()/0;\n    $$;\n};\n\nCREATE FUNCTION rand_int(top: int64) -> int64 {\n    USING (<int64>(random() * top))\n};\n\nCREATE FUNCTION vol_id(x: int64) -> int64 {\n    SET volatility := 'Volatile';\n    USING (x)\n};\n\nCREATE SCALAR TYPE TestCounter extending std::sequence;\nCREATE FUNCTION next() -> int64 USING (\n\tSELECT sequence_next(INTROSPECT TestCounter)\n);\n\nINSERT Tgt { n := 1 };\nINSERT Tgt { n := 2 };\nINSERT Tgt { n := 3 };\nINSERT Tgt { n := 4 };\nINSERT Obj { n := 1, tgt := (SELECT Tgt FILTER .n IN {1, 2}) };\nINSERT Obj { n := 2, tgt := (SELECT Tgt FILTER .n IN {2, 3}) };\nINSERT Obj { n := 3, tgt := (SELECT Tgt FILTER .n IN {3, 4}) };\n"
  },
  {
    "path": "tests/test_api_errors.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport unittest\n\nfrom edb import errors\n\n\nclass TestErrorsClasses(unittest.TestCase):\n\n    def test_api_errors_01(self):\n        # Test that \"edb genexc\" tool generates correct\n        # class hierarchy.\n\n        self.assertTrue(\n            issubclass(errors.InternalServerError, errors.EdgeDBError))\n\n        self.assertEqual(\n            errors.InternalServerError('error').get_code(), 0x_01_00_00_00)\n\n        self.assertTrue(\n            issubclass(errors.ProtocolError, errors.EdgeDBError))\n\n        self.assertTrue(\n            issubclass(errors.BinaryProtocolError, errors.ProtocolError))\n\n        self.assertTrue(\n            issubclass(errors.QueryError, errors.EdgeDBError))\n        self.assertTrue(\n            issubclass(errors.InvalidTypeError, errors.QueryError))\n        self.assertTrue(\n            issubclass(errors.InvalidTargetError, errors.InvalidTypeError))\n        self.assertTrue(\n            issubclass(\n                errors.InvalidLinkTargetError, errors.InvalidTargetError))\n        self.assertFalse(\n            issubclass(\n                errors.InvalidLinkTargetError,\n                errors.InvalidPropertyTargetError))\n\n    def test_api_errors_02(self):\n        # Test that \"edb genexc\" tool doesn't generate errors\n        # intended for client libraries.\n\n        self.assertFalse(hasattr(errors, 'ClientError'))\n"
  },
  {
    "path": "tests/test_backend_connect.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nimport warnings\nfrom typing import Optional, Unpack\n\nimport asyncio\nimport contextlib\nimport ipaddress\nimport os\nimport pathlib\nimport pickle\nimport platform\nimport shutil\nimport socket\nimport ssl\nimport stat\nimport subprocess\nimport sys\nimport tempfile\nimport textwrap\nimport unittest\nimport unittest.mock\nimport urllib.parse\n\nimport click\nfrom click.testing import CliRunner\n\nfrom edb.pgsql import params as pg_params\nfrom edb.server import args as edb_args\nfrom edb.server import bootstrap\nfrom edb.server import pgcluster\nfrom edb.server import pgconnparams\nfrom edb.server.pgconnparams import SSLMode\nfrom edb.server import pgcon\nfrom edb.server.pgcon import errors\nfrom edb.testbase import server as tb\n\n\nCERTS = os.path.join(os.path.dirname(__file__), 'certs')\nSSL_CA_CERT_FILE = os.path.join(CERTS, 'ca.cert.pem')\nSSL_CA_CRL_FILE = os.path.join(CERTS, 'ca.crl.pem')\nSSL_CERT_FILE = os.path.join(CERTS, 'server.cert.pem')\nSSL_KEY_FILE = os.path.join(CERTS, 'server.key.pem')\nCLIENT_CA_CERT_FILE = os.path.join(CERTS, 'client_ca.cert.pem')\nCLIENT_SSL_CERT_FILE = os.path.join(CERTS, 'client.cert.pem')\nCLIENT_SSL_KEY_FILE = os.path.join(CERTS, 'client.key.pem')\nCLIENT_SSL_PROTECTED_KEY_FILE = os.path.join(CERTS, 'client.key.protected.pem')\n\n\n@contextlib.contextmanager\ndef mock_dot_postgresql(*, ca=True, crl=False, client=False, protected=False):\n    with tempfile.TemporaryDirectory() as temp_dir:\n        home = pathlib.Path(temp_dir)\n        pg_home = home / '.postgresql'\n        pg_home.mkdir()\n        if ca:\n            shutil.copyfile(SSL_CA_CERT_FILE, pg_home / 'root.crt')\n        if crl:\n            shutil.copyfile(SSL_CA_CRL_FILE, pg_home / 'root.crl')\n        if client:\n            shutil.copyfile(CLIENT_SSL_CERT_FILE, pg_home / 'postgresql.crt')\n            if protected:\n                shutil.copyfile(\n                    CLIENT_SSL_PROTECTED_KEY_FILE, pg_home / 'postgresql.key'\n                )\n            else:\n                shutil.copyfile(\n                    CLIENT_SSL_KEY_FILE, pg_home / 'postgresql.key'\n                )\n        with unittest.mock.patch(\n            'pathlib.Path.home', unittest.mock.Mock(return_value=home)\n        ):\n            yield\n\n\ndef _get_initdb_options(initdb_options=None):\n    if not initdb_options:\n        initdb_options = {}\n    else:\n        initdb_options = dict(initdb_options)\n\n    # Make the default superuser name stable.\n    if 'username' not in initdb_options:\n        initdb_options['username'] = 'postgres'\n\n    return initdb_options\n\n\n@click.command()\n@edb_args.server_options\ndef get_default_args(version, **kwargs):\n    pickle.dump(kwargs, sys.stdout.buffer)\n\n\nclass TempCluster(pgcluster.Cluster):\n    def __init__(self, *,\n                 data_dir_suffix=None, data_dir_prefix=None,\n                 data_dir_parent=None):\n        self._data_dir = tempfile.mkdtemp(suffix=data_dir_suffix,\n                                          prefix=data_dir_prefix,\n                                          dir=data_dir_parent)\n        super().__init__(self._data_dir)\n\n\nclass ClusterTestCase(tb.TestCase):\n    cluster: Optional[TempCluster]\n    loop: asyncio.AbstractEventLoop\n\n    @classmethod\n    def get_server_settings(cls):\n        return {\n            'log_connections': 'on',\n            # JITting messes up timing tests, and\n            # is not essential for testing.\n            'jit': 'off',\n            'listen_addresses': '127.0.0.1',\n        }\n\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        cls.loop.run_until_complete(cls.init_temp_cluster())\n\n    @classmethod\n    async def init_temp_cluster(cls):\n        cluster = cls.cluster = TempCluster()\n        await cluster.lookup_postgres()\n        cluster.update_connection_params(\n            user='postgres',\n        )\n        await cluster.init(**_get_initdb_options({}))\n        await cluster.trust_local_connections()\n        port = tb.find_available_port()\n        await cluster.start(\n            port=port,\n            server_settings=cls.get_server_settings(),\n            wait=120,\n        )\n        result = CliRunner().invoke(get_default_args, [])\n        arg_input = pickle.loads(result.stdout_bytes)\n        arg_input[\"data_dir\"] = pathlib.Path(cluster.get_data_dir())\n        arg_input[\"multitenant_config_file\"] = \"\"\n        arg_input[\"tls_cert_mode\"] = \"generate_self_signed\"\n        arg_input[\"jose_key_mode\"] = \"generate\"\n        cls.dbname = cluster.get_db_name('main')  # XXX\n        args = edb_args.parse_args(**arg_input)\n        await bootstrap.ensure_bootstrapped(cluster, args)\n\n    @classmethod\n    def tearDownClass(cls):\n        try:\n            cluster, cls.cluster = cls.cluster, None\n            if cluster is not None:\n                try:\n                    cls.loop.run_until_complete(cluster.stop())\n                finally:\n                    cluster.destroy()\n        finally:\n            super().tearDownClass()\n\n    @classmethod\n    async def connect(cls, **kwargs: Unpack[pgconnparams.CreateParamsKwargs]):\n        import inspect\n        assert cls.cluster is not None\n        source_description = (\"ClusterTestCase: \"\n                              f\"{inspect.currentframe().f_back.f_code.co_name}\")  # type: ignore\n        kwargs['database'] = cls.dbname\n        return await cls.cluster.connect(\n            source_description=source_description,\n            **kwargs\n        )\n\n    def setUp(self):\n        super().setUp()\n\n        self.con = self.loop.run_until_complete(self.connect())\n\n    def tearDown(self):\n        try:\n            if self.con:\n                self.con.terminate()\n            self.con = None\n        finally:\n            super().tearDown()\n\n    async def assertConnected(self, con):\n        self.assertEqual(await con.sql_fetch_val(b\"SELECT 'OK'\"), b'OK')\n\n\nclass TestAuthentication(ClusterTestCase):\n    def setUp(self):\n        super().setUp()\n\n        methods = [\n            ('trust', None),\n            ('reject', None),\n            ('scram-sha-256', 'correctpassword'),\n            ('md5', 'correctpassword'),\n            ('password', 'correctpassword'),\n        ]\n\n        self.cluster.reset_hba()\n\n        create_script = []\n        for method, password in methods:\n            username = method.replace('-', '_')\n\n            # if this is a SCRAM password, we need to set the encryption method\n            # to \"scram-sha-256\" in order to properly hash the password\n            if method == 'scram-sha-256':\n                create_script.append(\n                    \"SET password_encryption = 'scram-sha-256';\"\n                )\n\n            create_script.append(\n                'CREATE ROLE {}_user WITH LOGIN{};'.format(\n                    username,\n                    ' PASSWORD {!r}'.format(password) if password else ''\n                )\n            )\n            create_script.append(\n                f'GRANT postgres TO {username}_user;'\n            )\n\n            # to be courteous to the MD5 test, revert back to MD5 after the\n            # scram-sha-256 password is set\n            if method == 'scram-sha-256':\n                create_script.append(\n                    \"SET password_encryption = 'md5';\"\n                )\n\n            self.cluster.add_hba_entry(\n                type='local',\n                database=self.dbname, user='{}_user'.format(username),\n                auth_method=method)\n\n            self.cluster.add_hba_entry(\n                type='host', address=ipaddress.ip_network('127.0.0.0/24'),\n                database=self.dbname, user='{}_user'.format(username),\n                auth_method=method)\n\n            self.cluster.add_hba_entry(\n                type='host', address=ipaddress.ip_network('::1/128'),\n                database=self.dbname, user='{}_user'.format(username),\n                auth_method=method)\n\n        # Put hba changes into effect\n        self.loop.run_until_complete(self.cluster.reload())\n\n        create_script = '\\n'.join(create_script).encode()\n        self.loop.run_until_complete(self.con.sql_execute(create_script))\n\n    def tearDown(self):\n        # Reset cluster's pg_hba.conf since we've meddled with it\n        self.loop.run_until_complete(self.cluster.trust_local_connections())\n\n        methods = [\n            'trust',\n            'reject',\n            'scram-sha-256',\n            'md5',\n            'password',\n        ]\n\n        drop_script = []\n        for method in methods:\n            username = method.replace('-', '_')\n\n            drop_script.append('DROP ROLE {}_user;'.format(username))\n\n        drop_script = '\\n'.join(drop_script).encode()\n        self.loop.run_until_complete(self.con.sql_execute(drop_script))\n\n        super().tearDown()\n\n    async def test_auth_bad_user(self):\n        with self.assertRaises(errors.BackendError) as cm:\n            await self.connect(user='__nonexistent__')\n        self.assertTrue(\n            cm.exception.code_is(\n                errors.ERROR_INVALID_AUTHORIZATION_SPECIFICATION\n            )\n        )\n\n    async def test_auth_trust(self):\n        conn = await self.connect(user='trust_user')\n        conn.terminate()\n\n    async def test_auth_reject(self):\n        with self.assertRaisesRegex(\n            errors.BackendError,\n            'pg_hba.conf rejects connection'\n        ):\n            await self.connect(user='reject_user')\n\n    async def test_auth_password_cleartext(self):\n        conn = await self.connect(\n            user='password_user',\n            password='correctpassword')\n        conn.terminate()\n\n    async def test_auth_password_md5(self):\n        conn = await self.connect(\n            user='md5_user', password='correctpassword')\n        conn.terminate()\n\n        with self.assertRaisesRegex(\n            errors.BackendError,\n            'password authentication failed for user \"md5_user\"'\n        ):\n            await self.connect(\n                user='md5_user', password='wrongpassword')\n\n    async def test_auth_password_scram_sha_256(self):\n        conn = await self.connect(\n            user='scram_sha_256_user', password='correctpassword')\n        conn.terminate()\n\n        with self.assertRaisesRegex(\n            errors.BackendError,\n            'password authentication failed for user \"scram_sha_256_user\"'\n        ):\n            await self.connect(\n                user='scram_sha_256_user', password='wrongpassword')\n\n        # various SASL prep tests\n        # first ensure that password are being hashed for SCRAM-SHA-256\n        await self.con.sql_execute(\n            b\"SET password_encryption = 'scram-sha-256';\",\n        )\n        alter_password = \"ALTER ROLE scram_sha_256_user PASSWORD E{!r};\"\n        passwords = [\n            'nonascii\\u1680space',  # C.1.2\n            'common\\u1806nothing',  # B.1\n            'ab\\ufb01c',            # normalization\n            'ab\\u007fc',            # C.2.1\n            'ab\\u206ac',            # C.2.2, C.6\n            'ab\\ue000c',            # C.3, C.5\n            'ab\\ufdd0c',            # C.4\n            'ab\\u2ff0c',            # C.7\n            'ab\\u2000c',            # C.8\n            'ab\\ue0001',            # C.9\n        ]\n\n        # ensure the passwords that go through SASLprep work\n        for password in passwords:\n            # update the password\n            await self.con.sql_execute(\n                alter_password.format(password).encode(),\n            )\n            # test to see that passwords are properly SASL prepped\n            conn = await self.connect(\n                user='scram_sha_256_user', password=password)\n            conn.terminate()\n\n        alter_password = \\\n            b\"ALTER ROLE scram_sha_256_user PASSWORD 'correctpassword';\"\n        await self.con.sql_execute(alter_password)\n        await self.con.sql_execute(b\"SET password_encryption = 'md5';\")\n\n    async def test_auth_unsupported(self):\n        pass\n\n\nclass TestConnectParams(tb.TestCase):\n\n    @contextlib.contextmanager\n    def environ(self, **kwargs):\n        old_vals = {}\n        for key in kwargs:\n            if key in os.environ:\n                old_vals[key] = os.environ[key]\n\n        for key, val in kwargs.items():\n            if val is None:\n                if key in os.environ:\n                    del os.environ[key]\n            else:\n                os.environ[key] = val\n\n        try:\n            yield\n        finally:\n            for key in kwargs:\n                if key in os.environ:\n                    del os.environ[key]\n            for key, val in old_vals.items():\n                os.environ[key] = val\n\n    def run_testcase(self, testcase):\n        env = testcase.get('env', {})\n        test_env = {'PGHOST': None, 'PGPORT': None,\n                    'PGUSER': None, 'PGPASSWORD': None,\n                    'PGDATABASE': None, 'PGSSLMODE': None}\n        test_env.update(env)\n\n        dsn = testcase.get('dsn', 'postgres://')\n\n        expected = testcase.get('result')\n        expected_error = testcase.get('error')\n        if expected is None and expected_error is None:\n            raise RuntimeError(\n                'invalid test case: either \"result\" or \"error\" key '\n                'has to be specified')\n        if expected is not None and expected_error is not None:\n            raise RuntimeError(\n                'invalid test case: either \"result\" or \"error\" key '\n                'has to be specified, got both')\n\n        with contextlib.ExitStack() as es:\n            es.enter_context(self.subTest(dsn=dsn, env=env))\n            es.enter_context(self.environ(**test_env))\n\n            if expected_error:\n                es.enter_context(self.assertRaisesRegex(*expected_error))\n\n            conn_params = pgconnparams.ConnectionParams(dsn=dsn)\n            conn_params = conn_params.resolve()\n\n            to_dict = conn_params.__dict__\n            host = to_dict.pop('host', None).split(',')\n            port = map(int, to_dict.pop('port', None).split(','))\n            to_dict.pop('sslmode', None)\n            result = (list(zip(host, port)), to_dict)\n\n        if expected is not None:\n            self.assertEqual(\n                expected,\n                result,\n                'Testcase: {}'.format(testcase.get('name', testcase))\n            )\n\n    def test_test_connect_params_environ(self):\n        self.assertNotIn('AAAAAAAAAA123', os.environ)\n        self.assertNotIn('AAAAAAAAAA456', os.environ)\n        self.assertNotIn('AAAAAAAAAA789', os.environ)\n\n        try:\n\n            os.environ['AAAAAAAAAA456'] = '123'\n            os.environ['AAAAAAAAAA789'] = '123'\n\n            with self.environ(AAAAAAAAAA123='1',\n                              AAAAAAAAAA456='2',\n                              AAAAAAAAAA789=None):\n\n                self.assertEqual(os.environ['AAAAAAAAAA123'], '1')\n                self.assertEqual(os.environ['AAAAAAAAAA456'], '2')\n                self.assertNotIn('AAAAAAAAAA789', os.environ)\n\n            self.assertNotIn('AAAAAAAAAA123', os.environ)\n            self.assertEqual(os.environ['AAAAAAAAAA456'], '123')\n            self.assertEqual(os.environ['AAAAAAAAAA789'], '123')\n\n        finally:\n            for key in {'AAAAAAAAAA123', 'AAAAAAAAAA456', 'AAAAAAAAAA789'}:\n                if key in os.environ:\n                    del os.environ[key]\n\n    def test_test_connect_params_run_testcase(self):\n        with self.environ(PGPORT='777'):\n            self.run_testcase({\n                'env': {\n                    'PGUSER': '__test__'\n                },\n                'dsn': 'postgres://abc',\n                'result': (\n                    [('abc', 5432)],\n                    {'user': '__test__', 'database': '__test__'}\n                )\n            })\n\n    def test_connect_pgpass_badness_mode(self):\n        # Verify that .pgpass permissions are checked\n        with tempfile.NamedTemporaryFile('w+t') as passfile:\n            os.chmod(passfile.name,\n                     stat.S_IWUSR | stat.S_IRUSR | stat.S_IWGRP | stat.S_IRGRP)\n\n            with self.assertWarnsRegex(\n                    UserWarning,\n                    'Password file .* has group or world access'):\n                self.run_testcase({\n                    'dsn': 'postgres://user@abc/db?passfile={}'.format(\n                        passfile.name\n                    ),\n                    'result': (\n                        [('abc', 5432)],\n                        {\n                            'user': 'user',\n                            'database': 'db',\n                        }\n                    )\n                })\n\n    def test_connect_pgpass_badness_non_file(self):\n        # Verify warnings when .pgpass is not a file\n        with tempfile.TemporaryDirectory() as passfile:\n            with self.assertWarnsRegex(\n                    UserWarning,\n                    'Password file .* is not a plain file'):\n                self.run_testcase({\n                    'dsn': 'postgres://user@abc/db?passfile={}'.format(\n                        passfile\n                    ),\n                    'result': (\n                        [('abc', 5432)],\n                        {\n                            'user': 'user',\n                            'database': 'db',\n                        }\n                    )\n                })\n\n    def test_connect_pgpass_nonexistent(self):\n        # nonexistent passfile is OK\n        with self.assertWarnsRegex(\n            UserWarning,\n            'Password file .* does not exist',\n        ):\n            self.run_testcase({\n                'dsn': 'postgres://user@abc/db?passfile=totally+nonexistent',\n                'result': (\n                    [('abc', 5432)],\n                    {\n                        'user': 'user',\n                        'database': 'db',\n                    }\n                )\n            })\n\n    def test_connect_pgpass_inaccessible_file(self):\n        with tempfile.NamedTemporaryFile('w+t') as passfile:\n            os.chmod(passfile.name, stat.S_IWUSR)\n            with self.assertWarnsRegex(\n                UserWarning,\n                'Password file .* is not accessible'):\n                # inaccessible passfile is OK\n                self.run_testcase({\n                    'dsn': 'postgres://user@abc/db?passfile={}'.format(\n                        passfile.name\n                    ),\n                    'result': (\n                        [('abc', 5432)],\n                        {\n                            'user': 'user',\n                            'database': 'db',\n                        }\n                    )\n                })\n\n    def test_connect_pgpass_inaccessible_directory(self):\n        with tempfile.TemporaryDirectory() as passdir:\n            with tempfile.NamedTemporaryFile('w+t', dir=passdir) as passfile:\n                os.chmod(passdir, stat.S_IWUSR)\n\n                try:\n                    with self.assertWarnsRegex(\n                        UserWarning,\n                        'Password file .* is not accessible'):\n                        # inaccessible passfile is OK\n                        self.run_testcase({\n                            'dsn': 'postgres://user@abc/db?passfile={}'.format(\n                                passfile.name\n                            ),\n                            'result': (\n                                [('abc', 5432)],\n                                {\n                                    'user': 'user',\n                                    'database': 'db',\n                                }\n                            )\n                        })\n                finally:\n                    os.chmod(passdir, stat.S_IRWXU)\n\n    async def test_connection_connect_timeout(self):\n        server = socket.socket()\n        gc = []\n        try:\n            server.bind(('localhost', 0))\n            if platform.system() != \"Darwin\":\n                # The backlog on macOS is different from Linux\n                server.listen(0)\n            host, port = server.getsockname()\n            conn_spec = pgconnparams.ConnectionParams(\n                hosts=[(host, port)],\n                user='foo',\n                connect_timeout=2,\n            )\n\n            async def placeholder():\n                async with asyncio.timeout(2):\n                    _, w = await asyncio.open_connection(host, port)\n                gc.append(w)\n\n            # Fill up the TCP server backlog so that our future pgcon.connect\n            # could time out reliably\n            i = 0\n            while i < 8:\n                i += 1\n                try:\n                    async with asyncio.TaskGroup() as tg:\n                        for _ in range(4):\n                            tg.create_task(placeholder())\n                except* TimeoutError:\n                    i = 10\n            if i < 10:\n                self.fail(\"Couldn't fill TCP server backlog within 32 tries\")\n\n            with self.assertRaises(errors.BackendConnectionError):\n                async with asyncio.timeout(4):  # failsafe\n                    await pgcon.pg_connect(\n                        conn_spec,\n                        source_description=\"test_connection_connect_timeout\",\n                        backend_params=pg_params.get_default_runtime_params(),\n                    )\n\n        finally:\n            server.close()\n            if gc:\n                for writer in gc:\n                    writer.close()\n                await asyncio.wait(\n                    [asyncio.create_task(w.wait_closed()) for w in gc]\n                )\n\n\nclass TestConnection(ClusterTestCase):\n\n    async def test_connection_isinstance(self):\n        self.assertTrue(isinstance(self.con, pgcon.PGConnection))\n        self.assertTrue(isinstance(self.con, object))\n        self.assertFalse(isinstance(self.con, list))\n\n    async def test_connection_use_after_close(self):\n        def check():\n            return self.assertRaisesRegex(RuntimeError,\n                                          'not connected')\n\n        self.con.terminate()\n\n        with check():\n            await self.con.sql_fetch_val(b'SELECT 1')\n\n        with check():\n            await self.con.sql_fetch_col(b'SELECT 1')\n\n        with check():\n            await self.con.sql_fetch(b'SELECT 1')\n\n        with check():\n            await self.con.sql_execute(b'SELECT 1')\n\n        with check():\n            await self.con.parse_execute(query=None, bind_data=None)\n\n        with check():\n            await self.con.dump(None, None, None)\n\n        with check():\n            await self.con.restore(None, b'', {})\n\n    async def test_connection_ssl_to_no_ssl_server(self):\n        with self.assertRaisesRegex(ConnectionError, 'rejected SSL'):\n            await self.connect(\n                host='localhost',\n                sslmode=SSLMode.require)\n\n    async def test_connection_sslmode_no_ssl_server(self):\n        async def verify_works(sslmode):\n            con = None\n            try:\n                con = await self.connect(\n                    sslmode=SSLMode.parse(sslmode),\n                    user='postgres',\n                    database='postgres',\n                    host='localhost')\n                await self.assertConnected(con)\n                self.assertFalse(con.is_ssl)\n            finally:\n                if con:\n                    con.terminate()\n\n        async def verify_fails(sslmode):\n            con = None\n            try:\n                with self.assertRaises(ConnectionError):\n                    con = await self.connect(\n                        sslmode=SSLMode.parse(sslmode),\n                        user='postgres',\n                        database='postgres',\n                        host='localhost')\n                    await self.assertConnected(con)\n            finally:\n                if con:\n                    con.terminate()\n\n        await verify_works('disable')\n        await verify_works('allow')\n        await verify_works('prefer')\n        await verify_fails('require')\n        with mock_dot_postgresql():\n            await verify_fails('require')\n            await verify_fails('verify-ca')\n            await verify_fails('verify-full')\n\n    async def test_connection_memory_leak(self):\n        import inspect\n\n        source_description = (\n            f\"ClusterTestCase: {inspect.currentframe().f_back.f_code.co_name}\"\n        )  # type: ignore\n        params = self.cluster.get_pgaddr()\n        params.update(database=self.dbname)\n\n        script = textwrap.dedent(f\"\"\"\n            import asyncio\n            import gc\n\n            from edb.server import pgcon\n            from edb.pgsql import params\n\n            async def run():\n                con = await pgcon.pg_connect(\n                    {params.to_dsn()!r},\n                    source_description={source_description!r},\n                    backend_params=params.get_default_runtime_params(),\n                    apply_init_script=False,\n                )\n                con.terminate()\n\n            asyncio.run(run())  # warm up\n\n            gc.collect()\n            gc.collect()\n            gc.collect()\n\n            count = len(gc.get_objects())\n\n            for i in range(100):\n                asyncio.run(run())\n\n                gc.collect()\n                gc.collect()\n                gc.collect()\n\n                new_count = len(gc.get_objects())\n                if count != new_count:\n                    raise AssertionError(\n                        f\"Memory leak detected on iteration {{i}}: \"\n                        f\"{{count}} != {{new_count}}\"\n                    )\n        \"\"\")\n        try:\n            subprocess.check_output(\n                [sys.executable, \"-I\", \"-c\", script],\n                stderr=subprocess.STDOUT,\n                text=True,\n            )\n        except subprocess.CalledProcessError as e:\n            self.fail(e.stdout)\n\n\nclass BaseTestSSLConnection(ClusterTestCase):\n    @classmethod\n    def get_server_settings(cls):\n        conf = super().get_server_settings()\n        conf.update({\n            'ssl': 'on',\n            'ssl_cert_file': SSL_CERT_FILE,\n            'ssl_key_file': SSL_KEY_FILE,\n            'ssl_ca_file': CLIENT_CA_CERT_FILE,\n            'ssl_min_protocol_version': 'TLSv1.2',\n            'ssl_max_protocol_version': 'TLSv1.2',\n        })\n        return conf\n\n    def setUp(self):\n        super().setUp()\n\n        self.cluster.reset_hba()\n\n        create_script = []\n        create_script.append('CREATE ROLE ssl_user WITH LOGIN;')\n        create_script.append('GRANT postgres TO ssl_user;')\n\n        self._add_hba_entry()\n\n        # Put hba changes into effect\n        self.loop.run_until_complete(self.cluster.reload())\n\n        create_script = '\\n'.join(create_script).encode()\n        self.loop.run_until_complete(\n            self.con.sql_execute(create_script)\n        )\n\n    def tearDown(self):\n        # Reset cluster's pg_hba.conf since we've meddled with it\n        self.loop.run_until_complete(self.cluster.trust_local_connections())\n\n        drop_script = []\n        drop_script.append('DROP ROLE ssl_user;')\n        drop_script = '\\n'.join(drop_script).encode()\n        self.loop.run_until_complete(self.con.sql_execute(drop_script))\n\n        super().tearDown()\n\n    def _add_hba_entry(self):\n        raise NotImplementedError()\n\n\n@unittest.skipIf(os.environ.get('PGHOST'), 'unmanaged cluster')\nclass TestSSLConnection(BaseTestSSLConnection):\n    def _add_hba_entry(self):\n        self.cluster.add_hba_entry(\n            type='hostssl', address=\"all\",\n            database=self.dbname, user='ssl_user',\n            auth_method='trust')\n\n    async def test_ssl_connection_custom_context(self):\n        con = await self.connect(\n            host='localhost',\n            user='ssl_user',\n            sslmode=SSLMode.require,\n            sslrootcert=SSL_CA_CERT_FILE)\n\n        try:\n            await self.assertConnected(con)\n            self.assertTrue(con.is_ssl)\n        finally:\n            con.terminate()\n\n    async def test_ssl_connection_sslmode(self):\n        async def verify_works(sslmode, *, host='localhost'):\n            con = None\n            try:\n                con = await self.connect(\n                    sslmode=SSLMode.parse(sslmode),\n                    host=host,\n                    user='ssl_user')\n                await self.assertConnected(con)\n                self.assertTrue(con.is_ssl)\n            finally:\n                if con:\n                    con.terminate()\n\n        async def verify_fails(sslmode, *, host='localhost', exn_type):\n            # XXX: uvloop artifact\n            old_handler = self.loop.get_exception_handler()\n            con = None\n            try:\n                self.loop.set_exception_handler(lambda *args: None)\n                with self.assertRaises(exn_type, msg=f\"{sslmode} {host}\"):\n                    con = await self.connect(\n                        sslmode=SSLMode.parse(sslmode),\n                        host=host,\n                        user='ssl_user')\n                    await self.assertConnected(con)\n            finally:\n                if con:\n                    con.terminate()\n                self.loop.set_exception_handler(old_handler)\n\n        invalid_auth_err = errors.BackendError\n        await verify_fails('disable', exn_type=invalid_auth_err)\n        await verify_works('allow')\n        await verify_works('prefer')\n        await verify_works('require')\n        await verify_fails('verify-ca', exn_type=ValueError)\n        await verify_fails('verify-full', exn_type=ValueError)\n\n        with mock_dot_postgresql():\n            await verify_works('require')\n            await verify_works('verify-ca')\n            await verify_works('verify-ca', host='127.0.0.1')\n            await verify_works('verify-full')\n            await verify_fails('verify-full', host='127.0.0.1',\n                               exn_type=ssl.CertificateError)\n\n        with mock_dot_postgresql(crl=True):\n            await verify_fails('disable', exn_type=invalid_auth_err)\n            await verify_works('allow')\n            await verify_works('prefer')\n            await verify_fails('require',\n                               exn_type=ssl.SSLError)\n            await verify_fails('verify-ca',\n                               exn_type=ssl.SSLError)\n            await verify_fails('verify-ca', host='127.0.0.1',\n                               exn_type=ssl.SSLError)\n            await verify_fails('verify-full',\n                               exn_type=ssl.SSLError)\n\n    async def test_ssl_connection_default_context(self):\n        # XXX: uvloop artifact\n        old_handler = self.loop.get_exception_handler()\n        try:\n            self.loop.set_exception_handler(lambda *args: None)\n            with self.assertRaisesRegex(ssl.SSLError, 'verify failed'):\n                await self.connect(\n                    host='localhost',\n                    user='ssl_user',\n                    sslmode=SSLMode.verify_full,\n                    # This won't validate\n                    sslrootcert=CLIENT_CA_CERT_FILE\n                )\n        finally:\n            self.loop.set_exception_handler(old_handler)\n\n    async def test_tls_version_bad(self):\n        with self.assertRaisesRegex(ssl.SSLError, 'protocol version'):\n            await self.connect(\n                dsn=f'postgresql://ssl_user@localhost/{self.dbname}'\n                    '?sslmode=require&ssl_min_protocol_version=TLSv1.3'\n            )\n        with warnings.catch_warnings():\n            warnings.simplefilter('ignore', DeprecationWarning)\n            with self.assertRaises(ssl.SSLError):\n                await self.connect(\n                    dsn=f'postgresql://ssl_user@localhost/{self.dbname}'\n                        '?sslmode=require'\n                        '&ssl_min_protocol_version=TLSv1.1'\n                        '&ssl_max_protocol_version=TLSv1.1'\n                )\n            with self.assertRaisesRegex(ssl.SSLError, 'no protocols'):\n                await self.connect(\n                    dsn=f'postgresql://ssl_user@localhost/{self.dbname}'\n                        '?sslmode=require'\n                        '&ssl_min_protocol_version=TLSv1.2'\n                        '&ssl_max_protocol_version=TLSv1.1'\n                )\n\n    async def test_tls_version_ok(self):\n        con = await self.connect(\n            dsn=f'postgresql://ssl_user@localhost/{self.dbname}'\n                '?sslmode=require'\n        )\n        try:\n            await self.assertConnected(con)\n        finally:\n            con.terminate()\n\n\nclass TestClientSSLConnection(BaseTestSSLConnection):\n    def _add_hba_entry(self):\n        self.cluster.add_hba_entry(\n            type='hostssl', address=ipaddress.ip_network('127.0.0.0/24'),\n            database=self.dbname, user='ssl_user',\n            auth_method='cert')\n\n        self.cluster.add_hba_entry(\n            type='hostssl', address=ipaddress.ip_network('::1/128'),\n            database=self.dbname, user='ssl_user',\n            auth_method='cert')\n\n    async def test_ssl_connection_client_auth_fails_with_wrong_setup(self):\n        with self.assertRaisesRegex(\n            errors.BackendError,\n            \"requires a valid client certificate\",\n        ):\n            await self.connect(\n                host='localhost',\n                user='ssl_user',\n                sslmode=SSLMode.require,\n                sslrootcert=SSL_CA_CERT_FILE,\n            )\n\n    async def _test_works(self, **conn_args):\n        con = await self.connect(**conn_args)\n\n        try:\n            await self.assertConnected(con)\n        finally:\n            con.terminate()\n\n    async def test_ssl_connection_client_auth_custom_context(self):\n        for key_file in (CLIENT_SSL_KEY_FILE, CLIENT_SSL_PROTECTED_KEY_FILE):\n            await self._test_works(\n                host='localhost',\n                user='ssl_user',\n                sslmode=SSLMode.require,\n                sslcert=CLIENT_SSL_CERT_FILE,\n                sslrootcert=SSL_CA_CERT_FILE,\n                sslpassword='secret1234',\n                sslkey=key_file\n            )\n\n    async def test_ssl_connection_client_auth_dsn(self):\n        params = {\n            'sslrootcert': SSL_CA_CERT_FILE,\n            'sslcert': CLIENT_SSL_CERT_FILE,\n            'sslkey': CLIENT_SSL_KEY_FILE,\n            'sslmode': 'verify-full',\n        }\n        params_str = urllib.parse.urlencode(params)\n        dsn = 'postgres://ssl_user@localhost/postgres?' + params_str\n        await self._test_works(dsn=dsn)\n\n        params['sslkey'] = CLIENT_SSL_PROTECTED_KEY_FILE\n        params['sslpassword'] = 'secret1234'\n        params_str = urllib.parse.urlencode(params)\n        dsn = 'postgres://ssl_user@localhost/postgres?' + params_str\n        await self._test_works(dsn=dsn)\n\n    async def test_ssl_connection_client_auth_env(self):\n        env = {\n            'PGSSLROOTCERT': SSL_CA_CERT_FILE,\n            'PGSSLCERT': CLIENT_SSL_CERT_FILE,\n            'PGSSLKEY': CLIENT_SSL_KEY_FILE,\n        }\n        dsn = 'postgres://ssl_user@localhost/postgres?sslmode=verify-full'\n        with unittest.mock.patch.dict('os.environ', env):\n            await self._test_works(dsn=dsn)\n\n        env['PGSSLKEY'] = CLIENT_SSL_PROTECTED_KEY_FILE\n        with unittest.mock.patch.dict('os.environ', env):\n            await self._test_works(dsn=dsn + '&sslpassword=secret1234')\n\n    async def test_ssl_connection_client_auth_dot_postgresql(self):\n        dsn = 'postgres://ssl_user@localhost/postgres?sslmode=verify-full'\n        with mock_dot_postgresql(client=True):\n            await self._test_works(dsn=dsn)\n        with mock_dot_postgresql(client=True, protected=True):\n            await self._test_works(dsn=dsn + '&sslpassword=secret1234')\n\n\nclass TestNoSSLConnection(BaseTestSSLConnection):\n    def _add_hba_entry(self):\n        self.cluster.add_hba_entry(\n            type='hostnossl', address=ipaddress.ip_network('127.0.0.0/24'),\n            database=self.dbname, user='ssl_user',\n            auth_method='trust')\n\n        self.cluster.add_hba_entry(\n            type='hostnossl', address=ipaddress.ip_network('::1/128'),\n            database=self.dbname, user='ssl_user',\n            auth_method='trust')\n\n    async def test_nossl_connection_sslmode(self):\n        async def verify_works(sslmode, *, host='localhost'):\n            con = None\n            try:\n                con = await self.connect(\n                    sslmode=SSLMode.parse(sslmode),\n                    host=host,\n                    user='ssl_user')\n                await self.assertConnected(con)\n                self.assertFalse(con.is_ssl)\n            finally:\n                if con:\n                    con.terminate()\n\n        async def verify_fails(sslmode, *, host='localhost'):\n            # XXX: uvloop artifact\n            old_handler = self.loop.get_exception_handler()\n            con = None\n            try:\n                self.loop.set_exception_handler(lambda *args: None)\n                with self.assertRaises(\n                    errors.BackendError\n                ) as cm:\n                    con = await self.connect(\n                        sslmode=SSLMode.parse(sslmode),\n                        host=host,\n                        user='ssl_user')\n                    await self.assertConnected(con)\n                self.assertTrue(\n                    cm.exception.code_is(\n                        errors.ERROR_INVALID_AUTHORIZATION_SPECIFICATION\n                    )\n                )\n            finally:\n                if con:\n                    con.terminate()\n                self.loop.set_exception_handler(old_handler)\n\n        # We no longer retry without SSL if SSL is presented but\n        # fails to authenticate.\n        await verify_works('disable')\n        await verify_fails('allow')\n        await verify_fails('prefer')\n        await verify_fails('require')\n        with mock_dot_postgresql():\n            await verify_fails('require')\n            await verify_fails('verify-ca')\n            await verify_fails('verify-full')\n"
  },
  {
    "path": "tests/test_backend_ha.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2021-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport asyncio\nimport base64\nimport contextlib\nimport json\nimport os\nimport pathlib\nimport subprocess\nimport tempfile\nimport time\nimport unittest\nimport urllib.parse\n\nimport edgedb\nimport httptools\n\nfrom edb.testbase import server as tb\nfrom edb.server import pgcluster\nfrom edb.server.ha import base as ha_base\n\n\nclass HTTPGet(asyncio.Protocol):\n    def __init__(self, path, host=\"127.0.0.1\", port=None, timeout=10):\n        self._buffers = []\n        self._transport = None\n        self._host = host\n        self._path = path\n        self._waiter = asyncio.Future()\n        self._parser = httptools.HttpResponseParser(self)\n        self._timeout_handle = None\n\n        loop = asyncio.get_running_loop()\n        loop.create_task(\n            loop.create_connection(lambda: self, host, port or 80)\n        ).add_done_callback(\n            self._connect_cb\n        )\n        if timeout:\n            self._timeout_handle = loop.call_later(timeout, self._on_timeout)\n\n    def _set_result(self, result):\n        if self._timeout_handle is not None:\n            self._timeout_handle.cancel()\n        if not self._waiter.done():\n            if isinstance(result, BaseException):\n                self._waiter.set_exception(result)\n            else:\n                self._waiter.set_result(result)\n            if self._transport is not None:\n                self._transport.close()\n\n    def _connect_cb(self, task: asyncio.Task):\n        ex = task.exception()\n        if ex is not None:\n            self._set_result(ex)\n\n    def _on_timeout(self):\n        self._set_result(TimeoutError(\"HTTP request timeout\"))\n\n    def connection_made(self, transport):\n        self._transport = transport\n        transport.write(\n            f\"GET {self._path} HTTP/1.1\\r\\n\"\n            f\"Host: {self._host}\\r\\n\"\n            f\"\\r\\n\".encode()\n        )\n\n    def data_received(self, data: bytes):\n        try:\n            self._parser.feed_data(data)\n        except Exception as ex:\n            self._set_result(ex)\n\n    def connection_lost(self, exc):\n        self._transport = None\n        self._set_result(exc or RuntimeError(\"Connection broken unexpectedly\"))\n\n    def on_status(self, status: bytes):\n        code = self._parser.get_status_code()\n        if code != 200:\n            raise RuntimeError(f\"Server returned {code}: {status.decode()}\")\n\n    def on_body(self, body: bytes):\n        self._buffers.append(body)\n\n    def on_message_complete(self):\n        self._set_result(b\"\".join(self._buffers))\n\n    def __await__(self):\n        return self._waiter.__await__()\n\n\nclass ServerContext:\n    title: str = NotImplemented\n\n    def __init__(self, debug=False):\n        self.debug = debug\n        self.proc = None\n\n    async def run(self, *args):\n        if self.debug:\n            print(\"Running: \", args)\n        self.proc = await asyncio.create_subprocess_exec(\n            *args,\n            stdout=None if self.debug else subprocess.PIPE,\n            stderr=None if self.debug else subprocess.STDOUT,\n        )\n\n    async def stop(self, success=True):\n        if self.proc is not None:\n            self.proc.terminate()\n            try:\n                await asyncio.wait_for(self.proc.wait(), 30)\n                if success:\n                    return\n            except asyncio.TimeoutError:\n                self.proc.kill()\n\n            if not self.debug:\n                stdout, _ = await self.proc.communicate()\n                print(\"=\" * 79)\n                print(f\"Captured {self.title}\")\n                print(\"=\" * 79)\n                print(stdout.decode())\n                print()\n\n\nclass ConsulAgent(ServerContext):\n    title = \"Consul Log\"\n\n    async def __aenter__(self):\n        self.server_port = tb.find_available_port()\n        self.http_port = tb.find_available_port()\n        self.tmp_dir = tempfile.TemporaryDirectory()\n        tmp_dir = pathlib.Path(self.tmp_dir.name)\n        config_file = tmp_dir / \"config.json\"\n        with config_file.open(\"w\") as f:\n            json.dump(\n                dict(\n                    ports=dict(\n                        server=self.server_port,\n                        http=self.http_port,\n                        serf_lan=tb.find_available_port(),\n                        dns=-1,\n                        grpc=-1,\n                        serf_wan=-1,\n                    ),\n                    data_dir=str(tmp_dir),\n                ),\n                f,\n            )\n        if self.debug:\n            with config_file.open() as f:\n                print('config.json:')\n                print(f.read())\n        await self.run(\n            os.environ.get(\"EDGEDB_TEST_CONSUL_PATH\", \"consul\"),\n            \"agent\",\n            \"-dev\",\n            \"-log-level=info\",\n            \"-config-file\",\n            str(config_file),\n        )\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        try:\n            await self.stop(exc_type is None)\n        finally:\n            self.tmp_dir.cleanup()\n\n\nclass StolonSentinel(ServerContext):\n    title = \"Stolon Sentinel Log\"\n\n    def __init__(\n        self, consul: ConsulAgent, *, debug=False, cluster_name=\"test-cluster\"\n    ):\n        super().__init__(debug)\n        self.cluster_name = cluster_name\n        self.consul_http_port = consul.http_port\n\n    async def init(self):\n        args = [\n            os.environ.get(\"EDGEDB_TEST_STOLON_CTL\", \"stolonctl\"),\n            \"--cluster-name\",\n            self.cluster_name,\n            \"--store-backend=consul\",\n            \"--store-endpoints\",\n            f\"http://127.0.0.1:{self.consul_http_port}\",\n            \"init\",\n            \"--yes\",\n            json.dumps(dict(\n                initMode=\"new\",\n                sleepInterval=\"0.5s\",\n                requestTimeout=\"1s\",\n                failInterval=\"2s\",\n            )),\n        ]\n        if self.debug:\n            print(\"Running:\", args)\n        p = await asyncio.create_subprocess_exec(\n            *args,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.STDOUT,\n        )\n        stdout, _ = await p.communicate()\n        if p.returncode:\n            return stdout.decode()\n        return \"\"\n\n    async def __aenter__(self):\n        start = time.monotonic()\n        while True:\n            error = await self.init()\n            if not error:\n                break\n            if \"connection refused\" not in error:\n                print(\"=\" * 79)\n                print(\"Captured Output for: stolonctl init\")\n                print(\"=\" * 79)\n                print(error)\n                raise RuntimeError(\"Stolon cannot connect to Consul\")\n            await asyncio.sleep(0.1)\n            if time.monotonic() - start > 60:\n                raise RuntimeError(\"Stolon cannot connect to Consul in 60s\")\n        await self.run(\n            os.environ.get(\"EDGEDB_TEST_STOLON_SENTINEL\", \"stolon-sentinel\"),\n            \"--cluster-name\",\n            self.cluster_name,\n            \"--store-backend=consul\",\n            \"--store-endpoints\",\n            f\"http://127.0.0.1:{self.consul_http_port}\",\n        )\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        await self.stop(exc_type is None)\n\n\nclass StolonKeeper(ServerContext):\n    title = \"Stolon Keeper Log\"\n\n    def __init__(\n        self,\n        consul: ConsulAgent,\n        pg_bin_dir,\n        debug=False,\n        cluster_name=\"test-cluster\",\n    ):\n        super().__init__(debug)\n        self.cluster_name = cluster_name\n        self.consul_http_port = consul.http_port\n        self.pg_bin_dir = pg_bin_dir\n        self.tmp_dir = None\n\n    async def __aenter__(self):\n        self.port = tb.find_available_port()\n        self.tmp_dir = tempfile.TemporaryDirectory()\n        await self.start()\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        try:\n            await self.stop(exc_type is None)\n        finally:\n            if self.tmp_dir is not None:\n                self.tmp_dir.cleanup()\n\n    async def stop(self, success=True):\n        proc, self.proc = self.proc, None\n        if proc is not None:\n            proc.terminate()\n            if not await self.wait_for_stop():\n                try:\n                    proc.kill()\n                except ProcessLookupError:\n                    pass\n                if not self.debug:\n                    stdout, _ = await proc.communicate()\n                    print(\"=\" * 79)\n                    print(f\"Captured {self.title}\")\n                    print(\"=\" * 79)\n                    print(stdout.decode())\n                    print()\n                raise RuntimeError(\"Stolon keeper didn't stop in 60s\")\n\n    async def start(self):\n        await self.run(\n            os.environ.get(\"EDGEDB_TEST_STOLON_KEEPER\", \"stolon-keeper\"),\n            \"--log-level\",\n            \"warn\",\n            \"--cluster-name\",\n            self.cluster_name,\n            \"--store-backend=consul\",\n            \"--store-endpoints\",\n            f\"http://127.0.0.1:{self.consul_http_port}\",\n            \"--uid\",\n            f\"pg{self.port}\",\n            \"--data-dir\",\n            self.tmp_dir.name,\n            \"--pg-su-username=suname\",\n            \"--pg-su-password=supass\",\n            \"--pg-repl-username=repluser\",\n            \"--pg-repl-password=replpassword\",\n            \"--pg-listen-address=127.0.0.1\",\n            \"--pg-port\",\n            str(self.port),\n            \"--pg-bin-path\",\n            str(self.pg_bin_dir),\n        )\n        try:\n            await self.wait_for_healthy()\n        except Exception:\n            if self.tmp_dir is not None:\n                self.tmp_dir.cleanup()\n            await self.stop(False)\n            raise\n\n    async def wait_for_healthy(self):\n        start = time.monotonic()\n        while True:\n            payload = await HTTPGet(\n                \"/v1/kv/stolon/cluster/test-cluster/clusterdata\",\n                port=self.consul_http_port,\n            )\n            payload = json.loads(payload)[0]\n            cluster_data = json.loads(base64.b64decode(payload[\"Value\"]))\n            for db in cluster_data.get(\"dbs\", {}).values():\n                if db.get(\"spec\", {}).get(\"keeperUID\") == f\"pg{self.port}\":\n                    if db.get(\"status\", {}).get(\"healthy\"):\n                        if db.get(\"spec\", {}).get(\"initMode\") == \"none\":\n                            return\n            await asyncio.sleep(1)\n            if time.monotonic() - start > 60:\n                raise RuntimeError(\"Stolon keeper didn't start in 60s\")\n\n    async def wait_for_stop(self):\n        start = time.monotonic()\n        while True:\n            payload = await HTTPGet(\n                \"/v1/kv/stolon/cluster/test-cluster/clusterdata\",\n                port=self.consul_http_port,\n            )\n            payload = json.loads(payload)[0]\n            cluster_data = json.loads(base64.b64decode(payload[\"Value\"]))\n            for db in cluster_data.get(\"dbs\", {}).values():\n                if db.get(\"spec\", {}).get(\"keeperUID\") == f\"pg{self.port}\":\n                    if not db.get(\"status\", {}).get(\"healthy\", False):\n                        return True\n                    break\n            else:\n                return True\n            await asyncio.sleep(1)\n            if time.monotonic() - start > 60:\n                return False\n\n\nclass AdaptiveHAProxy(ha_base.ClusterProtocol):\n    def __init__(\n        self, consul_http_port, debug=False, cluster_name=\"test-cluster\"\n    ):\n        self.parsed_dsn = urllib.parse.urlparse(\n            f\"stolon+consul+http://127.0.0.1:{consul_http_port}/{cluster_name}\"\n        )\n        self.debug = debug\n        self.server = None\n        self.port = None\n        self.master_addr = None\n\n    async def __aenter__(self):\n        self.consul = ha_base.get_backend(self.parsed_dsn)\n        self.master_addr = await self.consul.get_cluster_consensus()\n        self.port = tb.find_available_port()\n        self.consul.set_failover_callback(self.on_switch_over)\n        await self.consul.start_watching()\n        self.server = await asyncio.start_server(\n            self._proxy_connection, \"127.0.0.1\", self.port\n        )\n        return self.port\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        self.consul.stop_watching()\n        self.server.close()\n        await self.server.wait_closed()\n\n    async def _proxy_connection(self, reader, writer):\n        try:\n            pg_reader, pg_writer = await asyncio.open_connection(\n                *self.master_addr\n            )\n        except Exception as e:\n            if self.debug:\n                print(\n                    f\"AdaptiveHAProxy: failed to proxy connection to \"\n                    f\"{self.master_addr}, reason: {e!r}\"\n                )\n            writer.close()\n            await writer.wait_closed()\n        else:\n            if self.debug:\n                print(\n                    f\"AdaptiveHAProxy: proxying connection to \"\n                    f\"{self.master_addr}\"\n                )\n            await asyncio.gather(\n                self._proxy_traffic(reader, pg_writer),\n                self._proxy_traffic(pg_reader, writer),\n            )\n\n    @staticmethod\n    async def _proxy_traffic(\n        reader: asyncio.StreamReader, writer: asyncio.StreamWriter\n    ):\n        try:\n            while True:\n                data = await reader.read(32768)\n                if not data:\n                    return\n                writer.write(data)\n        finally:\n            writer.close()\n            await writer.wait_closed()\n\n    def on_switch_over(self):\n        self.master_addr = self.consul.get_master_addr()\n        if self.debug:\n            print(f\"AdaptiveHAProxy: master switched to {self.master_addr}\")\n\n    def get_active_pgcon_num(self) -> int:\n        return 0\n\n\n@contextlib.asynccontextmanager\nasync def stolon_setup(*, debug=False):\n    pg_bin_dir = await pgcluster.get_pg_bin_dir()\n\n    async with ConsulAgent() as consul:\n        async with StolonSentinel(consul, debug=debug):\n            async with StolonKeeper(consul, pg_bin_dir, debug=debug) as pg1:\n                async with StolonKeeper(\n                    consul, pg_bin_dir, debug=debug\n                ) as pg2:\n                    yield consul, pg1, pg2\n\n\ndef setUpModule():\n    debug = False\n\n    try:\n        consul_path = os.environ.get(\"EDGEDB_TEST_CONSUL_PATH\", \"consul\")\n        subprocess.check_call(\n            [consul_path, \"--version\"],\n            stdout=None if debug else subprocess.DEVNULL,\n            stderr=None if debug else subprocess.DEVNULL,\n        )\n        stolon_path = os.environ.get(\"EDGEDB_TEST_STOLON_CTL\", \"stolonctl\")\n        subprocess.check_call(\n            [stolon_path, \"--version\"],\n            stdout=None if debug else subprocess.DEVNULL,\n            stderr=None if debug else subprocess.DEVNULL,\n        )\n    except Exception:\n        raise unittest.SkipTest(\"Consul not installed\")\n\n\n@unittest.skipIf(\n    not os.environ.get(\"EDGEDB_TEST_HA\"), \"EDGEDB_TEST_HA is not set\"\n)\nclass TestBackendHA(tb.TestCase):\n    async def _wait_for_failover(self, con):\n        async for tx in con.with_retry_options(\n            edgedb.RetryOptions(60, lambda x: 1)\n        ).retrying_transaction():\n            async with tx:\n                rv = await tx.query_single(\"SELECT 1\")\n        else:\n            self.assertEqual(rv, 1)\n\n    async def test_ha_stolon(self):\n        debug = False\n        async with stolon_setup(debug=debug) as (consul, pg1, pg2):\n            if debug:\n                print(\"=\" * 80)\n                print(\"Stolon is ready\")\n            async with tb.start_edgedb_server(\n                backend_dsn=(\n                    f\"stolon+consul+http://127.0.0.1:{consul.http_port}\"\n                    f\"/{pg1.cluster_name}\"\n                    f\"?pguser=suname&pgpassword=supass&pgdatabase=postgres\"\n                ),\n                runstate_dir=str(pathlib.Path(consul.tmp_dir.name) / \"edb\"),\n                reset_auth=True,\n                debug=debug,\n            ) as sd:\n                await self._test_failover(pg1, pg2, sd, debug=debug)\n\n    async def test_ha_adaptive(self):\n        debug = False\n        env = dict(\n            EDGEDB_SERVER_BACKEND_ADAPTIVE_HA_UNHEALTHY_MIN_TIME=\"3\"\n        )\n        async with stolon_setup(debug=debug) as (consul, pg1, pg2):\n            async with AdaptiveHAProxy(consul.http_port, debug=debug) as port:\n                async with tb.start_edgedb_server(\n                    backend_dsn=(\n                        f\"postgresql://suname:supass@127.0.0.1:{port}/postgres\"\n                    ),\n                    runstate_dir=str(\n                        pathlib.Path(consul.tmp_dir.name) / \"edb\"\n                    ),\n                    enable_backend_adaptive_ha=True,\n                    reset_auth=True,\n                    debug=debug,\n                    env=env,\n                ) as sd:\n                    await self._test_failover(pg1, pg2, sd, debug=debug)\n\n    async def _test_failover(self, pg1, pg2, sd, debug=False):\n        if debug:\n            print(\"=\" * 80)\n            print(\"Initialize the State\")\n        con = await sd.connect()\n        await con.execute(\n            \"CREATE TYPE State { \"\n            \"   CREATE REQUIRED PROPERTY value -> int32;\"\n            \"};\"\n        )\n        await con.execute(\"INSERT State { value := 1 };\")\n        self.assertEqual(\n            await con.query_single(\"SELECT State.value LIMIT 1\"), 1\n        )\n        if debug:\n            print(\"=\" * 80)\n            print(\"Stop the master, failover to slave\")\n        await pg1.stop()\n        if debug:\n            print(\"=\" * 80)\n            print(\"Master stopped\")\n        async for tx in con.with_retry_options(\n            edgedb.RetryOptions(60, lambda x: 1)\n        ).retrying_transaction():\n            async with tx:\n                self.assertEqual(\n                    await tx.query_single(\"SELECT State.value LIMIT 1\"),\n                    1,\n                )\n                await tx.execute(\"UPDATE State SET { value := 2 };\")\n        if debug:\n            print(\"=\" * 80)\n            print(\"State updated to 2\")\n        self.assertEqual(\n            await con.query_single(\"SELECT State.value LIMIT 1\"), 2\n        )\n        if debug:\n            print(\"=\" * 80)\n            print(\"Start the old master as slave\")\n        await pg1.start()\n        if debug:\n            print(\"=\" * 80)\n            print(\"Stop the new master, failover to old master again\")\n        await pg2.stop()\n        if debug:\n            print(\"=\" * 80)\n            print(\"State should still be 2\")\n        async for tx in con.with_retry_options(\n            edgedb.RetryOptions(60, lambda x: 1)\n        ).retrying_transaction():\n            async with tx:\n                self.assertEqual(\n                    await tx.query_single(\"SELECT State.value LIMIT 1\"),\n                    2,\n                )\n"
  },
  {
    "path": "tests/test_constraints.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2012-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport os.path\nimport re\n\nimport edgedb\n\nfrom edb.testbase import server as tb\n\n\nclass TestConstraintsSchema(tb.QueryTestCase):\n\n    SCHEMA = os.path.join(os.path.dirname(__file__), 'schemas',\n                          'constraints.esdl')\n\n    SETUP = '''\n    administer _remove_pointless_triggers();\n    '''\n\n    async def _run_link_tests(self, cases, objtype, link, *,\n                              values_as_str=True):\n        qry = f\"\"\"\n            INSERT {objtype} {{{{\n                {link} := {{value}}\n            }}}};\n        \"\"\"\n\n        for val, expected in cases:\n            async with self._run_and_rollback():\n                if values_as_str:\n                    expr = qry.format(value=f'{str(val)!r}')\n                else:\n                    expr = qry.format(value=val)\n\n                if expected == 'good':\n                    try:\n                        await self.con.execute(expr)\n                    except Exception as ex:\n                        raise AssertionError(f'{expr!r} failed') from ex\n                else:\n                    with self.assertRaisesRegex(\n                            edgedb.ConstraintViolationError, expected):\n                        await self.con.execute(expr)\n\n    async def test_constraints_scalar_length(self):\n        data = {\n            # max-length is 10\n            (10 ** 10,\n             'constraint_length must be no longer than 10 characters.'),\n            (10 ** 10 - 1, 'good'),\n            (10 ** 7 - 1,\n             'constraint_length must be no shorter than 8 characters'),\n            (10 ** 7, 'good'),\n        }\n\n        await self._run_link_tests(data, 'default::Object', 'c_length')\n\n        data = {\n            (10 ** 10,\n             'constraint_length must be no longer than 10 characters.'),\n            (10 ** 10 - 1, 'good'),\n\n            (10 ** 8 - 1,\n             'constraint_length_2 must be no shorter than 9 characters'),\n            (10 ** 8, 'good'),\n        }\n\n        await self._run_link_tests(data, 'default::Object', 'c_length_2')\n\n        data = {\n            (10 ** 10,\n             'constraint_length must be no longer than 10 characters.'),\n            (10 ** 10 - 1, 'good'),\n            (10 ** 9 - 1, 'c_length_3 must be no shorter than 10 characters'),\n        }\n\n        await self._run_link_tests(data, 'default::Object', 'c_length_3')\n\n    async def test_constraints_scalar_minmax_01(self):\n        data = {\n            # max-value is \"9999999989\"\n            (10 ** 9 - 1, \"Maximum allowed value for .* is '9999999989'.\"),\n            (10 ** 9 - 11, 'good'),\n\n            # min-value is \"99990000\"\n            (10 ** 8 - 10 ** 4 - 1,\n             \"Minimum allowed value for .* is '99990000'.\"),\n            (10 ** 8 - 21, 'good'),\n        }\n\n        await self._run_link_tests(data, 'default::Object', 'c_minmax')\n\n    async def test_constraints_scalar_minmax_02(self):\n        data = {\n            # exclusive max-value is \"100\"\n            (1000, \".* must be less than 100.\"),\n            (100, \".* must be less than 100.\"),\n            (99.9999, 'good'),\n            (99, 'good'),\n\n            # exclusive min-value is \"13\"\n            (56, 'good'),\n            (13.0001, \"good\"),\n            (13, \".* must be greater than 13.\"),\n            (0, \".* must be greater than 13.\"),\n        }\n\n        await self._run_link_tests(data, 'default::Object', 'c_ex_minmax',\n                                   values_as_str=False)\n\n    async def test_constraints_scalar_strvalue(self):\n        data = {\n            # last digit is 9\n            (10 ** 9 - 12, 'invalid .*'),\n\n            # and the first is 9 too\n            (10 ** 9 - 10 ** 8 - 1, 'invalid .*'),\n\n            # and that all characters are digits\n            ('99900~0009', 'invalid .*'),\n\n            # and that first three chars are nines\n            ('9900000009', 'invalid .*'),\n            ('9999000009', 'good'),\n        }\n\n        await self._run_link_tests(data, 'default::Object', 'c_strvalue')\n\n    async def test_constraints_scalar_enum_01(self):\n        data = {\n            ('foobar', 'must be one of:'),\n            ('bar', 'good'),\n            ('foo', 'good'),\n        }\n\n        await self._run_link_tests(data, 'default::Object', 'c_enum')\n\n    async def test_constraints_scalar_enum_02(self):\n        data = {\n            ('foo', 'invalid'),\n            ('fuz', 'good'),\n            ('buz', 'good'),\n        }\n\n        await self._run_link_tests(data, 'default::Object', 'c_my_enum')\n\n    async def test_constraints_exclusive_simple(self):\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    'name violates exclusivity constraint'):\n                await self.con.execute(\"\"\"\n                    INSERT UniqueName {\n                        name := 'Test'\n                    };\n\n                    INSERT UniqueName {\n                        name := 'Test'\n                    };\n                \"\"\")\n\n    async def test_constraints_exclusive_inherited(self):\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    'name violates exclusivity constraint'):\n                await self.con.execute(\"\"\"\n                    INSERT UniqueNameInherited {\n                        name := 'Test'\n                    };\n\n                    INSERT UniqueNameInherited {\n                        name := 'Test'\n                    };\n                \"\"\")\n\n    async def test_constraints_exclusive_across_ancestry(self):\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    'name violates exclusivity constraint'):\n\n                await self.con.execute(\"\"\"\n                    INSERT UniqueName {\n                        name := 'exclusive_name_across'\n                    };\n\n                    INSERT UniqueNameInherited {\n                        name := 'exclusive_name_across'\n                    };\n                \"\"\")\n\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    'name violates exclusivity constraint'):\n\n                await self.con.execute(\"\"\"\n                    INSERT UniqueNameInherited {\n                        name := 'exclusive_name_across'\n                    };\n\n                    INSERT UniqueName {\n                        name := 'exclusive_name_across'\n                    };\n                \"\"\")\n\n        async with self._run_and_rollback():\n            await self.con.execute(\"\"\"\n                INSERT UniqueName {\n                    name := 'exclusive_name_ok'\n                };\n\n                INSERT UniqueNameInherited {\n                    name := 'exclusive_name_inherited_ok'\n                };\n            \"\"\")\n\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    'name violates exclusivity constraint'):\n                await self.con.execute(\"\"\"\n                    UPDATE\n                        UniqueNameInherited\n                    FILTER\n                        UniqueNameInherited.name =\n                            'exclusive_name_inherited_ok'\n                    SET {\n                        name := 'exclusive_name_ok'\n                    };\n                \"\"\")\n\n    async def test_constraints_exclusive_case_insensitive(self):\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    'name violates exclusivity constraint'):\n                await self.con.execute(\"\"\"\n                    INSERT UniqueName_3 {\n                        name := 'TeSt'\n                    };\n\n                    INSERT UniqueName_3 {\n                        name := 'tEsT'\n                    };\n                \"\"\")\n\n    async def test_constraints_exclusive_delegation(self):\n        async with self._run_and_rollback():\n            # This is OK, the name exclusivity constraint is delegating\n            await self.con.execute(\"\"\"\n                INSERT AbstractConstraintParent {\n                    name := 'exclusive_name_ap'\n                };\n\n                INSERT AbstractConstraintParent {\n                    name := 'exclusive_name_ap'\n                };\n            \"\"\")\n\n            # This is OK too\n            await self.con.execute(\"\"\"\n                INSERT AbstractConstraintParent {\n                    name := 'exclusive_name_ap1'\n                };\n\n                INSERT AbstractConstraintPureChild {\n                    name := 'exclusive_name_ap1'\n                };\n            \"\"\")\n\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    'name violates exclusivity constraint'):\n                # Not OK, abstract constraint materializes into a real one\n                await self.con.execute(\"\"\"\n                    INSERT AbstractConstraintPureChild {\n                        name := 'exclusive_name_ap2'\n                    };\n\n                    INSERT AbstractConstraintPureChild {\n                        name := 'exclusive_name_ap2'\n                    };\n                \"\"\")\n\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    'name violates exclusivity constraint'):\n                # Not OK, abstract constraint materializes into a real one\n                await self.con.execute(\"\"\"\n                    INSERT AbstractConstraintMixedChild {\n                        name := 'exclusive_name_ap2'\n                    };\n\n                    INSERT AbstractConstraintMixedChild {\n                        name := 'exclusive_name_AP2'\n                    };\n                \"\"\")\n\n        async with self._run_and_rollback():\n            # This is OK, duplication is in different children\n            await self.con.execute(\"\"\"\n                INSERT AbstractConstraintPureChild {\n                    name := 'exclusive_name_ap3'\n                };\n\n                INSERT AbstractConstraintMixedChild {\n                    name := 'exclusive_name_ap3'\n                };\n            \"\"\")\n\n            # This is OK, the name exclusivity constraint is abstract again\n            await self.con.execute(\"\"\"\n                INSERT AbstractConstraintPropagated {\n                    name := 'exclusive_name_ap4'\n                };\n\n                INSERT AbstractConstraintPropagated {\n                    name := 'exclusive_name_ap4'\n                };\n            \"\"\")\n\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    'name violates exclusivity constraint'):\n                # Not OK, yet\n                await self.con.execute(\"\"\"\n                    INSERT BecomingAbstractConstraint {\n                        name := 'exclusive_name_ap5'\n                    };\n\n                    INSERT BecomingAbstractConstraintChild {\n                        name := 'exclusive_name_ap5'\n                    };\n                \"\"\")\n\n        async with self._run_and_rollback():\n            await self.con.execute(\"\"\"\n                INSERT BecomingConcreteConstraint {\n                    name := 'exclusive_name_ap6'\n                };\n\n                INSERT BecomingConcreteConstraintChild {\n                    name := 'exclusive_name_ap6'\n                };\n            \"\"\")\n\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    'name violates exclusivity constraint'):\n                await self.con.execute(\"\"\"\n                    INSERT LosingAbstractConstraintParent {\n                        name := 'exclusive_name_ap7'\n                    };\n\n                    INSERT LosingAbstractConstraintParent {\n                        name := 'exclusive_name_ap7'\n                    };\n                \"\"\")\n\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    'name violates exclusivity constraint'):\n                await self.con.execute(\"\"\"\n                    INSERT AbstractConstraintMultipleParentsFlattening{\n                        name := 'exclusive_name_ap8'\n                    };\n\n                    INSERT AbstractConstraintMultipleParentsFlattening{\n                        name := 'exclusive_name_ap8'\n                    };\n                \"\"\")\n\n    async def test_constraints_exclusive_pair(self):\n        await self.assert_query_result(\n            r'''\n                select {\n                    single z := (\n                        select Pair {x, y} filter .x = 'a' and .y = 'b')\n                }\n            ''',\n            [\n                {\"z\": None}\n            ],\n        )\n\n    async def test_constraints_exclusive_multi_property_distinct(self):\n        await self.con.execute(\"\"\"\n            INSERT PropertyContainer {\n                tags := {\"one\", \"two\"}\n            };\n        \"\"\")\n\n        # Update to same values should be fine\n        await self.con.execute(\"\"\"\n            UPDATE PropertyContainer SET {\n                tags := {\"one\", \"two\"}\n            };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"tags violates exclusivity constraint\",\n        ):\n            await self.con.execute(\"\"\"\n                INSERT PropertyContainer {\n                    tags := {\"one\", \"three\"}\n                };\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"tags violates exclusivity constraint\",\n        ):\n            await self.con.execute(\"\"\"\n                INSERT PropertyContainer {\n                    tags := {\"four\", \"four\"}\n                };\n            \"\"\")\n\n        await self.con.execute(\"\"\"\n            UPDATE PropertyContainer SET {\n                tags := \"one\"\n            };\n        \"\"\")\n\n    async def test_constraints_objects(self):\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    \"ObjCnstr violates exclusivity constraint\"):\n                await self.con.execute(\"\"\"\n                    INSERT ObjCnstr {\n                        first_name := \"foo\", last_name := \"bar\" };\n\n                    INSERT ObjCnstr {\n                        first_name := \"foo\", last_name := \"baz\" }\n            \"\"\")\n\n        async with self._run_and_rollback():\n            await self.con.execute(\"\"\"\n                INSERT ObjCnstr {\n                    first_name := \"foo\", last_name := \"bar\",\n                    label := (INSERT Label {text := \"obj_test\" })\n                };\n            \"\"\")\n\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    \"ObjCnstr violates exclusivity constraint\"):\n                await self.con.execute(\"\"\"\n                    INSERT ObjCnstr {\n                        first_name := \"emarg\", last_name := \"hatch\",\n                        label := (SELECT Label\n                                  FILTER .text = \"obj_test\" LIMIT 1) };\n                \"\"\")\n\n    async def test_constraints_endpoint_constraint_01(self):\n        # testing (@source, @lang) on a single link\n        # This constraint is pointless and can never fail\n        await self.con.execute(\"\"\"\n            insert UniqueName {\n                translated_label := ((insert Label) { @lang := 'xxx' })\n            };\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            insert UniqueName {\n                translated_label := ((insert Label) { @lang := 'xxx' })\n            };\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            insert UniqueName {\n                translated_label := ((select Label limit 1) { @lang := 'yyy' })\n            };\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            insert UniqueName {\n                translated_label := ((select Label limit 1) { @lang := 'xxx' })\n            };\n        \"\"\")\n\n    async def test_constraints_endpoint_constraint_02(self):\n        # testing (@source, @lang) on a multi link\n        await self.con.execute(\"\"\"\n            insert UniqueName {\n                translated_labels := ((insert Label { text := \"x\" }) {\n                 @lang := 'x' })\n            };\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            update UniqueName set {\n                translated_labels := Label {@lang := 'x' }\n            };\n        \"\"\")\n\n        # Should be fine\n        await self.con.execute(\"\"\"\n            insert UniqueName {\n                translated_labels := ((insert Label { text := \"y\"  }) {\n                  @lang := 'x' })\n            };\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            insert UniqueName {\n                translated_labels := (Label { @lang := .text })\n            };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'violates exclusivity constraint'\n        ):\n            await self.con.execute(\"\"\"\n                insert UniqueName {\n                    translated_labels := (Label { @lang := 'x' })\n                };\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'violates exclusivity constraint'\n        ):\n            await self.con.execute(\"\"\"\n                insert UniqueNameInherited {\n                    translated_labels := (Label { @lang := 'x' })\n                };\n            \"\"\")\n\n    async def test_constraints_endpoint_constraint_03(self):\n        # testing (@target, @lang) on a single link\n        await self.con.execute(\"\"\"\n            insert UniqueName {\n                translated_label_tgt := ((insert Label { text := \"x\" }) {\n                 @lang := 'x' })\n            };\n        \"\"\")\n\n        # Same @lang different @target\n        await self.con.execute(\"\"\"\n            insert UniqueName {\n                translated_label_tgt := ((insert Label { text := \"y\" }) {\n                  @lang := 'x' })\n            };\n        \"\"\")\n\n        # Same @target different @lang\n        await self.con.execute(\"\"\"\n            insert UniqueNameInherited {\n                translated_label_tgt := (\n                  select Label { @lang := 'y' } filter .text = 'x' limit 1)\n            };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'violates exclusivity constraint'\n        ):\n            await self.con.execute(\"\"\"\n                insert UniqueName {\n                    translated_label_tgt := (\n                      select Label { @lang := 'x' } filter .text = 'x' limit 1)\n                };\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'violates exclusivity constraint'\n        ):\n            await self.con.execute(\"\"\"\n                insert UniqueNameInherited {\n                    translated_label_tgt := (\n                      select Label { @lang := 'x' } filter .text = 'x' limit 1)\n                };\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'violates exclusivity constraint'\n        ):\n            await self.con.execute(\"\"\"\n                update UniqueName\n                filter .translated_label_tgt.text = 'x'\n                set { translated_label_tgt :=\n                    .translated_label_tgt { @lang := '!' }\n                }\n            \"\"\")\n\n        await self.con.execute(\"\"\"\n            update UniqueName\n            filter .translated_label_tgt.text = 'x'\n            set { translated_label_tgt :=\n                .translated_label_tgt { @lang := @lang }\n            }\n        \"\"\")\n\n    async def test_constraints_endpoint_constraint_04(self):\n        # testing (@target, @lang) on a multi link\n        await self.con.execute(\"\"\"\n            insert UniqueName {\n                translated_labels_tgt := ((insert Label { text := \"x\" }) {\n                 @lang := 'x' })\n            };\n        \"\"\")\n\n        # Same @lang different @target\n        await self.con.execute(\"\"\"\n            insert UniqueName {\n                translated_labels_tgt := ((insert Label { text := \"y\" }) {\n                  @lang := 'x' })\n            };\n        \"\"\")\n\n        # Same @target different @lang\n        await self.con.execute(\"\"\"\n            insert UniqueName {\n                translated_labels_tgt := (\n                  select Label { @lang := 'y' } filter .text = 'x')\n            };\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            insert UniqueNameInherited {\n                translated_labels_tgt := (\n                  select Label { @lang := 'x!' })\n            };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'violates exclusivity constraint'\n        ):\n            await self.con.execute(\"\"\"\n                insert UniqueName {\n                    translated_labels_tgt := (\n                      select Label { @lang := 'x' } filter .text = 'x')\n                };\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'violates exclusivity constraint'\n        ):\n            await self.con.execute(\"\"\"\n                insert UniqueNameInherited {\n                    translated_labels_tgt := (\n                      select Label { @lang := 'x' } filter .text = 'x')\n                };\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'violates exclusivity constraint'\n        ):\n            await self.con.execute(\"\"\"\n                insert UniqueNameInherited {\n                    translated_labels_tgt := (\n                      select Label { @lang := .text ++ '!' }\n                      filter .text = 'x')\n                };\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'violates exclusivity constraint'\n        ):\n            await self.con.execute(\"\"\"\n                update UniqueName\n                filter 'x' in .translated_labels_tgt.text\n                set { translated_labels_tgt :=\n                    .translated_labels_tgt { @lang := '!' }\n                }\n            \"\"\")\n\n        await self.con.execute(\"\"\"\n            update UniqueName\n            filter 'x' in .translated_labels_tgt.text\n            set { translated_labels_tgt :=\n                .translated_labels_tgt { @lang := @lang }\n            }\n        \"\"\")\n\n\nclass TestConstraintsSchemaMigration(tb.QueryTestCase):\n\n    SCHEMA = os.path.join(os.path.dirname(__file__),\n                          'schemas', 'constraints_migration',\n                          'schema.esdl')\n\n    async def test_constraints_exclusive_migration(self):\n        new_schema_f = os.path.join(os.path.dirname(__file__),\n                                    'schemas', 'constraints_migration',\n                                    'updated_schema.esdl')\n\n        with open(new_schema_f) as f:\n            new_schema = f.read()\n\n        await self.migrate(new_schema)\n\n        async with self._run_and_rollback():\n            # This is OK, the name exclusivity constraint is abstract\n            await self.con.execute(\"\"\"\n                INSERT AbstractConstraintParent {\n                    name := 'exclusive_name_ap'\n                };\n\n                INSERT AbstractConstraintParent {\n                    name := 'exclusive_name_ap'\n                };\n            \"\"\")\n\n            # This is OK too\n            await self.con.execute(\"\"\"\n                INSERT AbstractConstraintParent {\n                    name := 'exclusive_name_ap1'\n                };\n\n                INSERT AbstractConstraintPureChild {\n                    name := 'exclusive_name_ap1'\n                };\n            \"\"\")\n\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    'name violates exclusivity constraint'):\n                # Not OK, abstract constraint materializes into a real one\n                await self.con.execute(\"\"\"\n                    INSERT AbstractConstraintPureChild {\n                        name := 'exclusive_name_ap2'\n                    };\n\n                    INSERT AbstractConstraintPureChild {\n                        name := 'exclusive_name_ap2'\n                    };\n                \"\"\")\n\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    'name violates exclusivity constraint'):\n                # Not OK, abstract constraint materializes into a real one\n                await self.con.execute(\"\"\"\n                    INSERT AbstractConstraintMixedChild {\n                        name := 'exclusive_name_ap2'\n                    };\n\n                    INSERT AbstractConstraintMixedChild {\n                        name := 'exclusive_name_AP2'\n                    };\n                \"\"\")\n\n        async with self._run_and_rollback():\n            # This is OK, duplication is in different children\n            await self.con.execute(\"\"\"\n                INSERT AbstractConstraintMixedChild {\n                    name := 'exclusive_name_ap3'\n                };\n\n                INSERT AbstractConstraintPureChild {\n                    name := 'exclusive_name_ap3'\n                };\n            \"\"\")\n\n        async with self._run_and_rollback():\n            # This is OK, the name exclusivity constraint is abstract again\n            await self.con.execute(\"\"\"\n                INSERT AbstractConstraintPropagated {\n                    name := 'exclusive_name_ap4'\n                };\n\n                INSERT AbstractConstraintPropagated {\n                    name := 'exclusive_name_ap4'\n                };\n            \"\"\")\n\n        async with self._run_and_rollback():\n            # OK, former constraint was turned into an abstract constraint\n            await self.con.execute(\"\"\"\n                INSERT BecomingAbstractConstraint {\n                    name := 'exclusive_name_ap5'\n                };\n\n                INSERT BecomingAbstractConstraintChild {\n                    name := 'exclusive_name_ap5'\n                };\n            \"\"\")\n\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    'name violates exclusivity constraint'):\n                # Constraint is no longer abstract\n                await self.con.execute(\"\"\"\n                    INSERT BecomingConcreteConstraint {\n                        name := 'exclusive_name_ap6'\n                    };\n\n                    INSERT BecomingConcreteConstraintChild {\n                        name := 'exclusive_name_ap6'\n                    };\n                \"\"\")\n\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    'name violates exclusivity constraint'):\n                # Constraint is no longer abstract\n                await self.con.execute(\"\"\"\n                    INSERT LosingAbstractConstraintParent {\n                        name := 'exclusive_name_ap6'\n                    };\n\n                    INSERT LosingAbstractConstraintParent {\n                        name := 'exclusive_name_ap6'\n                    };\n                \"\"\")\n\n        async with self._run_and_rollback():\n            await self.con.execute(\"\"\"\n                INSERT LosingAbstractConstraintParent2 {\n                    name := 'exclusive_name_ap7'\n                };\n\n                INSERT LosingAbstractConstraintParent2 {\n                    name := 'exclusive_name_ap7'\n                };\n            \"\"\")\n\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    'name violates exclusivity constraint'):\n                # Constraint is no longer abstract\n                await self.con.execute(\"\"\"\n                    INSERT AbstractConstraintMultipleParentsFlattening{\n                        name := 'exclusive_name_ap8'\n                    };\n\n                    INSERT AbstractConstraintMultipleParentsFlattening{\n                        name := 'exclusive_name_AP8'\n                    };\n                \"\"\")\n\n        async with self._run_and_rollback():\n            with self.assertRaisesRegex(\n                    edgedb.ConstraintViolationError,\n                    \"nope!\"):\n                await self.con.execute(\"\"\"\n                    INSERT ObjCnstr {\n                        first_name := \"foo\", last_name := \"bar\" };\n\n                    INSERT ObjCnstr {\n                        first_name := \"foo\", last_name := \"baz\" }\n            \"\"\")\n\n\nclass TestConstraintsDDL(tb.DDLTestCase):\n\n    async def test_constraints_ddl_01(self):\n        qry = \"\"\"\n            CREATE ABSTRACT LINK translated_label {\n                CREATE PROPERTY lang -> std::str;\n                CREATE PROPERTY prop1 -> std::str;\n            };\n\n            CREATE ABSTRACT LINK link_with_exclusive_property {\n                CREATE PROPERTY exclusive_property -> std::str {\n                    CREATE CONSTRAINT std::exclusive;\n                };\n            };\n\n            CREATE ABSTRACT LINK link_with_exclusive_property_inherited\n                EXTENDING link_with_exclusive_property;\n\n            CREATE TYPE UniqueName {\n                CREATE PROPERTY name -> std::str {\n                    CREATE CONSTRAINT std::exclusive;\n                };\n\n                CREATE LINK link_with_exclusive_property -> std::Object;\n            };\n        \"\"\"\n\n        await self.con.execute(qry)\n\n        # Simple exclusivity constraint on a link\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'name violates exclusivity constraint',\n        ):\n            await self.con.execute(\"\"\"\n                INSERT UniqueName {\n                    name := 'Test'\n                };\n\n                INSERT UniqueName {\n                    name := 'Test'\n                };\n            \"\"\")\n\n        qry = \"\"\"\n            CREATE TYPE AbstractConstraintParent {\n                CREATE PROPERTY name -> std::str {\n                    CREATE DELEGATED CONSTRAINT std::exclusive;\n                };\n            };\n\n            CREATE TYPE AbstractConstraintPureChild\n                EXTENDING AbstractConstraintParent;\n        \"\"\"\n\n        await self.con.execute(qry)\n\n        # This is OK, the name exclusivity constraint is abstract\n        await self.con.execute(\"\"\"\n            INSERT AbstractConstraintParent {\n                name := 'exclusive_name_ap'\n            };\n\n            INSERT AbstractConstraintParent {\n                name := 'exclusive_name_ap'\n            };\n        \"\"\")\n\n        # This is OK too\n        await self.con.execute(\"\"\"\n            INSERT AbstractConstraintParent {\n                name := 'exclusive_name_ap1'\n            };\n\n            INSERT AbstractConstraintPureChild {\n                name := 'exclusive_name_ap1'\n            };\n        \"\"\")\n\n    async def test_constraints_ddl_02(self):\n        # testing the generalized constraint with 'ON (...)' clause\n        qry = r\"\"\"\n            CREATE ABSTRACT CONSTRAINT mymax1(max: std::int64)\n                    ON (len(__subject__))\n            {\n                SET errmessage :=\n                    '\\(__subject__) must be no longer than \\(max) characters.';\n                USING (__subject__ <= max);\n            };\n\n            CREATE ABSTRACT CONSTRAINT mymax_ext1(max: std::int64)\n                    ON (len(__subject__)) EXTENDING std::max_value\n            {\n                SET errmessage :=\n                    '\\(__subject__) must be no longer than \\(max) characters.';\n            };\n\n            CREATE TYPE ConstraintOnTest1 {\n                CREATE PROPERTY foo -> std::str {\n                    CREATE CONSTRAINT mymax1(3);\n                };\n\n                CREATE PROPERTY bar -> std::str {\n                    CREATE CONSTRAINT mymax_ext1(3);\n                };\n            };\n        \"\"\"\n\n        await self.con.execute(qry)\n\n        await self.assert_query_result(\n            r'''\n                SELECT schema::Constraint {\n                    name,\n                    params: {\n                        num,\n                        name,\n                        kind,\n                        type: {\n                            name\n                        },\n                        typemod,\n                        @value\n                    }\n                    FILTER .num > 0\n                    ORDER BY .num ASC\n                } FILTER\n                    .name = 'default::mymax_ext1'\n                    AND exists(.subject);\n            ''',\n            [\n                {\n                    \"name\": 'default::mymax_ext1',\n                    \"params\": [\n                        {\n                            \"num\": 1,\n                            \"kind\": 'PositionalParam',\n                            \"name\": 'max',\n                            \"type\": {\"name\": 'std::int64'},\n                            \"@value\": '3',\n                            \"typemod\": 'SingletonType'\n                        }\n                    ],\n                },\n            ]\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT schema::Constraint {\n                    name,\n                    params: {\n                        num,\n                        name,\n                        kind,\n                        type: {\n                            name\n                        },\n                        typemod\n                    }\n                    FILTER .num > 0\n                    ORDER BY .num ASC\n                } FILTER\n                    .name = 'default::mymax_ext1'\n                    AND NOT exists(.subject);\n            ''',\n            [\n                {\n                    \"name\": 'default::mymax_ext1',\n                    \"params\": [\n                        {\n                            \"num\": 1,\n                            \"kind\": 'PositionalParam',\n                            \"name\": 'max',\n                            \"type\": {\"name\": 'std::int64'},\n                            \"typemod\": 'SingletonType'\n                        }\n                    ],\n                },\n            ]\n        )\n\n        # making sure the constraint was applied successfully\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'foo must be no longer than 3 characters.',\n        ):\n            await self.con.execute(\"\"\"\n                INSERT ConstraintOnTest1 {\n                    foo := 'Test'\n                };\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'bar must be no longer than 3 characters.',\n        ):\n            await self.con.execute(\"\"\"\n                INSERT ConstraintOnTest1 {\n                    bar := 'Test'\n                };\n            \"\"\")\n\n        # constraint should not fail\n        await self.con.execute(\"\"\"\n            INSERT ConstraintOnTest1 {\n                foo := '',\n                bar := ''\n            };\n\n            INSERT ConstraintOnTest1 {\n                foo := 'a',\n                bar := 'q'\n            };\n\n            INSERT ConstraintOnTest1 {\n                foo := 'ab',\n                bar := 'qw'\n            };\n\n            INSERT ConstraintOnTest1 {\n                foo := 'abc',\n                bar := 'qwe'\n            };\n\n            # a duplicate 'foo' and 'bar' just for good measure\n            INSERT ConstraintOnTest1 {\n                foo := 'ab',\n                bar := 'qw'\n            };\n        \"\"\")\n\n    async def test_constraints_ddl_03(self):\n        # testing the specialized constraint with 'ON (...)' clause\n        qry = r\"\"\"\n            CREATE ABSTRACT CONSTRAINT mymax2(max: std::int64) {\n                SET errmessage :=\n                    '{__subject__} must be no longer than {max} characters.';\n                USING (__subject__ <= max);\n            };\n\n            CREATE TYPE ConstraintOnTest2 {\n                CREATE PROPERTY foo -> std::str {\n                    CREATE CONSTRAINT mymax2(3) ON (len(__subject__));\n                };\n\n                CREATE PROPERTY bar -> std::str {\n                    CREATE CONSTRAINT std::max_value(3) ON (len(__subject__)) {\n                        SET errmessage :=\n                    # XXX: once simple string concat is possible here\n                    #      formatting can be saner\n                    '{__subject__} must be no longer than {max} characters.';\n                    };\n                };\n            };\n        \"\"\"\n\n        await self.con.execute(qry)\n\n        # making sure the constraint was applied successfully\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'foo must be no longer than 3 characters.',\n        ):\n            await self.con.execute(\"\"\"\n                INSERT ConstraintOnTest2 {\n                    foo := 'Test'\n                };\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'bar must be no longer than 3 characters.',\n        ):\n            await self.con.execute(\"\"\"\n                INSERT ConstraintOnTest2 {\n                    bar := 'Test'\n                };\n            \"\"\")\n\n        # constraint should not fail\n        await self.con.execute(\"\"\"\n            INSERT ConstraintOnTest2 {\n                foo := '',\n                bar := ''\n            };\n\n            INSERT ConstraintOnTest2 {\n                foo := 'a',\n                bar := 'q'\n            };\n\n            INSERT ConstraintOnTest2 {\n                foo := 'ab',\n                bar := 'qw'\n            };\n\n            INSERT ConstraintOnTest2 {\n                foo := 'abc',\n                bar := 'qwe'\n            };\n\n            # a duplicate 'foo' and 'bar' just for good measure\n            INSERT ConstraintOnTest2 {\n                foo := 'ab',\n                bar := 'qw'\n            };\n        \"\"\")\n\n    async def test_constraints_ddl_04(self):\n        # testing an issue with expressions used for 'errmessage'\n        qry = r\"\"\"\n            CREATE ABSTRACT CONSTRAINT mymax3(max: std::int64) {\n                SET errmessage :=\n                    'My custom ' ++ 'message.';\n                USING (__subject__ <= max);\n            };\n\n            CREATE TYPE ConstraintOnTest3 {\n                CREATE PROPERTY foo -> std::str {\n                    CREATE CONSTRAINT mymax3(3) ON (len(__subject__));\n                };\n            };\n        \"\"\"\n\n        await self.con.execute(qry)\n\n        # making sure the constraint was applied successfully\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'My custom message.',\n            _details=\"violated constraint 'default::mymax3' on \"\n            \"property 'foo' of \"\n            \"object type 'default::ConstraintOnTest3'\",\n        ):\n            await self.con.execute(\n                \"\"\"\n                INSERT ConstraintOnTest3 {\n                    foo := 'Test'\n                };\n                \"\"\"\n            )\n\n        # testing interpolation\n        await self.con.execute(r\"\"\"\n            CREATE type ConstraintOnTest4_2 {\n                CREATE required property email -> str {\n                    CREATE constraint min_len_value(4) {\n                        SET errmessage := '{\"json\": \"{nope} {{min}} {min}\"}';\n                    };\n                };\n            };\n        \"\"\")\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            '{\"json\": \"{nope} {min} 4\"}',\n            _details=\"violated constraint 'std::min_len_value' on \"\n            \"property 'email' of \"\n            \"object type 'default::ConstraintOnTest4_2'\",\n        ):\n            await self.con.execute(\n                \"\"\"\n                INSERT ConstraintOnTest4_2 { email := '' };\n                \"\"\"\n            )\n\n        await self.con.execute(r\"\"\"\n            CREATE type ConstraintOnTest4_3 {\n                CREATE required property email -> str {\n                    CREATE constraint min_len_value(4) {\n                        SET errmessage := '\\\\(min) \\\\(min)';\n                    };\n                };\n            };\n        \"\"\")\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            re.escape(r'\\(min) \\(min)'),\n        ):\n            await self.con.execute(\n                \"\"\"\n                INSERT ConstraintOnTest4_3 { email := '' };\n                \"\"\"\n            )\n\n    async def test_constraints_ddl_05(self):\n        # Test that constraint expression returns a boolean.\n\n        await self.con.execute(r\"\"\"\n            CREATE FUNCTION con05(a: int64) -> str\n                USING EdgeQL $$\n                    SELECT <str>a\n                $$;\n        \"\"\")\n\n        # create a type with a constraint\n        await self.con.execute(r\"\"\"\n            CREATE TYPE ConstraintOnTest5 {\n                CREATE REQUIRED PROPERTY foo -> int64 {\n                    # Use the function in a constraint expression,\n                    # s.t. it will effectively fail for any int\n                    # outside 0-9 range.\n                    CREATE CONSTRAINT\n                        std::expression on (len(con05(__subject__)) < 2);\n                }\n            }\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.errors.ConstraintViolationError,\n            r'invalid foo',\n        ):\n            await self.con.execute(\"\"\"\n                INSERT ConstraintOnTest5 {\n                    foo := 42\n                };\n            \"\"\")\n\n        # constraint should not fail\n        await self.con.execute(\"\"\"\n            INSERT ConstraintOnTest5 {\n                foo := 2\n            };\n        \"\"\")\n\n    async def test_constraints_ddl_06(self):\n        # Test that constraint expression returns a boolean.\n\n        await self.con.execute(r\"\"\"\n            CREATE FUNCTION con06(a: int64) -> array<int64>\n                USING EdgeQL $$\n                    SELECT [a]\n                $$;\n        \"\"\")\n\n        # create a type with a constraint\n        await self.con.execute(r\"\"\"\n            CREATE TYPE ConstraintOnTest6 {\n                CREATE REQUIRED PROPERTY foo -> int64 {\n                    # Use the function in a constraint expression,\n                    # s.t. it will never fail.\n                    CREATE CONSTRAINT\n                        std::expression on (len(con06(__subject__)) < 2);\n                }\n            }\n        \"\"\")\n\n        # constraint should not fail\n        await self.con.execute(\"\"\"\n            INSERT ConstraintOnTest6 {\n                foo := 42\n            };\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            INSERT ConstraintOnTest6 {\n                foo := 2\n            };\n        \"\"\")\n\n    async def test_constraints_ddl_07(self):\n        await self.con.execute(\"\"\"\n            CREATE TYPE ObjCnstr {\n                CREATE PROPERTY first_name -> str;\n                CREATE PROPERTY last_name -> str;\n                CREATE CONSTRAINT exclusive on (__subject__.first_name);\n            };\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            INSERT ObjCnstr { first_name := \"foo\", last_name := \"bar\" }\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"ObjCnstr violates exclusivity constraint\",\n        ):\n            await self.con.execute(\"\"\"\n                INSERT ObjCnstr {\n                    first_name := \"foo\", last_name := \"baz\" }\n            \"\"\")\n\n        await self.con.execute(\"\"\"\n            ALTER TYPE ObjCnstr {\n                DROP CONSTRAINT exclusive on (__subject__.first_name);\n            }\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            ALTER TYPE ObjCnstr {\n                CREATE CONSTRAINT exclusive\n                on ((__subject__.first_name, __subject__.last_name));\n            }\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            ALTER TYPE ObjCnstr {\n                ALTER CONSTRAINT exclusive\n                on ((__subject__.first_name, __subject__.last_name)) {\n                    SET errmessage := \"nope!\";\n                }\n            }\n        \"\"\")\n\n        # This one should work now\n        await self.con.execute(\"\"\"\n            INSERT ObjCnstr { first_name := \"foo\", last_name := \"baz\" }\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"nope!\",\n        ):\n            await self.con.execute(\"\"\"\n                INSERT ObjCnstr {\n                    first_name := \"foo\", last_name := \"bar\" }\n            \"\"\")\n\n    async def test_constraints_ddl_08(self):\n        await self.con.execute(\"\"\"\n            CREATE TYPE ObjCnstr2 {\n                CREATE MULTI PROPERTY first_name -> str;\n                CREATE MULTI PROPERTY last_name -> str;\n                CREATE LINK foo -> Object {\n                    CREATE PROPERTY p -> str;\n                };\n                CREATE CONSTRAINT exclusive on (__subject__.first_name);\n            };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidConstraintDefinitionError,\n            \"cannot reference multiple links or properties in a \"\n            \"constraint where at least one link or property is MULTI\",\n        ):\n            await self.con.execute(\"\"\"\n                ALTER TYPE ObjCnstr2 {\n                    CREATE CONSTRAINT exclusive\n                    on ((__subject__.first_name, __subject__.last_name));\n                };\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.UnsupportedFeatureError,\n            \"cannot use SET OF operator 'std::EXISTS' \"\n            \"in a constraint\",\n        ):\n            await self.con.execute(\"\"\"\n                ALTER TYPE ObjCnstr2 {\n                    CREATE CONSTRAINT expression on (EXISTS .first_name);\n                };\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.UnsupportedFeatureError,\n            \"cannot use SET OF operator 'std::EXISTS' \"\n            \"in a constraint\",\n        ):\n            await self.con.execute(\"\"\"\n                ALTER TYPE ObjCnstr2 {\n                    ALTER PROPERTY first_name {\n                        CREATE CONSTRAINT expression on (EXISTS __subject__);\n                    }\n                };\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidConstraintDefinitionError,\n            \"constraints cannot contain paths with more than one hop\",\n        ):\n            await self.con.execute(\"\"\"\n                ALTER TYPE ObjCnstr2 {\n                    CREATE CONSTRAINT expression on (<str>.foo.id != 'lol');\n                };\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidConstraintDefinitionError,\n            \"link constraints may not access the link target\",\n        ):\n            await self.con.execute(\"\"\"\n                ALTER TYPE ObjCnstr2 {\n                    ALTER LINK foo {\n                        CREATE CONSTRAINT expression on (\n                            <str>__subject__.id != 'lol');\n                    }\n                };\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidConstraintDefinitionError,\n            \"constraint expressions must be immutable\",\n        ):\n            await self.con.execute(\"\"\"\n                ALTER TYPE ObjCnstr2 {\n                    CREATE CONSTRAINT expression on (<str>.id != .foo@p);\n                };\n            \"\"\")\n\n    async def test_constraints_ddl_09(self):\n        await self.con.execute(\"\"\"\n            CREATE TYPE Label {\n                CREATE PROPERTY text -> str;\n            };\n            CREATE TYPE ObjCnstr3 {\n                CREATE LINK label -> Label;\n                CREATE CONSTRAINT exclusive on (__subject__.label);\n            };\n            INSERT ObjCnstr3 {\n                label := (INSERT Label {text := \"obj_test\" })\n            };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"ObjCnstr3 violates exclusivity constraint\",\n        ):\n            await self.con.execute(\"\"\"\n                INSERT ObjCnstr3 {\n                    label := (SELECT Label\n                                FILTER .text = \"obj_test\" LIMIT 1) };\n            \"\"\")\n\n    async def test_constraints_ddl_10(self):\n        await self.con.execute(r\"\"\"\n            CREATE ABSTRACT CONSTRAINT mymax5(max: std::int64) {\n                USING (__subject__ <= max);\n            };\n\n            CREATE TYPE ConstraintTest10 {\n                CREATE PROPERTY foo -> std::int64 {\n                    CREATE CONSTRAINT mymax5(3);\n                };\n            };\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            ALTER ABSTRACT CONSTRAINT mymax5\n            RENAME TO mymax6;\n        \"\"\")\n\n        async with self._run_and_rollback():\n            with self.assertRaises(edgedb.ConstraintViolationError):\n                await self.con.execute(r\"\"\"\n                    INSERT ConstraintTest10 { foo := 4 }\n                \"\"\")\n\n        await self.con.execute(r\"\"\"\n            CREATE MODULE foo IF NOT EXISTS;\n            ALTER ABSTRACT CONSTRAINT mymax6\n            RENAME TO foo::mymax2;\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            ALTER TYPE ConstraintTest10 {\n                ALTER PROPERTY foo {\n                    DROP CONSTRAINT foo::mymax2(3);\n                }\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            DROP ABSTRACT CONSTRAINT foo::mymax2;\n        \"\"\")\n\n    async def test_constraints_ddl_11(self):\n        qry = r\"\"\"\n            CREATE ABSTRACT CONSTRAINT mymax7(max: std::int64) {\n                USING (__subject__ <= max);\n            };\n        \"\"\"\n\n        # Check that renaming and then recreating works\n        await self.con.execute(qry)\n        await self.con.execute(\"\"\"\n            ALTER ABSTRACT CONSTRAINT mymax7 RENAME TO mymax8;\n        \"\"\")\n        await self.con.execute(qry)\n\n    async def test_constraints_ddl_12(self):\n        qry = r\"\"\"\n            CREATE ABSTRACT CONSTRAINT mymax9(max: std::int64) {\n                USING (__subject__ <= max);\n            };\n        \"\"\"\n\n        # Check that deleting and then recreating works\n        await self.con.execute(qry)\n        await self.con.execute(\"\"\"\n            DROP ABSTRACT CONSTRAINT mymax9;\n        \"\"\")\n        await self.con.execute(qry)\n\n    async def test_constraints_ddl_13(self):\n        await self.con.execute(r\"\"\"\n            CREATE ABSTRACT CONSTRAINT mymax13(max: std::int64) {\n                USING (__subject__ <= max);\n            };\n\n            CREATE TYPE ConstraintTest13 {\n                CREATE PROPERTY foo -> std::int64 {\n                    CREATE CONSTRAINT mymax13(3);\n                };\n            };\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            ALTER ABSTRACT CONSTRAINT mymax13\n            RENAME TO mymax13b;\n        \"\"\")\n\n        res = await self.con.query_single(\"\"\"\n            DESCRIBE MODULE default\n        \"\"\")\n\n        self.assertEqual(res.count(\"mymax13b\"), 2)\n\n    async def test_constraints_ddl_14(self):\n        await self.con.execute(r\"\"\"\n            CREATE ABSTRACT CONSTRAINT mymax14(max: std::int64) {\n                USING (__subject__ <= max);\n            };\n\n            CREATE TYPE ConstraintTest14 {\n                CREATE PROPERTY foo -> std::int64 {\n                    CREATE CONSTRAINT mymax14(3);\n                };\n            };\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            ALTER TYPE ConstraintTest14 {\n                ALTER PROPERTY foo {\n                    DROP CONSTRAINT mymax14(3);\n                }\n            }\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            ALTER TYPE ConstraintTest14 {\n                ALTER PROPERTY foo {\n                    CREATE CONSTRAINT mymax14(5);\n                }\n            }\n        \"\"\")\n\n    async def test_constraints_ddl_15(self):\n        await self.con.execute(r\"\"\"\n            CREATE ABSTRACT CONSTRAINT not_bad {\n                USING (__subject__ != \"bad\" and __subject__ != \"terrible\")\n            };\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            CREATE TYPE Foo {\n                CREATE PROPERTY foo -> str {\n                    CREATE CONSTRAINT not_bad;\n                }\n            };\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            ALTER ABSTRACT CONSTRAINT not_bad {\n                USING (__subject__ != \"bad\" and __subject__ != \"terrible\"\n                       and __subject__ != \"awful\")\n            };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"invalid foo\",\n        ):\n            await self.con.execute(r\"\"\"\n                INSERT Foo { foo := \"awful\" };\n            \"\"\")\n\n        await self.con.execute(r\"\"\"\n            INSERT Foo { foo := \"scow\" };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"invalid foo\",\n        ):\n            await self.con.execute(r\"\"\"\n                ALTER ABSTRACT CONSTRAINT not_bad {\n                    USING (__subject__ != \"bad\" and __subject__ != \"terrible\"\n                           and __subject__ != \"scow\")\n                };\n            \"\"\")\n\n    async def test_constraints_ddl_16(self):\n        await self.con.execute(\"\"\"\n            CREATE TYPE ObjCnstr {\n                CREATE PROPERTY first_name -> str;\n                CREATE PROPERTY last_name -> str;\n                CREATE CONSTRAINT exclusive ON (\n                    (.first_name ?? \"N/A\", .last_name ?? \"N/A\")\n                );\n            };\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            INSERT ObjCnstr { first_name := \"foo\", last_name := \"bar\" }\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"ObjCnstr violates exclusivity constraint\",\n        ):\n            await self.con.execute(\"\"\"\n                INSERT ObjCnstr {\n                    first_name := \"foo\", last_name := \"bar\" }\n            \"\"\")\n\n        await self.con.execute(\"\"\"\n            INSERT ObjCnstr { first_name := \"test\" }\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"ObjCnstr violates exclusivity constraint\",\n        ):\n            await self.con.execute(\"\"\"\n                INSERT ObjCnstr {\n                    first_name := \"test\"\n                }\n            \"\"\")\n\n        await self.con.execute(\"\"\"\n            INSERT ObjCnstr { last_name := \"test\" }\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"ObjCnstr violates exclusivity constraint\",\n        ):\n            await self.con.execute(\"\"\"\n                INSERT ObjCnstr {\n                    last_name := \"test\"\n                }\n            \"\"\")\n\n    async def test_constraints_ddl_function(self):\n        await self.con.execute('''\\\n            CREATE FUNCTION comp_func(s: str) -> str {\n                USING (\n                    SELECT str_lower(s)\n                );\n                SET volatility := 'Immutable';\n            };\n\n            CREATE TYPE CompPropFunction {\n                CREATE PROPERTY title -> str {\n                    CREATE CONSTRAINT exclusive ON\n                        (comp_func(__subject__));\n                };\n                CREATE PROPERTY comp_prop := comp_func(.title);\n            };\n        ''')\n\n    async def test_constraints_ddl_error_02(self):\n        # testing that subjectexpr cannot be overridden after it is\n        # specified explicitly\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidConstraintDefinitionError,\n            r\"subjectexpr is already defined for .+max_int\",\n        ):\n            await self.con.execute(r\"\"\"\n                CREATE ABSTRACT CONSTRAINT max_int(m: std::int64)\n                    ON (<int64>__subject__)\n                {\n                    SET errmessage :=\n                    # XXX: once simple string concat is possible here\n                    #      formatting can be saner\n                    '{__subject__} must be no longer than {m} characters.';\n                    USING (__subject__ <= m);\n                };\n\n                CREATE TYPE InvalidConstraintTest2 {\n                    CREATE PROPERTY foo -> std::str {\n                        CREATE CONSTRAINT max_int(3)\n                            ON (len(__subject__));\n                    };\n                };\n            \"\"\")\n\n    async def test_constraints_ddl_error_05(self):\n        # Test that constraint expression returns a boolean.\n        schema = \"\"\"\n            type User {\n                required property login -> str {\n                    constraint expression on (len(__subject__))\n                }\n            };\n        \"\"\"\n\n        async with self.assertRaisesRegexTx(\n            edgedb.SchemaDefinitionError,\n            \"constraint expression expected to return a bool value, \"\n            \"got scalar type 'std::int64'\",\n        ):\n            await self.migrate(schema)\n\n        qry = \"\"\"\n            CREATE TYPE User {\n                CREATE REQUIRED PROPERTY login -> str {\n                    CREATE CONSTRAINT expression on (len(__subject__));\n                };\n            };\n        \"\"\"\n\n        async with self.assertRaisesRegexTx(\n            edgedb.SchemaDefinitionError,\n            \"constraint expression expected to return a bool value, \"\n            \"got scalar type 'std::int64'\",\n        ):\n            await self.con.execute(qry)\n\n        qry = \"\"\"\n            CREATE ABSTRACT CONSTRAINT foo {\n                USING (__subject__);\n            };\n\n            CREATE TYPE User {\n                CREATE REQUIRED PROPERTY login -> str {\n                    CREATE CONSTRAINT foo;\n                };\n            };\n        \"\"\"\n\n        async with self.assertRaisesRegexTx(\n            edgedb.SchemaDefinitionError,\n            \"constraint expression expected to return a bool value, \"\n            \"got scalar type 'std::str'\",\n        ):\n            await self.con.execute(qry)\n\n        qry = \"\"\"\n            CREATE SCALAR TYPE wrong extending str {\n                CREATE CONSTRAINT exclusive;\n            };\n        \"\"\"\n\n        async with self.assertRaisesRegexTx(\n            edgedb.SchemaDefinitionError,\n            \"abstract constraint 'std::exclusive' may not \"\n            \"be used on scalar types\",\n        ):\n            await self.con.execute(qry)\n\n    async def test_constraints_ddl_error_06(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidConstraintDefinitionError,\n            r'dollar-prefixed.*cannot be used',\n        ):\n            await self.con.execute(r\"\"\"\n                CREATE ABSTRACT CONSTRAINT\n                mymax_er_06(max: std::int64) ON (len(__subject__))\n                {\n                    USING (__subject__ <= $max);\n                };\n\n                CREATE TYPE ConstraintOnTest_err_06 {\n                    CREATE PROPERTY foo -> std::str {\n                        CREATE CONSTRAINT mymax_er_06(3);\n                    };\n                };\n            \"\"\")\n\n    async def test_constraints_ddl_error_07(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.UnsupportedFeatureError,\n            r'Constant sets not allowed in singleton mode'\n        ):\n            await self.con.execute(r\"\"\"\n                CREATE TYPE ConstraintOnTest_err_07 {\n                    CREATE PROPERTY less_than_three -> std::int64 {\n                        CREATE CONSTRAINT std::one_of({1,2,3});\n                    };\n                };\n            \"\"\")\n\n    async def test_constraints_tuple(self):\n        await self.con.execute(r\"\"\"\n            CREATE TYPE Transaction {\n                CREATE PROPERTY credit\n                    -> tuple<nest: tuple<amount: decimal, currency: str>> {\n                    CREATE CONSTRAINT max_value(0)\n                        ON (__subject__.nest.amount)\n                };\n            };\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            INSERT Transaction {\n                credit := (nest := (amount := -1, currency := \"usd\")) };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"Maximum allowed value for credit is 0.\",\n        ):\n            await self.con.execute(r\"\"\"\n                INSERT Transaction {\n                    credit := (nest := (amount := 1, currency := \"usd\")) };\n            \"\"\")\n\n    async def test_constraints_partial_path(self):\n        await self.con.execute('''\\\n            CREATE TYPE Vector {\n                CREATE PROPERTY x -> float64;\n                CREATE PROPERTY y -> float64;\n                CREATE CONSTRAINT expression ON (\n                    .x^2 + .y^2 < 25\n                );\n            };\n        ''')\n\n        await self.con.execute(r\"\"\"\n            INSERT Vector { x := 3, y := 3 };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'invalid Vector',\n        ):\n            await self.con.execute(r\"\"\"\n                INSERT Vector { x := 4, y := 4 };\n            \"\"\")\n\n    async def test_constraints_exclusive_link_prop_01(self):\n        await self.con.execute(\"\"\"\n            CREATE TYPE Tgt;\n            CREATE TYPE Obj {\n                CREATE LINK asdf -> Tgt {\n                    CREATE PROPERTY what -> str;\n                    CREATE CONSTRAINT exclusive ON (\n                        __subject__@what ?? '??'\n                    )\n               }\n            };\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            INSERT Tgt;\n            INSERT Obj { asdf := assert_single((SELECT Tgt)) };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"asdf violates exclusivity constraint\",\n        ):\n            await self.con.execute(\"\"\"\n                INSERT Obj { asdf := assert_single((SELECT Tgt)) };\n            \"\"\")\n\n    async def test_constraints_exclusive_link_prop_02(self):\n        await self.con.execute(\"\"\"\n            CREATE TYPE Tgt;\n            CREATE TYPE Obj {\n                CREATE LINK asdf -> Tgt {\n                    CREATE PROPERTY what -> str {\n                        CREATE CONSTRAINT exclusive ON (__subject__ ?? '??')\n                    }\n                }\n            };\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            INSERT Tgt;\n            INSERT Obj { asdf := assert_single((SELECT Tgt)) };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"what violates exclusivity constraint\",\n        ):\n            await self.con.execute(\"\"\"\n                INSERT Obj { asdf := assert_single((SELECT Tgt)) };\n            \"\"\")\n\n    async def test_constraints_exclusive_link_prop_03(self):\n        await self.con.execute(\"\"\"\n            CREATE TYPE Tgt;\n            CREATE ABSTRACT LINK asdf {\n                CREATE PROPERTY what -> str {\n                    CREATE CONSTRAINT exclusive ON (__subject__ ?? '??')\n                }\n            };\n            CREATE TYPE Obj {\n                CREATE LINK asdf extending asdf -> Tgt;\n            };\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            INSERT Tgt;\n            INSERT Obj { asdf := assert_single((SELECT Tgt)) };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"what violates exclusivity constraint\",\n        ):\n            await self.con.execute(\"\"\"\n                INSERT Obj { asdf := assert_single((SELECT Tgt)) };\n            \"\"\")\n\n    async def test_constraints_non_strict_01(self):\n        # Test constraints that use a function that is implemented\n        # \"non-strictly\" (and so requires some special handling in the\n        # compiler)\n        await self.con.execute(\"\"\"\n            create type X {\n                create property a -> array<str>;\n                create property b -> array<str>;\n                create constraint expression on (\n                    .a ++ .b != [\"foo\", \"bar\", \"baz\"]);\n            };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"invalid X\",\n        ):\n            await self.con.execute(\"\"\"\n                insert X { a := ['foo'], b := ['bar', 'baz'] };\n            \"\"\")\n\n        # These should succeed, though, because the LHS is just {}\n        await self.con.execute(\"\"\"\n            insert X { a := {}, b := ['foo', 'bar', 'baz'] };\n        \"\"\")\n        await self.con.execute(\"\"\"\n            insert X { a := ['foo', 'bar', 'baz'], b := {} };\n        \"\"\")\n\n    async def test_constraints_bad_args(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.SchemaDefinitionError, \"Expected 0 arguments, but found 1\"\n        ):\n            await self.con.execute(\n                \"\"\"\n                create type X {\n                    create property a -> bool;\n                    create link parent -> X {\n                        create constraint expression (false);\n                    }\n                };\n            \"\"\"\n            )\n\n    async def test_constraints_no_refs(self):\n        await self.con.execute(\n            \"\"\"\n            create type X {\n                create property name -> str {\n                    create constraint std::expression on (true);\n                };\n            };\n        \"\"\"\n        )\n\n    async def test_constraints_abstract_scalar(self):\n        await self.con.execute(\n            \"\"\"\n            create abstract scalar type posint64 extending int64 {\n                create constraint min_value(0);\n            };\n\n            create scalar type limited_int64 extending posint64;\n\n            create type X {\n                create property y -> limited_int64;\n            };\n            \"\"\"\n        )\n        await self.con.execute(\"insert X { y := 1 }\")\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError, \"Minimum allowed value for\"\n        ):\n            await self.con.execute(\"insert X { y := -1 }\")\n\n    async def test_constraints_abstract_object_01(self):\n        await self.con.execute(\n            \"\"\"\n                create abstract type ChatBase {\n                    create multi property messages: str {\n                        create constraint exclusive;\n                    };\n                };\n\n                create type Dialog extending ChatBase;\n                create type Monolog extending ChatBase;\n                insert Dialog;\n                insert Monolog;\n            \"\"\"\n        )\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"messages violates exclusivity constraint\"\n        ):\n            await self.con.execute(\"\"\"\n                update ChatBase set { messages += 'hello world' };\n            \"\"\")\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"messages violates exclusivity constraint\"\n        ):\n            await self.con.execute(\"\"\"\n                analyze\n                update ChatBase set { messages += 'hello world' };\n            \"\"\")\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"messages violates exclusivity constraint\"\n        ):\n            await self.con.execute(\"\"\"\n                update ChatBase set { messages := 'hello world' };\n            \"\"\")\n\n    async def test_constraints_abstract_object_02(self):\n        await self.con.execute(\n            \"\"\"\n                create abstract type ChatBase {\n                    create single property messages: str {\n                        create constraint exclusive;\n                    };\n                };\n\n                create type Dialog extending ChatBase;\n                create type Monolog extending ChatBase;\n                insert Dialog;\n                insert Monolog;\n            \"\"\"\n        )\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"messages violates exclusivity constraint\"\n        ):\n            await self.con.execute(\"\"\"\n                update ChatBase set { messages := 'hello world' };\n            \"\"\")\n\n    async def test_constraints_abstract_object_03(self):\n        # Add one where the constraint comes from a different type\n        # than the update\n        await self.con.execute(\n            \"\"\"\n                create abstract type ChatBase {\n                    create single property messages: str {\n                    };\n                };\n                create abstract type ChatBase2 {\n                    create single property messages: str {\n                        create constraint exclusive;\n                    };\n                };\n\n                create type Dialog extending ChatBase, ChatBase2;\n                create type Monolog extending ChatBase, ChatBase2;\n                insert Dialog;\n                insert Monolog;\n            \"\"\"\n        )\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"messages violates exclusivity constraint\"\n        ):\n            await self.con.execute(\"\"\"\n                update ChatBase set { messages := 'hello world' };\n            \"\"\")\n\n    async def test_constraints_singleton_set_ops_01(self):\n        await self.con.execute(\n            \"\"\"\n            create type X {\n                create property a -> int64 {\n                    create constraint expression on (\n                        __subject__ in {1}\n                    );\n                }\n            };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.UnsupportedFeatureError,\n            \"cannot use SET OF operator 'std::IN' \"\n            \"in a constraint\"\n        ):\n            await self.con.execute(\n                \"\"\"\n                create type Y {\n                    create multi property a -> int64 {\n                        create constraint expression on (\n                            __subject__ in {1}\n                        );\n                    }\n                };\n            \"\"\")\n\n    async def test_constraints_singleton_set_ops_02(self):\n        await self.con.execute(\n            \"\"\"\n            create type X {\n                create property a -> int64 {\n                    create constraint expression on (\n                        __subject__ not in {1}\n                    );\n                }\n            };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.UnsupportedFeatureError,\n            \"cannot use SET OF operator 'std::NOT IN' \"\n            \"in a constraint\"\n        ):\n            await self.con.execute(\n                \"\"\"\n                create type Y {\n                    create multi property a -> int64 {\n                        create constraint expression on (\n                            __subject__ not in {1}\n                        );\n                    }\n                };\n            \"\"\")\n\n    async def test_constraints_singleton_set_ops_03(self):\n        await self.con.execute(\n            \"\"\"\n            create type X {\n                create property a -> int64 {\n                    create constraint expression on (\n                        exists(__subject__)\n                    );\n                }\n            };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.UnsupportedFeatureError,\n            \"cannot use SET OF operator 'std::EXISTS' \"\n            \"in a constraint\"\n        ):\n            await self.con.execute(\n                \"\"\"\n                create type Y {\n                    create multi property a -> int64 {\n                        create constraint expression on (\n                            exists(__subject__)\n                        );\n                    }\n                };\n            \"\"\")\n\n    async def test_constraints_singleton_set_ops_04(self):\n        await self.con.execute(\n            \"\"\"\n            create type X {\n                create property a -> int64 {\n                    create constraint expression on (\n                        __subject__ ?? 1 = 0\n                    );\n                }\n            };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.UnsupportedFeatureError,\n            r\"cannot use SET OF operator 'std::\\?\\?' \"\n            r\"in a constraint\"\n        ):\n            await self.con.execute(\n                \"\"\"\n                create type Y {\n                    create multi property a -> int64 {\n                        create constraint expression on (\n                            __subject__ ?? 1 = 0\n                        );\n                    }\n                };\n            \"\"\")\n\n    async def test_constraints_singleton_set_ops_05(self):\n        await self.con.execute(\n            \"\"\"\n            create type X {\n                create property a -> tuple<bool, int64> {\n                    create constraint expression on (\n                        __subject__.1 < 0\n                        if __subject__.0 else\n                        __subject__.1 >= 0\n                    );\n                }\n            };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.UnsupportedFeatureError,\n            \"cannot use SET OF operator 'std::IF' \"\n            \"in a constraint\"\n        ):\n            await self.con.execute(\n                \"\"\"\n                create type Y {\n                    create multi property a -> tuple<bool, int64> {\n                        create constraint expression on (\n                            __subject__.1 < 0\n                            if __subject__.0 else\n                            __subject__.1 >= 0\n                        );\n                    }\n                };\n            \"\"\")\n\n\nclass TestConstraintsInheritance(tb.DDLTestCase):\n\n    async def _check_constraint_inheritance(\n        self,\n        constraint_groups: list[list[tuple[str, ...]]]\n    ) -> None:\n        all_constrained_entries = list(set(\n            entry\n            for group in constraint_groups\n            for entry in group\n        ))\n\n        relatives: dict[tuple[str, ...], set[str]] = {\n            entry: set()\n            for entry in all_constrained_entries\n        }\n        for group in constraint_groups:\n            for entry in group:\n                relatives[entry] = relatives[entry].union(\n                    other[0] for other in group\n                )\n\n        for entry in all_constrained_entries:\n            for other in all_constrained_entries:\n                if entry[0] == other[0]:\n                    continue\n\n                name = entry[0]\n                other_name = other[0]\n\n                if len(entry) == 2:\n                    prop = entry[1]\n\n                    if other_name in relatives[entry]:\n                        async with self.assertRaisesRegexTx(\n                            edgedb.ConstraintViolationError,\n                            f\"violates exclusivity constraint\"\n                        ):\n                            await self.con.execute(\n                                f\"insert {name} {{\"\n                                f\"    {prop} := '{other_name.lower()}'\"\n                                f\"}};\"\n                            )\n                    else:\n                        await self.con.execute(\n                            f\"insert {name} {{\"\n                            f\"    {prop} := '{other_name.lower()}'\"\n                            f\"}};\"\n                        )\n                        await self.con.execute(\n                            f\"delete {name} filter .{prop} = (\"\n                            f\"    '{other_name.lower()}'\"\n                            f\");\"\n                        )\n\n                elif len(entry) == 4:\n                    link = entry[1]\n                    link_type = entry[2]\n                    link_prop = entry[3]\n\n                    if other_name in relatives[entry]:\n                        async with self.assertRaisesRegexTx(\n                            edgedb.ConstraintViolationError,\n                            f\"violates exclusivity constraint\"\n                        ):\n                            await self.con.execute(\n                                f\"insert {name} {{\"\n                                f\"    {link} := (insert {link_type}) {{\"\n                                f\"        @{link_prop} := (\"\n                                f\"            '{other_name.lower()}'\"\n                                f\"        )\"\n                                f\"    }}\"\n                                f\"}};\"\n                            )\n                    else:\n                        await self.con.execute(\n                            f\"insert {name} {{\"\n                            f\"    {link} := (insert {link_type}) {{\"\n                            f\"        @{link_prop} := (\"\n                            f\"            '{other_name.lower()}'\"\n                            f\"        )\"\n                            f\"    }}\"\n                            f\"}};\"\n                        )\n                        await self.con.execute(\n                            f\"delete {name} \"\n                            f\"filter .{link}@{link_prop} = (\"\n                            f\"    '{other_name.lower()}'\"\n                            f\");\"\n                        )\n\n                else:\n                    raise NotImplementedError()\n\n    async def _apply_schema_inheritance_single_object(self):\n        # - single inheritance\n        #   - type constraint\n        await self.con.execute(\"\"\"\n            create type AAA {\n                create required property name -> str;\n                create constraint exclusive on (.name);\n            };\n            create type BBB extending AAA;\n            create type XXX extending AAA;\n            insert AAA {name := 'aaa'};\n            insert BBB {name := 'bbb'};\n            insert XXX {name := 'xxx'};\n        \"\"\")\n\n    async def test_constraints_inheritance_single_object_01(self):\n        await self._apply_schema_inheritance_single_object()\n\n        # Add descendant\n        await self.con.execute(\"\"\"\n            create type CCC extending XXX;\n            insert CCC {name := 'ccc'};\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [('AAA', 'name'), ('BBB', 'name'), ('XXX', 'name'), ('CCC', 'name')]\n        ])\n\n    async def test_constraints_inheritance_single_object_02(self):\n        await self._apply_schema_inheritance_single_object()\n\n        # Add base\n        await self.con.execute(\"\"\"\n            create type CCC {\n                create required property name -> str;\n                create constraint exclusive on (.name);\n            };\n            create type DDD extending CCC;\n            insert CCC {name := 'ccc'};\n            insert DDD {name := 'ddd'};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter type XXX {\n                extending CCC;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [('AAA', 'name'), ('BBB', 'name'), ('XXX', 'name')],\n            [('CCC', 'name'), ('DDD', 'name'), ('XXX', 'name')],\n        ])\n\n    async def test_constraints_inheritance_single_object_03(self):\n        await self._apply_schema_inheritance_single_object()\n\n        # Change base\n        await self.con.execute(\"\"\"\n            create type CCC {\n                create required property name -> str;\n                create constraint exclusive on (.name);\n            };\n            create type DDD extending CCC;\n            insert CCC {name := 'ccc'};\n            insert DDD {name := 'ddd'};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter type XXX {\n                drop extending AAA;\n                extending CCC;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [('AAA', 'name'), ('BBB', 'name')],\n            [('CCC', 'name'), ('DDD', 'name'), ('XXX', 'name')],\n        ])\n\n    async def test_constraints_inheritance_single_object_04(self):\n        await self._apply_schema_inheritance_single_object()\n\n        # Remove base\n        await self.con.execute(\"\"\"\n            delete XXX;\n            alter type XXX { drop extending AAA; };\n            alter type XXX { create required property name -> str; };\n            insert XXX {name := 'xxx'};\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [('AAA', 'name'), ('BBB', 'name')],\n            [('XXX', 'name')],\n        ])\n\n    async def _apply_schema_inheritance_mutli_object(self):\n        # - multiple inheritance\n        #   - abstract type constraint\n        #   - type constraint\n        await self.con.execute(\"\"\"\n            create abstract type AAA {\n                create required property name -> str;\n                create constraint exclusive on (.name);\n            };\n            create type BBB extending AAA;\n            create type CCC {\n                create required property name -> str;\n                create constraint exclusive on (.name);\n            };\n            create type DDD extending CCC;\n            create type XXX extending AAA, CCC;\n            insert BBB {name := 'bbb'};\n            insert CCC {name := 'ccc'};\n            insert DDD {name := 'ddd'};\n            insert XXX {name := 'xxx'};\n        \"\"\")\n\n    async def test_constraints_inheritance_multi_object_01(self):\n        await self._apply_schema_inheritance_mutli_object()\n\n        # Add descendant\n        await self.con.execute(\"\"\"\n            create type EEE extending XXX;\n            insert EEE {name := 'eee'};\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('BBB', 'name'),\n                ('XXX', 'name'),\n                ('EEE', 'name'),\n            ],\n            [\n                ('CCC', 'name'),\n                ('DDD', 'name'),\n                ('XXX', 'name'),\n                ('EEE', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_multi_object_02(self):\n        await self._apply_schema_inheritance_mutli_object()\n\n        # Add base\n        await self.con.execute(\"\"\"\n            create type EEE {\n                create required property name -> str;\n                create constraint exclusive on (.name);\n            };\n            create type FFF extending EEE;\n            insert EEE {name := 'eee'};\n            insert FFF {name := 'fff'};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter type XXX {\n                extending EEE;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [('BBB', 'name'), ('XXX', 'name')],\n            [('CCC', 'name'), ('DDD', 'name'), ('XXX', 'name')],\n            [('EEE', 'name'), ('FFF', 'name'), ('XXX', 'name')],\n        ])\n\n    async def test_constraints_inheritance_multi_object_03(self):\n        await self._apply_schema_inheritance_mutli_object()\n\n        # Change base\n        await self.con.execute(\"\"\"\n            create type EEE {\n                create required property name -> str;\n                create constraint exclusive on (.name);\n            };\n            create type FFF extending EEE;\n            insert EEE {name := 'eee'};\n            insert FFF {name := 'fff'};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter type XXX {\n                drop extending AAA;\n                extending EEE;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [('BBB', 'name')],\n            [('CCC', 'name'), ('DDD', 'name'), ('XXX', 'name')],\n            [('EEE', 'name'), ('FFF', 'name'), ('XXX', 'name')],\n        ])\n\n    async def test_constraints_inheritance_multi_object_04(self):\n        await self._apply_schema_inheritance_mutli_object()\n\n        # Remove base\n        await self.con.execute(\"\"\"\n            alter type XXX {\n                drop extending AAA;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [('BBB', 'name')],\n            [('CCC', 'name'), ('DDD', 'name'), ('XXX', 'name')],\n        ])\n\n    async def _apply_schema_inheritance_single_pointer(self):\n        # - single inheritance\n        #   - property\n        await self.con.execute(\"\"\"\n            create type AAA {\n                create required property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            create type BBB extending AAA;\n            create type XXX extending AAA;\n            insert AAA {name := 'aaa'};\n            insert BBB {name := 'bbb'};\n            insert XXX {name := 'xxx'};\n        \"\"\")\n\n    async def test_constraints_inheritance_single_pointer_01(self):\n        await self._apply_schema_inheritance_single_pointer()\n\n        # Add descendant\n        await self.con.execute(\"\"\"\n            create type CCC extending XXX;\n            insert CCC {name := 'ccc'};\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'name'),\n                ('BBB', 'name'),\n                ('XXX', 'name'),\n                ('CCC', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_single_pointer_02(self):\n        await self._apply_schema_inheritance_single_pointer()\n\n        # Add base\n        await self.con.execute(\"\"\"\n            create type CCC {\n                create required property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            create type DDD extending CCC;\n            insert CCC {name := 'ccc'};\n            insert DDD {name := 'ddd'};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter type XXX {\n                extending CCC;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'name'),\n                ('BBB', 'name'),\n                ('XXX', 'name'),\n            ],\n            [\n                ('CCC', 'name'),\n                ('DDD', 'name'),\n                ('XXX', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_single_pointer_03(self):\n        await self._apply_schema_inheritance_single_pointer()\n\n        # Change base\n        await self.con.execute(\"\"\"\n            create type CCC {\n                create required property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            create type DDD extending CCC;\n            insert CCC {name := 'ccc'};\n            insert DDD {name := 'ddd'};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter type XXX {\n                drop extending AAA;\n                extending CCC;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'name'),\n                ('BBB', 'name'),\n            ],\n            [\n                ('CCC', 'name'),\n                ('DDD', 'name'),\n                ('XXX', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_single_pointer_04(self):\n        await self._apply_schema_inheritance_single_pointer()\n\n        # Remove base\n        await self.con.execute(\"\"\"\n            delete XXX;\n            alter type XXX { drop extending AAA; };\n            alter type XXX {\n                create required property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            insert XXX {name := 'xxx'};\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'name'),\n                ('BBB', 'name'),\n            ],\n            [\n                ('XXX', 'name'),\n            ],\n        ])\n\n    async def _apply_schema_inheritance_multi_pointer(self):\n        # - multiple inheritance\n        #   - pointer constraint\n        #   - pointer constraint\n        await self.con.execute(\"\"\"\n            create type AAA {\n                create required property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            create type BBB extending AAA;\n            create type CCC {\n                create required property name -> str\n                {\n                    create constraint exclusive;\n                };\n            };\n            create type DDD extending CCC;\n            create type XXX extending AAA, CCC;\n            insert AAA {name := 'aaa'};\n            insert BBB {name := 'bbb'};\n            insert CCC {name := 'ccc'};\n            insert DDD {name := 'ddd'};\n            insert XXX {name := 'xxx'};\n        \"\"\")\n\n    async def test_constraints_inheritance_multi_pointer_01(self):\n        await self._apply_schema_inheritance_multi_pointer()\n\n        # Add descendant\n        await self.con.execute(\"\"\"\n            create type EEE extending XXX;\n            insert EEE {name := 'eee'};\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'name'),\n                ('BBB', 'name'),\n                ('XXX', 'name'),\n                ('EEE', 'name'),\n            ],\n            [\n                ('CCC', 'name'),\n                ('DDD', 'name'),\n                ('XXX', 'name'),\n                ('EEE', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_multi_pointer_02(self):\n        await self._apply_schema_inheritance_multi_pointer()\n\n        # Add base\n        await self.con.execute(\"\"\"\n            create type EEE {\n                create required property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            create type FFF extending EEE;\n            insert EEE {name := 'eee'};\n            insert FFF {name := 'fff'};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter type XXX {\n                extending EEE;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [('AAA', 'name'), ('BBB', 'name'), ('XXX', 'name')],\n            [('CCC', 'name'), ('DDD', 'name'), ('XXX', 'name')],\n            [('EEE', 'name'), ('FFF', 'name'), ('XXX', 'name')],\n        ])\n\n    async def test_constraints_inheritance_multi_pointer_03(self):\n        await self._apply_schema_inheritance_multi_pointer()\n\n        # Change base\n        await self.con.execute(\"\"\"\n            create type EEE {\n                create required property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            create type FFF extending EEE;\n            insert EEE {name := 'eee'};\n            insert FFF {name := 'fff'};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter type XXX {\n                drop extending AAA;\n                extending EEE;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [('AAA', 'name'), ('BBB', 'name')],\n            [('CCC', 'name'), ('DDD', 'name'), ('XXX', 'name')],\n            [('EEE', 'name'), ('FFF', 'name'), ('XXX', 'name')],\n        ])\n\n    async def test_constraints_inheritance_multi_pointer_04(self):\n        await self._apply_schema_inheritance_multi_pointer()\n\n        # Remove base\n        await self.con.execute(\"\"\"\n            alter type XXX {\n                drop extending AAA;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [('AAA', 'name'), ('BBB', 'name')],\n            [('CCC', 'name'), ('DDD', 'name'), ('XXX', 'name')],\n        ])\n\n    async def _apply_schema_inheritance_single_abstract_link(self):\n        # - single inheritance\n        #   - abstract link constraint\n        await self.con.execute(\"\"\"\n            create type Tag;\n            create abstract link PPP {\n                create property name -> str;\n                create constraint exclusive on (@name);\n            };\n            create abstract link QQQ extending PPP;\n            create abstract link XXX extending PPP;\n            create type AAA {\n                create required link tag extending PPP -> Tag;\n            };\n            create type BBB {\n                create required link tag extending QQQ -> Tag;\n            };\n            create type YYY {\n                create required link tag extending XXX -> Tag;\n            };\n            insert AAA {tag := (insert Tag){@name := 'aaa'}};\n            insert BBB {tag := (insert Tag){@name := 'bbb'}};\n            insert YYY {tag := (insert Tag){@name := 'yyy'}};\n        \"\"\")\n\n    async def test_constraints_inheritance_single_abstract_link_01(self):\n        await self._apply_schema_inheritance_single_abstract_link()\n\n        # Add descendant\n        await self.con.execute(\"\"\"\n            create abstract link RRR extending XXX;\n            create type CCC {\n                create required link tag extending RRR -> Tag;\n            };\n            insert CCC {tag := (insert Tag){@name := 'ccc'}};\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n                ('CCC', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_single_abstract_link_02(self):\n        await self._apply_schema_inheritance_single_abstract_link()\n\n        # Add base\n        await self.con.execute(\"\"\"\n            create abstract link RRR {\n                create property name -> str;\n                create constraint exclusive on (@name);\n            };\n            create abstract link SSS extending RRR;\n            create type CCC {\n                create required link tag extending RRR -> Tag;\n            };\n            create type DDD {\n                create required link tag extending SSS -> Tag;\n            };\n            insert CCC {tag := (insert Tag){@name := 'ccc'}};\n            insert DDD {tag := (insert Tag){@name := 'ddd'}};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter abstract link XXX {\n                extending RRR;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('CCC', 'tag', 'Tag', 'name'),\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_single_abstract_link_03(self):\n        await self._apply_schema_inheritance_single_abstract_link()\n\n        # Change base\n        await self.con.execute(\"\"\"\n            create abstract link RRR {\n                create property name -> str;\n                create constraint exclusive on (@name);\n            };\n            create abstract link SSS extending RRR;\n            create type CCC {\n                create required link tag extending RRR -> Tag;\n            };\n            create type DDD {\n                create required link tag extending SSS -> Tag;\n            };\n            insert CCC {tag := (insert Tag){@name := 'ccc'}};\n            insert DDD {tag := (insert Tag){@name := 'ddd'}};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter abstract link XXX {\n                extending RRR;\n                drop extending PPP;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('CCC', 'tag', 'Tag', 'name'),\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_single_abstract_link_04(self):\n        await self._apply_schema_inheritance_single_abstract_link()\n\n        # Remove base\n        await self.con.execute(\"\"\"\n            delete YYY;\n            alter abstract link XXX { drop extending PPP; };\n            alter abstract link XXX {\n                create property name -> str;\n                create constraint exclusive on (@name);\n            };\n            insert YYY {tag := (insert Tag){@name := 'yyy'}};\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def _apply_schema_inheritance_multi_abstract_link(self):\n        # - multiple inheritance\n        #   - abstract link constraint\n        #   - abstract link constraint\n        await self.con.execute(\"\"\"\n            create type Tag;\n            create abstract link PPP {\n                create property name -> str;\n                create constraint exclusive on (@name);\n            };\n            create abstract link QQQ extending PPP;\n            create abstract link RRR {\n                create property name -> str;\n                create constraint exclusive on (@name);\n            };\n            create abstract link SSS extending RRR;\n            create abstract link XXX extending PPP, RRR;\n            create type AAA {\n                create required link tag extending PPP -> Tag;\n            };\n            create type BBB {\n                create required link tag extending QQQ -> Tag;\n            };\n            create type CCC {\n                create required link tag extending RRR -> Tag;\n            };\n            create type DDD {\n                create required link tag extending SSS -> Tag;\n            };\n            create type YYY {\n                create required link tag extending XXX -> Tag;\n            };\n            insert AAA {tag := (insert Tag){@name := 'aaa'}};\n            insert BBB {tag := (insert Tag){@name := 'bbb'}};\n            insert CCC {tag := (insert Tag){@name := 'ccc'}};\n            insert DDD {tag := (insert Tag){@name := 'ddd'}};\n            insert YYY {tag := (insert Tag){@name := 'yyy'}};\n        \"\"\")\n\n    async def test_constraints_inheritance_multi_abstract_link_01(self):\n        await self._apply_schema_inheritance_multi_abstract_link()\n\n        # Add descendant\n        await self.con.execute(\"\"\"\n            create abstract link TTT extending XXX;\n            create type EEE {\n                create required link tag extending TTT -> Tag;\n            };\n            insert EEE {tag := (insert Tag){@name := 'eee'}};\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n                ('EEE', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('CCC', 'tag', 'Tag', 'name'),\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n                ('EEE', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_multi_abstract_link_02(self):\n        await self._apply_schema_inheritance_multi_abstract_link()\n\n        # Add base\n        await self.con.execute(\"\"\"\n            create abstract link TTT {\n                create property name -> str;\n                create constraint exclusive on (@name);\n            };\n            create abstract link UUU extending TTT;\n            create type EEE {\n                create required link tag extending TTT -> Tag;\n            };\n            create type FFF {\n                create required link tag extending UUU -> Tag;\n            };\n            insert EEE {tag := (insert Tag){@name := 'eee'}};\n            insert FFF {tag := (insert Tag){@name := 'fff'}};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter abstract link XXX {\n                extending TTT;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('CCC', 'tag', 'Tag', 'name'),\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('EEE', 'tag', 'Tag', 'name'),\n                ('FFF', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_multi_abstract_link_03(self):\n        await self._apply_schema_inheritance_multi_abstract_link()\n\n        # Change base\n        await self.con.execute(\"\"\"\n            create abstract link TTT {\n                create property name -> str;\n                create constraint exclusive on (@name);\n            };\n            create abstract link UUU extending TTT;\n            create type EEE {\n                create required link tag extending TTT -> Tag;\n            };\n            create type FFF {\n                create required link tag extending UUU -> Tag;\n            };\n            insert EEE {tag := (insert Tag){@name := 'eee'}};\n            insert FFF {tag := (insert Tag){@name := 'fff'}};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter abstract link XXX {\n                extending TTT;\n                drop extending PPP;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('CCC', 'tag', 'Tag', 'name'),\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('EEE', 'tag', 'Tag', 'name'),\n                ('FFF', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_multi_abstract_link_04(self):\n        await self._apply_schema_inheritance_multi_abstract_link()\n\n        # Remove base\n        await self.con.execute(\"\"\"\n            alter abstract link XXX {\n                drop extending PPP;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('CCC', 'tag', 'Tag', 'name'),\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def _apply_schema_inheritance_multi_mixed_link(self):\n        # - multiple inheritance\n        #   - abstract link constraint\n        #   - abstract type link constraint\n        await self.con.execute(\"\"\"\n            create type Tag;\n            create type Tag2;\n            create abstract link PPP {\n                create property name -> str;\n                create constraint exclusive on (@name);\n            };\n            create abstract link QQQ extending PPP;\n            create type AAA {\n                create required link tag extending PPP -> Tag;\n            };\n            create type BBB {\n                create required link tag2 extending QQQ -> Tag2;\n            };\n            create abstract type CCC {\n                create required link tag -> Tag {\n                    create property name -> str;\n                    create constraint exclusive on (@name);\n                };\n            };\n            create type DDD extending CCC;\n            create type XXX extending AAA, CCC;\n            insert AAA {tag := (insert Tag){@name := 'aaa'}};\n            insert BBB {tag2 := (insert Tag2){@name := 'bbb'}};\n            insert DDD {tag := (insert Tag){@name := 'ddd'}};\n            insert XXX {tag := (insert Tag){@name := 'xxx'}};\n        \"\"\")\n\n    async def test_constraints_inheritance_multi_mixed_link_01(self):\n        await self._apply_schema_inheritance_multi_mixed_link()\n\n        # Add descendant\n        await self.con.execute(\"\"\"\n            create type EEE extending XXX;\n            insert EEE {tag := (insert Tag){@name := 'eee'}};\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag2', 'Tag2', 'name'),\n                ('XXX', 'tag', 'Tag', 'name'),\n                ('EEE', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('XXX', 'tag', 'Tag', 'name'),\n                ('EEE', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_multi_mixed_link_02(self):\n        await self._apply_schema_inheritance_multi_mixed_link()\n\n        # Add base\n        await self.con.execute(\"\"\"\n            create abstract link RRR {\n                create property name -> str;\n                create constraint exclusive on (@name);\n            };\n            create abstract link SSS extending RRR;\n            create type EEE {\n                create required link tag extending RRR -> Tag;\n            };\n            create type FFF {\n                create required link tag2 extending SSS -> Tag2;\n            };\n            insert EEE {tag := (insert Tag){@name := 'eee'}};\n            insert FFF {tag2 := (insert Tag2){@name := 'fff'}};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter type XXX {\n                extending EEE;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag2', 'Tag2', 'name'),\n                ('XXX', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('XXX', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('EEE', 'tag', 'Tag', 'name'),\n                ('FFF', 'tag2', 'Tag2', 'name'),\n                ('XXX', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_multi_mixed_link_03(self):\n        await self._apply_schema_inheritance_multi_mixed_link()\n\n        # Change base\n        await self.con.execute(\"\"\"\n            create abstract link TTT {\n                create property name -> str;\n                create constraint exclusive on (@name);\n            };\n            create abstract link UUU extending TTT;\n            create type EEE {\n                create required link tag extending TTT -> Tag;\n            };\n            create type FFF {\n                create required link tag2 extending UUU -> Tag2;\n            };\n            insert EEE {tag := (insert Tag){@name := 'eee'}};\n            insert FFF {tag2 := (insert Tag2){@name := 'fff'}};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter type XXX {\n                extending EEE;\n                drop extending AAA;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag2', 'Tag2', 'name'),\n            ],\n            [\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('XXX', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('EEE', 'tag', 'Tag', 'name'),\n                ('FFF', 'tag2', 'Tag2', 'name'),\n                ('XXX', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_multi_mixed_link_04(self):\n        await self._apply_schema_inheritance_multi_mixed_link()\n\n        # Remove base\n        await self.con.execute(\"\"\"\n            alter type XXX {\n                drop extending AAA;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag2', 'Tag2', 'name'),\n            ],\n            [\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('XXX', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def _apply_schema_inheritance_single_abstract_link_prop(self):\n        # - single inheritance\n        #   - abstract link constraint\n        await self.con.execute(\"\"\"\n            create type Tag;\n            create abstract link PPP {\n                create property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            create abstract link QQQ extending PPP;\n            create abstract link XXX extending PPP;\n            create type AAA {\n                create required link tag extending PPP -> Tag;\n            };\n            create type BBB {\n                create required link tag extending QQQ -> Tag;\n            };\n            create type YYY {\n                create required link tag extending XXX -> Tag;\n            };\n            insert AAA {tag := (insert Tag){@name := 'aaa'}};\n            insert BBB {tag := (insert Tag){@name := 'bbb'}};\n            insert YYY {tag := (insert Tag){@name := 'yyy'}};\n        \"\"\")\n\n    async def test_constraints_inheritance_single_abstract_link_prop_01(self):\n        await self._apply_schema_inheritance_single_abstract_link_prop()\n\n        # Add descendant\n        await self.con.execute(\"\"\"\n            create abstract link RRR extending XXX;\n            create type CCC {\n                create required link tag extending RRR -> Tag;\n            };\n            insert CCC {tag := (insert Tag){@name := 'ccc'}};\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n                ('CCC', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_single_abstract_link_prop_02(self):\n        await self._apply_schema_inheritance_single_abstract_link_prop()\n\n        # Add base\n        await self.con.execute(\"\"\"\n            create abstract link RRR {\n                create property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            create abstract link SSS extending RRR;\n            create type CCC {\n                create required link tag extending RRR -> Tag;\n            };\n            create type DDD {\n                create required link tag extending SSS -> Tag;\n            };\n            insert CCC {tag := (insert Tag){@name := 'ccc'}};\n            insert DDD {tag := (insert Tag){@name := 'ddd'}};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter abstract link XXX {\n                extending RRR;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('CCC', 'tag', 'Tag', 'name'),\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_single_abstract_link_prop_03(self):\n        await self._apply_schema_inheritance_single_abstract_link_prop()\n\n        # Change base\n        await self.con.execute(\"\"\"\n            create abstract link RRR {\n                create property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            create abstract link SSS extending RRR;\n            create type CCC {\n                create required link tag extending RRR -> Tag;\n            };\n            create type DDD {\n                create required link tag extending SSS -> Tag;\n            };\n            insert CCC {tag := (insert Tag){@name := 'ccc'}};\n            insert DDD {tag := (insert Tag){@name := 'ddd'}};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter abstract link XXX {\n                extending RRR;\n                drop extending PPP;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('CCC', 'tag', 'Tag', 'name'),\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_single_abstract_link_prop_04(self):\n        await self._apply_schema_inheritance_single_abstract_link_prop()\n\n        # Remove base\n        await self.con.execute(\"\"\"\n            delete YYY;\n            alter abstract link XXX { drop extending PPP; };\n            alter abstract link XXX {\n                create property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            insert YYY {tag := (insert Tag){@name := 'yyy'}};\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def _apply_schema_inheritance_multi_abstract_link_prop(self):\n        # - multiple inheritance\n        #   - abstract link constraint\n        #   - abstract link constraint\n        await self.con.execute(\"\"\"\n            create type Tag;\n            create abstract link PPP {\n                create property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            create abstract link QQQ extending PPP;\n            create abstract link RRR {\n                create property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            create abstract link SSS extending RRR;\n            create abstract link XXX extending PPP, RRR;\n            create type AAA {\n                create required link tag extending PPP -> Tag;\n            };\n            create type BBB {\n                create required link tag extending QQQ -> Tag;\n            };\n            create type CCC {\n                create required link tag extending RRR -> Tag;\n            };\n            create type DDD {\n                create required link tag extending SSS -> Tag;\n            };\n            create type YYY {\n                create required link tag extending XXX -> Tag;\n            };\n            insert AAA {tag := (insert Tag){@name := 'aaa'}};\n            insert BBB {tag := (insert Tag){@name := 'bbb'}};\n            insert CCC {tag := (insert Tag){@name := 'ccc'}};\n            insert DDD {tag := (insert Tag){@name := 'ddd'}};\n            insert YYY {tag := (insert Tag){@name := 'yyy'}};\n        \"\"\")\n\n    async def test_constraints_inheritance_multi_abstract_link_prop_01(self):\n        await self._apply_schema_inheritance_multi_abstract_link_prop()\n\n        # Add descendant\n        await self.con.execute(\"\"\"\n            create abstract link TTT extending XXX;\n            create type EEE {\n                create required link tag extending TTT -> Tag;\n            };\n            insert EEE {tag := (insert Tag){@name := 'eee'}};\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n                ('EEE', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('CCC', 'tag', 'Tag', 'name'),\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n                ('EEE', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_multi_abstract_link_prop_02(self):\n        await self._apply_schema_inheritance_multi_abstract_link_prop()\n\n        # Add base\n        await self.con.execute(\"\"\"\n            create abstract link TTT {\n                create property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            create abstract link UUU extending TTT;\n            create type EEE {\n                create required link tag extending TTT -> Tag;\n            };\n            create type FFF {\n                create required link tag extending UUU -> Tag;\n            };\n            insert EEE {tag := (insert Tag){@name := 'eee'}};\n            insert FFF {tag := (insert Tag){@name := 'fff'}};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter abstract link XXX {\n                extending TTT;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('CCC', 'tag', 'Tag', 'name'),\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('EEE', 'tag', 'Tag', 'name'),\n                ('FFF', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_multi_abstract_link_prop_03(self):\n        await self._apply_schema_inheritance_multi_abstract_link_prop()\n\n        # Change base\n        await self.con.execute(\"\"\"\n            create abstract link TTT {\n                create property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            create abstract link UUU extending TTT;\n            create type EEE {\n                create required link tag extending TTT -> Tag;\n            };\n            create type FFF {\n                create required link tag extending UUU -> Tag;\n            };\n            insert EEE {tag := (insert Tag){@name := 'eee'}};\n            insert FFF {tag := (insert Tag){@name := 'fff'}};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter abstract link XXX {\n                extending TTT;\n                drop extending PPP;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('CCC', 'tag', 'Tag', 'name'),\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('EEE', 'tag', 'Tag', 'name'),\n                ('FFF', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_multi_abstract_link_prop_04(self):\n        await self._apply_schema_inheritance_multi_abstract_link_prop()\n\n        # Remove base\n        await self.con.execute(\"\"\"\n            alter abstract link XXX {\n                drop extending PPP;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('CCC', 'tag', 'Tag', 'name'),\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('YYY', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def _apply_schema_inheritance_multi_mixed_link_prop(self):\n        # - multiple inheritance\n        #   - abstract link property constraint\n        #   - abstract type link property constraint\n        await self.con.execute(\"\"\"\n            create type Tag;\n            create type Tag2;\n            create abstract link PPP {\n                create property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            create abstract link QQQ extending PPP;\n            create type AAA {\n                create required link tag extending PPP -> Tag;\n            };\n            create type BBB {\n                create required link tag2 extending QQQ -> Tag2;\n            };\n            create abstract type CCC {\n                create required link tag -> Tag {\n                    create property name -> str {\n                        create constraint exclusive;\n                    };\n                };\n            };\n            create type DDD extending CCC;\n            create type XXX extending AAA, CCC;\n            insert AAA {tag := (insert Tag){@name := 'aaa'}};\n            insert BBB {tag2 := (insert Tag2){@name := 'bbb'}};\n            insert DDD {tag := (insert Tag){@name := 'ddd'}};\n            insert XXX {tag := (insert Tag){@name := 'xxx'}};\n        \"\"\")\n\n    async def test_constraints_inheritance_multi_mixed_link_prop_01(self):\n        await self._apply_schema_inheritance_multi_mixed_link_prop()\n\n        # Add descendant\n        await self.con.execute(\"\"\"\n            create type EEE extending XXX;\n            insert EEE {tag := (insert Tag){@name := 'eee'}};\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag2', 'Tag2', 'name'),\n                ('XXX', 'tag', 'Tag', 'name'),\n                ('EEE', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('XXX', 'tag', 'Tag', 'name'),\n                ('EEE', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_multi_mixed_link_prop_02(self):\n        await self._apply_schema_inheritance_multi_mixed_link_prop()\n\n        # Add base\n        await self.con.execute(\"\"\"\n            create abstract link RRR {\n                create property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            create abstract link SSS extending RRR;\n            create type EEE {\n                create required link tag extending RRR -> Tag;\n            };\n            create type FFF {\n                create required link tag2 extending SSS -> Tag2;\n            };\n            insert EEE {tag := (insert Tag){@name := 'eee'}};\n            insert FFF {tag2 := (insert Tag2){@name := 'fff'}};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter type XXX {\n                extending EEE;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag2', 'Tag2', 'name'),\n                ('XXX', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('XXX', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('EEE', 'tag', 'Tag', 'name'),\n                ('FFF', 'tag2', 'Tag2', 'name'),\n                ('XXX', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_multi_mixed_link_prop_03(self):\n        await self._apply_schema_inheritance_multi_mixed_link_prop()\n\n        # Change base\n        await self.con.execute(\"\"\"\n            create abstract link TTT {\n                create property name -> str {\n                    create constraint exclusive;\n                };\n            };\n            create abstract link UUU extending TTT;\n            create type EEE {\n                create required link tag extending TTT -> Tag;\n            };\n            create type FFF {\n                create required link tag2 extending UUU -> Tag2;\n            };\n            insert EEE {tag := (insert Tag){@name := 'eee'}};\n            insert FFF {tag2 := (insert Tag2){@name := 'fff'}};\n        \"\"\")\n\n        await self.con.execute(\"\"\"\n            alter type XXX {\n                extending EEE;\n                drop extending AAA;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag2', 'Tag2', 'name'),\n            ],\n            [\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('XXX', 'tag', 'Tag', 'name'),\n            ],\n            [\n                ('EEE', 'tag', 'Tag', 'name'),\n                ('FFF', 'tag2', 'Tag2', 'name'),\n                ('XXX', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_multi_mixed_link_prop_04(self):\n        await self._apply_schema_inheritance_multi_mixed_link_prop()\n\n        # Remove base\n        await self.con.execute(\"\"\"\n            alter type XXX {\n                drop extending AAA;\n            }\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [\n                ('AAA', 'tag', 'Tag', 'name'),\n                ('BBB', 'tag2', 'Tag2', 'name'),\n            ],\n            [\n                ('DDD', 'tag', 'Tag', 'name'),\n                ('XXX', 'tag', 'Tag', 'name'),\n            ],\n        ])\n\n    async def test_constraints_inheritance_abstract_constraint_01(self):\n        # Abstract constraints do not share their exclusiveness with descendants\n        await self.con.execute(\"\"\"\n            create abstract constraint PPP extending exclusive;\n            create abstract constraint QQQ extending PPP;\n            create abstract constraint RRR extending PPP;\n            create type AAA {\n                create required property name -> str\n                {\n                    create constraint PPP;\n                };\n            };\n            create type BBB {\n                create required property name -> str\n                {\n                    create constraint QQQ;\n                };\n            };\n            create type CCC {\n                create required property name -> str\n                {\n                    create constraint RRR;\n                };\n            };\n            create type DDD extending CCC;\n            insert AAA {name := 'aaa'};\n            insert BBB {name := 'bbb'};\n            insert CCC {name := 'ccc'};\n            insert DDD {name := 'ddd'};\n        \"\"\")\n\n        # Check constraints\n        await self._check_constraint_inheritance([\n            [('AAA', 'name')],\n            [('BBB', 'name')],\n            [('CCC', 'name'), ('DDD', 'name')],\n        ])\n"
  },
  {
    "path": "tests/test_database.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport asyncio\nimport edgedb\n\nfrom edb.schema import defines as s_def\nfrom edb.testbase import server as tb\n\n\nclass TestDatabase(tb.ConnectedTestCase):\n    TRANSACTION_ISOLATION = False\n    PARALLELISM_GRANULARITY = 'suite'\n\n    async def test_database_create_01(self):\n        if not self.has_create_database:\n            self.skipTest(\"create database is not supported by the backend\")\n\n        await self.con.execute('CREATE DATABASE mytestdb;')\n\n        try:\n            conn = await self.connect(database='mytestdb')\n\n            dbname = await conn.query('SELECT sys::get_current_branch();')\n            self.assertEqual(dbname, ['mytestdb'])\n\n            with self.assertRaisesRegex(\n                    edgedb.ExecutionError,\n                    r'cannot drop the currently open database'):\n                await conn.execute('DROP DATABASE mytestdb;')\n\n            with self.assertRaisesRegex(\n                    edgedb.ExecutionError,\n                    r'''branch [\"']mytestdb[\"'] is being '''\n                    r'''accessed by other users'''):\n                await self.con.execute('DROP DATABASE mytestdb;')\n\n            await conn.aclose()\n        finally:\n            await tb.drop_db(self.con, 'mytestdb')\n\n    async def test_database_create_02(self):\n        with self.assertRaisesRegex(\n                edgedb.SchemaDefinitionError,\n                r'Branch names longer than \\d+ '\n                r'characters are not supported'):\n            await self.con.execute(\n                f'CREATE DATABASE mytestdb_{\"x\" * s_def.MAX_NAME_LENGTH};')\n\n    async def test_database_create_03(self):\n        if not self.has_create_database:\n            self.skipTest(\"create database is not supported by the backend\")\n\n        await self.con.execute('CREATE DATABASE databasename;')\n\n        try:\n            with self.assertRaisesRegex(\n                    edgedb.DuplicateDatabaseDefinitionError,\n                    r'branch \"databasename\" already exists'):\n                await self.con.execute('CREATE DATABASE databasename;')\n        finally:\n            await tb.drop_db(self.con, 'databasename')\n\n    async def test_database_create_04(self):\n        if not self.has_create_database:\n            self.skipTest(\"create database is not supported by the backend\")\n\n        # create database name that conflicts with names in schema\n        await self.con.execute('CREATE DATABASE range;')\n\n        conn = await self.connect(database='range')\n\n        res = await conn.query('select range(5, 10)')\n        self.assertEqual(res, [edgedb.Range(5, 10)])\n\n        await conn.aclose()\n\n        await tb.drop_db(self.con, 'range')\n\n    async def test_database_drop_01(self):\n        if not self.has_create_database:\n            self.skipTest(\"create database is not supported by the backend\")\n\n        with self.assertRaisesRegex(\n                edgedb.UnknownDatabaseError,\n                r'branch \"databasename\" does not exist'):\n            await self.con.execute('DROP DATABASE databasename;')\n\n    async def test_database_drop_recreate(self):\n        if not self.has_create_database:\n            self.skipTest(\"create database is not supported by the backend\")\n\n        with self.assertRaises(edgedb.UnknownDatabaseError):\n            await self.con.execute('DROP DATABASE test_db_drop;')\n\n        await self.con.execute('CREATE DATABASE test_db_drop;')\n        try:\n            conn = await self.connect(database='test_db_drop')\n\n            try:\n                dbname = await conn.query(\n                    'SELECT sys::get_current_database();')\n                self.assertEqual(dbname, ['test_db_drop'])\n            finally:\n                await conn.aclose()\n\n        finally:\n            await tb.drop_db(self.con, 'test_db_drop')\n\n    async def test_database_non_exist_template(self):\n        if not self.has_create_database:\n            self.skipTest(\"create database is not supported by the backend\")\n\n        with self.assertRaises(edgedb.UnknownDatabaseError):\n            await self.con.execute('CREATE DATABASE _dummy FROM test_tpl')\n\n        await self.con.execute('CREATE DATABASE test_tpl;')\n        try:\n            conn = await self.connect(database='test_tpl')\n\n            try:\n                dbname = await conn.query(\n                    'SELECT sys::get_current_database();')\n                self.assertEqual(dbname, ['test_tpl'])\n            finally:\n                await conn.aclose()\n\n        finally:\n            await tb.drop_db(self.con, 'test_tpl')\n\n    async def test_branch_create_01(self):\n        if not self.has_create_database:\n            self.skipTest(\"create branch is not supported by the backend\")\n\n        await self.con.execute('CREATE EMPTY BRANCH mytestdb;')\n\n        try:\n            conn = await self.connect(database='mytestdb')\n\n            dbname = await conn.query('SELECT sys::get_current_database();')\n            self.assertEqual(dbname, ['mytestdb'])\n\n            with self.assertRaisesRegex(\n                    edgedb.ExecutionError,\n                    r'cannot drop the currently open database'):\n                await conn.execute('DROP BRANCH mytestdb;')\n\n            with self.assertRaisesRegex(\n                    edgedb.ExecutionError,\n                    r'''branch [\"']mytestdb[\"'] is being '''\n                    r'''accessed by other users'''):\n                await self.con.execute('DROP BRANCH mytestdb;')\n\n            await conn.aclose()\n        finally:\n            await tb.drop_db(self.con, 'mytestdb')\n\n    async def test_branch_create_02(self):\n        with self.assertRaisesRegex(\n                edgedb.SchemaDefinitionError,\n                r'Branch names longer than \\d+ '\n                r'characters are not supported'):\n            await self.con.execute(\n                f'CREATE EMPTY BRANCH mytestdb_{\"x\" * s_def.MAX_NAME_LENGTH};')\n\n    async def test_branch_create_03(self):\n        if not self.has_create_database:\n            self.skipTest(\"create branch is not supported by the backend\")\n\n        await self.con.execute('CREATE EMPTY BRANCH databasename;')\n\n        try:\n            with self.assertRaisesRegex(\n                    edgedb.DuplicateDatabaseDefinitionError,\n                    r'branch \"databasename\" already exists'):\n                await self.con.execute('CREATE EMPTY BRANCH databasename;')\n        finally:\n            await tb.drop_db(self.con, 'databasename')\n\n    async def test_branch_create_04(self):\n        if not self.has_create_database:\n            self.skipTest(\"create branch is not supported by the backend\")\n\n        # create branch name that conflicts with names in schema\n        await self.con.execute('CREATE EMPTY BRANCH range;')\n\n        conn = await self.connect(database='range')\n\n        res = await conn.query('select range(5, 10)')\n        self.assertEqual(res, [edgedb.Range(5, 10)])\n\n        await conn.aclose()\n\n        await tb.drop_db(self.con, 'range')\n\n    async def test_branch_drop_01(self):\n        if not self.has_create_database:\n            self.skipTest(\"create branch is not supported by the backend\")\n\n        with self.assertRaisesRegex(\n                edgedb.UnknownDatabaseError,\n                r'branch \"databasename\" does not exist'):\n            await self.con.execute('DROP BRANCH databasename;')\n\n    async def test_branch_drop_recreate(self):\n        if not self.has_create_database:\n            self.skipTest(\"create branch is not supported by the backend\")\n\n        with self.assertRaises(edgedb.UnknownDatabaseError):\n            await self.con.execute('DROP BRANCH test_db_drop;')\n\n        await self.con.execute('CREATE EMPTY BRANCH test_db_drop;')\n        try:\n            conn = await self.connect(database='test_db_drop')\n\n            try:\n                dbname = await conn.query(\n                    'SELECT sys::get_current_database();')\n                self.assertEqual(dbname, ['test_db_drop'])\n            finally:\n                await conn.aclose()\n\n        finally:\n            await tb.drop_db(self.con, 'test_db_drop')\n\n    async def _test_branch_drop_disconnect(\n        self, *, with_transaction, with_query\n    ):\n        if not self.has_create_database:\n            self.skipTest(\"create branch is not supported by the backend\")\n\n        await self.con.execute('CREATE EMPTY BRANCH test_db_disconnect;')\n        conn = await self.connect(database='test_db_disconnect')\n        sleeping = None\n\n        try:\n            if with_transaction:\n                await conn.query('START TRANSACTION')\n\n            dbname = await conn.query(\n                'SELECT sys::get_current_database();')\n            self.assertEqual(dbname, ['test_db_disconnect'])\n\n            if with_query:\n                await conn.query('select sys::_sleep(0)')\n                sleeping = asyncio.create_task(\n                    conn.query('select sys::_sleep(3)')\n                )\n                await asyncio.sleep(1)\n\n            # Drop branch while the frontend connection is active\n            await self.con.execute('''\n                DROP BRANCH test_db_disconnect FORCE\n            ''')\n\n            if with_query:\n                try:\n                    await sleeping\n                except edgedb.EdgeDBError:\n                    pass\n                sleeping = None\n\n            # The frontend connection should be closed by the server now\n            self.assertTrue(conn.is_closed())\n        finally:\n            if sleeping:\n                try:\n                    await sleeping\n                except Exception:\n                    pass\n\n            await conn.aclose()\n            try:\n                await tb.drop_db(self.con, 'test_db_disconnect')\n            except edgedb.UnknownDatabaseError:\n                pass\n\n    async def test_branch_drop_disconnect_01(self):\n        await self._test_branch_drop_disconnect(\n            with_transaction=False, with_query=False,\n        )\n\n    async def test_branch_drop_disconnect_02(self):\n        await self._test_branch_drop_disconnect(\n            with_transaction=True, with_query=False,\n        )\n\n    async def test_branch_drop_disconnect_03(self):\n        await self._test_branch_drop_disconnect(\n            with_transaction=False, with_query=True,\n        )\n\n    async def test_branch_drop_disconnect_04(self):\n        await self._test_branch_drop_disconnect(\n            with_transaction=True, with_query=True,\n        )\n\n    async def test_branch_rename_disconnect(self):\n        if not self.has_create_database:\n            self.skipTest(\"create branch is not supported by the backend\")\n\n        await self.con.execute('CREATE EMPTY BRANCH test_db_rename;')\n        conn = await self.connect(database='test_db_rename')\n\n        try:\n            dbname = await conn.query(\n                'SELECT sys::get_current_database();')\n            self.assertEqual(dbname, ['test_db_rename'])\n\n            # Drop branch while the frontend connection is active\n            await self.con.execute('''\n                ALTER BRANCH test_db_rename FORCE\n                RENAME TO test_db_rename2\n            ''')\n\n            # The frontend connection should be closed by the server now\n            self.assertTrue(conn.is_closed())\n\n            conn2 = await self.connect(database='test_db_rename2')\n            dbname = await conn2.query(\n                'SELECT sys::get_current_database();')\n            self.assertEqual(dbname, ['test_db_rename2'])\n\n        finally:\n            await conn.aclose()\n            await conn2.aclose()\n            try:\n                await tb.drop_db(self.con, 'test_db_rename')\n            except edgedb.UnknownDatabaseError:\n                await tb.drop_db(self.con, 'test_db_rename2')\n\n    async def test_branch_non_exist_template(self):\n        if not self.has_create_database:\n            self.skipTest(\"create branch is not supported by the backend\")\n\n        with self.assertRaises(edgedb.UnknownDatabaseError):\n            await self.con.execute('CREATE DATA BRANCH _dummy FROM test_tpl')\n\n        await self.con.execute('CREATE EMPTY BRANCH test_tpl;')\n        try:\n            conn = await self.connect(database='test_tpl')\n\n            try:\n                dbname = await conn.query(\n                    'SELECT sys::get_current_database();')\n                self.assertEqual(dbname, ['test_tpl'])\n            finally:\n                await conn.aclose()\n\n        finally:\n            await tb.drop_db(self.con, 'test_tpl')\n\n    async def test_branch_rename_01(self):\n        if not self.has_create_database:\n            self.skipTest(\"create database is not supported by the backend\")\n\n        await self.con.execute('CREATE EMPTY BRANCH mytestdb;')\n\n        name = 'mytestdb'\n        conn = None\n        try:\n            res_old = await self.con.query('''\n                SELECT sys::Branch.id filter sys::Branch.name = <str>$0\n            ''', name)\n\n            await self.con.execute('''\n                ALTER BRANCH mytestdb RENAME TO mytestdb2;\n            ''')\n            name = 'mytestdb2'\n\n            res_new = await self.con.query('''\n                SELECT sys::Branch.id filter sys::Branch.name = <str>$0\n            ''', name)\n            self.assertEqual(res_old, res_new)\n\n            conn = await self.connect(database=name)\n\n            dbname = await conn.query('SELECT sys::get_current_database();')\n            self.assertEqual(dbname, [name])\n\n        finally:\n            if conn:\n                await conn.aclose()\n            await tb.drop_db(self.con, name)\n\n    async def test_branch_alias(self):\n        if not self.has_create_database:\n            self.skipTest(\"create database is not supported by the backend\")\n\n        name = 'mydbalias'\n        await self.con.execute(f'CREATE EMPTY BRANCH {name};')\n\n        try:\n            res_old = await self.con.query('''\n                SELECT sys::Database.id filter sys::Database.name = <str>$0\n            ''', name)\n            res_new = await self.con.query('''\n                SELECT sys::Branch.id filter sys::Branch.name = <str>$0\n            ''', name)\n            self.assertEqual(res_old, res_new)\n\n        finally:\n            await tb.drop_db(self.con, name)\n"
  },
  {
    "path": "tests/test_docs.py",
    "content": "##\n# Copyright (c) 2017-present MagicStack Inc.\n# All rights reserved.\n#\n# See LICENSE for details.\n##\n\n\nimport collections\nimport contextlib\nimport json\nimport os\nimport re\nimport subprocess\nimport sys\nimport tempfile\nimport textwrap\nimport unittest\n\ntry:\n    import docutils.nodes\n    import docutils.parsers\n    import docutils.utils\n    import docutils.frontend\n    import docutils.parsers.rst.directives.body  # type: ignore\n    from edb.tools.docs.shared import make_CodeBlock\n\n    docutils.parsers.rst.directives.register_directive(\n        'code-block',\n        make_CodeBlock(docutils.parsers.rst.directives.body.CodeBlock)\n    )\nexcept ImportError:\n    docutils = None  # type: ignore\n\ntry:\n    import sphinx\nexcept ImportError:\n    sphinx = None  # type: ignore\n\nfrom graphql.language import parser as graphql_parser\n\nfrom edb.edgeql import parser as ql_parser\n\n\ndef find_edgedb_root():\n    return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n\nclass LintControl:\n\n    _filename: str | None\n    _errors: list[str]\n\n    def __init__(self):\n        self._lint_on = True\n        self._line_no = 0\n        self._errors = []\n\n    @contextlib.contextmanager\n    def enter_file(self, filename: str):\n        self._filename = filename\n        self._line_no = 0\n        self._lint_on = True\n\n        try:\n            yield\n        finally:\n            try:\n                if not self._lint_on:\n                    raise AssertionError(\n                        f\"Unexpected EOF. No closing '.. lint-on' found in \"\n                        f\"{self._filename}\")\n            finally:\n                self._filename = None\n\n    @property\n    def filename(self) -> str:\n        assert self._filename is not None\n        return self._filename\n\n    def feed_line(self, line: str):\n        assert self._filename is not None\n        self._line_no += 1\n\n        if line.startswith('.. lint-off'):\n            if self._lint_on:\n                self._lint_on = False\n            else:\n                raise AssertionError(\n                    f'Mismatched lint-on/lint-off in '\n                    f'{self._filename}, line {self._line_no}'\n                )\n        elif line.startswith('.. lint-on'):\n            if not self._lint_on:\n                self._lint_on = True\n            else:\n                raise AssertionError(\n                    f'Mismatched lint-on/lint-off in '\n                    f'{self._filename}, line {self._line_no}'\n                )\n\n    def report_error(self, message: str):\n        self._errors.append(f'{self._filename}:{self._line_no}: {message}')\n\n    def raise_errors_if_any(self):\n        if self._errors:\n            raise AssertionError(\n                '\\n'.join(self._errors)\n            )\n\n    def is_linting(self):\n        return self._lint_on\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, _et, _ev, _tb):\n        pass\n\n\nclass TestDocSnippets(unittest.TestCase):\n    \"\"\"Lint and validate EdgeDB documentation files.\n\n    Checks:\n\n    * all source code in \"code-block\" directives is parsed to\n      check that the syntax is valid;\n\n    * any ReST warnings (like improper headers or broken indentation)\n      are reported as errors.\n    \"\"\"\n\n    CodeSnippet = collections.namedtuple(\n        'CodeSnippet',\n        ['filename', 'lineno', 'lang', 'code'])\n\n    class RestructuredTextStyleError(Exception):\n        pass\n\n    if docutils is not None:\n        class CustomDocutilsReporter(docutils.utils.Reporter):\n\n            def __init__(self, *args, **kwargs):\n                super().__init__(*args, **kwargs)\n                self.lint_errors = set()\n\n            def system_message(self, level, message, *children, **kwargs):\n                skip = (\n                    message.startswith('Unknown interpreted text role') or\n                    message.startswith('No role entry for') or\n                    message.startswith('Unknown directive type') or\n                    message.startswith('No directive entry for') or\n                    level < 2  # Ignore DEBUG and INFO messages.\n                )\n\n                msg = super().system_message(\n                    level, message, *children, **kwargs)\n\n                if not skip:\n                    self.lint_errors.add(\n                        f\"{message} at {msg['source']} on line \"\n                        f\"{msg.get('line', '?')}\")\n\n                return msg\n\n    def find_rest_files(self, path: str) -> list[str]:\n        def scan(path):\n            with os.scandir(path) as it:\n                for entry in it:\n                    if entry.is_file() and entry.name.endswith('.rst'):\n                        files.append(entry.path)\n                    if entry.is_dir():\n                        scan(entry.path)\n\n        files: list[str] = []\n        scan(path)\n        return files\n\n    def extract_code_blocks(self, source: str, filename: str):\n        blocks = []\n\n        parser_class = docutils.parsers.get_parser_class('rst')\n        parser = parser_class()\n\n        settings = docutils.frontend.OptionParser(\n            components=(parser_class, )).get_default_values()\n        settings.syntax_highlight = 'none'\n\n        min_error_code = 100  # Ignore all errors, we process them manually.\n        reporter = self.CustomDocutilsReporter(\n            filename, min_error_code, min_error_code)\n        document = docutils.nodes.document(settings, reporter, source=filename)\n        document.note_source(filename, -1)\n\n        parser.parse(source, document)\n\n        if reporter.lint_errors:\n            raise self.RestructuredTextStyleError(\n                '\\n\\nRestructuredText lint errors:\\n' +\n                '\\n'.join(reporter.lint_errors))\n\n        directives = []\n        for node in document.traverse():\n            if node.tagname == 'literal_block':\n                if 'code' in node.attributes['classes']:\n                    directives.append(node)\n\n                else:\n                    block = node.astext()\n\n                    # certain literal blocks also contain code-blocks\n                    if re.match(r'^\\.\\. eql:(operator|function|constraint)::',\n                                block):\n\n                        # figure out the line offset of the start of the block\n                        node_parent = node\n                        while node_parent and node_parent.line is None:\n                            node_parent = node_parent.parent\n                        if node_parent:\n                            node_parent_line = \\\n                                node_parent.line - block.count('\\n')\n                        else:\n                            node_parent_line = 0\n\n                        subdoc = docutils.nodes.document(\n                            settings, reporter, source=filename)\n                        subdoc.note_source(filename, node_parent_line)\n\n                        # cut off the first chunk\n                        block = block.split('\\n\\n', maxsplit=1)[1]\n                        # dedent the rest\n                        block = textwrap.dedent(block)\n\n                        parser.parse(block, subdoc)\n\n                        subdirs = subdoc.traverse(\n                            condition=lambda node: (\n                                node.tagname == 'literal_block' and\n                                'code' in node.attributes['classes'])\n                        )\n                        for subdir in subdirs:\n                            if subdir.line is not None:\n                                subdir.line += node_parent_line\n                            directives.append(subdir)\n\n        for directive in directives:\n            classes = directive.attributes['classes']\n\n            if len(classes) < 2 or classes[0] != 'code':\n                continue\n\n            lang = directive.attributes['classes'][1]\n            code = directive.astext()\n\n            lineno = directive.line\n            if lineno is None:\n                # Some docutils blocks (like tables) do not support\n                # line numbers, so we try to traverse the parent tree\n                # to find the nearest block with a line number.\n                parent_directive = directive\n                while parent_directive and parent_directive.line is None:\n                    parent_directive = parent_directive.parent\n                if parent_directive and parent_directive.line is not None:\n                    lineno = parent_directive.line\n            else:\n                lineno = lineno\n\n            blocks.append(self.CodeSnippet(filename, str(lineno), lang, code))\n\n        return blocks\n\n    def extract_snippets_from_repl(self, replblock):\n        in_query = False\n        snips = []\n        for line in replblock.split('\\n'):\n            if not in_query:\n                m = re.match(r'(?P<p>[\\w\\[:\\]>]+>\\s)(?P<l>.*)', line)\n                if m:\n                    # >>> prompt\n                    in_query = True\n                    snips.append(\n                        (len(m.group('p')), [])\n                    )\n                    snips[len(snips) - 1][1].append(m.group('l'))\n                else:\n                    # output\n                    if not snips:\n                        raise AssertionError(\n                            f'invalid REPL block (starts with output); '\n                            f'offending line {line!r}')\n            else:\n                # ... prompt?\n                m = re.match(r'(?P<p>\\.+\\s)(?P<l>.*)', line)\n                if m:\n                    # yes, it's \"... \" line\n                    if not snips:\n                        raise AssertionError(\n                            f'invalid REPL block (... before >>>); '\n                            f'offending line {line!r}')\n                    if len(m.group('p')) != snips[len(snips) - 1][0]:\n                        raise AssertionError(\n                            f'invalid REPL block: number of \".\" does not '\n                            f'match number of \">\"; '\n                            f'offending line {line!r}')\n                    snips[len(snips) - 1][1].append(m.group('l'))\n                else:\n                    # no, this is output\n                    in_query = False\n\n        return ['\\n'.join(s[1]) for s in snips\n                # ignore the \"\\c\" and other REPL commands\n                if not re.match(r'\\\\\\w+', s[1][0])]\n\n    def run_block_test(self, block):\n        try:\n            lang = block.lang\n            expect_invalid = False\n\n            code = []\n            if lang.endswith('-repl'):\n                lang = lang.rpartition('-')[0]\n                code = self.extract_snippets_from_repl(block.code)\n            elif lang.endswith('-diff'):\n                # In the diff block we need to truncate \"-\"/\"+\" at the\n                # beginning of each line. We will make two copies of\n                # the code as the before and after version. Both will\n                # be validated.\n                before = []\n                after = []\n                for line in block.code.split('\\n'):\n\n                    if line == \"\":\n                        continue\n\n                    first = line.strip()[0]\n                    if first == '-':\n                        before.append(line[1:])\n                    elif first == '+':\n                        after.append(line[1:])\n                    else:\n                        before.append(line[1:])\n                        after.append(line[1:])\n\n                code = ['\\n'.join(before), '\\n'.join(after)]\n                # truncate the \"-diff\" from the language\n                lang = lang[:-5]\n            else:\n                code = [block.code]\n\n            if lang.endswith('-invalid'):\n                lang = lang[:-8]\n                expect_invalid = True\n\n            try:\n                for snippet in code:\n                    if lang == 'edgeql':\n                        ql_parser.parse_block(snippet)\n                    elif lang == 'sdl':\n                        # Strip all the \"using extension ...\" and comment\n                        # lines as they interfere with our module\n                        # detection.\n                        sdl = re.sub(\n                            r'(using\\s+extension\\s+\\w+;)|(#.*?\\n)',\n                            '',\n                            snippet\n                        ).strip()\n\n                        # the snippet itself may either contain a module\n                        # block or have a fully-qualified top-level name\n                        if not sdl or re.match(\n                                r'''(?xm)\n                                    (\\bmodule\\s+\\w+\\s*{) |\n                                    (^.*\n                                        (type|annotation|link|property|constraint)\n                                        \\s+(\\w+::\\w+)\\s+\n                                        ({|extending)\n                                    )\n                                ''',\n                                sdl):\n                            ql_parser.parse_sdl(snippet)\n                        else:\n                            ql_parser.parse_sdl(\n                                f'module default {{ {snippet} }}'\n                            )\n                    elif lang == 'edgeql-result':\n                        # REPL results\n                        pass\n                    elif lang == 'pseudo-eql':\n                        # Skip \"pseudo-eql\" language as we don't have a\n                        # parser for it.\n                        pass\n                    elif lang == 'graphql':\n                        graphql_parser.parse(snippet)\n                    elif lang == 'graphql-schema':\n                        # The graphql-schema can be highlighted using graphql\n                        # lexer, but it does not have a dedicated parser.\n                        pass\n                    elif lang == 'json':\n                        json.loads(snippet)\n                    elif lang in {\n                        'bash',\n                        'powershell',\n                        'shell',\n                        'c',\n                        'javascript',\n                        'python',\n                        'typescript',\n                        'go',\n                        'yaml',\n                        'text',\n                        'jsx',\n                        'rust',\n                        'tsx',\n                        'elixir',\n                        'toml',\n                        'sql',\n                        'dockerfile'\n                    }:\n                        pass\n                    elif lang[-5:] == '-diff':\n                        pass\n                    else:\n                        raise LookupError(f'unknown code-lang {lang}')\n            except LookupError as ex:\n                raise ex\n            except Exception as ex:\n                if not expect_invalid:\n                    raise ex\n            else:\n                if expect_invalid:\n                    raise AssertionError(\"code block is marked with '-invalid'\"\n                                         \" lang, but did not fail validation\")\n        except Exception as ex:\n            raise AssertionError(\n                f'unable to parse {block.lang} code block in '\n                f'{block.filename}, around line {block.lineno}: '\n                f'{code}') from ex\n\n    @unittest.skipIf(docutils is None, 'docutils is missing')\n    def test_cqa_doc_snippets(self):\n        edgepath = edgepath = find_edgedb_root()\n        docspath = os.path.join(edgepath, 'docs')\n\n        for filename in self.find_rest_files(docspath):\n            with open(filename, 'rt') as f:\n                source = f.read()\n\n            blocks = self.extract_code_blocks(source, filename)\n\n            for block in blocks:\n                self.run_block_test(block)\n\n    def test_cqa_doc_trailing_whitespace(self):\n        edgepath = find_edgedb_root()\n        docspath = os.path.join(edgepath, 'docs')\n\n        ws_errors = collections.defaultdict(set)\n\n        for filename in self.find_rest_files(docspath):\n            with open(filename, 'rt') as f:\n                source = f.readlines()\n\n            for lineno, line in enumerate(source):\n                if re.match(r'\\s+\\n$', line):\n                    ws_errors[filename].add(lineno)\n\n        if ws_errors:\n            raise AssertionError(\n                'trailing whitespace:\\n\\n' +\n                '\\n'.join(\n                    f'{filename}:{linenos!r}'\n                    for filename, linenos in ws_errors.items()\n                )\n            )\n\n    def test_cqa_doc_substitutions(self):\n        edgepath = find_edgedb_root()\n        docspath = os.path.join(edgepath, 'docs')\n\n        with LintControl() as lc:\n            for filename in self.find_rest_files(docspath):\n                if '/docs/resources/changelog/' in filename:\n                    # changelog files contain a bunch of historical\n                    # text that sometimes can't use the new stuff.\n                    continue\n\n                with lc.enter_file(filename):\n                    with open(filename, 'rt') as f:\n                        source = f.readlines()\n\n                    for line in source:\n                        lc.feed_line(line)\n\n                        if not lc.is_linting():\n                            continue\n\n                        if '``edgedb://' in line:\n                            lc.report_error(\n                                f'do not use ``edgedb://``, '\n                                f'use |geluri| for \"gel://\" and '\n                                f':geluri:`blah` for \"gel://blah\"')\n                        if '``gel://' in line:\n                            lc.report_error(\n                                f'do not use ``gel://``, '\n                                f'use |geluri| for \"gel://\" and '\n                                f':geluri:`blah` for \"gel://blah\"')\n\n                        if '.esdl``' in line or '.gel``' in line:\n                            lc.report_error(\n                                f\"don't use ``filename.esdl`` \"\n                                f\"or ``filename.gel``, use :dotgel:`filename` \"\n                                f\"instead\")\n                        if '``gel ' in line:\n                            lc.report_error(\n                                f'do not use ``gel`` markup '\n                                f'for \"gel\" cli commands, '\n                                f'use :gelcmd:`command` instead; '\n                                f'it will be rendered as ``gel command``')\n                        if '``edgedb ' in line:\n                            lc.report_error(\n                                f'do not use ``edgedb`` markup '\n                                f'for \"gel\" cli commands, '\n                                f'use :gelcmd:`command` instead; '\n                                f'it will be rendered as ``gel command``')\n\n        lc.raise_errors_if_any()\n\n    @unittest.skipIf(docutils is None, 'docutils is missing')\n    def test_doc_test_broken_code_block_01(self):\n        source = '''\n        In large applications, the schema will usually be split\n        into several :ref:`modules<ref_schema_evolution_modules>`.\n\n        .. code-block:: edgeql\n\n            SELECT 122 + foo();\n\n        A *schema module* defines the effective namespace for\n        elements it defines.\n\n        .. code-block:: edgeql\n\n            SELECT 42;\n            # ^ expected to return 42\n            SELECT foo(\n\n        Schema modules can import other modules to use schema\n        elements they define.\n        '''\n\n        blocks = self.extract_code_blocks(source, '<test>')\n        self.assertEqual(len(blocks), 2)\n        self.assertEqual(blocks[0].code, 'SELECT 122 + foo();')\n        self.run_block_test(blocks[0])\n\n        with self.assertRaisesRegex(AssertionError, 'unable to parse edgeql'):\n            self.run_block_test(blocks[1])\n\n    @unittest.skipIf(docutils is None, 'docutils is missing')\n    def test_doc_test_broken_code_block_02(self):\n        source = r'''\n        String operator with a buggy example.\n\n        .. eql:operator:: LIKE: str LIKE str -> bool\n                                str NOT LIKE str -> bool\n\n            Case-sensitive simple string matching.\n\n            Example:\n\n            .. code-block:: edgeql-repl\n\n                db> SELECT 'a%%c' NOT LIKE 'a\\%c';\n                {true}\n        '''\n\n        blocks = self.extract_code_blocks(source, '<test>')\n        self.assertEqual(len(blocks), 1)\n        self.assertEqual(blocks[0].code,\n                         \"db> SELECT 'a%%c' NOT LIKE 'a\\\\%c';\\n{true}\")\n\n        with self.assertRaisesRegex(AssertionError, 'unable to parse edgeql'):\n            self.run_block_test(blocks[0])\n\n    @unittest.skipIf(docutils is None, 'docutils is missing')\n    def test_doc_test_bad_header(self):\n        source = textwrap.dedent('''\n            Section\n            -----\n\n            aaa aaa aaa\n        ''')\n\n        with self.assertRaisesRegex(\n                self.RestructuredTextStyleError,\n                r'lint errors:[.\\s]*Title underline too short'):\n            self.extract_code_blocks(source, '<test>')\n\n    @unittest.skipIf(sphinx is None, 'sphinx is missing')\n    def test_doc_full_build(self):\n        docs_root = os.path.join(find_edgedb_root(), 'docs')\n\n        with tempfile.TemporaryDirectory() as td:\n            proc = subprocess.run(\n                [\n                    sys.executable,\n                    '-I',\n                    '-m', 'sphinx',\n                    '-n',\n                    '-b', 'xml',\n                    '-q',\n                    '-D', 'master_doc=index',\n                    '-W',\n                    docs_root,\n                    td,\n                ],\n                text=True,\n                stderr=subprocess.PIPE,\n                stdout=subprocess.PIPE,\n            )\n\n        if proc.returncode:\n            raise AssertionError(\n                f'Unable to build docs with Sphinx.\\n\\n'\n                f'STDOUT:\\n{proc.stdout}\\n\\n'\n                f'STDERR:\\n{proc.stderr}\\n'\n            )\n\n        errors = []\n        ignored_errors = re.compile(\n            r'^.* WARNING: undefined label: edgedb-'\n            r'(python|js|go|dart|dotnet|elixir|java)-.*$'\n        )\n        for line in proc.stderr.splitlines():\n            if not ignored_errors.match(line):\n                errors.append(line)\n\n        if len(errors) > 0:\n            errors = '\\n'.join(errors)\n            raise AssertionError(\n                f'Unable to build docs with Sphinx.\\n\\n'\n                f'{errors}\\n\\n'\n            )\n"
  },
  {
    "path": "tests/test_docs_sphinx_ext.py",
    "content": "import contextlib\nimport os.path\nimport subprocess\nimport tempfile\nimport textwrap\nimport unittest\n\ntry:\n    import requests_xml\nexcept ModuleNotFoundError:\n    requests_xml = None\n\n\nclass BuildFailedError(Exception):\n    pass\n\n\nclass BaseDomainTest:\n\n    def build(self, src, *, format='html'):\n        src = textwrap.dedent(src)\n\n        with tempfile.TemporaryDirectory() as td_in, \\\n                tempfile.TemporaryDirectory() as td_out:\n\n            # Since v2.0, Sphinx uses \"index\" as master_doc by default.\n            fn = os.path.join(td_in, 'index.rst')\n            with open(fn, 'wt') as f:\n                f.write(src)\n                f.flush()\n\n            args = [\n                'sphinx-build',\n                '-b', format,\n                '-W',\n                '-n',\n                '-C',\n                '-D', 'extensions=edb.tools.docs',\n                '-D', 'smartquotes_action=De',\n                '-q',\n                td_in,\n                td_out,\n                fn\n            ]\n\n            try:\n                subprocess.run(\n                    args, check=True,\n                    stderr=subprocess.PIPE, stdout=subprocess.PIPE)\n            except subprocess.CalledProcessError as ex:\n                msg = [\n                    'The build has failed.',\n                    '',\n                    'STDOUT',\n                    '======',\n                    ex.stdout.decode(),\n                    '',\n                    'STDERR',\n                    '======',\n                    ex.stderr.decode(),\n                    '',\n                    'INPUT',\n                    '=====',\n                    src\n                ]\n                new_ex = BuildFailedError('\\n'.join(msg))\n                new_ex.stdout = ex.stdout.decode()\n                new_ex.stderr = ex.stderr.decode()\n                raise new_ex from ex\n\n            with open(os.path.join(td_out, f'index.{format}'), 'rt') as f:\n                out = f.read()\n\n            return out\n\n    @contextlib.contextmanager\n    def assert_fails(self, err):\n        with self.assertRaises(BuildFailedError) as raised:\n            yield\n\n        self.assertRegex(raised.exception.stderr, err)\n\n\n@unittest.skipIf(requests_xml is None, 'requests-xml package is not installed')\nclass TestEqlType(unittest.TestCase, BaseDomainTest):\n\n    def test_sphinx_eql_type_01(self):\n        src = '''\n        .. eql:type:: int64\n\n            descr\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            x.xpath('''\n                //desc_signature\n                    [@eql-fullname=\"std::int64\"] /\n                    desc_name / text()\n            '''),\n            ['int64'])\n\n    def test_sphinx_eql_type_02(self):\n        src = '''\n        .. eql:type:: std::int64\n        '''\n\n        with self.assert_fails('the directive must include a description'):\n            self.build(src)\n\n    def test_sphinx_eql_type_03(self):\n        src = '''\n        .. eql:type:: std::int64\n\n            aaa\n\n        Testing refs :eql:type:`int1`\n        '''\n\n        with self.assert_fails(\n                \"cannot resolve :eql:type: targeting 'type::std::int1'\"):\n            self.build(src)\n\n    def test_sphinx_eql_type_04(self):\n        src = '''\n        .. eql:type:: std::int64\n\n            aaa\n\n        Testing refs :eql:type:`int64`\n        '''\n\n        self.assertRegex(\n            self.build(src),\n            r'(?x).*<a .* href=\"#std::int64\".*')\n\n    def test_sphinx_eql_type_05(self):\n        src = '''\n        .. eql:type:: std::int64\n\n            long text long text long text long text long text long text\n            long text long text long text long text long text long text\n\n            long text\n        '''\n\n        with self.assert_fails(\"shorter than 80 characters\"):\n            self.build(src)\n\n    def test_sphinx_eql_type_06(self):\n        src = r'''\n        .. eql:type:: std::int64\n\n            An integer.\n\n        .. eql:type:: std::array\n\n            Array.\n\n        Testing :eql:type:`XXX <array<int64>>` ref.\n        Testing :eql:type:`array\\<int64\\>` ref.\n        Testing :eql:type:`array\\<int64\\> <array<int64>>` ref.\n        Testing :eql:type:`array\\<array\\<int64\\>\\>` ref.\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            x.xpath('''\n                //paragraph /\n                reference[@eql-type=\"type\"] /\n                literal / text()\n            '''),\n            ['XXX', 'array<int64>', 'array<int64>', 'array<array<int64>>'])\n\n    def test_sphinx_eql_type_07(self):\n        src = '''\n        .. eql:type:: int64\n\n            An integer.\n\n        Testing :eql:type:`OPTIONAL  int64` ref.\n        Testing :eql:type:`OPTIONAL int64` ref.\n        Testing :eql:type:`SET  OF  int64` ref.\n        Testing :eql:type:`SET OF int64` ref.\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            x.xpath('''\n                //paragraph /\n                reference[@eql-type=\"type\"] /\n                literal / text()\n            '''),\n            ['OPTIONAL  int64', 'OPTIONAL int64',\n             'SET  OF  int64', 'SET OF int64'])\n\n    def test_sphinx_eql_type_08(self):\n        src = '''\n        .. eql:type:: SET OF\n\n            An integer.\n\n        Testing :eql:type:`SET OF`.\n        Testing :eql:type:`XXX <SET OF>`.\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            x.xpath('''\n                //paragraph /\n                reference[@eql-type=\"type\"] /\n                literal / text()\n            '''),\n            ['SET OF', 'XXX'])\n\n\n@unittest.skipIf(requests_xml is None, 'requests-xml package is not installed')\nclass TestEqlFunction(unittest.TestCase, BaseDomainTest):\n\n    def test_sphinx_eql_func_01(self):\n        src = '''\n        Testing DESC !! :eql:func-desc:`test` !! >> ref.\n\n        .. eql:type:: std::int64\n\n            An integer.\n\n        .. eql:type:: any\n\n            any.\n\n        .. eql:function:: std::test(v: any) -> any\n\n            :index: xxx YyY\n\n            A super function.\n\n        Testing :eql:func:`XXX <test>` ref.\n        Testing :eql:func:`test` ref.\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        func = x.xpath('//desc[@desctype=\"function\"]')\n        self.assertEqual(len(func), 1)\n        func = func[0]\n\n        self.assertEqual(func.attrs['summary'], 'A super function.')\n        self.assertIn('!! A super function. !!', out)\n\n        self.assertEqual(\n            x.xpath('//desc_returns / text()'),\n            ['any'])\n\n        self.assertEqual(\n            x.xpath('''\n                //paragraph /\n                reference[@eql-type=\"function\" and\n                    @refid=\"function::std::test\"] /\n                literal / text()\n            '''),\n            ['XXX', 'test()'])\n\n        self.assertEqual(\n            x.xpath('''\n                //field[@eql-name=\"index\"] / field_body / paragraph / text()\n            '''),\n            ['xxx YyY'])\n\n    def test_sphinx_eql_func_02(self):\n        src = '''\n        .. eql:function:: std::test() -> any\n\n            long text long text long text long text long text long text\n            long text long text long text long text long text long text\n\n            long text\n        '''\n\n        with self.assert_fails(\"shorter than 80 characters\"):\n            self.build(src)\n\n    def test_sphinx_eql_func_03(self):\n        src = '''\n        .. eql:function:: std::test(v: any) -> any\n\n            :type v: int64\n\n            blah\n        '''\n\n        expected = r'''(?xs)\n        found\\sunknown\\sfield\\s'type' .*\n        Possible\\sreason:\\sfield\\s'type'\\sis\\snot\\ssupported\n        '''\n\n        with self.assert_fails(expected):\n            self.build(src)\n\n    def test_sphinx_eql_func_05(self):\n        src = '''\n        .. eql:function:: std::test(v: any) -> any\n\n        blah\n        '''\n\n        with self.assert_fails('the directive must include a description'):\n            self.build(src)\n\n    def test_sphinx_eql_func_06(self):\n        src = '''\n        .. eql:function:: std::test(v: any) -> any\n\n            blah\n\n            :index: foo bar\n\n            blah\n        '''\n\n        with self.assert_fails(\n                'fields must be specified before all other content'):\n            self.build(src)\n\n    def test_sphinx_eql_func_07(self):\n        src = '''\n        .. eql:function:: std::test(a: OPTIONAL str, b: SET OF str, \\\\\n                            c: str) -> SET OF str\n\n            blah\n\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            x.xpath('//desc_returns / text()'),\n            ['set of str'])\n\n        self.assertEqual(\n            x.xpath('//desc_signature/@eql-signature'),\n            ['std::test(a: optional str, b: set of str, c: str) -> set of str']\n        )\n\n    def test_sphinx_eql_func_08(self):\n        src = '''\n        .. eql:function:: std::test(NAMED ONLY v: in64=42) -> OPTIONAL int64\n\n            blah\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            x.xpath('//desc_returns / text()'),\n            ['optional int64'])\n\n        self.assertEqual(\n            x.xpath('//desc_signature/@eql-signature'),\n            ['std::test(named only v: in64 = 42) -> optional int64'])\n\n    def test_sphinx_eql_func_09(self):\n        src = '''\n        .. eql:function:: sys::sleep(duration: duration) -> bool\n                          sys::sleep(duration: float64) -> bool\n\n            :index: sleep delay\n\n            blah\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            x.xpath('//desc_signature/@eql-signature'),\n            ['sys::sleep(duration: duration) ->  bool',\n             'sys::sleep(duration: float64) ->  bool'])\n\n\n@unittest.skipIf(requests_xml is None, 'requests-xml package is not installed')\nclass TestEqlConstraint(unittest.TestCase, BaseDomainTest):\n\n    def test_sphinx_eql_constr_01(self):\n        src = '''\n        .. eql:type:: std::int64\n\n            An integer.\n\n        .. eql:type:: any\n\n            any.\n\n        .. eql:constraint:: std::max_len_value(v: any)\n\n            blah\n\n        Testing :eql:constraint:`XXX <max_len_value>` ref.\n        Testing :eql:constraint:`max_len_value` ref.\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        constr = x.xpath('//desc[@desctype=\"constraint\"]')\n        self.assertEqual(len(constr), 1)\n        constr = constr[0]\n\n        self.assertEqual(constr.attrs['summary'], 'blah')\n\n        self.assertEqual(\n            x.xpath('''\n                //paragraph /\n                reference[@eql-type=\"constraint\" and\n                    @refid=\"constraint::std::max_len_value\"] /\n                literal / text()\n            '''),\n            ['XXX', 'max_len_value'])\n\n    def test_sphinx_eql_constr_02(self):\n        src = '''\n        .. eql:constraint:: std::len_value on (len(<std::str>__subject__))\n\n            blah\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        sig = x.xpath('//desc[@desctype=\"constraint\"]/desc_signature')[0]\n\n        self.assertEqual(\n            sig.attrs['eql-signature'],\n            'std::len_value on (len(<std::str>__subject__))')\n\n        self.assertEqual(\n            sig.attrs['eql-subjexpr'],\n            'len(<std::str>__subject__)')\n\n\n@unittest.skipIf(requests_xml is None, 'requests-xml package is not installed')\nclass TestEqlOperator(unittest.TestCase, BaseDomainTest):\n\n    def test_sphinx_eql_op_01(self):\n        src = '''\n        Testing ?? :eql:op-desc:`PLUS` ??.\n\n        .. eql:type:: int64\n\n            int64\n\n        .. eql:type:: str\n\n            123\n\n        .. eql:operator:: PLUS: A + B\n\n            :optype A: int64 or str\n            :optype B: int64 or str\n            :resulttype: int64 or str\n\n            Arithmetic addition.\n\n        some text\n\n        :eql:op:`XXX <PLUS>`\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertIn('Testing ?? Arithmetic addition. ??.', out)\n\n        self.assertEqual(\n            len(x.xpath('''\n                //desc_signature[@eql-name=\"PLUS\" and @eql-signature=\"A + B\"] /\n                *[\n                    (self::desc_annotation and text()=\"operator\") or\n                    (self::desc_name and text()=\"A + B\")\n                ]\n            ''')),\n            2)\n\n        self.assertEqual(len(x.xpath('//field[@eql-name=\"operand\"]')), 2)\n        self.assertEqual(len(x.xpath('//field[@eql-name=\"resulttype\"]')), 1)\n\n        self.assertEqual(\n            x.xpath('''\n                //paragraph /\n                reference[@eql-type=\"operator\" and @refid=\"operator::PLUS\"] /\n                literal / text()\n            '''),\n            ['XXX'])\n\n    def test_sphinx_eql_op_02(self):\n        src = '''\n        .. eql:type:: any\n\n            123\n\n        .. eql:operator:: IS: A IS B\n\n            :optype A: any\n            :optype B: type\n            :resulttype: any\n\n            Is\n\n        :eql:op:`XXX <IS>`\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            x.xpath('''\n                //field[@eql-opname=\"B\"] /\n                field_body / * / literal_strong / text()\n            '''),\n            ['B'])\n\n\n@unittest.skipIf(requests_xml is None, 'requests-xml package is not installed')\nclass TestEqlKeyword(unittest.TestCase, BaseDomainTest):\n\n    def test_sphinx_eql_kw_01(self):\n        src = '''\n        .. eql:keyword:: SET OF\n\n            blah\n\n        some text\n\n        :eql:kw:`XXX <SET OF>`\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            len(x.xpath('''\n                //desc[@desctype=\"keyword\"] /\n\n                desc_signature[@eql-name=\"SET OF\"] /\n                *[\n                    (self::desc_annotation and text()=\"keyword\") or\n                    (self::desc_name and text()=\"SET OF\")\n                ]\n            ''')),\n            2)\n\n        self.assertEqual(\n            x.xpath('''\n                //paragraph /\n                reference[@eql-type=\"keyword\" and @refid=\"keyword::SET-OF\"] /\n                literal / text()\n            '''),\n            ['XXX'])\n\n\n@unittest.skipIf(requests_xml is None, 'requests-xml package is not installed')\nclass TestEqlStatement(unittest.TestCase, BaseDomainTest):\n\n    def test_sphinx_eql_stmt_05(self):\n        src = '''\n\n        CREATE FUNCTION\n        ===============\n\n        :eql-statement:\n\n        ``CREATE FUNCTION``--creates a function.\n\n        fooing and baring.\n\n        Subhead\n        -------\n\n        asdasdas\n\n\n        CREATE TYPE\n        ===========\n\n        :eql-statement:\n\n        blah.\n\n\n        Test\n        ====\n\n        A ref to :eql:stmt:`create function`\n\n        A ref to :eql:stmt:`ttt <create type>`\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            x.xpath('''\n                //paragraph /\n                reference[@eql-type=\"statement\" and\n                          @refid=\"statement::create-function\"] /\n                literal / text()\n            '''),\n            ['create function'])\n\n        self.assertEqual(\n            x.xpath('''\n                //paragraph /\n                reference[@eql-type=\"statement\" and\n                          @refid=\"statement::create-type\"] /\n                literal / text()\n            '''),\n            ['ttt'])\n\n        self.assertEqual(\n            x.xpath('''\n                //section[@eql-statement=\"true\"]/@ids\n            '''),\n            ['create-function statement::create-function',\n             'create-type statement::create-type'])\n\n        self.assertEqual(\n            x.xpath('''\n                //section[@eql-statement=\"true\"]/@summary\n            '''),\n            ['CREATE FUNCTION--creates a function.', 'blah.'])\n\n    def test_sphinx_eql_stmt_06(self):\n        src = '''\n\n        AAAAAA\n        ======\n\n        :eql-statement:\n\n        aa aaaaaa aaaaa aaaa aa aaaaaa aaaaa aaaa aa aaaaaa aaaaa aaaa aa\n        aa aaaaaa aaaaa aaaa aa aaaaaa aaaaa aaaa.\n        '''\n\n        with self.assert_fails(\n                'first paragraph is longer than 79 characters'):\n            self.build(src)\n\n    def test_sphinx_eql_stmt_08(self):\n        src = '''\n\n        AA AA\n        =====\n\n        :eql-statement:\n\n        aa aaaaaa aaaaa aaaa aa.\n\n        BB\n        --\n\n        :eql-statement:\n\n        bbb.\n        '''\n\n        with self.assert_fails(\n                ' has a nested section with a :eql-statement:'):\n            self.build(src)\n\n    def test_sphinx_eql_stmt_09(self):\n        src = '''\n\n        AA AA\n        =====\n\n        :eql-statement:\n\n        aa aaaaaa aaaaa aaaa aa.\n\n        AA AA\n        =====\n\n        :eql-statement:\n\n        aa aaaaaa aaaaa aaaa aa.\n        '''\n\n        with self.assert_fails(\"duplicate 'AA AA' statement\"):\n            self.build(src)\n\n    def test_sphinx_eql_stmt_10(self):\n        src = '''\n        =========\n        Functions\n        =========\n\n        :edb-alt-title: Functions and Operators\n\n        This section describes the DDL commands ...\n\n\n        CREATE FUNCTION\n        ===============\n\n        :eql-statement:\n\n        Define a new function.\n\n\n        DROP FUNCTION\n        =============\n\n        :eql-statement:\n        :eql-haswith:\n\n        Remove a function.\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            x.xpath('''\n                //section/title[text()=\"Functions\"]/@edb-alt-title\n            '''),\n            ['Functions and Operators'])\n\n        self.assertEqual(\n            x.xpath('''\n                //section[@eql-statement=\"true\"]/title/text()\n            '''),\n            ['CREATE FUNCTION', 'DROP FUNCTION'])\n\n        self.assertEqual(\n            x.xpath('''\n                //section[@eql-statement=\"true\" and @eql-haswith=\"true\"]\n                    /title/text()\n            '''),\n            ['DROP FUNCTION'])\n\n    def test_sphinx_eql_struct_01(self):\n        src = '''\n        .. eql:struct:: edb.protocol.AuthenticationSASLFinal\n\n        .. eql:struct:: edb.protocol.enums.Cardinality\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            x.xpath('''\n                literal_block/@language\n            '''),\n            ['c', 'c'])\n\n        val = x.xpath('''\n            literal_block/text()\n        ''')\n        self.assertIn('struct AuthenticationSASLFinal {', val[0])\n        self.assertIn('enum Cardinality {', val[1])\n\n\n@unittest.skipIf(requests_xml is None, 'requests-xml package is not installed')\nclass TestEqlRoles(unittest.TestCase, BaseDomainTest):\n\n    def test_sphinx_eql_inline_role_01(self):\n        src = '''\n        a test of :eql:synopsis:`WITH <aaaa>`.\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            x.xpath('''\n                //literal[@eql-lang=\"edgeql-synopsis\"] / text()\n            '''),\n            ['WITH <aaaa>'])\n\n    def test_sphinx_eql_inline_role_02(self):\n        cases = [\n            (\n                '#123',\n                'edgedb/edgedb/issues/123',\n                None,\n                '#123',\n            ),\n            (\n                'magicstack/asyncpg/#227',\n                'magicstack/asyncpg/issues/227',\n                None,\n                'magicstack/asyncpg/#227',\n            ),\n            (\n                'ff123aaaaeeeee',\n                'edgedb/edgedb/commit/ff123aaaaeeeee',\n                None,\n                'ff123aaa'\n            ),\n            (\n                'magicstack/asyncpg/ff123aaaaeeeee',\n                'magicstack/asyncpg/commit/ff123aaaaeeeee',\n                None,\n                'magicstack/asyncpg/ff123aaa'\n            ),\n\n            (\n                '#123',\n                'edgedb/edgedb/issues/123',\n                'blah1',\n                'blah1',\n            ),\n            (\n                'magicstack/asyncpg/#227',\n                'magicstack/asyncpg/issues/227',\n                'blah2',\n                'blah2',\n            ),\n            (\n                'ff123aaaaeeeee',\n                'edgedb/edgedb/commit/ff123aaaaeeeee',\n                'blah3',\n                'blah3',\n            ),\n            (\n                'magicstack/asyncpg/ff123aaaaeeeee',\n                'magicstack/asyncpg/commit/ff123aaaaeeeee',\n                'blah4',\n                'blah4',\n            ),\n        ]\n\n        src = ''\n        for (body, _, title, _) in cases:\n            if title:\n                src += f':eql:gh:`{title} <{body}>`\\n'\n            else:\n                src += f':eql:gh:`{body}`\\n'\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        for (_, expected_link, _, expected_title) in cases:\n            self.assertEqual(\n                x.xpath(f'''\n                    //reference[\n                        @eql-github=\"True\" and\n                        @name=\"{expected_title}\" and\n                        @refuri=\"https://github.com/{expected_link}\"\n                    ]/text()\n                '''),\n                [expected_title]\n            )\n\n\n@unittest.skipIf(requests_xml is None, 'requests-xml package is not installed')\nclass TestBlockquote(unittest.TestCase, BaseDomainTest):\n\n    def test_sphinx_eql_blockquote_01(self):\n        src = '''\n        blah\n\n         * list\n         * item\n        '''\n\n        with self.assert_fails('blockquote found'):\n            self.build(src, format='xml')\n\n        with self.assert_fails('blockquote found'):\n            self.build(src, format='html')\n\n    def test_sphinx_eql_blockquote_02(self):\n        # Test that although regular block-qoutes are blocked\n        # (as their syntax is very confusing and fragile), we can\n        # still use explicit block-quotes via the `.. pull-quote::`\n        # directive.\n\n        src = '''\n        blah\n\n        .. pull-quote::\n\n            spam\n\n        blah2\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            x.xpath('''\n                block_quote/*/text()\n            '''),\n            ['spam']\n        )\n\n    def test_sphinx_eql_singlebacktick_01(self):\n        src = '''\n        Another use case is for giving short aliases to long module names\n        (especially if module names contain `.`).\n        '''\n\n        with self.assert_fails('title reference'):\n            self.build(src, format='xml')\n\n        with self.assert_fails('title reference'):\n            self.build(src, format='html')\n\n    def test_sphinx_edb_collapsed_01(self):\n        src = '''\n        blah\n\n        Foo\n        ===\n\n        bar\n\n        .. edb:collapsed::\n\n            spam\n\n            ham\n\n        blah2\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            x.xpath('''\n                //container[@collapsed_block=\"True\"]/paragraph/text()\n            '''),\n            ['spam', 'ham'])\n\n\n@unittest.skipIf(requests_xml is None, 'requests-xml package is not installed')\nclass TestOthers(unittest.TestCase, BaseDomainTest):\n\n    def test_sphinx_edb_brand_name_01(self):\n        src = '''\n        blah |Gel|\n        blah2 |Gel's|\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            x.xpath('''\n                //paragraph/inline[@edb-substitution=\"true\"]/text()\n            '''),\n            [\"Gel\", \"Gel's\"])\n\n    def test_sphinx_edb_brand_name_02(self):\n        src = '''\n        blah |gelcmd|\n        blah 2 :gelcmd:`migrate --help`\n        blah 3 :gelcmd:`migrate\n        --help\n        --foo`\n\n        blah4 |geluri|\n        blah5 :geluri:`foo:bar@nax/a:123#12`\n\n        blah6 :dotgel:`default`\n\n        blah7 :gelenv:`HOST`\n        blah8 :gelenv:`HOST=AB`\n\n        DONE\n        '''\n\n        out = self.build(src, format='xml')\n        x = requests_xml.XML(xml=out)\n\n        self.assertEqual(\n            x.xpath('''\n                //paragraph/literal\n                    [@edb-substitution=\"true\"]\n                    [@edb-gelcmd=\"true\"]\n                    [@edb-gelcmd-top=\"true\"]\n                    / text()\n            '''),\n            ['gel']\n        )\n\n        self.assertEqual(\n            x.xpath('''\n                //paragraph/literal\n                    [@edb-substitution=\"true\"]\n                    [@edb-gelcmd=\"true\"]\n                    [@edb-gelcmd-top=\"false\"]\n                    / text()\n            '''),\n            ['gel migrate --help', 'gel migrate --help --foo']\n        )\n\n        self.assertEqual(\n            x.xpath('''\n                //paragraph/literal\n                    [@edb-substitution=\"true\"]\n                    [@edb-geluri=\"true\"]\n                    / text()\n            '''),\n            ['gel://', 'gel://foo:bar@nax/a:123#12']\n        )\n\n        self.assertEqual(\n            x.xpath('''\n                //paragraph/literal\n                    [@edb-substitution=\"true\"]\n                    [@edb-dotgel=\"true\"]\n                    / text()\n            '''),\n             ['default.gel']\n        )\n\n        self.assertEqual(\n            x.xpath('''\n                //paragraph/literal\n                    [@edb-substitution=\"true\"]\n                    [@edb-gelenv=\"true\"]\n                    / text()\n            '''),\n             ['GEL_HOST', 'GEL_HOST=AB']\n        )\n"
  },
  {
    "path": "tests/test_dump01.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport os.path\n\nimport edgedb\n\nfrom edb.testbase import server as tb\n\n\nclass DumpTestCaseMixin:\n\n    async def ensure_schema_data_integrity(self, include_data=True):\n        async for tx in self._run_and_rollback_retrying():\n            async with tx:\n                await self._ensure_schema_integrity()\n                if include_data:\n                    await self._ensure_data_integrity()\n\n    async def _ensure_schema_integrity(self):\n        # check that all the type annotations are in place\n        await self.assert_query_result(\n            r'''\n            WITH MODULE schema\n            SELECT ObjectType {\n                name,\n                annotations: {\n                    name,\n                    @value,\n                } ORDER BY .name\n            }\n            FILTER\n                EXISTS .annotations\n                AND\n                .name LIKE 'default::%'\n            ORDER BY .name;\n            ''',\n            [\n                {\n                    'name': 'default::A',\n                    'annotations': [\n                        {\n                            'name': 'std::title',\n                            '@value': 'A',\n                        },\n                    ],\n                },\n                {\n                    'name': 'default::B',\n                    'annotations': [\n                        {\n                            'name': 'std::title',\n                            '@value': 'B',\n                        },\n                    ],\n                },\n                {\n                    'name': 'default::C',\n                    'annotations': [\n                        {\n                            'name': 'std::title',\n                            '@value': 'C',\n                        },\n                    ],\n                },\n                {\n                    'name': 'default::D',\n                    'annotations': [\n                        {\n                            'name': 'default::heritable_user_anno',\n                            '@value': 'all D',\n                        },\n                        {\n                            'name': 'default::user_anno',\n                            '@value': 'D only',\n                        },\n                        {\n                            'name': 'std::title',\n                            '@value': 'D',\n                        },\n                    ],\n                },\n                {\n                    'name': 'default::E',\n                    'annotations': [\n                        {\n                            'name': 'default::heritable_user_anno',\n                            '@value': 'all D',\n                        },\n                        {\n                            'name': 'std::title',\n                            '@value': 'E',\n                        },\n                    ],\n                },\n                {\n                    'name': 'default::F',\n                    'annotations': [\n                        {\n                            'name': 'default::heritable_user_anno',\n                            '@value': 'all D',\n                        },\n                        {\n                            'name': 'std::title',\n                            '@value': 'F',\n                        },\n                    ],\n                },\n            ]\n        )\n\n        # check that all the prop/link annotations are in place\n        await self.assert_query_result(\n            r'''\n            WITH MODULE schema\n            SELECT ObjectType {\n                name,\n                properties: {\n                    name,\n                    annotations: {\n                        name,\n                        @value,\n                    },\n                } # keep only annotated props\n                FILTER EXISTS .annotations\n                ORDER BY .name,\n                links: {\n                    name,\n                    annotations: {\n                        name,\n                        @value,\n                    },\n                } # keep only annotated links\n                FILTER EXISTS .annotations\n                ORDER BY .name,\n            }\n            FILTER\n                # keep only types with annotated pointers\n                EXISTS .pointers.annotations\n                AND\n                .name LIKE 'default::%'\n            ORDER BY .name;\n            ''',\n            [\n\n                {\n                    'name': 'default::A',\n                    'properties': [\n                        {\n                            'name': 'p_bool',\n                            'annotations': [\n                                {\n                                    'name': 'std::title',\n                                    '@value': 'single bool',\n                                },\n                            ],\n                        },\n                    ],\n                    'links': [],\n                },\n                {\n                    'name': 'default::B',\n                    'properties': [\n                        {\n                            'name': 'p_bool',\n                            'annotations': [\n                                {\n                                    'name': 'std::title',\n                                    '@value': 'multi bool',\n                                },\n                            ],\n                        },\n                    ],\n                    'links': []\n                },\n                {\n                    'name': 'default::C',\n                    'properties': [\n                        {\n                            'name': 'val',\n                            'annotations': [\n                                {\n                                    'name': 'std::title',\n                                    '@value': 'val',\n                                },\n                            ],\n                        },\n                    ],\n                    'links': []\n                },\n                {\n                    'name': 'default::D',\n                    'properties': [],\n                    'links': [\n                        {\n                            'name': 'multi_link',\n                            'annotations': [\n                                {\n                                    'name': 'std::title',\n                                    '@value': 'multi link to C',\n                                },\n                            ],\n                        },\n                        {\n                            'name': 'single_link',\n                            'annotations': [\n                                {\n                                    'name': 'std::title',\n                                    '@value': 'single link to C',\n                                },\n                            ],\n                        },\n                    ]\n                }\n            ]\n        )\n\n        # check that all link prop annotations are in place\n        await self.assert_query_result(\n            r'''\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    links: {\n                        name,\n                        properties: {\n                            name,\n                            annotations: {\n                                name,\n                                @value,\n                            },\n                        }\n                        FILTER EXISTS .annotations\n                        ORDER BY .name,\n                    } # keep only links with user-annotated props\n                    FILTER 'std::title' IN .properties.annotations.name\n                    ORDER BY .name,\n                }\n                FILTER\n                    .name = 'default::E'\n                ORDER BY .name;\n            ''',\n            [\n                {\n                    'name': 'default::E',\n                    'links': [\n                        {\n                            'name': 'multi_link',\n                            'properties': [\n                                {\n                                    'name': 'lp1',\n                                    'annotations': [\n                                        {\n                                            'name': 'std::title',\n                                            '@value': 'single lp1',\n                                        },\n                                    ],\n                                },\n                            ]\n                        },\n                        {\n                            'name': 'single_link',\n                            'properties': [\n                                {\n                                    'name': 'lp0',\n                                    'annotations': [\n                                        {\n                                            'name': 'std::title',\n                                            '@value': 'single lp0',\n                                        },\n                                    ],\n                                },\n                            ]\n                        },\n                    ]\n                }\n            ]\n        )\n\n        # check that all constraint annotations are in place\n        await self.assert_query_result(\n            r'''\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    properties: {\n                        name,\n                        constraints: {\n                            name,\n                            annotations: {\n                                name,\n                                @value,\n                            },\n                        },\n                    }\n                    ORDER BY .name,\n                }\n                FILTER\n                    .name = 'default::C'\n                ORDER BY .name;\n            ''',\n            [\n                {\n                    'name': 'default::C',\n                    'properties': [\n                        {\n                            'name': 'id',\n                            'constraints': [{\n                                'annotations': [],\n                            }],\n                        },\n                        {\n                            'name': 'val',\n                            'constraints': [{\n                                'annotations': [\n                                    {\n                                        'name': 'std::title',\n                                        '@value': 'exclusive C val',\n                                    },\n                                ],\n                            }],\n                        },\n                    ]\n                }\n            ]\n        )\n\n        # check that all constraint annotations are in place\n        await self.assert_query_result(\n            r'''\n                WITH MODULE schema\n                SELECT `Constraint` {\n                    name,\n                    annotations: {\n                        name,\n                        @value,\n                    },\n                    errmessage,\n                    expr,\n                }\n                FILTER\n                    .abstract\n                    AND\n                    .name LIKE 'default::%'\n                ORDER BY .name;\n            ''',\n            [\n\n                {\n                    'name': 'default::user_int_constr',\n                    'annotations': [\n                        {\n                            'name': 'std::title',\n                            '@value': 'user_int_constraint constraint',\n                        },\n                    ],\n                    'errmessage': '{__subject__} must be greater than {x}',\n                    'expr': '(__subject__ > x)',\n                },\n            ]\n        )\n\n        # check that all function annotations are in place\n        await self.assert_query_result(\n            r'''\n                WITH MODULE schema\n                SELECT Function {\n                    name,\n                    annotations: {\n                        name,\n                        @value,\n                    },\n                    vol := <str>.volatility,\n                }\n                FILTER\n                    EXISTS .annotations\n                    AND\n                    .name LIKE 'default::%'\n                ORDER BY .name;\n            ''',\n            [\n                {\n                    'name': 'default::user_func_0',\n                    'annotations': [\n                        {\n                            'name': 'std::title',\n                            '@value': 'user_func(int64) -> str',\n                        },\n                    ],\n                    'vol': 'Immutable',\n                },\n            ]\n        )\n\n        # check that indexes are in place\n        await self.assert_query_result(\n            r'''\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    indexes: {\n                        expr\n                    },\n                }\n                FILTER\n                    EXISTS .indexes\n                    AND\n                    .name LIKE 'default::%'\n                ORDER BY .name;\n            ''',\n            [\n                {\n                    'name': 'default::K',\n                    'indexes': [{'expr': '.k'}],\n                },\n                {\n                    'name': 'default::L',\n                    'indexes': [{'expr': '(.l0 ++ .l1)'}],\n                }\n            ]\n        )\n\n        # check the custom scalars\n        await self.assert_query_result(\n            r'''\n                WITH MODULE schema\n                SELECT ScalarType {\n                    name,\n                    ancestors: {\n                        name,\n                    } ORDER BY @index,\n                    constraints: {\n                        name,\n                        params: {\n                            name,\n                            @value,\n                        } FILTER .name != '__subject__',\n                    },\n                }\n                FILTER\n                    .name LIKE 'default::User%'\n                ORDER BY .name;\n            ''',\n            [\n                {\n                    'name': 'default::UserEnum',\n                    'ancestors': [\n                        {'name': 'std::anyenum'},\n                        {'name': 'std::anyscalar'},\n                    ],\n                    'constraints': [],\n                },\n                {\n                    'name': 'default::UserInt',\n                    'ancestors': [\n                        {'name': 'std::int64'},\n                        {'name': 'std::anyint'},\n                        {'name': 'std::anyreal'},\n                        {'name': 'std::anydiscrete'},\n                        {'name': 'std::anypoint'},\n                        {'name': 'std::anyscalar'},\n                    ],\n                    'constraints': [\n                        {\n                            'name': 'default::user_int_constr',\n                            'params': [{'name': 'x', '@value': '5'}],\n                        },\n                    ],\n                },\n                {\n                    'name': 'default::UserStr',\n                    'ancestors': [\n                        {'name': 'std::str'},\n                        {'name': 'std::anyscalar'}\n                    ],\n                    'constraints': [\n                        {\n                            'name': 'std::max_len_value',\n                            'params': [{'name': 'max', '@value': '5'}],\n                        },\n                    ],\n                },\n            ]\n        )\n\n        # check the custom scalars\n        await self.assert_query_result(\n            r'''\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    properties: {\n                        name,\n                        constraints: {\n                            name,\n                            params: {\n                                name,\n                                @value,\n                            } FILTER .name != '__subject__',\n                        },\n                    }\n                    FILTER .name IN {'m0', 'm1'}\n                    ORDER BY .name,\n                }\n                FILTER\n                    .name = 'default::M';\n            ''',\n            [\n                {\n                    'name': 'default::M',\n                    'properties': [\n                        {\n                            'name': 'm0',\n                            'constraints': [\n                                {\n                                    'name': 'default::user_int_constr',\n                                    'params': [{\n                                        'name': 'x',\n                                        '@value': '3'\n                                    }],\n                                },\n                            ],\n                        },\n                        {\n                            'name': 'm1',\n                            'constraints': [\n                                {\n                                    'name': 'std::max_len_value',\n                                    'params': [{\n                                        'name': 'max',\n                                        '@value': '3'\n                                    }],\n                                },\n                            ],\n                        }\n                    ],\n                },\n            ]\n        )\n\n        # check the custom scalars\n        await self.assert_query_result(\n            r'''\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    properties: {\n                        name,\n                        target: {\n                            name,\n                        },\n                    }\n                    FILTER .name IN {'n0', 'n1'}\n                    ORDER BY .name,\n                }\n                FILTER\n                    .name = 'default::N';\n            ''',\n            [\n                {\n                    'name': 'default::N',\n                    'properties': [\n                        {\n                            'name': 'n0',\n                            'target': {'name': 'default::UserInt'},\n                        },\n                        {\n                            'name': 'n1',\n                            'target': {'name': 'default::UserStr'},\n                        },\n                    ],\n                },\n            ]\n        )\n\n        # check that the bases and ancestors order is preserved\n        await self.assert_query_result(\n            r'''\n            WITH MODULE schema\n            SELECT ObjectType {\n                name,\n                bases: {\n                    name,\n                    @index,\n                } ORDER BY @index,\n                ancestors: {\n                    name,\n                    @index,\n                } ORDER BY @index,\n            }\n            FILTER\n                .name = 'default::V';\n            ''',\n            [\n                {\n                    'name': 'default::V',\n                    'bases': [\n                        {'name': 'default::U', '@index': 0},\n                        {'name': 'default::S', '@index': 1},\n                        {'name': 'default::T', '@index': 2},\n                    ],\n                    'ancestors': [\n                        {'name': 'default::U', '@index': 0},\n                        {'name': 'default::S', '@index': 1},\n                        {'name': 'default::T', '@index': 2},\n                        {'name': 'default::R', '@index': 3},\n                        {'name': 'std::Object', '@index': 4},\n                        {'name': 'std::BaseObject', '@index': 5},\n                    ],\n                }\n            ]\n        )\n\n        # check delegated constraint\n        await self.assert_query_result(\n            r'''\n            WITH MODULE schema\n            SELECT ObjectType {\n                name,\n                properties: {\n                    name,\n                    constraints: {\n                        name,\n                        delegated,\n                    },\n                } ORDER BY .name,\n            }\n            FILTER\n                .name = 'default::R'\n                OR\n                .name = 'default::S'\n            ORDER BY .name;\n            ''',\n            [\n\n                {\n                    'name': 'default::R',\n                    'properties': [\n                        {\n                            'name': 'id',\n                            'constraints': [\n                                {\n                                    'name': 'std::exclusive',\n                                    'delegated': False,\n                                }\n                            ],\n                        },\n                        {\n                            'name': 'name',\n                            'constraints': [\n                                {\n                                    'name': 'std::exclusive',\n                                    'delegated': True,\n                                }\n                            ],\n                        },\n                    ],\n                },\n                {\n                    'name': 'default::S',\n                    'properties': [\n                        {\n                            'name': 'id',\n                            'constraints': [\n                                {\n                                    'name': 'std::exclusive',\n                                    'delegated': False,\n                                }\n                            ],\n                        },\n                        {\n                            'name': 'name',\n                            'constraints': [\n                                {\n                                    'name': 'std::exclusive',\n                                    'delegated': False,\n                                }\n                            ],\n                        },\n                        {\n                            'name': 's',\n                            'constraints': [],\n                        },\n                    ],\n                }\n\n            ]\n        )\n\n    async def _ensure_data_integrity(self):\n        # validate single props for all basic scalar types\n        await self.assert_query_result(\n            r'''\n            SELECT A {\n                p_bool,\n                p_str,\n                p_int16,\n                p_int32,\n                p_int64,\n                p_float32,\n                p_float64,\n                p_bigint,\n                p_decimal,\n            };\n            ''',\n            [{\n                'p_bool': True,\n                'p_str': 'Hello',\n                'p_int16': 12345,\n                'p_int32': 1234567890,\n                'p_int64': 1234567890123,\n                'p_float32': 2.5,\n                'p_float64': 2.5,\n                'p_bigint': 123456789123456789123456789,\n                'p_decimal':\n                    123456789123456789123456789.123456789123456789123456789,\n            }]\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT (\n                <str>A.p_datetime,\n                <str>A.p_local_datetime,\n                <str>A.p_local_date,\n                <str>A.p_local_time,\n                <str>A.p_duration,\n            );\n            ''',\n            [[\n                '2018-05-07T20:01:22.306916+00:00',\n                '2018-05-07T20:01:22.306916',\n                '2018-05-07',\n                '20:01:22.306916',\n                'PT20H',\n            ]]\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT A.p_json;\n            ''',\n            [[{\"a\": None, \"b\": True}, 1, 2.5, \"foo\"]],\n            ['[{\"a\": null, \"b\": true}, 1, 2.5, \"foo\"]'],\n        )\n\n        # validate multi props for all basic scalar types\n        await self.assert_query_result(\n            r'''\n            SELECT B {\n                p_bool,\n                p_str,\n                p_int16,\n                p_int32,\n                p_int64,\n                p_float32,\n                p_float64,\n                p_bigint,\n                p_decimal,\n            };\n            ''',\n            [{\n                'p_bool': {True, False},\n                'p_str': {'Hello', 'world'},\n                'p_int16': {12345, -42},\n                'p_int32': {1234567890, -42},\n                'p_int64': {1234567890123, -42},\n                'p_float32': {2.5, -42},\n                'p_float64': {2.5, -42},\n                'p_bigint': {123456789123456789123456789, -42},\n                'p_decimal': {\n                    123456789123456789123456789.123456789123456789123456789,\n                    -42,\n                },\n            }]\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT B {\n                str_datetime := <str>.p_datetime,\n                str_local_datetime := <str>.p_local_datetime,\n                str_local_date := <str>.p_local_date,\n                str_local_time := <str>.p_local_time,\n                str_duration := <str>.p_duration,\n            };\n            ''',\n            [{\n                'str_datetime': {\n                    '2018-05-07T20:01:22.306916+00:00',\n                    '2019-05-07T20:01:22.306916+00:00',\n                },\n                'str_local_datetime': {\n                    '2018-05-07T20:01:22.306916',\n                    '2019-05-07T20:01:22.306916',\n                },\n                'str_local_date': {\n                    '2018-05-07',\n                    '2019-05-07',\n                },\n                'str_local_time': {\n                    '20:01:22.306916',\n                    '20:02:22.306916',\n                },\n                'str_duration': {\n                    'PT20H',\n                    'PT20S',\n                },\n            }]\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT B.p_json\n            ORDER BY B.p_json;\n            ''',\n            [\n                \"bar\",\n                False,\n                [{\"a\": None, \"b\": True}, 1, 2.5, \"foo\"],\n            ],\n            [\n                '\"bar\"',\n                'false',\n                '[{\"a\": null, \"b\": true}, 1, 2.5, \"foo\"]',\n            ],\n        )\n\n        # bytes don't play nice with being cast into other types, so\n        # we want to test them using binary fetch\n        self.assertEqual(\n            await self.con.query(r'SELECT A.p_bytes;'),\n            edgedb.Set((b'Hello',))\n        )\n        self.assertEqual(\n            await self.con.query(r'SELECT B.p_bytes ORDER BY B.p_bytes;'),\n            edgedb.Set((b'Hello', b'world'))\n        )\n\n        # validate the data for types used to test links\n        await self.assert_query_result(\n            r'''\n            SELECT C {val}\n            ORDER BY .val;\n            ''',\n            [\n                {'val': 'D00'},\n                {'val': 'D01'},\n                {'val': 'D02'},\n                {'val': 'D03'},\n                {'val': 'E00'},\n                {'val': 'E01'},\n                {'val': 'E02'},\n                {'val': 'E03'},\n                {'val': 'F00'},\n                {'val': 'F01'},\n                {'val': 'F02'},\n                {'val': 'F03'},\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT D {\n                num,\n                single_link: {\n                    val,\n                },\n                multi_link: {\n                    val,\n                } ORDER BY .val,\n            }\n            FILTER .__type__.name = 'default::D'\n            ORDER BY .num;\n            ''',\n            [\n                {\n                    'num': 0,\n                    'single_link': None,\n                    'multi_link': [],\n                },\n                {\n                    'num': 1,\n                    'single_link': {'val': 'D00'},\n                    'multi_link': [],\n                },\n                {\n                    'num': 2,\n                    'single_link': None,\n                    'multi_link': [\n                        {'val': 'D01'}, {'val': 'D02'},\n                    ],\n                },\n                {\n                    'num': 3,\n                    'single_link': {'val': 'D00'},\n                    'multi_link': [\n                        {'val': 'D01'}, {'val': 'D02'}, {'val': 'D03'},\n                    ],\n                },\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT E {\n                num,\n                single_link: {\n                    val,\n                },\n                multi_link: {\n                    val,\n                } ORDER BY .val,\n            }\n            ORDER BY .num;\n            ''',\n            [\n                {\n                    'num': 4,\n                    'single_link': None,\n                    'multi_link': [],\n                },\n                {\n                    'num': 5,\n                    'single_link': {'val': 'E00'},\n                    'multi_link': [],\n                },\n                {\n                    'num': 6,\n                    'single_link': None,\n                    'multi_link': [\n                        {'val': 'E01'}, {'val': 'E02'},\n                    ],\n                },\n                {\n                    'num': 7,\n                    'single_link': {'val': 'E00'},\n                    'multi_link': [\n                        {'val': 'E01'}, {'val': 'E02'}, {'val': 'E03'},\n                    ],\n                },\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT F {\n                num,\n                single_link: {\n                    val,\n                },\n                multi_link: {\n                    val,\n                } ORDER BY .val,\n            }\n            ORDER BY .num;\n            ''',\n            [\n                {\n                    'num': 8,\n                    'single_link': {'val': 'F00'},\n                    'multi_link': [\n                        {'val': 'F01'}, {'val': 'F02'}, {'val': 'F03'},\n                    ],\n                },\n            ],\n        )\n\n        # validate link prop values\n        await self.assert_query_result(\n            r'''\n            SELECT E {\n                num,\n                single_link: {\n                    val,\n                    @lp0,\n                },\n                multi_link: {\n                    val,\n                    @lp1,\n                } ORDER BY .val,\n            } ORDER BY .num;\n            ''',\n            [\n                {\n                    'num': 4,\n                    'single_link': None,\n                    'multi_link': [],\n                },\n                {\n                    'num': 5,\n                    'single_link': {\n                        'val': 'E00',\n                        '@lp0': None,\n                    },\n                    'multi_link': [],\n                },\n                {\n                    'num': 6,\n                    'single_link': None,\n                    'multi_link': [\n                        {\n                            'val': 'E01',\n                            '@lp1': None,\n                        },\n                        {\n                            'val': 'E02',\n                            '@lp1': None,\n                        },\n                    ],\n                },\n                {\n                    'num': 7,\n                    'single_link': {\n                        'val': 'E00',\n                        '@lp0': 'E00',\n                    },\n                    'multi_link': [\n                        {\n                            'val': 'E01',\n                            '@lp1': 'E01',\n                        },\n                        {\n                            'val': 'E02',\n                            '@lp1': 'E02',\n                        },\n                        {\n                            'val': 'E03',\n                            '@lp1': 'E03',\n                        },\n                    ],\n                },\n            ],\n        )\n\n        # validate existence of data for types with computables and defaults\n        await self.assert_query_result(\n            r'''\n            SELECT K {\n                k,\n            };\n            ''',\n            [\n                {\n                    'k': 'k0',\n                },\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT L {\n                l0,\n                l1,\n            };\n            ''',\n            [\n                {\n                    'l0': 'l0_0',\n                    'l1': 'l1_0',\n                },\n            ],\n        )\n\n        # validate existence of data for indexed types\n        await self.assert_query_result(\n            r'''\n            SELECT G {g0, g1, g2};\n            ''',\n            [\n                {'g0': 'fixed', 'g1': 'func1', 'g2': '2'},\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT H {h0, h1, h2};\n            ''',\n            [\n                {'h0': 'fixed', 'h1': 'func1', 'h2': '2'},\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT I {\n                i0: {val},\n                i1: {val},\n                i2: {val},\n            };\n            ''',\n            [\n                {\n                    'i0': {'val': 'D00'},\n                    'i1': {'val': 'D01'},\n                    'i2': {'val': 'D02'},\n                }\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT J {\n                j0: {val},\n                j1: {val},\n                j2: {val},\n            };\n            ''',\n            [\n                {\n                    'j0': {'val': 'D00'},\n                    'j1': {'val': 'D01'},\n                    'j2': {'val': 'D02'},\n                }\n            ],\n        )\n\n        # validate existence of data for types with constraints\n        await self.assert_query_result(\n            r'''\n            SELECT M {\n                m0,\n                m1,\n            };\n            ''',\n            [\n                {\n                    'm0': 10,\n                    'm1': 'm1',\n                },\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT N {\n                n0,\n                n1,\n            };\n            ''',\n            [\n                {\n                    'n0': 10,\n                    'n1': 'n1',\n                },\n            ],\n        )\n\n        # validate user functions\n        await self.assert_query_result(\n            r'''\n            SELECT user_func_0(99);\n            ''',\n            ['func99'],\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT user_func_1([1, 3, -88], '+');\n            ''',\n            ['1+3+-88'],\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT user_func_2(<int64>{});\n            ''',\n            {'x'},\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT user_func_2(11);\n            ''',\n            {'11', 'x'},\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT user_func_2(22, 'a');\n            ''',\n            {'22', 'a'},\n        )\n\n        # validate user enum\n        await self.assert_query_result(\n            r'''\n            WITH w := {'Lorem', 'ipsum', 'dolor', 'sit', 'amet'}\n            SELECT w\n            ORDER BY str_lower(w);\n            ''',\n            ['amet', 'dolor', 'ipsum', 'Lorem', 'sit'],\n        )\n\n        await self.assert_query_result(\n            r'''\n            WITH w := {'Lorem', 'ipsum', 'dolor', 'sit', 'amet'}\n            SELECT w\n            ORDER BY <UserEnum>w;\n            ''',\n            # the enum ordering is not like str, but like the real phrase\n            ['Lorem', 'ipsum', 'dolor', 'sit', 'amet'],\n        )\n\n        # validate user enum\n        await self.assert_query_result(\n            r'''\n            SELECT <str>{O.o0, O.o1, O.o2};\n            ''',\n            {'ipsum', 'Lorem', 'dolor'},\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT <str>(\n                SELECT _ := {O.o0, O.o1, O.o2}\n                ORDER BY _\n            );\n            ''',\n            [\n                'Lorem', 'ipsum', 'dolor',\n            ]\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT {O.o0, O.o1, O.o2} IS UserEnum;\n            ''',\n            [True, True, True],\n        )\n\n        # validate collection properties\n        await self.assert_query_result(\n            r'''\n            SELECT P {\n                plink0: {val, @p0},\n                plink1: {val, @p1},\n                p2,\n                p3,\n            };\n            ''',\n            [\n                {\n                    'plink0': {'val': 'E00', '@p0': ['hello', 'world']},\n                    'plink1': {'val': 'E00', '@p1': [2.5, -4.25]},\n                    'p2': ['hello', 'world'],\n                    'p3': [2.5, -4.25]\n                }\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT Q {q0, q1, q2, q3};\n            ''',\n            [\n                {\n                    'q0': [2, False],\n                    'q1': ['p3', 3.33],\n                    'q2': {'x': 2, 'y': False},\n                    'q3': {'x': 'p11', 'y': 3.33},\n                }\n            ],\n        )\n\n        # validate multiple inheritance\n        await self.assert_query_result(\n            r'''\n            SELECT S {name, s}\n            ORDER BY .name;\n            ''',\n            [\n                {'name': 'name0', 's': 's0'},\n                {'name': 'name1', 's': 's1'},\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT T {name, t}\n            ORDER BY .name;\n            ''',\n            [\n                {'name': 'name0', 't': 't0'},\n                {'name': 'name1', 't': 't1'},\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT V {name, s, t, u};\n            ''',\n            [\n                {\n                    'name': 'name1',\n                    's': 's1',\n                    't': 't1',\n                    'u': 'u1',\n                },\n            ],\n        )\n\n        # validate aliases\n        await self.assert_query_result(\n            r'''\n            SELECT Primes;\n            ''',\n            {2, 3, 5, 7},\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT AliasP {\n                name,\n                plink0: {val, @p0},\n                plink1: {val, @p1},\n                p2,\n                p3,\n                f: {\n                    num,\n                    single_link: {val},\n                    multi_link: {val} ORDER BY .val,\n                    k: {k},\n                },\n            };\n            ''',\n            [\n                {\n                    'name': 'alias P',\n                    'plink0': {'val': 'E00', '@p0': ['hello', 'world']},\n                    'plink1': {'val': 'E00', '@p1': [2.5, -4.25]},\n                    'p2': ['hello', 'world', '!'],\n                    'p3': [2.5, -4.25],\n                    'f': [\n                        {\n                            'num': 8,\n                            'single_link': {'val': 'F00'},\n                            'multi_link': [\n                                {'val': 'F01'},\n                                {'val': 'F02'},\n                                {'val': 'F03'},\n                            ],\n                            'k': {'k': 'k0'},\n                        },\n                    ],\n                }\n            ],\n        )\n\n        # validate self/mutually-referencing types\n        await self.assert_query_result(\n            r'''\n            SELECT W {\n                name,\n                w: {\n                    name\n                }\n            }\n            ORDER BY .name;\n            ''',\n            [\n                {'name': 'w0', 'w': None},\n                {'name': 'w1', 'w': {'name': 'w2'}},\n                {'name': 'w2', 'w': None},\n                {'name': 'w3', 'w': {'name': 'w4'}},\n                {'name': 'w4', 'w': {'name': 'w3'}},\n            ],\n        )\n\n        # validate self/mutually-referencing types\n        await self.assert_query_result(\n            r'''\n            SELECT X {\n                name,\n                y: {\n                    name,\n                    x: {\n                        name\n                    }\n                }\n            }\n            ORDER BY .name;\n            ''',\n            [\n                {\n                    'name': 'x0',\n                    'y': {\n                        'name': 'y0',\n                        'x': {\n                            'name': 'x0',\n                        },\n                    },\n                },\n            ],\n        )\n\n        # validate self/mutually-referencing types\n        await self.assert_query_result(\n            r'''\n            SELECT Z {\n                ck: {\n                    typename := .__type__.name,\n                },\n                stw: {\n                    name,\n                    typename := .__type__.name,\n                } ORDER BY .typename,\n            }\n            ORDER BY .ck.typename;\n            ''',\n            [\n                {\n                    'ck': {'typename': 'default::C'},\n                    'stw': [\n                        {'name': 'name0', 'typename': 'default::S'}\n                    ],\n                },\n                {\n                    'ck': {'typename': 'default::K'},\n                    'stw': [\n                        {'name': 'name0', 'typename': 'default::S'},\n                        {'name': 'name0', 'typename': 'default::T'},\n                        {'name': 'w1', 'typename': 'default::W'},\n                    ],\n                },\n            ],\n        )\n\n        # validate cross module types\n        await self.assert_query_result(\n            r'''\n            SELECT DefA {a};\n            ''',\n            [\n                {\n                    'a': 'DefA',\n                },\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT DefB {\n                name,\n                other: {\n                    b,\n                    blink: {\n                        a\n                    }\n                }\n            };\n            ''',\n            [\n                {\n                    'name': 'test0',\n                    'other': {\n                        'b': 'TestB',\n                        'blink': {\n                            'a': 'DefA',\n                        },\n                    },\n                },\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n            SELECT DefC {\n                name,\n                other: {\n                    c,\n                    clink: {\n                        name\n                    }\n                }\n            };\n            ''',\n            [\n                {\n                    'name': 'test1',\n                    'other': {\n                        'c': 'TestC',\n                        'clink': {\n                            'name': 'test1',\n                        },\n                    },\n                },\n            ],\n        )\n\n        # validate on delete settings\n        await self.assert_query_result(\n            r'''\n            SELECT SourceA {\n                name,\n                link1: {\n                    name,\n                },\n            }\n            FILTER .name = 's1';\n            ''',\n            [\n                {\n                    'name': 's1',\n                    'link1': {\n                        'name': 't1',\n                    },\n                },\n            ],\n        )\n\n        await self.con.execute(r'DELETE TargetA FILTER .name = \"t1\"')\n\n        await self.assert_query_result(\n            r'''\n            SELECT SourceA {name}\n            FILTER .name = 's1';\n            ''',\n            [],\n        )\n\n        # validate on delete settings\n        await self.assert_query_result(\n            r'''\n            SELECT SourceA {\n                name,\n                link2: {\n                    name,\n                },\n            }\n            FILTER .name = 's2';\n            ''',\n            [\n                {\n                    'name': 's2',\n                    'link2': {\n                        'name': 't2',\n                    },\n                },\n            ],\n        )\n\n        await self.con.execute(r'DELETE TargetA FILTER .name = \"t2\"')\n\n        await self.assert_query_result(\n            r'''\n            SELECT SourceA {\n                name,\n                link2: {\n                    name,\n                },\n            }\n            FILTER .name = 's2';\n            ''',\n            [\n                {\n                    'name': 's2',\n                    'link2': None,\n                },\n            ],\n        )\n\n        # validate on delete settings\n        await self.assert_query_result(\n            r'''\n            SELECT SourceA {\n                name,\n                link0: {\n                    name,\n                },\n            }\n            FILTER .name = 's0';\n            ''',\n            [\n                {\n                    'name': 's0',\n                    'link0': {\n                        'name': 't0',\n                    },\n                },\n            ],\n        )\n\n        with self.assertRaisesRegex(\n                edgedb.ConstraintViolationError,\n                r'prohibited by link target policy'):\n            async with self.con.transaction():\n                await self.con.execute(r'DELETE TargetA FILTER .name = \"t0\"')\n\n        # validate constraints\n        with self.assertRaisesRegex(\n                edgedb.ConstraintViolationError,\n                r'must be greater than 5'):\n            async with self.con.transaction():\n                await self.con.execute(r'SELECT <UserInt>1;')\n\n        # validate constraints\n        with self.assertRaisesRegex(\n                edgedb.ConstraintViolationError,\n                r'must be no longer than 5 characters'):\n            async with self.con.transaction():\n                await self.con.execute(r'SELECT <UserStr>\"qwerty\";')\n\n        # validate constraints\n        with self.assertRaisesRegex(\n                edgedb.ConstraintViolationError,\n                r'must be greater than 3'):\n            async with self.con.transaction():\n                await self.con.execute(r\"INSERT M {m0 := 1, m1 := '1'};\")\n\n        # validate constraints\n        with self.assertRaisesRegex(\n                edgedb.ConstraintViolationError,\n                r'must be no longer than 3 characters'):\n            async with self.con.transaction():\n                await self.con.execute(r\"INSERT M {m0 := 4, m1 := '12345'};\")\n\n        # validate constraints\n        with self.assertRaisesRegex(\n                edgedb.ConstraintViolationError,\n                r'name violates exclusivity constraint'):\n            async with self.con.transaction():\n                await self.con.execute(r\"INSERT W {name := 'w0'};\")\n\n        # validate constraints\n        with self.assertRaisesRegex(\n                edgedb.MissingRequiredError,\n                r'missing value for required property'):\n            async with self.con.transaction():\n                await self.con.execute(r\"INSERT C;\")\n\n        # validate constraints\n        with self.assertRaisesRegex(\n                edgedb.MissingRequiredError,\n                r'missing value for required link'):\n            async with self.con.transaction():\n                await self.con.execute(r\"INSERT F {num := 999};\")\n\n        # validate read-only\n        await self.assert_query_result(\n            r'''\n            SELECT ROPropsA {\n                name,\n                rop0,\n                rop1,\n            }\n            ORDER BY .name;\n            ''',\n            [\n                {\n                    'name': 'ro0',\n                    'rop0': None,\n                    'rop1': int,\n                },\n                {\n                    'name': 'ro1',\n                    'rop0': 100,\n                    'rop1': int,\n                },\n                {\n                    'name': 'ro2',\n                    'rop0': None,\n                    'rop1': -2,\n                },\n            ],\n        )\n\n        # validate read-only\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r'rop0.*read-only'):\n            async with self.con.transaction():\n                await self.con.execute(\n                    r'''\n                    UPDATE ROPropsA\n                    SET {\n                        rop0 := 99,\n                    };\n                    ''')\n\n        # validate read-only\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r'rop1.*read-only'):\n            async with self.con.transaction():\n                await self.con.execute(\n                    r'''\n                    UPDATE ROPropsA\n                    SET {\n                        rop1 := 99,\n                    };\n                    ''')\n\n        # validate read-only\n        await self.assert_query_result(\n            r'''\n            SELECT ROLinksA {\n                name,\n                rol0: {val},\n                rol1: {val},\n                rol2: {val} ORDER BY .val,\n            }\n            ORDER BY .name;\n            ''',\n            [\n                {\n                    'name': 'ro0',\n                    'rol0': None,\n                    'rol1': {'val': 'D00'},\n                    'rol2': [{'val': 'D01'}, {'val': 'D02'}]\n                },\n                {\n                    'name': 'ro1',\n                    'rol0': {'val': 'F00'},\n                    'rol1': {'val': 'D00'},\n                    'rol2': [{'val': 'D01'}, {'val': 'D02'}],\n                },\n                {\n                    'name': 'ro2',\n                    'rol0': None,\n                    'rol1': {'val': 'F00'},\n                    'rol2': [{'val': 'D01'}, {'val': 'D02'}]\n                },\n                {\n                    'name': 'ro3',\n                    'rol0': None,\n                    'rol1': {'val': 'D00'},\n                    'rol2': [{'val': 'F01'}, {'val': 'F02'}]\n                },\n            ],\n        )\n\n        # validate read-only\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r'rol0.*read-only'):\n            async with self.con.transaction():\n                await self.con.execute(\n                    r'''\n                    UPDATE ROLinksA\n                    SET {\n                        rol0 := <C>{},\n                    };\n                    ''')\n\n        # validate read-only\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r'rol1.*read-only'):\n            async with self.con.transaction():\n                await self.con.execute(\n                    r'''\n                    UPDATE ROLinksA\n                    SET {\n                        rol1 := <C>{},\n                    };\n                    ''')\n\n        # validate read-only\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r'rol2.*read-only'):\n            async with self.con.transaction():\n                await self.con.execute(\n                    r'''\n                    UPDATE ROLinksA\n                    SET {\n                        rol2 := <C>{},\n                    };\n                    ''')\n\n        # validate read-only\n        await self.assert_query_result(\n            r'''\n            SELECT ROLinksB {\n                name,\n                rol0: {val, @rolp00, @rolp01},\n                rol1: {val, @rolp10, @rolp11} ORDER BY .val,\n            }\n            ORDER BY .name;\n            ''',\n            [\n                {\n                    'name': 'ro0',\n                    'rol0': {'val': 'D00', '@rolp00': None, '@rolp01': int},\n                    'rol1': [\n                        {'val': 'D01', '@rolp10': None, '@rolp11': int},\n                        {'val': 'D02', '@rolp10': None, '@rolp11': int},\n                    ],\n                },\n                {\n                    'name': 'ro1',\n                    'rol0': {'val': 'D00', '@rolp00': 99, '@rolp01': int},\n                    'rol1': [\n                        {'val': 'D01', '@rolp10': 99, '@rolp11': int},\n                        {'val': 'D02', '@rolp10': 98, '@rolp11': int},\n                    ],\n                },\n                {\n                    'name': 'ro2',\n                    'rol0': {'val': 'E00', '@rolp00': None, '@rolp01': -10},\n                    'rol1': [\n                        {'val': 'E01', '@rolp10': None, '@rolp11': -1},\n                        {'val': 'E02', '@rolp10': None, '@rolp11': -2},\n                    ],\n                },\n            ],\n        )\n\n        \"\"\"XXX: uncomment the below once direct link property updates are\n                implemented\n        # validate read-only\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r'rolp00.*read-only'):\n            async with self.con.transaction():\n                await self.con.execute(\n                    r'''\n                    UPDATE ROLinksB\n                    SET {\n                        rol0: {@rolp00 := 1},\n                    };\n                    ''')\n\n        # validate read-only\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r'rolp01.*read-only'):\n            async with self.con.transaction():\n                await self.con.execute(\n                    r'''\n                    UPDATE ROLinksB\n                    SET {\n                        rol0: {@rolp01 := 1},\n                    };\n                    ''')\n\n        # validate read-only\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r'rolp10.*read-only'):\n            async with self.con.transaction():\n                await self.con.execute(\n                    r'''\n                    UPDATE ROLinksB\n                    SET {\n                        rol1: {@rolp10 := 1},\n                    };\n                    ''')\n\n        # validate read-only\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r'rolp11.*read-only'):\n            async with self.con.transaction():\n                await self.con.execute(\n                    r'''\n                    UPDATE ROLinksB\n                    SET {\n                        rol1: {@rolp11 := 1},\n                    };\n                    ''')\n        \"\"\"\n\n\nclass TestDump01(tb.StableDumpTestCase, DumpTestCaseMixin):\n    SCHEMA_TEST = os.path.join(os.path.dirname(__file__), 'schemas',\n                               'dump01_test.esdl')\n    SCHEMA_DEFAULT = os.path.join(os.path.dirname(__file__), 'schemas',\n                                  'dump01_default.esdl')\n\n    SETUP = os.path.join(os.path.dirname(__file__), 'schemas',\n                         'dump01_setup.edgeql')\n\n    async def test_dump01_dump_restore(self):\n        await self.check_dump_restore(\n            DumpTestCaseMixin.ensure_schema_data_integrity)\n\n    async def test_dump01_branch_schema(self):\n        await self.check_branching(\n            include_data=False,\n            check_method=DumpTestCaseMixin.ensure_schema_data_integrity)\n\n    async def test_dump01_branch_data(self):\n        await self.check_branching(\n            include_data=True,\n            check_method=DumpTestCaseMixin.ensure_schema_data_integrity)\n\n    async def test_dump01_future_scope(self):\n        await self.con.execute('''\n            create future _scoping_noop_test;\n        ''')\n\n\nclass TestDump01Compat(\n    tb.DumpCompatTestCase,\n    DumpTestCaseMixin,\n    dump_subdir='dump01',\n    check_method=DumpTestCaseMixin.ensure_schema_data_integrity,\n):\n    pass\n"
  },
  {
    "path": "tests/test_dump02.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport os.path\n\nfrom edb.testbase import server as tb\n\n\nclass DumpTestCaseMixin:\n\n    async def ensure_schema_data_integrity(self, include_data=True):\n        async for tx in self._run_and_rollback_retrying():\n            async with tx:\n                await self._ensure_schema_integrity()\n                if include_data:\n                    await self._ensure_data_integrity()\n\n    async def _ensure_schema_integrity(self):\n        # Check that index exists\n        await self.assert_query_result(\n            r'''\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    indexes: {\n                        expr,\n                    },\n                    properties: {\n                        name,\n                        default,\n                    } FILTER .name != 'id',\n                } FILTER .name = 'default::Łukasz';\n            ''',\n            [\n                {\n                    'name': 'default::Łukasz',\n                    'indexes': [{\n                        'expr': '.`Ł🤞`'\n                    }],\n                }\n            ]\n        )\n\n        # Check that scalar types exist\n        await self.assert_query_result(\n            r'''\n                WITH MODULE schema\n                SELECT (\n                    SELECT ScalarType {\n                        name,\n                    } FILTER .name LIKE 'default%'\n                ).name;\n            ''',\n            {\n                'default::你好',\n                'default::مرحبا',\n                'default::🚀🚀🚀',\n            }\n        )\n\n        # Check that abstract constraint exists\n        await self.assert_query_result(\n            r'''\n                WITH MODULE schema\n                SELECT Constraint {\n                    name,\n                } FILTER .name LIKE 'default%' AND .abstract;\n            ''',\n            [\n                {'name': 'default::🚀🍿'},\n            ]\n        )\n\n        # Check that abstract constraint was applied properly\n        await self.assert_query_result(\n            r'''\n                WITH MODULE schema\n                SELECT Constraint {\n                    name,\n                    params: {\n                        @value\n                    } FILTER .num > 0\n                }\n                FILTER\n                    .name = 'default::🚀🍿' AND\n                    NOT .abstract AND\n                    Constraint.<constraints[IS ScalarType].name =\n                        'default::🚀🚀🚀';\n            ''',\n            [\n                {\n                    'name': 'default::🚀🍿',\n                    'params': [\n                        {'@value': '100'}\n                    ]\n                },\n            ]\n        )\n\n    async def _ensure_data_integrity(self):\n        await self.assert_query_result(\n            r'''\n                SELECT A {\n                    `s p A m 🤞`: {\n                        `🚀`,\n                        c100,\n                        c101 := `💯`(`🙀` := .`🚀` + 1)\n                    }\n                }\n            ''',\n            [\n                {\n                    's p A m 🤞': {\n                        '🚀': 42,\n                        'c100': 58,\n                        'c101': 57,\n                    }\n                }\n            ]\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT Łukasz {\n                    `Ł🤞`,\n                    `Ł💯`: {\n                        @`🙀🚀🚀🚀🙀`,\n                        @`🙀مرحبا🙀`,\n                        `s p A m 🤞`: {\n                            `🚀`,\n                            c100,\n                            c101 := `💯`(`🙀` := .`🚀` + 1)\n                        }\n                    }\n                } ORDER BY .`Ł💯` EMPTY LAST\n            ''',\n            [\n                {\n                    'Ł🤞': 'simple 🚀',\n                    'Ł💯': {\n                        '@🙀🚀🚀🚀🙀': None,\n                        '@🙀مرحبا🙀': None,\n                        's p A m 🤞': {\n                            '🚀': 42,\n                            'c100': 58,\n                            'c101': 57,\n                        }\n                    }\n                },\n                {\n                    'Ł🤞': '你好🤞',\n                    'Ł💯': None,\n                },\n            ]\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT `💯💯💯`::`🚀🙀🚀`('Łink prop 🙀مرحبا🙀');\n            ''',\n            [\n                'Łink prop 🙀مرحبا🙀Ł🙀',\n            ]\n        )\n\n        # Check that annotation exists\n        await self.assert_query_result(\n            r'''\n                WITH MODULE schema\n                SELECT Function {\n                    name,\n                    annotations: {\n                        name,\n                        @value\n                    },\n                } FILTER .name = 'default::💯';\n            ''',\n            [\n                {\n                    'name': 'default::💯',\n                    'annotations': [{\n                        'name': 'default::🍿',\n                        '@value': 'fun!🚀',\n                    }]\n                }\n            ]\n        )\n\n        # Check the default value\n        await self.con.execute(r'INSERT Łukasz')\n        await self.assert_query_result(\n            r'''\n                SELECT Łukasz {\n                    `Ł🤞`,\n                } FILTER NOT EXISTS .`Ł💯`;\n            ''',\n            [\n                # We had one before and expect one more now.\n                {'Ł🤞': '你好🤞'},\n                {'Ł🤞': '你好🤞'},\n            ]\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT count(schema::Migration) >= 2\n            ''',\n            [True],\n        )\n\n\nclass TestDump02(tb.StableDumpTestCase, DumpTestCaseMixin):\n    DEFAULT_MODULE = 'test'\n\n    SCHEMA_DEFAULT = os.path.join(os.path.dirname(__file__), 'schemas',\n                                  'dump02_default.esdl')\n\n    SETUP = os.path.join(os.path.dirname(__file__), 'schemas',\n                         'dump02_setup.edgeql')\n\n    TEARDOWN = '''\n        CONFIGURE CURRENT DATABASE RESET allow_dml_in_functions;\n    '''\n\n    @classmethod\n    def get_setup_script(cls):\n        script = (\n            'CONFIGURE CURRENT DATABASE SET allow_dml_in_functions := true;\\n'\n        )\n        return script + super().get_setup_script()\n\n    async def test_dump02_dump_restore(self):\n        await self.check_dump_restore(\n            DumpTestCaseMixin.ensure_schema_data_integrity)\n\n    async def test_dump02_branch_schema(self):\n        await self.check_branching(\n            include_data=False,\n            check_method=DumpTestCaseMixin.ensure_schema_data_integrity)\n\n    async def test_dump02_branch_data(self):\n        await self.check_branching(\n            include_data=True,\n            check_method=DumpTestCaseMixin.ensure_schema_data_integrity)\n\n\nclass TestDump02Compat(\n    tb.DumpCompatTestCase,\n    DumpTestCaseMixin,\n    dump_subdir='dump02',\n    check_method=DumpTestCaseMixin.ensure_schema_data_integrity,\n):\n    @classmethod\n    def tearDownClass(cls):\n        try:\n            cls.loop.run_until_complete(cls.con.execute('''\n                CONFIGURE CURRENT DATABASE RESET allow_dml_in_functions;\n            '''))\n        finally:\n            super().tearDownClass()\n"
  },
  {
    "path": "tests/test_dump03.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport os.path\n\nfrom edb.testbase import server as tb\n\n\nclass DumpTestCaseMixin:\n\n    async def ensure_schema_data_integrity(self):\n        # We can't use _retrying here, since the sequences won't get reset.\n        # Hopefully this won't be a problem.\n        async with self._run_and_rollback():\n            await self._ensure_schema_data_integrity()\n\n    async def _ensure_schema_data_integrity(self):\n        await self.assert_query_result(\n            r'''\n                SELECT Test {\n                    array_of_tuples,\n                    tuple_of_arrays,\n                    seq,\n                } FILTER .name = 'test01'\n            ''',\n            [\n                {\n                    'array_of_tuples': [\n                        [1, '2', 3],\n                        [4, '5', 6],\n                    ],\n                    'tuple_of_arrays': [\n                        '1',\n                        ['2', '3'],\n                        [4, 5, ['6']],\n                    ],\n                    'seq': 1,\n                },\n            ]\n        )\n\n        result = await self.con.query_single(\n            \"SELECT sequence_next(INTROSPECT TYPEOF Test.seq)\"\n        )\n        self.assertEqual(result, 2)\n\n        result = await self.con.query_single(\n            \"SELECT sequence_next(INTROSPECT MyPristineSeq)\"\n        )\n        self.assertEqual(result, 1)\n\n\nclass TestDump03(tb.StableDumpTestCase, DumpTestCaseMixin):\n    DEFAULT_MODULE = 'test'\n\n    SCHEMA_DEFAULT = os.path.join(os.path.dirname(__file__), 'schemas',\n                                  'dump03_default.esdl')\n\n    SETUP = os.path.join(os.path.dirname(__file__), 'schemas',\n                         'dump03_setup.edgeql')\n\n    async def test_dump03_dump_restore(self):\n        await self.check_dump_restore(\n            DumpTestCaseMixin.ensure_schema_data_integrity)\n\n    async def test_dump03_zbranch_data(self):\n        await self.check_branching(\n            include_data=True,\n            check_method=DumpTestCaseMixin.ensure_schema_data_integrity)\n\n\nclass TestDump03Compat(\n    tb.DumpCompatTestCase,\n    DumpTestCaseMixin,\n    dump_subdir='dump03',\n    check_method=DumpTestCaseMixin.ensure_schema_data_integrity,\n):\n    pass\n"
  },
  {
    "path": "tests/test_dump_basic.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport hashlib\nimport os\nimport random\nimport tempfile\n\nfrom edb.testbase import server as tb\n\n\nclass TestDumpBasics(tb.DatabaseTestCase, tb.CLITestCaseMixin):\n    DEFAULT_MODULE = 'test'\n\n    TRANSACTION_ISOLATION = False\n\n    SETUP = '''\n        CREATE TYPE test::Tmp {\n            CREATE REQUIRED PROPERTY idx -> std::int64;\n            CREATE REQUIRED PROPERTY data -> std::bytes;\n        };\n    '''\n\n    TEARDOWN = '''\n        DROP TYPE test::Tmp;\n    '''\n\n    RANDBYTES = os.urandom(1024) * 1024\n    DBSIZE = 1024 * 1024 * 50\n\n    def some_bytes(self, nbytes):\n        buf = b''\n        while len(buf) < nbytes:\n            buf += self.RANDBYTES[:nbytes - len(buf)]\n        return buf\n\n    async def test_dump_fuzz_01(self):\n        if not self.has_create_database:\n            self.skipTest('create database is not supported by the backend')\n\n        # This test creates a simple `DBSIZE` DB filled with semi-random\n        # byte strings. While the DB is populated a hash of all byte\n        # strings is computed. The DB is then dumped and restored.\n        # A new hash computed of all byte strings in the new DB.\n        # The former and latter hashes must be the same.\n        #\n        # This test is not designed to test how well the schema is\n        # preserved or compatibility between different edgedb or\n        # dump versions. Its only purpose is to make sure that\n        # the basic dump I/O and network protocol functions correctly.\n\n        hasher = hashlib.sha1()\n\n        idx = 0\n        total_len = 0\n        while total_len < self.DBSIZE:\n            data = self.some_bytes(random.randint(100_000, 10_000_000))\n            hasher.update(data)\n            total_len += len(data)\n\n            await self.con.query_single('''\n                INSERT test::Tmp {\n                    idx := <int64>$idx,\n                    data := <bytes>$data,\n                }\n            ''', idx=idx, data=data)\n\n            idx += 1\n\n        expected_hash = hasher.digest()\n        nrows = idx\n        dbname = self.get_database_name()\n        restored_dbname = f'{dbname}_restored'\n\n        try:\n            await self.con.execute(f'CREATE DATABASE {restored_dbname}')\n            with tempfile.TemporaryDirectory() as f:\n                fname = os.path.join(f, 'dump')\n                self.run_cli('-d', f\"{dbname}\", 'dump', fname)\n                self.run_cli('-d', restored_dbname, 'restore', fname)\n            con2 = await self.connect(database=restored_dbname)\n        except Exception:\n            await tb.drop_db(self.con, restored_dbname)\n            raise\n\n        try:\n            hasher = hashlib.sha1()\n            for idx in range(nrows):\n                # We don't have cursors yet and we also don't want to fetch\n                # a huge data set in one hop; so we fetch row by row.\n                # Not ideal, but isn't too bad either.\n                r = await con2.query_single('''\n                    WITH\n                        MODULE test,\n                        A := (SELECT Tmp FILTER Tmp.idx = <int64>$idx)\n                    SELECT A.data\n                    LIMIT 1\n                ''', idx=idx)\n\n                hasher.update(r)\n\n            self.assertEqual(hasher.digest(), expected_hash)\n        finally:\n            await con2.aclose()\n            await tb.drop_db(self.con, restored_dbname)\n"
  },
  {
    "path": "tests/test_dump_v2.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport os.path\n\nimport edgedb\n\nfrom edb.testbase import server as tb\n\n\nclass DumpTestCaseMixin:\n\n    async def ensure_schema_data_integrity(self, include_data=True):\n        async for tx in self._run_and_rollback_retrying():\n            async with tx:\n                await self._ensure_schema_integrity()\n                if include_data:\n                    await self._ensure_data_integrity()\n\n    async def _ensure_schema_integrity(self):\n        # Validate access policies\n        await self.assert_query_result(\n            r'''\n            SELECT schema::ObjectType {access_policies: {name}}\n            FILTER .name = 'default::Test2';\n            ''',\n            [\n                {'access_policies': [{'name': 'test'}]},\n            ],\n        )\n\n        # Validate globals\n        await self.assert_query_result(\n            r'''\n            SELECT schema::Global {\n                name, tgt := .target.name, required, default\n            }\n            FILTER NOT .name LIKE 'sys::%'\n            ORDER BY .name\n            ''',\n            [\n                {\n                    'name': 'default::bar',\n                    'tgt': 'std::int64',\n                    'required': True,\n                    'default': '-1',\n                },\n                {\n                    'name': 'default::baz',\n                    'tgt': 'default::baz',\n                    'required': False,\n                    'default': None,\n                },\n                {\n                    'name': 'default::foo',\n                    'tgt': 'std::str',\n                    'required': False,\n                    'default': None,\n                },\n            ],\n        )\n\n    async def _ensure_data_integrity(self):\n        # Test that on source delete all work correctly still\n        await self.con.execute(r'DELETE SourceA FILTER .name = \"s0\"')\n\n        await self.assert_query_result(\n            r'''\n            SELECT TargetA {name}\n            FILTER .name = 't0';\n            ''',\n            [],\n        )\n\n        # Should trigger a cascade that then causes a link policy error\n        with self.assertRaisesRegex(\n                edgedb.ConstraintViolationError,\n                r'prohibited by link target policy'):\n            async with self.con.transaction():\n                await self.con.execute(r'DELETE SourceA FILTER .name = \"s1\"')\n\n        # Shouldn't delete anything\n        await self.con.execute(r'DELETE SourceA FILTER .name = \"s3\"')\n        await self.assert_query_result(\n            r'''\n            SELECT TargetA {name}\n            FILTER .name = 't2';\n            ''',\n            [{'name': 't2'}],\n        )\n\n        # But deleting the last reamining one should\n        await self.con.execute(r'DELETE SourceA FILTER .name = \"s4\"')\n        await self.assert_query_result(\n            r'''\n            SELECT TargetA {name}\n            FILTER .name = 't2';\n            ''',\n            [],\n        )\n\n\nclass TestDumpV2(tb.StableDumpTestCase, DumpTestCaseMixin):\n    DEFAULT_MODULE = 'test'\n\n    SCHEMA_DEFAULT = os.path.join(os.path.dirname(__file__), 'schemas',\n                                  'dump_v2_default.esdl')\n\n    SETUP = os.path.join(os.path.dirname(__file__), 'schemas',\n                         'dump_v2_setup.edgeql')\n\n    async def test_dump_v2_dump_restore(self):\n        await self.check_dump_restore(\n            DumpTestCaseMixin.ensure_schema_data_integrity)\n\n    async def test_dump_v2_branch_data(self):\n        await self.check_branching(\n            include_data=True,\n            check_method=DumpTestCaseMixin.ensure_schema_data_integrity)\n\n\nclass TestDumpV2Compat(\n    tb.DumpCompatTestCase,\n    DumpTestCaseMixin,\n    dump_subdir='dumpv2',\n    check_method=DumpTestCaseMixin.ensure_schema_data_integrity,\n):\n    pass\n"
  },
  {
    "path": "tests/test_dump_v3.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport os.path\n\nfrom edb.testbase import server as tb\n\n\nclass DumpTestCaseMixin:\n\n    async def ensure_schema_data_integrity(self):\n        async for tx in self._run_and_rollback_retrying():\n            async with tx:\n                await self._ensure_schema_data_integrity()\n\n    async def _ensure_schema_data_integrity(self):\n        await self.assert_query_result(\n            r'''\n            SELECT _ := schema::Module.name\n            FILTER _ LIKE 'default%'\n            ''',\n            {'default', 'default::nested', 'default::back`ticked'},\n        )\n\n        # We don't bother to validate these but we need them to work\n        await self.con.query('describe schema as sdl')\n        await self.con.query('describe schema as ddl')\n\n        # We took a dev version snapshot for 3.0, but then needed to\n        # add more stuff to the 3.0 dump tests. It didn't seem worth\n        # adding a new dump test for it (both ergonomically and\n        # because it would be slower), so just quit early in that case.\n        if (\n            self._testMethodName\n            == 'test_dumpv3_restore_compatibility_3_0_dev_7258'\n        ):\n            return\n\n        await self.assert_query_result(\n            r'''\n            select schema::Migration { script, message, generated_by }\n            order by exists .parents then exists .parents.parents\n            limit 3\n            ''',\n            [\n                {\"message\": None, \"generated_by\": None},\n                {\"message\": \"test\", \"generated_by\": None},\n                {\"message\": None, \"generated_by\": \"DDLStatement\"},\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n            select schema::Trigger {\n                name, scope, kinds, sname := .subject.name\n            };\n            ''',\n            [\n                {\n                    \"name\": \"log\",\n                    \"scope\": \"Each\",\n                    \"kinds\": [\"Insert\"],\n                    \"sname\": \"default::Foo\"\n                }\n            ]\n        )\n        await self.assert_query_result(\n            r'''\n            select schema::Rewrite {\n                sname := .subject.source.name ++ \".\" ++ .subject.name,\n                name,\n            };\n            ''',\n            tb.bag([\n                {\"sname\": \"default::Log.timestamp\", \"name\": \"Insert\"},\n                {\"sname\": \"default::Log.timestamp\", \"name\": \"Update\"},\n            ]),\n        )\n        await self.assert_query_result(\n            r'''\n            select schema::AccessPolicy { name, errmessage }\n            filter .name = 'whatever_no';\n            ''',\n            [{\"name\": \"whatever_no\", \"errmessage\": \"aaaaaa\"}],\n        )\n\n        await self.assert_query_result(\n            r'''\n            select cfg::Config.allow_user_specified_id;\n            ''',\n            [True],\n        )\n        await self.assert_query_result(\n            r'''\n            select <str>cfg::Config.query_execution_timeout;\n            ''',\n            ['PT1H20M13S'],\n        )\n\n\nclass TestDumpV3(tb.StableDumpTestCase, DumpTestCaseMixin):\n    DEFAULT_MODULE = 'test'\n\n    SCHEMA_DEFAULT = os.path.join(os.path.dirname(__file__), 'schemas',\n                                  'dump_v3_default.esdl')\n\n    SETUP = os.path.join(os.path.dirname(__file__), 'schemas',\n                         'dump_v3_setup.edgeql')\n\n    async def test_dump_v3_dump_restore(self):\n        await self.check_dump_restore(\n            DumpTestCaseMixin.ensure_schema_data_integrity)\n\n    async def test_dump_v3_branch_data(self):\n        await self.check_branching(\n            include_data=True,\n            check_method=DumpTestCaseMixin.ensure_schema_data_integrity)\n\n\nclass TestDumpV3Compat(\n    tb.DumpCompatTestCase,\n    DumpTestCaseMixin,\n    dump_subdir='dumpv3',\n    check_method=DumpTestCaseMixin.ensure_schema_data_integrity,\n):\n    pass\n"
  },
  {
    "path": "tests/test_dump_v4.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport os.path\n\nfrom edb.testbase import server as tb\n\n\nclass DumpTestCaseMixin:\n\n    async def ensure_schema_data_integrity(self, include_secrets=False):\n        async for tx in self._run_and_rollback_retrying():\n            async with tx:\n                await self._ensure_schema_data_integrity(\n                    include_secrets=include_secrets)\n\n    async def _ensure_schema_data_integrity(self, include_secrets):\n        await self.assert_query_result(\n            r'''\n                select count(L2)\n            ''',\n            [\n                999\n            ]\n        )\n\n        await self.assert_query_result(\n            r'''\n                select all(\n                    L2.vec in (\n                        for x in range_unpack(range(1, 1000))\n                        select <v3>[x % 10, std::math::ln(x), x / 7 % 13]\n                    )\n                )\n            ''',\n            [\n                True\n            ]\n        )\n\n        # We put pgvector dump tests in v4 dump even though they\n        # shipped in 3.0-rc3 (shipping pgvector was a wild ride). It\n        # doesn't seem worth adding a second v4 dump test for (both\n        # ergonomically and because it would be slower), so just quit\n        # early in that case.\n        if (\n            self._testMethodName\n            == 'test_dumpv4_restore_compatibility_3_0'\n        ):\n            return\n\n        if include_secrets:\n            secrets = [\n                dict(name='4', value='spam', extra=None,\n                     tname='ext::_conf::SecretObj')\n            ]\n        else:\n            secrets = []\n\n        await self.assert_query_result(\n            '''\n                select cfg::Config {\n                    conf := assert_single(.extensions[is ext::_conf::Config] {\n                        config_name,\n                        objs: { name, value, [is ext::_conf::SubObj].extra,\n                                tname := .__type__.name }\n                              order by .name,\n                    })\n                };\n            ''',\n            [dict(conf=dict(\n                config_name='ready',\n                objs=[\n                    dict(name='1', value='foo', tname='ext::_conf::Obj'),\n                    dict(name='2', value='bar', tname='ext::_conf::Obj'),\n                    dict(name='3', value='baz', extra=42,\n                         tname='ext::_conf::SubObj'),\n                    *secrets,\n                ],\n            ))]\n        )\n\n        await self.assert_query_result(\n            '''\n            select ext::_conf::get_top_secret()\n            ''',\n            ['secret'] if include_secrets else [],\n        )\n\n        # __fts_document__ should be repopulated\n        await self.assert_query_result(\n            r'''\n            SELECT fts::search(L3, 'satisfying').object { x }\n            ''',\n            [\n                {\n                    'x': 'satisfied customer',\n                },\n            ],\n        )\n\n        if include_secrets:\n            await self.assert_query_result(\n                '''\n                    select cfg::Config.extensions[is ext::auth::AuthConfig] {\n                      providers: { name } order by .name\n                    };\n                ''',\n                [{\n                    'providers': [\n                        {'name': 'builtin::local_emailpassword'},\n                        {'name': 'builtin::oauth_apple'},\n                        {'name': 'builtin::oauth_azure'},\n                        {'name': 'builtin::oauth_github'},\n                        {'name': 'builtin::oauth_google'},\n                    ]\n                }]\n            )\n\n        # We didn't specify include_secrets in the dumps we made for\n        # 4.0, but the way that smtp config was done then, it got\n        # dumped anyway. (The secret wasn't specified.)\n        has_smtp = (\n            include_secrets\n            or self._testMethodName == 'test_dumpv4_restore_compatibility_4_0'\n        )\n\n        # N.B: This is not what it looked like in the original\n        # dumps. We patched it up during restore starting with 6.0.\n        if has_smtp:\n            await self.assert_query_result(\n                '''\n                select cfg::Config {\n                    email_providers[is cfg::SMTPProviderConfig]: {\n                        name, sender\n                    },\n                    current_email_provider_name,\n                };\n                ''',\n                [\n                    {\n                        \"email_providers\": [\n                            {\n                                \"name\": \"_default\",\n                                \"sender\": \"noreply@example.com\",\n                            }\n                        ],\n                        \"current_email_provider_name\": \"_default\"\n                    }\n                ],\n            )\n\n\nclass TestDumpV4(tb.StableDumpTestCase, DumpTestCaseMixin):\n    EXTENSIONS = [\"pgvector\", \"_conf\", \"pgcrypto\", \"auth\"]\n    BACKEND_SUPERUSER = True\n\n    SCHEMA_DEFAULT = os.path.join(os.path.dirname(__file__), 'schemas',\n                                  'dump_v4_default.esdl')\n\n    SETUP = os.path.join(os.path.dirname(__file__), 'schemas',\n                         'dump_v4_setup.edgeql')\n\n    async def test_dump_v4_dump_restore(self):\n        await self.check_dump_restore(\n            DumpTestCaseMixin.ensure_schema_data_integrity)\n\n    async def test_dump_v4_dump_restore_secrets(self):\n        await self.check_dump_restore(\n            lambda self: self.ensure_schema_data_integrity(\n                include_secrets=True),\n            include_secrets=True,\n        )\n\n    async def test_dump_v4_branch_data(self):\n        await self.check_branching(\n            include_data=True,\n            check_method=lambda self: self.ensure_schema_data_integrity(\n                include_secrets=True))\n\n\nclass TestDumpV4Compat(\n    tb.DumpCompatTestCase,\n    DumpTestCaseMixin,\n    dump_subdir='dumpv4',\n    check_method=DumpTestCaseMixin.ensure_schema_data_integrity,\n):\n    BACKEND_SUPERUSER = True\n"
  },
  {
    "path": "tests/test_dump_v5.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport os.path\n\nfrom edb.testbase import server as tb\n\n\nclass DumpTestCaseMixin:\n\n    async def ensure_schema_data_integrity(self, include_data=True):\n        tx = self.con.transaction()\n        await tx.start()\n        try:\n            await self._ensure_schema_integrity()\n            if include_data:\n                await self._ensure_data_integrity()\n        finally:\n            await tx.rollback()\n\n    async def _ensure_schema_integrity(self):\n        # Validate branch-level config\n        await self.assert_query_result(\n            r'''\n                select cfg::Config.extensions[is ext::pgvector::Config]\n                                  .ef_search\n            ''',\n            [\n                5\n            ]\n        )\n\n        await self.assert_query_result(\n            r'''\n                select ext::pgvector::Config.ef_search\n                filter ext::pgvector::Config.cfg is cfg::DatabaseConfig\n            ''',\n            [\n                5\n            ]\n        )\n\n        # This got added in 6.6. We add it in here as part of testing\n        # in place upgrades, which use dump tests as their core things.\n        await self.assert_query_result(\n            r'''\n                select schema::ObjectType { name }\n                filter .name = 'ext::ai::OllamaBgeM3Model';\n            ''',\n            [{}],\n        )\n\n    async def _ensure_data_integrity(self):\n        await self.assert_query_result(\n            r'''\n                select count(L2)\n            ''',\n            [\n                999\n            ]\n        )\n\n        await self.assert_query_result(\n            r'''\n                select all(\n                    L2.vec in (\n                        for x in range_unpack(range(1, 1000))\n                        select <v3>[x % 10, std::math::ln(x), x / 7 % 13]\n                    )\n                )\n            ''',\n            [\n                True\n            ]\n        )\n\n\nclass TestDumpV5(tb.StableDumpTestCase, DumpTestCaseMixin):\n    EXTENSIONS = [\"pgvector\", \"ai\"]\n    BACKEND_SUPERUSER = True\n\n    SCHEMA_DEFAULT = os.path.join(os.path.dirname(__file__), 'schemas',\n                                  'dump_v5_default.esdl')\n\n    SETUP = os.path.join(os.path.dirname(__file__), 'schemas',\n                         'dump_v5_setup.edgeql')\n\n    async def test_dump_v5_dump_restore(self):\n        await self.check_dump_restore(\n            DumpTestCaseMixin.ensure_schema_data_integrity)\n\n    async def test_dump_v5_branch_schema(self):\n        await self.check_branching(\n            include_data=False,\n            check_method=DumpTestCaseMixin.ensure_schema_data_integrity)\n\n    async def test_dump_v5_branch_data(self):\n        await self.check_branching(\n            include_data=True,\n            check_method=DumpTestCaseMixin.ensure_schema_data_integrity)\n\n\nclass TestDumpV5Compat(\n    tb.DumpCompatTestCase,\n    DumpTestCaseMixin,\n    dump_subdir='dumpv5',\n    check_method=DumpTestCaseMixin.ensure_schema_data_integrity,\n):\n    BACKEND_SUPERUSER = True\n"
  },
  {
    "path": "tests/test_dump_v6.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport os.path\n\nfrom edb.testbase import server as tb\n\n\nclass DumpTestCaseMixin:\n\n    async def ensure_schema_data_integrity(self, include_data=True):\n        tx = self.con.transaction()\n        await tx.start()\n        try:\n            await self._ensure_schema_integrity()\n            if include_data:\n                await self._ensure_data_integrity()\n        finally:\n            await tx.rollback()\n\n    async def _ensure_schema_integrity(self):\n        pass\n\n    async def _ensure_data_integrity(self):\n        async with self._run_and_rollback():\n            await self.con.execute('select insert_foo(\"test\")')\n            await self.assert_query_result(\n                r'''\n                    select Foo { name }\n                ''',\n                [{'name': \"test\"}],\n            )\n\n\nclass TestDumpV6(tb.StableDumpTestCase, DumpTestCaseMixin):\n    BACKEND_SUPERUSER = True\n\n    SCHEMA_DEFAULT = os.path.join(os.path.dirname(__file__), 'schemas',\n                                  'dump_v6_default.esdl')\n\n    SETUP = os.path.join(os.path.dirname(__file__), 'schemas',\n                         'dump_v6_setup.edgeql')\n\n    async def test_dump_v6_dump_restore(self):\n        await self.check_dump_restore(\n            DumpTestCaseMixin.ensure_schema_data_integrity)\n\n    async def test_dump_v6_branch_schema(self):\n        await self.check_branching(\n            include_data=False,\n            check_method=DumpTestCaseMixin.ensure_schema_data_integrity)\n\n    async def test_dump_v6_branch_data(self):\n        await self.check_branching(\n            include_data=True,\n            check_method=DumpTestCaseMixin.ensure_schema_data_integrity)\n\n\nclass TestDumpV6Compat(\n    tb.DumpCompatTestCase,\n    DumpTestCaseMixin,\n    dump_subdir='dumpv6',\n    check_method=DumpTestCaseMixin.ensure_schema_data_integrity,\n):\n    BACKEND_SUPERUSER = True\n"
  },
  {
    "path": "tests/test_edgeql_advtypes.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport os.path\n\nimport edgedb\n\nfrom edb.testbase import server as tb\n\n\nclass TestEdgeQLAdvancedTypes(tb.QueryTestCase):\n    '''Test type expressions'''\n\n    SCHEMA_DEFAULT = os.path.join(os.path.dirname(__file__), 'schemas',\n                                  'advtypes.esdl')\n\n    async def test_edgeql_advtypes_overlapping_union(self):\n        await self.con.execute('''\n            INSERT V {name:= 'v0', s := 's0', t := 't0', u := 'u0'};\n\n            INSERT Z {\n                name := 'z0',\n                stw0 := (\n                    SELECT V FILTER .name = 'v0'\n                ),\n            };\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Z {stw0: {name}} FILTER .name = 'z0';\n            ''',\n            [{\n                'stw0': [{'name': 'v0'}],\n            }]\n        )\n\n    async def test_edgeql_advtypes_overlapping_link_union(self):\n        await self.con.execute(\"\"\"\n            INSERT A { name := 'a1' };\n            INSERT V {\n                name:= 'v1',\n                s := 's1',\n                t := 't1',\n                u := 'u1',\n                l_a := (SELECT A FILTER .name = 'a1'),\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT (DISTINCT (SELECT S UNION T)) {\n                cla := count(.l_a)\n            }\n            \"\"\",\n            [{\n                'cla': 1,\n            }]\n        )\n\n    async def _setup_basic_data(self):\n        await self.con.execute(\"\"\"\n            INSERT CBa {ba := 'cba0'};\n            INSERT CBa {ba := 'cba1'};\n            INSERT CBb {bb := 0};\n            INSERT CBb {bb := 1};\n            INSERT CBc {bc := 0.5};\n            INSERT CBc {bc := 1.5};\n            INSERT CBaBb {ba := 'cba2', bb := 2};\n            INSERT CBaBb {ba := 'cba3', bb := 3};\n            INSERT CBaBc {ba := 'cba4', bc := 4.5};\n            INSERT CBaBc {ba := 'cba5', bc := 5.5};\n            INSERT CBbBc {bb := 6, bc := 6.5};\n            INSERT CBbBc {bb := 7, bc := 7.5};\n            INSERT CBaBbBc {ba := 'cba8', bb := 8, bc := 8.5};\n            INSERT CBaBbBc {ba := 'cba9', bb := 9, bc := 9.5};\n            INSERT XBa {ba := 'xba0'};\n            INSERT XBa {ba := 'xba1'};\n            INSERT XBb {bb := 90};\n            INSERT XBb {bb := 91};\n            INSERT XBc {bc := 90.5};\n            INSERT XBc {bc := 90.5};\n        \"\"\")\n\n    async def test_edgeql_advtypes_basic_union_01(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT (DISTINCT {Ba, Bb}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            } ORDER BY\n                .ba EMPTY LAST THEN\n                .bb EMPTY LAST THEN\n                .bc EMPTY LAST;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_basic_union_02(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT {CBaBb, CBbBc} {\n                tn := .__type__.name,\n                bb,\n            } ORDER BY .bb;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBb', 'bb': 2},\n                {'tn': 'default::CBaBb', 'bb': 3},\n                {'tn': 'default::CBbBc', 'bb': 6},\n                {'tn': 'default::CBbBc', 'bb': 7},\n            ],\n        )\n\n    async def test_edgeql_advtypes_basic_union_03(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT {CBaBb, CBaBbBc} {\n                tn := .__type__.name,\n                ba,\n                bb,\n            } ORDER BY .bb;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9},\n            ],\n        )\n\n    async def test_edgeql_advtypes_basic_intersection_01(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Ba[IS Bb].__type__.name;\n            \"\"\",\n            {'default::CBaBb', 'default::CBaBbBc'},\n        )\n\n    async def test_edgeql_advtypes_basic_intersection_02(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Ba[IS Bb].ba;\n            \"\"\",\n            {'cba2', 'cba3', 'cba8', 'cba9'},\n        )\n\n    async def test_edgeql_advtypes_basic_intersection_03(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Ba[IS Bb].bb;\n            \"\"\",\n            {2, 3, 8, 9},\n        )\n\n    async def test_edgeql_advtypes_basic_intersection_04(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Ba[IS Bb][IS Bc] {\n                tn := .__type__.name,\n                ba,\n                bb,\n                bc,\n            }\n            ORDER BY .ba;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_01(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Ba[IS Bb | Bc] {\n                tn := .__type__.name,\n                ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            ORDER BY .ba;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_02(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Ba[IS Bb & Bc] {\n                tn := .__type__.name,\n                ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            ORDER BY .ba;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_03(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Ba[IS CBa | Bb & Bc] {\n                tn := .__type__.name,\n                ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            ORDER BY .ba;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_04(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT {CBa, Ba[IS Bb & Bc]} {\n                tn := .__type__.name,\n                ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            ORDER BY .ba;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_05(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Ba[IS CBaBc | Bb][is Bc] {\n                tn := .__type__.name,\n                ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            ORDER BY .ba;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_06(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Ba[IS (CBaBc | Bb) & Bc] {\n                tn := .__type__.name,\n                ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            ORDER BY .ba;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_07(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Object[IS (Ba | Bb)][IS (Ba | Bc)] {\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            ORDER BY\n                .ba EMPTY LAST THEN\n                .bb EMPTY LAST THEN\n                .bc EMPTY LAST;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_08(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Object[IS (Ba | Bb) & (Ba | Bc)] {\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            ORDER BY\n                .ba EMPTY LAST THEN\n                .bb EMPTY LAST THEN\n                .bc EMPTY LAST;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_09(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Object[IS (Ba | Bb) | (Ba | Bc)] {\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            ORDER BY\n                .ba EMPTY LAST THEN\n                .bb EMPTY LAST THEN\n                .bc EMPTY LAST;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_10(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Object[IS (Ba & Bb) | (Ba & Bc)] {\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            ORDER BY\n                .ba EMPTY LAST THEN\n                .bb EMPTY LAST THEN\n                .bc EMPTY LAST;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_11(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT {Object[IS Ba & Bb], Object[IS Ba & Bc]} {\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            ORDER BY\n                .ba EMPTY LAST THEN\n                .bb EMPTY LAST THEN\n                .bc EMPTY LAST;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_12(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT {Ba, XBa}[is Bb | XBa] {\n                tn := .__type__.name,\n                ba,\n            }\n            ORDER BY .ba;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBb', 'ba': 'cba2'},\n                {'tn': 'default::CBaBb', 'ba': 'cba3'},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8'},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9'},\n                {'tn': 'default::XBa', 'ba': 'xba0'},\n                {'tn': 'default::XBa', 'ba': 'xba1'},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_13(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT {Ba[is Bb], XBa} {\n                tn := .__type__.name,\n                ba,\n            }\n            ORDER BY .ba;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBb', 'ba': 'cba2'},\n                {'tn': 'default::CBaBb', 'ba': 'cba3'},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8'},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9'},\n                {'tn': 'default::XBa', 'ba': 'xba0'},\n                {'tn': 'default::XBa', 'ba': 'xba1'},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_14(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Object[is (Ba & Bb) | XBa | XBb] {\n                tn := .__type__.name,\n                [is Ba | XBa].ba,\n                [is Bb | XBb].bb,\n            }\n            ORDER BY\n                .ba EMPTY LAST THEN\n                .bb EMPTY LAST;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9},\n                {'tn': 'default::XBa', 'ba': 'xba0', 'bb': None},\n                {'tn': 'default::XBa', 'ba': 'xba1', 'bb': None},\n                {'tn': 'default::XBb', 'ba': None, 'bb': 90},\n                {'tn': 'default::XBb', 'ba': None, 'bb': 91},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_15(self):\n        await self.con.execute(\"\"\"\n            INSERT A { name := 'a1' };\n            INSERT A { name := 'a2' };\n            INSERT A { name := 'a3' };\n            INSERT S { name := 'sss', s := 's', l_a := (select A) };\n            INSERT T { name := 'ttt', t := 't', l_a := (select A) };\n            INSERT V {\n                name := 'vvv',\n                s := 'u',\n                t := 'u',\n                u := 'u',\n                l_a := (select A)\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT A.<l_a[is S | T] { name } ORDER BY .name;\n            \"\"\",\n            [{'name': 'sss'}, {'name': 'ttt'}, {'name': 'vvv'}],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT A.<l_a[is S & T] { name } ORDER BY .name;\n            \"\"\",\n            [{'name': 'vvv'}],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_16(self):\n        # Testing finding path var for type intersections (#7656)\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT {CBa, Ba[is Bb]} {\n                tn := .__type__.name,\n                [IS Ba].ba,\n            }\n            ORDER BY .ba EMPTY LAST;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0'},\n                {'tn': 'default::CBa', 'ba': 'cba1'},\n                {'tn': 'default::CBaBb', 'ba': 'cba2'},\n                {'tn': 'default::CBaBb', 'ba': 'cba3'},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8'},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9'},\n            ],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT {CBa, Bb[is Ba]} {\n                tn := .__type__.name,\n                [IS Ba].ba,\n            }\n            ORDER BY .ba EMPTY LAST;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0'},\n                {'tn': 'default::CBa', 'ba': 'cba1'},\n                {'tn': 'default::CBaBb', 'ba': 'cba2'},\n                {'tn': 'default::CBaBb', 'ba': 'cba3'},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8'},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9'},\n            ],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT {Bb[is Ba], Bb[is Ba & Bc | CBaBb]} {\n                tn := .__type__.name,\n                [IS Ba].ba,\n            }\n            ORDER BY .ba EMPTY LAST;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBb', 'ba': 'cba2'},\n                {'tn': 'default::CBaBb', 'ba': 'cba2'},\n                {'tn': 'default::CBaBb', 'ba': 'cba3'},\n                {'tn': 'default::CBaBb', 'ba': 'cba3'},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8'},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8'},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9'},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9'},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_17(self):\n        # Type intersection on an alias which is not a type intersection\n        await self._setup_basic_data()\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH x := Ba\n            SELECT x[IS Bb]\n            {\n                tn := .__type__.name,\n                ba,\n                bb,\n                [IS Bc].bc,\n            }\n            \"\"\",\n            tb.bag([\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ]),\n        )\n        await self.assert_query_result(\n            r\"\"\"\n            WITH x := Ba\n            SELECT x[IS Bb & Bc]\n            {\n                tn := .__type__.name,\n                ba,\n                bb,\n                bc,\n            }\n            \"\"\",\n            tb.bag([\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ]),\n        )\n        await self.assert_query_result(\n            r\"\"\"\n            WITH x := {Ba, Bc}\n            SELECT x[IS Bb]\n            {\n                tn := .__type__.name,\n                [IS Ba].ba,\n                bb,\n                [IS Bc].bc,\n            }\n            \"\"\",\n            tb.bag([\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ]),\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH x := Ba\n            SELECT x[IS Bb].ba\n            \"\"\",\n            tb.bag(['cba2', 'cba3', 'cba8', 'cba9']),\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH x := (SELECT Bb FILTER .bb % 2 = 0)\n            SELECT x[IS Ba]\n            {\n                tn := .__type__.name,\n                ba,\n                bb,\n                [IS Bc].bc,\n            }\n            \"\"\",\n            tb.bag([\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n            ]),\n        )\n\n    async def test_edgeql_advtypes_complex_intersection_18(self):\n        # Type intersection on an alias which is a type intersection\n        await self._setup_basic_data()\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH x := Ba[IS Bb]\n            SELECT x[IS Bc]\n            {\n                tn := .__type__.name,\n                ba,\n                bb,\n                [IS Bc].bc,\n            }\n            \"\"\",\n            tb.bag([\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ]),\n        )\n        await self.assert_query_result(\n            r\"\"\"\n            WITH x := Ba[IS Bb | Bc]\n            SELECT x[IS Bb & Bc]\n            {\n                tn := .__type__.name,\n                ba,\n                bb,\n                bc,\n            }\n            \"\"\",\n            tb.bag([\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ]),\n        )\n        await self.assert_query_result(\n            r\"\"\"\n            WITH x := Object[IS Ba]\n            SELECT x[IS Bb]\n            {\n                tn := .__type__.name,\n                ba,\n                bb,\n                [IS Bc].bc,\n            }\n            \"\"\",\n            tb.bag([\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ]),\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH x := Ba[IS Bb]\n            SELECT x[IS Bc].ba\n            \"\"\",\n            tb.bag(['cba8', 'cba9']),\n        )\n\n    async def test_edgeql_advtypes_complex_polymorphism_01(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Ba {\n                tn := .__type__.name,\n                ba,\n                [is Bb & Bc].bb,\n                [is (CBaBc | Bb) & Bc].bc,\n            }\n            ORDER BY .ba;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_polymorphism_02(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Bb {\n                tn := .__type__.name,\n                bb,\n                ua := [IS Ba | Bc].bb,\n                ia := [IS Ba & Bc].bb,\n            }\n            ORDER BY .bb;\n            \"\"\",\n            [\n                {'tn': 'default::CBb', 'bb': 0, 'ua': None, 'ia': None},\n                {'tn': 'default::CBb', 'bb': 1, 'ua': None, 'ia': None},\n                {'tn': 'default::CBaBb', 'bb': 2, 'ua': 2, 'ia': None},\n                {'tn': 'default::CBaBb', 'bb': 3, 'ua': 3, 'ia': None},\n                {'tn': 'default::CBbBc', 'bb': 6, 'ua': 6, 'ia': None},\n                {'tn': 'default::CBbBc', 'bb': 7, 'ua': 7, 'ia': None},\n                {'tn': 'default::CBaBbBc', 'bb': 8, 'ua': 8, 'ia': 8},\n                {'tn': 'default::CBaBbBc', 'bb': 9, 'ua': 9, 'ia': 9},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_type_checking_01(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Object[IS Ba | Bb | Bc] {\n                tn := .__type__.name,\n                a := Object IS Ba,\n                b := Object IS Bb,\n                c := Object IS Bc,\n            }\n            ORDER BY .tn;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'a': True, 'b': False, 'c': False},\n                {'tn': 'default::CBa', 'a': True, 'b': False, 'c': False},\n                {'tn': 'default::CBaBb', 'a': True, 'b': True, 'c': False},\n                {'tn': 'default::CBaBb', 'a': True, 'b': True, 'c': False},\n                {'tn': 'default::CBaBbBc', 'a': True, 'b': True, 'c': True},\n                {'tn': 'default::CBaBbBc', 'a': True, 'b': True, 'c': True},\n                {'tn': 'default::CBaBc', 'a': True, 'b': False, 'c': True},\n                {'tn': 'default::CBaBc', 'a': True, 'b': False, 'c': True},\n                {'tn': 'default::CBb', 'a': False, 'b': True, 'c': False},\n                {'tn': 'default::CBb', 'a': False, 'b': True, 'c': False},\n                {'tn': 'default::CBbBc', 'a': False, 'b': True, 'c': True},\n                {'tn': 'default::CBbBc', 'a': False, 'b': True, 'c': True},\n                {'tn': 'default::CBc', 'a': False, 'b': False, 'c': True},\n                {'tn': 'default::CBc', 'a': False, 'b': False, 'c': True},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_type_checking_02(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Object[IS Ba | Bb | Bc] {\n                tn := .__type__.name,\n                ab := Object IS (Ba | Bb),\n                ac := Object IS (Ba | Bc),\n                bc := Object IS (Bb | Bc),\n            }\n            ORDER BY .tn;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ab': True, 'ac': True, 'bc': False},\n                {'tn': 'default::CBa', 'ab': True, 'ac': True, 'bc': False},\n                {'tn': 'default::CBaBb', 'ab': True, 'ac': True, 'bc': True},\n                {'tn': 'default::CBaBb', 'ab': True, 'ac': True, 'bc': True},\n                {'tn': 'default::CBaBbBc', 'ab': True, 'ac': True, 'bc': True},\n                {'tn': 'default::CBaBbBc', 'ab': True, 'ac': True, 'bc': True},\n                {'tn': 'default::CBaBc', 'ab': True, 'ac': True, 'bc': True},\n                {'tn': 'default::CBaBc', 'ab': True, 'ac': True, 'bc': True},\n                {'tn': 'default::CBb', 'ab': True, 'ac': False, 'bc': True},\n                {'tn': 'default::CBb', 'ab': True, 'ac': False, 'bc': True},\n                {'tn': 'default::CBbBc', 'ab': True, 'ac': True, 'bc': True},\n                {'tn': 'default::CBbBc', 'ab': True, 'ac': True, 'bc': True},\n                {'tn': 'default::CBc', 'ab': False, 'ac': True, 'bc': True},\n                {'tn': 'default::CBc', 'ab': False, 'ac': True, 'bc': True},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_type_checking_03(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Object[IS Ba | Bb | Bc] {\n                tn := .__type__.name,\n                ab := Object IS (Ba & Bb),\n                ac := Object IS (Ba & Bc),\n                bc := Object IS (Bb & Bc),\n            }\n            ORDER BY .tn;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ab': False, 'ac': False, 'bc': False},\n                {'tn': 'default::CBa', 'ab': False, 'ac': False, 'bc': False},\n                {'tn': 'default::CBaBb', 'ab': True, 'ac': False, 'bc': False},\n                {'tn': 'default::CBaBb', 'ab': True, 'ac': False, 'bc': False},\n                {'tn': 'default::CBaBbBc', 'ab': True, 'ac': True, 'bc': True},\n                {'tn': 'default::CBaBbBc', 'ab': True, 'ac': True, 'bc': True},\n                {'tn': 'default::CBaBc', 'ab': False, 'ac': True, 'bc': False},\n                {'tn': 'default::CBaBc', 'ab': False, 'ac': True, 'bc': False},\n                {'tn': 'default::CBb', 'ab': False, 'ac': False, 'bc': False},\n                {'tn': 'default::CBb', 'ab': False, 'ac': False, 'bc': False},\n                {'tn': 'default::CBbBc', 'ab': False, 'ac': False, 'bc': True},\n                {'tn': 'default::CBbBc', 'ab': False, 'ac': False, 'bc': True},\n                {'tn': 'default::CBc', 'ab': False, 'ac': False, 'bc': False},\n                {'tn': 'default::CBc', 'ab': False, 'ac': False, 'bc': False},\n            ],\n        )\n\n    async def test_edgeql_advtypes_complex_type_checking_04(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT Object[IS Ba | Bb | Bc] {\n                tn := .__type__.name,\n                u := Object IS (Ba | Bb | Bc),\n                i := Object IS (Ba & Bb & Bc),\n            }\n            ORDER BY .tn;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'u': True, 'i': False},\n                {'tn': 'default::CBa', 'u': True, 'i': False},\n                {'tn': 'default::CBaBb', 'u': True, 'i': False},\n                {'tn': 'default::CBaBb', 'u': True, 'i': False},\n                {'tn': 'default::CBaBbBc', 'u': True, 'i': True},\n                {'tn': 'default::CBaBbBc', 'u': True, 'i': True},\n                {'tn': 'default::CBaBc', 'u': True, 'i': False},\n                {'tn': 'default::CBaBc', 'u': True, 'i': False},\n                {'tn': 'default::CBb', 'u': True, 'i': False},\n                {'tn': 'default::CBb', 'u': True, 'i': False},\n                {'tn': 'default::CBbBc', 'u': True, 'i': False},\n                {'tn': 'default::CBbBc', 'u': True, 'i': False},\n                {'tn': 'default::CBc', 'u': True, 'i': False},\n                {'tn': 'default::CBc', 'u': True, 'i': False},\n            ],\n        )\n\n    async def test_edgeql_advtypes_union_narrowing_supertype(self):\n        await self.con.execute(\"\"\"\n            INSERT S { name := 'sss', s := 'sss' };\n            INSERT T { name := 'ttt', t := 'ttt' };\n            INSERT W { name := 'www' };\n            INSERT Z {\n                name := 'zzz',\n                stw0 := {S, T, W},\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH My_Z := (SELECT Z FILTER .name = 'zzz')\n            SELECT _ := My_Z.stw0[IS R].name\n            ORDER BY _\n            \"\"\",\n            [\n                'sss',\n                'ttt',\n            ]\n        )\n\n    async def test_edgeql_advtypes_union_narrowing_subtype(self):\n        await self.con.execute(\"\"\"\n            INSERT S { name := 'sss', s := 'sss' };\n            INSERT T { name := 'ttt', t := 'ttt' };\n            INSERT W { name := 'www' };\n            INSERT X { name := 'xxx', u := 'xxx_uuu' };\n            INSERT Z {\n                name := 'zzz',\n                stw0 := {S, T, W},\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH My_Z := (SELECT Z FILTER .name = 'zzz')\n            SELECT _ := My_Z.stw0[IS X].name\n            ORDER BY _\n            \"\"\",\n            [\n                'xxx',\n            ]\n        )\n\n    async def test_edgeql_advtypes_union_opaque_narrowing_subtype(self):\n        await self.con.execute(\"\"\"\n            INSERT W { name := 'www' };\n            INSERT X {\n                name := 'xxx',\n                u := 'xxx_uuu',\n                w := (SELECT DETACHED W LIMIT 1),\n            };\n            INSERT W {\n                name := 'www-2',\n                w := (SELECT (DETACHED W) FILTER .name = 'www'),\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT W {\n                w_of := .<w[IS X] {\n                    name\n                }\n            }\n            FILTER .name = 'www'\n            \"\"\",\n            [{\n                'w_of': [{\n                    'name': 'xxx',\n                }],\n            }]\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT W {\n                w_of := .<w[IS U] {\n                    u\n                }\n            }\n            FILTER .name = 'www'\n            \"\"\",\n            [{\n                'w_of': [{\n                    'u': 'xxx_uuu',\n                }],\n            }]\n        )\n\n    async def test_edgeql_advtypes_union_opaque_narrowing_nop(self):\n        await self.con.execute(\"\"\"\n            INSERT A { name := 'aaa' };\n            INSERT S { name := 'sss', s := 'sss', l_a := A };\n        \"\"\")\n\n        await self.assert_query_result(\n            'SELECT A.<l_a[IS R].name',\n            ['sss'],\n        )\n\n    async def test_edgeql_advtypes_intersection_with_comp(self):\n        await self.con.execute(\"\"\"\n            INSERT A { name := 'aaa' };\n        \"\"\")\n\n        await self.assert_query_result(\n            \"\"\"\n            WITH Rc := R\n            SELECT Rc[IS A].name\n            \"\"\",\n            ['aaa'],\n        )\n\n    async def test_edgeql_advtypes_intersection_alias(self):\n        await self.con.execute(\"\"\"\n            INSERT S { name := 'aaa', s := '' };\n            INSERT Z { name := 'lol', stw0 := S };\n        \"\"\")\n\n        await self.assert_query_result(\n            \"\"\"\n            WITH X := Z.stw0\n            SELECT X { name }\n            \"\"\",\n            [{'name': 'aaa'}],\n        )\n\n    async def test_edgeql_advtypes_intersection_semijoin_01(self):\n        await self.con.execute(\"\"\"\n            insert V {\n                name := \"x\", s := \"!\", t := \"!\", u := '...',\n                l_a := (insert A { name := \"test\" })\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            \"\"\"\n            select S[is T].l_a { name }\n            \"\"\",\n            [{\"name\": \"test\"}]\n        )\n\n        await self.assert_query_result(\n            \"\"\"\n            select S[is T] { l_a: {name} }\n            \"\"\",\n            [{\"l_a\": [{\"name\": \"test\"}]}]\n        )\n\n    async def test_edgeql_advtypes_update_complex_type_01(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            with\n                temp := (\n                    update Ba[is Bb] set {\n                        ba := .ba ++ '!',\n                        bb := .bb + 1,\n                    }\n                )\n            select temp {\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBb', 'ba': 'cba2!', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3!', 'bb': 4, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8!', 'bb': 9, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9!', 'bb': 10, 'bc': 9.5},\n            ],\n        )\n\n        # Ensure the rest of the data is unchanged\n        await self.assert_query_result(\n            r\"\"\"\n            select (DISTINCT {Ba, Bb, Bc}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba2!', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3!', 'bb': 4, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8!', 'bb': 9, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9!', 'bb': 10, 'bc': 9.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_update_complex_type_02(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            with\n                temp := (\n                    update Ba[is Bb][is Bc] set {\n                        ba := .ba ++ '!',\n                        bb := .bb + 1,\n                        bc := .bc + 0.1,\n                    }\n                )\n            select temp {\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8!', 'bb': 9, 'bc': 8.6},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9!', 'bb': 10, 'bc': 9.6},\n            ],\n        )\n\n        # Ensure the rest of the data is unchanged\n        await self.assert_query_result(\n            r\"\"\"\n            select (DISTINCT {Ba, Bb, Bc}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8!', 'bb': 9, 'bc': 8.6},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9!', 'bb': 10, 'bc': 9.6},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_update_complex_type_03(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            with\n                temp := (\n                    update Ba[is Bb | Bc] set {\n                        ba := .ba ++ '!',\n                    }\n                )\n            select temp {\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBb', 'ba': 'cba2!', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3!', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8!', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9!', 'bb': 9, 'bc': 9.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba4!', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5!', 'bb': None, 'bc': 5.5},\n            ],\n        )\n\n        # Ensure the rest of the data is unchanged\n        await self.assert_query_result(\n            r\"\"\"\n            select (DISTINCT {Ba, Bb, Bc}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba2!', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3!', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8!', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9!', 'bb': 9, 'bc': 9.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba4!', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5!', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_update_complex_type_04(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            with\n                temp := (\n                    update Ba[is Bb & Bc] set {\n                        ba := .ba ++ '!',\n                        bb := .bb + 1,\n                        bc := .bc + 0.1,\n                    }\n                )\n            select temp {\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8!', 'bb': 9, 'bc': 8.6},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9!', 'bb': 10, 'bc': 9.6},\n            ],\n        )\n\n        # Ensure the rest of the data is unchanged\n        await self.assert_query_result(\n            r\"\"\"\n            select (DISTINCT {Ba, Bb, Bc}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8!', 'bb': 9, 'bc': 8.6},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9!', 'bb': 10, 'bc': 9.6},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_update_complex_type_05(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            with\n                temp := (\n                    update Ba[IS CBa | Bb & Bc] set {\n                        ba := .ba ++ '!',\n                    }\n                )\n            select temp {\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0!', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1!', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8!', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9!', 'bb': 9, 'bc': 9.5},\n            ],\n        )\n\n        # Ensure the rest of the data is unchanged\n        await self.assert_query_result(\n            r\"\"\"\n            select (DISTINCT {Ba, Bb, Bc}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0!', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1!', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8!', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9!', 'bb': 9, 'bc': 9.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_update_complex_type_06(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            with\n                temp := (\n                    update {CBa, Ba[IS Bb & Bc]} set {\n                        ba := .ba ++ '!',\n                    }\n                )\n            select temp {\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0!', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1!', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8!', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9!', 'bb': 9, 'bc': 9.5},\n            ],\n        )\n\n        # Ensure the rest of the data is unchanged\n        await self.assert_query_result(\n            r\"\"\"\n            select (DISTINCT {Ba, Bb, Bc}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0!', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1!', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8!', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9!', 'bb': 9, 'bc': 9.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_update_complex_type_07(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            with\n                temp := (\n                    update Object[IS (Ba & Bb) | (Ba & Bc)] set {\n                        ba := .ba ++ '!',\n                    }\n                )\n            select temp {\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBb', 'ba': 'cba2!', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3!', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8!', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9!', 'bb': 9, 'bc': 9.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba4!', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5!', 'bb': None, 'bc': 5.5},\n            ],\n        )\n\n        # Ensure the rest of the data is unchanged\n        await self.assert_query_result(\n            r\"\"\"\n            select (DISTINCT {Ba, Bb, Bc}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba2!', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3!', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8!', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9!', 'bb': 9, 'bc': 9.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba4!', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5!', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_update_complex_type_08(self):\n        await self._setup_basic_data()\n        await self.assert_query_result(\n            r\"\"\"\n            with\n                temp := (\n                    update {Object[IS Ba & Bb], Object[IS Ba & Bc]} set {\n                        ba := .ba ++ '!',\n                    }\n                )\n            select temp {\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBb', 'ba': 'cba2!', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3!', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8!', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9!', 'bb': 9, 'bc': 9.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba4!', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5!', 'bb': None, 'bc': 5.5},\n            ],\n        )\n\n        # Ensure the rest of the data is unchanged\n        await self.assert_query_result(\n            r\"\"\"\n            select (DISTINCT {Ba, Bb, Bc}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba2!', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3!', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8!', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9!', 'bb': 9, 'bc': 9.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba4!', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5!', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_delete_complex_type_01(self):\n        await self._setup_basic_data()\n        await self.con.execute(\n            r\"\"\"\n            delete Ba[is Bb];\n            \"\"\"\n        )\n\n        # Ensure the rest of the data is unchanged\n        await self.assert_query_result(\n            r\"\"\"\n            select (DISTINCT {Ba, Bb, Bc}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_delete_complex_type_02(self):\n        await self._setup_basic_data()\n        await self.con.execute(\n            r\"\"\"\n            delete Ba[is Bb][is Bc];\n            \"\"\"\n        )\n\n        # Ensure the rest of the data is unchanged\n        await self.assert_query_result(\n            r\"\"\"\n            select (DISTINCT {Ba, Bb, Bc}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_delete_complex_type_03(self):\n        await self._setup_basic_data()\n        await self.con.execute(\n            r\"\"\"\n            delete Ba[is Bb | Bc];\n            \"\"\"\n        )\n\n        # Ensure the rest of the data is unchanged\n        await self.assert_query_result(\n            r\"\"\"\n            select (DISTINCT {Ba, Bb, Bc}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_delete_complex_type_04(self):\n        await self._setup_basic_data()\n        await self.con.execute(\n            r\"\"\"\n            delete Ba[is Bb & Bc];\n            \"\"\"\n        )\n\n        # Ensure the rest of the data is unchanged\n        await self.assert_query_result(\n            r\"\"\"\n            select (DISTINCT {Ba, Bb, Bc}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_delete_complex_type_05(self):\n        await self._setup_basic_data()\n        await self.con.execute(\n            r\"\"\"\n            delete Ba[IS CBa | Bb & Bc];\n            \"\"\"\n        )\n\n        # Ensure the rest of the data is unchanged\n        await self.assert_query_result(\n            r\"\"\"\n            select (DISTINCT {Ba, Bb, Bc}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_delete_complex_type_06(self):\n        await self._setup_basic_data()\n        await self.con.execute(\n            r\"\"\"\n            delete {CBa, Ba[IS Bb & Bc]};\n            \"\"\"\n        )\n\n        # Ensure the rest of the data is unchanged\n        await self.assert_query_result(\n            r\"\"\"\n            select (DISTINCT {Ba, Bb, Bc}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBc', 'ba': 'cba4', 'bb': None, 'bc': 4.5},\n                {'tn': 'default::CBaBc', 'ba': 'cba5', 'bb': None, 'bc': 5.5},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_delete_complex_type_07(self):\n        await self._setup_basic_data()\n        await self.con.execute(\n            r\"\"\"\n            delete Object[IS (Ba & Bb) | (Ba & Bc)];\n            \"\"\"\n        )\n\n        # Ensure the rest of the data is unchanged\n        await self.assert_query_result(\n            r\"\"\"\n            select (DISTINCT {Ba, Bb, Bc}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_delete_complex_type_08(self):\n        await self._setup_basic_data()\n        await self.con.execute(\n            r\"\"\"\n            delete {Object[IS Ba & Bb], Object[IS Ba & Bc]};\n            \"\"\"\n        )\n\n        # Ensure the rest of the data is unchanged\n        await self.assert_query_result(\n            r\"\"\"\n            select (DISTINCT {Ba, Bb, Bc}){\n                tn := .__type__.name,\n                [IS Ba].ba,\n                [IS Bb].bb,\n                [IS Bc].bc,\n            }\n            order by .tn then .ba then .bb then .bc;\n            \"\"\",\n            [\n                {'tn': 'default::CBa', 'ba': 'cba0', 'bb': None, 'bc': None},\n                {'tn': 'default::CBa', 'ba': 'cba1', 'bb': None, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 0, 'bc': None},\n                {'tn': 'default::CBb', 'ba': None, 'bb': 1, 'bc': None},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 6, 'bc': 6.5},\n                {'tn': 'default::CBbBc', 'ba': None, 'bb': 7, 'bc': 7.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 0.5},\n                {'tn': 'default::CBc', 'ba': None, 'bb': None, 'bc': 1.5},\n            ],\n        )\n\n    async def test_edgeql_advtypes_for_complex_intersection_01(self):\n        # Type intersection on for loop iterator\n        await self._setup_basic_data()\n\n        await self.assert_query_result(\n            r\"\"\"\n            FOR x IN Ba UNION (\n                x[IS Bb]\n                {\n                    tn := .__type__.name,\n                    ba,\n                    bb,\n                    [IS Bc].bc,\n                }\n            )\n            \"\"\",\n            tb.bag([\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ]),\n        )\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT (FOR x IN Ba UNION (x[IS Bb]))\n            {\n                tn := .__type__.name,\n                ba,\n                bb,\n                [IS Bc].bc,\n            }\n            \"\"\",\n            tb.bag([\n                {'tn': 'default::CBaBb', 'ba': 'cba2', 'bb': 2, 'bc': None},\n                {'tn': 'default::CBaBb', 'ba': 'cba3', 'bb': 3, 'bc': None},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba8', 'bb': 8, 'bc': 8.5},\n                {'tn': 'default::CBaBbBc', 'ba': 'cba9', 'bb': 9, 'bc': 9.5},\n            ]),\n        )\n\n    async def test_edgeql_advtypes_intersection_pointers_01(self):\n        # Type intersections with incompatible pointers should produce errors.\n\n        type_roots = [\n            \"SoloNonCompSingle\",\n            \"SoloNonCompMulti\",\n            \"SoloCompSingle\",\n            \"SoloCompMulti\",\n            \"DerivedNonCompSingle\",\n            \"DerivedNonCompMulti\",\n            \"DerivedCompSingle\",\n            \"DerivedCompMulti\",\n        ]\n\n        for type_root_a in type_roots:\n            for type_root_b in type_roots:\n                for type_suffix, ptr_name in (\n                    (\"Prop\", \"numbers\"),\n                    (\"Link\", \"siblings\"),\n                ):\n                    if (\n                        # Either type has computed pointer\n                        (\n                            \"NonComp\" not in type_root_a\n                            or \"NonComp\" not in type_root_b\n                        )\n                        # but the pointer doesn't come from a common base\n                        and not (\n                            \"Derived\" in type_root_a\n                            and type_root_a == type_root_b\n                        )\n                    ):\n                        async with self.assertRaisesRegexTx(\n                            edgedb.SchemaError,\n                            r\"it is illegal to create a type intersection \"\n                            r\"that causes a computed .* to mix \"\n                            r\"with other versions of the same .*\"\n                        ):\n                            await self.con.execute(f\"\"\"\n                                select {type_root_a}{type_suffix}A {{\n                                    x := (\n                                        [is {type_root_b}{type_suffix}B]\n                                        .{ptr_name}\n                                    )\n                                }};\n                            \"\"\")\n\n                    elif (\n                        # differing pointer cardinalities\n                        (\"Single\" in type_root_a) != (\"Single\" in type_root_b)\n                    ):\n                        async with self.assertRaisesRegexTx(\n                            edgedb.SchemaError,\n                            r\"it is illegal to create a type intersection \"\n                            r\"that causes a .* to mix \"\n                            r\"with other versions of .* \"\n                            r\"which have a different cardinality\"\n                        ):\n                            await self.con.execute(f\"\"\"\n                                select {type_root_a}{type_suffix}A {{\n                                    x := (\n                                        [is {type_root_b}{type_suffix}B]\n                                        .{ptr_name}\n                                    )\n                                }};\n                            \"\"\")\n\n                    else:\n                        await self.con.execute(f\"\"\"\n                            select {type_root_a}{type_suffix}A {{\n                                x := (\n                                    [is {type_root_b}{type_suffix}B]\n                                    .{ptr_name}\n                                )\n                            }};\n                        \"\"\")\n\n    async def test_edgeql_advtypes_intersection_pointers_02(self):\n        # Intersection pointer should return nothing if they types are\n        # unrelated.\n\n        await self.con.execute(\"\"\"\n            INSERT SoloOriginA { dest := (INSERT Destination{ name := \"A\" }) };\n            INSERT SoloOriginB { dest := (INSERT Destination{ name := \"B\" }) };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT SoloOriginA {\n                x := [is SoloOriginB].dest.name\n            }\n            \"\"\",\n            [{'x': None}],\n        )\n\n    async def test_edgeql_advtypes_intersection_pointers_03(self):\n        # Intersection pointer should return the correct values if the type\n        # intersection is not empty.\n\n        await self.con.execute(\"\"\"\n            INSERT BaseOriginA { dest := (INSERT Destination{ name := \"A\" }) };\n            INSERT BaseOriginB { dest := (INSERT Destination{ name := \"B\" }) };\n            INSERT DerivedOriginC {\n                dest := (INSERT Destination{ name := \"C\" })\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT BaseOriginA {\n                x := [is BaseOriginB].dest.name\n            }\n            ORDER BY .x\n            \"\"\",\n            [{'x': None}, {'x': 'C'}],\n        )\n"
  },
  {
    "path": "tests/test_edgeql_casts.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2018-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport itertools\nimport os.path\n\nimport edgedb\n\nfrom edb.testbase import server as tb\n\n\nclass TestEdgeQLCasts(tb.QueryTestCase):\n    '''Testing symmetry and validity of casts.\n\n    Scalar casting is symmetric in the sense that if casting scalar\n    type X into Y is valid then it is also valid to cast Y into X.\n\n    Some casts are lossless. A cast from X into Y is lossless if all\n    the relevant details of the value of type X can be unambiguously\n    represented by a value of type Y. Examples of lossless casts:\n    - any scalar can be losslessly cast into a str\n    - int16 and int32 can be losslessly cast into int64\n    - int16 can be losslessly cast into float32\n    - any numeric type can be losslessly cast into a decimal\n\n    Sometimes only specific values (a subset of the entire domain of\n    the scalar) can be cast losslessly:\n    - 2147299968 can be cast losslessly into a float32, but not 2147299969\n    - decimal 2.5 can be cast losslessly into a float32, but not decimal\n      2.5000000001\n\n    Consider two types X and Y with corresponding values x and y.\n    If x can be losslessly cast into Y, then casting it back is also lossless:\n        x = <X><Y>x\n    '''\n\n    # FIXME: a special schema should be used here since we need to\n    # cover all known scalars and even some arrays and tuples.\n    SCHEMA = os.path.join(os.path.dirname(__file__), 'schemas',\n                          'casts.esdl')\n\n    SETUP = os.path.join(os.path.dirname(__file__), 'schemas',\n                         'casts_setup.edgeql')\n\n    # NOTE: nothing can be cast into bytes\n    async def test_edgeql_casts_bytes_01(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'cannot cast'):\n            await self.con.execute(\"\"\"\n                SELECT <bytes>True;\n            \"\"\")\n\n    async def test_edgeql_casts_bytes_02(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'cannot cast'):\n            await self.con.execute(\"\"\"\n                SELECT <bytes>uuid_generate_v1mc();\n            \"\"\")\n\n    async def test_edgeql_casts_bytes_03(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'cannot cast'):\n            await self.con.execute(\"\"\"\n                SELECT <bytes>'Hello';\n            \"\"\")\n\n    async def test_edgeql_casts_bytes_04(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidValueError,\n            r'expected JSON string or null',\n        ):\n            await self.con.query_single(\"SELECT <bytes>to_json('1');\")\n\n        self.assertEqual(\n            await self.con.query_single(r'''\n                SELECT <bytes>to_json('\"aGVsbG8=\"');\n            '''),\n            b'hello',\n        )\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError, r'invalid symbol'):\n            await self.con.query_single(\"\"\"\n                SELECT <bytes>to_json('\"not base64!\"');\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError, r'invalid base64 end sequence'):\n            await self.con.query_single(\"\"\"\n                SELECT <bytes>to_json('\"a\"');\n            \"\"\")\n\n    async def test_edgeql_casts_bytes_05(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'cannot cast'):\n            await self.con.execute(\"\"\"\n                SELECT <bytes>datetime_current();\n            \"\"\")\n\n    async def test_edgeql_casts_bytes_06(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'cannot cast'):\n            await self.con.execute(\"\"\"\n                SELECT\n                  <bytes>cal::to_local_datetime('2018-05-07T20:01:22.306916');\n            \"\"\")\n\n    async def test_edgeql_casts_bytes_07(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'cannot cast'):\n            await self.con.execute(\"\"\"\n                SELECT <bytes>cal::to_local_date('2018-05-07');\n            \"\"\")\n\n    async def test_edgeql_casts_bytes_08(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'cannot cast'):\n            await self.con.execute(\"\"\"\n                SELECT <bytes>cal::to_local_time('20:01:22.306916');\n            \"\"\")\n\n    async def test_edgeql_casts_bytes_09(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'cannot cast'):\n            await self.con.execute(\"\"\"\n                SELECT <bytes>to_duration(hours:=20);\n            \"\"\")\n\n    async def test_edgeql_casts_bytes_10(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'cannot cast'):\n            await self.con.execute(\"\"\"\n                SELECT <bytes>to_int16('2');\n            \"\"\")\n\n    async def test_edgeql_casts_bytes_11(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'cannot cast'):\n            await self.con.execute(\"\"\"\n                SELECT <bytes>to_int32('2');\n            \"\"\")\n\n    async def test_edgeql_casts_bytes_12(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'cannot cast'):\n            await self.con.execute(\"\"\"\n                SELECT <bytes>to_int64('2');\n            \"\"\")\n\n    async def test_edgeql_casts_bytes_13(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'cannot cast'):\n            await self.con.execute(\"\"\"\n                SELECT <bytes>to_float32('2');\n            \"\"\")\n\n    async def test_edgeql_casts_bytes_14(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'cannot cast'):\n            await self.con.execute(\"\"\"\n                SELECT <bytes>to_float64('2');\n            \"\"\")\n\n    async def test_edgeql_casts_bytes_15(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'cannot cast'):\n            await self.con.execute(\"\"\"\n                SELECT <bytes>to_decimal('2');\n            \"\"\")\n\n    async def test_edgeql_casts_bytes_16(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'cannot cast'):\n            await self.con.execute(\"\"\"\n                SELECT <bytes>to_bigint('2');\n            \"\"\")\n\n    # NOTE: casts are idempotent\n\n    async def test_edgeql_casts_idempotence_01(self):\n        await self.assert_query_result(\n            r'''SELECT <bool><bool>True IS bool;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <bytes><bytes>b'Hello' IS bytes;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <str><str>'Hello' IS str;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <json><json>to_json('1') IS json;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <uuid><uuid>uuid_generate_v1mc() IS uuid;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <datetime><datetime>datetime_current() IS datetime;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <cal::local_datetime><cal::local_datetime>\n                    cal::to_local_datetime(\n                    '2018-05-07T20:01:22.306916') IS cal::local_datetime;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <cal::local_date><cal::local_date>cal::to_local_date(\n                    '2018-05-07') IS cal::local_date;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <cal::local_time><cal::local_time>cal::to_local_time(\n                    '20:01:22.306916') IS cal::local_time;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <duration><duration>to_duration(\n                    hours:=20) IS duration;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <int16><int16>to_int16('12345') IS int16;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <int32><int32>to_int32('1234567890') IS int32;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <int64><int64>to_int64('1234567890123') IS int64;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <float32><float32>to_float32('2.5') IS float32;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <float64><float64>to_float64('2.5') IS float64;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <bigint><bigint>to_bigint(\n                    '123456789123456789123456789')\n                IS bigint;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <decimal><decimal>to_decimal(\n                    '123456789123456789123456789.123456789123456789123456789')\n                IS decimal;\n            ''',\n            [True],\n        )\n\n    async def test_edgeql_casts_idempotence_02(self):\n        await self.assert_query_result(\n            r'''SELECT <bool><bool>True = True;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <bytes><bytes>b'Hello' = b'Hello';''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <str><str>'Hello' = 'Hello';''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <json><json>to_json('1') = to_json('1');''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH U := uuid_generate_v4()\n                SELECT <uuid><uuid>U = U;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <datetime><datetime>datetime_of_statement() =\n                    datetime_of_statement();\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <cal::local_datetime><cal::local_datetime>\n                    cal::to_local_datetime('2018-05-07T20:01:22.306916') =\n                    cal::to_local_datetime('2018-05-07T20:01:22.306916');\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <cal::local_date><cal::local_date>\n                    cal::to_local_date('2018-05-07') =\n                    cal::to_local_date('2018-05-07');\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <cal::local_time><cal::local_time>cal::to_local_time(\n                    '20:01:22.306916') = cal::to_local_time('20:01:22.306916');\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <duration><duration>to_duration(hours:=20) =\n                    to_duration(hours:=20);\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <int16><int16>to_int16('12345') = 12345;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <int32><int32>to_int32('1234567890') = 1234567890;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <int64><int64>to_int64('1234567890123') =\n                    1234567890123;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <float32><float32>to_float32('2.5') = 2.5;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <float64><float64>to_float64('2.5') = 2.5;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <bigint><bigint>to_bigint(\n                    '123456789123456789123456789')\n                = to_bigint(\n                    '123456789123456789123456789');\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <decimal><decimal>to_decimal(\n                    '123456789123456789123456789.123456789123456789123456789')\n                = to_decimal(\n                    '123456789123456789123456789.123456789123456789123456789');\n            ''',\n            [True],\n        )\n\n    async def test_edgeql_casts_str_01(self):\n        # Casting to str and back is lossless for every scalar (if\n        # legal). It's still not legal to cast bytes into str or some\n        # of the json values.\n        await self.assert_query_result(\n            r'''SELECT <bool><str>True = True;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <bool><str>False = False;''',\n            [True],\n            # only JSON strings can be cast into EdgeQL str\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <json><str>to_json('\"Hello\"') = to_json('\"Hello\"');''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH U := uuid_generate_v1mc()\n                SELECT <uuid><str>U = U;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <datetime><str>datetime_of_statement() =\n                    datetime_of_statement();\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <cal::local_datetime><str>cal::to_local_datetime(\n                        '2018-05-07T20:01:22.306916') =\n                    cal::to_local_datetime('2018-05-07T20:01:22.306916');\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <cal::local_date><str>cal::to_local_date('2018-05-07') =\n                    cal::to_local_date('2018-05-07');\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <cal::local_time><str>\n                    cal::to_local_time('20:01:22.306916') =\n                    cal::to_local_time('20:01:22.306916');\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <duration><str>to_duration(hours:=20) =\n                    to_duration(hours:=20);\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <int16><str>to_int16('12345') = 12345;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <int32><str>to_int32('1234567890') = 1234567890;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <int64><str>to_int64(\n                    '1234567890123') = 1234567890123;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <float32><str>to_float32('2.5') = 2.5;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <float64><str>to_float64('2.5') = 2.5;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <bigint><str>to_bigint(\n                    '123456789123456789123456789')\n                = to_bigint(\n                    '123456789123456789123456789');\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <decimal><str>to_decimal(\n                    '123456789123456789123456789.123456789123456789123456789')\n                = to_decimal(\n                    '123456789123456789123456789.123456789123456789123456789');\n            ''',\n            [True],\n        )\n\n    async def test_edgeql_casts_str_02(self):\n        # Certain strings can be cast into other types losslessly,\n        # making them \"canonical\" string representations of those\n        # values.\n        await self.assert_query_result(\n            r'''\n                FOR x in {'true', 'false'}\n                SELECT <str><bool>x = x;\n            ''',\n            [True, True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                FOR x in {'True', 'False', 'TRUE', 'FALSE', '  TrUe   '}\n                SELECT <str><bool>x = x;\n            ''',\n            [False, False, False, False, False],\n        )\n\n        await self.assert_query_result(\n            r'''\n                FOR x in {'True', 'False', 'TRUE', 'FALSE', 'TrUe'}\n                SELECT <str><bool>x = str_lower(x);\n            ''',\n            [True, True, True, True, True],\n        )\n\n        for variant in {'😈', 'yes', '1', 'no', 'on', 'OFF',\n                        't', 'f', 'tr', 'fa'}:\n            async with self.assertRaisesRegexTx(\n                    edgedb.InvalidValueError,\n                    fr\"invalid input syntax for type std::bool: '{variant}'\"):\n                await self.con.query_single(f'SELECT <bool>\"{variant}\"')\n\n        self.assertTrue(\n            await self.con.query_single('SELECT <bool>\"    TruE   \"'))\n        self.assertFalse(\n            await self.con.query_single('SELECT <bool>\"    FalsE   \"'))\n\n    async def test_edgeql_casts_str_03(self):\n        # str to json is always lossless\n        await self.assert_query_result(\n            r'''\n                FOR x in {'any', 'arbitrary', '♠gibberish♠'}\n                SELECT <str><json>x = x;\n            ''',\n            [True, True, True],\n        )\n\n    async def test_edgeql_casts_str_04(self):\n        # canonical uuid representation as a string is using lowercase\n        await self.assert_query_result(\n            r'''\n                FOR x in 'd4288330-eea3-11e8-bc5f-7faf132b1d84'\n                SELECT <str><uuid>x = x;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            # non-canonical\n            r'''\n                FOR x in {\n                    'D4288330-EEA3-11E8-BC5F-7FAF132B1D84',\n                    'D4288330-Eea3-11E8-Bc5F-7Faf132B1D84',\n                    'D4288330-eea3-11e8-bc5f-7faf132b1d84',\n                }\n                SELECT <str><uuid>x = x;\n            ''',\n            [False, False, False],\n        )\n\n        await self.assert_query_result(\n            r'''\n                FOR x in {\n                    'D4288330-EEA3-11E8-BC5F-7FAF132B1D84',\n                    'D4288330-Eea3-11E8-Bc5F-7Faf132B1D84',\n                    'D4288330-eea3-11e8-bc5f-7faf132b1d84',\n                }\n                SELECT <str><uuid>x = str_lower(x);\n            ''',\n            [True, True, True],\n        )\n\n    async def test_edgeql_casts_str_05(self):\n        # Canonical date and time str representations must follow ISO\n        # 8601. This test assumes that the server is configured to be\n        # in UTC time zone.\n        await self.assert_query_result(\n            r'''\n                FOR x in '2018-05-07T20:01:22.306916+00:00'\n                SELECT <str><datetime>x = x;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            # validating that these are all in fact the same datetime\n            r'''\n                FOR x in {\n                    '2018-05-07T15:01:22.306916-05:00',\n                    '2018-05-07T15:01:22.306916-05',\n                    '2018-05-07T20:01:22.306916Z',\n                    '2018-05-07T20:01:22.306916+0000',\n                    '2018-05-07T20:01:22.306916+00',\n                    # the '-' and ':' separators may be omitted\n                    '20180507T200122.306916+00',\n                    # acceptable RFC 3339\n                    '2018-05-07 20:01:22.306916+00:00',\n                    '2018-05-07t20:01:22.306916z',\n                }\n                SELECT <datetime>x =\n                    <datetime>'2018-05-07T20:01:22.306916+00:00';\n            ''',\n            [True, True, True, True, True, True, True, True],\n        )\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax'):\n            await self.con.query_single(\n                'SELECT <datetime>\"2018-05-07;20:01:22.306916+00:00\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax'):\n            await self.con.query_single(\n                'SELECT <datetime>\"2018-05-07T20:01:22.306916\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax'):\n            await self.con.query_single(\n                'SELECT <datetime>\"2018-05-07T20:01:22.306916 1000\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax'):\n            await self.con.query_single(\n                'SELECT <datetime>\"2018-05-07T20:01:22.306916 US/Central\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax'):\n            await self.con.query_single(\n                'SELECT <datetime>\"2018-05-07T20:01:22.306916 +GMT1\"')\n\n    async def test_edgeql_casts_str_06(self):\n        # Canonical date and time str representations must follow ISO\n        # 8601. This test assumes that the server is configured to be\n        # in UTC time zone.\n        await self.assert_query_result(\n            r'''\n                FOR x in '2018-05-07T20:01:22.306916'\n                SELECT <str><cal::local_datetime>x = x;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            # validating that these are all in fact the same datetime\n            r'''\n                FOR x in {\n                    # the '-' and ':' separators may be omitted\n                    '20180507T200122.306916',\n                    # acceptable RFC 3339\n                    '2018-05-07 20:01:22.306916',\n                    '2018-05-07t20:01:22.306916',\n                }\n                SELECT <cal::local_datetime>x =\n                    <cal::local_datetime>'2018-05-07T20:01:22.306916';\n            ''',\n            [True, True, True],\n        )\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                'SELECT <cal::local_datetime>\"2018-05-07;20:01:22.306916\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                '''\n                    SELECT\n                        <cal::local_datetime>\"2018-05-07T20:01:22.306916+01:00\"\n                ''')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                'SELECT <cal::local_datetime>\"2018-05-07T20:01:22.306916 GMT\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                '''\n                    SELECT\n                      <cal::local_datetime>\"2018-05-07T20:01:22.306916 GMT0\"\n                ''')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                '''SELECT <cal::local_datetime>\n                    \"2018-05-07T20:01:22.306916 US/Central\"\n                ''')\n\n    async def test_edgeql_casts_str_07(self):\n        # Canonical date and time str representations must follow ISO\n        # 8601.\n        await self.assert_query_result(\n            r'''\n                FOR x in '2018-05-07'\n                SELECT <str><cal::local_date>x = x;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            # validating that these are all in fact the same date\n            r'''\n                FOR x in {\n                    # the '-' separators may be omitted\n                    '20180507',\n                }\n                SELECT <cal::local_date>x = <cal::local_date>'2018-05-07';\n            ''',\n            [True],\n        )\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                'SELECT <cal::local_date>\"2018-05-07T20:01:22.306916\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                'SELECT <cal::local_date>\"2018/05/07\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                'SELECT <cal::local_date>\"2018.05.07\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                'SELECT <cal::local_date>\"2018-05-07+01:00\"')\n\n    async def test_edgeql_casts_str_08(self):\n        # Canonical date and time str representations must follow ISO\n        # 8601.\n        await self.assert_query_result(\n            r'''\n                FOR x in '20:01:22.306916'\n                SELECT <str><cal::local_time>x = x;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                FOR x in {\n                    '20:01',\n                    '20:01:00',\n                    # the ':' separators may be omitted\n                    '2001',\n                    '200100',\n                }\n                SELECT <cal::local_time>x = <cal::local_time>'20:01:00';\n            ''',\n            [True, True, True, True],\n        )\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                'invalid input syntax for type std::cal::local_time'):\n            await self.con.query_single(\n                \"SELECT <cal::local_time>'2018-05-07 20:01:22'\")\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                'SELECT <cal::local_time>\"20:01:22.306916+01:00\"')\n\n    async def test_edgeql_casts_str_09(self):\n        # Canonical duration\n        await self.assert_query_result(\n            r'''\n                FOR x in 'PT20H1M22.306916S'\n                SELECT <str><duration>x = x;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            # non-canonical\n            r'''\n                FOR x in {\n                    '20:01:22.306916',\n                    '20h 1m 22.306916s',\n                    '20 hours 1 minute 22.306916 seconds',\n                    '72082.306916',  # the duration in seconds\n                    '0.834285959675926 days',\n                }\n                SELECT <str><duration>x = x;\n            ''',\n            [False, False, False, False, False],\n        )\n\n        await self.assert_query_result(\n            # validating that these are all in fact the same duration\n            r'''\n                FOR x in {\n                    '20:01:22.306916',\n                    '20h 1m 22.306916s',\n                    '20 hours 1 minute 22.306916 seconds',\n                    '72082.306916',  # the duration in seconds\n                    '0.834285959675926 days',\n                }\n                SELECT <duration>x = <duration>'PT20H1M22.306916S';\n            ''',\n            [True, True, True, True, True],\n        )\n\n    async def test_edgeql_casts_str_10(self):\n        # valid casts from str to any integer is lossless, as long as\n        # there's no whitespace, which is trimmed\n        await self.assert_query_result(\n            r'''\n                FOR x in {'-20', '0', '7', '12345'}\n                SELECT <str><int16>x = x;\n            ''',\n            [True, True, True, True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                FOR x in {'-20', '0', '7', '12345'}\n                SELECT <str><int32>x = x;\n            ''',\n            [True, True, True, True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                FOR x in {'-20', '0', '7', '12345'}\n                SELECT <str><int64>x = x;\n            ''',\n            [True, True, True, True],\n        )\n\n        await self.assert_query_result(\n            # with whitespace\n            r'''\n                FOR x in {\n                    '       42',\n                    '42     ',\n                    '       42      ',\n                }\n                SELECT <str><int16>x = x;\n            ''',\n            [False, False, False],\n        )\n\n        await self.assert_query_result(\n            # validating that these are all in fact the same value\n            r'''\n                FOR x in {\n                    '       42',\n                    '42     ',\n                    '       42      ',\n                }\n                SELECT <int16>x = 42;\n            ''',\n            [True, True, True],\n        )\n\n    async def test_edgeql_casts_str_11(self):\n        # There's too many ways of representing floats. Outside of\n        # trivial 1-2 digit cases, relying on any str being\n        # \"canonical\" is not safe, making most casts from str to float\n        # lossy.\n        await self.assert_query_result(\n            r'''\n                FOR x in {'-20', '0', '7.2'}\n                SELECT <str><float32>x = x;\n            ''',\n            [True, True, True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                FOR x in {'-20', '0', '7.2'}\n                SELECT <str><float64>x = x;\n            ''',\n            [True, True, True],\n        )\n\n        await self.assert_query_result(\n            # non-canonical\n            r'''\n                FOR x in {\n                    '0.0000000001234',\n                    '1234E-13',\n                    '0.1234e-9',\n                }\n                SELECT <str><float32>x = x;\n            ''',\n            [False, False, False],\n        )\n\n        await self.assert_query_result(\n            r'''\n                FOR x in {\n                    '0.0000000001234',\n                    '1234E-13',\n                    '0.1234e-9',\n                }\n                SELECT <str><float64>x = x;\n            ''',\n            [False, False, False],\n        )\n\n        await self.assert_query_result(\n            # validating that these are all in fact the same value\n            r'''\n                FOR x in {\n                    '0.0000000001234',\n                    '1234E-13',\n                    '0.1234e-9',\n                }\n                SELECT <float64>x = 1234e-13;\n            ''',\n            [True, True, True],\n        )\n\n    async def test_edgeql_casts_str_12(self):\n        # The canonical string representation of decimals is without\n        # use of scientific notation.\n        await self.assert_query_result(\n            r'''\n                FOR x in {\n                    '-20', '0', '7.2', '0.0000000001234', '1234.00000001234'\n                }\n                SELECT <str><decimal>x = x;\n            ''',\n            [True, True, True, True, True],\n        )\n\n        await self.assert_query_result(\n            # non-canonical\n            r'''\n                FOR x in {\n                    '1234E-13',\n                    '0.1234e-9',\n                }\n                SELECT <str><decimal>x = x;\n            ''',\n            [False, False],\n        )\n\n        await self.assert_query_result(\n            # validating that these are all in fact the same date\n            r'''\n                FOR x in {\n                    '1234E-13',\n                    '0.1234e-9',\n                }\n                SELECT <decimal>x = <decimal>'0.0000000001234';\n            ''',\n            [True, True],\n        )\n\n    async def test_edgeql_casts_str_13(self):\n        # Casting to str and back is lossless for every scalar (if\n        # legal). It's still not legal to cast bytes into str or some\n        # of the json values.\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <uuid><str>T.id = T.id;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <bool><str>T.p_bool = T.p_bool;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <str><str>T.p_str = T.p_str;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <datetime><str>T.p_datetime = T.p_datetime;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <cal::local_datetime><str>T.p_local_datetime =\n                    T.p_local_datetime;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <cal::local_date><str>T.p_local_date = T.p_local_date;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <cal::local_time><str>T.p_local_time = T.p_local_time;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <duration><str>T.p_duration = T.p_duration;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <int16><str>T.p_int16 = T.p_int16;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <int32><str>T.p_int32 = T.p_int32;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <int64><str>T.p_int64 = T.p_int64;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <float32><str>T.p_float32 = T.p_float32;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <float64><str>T.p_float64 = T.p_float64;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <bigint><str>T.p_bigint = T.p_bigint;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <decimal><str>T.p_decimal = T.p_decimal;\n            ''',\n            [True],\n        )\n\n    async def test_edgeql_casts_numeric_01(self):\n        # Casting to decimal and back should be lossless for any other\n        # integer type.\n        for numtype in {'bigint', 'decimal'}:\n            await self.assert_query_result(\n                # technically we're already casting a literal int64\n                # to int16 first\n                f'''\n                    FOR x in <int16>{{-32768, -32767, -100,\n                                      0, 13, 32766, 32767}}\n                    SELECT <int16><{numtype}>x = x;\n                ''',\n                [True, True, True, True, True, True, True],\n            )\n\n            await self.assert_query_result(\n                # technically we're already casting a literal int64\n                # to int32 first\n                f'''\n                    FOR x in <int32>{{-2147483648, -2147483647, -65536, -100,\n                                      0, 13, 32768, 2147483646, 2147483647}}\n                    SELECT <int32><{numtype}>x = x;\n                ''',\n                [True, True, True, True, True, True, True, True, True],\n            )\n\n            await self.assert_query_result(\n                f'''\n                    FOR x in <int64>{{\n                        -9223372036854775808,\n                        -9223372036854775807,\n                        -4294967296,\n                        -65536,\n                        -100,\n                        0,\n                        13,\n                        65536,\n                        4294967296,\n                        9223372036854775806,\n                        9223372036854775807\n                    }}\n                    SELECT <int64><{numtype}>x = x;\n                ''',\n                [True, True, True, True, True, True,\n                 True, True, True, True, True],\n            )\n\n    async def test_edgeql_casts_numeric_02(self):\n        # Casting to decimal and back should be lossless for any other\n        # float type of low precision (a couple of digits less than\n        # the maximum possible float precision).\n        await self.assert_query_result(\n            # technically we're already casting a literal int64 or\n            # float64 to float32 first\n            r'''\n                FOR x in <float32>{-3.31234e+38, -1.234e+12, -1.234e-12,\n                                    -100, 0, 13, 1.234e-12, 1.234e+12, 3.4e+38}\n                SELECT <float32><decimal>x = x;\n            ''',\n            [True, True, True, True, True, True, True, True, True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                FOR x in <float64>{-1.61234e+308, -1.234e+42, -1.234e-42,\n                                    -100, 0, 13, 1.234e-42, 1.234e+42,\n                                    1.7e+308}\n                SELECT <float64><decimal>x = x;\n            ''',\n            [True, True, True, True, True, True, True, True, True],\n        )\n\n    async def test_edgeql_casts_numeric_03(self):\n        # It is especially dangerous to cast an int32 into float32 and\n        # back because float32 cannot losslessly represent the entire\n        # range of int32, but it can represent some of it, so no\n        # obvious errors would be raised (as any int32 value is\n        # technically withing valid range of float32), but the value\n        # could be mangled.\n        await self.assert_query_result(\n            # ints <= 2^24 can be represented exactly in a float32\n            r'''\n            FOR x in <int32>{16777216, 16777215, 16777214,\n                              1677721, 167772, 16777}\n            SELECT <int32><float32>x = x;\n            ''',\n            [True, True, True, True, True, True],\n        )\n\n        await self.assert_query_result(\n            # max int32 -100, -1000\n            r'''\n            FOR x in <int32>{2147483548, 2147482648}\n            SELECT <int32><float32>x = x;\n            ''',\n            [False, False],\n        )\n\n        await self.assert_query_result(\n            r'''\n            FOR x in <int32>{2147483548, 2147482648}\n            SELECT <int32><float32>x;\n            ''',\n            [2147483520, 2147482624],\n        )\n\n    async def test_edgeql_casts_numeric_04(self):\n        await self.assert_query_result(\n            # ints <= 2^24 can be represented exactly in a float32\n            r'''\n                FOR x in <int32>{16777216, 16777215, 16777214,\n                                  1677721, 167772, 16777}\n                SELECT <int32><float64>x = x;\n            ''',\n            [True, True, True, True, True, True],\n        )\n\n        await self.assert_query_result(\n            # max int32 -1, -2, -3, -10, -100, -1000\n            r'''\n            FOR x in <int32>{2147483647, 2147483646, 2147483645,\n                              2147483638, 2147483548, 2147482648}\n            SELECT <int32><float64>x = x;\n            ''',\n            [True, True, True, True, True, True],\n        )\n\n    async def test_edgeql_casts_numeric_05(self):\n        # Due to the sparseness of float values large integers may not\n        # be representable exactly if they require better precision\n        # than float provides.\n        await self.assert_query_result(\n            r'''\n                # 2^31 -1, -2, -3, -10\n                FOR x in <int32>{2147483647, 2147483646, 2147483645,\n                                  2147483638}\n                # 2147483647 is the max int32\n                SELECT x <= <int32>2147483647;\n            ''',\n            [True, True, True, True],\n        )\n\n        async with self.assertRaisesRegexTx(\n                edgedb.NumericOutOfRangeError, r\"std::int32 out of range\"):\n            async with self.con.transaction():\n                await self.con.execute(\"\"\"\n                    SELECT <int32><float32><int32>2147483647;\n                \"\"\")\n\n        async with self.assertRaisesRegexTx(\n                edgedb.NumericOutOfRangeError, r\"std::int32 out of range\"):\n            async with self.con.transaction():\n                await self.con.execute(\"\"\"\n                    SELECT <int32><float32><int32>2147483646;\n                \"\"\")\n\n        async with self.assertRaisesRegexTx(\n                edgedb.NumericOutOfRangeError, r\"std::int32 out of range\"):\n            async with self.con.transaction():\n                await self.con.execute(\"\"\"\n                    SELECT <int32><float32><int32>2147483645;\n                \"\"\")\n\n        async with self.assertRaisesRegexTx(\n                edgedb.NumericOutOfRangeError, r\"std::int32 out of range\"):\n            async with self.con.transaction():\n                await self.con.execute(\"\"\"\n                    SELECT <int32><float32><int32>2147483638;\n                \"\"\")\n\n    async def test_edgeql_casts_numeric_06(self):\n        await self.assert_query_result(\n            r'''SELECT <int16>1;''',\n            [1],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <int32>1;''',\n            [1],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <int64>1;''',\n            [1],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <float32>1;''',\n            [1.0],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <float64>1;''',\n            [1.0],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <bigint>1;''',\n            [1],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <decimal>1;''',\n            [1],\n        )\n\n    async def test_edgeql_casts_numeric_07(self):\n        numerics = ['int16', 'int32', 'int64', 'float32', 'float64', 'bigint',\n                    'decimal']\n\n        for t1, t2 in itertools.product(numerics, numerics):\n            await self.assert_query_result(\n                f'''\n                    SELECT <{t1}><{t2}>1;\n                ''',\n                [1],\n            )\n\n    async def test_edgeql_casts_numeric_08(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type std::bigint'):\n            await self.con.query_single(\n                'SELECT <bigint>\"100000n\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type std::decimal'):\n            await self.con.query_single(\n                'SELECT <decimal>\"12313.132n\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"invalid input syntax for type std::bigint: 'bigint'\"):\n            await self.con.query_single(\n                'SELECT <bigint>\"bigint\"')\n\n    async def test_edgeql_casts_collections_01(self):\n        await self.assert_query_result(\n            r'''SELECT <array<str>>[1, 2, 3];''',\n            [['1', '2', '3']],\n        )\n\n        await self.assert_query_result(\n            r'''WITH X := [1, 2, 3] SELECT <array<str>> X;''',\n            [['1', '2', '3']],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <tuple<str, float64>> (1, '2');''',\n            [['1', 2.0]],\n        )\n\n        await self.assert_query_result(\n            r'''WITH X := (1, '2') SELECT <tuple<str, float64>> X;''',\n            [['1', 2.0]],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <array<tuple<str, float64>>> [(1, '2')];''',\n            [[['1', 2.0]]],\n        )\n\n        await self.assert_query_result(\n            r'''WITH X := [(1, '2')]\n                SELECT <array<tuple<str, float64>>> X;''',\n            [[['1', 2.0]]],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <tuple<array<float64>>> (['1'],);''',\n            [[[1.0]]],\n        )\n\n    async def test_edgeql_casts_collections_02(self):\n        await self.assert_query_result(\n            R'''\n                WITH\n                    std AS MODULE math,\n                    foo := (SELECT [1, 2, 3])\n                SELECT <array<str>>foo;\n            ''',\n            [['1', '2', '3']],\n        )\n\n        await self.assert_query_result(\n            R'''\n                WITH\n                    std AS MODULE math,\n                    foo := (SELECT [<int32>1, <int32>2, <int32>3])\n                SELECT <array<str>>foo;\n            ''',\n            [['1', '2', '3']],\n        )\n\n        await self.assert_query_result(\n            R'''\n                WITH\n                    std AS MODULE math,\n                    foo := (SELECT [(1,), (2,), (3,)])\n                SELECT <array<tuple<str>>>foo;\n            ''',\n            [[['1'], ['2'], ['3']]],\n        )\n\n    # check that casting to collections produces the correct error messages\n    async def test_edgeql_casts_collection_errors_01(self):\n        # scalar to array\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"cannot cast 'std::int64' to 'array<std::int64>'\"):\n            await self.con.execute(\"\"\"\n                SELECT <array<int64>>1;\n            \"\"\")\n\n    async def test_edgeql_casts_collection_errors_02(self):\n        # tuple to array\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"cannot cast 'tuple<std::int64>' to 'array<std::int64>'\"):\n            await self.con.execute(\"\"\"\n                SELECT <array<int64>>(1,);\n            \"\"\")\n\n    async def test_edgeql_casts_collection_errors_03(self):\n        # object to array\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"cannot cast 'std::FreeObject' to 'array<std::int64>'\"):\n            await self.con.execute(\"\"\"\n                SELECT <array<int64>>{a := 1};\n            \"\"\")\n\n    async def test_edgeql_casts_collection_errors_04(self):\n        # array to array, mismatched element types\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"while casting 'array<tuple<std::int64>>' \"\n                r\"to 'array<std::int64>', \"\n                r\"in array elements, \"\n                r\"cannot cast 'tuple<std::int64>' to 'std::int64'\"):\n            await self.con.execute(\"\"\"\n                SELECT <array<int64>>[(1,)];\n            \"\"\")\n\n    async def test_edgeql_casts_collection_errors_05(self):\n        # scalar to tuple\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"cannot cast 'std::int64' to 'tuple<std::int64>'\"):\n            await self.con.execute(\"\"\"\n                SELECT <tuple<int64>>1;\n            \"\"\")\n\n    async def test_edgeql_casts_collection_errors_06(self):\n        # array to tuple\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"cannot cast 'array<std::int64>' to 'tuple<std::int64>'\"):\n            await self.con.execute(\"\"\"\n                SELECT <tuple<int64>>[1];\n            \"\"\")\n\n    async def test_edgeql_casts_collection_errors_07(self):\n        # object to array\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"cannot cast 'std::FreeObject' to 'tuple<std::int64>'\"):\n            await self.con.execute(\"\"\"\n                SELECT <tuple<int64>>{a := 1};\n            \"\"\")\n\n    async def test_edgeql_casts_collection_errors_08(self):\n        # tuple to tuple, mismatched element types\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"while casting 'tuple<array<std::int64>>' \"\n                r\"to 'tuple<std::int64>', \"\n                r\"at tuple element '0', \"\n                r\"cannot cast 'array<std::int64>' to 'std::int64'\"):\n            await self.con.execute(\"\"\"\n                SELECT <tuple<int64>>([1],);\n            \"\"\")\n\n    async def test_edgeql_casts_collection_errors_09(self):\n        # named tuple to named tuple, use new element name\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"while casting 'tuple<b: array<std::int64>>' \"\n                r\"to 'tuple<a: std::int64>', \"\n                r\"at tuple element 'a', \"\n                r\"cannot cast 'array<std::int64>' to 'std::int64'\"):\n            await self.con.execute(\"\"\"\n                SELECT <tuple<a: int64>>(b := [1]);\n            \"\"\")\n\n    async def test_edgeql_casts_collection_errors_10(self):\n        # nested tuple to nested tuple\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"while casting 'tuple<tuple<array<std::int64>>>' \"\n                r\"to 'tuple<a: tuple<b: std::int64>>', \"\n                r\"at tuple element 'a', \"\n                r\"at tuple element 'b', \"\n                r\"cannot cast 'array<std::int64>' to 'std::int64'\"):\n            await self.con.execute(\"\"\"\n                SELECT <tuple<a: tuple<b: int64>>>(([1],),);\n            \"\"\")\n\n    async def test_edgeql_casts_collection_errors_11(self):\n        # nested array to nested array\n        # note: arrays can't be directly nested\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"while casting 'array<tuple<array<tuple<std::int64>>>>' \"\n                r\"to 'array<tuple<array<std::int64>>>', \"\n                r\"in array elements, \"\n                r\"at tuple element '0', \"\n                r\"in array elements, \"\n                r\"cannot cast 'tuple<std::int64>' to 'std::int64'\"):\n            await self.con.execute(\"\"\"\n                SELECT <array<tuple<array<int64>>>>[([(1,)],)];\n            \"\"\")\n\n    async def test_edgeql_casts_collection_errors_12(self):\n        # tuple with multiple elements, error in later element\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"while casting 'tuple<std::int64, std::int64, std::int64>' \"\n                r\"to 'tuple<std::int64, std::int64, array<std::int64>>', \"\n                r\"at tuple element '2', \"\n                r\"cannot cast 'std::int64' to 'array<std::int64>\"):\n            await self.con.execute(\"\"\"\n                SELECT <tuple<int64, int64, array<int64>>>(1, 2, 3);\n            \"\"\")\n\n    # casting into an abstract scalar should be illegal\n    async def test_edgeql_casts_illegal_01(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r\"cannot cast into generic.*'anytype'\"):\n            await self.con.execute(\"\"\"\n                SELECT <anytype>123;\n            \"\"\")\n\n    async def test_edgeql_casts_illegal_02(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r\"cannot cast into generic.*anyscalar'\"):\n            await self.con.execute(\"\"\"\n                SELECT <anyscalar>123;\n            \"\"\")\n\n    async def test_edgeql_casts_illegal_03(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r\"cannot cast into generic.*anyreal'\"):\n            await self.con.execute(\"\"\"\n                SELECT <anyreal>123;\n            \"\"\")\n\n    async def test_edgeql_casts_illegal_04(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r\"cannot cast into generic.*anyint'\"):\n            await self.con.execute(\"\"\"\n                SELECT <anyint>123;\n            \"\"\")\n\n    async def test_edgeql_casts_illegal_05(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'cannot cast.*'):\n            await self.con.execute(\"\"\"\n                SELECT <anyfloat>123;\n            \"\"\")\n\n    async def test_edgeql_casts_illegal_06(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r\"cannot cast into generic.*sequence'\"):\n            await self.con.execute(\"\"\"\n                SELECT <sequence>123;\n            \"\"\")\n\n    async def test_edgeql_casts_illegal_07(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r\"cannot cast into generic.*anytype\"):\n            await self.con.execute(\"\"\"\n                SELECT <array<anytype>>[123];\n            \"\"\")\n\n    async def test_edgeql_casts_illegal_08(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r\"cannot cast into generic.*anytype\"):\n            await self.con.execute(\"\"\"\n                SELECT <tuple<int64, anytype>>(123, 123);\n            \"\"\")\n\n    async def test_edgeql_casts_illegal_09(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"cannot cast.*std::Object.*use.*IS schema::Object.*\"):\n            await self.con.execute(\"\"\"\n                SELECT <schema::Object>std::Object;\n            \"\"\")\n\n    async def test_edgeql_casts_illegal_10(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r\"cannot cast into generic.*anyenum\"):\n            await self.con.execute(\"\"\"\n                SELECT <array<anyenum>>{};\n            \"\"\")\n\n    async def test_edgeql_casts_illegal_11(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r\"cannot cast into generic.*anyenum\"):\n            await self.con.execute(\"\"\"\n                SELECT <tuple<int64, anyenum>>{};\n            \"\"\")\n\n    async def test_edgeql_casts_illegal_12(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r\"cannot cast into generic.*anypoint\"):\n            await self.con.execute(\"\"\"\n                SELECT <range<anypoint>>{};\n            \"\"\")\n\n    async def test_edgeql_casts_illegal_13(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r\"cannot cast into generic.*anypoint\"):\n            await self.con.execute(\"\"\"\n                SELECT <multirange<anypoint>>{};\n            \"\"\")\n\n    # abstract scalar params should be illegal\n    async def test_edgeql_casts_illegal_param_01(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"parameter cannot be a generic type.*'anytype'\"):\n            await self.con.execute(\"\"\"\n                SELECT <anytype>$0;\n            \"\"\", 123)\n\n    async def test_edgeql_casts_illegal_param_02(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"parameter cannot be a generic type.*anyscalar'\"):\n            await self.con.execute(\"\"\"\n                SELECT <anyscalar>$0;\n            \"\"\", 123)\n\n    async def test_edgeql_casts_illegal_param_03(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"parameter cannot be a generic type.*anyreal'\"):\n            await self.con.execute(\"\"\"\n                SELECT <anyreal>$0;\n            \"\"\", 123)\n\n    async def test_edgeql_casts_illegal_param_04(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"parameter cannot be a generic type.*anyint'\"):\n            await self.con.execute(\"\"\"\n                SELECT <anyint>$0;\n            \"\"\", 123)\n\n    async def test_edgeql_casts_illegal_param_05(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"parameter cannot be a generic type.*anyfloat'\"):\n            await self.con.execute(\"\"\"\n                SELECT <anyfloat>$0;\n            \"\"\", 123)\n\n    async def test_edgeql_casts_illegal_param_06(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"parameter cannot be a generic type.*sequence'\"):\n            await self.con.execute(\"\"\"\n                SELECT <sequence>$0;\n            \"\"\", 123)\n\n    async def test_edgeql_casts_illegal_param_07(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"parameter cannot be a generic type.*anytype\"):\n            await self.con.execute(\"\"\"\n                SELECT <array<anytype>>$0;\n            \"\"\", [123])\n\n    async def test_edgeql_casts_illegal_param_08(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"parameter cannot be a generic type.*anytype\"):\n            await self.con.execute(\"\"\"\n                SELECT <tuple<int64, anytype>>$0;\n            \"\"\", (123, 123))\n\n    async def test_edgeql_casts_illegal_param_10(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"parameter cannot be a generic type.*anyenum\"):\n            await self.con.execute(\"\"\"\n                SELECT <array<anyenum>>$0;\n            \"\"\", [])\n\n    async def test_edgeql_casts_illegal_param_11(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"parameter cannot be a generic type.*anyenum\"):\n            await self.con.execute(\"\"\"\n                SELECT <optional tuple<int64, anyenum>>$0;\n            \"\"\", None)\n\n    async def test_edgeql_casts_illegal_param_12(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"parameter cannot be a generic type.*anypoint\"):\n            await self.con.execute(\"\"\"\n                SELECT <optional range<anypoint>>$0;\n            \"\"\", None)\n\n    async def test_edgeql_casts_illegal_param_13(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                r\"parameter cannot be a generic type.*anypoint\"):\n            await self.con.execute(\"\"\"\n                SELECT <optional multirange<anypoint>>$0;\n            \"\"\", None)\n\n    # NOTE: json is a special type as it has its own type system. A\n    # json value can be JSON array, object, boolean, number, string or\n    # null. All of these JSON types have their own semantics. Casting\n    # into json converts data into one of those specific JSON types.\n    # Any of the EdgeDB numeric types (derived from anyreal) are cast\n    # into JSON number, str is cast into JSON string, bool is cast\n    # into JSON bool. Other EdgeDB scalars (like datetime) are cast\n    # into JSON string that represents that value (similar to casting\n    # to str first). Thus json values also have some type information\n    # and when casting back to EdgeDB scalars this type information is\n    # used to determine the valid casts (e.g. it's illegal to cast a\n    # JSON string \"true\" into a bool).\n    #\n    # Casting to json is lossless (in the same way and for the same\n    # reason as casting into str).\n\n    async def test_edgeql_casts_json_01(self):\n        await self.assert_query_result(\n            r'''SELECT <bool><json>True = True;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <bool><json>False = False;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <str><json>\"Hello\" = 'Hello';''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH U := uuid_generate_v1mc()\n                SELECT <uuid><json>U = U;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <datetime><json>datetime_of_statement() =\n                    datetime_of_statement();\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <cal::local_datetime><json>cal::to_local_datetime(\n                        '2018-05-07T20:01:22.306916') =\n                    cal::to_local_datetime('2018-05-07T20:01:22.306916');\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <cal::local_date><json>cal::to_local_date('2018-05-07')\n                    = cal::to_local_date('2018-05-07');\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <cal::local_time><json>\n                    cal::to_local_time('20:01:22.306916') =\n                    cal::to_local_time('20:01:22.306916');\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <duration><json>to_duration(hours:=20) =\n                    to_duration(hours:=20);\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <int16><json>to_int16('12345') = 12345;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <int32><json>to_int32('1234567890') = 1234567890;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <int64><json>to_int64(\n                    '1234567890123') = 1234567890123;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <float32><json>to_float32('2.5') = 2.5;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <float64><json>to_float64('2.5') = 2.5;''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <bigint><json>to_bigint(\n                    '123456789123456789123456789')\n                = to_bigint(\n                    '123456789123456789123456789');\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <decimal><json>to_decimal(\n                    '123456789123456789123456789.123456789123456789123456789')\n                = to_decimal(\n                    '123456789123456789123456789.123456789123456789123456789');\n            ''',\n            [True],\n        )\n\n    async def test_edgeql_casts_json_02(self):\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <bool><json>T.p_bool = T.p_bool;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <str><json>T.p_str = T.p_str;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <datetime><json>T.p_datetime = T.p_datetime;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <cal::local_datetime><json>T.p_local_datetime =\n                    T.p_local_datetime;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <cal::local_date><json>T.p_local_date = T.p_local_date;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <cal::local_time><json>T.p_local_time = T.p_local_time;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <duration><json>T.p_duration = T.p_duration;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <int16><json>T.p_int16 = T.p_int16;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <int32><json>T.p_int32 = T.p_int32;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <int64><json>T.p_int64 = T.p_int64;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <float32><json>T.p_float32 = T.p_float32;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <float64><json>T.p_float64 = T.p_float64;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <bigint><json>T.p_bigint = T.p_bigint;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH T := (SELECT Test FILTER .p_str = 'Hello')\n                SELECT <decimal><json>T.p_decimal = T.p_decimal;\n            ''',\n            [True],\n        )\n\n    async def test_edgeql_casts_json_03(self):\n        await self.assert_query_result(\n            r'''\n                WITH\n                    T := (SELECT Test FILTER .p_str = 'Hello'),\n                    J := (SELECT JSONTest FILTER .j_str = <json>'Hello')\n                SELECT <bool>J.j_bool = T.p_bool;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH\n                    T := (SELECT Test FILTER .p_str = 'Hello'),\n                    J := (SELECT JSONTest FILTER .j_str = <json>'Hello')\n                SELECT <str>J.j_str = T.p_str;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH\n                    T := (SELECT Test FILTER .p_str = 'Hello'),\n                    J := (SELECT JSONTest FILTER .j_str = <json>'Hello')\n                SELECT <datetime>J.j_datetime = T.p_datetime;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH\n                    T := (SELECT Test FILTER .p_str = 'Hello'),\n                    J := (SELECT JSONTest FILTER .j_str = <json>'Hello')\n                SELECT <cal::local_datetime>J.j_local_datetime =\n                    T.p_local_datetime;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH\n                    T := (SELECT Test FILTER .p_str = 'Hello'),\n                    J := (SELECT JSONTest FILTER .j_str = <json>'Hello')\n                SELECT <cal::local_date>J.j_local_date = T.p_local_date;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH\n                    T := (SELECT Test FILTER .p_str = 'Hello'),\n                    J := (SELECT JSONTest FILTER .j_str = <json>'Hello')\n                SELECT <cal::local_time>J.j_local_time = T.p_local_time;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH\n                    T := (SELECT Test FILTER .p_str = 'Hello'),\n                    J := (SELECT JSONTest FILTER .j_str = <json>'Hello')\n                SELECT <duration>J.j_duration = T.p_duration;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH\n                    T := (SELECT Test FILTER .p_str = 'Hello'),\n                    J := (SELECT JSONTest FILTER .j_str = <json>'Hello')\n                SELECT <int16>J.j_int16 = T.p_int16;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH\n                    T := (SELECT Test FILTER .p_str = 'Hello'),\n                    J := (SELECT JSONTest FILTER .j_str = <json>'Hello')\n                SELECT <int32>J.j_int32 = T.p_int32;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH\n                    T := (SELECT Test FILTER .p_str = 'Hello'),\n                    J := (SELECT JSONTest FILTER .j_str = <json>'Hello')\n                SELECT <int64>J.j_int64 = T.p_int64;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH\n                    T := (SELECT Test FILTER .p_str = 'Hello'),\n                    J := (SELECT JSONTest FILTER .j_str = <json>'Hello')\n                SELECT <float32>J.j_float32 = T.p_float32;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH\n                    T := (SELECT Test FILTER .p_str = 'Hello'),\n                    J := (SELECT JSONTest FILTER .j_str = <json>'Hello')\n                SELECT <float64>J.j_float64 = T.p_float64;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH\n                    T := (SELECT Test FILTER .p_str = 'Hello'),\n                    J := (SELECT JSONTest FILTER .j_str = <json>'Hello')\n                SELECT <bigint>J.j_bigint = T.p_bigint;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH\n                    T := (SELECT Test FILTER .p_str = 'Hello'),\n                    J := (SELECT JSONTest FILTER .j_str = <json>'Hello')\n                SELECT <decimal>J.j_decimal = T.p_decimal;\n            ''',\n            [True],\n        )\n\n    async def test_edgeql_casts_json_04(self):\n        self.assertEqual(\n            await self.con.query('''\n                select <json>(\n                    select schema::Type{name} filter .name = 'std::bool'\n                )\n            '''),\n            edgedb.Set(('{\"name\": \"std::bool\"}',))\n        )\n\n    async def test_edgeql_casts_json_05(self):\n        self.assertEqual(\n            await self.con.query(\n                'select <json>{(1, 2), (3, 4)}'),\n            ['[1, 2]', '[3, 4]'])\n\n        self.assertEqual(\n            await self.con.query(\n                'select <json>{(a := 1, b := 2), (a := 3, b := 4)}'),\n            ['{\"a\": 1, \"b\": 2}', '{\"a\": 3, \"b\": 4}'])\n\n        self.assertEqual(\n            await self.con.query(\n                'select <json>{[1, 2], [3, 4]}'),\n            ['[1, 2]', '[3, 4]'])\n\n        self.assertEqual(\n            await self.con.query(\n                'select <json>{[(1, 2)], [(3, 4)]}'),\n            ['[[1, 2]]', '[[3, 4]]'])\n\n    async def test_edgeql_casts_json_06(self):\n        self.assertEqual(\n            await self.con.query_json(\n                'select <json>{(1, 2), (3, 4)}'),\n            '[[1, 2], [3, 4]]')\n\n        self.assertEqual(\n            await self.con.query_json(\n                'select <json>{[1, 2], [3, 4]}'),\n            '[[1, 2], [3, 4]]')\n\n        self.assertEqual(\n            await self.con.query_json(\n                'select <json>{[(1, 2)], [(3, 4)]}'),\n            '[[[1, 2]], [[3, 4]]]')\n\n    async def test_edgeql_casts_json_07(self):\n        # This is the same suite of tests as for str. The point is\n        # that when it comes to casting into various date and time\n        # types JSON strings and regular strings should behave\n        # identically.\n        #\n        # Canonical date and time str representations must follow ISO\n        # 8601. This test assumes that the server is configured to be\n        # in UTC time zone.\n        await self.assert_query_result(\n            r'''\n                FOR x in <json>'2018-05-07T20:01:22.306916+00:00'\n                SELECT <json><datetime>x = x;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            # validating that these are all in fact the same datetime\n            r'''\n                FOR x in <json>{\n                    '2018-05-07T15:01:22.306916-05:00',\n                    '2018-05-07T15:01:22.306916-05',\n                    '2018-05-07T20:01:22.306916Z',\n                    '2018-05-07T20:01:22.306916+0000',\n                    '2018-05-07T20:01:22.306916+00',\n                    # the '-' and ':' separators may be omitted\n                    '20180507T200122.306916+00',\n                    # acceptable RFC 3339\n                    '2018-05-07 20:01:22.306916+00:00',\n                    '2018-05-07t20:01:22.306916z',\n                }\n                SELECT <datetime>x =\n                    <datetime><json>'2018-05-07T20:01:22.306916+00:00';\n            ''',\n            [True, True, True, True, True, True, True, True],\n        )\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax'):\n            await self.con.query_single(\n                'SELECT <datetime><json>\"2018-05-07;20:01:22.306916+00:00\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax'):\n            await self.con.query_single(\n                'SELECT <datetime><json>\"2018-05-07T20:01:22.306916\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax'):\n            await self.con.query_single(\n                'SELECT <datetime><json>\"2018-05-07T20:01:22.306916 1000\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax'):\n            await self.con.query_single(\n                '''SELECT <datetime><json>\n                    \"2018-05-07T20:01:22.306916 US/Central\"\n                ''')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax'):\n            await self.con.query_single(\n                'SELECT <datetime><json>\"2018-05-07T20:01:22.306916 +GMT1\"')\n\n    async def test_edgeql_casts_json_08(self):\n        # This is the same suite of tests as for str. The point is\n        # that when it comes to casting into various date and time\n        # types JSON strings and regular strings should behave\n        # identically.\n        #\n        # Canonical date and time str representations must follow ISO\n        # 8601. This test assumes that the server is configured to be\n        # in UTC time zone.\n        await self.assert_query_result(\n            r'''\n                FOR x in <json>'2018-05-07T20:01:22.306916'\n                SELECT <json><cal::local_datetime>x = x;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            # validating that these are all in fact the same datetime\n            r'''\n                FOR x in <json>{\n                    # the '-' and ':' separators may be omitted\n                    '20180507T200122.306916',\n                    # acceptable RFC 3339\n                    '2018-05-07 20:01:22.306916',\n                    '2018-05-07t20:01:22.306916',\n                }\n                SELECT <cal::local_datetime>x =\n                    <cal::local_datetime><json>'2018-05-07T20:01:22.306916';\n            ''',\n            [True, True, True],\n        )\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                '''SELECT\n                    <cal::local_datetime><json>\"2018-05-07;20:01:22.306916\"\n                ''')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                '''SELECT <cal::local_datetime><json>\n                    \"2018-05-07T20:01:22.306916+01:00\"\n                ''')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                '''SELECT <cal::local_datetime><json>\n                    \"2018-05-07T20:01:22.306916 GMT\"''')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                '''SELECT <cal::local_datetime><json>\n                    \"2018-05-07T20:01:22.306916 GMT0\"''')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                '''SELECT <cal::local_datetime><json>\n                    \"2018-05-07T20:01:22.306916 US/Central\"\n                ''')\n\n    async def test_edgeql_casts_json_09(self):\n        # This is the same suite of tests as for str. The point is\n        # that when it comes to casting into various date and time\n        # types JSON strings and regular strings should behave\n        # identically.\n        #\n        # Canonical date and time str representations must follow ISO\n        # 8601.\n        await self.assert_query_result(\n            r'''\n                FOR x in <json>'2018-05-07'\n                SELECT <json><cal::local_date>x = x;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            # validating that these are all in fact the same date\n            r'''\n                # the '-' separators may be omitted\n                FOR x in <json>'20180507'\n                SELECT\n                    <cal::local_date>x = <cal::local_date><json>'2018-05-07';\n            ''',\n            [True],\n        )\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                'SELECT <cal::local_date><json>\"2018-05-07T20:01:22.306916\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                'SELECT <cal::local_date><json>\"2018/05/07\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                'SELECT <cal::local_date><json>\"2018.05.07\"')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                'SELECT <cal::local_date><json>\"2018-05-07+01:00\"')\n\n    async def test_edgeql_casts_json_10(self):\n        # This is the same suite of tests as for str. The point is\n        # that when it comes to casting into various date and time\n        # types JSON strings and regular strings should behave\n        # identically.\n        #\n        # Canonical date and time str representations must follow ISO\n        # 8601.\n        await self.assert_query_result(\n            r'''\n                FOR x in <json>'20:01:22.306916'\n                SELECT <json><cal::local_time>x = x;\n            ''',\n            [True],\n        )\n\n        await self.assert_query_result(\n            r'''\n                FOR x in <json>{\n                    '20:01',\n                    '20:01:00',\n                    # the ':' separators may be omitted\n                    '2001',\n                    '200100',\n                }\n                SELECT <cal::local_time>x = <cal::local_time>'20:01:00';\n            ''',\n            [True, True, True, True],\n        )\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                'invalid input syntax for type std::cal::local_time'):\n            await self.con.query_single(\n                \"SELECT <cal::local_time><json>'2018-05-07 20:01:22'\")\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'invalid input syntax for type'):\n            await self.con.query_single(\n                'SELECT <cal::local_time><json>\"20:01:22.306916+01:00\"')\n\n    async def test_edgeql_casts_json_11(self):\n        await self.assert_query_result(\n            r\"SELECT <array<int64>><json>[1, 1, 2, 3, 5]\",\n            [[1, 1, 2, 3, 5]]\n        )\n\n        # string to array\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'expected JSON array; got JSON string'):\n            await self.con.query_single(\n                r\"SELECT <array<int64>><json>'asdf'\")\n\n        # array of string to array of int\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'array<std::int64>', \"\n                r\"in array elements, \"\n                r\"expected JSON number or null; got JSON string\"):\n            await self.con.query_single(\n                r\"SELECT <array<int64>><json>['asdf']\")\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'array<std::int64>', \"\n                r\"in array elements, \"\n                r\"expected JSON number or null; got JSON string\"):\n            await self.con.query_single(\n                r\"SELECT <array<int64>>to_json('[1, 2, \\\"asdf\\\"]')\")\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'array<std::int64>', \"\n                r\"in array elements, \"\n                r\"expected JSON number or null; got JSON string\"):\n            await self.con.execute(\"\"\"\n                SELECT <array<int64>>to_json('[\"a\"]');\n            \"\"\")\n\n        # array with null to array\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'array<std::json>' \"\n                r\"to 'array<std::int64>', \"\n                r\"in array elements, \"\n                r\"invalid null value in cast\"):\n            await self.con.query_single(\n                r\"SELECT <array<int64>>[to_json('1'), to_json('null')]\")\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"array<std::int64>', \"\n                r\"in array elements, \"\n                r\"invalid null value in cast\"):\n            await self.con.query_single(\n                r\"SELECT <array<int64>>to_json('[1, 2, null]')\")\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'array<std::json>' \"\n                r\"to 'array<std::int64>', \"\n                r\"in array elements, \"\n                r\"invalid null value in cast\"):\n            await self.con.query_single(\n                r\"SELECT <array<int64>><array<json>>to_json('[1, 2, null]')\")\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'tuple<array<std::str>>', \"\n                r\"at tuple element '0', \"\n                r\"invalid null value in cast\"):\n            await self.con.query_single(\n                r\"select <tuple<array<str>>>to_json('[null]')\")\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'tuple<array<std::str>>', \"\n                r\"at tuple element '0', \"\n                r\"in array elements, \"\n                r\"invalid null value in cast\"):\n            await self.con.query_single(\n                r\"select <tuple<array<str>>>to_json('[[null]]')\")\n\n        # object to array\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"expected JSON array; got JSON object\"):\n            await self.con.execute(\"\"\"\n                SELECT <array<int64>>to_json('{\"a\": 1}');\n            \"\"\")\n\n        # array of object to array of scalar\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'array<std::int64>', \"\n                r\"in array elements, \"\n                r\"expected JSON number or null; got JSON object\"):\n            await self.con.execute(\"\"\"\n                SELECT <array<int64>>to_json('[{\"a\": 1}]');\n            \"\"\")\n\n        # nested array\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'array<tuple<array<std::str>>>', \"\n                r\"in array elements, \"\n                r\"at tuple element '0', \"\n                r\"in array elements, \"\n                r\"expected JSON string or null; got JSON number\"):\n            await self.con.execute(\"\"\"\n                SELECT <array<tuple<array<str>>>>to_json('[[[1]]]');\n            \"\"\")\n\n    async def test_edgeql_casts_json_12(self):\n        self.assertEqual(\n            await self.con.query(\n                r\"\"\"\n                    SELECT <tuple<a: int64, b: int64>>\n                    to_json('{\"a\": 1, \"b\": 2}')\n                \"\"\"\n            ),\n            [edgedb.NamedTuple(a=1, b=2)],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT <tuple<a: int64, b: int64>>\n                to_json({'{\"a\": 3000, \"b\": -1}', '{\"a\": 1, \"b\": 12}'});\n            \"\"\",\n            [{\"a\": 3000, \"b\": -1}, {\"a\": 1, \"b\": 12}],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT <tuple<int64, int64>>\n                to_json({'[3000, -1]', '[1, 12]'})\n            \"\"\",\n            [[3000, -1], [1, 12]],\n        )\n\n        self.assertEqual(\n            await self.con.query(\n                r\"\"\"\n                    SELECT <tuple<int64, int64>>\n                    to_json({'[3000, -1]', '[1, 12]'})\n                \"\"\"\n            ),\n            [(3000, -1), (1, 12)],\n        )\n\n        self.assertEqual(\n            await self.con.query(\n                r\"\"\"\n                    SELECT <tuple<json, json>>\n                    to_json({'[3000, -1]', '[1, 12]'})\n                \"\"\"\n            ),\n            [('3000', '-1'), ('1', '12')],\n        )\n\n        self.assertEqual(\n            await self.con.query(\n                r\"\"\"\n                    SELECT <tuple<json, json>>\n                    to_json({'[3000, -1]', '[1, null]'})\n                \"\"\"\n            ),\n            [('3000', '-1'), ('1', 'null')],\n        )\n\n        self.assertEqual(\n            await self.con.query_single(\n                r\"\"\"\n                    SELECT <tuple<int64, tuple<a: int64, b: int64>>>\n                    to_json('[3000, {\"a\": 1, \"b\": 2}]')\n                \"\"\"\n            ),\n            (3000, edgedb.NamedTuple(a=1, b=2))\n        )\n\n        self.assertEqual(\n            await self.con.query_single(\n                r\"\"\"\n                    SELECT <tuple<int64, array<tuple<a: int64, b: str>>>>\n                    to_json('[3000, [{\"a\": 1, \"b\": \"foo\"},\n                                     {\"a\": 12, \"b\": \"bar\"}]]')\n                \"\"\"\n            ),\n            (3000,\n             [edgedb.NamedTuple(a=1, b=\"foo\"),\n              edgedb.NamedTuple(a=12, b=\"bar\")])\n        )\n\n        # object with wrong element type to tuple\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'tuple<a: std::int64, b: std::int64>', \"\n                r\"at tuple element 'b', \"\n                r\"expected JSON number or null; got JSON string\"):\n            await self.con.query(\n                r\"\"\"\n                    SELECT <tuple<a: int64, b: int64>>\n                    to_json('{\"a\": 1, \"b\": \"2\"}')\n                \"\"\"\n            )\n\n        # object with null value to tuple\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'tuple<a: std::int64>', \"\n                r\"at tuple element 'a', \"\n                r\"invalid null value in cast\"):\n            await self.con.query(\n                r\"\"\"SELECT <tuple<a: int64>>to_json('{\"a\": null}')\"\"\"\n            )\n\n        # object with missing element to tuple\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'tuple<a: std::int64, b: std::int64>', \"\n                r\"at tuple element 'b', \"\n                r\"missing value in JSON object\"):\n            await self.con.query(\n                r\"\"\"SELECT <tuple<a: int64, b: int64>>to_json('{\"a\": 1}')\"\"\"\n            )\n\n        # short array to unnamed tuple\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'tuple<std::int64, std::int64>', \"\n                r\"at tuple element '1', \"\n                r\"missing value in JSON object\"):\n            await self.con.query(\n                r\"\"\"SELECT <tuple<int64, int64>>to_json('[3000]')\"\"\"\n            )\n\n        # array to named tuple\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'tuple<a: std::int64, b: std::int64>', \"\n                r\"at tuple element 'a', \"\n                r\"missing value in JSON object\"):\n            await self.con.query(\n                r\"\"\"\n                    SELECT <tuple<a: int64, b: int64>>\n                    to_json('[3000, 1000]')\n                \"\"\"\n            )\n\n        # string to tuple\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r'expected JSON array or object or null; got JSON string'):\n            await self.con.query(\n                r\"\"\"SELECT <tuple<a: int64, b: int64>> to_json('\"test\"')\"\"\"\n            )\n\n        # short array to unnamed tuple\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'tuple<std::json, std::json>', \"\n                r\"at tuple element '1', \"\n                r\"missing value in JSON object\"):\n            await self.con.query(\n                r\"\"\"SELECT <tuple<json, json>> to_json('[3000]')\"\"\"\n            )\n\n        # object to unnamed tuple\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' to \"\n                r\"'tuple<std::int64>', \"\n                r\"at tuple element '0', \"\n                r\"missing value in JSON object\"):\n            await self.con.execute(\"\"\"\n                SELECT <tuple<int64>>to_json('{\"a\": 1}');\n            \"\"\")\n\n        # nested tuple\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'tuple<a: tuple<b: std::str>>', \"\n                r\"at tuple element 'a', \"\n                r\"at tuple element 'b', \"\n                r\"expected JSON string or null; got JSON number\"):\n            await self.con.execute(\"\"\"\n                SELECT <tuple<a: tuple<b: str>>>to_json('{\"a\": {\"b\": 1}}');\n            \"\"\")\n\n        # array with null to tuple\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'tuple<std::int64, std::int64>', \"\n                r\"at tuple element '1', \"\n                r\"invalid null value in cast\"):\n            await self.con.execute(\"\"\"\n                SELECT <tuple<int64, int64>>to_json('[1, null]');\n            \"\"\")\n\n        # object with null to tuple\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'tuple<a: std::int64>', \"\n                r\"at tuple element 'a', \"\n                r\"invalid null value in cast\"):\n            await self.con.execute(\"\"\"\n                SELECT <tuple<a: int64>>to_json('{\"a\": null}');\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'tuple<a: array<std::int64>>', \"\n                r\"at tuple element 'a', \"\n                r\"invalid null value in cast\"):\n            await self.con.execute(\"\"\"\n                SELECT <tuple<a: array<int64>>>to_json('{\"a\": null}');\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'tuple<a: tuple<b: std::str>>', \"\n                r\"at tuple element 'a', \"\n                r\"invalid null value in cast\"):\n            await self.con.execute(\"\"\"\n                SELECT <tuple<a: tuple<b: str>>>to_json('{\"a\": null}');\n            \"\"\")\n\n    async def test_edgeql_casts_json_13(self):\n        await self.assert_query_result(\n            r'''\n                select <array<json>>to_json('null')\n            ''',\n            [],\n        )\n\n        await self.assert_query_result(\n            r'''\n                select <array<str>>to_json('null')\n            ''',\n            [],\n        )\n\n        await self.assert_query_result(\n            r'''\n                select <array<int64>>json_get(to_json('{}'), 'foo')\n            ''',\n            [],\n        )\n\n        await self.assert_query_result(\n            r'''\n                select <tuple<str>>to_json('null')\n            ''',\n            [],\n        )\n\n        await self.assert_query_result(\n            r'''\n                select <tuple<json>>to_json('null')\n            ''',\n            [],\n        )\n\n        await self.assert_query_result(\n            r'''\n                select <bigint>to_json('null')\n            ''',\n            [],\n        )\n\n        await self.assert_query_result(\n            r'''\n                select <decimal>to_json('null')\n            ''',\n            [],\n        )\n\n        await self.assert_query_result(\n            r'''\n                select <bigint><str>to_json('null')\n            ''',\n            [],\n        )\n\n        await self.assert_query_result(\n            r'''\n                select <decimal><str>to_json('null')\n            ''',\n            [],\n        )\n\n    async def test_edgeql_casts_json_14(self):\n        await self.assert_query_result(\n            r'''\n                select <array<json>>to_json('[]')\n            ''',\n            [[]],\n        )\n\n        await self.assert_query_result(\n            r'''\n                select <array<str>>to_json('[]')\n            ''',\n            [[]],\n        )\n\n    async def test_edgeql_casts_json_15(self):\n        # At one point, a cast from an object inside a binary\n        # operation triggered an infinite loop in staeval if the\n        # object had a self link.\n        await self.con.execute('''\n            create type Z { create link z -> Z; };\n        ''')\n        await self.con.query('''\n            select <json>Z union <json>Z;\n        ''')\n\n    async def test_edgeql_casts_json_16(self):\n        # number to range\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"expected JSON object or null; got JSON number\"):\n            await self.con.execute(\"\"\"\n                SELECT <range<int64>>to_json('1');\n            \"\"\")\n\n        # array to range\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"expected JSON object or null; got JSON array\"):\n            await self.con.execute(\"\"\"\n                SELECT <range<int64>>to_json('[1]');\n            \"\"\")\n\n        # object to range, bad empty\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'range<std::int64>', \"\n                r\"in range parameter 'empty', \"\n                r\"expected JSON boolean or null; got JSON number\"):\n            await self.con.execute(\"\"\"\n                SELECT <range<int64>>to_json('{\n                    \"empty\": 1\n                }');\n            \"\"\")\n\n        # object to range, empty with distinct lower and upper\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"conflicting arguments in range constructor: 'empty' is \"\n                r\"`true` while the specified bounds suggest otherwise\"):\n            await self.con.execute(\"\"\"\n                SELECT <range<int64>>to_json('{\n                    \"empty\": true,\n                    \"lower\": 1,\n                    \"upper\": 2\n                }');\n            \"\"\")\n\n        # object to range, empty with same lower and upper\n        # and inc_lower and inc_upper\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"conflicting arguments in range constructor: 'empty' is \"\n                r\"`true` while the specified bounds suggest otherwise\"):\n            await self.con.execute(\"\"\"\n                SELECT <range<int64>>to_json('{\n                    \"empty\": true,\n                    \"lower\": 1,\n                    \"upper\": 2,\n                    \"inc_lower\": true,\n                    \"inc_upper\": true\n                }');\n            \"\"\")\n\n        # object to range, missing inc_lower\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"JSON object representing a range must include an \"\n                r\"'inc_lower'\"):\n            await self.con.execute(\"\"\"\n                SELECT <range<int64>>to_json('{\n                    \"inc_upper\": false\n                }');\n            \"\"\")\n\n        # object to range, missing inc_upper\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"JSON object representing a range must include an \"\n                r\"'inc_upper'\"):\n            await self.con.execute(\"\"\"\n                SELECT <range<int64>>to_json('{\n                    \"inc_lower\": false\n                }');\n            \"\"\")\n\n        # object to range, bad inc_lower\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'range<std::int64>', \"\n                r\"in range parameter 'inc_lower', \"\n                r\"expected JSON boolean or null; got JSON number\"):\n            await self.con.execute(\"\"\"\n                SELECT <range<int64>>to_json('{\n                    \"inc_lower\": 1,\n                    \"inc_upper\": false\n                }');\n            \"\"\")\n\n        # object to range, bad inc_upper\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"while casting 'std::json' \"\n                r\"to 'range<std::int64>', \"\n                r\"in range parameter 'inc_upper', \"\n                r\"expected JSON boolean or null; got JSON number\"):\n            await self.con.execute(\"\"\"\n                SELECT <range<int64>>to_json('{\n                    \"inc_lower\": false,\n                    \"inc_upper\": 1\n                }');\n            \"\"\")\n\n        # object to range, extra parameters\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"JSON object representing a range contains unexpected keys: \"\n                r\"bar, foo\"):\n            await self.con.execute(\"\"\"\n                SELECT <range<int64>>to_json('{\n                    \"lower\": 1,\n                    \"upper\": 2,\n                    \"inc_lower\": true,\n                    \"inc_upper\": true,\n                    \"foo\": \"foo\",\n                    \"bar\": \"bar\"\n                }');\n            \"\"\")\n\n    async def test_edgeql_casts_json_17(self):\n        # number to multirange\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"expected JSON array; got JSON number\"):\n            await self.con.execute(\"\"\"\n                SELECT <multirange<int64>>to_json('1');\n            \"\"\")\n\n        # object to multirange\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                r\"expected JSON array; got JSON object\"):\n            await self.con.execute(\"\"\"\n                SELECT <multirange<int64>>to_json('{\"a\": 1}');\n            \"\"\")\n\n    async def test_edgeql_casts_multirange_set_01(self):\n        await self.assert_query_result(\n            r\"\"\"\n                select count(\n                  <multirange<int32>>{range(0, 10), range(12, 15)}\n                 );\n            \"\"\",\n            [2],\n        )\n\n    async def test_edgeql_casts_assignment_01(self):\n        async with self._run_and_rollback():\n            await self.con.execute(r\"\"\"\n\n                # int64 is assignment castable or implicitly castable\n                # into any other numeric type\n                INSERT ScalarTest {\n                    p_int16 := 1,\n                    p_int32 := 1,\n                    p_int64 := 1,\n                    p_float32 := 1,\n                    p_float64 := 1,\n                    p_bigint := 1,\n                    p_decimal := 1,\n                };\n            \"\"\")\n\n            await self.assert_query_result(\n                r\"\"\"\n                    SELECT ScalarTest {\n                        p_int16,\n                        p_int32,\n                        p_int64,\n                        p_float32,\n                        p_float64,\n                        p_bigint,\n                        p_decimal,\n                    };\n                \"\"\",\n                [{\n                    'p_int16': 1,\n                    'p_int32': 1,\n                    'p_int64': 1,\n                    'p_float32': 1,\n                    'p_float64': 1,\n                    'p_bigint': 1,\n                    'p_decimal': 1,\n                }],\n            )\n\n    async def test_edgeql_casts_assignment_02(self):\n        async with self._run_and_rollback():\n            await self.con.execute(r\"\"\"\n\n                # float64 is assignment castable to float32\n                INSERT ScalarTest {\n                    p_float32 := 1.5,\n                };\n            \"\"\")\n\n            await self.assert_query_result(\n                r\"\"\"\n                    SELECT ScalarTest {\n                        p_float32,\n                    };\n                \"\"\",\n                [{\n                    'p_float32': 1.5,\n                }],\n            )\n\n    async def test_edgeql_casts_assignment_03(self):\n        async with self._run_and_rollback():\n            # in particular, bigint and decimal are not assignment-castable\n            # into any other numeric type\n            for typename in ['int16',\n                             'int32',\n                             'int64',\n                             'float32',\n                             'float64']:\n\n                for numtype in {'bigint', 'decimal'}:\n\n                    query = f'''\n                        INSERT ScalarTest {{\n                            p_{typename} := <{numtype}>3,\n                            p_{numtype} := 1001,\n                        }};\n                    '''\n                    async with self.assertRaisesRegexTx(\n                            edgedb.QueryError,\n                            r'invalid target for property',\n                            msg=query):\n                        await self.con.execute(query + f'''\n                            # clean up, so other tests can proceed\n                            DELETE (\n                                SELECT ScalarTest\n                                FILTER .p_{numtype} = 1001\n                            );\n                        ''')\n\n    async def test_edgeql_casts_custom_scalar_01(self):\n        await self.assert_query_result(\n            '''\n                SELECT <custom_str_t>'ABC'\n            ''',\n            ['ABC']\n        )\n\n        async with self.assertRaisesRegexTx(\n                edgedb.ConstraintViolationError,\n                'invalid custom_str_t'):\n            await self.con.query(\n                \"SELECT <custom_str_t>'123'\")\n\n    async def test_edgeql_casts_custom_scalar_02(self):\n        await self.assert_query_result(\n            \"\"\"\n                SELECT <foo><bar>'test'\n            \"\"\",\n            ['test'],\n        )\n\n        await self.assert_query_result(\n            \"\"\"\n                SELECT <array<foo>><array<bar>>['test']\n            \"\"\",\n            [['test']],\n        )\n\n    async def test_edgeql_casts_custom_scalar_03(self):\n        await self.assert_query_result(\n            \"\"\"\n                SELECT <array<custom_str_t>><array<bar>>['TEST']\n            \"\"\",\n            [['TEST']],\n        )\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError, r'invalid'\n        ):\n            await self.con.query(\"\"\"\n                SELECT <custom_str_t><bar>'test'\n            \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError, r'invalid'\n        ):\n            await self.con.query(\"\"\"\n                SELECT <array<custom_str_t>><array<bar>>['test']\n            \"\"\")\n\n    async def test_edgeql_casts_custom_scalar_04(self):\n        await self.con.execute('''\n            create abstract scalar type abs extending int64;\n            create scalar type foo2 extending abs;\n            create scalar type bar2 extending abs;\n        ''')\n\n        await self.assert_query_result(\n            \"\"\"\n                SELECT <foo2><bar2>42\n            \"\"\",\n            [42],\n        )\n\n        await self.assert_query_result(\n            \"\"\"\n                SELECT <array<foo2>><array<bar2>>[42]\n            \"\"\",\n            [[42]],\n        )\n\n    async def test_edgeql_casts_custom_scalar_05(self):\n        await self.con.execute('''\n            create abstract scalar type xfoo extending int64;\n            create abstract scalar type xbar extending int64;\n            create scalar type bar1 extending xfoo, xbar;\n            create scalar type bar2 extending xfoo, xbar;\n        ''')\n\n        await self.assert_query_result(\n            \"\"\"\n                SELECT <bar1><bar2>42\n            \"\"\",\n            [42],\n        )\n\n        await self.assert_query_result(\n            \"\"\"\n                SELECT <array<bar1>><array<bar2>>[42]\n            \"\"\",\n            [[42]],\n        )\n\n    async def test_edgeql_casts_custom_scalar_06(self):\n        await self.con.execute(\n            '''\n            create scalar type x extending str {\n                create constraint expression on (false)\n            };\n        '''\n        )\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError, 'invalid x'\n        ):\n            await self.con.query(\"\"\"SELECT <x>42\"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError, 'invalid x'\n        ):\n            await self.con.query(\"\"\"SELECT <x>to_json('\"a\"')\"\"\")\n\n    async def test_edgeql_casts_tuple_params_01(self):\n        # insert tuples into a nested array\n        def nest(data):\n            return [(nest(x),) if isinstance(x, list) else x for x in data]\n\n        tests = {\n            # Basic tuples\n            'tuple<str, bool>': [('x', True), ('y', False)],\n            'optional tuple<str, bool>': [('x', True), None],\n\n            # Some pointlessly nested tuples\n            'tuple<tuple<str, bool>>': [(('x', True),)],\n            'tuple<tuple<str, bool>, int64>': [(('x', True), 1)],\n\n            # Basic array examples\n            'array<tuple<int64, str>>': [\n                [],\n                [(0, 'zero')],\n                [(0, 'zero'), (1, 'one')],\n            ],\n\n            'optional array<tuple<int64, str>>': [\n                None,\n                [],\n                [(0, 'zero')],\n                [(0, 'zero'), (1, 'one')],\n            ],\n\n            'array<tuple<str, array<int64>>>': [\n                [],\n                [('x', [])],\n                [('x', [1])],\n                [('x', []), ('y', []), ('z', [])],\n                [('x', [1]), ('y', []), ('z', [])],\n                [('x', []), ('y', [1]), ('z', [])],\n                [('x', []), ('y', []), ('z', [1])],\n                [('x', []), ('y', [1, 2]), ('z', [1, 2, 3])],\n            ],\n\n            # Arrays of pointlessly nested tuples\n            'array<tuple<tuple<str, bool>, int64>>': [\n                [],\n                [(('x', True), 1)],\n                [(('x', True), 1), (('z', False), 2)],\n            ],\n            'array<tuple<tuple<array<str>, bool>, int64>>': [\n                [],\n                [(([], True), 1)],\n                [((['x', 'y', 'z'], True), 1), ((['z'], False), 2)],\n            ],\n\n            # Using tuples to produce just a pure nested array\n            'array<tuple<array<int64>>>': [nest(x) for x in [\n                [],\n                [[], []],\n                [[], [], []],\n                [[1], [], []],\n                [[], [1], []],\n                [[], [], [1]],\n                [[1, 2, 3], [], [4, 5, 6]],\n                [[1], [2, 3], [4, 5, 6]],\n            ]],\n\n            'array<tuple<array<tuple<array<int64>>>>>': [nest(x) for x in [\n                [],\n                [[], []],\n                [[], [], []],\n                [[[], [], []], [[], []], [[]]],\n                [[[1]], [], []],\n                [[], [[1]], []],\n                [[], [], [[1]]],\n                [[[1, 2, 3], [], [4, 5, 6]]],\n                [[[1, 2, 3], []], [[4, 5, 6]]],\n                [[[1, 2], [3]], [], [[4, 5], [6]]],\n            ]],\n        }\n\n        for typ, vals in tests.items():\n            qry = f\"SELECT <{typ}>$0\"\n            for val in vals:\n                await self.assert_query_result(\n                    qry,\n                    [v for v in [val] if v is not None],\n                    variables=(val,),\n                    msg=f'type: {typ}, data: {val}',\n                )\n\n    async def test_edgeql_casts_tuple_params_02(self):\n        await self.assert_query_result(\n            '''\n            SELECT Test {\n                id,\n                num := (<tuple<int64, float64, str, bytes>>$tup).0,\n                st := (<tuple<int64, float64, str, bytes>>$tup).2,\n            };\n            ''',\n            [{'num': 0, 'st': \"str\"}],\n            variables={'tup': (0, 1.0, \"str\", b\"bytes\")},\n        )\n\n    async def test_edgeql_casts_tuple_params_03(self):\n        # try *doing* something with the input\n        await self.con.query(\n            r'''\n            create type Record {\n                 create required property name -> str;\n                 create multi property tags -> int64;\n            }\n            '''\n        )\n\n        data = [\n            [],\n            [('x', [])],\n            [('x', [1])],\n            [('x', []), ('y', []), ('z', [])],\n            [('x', [1]), ('y', []), ('z', [])],\n            [('x', []), ('y', [1]), ('z', [])],\n            [('x', []), ('y', []), ('z', [1])],\n            [('x', []), ('y', [1, 2]), ('z', [1, 2, 3])],\n        ]\n\n        qry = r'''\n        for row in array_unpack(<array<tuple<str, array<int64>>>>$0) union ((\n            insert Record { name := row.0, tags := array_unpack(row.1) }\n        ))\n        '''\n\n        for inp in data:\n            exp = tb.bag([\n                {'name': name, 'tags': tb.bag(tags)}\n                for name, tags in inp\n            ])\n\n            async with self._run_and_rollback():\n                await self.con.execute(qry, inp)\n\n                await self.assert_query_result(\n                    '''\n                    select Record { name, tags }\n                    ''',\n                    exp,\n                    msg=f'inp: {inp}',\n                )\n\n    async def test_edgeql_casts_tuple_params_04(self):\n        # Test doing a coalesce on an optional tuple input\n        await self.assert_query_result(\n            '''\n            select (<optional tuple<str, int64>>$0) ?? ('foo', 0)\n            ''',\n            [('foo', 0)],\n            variables=(None,),\n        )\n\n    async def test_edgeql_casts_tuple_params_05(self):\n        max_depth = 20\n\n        # Test deep nesting\n        t = 'int64'\n        v = 0\n        for _ in range(max_depth):\n            t = f'tuple<{t}>'\n            v = (v,)\n\n        await self.assert_query_result(\n            f'''\n            select <{t}>$0\n            ''',\n            [v],\n            variables=(v,),\n        )\n\n        # One more and it should fail\n        t = f'tuple<{t}>'\n        v = (v,)\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError, r'too deeply nested'):\n            await self.con.query(f\"\"\"\n                select <{t}>$0\n            \"\"\", v)\n\n    async def test_edgeql_casts_tuple_params_06(self):\n        # Test multiple tuple params mixed with other stuff\n        await self.assert_query_result(\n            '''\n            select\n                (<tuple<str, str>>$0).0 ++ <str>$1 ++ (<tuple<str, str>>$0).1\n            ''',\n            ['foo bar'],\n            variables=(('foo', 'bar'), ' ',),\n        )\n        await self.assert_query_result(\n            '''\n            select\n                (<tuple<str, str>>$0).0 ++ <str>$1 ++ (<tuple<str, str>>$0).1\n                ++ '!'\n            ''',\n            ['foo bar!'],\n            variables=(('foo', 'bar'), ' ',),\n        )\n        await self.assert_query_result(\n            '''\n            select\n                (<tuple<str, str>>$0).0 ++ <str>$1 ++ (<tuple<str, str>>$0).1\n                ++ (with z := (<tuple<str, str>>$2) select (z.0 ++ z.1))\n                ++ '!'\n            ''',\n            ['foo barxy!'],\n            variables=(('foo', 'bar'), ' ', ('x', 'y')),\n        )\n\n    async def test_edgeql_casts_tuple_params_07(self):\n        await self.assert_query_result(\n            '''\n            select <tuple<name: str, flag: bool>>$0\n            ''',\n            [{'name': 'a', 'flag': True}],\n            # The server supports named tuple input, but edgedb-python\n            # doesn't let you specify them nicely.\n            variables=(('a', True),)\n        )\n\n    async def test_edgeql_casts_tuple_params_08(self):\n        await self.assert_query_result(\n            '''\n            select { x := <optional tuple<str, str>>$0, y := <str>$1 };\n            ''',\n            [{'x': None, 'y': \"test\"}],\n            variables=(None, 'test'),\n        )\n\n        await self.assert_query_result(\n            '''\n            select { x := <optional tuple<str, str>>$0, y := <int64>$1 };\n            ''',\n            [{'x': None, 'y': 11111}],\n            variables=(None, 11111),\n        )\n\n    async def test_edgeql_casts_tuple_params_09(self):\n        await self.con.query('''\n            WITH\n              p := <tuple<test: str>>$0\n            insert Test { p_tup := p };\n        ''', ('foo',))\n\n        await self.assert_query_result(\n            '''\n            select Test { p_tup } filter exists .p_tup\n            ''',\n            [{'p_tup': {'test': 'foo'}}],\n        )\n        await self.assert_query_result(\n            '''\n            WITH\n              p := <tuple<test: str>>$0\n            select p\n            ''',\n            [{'test': 'foo'}],\n            variables=(('foo',),),\n        )\n        await self.assert_query_result(\n            '''\n            select <tuple<test: str>>$0\n            ''',\n            [{'test': 'foo'}],\n            variables=(('foo',),),\n        )\n        await self.assert_query_result(\n            '''\n            select <array<tuple<test: str>>>$0\n            ''',\n            [[{'test': 'foo'}, {'test': 'bar'}]],\n            variables=([('foo',), ('bar',)],),\n        )\n\n    async def test_edgeql_cast_empty_set_to_array_01(self):\n        await self.assert_query_result(\n            r'''\n                SELECT <array<Object>>{};\n            ''',\n            [],\n        )\n\n    async def test_edgeql_casts_std_enum_01(self):\n        await self.assert_query_result(\n            '''\n            select <schema::Cardinality>{}\n            ''',\n            [],\n        )\n\n    async def test_edgeql_casts_json_set_02(self):\n        await self.assert_query_result(\n            '''\n            select <tuple<str>>json_set(\n                to_json('[\"b\"]'), \"0\", value := <json>\"a\");\n            ''',\n            [('a',)],\n        )\n\n    async def test_edgeql_casts_all_null(self):\n        # For *every* cast, try casting a value we know\n        # will be represented as NULL.\n        casts = await self.con.query('''\n            select schema::Cast { from_type: {name}, to_type: {name} }\n            filter not .from_type is schema::ObjectType\n        ''')\n\n        def _t(s):\n            # Instantiate polymorphic types\n            return (\n                s\n                .replace('anytype', 'str')\n                .replace('anytuple', 'tuple<str, int64>')\n                .replace('std::anyenum', 'schema::Cardinality')\n                .replace('std::anypoint', 'int64')\n            )\n\n        from_types = {_t(cast.from_type.name) for cast in casts}\n        type_keys = {\n            name: f'x{i}' for i, name in enumerate(sorted(from_types))\n        }\n        # Populate a type that has an optional field for each cast source type\n        sep = '\\n                '\n        props = sep.join(\n            f'CREATE PROPERTY {n} -> {_t(t)};'\n            for t, n in type_keys.items()\n        )\n        setup = f'''\n            CREATE TYPE Null {{\n                {props}\n            }};\n            INSERT Null;\n        '''\n        await self.con.execute(setup)\n\n        # Do each cast\n        for cast in casts:\n            prop = type_keys[_t(cast.from_type.name)]\n\n            await self.assert_query_result(\n                f'''\n                SELECT Null {{\n                    res := <{_t(cast.to_type.name)}>.{prop}\n                }}\n                ''',\n                [{\"res\": None}],\n                msg=f'{cast.from_type.name} to {cast.to_type.name}',\n            )\n\n            # For casts from JSON, also do the related operation of\n            # casting from a JSON null, which should produce an empty\n            # set.\n            if cast.from_type.name == 'std::json':\n                await self.assert_query_result(\n                    f'''\n                    SELECT <{_t(cast.to_type.name)}>to_json('null')\n                    ''',\n                    [],\n                    msg=f'json null cast to {cast.to_type.name}',\n                )\n\n    async def test_edgeql_casts_uuid_to_object(self):\n        persons = await self.con.query('select Person { id }')\n\n        dummy_uuid = '1' * 32\n\n        res = await self.con.query('select <Person><uuid>$0', persons[0].id)\n        self.assertEqual(len(res), 1)\n\n        async with self.assertRaisesRegexTx(\n            edgedb.CardinalityViolationError, r'with id .* does not exist'\n        ):\n            await self.con.query('select <Person><uuid>$0', dummy_uuid)\n\n        await self.assert_query_result(\n            '''\n            select (<Person>{<uuid>$0, <uuid>$1}) { name }\n            order by .name;\n            ''',\n            [{'name': 'kelly'}, {'name': 'tom'}],\n            json_only=True,\n            variables=(persons[0].id, persons[1].id),\n        )\n\n        async with self.assertRaisesRegexTx(\n            edgedb.CardinalityViolationError, r'with id .* does not exist'\n        ):\n            await self.con.query(\n                '''\n                select (<Person>{<uuid>$0, <uuid>$1}) { name }\n                order by .name;\n                ''', persons[0].id, dummy_uuid\n            )\n\n        res = await self.con.query(\n            'select <Person><optional uuid>$0', persons[0].id\n        )\n        self.assertEqual(len(res), 1)\n\n        res = await self.con.query(\n            'select <optional Person><optional uuid>$0', None\n        )\n        self.assertEqual(len(res), 0)\n\n        res = await self.con.query('select <optional Person><optional uuid>{}')\n        self.assertEqual(len(res), 0)\n\n        res = await self.con.query('select <Person><optional uuid>$0', None)\n        self.assertEqual(len(res), 0)\n\n        res = await self.con.query('select <Person>$0', persons[0].id)\n        self.assertEqual(len(res), 1)\n\n        res = await self.con.query('select <optional Person>$0', None)\n        self.assertEqual(len(res), 0)\n\n        async with self.assertRaisesRegexTx(\n            edgedb.CardinalityViolationError, r'with id .* does not exist'\n        ):\n            await self.con.query('select <optional Person>$0', dummy_uuid)\n\n        async with self.assertRaisesRegexTx(\n            edgedb.CardinalityViolationError, r'with id .* does not exist'\n        ):\n            await self.con.query('select <Person>$0', dummy_uuid)\n"
  },
  {
    "path": "tests/test_edgeql_data_migration.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import annotations\nfrom typing import Optional, Iterable, Iterator\n\nimport json\nimport os.path\nimport re\nimport textwrap\nimport unittest\nimport uuid\n\nimport edgedb\n\nfrom edb.common import assert_data_shape\n\nfrom edb.testbase import server as tb\nfrom edb.testbase import serutils\nfrom edb.tools import test\n\n\nclass EdgeQLDataMigrationTestCase(tb.DDLTestCase):\n    \"\"\"Test that migrations preserve data under certain circumstances.\n\n    Renaming, changing constraints, increasing cardinality should not\n    destroy data.\n\n    Some of the test cases here use the same migrations as\n    `test_schema_migrations_equivalence`, therefore the test numbers\n    should match for easy reference, even if it means skipping some.\n    \"\"\"\n\n    DEFAULT_MODULE = 'test'\n\n    def setUp(self):\n        super().setUp()\n        self._ignore = self.ignore_warnings(\n            'Non-simple_scoping will be removed'\n        )\n        self._ignore.__enter__()\n\n    def tearDown(self):\n        super().tearDown()\n        self._ignore.__exit__(None, None, None)\n\n    def normalize_statement(self, s: str) -> str:\n        re_filter = re.compile(r'[\\s]+|(#.*?(\\n|$))|(,(?=\\s*[})]))')\n        stripped = textwrap.dedent(s.lstrip('\\n')).rstrip('\\n')\n        folded = re_filter.sub('', stripped).lower()\n        return folded\n\n    def cleanup_migration_exp_json(self, exp_result_json):\n        # Cleanup the expected values by dedenting/stripping them\n        if 'confirmed' in exp_result_json:\n            exp_result_json['confirmed'] = [\n                self.normalize_statement(v)\n                for v in exp_result_json['confirmed']\n            ]\n        if (\n            'proposed' in exp_result_json\n            and exp_result_json['proposed']\n            and 'statements' in exp_result_json['proposed']\n        ):\n            for stmt in exp_result_json['proposed']['statements']:\n                stmt['text'] = self.normalize_statement(stmt['text'])\n\n    async def assert_describe_migration(self, exp_result_json, *, msg=None):\n        self.cleanup_migration_exp_json(exp_result_json)\n\n        try:\n            res = await self.con.query_single(\n                'DESCRIBE CURRENT MIGRATION AS JSON;')\n\n            res = json.loads(res)\n            self.cleanup_migration_exp_json(res)\n            assert_data_shape.assert_data_shape(\n                res, exp_result_json, self.fail, message=msg)\n        except Exception:\n            self.add_fail_notes(serialization='json')\n            raise\n\n    async def fast_forward_describe_migration(\n        self,\n        *,\n        limit: Optional[int] = None,\n        user_input: Optional[Iterable[str]] = None,\n        commit: bool = True,\n    ):\n        '''Repeatedly get the next step from DESCRIBE and execute it.\n\n        The point of this as opposed to just using \"POPULATE\n        MIGRATION; COMMIT MIGRATION;\" is that we want to make sure\n        that the generated DDL is valid and in case it's not, narrow\n        down which step is causing issues.\n        '''\n\n        # Keep track of proposed DDL\n        prevddl = ''\n\n        if user_input is None:\n            input_iter: Iterator[str] = iter(tuple())\n        else:\n            input_iter = iter(user_input)\n\n        try:\n            step = 0\n            while True:\n                mig = await self.con.query_single(\n                    'DESCRIBE CURRENT MIGRATION AS JSON;')\n                mig = json.loads(mig)\n                if mig['proposed'] is None:\n                    assert_data_shape.assert_data_shape(\n                        mig, {'complete': True},\n                        self.fail,\n                        message='No more \"proposed\", but not \"completed\" '\n                                'either.'\n                    )\n                    if commit:\n                        await self.con.execute('COMMIT MIGRATION;')\n                    break\n\n                interpolations = {}\n\n                user_input_reqs = mig['proposed']['required_user_input']\n                if user_input_reqs:\n                    for var in user_input_reqs:\n                        var_name = var['placeholder']\n                        var_desc = var['prompt']\n                        try:\n                            var_value = next(input_iter)\n                        except StopIteration:\n                            raise AssertionError(\n                                f'missing input value for prompt: {var_desc}'\n                            ) from None\n\n                        interpolations[var_name] = var_value\n\n                for stmt in mig['proposed']['statements']:\n                    curddl = stmt['text']\n\n                    if interpolations:\n                        def _replace(match, interpolations=interpolations):\n                            var_name = match.group(1)\n                            var_value = interpolations.get(var_name)\n                            if var_value is None:\n                                raise AssertionError(\n                                    f'missing value for '\n                                    f'placeholder {var_name!r}'\n                                )\n                            return var_value\n\n                        curddl = re.sub(r'\\\\\\((\\w+)\\)', _replace, curddl)\n\n                    if prevddl == curddl:\n                        raise Exception(\n                            f\"Repeated previous proposed DDL {curddl!r}\"\n                        )\n                    try:\n                        await self.con.execute(curddl)\n                    except Exception as exc:\n                        raise Exception(\n                            f\"Error while processing {curddl!r}\"\n                        ) from exc\n                    prevddl = curddl\n                step += 1\n                if limit is not None and step == limit:\n                    break\n        except Exception:\n            self.add_fail_notes(serialization='json')\n            raise\n\n    async def start_migration(\n        self,\n        migration,\n        *,\n        populate: bool = False,\n        module: str | None = 'test',\n        explicit_modules: bool = False,\n    ):\n        if explicit_modules or module is None:\n            migration_text = migration\n        else:\n            migration_text = f'''\n                module {module} {{\n                    {migration}\n                }}\n            '''\n\n        mig = f\"\"\"\n            START MIGRATION TO {{\n                {migration_text}\n            }};\n        \"\"\"\n        await self.con.execute(mig)\n        if populate:\n            await self.con.execute('POPULATE MIGRATION;')\n\n    async def migrate(\n        self,\n        migration,\n        *,\n        populate: bool = False,\n        module: str | None = 'test',\n        explicit_modules: bool = False,\n        user_input: Optional[Iterable[str]] = None,\n    ):\n        async with self.con.transaction():\n            await self.start_migration(\n                migration,\n                populate=populate,\n                module=module,\n                explicit_modules=explicit_modules,\n            )\n            await self.fast_forward_describe_migration(user_input=user_input)\n        await self.assert_last_migration()\n\n    async def interact(self, parts, check_complete=True):\n        for part in parts:\n            if isinstance(part, str):\n                prompt = part\n                ans = \"y\"\n                user_input = None\n            else:\n                prompt, ans, *user_input = part\n\n            await self.assert_describe_migration({\n                'proposed': {'prompt': prompt}\n            })\n\n            if ans == \"y\":\n                await self.fast_forward_describe_migration(\n                    limit=1, user_input=user_input)\n            else:\n                await self.con.execute('''\n                    ALTER CURRENT MIGRATION REJECT PROPOSED;\n                ''')\n\n        if check_complete:\n            await self.assert_describe_migration({\n                'complete': True\n            })\n\n    async def assert_last_migration(self):\n        last_name = await self.con.query_single(\n            '''\n            select assert_single(\n                sys::Branch.last_migration\n                    filter sys::Branch.name = sys::get_current_database());\n            '''\n        )\n        last_mig = await self.con.query_single(\n            '''\n            select assert_single(\n                schema::Migration { name } filter not exists .<parents);\n            '''\n        )\n        if last_mig:\n            self.assertEqual(last_mig.name, last_name)\n        else:\n            self.assertIsNone(last_name)\n\n\nclass TestEdgeQLDataMigration(EdgeQLDataMigrationTestCase):\n    async def test_edgeql_migration_simple_01(self):\n        # Base case, ensuring a single SDL migration from a clean\n        # state works.\n        await self.migrate(\"\"\"\n            type NamedObject {\n                required property name -> str;\n                multi link related -> NamedObject {\n                    property lang -> str;\n                };\n            };\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n\n            INSERT NamedObject {\n                name := 'Test'\n            };\n\n            INSERT NamedObject {\n                name := 'Test 2',\n                related := (SELECT DETACHED NamedObject\n                            FILTER .name = 'Test')\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT\n                    NamedObject {\n                        related: {\n                            name,\n                            @lang\n                        }\n                    }\n                FILTER\n                    .name = 'Test 2';\n            \"\"\",\n            [\n                {\n                    'related': [{'name': 'Test', '@lang': None}],\n                }\n            ]\n        )\n\n    async def test_edgeql_migration_link_inheritance(self):\n        schema_f = os.path.join(os.path.dirname(__file__), 'schemas',\n                                'links_1.esdl')\n\n        with open(schema_f) as f:\n            schema = f.read()\n\n        await self.migrate(schema)\n\n        await self.con.execute('''\n            SET MODULE test;\n\n            INSERT Target1 {\n                name := 'Target1_linkinh_2'\n            };\n\n            INSERT ObjectType01 {\n                target := (SELECT Target1\n                           FILTER .name = 'Target1_linkinh_2'\n                           LIMIT 1)\n            };\n\n            INSERT Target0 {\n                name := 'Target0_linkinh_2'\n            };\n\n            INSERT ObjectType23 {\n                target := (SELECT Target0\n                           FILTER .name = 'Target0_linkinh_2'\n                           LIMIT 1)\n            };\n        ''')\n\n        await self.con.query('DECLARE SAVEPOINT t0;')\n\n        with self.assertRaisesRegex(\n                edgedb.InvalidLinkTargetError,\n                r\"invalid target for link 'target' of object type \"\n                r\"'test::ObjectType01': \"\n                r\"'test::Target0' \\(expecting 'test::Target1'\\)\"):\n            # Target0 is not allowed to be targeted by ObjectType01, since\n            # ObjectType01 inherits from ObjectType1 which requires more\n            # specific Target1.\n            await self.con.execute('''\n                INSERT ObjectType01 {\n                    target := (\n                        SELECT\n                            Target0\n                        FILTER\n                            .name = 'Target0_linkinh_2'\n                        LIMIT 1\n                    )\n                };\n            ''')\n\n        schema_f = os.path.join(os.path.dirname(__file__), 'schemas',\n                                'links_1_migrated.esdl')\n\n        with open(schema_f) as f:\n            schema = f.read()\n\n        await self.con.query('ROLLBACK TO SAVEPOINT t0')\n        await self.migrate(schema)\n\n    async def test_edgeql_migration_describe_reject_01(self):\n        await self.migrate('''\n            type Foo;\n        ''')\n\n        await self.start_migration('''\n            type Bar;\n        ''')\n\n        await self.assert_describe_migration({\n            'proposed': {\n                'statements': [{\n                    'text': \"\"\"\n                        ALTER TYPE test::Foo RENAME TO test::Bar;\n                    \"\"\"\n                }]\n            }\n        })\n\n        await self.con.execute('''\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n        ''')\n\n        await self.assert_describe_migration({\n            'proposed': {\n                'statements': [{\n                    'text': \"\"\"\n                        CREATE TYPE test::Bar;\n                    \"\"\"\n                }]\n            }\n        })\n\n        await self.con.execute('''\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n        ''')\n\n        await self.assert_describe_migration({\n            'proposed': {\n                'statements': [{\n                    'text': \"\"\"\n                        DROP TYPE test::Foo;\n                    \"\"\"\n                }]\n            }\n        })\n\n        await self.con.execute('''\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n        ''')\n\n        await self.assert_describe_migration({\n            'proposed': None,\n            'complete': False,\n        })\n\n    async def test_edgeql_migration_describe_reject_02(self):\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'parent': 'm1a2l6lbzimqokzygdzbkyjrhbmjh3iljg7i2m6r2ias2z2de4x4cq',\n            'confirmed': [],\n            'complete': True,\n            'proposed': None,\n        })\n\n        # Reject an empty proposal, which should be an idempotent\n        # operation. So reject it several times.\n        await self.con.execute('''\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n        ''')\n\n    async def test_edgeql_migration_describe_reject_03(self):\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Type0;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': 'CREATE TYPE test::Type0;'\n                }],\n                'prompt': \"did you create object type 'test::Type0'?\",\n            },\n        })\n\n        # Reject a proposal until we run out of options.\n        await self.con.execute('''\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': None,\n        })\n\n    async def test_edgeql_migration_describe_reject_04(self):\n        # Migration involving 2 modules\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Test;\n                };\n\n                module other {\n                    type Test;\n                };\n            };\n        ''')\n        await self.fast_forward_describe_migration()\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Test2;\n                };\n\n                module other {\n                    type Test3;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': 'ALTER TYPE other::Test RENAME TO other::Test3;',\n                }],\n                'prompt': (\n                    \"did you rename object type 'other::Test' to \"\n                    \"'other::Test3'?\"\n                ),\n            },\n        })\n\n        await self.con.execute('''\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': 'ALTER TYPE other::Test RENAME TO test::Test2;',\n                }],\n                'prompt': (\n                    \"did you rename object type 'other::Test' to \"\n                    \"'test::Test2'?\"\n                ),\n            },\n        })\n\n        await self.con.execute('''\n            ALTER TYPE other::Test RENAME TO test::Test2;\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [\n                'ALTER TYPE other::Test RENAME TO test::Test2;'\n            ],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': 'ALTER TYPE test::Test RENAME TO other::Test3;',\n                }],\n                'prompt': (\n                    \"did you rename object type 'test::Test' to \"\n                    \"'other::Test3'?\"\n                ),\n            },\n        })\n\n        await self.con.execute('''\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [\n                'ALTER TYPE other::Test RENAME TO test::Test2;'\n            ],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': 'CREATE TYPE other::Test3;',\n                }],\n                'prompt': (\n                    \"did you create object type 'other::Test3'?\"\n                ),\n            },\n        })\n\n        # Change our mind and use a rejected operation to rename the\n        # type after all. So, we should be done now.\n        await self.con.execute('''\n            ALTER TYPE test::Test RENAME TO other::Test3;\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [\n                'ALTER TYPE other::Test RENAME TO test::Test2;',\n                'ALTER TYPE test::Test RENAME TO other::Test3;',\n            ],\n            'complete': True,\n            'proposed': None,\n        })\n\n    async def test_edgeql_migration_describe_reject_05(self):\n        await self.migrate('''\n            type User {\n                required property username -> str {\n                    constraint exclusive;\n                    constraint regexp(r'asdf');\n                }\n            }\n        ''')\n\n        await self.start_migration('''\n            type User {\n                required property username -> str {\n                    constraint exclusive;\n                    constraint regexp(r'foo');\n                }\n            }\n        ''')\n\n        await self.assert_describe_migration({\n            'proposed': {\n                'prompt': (\n                    \"did you drop constraint 'std::regexp' \"\n                    \"of property 'username'?\"\n                )\n            }\n        })\n\n        await self.con.execute('''\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n        ''')\n\n        # ... nothing to do, we can't get it\n        await self.assert_describe_migration({\n            'complete': False,\n            'proposed': None,\n        })\n\n    async def test_edgeql_migration_describe_module_01(self):\n        # Migration that creates a new module.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module new_module {\n                    type Type0;\n                };\n            };\n        ''')\n\n        # Validate that we create a 'new_module'\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': 'CREATE MODULE new_module IF NOT EXISTS;'\n                }],\n            },\n        })\n\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        # Drop the 'new_module'\n        await self.con.execute('''\n            START MIGRATION TO {\n                module default {};\n            };\n        ''')\n\n        # Validate that we drop a 'new_module'\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': 'DROP TYPE new_module::Type0;'\n                }],\n            },\n        })\n        await self.con.execute('''\n            DROP TYPE new_module::Type0;\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [\n                'DROP TYPE new_module::Type0;'\n            ],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': 'DROP MODULE new_module;'\n                }],\n            },\n        })\n\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        # Make sure that 'new_module' can be created again with no\n        # problems (i.e. it was dropped cleanly).\n        await self.con.execute('''\n            START MIGRATION TO {\n                module new_module {\n                    type Type0;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': 'CREATE MODULE new_module IF NOT EXISTS;'\n                }],\n            },\n        })\n        await self.con.execute('''\n            CREATE MODULE new_module;\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [\n                'CREATE MODULE new_module;',\n            ],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': 'CREATE TYPE new_module::Type0;'\n                }],\n            },\n        })\n        await self.con.execute('''\n            CREATE TYPE new_module::Type0;\n            COMMIT MIGRATION;\n        ''')\n\n        await self.assert_query_result(\n            r\"\"\"\n                INSERT new_module::Type0;\n            \"\"\",\n            [{\n                'id': uuid.UUID,\n            }],\n        )\n\n    async def test_edgeql_migration_describe_type_01(self):\n        # Migration that renames a type.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Type1;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'parent': 'm1a2l6lbzimqokzygdzbkyjrhbmjh3iljg7i2m6r2ias2z2de4x4cq',\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'CREATE TYPE test::Type1;'\n                    )\n                }],\n                'prompt': \"did you create object type 'test::Type1'?\",\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        res = await self.con.query(r'''INSERT test::Type1;''')\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Type01;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'parent': 'm1jywblj6c7z25ouifcicpxniu37jdpyunf62q4th7isdafcqu67gq',\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER TYPE test::Type1 RENAME TO test::Type01;'\n                    )\n                }],\n                'prompt': (\n                    \"did you rename object type 'test::Type1' to \"\n                    \"'test::Type01'?\"\n                ),\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            SELECT test::Type01;\n        ''', [{'id': res[0].id}])\n\n    async def test_edgeql_migration_describe_type_02(self):\n        # Migration that creates a type.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Type02;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'parent': 'm1a2l6lbzimqokzygdzbkyjrhbmjh3iljg7i2m6r2ias2z2de4x4cq',\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'CREATE TYPE test::Type02;'\n                    )\n                }],\n                'prompt': \"did you create object type 'test::Type02'?\",\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.con.query(r'''INSERT test::Type02;''')\n\n        # Migration that drops a type.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'parent': 'm1fcvk56n44i62qwjnw5nqgafnbpulfhhaeb6kxqhh4c6lc4elwysa',\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'DROP TYPE test::Type02;'\n                    )\n                }],\n                'prompt': (\n                    \"did you drop object type 'test::Type02'?\"\n                ),\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            WITH MODULE schema\n            SELECT ObjectType\n            FILTER .name = 'test::Type02';\n        ''', [])\n\n        # Make sure that type dropped cleanly by re-creating and\n        # using the type again.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Type02;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'parent': 'm1yee6qj63nps27cjnrcudwiupusdqkzrwistpvfbqf2fstcmwauwa',\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'CREATE TYPE test::Type02;'\n                    )\n                }],\n                'prompt': \"did you create object type 'test::Type02'?\",\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            INSERT test::Type02;\n        ''', [{'id': uuid.UUID}])\n\n    async def test_edgeql_migration_describe_type_03(self):\n        await self.migrate('''\n            type Type0;\n        ''')\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Type1;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': \"ALTER TYPE test::Type0 RENAME TO test::Type1;\"\n                }],\n                'prompt': (\n                    \"did you rename object type 'test::Type0' to \"\n                    \"'test::Type1'?\"\n                ),\n            },\n        })\n\n        # Instead of the suggestion do a couple of different, but\n        # equivalent commands.\n        await self.con.execute('''\n            ALTER TYPE test::Type0 RENAME TO test::TypeXX;\n            ALTER TYPE test::TypeXX RENAME TO test::Type1;\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [\n                'ALTER TYPE test::Type0 RENAME TO test::TypeXX;',\n                'ALTER TYPE test::TypeXX RENAME TO test::Type1;',\n            ],\n            'complete': True,\n            'proposed': None,\n        })\n\n    async def test_edgeql_migration_describe_type_04(self):\n        self.maxDiff = None\n        await self.migrate('''\n            type Test;\n        ''')\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Test2;\n                    type Test3;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'parent': 'm1xh653zionj2aehqbh7x6km5lo3b2mjaftxdkvqoh3wluc3iv6k2a',\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': 'ALTER TYPE test::Test RENAME TO test::Test2;',\n                }],\n                'prompt': (\n                    \"did you rename object type 'test::Test' to 'test::Test2'?\"\n                ),\n            },\n        })\n\n        await self.con.execute('''\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n        ''')\n\n        await self.assert_describe_migration({\n            'parent': 'm1xh653zionj2aehqbh7x6km5lo3b2mjaftxdkvqoh3wluc3iv6k2a',\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': 'ALTER TYPE test::Test RENAME TO test::Test3;',\n                }],\n                'prompt': (\n                    \"did you rename object type 'test::Test' to 'test::Test3'?\"\n                ),\n            },\n        })\n\n        await self.con.execute('''\n            ALTER TYPE test::Test RENAME TO test::Test3;\n        ''')\n\n        await self.assert_describe_migration({\n            'parent': 'm1xh653zionj2aehqbh7x6km5lo3b2mjaftxdkvqoh3wluc3iv6k2a',\n            'confirmed': ['ALTER TYPE test::Test RENAME TO test::Test3;'],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': 'CREATE TYPE test::Test2;',\n                }],\n                'prompt': (\n                    \"did you create object type 'test::Test2'?\"\n                ),\n            },\n        })\n\n    async def test_edgeql_migration_describe_property_01(self):\n        # Migration that renames a property.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Type01 {\n                        property field1 -> str;\n                    };\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'CREATE TYPE test::Type01 {\\n'\n                        '    CREATE PROPERTY field1'\n                        ': std::str;\\n'\n                        '};'\n                    )\n                }],\n                'prompt': \"did you create object type 'test::Type01'?\",\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.con.execute('''\n            INSERT test::Type01 {\n                field1 := 'prop_test'\n            };\n        ''')\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Type01 {\n                        property field01 -> str;\n                    };\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER TYPE test::Type01 {\\n'\n                        '    ALTER PROPERTY field1 {\\n'\n                        '        RENAME TO field01;\\n'\n                        '    };\\n'\n                        '};'\n                    )\n                }],\n                'prompt': (\n                    \"did you rename property 'field1' of object type\"\n                    \" 'test::Type01' to 'field01'?\"\n                ),\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            SELECT test::Type01 {\n                field01\n            };\n        ''', [{'field01': 'prop_test'}])\n\n    async def test_edgeql_migration_describe_property_02(self):\n        # Migration that creates a type with property.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Type02 {\n                        property field02 -> str;\n                    };\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'CREATE TYPE test::Type02 {\\n'\n                        '    CREATE PROPERTY field02'\n                        ': std::str;\\n'\n                        '};'\n                    )\n                }],\n                'prompt': \"did you create object type 'test::Type02'?\",\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        res = await self.con.query('''\n            INSERT test::Type02 {\n                field02 := 'prop_test'\n            };\n        ''')\n\n        # Migration that drops a property.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Type02;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER TYPE test::Type02 {\\n'\n                        '    DROP PROPERTY field02;\\n'\n                        '};'\n                    )\n                }],\n                'prompt': (\n                    \"did you drop property 'field02'\"\n                    \" of object type 'test::Type02'?\"\n                ),\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            SELECT test::Type02 {\n                id\n            };\n        ''', [{\n            'id': res[0].id\n        }])\n\n        # Make sure that property dropped cleanly by re-creating and\n        # using the property again.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Type02 {\n                        property field02 -> str;\n                    };\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER TYPE test::Type02 {\\n'\n                        '    CREATE PROPERTY field02'\n                        ': std::str;\\n'\n                        '};'\n                    )\n                }],\n                'prompt': (\n                    \"did you create property 'field02'\"\n                    \" of object type 'test::Type02'?\"\n                ),\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            SELECT test::Type02 {\n                id,\n                field02,\n            };\n        ''', [{\n            'id': res[0].id,\n            'field02': None,\n        }])\n\n    async def test_edgeql_migration_describe_link_01(self):\n        # Migration that renames a link.\n        await self.con.execute(r'''\n            START MIGRATION TO {\n                module test {\n                    type Foo;\n                    type Type01 {\n                        link foo1 -> Foo;\n                    };\n                };\n            };\n\n            # just initialize Foo, since we're interested in the other type\n            CREATE TYPE test::Foo;\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': ['CREATE TYPE test::Foo;'],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'CREATE TYPE test::Type01 {\\n'\n                        '    CREATE LINK foo1'\n                        ': test::Foo;\\n'\n                        '};'\n                    )\n                }],\n                'prompt': \"did you create object type 'test::Type01'?\",\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        res = await self.con.query('''\n            WITH MODULE test\n            SELECT (\n                INSERT Type01 {\n                    foo1 := (INSERT Foo)\n                }\n            ) {\n                foo1\n            };\n        ''')\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Foo;\n                    type Type01 {\n                        link foo01 -> Foo;\n                    };\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER TYPE test::Type01 {\\n'\n                        '    ALTER LINK foo1 {\\n'\n                        '        RENAME TO foo01;\\n'\n                        '    };\\n'\n                        '};'\n                    )\n                }],\n                'prompt': (\n                    \"did you rename link 'foo1' of object type\"\n                    \" 'test::Type01' to 'foo01'?\"\n                ),\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            SELECT test::Type01 {\n                foo01: {\n                    id\n                }\n            };\n        ''', [{'foo01': {'id': res[0].foo1.id}}])\n\n    async def test_edgeql_migration_describe_link_02(self):\n        # Migration that creates a type with link.\n        await self.con.execute(r'''\n            START MIGRATION TO {\n                module test {\n                    type Foo;\n                    type Type02 {\n                        link foo02 -> Foo;\n                    };\n                };\n            };\n\n            # just initialize Foo, since we're interested in the other type\n            CREATE TYPE test::Foo;\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': ['CREATE TYPE test::Foo;'],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'CREATE TYPE test::Type02 {\\n'\n                        '    CREATE LINK foo02'\n                        ': test::Foo;\\n'\n                        '};'\n                    )\n                }],\n                'prompt': \"did you create object type 'test::Type02'?\",\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        res = await self.con.query('''\n            WITH MODULE test\n            SELECT (\n                INSERT Type02 {\n                    foo02 := (INSERT Foo)\n                }\n            ) {\n                foo02\n            }\n        ''')\n\n        # Migration that drops a link.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Foo;\n                    type Type02;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER TYPE test::Type02 {\\n'\n                        '    DROP LINK foo02;\\n'\n                        '};'\n                    )\n                }],\n                'prompt': (\n                    \"did you drop link 'foo02' of object type 'test::Type02'?\"\n                ),\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            SELECT test::Type02 {\n                id\n            };\n        ''', [{\n            'id': res[0].id\n        }])\n        await self.assert_query_result('''\n            SELECT test::Foo {\n                id\n            };\n        ''', [{\n            'id': res[0].foo02.id\n        }])\n\n        # Make sure that link dropped cleanly by re-creating and\n        # using the link again.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Foo;\n                    type Type02 {\n                        link foo02 -> Foo;\n                    };\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER TYPE test::Type02 {\\n'\n                        '    CREATE LINK foo02'\n                        ': test::Foo;\\n'\n                        '};'\n                    )\n                }],\n                'prompt': (\n                    \"did you create link 'foo02'\"\n                    \" of object type 'test::Type02'?\"\n                ),\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            SELECT test::Type02 {\n                id,\n                foo02: {\n                    id\n                },\n            };\n        ''', [{\n            'id': res[0].id,\n            'foo02': None,\n        }])\n\n    async def test_edgeql_migration_describe_link_03(self):\n        # Migration that renames a link.\n        await self.con.execute(r'''\n            START MIGRATION TO {\n                module test {\n                    abstract link foo3;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'CREATE ABSTRACT LINK test::foo3;'\n                    )\n                }],\n                'prompt': \"did you create abstract link 'test::foo3'?\",\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    abstract link foo03;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER ABSTRACT LINK test::foo3 '\n                        'RENAME TO test::foo03;'\n                    )\n                }],\n                'prompt': (\n                    \"did you rename abstract link 'test::foo3' to \"\n                    \"'test::foo03'?\"\n                ),\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'DROP ABSTRACT LINK test::foo03;'\n                    )\n                }],\n                'prompt': (\n                    \"did you drop abstract link 'test::foo03'?\"\n                ),\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n    async def test_edgeql_migration_describe_index_01(self):\n        # Migration that creates index.\n        await self.con.execute(r'''\n            START MIGRATION TO {\n                module test {\n                    type Foo {\n                        property a -> int64;\n                    };\n                };\n            };\n        ''')\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Foo {\n                        property a -> int64;\n                        index on (.a)\n                    };\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'prompt': (\n                    \"did you create index on (.a) \"\n                    \"of object type 'test::Foo'?\"\n                )\n            }\n        })\n\n    async def test_edgeql_migration_describe_index_02(self):\n        # Migration that drops index expression.\n        await self.con.execute(r'''\n            START MIGRATION TO {\n                module test {\n                    type Foo {\n                        property a -> int64;\n                        index on (.a)\n                    };\n                };\n            };\n        ''')\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Foo {\n                        property a -> int64;\n                    };\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'prompt': (\n                    \"did you drop index on (.a) \"\n                    \"of object type 'test::Foo'?\"\n                )\n            }\n        })\n\n    async def test_edgeql_migration_describe_index_03(self):\n        # Migration that creates index on link property\n        await self.con.execute(r'''\n            START MIGRATION TO {\n                module test {\n                    type Foo {\n                        link bar -> Bar {\n                            baz -> int64;\n                        }\n                    };\n                    type Bar;\n                };\n            };\n        ''')\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Foo {\n                        link bar -> Bar {\n                            baz -> int64;\n                            index on (@baz);\n                        }\n                    };\n                    type Bar;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'prompt': (\n                    \"did you create index on (@baz) \"\n                    \"of link 'bar'?\"\n                )\n            }\n        })\n\n    async def test_edgeql_migration_describe_index_04(self):\n        # Migration that drops index on link property\n        await self.con.execute(r'''\n            START MIGRATION TO {\n                module test {\n                    type Foo {\n                        link bar -> Bar {\n                            baz -> int64;\n                            index on (@baz);\n                        }\n                    };\n                    type Bar;\n                };\n            };\n        ''')\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Foo {\n                        link bar -> Bar {\n                            baz -> int64;\n                        }\n                    };\n                    type Bar;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'prompt': (\n                    \"did you drop index on (@baz) \"\n                    \"of link 'bar'?\"\n                )\n            }\n        })\n\n    async def test_edgeql_migration_describe_index_05(self):\n        # Migration that creates index.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module default {\n                    type Foo {\n                        property x -> int64;\n                        index on (.x);\n                    };\n                };\n            };\n        ''')\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module default {\n                    type Foo {\n                        property y -> int64;\n                        index on (.y);\n                    };\n                };\n            };\n        ''')\n\n        await self.interact([\n            (\"did you drop index on (.x) of object type 'default::Foo'?\", \"n\"),\n            \"did you rename property 'x' of object type 'default::Foo' to 'y'?\",\n        ])\n\n    async def test_edgeql_migration_describe_scalar_01(self):\n        # Migration that renames a type.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    scalar type ScalarType1 extending int64;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'CREATE SCALAR TYPE test::ScalarType1'\n                        ' EXTENDING std::int64;'\n                    )\n                }],\n                'prompt': \"did you create scalar type 'test::ScalarType1'?\",\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            SELECT <test::ScalarType1>'1' + 2;\n        ''', [3])\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    scalar type ScalarType01 extending int64;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER SCALAR TYPE test::ScalarType1'\n                        ' RENAME TO test::ScalarType01;'\n                    )\n                }],\n                'prompt': (\n                    \"did you rename scalar type 'test::ScalarType1' to \"\n                    \"'test::ScalarType01'?\"\n                ),\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            SELECT <test::ScalarType01>'2' + 1;\n        ''', [3])\n\n    async def test_edgeql_migration_describe_scalar_02(self):\n        # Migration that creates a type.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    scalar type ScalarType02 extending str;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'CREATE SCALAR TYPE test::ScalarType02'\n                        ' EXTENDING std::str;'\n                    )\n                }],\n                'prompt': \"did you create scalar type 'test::ScalarType02'?\",\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            SELECT <test::ScalarType02>1 ++ '2';\n        ''', ['12'])\n\n        # Migration that drops a type.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'DROP SCALAR TYPE test::ScalarType02;'\n                    )\n                }],\n                'prompt': (\n                    \"did you drop scalar type 'test::ScalarType02'?\"\n                ),\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            WITH MODULE schema\n            SELECT ScalarType\n            FILTER .name = 'test::ScalarType02';\n        ''', [])\n\n        # Make sure that type dropped cleanly by re-creating and\n        # using the type again.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    scalar type ScalarType02 extending str;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'CREATE SCALAR TYPE test::ScalarType02'\n                        ' EXTENDING std::str;'\n                    )\n                }],\n                'prompt': \"did you create scalar type 'test::ScalarType02'?\",\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            SELECT <test::ScalarType02>2 ++ '1';\n        ''', ['21'])\n\n    async def test_edgeql_migration_describe_enum_01(self):\n        # Migration that renames an enum.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    scalar type EnumType1 extending enum<foo, bar>;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        \"CREATE SCALAR TYPE test::EnumType1\"\n                        \" EXTENDING enum<foo, bar>;\"\n                    )\n                }],\n                'prompt': \"did you create scalar type 'test::EnumType1'?\",\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            SELECT <test::EnumType1>'bar';\n        ''', ['bar'])\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    scalar type EnumType01 extending enum<foo, bar>;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER SCALAR TYPE test::EnumType1'\n                        ' RENAME TO test::EnumType01;'\n                    )\n                }],\n                'prompt': (\n                    \"did you rename scalar type 'test::EnumType1' to \"\n                    \"'test::EnumType01'?\"\n                ),\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            SELECT <test::EnumType01>'foo';\n        ''', ['foo'])\n\n    async def test_edgeql_migration_describe_enum_02(self):\n        # Migration that creates an enum.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    scalar type EnumType02 extending enum<foo, bar>;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        \"CREATE SCALAR TYPE test::EnumType02\"\n                        \" EXTENDING enum<foo, bar>;\"\n                    )\n                }],\n                'prompt': \"did you create scalar type 'test::EnumType02'?\",\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            SELECT <test::EnumType02>'bar';\n        ''', ['bar'])\n\n        # Migration that drops an enum.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'DROP SCALAR TYPE test::EnumType02;'\n                    )\n                }],\n                'prompt': (\n                    \"did you drop scalar type 'test::EnumType02'?\"\n                ),\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            WITH MODULE schema\n            SELECT ScalarType\n            FILTER .name = 'test::EnumType02';\n        ''', [])\n\n        # Make sure that enum dropped cleanly by re-creating and\n        # using the enum again.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    scalar type EnumType02 extending enum<foo, bar>;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        \"CREATE SCALAR TYPE test::EnumType02\"\n                        \" EXTENDING enum<foo, bar>;\"\n                    )\n                }],\n                'prompt': \"did you create scalar type 'test::EnumType02'?\",\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result('''\n            SELECT <test::EnumType02>'foo';\n        ''', ['foo'])\n\n    async def test_edgeql_migration_describe_annotation_01(self):\n        # Migration that renames an annotation.\n        await self.migrate('''\n            abstract annotation my_anno1;\n        ''')\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    abstract annotation renamed_anno1;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER ABSTRACT ANNOTATION test::my_anno1 '\n                        'RENAME TO test::renamed_anno1;'\n                    )\n                }],\n            },\n        })\n\n    async def test_edgeql_migration_describe_annotation_02(self):\n        # Migration that creates an annotation.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    abstract annotation my_anno2;\n\n                    type AnnoType2 {\n                        annotation my_anno2 := 'test_my_anno2';\n                    }\n                };\n            };\n        ''')\n\n        await self.con.execute('''\n            CREATE TYPE test::AnnoType2;\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [\n                'CREATE TYPE test::AnnoType2;'\n            ],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'CREATE ABSTRACT ANNOTATION test::my_anno2;'\n                    )\n                }],\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        # Migration that drops an annotation.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type AnnoType2;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER TYPE test::AnnoType2 {\\n'\n                        '    DROP ANNOTATION test::my_anno2;\\n'\n                        '};'\n                    )\n                }],\n            },\n        })\n        # Auto-complete migration\n        await self.con.execute('''\n            ALTER TYPE test::AnnoType2 {\n                DROP ANNOTATION test::my_anno2;\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'DROP ABSTRACT ANNOTATION test::my_anno2;'\n                    )\n                }],\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        # Make sure that annotation dropped cleanly by re-creating and\n        # using the annotation.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    abstract annotation my_anno2;\n\n                    type AnnoType2 {\n                        annotation my_anno2 := 'retest_my_anno2';\n                    }\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'CREATE ABSTRACT ANNOTATION test::my_anno2;'\n                    )\n                }],\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    annotations: {\n                        name,\n                        @value,\n                    },\n                } FILTER .name = 'test::AnnoType2';\n            \"\"\",\n            [{\n                'name': 'test::AnnoType2',\n                'annotations': [{\n                    'name': 'test::my_anno2',\n                    '@value': 'retest_my_anno2',\n                }]\n            }],\n        )\n\n    async def test_edgeql_migration_describe_constraint_01(self):\n        # Migration that renames a constraint.\n        await self.migrate('''\n            abstract constraint my_oneof(one_of: array<anytype>) {\n                using (contains(one_of, __subject__));\n            };\n\n            type Foo {\n                property note -> str {\n                    constraint my_oneof([\"foo\", \"bar\"]);\n                }\n            }\n        ''')\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    abstract constraint my_one_of(one_of: array<anytype>) {\n                        using (contains(one_of, __subject__));\n                    };\n\n                    type Foo {\n                        property note -> str {\n                            constraint my_one_of([\"foo\", \"bar\"]);\n                        }\n                    }\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER ABSTRACT CONSTRAINT test::my_oneof '\n                        'RENAME TO test::my_one_of;'\n                    )\n                }],\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n    async def test_edgeql_migration_describe_constraint_02(self):\n        # Migration that renames a link constraint.\n        # Honestly I'm not sure if link constraints can really be\n        # anything other than exclusive?\n        await self.migrate('''\n            abstract constraint my_exclusive() extending std::exclusive;\n\n            type Foo;\n            type Bar {\n                link foo -> Foo {\n                    constraint my_exclusive;\n                }\n            }\n        ''')\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    abstract constraint myexclusive() extending std::exclusive;\n\n                    type Foo;\n                    type Bar {\n                        link foo -> Foo {\n                            constraint myexclusive;\n                        }\n                    }\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER ABSTRACT CONSTRAINT test::my_exclusive '\n                        'RENAME TO test::myexclusive;'\n                    )\n                }],\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n    async def test_edgeql_migration_describe_constraint_03(self):\n        # Migration that renames a object constraint.\n        await self.migrate('''\n            abstract constraint my_oneof(one_of: array<anytype>) {\n                using (contains(one_of, __subject__));\n            };\n\n            type Foo {\n                property a -> str;\n                property b -> str;\n                constraint my_oneof([\"foo\", \"bar\"])\n                    ON (__subject__.a++__subject__.b);\n            }\n        ''')\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    abstract constraint my_one_of(one_of: array<anytype>) {\n                        using (contains(one_of, __subject__));\n                    };\n\n                    type Foo {\n                        property a -> str;\n                        property b -> str;\n                        constraint my_one_of([\"foo\", \"bar\"])\n                            ON (__subject__.a++__subject__.b);\n                    }\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER ABSTRACT CONSTRAINT test::my_oneof '\n                        'RENAME TO test::my_one_of;'\n                    )\n                }],\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n    async def test_edgeql_migration_describe_constraint_04(self):\n        # Migration that creates a constraint.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    abstract constraint my_one_of(one_of: array<anytype>) {\n                        using (contains(one_of, __subject__));\n                    };\n\n                    scalar type my_str extending str {\n                        constraint my_one_of(['my', 'str']);\n                    };\n                };\n            };\n        ''')\n\n        await self.con.execute('''\n            CREATE SCALAR TYPE test::my_str EXTENDING std::str;\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [\n                'CREATE SCALAR TYPE test::my_str EXTENDING std::str;'\n            ],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'CREATE ABSTRACT CONSTRAINT test::my_one_of('\n                        'one_of: array<anytype>) {\\n'\n                        '    USING (std::contains(one_of, __subject__));\\n'\n                        '};'\n                    ),\n                }],\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n        await self.con.query('DECLARE SAVEPOINT migration_01')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT <test::my_str>'my';\n            \"\"\",\n            ['my'],\n        )\n        with self.assertRaisesRegex(\n                edgedb.ConstraintViolationError,\n                r\"invalid my_str\"):\n            await self.con.execute(r\"\"\"\n                SELECT <test::my_str>'nope';\n            \"\"\")\n        await self.con.query('ROLLBACK TO SAVEPOINT migration_01')\n\n        # Migration that drops a constraint.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    scalar type my_str extending str;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        \"ALTER SCALAR TYPE test::my_str {\\n\"\n                        \"    DROP CONSTRAINT test::my_one_of(['my', 'str']);\\n\"\n                        \"};\"\n                    ),\n                }],\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT <test::my_str>'my';\n            \"\"\",\n            ['my'],\n        )\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT <test::my_str>'nope';\n            \"\"\",\n            ['nope'],\n        )\n\n        # Test that dropping constraint was clean with a migration\n        # that re-creates a constraint.\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    abstract constraint my_one_of(one_of: array<anytype>) {\n                        using (contains(one_of, __subject__));\n                    };\n\n                    scalar type my_str extending str {\n                        constraint my_one_of(['my2', 'str2']);\n                    };\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'CREATE ABSTRACT CONSTRAINT '\n                        'test::my_one_of(one_of: array<anytype>) {\\n'\n                        '    USING (std::contains(one_of, __subject__));\\n'\n                        '};'\n                    ),\n                }],\n            },\n        })\n        # Auto-complete migration\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT <test::my_str>'my2';\n            \"\"\",\n            ['my2'],\n        )\n        with self.assertRaisesRegex(\n                edgedb.ConstraintViolationError,\n                r\"invalid my_str\"):\n            await self.con.execute(r\"\"\"\n                SELECT <test::my_str>'my';\n            \"\"\")\n\n    async def test_edgeql_migration_describe_abs_ptr_01(self):\n        await self.migrate('''\n            abstract link abs_link;\n        ''')\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    abstract link new_abs_link;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER ABSTRACT LINK test::abs_link '\n                        'RENAME TO test::new_abs_link;'\n                    )\n                }],\n            },\n        })\n\n    async def test_edgeql_migration_abs_ptr_01(self):\n        await self.migrate(r\"\"\"\n            type T { multi link following := T; }\n        \"\"\")\n        await self.migrate(r\"\"\"\n            abstract link abs { property foo: str };\n            type T { multi link following extending abs -> T; }\n        \"\"\")\n\n    async def test_edgeql_migration_describe_function_01(self):\n        await self.migrate('''\n            function foo(x: str) -> str using (SELECT <str>random());\n        ''')\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    function bar(x: str) -> str using (SELECT <str>random());\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER FUNCTION test::foo(x: std::str) '\n                        '{RENAME TO test::bar;};'\n                    )\n                }],\n            },\n        })\n\n    async def test_edgeql_migration_function_01(self):\n        await self.migrate('''\n            type Note {\n                required property name -> str;\n            }\n\n            function hello_note(x: Note) -> str {\n                USING (SELECT x.name)\n            }\n        ''')\n\n    async def test_edgeql_migration_function_02(self):\n        await self.migrate('''\n            type Foo;\n\n            function foo(x: Foo) -> int64 {\n                USING (SELECT 0)\n            }\n        ''')\n\n        await self.migrate('''\n            type Bar;\n\n            function foo(x: Bar) -> int64 {\n                USING (SELECT 0)\n            }\n        ''')\n\n        await self.con.execute('''\n            DROP FUNCTION test::foo(x: test::Bar);\n        ''')\n\n    async def test_edgeql_migration_function_03(self):\n        await self.migrate('''\n            type Foo;\n\n            function foo(x: Foo) -> int64 {\n                USING (SELECT 0)\n            }\n        ''')\n\n        await self.migrate('''\n            type Bar;\n\n            function foo2(x: Bar) -> int64 {\n                USING (SELECT 0)\n            }\n        ''')\n\n        await self.con.execute('''\n            DROP FUNCTION test::foo2(x: test::Bar);\n        ''')\n\n    async def test_edgeql_migration_function_04(self):\n        await self.migrate('''\n            function foo() -> str USING ('foo');\n        ''')\n\n        await self.start_migration('''\n            function foo() -> str USING ('bar');\n        ''')\n\n        await self.interact([\n            \"did you alter function 'test::foo'?\"\n        ])\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT test::foo()\n            \"\"\",\n            [\"bar\"],\n        )\n\n    async def test_edgeql_migration_function_05(self):\n        await self.migrate(\"\"\"\n            type Person {\n                required property name -> str {\n                    constraint exclusive;\n                }\n                multi link places_visited -> Place;\n            }\n\n            type Place {\n                required property name -> str {\n                    constraint exclusive;\n                }\n            }\n\n            function visited(person: str, city: str) -> bool\n                using (\n                    WITH person := (SELECT Person FILTER .name = person),\n                    SELECT city IN person.places_visited.name\n                );\n        \"\"\")\n\n    async def test_edgeql_migration_function_06(self):\n        await self.migrate(\"\"\"\n            type Foo;\n            type Bar;\n            type Baz {\n                optional link baz -> (Foo | Bar)\n            };\n            function getBaz(bar: Baz) -> optional (Foo | Bar)\n                using(bar.baz)\n        \"\"\")\n        await self.con.execute('insert test::Baz {}')\n        await self.assert_query_result(\n            \"select test::getBaz((select test::Baz limit 1))\",\n            [])\n        await self.con.execute(\"drop function test::getBaz(baz: test::Baz)\")\n\n    async def test_edgeql_migration_constraint_01(self):\n        await self.migrate('''\n            abstract constraint not_bad {\n                using (__subject__ != \"bad\" and __subject__ != \"terrible\")\n            }\n\n            type Foo {\n                property foo -> str {\n                    constraint not_bad;\n                }\n            }\n            type Bar extending Foo;\n        ''')\n\n        await self.start_migration('''\n            abstract constraint not_bad {\n                using (__subject__ != \"bad\" and __subject__ != \"awful\")\n            }\n\n            type Foo {\n                property foo -> str {\n                    constraint not_bad;\n                }\n            }\n            type Bar extending Foo;\n        ''')\n\n        await self.interact([\n            \"did you alter abstract constraint 'test::not_bad'?\"\n        ])\n        await self.fast_forward_describe_migration()\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            \"invalid foo\",\n        ):\n            await self.con.execute(r\"\"\"\n                INSERT test::Foo { foo := \"awful\" };\n            \"\"\")\n\n    async def test_edgeql_migration_describe_type_rename_01(self):\n        await self.migrate('''\n            type Foo;\n            type Baz {\n                link l -> Foo;\n            };\n        ''')\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Bar;\n                    type Baz {\n                        link l -> Bar;\n                    };\n                }\n            };\n            POPULATE MIGRATION;\n        ''')\n\n        await self.assert_describe_migration({\n            'complete': True,\n            'confirmed': [\n                'ALTER TYPE test::Foo RENAME TO test::Bar;'\n            ],\n        })\n\n        await self.fast_forward_describe_migration()\n\n    async def test_edgeql_migration_describe_populate_describe(self):\n        await self.start_migration('''\n            type Foo;\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'CREATE TYPE test::Foo;'\n                    )\n                }],\n            },\n        })\n\n        await self.con.execute('POPULATE MIGRATION;')\n\n        await self.assert_describe_migration({\n            'confirmed': ['CREATE TYPE test::Foo;'],\n            'complete': True,\n            'proposed': None,\n        })\n\n    async def test_edgeql_migration_computed_01(self):\n        await self.migrate(r'''\n            type Foo {\n                property val -> str;\n                property comp := count((\n                    # Use an alias in WITH block in a computable\n                    WITH x := .val\n                    # Use an alias in SELECT in a computable\n                    SELECT y := Bar FILTER x = y.val\n                ))\n            }\n\n            type Bar {\n                property val -> str;\n            }\n        ''')\n\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n\n            INSERT Foo {val := 'c'};\n            INSERT Foo {val := 'd'};\n\n            INSERT Bar {val := 'a'};\n            INSERT Bar {val := 'b'};\n            INSERT Bar {val := 'c'};\n            INSERT Bar {val := 'c'};\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Foo {\n                    val,\n                    comp,\n                } ORDER BY .val;\n            \"\"\",\n            [{\n                'val': 'c',\n                'comp': 2,\n            }, {\n                'val': 'd',\n                'comp': 0,\n            }],\n        )\n\n    async def test_edgeql_migration_computed_02(self):\n        await self.migrate(r'''\n            type Foo { property foo := '1' };\n            type Bar extending Foo;\n        ''')\n\n        await self.migrate(r'''\n            type Foo { property foo := 1 };\n            type Bar extending Foo;\n        ''')\n\n    async def test_edgeql_migration_computed_03(self):\n        await self.migrate(r'''\n            type User {\n                property name -> str;\n                multi link tweets := Tweet;\n            }\n            type Tweet {\n                property text -> str;\n                link author -> User;\n            }\n        ''', module='default')\n\n        await self.con.execute(\"\"\"\n            INSERT Tweet {\n                text := 'Hello',\n                author := (\n                    INSERT User {name := 'Alice'}\n                )\n            };\n            INSERT Tweet {\n                text := 'Hi',\n                author := (\n                    INSERT User {name := 'Billie'}\n                )\n            };\n        \"\"\")\n\n        # Validate our structures\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Tweet {\n                    text,\n                    author: {\n                        name\n                    },\n                } ORDER BY .text;\n            \"\"\",\n            [{\n                'text': 'Hello',\n                'author': {\n                    'name': 'Alice'\n                },\n            }, {\n                'text': 'Hi',\n                'author': {\n                    'name': 'Billie'\n                },\n            }],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User {\n                    name,\n                    tweets: {\n                        text\n                    } ORDER BY .text,\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'Alice',\n                'tweets': [{\n                    'text': 'Hello'\n                }, {\n                    'text': 'Hi'\n                }],\n            }, {\n                'name': 'Billie',\n                'tweets': [{\n                    'text': 'Hello'\n                }, {\n                    'text': 'Hi'\n                }],\n            }],\n        )\n\n        await self.migrate(r'''\n            type User {\n                property name -> str;\n                multi link tweets := User.<author[IS Tweet];\n            }\n            type Tweet {\n                property text -> str;\n                link author -> User;\n            }\n        ''', module='default')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User {\n                    name,\n                    tweets: {\n                        text\n                    } ORDER BY .text,\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'Alice',\n                'tweets': [{\n                    'text': 'Hello'\n                }],\n            }, {\n                'name': 'Billie',\n                'tweets': [{\n                    'text': 'Hi'\n                }],\n            }],\n        )\n\n        await self.migrate(r'''\n            type User {\n                property name -> str;\n                multi link tweets := .<author[IS Tweet];\n            }\n            type Tweet {\n                property text -> str;\n                link author -> User;\n            }\n        ''', module='default')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User {\n                    name,\n                    tweets: {\n                        text\n                    } ORDER BY .text,\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'Alice',\n                'tweets': [{\n                    'text': 'Hello'\n                }],\n            }, {\n                'name': 'Billie',\n                'tweets': [{\n                    'text': 'Hi'\n                }],\n            }],\n        )\n\n        await self.migrate(r'''\n            type User {\n                property name -> str;\n                multi link tweets := (\n                    SELECT Tweet FILTER .author = User\n                );\n            }\n            type Tweet {\n                property text -> str;\n                link author -> User;\n            }\n        ''', module='default')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User {\n                    name,\n                    tweets: {\n                        text\n                    } ORDER BY .text,\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'Alice',\n                'tweets': [{\n                    'text': 'Hello'\n                }],\n            }, {\n                'name': 'Billie',\n                'tweets': [{\n                    'text': 'Hi'\n                }],\n            }],\n        )\n\n        await self.migrate(r'''\n            type User {\n                property name -> str;\n                multi link tweets := (\n                    WITH U := User\n                    SELECT Tweet FILTER .author IN U\n                );\n            }\n            type Tweet {\n                property text -> str;\n                link author -> User;\n            }\n        ''', module='default')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User {\n                    name,\n                    tweets: {\n                        text\n                    } ORDER BY .text,\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'Alice',\n                'tweets': [{\n                    'text': 'Hello'\n                }],\n            }, {\n                'name': 'Billie',\n                'tweets': [{\n                    'text': 'Hi'\n                }],\n            }],\n        )\n\n        await self.migrate(r'''\n            type User {\n                property name -> str;\n                multi link tweets := (\n                    WITH U := DETACHED User\n                    SELECT Tweet FILTER .author IN U\n                );\n            }\n            type Tweet {\n                property text -> str;\n                link author -> User;\n            }\n        ''', module='default')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User {\n                    name,\n                    tweets: {\n                        text\n                    } ORDER BY .text,\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'Alice',\n                'tweets': [{\n                    'text': 'Hello'\n                }, {\n                    'text': 'Hi'\n                }],\n            }, {\n                'name': 'Billie',\n                'tweets': [{\n                    'text': 'Hello'\n                }, {\n                    'text': 'Hi'\n                }],\n            }],\n        )\n\n        await self.migrate(r'''\n            type User {\n                property name -> str;\n                multi link tweets := (\n                    WITH U := User\n                    SELECT U.<author[IS Tweet]\n                );\n            }\n            type Tweet {\n                property text -> str;\n                link author -> User;\n            }\n        ''', module='default')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User {\n                    name,\n                    tweets: {\n                        text\n                    } ORDER BY .text,\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'Alice',\n                'tweets': [{\n                    'text': 'Hello'\n                }],\n            }, {\n                'name': 'Billie',\n                'tweets': [{\n                    'text': 'Hi'\n                }],\n            }],\n        )\n\n        await self.migrate(r'''\n            type User {\n                property name -> str;\n                multi link tweets := (\n                    WITH User := DETACHED User\n                    SELECT User.<author[IS Tweet]\n                );\n            }\n            type Tweet {\n                property text -> str;\n                link author -> User;\n            }\n        ''', module='default')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User {\n                    name,\n                    tweets: {\n                        text\n                    } ORDER BY .text,\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'Alice',\n                'tweets': [{\n                    'text': 'Hello'\n                }, {\n                    'text': 'Hi'\n                }],\n            }, {\n                'name': 'Billie',\n                'tweets': [{\n                    'text': 'Hello'\n                }, {\n                    'text': 'Hi'\n                }],\n            }],\n        )\n\n    @tb.ignore_warnings('more than one.* in a FILTER clause')\n    async def test_edgeql_migration_computed_04(self):\n        await self.migrate(r'''\n            type User {\n                property name -> str;\n                multi property tweets := Tweet.text;\n            }\n            type Tweet {\n                property text -> str;\n                link author -> User;\n            }\n        ''', module='default')\n\n        await self.con.execute(\"\"\"\n            INSERT Tweet {\n                text := 'Hello',\n                author := (\n                    INSERT User {name := 'Alice'}\n                )\n            };\n            INSERT Tweet {\n                text := 'Hi',\n                author := (\n                    INSERT User {name := 'Billie'}\n                )\n            };\n        \"\"\")\n\n        # Validate our structures\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Tweet {\n                    text,\n                    author: {\n                        name\n                    },\n                } ORDER BY .text;\n            \"\"\",\n            [{\n                'text': 'Hello',\n                'author': {\n                    'name': 'Alice'\n                },\n            }, {\n                'text': 'Hi',\n                'author': {\n                    'name': 'Billie'\n                },\n            }],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User {\n                    name,\n                    tweets,\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'Alice',\n                'tweets': {'Hello', 'Hi'},\n            }, {\n                'name': 'Billie',\n                'tweets': {'Hello', 'Hi'},\n            }],\n        )\n\n        await self.migrate(r'''\n            type User {\n                property name -> str;\n                multi property tweets := User.<author[IS Tweet].text;\n            }\n            type Tweet {\n                property text -> str;\n                link author -> User;\n            }\n        ''', module='default')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User {\n                    name,\n                    tweets,\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'Alice',\n                'tweets': {'Hello'},\n            }, {\n                'name': 'Billie',\n                'tweets': {'Hi'},\n            }],\n        )\n\n        await self.migrate(r'''\n            type User {\n                property name -> str;\n                multi property tweets := .<author[IS Tweet].text;\n            }\n            type Tweet {\n                property text -> str;\n                link author -> User;\n            }\n        ''', module='default')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User {\n                    name,\n                    tweets,\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'Alice',\n                'tweets': {'Hello'},\n            }, {\n                'name': 'Billie',\n                'tweets': {'Hi'},\n            }],\n        )\n\n        await self.migrate(r'''\n            type User {\n                property name -> str;\n                multi property tweets := (\n                    SELECT Tweet FILTER .author = User\n                ).text;\n            }\n            type Tweet {\n                property text -> str;\n                link author -> User;\n            }\n        ''', module='default')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User {\n                    name,\n                    tweets,\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'Alice',\n                'tweets': {'Hello'},\n            }, {\n                'name': 'Billie',\n                'tweets': {'Hi'},\n            }],\n        )\n\n        await self.migrate(r'''\n            type User {\n                property name -> str;\n                multi property tweets := (\n                    WITH U := User\n                    SELECT Tweet FILTER .author = U\n                ).text;\n            }\n            type Tweet {\n                property text -> str;\n                link author -> User;\n            }\n        ''', module='default')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User {\n                    name,\n                    tweets,\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'Alice',\n                'tweets': {'Hello'},\n            }, {\n                'name': 'Billie',\n                'tweets': {'Hi'},\n            }],\n        )\n\n        await self.migrate(r'''\n            type User {\n                property name -> str;\n                multi property tweets := (\n                    WITH U := DETACHED User\n                    SELECT Tweet FILTER .author = U\n                ).text;\n            }\n            type Tweet {\n                property text -> str;\n                link author -> User;\n            }\n        ''', module='default')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User {\n                    name,\n                    tweets,\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'Alice',\n                'tweets': {'Hello', 'Hi'},\n            }, {\n                'name': 'Billie',\n                'tweets': {'Hello', 'Hi'},\n            }],\n        )\n\n        await self.migrate(r'''\n            type User {\n                property name -> str;\n                multi property tweets := (\n                    WITH U := User\n                    SELECT U.<author[IS Tweet].text\n                );\n            }\n            type Tweet {\n                property text -> str;\n                link author -> User;\n            }\n        ''', module='default')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User {\n                    name,\n                    tweets,\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'Alice',\n                'tweets': {'Hello'},\n            }, {\n                'name': 'Billie',\n                'tweets': {'Hi'},\n            }],\n        )\n\n        await self.migrate(r'''\n            type User {\n                property name -> str;\n                multi property tweets := (\n                    WITH User := DETACHED User\n                    SELECT User.<author[IS Tweet].text\n                );\n            }\n            type Tweet {\n                property text -> str;\n                link author -> User;\n            }\n        ''', module='default')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User {\n                    name,\n                    tweets,\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'Alice',\n                'tweets': {'Hello', 'Hi'},\n            }, {\n                'name': 'Billie',\n                'tweets': {'Hello', 'Hi'},\n            }],\n        )\n\n    async def test_edgeql_migration_computed_05(self):\n        await self.migrate(r'''\n            type Bar {\n                multi link foo := Foo;\n                property name -> str;\n            };\n            type Foo {\n                link bar -> Bar;\n                property val -> str;\n            };\n        ''', module='default')\n\n        await self.con.execute(\"\"\"\n            INSERT Foo {\n                val := 'foo0',\n                bar := (\n                    INSERT Bar {name := 'bar0'}\n                ),\n            };\n            INSERT Foo {\n                val := 'foo1',\n                bar := (\n                    INSERT Bar {name := 'bar1'}\n                ),\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Foo {\n                    val,\n                    bar: {\n                        name,\n                        foo: {\n                            val\n                        } ORDER BY .val,\n                    },\n                } ORDER BY .val;\n            \"\"\",\n            [{\n                'val': 'foo0',\n                'bar': {\n                    'name': 'bar0',\n                    'foo': [{'val': 'foo0'}, {'val': 'foo1'}],\n                },\n            }, {\n                'val': 'foo1',\n                'bar': {\n                    'name': 'bar1',\n                    'foo': [{'val': 'foo0'}, {'val': 'foo1'}],\n                },\n            }],\n        )\n\n    async def test_edgeql_migration_computed_06(self):\n        await self.migrate(r'''\n            type Bar {\n                multi property foo := Foo.val;\n                property name -> str;\n            };\n            type Foo {\n                link bar -> Bar;\n                property val -> str;\n            };\n        ''', module='default')\n\n        await self.con.execute(\"\"\"\n            INSERT Foo {\n                val := 'foo0',\n                bar := (\n                    INSERT Bar {name := 'bar0'}\n                ),\n            };\n            INSERT Foo {\n                val := 'foo1',\n                bar := (\n                    INSERT Bar {name := 'bar1'}\n                ),\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Foo {\n                    val,\n                    bar: {\n                        name,\n                        foo,\n                    },\n                } ORDER BY .val;\n            \"\"\",\n            [{\n                'val': 'foo0',\n                'bar': {\n                    'name': 'bar0',\n                    'foo': {'foo0', 'foo1'},\n                },\n            }, {\n                'val': 'foo1',\n                'bar': {\n                    'name': 'bar1',\n                    'foo': {'foo0', 'foo1'},\n                },\n            }],\n        )\n\n    async def test_edgeql_migration_computed_07(self):\n        await self.migrate(r'''\n            type T;\n            type S {\n                multi ts: T;\n                val := count(.ts);\n            };\n        ''', module='default')\n        await self.migrate(r'''\n            type T;\n            type S {\n                multi ts: T;\n                val := 0;\n            };\n        ''', module='default')\n        await self.migrate(r'''\n            type T;\n            type S {\n                multi ts: T;\n                val := count(.ts);\n            };\n        ''', module='default')\n\n    async def test_edgeql_migration_reject_prop_01(self):\n        await self.migrate('''\n            type User {\n                property foo -> str;\n            };\n        ''')\n\n        await self.start_migration('''\n            type User {\n                property bar -> str;\n            };\n        ''')\n\n        await self.interact([\n            (\"did you rename property 'foo' of object type \"\n             \"'test::User' to 'bar'?\", \"n\"),\n            # XXX: or should this be split up?\n            \"did you alter object type 'test::User'?\"\n        ])\n        await self.fast_forward_describe_migration()\n\n    async def test_edgeql_migration_reject_prop_02(self):\n        await self.migrate('''\n            type User {\n                required property foo -> str;\n            };\n        ''')\n\n        await self.start_migration('''\n            type User {\n                property bar -> str;\n            };\n        ''')\n\n        # Initial confidence should *not* be 1.0 here\n        res = json.loads(await self.con.query_single(\n            'DESCRIBE CURRENT MIGRATION AS JSON;'))\n        self.assertLess(res['proposed']['confidence'], 1.0)\n\n        await self.interact([\n            (\"did you rename property 'foo' of object type 'test::User' to \"\n             \"'bar'?\", \"n\"),\n            # XXX: or should this be split up?\n            \"did you alter object type 'test::User'?\"\n        ])\n        await self.fast_forward_describe_migration()\n\n    async def test_edgeql_migration_reject_prop_03(self):\n        await self.migrate('''\n            type User {\n                required property foo -> str;\n            };\n        ''')\n\n        await self.start_migration('''\n            type User {\n                required property bar -> int64;\n            };\n        ''')\n\n        await self.interact([\n            # Or should this be split into rename and reset optionality?\n            (\"did you create property 'bar' of object type 'test::User'?\",\n             \"n\"),\n            (\"did you rename property 'foo' of object type 'test::User' to \"\n             \"'bar'?\"),\n            (\"did you alter the type of property 'bar' of object type \"\n             \"'test::User'?\",\n             \"y\",\n             \"<int64>.bar\"),\n        ])\n        await self.fast_forward_describe_migration()\n\n    async def test_edgeql_migration_reject_prop_04(self):\n        await self.migrate('''\n            type Foo;\n            type Bar;\n        ''')\n\n        await self.start_migration('''\n            type Foo;\n            type Bar extending Foo;\n        ''')\n\n        await self.interact([\n            (\"did you alter object type 'test::Bar'?\", \"n\"),\n            \"did you drop object type 'test::Bar'?\",\n            \"did you create object type 'test::Bar'?\",\n        ])\n        await self.fast_forward_describe_migration()\n\n    @test.xerror('Fails to rebase because the type is mismatched')\n    async def test_edgeql_migration_reject_prop_05(self):\n        await self.migrate('''\n            scalar type Slug extending str;\n            abstract type Named {\n                required property name -> Slug;\n            };\n            type User {\n                required property name -> str;\n            };\n        ''')\n\n        await self.start_migration('''\n            scalar type Slug extending str;\n            abstract type Named {\n                required property name -> Slug;\n            };\n            type User extending Named;\n        ''')\n\n        await self.interact([\n            (\"did you drop property 'name' of object type 'test::User'?\", \"n\"),\n        ], check_complete=False)\n        await self.fast_forward_describe_migration()\n\n    async def test_edgeql_migration_vector_change_01(self):\n        await self.migrate('''\n            using extension pgvector;\n            module default {\n                scalar type Embedding extending ext::pgvector::vector<384>;\n                type Obj {\n                   embeddings: Embedding;\n                }\n            }\n        ''', explicit_modules=True)\n\n        await self.start_migration('''\n            using extension pgvector;\n            module default {\n                scalar type Embedding extending ext::pgvector::vector<768>;\n                type Obj {\n                   embeddings: Embedding;\n                }\n            }\n        ''', explicit_modules=True)\n\n        await self.assert_describe_migration({\n            'proposed': {\n                'statements': [{\n                    'text': \"\"\"\n                        ALTER TYPE default::Obj {DROP PROPERTY embeddings;};\n                    \"\"\"\n                }]\n            }\n        })\n\n        async with self.assertRaisesRegexTx(\n            edgedb.SchemaDefinitionError,\n            r\"cannot produce migration because of a dependency cycle\",\n        ):\n            await self.interact([\n                (\"did you drop property 'embeddings' of object type \"\n                 \"'default::Obj'?\", \"n\"),\n            ])\n\n    async def test_edgeql_migration_force_delete_01(self):\n        await self.migrate('''\n            type Base;\n            type Foo;\n            type Bar { link foo -> Foo; };\n        ''')\n\n        await self.start_migration('''\n            type Base;\n            type Foo extending Base;\n            type Bar { link foo -> Foo; };\n        ''')\n\n        await self.interact([\n            (\"did you alter object type 'test::Foo'?\", \"n\"),\n            \"did you drop link 'foo' of object type 'test::Bar'?\"\n        ], check_complete=False)\n        await self.fast_forward_describe_migration()\n\n    async def test_edgeql_migration_force_delete_02(self):\n        await self.migrate('''\n            type Base;\n            type Foo;\n            type Bar extending Foo;\n        ''')\n\n        await self.start_migration('''\n            type Base;\n            type Foo extending Base;\n            type Bar extending Foo;\n        ''')\n\n        await self.interact([\n            (\"did you alter object type 'test::Foo'?\", \"n\"),\n            \"did you drop object type 'test::Bar'?\"\n        ], check_complete=False)\n        await self.fast_forward_describe_migration()\n\n    async def test_edgeql_migration_eq_01(self):\n        await self.migrate(\"\"\"\n            type Base;\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n        \"\"\")\n\n        # Try altering the schema to a state inconsistent with current\n        # data.\n        with self.assertRaisesRegex(\n            AssertionError,\n            r\"Please specify an expression to populate existing objects \"\n            r\"in order to make property 'name' of object type 'test::Base' \"\n            r\"required\"\n        ):\n            await self.migrate(\"\"\"\n                type Base {\n                    required property name -> str;\n                }\n            \"\"\")\n        # Migration without making the property required.\n        await self.migrate(\"\"\"\n            type Base {\n                property name -> str;\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    name\n                };\n            \"\"\",\n            [{\n                'name': None,\n            }],\n        )\n\n        await self.con.execute(\"\"\"\n            UPDATE\n                Base\n            SET {\n                name := 'base_01'\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    name\n                };\n            \"\"\",\n            [{\n                'name': 'base_01',\n            }],\n        )\n\n        # Inherit from the Base, making name required.\n        await self.migrate(\"\"\"\n            type Base {\n                property name -> str;\n            }\n\n            type Derived extending Base {\n                overloaded required property name -> str;\n            }\n        \"\"\")\n        await self.con.execute(\"\"\"\n            INSERT Derived {\n                name := 'derived_01'\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base.name;\n            \"\"\",\n            {'base_01', 'derived_01'},\n        )\n\n    async def test_edgeql_migration_eq_02(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> str;\n            }\n\n            type Derived extending Base {\n                overloaded required property foo -> str;\n            }\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                foo := 'base_02',\n            };\n            INSERT Derived {\n                foo := 'derived_02',\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Base {\n                # rename 'foo'\n                property foo2 -> str;\n            }\n\n            type Derived extending Base {\n                overloaded required property foo2 -> str;\n            }\n        \"\"\")\n\n        # the data still persists\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    __type__: {name},\n                    foo2,\n                } ORDER BY .foo2;\n            \"\"\",\n            [{\n                '__type__': {'name': 'test::Base'},\n                'foo2': 'base_02',\n            }, {\n                '__type__': {'name': 'test::Derived'},\n                'foo2': 'derived_02',\n            }],\n        )\n\n    async def test_edgeql_migration_eq_03(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> str;\n            }\n\n            type Derived extending Base {\n                overloaded required property foo -> str;\n            }\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                foo := 'base_03',\n            };\n            INSERT Derived {\n                foo := 'derived_03',\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Base;\n                # drop 'foo'\n\n            type Derived extending Base {\n                # completely different property\n                property foo2 -> str;\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    __type__: {name},\n                    [IS Derived].foo2,\n                } ORDER BY .foo2;\n            \"\"\",\n            [{\n                '__type__': {'name': 'test::Base'},\n                'foo2': None,\n            }, {\n                '__type__': {'name': 'test::Derived'},\n                'foo2': None,\n            }],\n        )\n\n    async def test_edgeql_migration_eq_04(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> str;\n            }\n\n            type Derived extending Base;\n\n            type Further extending Derived {\n                overloaded required property foo -> str;\n            }\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                foo := 'base_04',\n            };\n            INSERT Derived {\n                foo := 'derived_04',\n            };\n            INSERT Further {\n                foo := 'further_04',\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Base;\n                # drop 'foo'\n\n            type Derived extending Base;\n\n            type Further extending Derived {\n                # completely different property\n                property foo2 -> str;\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    __type__: {name},\n                    [IS Further].foo2,\n                } ORDER BY .__type__.name;\n            \"\"\",\n            [{\n                '__type__': {'name': 'test::Base'},\n                'foo2': None,\n            }, {\n                '__type__': {'name': 'test::Derived'},\n                'foo2': None,\n            }, {\n                '__type__': {'name': 'test::Further'},\n                'foo2': None,\n            }],\n        )\n\n    async def test_edgeql_migration_eq_06(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> int64;\n            }\n\n            type Derived extending Base {\n                overloaded required property foo -> int64;\n            }\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                foo := 6,\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    __type__: {name},\n                    foo,\n                };\n            \"\"\",\n            [{\n                '__type__': {'name': 'test::Base'},\n                # the value was correctly inserted\n                'foo': 6,\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> float64;\n            }\n\n            type Derived extending Base {\n                overloaded required property foo -> float64;\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    __type__: {name},\n                    foo,\n                };\n            \"\"\",\n            [{\n                '__type__': {'name': 'test::Base'},\n                'foo': 6.0,\n            }],\n        )\n\n    async def test_edgeql_migration_eq_07(self):\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child {\n                required property name -> str {\n                    constraint exclusive;\n                }\n            }\n\n            type Base {\n                required property name -> str;\n                link bar -> Child;\n            }\n        \"\"\")\n\n        await self.con.execute('''\n            INSERT Child { name := 'c1' };\n            INSERT Child { name := 'c2' };\n\n            INSERT Base {\n                name := 'b1',\n                bar := (SELECT Child FILTER .name = 'c1'),\n            };\n\n            INSERT Base {\n                name := 'b2',\n            };\n        ''')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    bar: {\n                        name\n                    }\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'bar': {\n                    'name': 'c1',\n                },\n            }, {\n                'bar': None,\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            type Child {\n                required property name -> str {\n                    constraint exclusive;\n                }\n            }\n\n            type Base {\n                required property name -> str;\n                required link bar -> Child {\n                    # add a constraint\n                    constraint exclusive;\n                }\n            }\n        \"\"\", user_input=[\n            \"SELECT Child FILTER .name = 'c2'\"\n        ])\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    bar: {\n                        name\n                    }\n                } ORDER BY .name;\n            \"\"\",\n            [{\n                'bar': {\n                    'name': 'c1',\n                },\n            }, {\n                'bar': {\n                    'name': 'c2',\n                },\n            }],\n        )\n\n    async def test_edgeql_migration_eq_08(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> str;\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                foo := 'very_long_test_str_base_08',\n            };\n        \"\"\")\n\n        # Try altering the schema to a state inconsistent with current\n        # data.\n        new_state = r\"\"\"\n            type Base {\n                property foo -> str {\n                    # add a constraint\n                    constraint max_len_value(10);\n                }\n            }\n        \"\"\"\n        with self.assertRaisesRegex(\n                edgedb.ConstraintViolationError,\n                r\"foo must be no longer than 10 characters\"):\n            await self.migrate(new_state)\n\n        # Fix the data.\n        await self.con.execute(r\"\"\"\n            UPDATE Base\n            SET {\n                foo := 'base_08',\n            };\n        \"\"\")\n\n        # Migrate to same state as before now that the data is fixed.\n        await self.migrate(new_state)\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo,\n                };\n            \"\"\",\n            [{\n                'foo': 'base_08',\n            }],\n        )\n\n    async def test_edgeql_migration_eq_09(self):\n        await self.migrate(r\"\"\"\n            scalar type constraint_length extending str {\n                constraint max_len_value(10);\n            }\n            type Base {\n                property foo -> constraint_length;\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                foo := 'b09',\n            };\n        \"\"\")\n\n        # Try altering the schema to a state inconsistent with current\n        # data.\n        new_state = r\"\"\"\n            scalar type constraint_length extending str {\n                constraint max_len_value(10);\n                # add a constraint\n                constraint min_len_value(5);\n            }\n            type Base {\n                property foo -> constraint_length;\n            }\n        \"\"\"\n        with self.assertRaisesRegex(\n                edgedb.ConstraintViolationError,\n                r'Existing test::Base\\.foo values violate the new constraint'):\n            await self.migrate(new_state)\n\n        # Fix the data.\n        await self.con.execute(r\"\"\"\n            UPDATE Base\n            SET {\n                foo := 'base_09',\n            };\n        \"\"\")\n\n        # Migrate to same state as before now that the data is fixed.\n        await self.migrate(new_state)\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo,\n                };\n            \"\"\",\n            [{\n                'foo': 'base_09',\n            }],\n        )\n\n    async def test_edgeql_migration_eq_11(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> str;\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                foo := 'base_11',\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                # change property to link with same name\n                link foo -> Child {\n                    # add a constraint\n                    constraint exclusive;\n                }\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo,\n                };\n            \"\"\",\n            [{\n                'foo': None,\n            }],\n        )\n\n    async def test_edgeql_migration_eq_12(self):\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                property foo -> str {\n                    constraint exclusive;\n                }\n\n                link bar -> Child {\n                    constraint exclusive;\n                }\n            }\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n        data = await self.con.query(r\"\"\"\n            SELECT (\n                INSERT Base {\n                    foo := 'base_12',\n                    bar := (INSERT Child)\n                })\n            {\n                foo,\n                bar: {id}\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                # drop constraints\n                property foo -> str;\n                link bar -> Child;\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo,\n                    bar: {id}\n                };\n            \"\"\",\n            [{\n                'foo': 'base_12',\n                'bar': {'id': data[0].bar.id}\n            }],\n        )\n\n    async def test_edgeql_migration_eq_13(self):\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link bar -> Child;\n            }\n\n            type Derived extending Base {\n                overloaded required link bar -> Child;\n            }\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n        data = await self.con.query(r\"\"\"\n            SELECT (\n                INSERT Derived {\n                    bar := (INSERT Child)\n                })\n            {\n                bar: {id}\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base;\n                # drop 'bar'\n\n            type Derived extending Base {\n                # no longer inherit link 'bar'\n                link bar -> Child;\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Derived {\n                    bar: {id}\n                };\n            \"\"\",\n            [{\n                'bar': {'id': data[0].bar.id}\n            }],\n        )\n\n    async def test_edgeql_migration_eq_14a(self):\n        await self.migrate(r\"\"\"\n            type Base;\n\n            type Derived extending Base {\n                property foo -> str;\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Derived {\n                foo := 'derived_14',\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Base {\n                # move the property earlier in the inheritance\n                property foo -> str;\n            }\n\n            type Derived extending Base {\n                overloaded property foo -> str {\n                    annotation title := 'overloaded'\n                }\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Derived {\n                    foo,\n                };\n            \"\"\",\n            [{\n                'foo': 'derived_14',\n            }],\n        )\n\n    async def test_edgeql_migration_eq_14b(self):\n        # Same as above, except POPULATE and inspect the query\n        await self.migrate(r\"\"\"\n            type Base;\n\n            type Derived extending Base {\n                property foo -> str;\n            }\n        \"\"\")\n\n        await self.start_migration(r\"\"\"\n            type Base {\n                # move the property earlier in the inheritance\n                property foo -> str;\n            }\n\n            type Derived extending Base {\n                overloaded required property foo -> str;\n            }\n        \"\"\", populate=True)\n\n        await self.assert_describe_migration({\n            'confirmed': [\"\"\"\n                ALTER TYPE test::Base {\n                    CREATE PROPERTY foo: std::str;\n                };\n            \"\"\", \"\"\"\n                ALTER TYPE test::Derived {\n                    ALTER PROPERTY foo {\n                        SET REQUIRED;\n                    };\n                };\n            \"\"\"],\n            'complete': True,\n        })\n\n    async def test_edgeql_migration_eq_16(self):\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base;\n\n            type Derived extending Base {\n                link bar -> Child;\n            }\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n        data = await self.con.query(r\"\"\"\n            SELECT (\n                INSERT Derived {\n                    bar := (INSERT Child),\n                }\n            ) {\n                bar: {id}\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                # move the link earlier in the inheritance\n                link bar -> Child;\n            }\n\n            type Derived extending Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Derived {\n                    bar,\n                };\n            \"\"\",\n            [{\n                'bar': {'id': data[0].bar.id},\n            }],\n        )\n\n        await self.migrate(\n            r\"\"\"\n                type Child;\n\n                type Base {\n                    link bar -> Child;\n                }\n\n                type Derived extending Base {\n                    # also make the link 'required'\n                    overloaded required link bar -> Child;\n                }\n            \"\"\",\n            user_input=[\n                '.bar',\n            ],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Derived {\n                    bar,\n                };\n            \"\"\",\n            [{\n                'bar': {'id': data[0].bar.id},\n            }],\n        )\n\n    async def test_edgeql_migration_eq_18a(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property name := 'computable'\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Base {\n                # change a property from a computable to regular with a default\n                property name -> str {\n                    default := 'something'\n                }\n            }\n        \"\"\")\n\n        # Insert a new object, this one should have a new default name.\n        await self.con.execute(r\"\"\"\n            INSERT Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    name,\n                } ORDER BY .name EMPTY LAST;\n            \"\"\",\n            [{\n                'name': 'something',\n            }, {\n                'name': 'something',\n            }],\n        )\n\n    async def test_edgeql_migration_eq_18b(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property name := 'computable'\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Base {\n                # change a property from a computable to regular with a default\n                property name -> str {\n                    default := <str>count(Object)\n                }\n            }\n        \"\"\")\n\n        # Insert a new object, this one should have a new default name.\n        await self.con.execute(r\"\"\"\n            INSERT Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    name,\n                } ORDER BY .name EMPTY LAST;\n            \"\"\",\n            [{\n                'name': str,\n            }, {\n                'name': str,\n            }],\n        )\n\n    async def test_edgeql_migration_eq_18c(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property name := 'computable'\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Base {\n                # change a property from a computable to regular with a default\n                required property name -> str {\n                    default := <str>count(Object)\n                }\n            }\n        \"\"\")\n\n        # Insert a new object, this one should have a new default name.\n        await self.con.execute(r\"\"\"\n            INSERT Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    name,\n                } ORDER BY .name EMPTY LAST;\n            \"\"\",\n            [{\n                'name': str,\n            }, {\n                'name': str,\n            }],\n        )\n\n    async def test_edgeql_migration_eq_19(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                name := 'base_19'\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Base {\n                # change a regular property to a computable\n                property name := 'computable'\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    name,\n                };\n            \"\"\",\n            [{\n                'name': 'computable',\n            }],\n        )\n\n    async def test_edgeql_migration_eq_21(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> str;\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                foo := 'base_21'\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> str;\n                # add a property\n                property bar -> int64;\n            }\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            UPDATE Base\n            SET {\n                bar := 21\n            };\n        \"\"\")\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo,\n                    bar\n                };\n            \"\"\",\n            [{\n                'foo': 'base_21',\n                'bar': 21,\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                # make the old property into a computable\n                property foo := <str>__source__.bar;\n                property bar -> int64;\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo,\n                    bar\n                };\n            \"\"\",\n            [{\n                'foo': '21',\n                'bar': 21,\n            }],\n        )\n\n    async def test_edgeql_migration_eq_22(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> str;\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                foo := 'base_22'\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            # rename the type, although this test doesn't ensure that\n            # renaming actually took place\n            type NewBase {\n                property foo -> str;\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT NewBase {\n                    foo,\n                };\n            \"\"\",\n            [{\n                'foo': 'base_22',\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            type NewBase {\n                property foo -> str;\n                # add a property\n                property bar -> int64;\n            }\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            UPDATE NewBase\n            SET {\n                bar := 22\n            };\n        \"\"\")\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT NewBase {\n                    foo,\n                    bar\n                };\n            \"\"\",\n            [{\n                'foo': 'base_22',\n                'bar': 22,\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            type NewBase {\n                # drop 'foo'\n                property bar -> int64;\n            }\n\n            # add a alias to emulate the original\n            alias Base := (\n                SELECT NewBase {\n                    foo := <str>.bar\n                }\n            );\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo,\n                };\n            \"\"\",\n            [{\n                'foo': '22',\n            }],\n        )\n\n    async def test_edgeql_migration_eq_23(self):\n        await self.migrate(r\"\"\"\n            type Child {\n                property foo -> str;\n            }\n\n            type Base {\n                link bar -> Child;\n            }\n\n            alias Alias01 := (\n                SELECT Base {\n                    child_foo := .bar.foo\n                }\n            );\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                bar := (\n                    INSERT Child {\n                        foo := 'child_23'\n                    }\n                )\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child {\n                property foo -> str;\n            }\n\n            # exchange a type for a alias\n            alias Base := (\n                SELECT Child {\n                    # bar is the same as the root object\n                    bar := Child\n                }\n            );\n\n            alias Alias01 := (\n                # now this alias refers to another alias\n                SELECT Base {\n                    child_foo := .bar.foo\n                }\n            );\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Alias01 {\n                    child_foo,\n                };\n            \"\"\",\n            [{\n                'child_foo': 'child_23',\n            }],\n        )\n\n    async def test_edgeql_migration_eq_24(self):\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link bar -> Child;\n            }\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n        data = await self.con.query(r\"\"\"\n            SELECT (\n                INSERT Base {\n                    bar := (INSERT Child)\n                }\n            ) {\n                bar: {id}\n            }\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                # increase link cardinality\n                multi link bar -> Child;\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    bar: {id},\n                };\n            \"\"\",\n            [{\n                'bar': [{'id': data[0].bar.id}],\n            }],\n        )\n\n    async def test_edgeql_migration_eq_25(self):\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                multi link bar -> Child;\n            }\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n        data = await self.con.query(r\"\"\"\n            SELECT (\n                INSERT Base {\n                    bar := (INSERT Child)\n                }\n            ) {\n                bar: {id}\n            }\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                # reduce link cardinality\n                link bar -> Child;\n            }\n        \"\"\", user_input=[\n            '(SELECT .bar LIMIT 1)'\n        ])\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    bar: {id},\n                };\n            \"\"\",\n            [{\n                'bar': {'id': data[0].bar[0].id},\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link bar -> Child {\n                    # further restrict the link\n                    constraint exclusive\n                }\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    bar: {id},\n                };\n            \"\"\",\n            [{\n                'bar': {'id': data[0].bar[0].id},\n            }],\n        )\n\n    async def test_edgeql_migration_eq_26(self):\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Parent {\n                link bar -> Child;\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Parent {\n                bar := (INSERT Child)\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Parent {\n                link bar -> Child;\n            }\n\n            # derive a type\n            type DerivedParent extending Parent;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Parent {\n                    type := .__type__.name,\n                    bar_type := .bar.__type__.name\n                };\n            \"\"\",\n            [{\n                'type': 'test::Parent',\n                'bar_type': 'test::Child',\n            }],\n        )\n\n        await self.migrate(\n            r\"\"\"\n                type Child;\n\n                type DerivedChild extending Child;\n\n                type Parent {\n                    link bar -> Child;\n                }\n\n                type DerivedParent extending Parent;\n            \"\"\",\n        )\n\n        await self.migrate(\n            r\"\"\"\n                type Child;\n\n                type DerivedChild extending Child;\n\n                type Parent {\n                    link bar -> Child;\n                }\n\n                # derive a type with a more restrictive link\n                type DerivedParent extending Parent {\n                    overloaded link bar -> DerivedChild;\n                }\n            \"\"\",\n            user_input=[\".bar[IS DerivedChild]\"],\n        )\n\n        await self.con.execute(r\"\"\"\n            INSERT DerivedParent {\n                bar := (INSERT DerivedChild)\n            }\n        \"\"\")\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Parent {\n                    type := .__type__.name,\n                    bar_type := .bar.__type__.name\n                } ORDER BY .bar_type;\n            \"\"\",\n            [{\n                'type': 'test::Parent',\n                'bar_type': 'test::Child',\n            }, {\n                'type': 'test::DerivedParent',\n                'bar_type': 'test::DerivedChild',\n            }],\n        )\n\n    async def test_edgeql_migration_eq_27(self):\n        await self.migrate(r\"\"\"\n            abstract type Named {\n                property name -> str;\n            }\n\n            type Foo extending Named;\n            type Bar extending Named;\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Foo {\n                name := 'foo_27',\n            };\n            INSERT Bar {\n                name := 'bar_27',\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            abstract type Named {\n                property name -> str;\n            }\n\n            # the types stop extending named, but retain the property\n            # 'name'\n            type Foo {\n                property name -> str;\n            };\n\n            type Bar {\n                property name -> str;\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Foo.name;\n            \"\"\",\n            [\n                'foo_27',\n            ],\n        )\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Bar.name;\n            \"\"\",\n            [\n                'bar_27',\n            ],\n        )\n\n        await self.migrate(r\"\"\"\n            abstract type Named {\n                property name -> str;\n            }\n\n            type Foo {\n                property name -> str;\n            };\n\n            type Bar {\n                # rename 'name' to 'title'\n                property title -> str;\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Foo.name;\n            \"\"\",\n            [\n                'foo_27',\n            ],\n        )\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Bar.title;\n            \"\"\",\n            [\n                'bar_27',\n            ],\n        )\n\n    async def test_edgeql_migration_eq_29(self):\n        await self.migrate(r\"\"\"\n            type Child {\n                property foo -> str;\n            }\n\n            alias Base := (\n                SELECT Child {\n                    bar := .foo\n                }\n            );\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Child {\n                foo := 'child_29',\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            # drop everything\n        \"\"\")\n\n    async def test_edgeql_migration_eq_30(self):\n        await self.migrate(r\"\"\"\n            type Foo {\n                property name -> str;\n            };\n\n            type Bar {\n                property title -> str;\n            };\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Foo {\n                name := 'foo_30',\n            };\n            INSERT Bar {\n                title := 'bar_30',\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Foo {\n                property name -> str;\n            };\n\n            type Bar {\n                # rename 'title' to 'name'\n                property name -> str;\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Foo.name;\n            \"\"\",\n            [\n                'foo_30',\n            ],\n        )\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Bar.name;\n            \"\"\",\n            [\n                'bar_30',\n            ],\n        )\n\n        await self.migrate(r\"\"\"\n            # both types have a name, so the name prop is factored out\n            # into a more basic type.\n            abstract type Named {\n                property name -> str;\n            }\n\n            type Foo extending Named;\n            type Bar extending Named;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Foo.name;\n            \"\"\",\n            [\n                'foo_30',\n            ],\n        )\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Bar.name;\n            \"\"\",\n            [\n                'bar_30',\n            ],\n        )\n\n    async def test_edgeql_migration_eq_31(self):\n        # Issue 727.\n        #\n        # Starting with the sample schema (from frontpage) migrate to\n        # a schema with only type User.\n        await self.migrate(r\"\"\"\n            # This is an abstract object containing\n            # text.\n            abstract type Text {\n              required property body -> str {\n                # Maximum length of text is 10000\n                # characters.\n                constraint max_len_value(10000);\n              }\n            }\n\n            type User {\n              required property name -> str;\n            }\n\n            abstract type Owned {\n              # By default links are optional.\n              required link owner -> User;\n            }\n\n            # UniquelyNamed is a an abstract type that\n            # enforces name uniqueness across all\n            # instances of its subtype.\n            abstract type UniquelyNamed {\n              required property name -> str {\n                delegated constraint exclusive;\n              }\n            }\n\n            type Status extending UniquelyNamed;\n\n            type Priority extending UniquelyNamed;\n\n            # LogEntry is an Owned and a Text,\n            # so it will have all of their links\n            # and properties, in particular, the\n            # \"owner\" link and the \"body\" property.\n            type LogEntry extending Owned, Text {\n              required property spent_time -> int64;\n            }\n\n            type Comment extending Text, Owned {\n              required link issue -> Issue;\n              link parent -> Comment;\n            }\n            # issue_num_t is defined as a concrete\n            # sequence type, used to generate\n            # sequential issue numbers.\n            scalar type issue_num_t extending sequence;\n\n            type Issue extending Owned, Text {\n              required property title -> str;\n\n              required property number -> issue_num_t {\n                # The number values are automatically\n                # generated, and are not supposed to be\n                # directly writable.\n                readonly := true;\n              }\n\n              property time_estimate -> int64;\n\n              property start_date -> datetime {\n                # The default value of start_date will be a\n                # result of the EdgeQL expression above.\n                default := (SELECT datetime_current());\n              }\n\n              property due_date -> datetime;\n\n              required link status -> Status;\n\n              link priority -> Priority;\n\n              # The watchers link is mapped to User\n              # type in many-to-many relation.\n              multi link watchers -> User;\n\n              multi link time_spent_log -> LogEntry {\n                # Exclusive multi-link represents\n                # a one-to-many relation.\n                constraint exclusive;\n              }\n\n              multi link related_to -> Issue;\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Status {\n                name := 'Open'\n            };\n            INSERT Status {\n                name := 'Closed'\n            };\n\n            INSERT User {\n                name := 'cosmophile'\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type User {\n              required property name -> str;\n            }\n        \"\"\")\n\n        # there's only the User left\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT User.name;\n            \"\"\",\n            [\n                'cosmophile',\n            ],\n        )\n\n    async def test_edgeql_migration_eq_32(self):\n        # Issue 727.\n        #\n        # Starting with a small schema migrate to remove its elements.\n        # There are non-zero default Objects existing in a fresh blank\n        # database because of placeholder objects used for GraphQL.\n        start_objects = await self.con.query_single(r\"\"\"\n            SELECT count(Object);\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type LogEntry {\n              required property spent_time -> int64;\n            }\n            type Issue {\n              multi link time_spent_log -> LogEntry {\n                constraint exclusive;\n              }\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT LogEntry {\n                spent_time := 100\n            };\n\n            INSERT Issue {\n                time_spent_log := LogEntry\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type LogEntry {\n              required property spent_time -> int64;\n            }\n        \"\"\")\n\n        # there's only the LogEntry left\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT LogEntry.spent_time;\n            \"\"\",\n            [\n                100,\n            ],\n        )\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT count(Object);\n            \"\"\",\n            [\n                start_objects + 1,\n            ],\n        )\n\n        await self.migrate(r\"\"\"\n            # empty schema\n        \"\"\")\n\n        # no more additional objects\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT count(Object);\n            \"\"\",\n            [\n                start_objects,\n            ],\n        )\n\n    async def test_edgeql_migration_eq_33(self):\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link foo -> Child;\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Child;\n            INSERT Base {\n                foo := (SELECT Child LIMIT 1)\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo: {\n                        __type__: {name},\n                    }\n                };\n            \"\"\",\n            [{\n                'foo': {\n                    '__type__': {'name': 'test::Child'},\n                }\n            }],\n        )\n\n        await self.migrate(\n            r\"\"\"\n                type Child;\n                type Child2;\n\n                type Base {\n                    link foo -> Child;\n                }\n            \"\"\",\n        )\n\n        await self.migrate(\n            r\"\"\"\n                type Child;\n                type Child2;\n\n                type Base {\n                    # change link type\n                    link foo -> Child2;\n                }\n            \"\"\",\n            user_input=[\n                '.foo[IS Child2]'\n            ],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo: {\n                        __type__: {name},\n                    }\n                };\n            \"\"\",\n            [{\n                # the link is empty because the target was changed\n                'foo': None\n            }],\n        )\n\n        await self.con.execute(r\"\"\"\n            INSERT Child2;\n\n            UPDATE Base\n            SET {\n                foo := (SELECT Child2 LIMIT 1)\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo: {\n                        __type__: {name},\n                    }\n                };\n            \"\"\",\n            [{\n                'foo': {\n                    '__type__': {'name': 'test::Child2'},\n                }\n            }],\n        )\n\n    async def test_edgeql_migration_eq_34(self):\n        # this is the reverse of test_edgeql_migration_eq_11\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link foo -> Child {\n                    constraint exclusive;\n                }\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Child;\n            INSERT Base {\n                foo := (SELECT Child LIMIT 1)\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo: {\n                        __type__: {name},\n                    }\n                };\n            \"\"\",\n            [{\n                'foo': {\n                    '__type__': {'name': 'test::Child'},\n                }\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                # change link to property with same name\n                property foo -> str;\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo\n                };\n            \"\"\",\n            [{\n                # the property is empty now\n                'foo': None\n            }],\n        )\n\n        await self.con.execute(r\"\"\"\n            UPDATE Base\n            SET {\n                foo := 'base_foo_34'\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo\n                };\n            \"\"\",\n            [{\n                'foo': 'base_foo_34'\n            }],\n        )\n\n    async def test_edgeql_migration_eq_35(self):\n        await self.migrate(r\"\"\"\n            type Child {\n                required property name -> str;\n            }\n\n            type Base {\n                multi link foo := (\n                    SELECT Child FILTER .name = 'computable_35'\n                )\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Child {\n                name := 'computable_35'\n            };\n            INSERT Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo: {\n                        name\n                    },\n                };\n            \"\"\",\n            [{\n                'foo': [{\n                    'name': 'computable_35',\n                }]\n            }]\n        )\n\n        await self.migrate(r\"\"\"\n            type Child {\n                required property name -> str;\n            }\n\n            type Base {\n                # change a link from a computable to regular\n                multi link foo -> Child;\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo: {\n                        name\n                    },\n                };\n            \"\"\",\n            [{\n                'foo': []\n            }]\n        )\n\n        # Make sure that the new 'foo' can be updated.\n        await self.con.execute(r\"\"\"\n            INSERT Child {\n                name := 'child_35'\n            };\n            UPDATE Base\n            SET {\n                foo := (\n                    SELECT Child FILTER .name = 'child_35'\n                )\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo: {\n                        name\n                    },\n                };\n            \"\"\",\n            [{\n                'foo': [{\n                    'name': 'child_35'\n                }]\n            }]\n        )\n\n    async def test_edgeql_migration_eq_36(self):\n        await self.migrate(r\"\"\"\n            type Child {\n                required property name -> str;\n            }\n\n            type Base {\n                multi link foo -> Child;\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Child {\n                name := 'computable_36'\n            };\n            INSERT Child {\n                name := 'child_36'\n            };\n            INSERT Base {\n                foo := (\n                    SELECT Child FILTER .name = 'child_36'\n                )\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child {\n                required property name -> str;\n            }\n\n            type Base {\n                # change a regular link to a computable\n                link foo := (\n                    SELECT Child FILTER .name = 'computable_36'\n                    LIMIT 1\n                )\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo: {\n                        name\n                    },\n                };\n            \"\"\",\n            [{\n                'foo': {\n                    'name': 'computable_36'\n                }\n            }]\n        )\n\n    async def test_edgeql_migration_eq_37(self):\n        # testing schema alias\n        await self.migrate(r\"\"\"\n            type Base;\n\n            alias BaseAlias := (\n                SELECT Base {\n                    foo := 'base_alias_37'\n                }\n            )\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT BaseAlias {\n                    foo\n                };\n            \"\"\",\n            [{\n                'foo': 'base_alias_37'\n            }]\n        )\n\n        await self.migrate(r\"\"\"\n            type Base;\n\n            alias BaseAlias := (\n                SELECT Base {\n                    # \"rename\" a computable, since the value is given and\n                    # not stored, this is no different from dropping\n                    # original and creating a new property\n                    foo2 := 'base_alias_37'\n                }\n            )\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT BaseAlias {\n                    foo2\n                };\n            \"\"\",\n            [{\n                'foo2': 'base_alias_37'\n            }]\n        )\n\n        with self.assertRaisesRegex(\n                edgedb.InvalidReferenceError,\n                r\"object type 'test::Base' has no link or property 'foo'\"):\n            await self.con.execute(r\"\"\"\n                SELECT BaseAlias {\n                    foo\n                };\n            \"\"\")\n\n    async def test_edgeql_migration_eq_38(self):\n        # testing schema alias\n        await self.migrate(r\"\"\"\n            type Base;\n\n            alias BaseAlias := (\n                SELECT Base {\n                    foo := 'base_alias_38'\n                }\n            )\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT BaseAlias {\n                    foo\n                };\n            \"\"\",\n            [{\n                'foo': 'base_alias_38'\n            }]\n        )\n\n        await self.migrate(r\"\"\"\n            type Base;\n\n            alias BaseAlias := (\n                SELECT Base {\n                    # keep the name, but change the type\n                    foo := 38\n                }\n            )\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT BaseAlias {\n                    foo\n                };\n            \"\"\",\n            [{\n                'foo': 38\n            }]\n        )\n\n    async def test_edgeql_migration_eq_39(self):\n        # testing schema alias\n        await self.migrate(r\"\"\"\n            type Base;\n\n            type Foo {\n                property name -> str\n            }\n\n            alias BaseAlias := (\n                SELECT Base {\n                    foo := (SELECT Foo FILTER .name = 'base_alias_39')\n                }\n            )\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n            INSERT Foo {name := 'base_alias_39'};\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT BaseAlias {\n                    foo: {\n                        name\n                    }\n                };\n            \"\"\",\n            [{\n                'foo': [{\n                    'name': 'base_alias_39'\n                }]\n            }]\n        )\n\n        await self.migrate(r\"\"\"\n            type Base;\n\n            type Foo {\n                property name -> str\n            }\n\n            alias BaseAlias := (\n                SELECT Base {\n                    # \"rename\" a computable, since the value is given and\n                    # not stored, this is no different from dropping\n                    # original and creating a new multi-link\n                    foo2 := (SELECT Foo FILTER .name = 'base_alias_39')\n                }\n            )\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT BaseAlias {\n                    foo2: {\n                        name\n                    }\n                };\n            \"\"\",\n            [{\n                'foo2': [{\n                    'name': 'base_alias_39'\n                }]\n            }]\n        )\n\n        with self.assertRaisesRegex(\n                edgedb.InvalidReferenceError,\n                r\"object type 'test::Base' has no link or property 'foo'\"):\n            await self.con.execute(r\"\"\"\n                SELECT BaseAlias {\n                    foo: {\n                        name\n                    }\n                };\n            \"\"\")\n\n    async def test_edgeql_migration_eq_40(self):\n        # testing schema alias\n        await self.migrate(r\"\"\"\n            type Base;\n\n            type Foo {\n                property name -> str\n            }\n\n            type Bar {\n                property name -> str\n            }\n\n            alias BaseAlias := (\n                SELECT Base {\n                    foo := (SELECT Foo FILTER .name = 'foo_40')\n                }\n            )\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n            INSERT Foo {name := 'foo_40'};\n            INSERT Bar {name := 'bar_40'};\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT BaseAlias {\n                    foo: {\n                        name\n                    }\n                };\n            \"\"\",\n            [{\n                'foo': [{\n                    'name': 'foo_40'\n                }]\n            }]\n        )\n\n        await self.migrate(r\"\"\"\n            type Base;\n\n            type Foo {\n                property name -> str\n            }\n\n            type Bar {\n                property name -> str\n            }\n\n            alias BaseAlias := (\n                SELECT Base {\n                    # keep the name, but change the type\n                    foo := (SELECT Bar FILTER .name = 'bar_40')\n                }\n            )\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT BaseAlias {\n                    foo: {\n                        name\n                    }\n                };\n            \"\"\",\n            [{\n                'foo': [{\n                    'name': 'bar_40'\n                }]\n            }]\n        )\n\n    async def test_edgeql_migration_eq_41(self):\n        # testing schema alias\n        await self.migrate(r\"\"\"\n            type Base;\n\n            type Foo {\n                property name -> str\n            }\n\n            alias BaseAlias := (\n                SELECT Base {\n                    foo := (\n                        SELECT Foo {\n                            @bar := 'foo_bar_alias_41'\n                        }\n                        FILTER .name = 'base_alias_41'\n                    )\n                }\n            )\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n            INSERT Foo {name := 'base_alias_41'};\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT BaseAlias {\n                    foo: {\n                        name,\n                        @bar\n                    }\n                };\n            \"\"\",\n            [{\n                'foo': [{\n                    'name': 'base_alias_41',\n                    '@bar': 'foo_bar_alias_41',\n                }]\n            }]\n        )\n\n        await self.migrate(r\"\"\"\n            type Base;\n\n            type Foo {\n                property name -> str\n            }\n\n            alias BaseAlias := (\n                SELECT Base {\n                    foo := (\n                        SELECT Foo {\n                            # \"rename\" a computable link property, since\n                            # the value is given and not stored, this is\n                            # no different from dropping original and\n                            # creating a new multi-link\n                            @baz := 'foo_bar_alias_41'\n                        }\n                        FILTER .name = 'base_alias_41'\n                    )\n                }\n            )\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT BaseAlias {\n                    foo: {\n                        name,\n                        @baz\n                    }\n                };\n            \"\"\",\n            [{\n                'foo': [{\n                    'name': 'base_alias_41',\n                    '@baz': 'foo_bar_alias_41'\n                }]\n            }]\n        )\n\n        with self.assertRaisesRegex(\n                edgedb.InvalidReferenceError,\n                r\"link 'foo' .* has no property 'bar'\"):\n            await self.con.execute(r\"\"\"\n                SELECT BaseAlias {\n                    foo: {\n                        name,\n                        @bar\n                    }\n                };\n            \"\"\")\n\n    async def test_edgeql_migration_eq_42(self):\n        # testing schema alias\n        await self.migrate(r\"\"\"\n            type Base;\n\n            type Foo {\n                property name -> str\n            }\n\n            alias BaseAlias := (\n                SELECT Base {\n                    foo := (\n                        SELECT Foo {\n                            @bar := 'foo_bar_alias_42'\n                        }\n                        FILTER .name = 'base_alias_42'\n                    )\n                }\n            )\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n            INSERT Foo {name := 'base_alias_42'};\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT BaseAlias {\n                    foo: {\n                        name,\n                        @bar\n                    }\n                };\n            \"\"\",\n            [{\n                'foo': [{\n                    'name': 'base_alias_42',\n                    '@bar': 'foo_bar_alias_42',\n                }]\n            }]\n        )\n\n        await self.migrate(r\"\"\"\n            type Base;\n\n            type Foo {\n                property name -> str\n            }\n\n            alias BaseAlias := (\n                SELECT Base {\n                    foo := (\n                        SELECT Foo {\n                            # keep the name, but change the type\n                            @bar := 42\n                        }\n                        FILTER .name = 'base_alias_42'\n                    )\n                }\n            )\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT BaseAlias {\n                    foo: {\n                        name,\n                        @bar\n                    }\n                };\n            \"\"\",\n            [{\n                'foo': [{\n                    'name': 'base_alias_42',\n                    '@bar': 42,\n                }]\n            }]\n        )\n\n    async def test_edgeql_migration_eq_43(self):\n        await self.migrate(r\"\"\"\n            abstract link Ordered {\n                property index -> int32;\n            }\n            type User;\n            abstract type Permissions {\n                multi link owners extending Ordered -> User;\n            };\n        \"\"\")\n        await self.migrate(r\"\")\n\n    async def test_edgeql_migration_permissions_03a(self):\n        # Check tracing dependency works\n        await self.migrate(r\"\"\"\n          function test(x: int64) -> int64 {\n              using (x);\n              required_permissions := foo;\n          };\n          permission foo;\n       \"\"\")\n\n    async def test_edgeql_migration_permissions_03b(self):\n        # Check tracing dependency works\n        await self.migrate(r\"\"\"\n          function test(x: int64) -> int64 {\n              using (1);\n              required_permissions := {foo, bar};\n          };\n          permission foo;\n          permission bar;\n       \"\"\")\n\n    async def test_edgeql_migration_permissions_03c(self):\n        # Check tracing dependency works\n        await self.migrate(r\"\"\"\n          permission foo;\n          permission bar;\n          function test(x: int64) -> int64 {\n              using (1);\n              required_permissions := {foo, bar};\n          };\n       \"\"\")\n\n    async def test_edgeql_migration_permissions_03d(self):\n        # Sigh... test using POPULATE MIGRATION also...\n        # Check tracing dependency works\n        await tb.DDLTestCase.migrate(self, r\"\"\"\n          permission foo;\n          function test(x: int64) -> int64 {\n              using (x);\n              required_permissions := foo;\n          };\n       \"\"\")\n\n    async def test_edgeql_migration_index_01(self):\n        await self.migrate('''\n            type Message {\n                required property text -> str;\n                index on (.text);\n            };\n        ''')\n\n        await self.migrate('''\n            type Message {\n                required property text -> str;\n                property ts -> datetime;\n                index on (.text);\n                index on (.ts);\n            };\n        ''')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT count((SELECT schema::ObjectType\n                              FILTER .name = 'test::Message').indexes)\n            \"\"\",\n            [2],\n        )\n\n    async def test_edgeql_migration_rebase_01(self):\n        await self.migrate(r\"\"\"\n            abstract type C;\n            abstract type P {\n                property p -> str;\n                property p2 -> str;\n                index on (.p);\n            };\n            type Foo extending C {\n                property foo -> str;\n            }\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            abstract type C;\n            abstract type P {\n                property p -> str;\n                property p2 -> str;\n                index on (.p);\n            };\n            type Foo extending C, P {\n                property foo -> str;\n            }\n        \"\"\")\n\n    async def test_edgeql_migration_rebase_02(self):\n        await self.migrate('''\n            type User;\n            abstract type Event {\n              required property createdAt -> datetime {\n                default := datetime_current();\n              }\n\n              required link user -> User;\n            }\n\n            type Post extending Event {\n              required property content -> str {\n                constraint min_len_value(1);\n                constraint max_len_value(280);\n              }\n            }\n        ''')\n\n        await self.start_migration('''\n            type User;\n            abstract type Event {\n              required property createdAt -> datetime {\n                default := datetime_current();\n              }\n\n              required link user -> User;\n            }\n\n            abstract type HasContent {\n              required property content -> str {\n                constraint min_len_value(1);\n                constraint max_len_value(280);\n              }\n            }\n\n            type Post extending Event, HasContent {\n            }\n\n            type Reply extending Event, HasContent {\n              required link post -> Post;\n            }\n        ''')\n\n        # N.B.: these prompts are OK but not canonical; if they are\n        # broken in favor of something better, just fix them.\n        await self.interact([\n            \"did you create object type 'test::HasContent'?\",\n            \"did you alter object type 'test::Post'?\",\n            \"did you alter property 'content' of object type 'test::Post'?\",\n            \"did you create object type 'test::Reply'?\",\n        ])\n\n    async def test_edgeql_migration_rebase_03(self):\n        await self.migrate('''\n            abstract type Named {\n                required property name -> str;\n            };\n\n            type Org;\n\n            abstract type OrgBound {\n                required link org -> Org;\n            };\n\n            abstract type OrgUniquelyNamed\n                extending Named, OrgBound\n            {\n                constraint exclusive on ((.name, .org))\n            }\n        ''')\n\n        await self.start_migration('''\n            abstract type Named {\n                required property name -> str;\n            };\n\n            type Org;\n            abstract type Resource;\n\n            abstract type OrgBound {\n                required link org -> Org;\n            };\n\n            abstract type OrgUniquelyNamedResource\n                extending Named, Resource, OrgBound\n            {\n                delegated constraint exclusive on ((.name, .org))\n            }\n        ''')\n\n        # N.B.: these prompts are OK but not canonical; if they are\n        # broken in favor of something better, just fix them.\n        await self.interact([\n            (\"did you drop object type 'test::OrgUniquelyNamed'?\", \"n\"),\n            \"did you create object type 'test::Resource'?\",\n            \"did you rename object type 'test::OrgUniquelyNamed' to \"\n            \"'test::OrgUniquelyNamedResource'?\",\n\n            \"did you alter object type 'test::OrgUniquelyNamedResource'?\",\n        ])\n\n    async def test_edgeql_migration_rename_01(self):\n        await self.migrate('''\n            type Foo;\n        ''')\n\n        await self.start_migration('''\n            type Bar {\n                property asdf -> str;\n            };\n        ''')\n\n        await self.interact([\n            \"did you rename object type 'test::Foo' to 'test::Bar'?\",\n            \"did you create property 'asdf' of object type 'test::Bar'?\",\n        ])\n\n    async def test_edgeql_migration_rename_02(self):\n        await self.migrate('''\n            type Foo {\n                property asdf -> str;\n            };\n            type Bar extending Foo {\n                overloaded property asdf -> str;\n            };\n        ''')\n\n        await self.start_migration('''\n            type Foo {\n                property womp -> str;\n            };\n            type Bar extending Foo {\n                overloaded property womp -> str {\n                    annotation title := \"foo\";\n                };\n            };\n        ''')\n\n        await self.interact([\n            \"did you rename property 'asdf' of object type 'test::Foo' to \"\n            \"'womp'?\",\n\n            \"did you create annotation 'std::title' of property 'womp'?\",\n        ])\n\n    async def test_edgeql_migration_rename_03(self):\n        await self.migrate('''\n            abstract constraint Asdf { using (__subject__ < 10) };\n            type Foo {\n                property x -> int64 {\n                    constraint Asdf;\n                }\n            }\n            type Bar extending Foo;\n        ''')\n\n        await self.start_migration('''\n            abstract constraint Womp { using (__subject__ < 10) };\n            type Foo {\n                property x -> int64 {\n                    constraint Womp;\n                }\n            }\n            type Bar extending Foo;\n        ''')\n\n        await self.interact([\n            \"did you rename abstract constraint 'test::Asdf' to \"\n            \"'test::Womp'?\",\n        ])\n\n    async def test_edgeql_migration_eq_function_01(self):\n        await self.migrate(r\"\"\"\n            function hello01(a: int64) -> str\n                using edgeql $$\n                    SELECT 'hello' ++ <str>a\n                $$\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello01(1);\"\"\",\n            ['hello1'],\n        )\n\n        # add an extra parameter with a default (so it can be omitted\n        # in principle)\n        await self.migrate(r\"\"\"\n            function hello01(a: int64, b: int64=42) -> str\n                using edgeql $$\n                    SELECT 'hello' ++ <str>(a + b)\n                $$\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello01(1);\"\"\",\n            ['hello43'],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello01(1, 2);\"\"\",\n            ['hello3'],\n        )\n\n    async def test_edgeql_migration_eq_function_02(self):\n        await self.migrate(r\"\"\"\n            function hello02(a: int64) -> str\n                using edgeql $$\n                    SELECT 'hello' ++ <str>a\n                $$\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello02(1);\"\"\",\n            ['hello1'],\n        )\n\n        # add an extra parameter with a default (so it can be omitted\n        # in principle)\n        await self.migrate(r\"\"\"\n            function hello02(a: int64, b: OPTIONAL int64=42) -> str\n                using edgeql $$\n                    SELECT 'hello' ++ <str>(a + (b ?? -1))\n                $$\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello02(1);\"\"\",\n            ['hello43'],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello02(1, 2);\"\"\",\n            ['hello3'],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello02(1, <int64>{});\"\"\",\n            ['hello0'],\n        )\n\n    async def test_edgeql_migration_eq_function_03(self):\n        await self.migrate(r\"\"\"\n            function hello03(a: int64) -> str\n                using edgeql $$\n                    SELECT 'hello' ++ <str>a\n                $$\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello03(1);\"\"\",\n            ['hello1'],\n        )\n\n        # add an extra parameter with a default (so it can be omitted\n        # in principle)\n        await self.migrate(r\"\"\"\n            function hello03(a: int64, NAMED ONLY b: int64=42) -> str\n                using edgeql $$\n                    SELECT 'hello' ++ <str>(a + b)\n                $$\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello03(1);\"\"\",\n            ['hello43'],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello03(1, b := 2);\"\"\",\n            ['hello3'],\n        )\n\n    async def test_edgeql_migration_eq_function_04(self):\n        await self.migrate(r\"\"\"\n            function hello04(a: int64) -> str\n                using edgeql $$\n                    SELECT 'hello' ++ <str>a\n                $$\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello04(1);\"\"\",\n            ['hello1'],\n        )\n\n        # same parameters, different return type\n        await self.migrate(r\"\"\"\n            function hello04(a: int64) -> int64\n                using edgeql $$\n                    SELECT -a\n                $$\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello04(1);\"\"\",\n            [-1],\n        )\n\n    async def test_edgeql_migration_eq_function_05(self):\n        await self.migrate(r\"\"\"\n            function hello05(a: int64) -> str\n                using edgeql $$\n                    SELECT <str>a\n                $$\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello05(1);\"\"\",\n            ['1'],\n        )\n\n        # same parameters, different return type (array)\n        await self.migrate(r\"\"\"\n            function hello05(a: int64) -> array<int64>\n                using edgeql $$\n                    SELECT [a]\n                $$\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello05(1);\"\"\",\n            [[1]],\n        )\n\n    @test.xfail('''\n        It should be possible to change the underlying function (to a\n        compatible one) of a default value without explicitly dropping\n        the default first.\n\n        Currently this kind of works... by proposing we delete the property\n        and recreate it.\n    ''')\n    async def test_edgeql_migration_eq_function_06(self):\n        await self.migrate(r\"\"\"\n            function hello06(a: int64) -> str\n                using edgeql $$\n                    SELECT <str>a\n                $$;\n\n            type Base {\n                property foo -> int64 {\n                    # use the function in default value computation\n                    default := len(hello06(2) ++ hello06(123))\n                }\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT Base.foo;\"\"\",\n            {4},\n        )\n\n        # same parameters, different return type (array)\n        await self.migrate(r\"\"\"\n            function hello06(a: int64) -> array<int64>\n                using edgeql $$\n                    SELECT [a]\n                $$;\n\n            type Base {\n                property foo -> int64 {\n                    # use the function in default value computation\n                    default := len(hello06(2) ++ hello06(123))\n                }\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            INSERT Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT Base.foo;\"\"\",\n            {4, 2},\n        )\n\n    async def test_edgeql_migration_eq_function_07(self):\n        await self.migrate(r\"\"\"\n            function hello07(a: int64) -> str\n                using edgeql $$\n                    SELECT <str>a\n                $$;\n\n            type Base {\n                # use the function in computable value\n                property foo := len(hello07(2) ++ hello07(123))\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT Base.foo;\"\"\",\n            {4},\n        )\n\n        # same parameters, different return type (array)\n        await self.migrate(r\"\"\"\n            function hello07(a: int64) -> array<int64>\n                using edgeql $$\n                    SELECT [a]\n                $$;\n\n            type Base {\n                # use the function in computable value\n                property foo := len(hello07(2) ++ hello07(123))\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT Base.foo;\"\"\",\n            {2},\n        )\n\n    async def test_edgeql_migration_eq_function_08(self):\n        await self.migrate(r\"\"\"\n            function hello08(a: int64) -> str\n                using edgeql $$\n                    SELECT <str>a\n                $$;\n\n            # use the function in a alias directly\n            alias foo := len(hello08(2) ++ hello08(123));\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT foo;\"\"\",\n            {4},\n        )\n\n        # same parameters, different return type (array)\n        await self.migrate(r\"\"\"\n            function hello08(a: int64) -> array<int64>\n                using edgeql $$\n                    SELECT [a]\n                $$;\n\n            # use the function in a alias directly\n            alias foo := len(hello08(2) ++ hello08(123));\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT foo;\"\"\",\n            {2},\n        )\n\n    async def test_edgeql_migration_eq_function_09(self):\n        await self.migrate(r\"\"\"\n            function hello09(a: int64) -> str\n                using edgeql $$\n                    SELECT <str>a\n                $$;\n\n            type Base;\n\n            # use the function in a alias directly\n            alias BaseAlias := (\n                SELECT Base {\n                    foo := len(hello09(2) ++ hello09(123))\n                }\n            );\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias.foo;\"\"\",\n            {4},\n        )\n\n        # same parameters, different return type (array)\n        await self.migrate(r\"\"\"\n            function hello09(a: int64) -> array<int64>\n                using edgeql $$\n                    SELECT [a]\n                $$;\n\n            type Base;\n\n            # use the function in a alias directly\n            alias BaseAlias := (\n                SELECT Base {\n                    foo := len(hello09(2) ++ hello09(123))\n                }\n            );\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias.foo;\"\"\",\n            {2},\n        )\n\n    async def test_edgeql_migration_eq_function_10(self):\n        await self.migrate(r\"\"\"\n            function hello10(a: int64) -> str\n                using edgeql $$\n                    SELECT <str>a\n                $$;\n\n            type Base {\n                required property foo -> int64 {\n                    # use the function in a constraint expression\n                    constraint expression on (len(hello10(__subject__)) < 2)\n                }\n            }\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        with self.assertRaisesRegex(\n                edgedb.ConstraintViolationError,\n                r'invalid foo'):\n            async with self.con.transaction():\n                await self.con.execute(r\"\"\"\n                    INSERT Base {foo := 42};\n                \"\"\")\n\n        # same parameters, different return type (array)\n        await self.migrate(r\"\"\"\n            function hello10(a: int64) -> array<int64>\n                using edgeql $$\n                    SELECT [a]\n                $$;\n\n            type Base {\n                required property foo -> int64 {\n                    # use the function in a constraint expression\n                    constraint expression on (len(hello10(__subject__)) < 2)\n                }\n            }\n        \"\"\")\n\n        # no problem with the constraint now\n        await self.con.execute(r\"\"\"\n            INSERT Base {foo := 42};\n        \"\"\")\n\n    async def test_edgeql_migration_eq_function_11(self):\n        await self.migrate(r\"\"\"\n            function hello11(a: int64) -> str\n                using edgeql $$\n                    SELECT 'hello' ++ <str>a\n                $$\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello11(1);\"\"\",\n            ['hello1'],\n        )\n\n        await self.migrate(r\"\"\"\n            # replace the function with a new one by the same name\n            function hello11(a: str) -> str\n                using edgeql $$\n                    SELECT 'hello' ++ a\n                $$\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello11(' world');\"\"\",\n            ['hello world'],\n        )\n\n        # make sure that the old one is gone\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r'function \"hello11\\(arg0: std::int64\\)\" does not exist'):\n            await self.con.execute(\n                r\"\"\"SELECT hello11(1);\"\"\"\n            )\n\n    async def test_edgeql_migration_eq_function_12(self):\n        await self.migrate(r\"\"\"\n            function hello12(a: int64) -> str\n                using edgeql $$\n                    SELECT 'hello' ++ <str>a\n                $$;\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello12(1);\"\"\",\n            ['hello1'],\n        )\n\n        await self.migrate(r\"\"\"\n            function hello12(a: int64) -> str\n                using edgeql $$\n                    SELECT 'hello' ++ <str>a\n                $$;\n\n            # make the function polymorphic\n            function hello12(a: str) -> str\n                using edgeql $$\n                    SELECT 'hello' ++ a\n                $$;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello12(' world');\"\"\",\n            ['hello world'],\n        )\n\n        # make sure that the old one still works\n        await self.assert_query_result(\n            r\"\"\"SELECT hello12(1);\"\"\",\n            ['hello1'],\n        )\n\n    async def test_edgeql_migration_eq_function_13(self):\n        # this is the inverse of test_edgeql_migration_eq_function_12\n        await self.migrate(r\"\"\"\n            # start with a polymorphic function\n            function hello13(a: int64) -> str\n                using edgeql $$\n                    SELECT 'hello' ++ <str>a\n                $$;\n\n            function hello13(a: str) -> str\n                using edgeql $$\n                    SELECT 'hello' ++ a\n                $$;\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello13(' world');\"\"\",\n            ['hello world'],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT hello13(1);\"\"\",\n            ['hello1'],\n        )\n\n        await self.migrate(r\"\"\"\n            # remove one of the 2 versions\n            function hello13(a: int64) -> str\n                using edgeql $$\n                    SELECT 'hello' ++ <str>a\n                $$;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello13(1);\"\"\",\n            ['hello1'],\n        )\n\n        # make sure that the other one is gone\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r'function \"hello13\\(arg0: std::str\\)\" does not exist'):\n            await self.con.execute(\n                r\"\"\"SELECT hello13(' world');\"\"\"\n            )\n\n    async def test_edgeql_migration_eq_function_14(self):\n        await self.migrate(r\"\"\"\n            function hello14(a: str, b: str) -> str\n                using edgeql $$\n                    SELECT a ++ b\n                $$\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello14('hello', '14');\"\"\",\n            ['hello14'],\n        )\n\n        await self.migrate(r\"\"\"\n            # Replace the function with a new one by the same name,\n            # but working with arrays.\n            function hello14(a: array<str>, b: array<str>) -> array<str>\n                using edgeql $$\n                    SELECT a ++ b\n                $$\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello14(['hello'], ['14']);\"\"\",\n            [['hello', '14']],\n        )\n\n        # make sure that the old one is gone\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r'function \"hello14\\(arg0: std::str, arg1: std::str\\)\" '\n                r'does not exist'):\n            await self.assert_query_result(\n                r\"\"\"SELECT hello14('hello', '14');\"\"\",\n                ['hello14'],\n            )\n\n    async def test_edgeql_migration_eq_function_15(self):\n        await self.migrate(r\"\"\"\n            function hello15(a: str, b: str) -> str\n                using edgeql $$\n                    SELECT a ++ b\n                $$\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello15('hello', '15');\"\"\",\n            ['hello15'],\n        )\n\n        await self.migrate(r\"\"\"\n            # Replace the function with a new one by the same name,\n            # but working with arrays.\n            function hello15(a: tuple<str, str>) -> str\n                using edgeql $$\n                    SELECT a.0 ++ a.1\n                $$\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT hello15(('hello', '15'));\"\"\",\n            ['hello15'],\n        )\n\n        # make sure that the old one is gone\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r'function \"hello15\\(arg0: std::str, arg1: std::str\\)\" '\n                r'does not exist'):\n            await self.assert_query_result(\n                r\"\"\"SELECT hello15('hello', '15');\"\"\",\n                ['hello15'],\n            )\n\n    async def test_edgeql_migration_eq_function_16(self):\n        # Test prop default and function order of definition. The\n        # function happens to be shadowing a \"std\" function. We expect\n        # that the function `test::to_upper` will actually be used.\n        #\n        # See also `test_schema_get_migration_21`\n        await self.migrate(r\"\"\"\n            type Foo16 {\n                property name -> str {\n                    default := str_upper('some_name');\n                };\n            }\n\n            function str_upper(val: str) -> str {\n                using (SELECT '^^' ++ std::str_upper(val) ++ '^^');\n            }\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT str_upper('hello');\"\"\",\n            ['^^HELLO^^'],\n        )\n\n        await self.con.execute(\"\"\"\n            INSERT Foo16;\n        \"\"\")\n        await self.assert_query_result(\n            r\"\"\"SELECT Foo16.name;\"\"\",\n            ['^^SOME_NAME^^'],\n        )\n\n    async def test_edgeql_migration_enum_and_var_function_01(self):\n        # Create an enum and a variadic function that references it\n        # in the same migration. Issue #4213.\n        await self.migrate(r\"\"\"\n            scalar type E extending enum<a, b, c>;\n            function f(variadic e: E) -> bool using (true);\n        \"\"\")\n\n    async def test_edgeql_migration_eq_linkprops_01(self):\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link foo -> Child;\n            };\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                foo := (INSERT Child)\n            };\n        \"\"\")\n\n        # Migration adding a link property.\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link foo -> Child {\n                    property bar -> str\n                }\n            };\n        \"\"\")\n\n        # actually record a link property\n        await self.con.execute(r\"\"\"\n            UPDATE\n                Base\n            SET {\n                foo := .foo {\n                    @bar := 'lp01'\n                }\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo: { @bar }\n                };\n            \"\"\",\n            [{'foo': {'@bar': 'lp01'}}],\n        )\n\n    async def test_edgeql_migration_eq_linkprops_02(self):\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link foo -> Child {\n                    property bar -> str\n                }\n            };\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {foo := (INSERT Child)};\n            UPDATE Base\n            SET {\n                foo := .foo { @bar := 'lp02' },\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link foo -> Child {\n                    # change the link property name\n                    property bar2 -> str\n                }\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo: { @bar2 }\n                };\n            \"\"\",\n            [{'foo': {'@bar2': 'lp02'}}],\n        )\n\n    async def test_edgeql_migration_eq_linkprops_03(self):\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link foo -> Child {\n                    property bar -> int64\n                }\n            };\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {foo := (INSERT Child)};\n            UPDATE Base\n            SET {\n                foo := .foo { @bar := 3 },\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link foo -> Child {\n                    # change the link property type\n                    property bar -> int32\n                }\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo: { @bar }\n                };\n            \"\"\",\n            [{'foo': {'@bar': 3}}],\n        )\n\n    async def test_edgeql_migration_eq_linkprops_04(self):\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link foo -> Child {\n                    property bar -> str\n                }\n            };\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {foo := (INSERT Child)};\n            UPDATE Base\n            SET {\n                foo := .foo { @bar := 'lp04' },\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                # change the link cardinality\n                multi link foo -> Child {\n                    property bar -> str\n                }\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo: { @bar }\n                };\n            \"\"\",\n            [{'foo': [{'@bar': 'lp04'}]}],\n        )\n\n    async def test_edgeql_migration_eq_linkprops_05(self):\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                multi link foo -> Child {\n                    property bar -> str\n                }\n            };\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {foo := (INSERT Child)};\n            UPDATE Base\n            SET {\n                foo := .foo { @bar := 'lp05' },\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                # change the link cardinality\n                link foo -> Child {\n                    property bar -> str\n                }\n            };\n        \"\"\", user_input=[\n            'SELECT .foo LIMIT 1'\n        ])\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    foo: { @bar }\n                };\n            \"\"\",\n            [{'foo': {'@bar': 'lp05'}}],\n        )\n\n    async def test_edgeql_migration_eq_linkprops_06(self):\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link child -> Child {\n                    property foo -> str;\n                }\n            };\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {child := (INSERT Child)};\n            UPDATE Base\n            SET {\n                child := .child {\n                    @foo := 'lp06',\n                },\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link child -> Child {\n                    property foo -> str;\n                    # add another link prop\n                    property bar -> int64;\n                }\n            };\n        \"\"\")\n        # update the existing data with a new link prop 'bar'\n        await self.con.execute(r\"\"\"\n            UPDATE Base\n            SET {\n                child := .child {\n                    @bar := 111,\n                    @foo := 'lp06',\n                },\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    child: {\n                        @foo,\n                        @bar\n                    }\n                };\n            \"\"\",\n            [{\n                'child': {\n                    '@foo': 'lp06',\n                    '@bar': 111\n                }\n            }],\n        )\n\n    async def test_edgeql_migration_eq_linkprops_07(self):\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link child -> Child\n            };\n\n            type Derived extending Base {\n                overloaded link child -> Child {\n                    property foo -> str\n                }\n            };\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Derived {child := (INSERT Child)};\n            UPDATE Derived\n            SET {\n                child := .child {\n                    @foo := 'lp07',\n                },\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                # move the link property earlier in the inheritance tree\n                link child -> Child {\n                    property foo -> str\n                }\n            };\n\n            type Derived extending Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    child: {\n                        @foo,\n                    }\n                };\n            \"\"\",\n            [{\n                'child': {\n                    '@foo': 'lp07',\n                }\n            }],\n        )\n\n    async def test_edgeql_migration_eq_linkprops_08(self):\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link child -> Child {\n                    property foo -> str\n                }\n            };\n\n            type Derived extending Base;\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Derived {child := (INSERT Child)};\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            UPDATE Derived\n            SET {\n                child := .child {\n                    @foo := 'lp08',\n                },\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link child -> Child\n            };\n\n            type Derived extending Base {\n                overloaded link child -> Child {\n                    # move the link property later in the inheritance tree\n                    property foo -> str\n                }\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Derived {\n                    children := count(.child)\n                };\n            \"\"\",\n            [{\n                'children': 1,\n            }],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Derived {\n                    child: {\n                        @foo,\n                    }\n                };\n            \"\"\",\n            [{\n                'child': {\n                    '@foo': 'lp08',\n                }\n            }],\n        )\n\n    async def test_edgeql_migration_eq_linkprops_09(self):\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link child -> Child\n            };\n\n            type Derived extending Base {\n                overloaded link child -> Child {\n                    property foo -> str\n                }\n            };\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Derived {child := (INSERT Child)};\n            UPDATE Derived\n            SET {\n                child := .child {\n                    @foo := 'lp09',\n                },\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            # factor out link property all the way to an abstract link\n            abstract link base_child {\n                property foo -> str;\n            }\n\n            type Base {\n                link child extending base_child -> Child;\n            };\n\n            type Derived extending Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Base {\n                    child: {\n                        @foo,\n                    }\n                };\n            \"\"\",\n            [{\n                'child': {\n                    '@foo': 'lp09',\n                }\n            }],\n        )\n\n    async def test_edgeql_migration_eq_linkprops_10(self):\n        # reverse of the test_edgeql_migration_eq_linkprops_09 refactoring\n        await self.migrate(r\"\"\"\n            type Child;\n\n            abstract link base_child {\n                property foo -> str;\n            }\n\n            type Base {\n                link child extending base_child -> Child;\n            };\n\n            type Derived extending Base;\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Derived {child := (INSERT Child)};\n            UPDATE Derived\n            SET {\n                child := .child {\n                    @foo := 'lp10',\n                },\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Child;\n\n            type Base {\n                link child -> Child\n            };\n\n            type Derived extending Base {\n                overloaded link child -> Child {\n                    # move the link property later in the inheritance tree\n                    property foo -> str\n                }\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Derived {\n                    child: {\n                        @foo,\n                    }\n                };\n            \"\"\",\n            [{\n                'child': {\n                    '@foo': 'lp10',\n                }\n            }],\n        )\n\n        await self.migrate(\"\")\n\n    async def test_edgeql_migration_eq_linkprops_11(self):\n        # merging a link with the same properties\n        await self.migrate(r\"\"\"\n            type Thing;\n\n            type Owner {\n                link item -> Thing {\n                    property foo -> str;\n                }\n            };\n\n            type Renter {\n                link item -> Thing {\n                    property foo -> str;\n                }\n            };\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Owner {item := (INSERT Thing)};\n            UPDATE Owner\n            SET {\n                item := .item {\n                    @foo := 'owner_lp11',\n                },\n            };\n\n            INSERT Renter {item := (INSERT Thing)};\n            UPDATE Renter\n            SET {\n                item := .item {\n                    @foo := 'renter_lp11',\n                },\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Thing;\n\n            type Base {\n                link item -> Thing {\n                    property foo -> str;\n                }\n            };\n\n            type Owner extending Base;\n\n            type Renter extending Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Owner {\n                    item: {\n                        @foo,\n                    }\n                };\n            \"\"\",\n            [{\n                'item': {\n                    '@foo': 'owner_lp11',\n                }\n            }],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Renter {\n                    item: {\n                        @foo,\n                    }\n                };\n            \"\"\",\n            [{\n                'item': {\n                    '@foo': 'renter_lp11',\n                }\n            }],\n        )\n\n    async def test_edgeql_migration_eq_linkprops_12(self):\n        # merging a link with different properties\n        await self.migrate(r\"\"\"\n            type Thing;\n\n            type Owner {\n                link item -> Thing {\n                    property foo -> str;\n                }\n            };\n\n            type Renter {\n                link item -> Thing {\n                    property bar -> str;\n                }\n            };\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Owner {item := (INSERT Thing)};\n            UPDATE Owner\n            SET {\n                item := .item {\n                    @foo := 'owner_lp11',\n                },\n            };\n\n            INSERT Renter {item := (INSERT Thing)};\n            UPDATE Renter\n            SET {\n                item := .item {\n                    @bar := 'renter_lp11',\n                },\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Thing;\n\n            type Base {\n                link item -> Thing {\n                    property foo -> str;\n                    property bar -> str;\n                }\n            };\n\n            type Owner extending Base;\n\n            type Renter extending Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Owner {\n                    item: {\n                        @foo,\n                        @bar,\n                    }\n                };\n            \"\"\",\n            [{\n                'item': {\n                    '@foo': 'owner_lp11',\n                    '@bar': None,\n                }\n            }],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT Renter {\n                    item: {\n                        @foo,\n                        @bar,\n                    }\n                };\n            \"\"\",\n            [{\n                'item': {\n                    '@foo': None,\n                    '@bar': 'renter_lp11',\n                }\n            }],\n        )\n\n    async def test_edgeql_migration_eq_annotation_01(self):\n        await self.migrate(r\"\"\"\n            type Base;\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    annotations: {\n                        name,\n                        @value\n                    } ORDER BY .name\n                }\n                FILTER .name LIKE 'test::%'\n                ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'annotations': [],\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                # add a title annotation\n                annotation title := 'Base description 01'\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    annotations: {\n                        name,\n                        @value\n                    } ORDER BY .name\n                }\n                FILTER .name LIKE 'test::%'\n                ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'annotations': [{\n                    'name': 'std::title',\n                    '@value': 'Base description 01'\n                }],\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            # add inheritable and non-inheritable annotations\n            abstract annotation foo_anno;\n            abstract inheritable annotation bar_anno;\n\n            type Base {\n                annotation title := 'Base description 01';\n                annotation foo_anno := 'Base foo_anno 01';\n                annotation bar_anno := 'Base bar_anno 01';\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    annotations: {\n                        name,\n                        @value\n                    } ORDER BY .name\n                }\n                FILTER .name LIKE 'test::%'\n                ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'annotations': [{\n                    'name': 'std::title',\n                    '@value': 'Base description 01'\n                }, {\n                    'name': 'test::bar_anno',\n                    '@value': 'Base bar_anno 01'\n                }, {\n                    'name': 'test::foo_anno',\n                    '@value': 'Base foo_anno 01'\n                }],\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            abstract annotation foo_anno;\n            abstract inheritable annotation bar_anno;\n\n            type Base {\n                annotation title := 'Base description 01';\n                annotation foo_anno := 'Base foo_anno 01';\n                annotation bar_anno := 'Base bar_anno 01';\n            }\n\n            # extend Base\n            type Derived extending Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    annotations: {\n                        name,\n                        @value\n                    } ORDER BY .name\n                }\n                FILTER .name LIKE 'test::%'\n                ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'annotations': [{\n                    'name': 'std::title',\n                    '@value': 'Base description 01'\n                }, {\n                    'name': 'test::bar_anno',\n                    '@value': 'Base bar_anno 01'\n                }, {\n                    'name': 'test::foo_anno',\n                    '@value': 'Base foo_anno 01'\n                }],\n            }, {\n                'name': 'test::Derived',\n                'annotations': [{\n                    'name': 'test::bar_anno',\n                    '@value': 'Base bar_anno 01'\n                }],\n            }],\n        )\n\n    async def test_edgeql_migration_eq_annotation_02(self):\n        await self.migrate(r\"\"\"\n            type Base;\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    annotations: {\n                        name,\n                        @value\n                    } ORDER BY .name\n                }\n                FILTER .name LIKE 'test::%'\n                ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'annotations': [],\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            abstract annotation foo_anno;\n\n            type Base {\n                annotation title := 'Base description 02';\n                annotation foo_anno := 'Base foo_anno 02';\n            }\n\n            type Derived extending Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    annotations: {\n                        name,\n                        @value\n                    } ORDER BY .name\n                }\n                FILTER .name LIKE 'test::%'\n                ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'annotations': [{\n                    'name': 'std::title',\n                    '@value': 'Base description 02'\n                }, {\n                    'name': 'test::foo_anno',\n                    '@value': 'Base foo_anno 02'\n                }],\n            }, {\n                'name': 'test::Derived',\n                # annotation not inherited\n                'annotations': [],\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            # remove foo_anno\n            type Base {\n                annotation title := 'Base description 02';\n            }\n\n            type Derived extending Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    annotations: {\n                        name,\n                        @value\n                    } ORDER BY .name\n                }\n                FILTER .name LIKE 'test::%'\n                ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'annotations': [{\n                    'name': 'std::title',\n                    '@value': 'Base description 02'\n                }],\n            }, {\n                'name': 'test::Derived',\n                'annotations': [],\n            }],\n        )\n\n    async def test_edgeql_migration_eq_annotation_03(self):\n        await self.migrate(r\"\"\"\n            type Base;\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    annotations: {\n                        name,\n                        @value\n                    } ORDER BY .name\n                }\n                FILTER .name LIKE 'test::%'\n                ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'annotations': [],\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            abstract inheritable annotation bar_anno;\n\n            type Base {\n                annotation title := 'Base description 03';\n                annotation bar_anno := 'Base bar_anno 03';\n            }\n\n            type Derived extending Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    annotations: {\n                        name,\n                        @value\n                    } ORDER BY .name\n                }\n                FILTER .name LIKE 'test::%'\n                ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'annotations': [{\n                    'name': 'std::title',\n                    '@value': 'Base description 03'\n                }, {\n                    'name': 'test::bar_anno',\n                    '@value': 'Base bar_anno 03'\n                }],\n            }, {\n                'name': 'test::Derived',\n                # annotation inherited\n                'annotations': [{\n                    'name': 'test::bar_anno',\n                    '@value': 'Base bar_anno 03'\n                }],\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            # remove bar_anno\n            type Base {\n                annotation title := 'Base description 03';\n            }\n\n            type Derived extending Base;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    annotations: {\n                        name,\n                        @value\n                    } ORDER BY .name\n                }\n                FILTER .name LIKE 'test::%'\n                ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'annotations': [{\n                    'name': 'std::title',\n                    '@value': 'Base description 03'\n                }],\n            }, {\n                'name': 'test::Derived',\n                'annotations': [],\n            }],\n        )\n\n    async def test_edgeql_migration_eq_annotation_04(self):\n        # Test migration of annotation value and nothing else.\n        await self.migrate(r\"\"\"\n            abstract annotation description;\n\n            type Base {\n                annotation description := \"1\";\n            }\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    annotations: {\n                        name,\n                        @value\n                    } ORDER BY .name\n                }\n                FILTER .name LIKE 'test::%'\n                ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'annotations': [{\n                    'name': 'test::description',\n                    '@value': '1',\n                }],\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            abstract annotation description;\n\n            type Base {\n                annotation description := \"2\";\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    annotations: {\n                        name,\n                        @value\n                    } ORDER BY .name\n                }\n                FILTER .name LIKE 'test::%'\n                ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'annotations': [{\n                    'name': 'test::description',\n                    '@value': '2',\n                }],\n            }],\n        )\n\n    async def test_edgeql_migration_describe_annot_01(self):\n        await self.migrate('''\n            abstract annotation foo;\n\n            type Base {\n                annotation foo := \"1\";\n            };\n        ''')\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    abstract annotation bar;\n\n                    type Base {\n                        annotation bar := \"1\";\n                    };\n                }\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text': (\n                        'ALTER ABSTRACT ANNOTATION test::foo '\n                        'RENAME TO test::bar;'\n                    )\n                }],\n            },\n        })\n\n    async def test_edgeql_migration_eq_index_01(self):\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str;\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    indexes: {\n                        expr\n                    }\n                }\n                FILTER .name = 'test::Base';\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'indexes': [],\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str;\n                # an index\n                index on (.name);\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    indexes: {\n                        expr\n                    }\n                }\n                FILTER .name = 'test::Base';\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'indexes': [{\n                    'expr': '.name'\n                }]\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                # rename the indexed property\n                property title -> str;\n                index on (.title);\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    indexes: {\n                        expr\n                    }\n                }\n                FILTER .name = 'test::Base';\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'indexes': [{\n                    'expr': '.title'\n                }]\n            }],\n        )\n\n    async def test_edgeql_migration_eq_index_02(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str;\n                index on (.name);\n            }\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    indexes: {\n                        expr\n                    }\n                }\n                FILTER .name = 'test::Base';\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'indexes': [{\n                    'expr': '.name'\n                }]\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str;\n                # remove the index\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    indexes: {\n                        expr\n                    }\n                }\n                FILTER .name = 'test::Base';\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'indexes': [],\n            }],\n        )\n\n    async def test_edgeql_migration_eq_index_03(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> int64;\n            }\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    indexes: {\n                        expr\n                    }\n                }\n                FILTER .name = 'test::Base';\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'indexes': [],\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> int64;\n                # an index\n                index on (.name);\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    indexes: {\n                        expr\n                    }\n                }\n                FILTER .name = 'test::Base';\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'indexes': [{\n                    'expr': '.name'\n                }]\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                # change the indexed property type\n                property name -> int32;\n                index on (.name);\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    indexes: {\n                        expr\n                    }\n                }\n                FILTER .name = 'test::Base';\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'indexes': [{\n                    'expr': '.name'\n                }]\n            }],\n        )\n\n    async def test_edgeql_migration_eq_index_04(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property first_name -> str;\n                property last_name -> str;\n                property name := .first_name ++ ' ' ++ .last_name;\n            }\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    indexes: {\n                        expr\n                    }\n                }\n                FILTER .name = 'test::Base';\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'indexes': [],\n            }],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property first_name -> str;\n                property last_name -> str;\n                property name := .first_name ++ ' ' ++ .last_name;\n                # an index on a computable\n                index on (.name);\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT ObjectType {\n                    name,\n                    indexes: {\n                        expr\n                    }\n                }\n                FILTER .name = 'test::Base';\n            \"\"\",\n            [{\n                'name': 'test::Base',\n                'indexes': [{\n                    'expr': '.name'\n                }]\n            }],\n        )\n\n    async def test_edgeql_migration_eq_index_05(self):\n        await self.migrate('''\n            abstract type Named {\n                index fts::index on (\n                    std::fts::with_options(\n                        .name, language := std::fts::Language.eng));\n                required property name: std::str;\n            };\n        ''')\n\n        await self.start_migration('''\n            abstract type Named {\n                index std::fts::index on (\n                    std::fts::with_options(\n                        .name, language := std::fts::Language.eng));\n                required property name: std::str;\n            };\n        ''')\n\n        # no changes\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': True,\n        })\n\n    async def test_edgeql_migration_eq_collections_01(self):\n        await self.migrate(r\"\"\"\n            type Base;\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> array<float32>;\n            }\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            UPDATE Base\n            SET {\n                foo := [1.2, 4.5]\n            };\n        \"\"\")\n        await self.assert_query_result(\n            r\"\"\"SELECT Base.foo;\"\"\",\n            [[1.2, 4.5]],\n        )\n\n    async def test_edgeql_migration_eq_collections_02(self):\n        await self.migrate(r\"\"\"\n            type Base;\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> tuple<str, int32>;\n            }\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            UPDATE Base\n            SET {\n                foo := ('hello', 42)\n            };\n        \"\"\")\n        await self.assert_query_result(\n            r\"\"\"SELECT Base.foo;\"\"\",\n            [['hello', 42]],\n        )\n\n    async def test_edgeql_migration_eq_collections_03(self):\n        await self.migrate(r\"\"\"\n            type Base;\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Base {\n                # nested collection\n                property foo -> tuple<str, int32, array<float32>>;\n            }\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            UPDATE Base\n            SET {\n                foo := ('test', 42, [1.2, 4.5])\n            };\n        \"\"\")\n        await self.assert_query_result(\n            r\"\"\"SELECT Base.foo;\"\"\",\n            [['test', 42, [1.2, 4.5]]],\n        )\n\n    async def test_edgeql_migration_eq_collections_04(self):\n        await self.migrate(r\"\"\"\n            type Base;\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> tuple<a: str, b: int32>;\n            }\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            UPDATE Base\n            SET {\n                foo := (a := 'hello', b := 42)\n            };\n        \"\"\")\n        await self.assert_query_result(\n            r\"\"\"SELECT Base.foo;\"\"\",\n            [{'a': 'hello', 'b': 42}],\n        )\n\n    async def test_edgeql_migration_eq_collections_06(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> array<int32>;\n            }\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                foo := [1, 2]\n            }\n        \"\"\")\n        # sanity check\n        await self.assert_query_result(\n            r\"\"\"SELECT Base.foo;\"\"\",\n            [[1, 2]],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> array<float64>;\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT Base.foo;\"\"\",\n            [[1.0, 2.0]],\n        )\n\n    async def test_edgeql_migration_eq_collections_07(self):\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n        \"\"\")\n        await self.migrate(r\"\"\"\n            type Base {\n                # convert property type to tuple\n                property bar -> array<str>;\n                property foo -> tuple<str, int32>;\n            }\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            INSERT Base {\n                bar := ['123'],\n                foo := ('test', <int32>7),\n            }\n        \"\"\")\n\n        await self.migrate(\n            r\"\"\"\n                type Base {\n                    property bar -> array<int64>;\n                    property foo -> tuple<str, int32, int32>;\n                }\n            \"\"\",\n            user_input=[\n                \"<array<int64>>.bar\",\n                \"(.foo.0, .foo.1, 0)\",\n            ]\n        )\n\n        await self.assert_query_result(\n            r\"\"\"SELECT Base.bar;\"\"\",\n            [[123]],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"SELECT Base.foo;\"\"\",\n            [['test', 7, 0]],\n        )\n\n    async def test_edgeql_migration_eq_collections_08(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> tuple<int32, int32>;\n            }\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                foo := (0, 8)\n            }\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Base {\n                # convert property type to a tuple with different (but\n                # cast-compatible) element types\n                property foo -> tuple<int64, int32>;\n            }\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT Base.foo;\"\"\",\n            [[0, 8]],\n        )\n\n    async def test_edgeql_migration_eq_collections_09(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> tuple<str, int32>;\n            }\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                foo := ('test', 9)\n            }\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Base {\n                # convert property type from unnamed to named tuple\n                property foo -> tuple<a: str, b: int32>;\n            }\n        \"\"\")\n\n        # In theory, since under normal circumstances we can cast an\n        # unnamed tuple into named, it's reasonable to expect this\n        # migration to preserve data here.\n        await self.assert_query_result(\n            r\"\"\"SELECT Base.foo;\"\"\",\n            [{'a': 'test', 'b': 9}],\n        )\n\n    async def test_edgeql_migration_eq_collections_13(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> float32;\n            };\n\n            # alias that don't have arrays\n            alias BaseAlias := Base { bar := Base.foo };\n            alias CollAlias := Base.foo;\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                foo := 13.5,\n            }\n        \"\"\")\n\n        # make sure that the alias initialized correctly\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias{bar};\"\"\",\n            [{'bar': 13.5}],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT CollAlias;\"\"\",\n            [13.5],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> float32;\n            };\n\n            # \"same\" alias that now have arrays\n            alias BaseAlias := Base { bar := [Base.foo] };\n            alias CollAlias := [Base.foo];\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias{bar};\"\"\",\n            [{'bar': [13.5]}],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT CollAlias;\"\"\",\n            [[13.5]],\n        )\n\n    async def test_edgeql_migration_eq_collections_14(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str;\n                property foo -> float32;\n            };\n\n            # alias that don't have tuples\n            alias BaseAlias := Base { bar := Base.foo };\n            alias CollAlias := Base.foo;\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                name := 'coll_14',\n                foo := 14.5,\n            }\n        \"\"\")\n\n        # make sure that the alias initialized correctly\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias{bar};\"\"\",\n            [{'bar': 14.5}],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT CollAlias;\"\"\",\n            [14.5],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str;\n                property foo -> float32;\n            };\n\n            # \"same\" alias that now have tuples\n            alias BaseAlias := Base { bar := (Base.name, Base.foo) };\n            alias CollAlias := (Base.name, Base.foo);\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias{bar};\"\"\",\n            [{'bar': ['coll_14', 14.5]}],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT CollAlias;\"\"\",\n            [['coll_14', 14.5]],\n        )\n\n    async def test_edgeql_migration_eq_collections_15(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str;\n                property number -> int32;\n                property foo -> float32;\n            };\n\n            # alias that don't have nested collections\n            alias BaseAlias := Base { bar := Base.foo };\n            alias CollAlias := Base.foo;\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                name := 'coll_15',\n                number := 15,\n                foo := 15.5,\n            }\n        \"\"\")\n\n        # make sure that the alias initialized correctly\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias{bar};\"\"\",\n            [{'bar': 15.5}],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT CollAlias;\"\"\",\n            [15.5],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str;\n                property number -> int32;\n                property foo -> float32;\n            };\n\n            # \"same\" alias that now have nested collections\n            alias BaseAlias := Base {\n                bar := (Base.name, Base.number, [Base.foo])\n            };\n            alias CollAlias := (Base.name, Base.number, [Base.foo]);\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias{bar};\"\"\",\n            [{'bar': ['coll_15', 15, [15.5]]}],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT CollAlias;\"\"\",\n            [['coll_15', 15, [15.5]]],\n        )\n\n    async def test_edgeql_migration_eq_collections_16(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str;\n                property foo -> float32;\n            };\n\n            # alias that don't have named tuples\n            alias BaseAlias := Base { bar := Base.foo };\n            alias CollAlias := Base.foo;\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                name := 'coll_16',\n                foo := 16.5,\n            }\n        \"\"\")\n\n        # make sure that the alias initialized correctly\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias{bar};\"\"\",\n            [{'bar': 16.5}],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT CollAlias;\"\"\",\n            [16.5],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str;\n                property foo -> float32;\n            };\n\n            # \"same\" alias that now have named tuples\n            alias BaseAlias := Base {\n                bar := (a := Base.name, b := Base.foo)\n            };\n            alias CollAlias := (a := Base.name, b := Base.foo);\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias{bar};\"\"\",\n            [{'bar': {'a': 'coll_16', 'b': 16.5}}],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT CollAlias;\"\"\",\n            [{'a': 'coll_16', 'b': 16.5}],\n        )\n\n    async def test_edgeql_migration_eq_collections_17(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> float32;\n                property bar -> int32;\n            };\n\n            # alias with array<int32>\n            alias BaseAlias := Base { data := [Base.bar] };\n            alias CollAlias := [Base.bar];\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                foo := 17.5,\n                bar := 17,\n            }\n        \"\"\")\n\n        # make sure that the alias initialized correctly\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias{data};\"\"\",\n            [{'data': [17]}],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT CollAlias;\"\"\",\n            [[17]],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property foo -> float32;\n                property bar -> int32;\n            };\n\n            # alias with array<float32>\n            alias BaseAlias := Base { data := [Base.foo] };\n            alias CollAlias := [Base.foo];\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias{data};\"\"\",\n            [{'data': [17.5]}],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT CollAlias;\"\"\",\n            [[17.5]],\n        )\n\n    async def test_edgeql_migration_eq_collections_18(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str;\n                property number -> int32;\n                property foo -> float32;\n            };\n\n            # alias with tuple<str, int32>\n            alias BaseAlias := Base {\n                data := (Base.name, Base.number)\n            };\n            alias CollAlias := (Base.name, Base.number);\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                name := 'coll_18',\n                number := 18,\n                foo := 18.5,\n            }\n        \"\"\")\n\n        # make sure that the alias initialized correctly\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias{data};\"\"\",\n            [{'data': ['coll_18', 18]}],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT CollAlias;\"\"\",\n            [['coll_18', 18]],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str;\n                property number -> int32;\n                property foo -> float32;\n            };\n\n            # alias with tuple<str, int32, float32>\n            alias BaseAlias := Base {\n                data := (Base.name, Base.number, Base.foo)\n            };\n            alias CollAlias := (Base.name, Base.number, Base.foo);\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias{data};\"\"\",\n            [{'data': ['coll_18', 18, 18.5]}],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT CollAlias;\"\"\",\n            [['coll_18', 18, 18.5]],\n        )\n\n    async def test_edgeql_migration_eq_collections_20(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str;\n                property number -> int32;\n                property foo -> float32;\n            };\n\n            # alias with tuple<str, int32>\n            alias BaseAlias := Base {\n                data := (Base.name, Base.number)\n            };\n            alias CollAlias := (Base.name, Base.number);\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                name := 'test20',\n                number := 20,\n                foo := 123.5,\n            }\n        \"\"\")\n\n        # make sure that the alias initialized correctly\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias{data};\"\"\",\n            [{'data': ['test20', 20]}],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT CollAlias;\"\"\",\n            [['test20', 20]],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str;\n                property number -> int32;\n                property foo -> float32;\n            };\n\n            # alias with tuple<str, float32>\n            alias BaseAlias := Base {\n                data := (Base.name, Base.foo)\n            };\n            alias CollAlias := (Base.name, Base.foo);\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias {data};\"\"\",\n            [{'data': ['test20', 123.5]}],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT CollAlias;\"\"\",\n            [['test20', 123.5]],\n        )\n\n    async def test_edgeql_migration_eq_collections_21(self):\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str;\n                property foo -> float32;\n            };\n\n            # alias with tuple<str, float32>\n            alias BaseAlias := Base {\n                data := (Base.name, Base.foo)\n            };\n            alias CollAlias := (Base.name, Base.foo);\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            SET MODULE test;\n\n            INSERT Base {\n                name := 'coll_21',\n                foo := 21.5,\n            }\n        \"\"\")\n\n        # make sure that the alias initialized correctly\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias{data};\"\"\",\n            [{'data': ['coll_21', 21.5]}],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT CollAlias;\"\"\",\n            [['coll_21', 21.5]],\n        )\n\n        await self.migrate(r\"\"\"\n            type Base {\n                property name -> str;\n                property foo -> float32;\n            };\n\n            # alias with named tuple<a: str, b: float32>\n            alias BaseAlias := Base {\n                data := (a := Base.name, b := Base.foo)\n            };\n            alias CollAlias := (a := Base.name, b := Base.foo);\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"SELECT BaseAlias{data};\"\"\",\n            [{'data': {'a': 'coll_21', 'b': 21.5}}],\n        )\n        await self.assert_query_result(\n            r\"\"\"SELECT CollAlias;\"\"\",\n            [{'a': 'coll_21', 'b': 21.5}],\n        )\n\n    async def test_edgeql_migration_eq_drop_module(self):\n        await self.migrate(r\"\"\"\n            type Base;\n        \"\"\", module='test')\n\n        await self.migrate(r\"\"\"\n            scalar type foo extending std::str;\n        \"\"\", module='newtest')\n\n        await self.assert_query_result(\n            'SELECT (SELECT schema::Module FILTER .name LIKE \"%test\").name;',\n            {'newtest', 'std::_test'},\n        )\n\n    async def test_edgeql_migration_inherited_optionality_01(self):\n        await self.migrate(r\"\"\"\n            type User;\n\n            type Message {\n                required link author -> User;\n                required property body -> str;\n            };\n        \"\"\")\n\n        await self.start_migration(r\"\"\"\n            type User;\n\n            type BaseMessage {\n                required link author -> User;\n                required property body -> str;\n            }\n\n            type Message extending BaseMessage;\n        \"\"\")\n\n        await self.fast_forward_describe_migration()\n\n    async def test_edgeql_migration_rename_type_02(self):\n        await self.migrate(r\"\"\"\n            type Note {\n                property note -> str;\n            }\n            type Subtype extending Note;\n            type Link {\n                link a -> Note;\n            }\n            type Uses {\n                required property x -> str {\n                    default := (SELECT Note.note LIMIT 1)\n                }\n            };\n            type ComputeLink {\n                property foo -> str;\n                multi link x := (\n                    SELECT Note FILTER Note.note = ComputeLink.foo);\n            };\n            alias Alias := Note;\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Remark {\n                property note -> str;\n            }\n            type Subtype extending Remark;\n            type Link {\n                link a -> Remark;\n            }\n            type Uses {\n                required property x -> str {\n                    default := (SELECT Remark.note LIMIT 1)\n                }\n            };\n            type ComputeLink {\n                property foo -> str;\n                multi link x := (\n                    SELECT Remark FILTER Remark.note = ComputeLink.foo);\n            };\n            alias Alias := Remark;\n        \"\"\")\n\n        await self.migrate(\"\")\n\n    async def test_edgeql_migration_rename_type_03(self):\n        await self.migrate(r\"\"\"\n            type Note {\n                property note -> str;\n            }\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Remark {\n                property note -> str;\n            }\n            type Subtype extending Remark;\n            type Link {\n                link a -> Remark;\n            }\n            type Uses {\n                required property x -> str {\n                    default := (SELECT Remark.note LIMIT 1)\n                }\n            };\n            type ComputeLink {\n                property foo -> str;\n                multi link x := (\n                    SELECT Remark FILTER Remark.note = ComputeLink.foo);\n            };\n            alias Alias := Remark;\n        \"\"\")\n\n        await self.migrate(\"\")\n\n    async def test_edgeql_migration_annotation_05(self):\n        await self.migrate(r\"\"\"\n            abstract inheritable annotation my_anno;\n\n            type Base {\n                property my_prop -> str {\n                    annotation my_anno := 'Base my_anno 05';\n                }\n            }\n\n            type Derived extending Base {\n                overloaded property my_prop -> str {\n                    annotation my_anno := 'Derived my_anno 05';\n                }\n            }\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            # rename annotated & inherited property\n            abstract inheritable annotation my_anno;\n\n            type Base {\n                property renamed_prop -> str {\n                    annotation my_anno := 'Base my_anno 05';\n                }\n            }\n\n            type Derived extending Base {\n                overloaded property renamed_prop -> str {\n                    annotation my_anno := 'Derived my_anno 05';\n                }\n            }\n        \"\"\")\n\n        await self.migrate(\"\")\n\n    async def test_edgeql_migration_reset_optional_01(self):\n        await self.migrate(r'''\n            abstract type Person {\n                required property name -> str;\n            }\n\n            type PC extending Person;\n        ''')\n\n        await self.migrate(r'''\n            abstract type Person {\n                property name -> str;\n            }\n\n            type PC extending Person;\n        ''')\n\n        await self.migrate(r'''\n            abstract type Person {\n                required property name -> str;\n            }\n\n            type PC extending Person;\n        ''', user_input=['\"\"'])\n\n    async def test_edgeql_migration_reset_optional_02(self):\n        await self.migrate(r'''\n            abstract type Person {\n                required property name -> str;\n            }\n\n            type PC extending Person {\n                overloaded required property name -> str;\n            }\n        ''')\n\n        await self.migrate(r'''\n            abstract type Person {\n                property name -> str;\n            }\n\n            type PC extending Person {\n                overloaded property name -> str;\n            }\n        ''')\n\n    async def test_edgeql_migration_reset_optional_03(self):\n        await self.migrate(r'''\n            abstract type Person {\n                required property name -> str;\n            }\n\n            type PC extending Person {\n                overloaded required property name -> str;\n            }\n        ''')\n\n        await self.migrate(r'''\n            abstract type Person {\n                optional property name -> str;\n            }\n\n            type PC extending Person {\n                overloaded optional property name -> str;\n            }\n        ''')\n\n    @test.xerror('''\n        This fails because we try to set the parent as required while the\n        child is still optional.\n\n        For this to work, we need to process the *child* first,\n        but in order for the reverse case above to work we need to\n        process the *parent* first.\n\n        I don't know if there is any way to fix this sort of thing without\n        exposing this kind of semantic understanding to ordering.\n\n        Maybe suppressing some kinds of intermediate-state errors during\n        migrations would be OK?\n    ''')\n    async def test_edgeql_migration_reset_optional_04(self):\n        await self.migrate(r'''\n            abstract type Person {\n                optional property name -> str;\n            }\n\n            type PC extending Person {\n                overloaded optional property name -> str;\n            }\n        ''')\n\n        await self.migrate(r'''\n            abstract type Person {\n                required property name -> str;\n            }\n\n            type PC extending Person {\n                overloaded required property name -> str;\n            }\n        ''', user_input=[\"''\", \"''\"])\n\n    async def test_edgeql_migration_invalid_scalar_01(self):\n        with self.assertRaisesRegex(\n                edgedb.SchemaError,\n                r\"may not have more than one concrete base type\"):\n            await self.con.execute(r\"\"\"\n                START MIGRATION TO {\n                    abstract scalar type test::lol extending str;\n                    scalar type test::myint extending int64, test::lol;\n                };\n                POPULATE MIGRATION;\n            \"\"\")\n\n    async def test_edgeql_migration_inherited_default_01(self):\n        await self.migrate(r\"\"\"\n            abstract type Foo {\n                multi link link -> Obj {\n                    default := ( select Obj filter .name = 'X' )\n                };\n            }\n\n            type Bar extending Foo {}\n\n            type Obj {\n                required property name -> str {\n                    constraint exclusive;\n                }\n            }\n        \"\"\")\n\n    async def test_edgeql_migration_inherited_default_02(self):\n        await self.migrate(r\"\"\"\n            abstract type Foo {\n                multi link link -> Obj {\n                };\n            }\n\n            type Bar extending Foo {}\n\n            type Obj {\n                required property name -> str {\n                    constraint exclusive;\n                }\n            }\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            abstract type Foo {\n                multi link link -> Obj {\n                    default := ( select Obj filter .name = 'X' )\n                };\n            }\n\n            type Bar extending Foo {}\n\n            type Obj {\n                required property name -> str {\n                    constraint exclusive;\n                }\n            }\n        \"\"\")\n\n    async def test_edgeql_migration_scalar_array_01(self):\n        await self.migrate(r\"\"\"\n            type User {\n                required property scopes -> array<scope>;\n            }\n            scalar type scope extending int64 {\n                constraint one_of (1, 2);\n            }\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type User {\n                required property scopes -> array<scope>;\n            }\n            scalar type scope extending int64 {\n                constraint one_of (1, 2, 3);\n            }\n        \"\"\")\n\n    async def test_edgeql_migration_scalar_array_02(self):\n        await self.migrate(r\"\"\"\n            scalar type scope extending int64;\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type User {\n                required property scopes -> array<scope>;\n            }\n            scalar type scope extending int64 {\n                constraint one_of (1, 2);\n            }\n        \"\"\")\n\n    async def test_edgeql_migration_force_alter(self):\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Obj1 {\n                        property foo -> str;\n                        property bar -> str;\n                    }\n\n                    type Obj2 {\n                        property o -> int64;\n                        link o1 -> Obj1;\n                    }\n                };\n            };\n        ''')\n        await self.fast_forward_describe_migration()\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Obj1 {\n                        property foo -> str;\n                        property bar -> str;\n                    }\n\n                    type NewObj2 {\n                        property name -> str;\n                        annotation title := 'Obj2';\n                    }\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text':\n                        'CREATE TYPE test::NewObj2 {\\n'\n                        \"    CREATE ANNOTATION std::title := 'Obj2';\\n\"\n                        '    CREATE PROPERTY name'\n                        ': std::str;\\n'\n                        '};'\n                }],\n            },\n        })\n\n        await self.con.execute('''\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n        ''')\n\n        # We get the parts suggested to us granularly. We only bother\n        # to check the first one.\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text':\n                        'ALTER TYPE test::Obj2 {\\n'\n                        '    DROP LINK o1;\\n'\n                        '\\n'\n                        '};'\n                }],\n            },\n        })\n        await self.fast_forward_describe_migration()\n\n    async def test_edgeql_migration_non_ddl_statements(self):\n        await self.con.execute('SET MODULE test')\n\n        await self.start_migration('''\n            type Obj1 {\n                property foo -> str;\n            }\n        ''')\n\n        await self.con.execute('SELECT 1')\n\n        await self.fast_forward_describe_migration(commit=False)\n\n        await self.con.execute('INSERT Obj1 { foo := \"test\" }')\n\n        await self.assert_describe_migration({\n            'confirmed': [\n                'SELECT 1;',\n                'CREATE TYPE test::Obj1 { CREATE PROPERTY foo: std::str; };',\n                \"INSERT Obj1 { foo := 'test' };\"\n            ],\n            'complete': True,\n        })\n\n        await self.con.execute('COMMIT MIGRATION')\n\n        await self.assert_query_result(\n            'SELECT Obj1 { foo }',\n            [{'foo': 'test'}],\n        )\n\n    async def test_edgeql_migration_future_01(self):\n        await self.con.execute('''\n            START MIGRATION TO {\n                using future nonrecursive_access_policies;\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text':\n                        \"CREATE FUTURE nonrecursive_access_policies;\"\n                }],\n                'confidence': 1.0,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT schema::FutureBehavior {\n                    name,\n                }\n                FILTER .name = 'nonrecursive_access_policies'\n            \"\"\",\n            [{\n                'name': 'nonrecursive_access_policies',\n            }]\n        )\n\n        await self.con.execute('''\n            START MIGRATION TO {\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text':\n                        'DROP FUTURE nonrecursive_access_policies;'\n                }],\n                'confidence': 1.0,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT schema::FutureBehavior {\n                    name,\n                }\n                FILTER .name = 'nonrecursive_access_policies'\n            \"\"\",\n            []\n        )\n\n    async def test_edgeql_migration_extensions_01(self):\n        await self.con.execute('''\n            START MIGRATION TO {\n                using extension graphql;\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text':\n                        \"CREATE EXTENSION graphql VERSION '1.0';\"\n                }],\n                'confidence': 1.0,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT schema::Extension {\n                    name,\n                }\n                FILTER .name = 'graphql'\n            \"\"\",\n            [{\n                'name': 'graphql',\n            }]\n        )\n\n        await self.con.execute('''\n            START MIGRATION TO {\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text':\n                        'DROP EXTENSION graphql;'\n                }],\n                'confidence': 1.0,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT schema::Extension {\n                    name,\n                }\n                FILTER .name = 'graphql'\n            \"\"\",\n            [],\n        )\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                using extension graphql version '1.0';\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text':\n                        \"CREATE EXTENSION graphql VERSION '1.0';\"\n                }],\n                'confidence': 1.0,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n    async def test_edgeql_migration_confidence_01(self):\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Obj1 {\n                        property foo -> str;\n                        property bar -> str;\n                    }\n                };\n            };\n        ''')\n        await self.fast_forward_describe_migration()\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type NewObj1 {\n                        property foo -> str;\n                        property bar -> str;\n                    }\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text':\n                        'ALTER TYPE test::Obj1 RENAME TO test::NewObj1;'\n                }],\n                'confidence': 0.637027,\n            },\n        })\n\n        await self.con.execute('''\n            ALTER CURRENT MIGRATION REJECT PROPOSED;\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text':\n                        'CREATE TYPE test::NewObj1 {\\n'\n                        '    CREATE PROPERTY bar: std::str;'\n                        '\\n    CREATE PROPERTY foo: std::str;'\n                        '\\n};'\n                }],\n                'confidence': 1.0,\n            },\n        })\n\n    async def test_edgeql_migration_confidence_02(self):\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Obj1;\n                };\n            };\n        ''')\n        await self.fast_forward_describe_migration()\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Obj1;\n                    type Obj2;\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text':\n                        'CREATE TYPE test::Obj2;'\n                }],\n                'confidence': 1.0,\n            },\n        })\n\n    async def test_edgeql_migration_confidence_03(self):\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Obj1;\n                };\n            };\n        ''')\n        await self.fast_forward_describe_migration()\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Obj1 {\n                        property x -> str;\n                    }\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text':\n                        'ALTER TYPE test::Obj1 {\\n    '\n                        'CREATE PROPERTY x: std::str;\\n};'\n                }],\n                'confidence': 1.0,\n            },\n        })\n\n    async def test_edgeql_migration_confidence_04(self):\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Obj1 {\n                        link link -> Object;\n                    }\n                };\n            };\n        ''')\n        await self.fast_forward_describe_migration()\n\n        await self.con.execute('''\n            START MIGRATION TO {\n                module test {\n                    type Obj1 {\n                        link link -> Object {\n                            property x -> str;\n                         }\n                    }\n                };\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'complete': False,\n            'proposed': {\n                'statements': [{\n                    'text':\n                        'ALTER TYPE test::Obj1 {\\n    '\n                        'ALTER LINK link {\\n        '\n                        'CREATE PROPERTY x: std::str;\\n    };\\n};'\n                }],\n                'confidence': 1.0,\n            },\n        })\n\n    async def test_edgeql_migration_data_safety_01(self):\n        await self.start_migration('''\n            type Obj1;\n        ''')\n        await self.fast_forward_describe_migration()\n\n        await self.start_migration('''\n            type Obj1;\n            type Obj2;\n        ''')\n\n        # Creations are safe\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'proposed': {\n                'statements': [{\n                    'text':\n                        'CREATE TYPE test::Obj2;'\n                }],\n                'data_safe': True,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        await self.start_migration('''\n            type Obj1;\n        ''')\n\n        # Deletions are NOT safe\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'proposed': {\n                'statements': [{\n                    'text': 'DROP TYPE test::Obj2;'\n                }],\n                'data_safe': False,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        # Renames are safe\n        await self.start_migration('''\n            type Obj11;\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'proposed': {\n                'statements': [{\n                    'text': 'ALTER TYPE test::Obj1 RENAME TO test::Obj11;'\n                }],\n                'data_safe': True,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        # Again, creations are safe.\n        await self.start_migration('''\n            type Obj11 {\n                property name -> str {\n                    constraint exclusive;\n                }\n            }\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'proposed': {\n                'statements': [{\n                    'text': \"\"\"\n                        ALTER TYPE test::Obj11 {\n                            CREATE PROPERTY name: std::str {\n                                CREATE CONSTRAINT std::exclusive;\n                            };\n                        };\n                    \"\"\",\n                }],\n                'data_safe': True,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        await self.start_migration('''\n            type Obj11 {\n                property name -> str;\n            }\n        ''')\n\n        # Dropping constraints is safe.\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'proposed': {\n                'statements': [{\n                    'text': \"\"\"\n                        ALTER TYPE test::Obj11 {\n                            ALTER PROPERTY name {\n                                DROP CONSTRAINT std::exclusive;\n                            };\n                        };\n                    \"\"\",\n                }],\n                'data_safe': True,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        await self.start_migration('''\n            type Obj11 {\n                property name -> str {\n                    annotation title := 'name';\n                }\n            }\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'proposed': {\n                'statements': [{\n                    'text': \"\"\"\n                        ALTER TYPE test::Obj11 {\n                            ALTER PROPERTY name {\n                                CREATE ANNOTATION std::title := 'name';\n                            };\n                        };\n                    \"\"\",\n                }],\n                'data_safe': True,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        # Dropping annotations is fine also.\n        await self.start_migration('''\n            type Obj11 {\n                property name -> str;\n            }\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'proposed': {\n                'statements': [{\n                    'text': \"\"\"\n                        ALTER TYPE test::Obj11 {\n                            ALTER PROPERTY name {\n                                DROP ANNOTATION std::title;\n                            };\n                        };\n                    \"\"\",\n                }],\n                'data_safe': True,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        await self.start_migration('''\n            scalar type foo extending str;\n            type Obj11 {\n                property name -> str;\n            }\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'proposed': {\n                'statements': [{\n                    'text': \"CREATE SCALAR TYPE test::foo EXTENDING std::str;\",\n                }],\n                'data_safe': True,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        # Dropping scalar types is fine also.\n        await self.start_migration('''\n            type Obj11 {\n                property name -> str;\n            }\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'proposed': {\n                'statements': [{\n                    'text': \"DROP SCALAR TYPE test::foo;\",\n                }],\n                'data_safe': True,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        await self.start_migration('''\n            type Obj11 {\n                property name -> str;\n                index on (.name);\n            }\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'proposed': {\n                'statements': [{\n                    'text': \"\"\"\n                        ALTER TYPE test::Obj11 {\n                            CREATE INDEX ON (.name);\n                        };\n                    \"\"\",\n                }],\n                'data_safe': True,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        # Dropping indexes is fine also.\n        await self.start_migration('''\n            type Obj11 {\n                property name -> str;\n            }\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'proposed': {\n                'statements': [{\n                    'text': \"\"\"\n                        ALTER TYPE test::Obj11 {\n                            DROP INDEX ON (.name);\n                        };\n                    \"\"\",\n                }],\n                'data_safe': True,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        # Changing single to multi is fine.\n        await self.start_migration('''\n            type Obj11 {\n                multi property name -> str;\n            }\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'proposed': {\n                'statements': [{\n                    'text': \"\"\"\n                        ALTER TYPE test::Obj11 {\n                            ALTER PROPERTY name {\n                                SET MULTI;\n                            };\n                        };\n                    \"\"\",\n                }],\n                'data_safe': True,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        # But changing multi to single is NOT\n        await self.start_migration('''\n            type Obj11 {\n                single property name -> str;\n            }\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'proposed': {\n                'statements': [{\n                    'text': r\"\"\"\n                        ALTER TYPE test::Obj11 {\n                            ALTER PROPERTY name {\n                                SET SINGLE USING (\\(conv_expr));\n                            };\n                        };\n                    \"\"\",\n                }],\n                'data_safe': False,\n            },\n        })\n\n        await self.fast_forward_describe_migration(\n            user_input=[\n                '(SELECT .name LIMIT 1)'\n            ]\n        )\n\n        # And changing a property to computed is NOT!\n        await self.start_migration('''\n            type Obj11 {\n                single property name := \"test\";\n            }\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'proposed': {\n                'data_safe': False,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n        # But just changing a computed is\n        await self.start_migration('''\n            type Obj11 {\n                single property name := \"fffff\";\n            }\n        ''')\n\n        await self.assert_describe_migration({\n            'confirmed': [],\n            'proposed': {\n                'data_safe': True,\n            },\n        })\n\n        await self.fast_forward_describe_migration()\n\n    async def test_edgeql_migration_prompt_id_01(self):\n        await self.start_migration('''\n            type Bar { link spam -> Spam };\n            type Spam { link bar -> Bar };\n        ''')\n\n        await self.assert_describe_migration({\n            'proposed': {\n                'prompt_id': 'CreateObjectType TYPE test::Bar',\n                'statements': [{\n                    'text': 'CREATE TYPE test::Bar;'\n                }],\n                'confidence': 1.0,\n            },\n        })\n\n        await self.fast_forward_describe_migration(limit=1)\n\n        await self.assert_describe_migration({\n            'proposed': {\n                'prompt_id': 'CreateObjectType TYPE test::Spam',\n                'statements': [{\n                    'text': \"\"\"\n                        CREATE TYPE test::Spam {\n                            CREATE LINK bar: test::Bar;\n                        };\n                    \"\"\",\n                }],\n                'confidence': 1.0,\n            },\n        })\n\n        await self.fast_forward_describe_migration(limit=1)\n\n        # N.B: It is important that the prompt_id here match the\n        # prompt_id in the first migration, so that the migration tool\n        # will automatically apply this proposal as part of the\n        # earlier action.\n        await self.assert_describe_migration({\n            'proposed': {\n                'prompt_id': 'CreateObjectType TYPE test::Bar',\n                'statements': [{\n                    'text': \"\"\"\n                        ALTER TYPE test::Bar {\n                            CREATE LINK spam: test::Spam;\n                        };\n                    \"\"\",\n                }],\n                'confidence': 1.0,\n            },\n        })\n\n    async def test_edgeql_migration_user_input_01(self):\n        await self.migrate('''\n            type Bar { property foo -> str };\n        ''')\n\n        await self.start_migration('''\n            type Bar { property foo -> int64 };\n        ''')\n\n        await self.assert_describe_migration({\n            'proposed': {\n                'statements': [{\n                    'text': '''\n                        ALTER TYPE test::Bar {\n                            ALTER PROPERTY foo {\n                                SET TYPE std::int64 USING (\\\\(cast_expr));\n                            };\n                        };\n                    '''\n                }],\n                'required_user_input': [{\n                    'placeholder': 'cast_expr',\n                    'prompt': (\n                        \"Please specify a conversion expression\"\n                        \" to alter the type of property 'foo'\"\n                    ),\n                    'old_type': 'std::str',\n                    'new_type': 'std::int64',\n                    'pointer_name': 'foo',\n                }],\n            },\n        })\n\n    async def test_edgeql_migration_user_input_02(self):\n        await self.migrate('''\n            type Bar { multi property foo -> str };\n        ''')\n\n        await self.start_migration('''\n            type Bar { single property foo -> str };\n        ''')\n\n        await self.assert_describe_migration({\n            'proposed': {\n                'statements': [{\n                    'text': '''\n                        ALTER TYPE test::Bar {\n                            ALTER PROPERTY foo {\n                                SET SINGLE USING (\\\\(conv_expr));\n                            };\n                        };\n                    '''\n                }],\n                'required_user_input': [{\n                    'placeholder': 'conv_expr',\n                    'prompt': (\n                        \"Please specify an expression in order to convert\"\n                        \" property 'foo' of object type 'test::Bar' to\"\n                        \" 'single' cardinality\"\n                    ),\n                    'type': 'std::str',\n                    'pointer_name': 'foo',\n                }],\n            },\n        })\n\n    async def test_edgeql_migration_user_input_03(self):\n        await self.migrate('''\n            type Bar {\n                required property foo -> int64;\n            };\n        ''')\n        await self.con.execute('''\n            SET MODULE test;\n            INSERT Bar { foo := 42 };\n            INSERT Bar { foo := 1337 };\n        ''')\n\n        await self.start_migration('''\n            type Bar {\n                required property foo -> int64;\n                required property bar -> str;\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'proposed': {\n                'statements': [{\n                    'text': '''\n                        ALTER TYPE test::Bar {\n                            CREATE REQUIRED PROPERTY bar: std::str {\n                                SET REQUIRED USING (\\\\(fill_expr));\n                            };\n                        };\n                    '''\n                }],\n                'required_user_input': [{\n                    'placeholder': 'fill_expr',\n                    'prompt': (\n                        \"Please specify an expression to populate existing \"\n                        \"objects in order to make property 'bar' of object \"\n                        \"type 'test::Bar' required\"\n                    ),\n                    'type': 'std::str',\n                    'pointer_name': 'bar',\n                }],\n            },\n        })\n\n        await self.fast_forward_describe_migration(\n            user_input=[\n                '<str>.foo ++ \"!\"'\n            ]\n        )\n\n        await self.assert_query_result(\n            '''\n                SELECT Bar {foo, bar} ORDER BY .foo\n            ''',\n            [\n                {'foo': 42, 'bar': \"42!\"},\n                {'foo': 1337, 'bar': \"1337!\"},\n            ],\n        )\n\n    async def test_edgeql_migration_user_input_04(self):\n        await self.migrate('''\n            type BlogPost {\n                property title -> str;\n            }\n        ''')\n        await self.con.execute('''\n            SET MODULE test;\n            INSERT BlogPost { title := \"Programming Considered Harmful\" }\n        ''')\n\n        await self.start_migration('''\n            abstract type HasContent {\n                required property content -> str;\n            }\n            type BlogPost extending HasContent {\n                property title -> str;\n            }\n        ''')\n\n        await self.interact([\n            \"did you create object type 'test::HasContent'?\",\n            (\"did you alter object type 'test::BlogPost'?\", \"y\",\n             '\"This page intentionally left blank\"'),\n            # XXX: There is a final follow-up prompt, since the DDL\n            # generated above somewhat wrongly leaves 'content' owned\n            # by the child. This is kind of wrong, but also *works*, so\n            # maybe it's fine for now.\n            \"did you alter property 'content' of object type \"\n            \"'test::BlogPost'?\",\n        ])\n        await self.fast_forward_describe_migration()\n\n        await self.assert_query_result(\n            '''\n                SELECT BlogPost {title, content}\n            ''',\n            [\n                {\n                    'title': \"Programming Considered Harmful\",\n                    'content': \"This page intentionally left blank\",\n                },\n            ],\n        )\n\n    async def test_edgeql_migration_user_input_05(self):\n        await self.migrate(\n            '''\n            type Organization {\n                required property name -> str;\n            }\n            type Department {\n                required property name -> str;\n            }\n            '''\n        )\n        await self.start_migration(\n            '''\n            type Organization {\n                required property name -> str;\n            }\n            type Department {\n                required link org -> Organization;\n            };\n            '''\n        )\n\n        await self.fast_forward_describe_migration(\n            user_input=[\n                'insert test::Organization { name := \"default\" }'\n            ]\n        )\n\n    async def test_edgeql_migration_user_input_06(self):\n        await self.migrate('''\n            type Bar {\n                property foo -> int64;\n            };\n        ''')\n        await self.con.execute('''\n            SET MODULE test;\n            INSERT Bar { foo := 42 };\n            INSERT Bar;\n        ''')\n\n        await self.start_migration('''\n            type Bar {\n                required property foo -> str;\n            };\n        ''')\n\n        await self.assert_describe_migration({\n            'proposed': {\n                'required_user_input': [\n                    {\n                        'placeholder': 'fill_expr',\n                        'type': 'std::int64',\n                    },\n                    {\n                        'placeholder': 'cast_expr',\n                        'old_type': 'std::int64',\n                        'new_type': 'std::str',\n                    },\n                ],\n            },\n        })\n\n        await self.fast_forward_describe_migration(\n            user_input=[\n                \"0\",\n                \"<str>.foo\",\n            ]\n        )\n\n        await self.assert_query_result(\n            '''\n                SELECT Bar {foo} ORDER BY .foo\n            ''',\n            [\n                {'foo': \"0\"},\n                {'foo': \"42\"},\n            ],\n        )\n\n    async def test_edgeql_migration_union_01(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            \"it is illegal to create a type union that causes a \"\n            \"computed property 'deleted' to mix with other versions of the \"\n            \"same property 'deleted'\"\n        ):\n            await self.migrate('''\n                type Category {\n                    required property title -> str;\n                    required property deleted :=\n                        EXISTS(.<element[IS DeletionRecord]);\n                };\n                type Article {\n                    required property title -> str;\n                    required property deleted :=\n                        EXISTS(.<element[IS DeletionRecord]);\n                };\n                type DeletionRecord {\n                    link element -> Article | Category;\n                }\n            ''')\n\n    async def test_edgeql_migration_union_02(self):\n        await self.migrate('''\n            type Target1;\n            type Target1Child extending Target1;\n            type Target2;\n\n            type Source1 {\n                link tgt_union_restrict -> Target1 | Target2;\n                multi link tgt_union_m2m_del_source -> Target1 | Target2;\n            }\n\n            type Source3 extending Source1;\n        ''')\n        await self.migrate('')\n\n    async def test_edgeql_migration_backlink_01(self):\n        await self.migrate('''\n            type User {\n                link posts := .<user;\n            }\n\n            abstract type Action {\n                required link user -> User;\n            }\n\n            type Post extending Action;\n        ''')\n\n        # Make sure that the objects can actually be created and\n        # queried.\n        await self.con.execute('''\n            SET MODULE test;\n            INSERT User;\n        ''')\n        post = await self.con.query_single('''\n            INSERT Post {\n                user := (SELECT User LIMIT 1),\n            };\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT User{\n                    id,\n                    posts: {\n                        id\n                    } LIMIT 1  # this LIMIT is needed as a workaround\n                               # for another bug\n                }\n            ''',\n            [\n                {\n                    'posts': [{'id': str(post.id)}],\n                },\n            ],\n        )\n\n    async def test_edgeql_migration_misplaced_commands(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r\"cannot execute ALTER CURRENT MIGRATION\"\n            r\" outside of a migration block\",\n        ):\n            await self.con.execute('''\n                ALTER CURRENT MIGRATION REJECT PROPOSED;\n            ''')\n\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r\"cannot execute DESCRIBE CURRENT MIGRATION\"\n            r\" outside of a migration block\",\n        ):\n            await self.con.execute('''\n                DESCRIBE CURRENT MIGRATION;\n            ''')\n\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r\"cannot execute COMMIT MIGRATION\"\n            r\" outside of a migration block\",\n        ):\n            await self.con.execute('''\n                COMMIT MIGRATION;\n            ''')\n\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r\"cannot execute ABORT MIGRATION\"\n            r\" outside of a migration block\",\n        ):\n            await self.con.execute('''\n                ABORT MIGRATION;\n            ''')\n\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r\"cannot execute POPULATE MIGRATION\"\n            r\" outside of a migration block\",\n        ):\n            await self.con.execute('''\n                POPULATE MIGRATION;\n            ''')\n\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r\"cannot execute CREATE DATABASE\"\n            r\" in a migration block\",\n        ):\n            await self.start_migration('type Foo;')\n            await self.con.execute('''\n                CREATE DATABASE should_not_happen;\n            ''')\n\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r\"cannot execute CREATE ROLE\"\n            r\" in a migration block\",\n        ):\n            await self.start_migration('type Foo;')\n            await self.con.execute('''\n                CREATE ROLE should_not_happen;\n            ''')\n\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r\"cannot execute CREATE MIGRATION\"\n            r\" in a migration block\",\n        ):\n            await self.start_migration('type Foo;')\n            await self.con.execute('''\n                CREATE MIGRATION blah;\n            ''')\n\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r\"cannot execute START MIGRATION\"\n            r\" in a migration block\",\n        ):\n            await self.start_migration('type Foo;')\n            await self.con.execute('''\n                START MIGRATION TO { module test { type Foo; }};\n            ''')\n\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r\"cannot execute START TRANSACTION\"\n            r\" in a migration block\",\n        ):\n            await self.start_migration('type Foo;')\n            await self.con.query('''\n                START TRANSACTION;\n            ''')\n\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r\"cannot execute START TRANSACTION\"\n            r\" in a migration block\",\n        ):\n            await self.start_migration('type Foo;')\n            await self.con.query('''\n                START TRANSACTION;\n            ''')\n\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r\"cannot execute CONFIGURE INSTANCE\"\n            r\" in a migration block\",\n        ):\n            await self.start_migration('type Foo;')\n            await self.con.execute('''\n                CONFIGURE INSTANCE SET _foo := 123;\n            ''')\n\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r\"cannot execute CONFIGURE DATABASE\"\n            r\" in a migration block\",\n        ):\n            await self.start_migration('type Foo;')\n            await self.con.execute('''\n                CONFIGURE CURRENT DATABASE SET _foo := 123;\n            ''')\n\n    @test.xerror('''\n        Referring to alias unsupported from computable\n    ''')\n    async def test_edgeql_migration_alias_01(self):\n        await self.migrate(r'''\n            type Foo {\n                property name -> str;\n                property comp := Alias;\n            };\n\n            alias Alias := {0, 1, 2, 3};\n        ''')\n\n        # Make sure that the objects can actually be created and\n        # queried.\n        await self.con.execute('''\n            SET MODULE test;\n            INSERT Foo {name := 'foo'};\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Foo {\n                    name,\n                    comp,\n                }\n            ''',\n            [\n                {\n                    'name': 'foo',\n                    'comp': {0, 1, 2, 3},\n                },\n            ],\n        )\n\n    @test.xerror('''\n       Referring to alias unsupported from computable\n       This is the only test that broke when we disallowed that!\n    ''')\n    async def test_edgeql_migration_alias_02(self):\n        await self.migrate(r'''\n            type Foo {\n                property name -> str;\n                property comp := Alias + 0;\n            };\n\n            alias Alias := {0, 1, 2, 3};\n        ''')\n\n        # Make sure that the objects can actually be created and\n        # queried.\n        await self.con.execute('''\n            SET MODULE test;\n            INSERT Foo {name := 'foo'};\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Foo {\n                    name,\n                    comp,\n                }\n            ''',\n            [\n                {\n                    'name': 'foo',\n                    'comp': {0, 1, 2, 3},\n                },\n            ],\n        )\n\n        await self.migrate(r'''\n            type Foo {\n                property name -> str;\n                property comp := Alias + 0;\n            };\n\n            alias Alias := {4, 5};\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Foo {\n                    name,\n                    comp,\n                }\n            ''',\n            [\n                {\n                    'name': 'foo',\n                    'comp': {4, 5},\n                },\n            ],\n        )\n\n    @test.xerror('''\n        Referring to alias unsupported from computable\n    ''')\n    async def test_edgeql_migration_alias_03(self):\n        await self.migrate(r'''\n            type Foo {\n                property name -> str;\n                property comp := {Alias, Alias};\n            };\n\n            alias Alias := 42;\n        ''')\n\n        # Make sure that the objects can actually be created and\n        # queried.\n        await self.con.execute('''\n            SET MODULE test;\n            INSERT Foo {name := 'foo'};\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Foo {\n                    name,\n                    comp,\n                }\n            ''',\n            [\n                {\n                    'name': 'foo',\n                    'comp': [42, 42],\n                },\n            ],\n        )\n\n        await self.migrate(r'''\n            type Foo {\n                property name -> str;\n                property comp := {Alias, Alias};\n            };\n\n            alias Alias := 'alias';\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Foo {\n                    name,\n                    comp,\n                }\n            ''',\n            [\n                {\n                    'name': 'foo',\n                    'comp': ['alias', 'alias'],\n                },\n            ],\n        )\n\n    @test.xerror('''\n        Referring to alias unsupported from computable\n    ''')\n    async def test_edgeql_migration_alias_04(self):\n        # Same as the previous test, but using a single DDL command to\n        # migrate.\n        await self.migrate(r'''\n            type Foo {\n                property name -> str;\n                property comp := {Alias, Alias};\n            };\n\n            alias Alias := 42;\n        ''')\n\n        # Make sure that the objects can actually be created and\n        # queried.\n        await self.con.execute('''\n            SET MODULE test;\n            INSERT Foo {name := 'foo'};\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Foo {\n                    name,\n                    comp,\n                }\n            ''',\n            [\n                {\n                    'name': 'foo',\n                    'comp': [42, 42],\n                },\n            ],\n        )\n\n        # Instead of using an SDL migration, use a single DDL command.\n        await self.con.execute('''\n            ALTER ALIAS Alias USING ('alias');\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Foo {\n                    name,\n                    comp,\n                }\n            ''',\n            [\n                {\n                    'name': 'foo',\n                    'comp': ['alias', 'alias'],\n                },\n            ],\n        )\n\n    @test.xerror('''\n        Referring to alias unsupported from computable\n    ''')\n    async def test_edgeql_migration_alias_05(self):\n        await self.migrate(r'''\n            type Foo {\n                property name -> str;\n                link comp := Alias;\n            };\n\n            type Bar;\n\n            alias Alias := Bar {val := 42};\n        ''')\n\n        # Make sure that the objects can actually be created and\n        # queried.\n        await self.con.execute('''\n            SET MODULE test;\n            INSERT Bar;\n            INSERT Foo {name := 'foo'};\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Foo {\n                    name,\n                    comp: {\n                        val\n                    },\n                }\n            ''',\n            [\n                {\n                    'name': 'foo',\n                    'comp': {\n                        'val': 42,\n                    },\n                },\n            ],\n        )\n\n    @test.xerror('''\n        Referring to alias unsupported from computable\n    ''')\n    async def test_edgeql_migration_alias_06(self):\n        await self.migrate(r'''\n            type Foo {\n                property name -> str;\n                property comp := Alias.val;\n            };\n\n            type Bar;\n\n            alias Alias := Bar {val := 42};\n        ''')\n\n        # Make sure that the objects can actually be created and\n        # queried.\n        await self.con.execute('''\n            SET MODULE test;\n            INSERT Bar;\n            INSERT Foo {name := 'foo'};\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Foo {\n                    name,\n                    comp,\n                }\n            ''',\n            [\n                {\n                    'name': 'foo',\n                    'comp': {42},\n                },\n            ],\n        )\n\n        await self.migrate(r'''\n            type Foo {\n                property name -> str;\n                property comp := Alias.val;\n            };\n\n            type Bar;\n\n            alias Alias := Bar {val := 'val'};\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Foo {\n                    name,\n                    comp,\n                }\n            ''',\n            [\n                {\n                    'name': 'foo',\n                    'comp': {'val'},\n                },\n            ],\n        )\n\n    @test.xerror('''\n        Referring to alias unsupported from computable\n    ''')\n    async def test_edgeql_migration_alias_07(self):\n        await self.migrate(r'''\n            type Foo {\n                property name -> str;\n                link comp := Alias.alias_link;\n            };\n\n            type Bar {\n                property val -> str;\n            };\n            type Fuz {\n                property val -> str;\n            };\n\n            alias Alias := Bar {\n                alias_link := Fuz {\n                    alias_comp := 42,\n                }\n            };\n        ''')\n\n        # Make sure that the objects can actually be created and\n        # queried.\n        await self.con.execute('''\n            SET MODULE test;\n            INSERT Bar {val := 'bar'};\n            INSERT Fuz {val := 'fuz'};\n            INSERT Foo {name := 'foo'};\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Foo {\n                    name,\n                    comp: {\n                        val,\n                        alias_comp,\n                    },\n                }\n            ''',\n            [\n                {\n                    'name': 'foo',\n                    'comp': [{\n                        'val': 'fuz',\n                        'alais_comp': 42,\n                    }],\n                },\n            ],\n        )\n\n        await self.migrate(r'''\n            type Foo {\n                property name -> str;\n                link comp := Alias.alias_link;\n            };\n\n            type Bar {\n                property val -> str;\n            };\n            type Fuz {\n                property val -> str;\n            };\n\n            alias Alias := Bar {\n                alias_link := Fuz {\n                    alias_comp := 42,\n                }\n            };\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Foo {\n                    name,\n                    comp: {\n                        val\n                    },\n                }\n            ''',\n            [\n                {\n                    'name': 'foo',\n                    'comp': [{\n                        'val': 'bar',\n                        'alais_comp': 42,\n                    }],\n                },\n            ],\n        )\n\n    async def test_edgeql_migration_alias_08(self):\n        await self.migrate(r'''\n            type Foo;\n            type Bar;\n            alias X := Foo;\n        ''')\n\n        await self.migrate(r'''\n            type Foo;\n            type Bar;\n            alias X := Bar;\n        ''')\n\n        await self.migrate(r'''\n            type Foo;\n            type Bar;\n            alias X := 30;\n        ''')\n\n        await self.migrate(r'''\n            type Foo;\n            type Bar;\n            alias X := \"30\";\n        ''')\n\n        await self.migrate(r'''\n            type Foo;\n            type Bar;\n            alias X := (Bar { z := 10 }, 30);\n        ''')\n\n        await self.migrate(r'''\n            type Foo;\n            type Bar;\n            alias X := ((Bar { z := 10 }, 30), 20);\n        ''')\n\n        # delete it\n        await self.migrate(r'''\n            type Foo;\n            type Bar;\n        ''')\n\n    async def test_edgeql_migration_alias_09(self):\n        await self.migrate(r'''\n            type Foo;\n            type Bar;\n            alias X := Foo { bar := Bar { z := 1 } };\n        ''')\n\n        await self.migrate(r'''\n            type Foo;\n            type Bar;\n            alias X := Bar;\n        ''')\n\n    async def test_edgeql_migration_tuple_01(self):\n        await self.migrate(r'''\n            type Bag {\n                property switching_tuple -> tuple<name: str, weight: int64>;\n            };\n        ''')\n\n        with self.assertRaisesRegex(\n            AssertionError,\n            r\"Please specify a conversion expression to alter the type of \"\n            r\"property 'switching_tuple'\"\n        ):\n            await self.migrate(r'''\n                type Bag {\n                    property switching_tuple -> tuple<name: str, age: int64>;\n                };\n            ''')\n\n    async def test_edgeql_migration_inheritance_to_empty_01(self):\n        await self.migrate(r'''\n            type A {\n                property name -> str;\n            }\n            type B {\n                property name -> str;\n            }\n            type C extending A, B {\n            }\n        ''')\n\n        await self.migrate('')\n\n    async def test_edgeql_migration_inheritance_to_empty_02(self):\n        await self.migrate(r'''\n            abstract type Named {\n                required property name -> str {\n                    delegated constraint exclusive;\n                }\n            }\n\n            type User {\n                link avatar -> Card {\n                    property text -> str;\n                    property tag := .name ++ ((\"-\" ++ @text) ?? \"\");\n                }\n            }\n\n            type Card extending Named;\n\n            type SpecialCard extending Card;\n        ''')\n\n        await self.migrate('')\n\n    async def test_edgeql_migration_drop_constraint_01(self):\n        await self.migrate(r'''\n            abstract type Named {\n                required property name -> str {\n                    delegated constraint exclusive;\n                }\n            }\n\n            type User {\n                link avatar -> Card {\n                    property text -> str;\n                    property tag := .name ++ ((\"-\" ++ @text) ?? \"\");\n                }\n            }\n\n            type Card extending Named;\n\n            type SpecialCard extending Card;\n        ''')\n\n        await self.migrate(r'''\n            abstract type Named {\n                required property name -> str;\n            }\n\n            type User {\n                link avatar -> Card {\n                    property text -> str;\n                    property tag := .name ++ ((\"-\" ++ @text) ?? \"\");\n                }\n            }\n\n            type Card extending Named;\n\n            type SpecialCard extending Card;\n        ''')\n\n    async def test_edgeql_migration_drop_constraint_02(self):\n        await self.migrate(r'''\n            abstract type Named {\n                required property name -> str {\n                    delegated constraint exclusive;\n                }\n            }\n\n            type User {\n                link avatar -> Card {\n                    property text -> str;\n                    property tag := .name ++ ((\"-\" ++ @text) ?? \"\");\n                }\n            }\n\n            type Card extending Named;\n\n            type SpecialCard extending Card;\n            type SpecialCard2 extending Card;\n            type VerySpecialCard extending SpecialCard, SpecialCard2;\n        ''')\n\n        await self.migrate(r'''\n            abstract type Named {\n                required property name -> str;\n            }\n\n            type User {\n                link avatar -> Card {\n                    property text -> str;\n                    property tag := .name ++ ((\"-\" ++ @text) ?? \"\");\n                }\n            }\n\n            type Card extending Named;\n\n            type SpecialCard extending Card;\n            type SpecialCard2 extending Card;\n            type VerySpecialCard extending SpecialCard, SpecialCard2;\n        ''')\n\n    async def test_edgeql_migration_drop_constraint_03(self):\n        await self.migrate(r'''\n            type C {\n                required property val -> str {\n                    constraint exclusive;\n                }\n            }\n\n            type Foo {\n                required link foo -> C {\n                    default := (SELECT C FILTER .val = 'D00');\n                }\n            }\n        ''')\n\n        await self.migrate(r'''\n            type Foo {\n                required link foo -> C {\n                    default := (SELECT C FILTER .val = 'D00');\n                }\n            }\n\n            type C {\n                required property val -> str {\n                    constraint exclusive;\n                }\n            }\n        ''')\n\n        await self.migrate('')\n\n    async def test_edgeql_migration_drop_constraint_04(self):\n        await self.migrate(r'''\n            type C {\n                required property val -> str {\n                    constraint exclusive;\n                }\n            }\n\n            type Foo {\n                required link foo -> C {\n                    default := (SELECT C FILTER .val = 'D00');\n                }\n            }\n        ''')\n\n        await self.migrate(r'''\n            type C {\n                required property val -> str;\n            }\n\n            type Foo {\n                required multi link foo -> C {\n                    default := (SELECT C FILTER .val = 'D00');\n                }\n            }\n        ''')\n\n    async def test_edgeql_migration_drop_constraint_05(self):\n        await self.migrate(r'''\n            type C {\n                required property val -> str {\n                    constraint exclusive;\n                }\n                required property val2 -> str {\n                    constraint exclusive;\n                }\n            }\n\n            type Foo {\n                required link foo -> C {\n                    default := (SELECT C FILTER .val = 'D00');\n                }\n            }\n        ''')\n\n        await self.migrate(r'''\n            type C {\n                required property val2 -> str {\n                    constraint exclusive;\n                }\n            }\n\n            type Foo {\n                required link foo -> C {\n                    default := (SELECT C FILTER .val2 = 'D00');\n                }\n            }\n        ''')\n\n    async def test_edgeql_migration_fiddly_delete_01(self):\n        await self.migrate(r'''\n            type Document {\n              multi link entries -> Entry {\n                constraint exclusive;\n              }\n              multi link fields := .entries.field;\n              required link form -> Form;\n            }\n\n            type Entry {\n              required link field -> Field;\n              required property value -> str;\n              link form := .field.form;\n            }\n\n            type Field {\n              required property name -> str;\n\n              link form := .<fields[IS Form];\n            }\n\n            type Form {\n              required property name -> str {\n                constraint exclusive;\n              }\n\n              multi link fields -> Field;\n            }\n        ''')\n        await self.migrate(r'''\n            type Entry {\n              required link field -> Field;\n              required property value -> str;\n              link form := .field.form;\n            }\n\n            type Field {\n              required property name -> str;\n\n              link form := .<fields[IS Form];\n            }\n\n            type Form {\n              required property name -> str {\n                constraint exclusive;\n              }\n\n              multi link fields -> Field;\n            }\n        ''')\n\n    async def test_edgeql_migration_uuid_array_01(self):\n        await self.migrate(r'''\n            type Foo {\n                property x -> array<uuid>;\n            }\n        ''')\n\n    async def test_edgeql_migration_on_target_delete_01(self):\n        await self.migrate(\n            r\"\"\"\n                type User {\n                    multi link workspaces -> Workspace {\n                        property title -> str;\n                        on target delete allow;\n                    }\n                }\n\n                type Workspace {\n                    multi link users := .<workspaces[is User];\n                }\n            \"\"\"\n        )\n\n        await self.migrate(\n            r\"\"\"\n                type User {\n                    multi link workspaces := .<users[is Workspace];\n                }\n\n                type Workspace {\n                    multi link users -> User {\n                        property title -> str;\n                        on target delete allow;\n                    }\n                }\n            \"\"\"\n        )\n\n    async def test_edgeql_migration_on_target_delete_02(self):\n        await self.migrate(\n            r\"\"\"\n                type Tgt;\n                type Foo {\n                    link tgt -> Tgt {\n                        on target delete allow;\n                    }\n                }\n                type Bar extending Foo {\n                    overloaded link tgt -> Tgt {\n                        on target delete restrict;\n                    }\n                }\n            \"\"\"\n        )\n\n        await self.con.execute(\"\"\"\n            with module test\n            insert Bar { tgt := (insert Tgt) };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n            edgedb.ConstraintViolationError,\n            'prohibited by link target policy',\n        ):\n            await self.con.execute(\"\"\"\n                with module test\n                delete Tgt;\n            \"\"\")\n\n        await self.migrate(\n            r\"\"\"\n                type Tgt;\n                type Foo {\n                    link tgt -> Tgt {\n                        on target delete allow;\n                    }\n                }\n                type Bar extending Foo;\n            \"\"\"\n        )\n\n        await self.con.execute(\"\"\"\n            with module test\n            delete Tgt;\n        \"\"\")\n\n    async def test_edgeql_migration_rename_with_stuff_01(self):\n        await self.migrate(\n            r\"\"\"\n                type Base {\n                        property x -> str;\n                        property xbang := .x ++ \"!\";\n                }\n\n                type NamedObject extending Base {\n                        required property foo -> str;\n                }\n            \"\"\"\n        )\n\n        await self.migrate(\n            r\"\"\"\n                type Base {\n                        property x -> str;\n                        property xbang := .x ++ \"!\";\n                }\n\n                type ReNamedObject extending Base {\n                        required property foo -> str;\n                }\n            \"\"\"\n        )\n\n    async def test_edgeql_migration_access_policy_01(self):\n        await self.migrate(r\"\"\"\n            type Test2 {\n                access policy asdf allow all using (true);\n            }\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Test2 {\n                access policy asdf allow all;\n            }\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            abstract type Parent {\n                access policy asdf allow all;\n            }\n            type Test2 extending Parent;\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            abstract type Parent {\n                access policy asdf when (true) allow all;\n            }\n            type Test2 extending Parent;\n        \"\"\")\n\n    async def test_edgeql_migration_access_policy_02(self):\n        # Make sure policies don't interfere with constraints or indexes\n        await self.migrate(r\"\"\"\n            required global foo -> bool { default := true };\n            abstract type Base {\n                access policy locked allow all using (global foo);\n            }\n\n            type Tgt extending Base;\n\n            type Src {\n                required link tgt -> Tgt {\n                    constraint exclusive;\n                }\n                index on (.tgt)\n            }\n        \"\"\")\n\n    async def test_edgeql_migration_globals_01(self):\n        schema = r\"\"\"\n            global current_user_id -> uuid;\n            global current_user := (\n              select Member filter .id = global current_user_id\n            );\n\n            type Foo {\n              link owner := .<avatar[is Member];\n            };\n            type Member {\n              link avatar -> Foo {\n                constraint exclusive;\n              }\n            }\n        \"\"\"\n        # Make sure it doesn't get into a wedged state\n        await self.migrate(schema)\n        await self.migrate(schema)\n\n    async def test_edgeql_migration_globals_02(self):\n        await self.migrate(r\"\"\"\n            global current_user_id -> uuid;\n            global current_user := (\n              select Member filter .id = global current_user_id\n            );\n\n            type Foo;\n            type Base {\n              link avatar -> Foo {\n                constraint exclusive;\n              }\n            }\n            type Member;\n        \"\"\")\n\n        schema = r\"\"\"\n            global current_user_id -> uuid;\n            global current_user := (\n              select Member filter .id = global current_user_id\n            );\n\n            type Foo;\n            type Base {\n              link avatar -> Foo {\n                constraint exclusive;\n              }\n            }\n            type Member extending Base;\n        \"\"\"\n\n        # Make sure it doesn't get into a wedged state\n        await self.migrate(schema)\n        await self.migrate(schema)\n\n    async def test_edgeql_migration_globals_03(self):\n        # Test modifying a computed global that is used in an access\n        # policy on the type it refers to.\n        await self.migrate(r\"\"\"\n            global cur -> uuid;\n            global scopes := ((select Foo filter .id = global cur).scopes);\n\n            type Foo {\n              multi scopes: str;\n              access policy s allow select using ('f' in global scopes);\n            };\n        \"\"\")\n        await self.migrate(r\"\"\"\n            global cur -> uuid;\n            global scopes := (\n                select (select Foo filter .id = global cur).scopes);\n\n            type Foo {\n              multi scopes: str;\n              access policy s allow select using ('f' in global scopes);\n            };\n        \"\"\")\n\n    @test.skip('''\n        This fails ~half the time depending on how it orders thing\n    ''')\n    async def test_edgeql_migration_globals_04(self):\n        await self.migrate(r\"\"\"\n            global cur := true;\n\n            type Foo {\n              access policy s allow select using (global cur);\n            };\n            type Bar {\n              access policy s allow select using (global cur);\n            };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            global ok := (exists Foo);\n            global cur := global ok;\n\n            type Foo {\n              access policy s allow select using (global cur);\n            };\n            type Bar {\n              access policy s allow select using (global cur);\n            };\n        \"\"\")\n\n    async def test_edgeql_migration_dml_rewrites_01(self):\n        await self.migrate(r\"\"\"\n            type Post {\n              required title: str;\n              modified: datetime {\n                rewrite insert, update using (datetime_of_statement())\n              }\n            }\n        \"\"\")\n        await self.migrate(r\"\"\"\n            type BlogPost {\n              required title: str;\n              modified: datetime {\n                rewrite insert, update using (datetime_of_statement())\n              }\n            }\n        \"\"\")\n        await self.migrate(r\"\"\"\n            type BlogPost {\n              required title: str;\n              modified: datetime {\n                rewrite insert, update using (datetime_of_transaction())\n              }\n            }\n        \"\"\")\n        await self.migrate('')\n\n    async def test_edgeql_migration_policies_and_collections(self):\n        # An infinite recursion bug with this this was found by accident\n        # when a number of tests accidentally were in the non isolated test.\n        # (Simplified a bit.)\n        await self.migrate(r\"\"\"\n            abstract type Base {\n                access policy locked allow all using (false);\n            }\n\n            type Src {\n                required link tgt -> Base {\n                    constraint exclusive;\n                }\n            }\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            alias Foo := 20;\n        \"\"\")\n\n    async def test_edgeql_migration_drop_required_01(self):\n        await self.migrate(r\"\"\"\n            abstract type AbstractLinkTarget {\n                multi link linkSources := .<abstractTarget[is LinkSource];\n            }\n\n            type ImplementationType extending AbstractLinkTarget {}\n\n            type LinkSource {\n                required link abstractTarget -> AbstractLinkTarget;\n            }\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            abstract type AbstractLinkTarget {\n                multi link linkSources := .<abstractTarget[is LinkSource];\n            }\n\n            type ImplementationType extending AbstractLinkTarget {}\n\n            type LinkSource {\n                link abstractTarget -> AbstractLinkTarget;\n            }\n        \"\"\")\n\n    async def test_edgeql_migration_link_to_sub_with_ref_01(self):\n        # Test moving a link to a subtype while a ref exists to it\n        await self.migrate(r\"\"\"\n            type Athlete {\n                multi link schedules := Athlete.<owner[IS AthleteSchedule];\n            }\n\n            abstract type Schedule  {\n                required property name -> str;\n                required link owner -> Athlete;\n            }\n            type AthleteSchedule extending Schedule;\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            type Athlete {\n                multi link schedules := Athlete.<owner[IS AthleteSchedule];\n            }\n\n            abstract type Schedule  {\n                required property name -> str;\n            }\n            type AthleteSchedule extending Schedule {\n                required link owner -> Athlete;\n            }\n        \"\"\")\n\n    async def test_edgeql_migration_alias_linkprop_01(self):\n        await self.migrate(r\"\"\"\n            alias UserAlias := User;\n\n            type User {\n              multi link ml -> Target {\n                property lp -> str;\n              };\n            }\n\n            type Target;\n        \"\"\")\n\n    async def test_edgeql_migration_lift_01(self):\n        await self.migrate(r\"\"\"\n            abstract type A;\n            abstract type B;\n\n            abstract type Foo extending A;\n            type Bar extending Foo, B;\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            abstract type A;\n            abstract type B;\n\n            abstract type Foo extending A, B;\n            type Bar extending Foo;\n        \"\"\")\n\n    async def test_edgeql_migration_nested_backticks_01(self):\n        await self.migrate(r\"\"\"\n            module nested { type Test };\n        \"\"\")\n\n        await self.migrate(r\"\"\"\n            module nested { type Test };\n            module `back``ticked` { type Test };\n        \"\"\")\n\n    async def test_edgeql_migration_abstract_index_01(self):\n        await self.migrate(\n            r\"\"\"\n            abstract index MyIndex extending fts::index;\n            type Base {\n                property name -> str;\n                index MyIndex on (\n                    fts::with_options(.name, language := fts::Language.eng)\n                );\n            };\n            \"\"\"\n        )\n\n        await self.migrate(\n            r\"\"\"\n            abstract index MyIndex extending fts::index;\n            type Base {\n                property name -> str;\n                index MyIndex on (\n                    fts::with_options(.name, language := fts::Language.eng)\n                );\n            };\n            type Child extending Base;\n            \"\"\"\n        )\n\n        async with self.assertRaisesRegexTx(\n                edgedb.SchemaError,\n                r\"because other objects in the schema depend on it\"):\n            await self.con.execute('''\n                drop abstract index test::MyIndex\n            ''')\n\n        await self.migrate(\n            r\"\"\"\n            abstract index MyIndex extending fts::index;\n            type Base {\n                property name -> str;\n                index MyIndex on (\n                    fts::with_options(.name, language := fts::Language.eng)\n                );\n            };\n            type Child extending Base;\n            \"\"\"\n        )\n\n        await self.migrate(\n            r\"\"\"\n            abstract index MyIndex extending fts::index {\n                annotation title := \"test\";\n            }\n            type Base {\n                property name -> str;\n                index MyIndex on (\n                    fts::with_options(.name, language := fts::Language.eng)\n                );\n            };\n            type Child extending Base;\n            \"\"\"\n        )\n\n        await self.migrate(\"\")\n\n    async def test_edgeql_migration_backlink_overloaded(self):\n        await self.migrate(r'''\n            type Target {\n                multi link meta_sources := .<target[is MetaSource];\n            }\n            abstract type MetaSource {\n                link target -> Target;\n            }\n\n            type Source extending MetaSource;\n            type ExternalSource extending MetaSource {\n                overloaded link target -> Target;\n            }\n        ''')\n\n    async def test_edgeql_migration_property_ref(self):\n        await self.migrate(r'''\n            type Log {\n                body: str;\n                timestamp: datetime {\n                    default := datetime_current();\n                }\n            }\n\n            type Person {\n                required name: str;\n                trigger log_delete after insert for each\n                when (__new__.name not like \"SKIP%\") do (\n                    insert Log { body := __new__.name }\n                );\n            }\n        ''')\n\n        await self.migrate(r'''\n            type Log {\n                body: str;\n            }\n\n            type Person {\n                required name: str;\n                trigger log_delete after insert for each\n                when (false) do (\n                    insert Log { body := __new__.name }\n                );\n            }\n        ''')\n\n    async def test_edgeql_migration_to_computed_drop_exclusive(self):\n        await self.migrate(r'''\n            type User {\n              multi posts: Post {\n                constraint exclusive;\n              }\n              multi foo: int64 {\n                constraint exclusive;\n              }\n              bar: int64 {\n                constraint exclusive;\n              }\n            }\n            type Post;\n        ''')\n\n        await self.migrate(r'''\n            type User {\n              multi link posts := .<user[is Post];\n              multi property foo := Post.num;\n              property bar := -1;\n            }\n            type Post {\n              user: User;\n              num: int64;\n            }\n        ''')\n\n    async def test_edgeql_migration_between_computeds_01(self):\n        await self.migrate(r'''\n            type Away {\n                x: str;\n                property y {\n                    using (.x ++ \"!\");\n                    constraint exclusive;\n                }\n            };\n            type Away2 extending Away;\n        ''')\n\n        await self.migrate(r'''\n            type Away {\n                x: str;\n                property y -> str {\n                    constraint exclusive;\n                }\n            };\n            type Away2 extending Away;\n        ''')\n\n    async def test_edgeql_migration_between_computeds_02(self):\n        await self.migrate(r'''\n            type Away {\n                x: str;\n                property y {\n                    using (.x);\n                    constraint exclusive;\n                }\n            };\n            type Away2 extending Away;\n        ''')\n\n        await self.migrate(r'''\n            type Away {\n                x: str;\n                property y {\n                    using (.x ++ \"!\");\n                    constraint exclusive;\n                }\n            };\n            type Away2 extending Away;\n        ''')\n\n        await self.migrate(r'''\n            type Away {\n                x: str;\n                property y {\n                    using (.x);\n                    constraint exclusive;\n                }\n            };\n            type Away2 extending Away;\n        ''')\n\n        await self.migrate(r'''\n            type Away {\n                x: str;\n                property y -> str {\n                    constraint exclusive;\n                }\n            };\n            type Away2 extending Away;\n        ''')\n\n    async def test_edgeql_migration_alias_new_computed_01(self):\n        await self.migrate(r'''\n            global a_id -> str;\n            global current_user := (\n              select User filter .x_id = global a_id);\n\n            type User {\n                required property x_id -> str {\n                  constraint exclusive;\n              }\n            }\n        ''')\n\n        await self.migrate(r'''\n            global a_id -> str;\n            global current_user := (\n              select User filter .x_id = global a_id);\n\n            type User {\n                required property x_id -> str {\n                  constraint exclusive;\n              }\n              required property a_id := .x_id;\n            }\n        ''')\n\n    async def test_edgeql_migration_alias_new_computed_02(self):\n        await self.migrate(r'''\n            global a_id -> str;\n            alias current_user := (\n              select User filter .x_id = global a_id);\n\n            type User {\n                required property x_id -> str {\n                  constraint exclusive;\n              }\n            }\n        ''')\n\n        await self.migrate(r'''\n            global a_id -> str;\n            alias current_user := (\n              select User filter .x_id = global a_id);\n\n            type User {\n                required property x_id -> str {\n                  constraint exclusive;\n              }\n              required property a_id := .x_id;\n            }\n        ''')\n\n    async def test_edgeql_migration_trigger_shift_01(self):\n        await self.migrate(r'''\n            type Log {\n                body: str;\n                timestamp: datetime {\n                    default := datetime_current();\n                }\n            }\n\n\n            abstract type Named {\n                required name: str;\n            }\n\n            type Person extending Named {\n                trigger log_insert after insert for each when (false) do (\n                    insert Log {\n                        body := __new__.__type__.name ++ ' ' ++ __new__.name,\n                    }\n                );\n            }\n        ''')\n\n        await self.migrate(r'''\n            type Log {\n                body: str;\n                timestamp: datetime {\n                    default := datetime_current();\n                }\n            }\n\n\n            abstract type Named {\n                required name: str;\n\n                trigger log_insert after insert for each when (false) do (\n                    insert Log {\n                        body := __new__.__type__.name ++ ' ' ++ __new__.name,\n                    }\n                );\n            }\n\n            type Person extending Named {\n            }\n        ''')\n\n        await self.migrate(r'''\n            type Log {\n                body: str;\n                timestamp: datetime {\n                    default := datetime_current();\n                }\n            }\n\n\n            abstract type Named {\n                required name: str;\n\n                trigger log_insert after insert for each when (true) do (\n                    insert Log {\n                        body := __new__.__type__.name ++ ' ' ++ __new__.name,\n                    }\n                );\n            }\n\n            type Person extending Named {\n            }\n        ''')\n\n\nclass TestEdgeQLDataMigrationNonisolated(EdgeQLDataMigrationTestCase):\n    TRANSACTION_ISOLATION = False\n\n    PER_TEST_TEARDOWN = [\n        'reset schema to initial'\n    ]\n\n    async def test_edgeql_migration_eq_collections_25(self):\n        await self.con.execute(r\"\"\"\n            START MIGRATION TO {\n                module test {\n                    alias Foo := [20];\n                }\n            };\n            POPULATE MIGRATION;\n            COMMIT MIGRATION;\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            START MIGRATION TO {\n                module test {\n                }\n            };\n            POPULATE MIGRATION;\n            COMMIT MIGRATION;\n        \"\"\")\n\n    async def test_edgeql_ddl_collection_cleanup_06(self):\n        for _ in range(2):\n            await self.con.execute(r\"\"\"\n                CREATE FUNCTION cleanup_06(\n                    a: int64\n                ) -> tuple<int64, tuple<int64>>\n                    USING EdgeQL $$\n                        SELECT (a, ((a + 1),))\n                    $$;\n            \"\"\")\n\n            await self.con.execute(r\"\"\"\n                DROP FUNCTION cleanup_06(a: int64)\n            \"\"\")\n\n    async def test_edgeql_migration_enum_01(self):\n        # Test some enum stuff. This needs to be nonisolated because postgres\n        # won't let you *use* an enum until it has been committed!\n        await self.migrate('''\n            scalar type Status extending enum<pending, in_progress, finished>;\n            scalar type ImportStatus extending Status;\n            scalar type ImportAnalyticsStatus extending Status;\n\n            type Foo { property x -> ImportStatus };\n        ''')\n        await self.con.execute('''\n            with module test\n            insert Foo { x := 'pending' };\n        ''')\n\n        await self.migrate('''\n            scalar type Status extending enum<\n                pending, in_progress, finished, wontfix>;\n            scalar type ImportStatus extending Status;\n scalar type ImportAnalyticsStatus extending Status;\n\n            type Foo { property x -> ImportStatus };\n            function f(x: Status) -> str USING (<str>x);\n        ''')\n\n        await self.migrate('''\n            scalar type Status extending enum<\n                pending, in_progress, finished, wontfix, again>;\n            scalar type ImportStatus extending Status;\n            scalar type ImportAnalyticsStatus extending Status;\n\n            type Foo { property x -> ImportStatus };\n            function f(x: Status) -> str USING (<str>x);\n        ''')\n\n        await self.assert_query_result(\n            r\"\"\"\n                with module test\n                select <ImportStatus>'wontfix'\n            \"\"\",\n            ['wontfix'],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                with module test\n                select f(<ImportStatus>'wontfix')\n            \"\"\",\n            ['wontfix'],\n        )\n\n        # Retry for https://github.com/edgedb/edgedb/issues/7553\n        async for tr in self.try_until_succeeds(\n            ignore_regexp=\"cannot drop type .* \"\n                          \"because other objects depend on it\",\n        ):\n            async with tr:\n                await self.migrate('''\n                    scalar type Status extending enum<\n                        pending, in_progress, wontfix, again>;\n                    scalar type ImportStatus extending Status;\n                    scalar type ImportAnalyticsStatus extending Status;\n\n                    type Foo { property x -> ImportStatus };\n                    function f(x: Status) -> str USING (<str>x);\n                ''')\n\n        await self.migrate('')\n\n    async def test_edgeql_migration_splat_01(self):\n        await self.migrate('''\n            type Foo {\n                property bar := (select <json>Bar { ** })\n            }\n\n            type Bar {\n                property a -> str;\n                link foo -> Foo;\n            }\n        ''')\n\n        await self.migrate('''\n            type Foo {\n                property bar := (<json>{})\n            }\n        ''')\n\n    async def test_edgeql_migration_recovery(self):\n        await self.con.execute(r\"\"\"\n            START MIGRATION TO {\n                module test {\n                    type Foo;\n                }\n            };\n        \"\"\")\n        await self.con.execute('POPULATE MIGRATION')\n\n        with self.assertRaises(edgedb.EdgeQLSyntaxError):\n            await self.con.execute(r\"\"\"\n                ALTER TYPE Foo;\n            \"\"\")\n\n        with self.assertRaises(edgedb.TransactionError):\n            await self.con.execute(\"COMMIT MIGRATION\")\n\n        await self.con.execute(\"ABORT MIGRATION\")\n\n        self.assertEqual(await self.con.query_single(\"SELECT 1\"), 1)\n\n    async def test_edgeql_script_partial_migration(self):\n        with self.assertRaisesRegex(edgedb.QueryError, \"incomplete migration\"):\n            await self.con.execute(r\"\"\"\n                START MIGRATION TO {\n                    module test {\n                        type Foo;\n                    }\n                };\n                POPULATE MIGRATION;\n            \"\"\")\n\n    async def test_edgeql_migration_recovery_in_tx(self):\n        await self.con.execute(\"START TRANSACTION\")\n        try:\n            await self.con.execute(\"CREATE TYPE Bar\")\n            await self.con.execute(r\"\"\"\n                START MIGRATION TO {\n                    module test {\n                        type Foo;\n                    }\n                };\n            \"\"\")\n\n            with self.assertRaises(edgedb.EdgeQLSyntaxError):\n                await self.con.execute(r\"\"\"\n                    ALTER TYPE Foo;\n                \"\"\")\n\n            await self.con.execute(\"ABORT MIGRATION\")\n\n            self.assertEqual(await self.con.query(\"SELECT Bar\"), [])\n        finally:\n            await self.con.execute(\"ROLLBACK\")\n\n    async def test_edgeql_migration_recovery_in_script(self):\n        await self.migrate(\"\"\"\n            type Base;\n        \"\"\")\n        await self.con.execute(\"\"\"\n            SET MODULE test;\n\n            INSERT Base;\n        \"\"\")\n        res = await self.con.query(r\"\"\"\n            CREATE TYPE Bar;\n            START MIGRATION TO {\n                module test {\n                    type Base {\n                        required property name -> str;\n                    }\n                }\n            };\n            POPULATE MIGRATION;\n            ABORT MIGRATION;\n            SELECT Bar;\n        \"\"\")\n        self.assertEqual(res, [])\n\n        await self.migrate('')\n\n    async def test_edgeql_migration_recovery_commit_fail(self):\n        con2 = await self.connect()\n        try:\n            with con2.capture_warnings():\n                await con2.execute('START MIGRATION TO {}')\n            await con2.execute('POPULATE MIGRATION')\n\n            await self.migrate(\"type Base;\")\n\n            with self.assertRaises(edgedb.TransactionError):\n                await con2.execute(\"COMMIT MIGRATION\")\n\n            await con2.execute(\"ROLLBACK\")\n\n            self.assertEqual(await con2.query_single(\"SELECT 1\"), 1)\n        finally:\n            await con2.aclose()\n\n    async def test_edgeql_migration_reset_schema(self):\n        await self.migrate(r'''\n            type Bar;\n\n            alias Alias := Bar {val := 42};\n        ''')\n        await self.migrate(r'''\n            type Foo {\n                property name -> str;\n                link comp := Bar;\n            };\n\n            type Bar;\n        ''')\n\n        res = await self.con.query('''\n            select schema::ObjectType { name } filter .name ilike 'test::%'\n        ''')\n        self.assertEqual(len(res), 2)\n\n        await self.con.query('reset schema to initial')\n        await self.assert_last_migration()\n\n        res = await self.con.query('''\n            select schema::ObjectType { name } filter .name ilike 'test::%'\n        ''')\n        self.assertEqual(res, [])\n\n        res = await self.con.query('''\n            select schema::Migration { script, name };\n        ''')\n        self.assertEqual(res, [])\n\n        await self.migrate(r'''\n            type SomethingElse;\n        ''')\n\n        res = await self.con.query('''\n            select schema::Migration { script, name };\n        ''')\n        self.assertEqual(len(res), 1)\n\n    async def test_edgeql_migration_extension_01(self):\n        # Test migrations getting from an array in integers to vector and then\n        # revert to an array of floats.\n\n        await self.migrate('''\n            module default {\n                type Foo {\n                    required data: array<int64>\n                }\n            }\n        ''', explicit_modules=True)\n\n        # Populate with some data.\n        await self.con.execute('''\n            insert Foo {data := [3, 1, 4]};\n            insert Foo {data := [5, 6, 0]};\n        ''')\n\n        # Add vector and migrate automatically (because we have an assignment\n        # cast available from array<int64> to vector).\n        await self.migrate('''\n            using extension pgvector version '0.5';\n\n            module default {\n                scalar type v3 extending ext::pgvector::vector<3>;\n                type Foo {\n                    property data: v3;\n                };\n            }\n        ''', explicit_modules=True)\n\n        await self.assert_query_result(\n            r\"\"\"\n                select Foo.data\n            \"\"\",\n            tb.bag([[3, 1, 4], [5, 6, 0]]),\n            json_only=True,\n        )\n\n        await self.migrate(\n            '''\n            module default {\n                type Foo {\n                    property data: array<float32>;\n                };\n            }\n            ''',\n            explicit_modules=True,\n            # This migration needs the conversion expression\n            user_input=[\"<array<float32>>.data\"]\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                select Foo.data\n            \"\"\",\n            tb.bag([[3, 1, 4], [5, 6, 0]]),\n        )\n\n    async def _test_schema_repair(\n        self, *,\n        schema,\n        setup_script=None,\n        breakage_script,\n        check_breakage=True,\n    ):\n        '''Helper for testing ADMINISTER schema_repair()\n\n        Takes a schema and an optional setup_script and runs them.\n\n        Then runs the breakage_script, which will be run with access\n        to the reflschema and which should break the schema in some\n        way.\n\n        We will then load back the broken schema, verify migrations\n        are broken, run schema_repair, and verify migrations work.\n        '''\n\n        await self.migrate(schema, module='default')\n        if setup_script:\n            await self.con.execute(setup_script)\n\n        # Use a transaction to make sure the configures get rolled back\n        async with self.con.transaction():\n            # Enable poking directly at the schema\n            await self.con.execute('''\n                configure session set __internal_query_reflschema := true;\n                configure session set __internal_no_apply_query_rewrites :=\n                    true;\n            ''')\n\n            # Break the schema\n            await self.con.execute(breakage_script)\n\n        # Do some random configure current database in order to force\n        # a reload of the schema from reflection.\n        await self.con.execute(\"\"\"\n            configure current database reset __internal_sess_testvalue\n        \"\"\")\n\n        # Make sure we succesfully busted the schema.\n        async with self.assertRaisesRegexTx(\n            # Error could be the 'complete' assertion in this test\n            # suite or some failure in the server.\n            (AssertionError, edgedb.EdgeDBError),\n            '',\n        ):\n            await self.migrate(schema, module='default')\n\n        await self.con.execute('''\n            administer schema_repair()\n        ''')\n\n        # Make sure the schema is repaired\n        async with self._run_and_rollback():\n            await self.start_migration(schema, module='default')\n            await self.assert_describe_migration({\n                'complete': True\n            })\n\n        # Make sure objects still can be fetched\n        await self.con.query('''\n            select Object\n        ''')\n\n    async def test_edgeql_migration_schema_repair_01(self):\n        await self._test_schema_repair(\n            schema='''\n                type T {\n                    lol := count(Object);\n                    foo: str;\n                }\n                type S extending T;\n            ''',\n            setup_script='''\n                insert T { foo := \"t\" };\n                insert S { foo := \"s\" };\n            ''',\n            breakage_script='''\n                update schema::Property\n                filter .name__internal = 'default::__|foo@default|S'\n                set { required := true };\n            ''',\n        )\n\n        # Check that a query works and the data is still there\n        await self.assert_query_result(\n            '''\n                select T { foo };\n            ''',\n            tb.bag([\n                {'foo': 't'},\n                {'foo': 's'},\n            ])\n        )\n\n    async def test_edgeql_migration_schema_repair_02(self):\n        await self._test_schema_repair(\n            schema='''\n            abstract type Named {\n                index std::fts::index on (\n                    std::fts::with_options(\n                        .name, language := std::fts::Language.eng));\n                required name: std::str;\n            };\n\n            type Project0 extending Named;\n            ''',\n            breakage_script='''\n                update schema::Index\n                filter .name__internal =\n                'default::std|fts|index@default|Project0' ++\n                '@bb75b87f16a631d2bd48c6c660d4b52a8b7274cb'\n                set { name__internal :=\n                    'default::fts|index@default|Project0' ++\n                    '@bb75b87f16a631d2bd48c6c660d4b52a8b7274cb'\n                }\n            ''',\n        )\n\n\nclass EdgeQLAIMigrationTestCase(EdgeQLDataMigrationTestCase):\n    # AI specific tests, just because creating the extension is so slow,\n    # so we want to skip doing that each time.\n\n    DEFAULT_MODULE = 'default'\n    PARALLELISM_GRANULARITY = 'default'\n\n    SETUP = '''\n        create extension pgvector;\n        create extension ai;\n        CONFIGURE CURRENT DATABASE\n        INSERT ext::ai::OpenAIProviderConfig {\n            secret := 'very secret',\n            api_url := '',\n        };\n\n        CONFIGURE CURRENT DATABASE\n            SET ext::ai::Config::indexer_naptime := <duration>'100ms';\n    '''\n\n    async def test_edgeql_migration_ai_01(self):\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n              type Astronomy {\n                content: str;\n                deferred index ext::ai::index(\n                  embedding_model := 'text-embedding-3-small'\n                ) on (.content);\n              }\n            };\n        ''', explicit_modules=True)\n\n        await self.migrate('''\n            using extension ai;\n        ''', explicit_modules=True)\n\n    async def test_edgeql_migration_ai_02(self):\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n              type Astronomy {\n                content: str;\n                deferred index ext::ai::index(\n                  embedding_model := 'text-embedding-3-small'\n                ) on (.content);\n              }\n            };\n        ''', explicit_modules=True)\n\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n              type Astronomy {\n                content: str;\n                deferred index ext::ai::index(\n                  embedding_model := 'text-embedding-3-large'\n                ) on (.content);\n              }\n            };\n        ''', explicit_modules=True)\n\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n              type Astronomy {\n                content: str;\n                deferred index ext::ai::index(\n                  embedding_model := 'text-embedding-3-small'\n                ) on (.content);\n              }\n            };\n        ''', explicit_modules=True)\n\n    async def test_edgeql_migration_ai_03(self):\n        schema = '''\n        using extension ai;\n\n        module default {\n            type TestEmbeddingModel\n                extending ext::ai::EmbeddingModel\n            {\n                annotation ext::ai::model_name := \"text-embedding-test\";\n                annotation ext::ai::model_provider := \"custom::test\";\n                annotation ext::ai::embedding_model_max_batch_tokens := \"16384\";\n                annotation ext::ai::embedding_model_max_output_dimensions\n                  := \"10\";\n                annotation ext::ai::embedding_model_supports_shortening\n                  := \"true\";\n            };\n\n            type Astronomy {\n                content: str;\n                deferred index ext::ai::index(\n                    embedding_model := 'text-embedding-test'\n                ) on (.content);\n            };\n        };\n        '''\n\n        with self.assertRaisesRegex(\n            edgedb.SchemaDefinitionError,\n            \"object type 'default::TestEmbeddingModel' is missing a value \"\n            \"for the 'ext::ai::embedding_model_max_input_tokens' annotation\"\n        ):\n            await self.migrate(schema, explicit_modules=True)\n\n    @test.xfail('''\n        No more \"proposed\", but not \"completed\" either.\n\n        It would be OK if we produced a real error here too, though.\n    ''')\n    async def test_edgeql_migration_ai_04(self):\n        # Try changing embedding_model_max_output_dimensions on a\n        # custom EmbeddingModel\n\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n                type TestEmbeddingModel\n                    extending ext::ai::EmbeddingModel\n                {\n                    annotation ext::ai::model_name := \"text-embedding-test\";\n                    annotation ext::ai::model_provider := \"custom::test\";\n                    annotation ext::ai::embedding_model_max_input_tokens\n                      := \"8191\";\n                    annotation ext::ai::embedding_model_max_batch_tokens\n                      := \"16384\";\n                    annotation ext::ai::embedding_model_max_output_dimensions\n                      := \"10\";\n                    annotation ext::ai::embedding_model_supports_shortening\n                      := \"true\";\n                };\n\n                type Astronomy {\n                    content: str;\n                    deferred index ext::ai::index(\n                        embedding_model := 'text-embedding-test'\n                    ) on (.content);\n                };\n            };\n        ''', explicit_modules=True)\n\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n                type TestEmbeddingModel\n                    extending ext::ai::EmbeddingModel\n                {\n                    annotation ext::ai::model_name := \"text-embedding-test\";\n                    annotation ext::ai::model_provider := \"custom::test\";\n                    annotation ext::ai::embedding_model_max_input_tokens\n                      := \"8191\";\n                    annotation ext::ai::embedding_model_max_batch_tokens\n                      := \"16384\";\n                    annotation ext::ai::embedding_model_max_output_dimensions\n                      := \"20\";\n                    annotation ext::ai::embedding_model_supports_shortening\n                      := \"true\";\n                };\n\n                type Astronomy {\n                    content: str;\n                    deferred index ext::ai::index(\n                        embedding_model := 'text-embedding-test'\n                    ) on (.content);\n                };\n            };\n        ''', explicit_modules=True)\n\n    @test.xerror('''\n        \"undefined embedding model: no subtype of ext::ai::EmbeddingModel is\n        annotated as 'text-embedding-test'\"\n    ''')\n    async def test_edgeql_migration_ai_05(self):\n        # Try changing the name of a model (and the type, too, though\n        # that is not hugely relevant.)\n\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n                type TestEmbeddingModel\n                    extending ext::ai::EmbeddingModel\n                {\n                    annotation ext::ai::model_name := \"text-embedding-test\";\n                    annotation ext::ai::model_provider := \"custom::test\";\n                    annotation ext::ai::embedding_model_max_input_tokens\n                      := \"8191\";\n                    annotation ext::ai::embedding_model_max_batch_tokens\n                      := \"16384\";\n                    annotation ext::ai::embedding_model_max_output_dimensions\n                      := \"10\";\n                    annotation ext::ai::embedding_model_supports_shortening\n                      := \"true\";\n                };\n\n                type Astronomy {\n                    content: str;\n                    deferred index ext::ai::index(\n                        embedding_model := 'text-embedding-test'\n                    ) on (.content);\n                };\n            };\n        ''', explicit_modules=True)\n\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n                type TestEmbeddingModel2\n                    extending ext::ai::EmbeddingModel\n                {\n                    annotation ext::ai::model_name := \"text-embedding-test-2\";\n                    annotation ext::ai::model_provider := \"custom::test\";\n                    annotation ext::ai::embedding_model_max_input_tokens\n                      := \"8191\";\n                    annotation ext::ai::embedding_model_max_batch_tokens\n                      := \"16384\";\n                    annotation ext::ai::embedding_model_max_output_dimensions\n                      := \"10\";\n                    annotation ext::ai::embedding_model_supports_shortening\n                      := \"true\";\n                };\n\n                type Astronomy {\n                    content: str;\n                    deferred index ext::ai::index(\n                        embedding_model := 'text-embedding-test'\n                    ) on (.content);\n                };\n            };\n        ''', explicit_modules=True)\n\n    async def test_edgeql_migration_ai_06(self):\n        # Try putting the EmbeddingModel declaration below the type\n        # that uses it.\n\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n                type Astronomy {\n                    content: str;\n                    deferred index ext::ai::index(\n                        embedding_model := 'text-embedding-test'\n                    ) on (.content);\n                };\n\n                type TestEmbeddingModel\n                    extending ext::ai::EmbeddingModel\n                {\n                    annotation ext::ai::model_name := \"text-embedding-test\";\n                    annotation ext::ai::model_provider := \"custom::test\";\n                    annotation ext::ai::embedding_model_max_input_tokens\n                      := \"8191\";\n                    annotation ext::ai::embedding_model_max_batch_tokens\n                      := \"16384\";\n                    annotation ext::ai::embedding_model_max_output_dimensions\n                      := \"10\";\n                    annotation ext::ai::embedding_model_supports_shortening\n                      := \"true\";\n                };\n            };\n        ''', explicit_modules=True)\n\n    async def test_edgeql_migration_ai_07a(self):\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n                type Astronomy {\n                    content: str;\n                    deferred index ext::ai::index(\n                        embedding_model := 'text-embedding-3-small'\n                    ) on (.content);\n                };\n\n                type Sub extending Astronomy {\n                    deferred index ext::ai::index(\n                        embedding_model := 'text-embedding-3-small'\n                    ) on (.content);\n                };\n\n            };\n        ''', explicit_modules=True)\n\n        await self.migrate('''\n            using extension ai;\n        ''', explicit_modules=True)\n\n    async def test_edgeql_migration_ai_07b(self):\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n                type Astronomy {\n                    content: str;\n                    deferred index ext::ai::index(\n                        embedding_model := 'text-embedding-3-small'\n                    ) on (.content);\n                };\n\n                type Sub extending Astronomy;\n\n            };\n        ''', explicit_modules=True)\n\n        await self.assert_query_result(\n            r\"\"\"\n                select schema::Index {\n                    annotations: {name, @value},\n                    subject_name := .<indexes[is schema::ObjectType].name\n                }\n                filter 'ext::ai::index' IN .ancestors.name\n                and .subject_name = 'default::Sub';\n            \"\"\",\n            [\n                {\n                    \"annotations\": tb.bag([\n                        {\"name\": \"ext::ai::model_name\",\n                         \"@value\": \"text-embedding-3-small\"},\n                        {\"name\": \"ext::ai::model_provider\",\n                         \"@value\": \"builtin::openai\"},\n                        {\"name\": \"ext::ai::embedding_model_max_input_tokens\",\n                         \"@value\": \"8191\"},\n                        {\"name\": \"ext::ai::embedding_model_max_batch_tokens\",\n                         \"@value\": \"8191\"},\n                        {\n                            \"name\":\n                            \"ext::ai::embedding_model_max_output_dimensions\",\n                            \"@value\": \"1536\"\n                        },\n                        {\"name\": \"ext::ai::embedding_model_supports_shortening\",\n                         \"@value\": \"true\"},\n                        {\"name\": \"ext::ai::embedding_dimensions\",\n                         \"@value\": \"1536\"},\n                        {'name': 'ext::ai::embedding_model_max_batch_size',\n                         '@value': '<optional>'},\n                    ]),\n                    \"subject_name\": \"default::Sub\"\n                }\n            ]\n        )\n\n        await self.migrate('''\n            using extension ai;\n        ''', explicit_modules=True)\n\n    async def test_edgeql_migration_ai_07c(self):\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n                type Astronomy {\n                    content: str;\n                    deferred index ext::ai::index(\n                        embedding_model := 'text-embedding-3-small'\n                    ) on (.content);\n                };\n\n                type Sub extending Astronomy {\n                };\n\n            };\n        ''', explicit_modules=True)\n\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n                type Astronomy {\n                    content: str;\n                    deferred index ext::ai::index(\n                        embedding_model := 'text-embedding-3-small'\n                    ) on (.content);\n                };\n\n                type Sub extending Astronomy {\n                    deferred index ext::ai::index(\n                        embedding_model := 'text-embedding-3-small'\n                    ) on (.content);\n                };\n\n            };\n        ''', explicit_modules=True)\n\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n                type Astronomy {\n                    content: str;\n                    deferred index ext::ai::index(\n                        embedding_model := 'text-embedding-3-small'\n                    ) on (.content);\n                };\n\n                type Sub extending Astronomy {\n                };\n\n            };\n        ''', explicit_modules=True)\n\n        await self.migrate('''\n            using extension ai;\n        ''', explicit_modules=True)\n\n    async def test_edgeql_migration_ai_08(self):\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n                type Base {\n                    content: str;\n                    deferred index ext::ai::index(\n                        embedding_model := 'text-embedding-3-small'\n                    ) on (.content);\n                };\n\n                type Sub extending Base {\n                    # deferred index ext::ai::index(\n                    #     embedding_model := 'text-embedding-3-small'\n                    # ) on (.content ++ '!');\n                };\n\n            };\n        ''', explicit_modules=True)\n\n        arg = [0.0] * 1536\n        await self.con.query('''\n            select {\n                base := ext::ai::search(Base, <array<float32>>$0),\n                sub := ext::ai::search(Sub, <array<float32>>$0),\n            }\n        ''', arg)\n\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n                type Base {\n                    content: str;\n                    deferred index ext::ai::index(\n                        embedding_model := 'text-embedding-3-small'\n                    ) on (.content);\n                };\n\n                type Sub extending Base {\n                    deferred index ext::ai::index(\n                        embedding_model := 'text-embedding-3-small'\n                    ) on (.content ++ '!');\n                };\n\n            };\n        ''', explicit_modules=True)\n\n        await self.con.query('''\n            select {\n                base := ext::ai::search(Base, <array<float32>>$0),\n                sub := ext::ai::search(Sub, <array<float32>>$0),\n            }\n        ''', arg)\n\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n                type Base {\n                    content: str;\n                };\n\n                type Sub extending Base {\n                    deferred index ext::ai::index(\n                        embedding_model := 'text-embedding-3-small'\n                    ) on (.content ++ '!');\n                };\n\n            };\n        ''', explicit_modules=True)\n\n        # Base lost the index, just select Sub\n        await self.con.query('''\n            select {\n                sub := ext::ai::search(Sub, <array<float32>>$0),\n            }\n        ''', arg)\n\n    async def test_edgeql_migration_ai_09(self):\n        with self.assertRaisesRegex(\n            edgedb.InvalidDefinitionError,\n            r\"with different parameters than on base type\",\n        ):\n            await self.migrate('''\n                using extension ai;\n\n                module default {\n                    type Base {\n                        content: str;\n                        deferred index ext::ai::index(\n                            embedding_model := 'text-embedding-3-small'\n                        ) on (.content);\n                    };\n\n                    type Sub extending Base {\n                        deferred index ext::ai::index(\n                            embedding_model := 'text-embedding-3-large'\n                        ) on (.content ++ '!');\n                    };\n\n                };\n            ''', explicit_modules=True)\n\n    async def test_edgeql_migration_ai_10(self):\n        # EmbeddingModel with default embedding_model_max_batch_tokens.\n\n        await self.migrate('''\n            using extension ai;\n\n            module default {\n                type TestEmbeddingModel\n                    extending ext::ai::EmbeddingModel\n                {\n                    annotation ext::ai::model_name := \"text-embedding-test\";\n                    annotation ext::ai::model_provider := \"custom::test\";\n                    annotation ext::ai::embedding_model_max_input_tokens\n                      := \"8191\";\n                    annotation ext::ai::embedding_model_max_output_dimensions\n                      := \"10\";\n                    annotation ext::ai::embedding_model_supports_shortening\n                      := \"true\";\n                };\n            };\n        ''', explicit_modules=True)\n\n        await self.assert_query_result(\n            r\"\"\"\n                with model := (\n                    select schema::ObjectType {\n                        x := (\n                            for ann in .annotations\n                            select ann@value\n                            filter ann.name\n                            = 'ext::ai::embedding_model_max_batch_tokens'\n                        )\n                    }\n                    filter .name = 'default::TestEmbeddingModel'\n                )\n                select model.x\n            \"\"\",\n            ['8191'],\n        )\n\n\nclass EdgeQLMigrationRewriteTestCase(EdgeQLDataMigrationTestCase):\n    DEFAULT_MODULE = 'default'\n\n    async def migrate(\n        self,\n        migration: str,\n        *,\n        module: str | None = 'default',\n        **kwargs,\n    ):\n        await super().migrate(migration, module=module, **kwargs)\n\n    async def get_migrations(self):\n        res = await self.con.query(\n            '''\n            select schema::Migration {\n                id, name, script, sdl, parents: {name, id}, generated_by\n            }\n            '''\n        )\n        if not res:\n            return []\n        children = {m.parents[0].id: m for m in res if m.parents}\n        root = [m for m in res if not m.parents][0]\n\n        sorted_migs = []\n        while root:\n            sorted_migs.append(root)\n            root = children.get(root.id)\n\n        return sorted_migs\n\n    async def assert_migration_history(self, exp_result):\n        res = await self.get_migrations()\n        res = serutils.serialize(res)\n        assert_data_shape.assert_data_shape(\n            res, exp_result, self.fail)\n        await self.assert_last_migration()\n\n\nclass TestEdgeQLMigrationRewrite(EdgeQLMigrationRewriteTestCase):\n    # N.B: These test cases get duplicated as nonisolated test cases,\n    # to verify that it all works *outside* a transaction also.\n    # If that is a problem, a test case can be made to skip it\n\n    async def test_edgeql_migration_rewrite_01(self):\n        await self.con.execute('''\n            CONFIGURE SESSION SET store_migration_sdl :=\n                cfg::StoreMigrationSDL.AlwaysStore;\n        ''')\n\n        # Split one migration up into several\n        await self.migrate(r\"\"\"\n            type A;\n            type B;\n            type C;\n            type D;\n        \"\"\")\n        # Try a bunch of different ways to do it!\n        await self.con.execute(r\"\"\"\n            start migration rewrite;\n            start migration to {\n                module default {\n                    type A;\n                }\n            };\n            populate migration;\n            commit migration;\n\n            start migration to {\n                module default {\n                    type A;\n                    type B;\n                }\n            };\n            CREATE type B;\n            commit migration;\n\n            create migration {\n                create type C;\n            };\n\n            CREATE TYPE D;\n\n            commit migration rewrite;\n        \"\"\")\n\n        await self.assert_migration_history([\n            {\n                'script': 'CREATE TYPE default::A;',\n                'sdl': (\n                    'module default {\\n'\n                    '    type A;\\n'\n                    '};'\n                ),\n                'generated_by': None,\n            },\n            {\n                'script': 'CREATE TYPE B;',\n                'sdl': (\n                    'module default {\\n'\n                    '    type A;\\n'\n                    '    type B;\\n'\n                    '};'\n                ),\n                'generated_by': None,\n            },\n            {\n                'script': 'create type C;',\n                'sdl': (\n                    'module default {\\n'\n                    '    type A;\\n'\n                    '    type B;\\n'\n                    '    type C;\\n'\n                    '};'\n                ),\n                'generated_by': None,\n            },\n            {\n                'script': (\n                    'SET generated_by '\n                    ':= (schema::MigrationGeneratedBy.DDLStatement);\\n'\n                    'CREATE TYPE D;'\n                ),\n                'sdl': (\n                    'module default {\\n'\n                    '    type A;\\n'\n                    '    type B;\\n'\n                    '    type C;\\n'\n                    '    type D;\\n'\n                    '};'\n                ),\n                'generated_by': 'DDLStatement',\n            },\n        ])\n\n    async def test_edgeql_migration_rewrite_02(self):\n        await self.con.execute('''\n            CONFIGURE SESSION SET store_migration_sdl :=\n                cfg::StoreMigrationSDL.AlwaysStore;\n        ''')\n\n        # Simulate a potential migration squashing flow from the CLI,\n        # where we generate a script using start migration and then apply it\n        # later.\n        await self.con.execute(r\"\"\"\n            create type Foo;\n            create type Tgt;\n            alter type Foo { create link tgt -> Tgt; };\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            start migration rewrite;\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            start migration to committed schema;\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            populate migration;\n        \"\"\")\n        res = json.loads(await self.con.query_json(r\"\"\"\n            describe current migration as json;\n        \"\"\"))\n\n        await self.con.execute(r\"\"\"\n            commit migration;\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            abort migration rewrite;\n        \"\"\")\n\n        self.assertTrue(res[0]['complete'])\n        commands = '\\n'.join(res[0]['confirmed'])\n        script = textwrap.dedent('''\\\n            start migration rewrite;\n            create migration {\n            %s\n            };\n            commit migration rewrite;\n        ''') % textwrap.indent(commands, ' ' * 4)\n\n        await self.con.execute(script)\n\n        await self.assert_migration_history([\n            {\n                'script': commands,\n                'sdl': (\n                    'module default {\\n'\n                    '    type Foo {\\n'\n                    '        link tgt: default::Tgt;\\n'\n                    '    };\\n'\n                    '    type Tgt;\\n'\n                    '};'\n                ),\n            }\n        ])\n\n    async def test_edgeql_migration_rewrite_03(self):\n        await self.con.execute('''\n            CONFIGURE SESSION SET store_migration_sdl :=\n                cfg::StoreMigrationSDL.AlwaysStore;\n        ''')\n\n        # Test rolling back to a savepoint after a commit failure\n        await self.con.execute(r\"\"\"\n            create type A;\n            create type B;\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            start migration rewrite;\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            declare savepoint s0;\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            create type B;\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            declare savepoint s1;\n        \"\"\")\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r\"does not match\"):\n            await self.con.execute(r\"\"\"\n                commit migration rewrite;\n            \"\"\")\n\n        # Rollback and try again\n        await self.con.execute(r\"\"\"\n            rollback to savepoint s1;\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            create type A;\n        \"\"\")\n        await self.con.execute(r\"\"\"\n            commit migration rewrite\n        \"\"\")\n\n        gby = (\n            'SET generated_by := (schema::MigrationGeneratedBy.DDLStatement);'\n        )\n        await self.assert_migration_history([\n            {\n                'script': gby + '\\n' + 'CREATE TYPE B;',\n                'sdl': (\n                    'module default {\\n'\n                    '    type B;\\n'\n                    '};'\n                ),\n            },\n            {\n                'script': gby + '\\n' + 'CREATE TYPE A;',\n                'sdl': (\n                    'module default {\\n'\n                    '    type A;\\n'\n                    '    type B;\\n'\n                    '};'\n                ),\n            },\n        ])\n\n    async def test_edgeql_migration_rewrite_05(self):\n        # Test ABORT MIGRATION REWRITE\n        await self.con.execute(r\"\"\"\n            create type A;\n            create type B;\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            start migration rewrite;\n        \"\"\")\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r\"does not match\"):\n            await self.con.execute(r\"\"\"\n                commit migration rewrite;\n            \"\"\")\n        await self.con.execute(r\"\"\"\n            abort migration rewrite;\n        \"\"\")\n\n    async def test_edgeql_migration_rewrite_06(self):\n        await self.con.execute('''\n            CONFIGURE SESSION SET store_migration_sdl :=\n                cfg::StoreMigrationSDL.AlwaysStore;\n        ''')\n\n        # Test doing the interactive migration flow\n        await self.con.execute(r\"\"\"\n            create type A;\n            create type B;\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            start migration rewrite;\n        \"\"\")\n\n        await self.start_migration(r\"\"\"\n            type A;\n            type B;\n        \"\"\", module='default')\n        await self.fast_forward_describe_migration()\n\n        await self.con.execute(r\"\"\"\n            commit migration rewrite;\n        \"\"\")\n\n        await self.assert_migration_history([\n            {\n                'script': 'CREATE TYPE default::A;\\nCREATE TYPE default::B;',\n                'sdl': (\n                    'module default {\\n'\n                    '    type A;\\n'\n                    '    type B;\\n'\n                    '};'\n                ),\n            },\n        ])\n\n    async def test_edgeql_migration_preexisting_01(self):\n        await self.con.execute(\"create type Foo;\")\n        with self.assertRaisesRegex(\n            edgedb.InvalidReferenceError,\n            r\"type 'default::Foo' does not exist\"\n        ):\n            await self.start_migration(\"\"\"\n                type Bar {\n                    baz: Foo\n                }\n            \"\"\", module='default')\n\n\nclass TestEdgeQLMigrationRewriteNonisolated(TestEdgeQLMigrationRewrite):\n    # N.B: This test suite duplicates all the tests in the above\n    # TestEdgeQLMigrationRewrite, but not in transactions.\n    TRANSACTION_ISOLATION = False\n\n    PER_TEST_TEARDOWN = [\n        'rollback;',  # just in case, avoid extra errors\n        '''\n            start migration to { module default {}; };\n            populate migration;\n            commit migration;\n\n            start migration rewrite;\n            commit migration rewrite;\n        ''',\n    ]\n\n    async def test_edgeql_migration_rewrite_raw_01(self):\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r\"Cannot leave an incomplete migration rewrite in scripts\"):\n            await self.con.execute(r\"\"\"\n                START MIGRATION REWRITE;\n                START MIGRATION TO {\n                    module default {\n                        type A;\n                    }\n                };\n                POPULATE MIGRATION;\n                COMMIT MIGRATION;\n            \"\"\")\n\n    @unittest.skipIf(\n        True,\n        \"\"\"\n        This test is still pretty slow\n        \"\"\"\n    )\n    async def test_edgeql_migration_rewrite_raw_02(self):\n        for _ in range(1200):\n            await self.con.execute(r\"\"\"\n                create applied migration { }\n            \"\"\")\n"
  },
  {
    "path": "tests/test_edgeql_datatypes.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2012-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file exceptionsept in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom datetime import timedelta\n\nimport edgedb\nimport decimal\nimport immutables\n\nfrom edb import errors\nfrom edb.ir import statypes\nfrom edb.server import config\nfrom edb.testbase import server as tb\n\n\nclass TestEdgeQLDT(tb.QueryTestCase):\n    SETUP = '''\n        START MIGRATION TO {\n            module default {\n                scalar type seq_t extending sequence;\n                scalar type seq2_t extending sequence;\n                scalar type enum_t extending enum<'foo', 'bar'>;\n\n                type Obj {\n                    property seq_prop -> seq_t;\n                };\n\n                type Obj2 {\n                    property seq_prop -> seq2_t;\n                };\n            };\n        };\n        POPULATE MIGRATION;\n        COMMIT MIGRATION;\n    '''\n\n    async def test_edgeql_dt_realativedelta_01(self):\n        await self.assert_query_result(\n            r\"SELECT <cal::relative_duration>'1 year 2 seconds'\",\n            ['P1YT2S'],\n            [edgedb.RelativeDuration(months=12, microseconds=2_000_000)],\n        )\n\n        await self.assert_query_result(\n            r\"SELECT <str><cal::relative_duration>'1 year 2 seconds'\",\n            ['P1YT2S'],\n        )\n\n        await self.assert_query_result(\n            r\"SELECT <json><cal::relative_duration><json>'1 year 2 seconds'\",\n            ['P1YT2S'],\n            ['\"P1YT2S\"'],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH\n                dt := <datetime>'2000-01-01T00:00:00Z',\n                rd := <cal::relative_duration>'3 years 2 months 14 days'\n            SELECT (dt + rd, rd + dt, dt - rd)\n            \"\"\",\n            [(\n                '2003-03-15T00:00:00+00:00',\n                '2003-03-15T00:00:00+00:00',\n                '1996-10-18T00:00:00+00:00',\n            )],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH\n                dt := <cal::local_datetime>'2000-01-01T00:00:00',\n                rd := <cal::relative_duration>'3 years 2 months 14 days'\n            SELECT (dt + rd, rd + dt, dt - rd)\n            \"\"\",\n            [(\n                '2003-03-15T00:00:00',\n                '2003-03-15T00:00:00',\n                '1996-10-18T00:00:00',\n            )],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH\n                d := <cal::local_date>'2000-01-01',\n                rd := <cal::relative_duration>'3 years 2 months 14 days'\n            SELECT (d + rd, rd + d, d - rd)\n            \"\"\",\n            [('2003-03-15T00:00:00',\n              '2003-03-15T00:00:00',\n              '1996-10-18T00:00:00')],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH\n                t := <cal::local_time>'00:00:00',\n                rd := <cal::relative_duration>'3h2m1s'\n            SELECT (t + rd, rd + t, t - rd)\n            \"\"\",\n            [('03:02:01', '03:02:01', '20:57:59')],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH rd := <cal::relative_duration>'3h2m1s'\n            SELECT (\n                rd = rd, rd ?= rd,\n                rd != rd, rd ?!= rd,\n                rd > rd, rd >= rd,\n                rd < rd, rd <= rd,\n                rd + rd, rd - rd,\n                -rd,\n            )\n            \"\"\",\n            [(\n                True, True,\n                False, False,\n                False, True,\n                False, True,\n                'PT6H4M2S', 'PT0S',\n                'PT-3H-2M-1S',\n            )],\n            [(\n                True, True,\n                False, False,\n                False, True,\n                False, True,\n                edgedb.RelativeDuration(microseconds=21_842_000_000),\n                edgedb.RelativeDuration(),\n                edgedb.RelativeDuration(microseconds=-10_921_000_000),\n            )],\n        )\n\n        await self.assert_query_result(\n            r\" SELECT <json><cal::relative_duration>'3y2h' \",\n            ['P3YT2H'],\n            ['\"P3YT2H\"'],\n        )\n\n        await self.assert_query_result(\n            r\" SELECT <cal::relative_duration><json>'P3YT2H' \",\n            ['P3YT2H'],\n            [edgedb.RelativeDuration(months=36, microseconds=7200000000)],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT (\n                to_str(\n                    <cal::relative_duration>'3y' +\n                    <cal::relative_duration>'1h'\n                ),\n                to_str(<cal::relative_duration>'3y1h', 'YYYY\"y\"HH24\"h\"'),\n            )\n            \"\"\",\n            [['P3YT1H', '0003y01h']],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT cal::to_relative_duration(\n                years := 1,\n                months := 2,\n                days := 3,\n                hours := 4,\n                minutes := 5,\n                seconds := 6,\n                microseconds := 7,\n            )\n            \"\"\",\n            ['P1Y2M3DT4H5M6.000007S'],\n            [edgedb.RelativeDuration(months=14, days=3,\n                                     microseconds=14706000007)],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH\n                x := <cal::relative_duration>'1y',\n                y := <cal::relative_duration>'5y',\n            SELECT (\n                max({x, y}),\n                min({x, y}),\n            )\n            \"\"\",\n            [['P5Y', 'P1Y']],\n            [(\n                edgedb.RelativeDuration(months=60),\n                edgedb.RelativeDuration(months=12),\n            )]\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH\n                rd := <cal::relative_duration>'1s',\n                d := <duration>'5s',\n            SELECT (<duration>rd, <cal::relative_duration>d)\n            \"\"\",\n            [['PT1S', 'PT5S']],\n            [(\n                timedelta(seconds=1),\n                edgedb.RelativeDuration(microseconds=5_000_000),\n            )]\n        )\n\n        with self.assertRaisesRegex(\n                edgedb.InvalidValueError,\n                \"invalid value for scalar type 'std::duration'\"):\n            await self.con.query(r\"\"\"\n                WITH rd := <cal::relative_duration>'1y'\n                SELECT <duration>rd\n                \"\"\")\n\n    async def test_edgeql_dt_realativedelta_02(self):\n        await self.assert_query_result(\n            r\"SELECT <str><cal::date_duration>'1 year 2 days'\",\n            ['P1Y2D'],\n        )\n\n        await self.assert_query_result(\n            r\"SELECT <json><cal::date_duration><json>'1 year 2 days'\",\n            ['P1Y2D'],\n            ['\"P1Y2D\"'],\n        )\n\n        await self.assert_query_result(\n            r\"SELECT <str><cal::date_duration>'0 days'\",\n            ['P0D'],\n        )\n\n        await self.assert_query_result(\n            r\"SELECT <json><cal::date_duration>'0 days'\",\n            ['P0D'],\n            ['\"P0D\"'],\n        )\n\n        await self.assert_query_result(\n            r\"SELECT <json><cal::date_duration>'5 months -150 days'\",\n            ['P5M-150D'],\n            ['\"P5M-150D\"'],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH\n                dt := <datetime>'2000-01-01T00:00:00Z',\n                rd := <cal::date_duration>'3 years 2 months 14 days'\n            SELECT (dt + rd, rd + dt, dt - rd)\n            \"\"\",\n            [(\n                '2003-03-15T00:00:00+00:00',\n                '2003-03-15T00:00:00+00:00',\n                '1996-10-18T00:00:00+00:00',\n            )],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH\n                dt := <cal::local_datetime>'2000-01-01T00:00:00',\n                rd := <cal::date_duration>'3 years 2 months 14 days'\n            SELECT (dt + rd, rd + dt, dt - rd)\n            \"\"\",\n            [(\n                '2003-03-15T00:00:00',\n                '2003-03-15T00:00:00',\n                '1996-10-18T00:00:00',\n            )],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH\n                d := <cal::local_date>'2000-01-01',\n                rd := <cal::date_duration>'3 years 2 months 14 days'\n            SELECT (d + rd, rd + d, d - rd)\n            \"\"\",\n            [('2003-03-15', '2003-03-15', '1996-10-18')],\n        )\n\n        await self.assert_query_result(\n            r\" SELECT <json><cal::date_duration>'3y2d' \",\n            ['P3Y2D'],\n            ['\"P3Y2D\"'],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT (\n                to_str(\n                    <cal::date_duration>'3y' +\n                    <cal::date_duration>'1d'\n                ),\n                to_str(<cal::date_duration>'3y1d', 'YYYY\"y\"DD\"d\"'),\n            )\n            \"\"\",\n            [['P3Y1D', '0003y01d']],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            SELECT <str>cal::to_date_duration(\n                years := 1,\n                months := 2,\n                days := 3,\n            )\n            \"\"\",\n            ['P1Y2M3D'],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n            WITH\n                x := <cal::date_duration>'1y',\n                y := <cal::date_duration>'5y',\n            SELECT (\n                <str>max({x, y}),\n                <str>min({x, y}),\n            )\n            \"\"\",\n            [['P5Y', 'P1Y']],\n        )\n\n        with self.assertRaisesRegex(\n                edgedb.InvalidValueError,\n                \"invalid input syntax for type std::cal::date_duration: '1s'\"):\n            async with self.con.transaction():\n                await self.con.query(r\"\"\"\n                    SELECT <str><cal::date_duration>'1s'\n                    \"\"\")\n\n        with self.assertRaisesRegex(\n                edgedb.InvalidValueError,\n                \"invalid input syntax for type std::cal::date_duration: '1s'\"):\n            async with self.con.transaction():\n                await self.con.query(r\"\"\"\n                    SELECT <str><cal::date_duration><json>'1s'\n                    \"\"\")\n\n    async def test_edgeql_dt_datetime_01(self):\n        await self.assert_query_result(\n            r'''SELECT <datetime>'2017-10-10T00:00:00+00' +\n                <duration>'24 hours';''',\n            ['2017-10-11T00:00:00+00:00'],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <duration>'24 hours' +\n                <datetime>'2017-10-10 00:00:00+00';''',\n            ['2017-10-11T00:00:00+00:00'],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <datetime>'2017-10-10T00:00:00+00' -\n                <duration>'24 hours';''',\n            ['2017-10-09T00:00:00+00:00'],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT to_str(<duration>'24 hours' + <duration>'24 hours')''',\n            ['PT48H'],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT to_str(<duration>'4 hours' - <duration>'1 hour')''',\n            ['PT3H'],\n        )\n\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                \"operator '-' cannot be applied.*duration.*datetime\"):\n\n            await self.con.query(\"\"\"\n                SELECT <duration>'1 hour' - <datetime>'2017-10-10T00:00:00+00';\n            \"\"\")\n\n    async def test_edgeql_dt_datetime_02(self):\n        await self.assert_query_result(\n            r'''SELECT <str><datetime>'2017-10-10T00:00:00+00';''',\n            ['2017-10-10T00:00:00+00:00'],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <str>(<datetime>'2017-10-10T00:00:00+00' -\n                             <duration>'24 hours');\n            ''',\n            ['2017-10-09T00:00:00+00:00'],\n        )\n\n    async def test_edgeql_dt_datetime_03(self):\n        await self.assert_query_result(\n            r'''SELECT <tuple<str,datetime>>(\n                'foo', '2017-10-10T00:00:00+00');\n            ''',\n            [['foo', '2017-10-10T00:00:00+00:00']],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT (<tuple<str,datetime>>(\n                    'foo', '2017-10-10T00:00:00+00')).1 +\n                   <duration>'744 hours';\n            ''',\n            ['2017-11-10T00:00:00+00:00'],\n        )\n\n    async def test_edgeql_dt_datetime_04(self):\n        await self.assert_query_result(\n            r'''SELECT <datetime>'2017-10-11T00:00:00+00' -\n                <datetime>'2017-10-10T00:00:00+00';''',\n            ['PT24H'],\n            [timedelta(days=1)],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <datetime>'2018-10-10T00:00:00+00' -\n                <datetime>'2017-10-10T00:00:00+00';''',\n            ['PT8760H'],\n            [timedelta(days=365)],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <datetime>'2017-10-17T01:02:03.004005+00' -\n                <datetime>'2017-10-10T00:00:00+00';''',\n            ['PT169H2M3.004005S'],\n            [timedelta(days=7, seconds=3723, microseconds=4005)],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <datetime>'2017-10-10T01:02:03.004005-02' -\n                <datetime>'2017-10-10T00:00:00+00';''',\n            ['PT3H2M3.004005S'],\n            [timedelta(seconds=10923, microseconds=4005)],\n        )\n\n    async def test_edgeql_dt_duration_01_err(self):\n        with self.assertRaisesRegex(\n                edgedb.InvalidValueError,\n                \"invalid input syntax for type std::duration: '7 days'\"):\n            await self.con.execute(\"SELECT <duration>'7 days';\")\n\n    async def test_edgeql_dt_duration_02_err(self):\n        with self.assertRaisesRegex(\n                edgedb.InvalidValueError,\n                \"invalid input syntax for type std::duration: '13 months'\"):\n            await self.con.execute(\"SELECT <duration>'13 months';\")\n\n    async def test_edgeql_dt_duration_03_err(self):\n        with self.assertRaisesRegex(\n                edgedb.InvalidValueError,\n                \"invalid input syntax for type std::duration: '17 years'\"):\n            await self.con.execute(\"SELECT <duration>'17 years';\")\n\n    async def test_edgeql_dt_duration_04_err(self):\n        with self.assertRaisesRegex(\n                edgedb.InvalidValueError,\n                \"invalid input syntax for type std::duration: \"\n                \"'100 centuries'\"):\n            await self.con.execute(\"SELECT <duration>'100 centuries';\")\n\n    async def test_edgeql_dt_duration_05_err(self):\n        with self.assertRaisesRegex(\n                edgedb.InvalidValueError,\n                'invalid input syntax for type std::duration: \"100 cats\"'):\n            await self.con.execute(\"SELECT <duration>'100 cats';\")\n\n    async def test_edgeql_dt_duration_06_interval_style(self):\n        await self.assert_query_result(\n            r'''SELECT <duration>'-6h51m14.045854s';''',\n            ['PT-5H-8M-45.954146S'],\n            [-timedelta(seconds=18525, microseconds=954146)],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <duration>'-6h -51m -14.045854s';''',\n            ['PT-6H-51M-14.045854S'],\n            [-timedelta(seconds=24674, microseconds=45854)],\n        )\n\n    async def test_edgeql_dt_duration_07_datetime_range(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidValueError,\n            'value out of range',\n        ):\n            await self.con.execute(\n                \"\"\"\n                SELECT <datetime>'9999-12-31T00:00:00Z' + <duration>'30 hours'\n                \"\"\"\n            )\n\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidValueError,\n            'value out of range',\n        ):\n            await self.con.execute(\n                \"\"\"\n                SELECT <datetime>'0001-01-01T00:00:00Z' - <duration>'30 hours'\n                \"\"\"\n            )\n\n    async def test_edgeql_dt_duration_08_local_datetime_range(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidValueError,\n            'value out of range',\n        ):\n            await self.con.execute(\n                \"\"\"\n                SELECT\n                    <cal::local_datetime>'9999-12-31T00:00:00'\n                    + <duration>'30 hours'\n                \"\"\"\n            )\n\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidValueError,\n            'value out of range',\n        ):\n            await self.con.execute(\n                \"\"\"\n                SELECT\n                    <cal::local_datetime>'0001-01-01T00:00:00'\n                    - <duration>'30 hours'\n                \"\"\"\n            )\n\n    async def test_edgeql_dt_duration_09_local_date_range(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidValueError,\n            'value out of range',\n        ):\n            await self.con.execute(\n                \"\"\"\n                SELECT\n                    <cal::local_date>'9999-12-31'\n                    + <duration>'30 hours'\n                \"\"\"\n            )\n\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidValueError,\n            'value out of range',\n        ):\n            await self.con.execute(\n                \"\"\"\n                SELECT\n                    <cal::local_date>'0001-01-01'\n                    - <duration>'30 hours'\n                \"\"\"\n            )\n\n    async def test_edgeql_dt_duration_10_datetime_range(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidValueError,\n            'value out of range',\n        ):\n            await self.con.execute(\n                \"\"\"\n                SELECT <datetime>'9999-12-31T00:00:00Z' +\n                    <cal::relative_duration>'1 week'\n                \"\"\"\n            )\n\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidValueError,\n            'value out of range',\n        ):\n            await self.con.execute(\n                \"\"\"\n                SELECT <datetime>'0001-01-01T00:00:00Z' -\n                    <cal::relative_duration>'1 week'\n                \"\"\"\n            )\n\n    async def test_edgeql_dt_duration_11_local_datetime_range(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidValueError,\n            'value out of range',\n        ):\n            await self.con.execute(\n                \"\"\"\n                SELECT <cal::local_datetime>'9999-12-31T00:00:00' +\n                    <cal::relative_duration>'1 week'\n                \"\"\"\n            )\n\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidValueError,\n            'value out of range',\n        ):\n            await self.con.execute(\n                \"\"\"\n                SELECT <cal::local_datetime>'0001-01-01T00:00:00' -\n                    <cal::relative_duration>'1 week'\n                \"\"\"\n            )\n\n    async def test_edgeql_dt_duration_12_local_date_range(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidValueError,\n            'value out of range',\n        ):\n            await self.con.execute(\n                \"\"\"\n                SELECT\n                    <cal::local_date>'9999-12-31'\n                    + <cal::relative_duration>'30 hours'\n                \"\"\"\n            )\n\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidValueError,\n            'value out of range',\n        ):\n            await self.con.execute(\n                \"\"\"\n                SELECT\n                    <cal::local_date>'0001-01-01'\n                    - <cal::relative_duration>'30 hours'\n                \"\"\"\n            )\n\n    async def test_edgeql_dt_local_datetime_01(self):\n        await self.assert_query_result(\n            r'''\n                SELECT <cal::local_datetime>'2017-10-10T13:11' +\n                    <duration>'24 hours';\n            ''',\n            ['2017-10-11T13:11:00'],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <duration>'24 hours' +\n                    <cal::local_datetime>'2017-10-10T13:11';\n            ''',\n            ['2017-10-11T13:11:00'],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <cal::local_datetime>'2017-10-10T13:11' -\n                    <duration>'24 hours';\n            ''',\n            ['2017-10-09T13:11:00'],\n        )\n\n    async def test_edgeql_dt_local_datetime_02(self):\n        await self.assert_query_result(\n            r'''SELECT <cal::local_datetime>'2017-10-11T00:00:00' -\n                <cal::local_datetime>'2017-10-10T00:00:00';''',\n            ['P1D'],\n            [edgedb.RelativeDuration(days=1)],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <cal::local_datetime>'2018-10-10T00:00:00' -\n                <cal::local_datetime>'2017-10-10T00:00:00';''',\n            ['P365D'],\n            [edgedb.RelativeDuration(days=365)],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <cal::local_datetime>'2017-10-17T01:02:03.004005' -\n                <cal::local_datetime>'2017-10-10T00:00:00';''',\n            ['P7DT1H2M3.004005S'],\n            [edgedb.RelativeDuration(days=7, microseconds=3723004005)],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <cal::local_datetime>'2017-10-10T01:02:03.004005' -\n                <cal::local_datetime>'2017-10-10T00:00:00';''',\n            ['PT1H2M3.004005S'],\n            [edgedb.RelativeDuration(microseconds=3723004005)],\n        )\n\n    async def test_edgeql_dt_local_datetime_03(self):\n        # Corner case which interprets month in a fuzzy way.\n        await self.assert_query_result(\n            r'''\n            with dur := <cal::relative_duration>'1 month'\n            select <cal::local_datetime>'2021-01-31T00:00:00' + dur;\n            ''',\n            ['2021-02-28T00:00:00'],\n        )\n\n        # + not always associative\n        await self.assert_query_result(\n            r'''\n            with\n                dur := <cal::relative_duration>'1 month',\n                date := <cal::local_datetime>'2021-01-31T00:00:00',\n            select date + (dur + dur) = (date + dur) + dur;\n            ''',\n            [False],\n        )\n\n        # - not always inverse of plus\n        await self.assert_query_result(\n            r'''\n            with\n                dur := <cal::relative_duration>'1 month',\n                date := <cal::local_datetime>'2021-01-31T00:00:00',\n            select date + dur - dur = date;\n            ''',\n            [False],\n        )\n\n        # - not always inverse of plus\n        await self.assert_query_result(\n            r'''\n            with\n                m1 := <cal::relative_duration>'1 month',\n                m11 := <cal::relative_duration>'11 month',\n                y1 := <cal::relative_duration>'1 year',\n                date := <cal::local_datetime>'2021-01-31T00:00:00',\n            select (\n                # duration alone\n                y1 = m1 + m11,\n                # date + duration\n                date + y1 = date + m1 + m11,\n            );\n            ''',\n            [[True, False]],\n        )\n\n    async def test_edgeql_dt_local_datetime_04(self):\n        # Order in which different parts of relative_duration is applied.\n        await self.assert_query_result(\n            r'''select <cal::local_datetime>'2021-04-30T23:59:59' +\n                <cal::relative_duration>'1 hr' +\n                <cal::relative_duration>'1 month';''',\n            ['2021-06-01T00:59:59'],\n        )\n\n        await self.assert_query_result(\n            r'''select <cal::local_datetime>'2021-04-30T23:59:59' +\n                <cal::relative_duration>'1 month' +\n                <cal::relative_duration>'1 hr';''',\n            ['2021-05-31T00:59:59'],\n        )\n\n        await self.assert_query_result(\n            r'''select <cal::local_datetime>'2021-04-30T23:59:59' +\n                <cal::relative_duration>'1 hr 1 month';''',\n            ['2021-05-31T00:59:59'],\n        )\n\n        await self.assert_query_result(\n            r'''select <cal::local_datetime>'2021-04-30T23:59:59' +\n                <cal::relative_duration>'1 month 1 hr';''',\n            ['2021-05-31T00:59:59'],\n        )\n\n    async def test_edgeql_dt_local_datetime_05(self):\n        # Order in which different parts of relative_duration is applied.\n        await self.assert_query_result(\n            r'''select <cal::local_datetime>'2021-04-30T23:59:59' +\n                <cal::relative_duration>'1 day' +\n                <cal::relative_duration>'1 month';''',\n            ['2021-06-01T23:59:59'],\n        )\n\n        await self.assert_query_result(\n            r'''select <cal::local_datetime>'2021-04-30T23:59:59' +\n                <cal::relative_duration>'1 month' +\n                <cal::relative_duration>'1 day';''',\n            ['2021-05-31T23:59:59'],\n        )\n\n        await self.assert_query_result(\n            r'''select <cal::local_datetime>'2021-04-30T23:59:59' +\n                <cal::relative_duration>'1 day 1 month';''',\n            ['2021-05-31T23:59:59'],\n        )\n\n        await self.assert_query_result(\n            r'''select <cal::local_datetime>'2021-04-30T23:59:59' +\n                <cal::relative_duration>'1 month 1 day';''',\n            ['2021-05-31T23:59:59'],\n        )\n\n    async def test_edgeql_dt_local_date_01(self):\n        await self.assert_query_result(\n            r'''SELECT\n                    <cal::local_date>'2017-10-10' + <duration>'24 hours';\n            ''',\n            ['2017-10-11T00:00:00'],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT\n                <duration>'24 hours' + <cal::local_date>'2017-10-10';\n            ''',\n            ['2017-10-11T00:00:00'],\n        )\n\n        await self.assert_query_result(\n            r'''SELECT <cal::local_date>'2017-10-10' - <duration>'24 hours';\n            ''',\n            ['2017-10-09T00:00:00'],\n        )\n\n    async def test_edgeql_dt_local_date_02(self):\n        await self.assert_query_result(\n            r'''select <str>(\n                    <cal::local_date>'2017-10-11' -\n                    <cal::local_date>'2017-10-10');''',\n            ['P1D'],\n        )\n\n        await self.assert_query_result(\n            r'''select <str>(\n                    <cal::local_date>'2018-10-10' -\n                    <cal::local_date>'2017-10-10');''',\n            ['P365D'],\n        )\n\n    async def test_edgeql_dt_local_date_03(self):\n        # Corner case which interprets month in a fuzzy way.\n        await self.assert_query_result(\n            r'''\n            with dur := <cal::date_duration>'1 month'\n            select <cal::local_date>'2021-01-31' + dur;\n            ''',\n            ['2021-02-28'],\n        )\n\n        # + not always associative\n        await self.assert_query_result(\n            r'''\n            with\n                dur := <cal::date_duration>'1 month',\n                date := <cal::local_date>'2021-01-31',\n            select date + (dur + dur) = (date + dur) + dur;\n            ''',\n            [False],\n        )\n\n        # - not always inverse of plus\n        await self.assert_query_result(\n            r'''\n            with\n                dur := <cal::date_duration>'1 month',\n                date := <cal::local_date>'2021-01-31',\n            select date + dur - dur = date;\n            ''',\n            [False],\n        )\n\n        # - not always inverse of plus\n        await self.assert_query_result(\n            r'''\n            with\n                m1 := <cal::date_duration>'1 month',\n                m11 := <cal::date_duration>'11 month',\n                y1 := <cal::date_duration>'1 year',\n                date := <cal::local_date>'2021-01-31',\n            select (\n                # duration alone\n                y1 = m1 + m11,\n                # date + duration\n                date + y1 = date + m1 + m11,\n            );\n            ''',\n            [[True, False]],\n        )\n\n    async def test_edgeql_dt_local_date_04(self):\n        # Order in which different parts of relative_duration is applied.\n        await self.assert_query_result(\n            r'''select <cal::local_date>'2021-04-30' +\n                <cal::relative_duration>'1 hr' +\n                <cal::relative_duration>'1 month';''',\n            ['2021-05-30T01:00:00'],\n        )\n\n        await self.assert_query_result(\n            r'''select <cal::local_date>'2021-04-30' +\n                <cal::relative_duration>'1 month' +\n                <cal::relative_duration>'1 hr';''',\n            ['2021-05-30T01:00:00'],\n        )\n\n        await self.assert_query_result(\n            r'''select <cal::local_date>'2021-04-30' +\n                <cal::relative_duration>'1 hr 1 month';''',\n            ['2021-05-30T01:00:00'],\n        )\n\n        await self.assert_query_result(\n            r'''select <cal::local_date>'2021-04-30' +\n                <cal::relative_duration>'1 month 1 hr';''',\n            ['2021-05-30T01:00:00'],\n        )\n\n    async def test_edgeql_dt_local_date_05(self):\n        # Order in which different parts of date_duration is applied.\n        await self.assert_query_result(\n            r'''select <cal::local_date>'2021-04-30' +\n                <cal::date_duration>'1 day' +\n                <cal::date_duration>'1 month';''',\n            ['2021-06-01'],\n        )\n\n        await self.assert_query_result(\n            r'''select <cal::local_date>'2021-04-30' +\n                <cal::date_duration>'1 month' +\n                <cal::date_duration>'1 day';''',\n            ['2021-05-31'],\n        )\n\n        await self.assert_query_result(\n            r'''select <cal::local_date>'2021-04-30' +\n                <cal::date_duration>'1 day 1 month';''',\n            ['2021-05-31'],\n        )\n\n        await self.assert_query_result(\n            r'''select <cal::local_date>'2021-04-30' +\n                <cal::date_duration>'1 month 1 day';''',\n            ['2021-05-31'],\n        )\n\n    async def test_edgeql_dt_local_date_06(self):\n        # Fractional day values make the result fractional\n        await self.assert_query_result(\n            r'''select <cal::local_date>'2021-04-30' +\n                <cal::relative_duration>'20 hr' +\n                <cal::relative_duration>'20 hr';''',\n            ['2021-05-01T16:00:00'],\n        )\n\n    async def test_edgeql_dt_local_time_01(self):\n        await self.assert_query_result(\n            r'''select <cal::local_time>'10:01:01' +\n                <cal::relative_duration>'24 hours';''',\n            ['10:01:01'],\n        )\n\n        await self.assert_query_result(\n            r'''select <cal::relative_duration>'1 hour' +\n                <cal::local_time>'10:01:01';''',\n            ['11:01:01'],\n        )\n\n        await self.assert_query_result(\n            r'''select <cal::local_time>'10:01:01' -\n                <cal::relative_duration>'1 hour';''',\n            ['09:01:01'],\n        )\n\n        await self.assert_query_result(\n            r'''select <cal::local_time>'01:02:03.004005' -\n                <cal::local_time>'00:00:00';''',\n            ['PT1H2M3.004005S'],\n            [edgedb.RelativeDuration(microseconds=3723004005)],\n        )\n\n        await self.assert_query_result(\n            r'''select <cal::local_time>'01:02:03.004005' -\n                <cal::local_time>'10:00:00';''',\n            ['PT-8H-57M-56.995995S'],\n            [edgedb.RelativeDuration(microseconds=-32276995995)],\n        )\n\n    async def test_edgeql_dt_sequence_01(self):\n        await self.con.execute(\n            r'''\n                INSERT Obj;\n                INSERT Obj;\n                INSERT Obj2;\n            '''\n        )\n\n        try:\n            await self.assert_query_result(\n                r'''SELECT Obj { seq_prop } ORDER BY Obj.seq_prop;''',\n                [\n                    {'seq_prop': 1}, {'seq_prop': 2}\n                ],\n            )\n        except AssertionError:\n            if self.is_repeat:\n                await self.assert_query_result(\n                    r'''SELECT Obj { seq_prop } ORDER BY Obj.seq_prop;''',\n                    [\n                        {'seq_prop': 3}, {'seq_prop': 4}\n                    ],\n                )\n            else:\n                raise\n\n        try:\n            await self.assert_query_result(\n                r'''SELECT Obj2 { seq_prop };''',\n                [\n                    {'seq_prop': 1},\n                ],\n            )\n        except AssertionError:\n            if self.is_repeat:\n                await self.assert_query_result(\n                    r'''SELECT Obj2 { seq_prop };''',\n                    [\n                        {'seq_prop': 2},\n                    ],\n                )\n            else:\n                raise\n\n    async def test_edgeql_dt_enum_01(self):\n        await self.assert_query_result(\n            r'''\n                SELECT <enum_t>'foo' = <enum_t>'bar'\n            ''',\n            [\n                False\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <enum_t>'foo' = <enum_t>'foo'\n            ''',\n            [\n                True\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <enum_t>'foo' < <enum_t>'bar'\n            ''',\n            [\n                True\n            ],\n        )\n\n        with self.assertRaisesRegex(\n                edgedb.InvalidValueError,\n                'invalid input value for enum \\'default::enum_t\\': \"bad\"'):\n            await self.con.execute(\n                'SELECT <enum_t>\"bad\";'\n            )\n\n    async def test_edgeql_dt_bigint_01(self):\n        with self.assertRaisesRegex(\n            edgedb.InvalidValueError,\n            'invalid input syntax for type std::bigint'\n        ):\n            await self.con.execute(\n                r'''\n                    SELECT <bigint>'NaN'\n                '''\n            )\n\n    async def test_edgeql_dt_bigint_02(self):\n        with self.assertRaisesRegex(\n            edgedb.InvalidValueError,\n            \"invalid value for scalar type 'std::bigint'\",\n        ):\n            await self.con.execute(\n                r'''\n                    SELECT <bigint><float64>'NaN'\n                '''\n            )\n\n    async def test_edgeql_dt_decimal_01(self):\n        with self.assertRaisesRegex(\n            edgedb.InvalidValueError,\n            \"invalid value for std::decimal\",\n        ):\n            await self.con.execute(\n                r'''\n                    SELECT <decimal><float64>'NaN'\n                '''\n            )\n\n    async def test_edgeql_dt_decimal_02(self):\n        with self.assertRaisesRegex(\n            edgedb.InvalidValueError,\n            \"invalid value for std::decimal\",\n        ):\n            await self.con.execute(\n                r'''\n                    SELECT <decimal><float64>'Infinity'\n                '''\n            )\n\n    async def test_edgeql_dt_decimal_03(self):\n        with self.assertRaisesRegex(\n            edgedb.InvalidValueError,\n            \"invalid value for std::decimal\",\n        ):\n            await self.con.execute(\n                r'''\n                    SELECT <decimal><float64>'-Infinity'\n                '''\n            )\n\n    async def test_edgeql_dt_decimal_04(self):\n        with self.assertRaisesRegex(\n            edgedb.InvalidValueError,\n            \"invalid value for std::decimal\",\n        ):\n            await self.con.execute(\n                r'''\n                    SELECT <decimal>(<float64>'Infinity' / <float64>'Infinity')\n                '''\n            )\n\n    async def test_edgeql_dt_decimal_05(self):\n        await self.assert_query_result(\n            r'''SELECT (INTROSPECT TYPEOF 1e100n).name''',\n            ['std::bigint'],\n        )\n        await self.assert_query_result(\n            r'''SELECT (INTROSPECT TYPEOF 1.0e100n).name''',\n            ['std::decimal'],\n        )\n        await self.assert_query_result(\n            r'''SELECT 1e100n''',\n            [10**100],\n        )\n        await self.assert_query_result(\n            r'''SELECT 1.0e100n''',\n            [10.0**100],\n            [decimal.Decimal('1e100')],\n        )\n\n    async def test_edgeql_named_tuple_typing_01(self):\n        await self.con.execute(r\"\"\"\n            CREATE TYPE Foo { CREATE PROPERTY x -> tuple<a: int64, b: int64> };\n        \"\"\")\n\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                \"invalid target for property 'x' of object type \"\n                \"'default::Foo': 'tuple<b: std::int64, a: std::int64>' \"\n                \"\\\\(expecting 'tuple<a: std::int64, b: std::int64>'\"):\n            await self.con.execute(\"INSERT Foo { x := (b := 1, a := 2) };\")\n\n    async def test_edgeql_named_tuple_typing_02(self):\n        await self.assert_query_result(\n            r'''SELECT (b := 1, a := 2) UNION (a := 3, b := 4)''',\n            [[1, 2], [3, 4]],\n            sort=True,\n        )\n\n    async def test_edgeql_named_tuple_typing_03(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                \"named tuple has duplicate field 'a'\"):\n            await self.con.execute(\"SELECT (a := 1, a := 2);\")\n\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                \"named tuple has duplicate field 'a'\"):\n            await self.con.execute(\"\"\"\n                CREATE TYPE Foo {\n                    CREATE PROPERTY x -> tuple<a: int64, a: str>;\n                };\n            \"\"\")\n\n    async def test_edgeql_memory_01(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.InvalidValueError,\n                \"invalid value for scalar type 'cfg::memory'\"):\n            await self.con.execute(\"SELECT <cfg::memory>-1\")\n\n        self.assertEqual(\n            await self.con.query_single(\"SELECT <int64><cfg::memory>'1KiB'\"),\n            1024)\n\n        self.assertEqual(\n            await self.con.query_single(\"SELECT <int64><cfg::memory>1025\"),\n            1025)\n\n        self.assertEqual(\n            await self.con.query_single(\"SELECT <str><cfg::memory>1025\"),\n            '1025B')\n\n        self.assertEqual(\n            await self.con.query_single(\n                \"SELECT <str><cfg::memory>2272753910888172544\"),\n            '2219486241101731KiB')\n\n        self.assertEqual(\n            await self.con.query_single(\"SELECT <str><cfg::memory>0\"),\n            '0B')\n\n        self.assertEqual(\n            await self.con.query_single(\"SELECT <str><cfg::memory>'0B'\"),\n            '0B')\n\n    async def test_edgeql_staeval_duration_01(self):\n        valid = [\n            ' 100   ',\n            '123',\n            '-123',\n            '  20 mins 1hr ',\n            '  20 mins -1hr ',\n            '  20us  1h    20   ',\n            '  -20us  1h    20   ',\n            '  -20US  1H    20   ',\n            '1 hour 20 minutes 30 seconds 40 milliseconds 50 microseconds',\n            '1 hour 20 minutes +30seconds 40 milliseconds -50microseconds',\n            '1 houR  20 minutes 30SECOND 40 milliseconds 50 us',\n            '  20 us 1H 20 minutes ',\n            '-1h',\n            '100h',\n            '   12:12:12.2131   ',\n            '-12:12:12.21313',\n            '-12:12:12.213134',\n            '-12:12:12.2131341',\n            '-12:12:12.2131341111111',\n            '-12:12:12.2131315111111',\n            '-12:12:12.2131316111111',\n            '-12:12:12.2131314511111',\n            '-0:12:12.2131',\n            '12:12',\n            '-12:12',\n            '-12:1:1',\n            '+12:1:1',\n            '-12:1:1.1234',\n            '1211:59:59.9999',\n            '-12:',\n            '0',\n            '00:00:00',\n            '00:00:10.9',\n            '00:00:10.09',\n            '00:00:10.009',\n            '00:00:10.0009',\n        ]\n\n        invalid = [\n            'blah',\n            '!',\n            '-',\n            '  20 us 1H 20 30 minutes ',\n            '   12:12:121.2131   ',\n            '   12:60:21.2131   ',\n            '  20us 20   1h       ',\n            '  20us $ 20   1h       ',\n            '1 houR  20 minutes 30SECOND 40 milliseconds 50 uss',\n        ]\n\n        v = await self.con.query_single(\n            '''\n            SELECT <array<duration>><array<str>>$0\n            ''',\n            valid\n        )\n        vs = await self.con.query_single(\n            '''\n            SELECT <array<str>><array<duration>><array<str>>$0\n            ''',\n            valid\n        )\n\n        for text, value, svalue in zip(valid, v, vs):\n            ref_value = int(value / timedelta(microseconds=1))\n\n            try:\n                parsed = statypes.Duration(text)\n            except Exception:\n                raise AssertionError(\n                    f'could not parse a valid std::duration: {text!r}')\n\n            self.assertEqual(\n                ref_value,\n                parsed.to_microseconds(),\n                text)\n\n            self.assertEqual(\n                svalue,\n                parsed.to_iso8601(),\n                text)\n\n            self.assertEqual(\n                statypes.Duration.from_iso8601(svalue).to_microseconds(),\n                parsed.to_microseconds(),\n                text)\n\n            self.assertEqual(\n                statypes.Duration(svalue).to_microseconds(),\n                parsed.to_microseconds(),\n                text)\n\n        for text in invalid:\n            async with self.assertRaisesRegexTx(\n                    edgedb.InvalidValueError,\n                    r'(invalid input syntax)|(interval field value out)'):\n                await self.con.query_single(\n                    '''SELECT <duration><str>$0''',\n                    text\n                )\n\n            with self.assertRaises(\n                    (errors.InvalidValueError, errors.NumericOutOfRangeError)):\n                statypes.Duration(text)\n\n    async def test_edgeql_staeval_memory_01(self):\n        valid = [\n            '0',\n            '0B',\n            '123KiB',\n            '11MiB',\n            '0PiB',\n            '1PiB',\n            '111111GiB',\n            '123B',\n            '2219486241101731KiB',\n        ]\n\n        invalid = [\n            '12kB',\n            '22KB',\n            '-1B',\n            '-1',\n            '+1',\n            '+12TiB',\n            '123TIB',\n        ]\n\n        v = await self.con.query_single(\n            '''\n            SELECT  <array<int64>><array<cfg::memory>><array<str>>$0\n            ''',\n            valid\n        )\n        vs = await self.con.query_single(\n            '''\n            SELECT <array<str>><array<cfg::memory>><array<str>>$0\n            ''',\n            valid\n        )\n\n        for text, ref_value, svalue in zip(valid, v, vs):\n            try:\n                parsed = statypes.ConfigMemory(text)\n            except Exception:\n                raise AssertionError(\n                    f'could not parse a valid cfg::memory: {text!r}')\n\n            self.assertEqual(\n                ref_value,\n                parsed.to_nbytes(),\n                text)\n\n            self.assertEqual(\n                svalue,\n                parsed.to_str(),\n                text)\n\n            self.assertEqual(\n                statypes.ConfigMemory(svalue).to_nbytes(),\n                parsed.to_nbytes(),\n                text)\n\n        for text in invalid:\n            async with self.assertRaisesRegexTx(\n                    edgedb.InvalidValueError,\n                    r'(unsupported memory size)|(unable to parse memory)'):\n                await self.con.query_single(\n                    '''SELECT <int64><cfg::memory><str>$0''',\n                    text\n                )\n\n            with self.assertRaises(errors.InvalidValueError):\n                statypes.ConfigMemory(text)\n\n    async def test_edgeql_as_cache_key(self):\n        def make_setting_value(name, value):\n            return config.SettingValue(\n                name, value, \"session\", config.ConfigScope.SESSION\n            )\n\n        def make_key():\n            return immutables.Map(\n                dict(\n                    duration=make_setting_value(\n                        \"duration\", statypes.Duration(\"123\")\n                    ),\n                    memory=make_setting_value(\n                        \"memory\", statypes.ConfigMemory(\"11MiB\")\n                    ),\n                )\n            )\n\n        cache = {make_key(): True}\n        self.assertIn(make_key(), cache)\n"
  },
  {
    "path": "tests/test_edgeql_delete.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport itertools\n\nimport edgedb\n\nfrom edb.testbase import server as tb\n\n\nclass TestDelete(tb.QueryTestCase):\n    SETUP = \"\"\"\n        START MIGRATION TO {\n            module default {\n                type LinkingType {\n                    multi link objs -> AbstractDeleteTest;\n                };\n\n                abstract type AbstractDeleteTest {\n                    property name -> str;\n                };\n\n                type DeleteTest extending AbstractDeleteTest;\n                type DeleteTest2 extending AbstractDeleteTest;\n            };\n        };\n        POPULATE MIGRATION;\n        COMMIT MIGRATION;\n    \"\"\"\n\n    async def test_edgeql_delete_bad_01(self):\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r'cannot delete non-ObjectType object'):\n\n            await self.con.execute('''\\\n                DELETE 42;\n            ''')\n\n    async def test_edgeql_delete_bad_02(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r'free objects cannot be deleted',\n        ):\n            await self.con.execute('''\\\n                WITH foo := {bar := 1}\n                DELETE foo\n            ''')\n\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r'free objects cannot be deleted',\n        ):\n            await self.con.execute('''\\\n                DELETE std::FreeObject\n            ''')\n\n    async def test_edgeql_delete_bad_03(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r'delete standard library type',\n        ):\n            await self.con.execute('''\\\n                DELETE schema::Object;\n            ''')\n\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r'delete standard library type',\n        ):\n            await self.con.execute('''\\\n                DELETE {default::LinkingType, schema::Object};\n            ''')\n\n    async def test_edgeql_delete_simple_01(self):\n        # ensure a clean slate, not part of functionality testing\n        await self.con.execute(r\"\"\"\n            DELETE DeleteTest;\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            INSERT DeleteTest {\n                name := 'delete-test'\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT DeleteTest;\n            \"\"\",\n            [{}],\n        )\n\n        await self.con.execute(r\"\"\"\n            DELETE DeleteTest;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT DeleteTest;\n            \"\"\",\n            [],\n        )\n\n    async def test_edgeql_delete_simple_02(self):\n        id1 = str((await self.con.query_single(r\"\"\"\n            SELECT(INSERT DeleteTest {\n                name := 'delete-test1'\n            }) LIMIT 1;\n        \"\"\")).id)\n\n        id2 = str((await self.con.query_single(r\"\"\"\n            SELECT(INSERT DeleteTest {\n                name := 'delete-test2'\n            }) LIMIT 1;\n        \"\"\")).id)\n\n        await self.assert_query_result(\n            r\"\"\"\n                DELETE (SELECT DeleteTest\n                        FILTER DeleteTest.name = 'bad name');\n            \"\"\",\n            [],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT DeleteTest ORDER BY DeleteTest.name;\n            \"\"\",\n            [{'id': id1}, {'id': id2}],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT (DELETE (SELECT DeleteTest\n                        FILTER DeleteTest.name = 'delete-test1'));\n            \"\"\",\n            [{'id': id1}],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT DeleteTest ORDER BY DeleteTest.name;\n            \"\"\",\n            [{'id': id2}],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT (DELETE (SELECT DeleteTest\n                        FILTER DeleteTest.name = 'delete-test2'));\n            \"\"\",\n            [{'id': id2}],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT DeleteTest ORDER BY DeleteTest.name;\n            \"\"\",\n            [],\n        )\n\n    async def test_edgeql_delete_returning_01(self):\n        id1 = str((await self.con.query_single(r\"\"\"\n            SELECT (INSERT DeleteTest {\n                name := 'delete-test1'\n            }) LIMIT 1;\n        \"\"\")).id)\n\n        await self.con.execute(r\"\"\"\n            INSERT DeleteTest {\n                name := 'delete-test2'\n            };\n            INSERT DeleteTest {\n                name := 'delete-test3'\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT (DELETE DeleteTest\n                        FILTER DeleteTest.name = 'delete-test1');\n            \"\"\",\n            [{'id': id1}],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH\n                    D := (DELETE DeleteTest\n                          FILTER DeleteTest.name = 'delete-test2')\n                SELECT D {name};\n            \"\"\",\n            [{'name': 'delete-test2'}],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT\n                    (DELETE DeleteTest\n                     FILTER DeleteTest.name = 'delete-test3'\n                    ).name ++ '--DELETED';\n            \"\"\",\n            ['delete-test3--DELETED'],\n        )\n\n    async def test_edgeql_delete_returning_02(self):\n        await self.con.execute(r\"\"\"\n            INSERT DeleteTest {\n                name := 'delete-test1'\n            };\n            INSERT DeleteTest {\n                name := 'delete-test2'\n            };\n            INSERT DeleteTest {\n                name := 'delete-test3'\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH D := (DELETE DeleteTest)\n                SELECT count(D);\n            \"\"\",\n            [3],\n        )\n\n    async def test_edgeql_delete_returning_03(self):\n        await self.con.execute(r\"\"\"\n            INSERT DeleteTest {\n                name := 'dt1.1'\n            };\n            INSERT DeleteTest {\n                name := 'dt1.2'\n            };\n            INSERT DeleteTest {\n                name := 'dt1.3'\n            };\n            # create a different object\n            INSERT DeleteTest2 {\n                name := 'dt2.1'\n            };\n\n            INSERT DeleteTest2 {\n                name := 'delete test2.2'\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH\n                    D := (DELETE DeleteTest)\n                SELECT DeleteTest2 {\n                    name,\n                    foo := 'bar'\n                } FILTER any(DeleteTest2.name LIKE D.name[:2] ++ '%');\n            \"\"\",\n            [{\n                'name': 'dt2.1',\n                'foo': 'bar',\n            }],\n        )\n\n        deleted = await self.con._fetchall(\n            r\"\"\"\n                DELETE DeleteTest2;\n            \"\"\",\n            __typeids__=True,\n            __typenames__=True\n        )\n\n        self.assertTrue(hasattr(deleted[0], '__tid__'))\n        self.assertEqual(deleted[0].__tname__, 'default::DeleteTest2')\n\n    async def test_edgeql_delete_returning_04(self):\n        await self.con.execute(r\"\"\"\n            INSERT DeleteTest {\n                name := 'dt1.1'\n            };\n            INSERT DeleteTest {\n                name := 'dt1.2'\n            };\n            INSERT DeleteTest {\n                name := 'dt1.3'\n            };\n            # create a different object\n            INSERT DeleteTest2 {\n                name := 'dt2.1'\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH\n                    # make sure that aliased deletion works as an expression\n                    #\n                    Q := (DELETE DeleteTest)\n                SELECT DeleteTest2 {\n                    name,\n                    count := count(Q),\n                } FILTER DeleteTest2.name = 'dt2.1';\n            \"\"\",\n            [{\n                'name': 'dt2.1',\n                'count': 3,\n            }],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT (DELETE DeleteTest2) {name};\n            \"\"\",\n            [{\n                'name': 'dt2.1',\n            }],\n        )\n\n    async def test_edgeql_delete_returning_05(self):\n        await self.con.execute(r\"\"\"\n            INSERT DeleteTest {\n                name := 'dt1.1'\n            };\n            INSERT DeleteTest {\n                name := 'dt1.2'\n            };\n            INSERT DeleteTest {\n                name := 'dt1.3'\n            };\n            # create a different object\n            INSERT DeleteTest2 {\n                name := 'dt2.1'\n            };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH\n                    D := (DELETE DeleteTest)\n                # the returning clause is actually trying to simulate\n                # returning \"stats\" of deleted objects\n                #\n                SELECT DeleteTest2 {\n                    name,\n                    count := count(D),\n                } FILTER DeleteTest2.name = 'dt2.1';\n            \"\"\",\n            [{\n                'name': 'dt2.1',\n                'count': 3\n            }],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT (DELETE DeleteTest2) {name};\n            \"\"\",\n            [{\n                'name': 'dt2.1',\n            }],\n        )\n\n    async def test_edgeql_delete_sugar_01(self):\n        await self.con.execute(r\"\"\"\n            FOR x IN {'1', '2', '3', '4', '5', '6'}\n            UNION (INSERT DeleteTest {\n                name := 'sugar delete ' ++ x\n            });\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            DELETE\n                DeleteTest\n            FILTER\n                .name[-1] != '2'\n            ORDER BY .name\n            OFFSET 2 LIMIT 2;\n            # should delete 4 and 5\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT DeleteTest.name;\n            \"\"\",\n            {\n                'sugar delete 1',\n                'sugar delete 2',\n                'sugar delete 3',\n                'sugar delete 6',\n            },\n        )\n\n    async def test_edgeql_delete_union(self):\n        await self.con.execute(r\"\"\"\n            FOR x IN {'1', '2', '3', '4', '5', '6'}\n            UNION (INSERT DeleteTest {\n                name := 'delete union ' ++ x\n            });\n\n            FOR x IN {'7', '8', '9'}\n            UNION (INSERT DeleteTest2 {\n                name := 'delete union ' ++ x\n            });\n\n            INSERT DeleteTest { name := 'not delete union 1' };\n\n            INSERT DeleteTest2 { name := 'not delete union 2' };\n        \"\"\")\n\n        await self.con.execute(r\"\"\"\n            WITH\n                ToDelete := (\n                    (SELECT DeleteTest FILTER .name ILIKE 'delete union%')\n                    UNION\n                    (SELECT DeleteTest2 FILTER .name ILIKE 'delete union%')\n                )\n            DELETE ToDelete;\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT\n                    DeleteTest\n                FILTER\n                    .name ILIKE 'delete union%';\n\n            \"\"\",\n            [],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT\n                    DeleteTest {name}\n                FILTER\n                    .name ILIKE 'not delete union%';\n\n            \"\"\",\n            [{\n                'name': 'not delete union 1'\n            }],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT\n                    DeleteTest2\n                FILTER\n                    .name ILIKE 'delete union%';\n\n            \"\"\",\n            [],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT\n                    DeleteTest2 {name}\n                FILTER\n                    .name ILIKE 'not delete union%';\n\n            \"\"\",\n            [{\n                'name': 'not delete union 2'\n            }],\n        )\n\n    async def test_edgeql_delete_abstract_01(self):\n        await self.con.execute(r\"\"\"\n\n            INSERT DeleteTest { name := 'child of abstract 1' };\n            INSERT DeleteTest2 { name := 'child of abstract 2' };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH\n                    D := (\n                        DELETE\n                            AbstractDeleteTest\n                        FILTER\n                            .name ILIKE 'child of abstract%'\n                    )\n                SELECT D { name } ORDER BY .name;\n            \"\"\",\n            [{\n                'name': 'child of abstract 1'\n            }, {\n                'name': 'child of abstract 2'\n            }],\n        )\n\n    async def test_edgeql_delete_assert_exists(self):\n        await self.con.execute(r\"\"\"\n            INSERT DeleteTest2 { name := 'x' };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n            select assert_exists((delete DeleteTest2 filter .name = 'x'));\n            \"\"\",\n            [{}],\n        )\n\n    async def test_edgeql_delete_then_union(self):\n        await self.con.execute(r\"\"\"\n            INSERT DeleteTest2 { name := 'x' };\n            INSERT DeleteTest2 { name := 'y' };\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n            with\n            delete1 := assert_exists((delete DeleteTest2 filter .name = 'x')),\n            delete2 := assert_exists((delete DeleteTest2 filter .name = 'y')),\n            select {delete1, delete2};\n            \"\"\",\n            [{}, {}],\n        )\n\n    async def test_edgeql_delete_multi_simultaneous_01(self):\n        await self.con.execute(r\"\"\"\n            with\n              a := (insert DeleteTest { name := '1' }),\n              b := (insert DeleteTest { name := '2' }),\n              c := (insert LinkingType { objs := {a, b} })\n            select c;\n        \"\"\")\n\n        dels = {\n            'a': '(DELETE DeleteTest)',\n            'b': '(DELETE LinkingType)',\n        }\n\n        # We want to try all the different permutations of deletion\n        # binding order and order that the variables are referenced in\n        # the body. (Somewhat upsettingly, the order that the delete CTEs\n        # are included into the SQL union affected the behavior.)\n        # All the queries look like some variant on:\n        #\n        #      with\n        #        a := (DELETE DeleteTest),\n        #        b := (DELETE LinkingType),\n        #      select {a, b};\n\n        for binds, uses in itertools.product(\n            list(itertools.permutations(dels.keys())),\n            list(itertools.permutations(dels.keys())),\n        ):\n            bind_q = '\\n'.join(\n                ' ' * 18 + f'{k} := {dels[k]},' for k in binds\n            ).lstrip()\n            q = f'''\n                with\n                  {bind_q}\n                select {{{', '.join(uses)}}};\n            '''\n\n            async with self._run_and_rollback():\n                with self.annotate(binds=binds, uses=uses):\n                    await self.con.execute(q)\n\n    async def test_edgeql_delete_multi_simultaneous_02(self):\n        populate = r\"\"\"\n            with\n              a := (insert DeleteTest { name := '1' }),\n              b := (insert DeleteTest2 { name := '2' }),\n              c := (insert LinkingType { objs := {a, b} })\n            select c;\n        \"\"\"\n\n        await self.con.execute(populate)\n        await self.con.execute(r\"\"\"\n             with\n               a := (DELETE AbstractDeleteTest),\n               b := (DELETE LinkingType),\n             select {a, b};\n        \"\"\")\n\n        await self.con.execute(populate)\n        await self.con.execute(r\"\"\"\n             with\n               a := (DELETE AbstractDeleteTest),\n               b := (DELETE LinkingType),\n             select {b, a};\n        \"\"\")\n\n    async def test_edgeql_delete_where_order_dml(self):\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                \"INSERT statements cannot be used in a FILTER clause\"):\n            await self.con.query('''\n                delete DeleteTest\n                filter\n                        (INSERT DeleteTest {\n                            name := 't1',\n                        })\n            ''')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                \"UPDATE statements cannot be used in a FILTER clause\"):\n            await self.con.query('''\n                delete DeleteTest\n                filter\n                        (UPDATE DeleteTest set {\n                            name := 't1',\n                        })\n            ''')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                \"DELETE statements cannot be used in a FILTER clause\"):\n            await self.con.query('''\n                delete DeleteTest\n                filter\n                        (DELETE DeleteTest filter .name = 't1')\n            ''')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                \"INSERT statements cannot be used in an ORDER BY clause\"):\n            await self.con.query('''\n                delete DeleteTest\n                order by\n                        (INSERT DeleteTest {\n                            name := 't1',\n                        })\n                limit 1\n            ''')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                \"UPDATE statements cannot be used in an ORDER BY clause\"):\n            await self.con.query('''\n                delete DeleteTest\n                order by\n                        (UPDATE DeleteTest set {\n                            name := 't1',\n                        })\n                limit 1\n            ''')\n\n        async with self.assertRaisesRegexTx(\n                edgedb.QueryError,\n                \"DELETE statements cannot be used in an ORDER BY clause\"):\n            await self.con.query('''\n                delete DeleteTest\n                order by\n                        (DELETE DeleteTest filter .name = 't1')\n                limit 1\n            ''')\n\n    async def test_edgeql_delete_read_only_tx_01(self):\n        con = (\n            edgedb.create_async_client(\n                **self.get_connect_args()\n            ).with_transaction_options(\n                edgedb.TransactionOptions(readonly=True)\n            )\n        )\n        try:\n            with self.assertRaisesRegex(\n                edgedb.TransactionError,\n                r'Modifications not allowed in a read-only transaction'\n            ):\n                async for tx in con.transaction():\n                    async with tx:\n                        await tx.execute(\"delete DeleteTest\")\n        finally:\n            await con.aclose()\n"
  },
  {
    "path": "tests/test_edgeql_enums.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2019-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport os.path\n\nimport edgedb\n\nfrom edb.testbase import server as tb\n\n\nclass TestEdgeQLEnums(tb.QueryTestCase):\n    SCHEMA = os.path.join(os.path.dirname(__file__), 'schemas',\n                          'enums.esdl')\n\n    async def test_edgeql_enums_cast_01(self):\n        await self.assert_query_result(\n            r'''\n                SELECT <color_enum_t>{'RED', 'GREEN', 'BLUE'};\n            ''',\n            {'RED', 'GREEN', 'BLUE'},\n        )\n\n    async def test_edgeql_enums_cast_02(self):\n        with self.assertRaisesRegex(\n                edgedb.InvalidValueError,\n                r'invalid input value for enum .+color_enum_t.+YELLOW'):\n            await self.con.execute(r'''\n                SELECT <color_enum_t>'YELLOW';\n            ''')\n\n    async def test_edgeql_enums_cast_03(self):\n        with self.assertRaisesRegex(\n                edgedb.InvalidValueError,\n                r'invalid input value for enum .+color_enum_t.+red'):\n            await self.con.execute(r'''\n                SELECT <color_enum_t>'red';\n            ''')\n\n    async def test_edgeql_enums_cast_04(self):\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                r\"operator '\\+\\+' cannot be applied to operands of type \"\n                r\"'std::str' and 'default::color_enum_t'\"):\n            await self.con.execute(r'''\n                INSERT Foo {\n                    color := 'BLUE'\n                };\n\n                SELECT 'The test color is: ' ++ Foo.color;\n            ''')\n\n    async def test_edgeql_enums_cast_05(self):\n        await self.con.execute(\n            r'''\n                INSERT Foo {\n                    color := 'BLUE'\n                };\n            ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT 'The test color is: ' ++ <str>Foo.color;\n            ''',\n            ['The test color is: BLUE'],\n        )\n\n    async def test_edgeql_enums_pathsyntax_01(self):\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                \"enum path expression lacks an enum member name\"):\n            async with self._run_and_rollback():\n                await self.con.execute('SELECT color_enum_t')\n\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                \"enum path expression lacks an enum member name\"):\n            async with self._run_and_rollback():\n                await self.con.execute(\n                    'WITH e := color_enum_t SELECT e.RED'\n                )\n\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                \"unexpected reference to link property 'RED'\"):\n            async with self._run_and_rollback():\n                await self.con.execute(\n                    'SELECT color_enum_t@RED'\n                )\n\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                \"enum types do not support backlink\"):\n            async with self._run_and_rollback():\n                await self.con.execute(\n                    'SELECT color_enum_t.<RED'\n                )\n\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                \"an enum member name must follow enum type name in the path\"):\n            async with self._run_and_rollback():\n                await self.con.execute(\n                    'SELECT color_enum_t[IS color_enum_t].RED'\n                )\n\n        with self.assertRaisesRegex(\n            edgedb.QueryError,\n            \"invalid property reference on an expression of primitive type \"\n            \"'default::color_enum_t'\"\n        ):\n            async with self._run_and_rollback():\n                await self.con.execute(\n                    'SELECT color_enum_t.RED.GREEN'\n                )\n\n        with self.assertRaisesRegex(\n            edgedb.QueryError,\n            \"invalid property reference on an expression of primitive type \"\n            \"'default::color_enum_t'\"\n        ):\n            async with self._run_and_rollback():\n                await self.con.execute(\n                    'WITH x := color_enum_t.RED SELECT x.GREEN'\n                )\n\n        with self.assertRaisesRegex(\n                edgedb.QueryError,\n                \"enum has no member called 'RAD'\",\n                _hint=\"did you mean 'RED'?\"):\n            async with self._run_and_rollback():\n                await self.con.execute(\n                    'SELECT color_enum_t.RAD'\n                )\n\n    async def test_edgeql_enums_pathsyntax_02(self):\n        await self.assert_query_result(\n            r'''\n                SELECT color_enum_t.GREEN;\n            ''',\n            {'GREEN'},\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT default::color_enum_t.BLUE;\n            ''',\n            {'BLUE'},\n        )\n\n        await self.assert_query_result(\n            r'''\n                WITH x := default::color_enum_t.RED SELECT x;\n            ''',\n            {'RED'},\n        )\n\n    async def test_edgeql_enums_assignment_01(self):\n        # testing the INSERT assignment cast\n        await self.con.execute(\n            r'''\n                INSERT Foo {\n                    color := 'RED'\n                };\n            ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Foo {\n                    color\n                };\n            ''',\n            [{\n                'color': 'RED',\n            }],\n        )\n\n    async def test_edgeql_enums_assignment_02(self):\n        await self.con.execute(\n            r'''\n                INSERT Foo {\n                    color := 'RED'\n                };\n            ''')\n\n        # testing the UPDATE assignment cast\n        await self.con.execute(\n            r'''\n                UPDATE Foo\n                SET {\n                    color := 'GREEN'\n                };\n            ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Foo {\n                    color\n                };\n            ''',\n            [{\n                'color': 'GREEN',\n            }],\n        )\n\n    async def test_edgeql_enums_assignment_03(self):\n        # testing the INSERT assignment cast\n        await self.con.execute(\n            r'''\n                INSERT Bar;\n            ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Bar {\n                    color\n                };\n            ''',\n            [{\n                'color': 'RED',\n            }],\n        )\n\n    async def test_edgeql_enums_assignment_04(self):\n        await self.con.execute(\n            r'''\n                INSERT Bar;\n            ''')\n\n        # testing the UPDATE assignment cast\n        await self.con.execute(\n            r'''\n                UPDATE Bar\n                SET {\n                    color := 'GREEN'\n                };\n            ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT Bar {\n                    color\n                };\n            ''',\n            [{\n                'color': 'GREEN',\n            }],\n        )\n\n    async def test_edgeql_enums_json_cast_01(self):\n        self.assertEqual(\n            await self.con.query(\n                \"SELECT <json><color_enum_t>'RED'\"\n            ),\n            ['\"RED\"'])\n\n        await self.assert_query_result(\n            \"SELECT <color_enum_t><json>'RED'\",\n            ['RED'])\n\n        await self.assert_query_result(\n            \"SELECT <color_enum_t>'RED'\",\n            ['RED'])\n\n    async def test_edgeql_enums_json_cast_02(self):\n        with self.assertRaisesRegex(\n                edgedb.InvalidValueError,\n                r'invalid input value for enum .+color_enum_t.+: \"BANANA\"'):\n            await self.con.execute(\"SELECT <color_enum_t><json>'BANANA'\")\n\n    async def test_edgeql_enums_json_cast_03(self):\n        with self.assertRaisesRegex(\n                edgedb.InvalidValueError,\n                r'expected JSON string or null; got JSON number'):\n            await self.con.execute(\"SELECT <color_enum_t><json>12\")\n\n    async def test_edgeql_enums_anonymous(self):\n        with self.assertRaisesRegex(\n            edgedb.InvalidPropertyDefinitionError,\n            r'this type cannot be anonymous',\n        ):\n            await self.con.execute(\n                \"\"\"\n                create type X {\n                    create property tier -> enum<\"Basic\", \"Common\", \"Rare\">;\n                }\n                \"\"\"\n            )\n"
  },
  {
    "path": "tests/test_edgeql_explain.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2017-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport edgedb\n\nimport unittest\nimport json\nimport os.path\n\nfrom edb.tools import test\nfrom edb.testbase import server as tb\nfrom edb.common import assert_data_shape\nfrom edb.schema import name as sn\nfrom edb.server.compiler.explain import pg_tree\n\n\nclass TestEdgeQLExplain(tb.QueryTestCase):\n    '''Tests for ANALYZE.\n\n    This is a good way of testing explain functionality, but also this can be\n    used to test indexes.\n    '''\n\n    SCHEMA = os.path.join(os.path.dirname(__file__), 'schemas',\n                          'explain.esdl')\n\n    SCHEMA_BUG5758 = os.path.join(os.path.dirname(__file__), 'schemas',\n                                  'explain_bug5758.esdl')\n\n    SCHEMA_BUG5791 = os.path.join(os.path.dirname(__file__), 'schemas',\n                                  'explain_bug5791.esdl')\n\n    SETUP = [\n        os.path.join(os.path.dirname(__file__), 'schemas',\n                     'explain_setup.edgeql'),\n    ]\n\n    def assert_plan(self, data, shape, message=None):\n        assert_data_shape.assert_data_shape(\n            data, shape, fail=self.fail, message=message)\n\n    async def explain(self, query, *, execute=True, con=None):\n        con = (con or self.con)\n        # Disable sequential scan so that we hit the index even on small\n        # datasets.\n        await self.con.query_single(\n            'select _set_config(\"enable_seqscan\", \"off\")'\n        )\n        no_ex = '(execute := False) ' if not execute else ''\n        return json.loads(await con.query_single(\n            f'analyze {no_ex}{query}'\n        ))\n\n    async def _assert_index_use(self, query):\n        def look(obj):\n            if (\n                isinstance(obj, dict)\n                and obj.get('plan_type') in ['IndexScan', 'BitmapHeapScan']\n            ):\n                # TODO: could add a param to check for  index name\n                return True\n\n            if isinstance(obj, dict):\n                return any([look(v) for v in obj.values()])\n            elif isinstance(obj, list):\n                return any(look(v) for v in obj)\n            else:\n                return False\n\n        plan = await self.explain(query)\n\n        if not look(plan):\n            raise AssertionError(f'query did not use an index')\n\n    async def test_edgeql_explain_simple_01(self):\n        res = await self.explain('''\n            select User { id, name } filter .name = 'Elvis'\n        ''')\n        self.assert_plan(res['fine_grained'], {\n            \"contexts\": [\n                {\n                    \"buffer_idx\": 0,\n                    \"start\": 28,\n                    \"end\": 43,\n                    \"text\": \"User { id, name\"\n                }\n            ],\n            \"pipeline\": [\n                {\n                    \"actual_loops\": 1,\n                    \"actual_rows\": 1,\n                    \"plan_rows\": 1,\n                    \"plan_type\": \"IndexScan\",\n                    \"properties\": tb.bag([\n                        {\n                            \"important\": False,\n                            \"title\": \"schema\",\n                            \"type\": \"text\",\n                            \"value\": \"edgedbpub\",\n                        },\n                        {\n                            \"important\": False,\n                            \"title\": \"alias\",\n                            \"type\": \"text\",\n                            \"value\": \"User~2\",\n                        },\n                        {\n                            \"important\": True,\n                            \"title\": \"relation_name\",\n                            \"type\": \"relation\",\n                        },\n                        {\n                            \"important\": True,\n                            \"title\": \"scan_direction\",\n                            \"type\": \"text\",\n                            \"value\": \"Forward\",\n                        },\n                        {\n                            \"important\": True,\n                            \"title\": \"index_name\",\n                            \"type\": \"index\",\n                            \"value\": \"index of object type 'default::User' \"\n                            \"on (__subject__.name)\",\n                        },\n                        {\n                            \"important\": False,\n                            \"title\": \"index_cond\",\n                            \"type\": \"expr\",\n                        },\n                    ]),\n                }\n            ],\n            \"subplans\": [],\n        })\n        self.assert_plan(res['config_vals'], {\n            \"allow_user_specified_id\": False,\n            \"apply_access_policies\": True\n        })\n\n    async def test_edgeql_explain_introspection_01(self):\n        res = await self.explain('select sys::Branch')\n        self.assertIn(\n            ('relation_name', 'pg_database'),\n            ((p['title'], p['value'])\n             for p in res['fine_grained']['pipeline'][0]['properties']),\n        )\n\n    @test.skip(\n        'fails on PostgreSQL 17 due to changes to indexes and operator classes'\n    )\n    async def test_edgeql_explain_with_bound_01(self):\n        res = await self.explain('''\n            with U := User,\n            select {\n                elvis := (select U filter .name like 'E%'),\n                yury := (select U filter .name[0] = 'Y'),\n            };\n        ''')\n\n        shape = {\n            \"contexts\": [{\n                \"start\": 31,\n                \"end\": 35,\n                \"buffer_idx\": 0,\n                \"text\": \"User\",\n            }],\n            \"pipeline\": [{\n                \"plan_rows\": 1,\n                \"actual_rows\": 1,\n                \"actual_loops\": 1,\n                \"plan_type\": \"Result\",\n                \"properties\": [],\n                # Just validating that these fields appear. This was part of\n                # the early tests and these fields are something the users may\n                # rely on and should be part of stable API.\n                \"startup_cost\": float,\n                \"total_cost\": float,\n            }],\n            \"subplans\": tb.bag([\n                {\n                    \"contexts\": [{\n                        \"start\": 74,\n                        \"end\": 115,\n                        \"buffer_idx\": 0,\n                        \"text\": \"elvis := (select U filter .name like 'E%'\",\n                    }],\n                    \"pipeline\": tb.bag([\n                        {\n                            \"plan_rows\": 1,\n                            \"actual_rows\": 1,\n                            \"actual_loops\": 1,\n                            \"plan_type\": \"Aggregate\",\n                            \"properties\": tb.bag([\n                                {\n                                    \"title\": \"parent_relationship\",\n                                    \"type\": \"text\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"subplan_name\",\n                                    \"type\": \"text\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"strategy\",\n                                    \"value\": \"Plain\",\n                                    \"type\": \"text\",\n                                    \"important\": True,\n                                },\n                                {\n                                    \"title\": \"partial_mode\",\n                                    \"value\": \"Simple\",\n                                    \"type\": \"text\",\n                                    \"important\": True,\n                                },\n                            ]),\n                        },\n                        {\n                            \"plan_rows\": 1,\n                            \"actual_rows\": 1,\n                            \"actual_loops\": 1,\n                            \"plan_type\": \"IndexScan\",\n                            \"properties\": tb.bag([\n                                {\n                                    \"title\": \"filter\",\n                                    \"type\": \"expr\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"parent_relationship\",\n                                    \"value\": \"Outer\",\n                                    \"type\": \"text\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"schema\",\n                                    \"value\": \"edgedbpub\",\n                                    \"type\": \"text\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"alias\",\n                                    \"value\": 'User~3',\n                                    \"type\": \"text\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"relation_name\",\n                                    \"value\": 'User',\n                                    \"type\": \"relation\",\n                                    \"important\": True,\n                                },\n                                {\n                                    \"title\": \"scan_direction\",\n                                    \"value\": \"Forward\",\n                                    \"type\": \"text\",\n                                    \"important\": True,\n                                },\n                                {\n                                    \"title\": \"index_name\",\n                                    \"value\":\n                                        \"index of object type 'default::User' \"\n                                        \"on (__subject__.name)\",\n                                    \"type\": \"index\",\n                                    \"important\": True,\n                                },\n                                {\n                                    \"title\": \"index_cond\",\n                                    \"type\": \"expr\",\n                                    \"important\": False,\n                                },\n                            ]),\n                        },\n                    ]),\n                    \"subplans\": [],\n                },\n                {\n                    \"contexts\": [{\n                        \"start\": 134,\n                        \"end\": 173,\n                        \"buffer_idx\": 0,\n                        \"text\": \"yury := (select U filter .name[0] = 'Y'\",\n                    }],\n                    \"pipeline\": tb.bag([\n                        {\n                            \"plan_rows\": 1,\n                            \"actual_rows\": 1,\n                            \"actual_loops\": 1,\n                            \"plan_type\": \"Aggregate\",\n                            \"properties\": tb.bag([\n                                {\n                                    \"title\": \"parent_relationship\",\n                                    \"type\": \"text\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"subplan_name\",\n                                    \"type\": \"text\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"strategy\",\n                                    \"value\": \"Plain\",\n                                    \"type\": \"text\",\n                                    \"important\": True,\n                                },\n                                {\n                                    \"title\": \"partial_mode\",\n                                    \"value\": \"Simple\",\n                                    \"type\": \"text\",\n                                    \"important\": True,\n                                },\n                            ]),\n                        },\n                        {\n                            \"plan_rows\": 1,\n                            \"actual_rows\": 1,\n                            \"actual_loops\": 1,\n                            \"plan_type\": \"SeqScan\",\n                            \"properties\": tb.bag([\n                                {\n                                    \"title\": \"filter\",\n                                    \"type\": \"expr\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"parent_relationship\",\n                                    \"value\": \"Outer\",\n                                    \"type\": \"text\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"schema\",\n                                    \"value\": \"edgedbpub\",\n                                    \"type\": \"text\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"alias\",\n                                    \"value\": 'User~7',\n                                    \"type\": \"text\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"relation_name\",\n                                    \"value\": 'User',\n                                    \"type\": \"relation\",\n                                    \"important\": True,\n                                },\n                            ]),\n                        }\n                    ]),\n                    \"subplans\": [],\n                }\n            ]),\n        }\n\n        self.assert_plan(res['fine_grained'], shape)\n\n    async def test_edgeql_explain_multi_link_01(self):\n        res = await self.explain('''\n            select User { name, todo: {name, number} }\n            filter .name = 'Elvis';\n        ''')\n\n        shape = {\n            \"contexts\": [\n                {\n                    \"start\": 28,\n                    \"end\": 60,\n                    \"buffer_idx\": 0,\n                    \"text\": \"User { name, todo: {name, number\",\n                },\n            ],\n            \"pipeline\": [\n                {\n                    \"plan_rows\": 1,\n                    \"actual_rows\": 1,\n                    \"actual_loops\": 1,\n                    \"plan_type\": \"IndexScan\",\n                    \"properties\": tb.bag([\n                        {\n                            \"title\": \"schema\",\n                            \"value\": \"edgedbpub\",\n                            \"type\": \"text\",\n                            \"important\": False,\n                        },\n                        {\n                            \"title\": \"alias\",\n                            \"value\": \"User~2\",\n                            \"type\": \"text\",\n                            \"important\": False,\n                        },\n                        {\n                            \"title\": \"relation_name\",\n                            \"value\": \"User\",\n                            \"type\": \"relation\",\n                            \"important\": True,\n                        },\n                        {\n                            \"title\": \"scan_direction\",\n                            \"value\": \"Forward\",\n                            \"type\": \"text\",\n                            \"important\": True,\n                        },\n                        {\n                            \"title\": \"index_name\",\n                            \"value\":\n                                \"index of object type 'default::User' \"\n                                \"on (__subject__.name)\",\n                            \"type\": \"index\",\n                            \"important\": True,\n                        },\n                        {\n                            \"title\": \"index_cond\",\n                            \"type\": \"expr\",\n                            \"important\": False,\n                        },\n                    ]),\n                },\n            ],\n            \"subplans\": [\n                {\n                    \"contexts\": [\n                        {\n                            \"start\": 41,\n                            \"end\": 60,\n                            \"buffer_idx\": 0,\n                            \"text\": \"todo: {name, number\",\n                        },\n                    ],\n                    \"pipeline\": tb.bag([\n                        {\n                            \"plan_rows\": 1,\n                            \"actual_rows\": 1,\n                            \"actual_loops\": 1,\n                            \"plan_type\": \"Aggregate\",\n                            \"properties\": tb.bag([\n                                {\n                                    \"title\": \"parent_relationship\",\n                                    \"value\": \"SubPlan\",\n                                    \"type\": \"text\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"subplan_name\",\n                                    \"type\": \"text\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"strategy\",\n                                    \"value\": \"Plain\",\n                                    \"type\": \"text\",\n                                    \"important\": True,\n                                },\n                                {\n                                    \"title\": \"partial_mode\",\n                                    \"value\": \"Simple\",\n                                    \"type\": \"text\",\n                                    \"important\": True,\n                                },\n                            ]),\n                        },\n                        {\n                            \"plan_rows\": 1,\n                            \"actual_rows\": 2,\n                            \"actual_loops\": 1,\n                            \"plan_type\": \"NestedLoop\",\n                            \"properties\": tb.bag([\n                                {\n                                    \"title\": \"parent_relationship\",\n                                    \"value\": \"Outer\",\n                                    \"type\": \"text\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"join_type\",\n                                    \"value\": \"Inner\",\n                                    \"type\": \"text\",\n                                    \"important\": True,\n                                },\n                            ]),\n                        },\n                    ]),\n                    \"subplans\": tb.bag([\n                        {\n                            \"pipeline\": [\n                                {\n                                    \"plan_rows\": 1,\n                                    \"actual_rows\": 2,\n                                    \"actual_loops\": 1,\n                                    \"plan_type\": \"IndexOnlyScan\",\n                                    \"properties\": tb.bag([\n                                        {\n                                            \"title\": \"parent_relationship\",\n                                            \"value\": \"Outer\",\n                                            \"type\": \"text\",\n                                            \"important\": False,\n                                        },\n                                        {\n                                            \"title\": \"schema\",\n                                            \"value\": \"edgedbpub\",\n                                            \"type\": \"text\",\n                                            \"important\": False,\n                                        },\n                                        {\n                                            \"title\": \"alias\",\n                                            \"value\": \"todo~1\",\n                                            \"type\": \"text\",\n                                            \"important\": False,\n                                        },\n                                        {\n                                            \"title\": \"relation_name\",\n                                            \"value\": \"User.todo\",\n                                            \"type\": \"relation\",\n                                            \"important\": True,\n                                        },\n                                        {\n                                            \"title\": \"scan_direction\",\n                                            \"value\": \"Forward\",\n                                            \"type\": \"text\",\n                                            \"important\": True,\n                                        },\n                                        {\n                                            \"title\": \"index_name\",\n                                            \"value\":\n                                                \"User.todo forward \"\n                                                \"link index\",\n                                            \"type\": \"index\",\n                                            \"important\": True,\n                                        },\n                                        {\n                                            \"title\": \"index_cond\",\n                                            \"type\": \"expr\",\n                                            \"important\": False,\n                                        },\n                                        {\n                                            \"title\": \"heap_fetches\",\n                                            \"type\": \"float\",\n                                            \"important\": False,\n                                        },\n                                    ]),\n                                },\n                            ],\n                            \"subplans\": [],\n                        },\n                        {\n                            \"pipeline\": [\n                                {\n                                    \"plan_rows\": 1,\n                                    \"actual_rows\": 1,\n                                    \"actual_loops\": 2,\n                                    \"plan_type\": \"IndexScan\",\n                                    \"properties\": tb.bag([\n                                        {\n                                            \"title\": \"parent_relationship\",\n                                            \"value\": \"Inner\",\n                                            \"type\": \"text\",\n                                            \"important\": False,\n                                        },\n                                        {\n                                            \"title\": \"schema\",\n                                            \"value\": \"edgedbpub\",\n                                            \"type\": \"text\",\n                                            \"important\": False,\n                                        },\n                                        {\n                                            \"title\": \"alias\",\n                                            \"value\": \"Issue~1\",\n                                            \"type\": \"text\",\n                                            \"important\": False,\n                                        },\n                                        {\n                                            \"title\": \"relation_name\",\n                                            \"value\": \"Issue\",\n                                            \"type\": \"relation\",\n                                            \"important\": True,\n                                        },\n                                        {\n                                            \"title\": \"scan_direction\",\n                                            \"value\": \"Forward\",\n                                            \"type\": \"text\",\n                                            \"important\": True,\n                                        },\n                                        {\n                                            \"title\": \"index_name\",\n                                            \"value\":\n                                                \"constraint 'std::exclusive' \"\n                                                \"of property 'id' of object \"\n                                                \"type 'default::Issue'\",\n                                            \"type\": \"index\",\n                                            \"important\": True,\n                                        },\n                                        {\n                                            \"title\": \"index_cond\",\n                                            \"type\": \"expr\",\n                                            \"important\": False,\n                                        },\n                                    ]),\n                                },\n                            ],\n                            \"subplans\": [],\n                        },\n                    ]),\n                },\n            ],\n        }\n\n        self.assert_plan(res['fine_grained'], shape)\n\n    async def test_edgeql_explain_computed_backlink_01(self):\n        res = await self.explain('''\n            select User { name, owned_issues: {name, number} }\n            filter .name = 'Elvis';\n        ''')\n\n        shape = {\n            \"contexts\": [{\n                \"start\": 28,\n                \"end\": 68,\n                \"buffer_idx\": 0,\n                \"text\": \"User { name, owned_issues: {name, number\"\n            }],\n            \"pipeline\": [{\n                \"plan_rows\": 1,\n                \"actual_rows\": 1,\n                \"actual_loops\": 1,\n                \"plan_type\": \"IndexScan\",\n                \"properties\": tb.bag([\n                    {\n                        \"title\": \"schema\",\n                        \"value\": \"edgedbpub\",\n                        \"type\": \"text\",\n                        \"important\": False\n                    },\n                    {\n                        \"title\": \"alias\",\n                        \"value\": \"User~2\",\n                        \"type\": \"text\",\n                        \"important\": False\n                    },\n                    {\n                        \"title\": \"relation_name\",\n                        \"value\": \"User\",\n                        \"type\": \"relation\",\n                        \"important\": True\n                    },\n                    {\n                        \"title\": \"scan_direction\",\n                        \"value\": \"Forward\",\n                        \"type\": \"text\",\n                        \"important\": True\n                    },\n                    {\n                        \"title\": \"index_name\",\n                        \"value\":\n                            \"index of object type 'default::User' on \"\n                            \"(__subject__.name)\",\n                        \"type\": \"index\",\n                        \"important\": True\n                    },\n                    {\n                        \"title\": \"index_cond\",\n                        \"type\": \"expr\",\n                        \"important\": False\n                    }\n                ]),\n            }],\n            \"subplans\": [{\n                \"contexts\": tb.bag([{\n                    \"start\": 0,\n                    \"end\": 26,\n                    \"buffer_idx\": 1,\n                    \"text\": '.<owner[is default::Issue]'\n                }, {\n                    \"start\": 41,\n                    \"end\": 68,\n                    \"buffer_idx\": 0,\n                    \"text\": \"owned_issues: {name, number\"\n                }]),\n                \"pipeline\": tb.bag([\n                    {\n                        \"plan_rows\": 1,\n                        \"actual_rows\": 1,\n                        \"actual_loops\": 1,\n                        \"plan_type\": \"Aggregate\",\n                        \"properties\": tb.bag([\n                            {\n                                \"title\": \"parent_relationship\",\n                                \"value\": \"SubPlan\",\n                                \"type\": \"text\",\n                                \"important\": False\n                            },\n                            {\n                                \"title\": \"subplan_name\",\n                                \"type\": \"text\",\n                                \"important\": False\n                            },\n                            {\n                                \"title\": \"strategy\",\n                                \"value\": \"Plain\",\n                                \"type\": \"text\",\n                                \"important\": True\n                            },\n                            {\n                                \"title\": \"partial_mode\",\n                                \"value\": \"Simple\",\n                                \"type\": \"text\",\n                                \"important\": True\n                            }\n                        ]),\n                    },\n                    {\n                        \"plan_rows\": 3,\n                        \"actual_rows\": 2,\n                        \"actual_loops\": 1,\n                        \"plan_type\": \"Result\",\n                        \"properties\": tb.bag([\n                            {\n                                \"title\": \"parent_relationship\",\n                                \"value\": \"Outer\",\n                                \"type\": \"text\",\n                                \"important\": False\n                            },\n                            {\n                                \"title\": \"one_time_filter\",\n                                \"value\": '(\"User~2\".id = \"User~2\".id)',\n                                \"type\": \"expr\",\n                                \"important\": False\n                            }\n                        ]),\n                    },\n                    {\n                        \"plan_rows\": 3,\n                        \"actual_rows\": 2,\n                        \"actual_loops\": 1,\n                        \"plan_type\": \"IndexScan\",\n                        \"properties\": tb.bag([\n                            {\n                                \"title\": \"parent_relationship\",\n                                \"value\": \"Outer\",\n                                \"type\": \"text\",\n                                \"important\": False\n                            },\n                            {\n                                \"title\": \"schema\",\n                                \"value\": \"edgedbpub\",\n                                \"type\": \"text\",\n                                \"important\": False\n                            },\n                            {\n                                \"title\": \"alias\",\n                                \"value\": \"Issue~1\",\n                                \"type\": \"text\",\n                                \"important\": False\n                            },\n                            {\n                                \"title\": \"relation_name\",\n                                \"value\": \"Issue\",\n                                \"type\": \"relation\",\n                                \"important\": True\n                            },\n                            {\n                                \"title\": \"scan_direction\",\n                                \"value\": \"Forward\",\n                                \"type\": \"text\",\n                                \"important\": True\n                            },\n                            {\n                                \"title\": \"index_name\",\n                                \"value\": \"Issue.owner index\",\n                                \"type\": \"index\",\n                                \"important\": True\n                            },\n                            {\n                                \"title\": \"index_cond\",\n                                \"type\": \"expr\",\n                                \"important\": False\n                            }\n                        ]),\n                    }\n                ]),\n                \"subplans\": []\n            }]\n        }\n\n        self.assert_plan(res['fine_grained'], shape)\n        self.assertEqual(len(res['buffers']), 2)\n        self.assertEqual(res['buffers'][1], \".<owner[is default::Issue]\")\n\n    async def test_edgeql_explain_inheritance_01(self):\n        res = await self.explain('''\n            WITH X := Text, select X;\n        ''')\n\n        shape = {\n            \"contexts\": tb.bag([\n                {\n                    \"start\": 31,\n                    \"end\": 35,\n                    \"buffer_idx\": 0,\n                    \"text\": \"Text\",\n                },\n                {\n                    \"start\": 44,\n                    \"end\": 45,\n                    \"buffer_idx\": 0,\n                    \"text\": \"X\",\n                },\n            ]),\n            \"pipeline\": tb.bag([\n                {\n                    \"plan_rows\": 33,\n                    \"actual_rows\": 33,\n                    \"actual_loops\": 1,\n                    \"plan_type\": \"Result\",\n                    \"properties\": [],\n                },\n                {\n                    \"plan_rows\": 33,\n                    \"actual_rows\": 33,\n                    \"actual_loops\": 1,\n                    \"plan_type\": \"Append\",\n                    \"properties\": [\n                        {\n                            \"title\": \"parent_relationship\",\n                            \"value\": \"Outer\",\n                            \"type\": \"text\",\n                            \"important\": False,\n                        },\n                    ],\n                },\n            ]),\n            \"subplans\": tb.bag([\n                {\n                    \"pipeline\": [\n                        {\n                            \"plan_rows\": 1,\n                            \"actual_rows\": 1,\n                            \"actual_loops\": 1,\n                            \"plan_type\": \"IndexOnlyScan\",\n                            \"properties\": tb.bag([\n                                {\n                                    \"title\": \"parent_relationship\",\n                                    \"value\": \"Member\",\n                                    \"type\": \"text\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"schema\",\n                                    \"value\": \"edgedbpub\",\n                                    \"type\": \"text\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"alias\",\n                                    \"value\": \"LogEntry~1\",\n                                    \"type\": \"text\",\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": \"relation_name\",\n                                    \"value\": \"LogEntry\",\n                                    \"type\": \"relation\",\n                                    \"important\": True,\n                                },\n                                {\n                                    \"title\": \"scan_direction\",\n                                    \"value\": \"Forward\",\n                                    \"type\": \"text\",\n                                    \"important\": True,\n                                },\n                                {\n                                    \"title\": \"index_name\",\n                                    \"value\":\n                                        \"constraint 'std::exclusive' of \"\n                                        \"property 'id' of object type \"\n                                        \"'default::LogEntry'\",\n                                    \"type\": \"index\",\n                                    \"important\": True,\n                                },\n                                {\n                                    \"title\": \"heap_fetches\",\n                                    \"type\": \"float\",\n                                    \"important\": False,\n                                },\n                            ]),\n                        },\n                    ],\n                    \"subplans\": [],\n                },\n                {\n                    \"pipeline\": [\n                        {\n                            \"plan_rows\": 31,\n                            \"actual_rows\": 31,\n                            \"actual_loops\": 1,\n                            \"plan_type\": \"IndexOnlyScan\",\n                            \"properties\": tb.bag([\n                                {\n                                    \"title\": \"parent_relationship\",\n                                    \"value\": \"Member\",\n                                    \"type\": \"text\",\n                                    \"important\": False\n                                },\n                                {\n                                    \"title\": \"schema\",\n                                    \"value\": \"edgedbpub\",\n                                    \"type\": \"text\",\n                                    \"important\": False\n                                },\n                                {\n                                    \"title\": \"alias\",\n                                    \"value\": \"Issue~1\",\n                                    \"type\": \"text\",\n                                    \"important\": False\n                                },\n                                {\n                                    \"title\": \"relation_name\",\n                                    \"value\": \"Issue\",\n                                    \"type\": \"relation\",\n                                    \"important\": True\n                                },\n                                {\n                                    \"title\": \"scan_direction\",\n                                    \"value\": \"Forward\",\n                                    \"type\": \"text\",\n                                    \"important\": True\n                                },\n                                {\n                                    \"title\": \"index_name\",\n                                    \"value\":\n                                        \"constraint 'std::exclusive' of \"\n                                        \"property 'id' of object type \"\n                                        \"'default::Issue'\",\n                                    \"type\": \"index\",\n                                    \"important\": True\n                                },\n                                {\n                                    \"title\": \"heap_fetches\",\n                                    \"type\": \"float\",\n                                    \"important\": False\n                                },\n                            ]),\n                        },\n                    ],\n                    \"subplans\": [],\n                },\n                {\n                    \"pipeline\": [\n                        {\n                            \"plan_rows\": 1,\n                            \"actual_rows\": 1,\n                            \"actual_loops\": 1,\n                            \"plan_type\": \"IndexOnlyScan\",\n                            \"properties\": tb.bag([\n                                {\n                                    \"title\": \"parent_relationship\",\n                                    \"value\": \"Member\",\n                                    \"type\": \"text\",\n                                    \"important\": False\n                                },\n                                {\n                                    \"title\": \"schema\",\n                                    \"value\": \"edgedbpub\",\n                                    \"type\": \"text\",\n                                    \"important\": False\n                                },\n                                {\n                                    \"title\": \"alias\",\n                                    \"value\": \"Comment~1\",\n                                    \"type\": \"text\",\n                                    \"important\": False\n                                },\n                                {\n                                    \"title\": \"relation_name\",\n                                    \"value\": \"Comment\",\n                                    \"type\": \"relation\",\n                                    \"important\": True\n                                },\n                                {\n                                    \"title\": \"scan_direction\",\n                                    \"value\": \"Forward\",\n                                    \"type\": \"text\",\n                                    \"important\": True\n                                },\n                                {\n                                    \"title\": \"index_name\",\n                                    \"value\":\n                                        \"constraint 'std::exclusive' of \"\n                                        \"property 'id' of object type \"\n                                        \"'default::Comment'\",\n                                    \"type\": \"index\",\n                                    \"important\": True\n                                },\n                                {\n                                    \"title\": \"heap_fetches\",\n                                    \"type\": \"float\",\n                                    \"important\": False\n                                },\n                            ]),\n                        },\n                    ],\n                    \"subplans\": [],\n                },\n            ]),\n        }\n\n        self.assert_plan(res['fine_grained'], shape)\n\n    async def test_edgeql_explain_type_intersect_01(self):\n        res = await self.explain('''\n            select Text {\n                body,\n                z := [is Issue].name\n            };\n        ''')\n\n        shape = {\n            \"pipeline\": [\n                {\n                    \"plan_rows\": 33,\n                    \"actual_rows\": 33,\n                    \"actual_loops\": 1,\n                    \"plan_type\": 'Result',\n                    \"properties\": [],\n                },\n            ],\n            \"subplans\": tb.bag([\n                {\n                    \"contexts\": [\n                        {\n                            \"start\": 28,\n                            \"end\": 93,\n                            \"buffer_idx\": 0,\n                            'text': (\n                                'Text {\\n'\n                                '                body,\\n'\n                                '                z := [is Issue].name'\n                            )\n                        },\n                    ],\n                    \"pipeline\": [\n                        {\n                            \"plan_rows\": 33,\n                            \"actual_rows\": 33,\n                            \"actual_loops\": 1,\n                            \"plan_type\": 'Append',\n                            \"properties\": [\n                                {\n                                    \"title\": 'parent_relationship',\n                                    \"value\": 'Outer',\n                                    \"type\": 'text',\n                                    \"important\": False,\n                                },\n                            ],\n                        },\n                    ],\n                    \"subplans\": tb.bag([\n                        {\n                            \"pipeline\": [\n                                {\n                                    \"plan_rows\": 1,\n                                    \"actual_rows\": 1,\n                                    \"actual_loops\": 1,\n                                    \"plan_type\": 'SeqScan',\n                                    \"properties\": tb.bag([\n                                        {\n                                            \"title\": 'parent_relationship',\n                                            \"value\": 'Member',\n                                            \"type\": 'text',\n                                            \"important\": False,\n                                        },\n                                        {\n                                            \"title\": 'schema',\n                                            \"value\": 'edgedbpub',\n                                            \"type\": 'text',\n                                            \"important\": False,\n                                        },\n                                        {\n                                            \"title\": 'alias',\n                                            \"value\": 'LogEntry~1',\n                                            \"type\": 'text',\n                                            \"important\": False,\n                                        },\n                                        {\n                                            \"title\": 'relation_name',\n                                            \"value\": 'LogEntry',\n                                            \"type\": 'relation',\n                                            \"important\": True,\n                                        },\n                                    ]),\n                                },\n                            ],\n                            \"subplans\": [],\n                        },\n                        {\n                            \"pipeline\": [\n                                {\n                                    \"plan_rows\": 1,\n                                    \"actual_rows\": 1,\n                                    \"actual_loops\": 1,\n                                    \"plan_type\": 'SeqScan',\n                                    \"properties\": tb.bag([\n                                        {\n                                            \"title\": 'parent_relationship',\n                                            \"value\": 'Member',\n                                            \"type\": 'text',\n                                            \"important\": False,\n                                        },\n                                        {\n                                            \"title\": 'schema',\n                                            \"value\": 'edgedbpub',\n                                            \"type\": 'text',\n                                            \"important\": False,\n                                        },\n                                        {\n                                            \"title\": 'alias',\n                                            \"value\": 'Comment~1',\n                                            \"type\": 'text',\n                                            \"important\": False,\n                                        },\n                                        {\n                                            \"title\": 'relation_name',\n                                            \"value\": 'Comment',\n                                            \"type\": 'relation',\n                                            \"important\": True,\n                                        },\n                                    ]),\n                                },\n                            ],\n                            \"subplans\": [],\n                        },\n                        {\n                            \"pipeline\": [\n                                {\n                                    \"plan_rows\": 31,\n                                    \"actual_rows\": 31,\n                                    \"actual_loops\": 1,\n                                    \"plan_type\": 'SeqScan',\n                                    \"properties\": tb.bag([\n                                        {\n                                            \"title\": 'parent_relationship',\n                                            \"value\": 'Member',\n                                            \"type\": 'text',\n                                            \"important\": False,\n                                        },\n                                        {\n                                            \"title\": 'schema',\n                                            \"value\": 'edgedbpub',\n                                            \"type\": 'text',\n                                            \"important\": False,\n                                        },\n                                        {\n                                            \"title\": 'alias',\n                                            \"value\": 'Issue~1',\n                                            \"type\": 'text',\n                                            \"important\": False,\n                                        },\n                                        {\n                                            \"title\": 'relation_name',\n                                            \"value\": 'Issue',\n                                            \"type\": 'relation',\n                                            \"important\": True,\n                                        },\n                                    ]),\n                                },\n                            ],\n                            \"subplans\": [],\n                        },\n                    ]),\n                },\n                {\n                    \"contexts\": [\n                        {\n                            \"start\": 73,\n                            \"end\": 93,\n                            \"buffer_idx\": 0,\n                            \"text\": 'z := [is Issue].name'\n                        },\n                    ],\n                    \"pipeline\": [\n                        {\n                            \"plan_rows\": 1,\n                            \"actual_rows\": 1,\n                            \"actual_loops\": 33,\n                            \"plan_type\": 'IndexScan',\n                            \"properties\": tb.bag([\n                                {\n                                    \"title\": 'parent_relationship',\n                                    \"value\": 'SubPlan',\n                                    \"type\": 'text',\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": 'subplan_name',\n                                    \"value\": 'SubPlan 1',\n                                    \"type\": 'text',\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": 'schema',\n                                    \"value\": 'edgedbpub',\n                                    \"type\": 'text',\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": 'alias',\n                                    \"value\": 'Issue~2',\n                                    \"type\": 'text',\n                                    \"important\": False,\n                                },\n                                {\n                                    \"title\": 'relation_name',\n                                    \"value\": 'Issue',\n                                    \"type\": 'relation',\n                                    \"important\": True,\n                                },\n                                {\n                                    \"title\": 'scan_direction',\n                                    \"value\": 'Forward',\n                                    \"type\": 'text',\n                                    \"important\": True,\n                                },\n                                {\n                                    \"title\": 'index_name',\n                                    \"value\":\n                                        \"constraint 'std::exclusive' \"\n                                        \"of property 'id' of object \"\n                                        \"type 'default::Issue'\",\n                                    \"type\": 'index',\n                                    \"important\": True,\n                                },\n                                {\n                                    \"title\": 'index_cond',\n                                    \"type\": 'expr',\n                                    \"important\": False,\n                                },\n                            ]),\n                        },\n                    ],\n                    \"subplans\": [],\n                },\n            ]),\n        }\n\n        self.assert_plan(res['fine_grained'], shape)\n\n    async def test_edgeql_explain_insert_01(self):\n        # Use an ad-hoc connection to avoid TRANSACTION_ISOLATION\n        con = await self.connect()\n        try:\n            res = await self.explain('''\n                insert User { name := 'Fantix' }\n            ''', execute=True, con=con)\n            self.assert_plan(res['fine_grained'], {\n                'pipeline': [{'plan_type': 'NestedLoop'}],\n            })\n            self.assertFalse(await con.query('''\n                select User { id, name } filter .name = 'Fantix'\n            '''))\n        finally:\n            await con.aclose()\n\n    async def test_edgeql_explain_insert_02(self):\n        async with self.con.transaction():\n            await self.con.execute('''\n                insert User { name := 'Sully' }\n            ''')\n            res = await self.explain('''\n                insert User { name := 'Fantix' }\n            ''', execute=True)\n            self.assert_plan(res['fine_grained'], {\n                'pipeline': [{'plan_type': 'NestedLoop'}],\n            })\n            self.assertTrue(await self.con.query('''\n                select User { id, name } filter .name = 'Sully'\n            '''))\n            self.assertFalse(await self.con.query('''\n                select User { id, name } filter .name = 'Fantix'\n            '''))\n\n        self.assertTrue(await self.con.query('''\n            select User { id, name } filter .name = 'Sully'\n        '''))\n        self.assertFalse(await self.con.query('''\n            select User { id, name } filter .name = 'Fantix'\n        '''))\n\n    async def test_edgeql_explain_options_01(self):\n        res = await self.explain('''\n            select User\n        ''', execute=False)\n        self.assertNotIn(\n            'actual_startup_time',\n            res['fine_grained']['pipeline'][0],\n        )\n        self.assertEqual(\n            {'buffers': False, 'execute': False},\n            res['arguments'],\n        )\n\n        res = json.loads(await self.con.query_single('''\n            analyze (buffers := True) select User\n        '''))\n        self.assertIn('shared_read_blocks', res['fine_grained']['pipeline'][0])\n        self.assertEqual({'buffers': True, 'execute': True}, res['arguments'])\n\n        res = json.loads(await self.con.query_single('''\n            analyze (buffers := false) select User\n        '''))\n        self.assertNotIn(\n            'shared_read_blocks',\n            res['fine_grained']['pipeline'][0],\n        )\n        self.assertEqual({'buffers': False, 'execute': True}, res['arguments'])\n\n    async def test_edgeql_explain_options_02(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r\"unknown ANALYZE argument\"\n        ):\n            await self.con.query_single('''\n                analyze (bogus_argument := True) select User\n            ''')\n\n        async with self.assertRaisesRegexTx(\n            edgedb.QueryError,\n            r\"incorrect type\"\n        ):\n            await self.con.query_single('''\n                analyze (execute := \"hell yeah\") select User\n            ''')\n\n    def assert_index_in_plan(self, data, propname, message='Index test'):\n        # First we check the plan_type as there are a couple of valid options\n        # here: IndexScan and BitmapHeapScan, but they have different\n        # substructure to check.\n        self.assert_plan(\n            data,\n            {\n                'fine_grained': {\n                    'pipeline': [dict()]\n                }\n            },\n            message=message,\n        )\n        plan_type = data['fine_grained']['pipeline'][0]['plan_type']\n        self.assert_plan(\n            data['fine_grained'],\n            self.get_gist_index_expected_res(\n                propname, plan_type, message=message\n            ),\n            message=message,\n        )\n\n    def get_gist_index_expected_res(self, fname, plan_type, message=None):\n        if plan_type == 'IndexScan':\n            return {\n                \"pipeline\": [\n                    {\n                        \"plan_type\": \"IndexScan\",\n                        \"properties\": tb.bag([\n                            {\n                                'important': False,\n                                'title': 'schema',\n                                'type': 'text',\n                                'value': 'edgedbpub',\n                            },\n                            {\n                                'important': False,\n                                'title': 'alias',\n                                'type': 'text',\n                            },\n                            {\n                                'important': True,\n                                'title': 'relation_name',\n                                'type': 'relation',\n                                'value': 'RangeTest',\n                            },\n                            {\n                                'important': True,\n                                'title': 'scan_direction',\n                                'type': 'text',\n                                'value': str,\n                            },\n                            {\n                                'important': True,\n                                'title': 'index_name',\n                                'type': 'index',\n                                'value':\n                                    f\"index 'std::pg::gist' of object type \"\n                                    f\"'default::RangeTest' on (.{fname})\",\n                            },\n                            {\n                                'important': False,\n                                'title': 'index_cond',\n                                'type': 'expr',\n                            },\n                        ]),\n                    },\n                ],\n            }\n        elif plan_type == 'BitmapHeapScan':\n            return {\n                \"pipeline\": [\n                    {\n                        \"plan_type\": \"BitmapHeapScan\",\n                    },\n                ],\n                \"subplans\": [\n                    {\n                        \"pipeline\": [\n                            {\n                                \"plan_type\": \"BitmapIndexScan\",\n                                \"properties\": tb.bag([\n                                    {\n                                        'important': False,\n                                        'title': 'parent_relationship',\n                                        'type': 'text',\n                                        'value': 'Outer',\n                                    },\n                                    {\n                                        'important': True,\n                                        'title': 'index_name',\n                                        'type': 'index',\n                                        'value':\n                                            f\"index 'std::pg::gist' of object\"\n                                            f\" type 'default::RangeTest'\"\n                                            f\" on (.{fname})\",\n                                    },\n                                    {\n                                        'important': False,\n                                        'title': 'index_cond',\n                                        'type': 'expr',\n                                    },\n                                ]),\n                            },\n                        ],\n                    },\n                ],\n            }\n        else:\n            self.fail(\n                f'{message}: \"plan_type\" expected to be \"IndexScan\" or '\n                f'\"BitmapHeapScan\", got {plan_type!r}'\n            )\n\n    async def test_edgeql_explain_ranges_contains_01(self):\n        res = await self.explain('''\n            select RangeTest {id, rval}\n            filter contains(.rval, 295)\n        ''')\n        self.assert_index_in_plan(res, 'rval')\n\n        res = await self.explain('''\n            select RangeTest {id, mval}\n            filter contains(.mval, 295)\n        ''')\n        self.assert_index_in_plan(res, 'mval')\n\n        res = await self.explain('''\n            select RangeTest {id, rdate}\n            filter contains(.rdate, <cal::local_date>'2000-01-05')\n        ''')\n        self.assert_index_in_plan(res, 'rdate')\n\n        res = await self.explain('''\n            select RangeTest {id, mdate}\n            filter contains(.mdate, <cal::local_date>'2000-01-05')\n        ''')\n        self.assert_index_in_plan(res, 'mdate')\n\n    async def test_edgeql_explain_ranges_contains_02(self):\n        res = await self.explain('''\n            select RangeTest {id, rval}\n            filter contains(.rval, range(295, 299))\n        ''')\n        self.assert_index_in_plan(res, 'rval')\n\n        res = await self.explain('''\n            select RangeTest {id, mval}\n            filter contains(.mval, range(295, 299))\n        ''')\n        self.assert_index_in_plan(res, 'mval')\n\n        res = await self.explain('''\n            select RangeTest {id, rdate}\n            filter contains(\n                .rdate,\n                range(<cal::local_date>'2000-01-05',\n                      <cal::local_date>'2000-01-10')\n            )\n        ''')\n        self.assert_index_in_plan(res, 'rdate')\n\n        res = await self.explain('''\n            select RangeTest {id, mdate}\n            filter contains(\n                .mdate,\n                range(<cal::local_date>'2000-01-05',\n                      <cal::local_date>'2000-01-10')\n            )\n        ''')\n        self.assert_index_in_plan(res, 'mdate')\n\n    async def test_edgeql_explain_ranges_contains_03(self):\n        res = await self.explain('''\n            select RangeTest {id, mval}\n            filter contains(\n                .mval,\n                multirange([\n                    range(-299, 297),\n                    range(297, 299),\n                ])\n            )\n        ''')\n        self.assert_index_in_plan(res, 'mval')\n\n        res = await self.explain('''\n            select RangeTest {id, mdate}\n            filter contains(\n                .mdate,\n                multirange([\n                    range(<cal::local_date>'2000-01-05',\n                          <cal::local_date>'2000-01-10'),\n                    range(<cal::local_date>'2010-01-05',\n                          <cal::local_date>'2010-01-10'),\n                ])\n            )\n        ''')\n        self.assert_index_in_plan(res, 'mdate')\n\n    async def test_edgeql_explain_ranges_overlaps_01(self):\n        # The field is the first arg in `overlaps`\n        res = await self.explain('''\n            select RangeTest {id, rval}\n            filter overlaps(.rval, range(295, 299))\n        ''')\n        self.assert_index_in_plan(res, 'rval')\n\n        res = await self.explain('''\n            select RangeTest {id, mval}\n            filter overlaps(.mval, range(295, 299))\n        ''')\n        self.assert_index_in_plan(res, 'mval')\n\n        res = await self.explain('''\n            select RangeTest {id, rdate}\n            filter overlaps(\n                .rdate,\n                range(<cal::local_date>'2000-01-05',\n                      <cal::local_date>'2000-01-10'),\n            )\n        ''')\n        self.assert_index_in_plan(res, 'rdate')\n\n        res = await self.explain('''\n            select RangeTest {id, mdate}\n            filter overlaps(\n                .mdate,\n                range(<cal::local_date>'2000-01-05',\n                      <cal::local_date>'2000-01-10'),\n            )\n        ''')\n        self.assert_index_in_plan(res, 'mdate')\n\n    async def test_edgeql_explain_ranges_overlaps_02(self):\n        # The field is the second arg in `overlaps`\n        res = await self.explain('''\n            select RangeTest {id, rval}\n            filter overlaps(range(295, 299), .rval)\n        ''')\n        self.assert_index_in_plan(res, 'rval')\n\n        res = await self.explain('''\n            select RangeTest {id, mval}\n            filter overlaps(range(295, 299), .mval)\n        ''')\n        self.assert_index_in_plan(res, 'mval')\n\n        res = await self.explain('''\n            select RangeTest {id, rdate}\n            filter overlaps(\n                range(<cal::local_date>'2000-01-05',\n                      <cal::local_date>'2000-01-10'),\n                .rdate,\n            )\n        ''')\n        self.assert_index_in_plan(res, 'rdate')\n\n        res = await self.explain('''\n            select RangeTest {id, mdate}\n            filter overlaps(\n                range(<cal::local_date>'2000-01-05',\n                      <cal::local_date>'2000-01-10'),\n                .mdate,\n            )\n        ''')\n        self.assert_index_in_plan(res, 'mdate')\n\n    async def test_edgeql_explain_ranges_adjacent_01(self):\n        # The field is the first arg in `adjacent`\n        res = await self.explain('''\n            select RangeTest {id, rval}\n            filter adjacent(.rval, range(295, 299))\n        ''')\n        self.assert_index_in_plan(res, 'rval')\n\n        res = await self.explain('''\n            select RangeTest {id, mval}\n            filter adjacent(.mval, range(295, 299))\n        ''')\n        self.assert_index_in_plan(res, 'mval')\n\n        res = await self.explain('''\n            select RangeTest {id, rdate}\n            filter adjacent(\n                .rdate,\n                range(<cal::local_date>'2000-01-05',\n                      <cal::local_date>'2000-01-10'),\n            )\n        ''')\n        self.assert_index_in_plan(res, 'rdate')\n\n        res = await self.explain('''\n            select RangeTest {id, mdate}\n            filter adjacent(\n                .mdate,\n                range(<cal::local_date>'2000-01-05',\n                      <cal::local_date>'2000-01-10'),\n            )\n        ''')\n        self.assert_index_in_plan(res, 'mdate')\n\n    async def test_edgeql_explain_ranges_adjacent_02(self):\n        # The field is the second arg in `adjacent`\n        res = await self.explain('''\n            select RangeTest {id, rval}\n            filter adjacent(range(295, 299), .rval)\n        ''')\n        self.assert_index_in_plan(res, 'rval')\n\n        res = await self.explain('''\n            select RangeTest {id, mval}\n            filter adjacent(range(295, 299), .mval)\n        ''')\n        self.assert_index_in_plan(res, 'mval')\n\n        res = await self.explain('''\n            select RangeTest {id, rdate}\n            filter adjacent(\n                range(<cal::local_date>'2000-01-05',\n                      <cal::local_date>'2000-01-10'),\n                .rdate,\n            )\n        ''')\n        self.assert_index_in_plan(res, 'rdate')\n\n        res = await self.explain('''\n            select RangeTest {id, mdate}\n            filter adjacent(\n                range(<cal::local_date>'2000-01-05',\n                      <cal::local_date>'2000-01-10'),\n                .mdate,\n            )\n        ''')\n        self.assert_index_in_plan(res, 'mdate')\n\n    async def test_edgeql_explain_ranges_strictly_below_01(self):\n        # The field is the first arg in `strictly_below`\n        res = await self.explain('''\n            select RangeTest {id, rval}\n            filter strictly_below(.rval, range(-50, 50))\n        ''')\n        self.assert_index_in_plan(res, 'rval')\n\n        res = await self.explain('''\n            select RangeTest {id, mval}\n            filter strictly_below(.mval, range(-50, 50))\n        ''')\n        self.assert_index_in_plan(res, 'mval')\n\n        res = await self.explain('''\n            select RangeTest {id, rdate}\n            filter strictly_below(\n                .rdate,\n                range(<cal::local_date>'2005-01-05',\n                      <cal::local_date>'2012-02-10'),\n            )\n        ''')\n        self.assert_index_in_plan(res, 'rdate')\n\n        res = await self.explain('''\n            select RangeTest {id, mdate}\n            filter strictly_below(\n                .mdate,\n                range(<cal::local_date>'2005-01-05',\n                      <cal::local_date>'2012-02-10'),\n            )\n        ''')\n        self.assert_index_in_plan(res, 'mdate')\n\n    async def test_edgeql_explain_ranges_strictly_below_02(self):\n        # The field is the second arg in `strictly_below`\n        res = await self.explain('''\n            select RangeTest {id, rval}\n            filter strictly_below(range(-50, 50), .rval)\n        ''')\n        self.assert_index_in_plan(res, 'rval')\n\n        res = await self.explain('''\n            select RangeTest {id, mval}\n            filter strictly_below(range(-50, 50), .mval)\n        ''')\n        self.assert_index_in_plan(res, 'mval')\n\n        res = await self.explain('''\n            select RangeTest {id, rdate}\n            filter strictly_below(\n                range(<cal::local_date>'2005-01-05',\n                      <cal::local_date>'2012-02-10'),\n                .rdate,\n            )\n        ''')\n        self.assert_index_in_plan(res, 'rdate')\n\n        res = await self.explain('''\n            select RangeTest {id, mdate}\n            filter strictly_below(\n                range(<cal::local_date>'2005-01-05',\n                      <cal::local_date>'2012-02-10'),\n                .mdate,\n            )\n        ''')\n        self.assert_index_in_plan(res, 'mdate')\n\n    async def test_edgeql_explain_ranges_strictly_above_01(self):\n        # The field is the first arg in `strictly_above`\n        res = await self.explain('''\n            select RangeTest {id, rval}\n            filter strictly_above(.rval, range(-50, 50))\n        ''')\n        self.assert_index_in_plan(res, 'rval')\n\n        res = await self.explain('''\n            select RangeTest {id, mval}\n            filter strictly_above(.mval, range(-50, 50))\n        ''')\n        self.assert_index_in_plan(res, 'mval')\n\n        res = await self.explain('''\n            select RangeTest {id, rdate}\n            filter strictly_above(\n                .rdate,\n                range(<cal::local_date>'2005-01-05',\n                      <cal::local_date>'2012-02-10'),\n            )\n        ''')\n        self.assert_index_in_plan(res, 'rdate')\n\n        res = await self.explain('''\n            select RangeTest {id, mdate}\n            filter strictly_above(\n                .mdate,\n                range(<cal::local_date>'2005-01-05',\n                      <cal::local_date>'2012-02-10'),\n            )\n        ''')\n        self.assert_index_in_plan(res, 'mdate')\n\n    async def test_edgeql_explain_ranges_strictly_above_02(self):\n        # The field is the second arg in `strictly_above`\n        res = await self.explain('''\n            select RangeTest {id, rval}\n            filter strictly_above(range(-50, 50), .rval)\n        ''')\n        self.assert_index_in_plan(res, 'rval')\n\n        res = await self.explain('''\n            select RangeTest {id, mval}\n            filter strictly_above(range(-50, 50), .mval)\n        ''')\n        self.assert_index_in_plan(res, 'mval')\n\n        res = await self.explain('''\n            select RangeTest {id, rdate}\n            filter strictly_above(\n                range(<cal::local_date>'2005-01-05',\n                      <cal::local_date>'2012-02-10'),\n                .rdate,\n            )\n        ''')\n        self.assert_index_in_plan(res, 'rdate')\n\n        res = await self.explain('''\n            select RangeTest {id, mdate}\n            filter strictly_above(\n                range(<cal::local_date>'2005-01-05',\n                      <cal::local_date>'2012-02-10'),\n                .mdate,\n            )\n        ''')\n        self.assert_index_in_plan(res, 'mdate')\n\n    async def test_edgeql_explain_ranges_bounded_below_01(self):\n        # The field is the first arg in `bounded_below`\n        res = await self.explain('''\n            select RangeTest {id, rval}\n            filter bounded_below(.rval, range(-50, 50))\n        ''')\n        self.assert_index_in_plan(res, 'rval')\n\n        res = await self.explain('''\n            select RangeTest {id, mval}\n            filter bounded_below(.mval, range(-50, 50))\n        ''')\n        self.assert_index_in_plan(res, 'mval')\n\n        res = await self.explain('''\n            select RangeTest {id, rdate}\n            filter bounded_below(\n                .rdate,\n                range(<cal::local_date>'2012-01-05',\n                      <cal::local_date>'2015-02-10'),\n            )\n        ''')\n        self.assert_index_in_plan(res, 'rdate')\n\n        res = await self.explain('''\n            select RangeTest {id, mdate}\n            filter bounded_below(\n                .mdate,\n                range(<cal::local_date>'2012-01-05',\n                      <cal::local_date>'2015-02-10'),\n            )\n        ''')\n        self.assert_index_in_plan(res, 'mdate')\n\n    async def test_edgeql_explain_ranges_bounded_above_01(self):\n        # The field is the first arg in `bounded_above`\n        res = await self.explain('''\n            select RangeTest {id, rval}\n            filter bounded_above(.rval, range(-50, 50))\n        ''')\n        self.assert_index_in_plan(res, 'rval')\n\n        res = await self.explain('''\n            select RangeTest {id, mval}\n            filter bounded_above(.mval, range(-50, 50))\n        ''')\n        self.assert_index_in_plan(res, 'mval')\n\n        res = await self.explain('''\n            select RangeTest {id, rdate}\n            filter bounded_above(\n                .rdate,\n                range(<cal::local_date>'2005-01-05',\n                      <cal::local_date>'2012-02-10'),\n            )\n        ''')\n        self.assert_index_in_plan(res, 'rdate')\n\n        res = await self.explain('''\n            select RangeTest {id, mdate}\n            filter bounded_above(\n                .mdate,\n                range(<cal::local_date>'2005-01-05',\n                      <cal::local_date>'2012-02-10'),\n            )\n        ''')\n        self.assert_index_in_plan(res, 'mdate')\n\n    async def test_edgeql_explain_json_contains_01(self):\n        res = await self.explain('''\n            select JSONTest {id, val}\n            filter contains(.val, <json>(b := 123))\n        ''')\n        res = res['fine_grained']\n\n        if len(res['subplans']) >= 2:\n            # Postgres version <16\n            res = res['subplans'][1]\n        else:\n            # Postgres version >=16\n            # Response seems to be inlined, so let's remove all but last thing\n            # in the pipeline.\n            res['pipeline'] = res['pipeline'][-1:]\n\n        self.assert_plan(\n            res,\n            {\n                \"pipeline\": [\n                    {\n                        \"plan_type\": \"BitmapHeapScan\",\n                    },\n                ],\n                \"subplans\": [\n                    {\n                        \"pipeline\": [\n                            {\n                                \"plan_type\": \"BitmapIndexScan\",\n                                \"properties\": tb.bag([\n                                    {\n                                        'important': False,\n                                        'title': 'parent_relationship',\n                                        'type': 'text',\n                                        'value': 'Outer',\n                                    },\n                                    {\n                                        'important': True,\n                                        'title': 'index_name',\n                                        'type': 'index',\n                                        'value':\n                                            f\"index 'std::pg::gin' of object\"\n                                            f\" type 'default::JSONTest'\"\n                                            f\" on (.val)\",\n                                    },\n                                    {\n                                        'important': False,\n                                        'title': 'index_cond',\n                                        'type': 'expr',\n                                    },\n                                ]),\n                            },\n                        ],\n                    },\n                ],\n            },\n        )\n\n    async def test_edgeql_explain_user_func_index_01(self):\n        await self._assert_index_use('''\n            select Issue {id}\n            filter .number2 = '500!'\n        ''')\n\n    async def test_edgeql_explain_order_index_01(self):\n        # name has a regular index\n        await self._assert_index_use('''\n            select User {id, name, rank}\n            order by .name limit 1\n        ''')\n\n    async def test_edgeql_explain_order_index_02(self):\n        # id's index is via an exclusive constraint\n        await self._assert_index_use('''\n            select User {id, name, rank}\n            order by .id limit 1\n        ''')\n\n    async def test_edgeql_explain_order_index_03(self):\n        # id's index is via an exclusive constraint\n        await self._assert_index_use('''\n            select User {id, name, rank}\n            filter .id > <uuid>'611155ee-9fe9-11f0-a8be-91239325009b'\n            order by .id empty last limit 1\n        ''')\n\n    async def test_edgeql_explain_bug_5758(self):\n        # Issue #5758\n        res = await self.explain('''\n            with\n                module bug5758,\n                user := (select User filter .id =\n                    <uuid>'17b5649c-58b2-11ee-a739-4706f31ed5ab'),\n                track := (select Track filter .id =\n                    <uuid>'81958316-58d4-11ee-a739-9b645ff26c66'),\n                shouldLike := (user not in track.liked_by)\n            select (\n                update track\n                set {\n                    liked_by := assert_distinct(\n                        (.liked_by union user) if shouldLike\n                        else (select .liked_by filter .id != user.id)\n                    )\n                }\n            );\n        ''', execute=False)\n        # We use execute := False above because we actually don't have data,\n        # but we can target the issue reliably with the \"default\" plan.\n        #\n        # The bug is that when coarse plan is generated \"main_alias\" may not\n        # be found in the plan, causing the coarse plan to be None.\n        #\n        # Part of the problem with this kind of bug is that we can definitely\n        # tell that having no coarse plan is cause by an exception, but\n        # besides that it's much harder to validate that the actual \"fixed\"\n        # coarse plan is \"good\".\n        self.assertIsNotNone(res['coarse_grained'])\n\n    async def test_edgeql_explain_bug_5791(self):\n        # Issue #5758\n        res = await self.explain('''\n            with\n                module bug5791,\n                users := (\n                    SELECT UserPreference\n                    FILTER count(\n                        File\n                        FILTER\n                            .userId = UserPreference.userId\n                            AND .isPremium = false\n                            AND .status = \"PUBLISHED\"\n                    ) >= 3\n                    AND .isHireable = true\n                ),\n                users_with_recent_files := (\n                    SELECT users {\n                        totalDownloadCount := sum((\n                            SELECT File\n                            FILTER\n                                .userId = users.userId\n                                AND .publishedAt >=\n                                    <datetime>\"2023-06-24T09:37:21.714Z\"\n                                AND .isPremium = false\n                                AND .status = \"PUBLISHED\"\n                            ).downloadCount\n                        ),\n                        files := (\n                            SELECT File {\n                                id,\n                                name,\n                                bgColor,\n                                isSticker,\n                                publishedAt,\n                                status,\n                                workflowId,\n                                userTags,\n                                userId,\n                                lottieSource,\n                                jsonSource,\n                                imageSource\n                            }\n                            FILTER\n                                .userId = users.userId\n                                AND .publishedAt >=\n                                    <datetime>\"2023-06-24T09:37:21.714Z\"\n                                AND .isPremium = false\n                                AND .status = \"PUBLISHED\"\n                            ORDER BY .downloadCount DESC\n                            LIMIT 3\n                        )\n                    }\n                )\n                SELECT users_with_recent_files {\n                    userId,\n                    isHireable,\n                    totalDownloadCount,\n                    files: {\n                        id,\n                        name,\n                        bgColor,\n                        isSticker,\n                        publishedAt,\n                        status,\n                        workflowId,\n                        userTags,\n                        userId,\n                        lottieSource,\n                        jsonSource,\n                        imageSource\n                    }\n                }\n                ORDER BY .totalDownloadCount DESC\n                OFFSET 0\n                LIMIT 6\n        ''', execute=False)\n        # We use execute := False above because we actually don't have data,\n        # but we can target the issue reliably with the \"default\" plan.\n        #\n        # The bug is that when coarse plan is generated \"main_alias\" may not\n        # be found in the plan, causing the coarse plan to be None.\n        #\n        # Part of the problem with this kind of bug is that we can definitely\n        # tell that having no coarse plan is cause by an exception, but\n        # besides that it's much harder to validate that the actual \"fixed\"\n        # coarse plan is \"good\".\n        self.assertIsNotNone(res['coarse_grained'])\n\n\nclass NameTranslation(unittest.TestCase):\n\n    def test_name_default(self):\n        raliases = {'default': None}\n        self.assertEqual(\n            pg_tree._translate_name(sn.QualName('default', 'Type1'), raliases),\n            \"Type1\",\n        )\n        self.assertEqual(\n            pg_tree._translate_name(sn.QualName('mod1', 'Type2'), raliases),\n            \"mod1::Type2\",\n        )\n        self.assertEqual(\n            pg_tree._translate_name(sn.QualName('m1::m2', 'Type3'), raliases),\n            \"m1::m2::Type3\",\n        )\n\n    def test_name_aliases_01(self):\n        raliases = {'mod1': None, 'mod2': 'main'}\n        self.assertEqual(\n            pg_tree._translate_name(sn.QualName('default', 'Type1'), raliases),\n            \"default::Type1\",\n        )\n        self.assertEqual(\n            pg_tree._translate_name(sn.QualName('mod1', 'Type2'), raliases),\n            \"Type2\",\n        )\n        self.assertEqual(\n            pg_tree._translate_name(sn.QualName('mod2', 'Type3'), raliases),\n            \"main::Type3\",\n        )\n\n    def test_name_aliases_nested_01(self):\n        raliases = {'mod1': None, 'mod2': 'main', 'mod3::mod4': 'aux'}\n        self.assertEqual(\n            pg_tree._translate_name(sn.QualName('default', 'Type1'), raliases),\n            \"default::Type1\",\n        )\n        self.assertEqual(\n            pg_tree._translate_name(sn.QualName('mod1::mod2', 'Type2'),\n                                    raliases),\n            # default module is not replaced if there is nesting\n            \"mod1::mod2::Type2\",\n        )\n        self.assertEqual(\n            pg_tree._translate_name(sn.QualName('mod3::mod4::mod5', 'Type3'),\n                                    raliases),\n            \"aux::mod5::Type3\",\n        )\n        self.assertEqual(\n            pg_tree._translate_name(\n                sn.QualName('mod3::mod4::mod5::mod6', 'Type4'),\n                raliases,\n            ),\n            \"aux::mod5::mod6::Type4\",\n        )\n        self.assertEqual(\n            pg_tree._translate_name(\n                sn.QualName('mod3::mod7', 'Type5'),\n                raliases,\n            ),\n            \"mod3::mod7::Type5\",\n        )\n        self.assertEqual(\n            pg_tree._translate_name(\n                sn.QualName('mod2::mod3::mod4', 'Type6'),\n                raliases,\n            ),\n            \"main::mod3::mod4::Type6\",\n        )\n"
  },
  {
    "path": "tests/test_edgeql_expr_aliases.py",
    "content": "#\n# This source file is part of the EdgeDB open source project.\n#\n# Copyright 2017-present MagicStack Inc. and the EdgeDB authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\nimport json\nimport os.path\n\nfrom edb.testbase import server as tb\nfrom edb.tools import test\n\nimport edgedb\n\n\nclass TestEdgeQLExprAliases(tb.QueryTestCase):\n    '''The scope is to test expression aliases.'''\n\n    SCHEMA = os.path.join(os.path.dirname(__file__), 'schemas',\n                          'cards.esdl')\n\n    SETUP = [os.path.join(os.path.dirname(__file__), 'schemas',\n                          'cards_setup.edgeql')]\n\n    async def test_edgeql_aliases_basic_01(self):\n        await self.assert_query_result(\n            r'''\n                SELECT AirCard {\n                    name,\n                    owners: {\n                        name\n                    } ORDER BY .name\n                } ORDER BY AirCard.name;\n            ''',\n            [\n                {\n                    'name': 'Djinn',\n                    'owners': [{'name': 'Carol'}, {'name': 'Dave'}]\n                },\n                {\n                    'name': 'Giant eagle',\n                    'owners': [{'name': 'Carol'}, {'name': 'Dave'}]\n                },\n                {\n                    'name': 'Sprite',\n                    'owners': [{'name': 'Carol'}, {'name': 'Dave'}]\n                }\n            ],\n        )\n\n    async def test_edgeql_aliases_basic_02(self):\n        await self.con.execute('''\n            CREATE ALIAS expert_map := (\n                SELECT {\n                    ('Alice', 'pro'),\n                    ('Bob', 'noob'),\n                    ('Carol', 'noob'),\n                    ('Dave', 'casual'),\n                }\n            );\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT expert_map\n                ORDER BY expert_map;\n            ''',\n            [\n                ['Alice', 'pro'],\n                ['Bob', 'noob'],\n                ['Carol', 'noob'],\n                ['Dave', 'casual'],\n            ],\n        )\n\n        await self.con.execute('''\n            DROP ALIAS expert_map;\n        ''')\n\n    async def test_edgeql_aliases_basic_03(self):\n        await self.con.execute('''\n            CREATE ALIAS scores := (\n                SELECT {\n                    (name := 'Alice', score := 100, games := 10),\n                    (name := 'Bob', score := 11, games := 2),\n                    (name := 'Carol', score := 31, games := 5),\n                    (name := 'Dave', score := 78, games := 10),\n                }\n            );\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT scores ORDER BY scores.name;\n            ''',\n            [\n                {'name': 'Alice', 'score': 100, 'games': 10},\n                {'name': 'Bob', 'score': 11, 'games': 2},\n                {'name': 'Carol', 'score': 31, 'games': 5},\n                {'name': 'Dave', 'score': 78, 'games': 10},\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <tuple<str, int64, int64>>scores\n                ORDER BY .0;\n            ''',\n            [\n                ['Alice', 100, 10],\n                ['Bob', 11, 2],\n                ['Carol', 31, 5],\n                ['Dave', 78, 10],\n            ],\n        )\n\n        await self.assert_query_result(\n            r'''\n                SELECT <tuple<name: str, points: int64, plays: int64>>scores\n                ORDER BY .name;\n            ''',\n            [\n                {'name': 'Alice', 'points': 100, 'plays': 10},\n                {'name': 'Bob', 'points': 11, 'plays': 2},\n                {'name': 'Carol', 'points': 31, 'plays': 5},\n                {'name': 'Dave', 'points': 78, 'plays': 10},\n            ],\n        )\n\n        await self.con.execute('''\n            DROP ALIAS scores;\n        ''')\n\n    async def test_edgeql_aliases_basic_04(self):\n        await self.con.execute('''\n            CREATE ALIAS levels := {'pro', 'casual', 'noob'};\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT levels;\n            ''',\n            {'pro', 'casual', 'noob'},\n        )\n\n    async def test_edgeql_aliases_create_01(self):\n        await self.con.execute(r'''\n            CREATE ALIAS DCard := (\n                SELECT Card {\n                    # This is an identical computable to the one\n                    # present in the type, but it must be legal to\n                    # override the link with any compatible\n                    # expression.\n                    owners := (\n                        SELECT Card.<deck[IS User] {\n                            name_upper := str_upper(.name)\n                        }\n                    )\n                } FILTER Card.name LIKE 'D%'\n            );\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                SELECT DCard {\n                    name,\n                    owners: {\n                        name_upper,\n                    } ORDER BY .name\n                } ORDER BY DCard.name;\n            ''',\n            [\n                {\n                    'name': 'Djinn',\n                    'owners': [{'name_upper': 'CAROL'},\n                               {'name_upper': 'DAVE'}],\n                },\n                {\n                    'name': 'Dragon',\n                    'owners': [{'name_upper': 'ALICE'},\n                               {'name_upper': 'DAVE'}],\n                },\n                {\n                    'name': 'Dwarf',\n                    'owners': [{'name_upper': 'BOB'},\n                               {'name_upper': 'CAROL'}],\n                }\n            ],\n        )\n\n        await self.con.execute('DROP ALIAS DCard;')\n\n        # Check that we can recreate the alias.\n        await self.con.execute(r'''\n            CREATE ALIAS DCard := (\n                SELECT Card {\n                    owners := (\n                        SELECT Card.<deck[IS User] {\n                            name_upper := str_upper(.name)\n                        }\n                    )\n                } FILTER Card.name LIKE 'D%'\n            );\n        ''')\n\n        await self.assert_query_result(\n            r'''\n                WITH\n                    MODULE schema,\n                    DCardT := (SELECT ObjectType\n                               FILTER .name = 'default::DCard'),\n                    DCardOwners := (SELECT DCardT.links\n                                    FILTER .name = 'owners')\n                SELECT\n                    DCardOwners {\n                        target[IS ObjectType]: {\n                            name,\n                            pointers: {\n                                name\n                            } FILTER .name = 'name_upper'\n                        }\n                    }\n            ''',\n            [{\n                'target': {\n                    'name': 'default::__DCard__owners',\n                    'pointers': [\n                        {\n                            'name': 'name_upper',\n                        }\n                    ]\n                }\n            }]\n        )\n\n    async def test_edgeql_aliases_filter_01(self):\n        await self.assert_query_result(\n            r'''\n                SELECT FireCard {name}\n                FILTER FireCard IN DaveCard\n                ORDER BY FireCard.name;\n            ''',\n            [{'name': 'Dragon'}],\n        )\n\n    async def test_edgeql_aliases_filter02(self):\n        await self.assert_query_result(\n            r'''\n                SELECT AirCard {name}\n                FILTER AirCard NOT IN (SELECT Card FILTER Card.name LIKE 'D%')\n                ORDER BY AirCard.name;\n            ''',\n            [\n                {'name': 'Giant eagle'},\n                {'name': 'Sprite'},\n            ],\n        )\n\n    async def test_edgeql_computable_link_01(self):\n        await self.assert_query_result(\n            r'''\n                SELECT Card {\n                    owners: {\n                        name\n                    } ORDER BY .name\n                }\n                FILTER .name = 'Djinn';\n            ''',\n            [{\n                'owners': [\n                    {'name': 'Carol'},\n                    {'name': 'Dave'}\n                ]\n            }]\n        )\n\n    async def test_edgeql_computable_link_02(self):\n        await self.assert_query_result(\n            r'''\n                SELECT User {\n                    name,\n                    deck_cost\n                }\n                ORDER BY User.name;\n            ''',\n            [\n                {\n                    'name': 'Alice',\n                    'deck_cost': 11\n                },\n                {\n                    'name': 'Bob',\n                    'deck_cost': 9\n                },\n                {\n                    'name': 'Carol',\n                    'deck_cost': 16\n                },\n                {\n                    'name': 'Dave',\n                    'deck_cost': 20\n                }\n            ]\n        )\n\n    async def test_edgeql_computable_aliased_link_01(self):\n        await self.assert_query_result(\n            r'''\n                SELECT AliasedFriends {\n                    my_name,\n                    my_friends: {\n                        @nickname\n                    } ORDER BY .name\n                }\n                FILTER .name = 'Alice';\n            ''',\n            [{\n                'my_name': 'Alice',\n                'my_friends': [\n                    {\n                        '@nickname': 'Swampy'\n                    },\n                    {\n                        '@nickname': 'Firefighter'\n                    },\n                    {\n                        '@nickname': 'Grumpy'\n                    },\n                ]\n            }]\n        )\n\n    async def test_edgeql_computable_nested_01(self):\n        await self.assert_query_result(\n            r'''\n                SELECT Card {\n                    name,\n                    owned := (\n                        WITH O := Card.<deck[IS User]\n                        SELECT O {\n                            name,\n                            # simple computable\n                            fr0 := count(O.friends),\n                            # computable with an alias defined\n                            fr1 := (WITH F := O.friends SELECT count(F)),\n                        }\n                        ORDER BY .name\n                    )\n                } FILTER .name = 'Giant turtle';\n            ''',\n            [{\n                'name': 'Giant turtle',\n                'owned': [\n                    {'fr0': 3, 'fr1': 3, 'name': 'Alice'},\n                    {'fr0': 0, 'fr1': 0, 'name': 'Bob'},\n                    {'fr0': 0, 'fr1': 0, 'name': 'Carol'},\n                    {'fr0': 1, 'fr1': 1, 'name': 'Dave'},\n                ]\n            }]\n        )\n\n    async def test_edgeql_computable_nested_02(self):\n        await self.assert_query_result(\n            r'''\n                WITH C := Card { ava_owners := .<avatar }\n                SELECT C {\n                    name,\n                    ava_owners: {\n                        typename := (\n                            WITH name := C.ava_owners.__type__.name\n                            SELECT name\n                        )\n                    }\n                }\n                FILTER EXISTS .ava_owners\n                ORDER BY .name\n            ''',\n            [{\n                'name': 'Djinn',\n                'ava_owners': [{\n                    'typename': 'default::Bot'\n                }],\n            }, {\n                'name': 'Dragon',\n                'ava_owners': [{\n                    'typename': 'default::User'\n                }],\n            }]\n        )\n\n    async def test_edgeql_computable_nested_03(self):\n        # This SHOULD be identical to the previous test case, except\n        # for the cardinality being forced to be MULTI.\n        await self.assert_query_result(\n            r'''\n                WITH C := Card { ava_owners := .<avatar }\n                SELECT C {\n                    name,\n                    ava_owners: {\n                        multi typename := (\n                            WITH name := C.ava_owners.__type__.name\n                            SELECT name\n                        )\n                    }\n                }\n                FILTER EXISTS .ava_owners\n                ORDER BY .name;\n            ''',\n            [{\n                'name': 'Djinn',\n                'ava_owners': [{\n                    'typename': {'default::Bot'}\n                }],\n            }, {\n                'name': 'Dragon',\n                'ava_owners': [{\n                    'typename': {'default::User'}\n                }],\n            }]\n        )\n\n    async def test_edgeql_aliases_shape_propagation_01(self):\n        await self.assert_query_result(\n            r'''\n                SELECT _ := {\n                    (SELECT User FILTER .name = 'Alice').deck,\n                    (SELECT User FILTER .name = 'Bob').deck\n                } {name}\n                ORDER BY _.name;\n            ''',\n            [\n                {'name': 'Bog monster'},\n                {'name': 'Bog monster'},\n                {'name': 'Dragon'},\n                {'name': 'Dwarf'},\n                {'name': 'Giant turtle'},\n                {'name': 'Giant turtle'},\n                {'name': 'Golem'},\n                {'name': 'Imp'},\n            ],\n        )\n\n    async def test_edgeql_aliases_shape_propagation_02(self):\n        await self.assert_query_result(\n            r'''\n                # the alias should be propagated through _ := DISTINCT since it\n                # maps `any` to `any`\n                SELECT _ := DISTINCT {\n                    (SELECT User FILTER .name = 'Alice').deck,\n                    (SELECT User FILTER .name = 'Bob').deck\n                } {name}\n                ORDER BY _.name;\n            ''',\n            [\n                {'name': 'Bog monster'},\n                {'name': 'Dragon'},\n                {'name': 'Dwarf'},\n                {'name': 'Giant turtle'},\n                {'name': 'Golem'},\n                {'name': 'Imp'},\n            ],\n        )\n\n    async def test_edgeql_aliases_shape_propagation_03(self):\n        await self.assert_query_result(\n            r'''\n                # the alias should be propagated through _ := DETACHED\n                SELECT _ := DETACHED {\n                    (SELECT User FILTER .name = 'Alice').deck,\n                    (SELECT User FILTER .name = 'Bob').deck\n                } {name}\n                ORDER BY _.name;\n            ''',\n            [\n                {'name': 'Bog monster'},\n                {'name': 'Bog monster'},\n                {'name': 'Dragon'},\n                {'name': 'Dwarf'},\n                {'name': 'Giant turtle'},\n                {'name': 'Giant turtle'},\n                {'name': 'Golem'},\n                {'name': 'Imp'},\n            ],\n        )\n\n    async def test_edgeql_aliases_shape_propagation_04(self):\n        await self.assert_query_result(\n            r'''\n                # the alias should be propagated through _ := DETACHED\n                SELECT _ := DETACHED ({\n                    (SELECT User FILTER .name = 'Alice').deck,\n                    (SELECT User FILTER .name = 'Bob').deck\n                } {name})\n                ORDER BY _.name;\n            ''',\n            [\n                {'name': 'Bog monster'},\n                {'name': 'Bog monster'},\n                {'name': 'Dragon'},\n                {'name': 'Dwarf'},\n                {'name': 'Giant turtle'},\n                {'name': 'Giant turtle'},\n                {'name': 'Golem'},\n                {'name': 'Imp'},\n            ],\n        )\n\n    async def test_edgeql_aliases_if_else_01(self):\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT\n                    _ := 'yes' IF Card.cost > 4 ELSE 'no'\n                ORDER BY _;\n            \"\"\",\n            ['no', 'no', 'no', 'no', 'no', 'no', 'no', 'no', 'yes'],\n        )\n\n    @test.xerror(\n        \"Known collation issue on Heroku Postgres\",\n        unless=os.getenv(\"EDGEDB_TEST_BACKEND_VENDOR\") != \"heroku-postgres\"\n    )\n    async def test_edgeql_aliases_if_else_02(self):\n        await self.assert_query_result(\n            r\"\"\"\n                # working with singletons\n                SELECT\n                    _ := (\n                      for u in User\n                      select 'ok' IF u.deck_cost < 19 ELSE u.deck.name\n                    )\n                ORDER BY _;\n            \"\"\",\n            [\n                'Bog monster',\n                'Djinn',\n                'Dragon',\n                'Giant eagle',\n                'Giant turtle',\n                'Golem',\n                'Sprite',\n                'ok',\n                'ok',\n                'ok',\n            ],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                # either result is a set, but the condition is a singleton\n                SELECT\n                    _ := (\n                        for u in User\n                         select u.deck.element IF u.deck_cost < 19\n                                ELSE u.deck.name\n                    )\n                ORDER BY _;\n            \"\"\",\n            [\n                'Air',\n                'Air',\n                'Air',\n                'Bog monster',\n                'Djinn',\n                'Dragon',\n                'Earth',\n                'Earth',\n                'Earth',\n                'Earth',\n                'Fire',\n                'Fire',\n                'Giant eagle',\n                'Giant turtle',\n                'Golem',\n                'Sprite',\n                'Water',\n                'Water',\n                'Water',\n                'Water',\n                'Water',\n                'Water',\n            ],\n        )\n\n    async def test_edgeql_aliases_if_else_03(self):\n        res = [\n            ['Air', 'Air', 'Air', 'Earth', 'Earth', 'Fire', 'Fire', 'Water',\n             'Water'],\n            ['1', '1', '1', '2', '2', '3', '3', '4', '5'],\n            [False, False, False, True, True],\n        ]\n\n        await self.assert_query_result(\n            r\"\"\"\n                # get the data that this test relies upon in a format\n                # that's easy to analyze\n                SELECT _ := User.deck.element\n                ORDER BY _;\n            \"\"\",\n            res[0]\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT _ := <str>User.deck.cost\n                ORDER BY _;\n            \"\"\",\n            res[1]\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT _ := {User.name[0] = 'A', EXISTS User.friends}\n                ORDER BY _;\n            \"\"\",\n            res[2]\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                # results and conditions are sets\n                SELECT _ :=\n                    User.deck.element\n                    # because the elements of {} are treated as SET OF,\n                    # all of the paths in this expression are independent sets\n                    IF {User.name[0] = 'A', EXISTS User.friends} ELSE\n                    <str>User.deck.cost\n                ORDER BY _;\n            \"\"\",\n            sorted(res[1] + res[1] + res[1] + res[0] + res[0]),\n        )\n\n    async def test_edgeql_aliases_if_else_04(self):\n        await self.assert_query_result(\n            r\"\"\"\n                FOR User in User\n                SELECT\n                    1   IF User.name[0] = 'A' ELSE\n                    10  IF User.name[0] = 'B' ELSE\n                    100 IF User.name[0] = 'C' ELSE\n                    0;\n            \"\"\",\n            {1, 10, 100, 0},\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                FOR User in User\n                SELECT (\n                    User.name,\n                    sum((\n                        FOR f in User.friends SELECT\n                        1   IF f.name[0] = 'A' ELSE\n                        10  IF f.name[0] = 'B' ELSE\n                        100 IF f.name[0] = 'C' ELSE\n                        0\n                    )),\n                ) ORDER BY .0;\n            \"\"\",\n            [['Alice', 110], ['Bob', 0], ['Carol', 0], ['Dave', 10]],\n        )\n\n    async def test_edgeql_aliases_if_else_05(self):\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT (\n                FOR Card in Card\n                SELECT\n                    (Card.name, 'yes' IF Card.cost > 4 ELSE 'no')\n                )\n                ORDER BY .0;\n            \"\"\",\n            [\n                ['Bog monster', 'no'],\n                ['Djinn', 'no'],\n                ['Dragon', 'yes'],\n                ['Dwarf', 'no'],\n                ['Giant eagle', 'no'],\n                ['Giant turtle', 'no'],\n                ['Golem', 'no'],\n                ['Imp', 'no'],\n                ['Sprite', 'no'],\n            ],\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT (\n                FOR Card in Card\n                SELECT\n                    (Card.name, 'yes') IF Card.cost > 4 ELSE (Card.name, 'no')\n                )\n                ORDER BY .0;\n            \"\"\",\n            [\n                ['Bog monster', 'no'],\n                ['Djinn', 'no'],\n                ['Dragon', 'yes'],\n                ['Dwarf', 'no'],\n                ['Giant eagle', 'no'],\n                ['Giant turtle', 'no'],\n                ['Golem', 'no'],\n                ['Imp', 'no'],\n                ['Sprite', 'no'],\n            ],\n        )\n\n    async def test_edgeql_aliases_nested_01(self):\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT AwardAlias {\n                    name,\n                    winner: {\n                        name\n                    }\n                } ORDER BY .name;\n            \"\"\",\n            [\n                {'name': '1st', 'winner': {'name': 'Alice'}},\n                {'name': '2nd', 'winner': {'name': 'Alice'}},\n                {'name': '3rd', 'winner': {'name': 'Bob'}},\n            ],\n        )\n\n    async def test_edgeql_aliases_nested_02(self):\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT {\n                    foo := (\n                        SELECT AwardAlias {\n                            name,\n                            winner: {\n                                name\n                            }\n                        } ORDER BY .name\n                    )\n                };\n            \"\"\",\n            [\n                {\n                    'foo': [\n                        {'name': '1st', 'winner': {'name': 'Alice'}},\n                        {'name': '2nd', 'winner': {'name': 'Alice'}},\n                        {'name': '3rd', 'winner': {'name': 'Bob'}},\n                    ]\n                }\n            ],\n        )\n\n    async def test_edgeql_aliases_nested_03(self):\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT AwardAlias {\n                    winner: {\n                        name_upper\n                    }\n                }\n                FILTER\n                    .winner.name_upper = 'ALICE';\n            \"\"\",\n            [\n                {'winner': {'name_upper': 'ALICE'}},\n                {'winner': {'name_upper': 'ALICE'}},\n            ],\n        )\n\n    async def test_edgeql_aliases_deep_01(self):\n        # fetch the result we will compare to\n        res = await self.con.query_json(r\"\"\"\n            SELECT AwardAlias {\n                winner: {\n                    deck: {\n                        owners\n                    }\n                }\n            }\n            FILTER .name = '1st'\n            LIMIT 1;\n        \"\"\")\n        res = json.loads(res)\n\n        # fetch the same data via a different alias, that should be\n        # functionally identical\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT AwardAlias2 {\n                    winner: {\n                        deck: {\n                            owners\n                        }\n                    }\n                }\n                FILTER .name = '1st';\n            \"\"\",\n            res\n        )\n\n    async def test_edgeql_aliases_clauses_01(self):\n        # fetch the result we will compare to\n        res = await self.con.query_json(r\"\"\"\n            SELECT User {\n                deck: {\n                    id\n                } ORDER BY User.deck.cost DESC\n                  LIMIT 1,\n            }\n            FILTER .name = 'Alice';\n        \"\"\")\n        res = json.loads(res)\n\n        # fetch the same data via an alias, that should be\n        # functionally identical\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT UserAlias {\n                    deck,\n                }\n                FILTER .name = 'Alice';\n            \"\"\",\n            res\n        )\n\n    async def test_edgeql_aliases_limit_01(self):\n        # Test interaction of aliases and the LIMIT clause\n        await self.con.execute(\"\"\"\n            CREATE ALIAS FirstUser := (\n                SELECT User {\n                    name_upper := str_upper(User.name)\n                }\n                ORDER BY .name\n                LIMIT 1\n            );\n        \"\"\")\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT FirstUser {\n                    name_upper,\n                }\n            \"\"\",\n            [\n                {\n                    'name_upper': 'ALICE',\n                },\n            ],\n        )\n\n    async def test_edgeql_aliases_ignore_alias(self):\n        await self.con.execute('''\n\n            CREATE ALIAS UserAlias2 := (\n                SELECT User {\n                    deck: {\n                        id\n                    } ORDER BY User.deck.cost DESC\n                    LIMIT 1,\n                }\n            );\n        ''')\n\n        # Explicitly reset the default module alias to test\n        # that aliases don't care.\n        await self.con.execute('''\n            SET MODULE std;\n        ''')\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT default::UserAlias2 {\n                    deck,\n                }\n                FILTER .name = 'Alice';\n            \"\"\",\n            [{\n                'deck': [\n                    {}\n                ]\n            }]\n        )\n\n    async def test_edgeql_aliases_esdl_01(self):\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT WaterOrEarthCard {\n                    name,\n                    owned_by_alice,\n                }\n                FILTER any(.name ILIKE {'%turtle%', 'dwarf'})\n                ORDER BY .name;\n            \"\"\",\n            [\n                {\n                    'name': 'Dwarf',\n                    'owned_by_alice': True,\n                },\n                {\n                    'name': 'Giant turtle',\n                    'owned_by_alice': True,\n                },\n            ]\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT EarthOrFireCard {\n                    name,\n                }\n                FILTER .name IN {'Imp', 'Dwarf'}\n                ORDER BY .name;\n            \"\"\",\n            [\n                {\n                    'name': 'Dwarf'\n                },\n                {\n                    'name': 'Imp'\n                },\n            ]\n        )\n\n    async def test_edgeql_aliases_collection_01(self):\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT SpecialCardAlias {\n                    name,\n                    el_cost,\n                };\n            \"\"\",\n            [\n                {\n                    'name': 'Djinn',\n                    'el_cost': ['Air', 4],\n                },\n            ]\n        )\n\n    async def test_edgeql_aliases_collection_02(self):\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT SpecialCardAlias.el_cost;\n            \"\"\",\n            [\n                ['Air', 4],\n            ]\n        )\n\n    async def test_edgeql_aliases_collection_03(self):\n        await self.assert_query_result(\n            r\"\"\"\n                WITH\n                    X := SpecialCard {\n                        el_cost := (.element, .cost)\n                    }\n                SELECT X.el_cost;\n            \"\"\",\n            [\n                ['Air', 4],\n            ]\n        )\n\n    async def test_edgeql_aliases_collection_04(self):\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT (\n                    SpecialCard {\n                        el_cost := (.element,)\n                    }\n                ).el_cost;\n            \"\"\",\n            [\n                ['Air'],\n            ]\n        )\n\n    async def test_edgeql_aliases_collection_05(self):\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT (\n                    SpecialCard {\n                        el_cost := [.element]\n                    }\n                ).el_cost;\n            \"\"\",\n            [\n                ['Air'],\n            ]\n        )\n\n    async def test_edgeql_aliases_subqueries_01(self):\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT count((\n                    (SELECT EarthOrFireCard.name),\n                    (EarthOrFireCard.name)\n                ))\n            \"\"\",\n            [16]\n        )\n\n    async def test_edgeql_aliases_subqueries_02(self):\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT count((\n                    (EarthOrFireCard.name),\n                    (SELECT EarthOrFireCard.name)\n                ))\n            \"\"\",\n            [16]\n        )\n\n    async def test_edgeql_aliases_subqueries_03(self):\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT count((\n                    (EarthOrFireCard.name),\n                    (EarthOrFireCard.name)\n                ))\n            \"\"\",\n            [16]\n        )\n\n    async def test_edgeql_aliases_subqueries_04(self):\n        await self.assert_query_result(\n            r\"\"\"\n                SELECT count((\n                    (SELECT EarthOrFireCard.name),\n                    (SELECT EarthOrFireCard.name)\n                ))\n            \"\"\",\n            [16]\n        )\n\n    async def test_edgeql_aliases_introspection(self):\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT Type {\n                    name\n                }\n                FILTER .from_alias AND .name LIKE 'default::Air%'\n                ORDER BY .name\n            \"\"\",\n            [{\n                'name': 'default::AirCard',\n            }]\n        )\n\n        await self.con.execute('''\n            CREATE ALIAS tuple_alias := ('foo', 10);\n        ''')\n\n        await self.assert_query_result(\n            r\"\"\"\n                WITH MODULE schema\n                SELECT Tuple {\n                    name,\n                    element_types: {\n                        name := .type.name\n                    } ORDER BY @index\n                }\n                FILTER\n                    .from_alias\n                    AND .name = 'default::tuple_alias'\n                ORDER BY .name\n            \"\"\",\n            [{\n                'name': 'default::tuple_alias',\n                'element_types': [{\n                    'name': 'std::str',\n                }, {\n                    'name': 'std::int64',\n                }]\n            }]\n        )\n\n        await self.assert_query_result(\n            r\"\"\"\n                select schema::Pointer {name, target: {from_alias}}\n                filter .name = 'winner'\n                and .source.name = 'default::AwardAlias'\n            \"\"\",\n            [{\"name\": \"winner\", \"target\": {\"from_alias\": True}}]\n        )\n\n    async def test_edgeql_aliases_backlinks_01(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidReferenceError,\n            \"cannot follow backlink 'owners'\",\n        ):\n            await self.con.execute(\"\"\"\n                SELECT User.<owners[Is Card];\n            \"\"\")\n\n    async def test_edgeql_aliases_backlinks_02(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidReferenceError,\n            \"cannot follow backlink 'owners'\",\n        ):\n            await self.con.execute(\"\"\"\n                SELECT User.<owners;\n            \"\"\")\n\n    async def test_edgeql_aliases_helper_01(self):\n        async with self.assertRaisesRegexTx(\n            edgedb.InvalidReferenceError,\n            \"cannot refer to alias link helper type \"\n            \"'default::__AwardAlias2__winner'\",\n        ):\n            await self.con.execute(\"\"\"\n                SELECT __AwardAlias2__winner\n            \"\"\")\n\n    async def test_edgeql_aliases_detached_01(self):\n        await self.assert_query_result(\n            r\"\"\"\n                select count((detached FireCard, detached FireCard))\n            \"\"\",\n            [4]\n        )\n\n    async def test_edgeql_aliases_coll_types_01(self):\n        await self.con.execute(\n            r\"\"\"\n                create type X;\n                create global y := (select\n                    (a := 'hello', b := [(select X limit 1)])\n                );\n                create alias z := (\n                   a := 'hello', b := [(select X limit 1)]\n                );\n            \"\"\"\n        )\n\n    async def test_edgeql_aliases_schema_types_01(self):\n        # Scalar alias adds a type\n        await self.con.execute('''\n            create alias best_card := 'Dragon';\n        ''')\n        await self.assert_query_result(\n            r'''\n            with module schema select Type { name }\n            filter .name ilike \"%best_card%\";\n            ''',\n            [{'name': 'default::best_card'}]\n        )\n\n        await self.con.execute('''\n            drop alias best_card;\n        ''')\n        await self.assert_query_result(\n            r'''\n            with module schema select Type { name }\n            filter .name ilike \"%best_card%\";\n            ''',\n            []\n        )\n\n        await self.con.execute('''\n            create module my_mod;\n            create alias my_mod::best_card := 'Dragon';\n        ''')\n        await self.assert_query_result(\n            r'''\n            with module schema select Type { name }\n            filter .name ilike \"%best_card%\";\n            ''',\n            [{'name': 'my_mod::best_card'}]\n        )\n\n        await self.con.execute('''\n            drop alias my_mod::best_card;\n        ''')\n        await self.assert_query_result(\n            r'''\n            with module schema select Type { name }\n            filter .name ilike \"%best_card%\";\n            ''',\n            []\n        )\n\n    async def test_edgeql_aliases_schema_types_02(self):\n        # Object alias adds a type\n        await self.con.execute('''\n            create alias best_card := (\n                select Card filter .name = 'Dragon' limit 1\n            );\n        ''')\n        await self.assert_query_result(\n            r'''\n            with module schema select Type { name }\n            filter .name ilike \"%best_card%\";\n            ''',\n            [{'name': 'default::best_card'}]\n        )\n\n        await self.con.execute('''\n            drop alias best_card;\n        ''')\n        await self.assert_query_result(\n            r'''\n            with module schema select Type { name }\n            filter .name ilike \"%best_card%\";\n            ''',\n            []\n        )\n\n        await self.con.execute('''\n            create module my_mod;\n            create alias my_mod::best_card := (\n                select Card filter .name = 'Dragon' limit 1\n            );\n        ''')\n        await self.assert_query_result(\n            r'''\n            with module schema select Type { name }\n            filter .name ilike \"%best_card%\";\n            ''',\n            [{'name': 'my_mod::best_card'}]\n        )\n\n        await self.con.execute('''\n            drop alias my_mod::best_card;\n        ''')\n        await self.assert_query_result(\n            r'''\n            with module schema select Type { name }\n            filter .name ilike \"%best_card%\";\n            ''',\n            []\n        )\n\n    async def test_edgeql_aliases_schema_types_03(self):\n        # Object alias with shape adds two types:\n        # - one for the alias\n        # - one for the shape\n        await self.con.execute('''\n            create alias best_card := (\n                select Card {name}\n                filter .name = 'Dragon' limit 1\n            );\n        ''')\n        await self.assert_query_result(\n            r'''\n            with module schema select Type { name }\n            filter .name ilike \"%best_card%\"\n            order by .name;\n            ''',\n            [\n                {'name': 'default::__best_card__Card'},\n                {'name': 'default::best_card'},\n            ]\n        )\n\n        await self.con.execute('''\n            drop alias best_card;\n        ''')\n        await self.assert_query_result(\n            r'''\n            with module schema select Type { name }\n            filter .name ilike \"%best_card%\";\n            ''',\n            []\n        )\n\n        await self.con.execute('''\n            create module my_mod;\n            create alias my_mod::best_card := (\n                select Card {name}\n                filter .name = 'Dragon' limit 1\n            );\n        ''')\n        await self.assert_query_result(\n            r'''\n            with module schema select Type { name }\n            filter .name ilike \"%best_card%\"\n            order by .name;\n            ''',\n            [\n                {'name': 'my_mod::__best_card__Card'},\n                {'name': 'my_mod::best_card'},\n            ]\n        )\n\n        await self.con.execute('''\n            drop alias my_mod::best_card;\n        ''')\n        await self.assert_query_result(\n            r'''\n            with module schema select Type { name }\n            filter .name ilike \"%best_card%\";\n            ''',\n            []\n        )\n\n    async def test_edgeql_aliases_array_of_array_01(self):\n        await self.assert_query_result(\n            r\"\"\"\n                select AliasArrayOfArrayOfScalar;\n            \"\"\",\n            [\n                [[1, 2, 3], [4, 5, 6]],\n            ],\n        )\n\n    async def test_edgeql_aliases_array_of_array_02(self):\n        await self.assert_query_result(\n            r\"\"\"\n                select array_agg((\n                    for card_group in array_unpack(AliasCardsByCost)\n                        select array_agg((\n                            for card in array_unpack(card_group)\n                                select card.name\n                        ))\n                ))\n            \"\"\",\n            [\n                [\n                    tb.bag([]),\n                    tb.bag(['Imp', 'Dwarf', 'Sprite']),\n                    tb.bag(['Bog monster', 'Giant eagle']),\n                    tb.bag(['Giant turtle', 'Golem']),\n                    tb.bag(['Djinn']),\n                    tb.bag(['Dragon']),\n                ],\n            ],\n        )\n"
  }
]